Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a grid pattern overlay feature to QField's QML camera #4745

Merged
merged 1 commit into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Comment on lines +378 to +379
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} else {
camera.flash.mode = Camera.FlashOff
}
else if (camera.flash.mod == Camera.FlashOn)
camera.flash.mode = Camera.FlashAuto;
} else {
camera.flash.mode = Camera.FlashOff;

Otherwise I can't find where the FlashAuto mode is set

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@domi4484 , the idea here was to only allow for On/Off, hence why Auto isn't set. We can tweak in a separate PR if that's something you feel is needed.

}
}
}
}

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
Loading