diff --git a/browsedatasource.py b/browsedatasource.py index 37135a7..012e8c7 100644 --- a/browsedatasource.py +++ b/browsedatasource.py @@ -1,46 +1,32 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'C:\Users\DEMO\Dropbox\dev\changeDataSource\browsedatasource.ui' -# -# Created: Wed Nov 04 23:39:53 2015 -# by: PyQt4 UI code generator 4.10.2 -# -# WARNING! All changes made in this file will be lost! +# Form implementation generated from reading ui file 'browsedatasource.ui' +# Hand-updated for Qt5/Qt6 dual compatibility (qualified enums). -from builtins import object -from qgis.PyQt import QtCore, QtGui, QtWidgets +from qgis.PyQt import QtCore, QtWidgets -try: - _fromUtf8 = QtCore.QString.fromUtf8 -except AttributeError: - def _fromUtf8(s): - return s - -try: - _encoding = QtWidgets.QApplication.UnicodeUTF8 - def _translate(context, text, disambig): - return QtWidgets.QApplication.translate(context, text, disambig, _encoding) -except AttributeError: - def _translate(context, text, disambig): - return QtWidgets.QApplication.translate(context, text, disambig) class Ui_dataSourceBrowser(object): def setupUi(self, dataSourceBrowser): - dataSourceBrowser.setObjectName(_fromUtf8("dataSourceBrowser")) + dataSourceBrowser.setObjectName("dataSourceBrowser") dataSourceBrowser.resize(400, 444) self.buttonBox = QtWidgets.QDialogButtonBox(dataSourceBrowser) self.buttonBox.setGeometry(QtCore.QRect(50, 400, 341, 32)) - self.buttonBox.setOrientation(QtCore.Qt.Horizontal) - self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) - self.buttonBox.setObjectName(_fromUtf8("buttonBox")) + self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal) + self.buttonBox.setStandardButtons( + QtWidgets.QDialogButtonBox.StandardButton.Cancel + | QtWidgets.QDialogButtonBox.StandardButton.Ok + ) + self.buttonBox.setObjectName("buttonBox") self.dataSourceTree = QtWidgets.QTreeView(dataSourceBrowser) self.dataSourceTree.setGeometry(QtCore.QRect(10, 11, 381, 381)) - self.dataSourceTree.setObjectName(_fromUtf8("dataSourceTree")) + self.dataSourceTree.setObjectName("dataSourceTree") self.retranslateUi(dataSourceBrowser) self.buttonBox.accepted.connect(dataSourceBrowser.accept) self.buttonBox.rejected.connect(dataSourceBrowser.reject) def retranslateUi(self, dataSourceBrowser): - dataSourceBrowser.setWindowTitle(_translate("dataSourceBrowser", "Dialog", None)) - + dataSourceBrowser.setWindowTitle( + QtCore.QCoreApplication.translate("dataSourceBrowser", "Dialog") + ) diff --git a/changeDataSource.py b/changeDataSource.py index a8a9d9d..558df00 100644 --- a/changeDataSource.py +++ b/changeDataSource.py @@ -20,23 +20,39 @@ * * ***************************************************************************/ """ -from __future__ import print_function -from __future__ import absolute_import -from builtins import range -from builtins import object -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtXml import * -from PyQt5.QtWidgets import * -from qgis.core import * -# Initialize Qt resources from file resources.py -from . import resources_rc -# Import the code for the dialog -from .changeDataSource_dialog import changeDataSourceDialog,dataSourceBrowser -from .setdatasource import setDataSource -from qgis.gui import QgsMessageBar import os.path +from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication, Qt, pyqtSignal +from qgis.PyQt.QtGui import QIcon +from qgis.PyQt.QtWidgets import ( + QAction, + QApplication, + QDialogButtonBox, + QHeaderView, + QLineEdit, + QPushButton, + QSizePolicy, + QStyle, + QTableWidgetItem, + QToolButton, +) +from qgis.core import ( + Qgis, + QgsExpression, + QgsExpressionContext, + QgsExpressionContextScope, + QgsFeature, + QgsFeatureRequest, + QgsGeometry, + QgsMapLayer, + QgsProject, + QgsVectorLayer, + QgsWkbTypes, +) + +from .changeDataSource_dialog import changeDataSourceDialog, dataSourceBrowser +from .setdatasource import setDataSource + class changeDataSource(object): """QGIS Plugin Implementation.""" @@ -49,47 +65,29 @@ def __init__(self, iface): application at run time. :type iface: QgsInterface """ - # Save reference to the QGIS interface self.iface = iface - # initialize plugin directory self.plugin_dir = os.path.dirname(__file__) - # initialize locale - locale = QSettings().value('locale/userLocale')[0:2] + locale_value = QSettings().value('locale/userLocale') or '' + locale = locale_value[0:2] locale_path = os.path.join( self.plugin_dir, 'i18n', 'changeDataSource_{}.qm'.format(locale)) - if os.path.exists(locale_path): + if locale and os.path.exists(locale_path): self.translator = QTranslator() self.translator.load(locale_path) + QCoreApplication.installTranslator(self.translator) - if qVersion() > '4.3.3': - QCoreApplication.installTranslator(self.translator) - - # Create the dialog (after translation) and keep reference self.dlg = changeDataSourceDialog() - # Declare instance attributes self.actions = [] self.menu = self.tr(u'&changeDataSource') - # TODO: We are going to let the user set this up in a future iteration self.toolbar = self.iface.addToolBar(u'changeDataSource') self.toolbar.setObjectName(u'changeDataSource') # noinspection PyMethodMayBeStatic def tr(self, message): - """Get the translation for a string using Qt translation API. - - We implement this ourselves since we do not inherit QObject. - - :param message: String for translation. - :type message: str, QString - - :returns: Translated version of message. - :rtype: QString - """ - # noinspection PyTypeChecker,PyArgumentList,PyCallByClass return QCoreApplication.translate('changeDataSource', message) @@ -104,44 +102,6 @@ def add_action( status_tip=None, whats_this=None, parent=None): - """Add a toolbar icon to the toolbar. - - :param icon_path: Path to the icon for this action. Can be a resource - path (e.g. ':/plugins/foo/bar.png') or a normal file system path. - :type icon_path: str - - :param text: Text that should be shown in menu items for this action. - :type text: str - - :param callback: Function to be called when the action is triggered. - :type callback: function - - :param enabled_flag: A flag indicating if the action should be enabled - by default. Defaults to True. - :type enabled_flag: bool - - :param add_to_menu: Flag indicating whether the action should also - be added to the menu. Defaults to True. - :type add_to_menu: bool - - :param add_to_toolbar: Flag indicating whether the action should also - be added to the toolbar. Defaults to True. - :type add_to_toolbar: bool - - :param status_tip: Optional text to show in a popup when mouse pointer - hovers over the action. - :type status_tip: str - - :param parent: Parent widget for the new action. Defaults None. - :type parent: QWidget - - :param whats_this: Optional text to show in the status bar when the - mouse pointer hovers over the action. - - :returns: The action that was created. Note that the action is also - added to self.actions list. - :rtype: QAction - """ icon = QIcon(icon_path) action = QAction(icon, text, parent) @@ -169,52 +129,47 @@ def add_action( def initGui(self): """Create the menu entries and toolbar icons inside the QGIS GUI.""" - icon_path = os.path.join(self.plugin_dir,"icon.png") + icon_path = os.path.join(self.plugin_dir, "icon.png") self.add_action( icon_path, text=self.tr(u'changeDataSource'), callback=self.run, parent=self.iface.mainWindow()) - self.changeDSActionVector = QAction(QIcon(os.path.join(self.plugin_dir,"icon.png")), u"Change vector datasource", self.iface ) - self.changeDSActionRaster = QAction(QIcon(os.path.join(self.plugin_dir,"icon.png")), u"Change raster datasource", self.iface ) - self.iface.addCustomActionForLayerType(self.changeDSActionVector,"", QgsMapLayer.VectorLayer,True) - self.iface.addCustomActionForLayerType(self.changeDSActionRaster,"", QgsMapLayer.RasterLayer,True) - self.changeDSTool = setDataSource(self, ) + self.changeDSActionVector = QAction(QIcon(os.path.join(self.plugin_dir, "icon.png")), u"Change vector datasource", self.iface.mainWindow()) + self.changeDSActionRaster = QAction(QIcon(os.path.join(self.plugin_dir, "icon.png")), u"Change raster datasource", self.iface.mainWindow()) + self.iface.addCustomActionForLayerType(self.changeDSActionVector, "", QgsMapLayer.VectorLayer, True) + self.iface.addCustomActionForLayerType(self.changeDSActionRaster, "", QgsMapLayer.RasterLayer, True) + self.changeDSTool = setDataSource(self) self.browserDialog = dataSourceBrowser() self.dlg.handleBadLayersCheckbox.hide() self.dlg.reconcileButton.hide() self.connectSignals() - self.session = 0 + self.session = 0 def connectSignals(self): self.changeDSActionVector.triggered.connect(self.changeLayerDS) self.changeDSActionRaster.triggered.connect(self.changeLayerDS) self.dlg.replaceButton.clicked.connect(self.replaceDS) self.dlg.layerTable.verticalHeader().sectionClicked.connect(self.activateSelection) - self.dlg.buttonBox.button(QDialogButtonBox.Reset).clicked.connect(lambda: self.buttonBoxHub("Reset")) - self.dlg.buttonBox.button(QDialogButtonBox.Apply).clicked.connect(lambda: self.buttonBoxHub("Apply")) - self.dlg.buttonBox.button(QDialogButtonBox.Cancel).clicked.connect(lambda: self.buttonBoxHub("Cancel")) - #self.dlg.reconcileButton.clicked.connect(self.reconcileUnhandled) + self.dlg.buttonBox.button(QDialogButtonBox.StandardButton.Reset).clicked.connect(lambda: self.buttonBoxHub("Reset")) + self.dlg.buttonBox.button(QDialogButtonBox.StandardButton.Apply).clicked.connect(lambda: self.buttonBoxHub("Apply")) + self.dlg.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).clicked.connect(lambda: self.buttonBoxHub("Cancel")) self.dlg.closedDialog.connect(self.removeServiceLayers) - #self.dlg.handleBadLayersCheckbox.stateChanged.connect(self.handleBadLayerOption) - #self.iface.initializationCompleted.connect(self.initHandleBadLayers) - #self.iface.projectRead.connect(self.recoverUnhandledLayers) self.iface.newProjectCreated.connect(self.updateSession) - #self.initHandleBadLayers() - def setEmbeddedLayer(self,layer): + def setEmbeddedLayer(self, layer): root = QgsProject.instance().layerTreeRoot() layerNode = root.findLayer(layer.id()) - layerNode.setCustomProperty("embedded","") + layerNode.setCustomProperty("embedded", "") def updateSession(self): - self.session += 1 + self.session += 1 - def activateSelection(self,idx): + def activateSelection(self, idx): indexes = [] for selectionRange in self.dlg.layerTable.selectedRanges(): - indexes.extend(list(range(selectionRange.topRow(), selectionRange.bottomRow()+1))) + indexes.extend(list(range(selectionRange.topRow(), selectionRange.bottomRow() + 1))) if indexes != []: self.dlg.onlySelectedCheck.setChecked(True) else: @@ -222,7 +177,7 @@ def activateSelection(self,idx): def changeLayerDS(self): self.dlg.hide() - self.changeDSTool.openDataSourceDialog(self.iface.layerTreeView().currentLayer())#, self.badLayersHandler) + self.changeDSTool.openDataSourceDialog(self.iface.layerTreeView().currentLayer()) def unload(self): """ @@ -236,63 +191,74 @@ def unload(self): self.tr(u'&changeDataSource'), action) self.iface.removeToolBarIcon(action) - # remove the toolbar del self.toolbar - def populateLayerTable(self, onlyUnhandled = None): + def populateLayerTable(self, onlyUnhandled=None): ''' method to write layer info in layer table ''' - self.changeDSTool.populateComboBox(self.dlg.datasourceCombo,[""]+list(self.changeDSTool.vectorDSList.keys())+list(self.changeDSTool.rasterDSList.keys())) + self.changeDSTool.populateComboBox( + self.dlg.datasourceCombo, + [""] + list(self.changeDSTool.vectorDSList.keys()) + list(self.changeDSTool.rasterDSList.keys()), + ) self.dlg.layerTable.clear() for row in range(self.dlg.layerTable.rowCount()): self.dlg.layerTable.removeRow(row) self.dlg.layerTable.setRowCount(0) self.dlg.layerTable.setColumnCount(5) - self.dlg.layerTable.setHorizontalHeaderItem(0,QTableWidgetItem("ID")) - self.dlg.layerTable.setHorizontalHeaderItem(1,QTableWidgetItem("Layer Name")) - self.dlg.layerTable.setHorizontalHeaderItem(2,QTableWidgetItem("Type")) - self.dlg.layerTable.setHorizontalHeaderItem(3,QTableWidgetItem("Data source")) - self.dlg.layerTable.setHorizontalHeaderItem(4,QTableWidgetItem("")) - - layersPropLayerDef = "Point?crs=epsg:3857&field=layerid:string(200)&field=layername:string(200)&field=layertype:string(20)&field=geometrytype:string(20)&field=provider:string(20)&field=datasource:string(250)&field=authid:string(20)" - self.layersPropLayer = QgsVectorLayer(layersPropLayerDef,"layerTable","memory") + self.dlg.layerTable.setHorizontalHeaderItem(0, QTableWidgetItem("ID")) + self.dlg.layerTable.setHorizontalHeaderItem(1, QTableWidgetItem("Layer Name")) + self.dlg.layerTable.setHorizontalHeaderItem(2, QTableWidgetItem("Type")) + self.dlg.layerTable.setHorizontalHeaderItem(3, QTableWidgetItem("Data source")) + self.dlg.layerTable.setHorizontalHeaderItem(4, QTableWidgetItem("")) + + # string(0) = unlimited length; prevents truncation of long PostGIS SQL views, + # which previously caused StopIteration when reading back the feature by fid. + layersPropLayerDef = ( + "Point?crs=epsg:3857" + "&field=layerid:string(0)" + "&field=layername:string(0)" + "&field=layertype:string(0)" + "&field=geometrytype:string(0)" + "&field=provider:string(0)" + "&field=datasource:string(0)" + "&field=authid:string(0)" + ) + self.layersPropLayer = QgsVectorLayer(layersPropLayerDef, "layerTable", "memory") dummyFeatures = [] - self.dlg.layerTable.horizontalHeader().setDefaultAlignment(Qt.AlignLeft) - + self.dlg.layerTable.horizontalHeader().setDefaultAlignment(Qt.AlignmentFlag.AlignLeft) self.dlg.layerTable.horizontalHeader().setSectionsClickable(False) - self.dlg.layerTable.hideColumn(0) - self.dlg.layerTable.hideColumn(5) - self.dlg.layerTable.hideColumn(6) + lr = QgsProject.instance() for layer in lr.mapLayers().values(): if layer.type() == QgsMapLayer.VectorLayer or layer.type() == QgsMapLayer.RasterLayer: - provider = layer.dataProvider().name() + provider = layer.dataProvider().name() if layer.dataProvider() else "" source = layer.source() cellStyle = "" if provider: lastRow = self.dlg.layerTable.rowCount() self.dlg.layerTable.insertRow(lastRow) - self.dlg.layerTable.setCellWidget(lastRow,0,self.getLabelWidget(layer.id(),0,style = cellStyle)) - self.dlg.layerTable.setCellWidget(lastRow,1,self.getLabelWidget(layer.name(),1,style = cellStyle)) - self.dlg.layerTable.setCellWidget(lastRow,2,self.getLabelWidget(provider,2,style = cellStyle)) - self.dlg.layerTable.setCellWidget(lastRow,3,self.getLabelWidget(source,3,style = cellStyle)) - self.dlg.layerTable.setCellWidget(lastRow,4,self.getButtonWidget(lastRow)) + self.dlg.layerTable.setCellWidget(lastRow, 0, self.getLabelWidget(layer.id(), 0, style=cellStyle)) + self.dlg.layerTable.setCellWidget(lastRow, 1, self.getLabelWidget(layer.name(), 1, style=cellStyle)) + self.dlg.layerTable.setCellWidget(lastRow, 2, self.getLabelWidget(provider, 2, style=cellStyle)) + self.dlg.layerTable.setCellWidget(lastRow, 3, self.getLabelWidget(source, 3, style=cellStyle)) + self.dlg.layerTable.setCellWidget(lastRow, 4, self.getButtonWidget(lastRow)) layerDummyFeature = QgsFeature(self.layersPropLayer.fields()) if layer.type() == QgsMapLayer.VectorLayer: - type = "vector" - enumGeometryTypes =('Point','Line','Polygon','UnknownGeometry','NoGeometry') - geometry = enumGeometryTypes[layer.geometryType()] + type_label = "vector" + geometry = QgsWkbTypes.geometryDisplayString(layer.geometryType()) else: - type = "raster" + type_label = "raster" geometry = "" dummyGeometry = QgsGeometry.fromPointXY(self.iface.mapCanvas().center()) layerDummyFeature.setGeometry(dummyGeometry) - layerDummyFeature.setAttributes([layer.id(), layer.name(), type, geometry, provider, source, layer.crs().authid()]) + layerDummyFeature.setAttributes( + [layer.id(), layer.name(), type_label, geometry, provider, source, layer.crs().authid()] + ) dummyFeatures.append(layerDummyFeature) self.layersPropLayer.dataProvider().addFeatures(dummyFeatures) @@ -300,46 +266,52 @@ def populateLayerTable(self, onlyUnhandled = None): QgsProject.instance().layerTreeRoot().findLayer(self.layersPropLayer.id()).setItemVisibilityChecked(False) self.dlg.mFieldExpressionWidget.setLayer(self.layersPropLayer) self.dlg.layerTable.resizeColumnToContents(1) - self.dlg.layerTable.horizontalHeader().setSectionResizeMode(2,QHeaderView.ResizeToContents) - self.dlg.layerTable.setColumnWidth(4,30) + self.dlg.layerTable.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) + self.dlg.layerTable.setColumnWidth(4, 30) self.dlg.layerTable.setShowGrid(False) - self.dlg.layerTable.horizontalHeader().setSectionResizeMode(3,QHeaderView.Stretch) # was QHeaderView.Stretch + self.dlg.layerTable.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch) - def getButtonWidget(self,row): - edit = QPushButton("...",parent = self.dlg.layerTable) - edit.setSizePolicy(QSizePolicy.Ignored,QSizePolicy.Ignored) + def getButtonWidget(self, row): + edit = QPushButton("...", parent=self.dlg.layerTable) + edit.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored) edit.clicked.connect(lambda: self.browseAction(row)) return edit - def browseAction(self,row): + def browseAction(self, row): ''' method to open qgis browser dialog to get new datasource/provider ''' - layerId = self.dlg.layerTable.cellWidget(row,0).text() - layerName = self.dlg.layerTable.cellWidget(row,1).text() - newType,newProvider,newDatasource = dataSourceBrowser.uri(title = layerName) - #check if databrowser return a incompatible layer type + layerId = self.dlg.layerTable.cellWidget(row, 0).text() + layerName = self.dlg.layerTable.cellWidget(row, 1).text() + newType, newProvider, newDatasource = dataSourceBrowser.uri(title=layerName) rowLayer = QgsProject.instance().mapLayer(layerId) - enumLayerTypes = ("vector","raster","plugin") + if rowLayer is None: + return None + enumLayerTypes = ("vector", "raster", "plugin") if newType and enumLayerTypes[rowLayer.type()] != newType: - self.iface.messageBar().pushMessage("Error", "Layer type mismatch %s/%s" % (enumLayerTypes[rowLayer.type()], newType), level=QgsMessageBar.CRITICAL, duration=4) + self.iface.messageBar().pushMessage( + "Error", + "Layer type mismatch %s/%s" % (enumLayerTypes[rowLayer.type()], newType), + level=Qgis.MessageLevel.Critical, + duration=4, + ) return None if newDatasource: - self.dlg.layerTable.cellWidget(row,3).setText(newDatasource) + self.dlg.layerTable.cellWidget(row, 3).setText(newDatasource) if newProvider: - self.dlg.layerTable.cellWidget(row,2).setText(newProvider) + self.dlg.layerTable.cellWidget(row, 2).setText(newProvider) - def getLabelWidget(self,txt,column, style = None): + def getLabelWidget(self, txt, column, style=None): ''' method that returns a preformatted qlineedit widget ''' - edit = QLineEdit(parent = self.dlg.layerTable) - idealWidth = QApplication.instance().fontMetrics().width(txt) + edit = QLineEdit(parent=self.dlg.layerTable) + idealWidth = QApplication.instance().fontMetrics().horizontalAdvance(txt) edit.setMinimumWidth(idealWidth) if column == 2: edit.setMaximumWidth(60) edit.setText(txt) - edit.setSizePolicy(QSizePolicy.Minimum,QSizePolicy.Ignored) + edit.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Ignored) if style: edit.setStyleSheet(style) else: @@ -347,13 +319,13 @@ def getLabelWidget(self,txt,column, style = None): edit.column = column edit.changed = None if column == 1: - edit.setReadOnly(True) + edit.setReadOnly(True) else: - edit.textChanged.connect(lambda: self.highlightCell(edit,"QLineEdit{background: yellow;}")) + edit.textChanged.connect(lambda: self.highlightCell(edit, "QLineEdit{background: yellow;}")) edit.setCursorPosition(0) return edit - def highlightCell(self,cell,newStyle): + def highlightCell(self, cell, newStyle): cell.setStyleSheet(newStyle) cell.changed = True @@ -361,56 +333,57 @@ def replaceDS(self): ''' method to replace the datasource string accordind to find/replace string or to expression result if valid ''' - self.replaceList=[] + self.replaceList = [] indexes = [] context = QgsExpressionContext() scope = QgsExpressionContextScope() context.appendScope(scope) - #build replace list if self.dlg.onlySelectedCheck.isChecked(): for selectionRange in self.dlg.layerTable.selectedRanges(): - indexes.extend(list(range(selectionRange.topRow(), selectionRange.bottomRow()+1))) + indexes.extend(list(range(selectionRange.topRow(), selectionRange.bottomRow() + 1))) for row in indexes: - self.replaceList.append(QgsProject.instance().mapLayer(self.dlg.layerTable.cellWidget(row,0).text())) + self.replaceList.append(QgsProject.instance().mapLayer(self.dlg.layerTable.cellWidget(row, 0).text())) else: - for row in range(0,self.dlg.layerTable.rowCount()): + for row in range(0, self.dlg.layerTable.rowCount()): indexes.append(row) - self.replaceList.append(QgsProject.instance().mapLayer(self.dlg.layerTable.cellWidget(row,0).text())) + self.replaceList.append(QgsProject.instance().mapLayer(self.dlg.layerTable.cellWidget(row, 0).text())) for row in indexes: - layerId = self.dlg.layerTable.cellWidget(row,0) - cell = self.dlg.layerTable.cellWidget(row,3) - orig = cell.text() + cell = self.dlg.layerTable.cellWidget(row, 3) if self.dlg.mFieldExpressionWidget.isValidExpression(): exp = QgsExpression(self.dlg.mFieldExpressionWidget.currentText()) - scope.setFeature(next(self.layersPropLayer.getFeatures(QgsFeatureRequest(row+1)))) + try: + feature = next(self.layersPropLayer.getFeatures(QgsFeatureRequest(row + 1))) + except StopIteration: + continue + scope.setFeature(feature) expResult = exp.evaluate(context) - cell.setText(expResult) + if expResult is None: + continue + cell.setText(str(expResult)) else: - cell.setText(cell.text().replace(self.dlg.findEdit.text(),self.dlg.replaceEdit.text())) + cell.setText(cell.text().replace(self.dlg.findEdit.text(), self.dlg.replaceEdit.text())) if self.dlg.datasourceCombo.currentText() != "": - self.dlg.layerTable.cellWidget(row,2).setText(self.dlg.datasourceCombo.currentText()) + self.dlg.layerTable.cellWidget(row, 2).setText(self.dlg.datasourceCombo.currentText()) - def applyDSChanges(self):#, reconcileUnhandled = False): + def applyDSChanges(self): ''' method to scan table row and apply the provider/datasource strings if changed ''' - - for row in range(0,self.dlg.layerTable.rowCount()): - rowProviderCell = self.dlg.layerTable.cellWidget(row,2) - rowDatasourceCell = self.dlg.layerTable.cellWidget(row,3) - rowLayerID = self.dlg.layerTable.cellWidget(row,0).text() - rowLayerName = self.dlg.layerTable.cellWidget(row,1).text() + for row in range(0, self.dlg.layerTable.rowCount()): + rowProviderCell = self.dlg.layerTable.cellWidget(row, 2) + rowDatasourceCell = self.dlg.layerTable.cellWidget(row, 3) + rowLayerID = self.dlg.layerTable.cellWidget(row, 0).text() rowProvider = rowProviderCell.text() rowDatasource = rowDatasourceCell.text() rowLayer = QgsProject.instance().mapLayer(rowLayerID) + if rowLayer is None: + continue rowProviderChanging = rowProviderCell.changed rowDatasourceChanging = rowDatasourceCell.changed if rowProviderChanging or rowDatasourceChanging: - # fix_print_with_import - print(("ROWS",rowLayer,rowProvider,rowDatasource)) - if self.changeDSTool.applyDataSource(rowLayer,rowProvider,rowDatasource): + if self.changeDSTool.applyDataSource(rowLayer, rowProvider, rowDatasource): resultStyle = "QLineEdit{background: green;}" else: resultStyle = "QLineEdit{background: red;}" @@ -424,23 +397,16 @@ def removeServiceLayers(self): method to remove service properties layer, used for expression changes and unhandled layers group if empty ''' - # fix_print_with_import - print("removing") try: QgsProject.instance().removeMapLayer(self.layersPropLayer.id()) - except: + except Exception: pass - - def buttonBoxHub(self,kod): + def buttonBoxHub(self, kod): ''' method to handle button box clicking ''' - # fix_print_with_import - print(kod) if kod == "Reset": - # fix_print_with_import - print("reset") self.removeServiceLayers() self.populateLayerTable() elif kod == "Cancel": @@ -449,25 +415,14 @@ def buttonBoxHub(self,kod): elif kod == "Apply": self.applyDSChanges() - def reconcileUnhandled(self): - self.applyDSChanges(reconcileUnhandled = True) - def run(self): """Run method that performs all the real work""" - # show the dialog if not self.dlg.isVisible(): self.populateLayerTable() - self.dlg.show() self.dlg.raise_() self.dlg.activateWindow() - # Run the dialog event loop - result = self.dlg.exec_() - # See if OK was pressed - if result: - # Do something useful here - delete the line containing pass and - # substitute with your code. - pass + self.dlg.exec() else: self.dlg.raise_() @@ -482,21 +437,22 @@ def __init__(self, parent=None): super(browseLineEdit, self).__init__(parent) self.button = QToolButton(self) - self.button.setIcon(QIcon(os.path.join(os.path.dirname(__file__),"BrowseButton.png"))) + self.button.setIcon(QIcon(os.path.join(os.path.dirname(__file__), "BrowseButton.png"))) self.button.setStyleSheet('border: 0px; padding: 0px;') - self.button.setCursor(Qt.ArrowCursor) + self.button.setCursor(Qt.CursorShape.ArrowCursor) self.button.clicked.connect(self.buttonClicked.emit) - frameWidth = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth) + frameWidth = self.style().pixelMetric(QStyle.PixelMetric.PM_DefaultFrameWidth) buttonSize = self.button.sizeHint() self.setStyleSheet('QLineEdit {padding-left: %dpx; }' % (buttonSize.width() + frameWidth + 1)) - self.setMinimumSize(max(self.minimumSizeHint().width(), buttonSize.width() + frameWidth*2 + 2), - max(self.minimumSizeHint().height(), buttonSize.height() + frameWidth*2 + 2)) + self.setMinimumSize(max(self.minimumSizeHint().width(), buttonSize.width() + frameWidth * 2 + 2), + max(self.minimumSizeHint().height(), buttonSize.height() + frameWidth * 2 + 2)) def resizeEvent(self, event): buttonSize = self.button.sizeHint() - frameWidth = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth) + frameWidth = self.style().pixelMetric(QStyle.PixelMetric.PM_DefaultFrameWidth) + # Qt6 requires integer coordinates for move() self.button.move(self.rect().right() - frameWidth - buttonSize.width(), - (self.rect().bottom() - buttonSize.height() + 1)/2) + int((self.rect().bottom() - buttonSize.height() + 1) / 2)) super(browseLineEdit, self).resizeEvent(event) diff --git a/changeDataSource_dialog.py b/changeDataSource_dialog.py index dbed5a9..53b8169 100644 --- a/changeDataSource_dialog.py +++ b/changeDataSource_dialog.py @@ -20,45 +20,32 @@ * * ***************************************************************************/ """ -from __future__ import absolute_import - -import os - -from qgis.PyQt import QtGui, uic, QtWidgets +from qgis.PyQt import QtWidgets from qgis.PyQt.QtCore import pyqtSignal from qgis.core import QgsBrowserModel, QgsMimeDataUtils + from .changeDataSource_dialog_base import Ui_changeDataSourceDialogBase from .browsedatasource import Ui_dataSourceBrowser + class changeDataSourceDialog(QtWidgets.QDialog, Ui_changeDataSourceDialogBase): + closedDialog = pyqtSignal() + def __init__(self, parent=None): """Constructor.""" super(changeDataSourceDialog, self).__init__(parent) - #QtWidgets.QDialog.__init__(self) - # Set up the user interface from Designer. - # After setupUI you can access any designer object by doing - # self., and you can use autoconnect slots - see - # http://qt-project.org/doc/qt-4.8/designer-using-a-ui-file.html - # #widgets-and-dialogs-with-auto-connect self.setupUi(self) - closedDialog = pyqtSignal() - - def closeEvent(self,evnt): + def closeEvent(self, evnt): self.closedDialog.emit() + class dataSourceBrowser(QtWidgets.QDialog, Ui_dataSourceBrowser): def __init__(self, parent=None): """Constructor.""" super(dataSourceBrowser, self).__init__(parent) - #QtWidgets.QDialog.__init__(self) - # Set up the user interface from Designer. - # After setupUI you can access any designer object by doing - # self., and you can use autoconnect slots - see - # http://qt-project.org/doc/qt-4.8/designer-using-a-ui-file.html - # #widgets-and-dialogs-with-auto-connect self.setupUi(self) self.browserModel = QgsBrowserModel() self.browserModel.initialize() @@ -69,18 +56,19 @@ def __init__(self, parent=None): self.buttonBox.accepted.connect(self.acceptedAction) self.buttonBox.rejected.connect(self.rejectedAction) self.acceptedFlag = None + self.result = (None, None, None) - def getUriFromBrowser(self,index): - uriItem = self.browserModel.dataItem(index) + def getUriFromBrowser(self, index): uri_list = QgsMimeDataUtils.decodeUriList(self.browserModel.mimeData([index])) + if not uri_list: + self.result = (None, None, None) + return try: - #print uri_list[0].providerKey,uri_list[0].uri - self.result = (uri_list[0].layerType,uri_list[0].providerKey,uri_list[0].uri) + self.result = (uri_list[0].layerType, uri_list[0].providerKey, uri_list[0].uri) self.close() self.acceptedFlag = True - except: - #print "NO VALID URI" - self.result = (None,None,None) + except (AttributeError, IndexError): + self.result = (None, None, None) def acceptedAction(self): self.getUriFromBrowser(self.dataSourceTree.currentIndex()) @@ -95,9 +83,9 @@ def rejectedAction(self): def uri(title=""): dialog = dataSourceBrowser() dialog.setWindowTitle(title) - result = dialog.exec_() + dialog.exec() dialog.show() if dialog.acceptedFlag: - return (dialog.result) + return dialog.result else: - return (None,None,None) + return (None, None, None) diff --git a/changeDataSource_dialog_base.py b/changeDataSource_dialog_base.py index 1a1112e..946998d 100644 --- a/changeDataSource_dialog_base.py +++ b/changeDataSource_dialog_base.py @@ -1,41 +1,24 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'C:\Users\DEMO\Dropbox\dev\changeDataSource\changeDataSource_dialog_base.ui' -# -# Created: Sun Nov 22 22:12:30 2015 -# by: PyQt4 UI code generator 4.10.2 -# -# WARNING! All changes made in this file will be lost! +# Form implementation generated from reading ui file 'changeDataSource_dialog_base.ui' +# Hand-updated for Qt5/Qt6 dual compatibility (qualified enums). -from builtins import object -from qgis.PyQt import QtCore, QtGui, QtWidgets - -try: - _fromUtf8 = QtCore.QString.fromUtf8 -except AttributeError: - def _fromUtf8(s): - return s +from qgis.PyQt import QtCore, QtWidgets +from qgis.gui import QgsFieldExpressionWidget -try: - _encoding = QtWidgets.QApplication.UnicodeUTF8 - def _translate(context, text, disambig): - return QtWidgets.QApplication.translate(context, text, disambig, _encoding) -except AttributeError: - def _translate(context, text, disambig): - return QtWidgets.QApplication.translate(context, text, disambig) class Ui_changeDataSourceDialogBase(object): def setupUi(self, changeDataSourceDialogBase): - changeDataSourceDialogBase.setObjectName(_fromUtf8("changeDataSourceDialogBase")) + changeDataSourceDialogBase.setObjectName("changeDataSourceDialogBase") changeDataSourceDialogBase.resize(1027, 461) self.verticalLayout = QtWidgets.QVBoxLayout(changeDataSourceDialogBase) - self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) + self.verticalLayout.setObjectName("verticalLayout") self.layerTable = QtWidgets.QTableWidget(changeDataSourceDialogBase) self.layerTable.setAlternatingRowColors(True) - self.layerTable.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) - self.layerTable.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) - self.layerTable.setGridStyle(QtCore.Qt.DotLine) - self.layerTable.setObjectName(_fromUtf8("layerTable")) + self.layerTable.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.ExtendedSelection) + self.layerTable.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows) + self.layerTable.setGridStyle(QtCore.Qt.PenStyle.DotLine) + self.layerTable.setObjectName("layerTable") self.layerTable.setColumnCount(0) self.layerTable.setRowCount(0) self.layerTable.horizontalHeader().setHighlightSections(False) @@ -43,59 +26,70 @@ def setupUi(self, changeDataSourceDialogBase): self.layerTable.verticalHeader().setVisible(True) self.verticalLayout.addWidget(self.layerTable) self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) + self.horizontalLayout.setObjectName("horizontalLayout") self.label = QtWidgets.QLabel(changeDataSourceDialogBase) - self.label.setObjectName(_fromUtf8("label")) + self.label.setObjectName("label") self.horizontalLayout.addWidget(self.label) self.findEdit = QtWidgets.QLineEdit(changeDataSourceDialogBase) self.findEdit.setMaximumSize(QtCore.QSize(100, 16777215)) - self.findEdit.setObjectName(_fromUtf8("findEdit")) + self.findEdit.setObjectName("findEdit") self.horizontalLayout.addWidget(self.findEdit) self.label_2 = QtWidgets.QLabel(changeDataSourceDialogBase) - self.label_2.setObjectName(_fromUtf8("label_2")) + self.label_2.setObjectName("label_2") self.horizontalLayout.addWidget(self.label_2) self.replaceEdit = QtWidgets.QLineEdit(changeDataSourceDialogBase) self.replaceEdit.setMaximumSize(QtCore.QSize(100, 16777215)) - self.replaceEdit.setObjectName(_fromUtf8("replaceEdit")) + self.replaceEdit.setObjectName("replaceEdit") self.horizontalLayout.addWidget(self.replaceEdit) self.label_4 = QtWidgets.QLabel(changeDataSourceDialogBase) - self.label_4.setObjectName(_fromUtf8("label_4")) + self.label_4.setObjectName("label_4") self.horizontalLayout.addWidget(self.label_4) self.mFieldExpressionWidget = QgsFieldExpressionWidget(changeDataSourceDialogBase) - self.mFieldExpressionWidget.setObjectName(_fromUtf8("mFieldExpressionWidget")) + self.mFieldExpressionWidget.setObjectName("mFieldExpressionWidget") self.horizontalLayout.addWidget(self.mFieldExpressionWidget) self.label_3 = QtWidgets.QLabel(changeDataSourceDialogBase) - self.label_3.setObjectName(_fromUtf8("label_3")) + self.label_3.setObjectName("label_3") self.horizontalLayout.addWidget(self.label_3) self.datasourceCombo = QtWidgets.QComboBox(changeDataSourceDialogBase) - self.datasourceCombo.setObjectName(_fromUtf8("datasourceCombo")) + self.datasourceCombo.setObjectName("datasourceCombo") self.horizontalLayout.addWidget(self.datasourceCombo) self.onlySelectedCheck = QtWidgets.QCheckBox(changeDataSourceDialogBase) - self.onlySelectedCheck.setObjectName(_fromUtf8("onlySelectedCheck")) + self.onlySelectedCheck.setObjectName("onlySelectedCheck") self.horizontalLayout.addWidget(self.onlySelectedCheck) self.replaceButton = QtWidgets.QPushButton(changeDataSourceDialogBase) - self.replaceButton.setObjectName(_fromUtf8("replaceButton")) + self.replaceButton.setObjectName("replaceButton") self.horizontalLayout.addWidget(self.replaceButton) self.verticalLayout.addLayout(self.horizontalLayout) self.horizontalLayout_2 = QtWidgets.QHBoxLayout() - self.horizontalLayout_2.setObjectName(_fromUtf8("horizontalLayout_2")) - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + spacerItem = QtWidgets.QSpacerItem( + 40, 20, + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Minimum, + ) self.horizontalLayout_2.addItem(spacerItem) self.handleBadLayersCheckbox = QtWidgets.QCheckBox(changeDataSourceDialogBase) - self.handleBadLayersCheckbox.setObjectName(_fromUtf8("handleBadLayersCheckbox")) + self.handleBadLayersCheckbox.setObjectName("handleBadLayersCheckbox") self.horizontalLayout_2.addWidget(self.handleBadLayersCheckbox) self.reconcileButton = QtWidgets.QPushButton(changeDataSourceDialogBase) - self.reconcileButton.setObjectName(_fromUtf8("reconcileButton")) + self.reconcileButton.setObjectName("reconcileButton") self.horizontalLayout_2.addWidget(self.reconcileButton) self.buttonBox = QtWidgets.QDialogButtonBox(changeDataSourceDialogBase) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Fixed, + QtWidgets.QSizePolicy.Policy.Fixed, + ) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.buttonBox.sizePolicy().hasHeightForWidth()) self.buttonBox.setSizePolicy(sizePolicy) - self.buttonBox.setOrientation(QtCore.Qt.Horizontal) - self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Apply|QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Reset) - self.buttonBox.setObjectName(_fromUtf8("buttonBox")) + self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal) + self.buttonBox.setStandardButtons( + QtWidgets.QDialogButtonBox.StandardButton.Apply + | QtWidgets.QDialogButtonBox.StandardButton.Cancel + | QtWidgets.QDialogButtonBox.StandardButton.Reset + ) + self.buttonBox.setObjectName("buttonBox") self.horizontalLayout_2.addWidget(self.buttonBox) self.verticalLayout.addLayout(self.horizontalLayout_2) @@ -104,15 +98,14 @@ def setupUi(self, changeDataSourceDialogBase): self.buttonBox.rejected.connect(changeDataSourceDialogBase.reject) def retranslateUi(self, changeDataSourceDialogBase): - changeDataSourceDialogBase.setWindowTitle(_translate("changeDataSourceDialogBase", "Change datasource", None)) + _translate = QtCore.QCoreApplication.translate + changeDataSourceDialogBase.setWindowTitle(_translate("changeDataSourceDialogBase", "Change datasource")) self.layerTable.setSortingEnabled(True) - self.label.setText(_translate("changeDataSourceDialogBase", "Find:", None)) - self.label_2.setText(_translate("changeDataSourceDialogBase", "Replace:", None)) - self.label_4.setText(_translate("changeDataSourceDialogBase", "expression", None)) - self.label_3.setText(_translate("changeDataSourceDialogBase", "New datasource type:", None)) - self.onlySelectedCheck.setText(_translate("changeDataSourceDialogBase", "Between selected rows", None)) - self.replaceButton.setText(_translate("changeDataSourceDialogBase", "Replace", None)) - self.handleBadLayersCheckbox.setText(_translate("changeDataSourceDialogBase", "Handle bad layers", None)) - self.reconcileButton.setText(_translate("changeDataSourceDialogBase", "Reconcile unhandled", None)) - -from qgis.gui import QgsFieldExpressionWidget + self.label.setText(_translate("changeDataSourceDialogBase", "Find:")) + self.label_2.setText(_translate("changeDataSourceDialogBase", "Replace:")) + self.label_4.setText(_translate("changeDataSourceDialogBase", "expression")) + self.label_3.setText(_translate("changeDataSourceDialogBase", "New datasource type:")) + self.onlySelectedCheck.setText(_translate("changeDataSourceDialogBase", "Between selected rows")) + self.replaceButton.setText(_translate("changeDataSourceDialogBase", "Replace")) + self.handleBadLayersCheckbox.setText(_translate("changeDataSourceDialogBase", "Handle bad layers")) + self.reconcileButton.setText(_translate("changeDataSourceDialogBase", "Reconcile unhandled")) diff --git a/metadata.txt b/metadata.txt index 59f0e18..42fd473 100644 --- a/metadata.txt +++ b/metadata.txt @@ -8,9 +8,9 @@ [general] name=changeDataSource -qgisMinimumVersion=3.0 +qgisMinimumVersion=3.20 description=Right click on layer tree to change single layer datasource of click on icon to change datasources globally -version=3.1 +version=3.2 author=enrico ferreguti email=enricofer@gmail.com about= The plugin allows to change the datasources of the loaded layers. The operation can be performed right clicking on legend items for single layer or globally on a summary table by clicking on toolbar plugin icon. The plugin takes control of bad layers handling allowing to specify valid datasources working on project @@ -36,6 +36,8 @@ changelog=V1.0 QGIS3 migration; courtesy of Christoph Franke @ChrFr V3.1 2020/10/15 Fix expression evaluation for QGIS 3.x; courtesy of Andrea Giudiceandrea @agiudiceandrea + V3.2 2026/04/20 + Qt6/QGIS 4 compatibility (dual Qt5/Qt6); native QgsMapLayer.setDataSource (preserves auxiliary storage / manual label positions and clears layer tree warning indicators); removed 250 char truncation on datasource field (fixes SQL views with WITH/window functions); added mssql/hana/wfs/wcs/arcgis providers; minor bug fixes # Tags are comma separated with spaces allowed tags=database, layers, legend, datasources, sources, bad layers, not found layers diff --git a/setdatasource.py b/setdatasource.py index 2c4ba8c..e55c3c9 100644 --- a/setdatasource.py +++ b/setdatasource.py @@ -19,26 +19,24 @@ * * ***************************************************************************/ """ -from __future__ import print_function -from __future__ import absolute_import - -from builtins import str -from builtins import range -from qgis.core import * -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from qgis.PyQt import QtCore, QtGui, QtWidgets -from PyQt5.QtXml import * +from qgis.core import ( + Qgis, + QgsDataProvider, + QgsMapLayer, + QgsProject, + QgsRasterLayer, + QgsVectorLayer, +) +from qgis.PyQt import QtWidgets +from qgis.PyQt.QtGui import QStandardItem, QStandardItemModel + from .ui_changeDSDialog import Ui_changeDataSourceDialog from .changeDataSource_dialog import dataSourceBrowser -from qgis.gui import QgsManageConnectionsDialog, QgsMessageBar -import os.path - class setDataSource(QtWidgets.QDialog, Ui_changeDataSourceDialog): - def __init__(self,parent): + def __init__(self, parent): QtWidgets.QDialog.__init__(self) self.parent = parent self.iface = parent.iface @@ -48,54 +46,65 @@ def __init__(self,parent): self.buttonBox.rejected.connect(self.cancelDialog) self.selectDatasourceCombo.activated.connect(self.selectDS) self.openBrowser.clicked.connect(self.openFileBrowser) - self.rasterDSList = {"wms":"Web Map Service (WMS)","gdal":"Raster images (GDAL)"} - self.vectorDSList = {"ogr":"Vector layers (OGR)","delimitedtext": "Delimited Text", "gpx":" GPS eXchange Format", "postgres": "Postgis database layer","spatialite": "Spatialite database layer","oracle": "Oracle spatial database layer","mysql": "Mysql spatial database layer"} - self.browsable=("ogr","gdal") + self.rasterDSList = { + "wms": "Web Map Service (WMS)", + "wcs": "Web Coverage Service (WCS)", + "gdal": "Raster images (GDAL)", + "arcgismapserver": "ArcGIS Map Server", + "xyz": "XYZ tiles", + } + self.vectorDSList = { + "ogr": "Vector layers (OGR)", + "delimitedtext": "Delimited Text", + "gpx": "GPS eXchange Format", + "postgres": "PostGIS database layer", + "spatialite": "Spatialite database layer", + "oracle": "Oracle spatial database layer", + "mssql": "MS SQL Server database layer", + "hana": "SAP HANA database layer", + "db2": "DB2 spatial database layer", + "mysql": "Mysql spatial database layer", + "wfs": "Web Feature Service (WFS)", + "arcgisfeatureserver": "ArcGIS Feature Server", + } + self.browsable = ("ogr", "gdal") def openFileBrowser(self): ''' method used to open datasource browser dialog to get new provider/uri for the single layer ''' - type,provider,fileName = dataSourceBrowser.uri() - enumLayerTypes = ("vector","raster","plugin") - if type and enumLayerTypes[self.layer.type()] != type: - self.iface.messageBar().pushMessage("Error", "Layer type mismatch: %s/%s" % (enumLayerTypes[self.layer.type()],type), level=QgsMessageBar.CRITICAL, duration=4) + layer_type, provider, fileName = dataSourceBrowser.uri() + enumLayerTypes = ("vector", "raster", "plugin") + if layer_type and enumLayerTypes[self.layer.type()] != layer_type: + self.iface.messageBar().pushMessage( + "Error", + "Layer type mismatch: %s/%s" % (enumLayerTypes[self.layer.type()], layer_type), + level=Qgis.MessageLevel.Critical, + duration=4, + ) else: if fileName: self.lineEdit.setPlainText(fileName) if provider: allSources = [self.selectDatasourceCombo.itemText(i) for i in range(self.selectDatasourceCombo.count())] - if type in allSources: + if provider in allSources: self.selectDatasourceCombo.setCurrentIndex(allSources.index(provider)) else: self.selectDatasourceCombo.addItem(provider) - self.selectDatasourceCombo.setCurrentIndex(self.selectDatasourceCombo.count()-1) + self.selectDatasourceCombo.setCurrentIndex(self.selectDatasourceCombo.count() - 1) - def selectDS(self,i): + def selectDS(self, i): ''' method to catch datasource combo edits. No longer used. Stay here for future uses. ''' pass - def openDataSourceDialog(self,layer):#,badLayersHandler): + def openDataSourceDialog(self, layer): ''' method to prep and show single datasource edit dialog ''' self.layer = layer - #self.badLayersHandler = badLayersHandler self.setWindowTitle(layer.name()) - ''' - #if layer is unhandled get unhandled parameters - if self.parent.badLayersHandler.getActualLayersIds() and self.layer.id() in self.parent.badLayersHandler.getActualLayersIds(): - - provider = self.parent.badLayersHandler.getUnhandledLayerFromActualId(self.layer.id())["provider"] - source = self.parent.badLayersHandler.getUnhandledLayerFromActualId(self.layer.id())["datasource"] - self.label.setText("unhandled URI:") - else: - provider = self.layer.dataProvider().name() - source = self.layer.source() - self.label.setText("URI:") - ''' provider = self.layer.dataProvider().name() source = self.layer.source() @@ -105,12 +114,10 @@ def openDataSourceDialog(self,layer):#,badLayersHandler): source = QgsProject.instance().readPath(source) if layer.type() == QgsMapLayer.VectorLayer: - self.populateComboBox(self.selectDatasourceCombo,list(self.vectorDSList.keys()),predef = provider) + self.populateComboBox(self.selectDatasourceCombo, list(self.vectorDSList.keys()), predef=provider) else: - self.populateComboBox(self.selectDatasourceCombo,list(self.rasterDSList.keys()),predef = provider) + self.populateComboBox(self.selectDatasourceCombo, list(self.rasterDSList.keys()), predef=provider) self.lineEdit.setPlainText(source) - #self.selectDS(self.selectDatasourceCombo.currentIndex()) - #print source self.show() self.raise_() self.activateWindow() @@ -121,103 +128,74 @@ def cancelDialog(self): ''' self.hide() - def exrecoverJoins(self, oldLayer, newLayer): - ''' - convenience method to rebuild joins if lost - ''' - for layer in QgsMapLayerRegistry.mapLayers().values(): - if layer.type() == QgsMapLayer.VectorLayer: - for joinDef in layer.vectorJoins(): - if joinDef.joinLayerId == oldLayer.id(): - newJoinDef = joinDef - newJoinDef.joinLayerId = newLayer.id() - layer.removeJoin(oldLayer.id()) - layer.addJoin(newJoinDef) - def changeDataSourceAction(self): ''' landing method clicking apply in button box ''' - self.applyDataSource(self.layer,self.selectDatasourceCombo.currentText().lower().replace(' ',''),self.lineEdit.toPlainText()) + self.applyDataSource( + self.layer, + self.selectDatasourceCombo.currentText().lower().replace(' ', ''), + self.lineEdit.toPlainText(), + ) - - def applyDataSource(self,applyLayer,newProvider,newDatasource): + def applyDataSource(self, applyLayer, newProvider, newDatasource): ''' method to verify applying datasource/provider before definitive change to avoid qgis crashes ''' self.hide() - # new layer import - # fix_print_with_import - print("applyDataSource", applyLayer.type()) if applyLayer.type() == QgsMapLayer.VectorLayer: - # fix_print_with_import - print("vector") - probeLayer = QgsVectorLayer(newDatasource,"probe", newProvider) - extent = None + probeLayer = QgsVectorLayer(newDatasource, "probe", newProvider) else: - # fix_print_with_import - print("raster") - probeLayer = QgsRasterLayer(newDatasource,"probe", newProvider) - extent = probeLayer.extent() + probeLayer = QgsRasterLayer(newDatasource, "probe", newProvider) if not probeLayer.isValid(): - self.iface.messageBar().pushMessage("Error", "New data source is not valid: "+newProvider+"|"+newDatasource, level=Qgis.Critical, duration=4) + self.iface.messageBar().pushMessage( + "Error", + "New data source is not valid: " + newProvider + "|" + newDatasource, + level=Qgis.MessageLevel.Critical, + duration=4, + ) return None - #print "geometryTypes",probeLayer.geometryType(), applyLayer.geometryType() if applyLayer.type() == QgsMapLayer.VectorLayer and probeLayer.geometryType() != applyLayer.geometryType(): - self.iface.messageBar().pushMessage("Error", "Geometry type mismatch", level=Qgis.Critical, duration=4) + self.iface.messageBar().pushMessage( + "Error", + "Geometry type mismatch", + level=Qgis.MessageLevel.Critical, + duration=4, + ) return None newDatasource = probeLayer.source() - self.setDataSource(applyLayer, newProvider, newDatasource, extent) + self.setDataSource(applyLayer, newProvider, newDatasource) return True - def setDataSource(self, layer, newProvider, newDatasource, extent=None): + def setDataSource(self, layer, newProvider, newDatasource): ''' - Method to write the new datasource to a raster Layer + Apply new datasource using the native QgsMapLayer.setDataSource API. + + Using the native method (QGIS >= 3.20) instead of XML patching preserves + auxiliary storage (manual label positions, data-defined overrides) and + emits dataSourceChanged, which lets the layer tree clear its warning / + "temporary memory" indicators when the new source is valid. ''' - if "setDataSource" in dir(layer): - qgisVersionOk = True - else: - qgisVersionOk = False - - XMLDocument = QDomDocument("style") - XMLMapLayers = XMLDocument.createElement("maplayers") - XMLMapLayer = XMLDocument.createElement("maplayer") - context = QgsReadWriteContext() - layer.writeLayerXml(XMLMapLayer,XMLDocument, context) - # apply layer definition - XMLMapLayer.firstChildElement("datasource").firstChild().setNodeValue(newDatasource) - XMLMapLayer.firstChildElement("provider").firstChild().setNodeValue(newProvider) - if extent: #if a new extent (for raster) is provided it is applied to the layer - XMLMapLayerExtent = XMLMapLayer.firstChildElement("extent") - XMLMapLayerExtent.firstChildElement("xmin").firstChild().setNodeValue(str(extent.xMinimum())) - XMLMapLayerExtent.firstChildElement("xmax").firstChild().setNodeValue(str(extent.xMaximum())) - XMLMapLayerExtent.firstChildElement("ymin").firstChild().setNodeValue(str(extent.yMinimum())) - XMLMapLayerExtent.firstChildElement("ymax").firstChild().setNodeValue(str(extent.yMaximum())) - - XMLMapLayers.appendChild(XMLMapLayer) - XMLDocument.appendChild(XMLMapLayers) - layer.readLayerXml(XMLMapLayer, context) + options = QgsDataProvider.ProviderOptions() + options.transformContext = QgsProject.instance().transformContext() + layer.setDataSource(newDatasource, layer.name(), newProvider, options) layer.reload() - self.iface.actionDraw().trigger() self.iface.mapCanvas().refresh() self.iface.layerTreeView().refreshLayerSymbology(layer.id()) - def populateComboBox(self,combo,list,dataPayload = None,predef = None,sort = None): + def populateComboBox(self, combo, items, dataPayload=None, predef=None, sort=None): ''' procedure to fill specified combobox with provided list ''' - combo.blockSignals (True) + combo.blockSignals(True) combo.clear() - model=QStandardItemModel(combo) + model = QStandardItemModel(combo) predefInList = None - for elem in list: - try: - item = QStandardItem(str(elem)) - except TypeError: - item = QStandardItem(str(elem)) + for elem in items: + item = QStandardItem(str(elem)) model.appendRow(item) if elem == predef: predefInList = elem @@ -228,6 +206,6 @@ def populateComboBox(self,combo,list,dataPayload = None,predef = None,sort = Non if predefInList: combo.setCurrentIndex(combo.findText(predefInList)) else: - combo.insertItem(0,predef) + combo.insertItem(0, predef) combo.setCurrentIndex(0) - combo.blockSignals (False) + combo.blockSignals(False) diff --git a/ui_changeDSDialog.py b/ui_changeDSDialog.py index 52eeea4..d428072 100644 --- a/ui_changeDSDialog.py +++ b/ui_changeDSDialog.py @@ -1,62 +1,47 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'Z:\dev\changeDataSource\ui_changeDSDialog.ui' -# -# Created: Tue Sep 29 13:40:52 2015 -# by: PyQt4 UI code generator 4.11.3 -# -# WARNING! All changes made in this file will be lost! +# Form implementation generated from reading ui file 'ui_changeDSDialog.ui' +# Hand-updated for Qt5/Qt6 dual compatibility (qualified enums). -from builtins import object -from qgis.PyQt import QtCore, QtGui, QtWidgets +from qgis.PyQt import QtCore, QtWidgets -try: - _fromUtf8 = QtCore.QString.fromUtf8 -except AttributeError: - def _fromUtf8(s): - return s - -try: - _encoding = QtWidgets.QApplication.UnicodeUTF8 - def _translate(context, text, disambig): - return QtWidgets.QApplication.translate(context, text, disambig, _encoding) -except AttributeError: - def _translate(context, text, disambig): - return QtWidgets.QApplication.translate(context, text, disambig) class Ui_changeDataSourceDialog(object): def setupUi(self, changeDataSourceDialog): - changeDataSourceDialog.setObjectName(_fromUtf8("changeDataSourceDialog")) + changeDataSourceDialog.setObjectName("changeDataSourceDialog") changeDataSourceDialog.resize(297, 305) self.verticalLayout = QtWidgets.QVBoxLayout(changeDataSourceDialog) - self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) + self.verticalLayout.setObjectName("verticalLayout") self.label_2 = QtWidgets.QLabel(changeDataSourceDialog) - self.label_2.setObjectName(_fromUtf8("label_2")) + self.label_2.setObjectName("label_2") self.verticalLayout.addWidget(self.label_2) self.selectDatasourceCombo = QtWidgets.QComboBox(changeDataSourceDialog) - self.selectDatasourceCombo.setObjectName(_fromUtf8("selectDatasourceCombo")) + self.selectDatasourceCombo.setObjectName("selectDatasourceCombo") self.verticalLayout.addWidget(self.selectDatasourceCombo) self.label = QtWidgets.QLabel(changeDataSourceDialog) - self.label.setObjectName(_fromUtf8("label")) + self.label.setObjectName("label") self.verticalLayout.addWidget(self.label) self.lineEdit = QtWidgets.QPlainTextEdit(changeDataSourceDialog) - self.lineEdit.setObjectName(_fromUtf8("lineEdit")) + self.lineEdit.setObjectName("lineEdit") self.verticalLayout.addWidget(self.lineEdit) self.openBrowser = QtWidgets.QPushButton(changeDataSourceDialog) - self.openBrowser.setObjectName(_fromUtf8("openBrowser")) + self.openBrowser.setObjectName("openBrowser") self.verticalLayout.addWidget(self.openBrowser) self.buttonBox = QtWidgets.QDialogButtonBox(changeDataSourceDialog) - self.buttonBox.setOrientation(QtCore.Qt.Horizontal) - self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) - self.buttonBox.setObjectName(_fromUtf8("buttonBox")) + self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal) + self.buttonBox.setStandardButtons( + QtWidgets.QDialogButtonBox.StandardButton.Cancel + | QtWidgets.QDialogButtonBox.StandardButton.Ok + ) + self.buttonBox.setObjectName("buttonBox") self.verticalLayout.addWidget(self.buttonBox) self.retranslateUi(changeDataSourceDialog) QtCore.QMetaObject.connectSlotsByName(changeDataSourceDialog) def retranslateUi(self, changeDataSourceDialog): - changeDataSourceDialog.setWindowTitle(_translate("changeDataSourceDialog", "undoLayerChanges", None)) - self.label_2.setText(_translate("changeDataSourceDialog", "Datasource Types", None)) - self.label.setText(_translate("changeDataSourceDialog", "URI:", None)) - self.openBrowser.setText(_translate("changeDataSourceDialog", "Browse", None)) - + _translate = QtCore.QCoreApplication.translate + changeDataSourceDialog.setWindowTitle(_translate("changeDataSourceDialog", "undoLayerChanges")) + self.label_2.setText(_translate("changeDataSourceDialog", "Datasource Types")) + self.label.setText(_translate("changeDataSourceDialog", "URI:")) + self.openBrowser.setText(_translate("changeDataSourceDialog", "Browse"))