feat(StatusImageCropPanel): Add image crop editor panel component
New QML component StatusQ.Components.StatusImageCropPanel that extends on StatusImageCrop with simper interface and extra features Features: - Minimizes drawing with Canvas to the crop window - Adds user interactions: pan, zoom - Zoom slider for zooming, beside the mouse scroll action - Optional checker pattern for background for the user to have a visual feedback on the transparent areas or image margins fixes: #5401 updates: #5118
This commit is contained in:
parent
107a3d1d34
commit
18ecb2b140
Binary file not shown.
After Width: | Height: | Size: 44 KiB |
|
@ -0,0 +1,283 @@
|
|||
import QtQuick 2.14
|
||||
import QtQuick.Layouts 1.14
|
||||
|
||||
import StatusQ.Core 0.1
|
||||
import StatusQ.Controls 0.1
|
||||
|
||||
/*!
|
||||
\qmltype StatusImageCropPanel
|
||||
\inherits Item
|
||||
\inqmlmodule StatusQ.Components
|
||||
\since StatusQ.Components 0.1
|
||||
\brief Draw a crop-window onto an image and allows manipulating the position of the crop-window. Inherits \l{https://doc.qt.io/qt-5/qml-qtquick-item.html}{Item}.
|
||||
|
||||
Adds mouse pan and zoom functionality on top of StatusImageCrop functionality. Also adds small
|
||||
practical features and optimizes drawing by minimizing StatusImageCrop area
|
||||
|
||||
\sa StatusImageCrop for more details
|
||||
|
||||
Example of how to use it:
|
||||
|
||||
\qml
|
||||
StatusImageCropPanel {
|
||||
width: 400
|
||||
height: 200
|
||||
source: "qrc:/demoapp/data/logo-test-image.png"
|
||||
windowStyle: StatusImageCrop.WindowStyle.Rectangular
|
||||
Component.onCompleted: setCropRect(Qt.rect(10, 0, sourceSize.width - 20, sourceSize.height))
|
||||
}
|
||||
\endqml
|
||||
|
||||
For a list of components available see StatusQ.
|
||||
*/
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitWidth: mainLayout.implicitWidth
|
||||
implicitHeight: mainLayout.implicitHeight
|
||||
|
||||
/*!
|
||||
\qmlproperty real StatusImageCrop::aspectRatio
|
||||
Initial aspect-ratio of the crop-window
|
||||
*/
|
||||
property real aspectRatio: 1
|
||||
/*!
|
||||
\qmlproperty bool StatusImageCrop::interactive
|
||||
If true allows user to interact with the image. Set to false for previewing existing crop-data
|
||||
*/
|
||||
property bool interactive: true
|
||||
/*!
|
||||
\qmlproperty bool StatusImageCrop::margins
|
||||
Space to keep around the control borders and crop area
|
||||
*/
|
||||
property int margins: 10
|
||||
|
||||
/*!
|
||||
\qmlproperty url StatusImageCropPanel::source
|
||||
\sa StatusImageCrop::source
|
||||
*/
|
||||
/*required*/ property alias source: cropEditor.source
|
||||
|
||||
/*!
|
||||
\qmlproperty WindowStyle StatusImageCropPanel::windowStyle
|
||||
\sa StatusImageCrop::windowStyle
|
||||
*/
|
||||
property alias windowStyle: cropEditor.windowStyle
|
||||
/*!
|
||||
\qmlproperty int StatusImageCropPanel::radius
|
||||
\sa StatusImageCrop::radius
|
||||
*/
|
||||
property alias radius: cropEditor.radius
|
||||
/*!
|
||||
\qmlproperty color StatusImageCropPanel::wallColor
|
||||
\sa StatusImageCrop::wallColor
|
||||
*/
|
||||
property alias wallColor: cropEditor.wallColor
|
||||
/*!
|
||||
\qmlproperty real StatusImageCropPanel::wallTransparency
|
||||
\sa StatusImageCrop::wallTransparency
|
||||
*/
|
||||
property alias wallTransparency: cropEditor.wallTransparency
|
||||
/*!
|
||||
\qmlproperty rect StatusImageCropPanel::cropRect
|
||||
\sa StatusImageCrop::cropRect
|
||||
*/
|
||||
property alias cropRect: cropEditor.cropRect
|
||||
/*!
|
||||
\qmlproperty rect StatusImageCropPanel::cropRect
|
||||
\sa StatusImageCrop::cropRect
|
||||
*/
|
||||
readonly property alias cropWindow: cropEditor.cropWindow
|
||||
/*!
|
||||
\qmlproperty real StatusImageCrop::scrollZoomFactor
|
||||
How fast is image scaled (zoomed) when using mouse scroll
|
||||
*/
|
||||
property real scrollZoomFactor: 0.5
|
||||
/*!
|
||||
\qmlproperty bool StatusImageCropPanel::enableCheckers
|
||||
Shows helper guiding checkers pattern where image is not covering
|
||||
*/
|
||||
property bool enableCheckers: root.interactive
|
||||
/*!
|
||||
\qmlproperty size StatusImageCropPanel::sourceSize
|
||||
\sa StatusImageCrop::sourceSize
|
||||
*/
|
||||
property alias sourceSize: cropEditor.sourceSize
|
||||
|
||||
/*
|
||||
\qmlmethod StatusImageCropPanel::setCropRect(rect)
|
||||
\sa StatusImageCrop::cropRect
|
||||
*/
|
||||
function setCropRect(newRect) {
|
||||
cropEditor.setCropRect(newRect)
|
||||
aspectRatio = cropEditor.aspectRatio
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: d
|
||||
|
||||
function updateAspectRatio(newAR) {
|
||||
// Keep width and adjust height
|
||||
const eW = cropEditor.cropRect.width
|
||||
const w = (eW <= 0) ? cropEditor.sourceSize.width : eW
|
||||
const h = w/newAR
|
||||
const c = (eW <= 0)
|
||||
? Qt.point(cropEditor.sourceSize.width/2, cropEditor.sourceSize.height/2)
|
||||
: Qt.point(cropEditor.cropRect.x + w/2, cropEditor.cropRect.y + cropEditor.cropRect.height/2)
|
||||
const nR = Qt.rect(c.x - w/2, c.y-h/2, w, h)
|
||||
cropEditor.setCropRect(nR)
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: d.updateAspectRatio(root.aspectRatio)
|
||||
onAspectRatioChanged: d.updateAspectRatio(root.aspectRatio)
|
||||
onSourceSizeChanged: d.updateAspectRatio(root.aspectRatio)
|
||||
|
||||
ColumnLayout {
|
||||
id: mainLayout
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Item {
|
||||
id: cropSpaceItem
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
id: leftOverlay
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.right: cropEditor.left
|
||||
anchors.bottom: parent.bottom
|
||||
color: wallColor
|
||||
opacity: wallTransparency
|
||||
z: cropEditor.z + 1
|
||||
}
|
||||
Rectangle {
|
||||
id: topOverlay
|
||||
anchors.left: leftOverlay.right
|
||||
anchors.top: parent.top
|
||||
anchors.right: rightOverlay.left
|
||||
anchors.bottom: cropEditor.top
|
||||
color: wallColor
|
||||
opacity: wallTransparency
|
||||
z: cropEditor.z + 1
|
||||
}
|
||||
|
||||
StatusImageCrop {
|
||||
id: cropEditor
|
||||
anchors.centerIn: parent
|
||||
width: aspectRatio < cropSpaceItem.width/cropSpaceItem.height ? cropSpaceItem.height * aspectRatio : cropSpaceItem.width - root.margins * 2
|
||||
height: aspectRatio < cropSpaceItem.width/cropSpaceItem.height ? cropSpaceItem.height - root.margins * 2 : cropSpaceItem.width / aspectRatio
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: rightOverlay
|
||||
anchors.left: cropEditor.right
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
color: wallColor
|
||||
opacity: wallTransparency
|
||||
z: cropEditor.z + 1
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: bottomOverlay
|
||||
anchors.left: leftOverlay.right
|
||||
anchors.top: cropEditor.bottom
|
||||
anchors.right: rightOverlay.left
|
||||
anchors.bottom: parent.bottom
|
||||
color: wallColor
|
||||
opacity: wallTransparency
|
||||
z: cropEditor.z + 1
|
||||
}
|
||||
|
||||
// Checkers
|
||||
Canvas {
|
||||
visible: root.enableCheckers
|
||||
anchors.fill: parent
|
||||
onPaint: {
|
||||
var ctx = getContext("2d")
|
||||
for(let xI = 0; xI < Math.ceil(width/10); xI++) {
|
||||
for(let yI = 0; yI < Math.ceil(height/10); yI++) {
|
||||
ctx.fillStyle = (xI % 2) === (yI % 2) ? "#FFFFFE" : "#DBDBDB"
|
||||
ctx.fillRect(xI * 10, yI * 10, 10, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
z: cropEditor.z - 1
|
||||
}
|
||||
|
||||
// Drag and zoom
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
enabled: root.interactive
|
||||
|
||||
property var lastDragPoint: null
|
||||
onReleased: lastDragPoint = null
|
||||
|
||||
onMouseXChanged: updateDrag(Qt.point(mouse.x, mouse.y))
|
||||
onMouseYChanged: updateDrag(Qt.point(mouse.x, mouse.y))
|
||||
|
||||
onWheel: {
|
||||
const delta = wheel.angleDelta.y / 120
|
||||
cropEditor.setCropRect(cropEditor.getZoomRect(cropEditor.zoomScale + delta * root.scrollZoomFactor))
|
||||
}
|
||||
|
||||
function moveRect(r /*rect*/, delta /*real*/) {
|
||||
return Qt.rect(r.x + delta.x, r.y + delta.y, r.width, r.height)
|
||||
}
|
||||
|
||||
function scaleSize(sz /*size*/, s /*size*/) {
|
||||
return Qt.point(sz.width * s, sz.height * s)
|
||||
}
|
||||
|
||||
function updateDrag(p) {
|
||||
let delta = (lastDragPoint ? Qt.size(lastDragPoint.x - p.x, lastDragPoint.y - p.y) : Qt.size(0, 0))
|
||||
delta = scaleSize(delta, 1/cropEditor.scrToImgScale)
|
||||
cropEditor.setCropRect(moveRect(cropEditor.cropRect, delta))
|
||||
lastDragPoint = p
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: root.interactive
|
||||
|
||||
StatusIcon {
|
||||
icon: "remove-circle"
|
||||
|
||||
Layout.preferredWidth: 20
|
||||
Layout.preferredHeight: 20
|
||||
}
|
||||
StatusSlider {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 5
|
||||
Layout.topMargin: 20
|
||||
Layout.bottomMargin: 25
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
enabled: root.interactive
|
||||
|
||||
from: 1
|
||||
value: cropEditor.zoomScale
|
||||
live: false
|
||||
onMoved: cropEditor.setCropRect(cropEditor.getZoomRect(valueAt(visualPosition)))
|
||||
|
||||
to: 10
|
||||
}
|
||||
StatusIcon {
|
||||
icon: "add-circle"
|
||||
|
||||
Layout.preferredWidth: 20
|
||||
Layout.preferredHeight: 20
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -32,3 +32,4 @@ StatusMessageDetails 0.1 StatusMessageDetails.qml
|
|||
StatusTagSelector 0.1 StatusTagSelector.qml
|
||||
StatusToastMessage 0.1 StatusToastMessage.qml
|
||||
StatusWizardStepper 0.1 StatusWizardStepper.qml
|
||||
StatusImageCropPanel 0.1 StatusImageCropPanel.qml
|
||||
|
|
Loading…
Reference in New Issue