Files
qt-creator/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectComposer.qml
Ali Kianian 34f51df1ea QmlDesigner: Add code editor for Effect Composer
Task-number: QDS-13443
Change-Id: I02c7a85336f283e0e55bab24459a91fa299abb40
Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io>
Reviewed-by: Miikka Heikkinen <miikka.heikkinen@qt.io>
2024-10-07 08:00:11 +00:00

424 lines
16 KiB
QML

// 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 QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Templates as T
import HelperWidgets as HelperWidgets
import StudioControls as StudioControls
import StudioTheme as StudioTheme
import EffectComposerBackend
Item {
id: root
readonly property var backendModel: EffectComposerBackend.effectComposerModel
property var draggedSec: null
property var secsY: []
property int moveFromIdx: 0
property int moveToIdx: 0
property bool previewAnimationRunning: false
property var expandStates: null
// Invoked after save changes is done
property var onSaveChangesCallback: () => {}
// Invoked from C++ side when open composition is requested and there are unsaved changes
function promptToSaveBeforeOpen() {
root.onSaveChangesCallback = () => { EffectComposerBackend.rootView.doOpenComposition() }
saveChangesDialog.open()
}
// Invoked from C++ side before resetting the model to store current expanded state of nodes
function storeExpandStates() {
root.expandStates = new Map()
for (let i = 0; i < repeater.count; ++i) {
var curItem = repeater.itemAt(i)
root.expandStates.set(curItem.caption, curItem.expanded)
}
}
// Invoked after model has been reset to restore expanded state for nodes
function restoreExpandStates() {
if (root.expandStates) {
for (let i = 0; i < repeater.count; ++i) {
var curItem = repeater.itemAt(i)
if (root.expandStates.has(curItem.caption))
curItem.expanded = root.expandStates.get(curItem.caption)
}
root.expandStates = null
}
}
function handleDragMove() {
dragTimer.stop()
if (root.secsY.length === 0) {
for (let i = 0; i < repeater.count; ++i)
root.secsY[i] = repeater.itemAt(i).y
}
let oldContentY = scrollView.contentY
if (root.draggedSec.y < scrollView.dragScrollMargin + scrollView.contentY
&& scrollView.contentY > 0) {
scrollView.contentY -= scrollView.dragScrollMargin / 2
} else if (root.draggedSec.y > scrollView.contentY + scrollView.height - scrollView.dragScrollMargin
&& scrollView.contentY < scrollView.contentHeight - scrollView.height) {
scrollView.contentY += scrollView.dragScrollMargin / 2
if (scrollView.contentY > scrollView.contentHeight - scrollView.height)
scrollView.contentY = scrollView.contentHeight - scrollView.height
}
if (scrollView.contentY < 0)
scrollView.contentY = 0
if (oldContentY !== scrollView.contentY) {
// Changing dragged section position in drag handler doesn't seem to stick
// when triggered by mouse move, so do it again async
dragTimer.targetY = root.draggedSec.y - oldContentY + scrollView.contentY
dragTimer.restart()
dragConnection.enabled = false
root.draggedSec.y = dragTimer.targetY
dragConnection.enabled = true
}
root.moveToIdx = root.moveFromIdx
for (let i = 0; i < repeater.count; ++i) {
let currItem = repeater.itemAt(i)
if (i > root.moveFromIdx) {
if (root.draggedSec.y > currItem.y) {
currItem.y = root.secsY[i] - root.draggedSec.height - nodesCol.spacing
root.moveToIdx = i
} else {
currItem.y = root.secsY[i]
}
} else if (i < root.moveFromIdx) {
if (!repeater.model.isDependencyNode(i) && root.draggedSec.y < currItem.y) {
currItem.y = root.secsY[i] + root.draggedSec.height + nodesCol.spacing
root.moveToIdx = Math.min(root.moveToIdx, i)
} else {
currItem.y = root.secsY[i]
}
}
}
}
Connections {
target: root.backendModel
function onIsEmptyChanged() {
if (root.backendModel.isEmpty)
saveAsDialog.close()
}
}
SaveAsDialog {
id: saveAsDialog
anchors.centerIn: parent
}
SaveChangesDialog {
id: saveChangesDialog
anchors.centerIn: parent
onSave: {
if (root.backendModel.currentComposition === "") {
// if current composition is unsaved, show save as dialog and clear afterwards
saveAsDialog.clearOnClose = true
saveAsDialog.open()
} else {
root.onSaveChangesCallback()
}
}
onDiscard: {
root.onSaveChangesCallback()
}
}
ConfirmClearAllDialog {
id: confirmClearAllDialog
anchors.centerIn: parent
}
Connections {
id: dragConnection
target: root.draggedSec
function onYChanged() { root.handleDragMove() }
}
Timer {
id: dragTimer
running: false
interval: 16
repeat: false
property real targetY: -1
onTriggered: {
// Ensure we get position change triggers even if user holds mouse still to
// make scrolling smooth
root.draggedSec.y = targetY
root.handleDragMove()
}
}
ColumnLayout {
anchors.fill: parent
spacing: 1
EffectComposerTopBar {
Layout.fillWidth: true
onAddClicked: {
root.onSaveChangesCallback = () => { root.backendModel.clear(true) }
if (root.backendModel.hasUnsavedChanges)
saveChangesDialog.open()
else
root.backendModel.clear(true)
}
onSaveClicked: {
let name = root.backendModel.currentComposition
if (name === "")
saveAsDialog.open()
else
root.backendModel.saveComposition(name)
}
onSaveAsClicked: saveAsDialog.open()
onAssignToSelectedClicked: {
root.backendModel.assignToSelected()
}
onOpenShadersCodeEditor: {
root.backendModel.openMainShadersCodeEditor()
}
}
SplitView {
id: splitView
Layout.fillWidth: true
Layout.fillHeight: true
orientation: root.width > root.height ? Qt.Horizontal : Qt.Vertical
handle: Rectangle {
implicitWidth: splitView.orientation === Qt.Horizontal ? StudioTheme.Values.splitterThickness : splitView.width
implicitHeight: splitView.orientation === Qt.Horizontal ? splitView.height : StudioTheme.Values.splitterThickness
color: T.SplitHandle.pressed ? StudioTheme.Values.themeSliderHandleInteraction
: (T.SplitHandle.hovered ? StudioTheme.Values.themeSliderHandleHover
: "transparent")
}
EffectComposerPreview {
mainRoot: root
SplitView.minimumWidth: 250
SplitView.minimumHeight: 200
SplitView.preferredWidth: 300
SplitView.preferredHeight: 300
Layout.fillWidth: true
Layout.fillHeight: true
FrameAnimation {
id: previewFrameTimer
running: true
paused: !root.previewAnimationRunning
}
}
Column {
spacing: 1
SplitView.minimumWidth: 250
SplitView.minimumHeight: 100
Component.onCompleted: HelperWidgets.Controller.mainScrollView = scrollView
Rectangle {
width: parent.width
height: StudioTheme.Values.toolbarHeight
color: StudioTheme.Values.themeToolbarBackground
EffectNodesComboBox {
id: nodesComboBox
mainRoot: root
anchors.verticalCenter: parent.verticalCenter
x: 5
width: parent.width - 50
}
HelperWidgets.AbstractButton {
anchors.right: parent.right
anchors.rightMargin: 5
anchors.verticalCenter: parent.verticalCenter
style: StudioTheme.Values.viewBarButtonStyle
buttonIcon: StudioTheme.Constants.clearList_medium
tooltip: qsTr("Remove all effect nodes.")
enabled: root.backendModel ? !root.backendModel.isEmpty : false
onClicked: {
if (root.backendModel.hasUnsavedChanges)
confirmClearAllDialog.open()
else
root.backendModel.clear()
}
}
HelperWidgets.AbstractButton {
anchors.right: parent.right
anchors.rightMargin: 5
anchors.verticalCenter: parent.verticalCenter
style: StudioTheme.Values.viewBarButtonStyle
buttonIcon: StudioTheme.Constants.code
tooltip: qsTr("Open Shader in Code Editor.")
visible: false // TODO: to be implemented
onClicked: {} // TODO
}
}
Item {
width: parent.width
height: parent.height - y
HelperWidgets.ScrollView {
id: scrollView
readonly property int dragScrollMargin: 50
anchors.fill: parent
clip: true
interactive: !HelperWidgets.Controller.contextMenuOpened
onContentHeightChanged: {
// Expand states are stored before full model reset.
// Content height change indicates the model has been updated after full
// reset, so we restore expand states if any are stored.
root.restoreExpandStates()
// If content height change was because a recent node addition, we want to
// scroll to the end of the content so the newly added item is visible.
if (nodesComboBox.nodeJustAdded && scrollView.contentItem.height > scrollView.height) {
let lastItemH = repeater.itemAt(repeater.count - 1).height
scrollView.contentY = scrollView.contentItem.height - lastItemH
nodesComboBox.nodeJustAdded = false
}
}
Column {
id: nodesCol
width: scrollView.width
spacing: 1
Repeater {
id: repeater
width: parent.width
model: root.backendModel
onCountChanged: {
HelperWidgets.Controller.setCount("EffectComposer", repeater.count)
}
delegate: EffectCompositionNode {
width: parent.width
modelIndex: index
property bool wasExpanded: false
Behavior on y {
id: dragAnimation
PropertyAnimation {
duration: 300
easing.type: Easing.InOutQuad
}
}
onStartDrag: (section) => {
root.draggedSec = section
root.moveFromIdx = index
// We only need to animate non-dragged sections
dragAnimation.enabled = false
wasExpanded = expanded
expanded = false
highlightBorder = true
root.secsY = []
}
onStopDrag: {
if (root.secsY.length !== 0) {
if (root.moveFromIdx === root.moveToIdx)
root.draggedSec.y = root.secsY[root.moveFromIdx]
else
root.backendModel.moveNode(root.moveFromIdx, root.moveToIdx)
}
highlightBorder = false
root.draggedSec = null
expanded = wasExpanded
dragAnimation.enabled = true
}
onOpenShadersCodeEditor: (idx) => root.backendModel.openShadersCodeEditor(idx)
}
} // Repeater
} // Column
} // ScrollView
Text {
text: root.backendModel ? root.backendModel.isEnabled
? qsTr("Add an effect node to start")
: qsTr("Effect Composer is disabled on MCU projects")
: ""
color: StudioTheme.Values.themeTextColor
font.pixelSize: StudioTheme.Values.baseFontSize
anchors.centerIn: parent
visible: root.backendModel ? root.backendModel.isEmpty : false
}
} // Item
} // Column
} // SplitView
} // ColumnLayout
DropArea {
id: dropArea
anchors.fill: parent
onEntered: (drag) => {
let accepted = false
if (drag.formats[0] === "application/vnd.qtdesignstudio.assets" && drag.hasUrls) {
accepted = EffectComposerBackend.rootView.isEffectAsset(drag.urls[0])
} else if (drag.formats[0] === "application/vnd.qtdesignstudio.modelnode.list") {
accepted = EffectComposerBackend.rootView.isEffectNode(
drag.getDataAsArrayBuffer("application/vnd.qtdesignstudio.modelnode.list"))
}
drag.accepted = accepted
}
onDropped: (drop) => {
if (drop.formats[0] === "application/vnd.qtdesignstudio.assets" && drop.hasUrls) {
EffectComposerBackend.rootView.dropAsset(drop.urls[0])
} else if (drop.formats[0] === "application/vnd.qtdesignstudio.modelnode.list") {
EffectComposerBackend.rootView.dropNode(
drop.getDataAsArrayBuffer("application/vnd.qtdesignstudio.modelnode.list"))
}
drop.accept()
}
}
}