mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-04-29 02:08:08 +02:00
1308 lines
48 KiB
QML
1308 lines
48 KiB
QML
import QtQuick 2.15
|
|
import QtQuick.Controls 2.15
|
|
import QtQuick.Controls 1.4 as Controls1 // For SplitView
|
|
import QtQuick.Layouts 1.11
|
|
import QtQuick.Window 2.15
|
|
import QtQml.Models 2.15
|
|
|
|
import Qt.labs.platform 1.0 as Platform
|
|
import QtQuick.Dialogs 1.3
|
|
|
|
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
|
|
|
|
title: {
|
|
var t = (_reconstruction && _reconstruction.graph && _reconstruction.graph.filepath) ? _reconstruction.graph.filepath : "Untitled"
|
|
if (_reconstruction && !_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() })
|
|
}
|
|
|
|
palette: _PaletteManager.palette
|
|
|
|
// TODO: uncomment for Qt6, which correctly supports palette for disabled elements AND an alternate base
|
|
/*
|
|
// QPalette is not convertible to QML palette (anymore)
|
|
Component.onCompleted: {
|
|
palette.alternateBase = _PaletteManager.alternateBase
|
|
palette.base = _PaletteManager.base
|
|
palette.button = _PaletteManager.button
|
|
palette.buttonText = _PaletteManager.buttonText
|
|
palette.highlight = _PaletteManager.highlight
|
|
palette.highlightedText = _PaletteManager.highlightedText
|
|
palette.link = _PaletteManager.link
|
|
palette.mid = _PaletteManager.mid
|
|
palette.shadow = _PaletteManager.shadow
|
|
palette.text = _PaletteManager.text
|
|
palette.toolTipBase = _PaletteManager.toolTipBase
|
|
palette.toolTipText = _PaletteManager.toolTipText
|
|
palette.window = _PaletteManager.window
|
|
palette.windowText = _PaletteManager.windowText
|
|
|
|
palette.disabled.buttonText = _PaletteManager.disabledButtonText
|
|
palette.disabled.highlight = _PaletteManager.disabledHighlight
|
|
palette.disabled.highlightedText = _PaletteManager.disabledHighlightedText
|
|
palette.disabled.text = _PaletteManager.disabledText
|
|
palette.disabled.windowText = _PaletteManager.disabledWindowText
|
|
} */
|
|
|
|
SystemPalette { id: activePalette }
|
|
SystemPalette { id: disabledPalette; colorGroup: SystemPalette.Disabled }
|
|
|
|
property url imagesFolder: {
|
|
var recentImportedImagesFolders = MeshroomApp.recentImportedImagesFolders
|
|
if (recentImportedImagesFolders.length > 0) {
|
|
for (var i = 0; i < recentImportedImagesFolders.length; i++) {
|
|
if (Filepath.exists(recentImportedImagesFolders[i]))
|
|
return Filepath.stringToUrl(recentImportedImagesFolders[i])
|
|
else
|
|
MeshroomApp.removeRecentImportedImagesFolder(Filepath.stringToUrl(recentImportedImagesFolders[i]))
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
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
|
|
property alias showImageViewer: imageViewerVisibilityCB.checked
|
|
property alias showViewer3D: viewer3DVisibilityCB.checked
|
|
}
|
|
|
|
Component.onDestruction: {
|
|
// store main window dimensions in persisting Settings
|
|
settings_General.windowWidth = _window.width
|
|
settings_General.windowHeight = _window.height
|
|
}
|
|
|
|
Shortcut {
|
|
// Ensures the Ctrl+N shortcut is always valid and creates a default pipeline
|
|
sequence: "Ctrl+N"
|
|
context: Qt.ApplicationShortcut
|
|
onActivated: ensureSaved(function() { _reconstruction.new() })
|
|
}
|
|
|
|
MessageDialog {
|
|
id: unsavedDialog
|
|
|
|
property var _callback: undefined
|
|
|
|
title: (_reconstruction ? Filepath.basename(_reconstruction.graph.filepath) : "") || "Unsaved Project"
|
|
preset: "Info"
|
|
canCopy: false
|
|
text: _reconstruction && _reconstruction.graph.filepath ? "Current project has unsaved modifications."
|
|
: "Current project has not been saved."
|
|
helperText: _reconstruction && _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
|
|
options: Platform.FileDialog.DontUseNativeDialog
|
|
|
|
signal closed(var result)
|
|
|
|
title: "Save File"
|
|
nameFilters: ["Meshroom Graphs (*.mg)"]
|
|
defaultSuffix: ".mg"
|
|
fileMode: Platform.FileDialog.SaveFile
|
|
onAccepted: {
|
|
_reconstruction.saveAs(currentFile)
|
|
closed(Platform.Dialog.Accepted)
|
|
MeshroomApp.addRecentProjectFile(currentFile.toString())
|
|
}
|
|
onRejected: closed(Platform.Dialog.Rejected)
|
|
}
|
|
|
|
Platform.FileDialog {
|
|
id: saveTemplateDialog
|
|
options: Platform.FileDialog.DontUseNativeDialog
|
|
|
|
signal closed(var result)
|
|
|
|
title: "Save Template"
|
|
nameFilters: ["Meshroom Graphs (*.mg)"]
|
|
defaultSuffix: ".mg"
|
|
fileMode: Platform.FileDialog.SaveFile
|
|
onAccepted: {
|
|
_reconstruction.saveAsTemplate(currentFile)
|
|
closed(Platform.Dialog.Accepted)
|
|
MeshroomApp.reloadTemplateList()
|
|
}
|
|
onRejected: closed(Platform.Dialog.Rejected)
|
|
}
|
|
|
|
Item {
|
|
id: computeManager
|
|
|
|
property bool warnIfUnsaved: true
|
|
|
|
// evaluate if graph computation can be submitted externally
|
|
property bool canSubmit: _reconstruction ?
|
|
_reconstruction.canSubmit // current setup allows to compute externally
|
|
&& _reconstruction.graph.filepath : // graph is saved on disk
|
|
false
|
|
|
|
function compute(nodes, force) {
|
|
if (!force && warnIfUnsaved && !_reconstruction.graph.filepath)
|
|
{
|
|
unsavedComputeDialog.selectedNodes = nodes;
|
|
unsavedComputeDialog.open();
|
|
}
|
|
else {
|
|
try {
|
|
_reconstruction.execute(nodes)
|
|
}
|
|
catch (error) {
|
|
const data = ErrorHandler.analyseError(error)
|
|
if (data.context === "COMPUTATION")
|
|
computeSubmitErrorDialog.openError(data.type, data.msg, nodes)
|
|
}
|
|
}
|
|
}
|
|
|
|
function submit(nodes) {
|
|
if (!canSubmit) {
|
|
unsavedSubmitDialog.open()
|
|
} else {
|
|
try {
|
|
_reconstruction.submit(nodes)
|
|
}
|
|
catch (error) {
|
|
const data = ErrorHandler.analyseError(error)
|
|
if (data.context === "SUBMITTING")
|
|
computeSubmitErrorDialog.openError(data.type, data.msg, nodes)
|
|
}
|
|
}
|
|
}
|
|
|
|
MessageDialog {
|
|
id: computeSubmitErrorDialog
|
|
|
|
property string errorType // Used to specify signals' behavior
|
|
property var currentNode: null
|
|
|
|
function openError(type, msg, node) {
|
|
errorType = type
|
|
switch (type) {
|
|
case "Already Submitted": {
|
|
this.setupPendingStatusError(msg, node)
|
|
break
|
|
}
|
|
case "Compatibility Issue": {
|
|
this.setupCompatibilityIssue(msg)
|
|
break
|
|
}
|
|
default: {
|
|
this.onlyDisplayError(msg)
|
|
}
|
|
}
|
|
|
|
this.open()
|
|
}
|
|
|
|
function onlyDisplayError(msg) {
|
|
text = msg
|
|
|
|
standardButtons = Dialog.Ok
|
|
}
|
|
|
|
function setupPendingStatusError(msg, node) {
|
|
currentNode = node
|
|
text = msg + "\n\nDo you want to Clear Pending Status and Start Computing?"
|
|
|
|
standardButtons = (Dialog.Ok | Dialog.Cancel)
|
|
}
|
|
|
|
function setupCompatibilityIssue(msg) {
|
|
text = msg + "\n\nDo you want to open the Compatibility Manager?"
|
|
|
|
standardButtons = (Dialog.Ok | Dialog.Cancel)
|
|
}
|
|
|
|
canCopy: false
|
|
icon.text: MaterialIcons.warning
|
|
parent: Overlay.overlay
|
|
preset: "Warning"
|
|
title: "Computation/Submitting"
|
|
text: ""
|
|
|
|
onAccepted: {
|
|
switch (errorType) {
|
|
case "Already Submitted": {
|
|
close()
|
|
_reconstruction.graph.clearSubmittedNodes()
|
|
_reconstruction.execute(currentNode)
|
|
break
|
|
}
|
|
case "Compatibility Issue": {
|
|
close()
|
|
compatibilityManager.open()
|
|
break
|
|
}
|
|
default: close()
|
|
}
|
|
}
|
|
|
|
onRejected: close()
|
|
}
|
|
|
|
MessageDialog {
|
|
id: unsavedComputeDialog
|
|
|
|
property var selectedNodes: 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 ? _reconstruction.graph.cacheDir : "unknown")
|
|
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(selectedNodes, true)
|
|
}
|
|
|
|
onAccepted: saveAsAction.trigger()
|
|
}
|
|
|
|
MessageDialog {
|
|
id: unsavedSubmitDialog
|
|
|
|
canCopy: false
|
|
icon.text: MaterialIcons.warning
|
|
parent: Overlay.overlay
|
|
preset: "Warning"
|
|
title: "Unsaved Project"
|
|
text: "The project cannot be submitted if it remains unsaved."
|
|
helperText: "Save the project to be able to submit it?"
|
|
standardButtons: Dialog.Cancel | Dialog.Save
|
|
|
|
onDiscarded: close()
|
|
onAccepted: saveAsAction.trigger()
|
|
}
|
|
|
|
MessageDialog {
|
|
id: fileModifiedDialog
|
|
|
|
canCopy: false
|
|
icon.text: MaterialIcons.warning
|
|
parent: Overlay.overlay
|
|
preset: "Warning"
|
|
title: "File Modified"
|
|
text: "The file has been modified by another instance."
|
|
detailedText: "Do you want to overwrite the file?"
|
|
|
|
// Add a reload file button next to the save button
|
|
footer: DialogButtonBox {
|
|
position: DialogButtonBox.Footer
|
|
standardButtons: Dialog.Save | Dialog.Cancel
|
|
|
|
Button {
|
|
text: "Reload File"
|
|
|
|
onClicked: {
|
|
_reconstruction.loadUrl(_reconstruction.graph.filepath)
|
|
fileModifiedDialog.close()
|
|
}
|
|
}
|
|
}
|
|
|
|
onAccepted: _reconstruction.save()
|
|
onDiscarded: close()
|
|
}
|
|
}
|
|
|
|
Platform.FileDialog {
|
|
id: openFileDialog
|
|
options: Platform.FileDialog.DontUseNativeDialog
|
|
title: "Open File"
|
|
nameFilters: ["Meshroom Graphs (*.mg)"]
|
|
onAccepted: {
|
|
if (_reconstruction.loadUrl(currentFile)) {
|
|
MeshroomApp.addRecentProjectFile(currentFile.toString())
|
|
}
|
|
}
|
|
}
|
|
|
|
Platform.FileDialog {
|
|
id: loadTemplateDialog
|
|
options: Platform.FileDialog.DontUseNativeDialog
|
|
title: "Load Template"
|
|
nameFilters: ["Meshroom Graphs (*.mg)"]
|
|
onAccepted: {
|
|
// Open the template as a regular file
|
|
if (_reconstruction.loadUrl(currentFile, true, true)) {
|
|
MeshroomApp.addRecentProjectFile(currentFile.toString())
|
|
}
|
|
}
|
|
}
|
|
|
|
Platform.FileDialog {
|
|
id: importImagesDialog
|
|
options: FileDialog.DontUseNativeDialog
|
|
title: "Import Images"
|
|
fileMode: Platform.FileDialog.OpenFiles
|
|
nameFilters: []
|
|
onAccepted: {
|
|
_reconstruction.importImagesUrls(currentFiles)
|
|
imagesFolder = Filepath.dirname(currentFiles[0])
|
|
MeshroomApp.addRecentImportedImagesFolder(imagesFolder)
|
|
}
|
|
}
|
|
|
|
Platform.FileDialog {
|
|
id: importProjectDialog
|
|
options: FileDialog.DontUseNativeDialog
|
|
title: "Import Project"
|
|
fileMode: Platform.FileDialog.OpenFile
|
|
nameFilters: ["Meshroom Graphs (*.mg)"]
|
|
onAccepted: {
|
|
graphEditor.uigraph.importProject(currentFile)
|
|
}
|
|
}
|
|
|
|
AboutDialog {
|
|
id: aboutDialog
|
|
}
|
|
|
|
// Check if document has been saved
|
|
function ensureSaved(callback)
|
|
{
|
|
var saved = _reconstruction.undoStack.clean
|
|
if (!saved) { // If current document is modified, open "unsaved dialog"
|
|
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 ? _reconstruction.buildingIntrinsics : false
|
|
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: removeAllImagesAction
|
|
property string tooltip: "Remove all the images from the current CameraInit group"
|
|
text: "Remove All Images"
|
|
onTriggered: {
|
|
_reconstruction.removeAllImages()
|
|
_reconstruction.selectedViewId = "-1"
|
|
}
|
|
}
|
|
|
|
Action {
|
|
id: removeImagesFromAllGroupsAction
|
|
property string tooltip: "Remove all the images from all the CameraInit groups"
|
|
text: "Remove Images From All CameraInit Nodes"
|
|
onTriggered: {
|
|
_reconstruction.removeImagesFromAllGroups()
|
|
_reconstruction.selectedViewId = "-1"
|
|
}
|
|
}
|
|
|
|
Action {
|
|
id: undoAction
|
|
|
|
property string tooltip: 'Undo "' + (_reconstruction ? _reconstruction.undoStack.undoText : "Unknown") + '"'
|
|
text: "Undo"
|
|
shortcut: "Ctrl+Z"
|
|
enabled: _reconstruction ? _reconstruction.undoStack.canUndo && _reconstruction.undoStack.isUndoableIndex : false
|
|
onTriggered: _reconstruction.undoStack.undo()
|
|
}
|
|
Action {
|
|
id: redoAction
|
|
|
|
property string tooltip: 'Redo "' + (_reconstruction ? _reconstruction.undoStack.redoText : "Unknown") + '"'
|
|
text: "Redo"
|
|
shortcut: "Ctrl+Shift+Z"
|
|
enabled: _reconstruction ? _reconstruction.undoStack.canRedo && !_reconstruction.undoStack.lockedRedo : false
|
|
onTriggered: _reconstruction.undoStack.redo()
|
|
}
|
|
|
|
function getSelectedNodesName()
|
|
{
|
|
if (!_reconstruction)
|
|
return ""
|
|
var nodesName = ""
|
|
for (var i = 0; i < _reconstruction.selectedNodes.count; i++)
|
|
{
|
|
if (nodesName !== "")
|
|
nodesName += ", "
|
|
var node = _reconstruction.selectedNodes.at(i)
|
|
nodesName += node.name
|
|
}
|
|
return nodesName
|
|
}
|
|
Action {
|
|
id: cutAction
|
|
|
|
property string tooltip: {
|
|
var s = "Copy selected node"
|
|
s += (_reconstruction && _reconstruction.selectedNodes.count > 1 ? "s (" : " (") + getSelectedNodesName()
|
|
s += ") to the clipboard and remove them from the graph"
|
|
return s
|
|
}
|
|
text: "Cut Node" + (_reconstruction && _reconstruction.selectedNodes.count > 1 ? "s " : " ")
|
|
enabled: _reconstruction ? _reconstruction.selectedNodes.count > 0 : false
|
|
onTriggered: {
|
|
graphEditor.copyNodes()
|
|
graphEditor.uigraph.removeNodes(graphEditor.uigraph.selectedNodes)
|
|
}
|
|
}
|
|
|
|
Action {
|
|
id: copyAction
|
|
|
|
property string tooltip: {
|
|
var s = "Copy selected node"
|
|
s += (_reconstruction && _reconstruction.selectedNodes.count > 1 ? "s (" : " (") + getSelectedNodesName()
|
|
s += ") to the clipboard"
|
|
return s
|
|
}
|
|
text: "Copy Node" + (_reconstruction && _reconstruction.selectedNodes.count > 1 ? "s " : " ")
|
|
enabled: _reconstruction ? _reconstruction.selectedNodes.count > 0 : false
|
|
onTriggered: graphEditor.copyNodes()
|
|
}
|
|
|
|
Action {
|
|
id: pasteAction
|
|
|
|
property string tooltip: "Paste the clipboard content to the project if it contains valid nodes"
|
|
text: "Paste Node(s)"
|
|
onTriggered: graphEditor.pasteNodes()
|
|
}
|
|
|
|
|
|
Action {
|
|
id: loadTemplateAction
|
|
|
|
property string tooltip: "Load a template like a regular project file (any \"Publish\" node will be displayed)"
|
|
text: "Load Template"
|
|
onTriggered: {
|
|
ensureSaved(function() {
|
|
initFileDialogFolder(loadTemplateDialog);
|
|
loadTemplateDialog.open();
|
|
})
|
|
}
|
|
}
|
|
|
|
// TODO: uncomment for Qt6 to re-enable the alternative palette (the alternative palette and the disabled items currently cannot both be supported)
|
|
/* Action {
|
|
|
|
shortcut: "Ctrl+Shift+P"
|
|
onTriggered: _PaletteManager.togglePalette()
|
|
} */
|
|
|
|
|
|
// Utility functions for elements in the menubar
|
|
|
|
function initFileDialogFolder(dialog, importImages = false) {
|
|
let folder = "";
|
|
|
|
if (imagesFolder.toString() === "" && workspaceView.imageGallery.galleryGrid.itemAtIndex(0) !== null) {
|
|
imagesFolder = Filepath.stringToUrl(Filepath.dirname(workspaceView.imageGallery.galleryGrid.itemAtIndex(0).source))
|
|
}
|
|
|
|
if (_reconstruction.graph && _reconstruction.graph.filepath) {
|
|
folder = Filepath.stringToUrl(Filepath.dirname(_reconstruction.graph.filepath))
|
|
} else {
|
|
var projects = MeshroomApp.recentProjectFiles;
|
|
if (projects.length > 0 && Filepath.exists(projects[0])) {
|
|
folder = Filepath.stringToUrl(Filepath.dirname(projects[0]))
|
|
}
|
|
}
|
|
|
|
if (importImages && imagesFolder.toString() !== "" && Filepath.exists(imagesFolder)) {
|
|
folder = imagesFolder
|
|
}
|
|
|
|
dialog.folder = folder
|
|
}
|
|
|
|
header: MenuBar {
|
|
palette.window: Qt.darker(activePalette.window, 1.15)
|
|
Menu {
|
|
title: "File"
|
|
Menu {
|
|
id: newPipelineMenu
|
|
title: "New Pipeline"
|
|
enabled: newPipelineMenuItems.model !== undefined && newPipelineMenuItems.model.length > 0
|
|
property int maxWidth: 1000
|
|
property int fullWidth: {
|
|
var result = 0;
|
|
for (var i = 0; i < count; ++i) {
|
|
var item = itemAt(i)
|
|
result = Math.max(item.implicitWidth + item.padding * 2, result)
|
|
}
|
|
return result;
|
|
}
|
|
implicitWidth: fullWidth
|
|
Repeater {
|
|
id: newPipelineMenuItems
|
|
model: MeshroomApp.pipelineTemplateFiles
|
|
MenuItem {
|
|
onTriggered: ensureSaved(function() {
|
|
_reconstruction.new(modelData["key"])
|
|
})
|
|
|
|
text: fileTextMetrics.elidedText
|
|
TextMetrics {
|
|
id: fileTextMetrics
|
|
text: modelData["name"]
|
|
elide: Text.ElideLeft
|
|
elideWidth: newPipelineMenu.maxWidth
|
|
}
|
|
ToolTip.text: modelData["path"]
|
|
ToolTip.visible: hovered
|
|
ToolTip.delay: 200
|
|
}
|
|
}
|
|
}
|
|
Action {
|
|
id: openActionItem
|
|
text: "Open"
|
|
shortcut: "Ctrl+O"
|
|
onTriggered: ensureSaved(function() {
|
|
initFileDialogFolder(openFileDialog)
|
|
openFileDialog.open()
|
|
})
|
|
}
|
|
Menu {
|
|
id: openRecentMenu
|
|
title: "Open Recent"
|
|
enabled: recentFilesMenuItems.model !== undefined && recentFilesMenuItems.model.length > 0
|
|
property int maxWidth: 1000
|
|
property int fullWidth: {
|
|
var result = 0;
|
|
for (var i = 0; i < count; ++i) {
|
|
var item = itemAt(i)
|
|
result = Math.max(item.implicitWidth + item.padding * 2, result)
|
|
}
|
|
return result
|
|
}
|
|
implicitWidth: fullWidth
|
|
Repeater {
|
|
id: recentFilesMenuItems
|
|
model: MeshroomApp.recentProjectFiles
|
|
MenuItem {
|
|
onTriggered: ensureSaved(function() {
|
|
openRecentMenu.dismiss()
|
|
if (_reconstruction.loadUrl(modelData)) {
|
|
MeshroomApp.addRecentProjectFile(modelData)
|
|
} else {
|
|
MeshroomApp.removeRecentProjectFile(modelData)
|
|
}
|
|
})
|
|
|
|
text: fileTextMetrics.elidedText
|
|
TextMetrics {
|
|
id: fileTextMetrics
|
|
text: modelData
|
|
elide: Text.ElideLeft
|
|
elideWidth: openRecentMenu.maxWidth
|
|
}
|
|
}
|
|
}
|
|
}
|
|
MenuSeparator { }
|
|
Action {
|
|
id: saveAction
|
|
text: "Save"
|
|
shortcut: "Ctrl+S"
|
|
enabled: _reconstruction ? (_reconstruction.graph && !_reconstruction.graph.filepath) || !_reconstruction.undoStack.clean : false
|
|
onTriggered: {
|
|
if (_reconstruction.graph.filepath) {
|
|
// get current time date
|
|
var date = _reconstruction.graph.getFileDateVersionFromPath(_reconstruction.graph.filepath)
|
|
|
|
// check if the file has been modified by another instance
|
|
if (_reconstruction.graph.fileDateVersion !== date) {
|
|
fileModifiedDialog.open()
|
|
} else
|
|
_reconstruction.save()
|
|
} else {
|
|
initFileDialogFolder(saveFileDialog)
|
|
saveFileDialog.open()
|
|
}
|
|
}
|
|
}
|
|
Action {
|
|
id: saveAsAction
|
|
text: "Save As..."
|
|
shortcut: "Ctrl+Shift+S"
|
|
onTriggered: {
|
|
initFileDialogFolder(saveFileDialog)
|
|
saveFileDialog.open()
|
|
}
|
|
}
|
|
MenuSeparator { }
|
|
Action {
|
|
id: importImagesAction
|
|
text: "Import Images"
|
|
shortcut: "Ctrl+I"
|
|
onTriggered: {
|
|
initFileDialogFolder(importImagesDialog, true)
|
|
importImagesDialog.open()
|
|
}
|
|
}
|
|
|
|
MenuItem {
|
|
action: removeAllImagesAction
|
|
ToolTip.visible: hovered
|
|
ToolTip.text: removeAllImagesAction.tooltip
|
|
}
|
|
|
|
MenuSeparator { }
|
|
Menu {
|
|
id: advancedMenu
|
|
title: "Advanced"
|
|
implicitWidth: 300
|
|
|
|
Action {
|
|
id: saveAsTemplateAction
|
|
text: "Save As Template..."
|
|
shortcut: Shortcut {
|
|
sequence: "Ctrl+Shift+T"
|
|
context: Qt.ApplicationShortcut
|
|
onActivated: saveAsTemplateAction.triggered()
|
|
}
|
|
onTriggered: {
|
|
initFileDialogFolder(saveTemplateDialog)
|
|
saveTemplateDialog.open()
|
|
}
|
|
}
|
|
|
|
MenuItem {
|
|
action: loadTemplateAction
|
|
ToolTip.visible: hovered
|
|
ToolTip.text: loadTemplateAction.tooltip
|
|
}
|
|
|
|
Action {
|
|
id: importProjectAction
|
|
text: "Import Project"
|
|
shortcut: Shortcut {
|
|
sequence: "Ctrl+Shift+I"
|
|
context: Qt.ApplicationShortcut
|
|
onActivated: importProjectAction.triggered()
|
|
}
|
|
onTriggered: {
|
|
initFileDialogFolder(importProjectDialog)
|
|
importProjectDialog.open()
|
|
}
|
|
}
|
|
|
|
MenuItem {
|
|
action: removeImagesFromAllGroupsAction
|
|
ToolTip.visible: hovered
|
|
ToolTip.text: removeImagesFromAllGroupsAction.tooltip
|
|
}
|
|
}
|
|
MenuSeparator { }
|
|
Action {
|
|
text: "Quit"
|
|
onTriggered: _window.close()
|
|
}
|
|
}
|
|
Menu {
|
|
title: "Edit"
|
|
MenuItem {
|
|
action: undoAction
|
|
ToolTip.visible: hovered
|
|
ToolTip.text: undoAction.tooltip
|
|
}
|
|
MenuItem {
|
|
action: redoAction
|
|
ToolTip.visible: hovered
|
|
ToolTip.text: redoAction.tooltip
|
|
}
|
|
MenuItem {
|
|
action: cutAction
|
|
ToolTip.visible: hovered
|
|
ToolTip.text: cutAction.tooltip
|
|
}
|
|
MenuItem {
|
|
action: copyAction
|
|
ToolTip.visible: hovered
|
|
ToolTip.text: copyAction.tooltip
|
|
}
|
|
MenuItem {
|
|
action: pasteAction
|
|
ToolTip.visible: hovered
|
|
ToolTip.text: pasteAction.tooltip
|
|
}
|
|
}
|
|
Menu {
|
|
title: "View"
|
|
MenuItem {
|
|
id: graphEditorVisibilityCB
|
|
text: "Graph Editor"
|
|
checkable: true
|
|
checked: true
|
|
}
|
|
MenuItem {
|
|
id: liveSfMVisibilityCB
|
|
text: "Live Reconstruction"
|
|
checkable: true
|
|
checked: false
|
|
}
|
|
MenuItem {
|
|
id: imageViewerVisibilityCB
|
|
text: "Image Viewer"
|
|
checkable: true
|
|
checked: true
|
|
}
|
|
MenuItem {
|
|
id: viewer3DVisibilityCB
|
|
text: "3D Viewer"
|
|
checkable: true
|
|
checked: true
|
|
}
|
|
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: "Online Documentation"
|
|
onTriggered: Qt.openUrlExternally("https://meshroom-manual.readthedocs.io")
|
|
}
|
|
Action {
|
|
text: "About Meshroom"
|
|
onTriggered: aboutDialog.open()
|
|
// should 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 ? _reconstruction.graph.cacheDir : "Unknown"
|
|
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
|
|
}
|
|
|
|
function onGraphChanged() {
|
|
// open CompatibilityManager after file loading if any issue is detected
|
|
if (compatibilityManager.issueCount)
|
|
compatibilityManager.open()
|
|
// trigger fit to visualize all nodes
|
|
graphEditor.fit()
|
|
}
|
|
|
|
function onInfo() { createDialog(dialogsFactory.info, arguments[0]) }
|
|
function onWarning() { createDialog(dialogsFactory.warning, arguments[0]) }
|
|
function 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
|
|
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
|
|
onClicked: computeManager.compute(null)
|
|
}
|
|
Button {
|
|
text: "Stop"
|
|
enabled: _reconstruction ? _reconstruction.computingLocally : false
|
|
onClicked: _reconstruction.stopExecution()
|
|
}
|
|
Item { width: 20; height: 1 }
|
|
Button {
|
|
visible: _reconstruction ? _reconstruction.canSubmit : false
|
|
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
|
|
}
|
|
}
|
|
|
|
// "ProgressBar" reflecting status of all the chunks in the graph, in their process order
|
|
NodeChunks {
|
|
id: chunksListView
|
|
Layout.fillWidth: true
|
|
height: 6
|
|
model: _reconstruction ? _reconstruction.sortedDFSChunks : null
|
|
}
|
|
|
|
WorkspaceView {
|
|
id: workspaceView
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
Layout.minimumHeight: 50
|
|
reconstruction: _reconstruction
|
|
readOnly: _reconstruction ? _reconstruction.computing : false
|
|
|
|
function viewNode(node, mouse) {
|
|
// 2D viewer
|
|
viewer2D.tryLoadNode(node)
|
|
|
|
// 3D viewer
|
|
// By default we only display the first 3D item, except if it has the semantic flag "3D"
|
|
var alreadyDisplay = false
|
|
for (var i = 0; i < node.attributes.count; i++) {
|
|
var attr = node.attributes.at(i)
|
|
if (attr.isOutput && attr.desc.semantic !== "image")
|
|
if (!alreadyDisplay || attr.desc.semantic == "3D")
|
|
if (workspaceView.viewIn3D(attr, mouse))
|
|
alreadyDisplay = true
|
|
}
|
|
}
|
|
|
|
function viewIn3D(attribute, mouse) {
|
|
if (!panel3dViewer || (!attribute.node.has3DOutput && !attribute.node.hasAttribute("useBoundingBox")))
|
|
return false
|
|
var loaded = panel3dViewer.viewer3D.view(attribute)
|
|
|
|
// solo media if Control modifier was held
|
|
if (loaded && mouse && mouse.modifiers & Qt.ControlModifier)
|
|
panel3dViewer.viewer3D.solo(attribute)
|
|
return loaded
|
|
}
|
|
}
|
|
}
|
|
|
|
Controls1.SplitView {
|
|
orientation: Qt.Horizontal
|
|
width: parent.width
|
|
height: Math.round(parent.height * 0.3)
|
|
visible: settings_UILayout.showGraphEditor
|
|
|
|
TabPanel {
|
|
id: graphEditorPanel
|
|
Layout.fillWidth: true
|
|
padding: 4
|
|
tabs: ["Graph Editor", "Task Manager", "Script Editor"]
|
|
|
|
headerBar: RowLayout {
|
|
MaterialToolButton {
|
|
text: MaterialIcons.refresh
|
|
ToolTip.text: "Refresh Nodes Status"
|
|
ToolTip.visible: hovered
|
|
font.pointSize: 11
|
|
padding: 2
|
|
onClicked: {
|
|
updatingStatus = true
|
|
_reconstruction.forceNodesStatusUpdate()
|
|
updatingStatus = false
|
|
}
|
|
property bool updatingStatus: false
|
|
enabled: !updatingStatus
|
|
}
|
|
MaterialToolButton {
|
|
id: filePollerRefreshStatus
|
|
text: {
|
|
if (_reconstruction.filePollerRefresh === 0)
|
|
return MaterialIcons.published_with_changes
|
|
else if (_reconstruction.filePollerRefresh === 2)
|
|
return MaterialIcons.sync
|
|
else
|
|
return MaterialIcons.sync_disabled
|
|
}
|
|
font.pointSize: 11
|
|
padding: 2
|
|
enabled: true
|
|
ToolTip {
|
|
id: filePollerTooltip
|
|
property string title: "Auto-Refresh Nodes Status For External Changes. "
|
|
property string description: "Check if the status of a node is changed by another instance on the same network, " +
|
|
"such as when computing in render farm."
|
|
text: {
|
|
var status = ""
|
|
if (_reconstruction.filePollerRefresh === 0)
|
|
status = "Enabled"
|
|
else if (_reconstruction.filePollerRefresh === 2)
|
|
status = "Minimal"
|
|
else
|
|
status = "Disabled"
|
|
return title + "(Current: " + status + ")\n\n" + description
|
|
}
|
|
visible: filePollerRefreshStatus.hovered
|
|
contentWidth: 420
|
|
}
|
|
onClicked: {
|
|
refreshFilesMenu.open()
|
|
}
|
|
Menu {
|
|
id: refreshFilesMenu
|
|
width: 210
|
|
y: parent.height
|
|
x: -width + parent.width
|
|
MenuItem {
|
|
id: enableAutoRefresh
|
|
text: "Enable Auto-Refresh"
|
|
checkable: true
|
|
checked: _reconstruction.filePollerRefresh === 0
|
|
ToolTip.text: "Check every file's status periodically"
|
|
ToolTip.visible: hovered
|
|
ToolTip.delay: 200
|
|
onToggled: {
|
|
if (checked) {
|
|
disableAutoRefresh.checked = false
|
|
minimalAutoRefresh.checked = false
|
|
_reconstruction.filePollerRefreshChanged(0)
|
|
}
|
|
// Prevents cases where the user unchecks the currently checked option
|
|
enableAutoRefresh.checked = true
|
|
filePollerRefreshStatus.text = MaterialIcons.published_with_changes
|
|
filePollerTooltip.text = filePollerTooltip.title + "(Current: Enabled)\n\n" + filePollerTooltip.description
|
|
}
|
|
}
|
|
MenuItem {
|
|
id: disableAutoRefresh
|
|
text: "Disable Auto-Refresh"
|
|
checkable: true
|
|
checked: _reconstruction.filePollerRefresh === 1
|
|
ToolTip.text: "No file status will be checked"
|
|
ToolTip.visible: hovered
|
|
ToolTip.delay: 200
|
|
onToggled: {
|
|
if (checked) {
|
|
enableAutoRefresh.checked = false
|
|
minimalAutoRefresh.checked = false
|
|
_reconstruction.filePollerRefreshChanged(1)
|
|
}
|
|
// Prevents cases where the user unchecks the currently checked option
|
|
disableAutoRefresh.checked = true
|
|
filePollerRefreshStatus.text = MaterialIcons.sync_disabled
|
|
filePollerTooltip.text = filePollerTooltip.title + "(Current: Disabled)\n\n" + filePollerTooltip.description
|
|
}
|
|
}
|
|
MenuItem {
|
|
id: minimalAutoRefresh
|
|
text: "Enable Minimal Auto-Refresh"
|
|
checkable: true
|
|
checked: _reconstruction.filePollerRefresh === 2
|
|
ToolTip.text: "Check the file status of submitted or running chunks periodically"
|
|
ToolTip.visible: hovered
|
|
ToolTip.delay: 200
|
|
onToggled: {
|
|
if (checked) {
|
|
disableAutoRefresh.checked = false
|
|
enableAutoRefresh.checked = false
|
|
_reconstruction.filePollerRefreshChanged(2)
|
|
}
|
|
// Prevents cases where the user unchecks the currently checked option
|
|
minimalAutoRefresh.checked = true
|
|
filePollerRefreshStatus.text = MaterialIcons.sync
|
|
filePollerTooltip.text = filePollerTooltip.title + "(Current: Minimal)\n\n" + filePollerTooltip.description
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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 ? !_reconstruction.computingLocally : false
|
|
onTriggered: _reconstruction.graph.clearSubmittedNodes()
|
|
}
|
|
MenuItem {
|
|
text: "Force Unlock Nodes"
|
|
onTriggered: _reconstruction.graph.forceUnlockNodes()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
GraphEditor {
|
|
id: graphEditor
|
|
|
|
visible: graphEditorPanel.currentTab === 0
|
|
|
|
anchors.fill: parent
|
|
uigraph: _reconstruction
|
|
nodeTypesModel: _nodeTypes
|
|
|
|
onNodeDoubleClicked: {
|
|
_reconstruction.setActiveNode(node);
|
|
workspaceView.viewNode(node, mouse);
|
|
}
|
|
onComputeRequest: {
|
|
_reconstruction.forceNodesStatusUpdate();
|
|
computeManager.compute(nodes)
|
|
}
|
|
onSubmitRequest: {
|
|
_reconstruction.forceNodesStatusUpdate();
|
|
computeManager.submit(nodes)
|
|
}
|
|
onFilesDropped: {
|
|
var filesByType = _reconstruction.getFilesByTypeFromDrop(drop.urls)
|
|
if (filesByType["meshroomScenes"].length == 1) {
|
|
ensureSaved(function() {
|
|
if (_reconstruction.handleFilesUrl(filesByType, null, mousePosition)) {
|
|
MeshroomApp.addRecentProjectFile(filesByType["meshroomScenes"][0])
|
|
}
|
|
})
|
|
} else {
|
|
_reconstruction.handleFilesUrl(filesByType, null, mousePosition)
|
|
}
|
|
}
|
|
}
|
|
|
|
TaskManager {
|
|
id: taskManager
|
|
|
|
visible: graphEditorPanel.currentTab === 1
|
|
|
|
uigraph: _reconstruction
|
|
taskManager: _reconstruction ? _reconstruction.taskManager : null
|
|
|
|
anchors.fill: parent
|
|
}
|
|
|
|
ScriptEditor {
|
|
id: scriptEditor
|
|
|
|
visible: graphEditorPanel.currentTab === 2
|
|
anchors.fill: parent
|
|
}
|
|
}
|
|
|
|
NodeEditor {
|
|
id: nodeEditor
|
|
width: Math.round(parent.width * 0.3)
|
|
node: _reconstruction ? _reconstruction.selectedNode : null
|
|
property bool computing: _reconstruction ? _reconstruction.computing : false
|
|
// Make NodeEditor readOnly when computing
|
|
readOnly: node ? node.locked : false
|
|
|
|
onUpgradeRequest: {
|
|
var n = _reconstruction.upgradeNode(node)
|
|
_reconstruction.selectedNode = n
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
background: MouseArea {
|
|
onPressed: {
|
|
forceActiveFocus();
|
|
}
|
|
}
|
|
}
|