mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-04-30 02:37:26 +02:00
Double click on a node or an attribute with an image file to display it in the 2D viewer.
663 lines
22 KiB
QML
Executable file
663 lines
22 KiB
QML
Executable file
import QtQuick 2.7
|
|
import QtQuick.Controls 2.3
|
|
import QtQuick.Controls 1.4 as Controls1 // For SplitView
|
|
import QtQuick.Layouts 1.1
|
|
import QtQuick.Window 2.3
|
|
import QtQml.Models 2.2
|
|
import Qt.labs.platform 1.0 as Platform
|
|
import Qt.labs.settings 1.0
|
|
import GraphEditor 1.0
|
|
import MaterialIcons 2.2
|
|
import Utils 1.0
|
|
import Controls 1.0
|
|
|
|
ApplicationWindow {
|
|
id: _window
|
|
|
|
width: settings_General.windowWidth
|
|
height: settings_General.windowHeight
|
|
minimumWidth: 650
|
|
minimumHeight: 500
|
|
visible: true
|
|
|
|
/// Whether graph is currently locked and therefore read-only
|
|
readonly property bool graphLocked: _reconstruction.computing && GraphEditorSettings.lockOnCompute
|
|
|
|
title: {
|
|
var t = _reconstruction.graph.filepath || "Untitled"
|
|
if(!_reconstruction.undoStack.clean)
|
|
t += "*"
|
|
t += " - " + Qt.application.name + " " + Qt.application.version
|
|
return t
|
|
}
|
|
|
|
onClosing: {
|
|
// make sure document is saved before exiting application
|
|
close.accepted = false
|
|
if(!ensureNotComputing())
|
|
return
|
|
ensureSaved(function(){ Qt.quit() })
|
|
}
|
|
|
|
// force Application palette assignation
|
|
// note: should be implicit (PySide bug)
|
|
palette: _PaletteManager.palette
|
|
|
|
SystemPalette { id: activePalette }
|
|
SystemPalette { id: disabledPalette; colorGroup: SystemPalette.Disabled }
|
|
|
|
Settings {
|
|
id: settings_General
|
|
category: 'General'
|
|
property int windowWidth: 1280
|
|
property int windowHeight: 720
|
|
}
|
|
|
|
Settings {
|
|
id: settings_UILayout
|
|
category: 'UILayout'
|
|
property alias showLiveReconstruction: liveSfMVisibilityCB.checked
|
|
property alias showGraphEditor: graphEditorVisibilityCB.checked
|
|
}
|
|
|
|
Component.onDestruction: {
|
|
// store main window dimensions in persisting Settings
|
|
settings_General.windowWidth = _window.width
|
|
settings_General.windowHeight = _window.height
|
|
}
|
|
|
|
MessageDialog {
|
|
id: unsavedDialog
|
|
|
|
property var _callback: undefined
|
|
|
|
title: Filepath.basename(_reconstruction.graph.filepath) || "Unsaved Project"
|
|
preset: "Info"
|
|
canCopy: false
|
|
text: _reconstruction.graph.filepath ? "Current project has unsaved modifications."
|
|
: "Current project has not been saved."
|
|
helperText: _reconstruction.graph.filepath ? "Would you like to save those changes?"
|
|
: "Would you like to save this project?"
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
Item {
|
|
id: computeManager
|
|
|
|
property bool warnIfUnsaved: true
|
|
|
|
// evaluate if global reconstruction computation can be started
|
|
property bool canStartComputation: _reconstruction.viewpoints.count >= 2 // at least 2 images
|
|
&& !_reconstruction.computing // computation is not started
|
|
&& _reconstruction.graph.canComputeLeaves // graph has no uncomputable nodes
|
|
|
|
// evaluate if graph computation can be submitted externally
|
|
property bool canSubmit: _reconstruction.canSubmit // current setup allows to compute externally
|
|
&& canStartComputation // can be computed
|
|
&& _reconstruction.graph.filepath // graph is saved on disk
|
|
|
|
function compute(node, force) {
|
|
if(!force && warnIfUnsaved && !_reconstruction.graph.filepath)
|
|
{
|
|
unsavedComputeDialog.currentNode = node;
|
|
unsavedComputeDialog.open();
|
|
}
|
|
else
|
|
_reconstruction.execute(node);
|
|
}
|
|
|
|
function submit(node) {
|
|
_reconstruction.submit(node);
|
|
}
|
|
|
|
|
|
MessageDialog {
|
|
id: unsavedComputeDialog
|
|
|
|
property var currentNode: null
|
|
|
|
canCopy: false
|
|
icon.text: MaterialIcons.warning
|
|
parent: Overlay.overlay
|
|
preset: "Warning"
|
|
title: "Unsaved Project"
|
|
text: "Data will be computed in the default cache folder if project remains unsaved."
|
|
detailedText: "Default cache folder: " + _reconstruction.graph.cacheDir
|
|
helperText: "Save project first?"
|
|
standardButtons: Dialog.Discard | Dialog.Cancel | Dialog.Save
|
|
|
|
CheckBox {
|
|
Layout.alignment: Qt.AlignRight
|
|
text: "Don't ask again for this session"
|
|
padding: 0
|
|
onToggled: computeManager.warnIfUnsaved = !checked
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
// set up discard button text
|
|
standardButton(Dialog.Discard).text = "Continue without Saving"
|
|
}
|
|
|
|
onDiscarded: { close(); computeManager.compute(currentNode, true) }
|
|
onAccepted: saveAsAction.trigger()
|
|
}
|
|
}
|
|
|
|
Platform.FileDialog {
|
|
id: openFileDialog
|
|
title: "Open File"
|
|
nameFilters: ["Meshroom Graphs (*.mg)"]
|
|
onAccepted: {
|
|
_reconstruction.loadUrl(file.toString())
|
|
}
|
|
}
|
|
|
|
AboutDialog {
|
|
id: aboutDialog
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
MessageDialog {
|
|
id: computingAtExitDialog
|
|
title: "Operation in progress"
|
|
modal: true
|
|
canCopy: false
|
|
Label {
|
|
text: "Please stop any local computation before exiting Meshroom"
|
|
}
|
|
}
|
|
|
|
// Check and return whether no local computation is in progress
|
|
function ensureNotComputing()
|
|
{
|
|
if(_reconstruction.computingLocally)
|
|
{
|
|
// Open a warning dialog to ask for computation to be stopped
|
|
computingAtExitDialog.open()
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
MessageDialog {
|
|
// Popup displayed while the application
|
|
// is busy building intrinsics while importing images
|
|
id: buildingIntrinsicsDialog
|
|
modal: true
|
|
visible: _reconstruction.buildingIntrinsics
|
|
closePolicy: Popup.NoAutoClose
|
|
title: "Initializing Cameras"
|
|
icon.text: MaterialIcons.camera
|
|
icon.font.pointSize: 10
|
|
canCopy: false
|
|
standardButtons: Dialog.NoButton
|
|
|
|
detailedText: "Extracting images metadata and creating Camera intrinsics..."
|
|
ProgressBar {
|
|
indeterminate: true
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
|
|
DialogsFactory {
|
|
id: dialogsFactory
|
|
}
|
|
|
|
CompatibilityManager {
|
|
id: compatibilityManager
|
|
uigraph: _reconstruction
|
|
}
|
|
|
|
Action {
|
|
id: undoAction
|
|
|
|
property string tooltip: 'Undo "' +_reconstruction.undoStack.undoText +'"'
|
|
text: "Undo"
|
|
shortcut: "Ctrl+Z"
|
|
enabled: _reconstruction.undoStack.canUndo && !graphLocked
|
|
onTriggered: _reconstruction.undoStack.undo()
|
|
}
|
|
Action {
|
|
id: redoAction
|
|
|
|
property string tooltip: 'Redo "' +_reconstruction.undoStack.redoText +'"'
|
|
text: "Redo"
|
|
shortcut: "Ctrl+Shift+Z"
|
|
enabled: _reconstruction.undoStack.canRedo && !graphLocked
|
|
onTriggered: _reconstruction.undoStack.redo()
|
|
}
|
|
|
|
Action {
|
|
shortcut: "Ctrl+Shift+P"
|
|
onTriggered: _PaletteManager.togglePalette()
|
|
}
|
|
|
|
header: MenuBar {
|
|
palette.window: Qt.darker(activePalette.window, 1.15)
|
|
Menu {
|
|
title: "File"
|
|
Action {
|
|
text: "New"
|
|
shortcut: "Ctrl+N"
|
|
onTriggered: ensureSaved(function() { _reconstruction.new() })
|
|
}
|
|
Action {
|
|
text: "Open"
|
|
shortcut: "Ctrl+O"
|
|
onTriggered: ensureSaved(function() { openFileDialog.open() })
|
|
}
|
|
Action {
|
|
id: saveAction
|
|
text: "Save"
|
|
shortcut: "Ctrl+S"
|
|
enabled: !_reconstruction.graph.filepath || !_reconstruction.undoStack.clean
|
|
onTriggered: _reconstruction.graph.filepath ? _reconstruction.save() : saveFileDialog.open()
|
|
}
|
|
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
|
|
}
|
|
}
|
|
Menu {
|
|
title: "View"
|
|
MenuItem {
|
|
id: graphEditorVisibilityCB
|
|
text: "Graph Editor"
|
|
checkable: true
|
|
checked: true
|
|
}
|
|
MenuItem {
|
|
id: liveSfMVisibilityCB
|
|
text: "Live Reconstruction"
|
|
checkable: true
|
|
checked: false
|
|
}
|
|
MenuSeparator {}
|
|
Action {
|
|
text: "Fullscreen"
|
|
checkable: true
|
|
checked: _window.visibility == ApplicationWindow.FullScreen
|
|
shortcut: "Ctrl+F"
|
|
onTriggered: _window.visibility == ApplicationWindow.FullScreen ? _window.showNormal() : showFullScreen()
|
|
}
|
|
}
|
|
Menu {
|
|
title: "Help"
|
|
Action {
|
|
text: "About Meshroom"
|
|
onTriggered: aboutDialog.open()
|
|
// shoud be StandardKey.HelpContents, but for some reason it's not stable
|
|
// (may cause crash, requires pressing F1 twice after closing the popup)
|
|
shortcut: "F1"
|
|
}
|
|
}
|
|
}
|
|
|
|
footer: ToolBar {
|
|
id: footer
|
|
padding: 1
|
|
leftPadding: 4
|
|
rightPadding: 4
|
|
palette.window: Qt.darker(activePalette.window, 1.15)
|
|
|
|
// Cache Folder
|
|
RowLayout {
|
|
spacing: 0
|
|
MaterialToolButton {
|
|
font.pointSize: 8
|
|
text: MaterialIcons.folder_open
|
|
ToolTip.text: "Open Cache Folder"
|
|
onClicked: Qt.openUrlExternally(Filepath.stringToUrl(_reconstruction.graph.cacheDir))
|
|
}
|
|
|
|
TextField {
|
|
readOnly: true
|
|
selectByMouse: true
|
|
text: _reconstruction.graph.cacheDir
|
|
color: Qt.darker(palette.text, 1.2)
|
|
background: Item {}
|
|
}
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: _reconstruction
|
|
|
|
// Bind messages to DialogsFactory
|
|
function createDialog(func, message)
|
|
{
|
|
var dialog = func(_window)
|
|
// Set text afterwards to avoid dialog sizing issues
|
|
dialog.title = message.title
|
|
dialog.text = message.text
|
|
dialog.detailedText = message.detailedText
|
|
}
|
|
|
|
onGraphChanged: {
|
|
// open CompatibilityManager after file loading if any issue is detected
|
|
if(compatibilityManager.issueCount)
|
|
compatibilityManager.open()
|
|
// trigger fit to visualize all nodes
|
|
graphEditor.fit()
|
|
}
|
|
|
|
onInfo: createDialog(dialogsFactory.info, arguments[0])
|
|
onWarning: createDialog(dialogsFactory.warning, arguments[0])
|
|
onError: createDialog(dialogsFactory.error, arguments[0])
|
|
}
|
|
|
|
|
|
Controls1.SplitView {
|
|
anchors.fill: parent
|
|
orientation: Qt.Vertical
|
|
|
|
// Setup global tooltip style
|
|
ToolTip.toolTip.background: Rectangle { color: activePalette.base; border.color: activePalette.mid }
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
Layout.topMargin: 2
|
|
implicitHeight: Math.round(parent.height * 0.7)
|
|
spacing: 4
|
|
RowLayout {
|
|
Layout.rightMargin: 4
|
|
Layout.leftMargin: 4
|
|
Layout.fillHeight: false
|
|
Item { Layout.fillWidth: true }
|
|
|
|
Row {
|
|
// disable controls if graph is executed externally
|
|
enabled: !_reconstruction.computingExternally
|
|
Layout.alignment: Qt.AlignHCenter
|
|
|
|
Button {
|
|
property color buttonColor: Qt.darker("#4CAF50", 1.8)
|
|
text: "Start"
|
|
palette.button: enabled ? buttonColor : disabledPalette.button
|
|
palette.window: enabled ? buttonColor : disabledPalette.window
|
|
palette.buttonText: enabled ? "white" : disabledPalette.buttonText
|
|
enabled: computeManager.canStartComputation
|
|
onClicked: computeManager.compute(null)
|
|
}
|
|
Button {
|
|
text: "Stop"
|
|
enabled: _reconstruction.computingLocally
|
|
onClicked: _reconstruction.stopExecution()
|
|
}
|
|
Item { width: 20; height: 1 }
|
|
Button {
|
|
visible: _reconstruction.canSubmit
|
|
enabled: computeManager.canSubmit
|
|
text: "Submit"
|
|
onClicked: computeManager.submit(null)
|
|
}
|
|
}
|
|
Item { Layout.fillWidth: true; Layout.fillHeight: true }
|
|
|
|
// CompatibilityManager indicator
|
|
ToolButton {
|
|
visible: compatibilityManager.issueCount
|
|
text: MaterialIcons.warning
|
|
font.family: MaterialIcons.fontFamily
|
|
palette.buttonText: "#FF9800"
|
|
font.pointSize: 12
|
|
onClicked: compatibilityManager.open()
|
|
ToolTip.text: "Compatibility Issues"
|
|
ToolTip.visible: hovered
|
|
}
|
|
}
|
|
|
|
Label {
|
|
text: "Graph is being computed externally"
|
|
font.italic: true
|
|
Layout.alignment: Qt.AlignHCenter
|
|
visible: _reconstruction.computingExternally
|
|
}
|
|
|
|
// "ProgressBar" reflecting status of all the chunks in the graph, in their process order
|
|
NodeChunks {
|
|
id: chunksListView
|
|
Layout.fillWidth: true
|
|
height: 6
|
|
model: _reconstruction.sortedDFSChunks
|
|
}
|
|
|
|
WorkspaceView {
|
|
id: workspaceView
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
Layout.minimumHeight: 50
|
|
reconstruction: _reconstruction
|
|
readOnly: _reconstruction.computing
|
|
|
|
function viewIn3D(attribute, mouse) {
|
|
var loaded = viewer3D.view(attribute);
|
|
// solo media if Control modifier was held
|
|
if(loaded && mouse && mouse.modifiers & Qt.ControlModifier)
|
|
viewer3D.solo(attribute);
|
|
return loaded;
|
|
}
|
|
function viewIn2D(attribute) {
|
|
var imageExts = ['.exr', '.jpg', '.tif', '.png'];
|
|
var ext = Filepath.extension(attribute.value);
|
|
if(imageExts.indexOf(ext) == -1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if(attribute.value.includes('*'))
|
|
{
|
|
// For now, the viewer only supports a single image.
|
|
var firstFile = Filepath.globFirst(attribute.value)
|
|
viewer2D.source = Filepath.stringToUrl(firstFile);
|
|
}
|
|
else
|
|
{
|
|
viewer2D.source = Filepath.stringToUrl(attribute.value);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
Controls1.SplitView {
|
|
orientation: Qt.Horizontal
|
|
width: parent.width
|
|
height: Math.round(parent.height * 0.3)
|
|
visible: settings_UILayout.showGraphEditor
|
|
|
|
Panel {
|
|
id: graphEditorPanel
|
|
Layout.fillWidth: true
|
|
padding: 4
|
|
title: "Graph Editor"
|
|
|
|
headerBar: RowLayout {
|
|
MaterialToolButton {
|
|
text: MaterialIcons.more_vert
|
|
font.pointSize: 11
|
|
padding: 2
|
|
onClicked: graphEditorMenu.open()
|
|
checkable: true
|
|
checked: graphEditorMenu.visible
|
|
Menu {
|
|
id: graphEditorMenu
|
|
y: parent.height
|
|
x: -width + parent.width
|
|
MenuItem {
|
|
text: "Clear Pending Status"
|
|
enabled: !_reconstruction.computingLocally
|
|
onTriggered: _reconstruction.graph.clearSubmittedNodes()
|
|
}
|
|
MenuItem {
|
|
text: "Refresh Nodes Status"
|
|
enabled: !_reconstruction.computingLocally
|
|
onTriggered: _reconstruction.forceNodesStatusUpdate()
|
|
}
|
|
Menu {
|
|
title: "Advanced"
|
|
MenuItem {
|
|
text: "Lock on Compute"
|
|
ToolTip.text: "Lock Graph when computing. This should only be disabled for advanced usage."
|
|
ToolTip.visible: hovered
|
|
checkable: true
|
|
checked: GraphEditorSettings.lockOnCompute
|
|
onClicked: GraphEditorSettings.lockOnCompute = !GraphEditorSettings.lockOnCompute
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
GraphEditor {
|
|
id: graphEditor
|
|
|
|
anchors.fill: parent
|
|
uigraph: _reconstruction
|
|
nodeTypesModel: _nodeTypes
|
|
readOnly: graphLocked
|
|
|
|
onNodeDoubleClicked: {
|
|
_reconstruction.setActiveNodeOfType(node);
|
|
|
|
for(var i=0; i < node.attributes.count; ++i)
|
|
{
|
|
var attr = node.attributes.at(i)
|
|
if(attr.isOutput)
|
|
{
|
|
if(workspaceView.viewIn2D(attr))
|
|
{
|
|
break;
|
|
}
|
|
if(workspaceView.viewIn3D(attr, mouse))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
onComputeRequest: computeManager.compute(node)
|
|
onSubmitRequest: computeManager.submit(node)
|
|
}
|
|
}
|
|
|
|
NodeEditor {
|
|
width: Math.round(parent.width * 0.3)
|
|
node: _reconstruction.selectedNode
|
|
// Make NodeEditor readOnly when computing
|
|
readOnly: graphLocked
|
|
onAttributeDoubleClicked: {
|
|
if(workspaceView.viewIn2D(attribute))
|
|
{
|
|
return;
|
|
}
|
|
if(workspaceView.viewIn3D(attribute, mouse))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
onUpgradeRequest: {
|
|
var n = _reconstruction.upgradeNode(node);
|
|
_reconstruction.selectedNode = n;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|