mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-04-29 02:08:08 +02:00
[ui/core] First version of For Loop implementation
If you connect a list to an attribute, you can iterate over the list as a for loop
This commit is contained in:
parent
52cb124589
commit
019e137386
9 changed files with 363 additions and 120 deletions
|
@ -36,7 +36,7 @@ def attributeFactory(description, value, isOutput, node, root=None, parent=None)
|
|||
class Attribute(BaseObject):
|
||||
"""
|
||||
"""
|
||||
stringIsLinkRe = re.compile(r'^\{[A-Za-z]+[A-Za-z0-9_.]*\}$')
|
||||
stringIsLinkRe = re.compile(r'^\{[A-Za-z]+[A-Za-z0-9_.\[\]]*\}$')
|
||||
|
||||
def __init__(self, node, attributeDesc, isOutput, root=None, parent=None):
|
||||
"""
|
||||
|
@ -324,6 +324,9 @@ class Attribute(BaseObject):
|
|||
# safety check to avoid evaluation errors
|
||||
if not self.node.graph or not self.node.graph.edges:
|
||||
return False
|
||||
# if the attribute is a ListAttribute, we need to check if any of its elements has output connections
|
||||
if isinstance(self, ListAttribute):
|
||||
return next((edge for edge in self.node.graph.edges.values() if edge.src == self), None) is not None or any(attr.hasOutputConnections for attr in self._value if hasattr(attr, 'hasOutputConnections'))
|
||||
return next((edge for edge in self.node.graph.edges.values() if edge.src == self), None) is not None
|
||||
|
||||
def _applyExpr(self):
|
||||
|
@ -447,6 +450,7 @@ class Attribute(BaseObject):
|
|||
uidIgnoreValue = Property(Variant, getUidIgnoreValue, constant=True)
|
||||
validValueChanged = Signal()
|
||||
validValue = Property(bool, getValidValue, setValidValue, notify=validValueChanged)
|
||||
root = Property(BaseObject, root.fget, constant=True)
|
||||
|
||||
|
||||
def raiseIfLink(func):
|
||||
|
|
|
@ -687,6 +687,8 @@ class UIGraph(QObject):
|
|||
Args:
|
||||
startNode (Node): the node to start from.
|
||||
"""
|
||||
if isinstance(nodes, Node):
|
||||
nodes = [nodes]
|
||||
with self.groupedGraphModification("Remove Nodes From Selected Nodes"):
|
||||
nodesToRemove, _ = self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True)
|
||||
# filter out nodes that will be removed more than once
|
||||
|
@ -706,7 +708,7 @@ class UIGraph(QObject):
|
|||
list[Node]: the list of duplicated nodes
|
||||
"""
|
||||
nodes = self.filterNodes(nodes)
|
||||
nPositions = []
|
||||
nPositions = [(n.x, n.y) for n in self._graph.nodes]
|
||||
# enable updates between duplication and layout to get correct depths during layout
|
||||
with self.groupedGraphModification("Duplicate Selected Nodes", disableUpdates=False):
|
||||
# disable graph updates during duplication
|
||||
|
@ -716,9 +718,8 @@ class UIGraph(QObject):
|
|||
bbox = self._layout.boundingBox(nodes)
|
||||
|
||||
for n in duplicates:
|
||||
idx = duplicates.index(n)
|
||||
yPos = n.y + self.layout.gridSpacing + bbox[3]
|
||||
if idx > 0 and (n.x, yPos) in nPositions:
|
||||
if (n.x, yPos) in nPositions:
|
||||
# make sure the node will not be moved on top of another node
|
||||
while (n.x, yPos) in nPositions:
|
||||
yPos = yPos + self.layout.gridSpacing + self.layout.nodeHeight
|
||||
|
@ -739,12 +740,59 @@ class UIGraph(QObject):
|
|||
Returns:
|
||||
list[Node]: the list of duplicated nodes
|
||||
"""
|
||||
if isinstance(nodes, Node):
|
||||
nodes = [nodes]
|
||||
with self.groupedGraphModification("Duplicate Nodes From Selected Nodes"):
|
||||
nodesToDuplicate, _ = self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True)
|
||||
# filter out nodes that will be duplicated more than once
|
||||
uniqueNodesToDuplicate = list(dict.fromkeys(nodesToDuplicate))
|
||||
duplicates = self.duplicateNodes(uniqueNodesToDuplicate)
|
||||
return duplicates
|
||||
|
||||
@Slot(Edge, result=bool)
|
||||
def canExpandForLoop(self, currentEdge):
|
||||
""" Check if the list attribute can be expanded by looking at all the edges connected to it. """
|
||||
listAttribute = currentEdge.src.root
|
||||
srcIndex = listAttribute.index(currentEdge.src)
|
||||
allSrc = [e.src for e in self._graph.edges.values()]
|
||||
for i in range(len(listAttribute)):
|
||||
if i == srcIndex:
|
||||
continue
|
||||
if listAttribute.at(i) in allSrc:
|
||||
return False
|
||||
return True
|
||||
|
||||
@Slot(Edge)
|
||||
def expandForLoop(self, currentEdge):
|
||||
""" Expand 'node' by creating all its output nodes. """
|
||||
with self.groupedGraphModification("Expand For Loop Node"):
|
||||
listAttribute = currentEdge.src.root
|
||||
srcIndex = listAttribute.index(currentEdge.src)
|
||||
dst = currentEdge.dst
|
||||
for i in range(len(listAttribute)):
|
||||
if i == srcIndex:
|
||||
continue
|
||||
self.duplicateNodesFrom(dst.node)
|
||||
newNode = self.graph.nodes.at(-1)
|
||||
previousEdge = self.graph.edge(newNode.attribute(dst.name))
|
||||
self.replaceEdge(previousEdge, listAttribute.at(i), previousEdge.dst)
|
||||
|
||||
@Slot(Edge)
|
||||
def collapseForLoop(self, currentEdge):
|
||||
""" Collapse 'node' by removing all its output nodes. """
|
||||
with self.groupedGraphModification("Collapse For Loop Node"):
|
||||
listAttribute = currentEdge.src.root
|
||||
srcIndex = listAttribute.index(currentEdge.src)
|
||||
allSrc = [e.src for e in self._graph.edges.values()]
|
||||
for i in range(len(listAttribute)):
|
||||
if i == srcIndex:
|
||||
continue
|
||||
occurence = allSrc.index(listAttribute.at(i)) if listAttribute.at(i) in allSrc else -1
|
||||
if occurence != -1:
|
||||
self.removeNodesFrom(self.graph.edges.at(occurence).dst.node)
|
||||
# remove the edge from allSrc
|
||||
allSrc.pop(occurence)
|
||||
|
||||
|
||||
@Slot(QObject)
|
||||
def clearData(self, nodes):
|
||||
|
@ -765,7 +813,9 @@ class UIGraph(QObject):
|
|||
|
||||
@Slot(Attribute, Attribute)
|
||||
def addEdge(self, src, dst):
|
||||
if isinstance(dst, ListAttribute) and not isinstance(src, ListAttribute):
|
||||
if isinstance(src, ListAttribute) and not isinstance(dst, ListAttribute):
|
||||
self._addEdge(src.at(0), dst)
|
||||
elif isinstance(dst, ListAttribute) and not isinstance(src, ListAttribute):
|
||||
with self.groupedGraphModification("Insert and Add Edge on {}".format(dst.getFullNameToNode())):
|
||||
self.appendAttribute(dst)
|
||||
self._addEdge(src, dst.at(-1))
|
||||
|
@ -787,6 +837,17 @@ class UIGraph(QObject):
|
|||
else:
|
||||
self.push(commands.RemoveEdgeCommand(self._graph, edge))
|
||||
|
||||
@Slot(Edge, Attribute, Attribute, result=Edge)
|
||||
def replaceEdge(self, edge, newSrc, newDst):
|
||||
with self.groupedGraphModification("Replace Edge '{}'->'{}' with '{}'->'{}'".format(edge.src.getFullNameToNode(), edge.dst.getFullNameToNode(), newSrc.getFullNameToNode(), newDst.getFullNameToNode())):
|
||||
self.removeEdge(edge)
|
||||
self.addEdge(newSrc, newDst)
|
||||
return self._graph.edge(newDst)
|
||||
|
||||
@Slot(Attribute, result=Edge)
|
||||
def getEdge(self, dst):
|
||||
return self._graph.edge(dst)
|
||||
|
||||
@Slot(Attribute, "QVariant")
|
||||
def setAttribute(self, attribute, value):
|
||||
self.push(commands.SetAttributeCommand(self._graph, attribute, value))
|
||||
|
@ -794,6 +855,12 @@ class UIGraph(QObject):
|
|||
@Slot(Attribute)
|
||||
def resetAttribute(self, attribute):
|
||||
""" Reset 'attribute' to its default value """
|
||||
# if the attribute is a ListAttribute, remove all edges
|
||||
if isinstance(attribute, ListAttribute):
|
||||
for edge in self._graph.edges:
|
||||
# if the edge is connected to one of the ListAttribute's elements, remove it
|
||||
if edge.src in attribute.value:
|
||||
self.removeEdge(edge)
|
||||
self.push(commands.SetAttributeCommand(self._graph, attribute, attribute.defaultValue()))
|
||||
|
||||
@Slot(CompatibilityNode, result=Node)
|
||||
|
|
101
meshroom/ui/qml/Controls/IntSelector.qml
Normal file
101
meshroom/ui/qml/Controls/IntSelector.qml
Normal file
|
@ -0,0 +1,101 @@
|
|||
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
|
||||
*/
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property int value: 0
|
||||
property var range: { "min" : 0, "max" : 0 }
|
||||
|
||||
Layout.preferredWidth: previousIntButton.width + intMetrics.width + nextIntButton.width
|
||||
Layout.preferredHeight: intInput.height
|
||||
|
||||
MouseArea {
|
||||
id: mouseAreaIntLabel
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
hoverEnabled: true
|
||||
|
||||
onEntered: {
|
||||
previousIntButton.opacity = 1
|
||||
nextIntButton.opacity = 1
|
||||
}
|
||||
|
||||
onExited: {
|
||||
previousIntButton.opacity = 0
|
||||
nextIntButton.opacity = 0
|
||||
}
|
||||
|
||||
MaterialToolButton {
|
||||
id: previousIntButton
|
||||
|
||||
anchors.verticalCenter: mouseAreaIntLabel.verticalCenter
|
||||
|
||||
opacity: 0
|
||||
width: 10
|
||||
text: MaterialIcons.navigate_before
|
||||
ToolTip.text: "Previous Integer"
|
||||
|
||||
onClicked: {
|
||||
if (value > range.min) {
|
||||
value -= 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextInput {
|
||||
id: intInput
|
||||
|
||||
anchors.horizontalCenter: mouseAreaIntLabel.horizontalCenter
|
||||
anchors.verticalCenter: mouseAreaIntLabel.verticalCenter
|
||||
Layout.preferredWidth: intMetrics.width
|
||||
|
||||
color: palette.text
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
MaterialToolButton {
|
||||
id: nextIntButton
|
||||
|
||||
anchors.right: mouseAreaIntLabel.right
|
||||
anchors.verticalCenter: mouseAreaIntLabel.verticalCenter
|
||||
|
||||
width: 10
|
||||
opacity: 0
|
||||
text: MaterialIcons.navigate_next
|
||||
ToolTip.text: "Next Integer"
|
||||
|
||||
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,6 +17,8 @@ Shape {
|
|||
property real point2y
|
||||
property alias thickness: path.strokeWidth
|
||||
property alias color: path.strokeColor
|
||||
property bool isForLoop: false
|
||||
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
|
||||
|
@ -32,31 +36,71 @@ 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
|
||||
|
||||
PathCubic {
|
||||
id: cubic
|
||||
property real ctrlPtDist: 30
|
||||
x: root.endX
|
||||
y: root.endY
|
||||
relativeControl1X: ctrlPtDist
|
||||
relativeControl1Y: 0
|
||||
control2X: x - ctrlPtDist
|
||||
control2Y: y
|
||||
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.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: 1
|
||||
width: childrenRect.width + 2 * margin
|
||||
height: childrenRect.height + 2 * margin
|
||||
radius: width
|
||||
color: path.strokeColor
|
||||
MaterialLabel {
|
||||
id: icon
|
||||
x: parent.margin
|
||||
y: parent.margin
|
||||
text: MaterialIcons.loop
|
||||
color: palette.base
|
||||
font.pointSize: 24
|
||||
|
||||
ToolTip.text: "This is a for loop"
|
||||
}
|
||||
|
||||
Label {
|
||||
x: icon.width / 2.4
|
||||
y: icon.height / 3
|
||||
font.pixelSize: 10
|
||||
text: root.iteration
|
||||
color: palette.base
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -379,13 +379,104 @@ 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: GridLayout {
|
||||
layoutDirection: Qt.LeftToRight
|
||||
columns: 2
|
||||
columnSpacing: 20
|
||||
|
||||
Column {
|
||||
id: listAttrColumn
|
||||
visible: edgeMenu.currentEdge && edgeMenu.forLoop && expandButton.canExpand
|
||||
property var listAttr: edgeMenu.currentEdge ? edgeMenu.currentEdge.src.root : null
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Text {
|
||||
id: listAttrMenuText
|
||||
text: "<b>Iteration:</b>"
|
||||
|
||||
color: activePalette.text
|
||||
bottomPadding: 15
|
||||
}
|
||||
|
||||
IntSelector {
|
||||
width: listAttrColumn.width
|
||||
anchors.top: listAttrMenuText.bottom
|
||||
anchors.horizontalCenter: listAttrColumn.horizontalCenter
|
||||
visible: edgeMenu.currentEdge && edgeMenu.forLoop
|
||||
value: listAttrColumn.listAttr ? listAttrColumn.listAttr.value.indexOf(edgeMenu.currentEdge.src) : 0
|
||||
range: { "min": 0, "max": listAttrColumn.listAttr ? listAttrColumn.listAttr.value.count - 1 : 0 }
|
||||
|
||||
onValueChanged: {
|
||||
if (listAttrColumn.listAttr === null) {
|
||||
return
|
||||
}
|
||||
const newSrcAttr = listAttrColumn.listAttr.value.at(value)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Column {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Text {
|
||||
text: "<b>Actions:</b>"
|
||||
|
||||
color: activePalette.text
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
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)
|
||||
}
|
||||
|
||||
MaterialToolButton {
|
||||
id: expandButton
|
||||
|
||||
property bool canExpand: edgeMenu.currentEdge && edgeMenu.forLoop
|
||||
|
||||
visible: edgeMenu.currentEdge && edgeMenu.forLoop && canExpand
|
||||
font.pointSize: 13
|
||||
ToolTip.text: "Expand"
|
||||
text: MaterialIcons.open_in_full
|
||||
|
||||
onClicked: {
|
||||
uigraph.expandForLoop(edgeMenu.currentEdge)
|
||||
canExpand = false
|
||||
}
|
||||
}
|
||||
|
||||
MaterialToolButton {
|
||||
id: collapseButton
|
||||
|
||||
visible: edgeMenu.currentEdge && edgeMenu.forLoop && !expandButton.canExpand
|
||||
font.pointSize: 13
|
||||
ToolTip.text: "Collapse"
|
||||
text: MaterialIcons.close_fullscreen
|
||||
|
||||
onClicked: {
|
||||
uigraph.collapseForLoop(edgeMenu.currentEdge)
|
||||
expandButton.canExpand = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -402,12 +493,16 @@ 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
|
||||
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: (forLoop && inFocus) ? 3 : (forLoop || 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -144,81 +144,14 @@ FloatingPane {
|
|||
|
||||
anchors.fill: parent
|
||||
|
||||
Item {
|
||||
Layout.preferredWidth: previousFrameButton.width + frameMetrics.width + nextFrameButton.width
|
||||
Layout.preferredHeight: frameInput.height
|
||||
IntSelector {
|
||||
id: frameInput
|
||||
|
||||
MouseArea {
|
||||
id: mouseAreaFrameLabel
|
||||
value: m.frame
|
||||
range: frameRange
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
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 +437,6 @@ FloatingPane {
|
|||
}
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: frameMetrics
|
||||
|
||||
font: frameInput.font
|
||||
text: "10000"
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: fpsMetrics
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue