mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-04-29 02:08:08 +02:00
[ui] introduce first functional UI with a graph editor
First functional UI that allows to visualize, modify and execute a graph locally. * use QtQuick Controls 2 + Shapes (Qt >=5.10) * main menu to save/load a graph
This commit is contained in:
parent
e683238a8d
commit
00366cda00
8 changed files with 1004 additions and 117 deletions
60
meshroom/ui/qml/AttributeEditor.qml
Normal file
60
meshroom/ui/qml/AttributeEditor.qml
Normal file
|
@ -0,0 +1,60 @@
|
|||
import QtQuick 2.9
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
/**
|
||||
A component to display and edit a Node's attributes.
|
||||
*/
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
// the node to edit
|
||||
property variant node: null
|
||||
|
||||
SystemPalette { id: palette }
|
||||
|
||||
Button {
|
||||
text: "Open Node Folder"
|
||||
onClicked: Qt.openUrlExternally("file://" + node.internalFolder)
|
||||
ToolTip.text: node.internalFolder
|
||||
ToolTip.visible: hovered
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: attributesListView
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
clip: true
|
||||
spacing: 4
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
|
||||
model: node ? node.attributes : undefined
|
||||
|
||||
delegate: RowLayout {
|
||||
width: attributesListView.width
|
||||
spacing: 4
|
||||
|
||||
Label {
|
||||
id: parameterLabel
|
||||
text: object.label
|
||||
Layout.preferredWidth: 200
|
||||
color: object.isOutput ? "orange" : palette.text
|
||||
ToolTip.text: object.desc.description
|
||||
ToolTip.visible: parameterMA.containsMouse
|
||||
ToolTip.delay: 200
|
||||
MouseArea {
|
||||
id: parameterMA
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
}
|
||||
}
|
||||
|
||||
AttributeItemDelegate {
|
||||
Layout.fillWidth: true
|
||||
height: childrenRect.height
|
||||
attribute: object
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
173
meshroom/ui/qml/AttributeItemDelegate.qml
Normal file
173
meshroom/ui/qml/AttributeItemDelegate.qml
Normal file
|
@ -0,0 +1,173 @@
|
|||
import QtQuick 2.9
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Controls 2.2
|
||||
|
||||
/**
|
||||
Instantiate a control to visualize and edit an Attribute based on its type.
|
||||
*/
|
||||
Loader {
|
||||
id: root
|
||||
property variant attribute: null
|
||||
|
||||
sourceComponent: {
|
||||
switch(attribute.type)
|
||||
{
|
||||
case "ChoiceParam": return attribute.desc.exclusive ? comboBox_component : multiChoice_component
|
||||
case "IntParam": return slider_component
|
||||
case "FloatParam": return slider_component
|
||||
case "BoolParam": return checkbox_component
|
||||
case "ListAttribute": return listAttribute_component
|
||||
case "GroupAttribute": return groupAttribute_component
|
||||
default: return textField_component
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: textField_component
|
||||
TextField {
|
||||
text: attribute.value
|
||||
readOnly: attribute.isOutput || attribute.isLink
|
||||
selectByMouse: true
|
||||
onEditingFinished: _reconstruction.setAttribute(attribute, text.trim())
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: comboBox_component
|
||||
ComboBox {
|
||||
model: attribute.desc.values
|
||||
enabled: !attribute.isOutput && !attribute.isLink
|
||||
Component.onCompleted: currentIndex = find(attribute.value)
|
||||
onActivated: _reconstruction.setAttribute(attribute, currentText)
|
||||
Connections {
|
||||
target: attribute
|
||||
onValueChanged: currentIndex = find(attribute.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: multiChoice_component
|
||||
Flow {
|
||||
Repeater {
|
||||
id: checkbox_repeater
|
||||
model: attribute.desc.values
|
||||
delegate: CheckBox {
|
||||
text: modelData
|
||||
checked: attribute.value.indexOf(modelData) >= 0
|
||||
onToggled: {
|
||||
var t = attribute.value
|
||||
if(!checked) { t.splice(t.indexOf(modelData), 1) } // remove element
|
||||
else { t.push(modelData) } // add element
|
||||
_reconstruction.setAttribute(attribute, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: slider_component
|
||||
RowLayout {
|
||||
Slider {
|
||||
id: s
|
||||
Layout.fillWidth: true
|
||||
value: attribute.value
|
||||
// TODO: range from desc
|
||||
|
||||
onPressedChanged: {
|
||||
if(!pressed)
|
||||
_reconstruction.setAttribute(attribute, value)
|
||||
}
|
||||
}
|
||||
IntValidator {
|
||||
id: intValidator
|
||||
}
|
||||
DoubleValidator {
|
||||
id: doubleValidator
|
||||
}
|
||||
TextField {
|
||||
text: attribute.value
|
||||
selectByMouse: true
|
||||
validator: attribute.type == "FloatParam" ? doubleValidator : intValidator
|
||||
onEditingFinished: _reconstruction.setAttribute(attribute, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: checkbox_component
|
||||
Row {
|
||||
CheckBox {
|
||||
checked: attribute.value
|
||||
onToggled: _reconstruction.setAttribute(attribute, !attribute.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: listAttribute_component
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
text: attribute.value.count + " elements"
|
||||
}
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
text: "+"
|
||||
onClicked: _reconstruction.appendAttribute(attribute, undefined)
|
||||
}
|
||||
ListView {
|
||||
id: lv
|
||||
model: attribute.value
|
||||
implicitHeight: childrenRect.height
|
||||
Layout.fillWidth: true
|
||||
|
||||
delegate: RowLayout {
|
||||
id: item
|
||||
property var childAttrib: object
|
||||
layoutDirection: Qt.RightToLeft
|
||||
//height: childrenRect.height
|
||||
width: lv.width
|
||||
Component.onCompleted: {
|
||||
var cpt = Qt.createComponent("AttributeItemDelegate.qml")
|
||||
var obj = cpt.createObject(item, {'attribute': Qt.binding(function() { return item.childAttrib })})
|
||||
obj.Layout.fillWidth = true
|
||||
}
|
||||
Button {
|
||||
text: "-"
|
||||
onClicked: _reconstruction.removeAttribute(item.childAttrib)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: groupAttribute_component
|
||||
ListView {
|
||||
id: someview
|
||||
model: attribute.value
|
||||
implicitWidth: parent.width
|
||||
implicitHeight: childrenRect.height
|
||||
onCountChanged: forceLayout()
|
||||
spacing: 4
|
||||
|
||||
delegate: RowLayout {
|
||||
id: row
|
||||
width: someview.width
|
||||
//height: childrenRect.height
|
||||
property var childAttrib: object
|
||||
|
||||
Label { text: childAttrib.name }
|
||||
Component.onCompleted: {
|
||||
var cpt = Qt.createComponent("AttributeItemDelegate.qml")
|
||||
var obj = cpt.createObject(row, {'attribute': Qt.binding(function() { return row.childAttrib })})
|
||||
obj.Layout.fillWidth = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
114
meshroom/ui/qml/AttributePin.qml
Executable file
114
meshroom/ui/qml/AttributePin.qml
Executable file
|
@ -0,0 +1,114 @@
|
|||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
|
||||
/**
|
||||
The representation of an Attribute on a Node.
|
||||
*/
|
||||
Row {
|
||||
id: root
|
||||
|
||||
property var nodeItem
|
||||
property var attribute
|
||||
|
||||
// position of the anchor for attaching and edge to this attribute pin
|
||||
readonly property point edgeAnchorPos: Qt.point(edgeAnchor.x + edgeAnchor.width/2,
|
||||
edgeAnchor.y + edgeAnchor.height/2)
|
||||
|
||||
objectName: attribute.name + "."
|
||||
layoutDirection: attribute.isOutput ? Qt.RightToLeft : Qt.LeftToRight
|
||||
spacing: 1
|
||||
|
||||
Rectangle {
|
||||
id: edgeAnchor
|
||||
|
||||
width: 6
|
||||
height: width
|
||||
radius: width/2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
border.color: "#3e3e3e"
|
||||
color: (dropArea.containsDrag && dropArea.containsDrag) || attribute.isLink ? "#3e3e3e" : "white"
|
||||
|
||||
DropArea {
|
||||
id: dropArea
|
||||
|
||||
property bool acceptableDrop: false
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
onEntered: {
|
||||
// Filter drops:
|
||||
if( drag.source.objectName != dragTarget.objectName // not an edge connector
|
||||
|| drag.source.nodeItem == dragTarget.nodeItem // connection between attributes of the same node
|
||||
|| (dragTarget.isOutput) // connection on an output
|
||||
|| dragTarget.attribute.isLink) // already connected attribute
|
||||
{
|
||||
drag.accepted = false
|
||||
}
|
||||
dropArea.acceptableDrop = drag.accepted
|
||||
}
|
||||
onExited: acceptableDrop = false
|
||||
|
||||
onDropped: {
|
||||
_reconstruction.addEdge(drag.source.attribute, dragTarget.attribute)
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: dragTarget
|
||||
objectName: "edgeConnector"
|
||||
readonly property alias attribute: root.attribute
|
||||
readonly property alias nodeItem: root.nodeItem
|
||||
readonly property bool isOutput: attribute.isOutput
|
||||
anchors.centerIn: root.state == "Dragging" ? undefined : parent
|
||||
//anchors.verticalCenter: root.verticalCenter
|
||||
width: 2
|
||||
height: 2
|
||||
Drag.active: connectMA.drag.active
|
||||
Drag.hotSpot.x: width*0.5
|
||||
Drag.hotSpot.y: height*0.5
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: connectMA
|
||||
drag.target: dragTarget
|
||||
drag.threshold: 0
|
||||
anchors.fill: parent
|
||||
onReleased: dragTarget.Drag.drop()
|
||||
}
|
||||
|
||||
Edge {
|
||||
id: connectEdge
|
||||
visible: false
|
||||
point1x: parent.width / 2
|
||||
point1y: parent.width / 2
|
||||
point2x: dragTarget.x + dragTarget.width/2
|
||||
point2y: dragTarget.y + dragTarget.height/2
|
||||
}
|
||||
}
|
||||
|
||||
// Attribute name
|
||||
Label {
|
||||
text: attribute.name
|
||||
font.pointSize: 5
|
||||
color: "#333" // TODO: style
|
||||
}
|
||||
|
||||
state: connectMA.pressed ? "Dragging" : ""
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: ""
|
||||
},
|
||||
|
||||
State {
|
||||
name: "Dragging"
|
||||
PropertyChanges {
|
||||
target: connectEdge
|
||||
z: 100
|
||||
visible: true
|
||||
}
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
}
|
65
meshroom/ui/qml/Edge.qml
Normal file
65
meshroom/ui/qml/Edge.qml
Normal file
|
@ -0,0 +1,65 @@
|
|||
import QtQuick 2.9
|
||||
import GraphEditor 1.0
|
||||
import QtQuick.Shapes 1.0
|
||||
|
||||
/**
|
||||
A cubic spline representing an edge, going from point1 to point2, providing mouse interaction.
|
||||
*/
|
||||
Shape {
|
||||
id: root
|
||||
|
||||
property var edge
|
||||
property real point1x
|
||||
property real point1y
|
||||
property real point2x
|
||||
property real point2y
|
||||
property alias thickness: path.strokeWidth
|
||||
property color color
|
||||
|
||||
signal pressed(var event)
|
||||
signal released(var event)
|
||||
|
||||
x: point1x
|
||||
y: point1y
|
||||
width: point2x - point1x
|
||||
height: point2y - point1y
|
||||
|
||||
property real startX: 0
|
||||
property real startY: 0
|
||||
property real endX: width
|
||||
property real endY: height
|
||||
|
||||
// cause rendering artifacts when enabled (and don't support hot reload really well)
|
||||
vendorExtensionsEnabled: false
|
||||
|
||||
ShapePath {
|
||||
id: path
|
||||
startX: root.startX
|
||||
startY: root.startY
|
||||
fillColor: "transparent"
|
||||
// BUG: edgeArea is destroyed before path, need to test if not null to avoid warnings
|
||||
strokeColor: edgeArea && edgeArea.containsMouse ? "#E91E63" : "#3E3E3E"
|
||||
capStyle: ShapePath.RoundCap
|
||||
strokeWidth: 1
|
||||
|
||||
PathCubic {
|
||||
id: cubic
|
||||
property real curveScale: 0.7
|
||||
property real ctrlPtDist: Math.abs(root.width * curveScale)
|
||||
x: root.endX
|
||||
y: root.endY
|
||||
relativeControl1X: ctrlPtDist; relativeControl1Y: 0
|
||||
control2X: x - ctrlPtDist; control2Y: y
|
||||
}
|
||||
}
|
||||
|
||||
EdgeMouseArea {
|
||||
id: edgeArea
|
||||
anchors.fill: parent
|
||||
curveScale: cubic.curveScale
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
thickness: root.thickness + 2
|
||||
onPressed: root.pressed(arguments[0]) // can't get named args, use arguments array
|
||||
onReleased: root.released(arguments[0])
|
||||
}
|
||||
}
|
198
meshroom/ui/qml/GraphEditor.qml
Executable file
198
meshroom/ui/qml/GraphEditor.qml
Executable file
|
@ -0,0 +1,198 @@
|
|||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.0
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
/**
|
||||
A component displaying a Graph (nodes, attributes and edges).
|
||||
*/
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property variant graph: null
|
||||
|
||||
property variant selectedNode: null
|
||||
|
||||
property int nodeWidth: 140
|
||||
property int nodeHeight: 40
|
||||
property int gridSpacing: 10
|
||||
property var _attributeToDelegate: ({})
|
||||
|
||||
// signals
|
||||
signal workspaceMoved()
|
||||
signal workspaceClicked()
|
||||
|
||||
clip: true
|
||||
// Activate multisampling for edges antialiasing
|
||||
layer.enabled: true
|
||||
layer.samples: 8
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
property double factor: 1.15
|
||||
|
||||
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
|
||||
workspaceClicked()
|
||||
}
|
||||
onPositionChanged: {
|
||||
if(drag.active)
|
||||
workspaceMoved()
|
||||
}
|
||||
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
|
||||
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)
|
||||
_reconstruction.removeEdge(edge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nodes
|
||||
Repeater {
|
||||
id: nodeRepeater
|
||||
|
||||
model: root.graph.nodes
|
||||
property bool loaded: count === model.count
|
||||
onLoadedChanged: if(loaded) { doAutoLayout() }
|
||||
|
||||
delegate: Node {
|
||||
node: object
|
||||
width: root.nodeWidth
|
||||
height: Math.max(root.nodeHeight, implicitHeight)
|
||||
radius: 1
|
||||
border.color: root.selectedNode == node ? Qt.darker(color, 1.8) : Qt.darker(color, 1.1)
|
||||
|
||||
onAttributePinCreated: root._attributeToDelegate[attribute] = pin
|
||||
|
||||
onPressed: {
|
||||
root.selectedNode = object
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {}
|
||||
}
|
||||
Behavior on y {
|
||||
NumberAnimation {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
Button {
|
||||
text: "Fit"
|
||||
onClicked: root.fit()
|
||||
z: 10
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "AutoLayout"
|
||||
onClicked: root.doAutoLayout()
|
||||
z: 10
|
||||
}
|
||||
}
|
||||
|
||||
// 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 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.depth][j] == undefined)
|
||||
{
|
||||
grid[obj.node.depth][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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
138
meshroom/ui/qml/Node.qml
Executable file
138
meshroom/ui/qml/Node.qml
Executable file
|
@ -0,0 +1,138 @@
|
|||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
property variant node: object
|
||||
property color baseColor: "#607D8B"
|
||||
|
||||
signal pressed(var mouse)
|
||||
signal attributePinCreated(var attribute, var pin)
|
||||
|
||||
implicitHeight: body.height + 4
|
||||
|
||||
opacity: 0.9
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
drag.target: parent
|
||||
drag.threshold: 0
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onPressed: {
|
||||
if(mouse.button == Qt.RightButton)
|
||||
nodeMenu.popup()
|
||||
root.pressed(mouse)
|
||||
}
|
||||
|
||||
Menu {
|
||||
id: nodeMenu
|
||||
MenuItem {
|
||||
text: "Compute"
|
||||
onTriggered: _reconstruction.execute(node)
|
||||
}
|
||||
MenuItem {
|
||||
text: "Open Folder"
|
||||
onTriggered: Qt.openUrlExternally(node.internalFolder)
|
||||
}
|
||||
MenuSeparator {}
|
||||
MenuItem {
|
||||
text: "Delete"
|
||||
onTriggered: _reconstruction.removeNode(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: body
|
||||
width: parent.width
|
||||
|
||||
Label {
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
padding: 2
|
||||
text: node.nodeType
|
||||
color: "#EEE"
|
||||
font.pointSize: 8
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
width: parent.width + 6
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Column {
|
||||
id: inputs
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Repeater {
|
||||
model: node.attributes
|
||||
delegate: Loader {
|
||||
active: !object.isOutput && object.type == "File" // TODO: review this
|
||||
|
||||
sourceComponent: AttributePin {
|
||||
id: inPin
|
||||
nodeItem: root
|
||||
attribute: object
|
||||
Component.onCompleted: attributePinCreated(object, inPin)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Column {
|
||||
id: outputs
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
anchors.right: parent.right
|
||||
Repeater {
|
||||
model: node.attributes
|
||||
|
||||
delegate: Loader {
|
||||
active: object.isOutput
|
||||
anchors.right: parent.right
|
||||
|
||||
sourceComponent: AttributePin {
|
||||
id: outPin
|
||||
nodeItem: root
|
||||
attribute: object
|
||||
Component.onCompleted: attributePinCreated(object, outPin)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateGroup {
|
||||
id: status
|
||||
|
||||
state: node.statusName
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "NONE"
|
||||
PropertyChanges { target: root; color: baseColor}
|
||||
},
|
||||
State {
|
||||
name: "SUBMITTED_EXTERN"
|
||||
PropertyChanges { target: root; color: "#2196F3"}
|
||||
},
|
||||
State {
|
||||
name: "SUBMITTED_LOCAL"
|
||||
PropertyChanges { target: root; color: "#009688"}
|
||||
},
|
||||
State {
|
||||
name: "RUNNING"
|
||||
PropertyChanges { target: root; color: "#FF9800"}
|
||||
},
|
||||
State {
|
||||
name: "ERROR"
|
||||
PropertyChanges { target: root; color: "#F44336"}
|
||||
},
|
||||
State {
|
||||
name: "SUCCESS"
|
||||
PropertyChanges { target: root; color: "#4CAF50"}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
308
meshroom/ui/qml/main.qml
Normal file → Executable file
308
meshroom/ui/qml/main.qml
Normal file → Executable file
|
@ -1,7 +1,9 @@
|
|||
import QtQuick 2.5
|
||||
import QtQuick.Controls 1.4 // as Controls1
|
||||
//import QtQuick.Controls 2.2
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQuick.Window 2.3
|
||||
import QtQml.Models 2.2
|
||||
import Qt.labs.platform 1.0 as Platform
|
||||
|
||||
ApplicationWindow {
|
||||
id: _window
|
||||
|
@ -9,129 +11,221 @@ ApplicationWindow {
|
|||
width: 1280
|
||||
height: 720
|
||||
visible: true
|
||||
title: "Meshroom"
|
||||
color: "#fafafa"
|
||||
title: (_reconstruction.filepath ? _reconstruction.filepath : "Untitled") + (_reconstruction.undoStack.clean ? "" : "*") + " - Meshroom"
|
||||
font.pointSize: 10
|
||||
|
||||
property variant node: null
|
||||
|
||||
Connections {
|
||||
target: _reconstruction.undoStack
|
||||
onIndexChanged: graphStr.update()
|
||||
onClosing: {
|
||||
// make sure document is saved before exiting application
|
||||
close.accepted = false
|
||||
ensureSaved(function(){ Qt.quit() })
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: unsavedDialog
|
||||
|
||||
property var _callback: undefined
|
||||
|
||||
title: "Unsaved Document"
|
||||
modal: true
|
||||
x: parent.width/2 - width/2
|
||||
y: parent.height/2 - height/2
|
||||
standardButtons: Dialog.Save | Dialog.Cancel | Dialog.Discard
|
||||
|
||||
onDiscarded: {
|
||||
close() // BUG ? discard does not close window
|
||||
fireCallback()
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
// save current file
|
||||
if(saveAction.enabled)
|
||||
{
|
||||
saveAction.trigger()
|
||||
fireCallback()
|
||||
}
|
||||
// open "save as" dialog
|
||||
else
|
||||
{
|
||||
saveFileDialog.open()
|
||||
function _callbackWrapper(rc) {
|
||||
if(rc == Platform.Dialog.Accepted)
|
||||
fireCallback()
|
||||
saveFileDialog.closed.disconnect(_callbackWrapper)
|
||||
}
|
||||
saveFileDialog.closed.connect(_callbackWrapper)
|
||||
}
|
||||
}
|
||||
|
||||
function fireCallback()
|
||||
{
|
||||
// call the callback and reset it
|
||||
if(_callback)
|
||||
_callback()
|
||||
_callback = undefined
|
||||
}
|
||||
|
||||
/// Open the unsaved dialog warning with an optional
|
||||
/// callback to fire when the dialog is accepted/discarded
|
||||
function prompt(callback)
|
||||
{
|
||||
_callback = callback
|
||||
open()
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "Your current Graph is not saved"
|
||||
}
|
||||
}
|
||||
|
||||
Platform.FileDialog {
|
||||
id: saveFileDialog
|
||||
|
||||
signal closed(var result)
|
||||
|
||||
title: "Save File"
|
||||
nameFilters: ["Meshroom Graphs (*.mg)"]
|
||||
defaultSuffix: ".mg"
|
||||
fileMode: Platform.FileDialog.SaveFile
|
||||
onAccepted: {
|
||||
_reconstruction.saveAs(file)
|
||||
closed(Platform.Dialog.Accepted)
|
||||
}
|
||||
onRejected: closed(Platform.Dialog.Rejected)
|
||||
}
|
||||
|
||||
Platform.FileDialog {
|
||||
id: openFileDialog
|
||||
title: "Open File"
|
||||
nameFilters: ["Meshroom Graphs (*.mg)"]
|
||||
onAccepted: {
|
||||
_reconstruction.loadUrl(file.toString())
|
||||
graphEditor.doAutoLayout()
|
||||
}
|
||||
}
|
||||
|
||||
// Check if document has been saved
|
||||
function ensureSaved(callback)
|
||||
{
|
||||
var saved = _reconstruction.undoStack.clean
|
||||
// If current document is modified, open "unsaved dialog"
|
||||
if(!saved)
|
||||
{
|
||||
unsavedDialog.prompt(callback)
|
||||
}
|
||||
else // otherwise, directly call the callback
|
||||
{
|
||||
callback()
|
||||
}
|
||||
return saved
|
||||
}
|
||||
|
||||
Action {
|
||||
id: undoAction
|
||||
|
||||
property string tooltip: 'Undo "' +_reconstruction.undoStack.undoText +'"'
|
||||
text: "Undo"
|
||||
shortcut: "Ctrl+Z"
|
||||
enabled: _reconstruction.undoStack.canUndo
|
||||
onTriggered: _reconstruction.undoStack.undo()
|
||||
}
|
||||
Action {
|
||||
id: redoAction
|
||||
|
||||
property string tooltip: 'Redo "' +_reconstruction.undoStack.redoText +'"'
|
||||
text: "Redo"
|
||||
shortcut: "Ctrl+Shift+Z"
|
||||
enabled: _reconstruction.undoStack.canRedo
|
||||
onTriggered: _reconstruction.undoStack.redo()
|
||||
}
|
||||
|
||||
header: MenuBar {
|
||||
Menu {
|
||||
title: "File"
|
||||
Action {
|
||||
text: "New"
|
||||
onTriggered: ensureSaved(function() { _reconstruction.new(); graphEditor.doAutoLayout() })
|
||||
}
|
||||
Action {
|
||||
text: "Open"
|
||||
shortcut: "Ctrl+O"
|
||||
onTriggered: ensureSaved(function() { openFileDialog.open() })
|
||||
}
|
||||
Action {
|
||||
id: saveAction
|
||||
text: "Save"
|
||||
shortcut: "Ctrl+S"
|
||||
enabled: _reconstruction.filepath != "" && !_reconstruction.undoStack.clean
|
||||
onTriggered: _reconstruction.save()
|
||||
}
|
||||
Action {
|
||||
id: saveAsAction
|
||||
text: "Save As..."
|
||||
shortcut: "Ctrl+Shift+S"
|
||||
onTriggered: saveFileDialog.open()
|
||||
}
|
||||
MenuSeparator { }
|
||||
Action {
|
||||
text: "Quit"
|
||||
onTriggered: Qt.quit()
|
||||
}
|
||||
}
|
||||
Menu {
|
||||
title: "Edit"
|
||||
MenuItem {
|
||||
action: undoAction
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: undoAction.tooltip
|
||||
}
|
||||
MenuItem {
|
||||
action: redoAction
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: redoAction.tooltip
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
Row
|
||||
{
|
||||
Row {
|
||||
spacing: 1
|
||||
Layout.fillWidth: true
|
||||
TextField {
|
||||
id: filepath
|
||||
width: 200
|
||||
}
|
||||
Button {
|
||||
text: "Load"
|
||||
onClicked: _reconstruction.graph.load(filepath.text)
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Add Node"
|
||||
onClicked: {
|
||||
_reconstruction.addNode("FeatureExtraction")
|
||||
}
|
||||
}
|
||||
|
||||
Item {width: 4; height: 1}
|
||||
|
||||
Button {
|
||||
text: "Undo"
|
||||
activeFocusOnPress: true
|
||||
enabled: _reconstruction.undoStack.canUndo
|
||||
tooltip: 'Undo "' +_reconstruction.undoStack.undoText +'"'
|
||||
onClicked: {
|
||||
_reconstruction.undoStack.undo()
|
||||
}
|
||||
text: "Execute"
|
||||
enabled: _reconstruction.graph.nodes.count && !_reconstruction.computing
|
||||
onClicked: _reconstruction.execute(null)
|
||||
}
|
||||
Button {
|
||||
text: "Redo"
|
||||
activeFocusOnPress: true
|
||||
enabled: _reconstruction.undoStack.canRedo
|
||||
tooltip: 'Redo "' +_reconstruction.undoStack.redoText +'"'
|
||||
onClicked: {
|
||||
_reconstruction.undoStack.redo()
|
||||
}
|
||||
text: "Stop"
|
||||
enabled: _reconstruction.computing
|
||||
onClicked: _reconstruction.stopExecution()
|
||||
}
|
||||
}
|
||||
GraphEditor {
|
||||
id: graphEditor
|
||||
graph: _reconstruction.graph
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: parent.height * 0.3
|
||||
Layout.margins: 10
|
||||
}
|
||||
|
||||
RowLayout{
|
||||
Loader {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
ListView {
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: 150
|
||||
model: _reconstruction.graph.nodes
|
||||
onCountChanged: {
|
||||
graphStr.update()
|
||||
}
|
||||
spacing: 2
|
||||
delegate: Rectangle {
|
||||
width: 130
|
||||
height: 40
|
||||
Label {
|
||||
text: object.name
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.AllButtons
|
||||
onClicked: {
|
||||
if(mouse.button == Qt.RightButton)
|
||||
_reconstruction.removeNode(object)
|
||||
else
|
||||
_window.node = object
|
||||
}
|
||||
|
||||
}
|
||||
color: "#81d4fa"
|
||||
border.color: _window.node == object ? Qt.darker(color) : "transparent"
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: attributesListView
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
model: _window.node != null ? _window.node.attributes : null
|
||||
delegate: RowLayout {
|
||||
width: attributesListView.width
|
||||
spacing: 4
|
||||
Label {
|
||||
text: object.label
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Layout.preferredWidth: 200
|
||||
}
|
||||
TextField {
|
||||
text: object.value
|
||||
Layout.fillWidth: true
|
||||
onEditingFinished: _reconstruction.setAttribute(object, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TextArea {
|
||||
id: graphStr
|
||||
Layout.preferredWidth: 400
|
||||
Layout.fillHeight: true
|
||||
wrapMode: TextEdit.WrapAnywhere
|
||||
selectByMouse: true
|
||||
readOnly: true
|
||||
function update() {
|
||||
graphStr.text = _reconstruction.graph.asString()
|
||||
active: graphEditor.selectedNode != null
|
||||
sourceComponent: Component {
|
||||
AttributeEditor {
|
||||
node: graphEditor.selectedNode
|
||||
// Disable editor when computing
|
||||
enabled: !_reconstruction.computing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
65
meshroom/ui/reconstruction.py
Normal file → Executable file
65
meshroom/ui/reconstruction.py
Normal file → Executable file
|
@ -1,18 +1,45 @@
|
|||
import os
|
||||
from threading import Thread
|
||||
|
||||
from PySide2.QtCore import QObject, Slot, Property, Signal
|
||||
from PySide2.QtCore import QObject, Slot, Property, Signal, QJsonValue, QUrl
|
||||
|
||||
from meshroom.core import graph
|
||||
from meshroom import multiview
|
||||
from meshroom.core import graph, defaultCacheFolder, cacheFolderName
|
||||
from meshroom.ui import commands
|
||||
|
||||
|
||||
class Reconstruction(QObject):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, graphFilepath="", parent=None):
|
||||
super(Reconstruction, self).__init__(parent)
|
||||
self._graph = graph.Graph("")
|
||||
self._graph = None
|
||||
self._undoStack = commands.UndoStack(self)
|
||||
self._computeThread = Thread()
|
||||
self._filepath = graphFilepath
|
||||
if self._filepath:
|
||||
self.load(self._filepath)
|
||||
else:
|
||||
self.new()
|
||||
|
||||
@Slot()
|
||||
def new(self):
|
||||
self.clear()
|
||||
self._graph = multiview.photogrammetryPipeline()
|
||||
self._graph.cacheDir = defaultCacheFolder
|
||||
self.graphChanged.emit()
|
||||
|
||||
def clear(self):
|
||||
if self._graph:
|
||||
self._graph.deleteLater()
|
||||
self._graph = None
|
||||
self.setFilepath("")
|
||||
self._undoStack.clear()
|
||||
|
||||
def setFilepath(self, path):
|
||||
if self._filepath == path:
|
||||
return
|
||||
self._filepath = path
|
||||
self.filepathChanged.emit()
|
||||
|
||||
@Slot(str)
|
||||
def addNode(self, nodeType):
|
||||
|
@ -42,11 +69,27 @@ class Reconstruction(QObject):
|
|||
def removeAttribute(self, attribute):
|
||||
self._undoStack.tryAndPush(commands.ListAttributeRemoveCommand(self._graph, attribute))
|
||||
|
||||
@Slot(str)
|
||||
def load(self, filepath):
|
||||
self.clear()
|
||||
self._graph = graph.Graph("")
|
||||
self._graph.load(filepath)
|
||||
self._graph.update()
|
||||
self._undoStack.clear()
|
||||
self.setFilepath(filepath)
|
||||
self.graphChanged.emit()
|
||||
|
||||
@Slot(QUrl)
|
||||
def loadUrl(self, url):
|
||||
self.load(url.toLocalFile())
|
||||
|
||||
@Slot(QUrl)
|
||||
def saveAs(self, url):
|
||||
self.setFilepath(url.toLocalFile())
|
||||
self.save()
|
||||
|
||||
@Slot()
|
||||
def save(self):
|
||||
self._graph.save(self._filepath)
|
||||
self._graph.cacheDir = os.path.join(os.path.dirname(self._filepath), cacheFolderName)
|
||||
self._undoStack.setClean()
|
||||
|
||||
@Slot(graph.Node)
|
||||
def execute(self, node=None):
|
||||
|
@ -70,7 +113,9 @@ class Reconstruction(QObject):
|
|||
self.computingChanged.emit()
|
||||
|
||||
undoStack = Property(QObject, lambda self: self._undoStack, constant=True)
|
||||
graph = Property(graph.Graph, lambda self: self._graph, constant=True)
|
||||
nodes = Property(QObject, lambda self: self._graph.nodes, constant=True)
|
||||
graphChanged = Signal()
|
||||
graph = Property(graph.Graph, lambda self: self._graph, notify=graphChanged)
|
||||
computingChanged = Signal()
|
||||
computing = Property(bool, lambda self: self._computeThread.is_alive(), notify=computingChanged)
|
||||
computing = Property(bool, lambda self: self._computeThread.is_alive(), notify=computingChanged)
|
||||
filepathChanged = Signal()
|
||||
filepath = Property(str, lambda self: self._filepath, notify=filepathChanged)
|
||||
|
|
Loading…
Add table
Reference in a new issue