Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Merge pull request #51309 from nirvn/validity_table
[attributes table] Add constraint-based conditional styling and failing constraints feature filter
  • Loading branch information
nirvn committed Jan 17, 2023
2 parents 8973eb0 + 4c992c4 commit 8e12541
Show file tree
Hide file tree
Showing 23 changed files with 306 additions and 46 deletions.
1 change: 1 addition & 0 deletions images/images.qrc
Expand Up @@ -369,6 +369,7 @@
<file>themes/default/mActionOpenTable.svg</file>
<file>themes/default/mActionOpenTableEdited.svg</file>
<file>themes/default/mActionOpenTableSelected.svg</file>
<file>themes/default/mActionOpenTableInvalid.svg</file>
<file>themes/default/mActionOpenTableVisible.svg</file>
<file>themes/default/mActionAddTable.svg</file>
<file>themes/default/mActionOffsetPointSymbols.svg</file>
Expand Down
2 changes: 2 additions & 0 deletions images/themes/default/mActionOpenTableInvalid.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 13 additions & 1 deletion python/core/auto_generated/qgsconditionalstyle.sip.in
Expand Up @@ -8,8 +8,8 @@



typedef QList<QgsConditionalStyle> QgsConditionalStyles;

typedef QList<QgsConditionalStyle> QgsConditionalStyles;

