// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 import QtQuick import HelperWidgets as HelperWidgets import StudioControls as StudioControls import StudioTheme as StudioTheme import EffectComposerBackend Column { id: root property real animatedTime: previewFrameTimer.elapsedTime property int animatedFrame: previewFrameTimer.currentFrame property bool timeRunning: previewAnimationRunning required property Item mainRoot property var effectComposerModel: EffectComposerBackend.effectComposerModel property alias source: source // The delay in ms to wait until updating the effect readonly property int updateDelay: 100 readonly property int previewMargin: 5 readonly property int extraMargin: 200 property real previewScale: 1 // Create a dummy parent to host the effect qml object function createNewComponent() { // If we have a working effect, do not show preview image as it shows through // transparent parts of the final image placeHolder.visible = false; var oldComponent = componentParent.children[0]; if (oldComponent) oldComponent.destroy(); try { const newObject = Qt.createQmlObject( effectComposerModel.qmlComponentString(), componentParent, "" ); effectComposerModel.resetEffectError(0); } catch (error) { let errorString = "QML: ERROR: "; let errorLine = -1; if (error.qmlErrors.length > 0) { // Show the first QML error let e = error.qmlErrors[0]; errorString += e.lineNumber + ": " + e.message; errorLine = e.lineNumber; } effectComposerModel.setEffectError(errorString, 0, errorLine); placeHolder.visible = true; } } Rectangle { // toolbar width: parent.width height: StudioTheme.Values.toolbarHeight color: StudioTheme.Values.themeToolbarBackground Row { spacing: 5 anchors.leftMargin: 5 anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter PreviewImagesComboBox { id: imagesComboBox mainRoot: root.mainRoot } StudioControls.ColorEditor { id: colorEditor actionIndicatorVisible: false showHexTextField: false color: "#dddddd" } } Row { spacing: 5 anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter HelperWidgets.AbstractButton { enabled: root.previewScale < 3 style: StudioTheme.Values.viewBarButtonStyle buttonIcon: StudioTheme.Constants.zoomIn_medium tooltip: qsTr("Zoom In") onClicked: { imageScaler.enableAnim(true) root.previewScale += .2 imageScaler.enableAnim(false) zoomIndicator.show() } } HelperWidgets.AbstractButton { enabled: root.previewScale > .4 style: StudioTheme.Values.viewBarButtonStyle buttonIcon: StudioTheme.Constants.zoomOut_medium tooltip: qsTr("Zoom out") onClicked: { imageScaler.enableAnim(true) root.previewScale -= .2 imageScaler.enableAnim(false) zoomIndicator.show() } } HelperWidgets.AbstractButton { enabled: root.previewScale !== 1 || imageScaler.x !== root.previewMargin || imageScaler.y !== root.previewMargin style: StudioTheme.Values.viewBarButtonStyle buttonIcon: StudioTheme.Constants.fitAll_medium tooltip: qsTr("Reset View") onClicked: { imageScaler.resetTransforms() } } } Row { spacing: 5 anchors.rightMargin: 5 anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter Column { Text { text: animatedTime >= 100 ? animatedTime.toFixed(1) + " s" : animatedTime.toFixed(3) + " s" color: StudioTheme.Values.themeTextColor font.pixelSize: 10 } Text { text: (animatedFrame).toString().padStart(6, '0') color: StudioTheme.Values.themeTextColor font.pixelSize: 10 } } HelperWidgets.AbstractButton { style: StudioTheme.Values.viewBarButtonStyle buttonIcon: StudioTheme.Constants.toStartFrame_medium tooltip: qsTr("Restart Animation") onClicked: { previewFrameTimer.reset() } } HelperWidgets.AbstractButton { style: StudioTheme.Values.viewBarButtonStyle buttonIcon: previewAnimationRunning ? StudioTheme.Constants.pause_medium : StudioTheme.Constants.playOutline_medium tooltip: qsTr("Play Animation") onClicked: { previewAnimationRunning = !previewAnimationRunning } } } } Rectangle { // preview image id: preview color: colorEditor.color width: parent.width height: root.height - y clip: true MouseArea { id: mouseArea anchors.fill: parent acceptedButtons: Qt.LeftButton property real pressX: 0 property real pressY: 0 property bool panning: false onPressed: { pressX = mouseX - imageScaler.x pressY = mouseY - imageScaler.y panning = true } onReleased: { panning = false } onWheel: (wheel) => { let oldPoint = imageScaler.mapFromItem(mouseArea, Qt.point(wheel.x, wheel.y)) if (wheel.angleDelta.y > 0) { if (root.previewScale < 3) root.previewScale += .2 } else { if (root.previewScale > .4) root.previewScale -= .2 } let newPoint = imageScaler.mapFromItem(mouseArea, Qt.point(wheel.x, wheel.y)) imageScaler.x -= (oldPoint.x - newPoint.x) * imageScaler.scale imageScaler.y -= (oldPoint.y - newPoint.y) * imageScaler.scale imageScaler.checkBounds() zoomIndicator.show() } Timer { // pan timer running: parent.panning interval: 16 repeat: true onTriggered: { imageScaler.x = mouseArea.mouseX - mouseArea.pressX imageScaler.y = mouseArea.mouseY - mouseArea.pressY imageScaler.checkBounds() } } } Image { id: placeHolder anchors.fill: parent anchors.margins: root.previewMargin fillMode: Image.PreserveAspectFit source: imagesComboBox.selectedImage smooth: true } Item { // Source item as a canvas (render target) for effect id: source width: sourceImage.sourceSize.width height: sourceImage.sourceSize.height layer.enabled: true layer.mipmap: true layer.smooth: true layer.sourceRect: Qt.rect(-root.extraMargin, -root.extraMargin, width + root.extraMargin * 2, height + root.extraMargin * 2) visible: false Image { id: sourceImage onSourceChanged: imageScaler.resetTransforms() fillMode: Image.Pad source: imagesComboBox.selectedImage smooth: true } } BlurHelper { id: blurHelper source: source property int blurMax: g_propertyData.blur_helper_max_level ? g_propertyData.blur_helper_max_level : 64 property real blurMultiplier: g_propertyData.blurMultiplier ? g_propertyData.blurMultiplier : 0 } Item { id: imageScaler x: root.previewMargin y: root.previewMargin width: parent.width - root.previewMargin * 2 height: parent.height - root.previewMargin * 2 scale: root.previewScale * (width > height ? height / sourceImage.sourceSize.height : width / sourceImage.sourceSize.width) Behavior on x { id: xBehavior enabled: false NumberAnimation { duration: 200 easing.type: Easing.OutQuad } } Behavior on y { id: yBehavior enabled: false NumberAnimation { duration: 200 easing.type: Easing.OutQuad } } Behavior on scale { id: scaleBehavior enabled: false NumberAnimation { duration: 200 easing.type: Easing.OutQuad } } function checkBounds() { let edgeMargin = 10 // correction factor to account for an observation that edgeMargin decreases // with increased zoom let corrFactor = 10 * imageScaler.scale let imgW2 = sourceImage.paintedWidth * imageScaler.scale * .5 let imgH2 = sourceImage.paintedHeight * imageScaler.scale * .5 let srcW2 = width * .5 let srcH2 = height * .5 if (imageScaler.x < -srcW2 - imgW2 + edgeMargin + corrFactor) imageScaler.x = -srcW2 - imgW2 + edgeMargin + corrFactor else if (x > srcW2 + imgW2 - edgeMargin - corrFactor) imageScaler.x = srcW2 + imgW2 - edgeMargin - corrFactor if (imageScaler.y < -srcH2 - imgH2 + edgeMargin + corrFactor) imageScaler.y = -srcH2 - imgH2 + edgeMargin + corrFactor else if (y > srcH2 + imgH2 - edgeMargin - corrFactor) imageScaler.y = srcH2 + imgH2 - edgeMargin - corrFactor } function resetTransforms() { imageScaler.enableAnim(true) root.previewScale = 1 imageScaler.x = root.previewMargin imageScaler.y = root.previewMargin imageScaler.enableAnim(false) } function enableAnim(flag) { xBehavior.enabled = flag yBehavior.enabled = flag scaleBehavior.enabled = flag } Item { id: componentParent width: source.width height: source.height anchors.centerIn: parent } } Rectangle { id: zoomIndicator width: 40 height: 20 color: StudioTheme.Values.themeDialogBackground visible: false function show() { zoomIndicator.visible = true zoomIndicatorTimer.start() } Text { text: Math.round(root.previewScale * 100) + "%" color: StudioTheme.Values.themeTextColor anchors.centerIn: parent } Timer { id: zoomIndicatorTimer interval: 1000 onTriggered: zoomIndicator.visible = false } } Connections { target: effectComposerModel function onShadersBaked() { updateTimer.restart() } } Timer { id: updateTimer interval: updateDelay onTriggered: { effectComposerModel.updateQmlComponent() createNewComponent() } } } }