import QtQuick 2.7 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 /** A component displaying a Graph (nodes, attributes and edges). */ Item { id: root property variant uigraph: null /// Meshroom ui graph (UIGraph) readonly property variant graph: uigraph ? uigraph.graph : null /// core graph contained in ui graph property variant nodeTypesModel: null /// the list of node types that can be instantiated property bool readOnly: false property variant selectedNode: null property int nodeWidth: 140 property int nodeHeight: 80 property int gridSpacing: 15 property bool useMinDepth: true property var _attributeToDelegate: ({}) // signals signal workspaceMoved() signal workspaceClicked() signal nodeDoubleClicked(var node) onUseMinDepthChanged: doAutoLayout() clip: true SystemPalette { id: activePalette } /// Get node delegate based on a node name function nodeDelegate(nodeName) { for(var i=0; i 0 ? factor : 1/factor var scale = draggable.scale * zoomFactor scale = Math.min(Math.max(minZoom, scale), maxZoom) if(draggable.scale == scale) return var point = mapToItem(draggable, wheel.x, wheel.y) draggable.x += (1-zoomFactor) * point.x * draggable.scale draggable.y += (1-zoomFactor) * point.y * draggable.scale draggable.scale = scale workspaceMoved() } onPressed: { if(mouse.button & Qt.MiddleButton) drag.target = draggable // start drag } onReleased: { drag.target = undefined // stop drag root.forceActiveFocus() workspaceClicked() } onPositionChanged: { if(drag.active) workspaceMoved() } onClicked: { if(mouse.button & Qt.RightButton) { // store mouse click position in 'draggable' coordinates as new node spawn position newNodeMenu.spawnPosition = mouseArea.mapToItem(draggable, mouse.x, mouse.y) newNodeMenu.popup() } } // Contextual Menu for creating new nodes // TODO: add filtering + validate on 'Enter' Menu { id: newNodeMenu property point spawnPosition function createNode(nodeType) { // add node via the proper command in uigraph var node = uigraph.addNewNode(nodeType) moveNode(node.name, spawnPosition.x, spawnPosition.y) } onVisibleChanged: { if(visible) { // when menu is shown, // clear and give focus to the TextField filter filterTextField.clear() filterTextField.forceActiveFocus() } } TextField { id: filterTextField selectByMouse: true width: parent.width // ensure down arrow give focus to the first MenuItem // (without this, we have to pressed the down key twice to do so) Keys.onDownPressed: nextItemInFocusChain().forceActiveFocus() } Repeater { model: root.nodeTypesModel // Create Menu items from available node types model delegate: MenuItem { id: menuItemDelegate font.pointSize: 8 padding: 3 // Hide items that does not match the filter text visible: modelData.toLowerCase().indexOf(filterTextField.text.toLocaleLowerCase()) > -1 text: modelData Keys.onPressed: { switch(event.key) { case Qt.Key_Return: case Qt.Key_Enter: // create node on validation (Enter/Return keys) newNodeMenu.createNode(modelData) newNodeMenu.dismiss() break; case Qt.Key_Home: // give focus back to filter filterTextField.forceActiveFocus() break; default: break; } } // Create node on mouse click onClicked: newNodeMenu.createNode(modelData) states: [ State { // Additional property setting when the MenuItem is not visible when: !visible name: "invisible" PropertyChanges { target: menuItemDelegate height: 0 // make sure the item is no visible by setting height to 0 focusPolicy: Qt.NoFocus // don't grab focus when not visible } } ] } } } Item { id: draggable transformOrigin: Item.TopLeft width: 1000 height: 1000 // Edges Repeater { id: edgesRepeater // delay edges loading after nodes (edges needs attribute pins to be created) model: nodeRepeater.loaded ? root.graph.edges : undefined delegate: Edge { property var src: root._attributeToDelegate[edge.src] property var dst: root._attributeToDelegate[edge.dst] property var srcAnchor: src.nodeItem.mapFromItem(src, src.edgeAnchorPos.x, src.edgeAnchorPos.y) property var dstAnchor: dst.nodeItem.mapFromItem(dst, dst.edgeAnchorPos.x, dst.edgeAnchorPos.y) edge: object color: containsMouse && !readOnly ? activePalette.highlight : activePalette.text opacity: 0.7 point1x: src.nodeItem.x + srcAnchor.x point1y: src.nodeItem.y + srcAnchor.y point2x: dst.nodeItem.x + dstAnchor.x point2y: dst.nodeItem.y + dstAnchor.y onPressed: { if(!root.readOnly && event.button == Qt.RightButton) uigraph.removeEdge(edge) } } } // Nodes Repeater { id: nodeRepeater model: root.graph.nodes property bool loaded: count === model.count onLoadedChanged: if(loaded) { doAutoLayout() } delegate: Node { id: nodeDelegate property bool animatePosition: true node: object width: root.nodeWidth readOnly: root.readOnly baseColor: root.selectedNode == node ? Qt.lighter(defaultColor, 1.2) : defaultColor onAttributePinCreated: registerAttributePin(attribute, pin) onAttributePinDeleted: unregisterAttributePin(attribute, pin) onPressed: { if(mouse.modifiers & Qt.AltModifier) { var delegates = duplicate(true) selectNode(delegates[0]) } else selectNode(nodeDelegate) } function duplicate(duplicateFollowingNodes) { var nodes = uigraph.duplicateNode(node, duplicateFollowingNodes) var delegates = [] var from = nodeRepeater.count - nodes.length var to = nodeRepeater.count - 1 for(var i=from; i <= to; ++i) { delegates.push(nodeRepeater.itemAt(i)) } doAutoLayout(from, to, x, y + (root.nodeHeight + root.gridSpacing)) return delegates } onDoubleClicked: root.nodeDoubleClicked(node) onComputeRequest: uigraph.execute(node) onSubmitRequest: uigraph.submit(node) onDuplicateRequest: { var delegate = duplicate(duplicateFollowingNodes)[0] selectNode(delegate) } onRemoveRequest: uigraph.removeNode(node) Keys.onDeletePressed: uigraph.removeNode(node) Behavior on x { enabled: animatePosition NumberAnimation {} } Behavior on y { enabled: animatePosition NumberAnimation {} } } } } } Row { anchors.bottom: parent.bottom Button { text: "Fit" onClicked: root.fit() z: 10 } Button { text: "Layout" onClicked: root.doAutoLayout() z: 10 } ComboBox { model: ['Min Depth', 'Max Depth'] onActivated: { useMinDepth = currentIndex == 0 } } } function registerAttributePin(attribute, pin) { root._attributeToDelegate[attribute] = pin } function unregisterAttributePin(attribute, pin) { delete root._attributeToDelegate[attribute] } function boundingBox() { var first = nodeRepeater.itemAt(0) var bbox = Qt.rect(first.x, first.y, 1, 1) for(var i=0; i 0 ? nodeRepeater.itemAt(from).node[depthProperty] : 0 for(var i=0; i