Skip to content

Commit

Permalink
Add a grid pattern overlay feature to QField's QML camera
Browse files Browse the repository at this point in the history
  • Loading branch information
nirvn committed Nov 10, 2023
1 parent 5cfafad commit 7fe799e
Show file tree
Hide file tree
Showing 4 changed files with 405 additions and 235 deletions.
1 change: 1 addition & 0 deletions images/images.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<file alias="qfield-love.png">pictures/qfield-love.png</file>
</qresource>
<qresource prefix="/">
<file>themes/qfield/nodpi/ic_3x3_grid_white_24dp.svg</file>
<file>themes/qfield/nodpi/ic_flash_auto_black_24dp.svg</file>
<file>themes/qfield/nodpi/ic_flash_on_black_24dp.svg</file>
<file>themes/qfield/nodpi/ic_flash_off_black_24dp.svg</file>
Expand Down
4 changes: 4 additions & 0 deletions images/themes/qfield/nodpi/ic_3x3_grid_white_24dp.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
301 changes: 190 additions & 111 deletions src/qml/imports/QFieldControls/+Qt5/QFieldCamera.qml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Shapes 1.14
import QtQuick.Window 2.14
import QtMultimedia 5.14
import Qt.labs.settings 1.0
Expand Down Expand Up @@ -45,6 +46,7 @@ Popup {
Settings {
id: settings
property bool geoTagging: true
property bool showGrid: false
}

Page {
Expand Down Expand Up @@ -120,6 +122,7 @@ Popup {
}

VideoOutput {
id: videoOutput
anchors.fill: parent

visible: cameraItem.state == "PhotoCapture" || cameraItem.state == "VideoCapture"
Expand All @@ -130,6 +133,55 @@ Popup {
autoOrientation: true
}

Shape {
id: grid
visible: settings.showGrid
anchors.centerIn: parent

property bool isLandscape: (mainWindow.width / mainWindow.height) > (videoOutput.contentRect.width / videoOutput.contentRect.height)

width: isLandscape
? videoOutput.contentRect.width * mainWindow.height / videoOutput.contentRect.height
: mainWindow.width
height: isLandscape
? mainWindow.height
: videoOutput.contentRect.height * mainWindow.width / videoOutput.contentRect.width

ShapePath {
strokeColor: "#99000000"
strokeWidth: 3
fillColor: "transparent"

startX: grid.width / 3
startY: 0

PathLine { x: grid.width / 3; y: grid.height }
PathMove { x: grid.width / 3 * 2; y: 0 }
PathLine { x: grid.width / 3 * 2; y: grid.height }
PathMove { x: 0; y: grid.height / 3 }
PathLine { x: grid.width; y: grid.height / 3 }
PathMove { x: 0; y: grid.height / 3 * 2 }
PathLine { x: grid.width; y: grid.height / 3 * 2 }
}

ShapePath {
strokeColor: "#AAFFFFFF"
strokeWidth: 1
fillColor: "transparent"

startX: grid.width / 3
startY: 0

PathLine { x: grid.width / 3; y: grid.height }
PathMove { x: grid.width / 3 * 2; y: 0 }
PathLine { x: grid.width / 3 * 2; y: grid.height }
PathMove { x: 0; y: grid.height / 3 }
PathLine { x: grid.width; y: grid.height / 3 }
PathMove { x: 0; y: grid.height / 3 * 2 }
PathLine { x: grid.width; y: grid.height / 3 * 2 }
}
}

MouseArea {
anchors.fill: parent

Expand Down Expand Up @@ -209,97 +261,103 @@ Popup {

Rectangle {
x: cameraItem.isPortraitMode ? 0 : parent.width - 100
y: cameraItem.isPortraitMode ? parent.height - 100 : 0
y: cameraItem.isPortraitMode ? parent.height - 100 - mainWindow.sceneBottomMargin : 0
width: cameraItem.isPortraitMode ? parent.width : 100
height: cameraItem.isPortraitMode ? 100 : parent.height
height: cameraItem.isPortraitMode ? 100 + mainWindow.sceneBottomMargin : parent.height

color: Theme.darkGraySemiOpaque

Rectangle {
id: captureRing
anchors.centerIn: parent
width: 64
height: 64
radius: 32
color: Theme.darkGraySemiOpaque
border.color: cameraItem.state == "VideoCapture" && camera.videoRecorder.recorderState != CameraRecorder.StoppedState
? "red"
: "white"
border.width: 2

QfToolButton {
id: captureButton
anchors.top: parent.top
width: parent.width
height: cameraItem.isPortraitMode ? parent.height - mainWindow.sceneBottomMargin : parent.height
color: "transparent"

Rectangle {
id: captureRing
anchors.centerIn: parent
visible: camera.cameraStatus == Camera.ActiveStatus ||
camera.cameraStatus == Camera.LoadedStatus ||
camera.cameraStatus == Camera.StandbyStatus

round: true
roundborder: true
iconSource: cameraItem.state == "PhotoPreview" || cameraItem.state == "VideoPreview"
? Theme.getThemeIcon("ic_check_white_48dp")
: ''
bgcolor: cameraItem.state == "PhotoPreview" || cameraItem.state == "VideoPreview"
? Theme.mainColor
: cameraItem.state == "VideoCapture" ? "red" : "white"

onClicked: {
if (cameraItem.state == "PhotoCapture") {
camera.imageCapture.captureToLocation(qgisProject.homePath+ '/DCIM/')
currentPosition = positionSource.positionInformation
} else if (cameraItem.state == "VideoCapture") {
if (camera.videoRecorder.recorderState == CameraRecorder.StoppedState) {
camera.videoRecorder.record()
} else {
camera.videoRecorder.stop()
videoPreview.source = camera.videoRecorder.actualLocation
var path = camera.videoRecorder.actualLocation.toString()
var filePos = path.indexOf('file://')
currentPath = filePos === 0 ? path.substring(7) : path
cameraItem.state = "VideoPreview"
}
} else if (cameraItem.state == "PhotoPreview" || cameraItem.state == "VideoPreview") {
if (cameraItem.state == "PhotoPreview") {
if (settings.geoTagging && positionSource.active) {
FileUtils.addImageMetadata(currentPath, currentPosition)
width: 64
height: 64
radius: 32
color: Theme.darkGraySemiOpaque
border.color: cameraItem.state == "VideoCapture" && camera.videoRecorder.recorderState != CameraRecorder.StoppedState
? "red"
: "white"
border.width: 2

QfToolButton {
id: captureButton

anchors.centerIn: parent
visible: camera.cameraStatus == Camera.ActiveStatus ||
camera.cameraStatus == Camera.LoadedStatus ||
camera.cameraStatus == Camera.StandbyStatus

round: true
roundborder: true
iconSource: cameraItem.state == "PhotoPreview" || cameraItem.state == "VideoPreview"
? Theme.getThemeIcon("ic_check_white_48dp")
: ''
bgcolor: cameraItem.state == "PhotoPreview" || cameraItem.state == "VideoPreview"
? Theme.mainColor
: cameraItem.state == "VideoCapture" ? "red" : "white"

onClicked: {
if (cameraItem.state == "PhotoCapture") {
camera.imageCapture.captureToLocation(qgisProject.homePath+ '/DCIM/')
currentPosition = positionSource.positionInformation
} else if (cameraItem.state == "VideoCapture") {
if (camera.videoRecorder.recorderState == CameraRecorder.StoppedState) {
camera.videoRecorder.record()
} else {
camera.videoRecorder.stop()
videoPreview.source = camera.videoRecorder.actualLocation
var path = camera.videoRecorder.actualLocation.toString()
var filePos = path.indexOf('file://')
currentPath = filePos === 0 ? path.substring(7) : path
cameraItem.state = "VideoPreview"
}
} else if (cameraItem.state == "PhotoPreview" || cameraItem.state == "VideoPreview") {
if (cameraItem.state == "PhotoPreview") {
if (settings.geoTagging && positionSource.active) {
FileUtils.addImageMetadata(currentPath, currentPosition)
}
}
cameraItem.finished(currentPath)
}
cameraItem.finished(currentPath)
}
}
}
}

QfToolButton {
id: zoomButton
visible: cameraItem.isCapturing
QfToolButton {
id: zoomButton
visible: cameraItem.isCapturing

x: cameraItem.isPortraitMode ? (parent.width / 4) - (width / 2) : (parent.width - width) / 2
y: cameraItem.isPortraitMode ? (parent.height - height) / 2 : (parent.height / 4) * 3 - (height / 2)
x: cameraItem.isPortraitMode ? (parent.width / 4) - (width / 2) : (parent.width - width) / 2
y: cameraItem.isPortraitMode ? (parent.height - height) / 2 : (parent.height / 4) * 3 - (height / 2)

iconColor: "white"
bgcolor: Theme.darkGraySemiOpaque
round: true
iconColor: "white"
bgcolor: Theme.darkGraySemiOpaque
round: true

text: (camera.digitalZoom * camera.opticalZoom).toFixed(1) +'X'
font: Theme.tinyFont
text: (camera.digitalZoom * camera.opticalZoom).toFixed(1) +'X'
font: Theme.tinyFont

onClicked: {
camera.opticalZoom = 1;
camera.digitalZoom = 1;
onClicked: {
camera.opticalZoom = 1;
camera.digitalZoom = 1;
}
}
}

QfToolButton {
id: flashButton
visible: cameraItem.isCapturing && camera.flash.supportedModes.length > 1
QfToolButton {
id: flashButton
visible: cameraItem.isCapturing && camera.flash.supportedModes.length > 1

x: cameraItem.isPortraitMode ? (parent.width / 4) * 3 - (width / 2) : (parent.width - width) / 2
y: cameraItem.isPortraitMode ? (parent.height - height) / 2 : (parent.height / 4) - (height / 2)
x: cameraItem.isPortraitMode ? (parent.width / 4) * 3 - (width / 2) : (parent.width - width) / 2
y: cameraItem.isPortraitMode ? (parent.height - height) / 2 : (parent.height / 4) - (height / 2)

iconSource: {
switch(camera.flash.mode) {
iconSource: {
switch(camera.flash.mode) {
case Camera.FlashAuto:
return Theme.getThemeVectorIcon('ic_flash_auto_black_24dp');
case Camera.FlashOn:
Expand All @@ -308,56 +366,58 @@ Popup {
return Theme.getThemeVectorIcon('ic_flash_off_black_24dp');
default:
return'';
}
}
}
iconColor: "white"
bgcolor: Qt.hsla(Theme.darkGray.hslHue, Theme.darkGray.hslSaturation, Theme.darkGray.hslLightness, 0.5)
round: true
iconColor: "white"
bgcolor: Qt.hsla(Theme.darkGray.hslHue, Theme.darkGray.hslSaturation, Theme.darkGray.hslLightness, 0.5)
round: true

onClicked: {
if (camera.flash.mode == Camera.FlashOff) {
camera.flash.mode = Camera.FlashOn;
} else {
camera.flash.mode = Camera.FlashOff
onClicked: {
if (camera.flash.mode == Camera.FlashOff) {
camera.flash.mode = Camera.FlashOn;
} else {
camera.flash.mode = Camera.FlashOff
}
}
}
}

Rectangle {
visible: cameraItem.state == "VideoCapture" && camera.videoRecorder.recorderState != CameraRecorder.StoppedState

x: cameraItem.isPortraitMode ? captureRing.x + captureRing.width / 2 - width / 2 : captureRing.x + captureRing.width / 2 - width / 2
y: cameraItem.isPortraitMode ? captureRing.y - height - 20 : captureRing.y - height - 20

width: durationLabelMetrics.boundingRect('00:00:00').width + 20
height: durationLabelMetrics.boundingRect('00:00:00').height + 10
radius: 6

color: 'red'

Text {
id: durationLabel
anchors.centerIn: parent
text: {
if (camera.videoRecorder.duration > 0) {
var seconds = Math.ceil(camera.videoRecorder.duration / 1000);
var hours = Math.floor(seconds / 60 / 60) + '';
seconds -= hours * 60 * 60;
var minutes = Math.floor(seconds / 60) + '';
seconds = (seconds - minutes * 60) + '';
return hours.padStart(2,'0') + ':' + minutes.padStart(2,'0') + ':' + seconds.padStart(2,'0');
} else {
// tiny bit of a cheat here as the first second isn't triggered
return '00:00:01';
Rectangle {
visible: cameraItem.state == "VideoCapture" && camera.videoRecorder.recorderState != CameraRecorder.StoppedState

x: cameraItem.isPortraitMode ? captureRing.x + captureRing.width / 2 - width / 2 : captureRing.x + captureRing.width / 2 - width / 2
y: cameraItem.isPortraitMode ? captureRing.y - height - 20 : captureRing.y - height - 20

width: durationLabelMetrics.boundingRect('00:00:00').width + 20
height: durationLabelMetrics.boundingRect('00:00:00').height + 10
radius: 6

color: 'red'

Text {
id: durationLabel
anchors.centerIn: parent
text: {
if (camera.videoRecorder.duration > 0) {
var seconds = Math.ceil(camera.videoRecorder.duration / 1000);
var hours = Math.floor(seconds / 60 / 60) + '';
seconds -= hours * 60 * 60;
var minutes = Math.floor(seconds / 60) + '';
seconds = (seconds - minutes * 60) + '';
return hours.padStart(2,'0') + ':' + minutes.padStart(2,'0') + ':' + seconds.padStart(2,'0');
} else {
// tiny bit of a cheat here as the first second isn't triggered
return '00:00:01';
}
}
color: 'white'
}
color: 'white'
}

FontMetrics {
id: durationLabelMetrics
font: durationLabel.font
FontMetrics {
id: durationLabelMetrics
font: durationLabel.font
}
}

}
}

Expand Down Expand Up @@ -407,5 +467,24 @@ Popup {
}
}
}

QfToolButton {
id: gridButton

anchors.left: parent.left
anchors.leftMargin: 4
anchors.top: geotagButton.bottom
anchors.topMargin: 4

iconSource: Theme.getThemeVectorIcon("ic_3x3_grid_white_24dp")
iconColor: settings.showGrid ? Theme.mainColor : "white"
bgcolor: Theme.darkGraySemiOpaque
round: true

onClicked: {
settings.showGrid = !settings.showGrid
displayToast(settings.showGrid ? qsTr("Grid enabled") : qsTr("Grid disabled"))
}
}
}
}
Loading

0 comments on commit 7fe799e

Please sign in to comment.