Merge branch 'zshell-img-tools' of git.zach-dev.cc:zach/z-bar-qt into zshell-img-tools
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 12s
Python / lint-format (pull_request) Successful in 20s
Python / test (pull_request) Successful in 50s
Lint & Format (Rust) / lint-format (pull_request) Failing after 1m2s

This commit is contained in:
2026-05-28 18:55:43 +02:00
44 changed files with 823 additions and 1311 deletions
+2
View File
@@ -13,3 +13,5 @@ uv.lock
.qtcreator/ .qtcreator/
dist/ dist/
**/target/ **/target/
**/test-plugins/
**/Charts/
+5
View File
@@ -59,6 +59,8 @@ JsonObject {
} }
property int rounding: 8 property int rounding: 8
property int smoothing: 32 property int smoothing: 32
property Tray tray: Tray {
}
component Popouts: JsonObject { component Popouts: JsonObject {
property bool activeWindow: true property bool activeWindow: true
@@ -69,4 +71,7 @@ JsonObject {
property bool tray: true property bool tray: true
property bool upower: true property bool upower: true
} }
component Tray: JsonObject {
property int trayIconSize: 24
}
} }
+3
View File
@@ -100,6 +100,9 @@ Singleton {
border: barConfig.border, border: barConfig.border,
smoothing: barConfig.smoothing, smoothing: barConfig.smoothing,
height: barConfig.height, height: barConfig.height,
tray: {
trayIconSize: barConfig.tray.trayIconSize
},
popouts: { popouts: {
tray: barConfig.popouts.tray, tray: barConfig.popouts.tray,
audio: barConfig.popouts.audio, audio: barConfig.popouts.audio,
+1 -1
View File
@@ -27,7 +27,7 @@ JsonObject {
{ {
perc: 20, perc: 20,
name: qsTr("Low battery"), name: qsTr("Low battery"),
message: qsTr("Battery at %1%").arg(Battery.currentPerc * 100), message: qsTr("Battery is low"),
icon: "battery_android_frame_2" icon: "battery_android_frame_2"
}, },
] ]
+1
View File
@@ -36,6 +36,7 @@ Scope {
for (const perc of root.popupThresholds) { for (const perc of root.popupThresholds) {
if (p <= perc.perc && !perc.warned) { if (p <= perc.perc && !perc.warned) {
perc.warned = true; perc.warned = true;
console.log(perc.warned + "\n" + [...Config.general.battery.popupThresholds][0].warned);
Toaster.toast(perc.title ?? qsTr("Battery warning"), perc.message ?? qsTr("Battery perc is low"), perc.icon ?? "battery_android_alert", perc.critical ? Toast.Error : Toast.Warning); Toaster.toast(perc.title ?? qsTr("Battery warning"), perc.message ?? qsTr("Battery perc is low"), perc.icon ?? "battery_android_alert", perc.critical ? Toast.Error : Toast.Warning);
} }
} }
+3 -26
View File
@@ -292,20 +292,11 @@ Singleton {
if (cachedImageSource === source) if (cachedImageSource === source)
return; return;
if (source.startsWith("file:")) {
cachedImageSource = source;
image = source;
return;
}
const hash = hashForString(source);
const cache = `${Paths.notifimagecache}/${hash}.png`;
const cacheUrl = Qt.resolvedUrl(cache);
cachingImage = true; cachingImage = true;
ZShellIo.saveImage(source, cacheUrl, () => {
ZShellIo.cacheImage(Qt.resolvedUrl(source), Paths.notifimagecache, (path, url) => {
cachedImageSource = source; cachedImageSource = source;
image = cache; image = path;
cachingImage = false; cachingImage = false;
}, () => { }, () => {
cachingImage = false; cachingImage = false;
@@ -321,20 +312,6 @@ Singleton {
} }
} }
function hashForString(s: string): string {
let h1 = 0xdeadbeef, h2 = 0x41c6ce57, ch;
for (let i = 0; i < s.length; i++) {
ch = s.charCodeAt(i);
h1 = Math.imul(h1 ^ ch, 2654435761);
h2 = Math.imul(h2 ^ ch, 1597334677);
}
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
return (h2 >>> 0).toString(16).padStart(8, "0") + (h1 >>> 0).toString(16).padStart(8, "0");
}
function lock(item: Item): void { function lock(item: Item): void {
locks.add(item); locks.add(item);
} }
-107
View File
@@ -1,107 +0,0 @@
import Quickshell
import QtQuick
import QtQuick.Shapes
import qs.Components
import qs.Config
import qs.Modules as Modules
import qs.Modules.Notifications as Notifications
import qs.Modules.Notifications.Sidebar as Sidebar
import qs.Modules.Notifications.Sidebar.Utils as Utils
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
required property Item bar
required property Panels panels
required property PersistentProperties visibilities
anchors.fill: parent
anchors.margins: Config.barConfig.border
anchors.topMargin: bar.implicitHeight
asynchronous: true
preferredRendererType: Shape.CurveRenderer
Drawing.Background {
startX: 0
startY: wrapper.y - rounding
wrapper: root.panels.drawing
}
Resources.Background {
startX: 0 - rounding
startY: 0
wrapper: root.panels.resources
}
Osd.Background {
startX: root.width - root.panels.sidebar.width
startY: (root.height - wrapper.height) / 2 - rounding
wrapper: root.panels.osd
}
Modules.Background {
invertBottomRounding: wrapper.x <= 0
rounding: root.panels.popouts.currentName.startsWith("updates") || root.panels.popouts.currentName.startsWith("audio") ? Appearance.rounding.normal : Appearance.rounding.smallest
startX: wrapper.x - rounding
startY: wrapper.y
wrapper: root.panels.popouts
}
Notifications.Background {
sidebar: sidebar
startX: root.width
startY: 0
wrapper: root.panels.notifications
}
Launcher.Background {
startX: (root.width - wrapper.width) / 2 - rounding
startY: root.height
wrapper: root.panels.launcher
}
Dashboard.Background {
startX: root.width - root.panels.dashboard.width - rounding
startY: 0
wrapper: root.panels.dashboard
}
Utils.Background {
sidebar: sidebar
startX: root.width
startY: root.height
wrapper: root.panels.utilities
}
Sidebar.Background {
id: sidebar
panels: root.panels
startX: root.width
startY: root.panels.notifications.height
wrapper: root.panels.sidebar
}
Settings.Background {
id: settings
startX: (root.width - wrapper.width) / 2 - rounding
startY: 0
wrapper: root.panels.settings
}
Dock.Background {
id: dock
startX: (root.width - wrapper.width) / 2 - rounding
startY: root.height
wrapper: root.panels.dock
}
}
+1
View File
@@ -23,6 +23,7 @@ Canvas {
ctx.save(); ctx.save();
ctx.lineWidth = root.penWidth; ctx.lineWidth = root.penWidth;
ctx.strokeStyle = root.penColor; ctx.strokeStyle = root.penColor;
ctx.lineJoin = "round";
ctx.lineCap = "round"; ctx.lineCap = "round";
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y); ctx.moveTo(points[0].x, points[0].y);
+3 -3
View File
@@ -22,20 +22,20 @@ CustomMouseArea {
} }
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
anchors.fill: root.visibilities.isDrawing ? parent : undefined enabled: z > 0
hoverEnabled: true hoverEnabled: true
visible: root.visibilities.isDrawing visible: root.visibilities.isDrawing
onPositionChanged: event => { onPositionChanged: event => {
const x = event.x; const x = event.x;
const y = event.y; const y = event.y;
if (root.visibilities.isDrawing && (event.buttons & Qt.LeftButton)) { if (root.visibilities.isDrawing && (event.buttons & Qt.LeftButton)) {
root.drawing.points.push(Qt.point(x, y)); root.drawing.points.push(Qt.point(x, y));
root.drawing.requestPaint(); root.drawing.requestPaint();
return;
} }
if (root.inLeftPanel(root.popout, x, y)) { if (!(event.buttons & Qt.LeftButton) && root.inLeftPanel(root.popout, x, y)) {
root.z = -2; root.z = -2;
root.panels.drawing.expanded = true; root.panels.drawing.expanded = true;
} }
+8 -1
View File
@@ -78,7 +78,7 @@ CustomMouseArea {
const dragY = y - dragStart.y; const dragY = y - dragStart.y;
if (root.visibilities.isDrawing && !root.inLeftPanel(root.panels.drawing, x, y)) { if (root.visibilities.isDrawing && !root.inLeftPanel(root.panels.drawing, x, y)) {
// root.input.z = 2; root.input.z = 2;
root.panels.drawing.expanded = false; root.panels.drawing.expanded = false;
} }
@@ -132,6 +132,8 @@ CustomMouseArea {
if (!inDashboardArea) { if (!inDashboardArea) {
root.dashboardShortcutActive = true; root.dashboardShortcutActive = true;
} }
if (root.panels.launcher.x + root.panels.launcher.width > root.panels.dashboardWrapper.x)
root.visibilities.launcher = false;
root.visibilities.settings = false; root.visibilities.settings = false;
root.visibilities.sidebar = false; root.visibilities.sidebar = false;
@@ -161,6 +163,11 @@ CustomMouseArea {
} }
if (root.visibilities.launcher) { if (root.visibilities.launcher) {
if (root.panels.dashboardWrapper.x < root.panels.launcher.x + root.panels.launcher.width) {
console.log("true");
root.visibilities.dashboard = false;
}
root.visibilities.dock = false; root.visibilities.dock = false;
root.visibilities.settings = false; root.visibilities.settings = false;
} }
+24 -12
View File
@@ -305,22 +305,34 @@ Variants {
} }
} }
Drawing { Loader {
id: drawing id: drawingLoader
active: visibilities.isDrawing
anchors.fill: parent anchors.fill: parent
z: 2 z: 2
sourceComponent: Drawing {
id: drawing
}
} }
DrawingInput { Loader {
id: input id: inputLoader
bar: bar active: visibilities.isDrawing
drawing: drawing anchors.fill: parent
panels: panels
popout: panels.drawing
visibilities: visibilities
z: 2 z: 2
sourceComponent: DrawingInput {
id: input
bar: bar
drawing: drawingLoader.item
panels: panels
popout: panels.drawing
visibilities: visibilities
}
} }
Interactions { Interactions {
@@ -328,8 +340,8 @@ Variants {
anchors.fill: parent anchors.fill: parent
bar: bar bar: bar
drawing: drawing drawing: drawingLoader.item
input: input input: inputLoader.item
panels: panels panels: panels
popouts: panels.popouts popouts: panels.popouts
screen: scope.modelData screen: scope.modelData
@@ -340,7 +352,7 @@ Variants {
id: panels id: panels
bar: bar bar: bar
drawingItem: drawing drawingItem: drawingLoader.item
screen: scope.modelData screen: scope.modelData
visibilities: visibilities visibilities: visibilities
+1
View File
@@ -17,6 +17,7 @@ Singleton {
property var disks: [] property var disks: []
property real gpuMemTotal: 0 property real gpuMemTotal: 0
property real gpuMemUsed property real gpuMemUsed
property string gpuName
property real gpuPerc property real gpuPerc
property real gpuTemp property real gpuTemp
readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType
+1 -3
View File
@@ -53,14 +53,12 @@ Searcher {
}; };
root.crops = updated; root.crops = updated;
monitorCrops.writeAdapter();
monitorCrops.reload();
} }
function setWallpaper(path: string): void { function setWallpaper(path: string): void {
actualCurrent = path; actualCurrent = path;
WallpaperPath.currentWallpaperPath = path; WallpaperPath.currentWallpaperPath = path;
Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 0, 0), Qt.rect(0, 0, 0, 0), 1.0)); Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 1, 1), Qt.rect(0, 0, 0, 0), 1.0));
Quickshell.execDetached(["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}`]);
if (Config.general.color.schemeGeneration) if (Config.general.color.schemeGeneration)
Quickshell.execDetached(["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}`]);
-68
View File
@@ -1,68 +0,0 @@
import QtQuick
import QtQuick.Shapes
import qs.Components
import qs.Config
ShapePath {
id: root
readonly property bool flatten: wrapper.height < rounding * 2
property real ibr: invertBottomRounding ? -1 : 1
required property bool invertBottomRounding
property real rounding: Appearance.rounding.smallest
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property Wrapper wrapper
fillColor: DynamicColors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
CAnim {
}
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.roundingY - root.roundingY * root.ibr
}
PathArc {
direction: root.invertBottomRounding ? PathArc.Clockwise : PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY * root.ibr
}
PathLine {
relativeX: root.wrapper.width - root.rounding * 2
relativeY: 0
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY * 2)
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
}
-65
View File
@@ -1,65 +0,0 @@
import qs.Components
import qs.Config
import QtQuick
import QtQuick.Shapes
ShapePath {
id: root
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real rounding: Appearance.rounding.normal
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property Wrapper wrapper
fillColor: DynamicColors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
CAnim {
}
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.roundingY * 2
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: root.wrapper.width - root.rounding * 2
relativeY: 0
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height)
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
}
-66
View File
@@ -1,66 +0,0 @@
import QtQuick
import QtQuick.Shapes
import qs.Components
import qs.Config
ShapePath {
id: root
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real rounding: Appearance.rounding.normal
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property Wrapper wrapper
fillColor: DynamicColors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
CAnim {
}
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY * 2)
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
PathLine {
relativeX: root.wrapper.width - root.rounding * 2
relativeY: 0
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.roundingY * 2
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
}
-66
View File
@@ -1,66 +0,0 @@
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
}
}
+2 -4
View File
@@ -44,11 +44,10 @@ Item {
Loader { Loader {
id: icon id: icon
active: Qt.binding(() => root.shouldBeActive || root.visible) active: root.shouldBeActive || root.visible
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
asynchronous: true asynchronous: true
height: content.contentItem.height
opacity: root.expanded ? 0 : 1 opacity: root.expanded ? 0 : 1
Behavior on opacity { Behavior on opacity {
@@ -64,6 +63,7 @@ Item {
Loader { Loader {
id: content id: content
active: root.shouldBeActive || root.visible
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
asynchronous: true asynchronous: true
@@ -77,7 +77,5 @@ Item {
drawing: root.drawing drawing: root.drawing
visibilities: root.visibilities visibilities: root.visibilities
} }
Component.onCompleted: active = Qt.binding(() => root.shouldBeActive || root.visible)
} }
} }
-66
View File
@@ -1,66 +0,0 @@
import QtQuick
import QtQuick.Shapes
import qs.Components
import qs.Config
ShapePath {
id: root
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real rounding: Appearance.rounding.smallest + 5
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property Wrapper wrapper
fillColor: DynamicColors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
CAnim {
}
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY * 2)
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
PathLine {
relativeX: root.wrapper.width - root.rounding * 2
relativeY: 0
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.roundingY * 2
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
}
+4 -5
View File
@@ -12,12 +12,11 @@ Item {
property int contentHeight property int contentHeight
readonly property real maxHeight: { readonly property real maxHeight: {
let max = screen.height - Appearance.spacing.large * 2; let max = screen.height - Appearance.spacing.large * 2;
if (visibilities.resources) if (visibilities.resources && panels.resourcesWrapper.x + panels.resourcesWrapper.width > root.x)
max -= panels.resources.nonAnimHeight; max -= panels.resources.nonAnimHeight;
if (visibilities.dashboard && panels.dashboard.x < root.x + root.implicitWidth) if (panels.popouts.hasCurrent)
max -= panels.dashboard.nonAnimHeight; if (panels.popouts.current.x + panels.popouts.current.width > root.x && panels.popouts.current.x < root.x + root.width)
if (panels.popouts.currentName.startsWith("updates")) max -= panels.popouts.nonAnimHeight;
max -= panels.popouts.nonAnimHeight;
return max; return max;
} }
property real offsetScale: shouldBeActive ? 0 : 1 property real offsetScale: shouldBeActive ? 0 : 1
-59
View File
@@ -1,59 +0,0 @@
import qs.Components
import qs.Config
import QtQuick
import QtQuick.Shapes
ShapePath {
id: root
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real rounding: 8
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property var sidebar
required property Wrapper wrapper
fillColor: DynamicColors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
CAnim {
}
}
PathLine {
relativeX: -(root.wrapper.width + root.rounding)
relativeY: 0
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.roundingY * 2
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.sidebar.notifsRoundingX
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.sidebar.notifsRoundingX
relativeY: root.roundingY
}
PathLine {
relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.sidebar.notifsRoundingX : root.wrapper.width
relativeY: 0
}
PathArc {
radiusX: root.rounding
radiusY: root.rounding
relativeX: root.rounding
relativeY: root.rounding
}
}
@@ -1,54 +0,0 @@
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 notifsRoundingX: panels.notifications.height > 0 && notifsWidthDiff < rounding * 2 ? notifsWidthDiff / 2 : rounding
readonly property real notifsWidthDiff: panels.notifications.width - wrapper.width
required property var panels
readonly property real rounding: Config.barConfig.rounding
readonly property real utilsRoundingX: utilsWidthDiff < rounding * 2 ? utilsWidthDiff / 2 : rounding
readonly property real utilsWidthDiff: panels.utilities.width - wrapper.width
required property Wrapper wrapper
fillColor: DynamicColors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
CAnim {
}
}
PathLine {
relativeX: -root.wrapper.width - root.notifsRoundingX
relativeY: 0
}
PathArc {
radiusX: root.notifsRoundingX
radiusY: root.rounding
relativeX: root.notifsRoundingX
relativeY: root.rounding
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.rounding * 2
}
PathArc {
radiusX: root.utilsRoundingX
radiusY: root.rounding
relativeX: -root.utilsRoundingX
relativeY: root.rounding
}
PathLine {
relativeX: root.wrapper.width + root.utilsRoundingX
relativeY: 0
}
}
-66
View File
@@ -1,66 +0,0 @@
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: 10
readonly property real roundingX: flatten ? wrapper.width / 2 : rounding
required property Wrapper wrapper
fillColor: DynamicColors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
CAnim {
}
}
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 * 3)
relativeY: 0
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: Math.min(root.rounding * 2, root.wrapper.width)
radiusY: root.rounding * 2
relativeX: -root.roundingX * 2
relativeY: root.rounding * 2
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.rounding * 4
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: Math.min(root.rounding * 2, root.wrapper.width)
radiusY: root.rounding * 2
relativeX: root.roundingX * 2
relativeY: root.rounding * 2
}
PathLine {
relativeX: root.wrapper.width - root.roundingX * 3
relativeY: 0
}
PathArc {
radiusX: Math.min(root.rounding, root.wrapper.width)
radiusY: root.rounding
relativeX: root.roundingX
relativeY: root.rounding
}
}
-65
View File
@@ -1,65 +0,0 @@
import qs.Components
import qs.Config
import QtQuick
import QtQuick.Shapes
ShapePath {
id: root
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real rounding: Appearance.rounding.normal
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property Wrapper wrapper
fillColor: DynamicColors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
CAnim {
}
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
PathLine {
relativeX: root.wrapper.width - root.rounding * 2
relativeY: 0
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY * 2)
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
}
+2 -3
View File
@@ -8,7 +8,7 @@ import qs.Config
Item { Item {
id: root id: root
readonly property real nonAnimHeight: state === "visible" ? (content.item?.nonAnimHeight ?? 0) : 0 readonly property real nonAnimHeight: content.item?.nonAnimHeight ?? 0
property real offsetScale: shouldBeActive ? 0 : 1 property real offsetScale: shouldBeActive ? 0 : 1
readonly property bool shouldBeActive: root.visibilities.resources readonly property bool shouldBeActive: root.visibilities.resources
required property PersistentProperties visibilities required property PersistentProperties visibilities
@@ -31,8 +31,7 @@ Item {
id: content id: content
active: root.shouldBeActive || root.visible active: root.shouldBeActive || root.visible
anchors.bottom: parent.bottom anchors.centerIn: parent
anchors.horizontalCenter: parent.horizontalCenter
sourceComponent: Content { sourceComponent: Content {
padding: Appearance.padding.normal padding: Appearance.padding.normal
-66
View File
@@ -1,66 +0,0 @@
import QtQuick
import QtQuick.Shapes
import qs.Components
import qs.Config
ShapePath {
id: root
readonly property bool flatten: wrapper.height < rounding * 2
readonly property real rounding: Appearance.rounding.large
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
required property Wrapper wrapper
fillColor: DynamicColors.palette.m3surface
strokeWidth: -1
Behavior on fillColor {
CAnim {
}
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.roundingY, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: 0
relativeY: root.wrapper.height - root.roundingY * 2
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: root.roundingY
}
PathLine {
relativeX: root.wrapper.width - root.rounding * 2
relativeY: 0
}
PathArc {
direction: PathArc.Counterclockwise
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
PathLine {
relativeX: 0
relativeY: -(root.wrapper.height - root.roundingY * 2)
}
PathArc {
radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height)
relativeX: root.rounding
relativeY: -root.roundingY
}
}
+15
View File
@@ -56,6 +56,21 @@ SettingsPage {
} }
} }
SettingsSection {
sectionId: "Tray"
SettingsHeader {
name: "System tray"
}
SettingSpinBox {
min: 16
name: "Tray icon size"
object: Config.barConfig.tray
setting: "trayIconSize"
}
}
SettingsSection { SettingsSection {
sectionId: "Popouts" sectionId: "Popouts"
+92 -60
View File
@@ -80,12 +80,26 @@ Item {
required property ShellScreen modelData required property ShellScreen modelData
function applyCrop(): void { function applyCrop(): void {
const croprect = cropRect.mapToItem(scaledImg, 0, 0, cropRect.width, cropRect.height); if (!cropRectLoader.item) return;
const upscaledRect = Qt.rect((croprect.x - cropRect.imageX) / scaledImg.paintedWidth, (croprect.y - cropRect.imageY) / scaledImg.paintedHeight, croprect.width / scaledImg.paintedWidth, croprect.height / scaledImg.paintedHeight); const cropRect = cropRectLoader.item;
Wallpapers.setCrop(delegate.modelData.name, upscaledRect, croprect, cropRect.zoom);
// We need to calculate the exact percentage coordinates that map perfectly
// to our C++ backend, regardless of current display scaling
const cropXPercent = (cropRect.x - cropRect.imageX) / scaledImg.paintedWidth;
const cropYPercent = (cropRect.y - cropRect.imageY) / scaledImg.paintedHeight;
const cropWidthPercent = cropRect.width / scaledImg.paintedWidth;
const cropHeightPercent = cropRect.height / scaledImg.paintedHeight;
const finalRect = Qt.rect(cropXPercent, cropYPercent, cropWidthPercent, cropHeightPercent);
// We just pass the percentages directly to the backend
Wallpapers.setCrop(delegate.modelData.name, finalRect, finalRect, cropRect.zoom);
} }
function zoomClipRect(zoom: real): void { function zoomClipRect(zoom: real): void {
if (!cropRectLoader.item) return;
const cropRect = cropRectLoader.item;
let oldCenterX = cropRect.x + cropRect.width * 0.5; let oldCenterX = cropRect.x + cropRect.width * 0.5;
let oldCenterY = cropRect.y + cropRect.height * 0.5; let oldCenterY = cropRect.y + cropRect.height * 0.5;
@@ -128,7 +142,7 @@ Item {
Layout.preferredHeight: 10 Layout.preferredHeight: 10
from: 1.0 from: 1.0
to: 5.0 to: 5.0
value: cropRect.zoom value: cropRectLoader.item ? cropRectLoader.item.zoom : 1.0
onMoved: { onMoved: {
delegate.zoomClipRect(value); delegate.zoomClipRect(value);
@@ -156,15 +170,20 @@ Item {
sourceSize.width: parent.width sourceSize.width: parent.width
onPaintedWidthChanged: { onPaintedWidthChanged: {
if (paintedWidth > 0) { if (paintedWidth > 0 && cropRectLoader.item) {
scaledImg.displayData = Wallpapers.getCrop(delegate.modelData.name); cropRectLoader.item.restoreFromData();
cropRect.zoom = Wallpapers.getCrop(delegate.modelData.name).zoom; }
cropRect.restoreFromData(); }
onSourceChanged: {
if (cropRectLoader.item) {
cropRectLoader.item.restoreFromData();
}
}
onStatusChanged: {
if (scaledImg.status == Image.Ready && cropRectLoader.item) {
cropRectLoader.item.restoreFromData();
} }
} }
onSourceChanged: cropRect.clampToBounds()
onStatusChanged: if (scaledImg.status == Image.Ready)
cropRect.clampToBounds()
CustomText { CustomText {
id: monitorId id: monitorId
@@ -177,72 +196,85 @@ Item {
text: delegate.modelData.name text: delegate.modelData.name
} }
CustomRect { Loader {
id: cropRect id: cropRectLoader
active: scaledImg.paintedWidth > 0 && scaledImg.status == Image.Ready
sourceComponent: Component {
CustomRect {
id: cropRect
property real aspectRatio: delegate.modelData.width / delegate.modelData.height property real aspectRatio: delegate.modelData.width / delegate.modelData.height
readonly property real baseHeight: baseWidth / aspectRatio readonly property real baseHeight: baseWidth / aspectRatio
readonly property real baseWidth: { readonly property real baseWidth: {
let fittedHeight = scaledImg.paintedHeight; let fittedHeight = scaledImg.paintedHeight;
let fittedWidth = fittedHeight * aspectRatio; let fittedWidth = fittedHeight * aspectRatio;
if (fittedWidth > scaledImg.paintedWidth) { if (fittedWidth > scaledImg.paintedWidth) {
fittedWidth = scaledImg.paintedWidth; fittedWidth = scaledImg.paintedWidth;
fittedHeight = fittedWidth / aspectRatio; fittedHeight = fittedWidth / aspectRatio;
} }
return fittedWidth; return fittedWidth;
} }
readonly property real imageX: (scaledImg.width - scaledImg.paintedWidth) / 2 readonly property real imageX: (scaledImg.width - scaledImg.paintedWidth) / 2
readonly property real imageY: (scaledImg.height - scaledImg.paintedHeight) / 2 readonly property real imageY: (scaledImg.height - scaledImg.paintedHeight) / 2
property real imgAspectRatio: scaledImg.paintedWidth / scaledImg.paintedHeight property real imgAspectRatio: scaledImg.paintedWidth / scaledImg.paintedHeight
property real zoom: scaledImg.displayData.zoom property real zoom: 1.0
function centerInImage() { function centerInImage() {
x = imageX + (scaledImg.paintedWidth - width) / 2; x = imageX + (scaledImg.paintedWidth - width) / 2;
y = imageY + (scaledImg.paintedHeight - height) / 2; y = imageY + (scaledImg.paintedHeight - height) / 2;
} }
function clampToBounds() { function clampToBounds() {
x = Math.max(imageX, Math.min(x, imageX + scaledImg.paintedWidth - width)); x = Math.max(imageX, Math.min(x, imageX + scaledImg.paintedWidth - width));
y = Math.max(imageY, Math.min(y, imageY + scaledImg.paintedHeight - height)); y = Math.max(imageY, Math.min(y, imageY + scaledImg.paintedHeight - height));
} }
function restoreFromData() { function restoreFromData() {
let data = scaledImg.displayData; let data = Wallpapers.getCrop(delegate.modelData.name);
if (data && data.scaledX !== 0 || data.scaledY !== 0 || data.scaledWidth !== 0 || data.scaledHeight !== 0) { if (data && (Math.abs(data.x) > 0.001 || Math.abs(data.y) > 0.001 || Math.abs(data.width - 1.0) > 0.001 || Math.abs(data.height - 1.0) > 0.001)) {
x = data.scaledX; zoom = data.zoom > 0 ? data.zoom : 1.0;
y = data.scaledY; x = imageX + (data.x * scaledImg.paintedWidth);
y = imageY + (data.y * scaledImg.paintedHeight);
clampToBounds();
} else {
zoom = 1.0;
centerInImage();
}
}
clampToBounds(); border.color: DynamicColors.palette.m3primary
} else { border.width: 2
zoom = 1.0; height: baseHeight / zoom
centerInImage(); opacity: 1
width: baseWidth / zoom
Behavior on opacity {
Anim {
}
}
Component.onCompleted: {
restoreFromData();
}
onHeightChanged: clampToBounds()
onWidthChanged: clampToBounds()
} }
} }
border.color: DynamicColors.palette.m3primary
border.width: 2
height: baseHeight / zoom
opacity: 1
width: baseWidth / zoom
Behavior on opacity {
Anim {
}
}
Component.onCompleted: clampToBounds()
onHeightChanged: clampToBounds()
onWidthChanged: clampToBounds()
} }
MouseArea { MouseArea {
id: mouse id: mouse
function updateCrop(mouseX, mouseY) { function updateCrop(mouseX, mouseY) {
if (!cropRectLoader.item) return;
const cropRect = cropRectLoader.item;
let nx = mouseX - cropRect.width * 0.5; let nx = mouseX - cropRect.width * 0.5;
let ny = mouseY - cropRect.height * 0.5; let ny = mouseY - cropRect.height * 0.5;
+1 -2
View File
@@ -32,10 +32,9 @@ Item {
Loader { Loader {
id: content id: content
active: true active: root.shouldBeActive || root.visible
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
visible: true
sourceComponent: Content { sourceComponent: Content {
screen: root.screen screen: root.screen
+32 -4
View File
@@ -3,6 +3,7 @@ import QtQuick
import QtQuick.VectorImage import QtQuick.VectorImage
import Quickshell import Quickshell
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
import qs.Helpers
import qs.Modules import qs.Modules
import qs.Components import qs.Components
import qs.Config import qs.Config
@@ -11,12 +12,36 @@ Item {
id: root id: root
property bool current: popouts.currentName.startsWith(`traymenu${ind}`) && popouts.hasCurrent property bool current: popouts.currentName.startsWith(`traymenu${ind}`) && popouts.hasCurrent
property bool hasLoaded: false readonly property real dpr: Hypr.monitorFor(loader.screen).scale
required property int ind required property int ind
required property SystemTrayItem item required property SystemTrayItem item
required property RowLayout loader required property RowLayout loader
required property Wrapper popouts required property Wrapper popouts
function resolveIcon(app: string, icon: string): string {
if (app === "chrome_status_icon_1") {
return Quickshell.iconPath("discord-tray");
} else if (app === "AyuGramDesktop") {
if (icon === Quickshell.iconPath("com.ayugram.desktop-attention-symbolic"))
return Quickshell.iconPath("telegram-attention-panel");
else if (icon === Quickshell.iconPath("com.ayugram.desktop-mute-symbolic"))
return Quickshell.iconPath("telegram-mute-panel");
else if (icon === Quickshell.iconPath("com.ayugram.desktop-symbolic"))
return Quickshell.iconPath("telegram-panel");
} else if (app === "TelegramDesktop") {
if (icon === Quickshell.iconPath("org.telegram.desktop-symbolic"))
return Quickshell.iconPath("telegram-panel");
else if (icon === Quickshell.iconPath("org.telegram.desktop-attention-symbolic"))
return Quickshell.iconPath("telegram-attention-panel");
else if (icon === Quickshell.iconPath("org.telegram.desktop-mute-symbolic"))
return Quickshell.iconPath("telegram-mute-panel");
} else if (app === "steam") {
return Quickshell.iconPath("steam_tray_mono");
}
return root.item.icon;
}
CustomRect { CustomRect {
anchors.fill: parent anchors.fill: parent
anchors.margins: 3 anchors.margins: 3
@@ -30,7 +55,8 @@ Item {
onClicked: { onClicked: {
if (mouse.button === Qt.LeftButton) { if (mouse.button === Qt.LeftButton) {
root.item.activate(); root.item.activate();
} else if (mouse.button === Qt.RightButton) { console.log(icon.source + "\n" + root.item.id);
} else if (mouse.button === Qt.RightButton && Config.barConfig.popouts.tray) {
root.popouts.currentName = `traymenu${root.ind}`; root.popouts.currentName = `traymenu${root.ind}`;
root.popouts.currentCenter = Qt.binding(() => root.mapToItem(root.loader, root.implicitWidth / 2, 0).x); root.popouts.currentCenter = Qt.binding(() => root.mapToItem(root.loader, root.implicitWidth / 2, 0).x);
root.popouts.hasCurrent = true; root.popouts.hasCurrent = true;
@@ -48,9 +74,11 @@ Item {
id: icon id: icon
anchors.centerIn: parent anchors.centerIn: parent
antialiasing: true
color: root.current ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface color: root.current ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
implicitSize: 22 implicitSize: Config.barConfig.tray.trayIconSize * root.dpr
layer.enabled: Config.general.color.smart || Config.general.color.scheduleDark layer.enabled: Config.general.color.smart || Config.general.color.scheduleDark
source: root.item.icon scale: 1 / root.dpr
source: root.resolveIcon(root.item.id, root.item.icon)
} }
} }
+2 -2
View File
@@ -23,12 +23,12 @@ RowLayout {
let modRowPos = sysTrayMod.mapToItem(sysModRow, modPos.x, modPos.y); let modRowPos = sysTrayMod.mapToItem(sysModRow, modPos.x, modPos.y);
let child = sysModRow.childAt(modRowPos.x, modRowPos.y); let child = sysModRow.childAt(modRowPos.x, modRowPos.y);
if (child) { if (child) {
if (child.objectName === "audioWidget") if (child.objectName === "audioWidget" && Config.barConfig.popouts.audio)
return { return {
id: "audio", id: "audio",
item: child item: child
}; };
if (child.objectName === "upowerWidget") if (child.objectName === "upowerWidget" && Config.barConfig.popouts.upower)
return { return {
id: "upower", id: "upower",
item: child item: child
+122 -111
View File
@@ -11,150 +11,161 @@ import qs.Helpers
CustomClippingRect { CustomClippingRect {
id: root id: root
readonly property bool hasUpdates: Object.keys(Updates.updates)?.length > 0
readonly property int itemHeight: 50 + Appearance.padding.smaller * 2 readonly property int itemHeight: 50 + Appearance.padding.smaller * 2
required property var wrapper required property var wrapper
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: updatesList.visible ? updatesList.implicitHeight + Appearance.padding.small * 2 : noUpdates.height implicitHeight: hasUpdates ? updatesListLoader.item?.implicitHeight + Appearance.padding.small * 2 : noUpdatesLoader.item.height
implicitWidth: updatesList.visible ? updatesList.contentWidth + Appearance.padding.small * 2 : noUpdates.width implicitWidth: hasUpdates ? updatesListLoader.item?.contentWidth + Appearance.padding.small * 2 : noUpdatesLoader.item.width
radius: Appearance.rounding.small radius: Appearance.rounding.small
Item { Loader {
id: noUpdates id: noUpdatesLoader
active: !root.hasUpdates
anchors.centerIn: parent anchors.centerIn: parent
height: 200
visible: script.values.length === 0
width: 600
MaterialIcon { sourceComponent: Item {
id: noUpdatesIcon id: noUpdates
anchors.horizontalCenter: parent.horizontalCenter height: 200
anchors.top: parent.top width: 300
color: DynamicColors.tPalette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.extraLarge * 3
horizontalAlignment: Text.AlignHCenter
text: "check"
}
CustomText { MaterialIcon {
anchors.horizontalCenter: parent.horizontalCenter id: noUpdatesIcon
anchors.top: noUpdatesIcon.bottom
color: DynamicColors.tPalette.m3onSurfaceVariant anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter anchors.top: parent.top
text: qsTr("No updates available") color: DynamicColors.tPalette.m3onSurfaceVariant
verticalAlignment: Text.AlignVCenter font.pointSize: Appearance.font.size.extraLarge * 3
horizontalAlignment: Text.AlignHCenter
text: "check"
}
CustomText {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: noUpdatesIcon.bottom
color: DynamicColors.tPalette.m3onSurfaceVariant
horizontalAlignment: Text.AlignHCenter
text: qsTr("No updates available")
verticalAlignment: Text.AlignVCenter
}
} }
} }
CustomListView { Loader {
id: updatesList id: updatesListLoader
active: root.hasUpdates
anchors.centerIn: parent anchors.centerIn: parent
contentHeight: childrenRect.height
contentWidth: 600
displayMarginBeginning: root.itemHeight
displayMarginEnd: root.itemHeight
implicitHeight: Math.min(contentHeight, (root.itemHeight + spacing) * 5 - spacing)
implicitWidth: contentWidth
spacing: Appearance.spacing.normal
visible: script.values.length > 0
delegate: CustomRect { sourceComponent: CustomListView {
id: update id: updatesList
required property var modelData contentHeight: childrenRect.height
readonly property list<string> sections: modelData.update.split(" ") contentWidth: 600
displayMarginBeginning: root.itemHeight
displayMarginEnd: root.itemHeight
implicitHeight: Math.min(contentHeight, (root.itemHeight + spacing) * 5 - spacing)
implicitWidth: contentWidth
spacing: Appearance.spacing.normal
// anchors.left: parent.left delegate: CustomRect {
// anchors.right: parent.right id: update
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: root.itemHeight
implicitWidth: 600
radius: Appearance.rounding.small - Appearance.padding.small
RowLayout { required property var modelData
anchors.fill: parent readonly property list<string> sections: modelData.update.split(" ")
anchors.leftMargin: Appearance.padding.smaller
anchors.rightMargin: Appearance.padding.smaller
MaterialIcon { // anchors.left: parent.left
font.pointSize: Appearance.font.size.large * 2 // anchors.right: parent.right
text: "package_2" color: DynamicColors.tPalette.m3surfaceContainer
} implicitHeight: root.itemHeight
implicitWidth: 600
ColumnLayout { radius: Appearance.rounding.small - Appearance.padding.small
Layout.fillWidth: true
CustomText {
Layout.fillWidth: true
Layout.preferredHeight: 25
elide: Text.ElideRight
font.pointSize: Appearance.font.size.large
text: update.sections[0]
}
CustomText {
Layout.fillWidth: true
color: DynamicColors.palette.m3onSurfaceVariant
text: Updates.formatUpdateTime(update.modelData.timestamp)
}
}
RowLayout { RowLayout {
Layout.fillHeight: true anchors.fill: parent
Layout.preferredWidth: 300 anchors.leftMargin: Appearance.padding.smaller
anchors.rightMargin: Appearance.padding.smaller
MarqueeText {
id: versionFrom
Layout.fillHeight: true
Layout.preferredWidth: 125
animate: true
color: DynamicColors.palette.m3tertiary
font.pointSize: Appearance.font.size.large
horizontalAlignment: Text.AlignHCenter
marqueeEnabled: true
pauseMs: 4000
text: update.sections[1]
width: 125
}
MaterialIcon { MaterialIcon {
Layout.fillHeight: true font.pointSize: Appearance.font.size.large * 2
color: DynamicColors.palette.m3secondary text: "package_2"
font.pointSize: Appearance.font.size.extraLarge
horizontalAlignment: Text.AlignHCenter
text: "arrow_right_alt"
verticalAlignment: Text.AlignVCenter
} }
MarqueeText { ColumnLayout {
id: versionTo Layout.fillWidth: true
CustomText {
Layout.fillWidth: true
Layout.preferredHeight: 25
elide: Text.ElideRight
font.pointSize: Appearance.font.size.large
text: update.sections[0]
}
CustomText {
Layout.fillWidth: true
color: DynamicColors.palette.m3onSurfaceVariant
text: Updates.formatUpdateTime(update.modelData.timestamp)
}
}
RowLayout {
Layout.fillHeight: true Layout.fillHeight: true
Layout.preferredWidth: 120 Layout.preferredWidth: 300
animate: true
color: DynamicColors.palette.m3primary MarqueeText {
font.pointSize: Appearance.font.size.large id: versionFrom
horizontalAlignment: Text.AlignHCenter
marqueeEnabled: true Layout.fillHeight: true
pauseMs: 4000 Layout.preferredWidth: 125
text: update.sections[3] animate: true
width: 125 color: DynamicColors.palette.m3tertiary
font.pointSize: Appearance.font.size.large
horizontalAlignment: Text.AlignHCenter
marqueeEnabled: true
pauseMs: 4000
text: update.sections[1]
width: 125
}
MaterialIcon {
Layout.fillHeight: true
color: DynamicColors.palette.m3secondary
font.pointSize: Appearance.font.size.extraLarge
horizontalAlignment: Text.AlignHCenter
text: "arrow_right_alt"
verticalAlignment: Text.AlignVCenter
}
MarqueeText {
id: versionTo
Layout.fillHeight: true
Layout.preferredWidth: 120
animate: true
color: DynamicColors.palette.m3primary
font.pointSize: Appearance.font.size.large
horizontalAlignment: Text.AlignHCenter
marqueeEnabled: true
pauseMs: 4000
text: update.sections[3]
width: 125
}
} }
} }
} }
} model: ScriptModel {
model: ScriptModel { id: script
id: script
objectProp: "update" objectProp: "update"
values: Object.entries(Updates.updates).sort((a, b) => b[1] - a[1]).map(([update, timestamp]) => ({ values: Object.entries(Updates.updates).sort((a, b) => b[1] - a[1]).map(([update, timestamp]) => ({
update, update,
timestamp timestamp
})) }))
}
} }
} }
} }
-80
View File
@@ -1,80 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Components
import qs.Helpers
import qs.Config
Item {
id: root
property Image current: one
property string source: Wallpapers.current
anchors.fill: parent
Component.onCompleted: {
if (source)
Qt.callLater(() => one.update());
}
onSourceChanged: {
if (!source) {
current = null;
} else if (current === one) {
two.update();
} else {
one.update();
}
}
Img {
id: one
}
Img {
id: two
}
component Img: Image {
id: img
function update(): void {
if (source === root.source) {
root.current = this;
} else {
source = root.source;
}
}
anchors.fill: parent
asynchronous: true
fillMode: Image.PreserveAspectCrop
opacity: 0
retainWhileLoading: true
scale: Wallpapers.showPreview ? 1 : 0.8
sourceClipRect: Qt.rect(Config.background.sourceClipX, Config.background.sourceClipY, Config.background.sourceClipW, Config.background.sourceClipH)
states: State {
name: "visible"
when: root.current === img
PropertyChanges {
img.opacity: 1
img.scale: 1
}
}
transitions: Transition {
Anim {
duration: Config.background.wallFadeDuration
properties: "opacity,scale"
target: img
}
}
onStatusChanged: {
if (status === Image.Ready) {
root.current = this;
}
}
}
}
+37 -30
View File
@@ -6,6 +6,7 @@ import QtQuick
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
import qs.Config import qs.Config
import ZShell.Internal
Item { Item {
id: root id: root
@@ -15,58 +16,64 @@ Item {
function refreshData(): void { function refreshData(): void {
Hyprland.refreshMonitors(); Hyprland.refreshMonitors();
const scale = Hyprland.monitorFor(root.screen).scale; let scale = Hyprland.monitorFor(root.screen).scale;
if (scale > 0 && img.resScale !== scale) { if (scale <= 0)
img.resScale = scale; scale = 1.0; // Fallback to avoid zeroes on initialization
img.sourceSize.width = root.screen.width * scale;
if (root.screen.width > 0 && root.screen.height > 0) {
img.screenResolution = Qt.size(root.screen.width * scale, root.screen.height * scale);
} }
const displayData = Wallpapers.getCrop(root.screen.name); const displayData = Wallpapers.getCrop(root.screen.name);
const displayRect = Qt.rect(img.sourceSize.width * displayData.x, img.implicitHeight * displayData.y, img.sourceSize.width * displayData.width, img.implicitHeight * displayData.height);
img.anchors.fill = null; if (displayData) {
img.zoom = displayData.zoom; img.cropX = displayData.x !== undefined ? displayData.x : 0.0;
img.x = -(displayRect.x * displayData.zoom / img.resScale); img.cropY = displayData.y !== undefined ? displayData.y : 0.0;
img.y = -(displayRect.y * displayData.zoom / img.resScale); img.cropWidth = (displayData.width !== undefined && displayData.width > 0) ? displayData.width : 1.0;
img.cropHeight = (displayData.height !== undefined && displayData.height > 0) ? displayData.height : 1.0;
}
} }
anchors.fill: parent anchors.fill: parent
Image { Component.onCompleted: root.refreshData()
Connections {
function onHeightChanged() {
root.refreshData();
}
function onWidthChanged() {
root.refreshData();
}
target: root.screen
}
WallpaperImage {
id: img id: img
property int displayH anchors.fill: parent
property int displayW
property real resScale
property real zoom: 1.0
asynchronous: true
fillMode: Image.PreserveAspectCrop
height: implicitHeight * zoom / resScale
opacity: 1
retainWhileLoading: true
source: root.source source: root.source
sourceSize.width: root.screen.width * resScale
width: implicitWidth * zoom / resScale
Behavior on height { Behavior on cropHeight {
Anim { Anim {
} }
} }
Behavior on width { Behavior on cropWidth {
Anim { Anim {
} }
} }
Behavior on x { Behavior on cropX {
Anim { Anim {
} }
} }
Behavior on y { Behavior on cropY {
Anim { Anim {
} }
} }
Behavior on zoom {
onStatusChanged: { Anim {
if (img.status == Image.Ready) {
root.refreshData();
} }
} }
+4 -49
View File
@@ -15,9 +15,8 @@ Item {
property real currentCenter property real currentCenter
property alias currentName: popoutState.currentName property alias currentName: popoutState.currentName
property string detachedMode property string detachedMode
readonly property bool isDetached: detachedMode.length > 0
property alias hasCurrent: popoutState.hasCurrent property alias hasCurrent: popoutState.hasCurrent
readonly property real nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight readonly property real nonAnimHeight: content.implicitHeight || 150
readonly property real nonAnimWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth readonly property real nonAnimWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth
required property real offsetScale required property real offsetScale
property string queuedMode property string queuedMode
@@ -28,29 +27,13 @@ Item {
detachedMode = ""; detachedMode = "";
} }
function detach(mode: string): void {
setAnims(true);
if (mode === "winfo") {
detachedMode = mode;
} else {
queuedMode = mode;
detachedMode = "any";
}
setAnims(false);
focus = true;
}
function setAnims(detach: bool): void {
const type = `expressive${detach ? "Slow" : "Default"}Spatial`;
animLength = Appearance.anim.durations[type];
animCurve = Appearance.anim.curves[type];
}
focus: hasCurrent focus: hasCurrent
implicitHeight: nonAnimHeight implicitHeight: nonAnimHeight
implicitWidth: nonAnimWidth implicitWidth: nonAnimWidth
Behavior on implicitHeight { Behavior on implicitHeight {
enabled: root.offsetScale < 1
Anim { Anim {
duration: root.animLength duration: root.animLength
easing.bezierCurve: root.animCurve easing.bezierCurve: root.animCurve
@@ -72,42 +55,14 @@ Item {
Comp { Comp {
id: content id: content
// anchors.horizontalCenter: parent.horizontalCenter
// anchors.top: parent.top
anchors.centerIn: parent anchors.centerIn: parent
shouldBeActive: root.hasCurrent && !root.detachedMode shouldBeActive: root.hasCurrent
sourceComponent: Content { sourceComponent: Content {
popouts: popoutState popouts: popoutState
} }
} }
// Comp {
// id: winfo
//
// anchors.centerIn: parent
// shouldBeActive: root.detachedMode === "winfo"
//
// sourceComponent: WindowInfo {
// client: Hypr.activeToplevel
// screen: root.screen
// }
// }
//
// Comp {
// id: controlCenter
//
// anchors.centerIn: parent
// shouldBeActive: root.detachedMode === "any"
//
// sourceComponent: ControlCenter {
// active: root.queuedMode
// screen: root.screen
//
// onClose: root.close()
// }
// }
component Comp: Loader { component Comp: Loader {
id: comp id: comp
+1
View File
@@ -8,6 +8,7 @@ qml_module(ZShell-internal
circularbuffer.hpp circularbuffer.cpp circularbuffer.hpp circularbuffer.cpp
sparklineitem.hpp sparklineitem.cpp sparklineitem.hpp sparklineitem.cpp
arcgauge.hpp arcgauge.cpp arcgauge.hpp arcgauge.cpp
wallpaperimage.hpp wallpaperimage.cpp
LIBRARIES LIBRARIES
Qt::Gui Qt::Gui
Qt::Quick Qt::Quick
+2 -2
View File
@@ -4,7 +4,7 @@
#include <qpainter.h> #include <qpainter.h>
#include <qpen.h> #include <qpen.h>
namespace caelestia::internal { namespace ZShell::internal {
ArcGauge::ArcGauge(QQuickItem* parent) ArcGauge::ArcGauge(QQuickItem* parent)
: QQuickPaintedItem(parent) { : QQuickPaintedItem(parent) {
@@ -116,4 +116,4 @@ void ArcGauge::setLineWidth(qreal width) {
update(); update();
} }
} // namespace caelestia::internal } // namespace ZShell::internal
+2 -2
View File
@@ -5,7 +5,7 @@
#include <qqmlintegration.h> #include <qqmlintegration.h>
#include <qquickpainteditem.h> #include <qquickpainteditem.h>
namespace caelestia::internal { namespace ZShell::internal {
class ArcGauge : public QQuickPaintedItem { class ArcGauge : public QQuickPaintedItem {
Q_OBJECT Q_OBJECT
@@ -58,4 +58,4 @@ qreal m_sweepAngle = 1.5 * M_PI;
qreal m_lineWidth = 10.0; qreal m_lineWidth = 10.0;
}; };
} // namespace caelestia::internal } // namespace ZShell::internal
+199
View File
@@ -0,0 +1,199 @@
#include "wallpaperimage.hpp"
#include <QStandardPaths>
#include <QCryptographicHash>
#include <QDir>
#include <QFileInfo>
#include <QtConcurrent>
#include <QSGImageNode>
#include <QQuickWindow>
namespace ZShell::internal {
WallpaperImage::WallpaperImage(QQuickItem *parent)
: QQuickItem(parent)
{
setFlag(ItemHasContents, true);
connect(&m_imageWatcher, &QFutureWatcher<QImage>::finished, this, &WallpaperImage::handleImageLoaded);
}
WallpaperImage::~WallpaperImage() {
if (m_texture) delete m_texture;
}
void WallpaperImage::setSource(const QUrl &source) {
if (m_source == source) return;
m_source = source;
emit sourceChanged();
loadImage();
}
void WallpaperImage::setScreenResolution(const QSize &screenResolution) {
if (m_screenResolution == screenResolution) return;
m_screenResolution = screenResolution;
emit screenResolutionChanged();
loadImage();
}
void WallpaperImage::setZoom(qreal zoom) {
if (qFuzzyCompare(m_zoom, zoom)) return;
m_zoom = zoom;
emit zoomChanged();
update();
}
void WallpaperImage::setCropX(qreal x) {
if (qFuzzyCompare(m_cropX, x)) return;
m_cropX = x;
emit cropXChanged();
update();
}
void WallpaperImage::setCropY(qreal y) {
if (qFuzzyCompare(m_cropY, y)) return;
m_cropY = y;
emit cropYChanged();
update();
}
void WallpaperImage::setCropWidth(qreal w) {
if (w <= 0.0) w = 1.0;
if (qFuzzyCompare(m_cropWidth, w)) return;
m_cropWidth = w;
emit cropWidthChanged();
update();
}
void WallpaperImage::setCropHeight(qreal h) {
if (h <= 0.0) h = 1.0;
if (qFuzzyCompare(m_cropHeight, h)) return;
m_cropHeight = h;
emit cropHeightChanged();
update();
}
QString WallpaperImage::getCacheFilePath() const {
if (m_source.isEmpty() || m_screenResolution.isEmpty()) return QString();
QString cachePath = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + "/zshell/imagecache";
QDir().mkpath(cachePath);
// Hash the source URL + resolution
QString id = m_source.toString() + "_" + QString::number(m_screenResolution.width()) + "x" + QString::number(m_screenResolution.height());
QByteArray hash = QCryptographicHash::hash(id.toUtf8(), QCryptographicHash::Md5).toHex();
return cachePath + "/" + hash + ".png";
}
void WallpaperImage::loadImage() {
if (m_source.isEmpty()) return;
QString cacheFile = getCacheFilePath();
QString sourceFile = m_source.isLocalFile() ? m_source.toLocalFile() : m_source.toString();
// Qt resource path correction if passed as a standard URL string
if (sourceFile.startsWith("qrc:/")) {
sourceFile = sourceFile.mid(3); // Converts "qrc:/" to ":/"
}
QSize targetRes = m_screenResolution;
// Run off the main thread to avoid blocking the UI
QFuture<QImage> future = QtConcurrent::run([sourceFile, cacheFile, targetRes]() -> QImage {
if (!targetRes.isEmpty() && !cacheFile.isEmpty() && QFileInfo::exists(cacheFile)) {
QImage cached(cacheFile);
if (!cached.isNull()) return cached;
}
QImage original(sourceFile);
if (original.isNull()) return QImage();
if (targetRes.isEmpty()) {
// Screen resolution not set yet by QML, return the unscaled original for now to prevent a black screen
return original;
}
// Check if original is strictly larger than screen resolution
if (original.width() > targetRes.width() || original.height() > targetRes.height()) {
QImage scaled = original.scaled(targetRes, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
if (!cacheFile.isEmpty()) scaled.save(cacheFile, "PNG");
return scaled;
}
// Otherwise just cache and return the original
if (!cacheFile.isEmpty()) original.save(cacheFile, "PNG");
return original;
});
m_imageWatcher.setFuture(future);
}
void WallpaperImage::handleImageLoaded() {
m_image = m_imageWatcher.result();
m_textureDirty = true;
update(); // Request redraw
}
QSGNode *WallpaperImage::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) {
auto *node = static_cast<QSGImageNode *>(oldNode);
if (m_image.isNull()) {
delete node;
return nullptr;
}
if (!node) {
node = window()->createImageNode();
}
if (m_textureDirty) {
if (m_texture) delete m_texture;
m_texture = window()->createTextureFromImage(m_image, QQuickWindow::TextureHasAlphaChannel);
m_textureDirty = false;
}
if (m_texture) {
node->setTexture(m_texture);
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;
// Force 'PreserveAspectCrop' behavior on the requested region
if (reqRatio > targetRatio) {
// Requested region is too wide, center-crop the sides
qreal newWidth = reqRect.height() * targetRatio;
qreal xOffset = (reqRect.width() - newWidth) / 2.0;
sourceRect.setX(reqRect.x() + xOffset);
sourceRect.setWidth(newWidth);
} else if (reqRatio < targetRatio) {
// Requested region is too tall, center-crop the top/bottom
qreal newHeight = reqRect.width() / targetRatio;
qreal yOffset = (reqRect.height() - newHeight) / 2.0;
sourceRect.setY(reqRect.y() + yOffset);
sourceRect.setHeight(newHeight);
}
node->setSourceRect(sourceRect);
}
return node;
}
} // namespace ZShell::internal
@@ -0,0 +1,95 @@
#pragma once
#include <QQuickItem>
#include <QImage>
#include <QUrl>
#include <QSGTexture>
#include <QFutureWatcher>
#include <QtQml/qqml.h>
namespace ZShell::internal {
class WallpaperImage : public QQuickItem {
Q_OBJECT
QML_NAMED_ELEMENT(WallpaperImage)
Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged)
Q_PROPERTY(QSize screenResolution READ screenResolution WRITE setScreenResolution NOTIFY screenResolutionChanged)
Q_PROPERTY(qreal zoom READ zoom WRITE setZoom NOTIFY zoomChanged)
Q_PROPERTY(qreal cropX READ cropX WRITE setCropX NOTIFY cropXChanged)
Q_PROPERTY(qreal cropY READ cropY WRITE setCropY NOTIFY cropYChanged)
Q_PROPERTY(qreal cropWidth READ cropWidth WRITE setCropWidth NOTIFY cropWidthChanged)
Q_PROPERTY(qreal cropHeight READ cropHeight WRITE setCropHeight NOTIFY cropHeightChanged)
public:
explicit WallpaperImage(QQuickItem *parent = nullptr);
~WallpaperImage() override;
QUrl source() const {
return m_source;
}
void setSource(const QUrl &source);
QSize screenResolution() const {
return m_screenResolution;
}
void setScreenResolution(const QSize &screenResolution);
qreal zoom() const {
return m_zoom;
}
void setZoom(qreal zoom);
qreal cropX() const {
return m_cropX;
}
void setCropX(qreal x);
qreal cropY() const {
return m_cropY;
}
void setCropY(qreal y);
qreal cropWidth() const {
return m_cropWidth;
}
void setCropWidth(qreal w);
qreal cropHeight() const {
return m_cropHeight;
}
void setCropHeight(qreal h);
protected:
QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *updatePaintNodeData) override;
signals:
void sourceChanged();
void screenResolutionChanged();
void zoomChanged();
void cropXChanged();
void cropYChanged();
void cropWidthChanged();
void cropHeightChanged();
private:
void loadImage();
void handleImageLoaded();
QString getCacheFilePath() const;
QUrl m_source;
QSize m_screenResolution;
qreal m_zoom = 1.0;
qreal m_cropX = 0.0;
qreal m_cropY = 0.0;
qreal m_cropWidth = 1.0;
qreal m_cropHeight = 1.0;
QImage m_image;
QSGTexture *m_texture = nullptr;
bool m_textureDirty = false;
QFutureWatcher<QImage> m_imageWatcher;
};
} // namespace ZShell::internal
+141 -155
View File
@@ -1,17 +1,22 @@
#include "writefile.hpp" #include "writefile.hpp"
#include <QtConcurrent/qtconcurrentrun.h> #include <QtConcurrent/qtconcurrentrun.h>
#include <QtCore/QCryptographicHash>
#include <QtCore/QSaveFile>
#include <QtGui/QImageReader>
#include <QtQuick/qquickimageprovider.h> #include <QtQuick/qquickimageprovider.h>
#include <QtQuick/qquickitemgrabresult.h> #include <QtQuick/qquickitemgrabresult.h>
#include <QtQuick/qquickwindow.h> #include <QtQuick/qquickwindow.h>
#include <qdir.h> #include <QDir>
#include <qfile.h> #include <QFile>
#include <qfileinfo.h> #include <QFileInfo>
#include <qfuturewatcher.h> #include <QFutureWatcher>
#include <qimage.h> #include <QImage>
#include <qjsengine.h> #include <QJSValue>
#include <qqmlengine.h> #include <QQmlEngine>
#include <QSize>
#include <QVariant>
namespace ZShell { namespace ZShell {
@@ -84,54 +89,51 @@ void ZShellIo::saveItem(
&QQuickItemGrabResult::ready, &QQuickItemGrabResult::ready,
this, this,
[grabResult, scaledRect, path, onSaved, onFailed, this]() { [grabResult, scaledRect, path, onSaved, onFailed, this]() {
const auto future = QtConcurrent::run([grabResult, scaledRect, path]() {
QImage image = grabResult->image();
QImage image = grabResult->image(); if (scaledRect.isValid()) {
image = image.copy(scaledRect);
if (scaledRect.isValid()) { }
image = image.copy(scaledRect);
}
const auto future = QtConcurrent::run([image, path]() {
const QString file = path.toLocalFile(); const QString file = path.toLocalFile();
const QString parent = QFileInfo(file).absolutePath(); const QString parent = QFileInfo(file).absolutePath();
return QDir().mkpath(parent) && image.save(file); QDir().mkpath(parent);
QSaveFile out(file);
if (!out.open(QIODevice::WriteOnly)) {
return false;
}
if (!image.save(&out, "PNG")) {
return false;
}
return out.commit();
}); });
auto* watcher = new QFutureWatcher<bool>(this); auto* watcher = new QFutureWatcher<bool>(this);
auto* engine = qmlEngine(this); auto* engine = qmlEngine(this);
QObject::connect( QObject::connect(watcher, &QFutureWatcher<bool>::finished, this, [=]() {
watcher,
&QFutureWatcher<bool>::finished,
this,
[=]() {
if (watcher->result()) { if (watcher->result()) {
if (onSaved.isCallable() && engine) {
if (onSaved.isCallable()) {
onSaved.call({ onSaved.call({
QJSValue(path.toLocalFile()), engine->toScriptValue(path.toLocalFile()),
engine->toScriptValue(QVariant::fromValue(path)) engine->toScriptValue(path)
}); });
} }
} else { } else {
qWarning() << "ZShellIo::saveItem: failed to save" << path;
qWarning() << "ZShellIo::saveItem: failed to save" if (onFailed.isCallable() && engine) {
<< path;
if (onFailed.isCallable()) {
onFailed.call({ onFailed.call({
engine->toScriptValue(QVariant::fromValue(path)) engine->toScriptValue(path)
}); });
} }
} }
watcher->deleteLater(); watcher->deleteLater();
} });
);
watcher->setFuture(future); watcher->setFuture(future);
} }
@@ -139,106 +141,130 @@ void ZShellIo::saveItem(
} }
// ============================================================ // ============================================================
// saveImage // cacheImage
// ============================================================ // ============================================================
void ZShellIo::saveImage(const QUrl& source, const QUrl& target) { void ZShellIo::cacheImage(const QUrl& source, const QString& cacheDir) {
this->saveImage(source, target, QJSValue(), QJSValue()); this->cacheImage(source, cacheDir, QJSValue(), QJSValue());
} }
void ZShellIo::saveImage(const QUrl& source, const QUrl& target, QJSValue onSaved) { void ZShellIo::cacheImage(const QUrl& source, const QString& cacheDir, QJSValue onSaved) {
this->saveImage(source, target, onSaved, QJSValue()); this->cacheImage(source, cacheDir, onSaved, QJSValue());
} }
void ZShellIo::saveImage( void ZShellIo::cacheImage(
const QUrl& source, const QUrl& source,
const QUrl& target, const QString& cacheDir,
QJSValue onSaved, QJSValue onSaved,
QJSValue onFailed QJSValue onFailed
) { ) {
auto* engine = qmlEngine(this); if (cacheDir.isEmpty()) {
qWarning() << "ZShellIo::cacheImage: cacheDir is empty";
return;
}
const auto future = QtConcurrent::run([this, source, target]() { QImage image;
return this->saveImageInternal(source, target); if (!loadSourceImage(source, image)) {
qWarning() << "ZShellIo::cacheImage: failed to load source image" << source;
auto* engine = qmlEngine(this);
if (onFailed.isCallable() && engine) {
onFailed.call({
engine->toScriptValue(source),
engine->toScriptValue(cacheDir)
});
}
return;
}
const auto future = QtConcurrent::run([image, cacheDir]() -> QString {
if (image.isNull()) {
return QString();
}
const QImage normalized = image.convertToFormat(QImage::Format_RGBA8888);
const QByteArray bytes(
reinterpret_cast<const char*>(normalized.constBits()),
qsizetype(normalized.sizeInBytes())
);
const QByteArray digest =
QCryptographicHash::hash(bytes, QCryptographicHash::Sha256).toHex();
QDir dir(cacheDir);
if (!dir.exists() && !QDir().mkpath(cacheDir)) {
return QString();
}
const QString finalPath = dir.filePath(QString::fromLatin1(digest) + ".png");
if (QFile::exists(finalPath)) {
return finalPath;
}
QSaveFile out(finalPath);
if (!out.open(QIODevice::WriteOnly)) {
return QString();
}
if (!normalized.save(&out, "PNG")) {
return QString();
}
if (!out.commit()) {
return QString();
}
return finalPath;
}); });
auto* watcher = new QFutureWatcher<bool>(this); auto* watcher = new QFutureWatcher<QString>(this);
auto* engine = qmlEngine(this);
QObject::connect( QObject::connect(watcher, &QFutureWatcher<QString>::finished, this, [=]() {
watcher, const QString finalPath = watcher->result();
&QFutureWatcher<bool>::finished,
this,
[=]() {
if (watcher->result()) { if (!finalPath.isEmpty()) {
if (onSaved.isCallable() && engine) {
if (onSaved.isCallable()) {
onSaved.call({ onSaved.call({
QJSValue(target.toLocalFile()), engine->toScriptValue(finalPath),
engine->toScriptValue(QVariant::fromValue(target)) engine->toScriptValue(QUrl::fromLocalFile(finalPath))
}); });
} }
} else { } else {
qWarning() << "ZShellIo::cacheImage: failed to cache" << source;
qWarning() << "ZShellIo::saveImage: failed to save" if (onFailed.isCallable() && engine) {
<< source
<< "to"
<< target;
if (onFailed.isCallable()) {
onFailed.call({ onFailed.call({
engine->toScriptValue(QVariant::fromValue(target)) engine->toScriptValue(source),
engine->toScriptValue(cacheDir)
}); });
} }
} }
watcher->deleteLater(); watcher->deleteLater();
} });
);
watcher->setFuture(future); watcher->setFuture(future);
} }
bool ZShellIo::saveImageInternal(const QUrl& source, const QUrl& target) const { // ============================================================
// loadSourceImage
// ============================================================
if (!target.isLocalFile()) { bool ZShellIo::loadSourceImage(const QUrl& source, QImage& image) const {
qWarning() << "ZShellIo::saveImage: target" image = QImage();
<< target
<< "is not a local file";
return false;
}
const QString targetFile = target.toLocalFile();
if (!QDir().mkpath(QFileInfo(targetFile).absolutePath())) {
return false;
}
// ========================================================
// Local file path
// ========================================================
if (source.isLocalFile()) { if (source.isLocalFile()) {
QImageReader reader(source.toLocalFile());
QFile::remove(targetFile); reader.setAutoTransform(true);
image = reader.read();
return QFile::copy( return !image.isNull();
source.toLocalFile(),
targetFile
);
} }
// ========================================================
// image:// provider path
// ========================================================
if (source.scheme() == "image") { if (source.scheme() == "image") {
auto* engine = qmlEngine(const_cast<ZShellIo*>(this)); auto* engine = qmlEngine(const_cast<ZShellIo*>(this));
if (!engine) { if (!engine) {
qWarning() << "ZShellIo::saveImage: no QQmlEngine"; qWarning() << "ZShellIo::loadSourceImage: no QQmlEngine";
return false; return false;
} }
@@ -246,70 +272,44 @@ bool ZShellIo::saveImageInternal(const QUrl& source, const QUrl& target) const {
const QString imageId = const QString imageId =
source.path().startsWith('/') source.path().startsWith('/')
? source.path().mid(1) ? source.path().mid(1)
: source.path(); : source.path();
auto* providerBase =
engine->imageProvider(providerId);
auto* providerBase = engine->imageProvider(providerId);
if (!providerBase) { if (!providerBase) {
qWarning() << "ZShellIo::saveImage: provider not found" qWarning() << "ZShellIo::loadSourceImage: provider not found"
<< providerId; << providerId;
return false; return false;
} }
auto* provider = auto* provider = dynamic_cast<QQuickImageProvider*>(providerBase);
dynamic_cast<QQuickImageProvider*>(providerBase);
if (!provider) { if (!provider) {
qWarning() << "ZShellIo::saveImage: provider is not a QQuickImageProvider" qWarning() << "ZShellIo::loadSourceImage: provider is not a QQuickImageProvider"
<< providerId;
return false;
}
if (!provider) {
qWarning() << "ZShellIo::saveImage: provider not found"
<< providerId; << providerId;
return false; return false;
} }
QSize size; QSize size;
QImage image;
switch (provider->imageType()) { switch (provider->imageType()) {
case QQuickImageProvider::Image: case QQuickImageProvider::Image:
image = provider->requestImage( image = provider->requestImage(imageId, &size, QSize());
imageId,
&size,
QSize()
);
break; break;
case QQuickImageProvider::Pixmap: case QQuickImageProvider::Pixmap:
image = provider->requestPixmap( image = provider->requestPixmap(imageId, &size, QSize()).toImage();
imageId,
&size,
QSize()
).toImage();
break; break;
default: default:
qWarning() << "ZShellIo::saveImage: unsupported provider type"; qWarning() << "ZShellIo::loadSourceImage: unsupported provider type"
<< providerId;
return false; return false;
} }
if (image.isNull()) { return !image.isNull();
qWarning() << "ZShellIo::saveImage: provider returned null image";
return false;
}
return image.save(targetFile);
} }
qWarning() << "ZShellIo::saveImage: unsupported source" qWarning() << "ZShellIo::loadSourceImage: unsupported source" << source;
<< source;
return false; return false;
} }
@@ -318,18 +318,12 @@ bool ZShellIo::saveImageInternal(const QUrl& source, const QUrl& target) const {
// ============================================================ // ============================================================
bool ZShellIo::copyFile(const QUrl& source, const QUrl& target, bool overwrite) const { bool ZShellIo::copyFile(const QUrl& source, const QUrl& target, bool overwrite) const {
if (!source.isLocalFile()) { if (!source.isLocalFile()) {
qWarning() << "ZShellIo::copyFile: source" qWarning() << "ZShellIo::copyFile: source" << source << "is not a local file";
<< source
<< "is not a local file";
return false; return false;
} }
if (!target.isLocalFile()) { if (!target.isLocalFile()) {
qWarning() << "ZShellIo::copyFile: target" qWarning() << "ZShellIo::copyFile: target" << target << "is not a local file";
<< target
<< "is not a local file";
return false; return false;
} }
@@ -337,18 +331,12 @@ bool ZShellIo::copyFile(const QUrl& source, const QUrl& target, bool overwrite)
QFile::remove(target.toLocalFile()); QFile::remove(target.toLocalFile());
} }
return QFile::copy( return QFile::copy(source.toLocalFile(), target.toLocalFile());
source.toLocalFile(),
target.toLocalFile()
);
} }
bool ZShellIo::deleteFile(const QUrl& path) const { bool ZShellIo::deleteFile(const QUrl& path) const {
if (!path.isLocalFile()) { if (!path.isLocalFile()) {
qWarning() << "ZShellIo::deleteFile: path" qWarning() << "ZShellIo::deleteFile: path" << path << "is not a local file";
<< path
<< "is not a local file";
return false; return false;
} }
@@ -356,10 +344,8 @@ bool ZShellIo::deleteFile(const QUrl& path) const {
} }
QString ZShellIo::toLocalFile(const QUrl& url) const { QString ZShellIo::toLocalFile(const QUrl& url) const {
if (!url.isLocalFile()) { if (!url.isLocalFile()) {
qWarning() << "ZShellIo::toLocalFile: given url is not a local file" qWarning() << "ZShellIo::toLocalFile: given url is not a local file" << url;
<< url;
return QString(); return QString();
} }
+8 -7
View File
@@ -1,10 +1,11 @@
#pragma once #pragma once
#include <QtQuick/qquickitem.h> #include <QtQuick/qquickitem.h>
#include <qjsvalue.h> #include <QImage>
#include <qobject.h> #include <QJSValue>
#include <QObject>
#include <qqmlintegration.h> #include <qqmlintegration.h>
#include <qurl.h> #include <QUrl>
namespace ZShell { namespace ZShell {
@@ -23,9 +24,9 @@ Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, QJSValue onSaved
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved); Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved);
Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed); Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed);
Q_INVOKABLE void saveImage(const QUrl& source, const QUrl& target); Q_INVOKABLE void cacheImage(const QUrl& source, const QString& cacheDir);
Q_INVOKABLE void saveImage(const QUrl& source, const QUrl& target, QJSValue onSaved); Q_INVOKABLE void cacheImage(const QUrl& source, const QString& cacheDir, QJSValue onSaved);
Q_INVOKABLE void saveImage(const QUrl& source, const QUrl& target, QJSValue onSaved, QJSValue onFailed); Q_INVOKABLE void cacheImage(const QUrl& source, const QString& cacheDir, QJSValue onSaved, QJSValue onFailed);
// clang-format on // clang-format on
Q_INVOKABLE bool copyFile(const QUrl& source, const QUrl& target, bool overwrite = true) const; Q_INVOKABLE bool copyFile(const QUrl& source, const QUrl& target, bool overwrite = true) const;
@@ -33,7 +34,7 @@ Q_INVOKABLE bool deleteFile(const QUrl& path) const;
Q_INVOKABLE QString toLocalFile(const QUrl& url) const; Q_INVOKABLE QString toLocalFile(const QUrl& url) const;
private: private:
bool saveImageInternal(const QUrl& source, const QUrl& target) const; bool loadSourceImage(const QUrl& source, QImage& image) const;
}; };
} // namespace ZShell } // namespace ZShell
+8
View File
@@ -180,6 +180,14 @@ export const settingsIndex = [
section: "Bar", section: "Bar",
keywords: ["smoothing", "rounding"], keywords: ["smoothing", "rounding"],
}, },
// System tray section
{
name: "Tray icon size",
category: "bar",
categoryName: "Bar",
section: "Tray",
keywords: ["tray", "icon", "size"],
},
// Popouts section // Popouts section
{ {
name: "Tray", name: "Tray",
+1 -1
View File
@@ -1,6 +1,6 @@
//@ pragma UseQApplication //@ pragma UseQApplication
//@ pragma Env QSG_RENDER_LOOP=threaded //@ pragma Env QSG_RENDER_LOOP=threaded
//@ pragma Env QSG_RHI_BACKEND=vulkan // @ pragma Env QSG_RHI_BACKEND=vulkan
//@ pragma Env QSG_NO_VSYNC=1 //@ pragma Env QSG_NO_VSYNC=1
//@ pragma Env QS_NO_RELOAD_POPUP=1 //@ pragma Env QS_NO_RELOAD_POPUP=1
//@ pragma Env QT_SCALE_FACTOR_ROUNDING_POLICY=Round //@ pragma Env QT_SCALE_FACTOR_ROUNDING_POLICY=Round