Skip to content
Snippets Groups Projects
labeling.py 42 KiB
Newer Older
import sys, os, re, enum
from qgis.gui import *
Benjamin Jakimow's avatar
Benjamin Jakimow committed
from qgis.PyQt.QtCore import *
from qgis.PyQt.QtGui import *
from qgis.PyQt.QtWidgets import *
from eotimeseriesviewer.externals.qps.layerproperties import *
from eotimeseriesviewer.utils import loadUI, qgisInstance
from eotimeseriesviewer.externals.qps.classification.classificationscheme \
    import ClassificationSchemeWidget, ClassificationScheme, ClassInfo, ClassificationSchemeComboBox

from eotimeseriesviewer.timeseries import TimeSeriesDate, TimeSeriesSource
#the QgsProject(s) and QgsMapLayerStore(s) to search for QgsVectorLayers
MAP_LAYER_STORES = [QgsProject.instance()]

CONFKEY_CLASSIFICATIONSCHEME = 'classificationScheme'
    """Enumeration for shortcuts to be derived from a TimeSeriesDate instance"""
    Off = 'No Quick Label (default)'
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    Date = 'Date-Time'
    DOY = 'Day of Year (DOY)'
    Year = 'Year'
    DecimalYear = 'Decimal Year'
    Sensor = 'Sensor Name'
    #Classification = 'Classification'
def shortcuts(field:QgsField):
    """
    Returns the possible LabelShortCutTypes for a certain field
    :param fieldName: str
    :return: [list]
    """
    assert isinstance(field, QgsField)

    shortCutsString = [LabelShortcutType.Sensor, LabelShortcutType.Date, LabelShortcutType.SourceImage]
    shortCutsInt = [LabelShortcutType.Year, LabelShortcutType.DOY]
    shortCutsFloat = [LabelShortcutType.Year, LabelShortcutType.DOY, LabelShortcutType.DecimalYear]
    t = field.typeName().lower()
    if t == 'string':
        options.extend(shortCutsString)
        options.extend(shortCutsInt)
        options.extend(shortCutsFloat)
    elif t.startswith('integer'):
        options.extend(shortCutsInt)
    elif t.startswith('real'):
        options.extend(shortCutsInt)
        options.extend(shortCutsFloat)
    else:
        s = ""
    result = []
    for o in options:
        if o not in result:
            result.append(o)
    return result

def layerClassSchemes(layer:QgsVectorLayer)->list:
    """
    Returns a list of (ClassificationScheme, QgsField) for all QgsFields with QgsEditorWidget being QgsClassificationWidgetWrapper or RasterClassification.
    :param layer: QgsVectorLayer
    :return: list [(ClassificationScheme, QgsField), ...]
    """
    assert isinstance(layer, QgsVectorLayer)
    from .externals.qps.classification.classificationscheme import EDITOR_WIDGET_REGISTRY_KEY as CS_KEY
    from .externals.qps.classification.classificationscheme import classSchemeFromConfig
    schemes = []
    for i in range(layer.fields().count()):
        setup = layer.editorWidgetSetup(i)
        field = layer.fields().at(i)
        assert isinstance(field, QgsField)
        assert isinstance(setup, QgsEditorWidgetSetup)

        if setup.type() == CS_KEY:
            cs = classSchemeFromConfig(setup.config())
            if isinstance(cs, ClassificationScheme) and len(cs) > 0:
                schemes.append((cs, field))

        elif setup.type() == 'Classification' and isinstance(layer.renderer(), QgsCategorizedSymbolRenderer):
            renderer = layer.renderer()
            cs = ClassificationScheme()
            for l, cat in enumerate(renderer.categories()):
                assert isinstance(cat, QgsRendererCategory)
                symbol = cat.symbol()
                assert isinstance(symbol, QgsSymbol)
                cs.insertClass(ClassInfo(l, name=cat.value(), color=symbol.color()))
            if len(cs) > 0:
                schemes.append((cs, field))
    return schemes

def labelShortcutLayerClassificationSchemes(layer:QgsVectorLayer):
    """
    Returns the ClassificationSchemes + QgsField used for labeling shortcuts
    :return: [(ClassificationScheme, QgsField), (ClassificationScheme, QgsField), ...]
    """
    classSchemes = []
    assert isinstance(layer, QgsVectorLayer)
    for i in range(layer.fields().count()):
        setup = layer.editorWidgetSetup(i)
        assert isinstance(setup, QgsEditorWidgetSetup)
        if setup.type() == EDITOR_WIDGET_REGISTRY_KEY:
            conf = setup.config()
            ci = conf.get(CONFKEY_CLASSIFICATIONSCHEME)
            if isinstance(ci, ClassificationScheme) and ci not in classSchemes:
                classSchemes.append((ci, layer.fields().at(i)))
