Merge pull request #2504 from alicevision/dev/forLoop

First version of For Loop implementation
This commit is contained in:
Fabien Castan 2024-09-04 23:36:33 +02:00 committed by GitHub
commit 91ebc1619c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 390 additions and 124 deletions

View file

@ -0,0 +1,92 @@
import QtQuick 2.15
import MaterialIcons 2.2
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.11
/*
* IntSelector with arrows and a text input to select a number
*/
Row {
id: root
property string tooltipText: ""
property int value: 0
property var range: { "min" : 0, "max" : 0 }
Layout.alignment: Qt.AlignVCenter
spacing: 0
property bool displayButtons: previousIntButton.hovered || intInputMouseArea.containsMouse || nextIntButton.hovered
property real buttonsOpacity: displayButtons ? 1.0 : 0.0
MaterialToolButton {
id: previousIntButton
opacity: buttonsOpacity
width: 10
text: MaterialIcons.navigate_before
ToolTip.text: "Previous"
onClicked: {
if (value > range.min) {
value -= 1
}
}
}
TextInput {
id: intInput
ToolTip.text: tooltipText
ToolTip.visible: tooltipText && intInputMouseArea.containsMouse
width: intMetrics.width
height: previousIntButton.height
color: palette.text
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
selectByMouse: true
text: value
onEditingFinished: {
// We first assign the frame to the entered text even if it is an invalid frame number. We do it for extreme cases, for example without doing it, if we are at 0, and put a negative number, value would be still 0 and nothing happens but we will still see the wrong number
value = parseInt(text)
value = Math.min(range.max, Math.max(range.min, parseInt(text)))
focus = false
}
MouseArea {
id: intInputMouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
}
}
MaterialToolButton {
id: nextIntButton
width: 10
opacity: buttonsOpacity
text: MaterialIcons.navigate_next
ToolTip.text: "Next"
onClicked: {
if (value < range.max) {
value += 1
}
}
}
TextMetrics {
id: intMetrics
font: intInput.font
text: "10000"
}
}

View file

@ -12,3 +12,4 @@ TabPanel 1.0 TabPanel.qml
TextFileViewer 1.0 TextFileViewer.qml
ExifOrientedViewer 1.0 ExifOrientedViewer.qml
FilterComboBox 1.0 FilterComboBox.qml
IntSelector 1.0 IntSelector.qml

View file

