mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-04-30 10:47:34 +02:00
* use it in MetadataListView and GraphEditor 'add Node' menu * GraphEditor: forward MenuItem key events to searchBar to be able to continue editing the filter even if it lost active focus
533 lines
19 KiB
QML
Executable file
533 lines
19 KiB
QML
Executable file
import QtQuick 2.7
|
|
import QtQuick.Controls 2.3
|
|
import QtQuick.Layouts 1.3
|
|
import Controls 1.0
|
|
import Utils 1.0
|
|
import MaterialIcons 2.2
|
|
|
|
/**
|
|
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 var _attributeToDelegate: ({})
|
|
|
|
// signals
|
|
signal workspaceMoved()
|
|
signal workspaceClicked()
|
|
signal nodeDoubleClicked(var mouse, var node)
|
|
|
|
// trigger initial fit() after initialization
|
|
// (ensure GraphEditor has its final size)
|
|
Component.onCompleted: firstFitTimer.start()
|
|
|
|
Timer {
|
|
id: firstFitTimer
|
|
running: false
|
|
interval: 10
|
|
onTriggered: fit()
|
|
}
|
|
|
|
clip: true
|
|
|
|
SystemPalette { id: activePalette }
|
|
|
|
/// Get node delegate for the given node object
|
|
function nodeDelegate(node)
|
|
{
|
|
for(var i=0; i<nodeRepeater.count; ++i)
|
|
{
|
|
if(nodeRepeater.itemAt(i).node === node)
|
|
return nodeRepeater.itemAt(i)
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
/// Select node delegate
|
|
function selectNode(node)
|
|
{
|
|
uigraph.selectedNode = node
|
|
}
|
|
|
|
/// Duplicate a node and optionnally all the following ones
|
|
function duplicateNode(node, duplicateFollowingNodes) {
|
|
if(root.readOnly)
|
|
return;
|
|
var nodes = uigraph.duplicateNode(node, duplicateFollowingNodes)
|
|
selectNode(nodes[0])
|
|
}
|
|
|
|
|
|
Keys.onPressed: {
|
|
if(event.key === Qt.Key_F)
|
|
fit()
|
|
}
|
|
|
|
MouseArea {
|
|
id: mouseArea
|
|
anchors.fill: parent
|
|
property double factor: 1.15
|
|
property real minZoom: 0.1
|
|
property real maxZoom: 2.0
|
|
// 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
|
|
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 && mouse.modifiers == Qt.NoModifier)
|
|
selectNode(null)
|
|
|
|
if(mouse.button == Qt.MiddleButton || (mouse.button & Qt.LeftButton && mouse.modifiers & Qt.ShiftModifier))
|
|
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)
|
|
{
|
|
if(readOnly)
|
|
lockedMenu.popup();
|
|
else {
|
|
// 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, spawnPosition)
|
|
selectNode(node)
|
|
}
|
|
|
|
onVisibleChanged: {
|
|
if(visible) {
|
|
// when menu is shown,
|
|
// clear and give focus to the TextField filter
|
|
searchBar.clear()
|
|
searchBar.forceActiveFocus()
|
|
}
|
|
}
|
|
|
|
SearchBar {
|
|
id: searchBar
|
|
width: parent.width
|
|
}
|
|
|
|
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(searchBar.text.toLowerCase()) > -1
|
|
// Reset menu currentIndex if highlighted items gets filtered out
|
|
onVisibleChanged: if(highlighted) newNodeMenu.currentIndex = 0
|
|
text: modelData
|
|
// Forward key events to the search bar to continue typing seamlessly
|
|
// even if this delegate took the activeFocus due to mouse hovering
|
|
Keys.forwardTo: [searchBar.textField]
|
|
Keys.onPressed: {
|
|
event.accepted = false;
|
|
switch(event.key)
|
|
{
|
|
case Qt.Key_Return:
|
|
case Qt.Key_Enter:
|
|
// create node on validation (Enter/Return keys)
|
|
newNodeMenu.createNode(modelData);
|
|
newNodeMenu.close();
|
|
event.accepted = true;
|
|
break;
|
|
default:
|
|
searchBar.textField.forceActiveFocus();
|
|
}
|
|
}
|
|
// 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
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
|
|
// Informative contextual menu when graph is read-only
|
|
Menu {
|
|
id: lockedMenu
|
|
MenuItem {
|
|
id: item
|
|
font.pointSize: 8
|
|
enabled: false
|
|
text: "Computing - Graph is Locked!"
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: draggable
|
|
transformOrigin: Item.TopLeft
|
|
width: 1000
|
|
height: 1000
|
|
|
|
Menu {
|
|
id: edgeMenu
|
|
property var currentEdge: null
|
|
MenuItem {
|
|
text: "Remove"
|
|
enabled: !root.readOnly
|
|
onTriggered: uigraph.removeEdge(edgeMenu.currentEdge)
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
|
|
property bool inFocus: containsMouse || (edgeMenu.opened && edgeMenu.currentEdge == edge)
|
|
|
|
edge: object
|
|
color: inFocus ? activePalette.highlight : activePalette.text
|
|
thickness: inFocus ? 2 : 1
|
|
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(event.button == Qt.RightButton)
|
|
{
|
|
if(!root.readOnly && event.modifiers & Qt.AltModifier) {
|
|
uigraph.removeEdge(edge)
|
|
}
|
|
else {
|
|
edgeMenu.currentEdge = edge
|
|
edgeMenu.popup()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Menu {
|
|
id: nodeMenu
|
|
property var currentNode: null
|
|
property bool canComputeNode: currentNode != null && uigraph.graph.canCompute(currentNode)
|
|
onClosed: currentNode = null
|
|
|
|
MenuItem {
|
|
text: "Compute"
|
|
enabled: !uigraph.computing && !root.readOnly && nodeMenu.canComputeNode
|
|
onTriggered: uigraph.execute(nodeMenu.currentNode)
|
|
}
|
|
MenuItem {
|
|
text: "Submit"
|
|
enabled: !uigraph.computing && !root.readOnly && nodeMenu.canComputeNode
|
|
visible: uigraph.canSubmit
|
|
height: visible ? implicitHeight : 0
|
|
onTriggered: uigraph.submit(nodeMenu.currentNode)
|
|
}
|
|
MenuItem {
|
|
text: "Open Folder"
|
|
onTriggered: Qt.openUrlExternally(Filepath.stringToUrl(nodeMenu.currentNode.internalFolder))
|
|
}
|
|
MenuSeparator {}
|
|
MenuItem {
|
|
text: "Duplicate Node" + (duplicateFollowingButton.hovered ? "s From Here" : "")
|
|
enabled: !root.readOnly
|
|
onTriggered: duplicateNode(nodeMenu.currentNode, false)
|
|
MaterialToolButton {
|
|
id: duplicateFollowingButton
|
|
height: parent.height
|
|
anchors { right: parent.right; rightMargin: parent.padding }
|
|
text: MaterialIcons.fast_forward
|
|
onClicked: {
|
|
duplicateNode(nodeMenu.currentNode, true);
|
|
nodeMenu.close();
|
|
}
|
|
}
|
|
}
|
|
MenuItem {
|
|
text: "Remove Node" + (removeFollowingButton.hovered ? "s From Here" : "")
|
|
enabled: !root.readOnly
|
|
onTriggered: uigraph.removeNode(nodeMenu.currentNode)
|
|
MaterialToolButton {
|
|
id: removeFollowingButton
|
|
height: parent.height
|
|
anchors { right: parent.right; rightMargin: parent.padding }
|
|
text: MaterialIcons.fast_forward
|
|
onClicked: {
|
|
uigraph.removeNodesFrom(nodeMenu.currentNode);
|
|
nodeMenu.close();
|
|
}
|
|
}
|
|
}
|
|
MenuSeparator {}
|
|
MenuItem {
|
|
text: "Delete Data" + (deleteFollowingButton.hovered ? " From Here" : "" ) + "..."
|
|
enabled: !root.readOnly
|
|
|
|
function showConfirmationDialog(deleteFollowing) {
|
|
var obj = deleteDataDialog.createObject(root,
|
|
{
|
|
"node": nodeMenu.currentNode,
|
|
"deleteFollowing": deleteFollowing
|
|
});
|
|
obj.open()
|
|
nodeMenu.close();
|
|
}
|
|
|
|
onTriggered: showConfirmationDialog(false)
|
|
|
|
MaterialToolButton {
|
|
id: deleteFollowingButton
|
|
anchors { right: parent.right; rightMargin: parent.padding }
|
|
height: parent.height
|
|
text: MaterialIcons.fast_forward
|
|
onClicked: parent.showConfirmationDialog(true)
|
|
}
|
|
|
|
// Confirmation dialog for node cache deletion
|
|
Component {
|
|
id: deleteDataDialog
|
|
MessageDialog {
|
|
property var node
|
|
property bool deleteFollowing: false
|
|
|
|
focus: true
|
|
modal: false
|
|
header.visible: false
|
|
|
|
text: "Delete Data computed by '" + node.label + (deleteFollowing ? "' and following Nodes?" : "'?")
|
|
helperText: "Warning: This operation can not be undone."
|
|
standardButtons: Dialog.Yes | Dialog.Cancel
|
|
|
|
onAccepted: {
|
|
if(deleteFollowing)
|
|
graph.clearDataFrom(node);
|
|
else
|
|
node.clearData();
|
|
}
|
|
onClosed: destroy()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Nodes
|
|
Repeater {
|
|
id: nodeRepeater
|
|
|
|
model: root.graph.nodes
|
|
property bool loaded: count === model.count
|
|
|
|
delegate: Node {
|
|
id: nodeDelegate
|
|
|
|
property bool animatePosition: true
|
|
|
|
node: object
|
|
width: uigraph.layout.nodeWidth
|
|
readOnly: root.readOnly
|
|
selected: uigraph.selectedNode === node
|
|
hovered: uigraph.hoveredNode === node
|
|
onSelectedChanged: if(selected) forceActiveFocus()
|
|
|
|
onAttributePinCreated: registerAttributePin(attribute, pin)
|
|
onAttributePinDeleted: unregisterAttributePin(attribute, pin)
|
|
|
|
onPressed: {
|
|
selectNode(node)
|
|
|
|
if(mouse.button == Qt.LeftButton && mouse.modifiers & Qt.AltModifier)
|
|
{
|
|
duplicateNode(node, true)
|
|
}
|
|
if(mouse.button == Qt.RightButton)
|
|
{
|
|
nodeMenu.currentNode = node
|
|
nodeMenu.popup()
|
|
}
|
|
}
|
|
|
|
onDoubleClicked: root.nodeDoubleClicked(mouse, node)
|
|
|
|
onMoved: uigraph.moveNode(node, position)
|
|
|
|
onEntered: uigraph.hoveredNode = node
|
|
onExited: uigraph.hoveredNode = null
|
|
|
|
Keys.onDeletePressed: {
|
|
if(root.readOnly)
|
|
return;
|
|
if(event.modifiers == Qt.AltModifier)
|
|
uigraph.removeNodesFrom(node)
|
|
else
|
|
uigraph.removeNode(node)
|
|
}
|
|
|
|
Behavior on x {
|
|
enabled: animatePosition
|
|
NumberAnimation { duration: 100 }
|
|
}
|
|
Behavior on y {
|
|
enabled: animatePosition
|
|
NumberAnimation { duration: 100 }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Toolbar
|
|
FloatingPane {
|
|
padding: 2
|
|
anchors.bottom: parent.bottom
|
|
RowLayout {
|
|
spacing: 4
|
|
// Fit
|
|
MaterialToolButton {
|
|
text: MaterialIcons.fullscreen
|
|
ToolTip.text: "Fit"
|
|
onClicked: root.fit()
|
|
}
|
|
// Auto-Layout
|
|
MaterialToolButton {
|
|
text: MaterialIcons.linear_scale
|
|
ToolTip.text: "Auto-Layout"
|
|
onClicked: uigraph.layout.reset()
|
|
}
|
|
|
|
// Separator
|
|
Rectangle {
|
|
Layout.fillHeight: true
|
|
Layout.margins: 2
|
|
implicitWidth: 1
|
|
color: activePalette.window
|
|
}
|
|
// Settings
|
|
MaterialToolButton {
|
|
text: MaterialIcons.settings
|
|
font.pointSize: 11
|
|
onClicked: menu.open()
|
|
Menu {
|
|
id: menu
|
|
y: -height
|
|
padding: 4
|
|
RowLayout {
|
|
spacing: 2
|
|
Label {
|
|
padding: 2
|
|
text: "Auto-Layout Depth:"
|
|
}
|
|
ComboBox {
|
|
flat: true
|
|
model: ['Minimum', 'Maximum']
|
|
implicitWidth: 80
|
|
currentIndex: uigraph.layout.depthMode
|
|
onActivated: {
|
|
uigraph.layout.depthMode = currentIndex
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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, first.x + first.width, first.y + first.height)
|
|
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
|
|
return bbox;
|
|
}
|
|
|
|
// Fit graph to fill root
|
|
function fit() {
|
|
// compute bounding box
|
|
var bbox = boundingBox()
|
|
// 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
|
|
}
|
|
|
|
}
|