Skip to content

Commit

Permalink
[FEATURE][composer] Auto wrapping for text in fixed width columns
Browse files Browse the repository at this point in the history
in attribute tables

Sponsored by City of Uster
  • Loading branch information
nyalldawson committed Aug 18, 2015
1 parent 9bf0295 commit 612df6a
Show file tree
Hide file tree
Showing 9 changed files with 243 additions and 17 deletions.
24 changes: 24 additions & 0 deletions python/core/composer/qgscomposertablev2.sip
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ class QgsComposerTableV2: QgsComposerMultiFrame
ShowMessage /*!< shows preset message instead of table contents*/
};

/** Controls how long strings in the table are handled
*/
enum WrapBehaviour
{
TruncateText = 0, /*!< text which doesn't fit inside the cell is truncated */
WrapText /*!< text which doesn't fit inside the cell is wrapped. Note that this only applies to text in columns with a fixed width. */
};

QgsComposerTableV2( QgsComposition* composition /TransferThis/, bool createUndoCommands );
QgsComposerTableV2();

Expand Down Expand Up @@ -263,6 +271,22 @@ class QgsComposerTableV2: QgsComposerMultiFrame
*/
QColor backgroundColor() const;

/** Sets the wrap behaviour for the table, which controls how text within cells is
* automatically wrapped.
* @param behaviour wrap behaviour
* @see wrapBehaviour
* @note added in QGIS 2.12
*/
void setWrapBehaviour( WrapBehaviour behaviour );

/** Returns the wrap behaviour for the table, which controls how text within cells is
* automatically wrapped.
* @returns current wrap behaviour
* @see setWrapBehaviour
* @note added in QGIS 2.12
*/
WrapBehaviour wrapBehaviour() const;

