Merge branch 'main' into module/wifi
C++ / fmt (pull_request) Successful in 8s
JS/TS / fmt (pull_request) Successful in 11s
JS/TS / lint (pull_request) Successful in 15s
Python / static (pull_request) Successful in 54s
Rust / fmt (pull_request) Successful in 30s
Rust / build (pull_request) Successful in 2m2s
C++ / build (pull_request) Successful in 4m0s
Rust / clippy (pull_request) Successful in 1m29s
Python / verify (pull_request) Successful in 3m13s
C++ / clang-tidy (pull_request) Successful in 5m7s

This commit is contained in:
2026-08-17 22:05:33 +02:00
32 changed files with 2387 additions and 664 deletions
+5 -2
View File
@@ -13,11 +13,13 @@ Slider {
property bool animateWave property bool animateWave
property color bgColor: enabled ? Colors.palette.m3secondaryContainer : Qt.alpha(Colors.palette.m3onSurface, 0.1) property color bgColor: enabled ? Colors.palette.m3secondaryContainer : Qt.alpha(Colors.palette.m3onSurface, 0.1)
readonly property bool dragging: mouse.pressed
property color fgColor: enabled ? Colors.palette.m3primary : Qt.alpha(Colors.palette.m3onSurface, 0.38) property color fgColor: enabled ? Colors.palette.m3primary : Qt.alpha(Colors.palette.m3onSurface, 0.38)
property real filledWidth property real filledWidth
property alias inset: inset property alias inset: inset
property color insetColor: enabled ? (inset.attached ? Colors.palette.m3onSecondaryContainer : Colors.palette.m3onPrimary) : Colors.palette.m3onSurface property color insetColor: enabled ? (inset.attached ? Colors.palette.m3onSecondaryContainer : Colors.palette.m3onPrimary) : Colors.palette.m3onSurface
property string insetIcon: "" property string insetIcon: ""
property bool interactionOnMove: true
property real pos: visualPosition property real pos: visualPosition
property int radius: Tokens.rounding.small property int radius: Tokens.rounding.small
property int waveDuration: 1000 property int waveDuration: 1000
@@ -139,7 +141,7 @@ Slider {
CAnim { CAnim {
} }
} }
Anim on waveProgress { NumberAnimation on waveProgress {
duration: root.waveDuration duration: root.waveDuration
easing.type: Easing.Linear easing.type: Easing.Linear
from: 0 from: 0
@@ -186,7 +188,8 @@ Slider {
onPositionChanged: e => { onPositionChanged: e => {
dragMovement = (e.x - pressStartX) / width; dragMovement = (e.x - pressStartX) / width;
root.interaction(posBinding.value); if (root.interactionOnMove)
root.interaction(posBinding.value);
} }
onPressed: e => { onPressed: e => {
widthBehavior.enabled = false; widthBehavior.enabled = false;
+4
View File
@@ -1,6 +1,7 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import Quickshell.Wayland
import QtQuick import QtQuick
import ZShell.Config import ZShell.Config
import qs.Components import qs.Components
@@ -15,6 +16,9 @@ Scope {
ExclusionZone { ExclusionZone {
id: top id: top
WlrLayershell.layer: WlrLayer.Overlay
anchors.left: true
anchors.right: true
anchors.top: true anchors.top: true
hasBar: root.geometry.barOnTop hasBar: root.geometry.barOnTop
} }
+17 -1
View File
@@ -162,6 +162,19 @@ Item {
} }
} }
Timer {
id: unhideTimer
interval: Config.bar.revealDelay
repeat: false
running: false
onTriggered: {
if (hoverHandler.hovered)
root.bar.isHovered = true;
}
}
HoverHandler { HoverHandler {
id: hoverHandler id: hoverHandler
@@ -178,7 +191,7 @@ Item {
root.popouts.hasCurrent = false; root.popouts.hasCurrent = false;
} }
if (Config.bar.autoHide) if (Config.bar.autoHide || root.bar.fullscreen)
root.bar.isHovered = false; root.bar.isHovered = false;
} }
} }
@@ -200,6 +213,9 @@ Item {
if (!root.visibilities.bar && Config.bar.autoHide && root.geometry.barContains(x, y, true)) if (!root.visibilities.bar && Config.bar.autoHide && root.geometry.barContains(x, y, true))
root.bar.isHovered = true; root.bar.isHovered = true;
if (root.bar.fullscreen && !root.bar.isHovered)
unhideTimer.start();
if (root.panels.sidebar.offsetScale === 1) { if (root.panels.sidebar.offsetScale === 1) {
const showOsd = root.inRightPanel(root.panels.osdWrapper, x, y); const showOsd = root.inRightPanel(root.panels.osdWrapper, x, y);
+8 -2
View File
@@ -52,7 +52,7 @@ CustomWindow {
readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen) readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen)
property var root: Quickshell.shellDir property var root: Quickshell.shellDir
readonly property real sdfBorderOffset: 2 * fsTransitionProg readonly property real sdfBorderOffset: 2 * fsTransitionProg
readonly property real shadowOpacity: 0.7 * (1 - fsTransitionProg) readonly property real shadowOpacity: 0.7 * (1 - (bar.isHovered ? 0 : fsTransitionProg))
property color surfaceColor: Colors.tPalette.m3surface property color surfaceColor: Colors.tPalette.m3surface
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
@@ -60,7 +60,7 @@ CustomWindow {
WlrLayershell.layer: (fsTransitionProg > 0 && Config.general.showOverFullscreen) || (hasSpecialWorkspace && hasFullscreenOnNormalWs) ? WlrLayer.Overlay : WlrLayer.Top WlrLayershell.layer: (fsTransitionProg > 0 && Config.general.showOverFullscreen) || (hasSpecialWorkspace && hasFullscreenOnNormalWs) ? WlrLayer.Overlay : WlrLayer.Top
color: "transparent" color: "transparent"
contentItem.focus: true contentItem.focus: true
mask: visibilities.isDrawing ? null : (hasFullscreen ? emptyRegion : regions) mask: visibilities.isDrawing ? null : (hasFullscreen && !bar.isHovered ? emptyRegion : regions)
name: "Bar" name: "Bar"
Behavior on fsTransitionProg { Behavior on fsTransitionProg {
@@ -119,6 +119,12 @@ CustomWindow {
x: root.width - width x: root.width - width
y: panels.osdWrapper.y + bar.implicitHeight y: panels.osdWrapper.y + bar.implicitHeight
} }
Region {
height: geometry.horizontal ? 1 : root.screen.height
width: geometry.horizontal ? root.screen.width : 1
y: geometry.position === "bottom" ? root.screen.height - 1 : 0
}
} }
Regions { Regions {
+1 -2
View File
@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import QtQuick.Controls
import ZShell.Config import ZShell.Config
import qs.Services import qs.Services
@@ -11,6 +10,6 @@ TextInput {
font { font {
family: Config.appearance?.font.family.sans ?? "sans-serif" family: Config.appearance?.font.family.sans ?? "sans-serif"
hintingPreference: Font.PreferFullHinting hintingPreference: Font.PreferFullHinting
pixelSize: Config.appearance?.font.size.normal ?? 15 pixelSize: Tokens?.font.size.normal ?? 15
} }
} }
+4 -1
View File
@@ -17,7 +17,10 @@ Singleton {
watchChanges: true watchChanges: true
onAdapterUpdated: writeAdapter() onAdapterUpdated: writeAdapter()
onFileChanged: reload() onFileChanged: {
reload();
Wallpapers.previewColorLock = false;
}
JsonAdapter { JsonAdapter {
id: adapter id: adapter
+28 -6
View File
@@ -6,7 +6,7 @@ import Quickshell.Io
import QtQuick import QtQuick
import ZShell.Models import ZShell.Models
import ZShell.Config import ZShell.Config
import qs.Modules import qs.Services
import qs.Helpers import qs.Helpers
import qs.Paths import qs.Paths
@@ -17,6 +17,8 @@ Searcher {
property alias crops: adapter.monitorCrops property alias crops: adapter.monitorCrops
readonly property string current: showPreview ? previewPath : actualCurrent readonly property string current: showPreview ? previewPath : actualCurrent
property alias monitorCrops: monitorCrops property alias monitorCrops: monitorCrops
property bool pendingPreviewClear
property bool previewColorLock
property string previewPath property string previewPath
property bool recentlyChanged property bool recentlyChanged
property bool showPreview: false property bool showPreview: false
@@ -34,9 +36,10 @@ Searcher {
function preview(path: string): void { function preview(path: string): void {
previewPath = path; previewPath = path;
if (Config.general.color.schemeGeneration)
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${previewPath}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.general.color.mode}`]);
showPreview = true; showPreview = true;
if (Config.general.color.schemeGeneration)
previewColorsProc.running = true;
} }
function setCrop(screen: string, rect: rect, zoom: real): void { function setCrop(screen: string, rect: rect, zoom: real): void {
@@ -52,7 +55,6 @@ Searcher {
height: rect.height, height: rect.height,
zoom: zoom zoom: zoom
}; };
// root.crops = updated;
} }
function setWallpaper(path: string): void { function setWallpaper(path: string): void {
@@ -66,8 +68,10 @@ Searcher {
function stopPreview(): void { function stopPreview(): void {
showPreview = false; showPreview = false;
if (Config.general.color.schemeGeneration) if (previewColorLock)
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.general.color.mode}`]); pendingPreviewClear = true;
else
Colors.showPreview = false;
} }
extraOpts: useFuzzy ? ({}) : ({ extraOpts: useFuzzy ? ({}) : ({
@@ -77,6 +81,11 @@ Searcher {
list: wallpapers.entries list: wallpapers.entries
useFuzzy: true useFuzzy: true
onPreviewColorLockChanged: {
if (!previewColorLock && pendingPreviewClear)
Colors.showPreview = false;
}
IpcHandler { IpcHandler {
function set(path: string): void { function set(path: string): void {
root.setWallpaper(path); root.setWallpaper(path);
@@ -118,4 +127,17 @@ Searcher {
path: Config.general.wallpaperPath path: Config.general.wallpaperPath
recursive: true recursive: true
} }
Process {
id: previewColorsProc
command: ["zshell-cli", "scheme", "generate", "-p", "-i", root.previewPath]
stdout: StdioCollector {
onStreamFinished: {
Colors.load(text, true);
Colors.showPreview = true;
}
}
}
} }
+11 -9
View File
@@ -12,6 +12,7 @@ GridLayout {
required property bool fullscreen required property bool fullscreen
required property bool horizontal required property bool horizontal
required property bool isHovered
required property Wrapper popouts required property Wrapper popouts
required property ClipWrapper popoutsWrapper required property ClipWrapper popoutsWrapper
required property ShellScreen screen required property ShellScreen screen
@@ -92,7 +93,7 @@ GridLayout {
delegate: EntryWrapper { delegate: EntryWrapper {
HyprsunsetWidget { HyprsunsetWidget {
horizontal: root.horizontal horizontal: root.horizontal
visible: !root.fullscreen visible: !root.fullscreen || root.isHovered
} }
} }
} }
@@ -113,7 +114,7 @@ GridLayout {
Workspaces { Workspaces {
horizontal: root.horizontal horizontal: root.horizontal
screen: root.screen screen: root.screen
visible: !root.fullscreen visible: !root.fullscreen || root.isHovered
} }
} }
} }
@@ -126,7 +127,7 @@ GridLayout {
horizontal: root.horizontal horizontal: root.horizontal
loader: root loader: root
popouts: root.popouts popouts: root.popouts
visible: !root.fullscreen visible: !root.fullscreen || root.isHovered
} }
} }
} }
@@ -137,6 +138,7 @@ GridLayout {
delegate: EntryWrapper { delegate: EntryWrapper {
StatusIcons { StatusIcons {
horizontal: root.horizontal horizontal: root.horizontal
visible: !root.fullscreen || root.isHovered
} }
} }
} }
@@ -148,7 +150,7 @@ GridLayout {
Resources { Resources {
horizontal: root.horizontal horizontal: root.horizontal
visibilities: root.visibilities visibilities: root.visibilities
visible: !root.fullscreen visible: !root.fullscreen || root.isHovered
} }
} }
} }
@@ -159,7 +161,7 @@ GridLayout {
delegate: EntryWrapper { delegate: EntryWrapper {
UpdatesWidget { UpdatesWidget {
horizontal: root.horizontal horizontal: root.horizontal
visible: !root.fullscreen visible: !root.fullscreen || root.isHovered
} }
} }
} }
@@ -172,7 +174,7 @@ GridLayout {
horizontal: root.horizontal horizontal: root.horizontal
popouts: root.popouts popouts: root.popouts
visibilities: root.visibilities visibilities: root.visibilities
visible: !root.fullscreen visible: !root.fullscreen || root.isHovered
} }
} }
} }
@@ -186,7 +188,7 @@ GridLayout {
loader: root loader: root
popouts: root.popouts popouts: root.popouts
visibilities: root.visibilities visibilities: root.visibilities
visible: !root.fullscreen visible: !root.fullscreen || root.isHovered
} }
} }
} }
@@ -198,7 +200,7 @@ GridLayout {
WindowTitle { WindowTitle {
bar: root bar: root
horizontal: root.horizontal horizontal: root.horizontal
visible: !root.fullscreen visible: !root.fullscreen || root.isHovered
} }
} }
} }
@@ -209,7 +211,7 @@ GridLayout {
delegate: EntryWrapper { delegate: EntryWrapper {
MediaWidget { MediaWidget {
horizontal: root.horizontal horizontal: root.horizontal
visible: !root.fullscreen visible: !root.fullscreen || root.isHovered
} }
} }
} }
+3 -1
View File
@@ -21,7 +21,7 @@ Item {
required property ClipWrapper popoutsWrapper required property ClipWrapper popoutsWrapper
required property string position required property string position
required property ShellScreen screen required property ShellScreen screen
readonly property bool shouldBeVisible: !fullscreen && (!Config.bar.autoHide || visibilities.bar || isHovered) readonly property bool shouldBeVisible: (!fullscreen && (!Config.bar.autoHide || visibilities.bar)) || isHovered
readonly property int vPadding: 6 readonly property int vPadding: 6
required property PersistentProperties visibilities required property PersistentProperties visibilities
@@ -79,7 +79,9 @@ Item {
sourceComponent: Bar { sourceComponent: Bar {
fullscreen: root.fullscreen fullscreen: root.fullscreen
height: root.contentThickness
horizontal: root.horizontal horizontal: root.horizontal
isHovered: root.isHovered
popouts: root.popouts popouts: root.popouts
popoutsWrapper: root.popoutsWrapper popoutsWrapper: root.popoutsWrapper
screen: root.screen screen: root.screen
+4 -4
View File
@@ -57,7 +57,7 @@ WidgetBase {
anchors.centerIn: parent anchors.centerIn: parent
color: root.contentColor color: root.contentColor
font: Config.appearance.font.family.mono // qmllint disable incompatible-type font.family: Config.appearance.font.family.mono // qmllint disable incompatible-type
height: implicitHeight height: implicitHeight
text: Time.dateStr text: Time.dateStr
visible: root.horizontal visible: root.horizontal
@@ -84,7 +84,7 @@ WidgetBase {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
color: root.contentColor color: root.contentColor
font: Config.appearance.font.family.mono // qmllint disable incompatible-type font.family: Config.appearance.font.family.mono // qmllint disable incompatible-type
text: Qt.formatDateTime(Time.date, modelData) text: Qt.formatDateTime(Time.date, modelData)
Behavior on color { Behavior on color {
@@ -114,7 +114,7 @@ WidgetBase {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
color: root.contentColor color: root.contentColor
font: Config.appearance.font.family.mono // qmllint disable incompatible-type font.family: Config.appearance.font.family.mono // qmllint disable incompatible-type
text: { text: {
if (modelData.includes("h")) if (modelData.includes("h"))
return Time.hourStr; return Time.hourStr;
@@ -135,7 +135,7 @@ WidgetBase {
CustomText { CustomText {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
color: root.contentColor color: root.contentColor
font: Config.appearance.font.family.mono // qmllint disable incompatible-type font.family: Config.appearance.font.family.mono // qmllint disable incompatible-type
text: Qt.formatDateTime(Time.date, "AP") text: Qt.formatDateTime(Time.date, "AP")
visible: Config.services.useTwelveHourClock && root.formatParts.timeTokens.length > 0 visible: Config.services.useTwelveHourClock && root.formatParts.timeTokens.length > 0
+1 -1
View File
@@ -41,7 +41,7 @@ Item {
Item { Item {
id: view id: view
readonly property int currentIndex: root.state.currentTab readonly property int currentIndex: root.dashState?.currentTab
readonly property Item currentItem: row.children[currentIndex] readonly property Item currentItem: row.children[currentIndex]
anchors.fill: parent anchors.fill: parent
+140 -147
View File
@@ -1,10 +1,12 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import Quickshell.Services.Mpris
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Shapes import QtQuick.Shapes
import QtQuick.Effects import QtQuick.Effects
import ZShell.Components
import ZShell.Services import ZShell.Services
import ZShell.Config import ZShell.Config
import qs.Daemons import qs.Daemons
@@ -14,7 +16,7 @@ import qs.Services
Item { Item {
id: root id: root
readonly property bool hasUnknownLength: (Players.active?.length ?? 0) > 2147483647
property real playerProgress: { property real playerProgress: {
const active = Players.active; const active = Players.active;
return active?.length ? (active.position % active.length) / active.length : 0; return active?.length ? (active.position % active.length) / active.length : 0;
@@ -23,7 +25,19 @@ Item {
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
implicitHeight: cover.height + rowHeight * 2 implicitHeight: layout.implicitHeight
function lengthStr(length: int): string {
if (length < 0)
return "-1:-1";
const hours = Math.floor(length / 3600);
const mins = Math.floor((length % 3600) / 60);
const secs = Math.floor(length % 60).toString().padStart(2, "0");
if (hours > 0)
return `${hours}:${mins.toString().padStart(2, "0")}:${secs}`;
return `${mins}:${secs}`;
}
Behavior on playerProgress { Behavior on playerProgress {
Anim { Anim {
@@ -32,11 +46,10 @@ Item {
} }
Timer { Timer {
interval: Config.dashboard.mediaUpdateInterval
repeat: true
running: Players.active?.isPlaying ?? false running: Players.active?.isPlaying ?? false
interval: Config.dashboard.mediaUpdateInterval
triggeredOnStart: true triggeredOnStart: true
repeat: true
onTriggered: Players.active?.positionChanged() onTriggered: Players.active?.positionChanged()
} }
@@ -72,7 +85,7 @@ Item {
const n = values.length; const n = values.length;
if (n < 2) if (n < 2)
return ""; return "";
const h = layout.height + shape.strokeWidth / 2; const h = layout.height + shape.strokeWidth / 2;
const w = layout.width + shape.strokeWidth; const w = layout.width + shape.strokeWidth;
@@ -126,58 +139,11 @@ Item {
} }
} }
Shape {
preferredRendererType: Shape.CurveRenderer
ShapePath {
capStyle: Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
fillColor: "transparent"
strokeColor: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
strokeWidth: Config.dashboard.sizes.mediaProgressThickness
Behavior on strokeColor {
CAnim {
}
}
PathAngleArc {
centerX: cover.x + cover.width / 2
centerY: cover.y + cover.height / 2
radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Tokens.spacing.small
radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Tokens.spacing.small
startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2
sweepAngle: Config.dashboard.sizes.mediaProgressSweep
}
}
ShapePath {
capStyle: Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap
fillColor: "transparent"
strokeColor: Colors.palette.m3primary
strokeWidth: Config.dashboard.sizes.mediaProgressThickness
Behavior on strokeColor {
CAnim {
}
}
PathAngleArc {
centerX: cover.x + cover.width / 2
centerY: cover.y + cover.height / 2
radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Tokens.spacing.small
radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Tokens.spacing.small
startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2
sweepAngle: Config.dashboard.sizes.mediaProgressSweep * root.playerProgress
}
}
}
RowLayout { RowLayout {
id: layout id: layout
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
implicitHeight: root.implicitHeight
CustomClippingRect { CustomClippingRect {
id: cover id: cover
@@ -211,147 +177,174 @@ Item {
} }
} }
CustomRect { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
Layout.leftMargin: Tokens.spacing.large
Layout.rightMargin: Tokens.spacing.large
Layout.preferredHeight: childrenRect.height Layout.preferredHeight: childrenRect.height
spacing: Tokens.spacing.normal
MarqueeText { MarqueeText {
id: title id: title
anchors.horizontalCenter: parent.horizontalCenter Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
anchors.top: parent.top
color: Colors.palette.m3primary color: Colors.palette.m3primary
font.pointSize: Tokens.font.size.normal font.pointSize: Tokens.font.size.normal
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
pauseMs: 4000 pauseMs: 4000
text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title") text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
width: parent.width - Tokens.padding.large * 4 implicitWidth: parent.width - Tokens.padding.large * 4
} }
MarqueeText { MarqueeText {
id: album id: album
anchors.horizontalCenter: parent.horizontalCenter Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
anchors.top: title.bottom
anchors.topMargin: Tokens.spacing.small
color: Colors.palette.m3outline color: Colors.palette.m3outline
font.pointSize: Tokens.font.size.small font.pointSize: Tokens.font.size.small
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
pauseMs: 4000 pauseMs: 4000
text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album") text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album")
width: parent.width - Tokens.padding.large * 4 implicitWidth: parent.width - Tokens.padding.large * 4
} }
MarqueeText { MarqueeText {
id: artist id: artist
anchors.horizontalCenter: parent.horizontalCenter Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
anchors.top: album.bottom
anchors.topMargin: Tokens.spacing.small
color: Colors.palette.m3secondary color: Colors.palette.m3secondary
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
pauseMs: 4000 pauseMs: 4000
text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist") text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist")
width: parent.width - Tokens.padding.large * 4 implicitWidth: parent.width - Tokens.padding.large * 4
} }
RowLayout { RowLayout {
id: controls id: positionSliderLayout
Layout.topMargin: Tokens.spacing.extraLargeIncreased
anchors.horizontalCenter: parent.horizontalCenter Layout.fillWidth: true
anchors.top: artist.bottom
anchors.topMargin: Tokens.spacing.smaller
spacing: Tokens.spacing.small spacing: Tokens.spacing.small
Control { TextMetrics {
function onClicked(): void { id: timeMetrics
Players.active?.previous();
}
canUse: Players.active?.canGoPrevious ?? false text: Players.active ? root.lengthStr(Math.max(Players.active.position, root.hasUnknownLength ? 0 : Players.active.length)).replace(/[1-9]/g, "0") : "00:00"
}
CustomText {
id: positionLabel
Layout.preferredWidth: timeMetrics.width
text: root.lengthStr(Players.active?.position ?? -1)
color: Colors.palette.m3onSurfaceVariant
font: timeMetrics.font
horizontalAlignment: Text.AlignHCenter
}
CustomSlider {
id: positionSlider
Layout.fillWidth: true
value: Players.active ? Players.active.position / (Players.active.length || 1) : 0
enabled: (Players.active?.canSeek ?? false) && !root.hasUnknownLength
wavy: true
animateWave: Players.active?.isPlaying ?? false
waveFrequency: 5
waveDuration: 2000
interactionOnMove: false
onInteraction: value => {
const active = Players.active;
if (active?.canSeek && active?.positionSupported)
active.position = value * active.length;
}
Binding {
target: positionLabel
property: "text"
value: root.lengthStr(positionSlider.pos * (Players.active?.length ?? 0))
when: positionSlider.dragging
}
}
CustomText {
Layout.preferredWidth: timeMetrics.width
text: root.hasUnknownLength ? "--:--" : root.lengthStr(Players.active?.length ?? -1)
color: Colors.palette.m3onSurfaceVariant
font: timeMetrics.font
horizontalAlignment: Text.AlignHCenter
}
}
ButtonRow {
Layout.topMargin: Tokens.spacing.largeIncreased
Layout.fillWidth: true
spacing: Tokens.spacing.extraSmall
Layout.leftMargin: positionSliderLayout.implicitWidth / 4
Layout.rightMargin: positionSliderLayout.implicitWidth / 4
IconButton {
type: IconButton.Tonal
icon: "shuffle"
isRound: true
shapeMorph: true
checked: Players.active?.shuffle ?? false
enabled: Players.active?.shuffleSupported
onClicked: Players.active.shuffle = !Players.active?.shuffle
implicitWidth: Math.round(implicitHeight * 0.9)
}
IconButton {
id: previousBtn
type: IconButton.Tonal
icon: "skip_previous" icon: "skip_previous"
isRound: true
shapeMorph: true
enabled: Players.active?.canGoPrevious
onClicked: Players.active?.previous()
} }
Control { IconButton {
function onClicked(): void { id: playPauseBtn
Players.active?.togglePlaying();
}
canUse: Players.active?.canTogglePlaying ?? false
icon: Players.active?.isPlaying ? "pause" : "play_arrow" icon: Players.active?.isPlaying ? "pause" : "play_arrow"
isRound: true
shapeMorph: true
fillWidth: true
checked: Players.active?.isPlaying ?? false
enabled: Players.active?.canTogglePlaying
onClicked: Players.active?.togglePlaying()
} }
Control { IconButton {
function onClicked(): void { id: nextBtn
Players.active?.next();
}
canUse: Players.active?.canGoNext ?? false type: IconButton.Tonal
icon: "skip_next" icon: "skip_next"
isRound: true
shapeMorph: true
enabled: Players.active?.canGoNext
onClicked: Players.active?.next()
}
IconButton {
type: IconButton.Tonal
icon: Players.active?.loopState === MprisLoopState.Track ? "repeat_one" : "repeat"
isRound: true
shapeMorph: true
checked: Players.active?.loopState === MprisLoopState.Track || Players.active?.loopState === MprisLoopState.Playlist
enabled: Players.active?.loopSupported
onClicked: {
const state = Players.active.loopState;
if (state === MprisLoopState.None)
Players.active.loopState = MprisLoopState.Track;
else if (state === MprisLoopState.Track)
Players.active.loopState = MprisLoopState.Playlist;
else
Players.active.loopState = MprisLoopState.None;
}
implicitWidth: Math.round(implicitHeight * 0.9)
} }
} }
} }
} }
component Control: CustomRect {
id: control
required property bool canUse
required property string icon
property int level: 1
property string set_color: "Secondary"
function onClicked(): void {
}
Layout.preferredWidth: implicitWidth + (controlState.pressed ? Tokens.padding.normal * 2 : 0)
color: canUse ? Colors.palette[`m3${set_color.toLowerCase()}`] : Colors.palette[`m3${set_color.toLowerCase()}Container`]
implicitHeight: implicitWidth
implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Tokens.padding.small
radius: Tokens.rounding.full
Behavior on Layout.preferredWidth {
Anim {
duration: Tokens.anim.durations.expressiveFastSpatial
easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial
}
}
Behavior on radius {
Anim {
duration: Tokens.anim.durations.expressiveFastSpatial
easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial
}
}
Elevation {
anchors.fill: parent
level: controlState.containsMouse && !controlState.pressed ? control.level + 1 : control.level
radius: parent.radius
z: -1
}
StateLayer {
id: controlState
color: control.canUse ? Colors.palette[`m3on${control.set_color}`] : Colors.palette[`m3on${control.set_color}Container`]
enabled: control.canUse
onClicked: {
control.onClicked();
}
// radius: Tokens.rounding.full
}
MaterialIcon {
id: icon
anchors.centerIn: parent
anchors.verticalCenterOffset: font.pointSize * 0.05
animate: true
color: control.canUse ? Colors.palette[`m3on${control.set_color}`] : Colors.palette[`m3on${control.set_color}Container`]
fill: control.canUse ? 1 : 0
font.pointSize: Tokens.font.size.large
text: control.icon
}
}
} }
+81 -30
View File
@@ -1,28 +1,36 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import Quickshell
import QtQuick import QtQuick
import qs.Modules.Launcher.Services import Quickshell
import qs.Modules.Launcher.Items
import qs.Components
import ZShell.Config import ZShell.Config
import qs.Components
import qs.Services import qs.Services
import qs.Modules.Launcher.Items
import qs.Modules.Launcher.Services
CustomListView { CustomListView {
id: root id: root
readonly property string displayState: stateForText(displayText)
property string displayText
readonly property string requestedState: stateForText(search.text)
required property SearchBar search required property SearchBar search
required property PersistentProperties visibilities required property PersistentProperties visibilities
highlightFollowsCurrentItem: false function resultsForText(text: string): var {
highlightRangeMode: ListView.ApplyRange switch (stateForText(text)) {
implicitHeight: (Config.launcher.sizes.itemHeight + spacing) * Math.min(Config.launcher.maxAppsShown, count) - spacing case "actions":
orientation: Qt.Vertical return Actions.query(text);
preferredHighlightBegin: 0 case "calc":
preferredHighlightEnd: height return [0];
spacing: Tokens.spacing.small case "variant":
state: { return SchemeVariants.query(text);
const text = search.text; default:
return Apps.search(text);
}
}
function stateForText(text: string): string {
const prefix = Config.launcher.actionPrefix; const prefix = Config.launcher.actionPrefix;
if (text.startsWith(prefix)) { if (text.startsWith(prefix)) {
for (const action of ["calc", "scheme", "variant"]) for (const action of ["calc", "scheme", "variant"])
@@ -34,6 +42,20 @@ CustomListView {
return "apps"; return "apps";
} }
function syncDisplayText(): void {
if (visibilities.launcher && requestedState === displayState)
displayText = search.text;
}
highlightFollowsCurrentItem: false
highlightRangeMode: ListView.ApplyRange
implicitHeight: (Config.launcher.sizes.itemHeight + spacing) * Math.min(Config.launcher.maxAppsShown, count) - spacing
orientation: Qt.Vertical
preferredHighlightBegin: 0
preferredHighlightEnd: height
spacing: Tokens.spacing.small
state: visibilities.launcher ? requestedState : displayState
verticalLayoutDirection: ListView.BottomToTop verticalLayoutDirection: ListView.BottomToTop
CustomScrollBar.vertical: CustomScrollBar { CustomScrollBar.vertical: CustomScrollBar {
@@ -44,19 +66,21 @@ CustomListView {
Anim { Anim {
from: 0 from: 0
properties: "opacity,scale" property: "opacity"
to: 1 to: 1
type: Anim.DefaultEffects
} }
} }
addDisplaced: Transition { addDisplaced: Transition {
Anim { Anim {
duration: Tokens.anim.durations.small
property: "y" property: "y"
type: Anim.StandardSmall
} }
Anim { Anim {
properties: "opacity,scale" property: "opacity"
to: 1 to: 1
type: Anim.DefaultEffects
} }
} }
displaced: Transition { displaced: Transition {
@@ -65,8 +89,9 @@ CustomListView {
} }
Anim { Anim {
properties: "opacity,scale" property: "opacity"
to: 1 to: 1
type: Anim.DefaultEffects
} }
} }
highlight: CustomRect { highlight: CustomRect {
@@ -85,7 +110,7 @@ CustomListView {
} }
} }
model: ScriptModel { model: ScriptModel {
id: model values: root.resultsForText(root.displayText)
onValuesChanged: root.currentIndex = 0 onValuesChanged: root.currentIndex = 0
} }
@@ -95,8 +120,9 @@ CustomListView {
} }
Anim { Anim {
properties: "opacity,scale" property: "opacity"
to: 1 to: 1
type: Anim.DefaultEffects
} }
} }
remove: Transition { remove: Transition {
@@ -104,8 +130,9 @@ CustomListView {
Anim { Anim {
from: 1 from: 1
properties: "opacity,scale" property: "opacity"
to: 0 to: 0
type: Anim.DefaultEffects
} }
} }
states: [ states: [
@@ -113,7 +140,6 @@ CustomListView {
name: "apps" name: "apps"
PropertyChanges { PropertyChanges {
model.values: Apps.search(search.text)
root.delegate: appItem root.delegate: appItem
} }
}, },
@@ -121,7 +147,6 @@ CustomListView {
name: "actions" name: "actions"
PropertyChanges { PropertyChanges {
model.values: Actions.query(search.text)
root.delegate: actionItem root.delegate: actionItem
} }
}, },
@@ -129,7 +154,6 @@ CustomListView {
name: "calc" name: "calc"
PropertyChanges { PropertyChanges {
model.values: [0]
root.delegate: calcItem root.delegate: calcItem
} }
}, },
@@ -137,7 +161,6 @@ CustomListView {
name: "variant" name: "variant"
PropertyChanges { PropertyChanges {
model.values: SchemeVariants.query(search.text)
root.delegate: variantItem root.delegate: variantItem
} }
} }
@@ -147,7 +170,7 @@ CustomListView {
ParallelAnimation { ParallelAnimation {
Anim { Anim {
duration: Tokens.anim.durations.small duration: Tokens.anim.durations.small
easing.bezierCurve: Tokens.anim.curves.expressiveEffects easing.bezierCurve: Tokens.anim.curves.standardAccel
from: 1 from: 1
property: "opacity" property: "opacity"
target: root target: root
@@ -156,7 +179,7 @@ CustomListView {
Anim { Anim {
duration: Tokens.anim.durations.small duration: Tokens.anim.durations.small
easing.bezierCurve: Tokens.anim.curves.expressiveEffects easing.bezierCurve: Tokens.anim.curves.standardAccel
from: 1 from: 1
property: "scale" property: "scale"
target: root target: root
@@ -165,14 +188,24 @@ CustomListView {
} }
PropertyAction { PropertyAction {
properties: "values,delegate" property: "delegate"
targets: [model, root] target: root
value: null
}
ScriptAction {
script: root.displayText = root.search.text
}
PropertyAction {
property: "delegate"
target: root
} }
ParallelAnimation { ParallelAnimation {
Anim { Anim {
duration: Tokens.anim.durations.small duration: Tokens.anim.durations.small
easing.bezierCurve: Tokens.anim.curves.expressiveEffects easing.bezierCurve: Tokens.anim.curves.standardDecel
from: 0 from: 0
property: "opacity" property: "opacity"
target: root target: root
@@ -181,7 +214,7 @@ CustomListView {
Anim { Anim {
duration: Tokens.anim.durations.small duration: Tokens.anim.durations.small
easing.bezierCurve: Tokens.anim.curves.expressiveEffects easing.bezierCurve: Tokens.anim.curves.standardDecel
from: 0.9 from: 0.9
property: "scale" property: "scale"
target: root target: root
@@ -197,6 +230,8 @@ CustomListView {
} }
} }
Component.onCompleted: displayText = search.text
Component { Component {
id: appItem id: appItem
@@ -228,4 +263,20 @@ CustomListView {
list: root list: root
} }
} }
Connections {
function onTextChanged() {
root.syncDisplayText();
}
target: root.search
}
Connections {
function onLauncherChanged() {
root.syncDisplayText();
}
target: root.visibilities
}
} }
+54
View File
@@ -0,0 +1,54 @@
import Quickshell
import QtQuick
import ZShell.Components
import ZShell.Config
import qs.Modules.Launcher.Items
import qs.Components
import qs.Helpers
CarouselView {
id: root
required property var panels
readonly property var screen: QsWindow.window?.screen
required property SearchBar search
readonly property var values: Wallpapers.query(root.search.text.split(" ").slice(1).join(" "))
required property var visibilities
delegateProperties: ({
visibilities: root.visibilities
})
focalWidth: Config.launcher.sizes.wallpaperWidth
glideDuration: 250
glideEasingType: Easing.OutCubic
implicitHeight: tileHeight + Tokens.spacing.small / 2 + Tokens.padding.large + Tokens.padding.normal
itemSpacing: Tokens.spacing.small
maxWidth: {
if (!screen)
return 0;
const barMargins = Config.bar.border * 2;
let margins = Math.round(screen.width / 6);
if (visibilities.utilities || visibilities.sidebar)
margins = Config.sidebar.sizes.width;
return screen.width - (barMargins + margins) * 2 - Config.bar.rounding * 4;
}
minEdgeWidth: 56
model: values
tileHeight: focalWidth / 16 * 9
delegate: Component {
WallpaperTile {
}
}
Component.onCompleted: {
const idx = Wallpapers.list.findIndex(w => w.path === Wallpapers.actualCurrent);
if (idx >= 0)
jumpToIndex(idx);
}
Component.onDestruction: Wallpapers.stopPreview()
onCurrentItemChanged: {
if (currentItem)
Wallpapers.preview(currentItem.modelData.path);
}
}
+1 -1
View File
@@ -79,7 +79,7 @@ Item {
if (currentItem) { if (currentItem) {
if (list.showWallpapers) { if (list.showWallpapers) {
if (Colors.scheme === "dynamic" && currentItem.modelData.path !== Wallpapers.actualCurrent) if (Colors.scheme === "dynamic" && currentItem.modelData.path !== Wallpapers.actualCurrent)
Wallpapers.previewColourLock = true; Wallpapers.previewColorLock = true;
Wallpapers.setWallpaper(currentItem.modelData.path); Wallpapers.setWallpaper(currentItem.modelData.path);
root.visibilities.launcher = false; root.visibilities.launcher = false;
} else if (text.startsWith(Config.launcher.actionPrefix)) { } else if (text.startsWith(Config.launcher.actionPrefix)) {
+2 -2
View File
@@ -110,9 +110,9 @@ Item {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top anchors.top: parent.top
width: root.implicitWidth
sourceComponent: WallpaperList { sourceComponent: CarouselView {
content: root.content
panels: root.panels panels: root.panels
search: root.search search: root.search
visibilities: root.visibilities visibilities: root.visibilities
-96
View File
@@ -1,96 +0,0 @@
import ZShell.Models
import Quickshell
import QtQuick
import qs.Components
import qs.Helpers
import ZShell.Config
import qs.Modules
import qs.Services
Item {
id: root
required property FileSystemEntry modelData
required property PersistentProperties visibilities
implicitHeight: image.height + label.height + Tokens.spacing.small / 2 + Tokens.padding.large + Tokens.padding.normal
implicitWidth: image.width + Tokens.padding.larger * 2
opacity: 0
scale: 0.5
z: PathView.z ?? 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Component.onCompleted: {
scale = Qt.binding(() => PathView.isCurrentItem ? 1 : PathView.onPath ? 0.8 : 0);
opacity = Qt.binding(() => PathView.onPath ? 1 : 0);
}
StateLayer {
radius: Tokens.rounding.normal
onClicked: {
Wallpapers.setWallpaper(root.modelData.path);
root.visibilities.launcher = false;
}
}
Elevation {
anchors.fill: image
level: 4
opacity: root.PathView.isCurrentItem ? 1 : 0
radius: image.radius
Behavior on opacity {
Anim {
}
}
}
CustomClippingRect {
id: image
anchors.horizontalCenter: parent.horizontalCenter
color: Colors.tPalette.m3surfaceContainer
implicitHeight: implicitWidth / 16 * 9
implicitWidth: Config.launcher.sizes.wallpaperWidth
radius: Tokens.rounding.small
y: Tokens.padding.large
MaterialIcon {
anchors.centerIn: parent
color: Colors.tPalette.m3outline
font.pointSize: Tokens.font.size.extraLarge * 2
font.weight: 600
text: "image"
}
CachingImage {
anchors.fill: parent
cache: true
path: root.modelData.path
smooth: !root.PathView.view.moving
}
}
CustomText {
id: label
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: image.bottom
anchors.topMargin: Tokens.spacing.small / 2
elide: Text.ElideRight
font.pointSize: Tokens.font.size.normal
horizontalAlignment: Text.AlignHCenter
renderType: Text.QtRendering
text: root.modelData.relativePath
width: image.width - Tokens.padding.normal * 2
}
}
+58
View File
@@ -0,0 +1,58 @@
import Quickshell
import QtQuick
import ZShell.Config
import qs.Components
import qs.Helpers
import qs.Services
Item {
id: root
required property bool isCurrent
required property var modelData // FileSystemEntry-like {path, relativePath}
required property real tileHeight
required property real tileWidth
required property PersistentProperties visibilities
signal requestActivate
implicitHeight: tileHeight + Tokens.padding.large * 2
implicitWidth: tileWidth
CustomClippingRect {
id: image
anchors.horizontalCenter: parent.horizontalCenter
color: Colors.tPalette.m3surfaceContainer
implicitHeight: root.tileHeight
implicitWidth: root.tileWidth
radius: Tokens.rounding.small
y: Tokens.padding.large
MaterialIcon {
anchors.centerIn: parent
color: Colors.tPalette.m3outline
font.pointSize: Tokens.font.size.extraLarge * 2
font.weight: 600
text: "image"
}
CachingImage {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
cache: true
fillMode: Image.PreserveAspectCrop
height: root.tileHeight
path: root.modelData ? root.modelData.path : ""
smooth: root.isCurrent
sourceSize.height: root.tileHeight
sourceSize.width: root.tileHeight * (16 / 9)
width: root.tileHeight * (16 / 9)
}
StateLayer {
onClicked: root.requestActivate()
}
}
}
-93
View File
@@ -1,93 +0,0 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import qs.Components
import qs.Helpers
import ZShell.Config
import qs.Modules.Launcher.Items
PathView {
id: root
required property var content
readonly property int itemWidth: Config.launcher.sizes.wallpaperWidth * 0.9 + Tokens.padding.larger * 2
readonly property int numItems: {
const screen = QsWindow.window?.screen;
if (!screen)
return 0;
// Screen width - 4x outer rounding - 2x max side thickness (cause centered)
const barMargins = panels.bar.implicitWidth;
let outerMargins = 0;
if ((visibilities.utilities || visibilities.sidebar) && panels.utilities.implicitWidth > outerMargins)
outerMargins = panels.utilities.implicitWidth;
const maxWidth = screen.width - Config.bar.rounding * 4 - (barMargins + outerMargins) * 2;
if (maxWidth <= 0)
return 0;
const maxItemsOnScreen = Math.floor(maxWidth / itemWidth);
const visible = Math.min(maxItemsOnScreen, Config.launcher.maxWallpapers, scriptModel.values.length);
if (visible === 2)
return 1;
if (visible > 1 && visible % 2 === 0)
return visible - 1;
return visible;
}
required property var panels
required property SearchBar search
required property var visibilities
cacheItemCount: 4
highlightRangeMode: PathView.StrictlyEnforceRange
implicitWidth: Math.min(numItems, count) * itemWidth
pathItemCount: numItems
preferredHighlightBegin: 0.5
preferredHighlightEnd: 0.5
snapMode: PathView.SnapToItem
delegate: WallpaperItem {
visibilities: root.visibilities
}
model: ScriptModel {
id: scriptModel
readonly property string search: root.search.text.split(" ").slice(1).join(" ")
values: Wallpapers.query(search)
onValuesChanged: root.currentIndex = search ? 0 : values.findIndex(w => w.path === Wallpapers.actualCurrent)
}
path: Path {
startY: root.height / 2
PathAttribute {
name: "z"
value: 0
}
PathLine {
relativeY: 0
x: root.width / 2
}
PathAttribute {
name: "z"
value: 1
}
PathLine {
relativeY: 0
x: root.width
}
}
Component.onCompleted: currentIndex = Wallpapers.list.findIndex(w => w.path === Wallpapers.actualCurrent)
Component.onDestruction: Wallpapers.stopPreview()
onCurrentItemChanged: {
if (currentItem)
Wallpapers.preview(currentItem.modelData.path);
}
}
+1 -5
View File
@@ -464,11 +464,7 @@ CustomRect {
enabled: root.expanded enabled: root.expanded
fillWidth: true fillWidth: true
font: { font.pointSize: Tokens.font.size.small
const f = Qt.font(font);
f.pointSize = Tokens.font.size.small;
return f;
}
implicitWidth: label.implicitWidth implicitWidth: label.implicitWidth
inactiveColor: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3secondary : Colors.layer(Colors.palette.m3surfaceContainerHighest, 2) inactiveColor: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3secondary : Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
inactiveOnColor: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3onSecondary : Colors.palette.m3onSurfaceVariant inactiveOnColor: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3onSecondary : Colors.palette.m3onSurfaceVariant
+22 -1
View File
@@ -35,7 +35,6 @@ PageBase {
SelectRow { SelectRow {
active: Config.bar.position === "top" ? menuItems[0] : Config.bar.position === "left" ? menuItems[1] : menuItems[2] active: Config.bar.position === "top" ? menuItems[0] : Config.bar.position === "left" ? menuItems[1] : menuItems[2]
last: true
settingAnchor: "bar-position" settingAnchor: "bar-position"
subtext: qsTr("Change which edge the bar appears on") subtext: qsTr("Change which edge the bar appears on")
text: qsTr("Position") text: qsTr("Position")
@@ -61,6 +60,28 @@ PageBase {
onSelected: item => Config.bar.position = item.value onSelected: item => Config.bar.position = item.value
} }
ToggleRow {
checked: Config.bar.fullscreenReveal
settingAnchor: "bar-fullscreen-reveal"
subtext: qsTr("Hover top edge to show bar in fullscreen")
text: qsTr("Reveal on edge")
onToggled: Config.bar.fullscreenReveal = checked
}
SpinRow {
from: 0
last: true
settingAnchor: "bar-reveal-delay"
stepSize: 100
subtext: qsTr("The delay before bar is revealed when cursor is at edge")
text: qsTr("Reveal delay")
to: 2000
value: Config.bar.revealDelay
onMoved: v => Config.bar.revealDelay = v
}
// Components // Components
SectionHeader { SectionHeader {
text: qsTr("Components") text: qsTr("Components")
@@ -40,40 +40,19 @@ PageBase {
id: localWalls id: localWalls
model: { model: {
const walls = Wallpapers.list; const walls = [...Wallpapers.list];
var baseDir = Paths.wallsdir; var baseDir = Paths.wallsdir;
const categories = {}; walls.sort((a, b) => ((a.parentDir === baseDir) - (b.parentDir === baseDir)) || a.name.localeCompare(b.name));
const list = []; while (walls.length < 4)
for (const w of walls) { walls.push(null);
var parentDir = w.parentDir;
if (!parentDir.endsWith("/"))
parentDir = parentDir + "/";
if (!baseDir.endsWith("/")) return walls;
baseDir = baseDir + "/";
if (parentDir !== baseDir) {
const category = Wallpapers.getCategoryFor(w);
if (category && (!(category in categories) || categories[category].name.localeCompare(w.name) > 0))
categories[category] = w;
} else {
list.push(w);
}
}
list.push(...Object.values(categories));
list.sort((a, b) => ((a.parentDir === baseDir) - (b.parentDir === baseDir)) || a.name.localeCompare(b.name));
while (list.length < 4)
list.push(null);
return list;
} }
WallItem { WallItem {
required property FileSystemEntry modelData required property FileSystemEntry modelData
enabled: modelData enabled: modelData
// Empty placeholders for sizing
opacity: modelData ? 1 : 0 opacity: modelData ? 1 : 0
source: String(modelData?.path ?? "") source: String(modelData?.path ?? "")
+4 -2
View File
@@ -1,14 +1,16 @@
pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import QtQuick import QtQuick
import qs.Components import qs.Components
import ZShell.Config import ZShell.Config
import qs.Helpers
Item { Item {
id: root id: root
property real offsetScale: shouldBeActive ? 0 : 1 property real offsetScale: shouldBeActive ? 0 : 1
required property var panels required property var panels
property string queuedMode: "wallpaper"
required property ShellScreen screen required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.settings readonly property bool shouldBeActive: visibilities.settings
required property PersistentProperties visibilities required property PersistentProperties visibilities
@@ -37,7 +39,7 @@ Item {
sourceComponent: Content { sourceComponent: Content {
sState.animatingContainer: content.opacity < 1 sState.animatingContainer: content.opacity < 1
sState.currentPageIdx: ["wallpaper"][0] sState.currentPageIdx: ["wallpaper"].indexOf(root.queuedMode)
sState.screen: root.screen sState.screen: root.screen
} }
} }
+22 -79
View File
@@ -5,103 +5,46 @@ import Quickshell.Hyprland
import QtQuick import QtQuick
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
import ZShell.Config
import ZShell.Internal import ZShell.Internal
Item { Item {
id: root id: root
property bool completed
property real cropHeight: displayData?.height ?? 1.0
property real cropWidth: displayData?.width ?? 1.0
property real cropX: displayData?.x ?? 0.0
property real cropY: displayData?.y ?? 0.0
property WallpaperImage current
readonly property var displayData: Wallpapers.getCrop(screen.name) readonly property var displayData: Wallpapers.getCrop(screen.name)
required property ShellScreen screen required property ShellScreen screen
property size screenResolution: Qt.size(screen.width * screenScale, screen.height * screenScale) property size screenResolution: Qt.size(screen.width * screenScale, screen.height * screenScale)
property real screenScale: Hyprland.monitorFor(screen).scale property real screenScale: Hyprland.monitorFor(screen).scale
property string source: Wallpapers.current
anchors.fill: parent anchors.fill: parent
Component.onCompleted: { Component.onCompleted: Hyprland.refreshMonitors()
Hyprland.refreshMonitors();
if (source) WallpaperImage {
Qt.callLater(() => { id: img
current = imgComp.createObject(this, {
source
});
completed = true;
});
}
onSourceChanged: {
if (!source)
current = null;
else
current = imgComp.createObject(this, {
source: source
});
}
Component { anchors.fill: parent
id: imgComp cropHeight: root.displayData?.height ?? 1.0
cropWidth: root.displayData?.width ?? 1.0
cropX: root.displayData?.x ?? 0.0
cropY: root.displayData?.y ?? 0.0
fadeDuration: 150
screenResolution: root.screenResolution
source: Wallpapers.current
WallpaperImage { Behavior on cropHeight {
id: img Anim {
anchors.fill: parent
cropHeight: root.cropHeight
cropWidth: root.cropWidth
cropX: root.cropX
cropY: root.cropY
opacity: 0
screenResolution: root.screenResolution
source: root.source
Behavior on cropHeight {
Anim {
id: heightAnim
}
} }
Behavior on cropWidth { }
Anim { Behavior on cropWidth {
id: widthAnim Anim {
}
} }
Behavior on cropX { }
Anim { Behavior on cropX {
id: xAnim Anim {
}
} }
Behavior on cropY { }
Anim { Behavior on cropY {
id: yAnim Anim {
}
}
Anim on opacity {
id: anim
from: 0
running: false
to: 1
type: Anim.SlowEffects
}
onStatusChanged: {
if (status === Image.Ready) {
anim.start();
}
}
Timer {
id: destroyTimer
interval: anim.duration * 2
running: root.current !== img && root.current?.status === Image.Ready
onTriggered: Qt.callLater(() => img.destroy())
} }
} }
} }
+1
View File
@@ -4,6 +4,7 @@ qml_module(ZShell-components
lazylistview.hpp lazylistview.cpp lazylistview.hpp lazylistview.cpp
wavyline.hpp wavyline.cpp wavyline.hpp wavyline.cpp
buttonrow.hpp buttonrow.cpp buttonrow.hpp buttonrow.cpp
carouselview.hpp carouselview.cpp
LIBRARIES LIBRARIES
Qt::Quick Qt::Quick
) )
File diff suppressed because it is too large Load Diff
+249
View File
@@ -0,0 +1,249 @@
#pragma once
#include <QElapsedTimer>
#include <QEasingCurve>
#include <QList>
#include <QPointF>
#include <QQuickItem>
#include <QQmlComponent>
#include <QQmlEngine>
#include <QVariantAnimation>
#include <QVariantList>
#include <QVector>
#include <qtypes.h>
#include <qvectornd.h>
namespace ZShell::components {
class CarouselView : public QQuickItem {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(
QQmlComponent* delegate READ delegate WRITE setDelegate NOTIFY
delegateChanged)
Q_PROPERTY(QVariantList model READ model WRITE setModel NOTIFY modelChanged)
Q_PROPERTY(
qreal focalWidth READ focalWidth WRITE setFocalWidth NOTIFY
focalWidthChanged)
Q_PROPERTY(
qreal minEdgeWidth READ minEdgeWidth WRITE setMinEdgeWidth NOTIFY
minEdgeWidthChanged)
Q_PROPERTY(
qreal itemSpacing READ itemSpacing WRITE setItemSpacing NOTIFY
itemSpacingChanged)
Q_PROPERTY(
qreal tileHeight READ tileHeight WRITE setTileHeight NOTIFY
tileHeightChanged)
Q_PROPERTY(
qreal maxWidth READ maxWidth WRITE setMaxWidth NOTIFY maxWidthChanged)
Q_PROPERTY(
int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY
currentIndexChanged)
Q_PROPERTY(
QQuickItem* currentItem READ currentItem NOTIFY currentItemChanged)
Q_PROPERTY(
int glideDuration READ glideDuration WRITE setGlideDuration NOTIFY
glideDurationChanged)
Q_PROPERTY(
QEasingCurve::Type glideEasingType READ glideEasingType WRITE
setGlideEasingType NOTIFY glideEasingTypeChanged)
Q_PROPERTY(
QVariantMap delegateProperties READ delegateProperties WRITE
setDelegateProperties NOTIFY delegatePropertiesChanged)
public:
explicit CarouselView(QQuickItem* parent = nullptr);
~CarouselView() override;
QQmlComponent* delegate() const { return m_delegate; }
void setDelegate(QQmlComponent* c);
QVariantList model() const { return m_model; }
void setModel(const QVariantList& m);
qreal focalWidth() const { return m_focalWidth; }
void setFocalWidth(qreal v);
qreal minEdgeWidth() const { return m_minEdgeWidth; }
void setMinEdgeWidth(qreal v);
qreal itemSpacing() const { return m_itemSpacing; }
void setItemSpacing(qreal v);
qreal tileHeight() const { return m_tileHeight; }
void setTileHeight(qreal v);
qreal maxWidth() const { return m_maxWidth; }
void setMaxWidth(qreal v);
int currentIndex() const { return m_currentRealIndex; }
void setCurrentIndex(int idx);
QQuickItem* currentItem() const { return m_currentItem; }
int glideDuration() const { return m_glideDuration; }
void setGlideDuration(int v);
QEasingCurve::Type glideEasingType() const { return m_glideEasingType; }
void setGlideEasingType(QEasingCurve::Type t);
QVariantMap delegateProperties() const { return m_delegateProperties; }
void setDelegateProperties(const QVariantMap& v);
Q_INVOKABLE void goToIndex(int realIndex);
Q_INVOKABLE void incrementCurrentIndex();
Q_INVOKABLE void decrementCurrentIndex();
Q_INVOKABLE void jumpToIndex(int realIndex);
signals:
void delegateChanged();
void modelChanged();
void focalWidthChanged();
void minEdgeWidthChanged();
void itemSpacingChanged();
void tileHeightChanged();
void maxWidthChanged();
void currentIndexChanged();
void currentItemChanged();
void glideDurationChanged();
void glideEasingTypeChanged();
void delegatePropertiesChanged();
void activated(int realIndex, QVariant modelData);
void previewIndexChanged(int realIndex, QVariant modelData);
protected:
bool childMouseEventFilter(QQuickItem* item, QEvent* event) override;
void geometryChange(const QRectF& newGeo, const QRectF& oldGeo) override;
void mousePressEvent(QMouseEvent* event) override;
void mouseMoveEvent(QMouseEvent* event) override;
void mouseReleaseEvent(QMouseEvent* event) override;
void mouseUngrabEvent() override;
void wheelEvent(QWheelEvent* event) override;
private slots:
void handleDelegateActivate();
private:
struct Arrangement {
int n = 0;
qreal width = 0;
qreal outerSpacing = 0;
QVector<qreal> sizes;
QVector<qreal> centers;
qreal effectiveExtraWidth = 0;
};
struct Slot {
QQuickItem* item = nullptr;
int virtualIndex = 0;
int realIndex = -1;
bool active = false;
};
struct VelocitySample {
qint64 t;
qreal x;
};
void rebuildArrangement();
void relayout();
Arrangement arrangementForCount(int n) const;
int layoutCapacityForWidth(qreal width) const;
qreal targetWidthForMaxWidth(qreal maxWidth) const;
void updateSlotContent(Slot& slot, int virtualIndex);
void positionSlot(const Slot& slot) const;
void applyPropertiesToItem(QQuickItem* item, int realIndex) const;
qreal pitch() const { return m_focalWidth + m_itemSpacing; }
qreal contentXForIndex(qreal index) const;
int indexForContentX(qreal x) const;
qreal snapTargetX(qreal x) const;
void snapToNearest();
void glideTo(qreal dest);
void cancelAnimations();
qreal keylineSize(int io) const;
qreal sampleSize(qreal childLoc) const;
qreal continuousIndex() const;
int realIndexOf(int virtualIndex) const;
void updateCurrentIndexFromContentX(bool emitPreview);
void updateCurrentItem();
void wrapContentIfNeeded();
void centerInstantlyOnReal(int realIndex);
QQuickItem* acquireItem();
void releaseAllItems();
void addVelocitySample(qint64 t, qreal x);
qreal sampledVelocity() const;
QQmlComponent* m_delegate = nullptr;
QVariantList m_model;
QVariantMap m_delegateProperties;
qreal m_focalWidth = 0;
qreal m_minEdgeWidth = 56;
qreal m_itemSpacing = 8;
qreal m_tileHeight = 0;
qreal m_maxWidth = 0;
Arrangement m_arrangement;
int m_layoutCapacity = 0;
qreal m_contentX = 0;
int m_currentVirtualIndex = 0;
int m_currentRealIndex = -1;
int m_previewRealIndex = -1;
QQuickItem* m_currentItem = nullptr;
int m_centerBlockStart = 0;
QList<Slot> m_slots;
int m_glideDuration = 250;
QEasingCurve::Type m_glideEasingType = QEasingCurve::OutCubic;
QVariantAnimation* m_glideAnim = nullptr;
bool m_dragging = false;
bool m_dragActive = false;
QPointF m_pressPos;
qreal m_pressContentX = 0;
QElapsedTimer m_dragTimer;
QVector<VelocitySample> m_velocitySamples;
static constexpr qint64 kVelocitySampleWindowMs = 80;
QVariantAnimation* m_flickAnim = nullptr;
bool m_initializedLayout = false;
bool m_relayoutQueued = false;
};
} // namespace ZShell::components
+134 -52
View File
@@ -1,5 +1,7 @@
#include "cachingimagemanager.hpp" #include "cachingimagemanager.hpp"
#include <algorithm>
#include <QtQuick/qquickwindow.h> #include <QtQuick/qquickwindow.h>
#include <qcryptographichash.h> #include <qcryptographichash.h>
#include <qdir.h> #include <qdir.h>
@@ -7,10 +9,27 @@
#include <qfuturewatcher.h> #include <qfuturewatcher.h>
#include <qimagereader.h> #include <qimagereader.h>
#include <qpainter.h> #include <qpainter.h>
#include <qthread.h>
#include <qthreadpool.h>
#include <qtconcurrentrun.h> #include <qtconcurrentrun.h>
namespace ZShell::internal { namespace ZShell::internal {
namespace {
QThreadPool* imageIoPool() {
static QThreadPool pool;
static const bool init = [] {
pool.setMaxThreadCount(std::max(2, QThread::idealThreadCount() / 2));
return true;
}();
(void)init;
return &pool;
}
} // namespace
qreal CachingImageManager::effectiveScale() const { qreal CachingImageManager::effectiveScale() const {
if (m_item && m_item->window()) { if (m_item && m_item->window()) {
return m_item->window()->devicePixelRatio(); return m_item->window()->devicePixelRatio();
@@ -101,14 +120,13 @@ void CachingImageManager::updateSource() {
void CachingImageManager::updateSource(const QString& path) { void CachingImageManager::updateSource(const QString& path) {
if (path.isEmpty() || path == m_shaPath) { if (path.isEmpty() || path == m_shaPath) {
// Path is empty or already calculating sha for path
return; return;
} }
m_shaPath = path; m_shaPath = path;
const auto future = const auto future =
QtConcurrent::run(&CachingImageManager::sha256sum, path); QtConcurrent::run(imageIoPool(), &CachingImageManager::sha256sum, path);
const auto watcher = new QFutureWatcher<QString>(this); const auto watcher = new QFutureWatcher<QString>(this);
@@ -118,7 +136,16 @@ void CachingImageManager::updateSource(const QString& path) {
this, this,
[watcher, path, this]() { [watcher, path, this]() {
if (m_path != path) { if (m_path != path) {
// Object is destroyed or path has changed, ignore watcher->deleteLater();
return;
}
const QString key = watcher->result();
if (key.isEmpty()) {
if (m_shaPath == path) {
m_shaPath = QString();
}
watcher->deleteLater(); watcher->deleteLater();
return; return;
} }
@@ -126,19 +153,29 @@ void CachingImageManager::updateSource(const QString& path) {
const QSize size = effectiveSize(); const QSize size = effectiveSize();
if (!m_item || !size.width() || !size.height()) { if (!m_item || !size.width() || !size.height()) {
if (m_shaPath == path) {
m_shaPath = QString();
}
watcher->deleteLater(); watcher->deleteLater();
return; return;
} }
const QString fillMode = m_item->property("fillMode").toString(); const QString fillMode = m_item->property("fillMode").toString();
// clang-format off const QString filename =
const QString filename = QString("%1@%2x%3-%4.png") QString("%1@%2x%3-%4.png")
.arg(watcher->result()).arg(size.width()).arg(size.height()) .arg(key)
.arg(fillMode == "PreserveAspectCrop" ? "crop" : fillMode == "PreserveAspectFit" ? "fit" : "stretch"); .arg(size.width())
// clang-format on .arg(size.height())
.arg(
fillMode == "PreserveAspectCrop" ? "crop"
: fillMode == "PreserveAspectFit" ? "fit"
: "stretch");
const QUrl cache = m_cacheDir.resolved(QUrl(filename)); const QUrl cache = m_cacheDir.resolved(QUrl(filename));
if (m_cachePath == cache) { if (m_cachePath == cache) {
if (m_shaPath == path) {
m_shaPath = QString();
}
watcher->deleteLater(); watcher->deleteLater();
return; return;
} }
@@ -149,6 +186,9 @@ void CachingImageManager::updateSource(const QString& path) {
if (!cache.isLocalFile()) { if (!cache.isLocalFile()) {
qWarning() << "CachingImageManager::updateSource: cachePath" qWarning() << "CachingImageManager::updateSource: cachePath"
<< cache << "is not a local file"; << cache << "is not a local file";
if (m_shaPath == path) {
m_shaPath = QString();
}
watcher->deleteLater(); watcher->deleteLater();
return; return;
} }
@@ -161,7 +201,6 @@ void CachingImageManager::updateSource(const QString& path) {
createCache(path, cache.toLocalFile(), fillMode, size); createCache(path, cache.toLocalFile(), fillMode, size);
} }
// Clear current running sha if same
if (m_shaPath == path) { if (m_shaPath == path) {
m_shaPath = QString(); m_shaPath = QString();
} }
@@ -170,7 +209,6 @@ void CachingImageManager::updateSource(const QString& path) {
}); });
watcher->setFuture(future); watcher->setFuture(future);
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
} }
QUrl CachingImageManager::cachePath() const { QUrl CachingImageManager::cachePath() const {
@@ -182,61 +220,105 @@ void CachingImageManager::createCache(
const QString& cache, const QString& cache,
const QString& fillMode, const QString& fillMode,
const QSize& size) const { const QSize& size) const {
QThreadPool::globalInstance()->start([path, cache, fillMode, size] { auto* watcher =
QImage image(path); new QFutureWatcher<bool>(const_cast<CachingImageManager*>(this));
if (image.isNull()) { connect(
qWarning() << "CachingImageManager::createCache: failed to read" watcher,
<< path; &QFutureWatcher<bool>::finished,
return; this,
} [this, watcher, cache, path]() {
if (watcher->result() && m_item && m_path == path &&
m_cachePath.toLocalFile() == cache) {
m_item->setProperty("source", QUrl::fromLocalFile(cache));
}
image.convertTo(QImage::Format_ARGB32); watcher->deleteLater();
});
if (fillMode == "PreserveAspectCrop") { const auto future =
image = image.scaled( QtConcurrent::run(
size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); imageIoPool(), [path, cache, fillMode, size]() -> bool {
} else if (fillMode == "PreserveAspectFit") { QImageReader reader(path);
image = image.scaled(
size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
} else {
image = image.scaled(
size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
}
if (fillMode == "PreserveAspectCrop" || if (reader.supportsOption(QImageIOHandler::ScaledSize)) {
fillMode == "PreserveAspectFit") { const QSize full = reader.size();
QImage canvas(size, QImage::Format_ARGB32); if (full.isValid() && !full.isEmpty()) {
canvas.fill(Qt::transparent); const QSize hint = size * 2;
QSize scaled = full;
scaled.scale(hint, Qt::KeepAspectRatioByExpanding);
scaled = QSize(
std::min(scaled.width(), full.width()),
std::min(scaled.height(), full.height()));
reader.setScaledSize(scaled);
}
}
QPainter painter(&canvas); QImage image = reader.read();
painter.drawImage(
(size.width() - image.width()) / 2,
(size.height() - image.height()) / 2,
image);
painter.end();
image = canvas; if (image.isNull()) {
} qWarning()
<< "CachingImageManager::createCache: failed to read"
<< path << reader.errorString();
return false;
}
const QString parent = QFileInfo(cache).absolutePath(); image.convertTo(QImage::Format_ARGB32);
if (!QDir().mkpath(parent) || !image.save(cache)) {
qWarning() << "CachingImageManager::createCache: failed to save to" if (fillMode == "PreserveAspectCrop") {
<< cache; image = image.scaled(
} size,
}); Qt::KeepAspectRatioByExpanding,
Qt::SmoothTransformation);
} else if (fillMode == "PreserveAspectFit") {
image = image.scaled(
size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
} else {
image = image.scaled(
size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
}
if (fillMode == "PreserveAspectCrop" ||
fillMode == "PreserveAspectFit") {
QImage canvas(size, QImage::Format_ARGB32);
canvas.fill(Qt::transparent);
QPainter painter(&canvas);
painter.drawImage(
(size.width() - image.width()) / 2,
(size.height() - image.height()) / 2,
image);
painter.end();
image = canvas;
}
const QString parent = QFileInfo(cache).absolutePath();
if (!QDir().mkpath(parent) || !image.save(cache)) {
qWarning()
<< "CachingImageManager::createCache: failed to save to"
<< cache;
return false;
}
return true;
});
watcher->setFuture(future);
} }
QString CachingImageManager::sha256sum(const QString& path) { QString CachingImageManager::sha256sum(const QString& path) {
QFile file(path); const QFileInfo info(path);
if (!file.open(QIODevice::ReadOnly)) {
qWarning() << "CachingImageManager::sha256sum: failed to open" << path; if (!info.exists() || !info.isFile()) {
return ""; qWarning() << "CachingImageManager::sha256sum: failed to stat" << path;
return QString();
} }
QCryptographicHash hash(QCryptographicHash::Sha256); QCryptographicHash hash(QCryptographicHash::Sha256);
hash.addData(&file); hash.addData(path.toUtf8());
file.close(); hash.addData(QByteArray::number(info.size()));
hash.addData(QByteArray::number(info.lastModified().toMSecsSinceEpoch()));
return hash.result().toHex(); return hash.result().toHex();
} }
+198 -87
View File
@@ -4,14 +4,31 @@
#include <QCryptographicHash> #include <QCryptographicHash>
#include <QDir> #include <QDir>
#include <QFileInfo> #include <QFileInfo>
#include <QImageReader>
#include <QtConcurrent> #include <QtConcurrent>
#include <QSGImageNode>
#include <QQuickWindow> #include <QQuickWindow>
namespace ZShell::internal { namespace ZShell::internal {
QThreadPool* WallpaperImage::loadPool() {
static QThreadPool* pool = [] {
auto* p = new QThreadPool();
p->setMaxThreadCount(4);
return p;
}();
return pool;
}
WallpaperImage::WallpaperImage(QQuickItem* parent) : QQuickItem(parent) { WallpaperImage::WallpaperImage(QQuickItem* parent) : QQuickItem(parent) {
setFlag(ItemHasContents, true); setFlag(ItemHasContents, true);
m_loadDebounceTimer.setSingleShot(true);
connect(
&m_loadDebounceTimer,
&QTimer::timeout,
this,
&WallpaperImage::beginLoad);
connect( connect(
&m_imageWatcher, &m_imageWatcher,
&QFutureWatcher<QImage>::finished, &QFutureWatcher<QImage>::finished,
@@ -20,12 +37,15 @@ WallpaperImage::WallpaperImage(QQuickItem* parent) : QQuickItem(parent) {
} }
WallpaperImage::~WallpaperImage() { WallpaperImage::~WallpaperImage() {
m_requestGeneration->fetch_add(1, std::memory_order_relaxed);
m_imageWatcher.disconnect();
if (m_texture) delete m_texture; if (m_texture) delete m_texture;
if (m_previousTexture) delete m_previousTexture;
} }
void WallpaperImage::setStatus(const Status s) { void WallpaperImage::setStatus(const Status s) {
if (m_status == s) return; if (m_status == s) return;
m_status = s; m_status = s;
emit statusChanged(); emit statusChanged();
} }
@@ -36,14 +56,14 @@ void WallpaperImage::setSource(const QUrl& source) {
emit sourceChanged(); emit sourceChanged();
setStatus(Loading); setStatus(Loading);
loadImage(); m_loadDebounceTimer.start(80);
} }
void WallpaperImage::setScreenResolution(const QSize& screenResolution) { void WallpaperImage::setScreenResolution(const QSize& screenResolution) {
if (m_screenResolution == screenResolution) return; if (m_screenResolution == screenResolution) return;
m_screenResolution = screenResolution; m_screenResolution = screenResolution;
emit screenResolutionChanged(); emit screenResolutionChanged();
loadImage(); beginLoad();
} }
void WallpaperImage::setZoom(qreal zoom) { void WallpaperImage::setZoom(qreal zoom) {
@@ -53,6 +73,13 @@ void WallpaperImage::setZoom(qreal zoom) {
update(); update();
} }
void WallpaperImage::setFadeDuration(int ms) {
if (ms < 0) ms = 0;
if (m_fadeDuration == ms) return;
m_fadeDuration = ms;
emit fadeDurationChanged();
}
void WallpaperImage::setCropX(qreal x) { void WallpaperImage::setCropX(qreal x) {
if (qFuzzyCompare(m_cropX, x)) return; if (qFuzzyCompare(m_cropX, x)) return;
m_cropX = x; m_cropX = x;
@@ -100,143 +127,227 @@ QString WallpaperImage::getCacheFilePath() const {
return cachePath + "/" + hash + ".png"; return cachePath + "/" + hash + ".png";
} }
void WallpaperImage::loadImage() { void WallpaperImage::beginLoad() {
if (m_source.isEmpty()) { if (m_source.isEmpty()) {
setStatus(Null); setStatus(Null);
return; return;
} }
const quint64 requestId =
m_requestGeneration->fetch_add(1, std::memory_order_relaxed) + 1;
QString cacheFile = getCacheFilePath(); QString cacheFile = getCacheFilePath();
QString sourceFile = m_source.isLocalFile() ? m_source.toLocalFile() QString sourceFile = m_source.isLocalFile() ? m_source.toLocalFile()
: m_source.toString(); : m_source.toString();
if (sourceFile.startsWith("qrc:/")) sourceFile = sourceFile.mid(3);
if (sourceFile.startsWith("qrc:/")) {
sourceFile = sourceFile.mid(3);
}
QSize targetRes = m_screenResolution; QSize targetRes = m_screenResolution;
auto generation = m_requestGeneration;
QFuture<QImage> future = QtConcurrent::run(
loadPool(),
[sourceFile, cacheFile, targetRes, requestId, generation]() -> QImage {
auto stale = [&] {
return generation->load(std::memory_order_relaxed) != requestId;
};
if (stale()) return QImage();
QFuture<QImage> future =
QtConcurrent::run([sourceFile, cacheFile, targetRes]() -> QImage {
if (!targetRes.isEmpty() && !cacheFile.isEmpty() && if (!targetRes.isEmpty() && !cacheFile.isEmpty() &&
QFileInfo::exists(cacheFile)) { QFileInfo::exists(cacheFile)) {
QImage cached(cacheFile); QImage cached(cacheFile);
if (!cached.isNull()) return cached; if (!cached.isNull()) return cached;
} }
QImage original(sourceFile); if (stale()) return QImage();
QImage original;
QImageReader reader(sourceFile);
if (!targetRes.isEmpty() &&
reader.supportsOption(QImageIOHandler::Size)) {
QSize actual = reader.size();
if (actual.isValid() &&
(actual.width() > targetRes.width() ||
actual.height() > targetRes.height())) {
QSize scaledSize =
actual.scaled(targetRes, Qt::KeepAspectRatioByExpanding);
reader.setScaledSize(scaledSize);
}
original = reader.read();
} else {
original = QImage(sourceFile);
}
if (original.isNull()) return QImage(); if (original.isNull()) return QImage();
if (targetRes.isEmpty()) { if (stale()) return QImage();
return original;
}
if (original.width() > targetRes.width() || if (targetRes.isEmpty()) return original;
original.height() > targetRes.height()) {
QImage scaled = original.scaled(
targetRes,
Qt::KeepAspectRatioByExpanding,
Qt::SmoothTransformation);
if (!cacheFile.isEmpty()) scaled.save(cacheFile, "PNG");
return scaled;
}
if (!cacheFile.isEmpty()) original.save(cacheFile, "PNG"); QImage scaled = (original.width() > targetRes.width() ||
return original; original.height() > targetRes.height())
? original.scaled(
targetRes,
Qt::KeepAspectRatioByExpanding,
Qt::SmoothTransformation)
: original;
if (stale()) return scaled;
if (!cacheFile.isEmpty()) scaled.save(cacheFile, "PNG");
return scaled;
}); });
m_imageWatcher.setFuture(future); m_imageWatcher.setFuture(future);
} }
void WallpaperImage::handleImageLoaded() { void WallpaperImage::handleImageLoaded() {
m_image = m_imageWatcher.result(); QImage result = m_imageWatcher.result();
if (result.isNull()) {
setStatus(Error);
return;
}
setStatus(m_image.isNull() ? Error : Ready); m_pendingImage = result;
m_hasPending = true;
m_textureDirty = true; setStatus(Ready);
update(); update();
} }
QSGNode* WallpaperImage::updatePaintNode( QRectF WallpaperImage::computeSourceRect(QSGTexture* texture) const {
QSGNode* oldNode, UpdatePaintNodeData*) { QSize texSize = texture->textureSize();
auto* node = static_cast<QSGImageNode*>(oldNode);
if (m_image.isNull()) { qreal cW = m_cropWidth / m_zoom;
delete node; qreal cH = m_cropHeight / m_zoom;
return nullptr;
QRectF reqRect(
m_cropX * texSize.width(),
m_cropY * texSize.height(),
cW * texSize.width(),
cH * texSize.height());
QRectF bounds = boundingRect();
if (bounds.isEmpty() || reqRect.isEmpty()) return reqRect;
qreal targetRatio = bounds.width() / bounds.height();
qreal reqRatio = reqRect.width() / reqRect.height();
QRectF sourceRect = reqRect;
if (reqRatio > targetRatio) {
qreal newWidth = reqRect.height() * targetRatio;
qreal xOffset = (reqRect.width() - newWidth) / 2.0;
sourceRect.setX(reqRect.x() + xOffset);
sourceRect.setWidth(newWidth);
} else if (reqRatio < targetRatio) {
qreal newHeight = reqRect.width() / targetRatio;
qreal yOffset = (reqRect.height() - newHeight) / 2.0;
sourceRect.setY(reqRect.y() + yOffset);
sourceRect.setHeight(newHeight);
} }
if (!node) { return sourceRect;
node = window()->createImageNode(); }
}
if (m_textureDirty) { void WallpaperImage::configureImageNode(
if (m_texture) delete m_texture; QSGImageNode* node, QSGTexture* texture) {
m_texture = window()->createTextureFromImage( node->setTexture(texture);
m_image, QQuickWindow::TextureHasAlphaChannel); node->setRect(boundingRect());
m_textureDirty = false; node->setFiltering(QSGTexture::Linear);
}
if (m_texture) { QRectF sourceRect = computeSourceRect(texture);
node->setTexture(m_texture); node->setSourceRect(sourceRect);
node->setRect(boundingRect());
node->setFiltering(QSGTexture::Linear);
qreal cW = m_cropWidth / m_zoom;
qreal cH = m_cropHeight / m_zoom;
QRectF reqRect(
m_cropX * m_texture->textureSize().width(),
m_cropY * m_texture->textureSize().height(),
cW * m_texture->textureSize().width(),
cH * m_texture->textureSize().height());
QRectF bounds = boundingRect();
if (bounds.isEmpty() || reqRect.isEmpty()) return node;
qreal targetRatio = bounds.width() / bounds.height();
qreal reqRatio = reqRect.width() / reqRect.height();
QRectF sourceRect = reqRect;
if (reqRatio > targetRatio) {
qreal newWidth = reqRect.height() * targetRatio;
qreal xOffset = (reqRect.width() - newWidth) / 2.0;
sourceRect.setX(reqRect.x() + xOffset);
sourceRect.setWidth(newWidth);
} else if (reqRatio < targetRatio) {
qreal newHeight = reqRect.width() / targetRatio;
qreal yOffset = (reqRect.height() - newHeight) / 2.0;
sourceRect.setY(reqRect.y() + yOffset);
sourceRect.setHeight(newHeight);
}
if (texture == m_texture) {
QSize texSize = texture->textureSize();
QRectF normalizedActual( QRectF normalizedActual(
sourceRect.x() / m_texture->textureSize().width(), sourceRect.x() / texSize.width(),
sourceRect.y() / m_texture->textureSize().height(), sourceRect.y() / texSize.height(),
sourceRect.width() / m_texture->textureSize().width(), sourceRect.width() / texSize.width(),
sourceRect.height() / m_texture->textureSize().height()); sourceRect.height() / texSize.height());
bool changed = false; bool changed = false;
auto updateIfChanged = [&](qreal& dst, qreal value) { auto updateIfChanged = [&](qreal& dst, qreal value) {
if (!qFuzzyCompare(dst, value)) { if (!qFuzzyCompare(dst, value)) {
dst = value; dst = value;
changed = true; changed = true;
} }
}; };
updateIfChanged(m_actualCropX, normalizedActual.x()); updateIfChanged(m_actualCropX, normalizedActual.x());
updateIfChanged(m_actualCropY, normalizedActual.y()); updateIfChanged(m_actualCropY, normalizedActual.y());
updateIfChanged(m_actualCropWidth, normalizedActual.width()); updateIfChanged(m_actualCropWidth, normalizedActual.width());
updateIfChanged(m_actualCropHeight, normalizedActual.height()); updateIfChanged(m_actualCropHeight, normalizedActual.height());
if (changed) emit actualCropChanged(); if (changed) emit actualCropChanged();
}
}
node->setSourceRect(sourceRect); QSGNode* WallpaperImage::updatePaintNode(
QSGNode* oldNode, UpdatePaintNodeData*) {
Q_UNUSED(oldNode);
if (m_hasPending) {
if (m_previousTexture) {
delete m_previousTexture;
m_previousTexture = nullptr;
}
m_previousTexture = m_texture;
m_texture = window()->createTextureFromImage(
m_pendingImage, QQuickWindow::TextureHasAlphaChannel);
m_pendingImage = QImage();
m_hasPending = false;
if (m_previousTexture && m_fadeDuration > 0) {
m_fading = true;
m_fadeTimer.restart();
} else {
if (m_previousTexture) {
delete m_previousTexture;
m_previousTexture = nullptr;
}
m_fading = false;
}
} }
return node; if (!m_texture) {
return m_rootNode;
}
if (!m_rootNode) {
m_rootNode = new QSGNode();
m_newOpacityNode = new QSGOpacityNode();
m_newImageNode = window()->createImageNode();
m_newOpacityNode->appendChildNode(m_newImageNode);
m_rootNode->appendChildNode(m_newOpacityNode);
m_oldOpacityNode = new QSGOpacityNode();
m_oldImageNode = window()->createImageNode();
m_oldOpacityNode->appendChildNode(m_oldImageNode);
m_rootNode->appendChildNode(m_oldOpacityNode);
}
configureImageNode(m_newImageNode, m_texture);
m_newOpacityNode->setOpacity(1.0);
if (m_fading && m_previousTexture) {
qreal progress =
qBound(0.0, qreal(m_fadeTimer.elapsed()) / m_fadeDuration, 1.0);
configureImageNode(m_oldImageNode, m_previousTexture);
m_oldOpacityNode->setOpacity(1.0 - progress);
if (progress >= 1.0) {
m_fading = false;
delete m_previousTexture;
m_previousTexture = nullptr;
m_oldOpacityNode->setOpacity(0.0);
} else {
update();
}
} else {
m_oldOpacityNode->setOpacity(0.0);
}
return m_rootNode;
} }
} // namespace ZShell::internal } // namespace ZShell::internal
+35 -7
View File
@@ -4,9 +4,15 @@
#include <QImage> #include <QImage>
#include <QUrl> #include <QUrl>
#include <QSGTexture> #include <QSGTexture>
#include <QSGOpacityNode>
#include <QSGImageNode>
#include <QFutureWatcher> #include <QFutureWatcher>
#include <QElapsedTimer>
#include <QThreadPool>
#include <QTimer>
#include <QtQml/qqml.h> #include <QtQml/qqml.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <atomic>
namespace ZShell::internal { namespace ZShell::internal {
@@ -19,6 +25,9 @@ class WallpaperImage : public QQuickItem {
NOTIFY screenResolutionChanged) NOTIFY screenResolutionChanged)
Q_PROPERTY(qreal zoom READ zoom WRITE setZoom NOTIFY zoomChanged) Q_PROPERTY(qreal zoom READ zoom WRITE setZoom NOTIFY zoomChanged)
Q_PROPERTY(Status status READ status NOTIFY statusChanged) Q_PROPERTY(Status status READ status NOTIFY statusChanged)
Q_PROPERTY(
int fadeDuration READ fadeDuration WRITE setFadeDuration NOTIFY
fadeDurationChanged)
Q_PROPERTY(qreal actualCropX READ actualCropX NOTIFY actualCropChanged) Q_PROPERTY(qreal actualCropX READ actualCropX NOTIFY actualCropChanged)
Q_PROPERTY(qreal actualCropY READ actualCropY NOTIFY actualCropChanged) Q_PROPERTY(qreal actualCropY READ actualCropY NOTIFY actualCropChanged)
@@ -54,12 +63,12 @@ class WallpaperImage : public QQuickItem {
qreal zoom() const { return m_zoom; } qreal zoom() const { return m_zoom; }
void setZoom(qreal zoom); void setZoom(qreal zoom);
int fadeDuration() const { return m_fadeDuration; }
void setFadeDuration(int ms);
qreal actualCropX() const { return m_actualCropX; } qreal actualCropX() const { return m_actualCropX; }
qreal actualCropY() const { return m_actualCropY; } qreal actualCropY() const { return m_actualCropY; }
qreal actualCropWidth() const { return m_actualCropWidth; } qreal actualCropWidth() const { return m_actualCropWidth; }
qreal actualCropHeight() const { return m_actualCropHeight; } qreal actualCropHeight() const { return m_actualCropHeight; }
qreal cropX() const { return m_cropX; } qreal cropX() const { return m_cropX; }
@@ -82,6 +91,7 @@ class WallpaperImage : public QQuickItem {
void sourceChanged(); void sourceChanged();
void screenResolutionChanged(); void screenResolutionChanged();
void zoomChanged(); void zoomChanged();
void fadeDurationChanged();
void actualCropChanged(); void actualCropChanged();
void cropXChanged(); void cropXChanged();
void cropYChanged(); void cropYChanged();
@@ -90,15 +100,18 @@ class WallpaperImage : public QQuickItem {
void statusChanged(); void statusChanged();
private: private:
void loadImage(); void beginLoad();
void handleImageLoaded(); void handleImageLoaded();
QString getCacheFilePath() const; QString getCacheFilePath() const;
void setStatus(const Status s); void setStatus(const Status s);
QRectF computeSourceRect(QSGTexture* texture) const;
void configureImageNode(QSGImageNode* node, QSGTexture* texture);
Status m_status = Null; Status m_status = Null;
QUrl m_source; QUrl m_source;
QSize m_screenResolution; QSize m_screenResolution;
qreal m_zoom = 1.0; qreal m_zoom = 1.0;
int m_fadeDuration = 400;
qreal m_actualCropX = 0.0; qreal m_actualCropX = 0.0;
qreal m_actualCropY = 0.0; qreal m_actualCropY = 0.0;
@@ -110,10 +123,25 @@ class WallpaperImage : public QQuickItem {
qreal m_cropWidth = 1.0; qreal m_cropWidth = 1.0;
qreal m_cropHeight = 1.0; qreal m_cropHeight = 1.0;
QImage m_image; QTimer m_loadDebounceTimer;
QSGTexture* m_texture = nullptr;
bool m_textureDirty = false;
QFutureWatcher<QImage> m_imageWatcher; QFutureWatcher<QImage> m_imageWatcher;
std::shared_ptr<std::atomic<quint64>> m_requestGeneration =
std::make_shared<std::atomic<quint64>>(0);
static QThreadPool* loadPool();
QImage m_pendingImage;
bool m_hasPending = false;
QSGTexture* m_texture = nullptr;
QSGTexture* m_previousTexture = nullptr;
bool m_fading = false;
QElapsedTimer m_fadeTimer;
QSGNode* m_rootNode = nullptr;
QSGOpacityNode* m_newOpacityNode = nullptr;
QSGOpacityNode* m_oldOpacityNode = nullptr;
QSGImageNode* m_newImageNode = nullptr;
QSGImageNode* m_oldImageNode = nullptr;
}; };
} // namespace ZShell::internal } // namespace ZShell::internal
+1 -1
View File
@@ -69,7 +69,7 @@ Singleton {
} }
for (const [name, color] of Object.entries(scheme.colors)) { for (const [name, color] of Object.entries(scheme.colors)) {
const propName = name.startsWith("term") ? name : `m3${name}`; const propName = `m3${name}`;
if (colors.hasOwnProperty(propName)) if (colors.hasOwnProperty(propName))
colors[propName] = `${color}`; colors[propName] = `${color}`;
} }
+25 -6
View File
@@ -116,10 +116,15 @@ def list_presets(
@app.command() @app.command()
def generate( def generate(
image_path: Path | None = typer.Option( image_path: Path | None = typer.Option(
None, help="Path to source image. Required for image mode." None,
"--image-path",
"-i",
help="Path to source image. Required for image mode.",
), ),
scheme: str | None = typer.Option( scheme: str | None = typer.Option(
None, None,
"--scheme",
"-s",
help="Color scheme algorithm to use for image mode. Ignored in preset mode.", help="Color scheme algorithm to use for image mode. Ignored in preset mode.",
autocompletion=_complete_scheme_name, autocompletion=_complete_scheme_name,
), ),
@@ -130,16 +135,26 @@ def generate(
), ),
mode: str | None = typer.Option( mode: str | None = typer.Option(
None, None,
"--mode",
"-m",
help="Mode of the preset scheme (dark or light).", help="Mode of the preset scheme (dark or light).",
autocompletion=_complete_mode, autocompletion=_complete_mode,
), ),
accent: str | None = typer.Option( accent: str | None = typer.Option(
None, None,
"--accent",
"-a",
help="Accent for schemes that support it (e.g. mauve).", help="Accent for schemes that support it (e.g. mauve).",
autocompletion=_complete_accent, autocompletion=_complete_accent,
), ),
preview: bool = typer.Option(
False,
"--preview",
"-p",
help="Preview scheme - internal use",
),
): ):
if not any([image_path, scheme, preset, mode, accent]): if not any([image_path, scheme, preset, mode, accent, preview]):
print( print(
"Hint: use --preset <scheme>:<variant> or --image-path <path>", "Hint: use --preset <scheme>:<variant> or --image-path <path>",
file=sys.stderr, file=sys.stderr,
@@ -685,10 +700,6 @@ def generate(
variant_val = scheme if not preset else p_variant variant_val = scheme if not preset else p_variant
if smart and not preset:
apply_gtk_mode(effective_mode)
apply_qt_mode(effective_mode, HOME)
output_dict = { output_dict = {
"name": name, "name": name,
"flavor": flavor, "flavor": flavor,
@@ -698,6 +709,14 @@ def generate(
"seed": seed.to_int(), "seed": seed.to_int(),
} }
if preview:
print(json.dumps(output_dict))
return
if smart and not preset:
apply_gtk_mode(effective_mode)
apply_qt_mode(effective_mode, HOME)
if TEMPLATE_DIR is not None: if TEMPLATE_DIR is not None:
wp = str(WALL_PATH) wp = str(WALL_PATH)
ctx = build_template_context( ctx = build_template_context(