mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-08-04 09:18:27 +02:00
Merge pull request #2504 from alicevision/dev/forLoop
First version of For Loop implementation
This commit is contained in:
commit
91ebc1619c
11 changed files with 390 additions and 124 deletions
92
meshroom/ui/qml/Controls/IntSelector.qml
Normal file
92
meshroom/ui/qml/Controls/IntSelector.qml
Normal 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"
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue