diff --git a/Helpers/Wallpapers.qml b/Helpers/Wallpapers.qml index 06fdfeb..10c8f22 100644 --- a/Helpers/Wallpapers.qml +++ b/Helpers/Wallpapers.qml @@ -32,14 +32,23 @@ Searcher { showPreview = true; } - function setCrop(screen: string, rect: rect, zoom: real): void { + function setCrop(screen: string, rect: rect, scaledRect: rect, zoom: real): void { let updated = Object.assign({}, root.crops); + if (zoom <= 0) + zoom = 1.0; + else if (zoom > 5.0) + zoom = 5.0; + updated[screen] = { x: rect.x, y: rect.y, width: rect.width, height: rect.height, + scaledX: scaledRect.x, + scaledY: scaledRect.y, + scaledWidth: scaledRect.width, + scaledHeight: scaledRect.height, zoom: zoom }; @@ -51,7 +60,7 @@ Searcher { function setWallpaper(path: string): void { actualCurrent = path; WallpaperPath.currentWallpaperPath = path; - Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 0, 0), 1.0)); + Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 0, 0), 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}`]); if (Config.general.color.schemeGeneration) Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.general.color.mode}`]); diff --git a/Modules/Settings/Controls/WallpaperCropper.qml b/Modules/Settings/Controls/WallpaperCropper.qml index 5627c9f..58de9fb 100644 --- a/Modules/Settings/Controls/WallpaperCropper.qml +++ b/Modules/Settings/Controls/WallpaperCropper.qml @@ -2,8 +2,8 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts -import QtQuick.Controls import Quickshell +import Quickshell.Hyprland import qs.Config import qs.Components import qs.Helpers @@ -13,124 +13,162 @@ RowLayout { required property ShellScreen screen + Layout.fillWidth: true spacing: Appearance.spacing.normal Repeater { model: Quickshell.screens - delegate: ImgCrop { - } - } - - component ImgCrop: Item { - id: cropper - - readonly property var displayData: Wallpapers.getCrop(modelData.name) - required property ShellScreen modelData - - Layout.fillHeight: true - Layout.fillWidth: true - - Image { - id: imageView - - property real displayH: paintedHeight - property real displayW: paintedWidth - property real displayX: (width - paintedWidth) * 0.5 - property real displayY: (height - paintedHeight) * 0.5 - property real scaleX: sourceW / displayW - property real scaleY: sourceH / displayH - property real sourceH: cropper.modelData.height - property real sourceW: cropper.modelData.width - - anchors.fill: parent - fillMode: Image.PreserveAspectFit - smooth: true - source: Wallpapers.current - } - Item { - id: overlay + id: delegate - clip: true - height: imageView.displayH - width: imageView.displayW - x: imageView.displayX - y: imageView.displayY + required property ShellScreen modelData - CustomRect { - id: cropRect + Layout.fillHeight: true + Layout.fillWidth: true - property real aspectRatio: cropper.modelData.width / cropper.modelData.height - readonly property rect sourceRect: Qt.rect(x * imageView.scaleX, y * imageView.scaleY, width * imageView.scaleX, height * imageView.scaleY) - property real zoom: cropper.displayData.zoom + Image { + id: scaledImg - function clampToBounds() { - x = Math.max(0, Math.min(x, overlay.width - width)); + property var displayData + readonly property real imageRatio: scaledImg.sourceSize.width / scaledImg.sourceSize.height + property real monitorScale: 1.0 + readonly property real scaleDownX: scaledImg.width / scaledImg.sourceSize.width + readonly property real scaleDownY: scaledImg.height / scaledImg.sourceSize.height + readonly property real scaleX: (scaledImg.sourceSize.width * monitorScale) / scaledImg.width + readonly property real scaleY: (scaledImg.sourceSize.height * monitorScale) / scaledImg.height - y = Math.max(0, Math.min(y, overlay.height - height)); + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + height: width / imageRatio + source: Wallpapers.current + + Component.onCompleted: { + Hyprland.refreshMonitors(); + // monitorScale = Hyprland.monitorFor(delegate.modelData).scale; + } + onScaleYChanged: console.log(scaleX, scaleY, "scale") + onStatusChanged: { + if (scaledImg.status == Image.Ready) { + console.log(scaledImg.sourceSize.width, scaledImg.sourceSize.height, scaledImg.width, scaledImg.height, delegate.modelData.width, delegate.modelData.height); + } } - border.color: DynamicColors.palette.m3primary - border.width: 2 - color: DynamicColors.tPalette.m3primary - height: width / aspectRatio - radius: Appearance.rounding.small - visible: imageView.status === Image.Ready - width: Math.min(overlay.width / zoom, overlay.height * aspectRatio / zoom) - x: cropper.displayData.x / imageView.scaleX - y: cropper.displayData.y / imageView.scaleY - } + Connections { + function onLoaded(): void { + scaledImg.displayData = Wallpapers.getCrop(delegate.modelData.name); + cropRect.zoom = scaledImg.displayData.zoom; + cropRect.restoreFromData(); + } - MouseArea { - function updateCrop(mouseX, mouseY) { - let nx = mouseX - cropRect.width * 0.5; - let ny = mouseY - cropRect.height * 0.5; - - nx = Math.max(0, Math.min(nx, overlay.width - cropRect.width)); - - ny = Math.max(0, Math.min(ny, overlay.height - cropRect.height)); - - cropRect.x = nx; - cropRect.y = ny; + target: Wallpapers.monitorCrops } - anchors.fill: parent - hoverEnabled: true - preventStealing: true + CustomRect { + id: cropRect - onPositionChanged: mouse => { - if (pressed) + property real aspectRatio: delegate.modelData.width / delegate.modelData.height + readonly property real baseHeight: baseWidth / aspectRatio + readonly property real baseWidth: { + let fittedHeight = scaledImg.height; + let fittedWidth = fittedHeight * aspectRatio; + + if (fittedWidth > scaledImg.width) { + fittedWidth = scaledImg.width; + fittedHeight = fittedWidth / aspectRatio; + } + + return fittedWidth; + } + readonly property real imageX: (scaledImg.width - scaledImg.width) / 2 + readonly property real imageY: (scaledImg.height - scaledImg.height) / 2 + property real zoom: scaledImg.displayData.zoom + + function centerInImage() { + x = imageX + (scaledImg.width - width) / 2; + y = imageY + (scaledImg.height - height) / 2; + } + + function clampToBounds() { + x = Math.max(imageX, Math.min(x, imageX + scaledImg.width - width)); + + y = Math.max(imageY, Math.min(y, imageY + scaledImg.height - height)); + } + + function restoreFromData() { + let data = scaledImg.displayData; + + if (data && data.scaledX !== 0 || data.scaledY !== 0 || data.scaledWidth !== 0 || data.scaledHeight !== 0) { + console.log("true"); + x = imageX + data.scaledX; + y = imageY + data.scaledY; + + clampToBounds(); + } else { + console.log("false"); + zoom = 1.0; + centerInImage(); + } + } + + border.color: "lime" + border.width: 2 + height: baseHeight / zoom + width: baseWidth / zoom + + onHeightChanged: clampToBounds() + onWidthChanged: clampToBounds() + } + + MouseArea { + id: mouse + + function updateCrop(mouseX, mouseY) { + let nx = mouseX - cropRect.width * 0.5; + let ny = mouseY - cropRect.height * 0.5; + + nx = Math.max(cropRect.imageX, Math.min(nx, cropRect.imageX + scaledImg.width - cropRect.width)); + + ny = Math.max(cropRect.imageY, Math.min(ny, cropRect.imageY + scaledImg.height - cropRect.height)); + + cropRect.x = nx; + cropRect.y = ny; + } + + anchors.fill: parent + hoverEnabled: true + preventStealing: true + + onPositionChanged: mouse => { + if (pressed) + updateCrop(mouse.x, mouse.y); + } + onPressed: mouse => { updateCrop(mouse.x, mouse.y); - } - onPressed: mouse => { - updateCrop(mouse.x, mouse.y); - } - onReleased: { - Wallpapers.recentlyChanged = false; - Wallpapers.setCrop(cropper.modelData.name, cropRect.sourceRect, cropRect.zoom); - // Config.background.sourceClipX = cropRect.sourceRect.x; - // Config.background.sourceClipY = cropRect.sourceRect.y; - // Config.background.sourceClipW = cropRect.sourceRect.width; - // Config.background.sourceClipH = cropRect.sourceRect.height; - // Config.save(); - } - onWheel: wheel => { - let oldCenterX = cropRect.x + cropRect.width * 0.5; - let oldCenterY = cropRect.y + cropRect.height * 0.5; + } + onReleased: { + const croprect = cropRect.mapToItem(scaledImg, 0, 0, cropRect.width, cropRect.height); + const upscaledRect = Qt.rect(croprect.x / scaledImg.width, croprect.y / scaledImg.height, croprect.width / scaledImg.width, croprect.height / scaledImg.height); + Wallpapers.setCrop(delegate.modelData.name, upscaledRect, croprect, cropRect.zoom); + } + onWheel: wheel => { + let oldCenterX = cropRect.x + cropRect.width * 0.5; + let oldCenterY = cropRect.y + cropRect.height * 0.5; - if (wheel.angleDelta.y > 0) - cropRect.zoom *= 1.1; - else - cropRect.zoom /= 1.1; + if (wheel.angleDelta.y > 0) + cropRect.zoom *= 1.1; + else + cropRect.zoom /= 1.1; - cropRect.zoom = Math.max(1.0, Math.min(cropRect.zoom, 10.0)); - Config.background.zoom = cropRect.zoom; + cropRect.zoom = Math.max(1.0, Math.min(cropRect.zoom, 5.0)); + console.log(cropRect.zoom); - cropRect.x = oldCenterX - cropRect.width * 0.5; - cropRect.y = oldCenterY - cropRect.height * 0.5; + cropRect.x = oldCenterX - cropRect.width * 0.5; + cropRect.y = oldCenterY - cropRect.height * 0.5; - cropRect.clampToBounds(); + cropRect.clampToBounds(); + } } } } diff --git a/Modules/SysTray/TrayItem.qml b/Modules/SysTray/TrayItem.qml index 60fd9ce..cd73d96 100644 --- a/Modules/SysTray/TrayItem.qml +++ b/Modules/SysTray/TrayItem.qml @@ -1,5 +1,6 @@ import QtQuick.Layouts import QtQuick +import QtQuick.VectorImage import Quickshell import Quickshell.Services.SystemTray import qs.Modules diff --git a/Modules/Wallpaper/WallBackground.qml b/Modules/Wallpaper/WallBackground.qml index ec2e667..9aae2e9 100644 --- a/Modules/Wallpaper/WallBackground.qml +++ b/Modules/Wallpaper/WallBackground.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound import Quickshell +import Quickshell.Hyprland import QtQuick import qs.Components import qs.Helpers @@ -17,13 +18,19 @@ Item { Image { id: img + property int displayH + property int displayW + property real resScale + property real zoom: 1.0 + asynchronous: true - fillMode: Image.Stretch + fillMode: Image.PreserveAspectCrop + height: implicitHeight * zoom / resScale opacity: 1 retainWhileLoading: true source: root.source - sourceSize.height: root.screen.height - sourceSize.width: root.screen.width + sourceSize.width: root.screen.width * resScale + width: implicitWidth * zoom / resScale Behavior on height { Anim { @@ -44,33 +51,33 @@ Item { Connections { function onAdapterUpdated(): void { - const displayData = Wallpapers.getCrop(root.screen.name); - const displayRect = Qt.rect(displayData.x, displayData.y, displayData.width, displayData.height); - const scale = root.screen.width / displayData.width; - if (displayRect.width > 0 && displayRect.height > 0) { - img.anchors.fill = null; - img.x = -displayRect.x; - img.y = -displayRect.y; - img.width = img.implicitWidth * scale; - img.height = img.implicitHeight * scale; - } else { - img.anchors.fill = root; + Hyprland.refreshMonitors(); + const scale = Hyprland.monitorFor(root.screen).scale; + if (scale > 0 && img.resScale !== scale) { + img.resScale = scale; + img.sourceSize.width = root.screen.width * scale; } + 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; + img.zoom = displayData.zoom; + img.x = -(displayRect.x * displayData.zoom / img.resScale); + img.y = -(displayRect.y * displayData.zoom / img.resScale); } function onLoaded(): void { - const displayData = Wallpapers.getCrop(root.screen.name); - const displayRect = Qt.rect(displayData.x, displayData.y, displayData.width, displayData.height); - const scale = root.screen.width / displayData.width; - if (displayRect.width > 0 && displayRect.height > 0) { - img.anchors.fill = null; - img.x = -displayRect.x; - img.y = -displayRect.y; - img.width = img.implicitWidth * scale; - img.height = img.implicitHeight * scale; - } else { - img.anchors.fill = root; + Hyprland.refreshMonitors(); + const scale = Hyprland.monitorFor(root.screen).scale; + if (scale > 0 && img.resScale !== scale) { + img.resScale = scale; + img.sourceSize.width = root.screen.width * scale; } + 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; + img.zoom = displayData.zoom; + img.x = -(displayRect.x * displayData.zoom / img.resScale); + img.y = -(displayRect.y * displayData.zoom / img.resScale); } target: Wallpapers.monitorCrops diff --git a/testcropper/Paths.qml b/testcropper/Paths.qml new file mode 100644 index 0000000..3dcabbe --- /dev/null +++ b/testcropper/Paths.qml @@ -0,0 +1,35 @@ +pragma Singleton + +import ZShell +import Quickshell +import QtQuick + +Singleton { + id: root + + readonly property string cache: `${Quickshell.env("XDG_CACHE_HOME") || `${home}/.cache`}/zshell` + readonly property string config: `${Quickshell.env("XDG_CONFIG_HOME") || `${home}/.config`}/zshell` + readonly property string data: `${Quickshell.env("XDG_DATA_HOME") || `${home}/.local/share`}/zshell` + readonly property string desktop: `${Quickshell.env("HOME")}/Desktop` + readonly property string home: Quickshell.env("HOME") + readonly property string imagecache: `${cache}/imagecache` + readonly property string libdir: Quickshell.env("ZSHELL_LIB_DIR") || "/usr/lib/zshell" + readonly property string notifimagecache: `${imagecache}/notifs` + readonly property string pictures: Quickshell.env("XDG_PICTURES_DIR") || `${home}/Pictures` + readonly property string recsdir: Quickshell.env("ZSHELL_RECORDINGS_DIR") || `${videos}/Recordings` + readonly property string state: `${Quickshell.env("XDG_STATE_HOME") || `${home}/.local/state`}/zshell` + readonly property string videos: Quickshell.env("XDG_VIDEOS_DIR") || `${home}/Videos` + + function absolutePath(path: string): string { + return toLocalFile(path.replace("~", home)); + } + + function shortenHome(path: string): string { + return path.replace(home, "~"); + } + + function toLocalFile(path: url): string { + path = Qt.resolvedUrl(path); + return path.toString() ? ZShellIo.toLocalFile(path) : ""; + } +} diff --git a/testcropper/WallpaperPath.qml b/testcropper/WallpaperPath.qml new file mode 100644 index 0000000..ea5dc22 --- /dev/null +++ b/testcropper/WallpaperPath.qml @@ -0,0 +1,28 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + property alias currentWallpaperPath: adapter.currentWallpaperPath + property alias lockscreenBg: adapter.lockscreenBg + + FileView { + id: fileView + + path: `${Paths.state}/wallpaper_path.json` + watchChanges: true + + onAdapterUpdated: writeAdapter() + onFileChanged: reload() + + JsonAdapter { + id: adapter + + property string currentWallpaperPath: "" + property string lockscreenBg: `${Paths.state}/lockscreen_bg.png` + } + } +} diff --git a/testcropper/Wallpapers.qml b/testcropper/Wallpapers.qml new file mode 100644 index 0000000..4eb5d9f --- /dev/null +++ b/testcropper/Wallpapers.qml @@ -0,0 +1,56 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Io +import QtQuick + +Singleton { + id: root + + property alias crops: adapter.monitorCrops + property alias monitorCrops: monitorCrops + property string previewPath + property bool recentlyChanged + property bool showPreview: false + + function getCrop(screen: string): var { + return root.crops[screen]; + } + + function setCrop(screen: string, rect: rect, scaledRect: rect, zoom: real): void { + let updated = Object.assign({}, root.crops); + + updated[screen] = { + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height, + scaledX: scaledRect.x, + scaledY: scaledRect.y, + scaledWidth: scaledRect.width, + scaledHeight: scaledRect.height, + zoom: zoom + }; + + root.crops = updated; + monitorCrops.writeAdapter(); + monitorCrops.reload(); + } + + FileView { + id: monitorCrops + + path: `${Paths.state}/wallpaper-crops.json` + watchChanges: true + + onAdapterUpdated: writeAdapter() + onFileChanged: reload() + + JsonAdapter { + id: adapter + + property var monitorCrops: ({}) + } + } +} diff --git a/testcropper/shell.qml b/testcropper/shell.qml new file mode 100644 index 0000000..efe6e5c --- /dev/null +++ b/testcropper/shell.qml @@ -0,0 +1,200 @@ +import Quickshell +import Quickshell.Hyprland +import QtQuick +import QtQuick.Layouts + +Variants { + model: Quickshell.screens + + PanelWindow { + id: root + + required property var modelData + readonly property list screens: Quickshell.screens + + implicitWidth: 300 + screen: modelData + + anchors { + bottom: true + left: false + right: true + top: true + } + + ColumnLayout { + id: layout + + anchors.fill: parent + + Repeater { + model: Quickshell.screens + + Item { + id: delegate + + required property ShellScreen modelData + + Layout.fillHeight: true + Layout.fillWidth: true + + Image { + id: scaledImg + + property var displayData + readonly property real imageRatio: scaledImg.sourceSize.width / scaledImg.sourceSize.height + property real monitorScale + readonly property real scaleDownX: scaledImg.paintedWidth / scaledImg.sourceSize.width + readonly property real scaleDownY: scaledImg.paintedHeight / scaledImg.sourceSize.height + readonly property real scaleX: (scaledImg.sourceSize.width * monitorScale) / scaledImg.paintedWidth + readonly property real scaleY: (scaledImg.sourceSize.height * monitorScale) / scaledImg.paintedHeight + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + fillMode: Image.PreserveAspectFit + height: width / imageRatio + source: "/mnt/IronWolf/SDImages/SWWW_Wals/wallhaven-43dyq6.jpg" + + Component.onCompleted: { + Hyprland.refreshMonitors(); + monitorScale = Hyprland.monitorFor(delegate.modelData).scale; + } + onScaleYChanged: console.log(scaleX, scaleY, "scale") + onStatusChanged: { + if (scaledImg.status == Image.Ready) { + console.log(scaledImg.sourceSize.width, scaledImg.sourceSize.height, scaledImg.paintedWidth, scaledImg.paintedHeight, delegate.modelData.width, delegate.modelData.height); + } + } + + Connections { + function onLoaded(): void { + scaledImg.displayData = Wallpapers.getCrop(delegate.modelData.name); + cropRect.restoreFromData(); + } + + target: Wallpapers.monitorCrops + } + + Rectangle { + id: cropRect + + property real aspectRatio: delegate.modelData.width / delegate.modelData.height + readonly property real baseHeight: baseWidth / aspectRatio + + // PreserveAspectFit baseline at zoom = 1 + readonly property real baseWidth: { + let fittedHeight = scaledImg.paintedHeight; + let fittedWidth = fittedHeight * aspectRatio; + + if (fittedWidth > scaledImg.paintedWidth) { + fittedWidth = scaledImg.paintedWidth; + fittedHeight = fittedWidth / aspectRatio; + } + + return fittedWidth; + } + + // actual visible image origin inside Image item + readonly property real imageX: (scaledImg.width - scaledImg.paintedWidth) / 2 + readonly property real imageY: (scaledImg.height - scaledImg.paintedHeight) / 2 + property real zoom: scaledImg.displayData.zoom + + function centerInImage() { + x = imageX + (scaledImg.paintedWidth - width) / 2; + y = imageY + (scaledImg.paintedHeight - height) / 2; + } + + function clampToBounds() { + x = Math.max(imageX, Math.min(x, imageX + scaledImg.paintedWidth - width)); + + y = Math.max(imageY, Math.min(y, imageY + scaledImg.paintedHeight - height)); + } + + function restoreFromData() { + let data = scaledImg.displayData; + + if (data && data.scaledX !== 0 || data.scaledY !== 0 || data.scaledWidth !== 0 || data.scaledHeight !== 0) { + console.log("true"); + zoom = data.zoom ?? 1.0; + + // width/height are authoritative + width = data.scaledWidth; + height = data.scaledHeight; + + x = imageX + data.scaledX; + y = imageY + data.scaledY; + + clampToBounds(); + } else { + console.log("false"); + zoom = 1.0; + centerInImage(); + } + } + + border.color: "lime" + border.width: 2 + color: "transparent" + height: baseHeight / zoom + width: baseWidth / zoom + + onHeightChanged: clampToBounds() + onWidthChanged: clampToBounds() + } + + MouseArea { + id: mouse + + function updateCrop(mouseX, mouseY) { + let nx = mouseX - cropRect.width * 0.5; + let ny = mouseY - cropRect.height * 0.5; + + nx = Math.max(cropRect.imageX, Math.min(nx, cropRect.imageX + scaledImg.paintedWidth - cropRect.width)); + + ny = Math.max(cropRect.imageY, Math.min(ny, cropRect.imageY + scaledImg.paintedHeight - cropRect.height)); + + cropRect.x = nx; + cropRect.y = ny; + } + + anchors.fill: parent + hoverEnabled: true + preventStealing: true + + onPositionChanged: mouse => { + if (pressed) + updateCrop(mouse.x, mouse.y); + } + onPressed: mouse => { + updateCrop(mouse.x, mouse.y); + } + onReleased: { + console.log(cropRect.mapToItem(scaledImg, 0, 0, cropRect.width, cropRect.height), cropRect.width, cropRect.height); + const croprect = cropRect.mapToItem(scaledImg, 0, 0, cropRect.width, cropRect.height); + const upscaledRect = Qt.rect(croprect.x * scaledImg.scaleX, croprect.y * scaledImg.scaleY, croprect.width * scaledImg.scaleX, croprect.height * scaledImg.scaleY); + Wallpapers.setCrop(delegate.modelData.name, upscaledRect, croprect, cropRect.zoom); + } + onWheel: wheel => { + let oldCenterX = cropRect.x + cropRect.width * 0.5; + let oldCenterY = cropRect.y + cropRect.height * 0.5; + + if (wheel.angleDelta.y > 0) + cropRect.zoom *= 1.1; + else + cropRect.zoom /= 1.1; + + cropRect.zoom = Math.max(1.0, Math.min(cropRect.zoom, 10.0)); + + cropRect.x = oldCenterX - cropRect.width * 0.5; + cropRect.y = oldCenterY - cropRect.height * 0.5; + + cropRect.clampToBounds(); + } + } + } + } + } + } + } +}