diff --git a/Modules/Settings/Categories/Background.qml b/Modules/Settings/Categories/Background.qml index 4f7390e..233c31f 100644 --- a/Modules/Settings/Categories/Background.qml +++ b/Modules/Settings/Categories/Background.qml @@ -36,7 +36,6 @@ SettingsPage { } WallpaperCropper { - screen: root.screen } } diff --git a/Modules/Settings/Controls/WallpaperCropper.qml b/Modules/Settings/Controls/WallpaperCropper.qml index 1bc84f5..bd3ff93 100644 --- a/Modules/Settings/Controls/WallpaperCropper.qml +++ b/Modules/Settings/Controls/WallpaperCropper.qml @@ -9,213 +9,250 @@ import qs.Config import qs.Components import qs.Helpers -RowLayout { - id: root +Item { + id: wrapper - required property ShellScreen screen + property bool changesMade: false + + signal requestCrop Layout.fillWidth: true Layout.preferredHeight: 400 - spacing: Appearance.spacing.normal - Repeater { - model: ScriptModel { - values: [...Quickshell.screens].sort((a, b) => { - return a.x - b.x; - }) + IconButton { + anchors.margins: Appearance.padding.normal + anchors.right: parent.right + anchors.top: parent.top + icon: "check" + opacity: wrapper.changesMade ? 1 : 0 + scale: wrapper.changesMade ? 1 : 0 + z: 2 + + Behavior on opacity { + Anim { + } + } + Behavior on scale { + Anim { + } } - Item { - id: delegate + onClicked: { + wrapper.requestCrop(); + wrapper.changesMade = false; + } + } - required property ShellScreen modelData + RowLayout { + id: root - function zoomClipRect(zoom: real): void { - let oldCenterX = cropRect.x + cropRect.width * 0.5; - let oldCenterY = cropRect.y + cropRect.height * 0.5; + anchors.fill: parent + spacing: Appearance.spacing.normal - cropRect.zoom = zoom; - - cropRect.x = oldCenterX - cropRect.width * 0.5; - cropRect.y = oldCenterY - cropRect.height * 0.5; - - cropRect.clampToBounds(); + Repeater { + model: ScriptModel { + values: [...Quickshell.screens].sort((a, b) => { + return a.x - b.x; + }) } - Layout.fillHeight: true - Layout.fillWidth: true + Item { + id: delegate - RowLayout { - id: sliderLayout + required property ShellScreen modelData - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - implicitHeight: 30 - spacing: Appearance.spacing.large - - CustomText { - text: qsTr("Crop scale") + function applyCrop(): void { + const croprect = cropRect.mapToItem(scaledImg, 0, 0, cropRect.width, cropRect.height); + const upscaledRect = Qt.rect((croprect.x - cropRect.imageX) / scaledImg.paintedWidth, (croprect.y - cropRect.imageY) / scaledImg.paintedHeight, croprect.width / scaledImg.paintedWidth, croprect.height / scaledImg.paintedHeight); + Wallpapers.setCrop(delegate.modelData.name, upscaledRect, croprect, cropRect.zoom); } - CustomSlider { - id: zoomSlider + function zoomClipRect(zoom: real): void { + let oldCenterX = cropRect.x + cropRect.width * 0.5; + let oldCenterY = cropRect.y + cropRect.height * 0.5; - Layout.fillWidth: true - Layout.preferredHeight: 10 - from: 1.0 - to: 5.0 - value: cropRect.zoom + cropRect.zoom = zoom; - onMoved: delegate.zoomClipRect(value) - } - } + cropRect.x = oldCenterX - cropRect.width * 0.5; + cropRect.y = oldCenterY - cropRect.height * 0.5; - CachingImage { - id: scaledImg - - property var displayData - property real monitorScale: 1.0 - - anchors.bottom: sliderLayout.top - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - asynchronous: true - fillMode: Image.PreserveAspectFit - // retainWhileLoading: true - source: Wallpapers.current - sourceSize.height: parent.height - sourceSize.width: parent.width - - onPaintedWidthChanged: { - if (paintedWidth > 0) { - scaledImg.displayData = Wallpapers.getCrop(delegate.modelData.name); - cropRect.zoom = Wallpapers.getCrop(delegate.modelData.name).zoom; - cropRect.restoreFromData(); - } + cropRect.clampToBounds(); } - CustomText { - id: monitorId + Layout.fillHeight: true + Layout.fillWidth: true - anchors.centerIn: parent - color: Qt.alpha(DynamicColors.palette.m3surface, 0.85) - font.pointSize: Appearance.font.size.large * 4 - style: Text.Outline - styleColor: DynamicColors.palette.m3onSurface - text: delegate.modelData.name + Connections { + function onRequestCrop(): void { + delegate.applyCrop(); + } + + target: wrapper } - CustomRect { - id: cropRect + RowLayout { + id: sliderLayout - property real aspectRatio: delegate.modelData.width / delegate.modelData.height - readonly property real baseHeight: baseWidth / aspectRatio - readonly property real baseWidth: { - let fittedHeight = scaledImg.paintedHeight; - let fittedWidth = fittedHeight * aspectRatio; + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + implicitHeight: 30 + spacing: Appearance.spacing.large - if (fittedWidth > scaledImg.paintedWidth) { - fittedWidth = scaledImg.paintedWidth; - fittedHeight = fittedWidth / aspectRatio; - } - - return fittedWidth; - } - readonly property real imageX: (scaledImg.width - scaledImg.paintedWidth) / 2 - readonly property real imageY: (scaledImg.height - scaledImg.paintedHeight) / 2 - property real imgAspectRatio: scaledImg.paintedWidth / scaledImg.paintedHeight - property real zoom: scaledImg.displayData.zoom - - function centerInImage() { - x = imageX + (scaledImg.paintedWidth - width) / 2; - y = imageY + (scaledImg.paintedHeight - height) / 2; + CustomText { + text: qsTr("Crop scale") } - function clampToBounds() { - x = Math.max(imageX, Math.min(x, imageX + scaledImg.paintedWidth - width)); + CustomSlider { + id: zoomSlider - y = Math.max(imageY, Math.min(y, imageY + scaledImg.paintedHeight - height)); - } + Layout.fillWidth: true + Layout.preferredHeight: 10 + from: 1.0 + to: 5.0 + value: cropRect.zoom - function restoreFromData() { - let data = scaledImg.displayData; - - if (data && data.scaledX !== 0 || data.scaledY !== 0 || data.scaledWidth !== 0 || data.scaledHeight !== 0) { - x = data.scaledX; - y = data.scaledY; - - clampToBounds(); - } else { - zoom = 1.0; - centerInImage(); + onMoved: { + delegate.zoomClipRect(value); + wrapper.changesMade = true; } } - - border.color: "lime" - border.width: 2 - height: baseHeight / zoom - opacity: zoom > 1.0 ? 1 : Math.abs(aspectRatio - imgAspectRatio) > 0.1 ? 1 : 0 - width: baseWidth / zoom - - Behavior on opacity { - Anim { - } - } - - onHeightChanged: clampToBounds() - onWidthChanged: clampToBounds() } - MouseArea { - id: mouse + CachingImage { + id: scaledImg - function updateCrop(mouseX, mouseY) { - let nx = mouseX - cropRect.width * 0.5; - let ny = mouseY - cropRect.height * 0.5; + property var displayData + property real monitorScale: 1.0 - nx = Math.max(cropRect.imageX, Math.min(nx, cropRect.imageX + scaledImg.paintedWidth - cropRect.width)); + anchors.bottom: sliderLayout.top + anchors.bottomMargin: Appearance.spacing.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + asynchronous: true + fillMode: Image.PreserveAspectFit + // retainWhileLoading: true + source: Wallpapers.current + sourceSize.height: parent.height + sourceSize.width: parent.width - ny = Math.max(cropRect.imageY, Math.min(ny, cropRect.imageY + scaledImg.paintedHeight - cropRect.height)); + onPaintedWidthChanged: { + if (paintedWidth > 0) { + scaledImg.displayData = Wallpapers.getCrop(delegate.modelData.name); + cropRect.zoom = Wallpapers.getCrop(delegate.modelData.name).zoom; + cropRect.restoreFromData(); + } + } + onSourceChanged: cropRect.clampToBounds() + onStatusChanged: if (scaledImg.status == Image.Ready) + cropRect.clampToBounds() - cropRect.x = nx; - cropRect.y = ny; + CustomText { + id: monitorId + + anchors.centerIn: parent + color: Qt.alpha(DynamicColors.palette.m3surface, 0.85) + font.pointSize: Appearance.font.size.large * 4 + style: Text.Outline + styleColor: DynamicColors.palette.m3onSurface + text: delegate.modelData.name } - 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.paintedHeight; + let fittedWidth = fittedHeight * aspectRatio; + + if (fittedWidth > scaledImg.paintedWidth) { + fittedWidth = scaledImg.paintedWidth; + fittedHeight = fittedWidth / aspectRatio; + } + + return fittedWidth; + } + readonly property real imageX: (scaledImg.width - scaledImg.paintedWidth) / 2 + readonly property real imageY: (scaledImg.height - scaledImg.paintedHeight) / 2 + property real imgAspectRatio: scaledImg.paintedWidth / scaledImg.paintedHeight + 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) { + x = data.scaledX; + y = data.scaledY; + + clampToBounds(); + } else { + zoom = 1.0; + centerInImage(); + } + } + + 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 { + 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); + wrapper.changesMade = true; + } + } + onPressed: mouse => { updateCrop(mouse.x, mouse.y); - } - onPressed: mouse => { - updateCrop(mouse.x, mouse.y); - } - onReleased: { - if (cropRect.opacity > 0) { - const croprect = cropRect.mapToItem(scaledImg, 0, 0, cropRect.width, cropRect.height); - const upscaledRect = Qt.rect((croprect.x - cropRect.imageX) / scaledImg.paintedWidth, (croprect.y - cropRect.imageY) / scaledImg.paintedHeight, croprect.width / scaledImg.paintedWidth, croprect.height / scaledImg.paintedHeight); - Wallpapers.setCrop(delegate.modelData.name, upscaledRect, croprect, cropRect.zoom); + wrapper.changesMade = true; + } + onReleased: { + wrapper.changesMade = true; } - } - 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, 5.0)); - - cropRect.x = oldCenterX - cropRect.width * 0.5; - cropRect.y = oldCenterY - cropRect.height * 0.5; - - cropRect.clampToBounds(); } } }