[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 cameraInits
property variant cameraInit property variant cameraInit
readonly property alias currentItem: grid.currentItem
readonly property string currentItemSource: grid.currentItem ? grid.currentItem.source : "" readonly property string currentItemSource: grid.currentItem ? grid.currentItem.source : ""
readonly property var currentItemMetadata: grid.currentItem ? grid.currentItem.metadata : undefined readonly property var currentItemMetadata: grid.currentItem ? grid.currentItem.metadata : undefined
signal removeImageRequest(var attribute) 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 2.7
import QtQuick.Controls 2.0 import QtQuick.Controls 2.0
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import MaterialIcons 2.2
FocusScope { FocusScope {
id: root id: root
clip: true clip: true
property alias source: image.source property alias source: image.source
property var metadata
// slots // slots
Keys.onPressed: { Keys.onPressed: {
@ -81,7 +83,7 @@ FocusScope {
property double factor: 1.2 property double factor: 1.2
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onPressed: { onPressed: {
root.forceActiveFocus(); image.forceActiveFocus()
if(mouse.button & Qt.MiddleButton) if(mouse.button & Qt.MiddleButton)
drag.target = image // start drag drag.target = image // start drag
} }
@ -105,12 +107,65 @@ FocusScope {
} }
} }
// zoom label // Image Metadata overlay Pane
Label { 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.bottom: parent.bottom
anchors.left: parent.left width: parent.width
anchors.margins: 4 padding: 2
text: (image.status == Image.Ready ? image.scale.toFixed(2) : "1.00") + "x" leftPadding: 4
state: "xsmall" 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 module Viewer
Viewer2D 1.0 Viewer2D.qml Viewer2D 1.0 Viewer2D.qml
ImageMetadataView 1.0 ImageMetadataView.qml
Viewer3D 1.0 Viewer3D.qml Viewer3D 1.0 Viewer3D.qml
DefaultCameraController 1.0 DefaultCameraController.qml DefaultCameraController 1.0 DefaultCameraController.qml
MayaCameraController 1.0 MayaCameraController.qml MayaCameraController 1.0 MayaCameraController.qml

View file

@ -91,11 +91,21 @@ Item {
Viewer2D { Viewer2D {
id: viewer2D id: viewer2D
anchors.fill: parent 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 { DropArea {
anchors.fill: parent anchors.fill: parent
onDropped: viewer2D.source = drop.urls[0] onDropped: {
viewer2D.source = drop.urls[0]
viewer2D.metadata = {}
}
} }
Rectangle { Rectangle {
z: -1 z: -1