[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:
Aurore LAFAURIE 2024-08-16 19:29:24 +02:00
parent 52cb124589
commit 019e137386
9 changed files with 363 additions and 120 deletions

View file

@ -36,7 +36,7 @@ def attributeFactory(description, value, isOutput, node, root=None, parent=None)
class Attribute(BaseObject): 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): def __init__(self, node, attributeDesc, isOutput, root=None, parent=None):
""" """
@ -324,6 +324,9 @@ class Attribute(BaseObject):
# safety check to avoid evaluation errors # safety check to avoid evaluation errors
if not self.node.graph or not self.node.graph.edges: if not self.node.graph or not self.node.graph.edges:
return False 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 return next((edge for edge in self.node.graph.edges.values() if edge.src == self), None) is not None
def _applyExpr(self): def _applyExpr(self):
@ -447,6 +450,7 @@ class Attribute(BaseObject):
uidIgnoreValue = Property(Variant, getUidIgnoreValue, constant=True) uidIgnoreValue = Property(Variant, getUidIgnoreValue, constant=True)
validValueChanged = Signal() validValueChanged = Signal()
validValue = Property(bool, getValidValue, setValidValue, notify=validValueChanged) validValue = Property(bool, getValidValue, setValidValue, notify=validValueChanged)
root = Property(BaseObject, root.fget, constant=True)
def raiseIfLink(func): def raiseIfLink(func):

View file