Benjamin Jakimow's avatar
Benjamin Jakimow committed
def quickLabelLayers()->list:
    Returns a list of known QgsVectorLayers with at least one LabelShortcutEditWidget
    :return: [list-of-QgsVectorLayer]
    """
    from .externals.qps.classification.classificationscheme import EDITOR_WIDGET_REGISTRY_KEY as CS_KEY
    classSchemes = set()
    for store in MAP_LAYER_STORES:
        assert isinstance(store, (QgsProject, QgsMapLayerStore))
        for layer in store.mapLayers().values():
            if isinstance(layer, QgsVectorLayer):
                for i in range(layer.fields().count()):
                    setup = layer.editorWidgetSetup(i)
                    assert isinstance(setup, QgsEditorWidgetSetup)
                    if setup.type() in [EDITOR_WIDGET_REGISTRY_KEY, CS_KEY, 'Classification']:
                        if layer not in layers:
                            layers.append(layer)
def setQuickTSDLabelsForRegisteredLayers(tsd:TimeSeriesDate, tss:TimeSeriesSource):
    :param tsd: TimeSeriesDate
Benjamin Jakimow's avatar
Benjamin Jakimow committed
    for layer in quickLabelLayers():
        assert isinstance(layer, QgsVectorLayer)

def setQuickClassInfo(vectorLayer:QgsVectorLayer, field, classInfo:ClassInfo):
    """
    Sets the ClassInfo value or label to selected features
    :param vectorLayer: QgsVectorLayer
    :param field: QgsField or int with field index
    :param classInfo: ClassInfo
    """
    assert isinstance(vectorLayer, QgsVectorLayer)
    assert isinstance(classInfo, ClassInfo)
    if isinstance(field, QgsField):
        idx = vectorLayer.fields().lookupField(field.name())
    else:
        idx = field

    field = vectorLayer.fields().at(idx)


    vectorLayer.beginEditCommand('Set class info "{}"'.format(classInfo.name()))

    if field.type() == QVariant.String:
        value = str(classInfo.name())
    else:
        value = classInfo.label()

    for feature in vectorLayer.selectedFeatures():
        assert isinstance(feature, QgsFeature)
        oldValue = feature.attribute(field.name())
        vectorLayer.changeAttributeValue(feature.id(), idx, value, oldValue)
    vectorLayer.endEditCommand()

def setQuickTSDLabels(vectorLayer:QgsVectorLayer, tsd:TimeSeriesDate, tss:TimeSeriesSource):
    Labels selected features with information related to TimeSeriesDate tsd, according to
    the settings specified in this model. Note: this will not the any ClassInfo or the source image values
    :param tsd: TimeSeriesDate
    :param classInfos:
    """
    assert isinstance(tsd, TimeSeriesDate)
    assert isinstance(vectorLayer, QgsVectorLayer)
    vectorLayer.beginEditCommand('Quick labels {}'.format(tsd.date()))
    for i in range(vectorLayer.fields().count()):
        setup = vectorLayer.editorWidgetSetup(i)
        assert isinstance(setup, QgsEditorWidgetSetup)
        if setup.type() == EDITOR_WIDGET_REGISTRY_KEY:
            field = vectorLayer.fields().at(i)
            assert isinstance(field, QgsField)

            conf = setup.config()
            labelType = conf.get(CONFKEY_LABELTYPE)
            if isinstance(labelType, LabelShortcutType):
                value = None
                if labelType == LabelShortcutType.Off:
                    pass
                if labelType == LabelShortcutType.Sensor:
                    value = tsd.sensor().name()
                elif labelType == LabelShortcutType.DOY:
                    value = tsd.doy()
                elif labelType == LabelShortcutType.Date:
                    value = str(tsd.date())
                elif labelType == LabelShortcutType.DecimalYear:
                    value = tsd.decimalYear()
                elif labelType == LabelShortcutType.SourceImage and isinstance(tss, TimeSeriesSource):
                    value = tss.uri()
                #elif labelType == LabelShortcutType.Classification:
                #    pass

                if value == None:
                    continue

                if field.type() == QVariant.String:
                    value = str(value)

                for feature in vectorLayer.selectedFeatures():
                    assert isinstance(feature, QgsFeature)
                    oldValue = feature.attribute(field.name())
                    vectorLayer.changeAttributeValue(feature.id(), i, value, oldValue)

