Skip to content

Commit 781b869

Browse files
committedMar 8, 2023
[feature][layouts] Support filtering layout legends by multiple linked map items
Instead of limiting layout legend filtering to a single linked map, this change permits legends to be filtered instead by multiple linked maps. It is designed to accomodate the use case where a layout has multiple maps, potentially at different scales and showing different extents, and a single legend is required which includes all symbols visible across all the maps. Sponsored by City of Canning
1 parent e3ffb8d commit 781b869

File tree

10 files changed

+699
-22
lines changed

10 files changed

+699
-22
lines changed
 

‎python/core/auto_generated/layout/qgslayoutitemlegend.sip.in

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,13 +561,37 @@ only drawn if :py:func:`~QgsLayoutItemLegend.drawRasterStroke` is ``True``.
561561
Sets the ``map`` to associate with the legend.
562562

563563
.. seealso:: :py:func:`linkedMap`
564+
565+
.. seealso:: :py:func:`setFilterByMapItems`
564566
%End
565567

566568
QgsLayoutItemMap *linkedMap() const;
567569
%Docstring
568570
Returns the associated map.
569571

570572
.. seealso:: :py:func:`setLinkedMap`
573+
%End
574+
575+
void setFilterByMapItems( const QList< QgsLayoutItemMap * > &maps );
576+
%Docstring
577+
Sets the ``maps`` to use when filtering legend content by map extents.
578+
579+
.. seealso:: :py:func:`filterByMapItems`
580+
581+
.. seealso:: :py:func:`setLinkedMap`
582+
583+
.. versionadded:: 3.32
584+
%End
585+
586+
QList< QgsLayoutItemMap * > filterByMapItems() const;
587+
%Docstring
588+
Returns the \maps to use when filtering legend content by map extents.
589+
590+
.. seealso:: :py:func:`setFilterByMapItems`
591+
592+
.. seealso:: :py:func:`setLinkedMap`
593+
594+
.. versionadded:: 3.32
571595
%End
572596

573597
QString themeName() const;

‎src/core/layout/qgslayoutitemlegend.cpp

Lines changed: 156 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,20 @@ void QgsLayoutItemLegend::finalizeRestoreFromXml()
189189
{
190190
setLinkedMap( qobject_cast< QgsLayoutItemMap * >( mLayout->itemByUuid( mMapUuid, true ) ) );
191191
}
192+
193+
if ( !mFilterByMapUuids.isEmpty() )
194+
{
195+
QList< QgsLayoutItemMap * > maps;
196+
maps.reserve( mFilterByMapUuids.size() );
197+
for ( const QString &uuid : std::as_const( mFilterByMapUuids ) )
198+
{
199+
if ( QgsLayoutItemMap *map = qobject_cast< QgsLayoutItemMap * >( mLayout->itemByUuid( uuid, true ) ) )
200+
{
201+
maps << map;
202+
}
203+
}
204+
setFilterByMapItems( maps );
205+
}
192206
}
193207

194208
void QgsLayoutItemLegend::refresh()
@@ -229,9 +243,6 @@ void QgsLayoutItemLegend::draw( QgsLayoutItemRenderContext &context )
229243
Q_NOWARN_DEPRECATED_POP
230244
}
231245

232-
233-
234-
235246
QgsLegendRenderer legendRenderer( mLegendModel.get(), mSettings );
236247
legendRenderer.setLegendSize( rect().size() );
237248

@@ -617,6 +628,21 @@ bool QgsLayoutItemLegend::writePropertiesToElement( QDomElement &legendElem, QDo
617628
legendElem.setAttribute( QStringLiteral( "map_uuid" ), mMap->uuid() );
618629
}
619630

631+
if ( !mFilterByMapItems.empty() )
632+
{
633+
QDomElement filterByMapsElem = doc.createElement( QStringLiteral( "filterByMaps" ) );
634+
for ( QgsLayoutItemMap *map : mFilterByMapItems )
635+
{
636+
if ( map )
637+
{
638+
QDomElement mapElem = doc.createElement( QStringLiteral( "map" ) );
639+
mapElem.setAttribute( QStringLiteral( "uuid" ), map->uuid() );
640+
filterByMapsElem.appendChild( mapElem );
641+
}
642+
}
643+
legendElem.appendChild( filterByMapsElem );
644+
}
645+
620646
QDomElement legendStyles = doc.createElement( QStringLiteral( "styles" ) );
621647
legendElem.appendChild( legendStyles );
622648

@@ -743,6 +769,26 @@ bool QgsLayoutItemLegend::readPropertiesFromElement( const QDomElement &itemElem
743769
{
744770
mMapUuid = itemElem.attribute( QStringLiteral( "map_uuid" ) );
745771
}
772+
773+
mFilterByMapUuids.clear();
774+
{
775+
const QDomElement filterByMapsElem = itemElem.firstChildElement( QStringLiteral( "filterByMaps" ) );
776+
if ( !filterByMapsElem.isNull() )
777+
{
778+
QDomElement mapsElem = filterByMapsElem.firstChildElement( QStringLiteral( "map" ) );
779+
while ( !mapsElem.isNull() )
780+
{
781+
mFilterByMapUuids << mapsElem.attribute( QStringLiteral( "uuid" ) );
782+
mapsElem = mapsElem.nextSiblingElement( QStringLiteral( "map" ) );
783+
}
784+
}
785+
else if ( !mMapUuid.isEmpty() )
786+
{
787+
// for compatibility with < QGIS 3.32 projects
788+
mFilterByMapUuids << mMapUuid;
789+
}
790+
}
791+
746792
// disconnect current map
747793
setupMapConnections( mMap, false );
748794
mMap = nullptr;
@@ -837,6 +883,42 @@ void QgsLayoutItemLegend::setLinkedMap( QgsLayoutItemMap *map )
837883
updateFilterByMap();
838884
}
839885