@ -687,6 +687,8 @@ class UIGraph(QObject):
Args: Args:
startNode (Node): the node to start from. startNode (Node): the node to start from.
""" """
if isinstance(nodes, Node):
nodes = [nodes]
with self.groupedGraphModification("Remove Nodes From Selected Nodes"): with self.groupedGraphModification("Remove Nodes From Selected Nodes"):
nodesToRemove, _ = self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True) nodesToRemove, _ = self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True)
# filter out nodes that will be removed more than once # filter out nodes that will be removed more than once
@ -706,7 +708,7 @@ class UIGraph(QObject):
list[Node]: the list of duplicated nodes list[Node]: the list of duplicated nodes
""" """
nodes = self.filterNodes(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 # enable updates between duplication and layout to get correct depths during layout
with self.groupedGraphModification("Duplicate Selected Nodes", disableUpdates=False): with self.groupedGraphModification("Duplicate Selected Nodes", disableUpdates=False):
# disable graph updates during duplication # disable graph updates during duplication
@ -716,9 +718,8 @@ class UIGraph(QObject):
bbox = self._layout.boundingBox(nodes) bbox = self._layout.boundingBox(nodes)
for n in duplicates: for n in duplicates:
idx = duplicates.index(n)
yPos = n.y + self.layout.gridSpacing + bbox[3] 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 # make sure the node will not be moved on top of another node
while (n.x, yPos) in nPositions: while (n.x, yPos) in nPositions:
yPos = yPos + self.layout.gridSpacing + self.layout.nodeHeight yPos = yPos + self.layout.gridSpacing + self.layout.nodeHeight
@ -739,12 +740,59 @@ class UIGraph(QObject):
Returns: Returns:
list[Node]: the list of duplicated nodes list[Node]: the list of duplicated nodes
""" """
if isinstance(nodes, Node):
nodes = [nodes]
with self.groupedGraphModification("Duplicate Nodes From Selected Nodes"): with self.groupedGraphModification("Duplicate Nodes From Selected Nodes"):
nodesToDuplicate, _ = self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True) nodesToDuplicate, _ = self._graph.dfsOnDiscover(startNodes=nodes, reverse=True, dependenciesOnly=True)
# filter out nodes that will be duplicated more than once # filter out nodes that will be duplicated more than once
uniqueNodesToDuplicate = list(dict.fromkeys(nodesToDuplicate)) uniqueNodesToDuplicate = list(dict.fromkeys(nodesToDuplicate))
duplicates = self.duplicateNodes(uniqueNodesToDuplicate) duplicates = self.duplicateNodes(uniqueNodesToDuplicate)
return duplicates 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) @Slot(QObject)
def clearData(self, nodes): def clearData(self, nodes):
@ -765,7 +813,9 @@ class UIGraph(QObject):
@Slot(Attribute, Attribute) @Slot(Attribute, Attribute)
def addEdge(self, src, dst): 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())): with self.groupedGraphModification("Insert and Add Edge on {}".format(dst.getFullNameToNode())):
self.appendAttribute(dst) self.appendAttribute(dst)
self._addEdge(src, dst.at(-1)) self._addEdge(src, dst.at(-1))
@ -787,6 +837,17 @@ class UIGraph(QObject):
else: else:
self.push(commands.RemoveEdgeCommand(self._graph, edge)) 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") @Slot(Attribute, "QVariant")
def setAttribute(self, attribute, value): def setAttribute(self, attribute, value):
self.push(commands.SetAttributeCommand(self._graph, attribute, value)) self.push(commands.SetAttributeCommand(self._graph, attribute, value))
@ -794,6 +855,12 @@ class UIGraph(QObject):
@Slot(Attribute) @Slot(Attribute)
def resetAttribute(self, attribute): def resetAttribute(self, attribute):
""" Reset 'attribute' to its default value """ """ 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())) self.push(commands.SetAttributeCommand(self._graph, attribute, attribute.defaultValue()))
@Slot(CompatibilityNode, result=Node) @Slot(CompatibilityNode, result=Node)

View 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"
}
}
}

View file

@ -12,3 +12,4 @@ TabPanel 1.0 TabPanel.qml
TextFileViewer 1.0 TextFileViewer.qml TextFileViewer 1.0 TextFileViewer.qml
ExifOrientedViewer 1.0 ExifOrientedViewer.qml ExifOrientedViewer 1.0 ExifOrientedViewer.qml
FilterComboBox 1.0 FilterComboBox.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.objectName != inputDragTarget.objectName // not an edge connector
|| drag.source.baseType !== inputDragTarget.baseType // not the same base type || drag.source.baseType !== inputDragTarget.baseType // not the same base type
|| drag.source.nodeItem === inputDragTarget.nodeItem // connection between attributes of the same node || 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.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) || 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 QtQuick 2.15
import GraphEditor 1.0 import GraphEditor 1.0
import QtQuick.Shapes 1.15 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. A cubic spline representing an edge, going from point1 to point2, providing mouse interaction.
*/ */
Shape { Item {
id: root id: root
property var edge property var edge
@ -15,6 +17,8 @@ Shape {
property real point2y property real point2y
property alias thickness: path.strokeWidth property alias thickness: path.strokeWidth
property alias color: path.strokeColor 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 // 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: edgeArea && edgeArea.containsMouse
@ -32,31 +36,71 @@ Shape {
property real endX: width property real endX: width
property real endY: height property real endY: height
// cause rendering artifacts when enabled (and don't support hot reload really well)
vendorExtensionsEnabled: false
ShapePath { Shape {
id: path anchors.fill: parent
startX: root.startX // cause rendering artifacts when enabled (and don't support hot reload really well)
startY: root.startY vendorExtensionsEnabled: false
fillColor: "transparent" opacity: 0.7
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 { ShapePath {
id: cubic id: path
property real ctrlPtDist: 30 startX: root.startX
x: root.endX startY: root.startY
y: root.endY fillColor: "transparent"
relativeControl1X: ctrlPtDist
relativeControl1Y: 0 strokeColor: "#3E3E3E"
control2X: x - ctrlPtDist strokeStyle: edge !== undefined && ((edge.src !== undefined && edge.src.isOutput) || edge.dst === undefined) ? ShapePath.SolidLine : ShapePath.DashLine
control2Y: y 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
}
} }
} }

View file

@ -379,13 +379,104 @@ Item {
width: 1000 width: 1000
height: 1000 height: 1000
Menu { Popup {
id: edgeMenu id: edgeMenu
property var currentEdge: null property var currentEdge: null
MenuItem { property bool forLoop: false
enabled: edgeMenu.currentEdge && !edgeMenu.currentEdge.dst.node.locked && !edgeMenu.currentEdge.dst.isReadOnly
text: "Remove" onOpened: {
onTriggered: uigraph.removeEdge(edgeMenu.currentEdge) 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 property bool isValidEdge: src !== undefined && dst !== undefined
visible: isValidEdge && src.visible && dst.visible 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) property bool inFocus: containsMouse || (edgeMenu.opened && edgeMenu.currentEdge === edge)
edge: object 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 color: edge.dst === root.edgeAboutToBeRemoved ? "red" : inFocus ? activePalette.highlight : activePalette.text
thickness: inFocus ? 2 : 1 thickness: (forLoop && inFocus) ? 3 : (forLoop || inFocus) ? 2 : 1
opacity: 0.7
point1x: isValidEdge ? src.globalX + src.outputAnchorPos.x : 0 point1x: isValidEdge ? src.globalX + src.outputAnchorPos.x : 0
point1y: isValidEdge ? src.globalY + src.outputAnchorPos.y : 0 point1y: isValidEdge ? src.globalY + src.outputAnchorPos.y : 0
point2x: isValidEdge ? dst.globalX + dst.inputAnchorPos.x : 0 point2x: isValidEdge ? dst.globalX + dst.inputAnchorPos.x : 0
@ -415,12 +510,16 @@ Item {
onPressed: { onPressed: {
const canEdit = !edge.dst.node.locked const canEdit = !edge.dst.node.locked
if (event.button === Qt.RightButton) { if (event.button) {
if (canEdit && (event.modifiers & Qt.AltModifier)) { if (canEdit && (event.modifiers & Qt.AltModifier)) {
uigraph.removeEdge(edge) uigraph.removeEdge(edge)
} else { } else {
edgeMenu.currentEdge = edge 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) onPressed: root.pressed(mouse)
onEdgeAboutToBeRemoved: root.edgeAboutToBeRemoved(input) onEdgeAboutToBeRemoved: root.edgeAboutToBeRemoved(input)
Component.onCompleted: attributePinCreated(object, outPin) Component.onCompleted: attributePinCreated(attribute, outPin)
onChildPinCreated: attributePinCreated(childAttribute, outPin)
Component.onDestruction: attributePinDeleted(attribute, outPin) Component.onDestruction: attributePinDeleted(attribute, outPin)
onChildPinDeleted: attributePinDeleted(childAttribute, outPin)
} }
} }
} }

View file

@ -144,81 +144,14 @@ FloatingPane {
anchors.fill: parent anchors.fill: parent
Item { IntSelector {
Layout.preferredWidth: previousFrameButton.width + frameMetrics.width + nextFrameButton.width id: frameInput
Layout.preferredHeight: frameInput.height
MouseArea { value: m.frame
id: mouseAreaFrameLabel range: frameRange
anchors.fill: parent onValueChanged: {
m.frame = value
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
}
}
}
} }
} }
@ -504,13 +437,6 @@ FloatingPane {
} }
} }
TextMetrics {
id: frameMetrics
font: frameInput.font
text: "10000"
}
TextMetrics { TextMetrics {
id: fpsMetrics id: fpsMetrics