class LabelAttributeTableModel(QAbstractTableModel):

    def __init__(self, parent=None, *args):

        super(LabelAttributeTableModel, self).__init__()

        self.cnField = 'Field'
        self.cnFieldType = 'Type'
        self.mColumnNames = [self.cnField, self.cnFieldType, self.cnLabel]
        self.mVectorLayer = None

    def setVectorLayer(self, layer:QgsVectorLayer):

        if isinstance(layer, QgsVectorLayer):
            layer.attributeAdded.connect(self.resetModel)
            layer.attributeDeleted.connect(self.resetModel)
            self.mVectorLayer = layer
        else:
            self.mVectorLayer = None

    def hasVectorLayer(self)->bool:
        """
        Returns true if a QgsVectorLayer is specified.
        :return: bool
        """
        return isinstance(self.mVectorLayer, QgsVectorLayer)

    def resetModel(self):
        self.beginResetModel()
        if isinstance(self.mVectorLayer, QgsVectorLayer):
            fields = self.mVectorLayer.fields()
            assert isinstance(fields, QgsFields)
            for i in range(fields.count()):
                field = fields.at(i)
                assert isinstance(field, QgsField)
                #self.mLabelTypes[field.name()] = LabelShortcutType.Off

        self.endResetModel()

    def rowCount(self, parent = QModelIndex())->int:
        if isinstance(self.mVectorLayer, QgsVectorLayer):
            return self.mVectorLayer.fields().count()
        else:
            return 0


    def fieldName2Index(self, fieldName:str)->str:
        assert isinstance(fieldName, str)

        if isinstance(self.mVectorLayer, QgsVectorLayer):
            fields = self.mVectorLayer.fields()
            assert isinstance(fields, QgsFields)
            return self.createIndex(i, 0)
        else:
            return QModelIndex()


    def field2index(self, field:QgsField)->QModelIndex:
        assert isinstance(field, QgsField)
        return self.fieldName2Index(field.name())


    def index2editorSetup(self, index:QModelIndex):
        if index.isValid() and isinstance(self.mVectorLayer, QgsVectorLayer):
            return self.mVectorLayer.editorWidgetSetup(index.row())
        else:
            return None


    def index2field(self, index:QModelIndex)->QgsField:
        if index.isValid() and isinstance(self.mVectorLayer, QgsVectorLayer):
            fields = self.mVectorLayer.fields()
            assert isinstance(fields, QgsFields)
            return fields.at(index.row())
        else:
            return None

    def columnCount(self, parent = QModelIndex())->int:
        return len(self.mColumnNames)

    def setFieldShortCut(self, fieldName:str, attributeType:LabelShortcutType):
        if isinstance(fieldName, QgsField):
            fieldName = fieldName.name()
        assert isinstance(fieldName, str)
        assert isinstance(attributeType, LabelShortcutType)

        if self.hasVectorLayer():
            fields = self.mVectorLayer.fields()
            assert isinstance(fields, QgsFields)
            i = self.mVectorLayer.fields().indexFromName(fieldName)
            assert i >= 0
            field = self.mVectorLayer.fields().at(i)
            idx = self.field2index(field)

            self.setData(self.createIndex(idx.row(), 2), attributeType, role=Qt.EditRole)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
    def shortcuts(self, field:QgsField):
        """
        Returns the possible LabelShortCutTypes for a certain field
        :param fieldName: str
        :return: [list]
        """
        if not self.hasVectorLayer():
            return []

        if isinstance(field, QModelIndex):
            field = self.index2field(field)

        if isinstance(field, str):
            i = self.mVectorLayer.fields().lookupField(field)
            field = self.mVectorLayer.fields().at(i)

        assert isinstance(field, QgsField)
        return shortcuts(field)
    def data(self, index, role = Qt.DisplayRole):
        if role is None or not index.isValid():
            return None

        value = None
        columnName = self.mColumnNames[index.column()]
        fields = self.mVectorLayer.fields()
        assert isinstance(fields, QgsFields)
        field = fields.at(index.row())
        assert isinstance(field, QgsField)
        setup = self.mVectorLayer.editorWidgetSetup(index.row())
        assert isinstance(setup, QgsEditorWidgetSetup)

        if role == Qt.DisplayRole or role == Qt.ToolTipRole:
            if columnName == self.cnField:
                value = field.name()
            elif columnName == self.cnFieldType:
                value = '{}'.format(field.typeName())
            elif columnName == self.cnLabel:
                fac = QgsGui.editorWidgetRegistry().factory(setup.type())
                value = fac.name()
        elif role == Qt.UserRole:
        return value

    def setData(self, index, value, role=None):
        if role is None or not index.isValid():
            return None

        columnName = self.mColumnNames[index.column()]
        fields = self.mVectorLayer.fields()
        assert isinstance(fields, QgsFields)
        field = fields.at(index.row())
        assert isinstance(field, QgsField)
        setup = self.mVectorLayer.editorWidgetSetup(index.row())
        assert isinstance(setup, QgsEditorWidgetSetup)

        changed = False
        if columnName == self.cnLabel and role == Qt.EditRole:
            if isinstance(value, str):
                setup = QgsEditorWidgetSetup(value,{})
                self.mVectorLayer.setEditorWidgetSetup(index.row(), setup)

                changed = True
        if changed:
            self.dataChanged.emit(index, index, [role])
        return changed

    def columnName(self, index: int)->str:
        if isinstance(index, QModelIndex):
            if not index.isValid():
                return None
            index = index.column()
        return self.mColumnNames[index]

    def flags(self, index):
        if index.isValid():
            flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
            if self.columnName(index) == self.cnLabel:
                flags = flags | Qt.ItemIsEditable
            return flags
        return None

    def headerData(self, col, orientation, role):
        if Qt is None:
            return None
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return self.mColumnNames[col]
        elif orientation == Qt.Vertical and role == Qt.DisplayRole:
            return col
        return None


