mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-07-31 07:18:25 +02:00
[ui] better UI Components split + improvements
* add WorkspaceView that contains Meshroom main modules (ImageGallery, 2D/3D viewer) * add Panel component to standardize UI modules layout * ImageGallery: * add images basename on image delegates * add explanatory placeholder when no image has been added to the reconstruction yet
This commit is contained in:
parent
e5acd916dc
commit
8d8bf0be5e
5 changed files with 387 additions and 190 deletions
|
@ -1,78 +1,66 @@
|
|||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Controls 1.4 as Controls1 // For SplitView
|
||||
import QtQuick.Layouts 1.3
|
||||
import Qt.labs.platform 1.0 as Platform
|
||||
import "Viewer" 1.0
|
||||
import MaterialIcons 2.2
|
||||
import "filepath.js" as Filepath
|
||||
import QtQml.Models 2.2
|
||||
|
||||
|
||||
Item {
|
||||
/**
|
||||
* ImageGallery displays as a grid of Images a model containing Viewpoints objects.
|
||||
*/
|
||||
Panel {
|
||||
id: root
|
||||
|
||||
property alias model: grid.model
|
||||
property bool readOnly: false
|
||||
property string meshFile: ''
|
||||
implicitWidth: 300
|
||||
implicitHeight: 400
|
||||
|
||||
readonly property string currentItemSource: grid.currentItem ? grid.currentItem.source : ""
|
||||
signal removeImageRequest(var attribute)
|
||||
onMeshFileChanged: viewer3D.clear()
|
||||
property int defaultCellSize: 160
|
||||
|
||||
SystemPalette {
|
||||
id: palette
|
||||
}
|
||||
implicitWidth: 100
|
||||
implicitHeight: 300
|
||||
title: "Images"
|
||||
|
||||
function dirname(filename) {
|
||||
return filename.substring(0, filename.lastIndexOf('/'))
|
||||
}
|
||||
|
||||
Controls1.SplitView {
|
||||
anchors.fill: parent
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
Layout.minimumWidth: grid.cellWidth
|
||||
anchors.fill: parent
|
||||
|
||||
GridView {
|
||||
id: grid
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
cellWidth: thumbnailSizeSlider.value
|
||||
cellHeight: cellWidth * 0.75
|
||||
cellHeight: cellWidth
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
keyNavigationEnabled: true
|
||||
highlightFollowsCurrentItem: true
|
||||
focus: true
|
||||
clip: true
|
||||
|
||||
|
||||
delegate: Item {
|
||||
id: imageDelegate
|
||||
property string source: object.value.get("path").value
|
||||
|
||||
readonly property bool isCurrentItem: grid.currentIndex == index
|
||||
readonly property alias source: _viewpoint.source
|
||||
|
||||
// retrieve viewpoints inner data
|
||||
QtObject {
|
||||
id: _viewpoint
|
||||
readonly property string source: object.value.get("path").value
|
||||
}
|
||||
|
||||
width: grid.cellWidth
|
||||
height: grid.cellHeight
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
source:parent.source
|
||||
sourceSize: Qt.size(100, 100)
|
||||
asynchronous: true
|
||||
autoTransform: true
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
Rectangle {
|
||||
color: Qt.darker(palette.base, 1.15)
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
border.color: palette.highlight
|
||||
border.width: imageMA.containsMouse || grid.currentIndex == index ? 2 : 0
|
||||
z: -1
|
||||
|
||||
MouseArea {
|
||||
id: imageMA
|
||||
anchors.fill: parent
|
||||
anchors.margins: 6
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onPressed: {
|
||||
onPressed: {
|
||||
grid.currentIndex = index
|
||||
if(mouse.button == Qt.RightButton)
|
||||
|
@ -81,23 +69,75 @@ Item {
|
|||
grid.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
Menu {
|
||||
id: imageMenu
|
||||
MenuItem {
|
||||
text: "Show Containing Folder"
|
||||
onClicked: {
|
||||
Qt.openUrlExternally(dirname(imageDelegate.source))
|
||||
Qt.openUrlExternally(Filepath.dirname(imageDelegate.source))
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: "Remove"
|
||||
enabled: !root.readOnly
|
||||
onClicked: removeImageRequest(object)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
// Image thumbnail and background
|
||||
Rectangle {
|
||||
id: imageBackground
|
||||
color: Qt.darker(palette.base, 1.15)
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
border.color: grid.currentIndex == index ? palette.highlight : Qt.darker(palette.highlight)
|
||||
border.width: imageMA.containsMouse || imageDelegate.isCurrentItem ? 2 : 0
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
source: imageDelegate.source
|
||||
sourceSize: Qt.size(100, 100)
|
||||
asynchronous: true
|
||||
autoTransform: true
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
}
|
||||
// Image basename
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
padding: 2
|
||||
font.pointSize: 8
|
||||
elide: Text.ElideMiddle
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Filepath.basename(imageDelegate.source)
|
||||
background: Rectangle {
|
||||
color: imageDelegate.isCurrentItem ? palette.highlight : "transparent"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Explanatory placeholder when no image has been added yet
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
visible: model.count == 0
|
||||
spacing: 4
|
||||
Label {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: MaterialIcons.photo_library
|
||||
font.pointSize: 24
|
||||
font.family: MaterialIcons.fontFamily
|
||||
}
|
||||
Label {
|
||||
text: "Drop Image Files / Folders"
|
||||
}
|
||||
}
|
||||
|
||||
DropArea {
|
||||
id: dropArea
|
||||
anchors.fill: parent
|
||||
|
@ -106,85 +146,41 @@ Item {
|
|||
onDropped: {
|
||||
_reconstruction.handleFilesDrop(drop)
|
||||
}
|
||||
// DropArea overlay
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
opacity: 0.4
|
||||
visible: parent.containsDrag
|
||||
color: palette.highlight
|
||||
}
|
||||
}
|
||||
Pane {
|
||||
Layout.fillWidth: true
|
||||
padding: 2
|
||||
background: Rectangle { color: Qt.darker(palette.window, 1.15) }
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
|
||||
footerContent: RowLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
// Image count
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: model.count + " image" + (model.count > 1 ? "s" : "")
|
||||
padding: 4
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
// Thumbnail size icon and slider
|
||||
Label {
|
||||
text: MaterialIcons.photo_size_select_large
|
||||
font.family: MaterialIcons.fontFamily
|
||||
font.pixelSize: 13
|
||||
}
|
||||
Slider {
|
||||
id: thumbnailSizeSlider
|
||||
from: 70
|
||||
value: 160
|
||||
value: defaultCellSize
|
||||
to: 250
|
||||
implicitWidth: 70
|
||||
Layout.margins: 2
|
||||
}
|
||||
}
|
||||
height: parent.height
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
implicitWidth: Math.round(parent.width * 0.4)
|
||||
Layout.minimumWidth: 40
|
||||
Viewer2D {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
source: grid.count && grid.currentItem ? grid.currentItem.source : ''
|
||||
Rectangle {
|
||||
z: -1
|
||||
anchors.fill: parent
|
||||
color: Qt.darker(palette.base, 1.1)
|
||||
}
|
||||
}
|
||||
}
|
||||
Item {
|
||||
implicitWidth: Math.round(parent.width * 0.3)
|
||||
Layout.minimumWidth: 20
|
||||
|
||||
Viewer3D {
|
||||
id: viewer3D
|
||||
anchors.fill: parent
|
||||
DropArea {
|
||||
anchors.fill: parent
|
||||
onDropped: viewer3D.source = drop.urls[0]
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
text: "Loading Model..."
|
||||
visible: viewer3D.loading
|
||||
padding: 6
|
||||
background: Rectangle { color: palette.base; opacity: 0.5 }
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "3D Model not available"
|
||||
visible: meshFile == ''
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 10
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
padding: 6
|
||||
background: Rectangle { color: palette.base; opacity: 0.5 }
|
||||
}
|
||||
|
||||
// Load reconstructed model
|
||||
Button {
|
||||
text: "Load Model"
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 10
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: meshFile != '' && (viewer3D.source != meshFile)
|
||||
onClicked: viewer3D.source = meshFile
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
82
meshroom/ui/qml/Panel.qml
Normal file
82
meshroom/ui/qml/Panel.qml
Normal file
|
@ -0,0 +1,82 @@
|
|||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
|
||||
/**
|
||||
* Panel is a container control with preconfigured header/footer.
|
||||
*
|
||||
* The header displays an optional icon and the title of the Panel,
|
||||
* and provides a placeholder (headerBar) at the top right corner, useful to create a contextual toolbar.
|
||||
*
|
||||
*
|
||||
* The footer is empty (and not visible) by default. It does not provided any layout.
|
||||
*/
|
||||
Page {
|
||||
id: root
|
||||
|
||||
property alias headerBar: headerLayout.data
|
||||
property alias footerContent: footerLayout.data
|
||||
property alias icon: iconPlaceHolder.data
|
||||
|
||||
QtObject {
|
||||
id: m
|
||||
property int headerHeight: 24
|
||||
property int footerHeight: 22
|
||||
property int hPadding: 6
|
||||
property int vPadding: 2
|
||||
readonly property color paneBackgroundColor: Qt.darker(palette.window, 1.15)
|
||||
}
|
||||
|
||||
padding: 2
|
||||
|
||||
SystemPalette { id: palette }
|
||||
|
||||
header: Pane {
|
||||
id: headerPane
|
||||
topPadding: m.vPadding; bottomPadding: m.vPadding
|
||||
leftPadding: m.hPadding; rightPadding: m.hPadding
|
||||
background: Rectangle { color: m.paneBackgroundColor }
|
||||
|
||||
Item { // Fix the height of the underlying RowLayout
|
||||
implicitHeight: m.headerHeight
|
||||
width: parent.width
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
// Icon
|
||||
Item {
|
||||
id: iconPlaceHolder
|
||||
width: childrenRect.width
|
||||
height: childrenRect.height
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: icon != ""
|
||||
}
|
||||
|
||||
// Title
|
||||
Label {
|
||||
text: root.title
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
//
|
||||
Row { id: headerLayout }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
footer: Pane {
|
||||
id: footerPane
|
||||
topPadding: m.vPadding; bottomPadding: m.vPadding
|
||||
leftPadding: m.hPadding; rightPadding: m.hPadding
|
||||
visible: footerLayout.children.length > 0
|
||||
background: Rectangle { color: m.paneBackgroundColor }
|
||||
|
||||
// Content place holder
|
||||
Item {
|
||||
id: footerLayout
|
||||
width: parent.width
|
||||
implicitHeight: m.footerHeight
|
||||
}
|
||||
}
|
||||
}
|
103
meshroom/ui/qml/WorkspaceView.qml
Normal file
103
meshroom/ui/qml/WorkspaceView.qml
Normal file
|
@ -0,0 +1,103 @@
|
|||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Controls 1.4 as Controls1 // For SplitView
|
||||
import QtQuick.Layouts 1.3
|
||||
import Qt.labs.platform 1.0 as Platform
|
||||
import Viewer 1.0
|
||||
import MaterialIcons 2.2
|
||||
import "filepath.js" as Filepath
|
||||
|
||||
|
||||
/**
|
||||
* WorkspaceView is an aggregation of Meshroom's main modules.
|
||||
*
|
||||
* It contains an ImageGallery, a 2D and a 3D viewer to manipulate and visualize reconstruction data.
|
||||
*/
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property variant reconstruction: _reconstruction
|
||||
readonly property variant viewpoints: _reconstruction.viewpoints
|
||||
readonly property string meshFile: _reconstruction.meshFile
|
||||
property bool readOnly: false
|
||||
|
||||
implicitWidth: 300
|
||||
implicitHeight: 400
|
||||
|
||||
onMeshFileChanged: viewer3D.clear()
|
||||
|
||||
SystemPalette { id: palette }
|
||||
|
||||
Controls1.SplitView {
|
||||
anchors.fill: parent
|
||||
|
||||
ImageGallery {
|
||||
id: imageGallery
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: defaultCellSize
|
||||
model: viewpoints
|
||||
onRemoveImageRequest: reconstruction.removeAttribute(attribute)
|
||||
}
|
||||
|
||||
Panel {
|
||||
title: "Image Viewer"
|
||||
Layout.fillHeight: true
|
||||
implicitWidth: Math.round(parent.width * 0.4)
|
||||
Layout.minimumWidth: 40
|
||||
Viewer2D {
|
||||
anchors.fill: parent
|
||||
source: imageGallery.currentItemSource
|
||||
Rectangle {
|
||||
z: -1
|
||||
anchors.fill: parent
|
||||
color: Qt.darker(palette.base, 1.1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Panel {
|
||||
title: "3D Viewer"
|
||||
implicitWidth: Math.round(parent.width * 0.33)
|
||||
Layout.minimumWidth: 20
|
||||
Layout.minimumHeight: 80
|
||||
|
||||
Viewer3D {
|
||||
id: viewer3D
|
||||
anchors.fill: parent
|
||||
DropArea {
|
||||
anchors.fill: parent
|
||||
onDropped: viewer3D.source = drop.urls[0]
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
text: "Loading Model..."
|
||||
visible: viewer3D.loading
|
||||
padding: 6
|
||||
background: Rectangle { color: palette.base; opacity: 0.5 }
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "3D Model not available"
|
||||
visible: meshFile == ''
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 10
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
padding: 6
|
||||
background: Rectangle { color: palette.base; opacity: 0.5 }
|
||||
}
|
||||
|
||||
// Load reconstructed model
|
||||
Button {
|
||||
text: "Load Model"
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 10
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: meshFile != '' && (viewer3D.source != meshFile)
|
||||
onClicked: viewer3D.source = meshFile
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
meshroom/ui/qml/filepath.js
Normal file
17
meshroom/ui/qml/filepath.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
/* Utility functions to manipulate file paths */
|
||||
|
||||
/// Returns the directory name of the given path.
|
||||
function dirname(path) {
|
||||
return path.substring(0, path.lastIndexOf('/'))
|
||||
}
|
||||
|
||||
/// Returns the basename (file.extension) of the given path.
|
||||
function basename(path) {
|
||||
return path.substring(path.lastIndexOf('/') + 1, path.length)
|
||||
}
|
||||
|
||||
/// Return the extension (prefixed by a '.') of the given path.
|
||||
function extension(path) {
|
||||
var dot_pos = path.lastIndexOf('.');
|
||||
return dot_pos > -1 ? path.substring(dot_pos, path.length) : ""
|
||||
}
|
|
@ -6,6 +6,7 @@ import QtQuick.Window 2.3
|
|||
import QtQml.Models 2.2
|
||||
import Qt.labs.platform 1.0 as Platform
|
||||
import GraphEditor 1.0
|
||||
import MaterialIcons 2.2
|
||||
|
||||
ApplicationWindow {
|
||||
id: _window
|
||||
|
@ -17,7 +18,6 @@ ApplicationWindow {
|
|||
font.pointSize: 9
|
||||
|
||||
property variant node: null
|
||||
|
||||
onClosing: {
|
||||
// make sure document is saved before exiting application
|
||||
close.accepted = false
|
||||
|
@ -29,6 +29,7 @@ ApplicationWindow {
|
|||
SystemPalette { id: palette }
|
||||
SystemPalette { id: disabledPalette; colorGroup: SystemPalette.Disabled}
|
||||
|
||||
|
||||
Dialog {
|
||||
id: unsavedDialog
|
||||
|
||||
|
@ -267,8 +268,10 @@ ApplicationWindow {
|
|||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: false
|
||||
Layout.fillHeight: true
|
||||
Layout.topMargin: 2
|
||||
implicitHeight: Math.round(parent.height * 0.7)
|
||||
spacing: 4
|
||||
Row {
|
||||
enabled: !_reconstruction.computingExternally
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
@ -279,7 +282,7 @@ ApplicationWindow {
|
|||
palette.button: enabled ? buttonColor : disabledPalette.button
|
||||
palette.window: enabled ? buttonColor : disabledPalette.window
|
||||
palette.buttonText: enabled ? "white" : disabledPalette.buttonText
|
||||
enabled: imageGallery.model.count > 2 && !_reconstruction.computing
|
||||
enabled: _reconstruction.viewpoints.count > 2 && !_reconstruction.computing
|
||||
onClicked: _reconstruction.execute(null)
|
||||
}
|
||||
Button {
|
||||
|
@ -289,7 +292,7 @@ ApplicationWindow {
|
|||
}
|
||||
Item { width: 20; height: 1 }
|
||||
Button {
|
||||
enabled: imageGallery.model.count > 2 && !_reconstruction.computing && _reconstruction.graph.filepath != ""
|
||||
enabled: _reconstruction.viewpoints.count > 2 && !_reconstruction.computing && _reconstruction.graph.filepath != ""
|
||||
text: "Submit"
|
||||
onClicked: _reconstruction.submit(null)
|
||||
}
|
||||
|
@ -305,7 +308,7 @@ ApplicationWindow {
|
|||
ListView {
|
||||
id: chunksListView
|
||||
Layout.fillWidth: true
|
||||
height: 10
|
||||
height: 6
|
||||
model: _reconstruction.sortedDFSNodes
|
||||
orientation: ListView.Horizontal
|
||||
interactive: false
|
||||
|
@ -318,31 +321,28 @@ ApplicationWindow {
|
|||
}
|
||||
}
|
||||
|
||||
ImageGallery {
|
||||
WorkspaceView {
|
||||
id: imageGallery
|
||||
property variant node: _reconstruction.graph.nodes.get("CameraInit_1")
|
||||
readOnly: _reconstruction.computing
|
||||
meshFile: _reconstruction.meshFile
|
||||
model: node ? node.attribute("viewpoints").value : undefined
|
||||
reconstruction: _reconstruction
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
onRemoveImageRequest: _reconstruction.removeAttribute(attribute)
|
||||
Layout.minimumHeight: 50
|
||||
}
|
||||
}
|
||||
Panel {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: false
|
||||
height: Math.round(parent.height * 0.3)
|
||||
title: "Graph Editor"
|
||||
|
||||
Controls1.SplitView {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
orientation: Qt.Horizontal
|
||||
anchors.fill: parent
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: 10
|
||||
Row {
|
||||
spacing: 1
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
GraphEditor {
|
||||
id: graphEditor
|
||||
graph: _reconstruction.graph
|
||||
|
@ -350,6 +350,7 @@ ApplicationWindow {
|
|||
Layout.fillWidth: true
|
||||
readOnly: _reconstruction.computing
|
||||
}
|
||||
|
||||
}
|
||||
Item {
|
||||
implicitHeight: Math.round(parent.height * 0.2)
|
||||
|
@ -371,6 +372,4 @@ ApplicationWindow {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue