Skip to content
Permalink
Browse files

[layouts] Add API to retrieve an expression context scope for a parti…

…cular

table cell, add backend support for QgsProperty based values in a manual
text table
  • Loading branch information
nyalldawson committed Jul 14, 2020
1 parent 59a7bb6 commit 1878f2eb3b6d63d72999e268c4389afe98a37f76
@@ -291,6 +291,8 @@ be replaced by a line break.

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

virtual QgsExpressionContextScope *scopeForCell( int row, int column ) const /Factory/;


virtual QgsExpressionContext createExpressionContext() const;

@@ -595,6 +595,13 @@ Fetches the contents used for the cells in the table.
Returns the conditional style to use for the cell at ``row``, ``column``.

.. versionadded:: 3.12
%End

virtual QgsExpressionContextScope *scopeForCell( int row, int column ) const /Factory/;
%Docstring
Creates a new QgsExpressionContextScope for the cell at ``row``, ``column``.

.. versionadded:: 3.16
%End

QgsLayoutTableContents &contents();
@@ -486,8 +486,9 @@ bool QgsLayoutItemAttributeTable::getTableContents( QgsLayoutTableContents &cont
QgsFeatureIterator fit = layer->getFeatures( req );

mConditionalStyles.clear();
mFeatures.clear();

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

while ( fit.nextFeature( f ) && counter < mMaximumNumberOfFeatures )
@@ -539,7 +540,7 @@ bool QgsLayoutItemAttributeTable::getTableContents( QgsLayoutTableContents &cont
// 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;
QVector< Cell > currentRow;
currentRow.reserve( mColumns.count() );
QgsLayoutTableRow rowContents;
rowContents.reserve( mColumns.count() );
@@ -563,7 +564,7 @@ bool QgsLayoutItemAttributeTable::getTableContents( QgsLayoutTableContents &cont
}

QVariant v = replaceWrapChar( val );
currentRow << qMakePair( v, style );
currentRow << Cell( v, style, f );
rowContents << v;
}
else
@@ -574,7 +575,7 @@ bool QgsLayoutItemAttributeTable::getTableContents( QgsLayoutTableContents &cont
expression->prepare( &context );
QVariant value = expression->evaluate( &context );

currentRow << qMakePair( value, rowStyle );
currentRow << Cell( value, rowStyle, f );
rowContents << value;
}
}
@@ -593,6 +594,7 @@ bool QgsLayoutItemAttributeTable::getTableContents( QgsLayoutTableContents &cont
// build final table contents
contents.reserve( tempContents.size() );
mConditionalStyles.reserve( tempContents.size() );
mFeatures.reserve( tempContents.size() );
for ( auto it = tempContents.constBegin(); it != tempContents.constEnd(); ++it )
{
QgsLayoutTableRow row;
@@ -602,8 +604,10 @@ bool QgsLayoutItemAttributeTable::getTableContents( QgsLayoutTableContents &cont

for ( auto cellIt = it->constBegin(); cellIt != it->constEnd(); ++cellIt )
{
row << cellIt->first;
rowStyles << cellIt->second;
row << cellIt->content;
rowStyles << cellIt->style;
if ( cellIt == it->constBegin() )
mFeatures << cellIt->feature;
}
contents << row;
mConditionalStyles << rowStyles;
@@ -621,6 +625,14 @@ QgsConditionalStyle QgsLayoutItemAttributeTable::conditionalCellStyle( int row,
return mConditionalStyles.at( row ).at( column );
}

QgsExpressionContextScope *QgsLayoutItemAttributeTable::scopeForCell( int row, int column ) const
{
std::unique_ptr< QgsExpressionContextScope >scope( QgsLayoutTable::scopeForCell( row, column ) );
scope->setFeature( mFeatures.value( row ) );
scope->setFields( scope->feature().fields() );
return scope.release();
}

QgsExpressionContext QgsLayoutItemAttributeTable::createExpressionContext() const
{
QgsExpressionContext context = QgsLayoutTable::createExpressionContext();
@@ -280,6 +280,7 @@ class CORE_EXPORT QgsLayoutItemAttributeTable: public QgsLayoutTable
bool getTableContents( QgsLayoutTableContents &contents ) override SIP_SKIP;

QgsConditionalStyle conditionalCellStyle( int row, int column ) const override;
QgsExpressionContextScope *scopeForCell( int row, int column ) const override SIP_FACTORY;

QgsExpressionContext createExpressionContext() const override;
void finalizeRestoreFromXml() override;
@@ -356,6 +357,18 @@ class CORE_EXPORT QgsLayoutItemAttributeTable: public QgsLayoutTable
bool mUseConditionalStyling = false;

QList< QList< QgsConditionalStyle > > mConditionalStyles;
QList< QgsFeature > mFeatures;

struct Cell
{
Cell( const QVariant &content, const QgsConditionalStyle &style, const QgsFeature &feature )
: content( content )
, style( style )
, feature( feature ) {}
QVariant content;
QgsConditionalStyle style;
QgsFeature feature;
};

/**
* Returns a list of attribute indices corresponding to displayed fields in the table.
@@ -21,6 +21,7 @@
#include "qgslayouttablecolumn.h"
#include "qgsnumericformat.h"
#include "qgsxmlutils.h"
#include "qgsexpressioncontextutils.h"

//
// QgsLayoutItemManualTable
@@ -63,25 +64,38 @@ bool QgsLayoutItemManualTable::getTableContents( QgsLayoutTableContents &content

QgsNumericFormatContext numericContext;

QgsExpressionContext context = createExpressionContext();

int rowNumber = 0;
for ( const QgsTableRow &row : qgis::as_const( mContents ) )
{
QgsLayoutTableRow currentRow;

for ( int i = 0; i < mColumns.count(); ++i )
for ( int columnNumber = 0; columnNumber < mColumns.count(); ++columnNumber )
{
if ( i < row.count() )
if ( columnNumber < row.count() )
{
if ( row.at( i ).numericFormat() )
currentRow << row.at( i ).numericFormat()->formatDouble( row.at( i ).content().toDouble(), numericContext );
QVariant cellContent = row.at( columnNumber ).content();

if ( cellContent.canConvert< QgsProperty >() )
{
// expression based cell content, evaluate now
QgsExpressionContextScopePopper popper( context, scopeForCell( rowNumber, columnNumber ) );
cellContent = cellContent.value< QgsProperty >().value( context );
}

if ( row.at( columnNumber ).numericFormat() )
currentRow << row.at( columnNumber ).numericFormat()->formatDouble( cellContent.toDouble(), numericContext );
else
currentRow << row.at( i ).content().toString();
currentRow << cellContent.toString();
}
else
{
currentRow << QString();
}
}
contents << currentRow;
rowNumber++;
}

recalculateTableSize();
@@ -989,6 +989,14 @@ QMap<int, QString> QgsLayoutTable::headerLabels() const
return headers;
}

QgsExpressionContextScope *QgsLayoutTable::scopeForCell( int row, int column ) const
{
std::unique_ptr< QgsExpressionContextScope > cellScope = qgis::make_unique< QgsExpressionContextScope >();
cellScope->setVariable( QStringLiteral( "table_row" ), row + 1, true );
cellScope->setVariable( QStringLiteral( "table_column" ), column + 1, true );
return cellScope.release();
}

QgsConditionalStyle QgsLayoutTable::conditionalCellStyle( int, int ) const
{
return QgsConditionalStyle();
@@ -537,6 +537,13 @@ class CORE_EXPORT QgsLayoutTable: public QgsLayoutMultiFrame
*/
virtual QgsConditionalStyle conditionalCellStyle( int row, int column ) const;

/**
* Creates a new QgsExpressionContextScope for the cell at \a row, \a column.
*
* \since QGIS 3.16
*/
virtual QgsExpressionContextScope *scopeForCell( int row, int column ) const SIP_FACTORY;

/**
* Returns the current contents of the table. Excludes header cells.
*/
@@ -44,6 +44,8 @@ class TestQgsLayoutManualTable : public QObject
void cleanup();// will be called after every testfunction.

void setContents();
void scopeForCell();
void expressionContents();
void cellStyles();
void cellFormat();
void rowHeight();
@@ -270,6 +272,62 @@ void TestQgsLayoutManualTable::setContents()
QCOMPARE( tableFromXml->columnWidths(), QList< double >() << 15.5 << 14.0 << 13.4 );
}

void TestQgsLayoutManualTable::scopeForCell()
{
QVector<QStringList> expectedRows;

QgsPrintLayout l( QgsProject::instance() );
l.initializeDefaults();
l.setName( QStringLiteral( "my layout" ) );
QgsLayoutItemManualTable *table = new QgsLayoutItemManualTable( &l );

std::unique_ptr< QgsExpressionContextScope > scope( table->scopeForCell( 1, 2 ) );

// variable values for row/col should start at 1, not 0!
QCOMPARE( scope->variable( QStringLiteral( "table_row" ) ).toInt(), 2 );
QCOMPARE( scope->variable( QStringLiteral( "table_column" ) ).toInt(), 3 );
}

void TestQgsLayoutManualTable::expressionContents()
{
QVector<QStringList> expectedRows;

QgsPrintLayout l( QgsProject::instance() );
l.initializeDefaults();
l.setName( QStringLiteral( "my layout" ) );
QgsLayoutItemManualTable *table = new QgsLayoutItemManualTable( &l );

QStringList row;

// 2 x 3
row << QStringLiteral( "Jet" ) << QStringLiteral( "1,2" ) << QStringLiteral( "1,3" );
expectedRows.append( row );
row.clear();
row << QStringLiteral( "my layout" ) << QStringLiteral( "Helicopter" ) << QStringLiteral( "Plane" );
expectedRows.append( row );

table->setTableContents(
QgsTableContents() << ( QgsTableRow() << QgsTableCell( QStringLiteral( "Jet" ) ) << QgsTableCell( QgsProperty::fromExpression( QStringLiteral( "@table_row || ',' || @table_column" ) ) ) << QgsTableCell( QgsProperty::fromExpression( QStringLiteral( "@table_row || ',' || @table_column" ) ) ) )
<< ( QgsTableRow() << QgsTableCell( QgsProperty::fromExpression( QStringLiteral( "@layout_name" ) ) ) << QgsTableCell( QStringLiteral( "Helicopter" ) ) << QgsTableCell( QStringLiteral( "Plane" ) ) ) );
compareTable( table, expectedRows );

// save and restore

//write to XML
QDomImplementation DomImplementation;
QDomDocumentType documentType =
DomImplementation.createDocumentType(
QStringLiteral( "qgis" ), QStringLiteral( "http://mrcc.com/qgis.dtd" ), QStringLiteral( "SYSTEM" ) );
QDomDocument doc( documentType );
QDomElement tableElement = doc.createElement( QStringLiteral( "table" ) );
QVERIFY( table->writeXml( tableElement, doc, QgsReadWriteContext(), true ) );

//read from XML
QgsLayoutItemManualTable *tableFromXml = new QgsLayoutItemManualTable( &l );
QVERIFY( tableFromXml->readXml( tableElement.firstChildElement(), doc, QgsReadWriteContext(), true ) );
compareTable( tableFromXml, expectedRows );
}

void TestQgsLayoutManualTable::cellStyles()
{
QgsLayout l( QgsProject::instance() );
@@ -82,6 +82,7 @@ class TestQgsLayoutTable : public QObject
void wrappedText();
void testBaseSort();
void testExpressionSort();
void testScopeForCell();

private:
QgsVectorLayer *mVectorLayer = nullptr;
@@ -1639,5 +1640,35 @@ void TestQgsLayoutTable::testExpressionSort()
compareTable( table, expectedRows );
}

void TestQgsLayoutTable::testScopeForCell()
{
QgsLayout l( QgsProject::instance() );
l.initializeDefaults();
QgsLayoutItemAttributeTable *table = new QgsLayoutItemAttributeTable( &l );
table->setVectorLayer( mVectorLayer );
table->refresh();

std::unique_ptr< QgsExpressionContextScope > scope( table->scopeForCell( 0, 0 ) );

// variable values for row/col should start at 1, not 0!
QCOMPARE( scope->variable( QStringLiteral( "table_row" ) ).toInt(), 1 );
QCOMPARE( scope->variable( QStringLiteral( "table_column" ) ).toInt(), 1 );
QCOMPARE( scope->feature().attribute( 0 ).toString(), QStringLiteral( "Jet" ) );
scope.reset( table->scopeForCell( 0, 1 ) );
QCOMPARE( scope->variable( QStringLiteral( "table_row" ) ).toInt(), 1 );
QCOMPARE( scope->variable( QStringLiteral( "table_column" ) ).toInt(), 2 );
QCOMPARE( scope->feature().attribute( 0 ).toString(), QStringLiteral( "Jet" ) );
scope.reset( table->scopeForCell( 1, 2 ) );
QCOMPARE( scope->variable( QStringLiteral( "table_row" ) ).toInt(), 2 );
QCOMPARE( scope->variable( QStringLiteral( "table_column" ) ).toInt(), 3 );
QCOMPARE( scope->feature().attribute( 0 ).toString(), QStringLiteral( "Biplane" ) );

// make sure fields are set
QgsExpressionContext context;
context.appendScope( scope.release() );
QCOMPARE( context.fields().size(), 6 );
QCOMPARE( context.fields().at( 0 ).name(), QStringLiteral( "Class" ) );
}

QGSTEST_MAIN( TestQgsLayoutTable )
#include "testqgslayouttable.moc"

0 comments on commit 1878f2e

Please sign in to comment.
You can’t perform that action at this time.