mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-04-30 02:37:26 +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"}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
300
meshroom/ui/qml/main.qml
Normal file → Executable file
300
meshroom/ui/qml/main.qml
Normal file → Executable file
|
@ -1,7 +1,9 @@
|
||||||
import QtQuick 2.5
|
import QtQuick 2.7
|
||||||
import QtQuick.Controls 1.4 // as Controls1
|
import QtQuick.Controls 2.3
|
||||||
//import QtQuick.Controls 2.2
|
|
||||||
import QtQuick.Layouts 1.1
|
import QtQuick.Layouts 1.1
|
||||||
|
import QtQuick.Window 2.3
|
||||||
|
import QtQml.Models 2.2
|
||||||
|
import Qt.labs.platform 1.0 as Platform
|
||||||
|
|
||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
id: _window
|
id: _window
|
||||||
|
@ -9,129 +11,221 @@ ApplicationWindow {
|
||||||
width: 1280
|
width: 1280
|
||||||
height: 720
|
height: 720
|
||||||
visible: true
|
visible: true
|
||||||
title: "Meshroom"
|
title: (_reconstruction.filepath ? _reconstruction.filepath : "Untitled") + (_reconstruction.undoStack.clean ? "" : "*") + " - Meshroom"
|
||||||
color: "#fafafa"
|
font.pointSize: 10
|
||||||
|
|
||||||
property variant node: null
|
property variant node: null
|
||||||
|
|
||||||
Connections {
|
onClosing: {
|
||||||
target: _reconstruction.undoStack
|
// make sure document is saved before exiting application
|
||||||
onIndexChanged: graphStr.update()
|
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 {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 4
|
anchors.margins: 4
|
||||||
Row
|
Row {
|
||||||
{
|
spacing: 1
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
TextField {
|
|
||||||
id: filepath
|
|
||||||
width: 200
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
text: "Load"
|
|
||||||
onClicked: _reconstruction.graph.load(filepath.text)
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
text: "Add Node"
|
text: "Execute"
|
||||||
onClicked: {
|
enabled: _reconstruction.graph.nodes.count && !_reconstruction.computing
|
||||||
_reconstruction.addNode("FeatureExtraction")
|
onClicked: _reconstruction.execute(null)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {width: 4; height: 1}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
text: "Undo"
|
|
||||||
activeFocusOnPress: true
|
|
||||||
enabled: _reconstruction.undoStack.canUndo
|
|
||||||
tooltip: 'Undo "' +_reconstruction.undoStack.undoText +'"'
|
|
||||||
onClicked: {
|
|
||||||
_reconstruction.undoStack.undo()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Button {
|
Button {
|
||||||
text: "Redo"
|
text: "Stop"
|
||||||
activeFocusOnPress: true
|
enabled: _reconstruction.computing
|
||||||
enabled: _reconstruction.undoStack.canRedo
|
onClicked: _reconstruction.stopExecution()
|
||||||
tooltip: 'Redo "' +_reconstruction.undoStack.redoText +'"'
|
|
||||||
onClicked: {
|
|
||||||
_reconstruction.undoStack.redo()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
GraphEditor {
|
||||||
|
id: graphEditor
|
||||||
|
graph: _reconstruction.graph
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: parent.height * 0.3
|
||||||
|
Layout.margins: 10
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout{
|
Loader {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
active: graphEditor.selectedNode != null
|
||||||
ListView {
|
sourceComponent: Component {
|
||||||
Layout.fillHeight: true
|
AttributeEditor {
|
||||||
Layout.preferredWidth: 150
|
node: graphEditor.selectedNode
|
||||||
model: _reconstruction.graph.nodes
|
// Disable editor when computing
|
||||||
onCountChanged: {
|
enabled: !_reconstruction.computing
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
63
meshroom/ui/reconstruction.py
Normal file → Executable file
63
meshroom/ui/reconstruction.py
Normal file → Executable file
|
@ -1,18 +1,45 @@
|
||||||
|
import os
|
||||||
from threading import Thread
|
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
|
from meshroom.ui import commands
|
||||||
|
|
||||||
|
|
||||||
class Reconstruction(QObject):
|
class Reconstruction(QObject):
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, graphFilepath="", parent=None):
|
||||||
super(Reconstruction, self).__init__(parent)
|
super(Reconstruction, self).__init__(parent)
|
||||||
self._graph = graph.Graph("")
|
self._graph = None
|
||||||
self._undoStack = commands.UndoStack(self)
|
self._undoStack = commands.UndoStack(self)
|
||||||
self._computeThread = Thread()
|
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)
|
@Slot(str)
|
||||||
def addNode(self, nodeType):
|
def addNode(self, nodeType):
|
||||||
|
@ -42,11 +69,27 @@ class Reconstruction(QObject):
|
||||||
def removeAttribute(self, attribute):
|
def removeAttribute(self, attribute):
|
||||||
self._undoStack.tryAndPush(commands.ListAttributeRemoveCommand(self._graph, attribute))
|
self._undoStack.tryAndPush(commands.ListAttributeRemoveCommand(self._graph, attribute))
|
||||||
|
|
||||||
@Slot(str)
|
|
||||||
def load(self, filepath):
|
def load(self, filepath):
|
||||||
|
self.clear()
|
||||||
|
self._graph = graph.Graph("")
|
||||||
self._graph.load(filepath)
|
self._graph.load(filepath)
|
||||||
self._graph.update()
|
self.setFilepath(filepath)
|
||||||
self._undoStack.clear()
|
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)
|
@Slot(graph.Node)
|
||||||
def execute(self, node=None):
|
def execute(self, node=None):
|
||||||
|
@ -70,7 +113,9 @@ class Reconstruction(QObject):
|
||||||
self.computingChanged.emit()
|
self.computingChanged.emit()
|
||||||
|
|
||||||
undoStack = Property(QObject, lambda self: self._undoStack, constant=True)
|
undoStack = Property(QObject, lambda self: self._undoStack, constant=True)
|
||||||
graph = Property(graph.Graph, lambda self: self._graph, constant=True)
|
graphChanged = Signal()
|
||||||
nodes = Property(QObject, lambda self: self._graph.nodes, constant=True)
|
graph = Property(graph.Graph, lambda self: self._graph, notify=graphChanged)
|
||||||
computingChanged = Signal()
|
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