886+
void QgsLayoutItemLegend::setFilterByMapItems( const QList<QgsLayoutItemMap *> &maps )
887+
{
888+
if ( filterByMapItems() == maps )
889+
return;
890+
891+
for ( QgsLayoutItemMap *map : std::as_const( mFilterByMapItems ) )
892+
{
893+
setupMapConnections( map, false );
894+
}
895+
896+
mFilterByMapItems.clear();
897+
mFilterByMapItems.reserve( maps.size() );
898+
for ( QgsLayoutItemMap *map : maps )
899+
{
900+
if ( map )
901+
{
902+
mFilterByMapItems.append( map );
903+
setupMapConnections( map, true );
904+
}
905+
}
906+
907+
updateFilterByMap();
908+
}
909+
910+
QList<QgsLayoutItemMap *> QgsLayoutItemLegend::filterByMapItems() const
911+
{
912+
QList<QgsLayoutItemMap *> res;
913+
res.reserve( mFilterByMapItems.size() );
914+
for ( QgsLayoutItemMap *map : mFilterByMapItems )
915+
{
916+
if ( map )
917+
res.append( map );
918+
}
919+
return res;
920+
}
921+
840922
void QgsLayoutItemLegend::invalidateCurrentMap()
841923
{
842924
setLinkedMap( nullptr );
@@ -999,26 +1081,89 @@ void QgsLayoutItemLegend::doUpdateFilterByMap()
9991081

10001082
const bool filterByExpression = QgsLayerTreeUtils::hasLegendFilterExpression( *( mCustomLayerTree ? mCustomLayerTree.get() : mLayout->project()->layerTreeRoot() ) );
10011083

1002-
if ( mMap && ( mLegendFilterByMap || filterByExpression || mInAtlas ) )
1084+
const bool hasValidFilter = filterByExpression
1085+
|| ( mLegendFilterByMap && ( mMap || !mFilterByMapItems.empty() ) )
1086+
|| mInAtlas;
1087+
1088+
if ( hasValidFilter )
10031089
{
10041090
const double dpi = mLayout->renderContext().dpi();
10051091

1006-
const QgsRectangle requestRectangle = mMap->requestedExtent();
1092+
QSet< QgsLayoutItemMap * > linkedFilterMaps;
1093+
if ( mLegendFilterByMap )
1094+
{
1095+
linkedFilterMaps = qgis::listToSet( filterByMapItems() );
1096+
if ( mMap )
1097+
linkedFilterMaps.insert( mMap );
1098+
}
1099+
1100+
QgsMapSettings ms;
1101+
if ( mMap )
1102+
{
1103+
// if a specific linked map has been set, use it for the reference scale and extent
1104+
const QgsRectangle requestRectangle = mMap->requestedExtent();
1105+
QSizeF size( requestRectangle.width(), requestRectangle.height() );
1106+
size *= mLayout->convertFromLayoutUnits( mMap->mapUnitsToLayoutUnits(), Qgis::LayoutUnit::Millimeters ).length() * dpi / 25.4;
1107+
ms = mMap->mapSettings( requestRectangle, size, dpi, true );
1108+
}
1109+
else if ( !linkedFilterMaps.empty() )
1110+
{
1111+
// otherwise just take the first linked filter map
1112+
const QgsRectangle requestRectangle = ( *linkedFilterMaps.constBegin() )->requestedExtent();
1113+
QSizeF size( requestRectangle.width(), requestRectangle.height() );
1114+
size *= mLayout->convertFromLayoutUnits( ( *linkedFilterMaps.constBegin() )->mapUnitsToLayoutUnits(), Qgis::LayoutUnit::Millimeters ).length() * dpi / 25.4;
1115+
ms = ( *linkedFilterMaps.constBegin() )->mapSettings( requestRectangle, size, dpi, true );
1116+
}
10071117

1008-
QSizeF size( requestRectangle.width(), requestRectangle.height() );
1009-
size *= mLayout->convertFromLayoutUnits( mMap->mapUnitsToLayoutUnits(), Qgis::LayoutUnit::Millimeters ).length() * dpi / 25.4;
1118+
QgsGeometry filterGeometry;
1119+
if ( !linkedFilterMaps.empty() )
1120+
{
1121+
QVector< QgsGeometry > filterMapGeometries;
1122+
filterMapGeometries.reserve( linkedFilterMaps.size() );
1123+
for ( QgsLayoutItemMap *map : std::as_const( linkedFilterMaps ) )
1124+
{
1125+
QgsGeometry mapExtent = QgsGeometry::fromQPolygonF( map->visibleExtentPolygon() );
10101126

1011-
const QgsMapSettings ms = mMap->mapSettings( requestRectangle, size, dpi, true );
1127+
//transform back to destination CRS
1128+
const QgsCoordinateTransform mapTransform( map->crs(), ms.destinationCrs(), mLayout->project() );
1129+
try
1130+
{
1131+
mapExtent.transform( mapTransform );
1132+
}
1133+
catch ( QgsCsException &cse )
1134+
{
1135+
continue;
1136+
}
1137+
filterMapGeometries.append( mapExtent );
1138+
}
1139+
filterGeometry = QgsGeometry::unaryUnion( filterMapGeometries );
1140+
}
1141+
else if ( mMap )
1142+
{
1143+
filterGeometry = QgsGeometry::fromQPolygonF( mMap->visibleExtentPolygon() );
1144+
}
10121145

1013-
QgsGeometry filterPolygon;
10141146
if ( mInAtlas )
10151147
{
1016-
filterPolygon = mLayout->reportContext().currentGeometry( mMap->crs() );
1148+
if ( !filterGeometry.isEmpty() )
1149+
filterGeometry = mLayout->reportContext().currentGeometry( ms.destinationCrs() );
1150+
else
1151+
filterGeometry = filterGeometry.intersection( mLayout->reportContext().currentGeometry( ms.destinationCrs() ) );
1152+
}
1153+
1154+
if ( !filterGeometry.isNull() )
1155+
{
1156+
mLegendModel->setLegendFilter( &ms, true, filterGeometry, true );
1157+
}
1158+
else
1159+
{
1160+
mLegendModel->setLegendFilter( &ms, false, QgsGeometry(), true );
10171161
}
1018-
mLegendModel->setLegendFilter( &ms, /* useExtent */ mInAtlas || mLegendFilterByMap, filterPolygon, /* useExpressions */ true );
10191162
}
10201163
else
1164+
{
10211165
mLegendModel->setLegendFilterByMap( nullptr );
1166+
}
10221167

10231168
clearLegendCachedData();
10241169
mForceResize = true;

‎src/core/layout/qgslayoutitemlegend.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,7 @@ class CORE_EXPORT QgsLayoutItemLegend : public QgsLayoutItem
528528
/**
529529
* Sets the \a map to associate with the legend.
530530
* \see linkedMap()
531+
* \see setFilterByMapItems()
531532
*/
532533
void setLinkedMap( QgsLayoutItemMap *map );
533534

@@ -537,6 +538,26 @@ class CORE_EXPORT QgsLayoutItemLegend : public QgsLayoutItem
537538
*/
538539
QgsLayoutItemMap *linkedMap() const { return mMap; }
539540

541+
/**
542+
* Sets the \a maps to use when filtering legend content by map extents.
543+
*
544+
* \see filterByMapItems()
545+
* \see setLinkedMap()
546+
*
547+
* \since QGIS 3.32
548+
*/
549+
void setFilterByMapItems( const QList< QgsLayoutItemMap * > &maps );
550+
551+
/**
552+
* Returns the \maps to use when filtering legend content by map extents.
553+
*
554+
* \see setFilterByMapItems()
555+
* \see setLinkedMap()
556+
*
557+
* \since QGIS 3.32
558+
*/
559+
QList< QgsLayoutItemMap * > filterByMapItems() const;
560+
540561
/**
541562
* Returns the name of the theme currently linked to the legend.
542563
*
@@ -622,6 +643,9 @@ class CORE_EXPORT QgsLayoutItemLegend : public QgsLayoutItem
622643
QString mMapUuid;
623644
QgsLayoutItemMap *mMap = nullptr;
624645

646+
QList< QString > mFilterByMapUuids;
647+
QList< QPointer< QgsLayoutItemMap >> mFilterByMapItems;
648+
625649
bool mLegendFilterByMap = false;
626650
bool mLegendFilterByExpression = false;
627651

‎src/gui/layout/qgslayoutlegendwidget.cpp

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
#include "qgsexpressioncontextutils.h"
4141
#include "qgscolorramplegendnodewidget.h"
4242
#include "qgssymbol.h"
43+
#include "qgslayoutundostack.h"
4344

4445
#include <QMenu>
4546
#include <QMessageBox>
@@ -121,6 +122,15 @@ QgsLayoutLegendWidget::QgsLayoutLegendWidget( QgsLayoutItemLegend *legend, QgsMa
121122
connect( mAddGroupToolButton, &QToolButton::clicked, this, &QgsLayoutLegendWidget::mAddGroupToolButton_clicked );
122123
connect( mFilterLegendByAtlasCheckBox, &QCheckBox::toggled, this, &QgsLayoutLegendWidget::mFilterLegendByAtlasCheckBox_toggled );
123124
connect( mItemTreeView, &QgsLayerTreeView::doubleClicked, this, &QgsLayoutLegendWidget::mItemTreeView_doubleClicked );
125+
126+
connect( mFilterByMapCheckBox, &QCheckBox::toggled, mButtonLinkedMaps, &QWidget::setEnabled );
127+
mButtonLinkedMaps->setEnabled( false );
128+
connect( mButtonLinkedMaps, &QToolButton::clicked, this, [ = ]
129+
{
130+
mMapFilteringWidget = new QgsLayoutLegendMapFilteringWidget( mLegend );
131+
openPanel( mMapFilteringWidget );
132+
} );
133+
124134
setPanelTitle( tr( "Legend Properties" ) );
125135

126136
mTitleFontButton->setMode( QgsFontButton::ModeTextRenderer );
@@ -249,6 +259,7 @@ void QgsLayoutLegendWidget::setGuiElements()
249259
whileBlocking( mItemAlignCombo )->setCurrentAlignment( mLegend->style( QgsLegendStyle::SymbolLabel ).alignment() );
250260
whileBlocking( mArrangementCombo )->setCurrentAlignment( mLegend->symbolAlignment() );
251261
mFilterByMapCheckBox->setChecked( mLegend->legendFilterByMapEnabled() );
262+
mButtonLinkedMaps->setEnabled( mLegend->legendFilterByMapEnabled() );
252263
mColumnCountSpinBox->setValue( mLegend->columnCount() );
253264
mSplitLayerCheckBox->setChecked( mLegend->splitLayer() );
254265
mEqualColumnWidthCheckBox->setChecked( mLegend->equalColumnWidth() );
@@ -1255,6 +1266,9 @@ bool QgsLayoutLegendWidget::setNewItem( QgsLayoutItem *item )
12551266
mLegend = qobject_cast< QgsLayoutItemLegend * >( item );
12561267
mItemPropertiesWidget->setItem( mLegend );
12571268

1269+
if ( mMapFilteringWidget )
1270+
mMapFilteringWidget->setItem( mLegend );
1271+
12581272
if ( mLegend )
12591273
{
12601274
mItemTreeView->setModel( mLegend->model() );
@@ -1886,4 +1900,204 @@ void QgsLayoutLegendNodeWidget::columnSplitChanged()
18861900
mLegend->endCommand();
18871901
}
18881902

1903+
//
1904+
// QgsLayoutLegendMapFilteringWidget
1905+
//
1906+
1907+
QgsLayoutLegendMapFilteringWidget::QgsLayoutLegendMapFilteringWidget( QgsLayoutItemLegend *legend )
1908+
: QgsLayoutItemBaseWidget( nullptr, legend )
1909+
, mLegendItem( legend )
1910+
{
1911+
setupUi( this );
1912+
setPanelTitle( tr( "Legend Filtering" ) );
1913+
1914+
setNewItem( legend );
1915+
}
1916+
1917+
bool QgsLayoutLegendMapFilteringWidget::setNewItem( QgsLayoutItem *item )
1918+
{
1919+
if ( item->type() != QgsLayoutItemRegistry::LayoutLegend )
1920+
return false;
1921+
1922+
if ( mLegendItem )
1923+
{
1924+
disconnect( mLegendItem, &QgsLayoutObject::changed, this, &QgsLayoutLegendMapFilteringWidget::updateGuiElements );
1925+
}
1926+
1927+
mLegendItem = qobject_cast< QgsLayoutItemLegend * >( item );
1928+
1929+
if ( mLegendItem )
1930+
{
1931+
connect( mLegendItem, &QgsLayoutObject::changed, this, &QgsLayoutLegendMapFilteringWidget::updateGuiElements );
1932+
}
1933+
1934+
updateGuiElements();
1935+
1936+
return true;
1937+
}
1938+
1939+
void QgsLayoutLegendMapFilteringWidget::updateGuiElements()
1940+
{
1941+
if ( mBlockUpdates )
1942+
return;
1943+
1944+
mBlockUpdates = true;
1945+
1946+
if ( mFilterMapItemsListView->model() )
1947+
{
1948+
QAbstractItemModel *oldModel = mFilterMapItemsListView->model();
1949+
mFilterMapItemsListView->setModel( nullptr );
1950+
oldModel->deleteLater();
1951+
}
1952+
1953+
QgsLayoutLegendMapFilteringModel *model = new QgsLayoutLegendMapFilteringModel( mLegendItem, mLegendItem->layout()->itemsModel(), mFilterMapItemsListView );
1954+
mFilterMapItemsListView->setModel( model );
1955+
1956+
mBlockUpdates = false;
1957+
}
1958+
1959+
//
1960+
// QgsLayoutLegendMapFilteringModel
1961+
//
1962+
1963+
QgsLayoutLegendMapFilteringModel::QgsLayoutLegendMapFilteringModel( QgsLayoutItemLegend *legend, QgsLayoutModel *layoutModel, QObject *parent )
1964+
: QSortFilterProxyModel( parent )
1965+
, mLayoutModel( layoutModel )
1966+
, mLegendItem( legend )
1967+
{
1968+
setSourceModel( layoutModel );
1969+
}
1970+
1971+
int QgsLayoutLegendMapFilteringModel::columnCount( const QModelIndex & ) const
1972+
{
1973+
return 1;
1974+
}
1975+
1976+
QVariant QgsLayoutLegendMapFilteringModel::data( const QModelIndex &i, int role ) const
1977+
{
1978+
if ( !i.isValid() )
1979+
return QVariant();
1980+
1981+
if ( i.column() != 0 )
1982+
return QVariant();
1983+
1984+
const QModelIndex sourceIndex = mapToSource( index( i.row(), QgsLayoutModel::ItemId, i.parent() ) );
1985+
1986+
QgsLayoutItemMap *mapItem = qobject_cast< QgsLayoutItemMap * >( mLayoutModel->itemFromIndex( mapToSource( i ) ) );
1987+
if ( !mapItem )
1988+
{
1989+
return QVariant();
1990+
}
1991+
1992+
switch ( role )
1993+
{
1994+
case Qt::CheckStateRole:
1995+
switch ( i.column() )
1996+
{
1997+
case 0:
1998+
{
1999+
if ( !mLegendItem )
2000+
return Qt::Unchecked;
2001+
2002+
return mLegendItem->filterByMapItems().contains( mapItem ) ? Qt::Checked : Qt::Unchecked;
2003+
}
2004+
2005+
default:
2006+
return QVariant();
2007+
}
2008+
2009+
default:
2010+
return mLayoutModel->data( sourceIndex, role );
2011+
}
2012+
}
2013+
2014+
bool QgsLayoutLegendMapFilteringModel::setData( const QModelIndex &index, const QVariant &value, int role )
2015+
{
2016+
Q_UNUSED( role )
2017+
2018+
if ( !index.isValid() )
2019+
return false;
2020+
2021+
QgsLayoutItemMap *mapItem = qobject_cast< QgsLayoutItemMap * >( mLayoutModel->itemFromIndex( mapToSource( index ) ) );
2022+
if ( !mapItem || !mLegendItem )
2023+
{
2024+
return false;
2025+
}
2026+
2027+
mLegendItem->layout()->undoStack()->beginCommand( mLegendItem, tr( "Change Legend Linked Maps" ) );
2028+
2029+
QList< QgsLayoutItemMap * > linkedMaps = mLegendItem->filterByMapItems();
2030+
if ( value.toBool() )
2031+
{
2032+
if ( !linkedMaps.contains( mapItem ) )
2033+
{
2034+
linkedMaps.append( mapItem );
2035+
mLegendItem->setFilterByMapItems( linkedMaps );
2036+
}
2037+
}
2038+
else
2039+
{
2040+
linkedMaps.removeAll( mapItem );
2041+
mLegendItem->setFilterByMapItems( linkedMaps );
2042+
}
2043+
emit dataChanged( index, index, QVector<int>() << role );
2044+
2045+
mLegendItem->layout()->undoStack()->endCommand();
2046+
mLegendItem->invalidateCache();
2047+
2048+
return true;
2049+
}
2050+
2051+
Qt::ItemFlags QgsLayoutLegendMapFilteringModel::flags( const QModelIndex &index ) const
2052+
{
2053+
Qt::ItemFlags flags = QAbstractItemModel::flags( index );
2054+
2055+
if ( ! index.isValid() )
2056+
{
2057+
return flags ;
2058+
}
2059+
2060+
QgsLayoutItemMap *mapItem = qobject_cast< QgsLayoutItemMap * >( mLayoutModel->itemFromIndex( mapToSource( index ) ) );
2061+
const bool isMainLinkedMapItem = mLegendItem ? ( mLegendItem->linkedMap() == mapItem ) : false;
2062+
2063+
// the main linked map item will always be considered checked in this panel.
2064+
// otherwise we have a potentially confusing user situation where they have selected a main linked map for their legend
2065+
// and enabled the filter by map option, but the filtering isn't applying to that main linked map (ie. things don't work
2066+
// as they did in < 3.32)
2067+
if ( !isMainLinkedMapItem )
2068+
{
2069+
flags |= Qt::ItemIsEnabled;
2070+
}
2071+
else
2072+
{
2073+
flags &= ~Qt::ItemIsEnabled;
2074+
}
2075+
2076+
switch ( index.column() )
2077+
{
2078+
case 0:
2079+
if ( !isMainLinkedMapItem )
2080+
return flags | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable;
2081+
else
2082+
return flags | Qt::ItemIsSelectable;
2083+
2084+
default:
2085+
return flags | Qt::ItemIsSelectable;
2086+
}
2087+
}
2088+
2089+
bool QgsLayoutLegendMapFilteringModel::filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const
2090+
{
2091+
QgsLayoutItem *item = mLayoutModel->itemFromIndex( mLayoutModel->index( source_row, 0, source_parent ) );
2092+
if ( !item || item->type() != QgsLayoutItemRegistry::ItemType::LayoutMap )
2093+
{
2094+
return false;
2095+
}
2096+
2097+
return true;
2098+
}
2099+
2100+
18892101
///@endcond
2102+
2103+

‎src/gui/layout/qgslayoutlegendwidget.h

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,14 @@
2323

2424
#include "qgis_gui.h"
2525
#include "ui_qgslayoutlegendwidgetbase.h"
26+
#include "ui_qgslayoutlegendmapfilteringwidgetbase.h"
2627
#include "qgslayoutitemwidget.h"
2728
#include "qgslayoutitemlegend.h"
2829
#include <QWidget>
2930
#include <QItemDelegate>
3031

32+
class QgsLayoutLegendMapFilteringWidget;
33+
3134
///@cond PRIVATE
3235

3336
/**
@@ -150,6 +153,8 @@ class GUI_EXPORT QgsLayoutLegendWidget: public QgsLayoutItemBaseWidget, public Q
150153
QPointer< QgsLayoutItemLegend > mLegend;
151154
QgsMapCanvas *mMapCanvas = nullptr;
152155
QgsLayoutItemPropertiesWidget *mItemPropertiesWidget = nullptr;
156+
157+
QPointer< QgsLayoutLegendMapFilteringWidget > mMapFilteringWidget;
153158
};
154159

155160
/**
@@ -213,6 +218,63 @@ class GUI_EXPORT QgsLayoutLegendNodeWidget: public QgsPanelWidget, private Ui::Q
213218

214219
};
215220

221+
222+
/**
223+
* \ingroup gui
224+
* \brief Model for legend linked map items
225+
*
226+
* \note This class is not a part of public API
227+
* \since QGIS 3.32
228+
*/
229+
class GUI_EXPORT QgsLayoutLegendMapFilteringModel : public QSortFilterProxyModel
230+
{
231+
Q_OBJECT
232+
233+
public:
234+
//! constructor
235+
explicit QgsLayoutLegendMapFilteringModel( QgsLayoutItemLegend *legend, QgsLayoutModel *layoutModel, QObject *parent = nullptr );
236+
237+
int columnCount( const QModelIndex &parent = QModelIndex() ) const override;
238+
QVariant data( const QModelIndex &index, int role ) const override;
239+
bool setData( const QModelIndex &index, const QVariant &value, int role ) override;
240+
Qt::ItemFlags flags( const QModelIndex &index ) const override;
241+
242+
protected:
243+
244+
bool filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const override;
245+
246+
private:
247+
QgsLayoutModel *mLayoutModel = nullptr;
248+
QPointer< QgsLayoutItemLegend > mLegendItem;
249+
250+
};
251+
252+
/**
253+
* \ingroup gui
254+
* \brief Allows configuration of layout legend map filtering settings.
255+
*
256+
* \note This class is not a part of public API
257+
* \since QGIS 3.32
258+
*/
259+
class GUI_EXPORT QgsLayoutLegendMapFilteringWidget: public QgsLayoutItemBaseWidget, private Ui::QgsLayoutLegendMapFilteringWidgetBase
260+
{
261+
Q_OBJECT
262+
263+
public:
264+
//! constructor
265+
explicit QgsLayoutLegendMapFilteringWidget( QgsLayoutItemLegend *legend );
266+
267+
protected:
268+
bool setNewItem( QgsLayoutItem *item ) final;
269+
270+
private slots:
271+
void updateGuiElements();
272+
273+
private:
274+
QPointer< QgsLayoutItemLegend > mLegendItem;
275+
bool mBlockUpdates = false;
276+
};
277+
216278
///@endcond
217279

218280
#endif //QGSLAYOUTLEGENDWIDGET_H

‎src/gui/layout/qgslayoutmapwidget.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ class GUI_EXPORT QgsLayoutMapLabelingWidget: public QgsLayoutItemBaseWidget, pri
235235
explicit QgsLayoutMapLabelingWidget( QgsLayoutItemMap *map );
236236

237237
protected:
238-
bool setNewItem( QgsLayoutItem *item ) override;
238+
bool setNewItem( QgsLayoutItem *item ) final;
239239

240240
private slots:
241241
void updateGuiElements();
@@ -266,7 +266,7 @@ class GUI_EXPORT QgsLayoutMapClippingWidget: public QgsLayoutItemBaseWidget, pri
266266
void setReportTypeString( const QString &string ) override;
267267

268268
protected:
269-
bool setNewItem( QgsLayoutItem *item ) override;
269+
bool setNewItem( QgsLayoutItem *item ) final;
270270

271271
private slots:
272272
void updateGuiElements();
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ui version="4.0">
3+
<class>QgsLayoutLegendMapFilteringWidgetBase</class>
4+
<widget class="QWidget" name="QgsLayoutLegendMapFilteringWidgetBase">
5+
<property name="geometry">
6+
<rect>
7+
<x>0</x>
8+
<y>0</y>
9+
<width>318</width>
10+
<height>619</height>
11+
</rect>
12+
</property>
13+
<property name="sizePolicy">
14+
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
15+
<horstretch>0</horstretch>
16+
<verstretch>0</verstretch>
17+
</sizepolicy>
18+
</property>
19+
<property name="windowTitle">
20+
<string>Legend Filtering Options</string>
21+
</property>
22+
<layout class="QVBoxLayout" name="verticalLayout">
23+
<property name="leftMargin">
24+
<number>0</number>
25+
</property>
26+
<property name="topMargin">
27+
<number>0</number>
28+
</property>
29+
<property name="rightMargin">
30+
<number>0</number>
31+
</property>
32+
<property name="bottomMargin">
33+
<number>0</number>
34+
</property>
35+
<item>
36+
<widget class="QGroupBox" name="groupBox">
37+
<property name="title">
38+
<string>Filter Legend by Map</string>
39+
</property>
40+
<layout class="QVBoxLayout" name="verticalLayout_2">
41+
<item>
42+
<widget class="QLabel" name="label_9">
43+
<property name="text">
44+
<string>Only show legend items visible in the selected maps</string>
45+
</property>
46+
<property name="wordWrap">
47+
<bool>true</bool>
48+
</property>
49+
</widget>
50+
</item>
51+
<item>
52+
<widget class="QListView" name="mFilterMapItemsListView"/>
53+
</item>
54+
</layout>
55+
</widget>
56+
</item>
57+
</layout>
58+
</widget>
59+
<layoutdefault spacing="6" margin="11"/>
60+
<resources/>
61+
<connections/>
62+
</ui>

‎src/ui/layout/qgslayoutlegendwidgetbase.ui

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@
6363
<property name="geometry">
6464
<rect>
6565
<x>0</x>
66-
<y>-1282</y>
66+
<y>0</y>
6767
<width>359</width>
68-
<height>2253</height>
68+
<height>2256</height>
6969
</rect>
7070
</property>
7171
<layout class="QVBoxLayout" name="mainLayout">
@@ -432,14 +432,28 @@
432432
</layout>
433433
</item>
434434
<item>
435-
<widget class="QCheckBox" name="mFilterByMapCheckBox">
436-
<property name="toolTip">
437-
<string>Filter out legend elements that lie outside the linked map item.</string>
438-
</property>
439-
<property name="text">
440-
<string>Only show items inside linked map</string>
435+
<layout class="QHBoxLayout" name="horizontalLayout_3">
436+
<property name="topMargin">
437+
<number>0</number>
441438
</property>
442-
</widget>
439+
<item>
440+
<widget class="QCheckBox" name="mFilterByMapCheckBox">
441+
<property name="toolTip">
442+
<string>Filter out legend elements that lie outside the linked map item.</string>
443+
</property>
444+
<property name="text">
445+
<string>Only show items inside linked maps</string>
446+
</property>
447+
</widget>
448+
</item>
449+
<item>
450+
<widget class="QToolButton" name="mButtonLinkedMaps">
451+
<property name="text">
452+
<string>...</string>
453+
</property>
454+
</widget>
455+
</item>
456+
</layout>
443457
</item>
444458
<item>
445459
<widget class="QCheckBox" name="mFilterLegendByAtlasCheckBox">
@@ -1388,6 +1402,7 @@
13881402
<tabstop>mCountToolButton</tabstop>
13891403
<tabstop>mExpressionFilterButton</tabstop>
13901404
<tabstop>mFilterByMapCheckBox</tabstop>
1405+
<tabstop>mButtonLinkedMaps</tabstop>
13911406
<tabstop>mFilterLegendByAtlasCheckBox</tabstop>
13921407
<tabstop>mFontsColGroupBox</tabstop>
13931408
<tabstop>mTitleFontButton</tabstop>
@@ -1426,6 +1441,9 @@
14261441
<tabstop>mIconLabelSpaceSpinBox</tabstop>
14271442
<tabstop>mBoxSpaceSpinBox</tabstop>
14281443
<tabstop>mColumnSpaceSpinBox</tabstop>
1444+
<tabstop>mGroupIndentSpinBox</tabstop>
1445+
<tabstop>mSubgroupIndentSpinBox</tabstop>
1446+
<tabstop>mGroupSpaceSpinBox</tabstop>
14291447
</tabstops>
14301448
<resources>
14311449
<include location="../../../images/images.qrc"/>

‎tests/src/python/test_qgslayoutlegend.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
from qgis.PyQt.QtCore import QDir, QRectF
1616
from qgis.PyQt.QtGui import QColor
17+
from qgis.PyQt.QtXml import QDomDocument
18+
1719
from qgis.core import (
1820
QgsCategorizedSymbolRenderer,
1921
QgsCoordinateReferenceSystem,
@@ -42,6 +44,7 @@
4244
QgsRuleBasedRenderer,
4345
QgsSingleSymbolRenderer,
4446
QgsVectorLayer,
47+
QgsReadWriteContext
4548
)
4649
from qgis.testing import start_app, unittest
4750

@@ -895,6 +898,131 @@ def test_rulebased_child_filter(self):
895898
TestQgsLayoutItemLegend.report += checker.report()
896899
self.assertTrue(result, message)
897900

901+
def test_filter_by_map_items(self):
902+
p = QgsProject()
903+
904+
layout = QgsLayout(p)
905+
layout.initializeDefaults()
906+
907+
map1 = QgsLayoutItemMap(layout)
908+
map1.setId('map 1')
909+
layout.addLayoutItem(map1)
910+
911+
map2 = QgsLayoutItemMap(layout)
912+
map2.setId('map 2')
913+
layout.addLayoutItem(map2)
914+
915+
map3 = QgsLayoutItemMap(layout)
916+
map3.setId('map 3')
917+
layout.addLayoutItem(map3)
918+
919+
legend = QgsLayoutItemLegend(layout)
920+
layout.addLayoutItem(legend)
921+
self.assertFalse(legend.filterByMapItems())
922+
923+
legend.setFilterByMapItems([map1, map3])
924+
self.assertEqual(legend.filterByMapItems(), [map1, map3])
925+
926+
# test restoring from xml
927+
doc = QDomDocument("testdoc")
928+
elem = layout.writeXml(doc, QgsReadWriteContext())
929+
930+
l2 = QgsLayout(p)
931+
self.assertTrue(l2.readXml(elem, doc, QgsReadWriteContext()))
932+
map1_restore = [i for i in l2.items() if isinstance(i, QgsLayoutItemMap) and i.id() == 'map 1'][0]
933+
map3_restore = [i for i in l2.items() if isinstance(i, QgsLayoutItemMap) and i.id() == 'map 3'][0]
934+
legend_restore = [i for i in l2.items() if isinstance(i, QgsLayoutItemLegend)][0]
935+
936+
self.assertEqual(legend_restore.filterByMapItems(), [map1_restore, map3_restore])
937+
938+
def test_filter_by_map_content_rendering(self):
939+
point_path = os.path.join(TEST_DATA_DIR, 'points.shp')
940+
point_layer = QgsVectorLayer(point_path, 'points', 'ogr')
941+
942+
root_rule = QgsRuleBasedRenderer.Rule(None)
943+
marker_symbol = QgsMarkerSymbol.createSimple(
944+
{'color': '#ff0000', 'outline_style': 'no', 'size': '8'})
945+
946+
less_than_two_rule = QgsRuleBasedRenderer.Rule(marker_symbol,
947+
filterExp='"Importance" <=2',
948+
label='lessthantwo')
949+
root_rule.appendChild(less_than_two_rule)
950+
951+
else_rule = QgsRuleBasedRenderer.Rule(None, elseRule=True)
952+
953+
marker_symbol = QgsMarkerSymbol.createSimple(
954+
{'color': '#00ffff', 'outline_style': 'no', 'size': '4'})
955+
one_rule = QgsRuleBasedRenderer.Rule(marker_symbol,
956+
filterExp='"Pilots" = 1',
957+
label='1')
958+
else_rule.appendChild(one_rule)
959+
marker_symbol = QgsMarkerSymbol.createSimple(
960+
{'color': '#ff8888', 'outline_style': 'no', 'size': '4'})
961+
two_rule = QgsRuleBasedRenderer.Rule(marker_symbol,
962+
filterExp='"Pilots" = 2',
963+
label='2')
964+
else_rule.appendChild(two_rule)
965+
marker_symbol = QgsMarkerSymbol.createSimple(
966+
{'color': '#8888ff', 'outline_style': 'no', 'size': '4'})
967+
three_rule = QgsRuleBasedRenderer.Rule(marker_symbol,
968+
filterExp='"Pilots" = 3',
969+
label='3')
970+
else_rule.appendChild(three_rule)
971+
972+
root_rule.appendChild(else_rule)
973+
974+
renderer = QgsRuleBasedRenderer(root_rule)
975+
point_layer.setRenderer(renderer)
976+
p = QgsProject()
977+
p.addMapLayer(point_layer)
978+
979+
layout = QgsLayout(p)
980+
layout.initializeDefaults()
981+
982+
map = QgsLayoutItemMap(layout)
983+
map.attemptSetSceneRect(QRectF(19, 17, 100, 165))
984+
map.setFrameEnabled(True)
985+
map.setCrs(QgsCoordinateReferenceSystem('EPSG:4326'))
986+
map.setLayers([point_layer])
987+
map.zoomToExtent(QgsRectangle(-120, 14, -100, 18))
988+
map.setMapRotation(45)
989+
990+
layout.addLayoutItem(map)
991+
992+
map2 = QgsLayoutItemMap(layout)
993+
map2.attemptSetSceneRect(QRectF(150, 117, 100, 165))
994+
map2.setFrameEnabled(True)
995+
map2.setCrs(QgsCoordinateReferenceSystem('EPSG:3857'))
996+
map2.setLayers([point_layer])
997+
map2.setExtent(QgsRectangle(-12309930, 3091263, -11329181, 3977074))
998+
999+
layout.addLayoutItem(map2)
1000+
1001+
legend = QgsLayoutItemLegend(layout)
1002+
legend.setLegendFilterByMapEnabled(True)
1003+
legend.setFilterByMapItems([map, map2])
1004+
layout.addLayoutItem(legend)
1005+
legend.setTitle("Legend")
1006+
legend.attemptSetSceneRect(QRectF(220, 20, 20, 20))
1007+
legend.setFrameEnabled(True)
1008+
legend.setFrameStrokeWidth(QgsLayoutMeasurement(2))
1009+
legend.setBackgroundColor(QColor(200, 200, 200))
1010+
legend.setTitle('')
1011+
1012+
legend.setStyleFont(QgsLegendStyle.Title, QgsFontUtils.getStandardTestFont('Bold', 16))
1013+
legend.setStyleFont(QgsLegendStyle.Group, QgsFontUtils.getStandardTestFont('Bold', 16))
1014+
legend.setStyleFont(QgsLegendStyle.Subgroup, QgsFontUtils.getStandardTestFont('Bold', 16))
1015+
legend.setStyleFont(QgsLegendStyle.Symbol, QgsFontUtils.getStandardTestFont('Bold', 16))
1016+
legend.setStyleFont(QgsLegendStyle.SymbolLabel,
1017+
QgsFontUtils.getStandardTestFont('Bold', 16))
1018+
1019+
checker = QgsLayoutChecker(
1020+
'legend_multiple_filter_maps', layout)
1021+
checker.setControlPathPrefix("composer_legend")
1022+
result, message = checker.testLayout()
1023+
TestQgsLayoutItemLegend.report += checker.report()
1024+
self.assertTrue(result, message)
1025+
8981026

8991027
if __name__ == '__main__':
9001028
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.