fix(StatusImageCrop): fix artefacts due to offscreen caching

Fix by disabling layering, leftover from the previous approach of not
using Canvas.

Also investigate TODO regarding artefacts workaround and implement
a proper approach

Fixes: #6640
This commit is contained in:
Stefan 2022-08-05 13:31:49 +02:00 committed by Michał Cieślak
parent fc7c0a07a5
commit 0a6c1bfcac
3 changed files with 209 additions and 32 deletions

View File

@ -1,11 +1,20 @@
import QtQuick 2.14 import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14 import QtQuick.Layouts 1.14
import QtQuick.Dialogs 1.3
import QtQml 2.14
import StatusQ.Controls 0.1 import StatusQ.Controls 0.1
import StatusQ.Components 0.1 import StatusQ.Components 0.1
import QtGraphicalEffects 1.14 import QtGraphicalEffects 1.14
import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Popups 0.1
Item { Item {
id: root id: root
@ -15,6 +24,12 @@ Item {
property var testControls: [ctrl1, ctrl2, ctrl3] property var testControls: [ctrl1, ctrl2, ctrl3]
property bool globalStylePreferRound: true property bool globalStylePreferRound: true
property var testImageList: ["qrc:/demoapp/data/logo-test-image.png",
"qrc:/demoapp/data/profile-image-2.jpeg",
"qrc:/demoapp/data/profile-image-1.jpeg"]
property int testImageIndex: 0
property int testSpacing: 0
property int testFrameMargins: 10
ColumnLayout { ColumnLayout {
id: mainLayout id: mainLayout
@ -22,7 +37,12 @@ Item {
anchors.fill: parent anchors.fill: parent
RowLayout { RowLayout {
spacing: root.testSpacing
ColumnLayout { ColumnLayout {
spacing: root.testSpacing
Layout.margins: root.testSpacing
Text { Text {
text: `AR: ${ctrl1.aspectRatio.toFixed(2)}` text: `AR: ${ctrl1.aspectRatio.toFixed(2)}`
} }
@ -32,8 +52,9 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
source: "qrc:/demoapp/data/logo-test-image.png" source: root.testImageList[root.testImageIndex]
windowStyle: globalStylePreferRound ? StatusImageCrop.WindowStyle.Rounded : StatusImageCrop.WindowStyle.Rectangular windowStyle: globalStylePreferRound ? StatusImageCrop.WindowStyle.Rounded : StatusImageCrop.WindowStyle.Rectangular
margins: root.testFrameMargins
} }
Text { Text {
text: `AR: ${ctrl2.aspectRatio.toFixed(2)}` text: `AR: ${ctrl2.aspectRatio.toFixed(2)}`
@ -43,13 +64,15 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
source: "qrc:/demoapp/data/logo-test-image.png" source: root.testImageList[root.testImageIndex]
windowStyle: globalStylePreferRound ? StatusImageCrop.WindowStyle.Rectangular : StatusImageCrop.WindowStyle.Rounded windowStyle: globalStylePreferRound ? StatusImageCrop.WindowStyle.Rectangular : StatusImageCrop.WindowStyle.Rounded
aspectRatio: 16/9 aspectRatio: 16/9
enableCheckers: true enableCheckers: true
margins: root.testFrameMargins + 2
} }
} }
ColumnLayout { ColumnLayout {
Layout.margins: root.testSpacing
Text { Text {
text: `AR: ${ctrl3.aspectRatio.toFixed(2)}` text: `AR: ${ctrl3.aspectRatio.toFixed(2)}`
} }
@ -59,36 +82,178 @@ Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
source: "qrc:/demoapp/data/logo-test-image.png" source: root.testImageList[root.testImageIndex]
windowStyle: globalStylePreferRound ? StatusImageCrop.WindowStyle.Rounded : StatusImageCrop.WindowStyle.Rectangular windowStyle: globalStylePreferRound ? StatusImageCrop.WindowStyle.Rounded : StatusImageCrop.WindowStyle.Rectangular
aspectRatio: 0.8 aspectRatio: 0.8
margins: root.testFrameMargins + 5.3
} }
} }
} }
RowLayout {
Shortcut { StatusButton {
sequence: StandardKey.ZoomIn text: qsTr("Cycle image")
onActivated: { onClicked: {
for(let i in testControls) { let newIndex = root.testImageIndex
const c = testControls[i] newIndex++
c.setCropRect(ctrl1.inflateRectBy(c.cropRect, -0.05)) if(newIndex >= root.testImageList.length)
newIndex = 0
root.testImageIndex = newIndex
} }
} }
} StatusButton {
text: qsTr("Cycle spacing")
Shortcut { onClicked: {
sequence: StandardKey.ZoomOut root.testSpacing += (root.testSpacing/2) + 1
onActivated: { if(root.testSpacing > 35)
for(let i in testControls) { root.testSpacing = 0
const c = testControls[i]
c.setCropRect(ctrl1.inflateRectBy(c.cropRect, 0.05))
} }
} }
} StatusButton {
text: qsTr("Cycle frame margins")
Shortcut { onClicked: {
sequences: ["Ctrl+W"] root.testFrameMargins += (root.testFrameMargins/2) + 1
onActivated: globalStylePreferRound = !globalStylePreferRound if(root.testFrameMargins > 50)
root.testFrameMargins = 0
}
}
StatusButton {
text: qsTr("Load external image")
onClicked: workflowLoader.active = true
}
} }
} }
Loader {
id: workflowLoader
sourceComponent: workflowComponent
active: false
onStatusChanged: {
if(status === Loader.Ready) {
item.imageFileDialogTitle = qsTr("Test Title")
item.title = "Test popup"
item.acceptButtonText = "Load Custom Image"
item.chooseImageToCrop()
}
}
Connections {
target: workflowLoader.item
function onImageCropped(image, cropRect) {
ctrl1.source = image
ctrl2.source = image
ctrl3.source = image
}
function onCanceled() {
workflowLoader.active = false
}
}
}
Component {
id: workflowComponent
Item {
id: workflowItem
property alias aspectRatio: imageCropper.aspectRatio
property alias windowStyle: imageCropper.windowStyle
/*required*/ property string imageFileDialogTitle: ""
/*required*/ property string title: ""
/*required*/ property string acceptButtonText: ""
property bool roundedImage: true
signal imageCropped(var image, var cropRect)
signal canceled()
function chooseImageToCrop() {
fileDialog.open()
}
FileDialog {
id: fileDialog
title: workflowItem.imageFileDialogTitle
folder: workflowItem.userSelectedImage ? imageCropper.source.substr(0, imageCropper.source.lastIndexOf("/")) : shortcuts.pictures
nameFilters: [qsTr("Supported image formats (%1)").arg("*.jpg *.jpeg *.jfif *.webp *.png *.heif")]
onAccepted: {
if (fileDialog.fileUrls.length > 0) {
imageCropper.source = fileDialog.fileUrls[0]
imageCropperModal.open()
}
}
onRejected: workflowItem.canceled()
} // FileDialog
StatusModal {
id: imageCropperModal
header.title: workflowItem.title
anchors.centerIn: Overlay.overlay
width: workflowItem.roundedImage ? 480 : 580
onClosed: workflowItem.canceled()
StatusImageCropPanel {
id: imageCropper
implicitHeight: workflowItem.roundedImage ? 350 : 370
anchors {
fill: parent
leftMargin: 10 * 2
rightMargin: 10 * 2
topMargin: 15
bottomMargin: 15
}
margins: workflowItem.roundedImage ? 10 : 20
windowStyle: workflowItem.roundedImage ? StatusImageCrop.WindowStyle.Rounded : StatusImageCrop.WindowStyle.Rectangular
enableCheckers: true
}
rightButtons: [
StatusButton {
text: workflowItem.acceptButtonText
enabled: imageCropper.sourceSize.width > 0 && imageCropper.sourceSize.height > 0
onClicked: {
workflowItem.imageCropped(imageCropper.source, imageCropper.cropRect)
imageCropperModal.close()
}
}
]
} // StatusModal
} // Item
}
Shortcut {
sequence: StandardKey.ZoomIn
onActivated: {
for(let i in testControls) {
const c = testControls[i]
c.setCropRect(ctrl1.inflateRectBy(c.cropRect, -0.05))
}
}
}
Shortcut {
sequence: StandardKey.ZoomOut
onActivated: {
for(let i in testControls) {
const c = testControls[i]
c.setCropRect(ctrl1.inflateRectBy(c.cropRect, 0.05))
}
}
}
Shortcut {
sequences: ["Ctrl+W"]
onActivated: globalStylePreferRound = !globalStylePreferRound
}
} }

