diff --git a/qfieldsync/core/layer.py b/qfieldsync/core/layer.py
index 4b4e3cea..a52b7b3b 100644
--- a/qfieldsync/core/layer.py
+++ b/qfieldsync/core/layer.py
@@ -71,16 +71,24 @@ def __init__(self, layer):
self.layer = layer
self._action = None
self._photo_naming = {}
+ self._is_geometry_locked = None
self.read_layer()
def read_layer(self):
self._action = self.layer.customProperty('QFieldSync/action')
self._photo_naming = json.loads(self.layer.customProperty('QFieldSync/photo_naming') or '{}')
+ self._is_geometry_locked = self.layer.customProperty('QFieldSync/is_geometry_locked', False)
def apply(self):
self.layer.setCustomProperty('QFieldSync/action', self.action)
self.layer.setCustomProperty('QFieldSync/photo_naming', json.dumps(self._photo_naming))
+ # custom properties does not store the data type, so it is safer to remove boolean custom properties, rather than setting them to the string 'false' (which is boolean `True`)
+ if self.is_geometry_locked:
+ self.layer.setCustomProperty('QFieldSync/is_geometry_locked', True)
+ else:
+ self.layer.removeCustomProperty('QFieldSync/is_geometry_locked')
+
@property
def action(self):
if self._action is None:
@@ -151,6 +159,18 @@ def is_supported(self):
else:
return True
+ @property
+ def can_lock_geometry(self):
+ return self.layer.type() == QgsMapLayer.VectorLayer
+
+ @property
+ def is_geometry_locked(self):
+ return bool(self._is_geometry_locked)
+
+ @is_geometry_locked.setter
+ def is_geometry_locked(self, is_geometry_locked):
+ self._is_geometry_locked = is_geometry_locked
+
@property
def warning(self):
if self.layer.source().endswith(('jp2', 'jpx')):
diff --git a/qfieldsync/gui/map_layer_config_widget.py b/qfieldsync/gui/map_layer_config_widget.py
new file mode 100644
index 00000000..f375805c
--- /dev/null
+++ b/qfieldsync/gui/map_layer_config_widget.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+"""
+/***************************************************************************
+ QFieldSyncDialog
+ A QGIS plugin
+ Sync your projects to QField on android
+ -------------------
+ begin : 2020-06-15
+ git sha : $Format:%H$
+ copyright : (C) 2020 by OPENGIS.ch
+ email : info@opengis.ch
+ ***************************************************************************/
+
+/***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+"""
+import os
+
+from qgis.core import Qgis, QgsProject, QgsMapLayer
+from qgis.gui import QgsMapLayerConfigWidget, QgsMapLayerConfigWidgetFactory
+
+from qgis.PyQt.uic import loadUiType
+
+from qfieldsync.core.layer import LayerSource
+from qfieldsync.gui.photo_naming_widget import PhotoNamingTableWidget
+from qfieldsync.gui.utils import set_available_actions
+
+WidgetUi, _ = loadUiType(os.path.join(os.path.dirname(__file__), '../ui/map_layer_config_widget.ui'))
+
+
+class MapLayerConfigWidgetFactory(QgsMapLayerConfigWidgetFactory):
+ def __init__(self, title, icon):
+ super(MapLayerConfigWidgetFactory, self).__init__(title, icon)
+
+
+ def createWidget(self, layer, canvas, dock_widget, parent):
+ return MapLayerConfigWidget(layer, canvas, parent)
+
+
+ def supportsLayer(self, layer):
+ return LayerSource(layer).is_supported
+
+
+ def supportLayerPropertiesDialog(self):
+ return True
+
+
+class MapLayerConfigWidget(QgsMapLayerConfigWidget, WidgetUi):
+ def __init__(self, layer, canvas, parent):
+ super(MapLayerConfigWidget, self).__init__(layer, canvas, parent)
+ self.setupUi(self)
+ self.layer_source = LayerSource(layer)
+ self.project = QgsProject.instance()
+
+ set_available_actions(self.layerActionComboBox, self.layer_source)
+
+ self.isGeometryLockedCheckBox.setEnabled(self.layer_source.can_lock_geometry)
+ self.isGeometryLockedCheckBox.setChecked(self.layer_source.is_geometry_locked)
+ self.photoNamingTable = PhotoNamingTableWidget()
+ self.photoNamingTable.addLayerFields(self.layer_source)
+ self.photoNamingTable.setLayerColumnHidden(True)
+
+ # insert the table as a second row only for vector layers
+ if Qgis.QGIS_VERSION_INT >= 31300 and layer.type() == QgsMapLayer.VectorLayer:
+ self.layout().insertRow(1, self.tr('Photo Naming'), self.photoNamingTable)
+ self.photoNamingTable.setEnabled(self.photoNamingTable.rowCount() > 0)
+
+
+ def apply(self):
+ old_layer_action = self.layer_source.action
+ old_is_geometry_locked = self.layer_source.is_geometry_locked
+
+ self.layer_source.action = self.layerActionComboBox.itemData(self.layerActionComboBox.currentIndex())
+ self.layer_source.is_geometry_locked = self.isGeometryLockedCheckBox.isChecked()
+ self.photoNamingTable.syncLayerSourceValues()
+
+ # apply always the photo_namings (to store default values on first apply as well)
+ if (self.layer_source.action != old_layer_action or
+ self.layer_source.is_geometry_locked != old_is_geometry_locked or
+ self.photoNamingTable.rowCount() > 0
+ ):
+ self.layer_source.apply()
+ self.project.setDirty(True)
diff --git a/qfieldsync/gui/photo_naming_widget.py b/qfieldsync/gui/photo_naming_widget.py
new file mode 100644
index 00000000..68402a41
--- /dev/null
+++ b/qfieldsync/gui/photo_naming_widget.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+"""
+/***************************************************************************
+ QFieldSyncDialog
+ A QGIS plugin
+ Sync your projects to QField on android
+ -------------------
+ begin : 2020-06-15
+ git sha : $Format:%H$
+ copyright : (C) 2020 by OPENGIS.ch
+ email : info@opengis.ch
+ ***************************************************************************/
+
+/***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+"""
+
+from qgis.core import QgsMapLayer
+from qgis.gui import QgsFieldExpressionWidget
+
+from qgis.PyQt.QtCore import Qt
+from qgis.PyQt.QtWidgets import QTableWidget, QTableWidgetItem, QAbstractScrollArea
+
+
+class PhotoNamingTableWidget(QTableWidget):
+ def __init__(self):
+ super(PhotoNamingTableWidget, self).__init__()
+
+ self.setColumnCount(3)
+ self.setHorizontalHeaderLabels([self.tr('Layer'), self.tr('Field'), self.tr('Naming Expression')])
+ self.horizontalHeaderItem(2).setToolTip(self.tr('Enter expression for a file path with the extension .jpg'))
+ self.horizontalHeader().setStretchLastSection(True)
+ self.setRowCount(0)
+ self.resizeColumnsToContents()
+ self.setMinimumHeight(100)
+ self.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents)
+
+
+ def addLayerFields(self, layer_source):
+ layer = layer_source.layer
+
+ if layer.type() != QgsMapLayer.VectorLayer:
+ return
+
+ for i, field in enumerate(layer.fields()):
+ row = self.rowCount()
+ ews = layer.editorWidgetSetup(i)
+
+ if ews.type() == 'ExternalResource':
+ # for later: if ews.config().get('DocumentViewer', QgsExternalResourceWidget.NoContent) == QgsExternalResourceWidget.Image:
+ self.insertRow(row)
+ item = QTableWidgetItem(layer.name())
+ item.setData(Qt.UserRole, layer_source)
+ self.setItem(row, 0, item)
+ item = QTableWidgetItem(field.name())
+ self.setItem(row, 1, item)
+ ew = QgsFieldExpressionWidget()
+ ew.setLayer(layer)
+ expression = layer_source.photo_naming(field.name())
+ ew.setExpression(expression)
+ self.setCellWidget(row, 2, ew)
+
+ self.resizeColumnsToContents()
+
+
+ def setLayerColumnHidden(self, is_hidden):
+ self.setColumnHidden(0, is_hidden)
+
+
+ def syncLayerSourceValues(self, should_apply=False):
+ for i in range(self.rowCount()):
+ layer_source = self.item(i, 0).data(Qt.UserRole)
+ field_name = self.item(i, 1).text()
+ new_expression = self.cellWidget(i, 2).currentText()
+ layer_source.set_photo_naming(field_name, new_expression)
+
+ if should_apply:
+ layer_source.apply()
diff --git a/qfieldsync/gui/project_configuration_dialog.py b/qfieldsync/gui/project_configuration_dialog.py
index aa062be7..5ecd094d 100644
--- a/qfieldsync/gui/project_configuration_dialog.py
+++ b/qfieldsync/gui/project_configuration_dialog.py
@@ -21,15 +21,16 @@
from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtGui import QIcon
-from qgis.PyQt.QtWidgets import QDialog, QTableWidgetItem, QToolButton, QComboBox, QMenu, QAction
+from qgis.PyQt.QtWidgets import QDialog, QTableWidgetItem, QToolButton, QComboBox, QCheckBox, QMenu, QAction, QWidget, QHBoxLayout
from qgis.PyQt.uic import loadUiType
-from qgis.core import QgsProject, QgsMapLayerProxyModel, QgsMapLayer, Qgis
-from qgis.gui import QgsFieldExpressionWidget
+from qgis.core import QgsProject, QgsMapLayerProxyModel, Qgis
from qfieldsync.core import ProjectConfiguration
from qfieldsync.core.layer import LayerSource, SyncAction
from qfieldsync.core.project import ProjectProperties
+from qfieldsync.gui.photo_naming_widget import PhotoNamingTableWidget
+from qfieldsync.gui.utils import set_available_actions
DialogUi, _ = loadUiType(
@@ -83,6 +84,10 @@ def reloadProject(self):
Load all layers from the map layer registry into the table.
"""
self.unsupportedLayersList = list()
+
+ self.photoNamingTable = PhotoNamingTableWidget()
+ self.photoNamingTab.layout().addWidget(self.photoNamingTable)
+
self.layersTable.setRowCount(0)
self.layersTable.setSortingEnabled(False)
for layer in self.project.mapLayers().values():
@@ -96,48 +101,32 @@ def reloadProject(self):
item.setData(Qt.EditRole, layer.name())
self.layersTable.setItem(count, 0, item)
- cbx = QComboBox()
- for action, description in layer_source.available_actions:
- cbx.addItem(description)
- cbx.setItemData(cbx.count() - 1, action)
- if layer_source.action == action:
- cbx.setCurrentIndex(cbx.count() - 1)
-
- self.layersTable.setCellWidget(count, 1, cbx)
+ cmb = QComboBox()
+ set_available_actions(cmb, layer_source)
+
+ cbx = QCheckBox()
+ cbx.setEnabled(layer_source.can_lock_geometry)
+ cbx.setChecked(layer_source.is_geometry_locked)
+ # it's more UI friendly when the checkbox is centered, an ugly workaround to achieve it
+ cbx_widget = QWidget()
+ cbx_layout = QHBoxLayout()
+ cbx_layout.setAlignment(Qt.AlignCenter)
+ cbx_layout.setContentsMargins(0, 0, 0, 0)
+ cbx_layout.addWidget(cbx)
+ cbx_widget.setLayout(cbx_layout)
+ # NOTE the margin is not updated when the table column is resized, so better rely on the code above
+ # cbx.setStyleSheet("margin-left:50%; margin-right:50%;")
+
+ self.layersTable.setCellWidget(count, 1, cbx_widget)
+ self.layersTable.setCellWidget(count, 2, cmb)
+
+ # make sure layer_source is the same instance everywhere
+ self.photoNamingTable.addLayerFields(layer_source)
self.layersTable.resizeColumnsToContents()
self.layersTable.sortByColumn(0, Qt.AscendingOrder)
self.layersTable.setSortingEnabled(True)
- self.photoResourceTable.setColumnCount(3)
- self.photoResourceTable.setHorizontalHeaderLabels([self.tr('Layer'), self.tr('Field'), self.tr('Naming Expression')])
- self.photoResourceTable.horizontalHeaderItem(2).setToolTip(self.tr('Enter expression for a file path with the extension .jpg'))
- self.photoResourceTable.horizontalHeader().setStretchLastSection(True)
- self.photoResourceTable.setRowCount(0)
- row = 0
- for layer in self.project.instance().mapLayers().values():
- if layer.type() == QgsMapLayer.VectorLayer:
- layer_source = LayerSource(layer)
- i = 0
- for field in layer.fields():
- ews = layer.editorWidgetSetup(i)
- i += 1
- if ews.type() == 'ExternalResource':
- # for later: if ews.config().get('DocumentViewer', QgsExternalResourceWidget.NoContent) == QgsExternalResourceWidget.Image:
- self.photoResourceTable.insertRow(row)
- item = QTableWidgetItem(layer.name())
- item.setData(Qt.UserRole, layer_source)
- self.photoResourceTable.setItem(row, 0, item)
- item = QTableWidgetItem(field.name())
- self.photoResourceTable.setItem(row, 1, item)
- ew = QgsFieldExpressionWidget()
- ew.setLayer(layer)
- expression = layer_source.photo_naming(field.name())
- ew.setExpression(expression)
- self.photoResourceTable.setCellWidget(row, 2, ew)
- row += 1
- self.photoResourceTable.resizeColumnsToContents()
-
# Remove the tab when not yet suported in QGIS
if Qgis.QGIS_VERSION_INT < 31300:
self.tabWidget.removeTab(self.tabWidget.count() - 1)
@@ -184,22 +173,23 @@ def onAccepted(self):
for i in range(self.layersTable.rowCount()):
item = self.layersTable.item(i, 0)
layer_source = item.data(Qt.UserRole)
- cbx = self.layersTable.cellWidget(i, 1)
+ cbx = self.layersTable.cellWidget(i, 1).layout().itemAt(0).widget()
+ cmb = self.layersTable.cellWidget(i, 2)
old_action = layer_source.action
- layer_source.action = cbx.itemData(cbx.currentIndex())
- if layer_source.action != old_action:
+ old_is_geometry_locked = layer_source.can_lock_geometry and layer_source.is_geometry_locked
+
+ layer_source.action = cmb.itemData(cmb.currentIndex())
+ layer_source.is_geometry_locked = cbx.isChecked()
+
+ if layer_source.action != old_action or layer_source.is_geometry_locked != old_is_geometry_locked:
self.project.setDirty(True)
layer_source.apply()
- for i in range(self.photoResourceTable.rowCount()):
- layer_source: LayerSource = self.photoResourceTable.item(i, 0).data(Qt.UserRole)
- field_name = self.photoResourceTable.item(i, 1).text()
- old_expression = layer_source.photo_naming(field_name)
- new_expression = self.photoResourceTable.cellWidget(i, 2).currentText()
- layer_source.set_photo_naming(field_name, new_expression)
+ # apply always the photo_namings (to store default values on first apply as well)
+ self.photoNamingTable.syncLayerSourceValues(should_apply=True)
+ if self.photoNamingTable.rowCount() > 0:
self.project.setDirty(True)
- layer_source.apply()
self.__project_configuration.create_base_map = self.createBaseMapGroupBox.isChecked()
self.__project_configuration.base_map_theme = self.mapThemeComboBox.currentText()
diff --git a/qfieldsync/gui/utils.py b/qfieldsync/gui/utils.py
new file mode 100644
index 00000000..68987a0a
--- /dev/null
+++ b/qfieldsync/gui/utils.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+"""
+/***************************************************************************
+ QFieldSyncDialog
+ A QGIS plugin
+ Sync your projects to QField on android
+ -------------------
+ begin : 2020-06-15
+ git sha : $Format:%H$
+ copyright : (C) 2020 by OPENGIS.ch
+ email : info@opengis.ch
+ ***************************************************************************/
+
+/***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+"""
+
+def set_available_actions(combobox, layer_source):
+ """Sets available actions on a checkbox and selects the current one.
+
+ Args:
+ combobox (QComboBox): target combobox
+ layer_source (LayerSource): target layer
+ """
+ for action, description in layer_source.available_actions:
+ combobox.addItem(description)
+ combobox.setItemData(combobox.count() - 1, action)
+
+ if layer_source.action == action:
+ combobox.setCurrentIndex(combobox.count() - 1)
diff --git a/qfieldsync/qfield_sync.py b/qfieldsync/qfield_sync.py
index dc7665e5..6344787e 100644
--- a/qfieldsync/qfield_sync.py
+++ b/qfieldsync/qfield_sync.py
@@ -40,6 +40,7 @@
from qfieldsync.gui.preferences_dialog import PreferencesDialog
from qfieldsync.gui.synchronize_dialog import SynchronizeDialog
from qfieldsync.gui.project_configuration_dialog import ProjectConfigurationDialog
+from qfieldsync.gui.map_layer_config_widget import MapLayerConfigWidgetFactory
class QFieldSync(object):
@@ -80,6 +81,9 @@ def __init__(self, iface):
self.toolbar = self.iface.addToolBar(u'QFieldSync')
self.toolbar.setObjectName(u'QFieldSync')
+ # instance of the map config widget factory, shown in layer properties
+ self.mapLayerConfigWidgetFactory = MapLayerConfigWidgetFactory('QField', QIcon(os.path.join(os.path.dirname(__file__), 'resources/icon.png')))
+
# instance of the QgsOfflineEditing
self.offline_editing = QgsOfflineEditing()
self.preferences = Preferences()
@@ -193,7 +197,7 @@ def initGui(self):
parent=self.iface.mainWindow())
self.add_action(
- './resources/icon.png',
+ os.path.join(os.path.dirname(__file__), './resources/icon.png'),
text=self.tr(u'Project Configuration'),
callback=self.show_project_configuration_dialog,
parent=self.iface.mainWindow(),
@@ -201,12 +205,14 @@ def initGui(self):
)
self.add_action(
- './resources/icon.png',
+ os.path.join(os.path.dirname(__file__), './resources/icon.png' ),
text=self.tr(u'Preferences'),
callback=self.show_preferences_dialog,
parent=self.iface.mainWindow(),
add_to_toolbar=False)
+ self.iface.registerMapLayerConfigWidgetFactory(self.mapLayerConfigWidgetFactory)
+
self.update_button_enabled_status()
def unload(self):
@@ -219,6 +225,8 @@ def unload(self):
# remove the toolbar
del self.toolbar
+ self.iface.unregisterMapLayerConfigWidgetFactory(self.mapLayerConfigWidgetFactory)
+
def show_preferences_dialog(self):
dlg = PreferencesDialog(self.iface.mainWindow())
dlg.exec_()
diff --git a/qfieldsync/ui/map_layer_config_widget.ui b/qfieldsync/ui/map_layer_config_widget.ui
new file mode 100644
index 00000000..6e675dc4
--- /dev/null
+++ b/qfieldsync/ui/map_layer_config_widget.ui
@@ -0,0 +1,48 @@
+
+
+ QFieldLayerSettingsPage
+
+
+
+ 0
+ 0
+ 500
+ 250
+
+
+
+ Form
+
+
+ -
+
+
+ Layer Action
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ -
+
+
+ When enabled, this option disables adding and deleting features, as well as modifying the geometries of existing features.
+
+
+ Lock Geometries
+
+
+
+
+
+
+
+
diff --git a/qfieldsync/ui/project_configuration_dialog.ui b/qfieldsync/ui/project_configuration_dialog.ui
index 077c47a8..d5195856 100644
--- a/qfieldsync/ui/project_configuration_dialog.ui
+++ b/qfieldsync/ui/project_configuration_dialog.ui
@@ -75,6 +75,14 @@
Layer
+
+
+ Lock Geometries
+
+
+ When enabled, this option disables adding and deleting features, as well as modifying the geometries of existing features.
+
+
Action
@@ -264,15 +272,11 @@
-
+
Photo Naming
-
- -
-
-
-
+