class LabelAttributeTypeWidgetDelegate(QStyledItemDelegate):
    """

    """

    def __init__(self, tableView: QTableView, labelAttributeTableModel: LabelAttributeTableModel, parent=None):
        super(LabelAttributeTypeWidgetDelegate, self).__init__(parent=parent)
        assert isinstance(tableView, QTableView)
        assert isinstance(labelAttributeTableModel, LabelAttributeTableModel)

        self.mTableView = tableView
        self.mLabelAttributeTableModel = labelAttributeTableModel
        self.setItemDelegates(tableView)

    def model(self)->LabelAttributeTableModel:
        return self.mTableView.model()

    def setItemDelegates(self, tableView):
        assert isinstance(tableView, QTableView)
        model = self.model()
        for c in [model.cnLabel]:
            i = model.mColumnNames.index(c)
            tableView.setItemDelegateForColumn(i, self)

    def columnName(self, index:QModelIndex)->str:
        if not index.isValid():
            return None
        return self.model().mColumnNames[index.column()]

    def createEditor(self, parent, option, index):
        cname = self.columnName(index)
        model = self.model()
        layer = model.mVectorLayer
        idx = index.row()
        w = None
        if index.isValid() and isinstance(model, LabelAttributeTableModel):
            if cname == model.cnLabel:
                w = QComboBox(parent=parent)
                reg = QgsGui.editorWidgetRegistry()
                assert isinstance(reg, QgsEditorWidgetRegistry)
                factories = reg.factories()
                i = 0
                for key, fac in reg.factories().items():
                    score = fac.fieldScore(layer, idx)
                    if score > 0:
                        w.addItem(fac.name(), key)




        return w


    def setEditorData(self, editor, index):
        cname = self.columnName(index)

        w = None
        if index.isValid() and isinstance(model, LabelAttributeTableModel):
            if cname == model.cnLabel and isinstance(editor, QComboBox):
                for i in range(editor.count()):
                    if editor.itemData(i, role=Qt.UserRole) == key:
                        editor.setCurrentIndex(i)
                        break

    def setModelData(self, w, model, index):
        assert isinstance(model, LabelAttributeTableModel)
        assert isinstance(index, QModelIndex)

        cname = model.columnName(index)
        if index.isValid() and isinstance(model, LabelAttributeTableModel):
            if cname == model.cnLabel and isinstance(w, QComboBox):
                model.setData(index, w.currentData(Qt.UserRole), Qt.EditRole)

