Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[FEATURE][layouts] Allow layout attribute tables to be styled using t…
…he foreground

and background colors of matching conditional styles attached to the layer

When the new "Apply layer conditional styling colors" option is enabled in the
layout attribute table settings, any conditional styling rules present in the
layer will be applied inside the layout attribute table (foreground and
background colors only, for now!).

Refs #25712

Sponsored by City of Canning
  • Loading branch information
nyalldawson committed Jan 8, 2020
1 parent 9a5855a commit 400e3da
Show file tree
Hide file tree
Showing 11 changed files with 333 additions and 77 deletions.
Expand Up @@ -290,6 +290,9 @@ be replaced by a line break.
%End


virtual QgsConditionalStyle conditionalCellStyle( int row, int column ) const;


virtual QgsExpressionContext createExpressionContext() const;

virtual void finalizeRestoreFromXml();
Expand All @@ -298,6 +301,26 @@ be replaced by a line break.
virtual void refreshDataDefinedProperty( QgsLayoutObject::DataDefinedProperty property = QgsLayoutObject::AllProperties );


bool useConditionalStyling() const;
%Docstring
Returns ``True`` if the attribute table will be rendered using the conditional styling
properties of the linked vector layer.

.. seealso:: :py:func:`setUseConditionalStyling`

.. versionadded:: 3.12
%End

void setUseConditionalStyling( bool enabled );
%Docstring
Sets whether the attribute table will be rendered using the conditional styling
properties of the linked vector layer.

.. seealso:: :py:func:`useConditionalStyling`

.. versionadded:: 3.12
%End

protected:

virtual bool writePropertiesToElement( QDomElement &elem, QDomDocument &doc, const QgsReadWriteContext &context ) const;
Expand Down
7 changes: 7 additions & 0 deletions python/core/auto_generated/layout/qgslayouttable.sip.in
Expand Up @@ -497,6 +497,13 @@ Fetches the contents used for the cells in the table.
:return: ``True`` if table contents were successfully retrieved.

:param contents: QgsLayoutTableContents to store retrieved row data in
%End

virtual QgsConditionalStyle conditionalCellStyle( int row, int column ) const;
%Docstring
Returns the conditional style to use for the cell at ``row``, ``column``.

.. versionadded:: 3.12
%End

QgsLayoutTableContents &contents();
Expand Down
15 changes: 15 additions & 0 deletions src/app/layout/qgslayoutattributetablewidget.cpp
Expand Up @@ -69,6 +69,7 @@ QgsLayoutAttributeTableWidget::QgsLayoutAttributeTableWidget( QgsLayoutFrame *fr
connect( mHideEmptyBgCheckBox, &QCheckBox::toggled, this, &QgsLayoutAttributeTableWidget::mHideEmptyBgCheckBox_toggled );
connect( mWrapBehaviorComboBox, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ), this, &QgsLayoutAttributeTableWidget::mWrapBehaviorComboBox_currentIndexChanged );
connect( mAdvancedCustomizationButton, &QPushButton::clicked, this, &QgsLayoutAttributeTableWidget::mAdvancedCustomizationButton_clicked );
connect( mUseConditionalStylingCheckBox, &QCheckBox::stateChanged, this, &QgsLayoutAttributeTableWidget::useConditionalStylingChanged );
setPanelTitle( tr( "Table Properties" ) );

mContentFontToolButton->setMode( QgsFontButton::ModeQFont );
Expand Down Expand Up @@ -476,6 +477,7 @@ void QgsLayoutAttributeTableWidget::updateGuiElements()
mFeatureFilterCheckBox->setCheckState( mTable->filterFeatures() ? Qt::Checked : Qt::Unchecked );
mFeatureFilterEdit->setEnabled( mTable->filterFeatures() );
mFeatureFilterButton->setEnabled( mTable->filterFeatures() );
mUseConditionalStylingCheckBox->setChecked( mTable->useConditionalStyling() );

mHeaderHAlignmentComboBox->setCurrentIndex( static_cast<int>( mTable->headerHAlignment() ) );
mHeaderModeComboBox->setCurrentIndex( static_cast<int>( mTable->headerMode() ) );
Expand Down Expand Up @@ -914,6 +916,19 @@ void QgsLayoutAttributeTableWidget::mAdvancedCustomizationButton_clicked()
d.exec();
}

void QgsLayoutAttributeTableWidget::useConditionalStylingChanged( bool checked )
{
if ( !mTable )
{
return;
}

mTable->beginCommand( tr( "Toggle Table Conditional Styling" ) );
mTable->setUseConditionalStyling( checked );
mTable->update();
mTable->endCommand();
}

void QgsLayoutAttributeTableWidget::mDrawEmptyCheckBox_toggled( bool checked )
{
if ( !mTable )
Expand Down
1 change: 1 addition & 0 deletions src/app/layout/qgslayoutattributetablewidget.h
Expand Up @@ -86,6 +86,7 @@ class QgsLayoutAttributeTableWidget: public QgsLayoutItemBaseWidget, private Ui:
void mHideEmptyBgCheckBox_toggled( bool checked );
void mWrapBehaviorComboBox_currentIndexChanged( int index );
void mAdvancedCustomizationButton_clicked();
void useConditionalStylingChanged( bool checked );

//! Inserts a new maximum number of features into the spin box (without the spinbox emitting a signal)
void setMaximumNumberOfFeatures( int n );
Expand Down
110 changes: 100 additions & 10 deletions src/core/layout/qgslayoutitemattributetable.cpp
Expand Up @@ -30,6 +30,7 @@
#include "qgsmapsettings.h"
#include "qgsexpressioncontextutils.h"
#include "qgsgeometryengine.h"
#include "qgsconditionalstyle.h"

//QgsLayoutAttributeTableCompare

Expand All @@ -46,10 +47,10 @@ class CORE_EXPORT QgsLayoutAttributeTableCompare
* Constructor for QgsLayoutAttributeTableCompare.
*/
QgsLayoutAttributeTableCompare() = default;
bool operator()( const QgsLayoutTableRow &m1, const QgsLayoutTableRow &m2 )
bool operator()( const QVector< QPair< QVariant, QgsConditionalStyle > > &m1, const QVector< QPair< QVariant, QgsConditionalStyle > > &m2 )
{
return ( mAscending ? qgsVariantLessThan( m1[mCurrentSortColumn], m2[mCurrentSortColumn] )
: qgsVariantGreaterThan( m1[mCurrentSortColumn], m2[mCurrentSortColumn] ) );
return ( mAscending ? qgsVariantLessThan( m1[mCurrentSortColumn].first, m2[mCurrentSortColumn].first )
: qgsVariantGreaterThan( m1[mCurrentSortColumn].first, m2[mCurrentSortColumn].first ) );
}

/**
Expand Down Expand Up @@ -238,6 +239,23 @@ void QgsLayoutItemAttributeTable::disconnectCurrentMap()
mMap = nullptr;
}

bool QgsLayoutItemAttributeTable::useConditionalStyling() const
{
return mUseConditionalStyling;
}

void QgsLayoutItemAttributeTable::setUseConditionalStyling( bool useConditionalStyling )
{
if ( useConditionalStyling == mUseConditionalStyling )
{
return;
}

mUseConditionalStyling = useConditionalStyling;
refreshAttributes();
emit changed();
}

void QgsLayoutItemAttributeTable::setMap( QgsLayoutItemMap *map )
{
if ( map == mMap )
Expand Down Expand Up @@ -414,6 +432,8 @@ bool QgsLayoutItemAttributeTable::getTableContents( QgsLayoutTableContents &cont
return false;
}

const QgsConditionalLayerStyles *conditionalStyles = layer->conditionalStyles();

QgsExpressionContext context = createExpressionContext();
context.setFields( layer->fields() );

Expand Down Expand Up @@ -503,6 +523,11 @@ bool QgsLayoutItemAttributeTable::getTableContents( QgsLayoutTableContents &cont
int counter = 0;
QgsFeatureIterator fit = layer->getFeatures( req );

mConditionalStyles.clear();

QVector< QVector< QPair< QVariant, QgsConditionalStyle > > > tempContents;
QgsLayoutTableContents existingContents;

while ( fit.nextFeature( f ) && counter < mMaximumNumberOfFeatures )
{
context.setFeature( f );
Expand Down Expand Up @@ -539,15 +564,45 @@ bool QgsLayoutItemAttributeTable::getTableContents( QgsLayoutTableContents &cont
continue;
}

QgsLayoutTableRow currentRow;
QgsConditionalStyle rowStyle;

if ( mUseConditionalStyling )
{
const QList<QgsConditionalStyle> styles = QgsConditionalStyle::matchingConditionalStyles( conditionalStyles->rowStyles(), QVariant(), context );
rowStyle = QgsConditionalStyle::compressStyles( styles );
}

// We need to build up two different lists here -- one is a pair of the cell contents along with the cell style.
// We need this one because we do a sorting step later, and we need to ensure that the cell styling is attached to the right row and sorted
// correctly when this occurs
// We also need a list of just the cell contents, so that we can do a quick check for row uniqueness (when the
// corresponding option is enabled)
QVector< QPair< QVariant, QgsConditionalStyle > > currentRow;
currentRow.reserve( mColumns.count() );
QgsLayoutTableRow rowContents;
rowContents.reserve( mColumns.count() );

for ( QgsLayoutTableColumn *column : qgis::as_const( mColumns ) )
{
int idx = layer->fields().lookupField( column->attribute() );

QgsConditionalStyle style;

if ( idx != -1 )
{
currentRow << replaceWrapChar( f.attributes().at( idx ) );
const QVariant val = f.attributes().at( idx );

if ( mUseConditionalStyling )
{
QList<QgsConditionalStyle> styles = conditionalStyles->fieldStyles( layer->fields().at( idx ).name() );
styles = QgsConditionalStyle::matchingConditionalStyles( styles, val, context );
styles.insert( 0, rowStyle );
style = QgsConditionalStyle::compressStyles( styles );
}

QVariant v = replaceWrapChar( val );
currentRow << qMakePair( v, style );
rowContents << v;
}
else
{
Expand All @@ -556,15 +611,21 @@ bool QgsLayoutItemAttributeTable::getTableContents( QgsLayoutTableContents &cont
context.lastScope()->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "row_number" ), counter + 1, true ) );
expression->prepare( &context );
QVariant value = expression->evaluate( &context );
currentRow << value;

currentRow << qMakePair( value, rowStyle );
rowContents << value;
}
}

if ( !mShowUniqueRowsOnly || !contentsContainsRow( contents, currentRow ) )
if ( mShowUniqueRowsOnly )
{
contents << currentRow;
++counter;
if ( contentsContainsRow( existingContents, rowContents ) )
continue;
}

tempContents << currentRow;
existingContents << rowContents;
++counter;
}

//sort the list, starting with the last attribute
Expand All @@ -574,13 +635,40 @@ bool QgsLayoutItemAttributeTable::getTableContents( QgsLayoutTableContents &cont
{
c.setSortColumn( sortColumns.at( i ).first );
c.setAscending( sortColumns.at( i ).second );
std::stable_sort( contents.begin(), contents.end(), c );
std::stable_sort( tempContents.begin(), tempContents.end(), c );
}

// build final table contents
contents.reserve( tempContents.size() );
mConditionalStyles.reserve( tempContents.size() );
for ( auto it = tempContents.constBegin(); it != tempContents.constEnd(); ++it )
{
QgsLayoutTableRow row;
QList< QgsConditionalStyle > rowStyles;
row.reserve( it->size() );
rowStyles.reserve( it->size() );

for ( auto cellIt = it->constBegin(); cellIt != it->constEnd(); ++cellIt )
{
row << cellIt->first;
rowStyles << cellIt->second;
}
contents << row;
mConditionalStyles << rowStyles;
}

recalculateTableSize();
return true;
}

QgsConditionalStyle QgsLayoutItemAttributeTable::conditionalCellStyle( int row, int column ) const
{
if ( row >= mConditionalStyles.size() )
return QgsConditionalStyle();

return mConditionalStyles.at( row ).at( column );
}

QgsExpressionContext QgsLayoutItemAttributeTable::createExpressionContext() const
{
QgsExpressionContext context = QgsLayoutTable::createExpressionContext();
Expand Down Expand Up @@ -736,6 +824,7 @@ bool QgsLayoutItemAttributeTable::writePropertiesToElement( QDomElement &tableEl
tableElem.setAttribute( QStringLiteral( "filterFeatures" ), mFilterFeatures ? QStringLiteral( "true" ) : QStringLiteral( "false" ) );
tableElem.setAttribute( QStringLiteral( "featureFilter" ), mFeatureFilter );
tableElem.setAttribute( QStringLiteral( "wrapString" ), mWrapString );
tableElem.setAttribute( QStringLiteral( "useConditionalStyling" ), mUseConditionalStyling );

if ( mMap )
{
Expand Down Expand Up @@ -778,6 +867,7 @@ bool QgsLayoutItemAttributeTable::readPropertiesFromElement( const QDomElement &
mFeatureFilter = itemElem.attribute( QStringLiteral( "featureFilter" ), QString() );
mMaximumNumberOfFeatures = itemElem.attribute( QStringLiteral( "maxFeatures" ), QStringLiteral( "5" ) ).toInt();
mWrapString = itemElem.attribute( QStringLiteral( "wrapString" ) );
mUseConditionalStyling = itemElem.attribute( QStringLiteral( "useConditionalStyling" ), QStringLiteral( "0" ) ).toInt();

//map
mMapUuid = itemElem.attribute( QStringLiteral( "mapUuid" ) );
Expand Down
24 changes: 24 additions & 0 deletions src/core/layout/qgslayoutitemattributetable.h
Expand Up @@ -288,11 +288,31 @@ class CORE_EXPORT QgsLayoutItemAttributeTable: public QgsLayoutTable
*/
bool getTableContents( QgsLayoutTableContents &contents ) override SIP_SKIP;

QgsConditionalStyle conditionalCellStyle( int row, int column ) const override;

QgsExpressionContext createExpressionContext() const override;
void finalizeRestoreFromXml() override;

void refreshDataDefinedProperty( QgsLayoutObject::DataDefinedProperty property = QgsLayoutObject::AllProperties ) override;

/**
* Returns TRUE if the attribute table will be rendered using the conditional styling
* properties of the linked vector layer.
*
* \see setUseConditionalStyling()
* \since QGIS 3.12
*/
bool useConditionalStyling() const;

/**
* Sets whether the attribute table will be rendered using the conditional styling
* properties of the linked vector layer.
*
* \see useConditionalStyling()
* \since QGIS 3.12
*/
void setUseConditionalStyling( bool enabled );

protected:

bool writePropertiesToElement( QDomElement &elem, QDomDocument &doc, const QgsReadWriteContext &context ) const override;
Expand Down Expand Up @@ -342,6 +362,10 @@ class CORE_EXPORT QgsLayoutItemAttributeTable: public QgsLayoutTable

QString mWrapString;

bool mUseConditionalStyling = false;

QList< QList< QgsConditionalStyle > > mConditionalStyles;

/**
* Returns a list of attribute indices corresponding to displayed fields in the table.
* \note kept for compatibility with 2.0 api only
Expand Down
20 changes: 19 additions & 1 deletion src/core/layout/qgslayouttable.cpp
Expand Up @@ -435,7 +435,13 @@ void QgsLayoutTable::render( QgsLayoutItemRenderContext &context, const QRectF &
p->save();
p->setClipRect( fullCell );
const QRectF textCell = QRectF( currentX, currentY + mCellMargin, mMaxColumnWidthMap[col], rowHeight - 2 * mCellMargin );
QgsLayoutUtils::drawText( p, textCell, str, mContentFont, mContentFontColor, column->hAlignment(), column->vAlignment(), textFlag );

const QgsConditionalStyle style = conditionalCellStyle( row, col );
QColor foreColor = mContentFontColor;
if ( style.textColor().isValid() )
foreColor = style.textColor();

QgsLayoutUtils::drawText( p, textCell, str, mContentFont, foreColor, column->hAlignment(), column->vAlignment(), textFlag );
p->restore();

currentX += mMaxColumnWidthMap[ col ];
Expand Down Expand Up @@ -789,6 +795,11 @@ QMap<int, QString> QgsLayoutTable::headerLabels() const
return headers;
}

QgsConditionalStyle QgsLayoutTable::conditionalCellStyle( int, int ) const
{
return QgsConditionalStyle();
}

QSizeF QgsLayoutTable::fixedFrameSize( const int frameIndex ) const
{
Q_UNUSED( frameIndex )
Expand Down Expand Up @@ -1218,6 +1229,13 @@ QColor QgsLayoutTable::backgroundColor( int row, int column ) const
if ( style->enabled && row == mTableContents.count() - 1 )
color = style->cellBackgroundColor;

if ( row >= 0 )
{
QgsConditionalStyle conditionalStyle = conditionalCellStyle( row, column );
if ( conditionalStyle.backgroundColor().isValid() )
color = conditionalStyle.backgroundColor();
}

return color;
}

Expand Down
8 changes: 8 additions & 0 deletions src/core/layout/qgslayouttable.h
Expand Up @@ -21,6 +21,7 @@
#include "qgis_core.h"
#include "qgis_sip.h"
#include "qgslayoutmultiframe.h"
#include "qgsconditionalstyle.h"
#include <QFont>
#include <QColor>
#include <QPair>
Expand Down Expand Up @@ -465,6 +466,13 @@ class CORE_EXPORT QgsLayoutTable: public QgsLayoutMultiFrame
*/
virtual bool getTableContents( QgsLayoutTableContents &contents ) = 0;

/**
* Returns the conditional style to use for the cell at \a row, \a column.
*
* \since QGIS 3.12
*/
virtual QgsConditionalStyle conditionalCellStyle( int row, int column ) const;

/**
* Returns the current contents of the table. Excludes header cells.
*/
Expand Down

0 comments on commit 400e3da

Please sign in to comment.