diff --git a/controller.py b/controller.py index eb07bf1..017d798 100644 --- a/controller.py +++ b/controller.py @@ -6,7 +6,7 @@ from qgis.utils import iface from .filters import FilterDefinition, Predicate, FilterManager -from .helpers import getPostgisLayers, removeFilterFromLayer, addFilterToLayer, refreshLayerTree +from .helpers import getPostgisLayers, removeFilterFromLayer, addFilterToLayer, refreshLayerTree, hasLayerException from .settings import FILTER_COMMENT_START, FILTER_COMMENT_STOP @@ -51,10 +51,10 @@ def onLayersAdded(self, layers: Iterable[QgsMapLayer]): def updateLayerFilters(self, checked: bool): for layer in getPostgisLayers(QgsProject.instance().mapLayers().values()): - if not checked: - removeFilterFromLayer(layer) - else: + if checked and not hasLayerException(layer): addFilterToLayer(layer, self.currentFilter) + else: + removeFilterFromLayer(layer) refreshLayerTree() def updateProjectLayers(self, checked): diff --git a/helpers.py b/helpers.py index 2ea1423..f722159 100644 --- a/helpers.py +++ b/helpers.py @@ -1,8 +1,8 @@ from typing import Any, List, Iterable -from qgis.core import QgsSettings, QgsMapLayer, QgsMapLayerType, QgsVectorLayer, QgsMessageLog, Qgis +from qgis.core import QgsExpressionContextUtils, QgsSettings, QgsMapLayer, QgsMapLayerType, QgsVectorLayer -from .settings import GROUP, FILTER_COMMENT_START, FILTER_COMMENT_STOP +from .settings import GROUP, FILTER_COMMENT_START, FILTER_COMMENT_STOP, LAYER_EXCEPTION_VARIABLE def saveSettingsValue(key: str, value: Any): @@ -72,6 +72,14 @@ def getLayerGeomName(layer: QgsVectorLayer): return layer.dataProvider().uri().geometryColumn() +def hasLayerException(layer: QgsVectorLayer) -> bool: + return QgsExpressionContextUtils.layerScope(layer).variable(LAYER_EXCEPTION_VARIABLE) == 'true' + + +def setLayerException(layer: QgsVectorLayer, exception: bool) -> None: + QgsExpressionContextUtils.setLayerVariable(layer, LAYER_EXCEPTION_VARIABLE, exception) + + def getTestFilterDefinition(): from .filters import Predicate, FilterDefinition name = 'museumsinsel' diff --git a/i18n/map_filter_de.qm b/i18n/map_filter_de.qm index 072b2ac..3a6fcee 100644 Binary files a/i18n/map_filter_de.qm and b/i18n/map_filter_de.qm differ diff --git a/i18n/map_filter_de.ts b/i18n/map_filter_de.ts index c078a51..f072ae1 100644 --- a/i18n/map_filter_de.ts +++ b/i18n/map_filter_de.ts @@ -109,66 +109,84 @@ Select a polygon layer - Polygonlayer auswählen + Polygonlayer auswählen No features selected - Keine Features gewählt + Keine Features gewählt New Filter - Neuer Filter + Neuer Filter ExtentDialog - + Set rectangular filter geometry Rechteckige Filtergeometrie setzen + + FilterController + + + New Filter + Neuer Filter + + + + Select a polygon layer + Polygonlayer auswählen + + + + No features selected + Keine Features gewählt + + FilterManager - + Current filter definition is not valid Aktuelle Filterdefinition ist ungültig - + Please provide a name for the filter Bitte benennen Sie erst den Filter - + Overwrite settings for filter Einstellungen überschreiben für Filter - + Overwrite? Überschreiben? - + Delete filter Filter löschen - + Delete? Löschen? - + Current settings will be lost. Apply anyway? Aktuelle Einstellungen gehen verloren. Trotzdem anwenden? - + Continue? Fortfahren? @@ -176,12 +194,12 @@ FilterToolbar - + Rectangular filter Rechteckiger Filter - + Filter from selected features Filter aus gewählten Features @@ -191,40 +209,71 @@ Filter ein/aus - + Save current filter Filter speichern - + Manage filters Filter verwalten - + Filter Toolbar Filterwerkzeugleiste - + Show filter geometry Filtergeometrie anzeigen - + Activate filter Filter aktivieren - + Deactivate filter Filter deaktivieren - + No filter geometry set Keine Filtergeometrie gesetzt + + + Exclude layers from filter + Ausnahmen festlegen + + + + Hide filter geometry + Filtergeometrie verstecken + + + + LayerExceptionsDialog + + + Exclude layers from filter + + + + + LayerModel + + + Layer has no spatial index + Layer hat keinen räumlichen Index + + + + Layer type is not supported + Layertyp wird nicht unterstützt + ManageFiltersDialog @@ -264,12 +313,12 @@ Schließen - + Change Name Namen ändern - + New Name: Neuer Name: @@ -285,7 +334,7 @@ PredicateButton - + Geometric predicate Räumlicher Operator diff --git a/models.py b/models.py index c1c26b8..1061bce 100644 --- a/models.py +++ b/models.py @@ -1,6 +1,12 @@ +from typing import Any + from PyQt5.QtCore import QAbstractListModel, Qt, QModelIndex -from qgis.core import QgsMessageLog, Qgis +from PyQt5.QtGui import QStandardItemModel, QStandardItem + +from qgis.core import QgsMessageLog, Qgis, QgsProject, QgsFeatureSource, QgsApplication +from .helpers import hasLayerException +from .settings import SUPPORTED_PROVIDERS from .filters import FilterManager @@ -29,3 +35,27 @@ def removeRows(self, row: int, count: int = 1, parent: QModelIndex = ...) -> boo self.filters = self.filters[:row] + self.tableData[row + count:] self.endRemoveRows() return True + + +class LayerModel(QStandardItemModel): + def __init__(self, parent=None): + super(LayerModel, self).__init__(parent) + + for layer in [layerNode.layer() for layerNode in QgsProject.instance().layerTreeRoot().findLayers()]: + item = QStandardItem(layer.name()) + item.setData(layer, role=DataRole) + item.setFlags(Qt.ItemIsUserCheckable) + if layer.providerType() in SUPPORTED_PROVIDERS: + item.setEnabled(True) + if layer.dataProvider().hasSpatialIndex() == QgsFeatureSource.SpatialIndexNotPresent: + item.setToolTip(self.tr('Layer has no spatial index')) + item.setIcon(QgsApplication.getThemeIcon('/mIconWarning.svg')) + else: + item.setEnabled(False) + item.setToolTip(self.tr('Layer type is not supported')) + if hasLayerException(layer): + item.setCheckState(Qt.Checked) + else: + item.setCheckState(Qt.Unchecked) + self.appendRow(item) + diff --git a/settings.py b/settings.py index ceb0402..1bd2767 100644 --- a/settings.py +++ b/settings.py @@ -1,7 +1,11 @@ GROUP = 'MapFilter' # The section name for filter definitions stored in QSettings +LAYER_EXCEPTION_VARIABLE = 'MapFilterException' SPLIT_STRING = '#!#!#' # String used to split filter definition parameters in QSettings # The filter string might contain user-specific parts so we surround *our* filter # string with text markers FILTER_COMMENT_START = '/* MapFilter Plugin Start */' FILTER_COMMENT_STOP = '/* MapFilter Plugin Stop */' + +# The QGIS Provider Types that can be filtered by the plugin +SUPPORTED_PROVIDERS = ['postgres'] diff --git a/widgets.py b/widgets.py index 3b1c7c8..9439ae7 100644 --- a/widgets.py +++ b/widgets.py @@ -15,14 +15,23 @@ QDialog, QVBoxLayout, QSizePolicy, - QDialogButtonBox, QListWidget, QMenu, QActionGroup, QLabel, QFrame, QInputDialog + QDialogButtonBox, QListWidget, QMenu, QActionGroup, QLabel, QFrame, QInputDialog, QTreeView ) from qgis.gui import QgsExtentWidget, QgsRubberBand -from qgis.core import QgsApplication, QgsGeometry, QgsProject, QgsCoordinateReferenceSystem, QgsCoordinateTransform, QgsWkbTypes +from qgis.core import ( + QgsApplication, + QgsGeometry, + QgsProject, + QgsCoordinateReferenceSystem, + QgsCoordinateTransform, + QgsVectorLayer, + QgsWkbTypes, +) from qgis.utils import iface +from .helpers import removeFilterFromLayer, setLayerException, hasLayerException, addFilterToLayer from .controller import FilterController -from .models import FilterModel, DataRole +from .models import FilterModel, LayerModel, DataRole from .filters import Predicate, FilterManager, FilterDefinition @@ -61,6 +70,49 @@ def accept(self) -> None: super().accept() +class LayerExceptionsDialog(QDialog): + def __init__(self, controller: FilterController, parent: Optional[QWidget] = None) -> None: + super().__init__(parent=parent) + self.controller = controller + self.setObjectName("mLayerExceptionsDialog") + self.setWindowTitle(self.tr("Exclude layers from filter")) + self.setupUi() + self.listView.setModel(LayerModel()) + self.adjustSize() + + def setupUi(self): + sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) + self.setSizePolicy(sizePolicy) + self.verticalLayout = QVBoxLayout(self) + self.listView = QTreeView(self) + self.listView.header().hide() + self.verticalLayout.addWidget(self.listView) + self.buttonBox = QDialogButtonBox(self) + self.buttonBox.setOrientation(Qt.Horizontal) + self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok) + self.verticalLayout.addWidget(self.buttonBox) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + def accept(self) -> None: + model = self.listView.model() + for index in range(model.rowCount()): + item = model.item(index) + layer = item.data() + self.setExceptionForLayer(layer, bool(item.checkState() == Qt.Checked)) + super().accept() + + def setExceptionForLayer(self, layer: QgsVectorLayer, exception: bool) -> None: + if exception: + removeFilterFromLayer(layer) + if not exception and hasLayerException(layer) and self.controller.toolbarIsActive: + addFilterToLayer(layer, self.controller.currentFilter) + setLayerException(layer, exception) + + FORM_CLASS, _ = uic.loadUiType(os.path.join(os.path.dirname(__file__), 'ui', 'named_filters_dialog.ui')) @@ -220,6 +272,11 @@ def setupUi(self): self.predicateButton.setIconSize(self.iconSize()) self.addWidget(self.predicateButton) + self.layerExceptionsAction = QAction(self) + self.layerExceptionsAction.setIcon(QgsApplication.getThemeIcon('/mIconLayerTree.svg')) + self.layerExceptionsAction.setToolTip(self.tr('Exclude layers from filter')) + self.addAction(self.layerExceptionsAction) + self.saveCurrentFilterAction = QAction(self) self.saveCurrentFilterAction.setIcon(QgsApplication.getThemeIcon('/mActionFileSave.svg')) self.saveCurrentFilterAction.setToolTip(self.tr('Save current filter')) @@ -233,6 +290,7 @@ def setupUi(self): def setupConnections(self): self.toggleFilterAction.toggled.connect(self.onToggled) self.filterFromExtentAction.triggered.connect(self.startFilterFromExtentDialog) + self.layerExceptionsAction.triggered.connect(self.startLayerExceptionsDialog) self.manageFiltersAction.triggered.connect(self.startManageFiltersDialog) self.saveCurrentFilterAction.triggered.connect(self.controller.saveCurrentFilter) self.predicateButton.predicateChanged.connect(self.controller.setFilterPredicate) @@ -270,6 +328,10 @@ def startFilterFromExtentDialog(self): dlg = ExtentDialog(self.controller, parent=self) dlg.show() + def startLayerExceptionsDialog(self): + dlg = LayerExceptionsDialog(self.controller, parent=self) + dlg.exec() + def startManageFiltersDialog(self): dlg = ManageFiltersDialog(self.controller, parent=self) dlg.exec()