mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-05-01 11:17:53 +02:00
332 lines
11 KiB
QML
Executable file
332 lines
11 KiB
QML
Executable file
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: palette }
|
|
|
|
MouseArea {
|
|
id: mouseArea
|
|
anchors.fill: parent
|
|
property double factor: 1.15
|
|
// Activate multisampling for edges antialiasing
|
|
layer.enabled: true
|
|
layer.samples: 8
|
|
|
|
hoverEnabled: true
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
|
drag.threshold: 0
|
|
onWheel: {
|
|
var zoomFactor = wheel.angleDelta.y > 0 ? factor : 1/factor
|
|
if(Math.min(draggable.width*draggable.scale*zoomFactor, draggable.height*draggable.scale*zoomFactor) < 10)
|
|
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 *= zoomFactor
|
|
draggable.scale = draggable.scale.toFixed(2)
|
|
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
|
|
uigraph.addNode(nodeType)
|
|
// retrieve node delegate (the last one created in the node repeater)
|
|
var item = nodeRepeater.itemAt(nodeRepeater.count-1)
|
|
// convert mouse position
|
|
// disable node animation on position
|
|
item.animatePosition = false
|
|
// set the node position
|
|
item.x = spawnPosition.x
|
|
item.y = spawnPosition.y
|
|
// reactivate animation on position
|
|
item.animatePosition = true
|
|
// select this node
|
|
draggable.selectNode(item)
|
|
}
|
|
|
|
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
|
|
|
|
function selectNode(delegate)
|
|
{
|
|
root.selectedNode = delegate.node
|
|
delegate.forceActiveFocus()
|
|
}
|
|
|
|
// 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 ? palette.highlight : palette.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("#607D8B", 1.2) : "#607D8B"
|
|
|
|
onAttributePinCreated: registerAttributePin(attribute, pin)
|
|
|
|
onPressed: draggable.selectNode(nodeDelegate)
|
|
onDoubleClicked: root.nodeDoubleClicked(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
|
|
}
|
|
|
|
// Fit graph to fill root
|
|
function fit() {
|
|
// compute bounding box
|
|
var first = nodeRepeater.itemAt(0)
|
|
var bbox = Qt.rect(first.x, first.y, 1, 1)
|
|
for(var i=0; i<root.graph.nodes.count; ++i) {
|
|
var item = nodeRepeater.itemAt(i)
|
|
bbox.x = Math.min(bbox.x, item.x)
|
|
bbox.y = Math.min(bbox.y, item.y)
|
|
bbox.width = Math.max(bbox.width, item.x+item.width)
|
|
bbox.height = Math.max(bbox.height, item.y+item.height)
|
|
}
|
|
bbox.width -= bbox.x
|
|
bbox.height -= bbox.y
|
|
// rescale
|
|
draggable.scale = Math.min(root.width/bbox.width, root.height/bbox.height)
|
|
// recenter
|
|
draggable.x = bbox.x*draggable.scale*-1 + (root.width-bbox.width*draggable.scale)*0.5
|
|
draggable.y = bbox.y*draggable.scale*-1 + (root.height-bbox.height*draggable.scale)*0.5
|
|
}
|
|
|
|
// Really basic auto-layout based on node depths
|
|
function doAutoLayout()
|
|
{
|
|
var depthProperty = useMinDepth ? 'minDepth' : 'depth'
|
|
var grid = new Array(nodeRepeater.count)
|
|
for(var i=0; i< nodeRepeater.count; ++i)
|
|
grid[i] = new Array(nodeRepeater.count)
|
|
for(var i=0; i<nodeRepeater.count; ++i)
|
|
{
|
|
var obj = nodeRepeater.itemAt(i);
|
|
}
|
|
|
|
for(var i=0; i<nodeRepeater.count; ++i)
|
|
{
|
|
var obj = nodeRepeater.itemAt(i);
|
|
var j=0;
|
|
while(1)
|
|
{
|
|
if(grid[obj.node[depthProperty]][j] == undefined)
|
|
{
|
|
grid[obj.node[depthProperty]][j] = obj;
|
|
break;
|
|
}
|
|
j++;
|
|
}
|
|
}
|
|
for(var x= 0; x<nodeRepeater.count; ++x)
|
|
{
|
|
for(var y=0; y<nodeRepeater.count; ++y)
|
|
{
|
|
if(grid[x][y] != undefined)
|
|
{
|
|
grid[x][y].x = x * (root.nodeWidth + root.gridSpacing)
|
|
grid[x][y].y = y * (root.nodeHeight + root.gridSpacing)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|