From 14690d07163f71e63014907176c0273d52d89a94 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 22 Sep 2014 22:59:30 +1000 Subject: [PATCH] [FEATURE][composer] Add choice of display style for empty tables. Options include hiding the entire table, showing empty cells, or displaying a set message in the table body. Sponsored by City of Uster, Switzerland. --- .../composer/qgscomposerattributetablev2.sip | 8 + python/core/composer/qgscomposertablev2.sip | 44 ++++- .../qgscomposerattributetablewidget.cpp | 50 ++++++ .../qgscomposerattributetablewidget.h | 2 + src/core/composer/qgscomposertablev2.cpp | 109 +++++++++++-- src/core/composer/qgscomposertablev2.h | 50 +++++- src/ui/qgscomposerattributetablewidgetbase.ui | 151 +++++++++++------- tests/src/core/testqgscomposertablev2.cpp | 31 +++- ...ected_composerattributetable_drawempty.png | Bin 0 -> 16879 bytes ...ted_composerattributetable_headersonly.png | Bin 0 -> 14253 bytes ...ected_composerattributetable_hidetable.png | Bin 0 -> 5663 bytes ...ted_composerattributetable_showmessage.png | Bin 0 -> 17392 bytes 12 files changed, 367 insertions(+), 78 deletions(-) create mode 100644 tests/testdata/control_images/expected_composerattributetable_drawempty/expected_composerattributetable_drawempty.png create mode 100644 tests/testdata/control_images/expected_composerattributetable_headersonly/expected_composerattributetable_headersonly.png create mode 100644 tests/testdata/control_images/expected_composerattributetable_hidetable/expected_composerattributetable_hidetable.png create mode 100644 tests/testdata/control_images/expected_composerattributetable_showmessage/expected_composerattributetable_showmessage.png diff --git a/python/core/composer/qgscomposerattributetablev2.sip b/python/core/composer/qgscomposerattributetablev2.sip index ce680429c1e..c5c7931728c 100644 --- a/python/core/composer/qgscomposerattributetablev2.sip +++ b/python/core/composer/qgscomposerattributetablev2.sip @@ -72,6 +72,14 @@ class QgsComposerAttributeTableV2 : QgsComposerTableV2 * @see setSource */ ContentSource source() const; + + /**Returns the source layer for the table, considering the table source mode. Eg, + * if the table is set to atlas feature mode, then the source layer will be the + * atlas coverage layer. If the table is set to layer attributes mode, then + * the source layer will be the user specified vector layer. + * @returns actual source layer + */ + QgsVectorLayer* sourceLayer(); /**Sets the vector layer from which to display feature attributes * @param layer Vector layer for attribute table diff --git a/python/core/composer/qgscomposertablev2.sip b/python/core/composer/qgscomposertablev2.sip index 22c33d3acaa..81a69af4890 100644 --- a/python/core/composer/qgscomposertablev2.sip +++ b/python/core/composer/qgscomposertablev2.sip @@ -45,6 +45,16 @@ class QgsComposerTableV2: QgsComposerMultiFrame AllFrames, /*!< headers shown on all frames */ NoHeaders /*!< no headers shown for table */ }; + + /*! Controls how empty tables are displayed + */ + enum EmptyTableMode + { + HeadersOnly, /*!< show header rows only */ + HideTable, /*!< hides entire table if empty */ + DrawEmptyCells, /*!< draws empty cells */ + ShowMessage /*!< shows preset message instead of table contents*/ + }; QgsComposerTableV2( QgsComposition* composition /TransferThis/, bool createUndoCommands ); QgsComposerTableV2(); @@ -62,6 +72,37 @@ class QgsComposerTableV2: QgsComposerMultiFrame * @see setCellMargin */ double cellMargin() const; + + /**Sets the behaviour for empty tables with no content rows. + * @param mode behaviour mode for empty tables + * @see emptyTableBehaviour + */ + void setEmptyTableBehaviour( const EmptyTableMode mode ); + + /**Returns the behaviour mode for empty tables. This property controls + * how the table is drawn if it contains no content rows. + * @returns behaviour mode for empty tables + * @see setEmptyTableBehaviour + */ + EmptyTableMode emptyTableBehaviour() const; + + /**Sets the message for empty tables with no content rows. This message + * is displayed in the table body if the empty table behaviour is + * set to ShowMessage + * @param message message to show for empty tables + * @see emptyTableMessage + * @see setEmptyTableBehaviour + */ + void setEmptyTableMessage( const QString message ); + + /**Returns the message for empty tables with no content rows. This message + * is displayed in the table body if the empty table behaviour is + * set to ShowMessage + * @returns message to show for empty tables + * @see setEmptyTableMessage + * @see emptyTableBehaviour + */ + QString emptyTableMessage() const; /**Sets the font used to draw header text in the table. * @param font font for header cells @@ -305,12 +346,13 @@ class QgsComposerTableV2: QgsComposerMultiFrame * maximum width of text present in the column. * @param numberRows number of rows of content in table frame * @param hasHeader set to true if table frame includes header cells + * @param mergeCells set to true to merge table content cells * @note not available in python bindings * @see drawVerticalGridLines * @see calculateMaxColumnWidths * @note not available in python bindings */ - //void drawVerticalGridLines( QPainter* painter, const QMap& maxWidthMap, const int numberRows, const bool hasHeader ) const; + //void drawVerticalGridLines( QPainter* painter, const QMap& maxWidthMap, const int numberRows, const bool hasHeader, const bool mergeCells = false ) const; /**Recalculates and updates the size of the table and all table frames. */ diff --git a/src/app/composer/qgscomposerattributetablewidget.cpp b/src/app/composer/qgscomposerattributetablewidget.cpp index 252da3e9540..5bbba859eed 100644 --- a/src/app/composer/qgscomposerattributetablewidget.cpp +++ b/src/app/composer/qgscomposerattributetablewidget.cpp @@ -47,6 +47,11 @@ QgsComposerAttributeTableWidget::QgsComposerAttributeTableWidget( QgsComposerAtt mResizeModeComboBox->addItem( tr( "Extend to next page" ), QgsComposerMultiFrame::ExtendToNextPage ); mResizeModeComboBox->addItem( tr( "Repeat until finished" ), QgsComposerMultiFrame::RepeatUntilFinished ); + mEmptyModeComboBox->addItem( tr( "Draw headers only" ), QgsComposerTableV2::HeadersOnly ); + mEmptyModeComboBox->addItem( tr( "Hide entire table" ), QgsComposerTableV2::HideTable ); + mEmptyModeComboBox->addItem( tr( "Draw empty cells" ), QgsComposerTableV2::DrawEmptyCells ); + mEmptyModeComboBox->addItem( tr( "Show set message" ), QgsComposerTableV2::ShowMessage ); + bool atlasEnabled = atlasComposition() && atlasComposition()->enabled(); mSourceComboBox->addItem( tr( "Layer features" ), QgsComposerAttributeTableV2::LayerAttributes ); toggleAtlasSpecificControls( atlasEnabled ); @@ -494,6 +499,11 @@ void QgsComposerAttributeTableWidget::updateGuiElements() mHeaderHAlignmentComboBox->setCurrentIndex(( int )mComposerTable->headerHAlignment() ); mHeaderModeComboBox->setCurrentIndex(( int )mComposerTable->headerMode() ); + mEmptyModeComboBox->setCurrentIndex( mEmptyModeComboBox->findData( mComposerTable->emptyTableBehaviour() ) ); + mEmptyMessageLineEdit->setText( mComposerTable->emptyTableMessage() ); + mEmptyMessageLineEdit->setEnabled( mComposerTable->emptyTableBehaviour() == QgsComposerTableV2::ShowMessage ); + mEmptyMessageLabel->setEnabled( mComposerTable->emptyTableBehaviour() == QgsComposerTableV2::ShowMessage ); + mResizeModeComboBox->setCurrentIndex( mResizeModeComboBox->findData( mComposerTable->resizeMode() ) ); mAddFramePushButton->setEnabled( mComposerTable->resizeMode() == QgsComposerMultiFrame::UseExistingFrames ); @@ -585,6 +595,8 @@ void QgsComposerAttributeTableWidget::blockAllSignals( bool b ) mContentFontColorButton->blockSignals( b ); mResizeModeComboBox->blockSignals( b ); mRelationsComboBox->blockSignals( b ); + mEmptyModeComboBox->blockSignals( b ); + mEmptyMessageLineEdit->blockSignals( b ); } void QgsComposerAttributeTableWidget::setMaximumNumberOfFeatures( int n ) @@ -850,6 +862,44 @@ void QgsComposerAttributeTableWidget::on_mRelationsComboBox_currentIndexChanged( } } +void QgsComposerAttributeTableWidget::on_mEmptyModeComboBox_currentIndexChanged( int index ) +{ + if ( !mComposerTable ) + { + return; + } + + QgsComposition* composition = mComposerTable->composition(); + if ( composition ) + { + composition->beginMultiFrameCommand( mComposerTable, tr( "Change empty table behaviour" ) ); + mComposerTable->setEmptyTableBehaviour(( QgsComposerTableV2::EmptyTableMode ) mEmptyModeComboBox->itemData( index ).toInt() ); + composition->endMultiFrameCommand(); + mEmptyMessageLineEdit->setEnabled( mComposerTable->emptyTableBehaviour() == QgsComposerTableV2::ShowMessage ); + mEmptyMessageLabel->setEnabled( mComposerTable->emptyTableBehaviour() == QgsComposerTableV2::ShowMessage ); + } +} + +void QgsComposerAttributeTableWidget::on_mEmptyMessageLineEdit_editingFinished() +{ + if ( !mComposerTable ) + { + return; + } + + QgsComposition* composition = mComposerTable->composition(); + if ( composition ) + { + composition->beginMultiFrameCommand( mComposerTable, tr( "Empty table message changed" ) ); + } + mComposerTable->setEmptyTableMessage( mEmptyMessageLineEdit->text() ); + mComposerTable->update(); + if ( composition ) + { + composition->endMultiFrameCommand(); + } +} + void QgsComposerAttributeTableWidget::toggleSourceControls() { switch ( mComposerTable->source() ) diff --git a/src/app/composer/qgscomposerattributetablewidget.h b/src/app/composer/qgscomposerattributetablewidget.h index 8143297abd7..5d7e356966a 100644 --- a/src/app/composer/qgscomposerattributetablewidget.h +++ b/src/app/composer/qgscomposerattributetablewidget.h @@ -70,6 +70,8 @@ class QgsComposerAttributeTableWidget: public QgsComposerItemBaseWidget, private void on_mResizeModeComboBox_currentIndexChanged( int index ); void on_mSourceComboBox_currentIndexChanged( int index ); void on_mRelationsComboBox_currentIndexChanged( int index ); + void on_mEmptyModeComboBox_currentIndexChanged( int index ); + void on_mEmptyMessageLineEdit_editingFinished(); /**Inserts a new maximum number of features into the spin box (without the spinbox emitting a signal)*/ void setMaximumNumberOfFeatures( int n ); diff --git a/src/core/composer/qgscomposertablev2.cpp b/src/core/composer/qgscomposertablev2.cpp index 3b817e62cbb..9780f4cf395 100644 --- a/src/core/composer/qgscomposertablev2.cpp +++ b/src/core/composer/qgscomposertablev2.cpp @@ -24,6 +24,7 @@ QgsComposerTableV2::QgsComposerTableV2( QgsComposition *composition, bool createUndoCommands ) : QgsComposerMultiFrame( composition, createUndoCommands ) , mCellMargin( 1.0 ) + , mEmptyTableMode( HeadersOnly ) , mHeaderFontColor( Qt::black ) , mHeaderHAlignment( FollowColumn ) , mHeaderMode( FirstFrame ) @@ -63,6 +64,8 @@ QgsComposerTableV2::~QgsComposerTableV2() bool QgsComposerTableV2::writeXML( QDomElement& elem, QDomDocument & doc, bool ignoreFrames ) const { elem.setAttribute( "cellMargin", QString::number( mCellMargin ) ); + elem.setAttribute( "emptyTableMode", QString::number(( int )mEmptyTableMode ) ); + elem.setAttribute( "emptyTableMessage", mEmptyTableMessage ); elem.setAttribute( "headerFont", mHeaderFont.toString() ); elem.setAttribute( "headerFontColor", QgsSymbolLayerV2Utils::encodeColor( mHeaderFontColor ) ); elem.setAttribute( "headerHAlignment", QString::number(( int )mHeaderHAlignment ) ); @@ -103,6 +106,8 @@ bool QgsComposerTableV2::readXML( const QDomElement &itemElem, const QDomDocumen return false; } + mEmptyTableMode = QgsComposerTableV2::EmptyTableMode( itemElem.attribute( "emptyTableMode", "0" ).toInt() ); + mEmptyTableMessage = itemElem.attribute( "emptyTableMessage", tr( "No matching records" ) ); mHeaderFont.fromString( itemElem.attribute( "headerFont", "" ) ); mHeaderFontColor = QgsSymbolLayerV2Utils::decodeColor( itemElem.attribute( "headerFontColor", "0,0,0,255" ) ); mHeaderHAlignment = QgsComposerTableV2::HeaderHAlignment( itemElem.attribute( "headerHAlignment", "0" ).toInt() ); @@ -229,6 +234,13 @@ void QgsComposerTableV2::render( QPainter *p, const QRectF &renderExtent, const return; } + bool emptyTable = mTableContents.length() == 0; + if ( emptyTable && mEmptyTableMode == QgsComposerTableV2::HideTable ) + { + //empty table set to hide table mode, so don't draw anything + return; + } + //calculate which rows to show in this frame QPair< int, int > rowsToShow = rowRange( renderExtent, frameIndex ); @@ -260,6 +272,8 @@ void QgsComposerTableV2::render( QPainter *p, const QRectF &renderExtent, const //calculate whether a header is required bool drawHeader = (( mHeaderMode == QgsComposerTableV2::FirstFrame && frameIndex < 1 ) || ( mHeaderMode == QgsComposerTableV2::AllFrames ) ); + //calculate whether drawing table contents is required + bool drawContents = !( emptyTable && mEmptyTableMode == QgsComposerTableV2::ShowMessage ); for ( ; columnIt != mColumns.constEnd(); ++columnIt ) { @@ -304,18 +318,21 @@ void QgsComposerTableV2::render( QPainter *p, const QRectF &renderExtent, const currentY += ( mShowGrid ? mGridStrokeWidth : 0 ); } - //draw the attribute values - for ( int row = rowsToShow.first; row < rowsToShow.second; ++row ) + if ( drawContents ) { - cell = QRectF( currentX, currentY, mMaxColumnWidthMap[col], cellBodyHeight ); + //draw the attribute values + for ( int row = rowsToShow.first; row < rowsToShow.second; ++row ) + { + cell = QRectF( currentX, currentY, mMaxColumnWidthMap[col], cellBodyHeight ); - QVariant cellContents = mTableContents.at( row ).at( col ); - QString str = cellContents.toString(); + QVariant cellContents = mTableContents.at( row ).at( col ); + QString str = cellContents.toString(); - QgsComposerUtils::drawText( p, cell, str, mContentFont, mContentFontColor, ( *columnIt )->hAlignment(), Qt::AlignVCenter, textFlag ); + QgsComposerUtils::drawText( p, cell, str, mContentFont, mContentFontColor, ( *columnIt )->hAlignment(), Qt::AlignVCenter, textFlag ); - currentY += cellBodyHeight; - currentY += ( mShowGrid ? mGridStrokeWidth : 0 ); + currentY += cellBodyHeight; + currentY += ( mShowGrid ? mGridStrokeWidth : 0 ); + } } currentX += mMaxColumnWidthMap[ col ]; @@ -324,17 +341,39 @@ void QgsComposerTableV2::render( QPainter *p, const QRectF &renderExtent, const col++; } - //and the borders if ( mShowGrid ) { + int numberRowsToDraw = rowsToShow.second - rowsToShow.first; + if ( mEmptyTableMode == QgsComposerTableV2::DrawEmptyCells ) + { + numberRowsToDraw = rowsVisible( frameIndex ); + } + bool mergeCells = false; + if ( emptyTable && mEmptyTableMode == QgsComposerTableV2::ShowMessage ) + { + //draw a merged row for the empty table message + numberRowsToDraw++; + mergeCells = true; + } + QPen gridPen; gridPen.setWidthF( mGridStrokeWidth ); gridPen.setColor( mGridColor ); gridPen.setJoinStyle( Qt::MiterJoin ); p->setPen( gridPen ); - drawHorizontalGridLines( p, rowsToShow.second - rowsToShow.first, drawHeader ); - drawVerticalGridLines( p, mMaxColumnWidthMap, rowsToShow.second - rowsToShow.first, drawHeader ); + drawHorizontalGridLines( p, numberRowsToDraw, drawHeader ); + drawVerticalGridLines( p, mMaxColumnWidthMap, numberRowsToDraw, drawHeader, mergeCells ); + } + + //special case - no records and table is set to ShowMessage mode + if ( emptyTable && mEmptyTableMode == QgsComposerTableV2::ShowMessage ) + { + double messageX = ( mShowGrid ? mGridStrokeWidth : 0 ) + mCellMargin; + double messageY = ( mShowGrid ? mGridStrokeWidth : 0 ) + + ( drawHeader ? cellHeaderHeight + ( mShowGrid ? mGridStrokeWidth : 0 ) : 0 ); + cell = QRectF( messageX, messageY, mTableSize.width() - messageX, cellBodyHeight ); + QgsComposerUtils::drawText( p, cell, mEmptyTableMessage, mContentFont, mContentFontColor, Qt::AlignHCenter, Qt::AlignVCenter, ( Qt::TextFlag )0 ); } p->restore(); @@ -356,6 +395,36 @@ void QgsComposerTableV2::setCellMargin( const double margin ) emit changed(); } +void QgsComposerTableV2::setEmptyTableBehaviour( const QgsComposerTableV2::EmptyTableMode mode ) +{ + if ( mode == mEmptyTableMode ) + { + return; + } + + mEmptyTableMode = mode; + + //since appearance has changed, we need to recalculate the table size + recalculateTableSize(); + + emit changed(); +} + +void QgsComposerTableV2::setEmptyTableMessage( const QString message ) +{ + if ( message == mEmptyTableMessage ) + { + return; + } + + mEmptyTableMessage = message; + + //since message has changed, we need to recalculate the table size + recalculateTableSize(); + + emit changed(); +} + void QgsComposerTableV2::setHeaderFont( const QFont &font ) { if ( font == mHeaderFont ) @@ -683,7 +752,7 @@ void QgsComposerTableV2::drawHorizontalGridLines( QPainter *painter, const int r painter->drawLine( QPointF( halfGridStrokeWidth, currentY ), QPointF( mTableSize.width() - halfGridStrokeWidth, currentY ) ); } -void QgsComposerTableV2::drawVerticalGridLines( QPainter *painter, const QMap &maxWidthMap, const int numberRows, const bool hasHeader ) const +void QgsComposerTableV2::drawVerticalGridLines( QPainter *painter, const QMap &maxWidthMap, const int numberRows, const bool hasHeader, const bool mergeCells ) const { //vertical lines if ( numberRows < 1 && !hasHeader ) @@ -697,20 +766,30 @@ void QgsComposerTableV2::drawVerticalGridLines( QPainter *painter, const QMapdrawLine( QPointF( currentX, halfGridStrokeWidth ), QPointF( currentX, tableHeight - halfGridStrokeWidth ) ); currentX += ( mShowGrid ? mGridStrokeWidth : 0 ); QMap::const_iterator maxColWidthIt = maxWidthMap.constBegin(); + int col = 1; for ( ; maxColWidthIt != maxWidthMap.constEnd(); ++maxColWidthIt ) { currentX += ( maxColWidthIt.value() + 2 * mCellMargin ); - painter->drawLine( QPointF( currentX, halfGridStrokeWidth ), QPointF( currentX, tableHeight - halfGridStrokeWidth ) ); + if ( col == maxWidthMap.size() || !mergeCells ) + { + painter->drawLine( QPointF( currentX, halfGridStrokeWidth ), QPointF( currentX, tableHeight - halfGridStrokeWidth ) ); + } + else if ( hasHeader ) + { + painter->drawLine( QPointF( currentX, halfGridStrokeWidth ), QPointF( currentX, headerHeight - halfGridStrokeWidth ) ); + } + currentX += ( mShowGrid ? mGridStrokeWidth : 0 ); + col++; } } diff --git a/src/core/composer/qgscomposertablev2.h b/src/core/composer/qgscomposertablev2.h index 4dc6436420a..3f6929d6133 100644 --- a/src/core/composer/qgscomposertablev2.h +++ b/src/core/composer/qgscomposertablev2.h @@ -71,6 +71,16 @@ class CORE_EXPORT QgsComposerTableV2: public QgsComposerMultiFrame NoHeaders /*!< no headers shown for table */ }; + /*! Controls how empty tables are displayed + */ + enum EmptyTableMode + { + HeadersOnly = 0, /*!< show header rows only */ + HideTable, /*!< hides entire table if empty */ + DrawEmptyCells, /*!< draws empty cells */ + ShowMessage /*!< shows preset message instead of table contents*/ + }; + QgsComposerTableV2( QgsComposition* composition, bool createUndoCommands ); QgsComposerTableV2(); @@ -88,6 +98,37 @@ class CORE_EXPORT QgsComposerTableV2: public QgsComposerMultiFrame */ double cellMargin() const { return mCellMargin; } + /**Sets the behaviour for empty tables with no content rows. + * @param mode behaviour mode for empty tables + * @see emptyTableBehaviour + */ + void setEmptyTableBehaviour( const EmptyTableMode mode ); + + /**Returns the behaviour mode for empty tables. This property controls + * how the table is drawn if it contains no content rows. + * @returns behaviour mode for empty tables + * @see setEmptyTableBehaviour + */ + EmptyTableMode emptyTableBehaviour() const { return mEmptyTableMode; } + + /**Sets the message for empty tables with no content rows. This message + * is displayed in the table body if the empty table behaviour is + * set to ShowMessage + * @param message message to show for empty tables + * @see emptyTableMessage + * @see setEmptyTableBehaviour + */ + void setEmptyTableMessage( const QString message ); + + /**Returns the message for empty tables with no content rows. This message + * is displayed in the table body if the empty table behaviour is + * set to ShowMessage + * @returns message to show for empty tables + * @see setEmptyTableMessage + * @see emptyTableBehaviour + */ + QString emptyTableMessage() const { return mEmptyTableMessage; } + /**Sets the font used to draw header text in the table. * @param font font for header cells * @see headerFont @@ -279,6 +320,12 @@ class CORE_EXPORT QgsComposerTableV2: public QgsComposerMultiFrame /**Margin between cell borders and cell text*/ double mCellMargin; + /**Behaviour for empty tables*/ + EmptyTableMode mEmptyTableMode; + + /**String to show in empty tables*/ + QString mEmptyTableMessage; + /**Header font*/ QFont mHeaderFont; @@ -370,12 +417,13 @@ class CORE_EXPORT QgsComposerTableV2: public QgsComposerMultiFrame * maximum width of text present in the column. * @param numberRows number of rows of content in table frame * @param hasHeader set to true if table frame includes header cells + * @param mergeCells set to true to merge table content cells * @note not available in python bindings * @see drawVerticalGridLines * @see calculateMaxColumnWidths * @note not available in python bindings */ - void drawVerticalGridLines( QPainter* painter, const QMap& maxWidthMap, const int numberRows, const bool hasHeader ) const; + void drawVerticalGridLines( QPainter* painter, const QMap& maxWidthMap, const int numberRows, const bool hasHeader, const bool mergeCells = false ) const; /**Recalculates and updates the size of the table and all table frames. */ diff --git a/src/ui/qgscomposerattributetablewidgetbase.ui b/src/ui/qgscomposerattributetablewidgetbase.ui index febc17058cc..84c7607f4ec 100755 --- a/src/ui/qgscomposerattributetablewidgetbase.ui +++ b/src/ui/qgscomposerattributetablewidgetbase.ui @@ -45,9 +45,9 @@ 0 - 0 + -195 392 - 918 + 1020 @@ -99,6 +99,16 @@ + + + + Relation + + + + + + @@ -113,36 +123,6 @@ - - - - Margin - - - true - - - mMarginSpinBox - - - - - - - mm - - - - - - - - - - Relation - - - @@ -235,6 +215,87 @@ mMaxNumFeaturesLabel + + + + Appearance + + + composeritem + + + false + + + + + + Cell margins + + + true + + + mMarginSpinBox + + + + + + + mm + + + + + + + Display header + + + + + + + + On first frame + + + + + On all frames + + + + + No header + + + + + + + + Empty tables + + + + + + + + + + Message to display + + + + + + + + + @@ -317,13 +378,6 @@ Table heading - - - - Display - - - @@ -401,25 +455,6 @@ - - - - - On first frame - - - - - On all frames - - - - - No header - - - - diff --git a/tests/src/core/testqgscomposertablev2.cpp b/tests/src/core/testqgscomposertablev2.cpp index 00ec5be8b15..fbc6feeea37 100644 --- a/tests/src/core/testqgscomposertablev2.cpp +++ b/tests/src/core/testqgscomposertablev2.cpp @@ -49,12 +49,10 @@ class TestQgsComposerTableV2: public QObject void attributeTableSetAttributes(); //test subset of attributes in table void attributeTableVisibleOnly(); //test displaying only visible attributes void attributeTableRender(); //test rendering attribute table - void manualColumnWidth(); //test setting manual column widths - + void attributeTableEmpty(); //test empty modes for attribute table void attributeTableExtend(); void attributeTableRepeat(); - void attributeTableAtlasSource(); //test attribute table in atlas feature mode void attributeTableRelationSource(); //test attribute table in relation mode @@ -325,6 +323,33 @@ void TestQgsComposerTableV2::manualColumnWidth() QVERIFY( result ); } +void TestQgsComposerTableV2::attributeTableEmpty() +{ + mComposerAttributeTable->setMaximumNumberOfFeatures( 20 ); + //hide all features from table + mComposerAttributeTable->setFeatureFilter( QString( "1=2" ) ); + mComposerAttributeTable->setFilterFeatures( true ); + + mComposerAttributeTable->setEmptyTableBehaviour( QgsComposerTableV2::HeadersOnly ); + QgsCompositionChecker checker( "composerattributetable_headersonly", mComposition ); + QVERIFY( checker.testComposition( mReport, 0 ) ); + + mComposerAttributeTable->setEmptyTableBehaviour( QgsComposerTableV2::HideTable ); + QgsCompositionChecker checker2( "composerattributetable_hidetable", mComposition ); + QVERIFY( checker2.testComposition( mReport, 0 ) ); + + mComposerAttributeTable->setEmptyTableBehaviour( QgsComposerTableV2::DrawEmptyCells ); + QgsCompositionChecker checker3( "composerattributetable_drawempty", mComposition ); + QVERIFY( checker3.testComposition( mReport, 0 ) ); + + mComposerAttributeTable->setEmptyTableBehaviour( QgsComposerTableV2::ShowMessage ); + mComposerAttributeTable->setEmptyTableMessage( "no rows" ); + QgsCompositionChecker checker4( "composerattributetable_showmessage", mComposition ); + QVERIFY( checker4.testComposition( mReport, 0 ) ); + + mComposerAttributeTable->setFilterFeatures( false ); +} + void TestQgsComposerTableV2::attributeTableExtend() { //test that adding and removing frames automatically does not result in a crash diff --git a/tests/testdata/control_images/expected_composerattributetable_drawempty/expected_composerattributetable_drawempty.png b/tests/testdata/control_images/expected_composerattributetable_drawempty/expected_composerattributetable_drawempty.png new file mode 100644 index 0000000000000000000000000000000000000000..f62b7046692c2dba149cae56598e3399c657814e GIT binary patch literal 16879 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 qH?aKwt7{CkwnoV#&*AfL0q zBeIx*f$uN~Gak=hk;1?rdeGCwF{EP7+dGPWvC$H350CQx6Y#NSaG9JW$;tE7pvkLA z;bo!6I+ZgFhZQ(h9!v^gxtdZgwEF$NozE-TK0Lj?{s;fG)JTx64E1;Iv*kgI4f-t- zK&qibfEh>$2m)Qkz@X&B10)?>Iv9ae2Ztd@Oi&S|Q9{J4T@Jb9b6Jmn=VyYN8~CjD{Xc=$&}DW?_GS ze_?sK`M$cpR^S{{SsD48g@NJQoh<^N|Nj2|@xup$x_^HxL2|!7Jw0ts^| zIJimY^euN#ZhgaSPs?nL5^0umOiSwP?d$&jGR@{=_}A6l-CbBzwCS59&>1T;6`wtO zmS$sX+xz+R=g7$n23P<7{cB-sD++X8#MvzZ#WgiH>(;G1Rc4rq`njxgN@xNAl*k%p literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/expected_composerattributetable_showmessage/expected_composerattributetable_showmessage.png b/tests/testdata/control_images/expected_composerattributetable_showmessage/expected_composerattributetable_showmessage.png new file mode 100644 index 0000000000000000000000000000000000000000..5aed542c1c021f8ad23cfc34d5399be7266234f4 GIT binary patch literal 17392 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 literal 0 HcmV?d00001