Skip to content

Commit

Permalink
Merge pull request #157 from suricactus/lock_geom
Browse files Browse the repository at this point in the history
FEATURE add option to lock geometries
  • Loading branch information
m-kuhn authored Jul 4, 2020
2 parents 5d0bb4b + 1008449 commit 4108410
Show file tree
Hide file tree
Showing 8 changed files with 337 additions and 58 deletions.
20 changes: 20 additions & 0 deletions qfieldsync/core/layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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')):
Expand Down
89 changes: 89 additions & 0 deletions qfieldsync/gui/map_layer_config_widget.py
Original file line number Diff line number Diff line change
@@ -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)
84 changes: 84 additions & 0 deletions qfieldsync/gui/photo_naming_widget.py
Original file line number Diff line number Diff line change
@@ -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()
90 changes: 40 additions & 50 deletions qfieldsync/gui/project_configuration_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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():
Expand All @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
36 changes: 36 additions & 0 deletions qfieldsync/gui/utils.py
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit 4108410

Please sign in to comment.