[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
This commit is contained in:
Yann Lanthony 2018-02-15 16:04:25 +01:00
parent d91601ca8e
commit 53764812bd
5 changed files with 287 additions and 10 deletions

View file

@ -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)

View file

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

View file

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

View file

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

View file

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