/** Returns a pointer to the list of QgsComposerTableColumns shown in the table
* @returns pointer to list of columns in table
* @see setColumns
Expand Down
21 changes: 21 additions & 0 deletions src/app/composer/qgscomposerattributetablewidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ QgsComposerAttributeTableWidget::QgsComposerAttributeTableWidget( QgsComposerAtt
mEmptyModeComboBox->addItem( tr( "Hide entire table" ), QgsComposerTableV2::HideTable );
mEmptyModeComboBox->addItem( tr( "Show set message" ), QgsComposerTableV2::ShowMessage );

mWrapBehaviourComboBox->addItem( tr( "Truncate text" ), QgsComposerTableV2::TruncateText );
mWrapBehaviourComboBox->addItem( tr( "Wrap text" ), QgsComposerTableV2::WrapText );

bool atlasEnabled = atlasComposition() && atlasComposition()->enabled();
mSourceComboBox->addItem( tr( "Layer features" ), QgsComposerAttributeTableV2::LayerAttributes );
toggleAtlasSpecificControls( atlasEnabled );
Expand Down Expand Up @@ -519,6 +522,7 @@ void QgsComposerAttributeTableWidget::updateGuiElements()
mEmptyMessageLabel->setEnabled( mComposerTable->emptyTableBehaviour() == QgsComposerTableV2::ShowMessage );
mDrawEmptyCheckBox->setChecked( mComposerTable->showEmptyRows() );
mWrapStringLineEdit->setText( mComposerTable->wrapString() );
mWrapBehaviourComboBox->setCurrentIndex( mWrapBehaviourComboBox->findData( mComposerTable->wrapBehaviour() ) );

mResizeModeComboBox->setCurrentIndex( mResizeModeComboBox->findData( mComposerTable->resizeMode() ) );
mAddFramePushButton->setEnabled( mComposerTable->resizeMode() == QgsComposerMultiFrame::UseExistingFrames );
Expand Down Expand Up @@ -636,6 +640,7 @@ void QgsComposerAttributeTableWidget::blockAllSignals( bool b )
mHideEmptyBgCheckBox->blockSignals( b );
mDrawEmptyCheckBox->blockSignals( b );
mWrapStringLineEdit->blockSignals( b );
mWrapBehaviourComboBox->blockSignals( b );
}

void QgsComposerAttributeTableWidget::setMaximumNumberOfFeatures( int n )
Expand Down Expand Up @@ -1003,6 +1008,22 @@ void QgsComposerAttributeTableWidget::on_mEmptyModeComboBox_currentIndexChanged(
}
}

void QgsComposerAttributeTableWidget::on_mWrapBehaviourComboBox_currentIndexChanged( int index )
{
if ( !mComposerTable )
{
return;
}

QgsComposition* composition = mComposerTable->composition();
if ( composition )
{
composition->beginMultiFrameCommand( mComposerTable, tr( "Change table wrap mode" ) );
mComposerTable->setWrapBehaviour(( QgsComposerTableV2::WrapBehaviour ) mWrapBehaviourComboBox->itemData( index ).toInt() );
composition->endMultiFrameCommand();
}
}

void QgsComposerAttributeTableWidget::on_mDrawEmptyCheckBox_toggled( bool checked )
{
if ( !mComposerTable )
Expand Down
1 change: 1 addition & 0 deletions src/app/composer/qgscomposerattributetablewidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ class QgsComposerAttributeTableWidget: public QgsComposerItemBaseWidget, private
void on_mUniqueOnlyCheckBox_stateChanged( int state );
void on_mEmptyFrameCheckBox_toggled( bool checked );
void on_mHideEmptyBgCheckBox_toggled( bool checked );
void on_mWrapBehaviourComboBox_currentIndexChanged( int index );

/** Inserts a new maximum number of features into the spin box (without the spinbox emitting a signal)*/
void setMaximumNumberOfFeatures( int n );
Expand Down
114 changes: 107 additions & 7 deletions src/core/composer/qgscomposertablev2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ QgsComposerTableV2::QgsComposerTableV2( QgsComposition *composition, bool create
, mGridStrokeWidth( 0.5 )
, mGridColor( Qt::black )
, mBackgroundColor( Qt::white )
, mWrapBehaviour( TruncateText )
{

if ( mComposition )
Expand Down Expand Up @@ -65,6 +66,7 @@ QgsComposerTableV2::QgsComposerTableV2()
, mGridStrokeWidth( 0.5 )
, mGridColor( Qt::black )
, mBackgroundColor( Qt::white )
, mWrapBehaviour( TruncateText )
{

}
Expand All @@ -91,6 +93,7 @@ bool QgsComposerTableV2::writeXML( QDomElement& elem, QDomDocument & doc, bool i
elem.setAttribute( "gridColor", QgsSymbolLayerV2Utils::encodeColor( mGridColor ) );
elem.setAttribute( "showGrid", mShowGrid );
elem.setAttribute( "backgroundColor", QgsSymbolLayerV2Utils::encodeColor( mBackgroundColor ) );
elem.setAttribute( "wrapBehaviour", QString::number(( int )mWrapBehaviour ) );

//columns
QDomElement displayColumnsElem = doc.createElement( "displayColumns" );
Expand Down Expand Up @@ -142,6 +145,7 @@ bool QgsComposerTableV2::readXML( const QDomElement &itemElem, const QDomDocumen
mShowGrid = itemElem.attribute( "showGrid", "1" ).toInt();
mGridColor = QgsSymbolLayerV2Utils::decodeColor( itemElem.attribute( "gridColor", "0,0,0,255" ) );
mBackgroundColor = QgsSymbolLayerV2Utils::decodeColor( itemElem.attribute( "backgroundColor", "255,255,255,0" ) );
mWrapBehaviour = QgsComposerTableV2::WrapBehaviour( itemElem.attribute( "wrapBehaviour", "0" ).toInt() );

//restore column specifications
qDeleteAll( mColumns );
Expand Down Expand Up @@ -459,20 +463,23 @@ void QgsComposerTableV2::render( QPainter *p, const QRectF &, const int frameInd
// currentY = gridSize;
currentX += mCellMargin;

QVariant cellContents = mTableContents.at( row ).at( col );
QString str = cellContents.toString();

Qt::TextFlag textFlag = ( Qt::TextFlag )0;
if (( *columnIt )->width() <= 0 )
if (( *columnIt )->width() <= 0 && mWrapBehaviour == TruncateText )
{
//automatic column width, so we use the Qt::TextDontClip flag when drawing contents, as this works nicer for italicised text
//which may slightly exceed the calculated width
//if column size was manually set then we do apply text clipping, to avoid painting text outside of columns width
textFlag = Qt::TextDontClip;
}
else if ( textRequiresWrapping( str, ( *columnIt )->width(), mContentFont ) )
{
str = wrappedText( str, ( *columnIt )->width(), mContentFont );
}

cell = QRectF( currentX, currentY, mMaxColumnWidthMap[col], rowHeight );

QVariant cellContents = mTableContents.at( row ).at( col );
QString str = cellContents.toString();

QgsComposerUtils::drawText( p, cell, str, mContentFont, mContentFontColor, ( *columnIt )->hAlignment(), ( *columnIt )->vAlignment(), textFlag );

currentX += mMaxColumnWidthMap[ col ];
Expand Down Expand Up @@ -701,6 +708,19 @@ void QgsComposerTableV2::setBackgroundColor( const QColor &color )
emit changed();
}

void QgsComposerTableV2::setWrapBehaviour( QgsComposerTableV2::WrapBehaviour behaviour )
{
if ( behaviour == mWrapBehaviour )
{
return;
}

mWrapBehaviour = behaviour;
recalculateTableSize();

emit changed();
}

void QgsComposerTableV2::setColumns( QgsComposerTableColumns columns )
{
//remove existing columns
Expand Down Expand Up @@ -864,8 +884,15 @@ bool QgsComposerTableV2::calculateMaxRowHeights()
col = 0;
for ( ; colIt != rowIt->constEnd(); ++colIt )
{
//height
heights[ row * cols + col ] = QgsComposerUtils::textHeightMM( mContentFont, ( *colIt ).toString() );
if ( textRequiresWrapping(( *colIt ).toString(), mColumns.at( col )->width(), mContentFont ) )
{
//contents too wide for cell, need to wrap
heights[ row * cols + col ] = QgsComposerUtils::textHeightMM( mContentFont, wrappedText(( *colIt ).toString(), mColumns.at( col )->width(), mContentFont ) );
}
else
{
heights[ row * cols + col ] = QgsComposerUtils::textHeightMM( mContentFont, ( *colIt ).toString() );
}

col++;
}
Expand Down Expand Up @@ -1018,6 +1045,79 @@ void QgsComposerTableV2::drawVerticalGridLines( QPainter *painter, const QMap<in
drawVerticalGridLines( painter, maxWidthMap, 100000, 100000 + numberRows, hasHeader, mergeCells );
}

bool QgsComposerTableV2::textRequiresWrapping( const QString& text, double columnWidth, const QFont &font ) const
{
if ( columnWidth == 0 || mWrapBehaviour != WrapText )
return false;

QStringList multiLineSplit = text.split( "\n" );
double currentTextWidth = 0;
Q_FOREACH ( QString line, multiLineSplit )
{
currentTextWidth = qMax( currentTextWidth, QgsComposerUtils::textWidthMM( font, line ) );
}

return ( currentTextWidth > columnWidth );
}

QString QgsComposerTableV2::wrappedText( const QString &value, double columnWidth, const QFont &font ) const
{
QStringList lines = value.split( "\n" );
QStringList outLines;
Q_FOREACH ( QString line, lines )
{
if ( textRequiresWrapping( line, columnWidth, font ) )
{
//first step is to identify words which must be on their own line (too long to fit)
QStringList words = line.split( " " );
QStringList linesToProcess;
QString wordsInCurrentLine;
Q_FOREACH ( QString word, words )
{
if ( textRequiresWrapping( word, columnWidth, font ) )
{
//too long to fit
if ( !wordsInCurrentLine.isEmpty() )
linesToProcess << wordsInCurrentLine;
wordsInCurrentLine.clear();
linesToProcess << word;
}
else
{
if ( !wordsInCurrentLine.isEmpty() )
wordsInCurrentLine.append( " " );
wordsInCurrentLine.append( word );
}
}
if ( !wordsInCurrentLine.isEmpty() )
linesToProcess << wordsInCurrentLine;

Q_FOREACH ( QString line, linesToProcess )
{
QString remainingText = line;
int lastPos = remainingText.lastIndexOf( " " );
while ( lastPos > -1 )
{
if ( !textRequiresWrapping( remainingText.left( lastPos ), columnWidth, font ) )
{
outLines << remainingText.left( lastPos );
remainingText = remainingText.mid( lastPos + 1 );
lastPos = 0;
}
lastPos = remainingText.lastIndexOf( " ", lastPos - 1 );
}
outLines << remainingText;
}
}
else
{
outLines << line;
}
}

return outLines.join( "\n" );
}

void QgsComposerTableV2::drawVerticalGridLines( QPainter *painter, const QMap<int, double> &maxWidthMap, int firstRow, int lastRow, bool hasHeader, bool mergeCells ) const
{
//vertical lines
Expand Down
32 changes: 32 additions & 0 deletions src/core/composer/qgscomposertablev2.h
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ class CORE_EXPORT QgsComposerTableV2: public QgsComposerMultiFrame
ShowMessage /*!< shows preset message instead of table contents*/
};

/** Controls how long strings in the table are handled
*/
enum WrapBehaviour
{
TruncateText = 0, /*!< text which doesn't fit inside the cell is truncated */
WrapText /*!< text which doesn't fit inside the cell is wrapped. Note that this only applies to text in columns with a fixed width. */
};

QgsComposerTableV2( QgsComposition* composition, bool createUndoCommands );
QgsComposerTableV2();

Expand Down Expand Up @@ -288,6 +296,22 @@ class CORE_EXPORT QgsComposerTableV2: public QgsComposerMultiFrame
*/
QColor backgroundColor() const { return mBackgroundColor; }

/** Sets the wrap behaviour for the table, which controls how text within cells is
* automatically wrapped.
* @param behaviour wrap behaviour
* @see wrapBehaviour
* @note added in QGIS 2.12
*/
void setWrapBehaviour( WrapBehaviour behaviour );

/** Returns the wrap behaviour for the table, which controls how text within cells is
* automatically wrapped.
* @returns current wrap behaviour
* @see setWrapBehaviour
* @note added in QGIS 2.12
*/
WrapBehaviour wrapBehaviour() const { return mWrapBehaviour; }

/** Returns a pointer to the list of QgsComposerTableColumns shown in the table
* @returns pointer to list of columns in table
* @see setColumns
Expand Down Expand Up @@ -398,6 +422,8 @@ class CORE_EXPORT QgsComposerTableV2: public QgsComposerMultiFrame

QSizeF mTableSize;

WrapBehaviour mWrapBehaviour;

/** Calculates the maximum width of text shown in columns.
*/
virtual bool calculateMaxColumnWidths();
Expand Down Expand Up @@ -537,6 +563,12 @@ class CORE_EXPORT QgsComposerTableV2: public QgsComposerMultiFrame
*/
Q_DECL_DEPRECATED void drawVerticalGridLines( QPainter* painter, const QMap<int, double>& maxWidthMap, const int numberRows, const bool hasHeader, const bool mergeCells = false ) const;

private:

bool textRequiresWrapping( const QString& text, double columnWidth , const QFont &font ) const;

QString wrappedText( const QString &value, double columnWidth, const QFont &font ) const;

friend class TestQgsComposerTableV2;
};

Expand Down
Loading

0 comments on commit 612df6a

Please sign in to comment.