mirror of
https://github.com/qgis/QGIS.git
synced 2025-10-22 00:07:53 -04:00
+ By adding a new 'NOT NULL' column in the mapping widget, and + By adding an orange color (same as used in attribute form warnings) to the whole row. New paragraph added to help panel to let users know why some rows are coloured.
583 lines
19 KiB
Python
583 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.QtGui import (
|
|
QBrush,
|
|
QColor
|
|
)
|
|
|
|
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
|
|
)
|
|
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")
|
|
}, {
|
|
'name': 'not_null',
|
|
'type': QVariant.Bool,
|
|
'header': self.tr("NOT NULL")
|
|
}]
|
|
|
|
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
|
|
|
|
if role == Qt.BackgroundRole:
|
|
return QBrush(QColor(255, 224, 178)) if 'not_null' in field and field['not_null'] else QVariant()
|
|
|
|
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': '',
|
|
'not_null': False}
|
|
|
|
return {'name': field.name(),
|
|
'type': field.type(),
|
|
'length': field.length(),
|
|
'precision': field.precision(),
|
|
'expression': QgsExpression.quotedColumnRef(field.name()),
|
|
'not_null': bool(field.constraints().constraints() & QgsFieldConstraints.ConstraintNotNull)}
|
|
|
|
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()
|