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 delay: 500 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: root.isList && !root.attribute.isLink ? root.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: !root.attribute.isOutput id: inputAnchor width: 8 height: width radius: root.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 || (root.attribute && root.attribute.isLink && linkEnabled) || inputConnectMA.drag.active || inputDropArea.containsDrag radius: root.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) || (drag.source.isGroup || inputDragTarget.isGroup) // Refuse connection between Groups, which is unsupported ) { // 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 readonly property alias isGroup: root.isGroup 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: root.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 property bool dragTriggered: false // An edge is being dragged from the output connector property bool isPressed: false // The mouse has been pressed but not yet released property double initialX: 0.0 property double initialY: 0.0 onPressed: function(mouse) { root.pressed(mouse) isPressed = true initialX = mouse.x initialY = mouse.y } onReleased: function(mouse) { inputDragTarget.Drag.drop() isPressed = false dragTriggered = false } onClicked: root.clicked() onPositionChanged: function(mouse) { // If there's been a significant (10px along the X- or Y- axis) while the mouse is being pressed, // then we can consider being in the dragging state if (isPressed && (Math.abs(mouse.x - initialX) >= 5.0 || Math.abs(mouse.y - initialY) >= 5.0)) { dragTriggered = true } } 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 (root.attribute.isOutput) { return Qt.AlignRight | Qt.AlignVCenter } return Qt.AlignLeft | Qt.AlignVCenter } MaterialToolLabel { id: nameLabel anchors.rightMargin: 0 labelIconRow.layoutDirection: root.attribute.isOutput ? Qt.RightToLeft : Qt.LeftToRight anchors.right: root.attribute && root.attribute.isOutput ? parent.right : undefined labelIconRow.spacing: 0 width: { if (hovered) { return icon.width + label.contentWidth } else { if (nameContainer.width > 0 && icon.width + label.contentWidth < nameContainer.width) return icon.width + label.contentWidth return nameContainer.width } } enabled: !root.readOnly visible: true property bool parentNotReady: nameContainer.width == 0 // Allows to trigger a change of state once the parent is ready, // ensuring the correct width of the elements upon their first // display without waiting for a mouse interaction property bool hovered: parentNotReady || (inputConnectMA.containsMouse || inputConnectMA.drag.active || inputDropArea.containsDrag || outputConnectMA.containsMouse || outputConnectMA.drag.active || outputDropArea.containsDrag) labelIconColor: { if ((root.attribute.hasOutputConnections || root.attribute.isLink) && !root.attribute.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: root.attribute.label label.font.pointSize: 7 labelWidth: hovered ? label.contentWidth : nameLabel.width - icon.width label.elide: hovered ? Text.ElideNone : Text.ElideMiddle label.horizontalAlignment: root.attribute && root.attribute.isOutput ? Text.AlignRight : Text.AlignLeft // Icon iconText: { if (root.isGroup) { return root.expanded ? MaterialIcons.expand_more : MaterialIcons.chevron_right } return "" } iconSize: 7 icon.horizontalAlignment: root.attribute && root.attribute.isOutput ? Text.AlignRight : Text.AlignLeft // Handle tree view for nested attributes icon.leftPadding: { if (root.attribute.depth != 0 && !root.attribute.isOutput) { return root.attribute.depth * 10 } return 0 } icon.rightPadding: { if (root.attribute.depth != 0 && root.attribute.isOutput) { return root.attribute.depth * 10 } return 0 } } } Rectangle { id: outputAnchor visible: root.displayOutputPinForInput || root.attribute.isOutput width: 8 height: width radius: root.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: (root.attribute.hasOutputConnections && linkEnabled) || outputConnectMA.containsMouse || outputConnectMA.drag.active || outputDropArea.containsDrag radius: root.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 || (drag.source.isGroup || outputDragTarget.isGroup) // Refuse connection between Groups, which is unsupported ) { // 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 alias isGroup: root.isGroup readonly property string baseType: root.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 property bool dragTriggered: false // An edge is being dragged from the output connector property bool isPressed: false // The mouse has been pressed but not yet released property double initialX: 0.0 property double initialY: 0.0 onPressed: function(mouse) { root.pressed(mouse) isPressed = true initialX = mouse.x initialY = mouse.y } onReleased: function(mouse) { outputDragTarget.Drag.drop() isPressed = false dragTriggered = false } onClicked: root.clicked() onPositionChanged: function(mouse) { // If there's been a significant (10px along the X- or Y- axis) while the mouse is being pressed, // then we can consider being in the dragging state if (isPressed && (Math.abs(mouse.x - initialX) >= 5.0 || Math.abs(mouse.y - initialY) >= 5.0)) { dragTriggered = true } } 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.dragTriggered ? "DraggingInput" : outputConnectMA.dragTriggered ? "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; } } } ] }