From 3b020fec4d9ea25bfe53df3c57974971e29370f4 Mon Sep 17 00:00:00 2001 From: pgipper Date: Sat, 12 Nov 2022 17:59:13 +0100 Subject: [PATCH 1/3] enable layer exceptions --- controller.py | 8 ++-- helpers.py | 12 +++++- models.py | 112 +++++++++++++++++++++++++++++++++++++++++++++++++- settings.py | 4 ++ widgets.py | 72 ++++++++++++++++++++++++++++++-- 5 files changed, 196 insertions(+), 12 deletions(-) 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/models.py b/models.py index c1c26b8..5676944 100644 --- a/models.py +++ b/models.py @@ -1,6 +1,12 @@ -from PyQt5.QtCore import QAbstractListModel, Qt, QModelIndex -from qgis.core import QgsMessageLog, Qgis +from typing import Any +from PyQt5.QtCore import QAbstractListModel, Qt, QModelIndex, QVariant, QAbstractTableModel, QAbstractItemModel +from PyQt5.QtGui import QStandardItemModel, QStandardItem + +from qgis.core import QgsMessageLog, Qgis, QgsMapLayerProxyModel, QgsProject, QgsFeatureSource, QgsApplication + +from .helpers import hasLayerException +from .settings import SUPPORTED_PROVIDERS from .filters import FilterManager @@ -29,3 +35,105 @@ def removeRows(self, row: int, count: int = 1, parent: QModelIndex = ...) -> boo self.filters = self.filters[:row] + self.tableData[row + count:] self.endRemoveRows() return True + + +def getLayerModel(): + model = QStandardItemModel() + 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) + else: + item.setEnabled(False) + if hasLayerException(layer): + item.setCheckState(Qt.Checked) + else: + item.setCheckState(Qt.Unchecked) + model.appendRow(item) + return model + + +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) + + +class LayerModel1(QgsMapLayerProxyModel): + + def __init__(self, parent=None): + super().__init__(parent) + self.headers = ['Layer', self.tr('Do not filter'), self.tr('Description')] + + def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...) -> Any: + if role == Qt.DisplayRole and orientation == Qt.Horizontal: + return self.headers[section] + return QVariant() + + def data(self, index: QModelIndex, role: int = ...) -> Any: + if not index.isValid(): + return QVariant() + if index.column() == 0: + return super().data(index, role=role) + if index.column() == 1 and role == Qt.DisplayRole: + return 'Checkbox' + if index.column() == 2: + layerIndex = self.index(row=index.row(), column=0) + return super().data(layerIndex, role=Qt.UserRole) + return QVariant() + + def columnCount(self, parent: QModelIndex = ...) -> int: + return len(self.headers) + + +class LayerModel2(QAbstractTableModel): + + def __init__(self, parent=None): + super().__init__(parent) + self.layers = list(QgsProject.instance().mapLayers().values()) + self.headers = [self.tr('Do not filter'), 'Layer', 'Status'] + + def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...) -> Any: + if role == Qt.DisplayRole and orientation == Qt.Horizontal: + return self.headers[section] + return QVariant() + + def display(self, index): + mapping = { + 0: 'checkbox', + 1: self.layers[index.row()].name(), + 2: 'Layer wird nicht unterstützt' if self.layers[index.row()].providerType() == 'postgres' else '', + } + return mapping[index.column()] + + def data(self, index, role=Qt.DisplayRole): + if role == Qt.DisplayRole: + return self.display(index) + elif role == DataRole: + return self.layers[index.row()] + return QVariant() + + def rowCount(self, parent=QModelIndex()): + return len(self.layers) + + def columnCount(self, parent: QModelIndex = ...) -> int: + return len(self.headers) \ No newline at end of file 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..e9af018 100644 --- a/widgets.py +++ b/widgets.py @@ -5,7 +5,7 @@ from PyQt5 import uic from PyQt5.QtCore import Qt, pyqtSignal -from PyQt5.QtGui import QIcon, QPixmap, QColor +from PyQt5.QtGui import QIcon, QPixmap, QColor, QStandardItemModel, QStandardItem from PyQt5.QtWidgets import ( QToolBar, QWidget, @@ -15,14 +15,25 @@ 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, + QgsExpressionContextUtils, + QgsGeometry, + QgsMapLayerProxyModel, + 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, getLayerModel, DataRole from .filters import Predicate, FilterManager, FilterDefinition @@ -61,6 +72,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("Check layers to exclude 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 +274,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 +292,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 +330,10 @@ def startFilterFromExtentDialog(self): dlg = ExtentDialog(self.controller, parent=self) dlg.show() + def startLayerExceptionsDialog(self): + dlg = LayerExceptionsDialog(self.controller, parent=self) + dlg.show() + def startManageFiltersDialog(self): dlg = ManageFiltersDialog(self.controller, parent=self) dlg.exec() From 7f827cd645a9aebe6e953af7c3b4ae8bc38e5ba3 Mon Sep 17 00:00:00 2001 From: pgipper Date: Sat, 12 Nov 2022 18:07:04 +0100 Subject: [PATCH 2/3] cleanup --- models.py | 82 ++---------------------------------------------------- widgets.py | 10 +++---- 2 files changed, 6 insertions(+), 86 deletions(-) diff --git a/models.py b/models.py index 5676944..1061bce 100644 --- a/models.py +++ b/models.py @@ -1,9 +1,9 @@ from typing import Any -from PyQt5.QtCore import QAbstractListModel, Qt, QModelIndex, QVariant, QAbstractTableModel, QAbstractItemModel +from PyQt5.QtCore import QAbstractListModel, Qt, QModelIndex from PyQt5.QtGui import QStandardItemModel, QStandardItem -from qgis.core import QgsMessageLog, Qgis, QgsMapLayerProxyModel, QgsProject, QgsFeatureSource, QgsApplication +from qgis.core import QgsMessageLog, Qgis, QgsProject, QgsFeatureSource, QgsApplication from .helpers import hasLayerException from .settings import SUPPORTED_PROVIDERS @@ -37,24 +37,6 @@ def removeRows(self, row: int, count: int = 1, parent: QModelIndex = ...) -> boo return True -def getLayerModel(): - model = QStandardItemModel() - 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) - else: - item.setEnabled(False) - if hasLayerException(layer): - item.setCheckState(Qt.Checked) - else: - item.setCheckState(Qt.Unchecked) - model.appendRow(item) - return model - - class LayerModel(QStandardItemModel): def __init__(self, parent=None): super(LayerModel, self).__init__(parent) @@ -77,63 +59,3 @@ def __init__(self, parent=None): item.setCheckState(Qt.Unchecked) self.appendRow(item) - -class LayerModel1(QgsMapLayerProxyModel): - - def __init__(self, parent=None): - super().__init__(parent) - self.headers = ['Layer', self.tr('Do not filter'), self.tr('Description')] - - def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...) -> Any: - if role == Qt.DisplayRole and orientation == Qt.Horizontal: - return self.headers[section] - return QVariant() - - def data(self, index: QModelIndex, role: int = ...) -> Any: - if not index.isValid(): - return QVariant() - if index.column() == 0: - return super().data(index, role=role) - if index.column() == 1 and role == Qt.DisplayRole: - return 'Checkbox' - if index.column() == 2: - layerIndex = self.index(row=index.row(), column=0) - return super().data(layerIndex, role=Qt.UserRole) - return QVariant() - - def columnCount(self, parent: QModelIndex = ...) -> int: - return len(self.headers) - - -class LayerModel2(QAbstractTableModel): - - def __init__(self, parent=None): - super().__init__(parent) - self.layers = list(QgsProject.instance().mapLayers().values()) - self.headers = [self.tr('Do not filter'), 'Layer', 'Status'] - - def headerData(self, section: int, orientation: Qt.Orientation, role: int = ...) -> Any: - if role == Qt.DisplayRole and orientation == Qt.Horizontal: - return self.headers[section] - return QVariant() - - def display(self, index): - mapping = { - 0: 'checkbox', - 1: self.layers[index.row()].name(), - 2: 'Layer wird nicht unterstützt' if self.layers[index.row()].providerType() == 'postgres' else '', - } - return mapping[index.column()] - - def data(self, index, role=Qt.DisplayRole): - if role == Qt.DisplayRole: - return self.display(index) - elif role == DataRole: - return self.layers[index.row()] - return QVariant() - - def rowCount(self, parent=QModelIndex()): - return len(self.layers) - - def columnCount(self, parent: QModelIndex = ...) -> int: - return len(self.headers) \ No newline at end of file diff --git a/widgets.py b/widgets.py index e9af018..9439ae7 100644 --- a/widgets.py +++ b/widgets.py @@ -5,7 +5,7 @@ from PyQt5 import uic from PyQt5.QtCore import Qt, pyqtSignal -from PyQt5.QtGui import QIcon, QPixmap, QColor, QStandardItemModel, QStandardItem +from PyQt5.QtGui import QIcon, QPixmap, QColor from PyQt5.QtWidgets import ( QToolBar, QWidget, @@ -20,9 +20,7 @@ from qgis.gui import QgsExtentWidget, QgsRubberBand from qgis.core import ( QgsApplication, - QgsExpressionContextUtils, QgsGeometry, - QgsMapLayerProxyModel, QgsProject, QgsCoordinateReferenceSystem, QgsCoordinateTransform, @@ -33,7 +31,7 @@ from .helpers import removeFilterFromLayer, setLayerException, hasLayerException, addFilterToLayer from .controller import FilterController -from .models import FilterModel, LayerModel, getLayerModel, DataRole +from .models import FilterModel, LayerModel, DataRole from .filters import Predicate, FilterManager, FilterDefinition @@ -77,7 +75,7 @@ def __init__(self, controller: FilterController, parent: Optional[QWidget] = Non super().__init__(parent=parent) self.controller = controller self.setObjectName("mLayerExceptionsDialog") - self.setWindowTitle(self.tr("Check layers to exclude from filter")) + self.setWindowTitle(self.tr("Exclude layers from filter")) self.setupUi() self.listView.setModel(LayerModel()) self.adjustSize() @@ -332,7 +330,7 @@ def startFilterFromExtentDialog(self): def startLayerExceptionsDialog(self): dlg = LayerExceptionsDialog(self.controller, parent=self) - dlg.show() + dlg.exec() def startManageFiltersDialog(self): dlg = ManageFiltersDialog(self.controller, parent=self) From be058c89a001db10a5cb7ea06b05ca7e422a732d Mon Sep 17 00:00:00 2001 From: pgipper Date: Sat, 12 Nov 2022 18:15:58 +0100 Subject: [PATCH 3/3] update de translations --- i18n/map_filter_de.qm | Bin 3173 -> 3746 bytes i18n/map_filter_de.ts | 97 +++++++++++++++++++++++++++++++----------- 2 files changed, 73 insertions(+), 24 deletions(-) diff --git a/i18n/map_filter_de.qm b/i18n/map_filter_de.qm index 072b2ace50dffe6e1feb74738ead65a84220786f..3a6fceec3fbafe84984b01d50efde413d3535949 100644 GIT binary patch delta 825 zcmaKoT}TvR6o%i~9d~9{SJRTVtQaE_!^n%Mq%eQO#EdMu3fW=jTX$@n*=A<_>7p(4 zDx#aoy78tU>!L`Izz7La5R|ZADyWMfA;YqQFhZy^W7y9{1H(Dr`ObNt_k1(aE9*0R zM%3u!{6PPM;&u08mq%aM0Gy2aiA{hoh=#5TAUuM`9sNLd6^<3`1p<1Y*;R@e{^o)RNGJI5Q)8ral%MLPKJ;Y`<B!DT>Km2Xr#Aw8 zuQWE@N5DDhW8xs4cZW-#&ypU(TRs#3S$CGAv^9KxavBJBWuJaW+)zuHtAy3)feTTWPJhxEgPn6rxb^DWzA4ow?_^8*9T0M z={~CR50pd56p400qlHz<49enUz{98Mi3DpRjQ6T_*+xh93;B$5j{@*iJBblv^?|a4crX9u$(Rf z$^%ty5PZ+Tz}U(boSy|W&!2t$4kn;JEB4d%4o86Uj_kkw2s1EnS#mJ0cm*`zF^A*L zDL@U!IF>yDI)MEpXNb8MP<{nx)_Elc29`q3cWM4WT9Zrj`b(fXMy^Ar&NDDDr*b#E z02<7?oQLnjCm>&n*KR{8(8342UFI SZ{QTzyoBQj 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