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:
parent
fc7c0a07a5
commit
0a6c1bfcac
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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++) {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue