[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:
Yann Lanthony 2017-12-14 19:11:52 +01:00
parent e5acd916dc
commit 8d8bf0be5e
5 changed files with 387 additions and 190 deletions

View file

@ -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
View 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
}
}
}

View 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
}
}
}
}

View 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) : ""
}

View file

@ -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 {
}
}
}
}