diff --git a/.github/assets/bongocat.gif b/.github/assets/bongocat.gif new file mode 100644 index 000000000..f960fecd7 Binary files /dev/null and b/.github/assets/bongocat.gif differ diff --git a/.github/assets/dino.png b/.github/assets/dino.png new file mode 100644 index 000000000..b5bc7bb3b Binary files /dev/null and b/.github/assets/dino.png differ diff --git a/.github/assets/kurukuru.gif b/.github/assets/kurukuru.gif new file mode 100644 index 000000000..38d203d9a Binary files /dev/null and b/.github/assets/kurukuru.gif differ diff --git a/.github/assets/logo.svg b/.github/assets/logo.svg new file mode 100644 index 000000000..6879c92b9 --- /dev/null +++ b/.github/assets/logo.svg @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + diff --git a/.github/assets/pam.d/fprint b/.github/assets/pam.d/fprint new file mode 100644 index 000000000..d4814e946 --- /dev/null +++ b/.github/assets/pam.d/fprint @@ -0,0 +1,3 @@ +#%PAM-1.0 + +auth required pam_fprintd.so max-tries=1 diff --git a/.github/assets/pam.d/passwd b/.github/assets/pam.d/passwd new file mode 100644 index 000000000..4b1406432 --- /dev/null +++ b/.github/assets/pam.d/passwd @@ -0,0 +1,6 @@ +#%PAM-1.0 + +auth required pam_faillock.so preauth +auth [success=1 default=bad] pam_unix.so nullok +auth [default=die] pam_faillock.so authfail +auth required pam_faillock.so authsucc diff --git a/.github/assets/shaders/opacitymask.frag b/.github/assets/shaders/opacitymask.frag new file mode 100644 index 000000000..94a80b8a9 --- /dev/null +++ b/.github/assets/shaders/opacitymask.frag @@ -0,0 +1,19 @@ +#version 440 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + // qt_Matrix and qt_Opacity must always be both present + // if the built-in vertex shader is used. + mat4 qt_Matrix; + float qt_Opacity; +}; + +layout(binding = 1) uniform sampler2D source; +layout(binding = 2) uniform sampler2D maskSource; + +void main() +{ + fragColor = texture(source, qt_TexCoord0.st) * (texture(maskSource, qt_TexCoord0.st).a) * qt_Opacity; +} diff --git a/.github/assets/shaders/opacitymask.frag.qsb b/.github/assets/shaders/opacitymask.frag.qsb new file mode 100644 index 000000000..7bf97c280 Binary files /dev/null and b/.github/assets/shaders/opacitymask.frag.qsb differ diff --git a/.github/assets/wrap_term_launch.sh b/.github/assets/wrap_term_launch.sh new file mode 100644 index 000000000..caf60c74f --- /dev/null +++ b/.github/assets/wrap_term_launch.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env sh + +cat ~/.local/state/caelestia/sequences.txt 2>/dev/null + +exec "$@" diff --git a/.github/components/Anim.qml b/.github/components/Anim.qml new file mode 100644 index 000000000..6883a7984 --- /dev/null +++ b/.github/components/Anim.qml @@ -0,0 +1,8 @@ +import qs.config +import QtQuick + +NumberAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard +} diff --git a/.github/components/CAnim.qml b/.github/components/CAnim.qml new file mode 100644 index 000000000..49484b789 --- /dev/null +++ b/.github/components/CAnim.qml @@ -0,0 +1,8 @@ +import qs.config +import QtQuick + +ColorAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard +} diff --git a/.github/components/CategoryNavbar.qml b/.github/components/CategoryNavbar.qml new file mode 100644 index 000000000..a3e813b5b --- /dev/null +++ b/.github/components/CategoryNavbar.qml @@ -0,0 +1,230 @@ +pragma ComponentBehavior: Bound + +import qs.components +import qs.components.controls +import qs.components.containers +import qs.services +import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + required property var categories + required property string activeCategory + property bool showScrollButtons: true + property bool showExtraContent: false + property Component extraContent: null + + signal categoryChanged(string categoryId) + + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.normal + + visible: opacity > 0 + implicitHeight: tabsRow.height + Appearance.padding.small + Appearance.padding.normal + clip: true + + function scrollToActiveTab(): void { + Qt.callLater(() => { + if (!tabsFlickable || !tabsRow) + return; + + const currentIndex = root.categories.findIndex(cat => cat.id === root.activeCategory); + if (currentIndex === -1) + return; + + let tabX = 0; + for (let i = 0; i < currentIndex && i < tabsRow.children.length; i++) { + const child = tabsRow.children[i]; + if (child) { + tabX += child.width + tabsRow.spacing; + } + } + + const activeTab = tabsRow.children[currentIndex]; + if (!activeTab) + return; + + const tabWidth = activeTab.width; + const viewportStart = tabsFlickable.contentX; + const viewportEnd = tabsFlickable.contentX + tabsFlickable.width; + + if (tabX < viewportStart) { + tabsFlickable.contentX = tabX; + } else if (tabX + tabWidth > viewportEnd) { + tabsFlickable.contentX = Math.min(tabsFlickable.contentWidth - tabsFlickable.width, tabX + tabWidth - tabsFlickable.width); + } + }); + } + + RowLayout { + id: tabsContent + anchors.fill: parent + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + anchors.topMargin: Appearance.padding.small + anchors.bottomMargin: Appearance.padding.smaller + spacing: Appearance.spacing.smaller + + IconButton { + icon: "chevron_left" + visible: root.showScrollButtons && tabsFlickable.contentWidth > tabsFlickable.width + type: IconButton.Text + radius: Appearance.rounding.small + padding: Appearance.padding.small + onClicked: { + tabsFlickable.contentX = Math.max(0, tabsFlickable.contentX - 100); + } + } + + StyledFlickable { + id: tabsFlickable + Layout.fillWidth: true + Layout.preferredHeight: tabsRow.height + flickableDirection: Flickable.HorizontalFlick + contentWidth: tabsRow.width + clip: true + + Behavior on contentX { + Anim { + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + + MouseArea { + anchors.fill: parent + propagateComposedEvents: true + + onWheel: wheel => { + const delta = wheel.angleDelta.y || wheel.angleDelta.x; + tabsFlickable.contentX = Math.max(0, Math.min(tabsFlickable.contentWidth - tabsFlickable.width, tabsFlickable.contentX - delta)); + wheel.accepted = true; + } + + onPressed: mouse => { + mouse.accepted = false; + } + } + + Item { + implicitWidth: tabsRow.width + implicitHeight: tabsRow.height + + StyledRect { + id: activeIndicator + + property Item activeTab: { + for (let i = 0; i < tabsRepeater.count; i++) { + const tab = tabsRepeater.itemAt(i); + if (tab && tab.isActive) { + return tab; + } + } + return null; + } + + visible: activeTab !== null + color: Colours.palette.m3primary + radius: 10 + + x: activeTab ? activeTab.x : 0 + y: activeTab ? activeTab.y : 0 + width: activeTab ? activeTab.width : 0 + height: activeTab ? activeTab.height : 0 + + Behavior on x { + Anim { + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + + Behavior on width { + Anim { + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + } + + Row { + id: tabsRow + spacing: Appearance.spacing.small + + Repeater { + id: tabsRepeater + model: root.categories + + delegate: Item { + required property var modelData + required property int index + + property bool isActive: root.activeCategory === modelData.id + + implicitWidth: tabContent.width + Appearance.padding.normal * 2 + implicitHeight: tabContent.height + Appearance.padding.smaller * 2 + + StateLayer { + anchors.fill: parent + radius: 6 + function onClicked(): void { + root.categoryChanged(modelData.id); + + const tabLeft = parent.x; + const tabRight = parent.x + parent.width; + const viewLeft = tabsFlickable.contentX; + const viewRight = tabsFlickable.contentX + tabsFlickable.width; + + const targetX = tabLeft - (tabsFlickable.width - parent.width) / 2; + + tabsFlickable.contentX = Math.max(0, Math.min(tabsFlickable.contentWidth - tabsFlickable.width, targetX)); + } + } + + Row { + id: tabContent + anchors.centerIn: parent + spacing: Appearance.spacing.smaller + + MaterialIcon { + anchors.verticalCenter: parent.verticalCenter + text: modelData.icon + font.pointSize: Appearance.font.size.small + color: isActive ? Colours.palette.m3surface : Colours.palette.m3onSurfaceVariant + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: modelData.name + font.pointSize: Appearance.font.size.small + color: isActive ? Colours.palette.m3surface : Colours.palette.m3onSurfaceVariant + } + } + } + } + } + } + } + + IconButton { + icon: "chevron_right" + visible: root.showScrollButtons && tabsFlickable.contentWidth > tabsFlickable.width + type: IconButton.Text + radius: Appearance.rounding.small + padding: Appearance.padding.small + onClicked: { + tabsFlickable.contentX = Math.min(tabsFlickable.contentWidth - tabsFlickable.width, tabsFlickable.contentX + 100); + } + } + + Loader { + Layout.fillHeight: true + active: root.showExtraContent && root.extraContent !== null + sourceComponent: root.extraContent + } + } +} diff --git a/.github/components/ConnectionHeader.qml b/.github/components/ConnectionHeader.qml new file mode 100644 index 000000000..12b427648 --- /dev/null +++ b/.github/components/ConnectionHeader.qml @@ -0,0 +1,31 @@ +import qs.components +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property string icon + required property string title + + spacing: Appearance.spacing.normal + Layout.alignment: Qt.AlignHCenter + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + animate: true + text: root.icon + font.pointSize: Appearance.font.size.extraLarge * 3 + font.bold: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + animate: true + text: root.title + font.pointSize: Appearance.font.size.large + font.bold: true + } +} diff --git a/.github/components/ConnectionInfoSection.qml b/.github/components/ConnectionInfoSection.qml new file mode 100644 index 000000000..927ef287d --- /dev/null +++ b/.github/components/ConnectionInfoSection.qml @@ -0,0 +1,59 @@ +import qs.components +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property var deviceDetails + + spacing: Appearance.spacing.small / 2 + + StyledText { + text: qsTr("IP Address") + } + + StyledText { + text: root.deviceDetails?.ipAddress || qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Subnet Mask") + } + + StyledText { + text: root.deviceDetails?.subnet || qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("Gateway") + } + + StyledText { + text: root.deviceDetails?.gateway || qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + text: qsTr("DNS Servers") + } + + StyledText { + text: (root.deviceDetails && root.deviceDetails.dns && root.deviceDetails.dns.length > 0) ? root.deviceDetails.dns.join(", ") : qsTr("Not available") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + wrapMode: Text.Wrap + Layout.maximumWidth: parent.width + } +} diff --git a/.github/components/Logo.qml b/.github/components/Logo.qml new file mode 100644 index 000000000..7cd41e17f --- /dev/null +++ b/.github/components/Logo.qml @@ -0,0 +1,70 @@ +import QtQuick +import QtQuick.Shapes +import qs.services + +Item { + id: root + + readonly property real designWidth: 128 + readonly property real designHeight: 90.38 + + property color topColour: Colours.palette.m3primary + property color bottomColour: Colours.palette.m3onSurface + + implicitWidth: designWidth + implicitHeight: designHeight + + Shape { + anchors.centerIn: parent + width: root.designWidth + height: root.designHeight + scale: Math.min(root.width / width, root.height / height) + transformOrigin: Item.Center + preferredRendererType: Shape.CurveRenderer + + ShapePath { + fillColor: root.topColour + strokeColor: "transparent" + + PathSvg { + path: "m42.56,42.96c-7.76,1.6-16.36,4.22-22.44,6.22-.49.16-.88-.44-.53-.82,5.37-5.85,9.66-13.3,9.66-13.3,8.66-14.67,22.97-23.51,39.85-21.14,6.47.91,12.33,3.38,17.26,6.98.99.72,1.14,2.14.31,3.04-.4.44-.95.67-1.51.67-.34,0-.69-.09-1-.26-3.21-1.84-6.82-2.69-10.71-3.24-13.1-1.84-25.41,4.75-31.06,15.83-.94,1.84-.61,3.81.45,5.21.22.3.07.72-.29.8Z" + } + } + + ShapePath { + fillColor: root.bottomColour + strokeColor: "transparent" + + PathSvg { + path: "m103.02,51.8c-.65.11-1.26-.37-1.28-1.03-.06-1.96.15-3.89-.2-5.78-.28-1.48-1.66-2.5-3.16-2.34h-.05c-6.53.73-24.63,3.1-48,9.32-6.89,1.83-9.83,10-5.67,15.79,4.62,6.44,11.84,10.93,20.41,12.13,11.82,1.66,22.99-3.36,29.21-12.65.54-.81,1.54-1.17,2.47-.86.91.3,1.47,1.15,1.47,2.04,0,.33-.08.66-.24.98-7.23,14.21-22.91,22.95-39.59,20.6-7.84-1.1-14.8-4.5-20.28-9.43,0,0,0,0-.02-.01-7.28-5.14-14.7-9.99-27.24-11.98-18.82-2.98-9.53-8.75.46-13.78,7.36-3.13,25.17-7.9,36.24-10.73.16-.03.31-.06.47-.1,1.52-.4,3.2-.83,5.02-1.29,1.06-.26,1.93-.48,2.58-.64.09-.02.18-.04.26-.06.31-.08.56-.14.73-.18.03,0,.06-.01.08-.02.03,0,.05-.01.07-.02.02,0,.04,0,.06-.01.01,0,.03,0,.04-.01,0,0,.02,0,.03,0,.01,0,.02,0,.02,0,10.62-2.58,24.63-5.62,37.74-7.34,1.02-.13,2.03-.26,3.03-.37,7.49-.87,14.58-1.26,20.42-.81,25.43,1.95-4.71,16.77-15.12,18.61Z" + } + } + + ShapePath { + fillColor: root.topColour + strokeColor: "transparent" + + PathSvg { + path: "m98.12.06c-.29,2.08-1.72,8.42-8.36,9.19-.09,0-.09.13,0,.14,6.64.78,8.07,7.11,8.36,9.19.01.08.13.08.14,0,.29-2.08,1.72-8.42,8.36-9.19.09,0,.09-.13,0-.14-6.64-.78-8.07-7.11-8.36-9.19-.01-.08-.13-.08-.14,0Z" + } + } + + ShapePath { + fillColor: root.topColour + strokeColor: "transparent" + + PathSvg { + path: "m113.36,15.5c-.22,1.29-1.08,4.35-4.38,4.87-.08.01-.08.13,0,.14,3.3.52,4.17,3.58,4.38,4.87.01.08.13.08.14,0,.22-1.29,1.08-4.35,4.38-4.87.08-.01.08-.13,0-.14-3.3-.52-4.17-3.58-4.38-4.87-.01-.08-.13-.08-.14,0Z" + } + } + + ShapePath { + fillColor: root.topColour + strokeColor: "transparent" + + PathSvg { + path: "m112.69,65.22c-.19,1.01-.86,3.15-3.2,3.57-.08.01-.08.13,0,.14,2.34.42,3.01,2.56,3.2,3.57.01.08.13.08.14,0,.19-1.01.86-3.15,3.2-3.57.08-.01.08-.13,0-.14-2.34-.42-3.01-2.56-3.2-3.57-.01-.08-.13-.08-.14,0Z" + } + } + } +} diff --git a/.github/components/MaterialIcon.qml b/.github/components/MaterialIcon.qml new file mode 100644 index 000000000..a1d19d3c0 --- /dev/null +++ b/.github/components/MaterialIcon.qml @@ -0,0 +1,16 @@ +import qs.services +import qs.config + +StyledText { + property real fill + property int grade: Colours.light ? 0 : -25 + + font.family: Appearance.font.family.material + font.pointSize: Appearance.font.size.larger + font.variableAxes: ({ + FILL: fill.toFixed(1), + GRAD: grade, + opsz: fontInfo.pixelSize, + wght: fontInfo.weight + }) +} diff --git a/.github/components/PropertyRow.qml b/.github/components/PropertyRow.qml new file mode 100644 index 000000000..640d5f743 --- /dev/null +++ b/.github/components/PropertyRow.qml @@ -0,0 +1,26 @@ +import qs.components +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property string label + required property string value + property bool showTopMargin: false + + spacing: Appearance.spacing.small / 2 + + StyledText { + Layout.topMargin: root.showTopMargin ? Appearance.spacing.normal : 0 + text: root.label + } + + StyledText { + text: root.value + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } +} diff --git a/.github/components/SectionContainer.qml b/.github/components/SectionContainer.qml new file mode 100644 index 000000000..2b653a5d9 --- /dev/null +++ b/.github/components/SectionContainer.qml @@ -0,0 +1,32 @@ +import qs.components +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + default property alias content: contentColumn.data + property real contentSpacing: Appearance.spacing.larger + property bool alignTop: false + + Layout.fillWidth: true + implicitHeight: contentColumn.implicitHeight + Appearance.padding.large * 2 + + radius: Appearance.rounding.normal + color: Colours.transparency.enabled ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : Colours.palette.m3surfaceContainerHigh + + ColumnLayout { + id: contentColumn + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: root.alignTop ? parent.top : undefined + anchors.verticalCenter: root.alignTop ? undefined : parent.verticalCenter + anchors.margins: Appearance.padding.large + + spacing: root.contentSpacing + } +} diff --git a/.github/components/SectionHeader.qml b/.github/components/SectionHeader.qml new file mode 100644 index 000000000..502e91895 --- /dev/null +++ b/.github/components/SectionHeader.qml @@ -0,0 +1,27 @@ +import qs.components +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property string title + property string description: "" + + spacing: 0 + + StyledText { + Layout.topMargin: Appearance.spacing.large + text: root.title + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + StyledText { + visible: root.description !== "" + text: root.description + color: Colours.palette.m3outline + } +} diff --git a/.github/components/StateLayer.qml b/.github/components/StateLayer.qml new file mode 100644 index 000000000..a20e26616 --- /dev/null +++ b/.github/components/StateLayer.qml @@ -0,0 +1,95 @@ +import qs.services +import qs.config +import QtQuick + +MouseArea { + id: root + + property bool disabled + property bool showHoverBackground: true + property color color: Colours.palette.m3onSurface + property real radius: parent?.radius ?? 0 + property alias rect: hoverLayer + + function onClicked(): void { + } + + anchors.fill: parent + + enabled: !disabled + cursorShape: disabled ? undefined : Qt.PointingHandCursor + hoverEnabled: true + + onPressed: event => { + if (disabled) + return; + + rippleAnim.x = event.x; + rippleAnim.y = event.y; + + const dist = (ox, oy) => ox * ox + oy * oy; + rippleAnim.radius = Math.sqrt(Math.max(dist(event.x, event.y), dist(event.x, height - event.y), dist(width - event.x, event.y), dist(width - event.x, height - event.y))); + + rippleAnim.restart(); + } + + onClicked: event => !disabled && onClicked(event) + + SequentialAnimation { + id: rippleAnim + + property real x + property real y + property real radius + + PropertyAction { + target: ripple + property: "x" + value: rippleAnim.x + } + PropertyAction { + target: ripple + property: "y" + value: rippleAnim.y + } + PropertyAction { + target: ripple + property: "opacity" + value: 0.08 + } + Anim { + target: ripple + properties: "implicitWidth,implicitHeight" + from: 0 + to: rippleAnim.radius * 2 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + Anim { + target: ripple + property: "opacity" + to: 0 + } + } + + StyledClippingRect { + id: hoverLayer + + anchors.fill: parent + + color: Qt.alpha(root.color, root.disabled ? 0 : root.pressed ? 0.12 : (root.showHoverBackground && root.containsMouse) ? 0.08 : 0) + radius: root.radius + + StyledRect { + id: ripple + + radius: Appearance.rounding.full + color: root.color + opacity: 0 + + transform: Translate { + x: -ripple.width / 2 + y: -ripple.height / 2 + } + } + } +} diff --git a/.github/components/StyledClippingRect.qml b/.github/components/StyledClippingRect.qml new file mode 100644 index 000000000..8f2630c13 --- /dev/null +++ b/.github/components/StyledClippingRect.qml @@ -0,0 +1,12 @@ +import Quickshell.Widgets +import QtQuick + +ClippingRectangle { + id: root + + color: "transparent" + + Behavior on color { + CAnim {} + } +} diff --git a/.github/components/StyledRect.qml b/.github/components/StyledRect.qml new file mode 100644 index 000000000..f5d514395 --- /dev/null +++ b/.github/components/StyledRect.qml @@ -0,0 +1,11 @@ +import QtQuick + +Rectangle { + id: root + + color: "transparent" + + Behavior on color { + CAnim {} + } +} diff --git a/.github/components/StyledText.qml b/.github/components/StyledText.qml new file mode 100644 index 000000000..ed961d26a --- /dev/null +++ b/.github/components/StyledText.qml @@ -0,0 +1,48 @@ +pragma ComponentBehavior: Bound + +import qs.services +import qs.config +import QtQuick + +Text { + id: root + + property bool animate: false + property string animateProp: "scale" + property real animateFrom: 0 + property real animateTo: 1 + property int animateDuration: Appearance.anim.durations.normal + + renderType: Text.NativeRendering + textFormat: Text.PlainText + color: Colours.palette.m3onSurface + font.family: Appearance.font.family.sans + font.pointSize: Appearance.font.size.smaller + + Behavior on color { + CAnim {} + } + + Behavior on text { + enabled: root.animate + + SequentialAnimation { + Anim { + to: root.animateFrom + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + PropertyAction {} + Anim { + to: root.animateTo + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + } + } + + component Anim: NumberAnimation { + target: root + property: root.animateProp + duration: root.animateDuration / 2 + easing.type: Easing.BezierSpline + } +} diff --git a/.github/components/containers/StyledFlickable.qml b/.github/components/containers/StyledFlickable.qml new file mode 100644 index 000000000..bc6ae0f62 --- /dev/null +++ b/.github/components/containers/StyledFlickable.qml @@ -0,0 +1,14 @@ +import ".." +import QtQuick + +Flickable { + id: root + + maximumFlickVelocity: 3000 + + rebound: Transition { + Anim { + properties: "x,y" + } + } +} diff --git a/.github/components/containers/StyledListView.qml b/.github/components/containers/StyledListView.qml new file mode 100644 index 000000000..626d20635 --- /dev/null +++ b/.github/components/containers/StyledListView.qml @@ -0,0 +1,14 @@ +import ".." +import QtQuick + +ListView { + id: root + + maximumFlickVelocity: 3000 + + rebound: Transition { + Anim { + properties: "x,y" + } + } +} diff --git a/.github/components/containers/StyledWindow.qml b/.github/components/containers/StyledWindow.qml new file mode 100644 index 000000000..8c6e39fc8 --- /dev/null +++ b/.github/components/containers/StyledWindow.qml @@ -0,0 +1,9 @@ +import Quickshell +import Quickshell.Wayland + +PanelWindow { + required property string name + + WlrLayershell.namespace: `caelestia-${name}` + color: "transparent" +} diff --git a/.github/components/controls/CircularIndicator.qml b/.github/components/controls/CircularIndicator.qml new file mode 100644 index 000000000..957899e5c --- /dev/null +++ b/.github/components/controls/CircularIndicator.qml @@ -0,0 +1,108 @@ +import ".." +import qs.services +import qs.config +import Caelestia.Internal +import QtQuick +import QtQuick.Templates + +BusyIndicator { + id: root + + enum AnimType { + Advance = 0, + Retreat + } + + enum AnimState { + Stopped, + Running, + Completing + } + + property real implicitSize: Appearance.font.size.normal * 3 + property real strokeWidth: Appearance.padding.small * 0.8 + property color fgColour: Colours.palette.m3primary + property color bgColour: Colours.palette.m3secondaryContainer + + property alias type: manager.indeterminateAnimationType + readonly property alias progress: manager.progress + + property real internalStrokeWidth: strokeWidth + property int animState + + padding: 0 + implicitWidth: implicitSize + implicitHeight: implicitSize + + Component.onCompleted: { + if (running) { + running = false; + running = true; + } + } + + onRunningChanged: { + if (running) { + manager.completeEndProgress = 0; + animState = CircularIndicator.Running; + } else { + if (animState == CircularIndicator.Running) + animState = CircularIndicator.Completing; + } + } + + states: State { + name: "stopped" + when: !root.running + + PropertyChanges { + root.opacity: 0 + root.internalStrokeWidth: root.strokeWidth / 3 + } + } + + transitions: Transition { + Anim { + properties: "opacity,internalStrokeWidth" + duration: manager.completeEndDuration * Appearance.anim.durations.scale + } + } + + contentItem: CircularProgress { + anchors.fill: parent + strokeWidth: root.internalStrokeWidth + fgColour: root.fgColour + bgColour: root.bgColour + padding: root.padding + rotation: manager.rotation + startAngle: manager.startFraction * 360 + value: manager.endFraction - manager.startFraction + } + + CircularIndicatorManager { + id: manager + } + + NumberAnimation { + running: root.animState !== CircularIndicator.Stopped + loops: Animation.Infinite + target: manager + property: "progress" + from: 0 + to: 1 + duration: manager.duration * Appearance.anim.durations.scale + } + + NumberAnimation { + running: root.animState === CircularIndicator.Completing + target: manager + property: "completeEndProgress" + from: 0 + to: 1 + duration: manager.completeEndDuration * Appearance.anim.durations.scale + onFinished: { + if (root.animState === CircularIndicator.Completing) + root.animState = CircularIndicator.Stopped; + } + } +} diff --git a/.github/components/controls/CircularProgress.qml b/.github/components/controls/CircularProgress.qml new file mode 100644 index 000000000..a15cd900b --- /dev/null +++ b/.github/components/controls/CircularProgress.qml @@ -0,0 +1,69 @@ +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Shapes + +Shape { + id: root + + property real value + property int startAngle: -90 + property int strokeWidth: Appearance.padding.smaller + property int padding: 0 + property int spacing: Appearance.spacing.small + property color fgColour: Colours.palette.m3primary + property color bgColour: Colours.palette.m3secondaryContainer + + readonly property real size: Math.min(width, height) + readonly property real arcRadius: (size - padding - strokeWidth) / 2 + readonly property real vValue: value || 1 / 360 + readonly property real gapAngle: ((spacing + strokeWidth) / (arcRadius || 1)) * (180 / Math.PI) + + preferredRendererType: Shape.CurveRenderer + asynchronous: true + + ShapePath { + fillColor: "transparent" + strokeColor: root.bgColour + strokeWidth: root.strokeWidth + capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + + PathAngleArc { + startAngle: root.startAngle + 360 * root.vValue + root.gapAngle + sweepAngle: Math.max(-root.gapAngle, 360 * (1 - root.vValue) - root.gapAngle * 2) + radiusX: root.arcRadius + radiusY: root.arcRadius + centerX: root.size / 2 + centerY: root.size / 2 + } + + Behavior on strokeColor { + CAnim { + duration: Appearance.anim.durations.large + } + } + } + + ShapePath { + fillColor: "transparent" + strokeColor: root.fgColour + strokeWidth: root.strokeWidth + capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + + PathAngleArc { + startAngle: root.startAngle + sweepAngle: 360 * root.vValue + radiusX: root.arcRadius + radiusY: root.arcRadius + centerX: root.size / 2 + centerY: root.size / 2 + } + + Behavior on strokeColor { + CAnim { + duration: Appearance.anim.durations.large + } + } + } +} diff --git a/.github/components/controls/CollapsibleSection.qml b/.github/components/controls/CollapsibleSection.qml new file mode 100644 index 000000000..e3d8eefd1 --- /dev/null +++ b/.github/components/controls/CollapsibleSection.qml @@ -0,0 +1,132 @@ +import ".." +import qs.components +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + required property string title + property string description: "" + property bool expanded: false + property bool showBackground: false + property bool nested: false + + signal toggleRequested + + spacing: Appearance.spacing.small + Layout.fillWidth: true + + Item { + id: sectionHeaderItem + Layout.fillWidth: true + Layout.preferredHeight: Math.max(titleRow.implicitHeight + Appearance.padding.normal * 2, 48) + + RowLayout { + id: titleRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + spacing: Appearance.spacing.normal + + StyledText { + text: root.title + font.pointSize: Appearance.font.size.larger + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + + MaterialIcon { + text: "expand_more" + rotation: root.expanded ? 180 : 0 + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.normal + Behavior on rotation { + Anim { + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.standard + } + } + } + } + + StateLayer { + anchors.fill: parent + color: Colours.palette.m3onSurface + radius: Appearance.rounding.normal + showHoverBackground: false + function onClicked(): void { + root.toggleRequested(); + root.expanded = !root.expanded; + } + } + } + + default property alias content: contentColumn.data + + Item { + id: contentWrapper + Layout.fillWidth: true + Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Appearance.spacing.small * 2) : 0 + clip: true + + Behavior on Layout.preferredHeight { + Anim { + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + StyledRect { + id: backgroundRect + anchors.fill: parent + radius: Appearance.rounding.normal + color: Colours.transparency.enabled ? Colours.layer(Colours.palette.m3surfaceContainer, root.nested ? 3 : 2) : (root.nested ? Colours.palette.m3surfaceContainerHigh : Colours.palette.m3surfaceContainer) + opacity: root.showBackground && root.expanded ? 1.0 : 0.0 + visible: root.showBackground + + Behavior on opacity { + Anim { + easing.bezierCurve: Appearance.anim.curves.standard + } + } + } + + ColumnLayout { + id: contentColumn + anchors.left: parent.left + anchors.right: parent.right + y: Appearance.spacing.small + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + anchors.bottomMargin: Appearance.spacing.small + spacing: Appearance.spacing.small + opacity: root.expanded ? 1.0 : 0.0 + + Behavior on opacity { + Anim { + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + StyledText { + id: descriptionText + Layout.fillWidth: true + Layout.topMargin: root.description !== "" ? Appearance.spacing.smaller : 0 + Layout.bottomMargin: root.description !== "" ? Appearance.spacing.small : 0 + visible: root.description !== "" + text: root.description + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.small + wrapMode: Text.Wrap + } + } + } +} diff --git a/.github/components/controls/CustomMouseArea.qml b/.github/components/controls/CustomMouseArea.qml new file mode 100644 index 000000000..7c973c244 --- /dev/null +++ b/.github/components/controls/CustomMouseArea.qml @@ -0,0 +1,21 @@ +import QtQuick + +MouseArea { + property int scrollAccumulatedY: 0 + + function onWheel(event: WheelEvent): void { + } + + onWheel: event => { + // Update accumulated scroll + if (Math.sign(event.angleDelta.y) !== Math.sign(scrollAccumulatedY)) + scrollAccumulatedY = 0; + scrollAccumulatedY += event.angleDelta.y; + + // Trigger handler and reset if above threshold + if (Math.abs(scrollAccumulatedY) >= 120) { + onWheel(event); + scrollAccumulatedY = 0; + } + } +} diff --git a/.github/components/controls/CustomSpinBox.qml b/.github/components/controls/CustomSpinBox.qml new file mode 100644 index 000000000..438dc0806 --- /dev/null +++ b/.github/components/controls/CustomSpinBox.qml @@ -0,0 +1,170 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +RowLayout { + id: root + + property real value + property real max: Infinity + property real min: -Infinity + property real step: 1 + property alias repeatRate: timer.interval + + signal valueModified(value: real) + + spacing: Appearance.spacing.small + + property bool isEditing: false + property string displayText: root.value.toString() + + onValueChanged: { + if (!root.isEditing) { + root.displayText = root.value.toString(); + } + } + + StyledTextField { + id: textField + + inputMethodHints: Qt.ImhFormattedNumbersOnly + text: root.isEditing ? text : root.displayText + validator: DoubleValidator { + bottom: root.min + top: root.max + decimals: root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0 + } + onActiveFocusChanged: { + if (activeFocus) { + root.isEditing = true; + } else { + root.isEditing = false; + root.displayText = root.value.toString(); + } + } + onAccepted: { + const numValue = parseFloat(text); + if (!isNaN(numValue)) { + const clampedValue = Math.max(root.min, Math.min(root.max, numValue)); + root.value = clampedValue; + root.displayText = clampedValue.toString(); + root.valueModified(clampedValue); + } else { + text = root.displayText; + } + root.isEditing = false; + } + onEditingFinished: { + if (text !== root.displayText) { + const numValue = parseFloat(text); + if (!isNaN(numValue)) { + const clampedValue = Math.max(root.min, Math.min(root.max, numValue)); + root.value = clampedValue; + root.displayText = clampedValue.toString(); + root.valueModified(clampedValue); + } else { + text = root.displayText; + } + } + root.isEditing = false; + } + + padding: Appearance.padding.small + leftPadding: Appearance.padding.normal + rightPadding: Appearance.padding.normal + + background: StyledRect { + implicitWidth: 100 + radius: Appearance.rounding.small + color: Colours.tPalette.m3surfaceContainerHigh + } + } + + StyledRect { + radius: Appearance.rounding.small + color: Colours.palette.m3primary + + implicitWidth: implicitHeight + implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2 + + StateLayer { + id: upState + + color: Colours.palette.m3onPrimary + + onPressAndHold: timer.start() + onReleased: timer.stop() + + function onClicked(): void { + let newValue = Math.min(root.max, root.value + root.step); + // Round to avoid floating point precision errors + const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0; + newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals); + root.value = newValue; + root.displayText = newValue.toString(); + root.valueModified(newValue); + } + } + + MaterialIcon { + id: upIcon + + anchors.centerIn: parent + text: "keyboard_arrow_up" + color: Colours.palette.m3onPrimary + } + } + + StyledRect { + radius: Appearance.rounding.small + color: Colours.palette.m3primary + + implicitWidth: implicitHeight + implicitHeight: downIcon.implicitHeight + Appearance.padding.small * 2 + + StateLayer { + id: downState + + color: Colours.palette.m3onPrimary + + onPressAndHold: timer.start() + onReleased: timer.stop() + + function onClicked(): void { + let newValue = Math.max(root.min, root.value - root.step); + // Round to avoid floating point precision errors + const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0; + newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals); + root.value = newValue; + root.displayText = newValue.toString(); + root.valueModified(newValue); + } + } + + MaterialIcon { + id: downIcon + + anchors.centerIn: parent + text: "keyboard_arrow_down" + color: Colours.palette.m3onPrimary + } + } + + Timer { + id: timer + + interval: 100 + repeat: true + triggeredOnStart: true + onTriggered: { + if (upState.pressed) + upState.onClicked(); + else if (downState.pressed) + downState.onClicked(); + } + } +} diff --git a/.github/components/controls/FilledSlider.qml b/.github/components/controls/FilledSlider.qml new file mode 100644 index 000000000..80dd44c5f --- /dev/null +++ b/.github/components/controls/FilledSlider.qml @@ -0,0 +1,146 @@ +import ".." +import "../effects" +import qs.services +import qs.config +import QtQuick +import QtQuick.Templates + +Slider { + id: root + + required property string icon + property real oldValue + property bool initialized + + orientation: Qt.Vertical + + background: StyledRect { + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.full + + StyledRect { + anchors.left: parent.left + anchors.right: parent.right + + y: root.handle.y + implicitHeight: parent.height - y + + color: Colours.palette.m3secondary + radius: parent.radius + } + } + + handle: Item { + id: handle + + property alias moving: icon.moving + + y: root.visualPosition * (root.availableHeight - height) + implicitWidth: root.width + implicitHeight: root.width + + Elevation { + anchors.fill: parent + radius: rect.radius + level: handleInteraction.containsMouse ? 2 : 1 + } + + StyledRect { + id: rect + + anchors.fill: parent + + color: Colours.palette.m3inverseSurface + radius: Appearance.rounding.full + + MouseArea { + id: handleInteraction + + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.NoButton + } + + MaterialIcon { + id: icon + + property bool moving + + function update(): void { + animate = !moving; + binding.when = moving; + font.pointSize = moving ? Appearance.font.size.small : Appearance.font.size.larger; + font.family = moving ? Appearance.font.family.sans : Appearance.font.family.material; + } + + text: root.icon + color: Colours.palette.m3inverseOnSurface + anchors.centerIn: parent + + onMovingChanged: anim.restart() + + Binding { + id: binding + + target: icon + property: "text" + value: Math.round(root.value * 100) + when: false + } + + SequentialAnimation { + id: anim + + Anim { + target: icon + property: "scale" + to: 0 + duration: Appearance.anim.durations.normal / 2 + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + ScriptAction { + script: icon.update() + } + Anim { + target: icon + property: "scale" + to: 1 + duration: Appearance.anim.durations.normal / 2 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + } + } + } + } + + onPressedChanged: handle.moving = pressed + + onValueChanged: { + if (!initialized) { + initialized = true; + return; + } + if (Math.abs(value - oldValue) < 0.01) + return; + oldValue = value; + handle.moving = true; + stateChangeDelay.restart(); + } + + Timer { + id: stateChangeDelay + + interval: 500 + onTriggered: { + if (!root.pressed) + handle.moving = false; + } + } + + Behavior on value { + Anim { + duration: Appearance.anim.durations.large + } + } +} diff --git a/.github/components/controls/IconButton.qml b/.github/components/controls/IconButton.qml new file mode 100644 index 000000000..ffb1d0663 --- /dev/null +++ b/.github/components/controls/IconButton.qml @@ -0,0 +1,83 @@ +import ".." +import qs.services +import qs.config +import QtQuick + +StyledRect { + id: root + + enum Type { + Filled, + Tonal, + Text + } + + property alias icon: label.text + property bool checked + property bool toggle + property real padding: type === IconButton.Text ? Appearance.padding.small / 2 : Appearance.padding.smaller + property alias font: label.font + property int type: IconButton.Filled + property bool disabled + + property alias stateLayer: stateLayer + property alias label: label + property alias radiusAnim: radiusAnim + + property bool internalChecked + property color activeColour: type === IconButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary + property color inactiveColour: { + if (!toggle && type === IconButton.Filled) + return Colours.palette.m3primary; + return type === IconButton.Filled ? Colours.tPalette.m3surfaceContainer : Colours.palette.m3secondaryContainer; + } + property color activeOnColour: type === IconButton.Filled ? Colours.palette.m3onPrimary : type === IconButton.Tonal ? Colours.palette.m3onSecondary : Colours.palette.m3primary + property color inactiveOnColour: { + if (!toggle && type === IconButton.Filled) + return Colours.palette.m3onPrimary; + return type === IconButton.Tonal ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurfaceVariant; + } + property color disabledColour: Qt.alpha(Colours.palette.m3onSurface, 0.1) + property color disabledOnColour: Qt.alpha(Colours.palette.m3onSurface, 0.38) + + signal clicked + + onCheckedChanged: internalChecked = checked + + radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + color: type === IconButton.Text ? "transparent" : disabled ? disabledColour : internalChecked ? activeColour : inactiveColour + + implicitWidth: implicitHeight + implicitHeight: label.implicitHeight + padding * 2 + + StateLayer { + id: stateLayer + + color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour + disabled: root.disabled + + function onClicked(): void { + if (root.toggle) + root.internalChecked = !root.internalChecked; + root.clicked(); + } + } + + MaterialIcon { + id: label + + anchors.centerIn: parent + color: root.disabled ? root.disabledOnColour : root.internalChecked ? root.activeOnColour : root.inactiveOnColour + fill: !root.toggle || root.internalChecked ? 1 : 0 + + Behavior on fill { + Anim {} + } + } + + Behavior on radius { + Anim { + id: radiusAnim + } + } +} diff --git a/.github/components/controls/IconTextButton.qml b/.github/components/controls/IconTextButton.qml new file mode 100644 index 000000000..b2bb96cc0 --- /dev/null +++ b/.github/components/controls/IconTextButton.qml @@ -0,0 +1,88 @@ +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + enum Type { + Filled, + Tonal, + Text + } + + property alias icon: iconLabel.text + property alias text: label.text + property bool checked + property bool toggle + property real horizontalPadding: Appearance.padding.normal + property real verticalPadding: Appearance.padding.smaller + property alias font: label.font + property int type: IconTextButton.Filled + + property alias stateLayer: stateLayer + property alias iconLabel: iconLabel + property alias label: label + + property bool internalChecked + property color activeColour: type === IconTextButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary + property color inactiveColour: type === IconTextButton.Filled ? Colours.tPalette.m3surfaceContainer : Colours.palette.m3secondaryContainer + property color activeOnColour: type === IconTextButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary + property color inactiveOnColour: type === IconTextButton.Filled ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer + + signal clicked + + onCheckedChanged: internalChecked = checked + + radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + color: type === IconTextButton.Text ? "transparent" : internalChecked ? activeColour : inactiveColour + + implicitWidth: row.implicitWidth + horizontalPadding * 2 + implicitHeight: row.implicitHeight + verticalPadding * 2 + + StateLayer { + id: stateLayer + + color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour + + function onClicked(): void { + if (root.toggle) + root.internalChecked = !root.internalChecked; + root.clicked(); + } + } + + RowLayout { + id: row + + anchors.centerIn: parent + spacing: Appearance.spacing.small + + MaterialIcon { + id: iconLabel + + Layout.alignment: Qt.AlignVCenter + Layout.topMargin: Math.round(fontInfo.pointSize * 0.0575) + color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour + fill: root.internalChecked ? 1 : 0 + + Behavior on fill { + Anim {} + } + } + + StyledText { + id: label + + Layout.alignment: Qt.AlignVCenter + Layout.topMargin: -Math.round(iconLabel.fontInfo.pointSize * 0.0575) + color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour + } + } + + Behavior on radius { + Anim {} + } +} diff --git a/.github/components/controls/Menu.qml b/.github/components/controls/Menu.qml new file mode 100644 index 000000000..c763b54a8 --- /dev/null +++ b/.github/components/controls/Menu.qml @@ -0,0 +1,113 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../effects" +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +Elevation { + id: root + + property list items + property MenuItem active: items[0] ?? null + property bool expanded + + signal itemSelected(item: MenuItem) + + radius: Appearance.rounding.small / 2 + level: 2 + + implicitWidth: Math.max(200, column.implicitWidth) + implicitHeight: root.expanded ? column.implicitHeight : 0 + opacity: root.expanded ? 1 : 0 + + StyledClippingRect { + anchors.fill: parent + radius: parent.radius + color: Colours.palette.m3surfaceContainer + + ColumnLayout { + id: column + + anchors.left: parent.left + anchors.right: parent.right + spacing: 0 + + Repeater { + model: root.items + + StyledRect { + id: item + + required property int index + required property MenuItem modelData + readonly property bool active: modelData === root.active + + Layout.fillWidth: true + implicitWidth: menuOptionRow.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: menuOptionRow.implicitHeight + Appearance.padding.normal * 2 + + color: Qt.alpha(Colours.palette.m3secondaryContainer, active ? 1 : 0) + + StateLayer { + color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + disabled: !root.expanded + + function onClicked(): void { + root.itemSelected(item.modelData); + root.active = item.modelData; + root.expanded = false; + } + } + + RowLayout { + id: menuOptionRow + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + spacing: Appearance.spacing.small + + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + text: item.modelData.icon + color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurfaceVariant + } + + StyledText { + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + text: item.modelData.text + color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + } + + Loader { + Layout.alignment: Qt.AlignVCenter + active: item.modelData.trailingIcon.length > 0 + visible: active + + sourceComponent: MaterialIcon { + text: item.modelData.trailingIcon + color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + } + } + } + } + } + } + } + + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + } + } + + Behavior on implicitHeight { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } +} diff --git a/.github/components/controls/MenuItem.qml b/.github/components/controls/MenuItem.qml new file mode 100644 index 000000000..5348bbe82 --- /dev/null +++ b/.github/components/controls/MenuItem.qml @@ -0,0 +1,11 @@ +import QtQuick + +QtObject { + required property string text + property string icon + property string trailingIcon + property string activeIcon: icon + property string activeText: text + + signal clicked +} diff --git a/.github/components/controls/SpinBoxRow.qml b/.github/components/controls/SpinBoxRow.qml new file mode 100644 index 000000000..fe6a19822 --- /dev/null +++ b/.github/components/controls/SpinBoxRow.qml @@ -0,0 +1,52 @@ +import ".." +import qs.components +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + required property string label + required property real value + required property real min + required property real max + property real step: 1 + property var onValueModified: function (value) {} + + Layout.fillWidth: true + implicitHeight: row.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: row + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: root.label + } + + CustomSpinBox { + min: root.min + max: root.max + step: root.step + value: root.value + onValueModified: value => { + root.onValueModified(value); + } + } + } +} diff --git a/.github/components/controls/SplitButton.qml b/.github/components/controls/SplitButton.qml new file mode 100644 index 000000000..c91474eae --- /dev/null +++ b/.github/components/controls/SplitButton.qml @@ -0,0 +1,164 @@ +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +Row { + id: root + + enum Type { + Filled, + Tonal + } + + property real horizontalPadding: Appearance.padding.normal + property real verticalPadding: Appearance.padding.smaller + property int type: SplitButton.Filled + property bool disabled + property bool menuOnTop + property string fallbackIcon + property string fallbackText + + property alias menuItems: menu.items + property alias active: menu.active + property alias expanded: menu.expanded + property alias menu: menu + property alias iconLabel: iconLabel + property alias label: label + property alias stateLayer: stateLayer + + property color colour: type == SplitButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondaryContainer + property color textColour: type == SplitButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondaryContainer + property color disabledColour: Qt.alpha(Colours.palette.m3onSurface, 0.1) + property color disabledTextColour: Qt.alpha(Colours.palette.m3onSurface, 0.38) + + spacing: Math.floor(Appearance.spacing.small / 2) + + StyledRect { + radius: implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + topRightRadius: Appearance.rounding.small / 2 + bottomRightRadius: Appearance.rounding.small / 2 + color: root.disabled ? root.disabledColour : root.colour + + implicitWidth: textRow.implicitWidth + root.horizontalPadding * 2 + implicitHeight: expandBtn.implicitHeight + + StateLayer { + id: stateLayer + + rect.topRightRadius: parent.topRightRadius + rect.bottomRightRadius: parent.bottomRightRadius + color: root.textColour + disabled: root.disabled + + function onClicked(): void { + root.active?.clicked(); + } + } + + RowLayout { + id: textRow + + anchors.centerIn: parent + anchors.horizontalCenterOffset: Math.floor(root.verticalPadding / 4) + spacing: Appearance.spacing.small + + MaterialIcon { + id: iconLabel + + Layout.alignment: Qt.AlignVCenter + animate: true + text: root.active?.activeIcon ?? root.fallbackIcon + color: root.disabled ? root.disabledTextColour : root.textColour + fill: 1 + } + + StyledText { + id: label + + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: implicitWidth + animate: true + text: root.active?.activeText ?? root.fallbackText + color: root.disabled ? root.disabledTextColour : root.textColour + clip: true + + Behavior on Layout.preferredWidth { + Anim { + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + } + } + } + + StyledRect { + id: expandBtn + + property real rad: root.expanded ? implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) : Appearance.rounding.small / 2 + + radius: implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + topLeftRadius: rad + bottomLeftRadius: rad + color: root.disabled ? root.disabledColour : root.colour + + implicitWidth: implicitHeight + implicitHeight: expandIcon.implicitHeight + root.verticalPadding * 2 + + StateLayer { + id: expandStateLayer + + rect.topLeftRadius: parent.topLeftRadius + rect.bottomLeftRadius: parent.bottomLeftRadius + color: root.textColour + disabled: root.disabled + + function onClicked(): void { + root.expanded = !root.expanded; + } + } + + MaterialIcon { + id: expandIcon + + anchors.centerIn: parent + anchors.horizontalCenterOffset: root.expanded ? 0 : -Math.floor(root.verticalPadding / 4) + + text: "expand_more" + color: root.disabled ? root.disabledTextColour : root.textColour + rotation: root.expanded ? 180 : 0 + + Behavior on anchors.horizontalCenterOffset { + Anim {} + } + + Behavior on rotation { + Anim {} + } + } + + Behavior on rad { + Anim {} + } + + Menu { + id: menu + + states: State { + when: root.menuOnTop + + AnchorChanges { + target: menu + anchors.top: undefined + anchors.bottom: expandBtn.top + } + } + + anchors.top: parent.bottom + anchors.right: parent.right + anchors.topMargin: Appearance.spacing.small + anchors.bottomMargin: Appearance.spacing.small + } + } +} diff --git a/.github/components/controls/SplitButtonRow.qml b/.github/components/controls/SplitButtonRow.qml new file mode 100644 index 000000000..db9925ff6 --- /dev/null +++ b/.github/components/controls/SplitButtonRow.qml @@ -0,0 +1,62 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + required property string label + property int expandedZ: 100 + property bool enabled: true + + property alias menuItems: splitButton.menuItems + property alias active: splitButton.active + property alias expanded: splitButton.expanded + property alias type: splitButton.type + + signal selected(item: MenuItem) + + Layout.fillWidth: true + implicitHeight: row.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + + clip: false + z: splitButton.menu.implicitHeight > 0 ? expandedZ : 1 + opacity: enabled ? 1.0 : 0.5 + + RowLayout { + id: row + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: root.label + color: root.enabled ? Colours.palette.m3onSurface : Colours.palette.m3onSurfaceVariant + } + + SplitButton { + id: splitButton + enabled: root.enabled + type: SplitButton.Filled + + menu.z: 1 + + stateLayer.onClicked: { + splitButton.expanded = !splitButton.expanded; + } + + menu.onItemSelected: item => { + root.selected(item); + } + } + } +} diff --git a/.github/components/controls/StyledInputField.qml b/.github/components/controls/StyledInputField.qml new file mode 100644 index 000000000..0d199c738 --- /dev/null +++ b/.github/components/controls/StyledInputField.qml @@ -0,0 +1,79 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.components +import qs.services +import qs.config +import QtQuick + +Item { + id: root + + property string text: "" + property var validator: null + property bool readOnly: false + property int horizontalAlignment: TextInput.AlignHCenter + property int implicitWidth: 70 + property bool enabled: true + + // Expose activeFocus through alias to avoid FINAL property override + readonly property alias hasFocus: inputField.activeFocus + + signal textEdited(string text) + signal editingFinished + + implicitHeight: inputField.implicitHeight + Appearance.padding.small * 2 + + StyledRect { + id: container + + anchors.fill: parent + color: inputHover.containsMouse || inputField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: inputField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) + opacity: root.enabled ? 1 : 0.5 + + Behavior on color { + CAnim {} + } + Behavior on border.color { + CAnim {} + } + + MouseArea { + id: inputHover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor + acceptedButtons: Qt.NoButton + enabled: root.enabled + } + + StyledTextField { + id: inputField + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: root.horizontalAlignment + validator: root.validator + readOnly: root.readOnly + enabled: root.enabled + + Binding { + target: inputField + property: "text" + value: root.text + when: !inputField.activeFocus + } + + onTextChanged: { + root.text = text; + root.textEdited(text); + } + + onEditingFinished: { + root.editingFinished(); + } + } + } +} diff --git a/.github/components/controls/StyledRadioButton.qml b/.github/components/controls/StyledRadioButton.qml new file mode 100644 index 000000000..b72fc77f3 --- /dev/null +++ b/.github/components/controls/StyledRadioButton.qml @@ -0,0 +1,57 @@ +import qs.components +import qs.services +import qs.config +import QtQuick +import QtQuick.Templates + +RadioButton { + id: root + + font.pointSize: Appearance.font.size.smaller + + implicitWidth: implicitIndicatorWidth + implicitContentWidth + contentItem.anchors.leftMargin + implicitHeight: Math.max(implicitIndicatorHeight, implicitContentHeight) + + indicator: Rectangle { + id: outerCircle + + implicitWidth: 20 + implicitHeight: 20 + radius: Appearance.rounding.full + color: "transparent" + border.color: root.checked ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + border.width: 2 + anchors.verticalCenter: parent.verticalCenter + + StateLayer { + anchors.margins: -Appearance.padding.smaller + color: root.checked ? Colours.palette.m3onSurface : Colours.palette.m3primary + z: -1 + + function onClicked(): void { + root.click(); + } + } + + StyledRect { + anchors.centerIn: parent + implicitWidth: 8 + implicitHeight: 8 + + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3primary, root.checked ? 1 : 0) + } + + Behavior on border.color { + CAnim {} + } + } + + contentItem: StyledText { + text: root.text + font.pointSize: root.font.pointSize + anchors.verticalCenter: parent.verticalCenter + anchors.left: outerCircle.right + anchors.leftMargin: Appearance.spacing.smaller + } +} diff --git a/.github/components/controls/StyledScrollBar.qml b/.github/components/controls/StyledScrollBar.qml new file mode 100644 index 000000000..de8b679cd --- /dev/null +++ b/.github/components/controls/StyledScrollBar.qml @@ -0,0 +1,190 @@ +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Templates + +ScrollBar { + id: root + + required property Flickable flickable + property bool shouldBeActive + property real nonAnimPosition + property bool animating + + onHoveredChanged: { + if (hovered) + shouldBeActive = true; + else + shouldBeActive = flickable.moving; + } + + property bool _updatingFromFlickable: false + property bool _updatingFromUser: false + + // Sync nonAnimPosition with Qt's automatic position binding + onPositionChanged: { + if (_updatingFromUser) { + _updatingFromUser = false; + return; + } + if (position === nonAnimPosition) { + animating = false; + return; + } + if (!animating && !_updatingFromFlickable && !fullMouse.pressed) { + nonAnimPosition = position; + } + } + + // Sync nonAnimPosition with flickable when not animating + Connections { + target: flickable + function onContentYChanged() { + if (!animating && !fullMouse.pressed) { + _updatingFromFlickable = true; + const contentHeight = flickable.contentHeight; + const height = flickable.height; + if (contentHeight > height) { + nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height))); + } else { + nonAnimPosition = 0; + } + _updatingFromFlickable = false; + } + } + } + + Component.onCompleted: { + if (flickable) { + const contentHeight = flickable.contentHeight; + const height = flickable.height; + if (contentHeight > height) { + nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height))); + } + } + } + implicitWidth: Appearance.padding.small + + contentItem: StyledRect { + anchors.left: parent.left + anchors.right: parent.right + opacity: { + if (root.size === 1) + return 0; + if (fullMouse.pressed) + return 1; + if (mouse.containsMouse) + return 0.8; + if (root.policy === ScrollBar.AlwaysOn || root.shouldBeActive) + return 0.6; + return 0; + } + radius: Appearance.rounding.full + color: Colours.palette.m3secondary + + MouseArea { + id: mouse + + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + acceptedButtons: Qt.NoButton + } + + Behavior on opacity { + Anim {} + } + } + + Connections { + target: root.flickable + + function onMovingChanged(): void { + if (root.flickable.moving) + root.shouldBeActive = true; + else + hideDelay.restart(); + } + } + + Timer { + id: hideDelay + + interval: 600 + onTriggered: root.shouldBeActive = root.flickable.moving || root.hovered + } + + CustomMouseArea { + id: fullMouse + + anchors.fill: parent + preventStealing: true + + onPressed: event => { + root.animating = true; + root._updatingFromUser = true; + const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); + root.nonAnimPosition = newPos; + // Update flickable position + // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] + if (root.flickable) { + const contentHeight = root.flickable.contentHeight; + const height = root.flickable.height; + if (contentHeight > height) { + const maxContentY = contentHeight - height; + const maxPos = 1 - root.size; + const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0; + root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY)); + } + } + } + + onPositionChanged: event => { + root._updatingFromUser = true; + const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); + root.nonAnimPosition = newPos; + // Update flickable position + // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] + if (root.flickable) { + const contentHeight = root.flickable.contentHeight; + const height = root.flickable.height; + if (contentHeight > height) { + const maxContentY = contentHeight - height; + const maxPos = 1 - root.size; + const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0; + root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY)); + } + } + } + + function onWheel(event: WheelEvent): void { + root.animating = true; + root._updatingFromUser = true; + let newPos = root.nonAnimPosition; + if (event.angleDelta.y > 0) + newPos = Math.max(0, root.nonAnimPosition - 0.1); + else if (event.angleDelta.y < 0) + newPos = Math.min(1 - root.size, root.nonAnimPosition + 0.1); + root.nonAnimPosition = newPos; + // Update flickable position + // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] + if (root.flickable) { + const contentHeight = root.flickable.contentHeight; + const height = root.flickable.height; + if (contentHeight > height) { + const maxContentY = contentHeight - height; + const maxPos = 1 - root.size; + const contentY = maxPos > 0 ? (newPos / maxPos) * maxContentY : 0; + root.flickable.contentY = Math.max(0, Math.min(maxContentY, contentY)); + } + } + } + } + + Behavior on position { + enabled: !fullMouse.pressed + + Anim {} + } +} diff --git a/.github/components/controls/StyledSlider.qml b/.github/components/controls/StyledSlider.qml new file mode 100644 index 000000000..0ef229df2 --- /dev/null +++ b/.github/components/controls/StyledSlider.qml @@ -0,0 +1,57 @@ +import qs.components +import qs.services +import qs.config +import QtQuick +import QtQuick.Templates + +Slider { + id: root + + background: Item { + StyledRect { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.topMargin: root.implicitHeight / 3 + anchors.bottomMargin: root.implicitHeight / 3 + + implicitWidth: root.handle.x - root.implicitHeight / 6 + + color: Colours.palette.m3primary + radius: Appearance.rounding.full + topRightRadius: root.implicitHeight / 15 + bottomRightRadius: root.implicitHeight / 15 + } + + StyledRect { + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.topMargin: root.implicitHeight / 3 + anchors.bottomMargin: root.implicitHeight / 3 + + implicitWidth: parent.width - root.handle.x - root.handle.implicitWidth - root.implicitHeight / 6 + + color: Colours.palette.m3surfaceContainerHighest + radius: Appearance.rounding.full + topLeftRadius: root.implicitHeight / 15 + bottomLeftRadius: root.implicitHeight / 15 + } + } + + handle: StyledRect { + x: root.visualPosition * root.availableWidth - implicitWidth / 2 + + implicitWidth: root.implicitHeight / 4.5 + implicitHeight: root.implicitHeight + + color: Colours.palette.m3primary + radius: Appearance.rounding.full + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + cursorShape: Qt.PointingHandCursor + } + } +} diff --git a/.github/components/controls/StyledSwitch.qml b/.github/components/controls/StyledSwitch.qml new file mode 100644 index 000000000..ce93cd505 --- /dev/null +++ b/.github/components/controls/StyledSwitch.qml @@ -0,0 +1,152 @@ +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Templates +import QtQuick.Shapes + +Switch { + id: root + + property int cLayer: 1 + + implicitWidth: implicitIndicatorWidth + implicitHeight: implicitIndicatorHeight + + indicator: StyledRect { + radius: Appearance.rounding.full + color: root.checked ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHighest, root.cLayer) + + implicitWidth: implicitHeight * 1.7 + implicitHeight: Appearance.font.size.normal + Appearance.padding.smaller * 2 + + StyledRect { + readonly property real nonAnimWidth: root.pressed ? implicitHeight * 1.3 : implicitHeight + + radius: Appearance.rounding.full + color: root.checked ? Colours.palette.m3onPrimary : Colours.layer(Colours.palette.m3outline, root.cLayer + 1) + + x: root.checked ? parent.implicitWidth - nonAnimWidth - Appearance.padding.small / 2 : Appearance.padding.small / 2 + implicitWidth: nonAnimWidth + implicitHeight: parent.implicitHeight - Appearance.padding.small + anchors.verticalCenter: parent.verticalCenter + + StyledRect { + anchors.fill: parent + radius: parent.radius + + color: root.checked ? Colours.palette.m3primary : Colours.palette.m3onSurface + opacity: root.pressed ? 0.1 : root.hovered ? 0.08 : 0 + + Behavior on opacity { + Anim {} + } + } + + Shape { + id: icon + + property point start1: { + if (root.pressed) + return Qt.point(width * 0.2, height / 2); + if (root.checked) + return Qt.point(width * 0.15, height / 2); + return Qt.point(width * 0.15, height * 0.15); + } + property point end1: { + if (root.pressed) { + if (root.checked) + return Qt.point(width * 0.4, height / 2); + return Qt.point(width * 0.8, height / 2); + } + if (root.checked) + return Qt.point(width * 0.4, height * 0.7); + return Qt.point(width * 0.85, height * 0.85); + } + property point start2: { + if (root.pressed) { + if (root.checked) + return Qt.point(width * 0.4, height / 2); + return Qt.point(width * 0.2, height / 2); + } + if (root.checked) + return Qt.point(width * 0.4, height * 0.7); + return Qt.point(width * 0.15, height * 0.85); + } + property point end2: { + if (root.pressed) + return Qt.point(width * 0.8, height / 2); + if (root.checked) + return Qt.point(width * 0.85, height * 0.2); + return Qt.point(width * 0.85, height * 0.15); + } + + anchors.centerIn: parent + width: height + height: parent.implicitHeight - Appearance.padding.small * 2 + preferredRendererType: Shape.CurveRenderer + asynchronous: true + + ShapePath { + strokeWidth: Appearance.font.size.larger * 0.15 + strokeColor: root.checked ? Colours.palette.m3primary : Colours.palette.m3surfaceContainerHighest + fillColor: "transparent" + capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + + startX: icon.start1.x + startY: icon.start1.y + + PathLine { + x: icon.end1.x + y: icon.end1.y + } + PathMove { + x: icon.start2.x + y: icon.start2.y + } + PathLine { + x: icon.end2.x + y: icon.end2.y + } + + Behavior on strokeColor { + CAnim {} + } + } + + Behavior on start1 { + PropAnim {} + } + Behavior on end1 { + PropAnim {} + } + Behavior on start2 { + PropAnim {} + } + Behavior on end2 { + PropAnim {} + } + } + + Behavior on x { + Anim {} + } + + Behavior on implicitWidth { + Anim {} + } + } + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + enabled: false + } + + component PropAnim: PropertyAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.standard + } +} diff --git a/.github/components/controls/StyledTextField.qml b/.github/components/controls/StyledTextField.qml new file mode 100644 index 000000000..60bcff259 --- /dev/null +++ b/.github/components/controls/StyledTextField.qml @@ -0,0 +1,76 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Controls + +TextField { + id: root + + color: Colours.palette.m3onSurface + placeholderTextColor: Colours.palette.m3outline + font.family: Appearance.font.family.sans + font.pointSize: Appearance.font.size.smaller + renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering + cursorVisible: !readOnly + + background: null + + cursorDelegate: StyledRect { + id: cursor + + property bool disableBlink + + implicitWidth: 2 + color: Colours.palette.m3primary + radius: Appearance.rounding.normal + + Connections { + target: root + + function onCursorPositionChanged(): void { + if (root.activeFocus && root.cursorVisible) { + cursor.opacity = 1; + cursor.disableBlink = true; + enableBlink.restart(); + } + } + } + + Timer { + id: enableBlink + + interval: 100 + onTriggered: cursor.disableBlink = false + } + + Timer { + running: root.activeFocus && root.cursorVisible && !cursor.disableBlink + repeat: true + triggeredOnStart: true + interval: 500 + onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1 + } + + Binding { + when: !root.activeFocus || !root.cursorVisible + cursor.opacity: 0 + } + + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.small + } + } + } + + Behavior on color { + CAnim {} + } + + Behavior on placeholderTextColor { + CAnim {} + } +} diff --git a/.github/components/controls/SwitchRow.qml b/.github/components/controls/SwitchRow.qml new file mode 100644 index 000000000..6dda3f0cc --- /dev/null +++ b/.github/components/controls/SwitchRow.qml @@ -0,0 +1,48 @@ +import ".." +import qs.components +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + required property string label + required property bool checked + property bool enabled: true + property var onToggled: function (checked) {} + + Layout.fillWidth: true + implicitHeight: row.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: row + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: root.label + } + + StyledSwitch { + checked: root.checked + enabled: root.enabled + onToggled: { + root.onToggled(checked); + } + } + } +} diff --git a/.github/components/controls/TextButton.qml b/.github/components/controls/TextButton.qml new file mode 100644 index 000000000..ecf7eb133 --- /dev/null +++ b/.github/components/controls/TextButton.qml @@ -0,0 +1,78 @@ +import ".." +import qs.services +import qs.config +import QtQuick + +StyledRect { + id: root + + enum Type { + Filled, + Tonal, + Text + } + + property alias text: label.text + property bool checked + property bool toggle + property real horizontalPadding: Appearance.padding.normal + property real verticalPadding: Appearance.padding.smaller + property alias font: label.font + property int type: TextButton.Filled + + property alias stateLayer: stateLayer + property alias label: label + + property bool internalChecked + property color activeColour: type === TextButton.Filled ? Colours.palette.m3primary : Colours.palette.m3secondary + property color inactiveColour: { + if (!toggle && type === TextButton.Filled) + return Colours.palette.m3primary; + return type === TextButton.Filled ? Colours.tPalette.m3surfaceContainer : Colours.palette.m3secondaryContainer; + } + property color activeOnColour: { + if (type === TextButton.Text) + return Colours.palette.m3primary; + return type === TextButton.Filled ? Colours.palette.m3onPrimary : Colours.palette.m3onSecondary; + } + property color inactiveOnColour: { + if (!toggle && type === TextButton.Filled) + return Colours.palette.m3onPrimary; + if (type === TextButton.Text) + return Colours.palette.m3primary; + return type === TextButton.Filled ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer; + } + + signal clicked + + onCheckedChanged: internalChecked = checked + + radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + color: type === TextButton.Text ? "transparent" : internalChecked ? activeColour : inactiveColour + + implicitWidth: label.implicitWidth + horizontalPadding * 2 + implicitHeight: label.implicitHeight + verticalPadding * 2 + + StateLayer { + id: stateLayer + + color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour + + function onClicked(): void { + if (root.toggle) + root.internalChecked = !root.internalChecked; + root.clicked(); + } + } + + StyledText { + id: label + + anchors.centerIn: parent + color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour + } + + Behavior on radius { + Anim {} + } +} diff --git a/.github/components/controls/ToggleButton.qml b/.github/components/controls/ToggleButton.qml new file mode 100644 index 000000000..98c7564f2 --- /dev/null +++ b/.github/components/controls/ToggleButton.qml @@ -0,0 +1,124 @@ +import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + required property bool toggled + property string icon + property string label + property string accent: "Secondary" + property real iconSize: Appearance.font.size.large + property real horizontalPadding: Appearance.padding.large + property real verticalPadding: Appearance.padding.normal + property string tooltip: "" + + property bool hovered: false + signal clicked + + Component.onCompleted: { + hovered = toggleStateLayer.containsMouse; + } + + Connections { + target: toggleStateLayer + function onContainsMouseChanged() { + const newHovered = toggleStateLayer.containsMouse; + if (hovered !== newHovered) { + hovered = newHovered; + } + } + } + + Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0) + implicitWidth: toggleBtnInner.implicitWidth + horizontalPadding * 2 + implicitHeight: toggleBtnIcon.implicitHeight + verticalPadding * 2 + + radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 * Math.min(1, Appearance.rounding.scale) + color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`] + + StateLayer { + id: toggleStateLayer + + color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`] + + function onClicked(): void { + root.clicked(); + } + } + + RowLayout { + id: toggleBtnInner + + anchors.centerIn: parent + spacing: Appearance.spacing.normal + + MaterialIcon { + id: toggleBtnIcon + + visible: !!text + fill: root.toggled ? 1 : 0 + text: root.icon + color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`] + font.pointSize: root.iconSize + + Behavior on fill { + Anim {} + } + } + + Loader { + active: !!root.label + visible: active + + sourceComponent: StyledText { + text: root.label + color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`] + } + } + } + + Behavior on radius { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + + Behavior on Layout.preferredWidth { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + + // Tooltip - positioned absolutely, doesn't affect layout + Loader { + id: tooltipLoader + active: root.tooltip !== "" + z: 10000 + width: 0 + height: 0 + sourceComponent: Component { + Tooltip { + target: root + text: root.tooltip + } + } + // Completely remove from layout + Layout.fillWidth: false + Layout.fillHeight: false + Layout.preferredWidth: 0 + Layout.preferredHeight: 0 + Layout.maximumWidth: 0 + Layout.maximumHeight: 0 + Layout.minimumWidth: 0 + Layout.minimumHeight: 0 + } +} diff --git a/.github/components/controls/ToggleRow.qml b/.github/components/controls/ToggleRow.qml new file mode 100644 index 000000000..269d3d6a5 --- /dev/null +++ b/.github/components/controls/ToggleRow.qml @@ -0,0 +1,28 @@ +import qs.components +import qs.components.controls +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +RowLayout { + id: root + + required property string label + property alias checked: toggle.checked + property alias toggle: toggle + + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: root.label + } + + StyledSwitch { + id: toggle + + cLayer: 2 + } +} diff --git a/.github/components/controls/Tooltip.qml b/.github/components/controls/Tooltip.qml new file mode 100644 index 000000000..b129a37b9 --- /dev/null +++ b/.github/components/controls/Tooltip.qml @@ -0,0 +1,185 @@ +import ".." +import qs.components.effects +import qs.services +import qs.config +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +Popup { + id: root + + required property Item target + required property string text + property int delay: 500 + property int timeout: 0 + + property bool tooltipVisible: false + property Timer showTimer: Timer { + interval: root.delay + onTriggered: root.tooltipVisible = true + } + property Timer hideTimer: Timer { + interval: root.timeout + onTriggered: root.tooltipVisible = false + } + + // Popup properties - doesn't affect layout + parent: { + let p = target; + // Walk up to find the root Item (usually has anchors.fill: parent) + while (p && p.parent) { + const parentItem = p.parent; + // Check if this looks like a root pane Item + if (parentItem && parentItem.anchors && parentItem.anchors.fill !== undefined) { + return parentItem; + } + p = parentItem; + } + // Fallback + return target.parent?.parent?.parent ?? target.parent?.parent ?? target.parent ?? target; + } + + visible: tooltipVisible + modal: false + closePolicy: Popup.NoAutoClose + padding: 0 + margins: 0 + background: Item {} + + // Update position when target moves or tooltip becomes visible + onTooltipVisibleChanged: { + if (tooltipVisible) { + Qt.callLater(updatePosition); + } + } + Connections { + target: root.target + function onXChanged() { + if (root.tooltipVisible) + root.updatePosition(); + } + function onYChanged() { + if (root.tooltipVisible) + root.updatePosition(); + } + function onWidthChanged() { + if (root.tooltipVisible) + root.updatePosition(); + } + function onHeightChanged() { + if (root.tooltipVisible) + root.updatePosition(); + } + } + + function updatePosition() { + if (!target || !parent) + return; + + // Wait for tooltipRect to have its size calculated + Qt.callLater(() => { + if (!target || !parent || !tooltipRect) + return; + + // Get target position in parent's coordinate system + const targetPos = target.mapToItem(parent, 0, 0); + const targetCenterX = targetPos.x + target.width / 2; + + // Get tooltip size (use width/height if available, otherwise implicit) + const tooltipWidth = tooltipRect.width > 0 ? tooltipRect.width : tooltipRect.implicitWidth; + const tooltipHeight = tooltipRect.height > 0 ? tooltipRect.height : tooltipRect.implicitHeight; + + // Center tooltip horizontally on target + let newX = targetCenterX - tooltipWidth / 2; + + // Position tooltip above target + let newY = targetPos.y - tooltipHeight - Appearance.spacing.small; + + // Keep within bounds + const padding = Appearance.padding.normal; + if (newX < padding) { + newX = padding; + } else if (newX + tooltipWidth > (parent.width - padding)) { + newX = parent.width - tooltipWidth - padding; + } + + // Update popup position + x = newX; + y = newY; + }); + } + + enter: Transition { + Anim { + property: "opacity" + from: 0 + to: 1 + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + + exit: Transition { + Anim { + property: "opacity" + from: 1 + to: 0 + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } + + // Monitor hover state + Connections { + target: root.target + function onHoveredChanged() { + if (target.hovered) { + showTimer.start(); + if (timeout > 0) { + hideTimer.stop(); + hideTimer.start(); + } + } else { + showTimer.stop(); + hideTimer.stop(); + tooltipVisible = false; + } + } + } + + contentItem: StyledRect { + id: tooltipRect + + implicitWidth: tooltipText.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: tooltipText.implicitHeight + Appearance.padding.smaller * 2 + + color: Colours.palette.m3surfaceContainerHighest + radius: Appearance.rounding.small + antialiasing: true + + // Add elevation for depth + Elevation { + anchors.fill: parent + radius: parent.radius + z: -1 + level: 3 + } + + StyledText { + id: tooltipText + + anchors.centerIn: parent + + text: root.text + color: Colours.palette.m3onSurface + font.pointSize: Appearance.font.size.small + } + } + + Component.onCompleted: { + if (tooltipVisible) { + updatePosition(); + } + } +} diff --git a/.github/components/effects/ColouredIcon.qml b/.github/components/effects/ColouredIcon.qml new file mode 100644 index 000000000..5ef4d4cc0 --- /dev/null +++ b/.github/components/effects/ColouredIcon.qml @@ -0,0 +1,35 @@ +pragma ComponentBehavior: Bound + +import Caelestia +import Quickshell.Widgets +import QtQuick + +IconImage { + id: root + + required property color colour + + asynchronous: true + + layer.enabled: true + layer.effect: Colouriser { + sourceColor: analyser.dominantColour + colorizationColor: root.colour + } + + layer.onEnabledChanged: { + if (layer.enabled && status === Image.Ready) + analyser.requestUpdate(); + } + + onStatusChanged: { + if (layer.enabled && status === Image.Ready) + analyser.requestUpdate(); + } + + ImageAnalyser { + id: analyser + + sourceItem: root + } +} diff --git a/.github/components/effects/Colouriser.qml b/.github/components/effects/Colouriser.qml new file mode 100644 index 000000000..2948155d6 --- /dev/null +++ b/.github/components/effects/Colouriser.qml @@ -0,0 +1,14 @@ +import ".." +import QtQuick +import QtQuick.Effects + +MultiEffect { + property color sourceColor: "black" + + colorization: 1 + brightness: 1 - sourceColor.hslLightness + + Behavior on colorizationColor { + CAnim {} + } +} diff --git a/.github/components/effects/Elevation.qml b/.github/components/effects/Elevation.qml new file mode 100644 index 000000000..fb29f16e8 --- /dev/null +++ b/.github/components/effects/Elevation.qml @@ -0,0 +1,18 @@ +import ".." +import qs.services +import QtQuick +import QtQuick.Effects + +RectangularShadow { + property int level + property real dp: [0, 1, 3, 6, 8, 12][level] + + color: Qt.alpha(Colours.palette.m3shadow, 0.7) + blur: (dp * 5) ** 0.7 + spread: -dp * 0.3 + (dp * 0.1) ** 2 + offset.y: dp / 2 + + Behavior on dp { + Anim {} + } +} diff --git a/.github/components/effects/InnerBorder.qml b/.github/components/effects/InnerBorder.qml new file mode 100644 index 000000000..d4a751f84 --- /dev/null +++ b/.github/components/effects/InnerBorder.qml @@ -0,0 +1,44 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Effects + +StyledRect { + property alias innerRadius: maskInner.radius + property alias thickness: maskInner.anchors.margins + property alias leftThickness: maskInner.anchors.leftMargin + property alias topThickness: maskInner.anchors.topMargin + property alias rightThickness: maskInner.anchors.rightMargin + property alias bottomThickness: maskInner.anchors.bottomMargin + + anchors.fill: parent + color: Colours.tPalette.m3surfaceContainer + + layer.enabled: true + layer.effect: MultiEffect { + maskSource: mask + maskEnabled: true + maskInverted: true + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + + Item { + id: mask + + anchors.fill: parent + layer.enabled: true + visible: false + + Rectangle { + id: maskInner + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + radius: Appearance.rounding.small + } + } +} diff --git a/.github/components/effects/OpacityMask.qml b/.github/components/effects/OpacityMask.qml new file mode 100644 index 000000000..22e424960 --- /dev/null +++ b/.github/components/effects/OpacityMask.qml @@ -0,0 +1,9 @@ +import Quickshell +import QtQuick + +ShaderEffect { + required property Item source + required property Item maskSource + + fragmentShader: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/shaders/opacitymask.frag.qsb`) +} diff --git a/.github/components/filedialog/CurrentItem.qml b/.github/components/filedialog/CurrentItem.qml new file mode 100644 index 000000000..bb87133c7 --- /dev/null +++ b/.github/components/filedialog/CurrentItem.qml @@ -0,0 +1,102 @@ +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Shapes + +Item { + id: root + + required property var currentItem + + implicitWidth: content.implicitWidth + Appearance.padding.larger + content.anchors.rightMargin + implicitHeight: currentItem ? content.implicitHeight + Appearance.padding.normal + content.anchors.bottomMargin : 0 + + Shape { + preferredRendererType: Shape.CurveRenderer + + ShapePath { + id: path + + readonly property real rounding: Appearance.rounding.small + readonly property bool flatten: root.implicitHeight < rounding * 2 + readonly property real roundingY: flatten ? root.implicitHeight / 2 : rounding + + strokeWidth: -1 + fillColor: Colours.tPalette.m3surfaceContainer + + startX: root.implicitWidth + startY: root.implicitHeight + + PathLine { + relativeX: -(root.implicitWidth + path.rounding) + relativeY: 0 + } + PathArc { + relativeX: path.rounding + relativeY: -path.roundingY + radiusX: path.rounding + radiusY: Math.min(path.rounding, root.implicitHeight) + direction: PathArc.Counterclockwise + } + PathLine { + relativeX: 0 + relativeY: -(root.implicitHeight - path.roundingY * 2) + } + PathArc { + relativeX: path.rounding + relativeY: -path.roundingY + radiusX: path.rounding + radiusY: Math.min(path.rounding, root.implicitHeight) + } + PathLine { + relativeX: root.implicitHeight > 0 ? root.implicitWidth - path.rounding * 2 : root.implicitWidth + relativeY: 0 + } + PathArc { + relativeX: path.rounding + relativeY: -path.rounding + radiusX: path.rounding + radiusY: path.rounding + direction: PathArc.Counterclockwise + } + + Behavior on fillColor { + CAnim {} + } + } + } + + Item { + anchors.fill: parent + clip: true + + StyledText { + id: content + + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.rightMargin: Appearance.padding.larger - Appearance.padding.small + anchors.bottomMargin: Appearance.padding.normal - Appearance.padding.small + + Connections { + target: root + + function onCurrentItemChanged(): void { + if (root.currentItem) + content.text = qsTr(`"%1" selected`).arg(root.currentItem.modelData.name); + } + } + } + } + + Behavior on implicitWidth { + enabled: !!root.currentItem + + Anim {} + } + + Behavior on implicitHeight { + Anim {} + } +} diff --git a/.github/components/filedialog/DialogButtons.qml b/.github/components/filedialog/DialogButtons.qml new file mode 100644 index 000000000..bde9ac277 --- /dev/null +++ b/.github/components/filedialog/DialogButtons.qml @@ -0,0 +1,93 @@ +import ".." +import qs.services +import qs.config +import QtQuick.Layouts + +StyledRect { + id: root + + required property var dialog + required property FolderContents folder + + implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 + + color: Colours.tPalette.m3surfaceContainer + + RowLayout { + id: inner + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Filter:") + } + + StyledRect { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.rightMargin: Appearance.spacing.normal + + color: Colours.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.small + + StyledText { + anchors.fill: parent + anchors.margins: Appearance.padding.normal + + text: `${root.dialog.filterLabel} (${root.dialog.filters.map(f => `*.${f}`).join(", ")})` + } + } + + StyledRect { + color: Colours.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.small + + implicitWidth: cancelText.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: cancelText.implicitHeight + Appearance.padding.normal * 2 + + StateLayer { + disabled: !root.dialog.selectionValid + + function onClicked(): void { + root.dialog.accepted(root.folder.currentItem.modelData.path); + } + } + + StyledText { + id: selectText + + anchors.centerIn: parent + anchors.margins: Appearance.padding.normal + + text: qsTr("Select") + color: root.dialog.selectionValid ? Colours.palette.m3onSurface : Colours.palette.m3outline + } + } + + StyledRect { + color: Colours.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.small + + implicitWidth: cancelText.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: cancelText.implicitHeight + Appearance.padding.normal * 2 + + StateLayer { + function onClicked(): void { + root.dialog.rejected(); + } + } + + StyledText { + id: cancelText + + anchors.centerIn: parent + anchors.margins: Appearance.padding.normal + + text: qsTr("Cancel") + } + } + } +} diff --git a/.github/components/filedialog/FileDialog.qml b/.github/components/filedialog/FileDialog.qml new file mode 100644 index 000000000..f3187a55b --- /dev/null +++ b/.github/components/filedialog/FileDialog.qml @@ -0,0 +1,102 @@ +pragma ComponentBehavior: Bound + +import qs.components +import qs.services +import Quickshell +import QtQuick +import QtQuick.Layouts + +LazyLoader { + id: loader + + property list cwd: ["Home"] + property string filterLabel: "All files" + property list filters: ["*"] + property string title: qsTr("Select a file") + + signal accepted(path: string) + signal rejected + + function open(): void { + activeAsync = true; + } + + function close(): void { + rejected(); + } + + onAccepted: activeAsync = false + onRejected: activeAsync = false + + FloatingWindow { + id: root + + property list cwd: loader.cwd + property string filterLabel: loader.filterLabel + property list filters: loader.filters + + readonly property bool selectionValid: { + const file = folderContents.currentItem?.modelData; + return (file && !file.isDir && (filters.includes("*") || filters.includes(file.suffix))) ?? false; + } + + function accepted(path: string): void { + loader.accepted(path); + } + + function rejected(): void { + loader.rejected(); + } + + implicitWidth: 1000 + implicitHeight: 600 + color: Colours.tPalette.m3surface + title: loader.title + + onVisibleChanged: { + if (!visible) + rejected(); + } + + RowLayout { + anchors.fill: parent + + spacing: 0 + + Sidebar { + Layout.fillHeight: true + dialog: root + } + + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + spacing: 0 + + HeaderBar { + Layout.fillWidth: true + dialog: root + } + + FolderContents { + id: folderContents + + Layout.fillWidth: true + Layout.fillHeight: true + dialog: root + } + + DialogButtons { + Layout.fillWidth: true + dialog: root + folder: folderContents + } + } + } + + Behavior on color { + CAnim {} + } + } +} diff --git a/.github/components/filedialog/FolderContents.qml b/.github/components/filedialog/FolderContents.qml new file mode 100644 index 000000000..e16c7a15c --- /dev/null +++ b/.github/components/filedialog/FolderContents.qml @@ -0,0 +1,228 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../controls" +import "../images" +import qs.services +import qs.config +import qs.utils +import Caelestia.Models +import Quickshell +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects + +Item { + id: root + + required property var dialog + property alias currentItem: view.currentItem + + StyledRect { + anchors.fill: parent + color: Colours.tPalette.m3surfaceContainer + + layer.enabled: true + layer.effect: MultiEffect { + maskSource: mask + maskEnabled: true + maskInverted: true + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } + } + + Item { + id: mask + + anchors.fill: parent + layer.enabled: true + visible: false + + Rectangle { + anchors.fill: parent + anchors.margins: Appearance.padding.small + radius: Appearance.rounding.small + } + } + + Loader { + anchors.centerIn: parent + + opacity: view.count === 0 ? 1 : 0 + active: opacity > 0 + + sourceComponent: ColumnLayout { + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "scan_delete" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.extraLarge * 2 + font.weight: 500 + } + + StyledText { + text: qsTr("This folder is empty") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + } + + Behavior on opacity { + Anim {} + } + } + + GridView { + id: view + + anchors.fill: parent + anchors.margins: Appearance.padding.small + Appearance.padding.normal + + cellWidth: Sizes.itemWidth + Appearance.spacing.small + cellHeight: Sizes.itemWidth + Appearance.spacing.small * 2 + Appearance.padding.normal * 2 + 1 + + clip: true + focus: true + currentIndex: -1 + Keys.onEscapePressed: currentIndex = -1 + + Keys.onReturnPressed: { + if (root.dialog.selectionValid) + root.dialog.accepted(currentItem.modelData.path); + } + Keys.onEnterPressed: { + if (root.dialog.selectionValid) + root.dialog.accepted(currentItem.modelData.path); + } + + StyledScrollBar.vertical: StyledScrollBar { + flickable: view + } + + model: FileSystemModel { + path: { + if (root.dialog.cwd[0] === "Home") + return `${Paths.home}/${root.dialog.cwd.slice(1).join("/")}`; + else + return root.dialog.cwd.join("/"); + } + onPathChanged: view.currentIndex = -1 + } + + delegate: StyledRect { + id: item + + required property int index + required property FileSystemEntry modelData + + readonly property real nonAnimHeight: icon.implicitHeight + name.anchors.topMargin + name.implicitHeight + Appearance.padding.normal * 2 + + implicitWidth: Sizes.itemWidth + implicitHeight: nonAnimHeight + + radius: Appearance.rounding.normal + color: Qt.alpha(Colours.tPalette.m3surfaceContainerHighest, GridView.isCurrentItem ? Colours.tPalette.m3surfaceContainerHighest.a : 0) + z: GridView.isCurrentItem || implicitHeight !== nonAnimHeight ? 1 : 0 + clip: true + + StateLayer { + onDoubleClicked: { + if (item.modelData.isDir) + root.dialog.cwd.push(item.modelData.name); + else if (root.dialog.selectionValid) + root.dialog.accepted(item.modelData.path); + } + + function onClicked(): void { + view.currentIndex = item.index; + } + } + + CachingIconImage { + id: icon + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: Appearance.padding.normal + + implicitSize: Sizes.itemWidth - Appearance.padding.normal * 2 + + Component.onCompleted: { + const file = item.modelData; + if (file.isImage) + source = Qt.resolvedUrl(file.path); + else if (!file.isDir) + source = Quickshell.iconPath(file.mimeType.replace("/", "-"), "application-x-zerosize"); + else if (root.dialog.cwd.length === 1 && ["Desktop", "Documents", "Downloads", "Music", "Pictures", "Public", "Templates", "Videos"].includes(file.name)) + source = Quickshell.iconPath(`folder-${file.name.toLowerCase()}`); + else + source = Quickshell.iconPath("inode-directory"); + } + } + + StyledText { + id: name + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: icon.bottom + anchors.topMargin: Appearance.spacing.small + anchors.margins: Appearance.padding.normal + + horizontalAlignment: Text.AlignHCenter + elide: item.GridView.isCurrentItem ? Text.ElideNone : Text.ElideRight + wrapMode: item.GridView.isCurrentItem ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap + + Component.onCompleted: text = item.modelData.name + } + + Behavior on implicitHeight { + Anim {} + } + } + + add: Transition { + Anim { + properties: "opacity,scale" + from: 0 + to: 1 + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + remove: Transition { + Anim { + property: "opacity" + to: 0 + } + Anim { + property: "scale" + to: 0.5 + } + } + + displaced: Transition { + Anim { + properties: "opacity,scale" + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + Anim { + properties: "x,y" + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + } + + CurrentItem { + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Appearance.padding.small + + currentItem: view.currentItem + } +} diff --git a/.github/components/filedialog/HeaderBar.qml b/.github/components/filedialog/HeaderBar.qml new file mode 100644 index 000000000..c9a3feb53 --- /dev/null +++ b/.github/components/filedialog/HeaderBar.qml @@ -0,0 +1,139 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + required property var dialog + + implicitWidth: inner.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 + + color: Colours.tPalette.m3surfaceContainer + + RowLayout { + id: inner + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + spacing: Appearance.spacing.small + + Item { + implicitWidth: implicitHeight + implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2 + + StateLayer { + radius: Appearance.rounding.small + disabled: root.dialog.cwd.length === 1 + + function onClicked(): void { + root.dialog.cwd.pop(); + } + } + + MaterialIcon { + id: upIcon + + anchors.centerIn: parent + text: "drive_folder_upload" + color: root.dialog.cwd.length === 1 ? Colours.palette.m3outline : Colours.palette.m3onSurface + grade: 200 + } + } + + StyledRect { + Layout.fillWidth: true + + radius: Appearance.rounding.small + color: Colours.tPalette.m3surfaceContainerHigh + + implicitHeight: pathComponents.implicitHeight + pathComponents.anchors.margins * 2 + + RowLayout { + id: pathComponents + + anchors.fill: parent + anchors.margins: Appearance.padding.small / 2 + anchors.leftMargin: 0 + + spacing: Appearance.spacing.small + + Repeater { + model: root.dialog.cwd + + RowLayout { + id: folder + + required property string modelData + required property int index + + spacing: 0 + + Loader { + Layout.rightMargin: Appearance.spacing.small + active: folder.index > 0 + sourceComponent: StyledText { + text: "/" + color: Colours.palette.m3onSurfaceVariant + font.bold: true + } + } + + Item { + implicitWidth: homeIcon.implicitWidth + (homeIcon.active ? Appearance.padding.small : 0) + folderName.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: folderName.implicitHeight + Appearance.padding.small * 2 + + Loader { + anchors.fill: parent + active: folder.index < root.dialog.cwd.length - 1 + sourceComponent: StateLayer { + radius: Appearance.rounding.small + + function onClicked(): void { + root.dialog.cwd = root.dialog.cwd.slice(0, folder.index + 1); + } + } + } + + Loader { + id: homeIcon + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Appearance.padding.normal + + active: folder.index === 0 && folder.modelData === "Home" + sourceComponent: MaterialIcon { + text: "home" + color: root.dialog.cwd.length === 1 ? Colours.palette.m3onSurface : Colours.palette.m3onSurfaceVariant + fill: 1 + } + } + + StyledText { + id: folderName + + anchors.left: homeIcon.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: homeIcon.active ? Appearance.padding.small : 0 + + text: folder.modelData + color: folder.index < root.dialog.cwd.length - 1 ? Colours.palette.m3onSurfaceVariant : Colours.palette.m3onSurface + font.bold: true + } + } + } + } + + Item { + Layout.fillWidth: true + } + } + } + } +} diff --git a/.github/components/filedialog/Sidebar.qml b/.github/components/filedialog/Sidebar.qml new file mode 100644 index 000000000..b55d7b379 --- /dev/null +++ b/.github/components/filedialog/Sidebar.qml @@ -0,0 +1,113 @@ +pragma ComponentBehavior: Bound + +import ".." +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + required property var dialog + + implicitWidth: Sizes.sidebarWidth + implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 + + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: inner + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.margins: Appearance.padding.normal + spacing: Appearance.spacing.small / 2 + + StyledText { + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Appearance.padding.small / 2 + Layout.bottomMargin: Appearance.spacing.normal + text: qsTr("Files") + color: Colours.palette.m3onSurface + font.pointSize: Appearance.font.size.larger + font.bold: true + } + + Repeater { + model: ["Home", "Downloads", "Desktop", "Documents", "Music", "Pictures", "Videos"] + + StyledRect { + id: place + + required property string modelData + readonly property bool selected: modelData === root.dialog.cwd[root.dialog.cwd.length - 1] + + Layout.fillWidth: true + implicitHeight: placeInner.implicitHeight + Appearance.padding.normal * 2 + + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3secondaryContainer, selected ? 1 : 0) + + StateLayer { + color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + + function onClicked(): void { + if (place.modelData === "Home") + root.dialog.cwd = ["Home"]; + else + root.dialog.cwd = ["Home", place.modelData]; + } + } + + RowLayout { + id: placeInner + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + anchors.leftMargin: Appearance.padding.large + anchors.rightMargin: Appearance.padding.large + + spacing: Appearance.spacing.normal + + MaterialIcon { + text: { + const p = place.modelData; + if (p === "Home") + return "home"; + if (p === "Downloads") + return "file_download"; + if (p === "Desktop") + return "desktop_windows"; + if (p === "Documents") + return "description"; + if (p === "Music") + return "music_note"; + if (p === "Pictures") + return "image"; + if (p === "Videos") + return "video_library"; + return "folder"; + } + color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + font.pointSize: Appearance.font.size.large + fill: place.selected ? 1 : 0 + + Behavior on fill { + Anim {} + } + } + + StyledText { + Layout.fillWidth: true + text: place.modelData + color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + font.pointSize: Appearance.font.size.normal + elide: Text.ElideRight + } + } + } + } + } +} diff --git a/.github/components/filedialog/Sizes.qml b/.github/components/filedialog/Sizes.qml new file mode 100644 index 000000000..2ad31f9f6 --- /dev/null +++ b/.github/components/filedialog/Sizes.qml @@ -0,0 +1,8 @@ +pragma Singleton + +import Quickshell + +Singleton { + property int itemWidth: 103 + property int sidebarWidth: 200 +} diff --git a/.github/components/images/CachingIconImage.qml b/.github/components/images/CachingIconImage.qml new file mode 100644 index 000000000..1acc6a181 --- /dev/null +++ b/.github/components/images/CachingIconImage.qml @@ -0,0 +1,42 @@ +pragma ComponentBehavior: Bound + +import qs.utils +import Quickshell.Widgets +import QtQuick + +Item { + id: root + + readonly property int status: loader.item?.status ?? Image.Null + readonly property real actualSize: Math.min(width, height) + property real implicitSize + property url source + + implicitWidth: implicitSize + implicitHeight: implicitSize + + Loader { + id: loader + + anchors.fill: parent + sourceComponent: root.source ? root.source.toString().startsWith("image://icon/") ? iconImage : cachingImage : null + } + + Component { + id: cachingImage + + CachingImage { + path: Paths.toLocalFile(root.source) + fillMode: Image.PreserveAspectFit + } + } + + Component { + id: iconImage + + IconImage { + source: root.source + asynchronous: true + } + } +} diff --git a/.github/components/images/CachingImage.qml b/.github/components/images/CachingImage.qml new file mode 100644 index 000000000..e8f957a7d --- /dev/null +++ b/.github/components/images/CachingImage.qml @@ -0,0 +1,28 @@ +import qs.utils +import Caelestia.Internal +import Quickshell +import QtQuick + +Image { + id: root + + property alias path: manager.path + + asynchronous: true + fillMode: Image.PreserveAspectCrop + + Connections { + target: QsWindow.window + + function onDevicePixelRatioChanged(): void { + manager.updateSource(); + } + } + + CachingImageManager { + id: manager + + item: root + cacheDir: Qt.resolvedUrl(Paths.imagecache) + } +} diff --git a/.github/components/misc/CustomShortcut.qml b/.github/components/misc/CustomShortcut.qml new file mode 100644 index 000000000..aa35ed8f5 --- /dev/null +++ b/.github/components/misc/CustomShortcut.qml @@ -0,0 +1,5 @@ +import Quickshell.Hyprland + +GlobalShortcut { + appid: "caelestia" +} diff --git a/.github/components/misc/Ref.qml b/.github/components/misc/Ref.qml new file mode 100644 index 000000000..0a694a472 --- /dev/null +++ b/.github/components/misc/Ref.qml @@ -0,0 +1,8 @@ +import QtQuick + +QtObject { + required property var service + + Component.onCompleted: service.refCount++ + Component.onDestruction: service.refCount-- +} diff --git a/.github/components/widgets/ExtraIndicator.qml b/.github/components/widgets/ExtraIndicator.qml new file mode 100644 index 000000000..db73ea08f --- /dev/null +++ b/.github/components/widgets/ExtraIndicator.qml @@ -0,0 +1,51 @@ +import ".." +import "../effects" +import qs.services +import qs.config +import QtQuick + +StyledRect { + required property int extra + + anchors.right: parent.right + anchors.margins: Appearance.padding.normal + + color: Colours.palette.m3tertiary + radius: Appearance.rounding.small + + implicitWidth: count.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: count.implicitHeight + Appearance.padding.small * 2 + + opacity: extra > 0 ? 1 : 0 + scale: extra > 0 ? 1 : 0.5 + + Elevation { + anchors.fill: parent + radius: parent.radius + opacity: parent.opacity + z: -1 + level: 2 + } + + StyledText { + id: count + + anchors.centerIn: parent + animate: parent.opacity > 0 + text: qsTr("+%1").arg(parent.extra) + color: Colours.palette.m3onTertiary + } + + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + } + } + + Behavior on scale { + Anim { + duration: Appearance.anim.durations.expressiveFastSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + } + } +} diff --git a/.github/config/Appearance.qml b/.github/config/Appearance.qml new file mode 100644 index 000000000..241c21a78 --- /dev/null +++ b/.github/config/Appearance.qml @@ -0,0 +1,14 @@ +pragma Singleton + +import Quickshell + +Singleton { + // Literally just here to shorten accessing stuff :woe: + // Also kinda so I can keep accessing it with `Appearance.xxx` instead of `Config.appearance.xxx` + readonly property AppearanceConfig.Rounding rounding: Config.appearance.rounding + readonly property AppearanceConfig.Spacing spacing: Config.appearance.spacing + readonly property AppearanceConfig.Padding padding: Config.appearance.padding + readonly property AppearanceConfig.FontStuff font: Config.appearance.font + readonly property AppearanceConfig.Anim anim: Config.appearance.anim + readonly property AppearanceConfig.Transparency transparency: Config.appearance.transparency +} diff --git a/.github/config/AppearanceConfig.qml b/.github/config/AppearanceConfig.qml new file mode 100644 index 000000000..3d590dca2 --- /dev/null +++ b/.github/config/AppearanceConfig.qml @@ -0,0 +1,94 @@ +import Quickshell.Io + +JsonObject { + property Rounding rounding: Rounding {} + property Spacing spacing: Spacing {} + property Padding padding: Padding {} + property FontStuff font: FontStuff {} + property Anim anim: Anim {} + property Transparency transparency: Transparency {} + + component Rounding: JsonObject { + property real scale: 1 + property int small: 12 * scale + property int normal: 17 * scale + property int large: 25 * scale + property int full: 1000 * scale + } + + component Spacing: JsonObject { + property real scale: 1 + property int small: 7 * scale + property int smaller: 10 * scale + property int normal: 12 * scale + property int larger: 15 * scale + property int large: 20 * scale + } + + component Padding: JsonObject { + property real scale: 1 + property int small: 5 * scale + property int smaller: 7 * scale + property int normal: 10 * scale + property int larger: 12 * scale + property int large: 15 * scale + } + + component FontFamily: JsonObject { + property string sans: "Rubik" + property string mono: "CaskaydiaCove NF" + property string material: "Material Symbols Rounded" + property string clock: "Rubik" + } + + component FontSize: JsonObject { + property real scale: 1 + property int small: 11 * scale + property int smaller: 12 * scale + property int normal: 13 * scale + property int larger: 15 * scale + property int large: 18 * scale + property int extraLarge: 28 * scale + } + + component FontStuff: JsonObject { + property FontFamily family: FontFamily {} + property FontSize size: FontSize {} + } + + component AnimCurves: JsonObject { + property list emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1] + property list emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1] + property list emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1] + property list standard: [0.2, 0, 0, 1, 1, 1] + property list standardAccel: [0.3, 0, 1, 1, 1, 1] + property list standardDecel: [0, 0, 0, 1, 1, 1] + property list expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1] + property list expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1] + property list expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1] + } + + component AnimDurations: JsonObject { + property real scale: 1 + property int small: 200 * scale + property int normal: 400 * scale + property int large: 600 * scale + property int extraLarge: 1000 * scale + property int expressiveFastSpatial: 350 * scale + property int expressiveDefaultSpatial: 500 * scale + property int expressiveEffects: 200 * scale + } + + component Anim: JsonObject { + property real mediaGifSpeedAdjustment: 300 + property real sessionGifSpeed: 0.7 + property AnimCurves curves: AnimCurves {} + property AnimDurations durations: AnimDurations {} + } + + component Transparency: JsonObject { + property bool enabled: false + property real base: 0.85 + property real layers: 0.4 + } +} diff --git a/.github/config/BackgroundConfig.qml b/.github/config/BackgroundConfig.qml new file mode 100644 index 000000000..8383f5248 --- /dev/null +++ b/.github/config/BackgroundConfig.qml @@ -0,0 +1,37 @@ +import Quickshell.Io + +JsonObject { + property bool enabled: true + property bool wallpaperEnabled: true + property DesktopClock desktopClock: DesktopClock {} + property Visualiser visualiser: Visualiser {} + + component DesktopClock: JsonObject { + property bool enabled: false + property real scale: 1.0 + property string position: "bottom-right" + property bool invertColors: false + property DesktopClockBackground background: DesktopClockBackground {} + property DesktopClockShadow shadow: DesktopClockShadow {} + } + + component DesktopClockBackground: JsonObject { + property bool enabled: false + property real opacity: 0.7 + property bool blur: true + } + + component DesktopClockShadow: JsonObject { + property bool enabled: true + property real opacity: 0.7 + property real blur: 0.4 + } + + component Visualiser: JsonObject { + property bool enabled: false + property bool autoHide: true + property bool blur: false + property real rounding: 1 + property real spacing: 1 + } +} diff --git a/.github/config/BarConfig.qml b/.github/config/BarConfig.qml new file mode 100644 index 000000000..310344b11 --- /dev/null +++ b/.github/config/BarConfig.qml @@ -0,0 +1,127 @@ +import Quickshell.Io + +JsonObject { + property bool persistent: true + property bool showOnHover: true + property int dragThreshold: 20 + property ScrollActions scrollActions: ScrollActions {} + property Popouts popouts: Popouts {} + property Workspaces workspaces: Workspaces {} + property ActiveWindow activeWindow: ActiveWindow {} + property Tray tray: Tray {} + property Status status: Status {} + property Clock clock: Clock {} + property Sizes sizes: Sizes {} + property list excludedScreens: [] + + property list entries: [ + { + id: "logo", + enabled: true + }, + { + id: "workspaces", + enabled: true + }, + { + id: "spacer", + enabled: true + }, + { + id: "activeWindow", + enabled: true + }, + { + id: "spacer", + enabled: true + }, + { + id: "tray", + enabled: true + }, + { + id: "clock", + enabled: true + }, + { + id: "statusIcons", + enabled: true + }, + { + id: "power", + enabled: true + } + ] + + component ScrollActions: JsonObject { + property bool workspaces: true + property bool volume: true + property bool brightness: true + } + + component Popouts: JsonObject { + property bool activeWindow: true + property bool tray: true + property bool statusIcons: true + } + + component Workspaces: JsonObject { + property int shown: 5 + property bool activeIndicator: true + property bool occupiedBg: false + property bool showWindows: true + property bool showWindowsOnSpecialWorkspaces: showWindows + property int maxWindowIcons: 0 // 0 = unlimited + property bool activeTrail: false + property bool perMonitorWorkspaces: true + property string label: " " // if empty, will show workspace name's first letter + property string occupiedLabel: "󰮯" + property string activeLabel: "󰮯" + property string capitalisation: "preserve" // upper, lower, or preserve - relevant only if label is empty + property list specialWorkspaceIcons: [] + property list windowIcons: [ + { + regex: "steam(_app_(default|[0-9]+))?", + icon: "sports_esports" + } + ] + } + + component ActiveWindow: JsonObject { + property bool compact: false + property bool inverted: false + property bool showOnHover: true + } + + component Tray: JsonObject { + property bool background: false + property bool recolour: false + property bool compact: false + property list iconSubs: [] + property list hiddenIcons: [] + } + + component Status: JsonObject { + property bool showAudio: false + property bool showMicrophone: false + property bool showKbLayout: false + property bool showNetwork: true + property bool showWifi: true + property bool showBluetooth: true + property bool showBattery: true + property bool showLockStatus: true + } + + component Clock: JsonObject { + property bool showIcon: true + } + + component Sizes: JsonObject { + property int innerWidth: 40 + property int windowPreviewSize: 400 + property int trayMenuWidth: 300 + property int batteryWidth: 250 + property int networkWidth: 320 + property int kbLayoutWidth: 320 + } +} diff --git a/.github/config/BorderConfig.qml b/.github/config/BorderConfig.qml new file mode 100644 index 000000000..b15811fdd --- /dev/null +++ b/.github/config/BorderConfig.qml @@ -0,0 +1,6 @@ +import Quickshell.Io + +JsonObject { + property int thickness: Appearance.padding.normal + property int rounding: Appearance.rounding.large +} diff --git a/.github/config/Config.qml b/.github/config/Config.qml new file mode 100644 index 000000000..fa7ca4998 --- /dev/null +++ b/.github/config/Config.qml @@ -0,0 +1,476 @@ +pragma Singleton + +import qs.utils +import Caelestia +import Quickshell +import Quickshell.Io +import QtQuick + +Singleton { + id: root + + property alias appearance: adapter.appearance + property alias general: adapter.general + property alias background: adapter.background + property alias bar: adapter.bar + property alias border: adapter.border + property alias dashboard: adapter.dashboard + property alias controlCenter: adapter.controlCenter + property alias launcher: adapter.launcher + property alias notifs: adapter.notifs + property alias osd: adapter.osd + property alias session: adapter.session + property alias winfo: adapter.winfo + property alias lock: adapter.lock + property alias utilities: adapter.utilities + property alias sidebar: adapter.sidebar + property alias services: adapter.services + property alias paths: adapter.paths + + property bool recentlySaved: false + + // Public save function - call this to persist config changes + function save(): void { + saveTimer.restart(); + recentlySaved = true; + recentSaveCooldown.restart(); + } + + // Helper function to serialize the config object + function serializeConfig(): var { + return { + appearance: serializeAppearance(), + general: serializeGeneral(), + background: serializeBackground(), + bar: serializeBar(), + border: serializeBorder(), + dashboard: serializeDashboard(), + controlCenter: serializeControlCenter(), + launcher: serializeLauncher(), + notifs: serializeNotifs(), + osd: serializeOsd(), + session: serializeSession(), + winfo: serializeWinfo(), + lock: serializeLock(), + utilities: serializeUtilities(), + sidebar: serializeSidebar(), + services: serializeServices(), + paths: serializePaths() + }; + } + + function serializeAppearance(): var { + return { + rounding: { + scale: appearance.rounding.scale + }, + spacing: { + scale: appearance.spacing.scale + }, + padding: { + scale: appearance.padding.scale + }, + font: { + family: { + sans: appearance.font.family.sans, + mono: appearance.font.family.mono, + material: appearance.font.family.material, + clock: appearance.font.family.clock + }, + size: { + scale: appearance.font.size.scale + } + }, + anim: { + mediaGifSpeedAdjustment: 300, + sessionGifSpeed: 0.7, + durations: { + scale: appearance.anim.durations.scale + } + }, + transparency: { + enabled: appearance.transparency.enabled, + base: appearance.transparency.base, + layers: appearance.transparency.layers + } + }; + } + + function serializeGeneral(): var { + return { + logo: general.logo, + excludedScreens: general.excludedScreens, + apps: { + terminal: general.apps.terminal, + audio: general.apps.audio, + playback: general.apps.playback, + explorer: general.apps.explorer + }, + idle: { + lockBeforeSleep: general.idle.lockBeforeSleep, + inhibitWhenAudio: general.idle.inhibitWhenAudio, + timeouts: general.idle.timeouts + }, + battery: { + warnLevels: general.battery.warnLevels, + criticalLevel: general.battery.criticalLevel + } + }; + } + + function serializeBackground(): var { + return { + enabled: background.enabled, + wallpaperEnabled: background.wallpaperEnabled, + desktopClock: { + enabled: background.desktopClock.enabled, + scale: background.desktopClock.scale, + position: background.desktopClock.position, + invertColors: background.desktopClock.invertColors, + background: { + enabled: background.desktopClock.background.enabled, + opacity: background.desktopClock.background.opacity, + blur: background.desktopClock.background.blur + }, + shadow: { + enabled: background.desktopClock.shadow.enabled, + opacity: background.desktopClock.shadow.opacity, + blur: background.desktopClock.shadow.blur + } + }, + visualiser: { + enabled: background.visualiser.enabled, + autoHide: background.visualiser.autoHide, + blur: background.visualiser.blur, + rounding: background.visualiser.rounding, + spacing: background.visualiser.spacing + } + }; + } + + function serializeBar(): var { + return { + persistent: bar.persistent, + showOnHover: bar.showOnHover, + dragThreshold: bar.dragThreshold, + scrollActions: { + workspaces: bar.scrollActions.workspaces, + volume: bar.scrollActions.volume, + brightness: bar.scrollActions.brightness + }, + popouts: { + activeWindow: bar.popouts.activeWindow, + tray: bar.popouts.tray, + statusIcons: bar.popouts.statusIcons + }, + workspaces: { + shown: bar.workspaces.shown, + activeIndicator: bar.workspaces.activeIndicator, + occupiedBg: bar.workspaces.occupiedBg, + showWindows: bar.workspaces.showWindows, + showWindowsOnSpecialWorkspaces: bar.workspaces.showWindowsOnSpecialWorkspaces, + maxWindowIcons: bar.workspaces.maxWindowIcons, + activeTrail: bar.workspaces.activeTrail, + perMonitorWorkspaces: bar.workspaces.perMonitorWorkspaces, + label: bar.workspaces.label, + occupiedLabel: bar.workspaces.occupiedLabel, + activeLabel: bar.workspaces.activeLabel, + capitalisation: bar.workspaces.capitalisation, + specialWorkspaceIcons: bar.workspaces.specialWorkspaceIcons, + windowIcons: bar.workspaces.windowIcons + }, + activeWindow: { + compact: bar.activeWindow.compact, + inverted: bar.activeWindow.inverted, + showOnHover: bar.activeWindow.showOnHover + }, + tray: { + background: bar.tray.background, + recolour: bar.tray.recolour, + compact: bar.tray.compact, + iconSubs: bar.tray.iconSubs, + hiddenIcons: bar.tray.hiddenIcons + }, + status: { + showAudio: bar.status.showAudio, + showMicrophone: bar.status.showMicrophone, + showKbLayout: bar.status.showKbLayout, + showNetwork: bar.status.showNetwork, + showWifi: bar.status.showWifi, + showBluetooth: bar.status.showBluetooth, + showBattery: bar.status.showBattery, + showLockStatus: bar.status.showLockStatus + }, + clock: { + showIcon: bar.clock.showIcon + }, + entries: bar.entries, + excludedScreens: bar.excludedScreens + }; + } + + function serializeBorder(): var { + return { + thickness: border.thickness, + rounding: border.rounding + }; + } + + function serializeDashboard(): var { + return { + enabled: dashboard.enabled, + showOnHover: dashboard.showOnHover, + mediaUpdateInterval: dashboard.mediaUpdateInterval, + resourceUpdateInterval: dashboard.resourceUpdateInterval, + dragThreshold: dashboard.dragThreshold, + performance: { + showBattery: dashboard.performance.showBattery, + showGpu: dashboard.performance.showGpu, + showCpu: dashboard.performance.showCpu, + showMemory: dashboard.performance.showMemory, + showStorage: dashboard.performance.showStorage, + showNetwork: dashboard.performance.showNetwork + } + }; + } + + function serializeControlCenter(): var { + return {}; + } + + function serializeLauncher(): var { + return { + enabled: launcher.enabled, + showOnHover: launcher.showOnHover, + enableCategories: launcher.enableCategories ?? true, + maxShown: launcher.maxShown, + maxWallpapers: launcher.maxWallpapers, + specialPrefix: launcher.specialPrefix, + actionPrefix: launcher.actionPrefix, + enableDangerousActions: launcher.enableDangerousActions, + dragThreshold: launcher.dragThreshold, + vimKeybinds: launcher.vimKeybinds, + favouriteApps: launcher.favouriteApps, + hiddenApps: launcher.hiddenApps, + categories: launcher.categories, + useFuzzy: { + apps: launcher.useFuzzy.apps, + actions: launcher.useFuzzy.actions, + schemes: launcher.useFuzzy.schemes, + variants: launcher.useFuzzy.variants, + wallpapers: launcher.useFuzzy.wallpapers + }, + contextMenuMain: launcher.contextMenuMain, + contextMenuAdvanced: launcher.contextMenuAdvanced, + actions: launcher.actions + }; + } + + function serializeNotifs(): var { + return { + expire: notifs.expire, + defaultExpireTimeout: notifs.defaultExpireTimeout, + clearThreshold: notifs.clearThreshold, + expandThreshold: notifs.expandThreshold, + actionOnClick: notifs.actionOnClick, + groupPreviewNum: notifs.groupPreviewNum + }; + } + + function serializeOsd(): var { + return { + enabled: osd.enabled, + hideDelay: osd.hideDelay, + enableBrightness: osd.enableBrightness, + enableMicrophone: osd.enableMicrophone + }; + } + + function serializeSession(): var { + return { + enabled: session.enabled, + dragThreshold: session.dragThreshold, + vimKeybinds: session.vimKeybinds, + icons: { + logout: session.icons.logout, + shutdown: session.icons.shutdown, + hibernate: session.icons.hibernate, + reboot: session.icons.reboot + }, + commands: { + logout: session.commands.logout, + shutdown: session.commands.shutdown, + hibernate: session.commands.hibernate, + reboot: session.commands.reboot + } + }; + } + + function serializeWinfo(): var { + return {}; + } + + function serializeLock(): var { + return { + recolourLogo: lock.recolourLogo, + enableFprint: lock.enableFprint, + maxFprintTries: lock.maxFprintTries, + hideNotifs: lock.hideNotifs + }; + } + + function serializeUtilities(): var { + return { + enabled: utilities.enabled, + maxToasts: utilities.maxToasts, + toasts: { + configLoaded: utilities.toasts.configLoaded, + chargingChanged: utilities.toasts.chargingChanged, + gameModeChanged: utilities.toasts.gameModeChanged, + dndChanged: utilities.toasts.dndChanged, + audioOutputChanged: utilities.toasts.audioOutputChanged, + audioInputChanged: utilities.toasts.audioInputChanged, + capsLockChanged: utilities.toasts.capsLockChanged, + numLockChanged: utilities.toasts.numLockChanged, + kbLayoutChanged: utilities.toasts.kbLayoutChanged, + vpnChanged: utilities.toasts.vpnChanged, + nowPlaying: utilities.toasts.nowPlaying + }, + vpn: { + enabled: utilities.vpn.enabled, + provider: utilities.vpn.provider + }, + quickToggles: utilities.quickToggles + }; + } + + function serializeSidebar(): var { + return { + enabled: sidebar.enabled, + dragThreshold: sidebar.dragThreshold + }; + } + + function serializeServices(): var { + return { + weatherLocation: services.weatherLocation, + useFahrenheit: services.useFahrenheit, + useFahrenheitPerformance: services.useFahrenheitPerformance, + useTwelveHourClock: services.useTwelveHourClock, + gpuType: services.gpuType, + visualiserBars: services.visualiserBars, + audioIncrement: services.audioIncrement, + brightnessIncrement: services.brightnessIncrement, + maxVolume: services.maxVolume, + smartScheme: services.smartScheme, + defaultPlayer: services.defaultPlayer, + playerAliases: services.playerAliases + }; + } + + function serializePaths(): var { + return { + wallpaperDir: paths.wallpaperDir, + sessionGif: paths.sessionGif, + mediaGif: paths.mediaGif + }; + } + + ElapsedTimer { + id: timer + } + + Timer { + id: saveTimer + + interval: 500 + onTriggered: { + timer.restart(); + try { + // Parse current config to preserve structure and comments if possible + let config = {}; + try { + config = JSON.parse(fileView.text()); + } catch (e) { + // If parsing fails, start with empty object + config = {}; + } + + // Update config with current values + config = root.serializeConfig(); + + // Save to file with pretty printing + fileView.setText(JSON.stringify(config, null, 2)); + } catch (e) { + Toaster.toast(qsTr("Failed to serialize config"), e.message, "settings_alert", Toast.Error); + } + } + } + + Timer { + id: recentSaveCooldown + + interval: 2000 + onTriggered: { + root.recentlySaved = false; + } + } + + FileView { + id: fileView + + path: `${Paths.config}/shell.json` + watchChanges: true + onFileChanged: { + // Prevent reload loop - don't reload if we just saved + if (!root.recentlySaved) { + timer.restart(); + reload(); + } else { + // Self-initiated save - reload without toast + reload(); + } + } + onLoaded: { + try { + JSON.parse(text()); + const elapsed = timer.elapsedMs(); + // Only show toast for external changes (not our own saves) and when elapsed time is meaningful + if (adapter.utilities.toasts.configLoaded && !root.recentlySaved && elapsed > 0) { + Toaster.toast(qsTr("Config loaded"), qsTr("Config loaded in %1ms").arg(elapsed), "rule_settings"); + } else if (adapter.utilities.toasts.configLoaded && root.recentlySaved && elapsed > 0) { + Toaster.toast(qsTr("Config saved"), qsTr("Config reloaded in %1ms").arg(elapsed), "rule_settings"); + } + } catch (e) { + Toaster.toast(qsTr("Failed to load config"), e.message, "settings_alert", Toast.Error); + } + } + onLoadFailed: err => { + if (err !== FileViewError.FileNotFound) + Toaster.toast(qsTr("Failed to read config file"), FileViewError.toString(err), "settings_alert", Toast.Warning); + } + onSaveFailed: err => Toaster.toast(qsTr("Failed to save config"), FileViewError.toString(err), "settings_alert", Toast.Error) + + JsonAdapter { + id: adapter + + property AppearanceConfig appearance: AppearanceConfig {} + property GeneralConfig general: GeneralConfig {} + property BackgroundConfig background: BackgroundConfig {} + property BarConfig bar: BarConfig {} + property BorderConfig border: BorderConfig {} + property DashboardConfig dashboard: DashboardConfig {} + property ControlCenterConfig controlCenter: ControlCenterConfig {} + property LauncherConfig launcher: LauncherConfig {} + property NotifsConfig notifs: NotifsConfig {} + property OsdConfig osd: OsdConfig {} + property SessionConfig session: SessionConfig {} + property WInfoConfig winfo: WInfoConfig {} + property LockConfig lock: LockConfig {} + property UtilitiesConfig utilities: UtilitiesConfig {} + property SidebarConfig sidebar: SidebarConfig {} + property ServiceConfig services: ServiceConfig {} + property UserPaths paths: UserPaths {} + } + } +} diff --git a/.github/config/ControlCenterConfig.qml b/.github/config/ControlCenterConfig.qml new file mode 100644 index 000000000..a5889491d --- /dev/null +++ b/.github/config/ControlCenterConfig.qml @@ -0,0 +1,10 @@ +import Quickshell.Io + +JsonObject { + property Sizes sizes: Sizes {} + + component Sizes: JsonObject { + property real heightMult: 0.7 + property real ratio: 16 / 9 + } +} diff --git a/.github/config/DashboardConfig.qml b/.github/config/DashboardConfig.qml new file mode 100644 index 000000000..0a16cc1f9 --- /dev/null +++ b/.github/config/DashboardConfig.qml @@ -0,0 +1,40 @@ +import Quickshell.Io + +JsonObject { + property bool enabled: true + property bool showOnHover: true + property int mediaUpdateInterval: 500 + property int resourceUpdateInterval: 1000 + property int dragThreshold: 50 + property bool showDashboard: true + property bool showMedia: true + property bool showPerformance: true + property bool showWeather: true + property Sizes sizes: Sizes {} + property Performance performance: Performance {} + + component Performance: JsonObject { + property bool showBattery: true + property bool showGpu: true + property bool showCpu: true + property bool showMemory: true + property bool showStorage: true + property bool showNetwork: true + } + + component Sizes: JsonObject { + readonly property int tabIndicatorHeight: 3 + readonly property int tabIndicatorSpacing: 5 + readonly property int infoWidth: 200 + readonly property int infoIconSize: 25 + readonly property int dateTimeWidth: 110 + readonly property int mediaWidth: 200 + readonly property int mediaProgressSweep: 180 + readonly property int mediaProgressThickness: 8 + readonly property int resourceProgessThickness: 10 + readonly property int weatherWidth: 250 + readonly property int mediaCoverArtSize: 150 + readonly property int mediaVisualiserSize: 80 + readonly property int resourceSize: 200 + } +} diff --git a/.github/config/GeneralConfig.qml b/.github/config/GeneralConfig.qml new file mode 100644 index 000000000..0d1d1e191 --- /dev/null +++ b/.github/config/GeneralConfig.qml @@ -0,0 +1,61 @@ +import Quickshell.Io + +JsonObject { + property string logo: "" + property list excludedScreens: [] + property Apps apps: Apps {} + property Idle idle: Idle {} + property Battery battery: Battery {} + + component Apps: JsonObject { + property list terminal: ["foot"] + property list audio: ["pavucontrol"] + property list playback: ["mpv"] + property list explorer: ["thunar"] + } + + component Idle: JsonObject { + property bool lockBeforeSleep: true + property bool inhibitWhenAudio: true + property list timeouts: [ + { + timeout: 180, + idleAction: "lock" + }, + { + timeout: 300, + idleAction: "dpms off", + returnAction: "dpms on" + }, + { + timeout: 600, + idleAction: ["systemctl", "suspend-then-hibernate"] + } + ] + } + + component Battery: JsonObject { + property list warnLevels: [ + { + level: 20, + title: qsTr("Low battery"), + message: qsTr("You might want to plug in a charger"), + icon: "battery_android_frame_2" + }, + { + level: 10, + title: qsTr("Did you see the previous message?"), + message: qsTr("You should probably plug in a charger now"), + icon: "battery_android_frame_1" + }, + { + level: 5, + title: qsTr("Critical battery level"), + message: qsTr("PLUG THE CHARGER RIGHT NOW!!"), + icon: "battery_android_alert", + critical: true + }, + ] + property int criticalLevel: 3 + } +} diff --git a/.github/config/LauncherConfig.qml b/.github/config/LauncherConfig.qml new file mode 100644 index 000000000..283fb7c58 --- /dev/null +++ b/.github/config/LauncherConfig.qml @@ -0,0 +1,159 @@ +import Quickshell.Io + +JsonObject { + property bool enabled: true + property bool showOnHover: false + property bool enableCategories: true + property int maxShown: 7 + property int maxWallpapers: 9 // Warning: even numbers look bad + property string specialPrefix: "@" + property string actionPrefix: ">" + property bool enableDangerousActions: false // Allow actions that can cause losing data, like shutdown, reboot and logout + property int dragThreshold: 50 + property bool vimKeybinds: false + property list favouriteApps: [] + property list hiddenApps: [] + property list contextMenuMain: [] + property list contextMenuAdvanced: [] + property list categories: [ + { name: "Development", icon: "code", apps: [] }, + { name: "Graphics", icon: "palette", apps: [] }, + { name: "Communication", icon: "chat", apps: [] }, + { name: "Media", icon: "play_circle", apps: [] }, + { name: "Office", icon: "description", apps: [] }, + { name: "Games", icon: "sports_esports", apps: [] }, + { name: "Utilities", icon: "build", apps: [] } + ] + property UseFuzzy useFuzzy: UseFuzzy {} + property Sizes sizes: Sizes {} + + component UseFuzzy: JsonObject { + property bool apps: false + property bool actions: false + property bool schemes: false + property bool variants: false + property bool wallpapers: false + } + + component Sizes: JsonObject { + property int itemWidth: 600 + property int itemHeight: 57 + property int wallpaperWidth: 280 + property int wallpaperHeight: 200 + } + + property list actions: [ + { + name: "Calculator", + icon: "calculate", + description: "Do simple math equations (powered by Qalc)", + command: ["autocomplete", "calc"], + enabled: true, + dangerous: false + }, + { + name: "Scheme", + icon: "palette", + description: "Change the current colour scheme", + command: ["autocomplete", "scheme"], + enabled: true, + dangerous: false + }, + { + name: "Wallpaper", + icon: "image", + description: "Change the current wallpaper", + command: ["autocomplete", "wallpaper"], + enabled: true, + dangerous: false + }, + { + name: "Variant", + icon: "colors", + description: "Change the current scheme variant", + command: ["autocomplete", "variant"], + enabled: true, + dangerous: false + }, + { + name: "Transparency", + icon: "opacity", + description: "Change shell transparency", + command: ["autocomplete", "transparency"], + enabled: false, + dangerous: false + }, + { + name: "Random", + icon: "casino", + description: "Switch to a random wallpaper", + command: ["caelestia", "wallpaper", "-r"], + enabled: true, + dangerous: false + }, + { + name: "Light", + icon: "light_mode", + description: "Change the scheme to light mode", + command: ["setMode", "light"], + enabled: true, + dangerous: false + }, + { + name: "Dark", + icon: "dark_mode", + description: "Change the scheme to dark mode", + command: ["setMode", "dark"], + enabled: true, + dangerous: false + }, + { + name: "Shutdown", + icon: "power_settings_new", + description: "Shutdown the system", + command: ["systemctl", "poweroff"], + enabled: true, + dangerous: true + }, + { + name: "Reboot", + icon: "cached", + description: "Reboot the system", + command: ["systemctl", "reboot"], + enabled: true, + dangerous: true + }, + { + name: "Logout", + icon: "exit_to_app", + description: "Log out of the current session", + command: ["loginctl", "terminate-user", ""], + enabled: true, + dangerous: true + }, + { + name: "Lock", + icon: "lock", + description: "Lock the current session", + command: ["loginctl", "lock-session"], + enabled: true, + dangerous: false + }, + { + name: "Sleep", + icon: "bedtime", + description: "Suspend then hibernate", + command: ["systemctl", "suspend-then-hibernate"], + enabled: true, + dangerous: false + }, + { + name: "Settings", + icon: "settings", + description: "Configure the shell", + command: ["caelestia", "shell", "controlCenter", "open"], + enabled: true, + dangerous: false + } + ] +} diff --git a/.github/config/LockConfig.qml b/.github/config/LockConfig.qml new file mode 100644 index 000000000..d0a9fb357 --- /dev/null +++ b/.github/config/LockConfig.qml @@ -0,0 +1,15 @@ +import Quickshell.Io + +JsonObject { + property bool recolourLogo: false + property bool enableFprint: true + property int maxFprintTries: 3 + property bool hideNotifs: false + property Sizes sizes: Sizes {} + + component Sizes: JsonObject { + property real heightMult: 0.7 + property real ratio: 16 / 9 + property int centerWidth: 600 + } +} diff --git a/.github/config/NotifsConfig.qml b/.github/config/NotifsConfig.qml new file mode 100644 index 000000000..fa2db494e --- /dev/null +++ b/.github/config/NotifsConfig.qml @@ -0,0 +1,18 @@ +import Quickshell.Io + +JsonObject { + property bool expire: true + property int defaultExpireTimeout: 5000 + property real clearThreshold: 0.3 + property int expandThreshold: 20 + property bool actionOnClick: false + property int groupPreviewNum: 3 + property bool openExpanded: false // Show the notifichation in expanded state when opening + property Sizes sizes: Sizes {} + + component Sizes: JsonObject { + property int width: 400 + property int image: 41 + property int badge: 20 + } +} diff --git a/.github/config/OsdConfig.qml b/.github/config/OsdConfig.qml new file mode 100644 index 000000000..543fc41e5 --- /dev/null +++ b/.github/config/OsdConfig.qml @@ -0,0 +1,14 @@ +import Quickshell.Io + +JsonObject { + property bool enabled: true + property int hideDelay: 2000 + property bool enableBrightness: true + property bool enableMicrophone: false + property Sizes sizes: Sizes {} + + component Sizes: JsonObject { + property int sliderWidth: 30 + property int sliderHeight: 150 + } +} diff --git a/.github/config/ServiceConfig.qml b/.github/config/ServiceConfig.qml new file mode 100644 index 000000000..29600cc54 --- /dev/null +++ b/.github/config/ServiceConfig.qml @@ -0,0 +1,22 @@ +import Quickshell.Io +import QtQuick + +JsonObject { + property string weatherLocation: "" // A lat,long pair or empty for autodetection, e.g. "37.8267,-122.4233" + property bool useFahrenheit: [Locale.ImperialUSSystem, Locale.ImperialSystem].includes(Qt.locale().measurementSystem) + property bool useFahrenheitPerformance: [Locale.ImperialUSSystem, Locale.ImperialSystem].includes(Qt.locale().measurementSystem) + property bool useTwelveHourClock: Qt.locale().timeFormat(Locale.ShortFormat).toLowerCase().includes("a") + property string gpuType: "" + property int visualiserBars: 45 + property real audioIncrement: 0.1 + property real brightnessIncrement: 0.1 + property real maxVolume: 1.0 + property bool smartScheme: true + property string defaultPlayer: "Spotify" + property list playerAliases: [ + { + "from": "com.github.th_ch.youtube_music", + "to": "YT Music" + } + ] +} diff --git a/.github/config/SessionConfig.qml b/.github/config/SessionConfig.qml new file mode 100644 index 000000000..414f821a2 --- /dev/null +++ b/.github/config/SessionConfig.qml @@ -0,0 +1,29 @@ +import Quickshell.Io + +JsonObject { + property bool enabled: true + property int dragThreshold: 30 + property bool vimKeybinds: false + property Icons icons: Icons {} + property Commands commands: Commands {} + + property Sizes sizes: Sizes {} + + component Icons: JsonObject { + property string logout: "logout" + property string shutdown: "power_settings_new" + property string hibernate: "downloading" + property string reboot: "cached" + } + + component Commands: JsonObject { + property list logout: ["loginctl", "terminate-user", ""] + property list shutdown: ["systemctl", "poweroff"] + property list hibernate: ["systemctl", "hibernate"] + property list reboot: ["systemctl", "reboot"] + } + + component Sizes: JsonObject { + property int button: 80 + } +} diff --git a/.github/config/SidebarConfig.qml b/.github/config/SidebarConfig.qml new file mode 100644 index 000000000..a871562b9 --- /dev/null +++ b/.github/config/SidebarConfig.qml @@ -0,0 +1,11 @@ +import Quickshell.Io + +JsonObject { + property bool enabled: true + property int dragThreshold: 80 + property Sizes sizes: Sizes {} + + component Sizes: JsonObject { + property int width: 430 + } +} diff --git a/.github/config/UserPaths.qml b/.github/config/UserPaths.qml new file mode 100644 index 000000000..f8de26782 --- /dev/null +++ b/.github/config/UserPaths.qml @@ -0,0 +1,8 @@ +import qs.utils +import Quickshell.Io + +JsonObject { + property string wallpaperDir: `${Paths.pictures}/Wallpapers` + property string sessionGif: "root:/assets/kurukuru.gif" + property string mediaGif: "root:/assets/bongocat.gif" +} diff --git a/.github/config/UtilitiesConfig.qml b/.github/config/UtilitiesConfig.qml new file mode 100644 index 000000000..4d1dd6e8d --- /dev/null +++ b/.github/config/UtilitiesConfig.qml @@ -0,0 +1,66 @@ +import Quickshell.Io + +JsonObject { + property bool enabled: true + property int maxToasts: 4 + + property Sizes sizes: Sizes {} + property Toasts toasts: Toasts {} + property Vpn vpn: Vpn {} + + component Sizes: JsonObject { + property int width: 430 + property int toastWidth: 430 + } + + component Toasts: JsonObject { + property bool configLoaded: true + property bool chargingChanged: true + property bool gameModeChanged: true + property bool dndChanged: true + property bool audioOutputChanged: true + property bool audioInputChanged: true + property bool capsLockChanged: true + property bool numLockChanged: true + property bool kbLayoutChanged: true + property bool kbLimit: true + property bool vpnChanged: true + property bool nowPlaying: false + } + + component Vpn: JsonObject { + property bool enabled: false + property list provider: ["netbird"] + } + + property list quickToggles: [ + { + id: "wifi", + enabled: true + }, + { + id: "bluetooth", + enabled: true + }, + { + id: "mic", + enabled: true + }, + { + id: "settings", + enabled: true + }, + { + id: "gameMode", + enabled: true + }, + { + id: "dnd", + enabled: true + }, + { + id: "vpn", + enabled: false + } + ] +} diff --git a/.github/config/WInfoConfig.qml b/.github/config/WInfoConfig.qml new file mode 100644 index 000000000..502578075 --- /dev/null +++ b/.github/config/WInfoConfig.qml @@ -0,0 +1,10 @@ +import Quickshell.Io + +JsonObject { + property Sizes sizes: Sizes {} + + component Sizes: JsonObject { + property real heightMult: 0.7 + property real detailsWidth: 500 + } +} diff --git a/.github/extras/CMakeLists.txt b/.github/extras/CMakeLists.txt new file mode 100644 index 000000000..52fe17c54 --- /dev/null +++ b/.github/extras/CMakeLists.txt @@ -0,0 +1,9 @@ +# Version +add_executable(version version.cpp) +target_compile_definitions(version PRIVATE + PROJECT_NAME="${PROJECT_NAME}" + VERSION="${VERSION}" + GIT_REVISION="${GIT_REVISION}" + DISTRIBUTOR="${DISTRIBUTOR}" +) +install(TARGETS version DESTINATION ${INSTALL_LIBDIR}) diff --git a/.github/extras/version.cpp b/.github/extras/version.cpp new file mode 100644 index 000000000..d63434170 --- /dev/null +++ b/.github/extras/version.cpp @@ -0,0 +1,27 @@ +#include + +int main(int argc, char* argv[]) { + if (argc > 1) { + std::string arg = argv[1]; + + if (arg == "-t" || arg == "--terse") { + std::cout << PROJECT_NAME << std::endl; + std::cout << VERSION << std::endl; + std::cout << GIT_REVISION << std::endl; + std::cout << DISTRIBUTOR << std::endl; + } else if (arg == "-s" || arg == "--short") { + std::cout << PROJECT_NAME << " " << VERSION << ", revision " << GIT_REVISION + << ", distributed by: " << DISTRIBUTOR << std::endl; + } else { + std::cout << "Usage: " << argv[0] << " [-t | --terse] [-s | --short]" << std::endl; + return arg != "-h" && arg != "--help"; + } + } else { + std::cout << "Project: " << PROJECT_NAME << std::endl; + std::cout << "Version: " << VERSION << std::endl; + std::cout << "Git revision: " << GIT_REVISION << std::endl; + std::cout << "Distributor: " << DISTRIBUTOR << std::endl; + } + + return 0; +} diff --git a/README.md b/README.md index 0c96b29fa..8efc5cb9a 100644 --- a/README.md +++ b/README.md @@ -337,8 +337,6 @@ default, you must create it manually. "showOnHover": true }, "clock": { - "background": false, - "showDate": false, "showIcon": true }, "dragThreshold": 20, @@ -577,7 +575,57 @@ default, you must create it manually. }, "showOnHover": false, "favouriteApps": [], - "hiddenApps": [] + "hiddenApps": [], + "categories": [ + { + "name": "Development", + "icon": "code", + "apps": ["code-oss"] + }, + { + "name": "Graphics", + "icon": "palette", + "apps": [] + }, + { + "name": "Communication", + "icon": "chat", + "apps": ["vesktop", "discord"] + }, + { + "name": "Media", + "icon": "play_circle", + "apps": ["spotify", "obs"] + }, + { + "name": "Games", + "icon": "sports_esports", + "apps": ["steam"] + }, + { + "name": "Utilities", + "icon": "build", + "apps": ["obs"] + } + ], + "contextMenuMain": [ + {"launch": {"text": "Launch", "icon": "play_arrow", "bold": true}}, + {"terminal": {"parent": "launch"}}, + "separator", + "favorites", + "categories", + "hide", + "workspaces" + ], + "contextMenuAdvanced": [ + "open-path", + "desktop-file", + "separator", + {"custom-submenu": {"text": "Advanced Options", "icon": "settings"}}, + {"kill": {"parent": "custom-submenu"}}, + {"separator": {"parent": "custom-submenu"}}, + {"copy-exec": {"parent": "custom-submenu"}} + ] }, "lock": { "recolourLogo": false, @@ -600,8 +648,7 @@ default, you must create it manually. "paths": { "mediaGif": "root:/assets/bongocat.gif", "sessionGif": "root:/assets/kurukuru.gif", - "wallpaperDir": "~/Pictures/Wallpapers", - "lyricsDir": "~/Music/lyrics" + "wallpaperDir": "~/Pictures/Wallpapers" }, "services": { "audioIncrement": 0.1, diff --git a/modules/BatteryMonitor.qml b/modules/BatteryMonitor.qml index 0596a1aa6..d24cff274 100644 --- a/modules/BatteryMonitor.qml +++ b/modules/BatteryMonitor.qml @@ -1,8 +1,8 @@ -import QtQuick +import qs.config +import Caelestia import Quickshell import Quickshell.Services.UPower -import Caelestia -import qs.config +import QtQuick Scope { id: root @@ -10,6 +10,8 @@ Scope { readonly property list warnLevels: [...Config.general.battery.warnLevels].sort((a, b) => b.level - a.level) Connections { + target: UPower + function onOnBatteryChanged(): void { if (UPower.onBattery) { if (Config.utilities.toasts.chargingChanged) @@ -21,11 +23,11 @@ Scope { level.warned = false; } } - - target: UPower } Connections { + target: UPower.displayDevice + function onPercentageChanged(): void { if (!UPower.onBattery) return; @@ -43,8 +45,6 @@ Scope { hibernateTimer.start(); } } - - target: UPower.displayDevice } Timer { diff --git a/modules/IdleMonitors.qml b/modules/IdleMonitors.qml index 440dc704d..b7ce05843 100644 --- a/modules/IdleMonitors.qml +++ b/modules/IdleMonitors.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound import "lock" +import qs.config +import qs.services +import Caelestia.Internal import Quickshell import Quickshell.Wayland -import Caelestia.Internal -import qs.services -import qs.config Scope { id: root diff --git a/modules/Shortcuts.qml b/modules/Shortcuts.qml index d73e63511..3bf20a4f6 100644 --- a/modules/Shortcuts.qml +++ b/modules/Shortcuts.qml @@ -1,9 +1,9 @@ -import Quickshell -import Quickshell.Io -import Caelestia import qs.components.misc -import qs.services import qs.modules.controlcenter +import qs.services +import Caelestia +import Quickshell +import Quickshell.Io Scope { id: root @@ -11,17 +11,13 @@ Scope { property bool launcherInterrupted readonly property bool hasFullscreen: Hypr.focusedWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "controlCenter" description: "Open control center" onPressed: WindowFactory.create() } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "showall" description: "Toggle launcher, dashboard and osd" onPressed: { @@ -32,9 +28,7 @@ Scope { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "dashboard" description: "Toggle dashboard" onPressed: { @@ -45,9 +39,7 @@ Scope { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "session" description: "Toggle session menu" onPressed: { @@ -58,9 +50,7 @@ Scope { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "launcher" description: "Toggle launcher" onPressed: root.launcherInterrupted = false @@ -73,17 +63,14 @@ Scope { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "launcherInterrupt" description: "Interrupt launcher keybind" onPressed: root.launcherInterrupted = true } - // qmllint disable unresolved-type + CustomShortcut { - // qmllint enable unresolved-type name: "sidebar" description: "Toggle sidebar" onPressed: { @@ -94,9 +81,7 @@ Scope { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "utilities" description: "Toggle utilities" onPressed: { @@ -108,6 +93,8 @@ Scope { } IpcHandler { + target: "drawers" + function toggle(drawer: string): void { if (list().split("\n").includes(drawer)) { if (root.hasFullscreen && ["launcher", "session", "dashboard"].includes(drawer)) @@ -123,19 +110,19 @@ Scope { const visibilities = Visibilities.getForActive(); return Object.keys(visibilities).filter(k => typeof visibilities[k] === "boolean").join("\n"); } - - target: "drawers" } IpcHandler { + target: "controlCenter" + function open(): void { WindowFactory.create(); } - - target: "controlCenter" } IpcHandler { + target: "toaster" + function info(title: string, message: string, icon: string): void { Toaster.toast(title, message, icon, Toast.Info); } @@ -151,7 +138,5 @@ Scope { function error(title: string, message: string, icon: string): void { Toaster.toast(title, message, icon, Toast.Error); } - - target: "toaster" } } diff --git a/modules/areapicker/AreaPicker.qml b/modules/areapicker/AreaPicker.qml index 76cc10399..308b7d232 100644 --- a/modules/areapicker/AreaPicker.qml +++ b/modules/areapicker/AreaPicker.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound -import Quickshell -import Quickshell.Io -import Quickshell.Wayland import qs.components.containers import qs.components.misc import qs.services +import Quickshell +import Quickshell.Wayland +import Quickshell.Io Scope { LazyLoader { @@ -48,6 +48,8 @@ Scope { } IpcHandler { + target: "picker" + function open(): void { root.freeze = false; root.closing = false; @@ -75,13 +77,9 @@ Scope { root.clipboardOnly = true; root.activeAsync = true; } - - target: "picker" } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "screenshot" description: "Open screenshot tool" onPressed: { @@ -92,9 +90,7 @@ Scope { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "screenshotFreeze" description: "Open screenshot tool (freeze mode)" onPressed: { @@ -105,9 +101,7 @@ Scope { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "screenshotClip" description: "Open screenshot tool (clipboard)" onPressed: { @@ -118,9 +112,7 @@ Scope { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "screenshotFreezeClip" description: "Open screenshot tool (freeze mode, clipboard)" onPressed: { diff --git a/modules/areapicker/Picker.qml b/modules/areapicker/Picker.qml index e6272737a..f4f4a3679 100644 --- a/modules/areapicker/Picker.qml +++ b/modules/areapicker/Picker.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Effects -import Quickshell -import Quickshell.Io -import Quickshell.Wayland -import Caelestia import qs.components import qs.services import qs.config +import Caelestia +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import QtQuick +import QtQuick.Effects MouseArea { id: root @@ -206,7 +206,6 @@ MouseArea { Loader { id: screencopy - asynchronous: true anchors.fill: parent active: root.loader.freeze diff --git a/modules/background/Background.qml b/modules/background/Background.qml index 95109e121..c1f149a00 100644 --- a/modules/background/Background.qml +++ b/modules/background/Background.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell -import Quickshell.Wayland +import qs.components import qs.components.containers import qs.services import qs.config +import Quickshell +import Quickshell.Wayland +import QtQuick Loader { - asynchronous: true active: Config.background.enabled sourceComponent: Variants { @@ -39,8 +39,6 @@ Loader { Loader { id: wallpaper - asynchronous: true - anchors.fill: parent active: Config.background.wallpaperEnabled @@ -56,8 +54,6 @@ Loader { Loader { id: clockLoader - - asynchronous: true active: Config.background.desktopClock.enabled anchors.margins: Appearance.padding.large * 2 @@ -67,7 +63,6 @@ Loader { states: [ State { name: "top-left" - AnchorChanges { target: clockLoader anchors.top: parent.top @@ -76,7 +71,6 @@ Loader { }, State { name: "top-center" - AnchorChanges { target: clockLoader anchors.top: parent.top @@ -85,7 +79,6 @@ Loader { }, State { name: "top-right" - AnchorChanges { target: clockLoader anchors.top: parent.top @@ -94,7 +87,6 @@ Loader { }, State { name: "middle-left" - AnchorChanges { target: clockLoader anchors.verticalCenter: parent.verticalCenter @@ -103,7 +95,6 @@ Loader { }, State { name: "middle-center" - AnchorChanges { target: clockLoader anchors.verticalCenter: parent.verticalCenter @@ -112,7 +103,6 @@ Loader { }, State { name: "middle-right" - AnchorChanges { target: clockLoader anchors.verticalCenter: parent.verticalCenter @@ -121,7 +111,6 @@ Loader { }, State { name: "bottom-left" - AnchorChanges { target: clockLoader anchors.bottom: parent.bottom @@ -130,7 +119,6 @@ Loader { }, State { name: "bottom-center" - AnchorChanges { target: clockLoader anchors.bottom: parent.bottom @@ -139,7 +127,6 @@ Loader { }, State { name: "bottom-right" - AnchorChanges { target: clockLoader anchors.bottom: parent.bottom diff --git a/modules/background/DesktopClock.qml b/modules/background/DesktopClock.qml index 86f9c623f..f9a06a2aa 100644 --- a/modules/background/DesktopClock.qml +++ b/modules/background/DesktopClock.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Effects -import QtQuick.Layouts import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects Item { id: root @@ -40,7 +40,6 @@ Item { } Loader { - asynchronous: true anchors.fill: parent active: root.blurEnabled @@ -102,7 +101,6 @@ Item { } Loader { - asynchronous: true Layout.alignment: Qt.AlignTop Layout.topMargin: Appearance.padding.large * 1.4 * root.scale diff --git a/modules/background/Visualiser.qml b/modules/background/Visualiser.qml index 780abdf4b..35a086b92 100644 --- a/modules/background/Visualiser.qml +++ b/modules/background/Visualiser.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Effects -import Quickshell -import Quickshell.Widgets -import Caelestia.Services import qs.components import qs.services import qs.config +import Caelestia.Services +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Effects Item { id: root @@ -21,7 +21,6 @@ Item { opacity: shouldBeActive ? 1 : 0 Loader { - asynchronous: true anchors.fill: parent active: root.opacity > 0 && Config.background.visualiser.blur @@ -43,7 +42,6 @@ Item { layer.enabled: true Loader { - asynchronous: true anchors.fill: parent anchors.topMargin: root.offset anchors.bottomMargin: -root.offset diff --git a/modules/background/Wallpaper.qml b/modules/background/Wallpaper.qml index 10d743baa..39a48fc8f 100644 --- a/modules/background/Wallpaper.qml +++ b/modules/background/Wallpaper.qml @@ -1,19 +1,18 @@ pragma ComponentBehavior: Bound -import QtQuick import qs.components -import qs.components.filedialog import qs.components.images +import qs.components.filedialog import qs.services import qs.config import qs.utils +import QtQuick Item { id: root property string source: Wallpapers.current property Image current: one - property bool completed onSourceChanged: { if (!source) @@ -26,17 +25,13 @@ Item { Component.onCompleted: { if (source) - Qt.callLater(() => { - one.update(); - completed = true; - }); + Qt.callLater(() => one.update()); } Loader { - asynchronous: true anchors.fill: parent - active: root.completed && !root.source + active: !root.source sourceComponent: StyledRect { color: Colours.palette.m3surfaceContainer @@ -79,12 +74,12 @@ Item { } StateLayer { + radius: parent.radius + color: Colours.palette.m3onPrimary + function onClicked(): void { dialog.open(); } - - radius: parent.radius - color: Colours.palette.m3onPrimary } StyledText { diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml index b82a17c74..95c166e6f 100644 --- a/modules/bar/Bar.qml +++ b/modules/bar/Bar.qml @@ -1,20 +1,19 @@ pragma ComponentBehavior: Bound +import qs.services +import qs.config import "popouts" as BarPopouts import "components" import "components/workspaces" +import Quickshell import QtQuick import QtQuick.Layouts -import Quickshell -import qs.components -import qs.services -import qs.config ColumnLayout { id: root required property ShellScreen screen - required property DrawerVisibilities visibilities + required property PersistentProperties visibilities required property BarPopouts.Wrapper popouts readonly property int vPadding: Appearance.padding.large @@ -23,9 +22,9 @@ ColumnLayout { return; for (let i = 0; i < repeater.count; i++) { - const loader = repeater.itemAt(i) as WrappedLoader; - if (loader?.enabled && loader.id === "tray") { - (loader.item as Tray).expanded = false; + const item = repeater.itemAt(i); + if (item?.enabled && item.id === "tray") { + item.item.expanded = false; } } } @@ -43,9 +42,11 @@ ColumnLayout { const id = ch.id; const top = ch.y; + const item = ch.item; + const itemHeight = item.implicitHeight; if (id === "statusIcons" && Config.bar.popouts.statusIcons) { - const items = (ch.item as StatusIcons).items; + const items = item.items; const icon = items.childAt(items.width / 2, mapToItem(items, 0, y).y); if (icon) { popouts.currentName = icon.name; @@ -53,10 +54,9 @@ ColumnLayout { popouts.hasCurrent = true; } } else if (id === "tray" && Config.bar.popouts.tray) { - const tray = ch.item as Tray; - if (!Config.bar.tray.compact || (tray.expanded && !tray.expandIcon.contains(mapToItem(tray.expandIcon, tray.implicitWidth / 2, y)))) { - const index = Math.floor(((y - top - tray.padding * 2 + tray.spacing) / tray.layout.implicitHeight) * tray.items.count); - const trayItem = tray.items.itemAt(index); + if (!Config.bar.tray.compact || (item.expanded && !item.expandIcon.contains(mapToItem(item.expandIcon, item.implicitWidth / 2, y)))) { + const index = Math.floor(((y - top - item.padding * 2 + item.spacing) / item.layout.implicitHeight) * item.items.count); + const trayItem = item.items.itemAt(index); if (trayItem) { popouts.currentName = `traymenu${index}`; popouts.currentCenter = Qt.binding(() => trayItem.mapToItem(root, 0, trayItem.implicitHeight / 2).y); @@ -66,11 +66,11 @@ ColumnLayout { } } else { popouts.hasCurrent = false; - tray.expanded = true; + item.expanded = true; } } else if (id === "activeWindow" && Config.bar.popouts.activeWindow && Config.bar.activeWindow.showOnHover) { popouts.currentName = id.toLowerCase(); - popouts.currentCenter = (ch.item as Item).mapToItem(root, 0, (ch.item as Item).implicitHeight / 2).y ?? 0; + popouts.currentCenter = item.mapToItem(root, 0, itemHeight / 2).y; popouts.hasCurrent = true; } } @@ -194,7 +194,6 @@ ColumnLayout { return null; } - asynchronous: true Layout.alignment: Qt.AlignHCenter // Cursed ahh thing to add padding to first and last enabled components diff --git a/modules/bar/BarWrapper.qml b/modules/bar/BarWrapper.qml index df29f0af1..29961b62c 100644 --- a/modules/bar/BarWrapper.qml +++ b/modules/bar/BarWrapper.qml @@ -1,20 +1,19 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell import qs.components import qs.config -import qs.modules.bar.popouts as BarPopouts +import "popouts" as BarPopouts +import Quickshell +import QtQuick Item { id: root required property ShellScreen screen - required property DrawerVisibilities visibilities + required property PersistentProperties visibilities required property BarPopouts.Wrapper popouts required property bool disabled - readonly property int clampedWidth: Math.max(Config.border.minThickness, implicitWidth) readonly property int padding: Math.max(Appearance.padding.smaller, Config.border.thickness) readonly property int contentWidth: Config.bar.sizes.innerWidth + padding * 2 readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentWidth : Config.border.thickness @@ -22,15 +21,15 @@ Item { property bool isHovered function closeTray(): void { - (content.item as Bar)?.closeTray(); + content.item?.closeTray(); } function checkPopout(y: real): void { - (content.item as Bar)?.checkPopout(y); + content.item?.checkPopout(y); } function handleWheel(y: real, angleDelta: point): void { - (content.item as Bar)?.handleWheel(y, angleDelta); + content.item?.handleWheel(y, angleDelta); } visible: width > Config.border.thickness @@ -82,7 +81,7 @@ Item { width: root.contentWidth screen: root.screen visibilities: root.visibilities - popouts: root.popouts // qmllint disable incompatible-type + popouts: root.popouts } } } diff --git a/modules/bar/components/ActiveWindow.qml b/modules/bar/components/ActiveWindow.qml index 0fa277ee4..414c9c579 100644 --- a/modules/bar/components/ActiveWindow.qml +++ b/modules/bar/components/ActiveWindow.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound -import QtQuick import qs.components import qs.services -import qs.config import qs.utils +import qs.config +import QtQuick Item { id: root @@ -19,7 +19,7 @@ Item { return qsTr("Desktop"); if (Config.bar.activeWindow.compact) { // " - " (standard hyphen), " — " (em dash), " – " (en dash) - const parts = title.split(/\s+[\-\u2013\u2014]\s+/); + const parts = root.windowTitle.split(/\s+[\-\u2013\u2014]\s+/); if (parts.length > 1) return parts[parts.length - 1].trim(); } @@ -39,7 +39,6 @@ Item { implicitHeight: icon.implicitHeight + current.implicitWidth + current.anchors.topMargin Loader { - asynchronous: true anchors.fill: parent active: !Config.bar.activeWindow.showOnHover diff --git a/modules/bar/components/Clock.qml b/modules/bar/components/Clock.qml index 90ed78cf1..801e93d77 100644 --- a/modules/bar/components/Clock.qml +++ b/modules/bar/components/Clock.qml @@ -1,71 +1,38 @@ pragma ComponentBehavior: Bound -import QtQuick import qs.components import qs.services import qs.config +import QtQuick -StyledRect { +Column { id: root - readonly property color colour: Colours.palette.m3tertiary - readonly property int padding: Config.bar.clock.background ? Appearance.padding.normal : Appearance.padding.small - - implicitWidth: Config.bar.sizes.innerWidth - implicitHeight: layout.implicitHeight + root.padding * 2 - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Config.bar.clock.background ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.full + property color colour: Colours.palette.m3tertiary - Column { - id: layout + spacing: Appearance.spacing.small - anchors.centerIn: parent - spacing: Appearance.spacing.small - - Loader { - asynchronous: true - anchors.horizontalCenter: parent.horizontalCenter - - active: Config.bar.clock.showIcon - visible: active - - sourceComponent: MaterialIcon { - text: "calendar_month" - color: root.colour - } - } + Loader { + anchors.horizontalCenter: parent.horizontalCenter - StyledText { - anchors.horizontalCenter: parent.horizontalCenter + active: Config.bar.clock.showIcon + visible: active - visible: Config.bar.clock.showDate - - horizontalAlignment: StyledText.AlignHCenter - text: Time.format("ddd\nd") - font.pointSize: Appearance.font.size.smaller - font.family: Appearance.font.family.sans + sourceComponent: MaterialIcon { + text: "calendar_month" color: root.colour } + } - Rectangle { - anchors.horizontalCenter: parent.horizontalCenter - visible: Config.bar.clock.showDate - height: visible ? 1 : 0 + StyledText { + id: text - width: parent.width * 0.8 - color: root.colour - opacity: 0.2 - } + anchors.horizontalCenter: parent.horizontalCenter - StyledText { - anchors.horizontalCenter: parent.horizontalCenter - - horizontalAlignment: StyledText.AlignHCenter - text: Time.format(Config.services.useTwelveHourClock ? "hh\nmm\nA" : "hh\nmm") - font.pointSize: Appearance.font.size.smaller - font.family: Appearance.font.family.mono - color: root.colour - } + horizontalAlignment: StyledText.AlignHCenter + text: Time.format(Config.services.useTwelveHourClock ? "hh\nmm\nA" : "hh\nmm") + font.pointSize: Appearance.font.size.smaller + font.family: Appearance.font.family.mono + color: root.colour } } diff --git a/modules/bar/components/OsIcon.qml b/modules/bar/components/OsIcon.qml index fc16a4fca..6710294a0 100644 --- a/modules/bar/components/OsIcon.qml +++ b/modules/bar/components/OsIcon.qml @@ -1,9 +1,9 @@ -import QtQuick import qs.components import qs.components.effects import qs.services import qs.config import qs.utils +import QtQuick Item { id: root @@ -21,7 +21,6 @@ Item { } Loader { - asynchronous: true anchors.centerIn: parent sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon } diff --git a/modules/bar/components/Power.qml b/modules/bar/components/Power.qml index 5f5738115..917bdf7fd 100644 --- a/modules/bar/components/Power.qml +++ b/modules/bar/components/Power.qml @@ -1,27 +1,29 @@ -import QtQuick import qs.components import qs.services import qs.config +import Quickshell +import QtQuick Item { id: root - required property DrawerVisibilities visibilities + required property PersistentProperties visibilities implicitWidth: icon.implicitHeight + Appearance.padding.small * 2 implicitHeight: icon.implicitHeight StateLayer { // Cursed workaround to make the height larger than the parent - function onClicked(): void { - root.visibilities.session = !root.visibilities.session; - } - anchors.fill: undefined anchors.centerIn: parent implicitWidth: implicitHeight implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 + radius: Appearance.rounding.full + + function onClicked(): void { + root.visibilities.session = !root.visibilities.session; + } } MaterialIcon { diff --git a/modules/bar/components/Settings.qml b/modules/bar/components/Settings.qml index f6ed88af6..5d562cef1 100644 --- a/modules/bar/components/Settings.qml +++ b/modules/bar/components/Settings.qml @@ -1,8 +1,9 @@ -import QtQuick import qs.components +import qs.modules.controlcenter import qs.services import qs.config -import qs.modules.controlcenter +import Quickshell +import QtQuick Item { id: root @@ -12,17 +13,18 @@ Item { StateLayer { // Cursed workaround to make the height larger than the parent - function onClicked(): void { - WindowFactory.create(null, { - active: "network" - }); - } - anchors.fill: undefined anchors.centerIn: parent implicitWidth: implicitHeight implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 + radius: Appearance.rounding.full + + function onClicked(): void { + WindowFactory.create(null, { + active: "network" + }); + } } MaterialIcon { diff --git a/modules/bar/components/SettingsIcon.qml b/modules/bar/components/SettingsIcon.qml index f6ed88af6..5d562cef1 100644 --- a/modules/bar/components/SettingsIcon.qml +++ b/modules/bar/components/SettingsIcon.qml @@ -1,8 +1,9 @@ -import QtQuick import qs.components +import qs.modules.controlcenter import qs.services import qs.config -import qs.modules.controlcenter +import Quickshell +import QtQuick Item { id: root @@ -12,17 +13,18 @@ Item { StateLayer { // Cursed workaround to make the height larger than the parent - function onClicked(): void { - WindowFactory.create(null, { - active: "network" - }); - } - anchors.fill: undefined anchors.centerIn: parent implicitWidth: implicitHeight implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 + radius: Appearance.rounding.full + + function onClicked(): void { + WindowFactory.create(null, { + active: "network" + }); + } } MaterialIcon { diff --git a/modules/bar/components/StatusIcons.qml b/modules/bar/components/StatusIcons.qml index 455e04ef1..ca7dc2e3a 100644 --- a/modules/bar/components/StatusIcons.qml +++ b/modules/bar/components/StatusIcons.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Bluetooth -import Quickshell.Services.UPower import qs.components import qs.services -import qs.config import qs.utils +import qs.config +import Quickshell +import Quickshell.Bluetooth +import Quickshell.Services.UPower +import QtQuick +import QtQuick.Layouts StyledRect { id: root @@ -178,9 +178,9 @@ StyledRect { MaterialIcon { animate: true text: { - if (!Bluetooth.defaultAdapter?.enabled) // qmllint disable unresolved-type + if (!Bluetooth.defaultAdapter?.enabled) return "bluetooth_disabled"; - if (Bluetooth.devices.values.some(d => d.connected)) // qmllint disable unresolved-type + if (Bluetooth.devices.values.some(d => d.connected)) return "bluetooth_connected"; return "bluetooth"; } @@ -190,7 +190,7 @@ StyledRect { // Connected bluetooth devices Repeater { model: ScriptModel { - values: Bluetooth.devices.values.filter(d => d.state !== BluetoothDeviceState.Disconnected) // qmllint disable unresolved-type + values: Bluetooth.devices.values.filter(d => d.state !== BluetoothDeviceState.Disconnected) } MaterialIcon { @@ -204,7 +204,7 @@ StyledRect { fill: 1 SequentialAnimation on opacity { - running: device.modelData?.state !== BluetoothDeviceState.Connected // qmllint disable unresolved-type + running: device.modelData?.state !== BluetoothDeviceState.Connected alwaysRunToEnd: true loops: Animation.Infinite @@ -264,7 +264,6 @@ StyledRect { component WrappedLoader: Loader { required property string name - asynchronous: true Layout.alignment: Qt.AlignHCenter visible: active } diff --git a/modules/bar/components/Tray.qml b/modules/bar/components/Tray.qml index 79b57755e..7bafda16f 100644 --- a/modules/bar/components/Tray.qml +++ b/modules/bar/components/Tray.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell -import Quickshell.Services.SystemTray import qs.components import qs.services import qs.config +import Quickshell +import Quickshell.Services.SystemTray +import QtQuick StyledRect { id: root @@ -82,8 +82,6 @@ StyledRect { Loader { id: expandIcon - asynchronous: true - anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom diff --git a/modules/bar/components/TrayItem.qml b/modules/bar/components/TrayItem.qml index c5cb9fe82..99119073d 100644 --- a/modules/bar/components/TrayItem.qml +++ b/modules/bar/components/TrayItem.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell.Services.SystemTray import qs.components.effects import qs.services import qs.config import qs.utils +import Quickshell.Services.SystemTray +import QtQuick MouseArea { id: root diff --git a/modules/bar/components/workspaces/ActiveIndicator.qml b/modules/bar/components/workspaces/ActiveIndicator.qml index e8a52d4d4..dae54b371 100644 --- a/modules/bar/components/workspaces/ActiveIndicator.qml +++ b/modules/bar/components/workspaces/ActiveIndicator.qml @@ -1,8 +1,8 @@ -import QtQuick import qs.components import qs.components.effects import qs.services import qs.config +import QtQuick StyledRect { id: root @@ -20,12 +20,13 @@ StyledRect { property real leading: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.y ?? 0 : 0 property real trailing: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.y ?? 0 : 0 - property real currentSize: workspaces.count > 0 ? (workspaces.itemAt(currentWsIdx) as Workspace)?.size ?? 0 : 0 + property real currentSize: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.size ?? 0 : 0 property real offset: Math.min(leading, trailing) property real size: { const s = Math.abs(leading - trailing) + currentSize; if (Config.bar.workspaces.activeTrail && lastWs > currentWsIdx) { - const ws = workspaces.itemAt(lastWs) as Workspace; + const ws = workspaces.itemAt(lastWs); + // console.log(ws, lastWs); return ws ? Math.min(ws.y + ws.size - offset, s) : 0; } return s; diff --git a/modules/bar/components/workspaces/OccupiedBg.qml b/modules/bar/components/workspaces/OccupiedBg.qml index 3f0871603..56b215e67 100644 --- a/modules/bar/components/workspaces/OccupiedBg.qml +++ b/modules/bar/components/workspaces/OccupiedBg.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell import qs.components import qs.services import qs.config +import Quickshell +import QtQuick Item { id: root @@ -52,8 +52,8 @@ Item { required property var modelData - readonly property Workspace start: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.start)) ?? null : null // qmllint disable incompatible-type - readonly property Workspace end: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.end)) ?? null : null // qmllint disable incompatible-type + readonly property Workspace start: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.start)) ?? null : null + readonly property Workspace end: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.end)) ?? null : null function getWsIdx(ws: int): int { let i = ws - 1; @@ -90,14 +90,14 @@ Item { } } + component Pill: QtObject { + property int start + property int end + } + Component { id: pillComp Pill {} } - - component Pill: QtObject { - property int start - property int end - } } diff --git a/modules/bar/components/workspaces/SpecialWorkspaces.qml b/modules/bar/components/workspaces/SpecialWorkspaces.qml index cef8a9906..cd3572be2 100644 --- a/modules/bar/components/workspaces/SpecialWorkspaces.qml +++ b/modules/bar/components/workspaces/SpecialWorkspaces.qml @@ -1,21 +1,21 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Hyprland import qs.components import qs.components.effects import qs.services -import qs.config import qs.utils +import qs.config +import Quickshell +import Quickshell.Hyprland +import QtQuick +import QtQuick.Layouts Item { id: root required property ShellScreen screen readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen) - readonly property string activeSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? monitor : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name ?? "" + readonly property string activeSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? monitor : Hypr.focusedMonitor)?.lastIpcObject?.specialWorkspace?.name ?? "" layer.enabled: true layer.effect: OpacityMask { @@ -105,14 +105,147 @@ Item { highlightFollowsCurrentItem: false highlight: Item { y: view.currentItem?.y ?? 0 - implicitHeight: (view.currentItem as SpecialWsDelegate)?.size ?? 0 + implicitHeight: view.currentItem?.size ?? 0 Behavior on y { Anim {} } } - delegate: SpecialWsDelegate {} + delegate: ColumnLayout { + id: ws + + required property HyprlandWorkspace modelData + readonly property int size: label.Layout.preferredHeight + (hasWindows ? windows.implicitHeight + Appearance.padding.small : 0) + property int wsId + property string icon + property bool hasWindows + + anchors.left: view.contentItem.left + anchors.right: view.contentItem.right + + spacing: 0 + + Component.onCompleted: { + wsId = modelData.id; + icon = Icons.getSpecialWsIcon(modelData.name); + hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && modelData.lastIpcObject.windows > 0; + } + + // Hacky thing cause modelData gets destroyed before the remove anim finishes + Connections { + target: ws.modelData + + function onIdChanged(): void { + if (ws.modelData) + ws.wsId = ws.modelData.id; + } + + function onNameChanged(): void { + if (ws.modelData) + ws.icon = Icons.getSpecialWsIcon(ws.modelData.name); + } + + function onLastIpcObjectChanged(): void { + if (ws.modelData) + ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; + } + } + + Connections { + target: Config.bar.workspaces + + function onShowWindowsOnSpecialWorkspacesChanged(): void { + if (ws.modelData) + ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; + } + } + + Loader { + id: label + + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + Layout.preferredHeight: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 + + sourceComponent: ws.icon.length === 1 ? letterComp : iconComp + + Component { + id: iconComp + + MaterialIcon { + fill: 1 + text: ws.icon + verticalAlignment: Qt.AlignVCenter + } + } + + Component { + id: letterComp + + StyledText { + text: ws.icon + verticalAlignment: Qt.AlignVCenter + } + } + } + + Loader { + id: windows + + Layout.alignment: Qt.AlignHCenter + Layout.fillHeight: true + Layout.preferredHeight: implicitHeight + + visible: active + active: ws.hasWindows + + sourceComponent: Column { + spacing: 0 + + add: Transition { + Anim { + properties: "scale" + from: 0 + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + } + + move: Transition { + Anim { + properties: "scale" + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + Anim { + properties: "x,y" + } + } + + Repeater { + model: ScriptModel { + values: { + const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === ws.wsId); + const maxIcons = Config.bar.workspaces.maxWindowIcons; + return maxIcons > 0 ? windows.slice(0, maxIcons) : windows; + } + } + + MaterialIcon { + required property var modelData + + grade: 0 + text: Icons.getAppCategoryIcon(modelData.lastIpcObject.class, "terminal") + color: Colours.palette.m3onSurfaceVariant + } + } + } + + Behavior on Layout.preferredHeight { + Anim {} + } + } + } add: Transition { Anim { @@ -160,7 +293,6 @@ Item { } Loader { - asynchronous: true active: Config.bar.workspaces.activeIndicator anchors.fill: parent @@ -172,7 +304,7 @@ Item { anchors.right: parent.right y: (view.currentItem?.y ?? 0) - view.contentY - implicitHeight: (view.currentItem as SpecialWsDelegate)?.size ?? 0 + implicitHeight: view.currentItem?.size ?? 0 color: Colours.palette.m3tertiary radius: Appearance.rounding.full @@ -221,150 +353,11 @@ Item { if (Math.abs(event.y - startY) > drag.threshold) return; - const ws = view.itemAt(event.x, event.y) as SpecialWsDelegate; + const ws = view.itemAt(event.x, event.y); if (ws?.modelData) Hypr.dispatch(`togglespecialworkspace ${ws.modelData.name.slice(8)}`); else Hypr.dispatch("togglespecialworkspace special"); } } - - component SpecialWsDelegate: ColumnLayout { - id: ws - - required property HyprlandWorkspace modelData - readonly property int size: label.Layout.preferredHeight + (hasWindows ? windows.implicitHeight + Appearance.padding.small : 0) - property int wsId - property string icon - property bool hasWindows - - anchors.left: view.contentItem.left - anchors.right: view.contentItem.right - - spacing: 0 - - Component.onCompleted: { - wsId = modelData.id; - icon = Icons.getSpecialWsIcon(modelData.name); - hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && modelData.lastIpcObject.windows > 0; - } - - // Hacky thing cause modelData gets destroyed before the remove anim finishes - Connections { - function onIdChanged(): void { - if (ws.modelData) - ws.wsId = ws.modelData.id; - } - - function onNameChanged(): void { - if (ws.modelData) - ws.icon = Icons.getSpecialWsIcon(ws.modelData.name); - } - - function onLastIpcObjectChanged(): void { - if (ws.modelData) - ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; - } - - target: ws.modelData - } - - Connections { - function onShowWindowsOnSpecialWorkspacesChanged(): void { - if (ws.modelData) - ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; - } - - target: Config.bar.workspaces - } - - Loader { - id: label - - asynchronous: true - - Layout.alignment: Qt.AlignHCenter | Qt.AlignTop - Layout.preferredHeight: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 - - sourceComponent: ws.icon.length === 1 ? letterComp : iconComp - - Component { - id: iconComp - - MaterialIcon { - fill: 1 - text: ws.icon - verticalAlignment: Qt.AlignVCenter - } - } - - Component { - id: letterComp - - StyledText { - text: ws.icon - verticalAlignment: Qt.AlignVCenter - } - } - } - - Loader { - id: windows - - asynchronous: true - - Layout.alignment: Qt.AlignHCenter - Layout.fillHeight: true - Layout.preferredHeight: implicitHeight - - visible: active - active: ws.hasWindows - - sourceComponent: Column { - spacing: 0 - - add: Transition { - Anim { - properties: "scale" - from: 0 - to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel - } - } - - move: Transition { - Anim { - properties: "scale" - to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel - } - Anim { - properties: "x,y" - } - } - - Repeater { - model: ScriptModel { - values: { - const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === ws.wsId); - const maxIcons = Config.bar.workspaces.maxWindowIcons; - return maxIcons > 0 ? windows.slice(0, maxIcons) : windows; - } - } - - MaterialIcon { - required property var modelData - - grade: 0 - text: Icons.getAppCategoryIcon(modelData.lastIpcObject.class, "terminal") - color: Colours.palette.m3onSurfaceVariant - } - } - } - - Behavior on Layout.preferredHeight { - Anim {} - } - } - } } diff --git a/modules/bar/components/workspaces/Workspace.qml b/modules/bar/components/workspaces/Workspace.qml index bfbdc2ac1..f6e767ef9 100644 --- a/modules/bar/components/workspaces/Workspace.qml +++ b/modules/bar/components/workspaces/Workspace.qml @@ -1,12 +1,10 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components import qs.services -import qs.config import qs.utils +import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root @@ -57,8 +55,6 @@ ColumnLayout { Loader { id: windows - asynchronous: true - Layout.alignment: Qt.AlignHCenter Layout.fillHeight: true Layout.topMargin: -Config.bar.sizes.innerWidth / 10 @@ -92,8 +88,7 @@ ColumnLayout { Repeater { model: ScriptModel { values: { - const ws = root.ws; - const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === ws); + const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === root.ws); const maxIcons = Config.bar.workspaces.maxWindowIcons; return maxIcons > 0 ? windows.slice(0, maxIcons) : windows; } diff --git a/modules/bar/components/workspaces/Workspaces.qml b/modules/bar/components/workspaces/Workspaces.qml index f205dfac0..b9fe87faf 100644 --- a/modules/bar/components/workspaces/Workspaces.qml +++ b/modules/bar/components/workspaces/Workspaces.qml @@ -1,19 +1,19 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Effects -import QtQuick.Layouts -import Quickshell -import qs.components import qs.services import qs.config +import qs.components +import Quickshell +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects StyledClippingRect { id: root required property ShellScreen screen - readonly property bool onSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name !== "" + readonly property bool onSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor)?.lastIpcObject?.specialWorkspace?.name !== "" readonly property int activeWsId: Config.bar.workspaces.perMonitorWorkspaces ? (Hypr.monitorFor(screen).activeWorkspace?.id ?? 1) : Hypr.activeWsId readonly property var occupied: { @@ -45,7 +45,6 @@ StyledClippingRect { } Loader { - asynchronous: true active: Config.bar.workspaces.occupiedBg anchors.fill: parent @@ -78,7 +77,6 @@ StyledClippingRect { } Loader { - asynchronous: true anchors.horizontalCenter: parent.horizontalCenter active: Config.bar.workspaces.activeIndicator @@ -92,7 +90,7 @@ StyledClippingRect { MouseArea { anchors.fill: layout onClicked: event => { - const ws = (layout.childAt(event.x, event.y) as Workspace)?.ws; + const ws = layout.childAt(event.x, event.y).ws; if (Hypr.activeWsId !== ws) Hypr.dispatch(`workspace ${ws}`); else @@ -112,8 +110,6 @@ StyledClippingRect { Loader { id: specialWs - asynchronous: true - anchors.fill: parent anchors.margins: Appearance.padding.small diff --git a/modules/bar/popouts/ActiveWindow.qml b/modules/bar/popouts/ActiveWindow.qml index 7e4bd60a0..adf7b7740 100644 --- a/modules/bar/popouts/ActiveWindow.qml +++ b/modules/bar/popouts/ActiveWindow.qml @@ -1,16 +1,16 @@ -import QtQuick -import QtQuick.Layouts -import Quickshell.Wayland -import Quickshell.Widgets import qs.components import qs.services -import qs.config import qs.utils +import qs.config +import Quickshell.Widgets +import Quickshell.Wayland +import QtQuick +import QtQuick.Layouts Item { id: root - required property PopoutState popouts + required property Item wrapper implicitWidth: Hypr.activeToplevel ? child.implicitWidth : -Appearance.padding.large * 2 implicitHeight: child.implicitHeight @@ -31,7 +31,6 @@ Item { IconImage { id: icon - asynchronous: true Layout.alignment: Qt.AlignVCenter implicitSize: details.implicitHeight source: Icons.getAppIcon(Hypr.activeToplevel?.lastIpcObject.class ?? "", "image-missing") @@ -65,11 +64,11 @@ Item { Layout.alignment: Qt.AlignVCenter StateLayer { + radius: Appearance.rounding.normal + function onClicked(): void { - root.popouts.detachRequested("winfo"); + root.wrapper.detach("winfo"); } - - radius: Appearance.rounding.normal } MaterialIcon { @@ -92,7 +91,7 @@ Item { ScreencopyView { id: preview - captureSource: Hypr.activeToplevel?.wayland ?? null // qmllint disable unresolved-type + captureSource: Hypr.activeToplevel?.wayland ?? null live: visible constraintSize.width: Config.bar.sizes.windowPreviewSize diff --git a/modules/bar/popouts/Audio.qml b/modules/bar/popouts/Audio.qml index 52ac3b806..58b29ba8d 100644 --- a/modules/bar/popouts/Audio.qml +++ b/modules/bar/popouts/Audio.qml @@ -1,18 +1,20 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell.Services.Pipewire import qs.components import qs.components.controls import qs.services import qs.config +import Quickshell +import Quickshell.Services.Pipewire +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import "../../controlcenter/network" Item { id: root - required property PopoutState popouts + required property var wrapper implicitWidth: layout.implicitWidth + Appearance.padding.normal * 2 implicitHeight: layout.implicitHeight + Appearance.padding.normal * 2 @@ -112,7 +114,7 @@ Item { text: qsTr("Open settings") icon: "settings" - onClicked: root.popouts.detachRequested("audio") + onClicked: root.wrapper.detach("audio") } } } diff --git a/modules/bar/popouts/Background.qml b/modules/bar/popouts/Background.qml index cfba86d3a..075b69881 100644 --- a/modules/bar/popouts/Background.qml +++ b/modules/bar/popouts/Background.qml @@ -1,8 +1,8 @@ -import QtQuick -import QtQuick.Shapes import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Shapes ShapePath { id: root diff --git a/modules/bar/popouts/Battery.qml b/modules/bar/popouts/Battery.qml index 86d903d24..ac975e1b7 100644 --- a/modules/bar/popouts/Battery.qml +++ b/modules/bar/popouts/Battery.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell.Services.UPower import qs.components import qs.services import qs.config +import Quickshell.Services.UPower +import QtQuick Column { id: root @@ -37,12 +37,11 @@ Column { } Loader { - asynchronous: true anchors.horizontalCenter: parent.horizontalCenter active: PowerProfiles.degradationReason !== PerformanceDegradationReason.None - height: active ? ((item as Item)?.implicitHeight ?? 0) : 0 + height: active ? (item?.implicitHeight ?? 0) : 0 sourceComponent: StyledRect { implicitWidth: child.implicitWidth + Appearance.padding.normal * 2 @@ -205,12 +204,12 @@ Column { implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 StateLayer { + radius: Appearance.rounding.full + color: profiles.current === parent.icon ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + function onClicked(): void { PowerProfiles.profile = parent.profile; } - - radius: Appearance.rounding.full - color: profiles.current === parent.icon ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface } MaterialIcon { diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml index 2e87270a1..676da82f5 100644 --- a/modules/bar/popouts/Bluetooth.qml +++ b/modules/bar/popouts/Bluetooth.qml @@ -1,19 +1,20 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Bluetooth import qs.components import qs.components.controls import qs.services import qs.config import qs.utils +import Quickshell +import Quickshell.Bluetooth +import QtQuick +import QtQuick.Layouts +import "../../controlcenter/network" ColumnLayout { id: root - required property PopoutState popouts + required property Item wrapper spacing: Appearance.spacing.small @@ -26,9 +27,9 @@ ColumnLayout { Toggle { label: qsTr("Enabled") - checked: Bluetooth.defaultAdapter?.enabled ?? false // qmllint disable unresolved-type + checked: Bluetooth.defaultAdapter?.enabled ?? false toggle.onToggled: { - const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type + const adapter = Bluetooth.defaultAdapter; if (adapter) adapter.enabled = checked; } @@ -36,9 +37,9 @@ ColumnLayout { Toggle { label: qsTr("Discovering") - checked: Bluetooth.defaultAdapter?.discovering ?? false // qmllint disable unresolved-type + checked: Bluetooth.defaultAdapter?.discovering ?? false toggle.onToggled: { - const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type + const adapter = Bluetooth.defaultAdapter; if (adapter) adapter.discovering = checked; } @@ -48,7 +49,7 @@ ColumnLayout { Layout.topMargin: Appearance.spacing.small Layout.rightMargin: Appearance.padding.small text: { - const devices = Bluetooth.devices.values; // qmllint disable unresolved-type + const devices = Bluetooth.devices.values; let available = qsTr("%1 device%2 available").arg(devices.length).arg(devices.length === 1 ? "" : "s"); const connected = devices.filter(d => d.connected).length; if (connected > 0) @@ -61,14 +62,14 @@ ColumnLayout { Repeater { model: ScriptModel { - values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired) || a.name.localeCompare(b.name)).slice(0, 5) // qmllint disable unresolved-type + values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired) || a.name.localeCompare(b.name)).slice(0, 5) } RowLayout { id: device required property BluetoothDevice modelData - readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting // qmllint disable unresolved-type + readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting Layout.fillWidth: true Layout.rightMargin: Appearance.padding.small @@ -108,7 +109,7 @@ ColumnLayout { implicitHeight: connectIcon.implicitHeight + Appearance.padding.small radius: Appearance.rounding.full - color: Qt.alpha(Colours.palette.m3primary, device.modelData.state === BluetoothDeviceState.Connected ? 1 : 0) // qmllint disable unresolved-type + color: Qt.alpha(Colours.palette.m3primary, device.modelData.state === BluetoothDeviceState.Connected ? 1 : 0) CircularIndicator { anchors.fill: parent @@ -116,12 +117,12 @@ ColumnLayout { } StateLayer { + color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + disabled: device.loading + function onClicked(): void { device.modelData.connected = !device.modelData.connected; } - - color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface // qmllint disable unresolved-type - disabled: device.loading } MaterialIcon { @@ -130,7 +131,7 @@ ColumnLayout { anchors.centerIn: parent animate: true text: device.modelData.connected ? "link_off" : "link" - color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface // qmllint disable unresolved-type + color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface opacity: device.loading ? 0 : 1 @@ -141,18 +142,17 @@ ColumnLayout { } Loader { - asynchronous: true active: device.modelData.bonded sourceComponent: Item { implicitWidth: connectBtn.implicitWidth implicitHeight: connectBtn.implicitHeight StateLayer { + radius: Appearance.rounding.full + function onClicked(): void { device.modelData.forget(); } - - radius: Appearance.rounding.full } MaterialIcon { @@ -173,7 +173,7 @@ ColumnLayout { text: qsTr("Open settings") icon: "settings" - onClicked: root.popouts.detachRequested("bluetooth") + onClicked: root.wrapper.detach("bluetooth") } component Toggle: RowLayout { diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index 1c4792e1e..6543e584d 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -1,16 +1,17 @@ pragma ComponentBehavior: Bound -import "./kblayout" -import QtQuick -import Quickshell -import Quickshell.Services.SystemTray import qs.components import qs.config +import Quickshell +import Quickshell.Services.SystemTray +import QtQuick + +import "./kblayout" Item { id: root - required property PopoutState popouts + required property Item wrapper readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null readonly property Item current: currentPopout?.item ?? null @@ -28,16 +29,15 @@ Item { Popout { name: "activewindow" sourceComponent: ActiveWindow { - popouts: root.popouts + wrapper: root.wrapper } } Popout { id: networkPopout - name: "network" sourceComponent: Network { - popouts: root.popouts + wrapper: root.wrapper view: "wireless" } } @@ -45,64 +45,60 @@ Item { Popout { name: "ethernet" sourceComponent: Network { - popouts: root.popouts + wrapper: root.wrapper view: "ethernet" } } Popout { id: passwordPopout - name: "wirelesspassword" sourceComponent: WirelessPassword { id: passwordComponent - - popouts: root.popouts - network: (networkPopout.item as Network)?.passwordNetwork ?? null + wrapper: root.wrapper + network: networkPopout.item?.passwordNetwork ?? null } Connections { + target: root.wrapper function onCurrentNameChanged() { // Update network immediately when password popout becomes active - if (root.popouts.currentName === "wirelesspassword") { + if (root.wrapper.currentName === "wirelesspassword") { // Set network immediately if available - if ((networkPopout.item as Network)?.passwordNetwork) { + if (networkPopout.item && networkPopout.item.passwordNetwork) { if (passwordPopout.item) { - (passwordPopout.item as WirelessPassword).network = (networkPopout.item as Network).passwordNetwork; + passwordPopout.item.network = networkPopout.item.passwordNetwork; } } // Also try after a short delay in case networkPopout.item wasn't ready Qt.callLater(() => { - if (passwordPopout.item && (networkPopout.item as Network)?.passwordNetwork) { - (passwordPopout.item as WirelessPassword).network = (networkPopout.item as Network).passwordNetwork; + if (passwordPopout.item && networkPopout.item && networkPopout.item.passwordNetwork) { + passwordPopout.item.network = networkPopout.item.passwordNetwork; } }, 100); } } - - target: root.popouts } Connections { + target: networkPopout function onItemChanged() { // When network popout loads, update password popout if it's active - if (root.popouts.currentName === "wirelesspassword" && passwordPopout.item) { + if (root.wrapper.currentName === "wirelesspassword" && passwordPopout.item) { Qt.callLater(() => { - if ((networkPopout.item as Network)?.passwordNetwork) { - (passwordPopout.item as WirelessPassword).network = (networkPopout.item as Network).passwordNetwork; + if (networkPopout.item && networkPopout.item.passwordNetwork) { + passwordPopout.item.network = networkPopout.item.passwordNetwork; } }); } } - - target: networkPopout } } Popout { name: "bluetooth" sourceComponent: Bluetooth { - popouts: root.popouts + wrapper: root.wrapper } } @@ -114,13 +110,15 @@ Item { Popout { name: "audio" sourceComponent: Audio { - popouts: root.popouts + wrapper: root.wrapper } } Popout { name: "kblayout" - sourceComponent: KbLayout {} + sourceComponent: KbLayout { + wrapper: root.wrapper + } } Popout { @@ -143,22 +141,22 @@ Item { sourceComponent: trayMenuComp Connections { + target: root.wrapper + function onHasCurrentChanged(): void { - if (root.popouts.hasCurrent && trayMenu.shouldBeActive) { + if (root.wrapper.hasCurrent && trayMenu.shouldBeActive) { trayMenu.sourceComponent = null; trayMenu.sourceComponent = trayMenuComp; } } - - target: root.popouts } Component { id: trayMenuComp TrayMenu { - popouts: root.popouts - trayItem: trayMenu.modelData.menu // qmllint disable unresolved-type + popouts: root.wrapper + trayItem: trayMenu.modelData.menu } } } @@ -169,7 +167,7 @@ Item { id: popout required property string name - readonly property bool shouldBeActive: root.popouts.currentName === name + readonly property bool shouldBeActive: root.wrapper.currentName === name anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right diff --git a/modules/bar/popouts/LockStatus.qml b/modules/bar/popouts/LockStatus.qml index 9b61e0372..7d74530e3 100644 --- a/modules/bar/popouts/LockStatus.qml +++ b/modules/bar/popouts/LockStatus.qml @@ -1,7 +1,7 @@ -import QtQuick.Layouts import qs.components import qs.services import qs.config +import QtQuick.Layouts ColumnLayout { spacing: Appearance.spacing.small diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index 53350ee73..5b32e4a6e 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -1,18 +1,18 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components import qs.components.controls import qs.services import qs.config import qs.utils +import Quickshell +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root - required property PopoutState popouts + required property Item wrapper property string connectingToSsid: "" property string view: "wireless" // "wireless" or "ethernet" @@ -45,7 +45,7 @@ ColumnLayout { Layout.preferredHeight: visible ? implicitHeight : 0 Layout.topMargin: visible ? Appearance.spacing.small : 0 Layout.rightMargin: Appearance.padding.small - text: qsTr("%1 networks available").arg(Nmcli.networks.length) // qmllint disable missing-property + text: qsTr("%1 networks available").arg(Nmcli.networks.length) color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small } @@ -123,6 +123,9 @@ ColumnLayout { } StateLayer { + color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + disabled: networkItem.loading || !Nmcli.wifiEnabled + function onClicked(): void { if (networkItem.modelData.active) { Nmcli.disconnectFromNetwork(); @@ -132,16 +135,13 @@ ColumnLayout { // Password is required - show password dialog root.passwordNetwork = network; root.showPasswordDialog = true; - root.popouts.currentName = "wirelesspassword"; + root.wrapper.currentName = "wirelesspassword"; }); // Clear connecting state if connection succeeds immediately (saved profile) // This is handled by the onActiveChanged connection below } } - - color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - disabled: networkItem.loading || !Nmcli.wifiEnabled } MaterialIcon { @@ -173,12 +173,12 @@ ColumnLayout { color: Colours.palette.m3primaryContainer StateLayer { + color: Colours.palette.m3onPrimaryContainer + disabled: Nmcli.scanning || !Nmcli.wifiEnabled + function onClicked(): void { Nmcli.rescanWifi(); } - - color: Colours.palette.m3onPrimaryContainer - disabled: Nmcli.scanning || !Nmcli.wifiEnabled } RowLayout { @@ -303,6 +303,9 @@ ColumnLayout { } StateLayer { + color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + disabled: ethernetItem.loading + function onClicked(): void { if (ethernetItem.modelData.connected && ethernetItem.modelData.connection) { Nmcli.disconnectEthernet(ethernetItem.modelData.connection, () => {}); @@ -310,9 +313,6 @@ ColumnLayout { Nmcli.connectEthernet(ethernetItem.modelData.connection || "", ethernetItem.modelData.interface || "", () => {}); } } - - color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - disabled: ethernetItem.loading } MaterialIcon { @@ -334,6 +334,8 @@ ColumnLayout { } Connections { + target: Nmcli + function onActiveChanged(): void { if (Nmcli.active && root.connectingToSsid === Nmcli.active.ssid) { root.connectingToSsid = ""; @@ -341,8 +343,8 @@ ColumnLayout { if (root.showPasswordDialog && root.passwordNetwork && Nmcli.active.ssid === root.passwordNetwork.ssid) { root.showPasswordDialog = false; root.passwordNetwork = null; - if (root.popouts.currentName === "wirelesspassword") { - root.popouts.currentName = "network"; + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; } } } @@ -352,20 +354,17 @@ ColumnLayout { if (!Nmcli.scanning) scanIcon.rotation = 0; } - - target: Nmcli } Connections { + target: root.wrapper function onCurrentNameChanged(): void { // Clear password network when leaving password dialog - if (root.popouts.currentName !== "wirelesspassword" && root.showPasswordDialog) { + if (root.wrapper.currentName !== "wirelesspassword" && root.showPasswordDialog) { root.showPasswordDialog = false; root.passwordNetwork = null; } } - - target: root.popouts } component Toggle: RowLayout { diff --git a/modules/bar/popouts/TrayMenu.qml b/modules/bar/popouts/TrayMenu.qml index 4347e39e1..9b743db19 100644 --- a/modules/bar/popouts/TrayMenu.qml +++ b/modules/bar/popouts/TrayMenu.qml @@ -1,21 +1,21 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Controls -import Quickshell -import Quickshell.Widgets import qs.components import qs.services import qs.config +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Controls StackView { id: root - required property PopoutState popouts + required property Item popouts required property QsMenuHandle trayItem - implicitWidth: currentItem?.implicitWidth ?? 0 - implicitHeight: currentItem?.implicitHeight ?? 0 + implicitWidth: currentItem.implicitWidth + implicitHeight: currentItem.implicitHeight initialItem: SubMenu { handle: root.trayItem @@ -26,12 +26,6 @@ StackView { popEnter: NoAnim {} popExit: NoAnim {} - Component { - id: subMenuComp - - SubMenu {} - } - component NoAnim: Transition { NumberAnimation { duration: 0 @@ -87,7 +81,6 @@ StackView { Loader { id: children - asynchronous: true anchors.left: parent.left anchors.right: parent.right @@ -97,6 +90,13 @@ StackView { implicitHeight: label.implicitHeight StateLayer { + anchors.margins: -Appearance.padding.small / 2 + anchors.leftMargin: -Appearance.padding.smaller + anchors.rightMargin: -Appearance.padding.smaller + + radius: item.radius + disabled: !item.modelData.enabled + function onClicked(): void { const entry = item.modelData; if (entry.hasChildren) @@ -109,25 +109,16 @@ StackView { root.popouts.hasCurrent = false; } } - - anchors.margins: -Appearance.padding.small / 2 - anchors.leftMargin: -Appearance.padding.smaller - anchors.rightMargin: -Appearance.padding.smaller - - radius: item.radius - disabled: !item.modelData.enabled } Loader { id: icon - asynchronous: true anchors.left: parent.left active: item.modelData.icon !== "" sourceComponent: IconImage { - asynchronous: true implicitSize: label.implicitHeight source: item.modelData.icon @@ -158,7 +149,6 @@ StackView { Loader { id: expand - asynchronous: true anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right @@ -175,7 +165,6 @@ StackView { } Loader { - asynchronous: true active: menu.isSubMenu sourceComponent: Item { @@ -197,12 +186,12 @@ StackView { color: Colours.palette.m3secondaryContainer StateLayer { + radius: parent.radius + color: Colours.palette.m3onSecondaryContainer + function onClicked(): void { root.pop(); } - - radius: parent.radius - color: Colours.palette.m3onSecondaryContainer } } @@ -227,4 +216,10 @@ StackView { } } } + + Component { + id: subMenuComp + + SubMenu {} + } } diff --git a/modules/bar/popouts/WirelessPassword.qml b/modules/bar/popouts/WirelessPassword.qml index 8858273d7..96639e711 100644 --- a/modules/bar/popouts/WirelessPassword.qml +++ b/modules/bar/popouts/WirelessPassword.qml @@ -1,77 +1,59 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components import qs.components.controls import qs.services import qs.config import qs.utils +import Quickshell +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root - required property PopoutState popouts + required property Item wrapper property var network: null property bool isClosing: false - readonly property bool shouldBeVisible: root.popouts.currentName === "wirelesspassword" - - function checkConnectionStatus(): void { - if (!root.shouldBeVisible || !connectButton.connecting) { - return; - } - - // Check if we're connected to the target network (case-insensitive SSID comparison) - const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); - - if (isConnected) { - // Successfully connected - give it a moment for network list to update - // Use Timer for actual delay - connectionSuccessTimer.start(); - return; - } + readonly property bool shouldBeVisible: root.wrapper.currentName === "wirelesspassword" - // Check for connection failures - if pending connection was cleared but we're not connected - if (Nmcli.pendingConnection === null && connectButton.connecting) { - // Wait a bit more before giving up (allow time for connection to establish) - if (connectionMonitor.repeatCount > 10) { - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.hasError = true; - connectButton.enabled = true; - connectButton.text = qsTr("Connect"); - passwordContainer.passwordBuffer = ""; - // Delete the failed connection - if (root.network && root.network.ssid) { - Nmcli.forgetNetwork(root.network.ssid); - } + Connections { + target: root.wrapper + function onCurrentNameChanged() { + if (root.wrapper.currentName === "wirelesspassword") { + // Update network when popout becomes active + Qt.callLater(() => { + // Try to get network from parent Content's networkPopout + const content = root.parent?.parent?.parent; + if (content) { + const networkPopout = content.children.find(c => c.name === "network"); + if (networkPopout && networkPopout.item) { + root.network = networkPopout.item.passwordNetwork; + } + } + // Force focus to password container when popout becomes active + // Use Timer for actual delay to ensure dialog is fully rendered + focusTimer.start(); + }); } } } - function closeDialog(): void { - if (isClosing) { - return; - } - - isClosing = true; - passwordContainer.passwordBuffer = ""; - connectButton.connecting = false; - connectButton.hasError = false; - connectButton.text = qsTr("Connect"); - connectionMonitor.stop(); - - // Return to network popout - if (root.popouts.currentName === "wirelesspassword") { - root.popouts.currentName = "network"; + Timer { + id: focusTimer + interval: 150 + onTriggered: { + root.forceActiveFocus(); + passwordContainer.forceActiveFocus(); } } spacing: Appearance.spacing.normal + implicitWidth: 400 implicitHeight: content.implicitHeight + Appearance.padding.large * 2 + visible: shouldBeVisible || isClosing enabled: shouldBeVisible && !isClosing focus: enabled @@ -92,49 +74,16 @@ ColumnLayout { Keys.onEscapePressed: closeDialog() - Connections { - function onCurrentNameChanged() { - if (root.popouts.currentName === "wirelesspassword") { - // Update network when popout becomes active - Qt.callLater(() => { - // Try to get network from parent Content's networkPopout - const content = root.parent?.parent?.parent; - if (content) { - const networkPopout = content.children.find(c => c.name === "network"); - if (networkPopout && networkPopout.item) { - root.network = networkPopout.item.passwordNetwork; - } - } - // Force focus to password container when popout becomes active - // Use Timer for actual delay to ensure dialog is fully rendered - focusTimer.start(); - }); - } - } - - target: root.popouts - } - - Timer { - id: focusTimer - - interval: 150 - onTriggered: { - root.forceActiveFocus(); - passwordContainer.forceActiveFocus(); - } - } - StyledRect { Layout.fillWidth: true Layout.preferredWidth: 400 implicitHeight: content.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer visible: root.shouldBeVisible || root.isClosing opacity: root.shouldBeVisible && !root.isClosing ? 1 : 0 scale: root.shouldBeVisible && !root.isClosing ? 1 : 0.7 - Keys.onEscapePressed: root.closeDialog() Behavior on opacity { Anim {} @@ -164,6 +113,8 @@ ColumnLayout { } } + Keys.onEscapePressed: root.closeDialog() + ColumnLayout { id: content @@ -189,7 +140,6 @@ ColumnLayout { StyledText { id: networkNameText - Layout.alignment: Qt.AlignHCenter text: { if (root.network) { @@ -205,11 +155,10 @@ ColumnLayout { } Timer { - property int attempts: 0 - interval: 50 running: root.shouldBeVisible && (!root.network || !root.network.ssid) repeat: true + property int attempts: 0 onTriggered: { attempts++; // Keep trying to get network from Network component @@ -257,22 +206,15 @@ ColumnLayout { FocusScope { id: passwordContainer - - property string passwordBuffer: "" - objectName: "passwordContainer" Layout.topMargin: Appearance.spacing.large Layout.fillWidth: true implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2) + focus: true activeFocusOnTab: true - Component.onCompleted: { - if (root.shouldBeVisible) { - // Use Timer for actual delay to ensure focus works correctly - passwordFocusTimer.start(); - } - } + property string passwordBuffer: "" Keys.onPressed: event => { // Ensure we have focus when receiving keyboard input @@ -280,11 +222,6 @@ ColumnLayout { forceActiveFocus(); } - if (event.key === Qt.Key_Escape) { - event.accepted = false; - closeDialog(); - } - // Clear error when user starts typing if (connectButton.hasError && event.text && event.text.length > 0) { connectButton.hasError = false; @@ -303,16 +240,13 @@ ColumnLayout { } event.accepted = true; } else if (event.text && event.text.length > 0) { - if (event.key === Qt.Key_Tab) { - event.accepted = false; - return; - } passwordBuffer += event.text; event.accepted = true; } } Connections { + target: root function onShouldBeVisibleChanged(): void { if (root.shouldBeVisible) { // Use Timer for actual delay to ensure focus works correctly @@ -321,19 +255,23 @@ ColumnLayout { connectButton.hasError = false; } } - - target: root } Timer { id: passwordFocusTimer - interval: 50 onTriggered: { passwordContainer.forceActiveFocus(); } } + Component.onCompleted: { + if (root.shouldBeVisible) { + // Use Timer for actual delay to ensure focus works correctly + passwordFocusTimer.start(); + } + } + StyledRect { anchors.fill: parent radius: Appearance.rounding.normal @@ -363,13 +301,13 @@ ColumnLayout { } StateLayer { - function onClicked(): void { - passwordContainer.forceActiveFocus(); - } - hoverEnabled: false cursorShape: Qt.IBeamCursor radius: Appearance.rounding.normal + + function onClicked(): void { + passwordContainer.forceActiveFocus(); + } } StyledText { @@ -553,14 +491,45 @@ ColumnLayout { } } - Timer { - id: connectionMonitor + function checkConnectionStatus(): void { + if (!root.shouldBeVisible || !connectButton.connecting) { + return; + } - property int repeatCount: 0 + // Check if we're connected to the target network (case-insensitive SSID comparison) + const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + + if (isConnected) { + // Successfully connected - give it a moment for network list to update + // Use Timer for actual delay + connectionSuccessTimer.start(); + return; + } + + // Check for connection failures - if pending connection was cleared but we're not connected + if (Nmcli.pendingConnection === null && connectButton.connecting) { + // Wait a bit more before giving up (allow time for connection to establish) + if (connectionMonitor.repeatCount > 10) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.hasError = true; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } + } + } + } + Timer { + id: connectionMonitor interval: 1000 repeat: true triggeredOnStart: false + property int repeatCount: 0 onTriggered: { repeatCount++; @@ -576,7 +545,6 @@ ColumnLayout { Timer { id: connectionSuccessTimer - interval: 500 onTriggered: { // Double-check connection is still active @@ -587,8 +555,8 @@ ColumnLayout { connectButton.connecting = false; connectButton.text = qsTr("Connect"); // Return to network popout on successful connection - if (root.popouts.currentName === "wirelesspassword") { - root.popouts.currentName = "network"; + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; } closeDialog(); } @@ -597,12 +565,12 @@ ColumnLayout { } Connections { + target: Nmcli function onActiveChanged() { if (root.shouldBeVisible) { root.checkConnectionStatus(); } } - function onConnectionFailed(ssid: string) { if (root.shouldBeVisible && root.network && root.network.ssid === ssid && connectButton.connecting) { connectionMonitor.stop(); @@ -615,7 +583,23 @@ ColumnLayout { Nmcli.forgetNetwork(ssid); } } + } - target: Nmcli + function closeDialog(): void { + if (isClosing) { + return; + } + + isClosing = true; + passwordContainer.passwordBuffer = ""; + connectButton.connecting = false; + connectButton.hasError = false; + connectButton.text = qsTr("Connect"); + connectionMonitor.stop(); + + // Return to network popout + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; + } } } diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml index 06bec9b0b..05a1d3c9e 100644 --- a/modules/bar/popouts/Wrapper.qml +++ b/modules/bar/popouts/Wrapper.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell -import Quickshell.Hyprland -import Quickshell.Wayland import qs.components import qs.services import qs.config -import qs.modules.controlcenter import qs.modules.windowinfo +import qs.modules.controlcenter +import Quickshell +import Quickshell.Wayland +import Quickshell.Hyprland +import QtQuick Item { id: root @@ -17,12 +17,11 @@ Item { readonly property real nonAnimWidth: x > 0 || hasCurrent ? children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth : 0 readonly property real nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight - readonly property Item current: (content.item as Content)?.current ?? null + readonly property Item current: content.item?.current ?? null - property alias currentName: popoutState.currentName + property string currentName property real currentCenter - property alias hasCurrent: popoutState.hasCurrent - readonly property PopoutState state: popoutState + property bool hasCurrent property string detachedMode property string queuedMode @@ -60,7 +59,7 @@ Item { Keys.onEscapePressed: { // Forward escape to password popout if active, otherwise close if (currentName === "wirelesspassword" && content.item) { - const passwordPopout = (content.item as Content)?.children.find(c => c.name === "wirelesspassword"); + const passwordPopout = content.item.children.find(c => c.name === "wirelesspassword"); if (passwordPopout && passwordPopout.item) { passwordPopout.item.closeDialog(); return; @@ -76,12 +75,6 @@ Item { } } - PopoutState { - id: popoutState - - onDetachRequested: mode => root.detach(mode) - } - HyprlandFocusGrab { active: root.isDetached windows: [QsWindow.window] @@ -112,7 +105,7 @@ Item { anchors.verticalCenter: parent.verticalCenter sourceComponent: Content { - popouts: popoutState + wrapper: root } } @@ -131,12 +124,12 @@ Item { anchors.centerIn: parent sourceComponent: ControlCenter { + screen: root.screen + active: root.queuedMode + function close(): void { root.close(); } - - screen: root.screen - active: root.queuedMode } } diff --git a/modules/bar/popouts/kblayout/KbLayout.qml b/modules/bar/popouts/kblayout/KbLayout.qml index 3e5819027..94b6f7ec5 100644 --- a/modules/bar/popouts/kblayout/KbLayout.qml +++ b/modules/bar/popouts/kblayout/KbLayout.qml @@ -4,25 +4,30 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.components +import qs.components.controls import qs.services import qs.config +import qs.utils + +import "." ColumnLayout { id: root - function refresh() { - kb.refresh(); - } + required property Item wrapper spacing: Appearance.spacing.small width: Config.bar.sizes.kbLayoutWidth - Component.onCompleted: kb.start() - KbLayoutModel { id: kb } + function refresh() { + kb.refresh(); + } + Component.onCompleted: kb.start() + StyledText { Layout.topMargin: Appearance.padding.normal Layout.rightMargin: Appearance.padding.small @@ -32,7 +37,6 @@ ColumnLayout { ListView { id: list - model: kb.visibleModel Layout.fillWidth: true @@ -81,45 +85,44 @@ ColumnLayout { } delegate: Item { - id: kbDelegate - required property int layoutIndex required property string label - readonly property bool isDisabled: layoutIndex > 3 width: list.width height: Math.max(36, rowText.implicitHeight + Appearance.padding.small * 2) - ToolTip.visible: isDisabled && layer.containsMouse - ToolTip.text: "XKB limitation: maximum 4 layouts allowed" + + readonly property bool isDisabled: layoutIndex > 3 StateLayer { id: layer - - function onClicked(): void { - if (!kbDelegate.isDisabled) - kb.switchTo(kbDelegate.layoutIndex); - } - anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter implicitHeight: parent.height - 4 + radius: Appearance.rounding.full - enabled: !kbDelegate.isDisabled + enabled: !isDisabled + + function onClicked(): void { + if (!isDisabled) + kb.switchTo(layoutIndex); + } } StyledText { id: rowText - anchors.verticalCenter: layer.verticalCenter anchors.left: layer.left anchors.right: layer.right anchors.leftMargin: Appearance.padding.small anchors.rightMargin: Appearance.padding.small - text: kbDelegate.label + text: label elide: Text.ElideRight - opacity: kbDelegate.isDisabled ? 0.4 : 1.0 + opacity: isDisabled ? 0.4 : 1.0 } + + ToolTip.visible: isDisabled && layer.containsMouse + ToolTip.text: "XKB limitation: maximum 4 layouts allowed" } } @@ -129,7 +132,7 @@ ColumnLayout { Layout.rightMargin: Appearance.padding.small Layout.topMargin: Appearance.spacing.small - implicitHeight: 1 + height: 1 color: Colours.palette.m3onSurfaceVariant opacity: 0.35 } @@ -160,18 +163,16 @@ ColumnLayout { } Connections { + target: kb function onActiveLabelChanged() { if (!activeRow.visible) return; popIn.restart(); } - - target: kb } SequentialAnimation { id: popIn - running: false ParallelAnimation { diff --git a/modules/bar/popouts/kblayout/KbLayoutModel.qml b/modules/bar/popouts/kblayout/KbLayoutModel.qml index 4caebbd29..437109530 100644 --- a/modules/bar/popouts/kblayout/KbLayoutModel.qml +++ b/modules/bar/popouts/kblayout/KbLayoutModel.qml @@ -1,20 +1,24 @@ pragma ComponentBehavior: Bound import QtQuick + +import Quickshell import Quickshell.Io -import Caelestia -import qs.config -// TODO: handle this better later +import qs.config +import Caelestia Item { id: model + visible: false + ListModel { + id: _visibleModel + } property alias visibleModel: _visibleModel + property string activeLabel: "" property int activeIndex: -1 - property var _xkbMap: ({}) - property bool _notifiedLimit: false function start() { _xkbXmlBase.running = true; @@ -31,6 +35,31 @@ Item { _switchProc.running = true; } + ListModel { + id: _layoutsModel + } + + property var _xkbMap: ({}) + property bool _notifiedLimit: false + + Process { + id: _xkbXmlBase + command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/base.xml"] + stdout: StdioCollector { + onStreamFinished: _buildXmlMap(text) + } + onRunningChanged: if (!running && (typeof exitCode !== "undefined") && exitCode !== 0) + _xkbXmlEvdev.running = true + } + + Process { + id: _xkbXmlEvdev + command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/evdev.xml"] + stdout: StdioCollector { + onStreamFinished: _buildXmlMap(text) + } + } + function _buildXmlMap(xml) { const map = {}; @@ -76,84 +105,8 @@ Item { return `${lang} (${code})`; } - function _setLayouts(raw) { - const parts = raw.split(",").map(s => s.trim()).filter(Boolean); - _layoutsModel.clear(); - - const seen = new Set(); - let idx = 0; - - for (const p of parts) { - if (seen.has(p)) - continue; - seen.add(p); - _layoutsModel.append({ - layoutIndex: idx, - token: p, - label: _pretty(p) - }); - idx++; - } - } - - function _rebuildVisible() { - _visibleModel.clear(); - - let arr = []; - for (let i = 0; i < _layoutsModel.count; i++) - arr.push(_layoutsModel.get(i)); - - arr = arr.filter(i => i.layoutIndex !== activeIndex); - arr.forEach(i => _visibleModel.append(i)); - - if (!Config.utilities.toasts.kbLimit) - return; - - if (_layoutsModel.count > 4) { - Toaster.toast(qsTr("Keyboard layout limit"), qsTr("XKB supports only 4 layouts at a time"), "warning"); - } - } - - function _pretty(token) { - const code = token.replace(/\(.*\)$/, "").trim(); - if (_xkbMap[code]) - return code.toUpperCase() + " - " + _xkbMap[code]; - return code.toUpperCase() + " - " + code; - } - - visible: false - - ListModel { - id: _visibleModel - } - - ListModel { - id: _layoutsModel - } - - Process { - id: _xkbXmlBase - - command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/base.xml"] - stdout: StdioCollector { - onStreamFinished: model._buildXmlMap(text) - } - onRunningChanged: if (!running && (typeof _xkbXmlBase.exitCode !== "undefined") && _xkbXmlBase.exitCode !== 0) // qmllint disable missing-property - _xkbXmlEvdev.running = true - } - - Process { - id: _xkbXmlEvdev - - command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/evdev.xml"] - stdout: StdioCollector { - onStreamFinished: model._buildXmlMap(text) - } - } - Process { id: _getKbLayoutOpt - command: ["hyprctl", "-j", "getoption", "input:kb_layout"] stdout: StdioCollector { onStreamFinished: { @@ -161,7 +114,7 @@ Item { const j = JSON.parse(text); const raw = (j?.str || j?.value || "").toString().trim(); if (raw.length) { - model._setLayouts(raw); + _setLayouts(raw); _fetchActiveLayouts.running = true; return; } @@ -173,7 +126,6 @@ Item { Process { id: _fetchLayoutsFromDevices - command: ["hyprctl", "-j", "devices"] stdout: StdioCollector { onStreamFinished: { @@ -182,7 +134,7 @@ Item { const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0]; const raw = (kb?.layout || "").trim(); if (raw.length) - model._setLayouts(raw); + _setLayouts(raw); } catch (e) {} _fetchActiveLayouts.running = true; } @@ -191,7 +143,6 @@ Item { Process { id: _fetchActiveLayouts - command: ["hyprctl", "-j", "devices"] stdout: StdioCollector { onStreamFinished: { @@ -200,22 +151,66 @@ Item { const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0]; const idx = kb?.active_layout_index ?? -1; - model.activeIndex = idx >= 0 ? idx : -1; - model.activeLabel = (idx >= 0 && idx < _layoutsModel.count) ? _layoutsModel.get(idx).label : ""; + activeIndex = idx >= 0 ? idx : -1; + activeLabel = (idx >= 0 && idx < _layoutsModel.count) ? _layoutsModel.get(idx).label : ""; } catch (e) { - model.activeIndex = -1; - model.activeLabel = ""; + activeIndex = -1; + activeLabel = ""; } - model._rebuildVisible(); + _rebuildVisible(); } } } Process { id: _switchProc - onRunningChanged: if (!running) _fetchActiveLayouts.running = true } + + function _setLayouts(raw) { + const parts = raw.split(",").map(s => s.trim()).filter(Boolean); + _layoutsModel.clear(); + + const seen = new Set(); + let idx = 0; + + for (const p of parts) { + if (seen.has(p)) + continue; + seen.add(p); + _layoutsModel.append({ + layoutIndex: idx, + token: p, + label: _pretty(p) + }); + idx++; + } + } + + function _rebuildVisible() { + _visibleModel.clear(); + + let arr = []; + for (let i = 0; i < _layoutsModel.count; i++) + arr.push(_layoutsModel.get(i)); + + arr = arr.filter(i => i.layoutIndex !== activeIndex); + arr.forEach(i => _visibleModel.append(i)); + + if (!Config.utilities.toasts.kbLimit) + return; + + if (_layoutsModel.count > 4) { + Toaster.toast(qsTr("Keyboard layout limit"), qsTr("XKB supports only 4 layouts at a time"), "warning"); + } + } + + function _pretty(token) { + const code = token.replace(/\(.*\)$/, "").trim(); + if (_xkbMap[code]) + return code.toUpperCase() + " - " + _xkbMap[code]; + return code.toUpperCase() + " - " + code; + } } diff --git a/modules/controlcenter/ControlCenter.qml b/modules/controlcenter/ControlCenter.qml index f542b5970..4aacfad99 100644 --- a/modules/controlcenter/ControlCenter.qml +++ b/modules/controlcenter/ControlCenter.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components import qs.components.controls import qs.services import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts Item { id: root @@ -18,7 +18,6 @@ Item { property alias active: session.active property alias navExpanded: session.navExpanded - readonly property bool initialOpeningComplete: panes.initialOpeningComplete readonly property Session session: Session { id: session @@ -43,7 +42,6 @@ Item { Layout.fillWidth: true Layout.columnSpan: 2 - asynchronous: true active: root.floating visible: active @@ -62,6 +60,8 @@ Item { color: Colours.tPalette.m3surfaceContainer CustomMouseArea { + anchors.fill: parent + function onWheel(event: WheelEvent): void { // Prevent tab switching during initial opening animation to avoid blank pages if (!panes.initialOpeningComplete) { @@ -73,8 +73,6 @@ Item { else if (event.angleDelta.y > 0) root.session.activeIndex = Math.max(root.session.activeIndex - 1, 0); } - - anchors.fill: parent } NavRail { @@ -97,4 +95,6 @@ Item { session: root.session } } + + readonly property bool initialOpeningComplete: panes.initialOpeningComplete } diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index 037ea0d64..e61a741a3 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components import qs.services import qs.config import qs.modules.controlcenter +import Quickshell +import QtQuick +import QtQuick.Layouts Item { id: root @@ -43,7 +43,6 @@ Item { Loader { Layout.topMargin: Appearance.spacing.large - asynchronous: true active: !root.session.floating visible: active @@ -59,6 +58,8 @@ Item { StateLayer { id: normalWinState + color: Colours.palette.m3onPrimaryContainer + function onClicked(): void { root.session.root.close(); WindowFactory.create(null, { @@ -66,8 +67,6 @@ Item { navExpanded: root.session.navExpanded }); } - - color: Colours.palette.m3onPrimaryContainer } MaterialIcon { @@ -122,7 +121,6 @@ Item { NavItem { required property int index - Layout.topMargin: index === 0 ? Appearance.spacing.large * 2 : 0 icon: PaneRegistry.getByIndex(index).icon label: PaneRegistry.getByIndex(index).label @@ -176,6 +174,8 @@ Item { implicitHeight: icon.implicitHeight + Appearance.padding.small StateLayer { + color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + function onClicked(): void { // Prevent tab switching during initial opening animation to avoid blank pages if (!root.initialOpeningComplete) { @@ -183,8 +183,6 @@ Item { } root.session.active = item.label; } - - color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface } MaterialIcon { diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 5660ea7d3..ab2f808e9 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -7,13 +7,13 @@ import "appearance" import "taskbar" import "launcher" import "dashboard" -import QtQuick -import QtQuick.Layouts -import Quickshell.Widgets import qs.components import qs.services import qs.config import qs.modules.controlcenter +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts ClippingRectangle { id: root @@ -37,26 +37,25 @@ ClippingRectangle { } Connections { + target: root.session + function onActiveIndexChanged(): void { root.focus = true; } - - target: root.session } ColumnLayout { id: layout - property bool animationComplete: true - property bool initialOpeningComplete: false - spacing: 0 y: -root.session.activeIndex * root.height clip: true + property bool animationComplete: true + property bool initialOpeningComplete: false + Timer { id: animationDelayTimer - interval: Appearance.anim.durations.normal onTriggered: { layout.animationComplete = true; @@ -65,7 +64,6 @@ ClippingRectangle { Timer { id: initialOpeningTimer - interval: Appearance.anim.durations.large running: true onTriggered: { @@ -78,7 +76,6 @@ ClippingRectangle { Pane { required property int index - paneIndex: index componentPath: PaneRegistry.getByIndex(index).component } @@ -89,12 +86,11 @@ ClippingRectangle { } Connections { + target: root.session function onActiveIndexChanged(): void { layout.animationComplete = false; animationDelayTimer.restart(); } - - target: root.session } } @@ -103,6 +99,10 @@ ClippingRectangle { required property int paneIndex required property string componentPath + + implicitWidth: root.width + implicitHeight: root.height + property bool hasBeenLoaded: false function updateActive(): void { @@ -125,14 +125,10 @@ ClippingRectangle { loader.active = shouldBeActive; } - implicitWidth: root.width - implicitHeight: root.height - Loader { id: loader anchors.fill: parent - asynchronous: true clip: false active: false @@ -160,22 +156,20 @@ ClippingRectangle { } Connections { + target: root.session function onActiveIndexChanged(): void { pane.updateActive(); } - - target: root.session } Connections { + target: layout function onInitialOpeningCompleteChanged(): void { pane.updateActive(); } function onAnimationCompleteChanged(): void { pane.updateActive(); } - - target: layout } } } diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml index e38396810..8a8545f0f 100644 --- a/modules/controlcenter/Session.qml +++ b/modules/controlcenter/Session.qml @@ -1,5 +1,5 @@ -import "./state" import QtQuick +import "./state" import qs.modules.controlcenter QtObject { diff --git a/modules/controlcenter/WindowFactory.qml b/modules/controlcenter/WindowFactory.qml index 266af9095..abcf5df19 100644 --- a/modules/controlcenter/WindowFactory.qml +++ b/modules/controlcenter/WindowFactory.qml @@ -1,9 +1,9 @@ pragma Singleton -import QtQuick -import Quickshell import qs.components import qs.services +import Quickshell +import QtQuick Singleton { id: root @@ -45,13 +45,13 @@ Singleton { ControlCenter { id: cc - function close(): void { - win.destroy(); - } - anchors.fill: parent screen: win.screen floating: true + + function close(): void { + win.destroy(); + } } Behavior on color { diff --git a/modules/controlcenter/WindowTitle.qml b/modules/controlcenter/WindowTitle.qml index a55445c5a..fb7160893 100644 --- a/modules/controlcenter/WindowTitle.qml +++ b/modules/controlcenter/WindowTitle.qml @@ -1,8 +1,8 @@ -import QtQuick -import Quickshell import qs.components import qs.services import qs.config +import Quickshell +import QtQuick StyledRect { id: root @@ -34,11 +34,11 @@ StyledRect { implicitHeight: closeIcon.implicitHeight + Appearance.padding.small StateLayer { + radius: Appearance.rounding.full + function onClicked(): void { QsWindow.window.destroy(); } - - radius: Appearance.rounding.full } MaterialIcon { diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index c42220762..f29f7ab3d 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -4,19 +4,19 @@ import ".." import "../components" import "./sections" import "../../launcher/services" -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets -import Caelestia.Models import qs.components -import qs.components.containers import qs.components.controls import qs.components.effects +import qs.components.containers import qs.components.images import qs.services import qs.config import qs.utils +import Caelestia.Models +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts Item { id: root @@ -54,6 +54,8 @@ Item { property real visualiserRounding: Config.background.visualiser.rounding ?? 1 property real visualiserSpacing: Config.background.visualiser.spacing ?? 1 + anchors.fill: parent + function saveConfig() { Config.appearance.anim.durations.scale = root.animDurationsScale; @@ -95,8 +97,6 @@ Item { Config.save(); } - anchors.fill: parent - Component { id: appearanceRightContentComponent @@ -124,7 +124,6 @@ Item { Layout.fillHeight: true Layout.bottomMargin: -Appearance.padding.large * 2 - asynchronous: true active: { const isActive = root.session.activeIndex === 3; const isAdjacent = Math.abs(root.session.activeIndex - 3) === 1; @@ -152,11 +151,10 @@ Item { anchors.fill: parent leftContent: Component { + StyledFlickable { id: sidebarFlickable - readonly property var rootPane: root - flickableDirection: Flickable.VerticalFlick contentHeight: sidebarLayout.height @@ -166,14 +164,14 @@ Item { ColumnLayout { id: sidebarLayout - - readonly property var rootPane: sidebarFlickable.rootPane - readonly property bool allSectionsExpanded: themeModeSection.expanded && colorVariantSection.expanded && colorSchemeSection.expanded && animationsSection.expanded && fontsSection.expanded && scalesSection.expanded && transparencySection.expanded && borderSection.expanded && backgroundSection.expanded - anchors.left: parent.left anchors.right: parent.right spacing: Appearance.spacing.small + readonly property var rootPane: sidebarFlickable.rootPane + + readonly property bool allSectionsExpanded: themeModeSection.expanded && colorVariantSection.expanded && colorSchemeSection.expanded && animationsSection.expanded && fontsSection.expanded && scalesSection.expanded && transparencySection.expanded && borderSection.expanded && backgroundSection.expanded + RowLayout { spacing: Appearance.spacing.smaller @@ -220,37 +218,31 @@ Item { AnimationsSection { id: animationsSection - rootPane: sidebarFlickable.rootPane } FontsSection { id: fontsSection - rootPane: sidebarFlickable.rootPane } ScalesSection { id: scalesSection - rootPane: sidebarFlickable.rootPane } TransparencySection { id: transparencySection - rootPane: sidebarFlickable.rootPane } BorderSection { id: borderSection - rootPane: sidebarFlickable.rootPane } BackgroundSection { id: backgroundSection - rootPane: sidebarFlickable.rootPane } } diff --git a/modules/controlcenter/appearance/sections/AnimationsSection.qml b/modules/controlcenter/appearance/sections/AnimationsSection.qml index e4d8a0333..0cba5cecd 100644 --- a/modules/controlcenter/appearance/sections/AnimationsSection.qml +++ b/modules/controlcenter/appearance/sections/AnimationsSection.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../../components" -import QtQuick -import QtQuick.Layouts import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config +import QtQuick +import QtQuick.Layouts CollapsibleSection { id: root diff --git a/modules/controlcenter/appearance/sections/BackgroundSection.qml b/modules/controlcenter/appearance/sections/BackgroundSection.qml index 8b50c1242..9d6bc6ebc 100644 --- a/modules/controlcenter/appearance/sections/BackgroundSection.qml +++ b/modules/controlcenter/appearance/sections/BackgroundSection.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../../components" -import QtQuick -import QtQuick.Layouts import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config +import QtQuick +import QtQuick.Layouts CollapsibleSection { id: root @@ -55,6 +55,9 @@ CollapsibleSection { SectionContainer { id: posContainer + contentSpacing: Appearance.spacing.small + z: 1 + readonly property var pos: (rootPane.desktopClockPosition || "top-left").split('-') readonly property string currentV: pos[0] readonly property string currentH: pos[1] @@ -64,9 +67,6 @@ CollapsibleSection { rootPane.saveConfig(); } - contentSpacing: Appearance.spacing.small - z: 1 - StyledText { text: qsTr("Positioning") font.pointSize: Appearance.font.size.larger @@ -79,22 +79,19 @@ CollapsibleSection { menuItems: [ MenuItem { - property string val: "top" - text: qsTr("Top") icon: "vertical_align_top" + property string val: "top" }, MenuItem { - property string val: "middle" - text: qsTr("Middle") icon: "vertical_align_center" + property string val: "middle" }, MenuItem { - property string val: "bottom" - text: qsTr("Bottom") icon: "vertical_align_bottom" + property string val: "bottom" } ] @@ -116,22 +113,19 @@ CollapsibleSection { menuItems: [ MenuItem { - property string val: "left" - text: qsTr("Left") icon: "align_horizontal_left" + property string val: "left" }, MenuItem { - property string val: "center" - text: qsTr("Center") icon: "align_horizontal_center" + property string val: "center" }, MenuItem { - property string val: "right" - text: qsTr("Right") icon: "align_horizontal_right" + property string val: "right" } ] diff --git a/modules/controlcenter/appearance/sections/BorderSection.qml b/modules/controlcenter/appearance/sections/BorderSection.qml index e0c677cf7..9532d70d6 100644 --- a/modules/controlcenter/appearance/sections/BorderSection.qml +++ b/modules/controlcenter/appearance/sections/BorderSection.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../../components" -import QtQuick -import QtQuick.Layouts import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config +import QtQuick +import QtQuick.Layouts CollapsibleSection { id: root @@ -50,7 +50,7 @@ CollapsibleSection { label: qsTr("Border thickness") value: rootPane.borderThickness - from: 0 + from: 0.1 to: 100 decimals: 1 suffix: "px" diff --git a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml index b954cd458..95cb4b725 100644 --- a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml +++ b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml @@ -2,14 +2,14 @@ pragma ComponentBehavior: Bound import ".." import "../../../launcher/services" -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts CollapsibleSection { title: qsTr("Color scheme") @@ -35,7 +35,6 @@ CollapsibleSection { radius: Appearance.rounding.normal border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary - implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 StateLayer { function onClicked(): void { @@ -54,7 +53,6 @@ CollapsibleSection { Timer { id: reloadTimer - interval: 300 onTriggered: { Schemes.reload(); @@ -84,7 +82,6 @@ CollapsibleSection { MaterialIcon { id: iconPlaceholder - visible: false text: "circle" font.pointSize: Appearance.font.size.large @@ -131,7 +128,6 @@ CollapsibleSection { } Loader { - asynchronous: true active: isCurrent sourceComponent: MaterialIcon { @@ -141,6 +137,8 @@ CollapsibleSection { } } } + + implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 } } } diff --git a/modules/controlcenter/appearance/sections/ColorVariantSection.qml b/modules/controlcenter/appearance/sections/ColorVariantSection.qml index b3cc4cfba..3aa17dd9c 100644 --- a/modules/controlcenter/appearance/sections/ColorVariantSection.qml +++ b/modules/controlcenter/appearance/sections/ColorVariantSection.qml @@ -2,14 +2,14 @@ pragma ComponentBehavior: Bound import ".." import "../../../launcher/services" -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts CollapsibleSection { title: qsTr("Color variant") @@ -32,7 +32,6 @@ CollapsibleSection { radius: Appearance.rounding.normal border.width: modelData.variant === Schemes.currentVariant ? 1 : 0 border.color: Colours.palette.m3primary - implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2 StateLayer { function onClicked(): void { @@ -49,7 +48,6 @@ CollapsibleSection { Timer { id: reloadTimer - interval: 300 onTriggered: { Schemes.reload(); @@ -85,6 +83,8 @@ CollapsibleSection { font.pointSize: Appearance.font.size.large } } + + implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2 } } } diff --git a/modules/controlcenter/appearance/sections/FontsSection.qml b/modules/controlcenter/appearance/sections/FontsSection.qml index 47b738f19..3988863af 100644 --- a/modules/controlcenter/appearance/sections/FontsSection.qml +++ b/modules/controlcenter/appearance/sections/FontsSection.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../../components" -import QtQuick -import QtQuick.Layouts import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config +import QtQuick +import QtQuick.Layouts CollapsibleSection { id: root @@ -19,53 +19,51 @@ CollapsibleSection { showBackground: true CollapsibleSection { - id: sansFontSection - - title: qsTr("Sans-serif font family") + id: materialFontSection + title: qsTr("Material font family") expanded: true showBackground: true nested: true Loader { + id: materialFontLoader Layout.fillWidth: true Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 - asynchronous: true - active: sansFontSection.expanded + active: materialFontSection.expanded sourceComponent: StyledListView { - id: sansFontList - - property alias contentHeight: sansFontList.contentHeight + id: materialFontList + property alias contentHeight: materialFontList.contentHeight clip: true spacing: Appearance.spacing.small / 2 model: Qt.fontFamilies() StyledScrollBar.vertical: StyledScrollBar { - flickable: sansFontList + flickable: materialFontList } delegate: StyledRect { required property string modelData required property int index - readonly property bool isCurrent: modelData === rootPane.fontFamilySans width: ListView.view.width + + readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary - implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 StateLayer { function onClicked(): void { - rootPane.fontFamilySans = modelData; + rootPane.fontFamilyMaterial = modelData; rootPane.saveConfig(); } } RowLayout { - id: fontFamilySansRow + id: fontFamilyMaterialRow anchors.left: parent.left anchors.right: parent.right @@ -84,7 +82,6 @@ CollapsibleSection { } Loader { - asynchronous: true active: isCurrent sourceComponent: MaterialIcon { @@ -94,6 +91,8 @@ CollapsibleSection { } } } + + implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 } } } @@ -101,7 +100,6 @@ CollapsibleSection { CollapsibleSection { id: monoFontSection - title: qsTr("Monospace font family") expanded: false showBackground: true @@ -110,12 +108,10 @@ CollapsibleSection { Loader { Layout.fillWidth: true Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 - asynchronous: true active: monoFontSection.expanded sourceComponent: StyledListView { id: monoFontList - property alias contentHeight: monoFontList.contentHeight clip: true @@ -129,14 +125,14 @@ CollapsibleSection { delegate: StyledRect { required property string modelData required property int index - readonly property bool isCurrent: modelData === rootPane.fontFamilyMono width: ListView.view.width + + readonly property bool isCurrent: modelData === rootPane.fontFamilyMono color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary - implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 StateLayer { function onClicked(): void { @@ -165,7 +161,6 @@ CollapsibleSection { } Loader { - asynchronous: true active: isCurrent sourceComponent: MaterialIcon { @@ -175,61 +170,58 @@ CollapsibleSection { } } } + + implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 } } } } CollapsibleSection { - id: materialFontSection - - title: qsTr("Material font family") + id: sansFontSection + title: qsTr("Sans-serif font family") expanded: false showBackground: true nested: true Loader { - id: materialFontLoader - Layout.fillWidth: true Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 - asynchronous: true - active: materialFontSection.expanded + active: sansFontSection.expanded sourceComponent: StyledListView { - id: materialFontList - - property alias contentHeight: materialFontList.contentHeight + id: sansFontList + property alias contentHeight: sansFontList.contentHeight clip: true spacing: Appearance.spacing.small / 2 - model: Qt.fontFamilies().filter(f => f.startsWith("Material Symbols")) + model: Qt.fontFamilies() StyledScrollBar.vertical: StyledScrollBar { - flickable: materialFontList + flickable: sansFontList } delegate: StyledRect { required property string modelData required property int index - readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial width: ListView.view.width + + readonly property bool isCurrent: modelData === rootPane.fontFamilySans color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary - implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 StateLayer { function onClicked(): void { - rootPane.fontFamilyMaterial = modelData; + rootPane.fontFamilySans = modelData; rootPane.saveConfig(); } } RowLayout { - id: fontFamilyMaterialRow + id: fontFamilySansRow anchors.left: parent.left anchors.right: parent.right @@ -248,7 +240,6 @@ CollapsibleSection { } Loader { - asynchronous: true active: isCurrent sourceComponent: MaterialIcon { @@ -258,6 +249,8 @@ CollapsibleSection { } } } + + implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 } } } diff --git a/modules/controlcenter/appearance/sections/ScalesSection.qml b/modules/controlcenter/appearance/sections/ScalesSection.qml index 6d5d5b303..b0e6e38b8 100644 --- a/modules/controlcenter/appearance/sections/ScalesSection.qml +++ b/modules/controlcenter/appearance/sections/ScalesSection.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../../components" -import QtQuick -import QtQuick.Layouts import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config +import QtQuick +import QtQuick.Layouts CollapsibleSection { id: root diff --git a/modules/controlcenter/appearance/sections/ThemeModeSection.qml b/modules/controlcenter/appearance/sections/ThemeModeSection.qml index c63c73aaf..04eed9113 100644 --- a/modules/controlcenter/appearance/sections/ThemeModeSection.qml +++ b/modules/controlcenter/appearance/sections/ThemeModeSection.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound import ".." -import QtQuick import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config +import QtQuick CollapsibleSection { title: qsTr("Theme mode") diff --git a/modules/controlcenter/appearance/sections/TransparencySection.qml b/modules/controlcenter/appearance/sections/TransparencySection.qml index 77582f9c6..9a48629c1 100644 --- a/modules/controlcenter/appearance/sections/TransparencySection.qml +++ b/modules/controlcenter/appearance/sections/TransparencySection.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../../components" -import QtQuick -import QtQuick.Layouts import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config +import QtQuick +import QtQuick.Layouts CollapsibleSection { id: root diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 172132f6b..01d90be70 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -2,15 +2,15 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Layouts -import Quickshell.Widgets import qs.components -import qs.components.containers import qs.components.controls import qs.components.effects +import qs.components.containers import qs.services import qs.config +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts Item { id: root @@ -23,9 +23,9 @@ Item { anchors.fill: parent leftContent: Component { + StyledFlickable { id: leftAudioFlickable - flickableDirection: Flickable.VerticalFlick contentHeight: leftContent.height @@ -94,7 +94,6 @@ Item { color: Audio.sink?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" radius: Appearance.rounding.normal - implicitHeight: outputRowLayout.implicitHeight + Appearance.padding.normal * 2 StateLayer { function onClicked(): void { @@ -127,6 +126,8 @@ Item { font.weight: Audio.sink?.id === modelData.id ? 500 : 400 } } + + implicitHeight: outputRowLayout.implicitHeight + Appearance.padding.normal * 2 } } } @@ -171,7 +172,6 @@ Item { color: Audio.source?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" radius: Appearance.rounding.normal - implicitHeight: inputRowLayout.implicitHeight + Appearance.padding.normal * 2 StateLayer { function onClicked(): void { @@ -204,6 +204,8 @@ Item { font.weight: Audio.source?.id === modelData.id ? 500 : 400 } } + + implicitHeight: inputRowLayout.implicitHeight + Appearance.padding.normal * 2 } } } @@ -215,7 +217,6 @@ Item { rightContent: Component { StyledFlickable { id: rightAudioFlickable - flickableDirection: Flickable.VerticalFlick contentHeight: contentLayout.height @@ -264,7 +265,6 @@ Item { StyledInputField { id: outputVolumeInput - Layout.preferredWidth: 70 validator: IntValidator { bottom: 0 @@ -276,6 +276,15 @@ Item { text = Math.round(Audio.volume * 100).toString(); } + Connections { + target: Audio + function onVolumeChanged() { + if (!outputVolumeInput.hasFocus) { + outputVolumeInput.text = Math.round(Audio.volume * 100).toString(); + } + } + } + onTextEdited: text => { if (hasFocus) { const val = parseInt(text); @@ -291,16 +300,6 @@ Item { text = Math.round(Audio.volume * 100).toString(); } } - - Connections { - function onVolumeChanged() { - if (!outputVolumeInput.hasFocus) { - outputVolumeInput.text = Math.round(Audio.volume * 100).toString(); - } - } - - target: Audio - } } StyledText { @@ -337,7 +336,6 @@ Item { StyledSlider { id: outputVolumeSlider - Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 @@ -382,7 +380,6 @@ Item { StyledInputField { id: inputVolumeInput - Layout.preferredWidth: 70 validator: IntValidator { bottom: 0 @@ -394,6 +391,15 @@ Item { text = Math.round(Audio.sourceVolume * 100).toString(); } + Connections { + target: Audio + function onSourceVolumeChanged() { + if (!inputVolumeInput.hasFocus) { + inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString(); + } + } + } + onTextEdited: text => { if (hasFocus) { const val = parseInt(text); @@ -409,16 +415,6 @@ Item { text = Math.round(Audio.sourceVolume * 100).toString(); } } - - Connections { - function onSourceVolumeChanged() { - if (!inputVolumeInput.hasFocus) { - inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString(); - } - } - - target: Audio - } } StyledText { @@ -455,7 +451,6 @@ Item { StyledSlider { id: inputVolumeSlider - Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 @@ -516,7 +511,6 @@ Item { StyledInputField { id: streamVolumeInput - Layout.preferredWidth: 70 validator: IntValidator { bottom: 0 @@ -528,6 +522,15 @@ Item { text = Math.round(Audio.getStreamVolume(modelData) * 100).toString(); } + Connections { + target: modelData + function onAudioChanged() { + if (!streamVolumeInput.hasFocus && modelData?.audio) { + streamVolumeInput.text = Math.round(modelData.audio.volume * 100).toString(); + } + } + } + onTextEdited: text => { if (hasFocus) { const val = parseInt(text); @@ -543,16 +546,6 @@ Item { text = Math.round(Audio.getStreamVolume(modelData) * 100).toString(); } } - - Connections { - function onAudioChanged() { - if (!streamVolumeInput.hasFocus && modelData?.audio) { - streamVolumeInput.text = Math.round(modelData.audio.volume * 100).toString(); - } - } - - target: modelData - } } StyledText { @@ -600,13 +593,12 @@ Item { } Connections { + target: modelData function onAudioChanged() { if (modelData?.audio) { value = modelData.audio.volume; } } - - target: modelData } } } diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml index 97ea99c57..7d3b9ca33 100644 --- a/modules/controlcenter/bluetooth/BtPane.qml +++ b/modules/controlcenter/bluetooth/BtPane.qml @@ -3,13 +3,13 @@ pragma ComponentBehavior: Bound import ".." import "../components" import "." -import QtQuick -import Quickshell.Bluetooth -import Quickshell.Widgets import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.config +import Quickshell.Widgets +import Quickshell.Bluetooth +import QtQuick SplitPaneWithDetails { id: root @@ -53,7 +53,6 @@ SplitPaneWithDetails { rightSettingsComponent: Component { StyledFlickable { id: settingsFlickable - flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml index 9b347acdc..bc276e097 100644 --- a/modules/controlcenter/bluetooth/Details.qml +++ b/modules/controlcenter/bluetooth/Details.qml @@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Layouts -import Quickshell.Bluetooth import qs.components -import qs.components.containers import qs.components.controls import qs.components.effects +import qs.components.containers import qs.services import qs.config import qs.utils +import Quickshell.Bluetooth +import QtQuick +import QtQuick.Layouts StyledFlickable { id: root @@ -241,13 +241,13 @@ StyledFlickable { scale: root.session.bt.editingDeviceName ? 1 : 0.5 StateLayer { + color: Colours.palette.m3onSecondaryContainer + disabled: !root.session.bt.editingDeviceName + function onClicked(): void { root.session.bt.editingDeviceName = false; deviceNameEdit.text = Qt.binding(() => root.device?.name ?? ""); } - - color: Colours.palette.m3onSecondaryContainer - disabled: !root.session.bt.editingDeviceName } MaterialIcon { @@ -279,6 +279,8 @@ StyledFlickable { color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingDeviceName ? 1 : 0) StateLayer { + color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + function onClicked(): void { root.session.bt.editingDeviceName = !root.session.bt.editingDeviceName; if (root.session.bt.editingDeviceName) @@ -286,8 +288,6 @@ StyledFlickable { else deviceNameEdit.accepted(); } - - color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface } MaterialIcon { @@ -359,7 +359,6 @@ StyledFlickable { RowLayout { id: batteryPercent - Layout.topMargin: Appearance.spacing.small / 2 Layout.fillWidth: true Layout.preferredHeight: Appearance.padding.smaller @@ -630,11 +629,11 @@ StyledFlickable { StateLayer { id: fabState + color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer + function onClicked(): void { root.session.bt.fabMenuOpen = !root.session.bt.fabMenuOpen; } - - color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer } MaterialIcon { diff --git a/modules/controlcenter/bluetooth/DeviceList.qml b/modules/controlcenter/bluetooth/DeviceList.qml index b53829b93..2a2bde934 100644 --- a/modules/controlcenter/bluetooth/DeviceList.qml +++ b/modules/controlcenter/bluetooth/DeviceList.qml @@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Bluetooth import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config import qs.utils +import Quickshell +import Quickshell.Bluetooth +import QtQuick +import QtQuick.Layouts DeviceList { id: root @@ -222,6 +222,9 @@ DeviceList { } StateLayer { + color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + disabled: device.loading + function onClicked(): void { if (device.loading) return; @@ -236,9 +239,6 @@ DeviceList { } } } - - color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - disabled: device.loading } MaterialIcon { diff --git a/modules/controlcenter/bluetooth/Settings.qml b/modules/controlcenter/bluetooth/Settings.qml index 6936c1ea2..c5472406b 100644 --- a/modules/controlcenter/bluetooth/Settings.qml +++ b/modules/controlcenter/bluetooth/Settings.qml @@ -2,14 +2,14 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Layouts -import Quickshell.Bluetooth import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config +import Quickshell.Bluetooth +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root @@ -131,11 +131,11 @@ ColumnLayout { implicitHeight: adapterPicker.implicitHeight + Appearance.padding.smaller * 2 StateLayer { + radius: Appearance.rounding.small + function onClicked(): void { adapterPickerButton.expanded = !adapterPickerButton.expanded; } - - radius: Appearance.rounding.small } RowLayout { @@ -210,12 +210,12 @@ ColumnLayout { implicitHeight: adapterInner.implicitHeight + Appearance.padding.normal * 2 StateLayer { + disabled: !adapterPickerButton.expanded + function onClicked(): void { adapterPickerButton.expanded = false; root.session.bt.currentAdapter = adapter.modelData; } - - disabled: !adapterPickerButton.expanded } RowLayout { @@ -381,13 +381,13 @@ ColumnLayout { scale: root.session.bt.editingAdapterName ? 1 : 0.5 StateLayer { + color: Colours.palette.m3onSecondaryContainer + disabled: !root.session.bt.editingAdapterName + function onClicked(): void { root.session.bt.editingAdapterName = false; adapterNameEdit.text = Qt.binding(() => root.session.bt.currentAdapter?.name ?? ""); } - - color: Colours.palette.m3onSecondaryContainer - disabled: !root.session.bt.editingAdapterName } MaterialIcon { @@ -419,6 +419,8 @@ ColumnLayout { color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingAdapterName ? 1 : 0) StateLayer { + color: root.session.bt.editingAdapterName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + function onClicked(): void { root.session.bt.editingAdapterName = !root.session.bt.editingAdapterName; if (root.session.bt.editingAdapterName) @@ -426,8 +428,6 @@ ColumnLayout { else adapterNameEdit.accepted(); } - - color: root.session.bt.editingAdapterName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface } MaterialIcon { diff --git a/modules/controlcenter/components/ConnectedButtonGroup.qml b/modules/controlcenter/components/ConnectedButtonGroup.qml index 15b1896cb..ab707fb73 100644 --- a/modules/controlcenter/components/ConnectedButtonGroup.qml +++ b/modules/controlcenter/components/ConnectedButtonGroup.qml @@ -1,11 +1,11 @@ import ".." -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config +import QtQuick +import QtQuick.Layouts StyledRect { id: root @@ -40,7 +40,6 @@ StyledRect { GridLayout { id: buttonGrid - Layout.alignment: Qt.AlignHCenter rowSpacing: Appearance.spacing.small columnSpacing: Appearance.spacing.small @@ -49,19 +48,18 @@ StyledRect { Repeater { id: repeater - model: root.options delegate: TextButton { id: button - required property int index required property var modelData - property bool _checked: false - Layout.fillWidth: true text: modelData.label + + property bool _checked: false + checked: _checked toggle: false type: TextButton.Tonal @@ -70,7 +68,8 @@ StyledRect { Component.onCompleted: { if (modelData.state !== undefined && modelData.state) { _checked = modelData.state; - } else if (root.rootItem && modelData.propertyName) { + } + else if (root.rootItem && modelData.propertyName) { const propName = modelData.propertyName; const rootItem = root.rootItem; _checked = Qt.binding(function () { diff --git a/modules/controlcenter/components/DeviceDetails.qml b/modules/controlcenter/components/DeviceDetails.qml index c150abd7d..a5d06471c 100644 --- a/modules/controlcenter/components/DeviceDetails.qml +++ b/modules/controlcenter/components/DeviceDetails.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound import ".." -import QtQuick -import QtQuick.Layouts import qs.components -import qs.components.containers import qs.components.controls import qs.components.effects +import qs.components.containers import qs.config +import QtQuick +import QtQuick.Layouts Item { id: root @@ -36,7 +36,6 @@ Item { id: headerLoader Layout.fillWidth: true - asynchronous: true sourceComponent: root.headerComponent visible: root.headerComponent !== null } @@ -45,7 +44,6 @@ Item { id: topContentLoader Layout.fillWidth: true - asynchronous: true sourceComponent: root.topContent visible: root.topContent !== null } @@ -57,7 +55,6 @@ Item { required property Component modelData Layout.fillWidth: true - asynchronous: true sourceComponent: modelData } } @@ -66,7 +63,6 @@ Item { id: bottomContentLoader Layout.fillWidth: true - asynchronous: true sourceComponent: root.bottomContent visible: root.bottomContent !== null } diff --git a/modules/controlcenter/components/DeviceList.qml b/modules/controlcenter/components/DeviceList.qml index 2134d8cfe..722f9a16e 100644 --- a/modules/controlcenter/components/DeviceList.qml +++ b/modules/controlcenter/components/DeviceList.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound import ".." -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root @@ -23,7 +23,6 @@ ColumnLayout { property Component headerComponent: null property Component titleSuffix: null property bool showHeader: true - property alias view: view signal itemSelected(var item) @@ -33,7 +32,6 @@ ColumnLayout { id: headerLoader Layout.fillWidth: true - asynchronous: true sourceComponent: root.headerComponent visible: root.headerComponent !== null && root.showHeader } @@ -52,7 +50,6 @@ ColumnLayout { } Loader { - asynchronous: true sourceComponent: root.titleSuffix visible: root.titleSuffix !== null } @@ -62,6 +59,8 @@ ColumnLayout { } } + property alias view: view + StyledText { visible: root.description !== "" Layout.fillWidth: true diff --git a/modules/controlcenter/components/PaneTransition.qml b/modules/controlcenter/components/PaneTransition.qml index ac9c3371a..5d80dbec2 100644 --- a/modules/controlcenter/components/PaneTransition.qml +++ b/modules/controlcenter/components/PaneTransition.qml @@ -1,7 +1,7 @@ pragma ComponentBehavior: Bound -import QtQuick import qs.config +import QtQuick SequentialAnimation { id: root diff --git a/modules/controlcenter/components/ReadonlySlider.qml b/modules/controlcenter/components/ReadonlySlider.qml index 105270451..169d63653 100644 --- a/modules/controlcenter/components/ReadonlySlider.qml +++ b/modules/controlcenter/components/ReadonlySlider.qml @@ -1,11 +1,11 @@ import ".." import "../components" -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.controls import qs.services import qs.config +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/controlcenter/components/SettingsHeader.qml b/modules/controlcenter/components/SettingsHeader.qml index 6d392f5ea..0dc190c05 100644 --- a/modules/controlcenter/components/SettingsHeader.qml +++ b/modules/controlcenter/components/SettingsHeader.qml @@ -1,9 +1,9 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts import qs.components import qs.config +import QtQuick +import QtQuick.Layouts Item { id: root diff --git a/modules/controlcenter/components/SliderInput.qml b/modules/controlcenter/components/SliderInput.qml index df3acf41e..11b3f70dd 100644 --- a/modules/controlcenter/components/SliderInput.qml +++ b/modules/controlcenter/components/SliderInput.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root @@ -21,9 +21,6 @@ ColumnLayout { property int decimals: 1 // Number of decimal places to show (default: 1) property var formatValueFunction: null // Optional custom format function property var parseValueFunction: null // Optional custom parse function - property bool _initialized: false - - signal valueModified(real newValue) function formatValue(val: real): string { if (formatValueFunction) { @@ -52,6 +49,10 @@ ColumnLayout { return parseFloat(text); } + signal valueModified(real newValue) + + property bool _initialized: false + spacing: Appearance.spacing.small Component.onCompleted: { @@ -61,14 +62,6 @@ ColumnLayout { }); } - // Update input field when value changes externally (slider is already bound) - onValueChanged: { - // Only update if component is initialized to avoid issues during creation - if (root._initialized && !inputField.hasFocus) { - inputField.text = root.formatValue(root.value); - } - } - RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.normal @@ -85,7 +78,6 @@ ColumnLayout { StyledInputField { id: inputField - Layout.preferredWidth: 70 validator: root.validator @@ -152,6 +144,14 @@ ColumnLayout { to: root.to stepSize: root.stepSize + // Use Binding to allow slider to move freely during dragging + Binding { + target: slider + property: "value" + value: root.value + when: !slider.pressed + } + onValueChanged: { // Update input field text in real-time as slider moves during dragging // Always update when slider value changes (during dragging or external updates) @@ -168,13 +168,13 @@ ColumnLayout { inputField.text = root.formatValue(newValue); } } + } - // Use Binding to allow slider to move freely during dragging - Binding { - target: slider - property: "value" - value: root.value - when: !slider.pressed + // Update input field when value changes externally (slider is already bound) + onValueChanged: { + // Only update if component is initialized to avoid issues during creation + if (root._initialized && !inputField.hasFocus) { + inputField.text = root.formatValue(root.value); } } } diff --git a/modules/controlcenter/components/SplitPaneLayout.qml b/modules/controlcenter/components/SplitPaneLayout.qml index 5bf5c41ad..89504a0b8 100644 --- a/modules/controlcenter/components/SplitPaneLayout.qml +++ b/modules/controlcenter/components/SplitPaneLayout.qml @@ -1,26 +1,28 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell.Widgets import qs.components import qs.components.effects import qs.config +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts RowLayout { id: root + spacing: 0 + property Component leftContent: null property Component rightContent: null + property real leftWidthRatio: 0.4 property int leftMinimumWidth: 420 property var leftLoaderProperties: ({}) property var rightLoaderProperties: ({}) + property alias leftLoader: leftLoader property alias rightLoader: rightLoader - spacing: 0 - Item { id: leftPane @@ -47,7 +49,6 @@ RowLayout { anchors.leftMargin: Appearance.padding.large anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 - asynchronous: true sourceComponent: root.leftContent Component.onCompleted: { @@ -89,7 +90,6 @@ RowLayout { anchors.fill: parent anchors.margins: Appearance.padding.large * 2 - asynchronous: true sourceComponent: root.rightContent Component.onCompleted: { diff --git a/modules/controlcenter/components/SplitPaneWithDetails.qml b/modules/controlcenter/components/SplitPaneWithDetails.qml index e8dcb269c..ce8c9d07d 100644 --- a/modules/controlcenter/components/SplitPaneWithDetails.qml +++ b/modules/controlcenter/components/SplitPaneWithDetails.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound import ".." -import QtQuick -import QtQuick.Layouts -import Quickshell.Widgets import qs.components -import qs.components.containers import qs.components.effects +import qs.components.containers import qs.config +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts Item { id: root @@ -48,17 +48,11 @@ Item { nextComponent = targetComponent; } - onPaneChanged: { - nextComponent = getComponentForPane(); - paneId = root.paneIdGenerator(pane); - } - Loader { id: rightLoader anchors.fill: parent - asynchronous: true opacity: 1 scale: 1 transformOrigin: Item.Center @@ -79,6 +73,11 @@ Item { ] } } + + onPaneChanged: { + nextComponent = getComponentForPane(); + paneId = root.paneIdGenerator(pane); + } } } } @@ -87,7 +86,6 @@ Item { id: overlayLoader anchors.fill: parent - asynchronous: true z: 1000 sourceComponent: root.overlayComponent active: root.overlayComponent !== null diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml index 6a65c8e15..ed6bb40a8 100644 --- a/modules/controlcenter/components/WallpaperGrid.qml +++ b/modules/controlcenter/components/WallpaperGrid.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound import ".." -import QtQuick -import Caelestia.Models import qs.components import qs.components.controls import qs.components.effects import qs.components.images import qs.services import qs.config +import Caelestia.Models +import QtQuick GridView { id: root @@ -32,24 +32,25 @@ GridView { delegate: Item { required property var modelData required property int index - readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent - readonly property real itemMargin: Appearance.spacing.normal / 2 - readonly property real itemRadius: Appearance.rounding.normal width: root.cellWidth height: root.cellHeight - StateLayer { - function onClicked(): void { - Wallpapers.setWallpaper(modelData.path); - } + readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent + readonly property real itemMargin: Appearance.spacing.normal / 2 + readonly property real itemRadius: Appearance.rounding.normal + StateLayer { anchors.fill: parent anchors.leftMargin: itemMargin anchors.rightMargin: itemMargin anchors.topMargin: itemMargin anchors.bottomMargin: itemMargin radius: itemRadius + + function onClicked(): void { + Wallpapers.setWallpaper(modelData.path); + } } StyledClippingRect { @@ -116,7 +117,6 @@ GridView { id: fallbackTimer property bool triggered: false - interval: 800 running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null onTriggered: triggered = true @@ -154,16 +154,16 @@ GridView { opacity: 0 - Component.onCompleted: { - opacity = 1; - } - Behavior on opacity { NumberAnimation { duration: 1000 easing.type: Easing.OutCubic } } + + Component.onCompleted: { + opacity = 1; + } } } @@ -201,7 +201,6 @@ GridView { StyledText { id: filenameText - anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom @@ -219,16 +218,16 @@ GridView { opacity: 0 - Component.onCompleted: { - opacity = 1; - } - Behavior on opacity { NumberAnimation { duration: 1000 easing.type: Easing.OutCubic } } + + Component.onCompleted: { + opacity = 1; + } } } } diff --git a/modules/controlcenter/dashboard/DashboardPane.qml b/modules/controlcenter/dashboard/DashboardPane.qml index bd6b9d5f1..df29f0964 100644 --- a/modules/controlcenter/dashboard/DashboardPane.qml +++ b/modules/controlcenter/dashboard/DashboardPane.qml @@ -2,17 +2,17 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets import qs.components -import qs.components.containers import qs.components.controls import qs.components.effects +import qs.components.containers import qs.services import qs.config import qs.utils +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts Item { id: root @@ -40,6 +40,8 @@ Item { property bool showStorage: Config.dashboard.performance.showStorage ?? true property bool showNetwork: Config.dashboard.performance.showNetwork ?? true + anchors.fill: parent + function saveConfig() { Config.dashboard.enabled = root.enabled; Config.dashboard.showOnHover = root.showOnHover; @@ -60,11 +62,8 @@ Item { Config.save(); } - anchors.fill: parent - ClippingRectangle { id: dashboardClippingRect - anchors.fill: parent anchors.margins: Appearance.padding.normal anchors.leftMargin: 0 @@ -81,14 +80,12 @@ Item { anchors.leftMargin: Appearance.padding.large anchors.rightMargin: Appearance.padding.large - asynchronous: true sourceComponent: dashboardContentComponent } } InnerBorder { id: dashboardBorder - leftThickness: 0 rightThickness: Appearance.padding.normal } @@ -98,7 +95,6 @@ Item { StyledFlickable { id: dashboardFlickable - flickableDirection: Flickable.VerticalFlick contentHeight: dashboardLayout.height @@ -108,7 +104,6 @@ Item { ColumnLayout { id: dashboardLayout - anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top diff --git a/modules/controlcenter/dashboard/GeneralSection.qml b/modules/controlcenter/dashboard/GeneralSection.qml index 61e83d3c7..95e7531ed 100644 --- a/modules/controlcenter/dashboard/GeneralSection.qml +++ b/modules/controlcenter/dashboard/GeneralSection.qml @@ -1,11 +1,11 @@ import ".." import "../components" -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.controls import qs.services import qs.config +import QtQuick +import QtQuick.Layouts SectionContainer { id: root diff --git a/modules/controlcenter/dashboard/PerformanceSection.qml b/modules/controlcenter/dashboard/PerformanceSection.qml index eebf5fd29..ac84752b6 100644 --- a/modules/controlcenter/dashboard/PerformanceSection.qml +++ b/modules/controlcenter/dashboard/PerformanceSection.qml @@ -5,8 +5,8 @@ import QtQuick.Layouts import Quickshell.Services.UPower import qs.components import qs.components.controls -import qs.services import qs.config +import qs.services SectionContainer { id: root diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 677cf4182..1df96403f 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -2,20 +2,19 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import "../../launcher/services" -import "../../../utils/scripts/fuzzysort.js" as Fuzzy -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets -import Caelestia import qs.components -import qs.components.containers import qs.components.controls import qs.components.effects +import qs.components.containers import qs.services import qs.config import qs.utils +import Caelestia +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts +import "../../../utils/scripts/fuzzysort.js" as Fuzzy Item { id: root @@ -25,8 +24,21 @@ Item { property var selectedApp: root.session.launcher.active property bool hideFromLauncherChecked: false property bool favouriteChecked: false - property string searchText: "" - property list filteredApps: [] + + anchors.fill: parent + + onSelectedAppChanged: { + root.session.launcher.active = root.selectedApp; + updateToggleState(); + } + + Connections { + target: root.session.launcher + function onActiveChanged() { + root.selectedApp = root.session.launcher.active; + updateToggleState(); + } + } function updateToggleState() { if (!root.selectedApp) { @@ -65,22 +77,147 @@ Item { Config.save(); } + AppDb { + id: allAppsDb + + path: `${Paths.state}/apps.sqlite` + favouriteApps: Config.launcher.favouriteApps + entries: DesktopEntries.applications.values + } + + property string searchText: "" + + // Helper function to get categories for an app (returns array) + function getAppCategories(appId: string): list { + const cats = []; + if (!Config.launcher.categories) + return cats; + + for (let i = 0; i < Config.launcher.categories.length; i++) { + const category = Config.launcher.categories[i]; + if (!category || !category.apps) + continue; + + // Check if this app is in this category's apps list + if (typeof category.apps === 'object' && category.apps.length !== undefined) { + for (let j = 0; j < category.apps.length; j++) { + if (category.apps[j] === appId) { + cats.push(category.name); + break; + } + } + } + } + return cats; + } + + // Helper function to check if app has a specific category + function appHasCategory(appId: string, categoryName: string): bool { + if (!Config.launcher.categories) + return false; + + for (let i = 0; i < Config.launcher.categories.length; i++) { + const category = Config.launcher.categories[i]; + if (!category || category.name.toLowerCase() !== categoryName.toLowerCase()) + continue; + if (!category.apps) + continue; + + if (typeof category.apps === 'object' && category.apps.length !== undefined) { + for (let j = 0; j < category.apps.length; j++) { + if (category.apps[j] === appId) { + return true; + } + } + } + } + return false; + } + + // Helper function to toggle category for an app + function toggleAppCategory(appId: string, categoryName: string): void { + if (!Config.launcher.categories) + return; + + const newCategories = []; + + for (let i = 0; i < Config.launcher.categories.length; i++) { + const category = Config.launcher.categories[i]; + if (!category) + continue; + + const newCategory = { + name: category.name, + icon: category.icon, + apps: [] + }; + + // Copy existing apps + if (category.apps && typeof category.apps === 'object' && category.apps.length !== undefined) { + for (let j = 0; j < category.apps.length; j++) { + newCategory.apps.push(category.apps[j]); + } + } + + // Toggle this app in this category + if (category.name.toLowerCase() === categoryName.toLowerCase()) { + const index = newCategory.apps.indexOf(appId); + if (index >= 0) { + newCategory.apps.splice(index, 1); + } else { + newCategory.apps.push(appId); + } + } + + newCategories.push(newCategory); + } + + Config.launcher.categories = newCategories; + Config.save(); + } + function filterApps(search: string): list { - if (!search || search.trim() === "") { - const apps = []; + let baseApps = []; + + // Filter by category first + if (root.activeCategory === "all") { + for (let i = 0; i < allAppsDb.apps.length; i++) { + baseApps.push(allAppsDb.apps[i]); + } + } else if (root.activeCategory === "favourites") { + for (let i = 0; i < allAppsDb.apps.length; i++) { + const app = allAppsDb.apps[i]; + const appId = app.id || app.entry?.id; + if (Config.launcher.favouriteApps && Config.launcher.favouriteApps.includes(appId)) { + baseApps.push(app); + } + } + } else { + // Custom category + console.log(`Filtering for category: ${root.activeCategory}`); for (let i = 0; i < allAppsDb.apps.length; i++) { - apps.push(allAppsDb.apps[i]); + const app = allAppsDb.apps[i]; + const appId = app.id || app.entry?.id; + if (appHasCategory(appId, root.activeCategory)) { + console.log(`Found app in category: ${appId}`); + baseApps.push(app); + } } - return apps; + console.log(`Total apps in ${root.activeCategory}: ${baseApps.length}`); + } + + // Then filter by search text + if (!search || search.trim() === "") { + return baseApps; } - if (!allAppsDb.apps || allAppsDb.apps.length === 0) { + if (baseApps.length === 0) { return []; } const preparedApps = []; - for (let i = 0; i < allAppsDb.apps.length; i++) { - const app = allAppsDb.apps[i]; + for (let i = 0; i < baseApps.length; i++) { + const app = baseApps[i]; const name = app.name || app.entry?.name || ""; preparedApps.push({ _item: app, @@ -97,57 +234,40 @@ Item { return results.sort((a, b) => b._score - a._score).map(r => r.obj._item); } + property list filteredApps: [] + function updateFilteredApps() { filteredApps = filterApps(searchText); } - anchors.fill: parent - - onSelectedAppChanged: { - root.session.launcher.active = root.selectedApp; - updateToggleState(); - } - onSearchTextChanged: { updateFilteredApps(); } - Component.onCompleted: { - updateFilteredApps(); - } - - Connections { - function onActiveChanged() { - root.selectedApp = root.session.launcher.active; - updateToggleState(); - } + property string activeCategory: "all" - target: root.session.launcher + onActiveCategoryChanged: { + updateFilteredApps(); } - AppDb { - id: allAppsDb - - path: `${Paths.state}/apps.sqlite` - favouriteApps: Config.launcher.favouriteApps - entries: DesktopEntries.applications.values + Component.onCompleted: { + updateFilteredApps(); } Connections { + target: allAppsDb function onAppsChanged() { updateFilteredApps(); } - - target: allAppsDb } SplitPaneLayout { anchors.fill: parent leftContent: Component { + ColumnLayout { id: leftLauncherLayout - anchors.fill: parent spacing: Appearance.spacing.small @@ -186,6 +306,86 @@ Item { } } + // Category tabs + StyledFlickable { + Layout.fillWidth: true + Layout.preferredHeight: categoryRow.height + Layout.topMargin: Appearance.spacing.normal + flickableDirection: Flickable.HorizontalFlick + contentWidth: categoryRow.width + clip: true + + Row { + id: categoryRow + spacing: Appearance.spacing.small + + Repeater { + model: [ + { + id: "all", + name: qsTr("All"), + icon: "apps" + }, + { + id: "favourites", + name: qsTr("Favourites"), + icon: "favorite" + } + ].concat(Config.launcher.categories.map(cat => ({ + id: cat.name.toLowerCase(), + name: cat.name, + icon: cat.icon + }))) + + delegate: StyledRect { + required property var modelData + + property bool isActive: root.activeCategory === modelData.id + + implicitWidth: tabContent.width + Appearance.padding.normal * 2 + implicitHeight: tabContent.height + Appearance.padding.smaller * 2 + + color: isActive ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.full + + StateLayer { + radius: parent.radius + function onClicked(): void { + root.activeCategory = modelData.id; + } + } + + Row { + id: tabContent + anchors.centerIn: parent + spacing: Appearance.spacing.smaller + + MaterialIcon { + anchors.verticalCenter: parent.verticalCenter + text: modelData.icon + font.pointSize: Appearance.font.size.small + color: isActive ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: modelData.name + font.pointSize: Appearance.font.size.small + font.weight: isActive ? 500 : 400 + color: isActive ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + } + } + + Behavior on color { + ColorAnimation { + duration: Appearance.anim.durations.small + } + } + } + } + } + } + StyledText { Layout.topMargin: Appearance.spacing.large text: qsTr("Applications (%1)").arg(root.searchText ? root.filteredApps.length : allAppsDb.apps.length) @@ -284,7 +484,6 @@ Item { Loader { id: appsListLoader - Layout.fillWidth: true Layout.fillHeight: true asynchronous: true @@ -307,20 +506,16 @@ Item { delegate: StyledRect { required property var modelData - readonly property bool isSelected: root.selectedApp === modelData - width: parent ? parent.width : 0 implicitHeight: 40 + readonly property bool isSelected: root.selectedApp === modelData + color: isSelected ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" radius: Appearance.rounding.normal opacity: 0 - Component.onCompleted: { - opacity = 1; - } - Behavior on opacity { NumberAnimation { duration: 1000 @@ -328,6 +523,10 @@ Item { } } + Component.onCompleted: { + opacity = 1; + } + StateLayer { function onClicked(): void { root.session.launcher.active = modelData; @@ -343,7 +542,6 @@ Item { spacing: Appearance.spacing.normal IconImage { - asynchronous: true Layout.alignment: Qt.AlignVCenter implicitSize: 32 source: { @@ -359,11 +557,9 @@ Item { } Loader { + Layout.alignment: Qt.AlignVCenter readonly property bool isHidden: modelData ? Strings.testRegexList(Config.launcher.hiddenApps, modelData.id) : false readonly property bool isFav: modelData ? Strings.testRegexList(Config.launcher.favouriteApps, modelData.id) : false - - Layout.alignment: Qt.AlignVCenter - asynchronous: true active: isHidden || isFav sourceComponent: isHidden ? hiddenIcon : (isFav ? favouriteIcon : null) @@ -371,7 +567,6 @@ Item { Component { id: hiddenIcon - MaterialIcon { text: "visibility_off" fill: 1 @@ -381,7 +576,6 @@ Item { Component { id: favouriteIcon - MaterialIcon { text: "favorite" fill: 1 @@ -415,30 +609,11 @@ Item { nextComponent = targetComponent; } - onPaneChanged: { - nextComponent = getComponentForPane(); - paneId = pane ? (pane.id || pane.entry?.id || "") : ""; - } - - onDisplayedAppChanged: { - if (displayedApp) { - const appId = displayedApp.id || displayedApp.entry?.id; - root.hideFromLauncherChecked = Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0 && Strings.testRegexList(Config.launcher.hiddenApps, appId); - root.favouriteChecked = Config.launcher.favouriteApps && Config.launcher.favouriteApps.length > 0 && Strings.testRegexList(Config.launcher.favouriteApps, appId); - } else { - root.hideFromLauncherChecked = false; - root.favouriteChecked = false; - } - } - Loader { id: rightLauncherLoader - property var displayedApp: rightLauncherPane.displayedApp - anchors.fill: parent - asynchronous: true opacity: 1 scale: 1 transformOrigin: Item.Center @@ -447,6 +622,8 @@ Item { sourceComponent: rightLauncherPane.targetComponent active: true + property var displayedApp: rightLauncherPane.displayedApp + onItemChanged: { if (item && rightLauncherPane.pane && rightLauncherPane.displayedApp !== rightLauncherPane.pane) { rightLauncherPane.displayedApp = rightLauncherPane.pane; @@ -481,6 +658,22 @@ Item { ] } } + + onPaneChanged: { + nextComponent = getComponentForPane(); + paneId = pane ? (pane.id || pane.entry?.id || "") : ""; + } + + onDisplayedAppChanged: { + if (displayedApp) { + const appId = displayedApp.id || displayedApp.entry?.id; + root.hideFromLauncherChecked = Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0 && Strings.testRegexList(Config.launcher.hiddenApps, appId); + root.favouriteChecked = Config.launcher.favouriteApps && Config.launcher.favouriteApps.length > 0 && Strings.testRegexList(Config.launcher.favouriteApps, appId); + } else { + root.hideFromLauncherChecked = false; + root.favouriteChecked = false; + } + } } } } @@ -490,7 +683,6 @@ Item { StyledFlickable { id: settingsFlickable - flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height @@ -514,10 +706,10 @@ Item { ColumnLayout { id: appDetailsLayout + anchors.fill: parent readonly property var displayedApp: parent && parent.displayedApp !== undefined ? parent.displayedApp : null - anchors.fill: parent spacing: Appearance.spacing.normal SettingsHeader { @@ -544,8 +736,6 @@ Item { IconImage { id: appIconImage - - asynchronous: true Layout.alignment: Qt.AlignHCenter implicitSize: Appearance.font.size.extraLarge * 3 * 2 source: { @@ -562,7 +752,6 @@ Item { StyledText { id: appTitleText - Layout.alignment: Qt.AlignHCenter text: displayedApp ? (displayedApp.name || displayedApp.entry?.name || qsTr("Application Details")) : "" font.pointSize: Appearance.font.size.large @@ -580,7 +769,6 @@ Item { StyledFlickable { id: detailsFlickable - anchors.fill: parent flickableDirection: Flickable.VerticalFlick contentHeight: debugLayout.height @@ -591,7 +779,6 @@ Item { ColumnLayout { id: debugLayout - anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top @@ -663,6 +850,120 @@ Item { } } } + + SectionHeader { + Layout.topMargin: Appearance.spacing.large + visible: appDetailsLayout.displayedApp !== null + title: qsTr("Category") + description: qsTr("Assign this app to a category") + } + + SectionContainer { + visible: appDetailsLayout.displayedApp !== null + + GridLayout { + Layout.fillWidth: true + columns: 2 + columnSpacing: Appearance.spacing.small + rowSpacing: Appearance.spacing.small + + Repeater { + model: Config.launcher.categories + + delegate: StyledRect { + required property var modelData + required property int index + + Layout.fillWidth: true + implicitHeight: categoryContent.height + Appearance.padding.normal * 2 + + property string categoryName: modelData.name + property bool isAssigned: { + const app = appDetailsLayout.displayedApp; + if (!app) + return false; + const appId = app.id || app.entry?.id; + return root.appHasCategory(appId, categoryName); + } + + color: isAssigned ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.normal + + StateLayer { + radius: parent.radius + function onClicked(): void { + const app = appDetailsLayout.displayedApp; + if (!app) + return; + const appId = app.id || app.entry?.id; + console.log(`Toggling category ${categoryName} for app ${appId}`); + root.toggleAppCategory(appId, categoryName); + } + } + + Row { + id: categoryContent + anchors.centerIn: parent + spacing: Appearance.spacing.normal + + MaterialIcon { + anchors.verticalCenter: parent.verticalCenter + text: modelData.icon + color: isAssigned ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + } + + StyledText { + anchors.verticalCenter: parent.verticalCenter + text: modelData.name + font.weight: isAssigned ? 500 : 400 + color: isAssigned ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + } + + MaterialIcon { + anchors.verticalCenter: parent.verticalCenter + text: "check" + visible: isAssigned + color: Colours.palette.m3onSecondaryContainer + } + } + } + } + + TextButton { + Layout.columnSpan: 2 + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.small + text: qsTr("Clear Category") + inactiveColour: Colours.palette.m3errorContainer + inactiveOnColour: Colours.palette.m3onErrorContainer + visible: { + const app = appDetailsLayout.displayedApp; + if (!app) + return false; + const appId = app.id || app.entry?.id; + return root.getAppCategories(appId).length > 0; + } + + onClicked: { + const app = appDetailsLayout.displayedApp; + if (!app) + return; + const appId = app.id || app.entry?.id; + + // Remove all categories for this app + const newCategories = []; + for (let i = 0; i < Config.launcher.appCategories.length; i++) { + const item = Config.launcher.appCategories[i]; + if (!item || item.appId !== appId) { + newCategories.push(item); + } + } + Config.launcher.appCategories = newCategories; + Config.save(); + } + } + } + } } } } diff --git a/modules/controlcenter/launcher/Settings.qml b/modules/controlcenter/launcher/Settings.qml index 95fc2fb07..61c904906 100644 --- a/modules/controlcenter/launcher/Settings.qml +++ b/modules/controlcenter/launcher/Settings.qml @@ -2,13 +2,14 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts ColumnLayout { id: root @@ -66,6 +67,135 @@ ColumnLayout { } } + SectionHeader { + Layout.topMargin: Appearance.spacing.large + title: qsTr("Categories") + description: qsTr("Manage launcher categories") + } + + SectionContainer { + contentSpacing: Appearance.spacing.smaller + + ToggleRow { + Layout.bottomMargin: Appearance.spacing.normal + label: qsTr("Enable categories") + checked: Config.launcher.enableCategories + toggle.onToggled: { + Config.launcher.enableCategories = checked; + Config.save(); + } + } + + TextButton { + Layout.fillWidth: true + text: qsTr("+ Add Category") + inactiveColour: Colours.palette.m3primaryContainer + inactiveOnColour: Colours.palette.m3onPrimaryContainer + + onClicked: { + editCategoryDialog.editIndex = -1; + editCategoryDialog.categoryName = ""; + editCategoryDialog.categoryIcon = ""; + editCategoryDialog.open(); + } + } + + ListView { + Layout.fillWidth: true + Layout.preferredHeight: contentHeight + interactive: false + spacing: Appearance.spacing.smaller + + model: Config.launcher.categories + + delegate: StyledRect { + required property var modelData + required property int index + + width: ListView.view ? ListView.view.width : undefined + color: Colours.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.normal + + implicitHeight: categoryRow.implicitHeight + Appearance.padding.normal * 2 + + RowLayout { + id: categoryRow + anchors.fill: parent + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + anchors.topMargin: Appearance.padding.normal + anchors.bottomMargin: Appearance.padding.normal + spacing: Appearance.spacing.normal + + MaterialIcon { + text: modelData.icon + color: Colours.palette.m3onSurface + } + + StyledText { + Layout.fillWidth: true + text: modelData.name + color: Colours.palette.m3onSurface + } + + IconButton { + type: IconButton.Tonal + icon: "arrow_upward" + radius: Appearance.rounding.normal + visible: index > 0 + onClicked: { + const categories = [...Config.launcher.categories]; + const temp = categories[index]; + categories[index] = categories[index - 1]; + categories[index - 1] = temp; + Config.launcher.categories = categories; + Config.save(); + } + } + + IconButton { + type: IconButton.Tonal + icon: "arrow_downward" + radius: Appearance.rounding.normal + visible: index < Config.launcher.categories.length - 1 + onClicked: { + const categories = [...Config.launcher.categories]; + const temp = categories[index]; + categories[index] = categories[index + 1]; + categories[index + 1] = temp; + Config.launcher.categories = categories; + Config.save(); + } + } + + IconButton { + type: IconButton.Tonal + icon: "edit" + radius: Appearance.rounding.normal + onClicked: { + editCategoryDialog.editIndex = index; + editCategoryDialog.categoryName = modelData.name; + editCategoryDialog.categoryIcon = modelData.icon; + editCategoryDialog.open(); + } + } + + IconButton { + type: IconButton.Tonal + icon: "delete" + radius: Appearance.rounding.normal + onClicked: { + const categories = [...Config.launcher.categories]; + categories.splice(index, 1); + Config.launcher.categories = categories; + Config.save(); + } + } + } + } + } + } + SectionHeader { Layout.topMargin: Appearance.spacing.large title: qsTr("Display") @@ -214,4 +344,102 @@ ColumnLayout { value: qsTr("%1").arg(Config.launcher.hiddenApps ? Config.launcher.hiddenApps.length : 0) } } + + Popup { + id: editCategoryDialog + + property int editIndex: -1 + property string categoryName: "" + property string categoryIcon: "" + + parent: Overlay.overlay + x: Math.round((parent.width - width) / 2) + y: Math.round((parent.height - height) / 2) + implicitWidth: Math.min(400, parent.width - Appearance.padding.large * 2) + padding: Appearance.padding.large + + modal: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + background: StyledRect { + color: Colours.palette.m3surfaceContainerHigh + radius: Appearance.rounding.large + } + + contentItem: ColumnLayout { + spacing: Appearance.spacing.normal + + StyledText { + text: editCategoryDialog.editIndex === -1 ? qsTr("Add Category") : qsTr("Edit Category") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + StyledText { + Layout.fillWidth: true + text: qsTr("Configure category name and icon") + wrapMode: Text.WordWrap + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + TextField { + id: categoryNameField + Layout.fillWidth: true + placeholderText: qsTr("Category name") + text: editCategoryDialog.categoryName + onTextChanged: editCategoryDialog.categoryName = text + } + + TextField { + id: categoryIconField + Layout.fillWidth: true + placeholderText: qsTr("Icon name (e.g., folder, code)") + text: editCategoryDialog.categoryIcon + onTextChanged: editCategoryDialog.categoryIcon = text + } + + Item { Layout.preferredHeight: Appearance.spacing.normal } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + TextButton { + Layout.fillWidth: true + text: qsTr("Cancel") + inactiveColour: Colours.tPalette.m3surfaceContainerHigh + inactiveOnColour: Colours.palette.m3onSurface + + onClicked: editCategoryDialog.close() + } + + TextButton { + Layout.fillWidth: true + text: editCategoryDialog.editIndex === -1 ? qsTr("Add") : qsTr("Save") + enabled: editCategoryDialog.categoryName.length > 0 && editCategoryDialog.categoryIcon.length > 0 + inactiveColour: Colours.palette.m3primaryContainer + inactiveOnColour: Colours.palette.m3onPrimaryContainer + + onClicked: { + const categories = [...Config.launcher.categories]; + const newCategory = { + name: editCategoryDialog.categoryName, + icon: editCategoryDialog.categoryIcon + }; + + if (editCategoryDialog.editIndex === -1) { + categories.push(newCategory); + } else { + categories[editCategoryDialog.editIndex] = newCategory; + } + + Config.launcher.categories = categories; + Config.save(); + editCategoryDialog.close(); + } + } + } + } + } } diff --git a/modules/controlcenter/network/EthernetDetails.qml b/modules/controlcenter/network/EthernetDetails.qml index 9b78ccafc..4e60b3d48 100644 --- a/modules/controlcenter/network/EthernetDetails.qml +++ b/modules/controlcenter/network/EthernetDetails.qml @@ -2,14 +2,14 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Layouts import qs.components -import qs.components.containers import qs.components.controls import qs.components.effects +import qs.components.containers import qs.services import qs.config +import QtQuick +import QtQuick.Layouts DeviceDetails { id: root diff --git a/modules/controlcenter/network/EthernetList.qml b/modules/controlcenter/network/EthernetList.qml index 4fcd5b92d..d1eb95798 100644 --- a/modules/controlcenter/network/EthernetList.qml +++ b/modules/controlcenter/network/EthernetList.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Layouts import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config +import QtQuick +import QtQuick.Layouts DeviceList { id: root @@ -147,6 +147,8 @@ DeviceList { color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) StateLayer { + color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + function onClicked(): void { if (modelData.connected && modelData.connection) { Nmcli.disconnectEthernet(modelData.connection, () => {}); @@ -154,8 +156,6 @@ DeviceList { Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {}); } } - - color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } MaterialIcon { diff --git a/modules/controlcenter/network/EthernetPane.qml b/modules/controlcenter/network/EthernetPane.qml index 8fb833fde..59d82bb08 100644 --- a/modules/controlcenter/network/EthernetPane.qml +++ b/modules/controlcenter/network/EthernetPane.qml @@ -2,11 +2,11 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import Quickshell.Widgets import qs.components import qs.components.containers import qs.config +import Quickshell.Widgets +import QtQuick SplitPaneWithDetails { id: root diff --git a/modules/controlcenter/network/EthernetSettings.qml b/modules/controlcenter/network/EthernetSettings.qml index 3d99c47c2..90bfcf46a 100644 --- a/modules/controlcenter/network/EthernetSettings.qml +++ b/modules/controlcenter/network/EthernetSettings.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/controlcenter/network/NetworkSettings.qml b/modules/controlcenter/network/NetworkSettings.qml index 15ac56d24..bda7cb18a 100644 --- a/modules/controlcenter/network/NetworkSettings.qml +++ b/modules/controlcenter/network/NetworkSettings.qml @@ -2,15 +2,15 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.components.effects import qs.services import qs.config +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 990c4a269..26cdbfacd 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -3,17 +3,17 @@ pragma ComponentBehavior: Bound import ".." import "../components" import "." -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets import qs.components -import qs.components.containers import qs.components.controls import qs.components.effects +import qs.components.containers import qs.services import qs.config import qs.utils +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts Item { id: root @@ -120,7 +120,6 @@ Item { Loader { Layout.fillWidth: true - asynchronous: true sourceComponent: Component { VpnList { session: root.session @@ -139,7 +138,6 @@ Item { Loader { Layout.fillWidth: true - asynchronous: true sourceComponent: Component { EthernetList { session: root.session @@ -158,7 +156,6 @@ Item { Loader { Layout.fillWidth: true - asynchronous: true sourceComponent: Component { WirelessList { session: root.session @@ -199,6 +196,9 @@ Item { } Connections { + target: root.session && root.session.vpn ? root.session.vpn : null + enabled: target !== null + function onActiveChanged() { // Clear others when VPN is selected if (root.session && root.session.vpn && root.session.vpn.active) { @@ -209,12 +209,12 @@ Item { } rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); } - - target: root.session && root.session.vpn ? root.session.vpn : null - enabled: target !== null } Connections { + target: root.session && root.session.ethernet ? root.session.ethernet : null + enabled: target !== null + function onActiveChanged() { // Clear others when ethernet is selected if (root.session && root.session.ethernet && root.session.ethernet.active) { @@ -225,12 +225,12 @@ Item { } rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); } - - target: root.session && root.session.ethernet ? root.session.ethernet : null - enabled: target !== null } Connections { + target: root.session && root.session.network ? root.session.network : null + enabled: target !== null + function onActiveChanged() { // Clear others when wireless is selected if (root.session && root.session.network && root.session.network.active) { @@ -241,9 +241,6 @@ Item { } rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); } - - target: root.session && root.session.network ? root.session.network : null - enabled: target !== null } Loader { @@ -281,7 +278,6 @@ Item { StyledFlickable { id: settingsFlickable - flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height @@ -305,7 +301,6 @@ Item { StyledFlickable { id: ethernetFlickable - flickableDirection: Flickable.VerticalFlick contentHeight: ethernetDetailsInner.height @@ -329,7 +324,6 @@ Item { StyledFlickable { id: wirelessFlickable - flickableDirection: Flickable.VerticalFlick contentHeight: wirelessDetailsInner.height @@ -353,7 +347,6 @@ Item { StyledFlickable { id: vpnFlickable - flickableDirection: Flickable.VerticalFlick contentHeight: vpnDetailsInner.height diff --git a/modules/controlcenter/network/VpnDetails.qml b/modules/controlcenter/network/VpnDetails.qml index 8d91067c7..1c71cd716 100644 --- a/modules/controlcenter/network/VpnDetails.qml +++ b/modules/controlcenter/network/VpnDetails.qml @@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts import qs.components -import qs.components.containers import qs.components.controls import qs.components.effects +import qs.components.containers import qs.services import qs.config import qs.utils +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts DeviceDetails { id: root @@ -201,10 +201,6 @@ DeviceDetails { property string displayName: "" property string interfaceName: "" - function closeWithAnimation(): void { - close(); - } - parent: Overlay.overlay anchors.centerIn: parent width: Math.min(400, parent.width - Appearance.padding.large * 2) @@ -250,6 +246,10 @@ DeviceDetails { } } + function closeWithAnimation(): void { + close(); + } + Overlay.modal: Rectangle { color: Qt.rgba(0, 0, 0, 0.4 * editVpnDialog.opacity) } @@ -302,7 +302,6 @@ DeviceDetails { StyledTextField { id: displayNameField - anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignLeft @@ -339,7 +338,6 @@ DeviceDetails { StyledTextField { id: interfaceNameField - anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignLeft diff --git a/modules/controlcenter/network/VpnList.qml b/modules/controlcenter/network/VpnList.qml index 3646841ce..81f4a45a3 100644 --- a/modules/controlcenter/network/VpnList.qml +++ b/modules/controlcenter/network/VpnList.qml @@ -1,15 +1,15 @@ pragma ComponentBehavior: Bound import ".." -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config +import Quickshell +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts ColumnLayout { id: root @@ -21,6 +21,7 @@ ColumnLayout { spacing: Appearance.spacing.normal Connections { + target: VPN function onConnectedChanged() { if (!VPN.connected && root.pendingSwitchIndex >= 0) { const targetIndex = root.pendingSwitchIndex; @@ -49,8 +50,6 @@ ColumnLayout { }); } } - - target: VPN } TextButton { @@ -98,7 +97,6 @@ ColumnLayout { required property int index width: ListView.view ? ListView.view.width : undefined - implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (root.session && root.session.vpn && root.session.vpn.active === modelData) ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal @@ -183,6 +181,7 @@ ColumnLayout { color: Qt.alpha(Colours.palette.m3primaryContainer, VPN.connected && modelData.enabled ? 1 : 0) StateLayer { + enabled: !VPN.connecting function onClicked(): void { const clickedIndex = modelData.index; @@ -217,8 +216,6 @@ ColumnLayout { } } } - - enabled: !VPN.connecting } MaterialIcon { @@ -259,6 +256,8 @@ ColumnLayout { } } } + + implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 } } } @@ -272,43 +271,6 @@ ColumnLayout { property string displayName: "" property string interfaceName: "" - function showProviderSelection(): void { - currentState = "selection"; - open(); - } - - function closeWithAnimation(): void { - close(); - } - - function showAddForm(providerType: string, defaultDisplayName: string): void { - editIndex = -1; - providerName = providerType; - displayName = defaultDisplayName; - interfaceName = ""; - - if (currentState === "selection") { - transitionToForm.start(); - } else { - currentState = "form"; - isClosing = false; - open(); - } - } - - function showEditForm(index: int): void { - const provider = Config.utilities.vpn.provider[index]; - const isObject = typeof provider === "object"; - - editIndex = index; - providerName = isObject ? (provider.name || "custom") : String(provider); - displayName = isObject ? (provider.displayName || providerName) : providerName; - interfaceName = isObject ? (provider.interface || "") : ""; - - currentState = "form"; - open(); - } - parent: Overlay.overlay x: Math.round((parent.width - width) / 2) y: Math.round((parent.height - height) / 2) @@ -359,6 +321,43 @@ ColumnLayout { } } + function showProviderSelection(): void { + currentState = "selection"; + open(); + } + + function closeWithAnimation(): void { + close(); + } + + function showAddForm(providerType: string, defaultDisplayName: string): void { + editIndex = -1; + providerName = providerType; + displayName = defaultDisplayName; + interfaceName = ""; + + if (currentState === "selection") { + transitionToForm.start(); + } else { + currentState = "form"; + isClosing = false; + open(); + } + } + + function showEditForm(index: int): void { + const provider = Config.utilities.vpn.provider[index]; + const isObject = typeof provider === "object"; + + editIndex = index; + providerName = isObject ? (provider.name || "custom") : String(provider); + displayName = isObject ? (provider.displayName || providerName) : providerName; + interfaceName = isObject ? (provider.interface || "") : ""; + + currentState = "form"; + open(); + } + Overlay.modal: Rectangle { color: Qt.rgba(0, 0, 0, 0.4 * vpnDialog.opacity) } @@ -367,6 +366,36 @@ ColumnLayout { currentState = "selection"; } + SequentialAnimation { + id: transitionToForm + + ParallelAnimation { + Anim { + target: selectionContent + property: "opacity" + to: 0 + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + + ScriptAction { + script: { + vpnDialog.currentState = "form"; + } + } + + ParallelAnimation { + Anim { + target: formContent + property: "opacity" + to: 1 + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + } + background: StyledRect { color: Colours.palette.m3surfaceContainerHigh radius: Appearance.rounding.large @@ -557,7 +586,6 @@ ColumnLayout { StyledTextField { id: displayNameField - anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignLeft @@ -594,7 +622,6 @@ ColumnLayout { StyledTextField { id: interfaceNameField - anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignLeft @@ -655,35 +682,5 @@ ColumnLayout { } } } - - SequentialAnimation { - id: transitionToForm - - ParallelAnimation { - Anim { - target: selectionContent - property: "opacity" - to: 0 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.emphasized - } - } - - ScriptAction { - script: { - vpnDialog.currentState = "form"; - } - } - - ParallelAnimation { - Anim { - target: formContent - property: "opacity" - to: 1 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.emphasized - } - } - } } } diff --git a/modules/controlcenter/network/VpnSettings.qml b/modules/controlcenter/network/VpnSettings.qml index ae689050e..49d801d9a 100644 --- a/modules/controlcenter/network/VpnSettings.qml +++ b/modules/controlcenter/network/VpnSettings.qml @@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts -import Quickshell import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.components.effects import qs.services import qs.config +import Quickshell +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts ColumnLayout { id: root @@ -82,7 +82,6 @@ ColumnLayout { required property int index width: ListView.view ? ListView.view.width : undefined - implicitHeight: 60 color: Colours.tPalette.m3surfaceContainerHigh radius: Appearance.rounding.normal @@ -148,6 +147,8 @@ ColumnLayout { } } } + + implicitHeight: 60 } } } diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index 913e5d75a..e8777cdf7 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -3,15 +3,15 @@ pragma ComponentBehavior: Bound import ".." import "../components" import "." -import QtQuick -import QtQuick.Layouts import qs.components -import qs.components.containers import qs.components.controls import qs.components.effects +import qs.components.containers import qs.services import qs.config import qs.utils +import QtQuick +import QtQuick.Layouts DeviceDetails { id: root @@ -19,12 +19,66 @@ DeviceDetails { required property Session session readonly property var network: root.session.network.active + device: network + + Component.onCompleted: { + updateDeviceDetails(); + checkSavedProfile(); + } + + onNetworkChanged: { + connectionUpdateTimer.stop(); + if (network && network.ssid) { + connectionUpdateTimer.start(); + } + updateDeviceDetails(); + checkSavedProfile(); + } + function checkSavedProfile(): void { if (network && network.ssid) { Nmcli.loadSavedConnections(() => {}); } } + Connections { + target: Nmcli + function onActiveChanged() { + updateDeviceDetails(); + } + function onWirelessDeviceDetailsChanged() { + if (network && network.ssid) { + const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); + if (isActive && Nmcli.wirelessDeviceDetails && Nmcli.wirelessDeviceDetails !== null) { + connectionUpdateTimer.stop(); + } + } + } + } + + Timer { + id: connectionUpdateTimer + interval: 500 + repeat: true + running: network && network.ssid + onTriggered: { + if (network) { + const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); + if (isActive) { + if (!Nmcli.wirelessDeviceDetails || Nmcli.wirelessDeviceDetails === null) { + Nmcli.getWirelessDeviceDetails("", () => {}); + } else { + connectionUpdateTimer.stop(); + } + } else { + if (Nmcli.wirelessDeviceDetails !== null) { + Nmcli.wirelessDeviceDetails = null; + } + } + } + } + } + function updateDeviceDetails(): void { if (network && network.ssid) { const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); @@ -38,22 +92,6 @@ DeviceDetails { } } - device: network - - Component.onCompleted: { - updateDeviceDetails(); - checkSavedProfile(); - } - - onNetworkChanged: { - connectionUpdateTimer.stop(); - if (network && network.ssid) { - connectionUpdateTimer.start(); - } - updateDeviceDetails(); - checkSavedProfile(); - } - headerComponent: Component { ConnectionHeader { icon: root.network?.isSecure ? "lock" : "wifi" @@ -170,44 +208,4 @@ DeviceDetails { } } ] - - Connections { - function onActiveChanged() { - updateDeviceDetails(); - } - function onWirelessDeviceDetailsChanged() { - if (network && network.ssid) { - const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); - if (isActive && Nmcli.wirelessDeviceDetails && Nmcli.wirelessDeviceDetails !== null) { - connectionUpdateTimer.stop(); - } - } - } - - target: Nmcli - } - - Timer { - id: connectionUpdateTimer - - interval: 500 - repeat: true - running: network && network.ssid - onTriggered: { - if (network) { - const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); - if (isActive) { - if (!Nmcli.wirelessDeviceDetails || Nmcli.wirelessDeviceDetails === null) { - Nmcli.getWirelessDeviceDetails("", () => {}); - } else { - connectionUpdateTimer.stop(); - } - } else { - if (Nmcli.wirelessDeviceDetails !== null) { - Nmcli.wirelessDeviceDetails = null; - } - } - } - } - } } diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index bff142d3a..57a155fd7 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -3,28 +3,22 @@ pragma ComponentBehavior: Bound import ".." import "../components" import "." -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.components.effects import qs.services import qs.config import qs.utils +import Quickshell +import QtQuick +import QtQuick.Layouts DeviceList { id: root required property Session session - function checkSavedProfileForNetwork(ssid: string): void { - if (ssid && ssid.length > 0) { - Nmcli.loadSavedConnections(() => {}); - } - } - title: qsTr("Networks (%1)").arg(Nmcli.networks.length) description: qsTr("All available WiFi networks") activeItem: session.network.active @@ -110,7 +104,6 @@ DeviceList { required property var modelData width: ListView.view ? ListView.view.width : undefined - implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal @@ -215,6 +208,8 @@ DeviceList { } } } + + implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 } } @@ -224,4 +219,10 @@ DeviceList { checkSavedProfileForNetwork(item.ssid); } } + + function checkSavedProfileForNetwork(ssid: string): void { + if (ssid && ssid.length > 0) { + Nmcli.loadSavedConnections(() => {}); + } + } } diff --git a/modules/controlcenter/network/WirelessPane.qml b/modules/controlcenter/network/WirelessPane.qml index cccf8222c..8150af9cf 100644 --- a/modules/controlcenter/network/WirelessPane.qml +++ b/modules/controlcenter/network/WirelessPane.qml @@ -2,11 +2,11 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import Quickshell.Widgets import qs.components import qs.components.containers import qs.config +import Quickshell.Widgets +import QtQuick SplitPaneWithDetails { id: root diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index 8700f1c43..7ad5204a4 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound import ".." import "." -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components -import qs.components.containers import qs.components.controls import qs.components.effects +import qs.components.containers import qs.services import qs.config import qs.utils +import Quickshell +import QtQuick +import QtQuick.Layouts Item { id: root @@ -29,47 +29,6 @@ Item { } property bool isClosing: false - - function checkConnectionStatus(): void { - if (!root.visible || !connectButton.connecting) { - return; - } - - const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); - - if (isConnected) { - connectionSuccessTimer.start(); - return; - } - - if (Nmcli.pendingConnection === null && connectButton.connecting) { - if (connectionMonitor.repeatCount > 10) { - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.hasError = true; - connectButton.enabled = true; - connectButton.text = qsTr("Connect"); - passwordContainer.passwordBuffer = ""; - if (root.network && root.network.ssid) { - Nmcli.forgetNetwork(root.network.ssid); - } - } - } - } - - function closeDialog(): void { - if (isClosing) { - return; - } - - isClosing = true; - passwordContainer.passwordBuffer = ""; - connectButton.connecting = false; - connectButton.hasError = false; - connectButton.text = qsTr("Connect"); - connectionMonitor.stop(); - } - visible: session.network.showPasswordDialog || isClosing enabled: session.network.showPasswordDialog && !isClosing focus: enabled @@ -105,7 +64,6 @@ Item { color: Colours.tPalette.m3surface opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0 scale: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0.7 - Keys.onEscapePressed: closeDialog() Behavior on opacity { Anim {} @@ -136,6 +94,8 @@ Item { } } + Keys.onEscapePressed: closeDialog() + ColumnLayout { id: content @@ -190,23 +150,16 @@ Item { Item { id: passwordContainer - - property string passwordBuffer: "" - Layout.topMargin: Appearance.spacing.large Layout.fillWidth: true implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2) + focus: true Keys.onPressed: event => { if (!activeFocus) { forceActiveFocus(); } - if (event.key === Qt.Key_Escape) { - event.accepted = false; - closeDialog(); - } - if (connectButton.hasError && event.text && event.text.length > 0) { connectButton.hasError = false; } @@ -224,16 +177,15 @@ Item { } event.accepted = true; } else if (event.text && event.text.length > 0) { - if (event.key === Qt.Key_Tab) { - event.accepted = false; - return; - } passwordBuffer += event.text; event.accepted = true; } } + property string passwordBuffer: "" + Connections { + target: root.session.network function onShowPasswordDialogChanged(): void { if (root.session.network.showPasswordDialog) { Qt.callLater(() => { @@ -243,11 +195,10 @@ Item { }); } } - - target: root.session.network } Connections { + target: root function onVisibleChanged(): void { if (root.visible) { Qt.callLater(() => { @@ -255,8 +206,6 @@ Item { }); } } - - target: root } StyledRect { @@ -288,17 +237,16 @@ Item { } StateLayer { + hoverEnabled: false + cursorShape: Qt.IBeamCursor + function onClicked(): void { passwordContainer.forceActiveFocus(); } - - hoverEnabled: false - cursorShape: Qt.IBeamCursor } StyledText { id: placeholder - anchors.centerIn: parent text: qsTr("Password") color: Colours.palette.m3outline @@ -466,14 +414,40 @@ Item { } } - Timer { - id: connectionMonitor + function checkConnectionStatus(): void { + if (!root.visible || !connectButton.connecting) { + return; + } - property int repeatCount: 0 + const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + if (isConnected) { + connectionSuccessTimer.start(); + return; + } + + if (Nmcli.pendingConnection === null && connectButton.connecting) { + if (connectionMonitor.repeatCount > 10) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.hasError = true; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } + } + } + } + + Timer { + id: connectionMonitor interval: 1000 repeat: true triggeredOnStart: false + property int repeatCount: 0 + onTriggered: { repeatCount++; checkConnectionStatus(); @@ -488,7 +462,6 @@ Item { Timer { id: connectionSuccessTimer - interval: 500 onTriggered: { if (root.visible && Nmcli.active && Nmcli.active.ssid) { @@ -504,6 +477,7 @@ Item { } Connections { + target: Nmcli function onActiveChanged() { if (root.visible) { checkConnectionStatus(); @@ -520,7 +494,18 @@ Item { Nmcli.forgetNetwork(ssid); } } + } - target: Nmcli + function closeDialog(): void { + if (isClosing) { + return; + } + + isClosing = true; + passwordContainer.passwordBuffer = ""; + connectButton.connecting = false; + connectButton.hasError = false; + connectButton.text = qsTr("Connect"); + connectionMonitor.stop(); } } diff --git a/modules/controlcenter/network/WirelessSettings.qml b/modules/controlcenter/network/WirelessSettings.qml index 7b929bf98..b4eb391d4 100644 --- a/modules/controlcenter/network/WirelessSettings.qml +++ b/modules/controlcenter/network/WirelessSettings.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/controlcenter/state/BluetoothState.qml b/modules/controlcenter/state/BluetoothState.qml index 9b16bfe59..8678672df 100644 --- a/modules/controlcenter/state/BluetoothState.qml +++ b/modules/controlcenter/state/BluetoothState.qml @@ -1,5 +1,5 @@ -import QtQuick import Quickshell.Bluetooth +import QtQuick QtObject { id: root diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index d61e932dc..ba65c1e74 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -2,17 +2,17 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets import qs.components -import qs.components.containers import qs.components.controls import qs.components.effects +import qs.components.containers import qs.services import qs.config import qs.utils +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts Item { id: root @@ -22,8 +22,6 @@ Item { property bool activeWindowCompact: Config.bar.activeWindow.compact ?? false property bool activeWindowInverted: Config.bar.activeWindow.inverted ?? false property bool clockShowIcon: Config.bar.clock.showIcon ?? true - property bool clockBackground: Config.bar.clock.background ?? false - property bool clockShowDate: Config.bar.clock.showDate ?? false property bool persistent: Config.bar.persistent ?? true property bool showOnHover: Config.bar.showOnHover ?? true property int dragThreshold: Config.bar.dragThreshold ?? 20 @@ -53,11 +51,24 @@ Item { property list monitorNames: Hypr.monitorNames() property list excludedScreens: Config.bar.excludedScreens ?? [] + anchors.fill: parent + + Component.onCompleted: { + if (Config.bar.entries) { + entriesModel.clear(); + for (let i = 0; i < Config.bar.entries.length; i++) { + const entry = Config.bar.entries[i]; + entriesModel.append({ + id: entry.id, + enabled: entry.enabled !== false + }); + } + } + } + function saveConfig(entryIndex, entryEnabled) { Config.bar.activeWindow.compact = root.activeWindowCompact; Config.bar.activeWindow.inverted = root.activeWindowInverted; - Config.bar.clock.background = root.clockBackground; - Config.bar.clock.showDate = root.clockShowDate; Config.bar.clock.showIcon = root.clockShowIcon; Config.bar.persistent = root.persistent; Config.bar.showOnHover = root.showOnHover; @@ -103,28 +114,12 @@ Item { Config.save(); } - anchors.fill: parent - - Component.onCompleted: { - if (Config.bar.entries) { - entriesModel.clear(); - for (let i = 0; i < Config.bar.entries.length; i++) { - const entry = Config.bar.entries[i]; - entriesModel.append({ - id: entry.id, - enabled: entry.enabled !== false - }); - } - } - } - ListModel { id: entriesModel } ClippingRectangle { id: taskbarClippingRect - anchors.fill: parent anchors.margins: Appearance.padding.normal anchors.leftMargin: 0 @@ -141,14 +136,12 @@ Item { anchors.leftMargin: Appearance.padding.large anchors.rightMargin: Appearance.padding.large - asynchronous: true sourceComponent: taskbarContentComponent } } InnerBorder { id: taskbarBorder - leftThickness: 0 rightThickness: Appearance.padding.normal } @@ -158,7 +151,6 @@ Item { StyledFlickable { id: sidebarFlickable - flickableDirection: Flickable.VerticalFlick contentHeight: sidebarLayout.height @@ -168,7 +160,6 @@ Item { ColumnLayout { id: sidebarLayout - anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top @@ -268,13 +259,11 @@ Item { RowLayout { id: mainRowLayout - Layout.fillWidth: true spacing: Appearance.spacing.normal ColumnLayout { id: leftColumnLayout - Layout.fillWidth: true Layout.alignment: Qt.AlignTop spacing: Appearance.spacing.normal @@ -300,7 +289,6 @@ Item { RowLayout { id: workspacesShownRow - anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -336,7 +324,6 @@ Item { RowLayout { id: workspacesActiveIndicatorRow - anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -370,7 +357,6 @@ Item { RowLayout { id: workspacesOccupiedBgRow - anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -404,7 +390,6 @@ Item { RowLayout { id: workspacesShowWindowsRow - anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -438,7 +423,6 @@ Item { RowLayout { id: workspacesMaxWindowIconsRow - anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -474,7 +458,6 @@ Item { RowLayout { id: workspacesPerMonitorRow - anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -541,7 +524,6 @@ Item { ColumnLayout { id: middleColumnLayout - Layout.fillWidth: true Layout.alignment: Qt.AlignTop spacing: Appearance.spacing.normal @@ -555,24 +537,6 @@ Item { font.pointSize: Appearance.font.size.normal } - SwitchRow { - label: qsTr("Background") - checked: root.clockBackground - onToggled: checked => { - root.clockBackground = checked; - root.saveConfig(); - } - } - - SwitchRow { - label: qsTr("Show date") - checked: root.clockShowDate - onToggled: checked => { - root.clockShowDate = checked; - root.saveConfig(); - } - } - SwitchRow { label: qsTr("Show clock icon") checked: root.clockShowIcon @@ -667,7 +631,6 @@ Item { ColumnLayout { id: rightColumnLayout - Layout.fillWidth: true Layout.alignment: Qt.AlignTop spacing: Appearance.spacing.normal diff --git a/modules/controlcenter/vpn/VpnDetails.qml b/modules/controlcenter/vpn/VpnDetails.qml new file mode 100644 index 000000000..e69de29bb diff --git a/modules/controlcenter/vpn/VpnList.qml b/modules/controlcenter/vpn/VpnList.qml new file mode 100644 index 000000000..e69de29bb diff --git a/modules/dashboard/Background.qml b/modules/dashboard/Background.qml index c6223eb6c..e2a91f741 100644 --- a/modules/dashboard/Background.qml +++ b/modules/dashboard/Background.qml @@ -1,8 +1,8 @@ -import QtQuick -import QtQuick.Shapes import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Shapes ShapePath { id: root diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml index 5b3bbbe6b..bbb42724c 100644 --- a/modules/dashboard/Content.qml +++ b/modules/dashboard/Content.qml @@ -1,27 +1,18 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets import qs.components import qs.components.filedialog import qs.config +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts Item { id: root - required property DrawerVisibilities visibilities - readonly property bool needsKeyboard: { - const count = repeater.count; - for (let i = 0; i < count; i++) { - const item = repeater.itemAt(i) as Loader; - if (item?.sourceComponent === mediaComponent && (item?.item as MediaWrapper)?.needsKeyboard) - return true; - } - return false; - } - required property DashboardState state + required property PersistentProperties visibilities + required property PersistentProperties state required property FileDialog facePicker readonly property var dashboardTabs: { @@ -90,24 +81,21 @@ Item { id: view readonly property int currentIndex: root.state.currentTab - readonly property Item currentItem: { - repeater.count; // Trigger update on count change - return repeater.itemAt(currentIndex); - } + readonly property Item currentItem: row.children[currentIndex] anchors.fill: parent flickableDirection: Flickable.HorizontalFlick - implicitWidth: currentItem?.implicitWidth ?? 0 - implicitHeight: currentItem?.implicitHeight ?? 0 + implicitWidth: currentItem.implicitWidth + implicitHeight: currentItem.implicitHeight - contentX: currentItem?.x ?? 0 + contentX: currentItem.x contentWidth: row.implicitWidth contentHeight: row.implicitHeight onContentXChanged: { - if (!moving || !currentItem) + if (!moving) return; const x = contentX - currentItem.x; @@ -118,24 +106,19 @@ Item { } onDragEnded: { - if (!currentItem) - return; - const x = contentX - currentItem.x; if (x > currentItem.implicitWidth / 10) root.state.currentTab = Math.min(root.state.currentTab + 1, tabs.count - 1); else if (x < -currentItem.implicitWidth / 10) root.state.currentTab = Math.max(root.state.currentTab - 1, 0); else - contentX = Qt.binding(() => currentItem?.x ?? 0); + contentX = Qt.binding(() => currentItem.x); } RowLayout { id: row Repeater { - id: repeater - model: ScriptModel { values: root.dashboardTabs } @@ -163,7 +146,6 @@ Item { Component { id: dashComponent - Dash { visibilities: root.visibilities state: root.state @@ -173,22 +155,19 @@ Item { Component { id: mediaComponent - - MediaWrapper { + Media { visibilities: root.visibilities } } Component { id: performanceComponent - Performance {} } Component { id: weatherComponent - - WeatherTab {} + Weather {} } Behavior on contentX { diff --git a/modules/dashboard/Dash.qml b/modules/dashboard/Dash.qml index c0657f58e..71e224fbe 100644 --- a/modules/dashboard/Dash.qml +++ b/modules/dashboard/Dash.qml @@ -1,15 +1,16 @@ -import "dash" -import QtQuick.Layouts import qs.components import qs.components.filedialog import qs.services import qs.config +import "dash" +import Quickshell +import QtQuick.Layouts GridLayout { id: root - required property DrawerVisibilities visibilities - required property DashboardState state + required property PersistentProperties visibilities + required property PersistentProperties state required property FileDialog facePicker rowSpacing: Appearance.spacing.normal @@ -40,7 +41,7 @@ GridLayout { radius: Appearance.rounding.large * 1.5 - SmallWeather {} + Weather {} } Rect { diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index 490461851..722bc9332 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -1,29 +1,21 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import QtQuick.Shapes -import Quickshell -import Quickshell.Services.Mpris -import Caelestia.Services import qs.components import qs.components.controls import qs.services -import qs.config import qs.utils +import qs.config +import Caelestia.Services +import Quickshell +import Quickshell.Services.Mpris +import QtQuick +import QtQuick.Layouts +import QtQuick.Shapes Item { id: root - required property DrawerVisibilities visibilities - readonly property bool needsKeyboard: lyricMenuOpen - - readonly property real nonAnimHeight: Math.max(cover.implicitHeight + Config.dashboard.sizes.mediaVisualiserSize * 2, lyricMenuOpen ? lyricMenu.implicitHeight : details.implicitHeight, bongocat.implicitHeight) + Appearance.padding.large * 2 - readonly property real detailsHeightWithoutLyrics: details.implicitHeight - lyricsViewInDetails.implicitHeight - - property bool lyricMenuOpen: false - property bool lyricsShowing: LyricsService.lyricsVisible && LyricsService.model.count != 0 - property bool lyricsShowingDebounced: false + required property PersistentProperties visibilities property real playerProgress: { const active = Players.active; @@ -43,21 +35,8 @@ Item { return `${mins}:${secs}`; } - onLyricsShowingChanged: { - if (lyricsShowing) { - lyricsHideDelay.stop(); - lyricsShowingDebounced = true; - } else { - lyricsHideDelay.restart(); - } - } - implicitWidth: cover.implicitWidth + Config.dashboard.sizes.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Appearance.padding.large * 2 - implicitHeight: nonAnimHeight - - Behavior on implicitHeight { - Anim {} - } + implicitHeight: Math.max(cover.implicitHeight + Config.dashboard.sizes.mediaVisualiserSize * 2, details.implicitHeight, bongocat.implicitHeight) + Appearance.padding.large * 2 Behavior on playerProgress { Anim { @@ -70,27 +49,7 @@ Item { interval: Config.dashboard.mediaUpdateInterval triggeredOnStart: true repeat: true - onTriggered: { - if (!Players.active) - return; - LyricsService.updatePosition(); - Players.active?.positionChanged(); - } - } - - Timer { - id: lyricsHideDelay - - interval: 300 - repeat: false - } - - Connections { - function onTriggered() { - root.lyricsShowingDebounced = false; - } - - target: lyricsHideDelay + onTriggered: Players.active?.positionChanged() } ServiceRef { @@ -186,13 +145,6 @@ Item { fillMode: Image.PreserveAspectCrop sourceSize.width: width sourceSize.height: height - - MouseArea { - anchors.fill: parent - onClicked: { - LyricsService.toggleVisibility(); - } - } } } @@ -248,13 +200,6 @@ Item { wrapMode: Players.active ? Text.NoWrap : Text.WordWrap } - LyricsView { - id: lyricsViewInDetails - - Layout.fillWidth: true - Layout.preferredHeight: 200 - } - RowLayout { id: controls @@ -264,14 +209,6 @@ Item { spacing: Appearance.spacing.small - PlayerControl { - type: IconButton.Text - icon: Players.active?.shuffle ? "shuffle_on" : "shuffle" - font.pointSize: Math.round(Appearance.font.size.large) - disabled: !Players.active?.shuffleSupported - onClicked: Players.active.shuffle = !Players.active?.shuffle - } - PlayerControl { type: IconButton.Text icon: "skip_previous" @@ -298,13 +235,6 @@ Item { disabled: !Players.active?.canGoNext onClicked: Players.active?.next() } - - PlayerControl { - type: IconButton.Text - icon: "lyrics" - font.pointSize: Math.round(Appearance.font.size.large) - onClicked: root.lyricMenuOpen = !root.lyricMenuOpen - } } StyledSlider { @@ -328,6 +258,9 @@ Item { } CustomMouseArea { + anchors.fill: parent + acceptedButtons: Qt.NoButton + function onWheel(event: WheelEvent) { const active = Players.active; if (!active?.canSeek || !active?.positionSupported) @@ -339,9 +272,6 @@ Item { active.position = Math.max(0, Math.min(active.length, active.position + delta)); }); } - - anchors.fill: parent - acceptedButtons: Qt.NoButton } } @@ -369,123 +299,83 @@ Item { font.pointSize: Appearance.font.size.small } } - } - - ColumnLayout { - id: leftSection - - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: playerChanger.parent == leftSection ? -playerChanger.height : 0 - anchors.left: details.right - anchors.leftMargin: Appearance.spacing.normal - visible: lyricMenu.height === 0 || opacity > 0 - opacity: lyricMenu.height === 0 ? 1 : 0 + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: Appearance.spacing.small - Behavior on opacity { - NumberAnimation { - duration: Appearance.anim.durations.normal - easing.type: Easing.OutCubic + PlayerControl { + type: IconButton.Text + icon: "move_up" + inactiveOnColour: Colours.palette.m3secondary + padding: Appearance.padding.small + font.pointSize: Appearance.font.size.large + disabled: !Players.active?.canRaise + onClicked: { + Players.active?.raise(); + root.visibilities.dashboard = false; + } } - } - Item { - id: bongocat + SplitButton { + id: playerSelector - implicitWidth: visualiser.width - implicitHeight: visualiser.height + disabled: !Players.list.length + active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null + menu.onItemSelected: item => Players.manualActive = (item as PlayerItem).modelData - AnimatedImage { - anchors.centerIn: parent + menuItems: playerList.instances + fallbackIcon: "music_off" + fallbackText: qsTr("No players") - width: visualiser.width * 0.75 - height: visualiser.height * 0.75 + label.Layout.maximumWidth: slider.implicitWidth * 0.28 + label.elide: Text.ElideRight - playing: Players.active?.isPlaying ?? false - speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment // qmllint disable unresolved-type - source: Paths.absolutePath(Config.paths.mediaGif) - asynchronous: true - fillMode: AnimatedImage.PreserveAspectFit - } - } - } + stateLayer.disabled: true + menuOnTop: true - LyricMenu { - id: lyricMenu + Variants { + id: playerList - anchors.top: parent.top - anchors.left: details.right - anchors.right: parent.right - anchors.leftMargin: Appearance.spacing.normal - - contentHeight: !root.lyricsShowingDebounced ? root.detailsHeightWithoutLyrics + Appearance.padding.large * 5 : root.detailsHeightWithoutLyrics + lyricsViewInDetails.implicitHeight + model: Players.list - visible: root.lyricMenuOpen || height > 0 - height: root.lyricMenuOpen ? implicitHeight : 0 - clip: true - - Behavior on height { - NumberAnimation { - duration: Appearance.anim.durations.normal - easing.type: Easing.OutCubic + PlayerItem {} + } } - } - } - - RowLayout { - id: playerChanger - - parent: !root.lyricsShowingDebounced ? details : leftSection - Layout.alignment: Qt.AlignHCenter - spacing: Appearance.spacing.small - PlayerControl { - type: IconButton.Text - icon: "move_up" - inactiveOnColour: Colours.palette.m3secondary - padding: Appearance.padding.small - font.pointSize: Appearance.font.size.large - disabled: !Players.active?.canRaise - onClicked: { - Players.active?.raise(); - root.visibilities.dashboard = false; + PlayerControl { + type: IconButton.Text + icon: "delete" + inactiveOnColour: Colours.palette.m3error + padding: Appearance.padding.small + font.pointSize: Appearance.font.size.large + disabled: !Players.active?.canQuit + onClicked: Players.active?.quit() } } + } - SplitButton { - id: playerSelector - - disabled: !Players.list.length - active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null - menu.onItemSelected: item => Players.manualActive = (item as PlayerItem).modelData - - menuItems: playerList.instances - fallbackIcon: "music_off" - fallbackText: qsTr("No players") - - label.Layout.maximumWidth: slider.implicitWidth * 0.28 - label.elide: Text.ElideRight + Item { + id: bongocat - stateLayer.disabled: true - menuOnTop: true + anchors.verticalCenter: parent.verticalCenter + anchors.left: details.right + anchors.leftMargin: Appearance.spacing.normal - Variants { - id: playerList + implicitWidth: visualiser.width + implicitHeight: visualiser.height - model: Players.list + AnimatedImage { + anchors.centerIn: parent - PlayerItem {} - } - } + width: visualiser.width * 0.75 + height: visualiser.height * 0.75 - PlayerControl { - type: IconButton.Text - icon: "delete" - inactiveOnColour: Colours.palette.m3error - padding: Appearance.padding.small - font.pointSize: Appearance.font.size.large - disabled: !Players.active?.canQuit - onClicked: Players.active?.quit() + playing: Players.active?.isPlaying ?? false + speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment // qmllint disable unresolved-type + source: Paths.absolutePath(Config.paths.mediaGif) + asynchronous: true + fillMode: AnimatedImage.PreserveAspectFit } } diff --git a/modules/dashboard/Performance.qml b/modules/dashboard/Performance.qml index 8d8d2c016..339c731fc 100644 --- a/modules/dashboard/Performance.qml +++ b/modules/dashboard/Performance.qml @@ -5,8 +5,8 @@ import Quickshell.Services.UPower import Caelestia.Internal import qs.components import qs.components.misc -import qs.services import qs.config +import qs.services Item { id: root @@ -685,22 +685,22 @@ Item { property real smoothMax: targetMax anchors.fill: parent - line1: NetworkUsage.uploadBuffer // qmllint disable missing-type + line1: NetworkUsage.uploadBuffer line1Color: Colours.palette.m3secondary line1FillAlpha: 0.15 - line2: NetworkUsage.downloadBuffer // qmllint disable missing-type + line2: NetworkUsage.downloadBuffer line2Color: Colours.palette.m3tertiary line2FillAlpha: 0.2 maxValue: smoothMax historyLength: NetworkUsage.historyLength Connections { + target: NetworkUsage.downloadBuffer + function onValuesChanged(): void { sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024); slideAnim.restart(); } - - target: NetworkUsage.downloadBuffer } NumberAnimation { diff --git a/modules/dashboard/Tabs.qml b/modules/dashboard/Tabs.qml index adeab928d..6e09e767a 100644 --- a/modules/dashboard/Tabs.qml +++ b/modules/dashboard/Tabs.qml @@ -1,19 +1,19 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Controls -import Quickshell -import Quickshell.Widgets import qs.components import qs.components.controls import qs.services import qs.config +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Controls Item { id: root required property real nonAnimWidth - required property DashboardState state + required property PersistentProperties state required property var tabs readonly property alias count: bar.count @@ -111,13 +111,6 @@ Item { contentItem: CustomMouseArea { id: mouse - function onWheel(event: WheelEvent): void { - if (event.angleDelta.y < 0) - root.state.currentTab = Math.min(root.state.currentTab + 1, bar.count - 1); - else if (event.angleDelta.y > 0) - root.state.currentTab = Math.max(root.state.currentTab - 1, 0); - } - implicitWidth: Math.max(icon.width, label.width) implicitHeight: icon.height + label.height @@ -136,6 +129,13 @@ Item { rippleAnim.restart(); } + function onWheel(event: WheelEvent): void { + if (event.angleDelta.y < 0) + root.state.currentTab = Math.min(root.state.currentTab + 1, bar.count - 1); + else if (event.angleDelta.y > 0) + root.state.currentTab = Math.max(root.state.currentTab - 1, 0); + } + SequentialAnimation { id: rippleAnim diff --git a/modules/dashboard/Weather.qml b/modules/dashboard/Weather.qml new file mode 100644 index 000000000..3981633ac --- /dev/null +++ b/modules/dashboard/Weather.qml @@ -0,0 +1,280 @@ +import qs.components +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + implicitWidth: layout.implicitWidth > 800 ? layout.implicitWidth : 840 + implicitHeight: layout.implicitHeight + + readonly property var today: Weather.forecast && Weather.forecast.length > 0 ? Weather.forecast[0] : null + + Component.onCompleted: Weather.reload() + + ColumnLayout { + id: layout + + anchors.fill: parent + spacing: Appearance.spacing.smaller + + RowLayout { + Layout.leftMargin: Appearance.padding.large + Layout.rightMargin: Appearance.padding.large + Layout.fillWidth: true + + Column { + spacing: Appearance.spacing.small / 2 + + StyledText { + text: Weather.city || qsTr("Loading...") + font.pointSize: Appearance.font.size.extraLarge + font.weight: 600 + color: Colours.palette.m3onSurface + } + + StyledText { + text: new Date().toLocaleDateString(Qt.locale(), "dddd, MMMM d") + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + } + + Item { + Layout.fillWidth: true + } + + Row { + spacing: Appearance.spacing.large + + WeatherStat { + icon: "wb_twilight" + label: "Sunrise" + value: Weather.sunrise + colour: Colours.palette.m3tertiary + } + + WeatherStat { + icon: "bedtime" + label: "Sunset" + value: Weather.sunset + colour: Colours.palette.m3tertiary + } + } + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: bigInfoRow.implicitHeight + Appearance.padding.small * 2 + + radius: Appearance.rounding.large * 2 + color: Colours.tPalette.m3surfaceContainer + + RowLayout { + id: bigInfoRow + + anchors.centerIn: parent + spacing: Appearance.spacing.large + + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + text: Weather.icon + font.pointSize: Appearance.font.size.extraLarge * 3 + color: Colours.palette.m3secondary + animate: true + } + + ColumnLayout { + Layout.alignment: Qt.AlignVCenter + spacing: -Appearance.spacing.small + + StyledText { + text: Weather.temp + font.pointSize: Appearance.font.size.extraLarge * 2 + font.weight: 500 + color: Colours.palette.m3primary + } + + StyledText { + Layout.leftMargin: Appearance.padding.small + text: Weather.description + font.pointSize: Appearance.font.size.normal + color: Colours.palette.m3onSurfaceVariant + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + DetailCard { + icon: "water_drop" + label: "Humidity" + value: Weather.humidity + "%" + colour: Colours.palette.m3secondary + } + DetailCard { + icon: "thermostat" + label: "Feels Like" + value: Weather.feelsLike + colour: Colours.palette.m3primary + } + DetailCard { + icon: "air" + label: "Wind" + value: Weather.windSpeed ? Weather.windSpeed + " km/h" : "--" + colour: Colours.palette.m3tertiary + } + } + + StyledText { + Layout.topMargin: Appearance.spacing.normal + Layout.leftMargin: Appearance.padding.normal + visible: forecastRepeater.count > 0 + text: qsTr("7-Day Forecast") + font.pointSize: Appearance.font.size.normal + font.weight: 600 + color: Colours.palette.m3onSurface + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + Repeater { + id: forecastRepeater + + model: Weather.forecast + + StyledRect { + id: forecastItem + + required property int index + required property var modelData + + Layout.fillWidth: true + implicitHeight: forecastItemColumn.implicitHeight + Appearance.padding.normal * 2 + + radius: Appearance.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: forecastItemColumn + + anchors.centerIn: parent + spacing: Appearance.spacing.small + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: forecastItem.index === 0 ? qsTr("Today") : new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale(), "ddd") + font.pointSize: Appearance.font.size.normal + font.weight: 600 + color: Colours.palette.m3primary + } + + StyledText { + Layout.topMargin: -Appearance.spacing.small / 2 + Layout.alignment: Qt.AlignHCenter + text: new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale(), "MMM d") + font.pointSize: Appearance.font.size.small + opacity: 0.7 + color: Colours.palette.m3onSurfaceVariant + } + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: forecastItem.modelData.icon + font.pointSize: Appearance.font.size.extraLarge + color: Colours.palette.m3secondary + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: Config.services.useFahrenheit ? forecastItem.modelData.maxTempF + "°" + " / " + forecastItem.modelData.minTempF + "°" : forecastItem.modelData.maxTempC + "°" + " / " + forecastItem.modelData.minTempC + "°" + font.weight: 600 + color: Colours.palette.m3tertiary + } + } + } + } + } + } + + component DetailCard: StyledRect { + id: detailRoot + + property string icon + property string label + property string value + property color colour + + Layout.fillWidth: true + Layout.preferredHeight: 60 + radius: Appearance.rounding.small + color: Colours.tPalette.m3surfaceContainer + + Row { + anchors.centerIn: parent + spacing: Appearance.spacing.normal + + MaterialIcon { + text: detailRoot.icon + color: detailRoot.colour + font.pointSize: Appearance.font.size.large + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 0 + + StyledText { + text: detailRoot.label + font.pointSize: Appearance.font.size.smaller + opacity: 0.7 + horizontalAlignment: Text.AlignLeft + } + StyledText { + text: detailRoot.value + font.weight: 600 + horizontalAlignment: Text.AlignLeft + } + } + } + } + + component WeatherStat: Row { + id: weatherStat + + property string icon + property string label + property string value + property color colour + + spacing: Appearance.spacing.small + + MaterialIcon { + text: weatherStat.icon + font.pointSize: Appearance.font.size.extraLarge + color: weatherStat.colour + } + + Column { + StyledText { + text: weatherStat.label + font.pointSize: Appearance.font.size.smaller + color: Colours.palette.m3onSurfaceVariant + } + StyledText { + text: weatherStat.value + font.pointSize: Appearance.font.size.small + font.weight: 600 + color: Colours.palette.m3onSurface + } + } + } +} diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml index 596c21b75..0e37909e8 100644 --- a/modules/dashboard/Wrapper.qml +++ b/modules/dashboard/Wrapper.qml @@ -1,19 +1,21 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell -import Caelestia import qs.components import qs.components.filedialog import qs.config import qs.utils +import Caelestia +import Quickshell +import QtQuick Item { id: root - required property DrawerVisibilities visibilities - readonly property bool needsKeyboard: (content.item as Content)?.needsKeyboard ?? false - readonly property DashboardState dashState: DashboardState { + required property PersistentProperties visibilities + readonly property PersistentProperties dashState: PersistentProperties { + property int currentTab + property date currentDate: new Date() + reloadableId: "dashboardState" } readonly property FileDialog facePicker: FileDialog { @@ -28,7 +30,7 @@ Item { } } - readonly property real nonAnimHeight: state === "visible" ? ((content.item as Content)?.nonAnimHeight ?? 0) : 0 + readonly property real nonAnimHeight: state === "visible" ? (content.item?.nonAnimHeight ?? 0) : 0 visible: height > 0 implicitHeight: 0 diff --git a/modules/dashboard/dash/Calendar.qml b/modules/dashboard/dash/Calendar.qml index bed3c4d34..56c04938c 100644 --- a/modules/dashboard/dash/Calendar.qml +++ b/modules/dashboard/dash/Calendar.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts import qs.components -import qs.components.controls import qs.components.effects +import qs.components.controls import qs.services import qs.config +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts CustomMouseArea { id: root @@ -17,13 +17,6 @@ CustomMouseArea { readonly property int currMonth: state.currentDate.getMonth() readonly property int currYear: state.currentDate.getFullYear() - function onWheel(event: WheelEvent): void { - if (event.angleDelta.y > 0) - root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1); - else if (event.angleDelta.y < 0) - root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1); - } - anchors.left: parent.left anchors.right: parent.right implicitHeight: inner.implicitHeight + inner.anchors.margins * 2 @@ -31,6 +24,13 @@ CustomMouseArea { acceptedButtons: Qt.MiddleButton onClicked: root.state.currentDate = new Date() + function onWheel(event: WheelEvent): void { + if (event.angleDelta.y > 0) + root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1); + else if (event.angleDelta.y < 0) + root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1); + } + ColumnLayout { id: inner @@ -51,11 +51,11 @@ CustomMouseArea { StateLayer { id: prevMonthStateLayer + radius: Appearance.rounding.full + function onClicked(): void { root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1); } - - radius: Appearance.rounding.full } MaterialIcon { @@ -76,10 +76,6 @@ CustomMouseArea { implicitHeight: monthYearDisplay.implicitHeight + Appearance.padding.small * 2 StateLayer { - function onClicked(): void { - root.state.currentDate = new Date(); - } - anchors.fill: monthYearDisplay anchors.margins: -Appearance.padding.small anchors.leftMargin: -Appearance.padding.normal @@ -90,6 +86,10 @@ CustomMouseArea { const now = new Date(); return root.currMonth === now.getMonth() && root.currYear === now.getFullYear(); } + + function onClicked(): void { + root.state.currentDate = new Date(); + } } StyledText { @@ -111,11 +111,11 @@ CustomMouseArea { StateLayer { id: nextMonthStateLayer + radius: Appearance.rounding.full + function onClicked(): void { root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1); } - - radius: Appearance.rounding.full } MaterialIcon { diff --git a/modules/dashboard/dash/DateTime.qml b/modules/dashboard/dash/DateTime.qml index 7f786a198..e74044883 100644 --- a/modules/dashboard/dash/DateTime.qml +++ b/modules/dashboard/dash/DateTime.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Layouts Item { id: root @@ -48,7 +48,6 @@ Item { } Loader { - asynchronous: true Layout.alignment: Qt.AlignHCenter active: Config.services.useTwelveHourClock diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index 1a4f0c938..d65066954 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -1,10 +1,10 @@ -import QtQuick -import QtQuick.Shapes -import Caelestia.Services import qs.components import qs.services import qs.config import qs.utils +import Caelestia.Services +import QtQuick +import QtQuick.Shapes Item { id: root @@ -174,30 +174,30 @@ Item { spacing: Appearance.spacing.small Control { + icon: "skip_previous" + canUse: Players.active?.canGoPrevious ?? false + function onClicked(): void { Players.active?.previous(); } - - icon: "skip_previous" - canUse: Players.active?.canGoPrevious ?? false } Control { + icon: Players.active?.isPlaying ? "pause" : "play_arrow" + canUse: Players.active?.canTogglePlaying ?? false + function onClicked(): void { Players.active?.togglePlaying(); } - - icon: Players.active?.isPlaying ? "pause" : "play_arrow" - canUse: Players.active?.canTogglePlaying ?? false } Control { + icon: "skip_next" + canUse: Players.active?.canGoNext ?? false + function onClicked(): void { Players.active?.next(); } - - icon: "skip_next" - canUse: Players.active?.canGoNext ?? false } } @@ -213,7 +213,7 @@ Item { anchors.margins: Appearance.padding.large * 2 playing: Players.active?.isPlaying ?? false - speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment // qmllint disable unresolved-type + speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment source: Paths.absolutePath(Config.paths.mediaGif) asynchronous: true fillMode: AnimatedImage.PreserveAspectFit @@ -224,7 +224,6 @@ Item { required property string icon required property bool canUse - function onClicked(): void { } @@ -232,12 +231,12 @@ Item { implicitHeight: implicitWidth StateLayer { + disabled: !control.canUse + radius: Appearance.rounding.full + function onClicked(): void { control.onClicked(); } - - disabled: !control.canUse - radius: Appearance.rounding.full } MaterialIcon { diff --git a/modules/dashboard/dash/Resources.qml b/modules/dashboard/dash/Resources.qml index f5ac45569..7f44a9d0b 100644 --- a/modules/dashboard/dash/Resources.qml +++ b/modules/dashboard/dash/Resources.qml @@ -1,8 +1,8 @@ -import QtQuick import qs.components import qs.components.misc import qs.services import qs.config +import QtQuick Row { id: root diff --git a/modules/dashboard/dash/User.qml b/modules/dashboard/dash/User.qml index 5fb71effa..5ede24bb5 100644 --- a/modules/dashboard/dash/User.qml +++ b/modules/dashboard/dash/User.qml @@ -1,17 +1,18 @@ -import QtQuick import qs.components import qs.components.effects -import qs.components.filedialog import qs.components.images +import qs.components.filedialog import qs.services import qs.config import qs.utils +import Quickshell +import QtQuick Row { id: root - required property DrawerVisibilities visibilities - required property DashboardState state + required property PersistentProperties visibilities + required property PersistentProperties state required property FileDialog facePicker padding: Appearance.padding.large @@ -70,12 +71,12 @@ Row { opacity: parent.containsMouse ? 1 : 0 StateLayer { + color: Colours.palette.m3onPrimary + function onClicked(): void { root.visibilities.launcher = false; root.facePicker.open(); } - - color: Colours.palette.m3onPrimary } MaterialIcon { diff --git a/modules/dashboard/dash/Weather.qml b/modules/dashboard/dash/Weather.qml new file mode 100644 index 000000000..c90ccf0a4 --- /dev/null +++ b/modules/dashboard/dash/Weather.qml @@ -0,0 +1,57 @@ +import qs.components +import qs.services +import qs.config +import qs.utils +import QtQuick + +Item { + id: root + + anchors.centerIn: parent + + implicitWidth: icon.implicitWidth + info.implicitWidth + info.anchors.leftMargin + + Component.onCompleted: Weather.reload() + + MaterialIcon { + id: icon + + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + + animate: true + text: Weather.icon + color: Colours.palette.m3secondary + font.pointSize: Appearance.font.size.extraLarge * 2 + } + + Column { + id: info + + anchors.verticalCenter: parent.verticalCenter + anchors.left: icon.right + anchors.leftMargin: Appearance.spacing.large + + spacing: Appearance.spacing.small + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + + animate: true + text: Weather.temp + color: Colours.palette.m3primary + font.pointSize: Appearance.font.size.extraLarge + font.weight: 500 + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + + animate: true + text: Weather.description + + elide: Text.ElideRight + width: Math.min(implicitWidth, root.parent.width - icon.implicitWidth - info.anchors.leftMargin - Appearance.padding.large * 2) + } + } +} diff --git a/modules/drawers/Backgrounds.qml b/modules/drawers/Backgrounds.qml index b79cd9195..7fa2ca176 100644 --- a/modules/drawers/Backgrounds.qml +++ b/modules/drawers/Backgrounds.qml @@ -1,14 +1,15 @@ -import QtQuick -import QtQuick.Shapes +import qs.services import qs.config -import qs.modules.dashboard as Dashboard -import qs.modules.launcher as Launcher -import qs.modules.notifications as Notifications import qs.modules.osd as Osd +import qs.modules.notifications as Notifications import qs.modules.session as Session -import qs.modules.sidebar as Sidebar -import qs.modules.utilities as Utilities +import qs.modules.launcher as Launcher +import qs.modules.dashboard as Dashboard import qs.modules.bar.popouts as BarPopouts +import qs.modules.utilities as Utilities +import qs.modules.sidebar as Sidebar +import QtQuick +import QtQuick.Shapes Shape { id: root @@ -22,14 +23,14 @@ Shape { preferredRendererType: Shape.CurveRenderer Osd.Background { - wrapper: root.panels.osd // qmllint disable incompatible-type + wrapper: root.panels.osd startX: root.width - root.panels.session.width - root.panels.sidebar.width startY: (root.height - wrapper.height) / 2 - rounding } Notifications.Background { - wrapper: root.panels.notifications // qmllint disable incompatible-type + wrapper: root.panels.notifications sidebar: sidebar startX: root.width @@ -37,28 +38,28 @@ Shape { } Session.Background { - wrapper: root.panels.session // qmllint disable incompatible-type + wrapper: root.panels.session startX: root.width - root.panels.sidebar.width startY: (root.height - wrapper.height) / 2 - rounding } Launcher.Background { - wrapper: root.panels.launcher // qmllint disable incompatible-type + wrapper: root.panels.launcher startX: (root.width - wrapper.width) / 2 - rounding startY: root.height } Dashboard.Background { - wrapper: root.panels.dashboard // qmllint disable incompatible-type + wrapper: root.panels.dashboard startX: (root.width - wrapper.width) / 2 - rounding startY: 0 } BarPopouts.Background { - wrapper: root.panels.popouts // qmllint disable incompatible-type + wrapper: root.panels.popouts invertBottomRounding: wrapper.y + wrapper.height + 1 >= root.height startX: wrapper.x @@ -66,7 +67,7 @@ Shape { } Utilities.Background { - wrapper: root.panels.utilities // qmllint disable incompatible-type + wrapper: root.panels.utilities sidebar: sidebar startX: root.width @@ -76,7 +77,7 @@ Shape { Sidebar.Background { id: sidebar - wrapper: root.panels.sidebar // qmllint disable incompatible-type + wrapper: root.panels.sidebar panels: root.panels startX: root.width diff --git a/modules/drawers/Border.qml b/modules/drawers/Border.qml index 13f92a4b2..6fdd73bd7 100644 --- a/modules/drawers/Border.qml +++ b/modules/drawers/Border.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Effects import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Effects Item { id: root diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 864ad5c46..5b33076b8 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -1,17 +1,16 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Controls -import QtQuick.Effects -import Quickshell -import Quickshell.Hyprland -import Quickshell.Wayland import qs.components import qs.components.containers import qs.services import qs.config import qs.utils import qs.modules.bar +import Quickshell +import Quickshell.Wayland +import Quickshell.Hyprland +import QtQuick +import QtQuick.Effects Variants { model: Screens.screens @@ -36,7 +35,7 @@ Variants { return 0; const mon = Hypr.monitorFor(screen); - if (mon?.lastIpcObject.specialWorkspace?.name || mon?.activeWorkspace?.lastIpcObject.windows > 0) + if (mon?.lastIpcObject?.specialWorkspace?.name || mon?.activeWorkspace?.lastIpcObject?.windows > 0) return 0; const thresholds = []; @@ -55,16 +54,25 @@ Variants { screen: scope.modelData name: "drawers" WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + + Region { + id: contextMenuRegion + x: panels.contextMenuX + bar.implicitWidth + y: panels.contextMenuY + Config.border.thickness + width: panels.contextMenuWidth + height: panels.contextMenuHeight + intersection: Intersection.Subtract + } mask: Region { - x: bar.clampedWidth + win.dragMaskPadding - y: Config.border.clampedThickness + win.dragMaskPadding - width: win.width - bar.clampedWidth - Config.border.clampedThickness - win.dragMaskPadding * 2 - height: win.height - Config.border.clampedThickness * 2 - win.dragMaskPadding * 2 + x: bar.implicitWidth + win.dragMaskPadding + y: Config.border.thickness + win.dragMaskPadding + width: win.width - bar.implicitWidth - Config.border.thickness - win.dragMaskPadding * 2 + height: win.height - Config.border.thickness * 2 - win.dragMaskPadding * 2 intersection: Intersection.Xor - regions: regions.instances // qmllint disable stale-property-read + regions: panels.contextMenuOpen ? regions.instances.concat([contextMenuRegion]) : regions.instances } anchors.top: true @@ -91,7 +99,7 @@ Variants { HyprlandFocusGrab { id: focusGrab - active: (visibilities.launcher && Config.launcher.enabled) || (visibilities.session && Config.session.enabled) || (visibilities.sidebar && Config.sidebar.enabled) || (!Config.dashboard.showOnHover && visibilities.dashboard && Config.dashboard.enabled) || (panels.popouts.currentName.startsWith("traymenu") && (panels.popouts.current as StackView)?.depth > 1) + active: !panels.contextMenuOpen && ((visibilities.launcher && Config.launcher.enabled) || (visibilities.session && Config.session.enabled) || (visibilities.sidebar && Config.sidebar.enabled) || (!Config.dashboard.showOnHover && visibilities.dashboard && Config.dashboard.enabled) || (panels.popouts.currentName.startsWith("traymenu") && panels.popouts.current?.depth > 1)) windows: [win] onCleared: { visibilities.launcher = false; @@ -133,9 +141,17 @@ Variants { } } - DrawerVisibilities { + PersistentProperties { id: visibilities + property bool bar + property bool osd + property bool session + property bool launcher + property bool dashboard + property bool utilities + property bool sidebar + Component.onCompleted: Visibilities.load(scope.modelData, this) } @@ -152,6 +168,7 @@ Variants { screen: scope.modelData visibilities: visibilities bar: bar + windowRef: win } BarWrapper { diff --git a/modules/drawers/Exclusions.qml b/modules/drawers/Exclusions.qml index f43afb9a2..e4015c89a 100644 --- a/modules/drawers/Exclusions.qml +++ b/modules/drawers/Exclusions.qml @@ -1,16 +1,15 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell import qs.components.containers import qs.config -import qs.modules.bar as Bar +import Quickshell +import QtQuick Scope { id: root required property ShellScreen screen - required property Bar.BarWrapper bar + required property Item bar ExclusionZone { anchors.left: true diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index fcb128a93..9579b15ae 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -1,20 +1,17 @@ -import QtQuick -import QtQuick.Controls -import Quickshell -import qs.components import qs.components.controls import qs.config -import qs.modules.bar as Bar import qs.modules.bar.popouts as BarPopouts +import Quickshell +import QtQuick CustomMouseArea { id: root required property ShellScreen screen required property BarPopouts.Wrapper popouts - required property DrawerVisibilities visibilities + required property PersistentProperties visibilities required property Panels panels - required property Bar.BarWrapper bar + required property Item bar property point dragStart property bool dashboardShortcutActive @@ -36,15 +33,15 @@ CustomMouseArea { } function inRightPanel(panel: Item, x: real, y: real): bool { - return x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panel.x) && withinPanelHeight(panel, x, y); + return x > bar.implicitWidth + panel.x && withinPanelHeight(panel, x, y); } function inTopPanel(panel: Item, x: real, y: real): bool { - return y < Math.max(Config.border.minThickness, Config.border.thickness + panel.height) + panel.y && withinPanelWidth(panel, x, y); + return y < Config.border.thickness + panel.y + panel.height && withinPanelWidth(panel, x, y); } - function inBottomPanel(panel: Item, x: real, y: real, isCorner = false): bool { - return y > height - Math.max(Config.border.minThickness, Config.border.thickness + panel.height) - (isCorner ? Config.border.rounding : 0) && withinPanelWidth(panel, x, y); + function inBottomPanel(panel: Item, x: real, y: real): bool { + return y > root.height - Config.border.thickness - panel.height - Config.border.rounding && withinPanelWidth(panel, x, y); } function onWheel(event: WheelEvent): void { @@ -71,7 +68,7 @@ CustomMouseArea { if (!utilitiesShortcutActive) visibilities.utilities = false; - if (!popouts.currentName.startsWith("traymenu") || ((popouts.current as StackView)?.depth ?? 0) <= 1) { + if (!popouts.currentName.startsWith("traymenu") || (popouts.current?.depth ?? 0) <= 1) { popouts.hasCurrent = false; bar.closeTray(); } @@ -91,11 +88,11 @@ CustomMouseArea { const dragY = y - dragStart.y; // Show bar in non-exclusive mode on hover - if (!visibilities.bar && Config.bar.showOnHover && x < bar.clampedWidth) + if (!visibilities.bar && Config.bar.showOnHover && x < bar.implicitWidth) bar.isHovered = true; // Show/hide bar on drag - if (pressed && dragStart.x < bar.clampedWidth) { + if (pressed && dragStart.x < bar.implicitWidth) { if (dragX > Config.bar.dragThreshold) visibilities.bar = true; else if (dragX < -Config.bar.dragThreshold) @@ -116,7 +113,7 @@ CustomMouseArea { root.panels.osd.hovered = true; } - const showSidebar = pressed && dragStart.x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panels.sidebar.x); + const showSidebar = pressed && dragStart.x > bar.implicitWidth + panels.sidebar.x; // Show/hide session on drag if (pressed && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { @@ -191,7 +188,7 @@ CustomMouseArea { } // Show utilities on hover - const showUtilities = inBottomPanel(panels.utilities, x, y, true); + const showUtilities = inBottomPanel(panels.utilities, x, y); // Always update visibility based on hover if not in shortcut mode if (!utilitiesShortcutActive) { @@ -204,7 +201,7 @@ CustomMouseArea { // Show popouts on hover if (x < bar.implicitWidth) { bar.checkPopout(y); - } else if ((!popouts.currentName.startsWith("traymenu") || ((popouts.current as StackView)?.depth ?? 0) <= 1) && !inLeftPanel(panels.popouts, x, y)) { + } else if ((!popouts.currentName.startsWith("traymenu") || (popouts.current?.depth ?? 0) <= 1) && !inLeftPanel(panels.popouts, x, y)) { popouts.hasCurrent = false; bar.closeTray(); } @@ -212,6 +209,8 @@ CustomMouseArea { // Monitor individual visibility changes Connections { + target: root.visibilities + function onLauncherChanged() { // If launcher is hidden, clear shortcut flags for dashboard and OSD if (!root.visibilities.launcher) { @@ -271,7 +270,5 @@ CustomMouseArea { root.utilitiesShortcutActive = false; } } - - target: root.visibilities } } diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index f2531cfa3..c99bd9029 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -1,24 +1,24 @@ -import QtQuick -import Quickshell -import qs.components import qs.config -import qs.modules.bar as Bar -import qs.modules.dashboard as Dashboard -import qs.modules.launcher as Launcher -import qs.modules.notifications as Notifications import qs.modules.osd as Osd +import qs.modules.notifications as Notifications import qs.modules.session as Session -import qs.modules.sidebar as Sidebar -import qs.modules.utilities as Utilities +import qs.modules.launcher as Launcher +import qs.modules.launcher.items.contextmenu as LauncherItems +import qs.modules.dashboard as Dashboard import qs.modules.bar.popouts as BarPopouts +import qs.modules.utilities as Utilities import qs.modules.utilities.toasts as Toasts +import qs.modules.sidebar as Sidebar +import Quickshell +import QtQuick Item { id: root required property ShellScreen screen - required property DrawerVisibilities visibilities - required property Bar.BarWrapper bar + required property PersistentProperties visibilities + required property Item bar + required property var windowRef readonly property alias osd: osd readonly property alias notifications: notifications @@ -30,6 +30,12 @@ Item { readonly property alias toasts: toasts readonly property alias sidebar: sidebar + readonly property bool contextMenuOpen: launcherContextMenu.visible + readonly property real contextMenuX: launcherContextMenu.visible ? launcherContextMenu.mapToItem(root, launcherContextMenu.gooBounds.x, launcherContextMenu.gooBounds.y).x : 0 + readonly property real contextMenuY: launcherContextMenu.visible ? launcherContextMenu.mapToItem(root, launcherContextMenu.gooBounds.x, launcherContextMenu.gooBounds.y).y : 0 + readonly property real contextMenuWidth: launcherContextMenu.visible ? launcherContextMenu.gooBounds.width : 0 + readonly property real contextMenuHeight: launcherContextMenu.visible ? launcherContextMenu.gooBounds.height : 0 + anchors.fill: parent anchors.margins: Config.border.thickness anchors.leftMargin: bar.implicitWidth @@ -50,9 +56,7 @@ Item { id: notifications visibilities: root.visibilities - sidebarPanel: sidebar - osdPanel: osd - sessionPanel: session + panels: root anchors.top: parent.top anchors.right: parent.right @@ -79,6 +83,64 @@ Item { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom + + onRequestShowContextMenu: (app, clickX, clickY) => { + const panelsPos = launcher.mapToItem(root, clickX, clickY); + launcherContextMenu.showAt(app, panelsPos.x, panelsPos.y); + } + + onRequestCloseContextMenu: { + if (launcherContextMenu.visible) { + launcherContextMenu.toggle(); + } + } + } + + LauncherItems.ContextMenu { + id: launcherContextMenu + + property real menuX: -10000 + property real menuY: -10000 + + x: menuX + y: menuY + z: 10000 + visible: false + visibilities: root.visibilities + windowRef: root.windowRef + + onVisibleChanged: { + if (!visible) { + menuX = -10000; + menuY = -10000; + } + } + + function showAt(app: var, x: real, y: real): void { + if (launcherContextMenu.visible) { + launcherContextMenu.toggle(); + return; + } + + launcherContextMenu.app = app; + + const menuWidth = 250; + const menuHeight = Math.max(launcherContextMenu.implicitHeight || 300, 100); + const padding = 16; + const spacing = 4; + + const posX = Math.max(padding, Math.min(x, root.width - menuWidth - padding)); + const tooWide = menuWidth + padding * 2 > root.width; + + launcherContextMenu.menuX = tooWide ? padding : posX; + launcherContextMenu.width = tooWide ? root.width - padding * 2 : menuWidth; + + const spaceBelow = root.height - y; + launcherContextMenu.showAbove = spaceBelow < menuHeight + spacing; + launcherContextMenu.menuY = launcherContextMenu.showAbove ? Math.max(0, y - menuHeight - spacing) : y + spacing; + + launcherContextMenu.toggle(); + } } Dashboard.Wrapper { diff --git a/modules/launcher/AppList.qml b/modules/launcher/AppList.qml index 15d6e44a6..176e25942 100644 --- a/modules/launcher/AppList.qml +++ b/modules/launcher/AppList.qml @@ -1,25 +1,138 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell +import "items" +import "services" import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config -import qs.modules.launcher.items -import qs.modules.launcher.services +import Quickshell +import QtQuick StyledListView { id: root required property StyledTextField search - required property DrawerVisibilities visibilities + required property PersistentProperties visibilities + required property string activeCategory model: ScriptModel { id: model - onValuesChanged: root.currentIndex = 0 + onValuesChanged: { + root.currentIndex = 0; + } + } + + // Force model refresh when favourites change + Connections { + target: Config.launcher + function onFavouriteAppsChanged() { + if (root.state === "apps") { + model.values = root.filterAppsByCategory(Apps.search(search.text)); + } + } + } + + property string previousCategory: "" + property var pendingModelUpdate: null + + onActiveCategoryChanged: { + if (previousCategory !== "" && root.state === "apps") { + if (categoryChangeAnimation.running) { + categoryChangeAnimation.stop(); + root.opacity = 1; + root.scale = 1; + } + pendingModelUpdate = root.filterAppsByCategory(Apps.search(search.text)); + categoryChangeAnimation.start(); + } + previousCategory = activeCategory; + } + + SequentialAnimation { + id: categoryChangeAnimation + + ParallelAnimation { + Anim { + target: root + property: "opacity" + to: 0 + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + Anim { + target: root + property: "scale" + to: 0.95 + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.emphasizedAccel + } + } + + ScriptAction { + script: { + // Update model while invisible + if (root.pendingModelUpdate !== null) { + model.values = root.pendingModelUpdate; + root.pendingModelUpdate = null; + } + } + } + + ParallelAnimation { + Anim { + target: root + property: "opacity" + to: 1 + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + Anim { + target: root + property: "scale" + to: 1 + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.emphasizedDecel + } + } + } + + function appHasCategory(appId: string, categoryName: string): bool { + if (!Config.launcher.categories) return false; + + for (let i = 0; i < Config.launcher.categories.length; i++) { + const category = Config.launcher.categories[i]; + if (!category || category.name.toLowerCase() !== categoryName.toLowerCase()) continue; + if (!category.apps) continue; + + if (typeof category.apps === 'object' && category.apps.length !== undefined) { + for (let j = 0; j < category.apps.length; j++) { + if (category.apps[j] === appId) { + return true; + } + } + } + } + return false; + } + + function filterAppsByCategory(apps) { + if (root.activeCategory === "all") { + return apps; + } else if (root.activeCategory === "favourites") { + return apps.filter(app => { + const appId = app.id || app.entry?.id; + return Config.launcher.favouriteApps && Config.launcher.favouriteApps.includes(appId); + }); + } else { + // Custom category + return apps.filter(app => { + const appId = app.id || app.entry?.id; + return appHasCategory(appId, root.activeCategory); + }); + } } spacing: Appearance.spacing.small @@ -72,8 +185,10 @@ StyledListView { name: "apps" PropertyChanges { - model.values: Apps.search(search.text) + model.values: root.filterAppsByCategory(Apps.search(search.text)) root.delegate: appItem + root.opacity: 1 + root.scale: 1 } }, State { @@ -167,30 +282,58 @@ StyledListView { add: Transition { enabled: !root.state - Anim { - properties: "opacity,scale" - from: 0 - to: 1 + ParallelAnimation { + Anim { + property: "opacity" + from: 0 + to: 1 + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + Anim { + property: "scale" + from: 0.8 + to: 1 + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.emphasizedDecel + } } } remove: Transition { enabled: !root.state - Anim { - properties: "opacity,scale" - from: 1 - to: 0 + ParallelAnimation { + Anim { + property: "opacity" + from: 1 + to: 0 + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + Anim { + property: "scale" + from: 1 + to: 0.8 + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.emphasizedAccel + } } } move: Transition { - Anim { - property: "y" - } - Anim { - properties: "opacity,scale" - to: 1 + ParallelAnimation { + Anim { + property: "y" + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.emphasized + } + Anim { + properties: "opacity,scale" + to: 1 + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.standardDecel + } } } @@ -215,11 +358,16 @@ StyledListView { } } + property var showContextMenuAt: null + property Item wrapperRoot: null + Component { id: appItem AppItem { visibilities: root.visibilities + showContextMenuAt: root.showContextMenuAt + wrapperRoot: root.wrapperRoot } } diff --git a/modules/launcher/Background.qml b/modules/launcher/Background.qml index 508c75d4b..709c7d035 100644 --- a/modules/launcher/Background.qml +++ b/modules/launcher/Background.qml @@ -1,8 +1,8 @@ -import QtQuick -import QtQuick.Shapes import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Shapes ShapePath { id: root diff --git a/modules/launcher/Content.qml b/modules/launcher/Content.qml index 88ff73e6d..53d77c642 100644 --- a/modules/launcher/Content.qml +++ b/modules/launcher/Content.qml @@ -1,46 +1,122 @@ pragma ComponentBehavior: Bound -import QtQuick +import "services" +import "../../components" as Components import qs.components import qs.components.controls +import qs.components.containers import qs.services import qs.config -import qs.modules.launcher.services +import Quickshell +import QtQuick +import QtQuick.Layouts Item { id: root - required property DrawerVisibilities visibilities + required property PersistentProperties visibilities required property var panels required property real maxHeight + readonly property alias searchField: search readonly property int padding: Appearance.padding.large readonly property int rounding: Appearance.rounding.large - implicitWidth: listWrapper.width + padding * 2 - implicitHeight: searchWrapper.height + listWrapper.height + padding * 2 + property var showContextMenuAt: null + property Item wrapperRoot: null + + property string activeCategory: "all" + property bool showNavbar: (Config.launcher.enableCategories ?? true) && !search.text.startsWith(Config.launcher.actionPrefix) + + readonly property var categoryList: [ + { + id: "all", + name: qsTr("All"), + icon: "apps" + }, + { + id: "favourites", + name: qsTr("Favourites"), + icon: "favorite" + } + ].concat(Config.launcher.categories.map(cat => ({ + id: cat.name.toLowerCase(), + name: cat.name, + icon: cat.icon + }))) + + function navigateCategory(direction: int): void { + const currentIndex = categoryList.findIndex(cat => cat.id === activeCategory); + if (currentIndex === -1) + return; + + const newIndex = currentIndex + direction; + if (newIndex >= 0 && newIndex < categoryList.length) { + activeCategory = categoryList[newIndex].id; + if (categoryNavbar) { + categoryNavbar.scrollToActiveTab(); + } + } + } + + implicitWidth: list.width + padding * 2 + implicitHeight: searchWrapper.implicitHeight + list.implicitHeight + categoryNavbar.height + (showNavbar ? padding * 2 : 0) + padding * 2 + Appearance.spacing.normal - Item { - id: listWrapper + Components.CategoryNavbar { + id: categoryNavbar + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.leftMargin: root.padding + anchors.rightMargin: root.padding + anchors.topMargin: root.padding + + categories: root.categoryList + activeCategory: root.activeCategory + showScrollButtons: true + + opacity: root.showNavbar ? 1 : 0 + height: root.showNavbar ? implicitHeight : 0 + + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + Behavior on height { + Anim { + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + onCategoryChanged: categoryId => { + root.activeCategory = categoryId; + } + } - implicitWidth: list.width - implicitHeight: list.height + root.padding + ContentList { + id: list anchors.horizontalCenter: parent.horizontalCenter + anchors.top: categoryNavbar.bottom anchors.bottom: searchWrapper.top + anchors.topMargin: root.showNavbar ? root.padding : 0 anchors.bottomMargin: root.padding - ContentList { - id: list - - content: root - visibilities: root.visibilities - panels: root.panels - maxHeight: root.maxHeight - searchWrapper.implicitHeight - root.padding * 3 - search: search - padding: root.padding - rounding: root.rounding - } + content: root + visibilities: root.visibilities + panels: root.panels + maxHeight: root.maxHeight - searchWrapper.implicitHeight - categoryNavbar.implicitHeight - (root.showNavbar ? root.padding * 2 : 0) - root.padding * 4 + search: search + padding: root.padding + rounding: root.rounding + activeCategory: root.activeCategory + showContextMenuAt: root.showContextMenuAt + wrapperRoot: root.wrapperRoot } StyledRect { @@ -80,6 +156,10 @@ Item { placeholderText: qsTr("Type \"%1\" for commands").arg(Config.launcher.actionPrefix) + onTextChanged: { + root.showNavbar = !text.startsWith(Config.launcher.actionPrefix); + } + onAccepted: { const currentItem = list.currentList?.currentItem; if (currentItem) { @@ -103,6 +183,20 @@ Item { Keys.onUpPressed: list.currentList?.decrementCurrentIndex() Keys.onDownPressed: list.currentList?.incrementCurrentIndex() + Keys.onLeftPressed: event => { + if (event.modifiers === Qt.NoModifier) { + root.navigateCategory(-1); + event.accepted = true; + } + } + + Keys.onRightPressed: event => { + if (event.modifiers === Qt.NoModifier) { + root.navigateCategory(1); + event.accepted = true; + } + } + Keys.onEscapePressed: root.visibilities.launcher = false Keys.onPressed: event => { @@ -129,6 +223,8 @@ Item { Component.onCompleted: forceActiveFocus() Connections { + target: root.visibilities + function onLauncherChanged(): void { if (!root.visibilities.launcher) search.text = ""; @@ -138,8 +234,6 @@ Item { if (!root.visibilities.session) search.forceActiveFocus(); } - - target: root.visibilities } } diff --git a/modules/launcher/ContentList.qml b/modules/launcher/ContentList.qml index 775b193fa..5d494cca6 100644 --- a/modules/launcher/ContentList.qml +++ b/modules/launcher/ContentList.qml @@ -1,27 +1,30 @@ pragma ComponentBehavior: Bound -import QtQuick import qs.components import qs.components.controls import qs.services import qs.config import qs.utils +import Quickshell +import QtQuick Item { id: root required property var content - required property DrawerVisibilities visibilities + required property PersistentProperties visibilities required property var panels required property real maxHeight required property StyledTextField search required property int padding required property int rounding + required property string activeCategory readonly property bool showWallpapers: search.text.startsWith(`${Config.launcher.actionPrefix}wallpaper `) - readonly property var currentList: showWallpapers ? wallpaperList.item : appList.item // Can be either ListView or PathView, so can't type properly + readonly property Item currentList: showWallpapers ? wallpaperList.item : appList.item anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top anchors.bottom: parent.bottom clip: true @@ -73,6 +76,9 @@ Item { } } + property var showContextMenuAt: null + property Item wrapperRoot: null + Loader { id: appList @@ -83,13 +89,15 @@ Item { sourceComponent: AppList { search: root.search visibilities: root.visibilities + activeCategory: root.activeCategory + showContextMenuAt: root.showContextMenuAt + wrapperRoot: root.wrapperRoot } } Loader { id: wallpaperList - asynchronous: true active: false anchors.top: parent.top diff --git a/modules/launcher/WallpaperList.qml b/modules/launcher/WallpaperList.qml index ad03e9dec..4aba4365b 100644 --- a/modules/launcher/WallpaperList.qml +++ b/modules/launcher/WallpaperList.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound import "items" -import QtQuick -import Quickshell import qs.components.controls import qs.services import qs.config +import Quickshell +import QtQuick PathView { id: root @@ -18,7 +18,7 @@ PathView { readonly property int itemWidth: Config.launcher.sizes.wallpaperWidth * 0.8 + Appearance.padding.larger * 2 readonly property int numItems: { - const screen = (QsWindow.window as QsWindow)?.screen; + const screen = QsWindow.window?.screen; if (!screen) return 0; @@ -58,7 +58,7 @@ PathView { onCurrentItemChanged: { if (currentItem) - Wallpapers.preview((currentItem as WallpaperItem).modelData.path); + Wallpapers.preview(currentItem.modelData.path); } implicitWidth: Math.min(numItems, count) * itemWidth diff --git a/modules/launcher/Wrapper.qml b/modules/launcher/Wrapper.qml index cc5e86c6a..322be7680 100644 --- a/modules/launcher/Wrapper.qml +++ b/modules/launcher/Wrapper.qml @@ -1,19 +1,21 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell +import "items" import qs.components import qs.config +import Quickshell +import QtQuick Item { id: root required property ShellScreen screen - required property DrawerVisibilities visibilities + required property PersistentProperties visibilities required property var panels readonly property bool shouldBeActive: visibilities.launcher && Config.launcher.enabled property int contentHeight + property bool animationComplete: false readonly property real maxHeight: { let max = screen.height - Config.border.thickness * 2 - Appearance.spacing.large; @@ -32,9 +34,12 @@ Item { if (shouldBeActive) { timer.stop(); hideAnim.stop(); + root.animationComplete = false; showAnim.start(); } else { + root.requestCloseContextMenu(); showAnim.stop(); + root.animationComplete = false; hideAnim.start(); } } @@ -50,7 +55,13 @@ Item { easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } ScriptAction { - script: root.implicitHeight = Qt.binding(() => content.implicitHeight) + script: { + root.implicitHeight = Qt.binding(() => content.implicitHeight); + // Wait one more frame after animation to ensure layout is stable + Qt.callLater(() => { + root.animationComplete = true; + }); + } } } @@ -69,6 +80,8 @@ Item { } Connections { + target: Config.launcher + function onEnabledChanged(): void { timer.start(); } @@ -76,17 +89,15 @@ Item { function onMaxShownChanged(): void { timer.start(); } - - target: Config.launcher } Connections { + target: DesktopEntries.applications + function onValuesChanged(): void { if (DesktopEntries.applications.values.length < Config.launcher.maxShown) timer.start(); } - - target: DesktopEntries.applications } Timer { @@ -123,8 +134,36 @@ Item { visibilities: root.visibilities panels: root.panels maxHeight: root.maxHeight + showContextMenuAt: root.showContextMenu + wrapperRoot: root + + Component.onCompleted: { + root.contentHeight = implicitHeight; + Qt.callLater(() => { + root.animationComplete = true; + }); + } + } + } + + signal requestShowContextMenu(app: DesktopEntry, clickX: real, clickY: real) + signal requestCloseContextMenu - Component.onCompleted: root.contentHeight = implicitHeight + function restoreFocus(): void { + if (content.item && content.item.searchField) { + content.item.searchField.forceActiveFocus(); } } + + function showContextMenu(app: DesktopEntry, clickX: real, clickY: real): void { + if (!app || !root.animationComplete) { + return; + } + + if (clickX < 0 || clickX > root.width || clickY < 0 || clickY > root.height) { + return; + } + + root.requestShowContextMenu(app, clickX, clickY); + } } diff --git a/modules/launcher/items/ActionItem.qml b/modules/launcher/items/ActionItem.qml index a3ef00dda..e15802907 100644 --- a/modules/launcher/items/ActionItem.qml +++ b/modules/launcher/items/ActionItem.qml @@ -1,7 +1,8 @@ -import QtQuick +import "../services" import qs.components import qs.services import qs.config +import QtQuick Item { id: root @@ -15,11 +16,11 @@ Item { anchors.right: parent?.right StateLayer { + radius: Appearance.rounding.normal + function onClicked(): void { root.modelData?.onClicked(root.list); } - - radius: Appearance.rounding.normal } Item { diff --git a/modules/launcher/items/AppItem.qml b/modules/launcher/items/AppItem.qml index 0b64c3da0..0f1d7ca6c 100644 --- a/modules/launcher/items/AppItem.qml +++ b/modules/launcher/items/AppItem.qml @@ -1,30 +1,51 @@ -import QtQuick -import Quickshell -import Quickshell.Widgets +pragma NativeMethodBehavior: AcceptThisObject + +import "../services" import qs.components import qs.services import qs.config import qs.utils -import qs.modules.launcher.services +import Quickshell +import Quickshell.Widgets +import QtQuick Item { id: root required property DesktopEntry modelData - required property DrawerVisibilities visibilities - + required property PersistentProperties visibilities + property var showContextMenuAt: null + property Item wrapperRoot: null + implicitHeight: Config.launcher.sizes.itemHeight anchors.left: parent?.left anchors.right: parent?.right StateLayer { - function onClicked(): void { - Apps.launch(root.modelData); - root.visibilities.launcher = false; - } - + id: stateLayer radius: Appearance.rounding.normal + acceptedButtons: Qt.LeftButton | Qt.RightButton + + function onClicked(event): void { + if (event.button === Qt.LeftButton) { + Apps.launch(root.modelData); + root.visibilities.launcher = false; + } else if (event.button === Qt.RightButton) { + if (!root.showContextMenuAt || !root.wrapperRoot || !root.modelData) { + return; + } + + try { + const pos = stateLayer.mapToItem(root.wrapperRoot, event.x, event.y); + if (pos && typeof pos.x === 'number' && typeof pos.y === 'number') { + root.showContextMenuAt(root.modelData, pos.x, pos.y); + } + } catch (error) { + console.error("Failed to show context menu:", error); + } + } + } } Item { @@ -36,7 +57,6 @@ Item { IconImage { id: icon - asynchronous: true source: Quickshell.iconPath(root.modelData?.icon, "image-missing") implicitSize: parent.height * 0.8 @@ -75,10 +95,9 @@ Item { Loader { id: favouriteIcon - asynchronous: true anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right - active: root.modelData && Strings.testRegexList(Config.launcher.favouriteApps, root.modelData.id) + active: modelData && Strings.testRegexList(Config.launcher.favouriteApps, modelData.id) sourceComponent: MaterialIcon { text: "favorite" diff --git a/modules/launcher/items/CalcItem.qml b/modules/launcher/items/CalcItem.qml index 369b35ef6..65489d9bc 100644 --- a/modules/launcher/items/CalcItem.qml +++ b/modules/launcher/items/CalcItem.qml @@ -1,10 +1,10 @@ -import QtQuick -import QtQuick.Layouts -import Quickshell -import Caelestia import qs.components import qs.services import qs.config +import Caelestia +import Quickshell +import QtQuick +import QtQuick.Layouts Item { id: root @@ -13,26 +13,21 @@ Item { readonly property string math: list.search.text.slice(`${Config.launcher.actionPrefix}calc `.length) function onClicked(): void { - Quickshell.execDetached(["wl-copy", Qalculator.rawResult]); + Quickshell.execDetached(["wl-copy", Qalculator.eval(math, false)]); root.list.visibilities.launcher = false; } - onMathChanged: { - if (math.length > 0) - Qalculator.evalAsync(math); - } - implicitHeight: Config.launcher.sizes.itemHeight anchors.left: parent?.left anchors.right: parent?.right StateLayer { + radius: Appearance.rounding.normal + function onClicked(): void { root.onClicked(); } - - radius: Appearance.rounding.normal } RowLayout { @@ -60,7 +55,7 @@ Item { return Colours.palette.m3onSurface; } - text: root.math.length > 0 ? (Qalculator.result || qsTr("Calculating...")) : qsTr("Type an expression to calculate") + text: root.math.length > 0 ? Qalculator.eval(root.math) : qsTr("Type an expression to calculate") elide: Text.ElideLeft Layout.fillWidth: true @@ -80,12 +75,12 @@ Item { StateLayer { id: stateLayer + color: Colours.palette.m3onTertiary + function onClicked(): void { Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.terminal, "fish", "-C", `exec qalc -i '${root.math}'`]); root.list.visibilities.launcher = false; } - - color: Colours.palette.m3onTertiary } StyledText { diff --git a/modules/launcher/items/SchemeItem.qml b/modules/launcher/items/SchemeItem.qml index 6cc47aa48..3ff184681 100644 --- a/modules/launcher/items/SchemeItem.qml +++ b/modules/launcher/items/SchemeItem.qml @@ -1,8 +1,8 @@ -import QtQuick +import "../services" import qs.components import qs.services import qs.config -import qs.modules.launcher.services +import QtQuick Item { id: root @@ -16,11 +16,11 @@ Item { anchors.right: parent?.right StateLayer { + radius: Appearance.rounding.normal + function onClicked(): void { root.modelData?.onClicked(root.list); } - - radius: Appearance.rounding.normal } Item { @@ -89,7 +89,6 @@ Item { Loader { id: current - asynchronous: true anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter diff --git a/modules/launcher/items/VariantItem.qml b/modules/launcher/items/VariantItem.qml index b63539b73..5c34fa89f 100644 --- a/modules/launcher/items/VariantItem.qml +++ b/modules/launcher/items/VariantItem.qml @@ -1,8 +1,8 @@ -import QtQuick +import "../services" import qs.components import qs.services import qs.config -import qs.modules.launcher.services +import QtQuick Item { id: root @@ -16,11 +16,11 @@ Item { anchors.right: parent?.right StateLayer { + radius: Appearance.rounding.normal + function onClicked(): void { root.modelData?.onClicked(root.list); } - - radius: Appearance.rounding.normal } Item { @@ -65,7 +65,6 @@ Item { Loader { id: current - asynchronous: true anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter diff --git a/modules/launcher/items/WallpaperItem.qml b/modules/launcher/items/WallpaperItem.qml index beb93426a..9fdac3f38 100644 --- a/modules/launcher/items/WallpaperItem.qml +++ b/modules/launcher/items/WallpaperItem.qml @@ -1,20 +1,21 @@ -import QtQuick -import Caelestia.Models import qs.components import qs.components.effects import qs.components.images import qs.services import qs.config +import Caelestia.Models +import Quickshell +import QtQuick Item { id: root required property FileSystemEntry modelData - required property DrawerVisibilities visibilities + required property PersistentProperties visibilities scale: 0.5 opacity: 0 - z: PathView.z ?? 0 // qmllint disable missing-property + z: PathView.z ?? 0 Component.onCompleted: { scale = Qt.binding(() => PathView.isCurrentItem ? 1 : PathView.onPath ? 0.8 : 0); @@ -25,12 +26,12 @@ Item { implicitHeight: image.height + label.height + Appearance.spacing.small / 2 + Appearance.padding.large + Appearance.padding.normal StateLayer { + radius: Appearance.rounding.normal + function onClicked(): void { Wallpapers.setWallpaper(root.modelData.path); root.visibilities.launcher = false; } - - radius: Appearance.rounding.normal } Elevation { diff --git a/modules/launcher/items/contextmenu/ActionRegistry.qml b/modules/launcher/items/contextmenu/ActionRegistry.qml new file mode 100644 index 000000000..c1405e60a --- /dev/null +++ b/modules/launcher/items/contextmenu/ActionRegistry.qml @@ -0,0 +1,222 @@ +pragma Singleton +import qs.utils +import qs.config +import Quickshell +import QtQuick + +QtObject { + id: registry + + readonly property var presets: ({ + "launch": { + text: qsTr("Launch"), + icon: "play_arrow", + hasSubmenu: true, + bold: true, + execute: function (app, context) { + if (app) { + context.launchApp(); + if (context.visibilities) + context.visibilities.launcher = false; + context.toggle(); + } + } + }, + "workspaces": { + text: qsTr("Open in Workspace"), + icon: "workspaces", + hasSubmenu: true + }, + "categories": { + text: qsTr("Assign Categories"), + icon: "category", + hasSubmenu: true + }, + "favorites": { + text: qsTr("Add to Favourites"), + icon: "favorite", + dynamicText: true, + execute: function (app, context) { + if (!app || !app.id) + return context.toggle(); + const favourites = Config.launcher.favouriteApps.slice(); + const index = favourites.indexOf(app.id); + if (index > -1) + favourites.splice(index, 1); + else + favourites.push(app.id); + Config.launcher.favouriteApps = favourites; + Config.save(); + context.toggle(); + } + }, + "hide": { + text: qsTr("Hide from Launcher"), + icon: "visibility_off", + execute: function (app, context) { + if (!app || !app.id) + return context.toggle(); + const hidden = Config.launcher.hiddenApps.slice(); + hidden.push(app.id); + Config.launcher.hiddenApps = hidden; + Config.save(); + if (context.visibilities) + context.visibilities.launcher = false; + context.toggle(); + } + }, + "desktop-file": { + text: qsTr("Open .desktop File"), + icon: "description", + execute: function (app, context) { + if (!app || !app.id) + return; + Quickshell.execDetached({ + command: ["sh", "-c", `file=$(find ~/.local/share/applications /usr/share/applications /usr/local/share/applications /var/lib/flatpak/exports/share/applications ~/.local/share/flatpak/exports/share/applications -name '${app.id}.desktop' 2>/dev/null | head -n1); [ -n "$file" ] && xdg-open "$file"`] + }); + context.toggle(); + } + } + }) + + readonly property var actions: ({ + "terminal": { + text: qsTr("Run in Terminal"), + icon: "terminal", + execute: function (app, context) { + if (!app || !app.execString) + return; + Quickshell.execDetached({ + command: [...Config.general.apps.terminal, "-e", app.execString] + }); + if (context.visibilities) + context.visibilities.launcher = false; + context.toggle(); + } + }, + "kill": { + text: qsTr("Force Quit"), + icon: "close", + execute: function (app, context) { + if (!app || !app.execString) + return; + const execName = app.execString.split(" ")[0].split("/").pop(); + Quickshell.execDetached({ + command: ["pkill", "-9", "-f", execName] + }); + context.toggle(); + } + }, + "open-path": { + text: qsTr("Open App Location"), + icon: "folder_open", + execute: function (app, context) { + if (!app || !app.execString) + return; + const execPath = app.execString.split(" ")[0]; + Quickshell.execDetached({ + command: ["sh", "-c", `realpath=$(which "${execPath}" 2>/dev/null || realpath "${execPath}" 2>/dev/null); [ -n "$realpath" ] && ${Config.general.apps.explorer.join(" ")} "$(dirname "$realpath")"`] + }); + context.toggle(); + } + }, + "copy-exec": { + text: qsTr("Copy Command"), + icon: "content_copy", + execute: function (app, context) { + if (!app || !app.execString) + return; + Quickshell.execDetached({ + command: ["sh", "-c", `echo -n "${app.execString}" | wl-copy`] + }); + context.toggle(); + } + } + }) + + readonly property string defaultSubmenuIcon: "folder" + + function isPreset(id) { + return presets.hasOwnProperty(id); + } + + function isAction(id) { + return actions.hasOwnProperty(id); + } + + function isKnown(id) { + return id === "separator" || isPreset(id) || isAction(id); + } + + function getDefaults(id) { + if (id === "separator") { + return { + type: "separator" + }; + } + if (presets.hasOwnProperty(id)) { + return Object.assign({ + type: "preset" + }, presets[id]); + } + if (actions.hasOwnProperty(id)) { + return Object.assign({ + type: "action" + }, actions[id]); + } + return { + type: "custom", + text: id, + icon: defaultSubmenuIcon + }; + } + + function getText(id, config, app) { + if (id === "favorites" && app) { + const isFavorite = app.id && Strings.testRegexList(Config.launcher.favouriteApps, app.id); + return isFavorite ? qsTr("Remove from Favourites") : qsTr("Add to Favourites"); + } + + if (config && config.text) { + return config.text; + } + + const defaults = getDefaults(id); + return defaults.text || id; + } + + function getIcon(id, config) { + if (config && config.icon) { + return config.icon; + } + + const defaults = getDefaults(id); + return defaults.icon || defaultSubmenuIcon; + } + + function execute(actionId, config, app, context) { + const defaults = getDefaults(actionId); + + if (defaults.execute) { + defaults.execute(app, context); + } else if (config && config.command) { + executeCustomCommand(config.command, app, context); + } + } + + function executeCustomCommand(command, app, context) { + if (!Array.isArray(command)) + return; + + const processedCommand = command.map(arg => { + if (typeof arg !== "string") + return arg; + return arg.replace(/\$appId/g, app?.id || "").replace(/\$appName/g, app?.name || "").replace(/\$execString/g, app?.execString || "").replace(/\$desktopPath/g, app?.desktopPath || "").replace(/\$iconName/g, app?.iconName || ""); + }); + + Quickshell.execDetached({ + command: processedCommand + }); + context.toggle(); + } +} diff --git a/modules/launcher/items/contextmenu/ContextMenu.qml b/modules/launcher/items/contextmenu/ContextMenu.qml new file mode 100644 index 000000000..abbfb65df --- /dev/null +++ b/modules/launcher/items/contextmenu/ContextMenu.qml @@ -0,0 +1,640 @@ +import "../../services" +import "../../../../services" as Services +import "." as ContextMenus +import qs.components +import qs.services +import qs.config +import Quickshell +import Quickshell.Hyprland +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + property DesktopEntry app: null + property PersistentProperties visibilities + property var windowRef: null + property bool showAbove: false + readonly property alias gooBounds: gooBounds + property int activeSubmenuIndex: -1 + property int targetSubmenuIndex: -1 + property real submenuProgress: 0 + property int hoveredSubmenuIndex: -1 + property bool transitionContentVisible: false + property real contentOpacity: { + if (targetSubmenuIndex >= 0) { + return transitionContentVisible ? 1 : 0; + } + if (activeSubmenuIndex >= 0) { + return submenuProgress; + } + return 0; + } + property real submenuItemY: 0 + property int displayedSubmenuIndex: -1 + property real targetWidth: 0 + property real targetHeight: 0 + property real previousTargetWidth: 0 + property real previousTargetHeight: 0 + property real previousTopY: 0 + property real gooOverlapPx: 28 + readonly property real gooMarginPx: 30 + readonly property real bottomPadding: 16 + property string currentPage: "main" + + onVisibleChanged: { + if (!visible) + currentPage = "main"; + else if (app) + buildSubmenuMap(); + } + + function navigateToPage(page) { + if (currentPage === page) + return; + menuColumn.opacity = 0; + pageTransitionTimer.page = page; + pageTransitionTimer.restart(); + } + + Timer { + id: pageTransitionTimer + interval: 75 + property string page: "main" + + onTriggered: { + currentPage = page; + Qt.callLater(() => { + menuColumn.opacity = 1; + }); + } + } + + readonly property var menuConfigMain: (Config.launcher.contextMenuMain && Config.launcher.contextMenuMain.length > 0) ? Config.launcher.contextMenuMain : [ + { + "launch": { + "text": "Launch", + "icon": "play_arrow", + "bold": true + } + }, + "separator", "favorites", "categories", "hide", "workspaces"] + + readonly property var menuConfigAdvanced: (Config.launcher.contextMenuAdvanced && Config.launcher.contextMenuAdvanced.length > 0) ? Config.launcher.contextMenuAdvanced : ["terminal", + { + "desktop-file": { + "text": "edit .desktop File", + "icon": "code" + } + }, + "open-path", "separator", + { + "custom-submenu": { + "text": "Advanced Options", + "icon": "settings" + } + }, + { + "kill": { + "parent": "custom-submenu" + } + }, + { + "separator": { + "parent": "custom-submenu" + } + }, + { + "copy-exec": { + "parent": "custom-submenu" + } + } + ] + + readonly property var menuConfig: ({ + "main": menuConfigMain, + "advanced": menuConfigAdvanced + }) + readonly property bool hasAdvancedItems: menuConfig.advanced && menuConfig.advanced.length > 0 + + property var menuContext: ({ + visibilities: root.visibilities, + toggle: root.toggle, + launchApp: root.launchApp, + navigateToPage: root.navigateToPage + }) + + ContextMenus.MenuItemFactory { + id: menuFactory + app: root.app + visibilities: root.visibilities + launchApp: root.launchApp + toggle: root.toggle + menuContext: root.menuContext + } + + readonly property var processedMainItems: menuFactory.processMenuItems(menuConfig.main || []) + readonly property var processedAdvancedItems: menuFactory.processMenuItems(menuConfig.advanced || []) + readonly property var currentPageItems: currentPage === "main" ? processedMainItems : processedAdvancedItems + + property var submenuMap: ({}) + property int nextSubmenuIndex: 0 + property int submenuMapVersion: 0 + + function updateSubmenuDimensions() { + targetWidth = submenuColumn.implicitWidth + Appearance.padding.smaller * 2; + targetHeight = submenuColumn.implicitHeight + Appearance.padding.smaller * 2; + } + + function buildSubmenuMap() { + submenuMap = {}; + nextSubmenuIndex = 0; + + const allItems = [...processedMainItems, ...processedAdvancedItems]; + allItems.forEach(item => { + const isSubmenu = menuFactory.shouldShowAsSubmenu(item.id, item.config, processedMainItems) || menuFactory.shouldShowAsSubmenu(item.id, item.config, processedAdvancedItems); + if (isSubmenu) { + submenuMap[item.id] = nextSubmenuIndex++; + } + }); + submenuMapVersion++; + } + + function getSubmenuIndex(itemId) { + submenuMapVersion; + return submenuMap[itemId] ?? -1; + } + + Component.onCompleted: buildSubmenuMap() + onProcessedMainItemsChanged: buildSubmenuMap() + onProcessedAdvancedItemsChanged: buildSubmenuMap() + onAppChanged: buildSubmenuMap() + + function getSubmenuItemsForIndex(index) { + const allItems = [...processedMainItems, ...processedAdvancedItems]; + for (const [itemId, submenuIdx] of Object.entries(submenuMap)) { + if (submenuIdx === index) { + const item = allItems.find(i => i.id === itemId); + return item ? item.children : []; + } + } + return []; + } + + function getSubmenuParentId(index) { + for (const [itemId, submenuIdx] of Object.entries(submenuMap)) { + if (submenuIdx === index) { + return itemId; + } + } + return ""; + } + + Timer { + id: contentSwitchTimer + interval: Appearance.anim.durations.small + onTriggered: { + if (targetSubmenuIndex >= 0) { + activeSubmenuIndex = displayedSubmenuIndex = targetSubmenuIndex; + targetSubmenuIndex = -1; + } + transitionContentVisible = true; + if (activeSubmenuIndex >= 0) + Qt.callLater(updateSubmenuDimensions); + } + } + + onHoveredSubmenuIndexChanged: { + if (hoveredSubmenuIndex < 0) + return; + if (activeSubmenuIndex < 0) { + activeSubmenuIndex = displayedSubmenuIndex = hoveredSubmenuIndex; + targetSubmenuIndex = -1; + Qt.callLater(updateSubmenuDimensions); + } else if (activeSubmenuIndex !== hoveredSubmenuIndex) { + previousTargetWidth = targetWidth; + previousTargetHeight = targetHeight; + previousTopY = submenuContainer.interpolatedTopY; + targetSubmenuIndex = hoveredSubmenuIndex; + transitionContentVisible = false; + contentSwitchTimer.restart(); + } + } + + onActiveSubmenuIndexChanged: { + if (activeSubmenuIndex < 0) { + targetSubmenuIndex = displayedSubmenuIndex = -1; + targetWidth = targetHeight = previousTargetWidth = previousTargetHeight = previousTopY = 0; + } else if (displayedSubmenuIndex < 0) { + displayedSubmenuIndex = activeSubmenuIndex; + Qt.callLater(updateSubmenuDimensions); + } + } + + Connections { + target: submenuColumn + function onImplicitWidthChanged() { + if (displayedSubmenuIndex >= 0) + updateSubmenuDimensions(); + } + function onImplicitHeightChanged() { + if (displayedSubmenuIndex >= 0) + updateSubmenuDimensions(); + } + } + + visible: false + property bool menuOpen: false + + function launchApp(workspace) { + if (!root.app) + return; + if (workspace) + Services.Hypr.dispatch(`workspace ${workspace}`); + Apps.launch(root.app); + root.visibilities.launcher = false; + toggle(); + } + + function toggle() { + if (!root.app) + return; + + menuOpen = !root.visible; + if (menuOpen) { + activeSubmenuIndex = -1; + submenuProgress = 0; + root.visible = true; + } + } + + HyprlandFocusGrab { + active: root.visible && root.windowRef !== null + windows: root.windowRef ? [root.windowRef] : [] + + onCleared: { + if (root.visible) { + toggle(); + } + } + } + + Behavior on submenuProgress { + NumberAnimation { + duration: Appearance.anim.durations.normal / 1.2 + easing.type: Easing.InOutCubic + } + } + + Behavior on contentOpacity { + enabled: targetSubmenuIndex >= 0 + Anim { + duration: Appearance.anim.durations.normal / 1.2 + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + Timer { + id: submenuCloseTimer + interval: 150 + onTriggered: { + if (hoveredSubmenuIndex < 0 && targetSubmenuIndex < 0 && !submenuHover.hovered) { + submenuProgress = 0; + Qt.callLater(() => { + if (submenuProgress === 0) + activeSubmenuIndex = -1; + }); + } + } + } + + Item { + id: menuWrapper + width: menuContainer.width + height: menuContainer.height + + opacity: menuOpen ? 1 : 0 + scale: menuOpen ? 1 : 0.85 + transformOrigin: Item.TopLeft + + Behavior on scale { + NumberAnimation { + duration: Appearance.anim.durations.fast || 150 + easing.type: Easing.OutCubic + } + } + + Behavior on opacity { + Anim { + duration: Appearance.anim.durations.fast || 150 + easing.bezierCurve: Appearance.anim.curves.standard + } + } + + onOpacityChanged: { + if (opacity === 0 && !menuOpen && root.visible) { + root.visible = false; + root.app = null; + activeSubmenuIndex = -1; + submenuProgress = 0; + } + } + + Item { + id: gooBounds + + readonly property real menuLeft: menuContainer.x + readonly property real menuTop: menuContainer.y + readonly property real menuRight: menuContainer.x + menuContainer.width + readonly property real menuBottom: menuContainer.y + menuContainer.height + + readonly property bool hasSub: submenuContainer.visible + readonly property real subLeft: submenuContainer.x - root.gooOverlapPx + readonly property real subTop: submenuContainer.y + readonly property real subRight: submenuContainer.x + submenuContainer.width + readonly property real subBottom: submenuContainer.y + submenuContainer.height + + readonly property real gooLeft: (hasSub ? Math.min(menuLeft, subLeft) : menuLeft) - root.gooMarginPx + readonly property real gooTop: (hasSub ? Math.min(menuTop, subTop) : menuTop) - root.gooMarginPx + readonly property real gooRight: (hasSub ? Math.max(menuRight, subRight) : menuRight) + root.gooMarginPx + readonly property real gooBottom: (hasSub ? Math.max(menuBottom, subBottom) : menuBottom) + root.gooMarginPx + + x: gooLeft + y: gooTop + width: Math.max(1, gooRight - gooLeft) + height: Math.max(1, gooBottom - gooTop) + } + + ShaderEffect { + id: gooEffect + x: gooBounds.x + y: gooBounds.y + width: gooBounds.width + height: gooBounds.height + z: -2 + visible: root.visible + + property vector2d sizePx: Qt.vector2d(width, height) + + property vector4d menuRectPx: Qt.vector4d(menuContainer.x - gooBounds.x, menuContainer.y - gooBounds.y, menuContainer.width, menuContainer.height) + + property vector4d subRectPx: submenuContainer.visible ? Qt.vector4d((submenuContainer.x - gooBounds.x) - 24, submenuContainer.y - gooBounds.y, submenuContainer.width + 24, submenuContainer.height) : Qt.vector4d(0, 0, 0, 0) + + property real radiusPx: Appearance.rounding.normal * 0.75 + + readonly property real topEdgeDiff: submenuContainer.visible ? Math.abs(menuContainer.y - submenuContainer.y) : 999 + readonly property real bottomEdgeDiff: submenuContainer.visible ? Math.abs((menuContainer.y + menuContainer.height) - (submenuContainer.y + submenuContainer.height)) : 999 + readonly property bool isTopAligned: submenuContainer.visible && topEdgeDiff < 3 + readonly property bool isBottomAligned: submenuContainer.visible && bottomEdgeDiff < 3 + + property real smoothPxTop: isTopAligned ? 0 : 12 + property real smoothPxBottom: isBottomAligned ? 0 : 12 + + property color fillColor: Colours.palette.m3surfaceContainer + property color shadowColor: Qt.rgba(0, 0, 0, 0.20) + property vector2d shadowOffsetPx: Qt.vector2d(0, 0) + property real shadowSoftPx: 6 + + vertexShader: Qt.resolvedUrl("shaders/goo_sdf.vert.qsb") + fragmentShader: Qt.resolvedUrl("shaders/goo_sdf.frag.qsb") + } + + Item { + id: menuContainer + width: menuColumn.implicitWidth + Appearance.padding.smaller * 2 + height: menuColumn.implicitHeight + Appearance.padding.smaller * 2 + + Behavior on width { + NumberAnimation { + duration: Appearance.anim.durations.normal / 2 + easing.type: Easing.InOutQuad + } + } + + Behavior on height { + NumberAnimation { + duration: Appearance.anim.durations.normal / 2 + easing.type: Easing.InOutQuad + } + } + + ColumnLayout { + id: menuColumn + anchors.fill: parent + anchors.margins: Appearance.padding.smaller + spacing: Appearance.spacing.smaller + clip: true + opacity: 1 + + Behavior on opacity { + NumberAnimation { + duration: (Appearance.anim.durations.fast || 150) / 2 + easing.type: Easing.InOutQuad + } + } + + // Back button for advanced page + MenuItem { + visible: currentPage === "advanced" + text: qsTr("Back") + icon: "arrow_back" + onTriggered: navigateToPage("main") + } + + MenuItem { + isSeparator: true + visible: currentPage === "advanced" + } + + Repeater { + model: currentPageItems + + MenuItem { + required property var modelData + readonly property var itemData: modelData.id === "separator" ? null : menuFactory.createMenuItem(modelData, false, -1) + readonly property int dynamicSubmenuIndex: modelData.id === "separator" ? -1 : getSubmenuIndex(modelData.id) + + isSeparator: modelData.id === "separator" + text: itemData?.text || "" + icon: itemData?.icon || "" + bold: itemData?.bold || false + hasSubMenu: dynamicSubmenuIndex >= 0 + submenuIndex: dynamicSubmenuIndex + isSubmenuItem: false + + onTriggered: { + if (itemData?.onTriggered) { + itemData.onTriggered(); + } + } + } + } + + // More Options button for main page + MenuItem { + isSeparator: true + visible: currentPage === "main" && hasAdvancedItems + } + + MenuItem { + visible: currentPage === "main" && hasAdvancedItems + text: qsTr("More Options") + icon: "more_horiz" + onTriggered: navigateToPage("advanced") + } + } + } + + Item { + id: submenuContainer + z: -1 + + readonly property bool isTransitioning: targetSubmenuIndex >= 0 + readonly property bool shouldShowSubmenu: activeSubmenuIndex >= 0 && submenuProgress > 0 + + property real interpolatedWidth: targetWidth + property real interpolatedHeight: targetHeight + property real interpolatedTopY: isTransitioning ? previousTopY : submenuItemY - targetHeight / 2 + + property real centerOffset: (interpolatedHeight - interpolatedHeight * submenuProgress) / 2 + property real clampedY: { + const unclampedY = interpolatedTopY + centerOffset; + if (activeSubmenuIndex < 0 || height === 0) + return unclampedY; + + // Clamp to menu top edge when close + const topDiff = Math.abs(unclampedY - menuContainer.y); + if (topDiff < 3) { + return menuContainer.y; + } + + // Clamp to menu bottom edge when close + const menuBottom = menuContainer.y + menuContainer.height; + const subBottom = unclampedY + height; + const bottomDiff = Math.abs(subBottom - menuBottom); + if (bottomDiff < 3) { + return menuBottom - height; + } + + // Clamp to screen bottom + const maxY = (root.parent ? root.parent.height - root.y : 1000) - height - bottomPadding; + return Math.min(unclampedY, maxY); + } + + readonly property real slideOffsetX: -10 * (1 - submenuProgress) + + Behavior on interpolatedWidth { + enabled: submenuProgress >= 1 + Anim { + duration: Appearance.anim.durations.normal * 1.2 + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + Behavior on interpolatedHeight { + enabled: submenuProgress >= 1 + Anim { + duration: Appearance.anim.durations.normal * 1.8 + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + Behavior on interpolatedTopY { + enabled: submenuProgress >= 1 + Anim { + duration: Appearance.anim.durations.normal * 1.5 + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + + width: shouldShowSubmenu ? interpolatedWidth * submenuProgress : 0 + height: shouldShowSubmenu ? interpolatedHeight * submenuProgress : 0 + + x: menuContainer.width + slideOffsetX + y: clampedY + visible: width > 0 || height > 0 + clip: true + + HoverHandler { + id: submenuHover + } + + ColumnLayout { + id: submenuColumn + anchors.fill: parent + anchors.margins: Appearance.padding.smaller + spacing: Appearance.spacing.smaller + opacity: contentOpacity + + Loader { + id: launchSubmenuLoader + active: displayedSubmenuIndex >= 0 && getSubmenuParentId(displayedSubmenuIndex) === "launch" + visible: active + Layout.fillWidth: true + Layout.preferredHeight: active ? implicitHeight : 0 + + property var factoryRef: menuFactory + property var childrenRef: getSubmenuItemsForIndex(displayedSubmenuIndex) + + sourceComponent: ContextMenus.Submenus.LaunchSubmenu { + app: root.app + visibilities: root.visibilities + launchApp: root.launchApp + toggle: root.toggle + children: launchSubmenuLoader.childrenRef + menuFactory: launchSubmenuLoader.factoryRef + } + } + + Loader { + active: displayedSubmenuIndex >= 0 && getSubmenuParentId(displayedSubmenuIndex) === "workspaces" + visible: active + Layout.fillWidth: true + Layout.preferredHeight: active ? implicitHeight : 0 + sourceComponent: ContextMenus.Submenus.WorkspaceSubmenu { + launchApp: root.launchApp + } + } + + Loader { + id: categorySubmenuLoader + active: displayedSubmenuIndex >= 0 && getSubmenuParentId(displayedSubmenuIndex) === "categories" + visible: active + Layout.fillWidth: true + Layout.preferredHeight: active ? implicitHeight : 0 + sourceComponent: ContextMenus.Submenus.CategorySubmenu { + app: root.app + Component.onCompleted: initializeState() + Component.onDestruction: saveChanges() + } + } + + // Generic submenu for custom items + Repeater { + model: { + if (displayedSubmenuIndex < 0) + return []; + const parentId = getSubmenuParentId(displayedSubmenuIndex); + if (parentId === "launch" || parentId === "workspaces" || parentId === "categories") + return []; + return getSubmenuItemsForIndex(displayedSubmenuIndex); + } + + MenuItem { + required property var modelData + readonly property var itemData: modelData.id === "separator" ? null : menuFactory.createMenuItem(modelData, true, displayedSubmenuIndex) + + isSeparator: modelData.id === "separator" + text: itemData?.text || "" + icon: itemData?.icon || "" + bold: itemData?.bold || false + hasSubMenu: false + submenuIndex: -1 + isSubmenuItem: true + + onTriggered: itemData?.onTriggered?.() + } + } + } + } + } +} diff --git a/modules/launcher/items/contextmenu/MenuItem.qml b/modules/launcher/items/contextmenu/MenuItem.qml new file mode 100644 index 000000000..a72c170d0 --- /dev/null +++ b/modules/launcher/items/contextmenu/MenuItem.qml @@ -0,0 +1,113 @@ +import qs.components +import qs.config +import qs.services +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: item + property string text: "" + property string icon: "" + property bool bold: false + property bool hasSubMenu: false + property int submenuIndex: -1 + property bool isSubmenuItem: false + property bool isSeparator: false + property bool preventSubmenuClose: false + signal triggered + signal hovered + + Layout.fillWidth: true + Layout.minimumWidth: isSeparator ? 0 : (itemRow.implicitWidth + Appearance.padding.small * 2) + implicitHeight: isSeparator ? 1 : (32 + Appearance.padding.small * 2) + radius: isSeparator ? 0 : Appearance.rounding.small + color: "transparent" + + Timer { + id: openTimer + interval: 250 + onTriggered: if (item.hasSubMenu && mouse.containsMouse) { + activeSubmenuIndex = item.submenuIndex; + Qt.callLater(() => { + submenuProgress = 1; + }); + } + } + + MouseArea { + id: mouse + anchors.fill: parent + enabled: !item.isSeparator + hoverEnabled: !item.isSeparator + cursorShape: item.hasSubMenu ? Qt.ArrowCursor : Qt.PointingHandCursor + onClicked: if (!item.hasSubMenu) + item.triggered() + onEntered: { + item.color = Qt.alpha(Colours.palette.m3primary, 0.15); + item.hovered(); + if (item.hasSubMenu) { + hoveredSubmenuIndex = item.submenuIndex; + submenuItemY = item.y + item.height / 2; + submenuCloseTimer.stop(); + openTimer.restart(); + } else if (item.isSubmenuItem) { + submenuCloseTimer.stop(); + } else { + hoveredSubmenuIndex = -1; + if (activeSubmenuIndex >= 0) { + submenuCloseTimer.restart(); + } + } + } + onExited: { + openTimer.stop(); + item.color = "transparent"; + if (activeSubmenuIndex >= 0 && !item.preventSubmenuClose) { + hoveredSubmenuIndex = -1; + submenuCloseTimer.restart(); + } + } + onPressed: if (!item.hasSubMenu) + item.color = Qt.alpha(Colours.palette.m3primary, 0.2) + onReleased: if (!item.hasSubMenu) + item.color = containsMouse ? Qt.alpha(Colours.palette.m3primary, 0.15) : "transparent" + } + + RowLayout { + id: itemRow + anchors.fill: parent + anchors.margins: Appearance.padding.small + spacing: Appearance.spacing.normal + visible: !item.isSeparator + + MaterialIcon { + text: item.icon + visible: text.length > 0 + color: Colours.palette.m3onSurface + font.pointSize: Appearance.font.size.normal + } + StyledText { + text: item.text + color: Colours.palette.m3onSurface + font.pointSize: Appearance.font.size.normal + font.weight: item.bold ? Font.DemiBold : Font.Normal + } + Item { + Layout.fillWidth: true + } + MaterialIcon { + text: "chevron_right" + visible: item.hasSubMenu + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.normal + } + } + + Rectangle { + anchors.centerIn: parent + width: parent.width - Appearance.padding.small * 2 + height: 1 + color: Colours.palette.m3outlineVariant + visible: item.isSeparator + } +} diff --git a/modules/launcher/items/contextmenu/MenuItemFactory.qml b/modules/launcher/items/contextmenu/MenuItemFactory.qml new file mode 100644 index 000000000..75b8ac731 --- /dev/null +++ b/modules/launcher/items/contextmenu/MenuItemFactory.qml @@ -0,0 +1,135 @@ +pragma ComponentBehavior: Bound +import "." as ContextMenus +import QtQuick + +QtObject { + id: factory + + required property var app + required property var visibilities + required property var launchApp + required property var toggle + required property var menuContext + + readonly property var registry: ContextMenus.ActionRegistry + + function parseConfigItem(item) { + if (typeof item === "string") { + return { + id: item, + config: {} + }; + } else if (typeof item === "object" && item !== null) { + const id = Object.keys(item)[0]; + return { + id: id, + config: item[id] || {} + }; + } + return { + id: "", + config: {} + }; + } + + function processMenuItems(configArray) { + if (!configArray || typeof configArray.length !== 'number' || !configArray.forEach) + return []; + + const items = []; + const submenus = {}; + + // First pass: collect all items and identify submenu children + configArray.forEach((item, index) => { + const parsed = parseConfigItem(item); + const parent = parsed.config.parent; + + if (parent) { + if (!submenus[parent]) + submenus[parent] = []; + submenus[parent].push({ + id: parsed.id, + config: parsed.config, + index: index + }); + } else { + items.push({ + id: parsed.id, + config: parsed.config, + index: index, + children: [] + }); + } + }); + + // Second pass: attach children to their parent submenus + items.forEach(item => { + if (submenus[item.id]) { + item.children = submenus[item.id]; + } + }); + + return items; + } + + function hasChildren(items, id) { + const item = items.find(i => i.id === id); + return item && item.children && item.children.length > 0; + } + + function getSubmenuItems(items, id) { + const item = items.find(i => i.id === id); + return item ? item.children : []; + } + + function shouldShowAsSubmenu(id, config, items) { + // Launch preset as submenu only if has children OR app has .desktop actions + if (id === "launch") { + const hasConfigChildren = hasChildren(items, id); + const hasDesktopActions = app && app.actions && app.actions.length > 0; + return Boolean(hasConfigChildren || hasDesktopActions); + } + + if (id === "workspaces" || id === "categories") + return true; + + if (hasChildren(items, id)) + return true; + + return false; + } + + function createMenuItem(itemData, isSubmenuItem, submenuIndex) { + const id = itemData.id; + const config = itemData.config; + + if (id === "separator") { + return { + type: "separator" + }; + } + + const defaults = registry.getDefaults(id); + const text = registry.getText(id, config, app); + const icon = registry.getIcon(id, config); + const hasSubmenu = submenuIndex >= 0; + const bold = config.bold !== undefined ? config.bold : (defaults.bold || false); + + return { + type: "menuitem", + id: id, + text: text, + icon: icon, + bold: bold, + hasSubmenu: hasSubmenu, + submenuIndex: submenuIndex, + isSubmenuItem: isSubmenuItem, + config: config, + onTriggered: function () { + if (!hasSubmenu) { + registry.execute(id, config, app, menuContext); + } + } + }; + } +} diff --git a/modules/launcher/items/contextmenu/Submenus.qml b/modules/launcher/items/contextmenu/Submenus.qml new file mode 100644 index 000000000..e887d64bd --- /dev/null +++ b/modules/launcher/items/contextmenu/Submenus.qml @@ -0,0 +1,197 @@ +import qs.config +import qs.services +import qs.services as Services +import Quickshell +import QtQuick +import QtQuick.Layouts + +Item { + id: submenus + + component LaunchSubmenu: ColumnLayout { + id: launchSubmenu + + required property DesktopEntry app + required property PersistentProperties visibilities + required property var launchApp + required property var toggle + property var children: [] + property var menuFactory: null + + spacing: Appearance.spacing.smaller + + MenuItem { + text: qsTr("Launch") + icon: "play_arrow" + isSubmenuItem: true + onTriggered: launchSubmenu.launchApp() + } + + Repeater { + model: launchSubmenu.app ? launchSubmenu.app.actions : [] + delegate: MenuItem { + required property var modelData + text: modelData.name || "" + icon: "play_arrow" + isSubmenuItem: true + visible: text.length > 0 + onTriggered: { + if (modelData && modelData.execute) + modelData.execute(); + if (launchSubmenu.visibilities) + launchSubmenu.visibilities.launcher = false; + launchSubmenu.toggle(); + } + } + } + + MenuItem { + isSeparator: true + visible: launchSubmenu.children && launchSubmenu.children.length > 0 + } + + Repeater { + model: launchSubmenu.children || [] + + delegate: MenuItem { + required property var modelData + readonly property var itemData: modelData.id === "separator" ? null : (launchSubmenu.menuFactory ? launchSubmenu.menuFactory.createMenuItem(modelData, true, -1) : null) + + visible: modelData.id === "separator" || (itemData !== null && itemData.text && itemData.text.length > 0) + isSeparator: modelData.id === "separator" + text: itemData?.text || "" + icon: itemData?.icon || "" + bold: itemData?.bold || false + hasSubMenu: false + submenuIndex: -1 + isSubmenuItem: true + + onTriggered: { + if (itemData?.onTriggered) { + itemData.onTriggered(); + } + } + } + } + } + + component WorkspaceSubmenu: ColumnLayout { + id: workspaceSubmenu + + required property var launchApp + + spacing: Appearance.spacing.smaller + + Repeater { + model: Services.Hypr.workspaces + delegate: MenuItem { + required property var modelData + property bool isCurrent: modelData.id === Services.Hypr.activeWsId + text: modelData.name || qsTr("Workspace %1").arg(modelData.id) + icon: isCurrent ? "radio_button_checked" : "radio_button_unchecked" + bold: isCurrent + isSubmenuItem: true + onTriggered: workspaceSubmenu.launchApp(modelData.id) + } + } + + MenuItem { + isSeparator: true + } + + MenuItem { + text: qsTr("New Workspace") + icon: "add_circle" + isSubmenuItem: true + onTriggered: { + Services.Hypr.dispatch("workspace empty"); + workspaceSubmenu.launchApp(); + } + } + } + + component CategorySubmenu: ColumnLayout { + id: categorySubmenu + + required property DesktopEntry app + + spacing: Appearance.spacing.smaller + + property var pendingChanges: ({}) + property int updateCounter: 0 + + function initializeState() { + pendingChanges = {}; + updateCounter = 0; + } + + function saveChanges() { + if (!categorySubmenu.app || Object.keys(pendingChanges).length === 0) + return; + + const appId = categorySubmenu.app.id; + const newCategories = []; + + for (let i = 0; i < Config.launcher.categories.length; i++) { + const category = Config.launcher.categories[i]; + const newCategory = { + name: category.name, + icon: category.icon, + apps: category.apps ? [...category.apps] : [] + }; + + if (pendingChanges.hasOwnProperty(category.name)) { + const shouldBeIncluded = pendingChanges[category.name]; + const index = newCategory.apps.indexOf(appId); + + if (shouldBeIncluded && index < 0) { + newCategory.apps.push(appId); + } else if (!shouldBeIncluded && index >= 0) { + newCategory.apps.splice(index, 1); + } + } + + newCategories.push(newCategory); + } + + Config.launcher.categories = newCategories; + Config.save(); + pendingChanges = {}; + } + + Repeater { + model: Config.launcher.categories || [] + delegate: MenuItem { + required property var modelData + + property bool checked: { + categorySubmenu.updateCounter; // Depend on counter + + if (!categorySubmenu.app || !modelData.apps) + return false; + const appId = categorySubmenu.app.id; + + if (categorySubmenu.pendingChanges.hasOwnProperty(modelData.name)) { + return categorySubmenu.pendingChanges[modelData.name]; + } + + for (let i = 0; i < modelData.apps.length; i++) { + if (modelData.apps[i] === appId) + return true; + } + return false; + } + + text: modelData.name || "" + icon: checked ? "check_box" : "check_box_outline_blank" + isSubmenuItem: true + preventSubmenuClose: true + + onTriggered: { + categorySubmenu.pendingChanges[modelData.name] = !checked; + categorySubmenu.updateCounter++; + } + } + } + } +} diff --git a/modules/launcher/items/contextmenu/shaders/goo_sdf.frag b/modules/launcher/items/contextmenu/shaders/goo_sdf.frag new file mode 100644 index 000000000..f7b074403 --- /dev/null +++ b/modules/launcher/items/contextmenu/shaders/goo_sdf.frag @@ -0,0 +1,105 @@ +#version 440 +layout(location = 0) in vec2 vUv; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + + vec2 sizePx; + vec4 menuRectPx; + vec4 subRectPx; + float radiusPx; + float smoothPxTop; + float smoothPxBottom; + + vec4 fillColor; + vec4 shadowColor; + vec2 shadowOffsetPx; + float shadowSoftPx; +} ubuf; + +// distance to rounded rectangle (centered at origin) +float sdRoundRect(vec2 p, vec2 b, float r) { + // b = half-size + vec2 q = abs(p) - (b - vec2(r)); + return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r; +} + +// smooth union (goo) +float smin(float a, float b, float k) { + // k in pixels-ish + float h = clamp(0.5 + 0.5*(b - a)/k, 0.0, 1.0); + return mix(b, a, h) - k*h*(1.0 - h); +} + +vec4 premul(vec4 c) { return vec4(c.rgb * c.a, c.a); } + +void main() { + // local pixel coords in this effect item + vec2 p = vUv * ubuf.sizePx; + + // menu rect SDF + vec2 mPos = ubuf.menuRectPx.xy; + vec2 mSize = ubuf.menuRectPx.zw; + vec2 mCenter = mPos + mSize * 0.5; + float dm = sdRoundRect(p - mCenter, mSize * 0.5, ubuf.radiusPx); + + // optional submenu rect SDF + float d = dm; + if (ubuf.subRectPx.z > 0.0 && ubuf.subRectPx.w > 0.0) { + vec2 sPos = ubuf.subRectPx.xy; + vec2 sSize = ubuf.subRectPx.zw; + vec2 sCenter = sPos + sSize * 0.5; + float ds = sdRoundRect(p - sCenter, sSize * 0.5, ubuf.radiusPx); + + // Interpolate smoothing based on Y position + // Calculate merge point Y (where the shapes meet) + float mergeY = (mCenter.y + sCenter.y) * 0.5; + + // Interpolate smoothing: top uses smoothPxTop, bottom uses smoothPxBottom + float t = clamp((p.y - mergeY) / max(1.0, abs(mCenter.y - sCenter.y)), 0.0, 1.0); + float smoothPx = mix(ubuf.smoothPxTop, ubuf.smoothPxBottom, t); + + // smooth union or hard union (straight line when smoothPx <= 0) + if (smoothPx > 0.0) { + d = smin(dm, ds, max(1.0, smoothPx)); + } else { + d = min(dm, ds); + } + } + + // Fill alpha: inside shape => 1, outside => 0 with soft edge + float edge = 1.0; + float fillA = 1.0 - smoothstep(0.0, edge, d); + + // Shadow: evaluate SDF with an offset, soften more + vec2 ps = p - ubuf.shadowOffsetPx; + float dmS = sdRoundRect(ps - mCenter, mSize * 0.5, ubuf.radiusPx); + float dS = dmS; + if (ubuf.subRectPx.z > 0.0 && ubuf.subRectPx.w > 0.0) { + vec2 sPos = ubuf.subRectPx.xy; + vec2 sSize = ubuf.subRectPx.zw; + vec2 sCenter = sPos + sSize * 0.5; + float dsS = sdRoundRect(ps - sCenter, sSize * 0.5, ubuf.radiusPx); + // Use same interpolated smoothing for shadow + float mergeY = (mCenter.y + sCenter.y) * 0.5; + float t = clamp((ps.y - mergeY) / max(1.0, abs(mCenter.y - sCenter.y)), 0.0, 1.0); + float smoothPx = mix(ubuf.smoothPxTop, ubuf.smoothPxBottom, t); + + if (smoothPx > 0.0) { + dS = smin(dmS, dsS, max(1.0, smoothPx)); + } else { + dS = min(dmS, dsS); + } + } + // Start shadow closer (-2px) but fade over longer distance (2x softness) + float sh = 1.0 - smoothstep(-2.0, ubuf.shadowSoftPx * 2.0, dS); + float shadowOnly = max(sh - fillA, 0.0); + + vec4 f = premul(ubuf.fillColor); + vec4 s = premul(ubuf.shadowColor); + + vec4 outC = f * fillA + s * shadowOnly; + fragColor = outC * ubuf.qt_Opacity; +} diff --git a/modules/launcher/items/contextmenu/shaders/goo_sdf.frag.qsb b/modules/launcher/items/contextmenu/shaders/goo_sdf.frag.qsb new file mode 100644 index 000000000..4caad0180 Binary files /dev/null and b/modules/launcher/items/contextmenu/shaders/goo_sdf.frag.qsb differ diff --git a/modules/launcher/items/contextmenu/shaders/goo_sdf.vert b/modules/launcher/items/contextmenu/shaders/goo_sdf.vert new file mode 100644 index 000000000..608d46aa5 --- /dev/null +++ b/modules/launcher/items/contextmenu/shaders/goo_sdf.vert @@ -0,0 +1,27 @@ +#version 440 +layout(location = 0) in vec4 qt_Vertex; +layout(location = 1) in vec2 qt_MultiTexCoord0; + +layout(location = 0) out vec2 vUv; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + + vec2 sizePx; + vec4 menuRectPx; + vec4 subRectPx; + float radiusPx; + float smoothPxTop; + float smoothPxBottom; + + vec4 fillColor; + vec4 shadowColor; + vec2 shadowOffsetPx; + float shadowSoftPx; +} ubuf; + +void main() { + vUv = qt_MultiTexCoord0; + gl_Position = ubuf.qt_Matrix * qt_Vertex; +} diff --git a/modules/launcher/items/contextmenu/shaders/goo_sdf.vert.qsb b/modules/launcher/items/contextmenu/shaders/goo_sdf.vert.qsb new file mode 100644 index 000000000..e236b719e Binary files /dev/null and b/modules/launcher/items/contextmenu/shaders/goo_sdf.vert.qsb differ diff --git a/modules/launcher/services/Actions.qml b/modules/launcher/services/Actions.qml index 2a76012fe..5c1cb6bb8 100644 --- a/modules/launcher/services/Actions.qml +++ b/modules/launcher/services/Actions.qml @@ -1,11 +1,11 @@ pragma Singleton import ".." -import QtQuick -import Quickshell import qs.services import qs.config import qs.utils +import Quickshell +import QtQuick Searcher { id: root diff --git a/modules/launcher/services/Apps.qml b/modules/launcher/services/Apps.qml index 1f1f35707..7f2d64556 100644 --- a/modules/launcher/services/Apps.qml +++ b/modules/launcher/services/Apps.qml @@ -1,9 +1,9 @@ pragma Singleton -import Quickshell -import Caelestia import qs.config import qs.utils +import Caelestia +import Quickshell Searcher { id: root diff --git a/modules/launcher/services/M3Variants.qml b/modules/launcher/services/M3Variants.qml index a951b8c65..963a4d435 100644 --- a/modules/launcher/services/M3Variants.qml +++ b/modules/launcher/services/M3Variants.qml @@ -1,10 +1,10 @@ pragma Singleton import ".." -import QtQuick -import Quickshell import qs.config import qs.utils +import Quickshell +import QtQuick Searcher { id: root diff --git a/modules/launcher/services/Schemes.qml b/modules/launcher/services/Schemes.qml index d389aa176..dbb2dac0a 100644 --- a/modules/launcher/services/Schemes.qml +++ b/modules/launcher/services/Schemes.qml @@ -1,11 +1,11 @@ pragma Singleton import ".." -import QtQuick -import Quickshell -import Quickshell.Io import qs.config import qs.utils +import Quickshell +import Quickshell.Io +import QtQuick Searcher { id: root @@ -55,7 +55,7 @@ Searcher { for (const f of s) flat.push(f); - schemes.model = flat.sort((a, b) => String(a.name + a.flavour).localeCompare((b.name + b.flavour))); + schemes.model = flat.sort((a, b) => (a.name + a.flavour).localeCompare((b.name + b.flavour))); } } } diff --git a/modules/lock/Center.qml b/modules/lock/Center.qml index 73b6e6705..aa926acd1 100644 --- a/modules/lock/Center.qml +++ b/modules/lock/Center.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.controls import qs.components.images import qs.services import qs.config import qs.utils +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root @@ -54,7 +54,6 @@ ColumnLayout { } Loader { - asynchronous: true Layout.leftMargin: Appearance.spacing.small Layout.alignment: Qt.AlignVCenter @@ -135,12 +134,12 @@ ColumnLayout { } StateLayer { + hoverEnabled: false + cursorShape: Qt.IBeamCursor + function onClicked(): void { parent.forceActiveFocus(); } - - hoverEnabled: false - cursorShape: Qt.IBeamCursor } RowLayout { @@ -194,11 +193,11 @@ ColumnLayout { radius: Appearance.rounding.full StateLayer { + color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + function onClicked(): void { root.lock.pam.passwd.start(); } - - color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface } MaterialIcon { @@ -357,6 +356,8 @@ ColumnLayout { } Connections { + target: root.lock.pam + function onFlashMsg(): void { exitAnim.stop(); if (message.scale < 1) @@ -364,8 +365,6 @@ ColumnLayout { else flashAnim.restart(); } - - target: root.lock.pam } Anim { diff --git a/modules/lock/Content.qml b/modules/lock/Content.qml index 4427099fb..a024ddc23 100644 --- a/modules/lock/Content.qml +++ b/modules/lock/Content.qml @@ -1,8 +1,8 @@ -import QtQuick -import QtQuick.Layouts import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Layouts RowLayout { id: root diff --git a/modules/lock/Fetch.qml b/modules/lock/Fetch.qml index ab6de7b6c..e96b14315 100644 --- a/modules/lock/Fetch.qml +++ b/modules/lock/Fetch.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell.Services.UPower import qs.components import qs.components.effects import qs.services import qs.config import qs.utils +import Quickshell.Services.UPower +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root @@ -162,7 +162,6 @@ ColumnLayout { } component WrappedLoader: Loader { - asynchronous: true visible: active } diff --git a/modules/lock/InputField.qml b/modules/lock/InputField.qml index 1286267a7..358093f39 100644 --- a/modules/lock/InputField.qml +++ b/modules/lock/InputField.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components import qs.services import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts Item { id: root @@ -20,6 +20,8 @@ Item { clip: true Connections { + target: root.pam + function onBufferChanged(): void { if (root.pam.buffer.length > root.buffer.length) { charList.bindImWidth(); @@ -30,8 +32,6 @@ Item { root.buffer = root.pam.buffer; } - - target: root.pam } StyledText { diff --git a/modules/lock/Lock.qml b/modules/lock/Lock.qml index 5d70266a0..6fd5277fe 100644 --- a/modules/lock/Lock.qml +++ b/modules/lock/Lock.qml @@ -1,9 +1,9 @@ pragma ComponentBehavior: Bound +import qs.components.misc import Quickshell import Quickshell.Io import Quickshell.Wayland -import qs.components.misc Scope { property alias lock: lock @@ -25,23 +25,21 @@ Scope { lock: lock } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "lock" description: "Lock the current session" onPressed: lock.locked = true } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "unlock" description: "Unlock the current session" onPressed: lock.unlock() } IpcHandler { + target: "lock" + function lock(): void { lock.locked = true; } @@ -53,7 +51,5 @@ Scope { function isLocked(): bool { return lock.locked; } - - target: "lock" } } diff --git a/modules/lock/LockSurface.qml b/modules/lock/LockSurface.qml index 46862f83f..279c55138 100644 --- a/modules/lock/LockSurface.qml +++ b/modules/lock/LockSurface.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Effects -import Quickshell.Wayland import qs.components import qs.services import qs.config +import Quickshell.Wayland +import QtQuick +import QtQuick.Effects WlSessionLockSurface { id: root @@ -18,11 +18,11 @@ WlSessionLockSurface { color: "transparent" Connections { + target: root.lock + function onUnlock(): void { unlockAnim.start(); } - - target: root.lock } SequentialAnimation { diff --git a/modules/lock/Media.qml b/modules/lock/Media.qml index cabe60b9a..b7e58bbcb 100644 --- a/modules/lock/Media.qml +++ b/modules/lock/Media.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.effects import qs.services import qs.config +import QtQuick +import QtQuick.Layouts Item { id: root @@ -18,7 +18,7 @@ Item { Image { anchors.fill: parent - source: Players.active?.trackArtUrl ?? "" // qmllint disable incompatible-type + source: Players.active?.trackArtUrl ?? "" asynchronous: true fillMode: Image.PreserveAspectCrop @@ -110,34 +110,34 @@ Item { spacing: Appearance.spacing.large PlayerControl { + icon: "skip_previous" + function onClicked(): void { if (Players.active?.canGoPrevious) Players.active.previous(); } - - icon: "skip_previous" } PlayerControl { - function onClicked(): void { - if (Players.active?.canTogglePlaying) - Players.active.togglePlaying(); - } - animate: true icon: active ? "pause" : "play_arrow" colour: "Primary" level: active ? 2 : 1 active: Players.active?.isPlaying ?? false + + function onClicked(): void { + if (Players.active?.canTogglePlaying) + Players.active.togglePlaying(); + } } PlayerControl { + icon: "skip_next" + function onClicked(): void { if (Players.active?.canGoNext) Players.active.next(); } - - icon: "skip_next" } } } @@ -171,11 +171,11 @@ Item { StateLayer { id: controlState + color: control.active ? Colours.palette[`m3on${control.colour}`] : Colours.palette[`m3on${control.colour}Container`] + function onClicked(): void { control.onClicked(); } - - color: control.active ? Colours.palette[`m3on${control.colour}`] : Colours.palette[`m3on${control.colour}Container`] } MaterialIcon { diff --git a/modules/lock/NotifDock.qml b/modules/lock/NotifDock.qml index 62439d0c3..cce86cdf3 100644 --- a/modules/lock/NotifDock.qml +++ b/modules/lock/NotifDock.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets import qs.components import qs.components.containers import qs.components.effects import qs.services import qs.config +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root @@ -39,7 +39,6 @@ ColumnLayout { color: "transparent" Loader { - asynchronous: true anchors.centerIn: parent active: opacity > 0 opacity: Notifs.list.length > 0 && !Config.lock.hideNotifs ? 0 : 1 diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index 133dc6266..7fcb108ec 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -1,15 +1,15 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets -import Quickshell.Services.Notifications import qs.components import qs.components.effects import qs.services import qs.config import qs.utils +import Quickshell +import Quickshell.Widgets +import Quickshell.Services.Notifications +import QtQuick +import QtQuick.Layouts StyledRect { id: root @@ -109,14 +109,12 @@ StyledRect { radius: Appearance.rounding.full Loader { - asynchronous: true anchors.centerIn: parent sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp } } Loader { - asynchronous: true anchors.right: parent.right anchors.bottom: parent.bottom active: root.appIcon && root.image @@ -176,11 +174,11 @@ StyledRect { Layout.preferredWidth: root.notifs.length > Config.notifs.groupPreviewNum ? implicitWidth : 0 StateLayer { + color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface + function onClicked(): void { root.expanded = !root.expanded; } - - color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface } RowLayout { @@ -272,7 +270,6 @@ StyledRect { } Loader { - asynchronous: true Layout.fillWidth: true opacity: root.expanded ? 1 : 0 @@ -306,7 +303,7 @@ StyledRect { component NotifLine: StyledText { id: notifLine - required property NotifData modelData + required property Notifs.Notif modelData Layout.fillWidth: true textFormat: Text.MarkdownText diff --git a/modules/lock/Pam.qml b/modules/lock/Pam.qml index 8345ebc4d..0186c2f84 100644 --- a/modules/lock/Pam.qml +++ b/modules/lock/Pam.qml @@ -1,9 +1,9 @@ -import QtQuick +import qs.config import Quickshell import Quickshell.Io import Quickshell.Wayland import Quickshell.Services.Pam -import qs.config +import QtQuick Scope { id: root @@ -132,7 +132,7 @@ Scope { id: availProc command: ["sh", "-c", "fprintd-list $USER"] - onExited: code => { // qmllint disable signal-handler-parameters + onExited: code => { fprint.available = code === 0; fprint.checkAvail(); } @@ -166,6 +166,8 @@ Scope { } Connections { + target: root.lock + function onSecureChanged(): void { if (root.lock.secure) { availProc.running = true; @@ -179,15 +181,13 @@ Scope { function onUnlock(): void { fprint.abort(); } - - target: root.lock } Connections { + target: Config.lock + function onEnableFprintChanged(): void { fprint.checkAvail(); } - - target: Config.lock } } diff --git a/modules/lock/Resources.qml b/modules/lock/Resources.qml index 33c0e4c00..82c004c22 100644 --- a/modules/lock/Resources.qml +++ b/modules/lock/Resources.qml @@ -1,10 +1,10 @@ -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.controls import qs.components.misc import qs.services import qs.config +import QtQuick +import QtQuick.Layouts GridLayout { id: root diff --git a/modules/lock/WeatherInfo.qml b/modules/lock/WeatherInfo.qml index 213bf155d..d6c25af29 100644 --- a/modules/lock/WeatherInfo.qml +++ b/modules/lock/WeatherInfo.qml @@ -1,10 +1,11 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts import qs.components import qs.services import qs.config +import qs.utils +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root @@ -18,7 +19,6 @@ ColumnLayout { spacing: Appearance.spacing.small Loader { - asynchronous: true Layout.topMargin: Appearance.padding.large * 2 Layout.bottomMargin: -Appearance.padding.large Layout.alignment: Qt.AlignHCenter @@ -71,7 +71,6 @@ ColumnLayout { } Loader { - asynchronous: true Layout.rightMargin: Appearance.padding.smaller active: root.width > 400 visible: active @@ -108,7 +107,6 @@ ColumnLayout { Loader { id: forecastLoader - asynchronous: true Layout.topMargin: Appearance.spacing.smaller Layout.bottomMargin: Appearance.padding.large * 2 Layout.fillWidth: true diff --git a/modules/notifications/Background.qml b/modules/notifications/Background.qml index 4d7a5ff76..a44cb19b7 100644 --- a/modules/notifications/Background.qml +++ b/modules/notifications/Background.qml @@ -1,8 +1,8 @@ -import QtQuick -import QtQuick.Shapes import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Shapes ShapePath { id: root diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml index bd18276ef..2d4590e0e 100644 --- a/modules/notifications/Content.qml +++ b/modules/notifications/Content.qml @@ -1,18 +1,16 @@ -import QtQuick -import Quickshell -import Quickshell.Widgets -import qs.components import qs.components.containers import qs.components.widgets import qs.services import qs.config +import Quickshell +import Quickshell.Widgets +import QtQuick Item { id: root - required property DrawerVisibilities visibilities - required property Item osdPanel - required property Item sessionPanel + required property PersistentProperties visibilities + required property Item panels readonly property int padding: Appearance.padding.large anchors.top: parent.top @@ -27,21 +25,23 @@ Item { let height = (count - 1) * Appearance.spacing.smaller; for (let i = 0; i < count; i++) - height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0; + height += list.itemAtIndex(i)?.nonAnimHeight ?? 0; - if (visibilities.osd) { - const h = osdPanel.y - Config.border.rounding * 2 - padding * 2; - if (height > h) - height = h; - } + if (visibilities && panels) { + if (visibilities.osd) { + const h = panels.osd.y - Config.border.rounding * 2 - padding * 2; + if (height > h) + height = h; + } - if (visibilities.session) { - const h = sessionPanel.y - Config.border.rounding * 2 - padding * 2; - if (height > h) - height = h; + if (visibilities.session) { + const h = panels.session.y - Config.border.rounding * 2 - padding * 2; + if (height > h) + height = h; + } } - return Math.min(((QsWindow.window as QsWindow)?.screen?.height ?? 0) - Config.border.thickness * 2, height + padding * 2); + return Math.min((QsWindow.window?.screen?.height ?? 0) - Config.border.thickness * 2, height + padding * 2); } ClippingWrapperRectangle { @@ -62,9 +62,79 @@ Item { orientation: Qt.Vertical spacing: 0 - cacheBuffer: (QsWindow.window as QsWindow)?.screen.height ?? 0 + cacheBuffer: QsWindow.window?.screen.height ?? 0 + + delegate: Item { + id: wrapper + + required property Notifs.Notif modelData + required property int index + readonly property alias nonAnimHeight: notif.nonAnimHeight + property int idx + + onIndexChanged: { + if (index !== -1) + idx = index; + } + + implicitWidth: notif.implicitWidth + implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Appearance.spacing.smaller) + + ListView.onRemove: removeAnim.start() + + SequentialAnimation { + id: removeAnim + + PropertyAction { + target: wrapper + property: "ListView.delayRemove" + value: true + } + PropertyAction { + target: wrapper + property: "enabled" + value: false + } + PropertyAction { + target: wrapper + property: "implicitHeight" + value: 0 + } + PropertyAction { + target: wrapper + property: "z" + value: 1 + } + Anim { + target: notif + property: "x" + to: (notif.x >= 0 ? Config.notifs.sizes.width : -Config.notifs.sizes.width) * 2 + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.emphasized + } + PropertyAction { + target: wrapper + property: "ListView.delayRemove" + value: false + } + } + + ClippingRectangle { + anchors.top: parent.top + anchors.topMargin: wrapper.idx === 0 ? 0 : Appearance.spacing.smaller + + color: "transparent" + radius: notif.radius + implicitWidth: notif.implicitWidth + implicitHeight: notif.implicitHeight - delegate: NotifWrapper {} + Notification { + id: notif + + modelData: wrapper.modelData + } + } + } move: Transition { Anim { @@ -89,7 +159,7 @@ Item { let height = 0; for (let i = 0; i < count; i++) { - height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Appearance.spacing.smaller; + height += (list.itemAtIndex(i)?.nonAnimHeight ?? 0) + Appearance.spacing.smaller; if (height - Appearance.spacing.smaller >= scrollY) return i; @@ -110,7 +180,7 @@ Item { let height = 0; for (let i = count - 1; i >= 0; i--) { - height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Appearance.spacing.smaller; + height += (list.itemAtIndex(i)?.nonAnimHeight ?? 0) + Appearance.spacing.smaller; if (height - Appearance.spacing.smaller >= scrollY) return count - i - 1; @@ -126,78 +196,6 @@ Item { Anim {} } - component NotifWrapper: Item { - id: wrapper - - required property NotifData modelData - required property int index - readonly property alias nonAnimHeight: notif.nonAnimHeight - property int idx - - onIndexChanged: { - if (index !== -1) - idx = index; - } - - implicitWidth: notif.implicitWidth - implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Appearance.spacing.smaller) - - ListView.onRemove: removeAnim.start() - - SequentialAnimation { - id: removeAnim - - PropertyAction { - target: wrapper - property: "ListView.delayRemove" - value: true - } - PropertyAction { - target: wrapper - property: "enabled" - value: false - } - PropertyAction { - target: wrapper - property: "implicitHeight" - value: 0 - } - PropertyAction { - target: wrapper - property: "z" - value: 1 - } - Anim { - target: notif - property: "x" - to: (notif.x >= 0 ? Config.notifs.sizes.width : -Config.notifs.sizes.width) * 2 - duration: Appearance.anim.durations.normal - easing.bezierCurve: Appearance.anim.curves.emphasized - } - PropertyAction { - target: wrapper - property: "ListView.delayRemove" - value: false - } - } - - ClippingRectangle { - anchors.top: parent.top - anchors.topMargin: wrapper.idx === 0 ? 0 : Appearance.spacing.smaller - - color: "transparent" - radius: notif.radius - implicitWidth: notif.implicitWidth - implicitHeight: notif.implicitHeight - - Notification { - id: notif - - modelData: wrapper.modelData - } - } - } - component Anim: NumberAnimation { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.type: Easing.BezierSpline diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index 7b580a74f..c8efa8d78 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -1,21 +1,21 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import QtQuick.Shapes -import Quickshell -import Quickshell.Widgets -import Quickshell.Services.Notifications import qs.components import qs.components.effects import qs.services import qs.config import qs.utils +import Quickshell +import Quickshell.Widgets +import Quickshell.Services.Notifications +import QtQuick +import QtQuick.Layouts +import QtQuick.Shapes StyledRect { id: root - required property NotifData modelData + required property Notifs.Notif modelData readonly property bool hasImage: modelData.image.length > 0 readonly property bool hasAppIcon: modelData.appIcon.length > 0 readonly property int bodyTextFormat: /[<*_`#\[\]]/.test(modelData.body) ? Text.MarkdownText : Text.PlainText @@ -85,7 +85,7 @@ StyledRect { return; const actions = root.modelData.actions; - if (actions.length === 1) + if (actions?.length === 1) actions[0].invoke(); } @@ -109,7 +109,6 @@ StyledRect { Loader { id: image - asynchronous: true active: root.hasImage anchors.left: parent.left @@ -138,7 +137,6 @@ StyledRect { Loader { id: appIcon - asynchronous: true active: root.hasAppIcon || !root.hasImage anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter @@ -155,7 +153,6 @@ StyledRect { Loader { id: icon - asynchronous: true active: root.hasAppIcon anchors.centerIn: parent @@ -172,7 +169,6 @@ StyledRect { } Loader { - asynchronous: true active: !root.hasAppIcon anchors.centerIn: parent anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02 @@ -361,12 +357,12 @@ StyledRect { implicitHeight: expandIcon.height StateLayer { + radius: Appearance.rounding.full + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + function onClicked() { root.expanded = !root.expanded; } - - radius: Appearance.rounding.full - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface } MaterialIcon { @@ -460,7 +456,6 @@ StyledRect { Action { modelData: QtObject { readonly property string text: qsTr("Close") - function invoke(): void { root.modelData.close(); } @@ -492,12 +487,12 @@ StyledRect { implicitHeight: actionText.height + Appearance.padding.small * 2 StateLayer { + radius: Appearance.rounding.full + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurface + function onClicked(): void { action.modelData.invoke(); } - - radius: Appearance.rounding.full - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurface } StyledText { diff --git a/modules/notifications/Wrapper.qml b/modules/notifications/Wrapper.qml index 2b581e93e..61acc56e1 100644 --- a/modules/notifications/Wrapper.qml +++ b/modules/notifications/Wrapper.qml @@ -1,17 +1,15 @@ -import QtQuick import qs.components import qs.config +import QtQuick Item { id: root - required property DrawerVisibilities visibilities - required property Item sidebarPanel - property alias osdPanel: content.osdPanel - property alias sessionPanel: content.sessionPanel + required property var visibilities + required property Item panels visible: height > 0 - implicitWidth: Math.max(sidebarPanel.width, content.implicitWidth) + implicitWidth: Math.max(panels.sidebar.width, content.implicitWidth) implicitHeight: content.implicitHeight states: State { @@ -36,5 +34,6 @@ Item { id: content visibilities: root.visibilities + panels: root.panels } } diff --git a/modules/osd/Background.qml b/modules/osd/Background.qml index a609f4601..78955c7a8 100644 --- a/modules/osd/Background.qml +++ b/modules/osd/Background.qml @@ -1,8 +1,8 @@ -import QtQuick -import QtQuick.Shapes import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Shapes ShapePath { id: root diff --git a/modules/osd/Content.qml b/modules/osd/Content.qml index 5ec6ad983..770fb6968 100644 --- a/modules/osd/Content.qml +++ b/modules/osd/Content.qml @@ -1,18 +1,18 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.controls import qs.services import qs.config import qs.utils +import QtQuick +import QtQuick.Layouts Item { id: root required property Brightness.Monitor monitor - required property DrawerVisibilities visibilities + required property var visibilities required property real volume required property bool muted @@ -31,6 +31,9 @@ Item { // Speaker volume CustomMouseArea { + implicitWidth: Config.osd.sizes.sliderWidth + implicitHeight: Config.osd.sizes.sliderHeight + function onWheel(event: WheelEvent) { if (event.angleDelta.y > 0) Audio.incrementVolume(); @@ -38,9 +41,6 @@ Item { Audio.decrementVolume(); } - implicitWidth: Config.osd.sizes.sliderWidth - implicitHeight: Config.osd.sizes.sliderHeight - FilledSlider { anchors.fill: parent @@ -56,6 +56,9 @@ Item { shouldBeActive: Config.osd.enableMicrophone && (!Config.osd.enableBrightness || !root.visibilities.session) sourceComponent: CustomMouseArea { + implicitWidth: Config.osd.sizes.sliderWidth + implicitHeight: Config.osd.sizes.sliderHeight + function onWheel(event: WheelEvent) { if (event.angleDelta.y > 0) Audio.incrementSourceVolume(); @@ -63,9 +66,6 @@ Item { Audio.decrementSourceVolume(); } - implicitWidth: Config.osd.sizes.sliderWidth - implicitHeight: Config.osd.sizes.sliderHeight - FilledSlider { anchors.fill: parent @@ -82,6 +82,9 @@ Item { shouldBeActive: Config.osd.enableBrightness sourceComponent: CustomMouseArea { + implicitWidth: Config.osd.sizes.sliderWidth + implicitHeight: Config.osd.sizes.sliderHeight + function onWheel(event: WheelEvent) { const monitor = root.monitor; if (!monitor) @@ -92,9 +95,6 @@ Item { monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); } - implicitWidth: Config.osd.sizes.sliderWidth - implicitHeight: Config.osd.sizes.sliderHeight - FilledSlider { anchors.fill: parent @@ -109,7 +109,6 @@ Item { component WrappedLoader: Loader { required property bool shouldBeActive - asynchronous: true Layout.preferredHeight: shouldBeActive ? Config.osd.sizes.sliderHeight : 0 opacity: shouldBeActive ? 1 : 0 active: opacity > 0 diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml index 939b57de1..2519609dd 100644 --- a/modules/osd/Wrapper.qml +++ b/modules/osd/Wrapper.qml @@ -1,16 +1,16 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell import qs.components import qs.services import qs.config +import Quickshell +import QtQuick Item { id: root required property ShellScreen screen - required property DrawerVisibilities visibilities + required property var visibilities property bool hovered readonly property Brightness.Monitor monitor: Brightness.getMonitorForScreen(root.screen) readonly property bool shouldBeActive: visibilities.osd && Config.osd.enabled && !(visibilities.utilities && Config.utilities.enabled) @@ -71,6 +71,8 @@ Item { ] Connections { + target: Audio + function onMutedChanged(): void { root.show(); root.muted = Audio.muted; @@ -90,17 +92,15 @@ Item { root.show(); root.sourceVolume = Audio.sourceVolume; } - - target: Audio } Connections { + target: root.monitor + function onBrightnessChanged(): void { root.show(); root.brightness = root.monitor?.brightness ?? 0; } - - target: root.monitor } Timer { diff --git a/modules/session/Background.qml b/modules/session/Background.qml index a609f4601..78955c7a8 100644 --- a/modules/session/Background.qml +++ b/modules/session/Background.qml @@ -1,8 +1,8 @@ -import QtQuick -import QtQuick.Shapes import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Shapes ShapePath { id: root diff --git a/modules/session/Content.qml b/modules/session/Content.qml index 405fce1ce..45152e28a 100644 --- a/modules/session/Content.qml +++ b/modules/session/Content.qml @@ -1,16 +1,16 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell import qs.components import qs.services import qs.config import qs.utils +import Quickshell +import QtQuick Column { id: root - required property DrawerVisibilities visibilities + required property PersistentProperties visibilities padding: Appearance.padding.large spacing: Appearance.spacing.large @@ -26,12 +26,12 @@ Column { Component.onCompleted: forceActiveFocus() Connections { + target: root.visibilities + function onLauncherChanged(): void { if (!root.visibilities.launcher) logout.forceActiveFocus(); } - - target: root.visibilities } } @@ -115,12 +115,12 @@ Column { } StateLayer { + radius: parent.radius + color: button.activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + function onClicked(): void { Quickshell.execDetached(button.command); } - - radius: parent.radius - color: button.activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface } MaterialIcon { diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml index 2924f776f..14b03a809 100644 --- a/modules/session/Wrapper.qml +++ b/modules/session/Wrapper.qml @@ -1,13 +1,14 @@ pragma ComponentBehavior: Bound -import QtQuick import qs.components import qs.config +import Quickshell +import QtQuick Item { id: root - required property DrawerVisibilities visibilities + required property PersistentProperties visibilities required property var panels readonly property real nonAnimWidth: content.implicitWidth diff --git a/modules/sidebar/Background.qml b/modules/sidebar/Background.qml index 4cc142628..beefdf5c4 100644 --- a/modules/sidebar/Background.qml +++ b/modules/sidebar/Background.qml @@ -1,8 +1,8 @@ -import QtQuick -import QtQuick.Shapes import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Shapes ShapePath { id: root diff --git a/modules/sidebar/Content.qml b/modules/sidebar/Content.qml index 7dbbd06c3..1b7feed66 100644 --- a/modules/sidebar/Content.qml +++ b/modules/sidebar/Content.qml @@ -1,14 +1,14 @@ -import QtQuick -import QtQuick.Layouts import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Layouts Item { id: root required property Props props - required property DrawerVisibilities visibilities + required property var visibilities ColumnLayout { id: layout diff --git a/modules/sidebar/Notif.qml b/modules/sidebar/Notif.qml index fe56798f7..5a317640f 100644 --- a/modules/sidebar/Notif.qml +++ b/modules/sidebar/Notif.qml @@ -1,21 +1,21 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components import qs.services import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts StyledRect { id: root - required property NotifData modelData + required property Notifs.Notif modelData required property Props props required property bool expanded - required property DrawerVisibilities visibilities + required property var visibilities - readonly property StyledText body: (expandedContent.item as ExpandedBody)?.body ?? null + readonly property StyledText body: expandedContent.item?.body ?? null readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Appearance.padding.normal * 2 : summaryHeightMetrics.height implicitHeight: nonAnimHeight @@ -118,45 +118,42 @@ StyledRect { anchors.right: parent.right anchors.topMargin: Appearance.spacing.small / 2 - sourceComponent: ExpandedBody {} - } + sourceComponent: ColumnLayout { + readonly property alias body: body - Behavior on implicitHeight { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - component ExpandedBody: ColumnLayout { - readonly property alias body: bodyText + spacing: Appearance.spacing.smaller - spacing: Appearance.spacing.smaller + StyledText { + id: body - StyledText { - id: bodyText + Layout.fillWidth: true + textFormat: Text.MarkdownText + text: root.modelData.body.replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body here! :/") + color: root.modelData.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline + wrapMode: Text.WordWrap - Layout.fillWidth: true - textFormat: Text.MarkdownText - text: root.modelData.body.replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body here! :/") - color: root.modelData.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline - wrapMode: Text.WordWrap + onLinkActivated: link => { + Quickshell.execDetached(["app2unit", "-O", "--", link]); + root.visibilities.sidebar = false; + } + } - onLinkActivated: link => { - Quickshell.execDetached(["app2unit", "-O", "--", link]); - root.visibilities.sidebar = false; + NotifActionList { + notif: root.modelData } } + } - NotifActionList { - notif: root.modelData + Behavior on implicitHeight { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } component WrappedLoader: Loader { required property bool shouldBeActive - asynchronous: true opacity: shouldBeActive ? 1 : 0 active: opacity > 0 diff --git a/modules/sidebar/NotifActionList.qml b/modules/sidebar/NotifActionList.qml index 370a79cbc..d1f1e1f51 100644 --- a/modules/sidebar/NotifActionList.qml +++ b/modules/sidebar/NotifActionList.qml @@ -1,19 +1,19 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets import qs.components import qs.components.containers import qs.components.effects import qs.services import qs.config +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts Item { id: root - required property NotifData notif + required property Notifs.Notif notif Layout.fillWidth: true implicitHeight: flickable.contentHeight @@ -167,7 +167,6 @@ Item { id: iconComp IconImage { - asynchronous: true source: Quickshell.iconPath(action.modelData.identifier) } } diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml index f041cc9be..d039d15d6 100644 --- a/modules/sidebar/NotifDock.qml +++ b/modules/sidebar/NotifDock.qml @@ -1,21 +1,21 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.components.effects import qs.services import qs.config +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts Item { id: root required property Props props - required property DrawerVisibilities visibilities + required property var visibilities readonly property int notifCount: Notifs.list.reduce((acc, n) => n.closed ? acc : acc + 1, 0) anchors.fill: parent @@ -86,7 +86,6 @@ Item { color: "transparent" Loader { - asynchronous: true anchors.centerIn: parent active: opacity > 0 opacity: root.notifCount > 0 ? 0 : 1 @@ -96,7 +95,7 @@ Item { Image { asynchronous: true - source: Quickshell.shellPath("assets/dino.png") + source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`) fillMode: Image.PreserveAspectFit sourceSize.width: clipRect.width * 0.8 @@ -156,19 +155,17 @@ Item { let next = null; for (let i = 0; i < notifList.repeater.count; i++) { next = notifList.repeater.itemAt(i); - if (!next?.closed) // qmllint disable missing-property + if (!next?.closed) break; } - if (next) { - next.closeAll(); // qmllint disable missing-property - } else { + if (next) + next.closeAll(); + else stop(); - } } } Loader { - asynchronous: true anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: Appearance.padding.normal diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index 7ffb64182..b927e91a7 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -1,17 +1,17 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell import qs.components import qs.services import qs.config +import Quickshell +import QtQuick Item { id: root required property Props props required property Flickable container - required property DrawerVisibilities visibilities + required property var visibilities readonly property alias repeater: repeater readonly property int spacing: Appearance.spacing.small @@ -39,130 +39,128 @@ Item { onValuesChanged: root.flagChanged() } - delegate: NotifGroupDelegate {} - } - - component NotifGroupDelegate: MouseArea { - id: notif + MouseArea { + id: notif - required property int index - required property string modelData + required property int index + required property string modelData - readonly property bool closed: notifInner.notifCount === 0 - readonly property alias nonAnimHeight: notifInner.nonAnimHeight - property int startY + readonly property bool closed: notifInner.notifCount === 0 + readonly property alias nonAnimHeight: notifInner.nonAnimHeight + property int startY - function closeAll(): void { - for (const n of Notifs.notClosed.filter(n => n.appName === modelData)) - n.close(); - } + function closeAll(): void { + for (const n of Notifs.notClosed.filter(n => n.appName === modelData)) + n.close(); + } - y: { - root.flag; // Force update - let y = 0; - for (let i = 0; i < index; i++) { - const item = repeater.itemAt(i) as NotifGroupDelegate; - if (item && !item.closed) - y += item.nonAnimHeight + root.spacing; + y: { + root.flag; // Force update + let y = 0; + for (let i = 0; i < index; i++) { + const item = repeater.itemAt(i); + if (!item.closed) + y += item.nonAnimHeight + root.spacing; + } + return y; } - return y; - } - containmentMask: QtObject { - function contains(p: point): bool { - if (!root.container.contains(notif.mapToItem(root.container, p))) - return false; - return notifInner.contains(p); + containmentMask: QtObject { + function contains(p: point): bool { + if (!root.container.contains(notif.mapToItem(root.container, p))) + return false; + return notifInner.contains(p); + } } - } - implicitWidth: root.width - implicitHeight: notifInner.implicitHeight + implicitWidth: root.width + implicitHeight: notifInner.implicitHeight - hoverEnabled: true - cursorShape: pressed ? Qt.ClosedHandCursor : undefined - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - preventStealing: true - enabled: !closed + hoverEnabled: true + cursorShape: pressed ? Qt.ClosedHandCursor : undefined + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + preventStealing: true + enabled: !closed - drag.target: this - drag.axis: Drag.XAxis + drag.target: this + drag.axis: Drag.XAxis - onPressed: event => { - startY = event.y; - if (event.button === Qt.RightButton) - notifInner.toggleExpand(!notifInner.expanded); - else if (event.button === Qt.MiddleButton) - closeAll(); - } - onPositionChanged: event => { - if (pressed) { - const diffY = event.y - startY; - if (Math.abs(diffY) > Config.notifs.expandThreshold) - notifInner.toggleExpand(diffY > 0); + onPressed: event => { + startY = event.y; + if (event.button === Qt.RightButton) + notifInner.toggleExpand(!notifInner.expanded); + else if (event.button === Qt.MiddleButton) + closeAll(); } - } - onReleased: event => { - if (Math.abs(x) < width * Config.notifs.clearThreshold) - x = 0; - else - closeAll(); - } - - ParallelAnimation { - running: true - - Anim { - target: notif - property: "opacity" - from: 0 - to: 1 + onPositionChanged: event => { + if (pressed) { + const diffY = event.y - startY; + if (Math.abs(diffY) > Config.notifs.expandThreshold) + notifInner.toggleExpand(diffY > 0); + } } - Anim { - target: notif - property: "scale" - from: 0 - to: 1 - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + onReleased: event => { + if (Math.abs(x) < width * Config.notifs.clearThreshold) + x = 0; + else + closeAll(); } - } - - ParallelAnimation { - running: notif.closed - Anim { - target: notif - property: "opacity" - to: 0 + ParallelAnimation { + running: true + + Anim { + target: notif + property: "opacity" + from: 0 + to: 1 + } + Anim { + target: notif + property: "scale" + from: 0 + to: 1 + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } } - Anim { - target: notif - property: "scale" - to: 0.6 + + ParallelAnimation { + running: notif.closed + + Anim { + target: notif + property: "opacity" + to: 0 + } + Anim { + target: notif + property: "scale" + to: 0.6 + } } - } - NotifGroup { - id: notifInner + NotifGroup { + id: notifInner - modelData: notif.modelData - props: root.props - container: root.container - visibilities: root.visibilities - } + modelData: notif.modelData + props: root.props + container: root.container + visibilities: root.visibilities + } - Behavior on x { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + Behavior on x { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } } - } - Behavior on y { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + Behavior on y { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } } } } diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index f1d1502f9..2c032aa7d 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Services.Notifications import qs.components import qs.components.effects import qs.services import qs.config import qs.utils +import Quickshell +import Quickshell.Services.Notifications +import QtQuick +import QtQuick.Layouts StyledRect { id: root @@ -16,7 +16,7 @@ StyledRect { required property string modelData required property Props props required property Flickable container - required property DrawerVisibilities visibilities + required property var visibilities readonly property list notifs: Notifs.list.filter(n => n.appName === modelData) readonly property var groupProps: { @@ -136,14 +136,12 @@ StyledRect { radius: Appearance.rounding.full Loader { - asynchronous: true anchors.centerIn: parent sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp } } Loader { - asynchronous: true anchors.right: parent.right anchors.bottom: parent.bottom active: root.appIcon && root.image @@ -204,11 +202,11 @@ StyledRect { radius: Appearance.rounding.full StateLayer { + color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface + function onClicked(): void { root.toggleExpand(!root.expanded); } - - color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface } RowLayout { diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index fe7510b55..e586b5f7a 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell import qs.components import qs.services import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts Item { id: root @@ -14,13 +14,13 @@ Item { required property list notifs required property bool expanded required property Flickable container - required property DrawerVisibilities visibilities + required property var visibilities readonly property real nonAnimHeight: { let h = -root.spacing; for (let i = 0; i < repeater.count; i++) { - const item = repeater.itemAt(i) as NotifDelegate; - if (item && !item.modelData.closed && !item.previewHidden) + const item = repeater.itemAt(i); + if (!item.modelData.closed && !item.previewHidden) h += item.nonAnimHeight + root.spacing; } return h; @@ -59,157 +59,155 @@ Item { onValuesChanged: root.flagChanged() } - delegate: NotifDelegate {} - } + MouseArea { + id: notif - Behavior on implicitHeight { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - component NotifDelegate: MouseArea { - id: notif - - required property int index - required property NotifData modelData + required property int index + required property Notifs.Notif modelData - readonly property alias nonAnimHeight: notifInner.nonAnimHeight - readonly property bool previewHidden: { - if (root.expanded) - return false; + readonly property alias nonAnimHeight: notifInner.nonAnimHeight + readonly property bool previewHidden: { + if (root.expanded) + return false; - let extraHidden = 0; - for (let i = 0; i < index; i++) - if (root.notifs[i].closed) - extraHidden++; + let extraHidden = 0; + for (let i = 0; i < index; i++) + if (root.notifs[i].closed) + extraHidden++; - return index >= Config.notifs.groupPreviewNum + extraHidden; - } - property int startY - - y: { - root.flag; // Force update - let y = 0; - for (let i = 0; i < index; i++) { - const item = repeater.itemAt(i) as NotifDelegate; - if (item && !item.modelData.closed && !item.previewHidden) - y += item.nonAnimHeight + root.spacing; + return index >= Config.notifs.groupPreviewNum + extraHidden; + } + property int startY + + y: { + root.flag; // Force update + let y = 0; + for (let i = 0; i < index; i++) { + const item = repeater.itemAt(i); + if (!item.modelData.closed && !item.previewHidden) + y += item.nonAnimHeight + root.spacing; + } + return y; } - return y; - } - containmentMask: QtObject { - function contains(p: point): bool { - if (!root.container.contains(notif.mapToItem(root.container, p))) - return false; - return notifInner.contains(p); + containmentMask: QtObject { + function contains(p: point): bool { + if (!root.container.contains(notif.mapToItem(root.container, p))) + return false; + return notifInner.contains(p); + } } - } - opacity: previewHidden ? 0 : 1 - scale: previewHidden ? 0.7 : 1 + opacity: previewHidden ? 0 : 1 + scale: previewHidden ? 0.7 : 1 - implicitWidth: root.width - implicitHeight: notifInner.implicitHeight + implicitWidth: root.width + implicitHeight: notifInner.implicitHeight - hoverEnabled: true - cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - preventStealing: !root.expanded - enabled: !modelData.closed + hoverEnabled: true + cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + preventStealing: !root.expanded + enabled: !modelData.closed - drag.target: this - drag.axis: Drag.XAxis + drag.target: this + drag.axis: Drag.XAxis - onPressed: event => { - startY = event.y; - if (event.button === Qt.RightButton) - root.requestToggleExpand(!root.expanded); - else if (event.button === Qt.MiddleButton) - modelData.close(); - } - onPositionChanged: event => { - if (pressed && !root.expanded) { - const diffY = event.y - startY; - if (Math.abs(diffY) > Config.notifs.expandThreshold) - root.requestToggleExpand(diffY > 0); + onPressed: event => { + startY = event.y; + if (event.button === Qt.RightButton) + root.requestToggleExpand(!root.expanded); + else if (event.button === Qt.MiddleButton) + modelData.close(); } - } - onReleased: event => { - if (Math.abs(x) < width * Config.notifs.clearThreshold) - x = 0; - else - modelData.close(); - } - - Component.onCompleted: modelData.lock(this) - Component.onDestruction: modelData.unlock(this) - - ParallelAnimation { - Component.onCompleted: running = !notif.previewHidden - - Anim { - target: notif - property: "opacity" - from: 0 - to: 1 + onPositionChanged: event => { + if (pressed && !root.expanded) { + const diffY = event.y - startY; + if (Math.abs(diffY) > Config.notifs.expandThreshold) + root.requestToggleExpand(diffY > 0); + } } - Anim { - target: notif - property: "scale" - from: 0.7 - to: 1 + onReleased: event => { + if (Math.abs(x) < width * Config.notifs.clearThreshold) + x = 0; + else + modelData.close(); } - } - - ParallelAnimation { - running: notif.modelData.closed - onFinished: notif.modelData.unlock(notif) - Anim { - target: notif - property: "opacity" - to: 0 + Component.onCompleted: modelData.lock(this) + Component.onDestruction: modelData.unlock(this) + + ParallelAnimation { + Component.onCompleted: running = !notif.previewHidden + + Anim { + target: notif + property: "opacity" + from: 0 + to: 1 + } + Anim { + target: notif + property: "scale" + from: 0.7 + to: 1 + } } - Anim { - target: notif - property: "x" - to: notif.x >= 0 ? notif.width : -notif.width + + ParallelAnimation { + running: notif.modelData.closed + onFinished: notif.modelData.unlock(notif) + + Anim { + target: notif + property: "opacity" + to: 0 + } + Anim { + target: notif + property: "x" + to: notif.x >= 0 ? notif.width : -notif.width + } } - } - Notif { - id: notifInner + Notif { + id: notifInner - anchors.fill: parent - modelData: notif.modelData - props: root.props - expanded: root.expanded - visibilities: root.visibilities - } + anchors.fill: parent + modelData: notif.modelData + props: root.props + expanded: root.expanded + visibilities: root.visibilities + } - Behavior on opacity { - Anim {} - } + Behavior on opacity { + Anim {} + } - Behavior on scale { - Anim {} - } + Behavior on scale { + Anim {} + } - Behavior on x { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + Behavior on x { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } } - } - Behavior on y { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + Behavior on y { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } } } } + + Behavior on implicitHeight { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } } diff --git a/modules/sidebar/Wrapper.qml b/modules/sidebar/Wrapper.qml index ad2564131..9303c6b94 100644 --- a/modules/sidebar/Wrapper.qml +++ b/modules/sidebar/Wrapper.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound -import QtQuick import qs.components import qs.config +import QtQuick Item { id: root - required property DrawerVisibilities visibilities + required property var visibilities required property var panels readonly property Props props: Props {} diff --git a/modules/utilities/Background.qml b/modules/utilities/Background.qml index 975461a58..fbce89616 100644 --- a/modules/utilities/Background.qml +++ b/modules/utilities/Background.qml @@ -1,8 +1,8 @@ -import QtQuick -import QtQuick.Shapes import qs.components import qs.services import qs.config +import QtQuick +import QtQuick.Shapes ShapePath { id: root diff --git a/modules/utilities/Content.qml b/modules/utilities/Content.qml index 6dcfedcdf..902656de5 100644 --- a/modules/utilities/Content.qml +++ b/modules/utilities/Content.qml @@ -1,16 +1,14 @@ import "cards" +import qs.config import QtQuick import QtQuick.Layouts -import qs.components -import qs.config -import qs.modules.bar.popouts as BarPopouts Item { id: root required property var props - required property DrawerVisibilities visibilities - required property BarPopouts.Wrapper popouts + required property var visibilities + required property Item popouts implicitWidth: layout.implicitWidth implicitHeight: layout.implicitHeight diff --git a/modules/utilities/RecordingDeleteModal.qml b/modules/utilities/RecordingDeleteModal.qml index 4d6d8af2d..127afe93b 100644 --- a/modules/utilities/RecordingDeleteModal.qml +++ b/modules/utilities/RecordingDeleteModal.qml @@ -1,21 +1,20 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import QtQuick.Shapes -import Caelestia import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config +import Caelestia +import QtQuick +import QtQuick.Layouts +import QtQuick.Shapes Loader { id: root required property var props - asynchronous: true anchors.fill: parent opacity: root.props.recordingConfirmDelete ? 1 : 0 diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index 66a616f07..77178e36e 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -1,17 +1,16 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell import qs.components import qs.config -import qs.modules.bar.popouts as BarPopouts +import Quickshell +import QtQuick Item { id: root - required property DrawerVisibilities visibilities + required property var visibilities required property Item sidebar - required property BarPopouts.Wrapper popouts + required property Item popouts readonly property PersistentProperties props: PersistentProperties { property bool recordingListExpanded: false @@ -80,7 +79,6 @@ Item { Loader { id: content - asynchronous: true anchors.top: parent.top anchors.left: parent.left anchors.margins: Appearance.padding.large diff --git a/modules/utilities/cards/IdleInhibit.qml b/modules/utilities/cards/IdleInhibit.qml index 20d232a37..0344e3ad2 100644 --- a/modules/utilities/cards/IdleInhibit.qml +++ b/modules/utilities/cards/IdleInhibit.qml @@ -1,9 +1,9 @@ -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.controls import qs.services import qs.config +import QtQuick +import QtQuick.Layouts StyledRect { id: root @@ -70,7 +70,6 @@ StyledRect { Loader { id: activeChip - asynchronous: true anchors.bottom: parent.bottom anchors.left: parent.left anchors.topMargin: Appearance.spacing.larger diff --git a/modules/utilities/cards/Record.qml b/modules/utilities/cards/Record.qml index 9dd16ec51..273c64002 100644 --- a/modules/utilities/cards/Record.qml +++ b/modules/utilities/cards/Record.qml @@ -1,17 +1,17 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts import qs.components import qs.components.controls import qs.services import qs.config +import QtQuick +import QtQuick.Layouts StyledRect { id: root required property var props - required property DrawerVisibilities visibilities + required property var visibilities Layout.fillWidth: true implicitHeight: layout.implicitHeight + layout.anchors.margins * 2 @@ -111,7 +111,6 @@ StyledRect { property bool running: Recorder.running - asynchronous: true Layout.fillWidth: true Layout.preferredHeight: implicitHeight sourceComponent: running ? recordingControls : recordingList diff --git a/modules/utilities/cards/RecordingList.qml b/modules/utilities/cards/RecordingList.qml index 7fb56f3ba..b9d757a4d 100644 --- a/modules/utilities/cards/RecordingList.qml +++ b/modules/utilities/cards/RecordingList.qml @@ -1,22 +1,23 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets -import Caelestia.Models import qs.components -import qs.components.containers import qs.components.controls +import qs.components.containers import qs.services import qs.config import qs.utils +import Caelestia +import Caelestia.Models +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root required property var props - required property DrawerVisibilities visibilities + required property var visibilities spacing: 0 @@ -162,7 +163,6 @@ ColumnLayout { } Loader { - asynchronous: true anchors.centerIn: parent opacity: list.count === 0 ? 1 : 0 diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index 996b027c3..d610586bf 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -1,19 +1,18 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import Quickshell.Bluetooth import qs.components import qs.components.controls import qs.services import qs.config -import qs.modules.bar.popouts as BarPopouts +import qs.modules.controlcenter +import Quickshell +import Quickshell.Bluetooth +import QtQuick +import QtQuick.Layouts StyledRect { id: root - required property DrawerVisibilities visibilities - required property BarPopouts.Wrapper popouts + required property var visibilities + required property Item popouts readonly property var quickToggles: { const seenIds = new Set(); @@ -21,13 +20,15 @@ StyledRect { return Config.utilities.quickToggles.filter(item => { if (!item.enabled) return false; - + if (seenIds.has(item.id)) { return false; } if (item.id === "vpn") { - return Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false); + return Config.utilities.vpn.provider.some(p => + typeof p === "object" ? (p.enabled === true) : false + ); } seenIds.add(item.id); @@ -55,17 +56,17 @@ StyledRect { font.pointSize: Appearance.font.size.normal } - QuickToggleRow { + ToggleRow { rowModel: root.needExtraRow ? root.quickToggles.slice(0, root.splitIndex) : root.quickToggles } - QuickToggleRow { + ToggleRow { visible: root.needExtraRow rowModel: root.needExtraRow ? root.quickToggles.slice(root.splitIndex) : [] } } - component QuickToggleRow: RowLayout { + component ToggleRow: RowLayout { property var rowModel: [] Layout.fillWidth: true @@ -89,9 +90,9 @@ StyledRect { roleValue: "bluetooth" delegate: Toggle { icon: "bluetooth" - checked: Bluetooth.defaultAdapter?.enabled ?? false // qmllint disable unresolved-type + checked: Bluetooth.defaultAdapter?.enabled ?? false onClicked: { - const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type + const adapter = Bluetooth.defaultAdapter; if (adapter) adapter.enabled = !adapter.enabled; } diff --git a/modules/utilities/toasts/ToastItem.qml b/modules/utilities/toasts/ToastItem.qml index 52473a48e..f47550006 100644 --- a/modules/utilities/toasts/ToastItem.qml +++ b/modules/utilities/toasts/ToastItem.qml @@ -1,10 +1,10 @@ -import QtQuick -import QtQuick.Layouts -import Caelestia import qs.components import qs.components.effects import qs.services import qs.config +import Caelestia +import QtQuick +import QtQuick.Layouts StyledRect { id: root diff --git a/modules/utilities/toasts/Toasts.qml b/modules/utilities/toasts/Toasts.qml index ac8772f57..2915404e0 100644 --- a/modules/utilities/toasts/Toasts.qml +++ b/modules/utilities/toasts/Toasts.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound -import QtQuick -import Quickshell -import Caelestia import qs.components import qs.config +import Caelestia +import Quickshell +import QtQuick Item { id: root diff --git a/modules/windowinfo/Buttons.qml b/modules/windowinfo/Buttons.qml index 15f71956c..89acfe6d6 100644 --- a/modules/windowinfo/Buttons.qml +++ b/modules/windowinfo/Buttons.qml @@ -1,11 +1,9 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import Quickshell.Widgets import qs.components import qs.services import qs.config +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root @@ -37,11 +35,11 @@ ColumnLayout { implicitHeight: moveToWsIcon.implicitHeight + Appearance.padding.small StateLayer { + color: Colours.palette.m3onPrimary + function onClicked(): void { root.moveToWsExpanded = !root.moveToWsExpanded; } - - color: Colours.palette.m3onPrimary } MaterialIcon { @@ -83,14 +81,14 @@ ColumnLayout { readonly property int wsId: Math.floor((Hypr.activeWsId - 1) / 10) * 10 + index + 1 readonly property bool isCurrent: root.client?.workspace.id === wsId - function onClicked(): void { - Hypr.dispatch(`movetoworkspace ${wsId},address:0x${root.client?.address}`); - } - color: isCurrent ? Colours.tPalette.m3surfaceContainerHighest : Colours.palette.m3tertiaryContainer onColor: isCurrent ? Colours.palette.m3onSurface : Colours.palette.m3onTertiaryContainer text: wsId disabled: isCurrent + + function onClicked(): void { + Hypr.dispatch(`movetoworkspace ${wsId},address:0x${root.client?.address}`); + } } } } @@ -109,41 +107,40 @@ ColumnLayout { spacing: root.client?.lastIpcObject.floating ? Appearance.spacing.normal : Appearance.spacing.small Button { - function onClicked(): void { - Hypr.dispatch(`togglefloating address:0x${root.client?.address}`); - } - color: Colours.palette.m3secondaryContainer onColor: Colours.palette.m3onSecondaryContainer text: root.client?.lastIpcObject.floating ? qsTr("Tile") : qsTr("Float") + + function onClicked(): void { + Hypr.dispatch(`togglefloating address:0x${root.client?.address}`); + } } Loader { - asynchronous: true active: root.client?.lastIpcObject.floating Layout.fillWidth: active Layout.leftMargin: active ? 0 : -parent.spacing Layout.rightMargin: active ? 0 : -parent.spacing sourceComponent: Button { - function onClicked(): void { - Hypr.dispatch(`pin address:0x${root.client?.address}`); - } - color: Colours.palette.m3secondaryContainer onColor: Colours.palette.m3onSecondaryContainer text: root.client?.lastIpcObject.pinned ? qsTr("Unpin") : qsTr("Pin") + + function onClicked(): void { + Hypr.dispatch(`pin address:0x${root.client?.address}`); + } } } Button { - function onClicked(): void { - Hypr.dispatch(`killwindow address:0x${root.client?.address}`); - } - color: Colours.palette.m3errorContainer onColor: Colours.palette.m3onErrorContainer text: qsTr("Kill") + + function onClicked(): void { + Hypr.dispatch(`killwindow address:0x${root.client?.address}`); + } } } @@ -163,11 +160,11 @@ ColumnLayout { StateLayer { id: stateLayer + color: parent.onColor + function onClicked(): void { parent.onClicked(); } - - color: parent.onColor } StyledText { diff --git a/modules/windowinfo/Details.qml b/modules/windowinfo/Details.qml index 3934a993d..f9ee66a68 100644 --- a/modules/windowinfo/Details.qml +++ b/modules/windowinfo/Details.qml @@ -1,9 +1,9 @@ -import QtQuick -import QtQuick.Layouts -import Quickshell.Hyprland import qs.components import qs.services import qs.config +import Quickshell.Hyprland +import QtQuick +import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/windowinfo/Preview.qml b/modules/windowinfo/Preview.qml index 2683e5447..4cc0aab86 100644 --- a/modules/windowinfo/Preview.qml +++ b/modules/windowinfo/Preview.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Hyprland -import Quickshell.Wayland import qs.components import qs.services import qs.config +import Quickshell +import Quickshell.Wayland +import Quickshell.Hyprland +import QtQuick +import QtQuick.Layouts Item { id: root @@ -33,7 +33,6 @@ Item { radius: Appearance.rounding.small Loader { - asynchronous: true anchors.centerIn: parent active: !root.client @@ -69,7 +68,7 @@ Item { anchors.centerIn: parent - captureSource: root.client?.wayland ?? null // qmllint disable unresolved-type + captureSource: root.client?.wayland ?? null live: true constraintSize.width: root.client ? parent.height * Math.min(root.screen.width / root.screen.height, root.client?.lastIpcObject.size[0] / root.client?.lastIpcObject.size[1]) : parent.height diff --git a/modules/windowinfo/WindowInfo.qml b/modules/windowinfo/WindowInfo.qml index d7cfc2fa1..919b3fbb1 100644 --- a/modules/windowinfo/WindowInfo.qml +++ b/modules/windowinfo/WindowInfo.qml @@ -1,10 +1,10 @@ -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Hyprland import qs.components import qs.services import qs.config +import Quickshell +import Quickshell.Hyprland +import QtQuick +import QtQuick.Layouts Item { id: root diff --git a/plugin/src/Caelestia/Internal/cachingimagemanager.cpp b/plugin/src/Caelestia/Internal/cachingimagemanager.cpp index 9f63f99a2..1c15cd203 100644 --- a/plugin/src/Caelestia/Internal/cachingimagemanager.cpp +++ b/plugin/src/Caelestia/Internal/cachingimagemanager.cpp @@ -105,26 +105,34 @@ void CachingImageManager::updateSource(const QString& path) { m_shaPath = path; - QtConcurrent::run(&CachingImageManager::sha256sum, path).then(this, [path, this](const QString& sha) { + const auto future = QtConcurrent::run(&CachingImageManager::sha256sum, path); + + const auto watcher = new QFutureWatcher(this); + + connect(watcher, &QFutureWatcher::finished, this, [watcher, path, this]() { if (m_path != path) { + // Object is destroyed or path has changed, ignore + watcher->deleteLater(); return; } const QSize size = effectiveSize(); if (!m_item || !size.width() || !size.height()) { + watcher->deleteLater(); return; } const QString fillMode = m_item->property("fillMode").toString(); // clang-format off const QString filename = QString("%1@%2x%3-%4.png") - .arg(sha).arg(size.width()).arg(size.height()) + .arg(watcher->result()).arg(size.width()).arg(size.height()) .arg(fillMode == "PreserveAspectCrop" ? "crop" : fillMode == "PreserveAspectFit" ? "fit" : "stretch"); // clang-format on const QUrl cache = m_cacheDir.resolved(QUrl(filename)); if (m_cachePath == cache) { + watcher->deleteLater(); return; } @@ -133,6 +141,7 @@ void CachingImageManager::updateSource(const QString& path) { if (!cache.isLocalFile()) { qWarning() << "CachingImageManager::updateSource: cachePath" << cache << "is not a local file"; + watcher->deleteLater(); return; } @@ -148,7 +157,11 @@ void CachingImageManager::updateSource(const QString& path) { if (m_shaPath == path) { m_shaPath = QString(); } + + watcher->deleteLater(); }); + + watcher->setFuture(future); } QUrl CachingImageManager::cachePath() const { diff --git a/plugin/src/Caelestia/Internal/cachingimagemanager.hpp b/plugin/src/Caelestia/Internal/cachingimagemanager.hpp index 1b707414d..3611699b6 100644 --- a/plugin/src/Caelestia/Internal/cachingimagemanager.hpp +++ b/plugin/src/Caelestia/Internal/cachingimagemanager.hpp @@ -2,7 +2,6 @@ #include #include -#include #include namespace caelestia::internal { @@ -19,7 +18,8 @@ class CachingImageManager : public QObject { public: explicit CachingImageManager(QObject* parent = nullptr) - : QObject(parent) {} + : QObject(parent) + , m_item(nullptr) {} [[nodiscard]] QQuickItem* item() const; void setItem(QQuickItem* item); @@ -46,7 +46,7 @@ class CachingImageManager : public QObject { private: QString m_shaPath; - QPointer m_item; + QQuickItem* m_item; QUrl m_cacheDir; QString m_path; diff --git a/plugin/src/Caelestia/Internal/sparklineitem.cpp b/plugin/src/Caelestia/Internal/sparklineitem.cpp index 5ffcc2f84..b4938d1d2 100644 --- a/plugin/src/Caelestia/Internal/sparklineitem.cpp +++ b/plugin/src/Caelestia/Internal/sparklineitem.cpp @@ -27,9 +27,6 @@ void SparklineItem::paint(QPainter* painter) { } void SparklineItem::drawLine(QPainter* painter, CircularBuffer* buffer, const QColor& color, qreal fillAlpha) { - if (m_historyLength < 2) - return; - const qreal w = width(); const qreal h = height(); const int len = buffer->count(); diff --git a/plugin/src/Caelestia/Models/filesystemmodel.cpp b/plugin/src/Caelestia/Models/filesystemmodel.cpp index 267a43946..4eb94cd49 100644 --- a/plugin/src/Caelestia/Models/filesystemmodel.cpp +++ b/plugin/src/Caelestia/Models/filesystemmodel.cpp @@ -219,7 +219,7 @@ void FileSystemModel::watchDirIfRecursive(const QString& path) { if (m_recursive && m_watchChanges) { const auto currentDir = m_dir; const bool showHidden = m_showHidden; - auto future = QtConcurrent::run([showHidden, path]() { + const auto future = QtConcurrent::run([showHidden, path]() { QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot; if (showHidden) { filters |= QDir::Hidden; @@ -232,12 +232,16 @@ void FileSystemModel::watchDirIfRecursive(const QString& path) { } return dirs; }); - future.then(this, [currentDir, showHidden, this](const QStringList& paths) { + const auto watcher = new QFutureWatcher(this); + connect(watcher, &QFutureWatcher::finished, this, [currentDir, showHidden, watcher, this]() { + const auto paths = watcher->result(); if (currentDir == m_dir && showHidden == m_showHidden && !paths.isEmpty()) { // Ignore if dir or showHidden has changed m_watcher.addPaths(paths); } + watcher->deleteLater(); }); + watcher->setFuture(future); } } @@ -291,7 +295,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) { oldPaths << entry->path(); } - auto future = QtConcurrent::run([=](QPromise, QSet>>& promise) { + const auto future = QtConcurrent::run([=](QPromise, QSet>>& promise) { const auto flags = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags; std::optional iter; @@ -349,7 +353,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) { newPaths.insert(path); } - if (promise.isCanceled()) { + if (promise.isCanceled() || newPaths == oldPaths) { return; } @@ -361,17 +365,23 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) { } m_futures.insert(dir, future); - future - .then(this, - [dir, this](QPair, QSet> result) { - m_futures.remove(dir); - if (!result.first.isEmpty() || !result.second.isEmpty()) { - applyChanges(result.first, result.second); - } - }) - .onCanceled(this, [dir, this]() { - m_futures.remove(dir); - }); + const auto watcher = new QFutureWatcher, QSet>>(this); + + connect(watcher, &QFutureWatcher, QSet>>::finished, this, [dir, watcher, this]() { + m_futures.remove(dir); + + if (!watcher->future().isResultReadyAt(0)) { + watcher->deleteLater(); + return; + } + + const auto result = watcher->result(); + applyChanges(result.first, result.second); + + watcher->deleteLater(); + }); + + watcher->setFuture(future); } void FileSystemModel::applyChanges(const QSet& removedPaths, const QSet& addedPaths) { diff --git a/plugin/src/Caelestia/Models/filesystemmodel.hpp b/plugin/src/Caelestia/Models/filesystemmodel.hpp index c3315858a..cf8eae822 100644 --- a/plugin/src/Caelestia/Models/filesystemmodel.hpp +++ b/plugin/src/Caelestia/Models/filesystemmodel.hpp @@ -132,7 +132,7 @@ class FileSystemModel : public QAbstractListModel { bool m_recursive; bool m_watchChanges; bool m_showHidden; - bool m_sortReverse = false; + bool m_sortReverse; Filter m_filter; QStringList m_nameFilters; diff --git a/plugin/src/Caelestia/Services/audiocollector.cpp b/plugin/src/Caelestia/Services/audiocollector.cpp index fb051ccbd..15634059e 100644 --- a/plugin/src/Caelestia/Services/audiocollector.cpp +++ b/plugin/src/Caelestia/Services/audiocollector.cpp @@ -221,7 +221,7 @@ AudioCollector::AudioCollector(QObject* parent) , m_writeBuffer(&m_buffer2) {} AudioCollector::~AudioCollector() { - AudioCollector::stop(); + stop(); } void AudioCollector::start() { diff --git a/plugin/src/Caelestia/Services/audioprovider.hpp b/plugin/src/Caelestia/Services/audioprovider.hpp index 4b85929f4..5bf9bb00d 100644 --- a/plugin/src/Caelestia/Services/audioprovider.hpp +++ b/plugin/src/Caelestia/Services/audioprovider.hpp @@ -23,7 +23,7 @@ public slots: virtual void process() = 0; private: - QTimer* m_timer = nullptr; + QTimer* m_timer; }; class AudioProvider : public Service { diff --git a/plugin/src/Caelestia/Services/beattracker.cpp b/plugin/src/Caelestia/Services/beattracker.cpp index 649705799..93addc679 100644 --- a/plugin/src/Caelestia/Services/beattracker.cpp +++ b/plugin/src/Caelestia/Services/beattracker.cpp @@ -19,9 +19,7 @@ BeatProcessor::~BeatProcessor() { if (m_in) { del_fvec(m_in); } - if (m_out) { - del_fvec(m_out); - } + del_fvec(m_out); } void BeatProcessor::process() { diff --git a/plugin/src/Caelestia/appdb.cpp b/plugin/src/Caelestia/appdb.cpp index 1e33990da..6952c0e08 100644 --- a/plugin/src/Caelestia/appdb.cpp +++ b/plugin/src/Caelestia/appdb.cpp @@ -11,7 +11,7 @@ AppEntry::AppEntry(QObject* entry, unsigned int frequency, QObject* parent) , m_entry(entry) , m_frequency(frequency) { const auto mo = m_entry->metaObject(); - const auto tmo = &AppEntry::staticMetaObject; + const auto tmo = metaObject(); for (const auto& prop : { "name", "comment", "execString", "startupClass", "genericName", "categories", "keywords" }) { @@ -303,13 +303,11 @@ void AppDb::updateApps() { newIds.insert(entry->property("id").toString()); } - for (auto it = m_apps.begin(); it != m_apps.end();) { - if (!newIds.contains(it.key())) { + for (auto it = m_apps.keyBegin(); it != m_apps.keyEnd(); ++it) { + const auto& id = *it; + if (!newIds.contains(id)) { dirty = true; - it.value()->deleteLater(); - it = m_apps.erase(it); - } else { - ++it; + m_apps.take(id)->deleteLater(); } } diff --git a/plugin/src/Caelestia/cutils.cpp b/plugin/src/Caelestia/cutils.cpp index b6ec33a88..6e3bfa99c 100644 --- a/plugin/src/Caelestia/cutils.cpp +++ b/plugin/src/Caelestia/cutils.cpp @@ -75,20 +75,13 @@ void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, Q QObject::connect(watcher, &QFutureWatcher::finished, this, [=]() { if (watcher->result()) { if (onSaved.isCallable()) { - QJSValueList args = { QJSValue(path.toLocalFile()) }; - if (engine) { - args << engine->toScriptValue(QVariant::fromValue(path)); - } - onSaved.call(args); + onSaved.call( + { QJSValue(path.toLocalFile()), engine->toScriptValue(QVariant::fromValue(path)) }); } } else { qWarning() << "CUtils::saveItem: failed to save" << path; if (onFailed.isCallable()) { - if (engine) { - onFailed.call({ engine->toScriptValue(QVariant::fromValue(path)) }); - } else { - onFailed.call(); - } + onFailed.call({ engine->toScriptValue(QVariant::fromValue(path)) }); } } watcher->deleteLater(); diff --git a/plugin/src/Caelestia/imageanalyser.cpp b/plugin/src/Caelestia/imageanalyser.cpp index 0b623162b..880b0785c 100644 --- a/plugin/src/Caelestia/imageanalyser.cpp +++ b/plugin/src/Caelestia/imageanalyser.cpp @@ -134,11 +134,6 @@ void ImageAnalyser::update() { if (m_sourceItem) { const QSharedPointer grabResult = m_sourceItem->grabToImage(); - if (!grabResult) { - QObject::connect(m_sourceItem, &QQuickItem::windowChanged, this, &ImageAnalyser::requestUpdate, - Qt::SingleShotConnection); - return; - } QObject::connect(grabResult.data(), &QQuickItemGrabResult::ready, this, [grabResult, this]() { m_futureWatcher->setFuture(QtConcurrent::run(&ImageAnalyser::analyse, grabResult->image(), m_rescaleSize)); }); @@ -196,14 +191,14 @@ void ImageAnalyser::analyse(QPromise& promise, const QImage& imag continue; } - const quint32 mr = static_cast(pixel[2] & 0xF8); + const quint32 mr = static_cast(pixel[0] & 0xF8); const quint32 mg = static_cast(pixel[1] & 0xF8); - const quint32 mb = static_cast(pixel[0] & 0xF8); + const quint32 mb = static_cast(pixel[2] & 0xF8); ++colours[(mr << 16) | (mg << 8) | mb]; - const qreal r = pixel[2] / 255.0; + const qreal r = pixel[0] / 255.0; const qreal g = pixel[1] / 255.0; - const qreal b = pixel[0] / 255.0; + const qreal b = pixel[2] / 255.0; totalLuminance += std::sqrt(0.299 * r * r + 0.587 * g * g + 0.114 * b * b); ++count; } diff --git a/plugin/src/Caelestia/imageanalyser.hpp b/plugin/src/Caelestia/imageanalyser.hpp index 63fbf9691..bbea2b32e 100644 --- a/plugin/src/Caelestia/imageanalyser.hpp +++ b/plugin/src/Caelestia/imageanalyser.hpp @@ -4,7 +4,6 @@ #include #include #include -#include #include namespace caelestia { @@ -49,7 +48,7 @@ class ImageAnalyser : public QObject { QFutureWatcher* const m_futureWatcher; QString m_source; - QPointer m_sourceItem; + QQuickItem* m_sourceItem; int m_rescaleSize; QColor m_dominantColour; diff --git a/plugin/src/Caelestia/qalculator.cpp b/plugin/src/Caelestia/qalculator.cpp index c72421793..44e8d21e2 100644 --- a/plugin/src/Caelestia/qalculator.cpp +++ b/plugin/src/Caelestia/qalculator.cpp @@ -1,19 +1,13 @@ #include "qalculator.hpp" #include -#include namespace caelestia { -QMutex Qalculator::s_calculatorMutex; - Qalculator::Qalculator(QObject* parent) : QObject(parent) { if (!CALCULATOR) { - // Calculator constructor sets the global `calculator` pointer (CALCULATOR macro), - // but we need to assign it to a var so compiler doesn't flag it as a leak - static const auto* const instance = new Calculator(); - Q_UNUSED(instance) + new Calculator(); CALCULATOR->loadExchangeRates(); CALCULATOR->loadGlobalDefinitions(); CALCULATOR->loadLocalDefinitions(); @@ -25,8 +19,6 @@ QString Qalculator::eval(const QString& expr, bool printExpr) const { return QString(); } - QMutexLocker locker(&s_calculatorMutex); - EvaluationOptions eo; PrintOptions po; @@ -57,92 +49,4 @@ QString Qalculator::eval(const QString& expr, bool printExpr) const { return QString::fromStdString(result); } -void Qalculator::evalAsync(const QString& expr) { - const quint64 gen = ++m_generation; - - if (expr.isEmpty()) { - if (!m_result.isEmpty()) { - m_result.clear(); - emit resultChanged(); - } - if (!m_rawResult.isEmpty()) { - m_rawResult.clear(); - emit rawResultChanged(); - } - if (m_busy) { - m_busy = false; - emit busyChanged(); - } - return; - } - - if (!m_busy) { - m_busy = true; - emit busyChanged(); - } - - QtConcurrent::run([expr]() -> QPair { - QMutexLocker locker(&s_calculatorMutex); - - EvaluationOptions eo; - PrintOptions po; - - std::string parsed; - std::string result = CALCULATOR->calculateAndPrint( - CALCULATOR->unlocalizeExpression(expr.toStdString(), eo.parse_options), 100, eo, po, &parsed); - - std::string error; - while (CALCULATOR->message()) { - if (!CALCULATOR->message()->message().empty()) { - if (CALCULATOR->message()->type() == MESSAGE_ERROR) { - error += "error: "; - } else if (CALCULATOR->message()->type() == MESSAGE_WARNING) { - error += "warning: "; - } - error += CALCULATOR->message()->message(); - } - CALCULATOR->nextMessage(); - } - - if (!error.empty()) { - const QString errorStr = QString::fromStdString(error); - return { errorStr, errorStr }; - } - - const QString rawStr = QString::fromStdString(result); - return { QString("%1 = %2").arg(parsed).arg(result), rawStr }; - }).then(this, [this, gen](QPair result) { - if (gen != m_generation) { - return; - } - - const auto& [formatted, raw] = result; - - if (m_result != formatted) { - m_result = formatted; - emit resultChanged(); - } - if (m_rawResult != raw) { - m_rawResult = raw; - emit rawResultChanged(); - } - if (m_busy) { - m_busy = false; - emit busyChanged(); - } - }); -} - -QString Qalculator::result() const { - return m_result; -} - -QString Qalculator::rawResult() const { - return m_rawResult; -} - -bool Qalculator::busy() const { - return m_busy; -} - } // namespace caelestia diff --git a/plugin/src/Caelestia/qalculator.hpp b/plugin/src/Caelestia/qalculator.hpp index b2f5517f7..a07a8a2fc 100644 --- a/plugin/src/Caelestia/qalculator.hpp +++ b/plugin/src/Caelestia/qalculator.hpp @@ -1,6 +1,5 @@ #pragma once -#include #include #include @@ -11,32 +10,10 @@ class Qalculator : public QObject { QML_ELEMENT QML_SINGLETON - Q_PROPERTY(QString result READ result NOTIFY resultChanged) - Q_PROPERTY(QString rawResult READ rawResult NOTIFY rawResultChanged) - Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) - public: explicit Qalculator(QObject* parent = nullptr); Q_INVOKABLE QString eval(const QString& expr, bool printExpr = true) const; - Q_INVOKABLE void evalAsync(const QString& expr); - - [[nodiscard]] QString result() const; - [[nodiscard]] QString rawResult() const; - [[nodiscard]] bool busy() const; - -signals: - void resultChanged(); - void rawResultChanged(); - void busyChanged(); - -private: - static QMutex s_calculatorMutex; - - QString m_result; - QString m_rawResult; - bool m_busy = false; - quint64 m_generation = 0; }; } // namespace caelestia diff --git a/plugin/src/Caelestia/requests.cpp b/plugin/src/Caelestia/requests.cpp index dbc746ed1..2ceddb351 100644 --- a/plugin/src/Caelestia/requests.cpp +++ b/plugin/src/Caelestia/requests.cpp @@ -1,8 +1,6 @@ #include "requests.hpp" -#include #include -#include #include #include @@ -12,27 +10,13 @@ Requests::Requests(QObject* parent) : QObject(parent) , m_manager(new QNetworkAccessManager(this)) {} -void Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError, QJSValue headers) const { +void Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError) const { if (!onSuccess.isCallable()) { qWarning() << "Requests::get: onSuccess is not callable"; return; } QNetworkRequest request(url); - request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); - request.setAttribute(QNetworkRequest::CookieSaveControlAttribute, QNetworkRequest::Manual); - request.setRawHeader("Cache-Control", "no-cache, no-store"); - request.setRawHeader("Pragma", "no-cache"); - request.setRawHeader("Connection", "close"); - - if (headers.isObject()) { - QJSValueIterator it(headers); - while (it.hasNext()) { - it.next(); - request.setRawHeader(it.name().toUtf8(), it.value().toString().toUtf8()); - } - } - auto reply = m_manager->get(request); QObject::connect(reply, &QNetworkReply::finished, [reply, onSuccess, onError]() { @@ -48,8 +32,4 @@ void Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError, QJSVal }); } -void Requests::resetCookies() const { - m_manager->setCookieJar(new QNetworkCookieJar(m_manager)); -} - } // namespace caelestia diff --git a/plugin/src/Caelestia/requests.hpp b/plugin/src/Caelestia/requests.hpp index d07d7e8f0..1db2f4cf6 100644 --- a/plugin/src/Caelestia/requests.hpp +++ b/plugin/src/Caelestia/requests.hpp @@ -14,9 +14,7 @@ class Requests : public QObject { public: explicit Requests(QObject* parent = nullptr); - Q_INVOKABLE void get( - const QUrl& url, QJSValue callback, QJSValue onError = QJSValue(), QJSValue headers = QJSValue()) const; - Q_INVOKABLE void resetCookies() const; + Q_INVOKABLE void get(const QUrl& url, QJSValue callback, QJSValue onError = QJSValue()) const; private: QNetworkAccessManager* m_manager; diff --git a/services/Audio.qml b/services/Audio.qml index 100f00101..14d0a4e81 100644 --- a/services/Audio.qml +++ b/services/Audio.qml @@ -1,11 +1,11 @@ pragma Singleton -import QtQuick +import qs.config +import Caelestia.Services +import Caelestia import Quickshell import Quickshell.Services.Pipewire -import Caelestia -import Caelestia.Services -import qs.config +import QtQuick Singleton { id: root @@ -92,7 +92,7 @@ Singleton { if (!stream) return qsTr("Unknown"); // Try application name first, then description, then name - return stream.properties["application.name"] || stream.description || stream.name || qsTr("Unknown Application"); + return stream.applicationName || stream.description || stream.name || qsTr("Unknown Application"); } onSinkChanged: { @@ -125,6 +125,8 @@ Singleton { } Connections { + target: Pipewire.nodes + function onValuesChanged(): void { const newSinks = []; const newSources = []; @@ -145,8 +147,6 @@ Singleton { root.sources = newSources; root.streams = newStreams; } - - target: Pipewire.nodes } PwObjectTracker { diff --git a/services/Brightness.qml b/services/Brightness.qml index 6e828d918..567824042 100644 --- a/services/Brightness.qml +++ b/services/Brightness.qml @@ -1,11 +1,11 @@ pragma Singleton pragma ComponentBehavior: Bound -import QtQuick +import qs.config +import qs.components.misc import Quickshell import Quickshell.Io -import qs.components.misc -import qs.config +import QtQuick Singleton { id: root @@ -17,34 +17,34 @@ Singleton { map[m.connector] = m; return map; } - readonly property list monitors: variants.instances // qmllint disable incompatible-type + readonly property list monitors: variants.instances property bool appleDisplayPresent: false function getMonitorForScreen(screen: ShellScreen): var { - return monitors.find(m => m.modelData === screen); // qmllint disable missing-property + return monitors.find(m => m.modelData === screen); } function getMonitor(query: string): var { if (query === "active") { - return monitors.find(m => Hypr.monitorFor(m.modelData)?.focused); // qmllint disable missing-property + return monitors.find(m => Hypr.monitorFor(m.modelData)?.focused); } if (query.startsWith("model:")) { const model = query.slice(6); - return monitors.find(m => m.modelData.model === model); // qmllint disable missing-property + return monitors.find(m => m.modelData.model === model); } if (query.startsWith("serial:")) { const serial = query.slice(7); - return monitors.find(m => m.modelData.serialNumber === serial); // qmllint disable missing-property + return monitors.find(m => m.modelData.serialNumber === serial); } if (query.startsWith("id:")) { const id = parseInt(query.slice(3), 10); - return monitors.find(m => Hypr.monitorFor(m.modelData)?.id === id); // qmllint disable missing-property + return monitors.find(m => Hypr.monitorFor(m.modelData)?.id === id); } - return monitors.find(m => m.modelData.name === query); // qmllint disable missing-property + return monitors.find(m => m.modelData.name === query); } function increaseBrightness(): void { @@ -92,23 +92,21 @@ Singleton { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "brightnessUp" description: "Increase brightness" onPressed: root.increaseBrightness() } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "brightnessDown" description: "Decrease brightness" onPressed: root.decreaseBrightness() } IpcHandler { + target: "brightness" + function get(): real { return getFor("active"); } @@ -157,8 +155,6 @@ Singleton { return `Set monitor ${monitor.modelData.name} brightness to ${+monitor.brightness.toFixed(2)}`; } - - target: "brightness" } component Monitor: QtObject { diff --git a/services/Colours.qml b/services/Colours.qml index 469118167..cd86c8fbf 100644 --- a/services/Colours.qml +++ b/services/Colours.qml @@ -1,13 +1,12 @@ pragma Singleton pragma ComponentBehavior: Bound -import QtQuick -import Quickshell -import Quickshell.Io -import Caelestia -import qs.services import qs.config import qs.utils +import Caelestia +import Quickshell +import Quickshell.Io +import QtQuick Singleton { id: root diff --git a/services/GameMode.qml b/services/GameMode.qml index 3a3fb7bda..83770b79f 100644 --- a/services/GameMode.qml +++ b/services/GameMode.qml @@ -1,11 +1,11 @@ pragma Singleton -import QtQuick -import Quickshell -import Quickshell.Io -import Caelestia import qs.services import qs.config +import Caelestia +import Quickshell +import Quickshell.Io +import QtQuick Singleton { id: root @@ -40,21 +40,23 @@ Singleton { PersistentProperties { id: props - property bool enabled: Hypr.options["animations:enabled"] === 0 // qmllint disable missing-property + property bool enabled: Hypr.options["animations:enabled"] === 0 reloadableId: "gameMode" } Connections { + target: Hypr + function onConfigReloaded(): void { if (props.enabled) root.setDynamicConfs(); } - - target: Hypr } IpcHandler { + target: "gameMode" + function isEnabled(): bool { return props.enabled; } @@ -70,7 +72,5 @@ Singleton { function disable(): void { props.enabled = false; } - - target: "gameMode" } } diff --git a/services/Hypr.qml b/services/Hypr.qml index 98c6e7186..c703f7047 100644 --- a/services/Hypr.qml +++ b/services/Hypr.qml @@ -1,13 +1,13 @@ pragma Singleton -import QtQuick +import qs.components.misc +import qs.config +import Caelestia +import Caelestia.Internal import Quickshell import Quickshell.Hyprland import Quickshell.Io -import Caelestia -import Caelestia.Internal -import qs.components.misc -import qs.config +import QtQuick Singleton { id: root @@ -120,6 +120,8 @@ Singleton { } Connections { + target: Hyprland + function onRawEvent(event: HyprlandEvent): void { const n = event.name; if (n.endsWith("v2")) @@ -142,11 +144,11 @@ Singleton { Hyprland.refreshToplevels(); } } - - target: Hyprland } Connections { + target: root.focusedMonitor + function onLastIpcObjectChanged(): void { const specialName = root.focusedMonitor.lastIpcObject.specialWorkspace.name; @@ -154,8 +156,6 @@ Singleton { root.lastSpecialWorkspace = specialName; } } - - target: root.focusedMonitor } FileView { @@ -192,6 +192,8 @@ Singleton { } IpcHandler { + target: "hypr" + function refreshDevices(): void { extras.refreshDevices(); } @@ -203,13 +205,9 @@ Singleton { function listSpecialWorkspaces(): string { return root.workspaces.values.filter(w => w.name.startsWith("special:") && w.lastIpcObject.windows > 0).map(w => w.name).join("\n"); } - - target: "hypr" } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "refreshDevices" description: "Reload devices" onPressed: extras.refreshDevices() diff --git a/services/IdleInhibitor.qml b/services/IdleInhibitor.qml index 9f556b3a4..29409abc1 100644 --- a/services/IdleInhibitor.qml +++ b/services/IdleInhibitor.qml @@ -35,6 +35,8 @@ Singleton { } IpcHandler { + target: "idleInhibitor" + function isEnabled(): bool { return props.enabled; } @@ -50,7 +52,5 @@ Singleton { function disable(): void { props.enabled = false; } - - target: "idleInhibitor" } } diff --git a/services/Network.qml b/services/Network.qml index 4e0b809ca..ede37c802 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -1,28 +1,44 @@ pragma Singleton -import QtQuick import Quickshell import Quickshell.Io +import QtQuick import qs.services Singleton { id: root + Component.onCompleted: { + // Trigger ethernet device detection after initialization + Qt.callLater(() => { + getEthernetDevices(); + }); + // Load saved connections on startup + Nmcli.loadSavedConnections(() => { + root.savedConnections = Nmcli.savedConnections; + root.savedConnectionSsids = Nmcli.savedConnectionSsids; + }); + // Get initial WiFi status + Nmcli.getWifiStatus(enabled => { + root.wifiEnabled = enabled; + }); + // Sync networks from Nmcli on startup + Qt.callLater(() => { + syncNetworksFromNmcli(); + }, 100); + } + readonly property list networks: [] readonly property AccessPoint active: networks.find(n => n.active) ?? null property bool wifiEnabled: true readonly property bool scanning: Nmcli.scanning + property list ethernetDevices: [] readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null property int ethernetDeviceCount: 0 property bool ethernetProcessRunning: false property var ethernetDeviceDetails: null property var wirelessDeviceDetails: null - property var pendingConnection: null - property list savedConnections: [] - property list savedConnectionSsids: [] - - signal connectionFailed(string ssid) function enableWifi(enabled: bool): void { Nmcli.enableWifi(enabled, result => { @@ -50,6 +66,9 @@ Singleton { Nmcli.rescanWifi(); } + property var pendingConnection: null + signal connectionFailed(string ssid) + function connectToNetwork(ssid: string, password: string, bssid: string, callback: var): void { // Set up pending connection tracking if callback provided if (callback) { @@ -140,6 +159,20 @@ Singleton { }); } + property list savedConnections: [] + property list savedConnectionSsids: [] + + // Sync saved connections from Nmcli when they're updated + Connections { + target: Nmcli + function onSavedConnectionsChanged() { + root.savedConnections = Nmcli.savedConnections; + } + function onSavedConnectionSsidsChanged() { + root.savedConnectionSsids = Nmcli.savedConnectionSsids; + } + } + function syncNetworksFromNmcli(): void { const rNetworks = root.networks; const nNetworks = Nmcli.networks; @@ -184,6 +217,22 @@ Singleton { } } + component AccessPoint: QtObject { + required property var lastIpcObject + readonly property string ssid: lastIpcObject.ssid + readonly property string bssid: lastIpcObject.bssid + readonly property int strength: lastIpcObject.strength + readonly property int frequency: lastIpcObject.frequency + readonly property bool active: lastIpcObject.active + readonly property string security: lastIpcObject.security + readonly property bool isSecure: security.length > 0 + } + + Component { + id: apComp + AccessPoint {} + } + function hasSavedProfile(ssid: string): bool { // Use Nmcli's hasSavedProfile which has the same logic return Nmcli.hasSavedProfile(ssid); @@ -260,39 +309,6 @@ Singleton { return octets.join("."); } - Component.onCompleted: { - // Trigger ethernet device detection after initialization - Qt.callLater(() => { - getEthernetDevices(); - }); - // Load saved connections on startup - Nmcli.loadSavedConnections(() => { - root.savedConnections = Nmcli.savedConnections; - root.savedConnectionSsids = Nmcli.savedConnectionSsids; - }); - // Get initial WiFi status - Nmcli.getWifiStatus(enabled => { - root.wifiEnabled = enabled; - }); - // Sync networks from Nmcli on startup - Qt.callLater(() => { - syncNetworksFromNmcli(); - }, 100); - } - - // Sync saved connections from Nmcli when they're updated - Connections { - function onSavedConnectionsChanged() { - root.savedConnections = Nmcli.savedConnections; - } - - function onSavedConnectionSsidsChanged() { - root.savedConnectionSsids = Nmcli.savedConnectionSsids; - } - - target: Nmcli - } - Timer { id: monitorDebounce @@ -312,21 +328,4 @@ Singleton { onRead: monitorDebounce.start() } } - - Component { - id: apComp - - AccessPoint {} - } - - component AccessPoint: QtObject { - required property var lastIpcObject - readonly property string ssid: lastIpcObject.ssid - readonly property string bssid: lastIpcObject.bssid - readonly property int strength: lastIpcObject.strength - readonly property int frequency: lastIpcObject.frequency - readonly property bool active: lastIpcObject.active - readonly property string security: lastIpcObject.security - readonly property bool isSecure: security.length > 0 - } } diff --git a/services/NetworkUsage.qml b/services/NetworkUsage.qml index 1f74c97bf..451864710 100644 --- a/services/NetworkUsage.qml +++ b/services/NetworkUsage.qml @@ -1,10 +1,13 @@ pragma Singleton -import QtQuick +import qs.config + import Quickshell import Quickshell.Io + import Caelestia.Internal -import qs.config + +import QtQuick Singleton { id: root @@ -138,19 +141,16 @@ Singleton { CircularBuffer { id: _downloadBuffer - capacity: root.historyLength + 1 } CircularBuffer { id: _uploadBuffer - capacity: root.historyLength + 1 } FileView { id: netDevFile - path: "/proc/net/dev" } diff --git a/services/Nmcli.qml b/services/Nmcli.qml index 7af9513bb..812387f1e 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -1,9 +1,9 @@ pragma Singleton pragma ComponentBehavior: Bound -import QtQuick import Quickshell import Quickshell.Io +import QtQuick Singleton { id: root @@ -24,14 +24,13 @@ Singleton { property var wifiConnectionQueue: [] property int currentSsidQueryIndex: 0 property var pendingConnection: null + signal connectionFailed(string ssid) property var wirelessDeviceDetails: null property var ethernetDeviceDetails: null property list ethernetDevices: [] readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null - property list activeProcesses: [] - readonly property alias connectionCheckTimer: connectionCheckTimer - readonly property alias immediateCheckTimer: immediateCheckTimer + property list activeProcesses: [] // Constants readonly property string deviceTypeWifi: "wifi" @@ -56,8 +55,6 @@ Singleton { readonly property string connectionParamPassword: "password" readonly property string connectionParamBssid: "802-11-wireless.bssid" - signal connectionFailed(string ssid) - function detectPasswordRequired(error: string): bool { if (!error || error.length === 0) { return false; @@ -872,6 +869,247 @@ Singleton { return false; } + component CommandProcess: Process { + id: proc + + property var callback: null + property list command: [] + property bool callbackCalled: false + property int exitCode: 0 + + signal processFinished + + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + + stdout: StdioCollector { + id: stdoutCollector + } + + stderr: StdioCollector { + id: stderrCollector + + onStreamFinished: { + const error = text.trim(); + if (error && error.length > 0) { + const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; + root.handlePasswordRequired(proc, error, output, -1); + } + } + } + + onExited: code => { + exitCode = code; + + Qt.callLater(() => { + if (callbackCalled) { + processFinished(); + return; + } + + if (proc.callback) { + const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; + const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; + const success = exitCode === 0; + const cmdIsConnection = isConnectionCommand(proc.command); + + if (root.handlePasswordRequired(proc, error, output, exitCode)) { + processFinished(); + return; + } + + const needsPassword = cmdIsConnection && root.detectPasswordRequired(error); + + if (!success && cmdIsConnection && root.pendingConnection) { + const failedSsid = root.pendingConnection.ssid; + root.connectionFailed(failedSsid); + } + + callbackCalled = true; + callback({ + success: success, + output: output, + error: error, + exitCode: proc.exitCode, + needsPassword: needsPassword || false + }); + processFinished(); + } else { + processFinished(); + } + }); + } + } + + Component { + id: commandProc + + CommandProcess {} + } + + component AccessPoint: QtObject { + required property var lastIpcObject + readonly property string ssid: lastIpcObject.ssid + readonly property string bssid: lastIpcObject.bssid + readonly property int strength: lastIpcObject.strength + readonly property int frequency: lastIpcObject.frequency + readonly property bool active: lastIpcObject.active + readonly property string security: lastIpcObject.security + readonly property bool isSecure: security.length > 0 + } + + Component { + id: apComp + + AccessPoint {} + } + + Timer { + id: connectionCheckTimer + + interval: 4000 + onTriggered: { + if (root.pendingConnection) { + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + + if (!connected && root.pendingConnection.callback) { + let foundPasswordError = false; + for (let i = 0; i < root.activeProcesses.length; i++) { + const proc = root.activeProcesses[i]; + if (proc && proc.stderr && proc.stderr.text) { + const error = proc.stderr.text.trim(); + if (error && error.length > 0) { + if (root.isConnectionCommand(proc.command)) { + const needsPassword = root.detectPasswordRequired(error); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection) { + const pending = root.pendingConnection; + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + proc.callbackCalled = true; + const result = { + success: false, + output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", + error: error, + exitCode: -1, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + foundPasswordError = true; + break; + } + } + } + } + } + + if (!foundPasswordError) { + const pending = root.pendingConnection; + const failedSsid = pending.ssid; + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + root.connectionFailed(failedSsid); + pending.callback({ + success: false, + output: "", + error: "Connection timeout", + exitCode: -1, + needsPassword: false + }); + } + } else if (connected) { + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } + } + + Timer { + id: immediateCheckTimer + + property int checkCount: 0 + + interval: 500 + repeat: true + triggeredOnStart: false + + onTriggered: { + if (root.pendingConnection) { + checkCount++; + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + + if (connected) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + if (root.pendingConnection.callback) { + root.pendingConnection.callback({ + success: true, + output: "Connected", + error: "", + exitCode: 0 + }); + } + root.pendingConnection = null; + } else { + for (let i = 0; i < root.activeProcesses.length; i++) { + const proc = root.activeProcesses[i]; + if (proc && proc.stderr && proc.stderr.text) { + const error = proc.stderr.text.trim(); + if (error && error.length > 0) { + if (root.isConnectionCommand(proc.command)) { + const needsPassword = root.detectPasswordRequired(error); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + const pending = root.pendingConnection; + root.pendingConnection = null; + proc.callbackCalled = true; + const result = { + success: false, + output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", + error: error, + exitCode: -1, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + return; + } + } + } + } + } + + if (checkCount >= 6) { + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } else { + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } + function checkPendingConnection(): void { if (root.pendingConnection) { Qt.callLater(() => { @@ -1023,14 +1261,44 @@ Singleton { return details; } - function refreshOnConnectionChange(): void { - getNetworks(networks => { - const newActive = root.active; + Process { + id: rescanProc - if (newActive && newActive.active) { - Qt.callLater(() => { - if (root.wirelessInterfaces.length > 0) { - const activeWireless = root.wirelessInterfaces.find(iface => { + command: ["nmcli", "dev", root.nmcliCommandWifi, "list", "--rescan", "yes"] + onExited: root.getNetworks() + } + + Process { + id: monitorProc + + running: true + command: ["nmcli", "monitor"] + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + stdout: SplitParser { + onRead: root.refreshOnConnectionChange() + } + onExited: monitorRestartTimer.start() + } + + Timer { + id: monitorRestartTimer + interval: 2000 + onTriggered: { + monitorProc.running = true; + } + } + + function refreshOnConnectionChange(): void { + getNetworks(networks => { + const newActive = root.active; + + if (newActive && newActive.active) { + Qt.callLater(() => { + if (root.wirelessInterfaces.length > 0) { + const activeWireless = root.wirelessInterfaces.find(iface => { return isConnectedState(iface.state); }); if (activeWireless && activeWireless.device) { @@ -1089,276 +1357,4 @@ Singleton { } }, 2000); } - - Component { - id: commandProc - - CommandProcess {} - } - - Component { - id: apComp - - AccessPoint {} - } - - Timer { - id: connectionCheckTimer - - interval: 4000 - onTriggered: { - if (root.pendingConnection) { - const connected = root.active && root.active.ssid === root.pendingConnection.ssid; - - if (!connected && root.pendingConnection.callback) { - let foundPasswordError = false; - for (let i = 0; i < root.activeProcesses.length; i++) { - const proc = root.activeProcesses[i]; - if (proc && proc.stderr && proc.stderr.text) { - const error = proc.stderr.text.trim(); - if (error && error.length > 0) { - if (root.isConnectionCommand(proc.command)) { - const needsPassword = root.detectPasswordRequired(error); - - if (needsPassword && !proc.callbackCalled && root.pendingConnection) { - const pending = root.pendingConnection; - root.pendingConnection = null; - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - proc.callbackCalled = true; - const result = { - success: false, - output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", - error: error, - exitCode: -1, - needsPassword: true - }; - if (pending.callback) { - pending.callback(result); - } - if (proc.callback && proc.callback !== pending.callback) { - proc.callback(result); - } - foundPasswordError = true; - break; - } - } - } - } - } - - if (!foundPasswordError) { - const pending = root.pendingConnection; - const failedSsid = pending.ssid; - root.pendingConnection = null; - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - root.connectionFailed(failedSsid); - pending.callback({ - success: false, - output: "", - error: "Connection timeout", - exitCode: -1, - needsPassword: false - }); - } - } else if (connected) { - root.pendingConnection = null; - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - } - } - } - } - - Timer { - id: immediateCheckTimer - - property int checkCount: 0 - - interval: 500 - repeat: true - triggeredOnStart: false - - onTriggered: { - if (root.pendingConnection) { - checkCount++; - const connected = root.active && root.active.ssid === root.pendingConnection.ssid; - - if (connected) { - connectionCheckTimer.stop(); - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - if (root.pendingConnection.callback) { - root.pendingConnection.callback({ - success: true, - output: "Connected", - error: "", - exitCode: 0 - }); - } - root.pendingConnection = null; - } else { - for (let i = 0; i < root.activeProcesses.length; i++) { - const proc = root.activeProcesses[i]; - if (proc && proc.stderr && proc.stderr.text) { - const error = proc.stderr.text.trim(); - if (error && error.length > 0) { - if (root.isConnectionCommand(proc.command)) { - const needsPassword = root.detectPasswordRequired(error); - - if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { - connectionCheckTimer.stop(); - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - const pending = root.pendingConnection; - root.pendingConnection = null; - proc.callbackCalled = true; - const result = { - success: false, - output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", - error: error, - exitCode: -1, - needsPassword: true - }; - if (pending.callback) { - pending.callback(result); - } - if (proc.callback && proc.callback !== pending.callback) { - proc.callback(result); - } - return; - } - } - } - } - } - - if (checkCount >= 6) { - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - } - } - } else { - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - } - } - } - - Process { - id: rescanProc - - command: ["nmcli", "dev", root.nmcliCommandWifi, "list", "--rescan", "yes"] - onExited: root.getNetworks() // qmllint disable signal-handler-parameters - } - - Process { - id: monitorProc - - running: true - command: ["nmcli", "monitor"] - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - stdout: SplitParser { - onRead: root.refreshOnConnectionChange() - } - onExited: monitorRestartTimer.start() // qmllint disable signal-handler-parameters - } - - Timer { - id: monitorRestartTimer - - interval: 2000 - onTriggered: { - monitorProc.running = true; - } - } - - component CommandProcess: Process { - id: proc - - property var callback: null - property list command: [] - property bool callbackCalled: false - property int exitCode: 0 - - signal processFinished - - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - - stdout: StdioCollector { - id: stdoutCollector - } - - stderr: StdioCollector { - id: stderrCollector - - onStreamFinished: { - const error = text.trim(); - if (error && error.length > 0) { - const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; - root.handlePasswordRequired(proc, error, output, -1); - } - } - } - - onExited: code => { // qmllint disable signal-handler-parameters - exitCode = code; - - Qt.callLater(() => { - if (callbackCalled) { - processFinished(); - return; - } - - if (proc.callback) { - const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; - const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; - const success = exitCode === 0; - const cmdIsConnection = isConnectionCommand(proc.command); - - if (root.handlePasswordRequired(proc, error, output, exitCode)) { - processFinished(); - return; - } - - const needsPassword = cmdIsConnection && root.detectPasswordRequired(error); - - if (!success && cmdIsConnection && root.pendingConnection) { - const failedSsid = root.pendingConnection.ssid; - root.connectionFailed(failedSsid); - } - - callbackCalled = true; - callback({ - success: success, - output: output, - error: error, - exitCode: proc.exitCode, - needsPassword: needsPassword || false - }); - processFinished(); - } else { - processFinished(); - } - }); - } - } - - component AccessPoint: QtObject { - required property var lastIpcObject - readonly property string ssid: lastIpcObject.ssid - readonly property string bssid: lastIpcObject.bssid - readonly property int strength: lastIpcObject.strength - readonly property int frequency: lastIpcObject.frequency - readonly property bool active: lastIpcObject.active - readonly property string security: lastIpcObject.security - readonly property bool isSecure: security.length > 0 - } } diff --git a/services/Notifs.qml b/services/Notifs.qml index 4e6c0dd6b..aff2dfc6a 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -1,22 +1,21 @@ pragma Singleton pragma ComponentBehavior: Bound -import QtQuick -import Quickshell -import Quickshell.Io -import Quickshell.Services.Notifications -import Caelestia import qs.components.misc -import qs.services import qs.config import qs.utils +import Caelestia +import Quickshell +import Quickshell.Io +import Quickshell.Services.Notifications +import QtQuick Singleton { id: root - property list list: [] - readonly property list notClosed: list.filter(n => !n.closed) - readonly property list popups: list.filter(n => n.popup) + property list list: [] + readonly property list notClosed: list.filter(n => !n.closed) + readonly property list popups: list.filter(n => n.popup) property alias dnd: props.dnd property bool loaded @@ -105,9 +104,7 @@ Singleton { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "clearNotifs" description: "Clear all notifications" onPressed: { @@ -117,6 +114,8 @@ Singleton { } IpcHandler { + target: "notifs" + function clear(): void { for (const notif of root.list.slice()) notif.close(); @@ -137,13 +136,225 @@ Singleton { function disableDnd(): void { props.dnd = false; } + } - target: "notifs" + component Notif: QtObject { + id: notif + + property bool popup + property bool closed + property var locks: new Set() + + property date time: new Date() + property string timeStr: qsTr("now") + + function updateTimeStr(): void { + const diff = Date.now() - time.getTime(); + const m = Math.floor(diff / 60000); + + if (m < 1) { + timeStr = qsTr("now"); + timeStrTimer.interval = 5000; + } else { + const h = Math.floor(m / 60); + const d = Math.floor(h / 24); + + if (d > 0) { + timeStr = `${d}d`; + timeStrTimer.interval = 3600000; + } else if (h > 0) { + timeStr = `${h}h`; + timeStrTimer.interval = 300000; + } else { + timeStr = `${m}m`; + timeStrTimer.interval = m < 10 ? 30000 : 60000; + } + } + } + + readonly property Timer timeStrTimer: Timer { + running: !notif.closed + repeat: true + interval: 5000 + onTriggered: notif.updateTimeStr() + } + + property Notification notification + property string id + property string summary + property string body + property string appIcon + property string appName + property string image + property var hints // Hints are not persisted across restarts + property real expireTimeout: Config.notifs.defaultExpireTimeout + property int urgency: NotificationUrgency.Normal + property bool resident + property bool hasActionIcons + property list actions + + readonly property Timer timer: Timer { + running: true + interval: notif.expireTimeout > 0 ? notif.expireTimeout : Config.notifs.defaultExpireTimeout + onTriggered: { + if (Config.notifs.expire) + notif.popup = false; + } + } + + readonly property LazyLoader dummyImageLoader: LazyLoader { + active: false + + PanelWindow { + implicitWidth: Config.notifs.sizes.image + implicitHeight: Config.notifs.sizes.image + color: "transparent" + mask: Region {} + + Image { + function tryCache(): void { + if (status !== Image.Ready || width != Config.notifs.sizes.image || height != Config.notifs.sizes.image) + return; + + const cacheKey = notif.appName + notif.summary + notif.id; + let h1 = 0xdeadbeef, h2 = 0x41c6ce57, ch; + for (let i = 0; i < cacheKey.length; i++) { + ch = cacheKey.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); + h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); + h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); + const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0); + + const cache = `${Paths.notifimagecache}/${hash}.png`; + CUtils.saveItem(this, Qt.resolvedUrl(cache), () => { + notif.image = cache; + notif.dummyImageLoader.active = false; + }); + } + + anchors.fill: parent + source: Qt.resolvedUrl(notif.image) + fillMode: Image.PreserveAspectCrop + cache: false + asynchronous: true + opacity: 0 + + onStatusChanged: tryCache() + onWidthChanged: tryCache() + onHeightChanged: tryCache() + } + } + } + + readonly property Connections conn: Connections { + target: notif.notification + + function onClosed(): void { + notif.close(); + } + + function onSummaryChanged(): void { + notif.summary = notif.notification.summary; + } + + function onBodyChanged(): void { + notif.body = notif.notification.body; + } + + function onAppIconChanged(): void { + notif.appIcon = notif.notification.appIcon; + } + + function onAppNameChanged(): void { + notif.appName = notif.notification.appName; + } + + function onImageChanged(): void { + notif.image = notif.notification.image; + if (notif.notification?.image) + notif.dummyImageLoader.active = true; + } + + function onExpireTimeoutChanged(): void { + notif.expireTimeout = notif.notification.expireTimeout; + } + + function onUrgencyChanged(): void { + notif.urgency = notif.notification.urgency; + } + + function onResidentChanged(): void { + notif.resident = notif.notification.resident; + } + + function onHasActionIconsChanged(): void { + notif.hasActionIcons = notif.notification.hasActionIcons; + } + + function onActionsChanged(): void { + notif.actions = notif.notification.actions.map(a => ({ + identifier: a.identifier, + text: a.text, + invoke: () => a.invoke() + })); + } + + function onHintsChanged(): void { + notif.hints = notif.notification.hints; + } + } + + function lock(item: Item): void { + locks.add(item); + } + + function unlock(item: Item): void { + locks.delete(item); + if (closed) + close(); + } + + function close(): void { + closed = true; + if (locks.size === 0 && root.list.includes(this)) { + root.list = root.list.filter(n => n !== this); + notification?.dismiss(); + destroy(); + } + } + + Component.onCompleted: { + if (!notification) + return; + + id = notification.id; + summary = notification.summary; + body = notification.body; + appIcon = notification.appIcon; + appName = notification.appName; + image = notification.image; + if (notification?.image) + dummyImageLoader.active = true; + expireTimeout = notification.expireTimeout; + hints = notification.hints; + urgency = notification.urgency; + resident = notification.resident; + hasActionIcons = notification.hasActionIcons; + actions = notification.actions.map(a => ({ + identifier: a.identifier, + text: a.text, + invoke: () => a.invoke() + })); + } } Component { id: notifComp - NotifData {} + Notif {} } } diff --git a/services/Players.qml b/services/Players.qml index d51b5df38..1191696ae 100644 --- a/services/Players.qml +++ b/services/Players.qml @@ -1,12 +1,12 @@ pragma Singleton -import QtQml +import qs.components.misc +import qs.config import Quickshell import Quickshell.Io import Quickshell.Services.Mpris +import QtQml import Caelestia -import qs.components.misc -import qs.config Singleton { id: root @@ -21,16 +21,16 @@ Singleton { } Connections { + target: active + function onPostTrackChanged() { if (!Config.utilities.toasts.nowPlaying) { return; } - if (root.active.trackArtist != "" && root.active.trackTitle != "") { - Toaster.toast(qsTr("Now Playing"), qsTr("%1 - %2").arg(root.active.trackArtist).arg(root.active.trackTitle), "music_note"); + if (active.trackArtist != "" && active.trackTitle != "") { + Toaster.toast(qsTr("Now Playing"), qsTr("%1 - %2").arg(active.trackArtist).arg(active.trackTitle), "music_note"); } } - - target: root.active } PersistentProperties { @@ -41,9 +41,7 @@ Singleton { reloadableId: "players" } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "mediaToggle" description: "Toggle media playback" onPressed: { @@ -53,9 +51,7 @@ Singleton { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "mediaPrev" description: "Previous track" onPressed: { @@ -65,9 +61,7 @@ Singleton { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "mediaNext" description: "Next track" onPressed: { @@ -77,15 +71,15 @@ Singleton { } } - // qmllint disable unresolved-type CustomShortcut { - // qmllint enable unresolved-type name: "mediaStop" description: "Stop media playback" onPressed: root.active?.stop() } IpcHandler { + target: "mpris" + function getActive(prop: string): string { const active = root.active; return active ? active[prop] ?? "Invalid property" : "No active player"; @@ -128,7 +122,5 @@ Singleton { function stop(): void { root.active?.stop(); } - - target: "mpris" } } diff --git a/services/Recorder.qml b/services/Recorder.qml index 8ad00145e..6eddce949 100644 --- a/services/Recorder.qml +++ b/services/Recorder.qml @@ -1,8 +1,8 @@ pragma Singleton -import QtQuick import Quickshell import Quickshell.Io +import QtQuick Singleton { id: root @@ -46,7 +46,7 @@ Singleton { running: true command: ["pidof", "gpu-screen-recorder"] - onExited: code => { // qmllint disable signal-handler-parameters + onExited: code => { props.running = code === 0; if (code === 0) { @@ -72,11 +72,11 @@ Singleton { } Connections { + target: Time // enabled: props.running && !props.paused + function onSecondsChanged(): void { props.elapsed++; } - - target: Time // qmllint disable incompatible-type } } diff --git a/services/Screens.qml b/services/Screens.qml index 9fed887f3..a64751785 100644 --- a/services/Screens.qml +++ b/services/Screens.qml @@ -1,8 +1,8 @@ pragma Singleton -import Quickshell import qs.config import qs.utils +import Quickshell Singleton { id: root diff --git a/services/SystemUsage.qml b/services/SystemUsage.qml index 79691fead..508564461 100644 --- a/services/SystemUsage.qml +++ b/services/SystemUsage.qml @@ -1,9 +1,9 @@ pragma Singleton -import QtQuick +import qs.config import Quickshell import Quickshell.Io -import qs.config +import QtQuick Singleton { id: root @@ -138,7 +138,6 @@ Singleton { Process { id: storage - // Get physical disks with aggregated usage from their partitions // -J triggers JSON output. -b triggers bytes. command: ["lsblk", "-J", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE,MOUNTPOINT"] diff --git a/services/Time.qml b/services/Time.qml index d41c9142c..a07d9ef8e 100644 --- a/services/Time.qml +++ b/services/Time.qml @@ -1,8 +1,8 @@ pragma Singleton -import QtQuick -import Quickshell import qs.config +import Quickshell +import QtQuick Singleton { property alias enabled: clock.enabled @@ -23,7 +23,6 @@ Singleton { SystemClock { id: clock - precision: SystemClock.Seconds } } diff --git a/services/VPN.qml b/services/VPN.qml index 2b25813b2..2d08631a1 100644 --- a/services/VPN.qml +++ b/services/VPN.qml @@ -1,10 +1,10 @@ pragma Singleton -import QtQuick import Quickshell import Quickshell.Io -import Caelestia +import QtQuick import qs.config +import Caelestia Singleton { id: root @@ -128,9 +128,7 @@ Singleton { id: statusProc command: ["ip", "link", "show"] - // qmllint disable incompatible-type environment: ({ - // qmllint enable incompatible-type LANG: "C.UTF-8", LC_ALL: "C.UTF-8" }) @@ -145,7 +143,7 @@ Singleton { Process { id: connectProc - onExited: statusCheckTimer.start() // qmllint disable signal-handler-parameters + onExited: statusCheckTimer.start() stderr: StdioCollector { onStreamFinished: { const error = text.trim(); @@ -161,7 +159,7 @@ Singleton { Process { id: disconnectProc - onExited: statusCheckTimer.start() // qmllint disable signal-handler-parameters + onExited: statusCheckTimer.start() stderr: StdioCollector { onStreamFinished: { const error = text.trim(); diff --git a/services/Visibilities.qml b/services/Visibilities.qml index 391870502..5ddde0c95 100644 --- a/services/Visibilities.qml +++ b/services/Visibilities.qml @@ -1,18 +1,16 @@ pragma Singleton import Quickshell -import qs.components -import qs.services Singleton { property var screens: new Map() property var bars: new Map() - function load(screen: ShellScreen, visibilities: DrawerVisibilities): void { + function load(screen: ShellScreen, visibilities: var): void { screens.set(Hypr.monitorFor(screen), visibilities); } - function getForActive(): DrawerVisibilities { + function getForActive(): PersistentProperties { return screens.get(Hypr.focusedMonitor); } } diff --git a/services/Wallpapers.qml b/services/Wallpapers.qml index 602abb205..cb96bc565 100644 --- a/services/Wallpapers.qml +++ b/services/Wallpapers.qml @@ -1,12 +1,11 @@ pragma Singleton -import QtQuick -import Quickshell -import Quickshell.Io -import Caelestia.Models -import qs.services import qs.config import qs.utils +import Caelestia.Models +import Quickshell +import Quickshell.Io +import QtQuick Searcher { id: root @@ -47,6 +46,8 @@ Searcher { }) IpcHandler { + target: "wallpaper" + function get(): string { return root.actualCurrent; } @@ -58,8 +59,6 @@ Searcher { function list(): string { return root.list.map(w => w.path).join("\n"); } - - target: "wallpaper" } FileView { diff --git a/services/Weather.qml b/services/Weather.qml index d226365fd..98e29bbba 100644 --- a/services/Weather.qml +++ b/services/Weather.qml @@ -1,10 +1,10 @@ pragma Singleton -import QtQuick -import Quickshell -import Caelestia import qs.config import qs.utils +import Caelestia +import Quickshell +import QtQuick Singleton { id: root @@ -210,11 +210,10 @@ Singleton { onLocChanged: fetchWeatherData() Connections { + target: Config.services function onWeatherLocationChanged(): void { root.reload(); } - - target: Config.services } // Refresh current location hourly diff --git a/utils/Icons.qml b/utils/Icons.qml index 8a62f62d7..34f8049bf 100644 --- a/utils/Icons.qml +++ b/utils/Icons.qml @@ -1,9 +1,9 @@ pragma Singleton -import QtQuick +import qs.config import Quickshell import Quickshell.Services.Notifications -import qs.config +import QtQuick Singleton { id: root diff --git a/utils/NetworkConnection.qml b/utils/NetworkConnection.qml index 8331813d9..e55b87bc4 100644 --- a/utils/NetworkConnection.qml +++ b/utils/NetworkConnection.qml @@ -1,7 +1,7 @@ pragma Singleton -import QtQuick import qs.services +import QtQuick /** * NetworkConnection diff --git a/utils/Paths.qml b/utils/Paths.qml index b0926348d..bc89770ab 100644 --- a/utils/Paths.qml +++ b/utils/Paths.qml @@ -1,9 +1,8 @@ pragma Singleton -import QtQuick -import Quickshell -import Caelestia import qs.config +import Caelestia +import Quickshell Singleton { id: root diff --git a/utils/Searcher.qml b/utils/Searcher.qml index 102c9e766..053b73bba 100644 --- a/utils/Searcher.qml +++ b/utils/Searcher.qml @@ -1,7 +1,8 @@ +import Quickshell + import "scripts/fzf.js" as Fzf import "scripts/fuzzysort.js" as Fuzzy import QtQuick -import Quickshell Singleton { required property list list diff --git a/utils/SysInfo.qml b/utils/SysInfo.qml index 74c94e9bc..19aa4a7a7 100644 --- a/utils/SysInfo.qml +++ b/utils/SysInfo.qml @@ -1,10 +1,10 @@ pragma Singleton -import QtQuick -import Quickshell -import Quickshell.Io import qs.config import qs.utils +import Quickshell +import Quickshell.Io +import QtQuick Singleton { id: root @@ -50,11 +50,11 @@ Singleton { } Connections { + target: Config.general + function onLogoChanged(): void { osRelease.reload(); } - - target: Config.general } Timer {