View File

@ -142,6 +142,7 @@ Item {
anchors.top: parent.top anchors.top: parent.top
anchors.right: cropEditor.left anchors.right: cropEditor.left
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: -1
color: wallColor color: wallColor
opacity: wallTransparency opacity: wallTransparency
z: cropEditor.z + 1 z: cropEditor.z + 1
@ -160,8 +161,9 @@ Item {
StatusImageCrop { StatusImageCrop {
id: cropEditor id: cropEditor
anchors.centerIn: parent anchors.centerIn: parent
width: aspectRatio < cropSpaceItem.width/cropSpaceItem.height ? cropSpaceItem.height * aspectRatio : cropSpaceItem.width - root.margins * 2 readonly property bool cropARSmall: aspectRatio < cropSpaceItem.width/cropSpaceItem.height
height: aspectRatio < cropSpaceItem.width/cropSpaceItem.height ? cropSpaceItem.height - root.margins * 2 : cropSpaceItem.width / aspectRatio width: cropARSmall ? cropSpaceItem.height * aspectRatio : cropSpaceItem.width - root.margins * 2
height: cropARSmall ? cropSpaceItem.height - root.margins * 2 : cropSpaceItem.width / aspectRatio
} }
Rectangle { Rectangle {
@ -170,6 +172,7 @@ Item {
anchors.top: parent.top anchors.top: parent.top
anchors.right: parent.right anchors.right: parent.right
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: -1
color: wallColor color: wallColor
opacity: wallTransparency opacity: wallTransparency
z: cropEditor.z + 1 z: cropEditor.z + 1
@ -181,6 +184,7 @@ Item {
anchors.top: cropEditor.bottom anchors.top: cropEditor.bottom
anchors.right: rightOverlay.left anchors.right: rightOverlay.left
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: -1
color: wallColor color: wallColor
opacity: wallTransparency opacity: wallTransparency
z: cropEditor.z + 1 z: cropEditor.z + 1
@ -190,6 +194,7 @@ Item {
Canvas { Canvas {
visible: root.enableCheckers visible: root.enableCheckers
anchors.fill: parent anchors.fill: parent
anchors.bottomMargin: -1
onPaint: { onPaint: {
var ctx = getContext("2d") var ctx = getContext("2d")
for(let xI = 0; xI < Math.ceil(width/10); xI++) { for(let xI = 0; xI < Math.ceil(width/10); xI++) {

View File

@ -303,8 +303,6 @@ Item {
anchors.fill: parent anchors.fill: parent
layer.enabled: true
property bool widthFit: (root.width / root.aspectRatio) <= root.height property bool widthFit: (root.width / root.aspectRatio) <= root.height
// Window width // Window width
property real wW: widthFit ? root.width : root.height * root.aspectRatio property real wW: widthFit ? root.width : root.height * root.aspectRatio
@ -312,22 +310,31 @@ Item {
property real wH: widthFit ? root.width / root.aspectRatio : root.height property real wH: widthFit ? root.width / root.aspectRatio : root.height
onPaint: { onPaint: {
const wSize = Qt.size(wW, wH)
const contentSize = canvasSize
const clearSize = Qt.size(Math.ceil(canvasSize.width), Math.ceil(canvasSize.height))
const r = root.radius
var ctx = getContext("2d") var ctx = getContext("2d")
ctx.save() ctx.save()
// TODO: observed drawing artefacts at the edge. Draw one pixel more for now until root cause is found
ctx.clearRect(0, 0, width + 1, height + 1) ctx.clearRect(0, 0, clearSize.width, clearSize.height)
// Fill all with wallColor in order to clip the window from it // Fill all with wallColor in order to clip the window from it
ctx.fillStyle = Qt.rgba(root.wallColor.r, root.wallColor.g, root.wallColor.b, root.wallTransparency) ctx.fillStyle = Qt.rgba(root.wallColor.r, root.wallColor.g, root.wallColor.b, root.wallTransparency)
ctx.fillRect(0, 0, width + 1, height + 1) ctx.fillRect(0, 0, clearSize.width, clearSize.height)
// Cut opaque new pixels from background // Cut opaque new pixels from background
ctx.globalCompositeOperation = "source-out" ctx.globalCompositeOperation = "source-out"
// Draw the window // Draw the window
ctx.beginPath() ctx.beginPath()
const cW = Qt.rect((width - wW)/2, (height - wH)/2, wW, wH) const cW = Qt.rect((contentSize.width - wSize.width)/2, (contentSize.height - wSize.height)/2, wSize.width, wSize.height)
if(root.windowStyle === StatusImageCrop.WindowStyle.Rounded) if(root.windowStyle === StatusImageCrop.WindowStyle.Rounded)
ctx.ellipse(cW.x, cW.y, cW.width, cW.height) ctx.ellipse(cW.x, cW.y, cW.width, cW.height)
else if(root.windowStyle === StatusImageCrop.WindowStyle.Rectangular) else if(root.windowStyle === StatusImageCrop.WindowStyle.Rectangular)
ctx.roundedRect(cW.x, cW.y, cW.width, cW.height, root.radius, root.radius) ctx.roundedRect(cW.x, cW.y, cW.width, cW.height, r, r)
ctx.fill() ctx.fill()
ctx.restore() ctx.restore()
} }