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.Controls
import qs.Config
Button {
id: control
required property color bgColor
property color bgColor: DynamicColors.palette.m3primary
property int radius: 4
required property color textColor
property color textColor: DynamicColors.palette.m3onPrimary
background: CustomRect {
color: control.bgColor
+1 -1
View File
@@ -5,7 +5,7 @@ import qs.Config
RadioButton {
id: root
font.pointSize: 12
font.pointSize: Appearance.font.size.normal
implicitHeight: Math.max(implicitIndicatorHeight, implicitContentHeight)
implicitWidth: implicitIndicatorWidth + implicitContentWidth + contentItem.anchors.leftMargin
+6 -3
View File
@@ -28,6 +28,7 @@ RowLayout {
CustomTextField {
id: textField
implicitHeight: upButton.implicitHeight
inputMethodHints: Qt.ImhFormattedNumbersOnly
leftPadding: Appearance.padding.normal
padding: Appearance.padding.small
@@ -37,7 +38,7 @@ RowLayout {
background: CustomRect {
color: DynamicColors.tPalette.m3surfaceContainerHigh
implicitWidth: 100
radius: Appearance.rounding.small
radius: Appearance.rounding.full
}
validator: DoubleValidator {
bottom: root.min
@@ -82,10 +83,12 @@ RowLayout {
}
CustomRect {
id: upButton
color: DynamicColors.palette.m3primary
implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2
implicitWidth: implicitHeight
radius: Appearance.rounding.small
radius: Appearance.rounding.full
StateLayer {
id: upState
@@ -119,7 +122,7 @@ RowLayout {
color: DynamicColors.palette.m3primary
implicitHeight: downIcon.implicitHeight + Appearance.padding.small * 2
implicitWidth: implicitHeight
radius: Appearance.rounding.small
radius: Appearance.rounding.full
StateLayer {
id: downState
+23 -1
View File
@@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Layouts
import qs.Config
import qs.Helpers
Row {
id: root
@@ -29,8 +30,29 @@ Row {
property int type: CustomSplitButton.Filled
property real verticalPadding: Appearance.padding.smaller
function closeDropdown(): void {
SettingsDropdowns.close(menu);
}
function openDropdown(): void {
if (root.disabled)
return;
SettingsDropdowns.open(menu, root);
}
function toggleDropdown(): void {
if (root.disabled)
return;
SettingsDropdowns.toggle(menu, root);
}
spacing: Math.floor(Appearance.spacing.small / 2)
onExpandedChanged: {
if (!expanded)
SettingsDropdowns.forget(menu);
}
CustomRect {
bottomRightRadius: Appearance.rounding.small / 2
color: root.disabled ? root.disabledColor : root.color
@@ -109,7 +131,7 @@ Row {
id: expandStateLayer
function onClicked(): void {
root.expanded = !root.expanded;
root.toggleDropdown();
}
color: root.textColor
+12 -10
View File
@@ -4,7 +4,7 @@ import QtQuick
import QtQuick.Layouts
import qs.Config
CustomRect {
Item {
id: root
property alias active: splitButton.active
@@ -18,38 +18,40 @@ CustomRect {
signal selected(item: MenuItem)
Layout.fillWidth: true
Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2
clip: false
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2)
implicitHeight: row.implicitHeight + Appearance.padding.large * 2
opacity: enabled ? 1.0 : 0.5
radius: Appearance.rounding.normal
z: splitButton.menu.implicitHeight > 0 ? expandedZ : 1
z: root.expanded ? expandedZ : -1
RowLayout {
id: row
anchors.fill: parent
anchors.margins: Appearance.padding.large
anchors.left: parent.left
anchors.margins: Appearance.padding.small
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Appearance.spacing.normal
CustomText {
Layout.fillWidth: true
color: root.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.larger
text: root.label
z: root.expanded ? root.expandedZ : -1
}
CustomSplitButton {
id: splitButton
enabled: root.enabled
menu.z: 1
type: CustomSplitButton.Filled
z: root.expanded ? root.expandedZ : -1
menu.onItemSelected: item => {
root.selected(item);
splitButton.closeDropdown();
}
stateLayer.onClicked: {
splitButton.expanded = !splitButton.expanded;
splitButton.toggleDropdown();
}
}
}
+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.Templates
import qs.Config
Slider {
BaseStyledSlider {
id: root
property color color: DynamicColors.palette.m3secondary
required property string icon
property bool initialized
property real oldValue
trackContent: Component {
Item {
property var groove
readonly property real handleHeight: handleItem ? handleItem.height : 0
property var handleItem
readonly property real handleWidth: handleItem ? handleItem.width : 0
orientation: Qt.Vertical
background: CustomRect {
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2)
radius: Appearance.rounding.full
CustomRect {
anchors.left: parent.left
anchors.right: parent.right
color: root.color
implicitHeight: parent.height - y
radius: parent.radius
y: root.handle.y
}
}
handle: Item {
id: handle
property alias moving: icon.moving
implicitHeight: root.width
implicitWidth: root.width
y: root.visualPosition * (root.availableHeight - height)
Elevation {
anchors.fill: parent
level: handleInteraction.containsMouse ? 2 : 1
radius: rect.radius
}
CustomRect {
id: rect
// Set by BaseStyledSlider's Loader
property var rootSlider
anchors.fill: parent
color: DynamicColors.palette.m3inverseSurface
radius: Appearance.rounding.full
MouseArea {
id: handleInteraction
acceptedButtons: Qt.NoButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
CustomRect {
color: rootSlider?.color
height: rootSlider?.isVertical ? handleHeight + (1 - rootSlider?.visualPosition) * (groove?.height - handleHeight) : groove?.height
radius: groove?.radius
width: rootSlider?.isHorizontal ? handleWidth + rootSlider?.visualPosition * (groove?.width - handleWidth) : groove?.width
x: rootSlider?.isHorizontal ? (rootSlider?.mirrored ? groove?.width - width : 0) : 0
y: rootSlider?.isVertical ? groove?.height - height : 0
}
MaterialIcon {
id: icon
property bool moving
function update(): void {
animate = !moving;
binding.when = moving;
font.pointSize = moving ? Appearance.font.size.small : Appearance.font.size.larger;
font.family = moving ? Appearance.font.family.sans : Appearance.font.family.material;
}
anchors.centerIn: parent
color: DynamicColors.palette.m3inverseOnSurface
text: root.icon
onMovingChanged: anim.restart()
Binding {
id: binding
property: "text"
target: icon
value: Math.round(root.value * 100)
when: false
}
SequentialAnimation {
id: anim
Anim {
duration: Appearance.anim.durations.normal / 2
easing.bezierCurve: Appearance.anim.curves.standardAccel
property: "scale"
target: icon
to: 0
}
ScriptAction {
script: icon.update()
}
Anim {
duration: Appearance.anim.durations.normal / 2
easing.bezierCurve: Appearance.anim.curves.standardDecel
property: "scale"
target: icon
to: 1
}
}
}
}
}
Behavior on value {
Anim {
duration: Appearance.anim.durations.large
}
}
onPressedChanged: handle.moving = pressed
onValueChanged: {
if (!initialized) {
initialized = true;
return;
}
if (Math.abs(value - oldValue) < 0.01)
return;
oldValue = value;
handle.moving = true;
stateChangeDelay.restart();
}
Timer {
id: stateChangeDelay
interval: 500
onTriggered: {
if (!root.pressed)
handle.moving = false;
}
}
}
+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
font.family: "Material Symbols Rounded"
font.pointSize: 15
font.pointSize: Appearance.font.size.larger
font.variableAxes: ({
FILL: fill.toFixed(1),
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 int small: 5 * scale
property int smaller: 7 * scale
property int smallest: 2 * scale
}
component Rounding: JsonObject {
property int full: 1000 * scale
@@ -78,6 +79,7 @@ JsonObject {
property int normal: 17 * scale
property real scale: 1
property int small: 12 * scale
property int smallest: 8 * scale
}
component Spacing: JsonObject {
property int large: 20 * scale
+2
View File
@@ -2,6 +2,7 @@ import Quickshell.Io
JsonObject {
property bool autoHide: false
property int border: 8
property list<var> entries: [
{
id: "workspaces",
@@ -60,6 +61,7 @@ JsonObject {
enabled: true
},
]
property int height: 34
property Popouts popouts: Popouts {
}
property int rounding: 8
+12 -5
View File
@@ -33,6 +33,10 @@ Singleton {
recentSaveCooldown.restart();
}
function saveNoToast(): void {
saveTimer.restart();
}
function serializeAppearance(): var {
return {
rounding: {
@@ -56,8 +60,8 @@ Singleton {
}
},
anim: {
mediaGifSpeedAdjustment: 300,
sessionGifSpeed: 0.7,
mediaGifSpeedAdjustment: appearance.anim.mediaGifSpeedAdjustment,
sessionGifSpeed: appearance.anim.sessionGifSpeed,
durations: {
scale: appearance.anim.durations.scale
}
@@ -81,6 +85,8 @@ Singleton {
return {
autoHide: barConfig.autoHide,
rounding: barConfig.rounding,
border: barConfig.border,
height: barConfig.height,
popouts: {
tray: barConfig.popouts.tray,
audio: barConfig.popouts.audio,
@@ -155,10 +161,10 @@ Singleton {
return {
enable: dock.enable,
height: dock.height,
hoverRegionHeight: dock.hoverRegionHeight,
hoverToReveal: dock.hoverToReveal,
pinnedApps: dock.pinnedApps,
pinnedOnStartup: dock.pinnedOnStartup
pinnedOnStartup: dock.pinnedOnStartup,
ignoredAppRegexes: dock.ignoredAppRegexes
};
}
@@ -166,6 +172,7 @@ Singleton {
return {
logo: general.logo,
wallpaperPath: general.wallpaperPath,
desktopIcons: general.desktopIcons,
color: {
wallust: general.color.wallust,
mode: general.color.mode,
@@ -182,7 +189,7 @@ Singleton {
explorer: general.apps.explorer
},
idle: {
timouts: general.idle.timeouts
timeouts: general.idle.timeouts
}
};
}
+1 -1
View File
@@ -3,8 +3,8 @@ import Quickshell.Io
JsonObject {
property bool enable: false
property real height: 60
property real hoverRegionHeight: 2
property bool hoverToReveal: true
property list<string> ignoredAppRegexes: []
property list<string> pinnedApps: ["org.kde.dolphin", "kitty",]
property bool pinnedOnStartup: false
}
+3
View File
@@ -6,6 +6,7 @@ JsonObject {
}
property Color color: Color {
}
property bool desktopIcons: false
property Idle idle: Idle {
}
property string logo: ""
@@ -29,10 +30,12 @@ JsonObject {
component Idle: JsonObject {
property list<var> timeouts: [
{
name: "Lock",
timeout: 180,
idleAction: "lock"
},
{
name: "Screen",
timeout: 300,
idleAction: "dpms off",
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.Launcher as Launcher
import qs.Modules.Resources as Resources
import qs.Modules.Drawing as Drawing
import qs.Modules.Settings as Settings
import qs.Modules.Dock as Dock
Shape {
id: root
@@ -22,13 +23,15 @@ Shape {
required property PersistentProperties visibilities
anchors.fill: parent
// anchors.margins: 8
anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? 0 : bar.implicitHeight
anchors.margins: Config.barConfig.border
anchors.topMargin: bar.implicitHeight
asynchronous: true
preferredRendererType: Shape.CurveRenderer
Behavior on anchors.topMargin {
Anim {
}
Drawing.Background {
startX: 0
startY: wrapper.y - rounding
wrapper: root.panels.drawing
}
Resources.Background {
@@ -45,7 +48,8 @@ Shape {
Modules.Background {
invertBottomRounding: wrapper.x <= 0
startX: wrapper.x - 8
rounding: root.panels.popouts.currentName.startsWith("updates") ? Appearance.rounding.normal : Appearance.rounding.smallest
startX: wrapper.x - rounding
startY: wrapper.y
wrapper: root.panels.popouts
}
@@ -92,4 +96,12 @@ Shape {
startY: 0
wrapper: root.panels.settings
}
Dock.Background {
id: dock
startX: (root.width - wrapper.width) / 2 - rounding
startY: root.height
wrapper: root.panels.dock
}
}
+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
property bool dashboardShortcutActive
property point dragStart
required property Drawing drawing
required property DrawingInput input
property bool osdShortcutActive
required property Panels panels
required property BarPopouts.Wrapper popouts
@@ -18,15 +20,15 @@ CustomMouseArea {
required property PersistentProperties visibilities
function inBottomPanel(panel: Item, x: real, y: real): bool {
return y > root.height - panel.height && withinPanelWidth(panel, x, y);
return y > root.height - panel.height - Config.barConfig.border && withinPanelWidth(panel, x, y);
}
function inLeftPanel(panel: Item, x: real, y: real): bool {
return x < panel.x + panel.width && withinPanelHeight(panel, x, y);
return x < panel.x + panel.width + Config.barConfig.border && withinPanelHeight(panel, x, y);
}
function inRightPanel(panel: Item, x: real, y: real): bool {
return x > panel.x && withinPanelHeight(panel, x, y);
return x > panel.x - Config.barConfig.border && withinPanelHeight(panel, x, y);
}
function inTopPanel(panel: Item, x: real, y: real): bool {
@@ -51,20 +53,10 @@ CustomMouseArea {
anchors.fill: parent
hoverEnabled: true
// onPressed: event => {
// if ( root.popouts.hasCurrent && !inTopPanel( root.popouts, event.x, event.y )) {
// root.popouts.hasCurrent = false;
// } else if (root.visibilities.sidebar && !inRightPanel( panels.sidebar, event.x, event.y )) {
// root.visibilities.sidebar = false;
// } else if (root.visibilities.dashboard && !inTopPanel( panels.dashboard, event.x, event.y )) {
// root.visibilities.dashboard = false;
// }
// }
propagateComposedEvents: true
onContainsMouseChanged: {
if (!containsMouse) {
// Only hide if not activated by shortcut
if (!osdShortcutActive) {
visibilities.osd = false;
root.panels.osd.hovered = false;
@@ -74,8 +66,8 @@ CustomMouseArea {
popouts.hasCurrent = false;
}
if (Config.barConfig.autoHide && !root.visibilities.sidebar && !root.visibilities.dashboard)
root.visibilities.bar = false;
if (Config.barConfig.autoHide)
bar.isHovered = false;
}
}
onPositionChanged: event => {
@@ -87,142 +79,69 @@ CustomMouseArea {
const dragX = x - dragStart.x;
const dragY = y - dragStart.y;
// Show bar in non-exclusive mode on hover
if (!visibilities.bar && Config.barConfig.autoHide && y < bar.implicitHeight + bar.anchors.topMargin)
visibilities.bar = true;
if (panels.sidebar.width === 0) {
// Show osd on hover
const showOsd = inRightPanel(panels.osd, x, y);
// // Always update visibility based on hover if not in shortcut mode
if (!osdShortcutActive) {
visibilities.osd = showOsd;
root.panels.osd.hovered = showOsd;
} else if (showOsd) {
// If hovering over OSD area while in shortcut mode, transition to hover control
osdShortcutActive = false;
root.panels.osd.hovered = true;
}
// const showSidebar = pressed && dragStart.x > bar.implicitWidth + panels.sidebar.x;
//
// // Show/hide session on drag
// if (pressed && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) {
// if (dragX < -Config.session.dragThreshold)
// visibilities.session = true;
// else if (dragX > Config.session.dragThreshold)
// visibilities.session = false;
//
// // Show sidebar on drag if in session area and session is nearly fully visible
// if (showSidebar && panels.session.width >= panels.session.nonAnimWidth && dragX < -Config.sidebar.dragThreshold)
// visibilities.sidebar = true;
// } else if (showSidebar && dragX < -Config.sidebar.dragThreshold) {
// // Show sidebar on drag if not in session area
// visibilities.sidebar = true;
// }
} else {
const outOfSidebar = x < width - panels.sidebar.width;
// Show osd on hover
const showOsd = outOfSidebar && inRightPanel(panels.osd, x, y);
// Always update visibility based on hover if not in shortcut mode
if (!osdShortcutActive) {
visibilities.osd = showOsd;
root.panels.osd.hovered = showOsd;
} else if (showOsd) {
// If hovering over OSD area while in shortcut mode, transition to hover control
osdShortcutActive = false;
root.panels.osd.hovered = true;
}
//
// // Show/hide session on drag
// if (pressed && outOfSidebar && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) {
// if (dragX < -Config.session.dragThreshold)
// visibilities.session = true;
// else if (dragX > Config.session.dragThreshold)
// visibilities.session = false;
// }
//
// // Hide sidebar on drag
// if (pressed && inRightPanel(panels.sidebar, dragStart.x, 0) && dragX > Config.sidebar.dragThreshold)
// visibilities.sidebar = false;
if (root.visibilities.isDrawing && !root.inLeftPanel(root.panels.drawing, x, y)) {
root.input.z = 2;
root.panels.drawing.expanded = false;
}
// Show launcher on hover, or show/hide on drag if hover is disabled
// if (Config.launcher.showOnHover) {
// if (!visibilities.launcher && inBottomPanel(panels.launcher, x, y))
// visibilities.launcher = true;
// } else if (pressed && inBottomPanel(panels.launcher, dragStart.x, dragStart.y) && withinPanelWidth(panels.launcher, x, y)) {
// if (dragY < -Config.launcher.dragThreshold)
// visibilities.launcher = true;
// else if (dragY > Config.launcher.dragThreshold)
// visibilities.launcher = false;
// }
//
// // Show dashboard on hover
// const showDashboard = Config.dashboard.showOnHover && inTopPanel(panels.dashboard, x, y);
//
// // Always update visibility based on hover if not in shortcut mode
// if (!dashboardShortcutActive) {
// visibilities.dashboard = showDashboard;
// } else if (showDashboard) {
// // If hovering over dashboard area while in shortcut mode, transition to hover control
// dashboardShortcutActive = false;
// }
//
// // Show/hide dashboard on drag (for touchscreen devices)
// if (pressed && inTopPanel(panels.dashboard, dragStart.x, dragStart.y) && withinPanelWidth(panels.dashboard, x, y)) {
// if (dragY > Config.dashboard.dragThreshold)
// visibilities.dashboard = true;
// else if (dragY < -Config.dashboard.dragThreshold)
// visibilities.dashboard = false;
// }
//
// // Show utilities on hover
// const showUtilities = inBottomPanel(panels.utilities, x, y);
//
// // Always update visibility based on hover if not in shortcut mode
// if (!utilitiesShortcutActive) {
// visibilities.utilities = showUtilities;
// } else if (showUtilities) {
// // If hovering over utilities area while in shortcut mode, transition to hover control
// utilitiesShortcutActive = false;
// }
if (!visibilities.bar && Config.barConfig.autoHide && y < bar.implicitHeight)
bar.isHovered = true;
// Show popouts on hover
if (y < bar.implicitHeight) {
bar.checkPopout(x);
if (panels.sidebar.width === 0) {
const showOsd = inRightPanel(panels.osd, x, y);
if (showOsd) {
osdShortcutActive = false;
root.panels.osd.hovered = true;
}
} else {
const outOfSidebar = x < width - panels.sidebar.width;
const showOsd = outOfSidebar && inRightPanel(panels.osd, x, y);
if (!osdShortcutActive) {
visibilities.osd = showOsd;
root.panels.osd.hovered = showOsd;
} else if (showOsd) {
osdShortcutActive = false;
root.panels.osd.hovered = true;
}
}
if (!visibilities.dock && !visibilities.launcher && inBottomPanel(panels.dock, x, y))
visibilities.dock = true;
if (y < root.bar.implicitHeight) {
root.bar.checkPopout(x);
}
}
// Monitor individual visibility changes
Connections {
function onDashboardChanged() {
if (root.visibilities.dashboard) {
// Dashboard became visible, immediately check if this should be shortcut mode
const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY);
if (!inDashboardArea) {
root.dashboardShortcutActive = true;
}
root.visibilities.settings = false;
root.visibilities.sidebar = false;
root.popouts.hasCurrent = false;
} else {
// Dashboard hidden, clear shortcut flag
root.dashboardShortcutActive = false;
// root.visibilities.bar = false;
}
}
function onIsDrawingChanged() {
if (!root.visibilities.isDrawing)
root.drawing.clear();
}
function onLauncherChanged() {
// If launcher is hidden, clear shortcut flags for dashboard and OSD
if (!root.visibilities.launcher) {
root.dashboardShortcutActive = false;
root.osdShortcutActive = false;
root.utilitiesShortcutActive = false;
// Also hide dashboard and OSD if they're not being hovered
const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
if (!inOsdArea) {
@@ -230,17 +149,20 @@ CustomMouseArea {
root.panels.osd.hovered = false;
}
}
if (root.visibilities.launcher) {
root.visibilities.dock = false;
root.visibilities.settings = false;
}
}
function onOsdChanged() {
if (root.visibilities.osd) {
// OSD became visible, immediately check if this should be shortcut mode
const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY);
if (!inOsdArea) {
root.osdShortcutActive = true;
}
} else {
// OSD hidden, clear shortcut flag
root.osdShortcutActive = false;
}
}
@@ -249,6 +171,18 @@ CustomMouseArea {
if (root.visibilities.resources && root.popouts.currentName.startsWith("audio")) {
root.popouts.hasCurrent = false;
}
if (root.visibilities.resources)
root.visibilities.settings = false;
}
function onSettingsChanged() {
if (root.visibilities.settings) {
root.visibilities.resources = false;
root.visibilities.dashboard = false;
root.panels.popouts.hasCurrent = false;
root.visibilities.launcher = false;
}
}
function onSidebarChanged() {
@@ -260,13 +194,11 @@ CustomMouseArea {
function onUtilitiesChanged() {
if (root.visibilities.utilities) {
// Utilities became visible, immediately check if this should be shortcut mode
const inUtilitiesArea = root.inBottomPanel(root.panels.utilities, root.mouseX, root.mouseY);
if (!inUtilitiesArea) {
root.utilitiesShortcutActive = true;
}
} else {
// Utilities hidden, clear shortcut flag
root.utilitiesShortcutActive = false;
}
}
+28 -7
View File
@@ -11,6 +11,8 @@ import qs.Components.Toast as Toasts
import qs.Modules.Launcher as Launcher
import qs.Modules.Resources as Resources
import qs.Modules.Settings as Settings
import qs.Modules.Drawing as Drawing
import qs.Modules.Dock as Dock
import qs.Config
Item {
@@ -18,6 +20,9 @@ Item {
required property Item bar
readonly property alias dashboard: dashboard
readonly property alias dock: dock
readonly property alias drawing: drawing
required property Canvas drawingItem
readonly property alias launcher: launcher
readonly property alias notifications: notifications
readonly property alias osd: osd
@@ -31,13 +36,8 @@ Item {
required property PersistentProperties visibilities
anchors.fill: parent
// anchors.margins: 8
anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? 0 : bar.implicitHeight
Behavior on anchors.topMargin {
Anim {
}
}
anchors.margins: Config.barConfig.border
anchors.topMargin: bar.implicitHeight
Resources.Wrapper {
id: resources
@@ -47,6 +47,16 @@ Item {
visibilities: root.visibilities
}
Drawing.Wrapper {
id: drawing
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
drawing: root.drawingItem
screen: root.screen
visibilities: root.visibilities
}
Osd.Wrapper {
id: osd
@@ -133,6 +143,17 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
panels: root
screen: root.screen
visibilities: root.visibilities
}
Dock.Wrapper {
id: dock
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
panels: root
screen: root.screen
visibilities: root.visibilities
}
}
+66 -69
View File
@@ -21,30 +21,25 @@ Variants {
required property var modelData
PanelWindow {
id: bar
Exclusions {
bar: bar
screen: scope.modelData
}
CustomWindow {
id: win
readonly property bool hasFullscreen: Hypr.monitorFor(screen)?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2)
property var root: Quickshell.shellDir
property bool trayMenuVisible: false
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.resources ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
WlrLayershell.namespace: "ZShell-Bar"
WlrLayershell.keyboardFocus: visibilities.dock || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.resources ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
color: "transparent"
contentItem.focus: true
mask: visibilities.isDrawing ? null : region
name: "Bar"
screen: scope.modelData
mask: Region {
id: region
height: bar.screen.height - backgroundRect.implicitHeight
intersection: Intersection.Xor
regions: popoutRegions.instances
width: bar.width
x: 0
y: Config.barConfig.autoHide && !visibilities.bar ? 4 : backgroundRect.height
}
contentItem.Keys.onEscapePressed: {
if (Config.barConfig.autoHide)
visibilities.bar = false;
@@ -54,22 +49,23 @@ Variants {
visibilities.settings = false;
visibilities.resources = false;
}
onHasFullscreenChanged: {
visibilities.launcher = false;
visibilities.dashboard = false;
visibilities.osd = false;
visibilities.settings = false;
visibilities.resources = false;
}
PanelWindow {
id: exclusionZone
Region {
id: region
WlrLayershell.exclusionMode: Config.barConfig.autoHide ? ExclusionMode.Ignore : ExclusionMode.Auto
WlrLayershell.layer: WlrLayer.Bottom
WlrLayershell.namespace: "ZShell-Bar-Exclusion"
color: "transparent"
implicitHeight: backgroundRect.height
screen: bar.screen
anchors {
left: true
right: true
top: true
}
height: win.height - bar.implicitHeight - Config.barConfig.border
intersection: Intersection.Xor
regions: popoutRegions.instances
width: win.width - Config.barConfig.border * 2
x: Config.barConfig.border
y: bar.implicitHeight
}
anchors {
@@ -90,16 +86,16 @@ Variants {
height: modelData.height
intersection: Intersection.Subtract
width: modelData.width
x: modelData.x
y: modelData.y + backgroundRect.implicitHeight
x: modelData.x + Config.barConfig.border
y: modelData.y + bar.implicitHeight
}
}
HyprlandFocusGrab {
id: focusGrab
active: visibilities.resources || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu"))
windows: [bar]
active: visibilities.dock || visibilities.resources || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu"))
windows: [win]
onCleared: {
visibilities.launcher = false;
@@ -108,6 +104,7 @@ Variants {
visibilities.osd = false;
visibilities.settings = false;
visibilities.resources = false;
visibilities.dock = false;
panels.popouts.hasCurrent = false;
}
}
@@ -117,6 +114,8 @@ Variants {
property bool bar
property bool dashboard
property bool dock
property bool isDrawing
property bool launcher
property bool notif: NotifServer.popups.length > 0
property bool osd
@@ -130,7 +129,7 @@ Variants {
Binding {
property: "bar"
target: visibilities
value: visibilities.sidebar || visibilities.dashboard || visibilities.osd || visibilities.notif || visibilities.resources
value: visibilities.sidebar || visibilities.dashboard || visibilities.osd || visibilities.notif || visibilities.resources || visibilities.settings || bar.isHovered
when: Config.barConfig.autoHide
}
@@ -146,68 +145,66 @@ Variants {
}
Border {
bar: backgroundRect
bar: bar
visibilities: visibilities
}
Backgrounds {
bar: backgroundRect
bar: bar
panels: panels
visibilities: visibilities
z: 1
}
}
Drawing {
id: drawing
anchors.fill: parent
z: 2
}
DrawingInput {
id: input
bar: bar
drawing: drawing
panels: panels
popout: panels.drawing
visibilities: visibilities
z: 2
}
Interactions {
id: mouseArea
anchors.fill: parent
bar: barLoader
bar: bar
drawing: drawing
input: input
panels: panels
popouts: panels.popouts
screen: scope.modelData
visibilities: visibilities
z: 1
Panels {
id: panels
bar: backgroundRect
bar: bar
drawingItem: drawing
screen: scope.modelData
visibilities: visibilities
}
CustomRect {
id: backgroundRect
property Wrapper popouts: panels.popouts
BarLoader {
id: bar
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: Config.barConfig.autoHide && !visibilities.bar ? -30 : 0
color: "transparent"
implicitHeight: barLoader.implicitHeight
radius: 0
Behavior on anchors.topMargin {
Anim {
}
}
Behavior on color {
CAnim {
}
}
BarLoader {
id: barLoader
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
bar: bar
popouts: panels.popouts
screen: scope.modelData
visibilities: visibilities
}
popouts: panels.popouts
screen: scope.modelData
visibilities: visibilities
}
}
}
+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() {
if (Config.general.color.neovimColors) {
if (Config.general.color.schemeGeneration) {
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${WallpaperPath.currentWallpaperPath}`, "--thumbnail-path", `${Paths.cache}/imagecache/thumbnail.jpg`, "--output", `${Paths.state}/scheme.json`, "--scheme", `${Config.colors.schemeType}`, "--mode", "light"]);
} else {
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--preset", `${DynamicColors.scheme}:${DynamicColors.flavour}`, "--output", `${Paths.state}/scheme.json`, "--mode", "light"]);
+17 -4
View File
@@ -141,7 +141,7 @@ MouseArea {
sy = ssy;
ex = x;
ey = y;
} else {
} else if (!saveTimer.running) {
checkClientRects(x, y);
}
}
@@ -154,7 +154,7 @@ MouseArea {
return;
if (root.loader.freeze) {
save();
saveTimer.start();
} else {
overlay.visible = border.visible = false;
screencopy.visible = false;
@@ -162,6 +162,16 @@ MouseArea {
}
}
Timer {
id: saveTimer
interval: 25
repeat: false
running: false
onTriggered: root.save()
}
SequentialAnimation {
id: closeAnim
@@ -217,9 +227,10 @@ MouseArea {
paintCursor: false
onHasContentChanged: {
if (hasContent && !root.loader.freeze) {
if (hasContent) {
overlay.visible = border.visible = true;
root.save();
if (!root.loader.freeze)
root.save();
}
}
}
@@ -233,6 +244,7 @@ MouseArea {
layer.enabled: true
opacity: 0.3
radius: root.realRounding
visible: false
layer.effect: MultiEffect {
maskEnabled: true
@@ -270,6 +282,7 @@ MouseArea {
implicitHeight: selectionRect.implicitHeight + root.realBorderWidth * 2
implicitWidth: selectionRect.implicitWidth + root.realBorderWidth * 2
radius: root.realRounding > 0 ? root.realRounding + root.realBorderWidth : 0
visible: false
x: selectionRect.x - root.realBorderWidth
y: selectionRect.y - root.realBorderWidth
+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 real gpuMemTotal: 0
property real gpuMemUsed
property string gpuName: ""
property real gpuPerc
property real gpuTemp
readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType
@@ -80,8 +79,29 @@ Singleton {
onTriggered: {
stat.reload();
meminfo.reload();
if (root.gpuType === "GENERIC")
gpuUsage.running = true;
}
}
Timer {
interval: 60000 * 120
repeat: true
running: true
triggeredOnStart: true
onTriggered: {
storage.running = true;
gpuUsage.running = true;
}
}
Timer {
interval: Config.dashboard.resourceUpdateInterval * 5
repeat: true
running: root.refCount > 0
triggeredOnStart: true
onTriggered: {
sensors.running = true;
}
}
@@ -112,10 +132,13 @@ Singleton {
const totalDiff = total - root.lastCpuTotal;
const idleDiff = idle - root.lastCpuIdle;
root.cpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0;
const newCpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0;
root.lastCpuTotal = total;
root.lastCpuIdle = idle;
if (Math.abs(newCpuPerc - root.cpuPerc) >= 0.01)
root.cpuPerc = newCpuPerc;
}
}
}
@@ -127,8 +150,14 @@ Singleton {
onLoaded: {
const data = text();
root.memTotal = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1;
root.memUsed = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0;
const total = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1;
const used = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0;
if (root.memTotal !== total)
root.memTotal = total;
if (Math.abs(used - root.memUsed) >= 16384)
root.memUsed = used;
}
}
@@ -274,22 +303,54 @@ Singleton {
}
}
Process {
id: gpuUsageNvidia
command: ["/usr/bin/nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu,memory.used", "--format=csv,noheader,nounits", "-lms", "1000"]
running: root.refCount > 0 && root.gpuType === "NVIDIA"
stdout: SplitParser {
onRead: data => {
const parts = String(data).trim().split(/\s*,\s*/);
if (parts.length < 3)
return;
const usageRaw = parseInt(parts[0], 10);
const tempRaw = parseInt(parts[1], 10);
const memRaw = parseInt(parts[2], 10);
if (!Number.isFinite(usageRaw) || !Number.isFinite(tempRaw) || !Number.isFinite(memRaw))
return;
const newGpuPerc = Math.max(0, Math.min(1, usageRaw / 100));
const newGpuTemp = tempRaw;
const newGpuMemUsed = root.gpuMemTotal > 0 ? Math.max(0, Math.min(1, memRaw / root.gpuMemTotal)) : 0;
// Only publish meaningful changes to avoid needless binding churn / repaints
if (Math.abs(root.gpuPerc - newGpuPerc) >= 0.01)
root.gpuPerc = newGpuPerc;
if (Math.abs(root.gpuTemp - newGpuTemp) >= 1)
root.gpuTemp = newGpuTemp;
if (Math.abs(root.gpuMemUsed - newGpuMemUsed) >= 0.01)
root.gpuMemUsed = newGpuMemUsed;
}
}
}
Process {
id: gpuUsage
command: root.gpuType === "GENERIC" ? ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent"] : root.gpuType === "NVIDIA" ? ["nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu,memory.used", "--format=csv,noheader,nounits"] : ["echo"]
command: root.gpuType === "GENERIC" ? ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent"] : ["echo"]
stdout: StdioCollector {
onStreamFinished: {
console.log("this is running");
if (root.gpuType === "GENERIC") {
const percs = text.trim().split("\n");
const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0);
root.gpuPerc = sum / percs.length / 100;
} else if (root.gpuType === "NVIDIA") {
const [usage, temp, mem] = text.trim().split(",");
root.gpuPerc = parseInt(usage, 10) / 100;
root.gpuTemp = parseInt(temp, 10);
root.gpuMemUsed = parseInt(mem, 10) / root.gpuMemTotal;
} else {
root.gpuPerc = 0;
root.gpuTemp = 0;
@@ -314,7 +375,7 @@ Singleton {
// If AMD Tdie pattern failed, try fallback on Tctl
cpuTemp = text.match(/Tctl:\s+((\+|-)[0-9.]+)(°| )C/);
if (cpuTemp)
if (cpuTemp && Math.abs(parseFloat(cpuTemp[1]) - root.cpuTemp) >= 0.5)
root.cpuTemp = parseFloat(cpuTemp[1]);
if (root.gpuType !== "GENERIC")
+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 {
previewPath = path;
if (Config.general.color.schemeGeneration)
Quickshell.execDetached(["sh", "-c", `zshell-cli scheme generate --image-path ${previewPath} --scheme ${Config.colors.schemeType} --mode ${Config.general.color.mode}`]);
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${previewPath}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.general.color.mode}`]);
showPreview = true;
}
function setWallpaper(path: string): void {
actualCurrent = path;
WallpaperPath.currentWallpaperPath = path;
Quickshell.execDetached(["sh", "-c", `zshell-cli wallpaper lockscreen --input-image=${root.actualCurrent} --output-path=${Paths.state}/lockscreen_bg.png --blur-amount=${Config.lock.blurAmount}`]);
Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]);
}
function stopPreview(): void {
showPreview = false;
if (Config.general.color.schemeGeneration)
Quickshell.execDetached(["sh", "-c", `zshell-cli scheme generate --image-path ${root.actualCurrent} --scheme ${Config.colors.schemeType} --mode ${Config.general.color.mode}`]);
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.general.color.mode}`]);
}
extraOpts: useFuzzy ? ({}) : ({
+11 -17
View File
@@ -7,15 +7,16 @@ import qs.Modules
import qs.Config
import qs.Components
Item {
CustomRect {
id: root
property color barColor: DynamicColors.palette.m3primary
property color textColor: DynamicColors.palette.m3onSurface
anchors.bottom: parent.bottom
anchors.top: parent.top
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2
implicitWidth: 150
radius: Appearance.rounding.full
Behavior on implicitWidth {
NumberAnimation {
@@ -24,34 +25,27 @@ Item {
}
}
CustomRect {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2)
radius: height / 2
}
Component.onCompleted: console.log(root.height, root.implicitHeight)
RowLayout {
id: layout
anchors.fill: parent
anchors.left: parent.left
anchors.leftMargin: Appearance.padding.small
anchors.rightMargin: Appearance.padding.small * 2
anchors.verticalCenter: parent.verticalCenter
width: root.implicitWidth - Appearance.padding.small * 3
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
animate: true
color: Audio.muted ? DynamicColors.palette.m3error : root.textColor
font.pointSize: 14
font.pointSize: Appearance.font.size.larger
text: Audio.muted ? "volume_off" : "volume_up"
}
CustomRect {
Layout.fillWidth: true
color: "#50ffffff"
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
implicitHeight: 4
radius: 20
@@ -74,13 +68,13 @@ Item {
Layout.alignment: Qt.AlignVCenter
animate: true
color: (Audio.sourceMuted ?? false) ? DynamicColors.palette.m3error : root.textColor
font.pointSize: 14
font.pointSize: Appearance.font.size.larger
text: Audio.sourceMuted ? "mic_off" : "mic"
}
CustomRect {
Layout.fillWidth: true
color: "#50ffffff"
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
implicitHeight: 4
radius: 20
+1 -1
View File
@@ -9,7 +9,7 @@ ShapePath {
readonly property bool flatten: wrapper.height < rounding * 2
property real ibr: invertBottomRounding ? -1 : 1
required property bool invertBottomRounding
readonly property real rounding: 8
property real rounding: Appearance.rounding.smallest
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property Wrapper wrapper
+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.Network
RowLayout {
Item {
id: root
required property PanelWindow bar
readonly property int contentHeight: Config.barConfig.height + padding * 2
readonly property int exclusiveZone: Config.barConfig.autoHide ? Config.barConfig.border : contentHeight
property bool isHovered
readonly property int padding: Math.max(Appearance.padding.smaller, Config.barConfig.border)
required property Wrapper popouts
required property ShellScreen screen
readonly property bool shouldBeVisible: (!Config.barConfig.autoHide || visibilities.bar || isHovered)
readonly property int vPadding: 6
required property PersistentProperties visibilities
function checkPopout(x: real): void {
const ch = childAt(x, 2) as WrappedLoader;
if (!ch || ch?.id === "spacer") {
if (!popouts.currentName.startsWith("traymenu"))
popouts.hasCurrent = false;
return;
}
if (visibilities.sidebar || visibilities.dashboard || visibilities.resources)
return;
const id = ch.id;
const top = ch.x;
const item = ch.item;
const itemWidth = item.implicitWidth;
if (id === "audio" && Config.barConfig.popouts.audio) {
popouts.currentName = "audio";
popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x);
popouts.hasCurrent = true;
} else if (id === "network" && Config.barConfig.popouts.network) {
popouts.currentName = "network";
popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x);
popouts.hasCurrent = true;
} else if (id === "upower" && Config.barConfig.popouts.upower) {
popouts.currentName = "upower";
popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x);
popouts.hasCurrent = true;
}
content.item?.checkPopout(x);
}
implicitHeight: 34
implicitHeight: Config.barConfig.border
visible: height > Config.barConfig.border
CustomShortcut {
name: "toggle-overview"
states: State {
name: "visible"
when: root.shouldBeVisible
onPressed: {
Hyprland.refreshWorkspaces();
Hyprland.refreshMonitors();
if (root.popouts.hasCurrent && root.popouts.currentName === "overview") {
root.popouts.hasCurrent = false;
} else {
root.popouts.currentName = "overview";
root.popouts.currentCenter = root.width / 2;
root.popouts.hasCurrent = true;
}
PropertyChanges {
root.implicitHeight: root.contentHeight
}
}
transitions: [
Transition {
from: ""
to: "visible"
Repeater {
id: repeater
// model: Config.barConfig.entries.filted(n => n.index > 50).sort(n => n.index)
model: Config.barConfig.entries
DelegateChooser {
role: "id"
DelegateChoice {
roleValue: "spacer"
delegate: WrappedLoader {
Layout.fillWidth: true
}
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
property: "implicitHeight"
target: root
}
},
Transition {
from: "visible"
to: ""
DelegateChoice {
roleValue: "workspaces"
delegate: WrappedLoader {
sourceComponent: Workspaces {
bar: root.bar
}
}
}
DelegateChoice {
roleValue: "audio"
delegate: WrappedLoader {
sourceComponent: AudioWidget {
}
}
}
DelegateChoice {
roleValue: "tray"
delegate: WrappedLoader {
sourceComponent: TrayWidget {
bar: root.bar
loader: root
popouts: root.popouts
}
}
}
DelegateChoice {
roleValue: "resources"
delegate: WrappedLoader {
sourceComponent: Resources {
visibilities: root.visibilities
}
}
}
DelegateChoice {
roleValue: "updates"
delegate: WrappedLoader {
sourceComponent: UpdatesWidget {
}
}
}
DelegateChoice {
roleValue: "notifBell"
delegate: WrappedLoader {
sourceComponent: NotifBell {
popouts: root.popouts
visibilities: root.visibilities
}
}
}
DelegateChoice {
roleValue: "clock"
delegate: WrappedLoader {
sourceComponent: Clock {
loader: root
popouts: root.popouts
visibilities: root.visibilities
}
}
}
DelegateChoice {
roleValue: "activeWindow"
delegate: WrappedLoader {
sourceComponent: WindowTitle {
bar: root
monitor: Brightness.getMonitorForScreen(root.screen)
}
}
}
DelegateChoice {
roleValue: "upower"
delegate: WrappedLoader {
sourceComponent: UPowerWidget {
}
}
}
DelegateChoice {
roleValue: "network"
delegate: WrappedLoader {
sourceComponent: NetworkWidget {
}
}
}
DelegateChoice {
roleValue: "media"
delegate: WrappedLoader {
sourceComponent: MediaWidget {
}
}
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
property: "implicitHeight"
target: root
}
}
}
]
component WrappedLoader: Loader {
required property bool enabled
required property string id
required property int index
Loader {
id: content
function findFirstEnabled(): Item {
const count = repeater.count;
for (let i = 0; i < count; i++) {
const item = repeater.itemAt(i);
if (item?.enabled)
return item;
}
return null;
active: root.shouldBeVisible || root.visible
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
sourceComponent: Bar {
height: root.contentHeight
popouts: root.popouts
screen: root.screen
visibilities: root.visibilities
}
function findLastEnabled(): Item {
for (let i = repeater.count - 1; i >= 0; i--) {
const item = repeater.itemAt(i);
if (item?.enabled)
return item;
}
return null;
}
Layout.alignment: Qt.AlignVCenter
Layout.fillHeight: true
Layout.leftMargin: findFirstEnabled() === this ? root.vPadding : 0
Layout.rightMargin: findLastEnabled() === this ? root.vPadding : 0
active: enabled
visible: enabled
}
}
+7 -10
View File
@@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import QtQuick.Effects
import qs.Modules
import qs.Config
import qs.Components
@@ -17,7 +16,8 @@ Item {
CustomRect {
anchors.fill: parent
color: Config.barConfig.autoHide && !root.visibilities.bar ? "transparent" : DynamicColors.palette.m3surface
anchors.margins: -1
color: DynamicColors.palette.m3surface
layer.enabled: true
layer.effect: MultiEffect {
@@ -38,14 +38,11 @@ Item {
Rectangle {
anchors.fill: parent
anchors.topMargin: Config.barConfig.autoHide && !root.visibilities.bar ? 4 : root.bar.implicitHeight
topLeftRadius: 8
topRightRadius: 8
Behavior on anchors.topMargin {
Anim {
}
}
anchors.margins: Config.barConfig.border + 1
anchors.topMargin: root.bar.implicitHeight + 1
radius: Config.barConfig.border > 0 ? Config.barConfig.rounding : 0
topLeftRadius: Config.barConfig.rounding
topRightRadius: Config.barConfig.rounding
}
}
}
-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.Components
Item {
CustomRect {
id: root
required property RowLayout loader
required property Wrapper popouts
required property PersistentProperties visibilities
anchors.bottom: parent.bottom
anchors.top: parent.top
implicitWidth: timeText.contentWidth + 5 * 2
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2
implicitWidth: timeText.contentWidth + Appearance.padding.normal * 2
radius: Appearance.rounding.full
CustomRect {
anchors.bottomMargin: 3
anchors.fill: parent
anchors.topMargin: 3
color: "transparent"
radius: 4
CustomText {
id: timeText
CustomText {
id: timeText
anchors.centerIn: parent
color: DynamicColors.palette.m3onSurface
text: Time.dateStr
anchors.centerIn: parent
color: DynamicColors.palette.m3onSurface
text: Time.dateStr
Behavior on color {
CAnim {
}
}
}
StateLayer {
acceptedButtons: Qt.LeftButton
onClicked: {
root.visibilities.dashboard = !root.visibilities.dashboard;
Behavior on color {
CAnim {
}
}
}
StateLayer {
acceptedButtons: Qt.LeftButton
onClicked: {
root.visibilities.dashboard = !root.visibilities.dashboard;
}
}
}
+9 -9
View File
@@ -5,10 +5,10 @@ import Quickshell.Services.SystemTray
import QtQuick
import qs.Config
import qs.Components
import qs.Modules.Calendar
import qs.Modules.WSOverview
import qs.Modules.Network
import qs.Modules.UPower
import qs.Modules.Updates
Item {
id: root
@@ -69,14 +69,6 @@ Item {
}
}
Popout {
name: "calendar"
sourceComponent: CalendarPopup {
wrapper: root.wrapper
}
}
Popout {
name: "overview"
@@ -101,6 +93,14 @@ Item {
wrapper: root.wrapper
}
}
Popout {
name: "updates"
sourceComponent: UpdatesPopout {
wrapper: root.wrapper
}
}
}
component Popout: Loader {
+1 -1
View File
@@ -7,7 +7,7 @@ ShapePath {
id: root
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real rounding: 8
readonly property real rounding: Appearance.rounding.normal
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property Wrapper wrapper
+1 -1
View File
@@ -40,7 +40,7 @@ Item {
anchors.right: parent.right
anchors.top: parent.top
color: "transparent"
radius: 6
radius: Appearance.rounding.normal - anchors.margins
Item {
id: view
+13 -9
View File
@@ -72,17 +72,21 @@ Item {
}
}
Loader {
id: content
CustomClippingRect {
anchors.fill: parent
active: true
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
visible: false
Loader {
id: content
sourceComponent: Content {
state: root.dashState
visibilities: root.visibilities
active: true
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
visible: false
sourceComponent: Content {
state: root.dashState
visibilities: root.visibilities
}
}
}
}
@@ -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 QtQuick
import QtQml.Models
import qs.Modules.Dock.Parts
import qs.Components
import qs.Helpers
import qs.Config
@@ -9,20 +11,310 @@ import qs.Config
Item {
id: root
readonly property int dockContentWidth: TaskbarApps.apps.reduce((sum, app, i) => sum + (app.appId === TaskbarApps.separatorId ? 1 : Config.dock.height) + (i > 0 ? dockRow.spacing : 0), 0)
property bool dragActive: false
property real dragHeight: Config.dock.height
property real dragStartX: 0
property real dragStartY: 0
property real dragWidth: Config.dock.height
property real dragX: 0
property real dragY: 0
property string draggedAppId: ""
property var draggedModelData: null
property bool dropAnimating: false
readonly property int padding: Appearance.padding.small
required property var panels
property var pendingCommitIds: []
readonly property int rounding: Appearance.rounding.large
required property ShellScreen screen
required property PersistentProperties visibilities
property var visualIds: []
function beginVisualDrag(appId, modelData, item) {
const pos = item.mapToItem(root, 0, 0);
root.visualIds = TaskbarApps.apps.map(app => app.appId);
root.draggedAppId = appId;
root.draggedModelData = modelData;
root.dragWidth = item.width;
root.dragHeight = item.height;
root.dragStartX = pos.x;
root.dragStartY = pos.y;
root.dragX = pos.x;
root.dragY = pos.y;
root.dragActive = true;
root.dropAnimating = false;
root.pendingCommitIds = [];
}
function endVisualDrag() {
const ids = root.visualIds.slice();
const finalIndex = root.visualIds.indexOf(root.draggedAppId);
const finalItem = dockRow.itemAtIndex(finalIndex);
// Stop sending drag events now, but keep the proxy alive while it settles.
root.dragActive = false;
// In a dock, the destination delegate should normally be instantiated.
// If not, just finish immediately.
if (!finalItem) {
root.pendingCommitIds = ids;
root.finishVisualDrag();
return;
}
const pos = finalItem.mapToItem(root, 0, 0);
root.pendingCommitIds = ids;
root.dropAnimating = true;
settleX.to = pos.x;
settleY.to = pos.y;
settleAnim.start();
}
function finishVisualDrag() {
const ids = root.pendingCommitIds.slice();
root.dragActive = false;
root.dropAnimating = false;
root.draggedAppId = "";
root.draggedModelData = null;
root.visualIds = [];
root.pendingCommitIds = [];
TaskbarApps.commitVisualOrder(ids);
}
function moveArrayItem(list, from, to) {
const next = list.slice();
const [item] = next.splice(from, 1);
next.splice(to, 0, item);
return next;
}
function previewVisualMove(from, hovered, before) {
let to = hovered + (before ? 0 : 1);
if (to > from)
to -= 1;
to = Math.max(0, Math.min(visualModel.items.count - 1, to));
if (from === to)
return;
visualModel.items.move(from, to);
root.visualIds = moveArrayItem(root.visualIds, from, to);
}
implicitHeight: Config.dock.height + root.padding * 2
implicitWidth: dockRow.implicitWidth + root.padding * 2
implicitWidth: dockRow.contentWidth + root.padding * 2
RowLayout {
ParallelAnimation {
id: settleAnim
onFinished: root.finishVisualDrag()
Anim {
id: settleX
duration: Appearance.anim.durations.normal
property: "dragX"
target: root
}
Anim {
id: settleY
duration: Appearance.anim.durations.normal
property: "dragY"
target: root
}
}
Component {
id: dockDelegate
DropArea {
id: slot
readonly property string appId: modelData.appId
readonly property bool isSeparator: appId === TaskbarApps.separatorId
required property var modelData
function previewReorder(drag) {
const source = drag.source;
if (!source || !source.appId || source.appId === appId)
return;
const from = source.visualIndex;
const hovered = slot.DelegateModel.itemsIndex;
if (from < 0 || hovered < 0)
return;
root.previewVisualMove(from, hovered, drag.x < width / 2);
}
height: Config.dock.height
width: isSeparator ? 1 : Config.dock.height
ListView.onRemove: removeAnim.start()
onEntered: drag => previewReorder(drag)
onPositionChanged: drag => previewReorder(drag)
SequentialAnimation {
id: removeAnim
ScriptAction {
script: slot.ListView.delayRemove = true
}
ParallelAnimation {
Anim {
property: "opacity"
target: slot
to: 0
}
Anim {
property: "scale"
target: slot
to: 0.5
}
}
ScriptAction {
script: {
slot.ListView.delayRemove = false;
}
}
}
DockAppButton {
id: button
anchors.centerIn: parent
appListRoot: root
appToplevel: modelData
visibilities: root.visibilities
visible: root.draggedAppId !== slot.appId
}
DragHandler {
id: dragHandler
enabled: !slot.isSeparator
grabPermissions: PointerHandler.CanTakeOverFromAnything
target: null
xAxis.enabled: true
yAxis.enabled: false
onActiveChanged: {
if (active) {
root.beginVisualDrag(slot.appId, slot.modelData, button);
} else if (root.draggedAppId === slot.appId) {
dragProxy.Drag.drop();
root.endVisualDrag();
}
}
onActiveTranslationChanged: {
if (!active || root.draggedAppId !== slot.appId)
return;
root.dragX = root.dragStartX + activeTranslation.x;
root.dragY = root.dragStartY + activeTranslation.y;
}
}
}
}
DelegateModel {
id: visualModel
delegate: dockDelegate
model: ScriptModel {
objectProp: "appId"
values: TaskbarApps.apps
}
}
CustomListView {
id: dockRow
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
property bool enableAddAnimation: false
anchors.left: parent.left
anchors.margins: Appearance.padding.small
anchors.top: parent.top
spacing: Appearance.spacing.small
boundsBehavior: Flickable.StopAtBounds
height: Config.dock.height
implicitWidth: root.dockContentWidth + Config.dock.height
interactive: !(root.dragActive || root.dropAnimating)
model: visualModel
orientation: ListView.Horizontal
spacing: Appearance.padding.smaller
add: Transition {
ParallelAnimation {
Anim {
duration: dockRow.enableAddAnimation ? Appearance.anim.durations.normal : 0
from: 0
property: "opacity"
to: 1
}
Anim {
duration: dockRow.enableAddAnimation ? Appearance.anim.durations.normal : 0
from: 0.5
property: "scale"
to: 1
}
}
}
displaced: Transition {
Anim {
duration: Appearance.anim.durations.small
properties: "x,y"
}
}
move: Transition {
Anim {
duration: Appearance.anim.durations.small
properties: "x,y"
}
}
Component.onCompleted: {
Qt.callLater(() => enableAddAnimation = true);
}
}
Item {
id: dragProxy
property string appId: root.draggedAppId
property int visualIndex: root.visualIds.indexOf(root.draggedAppId)
Drag.active: root.dragActive
Drag.hotSpot.x: width / 2
Drag.hotSpot.y: height / 2
Drag.source: dragProxy
height: root.dragHeight
visible: (root.dragActive || root.dropAnimating) && !!root.draggedModelData
width: root.dragWidth
x: root.dragX
y: root.dragY
z: 9999
DockAppButton {
anchors.fill: parent
appListRoot: root
appToplevel: root.draggedModelData
enabled: false
visibilities: root.visibilities
}
}
}
+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
visible: height > 0
Behavior on implicitWidth {
Anim {
duration: Appearance.anim.durations.small
}
}
onShouldBeActiveChanged: {
if (shouldBeActive) {
timer.stop();
@@ -84,12 +90,13 @@ Item {
id: content
active: false
anchors.horizontalCenter: parent.horizontalCenter
anchors.left: parent.left
anchors.top: parent.top
visible: false
sourceComponent: Content {
panels: root.panels
screen: root.screen
visibilities: root.visibilities
Component.onCompleted: root.contentHeight = implicitHeight
+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.Helpers
Item {
CustomRect {
id: root
readonly property string currentMedia: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
readonly property int textWidth: Math.min(metrics.width, 200)
anchors.bottom: parent.bottom
anchors.top: parent.top
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2
implicitWidth: layout.implicitWidth + Appearance.padding.normal * 2
radius: Appearance.rounding.full
Behavior on implicitWidth {
Anim {
}
}
CustomRect {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2)
radius: Appearance.rounding.full
}
TextMetrics {
id: metrics
@@ -39,11 +31,7 @@ Item {
RowLayout {
id: layout
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.left: parent.left
anchors.leftMargin: Appearance.padding.normal
anchors.top: parent.top
anchors.centerIn: parent
Behavior on implicitWidth {
Anim {
@@ -53,7 +41,7 @@ Item {
MaterialIcon {
animate: true
color: Players.active?.isPlaying ? DynamicColors.palette.m3primary : DynamicColors.palette.m3onSurface
font.pointSize: 14
font.pointSize: Appearance.font.size.larger
text: Players.active?.isPlaying ? "music_note" : "music_off"
}
+23 -30
View File
@@ -5,46 +5,39 @@ import qs.Config
import qs.Helpers
import qs.Components
Item {
CustomRect {
id: root
required property Wrapper popouts
required property PersistentProperties visibilities
anchors.bottom: parent.bottom
anchors.top: parent.top
implicitWidth: 30
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2
implicitWidth: implicitHeight
radius: Appearance.rounding.full
CustomRect {
anchors.bottomMargin: 3
anchors.fill: parent
anchors.topMargin: 3
color: "transparent"
radius: 4
MaterialIcon {
id: notificationCenterIcon
MaterialIcon {
id: notificationCenterIcon
property color iconColor: DynamicColors.palette.m3onSurface
property color iconColor: DynamicColors.palette.m3onSurface
anchors.centerIn: parent
color: iconColor
font.family: "Material Symbols Rounded"
font.pointSize: Appearance.font.size.larger
text: HasNotifications.hasNotifications ? "\uf4fe" : "\ue7f4"
anchors.centerIn: parent
color: iconColor
font.family: "Material Symbols Rounded"
font.pointSize: 16
text: HasNotifications.hasNotifications ? "\uf4fe" : "\ue7f4"
Behavior on color {
CAnim {
}
}
}
StateLayer {
cursorShape: Qt.PointingHandCursor
onClicked: {
root.visibilities.sidebar = !root.visibilities.sidebar;
Behavior on color {
CAnim {
}
}
}
StateLayer {
cursorShape: Qt.PointingHandCursor
onClicked: {
root.visibilities.sidebar = !root.visibilities.sidebar;
}
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ ShapePath {
readonly property real utilsWidthDiff: panels.utilities.width - wrapper.width
required property Wrapper wrapper
fillColor: flatten ? "transparent" : DynamicColors.palette.m3surface
fillColor: DynamicColors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
+1 -1
View File
@@ -14,7 +14,7 @@ Item {
states: State {
name: "hidden"
when: root.visibilities.sidebar
when: root.visibilities.sidebar || root.visibilities.dashboard || (root.panels.popouts.hasCurrent && root.panels.popouts.currentName.startsWith("traymenu"))
PropertyChanges {
root.implicitHeight: 0
+18 -14
View File
@@ -113,22 +113,26 @@ Item {
}
}
Loader {
id: content
CustomClippingRect {
anchors.fill: parent
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
Loader {
id: content
sourceComponent: Content {
brightness: root.brightness
monitor: root.monitor
muted: root.muted
sourceMuted: root.sourceMuted
sourceVolume: root.sourceVolume
visibilities: root.visibilities
volume: root.volume
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
sourceComponent: Content {
brightness: root.brightness
monitor: root.monitor
muted: root.muted
sourceMuted: root.sourceMuted
sourceVolume: root.sourceVolume
visibilities: root.visibilities
volume: root.volume
}
Component.onCompleted: active = Qt.binding(() => root.shouldBeActive || root.visible)
}
Component.onCompleted: active = Qt.binding(() => root.shouldBeActive || root.visible)
}
}
-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.Config
Item {
RowLayout {
id: root
property color accentColor: warning ? DynamicColors.palette.m3error : mainColor
@@ -19,75 +19,48 @@ Item {
property bool warning: percentage * 100 >= warningThreshold
property int warningThreshold: 80
clip: true
height: implicitHeight
implicitHeight: 34
implicitWidth: 34
percentage: 0
visible: width > 0 && height > 0
width: implicitWidth
Behavior on animatedPercentage {
Anim {
duration: Appearance.anim.durations.large
}
}
Component.onCompleted: animatedPercentage = percentage
onPercentageChanged: animatedPercentage = percentage
onPercentageChanged: {
const next = percentage;
Canvas {
id: gaugeCanvas
anchors.centerIn: parent
height: width
width: Math.min(parent.width, parent.height)
Component.onCompleted: requestPaint()
onPaint: {
const ctx = getContext("2d");
ctx.reset();
const cx = width / 2;
const cy = (height / 2) + 1;
const radius = (Math.min(width, height) - 12) / 2;
const lineWidth = 3;
ctx.beginPath();
ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep);
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.strokeStyle = DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2);
ctx.stroke();
if (root.animatedPercentage > 0) {
ctx.beginPath();
ctx.arc(cx, cy, radius, root.arcStartAngle, root.arcStartAngle + root.arcSweep * root.animatedPercentage);
ctx.lineWidth = lineWidth;
ctx.lineCap = "round";
ctx.strokeStyle = root.accentColor;
ctx.stroke();
}
}
Connections {
function onAnimatedPercentageChanged() {
gaugeCanvas.requestPaint();
}
target: root
}
Connections {
function onPaletteChanged() {
gaugeCanvas.requestPaint();
}
target: DynamicColors
}
if (Math.abs(next - animatedPercentage) >= 0.05)
animatedPercentage = next;
}
MaterialIcon {
anchors.centerIn: parent
id: icon
color: DynamicColors.palette.m3onSurface
font.pointSize: 12
font.pointSize: Appearance.font.size.larger
text: root.icon
}
CustomClippingRect {
Layout.preferredHeight: root.height - Appearance.padding.small
Layout.preferredWidth: 4
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
radius: Appearance.rounding.full
CustomRect {
id: fill
anchors.fill: parent
antialiasing: false
color: root.mainColor
implicitHeight: Math.ceil(root.percentage * parent.height)
radius: Appearance.rounding.full
transform: Scale {
origin.y: fill.height
yScale: Math.max(0.001, root.animatedPercentage)
}
}
}
}
-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.Components
Item {
CustomRect {
id: root
required property PersistentProperties visibilities
anchors.bottom: parent.bottom
anchors.top: parent.top
clip: true
implicitWidth: rowLayout.implicitWidth + Appearance.padding.small * 2
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2
implicitWidth: rowLayout.implicitWidth + Appearance.padding.normal * 2
radius: height / 2
CustomRect {
id: backgroundRect
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: root.parent.height - ((Appearance.padding.small - 1) * 2)
radius: height / 2
anchors {
left: parent.left
right: parent.right
verticalCenter: parent.verticalCenter
}
StateLayer {
onClicked: root.visibilities.resources = !root.visibilities.resources
}
StateLayer {
onClicked: root.visibilities.resources = !root.visibilities.resources
}
RowLayout {
id: rowLayout
anchors.centerIn: parent
spacing: 0
implicitHeight: root.implicitHeight
spacing: Appearance.spacing.smaller
Ref {
service: SystemUsage
@@ -48,6 +36,7 @@ Item {
Resource {
Layout.alignment: Qt.AlignVCenter
Layout.fillHeight: true
icon: "memory"
mainColor: DynamicColors.palette.m3primary
percentage: SystemUsage.cpuPerc
@@ -55,6 +44,7 @@ Item {
}
Resource {
Layout.fillHeight: true
icon: "memory_alt"
mainColor: DynamicColors.palette.m3secondary
percentage: SystemUsage.memPerc
@@ -62,12 +52,14 @@ Item {
}
Resource {
Layout.fillHeight: true
icon: "gamepad"
mainColor: DynamicColors.palette.m3tertiary
percentage: SystemUsage.gpuPerc
}
Resource {
Layout.fillHeight: true
icon: "developer_board"
mainColor: DynamicColors.palette.m3primary
percentage: SystemUsage.gpuMemUsed
+1 -1
View File
@@ -7,7 +7,7 @@ ShapePath {
id: root
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real rounding: 8
readonly property real rounding: Appearance.rounding.large
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property Wrapper wrapper
+24 -14
View File
@@ -22,83 +22,92 @@ Item {
ListElement {
icon: "settings"
key: "general"
name: "General"
}
ListElement {
icon: "wallpaper"
key: "wallpaper"
name: "Wallpaper"
}
ListElement {
icon: "settop_component"
key: "bar"
name: "Bar"
}
ListElement {
icon: "lock"
key: "lockscreen"
name: "Lockscreen"
}
ListElement {
icon: "build_circle"
key: "services"
name: "Services"
}
ListElement {
icon: "notifications"
key: "notifications"
name: "Notifications"
}
ListElement {
icon: "view_sidebar"
key: "sidebar"
name: "Sidebar"
}
ListElement {
icon: "handyman"
key: "utilities"
name: "Utilities"
}
ListElement {
icon: "dashboard"
key: "dashboard"
name: "Dashboard"
}
ListElement {
icon: "colors"
key: "appearance"
name: "Appearance"
}
ListElement {
icon: "display_settings"
key: "osd"
name: "On screen display"
}
ListElement {
icon: "rocket_launch"
key: "launcher"
name: "Launcher"
}
ListElement {
icon: "colors"
name: "Colors"
}
}
CustomRect {
CustomClippingRect {
anchors.fill: parent
color: DynamicColors.tPalette.m3surfaceContainer
radius: 4
radius: Appearance.rounding.normal
CustomListView {
id: clayout
anchors.centerIn: parent
contentHeight: contentItem.childrenRect.height
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.margins: Appearance.padding.smaller
anchors.top: parent.top
boundsBehavior: Flickable.StopAtBounds
contentWidth: contentItem.childrenRect.width
highlightFollowsCurrentItem: false
implicitHeight: contentItem.childrenRect.height
implicitWidth: contentItem.childrenRect.width
model: listModel
spacing: 5
@@ -109,7 +118,7 @@ Item {
color: DynamicColors.palette.m3primary
implicitHeight: clayout.currentItem?.implicitHeight ?? 0
implicitWidth: clayout.width
radius: 4
radius: Appearance.rounding.normal - Appearance.padding.smaller
y: clayout.currentItem?.y ?? 0
Behavior on y {
@@ -127,11 +136,12 @@ Item {
required property string icon
required property int index
required property string key
required property string name
implicitHeight: 42
implicitWidth: 200
radius: 4
radius: Appearance.rounding.normal - Appearance.padding.smaller
RowLayout {
id: layout
@@ -148,7 +158,7 @@ Item {
Layout.fillHeight: true
Layout.preferredWidth: icon.contentWidth
color: categoryItem.index === clayout.currentIndex ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
font.pointSize: 22
font.pointSize: Appearance.font.size.small * 2
text: categoryItem.icon
verticalAlignment: Text.AlignVCenter
}
@@ -170,7 +180,7 @@ Item {
id: layer
onClicked: {
root.content.currentCategory = categoryItem.name.toLowerCase();
root.content.currentCategory = categoryItem.key;
clayout.currentIndex = categoryItem.index;
}
}
+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.Config
import qs.Helpers
CustomRect {
SettingsPage {
id: root
ColumnLayout {
id: clayout
SettingsSection {
SettingsHeader {
name: "Scale"
}
anchors.left: parent.left
anchors.right: parent.right
SettingSpinBox {
name: "Rounding scale"
object: Config.appearance.rounding
setting: "scale"
step: 0.1
}
CustomRect {
Layout.fillWidth: true
Layout.preferredHeight: colorLayout.implicitHeight
color: DynamicColors.tPalette.m3surfaceContainer
Separator {
}
ColumnLayout {
id: colorLayout
SettingSpinBox {
name: "Spacing scale"
object: Config.appearance.spacing
setting: "scale"
step: 0.1
}
anchors.left: parent.left
anchors.margins: Appearance.padding.large
anchors.right: parent.right
Separator {
}
Settings {
name: "smth"
}
SettingSpinBox {
name: "Padding scale"
object: Config.appearance.padding
setting: "scale"
step: 0.1
}
SettingSwitch {
name: "wallust"
object: Config.general.color
setting: "wallust"
}
Separator {
}
CustomSplitButtonRow {
enabled: true
label: qsTr("Scheme mode")
SettingSpinBox {
name: "Font size scale"
object: Config.appearance.font.size
setting: "scale"
step: 0.1
}
menuItems: [
MenuItem {
property string val: "light"
Separator {
}
icon: "light_mode"
text: qsTr("Light")
},
MenuItem {
property string val: "dark"
icon: "dark_mode"
text: qsTr("Dark")
}
]
Component.onCompleted: {
if (Config.general.color.mode === "light")
active = menuItems[0];
else
active = menuItems[1];
}
onSelected: item => {
Config.general.color.mode = item.val;
Config.save();
}
}
}
SettingSpinBox {
name: "Animation duration scale"
object: Config.appearance.anim.durations
setting: "scale"
step: 0.1
}
}
component Settings: CustomRect {
id: settingsItem
SettingsSection {
SettingsHeader {
name: "Fonts"
}
required property string name
SettingInput {
name: "Sans family"
object: Config.appearance.font.family
setting: "sans"
}
Layout.preferredHeight: 42
Layout.preferredWidth: 200
radius: 4
Separator {
}
CustomText {
id: text
SettingInput {
name: "Monospace family"
object: Config.appearance.font.family
setting: "mono"
}
anchors.left: parent.left
anchors.margins: Appearance.padding.smaller
anchors.right: parent.right
font.bold: true
font.pointSize: 32
text: settingsItem.name
verticalAlignment: Text.AlignVCenter
Separator {
}
SettingInput {
name: "Material family"
object: Config.appearance.font.family
setting: "material"
}
Separator {
}
SettingInput {
name: "Clock family"
object: Config.appearance.font.family
setting: "clock"
}
}
SettingsSection {
SettingsHeader {
name: "Animation"
}
SettingSpinBox {
name: "Media GIF speed adjustment"
object: Config.appearance.anim
setting: "mediaGifSpeedAdjustment"
step: 10
}
Separator {
}
SettingSpinBox {
name: "Session GIF speed"
max: 5
min: 0
object: Config.appearance.anim
setting: "sessionGifSpeed"
step: 0.1
}
}
SettingsSection {
SettingsHeader {
name: "Transparency"
}
SettingSwitch {
name: "Enable transparency"
object: Config.appearance.transparency
setting: "enabled"
}
Separator {
}
SettingSpinBox {
name: "Base opacity"
max: 1
min: 0
object: Config.appearance.transparency
setting: "base"
step: 0.05
}
Separator {
}
SettingSpinBox {
name: "Layer opacity"
max: 1
min: 0
object: Config.appearance.transparency
setting: "layers"
step: 0.05
}
}
}
+24 -8
View File
@@ -1,13 +1,29 @@
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Modules as Modules
import qs.Modules.Settings.Controls
import qs.Config
import qs.Helpers
CustomRect {
SettingsPage {
id: root
SettingsSection {
SettingsHeader {
name: "Wallpaper"
}
SettingSwitch {
name: "Enable wallpaper rendering"
object: Config.background
setting: "enabled"
}
Separator {
}
SettingSpinBox {
name: "Fade duration"
min: 0
object: Config.background
setting: "wallFadeDuration"
step: 50
}
}
}
+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 Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Modules as Modules
import qs.Modules.Settings.Controls
import qs.Config
import qs.Helpers
import qs.Components
CustomRect {
SettingsPage {
id: root
ColumnLayout {
id: clayout
anchors.fill: parent
Settings {
name: "apps"
function schemeTypeItem(items, value) {
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.value === value)
return item;
}
Item {
return items[0] ?? null;
}
SettingsSection {
SettingsHeader {
name: "General"
}
SettingInput {
name: "Logo"
object: Config.general
setting: "logo"
}
Separator {
}
SettingInput {
name: "Wallpaper path"
object: Config.general
setting: "wallpaperPath"
}
Separator {
}
SettingSwitch {
name: "Desktop icons"
object: Config.general
setting: "desktopIcons"
}
}
component Settings: CustomRect {
id: settingsItem
SettingsSection {
SettingsHeader {
name: "Color"
}
required property string name
CustomSplitButtonRow {
active: Config.general.color.mode === "light" ? menuItems[0] : menuItems[1]
label: qsTr("Scheme mode")
implicitHeight: 42
implicitWidth: 200
radius: 4
menuItems: [
MenuItem {
icon: "light_mode"
text: qsTr("Light")
value: "light"
},
MenuItem {
icon: "dark_mode"
text: qsTr("Dark")
value: "dark"
}
]
RowLayout {
id: layout
anchors.left: parent.left
anchors.margins: Appearance.padding.smaller
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
CustomText {
id: text
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.fillHeight: true
Layout.fillWidth: true
Layout.leftMargin: Appearance.spacing.normal
text: settingsItem.name
verticalAlignment: Text.AlignVCenter
onSelected: item => {
Config.general.color.mode = item.value;
Config.save();
}
}
Separator {
}
CustomSplitButtonRow {
id: schemeType
active: root.schemeTypeItem(menuItems, Config.colors.schemeType)
label: qsTr("Scheme type")
z: 2
menuItems: [
MenuItem {
icon: "palette"
text: qsTr("Vibrant")
value: "vibrant"
},
MenuItem {
icon: "gesture"
text: qsTr("Expressive")
value: "expressive"
},
MenuItem {
icon: "contrast"
text: qsTr("Monochrome")
value: "monochrome"
},
MenuItem {
icon: "tonality"
text: qsTr("Neutral")
value: "neutral"
},
MenuItem {
icon: "gradient"
text: qsTr("Tonal spot")
value: "tonalSpot"
},
MenuItem {
icon: "target"
text: qsTr("Fidelity")
value: "fidelity"
},
MenuItem {
icon: "article"
text: qsTr("Content")
value: "content"
},
MenuItem {
icon: "colors"
text: qsTr("Rainbow")
value: "rainbow"
},
MenuItem {
icon: "nutrition"
text: qsTr("Fruit salad")
value: "fruitSalad"
}
]
onSelected: item => {
Config.colors.schemeType = item.value;
Config.save();
}
}
Separator {
}
SettingSwitch {
name: "Automatic color scheme"
object: Config.general.color
setting: "schemeGeneration"
}
Separator {
}
SettingSwitch {
name: "Smart color scheme"
object: Config.general.color
setting: "smart"
}
Separator {
}
SettingSpinner {
name: "Schedule dark mode"
object: Config.general.color
settings: ["scheduleDarkStart", "scheduleDarkEnd"]
}
}
SettingsSection {
z: -1
SettingsHeader {
name: "Default Apps"
}
SettingStringList {
addLabel: qsTr("Add terminal command")
name: "Terminal"
object: Config.general.apps
setting: "terminal"
}
Separator {
}
SettingStringList {
addLabel: qsTr("Add audio command")
name: "Audio"
object: Config.general.apps
setting: "audio"
}
Separator {
}
SettingStringList {
addLabel: qsTr("Add playback command")
name: "Playback"
object: Config.general.apps
setting: "playback"
}
Separator {
}
SettingStringList {
addLabel: qsTr("Add explorer command")
name: "Explorer"
object: Config.general.apps
setting: "explorer"
}
}
}
+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
property string currentCategory: "general"
readonly property real nonAnimHeight: view.implicitHeight + viewWrapper.anchors.margins * 2
readonly property real nonAnimWidth: view.implicitWidth + 500 + viewWrapper.anchors.margins * 2
readonly property real nonAnimHeight: Math.floor(screen.height / 1.5) + viewWrapper.anchors.margins * 2
readonly property real nonAnimWidth: view.implicitWidth + Math.floor(screen.width / 2) + viewWrapper.anchors.margins * 2
required property ShellScreen screen
required property PersistentProperties visibilities
implicitHeight: nonAnimHeight
@@ -22,24 +23,41 @@ Item {
Connections {
function onCurrentCategoryChanged() {
stack.pop();
if (currentCategory === "general") {
if (currentCategory === "general")
stack.push(general);
} else if (currentCategory === "wallpaper") {
else if (currentCategory === "wallpaper")
stack.push(background);
} else if (currentCategory === "appearance") {
else if (currentCategory === "bar")
stack.push(bar);
else if (currentCategory === "appearance")
stack.push(appearance);
}
else if (currentCategory === "lockscreen")
stack.push(lockscreen);
else if (currentCategory === "services")
stack.push(services);
else if (currentCategory === "notifications")
stack.push(notifications);
else if (currentCategory === "sidebar")
stack.push(sidebar);
else if (currentCategory === "utilities")
stack.push(utilities);
else if (currentCategory === "dashboard")
stack.push(dashboard);
else if (currentCategory === "osd")
stack.push(osd);
else if (currentCategory === "launcher")
stack.push(launcher);
}
target: root
}
ClippingRectangle {
CustomClippingRect {
id: viewWrapper
anchors.fill: parent
anchors.margins: Appearance.padding.smaller
color: "transparent"
radius: Appearance.rounding.large - Appearance.padding.smaller
Item {
id: view
@@ -47,7 +65,6 @@ Item {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.top: parent.top
implicitHeight: layout.implicitHeight
implicitWidth: layout.implicitWidth
Categories {
@@ -67,7 +84,7 @@ Item {
anchors.right: parent.right
anchors.top: parent.top
color: DynamicColors.tPalette.m3surfaceContainer
radius: 4
radius: Appearance.rounding.normal
StackView {
id: stack
@@ -99,4 +116,67 @@ Item {
Cat.Appearance {
}
}
Component {
id: bar
Cat.Bar {
}
}
Component {
id: lockscreen
Cat.Lockscreen {
}
}
Component {
id: services
Cat.Services {
}
}
Component {
id: notifications
Cat.Notifications {
}
}
Component {
id: sidebar
Cat.Sidebar {
}
}
Component {
id: utilities
Cat.Utilities {
}
}
Component {
id: dashboard
Cat.Dashboard {
}
}
Component {
id: osd
Cat.Osd {
}
}
Component {
id: launcher
Cat.Launcher {
}
}
}
+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.Config
RowLayout {
Item {
id: root
required property string name
@@ -11,26 +11,35 @@ RowLayout {
required property string setting
Layout.fillWidth: true
Layout.preferredHeight: 42
Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2
CustomText {
id: text
RowLayout {
id: row
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
font.pointSize: 16
text: root.name
}
anchors.left: parent.left
anchors.margins: Appearance.padding.small
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
CustomSwitch {
id: cswitch
CustomText {
id: text
Layout.alignment: Qt.AlignRight
checked: root.object[root.setting]
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.fillWidth: true
font.pointSize: Appearance.font.size.larger
text: root.name
}
onToggled: {
root.object[root.setting] = checked;
Config.save();
CustomSwitch {
id: cswitch
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
checked: root.object[root.setting]
onToggled: {
root.object[root.setting] = checked;
Config.save();
}
}
}
}

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