Skip to content
Permalink
Browse files

[FEATURE] Allow placing manual column breaks in legends

Adds the option when configuring a legend item to place a column
break before the item, causing it to be placed into a new column

This allows user control over the column content, for cases when
the automatic column generation doesn't result in the desired results
  • Loading branch information
nyalldawson committed May 5, 2020
1 parent 0d9a15b commit 32c17c316e70f50910e89742ed52079fa4402283
@@ -93,6 +93,24 @@ symbol width or height from :py:class:`QgsLegendSettings`.

.. seealso:: :py:func:`userPatchSize`

.. versionadded:: 3.14
%End

virtual void setColumnBreak( bool breakBeforeNode );
%Docstring
Sets whether a forced column break should occur before the node.

.. seealso:: :py:func:`columnBreak`

.. versionadded:: 3.14
%End

virtual bool columnBreak() const;
%Docstring
Returns whether a forced column break should occur before the node.

.. seealso:: :py:func:`setColumnBreak`

.. versionadded:: 3.14
%End

@@ -161,6 +161,24 @@ Caller takes ownership of the returned symbol.

.. seealso:: :py:func:`setLegendNodeCustomSymbol`

.. versionadded:: 3.14
%End

static void setLegendNodeColumnBreak( QgsLayerTreeLayer *nodeLayer, int originalIndex, bool columnBreakBeforeNode );
%Docstring
Sets whether a forced column break should occur before the node.

.. seealso:: :py:func:`legendNodeColumnBreak`

.. versionadded:: 3.14
%End

static bool legendNodeColumnBreak( QgsLayerTreeLayer *nodeLayer, int originalIndex );
%Docstring
Returns whether a forced column break should occur before the node.

.. seealso:: :py:func:`setLegendNodeColumnBreak`

.. versionadded:: 3.14
%End

@@ -109,6 +109,22 @@ class CORE_EXPORT QgsLayerTreeModelLegendNode : public QObject
*/
virtual void setUserPatchSize( QSizeF size ) { mUserSize = size; }

/**
* Sets whether a forced column break should occur before the node.
*
* \see columnBreak()
* \since QGIS 3.14
*/
virtual void setColumnBreak( bool breakBeforeNode ) { mColumnBreakBeforeNode = breakBeforeNode; }

/**
* Returns whether a forced column break should occur before the node.
*
* \see setColumnBreak()
* \since QGIS 3.14
*/
virtual bool columnBreak() const { return mColumnBreakBeforeNode; }

virtual bool isScaleOK( double scale ) const { Q_UNUSED( scale ) return true; }

/**
@@ -261,6 +277,7 @@ class CORE_EXPORT QgsLayerTreeModelLegendNode : public QObject
QString mUserLabel;
QgsLegendPatchShape mPatchShape;
QSizeF mUserSize;
bool mColumnBreakBeforeNode = false;
};

#include "qgslegendsymbolitem.h"
@@ -153,7 +153,7 @@ QSizeF QgsLegendRenderer::paintAndDetermineSize( QgsRenderContext &context )

QList<LegendComponentGroup> componentGroups = createComponentGroupList( rootGroup, mSettings.splitLayer(), context );

setColumns( componentGroups );
const int columnCount = setColumns( componentGroups );

QMap< int, double > maxColumnWidths;
qreal maxEqualColumnWidth = 0;
@@ -171,7 +171,7 @@ QSizeF QgsLegendRenderer::paintAndDetermineSize( QgsRenderContext &context )
maxColumnWidths[ group.column ] = std::max( actualSize.width(), maxColumnWidths.value( group.column, 0 ) );
}

if ( mSettings.columnCount() < 2 )
if ( columnCount == 1 )
{
// single column - use the full available width
maxEqualColumnWidth = std::max( maxEqualColumnWidth, mLegendSize.width() - 2 * mSettings.boxSpace() );
@@ -297,11 +297,14 @@ QList<QgsLegendRenderer::LegendComponentGroup> QgsLegendRenderer::createComponen
subgroups[0].components.prepend( component );
subgroups[0].size.rheight() += component.size.height();
subgroups[0].size.rwidth() = std::max( component.size.width(), subgroups[0].size.width() );
if ( nodeGroup->customProperty( QStringLiteral( "legend/column-break" ) ).toInt() )
subgroups[0].placeColumnBreakBeforeGroup = true;
}
else
{
// no subitems, create new group
LegendComponentGroup group;
group.placeColumnBreakBeforeGroup = nodeGroup->customProperty( QStringLiteral( "legend/column-break" ) ).toInt();
group.components.append( component );
group.size.rwidth() += component.size.width();
group.size.rheight() += component.size.height();
@@ -321,6 +324,7 @@ QList<QgsLegendRenderer::LegendComponentGroup> QgsLegendRenderer::createComponen
QgsLayerTreeLayer *nodeLayer = QgsLayerTree::toLayer( node );

LegendComponentGroup group;
group.placeColumnBreakBeforeGroup = nodeLayer->customProperty( QStringLiteral( "legend/column-break" ) ).toInt();

if ( nodeLegendStyle( nodeLayer ) != QgsLegendStyle::Hidden )
{
@@ -343,14 +347,30 @@ QList<QgsLegendRenderer::LegendComponentGroup> QgsLegendRenderer::createComponen
QList<LegendComponentGroup> layerGroups;
layerGroups.reserve( legendNodes.count() );

bool groupIsLayerGroup = true;

for ( int j = 0; j < legendNodes.count(); j++ )
{
QgsLayerTreeModelLegendNode *legendNode = legendNodes.at( j );

LegendComponent symbolComponent = drawSymbolItem( legendNode, context, ColumnContext(), 0 );

const bool forceBreak = legendNode->columnBreak();

if ( !mSettings.splitLayer() || j == 0 )
{
if ( forceBreak )
{
if ( groupIsLayerGroup )
layerGroups.prepend( group );
else
layerGroups.append( group );

group = LegendComponentGroup();
group.placeColumnBreakBeforeGroup = true;
groupIsLayerGroup = false;
}

// append to layer group
// the width is not correct at this moment, we must align all symbol labels
group.size.rwidth() = std::max( symbolComponent.size.width(), group.size.width() );
@@ -365,14 +385,30 @@ QList<QgsLegendRenderer::LegendComponentGroup> QgsLegendRenderer::createComponen
}
else
{
if ( group.size.height() > 0 )
{
if ( groupIsLayerGroup )
layerGroups.prepend( group );
else
layerGroups.append( group );
group = LegendComponentGroup();
groupIsLayerGroup = false;
}
LegendComponentGroup symbolGroup;
symbolGroup.placeColumnBreakBeforeGroup = forceBreak;
symbolGroup.components.append( symbolComponent );
symbolGroup.size.rwidth() = symbolComponent.size.width();
symbolGroup.size.rheight() = symbolComponent.size.height();
layerGroups.append( symbolGroup );
}
}
layerGroups.prepend( group );
if ( group.size.height() > 0 )
{
if ( groupIsLayerGroup )
layerGroups.prepend( group );
else
layerGroups.append( group );
}
componentGroups.append( layerGroups );
}
}
@@ -381,21 +417,32 @@ QList<QgsLegendRenderer::LegendComponentGroup> QgsLegendRenderer::createComponen
}


void QgsLegendRenderer::setColumns( QList<LegendComponentGroup> &componentGroups )
int QgsLegendRenderer::setColumns( QList<LegendComponentGroup> &componentGroups )
{
if ( mSettings.columnCount() == 0 )
return;

// Divide groups to columns
double totalHeight = 0;
qreal maxGroupHeight = 0;
int forcedColumnBreaks = 0;
bool first = true;
for ( const LegendComponentGroup &group : qgis::as_const( componentGroups ) )
{
totalHeight += spaceAboveGroup( group );
totalHeight += group.size.height();
maxGroupHeight = std::max( group.size.height(), maxGroupHeight );

if ( group.placeColumnBreakBeforeGroup )
forcedColumnBreaks++;

first = false;
}

if ( mSettings.columnCount() == 0 && forcedColumnBreaks == 0 )
return 0;

// the target number of columns allowed is dictated by the number of forced column
// breaks OR the manually set column count (whichever is greater!)
const int targetNumberColumns = std::max( forcedColumnBreaks + 1, mSettings.columnCount() );

// We know height of each group and we have to split them into columns
// minimizing max column height. It is sort of bin packing problem, NP-hard.
// We are using simple heuristic, brute fore appeared to be to slow,
@@ -410,7 +457,7 @@ void QgsLegendRenderer::setColumns( QList<LegendComponentGroup> &componentGroups
for ( int i = 0; i < componentGroups.size(); i++ )
{
// Recalc average height for remaining columns including current
double avgColumnHeight = ( totalHeight - closedColumnsHeight ) / ( mSettings.columnCount() - currentColumn );
double avgColumnHeight = ( totalHeight - closedColumnsHeight ) / ( targetNumberColumns - currentColumn );

LegendComponentGroup group = componentGroups.at( i );
double currentHeight = currentColumnHeight;
@@ -419,16 +466,19 @@ void QgsLegendRenderer::setColumns( QList<LegendComponentGroup> &componentGroups
currentHeight += group.size.height();

bool canCreateNewColumn = ( currentColumnGroupCount > 0 ) // do not leave empty column
&& ( currentColumn < mSettings.columnCount() - 1 ); // must not exceed max number of columns
&& ( currentColumn < targetNumberColumns - 1 ); // must not exceed max number of columns

bool shouldCreateNewColumn = ( currentHeight - avgColumnHeight ) > group.size.height() / 2 // center of current group is over average height
&& currentColumnGroupCount > 0 // do not leave empty column
&& currentHeight > maxGroupHeight // no sense to make smaller columns than max group height
&& currentHeight > maxColumnHeight; // no sense to make smaller columns than max column already created

shouldCreateNewColumn |= group.placeColumnBreakBeforeGroup;
canCreateNewColumn |= group.placeColumnBreakBeforeGroup;

// also should create a new column if the number of items left < number of columns left
// in this case we should spread the remaining items out over the remaining columns
shouldCreateNewColumn |= ( componentGroups.size() - i < mSettings.columnCount() - currentColumn );
shouldCreateNewColumn |= ( componentGroups.size() - i < targetNumberColumns - currentColumn );

if ( canCreateNewColumn && shouldCreateNewColumn )
{
@@ -447,7 +497,7 @@ void QgsLegendRenderer::setColumns( QList<LegendComponentGroup> &componentGroups
maxColumnHeight = std::max( currentColumnHeight, maxColumnHeight );
}

// Align labels of symbols for each layr/column to the same labelXOffset
// Align labels of symbols for each layer/column to the same labelXOffset
QMap<QString, qreal> maxSymbolWidth;
for ( int i = 0; i < componentGroups.size(); i++ )
{
@@ -477,6 +527,7 @@ void QgsLegendRenderer::setColumns( QList<LegendComponentGroup> &componentGroups
}
}
}
return targetNumberColumns;
}

QSizeF QgsLegendRenderer::drawTitle( QgsRenderContext &context, double top, Qt::AlignmentFlag halignment, double legendWidth )
@@ -185,6 +185,12 @@ class CORE_EXPORT QgsLegendRenderer

//! Corresponding column index
int column = 0;

/**
* TRUE if a forced column break should be placed just before the group
*/
bool placeColumnBreakBeforeGroup = false;

};

/**
@@ -213,8 +219,10 @@ class CORE_EXPORT QgsLegendRenderer

/**
* Divides a list of component groups into columns, and sets the column index for each group in the list.
*
* Returns the calculated number of columns.
*/
void setColumns( QList<LegendComponentGroup> &groupList );
int setColumns( QList<LegendComponentGroup> &groupList );

/**
* Returns the calculated padding space required above the given component \a group.
@@ -216,6 +216,19 @@ QgsSymbol *QgsMapLayerLegendUtils::legendNodeCustomSymbol( QgsLayerTreeLayer *no
return QgsSymbolLayerUtils::loadSymbol( elem, rwContext );
}

void QgsMapLayerLegendUtils::setLegendNodeColumnBreak( QgsLayerTreeLayer *nodeLayer, int originalIndex, bool columnBreakBeforeNode )
{
if ( columnBreakBeforeNode )
nodeLayer->setCustomProperty( "legend/column-break-" + QString::number( originalIndex ), QStringLiteral( "1" ) );
else
nodeLayer->removeCustomProperty( "legend/column-break-" + QString::number( originalIndex ) );
}

bool QgsMapLayerLegendUtils::legendNodeColumnBreak( QgsLayerTreeLayer *nodeLayer, int originalIndex )
{
return nodeLayer->customProperty( "legend/column-break-" + QString::number( originalIndex ) ).toInt();
}

void QgsMapLayerLegendUtils::applyLayerNodeProperties( QgsLayerTreeLayer *nodeLayer, QList<QgsLayerTreeModelLegendNode *> &nodes )
{
// handle user labels
@@ -241,6 +254,9 @@ void QgsMapLayerLegendUtils::applyLayerNodeProperties( QgsLayerTreeLayer *nodeLa
legendNode->setUserPatchSize( userSize );
}

if ( legendNodeColumnBreak( nodeLayer, i ) )
legendNode->setColumnBreak( true );

i++;
}

@@ -166,6 +166,22 @@ class CORE_EXPORT QgsMapLayerLegendUtils
*/
static QgsSymbol *legendNodeCustomSymbol( QgsLayerTreeLayer *nodeLayer, int originalIndex ) SIP_FACTORY;

/**
* Sets whether a forced column break should occur before the node.
*
* \see legendNodeColumnBreak()
* \since QGIS 3.14
*/
static void setLegendNodeColumnBreak( QgsLayerTreeLayer *nodeLayer, int originalIndex, bool columnBreakBeforeNode );

/**
* Returns whether a forced column break should occur before the node.
*
* \see setLegendNodeColumnBreak()
* \since QGIS 3.14
*/
static bool legendNodeColumnBreak( QgsLayerTreeLayer *nodeLayer, int originalIndex );

//! update according to layer node's custom properties (order of items, user labels for items)
static void applyLayerNodeProperties( QgsLayerTreeLayer *nodeLayer, QList<QgsLayerTreeModelLegendNode *> &nodes );
};

0 comments on commit 32c17c3

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