QGIS/python/plugins/processing/algs/qgis/ui/FieldsMappingPanel.py
Bas Couwenberg 2628c480c5 Don't include revision in sources.
Prevent changes to files that weren't changed between releases.
This eases review of the changes between releases significantly.
2019-05-17 16:47:47 +02:00

567 lines
19 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'
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.QtWidgets import (
QComboBox,
QHeaderView,
QLineEdit,
QSpacerItem,
QMessageBox,
QSpinBox,
QStyledItemDelegate,
QWidget,
QVBoxLayout
)
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, DIALOG_BATCH
from processing.tools import dataobjects
from processing.algs.qgis.FieldsMapper import FieldsMapper
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.List, "List"),
(QVariant.Bool, "Boolean")])
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")
}]
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):
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 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 setContextGenerator(self, generator):
self.model.setContextGenerator(generator)
def setDelegate(self, column_name, delegate):
self.fieldsView.setItemDelegateForColumn(
self.model.columnIndex(column_name),
delegate)
def setLayer(self, layer):
if self.model.layer() == layer:
return
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 createPanel(self):
return FieldsMappingPanel()
def createWidget(self):
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)
fieldsMappingInputs = self.dialog.getAvailableValuesOfType(FieldsMapper.ParameterFieldsMapping)
for input in fieldsMappingInputs:
self.combobox.addItem(self.dialog.resolveValueDescription(input), input)
def updatePanelEnabledState():
if self.combobox.currentData() is None:
self.panel.setEnabled(True)
else:
self.panel.setEnabled(False)
self.combobox.currentIndexChanged.connect(updatePanelEnabledState)
widget = QWidget()
widget.setLayout(QVBoxLayout())
widget.layout().addWidget(self.combobox)
widget.layout().addWidget(self.panel)
return widget
else:
return self.panel
def postInitialize(self, wrappers):
for wrapper in wrappers:
if wrapper.parameterDefinition().name() == self.parameterDefinition().parentLayerParameter():
if wrapper.parameterValue():
self.setLayer(wrapper.parameterValue())
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.panel.setLayer(self._layer)
def linkedVectorLayer(self):
return self._layer
def setValue(self, value):
self.panel.setValue(value)
def value(self):
if self.dialogType == DIALOG_MODELER:
if self.combobox.currentData() is None:
return self.panel.value()
else:
return self.comboValue(combobox=self.combobox)
else:
return self.panel.value()