Meshroom/meshroom/ui/qml/GraphEditor/AttributePin.qml
Candice Bentéjac f521bfa663 [GraphEditor] Remove enabling/disabling pins depending on edge's visibility
When edges could exist while being invisible in the graph, pins needed
to be enabled/disabled manually according to the visibility of the edge.
This was especially important when closing the application and removing
all the (hidden) edges as the pins needed to be properly destryed as well.

Edges are not hidden anymore, and any attribute that should be hidden
but is in fact connected remains visible as long as the connection exists.
2024-12-30 19:02:38 +01:00

457 lines
18 KiB
QML
Executable file

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Utils 1.0
import MaterialIcons 2.2
/**
* The representation of an Attribute on a Node.
*/
RowLayout {
id: root
property var nodeItem
property var attribute
property bool expanded: false
property bool readOnly: false
/// Whether to display an output pin for input attribute
property bool displayOutputPinForInput: true
// position of the anchor for attaching and edge to this attribute pin
readonly property point inputAnchorPos: Qt.point(inputAnchor.x + inputAnchor.width / 2,
inputAnchor.y + inputAnchor.height / 2)
readonly property point outputAnchorPos: Qt.point(outputAnchor.x + outputAnchor.width / 2,
outputAnchor.y + outputAnchor.height / 2)
readonly property bool isList: attribute && attribute.type === "ListAttribute"
readonly property bool isGroup: attribute && attribute.type === "GroupAttribute"
readonly property bool isChild: attribute && attribute.root
readonly property bool isConnected: attribute.isLinkNested || attribute.hasOutputConnections
signal childPinCreated(var childAttribute, var pin)
signal childPinDeleted(var childAttribute, var pin)
signal pressed(var mouse)
signal edgeAboutToBeRemoved(var input)
signal clicked()
objectName: attribute ? attribute.name + "." : ""
layoutDirection: Qt.LeftToRight
spacing: 3
ToolTip {
text: attribute.fullName + ": " + attribute.type
visible: nameLabel.hovered
y: nameLabel.y + nameLabel.height
x: nameLabel.x
}
function updateLabel() {
var label = ""
var expandedGroup = expanded ? "-" : "+"
if (attribute && attribute.label !== undefined) {
label = attribute.label
if (isGroup && attribute.isOutput) {
label = label + " " + expandedGroup
} else if (isGroup && !attribute.isOutput) {
label = expandedGroup + " " + label
}
}
return label
}
// Instantiate empty Items for each child attribute
Repeater {
id: childrenRepeater
model: isList && !attribute.isLink ? attribute.value : 0
onItemAdded: function(index, item) { childPinCreated(item.childAttribute, root) }
onItemRemoved: function(index, item) { childPinDeleted(item.childAttribute, root) }
delegate: Item {
property var childAttribute: object
visible: false
}
}
Rectangle {
visible: !attribute.isOutput
id: inputAnchor
width: 8
height: width
radius: isList ? 0 : width / 2
Layout.alignment: Qt.AlignVCenter
border.color: Colors.sysPalette.mid
color: Colors.sysPalette.base
Rectangle {
id: innerInputAnchor
property bool linkEnabled: true
visible: inputConnectMA.containsMouse || childrenRepeater.count > 0 || (attribute && attribute.isLink && linkEnabled) || inputConnectMA.drag.active || inputDropArea.containsDrag
radius: isList ? 0 : 2
anchors.fill: parent
anchors.margins: 2
color: {
if (inputConnectMA.containsMouse || inputConnectMA.drag.active || (inputDropArea.containsDrag && inputDropArea.acceptableDrop))
return Colors.sysPalette.highlight
return Colors.sysPalette.text
}
}
DropArea {
id: inputDropArea
property bool acceptableDrop: false
// Add negative margins for DropArea to make the connection zone easier to reach
anchors.fill: parent
anchors.margins: -2
// Add horizontal negative margins according to the current layout
anchors.rightMargin: -root.width * 0.3
keys: [inputDragTarget.objectName]
onEntered: function(drag) {
// Check if attributes are compatible to create a valid connection
if (root.readOnly // Cannot connect on a read-only attribute
|| 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 && 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)
) {
// Refuse attributes connection
drag.accepted = false
} else if (inputDragTarget.attribute.isLink) { // Already connected attribute
root.edgeAboutToBeRemoved(inputDragTarget.attribute)
}
inputDropArea.acceptableDrop = drag.accepted
}
onExited: {
if (inputDragTarget.attribute.isLink) { // Already connected attribute
root.edgeAboutToBeRemoved(undefined)
}
acceptableDrop = false
drag.source.dropAccepted = false
}
onDropped: function(drop) {
root.edgeAboutToBeRemoved(undefined)
_reconstruction.addEdge(drag.source.attribute, inputDragTarget.attribute)
}
}
Item {
id: inputDragTarget
objectName: "edgeConnector"
readonly property string connectorType: "input"
readonly property alias attribute: root.attribute
readonly property alias nodeItem: root.nodeItem
readonly property bool isOutput: Boolean(attribute.isOutput)
readonly property string baseType: attribute.baseType !== undefined ? attribute.baseType : ""
readonly property alias isList: root.isList
property bool dragAccepted: false
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width
height: parent.height
Drag.keys: [inputDragTarget.objectName]
Drag.active: inputConnectMA.drag.active
Drag.hotSpot.x: width * 0.5
Drag.hotSpot.y: height * 0.5
}
MouseArea {
id: inputConnectMA
drag.target: attribute.isReadOnly ? undefined : inputDragTarget
drag.threshold: 0
// Move the edge's tip straight to the the current mouse position instead of waiting after the drag operation has started
drag.smoothed: false
enabled: !root.readOnly
anchors.fill: parent
// Use the same negative margins as DropArea to ease pin selection
anchors.margins: inputDropArea.anchors.margins
anchors.leftMargin: inputDropArea.anchors.leftMargin
anchors.rightMargin: inputDropArea.anchors.rightMargin
onPressed: function(mouse) {
root.pressed(mouse)
}
onReleased: {
inputDragTarget.Drag.drop()
}
onClicked: root.clicked()
hoverEnabled: root.visible
}
Edge {
id: inputConnectEdge
visible: false
point1x: inputDragTarget.x + inputDragTarget.width / 2
point1y: inputDragTarget.y + inputDragTarget.height / 2
point2x: parent.width / 2
point2y: parent.width / 2
color: palette.highlight
thickness: outputDragTarget.dropAccepted ? 2 : 1
}
}
// Attribute name
Item {
id: nameContainer
implicitHeight: childrenRect.height
Layout.fillWidth: true
Layout.alignment: {
if (attribute.isOutput) {
return Qt.AlignRight | Qt.AlignVCenter
}
return Qt.AlignLeft | Qt.AlignVCenter
}
MaterialToolLabel {
id: nameLabel
anchors.rightMargin: 0
anchors.right: attribute && attribute.isOutput ? parent.right : undefined
labelIconRow.layoutDirection: attribute.isOutput ? Qt.RightToLeft : Qt.LeftToRight
labelIconRow.spacing: 0
enabled: !root.readOnly
visible: true
property bool hovered: (inputConnectMA.containsMouse || inputConnectMA.drag.active ||
inputDropArea.containsDrag || outputConnectMA.containsMouse ||
outputConnectMA.drag.active || outputDropArea.containsDrag)
labelIconColor: {
if ((object.hasOutputConnections || object.isLink) && !object.enabled) {
return Colors.lightgrey
} else if (hovered) {
return palette.highlight
}
return palette.text
}
labelIconMouseArea.enabled: false // Prevent mixing mouse interactions between the label and the pin context
// Text
label.text: attribute.label
label.font.pointSize: 7
label.elide: hovered ? Text.ElideNone : Text.ElideMiddle
label.horizontalAlignment: attribute && attribute.isOutput ? Text.AlignRight : Text.AlignLeft
// Icon
iconText: {
if (isGroup) {
return expanded ? MaterialIcons.expand_more : MaterialIcons.chevron_right
}
return ""
}
iconSize: 7
icon.horizontalAlignment: attribute && attribute.isOutput ? Text.AlignRight : Text.AlignLeft
// Handle tree view for nested attributes
icon.leftPadding: {
if (attribute.depth != 0 && !attribute.isOutput) {
return attribute.depth * 10
}
return 0
}
icon.rightPadding: {
if (attribute.depth != 0 && attribute.isOutput) {
return attribute.depth * 10
}
return 0
}
}
}
Rectangle {
id: outputAnchor
visible: displayOutputPinForInput || attribute.isOutput
width: 8
height: width
radius: isList ? 0 : width / 2
Layout.alignment: Qt.AlignVCenter
border.color: Colors.sysPalette.mid
color: Colors.sysPalette.base
Rectangle {
id: innerOutputAnchor
property bool linkEnabled: true
visible: (attribute.hasOutputConnections && linkEnabled) || outputConnectMA.containsMouse || outputConnectMA.drag.active || outputDropArea.containsDrag
radius: isList ? 0 : 2
anchors.fill: parent
anchors.margins: 2
color: {
if (modelData.enabled && (outputConnectMA.containsMouse || outputConnectMA.drag.active ||
(outputDropArea.containsDrag && outputDropArea.acceptableDrop)))
return Colors.sysPalette.highlight
return Colors.sysPalette.text
}
}
DropArea {
id: outputDropArea
property bool acceptableDrop: false
// Add negative margins for DropArea to make the connection zone easier to reach
anchors.fill: parent
anchors.margins: -2
// Add horizontal negative margins according to the current layout
anchors.leftMargin: -root.width * 0.2
keys: [outputDragTarget.objectName]
onEntered: function(drag) {
// Check if attributes are compatible to create a valid connection
if (drag.source.objectName != outputDragTarget.objectName // Not an edge connector
|| drag.source.baseType !== outputDragTarget.baseType // Not the same base type
|| drag.source.nodeItem === outputDragTarget.nodeItem // Connection between attributes of the same node
|| (!drag.source.isList && outputDragTarget.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 === "output" // Refuse to connect an output pin on another one
) {
// Refuse attributes connection
drag.accepted = false
} else if (drag.source.attribute.isLink) { // Already connected attribute
root.edgeAboutToBeRemoved(drag.source.attribute)
}
outputDropArea.acceptableDrop = drag.accepted
}
onExited: {
root.edgeAboutToBeRemoved(undefined)
acceptableDrop = false
}
onDropped: function(drop) {
root.edgeAboutToBeRemoved(undefined)
_reconstruction.addEdge(outputDragTarget.attribute, drag.source.attribute)
}
}
Item {
id: outputDragTarget
objectName: "edgeConnector"
readonly property string connectorType: "output"
readonly property alias attribute: root.attribute
readonly property alias nodeItem: root.nodeItem
readonly property bool isOutput: Boolean(attribute.isOutput)
readonly property alias isList: root.isList
readonly property string baseType: attribute.baseType !== undefined ? attribute.baseType : ""
property bool dropAccepted: false
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: parent.height
Drag.keys: [outputDragTarget.objectName]
Drag.active: outputConnectMA.drag.active
Drag.hotSpot.x: width * 0.5
Drag.hotSpot.y: height * 0.5
}
MouseArea {
id: outputConnectMA
drag.target: outputDragTarget
drag.threshold: 0
// Move the edge's tip straight to the the current mouse position instead of waiting after the drag operation has started
drag.smoothed: false
anchors.fill: parent
// Use the same negative margins as DropArea to ease pin selection
anchors.margins: outputDropArea.anchors.margins
anchors.leftMargin: outputDropArea.anchors.leftMargin
anchors.rightMargin: outputDropArea.anchors.rightMargin
onPressed: function(mouse) { root.pressed(mouse) }
onReleased: outputDragTarget.Drag.drop()
onClicked: root.clicked()
hoverEnabled: root.visible
}
Edge {
id: outputConnectEdge
visible: false
point1x: parent.width / 2
point1y: parent.width / 2
point2x: outputDragTarget.x + outputDragTarget.width / 2
point2y: outputDragTarget.y + outputDragTarget.height / 2
color: palette.highlight
thickness: outputDragTarget.dropAccepted ? 2 : 1
}
}
state: (inputConnectMA.pressed) ? "DraggingInput" : outputConnectMA.pressed ? "DraggingOutput" : ""
states: [
State {
name: ""
AnchorChanges {
target: outputDragTarget
anchors.horizontalCenter: outputAnchor.horizontalCenter
anchors.verticalCenter: outputAnchor.verticalCenter
}
AnchorChanges {
target: inputDragTarget
anchors.horizontalCenter: inputAnchor.horizontalCenter
anchors.verticalCenter: inputAnchor.verticalCenter
}
PropertyChanges {
target: inputDragTarget
x: 0
y: 0
}
PropertyChanges {
target: outputDragTarget
x: 0
y: 0
}
},
State {
name: "DraggingInput"
AnchorChanges {
target: inputDragTarget
anchors.horizontalCenter: undefined
anchors.verticalCenter: undefined
}
PropertyChanges {
target: inputConnectEdge
z: 100
visible: true
}
StateChangeScript {
script: {
// Add the right offset if the initial click is not exactly at the center of the connection circle.
var pos = inputDragTarget.mapFromItem(inputConnectMA, inputConnectMA.mouseX, inputConnectMA.mouseY);
inputDragTarget.x = pos.x - inputDragTarget.width / 2;
inputDragTarget.y = pos.y - inputDragTarget.height / 2;
}
}
},
State {
name: "DraggingOutput"
AnchorChanges {
target: outputDragTarget
anchors.horizontalCenter: undefined
anchors.verticalCenter: undefined
}
PropertyChanges {
target: outputConnectEdge
z: 100
visible: true
}
StateChangeScript {
script: {
var pos = outputDragTarget.mapFromItem(outputConnectMA, outputConnectMA.mouseX, outputConnectMA.mouseY);
outputDragTarget.x = pos.x - outputDragTarget.width / 2;
outputDragTarget.y = pos.y - outputDragTarget.height / 2;
}
}
}
]
}