class QgsConditionalLayerStyles : QObject
{
Expand Down Expand Up @@ -43,12 +43,24 @@ Each row will check be checked against each rule.
.. seealso:: :py:func:`rowStyles`

.. versionadded:: 2.12
%End

QgsConditionalStyle constraintFailureStyles( QgsFieldConstraints::ConstraintStrength strength );
%Docstring
Returns a style associated to a constraint failure.

:param strength: the type of constraint

.. versionadded:: 3.30
%End

void setFieldStyles( const QString &fieldName, const QList<QgsConditionalStyle> &styles );
%Docstring
Set the conditional ``styles`` for a field, with the specified ``fieldName``.

The field value is inserted into a 'value' variable to conduct
expression checks.

.. seealso:: :py:func:`fieldStyles`
%End

Expand Down
10 changes: 10 additions & 0 deletions python/core/auto_generated/vector/qgsvectorlayerutils.sip.in
Expand Up @@ -168,6 +168,16 @@ be unique within regard to ``existingValues``.
The optional seed value can be used as a basis for generated values.

.. versionadded:: 3.6
%End

static bool attributeHasConstraints( const QgsVectorLayer *layer, int attributeIndex );
%Docstring
Returns ``True`` if a feature attribute has active constraints.

:param layer: the vector layer from which field constraints will be checked for
:param attributeIndex: the attribute index

.. versionadded:: 3.30
%End

static bool validateAttribute( const QgsVectorLayer *layer, const QgsFeature &feature, int attributeIndex, QStringList &errors /Out/,
Expand Down
Expand Up @@ -24,7 +24,8 @@ class QgsAttributeTableFilterModel: QSortFilterProxyModel, QgsFeatureModel
ShowSelected,
ShowVisible,
ShowFilteredList,
ShowEdited
ShowEdited,
ShowInvalid,
};

enum ColumnType
Expand Down
Expand Up @@ -247,6 +247,22 @@ Any extra columns need to be implemented by proxy models in front of this model.
%Docstring
Empty extra columns to announce from this model.
Any extra columns need to be implemented by proxy models in front of this model.
%End

bool showValidityState() const;
%Docstring
Returns whether the attribute table will add a visual feedback to cells when an attribute
constraint is not met.

.. versionadded:: 3.30
%End

void setShowValidityState( bool show );
%Docstring
Sets whether the attribute table will add a visual feedback to cells when an attribute constraint
is not met.

.. versionadded:: 3.30
%End

public slots:
Expand Down
37 changes: 33 additions & 4 deletions src/core/qgsconditionalstyle.cpp
Expand Up @@ -23,7 +23,35 @@

QgsConditionalLayerStyles::QgsConditionalLayerStyles( QObject *parent )
: QObject( parent )
{}
{
}

QgsConditionalStyle QgsConditionalLayerStyles::constraintFailureStyles( QgsFieldConstraints::ConstraintStrength strength )
{
switch ( strength )
{
case QgsFieldConstraints::ConstraintStrengthHard:
{
QgsConditionalStyle hardConstraintFailureStyle;
hardConstraintFailureStyle.setBackgroundColor( QColor( 255, 152, 0 ) );
hardConstraintFailureStyle.setTextColor( QColor( 0, 0, 0 ) );
return hardConstraintFailureStyle;
}

case QgsFieldConstraints::ConstraintStrengthSoft:
{
QgsConditionalStyle softConstraintFailureStyle;
softConstraintFailureStyle.setBackgroundColor( QColor( 255, 191, 12 ) );
softConstraintFailureStyle.setTextColor( QColor( 0, 0, 0 ) );
return softConstraintFailureStyle;
}

case QgsFieldConstraints::ConstraintStrengthNotSet:
return QgsConditionalStyle();
}

return QgsConditionalStyle();
}

QgsConditionalStyles QgsConditionalLayerStyles::rowStyles() const
{
Expand Down Expand Up @@ -56,13 +84,13 @@ QList<QgsConditionalStyle> QgsConditionalLayerStyles::fieldStyles( const QString
bool QgsConditionalLayerStyles::writeXml( QDomNode &node, QDomDocument &doc, const QgsReadWriteContext &context ) const
{
QDomElement stylesel = doc.createElement( QStringLiteral( "conditionalstyles" ) );

QDomElement rowel = doc.createElement( QStringLiteral( "rowstyles" ) );
const auto constMRowStyles = mRowStyles;
for ( const QgsConditionalStyle &style : constMRowStyles )
{
style.writeXml( rowel, doc, context );
}

stylesel.appendChild( rowel );

QDomElement fieldsel = doc.createElement( QStringLiteral( "fieldstyles" ) );
Expand All @@ -79,7 +107,6 @@ bool QgsConditionalLayerStyles::writeXml( QDomNode &node, QDomDocument &doc, con
}
fieldsel.appendChild( fieldel );
}

stylesel.appendChild( fieldsel );

node.appendChild( stylesel );
Expand All @@ -100,9 +127,11 @@ bool QgsConditionalLayerStyles::rulesNeedGeometry() const

bool QgsConditionalLayerStyles::readXml( const QDomNode &node, const QgsReadWriteContext &context )
{
const QDomElement condel = node.firstChildElement( QStringLiteral( "conditionalstyles" ) );
mRowStyles.clear();
mFieldStyles.clear();

const QDomElement condel = node.firstChildElement( QStringLiteral( "conditionalstyles" ) );

const QDomElement rowstylesel = condel.firstChildElement( QStringLiteral( "rowstyles" ) );
QDomNodeList nodelist = rowstylesel.toElement().elementsByTagName( QStringLiteral( "style" ) );
for ( int i = 0; i < nodelist.count(); i++ )
Expand Down
13 changes: 12 additions & 1 deletion src/core/qgsconditionalstyle.h
Expand Up @@ -16,6 +16,8 @@
#define QGSCONDITIONALSTYLE_H

#include "qgis_core.h"
#include "qgsfield.h"

#include <QObject>
#include <QFont>
#include <QColor>
Expand All @@ -32,7 +34,6 @@ class QgsSymbol;

typedef QList<QgsConditionalStyle> QgsConditionalStyles;


/**
* \ingroup core
* \brief The QgsConditionalLayerStyles class holds conditional style information
Expand Down Expand Up @@ -65,9 +66,19 @@ class CORE_EXPORT QgsConditionalLayerStyles : public QObject
*/
void setRowStyles( const QgsConditionalStyles &styles );

/**
* Returns a style associated to a constraint failure.
* \param strength the type of constraint
* \since QGIS 3.30
*/
QgsConditionalStyle constraintFailureStyles( QgsFieldConstraints::ConstraintStrength strength );

/**
* Set the conditional \a styles for a field, with the specified \a fieldName.
*
* The field value is inserted into a 'value' variable to conduct
* expression checks.
*
* \see fieldStyles()
*/
void setFieldStyles( const QString &fieldName, const QList<QgsConditionalStyle> &styles );
Expand Down
14 changes: 14 additions & 0 deletions src/core/vector/qgsvectorlayerutils.cpp
Expand Up @@ -367,6 +367,20 @@ QVariant QgsVectorLayerUtils::createUniqueValueFromCache( const QgsVectorLayer *

}

bool QgsVectorLayerUtils::attributeHasConstraints( const QgsVectorLayer *layer, int attributeIndex )
{
if ( !layer )
return false;

if ( attributeIndex < 0 || attributeIndex >= layer->fields().count() )
return false;

const QgsFieldConstraints constraints = layer->fields().at( attributeIndex ).constraints();
return ( constraints.constraints() & QgsFieldConstraints::ConstraintNotNull ||
constraints.constraints() & QgsFieldConstraints::ConstraintUnique ||
constraints.constraints() & QgsFieldConstraints::ConstraintExpression );
}

bool QgsVectorLayerUtils::validateAttribute( const QgsVectorLayer *layer, const QgsFeature &feature, int attributeIndex, QStringList &errors,
QgsFieldConstraints::ConstraintStrength strength, QgsFieldConstraints::ConstraintOrigin origin )
{
Expand Down
8 changes: 8 additions & 0 deletions src/core/vector/qgsvectorlayerutils.h
Expand Up @@ -183,6 +183,14 @@ class CORE_EXPORT QgsVectorLayerUtils
*/
static QVariant createUniqueValueFromCache( const QgsVectorLayer *layer, int fieldIndex, const QSet<QVariant> &existingValues, const QVariant &seed = QVariant() );

/**
* Returns TRUE if a feature attribute has active constraints.
* \param layer the vector layer from which field constraints will be checked for
* \param attributeIndex the attribute index
* \since QGIS 3.30
*/
static bool attributeHasConstraints( const QgsVectorLayer *layer, int attributeIndex );

/**
* Tests a feature attribute value to check whether it passes all constraints which are present on the corresponding field.
* Returns TRUE if the attribute value is valid for the field. Any constraint failures will be reported in the errors argument.
Expand Down
18 changes: 15 additions & 3 deletions src/gui/attributetable/qgsattributetablefiltermodel.cpp
Expand Up @@ -313,7 +313,11 @@ bool QgsAttributeTableFilterModel::selectedOnTop()
void QgsAttributeTableFilterModel::setFilteredFeatures( const QgsFeatureIds &ids )
{
mFilteredFeatures = ids;
setFilterMode( ShowFilteredList );
if ( mFilterMode != ShowFilteredList &&
mFilterMode != ShowInvalid )
{
setFilterMode( ShowFilteredList );
}
invalidateFilter();
}

Expand All @@ -337,6 +341,11 @@ void QgsAttributeTableFilterModel::setFilterMode( FilterMode filterMode )
connectFilterModeConnections( filterMode );
mFilterMode = filterMode;
invalidate();

if ( mFilterMode == QgsAttributeTableFilterModel::ShowInvalid )
{
filterFeatures();
}
}
}

Expand All @@ -356,6 +365,7 @@ void QgsAttributeTableFilterModel::disconnectFilterModeConnections()
case ShowSelected:
break;
case ShowFilteredList:
case ShowInvalid:
disconnect( layer(), &QgsVectorLayer::featureAdded, this, &QgsAttributeTableFilterModel::startTimedFilterFeatures );
disconnect( layer(), &QgsVectorLayer::attributeValueChanged, this, &QgsAttributeTableFilterModel::onAttributeValueChanged );
disconnect( layer(), &QgsVectorLayer::geometryChanged, this, &QgsAttributeTableFilterModel::onGeometryChanged );
Expand All @@ -380,6 +390,7 @@ void QgsAttributeTableFilterModel::connectFilterModeConnections( QgsAttributeTab
case ShowSelected:
break;
case ShowFilteredList:
case ShowInvalid:
connect( layer(), &QgsVectorLayer::featureAdded, this, &QgsAttributeTableFilterModel::startTimedFilterFeatures );
connect( layer(), &QgsVectorLayer::attributeValueChanged, this, &QgsAttributeTableFilterModel::onAttributeValueChanged );
connect( layer(), &QgsVectorLayer::geometryChanged, this, &QgsAttributeTableFilterModel::onGeometryChanged );
Expand All @@ -396,6 +407,7 @@ bool QgsAttributeTableFilterModel::filterAcceptsRow( int sourceRow, const QModel
return true;

case ShowFilteredList:
case ShowInvalid:
return mFilteredFeatures.contains( masterModel()->rowToId( sourceRow ) );

case ShowSelected:
Expand Down Expand Up @@ -449,15 +461,15 @@ void QgsAttributeTableFilterModel::onAttributeValueChanged( QgsFeatureId fid, in
Q_UNUSED( fid );
Q_UNUSED( value );

if ( mFilterExpression.referencedAttributeIndexes( layer()->fields() ).contains( idx ) )
if ( mFilterMode == QgsAttributeTableFilterModel::ShowInvalid || mFilterExpression.referencedAttributeIndexes( layer()->fields() ).contains( idx ) )
{
startTimedFilterFeatures();
}
}

void QgsAttributeTableFilterModel::onGeometryChanged()
{
if ( mFilterExpression.needsGeometry() )
if ( mFilterMode == QgsAttributeTableFilterModel::ShowInvalid || mFilterExpression.needsGeometry() )
{
startTimedFilterFeatures();
}
Expand Down
3 changes: 2 additions & 1 deletion src/gui/attributetable/qgsattributetablefiltermodel.h
Expand Up @@ -48,7 +48,8 @@ class GUI_EXPORT QgsAttributeTableFilterModel: public QSortFilterProxyModel, pub
ShowSelected, //!< Show only selected features
ShowVisible, //!< Show only visible features (depends on the map canvas)
ShowFilteredList, //!< Show only features whose ids are on the filter list. {\see setFilteredFeatures}
ShowEdited //!< Show only features which have unsaved changes
ShowEdited, //!< Show only features which have unsaved changes
ShowInvalid, //!< Show only features not respecting constraints (since QGIS 3.30)
};
Q_ENUM( FilterMode )

Expand Down
31 changes: 30 additions & 1 deletion src/gui/attributetable/qgsattributetablemodel.cpp
Expand Up @@ -511,6 +511,7 @@ void QgsAttributeTableModel::fieldConditionalStyleChanged( const QString &fieldN
if ( fieldName.isNull() )
{
mRowStylesMap.clear();
mConstraintStylesMap.clear();
emit dataChanged( index( 0, 0 ), index( rowCount() - 1, columnCount() - 1 ) );
return;
}
Expand Down Expand Up @@ -749,11 +750,38 @@ QVariant QgsAttributeTableModel::data( const QModelIndex &index, int role ) cons
styles = QgsConditionalStyle::matchingConditionalStyles( mLayer->conditionalStyles()->rowStyles(), QVariant(), mExpressionContext );
mRowStylesMap.insert( mFeat.id(), styles );
}

const QgsConditionalStyle rowstyle = QgsConditionalStyle::compressStyles( styles );

QgsConditionalStyle constraintstyle;
if ( mShowValidityState && QgsVectorLayerUtils::attributeHasConstraints( mLayer, fieldId ) )
{
if ( mConstraintStylesMap.contains( mFeat.id() ) &&
mConstraintStylesMap[mFeat.id()].contains( fieldId ) )
{
constraintstyle = mConstraintStylesMap[mFeat.id()][fieldId];
}
else
{
QStringList errors;
if ( !QgsVectorLayerUtils::validateAttribute( mLayer, mFeat, fieldId, errors, QgsFieldConstraints::ConstraintStrengthHard ) )
{
constraintstyle = mLayer->conditionalStyles()->constraintFailureStyles( QgsFieldConstraints::ConstraintStrengthHard );
}
else
{
if ( !QgsVectorLayerUtils::validateAttribute( mLayer, mFeat, fieldId, errors, QgsFieldConstraints::ConstraintStrengthSoft ) )
{
constraintstyle = mLayer->conditionalStyles()->constraintFailureStyles( QgsFieldConstraints::ConstraintStrengthSoft );
}
}
mConstraintStylesMap[mFeat.id()].insert( fieldId, constraintstyle );
}
}

styles = mLayer->conditionalStyles()->fieldStyles( field.name() );
styles = QgsConditionalStyle::matchingConditionalStyles( styles, val, mExpressionContext );
styles.insert( 0, rowstyle );
styles.insert( 0, constraintstyle );
const QgsConditionalStyle style = QgsConditionalStyle::compressStyles( styles );

if ( style.isValid() )
Expand Down Expand Up @@ -796,6 +824,7 @@ bool QgsAttributeTableModel::setData( const QModelIndex &index, const QVariant &
return false;

mRowStylesMap.remove( mFeat.id() );
mConstraintStylesMap.remove( mFeat.id() );

if ( !mLayer->isModified() )
return false;
Expand Down

0 comments on commit 8e12541

Please sign in to comment.