Meshroom/meshroom/ui/qml/Application.qml
2024-08-26 16:21:27 +02:00

782 lines
No EOL
32 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
Page {
id: root
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
}
// 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
}
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();
})
}
}
header: RowLayout {
spacing: 0
MaterialToolButton {
id: homeButton
text: MaterialIcons.home
font.pointSize: 18
background: Rectangle {
color: homeButton.hovered ? activePalette.highlight : Qt.darker(activePalette.window, 1.15)
border.color: Qt.darker(activePalette.window, 1.15)
}
onClicked: ensureSaved(function() {
_reconstruction.clear()
if (mainStack.depth == 1)
mainStack.replace("Homepage.qml")
else
mainStack.pop()
})
}
MenuBar {
palette.window: Qt.darker(activePalette.window, 1.15)
Menu {
title: "File"
Action {
id: newAction
text: "New"
shortcut: "Ctrl+N"
onTriggered: ensureSaved(function() {
_reconstruction.new()
})
}
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["path"])) {
MeshroomApp.addRecentProjectFile(modelData["path"])
} else {
MeshroomApp.removeRecentProjectFile(modelData["path"])
}
})
text: fileTextMetrics.elidedText
TextMetrics {
id: fileTextMetrics
text: modelData["path"]
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: "Process"
Action {
text: "Compute all nodes"
onTriggered: computeManager.compute(null)
enabled: _reconstruction ? !_reconstruction.computingLocally : false
}
Action {
text: "Submit all nodes"
onTriggered: computeManager.submit(null)
enabled: _reconstruction ? _reconstruction.canSubmit : false
}
MenuSeparator {}
Action {
text: "Stop computation"
onTriggered: _reconstruction.stopExecution()
enabled: _reconstruction ? _reconstruction.computingLocally : false
}
}
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"
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Qt.darker(activePalette.window, 1.15)
}
Row {
// Process buttons
MaterialToolButton {
id: processButton
font.pointSize: 18
text: !(_reconstruction.computingLocally) ? MaterialIcons.send : MaterialIcons.cancel_schedule_send
ToolTip.text: !(_reconstruction.computingLocally) ? "Start the computation" : "Stop the computation"
ToolTip.visible: hovered
background: Rectangle {
color: processButton.hovered ? activePalette.highlight : Qt.darker(activePalette.window, 1.15)
border.color: Qt.darker(activePalette.window, 1.15)
}
onClicked: !(_reconstruction.computingLocally) ? computeManager.compute(null) : _reconstruction.stopExecution()
}
MaterialToolButton {
id: submitButton
font.pointSize: 18
visible: _reconstruction ? _reconstruction.canSubmit : false
text: MaterialIcons.rocket_launch
ToolTip.text: "Submit"
ToolTip.visible: hovered
background: Rectangle {
color: submitButton.hovered ? activePalette.highlight : Qt.darker(activePalette.window, 1.15)
border.color: Qt.darker(activePalette.window, 1.15)
}
onClicked: computeManager.submit(null)
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Qt.darker(activePalette.window, 1.15)
}
// CompatibilityManager indicator
ToolButton {
id: compatibilityIssuesButton
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
background: Rectangle {
color: compatibilityIssuesButton.hovered ? activePalette.highlight : Qt.darker(activePalette.window, 1.15)
border.color: Qt.darker(activePalette.window, 1.15)
}
}
}
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
implicitHeight: Math.round(parent.height * 0.7)
spacing: 4
// "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
highlightChunks: false
}
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
}
}
}
}
}