From d48a0e62f1e4e453c4aea567ff507288dcfdf056 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 20 May 2024 09:18:15 +1000 Subject: [PATCH] [feature][layouts] Add option to set geopdf group name for items This new setting, located in the layout item "Rendering" section, allows users to set an optional "group name" for use in GeoPDF exports. When set, a matching layer tree group will be created in the exported GeoPDF and the item will only be visible when this group is checked. This allows content to be selectively displayed as a group by viewers of the GeoPDF. Eg, it can allow extra layout content such as descriptive labels or legends to only be shown when layers from the group are visible, making GeoPDF export much more flexible. --- .../core/auto_additions/qgslayoutitem.py | 1 + .../layout/qgslayoutitem.sip.in | 41 ++- .../layout/qgslayoutitem.sip.in | 41 ++- src/core/layout/qgslayoutexporter.cpp | 20 +- src/core/layout/qgslayoutitem.cpp | 12 + src/core/layout/qgslayoutitem.h | 45 +++- src/gui/layout/qgslayoutitemwidget.cpp | 82 +++--- src/gui/layout/qgslayoutitemwidget.h | 1 + src/ui/layout/qgslayoutitemwidgetbase.ui | 82 +++--- tests/src/core/testqgslayoutexporter.cpp | 249 ++++++++++++++++++ tests/src/core/testqgslayoutitem.cpp | 15 ++ 11 files changed, 519 insertions(+), 70 deletions(-) diff --git a/python/PyQt6/core/auto_additions/qgslayoutitem.py b/python/PyQt6/core/auto_additions/qgslayoutitem.py index e36661cfa7b2..e742f5e31544 100644 --- a/python/PyQt6/core/auto_additions/qgslayoutitem.py +++ b/python/PyQt6/core/auto_additions/qgslayoutitem.py @@ -21,6 +21,7 @@ QgsLayoutItem.UndoMarginRight = QgsLayoutItem.UndoCommand.UndoMarginRight QgsLayoutItem.UndoSetId = QgsLayoutItem.UndoCommand.UndoSetId QgsLayoutItem.UndoRotation = QgsLayoutItem.UndoCommand.UndoRotation +QgsLayoutItem.UndoExportLayerName = QgsLayoutItem.UndoCommand.UndoExportLayerName QgsLayoutItem.UndoShapeStyle = QgsLayoutItem.UndoCommand.UndoShapeStyle QgsLayoutItem.UndoShapeCornerRadius = QgsLayoutItem.UndoCommand.UndoShapeCornerRadius QgsLayoutItem.UndoNodeMove = QgsLayoutItem.UndoCommand.UndoNodeMove diff --git a/python/PyQt6/core/auto_generated/layout/qgslayoutitem.sip.in b/python/PyQt6/core/auto_generated/layout/qgslayoutitem.sip.in index cba835dda818..be0f9d5dfc35 100644 --- a/python/PyQt6/core/auto_generated/layout/qgslayoutitem.sip.in +++ b/python/PyQt6/core/auto_generated/layout/qgslayoutitem.sip.in @@ -182,6 +182,7 @@ Base class for graphical items within a :py:class:`QgsLayout`. UndoMarginRight, UndoSetId, UndoRotation, + UndoExportLayerName, UndoShapeStyle, UndoShapeCornerRadius, UndoNodeMove, @@ -432,13 +433,49 @@ Sets the item's parent ``group``. virtual ExportLayerBehavior exportLayerBehavior() const; %Docstring -Returns the behavior of this item during exporting to layered exports (e.g. SVG). +Returns the behavior of this item during exporting to layered exports (e.g. SVG or GeoPDF). .. seealso:: :py:func:`numberExportLayers` .. seealso:: :py:func:`exportLayerDetails` +.. seealso:: :py:func:`exportLayerName` + .. versionadded:: 3.10 +%End + + QString exportLayerName() const; +%Docstring +Returns the name for this item during exporting to layered exports (e.g. SVG or GeoPDF). + +By default this is an empty string, which indicates that the item does not need to be placed in any specific +layer and will automatically be grouped with other items where possible. + +If the layer name is non-empty, then the item will be placed in a group with the corresponding name +during layered exports. + +.. seealso:: :py:func:`setExportLayerName` + +.. seealso:: :py:func:`exportLayerBehavior` + +.. versionadded:: 3.40 +%End + + void setExportLayerName( const QString &name ); +%Docstring +Sets the ``name`` for this item during exporting to layered exports (e.g. SVG or GeoPDF). + +If ``name`` is an empty string then the item does not need to be placed in any specific +layer and will automatically be grouped with other items where possible. + +If the layer ``name`` is non-empty, then the item will be placed in a group with the corresponding name +during layered exports. + +.. seealso:: :py:func:`exportLayerName` + +.. seealso:: :py:func:`exportLayerBehavior` + +.. versionadded:: 3.40 %End virtual int numberExportLayers() const /Deprecated/; @@ -502,6 +539,8 @@ Moves to the next export part for a multi-layered export item, during a multi-la double opacity; QString mapTheme; + + QString groupName; }; virtual QgsLayoutItem::ExportLayerDetail exportLayerDetails() const; diff --git a/python/core/auto_generated/layout/qgslayoutitem.sip.in b/python/core/auto_generated/layout/qgslayoutitem.sip.in index ba962f456e0f..72f52e6ac7f3 100644 --- a/python/core/auto_generated/layout/qgslayoutitem.sip.in +++ b/python/core/auto_generated/layout/qgslayoutitem.sip.in @@ -182,6 +182,7 @@ Base class for graphical items within a :py:class:`QgsLayout`. UndoMarginRight, UndoSetId, UndoRotation, + UndoExportLayerName, UndoShapeStyle, UndoShapeCornerRadius, UndoNodeMove, @@ -432,13 +433,49 @@ Sets the item's parent ``group``. virtual ExportLayerBehavior exportLayerBehavior() const; %Docstring -Returns the behavior of this item during exporting to layered exports (e.g. SVG). +Returns the behavior of this item during exporting to layered exports (e.g. SVG or GeoPDF). .. seealso:: :py:func:`numberExportLayers` .. seealso:: :py:func:`exportLayerDetails` +.. seealso:: :py:func:`exportLayerName` + .. versionadded:: 3.10 +%End + + QString exportLayerName() const; +%Docstring +Returns the name for this item during exporting to layered exports (e.g. SVG or GeoPDF). + +By default this is an empty string, which indicates that the item does not need to be placed in any specific +layer and will automatically be grouped with other items where possible. + +If the layer name is non-empty, then the item will be placed in a group with the corresponding name +during layered exports. + +.. seealso:: :py:func:`setExportLayerName` + +.. seealso:: :py:func:`exportLayerBehavior` + +.. versionadded:: 3.40 +%End + + void setExportLayerName( const QString &name ); +%Docstring +Sets the ``name`` for this item during exporting to layered exports (e.g. SVG or GeoPDF). + +If ``name`` is an empty string then the item does not need to be placed in any specific +layer and will automatically be grouped with other items where possible. + +If the layer ``name`` is non-empty, then the item will be placed in a group with the corresponding name +during layered exports. + +.. seealso:: :py:func:`exportLayerName` + +.. seealso:: :py:func:`exportLayerBehavior` + +.. versionadded:: 3.40 %End virtual int numberExportLayers() const /Deprecated/; @@ -502,6 +539,8 @@ Moves to the next export part for a multi-layered export item, during a multi-la double opacity; QString mapTheme; + + QString groupName; }; virtual QgsLayoutItem::ExportLayerDetail exportLayerDetails() const; diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index d2539a217f1d..c835ec579fac 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -605,6 +605,7 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f component.mapLayerId = layerDetail.mapLayerId; component.opacity = layerDetail.opacity; component.compositionMode = layerDetail.compositionMode; + component.group = layerDetail.groupName; if ( !layerDetail.mapTheme.isEmpty() ) { component.group = layerDetail.mapTheme; @@ -1777,6 +1778,7 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::handleLayeredExport( const QL int prevType = -1; QgsLayoutItem::ExportLayerBehavior prevItemBehavior = QgsLayoutItem::CanGroupWithAnyOtherItem; + QString previousItemGroup; unsigned int layerId = 1; QgsLayoutItem::ExportLayerDetail layerDetails; itemHider.hideAll(); @@ -1787,9 +1789,20 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::handleLayeredExport( const QL QgsLayoutItem *layoutItem = dynamic_cast( item ); bool canPlaceInExistingLayer = false; + QString thisItemExportGroupName; if ( layoutItem ) { - switch ( layoutItem->exportLayerBehavior() ) + QgsLayoutItem::ExportLayerBehavior itemExportBehavior = layoutItem->exportLayerBehavior(); + thisItemExportGroupName = layoutItem->exportLayerName(); + if ( !thisItemExportGroupName.isEmpty() ) + { + if ( thisItemExportGroupName != previousItemGroup && !currentLayerItems.empty() ) + itemExportBehavior = QgsLayoutItem::MustPlaceInOwnLayer; + else + layerDetails.groupName = thisItemExportGroupName; + } + + switch ( itemExportBehavior ) { case QgsLayoutItem::CanGroupWithAnyOtherItem: { @@ -1838,12 +1851,14 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::handleLayeredExport( const QL canPlaceInExistingLayer = false; break; } - prevItemBehavior = layoutItem->exportLayerBehavior(); + prevItemBehavior = itemExportBehavior; prevType = layoutItem->type(); + previousItemGroup = thisItemExportGroupName; } else { prevItemBehavior = QgsLayoutItem::MustPlaceInOwnLayer; + previousItemGroup.clear(); } if ( canPlaceInExistingLayer ) @@ -1899,6 +1914,7 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::handleLayeredExport( const QL { currentLayerItems << item; } + layerDetails.groupName = thisItemExportGroupName; } } if ( !currentLayerItems.isEmpty() ) diff --git a/src/core/layout/qgslayoutitem.cpp b/src/core/layout/qgslayoutitem.cpp index ecc6ea489818..428ae0a1ec7e 100644 --- a/src/core/layout/qgslayoutitem.cpp +++ b/src/core/layout/qgslayoutitem.cpp @@ -243,6 +243,16 @@ QgsLayoutItem::ExportLayerBehavior QgsLayoutItem::exportLayerBehavior() const return CanGroupWithAnyOtherItem; } +QString QgsLayoutItem::exportLayerName() const +{ + return mExportLayerName; +} + +void QgsLayoutItem::setExportLayerName( const QString &name ) +{ + mExportLayerName = name; +} + int QgsLayoutItem::numberExportLayers() const { return 0; @@ -633,6 +643,7 @@ bool QgsLayoutItem::writeXml( QDomElement &parentElement, QDomDocument &doc, con element.setAttribute( QStringLiteral( "size" ), mItemSize.encodeSize() ); element.setAttribute( QStringLiteral( "itemRotation" ), QString::number( mItemRotation ) ); element.setAttribute( QStringLiteral( "groupUuid" ), mParentGroupUuid ); + element.setAttribute( QStringLiteral( "exportLayer" ), mExportLayerName ); element.setAttribute( QStringLiteral( "zValue" ), QString::number( zValue() ) ); element.setAttribute( QStringLiteral( "visibility" ), isVisible() ); @@ -823,6 +834,7 @@ bool QgsLayoutItem::readXml( const QDomElement &element, const QDomDocument &doc mExcludeFromExports = element.attribute( QStringLiteral( "excludeFromExports" ), QStringLiteral( "0" ) ).toInt(); mEvaluatedExcludeFromExports = mExcludeFromExports; + mExportLayerName = element.attribute( QStringLiteral( "exportLayer" ) ); const bool result = readPropertiesFromElement( element, doc, context ); diff --git a/src/core/layout/qgslayoutitem.h b/src/core/layout/qgslayoutitem.h index f74b9c2fdb49..be5301966803 100644 --- a/src/core/layout/qgslayoutitem.h +++ b/src/core/layout/qgslayoutitem.h @@ -234,6 +234,7 @@ class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectIt UndoMarginRight, //!< Right margin (since QGIS 3.30) UndoSetId, //!< Change item ID UndoRotation, //!< Rotation adjustment + UndoExportLayerName, //!< Export layer name (since QGIS 3.40) UndoShapeStyle, //!< Shape symbol style UndoShapeCornerRadius, //!< Shape corner radius UndoNodeMove, //!< Node move @@ -464,13 +465,46 @@ class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectIt }; /** - * Returns the behavior of this item during exporting to layered exports (e.g. SVG). + * Returns the behavior of this item during exporting to layered exports (e.g. SVG or GeoPDF). + * * \see numberExportLayers() * \see exportLayerDetails() + * \see exportLayerName() + * * \since QGIS 3.10 */ virtual ExportLayerBehavior exportLayerBehavior() const; + /** + * Returns the name for this item during exporting to layered exports (e.g. SVG or GeoPDF). + * + * By default this is an empty string, which indicates that the item does not need to be placed in any specific + * layer and will automatically be grouped with other items where possible. + * + * If the layer name is non-empty, then the item will be placed in a group with the corresponding name + * during layered exports. + * + * \see setExportLayerName() + * \see exportLayerBehavior() + * \since QGIS 3.40 + */ + QString exportLayerName() const; + + /** + * Sets the \a name for this item during exporting to layered exports (e.g. SVG or GeoPDF). + * + * If \a name is an empty string then the item does not need to be placed in any specific + * layer and will automatically be grouped with other items where possible. + * + * If the layer \a name is non-empty, then the item will be placed in a group with the corresponding name + * during layered exports. + * + * \see exportLayerName() + * \see exportLayerBehavior() + * \since QGIS 3.40 + */ + void setExportLayerName( const QString &name ); + /** * Returns the number of layers that this item requires for exporting during layered exports (e.g. SVG). * Returns 0 if this item is to be placed on the same layer as the previous item, @@ -540,6 +574,13 @@ class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectIt //! Associated map theme, or an empty string if this export layer does not need to be associated with a map theme QString mapTheme; + + /** + * Associated group name, if this layer is associated with an export group. + * + * \since QGIS 3.40 + */ + QString groupName; }; /** @@ -1307,6 +1348,8 @@ class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectIt //! Whether item should be excluded in exports bool mExcludeFromExports = false; + QString mExportLayerName; + /** * Temporary evaluated item exclusion. Data defined properties may mean * this value differs from mExcludeFromExports. diff --git a/src/gui/layout/qgslayoutitemwidget.cpp b/src/gui/layout/qgslayoutitemwidget.cpp index fcee0b388a86..a97c085c2dae 100644 --- a/src/gui/layout/qgslayoutitemwidget.cpp +++ b/src/gui/layout/qgslayoutitemwidget.cpp @@ -27,6 +27,7 @@ #include "qgslayoutdesignerinterface.h" #include "qgslayoutpagecollection.h" #include "qgslayoutmultiframe.h" +#include "qgsfilterlineedit.h" #include // @@ -306,6 +307,11 @@ QgsLayoutItemPropertiesWidget::QgsLayoutItemPropertiesWidget( QWidget *parent, Q mStrokeUnitsComboBox->linkToWidget( mStrokeWidthSpinBox ); mStrokeUnitsComboBox->setConverter( &item->layout()->renderContext().measurementConverter() ); + QgsFilterLineEdit *exportGroupLineEdit = new QgsFilterLineEdit(); + exportGroupLineEdit->setShowClearButton( true ); + exportGroupLineEdit->setPlaceholderText( tr( "Not set" ) ); + mExportGroupNameCombo->setLineEdit( exportGroupLineEdit ); + mPosUnitsComboBox->linkToWidget( mXPosSpin ); mPosUnitsComboBox->linkToWidget( mYPosSpin ); mSizeUnitsComboBox->linkToWidget( mWidthSpin ); @@ -330,6 +336,7 @@ QgsLayoutItemPropertiesWidget::QgsLayoutItemPropertiesWidget( QWidget *parent, Q connect( mFrameJoinStyleCombo, static_cast( &QComboBox::currentIndexChanged ), this, &QgsLayoutItemPropertiesWidget::mFrameJoinStyleCombo_currentIndexChanged ); connect( mBackgroundGroupBox, &QgsCollapsibleGroupBoxBasic::toggled, this, &QgsLayoutItemPropertiesWidget::mBackgroundGroupBox_toggled ); connect( mItemIdLineEdit, &QLineEdit::editingFinished, this, &QgsLayoutItemPropertiesWidget::mItemIdLineEdit_editingFinished ); + connect( mExportGroupNameCombo, &QComboBox::currentTextChanged, this, &QgsLayoutItemPropertiesWidget::exportGroupNameEditingFinished ); connect( mPageSpinBox, static_cast < void ( QSpinBox::* )( int ) > ( &QSpinBox::valueChanged ), this, &QgsLayoutItemPropertiesWidget::mPageSpinBox_valueChanged ); connect( mXPosSpin, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutItemPropertiesWidget::mXPosSpin_valueChanged ); connect( mYPosSpin, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutItemPropertiesWidget::mYPosSpin_valueChanged ); @@ -728,37 +735,19 @@ void QgsLayoutItemPropertiesWidget::setValuesForGuiNonPositionElements() return; } - auto block = [ = ]( bool blocked ) - { - mStrokeWidthSpinBox->blockSignals( blocked ); - mStrokeUnitsComboBox->blockSignals( blocked ); - mFrameGroupBox->blockSignals( blocked ); - mBackgroundGroupBox->blockSignals( blocked ); - mItemIdLineEdit->blockSignals( blocked ); - mBlendModeCombo->blockSignals( blocked ); - mOpacityWidget->blockSignals( blocked ); - mFrameColorButton->blockSignals( blocked ); - mFrameJoinStyleCombo->blockSignals( blocked ); - mBackgroundColorButton->blockSignals( blocked ); - mItemRotationSpinBox->blockSignals( blocked ); - mExcludeFromPrintsCheckBox->blockSignals( blocked ); - }; - block( true ); - - mBackgroundColorButton->setColor( mItem->backgroundColor( false ) ); - mFrameColorButton->setColor( mItem->frameStrokeColor() ); - mStrokeUnitsComboBox->setUnit( mItem->frameStrokeWidth().units() ); - mStrokeWidthSpinBox->setValue( mItem->frameStrokeWidth().length() ); - mFrameJoinStyleCombo->setPenJoinStyle( mItem->frameJoinStyle() ); - mItemIdLineEdit->setText( mItem->id() ); - mFrameGroupBox->setChecked( mItem->frameEnabled() ); - mBackgroundGroupBox->setChecked( mItem->hasBackground() ); - mBlendModeCombo->setBlendMode( mItem->blendMode() ); - mOpacityWidget->setOpacity( mItem->itemOpacity() ); - mItemRotationSpinBox->setValue( mItem->itemRotation() ); - mExcludeFromPrintsCheckBox->setChecked( mItem->excludeFromExports() ); - - block( false ); + whileBlocking( mBackgroundColorButton )->setColor( mItem->backgroundColor( false ) ); + whileBlocking( mFrameColorButton )->setColor( mItem->frameStrokeColor() ); + whileBlocking( mStrokeUnitsComboBox )->setUnit( mItem->frameStrokeWidth().units() ); + whileBlocking( mStrokeWidthSpinBox )->setValue( mItem->frameStrokeWidth().length() ); + whileBlocking( mFrameJoinStyleCombo )->setPenJoinStyle( mItem->frameJoinStyle() ); + whileBlocking( mItemIdLineEdit )->setText( mItem->id() ); + whileBlocking( mFrameGroupBox )->setChecked( mItem->frameEnabled() ); + whileBlocking( mBackgroundGroupBox )->setChecked( mItem->hasBackground() ); + whileBlocking( mBlendModeCombo )->setBlendMode( mItem->blendMode() ); + whileBlocking( mOpacityWidget )->setOpacity( mItem->itemOpacity() ); + whileBlocking( mItemRotationSpinBox )->setValue( mItem->itemRotation() ); + whileBlocking( mExcludeFromPrintsCheckBox )->setChecked( mItem->excludeFromExports() ); + whileBlocking( mExportGroupNameCombo )->setCurrentText( mItem->exportLayerName() ); } void QgsLayoutItemPropertiesWidget::initializeDataDefinedButtons() @@ -798,6 +787,27 @@ void QgsLayoutItemPropertiesWidget::setValuesForGuiElements() mFrameColorButton->setAllowOpacity( true ); mFrameColorButton->setContext( QStringLiteral( "composer" ) ); + if ( QgsLayout *layout = mItem->layout() ) + { + // collect export groups from layout, so that we can offer auto completion in the PDF export group drop down + QList< QgsLayoutItem * > items; + layout->layoutItems( items ); + QStringList existingGroups; + for ( const QgsLayoutItem *item : std::as_const( items ) ) + { + if ( !item->exportLayerName().isEmpty() && !existingGroups.contains( item->exportLayerName() ) ) + existingGroups.append( item->exportLayerName() ); + } + + std::sort( existingGroups.begin(), existingGroups.end(), [ = ]( const QString & a, const QString & b ) -> bool + { + return a.localeAwareCompare( b ) < 0; + } ); + + whileBlocking( mExportGroupNameCombo )->clear(); + whileBlocking( mExportGroupNameCombo )->addItems( existingGroups ); + } + setValuesForGuiPositionElements(); setValuesForGuiNonPositionElements(); populateDataDefinedButtons(); @@ -837,6 +847,16 @@ void QgsLayoutItemPropertiesWidget::mItemIdLineEdit_editingFinished() } } +void QgsLayoutItemPropertiesWidget::exportGroupNameEditingFinished() +{ + if ( mItem ) + { + mItem->layout()->undoStack()->beginCommand( mItem, tr( "Change Export Group Name" ), QgsLayoutItem::UndoExportLayerName ); + mItem->setExportLayerName( mExportGroupNameCombo->currentText() ); + mItem->layout()->undoStack()->endCommand(); + } +} + void QgsLayoutItemPropertiesWidget::mPageSpinBox_valueChanged( int ) { mFreezePageSpin = true; diff --git a/src/gui/layout/qgslayoutitemwidget.h b/src/gui/layout/qgslayoutitemwidget.h index 04f816bc3184..518acd1ac4bf 100644 --- a/src/gui/layout/qgslayoutitemwidget.h +++ b/src/gui/layout/qgslayoutitemwidget.h @@ -272,6 +272,7 @@ class GUI_EXPORT QgsLayoutItemPropertiesWidget: public QWidget, private Ui::QgsL void mFrameJoinStyleCombo_currentIndexChanged( int index ); void mBackgroundGroupBox_toggled( bool state ); void mItemIdLineEdit_editingFinished(); + void exportGroupNameEditingFinished(); //adjust coordinates in line edits void mPageSpinBox_valueChanged( int ); diff --git a/src/ui/layout/qgslayoutitemwidgetbase.ui b/src/ui/layout/qgslayoutitemwidgetbase.ui index 2cb96bafe1b5..199ee7ed0d7b 100644 --- a/src/ui/layout/qgslayoutitemwidgetbase.ui +++ b/src/ui/layout/qgslayoutitemwidgetbase.ui @@ -685,24 +685,13 @@ composeritem - - - - Blending mode - - - - - + + - - - Qt::StrongFocus - - + - + @@ -728,6 +717,13 @@ + + + + Blending mode + + + @@ -735,13 +731,17 @@ - - + + - + + + Qt::StrongFocus + + - + @@ -749,6 +749,20 @@ + + + + GeoPDF group + + + + + + + true + + + @@ -791,37 +805,37 @@ - QgsColorButton - QToolButton -
qgscolorbutton.h
- 1 + QgsDoubleSpinBox + QDoubleSpinBox +
qgsdoublespinbox.h
QgsPropertyOverrideButton QToolButton
qgspropertyoverridebutton.h
- - QgsDoubleSpinBox - QDoubleSpinBox -
qgsdoublespinbox.h
-
QgsSpinBox QSpinBox
qgsspinbox.h
- - QgsPenJoinStyleComboBox - QComboBox -
qgspenstylecombobox.h
-
QgsCollapsibleGroupBoxBasic QGroupBox -
qgscollapsiblegroupbox.h
+
qgscollapsiblegroupbox.h
+ 1 +
+ + QgsColorButton + QToolButton +
qgscolorbutton.h
1
+ + QgsPenJoinStyleComboBox + QComboBox +
qgspenstylecombobox.h
+
QgsBlendModeComboBox QComboBox diff --git a/tests/src/core/testqgslayoutexporter.cpp b/tests/src/core/testqgslayoutexporter.cpp index 67f8898981a3..9756c0eaef20 100644 --- a/tests/src/core/testqgslayoutexporter.cpp +++ b/tests/src/core/testqgslayoutexporter.cpp @@ -40,6 +40,7 @@ class TestQgsLayoutExporter: public QgsTest void init(); void cleanup(); void testHandleLayeredExport(); + void testHandleLayeredExportCustomGroups(); }; @@ -263,5 +264,253 @@ void TestQgsLayoutExporter::testHandleLayeredExport() qDeleteAll( items ); } + +void TestQgsLayoutExporter::testHandleLayeredExportCustomGroups() +{ + QgsProject p; + QgsLayout l( &p ); + QgsLayoutExporter exporter( &l ); + + QList< unsigned int > layerIds; + QStringList layerNames; + QStringList mapLayerIds; + QStringList groupNames; + QgsLayout *layout = &l; + auto exportFunc = [&layerIds, &layerNames, &mapLayerIds, &groupNames, layout]( unsigned int layerId, const QgsLayoutItem::ExportLayerDetail & layerDetail )->QgsLayoutExporter::ExportResult + { + layerIds << layerId; + layerNames << layerDetail.name; + mapLayerIds << layerDetail.mapLayerId; + groupNames << layerDetail.groupName; + QImage im( 512, 512, QImage::Format_ARGB32_Premultiplied ); + QPainter p( &im ); + layout->render( &p ); + p.end(); + + return QgsLayoutExporter::Success; + }; + + QList< QGraphicsItem * > items; + QStringList expectedGroupNames; + QgsLayoutExporter::ExportResult res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QVERIFY( layerIds.isEmpty() ); + QVERIFY( layerNames.isEmpty() ); + QVERIFY( groupNames.isEmpty() ); + QVERIFY( mapLayerIds.isEmpty() ); + + // add two pages to a layout + QgsLayoutItemPage *page1 = new QgsLayoutItemPage( &l ); + items << page1; + expectedGroupNames << QString(); + res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QCOMPARE( layerIds, QList< unsigned int >() << 1 ); + QCOMPARE( layerNames, QStringList() << QStringLiteral( "Page" ) ); + QCOMPARE( groupNames, expectedGroupNames ); + QCOMPARE( mapLayerIds, QStringList() << QString() ); + layerIds.clear(); + layerNames.clear(); + groupNames.clear(); + mapLayerIds.clear(); + + QgsLayoutItemPage *page2 = new QgsLayoutItemPage( &l ); + items << page2; + res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QCOMPARE( layerIds, QList< unsigned int >() << 1 ); + QCOMPARE( layerNames, QStringList() << QStringLiteral( "Pages" ) ); + QCOMPARE( groupNames, expectedGroupNames ); + QCOMPARE( mapLayerIds, QStringList() << QString() ); + layerIds.clear(); + layerNames.clear(); + groupNames.clear(); + mapLayerIds.clear(); + + QgsLayoutItemLabel *label = new QgsLayoutItemLabel( &l ); + items << label; + expectedGroupNames << QString(); + res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QCOMPARE( layerIds, QList< unsigned int >() << 1 << 2 ); + QCOMPARE( layerNames, QStringList() << QStringLiteral( "Pages" ) << QStringLiteral( "Label" ) ); + QCOMPARE( groupNames, expectedGroupNames ); + QCOMPARE( mapLayerIds, QStringList() << QString() << QString() ); + layerIds.clear(); + layerNames.clear(); + groupNames.clear(); + mapLayerIds.clear(); + + QgsLayoutItemShape *shape = new QgsLayoutItemShape( &l ); + items << shape; + res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QCOMPARE( layerIds, QList< unsigned int >() << 1 << 2 ); + QCOMPARE( layerNames, QStringList() << QStringLiteral( "Pages" ) << QStringLiteral( "Label, Shape" ) ); + QCOMPARE( groupNames, expectedGroupNames ); + QCOMPARE( mapLayerIds, QStringList() << QString() << QString() ); + layerIds.clear(); + layerNames.clear(); + groupNames.clear(); + mapLayerIds.clear(); + + QgsLayoutItemLabel *label2 = new QgsLayoutItemLabel( &l ); + label2->setExportLayerName( QStringLiteral( "first group" ) ); + expectedGroupNames << QStringLiteral( "first group" ); + items << label2; + res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QCOMPARE( layerIds, QList< unsigned int >() << 1 << 2 << 3 ); + QCOMPARE( layerNames, QStringList() << QStringLiteral( "Pages" ) << QStringLiteral( "Label, Shape" ) << QStringLiteral( "Label" ) ); + QCOMPARE( groupNames, expectedGroupNames ); + layerIds.clear(); + layerNames.clear(); + groupNames.clear(); + mapLayerIds.clear(); + + // add an item which can only be used with other similar items, should break the next label into a different layer + QgsLayoutItemScaleBar *scaleBar = new QgsLayoutItemScaleBar( &l ); + scaleBar->setExportLayerName( QStringLiteral( "first group" ) ); + expectedGroupNames << QStringLiteral( "first group" ); + items << scaleBar; + res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QCOMPARE( layerIds, QList< unsigned int >() << 1 << 2 << 3 << 4 ); + QCOMPARE( layerNames, QStringList() << QStringLiteral( "Pages" ) << QStringLiteral( "Label, Shape" ) << QStringLiteral( "Label" ) << QStringLiteral( "Scalebar" ) ); + QCOMPARE( groupNames, expectedGroupNames ); + QCOMPARE( mapLayerIds, QStringList() << QString() << QString() << QString() << QString() ); + layerIds.clear(); + layerNames.clear(); + groupNames.clear(); + mapLayerIds.clear(); + + QgsLayoutItemLabel *label3 = new QgsLayoutItemLabel( &l ); + items << label3; + expectedGroupNames << QString(); + res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QCOMPARE( layerIds, QList< unsigned int >() << 1 << 2 << 3 << 4 << 5 ); + QCOMPARE( layerNames, QStringList() << QStringLiteral( "Pages" ) << QStringLiteral( "Label, Shape" ) << QStringLiteral( "Label" ) << QStringLiteral( "Scalebar" ) << QStringLiteral( "Label" ) ); + QCOMPARE( groupNames, expectedGroupNames ); + QCOMPARE( mapLayerIds, QStringList() << QString() << QString() << QString() << QString() << QString() ); + layerIds.clear(); + layerNames.clear(); + groupNames.clear(); + mapLayerIds.clear(); + + QgsLayoutItemScaleBar *scaleBar2 = new QgsLayoutItemScaleBar( &l ); + scaleBar2->setExportLayerName( QStringLiteral( "scales" ) ); + items << scaleBar2; + expectedGroupNames << QStringLiteral( "scales" ); + QgsLayoutItemScaleBar *scaleBar3 = new QgsLayoutItemScaleBar( &l ); + scaleBar3->setExportLayerName( QStringLiteral( "scales" ) ); + items << scaleBar3; + expectedGroupNames << QStringLiteral( "scales" ); + res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QCOMPARE( layerIds, QList< unsigned int >() << 1 << 2 << 3 << 4 << 5 << 6 << 7 ); + QCOMPARE( layerNames, QStringList() << QStringLiteral( "Pages" ) << QStringLiteral( "Label, Shape" ) << QStringLiteral( "Label" ) << QStringLiteral( "Scalebar" ) << QStringLiteral( "Label" ) << QStringLiteral( "Scalebar" ) << QStringLiteral( "Scalebar" ) ); + QCOMPARE( groupNames, expectedGroupNames ); + QCOMPARE( mapLayerIds, QStringList() << QString() << QString() << QString() << QString() << QString() << QString() << QString() ); + layerIds.clear(); + layerNames.clear(); + groupNames.clear(); + mapLayerIds.clear(); + + // with an item which has sublayers + QgsVectorLayer *linesLayer = new QgsVectorLayer( TEST_DATA_DIR + QStringLiteral( "/lines.shp" ), + QStringLiteral( "lines" ), QStringLiteral( "ogr" ) ); + QVERIFY( linesLayer->isValid() ); + + p.addMapLayer( linesLayer ); + + QgsLayoutItemMap *map = new QgsLayoutItemMap( &l ); + map->attemptSetSceneRect( QRectF( 20, 20, 200, 100 ) ); + map->setFrameEnabled( false ); + map->setBackgroundEnabled( false ); + map->setCrs( linesLayer->crs() ); + map->zoomToExtent( linesLayer->extent() ); + map->setLayers( QList() << linesLayer ); + + items << map; + expectedGroupNames << QString(); + res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QCOMPARE( layerIds, QList< unsigned int >() << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 ); + QCOMPARE( layerNames, QStringList() << QStringLiteral( "Pages" ) << QStringLiteral( "Label, Shape" ) << QStringLiteral( "Label" ) << QStringLiteral( "Scalebar" ) << QStringLiteral( "Label" ) << QStringLiteral( "Scalebar" ) << QStringLiteral( "Scalebar" ) << QStringLiteral( "Map 1: lines" ) ); + QCOMPARE( groupNames, expectedGroupNames ); + QCOMPARE( mapLayerIds, QStringList() << QString() << QString() << QString() << QString() << QString() << QString() << QString() << linesLayer->id() ); + layerIds.clear(); + layerNames.clear(); + groupNames.clear(); + mapLayerIds.clear(); + + map->setFrameEnabled( true ); + map->setBackgroundEnabled( true ); + res = exporter.handleLayeredExport( items, exportFunc ); + expectedGroupNames << QString() << QString(); + QCOMPARE( res, QgsLayoutExporter::Success ); + QCOMPARE( layerIds, QList< unsigned int >() << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10 ); + QCOMPARE( layerNames, QStringList() << QStringLiteral( "Pages" ) << QStringLiteral( "Label, Shape" ) << QStringLiteral( "Label" ) << QStringLiteral( "Scalebar" ) << QStringLiteral( "Label" ) << QStringLiteral( "Scalebar" ) << QStringLiteral( "Scalebar" ) << QStringLiteral( "Map 1: Background" ) << QStringLiteral( "Map 1: lines" ) << QStringLiteral( "Map 1: Frame" ) ); + QCOMPARE( groupNames, expectedGroupNames ); + QCOMPARE( mapLayerIds, QStringList() << QString() << QString() << QString() << QString() << QString() << QString() << QString() << QString() << linesLayer->id() << QString() ); + layerIds.clear(); + layerNames.clear(); + groupNames.clear(); + mapLayerIds.clear(); + + // add two legends -- legends are complex and must be placed in an isolated layer + QgsLayoutItemLegend *legend = new QgsLayoutItemLegend( &l ); + legend->setId( QStringLiteral( "my legend" ) ); + legend->setExportLayerName( QStringLiteral( "second group" ) ); + QgsLayoutItemLegend *legend2 = new QgsLayoutItemLegend( &l ); + legend2->setId( QStringLiteral( "my legend 2" ) ); + legend2->setExportLayerName( QStringLiteral( "second group" ) ); + items << legend << legend2; + expectedGroupNames << QStringLiteral( "second group" ) << QStringLiteral( "second group" ); + res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QCOMPARE( layerIds, QList< unsigned int >() << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10 << 11 << 12 ); + QCOMPARE( layerNames, QStringList() << QStringLiteral( "Pages" ) << QStringLiteral( "Label, Shape" ) << QStringLiteral( "Label" ) << QStringLiteral( "Scalebar" ) << QStringLiteral( "Label" ) << QStringLiteral( "Scalebar" ) << QStringLiteral( "Scalebar" ) << QStringLiteral( "Map 1: Background" ) << QStringLiteral( "Map 1: lines" ) << QStringLiteral( "Map 1: Frame" ) << QStringLiteral( "my legend" ) << QStringLiteral( "my legend 2" ) ); + QCOMPARE( groupNames, expectedGroupNames ); + QCOMPARE( mapLayerIds, QStringList() << QString() << QString() << QString() << QString() << QString() << QString() << QString() << QString() << linesLayer->id() << QString() << QString() << QString() ); + layerIds.clear(); + layerNames.clear(); + groupNames.clear(); + mapLayerIds.clear(); + + QgsLayoutItemLabel *label4 = new QgsLayoutItemLabel( &l ); + items << label4; + label4->setExportLayerName( QStringLiteral( "more labels" ) ); + expectedGroupNames << QStringLiteral( "more labels" ); + res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QCOMPARE( layerIds, QList< unsigned int >() << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10 << 11 << 12 << 13 ); + QCOMPARE( layerNames, QStringList() << QStringLiteral( "Pages" ) << QStringLiteral( "Label, Shape" ) << QStringLiteral( "Label" ) << QStringLiteral( "Scalebar" ) << QStringLiteral( "Label" ) << QStringLiteral( "Scalebar" ) << QStringLiteral( "Scalebar" ) << QStringLiteral( "Map 1: Background" ) << QStringLiteral( "Map 1: lines" ) << QStringLiteral( "Map 1: Frame" ) << QStringLiteral( "my legend" ) << QStringLiteral( "my legend 2" ) << QStringLiteral( "Label" ) ); + QCOMPARE( groupNames, expectedGroupNames ); + QCOMPARE( mapLayerIds, QStringList() << QString() << QString() << QString() << QString() << QString() << QString() << QString() << QString() << linesLayer->id() << QString() << QString() << QString() << QString() ); + layerIds.clear(); + layerNames.clear(); + groupNames.clear(); + mapLayerIds.clear(); + + QgsLayoutItemLabel *label5 = new QgsLayoutItemLabel( &l ); + items << label5; + label5->setExportLayerName( QStringLiteral( "more labels 2" ) ); + expectedGroupNames << QStringLiteral( "more labels 2" ); + res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QCOMPARE( layerIds, QList< unsigned int >() << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10 << 11 << 12 << 13 << 14 ); + QCOMPARE( layerNames, QStringList() << QStringLiteral( "Pages" ) << QStringLiteral( "Label, Shape" ) << QStringLiteral( "Label" ) << QStringLiteral( "Scalebar" ) << QStringLiteral( "Label" ) << QStringLiteral( "Scalebar" ) << QStringLiteral( "Scalebar" ) << QStringLiteral( "Map 1: Background" ) << QStringLiteral( "Map 1: lines" ) << QStringLiteral( "Map 1: Frame" ) << QStringLiteral( "my legend" ) << QStringLiteral( "my legend 2" ) << QStringLiteral( "Label" ) << QStringLiteral( "Label" ) ); + QCOMPARE( groupNames, expectedGroupNames ); + QCOMPARE( mapLayerIds, QStringList() << QString() << QString() << QString() << QString() << QString() << QString() << QString() << QString() << linesLayer->id() << QString() << QString() << QString() << QString() << QString() ); + layerIds.clear(); + layerNames.clear(); + groupNames.clear(); + mapLayerIds.clear(); + + qDeleteAll( items ); +} + QGSTEST_MAIN( TestQgsLayoutExporter ) #include "testqgslayoutexporter.moc" diff --git a/tests/src/core/testqgslayoutitem.cpp b/tests/src/core/testqgslayoutitem.cpp index 638ab86e6b07..a4635fec7454 100644 --- a/tests/src/core/testqgslayoutitem.cpp +++ b/tests/src/core/testqgslayoutitem.cpp @@ -170,6 +170,7 @@ class TestQgsLayoutItem: public QgsTest void blendMode(); void opacity(); void excludeFromExports(); + void exportName(); void setSceneRect(); void page(); void itemVariablesFunction(); @@ -1743,6 +1744,7 @@ void TestQgsLayoutItem::writeReadXmlProperties() original->setBlendMode( QPainter::CompositionMode_Darken ); original->setExcludeFromExports( true ); original->setItemOpacity( 0.75 ); + original->setExportLayerName( QStringLiteral( "_export_layer_" ) ); std::unique_ptr< QgsLayoutItem > copy = createCopyViaXml( &l, original ); @@ -1772,6 +1774,7 @@ void TestQgsLayoutItem::writeReadXmlProperties() QCOMPARE( copy->blendMode(), QPainter::CompositionMode_Darken ); QVERIFY( copy->excludeFromExports( ) ); QCOMPARE( copy->itemOpacity(), 0.75 ); + QCOMPARE( copy->exportLayerName(), QStringLiteral( "_export_layer_" ) ); delete original; } @@ -2096,6 +2099,18 @@ void TestQgsLayoutItem::excludeFromExports() QGSVERIFYLAYOUTCHECK( QStringLiteral( "layoutitem_excluded" ), &l, 0, 0, QSize( 400, 400 ) ); } +void TestQgsLayoutItem::exportName() +{ + QgsProject proj; + QgsLayout l( &proj ); + QgsLayoutItemShape *item = new QgsLayoutItemShape( &l ); + l.addLayoutItem( item ); + + QCOMPARE( item->exportLayerName(), QString() ); + item->setExportLayerName( QStringLiteral( "my export group" ) ); + QCOMPARE( item->exportLayerName(), QStringLiteral( "my export group" ) ); +} + std::unique_ptr TestQgsLayoutItem::createCopyViaXml( QgsLayout *layout, QgsLayoutItem *original ) { //save original item to xml