[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:
Yann Lanthony 2017-10-31 16:53:23 +01:00
parent e683238a8d
commit 00366cda00
8 changed files with 1004 additions and 117 deletions

View 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
}
}
}
}

View 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
View 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
View 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
View 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
View 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"}
}
]
}
}

300
meshroom/ui/qml/main.qml Normal file → Executable file
View 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"
active: graphEditor.selectedNode != null
sourceComponent: Component {
AttributeEditor {
node: graphEditor.selectedNode
// Disable editor when computing
enabled: !_reconstruction.computing
}
}
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()
}
}
}
}
}

63
meshroom/ui/reconstruction.py Normal file → Executable file
View 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)
filepathChanged = Signal()
filepath = Property(str, lambda self: self._filepath, notify=filepathChanged)