QGIS/python/plugins/processing/algs/qgis/ui/FieldsMappingPanel.py
Matthias Kuhn b619dcb00a Refactor fields configuration in the modeler does not insist on layer
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.
2017-12-14 09:04:07 +01:00

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()