43 Commits

Author SHA1 Message Date
Zach 6c5ca40b8e Merge pull request #23 from Zacharias-Brohn/settingsWindow
Merge settings window to main
2026-03-18 16:27:50 +01:00
Zach e65ec01b12 Merge pull request #22 from Zacharias-Brohn/nix-fixes-unstable
Nix fixes: minor changes
2026-03-18 16:22:04 +01:00
Zacharias-Brohn 9297fa4da2 settings fix 2026-03-18 13:21:18 +01:00
Zacharias-Brohn d885037739 border 2026-03-18 13:17:11 +01:00
Zacharias-Brohn 65703f3e71 border color 2026-03-18 13:04:29 +01:00
Zacharias-Brohn bc67da35e4 updates persistence 2026-03-18 11:37:14 +01:00
Zacharias-Brohn 159e10cc0f updates popout 2026-03-17 23:59:15 +01:00
Aram Markarov 9e8ee9b9de updated ideas.md 2026-03-17 21:26:36 +01:00
Zacharias-Brohn 152b363da2 updates popout 2026-03-17 18:46:10 +01:00
Aram Markarov 46a1af82f5 README.md update to match new config location. 2026-03-16 16:01:03 +01:00
Zacharias-Brohn ca49b9c6f1 plans 2026-03-16 15:45:37 +01:00
Zacharias-Brohn 35fe6c1e5f added settings options 2026-03-16 15:34:11 +01:00
Aram Markarov b555c96de1 added question to md file 2026-03-16 15:20:51 +01:00
Aram Markarov 386040d38a updated plans with current issues to be fixed on settingsWindow 2026-03-16 15:11:10 +01:00
Zacharias-Brohn 3dc6c0ee3e remove required property from borders 2026-03-15 22:57:23 +01:00
Zacharias-Brohn a4e086192d drawing sliders 2026-03-15 22:41:10 +01:00
Zacharias-Brohn 9c955581fa drawing clear on right click 2026-03-15 18:50:26 +01:00
Zacharias-Brohn f7b7260780 settings 2026-03-14 17:29:24 +01:00
Zacharias-Brohn 8bc7826f26 dock 2026-03-13 20:07:03 +01:00
Zacharias-Brohn b7ca2f5c93 dock 2026-03-13 17:35:46 +01:00
inorishio 31df6a6cdf settings 2026-03-13 16:28:03 +01:00
Zacharias-Brohn 8f427d06d0 dock 2026-03-13 16:27:31 +01:00
Zacharias-Brohn 37e482a361 dock 2026-03-13 14:03:40 +01:00
Zacharias-Brohn b65117e213 dock 2026-03-12 19:24:23 +01:00
Zacharias-Brohn 0cd2b3dbfc dock 2026-03-12 16:30:53 +01:00
Zacharias-Brohn 0b935a3096 dock 2026-03-12 16:27:02 +01:00
Zacharias-Brohn 9e9708ed12 desktop icons 2026-03-12 14:45:20 +01:00
Zacharias-Brohn 851b78f0ff kek test 2026-03-12 10:04:27 +01:00
Zacharias-Brohn 401ccef90c slight changes 2026-03-11 19:49:19 +01:00
Zacharias-Brohn 646605cc9b slight changes 2026-03-11 19:47:14 +01:00
Zacharias-Brohn bab596850b slight changes, gpu usage down by 20% 2026-03-11 11:22:22 +01:00
Zacharias-Brohn 0375491dc8 wtf is happening 2026-03-10 22:42:06 +01:00
Zacharias-Brohn 203b19ce45 shader hotfix 2026-03-10 19:15:26 +01:00
Zacharias-Brohn 098db5e903 refactor done? 2026-03-10 15:39:29 +01:00
Zacharias-Brohn 26bc5cd7c3 start of refactor 2026-03-09 23:18:13 +01:00
Zacharias-Brohn 88d795a73f start of refactor 2026-03-09 22:17:34 +01:00
Zacharias-Brohn fb15c2421b fix screenshot picker showing up in screenshots 2026-03-09 13:05:45 +01:00
Zacharias-Brohn 1ee345f946 drawing 2026-03-09 12:47:09 +01:00
Zacharias-Brohn 720bc2808e back to opengl 2026-03-09 00:36:18 +01:00
Zacharias-Brohn a9fc2cbf69 vulkan, screenshotting broken 2026-03-09 00:35:18 +01:00
Zacharias-Brohn 902a1adbfe rounding 2026-03-08 10:06:04 +01:00
Zacharias-Brohn 1b0eb9fdb2 smart 2026-03-07 18:14:45 +01:00
Zacharias-Brohn 7eb7cecf1e smart 2026-03-07 18:13:07 +01:00
133 changed files with 8290 additions and 2273 deletions
+165
View File
@@ -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;
}
}
}
+208
View File
@@ -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;
}
}
}
+3 -2
View File
@@ -1,12 +1,13 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import qs.Config
Button { Button {
id: control id: control
required property color bgColor property color bgColor: DynamicColors.palette.m3primary
property int radius: 4 property int radius: 4
required property color textColor property color textColor: DynamicColors.palette.m3onPrimary
background: CustomRect { background: CustomRect {
color: control.bgColor color: control.bgColor
+1 -1
View File
@@ -5,7 +5,7 @@ import qs.Config
RadioButton { RadioButton {
id: root id: root
font.pointSize: 12 font.pointSize: Appearance.font.size.normal
implicitHeight: Math.max(implicitIndicatorHeight, implicitContentHeight) implicitHeight: Math.max(implicitIndicatorHeight, implicitContentHeight)
implicitWidth: implicitIndicatorWidth + implicitContentWidth + contentItem.anchors.leftMargin implicitWidth: implicitIndicatorWidth + implicitContentWidth + contentItem.anchors.leftMargin
+6 -3
View File
@@ -28,6 +28,7 @@ RowLayout {
CustomTextField { CustomTextField {
id: textField id: textField
implicitHeight: upButton.implicitHeight
inputMethodHints: Qt.ImhFormattedNumbersOnly inputMethodHints: Qt.ImhFormattedNumbersOnly
leftPadding: Appearance.padding.normal leftPadding: Appearance.padding.normal
padding: Appearance.padding.small padding: Appearance.padding.small
@@ -37,7 +38,7 @@ RowLayout {
background: CustomRect { background: CustomRect {
color: DynamicColors.tPalette.m3surfaceContainerHigh color: DynamicColors.tPalette.m3surfaceContainerHigh
implicitWidth: 100 implicitWidth: 100
radius: Appearance.rounding.small radius: Appearance.rounding.full
} }
validator: DoubleValidator { validator: DoubleValidator {
bottom: root.min bottom: root.min
@@ -82,10 +83,12 @@ RowLayout {
} }
CustomRect { CustomRect {
id: upButton
color: DynamicColors.palette.m3primary color: DynamicColors.palette.m3primary
implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2 implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2
implicitWidth: implicitHeight implicitWidth: implicitHeight
radius: Appearance.rounding.small radius: Appearance.rounding.full
StateLayer { StateLayer {
id: upState id: upState
@@ -119,7 +122,7 @@ RowLayout {
color: DynamicColors.palette.m3primary color: DynamicColors.palette.m3primary
implicitHeight: downIcon.implicitHeight + Appearance.padding.small * 2 implicitHeight: downIcon.implicitHeight + Appearance.padding.small * 2
implicitWidth: implicitHeight implicitWidth: implicitHeight
radius: Appearance.rounding.small radius: Appearance.rounding.full
StateLayer { StateLayer {
id: downState id: downState
+23 -1
View File
@@ -1,6 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import qs.Config import qs.Config
import qs.Helpers
Row { Row {
id: root id: root
@@ -29,8 +30,29 @@ Row {
property int type: CustomSplitButton.Filled property int type: CustomSplitButton.Filled
property real verticalPadding: Appearance.padding.smaller 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) spacing: Math.floor(Appearance.spacing.small / 2)
onExpandedChanged: {
if (!expanded)
SettingsDropdowns.forget(menu);
}
CustomRect { CustomRect {
bottomRightRadius: Appearance.rounding.small / 2 bottomRightRadius: Appearance.rounding.small / 2
color: root.disabled ? root.disabledColor : root.color color: root.disabled ? root.disabledColor : root.color
@@ -109,7 +131,7 @@ Row {
id: expandStateLayer id: expandStateLayer
function onClicked(): void { function onClicked(): void {
root.expanded = !root.expanded; root.toggleDropdown();
} }
color: root.textColor color: root.textColor
+12 -10
View File
@@ -4,7 +4,7 @@ import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import qs.Config import qs.Config
CustomRect { Item {
id: root id: root
property alias active: splitButton.active property alias active: splitButton.active
@@ -18,38 +18,40 @@ CustomRect {
signal selected(item: MenuItem) signal selected(item: MenuItem)
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2
clip: false clip: false
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2) z: root.expanded ? expandedZ : -1
implicitHeight: row.implicitHeight + Appearance.padding.large * 2
opacity: enabled ? 1.0 : 0.5
radius: Appearance.rounding.normal
z: splitButton.menu.implicitHeight > 0 ? expandedZ : 1
RowLayout { RowLayout {
id: row id: row
anchors.fill: parent anchors.left: parent.left
anchors.margins: Appearance.padding.large anchors.margins: Appearance.padding.small
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Appearance.spacing.normal spacing: Appearance.spacing.normal
CustomText { CustomText {
Layout.fillWidth: true Layout.fillWidth: true
color: root.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSurfaceVariant color: root.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.larger
text: root.label text: root.label
z: root.expanded ? root.expandedZ : -1
} }
CustomSplitButton { CustomSplitButton {
id: splitButton id: splitButton
enabled: root.enabled enabled: root.enabled
menu.z: 1
type: CustomSplitButton.Filled type: CustomSplitButton.Filled
z: root.expanded ? root.expandedZ : -1
menu.onItemSelected: item => { menu.onItemSelected: item => {
root.selected(item); root.selected(item);
splitButton.closeDropdown();
} }
stateLayer.onClicked: { stateLayer.onClicked: {
splitButton.expanded = !splitButton.expanded; splitButton.toggleDropdown();
} }
} }
} }
+15
View File
@@ -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
}
}
+9
View File
@@ -0,0 +1,9 @@
import Quickshell
import Quickshell.Wayland
PanelWindow {
required property string name
WlrLayershell.namespace: `ZShell-${name}`
color: "transparent"
}
+16 -128
View File
@@ -1,141 +1,29 @@
import QtQuick import QtQuick
import QtQuick.Templates
import qs.Config import qs.Config
Slider { BaseStyledSlider {
id: root id: root
property color color: DynamicColors.palette.m3secondary trackContent: Component {
required property string icon Item {
property bool initialized property var groove
property real oldValue readonly property real handleHeight: handleItem ? handleItem.height : 0
property var handleItem
readonly property real handleWidth: handleItem ? handleItem.width : 0
orientation: Qt.Vertical // Set by BaseStyledSlider's Loader
property var rootSlider
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
anchors.fill: parent anchors.fill: parent
color: DynamicColors.palette.m3inverseSurface
radius: Appearance.rounding.full
MouseArea { CustomRect {
id: handleInteraction color: rootSlider?.color
height: rootSlider?.isVertical ? handleHeight + (1 - rootSlider?.visualPosition) * (groove?.height - handleHeight) : groove?.height
acceptedButtons: Qt.NoButton radius: groove?.radius
anchors.fill: parent width: rootSlider?.isHorizontal ? handleWidth + rootSlider?.visualPosition * (groove?.width - handleWidth) : groove?.width
cursorShape: Qt.PointingHandCursor x: rootSlider?.isHorizontal ? (rootSlider?.mirrored ? groove?.width - width : 0) : 0
hoverEnabled: true 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;
} }
} }
} }
+47
View File
@@ -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
}
}
}
}
}
}
+1 -1
View File
@@ -5,7 +5,7 @@ CustomText {
property int grade: DynamicColors.light ? 0 : -25 property int grade: DynamicColors.light ? 0 : -25
font.family: "Material Symbols Rounded" font.family: "Material Symbols Rounded"
font.pointSize: 15 font.pointSize: Appearance.font.size.larger
font.variableAxes: ({ font.variableAxes: ({
FILL: fill.toFixed(1), FILL: fill.toFixed(1),
GRAD: grade, GRAD: grade,
+76
View File
@@ -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
}
}
+178
View File
@@ -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
}
}
}
+2
View File
@@ -71,6 +71,7 @@ JsonObject {
property real scale: 1 property real scale: 1
property int small: 5 * scale property int small: 5 * scale
property int smaller: 7 * scale property int smaller: 7 * scale
property int smallest: 2 * scale
} }
component Rounding: JsonObject { component Rounding: JsonObject {
property int full: 1000 * scale property int full: 1000 * scale
@@ -78,6 +79,7 @@ JsonObject {
property int normal: 17 * scale property int normal: 17 * scale
property real scale: 1 property real scale: 1
property int small: 12 * scale property int small: 12 * scale
property int smallest: 8 * scale
} }
component Spacing: JsonObject { component Spacing: JsonObject {
property int large: 20 * scale property int large: 20 * scale
+2
View File
@@ -2,6 +2,7 @@ import Quickshell.Io
JsonObject { JsonObject {
property bool autoHide: false property bool autoHide: false
property int border: 8
property list<var> entries: [ property list<var> entries: [
{ {
id: "workspaces", id: "workspaces",
@@ -60,6 +61,7 @@ JsonObject {
enabled: true enabled: true
}, },
] ]
property int height: 34
property Popouts popouts: Popouts { property Popouts popouts: Popouts {
} }
property int rounding: 8 property int rounding: 8
+12 -5
View File
@@ -33,6 +33,10 @@ Singleton {
recentSaveCooldown.restart(); recentSaveCooldown.restart();
} }
function saveNoToast(): void {
saveTimer.restart();
}
function serializeAppearance(): var { function serializeAppearance(): var {
return { return {
rounding: { rounding: {
@@ -56,8 +60,8 @@ Singleton {
} }
}, },
anim: { anim: {
mediaGifSpeedAdjustment: 300, mediaGifSpeedAdjustment: appearance.anim.mediaGifSpeedAdjustment,
sessionGifSpeed: 0.7, sessionGifSpeed: appearance.anim.sessionGifSpeed,
durations: { durations: {
scale: appearance.anim.durations.scale scale: appearance.anim.durations.scale
} }
@@ -81,6 +85,8 @@ Singleton {
return { return {
autoHide: barConfig.autoHide, autoHide: barConfig.autoHide,
rounding: barConfig.rounding, rounding: barConfig.rounding,
border: barConfig.border,
height: barConfig.height,
popouts: { popouts: {
tray: barConfig.popouts.tray, tray: barConfig.popouts.tray,
audio: barConfig.popouts.audio, audio: barConfig.popouts.audio,
@@ -155,10 +161,10 @@ Singleton {
return { return {
enable: dock.enable, enable: dock.enable,
height: dock.height, height: dock.height,
hoverRegionHeight: dock.hoverRegionHeight,
hoverToReveal: dock.hoverToReveal, hoverToReveal: dock.hoverToReveal,
pinnedApps: dock.pinnedApps, pinnedApps: dock.pinnedApps,
pinnedOnStartup: dock.pinnedOnStartup pinnedOnStartup: dock.pinnedOnStartup,
ignoredAppRegexes: dock.ignoredAppRegexes
}; };
} }
@@ -166,6 +172,7 @@ Singleton {
return { return {
logo: general.logo, logo: general.logo,
wallpaperPath: general.wallpaperPath, wallpaperPath: general.wallpaperPath,
desktopIcons: general.desktopIcons,
color: { color: {
wallust: general.color.wallust, wallust: general.color.wallust,
mode: general.color.mode, mode: general.color.mode,
@@ -182,7 +189,7 @@ Singleton {
explorer: general.apps.explorer explorer: general.apps.explorer
}, },
idle: { idle: {
timouts: general.idle.timeouts timeouts: general.idle.timeouts
} }
}; };
} }
+1 -1
View File
@@ -3,8 +3,8 @@ import Quickshell.Io
JsonObject { JsonObject {
property bool enable: false property bool enable: false
property real height: 60 property real height: 60
property real hoverRegionHeight: 2
property bool hoverToReveal: true property bool hoverToReveal: true
property list<string> ignoredAppRegexes: []
property list<string> pinnedApps: ["org.kde.dolphin", "kitty",] property list<string> pinnedApps: ["org.kde.dolphin", "kitty",]
property bool pinnedOnStartup: false property bool pinnedOnStartup: false
} }
+3
View File
@@ -6,6 +6,7 @@ JsonObject {
} }
property Color color: Color { property Color color: Color {
} }
property bool desktopIcons: false
property Idle idle: Idle { property Idle idle: Idle {
} }
property string logo: "" property string logo: ""
@@ -29,10 +30,12 @@ JsonObject {
component Idle: JsonObject { component Idle: JsonObject {
property list<var> timeouts: [ property list<var> timeouts: [
{ {
name: "Lock",
timeout: 180, timeout: 180,
idleAction: "lock" idleAction: "lock"
}, },
{ {
name: "Screen",
timeout: 300, timeout: 300,
idleAction: "dpms off", idleAction: "dpms off",
activeAction: "dpms on" activeAction: "dpms on"
+19 -7
View File
@@ -11,8 +11,9 @@ import qs.Modules.Dashboard as Dashboard
import qs.Modules.Osd as Osd import qs.Modules.Osd as Osd
import qs.Modules.Launcher as Launcher import qs.Modules.Launcher as Launcher
import qs.Modules.Resources as Resources import qs.Modules.Resources as Resources
import qs.Modules.Drawing as Drawing
import qs.Modules.Settings as Settings import qs.Modules.Settings as Settings
import qs.Modules.Dock as Dock
Shape { Shape {
id: root id: root
@@ -22,13 +23,15 @@ Shape {
required property PersistentProperties visibilities required property PersistentProperties visibilities
anchors.fill: parent anchors.fill: parent
// anchors.margins: 8 anchors.margins: Config.barConfig.border
anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? 0 : bar.implicitHeight anchors.topMargin: bar.implicitHeight
asynchronous: true
preferredRendererType: Shape.CurveRenderer preferredRendererType: Shape.CurveRenderer
Behavior on anchors.topMargin { Drawing.Background {
Anim { startX: 0
} startY: wrapper.y - rounding
wrapper: root.panels.drawing
} }
Resources.Background { Resources.Background {
@@ -45,7 +48,8 @@ Shape {
Modules.Background { Modules.Background {
invertBottomRounding: wrapper.x <= 0 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 startY: wrapper.y
wrapper: root.panels.popouts wrapper: root.panels.popouts
} }
@@ -92,4 +96,12 @@ Shape {
startY: 0 startY: 0
wrapper: root.panels.settings wrapper: root.panels.settings
} }
Dock.Background {
id: dock
startX: (root.width - wrapper.width) / 2 - rounding
startY: root.height
wrapper: root.panels.dock
}
} }
+186
View File
@@ -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()
}
+58
View File
@@ -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();
}
}
+41
View File
@@ -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 {
}
}
}
+61 -129
View File
@@ -10,6 +10,8 @@ CustomMouseArea {
required property Item bar required property Item bar
property bool dashboardShortcutActive property bool dashboardShortcutActive
property point dragStart property point dragStart
required property Drawing drawing
required property DrawingInput input
property bool osdShortcutActive property bool osdShortcutActive
required property Panels panels required property Panels panels
required property BarPopouts.Wrapper popouts required property BarPopouts.Wrapper popouts
@@ -18,15 +20,15 @@ CustomMouseArea {
required property PersistentProperties visibilities required property PersistentProperties visibilities
function inBottomPanel(panel: Item, x: real, y: real): bool { 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 { 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 { 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 { function inTopPanel(panel: Item, x: real, y: real): bool {
@@ -51,20 +53,10 @@ CustomMouseArea {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
propagateComposedEvents: 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;
// }
// }
onContainsMouseChanged: { onContainsMouseChanged: {
if (!containsMouse) { if (!containsMouse) {
// Only hide if not activated by shortcut
if (!osdShortcutActive) { if (!osdShortcutActive) {
visibilities.osd = false; visibilities.osd = false;
root.panels.osd.hovered = false; root.panels.osd.hovered = false;
@@ -74,8 +66,8 @@ CustomMouseArea {
popouts.hasCurrent = false; popouts.hasCurrent = false;
} }
if (Config.barConfig.autoHide && !root.visibilities.sidebar && !root.visibilities.dashboard) if (Config.barConfig.autoHide)
root.visibilities.bar = false; bar.isHovered = false;
} }
} }
onPositionChanged: event => { onPositionChanged: event => {
@@ -87,142 +79,69 @@ CustomMouseArea {
const dragX = x - dragStart.x; const dragX = x - dragStart.x;
const dragY = y - dragStart.y; const dragY = y - dragStart.y;
// Show bar in non-exclusive mode on hover if (root.visibilities.isDrawing && !root.inLeftPanel(root.panels.drawing, x, y)) {
if (!visibilities.bar && Config.barConfig.autoHide && y < bar.implicitHeight + bar.anchors.topMargin) root.input.z = 2;
visibilities.bar = true; root.panels.drawing.expanded = false;
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;
} }
// Show launcher on hover, or show/hide on drag if hover is disabled if (!visibilities.bar && Config.barConfig.autoHide && y < bar.implicitHeight)
// if (Config.launcher.showOnHover) { bar.isHovered = true;
// 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;
// }
// Show popouts on hover if (panels.sidebar.width === 0) {
if (y < bar.implicitHeight) { const showOsd = inRightPanel(panels.osd, x, y);
bar.checkPopout(x);
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 { Connections {
function onDashboardChanged() { function onDashboardChanged() {
if (root.visibilities.dashboard) { 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); const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY);
if (!inDashboardArea) { if (!inDashboardArea) {
root.dashboardShortcutActive = true; root.dashboardShortcutActive = true;
} }
root.visibilities.settings = false;
root.visibilities.sidebar = false; root.visibilities.sidebar = false;
root.popouts.hasCurrent = false; root.popouts.hasCurrent = false;
} else { } else {
// Dashboard hidden, clear shortcut flag
root.dashboardShortcutActive = false; root.dashboardShortcutActive = false;
// root.visibilities.bar = false;
} }
} }
function onIsDrawingChanged() {
if (!root.visibilities.isDrawing)
root.drawing.clear();
}
function onLauncherChanged() { function onLauncherChanged() {
// If launcher is hidden, clear shortcut flags for dashboard and OSD
if (!root.visibilities.launcher) { if (!root.visibilities.launcher) {
root.dashboardShortcutActive = false; root.dashboardShortcutActive = false;
root.osdShortcutActive = false; root.osdShortcutActive = false;
root.utilitiesShortcutActive = 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); const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
if (!inOsdArea) { if (!inOsdArea) {
@@ -230,17 +149,20 @@ CustomMouseArea {
root.panels.osd.hovered = false; root.panels.osd.hovered = false;
} }
} }
if (root.visibilities.launcher) {
root.visibilities.dock = false;
root.visibilities.settings = false;
}
} }
function onOsdChanged() { function onOsdChanged() {
if (root.visibilities.osd) { 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); const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
if (!inOsdArea) { if (!inOsdArea) {
root.osdShortcutActive = true; root.osdShortcutActive = true;
} }
} else { } else {
// OSD hidden, clear shortcut flag
root.osdShortcutActive = false; root.osdShortcutActive = false;
} }
} }
@@ -249,6 +171,18 @@ CustomMouseArea {
if (root.visibilities.resources && root.popouts.currentName.startsWith("audio")) { if (root.visibilities.resources && root.popouts.currentName.startsWith("audio")) {
root.popouts.hasCurrent = false; 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() { function onSidebarChanged() {
@@ -260,13 +194,11 @@ CustomMouseArea {
function onUtilitiesChanged() { function onUtilitiesChanged() {
if (root.visibilities.utilities) { 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); const inUtilitiesArea = root.inBottomPanel(root.panels.utilities, root.mouseX, root.mouseY);
if (!inUtilitiesArea) { if (!inUtilitiesArea) {
root.utilitiesShortcutActive = true; root.utilitiesShortcutActive = true;
} }
} else { } else {
// Utilities hidden, clear shortcut flag
root.utilitiesShortcutActive = false; root.utilitiesShortcutActive = false;
} }
} }
+28 -7
View File
@@ -11,6 +11,8 @@ import qs.Components.Toast as Toasts
import qs.Modules.Launcher as Launcher import qs.Modules.Launcher as Launcher
import qs.Modules.Resources as Resources import qs.Modules.Resources as Resources
import qs.Modules.Settings as Settings import qs.Modules.Settings as Settings
import qs.Modules.Drawing as Drawing
import qs.Modules.Dock as Dock
import qs.Config import qs.Config
Item { Item {
@@ -18,6 +20,9 @@ Item {
required property Item bar required property Item bar
readonly property alias dashboard: dashboard 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 launcher: launcher
readonly property alias notifications: notifications readonly property alias notifications: notifications
readonly property alias osd: osd readonly property alias osd: osd
@@ -31,13 +36,8 @@ Item {
required property PersistentProperties visibilities required property PersistentProperties visibilities
anchors.fill: parent anchors.fill: parent
// anchors.margins: 8 anchors.margins: Config.barConfig.border
anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? 0 : bar.implicitHeight anchors.topMargin: bar.implicitHeight
Behavior on anchors.topMargin {
Anim {
}
}
Resources.Wrapper { Resources.Wrapper {
id: resources id: resources
@@ -47,6 +47,16 @@ Item {
visibilities: root.visibilities 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 { Osd.Wrapper {
id: osd id: osd
@@ -133,6 +143,17 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top anchors.top: parent.top
panels: root 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 visibilities: root.visibilities
} }
} }
+66 -69
View File
@@ -21,30 +21,25 @@ Variants {
required property var modelData required property var modelData
PanelWindow { Exclusions {
id: bar 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 var root: Quickshell.shellDir
property bool trayMenuVisible: false
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.resources ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None WlrLayershell.keyboardFocus: visibilities.dock || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.resources ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
WlrLayershell.namespace: "ZShell-Bar"
color: "transparent" color: "transparent"
contentItem.focus: true contentItem.focus: true
mask: visibilities.isDrawing ? null : region
name: "Bar"
screen: scope.modelData 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: { contentItem.Keys.onEscapePressed: {
if (Config.barConfig.autoHide) if (Config.barConfig.autoHide)
visibilities.bar = false; visibilities.bar = false;
@@ -54,22 +49,23 @@ Variants {
visibilities.settings = false; visibilities.settings = false;
visibilities.resources = false; visibilities.resources = false;
} }
onHasFullscreenChanged: {
visibilities.launcher = false;
visibilities.dashboard = false;
visibilities.osd = false;
visibilities.settings = false;
visibilities.resources = false;
}
PanelWindow { Region {
id: exclusionZone id: region
WlrLayershell.exclusionMode: Config.barConfig.autoHide ? ExclusionMode.Ignore : ExclusionMode.Auto height: win.height - bar.implicitHeight - Config.barConfig.border
WlrLayershell.layer: WlrLayer.Bottom intersection: Intersection.Xor
WlrLayershell.namespace: "ZShell-Bar-Exclusion" regions: popoutRegions.instances
color: "transparent" width: win.width - Config.barConfig.border * 2
implicitHeight: backgroundRect.height x: Config.barConfig.border
screen: bar.screen y: bar.implicitHeight
anchors {
left: true
right: true
top: true
}
} }
anchors { anchors {
@@ -90,16 +86,16 @@ Variants {
height: modelData.height height: modelData.height
intersection: Intersection.Subtract intersection: Intersection.Subtract
width: modelData.width width: modelData.width
x: modelData.x x: modelData.x + Config.barConfig.border
y: modelData.y + backgroundRect.implicitHeight y: modelData.y + bar.implicitHeight
} }
} }
HyprlandFocusGrab { HyprlandFocusGrab {
id: focusGrab id: focusGrab
active: visibilities.resources || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu")) active: visibilities.dock || visibilities.resources || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu"))
windows: [bar] windows: [win]
onCleared: { onCleared: {
visibilities.launcher = false; visibilities.launcher = false;
@@ -108,6 +104,7 @@ Variants {
visibilities.osd = false; visibilities.osd = false;
visibilities.settings = false; visibilities.settings = false;
visibilities.resources = false; visibilities.resources = false;
visibilities.dock = false;
panels.popouts.hasCurrent = false; panels.popouts.hasCurrent = false;
} }
} }
@@ -117,6 +114,8 @@ Variants {
property bool bar property bool bar
property bool dashboard property bool dashboard
property bool dock
property bool isDrawing
property bool launcher property bool launcher
property bool notif: NotifServer.popups.length > 0 property bool notif: NotifServer.popups.length > 0
property bool osd property bool osd
@@ -130,7 +129,7 @@ Variants {
Binding { Binding {
property: "bar" property: "bar"
target: visibilities 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 when: Config.barConfig.autoHide
} }
@@ -146,68 +145,66 @@ Variants {
} }
Border { Border {
bar: backgroundRect bar: bar
visibilities: visibilities visibilities: visibilities
} }
Backgrounds { Backgrounds {
bar: backgroundRect bar: bar
panels: panels panels: panels
visibilities: visibilities 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 { Interactions {
id: mouseArea id: mouseArea
anchors.fill: parent anchors.fill: parent
bar: barLoader bar: bar
drawing: drawing
input: input
panels: panels panels: panels
popouts: panels.popouts popouts: panels.popouts
screen: scope.modelData screen: scope.modelData
visibilities: visibilities visibilities: visibilities
z: 1
Panels { Panels {
id: panels id: panels
bar: backgroundRect bar: bar
drawingItem: drawing
screen: scope.modelData screen: scope.modelData
visibilities: visibilities visibilities: visibilities
} }
CustomRect { BarLoader {
id: backgroundRect id: bar
property Wrapper popouts: panels.popouts
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top popouts: panels.popouts
anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? -30 : 0 screen: scope.modelData
color: "transparent" visibilities: visibilities
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
}
} }
} }
} }
+150
View File
@@ -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<DesktopEntry> 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<var>, 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>): 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");
}
}
+201
View File
@@ -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";
}
}
+28
View File
@@ -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;
}
}
+1 -1
View File
@@ -33,7 +33,7 @@ Singleton {
} }
function applyLightMode() { 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"]); 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 { } else {
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--preset", `${DynamicColors.scheme}:${DynamicColors.flavour}`, "--output", `${Paths.state}/scheme.json`, "--mode", "light"]); Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--preset", `${DynamicColors.scheme}:${DynamicColors.flavour}`, "--output", `${Paths.state}/scheme.json`, "--mode", "light"]);
+17 -4
View File
@@ -141,7 +141,7 @@ MouseArea {
sy = ssy; sy = ssy;
ex = x; ex = x;
ey = y; ey = y;
} else { } else if (!saveTimer.running) {
checkClientRects(x, y); checkClientRects(x, y);
} }
} }
@@ -154,7 +154,7 @@ MouseArea {
return; return;
if (root.loader.freeze) { if (root.loader.freeze) {
save(); saveTimer.start();
} else { } else {
overlay.visible = border.visible = false; overlay.visible = border.visible = false;
screencopy.visible = false; screencopy.visible = false;
@@ -162,6 +162,16 @@ MouseArea {
} }
} }
Timer {
id: saveTimer
interval: 25
repeat: false
running: false
onTriggered: root.save()
}
SequentialAnimation { SequentialAnimation {
id: closeAnim id: closeAnim
@@ -217,9 +227,10 @@ MouseArea {
paintCursor: false paintCursor: false
onHasContentChanged: { onHasContentChanged: {
if (hasContent && !root.loader.freeze) { if (hasContent) {
overlay.visible = border.visible = true; overlay.visible = border.visible = true;
root.save(); if (!root.loader.freeze)
root.save();
} }
} }
} }
@@ -233,6 +244,7 @@ MouseArea {
layer.enabled: true layer.enabled: true
opacity: 0.3 opacity: 0.3
radius: root.realRounding radius: root.realRounding
visible: false
layer.effect: MultiEffect { layer.effect: MultiEffect {
maskEnabled: true maskEnabled: true
@@ -270,6 +282,7 @@ MouseArea {
implicitHeight: selectionRect.implicitHeight + root.realBorderWidth * 2 implicitHeight: selectionRect.implicitHeight + root.realBorderWidth * 2
implicitWidth: selectionRect.implicitWidth + root.realBorderWidth * 2 implicitWidth: selectionRect.implicitWidth + root.realBorderWidth * 2
radius: root.realRounding > 0 ? root.realRounding + root.realBorderWidth : 0 radius: root.realRounding > 0 ? root.realRounding + root.realBorderWidth : 0
visible: false
x: selectionRect.x - root.realBorderWidth x: selectionRect.x - root.realBorderWidth
y: selectionRect.y - root.realBorderWidth y: selectionRect.y - root.realBorderWidth
+60
View File
@@ -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);
}
}
+73 -12
View File
@@ -17,7 +17,6 @@ Singleton {
property var disks: [] property var disks: []
property real gpuMemTotal: 0 property real gpuMemTotal: 0
property real gpuMemUsed property real gpuMemUsed
property string gpuName: ""
property real gpuPerc property real gpuPerc
property real gpuTemp property real gpuTemp
readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType
@@ -80,8 +79,29 @@ Singleton {
onTriggered: { onTriggered: {
stat.reload(); stat.reload();
meminfo.reload(); meminfo.reload();
if (root.gpuType === "GENERIC")
gpuUsage.running = true;
}
}
Timer {
interval: 60000 * 120
repeat: true
running: true
triggeredOnStart: true
onTriggered: {
storage.running = true; storage.running = true;
gpuUsage.running = true; }
}
Timer {
interval: Config.dashboard.resourceUpdateInterval * 5
repeat: true
running: root.refCount > 0
triggeredOnStart: true
onTriggered: {
sensors.running = true; sensors.running = true;
} }
} }
@@ -112,10 +132,13 @@ Singleton {
const totalDiff = total - root.lastCpuTotal; const totalDiff = total - root.lastCpuTotal;
const idleDiff = idle - root.lastCpuIdle; 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.lastCpuTotal = total;
root.lastCpuIdle = idle; root.lastCpuIdle = idle;
if (Math.abs(newCpuPerc - root.cpuPerc) >= 0.01)
root.cpuPerc = newCpuPerc;
} }
} }
} }
@@ -127,8 +150,14 @@ Singleton {
onLoaded: { onLoaded: {
const data = text(); const data = text();
root.memTotal = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1; const total = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1;
root.memUsed = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0; 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 { Process {
id: gpuUsage 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 { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
console.log("this is running");
if (root.gpuType === "GENERIC") { if (root.gpuType === "GENERIC") {
const percs = text.trim().split("\n"); const percs = text.trim().split("\n");
const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0); const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0);
root.gpuPerc = sum / percs.length / 100; 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 { } else {
root.gpuPerc = 0; root.gpuPerc = 0;
root.gpuTemp = 0; root.gpuTemp = 0;
@@ -314,7 +375,7 @@ Singleton {
// If AMD Tdie pattern failed, try fallback on Tctl // If AMD Tdie pattern failed, try fallback on Tctl
cpuTemp = text.match(/Tctl:\s+((\+|-)[0-9.]+)(°| )C/); 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]); root.cpuTemp = parseFloat(cpuTemp[1]);
if (root.gpuType !== "GENERIC") if (root.gpuType !== "GENERIC")
+103
View File
@@ -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);
}
}
+123
View File
@@ -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;
}
}
}
+3 -3
View File
@@ -19,20 +19,20 @@ Searcher {
function preview(path: string): void { function preview(path: string): void {
previewPath = path; previewPath = path;
if (Config.general.color.schemeGeneration) 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; showPreview = true;
} }
function setWallpaper(path: string): void { function setWallpaper(path: string): void {
actualCurrent = path; actualCurrent = path;
WallpaperPath.currentWallpaperPath = 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 { function stopPreview(): void {
showPreview = false; showPreview = false;
if (Config.general.color.schemeGeneration) 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 ? ({}) : ({ extraOpts: useFuzzy ? ({}) : ({
+11 -17
View File
@@ -7,15 +7,16 @@ import qs.Modules
import qs.Config import qs.Config
import qs.Components import qs.Components
Item { CustomRect {
id: root id: root
property color barColor: DynamicColors.palette.m3primary property color barColor: DynamicColors.palette.m3primary
property color textColor: DynamicColors.palette.m3onSurface property color textColor: DynamicColors.palette.m3onSurface
anchors.bottom: parent.bottom color: DynamicColors.tPalette.m3surfaceContainer
anchors.top: parent.top implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2
implicitWidth: 150 implicitWidth: 150
radius: Appearance.rounding.full
Behavior on implicitWidth { Behavior on implicitWidth {
NumberAnimation { NumberAnimation {
@@ -24,34 +25,27 @@ Item {
} }
} }
CustomRect { Component.onCompleted: console.log(root.height, root.implicitHeight)
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 { RowLayout {
id: layout id: layout
anchors.fill: parent anchors.left: parent.left
anchors.leftMargin: Appearance.padding.small anchors.leftMargin: Appearance.padding.small
anchors.rightMargin: Appearance.padding.small * 2
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: root.implicitWidth - Appearance.padding.small * 3
MaterialIcon { MaterialIcon {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
animate: true animate: true
color: Audio.muted ? DynamicColors.palette.m3error : root.textColor color: Audio.muted ? DynamicColors.palette.m3error : root.textColor
font.pointSize: 14 font.pointSize: Appearance.font.size.larger
text: Audio.muted ? "volume_off" : "volume_up" text: Audio.muted ? "volume_off" : "volume_up"
} }
CustomRect { CustomRect {
Layout.fillWidth: true Layout.fillWidth: true
color: "#50ffffff" color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
implicitHeight: 4 implicitHeight: 4
radius: 20 radius: 20
@@ -74,13 +68,13 @@ Item {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
animate: true animate: true
color: (Audio.sourceMuted ?? false) ? DynamicColors.palette.m3error : root.textColor color: (Audio.sourceMuted ?? false) ? DynamicColors.palette.m3error : root.textColor
font.pointSize: 14 font.pointSize: Appearance.font.size.larger
text: Audio.sourceMuted ? "mic_off" : "mic" text: Audio.sourceMuted ? "mic_off" : "mic"
} }
CustomRect { CustomRect {
Layout.fillWidth: true Layout.fillWidth: true
color: "#50ffffff" color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
implicitHeight: 4 implicitHeight: 4
radius: 20 radius: 20
+1 -1
View File
@@ -9,7 +9,7 @@ ShapePath {
readonly property bool flatten: wrapper.height < rounding * 2 readonly property bool flatten: wrapper.height < rounding * 2
property real ibr: invertBottomRounding ? -1 : 1 property real ibr: invertBottomRounding ? -1 : 1
required property bool invertBottomRounding required property bool invertBottomRounding
readonly property real rounding: 8 property real rounding: Appearance.rounding.smallest
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property Wrapper wrapper required property Wrapper wrapper
+218
View File
@@ -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
}
}
+45 -197
View File
@@ -11,224 +11,72 @@ import qs.Helpers
import qs.Modules.UPower import qs.Modules.UPower
import qs.Modules.Network import qs.Modules.Network
RowLayout { Item {
id: root 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 Wrapper popouts
required property ShellScreen screen required property ShellScreen screen
readonly property bool shouldBeVisible: (!Config.barConfig.autoHide || visibilities.bar || isHovered)
readonly property int vPadding: 6 readonly property int vPadding: 6
required property PersistentProperties visibilities required property PersistentProperties visibilities
function checkPopout(x: real): void { function checkPopout(x: real): void {
const ch = childAt(x, 2) as WrappedLoader; content.item?.checkPopout(x);
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;
}
} }
implicitHeight: 34 implicitHeight: Config.barConfig.border
visible: height > Config.barConfig.border
CustomShortcut { states: State {
name: "toggle-overview" name: "visible"
when: root.shouldBeVisible
onPressed: { PropertyChanges {
Hyprland.refreshWorkspaces(); root.implicitHeight: root.contentHeight
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;
}
} }
} }
transitions: [
Transition {
from: ""
to: "visible"
Repeater { Anim {
id: repeater duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
// model: Config.barConfig.entries.filted(n => n.index > 50).sort(n => n.index) property: "implicitHeight"
model: Config.barConfig.entries target: root
DelegateChooser {
role: "id"
DelegateChoice {
roleValue: "spacer"
delegate: WrappedLoader {
Layout.fillWidth: true
}
} }
},
Transition {
from: "visible"
to: ""
DelegateChoice { Anim {
roleValue: "workspaces" duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
delegate: WrappedLoader { property: "implicitHeight"
sourceComponent: Workspaces { target: root
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 {
}
}
} }
} }
} ]
component WrappedLoader: Loader { Loader {
required property bool enabled id: content
required property string id
required property int index
function findFirstEnabled(): Item { active: root.shouldBeVisible || root.visible
const count = repeater.count; anchors.bottom: parent.bottom
for (let i = 0; i < count; i++) { anchors.left: parent.left
const item = repeater.itemAt(i); anchors.right: parent.right
if (item?.enabled)
return item; sourceComponent: Bar {
} height: root.contentHeight
return null; 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
} }
} }
+7 -10
View File
@@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import QtQuick import QtQuick
import QtQuick.Effects import QtQuick.Effects
import qs.Modules
import qs.Config import qs.Config
import qs.Components import qs.Components
@@ -17,7 +16,8 @@ Item {
CustomRect { CustomRect {
anchors.fill: parent 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.enabled: true
layer.effect: MultiEffect { layer.effect: MultiEffect {
@@ -38,14 +38,11 @@ Item {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
anchors.topMargin: Config.barConfig.autoHide && !root.visibilities.bar ? 4 : root.bar.implicitHeight anchors.margins: Config.barConfig.border + 1
topLeftRadius: 8 anchors.topMargin: root.bar.implicitHeight + 1
topRightRadius: 8 radius: Config.barConfig.border > 0 ? Config.barConfig.rounding : 0
topLeftRadius: Config.barConfig.rounding
Behavior on anchors.topMargin { topRightRadius: Config.barConfig.rounding
Anim {
}
}
} }
} }
} }
-73
View File
@@ -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;
}
}
}
}
}
-77
View File
@@ -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
}
}
}
}
}
-42
View File
@@ -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
}
}
}
}
-118
View File
@@ -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
}
}
-45
View File
@@ -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
}
}
}
}
+20 -27
View File
@@ -6,43 +6,36 @@ import qs.Modules
import qs.Helpers as Helpers import qs.Helpers as Helpers
import qs.Components import qs.Components
Item { CustomRect {
id: root id: root
required property RowLayout loader required property RowLayout loader
required property Wrapper popouts required property Wrapper popouts
required property PersistentProperties visibilities required property PersistentProperties visibilities
anchors.bottom: parent.bottom color: DynamicColors.tPalette.m3surfaceContainer
anchors.top: parent.top implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2
implicitWidth: timeText.contentWidth + 5 * 2 implicitWidth: timeText.contentWidth + Appearance.padding.normal * 2
radius: Appearance.rounding.full
CustomRect { CustomText {
anchors.bottomMargin: 3 id: timeText
anchors.fill: parent
anchors.topMargin: 3
color: "transparent"
radius: 4
CustomText { anchors.centerIn: parent
id: timeText color: DynamicColors.palette.m3onSurface
text: Time.dateStr
anchors.centerIn: parent Behavior on color {
color: DynamicColors.palette.m3onSurface CAnim {
text: Time.dateStr
Behavior on color {
CAnim {
}
}
}
StateLayer {
acceptedButtons: Qt.LeftButton
onClicked: {
root.visibilities.dashboard = !root.visibilities.dashboard;
} }
} }
} }
StateLayer {
acceptedButtons: Qt.LeftButton
onClicked: {
root.visibilities.dashboard = !root.visibilities.dashboard;
}
}
} }
+9 -9
View File
@@ -5,10 +5,10 @@ import Quickshell.Services.SystemTray
import QtQuick import QtQuick
import qs.Config import qs.Config
import qs.Components import qs.Components
import qs.Modules.Calendar
import qs.Modules.WSOverview import qs.Modules.WSOverview
import qs.Modules.Network import qs.Modules.Network
import qs.Modules.UPower import qs.Modules.UPower
import qs.Modules.Updates
Item { Item {
id: root id: root
@@ -69,14 +69,6 @@ Item {
} }
} }
Popout {
name: "calendar"
sourceComponent: CalendarPopup {
wrapper: root.wrapper
}
}
Popout { Popout {
name: "overview" name: "overview"
@@ -101,6 +93,14 @@ Item {
wrapper: root.wrapper wrapper: root.wrapper
} }
} }
Popout {
name: "updates"
sourceComponent: UpdatesPopout {
wrapper: root.wrapper
}
}
} }
component Popout: Loader { component Popout: Loader {
+1 -1
View File
@@ -7,7 +7,7 @@ ShapePath {
id: root id: root
readonly property bool flatten: wrapper.height < rounding * 2 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 readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property Wrapper wrapper required property Wrapper wrapper
+1 -1
View File
@@ -40,7 +40,7 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
color: "transparent" color: "transparent"
radius: 6 radius: Appearance.rounding.normal - anchors.margins
Item { Item {
id: view id: view
+13 -9
View File
@@ -72,17 +72,21 @@ Item {
} }
} }
Loader { CustomClippingRect {
id: content anchors.fill: parent
active: true Loader {
anchors.bottom: parent.bottom id: content
anchors.horizontalCenter: parent.horizontalCenter
visible: false
sourceComponent: Content { active: true
state: root.dashState anchors.bottom: parent.bottom
visibilities: root.visibilities anchors.horizontalCenter: parent.horizontalCenter
visible: false
sourceComponent: Content {
state: root.dashState
visibilities: root.visibilities
}
} }
} }
} }
@@ -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
}
}
@@ -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
}
}
@@ -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 {
}
}
}
}
}
}
+206
View File
@@ -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
}
}
+297 -5
View File
@@ -2,6 +2,8 @@ pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import QtQuick import QtQuick
import QtQml.Models
import qs.Modules.Dock.Parts
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
import qs.Config import qs.Config
@@ -9,20 +11,310 @@ import qs.Config
Item { Item {
id: root 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 readonly property int padding: Appearance.padding.small
required property var panels required property var panels
property var pendingCommitIds: []
readonly property int rounding: Appearance.rounding.large readonly property int rounding: Appearance.rounding.large
required property ShellScreen screen
required property PersistentProperties visibilities 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 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 id: dockRow
anchors.bottom: parent.bottom property bool enableAddAnimation: false
anchors.horizontalCenter: parent.horizontalCenter
anchors.left: parent.left
anchors.margins: Appearance.padding.small
anchors.top: parent.top 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
}
} }
} }
+91
View File
@@ -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
}
}
}
+12
View File
@@ -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
}
+8 -1
View File
@@ -18,6 +18,12 @@ Item {
implicitWidth: content.implicitWidth implicitWidth: content.implicitWidth
visible: height > 0 visible: height > 0
Behavior on implicitWidth {
Anim {
duration: Appearance.anim.durations.small
}
}
onShouldBeActiveChanged: { onShouldBeActiveChanged: {
if (shouldBeActive) { if (shouldBeActive) {
timer.stop(); timer.stop();
@@ -84,12 +90,13 @@ Item {
id: content id: content
active: false active: false
anchors.horizontalCenter: parent.horizontalCenter anchors.left: parent.left
anchors.top: parent.top anchors.top: parent.top
visible: false visible: false
sourceComponent: Content { sourceComponent: Content {
panels: root.panels panels: root.panels
screen: root.screen
visibilities: root.visibilities visibilities: root.visibilities
Component.onCompleted: root.contentHeight = implicitHeight Component.onCompleted: root.contentHeight = implicitHeight
+66
View File
@@ -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
}
}
+151
View File
@@ -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
}
}
}
+133
View File
@@ -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)
}
}
+6 -18
View File
@@ -5,30 +5,22 @@ import qs.Daemons
import qs.Config import qs.Config
import qs.Helpers import qs.Helpers
Item { CustomRect {
id: root id: root
readonly property string currentMedia: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title") readonly property string currentMedia: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
readonly property int textWidth: Math.min(metrics.width, 200) readonly property int textWidth: Math.min(metrics.width, 200)
anchors.bottom: parent.bottom color: DynamicColors.tPalette.m3surfaceContainer
anchors.top: parent.top implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2
implicitWidth: layout.implicitWidth + Appearance.padding.normal * 2 implicitWidth: layout.implicitWidth + Appearance.padding.normal * 2
radius: Appearance.rounding.full
Behavior on implicitWidth { Behavior on implicitWidth {
Anim { 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 { TextMetrics {
id: metrics id: metrics
@@ -39,11 +31,7 @@ Item {
RowLayout { RowLayout {
id: layout id: layout
anchors.bottom: parent.bottom anchors.centerIn: parent
anchors.horizontalCenter: parent.horizontalCenter
anchors.left: parent.left
anchors.leftMargin: Appearance.padding.normal
anchors.top: parent.top
Behavior on implicitWidth { Behavior on implicitWidth {
Anim { Anim {
@@ -53,7 +41,7 @@ Item {
MaterialIcon { MaterialIcon {
animate: true animate: true
color: Players.active?.isPlaying ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface 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" text: Players.active?.isPlaying ? "music_note" : "music_off"
} }
+23 -30
View File
@@ -5,46 +5,39 @@ import qs.Config
import qs.Helpers import qs.Helpers
import qs.Components import qs.Components
Item { CustomRect {
id: root id: root
required property Wrapper popouts required property Wrapper popouts
required property PersistentProperties visibilities required property PersistentProperties visibilities
anchors.bottom: parent.bottom color: DynamicColors.tPalette.m3surfaceContainer
anchors.top: parent.top implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2
implicitWidth: 30 implicitWidth: implicitHeight
radius: Appearance.rounding.full
CustomRect { MaterialIcon {
anchors.bottomMargin: 3 id: notificationCenterIcon
anchors.fill: parent
anchors.topMargin: 3
color: "transparent"
radius: 4
MaterialIcon { property color iconColor: DynamicColors.palette.m3onSurface
id: notificationCenterIcon
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 Behavior on color {
color: iconColor CAnim {
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;
} }
} }
} }
StateLayer {
cursorShape: Qt.PointingHandCursor
onClicked: {
root.visibilities.sidebar = !root.visibilities.sidebar;
}
}
} }
+1 -1
View File
@@ -15,7 +15,7 @@ ShapePath {
readonly property real utilsWidthDiff: panels.utilities.width - wrapper.width readonly property real utilsWidthDiff: panels.utilities.width - wrapper.width
required property Wrapper wrapper required property Wrapper wrapper
fillColor: flatten ? "transparent" : DynamicColors.palette.m3surface fillColor: DynamicColors.palette.m3surface
strokeWidth: -1 strokeWidth: -1
Behavior on fillColor { Behavior on fillColor {
+1 -1
View File
@@ -14,7 +14,7 @@ Item {
states: State { states: State {
name: "hidden" name: "hidden"
when: root.visibilities.sidebar when: root.visibilities.sidebar || root.visibilities.dashboard || (root.panels.popouts.hasCurrent && root.panels.popouts.currentName.startsWith("traymenu"))
PropertyChanges { PropertyChanges {
root.implicitHeight: 0 root.implicitHeight: 0
+18 -14
View File
@@ -113,22 +113,26 @@ Item {
} }
} }
Loader { CustomClippingRect {
id: content anchors.fill: parent
anchors.left: parent.left Loader {
anchors.verticalCenter: parent.verticalCenter id: content
sourceComponent: Content { anchors.left: parent.left
brightness: root.brightness anchors.verticalCenter: parent.verticalCenter
monitor: root.monitor
muted: root.muted sourceComponent: Content {
sourceMuted: root.sourceMuted brightness: root.brightness
sourceVolume: root.sourceVolume monitor: root.monitor
visibilities: root.visibilities muted: root.muted
volume: root.volume 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)
} }
} }
-116
View File
@@ -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
}
}
}
}
}
}
+30 -57
View File
@@ -4,7 +4,7 @@ import QtQuick.Shapes
import qs.Components import qs.Components
import qs.Config import qs.Config
Item { RowLayout {
id: root id: root
property color accentColor: warning ? DynamicColors.palette.m3error : mainColor property color accentColor: warning ? DynamicColors.palette.m3error : mainColor
@@ -19,75 +19,48 @@ Item {
property bool warning: percentage * 100 >= warningThreshold property bool warning: percentage * 100 >= warningThreshold
property int warningThreshold: 80 property int warningThreshold: 80
clip: true
height: implicitHeight
implicitHeight: 34
implicitWidth: 34
percentage: 0 percentage: 0
visible: width > 0 && height > 0
width: implicitWidth
Behavior on animatedPercentage { Behavior on animatedPercentage {
Anim { Anim {
duration: Appearance.anim.durations.large
} }
} }
Component.onCompleted: animatedPercentage = percentage Component.onCompleted: animatedPercentage = percentage
onPercentageChanged: animatedPercentage = percentage onPercentageChanged: {
const next = percentage;
Canvas { if (Math.abs(next - animatedPercentage) >= 0.05)
id: gaugeCanvas animatedPercentage = next;
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
}
} }
MaterialIcon { MaterialIcon {
anchors.centerIn: parent id: icon
color: DynamicColors.palette.m3onSurface color: DynamicColors.palette.m3onSurface
font.pointSize: 12 font.pointSize: Appearance.font.size.larger
text: root.icon 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)
}
}
}
} }
-77
View File
@@ -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
}
}
}
-59
View File
@@ -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
}
}
}
-123
View File
@@ -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;
}
}
}
}
+13 -21
View File
@@ -8,39 +8,27 @@ import qs.Modules
import qs.Config import qs.Config
import qs.Components import qs.Components
Item { CustomRect {
id: root id: root
required property PersistentProperties visibilities required property PersistentProperties visibilities
anchors.bottom: parent.bottom
anchors.top: parent.top
clip: true 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 { StateLayer {
id: backgroundRect onClicked: root.visibilities.resources = !root.visibilities.resources
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
}
} }
RowLayout { RowLayout {
id: rowLayout id: rowLayout
anchors.centerIn: parent anchors.centerIn: parent
spacing: 0 implicitHeight: root.implicitHeight
spacing: Appearance.spacing.smaller
Ref { Ref {
service: SystemUsage service: SystemUsage
@@ -48,6 +36,7 @@ Item {
Resource { Resource {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Layout.fillHeight: true
icon: "memory" icon: "memory"
mainColor: DynamicColors.palette.m3primary mainColor: DynamicColors.palette.m3primary
percentage: SystemUsage.cpuPerc percentage: SystemUsage.cpuPerc
@@ -55,6 +44,7 @@ Item {
} }
Resource { Resource {
Layout.fillHeight: true
icon: "memory_alt" icon: "memory_alt"
mainColor: DynamicColors.palette.m3secondary mainColor: DynamicColors.palette.m3secondary
percentage: SystemUsage.memPerc percentage: SystemUsage.memPerc
@@ -62,12 +52,14 @@ Item {
} }
Resource { Resource {
Layout.fillHeight: true
icon: "gamepad" icon: "gamepad"
mainColor: DynamicColors.palette.m3tertiary mainColor: DynamicColors.palette.m3tertiary
percentage: SystemUsage.gpuPerc percentage: SystemUsage.gpuPerc
} }
Resource { Resource {
Layout.fillHeight: true
icon: "developer_board" icon: "developer_board"
mainColor: DynamicColors.palette.m3primary mainColor: DynamicColors.palette.m3primary
percentage: SystemUsage.gpuMemUsed percentage: SystemUsage.gpuMemUsed
+1 -1
View File
@@ -7,7 +7,7 @@ ShapePath {
id: root id: root
readonly property bool flatten: wrapper.height < rounding * 2 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 readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property Wrapper wrapper required property Wrapper wrapper
+24 -14
View File
@@ -22,83 +22,92 @@ Item {
ListElement { ListElement {
icon: "settings" icon: "settings"
key: "general"
name: "General" name: "General"
} }
ListElement { ListElement {
icon: "wallpaper" icon: "wallpaper"
key: "wallpaper"
name: "Wallpaper" name: "Wallpaper"
} }
ListElement { ListElement {
icon: "settop_component" icon: "settop_component"
key: "bar"
name: "Bar" name: "Bar"
} }
ListElement { ListElement {
icon: "lock" icon: "lock"
key: "lockscreen"
name: "Lockscreen" name: "Lockscreen"
} }
ListElement { ListElement {
icon: "build_circle" icon: "build_circle"
key: "services"
name: "Services" name: "Services"
} }
ListElement { ListElement {
icon: "notifications" icon: "notifications"
key: "notifications"
name: "Notifications" name: "Notifications"
} }
ListElement { ListElement {
icon: "view_sidebar" icon: "view_sidebar"
key: "sidebar"
name: "Sidebar" name: "Sidebar"
} }
ListElement { ListElement {
icon: "handyman" icon: "handyman"
key: "utilities"
name: "Utilities" name: "Utilities"
} }
ListElement { ListElement {
icon: "dashboard" icon: "dashboard"
key: "dashboard"
name: "Dashboard" name: "Dashboard"
} }
ListElement { ListElement {
icon: "colors" icon: "colors"
key: "appearance"
name: "Appearance" name: "Appearance"
} }
ListElement { ListElement {
icon: "display_settings" icon: "display_settings"
key: "osd"
name: "On screen display" name: "On screen display"
} }
ListElement { ListElement {
icon: "rocket_launch" icon: "rocket_launch"
key: "launcher"
name: "Launcher" name: "Launcher"
} }
ListElement {
icon: "colors"
name: "Colors"
}
} }
CustomRect { CustomClippingRect {
anchors.fill: parent anchors.fill: parent
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainer
radius: 4 radius: Appearance.rounding.normal
CustomListView { CustomListView {
id: clayout id: clayout
anchors.centerIn: parent anchors.bottom: parent.bottom
contentHeight: contentItem.childrenRect.height anchors.horizontalCenter: parent.horizontalCenter
anchors.margins: Appearance.padding.smaller
anchors.top: parent.top
boundsBehavior: Flickable.StopAtBounds
contentWidth: contentItem.childrenRect.width contentWidth: contentItem.childrenRect.width
highlightFollowsCurrentItem: false highlightFollowsCurrentItem: false
implicitHeight: contentItem.childrenRect.height
implicitWidth: contentItem.childrenRect.width implicitWidth: contentItem.childrenRect.width
model: listModel model: listModel
spacing: 5 spacing: 5
@@ -109,7 +118,7 @@ Item {
color: DynamicColors.palette.m3primary color: DynamicColors.palette.m3primary
implicitHeight: clayout.currentItem?.implicitHeight ?? 0 implicitHeight: clayout.currentItem?.implicitHeight ?? 0
implicitWidth: clayout.width implicitWidth: clayout.width
radius: 4 radius: Appearance.rounding.normal - Appearance.padding.smaller
y: clayout.currentItem?.y ?? 0 y: clayout.currentItem?.y ?? 0
Behavior on y { Behavior on y {
@@ -127,11 +136,12 @@ Item {
required property string icon required property string icon
required property int index required property int index
required property string key
required property string name required property string name
implicitHeight: 42 implicitHeight: 42
implicitWidth: 200 implicitWidth: 200
radius: 4 radius: Appearance.rounding.normal - Appearance.padding.smaller
RowLayout { RowLayout {
id: layout id: layout
@@ -148,7 +158,7 @@ Item {
Layout.fillHeight: true Layout.fillHeight: true
Layout.preferredWidth: icon.contentWidth Layout.preferredWidth: icon.contentWidth
color: categoryItem.index === clayout.currentIndex ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface color: categoryItem.index === clayout.currentIndex ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
font.pointSize: 22 font.pointSize: Appearance.font.size.small * 2
text: categoryItem.icon text: categoryItem.icon
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }
@@ -170,7 +180,7 @@ Item {
id: layer id: layer
onClicked: { onClicked: {
root.content.currentCategory = categoryItem.name.toLowerCase(); root.content.currentCategory = categoryItem.key;
clayout.currentIndex = categoryItem.index; clayout.currentIndex = categoryItem.index;
} }
} }
+135 -73
View File
@@ -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.Modules.Settings.Controls
import qs.Config import qs.Config
import qs.Helpers
CustomRect { SettingsPage {
id: root id: root
ColumnLayout { SettingsSection {
id: clayout SettingsHeader {
name: "Scale"
}
anchors.left: parent.left SettingSpinBox {
anchors.right: parent.right name: "Rounding scale"
object: Config.appearance.rounding
setting: "scale"
step: 0.1
}
CustomRect { Separator {
Layout.fillWidth: true }
Layout.preferredHeight: colorLayout.implicitHeight
color: DynamicColors.tPalette.m3surfaceContainer
ColumnLayout { SettingSpinBox {
id: colorLayout name: "Spacing scale"
object: Config.appearance.spacing
setting: "scale"
step: 0.1
}
anchors.left: parent.left Separator {
anchors.margins: Appearance.padding.large }
anchors.right: parent.right
Settings { SettingSpinBox {
name: "smth" name: "Padding scale"
} object: Config.appearance.padding
setting: "scale"
step: 0.1
}
SettingSwitch { Separator {
name: "wallust" }
object: Config.general.color
setting: "wallust"
}
CustomSplitButtonRow { SettingSpinBox {
enabled: true name: "Font size scale"
label: qsTr("Scheme mode") object: Config.appearance.font.size
setting: "scale"
step: 0.1
}
menuItems: [ Separator {
MenuItem { }
property string val: "light"
icon: "light_mode" SettingSpinBox {
text: qsTr("Light") name: "Animation duration scale"
}, object: Config.appearance.anim.durations
MenuItem { setting: "scale"
property string val: "dark" step: 0.1
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();
}
}
}
} }
} }
component Settings: CustomRect { SettingsSection {
id: settingsItem SettingsHeader {
name: "Fonts"
}
required property string name SettingInput {
name: "Sans family"
object: Config.appearance.font.family
setting: "sans"
}
Layout.preferredHeight: 42 Separator {
Layout.preferredWidth: 200 }
radius: 4
CustomText { SettingInput {
id: text name: "Monospace family"
object: Config.appearance.font.family
setting: "mono"
}
anchors.left: parent.left Separator {
anchors.margins: Appearance.padding.smaller }
anchors.right: parent.right
font.bold: true SettingInput {
font.pointSize: 32 name: "Material family"
text: settingsItem.name object: Config.appearance.font.family
verticalAlignment: Text.AlignVCenter 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
} }
} }
} }
+24 -8
View File
@@ -1,13 +1,29 @@
import Quickshell import qs.Modules.Settings.Controls
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Modules as Modules
import qs.Config import qs.Config
import qs.Helpers
CustomRect { SettingsPage {
id: root 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
}
}
} }
+184
View File
@@ -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"
}
}
}
+212
View File
@@ -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)
}
}
}
+196 -39
View File
@@ -1,55 +1,212 @@
import Quickshell import qs.Modules.Settings.Controls
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Modules as Modules
import qs.Config import qs.Config
import qs.Helpers import qs.Components
CustomRect { SettingsPage {
id: root id: root
ColumnLayout { function schemeTypeItem(items, value) {
id: clayout for (let i = 0; i < items.length; i++) {
const item = items[i];
anchors.fill: parent if (item.value === value)
return item;
Settings {
name: "apps"
} }
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 { SettingsSection {
id: settingsItem SettingsHeader {
name: "Color"
}
required property string name CustomSplitButtonRow {
active: Config.general.color.mode === "light" ? menuItems[0] : menuItems[1]
label: qsTr("Scheme mode")
implicitHeight: 42 menuItems: [
implicitWidth: 200 MenuItem {
radius: 4 icon: "light_mode"
text: qsTr("Light")
value: "light"
},
MenuItem {
icon: "dark_mode"
text: qsTr("Dark")
value: "dark"
}
]
RowLayout { onSelected: item => {
id: layout Config.general.color.mode = item.value;
Config.save();
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
} }
} }
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"
}
} }
} }
+148
View File
@@ -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"
}
}
}
@@ -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 {
}
}
}
@@ -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
}
}
}
@@ -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"
}
}
}
+77
View File
@@ -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"
}
}
}
+120
View File
@@ -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"
}
}
}
+26
View File
@@ -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"
}
}
}
+170
View File
@@ -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"
}
}
}
+90 -10
View File
@@ -12,8 +12,9 @@ Item {
id: root id: root
property string currentCategory: "general" property string currentCategory: "general"
readonly property real nonAnimHeight: view.implicitHeight + viewWrapper.anchors.margins * 2 readonly property real nonAnimHeight: Math.floor(screen.height / 1.5) + viewWrapper.anchors.margins * 2
readonly property real nonAnimWidth: view.implicitWidth + 500 + 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 required property PersistentProperties visibilities
implicitHeight: nonAnimHeight implicitHeight: nonAnimHeight
@@ -22,24 +23,41 @@ Item {
Connections { Connections {
function onCurrentCategoryChanged() { function onCurrentCategoryChanged() {
stack.pop(); stack.pop();
if (currentCategory === "general") { if (currentCategory === "general")
stack.push(general); stack.push(general);
} else if (currentCategory === "wallpaper") { else if (currentCategory === "wallpaper")
stack.push(background); stack.push(background);
} else if (currentCategory === "appearance") { else if (currentCategory === "bar")
stack.push(bar);
else if (currentCategory === "appearance")
stack.push(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 target: root
} }
ClippingRectangle { CustomClippingRect {
id: viewWrapper id: viewWrapper
anchors.fill: parent anchors.fill: parent
anchors.margins: Appearance.padding.smaller anchors.margins: Appearance.padding.smaller
color: "transparent" radius: Appearance.rounding.large - Appearance.padding.smaller
Item { Item {
id: view id: view
@@ -47,7 +65,6 @@ Item {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.left: parent.left anchors.left: parent.left
anchors.top: parent.top anchors.top: parent.top
implicitHeight: layout.implicitHeight
implicitWidth: layout.implicitWidth implicitWidth: layout.implicitWidth
Categories { Categories {
@@ -67,7 +84,7 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainer
radius: 4 radius: Appearance.rounding.normal
StackView { StackView {
id: stack id: stack
@@ -99,4 +116,67 @@ Item {
Cat.Appearance { 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 {
}
}
} }
+12
View File
@@ -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
}
@@ -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")
}
}
}
@@ -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")
}
}
}
@@ -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)
}
}
}
}
}
}
@@ -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();
}
}
}
}
}
+250
View File
@@ -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")
}
}
}
}
}
}
@@ -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
}
}
}
@@ -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();
}
}
}
}
@@ -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<string> 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]);
}
}
}
}
@@ -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();
}
}
}
}
+25 -16
View File
@@ -3,7 +3,7 @@ import QtQuick.Layouts
import qs.Components import qs.Components
import qs.Config import qs.Config
RowLayout { Item {
id: root id: root
required property string name required property string name
@@ -11,26 +11,35 @@ RowLayout {
required property string setting required property string setting
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 42 Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2
CustomText { RowLayout {
id: text id: row
Layout.alignment: Qt.AlignLeft anchors.left: parent.left
Layout.fillWidth: true anchors.margins: Appearance.padding.small
font.pointSize: 16 anchors.right: parent.right
text: root.name anchors.verticalCenter: parent.verticalCenter
}
CustomSwitch { CustomText {
id: cswitch id: text
Layout.alignment: Qt.AlignRight Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
checked: root.object[root.setting] Layout.fillWidth: true
font.pointSize: Appearance.font.size.larger
text: root.name
}
onToggled: { CustomSwitch {
root.object[root.setting] = checked; id: cswitch
Config.save();
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
checked: root.object[root.setting]
onToggled: {
root.object[root.setting] = checked;
Config.save();
}
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show More