From 12dcfabb26518c1c22afee7f99eab5b0008f068c Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 8 Jun 2020 16:32:46 +1000 Subject: [PATCH] [geopdf] Allow users to reorder layers in the generated layer tree Fixes #36535 --- src/app/layout/qgslayoutdesignerdialog.cpp | 9 +++- src/core/layout/qgslayoutgeopdfexporter.cpp | 20 ++++++++- src/gui/layout/qgsgeopdflayertreemodel.cpp | 43 ++++++++++--------- src/gui/layout/qgsgeopdflayertreemodel.h | 7 +-- .../qgslayoutpdfexportoptionsdialog.cpp | 35 +++++++++++++-- .../layout/qgslayoutpdfexportoptionsdialog.h | 6 +++ src/ui/layout/qgspdfexportoptions.ui | 4 +- tests/src/core/testqgslayoutgeopdfexport.cpp | 41 ++++++++++++++++++ 8 files changed, 134 insertions(+), 31 deletions(-) diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index c9f58e609d0..fbf200a9fd5 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -4339,6 +4339,7 @@ bool QgsLayoutDesignerDialog::getPdfExportSettings( QgsLayoutExporter::PdfExport bool geoPdf = false; bool useOgcBestPracticeFormat = false; QStringList exportThemes; + QStringList geoPdfLayerOrder; if ( mLayout ) { settings.flags = mLayout->renderContext().flags(); @@ -4352,6 +4353,10 @@ bool QgsLayoutDesignerDialog::getPdfExportSettings( QgsLayoutExporter::PdfExport const QString themes = mLayout->customProperty( QStringLiteral( "pdfExportThemes" ) ).toString(); if ( !themes.isEmpty() ) exportThemes = themes.split( QStringLiteral( "~~~" ) ); + const QString layerOrder = mLayout->customProperty( QStringLiteral( "pdfLayerOrder" ) ).toString(); + if ( !layerOrder.isEmpty() ) + geoPdfLayerOrder = layerOrder.split( QStringLiteral( "~~~" ) ); + const int prevLayoutSettingLabelsAsOutlines = mLayout->customProperty( QStringLiteral( "pdfTextFormat" ), -1 ).toInt(); if ( prevLayoutSettingLabelsAsOutlines >= 0 ) { @@ -4383,7 +4388,7 @@ bool QgsLayoutDesignerDialog::getPdfExportSettings( QgsLayoutExporter::PdfExport } } - QgsLayoutPdfExportOptionsDialog dialog( this, allowGeoPdfExport, dialogGeoPdfReason ); + QgsLayoutPdfExportOptionsDialog dialog( this, allowGeoPdfExport, dialogGeoPdfReason, geoPdfLayerOrder ); dialog.setTextRenderFormat( prevTextRenderFormat ); dialog.setForceVector( forceVector ); @@ -4408,6 +4413,7 @@ bool QgsLayoutDesignerDialog::getPdfExportSettings( QgsLayoutExporter::PdfExport geoPdf = dialog.exportGeoPdf(); useOgcBestPracticeFormat = dialog.useOgcBestPracticeFormat(); exportThemes = dialog.exportThemes(); + geoPdfLayerOrder = dialog.geoPdfLayerOrder(); if ( mLayout ) { @@ -4421,6 +4427,7 @@ bool QgsLayoutDesignerDialog::getPdfExportSettings( QgsLayoutExporter::PdfExport mLayout->setCustomProperty( QStringLiteral( "pdfCreateGeoPdf" ), geoPdf ? 1 : 0 ); mLayout->setCustomProperty( QStringLiteral( "pdfOgcBestPracticeFormat" ), useOgcBestPracticeFormat ? 1 : 0 ); mLayout->setCustomProperty( QStringLiteral( "pdfExportThemes" ), exportThemes.join( QStringLiteral( "~~~" ) ) ); + mLayout->setCustomProperty( QStringLiteral( "pdfLayerOrder" ), geoPdfLayerOrder.join( QStringLiteral( "~~~" ) ) ); } settings.forceVectorOutput = forceVector; diff --git a/src/core/layout/qgslayoutgeopdfexporter.cpp b/src/core/layout/qgslayoutgeopdfexporter.cpp index e48bec89b6f..9195bb1c3e6 100644 --- a/src/core/layout/qgslayoutgeopdfexporter.cpp +++ b/src/core/layout/qgslayoutgeopdfexporter.cpp @@ -145,7 +145,25 @@ QgsLayoutGeoPdfExporter::QgsLayoutGeoPdfExporter( QgsLayout *layout ) map->addRenderedFeatureHandler( handler ); } - const QList< QgsMapLayer * > layerOrder = mLayout->project()->layerTreeRoot()->layerOrder(); + // start with project layer order, and then apply custom layer order if set + QStringList geoPdfLayerOrder; + const QString presetLayerOrder = mLayout->customProperty( QStringLiteral( "pdfLayerOrder" ) ).toString(); + if ( !presetLayerOrder.isEmpty() ) + geoPdfLayerOrder = presetLayerOrder.split( QStringLiteral( "~~~" ) ); + + QList< QgsMapLayer * > layerOrder = mLayout->project()->layerTreeRoot()->layerOrder(); + for ( auto it = geoPdfLayerOrder.rbegin(); it != geoPdfLayerOrder.rend(); ++it ) + { + for ( int i = 0; i < layerOrder.size(); ++i ) + { + if ( layerOrder.at( i )->id() == *it ) + { + layerOrder.move( i, 0 ); + break; + } + } + } + for ( const QgsMapLayer *layer : layerOrder ) mLayerOrder << layer->id(); } diff --git a/src/gui/layout/qgsgeopdflayertreemodel.cpp b/src/gui/layout/qgsgeopdflayertreemodel.cpp index bac3b91fa4d..81c8f54fc30 100644 --- a/src/gui/layout/qgsgeopdflayertreemodel.cpp +++ b/src/gui/layout/qgsgeopdflayertreemodel.cpp @@ -22,10 +22,10 @@ #include "qgsvectorlayer.h" #include "qgsapplication.h" -QgsGeoPdfLayerTreeModel::QgsGeoPdfLayerTreeModel( QgsLayerTree *rootNode, QObject *parent ) - : QgsLayerTreeModel( rootNode, parent ) +QgsGeoPdfLayerTreeModel::QgsGeoPdfLayerTreeModel( const QList &layers, QObject *parent ) + : QgsMapLayerModel( layers, parent ) { - setFlags( nullptr ); // ideally we'd just show embedded legend nodes - but the api doesn't exist for this + setItemsCanBeReordered( true ); } int QgsGeoPdfLayerTreeModel::columnCount( const QModelIndex &parent ) const @@ -36,37 +36,36 @@ int QgsGeoPdfLayerTreeModel::columnCount( const QModelIndex &parent ) const Qt::ItemFlags QgsGeoPdfLayerTreeModel::flags( const QModelIndex &idx ) const { + if ( !idx.isValid() ) + return Qt::ItemIsDropEnabled; + if ( idx.column() == IncludeVectorAttributes ) { if ( vectorLayer( idx ) ) - return QgsLayerTreeModel::flags( idx ) | Qt::ItemIsUserCheckable; + return QgsMapLayerModel::flags( idx ) | Qt::ItemIsUserCheckable; else - return QgsLayerTreeModel::flags( idx ); + return QgsMapLayerModel::flags( idx ); } if ( idx.column() == InitiallyVisible ) { - return QgsLayerTreeModel::flags( idx ) | Qt::ItemIsUserCheckable; + return QgsMapLayerModel::flags( idx ) | Qt::ItemIsUserCheckable; } if ( !mapLayer( idx ) ) { - return Qt::NoItemFlags; + return nullptr; } else { - return Qt::ItemIsEnabled | Qt::ItemIsEditable; + return Qt::ItemIsEnabled | Qt::ItemIsEditable | Qt::ItemIsDragEnabled; } return Qt::NoItemFlags; } QgsMapLayer *QgsGeoPdfLayerTreeModel::mapLayer( const QModelIndex &idx ) const { - QgsLayerTreeNode *node = index2node( index( idx.row(), LayerColumn, idx.parent() ) ); - if ( !node || !QgsLayerTree::isLayer( node ) ) - return nullptr; - - return QgsLayerTree::toLayer( node )->layer(); + return layerFromIndex( index( idx.row(), LayerColumn, idx.parent() ) ); } QgsVectorLayer *QgsGeoPdfLayerTreeModel::vectorLayer( const QModelIndex &idx ) const @@ -95,7 +94,7 @@ QVariant QgsGeoPdfLayerTreeModel::headerData( int section, Qt::Orientation orien } } } - return QgsLayerTreeModel::headerData( section, orientation, role ); + return QgsMapLayerModel::headerData( section, orientation, role ); } QVariant QgsGeoPdfLayerTreeModel::data( const QModelIndex &idx, int role ) const @@ -106,7 +105,7 @@ QVariant QgsGeoPdfLayerTreeModel::data( const QModelIndex &idx, int role ) const if ( role == Qt::CheckStateRole ) return QVariant(); - return QgsLayerTreeModel::data( idx, role ); + return QgsMapLayerModel::data( idx, role ); case GroupColumn: { @@ -145,14 +144,13 @@ QVariant QgsGeoPdfLayerTreeModel::data( const QModelIndex &idx, int role ) const } return QVariant(); } - return QgsLayerTreeModel::data( idx, role ); + return QVariant(); } case IncludeVectorAttributes: { if ( role == Qt::CheckStateRole ) { - QgsLayerTreeNode *node = index2node( index( idx.row(), LayerColumn, idx.parent() ) ); if ( QgsVectorLayer *vl = vectorLayer( idx ) ) { const QVariant v = vl->customProperty( QStringLiteral( "geopdf/includeFeatures" ) ); @@ -162,8 +160,8 @@ QVariant QgsGeoPdfLayerTreeModel::data( const QModelIndex &idx, int role ) const } else { - // otherwise, we default to the layer's visibility - return node->itemVisibilityChecked() ? Qt::Checked : Qt::Unchecked; + // otherwise, we default to true + return Qt::Checked; } } return QVariant(); @@ -219,6 +217,9 @@ bool QgsGeoPdfLayerTreeModel::setData( const QModelIndex &index, const QVariant } break; } + + case LayerColumn: + return QgsMapLayerModel::setData( index, value, role ); } return false; } @@ -244,10 +245,10 @@ QgsGeoPdfLayerFilteredTreeModel::QgsGeoPdfLayerFilteredTreeModel( QgsGeoPdfLayer bool QgsGeoPdfLayerFilteredTreeModel::filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const { - if ( QgsLayerTreeNode *node = mLayerTreeModel->index2node( sourceModel()->index( source_row, 0, source_parent ) ) ) + if ( QgsMapLayer *layer = mLayerTreeModel->layerFromIndex( sourceModel()->index( source_row, 0, source_parent ) ) ) { // filter out non-spatial layers - if ( QgsLayerTree::isLayer( node ) && QgsLayerTree::toLayer( node ) && QgsLayerTree::toLayer( node )->layer() && !QgsLayerTree::toLayer( node )->layer()->isSpatial() ) + if ( !layer->isSpatial() ) return false; } diff --git a/src/gui/layout/qgsgeopdflayertreemodel.h b/src/gui/layout/qgsgeopdflayertreemodel.h index 2c34df3d888..a52ba754474 100644 --- a/src/gui/layout/qgsgeopdflayertreemodel.h +++ b/src/gui/layout/qgsgeopdflayertreemodel.h @@ -22,10 +22,11 @@ #include #include "qgis_gui.h" -#include "qgslayertreemodel.h" +#include "qgsmaplayermodel.h" class QgsMapCanvas; class QgsProject; +class QgsVectorLayer; /** @@ -35,7 +36,7 @@ class QgsProject; * \note This class is not a part of public API * \since QGIS 3.12 */ -class GUI_EXPORT QgsGeoPdfLayerTreeModel : public QgsLayerTreeModel +class GUI_EXPORT QgsGeoPdfLayerTreeModel : public QgsMapLayerModel { Q_OBJECT @@ -51,7 +52,7 @@ class GUI_EXPORT QgsGeoPdfLayerTreeModel : public QgsLayerTreeModel }; //! constructor - QgsGeoPdfLayerTreeModel( QgsLayerTree *rootNode, QObject *parent = nullptr ); + QgsGeoPdfLayerTreeModel( const QList< QgsMapLayer * > &layers, QObject *parent = nullptr ); int columnCount( const QModelIndex &parent ) const override; QVariant headerData( int section, Qt::Orientation orientation, int role ) const override; diff --git a/src/gui/layout/qgslayoutpdfexportoptionsdialog.cpp b/src/gui/layout/qgslayoutpdfexportoptionsdialog.cpp index 0f8f7917ece..1cebbfac690 100644 --- a/src/gui/layout/qgslayoutpdfexportoptionsdialog.cpp +++ b/src/gui/layout/qgslayoutpdfexportoptionsdialog.cpp @@ -23,12 +23,13 @@ #include "qgsproject.h" #include "qgsmapthemecollection.h" #include "qgsgeopdflayertreemodel.h" +#include "qgslayertree.h" #include #include #include -QgsLayoutPdfExportOptionsDialog::QgsLayoutPdfExportOptionsDialog( QWidget *parent, bool allowGeoPdfExport, const QString &geoPdfReason, Qt::WindowFlags flags ) +QgsLayoutPdfExportOptionsDialog::QgsLayoutPdfExportOptionsDialog( QWidget *parent, bool allowGeoPdfExport, const QString &geoPdfReason, const QStringList &geoPdfLayerOrder, Qt::WindowFlags flags ) : QDialog( parent, flags ) { setupUi( this ); @@ -67,12 +68,30 @@ QgsLayoutPdfExportOptionsDialog::QgsLayoutPdfExportOptionsDialog( QWidget *paren mThemesList->addItem( item ); } - mGeoPdfStructureModel = new QgsGeoPdfLayerTreeModel( QgsProject::instance()->layerTreeRoot(), this ); + QList< QgsMapLayer * > order = QgsProject::instance()->layerTreeRoot()->layerOrder(); + for ( auto it = geoPdfLayerOrder.rbegin(); it != geoPdfLayerOrder.rend(); ++it ) + { + for ( int i = 0; i < order.size(); ++i ) + { + if ( order.at( i )->id() == *it ) + { + order.move( i, 0 ); + break; + } + } + } + mGeoPdfStructureModel = new QgsGeoPdfLayerTreeModel( order, this ); mGeoPdfStructureProxyModel = new QgsGeoPdfLayerFilteredTreeModel( mGeoPdfStructureModel, this ); mGeoPdfStructureTree->setModel( mGeoPdfStructureProxyModel ); mGeoPdfStructureTree->resizeColumnToContents( 0 ); mGeoPdfStructureTree->header()->show(); - mGeoPdfStructureTree->setSelectionMode( QAbstractItemView::NoSelection ); + mGeoPdfStructureTree->setSelectionMode( QAbstractItemView::SingleSelection ); + mGeoPdfStructureTree->setSelectionBehavior( QAbstractItemView::SelectRows ); + + mGeoPdfStructureTree->setDragEnabled( true ); + mGeoPdfStructureTree->setAcceptDrops( true ); + mGeoPdfStructureTree->setDragDropMode( QAbstractItemView::InternalMove ); + mGeoPdfStructureTree->setDefaultDropAction( Qt::MoveAction ); mGeoPdfStructureTree->setContextMenuPolicy( Qt::CustomContextMenu ); connect( mGeoPdfStructureTree, &QTreeView::customContextMenuRequested, this, [ = ]( const QPoint & point ) @@ -219,6 +238,16 @@ QStringList QgsLayoutPdfExportOptionsDialog::exportThemes() const return res; } +QStringList QgsLayoutPdfExportOptionsDialog::geoPdfLayerOrder() const +{ + QStringList order; + for ( int row = 0; row < mGeoPdfStructureProxyModel->rowCount(); ++row ) + { + order << mGeoPdfStructureProxyModel->data( mGeoPdfStructureProxyModel->index( row, 0 ), QgsGeoPdfLayerTreeModel::LayerIdRole ).toString(); + } + return order; +} + void QgsLayoutPdfExportOptionsDialog::showHelp() { QgsHelp::openHelp( QStringLiteral( "print_composer/create_output.html" ) ); diff --git a/src/gui/layout/qgslayoutpdfexportoptionsdialog.h b/src/gui/layout/qgslayoutpdfexportoptionsdialog.h index 273bde0d60a..0372bb3134a 100644 --- a/src/gui/layout/qgslayoutpdfexportoptionsdialog.h +++ b/src/gui/layout/qgslayoutpdfexportoptionsdialog.h @@ -48,11 +48,14 @@ class GUI_EXPORT QgsLayoutPdfExportOptionsDialog: public QDialog, private Ui::Qg * \param parent parent widget * \param allowGeoPdfExport set to FALSE if geoPdf export is blocked * \param geoPdfReason set to a descriptive translated string explaining why geopdf export is not available if applicable + * \param geoPdfLayerOrder optional layer ID order list for layers in the geopdf file. Any layers not present in this list + * will instead be appended to the end of the geopdf layer list * \param flags window flags */ QgsLayoutPdfExportOptionsDialog( QWidget *parent = nullptr, bool allowGeoPdfExport = true, const QString &geoPdfReason = QString(), + const QStringList &geoPdfLayerOrder = QStringList(), Qt::WindowFlags flags = nullptr ); //! Sets the text render format @@ -97,6 +100,9 @@ class GUI_EXPORT QgsLayoutPdfExportOptionsDialog: public QDialog, private Ui::Qg //! Returns the list of export themes QStringList exportThemes() const; + //! Returns a list of map layer IDs in the desired order they should appear in a generated GeoPDF file + QStringList geoPdfLayerOrder() const; + private slots: void showHelp(); diff --git a/src/ui/layout/qgspdfexportoptions.ui b/src/ui/layout/qgspdfexportoptions.ui index d6b66b5f1fe..69a0a0d7a24 100644 --- a/src/ui/layout/qgspdfexportoptions.ui +++ b/src/ui/layout/qgspdfexportoptions.ui @@ -101,7 +101,7 @@ 0 0 451 - 630 + 612 @@ -193,7 +193,7 @@ - Uncheck layers to avoid exporting vector feature information for those layers, and optionally set the group name to allow multiple layers to be joined into a single logical PDF group + Uncheck layers to avoid exporting vector feature information for those layers, and optionally set the group name to allow multiple layers to be joined into a single logical PDF group. Layers can be dragged and dropped to rearrange their order in the generated GeoPDF table of contents. true diff --git a/tests/src/core/testqgslayoutgeopdfexport.cpp b/tests/src/core/testqgslayoutgeopdfexport.cpp index 0fbe4e9b5b9..86343997eb3 100644 --- a/tests/src/core/testqgslayoutgeopdfexport.cpp +++ b/tests/src/core/testqgslayoutgeopdfexport.cpp @@ -43,6 +43,7 @@ class TestQgsLayoutGeoPdfExport : public QObject void cleanup();// will be called after every testfunction. void testCollectingFeatures(); void skipLayers(); + void layerOrder(); private: @@ -405,5 +406,45 @@ void TestQgsLayoutGeoPdfExport::skipLayers() QCOMPARE( polyFeatures.count(), 10 ); // should be features, layer did not have any setting set } +void TestQgsLayoutGeoPdfExport::layerOrder() +{ + QgsVectorLayer *linesLayer = new QgsVectorLayer( TEST_DATA_DIR + QStringLiteral( "/lines.shp" ), + QStringLiteral( "lines" ), QStringLiteral( "ogr" ) ); + QVERIFY( linesLayer->isValid() ); + QgsVectorLayer *pointsLayer = new QgsVectorLayer( TEST_DATA_DIR + QStringLiteral( "/points.shp" ), + QStringLiteral( "points" ), QStringLiteral( "ogr" ) ); + QVERIFY( pointsLayer->isValid() ); + QgsVectorLayer *polygonLayer = new QgsVectorLayer( TEST_DATA_DIR + QStringLiteral( "/polys.shp" ), + QStringLiteral( "polys" ), QStringLiteral( "ogr" ) ); + QVERIFY( polygonLayer->isValid() ); + pointsLayer->setDisplayExpression( QStringLiteral( "Staff" ) ); + + QgsProject p; + p.addMapLayer( linesLayer ); + p.addMapLayer( pointsLayer ); + p.addMapLayer( polygonLayer ); + + QgsLayout l( &p ); + l.initializeDefaults(); + QgsLayoutItemMap *map = new QgsLayoutItemMap( &l ); + map->attemptSetSceneRect( QRectF( 20, 20, 200, 100 ) ); + map->setFrameEnabled( true ); + map->setLayers( QList() << linesLayer << pointsLayer ); + map->setCrs( linesLayer->crs() ); + map->zoomToExtent( linesLayer->extent() ); + map->setBackgroundColor( QColor( 200, 220, 230 ) ); + map->setBackgroundEnabled( true ); + l.addLayoutItem( map ); + + QgsLayoutGeoPdfExporter geoPdfExporter( &l ); + // by default we should follow project layer order + QCOMPARE( geoPdfExporter.layerOrder(), QStringList() << polygonLayer->id() << pointsLayer->id() << linesLayer->id() ); + + // but if a custom order is specified, respected that + l.setCustomProperty( QStringLiteral( "pdfLayerOrder" ), QStringLiteral( "%1~~~%2" ).arg( linesLayer->id(), polygonLayer->id() ) ); + QgsLayoutGeoPdfExporter geoPdfExporter2( &l ); + QCOMPARE( geoPdfExporter2.layerOrder(), QStringList() << linesLayer->id() << polygonLayer->id() << pointsLayer->id() ); +} + QGSTEST_MAIN( TestQgsLayoutGeoPdfExport ) #include "testqgslayoutgeopdfexport.moc"