mirror of
https://github.com/qgis/QGIS.git
synced 2025-03-09 00:35:20 -05:00
Inside the modeler, there is not enough knowledge about the layer on which the refactor fields algorithm will run. Let's be graceful with error messages here therefore.
516 lines
17 KiB
Python
516 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
***************************************************************************
|
|
FieldsMappingWidget.py
|
|
---------------------
|
|
Date : October 2014
|
|
Copyright : (C) 2014 by Arnaud Morvan
|
|
Email : arnaud dot morvan at camptocamp dot com
|
|
***************************************************************************
|
|
* *
|
|
* 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. *
|
|
* *
|
|
***************************************************************************
|
|
"""
|
|
|
|
__author__ = 'Arnaud Morvan'
|
|
__date__ = 'October 2014'
|
|
__copyright__ = '(C) 2014, Arnaud Morvan'
|
|
|
|
# This will get replaced with a git SHA1 when you do a git archive
|
|
|
|
__revision__ = '$Format:%H$'
|
|
|
|
import os
|
|
from collections import OrderedDict
|
|
|
|
from qgis.PyQt import uic
|
|
from qgis.PyQt.QtCore import (
|
|
QItemSelectionModel,
|
|
QAbstractTableModel,
|
|
QModelIndex,
|
|
QVariant,
|
|
Qt,
|
|
pyqtSlot,
|
|
)
|
|
from qgis.PyQt.QtWidgets import (
|
|
QComboBox,
|
|
QHeaderView,
|
|
QLineEdit,
|
|
QSpacerItem,
|
|
QMessageBox,
|
|
QSpinBox,
|
|
QStyledItemDelegate,
|
|
)
|
|
|
|
from qgis.core import (
|
|
QgsApplication,
|
|
QgsExpression,
|
|
QgsMapLayerProxyModel,
|
|
QgsProcessingFeatureSourceDefinition,
|
|
QgsProcessingUtils,
|
|
QgsProject,
|
|
QgsVectorLayer,
|
|
)
|
|
from qgis.gui import QgsFieldExpressionWidget
|
|
|
|
from processing.gui.wrappers import WidgetWrapper, DIALOG_STANDARD, DIALOG_MODELER
|
|
from processing.tools import dataobjects
|
|
|
|
|
|
pluginPath = os.path.dirname(__file__)
|
|
WIDGET, BASE = uic.loadUiType(
|
|
os.path.join(pluginPath, 'fieldsmappingpanelbase.ui'))
|
|
|
|
|
|
class FieldsMappingModel(QAbstractTableModel):
|
|
|
|
fieldTypes = OrderedDict([
|
|
(QVariant.Date, "Date"),
|
|
(QVariant.DateTime, "DateTime"),
|
|
(QVariant.Double, "Double"),
|
|
(QVariant.Int, "Integer"),
|
|
(QVariant.LongLong, "Integer64"),
|
|
(QVariant.String, "String"),
|
|
(QVariant.Bool, "Boolean")])
|
|
|
|
def __init__(self, parent=None):
|
|
super(FieldsMappingModel, self).__init__(parent)
|
|
self._mapping = []
|
|
self._layer = None
|
|
self.configure()
|
|
|
|
def configure(self):
|
|
self.columns = [{
|
|
'name': 'expression',
|
|
'type': QgsExpression,
|
|
'header': self.tr("Source expression"),
|
|
'persistentEditor': True
|
|
}, {
|
|
'name': 'name',
|
|
'type': QVariant.String,
|
|
'header': self.tr("Field name")
|
|
}, {
|
|
'name': 'type',
|
|
'type': QVariant.Type,
|
|
'header': self.tr("Type"),
|
|
'persistentEditor': True
|
|
}, {
|
|
'name': 'length',
|
|
'type': QVariant.Int,
|
|
'header': self.tr("Length")
|
|
}, {
|
|
'name': 'precision',
|
|
'type': QVariant.Int,
|
|
'header': self.tr("Precision")
|
|
}]
|
|
|
|
def columnIndex(self, column_name):
|
|
for index, column in enumerate(self.columns):
|
|
if column['name'] == column_name:
|
|
return index
|
|
|
|
def mapping(self):
|
|
return self._mapping
|
|
|
|
def setMapping(self, value):
|
|
self.beginResetModel()
|
|
self._mapping = value
|
|
self.endResetModel()
|
|
|
|
def contextGenerator(self):
|
|
if self._layer:
|
|
return self._layer
|
|
return QgsProject.instance()
|
|
|
|
def layer(self):
|
|
return self._layer
|
|
|
|
def setLayer(self, layer):
|
|
self._layer = layer
|
|
|
|
def columnCount(self, parent=QModelIndex()):
|
|
if parent.isValid():
|
|
return 0
|
|
return len(self.columns)
|
|
|
|
def rowCount(self, parent=QModelIndex()):
|
|
if parent.isValid():
|
|
return 0
|
|
return self._mapping.__len__()
|
|
|
|
def headerData(self, section, orientation, role=Qt.DisplayRole):
|
|
if role == Qt.DisplayRole:
|
|
if orientation == Qt.Horizontal:
|
|
return self.columns[section]['header']
|
|
if orientation == Qt.Vertical:
|
|
return section
|
|
|
|
def flags(self, index):
|
|
return Qt.ItemFlags(Qt.ItemIsSelectable |
|
|
Qt.ItemIsEditable |
|
|
Qt.ItemIsEnabled)
|
|
|
|
def data(self, index, role=Qt.DisplayRole):
|
|
field = self._mapping[index.row()]
|
|
column_def = self.columns[index.column()]
|
|
|
|
if role == Qt.DisplayRole:
|
|
value = field[column_def['name']]
|
|
if column_def['type'] == QVariant.Type:
|
|
if value == QVariant.Invalid:
|
|
return ''
|
|
return self.fieldTypes[value]
|
|
return value
|
|
|
|
if role == Qt.EditRole:
|
|
return field[column_def['name']]
|
|
|
|
if role == Qt.TextAlignmentRole:
|
|
if column_def['type'] in [QVariant.Int]:
|
|
hAlign = Qt.AlignRight
|
|
else:
|
|
hAlign = Qt.AlignLeft
|
|
return hAlign + Qt.AlignVCenter
|
|
|
|
def setData(self, index, value, role=Qt.EditRole):
|
|
field = self._mapping[index.row()]
|
|
column_def = self.columns[index.column()]
|
|
|
|
if role == Qt.EditRole:
|
|
field[column_def['name']] = value
|
|
self.dataChanged.emit(index, index)
|
|
|
|
return True
|
|
|
|
def insertRows(self, row, count, index=QModelIndex()):
|
|
self.beginInsertRows(index, row, row + count - 1)
|
|
|
|
for i in range(count):
|
|
field = self.newField()
|
|
self._mapping.insert(row + i, field)
|
|
|
|
self.endInsertRows()
|
|
return True
|
|
|
|
def removeRows(self, row, count, index=QModelIndex()):
|
|
self.beginRemoveRows(index, row, row + count - 1)
|
|
|
|
for i in range(row + count - 1, row + 1):
|
|
self._mapping.pop(i)
|
|
|
|
self.endRemoveRows()
|
|
return True
|
|
|
|
def newField(self, field=None):
|
|
if field is None:
|
|
return {'name': '',
|
|
'type': QVariant.Invalid,
|
|
'length': 0,
|
|
'precision': 0,
|
|
'expression': ''}
|
|
|
|
return {'name': field.name(),
|
|
'type': field.type(),
|
|
'length': field.length(),
|
|
'precision': field.precision(),
|
|
'expression': QgsExpression.quotedColumnRef(field.name())}
|
|
|
|
def loadLayerFields(self, layer):
|
|
self.beginResetModel()
|
|
|
|
self._mapping = []
|
|
if layer is not None:
|
|
for field in layer.fields():
|
|
self._mapping.append(self.newField(field))
|
|
|
|
self.endResetModel()
|
|
|
|
|
|
class FieldTypeDelegate(QStyledItemDelegate):
|
|
|
|
def createEditor(self, parent, option, index):
|
|
editor = QComboBox(parent)
|
|
for key, text in list(FieldsMappingModel.fieldTypes.items()):
|
|
editor.addItem(text, key)
|
|
return editor
|
|
|
|
def setEditorData(self, editor, index):
|
|
if not editor:
|
|
return
|
|
value = index.model().data(index, Qt.EditRole)
|
|
editor.setCurrentIndex(editor.findData(value))
|
|
|
|
def setModelData(self, editor, model, index):
|
|
if not editor:
|
|
return
|
|
value = editor.currentData()
|
|
if value is None:
|
|
value = QVariant.Invalid
|
|
model.setData(index, value)
|
|
|
|
|
|
class ExpressionDelegate(QStyledItemDelegate):
|
|
|
|
def createEditor(self, parent, option, index):
|
|
editor = QgsFieldExpressionWidget(parent)
|
|
editor.setLayer(index.model().layer())
|
|
editor.registerExpressionContextGenerator(index.model().contextGenerator())
|
|
editor.fieldChanged.connect(self.on_expression_fieldChange)
|
|
editor.setAutoFillBackground(True)
|
|
editor.setAllowEvalErrors(self.parent().dialogType == DIALOG_MODELER)
|
|
return editor
|
|
|
|
def setEditorData(self, editor, index):
|
|
if not editor:
|
|
return
|
|
value = index.model().data(index, Qt.EditRole)
|
|
editor.setField(value)
|
|
|
|
def setModelData(self, editor, model, index):
|
|
if not editor:
|
|
return
|
|
(value, isExpression, isValid) = editor.currentField()
|
|
if isExpression is True:
|
|
model.setData(index, value)
|
|
else:
|
|
model.setData(index, QgsExpression.quotedColumnRef(value))
|
|
|
|
def on_expression_fieldChange(self, fieldName):
|
|
self.commitData.emit(self.sender())
|
|
|
|
|
|
class FieldsMappingPanel(BASE, WIDGET):
|
|
|
|
def __init__(self, parent=None):
|
|
super(FieldsMappingPanel, self).__init__(parent)
|
|
self.setupUi(self)
|
|
|
|
self.addButton.setIcon(QgsApplication.getThemeIcon("/mActionNewAttribute.svg"))
|
|
self.deleteButton.setIcon(QgsApplication.getThemeIcon('/mActionDeleteAttribute.svg'))
|
|
self.upButton.setIcon(QgsApplication.getThemeIcon('/mActionArrowUp.svg'))
|
|
self.downButton.setIcon(QgsApplication.getThemeIcon('/mActionArrowDown.svg'))
|
|
self.resetButton.setIcon(QgsApplication.getThemeIcon('/mIconClearText.svg'))
|
|
|
|
self.configure()
|
|
|
|
self.model.modelReset.connect(self.on_model_modelReset)
|
|
self.model.rowsInserted.connect(self.on_model_rowsInserted)
|
|
|
|
self.layerCombo.setAllowEmptyLayer(True)
|
|
self.layerCombo.setFilters(QgsMapLayerProxyModel.VectorLayer)
|
|
self.dialogType = None
|
|
|
|
def configure(self):
|
|
self.model = FieldsMappingModel()
|
|
self.fieldsView.setModel(self.model)
|
|
|
|
self.setDelegate('expression', ExpressionDelegate(self))
|
|
self.setDelegate('type', FieldTypeDelegate(self))
|
|
|
|
def setDelegate(self, column_name, delegate):
|
|
self.fieldsView.setItemDelegateForColumn(
|
|
self.model.columnIndex(column_name),
|
|
delegate)
|
|
|
|
def setLayer(self, layer):
|
|
self.model.setLayer(layer)
|
|
if layer is None:
|
|
return
|
|
if self.model.rowCount() == 0:
|
|
self.on_resetButton_clicked()
|
|
return
|
|
dlg = QMessageBox(self)
|
|
dlg.setText(self.tr("Do you want to reset the field mapping?"))
|
|
dlg.setStandardButtons(
|
|
QMessageBox.StandardButtons(QMessageBox.Yes |
|
|
QMessageBox.No))
|
|
dlg.setDefaultButton(QMessageBox.No)
|
|
if dlg.exec_() == QMessageBox.Yes:
|
|
self.on_resetButton_clicked()
|
|
|
|
def value(self):
|
|
return self.model.mapping()
|
|
|
|
def setValue(self, value):
|
|
self.model.setMapping(value)
|
|
|
|
@pyqtSlot(bool, name='on_addButton_clicked')
|
|
def on_addButton_clicked(self, checked=False):
|
|
rowCount = self.model.rowCount()
|
|
self.model.insertRows(rowCount, 1)
|
|
index = self.model.index(rowCount, 0)
|
|
self.fieldsView.selectionModel().select(
|
|
index,
|
|
QItemSelectionModel.SelectionFlags(
|
|
QItemSelectionModel.Clear |
|
|
QItemSelectionModel.Select |
|
|
QItemSelectionModel.Current |
|
|
QItemSelectionModel.Rows))
|
|
self.fieldsView.scrollTo(index)
|
|
self.fieldsView.scrollTo(index)
|
|
|
|
@pyqtSlot(bool, name='on_deleteButton_clicked')
|
|
def on_deleteButton_clicked(self, checked=False):
|
|
sel = self.fieldsView.selectionModel()
|
|
if not sel.hasSelection():
|
|
return
|
|
|
|
indexes = sel.selectedRows()
|
|
for index in indexes:
|
|
self.model.removeRows(index.row(), 1)
|
|
|
|
@pyqtSlot(bool, name='on_upButton_clicked')
|
|
def on_upButton_clicked(self, checked=False):
|
|
sel = self.fieldsView.selectionModel()
|
|
if not sel.hasSelection():
|
|
return
|
|
|
|
row = sel.selectedRows()[0].row()
|
|
if row == 0:
|
|
return
|
|
|
|
self.model.insertRows(row - 1, 1)
|
|
|
|
for column in range(self.model.columnCount()):
|
|
srcIndex = self.model.index(row + 1, column)
|
|
dstIndex = self.model.index(row - 1, column)
|
|
value = self.model.data(srcIndex, Qt.EditRole)
|
|
self.model.setData(dstIndex, value, Qt.EditRole)
|
|
|
|
self.model.removeRows(row + 1, 1)
|
|
|
|
sel.select(
|
|
self.model.index(row - 1, 0),
|
|
QItemSelectionModel.SelectionFlags(
|
|
QItemSelectionModel.Clear |
|
|
QItemSelectionModel.Select |
|
|
QItemSelectionModel.Current |
|
|
QItemSelectionModel.Rows))
|
|
|
|
@pyqtSlot(bool, name='on_downButton_clicked')
|
|
def on_downButton_clicked(self, checked=False):
|
|
sel = self.fieldsView.selectionModel()
|
|
if not sel.hasSelection():
|
|
return
|
|
|
|
row = sel.selectedRows()[0].row()
|
|
if row == self.model.rowCount() - 1:
|
|
return
|
|
|
|
self.model.insertRows(row + 2, 1)
|
|
|
|
for column in range(self.model.columnCount()):
|
|
srcIndex = self.model.index(row, column)
|
|
dstIndex = self.model.index(row + 2, column)
|
|
value = self.model.data(srcIndex, Qt.EditRole)
|
|
self.model.setData(dstIndex, value, Qt.EditRole)
|
|
|
|
self.model.removeRows(row, 1)
|
|
|
|
sel.select(
|
|
self.model.index(row + 1, 0),
|
|
QItemSelectionModel.SelectionFlags(
|
|
QItemSelectionModel.Clear |
|
|
QItemSelectionModel.Select |
|
|
QItemSelectionModel.Current |
|
|
QItemSelectionModel.Rows))
|
|
|
|
@pyqtSlot(bool, name='on_resetButton_clicked')
|
|
def on_resetButton_clicked(self, checked=False):
|
|
self.model.loadLayerFields(self.model.layer())
|
|
|
|
def resizeColumns(self):
|
|
header = self.fieldsView.horizontalHeader()
|
|
header.resizeSections(QHeaderView.ResizeToContents)
|
|
for section in range(header.count()):
|
|
size = header.sectionSize(section)
|
|
fieldType = self.model.columns[section]['type']
|
|
if fieldType == QgsExpression:
|
|
header.resizeSection(section, size + 100)
|
|
else:
|
|
header.resizeSection(section, size + 20)
|
|
|
|
def openPersistentEditors(self, row):
|
|
for index, column in enumerate(self.model.columns):
|
|
if 'persistentEditor' in column.keys() and column['persistentEditor']:
|
|
self.fieldsView.openPersistentEditor(self.model.index(row, index))
|
|
continue
|
|
|
|
editor = self.fieldsView.indexWidget(self.model.index(row, index))
|
|
if isinstance(editor, QLineEdit):
|
|
editor.deselect()
|
|
if isinstance(editor, QSpinBox):
|
|
lineEdit = editor.findChild(QLineEdit)
|
|
lineEdit.setAlignment(Qt.AlignRight or Qt.AlignVCenter)
|
|
lineEdit.deselect()
|
|
|
|
def on_model_modelReset(self):
|
|
for row in range(0, self.model.rowCount()):
|
|
self.openPersistentEditors(row)
|
|
self.resizeColumns()
|
|
|
|
def on_model_rowsInserted(self, parent, start, end):
|
|
for row in range(start, end + 1):
|
|
self.openPersistentEditors(row)
|
|
|
|
@pyqtSlot(bool, name='on_loadLayerFieldsButton_clicked')
|
|
def on_loadLayerFieldsButton_clicked(self, checked=False):
|
|
layer = self.layerCombo.currentLayer()
|
|
if layer is None:
|
|
return
|
|
self.model.loadLayerFields(layer)
|
|
|
|
|
|
class FieldsMappingWidgetWrapper(WidgetWrapper):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(FieldsMappingWidgetWrapper, self).__init__(*args, **kwargs)
|
|
self._layer = None
|
|
|
|
def createWidget(self):
|
|
panel = FieldsMappingPanel()
|
|
panel.dialogType = self.dialogType
|
|
return panel
|
|
|
|
def postInitialize(self, wrappers):
|
|
for wrapper in wrappers:
|
|
if wrapper.param.name() == self.param.parentLayerParameter():
|
|
if wrapper.value():
|
|
self.setLayer(wrapper.value())
|
|
wrapper.widgetValueHasChanged.connect(self.parentLayerChanged)
|
|
break
|
|
|
|
# remove exiting spacers to get FieldsMappingPanel fully expanded
|
|
if self.dialogType in (DIALOG_STANDARD, DIALOG_MODELER):
|
|
layout = self.widget.parent().layout()
|
|
spacer = layout.itemAt(layout.count() - 1)
|
|
if isinstance(spacer, QSpacerItem):
|
|
layout.removeItem(spacer)
|
|
|
|
def parentLayerChanged(self, layer=None):
|
|
self.setLayer(self.sender().value())
|
|
|
|
def setLayer(self, layer):
|
|
context = dataobjects.createContext()
|
|
if layer == self._layer:
|
|
return
|
|
if isinstance(layer, QgsProcessingFeatureSourceDefinition):
|
|
layer, ok = layer.source.valueAsString(context.expressionContext())
|
|
if isinstance(layer, str):
|
|
layer = QgsProcessingUtils.mapLayerFromString(layer, context)
|
|
if not isinstance(layer, QgsVectorLayer):
|
|
layer = None
|
|
self._layer = layer
|
|
self.widget.setLayer(self._layer)
|
|
|
|
def setValue(self, value):
|
|
self.widget.setValue(value)
|
|
|
|
def value(self):
|
|
return self.widget.value()
|