@ -100,7 +100,6 @@ RowLayout {
|| drag.source.objectName != inputDragTarget.objectName // not an edge connector
|| drag.source.baseType !== inputDragTarget.baseType // not the same base type
|| drag.source.nodeItem === inputDragTarget.nodeItem // connection between attributes of the same node
|| (drag.source.isList && !inputDragTarget.isList) // connection between a list and a simple attribute
|| (drag.source.isList && childrenRepeater.count) // source/target are lists but target already has children
|| drag.source.connectorType === "input" // refuse to connect an "input pin" on another one (input attr can be connected to input attr, but not the graphical pin)
) {

View file

@ -1,11 +1,13 @@
import QtQuick 2.15
import GraphEditor 1.0
import QtQuick.Shapes 1.15
import MaterialIcons 2.2
import QtQuick.Controls 2.15
/**
A cubic spline representing an edge, going from point1 to point2, providing mouse interaction.
*/
Shape {
Item {
id: root
property var edge
@ -15,9 +17,12 @@ Shape {
property real point2y
property alias thickness: path.strokeWidth
property alias color: path.strokeColor
property bool isForLoop: false
property int loopSize: 0
property int iteration: 0
// BUG: edgeArea is destroyed before path, need to test if not null to avoid warnings
readonly property bool containsMouse: edgeArea && edgeArea.containsMouse
readonly property bool containsMouse: (loopArea && loopArea.containsMouse) || (edgeArea && edgeArea.containsMouse)
signal pressed(var event)
signal released(var event)
@ -32,32 +37,96 @@ Shape {
property real endX: width
property real endY: height
// cause rendering artifacts when enabled (and don't support hot reload really well)
vendorExtensionsEnabled: false
ShapePath {
id: path
startX: root.startX
startY: root.startY
fillColor: "transparent"
strokeColor: "#3E3E3E"
strokeStyle: edge !== undefined && ((edge.src !== undefined && edge.src.isOutput) || edge.dst === undefined) ? ShapePath.SolidLine : ShapePath.DashLine
strokeWidth: 1
// final visual width of this path (never below 1)
readonly property real visualWidth: Math.max(strokeWidth, 1)
dashPattern: [6 / visualWidth, 4 / visualWidth]
capStyle: ShapePath.RoundCap
Shape {
anchors.fill: parent
// cause rendering artifacts when enabled (and don't support hot reload really well)
vendorExtensionsEnabled: false
opacity: 0.7
ShapePath {
id: path
startX: root.startX
startY: root.startY
fillColor: "transparent"
strokeColor: "#3E3E3E"
strokeStyle: edge !== undefined && ((edge.src !== undefined && edge.src.isOutput) || edge.dst === undefined) ? ShapePath.SolidLine : ShapePath.DashLine
strokeWidth: 1
// final visual width of this path (never below 1)
readonly property real visualWidth: Math.max(strokeWidth, 1)
dashPattern: [6 / visualWidth, 4 / visualWidth]
capStyle: ShapePath.RoundCap
PathCubic {
id: cubic
property real ctrlPtDist: 30
x: root.isForLoop ? (root.startX + root.endX) / 2 - loopArea.width / 2 : root.endX
y: root.isForLoop ? (root.startY + root.endY) / 2 : root.endY
relativeControl1X: ctrlPtDist
relativeControl1Y: 0
control2X: x - ctrlPtDist
control2Y: y
}
PathCubic {
id: cubic
property real ctrlPtDist: 30
x: root.endX
y: root.endY
relativeControl1X: ctrlPtDist
relativeControl1Y: 0
control2X: x - ctrlPtDist
control2Y: y
}
ShapePath {
id: pathSecondary
startX: (root.startX + root.endX) / 2 + loopArea.width / 2
startY: (root.startY + root.endY) / 2
fillColor: "transparent"
strokeColor: root.isForLoop ? root.color : "transparent"
strokeStyle: edge !== undefined && ((edge.src !== undefined && edge.src.isOutput) || edge.dst === undefined) ? ShapePath.SolidLine : ShapePath.DashLine
strokeWidth: root.thickness
// final visual width of this path (never below 1)
readonly property real visualWidth: Math.max(strokeWidth, 1)
dashPattern: [6 / visualWidth, 4 / visualWidth]
capStyle: ShapePath.RoundCap
PathCubic {
id: cubicSecondary
property real ctrlPtDist: 30
x: root.endX
y: root.endY
relativeControl1X: ctrlPtDist
relativeControl1Y: 0
control2X: x - ctrlPtDist
control2Y: y
}
}
}
Item {
// place the label at the middle of the edge
x: (root.startX + root.endX) / 2
y: (root.startY + root.endY) / 2
visible: root.isForLoop
Rectangle {
anchors.centerIn: parent
property int margin: 2
width: icon.width + 2 * margin
height: icon.height + 2 * margin
radius: width
color: path.strokeColor
MaterialToolLabel {
id: icon
anchors.centerIn: parent
iconText: MaterialIcons.loop
label: (root.iteration + 1) + "/" + root.loopSize + " "
color: palette.base
ToolTip.text: "Foreach Loop"
}
MouseArea {
id: loopArea
anchors.fill: parent
hoverEnabled: true
onClicked: root.pressed(arguments[0])
}
}
}
EdgeMouseArea {

View file

@ -379,13 +379,98 @@ Item {
width: 1000
height: 1000
Menu {
Popup {
id: edgeMenu
property var currentEdge: null
MenuItem {
enabled: edgeMenu.currentEdge && !edgeMenu.currentEdge.dst.node.locked && !edgeMenu.currentEdge.dst.isReadOnly
text: "Remove"
onTriggered: uigraph.removeEdge(edgeMenu.currentEdge)
property bool forLoop: false
onOpened: {
expandButton.canExpand = uigraph.canExpandForLoop(edgeMenu.currentEdge)
}
contentItem: Row {
IntSelector {
id: loopIterationSelector
tooltipText: "Iterations"
visible: edgeMenu.currentEdge && edgeMenu.forLoop
enabled: expandButton.canExpand
property var listAttr: edgeMenu.currentEdge ? edgeMenu.currentEdge.src.root : null
Connections {
target: edgeMenu
function onCurrentEdgeChanged() {
if (edgeMenu.currentEdge) {
loopIterationSelector.listAttr = edgeMenu.currentEdge.src.root
loopIterationSelector.value = loopIterationSelector.listAttr ? loopIterationSelector.listAttr.value.indexOf(edgeMenu.currentEdge.src) + 1 : 0
}
}
}
// We add 1 to the index because of human readable index (starting at 1)
value: listAttr ? listAttr.value.indexOf(edgeMenu.currentEdge.src) + 1 : 0
range: { "min": 1, "max": listAttr ? listAttr.value.count : 0 }
onValueChanged: {
if (listAttr === null) {
return
}
const newSrcAttr = listAttr.value.at(value - 1)
const dst = edgeMenu.currentEdge.dst
// if the edge exists do not replace it
if (newSrcAttr === edgeMenu.currentEdge.src && dst === edgeMenu.currentEdge.dst) {
return
}
edgeMenu.currentEdge = uigraph.replaceEdge(edgeMenu.currentEdge, newSrcAttr, dst)
}
}
MaterialToolButton {
font.pointSize: 13
ToolTip.text: "Remove Edge"
enabled: edgeMenu.currentEdge && !edgeMenu.currentEdge.dst.node.locked && !edgeMenu.currentEdge.dst.isReadOnly
text: MaterialIcons.delete_
onClicked: {
uigraph.removeEdge(edgeMenu.currentEdge)
edgeMenu.close()
}
}
MaterialToolButton {
id: expandButton
property bool canExpand: edgeMenu.currentEdge && edgeMenu.forLoop
visible: edgeMenu.currentEdge && edgeMenu.forLoop && canExpand
enabled: edgeMenu.currentEdge && !edgeMenu.currentEdge.dst.node.locked && !edgeMenu.currentEdge.dst.isReadOnly
font.pointSize: 13
ToolTip.text: "Expand"
text: MaterialIcons.open_in_full
onClicked: {
edgeMenu.currentEdge = uigraph.expandForLoop(edgeMenu.currentEdge)
canExpand = false
edgeMenu.close()
}
}
MaterialToolButton {
id: collapseButton
visible: edgeMenu.currentEdge && edgeMenu.forLoop && !expandButton.canExpand
enabled: edgeMenu.currentEdge && !edgeMenu.currentEdge.dst.node.locked && !edgeMenu.currentEdge.dst.isReadOnly
font.pointSize: 13
ToolTip.text: "Collapse"
text: MaterialIcons.close_fullscreen
onClicked: {
uigraph.collapseForLoop(edgeMenu.currentEdge)
expandButton.canExpand = true
edgeMenu.close()
}
}
}
}
@ -402,12 +487,22 @@ Item {
property bool isValidEdge: src !== undefined && dst !== undefined
visible: isValidEdge && src.visible && dst.visible
property bool forLoop: src.attribute.type === "ListAttribute" && dst.attribute.type != "ListAttribute"
property bool inFocus: containsMouse || (edgeMenu.opened && edgeMenu.currentEdge === edge)
edge: object
isForLoop: forLoop
loopSize: forLoop ? edge.src.root.value.count : 0
iteration: forLoop ? edge.src.root.value.indexOf(edge.src) : 0
color: edge.dst === root.edgeAboutToBeRemoved ? "red" : inFocus ? activePalette.highlight : activePalette.text
thickness: inFocus ? 2 : 1
opacity: 0.7
thickness: {
if (forLoop) {
return (inFocus) ? 4 : 3
}
return (inFocus) ? 2 : 1
}
point1x: isValidEdge ? src.globalX + src.outputAnchorPos.x : 0
point1y: isValidEdge ? src.globalY + src.outputAnchorPos.y : 0
point2x: isValidEdge ? dst.globalX + dst.inputAnchorPos.x : 0
@ -415,12 +510,16 @@ Item {
onPressed: {
const canEdit = !edge.dst.node.locked
if (event.button === Qt.RightButton) {
if (event.button) {
if (canEdit && (event.modifiers & Qt.AltModifier)) {
uigraph.removeEdge(edge)
} else {
edgeMenu.currentEdge = edge
edgeMenu.popup()
edgeMenu.forLoop = forLoop
var spawnPosition = mouseArea.mapToItem(draggable, mouseArea.mouseX, mouseArea.mouseY)
edgeMenu.x = spawnPosition.x
edgeMenu.y = spawnPosition.y
edgeMenu.open()
}
}
}

View file

@ -423,8 +423,10 @@ Item {
onPressed: root.pressed(mouse)
onEdgeAboutToBeRemoved: root.edgeAboutToBeRemoved(input)
Component.onCompleted: attributePinCreated(object, outPin)
Component.onCompleted: attributePinCreated(attribute, outPin)
onChildPinCreated: attributePinCreated(childAttribute, outPin)
Component.onDestruction: attributePinDeleted(attribute, outPin)
onChildPinDeleted: attributePinDeleted(childAttribute, outPin)
}
}
}

View file

@ -264,7 +264,7 @@ Item {
spacing: 3
delegate: Label {
width: (ListView.view.width / ListView.view.model.count) - 3
width: ListView.view.model ? (ListView.view.width / ListView.view.model.count) - 3 : 0
height: ListView.view.height
anchors.verticalCenter: parent.verticalCenter
background: Rectangle {

View file

@ -12,6 +12,7 @@ Item {
property alias iconText: iconItem.text
property alias iconSize: iconItem.font.pointSize
property alias label: labelItem.text
property var color: palette.text
implicitWidth: childrenRect.width
implicitHeight: childrenRect.height
anchors.rightMargin: 5
@ -23,12 +24,12 @@ Item {
font.pointSize: 13
padding: 0
text: ""
color: palette.text
color: color
}
Label {
id: labelItem
text: ""
color: palette.text
color: color
}
}

View file

@ -144,81 +144,16 @@ FloatingPane {
anchors.fill: parent
Item {
Layout.preferredWidth: previousFrameButton.width + frameMetrics.width + nextFrameButton.width
Layout.preferredHeight: frameInput.height
IntSelector {
id: frameInput
MouseArea {
id: mouseAreaFrameLabel
tooltipText: "Frame"
anchors.fill: parent
value: m.frame
range: frameRange
hoverEnabled: true
onEntered: {
previousFrameButton.opacity = 1
nextFrameButton.opacity = 1
}
onExited: {
previousFrameButton.opacity = 0
nextFrameButton.opacity = 0
}
MaterialToolButton {
id: previousFrameButton
anchors.verticalCenter: mouseAreaFrameLabel.verticalCenter
opacity: 0
width: 10
text: MaterialIcons.navigate_before
ToolTip.text: "Previous Frame"
onClicked: {
if (m.frame > frameRange.min) {
m.frame -= 1
}
}
}
TextInput {
id: frameInput
anchors.horizontalCenter: mouseAreaFrameLabel.horizontalCenter
Layout.preferredWidth: frameMetrics.width
color: palette.text
horizontalAlignment: Text.AlignHCenter
selectByMouse: true
text: m.frame
onEditingFinished: {
// We first assign the frame to the entered text even if it is an invalid frame number. We do it for extreme cases, for example without doing it, if we are at 0, and put a negative number, m.frame would be still 0 and nothing happens but we will still see the wrong number
m.frame = parseInt(text)
m.frame = Math.min(frameRange.max, Math.max(frameRange.min, parseInt(text)))
focus = false
}
}
MaterialToolButton {
id: nextFrameButton
anchors.right: mouseAreaFrameLabel.right
anchors.verticalCenter: mouseAreaFrameLabel.verticalCenter
width: 10
opacity: 0
text: MaterialIcons.navigate_next
ToolTip.text: "Next Frame"
onClicked: {
if (m.frame < frameRange.max) {
m.frame += 1
}
}
}
onValueChanged: {
m.frame = value
}
}
@ -504,13 +439,6 @@ FloatingPane {
}
}
TextMetrics {
id: frameMetrics
font: frameInput.font
text: "10000"
}
TextMetrics {
id: fpsMetrics