Use QgsFieldMappingWigdet in processing UI

This commit is contained in:
Alessandro Pasotti 2020-03-18 15:16:20 +01:00 committed by Nyall Dawson
parent dcb4987079
commit e1044d87f1
2 changed files with 74 additions and 421 deletions

View File

@ -22,49 +22,36 @@ __date__ = 'October 2014'
__copyright__ = '(C) 2014, Arnaud Morvan'
import os
from collections import OrderedDict
from qgis.PyQt import uic
from qgis.PyQt.QtCore import (
QItemSelectionModel,
QAbstractTableModel,
QModelIndex,
QVariant,
Qt,
pyqtSlot,
QCoreApplication
)
from qgis.PyQt.QtGui import (
QBrush,
QColor
QCoreApplication,
QVariant,
)
from qgis.PyQt.QtWidgets import (
QComboBox,
QHeaderView,
QLineEdit,
QSpacerItem,
QMessageBox,
QSpinBox,
QStyledItemDelegate,
QWidget,
QVBoxLayout
)
from qgis.core import (
QgsApplication,
QgsExpression,
QgsMapLayerProxyModel,
QgsProcessingFeatureSourceDefinition,
QgsProcessingUtils,
QgsProject,
QgsVectorLayer,
QgsFieldConstraints
QgsField,
QgsFields,
QgsExpression,
)
from qgis.gui import QgsFieldExpressionWidget
from processing.gui.wrappers import WidgetWrapper, DIALOG_STANDARD, DIALOG_MODELER, DIALOG_BATCH
from processing.gui.wrappers import WidgetWrapper, DIALOG_STANDARD, DIALOG_MODELER
from processing.tools import dataobjects
from processing.algs.qgis.FieldsMapper import FieldsMapper
@ -74,280 +61,6 @@ 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.List, "List"),
(QVariant.Bool, "Boolean")])
constraints = {
QgsFieldConstraints.ConstraintNotNull: "NOT NULL",
QgsFieldConstraints.ConstraintUnique: "Unique",
QgsFieldConstraints.ConstraintExpression: "Expression constraint"
}
def __init__(self, parent=None):
super(FieldsMappingModel, self).__init__(parent)
self._mapping = []
self._layer = None
self.configure()
self._generator = None
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")
}, {
'name': 'constraints',
'type': QVariant.String,
'header': self.tr("Template properties")
}]
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 setContextGenerator(self, generator):
self._generator = generator
def contextGenerator(self):
if self._generator:
return self._generator
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
try:
return len(self._mapping)
except TypeError:
return 0
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):
column_def = self.columns[index.column()]
flags = Qt.ItemFlags(Qt.ItemIsSelectable |
Qt.ItemIsEnabled)
if column_def['name'] != 'constraints':
flags = flags | Qt.ItemIsEditable
return flags
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['name'] in field else QVariant()
if column_def['type'] == QVariant.Type:
if value == QVariant.Invalid:
return ''
return self.fieldTypes[value]
elif column_def['name'] == 'constraints' and value:
return self.tr("Constraints active")
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
if role == Qt.BackgroundRole:
return QBrush(QColor(255, 224, 178)) if 'constraints' in field and field['constraints'] else QVariant()
if role == Qt.ToolTipRole:
if column_def['name'] == 'constraints' and 'constraints' in field:
return "<br>".join([self.constraints[constraint] for constraint in field['constraints']])
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': '',
'constraints': ''}
return {'name': field.name(),
'type': field.type(),
'length': field.length(),
'precision': field.precision(),
'expression': QgsExpression.quotedColumnRef(field.name()),
'constraints': self.get_field_constraints(field.constraints())}
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()
def get_field_constraints(self, field_constraints):
constraints = list()
if field_constraints.constraints() & QgsFieldConstraints.ConstraintNotNull and \
field_constraints.constraintStrength(
QgsFieldConstraints.ConstraintNotNull) & QgsFieldConstraints.ConstraintStrengthHard:
constraints.append(QgsFieldConstraints.ConstraintNotNull)
if field_constraints.constraints() & QgsFieldConstraints.ConstraintUnique and \
field_constraints.constraintStrength(
QgsFieldConstraints.ConstraintUnique) & QgsFieldConstraints.ConstraintStrengthHard:
constraints.append(QgsFieldConstraints.ConstraintUnique)
if field_constraints.constraints() & QgsFieldConstraints.ConstraintExpression and \
field_constraints.constraintStrength(
QgsFieldConstraints.ConstraintExpression) & QgsFieldConstraints.ConstraintStrengthHard:
constraints.append(QgsFieldConstraints.ConstraintExpression)
return constraints
class FieldTypeDelegate(QStyledItemDelegate):
def createEditor(self, parent, option, index):
editor = QComboBox(parent)
for key, text in 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):
@ -362,35 +75,20 @@ class FieldsMappingPanel(BASE, WIDGET):
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
self.layer = None
def configure(self):
self.model = FieldsMappingModel()
self.fieldsView.setModel(self.model)
self.setDelegate('expression', ExpressionDelegate(self))
self.setDelegate('type', FieldTypeDelegate(self))
def setContextGenerator(self, generator):
self.model.setContextGenerator(generator)
def setDelegate(self, column_name, delegate):
self.fieldsView.setItemDelegateForColumn(
self.model.columnIndex(column_name),
delegate)
self.model = self.fieldsView.model()
self.fieldsView.setDestinationEditable(True)
def setLayer(self, layer):
if self.model.layer() == layer:
if layer is None or self.layer == layer:
return
self.model.setLayer(layer)
if layer is None:
return
if self.model.rowCount() == 0:
self.layer = layer
if self.model.rowCount(QModelIndex()) == 0:
self.on_resetButton_clicked()
return
dlg = QMessageBox(self)
@ -403,15 +101,43 @@ class FieldsMappingPanel(BASE, WIDGET):
self.on_resetButton_clicked()
def value(self):
return self.model.mapping()
# Value is a dict with name, type, length, precision and expression
mapping = self.fieldsView.mapping()
results = []
for f in mapping:
results.append({
'name': f.field.name(),
'type': f.field.type(),
'length': f.field.length(),
'precision': f.field.precision(),
'expression': f.expression.expression(),
})
return results
def setValue(self, value):
self.model.setMapping(value)
if type(value) != dict:
return
destinationFields = QgsFields()
expressions = {}
for field_def in value:
f = QgsField(field_def.get('name'),
field_def.get('type', QVariant.Invalid),
field_def.get(QVariant.typeToName(field_def.get('type', QVariant.Invalid))),
field_def.get('length', 0),
field_def.get('precision', 0))
try:
expressions[f.name()] = QgsExpression(field_def['expressions'])
except AttributeError:
pass
destinationFields.append(f)
if len(destinationFields):
self.fieldsView.setDestinationFields(destinationFields, expressions)
@pyqtSlot(bool, name='on_addButton_clicked')
def on_addButton_clicked(self, checked=False):
rowCount = self.model.rowCount()
self.model.insertRows(rowCount, 1)
rowCount = self.model.rowCount(QModelIndex())
self.model.appendField(QgsField('new_field'))
index = self.model.index(rowCount, 0)
self.fieldsView.selectionModel().select(
index,
@ -421,118 +147,32 @@ class FieldsMappingPanel(BASE, WIDGET):
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)
self.fieldsView.removeSelectedFields()
@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))
self.fieldsView.moveSelectedFieldsUp()
@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))
self.fieldsView.moveSelectedFieldsDown()
@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)
"""Load fields from layer"""
if self.layer:
self.fieldsView.setDestinationFields(self.layer.fields())
self.on_loadLayerFieldsButton_clicked()
@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)
self.fieldsView.setSourceFields(layer.fields())
class FieldsMappingWidgetWrapper(WidgetWrapper):
@ -548,8 +188,6 @@ class FieldsMappingWidgetWrapper(WidgetWrapper):
self.panel = self.createPanel()
self.panel.dialogType = self.dialogType
self.panel.setContextGenerator(self)
if self.dialogType == DIALOG_MODELER:
self.combobox = QComboBox()
self.combobox.addItem(QCoreApplication.translate('Processing', '[Preconfigure]'), None)

View File

@ -20,18 +20,27 @@
<string>Fields</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="margin">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QTableView" name="fieldsView">
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
<widget class="QgsFieldMappingWidget" name="fieldsView" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
@ -150,6 +159,12 @@
<header>qgis.gui</header>
<container>1</container>
</customwidget>
<customwidget>
<class>QgsFieldMappingWidget</class>
<extends>QWidget</extends>
<header>qgis.gui</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>