From 53764812bd12bb47d487ce70a0fa61f3b71ff72c Mon Sep 17 00:00:00 2001 From: Yann Lanthony Date: Thu, 15 Feb 2018 16:04:25 +0100 Subject: [PATCH] [ui] add ImageMetadataView + integration in Viewer2D * display image metadata as a sorted table view with filtering * 2DViewer: new bottom toolbar with metadata toggle + image resolution --- meshroom/ui/qml/ImageGallery.qml | 1 + meshroom/ui/qml/Viewer/ImageMetadataView.qml | 210 +++++++++++++++++++ meshroom/ui/qml/Viewer/Viewer2D.qml | 69 +++++- meshroom/ui/qml/Viewer/qmldir | 1 + meshroom/ui/qml/WorkspaceView.qml | 16 +- 5 files changed, 287 insertions(+), 10 deletions(-) create mode 100644 meshroom/ui/qml/Viewer/ImageMetadataView.qml diff --git a/meshroom/ui/qml/ImageGallery.qml b/meshroom/ui/qml/ImageGallery.qml index c852275b..ca94e7b2 100644 --- a/meshroom/ui/qml/ImageGallery.qml +++ b/meshroom/ui/qml/ImageGallery.qml @@ -14,6 +14,7 @@ Panel { property variant cameraInits property variant cameraInit + readonly property alias currentItem: grid.currentItem readonly property string currentItemSource: grid.currentItem ? grid.currentItem.source : "" readonly property var currentItemMetadata: grid.currentItem ? grid.currentItem.metadata : undefined signal removeImageRequest(var attribute) diff --git a/meshroom/ui/qml/Viewer/ImageMetadataView.qml b/meshroom/ui/qml/Viewer/ImageMetadataView.qml new file mode 100644 index 00000000..04edbe0e --- /dev/null +++ b/meshroom/ui/qml/Viewer/ImageMetadataView.qml @@ -0,0 +1,210 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 +import MaterialIcons 2.2 +import QtPositioning 5.8 +import QtLocation 5.9 + +import Utils 1.0 + +/** + * ImageMetadataView displays a JSON model representing an image"s metadata as a ListView. + */ +Pane { + id: root + + property alias metadata: metadataModel.metadata + property var coordinates: QtPositioning.coordinate() + + clip: true + padding: 4 + + SystemPalette { id: palette } + + background: Rectangle { color: palette.window; opacity: 0.9 } + + /** + * Convert GPS metadata to degree coordinates. + * + * GPS coordinates in metadata can be store in 3 forms: + * (degrees), (degrees, minutes), (degrees, minutes, seconds) + */ + function gpsMetadataToCoordinates(value, ref) + { + var values = value.split(",") + var result = 0 + for(var i=0; i < values.length; ++i) + { + // divide each component by the corresponding power of 60 + // 1 for degree, 60 for minutes, 3600 for seconds + result += Number(values[i]) / Math.pow(60, i) + } + // handle opposite reference: South (latitude) or West (longitude) + return (ref === "S" || ref === "W") ? -result : result + } + + /// Try to get GPS coordinates from metadata + function getGPSCoordinates(metadata) + { + // GPS data available + if(metadata["GPS:Longitude"] != undefined && metadata["GPS:Latitude"] != undefined) + { + var latitude = gpsMetadataToCoordinates(metadata["GPS:Latitude"], metadata["GPS:LatitudeRef"]) + var longitude = gpsMetadataToCoordinates(metadata["GPS:Longitude"], metadata["GPS:LongitudeRef"]) + var altitude = metadata["GPS:Altitude"] || 0 + return QtPositioning.coordinate(latitude, longitude, altitude) + } + // GPS data unavailable: reset coordinates to default value + else + { + return QtPositioning.coordinate() + } + } + + // Metadata model + // Available roles for child items: + // - group: metadata group if any, "-" otherwise + // - key: metadata key + // - value: metadata value + // - raw: a sortable/filterable representation of the metadata as "group:key=value" + ListModel { + id: metadataModel + property var metadata: ({}) + + // reset model when metadata changes + onMetadataChanged: { + metadataModel.clear() + var entries = [] + // prepare data to populate the model from the input metadata object + for(var key in metadata) + { + var entry = {} + entry["raw"] = key + // split on ":" to get group and key + var sKey = key.split(":", 2) + if(sKey.length === 2) + { + entry["group"] = sKey[0] + entry["key"] = sKey[1] + } + else + { + // set default group to something convenient for sorting + entry["group"] = "-" + entry["key"] = key + } + entry["value"] = metadata[key] + entry["raw"] = entry["group"] + ":" + entry["key"] + "=" + entry["value"] + entries.push(entry) + } + // reset the model with prepared data (limit to one update event) + metadataModel.append(entries) + coordinates = getGPSCoordinates(metadata) + } + } + +// Button { +// onClicked: { +// if(sortedMetadataModel.sortOrder == Qt.DescendingOrder) +// sortedMetadataModel.sortOrder = Qt.AscendingOrder +// else +// sortedMetadataModel.sortOrder = Qt.DescendingOrder +// } +// } + + // Background WheelEvent grabber + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.MiddleButton + onWheel: wheel.accepted = true + } + + // SortFilter delegate over the metadataModel + SortFilterDelegateModel { + id: sortedMetadataModel + model: metadataModel + sortRole: "raw" + filterRole: "raw" + textFilter: filter.text + delegate: RowLayout { + width: parent.width + Label { + text: key + leftPadding: 6 + rightPadding: 4 + Layout.preferredWidth: sizeHandle.x + + elide: Text.ElideRight + } + Label { + text: value + Layout.fillWidth: true + wrapMode: Label.WrapAtWordBoundaryOrAnywhere + } + } + } + + // Main Layout + ColumnLayout { + anchors.fill: parent + + // Search toolbar + RowLayout { + Label { + text: MaterialIcons.search + font.family: MaterialIcons.fontFamily + } + TextField { + id: filter + Layout.fillWidth: true + z: 2 + } + } + + // Metadata ListView + ListView { + id: metadataView + Layout.fillWidth: true + Layout.fillHeight: true + model: sortedMetadataModel + spacing: 3 + clip: true + + // Categories resize handle + Rectangle { + id: sizeHandle + height: parent.contentHeight + width: 1 + color: palette.mid + x: parent.width * 0.4 + MouseArea { + anchors.fill: parent + anchors.margins: -4 + cursorShape: Qt.SizeHorCursor + drag { + target: parent + axis: Drag.XAxis + threshold: 0 + minimumX: metadataView.width * 0.2 + maximumX: metadataView.width * 0.8 + } + } + } + // Display section based on metadata group + section.property: "group" + section.delegate: Pane { + width: parent.width + padding: 3 + background: null + + Label { + width: parent.width + padding: 2 + background: Rectangle { color: palette.mid } + text: section + } + } + ScrollBar.vertical: ScrollBar{} + } + } +} diff --git a/meshroom/ui/qml/Viewer/Viewer2D.qml b/meshroom/ui/qml/Viewer/Viewer2D.qml index 8fc02103..d77cf764 100644 --- a/meshroom/ui/qml/Viewer/Viewer2D.qml +++ b/meshroom/ui/qml/Viewer/Viewer2D.qml @@ -1,12 +1,14 @@ import QtQuick 2.7 import QtQuick.Controls 2.0 import QtQuick.Layouts 1.3 +import MaterialIcons 2.2 FocusScope { id: root clip: true property alias source: image.source + property var metadata // slots Keys.onPressed: { @@ -81,7 +83,7 @@ FocusScope { property double factor: 1.2 acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton onPressed: { - root.forceActiveFocus(); + image.forceActiveFocus() if(mouse.button & Qt.MiddleButton) drag.target = image // start drag } @@ -105,12 +107,65 @@ FocusScope { } } - // zoom label - Label { + // Image Metadata overlay Pane + ImageMetadataView { + width: 350 + anchors { + top: parent.top + right: parent.right + bottom: bottomToolbar.top + margins: 2 + } + + visible: metadataCB.checked + // only load metadata model if visible + metadata: visible ? root.metadata : {} + } + + Pane { + id: bottomToolbar anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.margins: 4 - text: (image.status == Image.Ready ? image.scale.toFixed(2) : "1.00") + "x" - state: "xsmall" + width: parent.width + padding: 2 + leftPadding: 4 + rightPadding: leftPadding + + background: Rectangle { color: palette.base; opacity: 0.6 } + + RowLayout { + anchors.fill: parent + + // zoom label + Label { + text: (image.status == Image.Ready ? image.scale.toFixed(2) : "1.00") + "x" + state: "xsmall" + } + + Item { + Layout.fillWidth: true + Label { + id: resolutionLabel + text: image.sourceSize.width + "x" + image.sourceSize.height + anchors.centerIn: parent + elide: Text.ElideMiddle + } + } + + ToolButton { + id: metadataCB + padding: 3 + + font.family: MaterialIcons.fontFamily + text: MaterialIcons.info_outline + + ToolTip.text: "Image Metadata" + ToolTip.visible: hovered + + font.pointSize: 12 + smooth: false + flat: true + checkable: enabled + } + } } } diff --git a/meshroom/ui/qml/Viewer/qmldir b/meshroom/ui/qml/Viewer/qmldir index 2f9a7711..f0788027 100644 --- a/meshroom/ui/qml/Viewer/qmldir +++ b/meshroom/ui/qml/Viewer/qmldir @@ -1,6 +1,7 @@ module Viewer Viewer2D 1.0 Viewer2D.qml +ImageMetadataView 1.0 ImageMetadataView.qml Viewer3D 1.0 Viewer3D.qml DefaultCameraController 1.0 DefaultCameraController.qml MayaCameraController 1.0 MayaCameraController.qml diff --git a/meshroom/ui/qml/WorkspaceView.qml b/meshroom/ui/qml/WorkspaceView.qml index 8d3d2cae..2803d7f3 100644 --- a/meshroom/ui/qml/WorkspaceView.qml +++ b/meshroom/ui/qml/WorkspaceView.qml @@ -91,11 +91,21 @@ Item { Viewer2D { id: viewer2D anchors.fill: parent - property url imageGallerySource: imageGallery.currentItemSource - onImageGallerySourceChanged: viewer2D.source = imageGallerySource + + Connections { + target: imageGallery + onCurrentItemChanged: { + viewer2D.source = imageGallery.currentItemSource + viewer2D.metadata = imageGallery.currentItemMetadata + } + } + DropArea { anchors.fill: parent - onDropped: viewer2D.source = drop.urls[0] + onDropped: { + viewer2D.source = drop.urls[0] + viewer2D.metadata = {} + } } Rectangle { z: -1