diff --git a/.gitignore b/.gitignore index dc709e6..ad22547 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +./result/ .pyre/ .cache/ .venv/ diff --git a/Components/BaseStyledSlider.qml b/Components/BaseStyledSlider.qml new file mode 100644 index 0000000..48c8f33 --- /dev/null +++ b/Components/BaseStyledSlider.qml @@ -0,0 +1,165 @@ +import QtQuick +import QtQuick.Templates +import qs.Config + +Slider { + id: root + + property color color: DynamicColors.palette.m3secondary + required property string icon + property bool initialized: false + readonly property bool isHorizontal: orientation === Qt.Horizontal + readonly property bool isVertical: orientation === Qt.Vertical + property real multiplier: 100 + property real oldValue + + // Wrapper components can inject their own track visuals here. + property Component trackContent + + // Keep current behavior for existing usages. + orientation: Qt.Vertical + + background: CustomRect { + id: groove + + color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2) + height: root.availableHeight + radius: Appearance.rounding.full + width: root.availableWidth + x: root.leftPadding + y: root.topPadding + + Loader { + id: trackLoader + + anchors.fill: parent + sourceComponent: root.trackContent + + onLoaded: { + if (!item) + return; + + item.rootSlider = root; + item.groove = groove; + item.handleItem = handle; + } + } + } + handle: Item { + id: handle + + property alias moving: icon.moving + + implicitHeight: Math.min(root.width, root.height) + implicitWidth: Math.min(root.width, root.height) + x: root.isHorizontal ? root.leftPadding + root.visualPosition * (root.availableWidth - width) : root.leftPadding + (root.availableWidth - width) / 2 + y: root.isVertical ? root.topPadding + root.visualPosition * (root.availableHeight - height) : root.topPadding + (root.availableHeight - height) / 2 + + Elevation { + anchors.fill: parent + level: handleInteraction.containsMouse ? 2 : 1 + radius: rect.radius + } + + CustomRect { + id: rect + + anchors.fill: parent + color: DynamicColors.palette.m3inverseSurface + radius: Appearance.rounding.full + + MouseArea { + id: handleInteraction + + acceptedButtons: Qt.NoButton + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + } + + 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; + } + + anchors.centerIn: parent + color: DynamicColors.palette.m3inverseOnSurface + text: root.icon + + onMovingChanged: anim.restart() + + Binding { + id: binding + + property: "text" + target: icon + value: Math.round(root.value * root.multiplier) + when: false + } + + SequentialAnimation { + id: anim + + Anim { + duration: Appearance.anim.durations.normal / 2 + easing.bezierCurve: Appearance.anim.curves.standardAccel + property: "scale" + target: icon + to: 0 + } + + ScriptAction { + script: icon.update() + } + + Anim { + duration: Appearance.anim.durations.normal / 2 + easing.bezierCurve: Appearance.anim.curves.standardDecel + property: "scale" + target: icon + to: 1 + } + } + } + } + } + Behavior on value { + Anim { + duration: Appearance.anim.durations.large + } + } + + onPressedChanged: handle.moving = pressed + onValueChanged: { + if (!initialized) { + initialized = true; + oldValue = value; + 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; + } + } +} diff --git a/Components/ColorArcPicker.qml b/Components/ColorArcPicker.qml new file mode 100644 index 0000000..c909511 --- /dev/null +++ b/Components/ColorArcPicker.qml @@ -0,0 +1,208 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.Config + +Item { + id: root + + readonly property real arcStartAngle: 0.75 * Math.PI + readonly property real arcSweep: 1.5 * Math.PI + property real currentHue: 0 + property bool dragActive: false + required property var drawing + readonly property real handleAngle: hueToAngle(currentHue) + readonly property real handleCenterX: width / 2 + radius * Math.cos(handleAngle) + readonly property real handleCenterY: height / 2 + radius * Math.sin(handleAngle) + property real handleSize: 32 + property real lastChromaticHue: 0 + readonly property real radius: (Math.min(width, height) - handleSize) / 2 + readonly property int segmentCount: 240 + readonly property color thumbColor: DynamicColors.palette.m3inverseSurface + readonly property color thumbContentColor: DynamicColors.palette.m3inverseOnSurface + readonly property color trackColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2) + + function hueToAngle(hue) { + return arcStartAngle + arcSweep * hue; + } + + function normalizeAngle(angle) { + const tau = Math.PI * 2; + let a = angle % tau; + if (a < 0) + a += tau; + return a; + } + + function pointIsOnTrack(x, y) { + const cx = width / 2; + const cy = height / 2; + const dx = x - cx; + const dy = y - cy; + const distance = Math.sqrt(dx * dx + dy * dy); + + return distance >= radius - handleSize / 2 && distance <= radius + handleSize / 2; + } + + function syncFromPenColor() { + if (!drawing) + return; + + const c = drawing.penColor; + + if (c.hsvSaturation > 0) { + currentHue = c.hsvHue; + lastChromaticHue = c.hsvHue; + } else { + currentHue = lastChromaticHue; + } + + canvas.requestPaint(); + } + + function updateHueFromPoint(x, y, force = false) { + const cx = width / 2; + const cy = height / 2; + const dx = x - cx; + const dy = y - cy; + + const distance = Math.sqrt(dx * dx + dy * dy); + + if (!force && (distance < radius - handleSize / 2 || distance > radius + handleSize / 2)) + return; + + const angle = normalizeAngle(Math.atan2(dy, dx)); + const start = normalizeAngle(arcStartAngle); + + let relative = angle - start; + if (relative < 0) + relative += Math.PI * 2; + + if (relative > arcSweep) { + const gap = Math.PI * 2 - arcSweep; + relative = relative < arcSweep + gap / 2 ? arcSweep : 0; + } + + currentHue = relative / arcSweep; + lastChromaticHue = currentHue; + drawing.penColor = Qt.hsva(currentHue, drawing.penColor.hsvSaturation, drawing.penColor.hsvValue, drawing.penColor.a); + } + + implicitHeight: 180 + implicitWidth: 220 + + Component.onCompleted: syncFromPenColor() + onCurrentHueChanged: canvas.requestPaint() + onDrawingChanged: syncFromPenColor() + onHandleSizeChanged: canvas.requestPaint() + onHeightChanged: canvas.requestPaint() + onWidthChanged: canvas.requestPaint() + + Connections { + function onPenColorChanged() { + root.syncFromPenColor(); + } + + target: root.drawing + } + + Canvas { + id: canvas + + anchors.fill: parent + renderStrategy: Canvas.Threaded + renderTarget: Canvas.Image + + Component.onCompleted: requestPaint() + onPaint: { + const ctx = getContext("2d"); + ctx.reset(); + ctx.clearRect(0, 0, width, height); + + const cx = width / 2; + const cy = height / 2; + const radius = root.radius; + const trackWidth = root.handleSize; + + // Background track: always show the full hue spectrum + for (let i = 0; i < root.segmentCount; ++i) { + const t1 = i / root.segmentCount; + const t2 = (i + 1) / root.segmentCount; + const a1 = root.arcStartAngle + root.arcSweep * t1; + const a2 = root.arcStartAngle + root.arcSweep * t2; + + ctx.beginPath(); + ctx.arc(cx, cy, radius, a1, a2); + ctx.lineWidth = trackWidth; + ctx.lineCap = "round"; + ctx.strokeStyle = Qt.hsla(t1, 1.0, 0.5, 1.0); + ctx.stroke(); + } + } + } + + Item { + id: handle + + height: root.handleSize + width: root.handleSize + x: root.handleCenterX - width / 2 + y: root.handleCenterY - height / 2 + z: 1 + + Elevation { + anchors.fill: parent + level: handleHover.containsMouse ? 2 : 1 + radius: rect.radius + } + + Rectangle { + id: rect + + anchors.fill: parent + color: root.thumbColor + radius: width / 2 + + MouseArea { + id: handleHover + + acceptedButtons: Qt.NoButton + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + } + + Rectangle { + anchors.centerIn: parent + color: root.drawing ? root.drawing.penColor : Qt.hsla(root.currentHue, 1.0, 0.5, 1.0) + height: width + radius: width / 2 + width: parent.width - 12 + } + } + } + + MouseArea { + id: dragArea + + acceptedButtons: Qt.LeftButton + anchors.fill: parent + hoverEnabled: true + + onCanceled: { + root.dragActive = false; + } + onPositionChanged: mouse => { + if ((mouse.buttons & Qt.LeftButton) && root.dragActive) + root.updateHueFromPoint(mouse.x, mouse.y, true); + } + onPressed: mouse => { + root.dragActive = root.pointIsOnTrack(mouse.x, mouse.y); + if (root.dragActive) + root.updateHueFromPoint(mouse.x, mouse.y); + } + onReleased: { + root.dragActive = false; + } + } +} diff --git a/Components/CustomButton.qml b/Components/CustomButton.qml index 79580f7..adc521e 100644 --- a/Components/CustomButton.qml +++ b/Components/CustomButton.qml @@ -1,12 +1,13 @@ import QtQuick import QtQuick.Controls +import qs.Config Button { id: control - required property color bgColor + property color bgColor: DynamicColors.palette.m3primary property int radius: 4 - required property color textColor + property color textColor: DynamicColors.palette.m3onPrimary background: CustomRect { color: control.bgColor diff --git a/Components/CustomRadioButton.qml b/Components/CustomRadioButton.qml index f719261..7e743cb 100644 --- a/Components/CustomRadioButton.qml +++ b/Components/CustomRadioButton.qml @@ -5,7 +5,7 @@ import qs.Config RadioButton { id: root - font.pointSize: 12 + font.pointSize: Appearance.font.size.normal implicitHeight: Math.max(implicitIndicatorHeight, implicitContentHeight) implicitWidth: implicitIndicatorWidth + implicitContentWidth + contentItem.anchors.leftMargin diff --git a/Components/CustomSpinBox.qml b/Components/CustomSpinBox.qml index fe98e72..5a4245b 100644 --- a/Components/CustomSpinBox.qml +++ b/Components/CustomSpinBox.qml @@ -28,6 +28,7 @@ RowLayout { CustomTextField { id: textField + implicitHeight: upButton.implicitHeight inputMethodHints: Qt.ImhFormattedNumbersOnly leftPadding: Appearance.padding.normal padding: Appearance.padding.small @@ -37,7 +38,7 @@ RowLayout { background: CustomRect { color: DynamicColors.tPalette.m3surfaceContainerHigh implicitWidth: 100 - radius: Appearance.rounding.small + radius: Appearance.rounding.full } validator: DoubleValidator { bottom: root.min @@ -82,10 +83,12 @@ RowLayout { } CustomRect { + id: upButton + color: DynamicColors.palette.m3primary implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2 implicitWidth: implicitHeight - radius: Appearance.rounding.small + radius: Appearance.rounding.full StateLayer { id: upState @@ -119,7 +122,7 @@ RowLayout { color: DynamicColors.palette.m3primary implicitHeight: downIcon.implicitHeight + Appearance.padding.small * 2 implicitWidth: implicitHeight - radius: Appearance.rounding.small + radius: Appearance.rounding.full StateLayer { id: downState diff --git a/Components/CustomSplitButton.qml b/Components/CustomSplitButton.qml index becabf5..6f9ad95 100644 --- a/Components/CustomSplitButton.qml +++ b/Components/CustomSplitButton.qml @@ -1,6 +1,7 @@ import QtQuick import QtQuick.Layouts import qs.Config +import qs.Helpers Row { id: root @@ -29,8 +30,29 @@ Row { property int type: CustomSplitButton.Filled property real verticalPadding: Appearance.padding.smaller + function closeDropdown(): void { + SettingsDropdowns.close(menu); + } + + function openDropdown(): void { + if (root.disabled) + return; + SettingsDropdowns.open(menu, root); + } + + function toggleDropdown(): void { + if (root.disabled) + return; + SettingsDropdowns.toggle(menu, root); + } + spacing: Math.floor(Appearance.spacing.small / 2) + onExpandedChanged: { + if (!expanded) + SettingsDropdowns.forget(menu); + } + CustomRect { bottomRightRadius: Appearance.rounding.small / 2 color: root.disabled ? root.disabledColor : root.color @@ -109,7 +131,7 @@ Row { id: expandStateLayer function onClicked(): void { - root.expanded = !root.expanded; + root.toggleDropdown(); } color: root.textColor diff --git a/Components/CustomSplitButtonRow.qml b/Components/CustomSplitButtonRow.qml index 763ca8a..491a8ed 100644 --- a/Components/CustomSplitButtonRow.qml +++ b/Components/CustomSplitButtonRow.qml @@ -4,7 +4,7 @@ import QtQuick import QtQuick.Layouts import qs.Config -CustomRect { +Item { id: root property alias active: splitButton.active @@ -18,38 +18,40 @@ CustomRect { signal selected(item: MenuItem) Layout.fillWidth: true + Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 clip: false - color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2) - implicitHeight: row.implicitHeight + Appearance.padding.large * 2 - opacity: enabled ? 1.0 : 0.5 - radius: Appearance.rounding.normal - z: splitButton.menu.implicitHeight > 0 ? expandedZ : 1 + z: root.expanded ? expandedZ : -1 RowLayout { id: row - anchors.fill: parent - anchors.margins: Appearance.padding.large + anchors.left: parent.left + anchors.margins: Appearance.padding.small + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter spacing: Appearance.spacing.normal CustomText { Layout.fillWidth: true color: root.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.larger text: root.label + z: root.expanded ? root.expandedZ : -1 } CustomSplitButton { id: splitButton enabled: root.enabled - menu.z: 1 type: CustomSplitButton.Filled + z: root.expanded ? root.expandedZ : -1 menu.onItemSelected: item => { root.selected(item); + splitButton.closeDropdown(); } stateLayer.onClicked: { - splitButton.expanded = !splitButton.expanded; + splitButton.toggleDropdown(); } } } diff --git a/Components/CustomTextInput.qml b/Components/CustomTextInput.qml new file mode 100644 index 0000000..25b1eed --- /dev/null +++ b/Components/CustomTextInput.qml @@ -0,0 +1,15 @@ +import QtQuick +import QtQuick.Controls +import qs.Config + +TextInput { + renderType: Text.NativeRendering + selectedTextColor: DynamicColors.palette.m3onSecondaryContainer + selectionColor: DynamicColors.tPalette.colSecondaryContainer + + font { + family: Appearance?.font.family.sans ?? "sans-serif" + hintingPreference: Font.PreferFullHinting + pixelSize: Appearance?.font.size.normal ?? 15 + } +} diff --git a/Components/CustomWindow.qml b/Components/CustomWindow.qml new file mode 100644 index 0000000..7e5bf2c --- /dev/null +++ b/Components/CustomWindow.qml @@ -0,0 +1,9 @@ +import Quickshell +import Quickshell.Wayland + +PanelWindow { + required property string name + + WlrLayershell.namespace: `ZShell-${name}` + color: "transparent" +} diff --git a/Components/FilledSlider.qml b/Components/FilledSlider.qml index bab8051..d38d1cb 100644 --- a/Components/FilledSlider.qml +++ b/Components/FilledSlider.qml @@ -1,141 +1,29 @@ import QtQuick -import QtQuick.Templates import qs.Config -Slider { +BaseStyledSlider { id: root - property color color: DynamicColors.palette.m3secondary - required property string icon - property bool initialized - property real oldValue + trackContent: Component { + Item { + property var groove + readonly property real handleHeight: handleItem ? handleItem.height : 0 + property var handleItem + readonly property real handleWidth: handleItem ? handleItem.width : 0 - orientation: Qt.Vertical - - background: CustomRect { - color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.full - - CustomRect { - anchors.left: parent.left - anchors.right: parent.right - color: root.color - implicitHeight: parent.height - y - radius: parent.radius - y: root.handle.y - } - } - handle: Item { - id: handle - - property alias moving: icon.moving - - implicitHeight: root.width - implicitWidth: root.width - y: root.visualPosition * (root.availableHeight - height) - - Elevation { - anchors.fill: parent - level: handleInteraction.containsMouse ? 2 : 1 - radius: rect.radius - } - - CustomRect { - id: rect + // Set by BaseStyledSlider's Loader + property var rootSlider anchors.fill: parent - color: DynamicColors.palette.m3inverseSurface - radius: Appearance.rounding.full - MouseArea { - id: handleInteraction - - acceptedButtons: Qt.NoButton - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - hoverEnabled: true + CustomRect { + color: rootSlider?.color + height: rootSlider?.isVertical ? handleHeight + (1 - rootSlider?.visualPosition) * (groove?.height - handleHeight) : groove?.height + radius: groove?.radius + width: rootSlider?.isHorizontal ? handleWidth + rootSlider?.visualPosition * (groove?.width - handleWidth) : groove?.width + x: rootSlider?.isHorizontal ? (rootSlider?.mirrored ? groove?.width - width : 0) : 0 + y: rootSlider?.isVertical ? groove?.height - height : 0 } - - 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; - } - - anchors.centerIn: parent - color: DynamicColors.palette.m3inverseOnSurface - text: root.icon - - onMovingChanged: anim.restart() - - Binding { - id: binding - - property: "text" - target: icon - value: Math.round(root.value * 100) - when: false - } - - SequentialAnimation { - id: anim - - Anim { - duration: Appearance.anim.durations.normal / 2 - easing.bezierCurve: Appearance.anim.curves.standardAccel - property: "scale" - target: icon - to: 0 - } - - ScriptAction { - script: icon.update() - } - - Anim { - duration: Appearance.anim.durations.normal / 2 - easing.bezierCurve: Appearance.anim.curves.standardDecel - property: "scale" - target: icon - to: 1 - } - } - } - } - } - Behavior on value { - Anim { - duration: Appearance.anim.durations.large - } - } - - 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; } } } diff --git a/Components/GradientSlider.qml b/Components/GradientSlider.qml new file mode 100644 index 0000000..c19f161 --- /dev/null +++ b/Components/GradientSlider.qml @@ -0,0 +1,47 @@ +import QtQuick +import qs.Config + +BaseStyledSlider { + id: root + + property real alpha: 1.0 + property real brightness: 1.0 + property string channel: "saturation" + readonly property color currentColor: Qt.hsva(hue, channel === "saturation" ? value : saturation, channel === "brightness" ? value : brightness, alpha) + property real hue: 0.0 + property real saturation: 1.0 + + from: 0 + to: 1 + + trackContent: Component { + Item { + property var groove + property var handleItem + property var rootSlider + + anchors.fill: parent + + Rectangle { + anchors.fill: parent + antialiasing: true + color: "transparent" + radius: groove?.radius ?? 0 + + gradient: Gradient { + orientation: rootSlider?.isHorizontal ? Gradient.Horizontal : Gradient.Vertical + + GradientStop { + color: root.channel === "saturation" ? Qt.hsva(root.hue, 0.0, root.brightness, root.alpha) : Qt.hsva(root.hue, root.saturation, 0.0, root.alpha) + position: 0.0 + } + + GradientStop { + color: root.channel === "saturation" ? Qt.hsva(root.hue, 1.0, root.brightness, root.alpha) : Qt.hsva(root.hue, root.saturation, 1.0, root.alpha) + position: 1.0 + } + } + } + } + } +} diff --git a/Components/MaterialIcon.qml b/Components/MaterialIcon.qml index 031c6a7..69da4e3 100644 --- a/Components/MaterialIcon.qml +++ b/Components/MaterialIcon.qml @@ -5,7 +5,7 @@ CustomText { property int grade: DynamicColors.light ? 0 : -25 font.family: "Material Symbols Rounded" - font.pointSize: 15 + font.pointSize: Appearance.font.size.larger font.variableAxes: ({ FILL: fill.toFixed(1), GRAD: grade, diff --git a/Components/PathMenu.qml b/Components/PathMenu.qml new file mode 100644 index 0000000..0e2d10d --- /dev/null +++ b/Components/PathMenu.qml @@ -0,0 +1,76 @@ +import QtQuick + +Path { + id: root + + required property real viewHeight + required property real viewWidth + + startX: root.viewWidth / 2 + startY: 0 + + PathAttribute { + name: "itemOpacity" + value: 0.25 + } + + PathLine { + x: root.viewWidth / 2 + y: root.viewHeight * (1 / 6) + } + + PathAttribute { + name: "itemOpacity" + value: 0.45 + } + + PathLine { + x: root.viewWidth / 2 + y: root.viewHeight * (2 / 6) + } + + PathAttribute { + name: "itemOpacity" + value: 0.70 + } + + PathLine { + x: root.viewWidth / 2 + y: root.viewHeight * (3 / 6) + } + + PathAttribute { + name: "itemOpacity" + value: 1.00 + } + + PathLine { + x: root.viewWidth / 2 + y: root.viewHeight * (4 / 6) + } + + PathAttribute { + name: "itemOpacity" + value: 0.70 + } + + PathLine { + x: root.viewWidth / 2 + y: root.viewHeight * (5 / 6) + } + + PathAttribute { + name: "itemOpacity" + value: 0.45 + } + + PathLine { + x: root.viewWidth / 2 + y: root.viewHeight + } + + PathAttribute { + name: "itemOpacity" + value: 0.25 + } +} diff --git a/Components/PathViewMenu.qml b/Components/PathViewMenu.qml new file mode 100644 index 0000000..898dd4b --- /dev/null +++ b/Components/PathViewMenu.qml @@ -0,0 +1,178 @@ +import QtQuick +import QtQuick.Effects +import qs.Config + +Elevation { + id: root + + required property int currentIndex + property bool expanded + required property int from + property color insideTextColor: DynamicColors.palette.m3onPrimary + property int itemHeight + property int listHeight: 200 + property color outsideTextColor: DynamicColors.palette.m3onSurfaceVariant + readonly property var spinnerModel: root.range(root.from, root.to) + required property int to + property Item triggerItem + + signal itemSelected(item: int) + + function range(first, last) { + let out = []; + for (let i = first; i <= last; ++i) + out.push(i); + return out; + } + + implicitHeight: root.expanded ? view.implicitHeight : 0 + level: root.expanded ? 2 : 0 + radius: itemHeight / 2 + visible: implicitHeight > 0 + + Behavior on implicitHeight { + Anim { + } + } + + onExpandedChanged: { + if (!root.expanded) + root.itemSelected(view.currentIndex + 1); + } + + Component { + id: spinnerDelegate + + Item { + id: wrapper + + readonly property color delegateTextColor: wrapper.PathView.view ? wrapper.PathView.view.delegateTextColor : "white" + required property var modelData + + height: root.itemHeight + opacity: wrapper.PathView.itemOpacity + visible: wrapper.PathView.onPath + width: wrapper.PathView.view ? wrapper.PathView.view.width : 0 + z: wrapper.PathView.isCurrentItem ? 100 : Math.round(wrapper.PathView.itemScale * 100) + + CustomText { + anchors.centerIn: parent + color: wrapper.delegateTextColor + font.pointSize: Appearance.font.size.large + text: wrapper.modelData + } + } + } + + CustomClippingRect { + anchors.fill: parent + color: DynamicColors.palette.m3surfaceContainer + radius: parent.radius + + // Main visible spinner: normal/outside text color + PathView { + id: view + + property color delegateTextColor: root.outsideTextColor + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + clip: true + currentIndex: root.currentIndex - 1 + delegate: spinnerDelegate + dragMargin: width + highlightRangeMode: PathView.StrictlyEnforceRange + implicitHeight: root.listHeight + model: root.spinnerModel + pathItemCount: 7 + preferredHighlightBegin: 0.5 + preferredHighlightEnd: 0.5 + snapMode: PathView.SnapToItem + + path: PathMenu { + viewHeight: view.height + viewWidth: view.width + } + } + + // The selection rectangle itself + CustomRect { + id: selectionRect + + anchors.verticalCenter: parent.verticalCenter + color: DynamicColors.palette.m3primary + height: root.itemHeight + radius: root.itemHeight / 2 + width: parent.width + z: 2 + } + + // Hidden source: same PathView, but with the "inside selection" text color + Item { + id: selectedTextSource + + anchors.fill: parent + layer.enabled: true + visible: false + + PathView { + id: selectedTextView + + property color delegateTextColor: root.insideTextColor + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + clip: true + currentIndex: view.currentIndex + delegate: spinnerDelegate + dragMargin: view.dragMargin + highlightRangeMode: view.highlightRangeMode + implicitHeight: root.listHeight + interactive: false + model: view.model + + // Keep this PathView visually locked to the real one + offset: view.offset + pathItemCount: view.pathItemCount + preferredHighlightBegin: view.preferredHighlightBegin + preferredHighlightEnd: view.preferredHighlightEnd + snapMode: view.snapMode + + path: PathMenu { + viewHeight: selectedTextView.height + viewWidth: selectedTextView.width + } + } + } + + // Mask matching the selection rectangle + Item { + id: selectionMask + + anchors.fill: parent + layer.enabled: true + visible: false + + CustomRect { + color: "white" + height: selectionRect.height + radius: selectionRect.radius + width: selectionRect.width + x: selectionRect.x + y: selectionRect.y + } + } + + // Only show the "inside selection" text where the mask exists + MultiEffect { + anchors.fill: selectedTextSource + maskEnabled: true + maskInverted: false + maskSource: selectionMask + source: selectedTextSource + z: 3 + } + } +} diff --git a/Config/AppearanceConf.qml b/Config/AppearanceConf.qml index c76b05e..60c648c 100644 --- a/Config/AppearanceConf.qml +++ b/Config/AppearanceConf.qml @@ -71,6 +71,7 @@ JsonObject { property real scale: 1 property int small: 5 * scale property int smaller: 7 * scale + property int smallest: 2 * scale } component Rounding: JsonObject { property int full: 1000 * scale @@ -78,6 +79,7 @@ JsonObject { property int normal: 17 * scale property real scale: 1 property int small: 12 * scale + property int smallest: 8 * scale } component Spacing: JsonObject { property int large: 20 * scale diff --git a/Config/BarConfig.qml b/Config/BarConfig.qml index 4ec7578..7b39915 100644 --- a/Config/BarConfig.qml +++ b/Config/BarConfig.qml @@ -2,6 +2,7 @@ import Quickshell.Io JsonObject { property bool autoHide: false + property int border: 8 property list entries: [ { id: "workspaces", @@ -60,6 +61,7 @@ JsonObject { enabled: true }, ] + property int height: 34 property Popouts popouts: Popouts { } property int rounding: 8 diff --git a/Config/Config.qml b/Config/Config.qml index ef5fb65..b3f2bac 100644 --- a/Config/Config.qml +++ b/Config/Config.qml @@ -33,6 +33,10 @@ Singleton { recentSaveCooldown.restart(); } + function saveNoToast(): void { + saveTimer.restart(); + } + function serializeAppearance(): var { return { rounding: { @@ -56,8 +60,8 @@ Singleton { } }, anim: { - mediaGifSpeedAdjustment: 300, - sessionGifSpeed: 0.7, + mediaGifSpeedAdjustment: appearance.anim.mediaGifSpeedAdjustment, + sessionGifSpeed: appearance.anim.sessionGifSpeed, durations: { scale: appearance.anim.durations.scale } @@ -81,6 +85,8 @@ Singleton { return { autoHide: barConfig.autoHide, rounding: barConfig.rounding, + border: barConfig.border, + height: barConfig.height, popouts: { tray: barConfig.popouts.tray, audio: barConfig.popouts.audio, @@ -155,10 +161,10 @@ Singleton { return { enable: dock.enable, height: dock.height, - hoverRegionHeight: dock.hoverRegionHeight, hoverToReveal: dock.hoverToReveal, pinnedApps: dock.pinnedApps, - pinnedOnStartup: dock.pinnedOnStartup + pinnedOnStartup: dock.pinnedOnStartup, + ignoredAppRegexes: dock.ignoredAppRegexes }; } @@ -166,6 +172,7 @@ Singleton { return { logo: general.logo, wallpaperPath: general.wallpaperPath, + desktopIcons: general.desktopIcons, color: { wallust: general.color.wallust, mode: general.color.mode, @@ -182,7 +189,7 @@ Singleton { explorer: general.apps.explorer }, idle: { - timouts: general.idle.timeouts + timeouts: general.idle.timeouts } }; } diff --git a/Config/DockConfig.qml b/Config/DockConfig.qml index 135a3e0..eeaf1de 100644 --- a/Config/DockConfig.qml +++ b/Config/DockConfig.qml @@ -3,8 +3,8 @@ import Quickshell.Io JsonObject { property bool enable: false property real height: 60 - property real hoverRegionHeight: 2 property bool hoverToReveal: true + property list ignoredAppRegexes: [] property list pinnedApps: ["org.kde.dolphin", "kitty",] property bool pinnedOnStartup: false } diff --git a/Config/General.qml b/Config/General.qml index 9169d31..ce9d5f9 100644 --- a/Config/General.qml +++ b/Config/General.qml @@ -6,6 +6,7 @@ JsonObject { } property Color color: Color { } + property bool desktopIcons: false property Idle idle: Idle { } property string logo: "" @@ -29,10 +30,12 @@ JsonObject { component Idle: JsonObject { property list timeouts: [ { + name: "Lock", timeout: 180, idleAction: "lock" }, { + name: "Screen", timeout: 300, idleAction: "dpms off", activeAction: "dpms on" diff --git a/Drawers/Backgrounds.qml b/Drawers/Backgrounds.qml index bcd1488..5def5b9 100644 --- a/Drawers/Backgrounds.qml +++ b/Drawers/Backgrounds.qml @@ -11,8 +11,9 @@ import qs.Modules.Dashboard as Dashboard import qs.Modules.Osd as Osd import qs.Modules.Launcher as Launcher import qs.Modules.Resources as Resources - +import qs.Modules.Drawing as Drawing import qs.Modules.Settings as Settings +import qs.Modules.Dock as Dock Shape { id: root @@ -22,13 +23,15 @@ Shape { required property PersistentProperties visibilities anchors.fill: parent - // anchors.margins: 8 - anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? 0 : bar.implicitHeight + anchors.margins: Config.barConfig.border + anchors.topMargin: bar.implicitHeight + asynchronous: true preferredRendererType: Shape.CurveRenderer - Behavior on anchors.topMargin { - Anim { - } + Drawing.Background { + startX: 0 + startY: wrapper.y - rounding + wrapper: root.panels.drawing } Resources.Background { @@ -45,7 +48,8 @@ Shape { Modules.Background { invertBottomRounding: wrapper.x <= 0 - startX: wrapper.x - 8 + rounding: root.panels.popouts.currentName.startsWith("updates") ? Appearance.rounding.normal : Appearance.rounding.smallest + startX: wrapper.x - rounding startY: wrapper.y wrapper: root.panels.popouts } @@ -92,4 +96,12 @@ Shape { startY: 0 wrapper: root.panels.settings } + + Dock.Background { + id: dock + + startX: (root.width - wrapper.width) / 2 - rounding + startY: root.height + wrapper: root.panels.dock + } } diff --git a/Drawers/Drawing.qml b/Drawers/Drawing.qml new file mode 100644 index 0000000..6a30ff2 --- /dev/null +++ b/Drawers/Drawing.qml @@ -0,0 +1,186 @@ +import QtQuick + +Canvas { + id: root + + property rect dirtyRect: Qt.rect(0, 0, 0, 0) + property bool frameQueued: false + property bool fullRepaintPending: true + property point lastPoint: Qt.point(0, 0) + property real minPointDistance: 2.0 + property color penColor: "white" + property real penWidth: 4 + property var pendingSegments: [] + property bool strokeActive: false + property var strokes: [] + + function appendPoint(x, y) { + if (!strokeActive || strokes.length === 0) + return; + const dx = x - lastPoint.x; + const dy = y - lastPoint.y; + + if ((dx * dx + dy * dy) < (minPointDistance * minPointDistance)) + return; + const x1 = lastPoint.x; + const y1 = lastPoint.y; + const x2 = x; + const y2 = y; + + strokes[strokes.length - 1].push(Qt.point(x2, y2)); + + pendingSegments.push({ + dot: false, + x1: x1, + y1: y1, + x2: x2, + y2: y2 + }); + + lastPoint = Qt.point(x2, y2); + queueDirty(segmentDirtyRect(x1, y1, x2, y2)); + } + + function beginStroke(x, y) { + const p = Qt.point(x, y); + strokes.push([p]); + lastPoint = p; + strokeActive = true; + + pendingSegments.push({ + dot: true, + x: x, + y: y + }); + + queueDirty(pointDirtyRect(x, y)); + } + + function clear() { + strokes = []; + pendingSegments = []; + dirtyRect = Qt.rect(0, 0, 0, 0); + fullRepaintPending = true; + markDirty(Qt.rect(0, 0, width, height)); + } + + function drawDot(ctx, x, y) { + ctx.beginPath(); + ctx.arc(x, y, penWidth / 2, 0, Math.PI * 2); + ctx.fill(); + } + + function drawSegment(ctx, x1, y1, x2, y2) { + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.stroke(); + } + + function endStroke() { + strokeActive = false; + } + + function pointDirtyRect(x, y) { + const pad = penWidth + 2; + return Qt.rect(x - pad, y - pad, pad * 2, pad * 2); + } + + function queueDirty(r) { + dirtyRect = unionRects(dirtyRect, r); + + if (frameQueued) + return; + frameQueued = true; + + requestAnimationFrame(function () { + frameQueued = false; + + if (dirtyRect.width > 0 && dirtyRect.height > 0) { + markDirty(dirtyRect); + dirtyRect = Qt.rect(0, 0, 0, 0); + } + }); + } + + function replayAll(ctx) { + ctx.clearRect(0, 0, width, height); + + for (const stroke of strokes) { + if (!stroke || stroke.length === 0) + continue; + if (stroke.length === 1) { + const p = stroke[0]; + drawDot(ctx, p.x, p.y); + continue; + } + + ctx.beginPath(); + ctx.moveTo(stroke[0].x, stroke[0].y); + for (let i = 1; i < stroke.length; ++i) + ctx.lineTo(stroke[i].x, stroke[i].y); + ctx.stroke(); + } + } + + function requestFullRepaint() { + fullRepaintPending = true; + markDirty(Qt.rect(0, 0, width, height)); + } + + function segmentDirtyRect(x1, y1, x2, y2) { + const pad = penWidth + 2; + const left = Math.min(x1, x2) - pad; + const top = Math.min(y1, y2) - pad; + const right = Math.max(x1, x2) + pad; + const bottom = Math.max(y1, y2) + pad; + return Qt.rect(left, top, right - left, bottom - top); + } + + function unionRects(a, b) { + if (a.width <= 0 || a.height <= 0) + return b; + if (b.width <= 0 || b.height <= 0) + return a; + + const left = Math.min(a.x, b.x); + const top = Math.min(a.y, b.y); + const right = Math.max(a.x + a.width, b.x + b.width); + const bottom = Math.max(a.y + a.height, b.y + b.height); + + return Qt.rect(left, top, right - left, bottom - top); + } + + anchors.fill: parent + contextType: "2d" + renderStrategy: Canvas.Threaded + renderTarget: Canvas.Image + + onHeightChanged: requestFullRepaint() + onPaint: region => { + const ctx = getContext("2d"); + + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + ctx.lineWidth = penWidth; + ctx.strokeStyle = penColor; + ctx.fillStyle = penColor; + + if (fullRepaintPending) { + fullRepaintPending = false; + replayAll(ctx); + pendingSegments = []; + return; + } + + for (const seg of pendingSegments) { + if (seg.dot) + drawDot(ctx, seg.x, seg.y); + else + drawSegment(ctx, seg.x1, seg.y1, seg.x2, seg.y2); + } + + pendingSegments = []; + } + onWidthChanged: requestFullRepaint() +} diff --git a/Drawers/DrawingInput.qml b/Drawers/DrawingInput.qml new file mode 100644 index 0000000..c8f2c18 --- /dev/null +++ b/Drawers/DrawingInput.qml @@ -0,0 +1,58 @@ +import Quickshell +import QtQuick +import qs.Components +import qs.Config + +CustomMouseArea { + id: root + + required property var bar + required property Drawing drawing + required property Panels panels + required property var popout + required property PersistentProperties visibilities + + function inLeftPanel(panel: Item, x: real, y: real): bool { + return x < panel.x + panel.width + Config.barConfig.border && withinPanelHeight(panel, x, y); + } + + function withinPanelHeight(panel: Item, x: real, y: real): bool { + const panelY = panel.y + bar.implicitHeight; + return y >= panelY && y <= panelY + panel.height; + } + + acceptedButtons: Qt.LeftButton | Qt.RightButton + anchors.fill: root.visibilities.isDrawing ? parent : undefined + hoverEnabled: true + visible: root.visibilities.isDrawing + + onPositionChanged: event => { + const x = event.x; + const y = event.y; + + if (event.buttons & Qt.LeftButton) + root.drawing.appendPoint(x, y); + + if (root.inLeftPanel(root.popout, x, y)) { + root.z = -2; + root.panels.drawing.expanded = true; + } + } + onPressed: event => { + const x = event.x; + const y = event.y; + + if (root.visibilities.isDrawing && (event.buttons & Qt.LeftButton)) { + root.panels.drawing.expanded = false; + root.drawing.beginStroke(x, y); + return; + } + + if (event.buttons & Qt.RightButton) + root.drawing.clear(); + } + onReleased: { + if (root.visibilities.isDrawing) + root.drawing.endStroke(); + } +} diff --git a/Drawers/Exclusions.qml b/Drawers/Exclusions.qml new file mode 100644 index 0000000..7c3603f --- /dev/null +++ b/Drawers/Exclusions.qml @@ -0,0 +1,41 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import QtQuick +import qs.Config +import qs.Components + +Scope { + id: root + + required property Item bar + required property ShellScreen screen + + ExclusionZone { + anchors.top: true + exclusiveZone: root.bar.exclusiveZone + } + + ExclusionZone { + anchors.left: true + } + + ExclusionZone { + anchors.right: true + } + + ExclusionZone { + anchors.bottom: true + } + + component ExclusionZone: CustomWindow { + exclusiveZone: Config.barConfig.border + implicitHeight: 1 + implicitWidth: 1 + name: "Bar-Exclusion" + screen: root.screen + + mask: Region { + } + } +} diff --git a/Drawers/Interactions.qml b/Drawers/Interactions.qml index 11f6561..e27a765 100644 --- a/Drawers/Interactions.qml +++ b/Drawers/Interactions.qml @@ -10,6 +10,8 @@ CustomMouseArea { required property Item bar property bool dashboardShortcutActive property point dragStart + required property Drawing drawing + required property DrawingInput input property bool osdShortcutActive required property Panels panels required property BarPopouts.Wrapper popouts @@ -18,15 +20,15 @@ CustomMouseArea { required property PersistentProperties visibilities function inBottomPanel(panel: Item, x: real, y: real): bool { - return y > root.height - panel.height && withinPanelWidth(panel, x, y); + return y > root.height - panel.height - Config.barConfig.border && withinPanelWidth(panel, x, y); } function inLeftPanel(panel: Item, x: real, y: real): bool { - return x < panel.x + panel.width && withinPanelHeight(panel, x, y); + return x < panel.x + panel.width + Config.barConfig.border && withinPanelHeight(panel, x, y); } function inRightPanel(panel: Item, x: real, y: real): bool { - return x > panel.x && withinPanelHeight(panel, x, y); + return x > panel.x - Config.barConfig.border && withinPanelHeight(panel, x, y); } function inTopPanel(panel: Item, x: real, y: real): bool { @@ -51,20 +53,10 @@ CustomMouseArea { anchors.fill: parent hoverEnabled: true - - // onPressed: event => { - // if ( root.popouts.hasCurrent && !inTopPanel( root.popouts, event.x, event.y )) { - // root.popouts.hasCurrent = false; - // } else if (root.visibilities.sidebar && !inRightPanel( panels.sidebar, event.x, event.y )) { - // root.visibilities.sidebar = false; - // } else if (root.visibilities.dashboard && !inTopPanel( panels.dashboard, event.x, event.y )) { - // root.visibilities.dashboard = false; - // } - // } + propagateComposedEvents: true onContainsMouseChanged: { if (!containsMouse) { - // Only hide if not activated by shortcut if (!osdShortcutActive) { visibilities.osd = false; root.panels.osd.hovered = false; @@ -74,8 +66,8 @@ CustomMouseArea { popouts.hasCurrent = false; } - if (Config.barConfig.autoHide && !root.visibilities.sidebar && !root.visibilities.dashboard) - root.visibilities.bar = false; + if (Config.barConfig.autoHide) + bar.isHovered = false; } } onPositionChanged: event => { @@ -87,142 +79,69 @@ CustomMouseArea { const dragX = x - dragStart.x; const dragY = y - dragStart.y; - // Show bar in non-exclusive mode on hover - if (!visibilities.bar && Config.barConfig.autoHide && y < bar.implicitHeight + bar.anchors.topMargin) - visibilities.bar = true; - - if (panels.sidebar.width === 0) { - // Show osd on hover - const showOsd = inRightPanel(panels.osd, x, y); - - // // Always update visibility based on hover if not in shortcut mode - if (!osdShortcutActive) { - visibilities.osd = showOsd; - root.panels.osd.hovered = showOsd; - } else if (showOsd) { - // If hovering over OSD area while in shortcut mode, transition to hover control - osdShortcutActive = false; - root.panels.osd.hovered = true; - } - - // 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)) { - // if (dragX < -Config.session.dragThreshold) - // visibilities.session = true; - // else if (dragX > Config.session.dragThreshold) - // visibilities.session = false; - // - // // Show sidebar on drag if in session area and session is nearly fully visible - // if (showSidebar && panels.session.width >= panels.session.nonAnimWidth && dragX < -Config.sidebar.dragThreshold) - // visibilities.sidebar = true; - // } else if (showSidebar && dragX < -Config.sidebar.dragThreshold) { - // // Show sidebar on drag if not in session area - // visibilities.sidebar = true; - // } - } else { - const outOfSidebar = x < width - panels.sidebar.width; - // Show osd on hover - const showOsd = outOfSidebar && inRightPanel(panels.osd, x, y); - - // Always update visibility based on hover if not in shortcut mode - if (!osdShortcutActive) { - visibilities.osd = showOsd; - root.panels.osd.hovered = showOsd; - } else if (showOsd) { - // If hovering over OSD area while in shortcut mode, transition to hover control - osdShortcutActive = false; - root.panels.osd.hovered = true; - } - // - // // Show/hide session on drag - // if (pressed && outOfSidebar && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { - // if (dragX < -Config.session.dragThreshold) - // visibilities.session = true; - // else if (dragX > Config.session.dragThreshold) - // visibilities.session = false; - // } - // - // // Hide sidebar on drag - // if (pressed && inRightPanel(panels.sidebar, dragStart.x, 0) && dragX > Config.sidebar.dragThreshold) - // visibilities.sidebar = false; + if (root.visibilities.isDrawing && !root.inLeftPanel(root.panels.drawing, x, y)) { + root.input.z = 2; + root.panels.drawing.expanded = false; } - // Show launcher on hover, or show/hide on drag if hover is disabled - // if (Config.launcher.showOnHover) { - // if (!visibilities.launcher && inBottomPanel(panels.launcher, x, y)) - // visibilities.launcher = true; - // } else if (pressed && inBottomPanel(panels.launcher, dragStart.x, dragStart.y) && withinPanelWidth(panels.launcher, x, y)) { - // if (dragY < -Config.launcher.dragThreshold) - // visibilities.launcher = true; - // else if (dragY > Config.launcher.dragThreshold) - // visibilities.launcher = false; - // } - // - // // Show dashboard on hover - // const showDashboard = Config.dashboard.showOnHover && inTopPanel(panels.dashboard, x, y); - // - // // Always update visibility based on hover if not in shortcut mode - // if (!dashboardShortcutActive) { - // visibilities.dashboard = showDashboard; - // } else if (showDashboard) { - // // If hovering over dashboard area while in shortcut mode, transition to hover control - // dashboardShortcutActive = false; - // } - // - // // Show/hide dashboard on drag (for touchscreen devices) - // if (pressed && inTopPanel(panels.dashboard, dragStart.x, dragStart.y) && withinPanelWidth(panels.dashboard, x, y)) { - // if (dragY > Config.dashboard.dragThreshold) - // visibilities.dashboard = true; - // else if (dragY < -Config.dashboard.dragThreshold) - // visibilities.dashboard = false; - // } - // - // // Show utilities on hover - // const showUtilities = inBottomPanel(panels.utilities, x, y); - // - // // Always update visibility based on hover if not in shortcut mode - // if (!utilitiesShortcutActive) { - // visibilities.utilities = showUtilities; - // } else if (showUtilities) { - // // If hovering over utilities area while in shortcut mode, transition to hover control - // utilitiesShortcutActive = false; - // } + if (!visibilities.bar && Config.barConfig.autoHide && y < bar.implicitHeight) + bar.isHovered = true; - // Show popouts on hover - if (y < bar.implicitHeight) { - bar.checkPopout(x); + if (panels.sidebar.width === 0) { + const showOsd = inRightPanel(panels.osd, x, y); + + if (showOsd) { + osdShortcutActive = false; + root.panels.osd.hovered = true; + } + } else { + const outOfSidebar = x < width - panels.sidebar.width; + const showOsd = outOfSidebar && inRightPanel(panels.osd, x, y); + + if (!osdShortcutActive) { + visibilities.osd = showOsd; + root.panels.osd.hovered = showOsd; + } else if (showOsd) { + osdShortcutActive = false; + root.panels.osd.hovered = true; + } + } + + if (!visibilities.dock && !visibilities.launcher && inBottomPanel(panels.dock, x, y)) + visibilities.dock = true; + + if (y < root.bar.implicitHeight) { + root.bar.checkPopout(x); } } - // Monitor individual visibility changes Connections { function onDashboardChanged() { if (root.visibilities.dashboard) { - // Dashboard became visible, immediately check if this should be shortcut mode const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY); if (!inDashboardArea) { root.dashboardShortcutActive = true; } + root.visibilities.settings = false; root.visibilities.sidebar = false; root.popouts.hasCurrent = false; } else { - // Dashboard hidden, clear shortcut flag root.dashboardShortcutActive = false; - // root.visibilities.bar = false; } } + function onIsDrawingChanged() { + if (!root.visibilities.isDrawing) + root.drawing.clear(); + } + function onLauncherChanged() { - // If launcher is hidden, clear shortcut flags for dashboard and OSD if (!root.visibilities.launcher) { root.dashboardShortcutActive = false; root.osdShortcutActive = false; root.utilitiesShortcutActive = false; - // Also hide dashboard and OSD if they're not being hovered const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); if (!inOsdArea) { @@ -230,17 +149,20 @@ CustomMouseArea { root.panels.osd.hovered = false; } } + + if (root.visibilities.launcher) { + root.visibilities.dock = false; + root.visibilities.settings = false; + } } function onOsdChanged() { if (root.visibilities.osd) { - // OSD became visible, immediately check if this should be shortcut mode const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); if (!inOsdArea) { root.osdShortcutActive = true; } } else { - // OSD hidden, clear shortcut flag root.osdShortcutActive = false; } } @@ -249,6 +171,18 @@ CustomMouseArea { if (root.visibilities.resources && root.popouts.currentName.startsWith("audio")) { root.popouts.hasCurrent = false; } + + if (root.visibilities.resources) + root.visibilities.settings = false; + } + + function onSettingsChanged() { + if (root.visibilities.settings) { + root.visibilities.resources = false; + root.visibilities.dashboard = false; + root.panels.popouts.hasCurrent = false; + root.visibilities.launcher = false; + } } function onSidebarChanged() { @@ -260,13 +194,11 @@ CustomMouseArea { function onUtilitiesChanged() { if (root.visibilities.utilities) { - // Utilities became visible, immediately check if this should be shortcut mode const inUtilitiesArea = root.inBottomPanel(root.panels.utilities, root.mouseX, root.mouseY); if (!inUtilitiesArea) { root.utilitiesShortcutActive = true; } } else { - // Utilities hidden, clear shortcut flag root.utilitiesShortcutActive = false; } } diff --git a/Drawers/Panels.qml b/Drawers/Panels.qml index 7dea587..fd79941 100644 --- a/Drawers/Panels.qml +++ b/Drawers/Panels.qml @@ -11,6 +11,8 @@ import qs.Components.Toast as Toasts import qs.Modules.Launcher as Launcher import qs.Modules.Resources as Resources import qs.Modules.Settings as Settings +import qs.Modules.Drawing as Drawing +import qs.Modules.Dock as Dock import qs.Config Item { @@ -18,6 +20,9 @@ Item { required property Item bar readonly property alias dashboard: dashboard + readonly property alias dock: dock + readonly property alias drawing: drawing + required property Canvas drawingItem readonly property alias launcher: launcher readonly property alias notifications: notifications readonly property alias osd: osd @@ -31,13 +36,8 @@ Item { required property PersistentProperties visibilities anchors.fill: parent - // anchors.margins: 8 - anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? 0 : bar.implicitHeight - - Behavior on anchors.topMargin { - Anim { - } - } + anchors.margins: Config.barConfig.border + anchors.topMargin: bar.implicitHeight Resources.Wrapper { id: resources @@ -47,6 +47,16 @@ Item { visibilities: root.visibilities } + Drawing.Wrapper { + id: drawing + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + drawing: root.drawingItem + screen: root.screen + visibilities: root.visibilities + } + Osd.Wrapper { id: osd @@ -133,6 +143,17 @@ Item { anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top panels: root + screen: root.screen + visibilities: root.visibilities + } + + Dock.Wrapper { + id: dock + + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + panels: root + screen: root.screen visibilities: root.visibilities } } diff --git a/Drawers/Bar.qml b/Drawers/Windows.qml similarity index 58% rename from Drawers/Bar.qml rename to Drawers/Windows.qml index 02d3e3c..98772c7 100644 --- a/Drawers/Bar.qml +++ b/Drawers/Windows.qml @@ -21,30 +21,25 @@ Variants { required property var modelData - PanelWindow { - id: bar + Exclusions { + bar: bar + screen: scope.modelData + } + CustomWindow { + id: win + + readonly property bool hasFullscreen: Hypr.monitorFor(screen)?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) property var root: Quickshell.shellDir - property bool trayMenuVisible: false WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.resources ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None - WlrLayershell.namespace: "ZShell-Bar" + WlrLayershell.keyboardFocus: visibilities.dock || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.resources ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None color: "transparent" contentItem.focus: true + mask: visibilities.isDrawing ? null : region + name: "Bar" screen: scope.modelData - mask: Region { - id: region - - height: bar.screen.height - backgroundRect.implicitHeight - intersection: Intersection.Xor - regions: popoutRegions.instances - width: bar.width - x: 0 - y: Config.barConfig.autoHide && !visibilities.bar ? 4 : backgroundRect.height - } - contentItem.Keys.onEscapePressed: { if (Config.barConfig.autoHide) visibilities.bar = false; @@ -54,22 +49,23 @@ Variants { visibilities.settings = false; visibilities.resources = false; } + onHasFullscreenChanged: { + visibilities.launcher = false; + visibilities.dashboard = false; + visibilities.osd = false; + visibilities.settings = false; + visibilities.resources = false; + } - PanelWindow { - id: exclusionZone + Region { + id: region - WlrLayershell.exclusionMode: Config.barConfig.autoHide ? ExclusionMode.Ignore : ExclusionMode.Auto - WlrLayershell.layer: WlrLayer.Bottom - WlrLayershell.namespace: "ZShell-Bar-Exclusion" - color: "transparent" - implicitHeight: backgroundRect.height - screen: bar.screen - - anchors { - left: true - right: true - top: true - } + height: win.height - bar.implicitHeight - Config.barConfig.border + intersection: Intersection.Xor + regions: popoutRegions.instances + width: win.width - Config.barConfig.border * 2 + x: Config.barConfig.border + y: bar.implicitHeight } anchors { @@ -90,16 +86,16 @@ Variants { height: modelData.height intersection: Intersection.Subtract width: modelData.width - x: modelData.x - y: modelData.y + backgroundRect.implicitHeight + x: modelData.x + Config.barConfig.border + y: modelData.y + bar.implicitHeight } } HyprlandFocusGrab { id: focusGrab - active: visibilities.resources || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu")) - windows: [bar] + active: visibilities.dock || visibilities.resources || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu")) + windows: [win] onCleared: { visibilities.launcher = false; @@ -108,6 +104,7 @@ Variants { visibilities.osd = false; visibilities.settings = false; visibilities.resources = false; + visibilities.dock = false; panels.popouts.hasCurrent = false; } } @@ -117,6 +114,8 @@ Variants { property bool bar property bool dashboard + property bool dock + property bool isDrawing property bool launcher property bool notif: NotifServer.popups.length > 0 property bool osd @@ -130,7 +129,7 @@ Variants { Binding { property: "bar" target: visibilities - value: visibilities.sidebar || visibilities.dashboard || visibilities.osd || visibilities.notif || visibilities.resources + value: visibilities.sidebar || visibilities.dashboard || visibilities.osd || visibilities.notif || visibilities.resources || visibilities.settings || bar.isHovered when: Config.barConfig.autoHide } @@ -146,68 +145,66 @@ Variants { } Border { - bar: backgroundRect + bar: bar visibilities: visibilities } Backgrounds { - bar: backgroundRect + bar: bar panels: panels visibilities: visibilities + z: 1 } } + Drawing { + id: drawing + + anchors.fill: parent + z: 2 + } + + DrawingInput { + id: input + + bar: bar + drawing: drawing + panels: panels + popout: panels.drawing + visibilities: visibilities + z: 2 + } + Interactions { id: mouseArea anchors.fill: parent - bar: barLoader + bar: bar + drawing: drawing + input: input panels: panels popouts: panels.popouts screen: scope.modelData visibilities: visibilities + z: 1 Panels { id: panels - bar: backgroundRect + bar: bar + drawingItem: drawing screen: scope.modelData visibilities: visibilities } - CustomRect { - id: backgroundRect - - property Wrapper popouts: panels.popouts + BarLoader { + id: bar anchors.left: parent.left anchors.right: parent.right - anchors.top: parent.top - anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? -30 : 0 - color: "transparent" - implicitHeight: barLoader.implicitHeight - radius: 0 - - Behavior on anchors.topMargin { - Anim { - } - } - Behavior on color { - CAnim { - } - } - - BarLoader { - id: barLoader - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - bar: bar - popouts: panels.popouts - screen: scope.modelData - visibilities: visibilities - } + popouts: panels.popouts + screen: scope.modelData + visibilities: visibilities } } } diff --git a/Helpers/AppSearch.qml b/Helpers/AppSearch.qml new file mode 100644 index 0000000..400b3d9 --- /dev/null +++ b/Helpers/AppSearch.qml @@ -0,0 +1,150 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io +import "../scripts/levendist.js" as Levendist +import "../scripts/fuzzysort.js" as Fuzzy +import qs.Config + +Singleton { + id: root + + readonly property list list: Array.from(DesktopEntries.applications.values).filter((app, index, self) => index === self.findIndex(t => (t.id === app.id))) + readonly property var preppedIcons: list.map(a => ({ + name: Fuzzy.prepare(`${a.icon} `), + entry: a + })) + readonly property var preppedNames: list.map(a => ({ + name: Fuzzy.prepare(`${a.name} `), + entry: a + })) + property var regexSubstitutions: [ + { + "regex": /^steam_app_(\d+)$/, + "replace": "steam_icon_$1" + }, + { + "regex": /Minecraft.*/, + "replace": "minecraft" + }, + { + "regex": /.*polkit.*/, + "replace": "system-lock-screen" + }, + { + "regex": /gcr.prompter/, + "replace": "system-lock-screen" + } + ] + readonly property real scoreGapThreshold: 0.1 + readonly property real scoreThreshold: 0.6 + property var substitutions: ({ + "code-url-handler": "visual-studio-code", + "Code": "visual-studio-code", + "gnome-tweaks": "org.gnome.tweaks", + "pavucontrol-qt": "pavucontrol", + "wps": "wps-office2019-kprometheus", + "wpsoffice": "wps-office2019-kprometheus", + "footclient": "foot" + }) + + function bestFuzzyEntry(search: string, preppedList: list, key: string): var { + const results = Fuzzy.go(search, preppedList, { + key: key, + threshold: root.scoreThreshold, + limit: 2 + }); + + if (!results || results.length === 0) + return null; + + const best = results[0]; + const second = results.length > 1 ? results[1] : null; + + if (second && (best.score - second.score) < root.scoreGapThreshold) + return null; + + return best.obj.entry; + } + + function fuzzyQuery(search: string, preppedList: list): var { + const entry = bestFuzzyEntry(search, preppedList, "name"); + return entry ? [entry] : []; + } + + function getKebabNormalizedAppName(str: string): string { + return str.toLowerCase().replace(/\s+/g, "-"); + } + + function getReverseDomainNameAppName(str: string): string { + return str.split('.').slice(-1)[0]; + } + + function getUndescoreToKebabAppName(str: string): string { + return str.toLowerCase().replace(/_/g, "-"); + } + + function guessIcon(str) { + if (!str || str.length == 0) + return "image-missing"; + + if (iconExists(str)) + return str; + + const entry = DesktopEntries.byId(str); + if (entry) + return entry.icon; + + const heuristicEntry = DesktopEntries.heuristicLookup(str); + if (heuristicEntry) + return heuristicEntry.icon; + + if (substitutions[str]) + return substitutions[str]; + if (substitutions[str.toLowerCase()]) + return substitutions[str.toLowerCase()]; + + for (let i = 0; i < regexSubstitutions.length; i++) { + const substitution = regexSubstitutions[i]; + const replacedName = str.replace(substitution.regex, substitution.replace); + if (replacedName != str) + return replacedName; + } + + const lowercased = str.toLowerCase(); + if (iconExists(lowercased)) + return lowercased; + + const reverseDomainNameAppName = getReverseDomainNameAppName(str); + if (iconExists(reverseDomainNameAppName)) + return reverseDomainNameAppName; + + const lowercasedDomainNameAppName = reverseDomainNameAppName.toLowerCase(); + if (iconExists(lowercasedDomainNameAppName)) + return lowercasedDomainNameAppName; + + const kebabNormalizedGuess = getKebabNormalizedAppName(str); + if (iconExists(kebabNormalizedGuess)) + return kebabNormalizedGuess; + + const undescoreToKebabGuess = getUndescoreToKebabAppName(str); + if (iconExists(undescoreToKebabGuess)) + return undescoreToKebabGuess; + + const iconSearchResult = fuzzyQuery(str, preppedIcons); + if (iconSearchResult && iconExists(iconSearchResult.icon)) + return iconSearchResult.icon; + + const nameSearchResult = root.fuzzyQuery(str, preppedNames); + if (nameSearchResult && iconExists(nameSearchResult.icon)) + return nameSearchResult.icon; + + return "application-x-executable"; + } + + function iconExists(iconName) { + if (!iconName || iconName.length == 0) + return false; + return (Quickshell.iconPath(iconName, true).length > 0) && !iconName.includes("image-missing"); + } +} diff --git a/Helpers/DesktopUtils.qml b/Helpers/DesktopUtils.qml new file mode 100644 index 0000000..657eb73 --- /dev/null +++ b/Helpers/DesktopUtils.qml @@ -0,0 +1,201 @@ +pragma Singleton +import Quickshell + +Singleton { + id: root + + function getAppId(fileName) { + return fileName.endsWith(".desktop") ? fileName.replace(".desktop", "") : null; + } + + function getFileType(fileName, isDir) { + if (isDir) + return "directory"; + let ext = fileName.includes('.') ? fileName.split('.').pop().toLowerCase() : ""; + if (ext === "desktop") + return "desktop"; + + const map = { + "image": ["png", "jpg", "jpeg", "svg", "gif", "bmp", "webp", "ico", "tiff", "tif", "heic", "heif", "raw", "psd", "ai", "xcf"], + "video": ["mp4", "mkv", "webm", "avi", "mov", "flv", "wmv", "m4v", "mpg", "mpeg", "3gp", "vob", "ogv", "ts"], + "audio": ["mp3", "wav", "flac", "aac", "ogg", "m4a", "wma", "opus", "alac", "mid", "midi", "amr"], + "archive": ["zip", "tar", "gz", "rar", "7z", "xz", "bz2", "tgz", "iso", "img", "dmg", "deb", "rpm", "apk"], + "document": ["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "odt", "ods", "odp", "rtf", "epub", "mobi", "djvu"], + "text": ["txt", "md", "rst", "tex", "log", "json", "xml", "yaml", "yml", "toml", "ini", "conf", "cfg", "env", "csv", "tsv"], + "code": ["qml", "cpp", "c", "h", "hpp", "py", "js", "ts", "jsx", "tsx", "java", "rs", "go", "rb", "php", "cs", "swift", "kt", "sh", "bash", "zsh", "fish", "html", "htm", "css", "scss", "sass", "less", "vue", "svelte", "sql", "graphql", "lua", "pl", "dart", "r", "dockerfile", "make"], + "executable": ["exe", "msi", "bat", "cmd", "appimage", "run", "bin", "out", "so", "dll"], + "font": ["ttf", "otf", "woff", "woff2"] + }; + + for (const [type, extensions] of Object.entries(map)) { + if (extensions.includes(ext)) + return type; + } + return "unknown"; + } + + function getIconName(fileName, isDir) { + if (isDir) + return "folder"; + let ext = fileName.includes('.') ? fileName.split('.').pop().toLowerCase() : ""; + + const map = { + // Images + "png": "image-x-generic", + "jpg": "image-x-generic", + "jpeg": "image-x-generic", + "svg": "image-svg+xml", + "gif": "image-x-generic", + "bmp": "image-x-generic", + "webp": "image-x-generic", + "ico": "image-x-generic", + "tiff": "image-x-generic", + "tif": "image-x-generic", + "heic": "image-x-generic", + "heif": "image-x-generic", + "raw": "image-x-generic", + "psd": "image-vnd.adobe.photoshop", + "ai": "application-illustrator", + "xcf": "image-x-xcf", + + // Vidéos + "mp4": "video-x-generic", + "mkv": "video-x-generic", + "webm": "video-x-generic", + "avi": "video-x-generic", + "mov": "video-x-generic", + "flv": "video-x-generic", + "wmv": "video-x-generic", + "m4v": "video-x-generic", + "mpg": "video-x-generic", + "mpeg": "video-x-generic", + "3gp": "video-x-generic", + "vob": "video-x-generic", + "ogv": "video-x-generic", + "ts": "video-x-generic", + + // Audio + "mp3": "audio-x-generic", + "wav": "audio-x-generic", + "flac": "audio-x-generic", + "aac": "audio-x-generic", + "ogg": "audio-x-generic", + "m4a": "audio-x-generic", + "wma": "audio-x-generic", + "opus": "audio-x-generic", + "alac": "audio-x-generic", + "mid": "audio-midi", + "midi": "audio-midi", + "amr": "audio-x-generic", + + // Archives & Images + "zip": "application-zip", + "tar": "application-x-tar", + "gz": "application-gzip", + "rar": "application-vnd.rar", + "7z": "application-x-7z-compressed", + "xz": "application-x-xz", + "bz2": "application-x-bzip2", + "tgz": "application-x-compressed-tar", + "iso": "application-x-cd-image", + "img": "application-x-cd-image", + "dmg": "application-x-apple-diskimage", + "deb": "application-vnd.debian.binary-package", + "rpm": "application-x-rpm", + "apk": "application-vnd.android.package-archive", + + // Documents + "pdf": "application-pdf", + "doc": "application-msword", + "docx": "application-vnd.openxmlformats-officedocument.wordprocessingml.document", + "xls": "application-vnd.ms-excel", + "xlsx": "application-vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "ppt": "application-vnd.ms-powerpoint", + "pptx": "application-vnd.openxmlformats-officedocument.presentationml.presentation", + "odt": "application-vnd.oasis.opendocument.text", + "ods": "application-vnd.oasis.opendocument.spreadsheet", + "odp": "application-vnd.oasis.opendocument.presentation", + "rtf": "application-rtf", + "epub": "application-epub+zip", + "mobi": "application-x-mobipocket-ebook", + "djvu": "image-vnd.djvu", + "csv": "text-csv", + "tsv": "text-tab-separated-values", + + // Data & Config + "txt": "text-x-generic", + "md": "text-markdown", + "rst": "text-x-rst", + "tex": "text-x-tex", + "log": "text-x-log", + "json": "application-json", + "xml": "text-xml", + "yaml": "text-x-yaml", + "yml": "text-x-yaml", + "toml": "text-x-toml", + "ini": "text-x-generic", + "conf": "text-x-generic", + "cfg": "text-x-generic", + "env": "text-x-generic", + + // Code + "qml": "text-x-qml", + "cpp": "text-x-c++src", + "c": "text-x-csrc", + "h": "text-x-chdr", + "hpp": "text-x-c++hdr", + "py": "text-x-python", + "js": "text-x-javascript", + "ts": "text-x-typescript", + "jsx": "text-x-javascript", + "tsx": "text-x-typescript", + "java": "text-x-java", + "rs": "text-x-rust", + "go": "text-x-go", + "rb": "text-x-ruby", + "php": "application-x-php", + "cs": "text-x-csharp", + "swift": "text-x-swift", + "kt": "text-x-kotlin", + "sh": "application-x-shellscript", + "bash": "application-x-shellscript", + "zsh": "application-x-shellscript", + "fish": "application-x-shellscript", + "html": "text-html", + "htm": "text-html", + "css": "text-css", + "scss": "text-x-scss", + "sass": "text-x-sass", + "less": "text-x-less", + "vue": "text-html", + "svelte": "text-html", + "sql": "application-x-sql", + "graphql": "text-x-generic", + "lua": "text-x-lua", + "pl": "text-x-perl", + "dart": "text-x-dart", + "r": "text-x-r", + "dockerfile": "text-x-generic", + "make": "text-x-makefile", + + // Executables + "exe": "application-x-executable", + "msi": "application-x-msi", + "bat": "application-x-ms-dos-executable", + "cmd": "application-x-ms-dos-executable", + "appimage": "application-x-executable", + "run": "application-x-executable", + "bin": "application-x-executable", + "out": "application-x-executable", + "so": "application-x-sharedlib", + "dll": "application-x-sharedlib", + + // Fonts + "ttf": "font-x-generic", + "otf": "font-x-generic", + "woff": "font-x-generic", + "woff2": "font-x-generic" + }; + return map[ext] || "text-x-generic"; + } +} diff --git a/Helpers/FileUtils.qml b/Helpers/FileUtils.qml new file mode 100644 index 0000000..9690a6c --- /dev/null +++ b/Helpers/FileUtils.qml @@ -0,0 +1,28 @@ +pragma Singleton +import Quickshell + +Singleton { + id: root + + function fileNameForPath(str) { + if (typeof str !== "string") + return ""; + const trimmed = trimFileProtocol(str); + return trimmed.split(/[\\/]/).pop(); + } + + function trimFileExt(str) { + if (typeof str !== "string") + return ""; + const trimmed = trimFileProtocol(str); + const lastDot = trimmed.lastIndexOf("."); + if (lastDot > -1 && lastDot > trimmed.lastIndexOf("/")) { + return trimmed.slice(0, lastDot); + } + return trimmed; + } + + function trimFileProtocol(str) { + return str.startsWith("file://") ? str.slice(7) : str; + } +} diff --git a/Helpers/ModeScheduler.qml b/Helpers/ModeScheduler.qml index e4851f5..9ca6d40 100644 --- a/Helpers/ModeScheduler.qml +++ b/Helpers/ModeScheduler.qml @@ -33,7 +33,7 @@ Singleton { } function applyLightMode() { - if (Config.general.color.neovimColors) { + if (Config.general.color.schemeGeneration) { Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${WallpaperPath.currentWallpaperPath}`, "--thumbnail-path", `${Paths.cache}/imagecache/thumbnail.jpg`, "--output", `${Paths.state}/scheme.json`, "--scheme", `${Config.colors.schemeType}`, "--mode", "light"]); } else { Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--preset", `${DynamicColors.scheme}:${DynamicColors.flavour}`, "--output", `${Paths.state}/scheme.json`, "--mode", "light"]); diff --git a/Helpers/Picker.qml b/Helpers/Picker.qml index 802b4ac..39cf4f1 100644 --- a/Helpers/Picker.qml +++ b/Helpers/Picker.qml @@ -141,7 +141,7 @@ MouseArea { sy = ssy; ex = x; ey = y; - } else { + } else if (!saveTimer.running) { checkClientRects(x, y); } } @@ -154,7 +154,7 @@ MouseArea { return; if (root.loader.freeze) { - save(); + saveTimer.start(); } else { overlay.visible = border.visible = false; screencopy.visible = false; @@ -162,6 +162,16 @@ MouseArea { } } + Timer { + id: saveTimer + + interval: 25 + repeat: false + running: false + + onTriggered: root.save() + } + SequentialAnimation { id: closeAnim @@ -217,9 +227,10 @@ MouseArea { paintCursor: false onHasContentChanged: { - if (hasContent && !root.loader.freeze) { + if (hasContent) { overlay.visible = border.visible = true; - root.save(); + if (!root.loader.freeze) + root.save(); } } } @@ -233,6 +244,7 @@ MouseArea { layer.enabled: true opacity: 0.3 radius: root.realRounding + visible: false layer.effect: MultiEffect { maskEnabled: true @@ -270,6 +282,7 @@ MouseArea { implicitHeight: selectionRect.implicitHeight + root.realBorderWidth * 2 implicitWidth: selectionRect.implicitWidth + root.realBorderWidth * 2 radius: root.realRounding > 0 ? root.realRounding + root.realBorderWidth : 0 + visible: false x: selectionRect.x - root.realBorderWidth y: selectionRect.y - root.realBorderWidth diff --git a/Helpers/SettingsDropdowns.qml b/Helpers/SettingsDropdowns.qml new file mode 100644 index 0000000..4a4ac27 --- /dev/null +++ b/Helpers/SettingsDropdowns.qml @@ -0,0 +1,60 @@ +pragma Singleton +import QtQuick + +QtObject { + id: root + + property Item activeMenu: null + property Item activeTrigger: null + + function close(menu) { + if (!menu) + return; + + if (activeMenu === menu) { + activeMenu = null; + activeTrigger = null; + } + + menu.expanded = false; + } + + function closeActive() { + if (activeMenu) + activeMenu.expanded = false; + + activeMenu = null; + activeTrigger = null; + } + + function forget(menu) { + if (activeMenu === menu) { + activeMenu = null; + activeTrigger = null; + } + } + + function hit(item, scenePos) { + if (!item || !item.visible) + return false; + + const p = item.mapFromItem(null, scenePos.x, scenePos.y); + return item.contains(p); + } + + function open(menu, trigger) { + if (activeMenu && activeMenu !== menu) + activeMenu.expanded = false; + + activeMenu = menu; + activeTrigger = trigger || null; + menu.expanded = true; + } + + function toggle(menu, trigger) { + if (activeMenu === menu && menu.expanded) + close(menu); + else + open(menu, trigger); + } +} diff --git a/Helpers/SystemUsage.qml b/Helpers/SystemUsage.qml index d9234e9..e613060 100644 --- a/Helpers/SystemUsage.qml +++ b/Helpers/SystemUsage.qml @@ -17,7 +17,6 @@ Singleton { property var disks: [] property real gpuMemTotal: 0 property real gpuMemUsed - property string gpuName: "" property real gpuPerc property real gpuTemp readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType @@ -80,8 +79,29 @@ Singleton { onTriggered: { stat.reload(); meminfo.reload(); + if (root.gpuType === "GENERIC") + gpuUsage.running = true; + } + } + + Timer { + interval: 60000 * 120 + repeat: true + running: true + triggeredOnStart: true + + onTriggered: { storage.running = true; - gpuUsage.running = true; + } + } + + Timer { + interval: Config.dashboard.resourceUpdateInterval * 5 + repeat: true + running: root.refCount > 0 + triggeredOnStart: true + + onTriggered: { sensors.running = true; } } @@ -112,10 +132,13 @@ Singleton { const totalDiff = total - root.lastCpuTotal; const idleDiff = idle - root.lastCpuIdle; - root.cpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0; + const newCpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0; root.lastCpuTotal = total; root.lastCpuIdle = idle; + + if (Math.abs(newCpuPerc - root.cpuPerc) >= 0.01) + root.cpuPerc = newCpuPerc; } } } @@ -127,8 +150,14 @@ Singleton { onLoaded: { const data = text(); - root.memTotal = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1; - root.memUsed = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0; + const total = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1; + const used = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0; + + if (root.memTotal !== total) + root.memTotal = total; + + if (Math.abs(used - root.memUsed) >= 16384) + root.memUsed = used; } } @@ -274,22 +303,54 @@ Singleton { } } + Process { + id: gpuUsageNvidia + + command: ["/usr/bin/nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu,memory.used", "--format=csv,noheader,nounits", "-lms", "1000"] + running: root.refCount > 0 && root.gpuType === "NVIDIA" + + stdout: SplitParser { + onRead: data => { + const parts = String(data).trim().split(/\s*,\s*/); + if (parts.length < 3) + return; + + const usageRaw = parseInt(parts[0], 10); + const tempRaw = parseInt(parts[1], 10); + const memRaw = parseInt(parts[2], 10); + + if (!Number.isFinite(usageRaw) || !Number.isFinite(tempRaw) || !Number.isFinite(memRaw)) + return; + + const newGpuPerc = Math.max(0, Math.min(1, usageRaw / 100)); + const newGpuTemp = tempRaw; + const newGpuMemUsed = root.gpuMemTotal > 0 ? Math.max(0, Math.min(1, memRaw / root.gpuMemTotal)) : 0; + + // Only publish meaningful changes to avoid needless binding churn / repaints + if (Math.abs(root.gpuPerc - newGpuPerc) >= 0.01) + root.gpuPerc = newGpuPerc; + + if (Math.abs(root.gpuTemp - newGpuTemp) >= 1) + root.gpuTemp = newGpuTemp; + + if (Math.abs(root.gpuMemUsed - newGpuMemUsed) >= 0.01) + root.gpuMemUsed = newGpuMemUsed; + } + } + } + Process { id: gpuUsage - command: root.gpuType === "GENERIC" ? ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent"] : root.gpuType === "NVIDIA" ? ["nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu,memory.used", "--format=csv,noheader,nounits"] : ["echo"] + command: root.gpuType === "GENERIC" ? ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent"] : ["echo"] stdout: StdioCollector { onStreamFinished: { + console.log("this is running"); if (root.gpuType === "GENERIC") { const percs = text.trim().split("\n"); const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0); root.gpuPerc = sum / percs.length / 100; - } else if (root.gpuType === "NVIDIA") { - const [usage, temp, mem] = text.trim().split(","); - root.gpuPerc = parseInt(usage, 10) / 100; - root.gpuTemp = parseInt(temp, 10); - root.gpuMemUsed = parseInt(mem, 10) / root.gpuMemTotal; } else { root.gpuPerc = 0; root.gpuTemp = 0; @@ -314,7 +375,7 @@ Singleton { // If AMD Tdie pattern failed, try fallback on Tctl cpuTemp = text.match(/Tctl:\s+((\+|-)[0-9.]+)(°| )C/); - if (cpuTemp) + if (cpuTemp && Math.abs(parseFloat(cpuTemp[1]) - root.cpuTemp) >= 0.5) root.cpuTemp = parseFloat(cpuTemp[1]); if (root.gpuType !== "GENERIC") diff --git a/Helpers/TaskbarApps.qml b/Helpers/TaskbarApps.qml new file mode 100644 index 0000000..8f3f78b --- /dev/null +++ b/Helpers/TaskbarApps.qml @@ -0,0 +1,103 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Wayland +import qs.Config + +Singleton { + id: root + + property var apps: { + const pinnedApps = uniq((Config.dock.pinnedApps ?? []).map(normalizeId)); + const openMap = buildOpenMap(); + const openIds = [...openMap.keys()]; + const sessionOrder = uniq(root.unpinnedOrder.map(normalizeId)); + + const orderedUnpinned = sessionOrder.filter(id => openIds.includes(id) && !pinnedApps.includes(id)).concat(openIds.filter(id => !pinnedApps.includes(id) && !sessionOrder.includes(id))); + + const out = []; + + for (const appId of pinnedApps) { + out.push({ + appId, + pinned: true, + toplevels: openMap.get(appId) ?? [] + }); + } + + if (pinnedApps.length > 0) { + out.push({ + appId: root.separatorId, + pinned: false, + toplevels: [] + }); + } + + for (const appId of orderedUnpinned) { + out.push({ + appId, + pinned: false, + toplevels: openMap.get(appId) ?? [] + }); + } + + return out; + } + readonly property string separatorId: "__dock_separator__" + property var unpinnedOrder: [] + + function buildOpenMap() { + const ignoredRegexes = (Config.dock.ignoredAppRegexes ?? []).map(pattern => new RegExp(pattern, "i")); + + return ToplevelManager.toplevels.values.reduce((map, toplevel) => { + if (ignoredRegexes.some(re => re.test(toplevel.appId))) + return map; + + const appId = normalizeId(toplevel.appId); + if (!appId) + return map; + + map.set(appId, (map.get(appId) ?? []).concat([toplevel])); + return map; + }, new Map()); + } + + function commitVisualOrder(ids) { + const orderedIds = uniq(ids.map(normalizeId)); + const separatorIndex = orderedIds.indexOf(root.separatorId); + + const pinnedApps = (separatorIndex === -1 ? [] : orderedIds.slice(0, separatorIndex)).filter(id => id !== root.separatorId); + + const visibleUnpinned = orderedIds.slice(separatorIndex === -1 ? 0 : separatorIndex + 1).filter(id => id !== root.separatorId); + + Config.dock.pinnedApps = pinnedApps; + root.unpinnedOrder = visibleUnpinned.concat(root.unpinnedOrder.map(normalizeId).filter(id => !pinnedApps.includes(id) && !visibleUnpinned.includes(id))); + Config.saveNoToast(); + } + + function isPinned(appId) { + return uniq((Config.dock.pinnedApps ?? []).map(normalizeId)).includes(normalizeId(appId)); + } + + function normalizeId(appId) { + if (appId === root.separatorId) + return root.separatorId; + + return String(appId ?? "").toLowerCase(); + } + + function togglePin(appId) { + const id = normalizeId(appId); + const pinnedApps = uniq((Config.dock.pinnedApps ?? []).map(normalizeId)); + const pinned = pinnedApps.includes(id); + + Config.dock.pinnedApps = pinned ? pinnedApps.filter(x => x !== id) : pinnedApps.concat([id]); + + root.unpinnedOrder = pinned ? [id].concat(root.unpinnedOrder.map(normalizeId).filter(x => x !== id)) : root.unpinnedOrder.map(normalizeId).filter(x => x !== id); + } + + function uniq(ids) { + return (ids ?? []).filter((id, i, arr) => id && arr.indexOf(id) === i); + } +} diff --git a/Helpers/Updates.qml b/Helpers/Updates.qml new file mode 100644 index 0000000..7ad83b9 --- /dev/null +++ b/Helpers/Updates.qml @@ -0,0 +1,123 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Paths + +Singleton { + id: root + + property int availableUpdates: 0 + property bool loaded + property double now: Date.now() + property var updates: ({}) + + function formatUpdateTime(timestamp) { + const diffMs = root.now - timestamp; + const minuteMs = 60 * 1000; + const hourMs = 60 * minuteMs; + const dayMs = 24 * hourMs; + + if (diffMs < minuteMs) + return "just now"; + + if (diffMs < hourMs) + return Math.floor(diffMs / minuteMs) + " min ago"; + + if (diffMs < 48 * hourMs) + return Math.floor(diffMs / hourMs) + " hr ago"; + + return Qt.formatDateTime(new Date(timestamp), "dd hh:mm"); + } + + onUpdatesChanged: { + if (!root.loaded) + return; + + saveTimer.restart(); + availableUpdates = Object.keys(updates).length; + } + + Timer { + interval: 1 + repeat: true + running: true + + onTriggered: { + if (!root.loaded) + return; + + updatesProc.running = true; + interval = 5000; + } + } + + Timer { + interval: 60000 + repeat: true + running: true + + onTriggered: root.now = Date.now() + } + + Process { + id: updatesProc + + command: ["checkupdates"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + const output = this.text; + const lines = output.trim().split("\n").filter(line => line.length > 0); + + const oldMap = root.updates; + const now = Date.now(); + + root.updates = lines.reduce((acc, pkg) => { + acc[pkg] = oldMap[pkg] ?? now; + return acc; + }, {}); + root.availableUpdates = lines.length; + } + } + } + + Timer { + id: saveTimer + + interval: 1000 + + onTriggered: storage.setText(JSON.stringify(root.updates)) + } + + FileView { + id: storage + + path: `${Paths.state}/updates.json` + + onLoadFailed: err => { + if (err === FileViewError.FileNotFound) { + root.updates = ({}); + root.loaded = true; + setText("{}"); + return; + } + + root.updates = ({}); + root.loaded = true; + } + onLoaded: { + try { + const data = JSON.parse(text()); + root.updates = data && typeof data === "object" && !Array.isArray(data) ? data : {}; + } catch (e) { + root.updates = ({}); + } + + root.loaded = true; + } + } +} diff --git a/Helpers/Wallpapers.qml b/Helpers/Wallpapers.qml index 498bb5f..afbe9e8 100644 --- a/Helpers/Wallpapers.qml +++ b/Helpers/Wallpapers.qml @@ -19,20 +19,20 @@ Searcher { function preview(path: string): void { previewPath = path; if (Config.general.color.schemeGeneration) - Quickshell.execDetached(["sh", "-c", `zshell-cli scheme generate --image-path ${previewPath} --scheme ${Config.colors.schemeType} --mode ${Config.general.color.mode}`]); + Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${previewPath}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.general.color.mode}`]); showPreview = true; } function setWallpaper(path: string): void { actualCurrent = path; WallpaperPath.currentWallpaperPath = path; - Quickshell.execDetached(["sh", "-c", `zshell-cli wallpaper lockscreen --input-image=${root.actualCurrent} --output-path=${Paths.state}/lockscreen_bg.png --blur-amount=${Config.lock.blurAmount}`]); + Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]); } function stopPreview(): void { showPreview = false; if (Config.general.color.schemeGeneration) - Quickshell.execDetached(["sh", "-c", `zshell-cli scheme generate --image-path ${root.actualCurrent} --scheme ${Config.colors.schemeType} --mode ${Config.general.color.mode}`]); + Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.general.color.mode}`]); } extraOpts: useFuzzy ? ({}) : ({ diff --git a/Modules/AudioWidget.qml b/Modules/AudioWidget.qml index d9031cb..2a99d5f 100644 --- a/Modules/AudioWidget.qml +++ b/Modules/AudioWidget.qml @@ -7,15 +7,16 @@ import qs.Modules import qs.Config import qs.Components -Item { +CustomRect { id: root property color barColor: DynamicColors.palette.m3primary property color textColor: DynamicColors.palette.m3onSurface - anchors.bottom: parent.bottom - anchors.top: parent.top + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2 implicitWidth: 150 + radius: Appearance.rounding.full Behavior on implicitWidth { NumberAnimation { @@ -24,34 +25,27 @@ Item { } } - CustomRect { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - color: DynamicColors.tPalette.m3surfaceContainer - implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2) - radius: height / 2 - } + Component.onCompleted: console.log(root.height, root.implicitHeight) RowLayout { id: layout - anchors.fill: parent + anchors.left: parent.left anchors.leftMargin: Appearance.padding.small - anchors.rightMargin: Appearance.padding.small * 2 anchors.verticalCenter: parent.verticalCenter + width: root.implicitWidth - Appearance.padding.small * 3 MaterialIcon { Layout.alignment: Qt.AlignVCenter animate: true color: Audio.muted ? DynamicColors.palette.m3error : root.textColor - font.pointSize: 14 + font.pointSize: Appearance.font.size.larger text: Audio.muted ? "volume_off" : "volume_up" } CustomRect { Layout.fillWidth: true - color: "#50ffffff" + color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) implicitHeight: 4 radius: 20 @@ -74,13 +68,13 @@ Item { Layout.alignment: Qt.AlignVCenter animate: true color: (Audio.sourceMuted ?? false) ? DynamicColors.palette.m3error : root.textColor - font.pointSize: 14 + font.pointSize: Appearance.font.size.larger text: Audio.sourceMuted ? "mic_off" : "mic" } CustomRect { Layout.fillWidth: true - color: "#50ffffff" + color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) implicitHeight: 4 radius: 20 diff --git a/Modules/Background.qml b/Modules/Background.qml index 483ce94..a726d17 100644 --- a/Modules/Background.qml +++ b/Modules/Background.qml @@ -9,7 +9,7 @@ ShapePath { readonly property bool flatten: wrapper.height < rounding * 2 property real ibr: invertBottomRounding ? -1 : 1 required property bool invertBottomRounding - readonly property real rounding: 8 + property real rounding: Appearance.rounding.smallest readonly property real roundingY: flatten ? wrapper.height / 2 : rounding required property Wrapper wrapper diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml new file mode 100644 index 0000000..df30e99 --- /dev/null +++ b/Modules/Bar/Bar.qml @@ -0,0 +1,218 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Modules +import qs.Config +import qs.Helpers +import qs.Modules.UPower +import qs.Modules.Network +import qs.Modules.Updates + +RowLayout { + id: root + + required property Wrapper popouts + required property ShellScreen screen + readonly property int vPadding: 6 + required property PersistentProperties visibilities + + function checkPopout(x: real): void { + const ch = childAt(x, height / 2) as WrappedLoader; + + if (!ch || ch?.id === "spacer") { + if (!popouts.currentName.startsWith("traymenu")) + popouts.hasCurrent = false; + return; + } + + if (visibilities.sidebar || visibilities.dashboard || visibilities.resources || visibilities.settings) + return; + + const id = ch.id; + const top = ch.x; + const item = ch.item; + const itemWidth = item.implicitWidth; + + if (id === "audio" && Config.barConfig.popouts.audio) { + popouts.currentName = "audio"; + popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); + popouts.hasCurrent = true; + } else if (id === "network" && Config.barConfig.popouts.network) { + popouts.currentName = "network"; + popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); + popouts.hasCurrent = true; + } else if (id === "upower" && Config.barConfig.popouts.upower) { + popouts.currentName = "upower"; + popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); + popouts.hasCurrent = true; + } else if (id === "updates") { + popouts.currentName = "updates"; + popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); + popouts.hasCurrent = true; + } + } + + spacing: Appearance.spacing.small + + Repeater { + id: repeater + + // model: Config.barConfig.entries.filted(n => n.index > 50).sort(n => n.index) + model: Config.barConfig.entries + + DelegateChooser { + role: "id" + + DelegateChoice { + roleValue: "spacer" + + delegate: WrappedLoader { + Layout.fillWidth: true + } + } + + DelegateChoice { + roleValue: "workspaces" + + delegate: WrappedLoader { + sourceComponent: Workspaces { + screen: root.screen + } + } + } + + DelegateChoice { + roleValue: "audio" + + delegate: WrappedLoader { + sourceComponent: AudioWidget { + } + } + } + + DelegateChoice { + roleValue: "tray" + + delegate: WrappedLoader { + sourceComponent: TrayWidget { + loader: root + popouts: root.popouts + } + } + } + + DelegateChoice { + roleValue: "resources" + + delegate: WrappedLoader { + sourceComponent: Resources { + visibilities: root.visibilities + } + } + } + + DelegateChoice { + roleValue: "updates" + + delegate: WrappedLoader { + sourceComponent: UpdatesWidget { + } + } + } + + DelegateChoice { + roleValue: "notifBell" + + delegate: WrappedLoader { + sourceComponent: NotifBell { + popouts: root.popouts + visibilities: root.visibilities + } + } + } + + DelegateChoice { + roleValue: "clock" + + delegate: WrappedLoader { + sourceComponent: Clock { + loader: root + popouts: root.popouts + visibilities: root.visibilities + } + } + } + + DelegateChoice { + roleValue: "activeWindow" + + delegate: WrappedLoader { + sourceComponent: WindowTitle { + bar: root + } + } + } + + DelegateChoice { + roleValue: "upower" + + delegate: WrappedLoader { + sourceComponent: UPowerWidget { + } + } + } + + DelegateChoice { + roleValue: "network" + + delegate: WrappedLoader { + sourceComponent: NetworkWidget { + } + } + } + + DelegateChoice { + roleValue: "media" + + delegate: WrappedLoader { + sourceComponent: MediaWidget { + } + } + } + } + } + + component WrappedLoader: Loader { + required property bool enabled + required property string id + required property int index + + function findFirstEnabled(): Item { + const count = repeater.count; + for (let i = 0; i < count; i++) { + const item = repeater.itemAt(i); + if (item?.enabled) + return item; + } + return null; + } + + function findLastEnabled(): Item { + for (let i = repeater.count - 1; i >= 0; i--) { + const item = repeater.itemAt(i); + if (item?.enabled) + return item; + } + return null; + } + + Layout.alignment: Qt.AlignVCenter + Layout.leftMargin: findFirstEnabled() === this ? root.vPadding : 0 + Layout.rightMargin: findLastEnabled() === this ? root.vPadding : 0 + active: enabled + visible: enabled + } +} diff --git a/Modules/Bar/BarLoader.qml b/Modules/Bar/BarLoader.qml index 2c3a70a..0e4863f 100644 --- a/Modules/Bar/BarLoader.qml +++ b/Modules/Bar/BarLoader.qml @@ -11,224 +11,72 @@ import qs.Helpers import qs.Modules.UPower import qs.Modules.Network -RowLayout { +Item { id: root - required property PanelWindow bar + readonly property int contentHeight: Config.barConfig.height + padding * 2 + readonly property int exclusiveZone: Config.barConfig.autoHide ? Config.barConfig.border : contentHeight + property bool isHovered + readonly property int padding: Math.max(Appearance.padding.smaller, Config.barConfig.border) required property Wrapper popouts required property ShellScreen screen + readonly property bool shouldBeVisible: (!Config.barConfig.autoHide || visibilities.bar || isHovered) readonly property int vPadding: 6 required property PersistentProperties visibilities function checkPopout(x: real): void { - const ch = childAt(x, 2) as WrappedLoader; - - if (!ch || ch?.id === "spacer") { - if (!popouts.currentName.startsWith("traymenu")) - popouts.hasCurrent = false; - return; - } - - if (visibilities.sidebar || visibilities.dashboard || visibilities.resources) - return; - - const id = ch.id; - const top = ch.x; - const item = ch.item; - const itemWidth = item.implicitWidth; - - if (id === "audio" && Config.barConfig.popouts.audio) { - popouts.currentName = "audio"; - popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); - popouts.hasCurrent = true; - } else if (id === "network" && Config.barConfig.popouts.network) { - popouts.currentName = "network"; - popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); - popouts.hasCurrent = true; - } else if (id === "upower" && Config.barConfig.popouts.upower) { - popouts.currentName = "upower"; - popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x); - popouts.hasCurrent = true; - } + content.item?.checkPopout(x); } - implicitHeight: 34 + implicitHeight: Config.barConfig.border + visible: height > Config.barConfig.border - CustomShortcut { - name: "toggle-overview" + states: State { + name: "visible" + when: root.shouldBeVisible - onPressed: { - Hyprland.refreshWorkspaces(); - Hyprland.refreshMonitors(); - if (root.popouts.hasCurrent && root.popouts.currentName === "overview") { - root.popouts.hasCurrent = false; - } else { - root.popouts.currentName = "overview"; - root.popouts.currentCenter = root.width / 2; - root.popouts.hasCurrent = true; - } + PropertyChanges { + root.implicitHeight: root.contentHeight } } + transitions: [ + Transition { + from: "" + to: "visible" - Repeater { - id: repeater - - // model: Config.barConfig.entries.filted(n => n.index > 50).sort(n => n.index) - model: Config.barConfig.entries - - DelegateChooser { - role: "id" - - DelegateChoice { - roleValue: "spacer" - - delegate: WrappedLoader { - Layout.fillWidth: true - } + Anim { + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects + property: "implicitHeight" + target: root } + }, + Transition { + from: "visible" + to: "" - DelegateChoice { - roleValue: "workspaces" - - delegate: WrappedLoader { - sourceComponent: Workspaces { - bar: root.bar - } - } - } - - DelegateChoice { - roleValue: "audio" - - delegate: WrappedLoader { - sourceComponent: AudioWidget { - } - } - } - - DelegateChoice { - roleValue: "tray" - - delegate: WrappedLoader { - sourceComponent: TrayWidget { - bar: root.bar - loader: root - popouts: root.popouts - } - } - } - - DelegateChoice { - roleValue: "resources" - - delegate: WrappedLoader { - sourceComponent: Resources { - visibilities: root.visibilities - } - } - } - - DelegateChoice { - roleValue: "updates" - - delegate: WrappedLoader { - sourceComponent: UpdatesWidget { - } - } - } - - DelegateChoice { - roleValue: "notifBell" - - delegate: WrappedLoader { - sourceComponent: NotifBell { - popouts: root.popouts - visibilities: root.visibilities - } - } - } - - DelegateChoice { - roleValue: "clock" - - delegate: WrappedLoader { - sourceComponent: Clock { - loader: root - popouts: root.popouts - visibilities: root.visibilities - } - } - } - - DelegateChoice { - roleValue: "activeWindow" - - delegate: WrappedLoader { - sourceComponent: WindowTitle { - bar: root - monitor: Brightness.getMonitorForScreen(root.screen) - } - } - } - - DelegateChoice { - roleValue: "upower" - - delegate: WrappedLoader { - sourceComponent: UPowerWidget { - } - } - } - - DelegateChoice { - roleValue: "network" - - delegate: WrappedLoader { - sourceComponent: NetworkWidget { - } - } - } - - DelegateChoice { - roleValue: "media" - - delegate: WrappedLoader { - sourceComponent: MediaWidget { - } - } + Anim { + duration: MaterialEasing.expressiveEffectsTime + easing.bezierCurve: MaterialEasing.expressiveEffects + property: "implicitHeight" + target: root } } - } + ] - component WrappedLoader: Loader { - required property bool enabled - required property string id - required property int index + Loader { + id: content - function findFirstEnabled(): Item { - const count = repeater.count; - for (let i = 0; i < count; i++) { - const item = repeater.itemAt(i); - if (item?.enabled) - return item; - } - return null; + active: root.shouldBeVisible || root.visible + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + + sourceComponent: Bar { + height: root.contentHeight + popouts: root.popouts + screen: root.screen + visibilities: root.visibilities } - - function findLastEnabled(): Item { - for (let i = repeater.count - 1; i >= 0; i--) { - const item = repeater.itemAt(i); - if (item?.enabled) - return item; - } - return null; - } - - Layout.alignment: Qt.AlignVCenter - Layout.fillHeight: true - Layout.leftMargin: findFirstEnabled() === this ? root.vPadding : 0 - Layout.rightMargin: findLastEnabled() === this ? root.vPadding : 0 - active: enabled - visible: enabled } } diff --git a/Modules/Bar/Border.qml b/Modules/Bar/Border.qml index 875d218..ff9e476 100644 --- a/Modules/Bar/Border.qml +++ b/Modules/Bar/Border.qml @@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound import Quickshell import QtQuick import QtQuick.Effects -import qs.Modules import qs.Config import qs.Components @@ -17,7 +16,8 @@ Item { CustomRect { anchors.fill: parent - color: Config.barConfig.autoHide && !root.visibilities.bar ? "transparent" : DynamicColors.palette.m3surface + anchors.margins: -1 + color: DynamicColors.palette.m3surface layer.enabled: true layer.effect: MultiEffect { @@ -38,14 +38,11 @@ Item { Rectangle { anchors.fill: parent - anchors.topMargin: Config.barConfig.autoHide && !root.visibilities.bar ? 4 : root.bar.implicitHeight - topLeftRadius: 8 - topRightRadius: 8 - - Behavior on anchors.topMargin { - Anim { - } - } + anchors.margins: Config.barConfig.border + 1 + anchors.topMargin: root.bar.implicitHeight + 1 + radius: Config.barConfig.border > 0 ? Config.barConfig.rounding : 0 + topLeftRadius: Config.barConfig.rounding + topRightRadius: Config.barConfig.rounding } } } diff --git a/Modules/Calendar/CalendarHeader.qml b/Modules/Calendar/CalendarHeader.qml deleted file mode 100644 index bc7b158..0000000 --- a/Modules/Calendar/CalendarHeader.qml +++ /dev/null @@ -1,73 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import qs.Components -import qs.Config -import qs.Helpers - -RowLayout { - spacing: 12 - - Rectangle { - Layout.preferredHeight: 40 - Layout.preferredWidth: 40 - color: "transparent" - radius: 1000 - - MaterialIcon { - anchors.centerIn: parent - color: DynamicColors.palette.m3onSurface - fill: 1 - font.pointSize: 24 - text: "arrow_back_2" - } - - StateLayer { - onClicked: { - if (Calendar.displayMonth === 0) { - Calendar.displayMonth = 11; - Calendar.displayYear -= 1; - } else { - Calendar.displayMonth -= 1; - } - } - } - } - - CustomText { - Layout.fillWidth: true - color: DynamicColors.palette.m3onSurface - font.pointSize: 14 - font.weight: 600 - horizontalAlignment: Text.AlignHCenter - text: new Date(Calendar.displayYear, Calendar.displayMonth, 1).toLocaleDateString(Qt.locale(), "MMMM yyyy") - } - - Rectangle { - Layout.preferredHeight: 40 - Layout.preferredWidth: 40 - color: "transparent" - radius: 1000 - - MaterialIcon { - anchors.centerIn: parent - color: DynamicColors.palette.m3onSurface - fill: 1 - font.pointSize: 24 - rotation: 180 - text: "arrow_back_2" - } - - StateLayer { - onClicked: { - if (Calendar.displayMonth === 11) { - Calendar.displayMonth = 0; - Calendar.displayYear += 1; - } else { - Calendar.displayMonth += 1; - } - } - } - } -} diff --git a/Modules/Calendar/CalendarPopup.qml b/Modules/Calendar/CalendarPopup.qml deleted file mode 100644 index 0dcf110..0000000 --- a/Modules/Calendar/CalendarPopup.qml +++ /dev/null @@ -1,77 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import qs.Components -import qs.Config -import qs.Helpers - -Item { - id: root - - required property Item wrapper - - implicitHeight: layout.childrenRect.height + layout.anchors.margins * 2 - implicitWidth: layout.childrenRect.width + layout.anchors.margins * 2 - - ColumnLayout { - id: layout - - anchors.centerIn: parent - anchors.margins: 16 - spacing: 16 - - // Header with month/year and navigation - CalendarHeader { - Layout.fillWidth: true - Layout.preferredHeight: childrenRect.height - } - - // Calendar grid - RowLayout { - Layout.fillWidth: true - Layout.preferredHeight: childrenRect.height - spacing: 12 - - ColumnLayout { - Layout.alignment: Qt.AlignTop - Layout.preferredHeight: childrenRect.height - Layout.preferredWidth: weekNumberColumn.width - spacing: 8 - - Item { - Layout.preferredHeight: dayOfWeekRow.height - } - - WeekNumberColumn { - id: weekNumberColumn - - Layout.alignment: Qt.AlignTop - Layout.preferredHeight: weekNumbers.values.length * 44 - } - } - - ColumnLayout { - Layout.alignment: Qt.AlignTop - Layout.fillWidth: true - Layout.preferredHeight: childrenRect.height - spacing: 8 - - DayOfWeekRow { - id: dayOfWeekRow - - Layout.fillWidth: true - Layout.preferredHeight: 30 - locale: Qt.locale() - } - - MonthGrid { - Layout.preferredHeight: childrenRect.height - Layout.preferredWidth: childrenRect.width - locale: Qt.locale() - wrapper: root.wrapper - } - } - } - } -} diff --git a/Modules/Calendar/DayOfWeekRow.qml b/Modules/Calendar/DayOfWeekRow.qml deleted file mode 100644 index 10fa64c..0000000 --- a/Modules/Calendar/DayOfWeekRow.qml +++ /dev/null @@ -1,42 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import qs.Components -import qs.Config -import qs.Helpers - -RowLayout { - id: root - - required property var locale - - spacing: 4 - - Repeater { - model: 7 - - Item { - readonly property string dayName: { - // Get the day name for this column - const dayIndex = (index + Calendar.weekStartDay) % 7; - return root.locale.dayName(dayIndex, Locale.ShortFormat); - } - required property int index - - Layout.fillWidth: true - Layout.preferredHeight: 30 - - CustomText { - anchors.centerIn: parent - color: DynamicColors.palette.m3onSurfaceVariant - font.pointSize: 11 - font.weight: 500 - horizontalAlignment: Text.AlignHCenter - opacity: 0.8 - text: parent.dayName - verticalAlignment: Text.AlignVCenter - } - } - } -} diff --git a/Modules/Calendar/MonthGrid.qml b/Modules/Calendar/MonthGrid.qml deleted file mode 100644 index 5ab370b..0000000 --- a/Modules/Calendar/MonthGrid.qml +++ /dev/null @@ -1,118 +0,0 @@ -pragma ComponentBehavior: Bound - -import Quickshell -import QtQuick -import QtQuick.Layouts -import qs.Components -import qs.Config -import qs.Helpers - -GridLayout { - id: root - - required property var locale - required property Item wrapper - - columnSpacing: 4 - columns: 7 - rowSpacing: 4 - uniformCellHeights: true - uniformCellWidths: true - - Repeater { - id: repeater - - model: ScriptModel { - values: Calendar.getWeeksForMonth(Calendar.displayMonth, Calendar.displayYear) - - Behavior on values { - SequentialAnimation { - id: switchAnim - - ParallelAnimation { - Anim { - from: 1.0 - property: "opacity" - to: 0.0 - } - - Anim { - from: 1.0 - property: "scale" - to: 0.8 - } - } - - PropertyAction { - } - - ParallelAnimation { - Anim { - from: 0.0 - property: "opacity" - to: 1.0 - } - - Anim { - from: 0.8 - property: "scale" - to: 1.0 - } - } - } - } - } - - Rectangle { - required property int index - required property var modelData - - Layout.preferredHeight: width - Layout.preferredWidth: 40 - color: { - if (modelData.isToday) { - return DynamicColors.palette.m3primaryContainer; - } - return "transparent"; - } - radius: 1000 - - Behavior on color { - ColorAnimation { - duration: 200 - } - } - - CustomText { - anchors.centerIn: parent - color: { - if (parent.modelData.isToday) { - return DynamicColors.palette.m3onPrimaryContainer; - } - return DynamicColors.palette.m3onSurface; - } - horizontalAlignment: Text.AlignHCenter - opacity: parent.modelData.isCurrentMonth ? 1.0 : 0.4 - text: parent.modelData.day.toString() - verticalAlignment: Text.AlignVCenter - - Behavior on color { - ColorAnimation { - duration: 200 - } - } - Behavior on opacity { - NumberAnimation { - duration: 200 - } - } - } - } - } - - component Anim: NumberAnimation { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - target: root - } -} diff --git a/Modules/Calendar/WeekNumberColumn.qml b/Modules/Calendar/WeekNumberColumn.qml deleted file mode 100644 index 44e30b6..0000000 --- a/Modules/Calendar/WeekNumberColumn.qml +++ /dev/null @@ -1,45 +0,0 @@ -pragma ComponentBehavior: Bound - -import Quickshell -import QtQuick -import QtQuick.Layouts -import qs.Components -import qs.Config -import qs.Helpers - -ColumnLayout { - id: root - - readonly property var weekNumbers: Calendar.getWeekNumbers(Calendar.displayMonth, Calendar.displayYear) - - spacing: 4 - - Repeater { - model: ScriptModel { - values: root.weekNumbers - } - - Item { - id: weekItem - - required property int index - required property var modelData - - Layout.alignment: Qt.AlignHCenter - Layout.preferredHeight: 40 - Layout.preferredWidth: 20 - - CustomText { - id: weekText - - anchors.centerIn: parent - color: DynamicColors.palette.m3onSurfaceVariant - font.pointSize: 10 - horizontalAlignment: Text.AlignHCenter - opacity: 0.5 - text: weekItem.modelData - verticalAlignment: Text.AlignVCenter - } - } - } -} diff --git a/Modules/Clock.qml b/Modules/Clock.qml index 9598bc7..78dd270 100644 --- a/Modules/Clock.qml +++ b/Modules/Clock.qml @@ -6,43 +6,36 @@ import qs.Modules import qs.Helpers as Helpers import qs.Components -Item { +CustomRect { id: root required property RowLayout loader required property Wrapper popouts required property PersistentProperties visibilities - anchors.bottom: parent.bottom - anchors.top: parent.top - implicitWidth: timeText.contentWidth + 5 * 2 + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2 + implicitWidth: timeText.contentWidth + Appearance.padding.normal * 2 + radius: Appearance.rounding.full - CustomRect { - anchors.bottomMargin: 3 - anchors.fill: parent - anchors.topMargin: 3 - color: "transparent" - radius: 4 + CustomText { + id: timeText - CustomText { - id: timeText + anchors.centerIn: parent + color: DynamicColors.palette.m3onSurface + text: Time.dateStr - anchors.centerIn: parent - color: DynamicColors.palette.m3onSurface - text: Time.dateStr - - Behavior on color { - CAnim { - } - } - } - - StateLayer { - acceptedButtons: Qt.LeftButton - - onClicked: { - root.visibilities.dashboard = !root.visibilities.dashboard; + Behavior on color { + CAnim { } } } + + StateLayer { + acceptedButtons: Qt.LeftButton + + onClicked: { + root.visibilities.dashboard = !root.visibilities.dashboard; + } + } } diff --git a/Modules/Content.qml b/Modules/Content.qml index 80e16d0..0040b7a 100644 --- a/Modules/Content.qml +++ b/Modules/Content.qml @@ -5,10 +5,10 @@ import Quickshell.Services.SystemTray import QtQuick import qs.Config import qs.Components -import qs.Modules.Calendar import qs.Modules.WSOverview import qs.Modules.Network import qs.Modules.UPower +import qs.Modules.Updates Item { id: root @@ -69,14 +69,6 @@ Item { } } - Popout { - name: "calendar" - - sourceComponent: CalendarPopup { - wrapper: root.wrapper - } - } - Popout { name: "overview" @@ -101,6 +93,14 @@ Item { wrapper: root.wrapper } } + + Popout { + name: "updates" + + sourceComponent: UpdatesPopout { + wrapper: root.wrapper + } + } } component Popout: Loader { diff --git a/Modules/Dashboard/Background.qml b/Modules/Dashboard/Background.qml index b97793b..cf42103 100644 --- a/Modules/Dashboard/Background.qml +++ b/Modules/Dashboard/Background.qml @@ -7,7 +7,7 @@ ShapePath { id: root readonly property bool flatten: wrapper.height < rounding * 2 - readonly property real rounding: 8 + readonly property real rounding: Appearance.rounding.normal readonly property real roundingY: flatten ? wrapper.height / 2 : rounding required property Wrapper wrapper diff --git a/Modules/Dashboard/Content.qml b/Modules/Dashboard/Content.qml index ad1fd3d..867004e 100644 --- a/Modules/Dashboard/Content.qml +++ b/Modules/Dashboard/Content.qml @@ -40,7 +40,7 @@ Item { anchors.right: parent.right anchors.top: parent.top color: "transparent" - radius: 6 + radius: Appearance.rounding.normal - anchors.margins Item { id: view diff --git a/Modules/Dashboard/Wrapper.qml b/Modules/Dashboard/Wrapper.qml index 930b8a6..11858d6 100644 --- a/Modules/Dashboard/Wrapper.qml +++ b/Modules/Dashboard/Wrapper.qml @@ -72,17 +72,21 @@ Item { } } - Loader { - id: content + CustomClippingRect { + anchors.fill: parent - active: true - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - visible: false + Loader { + id: content - sourceComponent: Content { - state: root.dashState - visibilities: root.visibilities + active: true + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + visible: false + + sourceComponent: Content { + state: root.dashState + visibilities: root.visibilities + } } } } diff --git a/Modules/DesktopIcons/BackgroundContextMenu.qml b/Modules/DesktopIcons/BackgroundContextMenu.qml new file mode 100644 index 0000000..a9eefa8 --- /dev/null +++ b/Modules/DesktopIcons/BackgroundContextMenu.qml @@ -0,0 +1,183 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.Hyprland +import qs.Components +import qs.Config +import qs.Paths +import qs.Helpers + +Item { + id: root + + anchors.fill: parent + z: 998 + visible: false + + property real menuX: 0 + property real menuY: 0 + + MouseArea { + anchors.fill: parent + onClicked: root.close() + } + + CustomClippingRect { + id: popupBackground + readonly property real padding: 4 + + x: root.menuX + y: root.menuY + + color: DynamicColors.tPalette.m3surface + radius: Appearance.rounding.normal + + implicitWidth: menuLayout.implicitWidth + padding * 2 + implicitHeight: menuLayout.implicitHeight + padding * 2 + + Behavior on opacity { Anim {} } + opacity: root.visible ? 1 : 0 + + ColumnLayout { + id: menuLayout + anchors.centerIn: parent + spacing: 0 + + CustomRect { + Layout.preferredWidth: 200 + radius: popupBackground.radius - popupBackground.padding + implicitHeight: openTerminalRow.implicitHeight + Appearance.padding.small * 2 + + RowLayout { + id: openTerminalRow + spacing: 8 + anchors.fill: parent + anchors.leftMargin: Appearance.padding.smaller + + MaterialIcon { text: "terminal"; font.pointSize: 20 } + CustomText { text: "Open terminal"; Layout.fillWidth: true } + } + + StateLayer { + anchors.fill: parent + + onClicked: { + Quickshell.execDetached([Config.general.apps.terminal, "--working-directory", FileUtils.trimFileProtocol(Paths.desktop)]) + root.close() + } + } + } + + CustomRect { + Layout.fillWidth: true + implicitHeight: 1 + color: DynamicColors.palette.m3outlineVariant + Layout.topMargin: 4 + Layout.bottomMargin: 4 + } + + CustomRect { + Layout.fillWidth: true + radius: popupBackground.radius - popupBackground.padding + implicitHeight: settingsRow.implicitHeight + Appearance.padding.small * 2 + + RowLayout { + id: settingsRow + spacing: 8 + anchors.fill: parent + anchors.leftMargin: Appearance.padding.smaller + + MaterialIcon { text: "settings"; font.pointSize: 20 } + CustomText { text: "ZShell settings"; Layout.fillWidth: true } + } + + StateLayer { + anchors.fill: parent + + onClicked: { + const visibilities = Visibilities.getForActive(); + visibilities.settings = true; + root.close() + } + } + } + + CustomRect { + Layout.fillWidth: true + implicitHeight: 1 + color: DynamicColors.palette.m3outlineVariant + Layout.topMargin: 4 + Layout.bottomMargin: 4 + } + + CustomRect { + Layout.fillWidth: true + radius: popupBackground.radius - popupBackground.padding + implicitHeight: logoutRow.implicitHeight + Appearance.padding.small * 2 + + RowLayout { + id: logoutRow + spacing: 8 + anchors.fill: parent + anchors.leftMargin: Appearance.padding.smaller + + MaterialIcon { text: "logout"; font.pointSize: 20 } + CustomText { text: "Logout"; Layout.fillWidth: true } + } + + StateLayer { + anchors.fill: parent + + onClicked: { + Hyprland.dispatch("global quickshell:sessionOpen") + root.close() + } + } + } + + // CustomRect { + // Layout.fillWidth: true + // implicitHeight: 1 + // color: DynamicColors.palette.m3outlineVariant + // Layout.topMargin: 4 + // Layout.bottomMargin: 4 + // } + // + // CustomRect { + // Layout.fillWidth: true + // radius: popupBackground.radius - popupBackground.padding + // implicitHeight: desktopIconsRow.implicitHeight + Appearance.padding.small * 2 + // + // RowLayout { + // id: desktopIconsRow + // spacing: 8 + // anchors.fill: parent + // anchors.leftMargin: Appearance.padding.smaller + // + // MaterialIcon { text: Config.options.background.showDesktopIcons ? "visibility_off" : "visibility"; font.pointSize: 20 } + // CustomText { text: Config.options.background.showDesktopIcons ? "Hide icons" : "Show icons"; Layout.fillWidth: true } + // } + // + // StateLayer { + // anchors.fill: parent + // + // onClicked: { + // Config.options.background.showDesktopIcons = !Config.options.background.showDesktopIcons + // root.close() + // } + // } + // } + } + } + + function openAt(mouseX, mouseY, parentW, parentH) { + menuX = Math.floor(Math.min(mouseX, parentW - popupBackground.implicitWidth)) + menuY = Math.floor(Math.min(mouseY, parentH - popupBackground.implicitHeight)) + visible = true + } + + function close() { + visible = false + } +} diff --git a/Modules/DesktopIcons/DesktopIconContextMenu.qml b/Modules/DesktopIcons/DesktopIconContextMenu.qml new file mode 100644 index 0000000..e32cf9e --- /dev/null +++ b/Modules/DesktopIcons/DesktopIconContextMenu.qml @@ -0,0 +1,232 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import qs.Components +import qs.Config + +Item { + id: contextMenu + + anchors.fill: parent + z: 999 + visible: false + + property string targetFilePath: "" + property bool targetIsDir: false + property var targetAppEntry: null + + property var targetPaths: [] + + signal openFileRequested(string path, bool isDir) + signal renameRequested(string path) + + property real menuX: 0 + property real menuY: 0 + + CustomClippingRect { + id: popupBackground + readonly property real padding: Appearance.padding.small + + x: contextMenu.menuX + y: contextMenu.menuY + + color: DynamicColors.tPalette.m3surface + radius: Appearance.rounding.normal + + implicitWidth: menuLayout.implicitWidth + padding * 2 + implicitHeight: menuLayout.implicitHeight + padding * 2 + + Behavior on opacity { Anim {} } + opacity: contextMenu.visible ? 1 : 0 + + ColumnLayout { + id: menuLayout + anchors.centerIn: parent + spacing: 0 + + CustomRect { + Layout.preferredWidth: 160 + radius: popupBackground.radius - popupBackground.padding + implicitHeight: openRow.implicitHeight + Appearance.padding.small * 2 + + RowLayout { + id: openRow + spacing: 8 + anchors.fill: parent + anchors.leftMargin: Appearance.padding.smaller + + MaterialIcon { text: "open_in_new"; font.pointSize: 20 } + CustomText { text: "Open"; Layout.fillWidth: true } + } + + StateLayer { + anchors.fill: parent + + onClicked: { + for (let i = 0; i < contextMenu.targetPaths.length; i++) { + let p = contextMenu.targetPaths[i]; + if (p === contextMenu.targetFilePath) { + if (p.endsWith(".desktop") && contextMenu.targetAppEntry) contextMenu.targetAppEntry.execute() + else contextMenu.openFileRequested(p, contextMenu.targetIsDir) + } else { + Quickshell.execDetached(["xdg-open", p]) + } + } + contextMenu.close() + } + } + } + + CustomRect { + Layout.fillWidth: true + radius: popupBackground.radius - popupBackground.padding + implicitHeight: openWithRow.implicitHeight + Appearance.padding.small * 2 + + RowLayout { + id: openWithRow + spacing: 8 + anchors.fill: parent + anchors.leftMargin: Appearance.padding.smaller + + MaterialIcon { text: contextMenu.targetIsDir ? "terminal" : "apps"; font.pointSize: 20 } + CustomText { text: contextMenu.targetIsDir ? "Open in terminal" : "Open with..."; Layout.fillWidth: true } + } + + StateLayer { + anchors.fill: parent + + onClicked: { + if (contextMenu.targetIsDir) { + Quickshell.execDetached([Config.general.apps.terminal, "--working-directory", contextMenu.targetFilePath]) + } else { + Quickshell.execDetached(["xdg-open", contextMenu.targetFilePath]) + } + contextMenu.close() + } + } + } + + CustomRect { + Layout.fillWidth: true + implicitHeight: 1 + color: DynamicColors.palette.m3outlineVariant + Layout.topMargin: 4 + Layout.bottomMargin: 4 + } + + CustomRect { + Layout.fillWidth: true + radius: popupBackground.radius - popupBackground.padding + implicitHeight: copyPathRow.implicitHeight + Appearance.padding.small * 2 + + RowLayout { + id: copyPathRow + spacing: 8 + anchors.fill: parent + anchors.leftMargin: Appearance.padding.smaller + + MaterialIcon { text: "content_copy"; font.pointSize: 20 } + CustomText { text: "Copy path"; Layout.fillWidth: true } + } + + StateLayer { + anchors.fill: parent + + onClicked: { + Quickshell.execDetached(["wl-copy", contextMenu.targetPaths.join("\n")]) + contextMenu.close() + } + } + } + + CustomRect { + Layout.fillWidth: true + visible: contextMenu.targetPaths.length === 1 + radius: popupBackground.radius - popupBackground.padding + implicitHeight: renameRow.implicitHeight + Appearance.padding.small * 2 + + RowLayout { + id: renameRow + spacing: 8 + anchors.fill: parent + anchors.leftMargin: Appearance.padding.smaller + + MaterialIcon { text: "edit"; font.pointSize: 20 } + CustomText { text: "Rename"; Layout.fillWidth: true } + } + + StateLayer { + anchors.fill: parent + + onClicked: { + contextMenu.renameRequested(contextMenu.targetFilePath) + contextMenu.close() + } + } + } + + Rectangle { + Layout.fillWidth: true + implicitHeight: 1 + color: DynamicColors.palette.m3outlineVariant + Layout.topMargin: 4 + Layout.bottomMargin: 4 + } + + CustomRect { + Layout.fillWidth: true + radius: popupBackground.radius - popupBackground.padding + implicitHeight: deleteRow.implicitHeight + Appearance.padding.small * 2 + + RowLayout { + id: deleteRow + spacing: 8 + anchors.fill: parent + anchors.leftMargin: Appearance.padding.smaller + + MaterialIcon { + text: "delete" + font.pointSize: 20 + color: deleteButton.hovered ? DynamicColors.palette.m3onError : DynamicColors.palette.m3error + } + + CustomText { + text: "Move to trash" + Layout.fillWidth: true + color: deleteButton.hovered ? DynamicColors.palette.m3onError : DynamicColors.palette.m3error + } + } + + StateLayer { + id: deleteButton + anchors.fill: parent + color: DynamicColors.tPalette.m3error + + onClicked: { + let cmd = ["gio", "trash"].concat(contextMenu.targetPaths) + Quickshell.execDetached(cmd) + contextMenu.close() + } + } + } + } + } + + function openAt(mouseX, mouseY, path, isDir, appEnt, parentW, parentH, selectionArray) { + targetFilePath = path + targetIsDir = isDir + targetAppEntry = appEnt + + targetPaths = (selectionArray && selectionArray.length > 0) ? selectionArray : [path] + + menuX = Math.floor(Math.min(mouseX, parentW - popupBackground.implicitWidth)) + menuY = Math.floor(Math.min(mouseY, parentH - popupBackground.implicitHeight)) + + visible = true + } + + function close() { + visible = false + } +} diff --git a/Modules/DesktopIcons/DesktopIconDelegate.qml b/Modules/DesktopIcons/DesktopIconDelegate.qml new file mode 100644 index 0000000..1d77c9b --- /dev/null +++ b/Modules/DesktopIcons/DesktopIconDelegate.qml @@ -0,0 +1,274 @@ +import QtQuick +import Quickshell +import Quickshell.Widgets +import qs.Config +import qs.Components +import qs.Helpers + +Item { + id: delegateRoot + + property var appEntry: fileName.endsWith(".desktop") ? DesktopEntries.byId(DesktopUtils.getAppId(fileName)) : null + property bool fileIsDir: model.isDir + property string fileName: model.fileName + property string filePath: model.filePath + property int gridX: model.gridX + property int gridY: model.gridY + property bool isSnapping: snapAnimX.running || snapAnimY.running + property bool lassoActive + property string resolvedIcon: { + if (fileName.endsWith(".desktop")) { + if (appEntry && appEntry.icon && appEntry.icon !== "") + return appEntry.icon; + return AppSearch.guessIcon(DesktopUtils.getAppId(fileName)); + } else if (DesktopUtils.getFileType(fileName, fileIsDir) === "image") { + return "file://" + filePath; + } else { + return DesktopUtils.getIconName(fileName, fileIsDir); + } + } + + function compensateAndSnap(absVisX, absVisY) { + dragContainer.x = absVisX - delegateRoot.x; + dragContainer.y = absVisY - delegateRoot.y; + snapAnimX.start(); + snapAnimY.start(); + } + + function getDragX() { + return dragContainer.x; + } + + function getDragY() { + return dragContainer.y; + } + + height: root.cellHeight + width: root.cellWidth + x: gridX * root.cellWidth + y: gridY * root.cellHeight + + Behavior on x { + enabled: !mouseArea.drag.active && !isSnapping && !root.selectedIcons.includes(filePath) + + Anim { + } + } + Behavior on y { + enabled: !mouseArea.drag.active && !isSnapping && !root.selectedIcons.includes(filePath) + + Anim { + } + } + + Item { + id: dragContainer + + height: parent.height + width: parent.width + + states: State { + when: mouseArea.drag.active + + PropertyChanges { + opacity: 0.8 + scale: 1.1 + target: dragContainer + z: 100 + } + } + transform: Translate { + x: (root.selectedIcons.includes(filePath) && root.dragLeader !== "" && root.dragLeader !== filePath) ? root.groupDragX : 0 + y: (root.selectedIcons.includes(filePath) && root.dragLeader !== "" && root.dragLeader !== filePath) ? root.groupDragY : 0 + } + transitions: Transition { + Anim { + } + } + + onXChanged: { + if (mouseArea.drag.active) { + root.dragLeader = filePath; + root.groupDragX = x; + } + } + onYChanged: { + if (mouseArea.drag.active) { + root.dragLeader = filePath; + root.groupDragY = y; + } + } + + PropertyAnimation { + id: snapAnimX + + duration: 250 + easing.type: Easing.OutCubic + property: "x" + target: dragContainer + to: 0 + } + + PropertyAnimation { + id: snapAnimY + + duration: 250 + easing.type: Easing.OutCubic + property: "y" + target: dragContainer + to: 0 + } + + Column { + anchors.centerIn: parent + spacing: 6 + + IconImage { + anchors.horizontalCenter: parent.horizontalCenter + implicitSize: 48 + source: { + if (delegateRoot.resolvedIcon.startsWith("file://") || delegateRoot.resolvedIcon.startsWith("/")) { + return delegateRoot.resolvedIcon; + } else { + return Quickshell.iconPath(delegateRoot.resolvedIcon, fileIsDir ? "folder" : "text-x-generic"); + } + } + } + + Item { + height: 40 + width: 88 + + CustomText { + anchors.fill: parent + color: "white" + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter + maximumLineCount: 2 + style: Text.Outline + styleColor: "black" + text: (appEntry && appEntry.name !== "") ? appEntry.name : fileName + visible: !renameLoader.active + wrapMode: Text.Wrap + } + + Loader { + id: renameLoader + + active: root.editingFilePath === filePath + anchors.centerIn: parent + height: 24 + width: 110 + + sourceComponent: CustomTextInput { + anchors.fill: parent + anchors.margins: 2 + color: "white" + horizontalAlignment: Text.AlignHCenter + text: fileName + wrapMode: Text.Wrap + + Component.onCompleted: { + forceActiveFocus(); + selectAll(); + } + Keys.onPressed: function (event) { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + if (text.trim() !== "" && text !== fileName) { + let newName = text.trim(); + let newPath = filePath.substring(0, filePath.lastIndexOf('/') + 1) + newName; + + Quickshell.execDetached(["mv", filePath, newPath]); + } + root.editingFilePath = ""; + event.accepted = true; + } else if (event.key === Qt.Key_Escape) { + root.editingFilePath = ""; + event.accepted = true; + } + } + onActiveFocusChanged: { + if (!activeFocus && root.editingFilePath === filePath) { + root.editingFilePath = ""; + } + } + } + } + } + } + + CustomRect { + anchors.fill: parent + anchors.margins: 4 + color: "white" + opacity: root.selectedIcons.includes(filePath) ? 0.2 : 0.0 + radius: 8 + + Behavior on opacity { + Anim { + } + } + } + + MouseArea { + id: mouseArea + + acceptedButtons: Qt.LeftButton | Qt.RightButton + anchors.fill: parent + cursorShape: root.lassoActive ? undefined : Qt.PointingHandCursor + drag.target: dragContainer + hoverEnabled: true + + onClicked: mouse => { + root.forceActiveFocus(); + + if (mouse.button === Qt.RightButton) { + if (!root.selectedIcons.includes(filePath)) { + root.selectedIcons = [filePath]; + } + let pos = mapToItem(root, mouse.x, mouse.y); + root.contextMenu.openAt(pos.x, pos.y, filePath, fileIsDir, appEntry, root.width, root.height, root.selectedIcons); + } else { + root.selectedIcons = [filePath]; + root.contextMenu.close(); + } + } + onDoubleClicked: mouse => { + if (mouse.button === Qt.LeftButton) { + if (filePath.endsWith(".desktop") && appEntry) + appEntry.execute(); + else + root.exec(filePath, fileIsDir); + } + } + onPressed: mouse => { + if (mouse.button === Qt.LeftButton && !root.selectedIcons.includes(filePath)) { + root.selectedIcons = [filePath]; + } + } + onReleased: { + if (drag.active) { + let absoluteX = delegateRoot.x + dragContainer.x; + let absoluteY = delegateRoot.y + dragContainer.y; + let snapX = Math.max(0, Math.round(absoluteX / root.cellWidth)); + let snapY = Math.max(0, Math.round(absoluteY / root.cellHeight)); + + root.performMassDrop(filePath, snapX, snapY); + } + } + + CustomRect { + anchors.fill: parent + anchors.margins: 4 + color: "white" + opacity: parent.containsMouse ? 0.1 : 0.0 + radius: 8 + + Behavior on opacity { + Anim { + } + } + } + } + } +} diff --git a/Modules/DesktopIcons/DesktopIcons.qml b/Modules/DesktopIcons/DesktopIcons.qml new file mode 100644 index 0000000..c06ba69 --- /dev/null +++ b/Modules/DesktopIcons/DesktopIcons.qml @@ -0,0 +1,206 @@ +import QtQuick +import Quickshell +import qs.Modules +import qs.Helpers +import qs.Config +import qs.Components +import qs.Paths +import ZShell.Services + +Item { + id: root + + property int cellHeight: 110 + property int cellWidth: 100 + property var contextMenu: desktopMenu + property string dragLeader: "" + property string editingFilePath: "" + property real groupDragX: 0 + property real groupDragY: 0 + property bool lassoActive: false + property var selectedIcons: [] + property real startX: 0 + property real startY: 0 + + function exec(filePath, isDir) { + const cmd = ["xdg-open", filePath]; + Quickshell.execDetached(cmd); + } + + function performMassDrop(leaderPath, targetX, targetY) { + let maxCol = Math.max(0, Math.floor(gridArea.width / cellWidth) - 1); + let maxRow = Math.max(0, Math.floor(gridArea.height / cellHeight) - 1); + + let visuals = []; + for (let i = 0; i < gridArea.children.length; i++) { + let child = gridArea.children[i]; + if (child.filePath && root.selectedIcons.includes(child.filePath)) { + let isLeader = (root.dragLeader === child.filePath); + let offsetX = isLeader ? child.getDragX() : root.groupDragX; + let offsetY = isLeader ? child.getDragY() : root.groupDragY; + visuals.push({ + childRef: child, + absX: child.x + offsetX, + absY: child.y + offsetY + }); + } + } + + desktopModel.massMove(root.selectedIcons, leaderPath, targetX, targetY, maxCol, maxRow); + + for (let i = 0; i < visuals.length; i++) { + visuals[i].childRef.compensateAndSnap(visuals[i].absX, visuals[i].absY); + } + + root.dragLeader = ""; + root.groupDragX = 0; + root.groupDragY = 0; + } + + focus: true + + Keys.onPressed: event => { + if (event.key === Qt.Key_F2 && selectedIcons.length > 0) + editingFilePath = selectedIcons[0]; + } + + DesktopModel { + id: desktopModel + + Component.onCompleted: loadDirectory(FileUtils.trimFileProtocol(Paths.desktop)) + } + + CustomRect { + id: lasso + + function hideLasso() { + fadeIn.stop(); + fadeOut.start(); + root.lassoActive = false; + } + + function showLasso() { + root.lassoActive = true; + fadeOut.stop(); + visible = true; + fadeIn.start(); + } + + border.color: DynamicColors.palette.m3primary + border.width: 1 + color: DynamicColors.tPalette.m3primary + opacity: 0 + radius: Appearance.rounding.small + visible: false + z: 99 + + NumberAnimation { + id: fadeIn + + duration: 120 + from: 0 + property: "opacity" + target: lasso + to: 1 + } + + SequentialAnimation { + id: fadeOut + + NumberAnimation { + duration: 120 + from: lasso.opacity + property: "opacity" + target: lasso + to: 0 + } + + ScriptAction { + script: lasso.visible = false + } + } + } + + MouseArea { + acceptedButtons: Qt.LeftButton | Qt.RightButton + anchors.fill: parent + + onPositionChanged: mouse => { + if (lasso.visible) { + lasso.x = Math.floor(Math.min(mouse.x, root.startX)); + lasso.y = Math.floor(Math.min(mouse.y, root.startY)); + lasso.width = Math.floor(Math.abs(mouse.x - root.startX)); + lasso.height = Math.floor(Math.abs(mouse.y - root.startY)); + + let minCol = Math.floor((lasso.x - gridArea.x) / cellWidth); + let maxCol = Math.floor((lasso.x + lasso.width - gridArea.x) / cellWidth); + let minRow = Math.floor((lasso.y - gridArea.y) / cellHeight); + let maxRow = Math.floor((lasso.y + lasso.height - gridArea.y) / cellHeight); + + let newSelection = []; + for (let i = 0; i < gridArea.children.length; i++) { + let child = gridArea.children[i]; + if (child.filePath !== undefined && child.gridX >= minCol && child.gridX <= maxCol && child.gridY >= minRow && child.gridY <= maxRow) { + newSelection.push(child.filePath); + } + } + root.selectedIcons = newSelection; + } + } + onPressed: mouse => { + root.editingFilePath = ""; + desktopMenu.close(); + + if (mouse.button === Qt.RightButton) { + root.selectedIcons = []; + bgContextMenu.openAt(mouse.x, mouse.y, root.width, root.height); + } else { + bgContextMenu.close(); + root.selectedIcons = []; + root.startX = Math.floor(mouse.x); + root.startY = Math.floor(mouse.y); + lasso.x = Math.floor(mouse.x); + lasso.y = Math.floor(mouse.y); + lasso.width = 0; + lasso.height = 0; + lasso.showLasso(); + } + } + onReleased: { + lasso.hideLasso(); + } + } + + Item { + id: gridArea + + anchors.fill: parent + anchors.margins: 20 + anchors.topMargin: 40 + visible: true + + Repeater { + model: desktopModel + + delegate: DesktopIconDelegate { + property int itemIndex: index + + lassoActive: root.lassoActive + } + } + } + + DesktopIconContextMenu { + id: desktopMenu + + onOpenFileRequested: (path, isDir) => root.exec(path, isDir) + onRenameRequested: path => { + root.editingFilePath = path; + } + } + + BackgroundContextMenu { + id: bgContextMenu + + } +} diff --git a/Modules/Dock/Content.qml b/Modules/Dock/Content.qml index 6089700..47b1d13 100644 --- a/Modules/Dock/Content.qml +++ b/Modules/Dock/Content.qml @@ -2,6 +2,8 @@ pragma ComponentBehavior: Bound import Quickshell import QtQuick +import QtQml.Models +import qs.Modules.Dock.Parts import qs.Components import qs.Helpers import qs.Config @@ -9,20 +11,310 @@ import qs.Config Item { id: root + readonly property int dockContentWidth: TaskbarApps.apps.reduce((sum, app, i) => sum + (app.appId === TaskbarApps.separatorId ? 1 : Config.dock.height) + (i > 0 ? dockRow.spacing : 0), 0) + property bool dragActive: false + property real dragHeight: Config.dock.height + property real dragStartX: 0 + property real dragStartY: 0 + property real dragWidth: Config.dock.height + property real dragX: 0 + property real dragY: 0 + property string draggedAppId: "" + property var draggedModelData: null + property bool dropAnimating: false readonly property int padding: Appearance.padding.small required property var panels + property var pendingCommitIds: [] readonly property int rounding: Appearance.rounding.large + required property ShellScreen screen required property PersistentProperties visibilities + property var visualIds: [] + + function beginVisualDrag(appId, modelData, item) { + const pos = item.mapToItem(root, 0, 0); + + root.visualIds = TaskbarApps.apps.map(app => app.appId); + root.draggedAppId = appId; + root.draggedModelData = modelData; + root.dragWidth = item.width; + root.dragHeight = item.height; + root.dragStartX = pos.x; + root.dragStartY = pos.y; + root.dragX = pos.x; + root.dragY = pos.y; + root.dragActive = true; + root.dropAnimating = false; + root.pendingCommitIds = []; + } + + function endVisualDrag() { + const ids = root.visualIds.slice(); + const finalIndex = root.visualIds.indexOf(root.draggedAppId); + const finalItem = dockRow.itemAtIndex(finalIndex); + + // Stop sending drag events now, but keep the proxy alive while it settles. + root.dragActive = false; + + // In a dock, the destination delegate should normally be instantiated. + // If not, just finish immediately. + if (!finalItem) { + root.pendingCommitIds = ids; + root.finishVisualDrag(); + return; + } + + const pos = finalItem.mapToItem(root, 0, 0); + + root.pendingCommitIds = ids; + root.dropAnimating = true; + + settleX.to = pos.x; + settleY.to = pos.y; + settleAnim.start(); + } + + function finishVisualDrag() { + const ids = root.pendingCommitIds.slice(); + + root.dragActive = false; + root.dropAnimating = false; + root.draggedAppId = ""; + root.draggedModelData = null; + root.visualIds = []; + root.pendingCommitIds = []; + + TaskbarApps.commitVisualOrder(ids); + } + + function moveArrayItem(list, from, to) { + const next = list.slice(); + const [item] = next.splice(from, 1); + next.splice(to, 0, item); + return next; + } + + function previewVisualMove(from, hovered, before) { + let to = hovered + (before ? 0 : 1); + + if (to > from) + to -= 1; + + to = Math.max(0, Math.min(visualModel.items.count - 1, to)); + + if (from === to) + return; + + visualModel.items.move(from, to); + root.visualIds = moveArrayItem(root.visualIds, from, to); + } implicitHeight: Config.dock.height + root.padding * 2 - implicitWidth: dockRow.implicitWidth + root.padding * 2 + implicitWidth: dockRow.contentWidth + root.padding * 2 - RowLayout { + ParallelAnimation { + id: settleAnim + + onFinished: root.finishVisualDrag() + + Anim { + id: settleX + + duration: Appearance.anim.durations.normal + property: "dragX" + target: root + } + + Anim { + id: settleY + + duration: Appearance.anim.durations.normal + property: "dragY" + target: root + } + } + + Component { + id: dockDelegate + + DropArea { + id: slot + + readonly property string appId: modelData.appId + readonly property bool isSeparator: appId === TaskbarApps.separatorId + required property var modelData + + function previewReorder(drag) { + const source = drag.source; + if (!source || !source.appId || source.appId === appId) + return; + + const from = source.visualIndex; + const hovered = slot.DelegateModel.itemsIndex; + + if (from < 0 || hovered < 0) + return; + + root.previewVisualMove(from, hovered, drag.x < width / 2); + } + + height: Config.dock.height + width: isSeparator ? 1 : Config.dock.height + + ListView.onRemove: removeAnim.start() + onEntered: drag => previewReorder(drag) + onPositionChanged: drag => previewReorder(drag) + + SequentialAnimation { + id: removeAnim + + ScriptAction { + script: slot.ListView.delayRemove = true + } + + ParallelAnimation { + Anim { + property: "opacity" + target: slot + to: 0 + } + + Anim { + property: "scale" + target: slot + to: 0.5 + } + } + + ScriptAction { + script: { + slot.ListView.delayRemove = false; + } + } + } + + DockAppButton { + id: button + + anchors.centerIn: parent + appListRoot: root + appToplevel: modelData + visibilities: root.visibilities + visible: root.draggedAppId !== slot.appId + } + + DragHandler { + id: dragHandler + + enabled: !slot.isSeparator + grabPermissions: PointerHandler.CanTakeOverFromAnything + target: null + xAxis.enabled: true + yAxis.enabled: false + + onActiveChanged: { + if (active) { + root.beginVisualDrag(slot.appId, slot.modelData, button); + } else if (root.draggedAppId === slot.appId) { + dragProxy.Drag.drop(); + root.endVisualDrag(); + } + } + onActiveTranslationChanged: { + if (!active || root.draggedAppId !== slot.appId) + return; + + root.dragX = root.dragStartX + activeTranslation.x; + root.dragY = root.dragStartY + activeTranslation.y; + } + } + } + } + + DelegateModel { + id: visualModel + + delegate: dockDelegate + + model: ScriptModel { + objectProp: "appId" + values: TaskbarApps.apps + } + } + + CustomListView { id: dockRow - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter + property bool enableAddAnimation: false + + anchors.left: parent.left + anchors.margins: Appearance.padding.small anchors.top: parent.top - spacing: Appearance.spacing.small + boundsBehavior: Flickable.StopAtBounds + height: Config.dock.height + implicitWidth: root.dockContentWidth + Config.dock.height + interactive: !(root.dragActive || root.dropAnimating) + model: visualModel + orientation: ListView.Horizontal + spacing: Appearance.padding.smaller + + add: Transition { + ParallelAnimation { + Anim { + duration: dockRow.enableAddAnimation ? Appearance.anim.durations.normal : 0 + from: 0 + property: "opacity" + to: 1 + } + + Anim { + duration: dockRow.enableAddAnimation ? Appearance.anim.durations.normal : 0 + from: 0.5 + property: "scale" + to: 1 + } + } + } + displaced: Transition { + Anim { + duration: Appearance.anim.durations.small + properties: "x,y" + } + } + move: Transition { + Anim { + duration: Appearance.anim.durations.small + properties: "x,y" + } + } + + Component.onCompleted: { + Qt.callLater(() => enableAddAnimation = true); + } + } + + Item { + id: dragProxy + + property string appId: root.draggedAppId + property int visualIndex: root.visualIds.indexOf(root.draggedAppId) + + Drag.active: root.dragActive + Drag.hotSpot.x: width / 2 + Drag.hotSpot.y: height / 2 + Drag.source: dragProxy + height: root.dragHeight + visible: (root.dragActive || root.dropAnimating) && !!root.draggedModelData + width: root.dragWidth + x: root.dragX + y: root.dragY + z: 9999 + + DockAppButton { + anchors.fill: parent + appListRoot: root + appToplevel: root.draggedModelData + enabled: false + visibilities: root.visibilities + } } } diff --git a/Modules/Dock/Parts/DockAppButton.qml b/Modules/Dock/Parts/DockAppButton.qml new file mode 100644 index 0000000..c7e79b9 --- /dev/null +++ b/Modules/Dock/Parts/DockAppButton.qml @@ -0,0 +1,91 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import qs.Components +import qs.Helpers +import qs.Config + +CustomRect { + id: root + + property bool appIsActive: appToplevel?.toplevels.find(t => (t.activated == true)) !== undefined + property var appListRoot + property var appToplevel + property real countDotHeight: 4 + property real countDotWidth: 10 + property var desktopEntry: DesktopEntries.heuristicLookup(appToplevel?.appId) + property real iconSize: implicitHeight - 20 + readonly property bool isSeparator: appToplevel?.appId === "__dock_separator__" + property int lastFocused: -1 + required property PersistentProperties visibilities + + implicitHeight: Config.dock.height + implicitWidth: isSeparator ? 1 : implicitHeight + radius: Appearance.rounding.normal - Appearance.padding.small + + Loader { + active: !isSeparator + anchors.centerIn: parent + + sourceComponent: ColumnLayout { + IconImage { + id: icon + + Layout.alignment: Qt.AlignHCenter + implicitSize: root.iconSize + source: Quickshell.iconPath(AppSearch.guessIcon(appToplevel?.appId), "image-missing") + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 3 + + Repeater { + model: Math.min(appToplevel?.toplevels.length, 3) + + delegate: Rectangle { + required property int index + + color: appIsActive ? DynamicColors.palette.m3primary : DynamicColors.tPalette.m3primary + implicitHeight: root.countDotHeight + implicitWidth: (appToplevel?.toplevels.length <= 3) ? root.countDotWidth : root.countDotHeight // Circles when too many + radius: Appearance.rounding.full + } + } + } + } + } + + StateLayer { + onClicked: { + if (appToplevel?.toplevels.length === 0) { + root.desktopEntry?.execute(); + root.visibilities.dock = false; + return; + } + lastFocused = (lastFocused + 1) % appToplevel?.toplevels.length; + appToplevel?.toplevels[lastFocused].activate(); + root.visibilities.dock = false; + } + } + + Connections { + function onApplicationsChanged() { + root.desktopEntry = DesktopEntries.heuristicLookup(appToplevel?.appId); + } + + target: DesktopEntries + } + + Loader { + active: isSeparator + + sourceComponent: DockSeparator { + } + + anchors { + fill: parent + } + } +} diff --git a/Modules/Dock/Parts/DockSeparator.qml b/Modules/Dock/Parts/DockSeparator.qml new file mode 100644 index 0000000..b535412 --- /dev/null +++ b/Modules/Dock/Parts/DockSeparator.qml @@ -0,0 +1,12 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +CustomRect { + Layout.bottomMargin: dockRow.padding + Appearance.rounding.normal + Layout.fillHeight: true + Layout.topMargin: dockRow.padding + Appearance.rounding.normal + color: DynamicColors.palette.m3outlineVariant + implicitWidth: 1 +} diff --git a/Modules/Dock/Wrapper.qml b/Modules/Dock/Wrapper.qml index 5d7e36d..667ab62 100644 --- a/Modules/Dock/Wrapper.qml +++ b/Modules/Dock/Wrapper.qml @@ -18,6 +18,12 @@ Item { implicitWidth: content.implicitWidth visible: height > 0 + Behavior on implicitWidth { + Anim { + duration: Appearance.anim.durations.small + } + } + onShouldBeActiveChanged: { if (shouldBeActive) { timer.stop(); @@ -84,12 +90,13 @@ Item { id: content active: false - anchors.horizontalCenter: parent.horizontalCenter + anchors.left: parent.left anchors.top: parent.top visible: false sourceComponent: Content { panels: root.panels + screen: root.screen visibilities: root.visibilities Component.onCompleted: root.contentHeight = implicitHeight diff --git a/Modules/Drawing/Background.qml b/Modules/Drawing/Background.qml new file mode 100644 index 0000000..f19ba9a --- /dev/null +++ b/Modules/Drawing/Background.qml @@ -0,0 +1,66 @@ +import QtQuick +import QtQuick.Shapes +import qs.Components +import qs.Config + +ShapePath { + id: root + + readonly property bool flatten: wrapper.width < rounding * 2 + readonly property real rounding: Appearance.rounding.normal + readonly property real roundingX: flatten ? wrapper.width / 2 : rounding + required property Wrapper wrapper + + fillColor: DynamicColors.palette.m3surface + strokeWidth: -1 + + Behavior on fillColor { + CAnim { + } + } + + PathArc { + direction: PathArc.Counterclockwise + radiusX: Math.min(root.rounding, root.wrapper.width) + radiusY: root.rounding + relativeX: root.roundingX + relativeY: root.rounding + } + + PathLine { + relativeX: root.wrapper.width - root.roundingX * 2 + relativeY: 0 + } + + PathArc { + radiusX: Math.min(root.rounding, root.wrapper.width) + radiusY: root.rounding + relativeX: root.roundingX + relativeY: root.rounding + } + + PathLine { + relativeX: 0 + relativeY: root.wrapper.height - root.rounding * 2 + } + + PathArc { + radiusX: Math.min(root.rounding, root.wrapper.width) + radiusY: root.rounding + relativeX: -root.roundingX + relativeY: root.rounding + } + + PathLine { + relativeX: -(root.wrapper.width - root.roundingX * 2) + relativeY: 0 + } + + PathArc { + direction: PathArc.Counterclockwise + radiusX: Math.min(root.rounding, root.wrapper.width) + radiusY: root.rounding + relativeX: -root.roundingX + relativeY: root.rounding + } +} diff --git a/Modules/Drawing/Content.qml b/Modules/Drawing/Content.qml new file mode 100644 index 0000000..98c4caf --- /dev/null +++ b/Modules/Drawing/Content.qml @@ -0,0 +1,151 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import qs.Config +import qs.Components + +Item { + id: root + + readonly property var colors: ["#ef4444", "#f97316", "#eab308", "#22c55e", "#06b6d4", "#3b82f6", "#a855f7", "#ec4899", "#ffffff", "#000000"] + required property Canvas drawing + required property var visibilities + + function syncFromPenColor() { + if (!drawing) + return; + + if (!saturationSlider.pressed) + saturationSlider.value = drawing.penColor.hsvSaturation; + + if (!brightnessSlider.pressed) + brightnessSlider.value = drawing.penColor.hsvValue; + } + + function updatePenColorFromHsv() { + if (!drawing) + return; + + drawing.penColor = Qt.hsva(huePicker.currentHue, saturationSlider.value, brightnessSlider.value, drawing.penColor.a); + } + + implicitHeight: column.height + Appearance.padding.larger * 2 + implicitWidth: huePicker.implicitWidth + Appearance.padding.normal * 2 + + Component.onCompleted: syncFromPenColor() + + Connections { + function onPenColorChanged() { + root.syncFromPenColor(); + } + + target: root.drawing + } + + Column { + id: column + + anchors.centerIn: parent + spacing: 12 + + ColorArcPicker { + id: huePicker + + drawing: root.drawing + } + + GradientSlider { + id: saturationSlider + + brightness: brightnessSlider.value + channel: "saturation" + from: 0 + hue: huePicker.currentHue + icon: "\ue40a" + implicitHeight: 30 + implicitWidth: palette.width + orientation: Qt.Horizontal + to: 1 + + onMoved: root.updatePenColorFromHsv() + } + + GradientSlider { + id: brightnessSlider + + channel: "brightness" + from: 0 + hue: huePicker.currentHue + icon: "\ue1ac" + implicitHeight: 30 + implicitWidth: palette.width + orientation: Qt.Horizontal + saturation: saturationSlider.value + to: 1 + + onMoved: root.updatePenColorFromHsv() + } + + GridLayout { + id: palette + + anchors.left: parent.left + anchors.right: parent.right + columns: 5 + rowSpacing: 8 + rows: 2 + + Repeater { + model: root.colors + + delegate: Item { + id: colorCircle + + required property color modelData + readonly property bool selected: Qt.colorEqual(root.drawing.penColor, modelData) + + Layout.fillWidth: true + height: 28 + + CustomRect { + anchors.centerIn: parent + border.color: Qt.rgba(0, 0, 0, 0.25) + border.width: Qt.colorEqual(modelData, "#ffffff") ? 1 : 0 + color: colorCircle.modelData + height: 20 + radius: width / 2 + width: 20 + } + + CustomRect { + anchors.centerIn: parent + border.color: selected ? "#ffffff" : Qt.rgba(1, 1, 1, 0.28) + border.width: selected ? 3 : 1 + color: "transparent" + height: parent.height + radius: width / 2 + width: parent.height + + StateLayer { + onClicked: root.drawing.penColor = colorCircle.modelData + } + } + } + } + } + + FilledSlider { + from: 1 + icon: "border_color" + implicitHeight: 30 + implicitWidth: palette.width + multiplier: 1 + orientation: Qt.Horizontal + to: 45 + value: root.drawing.penWidth + + onMoved: root.drawing.penWidth = value + } + } +} diff --git a/Modules/Drawing/Wrapper.qml b/Modules/Drawing/Wrapper.qml new file mode 100644 index 0000000..a2df74d --- /dev/null +++ b/Modules/Drawing/Wrapper.qml @@ -0,0 +1,133 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import QtQuick +import qs.Components +import qs.Helpers +import qs.Config +import qs.Daemons + +Item { + id: root + + required property Canvas drawing + property bool expanded: true + required property ShellScreen screen + readonly property bool shouldBeActive: visibilities.isDrawing + required property var visibilities + + implicitHeight: content.implicitHeight + implicitWidth: 0 + visible: width > 0 + + states: [ + State { + name: "hidden" + when: !root.shouldBeActive + + PropertyChanges { + root.implicitWidth: 0 + } + + PropertyChanges { + icon.opacity: 0 + } + + PropertyChanges { + content.opacity: 0 + } + }, + State { + name: "collapsed" + when: root.shouldBeActive && !root.expanded + + PropertyChanges { + root.implicitWidth: icon.implicitWidth + } + + PropertyChanges { + icon.opacity: 1 + } + + PropertyChanges { + content.opacity: 0 + } + }, + State { + name: "visible" + when: root.shouldBeActive && root.expanded + + PropertyChanges { + root.implicitWidth: content.implicitWidth + } + + PropertyChanges { + icon.opacity: 0 + } + + PropertyChanges { + content.opacity: 1 + } + } + ] + transitions: [ + Transition { + from: "*" + to: "*" + + ParallelAnimation { + Anim { + easing.bezierCurve: MaterialEasing.expressiveEffects + property: "implicitWidth" + target: root + } + + Anim { + duration: Appearance.anim.durations.small + property: "opacity" + target: icon + } + + Anim { + duration: Appearance.anim.durations.small + property: "opacity" + target: content + } + } + } + ] + + onVisibleChanged: { + if (!visible) + root.expanded = true; + } + + Loader { + id: icon + + active: Qt.binding(() => root.shouldBeActive || root.visible) + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + height: content.contentItem.height + opacity: 1 + + sourceComponent: MaterialIcon { + font.pointSize: Appearance.font.size.larger + text: "arrow_forward_ios" + } + } + + Loader { + id: content + + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + sourceComponent: Content { + drawing: root.drawing + visibilities: root.visibilities + } + + Component.onCompleted: active = Qt.binding(() => root.shouldBeActive || root.visible) + } +} diff --git a/Modules/MediaWidget.qml b/Modules/MediaWidget.qml index cbb67d2..1b7c821 100644 --- a/Modules/MediaWidget.qml +++ b/Modules/MediaWidget.qml @@ -5,30 +5,22 @@ import qs.Daemons import qs.Config import qs.Helpers -Item { +CustomRect { id: root readonly property string currentMedia: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title") readonly property int textWidth: Math.min(metrics.width, 200) - anchors.bottom: parent.bottom - anchors.top: parent.top + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2 implicitWidth: layout.implicitWidth + Appearance.padding.normal * 2 + radius: Appearance.rounding.full Behavior on implicitWidth { Anim { } } - CustomRect { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - color: DynamicColors.tPalette.m3surfaceContainer - implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2) - radius: Appearance.rounding.full - } - TextMetrics { id: metrics @@ -39,11 +31,7 @@ Item { RowLayout { id: layout - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - anchors.left: parent.left - anchors.leftMargin: Appearance.padding.normal - anchors.top: parent.top + anchors.centerIn: parent Behavior on implicitWidth { Anim { @@ -53,7 +41,7 @@ Item { MaterialIcon { animate: true color: Players.active?.isPlaying ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface - font.pointSize: 14 + font.pointSize: Appearance.font.size.larger text: Players.active?.isPlaying ? "music_note" : "music_off" } diff --git a/Modules/NotifBell.qml b/Modules/NotifBell.qml index ebe9746..7ddd506 100644 --- a/Modules/NotifBell.qml +++ b/Modules/NotifBell.qml @@ -5,46 +5,39 @@ import qs.Config import qs.Helpers import qs.Components -Item { +CustomRect { id: root required property Wrapper popouts required property PersistentProperties visibilities - anchors.bottom: parent.bottom - anchors.top: parent.top - implicitWidth: 30 + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2 + implicitWidth: implicitHeight + radius: Appearance.rounding.full - CustomRect { - anchors.bottomMargin: 3 - anchors.fill: parent - anchors.topMargin: 3 - color: "transparent" - radius: 4 + MaterialIcon { + id: notificationCenterIcon - MaterialIcon { - id: notificationCenterIcon + property color iconColor: DynamicColors.palette.m3onSurface - property color iconColor: DynamicColors.palette.m3onSurface + anchors.centerIn: parent + color: iconColor + font.family: "Material Symbols Rounded" + font.pointSize: Appearance.font.size.larger + text: HasNotifications.hasNotifications ? "\uf4fe" : "\ue7f4" - anchors.centerIn: parent - color: iconColor - font.family: "Material Symbols Rounded" - font.pointSize: 16 - text: HasNotifications.hasNotifications ? "\uf4fe" : "\ue7f4" - - Behavior on color { - CAnim { - } - } - } - - StateLayer { - cursorShape: Qt.PointingHandCursor - - onClicked: { - root.visibilities.sidebar = !root.visibilities.sidebar; + Behavior on color { + CAnim { } } } + + StateLayer { + cursorShape: Qt.PointingHandCursor + + onClicked: { + root.visibilities.sidebar = !root.visibilities.sidebar; + } + } } diff --git a/Modules/Notifications/Sidebar/Background.qml b/Modules/Notifications/Sidebar/Background.qml index c41693b..bf15baf 100644 --- a/Modules/Notifications/Sidebar/Background.qml +++ b/Modules/Notifications/Sidebar/Background.qml @@ -15,7 +15,7 @@ ShapePath { readonly property real utilsWidthDiff: panels.utilities.width - wrapper.width required property Wrapper wrapper - fillColor: flatten ? "transparent" : DynamicColors.palette.m3surface + fillColor: DynamicColors.palette.m3surface strokeWidth: -1 Behavior on fillColor { diff --git a/Modules/Notifications/Wrapper.qml b/Modules/Notifications/Wrapper.qml index fda5789..1e7038f 100644 --- a/Modules/Notifications/Wrapper.qml +++ b/Modules/Notifications/Wrapper.qml @@ -14,7 +14,7 @@ Item { states: State { name: "hidden" - when: root.visibilities.sidebar + when: root.visibilities.sidebar || root.visibilities.dashboard || (root.panels.popouts.hasCurrent && root.panels.popouts.currentName.startsWith("traymenu")) PropertyChanges { root.implicitHeight: 0 diff --git a/Modules/Osd/Wrapper.qml b/Modules/Osd/Wrapper.qml index 55f0311..b0815e8 100644 --- a/Modules/Osd/Wrapper.qml +++ b/Modules/Osd/Wrapper.qml @@ -113,22 +113,26 @@ Item { } } - Loader { - id: content + CustomClippingRect { + anchors.fill: parent - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter + Loader { + id: content - sourceComponent: Content { - brightness: root.brightness - monitor: root.monitor - muted: root.muted - sourceMuted: root.sourceMuted - sourceVolume: root.sourceVolume - visibilities: root.visibilities - volume: root.volume + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + + sourceComponent: Content { + brightness: root.brightness + monitor: root.monitor + muted: root.muted + sourceMuted: root.sourceMuted + sourceVolume: root.sourceVolume + visibilities: root.visibilities + volume: root.volume + } + + Component.onCompleted: active = Qt.binding(() => root.shouldBeActive || root.visible) } - - Component.onCompleted: active = Qt.binding(() => root.shouldBeActive || root.visible) } } diff --git a/Modules/Resource-old.qml b/Modules/Resource-old.qml deleted file mode 100644 index 3be26f9..0000000 --- a/Modules/Resource-old.qml +++ /dev/null @@ -1,116 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtQuick.Shapes -import qs.Components -import qs.Config - -Item { - id: root - - property color borderColor: warning ? DynamicColors.palette.m3onError : mainColor - required property color mainColor - required property double percentage - property bool shown: true - property color usageColor: warning ? DynamicColors.palette.m3error : mainColor - property bool warning: percentage * 100 >= warningThreshold - property int warningThreshold: 100 - - clip: true - implicitHeight: 22 - implicitWidth: resourceRowLayout.x < 0 ? 0 : resourceRowLayout.implicitWidth - visible: width > 0 && height > 0 - - Behavior on percentage { - NumberAnimation { - duration: 300 - easing.type: Easing.InOutQuad - } - } - - RowLayout { - id: resourceRowLayout - - spacing: 2 - x: shown ? 0 : -resourceRowLayout.width - - anchors { - verticalCenter: parent.verticalCenter - } - - Item { - Layout.alignment: Qt.AlignVCenter - implicitHeight: root.implicitHeight - implicitWidth: 14 - - Rectangle { - id: backgroundCircle - - anchors.centerIn: parent - border.color: "#404040" - border.width: 1 - color: "#40000000" - height: 14 - radius: height / 2 - width: 14 - } - - Shape { - anchors.fill: backgroundCircle - preferredRendererType: Shape.CurveRenderer - smooth: true - - ShapePath { - fillColor: root.usageColor - startX: backgroundCircle.width / 2 - startY: backgroundCircle.height / 2 - strokeWidth: 0 - - Behavior on fillColor { - CAnim { - } - } - - PathLine { - x: backgroundCircle.width / 2 - y: 0 + (1 / 2) - } - - PathAngleArc { - centerX: backgroundCircle.width / 2 - centerY: backgroundCircle.height / 2 - radiusX: backgroundCircle.width / 2 - (1 / 2) - radiusY: backgroundCircle.height / 2 - (1 / 2) - startAngle: -90 - sweepAngle: 360 * root.percentage - } - - PathLine { - x: backgroundCircle.width / 2 - y: backgroundCircle.height / 2 - } - } - - ShapePath { - capStyle: ShapePath.FlatCap - fillColor: "transparent" - strokeColor: root.borderColor - strokeWidth: 1 - - Behavior on strokeColor { - CAnim { - } - } - - PathAngleArc { - centerX: backgroundCircle.width / 2 - centerY: backgroundCircle.height / 2 - radiusX: backgroundCircle.width / 2 - (1 / 2) - radiusY: backgroundCircle.height / 2 - (1 / 2) - startAngle: -90 - sweepAngle: 360 * root.percentage - } - } - } - } - } -} diff --git a/Modules/Resource.qml b/Modules/Resource.qml index e26085c..43c4561 100644 --- a/Modules/Resource.qml +++ b/Modules/Resource.qml @@ -4,7 +4,7 @@ import QtQuick.Shapes import qs.Components import qs.Config -Item { +RowLayout { id: root property color accentColor: warning ? DynamicColors.palette.m3error : mainColor @@ -19,75 +19,48 @@ Item { property bool warning: percentage * 100 >= warningThreshold property int warningThreshold: 80 - clip: true - height: implicitHeight - implicitHeight: 34 - implicitWidth: 34 percentage: 0 - visible: width > 0 && height > 0 - width: implicitWidth Behavior on animatedPercentage { Anim { - duration: Appearance.anim.durations.large } } Component.onCompleted: animatedPercentage = percentage - onPercentageChanged: animatedPercentage = percentage + onPercentageChanged: { + const next = percentage; - Canvas { - id: gaugeCanvas - - anchors.centerIn: parent - height: width - width: Math.min(parent.width, parent.height) - - Component.onCompleted: requestPaint() - onPaint: { - const ctx = getContext("2d"); - ctx.reset(); - const cx = width / 2; - const cy = (height / 2) + 1; - const radius = (Math.min(width, height) - 12) / 2; - const lineWidth = 3; - ctx.beginPath(); - ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep); - ctx.lineWidth = lineWidth; - ctx.lineCap = "round"; - ctx.strokeStyle = DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2); - ctx.stroke(); - if (root.animatedPercentage > 0) { - ctx.beginPath(); - ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep * root.animatedPercentage); - ctx.lineWidth = lineWidth; - ctx.lineCap = "round"; - ctx.strokeStyle = root.accentColor; - ctx.stroke(); - } - } - - Connections { - function onAnimatedPercentageChanged() { - gaugeCanvas.requestPaint(); - } - - target: root - } - - Connections { - function onPaletteChanged() { - gaugeCanvas.requestPaint(); - } - - target: DynamicColors - } + if (Math.abs(next - animatedPercentage) >= 0.05) + animatedPercentage = next; } MaterialIcon { - anchors.centerIn: parent + id: icon + color: DynamicColors.palette.m3onSurface - font.pointSize: 12 + font.pointSize: Appearance.font.size.larger text: root.icon } + + CustomClippingRect { + Layout.preferredHeight: root.height - Appearance.padding.small + Layout.preferredWidth: 4 + color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) + radius: Appearance.rounding.full + + CustomRect { + id: fill + + anchors.fill: parent + antialiasing: false + color: root.mainColor + implicitHeight: Math.ceil(root.percentage * parent.height) + radius: Appearance.rounding.full + + transform: Scale { + origin.y: fill.height + yScale: Math.max(0.001, root.animatedPercentage) + } + } + } } diff --git a/Modules/ResourceDetail.qml b/Modules/ResourceDetail.qml deleted file mode 100644 index 1ec97f5..0000000 --- a/Modules/ResourceDetail.qml +++ /dev/null @@ -1,77 +0,0 @@ -import Quickshell -import QtQuick -import QtQuick.Layouts -import qs.Config -import qs.Components - -Item { - id: root - - property color barColor: DynamicColors.palette.m3primary - required property string details - required property string iconString - required property double percentage - required property string resourceName - property color textColor: DynamicColors.palette.m3onSurface - property color warningBarColor: DynamicColors.palette.m3error - required property int warningThreshold - - Layout.preferredHeight: columnLayout.implicitHeight - Layout.preferredWidth: 158 - - ColumnLayout { - id: columnLayout - - anchors.fill: parent - spacing: 4 - - Row { - Layout.alignment: Qt.AlignLeft - Layout.fillWidth: true - spacing: 6 - - MaterialIcon { - color: root.textColor - font.family: "Material Symbols Rounded" - font.pointSize: 28 - text: root.iconString - } - - CustomText { - anchors.verticalCenter: parent.verticalCenter - color: root.textColor - font.pointSize: 12 - text: root.resourceName - } - } - - Rectangle { - Layout.alignment: Qt.AlignLeft - Layout.fillWidth: true - Layout.preferredHeight: 6 - color: "#40000000" - radius: height / 2 - - Rectangle { - color: root.percentage * 100 >= root.warningThreshold ? root.warningBarColor : root.barColor - height: parent.height - radius: height / 2 - width: parent.width * Math.min(root.percentage, 1) - - Behavior on width { - Anim { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - } - } - - CustomText { - Layout.alignment: Qt.AlignLeft - color: root.textColor - font.pointSize: 10 - text: root.details - } - } -} diff --git a/Modules/ResourcePopout-old.qml b/Modules/ResourcePopout-old.qml deleted file mode 100644 index 4dcc35c..0000000 --- a/Modules/ResourcePopout-old.qml +++ /dev/null @@ -1,59 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Layouts -import qs.Config - -Item { - id: popoutWindow - - required property var wrapper - - implicitHeight: contentColumn.implicitHeight + 10 - implicitWidth: contentColumn.implicitWidth + 10 * 2 - - // ShadowRect { - // anchors.fill: contentRect - // radius: 8 - // } - - ColumnLayout { - id: contentColumn - - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - spacing: 10 - - ResourceDetail { - details: qsTr("%1 of %2 MB used").arg(Math.round(ResourceUsage.memoryUsed * 0.001)).arg(Math.round(ResourceUsage.memoryTotal * 0.001)) - iconString: "\uf7a3" - percentage: ResourceUsage.memoryUsedPercentage - resourceName: qsTr("Memory Usage") - warningThreshold: 95 - } - - ResourceDetail { - details: qsTr("%1% used").arg(Math.round(ResourceUsage.cpuUsage * 100)) - iconString: "\ue322" - percentage: ResourceUsage.cpuUsage - resourceName: qsTr("CPU Usage") - warningThreshold: 95 - } - - ResourceDetail { - details: qsTr("%1% used").arg(Math.round(ResourceUsage.gpuUsage * 100)) - iconString: "\ue30f" - percentage: ResourceUsage.gpuUsage - resourceName: qsTr("GPU Usage") - warningThreshold: 95 - } - - ResourceDetail { - details: qsTr("%1% used").arg(Math.round(ResourceUsage.gpuMemUsage * 100)) - iconString: "\ue30d" - percentage: ResourceUsage.gpuMemUsage - resourceName: qsTr("VRAM Usage") - warningThreshold: 95 - } - } -} diff --git a/Modules/ResourceUsage.qml b/Modules/ResourceUsage.qml deleted file mode 100644 index f1b01ce..0000000 --- a/Modules/ResourceUsage.qml +++ /dev/null @@ -1,123 +0,0 @@ -pragma Singleton -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import Quickshell.Io -import qs.Config - -Singleton { - id: root - - property string autoGpuType: "NONE" - property double cpuUsage: 0 - property double gpuMemUsage: 0 - readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType - property double gpuUsage: 0 - property double memoryFree: 1 - property double memoryTotal: 1 - property double memoryUsed: memoryTotal - memoryFree - property double memoryUsedPercentage: memoryUsed / memoryTotal - property var previousCpuStats - property double swapFree: 1 - property double swapTotal: 1 - property double swapUsed: swapTotal - swapFree - property double swapUsedPercentage: swapTotal > 0 ? (swapUsed / swapTotal) : 0 - property double totalMem: 0 - - Timer { - interval: 1 - repeat: true - running: true - - onTriggered: { - // Reload files - fileMeminfo.reload(); - fileStat.reload(); - - // Parse memory and swap usage - const textMeminfo = fileMeminfo.text(); - memoryTotal = Number(textMeminfo.match(/MemTotal: *(\d+)/)?.[1] ?? 1); - memoryFree = Number(textMeminfo.match(/MemAvailable: *(\d+)/)?.[1] ?? 0); - swapTotal = Number(textMeminfo.match(/SwapTotal: *(\d+)/)?.[1] ?? 1); - swapFree = Number(textMeminfo.match(/SwapFree: *(\d+)/)?.[1] ?? 0); - - // Parse CPU usage - const textStat = fileStat.text(); - const cpuLine = textStat.match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/); - if (cpuLine) { - const stats = cpuLine.slice(1).map(Number); - const total = stats.reduce((a, b) => a + b, 0); - const idle = stats[3]; - - if (previousCpuStats) { - const totalDiff = total - previousCpuStats.total; - const idleDiff = idle - previousCpuStats.idle; - cpuUsage = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0; - } - - previousCpuStats = { - total, - idle - }; - } - if (root.gpuType === "NVIDIA") { - processGpu.running = true; - } - - interval = 3000; - } - } - - FileView { - id: fileMeminfo - - path: "/proc/meminfo" - } - - FileView { - id: fileStat - - path: "/proc/stat" - } - - Process { - id: gpuTypeCheck - - command: ["sh", "-c", "if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi"] - running: !Config.services.gpuType - - stdout: StdioCollector { - onStreamFinished: root.autoGpuType = text.trim() - } - } - - Process { - id: oneshotMem - - command: ["nvidia-smi", "--query-gpu=memory.total", "--format=csv,noheader,nounits"] - running: root.gpuType === "NVIDIA" && totalMem === 0 - - stdout: StdioCollector { - onStreamFinished: { - totalMem = Number(this.text.trim()); - oneshotMem.running = false; - } - } - } - - Process { - id: processGpu - - command: ["nvidia-smi", "--query-gpu=utilization.gpu,memory.used", "--format=csv,noheader,nounits"] - running: false - - stdout: StdioCollector { - onStreamFinished: { - const parts = this.text.trim().split(", "); - gpuUsage = Number(parts[0]) / 100; - gpuMemUsage = Number(parts[1]) / totalMem; - } - } - } -} diff --git a/Modules/Resources.qml b/Modules/Resources.qml index ab36c71..e90100a 100644 --- a/Modules/Resources.qml +++ b/Modules/Resources.qml @@ -8,39 +8,27 @@ import qs.Modules import qs.Config import qs.Components -Item { +CustomRect { id: root required property PersistentProperties visibilities - anchors.bottom: parent.bottom - anchors.top: parent.top clip: true - implicitWidth: rowLayout.implicitWidth + Appearance.padding.small * 2 + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2 + implicitWidth: rowLayout.implicitWidth + Appearance.padding.normal * 2 + radius: height / 2 - CustomRect { - id: backgroundRect - - color: DynamicColors.tPalette.m3surfaceContainer - implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2) - radius: height / 2 - - anchors { - left: parent.left - right: parent.right - verticalCenter: parent.verticalCenter - } - - StateLayer { - onClicked: root.visibilities.resources = !root.visibilities.resources - } + StateLayer { + onClicked: root.visibilities.resources = !root.visibilities.resources } RowLayout { id: rowLayout anchors.centerIn: parent - spacing: 0 + implicitHeight: root.implicitHeight + spacing: Appearance.spacing.smaller Ref { service: SystemUsage @@ -48,6 +36,7 @@ Item { Resource { Layout.alignment: Qt.AlignVCenter + Layout.fillHeight: true icon: "memory" mainColor: DynamicColors.palette.m3primary percentage: SystemUsage.cpuPerc @@ -55,6 +44,7 @@ Item { } Resource { + Layout.fillHeight: true icon: "memory_alt" mainColor: DynamicColors.palette.m3secondary percentage: SystemUsage.memPerc @@ -62,12 +52,14 @@ Item { } Resource { + Layout.fillHeight: true icon: "gamepad" mainColor: DynamicColors.palette.m3tertiary percentage: SystemUsage.gpuPerc } Resource { + Layout.fillHeight: true icon: "developer_board" mainColor: DynamicColors.palette.m3primary percentage: SystemUsage.gpuMemUsed diff --git a/Modules/Settings/Background.qml b/Modules/Settings/Background.qml index cc242f7..3004b3d 100644 --- a/Modules/Settings/Background.qml +++ b/Modules/Settings/Background.qml @@ -7,7 +7,7 @@ ShapePath { id: root readonly property bool flatten: wrapper.height < rounding * 2 - readonly property real rounding: 8 + readonly property real rounding: Appearance.rounding.large readonly property real roundingY: flatten ? wrapper.height / 2 : rounding required property Wrapper wrapper diff --git a/Modules/Settings/Categories.qml b/Modules/Settings/Categories.qml index 08bcacc..731428c 100644 --- a/Modules/Settings/Categories.qml +++ b/Modules/Settings/Categories.qml @@ -22,83 +22,92 @@ Item { ListElement { icon: "settings" + key: "general" name: "General" } ListElement { icon: "wallpaper" + key: "wallpaper" name: "Wallpaper" } ListElement { icon: "settop_component" + key: "bar" name: "Bar" } ListElement { icon: "lock" + key: "lockscreen" name: "Lockscreen" } ListElement { icon: "build_circle" + key: "services" name: "Services" } ListElement { icon: "notifications" + key: "notifications" name: "Notifications" } ListElement { icon: "view_sidebar" + key: "sidebar" name: "Sidebar" } ListElement { icon: "handyman" + key: "utilities" name: "Utilities" } ListElement { icon: "dashboard" + key: "dashboard" name: "Dashboard" } ListElement { icon: "colors" + key: "appearance" name: "Appearance" } ListElement { icon: "display_settings" + key: "osd" name: "On screen display" } ListElement { icon: "rocket_launch" + key: "launcher" name: "Launcher" } - - ListElement { - icon: "colors" - name: "Colors" - } } - CustomRect { + CustomClippingRect { anchors.fill: parent color: DynamicColors.tPalette.m3surfaceContainer - radius: 4 + radius: Appearance.rounding.normal CustomListView { id: clayout - anchors.centerIn: parent - contentHeight: contentItem.childrenRect.height + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.margins: Appearance.padding.smaller + anchors.top: parent.top + boundsBehavior: Flickable.StopAtBounds contentWidth: contentItem.childrenRect.width highlightFollowsCurrentItem: false - implicitHeight: contentItem.childrenRect.height implicitWidth: contentItem.childrenRect.width model: listModel spacing: 5 @@ -109,7 +118,7 @@ Item { color: DynamicColors.palette.m3primary implicitHeight: clayout.currentItem?.implicitHeight ?? 0 implicitWidth: clayout.width - radius: 4 + radius: Appearance.rounding.normal - Appearance.padding.smaller y: clayout.currentItem?.y ?? 0 Behavior on y { @@ -127,11 +136,12 @@ Item { required property string icon required property int index + required property string key required property string name implicitHeight: 42 implicitWidth: 200 - radius: 4 + radius: Appearance.rounding.normal - Appearance.padding.smaller RowLayout { id: layout @@ -148,7 +158,7 @@ Item { Layout.fillHeight: true Layout.preferredWidth: icon.contentWidth color: categoryItem.index === clayout.currentIndex ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface - font.pointSize: 22 + font.pointSize: Appearance.font.size.small * 2 text: categoryItem.icon verticalAlignment: Text.AlignVCenter } @@ -170,7 +180,7 @@ Item { id: layer onClicked: { - root.content.currentCategory = categoryItem.name.toLowerCase(); + root.content.currentCategory = categoryItem.key; clayout.currentIndex = categoryItem.index; } } diff --git a/Modules/Settings/Categories/Appearance.qml b/Modules/Settings/Categories/Appearance.qml index 78c18a0..01c88e5 100644 --- a/Modules/Settings/Categories/Appearance.qml +++ b/Modules/Settings/Categories/Appearance.qml @@ -1,97 +1,159 @@ -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts -import qs.Components -import qs.Modules as Modules import qs.Modules.Settings.Controls import qs.Config -import qs.Helpers -CustomRect { +SettingsPage { id: root - ColumnLayout { - id: clayout + SettingsSection { + SettingsHeader { + name: "Scale" + } - anchors.left: parent.left - anchors.right: parent.right + SettingSpinBox { + name: "Rounding scale" + object: Config.appearance.rounding + setting: "scale" + step: 0.1 + } - CustomRect { - Layout.fillWidth: true - Layout.preferredHeight: colorLayout.implicitHeight - color: DynamicColors.tPalette.m3surfaceContainer + Separator { + } - ColumnLayout { - id: colorLayout + SettingSpinBox { + name: "Spacing scale" + object: Config.appearance.spacing + setting: "scale" + step: 0.1 + } - anchors.left: parent.left - anchors.margins: Appearance.padding.large - anchors.right: parent.right + Separator { + } - Settings { - name: "smth" - } + SettingSpinBox { + name: "Padding scale" + object: Config.appearance.padding + setting: "scale" + step: 0.1 + } - SettingSwitch { - name: "wallust" - object: Config.general.color - setting: "wallust" - } + Separator { + } - CustomSplitButtonRow { - enabled: true - label: qsTr("Scheme mode") + SettingSpinBox { + name: "Font size scale" + object: Config.appearance.font.size + setting: "scale" + step: 0.1 + } - menuItems: [ - MenuItem { - property string val: "light" + Separator { + } - icon: "light_mode" - text: qsTr("Light") - }, - MenuItem { - property string val: "dark" - - icon: "dark_mode" - text: qsTr("Dark") - } - ] - - Component.onCompleted: { - if (Config.general.color.mode === "light") - active = menuItems[0]; - else - active = menuItems[1]; - } - onSelected: item => { - Config.general.color.mode = item.val; - Config.save(); - } - } - } + SettingSpinBox { + name: "Animation duration scale" + object: Config.appearance.anim.durations + setting: "scale" + step: 0.1 } } - component Settings: CustomRect { - id: settingsItem + SettingsSection { + SettingsHeader { + name: "Fonts" + } - required property string name + SettingInput { + name: "Sans family" + object: Config.appearance.font.family + setting: "sans" + } - Layout.preferredHeight: 42 - Layout.preferredWidth: 200 - radius: 4 + Separator { + } - CustomText { - id: text + SettingInput { + name: "Monospace family" + object: Config.appearance.font.family + setting: "mono" + } - anchors.left: parent.left - anchors.margins: Appearance.padding.smaller - anchors.right: parent.right - font.bold: true - font.pointSize: 32 - text: settingsItem.name - verticalAlignment: Text.AlignVCenter + Separator { + } + + SettingInput { + name: "Material family" + object: Config.appearance.font.family + setting: "material" + } + + Separator { + } + + SettingInput { + name: "Clock family" + object: Config.appearance.font.family + setting: "clock" + } + } + + SettingsSection { + SettingsHeader { + name: "Animation" + } + + SettingSpinBox { + name: "Media GIF speed adjustment" + object: Config.appearance.anim + setting: "mediaGifSpeedAdjustment" + step: 10 + } + + Separator { + } + + SettingSpinBox { + name: "Session GIF speed" + max: 5 + min: 0 + object: Config.appearance.anim + setting: "sessionGifSpeed" + step: 0.1 + } + } + + SettingsSection { + SettingsHeader { + name: "Transparency" + } + + SettingSwitch { + name: "Enable transparency" + object: Config.appearance.transparency + setting: "enabled" + } + + Separator { + } + + SettingSpinBox { + name: "Base opacity" + max: 1 + min: 0 + object: Config.appearance.transparency + setting: "base" + step: 0.05 + } + + Separator { + } + + SettingSpinBox { + name: "Layer opacity" + max: 1 + min: 0 + object: Config.appearance.transparency + setting: "layers" + step: 0.05 } } } diff --git a/Modules/Settings/Categories/Background.qml b/Modules/Settings/Categories/Background.qml index 2d24d68..b7fabb9 100644 --- a/Modules/Settings/Categories/Background.qml +++ b/Modules/Settings/Categories/Background.qml @@ -1,13 +1,29 @@ -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts -import qs.Components -import qs.Modules as Modules +import qs.Modules.Settings.Controls import qs.Config -import qs.Helpers -CustomRect { +SettingsPage { id: root + SettingsSection { + SettingsHeader { + name: "Wallpaper" + } + + SettingSwitch { + name: "Enable wallpaper rendering" + object: Config.background + setting: "enabled" + } + + Separator { + } + + SettingSpinBox { + name: "Fade duration" + min: 0 + object: Config.background + setting: "wallFadeDuration" + step: 50 + } + } } diff --git a/Modules/Settings/Categories/Bar.qml b/Modules/Settings/Categories/Bar.qml new file mode 100644 index 0000000..150a709 --- /dev/null +++ b/Modules/Settings/Categories/Bar.qml @@ -0,0 +1,184 @@ +import qs.Modules.Settings.Controls +import qs.Config + +SettingsPage { + SettingsSection { + SettingsHeader { + name: "Bar" + } + + SettingSwitch { + name: "Auto hide" + object: Config.barConfig + setting: "autoHide" + } + + Separator { + } + + SettingSpinBox { + name: "Height" + min: 1 + object: Config.barConfig + setting: "height" + } + + Separator { + } + + SettingSpinBox { + name: "Rounding" + min: 0 + object: Config.barConfig + setting: "rounding" + } + + Separator { + } + + SettingSpinBox { + name: "Border" + min: 0 + object: Config.barConfig + setting: "border" + } + } + + SettingsSection { + SettingsHeader { + name: "Popouts" + } + + SettingSwitch { + name: "Tray" + object: Config.barConfig.popouts + setting: "tray" + } + + Separator { + } + + SettingSwitch { + name: "Audio" + object: Config.barConfig.popouts + setting: "audio" + } + + Separator { + } + + SettingSwitch { + name: "Active window" + object: Config.barConfig.popouts + setting: "activeWindow" + } + + Separator { + } + + SettingSwitch { + name: "Resources" + object: Config.barConfig.popouts + setting: "resources" + } + + Separator { + } + + SettingSwitch { + name: "Clock" + object: Config.barConfig.popouts + setting: "clock" + } + + Separator { + } + + SettingSwitch { + name: "Network" + object: Config.barConfig.popouts + setting: "network" + } + + Separator { + } + + SettingSwitch { + name: "Power" + object: Config.barConfig.popouts + setting: "upower" + } + } + + SettingsSection { + SettingsHeader { + name: "Entries" + } + + SettingBarEntryList { + name: "Bar entries" + object: Config.barConfig + setting: "entries" + } + } + + SettingsSection { + SettingsHeader { + name: "Dock" + } + + SettingSwitch { + name: "Enable dock" + object: Config.dock + setting: "enable" + } + + Separator { + } + + SettingSpinBox { + name: "Dock height" + min: 1 + object: Config.dock + setting: "height" + } + + Separator { + } + + SettingSwitch { + name: "Hover to reveal" + object: Config.dock + setting: "hoverToReveal" + } + + Separator { + } + + SettingSwitch { + name: "Pin on startup" + object: Config.dock + setting: "pinnedOnStartup" + } + + Separator { + } + + SettingStringList { + name: "Pinned apps" + addLabel: qsTr("Add pinned app") + object: Config.dock + setting: "pinnedApps" + } + + Separator { + } + + SettingStringList { + name: "Ignored app regexes" + addLabel: qsTr("Add ignored regex") + object: Config.dock + setting: "ignoredAppRegexes" + } + } +} diff --git a/Modules/Settings/Categories/Dashboard.qml b/Modules/Settings/Categories/Dashboard.qml new file mode 100644 index 0000000..f597541 --- /dev/null +++ b/Modules/Settings/Categories/Dashboard.qml @@ -0,0 +1,212 @@ +import qs.Modules.Settings.Controls +import qs.Config + +SettingsPage { + SettingsSection { + SettingsHeader { + name: "Dashboard" + } + + SettingSwitch { + name: "Enable dashboard" + object: Config.dashboard + setting: "enabled" + } + + Separator { + } + + SettingSpinBox { + name: "Media update interval" + min: 0 + object: Config.dashboard + setting: "mediaUpdateInterval" + step: 50 + } + + Separator { + } + + SettingSpinBox { + name: "Resource update interval" + min: 0 + object: Config.dashboard + setting: "resourceUpdateInterval" + step: 50 + } + + Separator { + } + + SettingSpinBox { + name: "Drag threshold" + min: 0 + object: Config.dashboard + setting: "dragThreshold" + } + } + + SettingsSection { + SettingsHeader { + name: "Performance" + } + + SettingSwitch { + name: "Show battery" + object: Config.dashboard.performance + setting: "showBattery" + } + + Separator { + } + + SettingSwitch { + name: "Show GPU" + object: Config.dashboard.performance + setting: "showGpu" + } + + Separator { + } + + SettingSwitch { + name: "Show CPU" + object: Config.dashboard.performance + setting: "showCpu" + } + + Separator { + } + + SettingSwitch { + name: "Show memory" + object: Config.dashboard.performance + setting: "showMemory" + } + + Separator { + } + + SettingSwitch { + name: "Show storage" + object: Config.dashboard.performance + setting: "showStorage" + } + + Separator { + } + + SettingSwitch { + name: "Show network" + object: Config.dashboard.performance + setting: "showNetwork" + } + } + + SettingsSection { + SettingsHeader { + name: "Layout Sizes" + } + + SettingReadOnly { + name: "Tab indicator height" + value: String(Config.dashboard.sizes.tabIndicatorHeight) + } + + Separator { + } + + SettingReadOnly { + name: "Tab indicator spacing" + value: String(Config.dashboard.sizes.tabIndicatorSpacing) + } + + Separator { + } + + SettingReadOnly { + name: "Info width" + value: String(Config.dashboard.sizes.infoWidth) + } + + Separator { + } + + SettingReadOnly { + name: "Info icon size" + value: String(Config.dashboard.sizes.infoIconSize) + } + + Separator { + } + + SettingReadOnly { + name: "Date time width" + value: String(Config.dashboard.sizes.dateTimeWidth) + } + + Separator { + } + + SettingReadOnly { + name: "Media width" + value: String(Config.dashboard.sizes.mediaWidth) + } + + Separator { + } + + SettingReadOnly { + name: "Media progress sweep" + value: String(Config.dashboard.sizes.mediaProgressSweep) + } + + Separator { + } + + SettingReadOnly { + name: "Media progress thickness" + value: String(Config.dashboard.sizes.mediaProgressThickness) + } + + Separator { + } + + SettingReadOnly { + name: "Resource progress thickness" + value: String(Config.dashboard.sizes.resourceProgessThickness) + } + + Separator { + } + + SettingReadOnly { + name: "Weather width" + value: String(Config.dashboard.sizes.weatherWidth) + } + + Separator { + } + + SettingReadOnly { + name: "Media cover art size" + value: String(Config.dashboard.sizes.mediaCoverArtSize) + } + + Separator { + } + + SettingReadOnly { + name: "Media visualiser size" + value: String(Config.dashboard.sizes.mediaVisualiserSize) + } + + Separator { + } + + SettingReadOnly { + name: "Resource size" + value: String(Config.dashboard.sizes.resourceSize) + } + } +} diff --git a/Modules/Settings/Categories/General.qml b/Modules/Settings/Categories/General.qml index 3e8335c..0c0bf0b 100644 --- a/Modules/Settings/Categories/General.qml +++ b/Modules/Settings/Categories/General.qml @@ -1,55 +1,212 @@ -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts -import qs.Components -import qs.Modules as Modules +import qs.Modules.Settings.Controls import qs.Config -import qs.Helpers +import qs.Components -CustomRect { +SettingsPage { id: root - ColumnLayout { - id: clayout - - anchors.fill: parent - - Settings { - name: "apps" + function schemeTypeItem(items, value) { + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (item.value === value) + return item; } - Item { + return items[0] ?? null; + } + + SettingsSection { + SettingsHeader { + name: "General" + } + + SettingInput { + name: "Logo" + object: Config.general + setting: "logo" + } + + Separator { + } + + SettingInput { + name: "Wallpaper path" + object: Config.general + setting: "wallpaperPath" + } + + Separator { + } + + SettingSwitch { + name: "Desktop icons" + object: Config.general + setting: "desktopIcons" } } - component Settings: CustomRect { - id: settingsItem + SettingsSection { + SettingsHeader { + name: "Color" + } - required property string name + CustomSplitButtonRow { + active: Config.general.color.mode === "light" ? menuItems[0] : menuItems[1] + label: qsTr("Scheme mode") - implicitHeight: 42 - implicitWidth: 200 - radius: 4 + menuItems: [ + MenuItem { + icon: "light_mode" + text: qsTr("Light") + value: "light" + }, + MenuItem { + icon: "dark_mode" + text: qsTr("Dark") + value: "dark" + } + ] - RowLayout { - id: layout - - anchors.left: parent.left - anchors.margins: Appearance.padding.smaller - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - - CustomText { - id: text - - Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter - Layout.fillHeight: true - Layout.fillWidth: true - Layout.leftMargin: Appearance.spacing.normal - text: settingsItem.name - verticalAlignment: Text.AlignVCenter + onSelected: item => { + Config.general.color.mode = item.value; + Config.save(); } } + + Separator { + } + + CustomSplitButtonRow { + id: schemeType + + active: root.schemeTypeItem(menuItems, Config.colors.schemeType) + label: qsTr("Scheme type") + z: 2 + + menuItems: [ + MenuItem { + icon: "palette" + text: qsTr("Vibrant") + value: "vibrant" + }, + MenuItem { + icon: "gesture" + text: qsTr("Expressive") + value: "expressive" + }, + MenuItem { + icon: "contrast" + text: qsTr("Monochrome") + value: "monochrome" + }, + MenuItem { + icon: "tonality" + text: qsTr("Neutral") + value: "neutral" + }, + MenuItem { + icon: "gradient" + text: qsTr("Tonal spot") + value: "tonalSpot" + }, + MenuItem { + icon: "target" + text: qsTr("Fidelity") + value: "fidelity" + }, + MenuItem { + icon: "article" + text: qsTr("Content") + value: "content" + }, + MenuItem { + icon: "colors" + text: qsTr("Rainbow") + value: "rainbow" + }, + MenuItem { + icon: "nutrition" + text: qsTr("Fruit salad") + value: "fruitSalad" + } + ] + + onSelected: item => { + Config.colors.schemeType = item.value; + Config.save(); + } + } + + Separator { + } + + SettingSwitch { + name: "Automatic color scheme" + object: Config.general.color + setting: "schemeGeneration" + } + + Separator { + } + + SettingSwitch { + name: "Smart color scheme" + object: Config.general.color + setting: "smart" + } + + Separator { + } + + SettingSpinner { + name: "Schedule dark mode" + object: Config.general.color + settings: ["scheduleDarkStart", "scheduleDarkEnd"] + } + } + + SettingsSection { + z: -1 + + SettingsHeader { + name: "Default Apps" + } + + SettingStringList { + addLabel: qsTr("Add terminal command") + name: "Terminal" + object: Config.general.apps + setting: "terminal" + } + + Separator { + } + + SettingStringList { + addLabel: qsTr("Add audio command") + name: "Audio" + object: Config.general.apps + setting: "audio" + } + + Separator { + } + + SettingStringList { + addLabel: qsTr("Add playback command") + name: "Playback" + object: Config.general.apps + setting: "playback" + } + + Separator { + } + + SettingStringList { + addLabel: qsTr("Add explorer command") + name: "Explorer" + object: Config.general.apps + setting: "explorer" + } } } diff --git a/Modules/Settings/Categories/Launcher.qml b/Modules/Settings/Categories/Launcher.qml new file mode 100644 index 0000000..18cb34f --- /dev/null +++ b/Modules/Settings/Categories/Launcher.qml @@ -0,0 +1,148 @@ +import qs.Modules.Settings.Controls +import qs.Config + +SettingsPage { + SettingsSection { + SettingsHeader { + name: "Launcher" + } + + SettingSpinBox { + name: "Max apps shown" + min: 1 + object: Config.launcher + setting: "maxAppsShown" + } + + Separator { + } + + SettingSpinBox { + name: "Max wallpapers shown" + min: 1 + object: Config.launcher + setting: "maxWallpapers" + } + + Separator { + } + + SettingInput { + name: "Action prefix" + object: Config.launcher + setting: "actionPrefix" + } + + Separator { + } + + SettingInput { + name: "Special prefix" + object: Config.launcher + setting: "specialPrefix" + } + } + + SettingsSection { + SettingsHeader { + name: "Fuzzy Search" + } + + SettingSwitch { + name: "Apps" + object: Config.launcher.useFuzzy + setting: "apps" + } + + Separator { + } + + SettingSwitch { + name: "Actions" + object: Config.launcher.useFuzzy + setting: "actions" + } + + Separator { + } + + SettingSwitch { + name: "Schemes" + object: Config.launcher.useFuzzy + setting: "schemes" + } + + Separator { + } + + SettingSwitch { + name: "Variants" + object: Config.launcher.useFuzzy + setting: "variants" + } + + Separator { + } + + SettingSwitch { + name: "Wallpapers" + object: Config.launcher.useFuzzy + setting: "wallpapers" + } + } + + SettingsSection { + SettingsHeader { + name: "Sizes" + } + + SettingSpinBox { + name: "Item width" + min: 1 + object: Config.launcher.sizes + setting: "itemWidth" + } + + Separator { + } + + SettingSpinBox { + name: "Item height" + min: 1 + object: Config.launcher.sizes + setting: "itemHeight" + } + + Separator { + } + + SettingSpinBox { + name: "Wallpaper width" + min: 1 + object: Config.launcher.sizes + setting: "wallpaperWidth" + } + + Separator { + } + + SettingSpinBox { + name: "Wallpaper height" + min: 1 + object: Config.launcher.sizes + setting: "wallpaperHeight" + } + } + + SettingsSection { + SettingsHeader { + name: "Actions" + } + + SettingActionList { + name: "Launcher actions" + object: Config.launcher + setting: "actions" + } + } +} diff --git a/Modules/Settings/Categories/Lockscreen.qml b/Modules/Settings/Categories/Lockscreen.qml new file mode 100644 index 0000000..7c9d2d2 --- /dev/null +++ b/Modules/Settings/Categories/Lockscreen.qml @@ -0,0 +1,90 @@ +import qs.Modules.Settings.Categories.Lockscreen +import qs.Modules.Settings.Controls +import qs.Config + +SettingsPage { + id: root + + SettingsSection { + SettingsHeader { + name: "Lockscreen" + } + + SettingSwitch { + name: "Recolor logo" + object: Config.lock + setting: "recolorLogo" + } + + Separator { + } + + SettingSwitch { + name: "Enable fingerprint" + object: Config.lock + setting: "enableFprint" + } + + Separator { + } + + SettingSpinBox { + name: "Max fingerprint tries" + min: 1 + object: Config.lock + setting: "maxFprintTries" + step: 1 + } + + Separator { + } + + SettingSpinBox { + name: "Blur amount" + min: 0 + object: Config.lock + setting: "blurAmount" + step: 1 + } + + Separator { + } + + SettingSpinBox { + name: "Height multiplier" + max: 2 + min: 0.1 + object: Config.lock.sizes + setting: "heightMult" + step: 0.05 + } + + Separator { + } + + SettingSpinBox { + name: "Aspect ratio" + max: 4 + min: 0.5 + object: Config.lock.sizes + setting: "ratio" + step: 0.05 + } + + Separator { + } + + SettingSpinBox { + name: "Center width" + min: 100 + object: Config.lock.sizes + setting: "centerWidth" + step: 10 + } + } + + SettingsSection { + Idle { + } + } +} diff --git a/Modules/Settings/Categories/Lockscreen/Idle.qml b/Modules/Settings/Categories/Lockscreen/Idle.qml new file mode 100644 index 0000000..eba0d67 --- /dev/null +++ b/Modules/Settings/Categories/Lockscreen/Idle.qml @@ -0,0 +1,83 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config +import qs.Modules.Settings.Controls + +ColumnLayout { + id: root + + function addTimeoutEntry() { + let list = [...Config.general.idle.timeouts]; + + list.push({ + name: "New Entry", + timeout: 600, + idleAction: "lock" + }); + + Config.general.idle.timeouts = list; + Config.save(); + } + + function updateTimeoutEntry(i, key, value) { + const list = [...Config.general.idle.timeouts]; + let entry = list[i]; + + entry[key] = value; + list[i] = entry; + + Config.general.idle.timeouts = list; + Config.save(); + } + + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + Settings { + name: "Idle Monitors" + } + + Repeater { + model: [...Config.general.idle.timeouts] + + SettingList { + Layout.fillWidth: true + + onAddActiveActionRequested: { + root.updateTimeoutEntry(index, "activeAction", ""); + } + onFieldEdited: function (key, value) { + root.updateTimeoutEntry(index, key, value); + } + } + } + + IconButton { + font.pointSize: Appearance.font.size.large + icon: "add" + + onClicked: root.addTimeoutEntry() + } + + component Settings: CustomRect { + id: settingsItem + + required property string name + + Layout.preferredHeight: 60 + Layout.preferredWidth: 200 + + CustomText { + id: text + + anchors.fill: parent + font.bold: true + font.pointSize: Appearance.font.size.large * 2 + text: settingsItem.name + verticalAlignment: Text.AlignVCenter + } + } +} diff --git a/Modules/Settings/Categories/Notifications.qml b/Modules/Settings/Categories/Notifications.qml new file mode 100644 index 0000000..ef0486b --- /dev/null +++ b/Modules/Settings/Categories/Notifications.qml @@ -0,0 +1,112 @@ +import qs.Modules.Settings.Controls +import qs.Config + +SettingsPage { + SettingsSection { + SettingsHeader { + name: "Notifications" + } + + SettingSwitch { + name: "Expire notifications" + object: Config.notifs + setting: "expire" + } + + Separator { + } + + SettingSpinBox { + name: "Default expire timeout" + min: 0 + object: Config.notifs + setting: "defaultExpireTimeout" + step: 100 + } + + Separator { + } + + SettingSpinBox { + name: "App notification cooldown" + min: 0 + object: Config.notifs + setting: "appNotifCooldown" + step: 100 + } + + Separator { + } + + SettingSpinBox { + name: "Clear threshold" + max: 1 + min: 0 + object: Config.notifs + setting: "clearThreshold" + step: 0.05 + } + + Separator { + } + + SettingSpinBox { + name: "Expand threshold" + min: 0 + object: Config.notifs + setting: "expandThreshold" + } + + Separator { + } + + SettingSwitch { + name: "Action on click" + object: Config.notifs + setting: "actionOnClick" + } + + Separator { + } + + SettingSpinBox { + name: "Group preview count" + min: 1 + object: Config.notifs + setting: "groupPreviewNum" + } + } + + SettingsSection { + SettingsHeader { + name: "Sizes" + } + + SettingSpinBox { + name: "Width" + min: 1 + object: Config.notifs.sizes + setting: "width" + } + + Separator { + } + + SettingSpinBox { + name: "Image size" + min: 1 + object: Config.notifs.sizes + setting: "image" + } + + Separator { + } + + SettingSpinBox { + name: "Badge size" + min: 1 + object: Config.notifs.sizes + setting: "badge" + } + } +} diff --git a/Modules/Settings/Categories/Osd.qml b/Modules/Settings/Categories/Osd.qml new file mode 100644 index 0000000..34d5724 --- /dev/null +++ b/Modules/Settings/Categories/Osd.qml @@ -0,0 +1,77 @@ +import qs.Modules.Settings.Controls +import qs.Config + +SettingsPage { + SettingsSection { + SettingsHeader { + name: "On Screen Display" + } + + SettingSwitch { + name: "Enable OSD" + object: Config.osd + setting: "enabled" + } + + Separator { + } + + SettingSpinBox { + name: "Hide delay" + min: 0 + object: Config.osd + setting: "hideDelay" + step: 100 + } + + Separator { + } + + SettingSwitch { + name: "Enable brightness OSD" + object: Config.osd + setting: "enableBrightness" + } + + Separator { + } + + SettingSwitch { + name: "Enable microphone OSD" + object: Config.osd + setting: "enableMicrophone" + } + + Separator { + } + + SettingSwitch { + name: "Brightness on all monitors" + object: Config.osd + setting: "allMonBrightness" + } + } + + SettingsSection { + SettingsHeader { + name: "Sizes" + } + + SettingSpinBox { + name: "Slider width" + min: 1 + object: Config.osd.sizes + setting: "sliderWidth" + } + + Separator { + } + + SettingSpinBox { + name: "Slider height" + min: 1 + object: Config.osd.sizes + setting: "sliderHeight" + } + } +} diff --git a/Modules/Settings/Categories/Services.qml b/Modules/Settings/Categories/Services.qml new file mode 100644 index 0000000..bae8112 --- /dev/null +++ b/Modules/Settings/Categories/Services.qml @@ -0,0 +1,120 @@ +import qs.Modules.Settings.Controls +import qs.Config + +SettingsPage { + SettingsSection { + SettingsHeader { + name: "Services" + } + + SettingInput { + name: "Weather location" + object: Config.services + setting: "weatherLocation" + } + + Separator { + } + + SettingSwitch { + name: "Use Fahrenheit" + object: Config.services + setting: "useFahrenheit" + } + + Separator { + } + + SettingSwitch { + name: "Use twelve hour clock" + object: Config.services + setting: "useTwelveHourClock" + } + + Separator { + } + + SettingSwitch { + name: "Enable ddcutil service" + object: Config.services + setting: "ddcutilService" + } + + Separator { + } + + SettingInput { + name: "GPU type" + object: Config.services + setting: "gpuType" + } + } + + SettingsSection { + SettingsHeader { + name: "Media" + } + + SettingSpinBox { + name: "Audio increment" + max: 1 + min: 0 + object: Config.services + setting: "audioIncrement" + step: 0.05 + } + + Separator { + } + + SettingSpinBox { + name: "Brightness increment" + max: 1 + min: 0 + object: Config.services + setting: "brightnessIncrement" + step: 0.05 + } + + Separator { + } + + SettingSpinBox { + name: "Max volume" + max: 5 + min: 0 + object: Config.services + setting: "maxVolume" + step: 0.05 + } + + Separator { + } + + SettingInput { + name: "Default player" + object: Config.services + setting: "defaultPlayer" + } + + Separator { + } + + SettingSpinBox { + name: "Visualizer bars" + min: 1 + object: Config.services + setting: "visualizerBars" + step: 1 + } + + Separator { + } + + SettingAliasList { + name: "Player aliases" + object: Config.services + setting: "playerAliases" + } + } +} diff --git a/Modules/Settings/Categories/Sidebar.qml b/Modules/Settings/Categories/Sidebar.qml new file mode 100644 index 0000000..b661e04 --- /dev/null +++ b/Modules/Settings/Categories/Sidebar.qml @@ -0,0 +1,26 @@ +import qs.Modules.Settings.Controls +import qs.Config + +SettingsPage { + SettingsSection { + SettingsHeader { + name: "Sidebar" + } + + SettingSwitch { + name: "Enable sidebar" + object: Config.sidebar + setting: "enabled" + } + + Separator { + } + + SettingSpinBox { + name: "Width" + min: 1 + object: Config.sidebar.sizes + setting: "width" + } + } +} diff --git a/Modules/Settings/Categories/Utilities.qml b/Modules/Settings/Categories/Utilities.qml new file mode 100644 index 0000000..3ec1e15 --- /dev/null +++ b/Modules/Settings/Categories/Utilities.qml @@ -0,0 +1,170 @@ +import qs.Modules.Settings.Controls +import qs.Config + +SettingsPage { + SettingsSection { + SettingsHeader { + name: "Utilities" + } + + SettingSwitch { + name: "Enable utilities" + object: Config.utilities + setting: "enabled" + } + + Separator { + } + + SettingSpinBox { + name: "Max toasts" + min: 1 + object: Config.utilities + setting: "maxToasts" + } + + Separator { + } + + SettingSpinBox { + name: "Panel width" + min: 1 + object: Config.utilities.sizes + setting: "width" + } + + Separator { + } + + SettingSpinBox { + name: "Toast width" + min: 1 + object: Config.utilities.sizes + setting: "toastWidth" + } + } + + SettingsSection { + SettingsHeader { + name: "Toasts" + } + + SettingSwitch { + name: "Config loaded" + object: Config.utilities.toasts + setting: "configLoaded" + } + + Separator { + } + + SettingSwitch { + name: "Charging changed" + object: Config.utilities.toasts + setting: "chargingChanged" + } + + Separator { + } + + SettingSwitch { + name: "Game mode changed" + object: Config.utilities.toasts + setting: "gameModeChanged" + } + + Separator { + } + + SettingSwitch { + name: "Do not disturb changed" + object: Config.utilities.toasts + setting: "dndChanged" + } + + Separator { + } + + SettingSwitch { + name: "Audio output changed" + object: Config.utilities.toasts + setting: "audioOutputChanged" + } + + Separator { + } + + SettingSwitch { + name: "Audio input changed" + object: Config.utilities.toasts + setting: "audioInputChanged" + } + + Separator { + } + + SettingSwitch { + name: "Caps lock changed" + object: Config.utilities.toasts + setting: "capsLockChanged" + } + + Separator { + } + + SettingSwitch { + name: "Num lock changed" + object: Config.utilities.toasts + setting: "numLockChanged" + } + + Separator { + } + + SettingSwitch { + name: "Keyboard layout changed" + object: Config.utilities.toasts + setting: "kbLayoutChanged" + } + + Separator { + } + + SettingSwitch { + name: "VPN changed" + object: Config.utilities.toasts + setting: "vpnChanged" + } + + Separator { + } + + SettingSwitch { + name: "Now playing" + object: Config.utilities.toasts + setting: "nowPlaying" + } + } + + SettingsSection { + SettingsHeader { + name: "VPN" + } + + SettingSwitch { + name: "Enable VPN integration" + object: Config.utilities.vpn + setting: "enabled" + } + + Separator { + } + + SettingStringList { + name: "Provider" + addLabel: qsTr("Add VPN provider") + object: Config.utilities.vpn + setting: "provider" + } + } +} diff --git a/Modules/Settings/Content.qml b/Modules/Settings/Content.qml index 1037a21..0e47180 100644 --- a/Modules/Settings/Content.qml +++ b/Modules/Settings/Content.qml @@ -12,8 +12,9 @@ Item { id: root property string currentCategory: "general" - readonly property real nonAnimHeight: view.implicitHeight + viewWrapper.anchors.margins * 2 - readonly property real nonAnimWidth: view.implicitWidth + 500 + viewWrapper.anchors.margins * 2 + readonly property real nonAnimHeight: Math.floor(screen.height / 1.5) + viewWrapper.anchors.margins * 2 + readonly property real nonAnimWidth: view.implicitWidth + Math.floor(screen.width / 2) + viewWrapper.anchors.margins * 2 + required property ShellScreen screen required property PersistentProperties visibilities implicitHeight: nonAnimHeight @@ -22,24 +23,41 @@ Item { Connections { function onCurrentCategoryChanged() { stack.pop(); - if (currentCategory === "general") { + if (currentCategory === "general") stack.push(general); - } else if (currentCategory === "wallpaper") { + else if (currentCategory === "wallpaper") stack.push(background); - } else if (currentCategory === "appearance") { + else if (currentCategory === "bar") + stack.push(bar); + else if (currentCategory === "appearance") stack.push(appearance); - } + else if (currentCategory === "lockscreen") + stack.push(lockscreen); + else if (currentCategory === "services") + stack.push(services); + else if (currentCategory === "notifications") + stack.push(notifications); + else if (currentCategory === "sidebar") + stack.push(sidebar); + else if (currentCategory === "utilities") + stack.push(utilities); + else if (currentCategory === "dashboard") + stack.push(dashboard); + else if (currentCategory === "osd") + stack.push(osd); + else if (currentCategory === "launcher") + stack.push(launcher); } target: root } - ClippingRectangle { + CustomClippingRect { id: viewWrapper anchors.fill: parent anchors.margins: Appearance.padding.smaller - color: "transparent" + radius: Appearance.rounding.large - Appearance.padding.smaller Item { id: view @@ -47,7 +65,6 @@ Item { anchors.bottom: parent.bottom anchors.left: parent.left anchors.top: parent.top - implicitHeight: layout.implicitHeight implicitWidth: layout.implicitWidth Categories { @@ -67,7 +84,7 @@ Item { anchors.right: parent.right anchors.top: parent.top color: DynamicColors.tPalette.m3surfaceContainer - radius: 4 + radius: Appearance.rounding.normal StackView { id: stack @@ -99,4 +116,67 @@ Item { Cat.Appearance { } } + + Component { + id: bar + + Cat.Bar { + } + } + + Component { + id: lockscreen + + Cat.Lockscreen { + } + } + + Component { + id: services + + Cat.Services { + } + } + + Component { + id: notifications + + Cat.Notifications { + } + } + + Component { + id: sidebar + + Cat.Sidebar { + } + } + + Component { + id: utilities + + Cat.Utilities { + } + } + + Component { + id: dashboard + + Cat.Dashboard { + } + } + + Component { + id: osd + + Cat.Osd { + } + } + + Component { + id: launcher + + Cat.Launcher { + } + } } diff --git a/Modules/Settings/Controls/Separator.qml b/Modules/Settings/Controls/Separator.qml new file mode 100644 index 0000000..86fcab4 --- /dev/null +++ b/Modules/Settings/Controls/Separator.qml @@ -0,0 +1,12 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +CustomRect { + id: root + + Layout.fillWidth: true + Layout.preferredHeight: 1 + color: DynamicColors.tPalette.m3outlineVariant +} diff --git a/Modules/Settings/Controls/SettingActionList.qml b/Modules/Settings/Controls/SettingActionList.qml new file mode 100644 index 0000000..90ccac5 --- /dev/null +++ b/Modules/Settings/Controls/SettingActionList.qml @@ -0,0 +1,235 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +ColumnLayout { + id: root + + required property string name + required property var object + required property string setting + + function addAction() { + const list = [...root.object[root.setting]]; + list.push({ + name: "New Action", + icon: "bolt", + description: "", + command: [], + enabled: true, + dangerous: false + }); + root.object[root.setting] = list; + Config.save(); + } + + function removeAction(index) { + const list = [...root.object[root.setting]]; + list.splice(index, 1); + root.object[root.setting] = list; + Config.save(); + } + + function updateAction(index, key, value) { + const list = [...root.object[root.setting]]; + const entry = list[index]; + entry[key] = value; + list[index] = entry; + root.object[root.setting] = list; + Config.save(); + } + + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + CustomText { + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.name + } + + Repeater { + model: [...root.object[root.setting]] + + CustomRect { + required property int index + required property var modelData + + Layout.fillWidth: true + Layout.preferredHeight: layout.implicitHeight + Appearance.padding.normal * 2 + color: DynamicColors.tPalette.m3surfaceContainer + radius: Appearance.rounding.normal + + ColumnLayout { + id: layout + + anchors.left: parent.left + anchors.margins: Appearance.padding.normal + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + + CustomText { + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: modelData.name ?? qsTr("Action") + } + + IconButton { + font.pointSize: Appearance.font.size.large + icon: "delete" + type: IconButton.Tonal + + onClicked: root.removeAction(index) + } + } + + Separator { + } + + RowLayout { + Layout.fillWidth: true + + CustomText { + Layout.fillWidth: true + text: qsTr("Name") + } + + CustomRect { + Layout.preferredHeight: 33 + Layout.preferredWidth: 350 + color: DynamicColors.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.full + + CustomTextField { + anchors.fill: parent + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + text: modelData.name ?? "" + + onEditingFinished: root.updateAction(index, "name", text) + } + } + } + + RowLayout { + Layout.fillWidth: true + + CustomText { + Layout.fillWidth: true + text: qsTr("Icon") + } + + CustomRect { + Layout.preferredHeight: 33 + Layout.preferredWidth: 350 + color: DynamicColors.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.full + + CustomTextField { + anchors.fill: parent + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + text: modelData.icon ?? "" + + onEditingFinished: root.updateAction(index, "icon", text) + } + } + } + + RowLayout { + Layout.fillWidth: true + + CustomText { + Layout.fillWidth: true + text: qsTr("Description") + } + + CustomRect { + Layout.preferredHeight: 33 + Layout.preferredWidth: 350 + color: DynamicColors.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.full + + CustomTextField { + anchors.fill: parent + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + text: modelData.description ?? "" + + onEditingFinished: root.updateAction(index, "description", text) + } + } + } + + StringListEditor { + Layout.fillWidth: true + addLabel: qsTr("Add command argument") + values: [...(modelData.command ?? [])] + + onListEdited: function (values) { + root.updateAction(index, "command", values); + } + } + + Separator { + } + + RowLayout { + Layout.fillWidth: true + + CustomText { + Layout.fillWidth: true + text: qsTr("Enabled") + } + + CustomSwitch { + checked: modelData.enabled ?? true + + onToggled: root.updateAction(index, "enabled", checked) + } + } + + Separator { + } + + RowLayout { + Layout.fillWidth: true + + CustomText { + Layout.fillWidth: true + text: qsTr("Dangerous") + } + + CustomSwitch { + checked: modelData.dangerous ?? false + + onToggled: root.updateAction(index, "dangerous", checked) + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + + IconButton { + font.pointSize: Appearance.font.size.large + icon: "add" + + onClicked: root.addAction() + } + + CustomText { + Layout.fillWidth: true + text: qsTr("Add action") + } + } +} diff --git a/Modules/Settings/Controls/SettingAliasList.qml b/Modules/Settings/Controls/SettingAliasList.qml new file mode 100644 index 0000000..f7f8959 --- /dev/null +++ b/Modules/Settings/Controls/SettingAliasList.qml @@ -0,0 +1,155 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +ColumnLayout { + id: root + + required property string name + required property var object + required property string setting + + function addAlias() { + const list = [...root.object[root.setting]]; + list.push({ + from: "", + to: "" + }); + root.object[root.setting] = list; + Config.save(); + } + + function removeAlias(index) { + const list = [...root.object[root.setting]]; + list.splice(index, 1); + root.object[root.setting] = list; + Config.save(); + } + + function updateAlias(index, key, value) { + const list = [...root.object[root.setting]]; + const entry = [...list[index]]; + entry[key] = value; + list[index] = entry; + root.object[root.setting] = list; + Config.save(); + } + + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + CustomText { + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.name + } + + Repeater { + model: [...root.object[root.setting]] + + Item { + required property int index + required property var modelData + + Layout.fillWidth: true + Layout.preferredHeight: layout.implicitHeight + Appearance.padding.smaller * 2 + + CustomRect { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: -(Appearance.spacing.smaller / 2) + color: DynamicColors.tPalette.m3outlineVariant + implicitHeight: 1 + visible: index !== 0 + } + + ColumnLayout { + id: layout + + anchors.left: parent.left + anchors.margins: Appearance.padding.small + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + + CustomText { + Layout.fillWidth: true + text: qsTr("From") + } + + CustomRect { + Layout.fillWidth: true + Layout.preferredHeight: 33 + color: DynamicColors.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.full + + CustomTextField { + anchors.fill: parent + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + text: modelData.from ?? "" + + onEditingFinished: root.updateAlias(index, "from", text) + } + } + + IconButton { + font.pointSize: Appearance.font.size.large + icon: "delete" + type: IconButton.Tonal + + onClicked: root.removeAlias(index) + } + } + + RowLayout { + Layout.fillWidth: true + + CustomText { + Layout.fillWidth: true + text: qsTr("To") + } + + CustomRect { + Layout.fillWidth: true + Layout.preferredHeight: 33 + color: DynamicColors.tPalette.m3surface + radius: Appearance.rounding.small + + CustomTextField { + anchors.fill: parent + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + text: modelData.to ?? "" + + onEditingFinished: root.updateAlias(index, "to", text) + } + } + } + } + } + } + + RowLayout { + Layout.fillWidth: true + + IconButton { + font.pointSize: Appearance.font.size.large + icon: "add" + + onClicked: root.addAlias() + } + + CustomText { + Layout.fillWidth: true + text: qsTr("Add alias") + } + } +} diff --git a/Modules/Settings/Controls/SettingBarEntryList.qml b/Modules/Settings/Controls/SettingBarEntryList.qml new file mode 100644 index 0000000..f082133 --- /dev/null +++ b/Modules/Settings/Controls/SettingBarEntryList.qml @@ -0,0 +1,523 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import QtQuick +import QtQuick.Layouts +import QtQml.Models +import qs.Components +import qs.Config + +Item { + id: root + + property bool dragActive: false + property real dragHeight: 0 + property real dragStartX: 0 + property real dragStartY: 0 + property real dragX: 0 + property real dragY: 0 + property var draggedModelData: null + property string draggedUid: "" + property bool dropAnimating: false + required property string name + required property var object + property var pendingCommitEntries: [] + required property string setting + property int uidCounter: 0 + property var visualEntries: [] + + function beginVisualDrag(uid, modelData, item) { + const pos = item.mapToItem(root, 0, 0); + + root.draggedUid = uid; + root.draggedModelData = modelData; + root.dragHeight = item.height; + root.dragStartX = pos.x; + root.dragStartY = pos.y; + root.dragX = pos.x; + root.dragY = pos.y; + root.dragActive = true; + root.dropAnimating = false; + root.pendingCommitEntries = []; + } + + function commitVisualOrder(entries) { + const list = []; + + for (let i = 0; i < entries.length; i++) + list.push(entries[i].entry); + + root.object[root.setting] = list; + Config.save(); + root.rebuildVisualEntries(); + } + + function endVisualDrag() { + const entries = root.visualEntries.slice(); + const finalIndex = root.indexForUid(root.draggedUid); + const finalItem = listView.itemAtIndex(finalIndex); + + root.dragActive = false; + + if (!finalItem) { + root.pendingCommitEntries = entries; + root.finishVisualDrag(); + return; + } + + const pos = finalItem.mapToItem(root, 0, 0); + + root.pendingCommitEntries = entries; + root.dropAnimating = true; + settleX.to = pos.x; + settleY.to = pos.y; + settleAnim.start(); + } + + function ensureVisualEntries() { + if (!root.dragActive && !root.dropAnimating) + root.rebuildVisualEntries(); + } + + function finishVisualDrag() { + const entries = root.pendingCommitEntries.slice(); + + root.dragActive = false; + root.dropAnimating = false; + root.draggedUid = ""; + root.draggedModelData = null; + root.pendingCommitEntries = []; + root.dragHeight = 0; + + root.commitVisualOrder(entries); + } + + function iconForId(id) { + switch (id) { + case "workspaces": + return "dashboard"; + case "audio": + return "volume_up"; + case "media": + return "play_arrow"; + case "resources": + return "monitoring"; + case "updates": + return "system_update"; + case "dash": + return "space_dashboard"; + case "spacer": + return "horizontal_rule"; + case "activeWindow": + return "web_asset"; + case "tray": + return "widgets"; + case "upower": + return "battery_full"; + case "network": + return "wifi"; + case "clock": + return "schedule"; + case "notifBell": + return "notifications"; + default: + return "drag_indicator"; + } + } + + function indexForUid(uid) { + for (let i = 0; i < root.visualEntries.length; i++) { + if (root.visualEntries[i].uid === uid) + return i; + } + + return -1; + } + + function labelForId(id) { + switch (id) { + case "workspaces": + return qsTr("Workspaces"); + case "audio": + return qsTr("Audio"); + case "media": + return qsTr("Media"); + case "resources": + return qsTr("Resources"); + case "updates": + return qsTr("Updates"); + case "dash": + return qsTr("Dash"); + case "spacer": + return qsTr("Spacer"); + case "activeWindow": + return qsTr("Active window"); + case "tray": + return qsTr("Tray"); + case "upower": + return qsTr("Power"); + case "network": + return qsTr("Network"); + case "clock": + return qsTr("Clock"); + case "notifBell": + return qsTr("Notification bell"); + default: + return id; + } + } + + function moveArrayItem(list, from, to) { + const next = list.slice(); + const [item] = next.splice(from, 1); + next.splice(to, 0, item); + return next; + } + + function previewVisualMove(from, hovered, before) { + let to = hovered + (before ? 0 : 1); + + if (to > from) + to -= 1; + + to = Math.max(0, Math.min(visualModel.items.count - 1, to)); + + if (from === to) + return; + + visualModel.items.move(from, to); + root.visualEntries = root.moveArrayItem(root.visualEntries, from, to); + } + + function rebuildVisualEntries() { + const entries = root.object[root.setting] ?? []; + const next = []; + + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + let existing = null; + + for (let j = 0; j < root.visualEntries.length; j++) { + if (root.visualEntries[j].entry === entry) { + existing = root.visualEntries[j]; + break; + } + } + + if (existing) + next.push(existing); + else + next.push({ + uid: `entry-${root.uidCounter++}`, + entry + }); + } + + root.visualEntries = next; + } + + function updateEntry(index, value) { + const list = [...root.object[root.setting]]; + const entry = list[index]; + entry.enabled = value; + list[index] = entry; + root.object[root.setting] = list; + Config.save(); + root.ensureVisualEntries(); + } + + Layout.fillWidth: true + implicitHeight: layout.implicitHeight + + Component.onCompleted: root.rebuildVisualEntries() + + ParallelAnimation { + id: settleAnim + + onFinished: root.finishVisualDrag() + + Anim { + id: settleX + + duration: Appearance.anim.durations.normal + property: "dragX" + target: root + } + + Anim { + id: settleY + + duration: Appearance.anim.durations.normal + property: "dragY" + target: root + } + } + + ColumnLayout { + id: layout + + anchors.fill: parent + spacing: Appearance.spacing.smaller + + CustomText { + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.name + } + + DelegateModel { + id: visualModel + + delegate: entryDelegate + + model: ScriptModel { + objectProp: "uid" + values: root.visualEntries + } + } + + ListView { + id: listView + + Layout.fillWidth: true + Layout.preferredHeight: contentHeight + boundsBehavior: Flickable.StopAtBounds + clip: false + implicitHeight: contentHeight + implicitWidth: width + interactive: !(root.dragActive || root.dropAnimating) + model: visualModel + spacing: Appearance.spacing.small + + add: Transition { + Anim { + properties: "opacity,scale" + to: 1 + } + } + addDisplaced: Transition { + Anim { + duration: Appearance.anim.durations.normal + property: "y" + } + } + displaced: Transition { + Anim { + duration: Appearance.anim.durations.normal + property: "y" + } + } + move: Transition { + Anim { + duration: Appearance.anim.durations.normal + property: "y" + } + } + removeDisplaced: Transition { + Anim { + duration: Appearance.anim.durations.normal + property: "y" + } + } + } + } + + Loader { + active: root.dragActive || root.dropAnimating + asynchronous: false + + sourceComponent: Item { + Drag.active: root.dragActive + Drag.hotSpot.x: width / 2 + Drag.hotSpot.y: height / 2 + height: proxyRect.implicitHeight + implicitHeight: proxyRect.implicitHeight + implicitWidth: listView.width + visible: root.draggedModelData !== null + width: listView.width + x: root.dragX + y: root.dragY + z: 100 + + Drag.source: QtObject { + property string uid: root.draggedUid + property int visualIndex: root.indexForUid(root.draggedUid) + } + + CustomRect { + id: proxyRect + + color: DynamicColors.tPalette.m3surface + implicitHeight: proxyRow.implicitHeight + Appearance.padding.small * 2 + implicitWidth: parent.width + opacity: 0.95 + radius: Appearance.rounding.normal + width: parent.width + + RowLayout { + id: proxyRow + + anchors.fill: parent + anchors.margins: Appearance.padding.small + spacing: Appearance.spacing.normal + + CustomRect { + color: Qt.alpha(DynamicColors.palette.m3onSurface, 0.12) + implicitHeight: 32 + implicitWidth: implicitHeight + radius: Appearance.rounding.small + + MaterialIcon { + anchors.centerIn: parent + color: DynamicColors.palette.m3onSurfaceVariant + text: "drag_indicator" + } + } + + MaterialIcon { + color: DynamicColors.palette.m3onSurfaceVariant + text: root.iconForId(root.draggedModelData?.entry?.id ?? "") + } + + CustomText { + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.labelForId(root.draggedModelData?.entry?.id ?? "") + } + + CustomSwitch { + checked: root.draggedModelData?.entry?.enabled ?? true + enabled: false + } + } + } + } + } + + Component { + id: entryDelegate + + DropArea { + id: slot + + readonly property var entryData: modelData.entry + required property var modelData + readonly property string uid: modelData.uid + + function previewReorder(drag) { + const source = drag.source; + if (!source || !source.uid || source.uid === slot.uid) + return; + + const from = source.visualIndex; + const hovered = slot.DelegateModel.itemsIndex; + + if (from < 0 || hovered < 0) + return; + + root.previewVisualMove(from, hovered, drag.y < height / 2); + } + + height: entryRow.implicitHeight + implicitHeight: entryRow.implicitHeight + implicitWidth: listView.width + width: ListView.view ? ListView.view.width : listView.width + + onEntered: drag => previewReorder(drag) + onPositionChanged: drag => previewReorder(drag) + + CustomRect { + id: entryRow + + anchors.fill: parent + color: DynamicColors.tPalette.m3surface + implicitHeight: entryLayout.implicitHeight + Appearance.padding.small * 2 + implicitWidth: parent.width + opacity: root.draggedUid === slot.uid ? 0 : 1 + radius: Appearance.rounding.full + + Behavior on opacity { + Anim { + } + } + + RowLayout { + id: entryLayout + + anchors.fill: parent + anchors.margins: Appearance.padding.small + spacing: Appearance.spacing.normal + + CustomRect { + id: handle + + color: Qt.alpha(DynamicColors.palette.m3onSurface, handleDrag.active ? 0.12 : handleHover.hovered ? 0.09 : 0.06) + implicitHeight: 32 + implicitWidth: implicitHeight + radius: Appearance.rounding.full + + Behavior on color { + CAnim { + } + } + + MaterialIcon { + anchors.centerIn: parent + color: DynamicColors.palette.m3onSurfaceVariant + text: "drag_indicator" + } + + HoverHandler { + id: handleHover + + cursorShape: handleDrag.active ? Qt.ClosedHandCursor : Qt.OpenHandCursor + } + + DragHandler { + id: handleDrag + + enabled: true + grabPermissions: PointerHandler.CanTakeOverFromAnything + target: null + xAxis.enabled: false + yAxis.enabled: true + + onActiveChanged: { + if (active) { + root.beginVisualDrag(slot.uid, slot.modelData, entryRow); + } else if (root.draggedUid === slot.uid) { + root.endVisualDrag(); + } + } + onActiveTranslationChanged: { + if (!active || root.draggedUid !== slot.uid) + return; + + root.dragX = root.dragStartX; + root.dragY = root.dragStartY + activeTranslation.y; + } + } + } + + MaterialIcon { + color: DynamicColors.palette.m3onSurfaceVariant + text: root.iconForId(slot.entryData.id) + } + + CustomText { + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.labelForId(slot.entryData.id) + } + + CustomSwitch { + Layout.rightMargin: Appearance.padding.small + checked: slot.entryData.enabled ?? true + + onToggled: root.updateEntry(slot.DelegateModel.itemsIndex, checked) + } + } + } + } + } +} diff --git a/Modules/Settings/Controls/SettingInput.qml b/Modules/Settings/Controls/SettingInput.qml new file mode 100644 index 0000000..1421664 --- /dev/null +++ b/Modules/Settings/Controls/SettingInput.qml @@ -0,0 +1,65 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +Item { + id: root + + required property string name + required property var object + required property string setting + + function formattedValue(): string { + const value = root.object[root.setting]; + + if (value === null || value === undefined) + return ""; + + return String(value); + } + + Layout.fillWidth: true + Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 + + RowLayout { + id: row + + anchors.left: parent.left + anchors.margins: Appearance.padding.small + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + CustomText { + id: text + + Layout.alignment: Qt.AlignLeft + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.name + } + + CustomRect { + id: rect + + Layout.preferredHeight: 33 + Layout.preferredWidth: Math.max(Math.min(textField.contentWidth + Appearance.padding.normal * 2, 550), 50) + color: DynamicColors.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.full + + CustomTextField { + id: textField + + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + implicitWidth: Math.min(contentWidth, 550) + text: root.formattedValue() + + onEditingFinished: { + root.object[root.setting] = textField.text; + Config.save(); + } + } + } + } +} diff --git a/Modules/Settings/Controls/SettingList.qml b/Modules/Settings/Controls/SettingList.qml new file mode 100644 index 0000000..3d2dd66 --- /dev/null +++ b/Modules/Settings/Controls/SettingList.qml @@ -0,0 +1,250 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +Item { + id: root + + required property int index + required property var modelData + + signal addActiveActionRequested + signal fieldEdited(string key, var value) + + Layout.fillWidth: true + Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 + + CustomRect { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: -(Appearance.spacing.smaller / 2) + color: DynamicColors.tPalette.m3outlineVariant + implicitHeight: 1 + visible: root.index !== 0 + } + + RowLayout { + id: row + + anchors.left: parent.left + anchors.margins: Appearance.padding.small + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Appearance.spacing.large + + Item { + id: nameCell + + property string draftName: "" + property bool editing: false + + function beginEdit() { + draftName = root.modelData.name ?? ""; + editing = true; + nameEditor.forceActiveFocus(); + } + + function cancelEdit() { + draftName = root.modelData.name ?? ""; + editing = false; + } + + function commitEdit() { + editing = false; + + if (draftName !== (root.modelData.name ?? "")) { + root.fieldEdited("name", draftName); + } + } + + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + Layout.fillHeight: true + Layout.preferredWidth: root.width / 2 + + HoverHandler { + id: nameHover + + } + + CustomText { + anchors.left: parent.left + anchors.right: editButton.left + anchors.rightMargin: Appearance.spacing.small + anchors.verticalCenter: parent.verticalCenter + elide: Text.ElideRight // enable if CustomText supports it + font.pointSize: Appearance.font.size.larger + text: root.modelData.name + visible: !nameCell.editing + } + + IconButton { + id: editButton + + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + font.pointSize: Appearance.font.size.large + icon: "edit" + visible: nameHover.hovered && !nameCell.editing + + onClicked: nameCell.beginEdit() + } + + CustomRect { + anchors.fill: parent + color: DynamicColors.tPalette.m3surface + radius: Appearance.rounding.small + visible: nameCell.editing + + CustomTextField { + id: nameEditor + + anchors.fill: parent + text: nameCell.draftName + + Keys.onEscapePressed: { + nameCell.cancelEdit(); + } + onEditingFinished: { + nameCell.commitEdit(); + } + onTextEdited: { + nameCell.draftName = nameEditor.text; + } + } + } + } + + VSeparator { + } + + ColumnLayout { + id: cLayout + + Layout.fillWidth: true + + RowLayout { + Layout.fillWidth: true + + CustomText { + Layout.fillWidth: true + text: qsTr("Timeout") + } + + CustomRect { + Layout.preferredHeight: 33 + Layout.preferredWidth: Math.max(Math.min(timeField.contentWidth + Appearance.padding.normal * 3, 200), 50) + color: DynamicColors.tPalette.m3surface + radius: Appearance.rounding.small + + CustomTextField { + id: timeField + + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + text: String(root.modelData.timeout ?? "") + + onEditingFinished: { + root.fieldEdited("timeout", Number(text)); + } + } + } + } + + Separator { + } + + RowLayout { + Layout.fillWidth: true + + CustomText { + Layout.fillWidth: true + text: qsTr("Idle Action") + } + + CustomRect { + Layout.preferredHeight: 33 + Layout.preferredWidth: Math.max(Math.min(idleField.contentWidth + Appearance.padding.normal * 3, 200), 50) + color: DynamicColors.tPalette.m3surface + radius: Appearance.rounding.small + + CustomTextField { + id: idleField + + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + text: root.modelData.idleAction ?? "" + + onEditingFinished: { + root.fieldEdited("idleAction", text); + } + } + } + } + + Separator { + } + + Item { + Layout.fillWidth: true + implicitHeight: activeActionRow.visible ? activeActionRow.implicitHeight : addButtonRow.implicitHeight + + RowLayout { + id: activeActionRow + + anchors.left: parent.left + anchors.right: parent.right + visible: root.modelData.activeAction !== undefined + + CustomText { + Layout.fillWidth: true + text: qsTr("Active Action") + } + + CustomRect { + Layout.preferredHeight: 33 + Layout.preferredWidth: Math.max(Math.min(actionField.contentWidth + Appearance.padding.normal * 3, 200), 50) + color: DynamicColors.tPalette.m3surface + radius: Appearance.rounding.small + + CustomTextField { + id: actionField + + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + text: root.modelData.activeAction ?? "" + + onEditingFinished: { + root.fieldEdited("activeAction", text); + } + } + } + } + + RowLayout { + id: addButtonRow + + anchors.left: parent.left + anchors.right: parent.right + visible: root.modelData.activeAction === undefined + + IconButton { + id: button + + Layout.alignment: Qt.AlignLeft + font.pointSize: Appearance.font.size.large + icon: "add" + + onClicked: root.addActiveActionRequested() + } + + CustomText { + Layout.alignment: Qt.AlignLeft + text: qsTr("Add active action") + } + } + } + } + } +} diff --git a/Modules/Settings/Controls/SettingReadOnly.qml b/Modules/Settings/Controls/SettingReadOnly.qml new file mode 100644 index 0000000..1fe580a --- /dev/null +++ b/Modules/Settings/Controls/SettingReadOnly.qml @@ -0,0 +1,36 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +Item { + id: root + + required property string name + required property string value + + Layout.fillWidth: true + Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 + + RowLayout { + id: row + + anchors.left: parent.left + anchors.margins: Appearance.padding.small + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + CustomText { + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.name + } + + CustomText { + color: DynamicColors.palette.m3onSurfaceVariant + font.family: Appearance.font.family.mono + font.pointSize: Appearance.font.size.normal + text: root.value + } + } +} diff --git a/Modules/Settings/Controls/SettingSpinBox.qml b/Modules/Settings/Controls/SettingSpinBox.qml new file mode 100644 index 0000000..0cbf240 --- /dev/null +++ b/Modules/Settings/Controls/SettingSpinBox.qml @@ -0,0 +1,47 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +Item { + id: root + + required property string name + required property var object + required property string setting + property real max: Infinity + property real min: -Infinity + property real step: 1 + + Layout.fillWidth: true + Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 + + RowLayout { + id: row + + anchors.left: parent.left + anchors.margins: Appearance.padding.small + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + CustomText { + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.name + } + + CustomSpinBox { + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + max: root.max + min: root.min + step: root.step + value: Number(root.object[root.setting] ?? 0) + + onValueModified: function (value) { + root.object[root.setting] = value; + Config.save(); + } + } + } +} diff --git a/Modules/Settings/Controls/SettingSpinner.qml b/Modules/Settings/Controls/SettingSpinner.qml new file mode 100644 index 0000000..2663343 --- /dev/null +++ b/Modules/Settings/Controls/SettingSpinner.qml @@ -0,0 +1,87 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +Item { + id: root + + required property string name + required property var object + required property list settings + + function commitChoice(choice: int, setting: string): void { + root.object[setting] = choice; + Config.save(); + } + + function formattedValue(setting: string): string { + const value = root.object[setting]; + + if (value === null || value === undefined) + return ""; + + return String(value); + } + + function hourToAmPm(hour) { + var h = Number(hour) % 24; + var d = new Date(2000, 0, 1, h, 0, 0); + return Qt.formatTime(d, "h AP"); + } + + Layout.fillWidth: true + Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 + + RowLayout { + id: row + + anchors.left: parent.left + anchors.margins: Appearance.padding.small + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + ColumnLayout { + Layout.fillHeight: true + Layout.fillWidth: true + + CustomText { + id: text + + Layout.alignment: Qt.AlignLeft + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.name + } + + CustomText { + Layout.alignment: Qt.AlignLeft + color: DynamicColors.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.normal + text: qsTr("Dark mode will turn on at %1, and turn off at %2.").arg(root.hourToAmPm(root.object[root.settings[0]])).arg(root.hourToAmPm(root.object[root.settings[1]])) + } + } + + SpinnerButton { + Layout.preferredHeight: Appearance.font.size.large + Appearance.padding.smaller * 2 + Layout.preferredWidth: height * 2 + currentIndex: root.object[root.settings[0]] + text: root.formattedValue(root.settings[0]) + + menu.onItemSelected: item => { + root.commitChoice(item, root.settings[0]); + } + } + + SpinnerButton { + Layout.preferredHeight: Appearance.font.size.large + Appearance.padding.smaller * 2 + Layout.preferredWidth: height * 2 + currentIndex: root.object[root.settings[1]] + text: root.formattedValue(root.settings[1]) + + menu.onItemSelected: item => { + root.commitChoice(item, root.settings[1]); + } + } + } +} diff --git a/Modules/Settings/Controls/SettingStringList.qml b/Modules/Settings/Controls/SettingStringList.qml new file mode 100644 index 0000000..c78414b --- /dev/null +++ b/Modules/Settings/Controls/SettingStringList.qml @@ -0,0 +1,41 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +Item { + id: root + + required property string name + required property var object + required property string setting + property string addLabel: qsTr("Add entry") + + Layout.fillWidth: true + Layout.preferredHeight: layout.implicitHeight + + ColumnLayout { + id: layout + + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.small + + CustomText { + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.name + } + + StringListEditor { + Layout.fillWidth: true + addLabel: root.addLabel + values: [...(root.object[root.setting] ?? [])] + + onListEdited: function (values) { + root.object[root.setting] = values; + Config.save(); + } + } + } +} diff --git a/Modules/Settings/Controls/SettingSwitch.qml b/Modules/Settings/Controls/SettingSwitch.qml index 524e740..09cff93 100644 --- a/Modules/Settings/Controls/SettingSwitch.qml +++ b/Modules/Settings/Controls/SettingSwitch.qml @@ -3,7 +3,7 @@ import QtQuick.Layouts import qs.Components import qs.Config -RowLayout { +Item { id: root required property string name @@ -11,26 +11,35 @@ RowLayout { required property string setting Layout.fillWidth: true - Layout.preferredHeight: 42 + Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 - CustomText { - id: text + RowLayout { + id: row - Layout.alignment: Qt.AlignLeft - Layout.fillWidth: true - font.pointSize: 16 - text: root.name - } + anchors.left: parent.left + anchors.margins: Appearance.padding.small + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter - CustomSwitch { - id: cswitch + CustomText { + id: text - Layout.alignment: Qt.AlignRight - checked: root.object[root.setting] + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + Layout.fillWidth: true + font.pointSize: Appearance.font.size.larger + text: root.name + } - onToggled: { - root.object[root.setting] = checked; - Config.save(); + CustomSwitch { + id: cswitch + + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter + checked: root.object[root.setting] + + onToggled: { + root.object[root.setting] = checked; + Config.save(); + } } } } diff --git a/Modules/Settings/Controls/SettingsHeader.qml b/Modules/Settings/Controls/SettingsHeader.qml new file mode 100644 index 0000000..257757a --- /dev/null +++ b/Modules/Settings/Controls/SettingsHeader.qml @@ -0,0 +1,21 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +CustomRect { + id: root + + required property string name + + Layout.preferredHeight: 60 + Layout.preferredWidth: 200 + + CustomText { + anchors.fill: parent + font.bold: true + font.pointSize: Appearance.font.size.large * 2 + text: root.name + verticalAlignment: Text.AlignVCenter + } +} diff --git a/Modules/Settings/Controls/SettingsPage.qml b/Modules/Settings/Controls/SettingsPage.qml new file mode 100644 index 0000000..1cfefec --- /dev/null +++ b/Modules/Settings/Controls/SettingsPage.qml @@ -0,0 +1,41 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config +import qs.Helpers + +CustomFlickable { + id: root + + default property alias contentData: clayout.data + + contentHeight: clayout.implicitHeight + + TapHandler { + acceptedButtons: Qt.LeftButton + + onTapped: function (eventPoint) { + const menu = SettingsDropdowns.activeMenu; + if (!menu) + return; + + const p = eventPoint.scenePosition; + + if (SettingsDropdowns.hit(SettingsDropdowns.activeTrigger, p)) + return; + + if (SettingsDropdowns.hit(menu, p)) + return; + + SettingsDropdowns.closeActive(); + } + } + + ColumnLayout { + id: clayout + + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.small + } +} diff --git a/Modules/Settings/Controls/SettingsSection.qml b/Modules/Settings/Controls/SettingsSection.qml new file mode 100644 index 0000000..081447a --- /dev/null +++ b/Modules/Settings/Controls/SettingsSection.qml @@ -0,0 +1,26 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +CustomRect { + id: root + + default property alias contentData: layout.data + property real contentPadding: Appearance.padding.large + + Layout.fillWidth: true + Layout.preferredHeight: layout.implicitHeight + contentPadding * 2 + color: DynamicColors.tPalette.m3surfaceContainer + radius: Appearance.rounding.normal - Appearance.padding.smaller + + ColumnLayout { + id: layout + + anchors.left: parent.left + anchors.margins: root.contentPadding + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: Appearance.spacing.normal + } +} diff --git a/Modules/Settings/Controls/SpinnerButton.qml b/Modules/Settings/Controls/SpinnerButton.qml new file mode 100644 index 0000000..7d5bee2 --- /dev/null +++ b/Modules/Settings/Controls/SpinnerButton.qml @@ -0,0 +1,42 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Helpers +import qs.Config + +CustomRect { + id: root + + property alias currentIndex: menu.currentIndex + property alias expanded: menu.expanded + property alias label: label + property alias menu: menu + property alias text: label.text + + color: DynamicColors.palette.m3primary + radius: Appearance.rounding.full + + CustomText { + id: label + + anchors.centerIn: parent + color: DynamicColors.palette.m3onPrimary + font.pointSize: Appearance.font.size.large + } + + StateLayer { + function onClicked(): void { + SettingsDropdowns.toggle(menu, root); + } + } + + PathViewMenu { + id: menu + + anchors.centerIn: parent + from: 1 + implicitWidth: root.width + itemHeight: root.height + to: 24 + } +} diff --git a/Modules/Settings/Controls/StringListEditor.qml b/Modules/Settings/Controls/StringListEditor.qml new file mode 100644 index 0000000..701e334 --- /dev/null +++ b/Modules/Settings/Controls/StringListEditor.qml @@ -0,0 +1,107 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +ColumnLayout { + id: root + + property string addLabel: qsTr("Add entry") + property var values: [] + + signal listEdited(var values) + + function addValue() { + const list = [...root.values]; + list.push(""); + root.listEdited(list); + } + + function removeValue(index) { + const list = [...root.values]; + list.splice(index, 1); + root.listEdited(list); + } + + function updateValue(index, value) { + const list = [...root.values]; + list[index] = value; + root.listEdited(list); + } + + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + Repeater { + model: [...root.values] + + Item { + required property int index + required property var modelData + + Layout.fillWidth: true + Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 + + CustomRect { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.topMargin: -(Appearance.spacing.smaller / 2) + color: DynamicColors.tPalette.m3outlineVariant + implicitHeight: 1 + visible: index !== 0 + } + + RowLayout { + id: row + + anchors.left: parent.left + anchors.margins: Appearance.padding.small + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + CustomRect { + Layout.fillWidth: true + Layout.preferredHeight: 33 + color: DynamicColors.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.full + + CustomTextField { + anchors.fill: parent + anchors.leftMargin: Appearance.padding.normal + anchors.rightMargin: Appearance.padding.normal + text: String(modelData ?? "") + + onEditingFinished: root.updateValue(index, text) + } + } + + IconButton { + font.pointSize: Appearance.font.size.large + icon: "delete" + type: IconButton.Tonal + + onClicked: root.removeValue(index) + } + } + } + } + + RowLayout { + Layout.fillWidth: true + + IconButton { + font.pointSize: Appearance.font.size.large + icon: "add" + + onClicked: root.addValue() + } + + CustomText { + Layout.fillWidth: true + text: root.addLabel + } + } +} diff --git a/Modules/Settings/Controls/VSeparator.qml b/Modules/Settings/Controls/VSeparator.qml new file mode 100644 index 0000000..c7d507e --- /dev/null +++ b/Modules/Settings/Controls/VSeparator.qml @@ -0,0 +1,12 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Config + +CustomRect { + id: root + + Layout.fillHeight: true + Layout.preferredWidth: 1 + color: DynamicColors.tPalette.m3outlineVariant +} diff --git a/Modules/Settings/Wrapper.qml b/Modules/Settings/Wrapper.qml index dec6264..dbcd141 100644 --- a/Modules/Settings/Wrapper.qml +++ b/Modules/Settings/Wrapper.qml @@ -8,6 +8,7 @@ Item { id: root required property var panels + required property ShellScreen screen required property PersistentProperties visibilities implicitHeight: 0 @@ -46,16 +47,21 @@ Item { } ] - Loader { - id: content + CustomClippingRect { + anchors.fill: parent - active: true - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - visible: true + Loader { + id: content - sourceComponent: Content { - visibilities: root.visibilities + active: true + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + visible: true + + sourceComponent: Content { + screen: root.screen + visibilities: root.visibilities + } } } } diff --git a/Modules/Shortcuts.qml b/Modules/Shortcuts.qml index 7da6200..ced923c 100644 --- a/Modules/Shortcuts.qml +++ b/Modules/Shortcuts.qml @@ -24,6 +24,15 @@ Scope { } } + CustomShortcut { + name: "toggle-drawing" + + onPressed: { + const visibilities = Visibilities.getForActive(); + visibilities.isDrawing = !visibilities.isDrawing; + } + } + CustomShortcut { name: "toggle-nc" diff --git a/Modules/TrayItem.qml b/Modules/TrayItem.qml index c613677..50fbf62 100644 --- a/Modules/TrayItem.qml +++ b/Modules/TrayItem.qml @@ -9,7 +9,6 @@ import qs.Config Item { id: root - required property PanelWindow bar property bool hasLoaded: false required property int ind required property SystemTrayItem item @@ -29,9 +28,10 @@ Item { root.popouts.currentName = `traymenu${root.ind}`; root.popouts.currentCenter = Qt.binding(() => root.mapToItem(root.loader, root.implicitWidth / 2, 0).x); root.popouts.hasCurrent = true; - if (visibilities.sidebar || visibilities.dashboard) { + if (visibilities.sidebar || visibilities.dashboard || visibilities.settings) { visibilities.sidebar = false; visibilities.dashboard = false; + visibilities.settings = false; } } } @@ -46,22 +46,4 @@ Item { layer.enabled: DynamicColors.light source: root.item.icon } - - // Image { - // id: icon - // - // property bool batteryHDPI: root.bar.screen.x < 0 && root.item.icon.includes("battery") - // property bool nmHDPI: root.bar.screen.x < 0 && root.item.icon.includes("nm-") - // - // anchors.centerIn: parent - // width: batteryHDPI ? 26 : ( nmHDPI ? 25 : 22 ) - // height: batteryHDPI ? 26 : ( nmHDPI ? 25 : 22 ) - // source: root.item.icon - // mipmap: true - // smooth: ( batteryHDPI || nmHDPI ) ? false : true - // asynchronous: true - // sourceSize.width: ( batteryHDPI || nmHDPI ) ? 16 : 22 - // sourceSize.height: ( batteryHDPI || nmHDPI ) ? 16 : 22 - // fillMode: Image.PreserveAspectFit - // } } diff --git a/Modules/TrayMenu.qml b/Modules/TrayMenu.qml deleted file mode 100644 index f10ed9c..0000000 --- a/Modules/TrayMenu.qml +++ /dev/null @@ -1,392 +0,0 @@ -pragma ComponentBehavior: Bound - -import Quickshell -import QtQuick -import QtQuick.Layouts -import Qt5Compat.GraphicalEffects -import Quickshell.Hyprland -import QtQml -import qs.Effects -import qs.Config - -PanelWindow { - id: root - - property color backgroundColor: DynamicColors.tPalette.m3surface - required property PanelWindow bar - property int biggestWidth: 0 - property color disabledHighlightColor: DynamicColors.layer(DynamicColors.palette.m3primaryContainer, 0) - property color disabledTextColor: DynamicColors.layer(DynamicColors.palette.m3onSurface, 0) - property int entryHeight: 30 - property alias focusGrab: grab.active - property color highlightColor: DynamicColors.tPalette.m3primaryContainer - property int menuItemCount: menuOpener.children.values.length - property var menuStack: [] - property real scaleValue: 0 - property color textColor: DynamicColors.palette.m3onSurface - required property point trayItemRect - required property QsMenuHandle trayMenu - - signal finishedLoading - signal menuActionTriggered - - function goBack() { - if (root.menuStack.length > 0) { - menuChangeAnimation.start(); - root.biggestWidth = 0; - root.trayMenu = root.menuStack.pop(); - listLayout.positionViewAtBeginning(); - backEntry.visible = false; - } - } - - function updateMask() { - root.mask.changed(); - } - - color: "transparent" - - // onTrayMenuChanged: { - // listLayout.forceLayout(); - // } - - visible: false - - mask: Region { - id: mask - - item: menuRect - } - - onMenuActionTriggered: { - if (root.menuStack.length > 0) { - backEntry.visible = true; - } - } - onVisibleChanged: { - if (!visible) - root.menuStack.pop(); - backEntry.visible = false; - - openAnim.start(); - } - - QsMenuOpener { - id: menuOpener - - menu: root.trayMenu - } - - anchors { - bottom: true - left: true - right: true - top: true - } - - HyprlandFocusGrab { - id: grab - - active: false - windows: [root] - - onCleared: { - closeAnim.start(); - } - } - - SequentialAnimation { - id: menuChangeAnimation - - ParallelAnimation { - NumberAnimation { - duration: MaterialEasing.standardTime / 2 - easing.bezierCurve: MaterialEasing.expressiveEffects - from: 0 - property: "x" - target: translateAnim - to: -listLayout.width / 2 - } - - NumberAnimation { - duration: MaterialEasing.standardTime / 2 - easing.bezierCurve: MaterialEasing.standard - from: 1 - property: "opacity" - target: columnLayout - to: 0 - } - } - - PropertyAction { - property: "menu" - target: columnLayout - } - - ParallelAnimation { - NumberAnimation { - duration: MaterialEasing.standardTime / 2 - easing.bezierCurve: MaterialEasing.standard - from: 0 - property: "opacity" - target: columnLayout - to: 1 - } - - NumberAnimation { - duration: MaterialEasing.standardTime / 2 - easing.bezierCurve: MaterialEasing.expressiveEffects - from: listLayout.width / 2 - property: "x" - target: translateAnim - to: 0 - } - } - } - - ParallelAnimation { - id: closeAnim - - onFinished: { - root.visible = false; - } - - Anim { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - property: "implicitHeight" - target: menuRect - to: 0 - } - - Anim { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - from: 1 - property: "opacity" - targets: [menuRect, shadowRect] - to: 0 - } - } - - ParallelAnimation { - id: openAnim - - Anim { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - from: 0 - property: "implicitHeight" - target: menuRect - to: listLayout.contentHeight + (root.menuStack.length > 0 ? root.entryHeight + 10 : 10) - } - - Anim { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - from: 0 - property: "opacity" - targets: [menuRect, shadowRect] - to: 1 - } - } - - ShadowRect { - id: shadowRect - - anchors.fill: menuRect - radius: menuRect.radius - } - - Rectangle { - id: menuRect - - clip: true - color: root.backgroundColor - implicitHeight: listLayout.contentHeight + (root.menuStack.length > 0 ? root.entryHeight + 10 : 10) - implicitWidth: listLayout.contentWidth + 10 - radius: 8 - x: Math.round(root.trayItemRect.x - (menuRect.implicitWidth / 2) + 11) - y: Math.round(root.trayItemRect.y - 5) - - Behavior on implicitHeight { - NumberAnimation { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - Behavior on implicitWidth { - NumberAnimation { - duration: MaterialEasing.expressiveEffectsTime - easing.bezierCurve: MaterialEasing.expressiveEffects - } - } - - ColumnLayout { - id: columnLayout - - anchors.fill: parent - anchors.margins: 5 - spacing: 0 - - transform: [ - Translate { - id: translateAnim - - x: 0 - y: 0 - } - ] - - ListView { - id: listLayout - - Layout.fillWidth: true - Layout.preferredHeight: contentHeight - contentHeight: contentItem.childrenRect.height - contentWidth: root.biggestWidth - model: menuOpener.children - spacing: 0 - - delegate: Rectangle { - id: menuItem - - property var child: QsMenuOpener { - menu: menuItem.modelData - } - property bool containsMouseAndEnabled: mouseArea.containsMouse && menuItem.modelData.enabled - property bool containsMouseAndNotEnabled: mouseArea.containsMouse && !menuItem.modelData.enabled - required property int index - required property QsMenuEntry modelData - - anchors.left: parent.left - anchors.right: parent.right - color: menuItem.modelData.isSeparator ? "#20FFFFFF" : containsMouseAndEnabled ? root.highlightColor : containsMouseAndNotEnabled ? root.disabledHighlightColor : "transparent" - height: menuItem.modelData.isSeparator ? 1 : root.entryHeight - radius: 4 - visible: true - width: widthMetrics.width + (menuItem.modelData.icon ?? "" ? 30 : 0) + (menuItem.modelData.hasChildren ? 30 : 0) + 20 - - Behavior on color { - CAnim { - duration: 150 - } - } - - Component.onCompleted: { - var biggestWidth = root.biggestWidth; - var currentWidth = widthMetrics.width + (menuItem.modelData.icon ?? "" ? 30 : 0) + (menuItem.modelData.hasChildren ? 30 : 0) + 20; - if (currentWidth > biggestWidth) { - root.biggestWidth = currentWidth; - } - } - - TextMetrics { - id: widthMetrics - - text: menuItem.modelData.text - } - - MouseArea { - id: mouseArea - - acceptedButtons: Qt.LeftButton - anchors.fill: parent - hoverEnabled: true - preventStealing: true - propagateComposedEvents: true - - onClicked: { - if (!menuItem.modelData.hasChildren) { - if (menuItem.modelData.enabled) { - menuItem.modelData.triggered(); - closeAnim.start(); - } - } else { - root.menuStack.push(root.trayMenu); - menuChangeAnimation.start(); - root.biggestWidth = 0; - root.trayMenu = menuItem.modelData; - listLayout.positionViewAtBeginning(); - root.menuActionTriggered(); - } - } - } - - RowLayout { - anchors.fill: parent - - Text { - id: menuText - - Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft - Layout.leftMargin: 10 - color: menuItem.modelData.enabled ? root.textColor : root.disabledTextColor - text: menuItem.modelData.text - } - - Image { - id: iconImage - - Layout.alignment: Qt.AlignVCenter | Qt.AlignRight - Layout.maximumHeight: 20 - Layout.maximumWidth: 20 - Layout.rightMargin: 10 - fillMode: Image.PreserveAspectFit - layer.enabled: true - source: menuItem.modelData.icon - sourceSize.height: height - sourceSize.width: width - - layer.effect: ColorOverlay { - color: menuItem.modelData.enabled ? "white" : "gray" - } - } - - Text { - id: textArrow - - Layout.alignment: Qt.AlignVCenter | Qt.AlignRight - Layout.bottomMargin: 5 - Layout.maximumHeight: 20 - Layout.maximumWidth: 20 - Layout.rightMargin: 10 - color: menuItem.modelData.enabled ? "white" : "gray" - text: "" - visible: menuItem.modelData.hasChildren ?? false - } - } - } - } - - Rectangle { - id: backEntry - - Layout.fillWidth: true - Layout.preferredHeight: root.entryHeight - color: mouseAreaBack.containsMouse ? "#15FFFFFF" : "transparent" - radius: 4 - visible: false - - MouseArea { - id: mouseAreaBack - - anchors.fill: parent - hoverEnabled: true - - onClicked: { - root.goBack(); - } - } - - Text { - anchors.fill: parent - anchors.leftMargin: 10 - color: "white" - text: "Back " - verticalAlignment: Text.AlignVCenter - } - } - } - } -} diff --git a/Modules/TrayMenuPopout.qml b/Modules/TrayMenuPopout.qml index 7115d9b..709c8c0 100644 --- a/Modules/TrayMenuPopout.qml +++ b/Modules/TrayMenuPopout.qml @@ -194,6 +194,8 @@ StackView { } Loader { + id: loader + active: menu.isSubMenu asynchronous: true diff --git a/Modules/TrayWidget.qml b/Modules/TrayWidget.qml index 07e2e02..9f1cb5b 100644 --- a/Modules/TrayWidget.qml +++ b/Modules/TrayWidget.qml @@ -7,52 +7,41 @@ import Quickshell.Services.SystemTray import qs.Components import qs.Config -Item { +CustomClippingRect { id: root - required property PanelWindow bar readonly property alias items: repeater required property RowLayout loader required property Wrapper popouts - anchors.bottom: parent.bottom - anchors.top: parent.top - implicitHeight: 34 + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2 implicitWidth: row.width + Appearance.padding.small * 2 + radius: height / 2 - CustomClippingRect { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - color: DynamicColors.tPalette.m3surfaceContainer - implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2) - radius: height / 2 + Row { + id: row - Row { - id: row + anchors.centerIn: parent + spacing: 0 - anchors.centerIn: parent - spacing: 0 + Repeater { + id: repeater - Repeater { - id: repeater + model: SystemTray.items - model: SystemTray.items + TrayItem { + id: trayItem - TrayItem { - id: trayItem + required property int index + required property SystemTrayItem modelData - required property int index - required property SystemTrayItem modelData - - bar: root.bar - implicitHeight: 34 - implicitWidth: 34 - ind: index - item: modelData - loader: root.loader - popouts: root.popouts - } + implicitHeight: 34 + implicitWidth: 34 + ind: index + item: modelData + loader: root.loader + popouts: root.popouts } } } diff --git a/Modules/UPower/UPowerPopout.qml b/Modules/UPower/UPowerPopout.qml index 8ab4b16..c60c2d1 100644 --- a/Modules/UPower/UPowerPopout.qml +++ b/Modules/UPower/UPowerPopout.qml @@ -167,7 +167,7 @@ Item { anchors.centerIn: parent color: profiles.current === text ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface fill: profiles.current === text ? 1 : 0 - font.pointSize: 36 + font.pointSize: Appearance.font.size.large * 2 text: parent.icon Behavior on fill { diff --git a/Modules/UPower/UPowerWidget.qml b/Modules/UPower/UPowerWidget.qml index beec824..d93a956 100644 --- a/Modules/UPower/UPowerWidget.qml +++ b/Modules/UPower/UPowerWidget.qml @@ -5,20 +5,13 @@ import qs.Components import qs.Config import qs.Helpers as Helpers -Item { +CustomRect { id: root - anchors.bottom: parent.bottom - anchors.top: parent.top - implicitWidth: layout.childrenRect.width + 10 * 2 - - CustomRect { - anchors.bottomMargin: 4 - anchors.fill: parent - anchors.topMargin: 4 - color: DynamicColors.tPalette.m3surfaceContainer - radius: 1000 - } + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2 + implicitWidth: layout.implicitWidth + Appearance.padding.normal * 2 + radius: Appearance.rounding.full RowLayout { id: layout diff --git a/Modules/Updates.qml b/Modules/Updates.qml deleted file mode 100644 index b84aeee..0000000 --- a/Modules/Updates.qml +++ /dev/null @@ -1,37 +0,0 @@ -pragma Singleton -pragma ComponentBehavior: Bound - -import QtQuick -import Quickshell -import Quickshell.Io -import qs.Modules - -Singleton { - property int availableUpdates: 0 - - Timer { - interval: 1 - repeat: true - running: true - - onTriggered: { - updatesProc.running = true; - interval = 5000; - } - } - - Process { - id: updatesProc - - command: ["checkupdates"] - running: false - - stdout: StdioCollector { - onStreamFinished: { - const output = this.text; - const lines = output.trim().split("\n").filter(line => line.length > 0); - availableUpdates = lines.length; - } - } - } -} diff --git a/Modules/Updates/UpdatesPopout.qml b/Modules/Updates/UpdatesPopout.qml new file mode 100644 index 0000000..2b592d3 --- /dev/null +++ b/Modules/Updates/UpdatesPopout.qml @@ -0,0 +1,153 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import qs.Config +import qs.Components +import qs.Modules +import qs.Helpers + +CustomClippingRect { + id: root + + readonly property int itemHeight: 50 + Appearance.padding.smaller * 2 + required property var wrapper + + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: updatesList.visible ? updatesList.implicitHeight + Appearance.padding.small * 2 : noUpdates.height + implicitWidth: updatesList.visible ? updatesList.contentWidth + Appearance.padding.small * 2 : noUpdates.width + radius: Appearance.rounding.small + + Item { + id: noUpdates + + anchors.centerIn: parent + height: 200 + visible: script.values.length === 0 + width: 300 + + MaterialIcon { + id: noUpdatesIcon + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + color: DynamicColors.tPalette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.extraLarge * 3 + horizontalAlignment: Text.AlignHCenter + text: "check" + } + + CustomText { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: noUpdatesIcon.bottom + color: DynamicColors.tPalette.m3onSurfaceVariant + horizontalAlignment: Text.AlignHCenter + text: qsTr("No updates available") + verticalAlignment: Text.AlignVCenter + } + } + + CustomListView { + id: updatesList + + anchors.centerIn: parent + contentHeight: childrenRect.height + contentWidth: 600 + implicitHeight: Math.min(contentHeight, (root.itemHeight + spacing) * 5 - spacing) + implicitWidth: contentWidth + spacing: Appearance.spacing.normal + visible: script.values.length > 0 + + delegate: CustomRect { + id: update + + required property var modelData + readonly property list sections: modelData.update.split(" ") + + anchors.left: parent.left + anchors.right: parent.right + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: root.itemHeight + radius: Appearance.rounding.small - Appearance.padding.small + + RowLayout { + anchors.fill: parent + anchors.leftMargin: Appearance.padding.smaller + anchors.rightMargin: Appearance.padding.smaller + + MaterialIcon { + font.pointSize: Appearance.font.size.large * 2 + text: "package_2" + } + + ColumnLayout { + Layout.fillWidth: true + + CustomText { + Layout.fillWidth: true + Layout.preferredHeight: 25 + elide: Text.ElideRight + font.pointSize: Appearance.font.size.large + text: update.sections[0] + } + + CustomText { + Layout.fillWidth: true + color: DynamicColors.palette.m3onSurfaceVariant + text: Updates.formatUpdateTime(update.modelData.timestamp) + } + } + + RowLayout { + Layout.fillHeight: true + Layout.preferredWidth: 300 + + CustomText { + id: versionFrom + + Layout.fillHeight: true + Layout.preferredWidth: 125 + color: DynamicColors.palette.m3tertiary + elide: Text.ElideRight + font.pointSize: Appearance.font.size.large + horizontalAlignment: Text.AlignHCenter + text: update.sections[1] + verticalAlignment: Text.AlignVCenter + } + + MaterialIcon { + Layout.fillHeight: true + color: DynamicColors.palette.m3secondary + font.pointSize: Appearance.font.size.extraLarge + horizontalAlignment: Text.AlignHCenter + text: "arrow_right_alt" + verticalAlignment: Text.AlignVCenter + } + + CustomText { + id: versionTo + + Layout.fillHeight: true + Layout.preferredWidth: 120 + color: DynamicColors.palette.m3primary + elide: Text.ElideRight + font.pointSize: Appearance.font.size.large + horizontalAlignment: Text.AlignHCenter + text: update.sections[3] + verticalAlignment: Text.AlignVCenter + } + } + } + } + model: ScriptModel { + id: script + + objectProp: "update" + values: Object.entries(Updates.updates).sort((a, b) => b[1] - a[1]).map(([update, timestamp]) => ({ + update, + timestamp + })) + } + } +} diff --git a/Modules/Updates/UpdatesWidget.qml b/Modules/Updates/UpdatesWidget.qml new file mode 100644 index 0000000..0eaa63a --- /dev/null +++ b/Modules/Updates/UpdatesWidget.qml @@ -0,0 +1,36 @@ +import QtQuick +import QtQuick.Layouts +import qs.Components +import qs.Modules +import qs.Helpers +import qs.Config + +CustomRect { + id: root + + property int countUpdates: Updates.availableUpdates + property color textColor: DynamicColors.palette.m3onSurface + + color: DynamicColors.tPalette.m3surfaceContainer + implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2 + implicitWidth: contentRow.implicitWidth + Appearance.spacing.smaller + radius: height / 2 + + RowLayout { + id: contentRow + + anchors.centerIn: parent + spacing: Appearance.spacing.small + + MaterialIcon { + font.pointSize: Appearance.font.size.larger + text: "package_2" + } + + CustomText { + color: root.textColor + font.pointSize: Appearance.font.size.normal + text: root.countUpdates + } + } +} diff --git a/Modules/UpdatesWidget.qml b/Modules/UpdatesWidget.qml deleted file mode 100644 index 13bdc54..0000000 --- a/Modules/UpdatesWidget.qml +++ /dev/null @@ -1,43 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import qs.Components -import qs.Modules -import qs.Config - -Item { - id: root - - property int countUpdates: Updates.availableUpdates - property color textColor: DynamicColors.palette.m3onSurface - - anchors.bottom: parent.bottom - anchors.top: parent.top - implicitWidth: contentRow.childrenRect.width + Appearance.spacing.smaller - - CustomRect { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - color: DynamicColors.tPalette.m3surfaceContainer - implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2) - radius: height / 2 - } - - RowLayout { - id: contentRow - - anchors.centerIn: parent - spacing: Appearance.spacing.small - - MaterialIcon { - font.pointSize: 14 - text: "package_2" - } - - CustomText { - color: root.textColor - font.pointSize: 12 - text: root.countUpdates - } - } -} diff --git a/Modules/Wallpaper/Wallpaper.qml b/Modules/Wallpaper/Wallpaper.qml index fa77580..44c44d2 100644 --- a/Modules/Wallpaper/Wallpaper.qml +++ b/Modules/Wallpaper/Wallpaper.qml @@ -2,6 +2,7 @@ import Quickshell import QtQuick import Quickshell.Wayland import qs.Config +import qs.Modules.DesktopIcons Loader { active: Config.background.enabled @@ -30,6 +31,16 @@ Loader { WallBackground { } + + Loader { + id: loader + + active: Config.general.desktopIcons + anchors.fill: parent + + sourceComponent: DesktopIcons { + } + } } } } diff --git a/Modules/WindowTitle.qml b/Modules/WindowTitle.qml index 68de9db..88c4cc0 100644 --- a/Modules/WindowTitle.qml +++ b/Modules/WindowTitle.qml @@ -19,7 +19,6 @@ Item { }, 0); return bar.width - otherWidth - bar.spacing * (bar.children.length - 1) - bar.vPadding * 2; } - required property Brightness.Monitor monitor clip: true implicitHeight: current.implicitHeight @@ -48,7 +47,7 @@ Item { elide: Qt.ElideRight elideWidth: root.maxWidth font.family: "Rubik" - font.pointSize: 12 + font.pointSize: Appearance.font.size.normal text: Hypr.activeToplevel?.title ?? qsTr("Desktop") onElideWidthChanged: root.current.text = elidedText diff --git a/Modules/Workspaces.qml b/Modules/Workspaces.qml index e444508..9f30f02 100644 --- a/Modules/Workspaces.qml +++ b/Modules/Workspaces.qml @@ -12,16 +12,16 @@ Item { id: root property real activeWorkspaceMargin: Math.ceil(Appearance.padding.small / 2) - required property PanelWindow bar readonly property int effectiveActiveWorkspaceId: monitor?.activeWorkspace?.id ?? 1 - readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.bar.screen) + readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) + required property ShellScreen screen property int workspaceButtonWidth: bgRect.implicitHeight - root.activeWorkspaceMargin * 2 property int workspaceIndexInGroup: (effectiveActiveWorkspaceId - 1) % root.workspacesShown readonly property list workspaces: Hyprland.workspaces.values.filter(w => w.monitor === root.monitor) readonly property int workspacesShown: workspaces.length - anchors.bottom: parent.bottom - anchors.top: parent.top + height: implicitHeight + implicitHeight: Config.barConfig.height + Math.max(Appearance.padding.smaller, Config.barConfig.border) * 2 implicitWidth: (root.workspaceButtonWidth * root.workspacesShown) + root.activeWorkspaceMargin * 2 Behavior on implicitWidth { @@ -36,7 +36,7 @@ Item { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter color: DynamicColors.tPalette.m3surfaceContainer - implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2) + implicitHeight: root.implicitHeight - ((Appearance.padding.small - 1) * 2) radius: height / 2 CustomRect { @@ -91,7 +91,7 @@ Item { CustomText { anchors.centerIn: parent - color: DynamicColors.palette.m3onSecondaryContainer + color: button.modelData.active ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSecondaryContainer elide: Text.ElideRight horizontalAlignment: Text.AlignHCenter text: button.modelData.name @@ -149,21 +149,11 @@ Item { } } - ShaderEffectSource { - id: activeTextTex - - anchors.fill: bgRect - anchors.margins: root.activeWorkspaceMargin - hideSource: true - live: true - recursive: true - sourceItem: activeTextSource - } - Item { id: indicatorMask anchors.fill: bgRect + layer.enabled: true visible: false CustomRect { @@ -176,21 +166,12 @@ Item { } } - ShaderEffectSource { - id: indicatorMaskEffect - - anchors.fill: activeTextSource - live: true - sourceItem: indicatorMask - visible: false - } - MultiEffect { anchors.fill: activeTextSource maskEnabled: true maskInverted: false - maskSource: indicatorMaskEffect - source: activeTextTex + maskSource: indicatorMask + source: activeTextSource z: 5 } } diff --git a/Paths/Paths.qml b/Paths/Paths.qml index fe33f82..27131f1 100644 --- a/Paths/Paths.qml +++ b/Paths/Paths.qml @@ -10,6 +10,7 @@ Singleton { readonly property string cache: `${Quickshell.env("XDG_CACHE_HOME") || `${home}/.cache`}/zshell` readonly property string config: `${Quickshell.env("XDG_CONFIG_HOME") || `${home}/.config`}/zshell` readonly property string data: `${Quickshell.env("XDG_DATA_HOME") || `${home}/.local/share`}/zshell` + readonly property string desktop: `${Quickshell.env("HOME")}/Desktop` readonly property string home: Quickshell.env("HOME") readonly property string imagecache: `${cache}/imagecache` readonly property string libdir: Quickshell.env("ZSHELL_LIB_DIR") || "/usr/lib/zshell" diff --git a/Plugins/ZShell/CMakeLists.txt b/Plugins/ZShell/CMakeLists.txt index 9b0b751..fda2330 100644 --- a/Plugins/ZShell/CMakeLists.txt +++ b/Plugins/ZShell/CMakeLists.txt @@ -4,6 +4,7 @@ pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED) pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED) pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED) pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET) +pkg_check_modules(GLIB REQUIRED glib-2.0 gobject-2.0 gio-2.0) if(NOT Cava_FOUND) pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED) endif() diff --git a/Plugins/ZShell/Services/CMakeLists.txt b/Plugins/ZShell/Services/CMakeLists.txt index 39ecda2..bc4274f 100644 --- a/Plugins/ZShell/Services/CMakeLists.txt +++ b/Plugins/ZShell/Services/CMakeLists.txt @@ -7,7 +7,11 @@ qml_module(ZShell-services audiocollector.hpp audiocollector.cpp audioprovider.hpp audioprovider.cpp cavaprovider.hpp cavaprovider.cpp + desktopmodel.hpp desktopmodel.cpp + desktopstatemanager.hpp desktopstatemanager.cpp LIBRARIES + Qt6::Core + Qt6::Qml PkgConfig::Pipewire PkgConfig::Aubio PkgConfig::Cava diff --git a/Plugins/ZShell/Services/desktopmodel.cpp b/Plugins/ZShell/Services/desktopmodel.cpp new file mode 100644 index 0000000..a75cd98 --- /dev/null +++ b/Plugins/ZShell/Services/desktopmodel.cpp @@ -0,0 +1,186 @@ +#include "desktopmodel.hpp" +#include "desktopstatemanager.hpp" +#include +#include + +namespace ZShell::services { + +DesktopModel::DesktopModel(QObject *parent) : QAbstractListModel(parent) { +} + +int DesktopModel::rowCount(const QModelIndex &parent) const { + if (parent.isValid()) return 0; + return m_items.count(); +} + +QVariant DesktopModel::data(const QModelIndex &index, int role) const { + if (!index.isValid() || index.row() >= m_items.size()) return QVariant(); + + const DesktopItem &item = m_items[index.row()]; + switch (role) { + case FileNameRole: return item.fileName; + case FilePathRole: return item.filePath; + case IsDirRole: return item.isDir; + case GridXRole: return item.gridX; + case GridYRole: return item.gridY; + default: return QVariant(); + } +} + +QHash DesktopModel::roleNames() const { + QHash roles; + roles[FileNameRole] = "fileName"; + roles[FilePathRole] = "filePath"; + roles[IsDirRole] = "isDir"; + roles[GridXRole] = "gridX"; + roles[GridYRole] = "gridY"; + return roles; +} + +void DesktopModel::loadDirectory(const QString &path) { + beginResetModel(); + m_items.clear(); + + QDir dir(path); + dir.setFilter(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); + QFileInfoList list = dir.entryInfoList(); + + DesktopStateManager sm; + QVariantMap savedLayout = sm.getLayout(); + + for (const QFileInfo &fileInfo : list) { + DesktopItem item; + item.fileName = fileInfo.fileName(); + item.filePath = fileInfo.absoluteFilePath(); + item.isDir = fileInfo.isDir(); + + if (savedLayout.contains(item.fileName)) { + QVariantMap pos = savedLayout[item.fileName].toMap(); + item.gridX = pos["x"].toInt(); + item.gridY = pos["y"].toInt(); + } else { + // TODO: make getEmptySpot in C++ and call it here to get the initial position for new icons + item.gridX = 0; + item.gridY = 0; + } + m_items.append(item); + } + endResetModel(); +} + +void DesktopModel::moveIcon(int index, int newX, int newY) { + if (index < 0 || index >= m_items.size()) return; + + m_items[index].gridX = newX; + m_items[index].gridY = newY; + + QModelIndex modelIndex = createIndex(index, 0); + emit dataChanged(modelIndex, modelIndex, {GridXRole, GridYRole}); + + saveCurrentLayout(); +} + +void DesktopModel::saveCurrentLayout() { + QVariantMap layout; + for (const auto& item : m_items) { + QVariantMap pos; + pos["x"] = item.gridX; + pos["y"] = item.gridY; + layout[item.fileName] = pos; + } + + DesktopStateManager sm; + sm.saveLayout(layout); +} + +void DesktopModel::massMove(const QVariantList& selectedPathsList, const QString& leaderPath, int targetX, int targetY, int maxCol, int maxRow) { + QStringList selectedPaths; + for (const QVariant& v : selectedPathsList) { + selectedPaths << v.toString(); + } + + if (selectedPaths.isEmpty()) return; + + int oldX = 0, oldY = 0; + for (const auto& item : m_items) { + if (item.filePath == leaderPath) { + oldX = item.gridX; + oldY = item.gridY; + break; + } + } + + int deltaX = targetX - oldX; + int deltaY = targetY - oldY; + + if (deltaX == 0 && deltaY == 0) return; + + if (selectedPaths.size() == 1 && targetX >= 0 && targetX <= maxCol && targetY >= 0 && targetY <= maxRow) { + QString movingPath = selectedPaths.first(); + int movingIndex = -1; + int targetIndex = -1; + + for (int i = 0; i < m_items.size(); ++i) { + if (m_items[i].filePath == movingPath) { + movingIndex = i; + } else if (m_items[i].gridX == targetX && m_items[i].gridY == targetY) { + targetIndex = i; + } + } + + if (targetIndex != -1 && movingIndex != -1) { + m_items[targetIndex].gridX = oldX; + m_items[targetIndex].gridY = oldY; + m_items[movingIndex].gridX = targetX; + m_items[movingIndex].gridY = targetY; + + emit dataChanged(index(0, 0), index(m_items.size() - 1, 0), {GridXRole, GridYRole}); + saveCurrentLayout(); + return; + } + } + + QList movingItems; + QSet occupied; + + for (int i = 0; i < m_items.size(); ++i) { + if (selectedPaths.contains(m_items[i].filePath)) { + movingItems.append(&m_items[i]); + } else { + occupied.insert(QString::number(m_items[i].gridX) + "," + QString::number(m_items[i].gridY)); + } + } + + for (auto* item : movingItems) { + int newX = item->gridX + deltaX; + int newY = item->gridY + deltaY; + + bool outOfBounds = newX < 0 || newX > maxCol || newY < 0 || newY > maxRow; + bool collision = occupied.contains(QString::number(newX) + "," + QString::number(newY)); + + if (outOfBounds || collision) { + bool found = false; + for (int x = 0; x <= maxCol && !found; ++x) { + for (int y = 0; y <= maxRow && !found; ++y) { + QString key = QString::number(x) + "," + QString::number(y); + if (!occupied.contains(key)) { + newX = x; + newY = y; + occupied.insert(key); + found = true; + } + } + } + } else { + occupied.insert(QString::number(newX) + "," + QString::number(newY)); + } + + item->gridX = newX; + item->gridY = newY; + } + + emit dataChanged(index(0, 0), index(m_items.size() - 1, 0), {GridXRole, GridYRole}); + saveCurrentLayout(); +} + +} // namespace ZShell::services diff --git a/Plugins/ZShell/Services/desktopmodel.hpp b/Plugins/ZShell/Services/desktopmodel.hpp new file mode 100644 index 0000000..d04dcd2 --- /dev/null +++ b/Plugins/ZShell/Services/desktopmodel.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include +#include + +namespace ZShell::services { + +struct DesktopItem { + QString fileName; + QString filePath; + bool isDir; + int gridX; + int gridY; +}; + +class DesktopModel : public QAbstractListModel { +Q_OBJECT +QML_ELEMENT + +public: +enum DesktopRoles { + FileNameRole = Qt::UserRole + 1, + FilePathRole, + IsDirRole, + GridXRole, + GridYRole +}; + +explicit DesktopModel(QObject *parent = nullptr); + +int rowCount(const QModelIndex &parent = QModelIndex()) const override; +QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; +QHash roleNames() const override; + +Q_INVOKABLE void loadDirectory(const QString &path); +Q_INVOKABLE void moveIcon(int index, int newX, int newY); +Q_INVOKABLE void massMove(const QVariantList &selectedPathsList, const QString &leaderPath, int targetX, int targetY, int maxCol, int maxRow); + +private: +QList m_items; +void saveCurrentLayout(); +}; + +} // namespace ZShell::services diff --git a/Plugins/ZShell/Services/desktopstatemanager.cpp b/Plugins/ZShell/Services/desktopstatemanager.cpp new file mode 100644 index 0000000..55f9c70 --- /dev/null +++ b/Plugins/ZShell/Services/desktopstatemanager.cpp @@ -0,0 +1,54 @@ +#include "desktopstatemanager.hpp" +#include +#include +#include +#include +#include +#include + +namespace ZShell::services { + +DesktopStateManager::DesktopStateManager(QObject *parent) : QObject(parent) { +} + +QString DesktopStateManager::getConfigFilePath() const { + QString configDir = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/sleex"; + QDir dir(configDir); + if (!dir.exists()) { + dir.mkpath("."); + } + return configDir + "/desktop_layout.json"; +} + +void DesktopStateManager::saveLayout(const QVariantMap& layout) { + QJsonObject jsonObj = QJsonObject::fromVariantMap(layout); + QJsonDocument doc(jsonObj); + QFile file(getConfigFilePath()); + + if (file.open(QIODevice::WriteOnly)) { + file.write(doc.toJson(QJsonDocument::Indented)); + file.close(); + } else { + qWarning() << "Sleex: Impossible de sauvegarder le layout du bureau dans" << getConfigFilePath(); + } +} + +QVariantMap DesktopStateManager::getLayout() { + QFile file(getConfigFilePath()); + + if (!file.open(QIODevice::ReadOnly)) { + return QVariantMap(); + } + + QByteArray data = file.readAll(); + file.close(); + + QJsonDocument doc = QJsonDocument::fromJson(data); + if (doc.isObject()) { + return doc.object().toVariantMap(); + } + + return QVariantMap(); +} + +} // namespace ZShell::services diff --git a/Plugins/ZShell/Services/desktopstatemanager.hpp b/Plugins/ZShell/Services/desktopstatemanager.hpp new file mode 100644 index 0000000..004b7bb --- /dev/null +++ b/Plugins/ZShell/Services/desktopstatemanager.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +namespace ZShell::services { + +class DesktopStateManager : public QObject { +Q_OBJECT +QML_ELEMENT +QML_SINGLETON + +public: +explicit DesktopStateManager(QObject *parent = nullptr); + +Q_INVOKABLE void saveLayout(const QVariantMap& layout); +Q_INVOKABLE QVariantMap getLayout(); + +private: +QString getConfigFilePath() const; +}; + +} // namespace ZShell::services diff --git a/README.md b/README.md index f461a2b..bdc4ef1 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ This installs the QML plugin to `/usr/lib/qt6/qml`. ### NixOS +**Note that not all features work well. This is due to limited testing on NixOS.** + In your flake.nix file, add the following in your inputs. ```nix @@ -91,7 +93,7 @@ Below a full example of what it could look like. } ``` -Now you can add z-bar-qt as a nixpkg in environment.systemPackages (or optionally in your homePackages). +Now you can add z-bar-qt as a nixpkg in environment.systemPackages (or optionally in your homePackages). It is named zshell and for cli options; zshell-cli. ```nix { pkgs, inputs, ... }: @@ -108,7 +110,7 @@ You can now run `zshell` to run the bar. `zshell-cli` can be used for additional ## Configuration -Configuration is stored in `~/.config/z-bar/config.json`. Options include: +Configuration is stored in `~/.config/zshell/config.json`. Options include: | Option | Description | | :-------------------------------: | :---------------------------------------------------------: | diff --git a/cli/src/zshell/subcommands/scheme.py b/cli/src/zshell/subcommands/scheme.py index 2b6e3fd..32520f3 100644 --- a/cli/src/zshell/subcommands/scheme.py +++ b/cli/src/zshell/subcommands/scheme.py @@ -2,6 +2,8 @@ import typer import json import shutil import os +import re +import subprocess from jinja2 import Environment, FileSystemLoader, StrictUndefined, Undefined from typing import Any, Optional, Tuple @@ -240,6 +242,53 @@ def generate( return is_dark + def apply_gtk_mode(mode: str) -> None: + mode = mode.lower() + preference = "prefer-dark" if mode == "dark" else "prefer-light" + + try: + subprocess.run( + [ + "gsettings", + "set", + "org.gnome.desktop.interface", + "color-scheme", + preference, + ], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + except FileNotFoundError: + pass + + def apply_qt_mode(mode: str, home: str) -> None: + mode = mode.lower() + qt_conf = Path(home) / ".config/qt6ct/qt6ct.conf" + + if not qt_conf.exists(): + return + + try: + text = qt_conf.read_text(encoding="utf-8") + except OSError: + return + + target = "Dark.colors" if mode == "dark" else "Light.colors" + + new_text, count = re.subn( + r"^(color_scheme_path=.*?)(?:Light|Dark)\.colors\s*$", + rf"\1{target}", + text, + flags=re.MULTILINE, + ) + + if count > 0 and new_text != text: + try: + qt_conf.write_text(new_text, encoding="utf-8") + except OSError: + pass + def build_template_context( *, colors: dict[str, str], @@ -440,6 +489,10 @@ def generate( colors = generate_color_scheme(seed, effective_mode, scheme_class) + if smart and not preset: + apply_gtk_mode(effective_mode) + apply_qt_mode(effective_mode, HOME) + output_dict = { "name": name, "flavor": flavor, diff --git a/nix/app2unit.nix b/nix/app2unit.nix index ce5fee2..a29a66a 100644 --- a/nix/app2unit.nix +++ b/nix/app2unit.nix @@ -1,11 +1,11 @@ { - pkgs, # To ensure the nixpkgs version of app2unit + pkgs, fetchFromGitHub, ... }: pkgs.app2unit.overrideAttrs ( final: prev: rec { - version = "1.0.3"; # Fix old issue related to missing env var + version = "1.0.3"; src = fetchFromGitHub { owner = "Vladimir-csp"; repo = "app2unit"; diff --git a/nix/default.nix b/nix/default.nix index 83edc51..636e6be 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -25,6 +25,8 @@ pkg-config, pythonEnv, zshell-cli, + ddcutil, + brightnessctl, }: let version = "1.0.0"; @@ -39,6 +41,8 @@ let bash hyprland zshell-cli + ddcutil + brightnessctl ]; fontconfig = makeFontsConf { diff --git a/plans/ideas.md b/plans/ideas.md index 941e513..b4df49b 100644 --- a/plans/ideas.md +++ b/plans/ideas.md @@ -1,6 +1,7 @@ # Ideas/Features - [ ] Change volume for `$BROWSER` environment variable? Most general media source apart from separate music/video players. +- [ ] Hyprsunset module. # Stupid idea's from Daivin @@ -21,3 +22,26 @@ - [x] Update module: When there is 1 package it still looks extremely off - [x] Audio module + cava / audio wave ;) ( Don't make it into minecraft blocks but aan actual wave) -- Probably not planned + +# Issues in settingsWindow (16-03-2026) + +- [ ] Drawing tool falls behind when accelerating with the cursor (slow start -> faster movement). // Unfortunately this is a limitation of either Qt or the math in my methods, you are free to look through and see if you can come up with better and more performant calculations +- [ ] Dock has an invisible border it has a visual that it attaches to; perhaps make it visible when the dock shows? // Yes +- [ ] Dock apps are clickable and navigates to app (good). If two instances are available, this feels arbitrarily chosen on one instance (maybe defaults to workspace closest to 1?) (like a selection or hover to see options). // I intend to add popups on hover that show a preview of the opened windows, so you can select which one to focus +- [ ] Dock cannot be closed with escape, user needs to click to leave Dock (Dock stops user from interacting with other apps like typing). // Intentional. It uses HyprlandFocusGrab for managing shown/hidden states. +- [ ] Global shortcut for opening Dock and perhaps keyboard navigation? (sounds hard to pull of) +- [ ] If nc or osd global shortcut are used, bar is 100% transparent apart from modules, seems to ignore the regular hover state opacity. +- [ ] Should volume/pipewire module be hover as well? No other bar module is hover apart from the Dock (which is a hidden module activated by hover)? // Unsure, probably + +- [x] Undo option for Drawing tool? // You can clear on right-click. True undo + would require me to store pen strokes in an array and is too advanced for a + simple drawing tool +- [x] Size 1-45 kinda weird numbers (not a real issue = ragebait). // It's just + the pixel width of the pencil. +- [x] Calendar swipe to other month has no animation -> on purpose? // On + purpose, QT doesn't allow for animations in their calendar grid. I used to have + an animation but it was extremely inefficient performance-wise. + +## Additional questions + +- [x] Can some features be disabled? As in, will they be unloaded from RAM or how is it loaded in memory? Let's say I do not want to use the Dock and Drawing Tool and want to disable them, are they loaded in memory at all? Or all the called upon when the shortcut is hit? // None of the modules that are not shown on start are loaded into memory save for notifications. I will make the options to disable different parts functional at some point, but it wouldn't help memory usage since they are loaded on-demand already diff --git a/scripts/levendist.js b/scripts/levendist.js new file mode 100644 index 0000000..332ed09 --- /dev/null +++ b/scripts/levendist.js @@ -0,0 +1,143 @@ +// Original code from https://github.com/koeqaife/hyprland-material-you +// Original code license: GPLv3 +// Translated to Js from Cython with an LLM and reviewed + +function min3(a, b, c) { + return a < b && a < c ? a : b < c ? b : c; +} + +function max3(a, b, c) { + return a > b && a > c ? a : b > c ? b : c; +} + +function min2(a, b) { + return a < b ? a : b; +} + +function max2(a, b) { + return a > b ? a : b; +} + +function levenshteinDistance(s1, s2) { + let len1 = s1.length; + let len2 = s2.length; + + if (len1 === 0) return len2; + if (len2 === 0) return len1; + + if (len2 > len1) { + [s1, s2] = [s2, s1]; + [len1, len2] = [len2, len1]; + } + + let prev = new Array(len2 + 1); + let curr = new Array(len2 + 1); + + for (let j = 0; j <= len2; j++) { + prev[j] = j; + } + + for (let i = 1; i <= len1; i++) { + curr[0] = i; + for (let j = 1; j <= len2; j++) { + let cost = s1[i - 1] === s2[j - 1] ? 0 : 1; + curr[j] = min3(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost); + } + [prev, curr] = [curr, prev]; + } + + return prev[len2]; +} + +function partialRatio(shortS, longS) { + let lenS = shortS.length; + let lenL = longS.length; + let best = 0.0; + + if (lenS === 0) return 1.0; + + for (let i = 0; i <= lenL - lenS; i++) { + let sub = longS.slice(i, i + lenS); + let dist = levenshteinDistance(shortS, sub); + let score = 1.0 - dist / lenS; + if (score > best) best = score; + } + + return best; +} + +function computeScore(s1, s2) { + if (s1 === s2) return 1.0; + + let dist = levenshteinDistance(s1, s2); + let maxLen = max2(s1.length, s2.length); + if (maxLen === 0) return 1.0; + + let full = 1.0 - dist / maxLen; + let part = + s1.length < s2.length ? partialRatio(s1, s2) : partialRatio(s2, s1); + + let score = 0.85 * full + 0.15 * part; + + if (s1 && s2 && s1[0] !== s2[0]) { + score -= 0.05; + } + + let lenDiff = Math.abs(s1.length - s2.length); + if (lenDiff >= 3) { + score -= (0.05 * lenDiff) / maxLen; + } + + let commonPrefixLen = 0; + let minLen = min2(s1.length, s2.length); + for (let i = 0; i < minLen; i++) { + if (s1[i] === s2[i]) { + commonPrefixLen++; + } else { + break; + } + } + score += 0.02 * commonPrefixLen; + + if (s1.includes(s2) || s2.includes(s1)) { + score += 0.06; + } + + return Math.max(0.0, Math.min(1.0, score)); +} + +function computeTextMatchScore(s1, s2) { + if (s1 === s2) return 1.0; + + let dist = levenshteinDistance(s1, s2); + let maxLen = max2(s1.length, s2.length); + if (maxLen === 0) return 1.0; + + let full = 1.0 - dist / maxLen; + let part = + s1.length < s2.length ? partialRatio(s1, s2) : partialRatio(s2, s1); + + let score = 0.4 * full + 0.6 * part; + + let lenDiff = Math.abs(s1.length - s2.length); + if (lenDiff >= 10) { + score -= (0.02 * lenDiff) / maxLen; + } + + let commonPrefixLen = 0; + let minLen = min2(s1.length, s2.length); + for (let i = 0; i < minLen; i++) { + if (s1[i] === s2[i]) { + commonPrefixLen++; + } else { + break; + } + } + score += 0.01 * commonPrefixLen; + + if (s1.includes(s2) || s2.includes(s1)) { + score += 0.2; + } + + return Math.max(0.0, Math.min(1.0, score)); +} diff --git a/shell.qml b/shell.qml index 3d19be0..16d49f5 100644 --- a/shell.qml +++ b/shell.qml @@ -1,5 +1,6 @@ //@ pragma UseQApplication //@ pragma Env QSG_RENDER_LOOP=threaded +//@ pragma Env QSG_USE_SIMPLE_ANIMATION_DRIVER=0 //@ pragma Env QS_NO_RELOAD_POPUP=1 import Quickshell import qs.Modules @@ -10,7 +11,7 @@ import qs.Helpers import qs.Modules.Polkit ShellRoot { - Bar { + Windows { } Wallpaper {