class LabelingWidget(QMainWindow, loadUI('labelingdock.ui')):

    sigVectorLayerChanged = pyqtSignal()
    sigMapExtentRequested = pyqtSignal(SpatialExtent)
    sigMapCenterRequested = pyqtSignal(SpatialPoint)

    def __init__(self, parent=None, canvas=None):
        super(LabelingWidget, self).__init__(parent)
        self.mVectorLayerComboBox = QgsMapLayerComboBox()
        self.mVectorLayerComboBox.setAllowEmptyLayer(True)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        self.mVectorLayerComboBox.setCurrentIndex(0)
        self.mVectorLayerComboBox.setShowCrs(True)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
        self.mVectorLayerComboBox.setSizePolicy(QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred))
        self.mVectorLayerComboBox.setMinimumWidth(150)

        assert isinstance(self.mVectorLayerComboBox, QgsMapLayerComboBox)




        allowed = ['DB2', 'WFS', 'arcgisfeatureserver', 'delimitedtext', 'memory', 'mssql', 'ogr', 'oracle', 'ows',
                   'postgres', 'spatialite', 'virtual']
        excluded = [k for k in QgsProviderRegistry.instance().providerList() if k not in allowed]
        self.mVectorLayerComboBox.setExcludedProviders(excluded)
        self.mVectorLayerComboBox.currentIndexChanged.connect(self.onVectorLayerChanged)

        self.toolBarSelectVectorSource.addWidget(self.mVectorLayerComboBox)

        self.mDualView = None

        if not isinstance(canvas, QgsMapCanvas):
            canvas = QgsMapCanvas(self)
            canvas.setVisible(False)

        self.mCanvas = canvas

        self.initActions()
        self.onVectorLayerChanged()

    def canvas(self)->QgsMapCanvas:
        Returns the internal map canvas
        :return: QgsMapCanvas
    def onCheckApply(self):
        """
        Checks if any QgsVectorLayer settings have changed and enables/disable buttons
        """
        btnApply = self.buttonBoxEditorWidget.button(QDialogButtonBox.Apply)
        btnReset = self.buttonBoxEditorWidget.button(QDialogButtonBox.Reset)
        changed = False
        for w in self.stackedFieldConfigs.findChildren(FieldConfigEditorWidget):
            assert isinstance(w, FieldConfigEditorWidget)
            if w.changed():
                changed = True
                break

        btnApply.setEnabled(changed)
        btnReset.setEnabled(not changed)
    def onReset(self):
        """
        Reloads the QgsVectorLayer and all its current settings
        """
        self.onVectorLayerChanged()
    def onApply(self):
        """
        Stores changes settings to current QgsVectorLayer
        """
        lyr = self.currentVectorSource()
        if isinstance(lyr, QgsVectorLayer):
            for w in self.stackedFieldConfigs.findChildren(FieldConfigEditorWidget):
                assert isinstance(w, FieldConfigEditorWidget)
                if w.changed():
                    config = w.currentFieldConfig()
                    lyr.setEditFormConfig(config.index(), config.editorWidgetSetup())
        self.onVectorLayerChanged()
    def isModified(self)->bool:
        return isinstance(self.currentVectorSource(), QgsVectorLayer) and self.currentVectorSource().isModified()

    def onVectorLayerChanged(self):
        lyr = self.currentVectorSource()
        # remove old QgsDualView
        if isinstance(self.mDualView, QgsDualView):
            self.mDualView.setParent(None)
            self.mDualView = None

        i = self.stackedWidget.count() - 1
        while i >= 0:
            self.stackedWidget.widget(i).setParent(None)
            i -= 1

        self.mCanvas.setLayers([])


        assert isinstance(self.stackedWidget, QStackedWidget)
        # add a config widget for each QgsField
        if isinstance(lyr, QgsVectorLayer):
            lyr.editingStarted.connect(lambda: self.mActionToggleEditing.setChecked(True))
            lyr.editingStopped.connect(lambda: self.mActionToggleEditing.setChecked(False))
            lyr.allowCommitChanged.connect(self.updateActions)

            self.mCanvas.setLayers([lyr])
            if not self.mCanvas.mapSettings().destinationCrs().isValid():
                self.mCanvas.setDestinationCrs(lyr.crs())
            if self.mCanvas.extent().width() == 0:
                self.mCanvas.setExtent(lyr.extent())

            self.mDualView = QgsDualView(self)

            # change selected row color: keep color also when attribute table looses focus
            pal = self.mDualView.tableView().palette()
            cSelected = pal.color(QPalette.Active, QPalette.Highlight)
            pal.setColor(QPalette.Inactive, QPalette.Highlight, cSelected)
            self.mDualView.tableView().setPalette(pal)

            self.stackedWidget.addWidget(self.mDualView)
            self.mDualView.init(lyr, self.mCanvas)  # , context=self.mAttributeEditorContext)
            self.mDualView.setView(QgsDualView.AttributeTable)
            self.mDualView.setAttributeTableConfig(lyr.attributeTableConfig())
        self.updateActions()
        self.sigVectorLayerChanged.emit()
    def updateActions(self, *args):
        if isinstance(self.currentVectorSource(), QgsVectorLayer):
            b = self.currentVectorSource().isEditable()
            self.mActionAddFeature.setEnabled(b)
            gType = self.currentVectorSource().geometryType()
            if gType == QgsWkbTypes.PointGeometry:
                self.mActionAddFeature.setIcon(QIcon(':/images/themes/default/mActionCapturePoint.svg'))
            elif gType == QgsWkbTypes.LineGeometry:
                self.mActionAddFeature.setIcon(QIcon(':/images/themes/default/mActionCaptureLine.svg'))
            elif gType == QgsWkbTypes.PolygonGeometry:
                self.mActionAddFeature.setIcon(QIcon(':/images/themes/default/mActionCapturePolygon.svg'))
            else:
                # unknown geometry type?
                self.mActionAddFeature.setIcon(QIcon(':/images/themes/default/mActionCapturePolygon.svg'))

            buffer = self.currentVectorSource().editBuffer()
            b2 = b and isinstance(buffer, QgsVectorLayerEditBuffer)
            self.mActionCancelEdits.setEnabled(b2)
            self.mActionSaveEdits.setEnabled(b2)
            self.mActionToggleEditing.setEnabled(True)
            for action in self.toolBarSelectFeatures.actions():
                action.setEnabled(True)
            for action in self.toolBarSelectFeatures.actions():
                action.setEnabled(False)
            for action in self.toolBarModifyFeatures.actions():
                action.setEnabled(False)

    def actionAddFeature(self)->QAction:
        return self.mActionAddFeature
    def actionSaveEdits(self)->QAction:
        return self.mActionSaveEdits
    def actionToggleEditing(self)->QAction:
        return self.mActionToggleEditing

    def onToggleEditing(self, b: bool):
        lyr = self.currentVectorSource()
        if isinstance(lyr, QgsVectorLayer):
            if b:
                lyr.startEditing()
            else:
                if lyr.isModified():
                    r = QMessageBox.question(self, '{}'.format(lyr.name()), 'Save changes?')
                    if r == QMessageBox.Yes:
                        lyr.commitChanges()
                    else:
                        lyr.rollBack()
    def cancelEdits(self, *args):
        lyr = self.currentVectorSource()
        if isinstance(lyr, QgsVectorLayer):
    def saveEdits(self, *args):
        lyr = self.currentVectorSource()
        if isinstance(lyr, QgsVectorLayer):
            b = lyr.isEditable()
            lyr.commitChanges()
            if b:
                lyr.startEditing()


    def initActions(self):

        iface = qgisInstance()

        # if isinstance(iface, QgisInterface):
        # self.mActionAddFeature = iface.actionAddFeature()
        # self.mActionSaveEdits = iface.actionSaveEdits()
        # self.mActionCancelEdits = iface.actionCancelEdits()
        # self.mActionToggleEditing = iface.actionToggleEditing()
        # self.mActionAddOgrLayer = iface.actionAddOgrLayer()
        self.mActionToggleEditing.toggled.connect(self.onToggleEditing)
        self.mActionCancelEdits.triggered.connect(self.cancelEdits)
        self.mActionSaveEdits.triggered.connect(self.saveEdits)
        self.mActionSwitchToTableView.triggered.connect(self.showTableView)
        self.mActionShowLayerProperties.triggered.connect(self.showLayerProperties)
        self.mActionSwitchToFormView.triggered.connect(self.showFormView)

        self.mActionSelectAll.triggered.connect(self.selectAll)
        self.mActionInvertSelection.triggered.connect(self.invertSelection)
        self.mActionRemoveSelection.triggered.connect(self.removeSelection)
        self.mActionPanMapToSelectedRows.triggered.connect(self.panMapToSelectedRows)
        self.mActionZoomMapToSelectedRows.triggered.connect(self.zoomMapToSelectedRows)


        self.mActionNextFeature.triggered.connect(self.nextFeature)
        self.mActionPreviousFeature.triggered.connect(self.previousFeature)

        # bottom button bar
        self.btnAttributeView.setDefaultAction(self.mActionSwitchToTableView)
        self.btnShowLayerProperties.setDefaultAction(self.mActionShowLayerProperties)
        self.btnFormView.setDefaultAction(self.mActionSwitchToFormView)

    def nextFeature(self):
        """
        Selects the next feature and moves the map extent to.
        """
        vl = self.currentVectorSource()
        if isinstance(vl, QgsVectorLayer) and vl.hasFeatures():
            allIDs = sorted(vl.allFeatureIds())
            fids = vl.selectedFeatureIds()
            if len(fids) == 0:
                nextFID = allIDs[0]
                i = min(allIDs.index(fids[0]) + 1, len(allIDs)-1)
                nextFID = allIDs[i]
            vl.selectByIds([nextFID])
            self.panMapToSelectedRows()

    def previousFeature(self):
        """
        Selects the previous feature and moves the map extent to.
        """
        vl = self.currentVectorSource()
        if isinstance(vl, QgsVectorLayer) and vl.hasFeatures():
            allIDs = sorted(vl.allFeatureIds())
            fids = vl.selectedFeatureIds()
            if len(fids) == 0:
                nextFID = allIDs[0]
                i = max(allIDs.index(fids[0]) - 1, 0)
                nextFID = allIDs[i]
            vl.selectByIds([nextFID])
            self.panMapToSelectedRows()

    def selectAll(self):
        if isinstance(self.currentVectorSource(), QgsVectorLayer):
            self.currentVectorSource().selectAll()

    def invertSelection(self):
        if isinstance(self.currentVectorSource(), QgsVectorLayer):
            self.currentVectorSource().invertSelection()

    def removeSelection(self):
        if isinstance(self.currentVectorSource(), QgsVectorLayer):
            self.currentVectorSource().removeSelection()

    def panMapToSelectedRows(self):
        """
        Pan to the selected layer features
        Requires that external maps respond to sigMapCenterRequested
        """
        lyr = self.currentVectorSource()
        if isinstance(lyr, QgsVectorLayer):
            crs = self.canvas().mapSettings().destinationCrs()
            bbox = lyr.boundingBoxOfSelected()
            center = SpatialPoint(lyr.crs(), bbox.center()).toCrs(crs)
            self.mCanvas.setCenter(center)
            self.sigMapCenterRequested.emit(center)

    def zoomMapToSelectedRows(self):
        """
        Zooms to the selected rows.
        Requires that external maps respond to sigMapExtentRequested
        """
        lyr = self.currentVectorSource()
        if isinstance(lyr, QgsVectorLayer):
            crs = self.canvas().mapSettings().destinationCrs()
            bbox = SpatialExtent(lyr.crs(), lyr.boundingBoxOfSelected()).toCrs(crs)
            if bbox.width() > 0 and bbox.height() > 0:
                self.mCanvas.setExtent(bbox)
                self.sigMapExtentRequested.emit(bbox)

        """
        Call to show the QgsDualView Attribute Table
        """
        if isinstance(self.mDualView, QgsDualView):
            self.mDualView.setView(QgsDualView.AttributeTable)

    def showFormView(self):
        """
        Call to show the QgsDualView Attribute Editor
        """
        if isinstance(self.mDualView, QgsDualView):
            self.mDualView.setView(QgsDualView.AttributeEditor)

    def showLayerProperties(self):
        """
        Call to show the QgsVectorLayer field settings
        """
        from .externals.qps.layerproperties import showLayerPropertiesDialog
        lyr = self.currentVectorSource()
        if isinstance(lyr, QgsVectorLayer):
            showLayerPropertiesDialog(lyr, self.mCanvas, parent=self, useQGISDialog=True)

    def setCurrentVectorSource(self, layer:QgsVectorLayer):
        """
        Sets the current vector source.
        :param layer: QgsVectorLayer
        """
        assert isinstance(layer, QgsVectorLayer)

        if layer not in QgsProject.instance().mapLayers().values():
            QgsProject.instance().addMapLayer(layer)
        cboxLayers = [self.mVectorLayerComboBox.layer(i) for i in range(self.mVectorLayerComboBox.count())]
        if layer in cboxLayers:
            self.mVectorLayerComboBox.setCurrentIndex(cboxLayers.index(layer))
    def currentVectorSource(self)->QgsVectorLayer:
        """
        Returns the current QgsVectorLayer
        :return: QgsVectorLayer
        """
        return self.mVectorLayerComboBox.currentLayer()



class LabelShortcutEditorConfigWidget(QgsEditorConfigWidget):

    def __init__(self, vl:QgsVectorLayer, fieldIdx:int, parent:QWidget):

        super(LabelShortcutEditorConfigWidget, self).__init__(vl, fieldIdx, parent)
        #self.setupUi(self)

        self.setLayout(QVBoxLayout())

        self.mCBShortCutType = QComboBox(self)
        #self.mClassWidget = ClassificationSchemeWidget(parent=self)
        self.layout().addWidget(self.mCBShortCutType)
        #self.layout().addWidget(self.mClassWidget)
        self.layout().addStretch(0)
        assert isinstance(vl, QgsVectorLayer)
        field = vl.fields().at(fieldIdx)
        assert isinstance(field, QgsField)
        self.mAllowedShortCuts = shortcuts(field)
        for i, option in enumerate(self.mAllowedShortCuts):
            self.mCBShortCutType.addItem(option.value, option)

        self.mCBShortCutType.currentIndexChanged[int].connect(self.onIndexChanged)
        self.mCBShortCutType.currentIndexChanged[int].connect(lambda : self.onIndexChanged())

        self.mLastConfig = {}

        self.onIndexChanged()

    def config(self, *args, **kwargs)->dict:

        conf = dict()
        conf[CONFKEY_LABELTYPE] = self.mCBShortCutType.currentData()
        #cs = self.mClassWidget.classificationScheme()
        #assert isinstance(cs, ClassificationScheme)
        #todo: json for serialization
        #conf[CONFKEY_CLASSIFICATIONSCHEME] = cs

        return conf

    def setConfig(self, config:dict):
        self.mLastConfig = config
        labelType = config.get(CONFKEY_LABELTYPE)
        if not isinstance(labelType, LabelShortcutType):
            labelType = LabelShortcutType.Off

        if labelType not in self.mAllowedShortCuts:
            labelType = self.mAllowedShortCuts[0]

        i = self.mCBShortCutType.findData(labelType)
        #self.mCBShortCutType.currentIndexChanged.connect(self.onIndexChanged)
        self.mCBShortCutType.setCurrentIndex(i)
        #classScheme = config.get(CONFKEY_CLASSIFICATIONSCHEME)
        #if isinstance(classScheme, ClassificationScheme):
        #    self.mClassWidget.setClassificationScheme(classScheme)


    def onIndexChanged(self, *args):
        ltype = self.shortcutType()
        #if ltype == LabelShortcutType.Classification:
        #    self.mClassWidget.setEnabled(True)
        #    self.mClassWidget.setVisible(True)
        #else:
        #    self.mClassWidget.setEnabled(False)
        #    self.mClassWidget.setVisible(False)
        self.changed.emit()
    #def classificationScheme(self)->ClassificationScheme:
    #    return self.mClassWidget.classificationScheme()
    #def setClassificationScheme(self, classScheme:ClassificationScheme):
    #    assert isinstance(classScheme, ClassificationScheme)
    #    self.mClassWidget.setClassificationScheme(classScheme)
    def shortcutType(self)->LabelShortcutType:
        return self.mCBShortCutType.currentData(Qt.UserRole)



class LabelShortcutEditorWidgetWrapper(QgsEditorWidgetWrapper):

    def __init__(self, vl:QgsVectorLayer, fieldIdx:int, editor:QWidget, parent:QWidget):
        super(LabelShortcutEditorWidgetWrapper, self).__init__(vl, fieldIdx, editor, parent)

        self.mEditor = None
        self.mValidator = None

    def configLabelType(self)->LabelShortcutType:
        return self.config(CONFKEY_LABELTYPE)
    #def configClassificationScheme(self)->ClassificationScheme:
    #    return self.config(CONFKEY_CLASSIFICATIONSCHEME)

    def createWidget(self, parent: QWidget):
        """
        Create the data input widget
        :param parent: QWidget
        :return: ClassificationSchemeComboBox | default widget
        """
        #log('createWidget')
        labelType = self.configLabelType()
        #if labelType == LabelShortcutType.Classification:
        #    self.mEditor = ClassificationSchemeComboBox(parent)
        #else:
        self.mEditor = QLineEdit(parent)
        self.mValidator = QRegExpValidator()
        return self.mEditor