From bd7784cd0004535fb845882c59fe14edbf6cf761 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 19 Dec 2017 14:31:00 +1000 Subject: [PATCH 001/105] Start working on restoring atlas --- python/core/core_auto.sip | 1 + python/core/layout/qgslayoutatlas.sip | 269 +++++++++++++++++++++++++ src/core/CMakeLists.txt | 2 + src/core/layout/qgslayoutatlas.cpp | 188 +++++++++++++++++ src/core/layout/qgslayoutatlas.h | 278 ++++++++++++++++++++++++++ 5 files changed, 738 insertions(+) create mode 100644 python/core/layout/qgslayoutatlas.sip create mode 100644 src/core/layout/qgslayoutatlas.cpp create mode 100644 src/core/layout/qgslayoutatlas.h diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index d49f11808eee..669918ffa726 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -406,6 +406,7 @@ %Include gps/qgsnmeaconnection.sip %Include gps/qgsgpsdconnection.sip %Include layout/qgslayout.sip +%Include layout/qgslayoutatlas.sip %Include layout/qgslayoutcontext.sip %Include layout/qgslayouteffect.sip %Include layout/qgslayoutguidecollection.sip diff --git a/python/core/layout/qgslayoutatlas.sip b/python/core/layout/qgslayoutatlas.sip new file mode 100644 index 000000000000..d413b798b3e4 --- /dev/null +++ b/python/core/layout/qgslayoutatlas.sip @@ -0,0 +1,269 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layout/qgslayoutatlas.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + +class QgsLayoutAtlas : QObject +{ +%Docstring +Class used to render an Atlas, iterating over geometry features. +prepareForFeature() modifies the atlas map's extent to zoom on the given feature. +This class is used for printing, exporting to PDF and images. + +.. note:: + + This class should not be created directly. For the atlas to function correctly +the atlasComposition() property for QgsComposition should be used to retrieve a +QgsLayoutAtlas which is automatically created and attached to the composition. + +.. versionadded:: 3.0 +%End + +%TypeHeaderCode +#include "qgslayoutatlas.h" +%End + public: + + QgsLayoutAtlas( QgsLayout *layout ); +%Docstring +Constructor for new QgsLayoutAtlas. +%End + + bool enabled() const; +%Docstring +Returns whether the atlas generation is enabled + +.. seealso:: :py:func:`setEnabled()` +%End + + void setEnabled( bool enabled ); +%Docstring +Sets whether the atlas is ``enabled``. + +.. seealso:: :py:func:`enabled()` +%End + + bool hideCoverage() const; +%Docstring +Returns true if the atlas is set to hide the coverage layer. + +.. seealso:: :py:func:`setHideCoverage()` +%End + + void setHideCoverage( bool hide ); +%Docstring +Sets whether the coverage layer should be hidden in map items in the layouts. + +.. seealso:: :py:func:`hideCoverage()` +%End + + QString filenameExpression() const; +%Docstring +Returns the filename expression used for generating output filenames for each +atlas page. + +.. seealso:: :py:func:`setFilenameExpression()` + +.. seealso:: :py:func:`filenameExpressionErrorString()` +%End + + bool setFilenameExpression( const QString &expression, QString &errorString ); +%Docstring +Sets the filename ``expression`` used for generating output filenames for each +atlas page. +If an invalid expression is passed, false will be returned and ``errorString`` +will be set to the expression error. + +.. seealso:: :py:func:`filenameExpression()` +%End + + QgsVectorLayer *coverageLayer() const; +%Docstring +Returns the coverage layer used for the atlas features. + +.. seealso:: :py:func:`setCoverageLayer()` +%End + + void setCoverageLayer( QgsVectorLayer *layer ); +%Docstring +Sets the coverage ``layer`` to use for the atlas features. + +.. seealso:: :py:func:`coverageLayer()` +%End + + QString pageNameExpression() const; +%Docstring +Returns the expression (or field name) used for calculating the page name. + +.. seealso:: :py:func:`setPageNameExpression()` + +.. seealso:: :py:func:`nameForPage()` +%End + + void setPageNameExpression( const QString &expression ); +%Docstring +Sets the ``expression`` (or field name) used for calculating the page name. + +.. seealso:: :py:func:`pageNameExpression()` +%End + + QString nameForPage( int page ) const; +%Docstring +Returns the calculated name for a specified atlas ``page`` number. Page numbers start at 0. + +.. seealso:: :py:func:`pageNameExpression()` +%End + + bool sortFeatures() const; +%Docstring +Returns true if features should be sorted in the atlas. + +.. seealso:: :py:func:`setSortFeatures()` + +.. seealso:: :py:func:`sortAscending()` + +.. seealso:: :py:func:`sortExpression()` +%End + + void setSortFeatures( bool enabled ); +%Docstring +Sets whether features should be sorted in the atlas. + +.. seealso:: :py:func:`sortFeatures()` + +.. seealso:: :py:func:`setSortAscending()` + +.. seealso:: :py:func:`setSortExpression()` +%End + + bool sortAscending() const; +%Docstring +Returns true if features should be sorted in an ascending order. + +This property has no effect is sortFeatures() is false. + +.. seealso:: :py:func:`sortFeatures()` + +.. seealso:: :py:func:`setSortAscending()` + +.. seealso:: :py:func:`sortExpression()` +%End + + void setSortAscending( bool ascending ); +%Docstring +Sets whether features should be sorted in an ascending order. + +This property has no effect is sortFeatures() is false. + +.. seealso:: :py:func:`setSortFeatures()` + +.. seealso:: :py:func:`sortAscending()` + +.. seealso:: :py:func:`setSortExpression()` +%End + + QString sortExpression() const; +%Docstring +Returns the expression (or field name) to use for sorting features. + +This property has no effect is sortFeatures() is false. + +.. seealso:: :py:func:`sortFeatures()` + +.. seealso:: :py:func:`sortAscending()` + +.. seealso:: :py:func:`setSortExpression()` +%End + + void setSortExpression( const QString &expression ); +%Docstring +Sets the ``expression`` (or field name) to use for sorting features. + +This property has no effect is sortFeatures() is false. + +.. seealso:: :py:func:`setSortFeatures()` + +.. seealso:: :py:func:`setSortAscending()` + +.. seealso:: :py:func:`sortExpression()` +%End + + bool filterFeatures() const; +%Docstring +Returns true if features should be filtered in the coverage layer. + +.. seealso:: :py:func:`filterExpression()` + +.. seealso:: :py:func:`setFilterExpression()` +%End + + void setFilterFeatures( bool filtered ); +%Docstring +Sets whether features should be ``filtered`` in the coverage layer. + +.. seealso:: :py:func:`filterFeatures()` + +.. seealso:: :py:func:`setFilterExpression()` +%End + + QString filterExpression() const; +%Docstring +Returns the expression used for filtering features in the coverage layer. + +This property has no effect is filterFeatures() is false. + +.. seealso:: :py:func:`setFilterExpression()` + +.. seealso:: :py:func:`filterFeatures()` +%End + + bool setFilterExpression( const QString &expression, QString &errorString ); +%Docstring +Sets the ``expression`` used for filtering features in the coverage layer. + +This property has no effect is filterFeatures() is false. + +If an invalid expression is passed, false will be returned and ``errorString`` +will be set to the expression error. + +.. seealso:: :py:func:`filterExpression()` + +.. seealso:: :py:func:`setFilterFeatures()` +%End + + public slots: + + signals: + + void changed(); +%Docstring +Emitted when one of the atlas parameters changes. +%End + + void toggled( bool ); +%Docstring +Emitted when atlas is enabled or disabled. +%End + + void coverageLayerChanged( QgsVectorLayer *layer ); +%Docstring +Emitted when the coverage layer for the atlas changes. +%End + +}; + + + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layout/qgslayoutatlas.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index ab95bc81c5b2..4e06e54fb6fc 100755 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -366,6 +366,7 @@ SET(QGIS_CORE_SRCS layout/qgslayout.cpp layout/qgslayoutaligner.cpp + layout/qgslayoutatlas.cpp layout/qgslayoutcontext.cpp layout/qgslayouteffect.cpp layout/qgslayoutexporter.cpp @@ -737,6 +738,7 @@ SET(QGIS_CORE_MOC_HDRS gps/qgsgpsdconnection.h layout/qgslayout.h + layout/qgslayoutatlas.h layout/qgslayoutcontext.h layout/qgslayouteffect.h layout/qgslayoutguidecollection.h diff --git a/src/core/layout/qgslayoutatlas.cpp b/src/core/layout/qgslayoutatlas.cpp new file mode 100644 index 000000000000..97520cf6f5cc --- /dev/null +++ b/src/core/layout/qgslayoutatlas.cpp @@ -0,0 +1,188 @@ +/*************************************************************************** + qgslayoutatlas.cpp + ---------------- + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#include +#include +#include + +#include "qgslayoutatlas.h" +#include "qgslayout.h" + +QgsLayoutAtlas::QgsLayoutAtlas( QgsLayout *layout ) + : mLayout( layout ) + , mFilenameExpressionString( QStringLiteral( "'output_'||@atlas_featurenumber" ) ) +{ + + //listen out for layer removal + connect( mLayout->project(), static_cast < void ( QgsProject::* )( const QStringList & ) >( &QgsProject::layersWillBeRemoved ), this, &QgsLayoutAtlas::removeLayers ); +} + +void QgsLayoutAtlas::setEnabled( bool enabled ) +{ + if ( enabled == mEnabled ) + { + return; + } + + mEnabled = enabled; + emit toggled( enabled ); + emit changed(); +} + +void QgsLayoutAtlas::removeLayers( const QStringList &layers ) +{ + if ( !mCoverageLayer ) + { + return; + } + + for ( const QString &layerId : layers ) + { + if ( layerId == mCoverageLayer.layerId ) + { + //current coverage layer removed + mCoverageLayer.setLayer( nullptr ); + setEnabled( false ); + break; + } + } +} + +void QgsLayoutAtlas::setCoverageLayer( QgsVectorLayer *layer ) +{ + if ( layer == mCoverageLayer.get() ) + { + return; + } + + mCoverageLayer.setLayer( layer ); + emit coverageLayerChanged( layer ); +} + +QString QgsLayoutAtlas::nameForPage( int pageNumber ) const +{ +#if 0 //TODO + if ( pageNumber < 0 || pageNumber >= mFeatureIds.count() ) + return QString(); + + return mFeatureIds.at( pageNumber ).second; +#endif +} + +bool QgsLayoutAtlas::setFilterExpression( const QString &expression, QString &errorString ) +{ + mFilterExpression = expression; + return true; +} + +/// @cond PRIVATE +class AtlasFieldSorter +{ + public: + AtlasFieldSorter( QgsLayoutAtlas::SorterKeys &keys, bool ascending = true ) + : mKeys( keys ) + , mAscending( ascending ) + {} + + bool operator()( const QPair< QgsFeatureId, QString > &id1, const QPair< QgsFeatureId, QString > &id2 ) + { + return mAscending ? qgsVariantLessThan( mKeys.value( id1.first ), mKeys.value( id2.first ) ) + : qgsVariantGreaterThan( mKeys.value( id1.first ), mKeys.value( id2.first ) ); + } + + private: + QgsLayoutAtlas::SorterKeys &mKeys; + bool mAscending; +}; + +/// @endcond + +void QgsLayoutAtlas::setHideCoverage( bool hide ) +{ + mHideCoverage = hide; + +#if 0 //TODO + if ( mComposition->atlasMode() == QgsComposition::PreviewAtlas ) + { + //an atlas preview is enabled, so reflect changes in coverage layer visibility immediately + updateAtlasMaps(); + mComposition->update(); + } +#endif +} + +bool QgsLayoutAtlas::setFilenameExpression( const QString &pattern, QString &errorString ) +{ + mFilenameExpressionString = pattern; + return updateFilenameExpression( errorString ); +} + +QgsExpressionContext QgsLayoutAtlas::createExpressionContext() +{ + QgsExpressionContext expressionContext; + expressionContext << QgsExpressionContextUtils::globalScope(); + if ( mLayout ) + expressionContext << QgsExpressionContextUtils::projectScope( mLayout->project() ); +#if 0 //TODO + << QgsExpressionContextUtils::compositionScope( mLayout ); + + expressionContext.appendScope( QgsExpressionContextUtils::atlasScope( this ) ); +#endif + + if ( mCoverageLayer ) + expressionContext.lastScope()->setFields( mCoverageLayer->fields() ); +#if 0 //TODO + if ( mLayout && mEnabled ) + expressionContext.lastScope()->setFeature( mCurrentFeature ); +#endif + + return expressionContext; +} + +bool QgsLayoutAtlas::updateFilenameExpression( QString &error ) +{ + if ( !mCoverageLayer ) + { + return false; + } + + QgsExpressionContext expressionContext = createExpressionContext(); + + if ( !mFilenameExpressionString.isEmpty() ) + { + mFilenameExpression = QgsExpression( mFilenameExpressionString ); + // expression used to evaluate each filename + // test for evaluation errors + if ( mFilenameExpression.hasParserError() ) + { + error = mFilenameExpression.parserErrorString(); + return false; + } + + // prepare the filename expression + mFilenameExpression.prepare( &expressionContext ); + } + +#if 0 //TODO + //if atlas preview is currently enabled, regenerate filename for current feature + if ( mComposition->atlasMode() == QgsComposition::PreviewAtlas ) + { + evalFeatureFilename( expressionContext ); + } +#endif + return true; +} + diff --git a/src/core/layout/qgslayoutatlas.h b/src/core/layout/qgslayoutatlas.h new file mode 100644 index 000000000000..0ecb2f4b4a92 --- /dev/null +++ b/src/core/layout/qgslayoutatlas.h @@ -0,0 +1,278 @@ +/*************************************************************************** + qgslayoutatlas.h + ---------------- + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#ifndef QGSLAYOUTATLAS_H +#define QGSLAYOUTATLAS_H + +#include "qgis_core.h" +#include "qgsvectorlayerref.h" +#include + +class QgsLayout; + +/** + * \ingroup core + * Class used to render an Atlas, iterating over geometry features. + * prepareForFeature() modifies the atlas map's extent to zoom on the given feature. + * This class is used for printing, exporting to PDF and images. + * \note This class should not be created directly. For the atlas to function correctly + * the atlasComposition() property for QgsComposition should be used to retrieve a + * QgsLayoutAtlas which is automatically created and attached to the composition. + * \since QGIS 3.0 + */ +class CORE_EXPORT QgsLayoutAtlas : public QObject +{ + Q_OBJECT + public: + + /** + * Constructor for new QgsLayoutAtlas. + */ + QgsLayoutAtlas( QgsLayout *layout ); + + /** + * Returns whether the atlas generation is enabled + * \see setEnabled() + */ + bool enabled() const { return mEnabled; } + + /** + * Sets whether the atlas is \a enabled. + * \see enabled() + */ + void setEnabled( bool enabled ); + + /** + * Returns true if the atlas is set to hide the coverage layer. + * \see setHideCoverage() + */ + bool hideCoverage() const { return mHideCoverage; } + + /** + * Sets whether the coverage layer should be hidden in map items in the layouts. + * \see hideCoverage() + */ + void setHideCoverage( bool hide ); + + /** + * Returns the filename expression used for generating output filenames for each + * atlas page. + * \see setFilenameExpression() + * \see filenameExpressionErrorString() + */ + QString filenameExpression() const { return mFilenameExpressionString; } + + /** + * Sets the filename \a expression used for generating output filenames for each + * atlas page. + * If an invalid expression is passed, false will be returned and \a errorString + * will be set to the expression error. + * \see filenameExpression() + */ + bool setFilenameExpression( const QString &expression, QString &errorString ); + + /** + * Returns the coverage layer used for the atlas features. + * \see setCoverageLayer() + */ + QgsVectorLayer *coverageLayer() const { return mCoverageLayer.get(); } + + /** + * Sets the coverage \a layer to use for the atlas features. + * \see coverageLayer() + */ + void setCoverageLayer( QgsVectorLayer *layer ); + + /** + * Returns the expression (or field name) used for calculating the page name. + * \see setPageNameExpression() + * \see nameForPage() + */ + QString pageNameExpression() const { return mPageNameExpression; } + + /** + * Sets the \a expression (or field name) used for calculating the page name. + * \see pageNameExpression() + */ + void setPageNameExpression( const QString &expression ) { mPageNameExpression = expression; } + + /** + * Returns the calculated name for a specified atlas \a page number. Page numbers start at 0. + * \see pageNameExpression() + */ + QString nameForPage( int page ) const; + + /** + * Returns true if features should be sorted in the atlas. + * \see setSortFeatures() + * \see sortAscending() + * \see sortExpression() + */ + bool sortFeatures() const { return mSortFeatures; } + + /** + * Sets whether features should be sorted in the atlas. + * \see sortFeatures() + * \see setSortAscending() + * \see setSortExpression() + */ + void setSortFeatures( bool enabled ) { mSortFeatures = enabled; } + + /** + * Returns true if features should be sorted in an ascending order. + * + * This property has no effect is sortFeatures() is false. + * + * \see sortFeatures() + * \see setSortAscending() + * \see sortExpression() + */ + bool sortAscending() const { return mSortAscending; } + + /** + * Sets whether features should be sorted in an ascending order. + * + * This property has no effect is sortFeatures() is false. + * + * \see setSortFeatures() + * \see sortAscending() + * \see setSortExpression() + */ + void setSortAscending( bool ascending ) { mSortAscending = ascending; } + + /** + * Returns the expression (or field name) to use for sorting features. + * + * This property has no effect is sortFeatures() is false. + * + * \see sortFeatures() + * \see sortAscending() + * \see setSortExpression() + */ + QString sortExpression() const { return mSortExpression; } + + /** + * Sets the \a expression (or field name) to use for sorting features. + * + * This property has no effect is sortFeatures() is false. + * + * \see setSortFeatures() + * \see setSortAscending() + * \see sortExpression() + */ + void setSortExpression( const QString &expression ) { mSortExpression = expression; } + + /** + * Returns true if features should be filtered in the coverage layer. + * \see filterExpression() + * \see setFilterExpression() + */ + bool filterFeatures() const { return mFilterFeatures; } + + /** + * Sets whether features should be \a filtered in the coverage layer. + * \see filterFeatures() + * \see setFilterExpression() + */ + void setFilterFeatures( bool filtered ) { mFilterFeatures = filtered; } + + /** + * Returns the expression used for filtering features in the coverage layer. + * + * This property has no effect is filterFeatures() is false. + * + * \see setFilterExpression() + * \see filterFeatures() + */ + QString filterExpression() const { return mFilterExpression; } + + /** + * Sets the \a expression used for filtering features in the coverage layer. + * + * This property has no effect is filterFeatures() is false. + * + * If an invalid expression is passed, false will be returned and \a errorString + * will be set to the expression error. + * + * \see filterExpression() + * \see setFilterFeatures() + */ + bool setFilterExpression( const QString &expression, QString &errorString ); + + public slots: + + signals: + + //! Emitted when one of the atlas parameters changes. + void changed(); + + //! Emitted when atlas is enabled or disabled. + void toggled( bool ); + + //! Emitted when the coverage layer for the atlas changes. + void coverageLayerChanged( QgsVectorLayer *layer ); + + private slots: + void removeLayers( const QStringList &layers ); + + private: + + /** + * Updates the filename expression. + * \returns true if expression was successfully parsed, false if expression is invalid + */ + bool updateFilenameExpression( QString &error ); + + /** + * Evaluates filename for current feature + * \returns true if feature filename was successfully evaluated + */ + bool evalFeatureFilename( const QgsExpressionContext &context ); + + QPointer< QgsLayout > mLayout; + + bool mEnabled = false; + bool mHideCoverage = false; + QString mFilenameExpressionString; + + QgsExpression mFilenameExpression; + QgsVectorLayerRef mCoverageLayer; + + QString mCurrentFilename; + bool mSortFeatures = false; + bool mSortAscending = true; + + typedef QMap< QgsFeatureId, QVariant > SorterKeys; + // value of field that is used for ordering of features + SorterKeys mFeatureKeys; + + QString mSortExpression; + + QString mPageNameExpression; + + bool mFilterFeatures = false; + QString mFilterExpression; + + QString mFilterParserError; + + QgsExpressionContext createExpressionContext(); + + friend class AtlasFieldSorter; +}; + +#endif //QGSLAYOUTATLAS_H + + + From e6a6db89a179924bab102fdcac4c1e426be5d717 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 19 Dec 2017 14:53:11 +1000 Subject: [PATCH 002/105] Add QgsLayout subclass, QgsPrintLayout A print layout is a layout with an atlas --- python/core/core_auto.sip | 1 + python/core/layout/qgslayoutatlas.sip | 2 +- python/core/layout/qgsprintlayout.sip | 42 ++++++++++++++++++++++ src/core/CMakeLists.txt | 2 ++ src/core/layout/qgslayoutatlas.cpp | 3 +- src/core/layout/qgslayoutatlas.h | 2 +- src/core/layout/qgsprintlayout.cpp | 24 +++++++++++++ src/core/layout/qgsprintlayout.h | 52 +++++++++++++++++++++++++++ 8 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 python/core/layout/qgsprintlayout.sip create mode 100644 src/core/layout/qgsprintlayout.cpp create mode 100644 src/core/layout/qgsprintlayout.h diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index 669918ffa726..8e6b5225f07c 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -437,6 +437,7 @@ %Include layout/qgslayouttable.sip %Include layout/qgslayouttablecolumn.sip %Include layout/qgslayoutundostack.sip +%Include layout/qgsprintlayout.sip %Include symbology/qgscptcityarchive.sip %Include symbology/qgssvgcache.sip %Include symbology/qgsstyle.sip diff --git a/python/core/layout/qgslayoutatlas.sip b/python/core/layout/qgslayoutatlas.sip index d413b798b3e4..b9f239894db6 100644 --- a/python/core/layout/qgslayoutatlas.sip +++ b/python/core/layout/qgslayoutatlas.sip @@ -29,7 +29,7 @@ QgsLayoutAtlas which is automatically created and attached to the composition. %End public: - QgsLayoutAtlas( QgsLayout *layout ); + QgsLayoutAtlas( QgsLayout *layout /TransferThis/ ); %Docstring Constructor for new QgsLayoutAtlas. %End diff --git a/python/core/layout/qgsprintlayout.sip b/python/core/layout/qgsprintlayout.sip new file mode 100644 index 000000000000..628dc78b1770 --- /dev/null +++ b/python/core/layout/qgsprintlayout.sip @@ -0,0 +1,42 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layout/qgsprintlayout.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + +class QgsPrintLayout : QgsLayout +{ +%Docstring + Print layout, a QgsLayout subclass for static or atlas-based layouts. + +.. versionadded:: 3.0 +%End + +%TypeHeaderCode +#include "qgsprintlayout.h" +%End + public: + + QgsPrintLayout( QgsProject *project ); +%Docstring +Constructor for QgsPrintLayout. +%End + + QgsLayoutAtlas *atlas(); +%Docstring +Returns the print layout's atlas. +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layout/qgsprintlayout.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 4e06e54fb6fc..0b96b7255065 100755 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -412,6 +412,7 @@ SET(QGIS_CORE_SRCS layout/qgslayoutpoint.cpp layout/qgslayoutserializableobject.cpp layout/qgslayoutsize.cpp + layout/qgsprintlayout.cpp pal/costcalculator.cpp pal/feature.cpp @@ -770,6 +771,7 @@ SET(QGIS_CORE_MOC_HDRS layout/qgslayouttable.h layout/qgslayouttablecolumn.h layout/qgslayoutundostack.h + layout/qgsprintlayout.h symbology/qgscptcityarchive.h symbology/qgssvgcache.h diff --git a/src/core/layout/qgslayoutatlas.cpp b/src/core/layout/qgslayoutatlas.cpp index 97520cf6f5cc..7d2ae84fca2d 100644 --- a/src/core/layout/qgslayoutatlas.cpp +++ b/src/core/layout/qgslayoutatlas.cpp @@ -22,7 +22,8 @@ #include "qgslayout.h" QgsLayoutAtlas::QgsLayoutAtlas( QgsLayout *layout ) - : mLayout( layout ) + : QObject( layout ) + , mLayout( layout ) , mFilenameExpressionString( QStringLiteral( "'output_'||@atlas_featurenumber" ) ) { diff --git a/src/core/layout/qgslayoutatlas.h b/src/core/layout/qgslayoutatlas.h index 0ecb2f4b4a92..93a9380d9b63 100644 --- a/src/core/layout/qgslayoutatlas.h +++ b/src/core/layout/qgslayoutatlas.h @@ -40,7 +40,7 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject /** * Constructor for new QgsLayoutAtlas. */ - QgsLayoutAtlas( QgsLayout *layout ); + QgsLayoutAtlas( QgsLayout *layout SIP_TRANSFERTHIS ); /** * Returns whether the atlas generation is enabled diff --git a/src/core/layout/qgsprintlayout.cpp b/src/core/layout/qgsprintlayout.cpp new file mode 100644 index 000000000000..7d2d2e7f3935 --- /dev/null +++ b/src/core/layout/qgsprintlayout.cpp @@ -0,0 +1,24 @@ +/*************************************************************************** + qgsprintlayout.cpp + ------------------- + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsprintlayout.h" +#include "qgslayoutatlas.h" + +QgsPrintLayout::QgsPrintLayout( QgsProject *project ) + : QgsLayout( project ) + , mAtlas( new QgsLayoutAtlas( this ) ) +{ +} diff --git a/src/core/layout/qgsprintlayout.h b/src/core/layout/qgsprintlayout.h new file mode 100644 index 000000000000..a3c0065ff1ea --- /dev/null +++ b/src/core/layout/qgsprintlayout.h @@ -0,0 +1,52 @@ +/*************************************************************************** + qgsprintlayout.h + ------------------- + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#ifndef QGSPRINTLAYOUT_H +#define QGSPRINTLAYOUT_H + +#include "qgis_core.h" +#include "qgslayout.h" + +class QgsLayoutAtlas; + +/** + * \ingroup core + * \class QgsPrintLayout + * \brief Print layout, a QgsLayout subclass for static or atlas-based layouts. + * \since QGIS 3.0 + */ +class CORE_EXPORT QgsPrintLayout : public QgsLayout +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsPrintLayout. + */ + QgsPrintLayout( QgsProject *project ); + + /** + * Returns the print layout's atlas. + */ + QgsLayoutAtlas *atlas(); + + private: + + QgsLayoutAtlas *mAtlas = nullptr; + +}; + +#endif //QGSPRINTLAYOUT_H From 83af35275e6aedf4bc7045418e8b8218472004db Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 19 Dec 2017 14:59:32 +1000 Subject: [PATCH 003/105] The layouts currently stored with a project are QgsPrintLayouts In future other layout types will be stored in projects, but for now we only have print layouts --- src/core/composer/qgslayoutmanager.cpp | 3 ++- src/core/layout/qgsprintlayout.cpp | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/core/composer/qgslayoutmanager.cpp b/src/core/composer/qgslayoutmanager.cpp index 15166eb66f76..b4942b78c33b 100644 --- a/src/core/composer/qgslayoutmanager.cpp +++ b/src/core/composer/qgslayoutmanager.cpp @@ -18,6 +18,7 @@ #include "qgsproject.h" #include "qgslogger.h" #include "qgslayoutundostack.h" +#include "qgsprintlayout.h" QgsLayoutManager::QgsLayoutManager( QgsProject *project ) : QObject( project ) @@ -193,7 +194,7 @@ bool QgsLayoutManager::readXml( const QDomElement &element, const QDomDocument & const QDomNodeList layoutNodes = element.elementsByTagName( QStringLiteral( "Layout" ) ); for ( int i = 0; i < layoutNodes.size(); ++i ) { - std::unique_ptr< QgsLayout > l = qgis::make_unique< QgsLayout >( mProject ); + std::unique_ptr< QgsLayout > l = qgis::make_unique< QgsPrintLayout >( mProject ); l->undoStack()->blockCommands( true ); if ( !l->readXml( layoutNodes.at( i ).toElement(), doc, context ) ) { diff --git a/src/core/layout/qgsprintlayout.cpp b/src/core/layout/qgsprintlayout.cpp index 7d2d2e7f3935..1d1c031157b5 100644 --- a/src/core/layout/qgsprintlayout.cpp +++ b/src/core/layout/qgsprintlayout.cpp @@ -22,3 +22,8 @@ QgsPrintLayout::QgsPrintLayout( QgsProject *project ) , mAtlas( new QgsLayoutAtlas( this ) ) { } + +QgsLayoutAtlas *QgsPrintLayout::atlas() +{ + return mAtlas; +} From f86c2988bb16ea5ba0ce197c90416b5e332e728f Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 19 Dec 2017 15:31:15 +1000 Subject: [PATCH 004/105] Serialize atlas settings --- python/core/layout/qgslayout.sip | 4 +- python/core/layout/qgslayoutatlas.sip | 15 +++- python/core/layout/qgsprintlayout.sip | 5 ++ src/core/layout/qgslayout.h | 4 +- src/core/layout/qgslayoutatlas.cpp | 78 +++++++++++++++++++++ src/core/layout/qgslayoutatlas.h | 12 +++- src/core/layout/qgsprintlayout.cpp | 17 +++++ src/core/layout/qgsprintlayout.h | 3 + tests/src/python/CMakeLists.txt | 1 + tests/src/python/test_qgslayoutatlas.py | 91 +++++++++++++++++++++++++ 10 files changed, 220 insertions(+), 10 deletions(-) create mode 100644 tests/src/python/test_qgslayoutatlas.py diff --git a/python/core/layout/qgslayout.sip b/python/core/layout/qgslayout.sip index da9da711a1f0..899567e42c22 100644 --- a/python/core/layout/qgslayout.sip +++ b/python/core/layout/qgslayout.sip @@ -500,14 +500,14 @@ If ``ok`` is specified, it will be set to true if the load was successful. Returns a list of loaded items. %End - QDomElement writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const; + virtual QDomElement writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const; %Docstring Returns the layout's state encapsulated in a DOM element. .. seealso:: :py:func:`readXml()` %End - bool readXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ); + virtual bool readXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ); %Docstring Sets the collection's state from a DOM element. ``layoutElement`` is the DOM node corresponding to the layout. diff --git a/python/core/layout/qgslayoutatlas.sip b/python/core/layout/qgslayoutatlas.sip index b9f239894db6..c713b8043a33 100644 --- a/python/core/layout/qgslayoutatlas.sip +++ b/python/core/layout/qgslayoutatlas.sip @@ -8,7 +8,7 @@ -class QgsLayoutAtlas : QObject +class QgsLayoutAtlas : QObject, QgsLayoutSerializableObject { %Docstring Class used to render an Atlas, iterating over geometry features. @@ -34,6 +34,15 @@ QgsLayoutAtlas which is automatically created and attached to the composition. Constructor for new QgsLayoutAtlas. %End + virtual QString stringType() const; + + virtual QgsLayout *layout(); + + virtual bool writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const; + + virtual bool readXml( const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context ); + + bool enabled() const; %Docstring Returns whether the atlas generation is enabled @@ -72,7 +81,7 @@ atlas page. .. seealso:: :py:func:`filenameExpressionErrorString()` %End - bool setFilenameExpression( const QString &expression, QString &errorString ); + bool setFilenameExpression( const QString &expression, QString &errorString /Out/ ); %Docstring Sets the filename ``expression`` used for generating output filenames for each atlas page. @@ -222,7 +231,7 @@ This property has no effect is filterFeatures() is false. .. seealso:: :py:func:`filterFeatures()` %End - bool setFilterExpression( const QString &expression, QString &errorString ); + bool setFilterExpression( const QString &expression, QString &errorString /Out/ ); %Docstring Sets the ``expression`` used for filtering features in the coverage layer. diff --git a/python/core/layout/qgsprintlayout.sip b/python/core/layout/qgsprintlayout.sip index 628dc78b1770..8a667fd564f5 100644 --- a/python/core/layout/qgsprintlayout.sip +++ b/python/core/layout/qgsprintlayout.sip @@ -31,6 +31,11 @@ Constructor for QgsPrintLayout. Returns the print layout's atlas. %End + virtual QDomElement writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const; + + virtual bool readXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ); + + }; /************************************************************************ diff --git a/src/core/layout/qgslayout.h b/src/core/layout/qgslayout.h index 737f595de3ed..326d9ba7c21f 100644 --- a/src/core/layout/qgslayout.h +++ b/src/core/layout/qgslayout.h @@ -514,13 +514,13 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext * Returns the layout's state encapsulated in a DOM element. * \see readXml() */ - QDomElement writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const; + virtual QDomElement writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const; /** * Sets the collection's state from a DOM element. \a layoutElement is the DOM node corresponding to the layout. * \see writeXml() */ - bool readXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ); + virtual bool readXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ); /** * Add items from an XML representation to the layout. Used for project file reading and pasting items from clipboard. diff --git a/src/core/layout/qgslayoutatlas.cpp b/src/core/layout/qgslayoutatlas.cpp index 7d2ae84fca2d..3f0d9a40ed06 100644 --- a/src/core/layout/qgslayoutatlas.cpp +++ b/src/core/layout/qgslayoutatlas.cpp @@ -31,6 +31,84 @@ QgsLayoutAtlas::QgsLayoutAtlas( QgsLayout *layout ) connect( mLayout->project(), static_cast < void ( QgsProject::* )( const QStringList & ) >( &QgsProject::layersWillBeRemoved ), this, &QgsLayoutAtlas::removeLayers ); } +QString QgsLayoutAtlas::stringType() const +{ + return QStringLiteral( "atlas" ); +} + +QgsLayout *QgsLayoutAtlas::layout() +{ + return mLayout; +} + +bool QgsLayoutAtlas::writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext & ) const +{ + QDomElement atlasElem = document.createElement( QStringLiteral( "Atlas" ) ); + atlasElem.setAttribute( QStringLiteral( "enabled" ), mEnabled ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); + + if ( mCoverageLayer ) + { + atlasElem.setAttribute( QStringLiteral( "coverageLayer" ), mCoverageLayer.layerId ); + atlasElem.setAttribute( QStringLiteral( "coverageLayerName" ), mCoverageLayer.name ); + atlasElem.setAttribute( QStringLiteral( "coverageLayerSource" ), mCoverageLayer.source ); + atlasElem.setAttribute( QStringLiteral( "coverageLayerProvider" ), mCoverageLayer.provider ); + } + else + { + atlasElem.setAttribute( QStringLiteral( "coverageLayer" ), QString() ); + } + + atlasElem.setAttribute( QStringLiteral( "hideCoverage" ), mHideCoverage ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); + atlasElem.setAttribute( QStringLiteral( "filenamePattern" ), mFilenameExpressionString ); + atlasElem.setAttribute( QStringLiteral( "pageNameExpression" ), mPageNameExpression ); + + atlasElem.setAttribute( QStringLiteral( "sortFeatures" ), mSortFeatures ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); + if ( mSortFeatures ) + { + atlasElem.setAttribute( QStringLiteral( "sortKey" ), mSortExpression ); + atlasElem.setAttribute( QStringLiteral( "sortAscending" ), mSortAscending ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); + } + atlasElem.setAttribute( QStringLiteral( "filterFeatures" ), mFilterFeatures ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); + if ( mFilterFeatures ) + { + atlasElem.setAttribute( QStringLiteral( "featureFilter" ), mFilterExpression ); + } + + parentElement.appendChild( atlasElem ); + + return true; +} + +bool QgsLayoutAtlas::readXml( const QDomElement &atlasElem, const QDomDocument &, const QgsReadWriteContext & ) +{ + mEnabled = atlasElem.attribute( QStringLiteral( "enabled" ), QStringLiteral( "0" ) ).toInt(); + + // look for stored layer name + QString layerId = atlasElem.attribute( QStringLiteral( "coverageLayer" ) ); + QString layerName = atlasElem.attribute( QStringLiteral( "coverageLayerName" ) ); + QString layerSource = atlasElem.attribute( QStringLiteral( "coverageLayerSource" ) ); + QString layerProvider = atlasElem.attribute( QStringLiteral( "coverageLayerProvider" ) ); + + mCoverageLayer = QgsVectorLayerRef( layerId, layerName, layerSource, layerProvider ); + mCoverageLayer.resolveWeakly( mLayout->project() ); + + mPageNameExpression = atlasElem.attribute( QStringLiteral( "pageNameExpression" ), QString() ); + QString error; + setFilenameExpression( atlasElem.attribute( QStringLiteral( "filenamePattern" ), QString() ), error ); + + mSortFeatures = atlasElem.attribute( QStringLiteral( "sortFeatures" ), QStringLiteral( "0" ) ).toInt(); + mSortExpression = atlasElem.attribute( QStringLiteral( "sortKey" ) ); + mSortAscending = atlasElem.attribute( QStringLiteral( "sortAscending" ), QStringLiteral( "1" ) ).toInt(); + mFilterFeatures = atlasElem.attribute( QStringLiteral( "filterFeatures" ), QStringLiteral( "0" ) ).toInt(); + mFilterExpression = atlasElem.attribute( QStringLiteral( "featureFilter" ) ); + + mHideCoverage = atlasElem.attribute( QStringLiteral( "hideCoverage" ), QStringLiteral( "0" ) ).toInt(); + + emit toggled( mEnabled ); + emit changed(); + return true; +} + void QgsLayoutAtlas::setEnabled( bool enabled ) { if ( enabled == mEnabled ) diff --git a/src/core/layout/qgslayoutatlas.h b/src/core/layout/qgslayoutatlas.h index 93a9380d9b63..6e740a3f7044 100644 --- a/src/core/layout/qgslayoutatlas.h +++ b/src/core/layout/qgslayoutatlas.h @@ -18,6 +18,7 @@ #include "qgis_core.h" #include "qgsvectorlayerref.h" +#include "qgslayoutserializableobject.h" #include class QgsLayout; @@ -32,7 +33,7 @@ class QgsLayout; * QgsLayoutAtlas which is automatically created and attached to the composition. * \since QGIS 3.0 */ -class CORE_EXPORT QgsLayoutAtlas : public QObject +class CORE_EXPORT QgsLayoutAtlas : public QObject, public QgsLayoutSerializableObject { Q_OBJECT public: @@ -42,6 +43,11 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject */ QgsLayoutAtlas( QgsLayout *layout SIP_TRANSFERTHIS ); + QString stringType() const override; + QgsLayout *layout() override; + bool writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const override; + bool readXml( const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context ) override; + /** * Returns whether the atlas generation is enabled * \see setEnabled() @@ -81,7 +87,7 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject * will be set to the expression error. * \see filenameExpression() */ - bool setFilenameExpression( const QString &expression, QString &errorString ); + bool setFilenameExpression( const QString &expression, QString &errorString SIP_OUT ); /** * Returns the coverage layer used for the atlas features. @@ -209,7 +215,7 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject * \see filterExpression() * \see setFilterFeatures() */ - bool setFilterExpression( const QString &expression, QString &errorString ); + bool setFilterExpression( const QString &expression, QString &errorString SIP_OUT ); public slots: diff --git a/src/core/layout/qgsprintlayout.cpp b/src/core/layout/qgsprintlayout.cpp index 1d1c031157b5..a596054ed409 100644 --- a/src/core/layout/qgsprintlayout.cpp +++ b/src/core/layout/qgsprintlayout.cpp @@ -27,3 +27,20 @@ QgsLayoutAtlas *QgsPrintLayout::atlas() { return mAtlas; } + +QDomElement QgsPrintLayout::writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const +{ + QDomElement layoutElem = QgsLayout::writeXml( document, context ); + mAtlas->writeXml( layoutElem, document, context ); + return layoutElem; +} + +bool QgsPrintLayout::readXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ) +{ + if ( !QgsLayout::readXml( layoutElement, document, context ) ) + return false; + + QDomElement atlasElem = layoutElement.firstChildElement( QStringLiteral( "Atlas" ) ); + mAtlas->readXml( atlasElem, document, context ); + return true; +} diff --git a/src/core/layout/qgsprintlayout.h b/src/core/layout/qgsprintlayout.h index a3c0065ff1ea..b72318577322 100644 --- a/src/core/layout/qgsprintlayout.h +++ b/src/core/layout/qgsprintlayout.h @@ -43,6 +43,9 @@ class CORE_EXPORT QgsPrintLayout : public QgsLayout */ QgsLayoutAtlas *atlas(); + QDomElement writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const override; + bool readXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ) override; + private: QgsLayoutAtlas *mAtlas = nullptr; diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index c673955aea6b..39c76e0e85e3 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -84,6 +84,7 @@ ADD_PYTHON_TEST(PyQgsLayerTreeMapCanvasBridge test_qgslayertreemapcanvasbridge.p ADD_PYTHON_TEST(PyQgsLayerTree test_qgslayertree.py) ADD_PYTHON_TEST(PyQgsLayout test_qgslayout.py) ADD_PYTHON_TEST(PyQgsLayoutAlign test_qgslayoutaligner.py) +ADD_PYTHON_TEST(PyQgsLayoutAtlas test_qgslayoutatlas.py) ADD_PYTHON_TEST(PyQgsLayoutExporter test_qgslayoutexporter.py) ADD_PYTHON_TEST(PyQgsLayoutFrame test_qgslayoutframe.py) ADD_PYTHON_TEST(PyQgsLayoutManager test_qgslayoutmanager.py) diff --git a/tests/src/python/test_qgslayoutatlas.py b/tests/src/python/test_qgslayoutatlas.py new file mode 100644 index 000000000000..ac2623761a27 --- /dev/null +++ b/tests/src/python/test_qgslayoutatlas.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsLayoutAtlas + +.. note:: This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +""" +__author__ = 'Nyall Dawson' +__date__ = '19/12/2017' +__copyright__ = 'Copyright 2017, The QGIS Project' +# This will get replaced with a git SHA1 when you do a git archive +__revision__ = '$Format:%H$' + +import qgis # NOQA +import sip +import tempfile +import shutil +import os + +from qgis.core import (QgsUnitTypes, + QgsLayout, + QgsPrintLayout, + QgsLayoutAtlas, + QgsLayoutItemPage, + QgsLayoutGuide, + QgsLayoutObject, + QgsProject, + QgsLayoutItemGroup, + QgsLayoutItem, + QgsProperty, + QgsLayoutPageCollection, + QgsLayoutMeasurement, + QgsFillSymbol, + QgsReadWriteContext, + QgsLayoutItemMap, + QgsLayoutItemLabel, + QgsLayoutSize, + QgsLayoutPoint, + QgsVectorLayer) +from qgis.PyQt.QtCore import QFileInfo +from qgis.PyQt.QtTest import QSignalSpy +from qgis.PyQt.QtXml import QDomDocument +from utilities import unitTestDataPath +from qgis.testing import start_app, unittest + +start_app() + + +class TestQgsLayoutAtlas(unittest.TestCase): + + def testReadWriteXml(self): + p = QgsProject() + vectorFileInfo = QFileInfo(unitTestDataPath() + "/france_parts.shp") + vector_layer = QgsVectorLayer(vectorFileInfo.filePath(), vectorFileInfo.completeBaseName(), "ogr") + self.assertTrue(vector_layer.isValid()) + p.addMapLayer(vector_layer) + + l = QgsPrintLayout(p) + atlas = l.atlas() + atlas.setEnabled(True) + atlas.setHideCoverage(True) + atlas.setFilenameExpression('filename exp') + atlas.setCoverageLayer(vector_layer) + atlas.setPageNameExpression('page name') + atlas.setSortFeatures(True) + atlas.setSortAscending(False) + atlas.setSortExpression('sort exp') + atlas.setFilterFeatures(True) + atlas.setFilterExpression('filter exp') + + doc = QDomDocument("testdoc") + elem = l.writeXml(doc, QgsReadWriteContext()) + + l2 = QgsPrintLayout(p) + self.assertTrue(l2.readXml(elem, doc, QgsReadWriteContext())) + atlas2 = l2.atlas() + self.assertTrue(atlas2.enabled()) + self.assertTrue(atlas2.hideCoverage()) + self.assertEqual(atlas2.filenameExpression(), 'filename exp') + self.assertEqual(atlas2.coverageLayer(), vector_layer) + self.assertEqual(atlas2.pageNameExpression(), 'page name') + self.assertTrue(atlas2.sortFeatures()) + self.assertFalse(atlas2.sortAscending()) + self.assertEqual(atlas2.sortExpression(), 'sort exp') + self.assertTrue(atlas2.filterFeatures()) + self.assertEqual(atlas2.filterExpression(), 'filter exp') + + +if __name__ == '__main__': + unittest.main() From 25170da03f3dcec24df983987bcc8c64cc480a77 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 19 Dec 2017 15:46:27 +1000 Subject: [PATCH 005/105] Start porting atlas GUI --- src/app/CMakeLists.txt | 2 + src/app/layout/qgslayoutatlaswidget.cpp | 343 +++++++++++++++++ src/app/layout/qgslayoutatlaswidget.h | 61 ++++ src/app/layout/qgslayoutdesignerdialog.cpp | 23 ++ src/app/layout/qgslayoutdesignerdialog.h | 2 + src/ui/layout/qgslayoutatlaswidgetbase.ui | 404 +++++++++++++++++++++ 6 files changed, 835 insertions(+) create mode 100644 src/app/layout/qgslayoutatlaswidget.cpp create mode 100644 src/app/layout/qgslayoutatlaswidget.h create mode 100644 src/ui/layout/qgslayoutatlaswidgetbase.ui diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 481a8dec71e1..744a4208a10a 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -180,6 +180,7 @@ SET(QGIS_APP_SRCS layout/qgslayoutaddpagesdialog.cpp layout/qgslayoutapputils.cpp + layout/qgslayoutatlaswidget.cpp layout/qgslayoutattributeselectiondialog.cpp layout/qgslayoutattributetablewidget.cpp layout/qgslayoutdesignerdialog.cpp @@ -399,6 +400,7 @@ SET (QGIS_APP_MOC_HDRS layout/qgslayoutaddpagesdialog.h layout/qgslayoutappmenuprovider.h + layout/qgslayoutatlaswidget.h layout/qgslayoutattributeselectiondialog.h layout/qgslayoutattributetablewidget.h layout/qgslayoutdesignerdialog.h diff --git a/src/app/layout/qgslayoutatlaswidget.cpp b/src/app/layout/qgslayoutatlaswidget.cpp new file mode 100644 index 000000000000..1c7933bd31c4 --- /dev/null +++ b/src/app/layout/qgslayoutatlaswidget.cpp @@ -0,0 +1,343 @@ +/*************************************************************************** + qgslayoutatlaswidget.cpp + ----------------------------- + begin : October 2012 + copyright : (C) 2012 Hugo Mercier + email : hugo dot mercier at oslandia dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include +#include +#include + +#include "qgslayoutatlaswidget.h" +#include "qgsprintlayout.h" +#include "qgslayoutatlas.h" +#include "qgsexpressionbuilderdialog.h" + +QgsLayoutAtlasWidget::QgsLayoutAtlasWidget( QWidget *parent, QgsPrintLayout *layout ) + : QWidget( parent ) + , mAtlas( layout->atlas() ) +{ + setupUi( this ); + connect( mUseAtlasCheckBox, &QCheckBox::stateChanged, this, &QgsLayoutAtlasWidget::mUseAtlasCheckBox_stateChanged ); + connect( mAtlasFilenamePatternEdit, &QLineEdit::editingFinished, this, &QgsLayoutAtlasWidget::mAtlasFilenamePatternEdit_editingFinished ); + connect( mAtlasFilenameExpressionButton, &QToolButton::clicked, this, &QgsLayoutAtlasWidget::mAtlasFilenameExpressionButton_clicked ); + connect( mAtlasHideCoverageCheckBox, &QCheckBox::stateChanged, this, &QgsLayoutAtlasWidget::mAtlasHideCoverageCheckBox_stateChanged ); + connect( mAtlasSingleFileCheckBox, &QCheckBox::stateChanged, this, &QgsLayoutAtlasWidget::mAtlasSingleFileCheckBox_stateChanged ); + connect( mAtlasSortFeatureCheckBox, &QCheckBox::stateChanged, this, &QgsLayoutAtlasWidget::mAtlasSortFeatureCheckBox_stateChanged ); + connect( mAtlasSortFeatureDirectionButton, &QToolButton::clicked, this, &QgsLayoutAtlasWidget::mAtlasSortFeatureDirectionButton_clicked ); + connect( mAtlasFeatureFilterEdit, &QLineEdit::editingFinished, this, &QgsLayoutAtlasWidget::mAtlasFeatureFilterEdit_editingFinished ); + connect( mAtlasFeatureFilterButton, &QToolButton::clicked, this, &QgsLayoutAtlasWidget::mAtlasFeatureFilterButton_clicked ); + connect( mAtlasFeatureFilterCheckBox, &QCheckBox::stateChanged, this, &QgsLayoutAtlasWidget::mAtlasFeatureFilterCheckBox_stateChanged ); + + mAtlasCoverageLayerComboBox->setFilters( QgsMapLayerProxyModel::VectorLayer ); + + connect( mAtlasCoverageLayerComboBox, &QgsMapLayerComboBox::layerChanged, mAtlasSortFeatureKeyComboBox, &QgsFieldComboBox::setLayer ); + connect( mAtlasCoverageLayerComboBox, &QgsMapLayerComboBox::layerChanged, mPageNameWidget, &QgsFieldExpressionWidget::setLayer ); + connect( mAtlasCoverageLayerComboBox, &QgsMapLayerComboBox::layerChanged, this, &QgsLayoutAtlasWidget::changeCoverageLayer ); + connect( mAtlasSortFeatureKeyComboBox, &QgsFieldComboBox::fieldChanged, this, &QgsLayoutAtlasWidget::changesSortFeatureField ); + connect( mPageNameWidget, static_cast < void ( QgsFieldExpressionWidget::* )( const QString &, bool ) > ( &QgsFieldExpressionWidget::fieldChanged ), this, &QgsLayoutAtlasWidget::pageNameExpressionChanged ); + + // Sort direction + mAtlasSortFeatureDirectionButton->setEnabled( false ); + mAtlasSortFeatureKeyComboBox->setEnabled( false ); + + // connect to updates + connect( mAtlas, &QgsLayoutAtlas::changed, this, &QgsLayoutAtlasWidget::updateGuiElements ); + + mPageNameWidget->registerExpressionContextGenerator( mLayout ); + + QList formats = QImageWriter::supportedImageFormats(); + for ( int i = 0; i < formats.size(); ++i ) + { + mAtlasFileFormat->addItem( QString( formats.at( i ) ) ); + } + connect( mAtlasFileFormat, static_cast( &QComboBox::currentIndexChanged ), this, [ = ]( int ) { changeFileFormat(); } ); + + updateGuiElements(); +} + +void QgsLayoutAtlasWidget::mUseAtlasCheckBox_stateChanged( int state ) +{ + if ( state == Qt::Checked ) + { + mAtlas->setEnabled( true ); + mConfigurationGroup->setEnabled( true ); + mOutputGroup->setEnabled( true ); + } + else + { + mAtlas->setEnabled( false ); + mConfigurationGroup->setEnabled( false ); + mOutputGroup->setEnabled( false ); + } +} + +void QgsLayoutAtlasWidget::changeCoverageLayer( QgsMapLayer *layer ) +{ + QgsVectorLayer *vl = dynamic_cast( layer ); + + if ( !vl ) + { + mAtlas->setCoverageLayer( nullptr ); + } + else + { + mAtlas->setCoverageLayer( vl ); + updateAtlasFeatures(); + } +} + +void QgsLayoutAtlasWidget::mAtlasFilenamePatternEdit_editingFinished() +{ + QString error; + if ( !mAtlas->setFilenameExpression( mAtlasFilenamePatternEdit->text(), error ) ) + { + //expression could not be set + QMessageBox::warning( this + , tr( "Could not evaluate filename pattern" ) + , tr( "Could not set filename pattern as '%1'.\nParser error:\n%2" ) + .arg( mAtlasFilenamePatternEdit->text(), + error ) + ); + } +} + +void QgsLayoutAtlasWidget::mAtlasFilenameExpressionButton_clicked() +{ + if ( !mAtlas || !mAtlas->coverageLayer() ) + { + return; + } + + QgsExpressionContext context = mLayout->createExpressionContext(); + QgsExpressionBuilderDialog exprDlg( mAtlas->coverageLayer(), mAtlasFilenamePatternEdit->text(), this, QStringLiteral( "generic" ), context ); + exprDlg.setWindowTitle( tr( "Expression Based Filename" ) ); + + if ( exprDlg.exec() == QDialog::Accepted ) + { + QString expression = exprDlg.expressionText(); + if ( !expression.isEmpty() ) + { + //set atlas filename expression + mAtlasFilenamePatternEdit->setText( expression ); + QString error; + if ( !mAtlas->setFilenameExpression( expression, error ) ) + { + //expression could not be set + QMessageBox::warning( this + , tr( "Could not evaluate filename pattern" ) + , tr( "Could not set filename pattern as '%1'.\nParser error:\n%2" ) + .arg( expression, + error ) + ); + } + } + } +} + +void QgsLayoutAtlasWidget::mAtlasHideCoverageCheckBox_stateChanged( int state ) +{ + mAtlas->setHideCoverage( state == Qt::Checked ); +} + +void QgsLayoutAtlasWidget::mAtlasSingleFileCheckBox_stateChanged( int state ) +{ + if ( state == Qt::Checked ) + { + mAtlasFilenamePatternEdit->setEnabled( false ); + mAtlasFilenameExpressionButton->setEnabled( false ); + } + else + { + mAtlasFilenamePatternEdit->setEnabled( true ); + mAtlasFilenameExpressionButton->setEnabled( true ); + } +#if 0 //TODO + mAtlas->setSingleFile( state == Qt::Checked ); +#endif +} + +void QgsLayoutAtlasWidget::mAtlasSortFeatureCheckBox_stateChanged( int state ) +{ + if ( state == Qt::Checked ) + { + mAtlasSortFeatureDirectionButton->setEnabled( true ); + mAtlasSortFeatureKeyComboBox->setEnabled( true ); + } + else + { + mAtlasSortFeatureDirectionButton->setEnabled( false ); + mAtlasSortFeatureKeyComboBox->setEnabled( false ); + } + mAtlas->setSortFeatures( state == Qt::Checked ); + updateAtlasFeatures(); +} + +void QgsLayoutAtlasWidget::updateAtlasFeatures() +{ +#if 0 //TODO + bool updated = mAtlas->updateFeatures(); + if ( !updated ) + { + QMessageBox::warning( nullptr, tr( "Atlas preview" ), + tr( "No matching atlas features found!" ), + QMessageBox::Ok, + QMessageBox::Ok ); + + //Perhaps atlas preview should be disabled now? If so, it may get annoying if user is editing + //the filter expression and it keeps disabling itself. + return; + } +#endif +} + +void QgsLayoutAtlasWidget::changesSortFeatureField( const QString &fieldName ) +{ + mAtlas->setSortExpression( fieldName ); + updateAtlasFeatures(); +} + +void QgsLayoutAtlasWidget::mAtlasFeatureFilterCheckBox_stateChanged( int state ) +{ + if ( state == Qt::Checked ) + { + mAtlasFeatureFilterEdit->setEnabled( true ); + mAtlasFeatureFilterButton->setEnabled( true ); + } + else + { + mAtlasFeatureFilterEdit->setEnabled( false ); + mAtlasFeatureFilterButton->setEnabled( false ); + } + mAtlas->setFilterFeatures( state == Qt::Checked ); + updateAtlasFeatures(); +} + +void QgsLayoutAtlasWidget::pageNameExpressionChanged( const QString &, bool valid ) +{ + QString expression = mPageNameWidget->asExpression(); + if ( !valid && !expression.isEmpty() ) + { + return; + } + + mAtlas->setPageNameExpression( expression ); +} + +void QgsLayoutAtlasWidget::mAtlasFeatureFilterEdit_editingFinished() +{ + QString error; + mAtlas->setFilterExpression( mAtlasFeatureFilterEdit->text(), error ); + updateAtlasFeatures(); +} + +void QgsLayoutAtlasWidget::mAtlasFeatureFilterButton_clicked() +{ + QgsVectorLayer *vl = dynamic_cast( mAtlasCoverageLayerComboBox->currentLayer() ); + + if ( !vl ) + { + return; + } + + QgsExpressionContext context = mLayout->createExpressionContext(); + QgsExpressionBuilderDialog exprDlg( vl, mAtlasFeatureFilterEdit->text(), this, QStringLiteral( "generic" ), context ); + exprDlg.setWindowTitle( tr( "Expression Based Filter" ) ); + + if ( exprDlg.exec() == QDialog::Accepted ) + { + QString expression = exprDlg.expressionText(); + if ( !expression.isEmpty() ) + { + mAtlasFeatureFilterEdit->setText( expression ); + QString error; + mAtlas->setFilterExpression( mAtlasFeatureFilterEdit->text(), error ); + updateAtlasFeatures(); + } + } +} + +void QgsLayoutAtlasWidget::mAtlasSortFeatureDirectionButton_clicked() +{ + Qt::ArrowType at = mAtlasSortFeatureDirectionButton->arrowType(); + at = ( at == Qt::UpArrow ) ? Qt::DownArrow : Qt::UpArrow; + mAtlasSortFeatureDirectionButton->setArrowType( at ); + + mAtlas->setSortAscending( at == Qt::UpArrow ); + updateAtlasFeatures(); +} + +void QgsLayoutAtlasWidget::changeFileFormat() +{ +#if 0 //TODO + QgsAtlasComposition *atlasMap = mAtlas; + atlasMap->setFileFormat( mAtlasFileFormat->currentText() ); +#endif +} +void QgsLayoutAtlasWidget::updateGuiElements() +{ + blockAllSignals( true ); + mUseAtlasCheckBox->setCheckState( mAtlas->enabled() ? Qt::Checked : Qt::Unchecked ); + mConfigurationGroup->setEnabled( mAtlas->enabled() ); + mOutputGroup->setEnabled( mAtlas->enabled() ); + + mAtlasCoverageLayerComboBox->setLayer( mAtlas->coverageLayer() ); + mPageNameWidget->setLayer( mAtlas->coverageLayer() ); + mPageNameWidget->setField( mAtlas->pageNameExpression() ); + + mAtlasSortFeatureKeyComboBox->setLayer( mAtlas->coverageLayer() ); + mAtlasSortFeatureKeyComboBox->setField( mAtlas->sortExpression() ); + + mAtlasFilenamePatternEdit->setText( mAtlas->filenameExpression() ); + mAtlasHideCoverageCheckBox->setCheckState( mAtlas->hideCoverage() ? Qt::Checked : Qt::Unchecked ); + +#if 0 //TODO + mAtlasSingleFileCheckBox->setCheckState( mAtlas->singleFile() ? Qt::Checked : Qt::Unchecked ); + mAtlasFilenamePatternEdit->setEnabled( !mAtlas->singleFile() ); + mAtlasFilenameExpressionButton->setEnabled( !mAtlas->singleFile() ); +#endif + + mAtlasSortFeatureCheckBox->setCheckState( mAtlas->sortFeatures() ? Qt::Checked : Qt::Unchecked ); + mAtlasSortFeatureDirectionButton->setEnabled( mAtlas->sortFeatures() ); + mAtlasSortFeatureKeyComboBox->setEnabled( mAtlas->sortFeatures() ); + + mAtlasSortFeatureDirectionButton->setArrowType( mAtlas->sortAscending() ? Qt::UpArrow : Qt::DownArrow ); + mAtlasFeatureFilterEdit->setText( mAtlas->filterExpression() ); + + mAtlasFeatureFilterCheckBox->setCheckState( mAtlas->filterFeatures() ? Qt::Checked : Qt::Unchecked ); + mAtlasFeatureFilterEdit->setEnabled( mAtlas->filterFeatures() ); + mAtlasFeatureFilterButton->setEnabled( mAtlas->filterFeatures() ); + +#if 0 //TODO + mAtlasFileFormat->setCurrentIndex( mAtlasFileFormat->findText( mAtlas->fileFormat() ) ); +#endif + + blockAllSignals( false ); +} + +void QgsLayoutAtlasWidget::blockAllSignals( bool b ) +{ + mUseAtlasCheckBox->blockSignals( b ); + mConfigurationGroup->blockSignals( b ); + mOutputGroup->blockSignals( b ); + mAtlasCoverageLayerComboBox->blockSignals( b ); + mPageNameWidget->blockSignals( b ); + mAtlasSortFeatureKeyComboBox->blockSignals( b ); + mAtlasFilenamePatternEdit->blockSignals( b ); + mAtlasHideCoverageCheckBox->blockSignals( b ); + mAtlasSingleFileCheckBox->blockSignals( b ); + mAtlasSortFeatureCheckBox->blockSignals( b ); + mAtlasSortFeatureDirectionButton->blockSignals( b ); + mAtlasFeatureFilterEdit->blockSignals( b ); + mAtlasFeatureFilterCheckBox->blockSignals( b ); +} diff --git a/src/app/layout/qgslayoutatlaswidget.h b/src/app/layout/qgslayoutatlaswidget.h new file mode 100644 index 000000000000..c3f5ab1ab27a --- /dev/null +++ b/src/app/layout/qgslayoutatlaswidget.h @@ -0,0 +1,61 @@ +/*************************************************************************** + qgslayoutatlaswidget.h + --------------------------- + begin : October 2012 + copyright : (C) 2012 Hugo Mercier + email : hugo dot mercier at oslandia dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "ui_qgslayoutatlaswidgetbase.h" + +class QgsPrintLayout; +class QgsLayoutAtlas; + +/** + * \ingroup app + * A widget for layout atlas settings. + */ +class QgsLayoutAtlasWidget: public QWidget, private Ui::QgsLayoutAtlasWidgetBase +{ + Q_OBJECT + public: + QgsLayoutAtlasWidget( QWidget *parent, QgsPrintLayout *layout ); + + public slots: + void mUseAtlasCheckBox_stateChanged( int state ); + void changeCoverageLayer( QgsMapLayer *layer ); + void mAtlasFilenamePatternEdit_editingFinished(); + void mAtlasFilenameExpressionButton_clicked(); + void mAtlasHideCoverageCheckBox_stateChanged( int state ); + void mAtlasSingleFileCheckBox_stateChanged( int state ); + + void mAtlasSortFeatureCheckBox_stateChanged( int state ); + void changesSortFeatureField( const QString &fieldName ); + void mAtlasSortFeatureDirectionButton_clicked(); + void mAtlasFeatureFilterEdit_editingFinished(); + void mAtlasFeatureFilterButton_clicked(); + void mAtlasFeatureFilterCheckBox_stateChanged( int state ); + void pageNameExpressionChanged( const QString &expression, bool valid ); + + void changeFileFormat(); + + private slots: + void updateGuiElements(); + + void updateAtlasFeatures(); + + private: + QgsPrintLayout *mLayout = nullptr; + QgsLayoutAtlas *mAtlas = nullptr; + + void blockAllSignals( bool b ); + void checkLayerType( QgsVectorLayer *layer ); +}; diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index a9e6b7ee92b6..12cd918b6b9b 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -36,6 +36,7 @@ #include "qgslayoutitemwidget.h" #include "qgslayoutimageexportoptionsdialog.h" #include "qgslayoutitemmap.h" +#include "qgsprintlayout.h" #include "qgsmessageviewer.h" #include "qgsgui.h" #include "qgslayoutitemguiregistry.h" @@ -53,6 +54,7 @@ #include "qgsproject.h" #include "qgsbusyindicatordialog.h" #include "qgslayoutundostack.h" +#include "qgslayoutatlaswidget.h" #include "qgslayoutpagecollection.h" #include "ui_qgssvgexportoptions.h" #include @@ -685,6 +687,11 @@ void QgsLayoutDesignerDialog::setCurrentLayout( QgsLayout *layout ) #endif createLayoutPropertiesWidget(); + + if ( qobject_cast< QgsPrintLayout * >( layout ) ) + { + createAtlasWidget(); + } } void QgsLayoutDesignerDialog::setIconSizes( int size ) @@ -1937,6 +1944,22 @@ void QgsLayoutDesignerDialog::createLayoutPropertiesWidget() mGuideStack->setMainPanel( guideWidget ); } +void QgsLayoutDesignerDialog::createAtlasWidget() +{ + if ( !mAtlasDock ) + { + mAtlasDock = new QgsDockWidget( tr( "Atlas" ), this ); + mAtlasDock->setObjectName( QStringLiteral( "AtlasDock" ) ); + mPanelsMenu->addAction( mAtlasDock->toggleViewAction() ); + addDockWidget( Qt::RightDockWidgetArea, mAtlasDock ); + tabifyDockWidget( mItemDock, mAtlasDock ); + } + + QgsLayoutAtlasWidget *atlasWidget = new QgsLayoutAtlasWidget( mGeneralDock, qobject_cast< QgsPrintLayout * >( mLayout ) ); + mAtlasDock->setWidget( atlasWidget ); + mAtlasDock->show(); +} + void QgsLayoutDesignerDialog::initializeRegistry() { sInitializedRegistry = true; diff --git a/src/app/layout/qgslayoutdesignerdialog.h b/src/app/layout/qgslayoutdesignerdialog.h index ff95beaaa43d..82a83b712ed1 100644 --- a/src/app/layout/qgslayoutdesignerdialog.h +++ b/src/app/layout/qgslayoutdesignerdialog.h @@ -333,6 +333,7 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner QgsPanelWidgetStack *mGeneralPropertiesStack = nullptr; QgsDockWidget *mGuideDock = nullptr; QgsPanelWidgetStack *mGuideStack = nullptr; + QgsDockWidget *mAtlasDock = nullptr; QgsLayoutPropertiesWidget *mLayoutPropertiesWidget = nullptr; @@ -372,6 +373,7 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner void activateNewItemCreationTool( int id, bool nodeBasedItem ); void createLayoutPropertiesWidget(); + void createAtlasWidget(); void initializeRegistry(); diff --git a/src/ui/layout/qgslayoutatlaswidgetbase.ui b/src/ui/layout/qgslayoutatlaswidgetbase.ui new file mode 100644 index 000000000000..5d239e606a11 --- /dev/null +++ b/src/ui/layout/qgslayoutatlaswidgetbase.ui @@ -0,0 +1,404 @@ + + + QgsLayoutAtlasWidgetBase + + + + 0 + 0 + 435 + 359 + + + + + 0 + 0 + + + + Atlas Generation + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::StyledPanel + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + 3 + + + + + Generate an atlas + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 12 + 20 + + + + + + + + Qt::WheelFocus + + + true + + + + true + + + + 0 + -63 + 417 + 389 + + + + + 0 + + + + + false + + + Qt::StrongFocus + + + Configuration + + + false + + + composeritem + + + false + + + + + + Sort direction + + + + + + Qt::UpArrow + + + + + + + + + + Filter with + + + + + + + + + + + :/images/themes/default/mIconExpression.svg:/images/themes/default/mIconExpression.svg + + + + + + + Hidden coverage layer + + + + + + + Coverage layer + + + + + + + Page name + + + + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + + + + + + 0 + 0 + + + + Sort by + + + + + + + + + + false + + + Qt::StrongFocus + + + Output + + + false + + + composeritem + + + false + + + + + + + + + + :/images/themes/default/mIconExpression.svg:/images/themes/default/mIconExpression.svg + + + + + + + + + + Single file export when possible + + + + + + + + + + Image export format + + + + + + + Output filename expression + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + QgsCollapsibleGroupBoxBasic + QGroupBox +
qgscollapsiblegroupbox.h
+ 1 +
+ + QgsScrollArea + QScrollArea +
qgsscrollarea.h
+ 1 +
+ + QgsMapLayerComboBox + QComboBox +
qgsmaplayercombobox.h
+
+ + QgsFieldComboBox + QComboBox +
qgsfieldcombobox.h
+
+ + QgsFieldExpressionWidget + QWidget +
qgsfieldexpressionwidget.h
+
+
+ + mUseAtlasCheckBox + mConfigurationGroup + mAtlasCoverageLayerComboBox + mAtlasHideCoverageCheckBox + mAtlasFeatureFilterCheckBox + mAtlasFeatureFilterEdit + mAtlasFeatureFilterButton + mAtlasSortFeatureCheckBox + mAtlasSortFeatureKeyComboBox + mAtlasSortFeatureDirectionButton + mOutputGroup + mAtlasFilenamePatternEdit + mAtlasFilenameExpressionButton + mAtlasSingleFileCheckBox + scrollArea + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
From d62bc35f640747911330915c4f365fc43ef76047 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 19 Dec 2017 15:52:03 +1000 Subject: [PATCH 006/105] Undo/redo for atlas settings changes --- src/app/layout/qgslayoutatlaswidget.cpp | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/app/layout/qgslayoutatlaswidget.cpp b/src/app/layout/qgslayoutatlaswidget.cpp index 1c7933bd31c4..85a79122401f 100644 --- a/src/app/layout/qgslayoutatlaswidget.cpp +++ b/src/app/layout/qgslayoutatlaswidget.cpp @@ -22,9 +22,11 @@ #include "qgsprintlayout.h" #include "qgslayoutatlas.h" #include "qgsexpressionbuilderdialog.h" +#include "qgslayoutundostack.h" QgsLayoutAtlasWidget::QgsLayoutAtlasWidget( QWidget *parent, QgsPrintLayout *layout ) : QWidget( parent ) + , mLayout( layout ) , mAtlas( layout->atlas() ) { setupUi( this ); @@ -86,6 +88,7 @@ void QgsLayoutAtlasWidget::changeCoverageLayer( QgsMapLayer *layer ) { QgsVectorLayer *vl = dynamic_cast( layer ); + mLayout->undoStack()->beginCommand( mAtlas, tr( "Change Atlas Layer" ) ); if ( !vl ) { mAtlas->setCoverageLayer( nullptr ); @@ -95,11 +98,13 @@ void QgsLayoutAtlasWidget::changeCoverageLayer( QgsMapLayer *layer ) mAtlas->setCoverageLayer( vl ); updateAtlasFeatures(); } + mLayout->undoStack()->endCommand(); } void QgsLayoutAtlasWidget::mAtlasFilenamePatternEdit_editingFinished() { QString error; + mLayout->undoStack()->beginCommand( mAtlas, tr( "Change Atlas Filename" ) ); if ( !mAtlas->setFilenameExpression( mAtlasFilenamePatternEdit->text(), error ) ) { //expression could not be set @@ -110,6 +115,7 @@ void QgsLayoutAtlasWidget::mAtlasFilenamePatternEdit_editingFinished() error ) ); } + mLayout->undoStack()->endCommand(); } void QgsLayoutAtlasWidget::mAtlasFilenameExpressionButton_clicked() @@ -131,6 +137,7 @@ void QgsLayoutAtlasWidget::mAtlasFilenameExpressionButton_clicked() //set atlas filename expression mAtlasFilenamePatternEdit->setText( expression ); QString error; + mLayout->undoStack()->beginCommand( mAtlas, tr( "Change Atlas Filename" ) ); if ( !mAtlas->setFilenameExpression( expression, error ) ) { //expression could not be set @@ -141,13 +148,16 @@ void QgsLayoutAtlasWidget::mAtlasFilenameExpressionButton_clicked() error ) ); } + mLayout->undoStack()->endCommand(); } } } void QgsLayoutAtlasWidget::mAtlasHideCoverageCheckBox_stateChanged( int state ) { + mLayout->undoStack()->beginCommand( mAtlas, tr( "Toggle Atlas Layer" ) ); mAtlas->setHideCoverage( state == Qt::Checked ); + mLayout->undoStack()->endCommand(); } void QgsLayoutAtlasWidget::mAtlasSingleFileCheckBox_stateChanged( int state ) @@ -179,7 +189,9 @@ void QgsLayoutAtlasWidget::mAtlasSortFeatureCheckBox_stateChanged( int state ) mAtlasSortFeatureDirectionButton->setEnabled( false ); mAtlasSortFeatureKeyComboBox->setEnabled( false ); } + mLayout->undoStack()->beginCommand( mAtlas, tr( "Toggle Atlas Sorting" ) ); mAtlas->setSortFeatures( state == Qt::Checked ); + mLayout->undoStack()->endCommand(); updateAtlasFeatures(); } @@ -203,7 +215,9 @@ void QgsLayoutAtlasWidget::updateAtlasFeatures() void QgsLayoutAtlasWidget::changesSortFeatureField( const QString &fieldName ) { + mLayout->undoStack()->beginCommand( mAtlas, tr( "Change Atlas Sort" ) ); mAtlas->setSortExpression( fieldName ); + mLayout->undoStack()->endCommand(); updateAtlasFeatures(); } @@ -219,7 +233,9 @@ void QgsLayoutAtlasWidget::mAtlasFeatureFilterCheckBox_stateChanged( int state ) mAtlasFeatureFilterEdit->setEnabled( false ); mAtlasFeatureFilterButton->setEnabled( false ); } + mLayout->undoStack()->beginCommand( mAtlas, tr( "Change Atlas Filter" ) ); mAtlas->setFilterFeatures( state == Qt::Checked ); + mLayout->undoStack()->endCommand(); updateAtlasFeatures(); } @@ -231,13 +247,17 @@ void QgsLayoutAtlasWidget::pageNameExpressionChanged( const QString &, bool vali return; } + mLayout->undoStack()->beginCommand( mAtlas, tr( "Change Atlas Name" ) ); mAtlas->setPageNameExpression( expression ); + mLayout->undoStack()->endCommand(); } void QgsLayoutAtlasWidget::mAtlasFeatureFilterEdit_editingFinished() { QString error; + mLayout->undoStack()->beginCommand( mAtlas, tr( "Change Atlas Filter" ) ); mAtlas->setFilterExpression( mAtlasFeatureFilterEdit->text(), error ); + mLayout->undoStack()->endCommand(); updateAtlasFeatures(); } @@ -261,7 +281,9 @@ void QgsLayoutAtlasWidget::mAtlasFeatureFilterButton_clicked() { mAtlasFeatureFilterEdit->setText( expression ); QString error; + mLayout->undoStack()->beginCommand( mAtlas, tr( "Change Atlas Filter" ) ); mAtlas->setFilterExpression( mAtlasFeatureFilterEdit->text(), error ); + mLayout->undoStack()->endCommand(); updateAtlasFeatures(); } } @@ -273,7 +295,9 @@ void QgsLayoutAtlasWidget::mAtlasSortFeatureDirectionButton_clicked() at = ( at == Qt::UpArrow ) ? Qt::DownArrow : Qt::UpArrow; mAtlasSortFeatureDirectionButton->setArrowType( at ); + mLayout->undoStack()->beginCommand( mAtlas, tr( "Change Atlas Sort" ) ); mAtlas->setSortAscending( at == Qt::UpArrow ); + mLayout->undoStack()->endCommand(); updateAtlasFeatures(); } From e169c219b363aed889218d214b17d8daba5e242c Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 19 Dec 2017 16:50:12 +1000 Subject: [PATCH 007/105] Work on modernizing atlas --- python/core/core_auto.sip | 1 + .../core/layout/qgsabstractlayoutiterator.sip | 103 ++++++ python/core/layout/qgslayoutatlas.sip | 44 ++- python/core/layout/qgslayoutcontext.sip | 10 + src/core/CMakeLists.txt | 1 + src/core/layout/qgsabstractlayoutiterator.h | 82 +++++ src/core/layout/qgslayoutatlas.cpp | 298 +++++++++++++++++- src/core/layout/qgslayoutatlas.h | 50 ++- src/core/layout/qgslayoutcontext.cpp | 7 + src/core/layout/qgslayoutcontext.h | 14 +- 10 files changed, 602 insertions(+), 8 deletions(-) create mode 100644 python/core/layout/qgsabstractlayoutiterator.sip create mode 100644 src/core/layout/qgsabstractlayoutiterator.h diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index 8e6b5225f07c..fc581abc92c2 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -161,6 +161,7 @@ %Include composer/qgscomposermultiframecommand.sip %Include composer/qgscomposertexttable.sip %Include composer/qgspaperitem.sip +%Include layout/qgsabstractlayoutiterator.sip %Include layout/qgslayoutaligner.sip %Include layout/qgslayoutexporter.sip %Include layout/qgslayoutgridsettings.sip diff --git a/python/core/layout/qgsabstractlayoutiterator.sip b/python/core/layout/qgsabstractlayoutiterator.sip new file mode 100644 index 000000000000..a39b121c9bf9 --- /dev/null +++ b/python/core/layout/qgsabstractlayoutiterator.sip @@ -0,0 +1,103 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layout/qgsabstractlayoutiterator.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + +class QgsAbstractLayoutIterator +{ +%Docstring +************************************************************************* +* +This program is free software; you can redistribute it and/or modify * +it under the terms of the GNU General Public License as published by * +the Free Software Foundation; either version 2 of the License, or * +(at your option) any later version. * +* +************************************************************************** +%End + +%TypeHeaderCode +#include "qgsabstractlayoutiterator.h" +%End + public: + + virtual ~QgsAbstractLayoutIterator(); + + virtual bool beginRender() = 0; +%Docstring +Called when rendering begins, before iteration commences. Returns true if successful, false if no iteration +is available or required. + +.. seealso:: :py:func:`endRender()` +%End + + virtual bool endRender() = 0; +%Docstring +Ends the render, performing any required cleanup tasks. +%End + + virtual int count() const = 0; +%Docstring +Returns the number of features to iterate over. +%End + + virtual bool next() = 0; +%Docstring +Iterates to next feature, returning false if no more features exist to iterate over. + +.. seealso:: :py:func:`previous()` + +.. seealso:: :py:func:`last()` + +.. seealso:: :py:func:`first()` +%End + + virtual bool previous() = 0; +%Docstring +Iterates to the previous feature, returning false if no previous feature exists. + +.. seealso:: :py:func:`next()` + +.. seealso:: :py:func:`last()` + +.. seealso:: :py:func:`first()` +%End + + virtual bool last() = 0; +%Docstring +Seeks to the last feature, returning false if no feature was found. + +.. seealso:: :py:func:`next()` + +.. seealso:: :py:func:`previous()` + +.. seealso:: :py:func:`first()` +%End + + virtual bool first() = 0; +%Docstring +Seeks to the first feature, returning false if no feature was found. + +.. seealso:: :py:func:`next()` + +.. seealso:: :py:func:`previous()` + +.. seealso:: :py:func:`last()` +%End +}; + + + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layout/qgsabstractlayoutiterator.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/layout/qgslayoutatlas.sip b/python/core/layout/qgslayoutatlas.sip index c713b8043a33..c088a35b50db 100644 --- a/python/core/layout/qgslayoutatlas.sip +++ b/python/core/layout/qgslayoutatlas.sip @@ -8,7 +8,7 @@ -class QgsLayoutAtlas : QObject, QgsLayoutSerializableObject +class QgsLayoutAtlas : QObject, QgsAbstractLayoutIterator, QgsLayoutSerializableObject { %Docstring Class used to render an Atlas, iterating over geometry features. @@ -245,8 +245,30 @@ will be set to the expression error. .. seealso:: :py:func:`setFilterFeatures()` %End + int updateFeatures(); +%Docstring +Requeries the current atlas coverage layer and applies filtering and sorting. Returns +number of matching features. +%End + + virtual bool beginRender(); + + virtual bool endRender(); + + virtual int count() const; + + public slots: + virtual bool next(); + + virtual bool previous(); + + virtual bool first(); + + virtual bool last(); + + signals: void changed(); @@ -262,6 +284,26 @@ Emitted when atlas is enabled or disabled. void coverageLayerChanged( QgsVectorLayer *layer ); %Docstring Emitted when the coverage layer for the atlas changes. +%End + + void messagePushed( const QString &message ); +%Docstring +Is emitted when the atlas has an updated status bar ``message``. +%End + + void numberFeaturesChanged( int numFeatures ); +%Docstring +Emitted when the number of features for the atlas changes. +%End + + void renderBegun(); +%Docstring +Emitted when atlas rendering has begun. +%End + + void renderEnded(); +%Docstring +Emitted when atlas rendering has ended. %End }; diff --git a/python/core/layout/qgslayoutcontext.sip b/python/core/layout/qgslayoutcontext.sip index 83e71d5a13f3..4c7bd0d0d416 100644 --- a/python/core/layout/qgslayoutcontext.sip +++ b/python/core/layout/qgslayoutcontext.sip @@ -90,6 +90,8 @@ Sets the current ``feature`` for evaluating the layout. This feature may be used for altering an item's content and appearance for a report or atlas layout. +Emits the changed() signal. + .. seealso:: :py:func:`feature()` %End @@ -113,6 +115,8 @@ Returns the vector layer associated with the layout's context. %Docstring Sets the vector ``layer`` associated with the layout's context. +Emits the changed() signal. + .. seealso:: :py:func:`layer()` %End @@ -220,6 +224,12 @@ If ``layer`` is -1, all item layers should be rendered. Emitted whenever the context's ``flags`` change. .. seealso:: :py:func:`setFlags()` +%End + + void changed(); +%Docstring +Emitted certain settings in the context is changed, e.g. by setting a new feature or vector layer +for the context. %End }; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 0b96b7255065..8b158ca73f32 100755 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -1026,6 +1026,7 @@ SET(QGIS_CORE_HDRS composer/qgscomposertexttable.h composer/qgspaperitem.h + layout/qgsabstractlayoutiterator.h layout/qgslayoutaligner.h layout/qgslayoutexporter.h layout/qgslayoutgridsettings.h diff --git a/src/core/layout/qgsabstractlayoutiterator.h b/src/core/layout/qgsabstractlayoutiterator.h new file mode 100644 index 000000000000..aa0899c26e94 --- /dev/null +++ b/src/core/layout/qgsabstractlayoutiterator.h @@ -0,0 +1,82 @@ +/*************************************************************************** + qgsabstractlayoutiterator.h + --------------------------- + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#ifndef QGSABSTRACTLAYOUTITERATOR_H +#define QGSABSTRACTLAYOUTITERATOR_H + +#include "qgis_core.h" + + +class CORE_EXPORT QgsAbstractLayoutIterator +{ + + public: + + virtual ~QgsAbstractLayoutIterator() = default; + + /** + * Called when rendering begins, before iteration commences. Returns true if successful, false if no iteration + * is available or required. + * \see endRender() + */ + virtual bool beginRender() = 0; + + /** + * Ends the render, performing any required cleanup tasks. + */ + virtual bool endRender() = 0; + + /** + * Returns the number of features to iterate over. + */ + virtual int count() const = 0; + + /** + * Iterates to next feature, returning false if no more features exist to iterate over. + * \see previous() + * \see last() + * \see first() + */ + virtual bool next() = 0; + + /** + * Iterates to the previous feature, returning false if no previous feature exists. + * \see next() + * \see last() + * \see first() + */ + virtual bool previous() = 0; + + /** + * Seeks to the last feature, returning false if no feature was found. + * \see next() + * \see previous() + * \see first() + */ + virtual bool last() = 0; + + /** + * Seeks to the first feature, returning false if no feature was found. + * \see next() + * \see previous() + * \see last() + */ + virtual bool first() = 0; +}; + +#endif //QGSABSTRACTLAYOUTITERATOR_H + + + diff --git a/src/core/layout/qgslayoutatlas.cpp b/src/core/layout/qgslayoutatlas.cpp index 3f0d9a40ed06..6b72200967cb 100644 --- a/src/core/layout/qgslayoutatlas.cpp +++ b/src/core/layout/qgslayoutatlas.cpp @@ -20,6 +20,7 @@ #include "qgslayoutatlas.h" #include "qgslayout.h" +#include "qgsmessagelog.h" QgsLayoutAtlas::QgsLayoutAtlas( QgsLayout *layout ) : QObject( layout ) @@ -153,12 +154,10 @@ void QgsLayoutAtlas::setCoverageLayer( QgsVectorLayer *layer ) QString QgsLayoutAtlas::nameForPage( int pageNumber ) const { -#if 0 //TODO if ( pageNumber < 0 || pageNumber >= mFeatureIds.count() ) return QString(); return mFeatureIds.at( pageNumber ).second; -#endif } bool QgsLayoutAtlas::setFilterExpression( const QString &expression, QString &errorString ) @@ -167,11 +166,12 @@ bool QgsLayoutAtlas::setFilterExpression( const QString &expression, QString &er return true; } + /// @cond PRIVATE -class AtlasFieldSorter +class AtlasFeatureSorter { public: - AtlasFieldSorter( QgsLayoutAtlas::SorterKeys &keys, bool ascending = true ) + AtlasFeatureSorter( QgsLayoutAtlas::SorterKeys &keys, bool ascending = true ) : mKeys( keys ) , mAscending( ascending ) {} @@ -189,6 +189,180 @@ class AtlasFieldSorter /// @endcond +int QgsLayoutAtlas::updateFeatures() +{ + if ( !mCoverageLayer ) + { + return 0; + } + + QgsExpressionContext expressionContext = createExpressionContext(); + + QString error; + updateFilenameExpression( error ); + + // select all features with all attributes + QgsFeatureRequest req; + + mFilterParserError.clear(); + if ( mFilterFeatures && !mFilterExpression.isEmpty() ) + { + QgsExpression filterExpression( mFilterExpression ); + if ( filterExpression.hasParserError() ) + { + mFilterParserError = filterExpression.parserErrorString(); + return 0; + } + + //filter good to go + req.setFilterExpression( mFilterExpression ); + } + + QgsFeatureIterator fit = mCoverageLayer->getFeatures( req ); + + std::unique_ptr nameExpression; + if ( !mPageNameExpression.isEmpty() ) + { + nameExpression = qgis::make_unique< QgsExpression >( mPageNameExpression ); + if ( nameExpression->hasParserError() ) + { + nameExpression.reset( nullptr ); + } + else + { + nameExpression->prepare( &expressionContext ); + } + } + + // We cannot use nextFeature() directly since the feature pointer is rewinded by the rendering process + // We thus store the feature ids for future extraction + QgsFeature feat; + mFeatureIds.clear(); + mFeatureKeys.clear(); + + std::unique_ptr sortExpression; + if ( mSortFeatures && !mSortExpression.isEmpty() ) + { + sortExpression = qgis::make_unique< QgsExpression >( mSortExpression ); + if ( sortExpression->hasParserError() ) + { + sortExpression.reset( nullptr ); + } + else + { + sortExpression->prepare( &expressionContext ); + } + } + + while ( fit.nextFeature( feat ) ) + { + expressionContext.setFeature( feat ); + + QString pageName; + if ( nameExpression ) + { + QVariant result = nameExpression->evaluate( &expressionContext ); + if ( nameExpression->hasEvalError() ) + { + QgsMessageLog::logMessage( tr( "Atlas name eval error: %1" ).arg( nameExpression->evalErrorString() ), tr( "Layout" ) ); + } + pageName = result.toString(); + } + + mFeatureIds.push_back( qMakePair( feat.id(), pageName ) ); + + if ( sortExpression ) + { + QVariant result = sortExpression->evaluate( &expressionContext ); + if ( sortExpression->hasEvalError() ) + { + QgsMessageLog::logMessage( tr( "Atlas sort eval error: %1" ).arg( sortExpression->evalErrorString() ), tr( "Layout" ) ); + } + mFeatureKeys.insert( feat.id(), result ); + } + } + + // sort features, if asked for + if ( !mFeatureKeys.isEmpty() ) + { + AtlasFeatureSorter sorter( mFeatureKeys, mSortAscending ); + std::sort( mFeatureIds.begin(), mFeatureIds.end(), sorter ); + } + + emit numberFeaturesChanged( mFeatureIds.size() ); + +#if 0 //TODO - move to app + //jump to first feature if currently using an atlas preview + //need to do this in case filtering/layer change has altered matching features + return first(); +#endif + + + return mFeatureIds.size(); +} + +bool QgsLayoutAtlas::beginRender() +{ + if ( !mCoverageLayer ) + { + return false; + } + + emit renderBegun(); + + if ( !updateFeatures() ) + { + //no matching features found + return false; + } + + return true; +} + +bool QgsLayoutAtlas::endRender() +{ + emit renderEnded(); + return true; +} + +int QgsLayoutAtlas::count() const +{ + return mFeatureIds.size(); +} + +bool QgsLayoutAtlas::next() +{ + int newFeatureNo = mCurrentFeatureNo + 1; + if ( newFeatureNo >= mFeatureIds.size() ) + { + return false; + } + + return prepareForFeature( newFeatureNo ); +} + +bool QgsLayoutAtlas::previous() +{ + int newFeatureNo = mCurrentFeatureNo - 1; + if ( newFeatureNo < 0 ) + { + return false; + } + + return prepareForFeature( newFeatureNo ); +} + +bool QgsLayoutAtlas::first() +{ + return prepareForFeature( 0 ); +} + +bool QgsLayoutAtlas::last() +{ + return prepareForFeature( mFeatureIds.size() - 1 ); +} + + void QgsLayoutAtlas::setHideCoverage( bool hide ) { mHideCoverage = hide; @@ -265,3 +439,119 @@ bool QgsLayoutAtlas::updateFilenameExpression( QString &error ) return true; } +bool QgsLayoutAtlas::evalFeatureFilename( const QgsExpressionContext &context ) +{ + //generate filename for current atlas feature + if ( !mFilenameExpressionString.isEmpty() && mFilenameExpression.isValid() ) + { + QVariant filenameRes = mFilenameExpression.evaluate( &context ); + if ( mFilenameExpression.hasEvalError() ) + { + QgsMessageLog::logMessage( tr( "Atlas filename evaluation error: %1" ).arg( mFilenameExpression.evalErrorString() ), tr( "Composer" ) ); + return false; + } + + mCurrentFilename = filenameRes.toString(); + } + return true; +} + +bool QgsLayoutAtlas::prepareForFeature( const int featureI ) +{ + if ( !mCoverageLayer ) + { + return false; + } + + if ( mFeatureIds.isEmpty() ) + { + emit messagePushed( tr( "No matching atlas features" ) ); + return false; + } + + if ( featureI >= mFeatureIds.size() ) + { + return false; + } + + mCurrentFeatureNo = featureI; + + // retrieve the next feature, based on its id + if ( !mCoverageLayer->getFeatures( QgsFeatureRequest().setFilterFid( mFeatureIds[ featureI ].first ) ).nextFeature( mCurrentFeature ) ) + return false; + + QgsExpressionContext expressionContext = createExpressionContext(); + + // generate filename for current feature + if ( !evalFeatureFilename( expressionContext ) ) + { + + //error evaluating filename + return false; + } + + mGeometryCache.clear(); + + mLayout->context().blockSignals( true ); // setFeature emits changed, we don't want 2 signals + mLayout->context().setLayer( mCoverageLayer.get() ); + mLayout->context().blockSignals( false ); + mLayout->context().setFeature( mCurrentFeature ); + + emit messagePushed( QString( tr( "Atlas feature %1 of %2" ) ).arg( featureI + 1 ).arg( mFeatureIds.size() ) ); + + if ( !mCurrentFeature.isValid() ) + { + //bad feature + return false; + } + +#if 0 //TODO - move to map + //update composer maps + + //build a list of atlas-enabled composer maps + QList maps; + QList atlasMaps; + mComposition->composerItems( maps ); + if ( maps.isEmpty() ) + { + return true; + } + for ( QList::iterator mit = maps.begin(); mit != maps.end(); ++mit ) + { + QgsComposerMap *currentMap = ( *mit ); + if ( !currentMap->atlasDriven() ) + { + continue; + } + atlasMaps << currentMap; + } + + if ( !atlasMaps.isEmpty() ) + { + //clear the transformed bounds of the previous feature + mTransformedFeatureBounds = QgsRectangle(); + + // compute extent of current feature in the map CRS. This should be set on a per-atlas map basis, + // but given that it's not currently possible to have maps with different CRSes we can just + // calculate it once based on the first atlas maps' CRS. + computeExtent( atlasMaps[0] ); + } + + for ( QList::iterator mit = maps.begin(); mit != maps.end(); ++mit ) + { + if ( ( *mit )->atlasDriven() ) + { + // map is atlas driven, so update it's bounds (causes a redraw) + prepareMap( *mit ); + } + else + { + // map is not atlas driven, so manually force a redraw (to reflect possibly atlas + // dependent symbology) + ( *mit )->invalidateCache(); + } + } +#endif + return true; +} + diff --git a/src/core/layout/qgslayoutatlas.h b/src/core/layout/qgslayoutatlas.h index 6e740a3f7044..1859542b5faf 100644 --- a/src/core/layout/qgslayoutatlas.h +++ b/src/core/layout/qgslayoutatlas.h @@ -19,6 +19,7 @@ #include "qgis_core.h" #include "qgsvectorlayerref.h" #include "qgslayoutserializableobject.h" +#include "qgsabstractlayoutiterator.h" #include class QgsLayout; @@ -33,7 +34,7 @@ class QgsLayout; * QgsLayoutAtlas which is automatically created and attached to the composition. * \since QGIS 3.0 */ -class CORE_EXPORT QgsLayoutAtlas : public QObject, public QgsLayoutSerializableObject +class CORE_EXPORT QgsLayoutAtlas : public QObject, public QgsAbstractLayoutIterator, public QgsLayoutSerializableObject { Q_OBJECT public: @@ -217,8 +218,23 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject, public QgsLayoutSerializableO */ bool setFilterExpression( const QString &expression, QString &errorString SIP_OUT ); + /** + * Requeries the current atlas coverage layer and applies filtering and sorting. Returns + * number of matching features. + */ + int updateFeatures(); + + bool beginRender() override; + bool endRender() override; + int count() const override; + public slots: + bool next() override; + bool previous() override; + bool first() override; + bool last() override; + signals: //! Emitted when one of the atlas parameters changes. @@ -230,6 +246,20 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject, public QgsLayoutSerializableO //! Emitted when the coverage layer for the atlas changes. void coverageLayerChanged( QgsVectorLayer *layer ); + //! Is emitted when the atlas has an updated status bar \a message. + void messagePushed( const QString &message ); + + /** + * Emitted when the number of features for the atlas changes. + */ + void numberFeaturesChanged( int numFeatures ); + + //! Emitted when atlas rendering has begun. + void renderBegun(); + + //! Emitted when atlas rendering has ended. + void renderEnded(); + private slots: void removeLayers( const QStringList &layers ); @@ -247,6 +277,13 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject, public QgsLayoutSerializableO */ bool evalFeatureFilename( const QgsExpressionContext &context ); + /** + * Prepare the atlas for the given feature. Sets the extent and context variables + * \param i feature number + * \returns true if feature was successfully prepared + */ + bool prepareForFeature( int i ); + QPointer< QgsLayout > mLayout; bool mEnabled = false; @@ -273,9 +310,18 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject, public QgsLayoutSerializableO QString mFilterParserError; + // id of each iterated feature (after filtering and sorting) paired with atlas page name + QVector< QPair > mFeatureIds; + // current atlas feature number + int mCurrentFeatureNo = 0; + QgsFeature mCurrentFeature; + + // projected geometry cache + mutable QMap mGeometryCache; + QgsExpressionContext createExpressionContext(); - friend class AtlasFieldSorter; + friend class AtlasFeatureSorter; }; #endif //QGSLAYOUTATLAS_H diff --git a/src/core/layout/qgslayoutcontext.cpp b/src/core/layout/qgslayoutcontext.cpp index 44be8fabda9c..15ef62df56da 100644 --- a/src/core/layout/qgslayoutcontext.cpp +++ b/src/core/layout/qgslayoutcontext.cpp @@ -69,6 +69,12 @@ QgsRenderContext::Flags QgsLayoutContext::renderContextFlags() const return flags; } +void QgsLayoutContext::setFeature( const QgsFeature &feature ) +{ + mFeature = feature; + emit changed(); +} + QgsVectorLayer *QgsLayoutContext::layer() const { return mLayer; @@ -77,6 +83,7 @@ QgsVectorLayer *QgsLayoutContext::layer() const void QgsLayoutContext::setLayer( QgsVectorLayer *layer ) { mLayer = layer; + emit changed(); } void QgsLayoutContext::setDpi( double dpi ) diff --git a/src/core/layout/qgslayoutcontext.h b/src/core/layout/qgslayoutcontext.h index cfbfcf703b0e..8caf64735f5c 100644 --- a/src/core/layout/qgslayoutcontext.h +++ b/src/core/layout/qgslayoutcontext.h @@ -93,9 +93,12 @@ class CORE_EXPORT QgsLayoutContext : public QObject * Sets the current \a feature for evaluating the layout. This feature may * be used for altering an item's content and appearance for a report * or atlas layout. + * + * Emits the changed() signal. + * * \see feature() */ - void setFeature( const QgsFeature &feature ) { mFeature = feature; } + void setFeature( const QgsFeature &feature ); /** * Returns the current feature for evaluating the layout. This feature may @@ -113,6 +116,9 @@ class CORE_EXPORT QgsLayoutContext : public QObject /** * Sets the vector \a layer associated with the layout's context. + * + * Emits the changed() signal. + * * \see layer() */ void setLayer( QgsVectorLayer *layer ); @@ -219,6 +225,12 @@ class CORE_EXPORT QgsLayoutContext : public QObject */ void flagsChanged( QgsLayoutContext::Flags flags ); + /** + * Emitted certain settings in the context is changed, e.g. by setting a new feature or vector layer + * for the context. + */ + void changed(); + private: Flags mFlags = nullptr; From 520c2aab195cc7713d85f3a8bb535ca35d6680b3 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 19 Dec 2017 19:58:37 +1000 Subject: [PATCH 008/105] More work on atlas GUI --- python/core/layout/qgslayoutatlas.sip | 16 + src/app/layout/qgslayoutdesignerdialog.cpp | 375 ++++++++++++++++++++- src/app/layout/qgslayoutdesignerdialog.h | 34 ++ src/app/layout/qgslayoutmanagerdialog.cpp | 3 +- src/app/qgisapp.cpp | 3 +- src/core/composer/qgslayoutmanager.cpp | 2 +- src/core/layout/qgslayoutatlas.cpp | 11 + src/core/layout/qgslayoutatlas.h | 14 + src/ui/layout/qgslayoutdesignerbase.ui | 153 ++++++++- 9 files changed, 595 insertions(+), 16 deletions(-) diff --git a/python/core/layout/qgslayoutatlas.sip b/python/core/layout/qgslayoutatlas.sip index c088a35b50db..d68bfdfa234f 100644 --- a/python/core/layout/qgslayoutatlas.sip +++ b/python/core/layout/qgslayoutatlas.sip @@ -258,6 +258,11 @@ number of matching features. virtual int count() const; + int currentFeatureNumber() const; +%Docstring +Returns the current feature number, where a value of 0 corresponds to the first feature. +%End + public slots: virtual bool next(); @@ -268,6 +273,12 @@ number of matching features. virtual bool last(); + bool seekTo( int feature ); + + void refreshCurrentFeature(); +%Docstring +Refreshes the current atlas feature, by refetching its attributes from the vector layer provider +%End signals: @@ -294,6 +305,11 @@ Is emitted when the atlas has an updated status bar ``message``. void numberFeaturesChanged( int numFeatures ); %Docstring Emitted when the number of features for the atlas changes. +%End + + void featureChanged( const QgsFeature &feature ); +%Docstring +Is emitted when the current atlas ``feature`` changes. %End void renderBegun(); diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 12cd918b6b9b..7324d6d2d615 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -22,6 +22,7 @@ #include "qgsfileutils.h" #include "qgslogger.h" #include "qgslayout.h" +#include "qgslayoutatlas.h" #include "qgslayoutappmenuprovider.h" #include "qgslayoutcustomdrophandler.h" #include "qgslayoutmanager.h" @@ -37,6 +38,7 @@ #include "qgslayoutimageexportoptionsdialog.h" #include "qgslayoutitemmap.h" #include "qgsprintlayout.h" +#include "qgsmapcanvas.h" #include "qgsmessageviewer.h" #include "qgsgui.h" #include "qgslayoutitemguiregistry.h" @@ -200,6 +202,17 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla connect( mActionPasteInPlace, &QAction::triggered, this, &QgsLayoutDesignerDialog::pasteInPlace ); + connect( mActionAtlasSettings, &QAction::triggered, this, &QgsLayoutDesignerDialog::showAtlasSettings ); + connect( mActionAtlasPreview, &QAction::triggered, this, &QgsLayoutDesignerDialog::atlasPreviewTriggered ); + connect( mActionAtlasNext, &QAction::triggered, this, &QgsLayoutDesignerDialog::atlasNext ); + connect( mActionAtlasPrev, &QAction::triggered, this, &QgsLayoutDesignerDialog::atlasPrevious ); + connect( mActionAtlasFirst, &QAction::triggered, this, &QgsLayoutDesignerDialog::atlasFirst ); + connect( mActionAtlasLast, &QAction::triggered, this, &QgsLayoutDesignerDialog::atlasLast ); + connect( mActionPrintAtlas, &QAction::triggered, this, &QgsLayoutDesignerDialog::printAtlas ); + connect( mActionExportAtlasAsImage, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportAtlasToRaster ); + connect( mActionExportAtlasAsSVG, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportAtlasToSvg ); + connect( mActionExportAtlasAsPDF, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportAtlasToPdf ); + mView = new QgsLayoutView(); //mView->setMapCanvas( mQgis->mapCanvas() ); mView->setContentsMargins( 0, 0, 0, 0 ); @@ -272,6 +285,28 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla resizeToolButton->setDefaultAction( mActionResizeNarrowest ); mActionsToolbar->addWidget( resizeToolButton ); + QToolButton *atlasExportToolButton = new QToolButton( mAtlasToolbar ); + atlasExportToolButton->setPopupMode( QToolButton::InstantPopup ); + atlasExportToolButton->setAutoRaise( true ); + atlasExportToolButton->setToolButtonStyle( Qt::ToolButtonIconOnly ); + atlasExportToolButton->addAction( mActionExportAtlasAsImage ); + atlasExportToolButton->addAction( mActionExportAtlasAsSVG ); + atlasExportToolButton->addAction( mActionExportAtlasAsPDF ); + atlasExportToolButton->setDefaultAction( mActionExportAtlasAsImage ); + mAtlasToolbar->insertWidget( mActionAtlasSettings, atlasExportToolButton ); + mAtlasPageComboBox = new QComboBox(); + mAtlasPageComboBox->setEditable( true ); + mAtlasPageComboBox->addItem( QString::number( 1 ) ); + mAtlasPageComboBox->setCurrentIndex( 0 ); + mAtlasPageComboBox->setMinimumHeight( mAtlasToolbar->height() ); + mAtlasPageComboBox->setMinimumContentsLength( 6 ); + mAtlasPageComboBox->setMaxVisibleItems( 20 ); + mAtlasPageComboBox->setSizeAdjustPolicy( QComboBox::AdjustToContents ); + mAtlasPageComboBox->setInsertPolicy( QComboBox::NoInsert ); + connect( mAtlasPageComboBox->lineEdit(), &QLineEdit::editingFinished, this, &QgsLayoutDesignerDialog::atlasPageComboEditingFinished ); + connect( mAtlasPageComboBox, static_cast( &QComboBox::currentIndexChanged ), this, &QgsLayoutDesignerDialog::atlasPageComboEditingFinished ); + mAtlasToolbar->insertWidget( mActionAtlasNext, mAtlasPageComboBox ); + mAddItemTool = new QgsLayoutViewToolAddItem( mView ); mAddNodeItemTool = new QgsLayoutViewToolAddNodeItem( mView ); mPanTool = new QgsLayoutViewToolPan( mView ); @@ -624,6 +659,21 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla tabifyDockWidget( mGeneralDock, mItemDock ); tabifyDockWidget( mItemDock, mItemsDock ); + //set initial state of atlas controls + mActionAtlasPreview->setEnabled( false ); + mActionAtlasPreview->setChecked( false ); + mActionAtlasFirst->setEnabled( false ); + mActionAtlasLast->setEnabled( false ); + mActionAtlasNext->setEnabled( false ); + mActionAtlasPrev->setEnabled( false ); + mActionPrintAtlas->setEnabled( false ); + mAtlasPageComboBox->setEnabled( false ); + mActionExportAtlasAsImage->setEnabled( false ); + mActionExportAtlasAsSVG->setEnabled( false ); + mActionExportAtlasAsPDF->setEnabled( false ); + mAtlasToolbar->hide(); + mMenuAtlas->hide(); + restoreWindowState(); //listen out to status bar updates from the view @@ -929,17 +979,17 @@ void QgsLayoutDesignerDialog::refreshLayout() return; } -#if 0 //TODO - //refresh atlas feature first, to update attributes - if ( mComposition->atlasMode() == QgsComposition::PreviewAtlas ) + //refresh atlas feature first, to force an update of feature + //in case feature attributes or geometry has changed + if ( QgsLayoutAtlas *printAtlas = atlas() ) { - //block signals from atlas, since the later call to mComposition->refreshItems() will - //also trigger items to refresh atlas dependent properties - mComposition->atlasComposition().blockSignals( true ); - mComposition->atlasComposition().refreshFeature(); - mComposition->atlasComposition().blockSignals( false ); + if ( printAtlas->enabled() && mActionAtlasPreview->isChecked() ) + { + //block signals from atlas, since the later call to mComposition->refreshItems() will + //also trigger items to refresh atlas dependent properties + whileBlocking( printAtlas )->refreshCurrentFeature(); + } } -#endif currentLayout()->refresh(); } @@ -1841,6 +1891,216 @@ void QgsLayoutDesignerDialog::exportToSvg() QApplication::restoreOverrideCursor(); } +void QgsLayoutDesignerDialog::showAtlasSettings() +{ + if ( !mAtlasDock ) + return; + + if ( !mAtlasDock->isVisible() ) + { + mAtlasDock->show(); + } + + mAtlasDock->raise(); +} + +void QgsLayoutDesignerDialog::atlasPreviewTriggered( bool checked ) +{ + QgsPrintLayout *printLayout = qobject_cast< QgsPrintLayout * >( mLayout ); + if ( !printLayout ) + return; + QgsLayoutAtlas *atlas = printLayout->atlas(); + + //check if composition has an atlas map enabled + if ( checked && !atlas->enabled() ) + { + //no atlas current enabled + mMessageBar->pushWarning( tr( "Atlas" ), + tr( "Atlas is not enabled for this layout!" ) ); + whileBlocking( mActionAtlasPreview )->setChecked( false ); + return; + } + + //toggle other controls depending on whether atlas preview is active + mActionAtlasFirst->setEnabled( checked ); + mActionAtlasLast->setEnabled( checked ); + mActionAtlasNext->setEnabled( checked ); + mActionAtlasPrev->setEnabled( checked ); + mAtlasPageComboBox->setEnabled( checked ); + + if ( checked ) + { +#if 0 //TODO + loadAtlasPredefinedScalesFromProject(); +#endif + } + + if ( checked ) + { + if ( !atlas->beginRender() ) + { + atlas->endRender(); + //something went wrong, e.g., no matching features + mMessageBar->pushWarning( tr( "Atlas" ), tr( "No matching atlas features found!" ) ); + mActionAtlasPreview->blockSignals( true ); + mActionAtlasPreview->setChecked( false ); + mActionAtlasFirst->setEnabled( false ); + mActionAtlasLast->setEnabled( false ); + mActionAtlasNext->setEnabled( false ); + mActionAtlasPrev->setEnabled( false ); + mAtlasPageComboBox->setEnabled( false ); + mActionAtlasPreview->blockSignals( false ); + } + else + { + QgisApp::instance()->mapCanvas()->stopRendering(); +#if 0 //TODO + emit atlasPreviewFeatureChanged(); +#endif + } + } + else + { + atlas->endRender(); + } +} + +void QgsLayoutDesignerDialog::atlasPageComboEditingFinished() +{ + QString text = mAtlasPageComboBox->lineEdit()->text(); + + //find matching record in combo box + int page = -1; //note - first page starts at 1, not 0 + for ( int i = 0; i < mAtlasPageComboBox->count(); ++i ) + { + if ( text.compare( mAtlasPageComboBox->itemData( i, Qt::UserRole + 1 ).toString(), Qt::CaseInsensitive ) == 0 + || text.compare( mAtlasPageComboBox->itemData( i, Qt::UserRole + 2 ).toString(), Qt::CaseInsensitive ) == 0 + || QString::number( i + 1 ) == text ) + { + page = i + 1; + break; + } + } + bool ok = ( page > 0 ); + + QgsPrintLayout *printLayout = qobject_cast< QgsPrintLayout * >( mLayout ); + if ( !printLayout ) + return; + QgsLayoutAtlas *atlas = printLayout->atlas(); + + if ( !ok || page > atlas->count() || page < 1 ) + { + whileBlocking( mAtlasPageComboBox )->setCurrentIndex( atlas->currentFeatureNumber() ); + } + else if ( page != atlas->currentFeatureNumber() + 1 ) + { + QgisApp::instance()->mapCanvas()->stopRendering(); +#if 0 //TODO + loadAtlasPredefinedScalesFromProject(); +#endif + atlas->seekTo( page - 1 ); +#if 0 //TODO + emit atlasPreviewFeatureChanged(); +#endif + } +} + +void QgsLayoutDesignerDialog::atlasNext() +{ + QgsLayoutAtlas *printAtlas = atlas(); + if ( !printAtlas ) + return; + + QgisApp::instance()->mapCanvas()->stopRendering(); + +#if 0 //TODO + loadAtlasPredefinedScalesFromProject(); +#endif + if ( printAtlas->next() ) + { +#if 0 //TODO + emit atlasPreviewFeatureChanged(); +#endif + } +} + +void QgsLayoutDesignerDialog::atlasPrevious() +{ + QgsLayoutAtlas *printAtlas = atlas(); + if ( !printAtlas ) + return; + + QgisApp::instance()->mapCanvas()->stopRendering(); + +#if 0 //TODO + loadAtlasPredefinedScalesFromProject(); +#endif + if ( printAtlas->previous() ) + { +#if 0 //TODO + emit atlasPreviewFeatureChanged(); +#endif + } +} + +void QgsLayoutDesignerDialog::atlasFirst() +{ + QgsLayoutAtlas *printAtlas = atlas(); + if ( !printAtlas ) + return; + + QgisApp::instance()->mapCanvas()->stopRendering(); + +#if 0 //TODO + loadAtlasPredefinedScalesFromProject(); +#endif + if ( printAtlas->first() ) + { +#if 0 //TODO + emit atlasPreviewFeatureChanged(); +#endif + } +} + +void QgsLayoutDesignerDialog::atlasLast() +{ + QgsLayoutAtlas *printAtlas = atlas(); + if ( !printAtlas ) + return; + + QgisApp::instance()->mapCanvas()->stopRendering(); + +#if 0 //TODO + loadAtlasPredefinedScalesFromProject(); +#endif + if ( printAtlas->last() ) + { +#if 0 //TODO + emit atlasPreviewFeatureChanged(); +#endif + } +} + +void QgsLayoutDesignerDialog::printAtlas() +{ + //TODO +} + +void QgsLayoutDesignerDialog::exportAtlasToRaster() +{ + //TODO +} + +void QgsLayoutDesignerDialog::exportAtlasToSvg() +{ + //TODO +} + +void QgsLayoutDesignerDialog::exportAtlasToPdf() +{ +//TODO +} + void QgsLayoutDesignerDialog::paste() { QPointF pt = mView->mapFromGlobal( QCursor::pos() ); @@ -1953,11 +2213,26 @@ void QgsLayoutDesignerDialog::createAtlasWidget() mPanelsMenu->addAction( mAtlasDock->toggleViewAction() ); addDockWidget( Qt::RightDockWidgetArea, mAtlasDock ); tabifyDockWidget( mItemDock, mAtlasDock ); + connect( mAtlasDock, &QDockWidget::visibilityChanged, this, &QgsLayoutDesignerDialog::dockVisibilityChanged ); } - QgsLayoutAtlasWidget *atlasWidget = new QgsLayoutAtlasWidget( mGeneralDock, qobject_cast< QgsPrintLayout * >( mLayout ) ); + QgsPrintLayout *printLayout = qobject_cast< QgsPrintLayout * >( mLayout ); + QgsLayoutAtlas *atlas = printLayout->atlas(); + QgsLayoutAtlasWidget *atlasWidget = new QgsLayoutAtlasWidget( mGeneralDock, printLayout ); mAtlasDock->setWidget( atlasWidget ); mAtlasDock->show(); + + mMenuAtlas->show(); + mAtlasToolbar->show(); + + connect( atlas, &QgsLayoutAtlas::messagePushed, mStatusBar, [ = ]( const QString & message ) + { + mStatusBar->showMessage( message ); + } ); + connect( atlas, &QgsLayoutAtlas::toggled, this, &QgsLayoutDesignerDialog::toggleAtlasControls ); + connect( atlas, &QgsLayoutAtlas::numberFeaturesChanged, this, &QgsLayoutDesignerDialog::updateAtlasPageComboBox ); + connect( atlas, &QgsLayoutAtlas::featureChanged, this, &QgsLayoutDesignerDialog::atlasFeatureChanged ); + toggleAtlasControls( atlas->enabled() && atlas->coverageLayer() ); } void QgsLayoutDesignerDialog::initializeRegistry() @@ -2096,6 +2371,86 @@ void QgsLayoutDesignerDialog::showForceVectorWarning() } } +void QgsLayoutDesignerDialog::toggleAtlasControls( bool atlasEnabled ) +{ + //preview defaults to unchecked + mActionAtlasPreview->blockSignals( true ); + mActionAtlasPreview->setChecked( false ); + mActionAtlasFirst->setEnabled( false ); + mActionAtlasLast->setEnabled( false ); + mActionAtlasNext->setEnabled( false ); + mActionAtlasPrev->setEnabled( false ); + mAtlasPageComboBox->setEnabled( false ); + mActionAtlasPreview->blockSignals( false ); + mActionAtlasPreview->setEnabled( atlasEnabled ); + mActionPrintAtlas->setEnabled( atlasEnabled ); + mActionExportAtlasAsImage->setEnabled( atlasEnabled ); + mActionExportAtlasAsSVG->setEnabled( atlasEnabled ); + mActionExportAtlasAsPDF->setEnabled( atlasEnabled ); +} + +void QgsLayoutDesignerDialog::updateAtlasPageComboBox( int pageCount ) +{ + QgsPrintLayout *printLayout = qobject_cast< QgsPrintLayout * >( mLayout ); + if ( !printLayout ) + return; + + QgsLayoutAtlas *atlas = printLayout->atlas(); + mAtlasPageComboBox->blockSignals( true ); + mAtlasPageComboBox->clear(); + for ( int i = 1; i <= pageCount && i < 500; ++i ) + { + QString name = atlas->nameForPage( i - 1 ); + QString fullName = ( !name.isEmpty() ? QStringLiteral( "%1: %2" ).arg( i ).arg( name ) : QString::number( i ) ); + + mAtlasPageComboBox->addItem( fullName, i ); + mAtlasPageComboBox->setItemData( i - 1, name, Qt::UserRole + 1 ); + mAtlasPageComboBox->setItemData( i - 1, fullName, Qt::UserRole + 2 ); + } + mAtlasPageComboBox->blockSignals( false ); + +} + +void QgsLayoutDesignerDialog::atlasFeatureChanged( const QgsFeature &feature ) +{ + //TODO - this should be disabled during an export + + QgsPrintLayout *printLayout = qobject_cast< QgsPrintLayout *>( mLayout ); + if ( !printLayout ) + return; + + QgsLayoutAtlas *atlas = printLayout->atlas(); + + mAtlasPageComboBox->blockSignals( true ); + //prefer to set index of current atlas page, if combo box is showing enough page items + if ( atlas->currentFeatureNumber() < mAtlasPageComboBox->count() ) + { + mAtlasPageComboBox->setCurrentIndex( atlas->currentFeatureNumber() ); + } + else + { + //fallback to setting the combo text to the page number + mAtlasPageComboBox->setEditText( QString::number( atlas->currentFeatureNumber() + 1 ) ); + } + mAtlasPageComboBox->blockSignals( false ); + + //update expression context variables in map canvas to allow for previewing atlas feature based rendering + QgsMapCanvas *mapCanvas = QgisApp::instance()->mapCanvas(); + mapCanvas->expressionContextScope().addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_featurenumber" ), atlas->currentFeatureNumber() + 1, true ) ); + mapCanvas->expressionContextScope().addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_pagename" ), atlas->nameForPage( atlas->currentFeatureNumber() ), true ) ); + mapCanvas->expressionContextScope().addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_feature" ), QVariant::fromValue( feature ), true ) ); + mapCanvas->expressionContextScope().addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_featureid" ), feature.id(), true ) ); + mapCanvas->expressionContextScope().addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_geometry" ), QVariant::fromValue( feature.geometry() ), true ) ); +} + +QgsLayoutAtlas *QgsLayoutDesignerDialog::atlas() +{ + QgsPrintLayout *layout = qobject_cast< QgsPrintLayout *>( mLayout ); + if ( !layout ) + return nullptr; + return layout->atlas(); +} + void QgsLayoutDesignerDialog::selectItems( const QList items ) { for ( QGraphicsItem *item : items ) diff --git a/src/app/layout/qgslayoutdesignerdialog.h b/src/app/layout/qgslayoutdesignerdialog.h index 82a83b712ed1..ed9d6536edea 100644 --- a/src/app/layout/qgslayoutdesignerdialog.h +++ b/src/app/layout/qgslayoutdesignerdialog.h @@ -43,6 +43,8 @@ class QTreeView; class QgsLayoutItemsListView; class QgsLayoutPropertiesWidget; class QgsMessageBar; +class QgsLayoutAtlas; +class QgsFeature; class QgsAppLayoutDesignerInterface : public QgsLayoutDesignerInterface { @@ -285,6 +287,17 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner void exportToRaster(); void exportToPdf(); void exportToSvg(); + void showAtlasSettings(); + void atlasPreviewTriggered( bool checked ); + void atlasPageComboEditingFinished(); + void atlasNext(); + void atlasPrevious(); + void atlasFirst(); + void atlasLast(); + void printAtlas(); + void exportAtlasToRaster(); + void exportAtlasToSvg(); + void exportAtlasToPdf(); private: @@ -363,6 +376,8 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner bool mBlockItemOptions = false; + QComboBox *mAtlasPageComboBox = nullptr; + //! Save window state void saveWindowState(); @@ -392,6 +407,25 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner //! Displays a warning because of incompatibility between blend modes and QPrinter void showRasterizationWarning(); void showForceVectorWarning(); + + void toggleAtlasActions( bool enabled ); + + /** + * Toggles the state of the atlas preview and navigation controls + */ + void toggleAtlasControls( bool atlasEnabled ); + + /** + * Repopulates the atlas page combo box with valid items. + */ + void updateAtlasPageComboBox( int pageCount ); + + + void atlasFeatureChanged( const QgsFeature &feature ); + + + + QgsLayoutAtlas *atlas(); }; #endif // QGSLAYOUTDESIGNERDIALOG_H diff --git a/src/app/layout/qgslayoutmanagerdialog.cpp b/src/app/layout/qgslayoutmanagerdialog.cpp index 61cce2af46ce..7bcea44471cd 100644 --- a/src/app/layout/qgslayoutmanagerdialog.cpp +++ b/src/app/layout/qgslayoutmanagerdialog.cpp @@ -26,6 +26,7 @@ #include "qgslayoutmanager.h" #include "qgsproject.h" #include "qgsgui.h" +#include "qgsprintlayout.h" #include #include @@ -245,7 +246,7 @@ void QgsLayoutManagerDialog::mAddButton_clicked() title = QgsProject::instance()->layoutManager()->generateUniqueTitle(); } - std::unique_ptr< QgsLayout > layout = qgis::make_unique< QgsLayout >( QgsProject::instance() ); + std::unique_ptr< QgsLayout > layout = qgis::make_unique< QgsPrintLayout >( QgsProject::instance() ); if ( loadingTemplate ) { bool loadedOK = false; diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 8089c67f898b..26fb38511950 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -246,6 +246,7 @@ Q_GUI_EXPORT extern int qt_defaultDpiX(); #include "qgspointxy.h" #include "qgsruntimeprofiler.h" #include "qgshandlebadlayers.h" +#include "qgsprintlayout.h" #include "qgsprocessingregistry.h" #include "qgsproject.h" #include "qgsprojectlayergroupdialog.h" @@ -7450,7 +7451,7 @@ QgsLayoutDesignerDialog *QgisApp::createNewLayout( QString title ) title = QgsProject::instance()->layoutManager()->generateUniqueTitle(); } //create new layout object - QgsLayout *layout = new QgsLayout( QgsProject::instance() ); + QgsLayout *layout = new QgsPrintLayout( QgsProject::instance() ); layout->setName( title ); layout->initializeDefaults(); QgsProject::instance()->layoutManager()->addLayout( layout ); diff --git a/src/core/composer/qgslayoutmanager.cpp b/src/core/composer/qgslayoutmanager.cpp index b4942b78c33b..333794a7d75f 100644 --- a/src/core/composer/qgslayoutmanager.cpp +++ b/src/core/composer/qgslayoutmanager.cpp @@ -289,7 +289,7 @@ QgsLayout *QgsLayoutManager::duplicateLayout( const QgsLayout *layout, const QSt QDomElement elem = layout->writeXml( currentDoc, context ); currentDoc.appendChild( elem ); - std::unique_ptr< QgsLayout > newLayout = qgis::make_unique< QgsLayout >( mProject ); + std::unique_ptr< QgsLayout > newLayout = qgis::make_unique< QgsPrintLayout >( mProject ); bool ok = false; newLayout->loadFromTemplate( currentDoc, context, true, &ok ); if ( !ok ) diff --git a/src/core/layout/qgslayoutatlas.cpp b/src/core/layout/qgslayoutatlas.cpp index 6b72200967cb..c53f4ee4372d 100644 --- a/src/core/layout/qgslayoutatlas.cpp +++ b/src/core/layout/qgslayoutatlas.cpp @@ -321,6 +321,7 @@ bool QgsLayoutAtlas::beginRender() bool QgsLayoutAtlas::endRender() { + emit featureChanged( QgsFeature() ); emit renderEnded(); return true; } @@ -362,6 +363,15 @@ bool QgsLayoutAtlas::last() return prepareForFeature( mFeatureIds.size() - 1 ); } +bool QgsLayoutAtlas::seekTo( int feature ) +{ + return prepareForFeature( feature ); +} + +void QgsLayoutAtlas::refreshCurrentFeature() +{ + prepareForFeature( mCurrentFeatureNo ); +} void QgsLayoutAtlas::setHideCoverage( bool hide ) { @@ -497,6 +507,7 @@ bool QgsLayoutAtlas::prepareForFeature( const int featureI ) mLayout->context().blockSignals( false ); mLayout->context().setFeature( mCurrentFeature ); + emit featureChanged( mCurrentFeature ); emit messagePushed( QString( tr( "Atlas feature %1 of %2" ) ).arg( featureI + 1 ).arg( mFeatureIds.size() ) ); if ( !mCurrentFeature.isValid() ) diff --git a/src/core/layout/qgslayoutatlas.h b/src/core/layout/qgslayoutatlas.h index 1859542b5faf..463789722770 100644 --- a/src/core/layout/qgslayoutatlas.h +++ b/src/core/layout/qgslayoutatlas.h @@ -228,12 +228,23 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject, public QgsAbstractLayoutItera bool endRender() override; int count() const override; + /** + * Returns the current feature number, where a value of 0 corresponds to the first feature. + */ + int currentFeatureNumber() const { return mCurrentFeatureNo; } + public slots: bool next() override; bool previous() override; bool first() override; bool last() override; + bool seekTo( int feature ); + + /** + * Refreshes the current atlas feature, by refetching its attributes from the vector layer provider + */ + void refreshCurrentFeature(); signals: @@ -254,6 +265,9 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject, public QgsAbstractLayoutItera */ void numberFeaturesChanged( int numFeatures ); + //! Is emitted when the current atlas \a feature changes. + void featureChanged( const QgsFeature &feature ); + //! Emitted when atlas rendering has begun. void renderBegun(); diff --git a/src/ui/layout/qgslayoutdesignerbase.ui b/src/ui/layout/qgslayoutdesignerbase.ui index 0ec9e68ebc36..436aab2e8163 100644 --- a/src/ui/layout/qgslayoutdesignerbase.ui +++ b/src/ui/layout/qgslayoutdesignerbase.ui @@ -98,7 +98,7 @@ 0 0 1083 - 25 + 42 @@ -251,11 +251,30 @@ + + + Atlas + + + + + + + + + + + + + + + + @@ -275,7 +294,7 @@ - toolBar + Actions TopToolBarArea @@ -288,6 +307,24 @@ + + + Atlas + + + TopToolBarArea + + + false + + + + + + + + + &Close @@ -1213,6 +1250,117 @@ Export as S&VG… + + + + :/images/themes/default/mActionAtlasFirst.svg:/images/themes/default/mActionAtlasFirst.svg + + + &First Feature + + + Ctrl+< + + + + + + :/images/themes/default/mActionAtlasPrev.svg:/images/themes/default/mActionAtlasPrev.svg + + + P&revious Feature + + + Ctrl+, + + + + + + :/images/themes/default/mActionAtlasNext.svg:/images/themes/default/mActionAtlasNext.svg + + + &Next Feature + + + Ctrl+. + + + + + + :/images/themes/default/mActionAtlasLast.svg:/images/themes/default/mActionAtlasLast.svg + + + &Last Feature + + + Ctrl+> + + + + + + :/images/themes/default/mActionFilePrint.svg:/images/themes/default/mActionFilePrint.svg + + + &Print Atlas... + + + + + + :/images/themes/default/mActionSaveMapAsImage.svg:/images/themes/default/mActionSaveMapAsImage.svg + + + Export Atlas as &Images... + + + + + + :/images/themes/default/mActionSaveAsSVG.svg:/images/themes/default/mActionSaveAsSVG.svg + + + Export Atlas as S&VG... + + + + + + :/images/themes/default/mActionSaveAsPDF.svg:/images/themes/default/mActionSaveAsPDF.svg + + + &Export Atlas as PDF... + + + Export Atlas as PDF + + + + + + :/images/themes/default/mActionAtlasSettings.svg:/images/themes/default/mActionAtlasSettings.svg + + + Atlas &Settings + + + + + true + + + + :/images/themes/default/mIconAtlas.svg:/images/themes/default/mIconAtlas.svg + + + Preview &Atlas + + + Ctrl+Alt+/ + + @@ -1242,7 +1390,6 @@ - From 108c9548a7d7b899580474dd60134ae00cf29c14 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 20 Dec 2017 07:31:22 +1000 Subject: [PATCH 009/105] Atlas sort can be an expression --- src/app/layout/qgslayoutatlaswidget.cpp | 34 +++++++++++------------ src/app/layout/qgslayoutatlaswidget.h | 9 ++---- src/ui/layout/qgslayoutatlaswidgetbase.ui | 18 ++++-------- 3 files changed, 25 insertions(+), 36 deletions(-) diff --git a/src/app/layout/qgslayoutatlaswidget.cpp b/src/app/layout/qgslayoutatlaswidget.cpp index 85a79122401f..a7d129e7efc8 100644 --- a/src/app/layout/qgslayoutatlaswidget.cpp +++ b/src/app/layout/qgslayoutatlaswidget.cpp @@ -43,15 +43,15 @@ QgsLayoutAtlasWidget::QgsLayoutAtlasWidget( QWidget *parent, QgsPrintLayout *lay mAtlasCoverageLayerComboBox->setFilters( QgsMapLayerProxyModel::VectorLayer ); - connect( mAtlasCoverageLayerComboBox, &QgsMapLayerComboBox::layerChanged, mAtlasSortFeatureKeyComboBox, &QgsFieldComboBox::setLayer ); + connect( mAtlasCoverageLayerComboBox, &QgsMapLayerComboBox::layerChanged, mAtlasSortExpressionWidget, &QgsFieldExpressionWidget::setLayer ); connect( mAtlasCoverageLayerComboBox, &QgsMapLayerComboBox::layerChanged, mPageNameWidget, &QgsFieldExpressionWidget::setLayer ); connect( mAtlasCoverageLayerComboBox, &QgsMapLayerComboBox::layerChanged, this, &QgsLayoutAtlasWidget::changeCoverageLayer ); - connect( mAtlasSortFeatureKeyComboBox, &QgsFieldComboBox::fieldChanged, this, &QgsLayoutAtlasWidget::changesSortFeatureField ); + connect( mAtlasSortExpressionWidget, static_cast < void ( QgsFieldExpressionWidget::* )( const QString &, bool ) > ( &QgsFieldExpressionWidget::fieldChanged ), this, &QgsLayoutAtlasWidget::changesSortFeatureExpression ); connect( mPageNameWidget, static_cast < void ( QgsFieldExpressionWidget::* )( const QString &, bool ) > ( &QgsFieldExpressionWidget::fieldChanged ), this, &QgsLayoutAtlasWidget::pageNameExpressionChanged ); // Sort direction mAtlasSortFeatureDirectionButton->setEnabled( false ); - mAtlasSortFeatureKeyComboBox->setEnabled( false ); + mAtlasSortExpressionWidget->setEnabled( false ); // connect to updates connect( mAtlas, &QgsLayoutAtlas::changed, this, &QgsLayoutAtlasWidget::updateGuiElements ); @@ -182,12 +182,12 @@ void QgsLayoutAtlasWidget::mAtlasSortFeatureCheckBox_stateChanged( int state ) if ( state == Qt::Checked ) { mAtlasSortFeatureDirectionButton->setEnabled( true ); - mAtlasSortFeatureKeyComboBox->setEnabled( true ); + mAtlasSortExpressionWidget->setEnabled( true ); } else { mAtlasSortFeatureDirectionButton->setEnabled( false ); - mAtlasSortFeatureKeyComboBox->setEnabled( false ); + mAtlasSortExpressionWidget->setEnabled( false ); } mLayout->undoStack()->beginCommand( mAtlas, tr( "Toggle Atlas Sorting" ) ); mAtlas->setSortFeatures( state == Qt::Checked ); @@ -195,6 +195,14 @@ void QgsLayoutAtlasWidget::mAtlasSortFeatureCheckBox_stateChanged( int state ) updateAtlasFeatures(); } +void QgsLayoutAtlasWidget::changesSortFeatureExpression( const QString &expression, bool ) +{ + mLayout->undoStack()->beginCommand( mAtlas, tr( "Change Atlas Sort" ) ); + mAtlas->setSortExpression( expression ); + mLayout->undoStack()->endCommand(); + updateAtlasFeatures(); +} + void QgsLayoutAtlasWidget::updateAtlasFeatures() { #if 0 //TODO @@ -213,14 +221,6 @@ void QgsLayoutAtlasWidget::updateAtlasFeatures() #endif } -void QgsLayoutAtlasWidget::changesSortFeatureField( const QString &fieldName ) -{ - mLayout->undoStack()->beginCommand( mAtlas, tr( "Change Atlas Sort" ) ); - mAtlas->setSortExpression( fieldName ); - mLayout->undoStack()->endCommand(); - updateAtlasFeatures(); -} - void QgsLayoutAtlasWidget::mAtlasFeatureFilterCheckBox_stateChanged( int state ) { if ( state == Qt::Checked ) @@ -319,8 +319,8 @@ void QgsLayoutAtlasWidget::updateGuiElements() mPageNameWidget->setLayer( mAtlas->coverageLayer() ); mPageNameWidget->setField( mAtlas->pageNameExpression() ); - mAtlasSortFeatureKeyComboBox->setLayer( mAtlas->coverageLayer() ); - mAtlasSortFeatureKeyComboBox->setField( mAtlas->sortExpression() ); + mAtlasSortExpressionWidget->setLayer( mAtlas->coverageLayer() ); + mAtlasSortExpressionWidget->setField( mAtlas->sortExpression() ); mAtlasFilenamePatternEdit->setText( mAtlas->filenameExpression() ); mAtlasHideCoverageCheckBox->setCheckState( mAtlas->hideCoverage() ? Qt::Checked : Qt::Unchecked ); @@ -333,7 +333,7 @@ void QgsLayoutAtlasWidget::updateGuiElements() mAtlasSortFeatureCheckBox->setCheckState( mAtlas->sortFeatures() ? Qt::Checked : Qt::Unchecked ); mAtlasSortFeatureDirectionButton->setEnabled( mAtlas->sortFeatures() ); - mAtlasSortFeatureKeyComboBox->setEnabled( mAtlas->sortFeatures() ); + mAtlasSortExpressionWidget->setEnabled( mAtlas->sortFeatures() ); mAtlasSortFeatureDirectionButton->setArrowType( mAtlas->sortAscending() ? Qt::UpArrow : Qt::DownArrow ); mAtlasFeatureFilterEdit->setText( mAtlas->filterExpression() ); @@ -356,7 +356,7 @@ void QgsLayoutAtlasWidget::blockAllSignals( bool b ) mOutputGroup->blockSignals( b ); mAtlasCoverageLayerComboBox->blockSignals( b ); mPageNameWidget->blockSignals( b ); - mAtlasSortFeatureKeyComboBox->blockSignals( b ); + mAtlasSortExpressionWidget->blockSignals( b ); mAtlasFilenamePatternEdit->blockSignals( b ); mAtlasHideCoverageCheckBox->blockSignals( b ); mAtlasSingleFileCheckBox->blockSignals( b ); diff --git a/src/app/layout/qgslayoutatlaswidget.h b/src/app/layout/qgslayoutatlaswidget.h index c3f5ab1ab27a..77a7cd78a6d7 100644 --- a/src/app/layout/qgslayoutatlaswidget.h +++ b/src/app/layout/qgslayoutatlaswidget.h @@ -29,27 +29,22 @@ class QgsLayoutAtlasWidget: public QWidget, private Ui::QgsLayoutAtlasWidgetBase public: QgsLayoutAtlasWidget( QWidget *parent, QgsPrintLayout *layout ); - public slots: + private slots: void mUseAtlasCheckBox_stateChanged( int state ); void changeCoverageLayer( QgsMapLayer *layer ); void mAtlasFilenamePatternEdit_editingFinished(); void mAtlasFilenameExpressionButton_clicked(); void mAtlasHideCoverageCheckBox_stateChanged( int state ); void mAtlasSingleFileCheckBox_stateChanged( int state ); - void mAtlasSortFeatureCheckBox_stateChanged( int state ); - void changesSortFeatureField( const QString &fieldName ); + void changesSortFeatureExpression( const QString &expression, bool valid ); void mAtlasSortFeatureDirectionButton_clicked(); void mAtlasFeatureFilterEdit_editingFinished(); void mAtlasFeatureFilterButton_clicked(); void mAtlasFeatureFilterCheckBox_stateChanged( int state ); void pageNameExpressionChanged( const QString &expression, bool valid ); - void changeFileFormat(); - - private slots: void updateGuiElements(); - void updateAtlasFeatures(); private: diff --git a/src/ui/layout/qgslayoutatlaswidgetbase.ui b/src/ui/layout/qgslayoutatlaswidgetbase.ui index 5d239e606a11..d63507b8d137 100644 --- a/src/ui/layout/qgslayoutatlaswidgetbase.ui +++ b/src/ui/layout/qgslayoutatlaswidgetbase.ui @@ -109,10 +109,10 @@ - 0 - -63 - 417 - 389 + -122 + -223 + 525 + 673 @@ -154,7 +154,7 @@ - + @@ -342,11 +342,6 @@ QComboBox
qgsmaplayercombobox.h
- - QgsFieldComboBox - QComboBox -
qgsfieldcombobox.h
-
QgsFieldExpressionWidget QWidget @@ -362,7 +357,7 @@ mAtlasFeatureFilterEdit mAtlasFeatureFilterButton mAtlasSortFeatureCheckBox - mAtlasSortFeatureKeyComboBox + mAtlasSortExpressionWidget mAtlasSortFeatureDirectionButton mOutputGroup mAtlasFilenamePatternEdit @@ -398,7 +393,6 @@ - From b7596970bc905cb99ff1558d8d2709a63631d7ac Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 20 Dec 2017 07:35:56 +1000 Subject: [PATCH 010/105] Remove outdated TODO --- src/core/layout/qgslayoutatlas.cpp | 8 -------- tests/src/python/test_qgslayoutatlas.py | 2 ++ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/core/layout/qgslayoutatlas.cpp b/src/core/layout/qgslayoutatlas.cpp index c53f4ee4372d..8ec670213f55 100644 --- a/src/core/layout/qgslayoutatlas.cpp +++ b/src/core/layout/qgslayoutatlas.cpp @@ -290,14 +290,6 @@ int QgsLayoutAtlas::updateFeatures() } emit numberFeaturesChanged( mFeatureIds.size() ); - -#if 0 //TODO - move to app - //jump to first feature if currently using an atlas preview - //need to do this in case filtering/layer change has altered matching features - return first(); -#endif - - return mFeatureIds.size(); } diff --git a/tests/src/python/test_qgslayoutatlas.py b/tests/src/python/test_qgslayoutatlas.py index ac2623761a27..9310c3edb53d 100644 --- a/tests/src/python/test_qgslayoutatlas.py +++ b/tests/src/python/test_qgslayoutatlas.py @@ -86,6 +86,8 @@ def testReadWriteXml(self): self.assertTrue(atlas2.filterFeatures()) self.assertEqual(atlas2.filterExpression(), 'filter exp') + def test + if __name__ == '__main__': unittest.main() From 171f402ab0b19f34b7a954ccd14cb2f7b2c27525 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 20 Dec 2017 08:11:58 +1000 Subject: [PATCH 011/105] Use messagebar for atlas messages instead of message box --- src/app/layout/qgslayoutatlaswidget.cpp | 55 +++++++++++++--------- src/app/layout/qgslayoutatlaswidget.h | 3 ++ src/app/layout/qgslayoutdesignerdialog.cpp | 1 + src/core/layout/qgslayoutatlas.cpp | 9 ++++ 4 files changed, 46 insertions(+), 22 deletions(-) diff --git a/src/app/layout/qgslayoutatlaswidget.cpp b/src/app/layout/qgslayoutatlaswidget.cpp index a7d129e7efc8..db61ab845020 100644 --- a/src/app/layout/qgslayoutatlaswidget.cpp +++ b/src/app/layout/qgslayoutatlaswidget.cpp @@ -16,13 +16,13 @@ #include #include -#include #include "qgslayoutatlaswidget.h" #include "qgsprintlayout.h" #include "qgslayoutatlas.h" #include "qgsexpressionbuilderdialog.h" #include "qgslayoutundostack.h" +#include "qgsmessagebar.h" QgsLayoutAtlasWidget::QgsLayoutAtlasWidget( QWidget *parent, QgsPrintLayout *layout ) : QWidget( parent ) @@ -68,6 +68,11 @@ QgsLayoutAtlasWidget::QgsLayoutAtlasWidget( QWidget *parent, QgsPrintLayout *lay updateGuiElements(); } +void QgsLayoutAtlasWidget::setMessageBar( QgsMessageBar *bar ) +{ + mMessageBar = bar; +} + void QgsLayoutAtlasWidget::mUseAtlasCheckBox_stateChanged( int state ) { if ( state == Qt::Checked ) @@ -108,12 +113,10 @@ void QgsLayoutAtlasWidget::mAtlasFilenamePatternEdit_editingFinished() if ( !mAtlas->setFilenameExpression( mAtlasFilenamePatternEdit->text(), error ) ) { //expression could not be set - QMessageBox::warning( this - , tr( "Could not evaluate filename pattern" ) - , tr( "Could not set filename pattern as '%1'.\nParser error:\n%2" ) - .arg( mAtlasFilenamePatternEdit->text(), - error ) - ); + mMessageBar->pushWarning( tr( "Atlas" ), + tr( "Could not set filename expression to '%1'.\nParser error:\n%2" ) + .arg( mAtlasFilenamePatternEdit->text(), + error ) ); } mLayout->undoStack()->endCommand(); } @@ -141,12 +144,9 @@ void QgsLayoutAtlasWidget::mAtlasFilenameExpressionButton_clicked() if ( !mAtlas->setFilenameExpression( expression, error ) ) { //expression could not be set - QMessageBox::warning( this - , tr( "Could not evaluate filename pattern" ) - , tr( "Could not set filename pattern as '%1'.\nParser error:\n%2" ) - .arg( expression, - error ) - ); + mMessageBar->pushWarning( tr( "Atlas" ), tr( "Could not set filename expression to '%1'.\nParser error:\n%2" ) + .arg( expression, + error ) ); } mLayout->undoStack()->endCommand(); } @@ -205,20 +205,15 @@ void QgsLayoutAtlasWidget::changesSortFeatureExpression( const QString &expressi void QgsLayoutAtlasWidget::updateAtlasFeatures() { -#if 0 //TODO bool updated = mAtlas->updateFeatures(); if ( !updated ) { - QMessageBox::warning( nullptr, tr( "Atlas preview" ), - tr( "No matching atlas features found!" ), - QMessageBox::Ok, - QMessageBox::Ok ); + mMessageBar->pushInfo( tr( "Atlas" ), + tr( "No matching atlas features found!" ) ); //Perhaps atlas preview should be disabled now? If so, it may get annoying if user is editing //the filter expression and it keeps disabling itself. - return; } -#endif } void QgsLayoutAtlasWidget::mAtlasFeatureFilterCheckBox_stateChanged( int state ) @@ -256,7 +251,15 @@ void QgsLayoutAtlasWidget::mAtlasFeatureFilterEdit_editingFinished() { QString error; mLayout->undoStack()->beginCommand( mAtlas, tr( "Change Atlas Filter" ) ); - mAtlas->setFilterExpression( mAtlasFeatureFilterEdit->text(), error ); + + if ( !mAtlas->setFilterExpression( mAtlasFeatureFilterEdit->text(), error ) ) + { + //expression could not be set + mMessageBar->pushWarning( tr( "Atlas" ), tr( "Could not set filter expression to '%1'.\nParser error:\n%2" ) + .arg( mAtlasFeatureFilterEdit->text(), + error ) ); + } + mLayout->undoStack()->endCommand(); updateAtlasFeatures(); } @@ -282,7 +285,15 @@ void QgsLayoutAtlasWidget::mAtlasFeatureFilterButton_clicked() mAtlasFeatureFilterEdit->setText( expression ); QString error; mLayout->undoStack()->beginCommand( mAtlas, tr( "Change Atlas Filter" ) ); - mAtlas->setFilterExpression( mAtlasFeatureFilterEdit->text(), error ); + if ( !mAtlas->setFilterExpression( mAtlasFeatureFilterEdit->text(), error ) ) + { + //expression could not be set + mMessageBar->pushWarning( tr( "Atlas" ), + tr( "Could not set filter expression to '%1'.\nParser error:\n%2" ) + .arg( mAtlasFeatureFilterEdit->text(), + error ) + ); + } mLayout->undoStack()->endCommand(); updateAtlasFeatures(); } diff --git a/src/app/layout/qgslayoutatlaswidget.h b/src/app/layout/qgslayoutatlaswidget.h index 77a7cd78a6d7..ab638d8261bc 100644 --- a/src/app/layout/qgslayoutatlaswidget.h +++ b/src/app/layout/qgslayoutatlaswidget.h @@ -18,6 +18,7 @@ class QgsPrintLayout; class QgsLayoutAtlas; +class QgsMessageBar; /** * \ingroup app @@ -28,6 +29,7 @@ class QgsLayoutAtlasWidget: public QWidget, private Ui::QgsLayoutAtlasWidgetBase Q_OBJECT public: QgsLayoutAtlasWidget( QWidget *parent, QgsPrintLayout *layout ); + void setMessageBar( QgsMessageBar *bar ); private slots: void mUseAtlasCheckBox_stateChanged( int state ); @@ -50,6 +52,7 @@ class QgsLayoutAtlasWidget: public QWidget, private Ui::QgsLayoutAtlasWidgetBase private: QgsPrintLayout *mLayout = nullptr; QgsLayoutAtlas *mAtlas = nullptr; + QgsMessageBar *mMessageBar = nullptr; void blockAllSignals( bool b ); void checkLayerType( QgsVectorLayer *layer ); diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 7324d6d2d615..6a4db7aea097 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -2219,6 +2219,7 @@ void QgsLayoutDesignerDialog::createAtlasWidget() QgsPrintLayout *printLayout = qobject_cast< QgsPrintLayout * >( mLayout ); QgsLayoutAtlas *atlas = printLayout->atlas(); QgsLayoutAtlasWidget *atlasWidget = new QgsLayoutAtlasWidget( mGeneralDock, printLayout ); + atlasWidget->setMessageBar( mMessageBar ); mAtlasDock->setWidget( atlasWidget ); mAtlasDock->show(); diff --git a/src/core/layout/qgslayoutatlas.cpp b/src/core/layout/qgslayoutatlas.cpp index 8ec670213f55..19d7bd1ab40b 100644 --- a/src/core/layout/qgslayoutatlas.cpp +++ b/src/core/layout/qgslayoutatlas.cpp @@ -162,7 +162,16 @@ QString QgsLayoutAtlas::nameForPage( int pageNumber ) const bool QgsLayoutAtlas::setFilterExpression( const QString &expression, QString &errorString ) { + errorString.clear(); mFilterExpression = expression; + + QgsExpression filterExpression( mFilterExpression ); + if ( filterExpression.hasParserError() ) + { + errorString = filterExpression.parserErrorString(); + return false; + } + return true; } From 60a28e32b7591979b21e03fbe2e41dd876b69ea1 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 20 Dec 2017 08:17:45 +1000 Subject: [PATCH 012/105] Add some unit tests --- python/core/layout/qgslayoutatlas.sip | 13 ++- src/core/layout/qgslayoutatlas.cpp | 18 ++- src/core/layout/qgslayoutatlas.h | 10 +- tests/src/python/test_qgslayoutatlas.py | 144 +++++++++++++++++++++++- 4 files changed, 172 insertions(+), 13 deletions(-) diff --git a/python/core/layout/qgslayoutatlas.sip b/python/core/layout/qgslayoutatlas.sip index d68bfdfa234f..38e742b44ec5 100644 --- a/python/core/layout/qgslayoutatlas.sip +++ b/python/core/layout/qgslayoutatlas.sip @@ -78,7 +78,7 @@ atlas page. .. seealso:: :py:func:`setFilenameExpression()` -.. seealso:: :py:func:`filenameExpressionErrorString()` +.. seealso:: :py:func:`currentFilename()` %End bool setFilenameExpression( const QString &expression, QString &errorString /Out/ ); @@ -89,6 +89,17 @@ If an invalid expression is passed, false will be returned and ``errorString`` will be set to the expression error. .. seealso:: :py:func:`filenameExpression()` + +.. seealso:: :py:func:`currentFilename()` +%End + + QString currentFilename() const; +%Docstring +Returns the current feature filename. + +.. seealso:: :py:func:`filenameExpression()` + +.. seealso:: :py:func:`setFilenameExpression()` %End QgsVectorLayer *coverageLayer() const; diff --git a/src/core/layout/qgslayoutatlas.cpp b/src/core/layout/qgslayoutatlas.cpp index 19d7bd1ab40b..f39fe25afb58 100644 --- a/src/core/layout/qgslayoutatlas.cpp +++ b/src/core/layout/qgslayoutatlas.cpp @@ -394,6 +394,11 @@ bool QgsLayoutAtlas::setFilenameExpression( const QString &pattern, QString &err return updateFilenameExpression( errorString ); } +QString QgsLayoutAtlas::currentFilename() const +{ + return mCurrentFilename; +} + QgsExpressionContext QgsLayoutAtlas::createExpressionContext() { QgsExpressionContext expressionContext; @@ -408,10 +413,9 @@ QgsExpressionContext QgsLayoutAtlas::createExpressionContext() if ( mCoverageLayer ) expressionContext.lastScope()->setFields( mCoverageLayer->fields() ); -#if 0 //TODO + if ( mLayout && mEnabled ) expressionContext.lastScope()->setFeature( mCurrentFeature ); -#endif return expressionContext; } @@ -440,13 +444,8 @@ bool QgsLayoutAtlas::updateFilenameExpression( QString &error ) mFilenameExpression.prepare( &expressionContext ); } -#if 0 //TODO - //if atlas preview is currently enabled, regenerate filename for current feature - if ( mComposition->atlasMode() == QgsComposition::PreviewAtlas ) - { - evalFeatureFilename( expressionContext ); - } -#endif + // regenerate current filename + evalFeatureFilename( expressionContext ); return true; } @@ -496,7 +495,6 @@ bool QgsLayoutAtlas::prepareForFeature( const int featureI ) // generate filename for current feature if ( !evalFeatureFilename( expressionContext ) ) { - //error evaluating filename return false; } diff --git a/src/core/layout/qgslayoutatlas.h b/src/core/layout/qgslayoutatlas.h index 463789722770..c2c1ba8aa435 100644 --- a/src/core/layout/qgslayoutatlas.h +++ b/src/core/layout/qgslayoutatlas.h @@ -77,7 +77,7 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject, public QgsAbstractLayoutItera * Returns the filename expression used for generating output filenames for each * atlas page. * \see setFilenameExpression() - * \see filenameExpressionErrorString() + * \see currentFilename() */ QString filenameExpression() const { return mFilenameExpressionString; } @@ -87,9 +87,17 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject, public QgsAbstractLayoutItera * If an invalid expression is passed, false will be returned and \a errorString * will be set to the expression error. * \see filenameExpression() + * \see currentFilename() */ bool setFilenameExpression( const QString &expression, QString &errorString SIP_OUT ); + /** + * Returns the current feature filename. + * \see filenameExpression() + * \see setFilenameExpression() + */ + QString currentFilename() const; + /** * Returns the coverage layer used for the atlas features. * \see setCoverageLayer() diff --git a/tests/src/python/test_qgslayoutatlas.py b/tests/src/python/test_qgslayoutatlas.py index 9310c3edb53d..12cbfdabf227 100644 --- a/tests/src/python/test_qgslayoutatlas.py +++ b/tests/src/python/test_qgslayoutatlas.py @@ -43,6 +43,7 @@ from qgis.PyQt.QtXml import QDomDocument from utilities import unitTestDataPath from qgis.testing import start_app, unittest +from qgis.PyQt.QtTest import QSignalSpy start_app() @@ -86,7 +87,148 @@ def testReadWriteXml(self): self.assertTrue(atlas2.filterFeatures()) self.assertEqual(atlas2.filterExpression(), 'filter exp') - def test + def testIteration(self): + p = QgsProject() + vectorFileInfo = QFileInfo(unitTestDataPath() + "/france_parts.shp") + vector_layer = QgsVectorLayer(vectorFileInfo.filePath(), vectorFileInfo.completeBaseName(), "ogr") + self.assertTrue(vector_layer.isValid()) + p.addMapLayer(vector_layer) + + l = QgsPrintLayout(p) + atlas = l.atlas() + atlas.setEnabled(True) + atlas.setCoverageLayer(vector_layer) + + atlas_feature_changed_spy = QSignalSpy(atlas.featureChanged) + context_changed_spy = QSignalSpy(l.context().changed) + + self.assertTrue(atlas.beginRender()) + self.assertTrue(atlas.first()) + self.assertEqual(len(atlas_feature_changed_spy), 1) + self.assertEqual(len(context_changed_spy), 1) + self.assertEqual(atlas.currentFeatureNumber(), 0) + self.assertEqual(l.context().feature()[4], 'Basse-Normandie') + self.assertEqual(l.context().layer(), vector_layer) + + self.assertTrue(atlas.next()) + self.assertEqual(len(atlas_feature_changed_spy), 2) + self.assertEqual(len(context_changed_spy), 2) + self.assertEqual(atlas.currentFeatureNumber(), 1) + self.assertEqual(l.context().feature()[4], 'Bretagne') + + self.assertTrue(atlas.next()) + self.assertEqual(len(atlas_feature_changed_spy), 3) + self.assertEqual(len(context_changed_spy), 3) + self.assertEqual(atlas.currentFeatureNumber(), 2) + self.assertEqual(l.context().feature()[4], 'Pays de la Loire') + + self.assertTrue(atlas.next()) + self.assertEqual(len(atlas_feature_changed_spy), 4) + self.assertEqual(len(context_changed_spy), 4) + self.assertEqual(atlas.currentFeatureNumber(), 3) + self.assertEqual(l.context().feature()[4], 'Centre') + + self.assertFalse(atlas.next()) + self.assertTrue(atlas.seekTo(2)) + self.assertEqual(len(atlas_feature_changed_spy), 5) + self.assertEqual(len(context_changed_spy), 5) + self.assertEqual(atlas.currentFeatureNumber(), 2) + self.assertEqual(l.context().feature()[4], 'Pays de la Loire') + + self.assertTrue(atlas.last()) + self.assertEqual(len(atlas_feature_changed_spy), 6) + self.assertEqual(len(context_changed_spy), 6) + self.assertEqual(atlas.currentFeatureNumber(), 3) + self.assertEqual(l.context().feature()[4], 'Centre') + + self.assertTrue(atlas.previous()) + self.assertEqual(len(atlas_feature_changed_spy), 7) + self.assertEqual(len(context_changed_spy), 7) + self.assertEqual(atlas.currentFeatureNumber(), 2) + self.assertEqual(l.context().feature()[4], 'Pays de la Loire') + + self.assertTrue(atlas.previous()) + self.assertTrue(atlas.previous()) + self.assertEqual(len(atlas_feature_changed_spy), 9) + self.assertFalse(atlas.previous()) + self.assertEqual(len(atlas_feature_changed_spy), 9) + + self.assertTrue(atlas.endRender()) + self.assertEqual(len(atlas_feature_changed_spy), 10) + + def testUpdateFeature(self): + p = QgsProject() + vectorFileInfo = QFileInfo(unitTestDataPath() + "/france_parts.shp") + vector_layer = QgsVectorLayer(vectorFileInfo.filePath(), vectorFileInfo.completeBaseName(), "ogr") + self.assertTrue(vector_layer.isValid()) + p.addMapLayer(vector_layer) + + l = QgsPrintLayout(p) + atlas = l.atlas() + atlas.setEnabled(True) + atlas.setCoverageLayer(vector_layer) + + self.assertTrue(atlas.beginRender()) + self.assertTrue(atlas.first()) + self.assertEqual(atlas.currentFeatureNumber(), 0) + self.assertEqual(l.context().feature()[4], 'Basse-Normandie') + self.assertEqual(l.context().layer(), vector_layer) + + vector_layer.startEditing() + self.assertTrue(vector_layer.changeAttributeValue(l.context().feature().id(), 4, 'Nah, Canberra mate!')) + self.assertEqual(l.context().feature()[4], 'Basse-Normandie') + l.atlas().refreshCurrentFeature() + self.assertEqual(l.context().feature()[4], 'Nah, Canberra mate!') + vector_layer.rollBack() + + def testFileName(self): + p = QgsProject() + vectorFileInfo = QFileInfo(unitTestDataPath() + "/france_parts.shp") + vector_layer = QgsVectorLayer(vectorFileInfo.filePath(), vectorFileInfo.completeBaseName(), "ogr") + self.assertTrue(vector_layer.isValid()) + p.addMapLayer(vector_layer) + + l = QgsPrintLayout(p) + atlas = l.atlas() + atlas.setEnabled(True) + atlas.setCoverageLayer(vector_layer) + atlas.setFilenameExpression("'output_' || \"NAME_1\"") + + self.assertTrue(atlas.beginRender()) + self.assertEqual(atlas.count(), 4) + atlas.first() + self.assertEqual(atlas.currentFilename(), 'output_Basse-Normandie') + atlas.next() + self.assertEqual(atlas.currentFilename(), 'output_Bretagne') + atlas.next() + self.assertEqual(atlas.currentFilename(), 'output_Pays de la Loire') + atlas.next() + self.assertEqual(atlas.currentFilename(), 'output_Centre') + + # try changing expression, filename should be updated instantly + atlas.setFilenameExpression("'export_' || \"NAME_1\"") + self.assertEqual(atlas.currentFilename(), 'export_Centre') + + atlas.endRender() + + def testNameForPage(self): + p = QgsProject() + vectorFileInfo = QFileInfo(unitTestDataPath() + "/france_parts.shp") + vector_layer = QgsVectorLayer(vectorFileInfo.filePath(), vectorFileInfo.completeBaseName(), "ogr") + self.assertTrue(vector_layer.isValid()) + p.addMapLayer(vector_layer) + + l = QgsPrintLayout(p) + atlas = l.atlas() + atlas.setEnabled(True) + atlas.setCoverageLayer(vector_layer) + atlas.setPageNameExpression("\"NAME_1\"") + + self.assertTrue(atlas.beginRender()) + self.assertEqual(atlas.nameForPage(0), 'Basse-Normandie') + self.assertEqual(atlas.nameForPage(1), 'Bretagne') + self.assertEqual(atlas.nameForPage(2), 'Pays de la Loire') + self.assertEqual(atlas.nameForPage(3), 'Centre') if __name__ == '__main__': From 3318bfbb4d7be20f6fbc38bcd31233d6f2645962 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 20 Dec 2017 08:31:17 +1000 Subject: [PATCH 013/105] Restore atlas based autogenerated filenames --- src/app/layout/qgslayoutdesignerdialog.cpp | 33 ++++++++-------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 6a4db7aea097..427bf19da275 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -1530,18 +1530,15 @@ void QgsLayoutDesignerDialog::exportToRaster() imageDlg.setGenerateWorldFile( mLayout->customProperty( QStringLiteral( "exportWorldFile" ), false ).toBool() ); imageDlg.setAntialiasing( antialias ); -#if 0 //TODO - QgsAtlasComposition *atlasMap = &mComposition->atlasComposition(); -#endif + QgsLayoutAtlas *printAtlas = atlas(); + QgsSettings s; QString outputFileName = QgsFileUtils::stringToSafeFilename( mLayout->name() ); -#if 0 //TODO - if ( atlasMap->enabled() && mComposition->atlasMode() == QgsComposition::PreviewAtlas ) + if ( printAtlas && printAtlas->enabled() && mActionAtlasPreview->isChecked() ) { - QString lastUsedDir = settings.value( QStringLiteral( "UI/lastSaveAsImageDir" ), QDir::homePath() ).toString(); - outputFileName = QDir( lastUsedDir ).filePath( atlasMap->currentFilename() ); + QString lastUsedDir = s.value( QStringLiteral( "UI/lastSaveAsImageDir" ), QDir::homePath() ).toString(); + outputFileName = QDir( lastUsedDir ).filePath( QgsFileUtils::stringToSafeFilename( printAtlas->currentFilename() ) ); } -#endif #ifdef Q_OS_MAC QgisApp::instance()->activateWindow(); @@ -1647,19 +1644,15 @@ void QgsLayoutDesignerDialog::exportToPdf() QFileInfo file( lastUsedFile ); QString outputFileName; -#if 0// TODO - if ( hasAnAtlas && !atlasOnASingleFile && - ( mode == QgsComposer::Atlas || mComposition->atlasMode() == QgsComposition::PreviewAtlas ) ) + QgsLayoutAtlas *printAtlas = atlas(); + if ( printAtlas && printAtlas->enabled() && mActionAtlasPreview->isChecked() ) { - outputFileName = QDir( file.path() ).filePath( atlasMap->currentFilename() ) + ".pdf"; + outputFileName = QDir( file.path() ).filePath( QgsFileUtils::stringToSafeFilename( printAtlas->currentFilename() ) + QStringLiteral( ".pdf" ) ); } else { -#endif outputFileName = file.path() + '/' + QgsFileUtils::stringToSafeFilename( mLayout->name() ) + QStringLiteral( ".pdf" ); -#if 0 //TODO } -#endif #ifdef Q_OS_MAC QgisApp::instance()->activateWindow(); @@ -1751,19 +1744,15 @@ void QgsLayoutDesignerDialog::exportToSvg() QFileInfo file( lastUsedFile ); QString outputFileName = QgsFileUtils::stringToSafeFilename( mLayout->name() ); -#if 0// TODO - if ( hasAnAtlas && !atlasOnASingleFile && - ( mode == QgsComposer::Atlas || mComposition->atlasMode() == QgsComposition::PreviewAtlas ) ) + QgsLayoutAtlas *printAtlas = atlas(); + if ( printAtlas && printAtlas->enabled() && mActionAtlasPreview->isChecked() ) { - outputFileName = QDir( file.path() ).filePath( atlasMap->currentFilename() ) + ".pdf"; + outputFileName = QDir( file.path() ).filePath( QgsFileUtils::stringToSafeFilename( printAtlas->currentFilename() + QStringLiteral( ".svg" ) ) ); } else { -#endif outputFileName = file.path() + '/' + QgsFileUtils::stringToSafeFilename( mLayout->name() ) + QStringLiteral( ".svg" ); -#if 0 //TODO } -#endif #ifdef Q_OS_MAC QgisApp::instance()->activateWindow(); From 5d1d25b36b3d5bc5b1878e043b8f13fb6408cab2 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 20 Dec 2017 17:37:10 +1000 Subject: [PATCH 014/105] Add atlas expression context scope to layouts --- python/core/layout/qgslayoutatlas.sip | 2 + python/core/layout/qgsprintlayout.sip | 2 + python/core/qgsexpressioncontext.sip | 10 ++++- src/app/qgsdiagramproperties.cpp | 4 +- src/app/qgslabelinggui.cpp | 2 +- src/app/qgslabelpropertydialog.cpp | 2 +- src/app/qgspointmarkeritem.cpp | 2 +- src/app/qgsrulebasedlabelingwidget.cpp | 2 +- src/app/qgsvectorlayerproperties.cpp | 2 +- src/core/composer/qgsatlascomposition.cpp | 2 +- src/core/composer/qgscomposition.cpp | 2 +- src/core/layout/qgslayout.cpp | 6 --- src/core/layout/qgslayoutatlas.cpp | 5 +++ src/core/layout/qgslayoutatlas.h | 7 ++++ src/core/layout/qgslayoutitemlabel.cpp | 21 +++------- src/core/layout/qgsprintlayout.cpp | 12 ++++++ src/core/layout/qgsprintlayout.h | 1 + src/core/qgsexpressioncontext.cpp | 42 ++++++++++++++++++- src/core/qgsexpressioncontext.h | 10 ++++- .../qgscategorizedsymbolrendererwidget.cpp | 4 +- .../qgsdatadefinedsizelegendwidget.cpp | 2 +- .../qgsgraduatedsymbolrendererwidget.cpp | 2 +- .../symbology/qgsheatmaprendererwidget.cpp | 2 +- .../symbology/qgslayerpropertieswidget.cpp | 2 +- src/gui/symbology/qgsrendererwidget.cpp | 2 +- src/gui/symbology/qgssymbolwidgetcontext.cpp | 2 +- 26 files changed, 110 insertions(+), 42 deletions(-) diff --git a/python/core/layout/qgslayoutatlas.sip b/python/core/layout/qgslayoutatlas.sip index 38e742b44ec5..085611b42eb8 100644 --- a/python/core/layout/qgslayoutatlas.sip +++ b/python/core/layout/qgslayoutatlas.sip @@ -38,6 +38,8 @@ Constructor for new QgsLayoutAtlas. virtual QgsLayout *layout(); + + virtual bool writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const; virtual bool readXml( const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context ); diff --git a/python/core/layout/qgsprintlayout.sip b/python/core/layout/qgsprintlayout.sip index 8a667fd564f5..474fd4d23457 100644 --- a/python/core/layout/qgsprintlayout.sip +++ b/python/core/layout/qgsprintlayout.sip @@ -35,6 +35,8 @@ Returns the print layout's atlas. virtual bool readXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ); + virtual QgsExpressionContext createExpressionContext() const; + }; diff --git a/python/core/qgsexpressioncontext.sip b/python/core/qgsexpressioncontext.sip index 3d87902db3ac..1be1ff1672fc 100644 --- a/python/core/qgsexpressioncontext.sip +++ b/python/core/qgsexpressioncontext.sip @@ -1045,11 +1045,19 @@ with the variables specified. .. versionadded:: 3.0 %End - static QgsExpressionContextScope *atlasScope( const QgsAtlasComposition *atlas ) /Factory/; + static QgsExpressionContextScope *compositionAtlasScope( const QgsAtlasComposition *atlas ) /Factory/; %Docstring Creates a new scope which contains variables and functions relating to a :py:class:`QgsAtlasComposition`. For instance, current page name and number. +:param atlas: source atlas. If null, a set of default atlas variables will be added to the scope. +%End + + static QgsExpressionContextScope *atlasScope( const QgsLayoutAtlas *atlas ) /Factory/; +%Docstring +Creates a new scope which contains variables and functions relating to a QgsLayoutAtlas. +For instance, current page name and number. + :param atlas: source atlas. If null, a set of default atlas variables will be added to the scope. %End diff --git a/src/app/qgsdiagramproperties.cpp b/src/app/qgsdiagramproperties.cpp index 334f0fb78558..d77af74e913e 100644 --- a/src/app/qgsdiagramproperties.cpp +++ b/src/app/qgsdiagramproperties.cpp @@ -52,7 +52,7 @@ QgsExpressionContext QgsDiagramProperties::createExpressionContext() const QgsExpressionContext expContext; expContext << QgsExpressionContextUtils::globalScope() << QgsExpressionContextUtils::projectScope( QgsProject::instance() ) - << QgsExpressionContextUtils::atlasScope( nullptr ) + << QgsExpressionContextUtils::compositionAtlasScope( nullptr ) << QgsExpressionContextUtils::mapSettingsScope( mMapCanvas->mapSettings() ) << QgsExpressionContextUtils::layerScope( mLayer ); @@ -931,7 +931,7 @@ QString QgsDiagramProperties::showExpressionBuilder( const QString &initialExpre QgsExpressionContext context; context << QgsExpressionContextUtils::globalScope() << QgsExpressionContextUtils::projectScope( QgsProject::instance() ) - << QgsExpressionContextUtils::atlasScope( nullptr ) + << QgsExpressionContextUtils::compositionAtlasScope( nullptr ) << QgsExpressionContextUtils::mapSettingsScope( mMapCanvas->mapSettings() ) << QgsExpressionContextUtils::layerScope( mLayer ); diff --git a/src/app/qgslabelinggui.cpp b/src/app/qgslabelinggui.cpp index e5cf8ff626e7..f03abbbff72d 100644 --- a/src/app/qgslabelinggui.cpp +++ b/src/app/qgslabelinggui.cpp @@ -29,7 +29,7 @@ QgsExpressionContext QgsLabelingGui::createExpressionContext() const QgsExpressionContext expContext; expContext << QgsExpressionContextUtils::globalScope() << QgsExpressionContextUtils::projectScope( QgsProject::instance() ) - << QgsExpressionContextUtils::atlasScope( nullptr ) + << QgsExpressionContextUtils::compositionAtlasScope( nullptr ) << QgsExpressionContextUtils::mapSettingsScope( QgisApp::instance()->mapCanvas()->mapSettings() ); if ( mLayer ) diff --git a/src/app/qgslabelpropertydialog.cpp b/src/app/qgslabelpropertydialog.cpp index a9684fa06080..8aa44da9ec54 100644 --- a/src/app/qgslabelpropertydialog.cpp +++ b/src/app/qgslabelpropertydialog.cpp @@ -255,7 +255,7 @@ void QgsLabelPropertyDialog::setDataDefinedValues( QgsVectorLayer *vlayer ) QgsExpressionContext context; context << QgsExpressionContextUtils::globalScope() << QgsExpressionContextUtils::projectScope( QgsProject::instance() ) - << QgsExpressionContextUtils::atlasScope( nullptr ) + << QgsExpressionContextUtils::compositionAtlasScope( nullptr ) << QgsExpressionContextUtils::mapSettingsScope( QgisApp::instance()->mapCanvas()->mapSettings() ) << QgsExpressionContextUtils::layerScope( vlayer ); context.setFeature( mCurLabelFeat ); diff --git a/src/app/qgspointmarkeritem.cpp b/src/app/qgspointmarkeritem.cpp index 36fc80de7929..96d6d03bef2e 100644 --- a/src/app/qgspointmarkeritem.cpp +++ b/src/app/qgspointmarkeritem.cpp @@ -33,7 +33,7 @@ QgsRenderContext QgsPointMarkerItem::renderContext( QPainter *painter ) QgsExpressionContext context; context << QgsExpressionContextUtils::globalScope() << QgsExpressionContextUtils::projectScope( QgsProject::instance() ) - << QgsExpressionContextUtils::atlasScope( nullptr ); + << QgsExpressionContextUtils::compositionAtlasScope( nullptr ); if ( mMapCanvas ) { context << QgsExpressionContextUtils::mapSettingsScope( mMapCanvas->mapSettings() ) diff --git a/src/app/qgsrulebasedlabelingwidget.cpp b/src/app/qgsrulebasedlabelingwidget.cpp index 669522bb4777..5150cd70f9ef 100644 --- a/src/app/qgsrulebasedlabelingwidget.cpp +++ b/src/app/qgsrulebasedlabelingwidget.cpp @@ -35,7 +35,7 @@ static QList _globalProjectAtlasMapLayerScopes( Qgs QList scopes; scopes << QgsExpressionContextUtils::globalScope() << QgsExpressionContextUtils::projectScope( QgsProject::instance() ) - << QgsExpressionContextUtils::atlasScope( nullptr ); + << QgsExpressionContextUtils::compositionAtlasScope( nullptr ); if ( mapCanvas ) { scopes << QgsExpressionContextUtils::mapSettingsScope( mapCanvas->mapSettings() ) diff --git a/src/app/qgsvectorlayerproperties.cpp b/src/app/qgsvectorlayerproperties.cpp index 3bc88c8e6c0f..56605fe15c25 100644 --- a/src/app/qgsvectorlayerproperties.cpp +++ b/src/app/qgsvectorlayerproperties.cpp @@ -130,7 +130,7 @@ QgsVectorLayerProperties::QgsVectorLayerProperties( mContext << QgsExpressionContextUtils::globalScope() << QgsExpressionContextUtils::projectScope( QgsProject::instance() ) - << QgsExpressionContextUtils::atlasScope( nullptr ) + << QgsExpressionContextUtils::compositionAtlasScope( nullptr ) << QgsExpressionContextUtils::mapSettingsScope( QgisApp::instance()->mapCanvas()->mapSettings() ) << QgsExpressionContextUtils::layerScope( mLayer ); diff --git a/src/core/composer/qgsatlascomposition.cpp b/src/core/composer/qgsatlascomposition.cpp index 853bc486ed4a..eddd2ccc0b94 100644 --- a/src/core/composer/qgsatlascomposition.cpp +++ b/src/core/composer/qgsatlascomposition.cpp @@ -723,7 +723,7 @@ QgsExpressionContext QgsAtlasComposition::createExpressionContext() expressionContext << QgsExpressionContextUtils::projectScope( mComposition->project() ) << QgsExpressionContextUtils::compositionScope( mComposition ); - expressionContext.appendScope( QgsExpressionContextUtils::atlasScope( this ) ); + expressionContext.appendScope( QgsExpressionContextUtils::compositionAtlasScope( this ) ); if ( mCoverageLayer ) expressionContext.lastScope()->setFields( mCoverageLayer->fields() ); if ( mComposition && mComposition->atlasMode() != QgsComposition::AtlasOff ) diff --git a/src/core/composer/qgscomposition.cpp b/src/core/composer/qgscomposition.cpp index 1940cb6d903b..b6ece59a769f 100644 --- a/src/core/composer/qgscomposition.cpp +++ b/src/core/composer/qgscomposition.cpp @@ -3284,7 +3284,7 @@ QgsExpressionContext QgsComposition::createExpressionContext() const context.appendScope( QgsExpressionContextUtils::compositionScope( this ) ); if ( mAtlasComposition.enabled() ) { - context.appendScope( QgsExpressionContextUtils::atlasScope( &mAtlasComposition ) ); + context.appendScope( QgsExpressionContextUtils::compositionAtlasScope( &mAtlasComposition ) ); } return context; } diff --git a/src/core/layout/qgslayout.cpp b/src/core/layout/qgslayout.cpp index 496e8bf6efa1..1cba98138408 100644 --- a/src/core/layout/qgslayout.cpp +++ b/src/core/layout/qgslayout.cpp @@ -325,12 +325,6 @@ QgsExpressionContext QgsLayout::createExpressionContext() const context.appendScope( QgsExpressionContextUtils::globalScope() ); context.appendScope( QgsExpressionContextUtils::projectScope( mProject ) ); context.appendScope( QgsExpressionContextUtils::layoutScope( this ) ); -#if 0 //TODO - if ( mAtlasComposition.enabled() ) - { - context.appendScope( QgsExpressionContextUtils::atlasScope( &mAtlasComposition ) ); - } -#endif return context; } diff --git a/src/core/layout/qgslayoutatlas.cpp b/src/core/layout/qgslayoutatlas.cpp index f39fe25afb58..4d925a37c56b 100644 --- a/src/core/layout/qgslayoutatlas.cpp +++ b/src/core/layout/qgslayoutatlas.cpp @@ -42,6 +42,11 @@ QgsLayout *QgsLayoutAtlas::layout() return mLayout; } +const QgsLayout *QgsLayoutAtlas::layout() const +{ + return mLayout.data(); +} + bool QgsLayoutAtlas::writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext & ) const { QDomElement atlasElem = document.createElement( QStringLiteral( "Atlas" ) ); diff --git a/src/core/layout/qgslayoutatlas.h b/src/core/layout/qgslayoutatlas.h index c2c1ba8aa435..3681e7fca37a 100644 --- a/src/core/layout/qgslayoutatlas.h +++ b/src/core/layout/qgslayoutatlas.h @@ -46,6 +46,13 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject, public QgsAbstractLayoutItera QString stringType() const override; QgsLayout *layout() override; + + /** + * Returns the atlas' layout. + * \note Not available in Python bindings. + */ + const QgsLayout *layout() const SIP_SKIP; + bool writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const override; bool readXml( const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context ) override; diff --git a/src/core/layout/qgslayoutitemlabel.cpp b/src/core/layout/qgslayoutitemlabel.cpp index 1563f7e1b361..94e46a8e6097 100644 --- a/src/core/layout/qgslayoutitemlabel.cpp +++ b/src/core/layout/qgslayoutitemlabel.cpp @@ -29,7 +29,7 @@ #include "qgsfontutils.h" #include "qgsexpressioncontext.h" #include "qgsmapsettings.h" -#include "qgscomposermap.h" +#include "qgslayoutitemmap.h" #include "qgssettings.h" #include "qgswebview.h" @@ -69,11 +69,9 @@ QgsLayoutItemLabel::QgsLayoutItemLabel( QgsLayout *layout ) if ( mLayout ) { -#if 0 //TODO - //connect to atlas feature changes + //connect to context feature changes //to update the expression context - connect( &mLayout->atlasComposition(), &QgsAtlasComposition::featureChanged, this, &QgsLayoutItemLabel::refreshExpressionContext ); -#endif + connect( &mLayout->context(), &QgsLayoutContext::changed, this, &QgsLayoutItemLabel::refreshExpressionContext ); } mWebPage.reset( new QgsWebPage( this ) ); @@ -244,14 +242,7 @@ void QgsLayoutItemLabel::refreshExpressionContext() if ( !mLayout ) return; - QgsVectorLayer *layer = nullptr; -#if 0 //TODO - if ( mComposition->atlasComposition().enabled() ) - { - layer = mComposition->atlasComposition().coverageLayer(); - } -#endif - + QgsVectorLayer *layer = mLayout->context().layer(); //setup distance area conversion if ( layer ) { @@ -259,12 +250,10 @@ void QgsLayoutItemLabel::refreshExpressionContext() } else { -#if 0 //TODO //set to composition's reference map's crs - QgsLayoutItemMap *referenceMap = mComposition->referenceMap(); + QgsLayoutItemMap *referenceMap = mLayout->referenceMap(); if ( referenceMap ) mDistanceArea->setSourceCrs( referenceMap->crs() ); -#endif } mDistanceArea->setEllipsoid( mLayout->project()->ellipsoid() ); contentChanged(); diff --git a/src/core/layout/qgsprintlayout.cpp b/src/core/layout/qgsprintlayout.cpp index a596054ed409..72afbbc80110 100644 --- a/src/core/layout/qgsprintlayout.cpp +++ b/src/core/layout/qgsprintlayout.cpp @@ -44,3 +44,15 @@ bool QgsPrintLayout::readXml( const QDomElement &layoutElement, const QDomDocume mAtlas->readXml( atlasElem, document, context ); return true; } + +QgsExpressionContext QgsPrintLayout::createExpressionContext() const +{ + QgsExpressionContext context = QgsLayout::createExpressionContext(); + + if ( mAtlas->enabled() ) + { + context.appendScope( QgsExpressionContextUtils::atlasScope( mAtlas ) ); + } + + return context; +} diff --git a/src/core/layout/qgsprintlayout.h b/src/core/layout/qgsprintlayout.h index b72318577322..69817528afd2 100644 --- a/src/core/layout/qgsprintlayout.h +++ b/src/core/layout/qgsprintlayout.h @@ -45,6 +45,7 @@ class CORE_EXPORT QgsPrintLayout : public QgsLayout QDomElement writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const override; bool readXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ) override; + QgsExpressionContext createExpressionContext() const override; private: diff --git a/src/core/qgsexpressioncontext.cpp b/src/core/qgsexpressioncontext.cpp index 5f686058eb1f..c6e7219dc80a 100644 --- a/src/core/qgsexpressioncontext.cpp +++ b/src/core/qgsexpressioncontext.cpp @@ -31,6 +31,7 @@ #include "qgsmaplayerlistutils.h" #include "qgsprocessingcontext.h" #include "qgsprocessingalgorithm.h" +#include "qgslayoutatlas.h" #include "qgslayout.h" #include @@ -1127,7 +1128,7 @@ void QgsExpressionContextUtils::setLayoutVariables( QgsLayout *layout, const QVa layout->setCustomProperty( QStringLiteral( "variableValues" ), variableValues ); } -QgsExpressionContextScope *QgsExpressionContextUtils::atlasScope( const QgsAtlasComposition *atlas ) +QgsExpressionContextScope *QgsExpressionContextUtils::compositionAtlasScope( const QgsAtlasComposition *atlas ) { QgsExpressionContextScope *scope = new QgsExpressionContextScope( QObject::tr( "Atlas" ) ); if ( !atlas ) @@ -1166,6 +1167,45 @@ QgsExpressionContextScope *QgsExpressionContextUtils::atlasScope( const QgsAtlas return scope; } +QgsExpressionContextScope *QgsExpressionContextUtils::atlasScope( const QgsLayoutAtlas *atlas ) +{ + QgsExpressionContextScope *scope = new QgsExpressionContextScope( QObject::tr( "Atlas" ) ); + if ( !atlas ) + { + //add some dummy atlas variables. This is done so that as in certain contexts we want to show + //users that these variables are available even if they have no current value + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_pagename" ), QString(), true ) ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_feature" ), QVariant::fromValue( QgsFeature() ), true ) ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_featureid" ), 0, true ) ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_geometry" ), QVariant::fromValue( QgsGeometry() ), true ) ); + return scope; + } + + //add known atlas variables + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_totalfeatures" ), atlas->count(), true ) ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_featurenumber" ), atlas->currentFeatureNumber() + 1, true ) ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_filename" ), atlas->currentFilename(), true ) ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_pagename" ), atlas->nameForPage( atlas->currentFeatureNumber() ), true ) ); + + if ( atlas->enabled() && atlas->coverageLayer() ) + { + scope->setFields( atlas->coverageLayer()->fields() ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_layerid" ), atlas->coverageLayer()->id(), true ) ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_layername" ), atlas->coverageLayer()->name(), true ) ); + } + + if ( atlas->enabled() ) + { + QgsFeature atlasFeature = atlas->layout()->context().feature(); + scope->setFeature( atlasFeature ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_feature" ), QVariant::fromValue( atlasFeature ), true ) ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_featureid" ), atlasFeature.id(), true ) ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_geometry" ), QVariant::fromValue( atlasFeature.geometry() ), true ) ); + } + + return scope; +} + QgsExpressionContextScope *QgsExpressionContextUtils::composerItemScope( const QgsComposerItem *composerItem ) { QgsExpressionContextScope *scope = new QgsExpressionContextScope( QObject::tr( "Composer Item" ) ); diff --git a/src/core/qgsexpressioncontext.h b/src/core/qgsexpressioncontext.h index 2f64bd8179c9..e909714e311f 100644 --- a/src/core/qgsexpressioncontext.h +++ b/src/core/qgsexpressioncontext.h @@ -40,6 +40,7 @@ class QgsProject; class QgsSymbol; class QgsProcessingAlgorithm; class QgsProcessingContext; +class QgsLayoutAtlas; /** * \ingroup core @@ -926,7 +927,14 @@ class CORE_EXPORT QgsExpressionContextUtils * For instance, current page name and number. * \param atlas source atlas. If null, a set of default atlas variables will be added to the scope. */ - static QgsExpressionContextScope *atlasScope( const QgsAtlasComposition *atlas ) SIP_FACTORY; + static QgsExpressionContextScope *compositionAtlasScope( const QgsAtlasComposition *atlas ) SIP_FACTORY; + + /** + * Creates a new scope which contains variables and functions relating to a QgsLayoutAtlas. + * For instance, current page name and number. + * \param atlas source atlas. If null, a set of default atlas variables will be added to the scope. + */ + static QgsExpressionContextScope *atlasScope( const QgsLayoutAtlas *atlas ) SIP_FACTORY; /** * Creates a new scope which contains variables and functions relating to a QgsComposerItem. diff --git a/src/gui/symbology/qgscategorizedsymbolrendererwidget.cpp b/src/gui/symbology/qgscategorizedsymbolrendererwidget.cpp index 9c134462a5b7..7c04d34b8680 100644 --- a/src/gui/symbology/qgscategorizedsymbolrendererwidget.cpp +++ b/src/gui/symbology/qgscategorizedsymbolrendererwidget.cpp @@ -635,7 +635,7 @@ void QgsCategorizedSymbolRendererWidget::addCategories() QgsExpressionContext context; context << QgsExpressionContextUtils::globalScope() << QgsExpressionContextUtils::projectScope( QgsProject::instance() ) - << QgsExpressionContextUtils::atlasScope( nullptr ) + << QgsExpressionContextUtils::compositionAtlasScope( nullptr ) << QgsExpressionContextUtils::layerScope( mLayer ); expression->prepare( &context ); @@ -1014,7 +1014,7 @@ QgsExpressionContext QgsCategorizedSymbolRendererWidget::createExpressionContext QgsExpressionContext expContext; expContext << QgsExpressionContextUtils::globalScope() << QgsExpressionContextUtils::projectScope( QgsProject::instance() ) - << QgsExpressionContextUtils::atlasScope( nullptr ); + << QgsExpressionContextUtils::compositionAtlasScope( nullptr ); if ( mContext.mapCanvas() ) { diff --git a/src/gui/symbology/qgsdatadefinedsizelegendwidget.cpp b/src/gui/symbology/qgsdatadefinedsizelegendwidget.cpp index 7e9e546949a1..906039f8ec6c 100644 --- a/src/gui/symbology/qgsdatadefinedsizelegendwidget.cpp +++ b/src/gui/symbology/qgsdatadefinedsizelegendwidget.cpp @@ -178,7 +178,7 @@ void QgsDataDefinedSizeLegendWidget::changeSymbol() QgsExpressionContext ec; ec << QgsExpressionContextUtils::globalScope() << QgsExpressionContextUtils::projectScope( QgsProject::instance() ) - << QgsExpressionContextUtils::atlasScope( nullptr ); + << QgsExpressionContextUtils::compositionAtlasScope( nullptr ); if ( mMapCanvas ) ec << QgsExpressionContextUtils::mapSettingsScope( mMapCanvas->mapSettings() ); context.setExpressionContext( &ec ); diff --git a/src/gui/symbology/qgsgraduatedsymbolrendererwidget.cpp b/src/gui/symbology/qgsgraduatedsymbolrendererwidget.cpp index 3d87d97cf774..f6dd9f02d9f1 100644 --- a/src/gui/symbology/qgsgraduatedsymbolrendererwidget.cpp +++ b/src/gui/symbology/qgsgraduatedsymbolrendererwidget.cpp @@ -396,7 +396,7 @@ QgsExpressionContext QgsGraduatedSymbolRendererWidget::createExpressionContext() QgsExpressionContext expContext; expContext << QgsExpressionContextUtils::globalScope() << QgsExpressionContextUtils::projectScope( QgsProject::instance() ) - << QgsExpressionContextUtils::atlasScope( nullptr ); + << QgsExpressionContextUtils::compositionAtlasScope( nullptr ); if ( mContext.mapCanvas() ) { diff --git a/src/gui/symbology/qgsheatmaprendererwidget.cpp b/src/gui/symbology/qgsheatmaprendererwidget.cpp index 33c9b22ba42d..c802d318edf7 100644 --- a/src/gui/symbology/qgsheatmaprendererwidget.cpp +++ b/src/gui/symbology/qgsheatmaprendererwidget.cpp @@ -38,7 +38,7 @@ QgsExpressionContext QgsHeatmapRendererWidget::createExpressionContext() const QgsExpressionContext expContext; expContext << QgsExpressionContextUtils::globalScope() << QgsExpressionContextUtils::projectScope( QgsProject::instance() ) - << QgsExpressionContextUtils::atlasScope( nullptr ); + << QgsExpressionContextUtils::compositionAtlasScope( nullptr ); if ( mContext.mapCanvas() ) { diff --git a/src/gui/symbology/qgslayerpropertieswidget.cpp b/src/gui/symbology/qgslayerpropertieswidget.cpp index feeecaa83c5b..a546daf67e4c 100644 --- a/src/gui/symbology/qgslayerpropertieswidget.cpp +++ b/src/gui/symbology/qgslayerpropertieswidget.cpp @@ -217,7 +217,7 @@ QgsExpressionContext QgsLayerPropertiesWidget::createExpressionContext() const QgsExpressionContext expContext; expContext << QgsExpressionContextUtils::globalScope() << QgsExpressionContextUtils::projectScope( QgsProject::instance() ) - << QgsExpressionContextUtils::atlasScope( nullptr ); + << QgsExpressionContextUtils::compositionAtlasScope( nullptr ); if ( mContext.mapCanvas() ) { diff --git a/src/gui/symbology/qgsrendererwidget.cpp b/src/gui/symbology/qgsrendererwidget.cpp index 225f1766a6f6..2336a89974c8 100644 --- a/src/gui/symbology/qgsrendererwidget.cpp +++ b/src/gui/symbology/qgsrendererwidget.cpp @@ -327,7 +327,7 @@ QgsExpressionContext QgsDataDefinedValueDialog::createExpressionContext() const QgsExpressionContext expContext; expContext << QgsExpressionContextUtils::globalScope() << QgsExpressionContextUtils::projectScope( QgsProject::instance() ) - << QgsExpressionContextUtils::atlasScope( nullptr ); + << QgsExpressionContextUtils::compositionAtlasScope( nullptr ); if ( mContext.mapCanvas() ) { expContext << QgsExpressionContextUtils::mapSettingsScope( mContext.mapCanvas()->mapSettings() ) diff --git a/src/gui/symbology/qgssymbolwidgetcontext.cpp b/src/gui/symbology/qgssymbolwidgetcontext.cpp index 72f444544c36..580c159b0a90 100644 --- a/src/gui/symbology/qgssymbolwidgetcontext.cpp +++ b/src/gui/symbology/qgssymbolwidgetcontext.cpp @@ -79,7 +79,7 @@ QList QgsSymbolWidgetContext::globalProjectAtlasMap QList scopes; scopes << QgsExpressionContextUtils::globalScope() << QgsExpressionContextUtils::projectScope( QgsProject::instance() ) - << QgsExpressionContextUtils::atlasScope( nullptr ); + << QgsExpressionContextUtils::compositionAtlasScope( nullptr ); if ( mMapCanvas ) { scopes << QgsExpressionContextUtils::mapSettingsScope( mMapCanvas->mapSettings() ) From b602b3d58ecc0f17d7433ee541cd73e94b442ed8 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 20 Dec 2017 18:05:05 +1000 Subject: [PATCH 015/105] Working label updates on atlas feature change --- src/app/layout/qgslayoutdesignerdialog.cpp | 2 ++ src/core/layout/qgslayoutitemlabel.cpp | 1 + src/core/layout/qgslayoutobject.cpp | 2 ++ 3 files changed, 5 insertions(+) diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 427bf19da275..25121134987a 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -1943,6 +1943,7 @@ void QgsLayoutDesignerDialog::atlasPreviewTriggered( bool checked ) else { QgisApp::instance()->mapCanvas()->stopRendering(); + atlas->first(); #if 0 //TODO emit atlasPreviewFeatureChanged(); #endif @@ -2220,6 +2221,7 @@ void QgsLayoutDesignerDialog::createAtlasWidget() mStatusBar->showMessage( message ); } ); connect( atlas, &QgsLayoutAtlas::toggled, this, &QgsLayoutDesignerDialog::toggleAtlasControls ); + connect( atlas, &QgsLayoutAtlas::toggled, this, &QgsLayoutDesignerDialog::refreshLayout ); connect( atlas, &QgsLayoutAtlas::numberFeaturesChanged, this, &QgsLayoutDesignerDialog::updateAtlasPageComboBox ); connect( atlas, &QgsLayoutAtlas::featureChanged, this, &QgsLayoutDesignerDialog::atlasFeatureChanged ); toggleAtlasControls( atlas->enabled() && atlas->coverageLayer() ); diff --git a/src/core/layout/qgslayoutitemlabel.cpp b/src/core/layout/qgslayoutitemlabel.cpp index 94e46a8e6097..308a35c2d395 100644 --- a/src/core/layout/qgslayoutitemlabel.cpp +++ b/src/core/layout/qgslayoutitemlabel.cpp @@ -492,6 +492,7 @@ void QgsLayoutItemLabel::setFrameStrokeWidth( const QgsLayoutMeasurement &stroke void QgsLayoutItemLabel::refresh() { + QgsLayoutItem::refresh(); contentChanged(); } diff --git a/src/core/layout/qgslayoutobject.cpp b/src/core/layout/qgslayoutobject.cpp index 9371b85aa200..5693b9c6c0ac 100644 --- a/src/core/layout/qgslayoutobject.cpp +++ b/src/core/layout/qgslayoutobject.cpp @@ -18,6 +18,7 @@ #include #include "qgslayout.h" +#include "qgslayoutcontext.h" #include "qgslayoutobject.h" @@ -92,6 +93,7 @@ QgsLayoutObject::QgsLayoutObject( QgsLayout *layout ) if ( mLayout ) { connect( mLayout, &QgsLayout::refreshed, this, &QgsLayoutObject::refresh ); + connect( &mLayout->context(), &QgsLayoutContext::changed, this, &QgsLayoutObject::refresh ); } } From e72e20b8e07ca42bf820de2e93708dbd5c1e88c7 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 20 Dec 2017 18:36:42 +1000 Subject: [PATCH 016/105] Restore attribute table atlas handling --- python/core/layout/qgslayoutcontext.sip | 5 ++ python/gui/layout/qgslayoutitemwidget.sip | 9 +++- .../layout/qgslayoutattributetablewidget.cpp | 35 +++++-------- src/app/layout/qgslayoutlabelwidget.cpp | 7 +-- src/core/layout/qgslayoutatlas.cpp | 1 + src/core/layout/qgslayoutcontext.cpp | 1 + src/core/layout/qgslayoutcontext.h | 5 ++ .../layout/qgslayoutitemattributetable.cpp | 52 +++---------------- src/gui/layout/qgslayoutitemwidget.cpp | 44 +++++++--------- src/gui/layout/qgslayoutitemwidget.h | 13 ++--- .../qgslayoutattributetablewidgetbase.ui | 14 ++--- 11 files changed, 75 insertions(+), 111 deletions(-) diff --git a/python/core/layout/qgslayoutcontext.sip b/python/core/layout/qgslayoutcontext.sip index 4c7bd0d0d416..203cebb7a9f6 100644 --- a/python/core/layout/qgslayoutcontext.sip +++ b/python/core/layout/qgslayoutcontext.sip @@ -224,6 +224,11 @@ If ``layer`` is -1, all item layers should be rendered. Emitted whenever the context's ``flags`` change. .. seealso:: :py:func:`setFlags()` +%End + + void layerChanged( QgsVectorLayer *layer ); +%Docstring +Emitted when the context's ``layer`` is changed. %End void changed(); diff --git a/python/gui/layout/qgslayoutitemwidget.sip b/python/gui/layout/qgslayoutitemwidget.sip index 5e93bb8b5a5e..fa884256bef9 100644 --- a/python/gui/layout/qgslayoutitemwidget.sip +++ b/python/gui/layout/qgslayoutitemwidget.sip @@ -50,6 +50,10 @@ Updates a data defined button to reflect the item's current properties. Returns the current layout context coverage layer (if set). %End + QgsLayoutAtlas *layoutAtlas() const; +%Docstring +Returns the atlas for the layout, if available +%End }; @@ -117,7 +121,10 @@ Implementations must return true if the item was accepted and the widget was updated. %End - + QgsLayoutAtlas *layoutAtlas() const; +%Docstring +Returns the atlas for the layout (if available) +%End }; diff --git a/src/app/layout/qgslayoutattributetablewidget.cpp b/src/app/layout/qgslayoutattributetablewidget.cpp index dcbe2baf9ce8..e0a48a56cb9f 100644 --- a/src/app/layout/qgslayoutattributetablewidget.cpp +++ b/src/app/layout/qgslayoutattributetablewidget.cpp @@ -16,7 +16,7 @@ ***************************************************************************/ #include "qgslayoutattributetablewidget.h" -#include "qgsatlascomposition.h" +#include "qgslayoutatlas.h" #include "qgslayout.h" #include "qgslayoutframe.h" #include "qgslayoutattributeselectiondialog.h" @@ -87,10 +87,7 @@ QgsLayoutAttributeTableWidget::QgsLayoutAttributeTableWidget( QgsLayoutFrame *fr mWrapBehaviorComboBox->addItem( tr( "Truncate text" ), QgsLayoutTable::TruncateText ); mWrapBehaviorComboBox->addItem( tr( "Wrap text" ), QgsLayoutTable::WrapText ); - bool atlasEnabled = false; -#if 0 //TODO - atlasComposition() &&atlasComposition()->enabled(); -#endif + bool atlasEnabled = layoutAtlas() && layoutAtlas()->enabled(); mSourceComboBox->addItem( tr( "Layer features" ), QgsLayoutItemAttributeTable::LayerAttributes ); toggleAtlasSpecificControls( atlasEnabled ); @@ -126,16 +123,14 @@ QgsLayoutAttributeTableWidget::QgsLayoutAttributeTableWidget( QgsLayoutFrame *fr { connect( mTable, &QgsLayoutMultiFrame::changed, this, &QgsLayoutAttributeTableWidget::updateGuiElements ); -#if 0 //TODO - QgsAtlasComposition *atlas = atlasComposition(); - if ( atlas ) + // repopulate relations combo box if atlas layer changes + connect( &mTable->layout()->context(), &QgsLayoutContext::layerChanged, + this, &QgsLayoutAttributeTableWidget::updateRelationsCombo ); + + if ( QgsLayoutAtlas *atlas = layoutAtlas() ) { - // repopulate relations combo box if atlas layer changes - connect( atlas, &QgsAtlasComposition::coverageLayerChanged, - this, &QgsLayoutAttributeTableWidget::updateRelationsCombo ); - connect( atlas, &QgsAtlasComposition::toggled, this, &QgsLayoutAttributeTableWidget::atlasToggled ); + connect( atlas, &QgsLayoutAtlas::toggled, this, &QgsLayoutAttributeTableWidget::atlasToggled ); } -#endif } //embed widget for general options @@ -486,9 +481,8 @@ void QgsLayoutAttributeTableWidget::updateGuiElements() void QgsLayoutAttributeTableWidget::atlasToggled() { -#if 0 //TODO //display/hide atlas options in source combobox depending on atlas status - bool atlasEnabled = atlasComposition() && atlasComposition()->enabled(); + bool atlasEnabled = layoutAtlas() && layoutAtlas()->enabled(); toggleAtlasSpecificControls( atlasEnabled ); if ( !mTable ) @@ -500,19 +494,18 @@ void QgsLayoutAttributeTableWidget::atlasToggled() { mTable->setFilterToAtlasFeature( false ); } -#endif } void QgsLayoutAttributeTableWidget::updateRelationsCombo() { mRelationsComboBox->blockSignals( true ); mRelationsComboBox->clear(); -#if 0 //TODO - QgsVectorLayer *atlasLayer = atlasCoverageLayer(); + + QgsVectorLayer *atlasLayer = coverageLayer(); if ( atlasLayer ) { - QList relations = QgsProject::instance()->relationManager()->referencedRelations( atlasLayer ); - Q_FOREACH ( const QgsRelation &relation, relations ) + const QList relations = QgsProject::instance()->relationManager()->referencedRelations( atlasLayer ); + for ( const QgsRelation &relation : relations ) { mRelationsComboBox->addItem( relation.name(), relation.id() ); } @@ -521,7 +514,7 @@ void QgsLayoutAttributeTableWidget::updateRelationsCombo() mRelationsComboBox->setCurrentIndex( mRelationsComboBox->findData( mTable->relationId() ) ); } } -#endif + mRelationsComboBox->blockSignals( false ); } diff --git a/src/app/layout/qgslayoutlabelwidget.cpp b/src/app/layout/qgslayoutlabelwidget.cpp index 91c41c2470e2..033ef5541989 100644 --- a/src/app/layout/qgslayoutlabelwidget.cpp +++ b/src/app/layout/qgslayoutlabelwidget.cpp @@ -192,13 +192,10 @@ void QgsLayoutLabelWidget::mInsertExpressionButton_clicked() selText = selText.mid( 2, selText.size() - 4 ); // use the atlas coverage layer, if any -#if 0 //TODO - QgsVectorLayer *coverageLayer = atlasCoverageLayer(); -#endif - QgsVectorLayer *coverageLayer = nullptr; + QgsVectorLayer *layer = coverageLayer(); QgsExpressionContext context = mLabel->createExpressionContext(); - QgsExpressionBuilderDialog exprDlg( coverageLayer, selText, this, QStringLiteral( "generic" ), context ); + QgsExpressionBuilderDialog exprDlg( layer, selText, this, QStringLiteral( "generic" ), context ); exprDlg.setWindowTitle( tr( "Insert Expression" ) ); if ( exprDlg.exec() == QDialog::Accepted ) diff --git a/src/core/layout/qgslayoutatlas.cpp b/src/core/layout/qgslayoutatlas.cpp index 4d925a37c56b..aeb0ef3179b4 100644 --- a/src/core/layout/qgslayoutatlas.cpp +++ b/src/core/layout/qgslayoutatlas.cpp @@ -97,6 +97,7 @@ bool QgsLayoutAtlas::readXml( const QDomElement &atlasElem, const QDomDocument & mCoverageLayer = QgsVectorLayerRef( layerId, layerName, layerSource, layerProvider ); mCoverageLayer.resolveWeakly( mLayout->project() ); + mLayout->context().setLayer( mCoverageLayer.get() ); mPageNameExpression = atlasElem.attribute( QStringLiteral( "pageNameExpression" ), QString() ); QString error; diff --git a/src/core/layout/qgslayoutcontext.cpp b/src/core/layout/qgslayoutcontext.cpp index 15ef62df56da..6fe6d928c153 100644 --- a/src/core/layout/qgslayoutcontext.cpp +++ b/src/core/layout/qgslayoutcontext.cpp @@ -83,6 +83,7 @@ QgsVectorLayer *QgsLayoutContext::layer() const void QgsLayoutContext::setLayer( QgsVectorLayer *layer ) { mLayer = layer; + emit layerChanged( layer ); emit changed(); } diff --git a/src/core/layout/qgslayoutcontext.h b/src/core/layout/qgslayoutcontext.h index 8caf64735f5c..339522359a20 100644 --- a/src/core/layout/qgslayoutcontext.h +++ b/src/core/layout/qgslayoutcontext.h @@ -225,6 +225,11 @@ class CORE_EXPORT QgsLayoutContext : public QObject */ void flagsChanged( QgsLayoutContext::Flags flags ); + /** + * Emitted when the context's \a layer is changed. + */ + void layerChanged( QgsVectorLayer *layer ); + /** * Emitted certain settings in the context is changed, e.g. by setting a new feature or vector layer * for the context. diff --git a/src/core/layout/qgslayoutitemattributetable.cpp b/src/core/layout/qgslayoutitemattributetable.cpp index 502812670418..f209aac4a6f6 100644 --- a/src/core/layout/qgslayoutitemattributetable.cpp +++ b/src/core/layout/qgslayoutitemattributetable.cpp @@ -79,13 +79,8 @@ QgsLayoutItemAttributeTable::QgsLayoutItemAttributeTable( QgsLayout *layout ) { connect( mLayout->project(), static_cast < void ( QgsProject::* )( const QString & ) >( &QgsProject::layerWillBeRemoved ), this, &QgsLayoutItemAttributeTable::removeLayer ); -#if 0 //TODO - //connect to atlas feature changes to update table rows - connect( &mComposition->atlasComposition(), &QgsAtlasComposition::featureChanged, this, &QgsLayoutTable::refreshAttributes ); - - //atlas coverage layer change = regenerate columns - connect( &mComposition->atlasComposition(), &QgsAtlasComposition::coverageLayerChanged, this, &QgsLayoutItemAttributeTable::atlasLayerChanged ); -#endif + //coverage layer change = regenerate columns + connect( &mLayout->context(), &QgsLayoutContext::layerChanged, this, &QgsLayoutItemAttributeTable::atlasLayerChanged ); } refreshAttributes(); } @@ -394,17 +389,7 @@ bool QgsLayoutItemAttributeTable::getTableContents( QgsLayoutTableContents &cont { contents.clear(); - if ( ( mSource == QgsLayoutItemAttributeTable::AtlasFeature || mSource == QgsLayoutItemAttributeTable::RelationChildren ) ) -#if 0 //TODO - // && !mComposition->atlasComposition().enabled() ) -#endif - { - //source mode requires atlas, but atlas disabled - return false; - } - QgsVectorLayer *layer = sourceLayer(); - if ( !layer ) { //no source layer @@ -451,10 +436,7 @@ bool QgsLayoutItemAttributeTable::getTableContents( QgsLayoutTableContents &cont if ( mSource == QgsLayoutItemAttributeTable::RelationChildren ) { QgsRelation relation = mLayout->project()->relationManager()->relation( mRelationId ); - QgsFeature atlasFeature; -#if 0 //TODO - = mLayout->atlasComposition().feature(); -#endif + QgsFeature atlasFeature = mLayout->context().feature(); req = relation.getRelatedFeaturesRequest( atlasFeature ); } @@ -464,15 +446,9 @@ bool QgsLayoutItemAttributeTable::getTableContents( QgsLayoutTableContents &cont req.setFlags( mShowOnlyVisibleFeatures ? QgsFeatureRequest::ExactIntersect : QgsFeatureRequest::NoFlags ); if ( mSource == QgsLayoutItemAttributeTable::AtlasFeature ) -#if 0 // TODO - && mComposition->atlasComposition().enabled() ) -#endif { //source mode is current atlas feature - QgsFeature atlasFeature; -#if 0 //TODO - = mLayout->atlasComposition().feature(); -#endif + QgsFeature atlasFeature = mLayout->context().feature(); req.setFilterFid( atlasFeature.id() ); } @@ -497,18 +473,12 @@ bool QgsLayoutItemAttributeTable::getTableContents( QgsLayoutTableContents &cont if ( mFilterToAtlasIntersection ) { if ( !f.hasGeometry() ) -#if 0 //TODO - || ! mComposition->atlasComposition().enabled() ) -#endif { continue; } - QgsFeature atlasFeature; -#if 0 //TODO - = mComposition->atlasComposition().feature(); -#endif + QgsFeature atlasFeature = mLayout->context().feature(); if ( !atlasFeature.hasGeometry() || - !f.geometry().intersects( atlasFeature.geometry() ) ) + !f.geometry().intersects( atlasFeature.geometry() ) ) { //feature falls outside current atlas feature continue; @@ -584,10 +554,7 @@ QgsVectorLayer *QgsLayoutItemAttributeTable::sourceLayer() switch ( mSource ) { case QgsLayoutItemAttributeTable::AtlasFeature: -#if 0 //TODO - return mComposition->atlasComposition().coverageLayer(); -#endif - return nullptr; + return mLayout->context().layer(); case QgsLayoutItemAttributeTable::LayerAttributes: return mVectorLayer.get(); case QgsLayoutItemAttributeTable::RelationChildren: @@ -704,10 +671,7 @@ bool QgsLayoutItemAttributeTable::readPropertiesFromElement( const QDomElement & if ( mSource == QgsLayoutItemAttributeTable::AtlasFeature ) { - mCurrentAtlasLayer = nullptr; -#if 0 //TODO - mComposition->atlasComposition().coverageLayer(); -#endif + mCurrentAtlasLayer = mLayout->context().layer(); } mShowUniqueRowsOnly = itemElem.attribute( QStringLiteral( "showUniqueRowsOnly" ), QStringLiteral( "0" ) ).toInt(); diff --git a/src/gui/layout/qgslayoutitemwidget.cpp b/src/gui/layout/qgslayoutitemwidget.cpp index 2535991855f5..33e08b4fc635 100644 --- a/src/gui/layout/qgslayoutitemwidget.cpp +++ b/src/gui/layout/qgslayoutitemwidget.cpp @@ -18,6 +18,8 @@ #include "qgslayout.h" #include "qgsproject.h" #include "qgslayoutundostack.h" +#include "qgsprintlayout.h" +#include "qgslayoutatlas.h" // // QgsLayoutConfigObject @@ -27,11 +29,15 @@ QgsLayoutConfigObject::QgsLayoutConfigObject( QWidget *parent, QgsLayoutObject * : QObject( parent ) , mLayoutObject( layoutObject ) { -#if 0 //TODO - connect( atlasComposition(), &QgsAtlasComposition::coverageLayerChanged, - this, [ = ] { updateDataDefinedButtons(); } ); - connect( atlasComposition(), &QgsAtlasComposition::toggled, this, &QgsComposerConfigObject::updateDataDefinedButtons ); -#endif + if ( mLayoutObject->layout() ) + { + connect( &mLayoutObject->layout()->context(), &QgsLayoutContext::layerChanged, + this, [ = ] { updateDataDefinedButtons(); } ); + } + if ( layoutAtlas() ) + { + connect( layoutAtlas(), &QgsLayoutAtlas::toggled, this, &QgsLayoutConfigObject::updateDataDefinedButtons ); + } } void QgsLayoutConfigObject::updateDataDefinedProperty() @@ -62,12 +68,11 @@ void QgsLayoutConfigObject::updateDataDefinedProperty() void QgsLayoutConfigObject::updateDataDefinedButtons() { -#if 0 //TODO - Q_FOREACH ( QgsPropertyOverrideButton *button, findChildren< QgsPropertyOverrideButton * >() ) + const QList< QgsPropertyOverrideButton * > buttons = findChildren< QgsPropertyOverrideButton * >(); + for ( QgsPropertyOverrideButton *button : buttons ) { - button->setVectorLayer( atlasCoverageLayer() ); + button->setVectorLayer( coverageLayer() ); } -#endif } void QgsLayoutConfigObject::initializeDataDefinedButton( QgsPropertyOverrideButton *button, QgsLayoutObject::DataDefinedProperty key ) @@ -91,24 +96,22 @@ void QgsLayoutConfigObject::updateDataDefinedButton( QgsPropertyOverrideButton * whileBlocking( button )->setToProperty( mLayoutObject->dataDefinedProperties().property( key ) ); } -#if 0 // TODO -QgsAtlasComposition *QgsLayoutConfigObject::atlasComposition() const +QgsLayoutAtlas *QgsLayoutConfigObject::layoutAtlas() const { if ( !mLayoutObject ) { return nullptr; } - QgsComposition *composition = mComposerObject->composition(); + QgsPrintLayout *printLayout = qobject_cast< QgsPrintLayout * >( mLayoutObject->layout() ); - if ( !composition ) + if ( !printLayout ) { return nullptr; } - return &composition->atlasComposition(); + return printLayout->atlas(); } -#endif QgsVectorLayer *QgsLayoutConfigObject::coverageLayer() const { @@ -171,13 +174,10 @@ bool QgsLayoutItemBaseWidget::setNewItem( QgsLayoutItem * ) return false; } -#if 0 //TODO -QgsAtlasComposition *QgsLayoutItemBaseWidget::atlasComposition() const +QgsLayoutAtlas *QgsLayoutItemBaseWidget::layoutAtlas() const { - return mConfigObject->atlasComposition(); + return mConfigObject->layoutAtlas(); } -#endif - // @@ -259,10 +259,6 @@ QgsLayoutItemPropertiesWidget::QgsLayoutItemPropertiesWidget( QWidget *parent, Q initializeDataDefinedButtons(); -#if 0 //TODO - connect( mItem->composition(), &QgsComposition::paperSizeChanged, this, &QgsLayoutItemPropertiesWidget::setValuesForGuiPositionElements ); -#endif - setItem( item ); connect( mOpacityWidget, &QgsOpacityWidget::opacityChanged, this, &QgsLayoutItemPropertiesWidget::opacityChanged ); diff --git a/src/gui/layout/qgslayoutitemwidget.h b/src/gui/layout/qgslayoutitemwidget.h index 1e719fced78f..248363f7b0b0 100644 --- a/src/gui/layout/qgslayoutitemwidget.h +++ b/src/gui/layout/qgslayoutitemwidget.h @@ -83,10 +83,8 @@ class GUI_EXPORT QgsLayoutConfigObject: public QObject */ QgsVectorLayer *coverageLayer() const; -#if 0 //TODO - //! Returns the atlas for the composition - QgsAtlasComposition *atlasComposition() const; -#endif + //! Returns the atlas for the layout, if available + QgsLayoutAtlas *layoutAtlas() const; private slots: //! Must be called when a data defined button changes @@ -165,11 +163,8 @@ class GUI_EXPORT QgsLayoutItemBaseWidget: public QgsPanelWidget */ virtual bool setNewItem( QgsLayoutItem *item ); - -#if 0 //TODO - //! Returns the atlas for the composition - QgsAtlasComposition *atlasComposition() const; -#endif + //! Returns the atlas for the layout (if available) + QgsLayoutAtlas *layoutAtlas() const; private: diff --git a/src/ui/layout/qgslayoutattributetablewidgetbase.ui b/src/ui/layout/qgslayoutattributetablewidgetbase.ui index 42bae14702c0..a9ac77b7dc2e 100644 --- a/src/ui/layout/qgslayoutattributetablewidgetbase.ui +++ b/src/ui/layout/qgslayoutattributetablewidgetbase.ui @@ -74,7 +74,7 @@ false - + @@ -789,6 +789,12 @@
+ + QgsCollapsibleGroupBoxBasic + QGroupBox +
qgscollapsiblegroupbox.h
+ 1 +
QgsScrollArea QScrollArea @@ -806,12 +812,6 @@
qgscolorbutton.h
1
- - QgsCollapsibleGroupBoxBasic - QGroupBox -
qgscollapsiblegroupbox.h
- 1 -
QgsDoubleSpinBox QDoubleSpinBox From a5ab4e16e4d436b43b3edb30da69c8f9e16ba638 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 20 Dec 2017 18:43:50 +1000 Subject: [PATCH 017/105] Restore shape atlas handling --- src/app/layout/qgslayoutshapewidget.cpp | 8 +++++--- src/core/layout/qgslayoutitemshape.cpp | 9 --------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/app/layout/qgslayoutshapewidget.cpp b/src/app/layout/qgslayoutshapewidget.cpp index 2c1e823ff123..03173efddf54 100644 --- a/src/app/layout/qgslayoutshapewidget.cpp +++ b/src/app/layout/qgslayoutshapewidget.cpp @@ -60,9 +60,11 @@ QgsLayoutShapeWidget::QgsLayoutShapeWidget( QgsLayoutItemShape *shape ) connect( mShapeStyleButton, &QgsSymbolButton::changed, this, &QgsLayoutShapeWidget::symbolChanged ); connect( mRadiusUnitsComboBox, &QgsLayoutUnitsComboBox::changed, this, &QgsLayoutShapeWidget::radiusUnitsChanged ); -#if 0 //TODO - mShapeStyleButton->setLayer( atlasCoverageLayer() ); -#endif + mShapeStyleButton->setLayer( coverageLayer() ); + if ( mShape->layout() ) + { + connect( &mShape->layout()->context(), &QgsLayoutContext::layerChanged, mShapeStyleButton, &QgsSymbolButton::setLayer ); + } } bool QgsLayoutShapeWidget::setNewItem( QgsLayoutItem *item ) diff --git a/src/core/layout/qgslayoutitemshape.cpp b/src/core/layout/qgslayoutitemshape.cpp index 0f59aec24fbc..6bf7c3ca7453 100644 --- a/src/core/layout/qgslayoutitemshape.cpp +++ b/src/core/layout/qgslayoutitemshape.cpp @@ -43,15 +43,6 @@ QgsLayoutItemShape::QgsLayoutItemShape( QgsLayout *layout ) updateBoundingRect(); update(); } ); - -#if 0 //TODO - if ( mComposition ) - { - //connect to atlas feature changes - //to update symbol style (in case of data-defined symbology) - connect( &mComposition->atlasComposition(), &QgsAtlasComposition::featureChanged, this, &QgsComposerItem::repaint ); - } -#endif } int QgsLayoutItemShape::type() const From 6506bcda20d4ca5af539d8a8c55db89bc157d141 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 20 Dec 2017 18:50:49 +1000 Subject: [PATCH 018/105] Restore some more atlas handling --- src/app/layout/qgslayoutpagepropertieswidget.cpp | 7 +++++++ src/app/layout/qgslayoutpolygonwidget.cpp | 10 +++++++--- src/app/layout/qgslayoutpolylinewidget.cpp | 9 ++++++--- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/app/layout/qgslayoutpagepropertieswidget.cpp b/src/app/layout/qgslayoutpagepropertieswidget.cpp index 20b680b6bf8f..aa342cd51e68 100644 --- a/src/app/layout/qgslayoutpagepropertieswidget.cpp +++ b/src/app/layout/qgslayoutpagepropertieswidget.cpp @@ -76,6 +76,13 @@ QgsLayoutPagePropertiesWidget::QgsLayoutPagePropertiesWidget( QWidget *parent, Q mExcludePageDDBtn->registerEnabledWidget( mExcludePageCheckBox, false ); + mSymbolButton->registerExpressionContextGenerator( mPage ); + mSymbolButton->setLayer( coverageLayer() ); + if ( mPage->layout() ) + { + connect( &mPage->layout()->context(), &QgsLayoutContext::layerChanged, mSymbolButton, &QgsSymbolButton::setLayer ); + } + showCurrentPageSize(); } diff --git a/src/app/layout/qgslayoutpolygonwidget.cpp b/src/app/layout/qgslayoutpolygonwidget.cpp index 45aaa420506d..0a28df41e697 100644 --- a/src/app/layout/qgslayoutpolygonwidget.cpp +++ b/src/app/layout/qgslayoutpolygonwidget.cpp @@ -46,9 +46,13 @@ QgsLayoutPolygonWidget::QgsLayoutPolygonWidget( QgsLayoutItemPolygon *polygon ) } setGuiElementValues(); -#if 0 //TODO - mShapeStyleButton->setLayer( atlasCoverageLayer() ); -#endif + + mPolygonStyleButton->registerExpressionContextGenerator( mPolygon ); + mPolygonStyleButton->setLayer( coverageLayer() ); + if ( mPolygon->layout() ) + { + connect( &mPolygon->layout()->context(), &QgsLayoutContext::layerChanged, mPolygonStyleButton, &QgsSymbolButton::setLayer ); + } } bool QgsLayoutPolygonWidget::setNewItem( QgsLayoutItem *item ) diff --git a/src/app/layout/qgslayoutpolylinewidget.cpp b/src/app/layout/qgslayoutpolylinewidget.cpp index dfc675c5d95b..d1b098c5c3f4 100644 --- a/src/app/layout/qgslayoutpolylinewidget.cpp +++ b/src/app/layout/qgslayoutpolylinewidget.cpp @@ -88,9 +88,12 @@ QgsLayoutPolylineWidget::QgsLayoutPolylineWidget( QgsLayoutItemPolyline *polylin } setGuiElementValues(); -#if 0 //TODO - mShapeStyleButton->setLayer( atlasCoverageLayer() ); -#endif + mLineStyleButton->registerExpressionContextGenerator( mPolyline ); + mLineStyleButton->setLayer( coverageLayer() ); + if ( mPolyline->layout() ) + { + connect( &mPolyline->layout()->context(), &QgsLayoutContext::layerChanged, mLineStyleButton, &QgsSymbolButton::setLayer ); + } } bool QgsLayoutPolylineWidget::setNewItem( QgsLayoutItem *item ) From 8072d4d35761f26b96dd51cda9770a4987af146c Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 20 Dec 2017 18:55:16 +1000 Subject: [PATCH 019/105] Restore some more atlas style related UI --- src/app/layout/qgslayoutmapgridwidget.cpp | 11 +++++++++++ src/app/layout/qgslayoutmapwidget.cpp | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/src/app/layout/qgslayoutmapgridwidget.cpp b/src/app/layout/qgslayoutmapgridwidget.cpp index aef77a399777..e2b09e119946 100644 --- a/src/app/layout/qgslayoutmapgridwidget.cpp +++ b/src/app/layout/qgslayoutmapgridwidget.cpp @@ -149,6 +149,17 @@ QgsLayoutMapGridWidget::QgsLayoutMapGridWidget( QgsLayoutItemMapGrid *mapGrid, Q connect( mAnnotationFontButton, &QgsFontButton::changed, this, &QgsLayoutMapGridWidget::annotationFontChanged ); connect( mGridLineStyleButton, &QgsSymbolButton::changed, this, &QgsLayoutMapGridWidget::lineSymbolChanged ); connect( mGridMarkerStyleButton, &QgsSymbolButton::changed, this, &QgsLayoutMapGridWidget::markerSymbolChanged ); + + mGridLineStyleButton->registerExpressionContextGenerator( mMapGrid ); + mGridLineStyleButton->setLayer( coverageLayer() ); + mGridMarkerStyleButton->registerExpressionContextGenerator( mMapGrid ); + mGridMarkerStyleButton->setLayer( coverageLayer() ); + if ( mMap->layout() ) + { + connect( &mMap->layout()->context(), &QgsLayoutContext::layerChanged, mGridLineStyleButton, &QgsSymbolButton::setLayer ); + connect( &mMap->layout()->context(), &QgsLayoutContext::layerChanged, mGridMarkerStyleButton, &QgsSymbolButton::setLayer ); + } + } void QgsLayoutMapGridWidget::populateDataDefinedButtons() diff --git a/src/app/layout/qgslayoutmapwidget.cpp b/src/app/layout/qgslayoutmapwidget.cpp index 0a8804f758b6..80a8bfbfe498 100644 --- a/src/app/layout/qgslayoutmapwidget.cpp +++ b/src/app/layout/qgslayoutmapwidget.cpp @@ -130,6 +130,14 @@ QgsLayoutMapWidget::QgsLayoutMapWidget( QgsLayoutItemMap *item ) connect( mCrsSelector, &QgsProjectionSelectionWidget::crsChanged, this, &QgsLayoutMapWidget::mapCrsChanged ); connect( mOverviewFrameStyleButton, &QgsSymbolButton::changed, this, &QgsLayoutMapWidget::overviewSymbolChanged ); + mOverviewFrameStyleButton->registerExpressionContextGenerator( item ); + mOverviewFrameStyleButton->setLayer( coverageLayer() ); + if ( item->layout() ) + { + connect( &item->layout()->context(), &QgsLayoutContext::layerChanged, mOverviewFrameStyleButton, &QgsSymbolButton::setLayer ); + } + + registerDataDefinedButton( mScaleDDBtn, QgsLayoutObject::MapScale ); registerDataDefinedButton( mMapRotationDDBtn, QgsLayoutObject::MapRotation ); registerDataDefinedButton( mXMinDDBtn, QgsLayoutObject::MapXMin ); From fee1c211a6bea43bccf87c685d803ef99b173aa4 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 23 Dec 2017 15:22:30 +1000 Subject: [PATCH 020/105] Restore HTML item atlas functionality --- src/app/layout/qgslayouthtmlwidget.cpp | 7 +--- src/core/layout/qgslayoutitemhtml.cpp | 53 ++++++++++---------------- src/core/layout/qgslayoutitemlabel.cpp | 11 +----- 3 files changed, 25 insertions(+), 46 deletions(-) diff --git a/src/app/layout/qgslayouthtmlwidget.cpp b/src/app/layout/qgslayouthtmlwidget.cpp index a0b67b6fe9ec..027131ce9716 100644 --- a/src/app/layout/qgslayouthtmlwidget.cpp +++ b/src/app/layout/qgslayouthtmlwidget.cpp @@ -352,14 +352,11 @@ void QgsLayoutHtmlWidget::mInsertExpressionButton_clicked() mHtmlEditor->getCursorPosition( &line, &index ); } -#if 0 //TODO // use the atlas coverage layer, if any - QgsVectorLayer *coverageLayer = atlasCoverageLayer(); -#endif - QgsVectorLayer *coverageLayer = nullptr; + QgsVectorLayer *layer = coverageLayer(); QgsExpressionContext context = mHtml->createExpressionContext(); - QgsExpressionBuilderDialog exprDlg( coverageLayer, selText, this, QStringLiteral( "generic" ), context ); + QgsExpressionBuilderDialog exprDlg( layer, selText, this, QStringLiteral( "generic" ), context ); exprDlg.setWindowTitle( tr( "Insert Expression" ) ); if ( exprDlg.exec() == QDialog::Accepted ) { diff --git a/src/core/layout/qgslayoutitemhtml.cpp b/src/core/layout/qgslayoutitemhtml.cpp index 52fc0aa432f2..5cca26b26294 100644 --- a/src/core/layout/qgslayoutitemhtml.cpp +++ b/src/core/layout/qgslayoutitemhtml.cpp @@ -28,6 +28,7 @@ #include "qgsmapsettings.h" #include "qgswebpage.h" #include "qgswebframe.h" +#include "qgslayoutitemmap.h" #include #include @@ -50,23 +51,11 @@ QgsLayoutItemHtml::QgsLayoutItemHtml( QgsLayout *layout ) mWebPage->setNetworkAccessManager( QgsNetworkAccessManager::instance() ); -#if 0 //TODO - if ( mLayout ) - { - connect( mLayout, &QgsComposition::itemRemoved, this, &QgsComposerMultiFrame::handleFrameRemoval ); - } + //a html item added to a layout needs to have the initial expression context set, + //otherwise fields in the html aren't correctly evaluated until atlas preview feature changes (#9457) + setExpressionContext( mLayout->context().feature(), mLayout->context().layer() ); - if ( mComposition && mComposition->atlasMode() == QgsComposition::PreviewAtlas ) - { - //a html item added while atlas preview is enabled needs to have the expression context set, - //otherwise fields in the html aren't correctly evaluated until atlas preview feature changes (#9457) - setExpressionContext( mComposition->atlasComposition().feature(), mComposition->atlasComposition().coverageLayer() ); - } - - //connect to atlas feature changes - //to update the expression context - connect( &mComposition->atlasComposition(), &QgsAtlasComposition::featureChanged, this, &QgsLayoutItemHtml::refreshExpressionContext ); -#endif + connect( &mLayout->context(), &QgsLayoutContext::changed, this, &QgsLayoutItemHtml::refreshExpressionContext ); mFetcher = new QgsNetworkContentFetcher(); } @@ -496,22 +485,27 @@ void QgsLayoutItemHtml::setExpressionContext( const QgsFeature &feature, QgsVect } else if ( mLayout ) { -#if 0 //TODO //set to composition's mapsettings' crs - QgsComposerMap *referenceMap = mComposition->referenceMap(); + QgsLayoutItemMap *referenceMap = mLayout->referenceMap(); if ( referenceMap ) - mDistanceArea->setSourceCrs( referenceMap->crs() ); -#endif + mDistanceArea.setSourceCrs( referenceMap->crs(), mLayout->project()->transformContext() ); } if ( mLayout ) { mDistanceArea.setEllipsoid( mLayout->project()->ellipsoid() ); } - // create JSON representation of feature - QgsJsonExporter exporter( layer ); - exporter.setIncludeRelated( true ); - mAtlasFeatureJSON = exporter.exportFeature( feature ); + if ( feature.isValid() ) + { + // create JSON representation of feature + QgsJsonExporter exporter( layer ); + exporter.setIncludeRelated( true ); + mAtlasFeatureJSON = exporter.exportFeature( feature ); + } + else + { + mAtlasFeatureJSON.clear(); + } } void QgsLayoutItemHtml::refreshExpressionContext() @@ -519,16 +513,11 @@ void QgsLayoutItemHtml::refreshExpressionContext() QgsVectorLayer *vl = nullptr; QgsFeature feature; -#if 0 //TODO - if ( mComposition->atlasComposition().enabled() ) - { - vl = mComposition->atlasComposition().coverageLayer(); - } - if ( mComposition->atlasMode() != QgsComposition::AtlasOff ) + if ( mLayout ) { - feature = mComposition->atlasComposition().feature(); + vl = mLayout->context().layer(); + feature = mLayout->context().feature(); } -#endif setExpressionContext( feature, vl ); loadHtml( true ); diff --git a/src/core/layout/qgslayoutitemlabel.cpp b/src/core/layout/qgslayoutitemlabel.cpp index 308a35c2d395..ec70ade9829f 100644 --- a/src/core/layout/qgslayoutitemlabel.cpp +++ b/src/core/layout/qgslayoutitemlabel.cpp @@ -67,13 +67,6 @@ QgsLayoutItemLabel::QgsLayoutItemLabel( QgsLayout *layout ) //otherwise fields in the label aren't correctly evaluated until atlas preview feature changes (#9457) refreshExpressionContext(); - if ( mLayout ) - { - //connect to context feature changes - //to update the expression context - connect( &mLayout->context(), &QgsLayoutContext::changed, this, &QgsLayoutItemLabel::refreshExpressionContext ); - } - mWebPage.reset( new QgsWebPage( this ) ); mWebPage->setIdentifier( tr( "Layout label item" ) ); mWebPage->setNetworkAccessManager( QgsNetworkAccessManager::instance() ); @@ -253,7 +246,7 @@ void QgsLayoutItemLabel::refreshExpressionContext() //set to composition's reference map's crs QgsLayoutItemMap *referenceMap = mLayout->referenceMap(); if ( referenceMap ) - mDistanceArea->setSourceCrs( referenceMap->crs() ); + mDistanceArea->setSourceCrs( referenceMap->crs(), mLayout->project()->transformContext() ); } mDistanceArea->setEllipsoid( mLayout->project()->ellipsoid() ); contentChanged(); @@ -493,7 +486,7 @@ void QgsLayoutItemLabel::setFrameStrokeWidth( const QgsLayoutMeasurement &stroke void QgsLayoutItemLabel::refresh() { QgsLayoutItem::refresh(); - contentChanged(); + refreshExpressionContext(); } void QgsLayoutItemLabel::itemShiftAdjustSize( double newWidth, double newHeight, double &xShift, double &yShift ) const From 4a7813b9537d1d923a700db516b7df0d35d7da10 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 23 Dec 2017 15:30:13 +1000 Subject: [PATCH 021/105] Restore picture atlas handling --- python/core/layout/qgslayoutcontext.sip | 5 +++++ src/core/layout/qgslayoutcontext.cpp | 4 ++++ src/core/layout/qgslayoutcontext.h | 5 +++++ src/core/layout/qgslayoutitempicture.cpp | 6 ++---- tests/src/core/testqgslayoutcontext.cpp | 8 ++++++++ 5 files changed, 24 insertions(+), 4 deletions(-) diff --git a/python/core/layout/qgslayoutcontext.sip b/python/core/layout/qgslayoutcontext.sip index 203cebb7a9f6..c3e987e77ffe 100644 --- a/python/core/layout/qgslayoutcontext.sip +++ b/python/core/layout/qgslayoutcontext.sip @@ -235,6 +235,11 @@ Emitted when the context's ``layer`` is changed. %Docstring Emitted certain settings in the context is changed, e.g. by setting a new feature or vector layer for the context. +%End + + void dpiChanged(); +%Docstring +Emitted when the context's DPI is changed. %End }; diff --git a/src/core/layout/qgslayoutcontext.cpp b/src/core/layout/qgslayoutcontext.cpp index 6fe6d928c153..dc70e54b0563 100644 --- a/src/core/layout/qgslayoutcontext.cpp +++ b/src/core/layout/qgslayoutcontext.cpp @@ -89,7 +89,11 @@ void QgsLayoutContext::setLayer( QgsVectorLayer *layer ) void QgsLayoutContext::setDpi( double dpi ) { + if ( dpi == mMeasurementConverter.dpi() ) + return; + mMeasurementConverter.setDpi( dpi ); + emit dpiChanged(); } double QgsLayoutContext::dpi() const diff --git a/src/core/layout/qgslayoutcontext.h b/src/core/layout/qgslayoutcontext.h index 339522359a20..0bb53b10ffbd 100644 --- a/src/core/layout/qgslayoutcontext.h +++ b/src/core/layout/qgslayoutcontext.h @@ -236,6 +236,11 @@ class CORE_EXPORT QgsLayoutContext : public QObject */ void changed(); + /** + * Emitted when the context's DPI is changed. + */ + void dpiChanged(); + private: Flags mFlags = nullptr; diff --git a/src/core/layout/qgslayoutitempicture.cpp b/src/core/layout/qgslayoutitempicture.cpp index 4ccb50e8966b..2d2a86e79002 100644 --- a/src/core/layout/qgslayoutitempicture.cpp +++ b/src/core/layout/qgslayoutitempicture.cpp @@ -54,14 +54,12 @@ QgsLayoutItemPicture::QgsLayoutItemPicture( QgsLayout *layout ) //connect some signals -#if 0 //TODO //connect to atlas feature changing //to update the picture source expression - connect( &mComposition->atlasComposition(), &QgsAtlasComposition::featureChanged, this, [ = ] { refreshPicture(); } ); + connect( &layout->context(), &QgsLayoutContext::changed, this, [ = ] { refreshPicture(); } ); //connect to layout print resolution changing - connect( layout->context(), &QgsLayoutContext::printResolutionChanged, this, &QgsLayoutItemPicture::recalculateSize ); -#endif + connect( &layout->context(), &QgsLayoutContext::dpiChanged, this, &QgsLayoutItemPicture::recalculateSize ); connect( this, &QgsLayoutItem::sizePositionChanged, this, &QgsLayoutItemPicture::shapeChanged ); } diff --git a/tests/src/core/testqgslayoutcontext.cpp b/tests/src/core/testqgslayoutcontext.cpp index 986931a4fa5d..ed9bd23eacf1 100644 --- a/tests/src/core/testqgslayoutcontext.cpp +++ b/tests/src/core/testqgslayoutcontext.cpp @@ -145,9 +145,17 @@ void TestQgsLayoutContext::layer() void TestQgsLayoutContext::dpi() { QgsLayoutContext context; + + QSignalSpy spyDpiChanged( &context, &QgsLayoutContext::dpiChanged ); context.setDpi( 600 ); QCOMPARE( context.dpi(), 600.0 ); QCOMPARE( context.measurementConverter().dpi(), 600.0 ); + QCOMPARE( spyDpiChanged.count(), 1 ); + + context.setDpi( 600 ); + QCOMPARE( spyDpiChanged.count(), 1 ); + context.setDpi( 6000 ); + QCOMPARE( spyDpiChanged.count(), 2 ); } void TestQgsLayoutContext::renderContextFlags() From 92003c87976357a7c5ef0ed63f85472ce4de46d5 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 23 Dec 2017 15:56:52 +1000 Subject: [PATCH 022/105] Code shuffle, to make QgsLayoutContext aware of parent QgsLayout --- python/core/layout/qgslayoutcontext.sip | 5 ++++- src/core/layout/qgslayout.cpp | 27 +++++++++++++++++-------- src/core/layout/qgslayout.h | 8 ++++---- src/core/layout/qgslayoutcontext.cpp | 8 +++++--- src/core/layout/qgslayoutcontext.h | 7 ++++++- tests/src/core/testqgslayoutcontext.cpp | 16 +++++++-------- 6 files changed, 46 insertions(+), 25 deletions(-) diff --git a/python/core/layout/qgslayoutcontext.sip b/python/core/layout/qgslayoutcontext.sip index c3e987e77ffe..3b835030b2b0 100644 --- a/python/core/layout/qgslayoutcontext.sip +++ b/python/core/layout/qgslayoutcontext.sip @@ -32,7 +32,10 @@ class QgsLayoutContext : QObject typedef QFlags Flags; - QgsLayoutContext(); + QgsLayoutContext( QgsLayout *layout /TransferThis/ ); +%Docstring +Constructor for QgsLayoutContext. +%End void setFlags( const QgsLayoutContext::Flags flags ); %Docstring diff --git a/src/core/layout/qgslayout.cpp b/src/core/layout/qgslayout.cpp index 1cba98138408..db482ffaadf4 100644 --- a/src/core/layout/qgslayout.cpp +++ b/src/core/layout/qgslayout.cpp @@ -30,6 +30,7 @@ QgsLayout::QgsLayout( QgsProject *project ) : mProject( project ) + , mContext( new QgsLayoutContext( this ) ) , mSnapper( QgsLayoutSnapper( this ) ) , mGridSettings( this ) , mPageCollection( new QgsLayoutPageCollection( this ) ) @@ -281,32 +282,42 @@ QgsLayoutItem *QgsLayout::layoutItemAt( QPointF position, const QgsLayoutItem *b double QgsLayout::convertToLayoutUnits( const QgsLayoutMeasurement &measurement ) const { - return mContext.measurementConverter().convert( measurement, mUnits ).length(); + return mContext->measurementConverter().convert( measurement, mUnits ).length(); } QSizeF QgsLayout::convertToLayoutUnits( const QgsLayoutSize &size ) const { - return mContext.measurementConverter().convert( size, mUnits ).toQSizeF(); + return mContext->measurementConverter().convert( size, mUnits ).toQSizeF(); } QPointF QgsLayout::convertToLayoutUnits( const QgsLayoutPoint &point ) const { - return mContext.measurementConverter().convert( point, mUnits ).toQPointF(); + return mContext->measurementConverter().convert( point, mUnits ).toQPointF(); } QgsLayoutMeasurement QgsLayout::convertFromLayoutUnits( const double length, const QgsUnitTypes::LayoutUnit unit ) const { - return mContext.measurementConverter().convert( QgsLayoutMeasurement( length, mUnits ), unit ); + return mContext->measurementConverter().convert( QgsLayoutMeasurement( length, mUnits ), unit ); } QgsLayoutSize QgsLayout::convertFromLayoutUnits( const QSizeF &size, const QgsUnitTypes::LayoutUnit unit ) const { - return mContext.measurementConverter().convert( QgsLayoutSize( size.width(), size.height(), mUnits ), unit ); + return mContext->measurementConverter().convert( QgsLayoutSize( size.width(), size.height(), mUnits ), unit ); } QgsLayoutPoint QgsLayout::convertFromLayoutUnits( const QPointF &point, const QgsUnitTypes::LayoutUnit unit ) const { - return mContext.measurementConverter().convert( QgsLayoutPoint( point.x(), point.y(), mUnits ), unit ); + return mContext->measurementConverter().convert( QgsLayoutPoint( point.x(), point.y(), mUnits ), unit ); +} + +QgsLayoutContext &QgsLayout::context() +{ + return *mContext; +} + +const QgsLayoutContext &QgsLayout::context() const +{ + return *mContext; } QgsLayoutGuideCollection &QgsLayout::guides() @@ -709,7 +720,7 @@ void QgsLayout::writeXmlLayoutSettings( QDomElement &element, QDomDocument &docu element.setAttribute( QStringLiteral( "name" ), mName ); element.setAttribute( QStringLiteral( "units" ), QgsUnitTypes::encodeUnit( mUnits ) ); element.setAttribute( QStringLiteral( "worldFileMap" ), mWorldFileMapId ); - element.setAttribute( QStringLiteral( "printResolution" ), mContext.dpi() ); + element.setAttribute( QStringLiteral( "printResolution" ), mContext->dpi() ); } QDomElement QgsLayout::writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const @@ -753,7 +764,7 @@ bool QgsLayout::readXmlLayoutSettings( const QDomElement &layoutElement, const Q setName( layoutElement.attribute( QStringLiteral( "name" ) ) ); setUnits( QgsUnitTypes::decodeLayoutUnit( layoutElement.attribute( QStringLiteral( "units" ) ) ) ); mWorldFileMapId = layoutElement.attribute( QStringLiteral( "worldFileMap" ) ); - mContext.setDpi( layoutElement.attribute( QStringLiteral( "printResolution" ), "300" ).toDouble() ); + mContext->setDpi( layoutElement.attribute( QStringLiteral( "printResolution" ), "300" ).toDouble() ); emit changed(); return true; diff --git a/src/core/layout/qgslayout.h b/src/core/layout/qgslayout.h index 326d9ba7c21f..92f731c51cf0 100644 --- a/src/core/layout/qgslayout.h +++ b/src/core/layout/qgslayout.h @@ -18,7 +18,6 @@ #include "qgis_core.h" #include -#include "qgslayoutcontext.h" #include "qgslayoutsnapper.h" #include "qgsexpressioncontextgenerator.h" #include "qgslayoutgridsettings.h" @@ -30,6 +29,7 @@ class QgsLayoutModel; class QgsLayoutMultiFrame; class QgsLayoutPageCollection; class QgsLayoutUndoStack; +class QgsLayoutContext; /** * \ingroup core @@ -318,13 +318,13 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext * Returns a reference to the layout's context, which stores information relating to the * current context and rendering settings for the layout. */ - QgsLayoutContext &context() { return mContext; } + QgsLayoutContext &context(); /** * Returns a reference to the layout's context, which stores information relating to the * current context and rendering settings for the layout. */ - SIP_SKIP const QgsLayoutContext &context() const { return mContext; } + SIP_SKIP const QgsLayoutContext &context() const; /** * Returns a reference to the layout's snapper, which stores handles layout snap grids and lines @@ -629,7 +629,7 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext QgsObjectCustomProperties mCustomProperties; QgsUnitTypes::LayoutUnit mUnits = QgsUnitTypes::LayoutMillimeters; - QgsLayoutContext mContext; + QgsLayoutContext *mContext = nullptr; QgsLayoutSnapper mSnapper; QgsLayoutGridSettings mGridSettings; diff --git a/src/core/layout/qgslayoutcontext.cpp b/src/core/layout/qgslayoutcontext.cpp index dc70e54b0563..d885f921330d 100644 --- a/src/core/layout/qgslayoutcontext.cpp +++ b/src/core/layout/qgslayoutcontext.cpp @@ -16,10 +16,12 @@ #include "qgslayoutcontext.h" #include "qgsfeature.h" +#include "qgslayout.h" - -QgsLayoutContext::QgsLayoutContext() - : mFlags( FlagAntialiasing | FlagUseAdvancedEffects ) +QgsLayoutContext::QgsLayoutContext( QgsLayout *layout ) + : QObject( layout ) + , mFlags( FlagAntialiasing | FlagUseAdvancedEffects ) + , mLayout( layout ) {} void QgsLayoutContext::setFlags( const QgsLayoutContext::Flags flags ) diff --git a/src/core/layout/qgslayoutcontext.h b/src/core/layout/qgslayoutcontext.h index 0bb53b10ffbd..0f72f6f5a501 100644 --- a/src/core/layout/qgslayoutcontext.h +++ b/src/core/layout/qgslayoutcontext.h @@ -49,7 +49,10 @@ class CORE_EXPORT QgsLayoutContext : public QObject }; Q_DECLARE_FLAGS( Flags, Flag ) - QgsLayoutContext(); + /** + * Constructor for QgsLayoutContext. + */ + QgsLayoutContext( QgsLayout *layout SIP_TRANSFERTHIS ); /** * Sets the combination of \a flags that will be used for rendering the layout. @@ -245,6 +248,8 @@ class CORE_EXPORT QgsLayoutContext : public QObject Flags mFlags = nullptr; + QgsLayout *mLayout = nullptr; + int mCurrentExportLayer = -1; QgsFeature mFeature; diff --git a/tests/src/core/testqgslayoutcontext.cpp b/tests/src/core/testqgslayoutcontext.cpp index ed9bd23eacf1..ebf8ef217635 100644 --- a/tests/src/core/testqgslayoutcontext.cpp +++ b/tests/src/core/testqgslayoutcontext.cpp @@ -75,14 +75,14 @@ void TestQgsLayoutContext::cleanup() void TestQgsLayoutContext::creation() { - QgsLayoutContext *context = new QgsLayoutContext(); + QgsLayoutContext *context = new QgsLayoutContext( nullptr ); QVERIFY( context ); delete context; } void TestQgsLayoutContext::flags() { - QgsLayoutContext context; + QgsLayoutContext context( nullptr ); QSignalSpy spyFlagsChanged( &context, &QgsLayoutContext::flagsChanged ); //test getting and setting flags @@ -108,7 +108,7 @@ void TestQgsLayoutContext::flags() void TestQgsLayoutContext::feature() { - QgsLayoutContext context; + QgsLayoutContext context( nullptr ); //test removing feature context.setFeature( QgsFeature() ); @@ -124,7 +124,7 @@ void TestQgsLayoutContext::feature() void TestQgsLayoutContext::layer() { - QgsLayoutContext context; + QgsLayoutContext context( nullptr ); //test clearing layer context.setLayer( nullptr ); @@ -144,7 +144,7 @@ void TestQgsLayoutContext::layer() void TestQgsLayoutContext::dpi() { - QgsLayoutContext context; + QgsLayoutContext context( nullptr ); QSignalSpy spyDpiChanged( &context, &QgsLayoutContext::dpiChanged ); context.setDpi( 600 ); @@ -160,7 +160,7 @@ void TestQgsLayoutContext::dpi() void TestQgsLayoutContext::renderContextFlags() { - QgsLayoutContext context; + QgsLayoutContext context( nullptr ); context.setFlags( 0 ); QgsRenderContext::Flags flags = context.renderContextFlags(); QVERIFY( !( flags & QgsRenderContext::Antialiasing ) ); @@ -182,7 +182,7 @@ void TestQgsLayoutContext::renderContextFlags() void TestQgsLayoutContext::boundingBoxes() { - QgsLayoutContext context; + QgsLayoutContext context( nullptr ); context.setBoundingBoxesVisible( false ); QVERIFY( !context.boundingBoxesVisible() ); context.setBoundingBoxesVisible( true ); @@ -191,7 +191,7 @@ void TestQgsLayoutContext::boundingBoxes() void TestQgsLayoutContext::exportLayer() { - QgsLayoutContext context; + QgsLayoutContext context( nullptr ); // must default to -1 QCOMPARE( context.currentExportLayer(), -1 ); context.setCurrentExportLayer( 1 ); From 2ef3a5f199070d16467af136cd77a2a9241135f2 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 23 Dec 2017 16:04:58 +1000 Subject: [PATCH 023/105] Port current geometry from atlas to layouts --- python/core/layout/qgslayoutcontext.sip | 14 ++++++++ src/core/layout/qgslayoutcontext.cpp | 33 +++++++++++++++++++ src/core/layout/qgslayoutcontext.h | 15 +++++++++ tests/src/core/testqgslayoutcontext.cpp | 44 +++++++++++++++++++++++++ 4 files changed, 106 insertions(+) diff --git a/python/core/layout/qgslayoutcontext.sip b/python/core/layout/qgslayoutcontext.sip index 3b835030b2b0..b8fe673de971 100644 --- a/python/core/layout/qgslayoutcontext.sip +++ b/python/core/layout/qgslayoutcontext.sip @@ -104,7 +104,21 @@ Returns the current feature for evaluating the layout. This feature may be used for altering an item's content and appearance for a report or atlas layout. +.. seealso:: :py:func:`currentGeometry()` + .. seealso:: :py:func:`setFeature()` +%End + + QgsGeometry currentGeometry( const QgsCoordinateReferenceSystem &crs = QgsCoordinateReferenceSystem() ) const; +%Docstring +Returns the current feature() geometry in the given ``crs``. +If no CRS is specified, the original feature geometry is returned. + +Reprojection only works if a valid layer is set for layer(). + +.. seealso:: :py:func:`feature()` + +.. seealso:: :py:func:`layer()` %End QgsVectorLayer *layer() const; diff --git a/src/core/layout/qgslayoutcontext.cpp b/src/core/layout/qgslayoutcontext.cpp index d885f921330d..fd74e234090b 100644 --- a/src/core/layout/qgslayoutcontext.cpp +++ b/src/core/layout/qgslayoutcontext.cpp @@ -74,9 +74,42 @@ QgsRenderContext::Flags QgsLayoutContext::renderContextFlags() const void QgsLayoutContext::setFeature( const QgsFeature &feature ) { mFeature = feature; + mGeometryCache.clear(); emit changed(); } +QgsGeometry QgsLayoutContext::currentGeometry( const QgsCoordinateReferenceSystem &crs ) const +{ + if ( !crs.isValid() ) + { + // no projection, return the native geometry + return mFeature.geometry(); + } + + if ( !mLayer || !mFeature.isValid() || !mFeature.hasGeometry() ) + { + return QgsGeometry(); + } + + if ( mLayer->crs() == crs ) + { + // no projection, return the native geometry + return mFeature.geometry(); + } + + auto it = mGeometryCache.constFind( crs.srsid() ); + if ( it != mGeometryCache.constEnd() ) + { + // we have it in cache, return it + return it.value(); + } + + QgsGeometry transformed = mFeature.geometry(); + transformed.transform( QgsCoordinateTransform( mLayer->crs(), crs, mLayout->project() ) ); + mGeometryCache[crs.srsid()] = transformed; + return transformed; +} + QgsVectorLayer *QgsLayoutContext::layer() const { return mLayer; diff --git a/src/core/layout/qgslayoutcontext.h b/src/core/layout/qgslayoutcontext.h index 0f72f6f5a501..7d5eba142849 100644 --- a/src/core/layout/qgslayoutcontext.h +++ b/src/core/layout/qgslayoutcontext.h @@ -107,10 +107,22 @@ class CORE_EXPORT QgsLayoutContext : public QObject * Returns the current feature for evaluating the layout. This feature may * be used for altering an item's content and appearance for a report * or atlas layout. + * \see currentGeometry() * \see setFeature() */ QgsFeature feature() const { return mFeature; } + /** + * Returns the current feature() geometry in the given \a crs. + * If no CRS is specified, the original feature geometry is returned. + * + * Reprojection only works if a valid layer is set for layer(). + * + * \see feature() + * \see layer() + */ + QgsGeometry currentGeometry( const QgsCoordinateReferenceSystem &crs = QgsCoordinateReferenceSystem() ) const; + /** * Returns the vector layer associated with the layout's context. * \see setLayer() @@ -262,6 +274,9 @@ class CORE_EXPORT QgsLayoutContext : public QObject bool mBoundingBoxesVisible = true; bool mPagesVisible = true; + // projected geometry cache + mutable QMap mGeometryCache; + friend class QgsLayoutExporter; friend class TestQgsLayout; friend class LayoutContextPreviewSettingRestorer; diff --git a/tests/src/core/testqgslayoutcontext.cpp b/tests/src/core/testqgslayoutcontext.cpp index ebf8ef217635..c6699945c246 100644 --- a/tests/src/core/testqgslayoutcontext.cpp +++ b/tests/src/core/testqgslayoutcontext.cpp @@ -19,6 +19,9 @@ #include "qgis.h" #include "qgsfeature.h" #include "qgsvectorlayer.h" +#include "qgsgeometry.h" +#include "qgsproject.h" +#include "qgslayout.h" #include #include "qgstest.h" #include @@ -40,6 +43,7 @@ class TestQgsLayoutContext: public QObject void renderContextFlags(); void boundingBoxes(); void exportLayer(); + void geometry(); private: QString mReport; @@ -198,5 +202,45 @@ void TestQgsLayoutContext::exportLayer() QCOMPARE( context.currentExportLayer(), 1 ); } +void TestQgsLayoutContext::geometry() +{ + QgsProject p; + QgsLayout l( &p ); + QgsLayoutContext context( &l ); + + // no feature set + QVERIFY( context.currentGeometry().isNull() ); + QVERIFY( context.currentGeometry( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3111" ) ) ).isNull() ); + + // no layer set + QgsFeature f; + f.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "LineString( 144 -38, 145 -39 )" ) ) ); + context.setFeature( f ); + QCOMPARE( context.currentGeometry().asWkt(), f.geometry().asWkt() ); + QVERIFY( context.currentGeometry( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3111" ) ) ).isNull() ); + + //with layer + QgsVectorLayer *layer = new QgsVectorLayer( QStringLiteral( "Point?crs=EPSG:4326&field=id_a:integer" ), QStringLiteral( "A" ), QStringLiteral( "memory" ) ); + context.setLayer( layer ); + + QCOMPARE( context.currentGeometry().asWkt(), f.geometry().asWkt() ); + QVERIFY( !context.currentGeometry( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3111" ) ) ).isNull() ); + QCOMPARE( context.currentGeometry( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3111" ) ) ).asWkt( 0 ), QStringLiteral( "LineString (2412169 2388563, 2500000 2277996)" ) ); + + // should be cached + QCOMPARE( context.currentGeometry( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3111" ) ) ).asWkt( 0 ), QStringLiteral( "LineString (2412169 2388563, 2500000 2277996)" ) ); + + // layer crs + QCOMPARE( context.currentGeometry( layer->crs() ).asWkt(), f.geometry().asWkt() ); + + // clear cache + QgsFeature f2; + context.setFeature( f2 ); + QVERIFY( context.currentGeometry().isNull() ); + QVERIFY( context.currentGeometry( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3111" ) ) ).isNull() ); + + delete layer; +} + QGSTEST_MAIN( TestQgsLayoutContext ) #include "testqgslayoutcontext.moc" From 3994c4a4764f499d0596d042cb8590afa83353cf Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 23 Dec 2017 16:17:57 +1000 Subject: [PATCH 024/105] Restore legend atlas behavior --- python/core/layout/qgslayoutitemlegend.sip | 1 + src/app/layout/qgslayoutlegendwidget.cpp | 25 ++++++++++------------ src/core/layout/qgslayoutitemlegend.cpp | 15 +++++++------ src/core/layout/qgslayoutitemlegend.h | 3 ++- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/python/core/layout/qgslayoutitemlegend.sip b/python/core/layout/qgslayoutitemlegend.sip index 6fb7da9f3153..f07428abd5f5 100644 --- a/python/core/layout/qgslayoutitemlegend.sip +++ b/python/core/layout/qgslayoutitemlegend.sip @@ -490,6 +490,7 @@ Returns the legend's renderer settings object. public slots: + void refresh(); virtual void refreshDataDefinedProperty( const QgsLayoutObject::DataDefinedProperty property = QgsLayoutObject::AllProperties ); diff --git a/src/app/layout/qgslayoutlegendwidget.cpp b/src/app/layout/qgslayoutlegendwidget.cpp index 4140ea93c51e..b4649c88b1ad 100644 --- a/src/app/layout/qgslayoutlegendwidget.cpp +++ b/src/app/layout/qgslayoutlegendwidget.cpp @@ -35,6 +35,7 @@ #include "qgsproject.h" #include "qgsvectorlayer.h" #include "qgslayoutitemlegend.h" +#include "qgslayoutatlas.h" #include #include @@ -136,11 +137,12 @@ QgsLayoutLegendWidget::QgsLayoutLegendWidget( QgsLayoutItemLegend *legend ) mItemTreeView->setMenuProvider( new QgsLayoutLegendMenuProvider( mItemTreeView, this ) ); connect( legend, &QgsLayoutObject::changed, this, &QgsLayoutLegendWidget::setGuiElements ); -#if 0 //TODO // connect atlas state to the filter legend by atlas checkbox - connect( &legend->composition()->atlasComposition(), &QgsAtlasComposition::toggled, this, &QgsLayoutLegendWidget::updateFilterLegendByAtlasButton ); - connect( &legend->composition()->atlasComposition(), &QgsAtlasComposition::coverageLayerChanged, this, &QgsLayoutLegendWidget::updateFilterLegendByAtlasButton ); -#endif + if ( layoutAtlas() ) + { + connect( layoutAtlas(), &QgsLayoutAtlas::toggled, this, &QgsLayoutLegendWidget::updateFilterLegendByAtlasButton ); + } + connect( &legend->layout()->context(), &QgsLayoutContext::layerChanged, this, &QgsLayoutLegendWidget::updateFilterLegendByAtlasButton ); registerDataDefinedButton( mLegendTitleDDBtn, QgsLayoutObject::LegendTitle ); registerDataDefinedButton( mColumnsDDBtn, QgsLayoutObject::LegendColumnCount ); @@ -900,14 +902,9 @@ void QgsLayoutLegendWidget::mFilterLegendByAtlasCheckBox_toggled( bool toggled ) Q_UNUSED( toggled ); if ( mLegend ) { -#if 0 //TODO mLegend->setLegendFilterOutAtlas( toggled ); // force update of legend when in preview mode - if ( mLegend->composition()->atlasMode() == QgsComposition::PreviewAtlas ) - { - mLegend->composition()->atlasComposition().refreshFeature(); - } -#endif + mLegend->refresh(); } } @@ -1034,10 +1031,10 @@ void QgsLayoutLegendWidget::setCurrentNodeStyleFromAction() void QgsLayoutLegendWidget::updateFilterLegendByAtlasButton() { -#if 0 //TODO - const QgsAtlasComposition &atlas = mLegend->composition()->atlasComposition(); - mFilterLegendByAtlasCheckBox->setEnabled( atlas.enabled() && atlas.coverageLayer() && atlas.coverageLayer()->geometryType() == QgsWkbTypes::PolygonGeometry ); -#endif + if ( QgsLayoutAtlas *atlas = layoutAtlas() ) + { + mFilterLegendByAtlasCheckBox->setEnabled( atlas->enabled() && mLegend->layout()->context().layer() && mLegend->layout()->context().layer()->geometryType() == QgsWkbTypes::PolygonGeometry ); + } } void QgsLayoutLegendWidget::mItemTreeView_doubleClicked( const QModelIndex &idx ) diff --git a/src/core/layout/qgslayoutitemlegend.cpp b/src/core/layout/qgslayoutitemlegend.cpp index 990696fd015b..a69d4d63f098 100644 --- a/src/core/layout/qgslayoutitemlegend.cpp +++ b/src/core/layout/qgslayoutitemlegend.cpp @@ -40,7 +40,6 @@ QgsLayoutItemLegend::QgsLayoutItemLegend( QgsLayout *layout ) { #if 0 //TODO connect( &layout->atlasComposition(), &QgsAtlasComposition::renderEnded, this, &QgsLayoutItemLegend::onAtlasEnded ); - connect( &layout->atlasComposition(), &QgsAtlasComposition::featureChanged, this, &QgsLayoutItemLegend::onAtlasFeature ); #endif // Connect to the main layertreeroot. @@ -139,6 +138,12 @@ void QgsLayoutItemLegend::finalizeRestoreFromXml() } } +void QgsLayoutItemLegend::refresh() +{ + QgsLayoutItem::refresh(); + onAtlasFeature(); +} + void QgsLayoutItemLegend::draw( QgsRenderContext &context, const QStyleOptionGraphicsItem * ) { QPainter *painter = context.painter(); @@ -761,9 +766,7 @@ void QgsLayoutItemLegend::doUpdateFilterByMap() QgsGeometry filterPolygon; if ( mInAtlas ) { -#if 0 //TODO - filterPolygon = composition()->atlasComposition().currentGeometry( mMap->crs() ); -#endif + filterPolygon = mLayout->context().currentGeometry( mMap->crs() ); } mLegendModel->setLegendFilter( &ms, /* useExtent */ mInAtlas || mLegendFilterByMap, filterPolygon, /* useExpressions */ true ); } @@ -783,9 +786,9 @@ bool QgsLayoutItemLegend::legendFilterOutAtlas() const return mFilterOutAtlas; } -void QgsLayoutItemLegend::onAtlasFeature( QgsFeature *feat ) +void QgsLayoutItemLegend::onAtlasFeature() { - if ( !feat ) + if ( !mLayout->context().feature().isValid() ) return; mInAtlas = mFilterOutAtlas; updateFilterByMap(); diff --git a/src/core/layout/qgslayoutitemlegend.h b/src/core/layout/qgslayoutitemlegend.h index 86104e0dcf22..6b0911f07f00 100644 --- a/src/core/layout/qgslayoutitemlegend.h +++ b/src/core/layout/qgslayoutitemlegend.h @@ -446,6 +446,7 @@ class CORE_EXPORT QgsLayoutItemLegend : public QgsLayoutItem public slots: + void refresh(); void refreshDataDefinedProperty( const QgsLayoutObject::DataDefinedProperty property = QgsLayoutObject::AllProperties ) override; protected: @@ -469,7 +470,7 @@ class CORE_EXPORT QgsLayoutItemLegend : public QgsLayoutItem //! react to atlas void onAtlasEnded(); - void onAtlasFeature( QgsFeature * ); + void onAtlasFeature(); void nodeCustomPropertyChanged( QgsLayerTreeNode *node, const QString &key ); From 5160ad942f68bc585a26d5304263a43c7e47c79f Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 23 Dec 2017 16:20:53 +1000 Subject: [PATCH 025/105] Remove some outdated todos --- src/core/layout/qgslayoutitemmap.cpp | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/core/layout/qgslayoutitemmap.cpp b/src/core/layout/qgslayoutitemmap.cpp index 476fc250090e..18b3b8555e5f 100644 --- a/src/core/layout/qgslayoutitemmap.cpp +++ b/src/core/layout/qgslayoutitemmap.cpp @@ -504,10 +504,6 @@ void QgsLayoutItemMap::draw( QgsRenderContext &, const QStyleOptionGraphicsItem bool QgsLayoutItemMap::writePropertiesToElement( QDomElement &composerMapElem, QDomDocument &doc, const QgsReadWriteContext &context ) const { -#if 0 //TODO - is this needed? - composerMapElem.setAttribute( QStringLiteral( "id" ), mId ); -#endif - if ( mKeepLayerSet ) { composerMapElem.setAttribute( QStringLiteral( "keepLayerSet" ), QStringLiteral( "true" ) ); @@ -608,14 +604,6 @@ bool QgsLayoutItemMap::writePropertiesToElement( QDomElement &composerMapElem, Q bool QgsLayoutItemMap::readPropertiesFromElement( const QDomElement &itemElem, const QDomDocument &doc, const QgsReadWriteContext &context ) { mUpdatesEnabled = false; -#if 0 //TODO - QString idRead = itemElem.attribute( QStringLiteral( "id" ), QStringLiteral( "not found" ) ); - if ( idRead != QLatin1String( "not found" ) ) - { - mId = idRead.toInt(); - updateToolTip(); - } -#endif //extent QDomNodeList extentNodeList = itemElem.elementsByTagName( QStringLiteral( "Extent" ) ); @@ -1597,7 +1585,6 @@ QPointF QgsLayoutItemMap::layoutMapPosForItem( const QgsAnnotation *annotation ) if ( annotationCrs != crs() ) { //need to reproject - // todo datum nyall set context QgsCoordinateTransform t( annotationCrs, crs(), mLayout->project() ); double z = 0.0; try From aafe1cc477dfade522cca95502b7399f724f6c0c Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 23 Dec 2017 16:24:36 +1000 Subject: [PATCH 026/105] Fix some untranslatable strings --- src/core/layout/qgslayoutitemlegend.cpp | 2 +- src/core/layout/qgslayoutitemregistry.cpp | 28 +++++++++++------------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/core/layout/qgslayoutitemlegend.cpp b/src/core/layout/qgslayoutitemlegend.cpp index a69d4d63f098..be32bbe2c60e 100644 --- a/src/core/layout/qgslayoutitemlegend.cpp +++ b/src/core/layout/qgslayoutitemlegend.cpp @@ -626,7 +626,7 @@ QString QgsLayoutItemLegend::displayName() const QString text = mSettings.title(); if ( text.isEmpty() ) { - return tr( "" ); + return tr( "" ); } if ( text.length() > 25 ) { diff --git a/src/core/layout/qgslayoutitemregistry.cpp b/src/core/layout/qgslayoutitemregistry.cpp index 3053071c58eb..1ac4dd15a8f5 100644 --- a/src/core/layout/qgslayoutitemregistry.cpp +++ b/src/core/layout/qgslayoutitemregistry.cpp @@ -59,26 +59,26 @@ bool QgsLayoutItemRegistry::populate() addLayoutItemType( new QgsLayoutItemMetadata( QgsLayoutItemRegistry::LayoutItem + 1002, QStringLiteral( "temp type" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddLabel.svg" ) ), createTemporaryItem ) ); #endif - addLayoutItemType( new QgsLayoutItemMetadata( LayoutGroup, QStringLiteral( "Group" ), QIcon(), QgsLayoutItemGroup::create ) ); - addLayoutItemType( new QgsLayoutItemMetadata( LayoutFrame, QStringLiteral( "Frame" ), QIcon(), QgsLayoutFrame::create ) ); - addLayoutItemType( new QgsLayoutItemMetadata( LayoutPage, QStringLiteral( "Page" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionFileNew.svg" ) ), QgsLayoutItemPage::create ) ); - addLayoutItemType( new QgsLayoutItemMetadata( LayoutMap, QStringLiteral( "Map" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddMap.svg" ) ), QgsLayoutItemMap::create ) ); - addLayoutItemType( new QgsLayoutItemMetadata( LayoutPicture, QStringLiteral( "Picture" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddImage.svg" ) ), QgsLayoutItemPicture::create ) ); - addLayoutItemType( new QgsLayoutItemMetadata( LayoutLabel, QStringLiteral( "Label" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionLabel.svg" ) ), QgsLayoutItemLabel::create ) ); - addLayoutItemType( new QgsLayoutItemMetadata( LayoutLegend, QStringLiteral( "Legend" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddLegend.svg" ) ), QgsLayoutItemLegend::create ) ); - addLayoutItemType( new QgsLayoutItemMetadata( LayoutScaleBar, QStringLiteral( "Scale Bar" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionScaleBar.svg" ) ), QgsLayoutItemScaleBar::create ) ); - addLayoutItemType( new QgsLayoutItemMetadata( LayoutShape, QStringLiteral( "Shape" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddBasicRectangle.svg" ) ), []( QgsLayout * layout ) + addLayoutItemType( new QgsLayoutItemMetadata( LayoutGroup, QObject::tr( "Group" ), QIcon(), QgsLayoutItemGroup::create ) ); + addLayoutItemType( new QgsLayoutItemMetadata( LayoutFrame, QObject::tr( "Frame" ), QIcon(), QgsLayoutFrame::create ) ); + addLayoutItemType( new QgsLayoutItemMetadata( LayoutPage, QObject::tr( "Page" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionFileNew.svg" ) ), QgsLayoutItemPage::create ) ); + addLayoutItemType( new QgsLayoutItemMetadata( LayoutMap, QObject::tr( "Map" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddMap.svg" ) ), QgsLayoutItemMap::create ) ); + addLayoutItemType( new QgsLayoutItemMetadata( LayoutPicture, QObject::tr( "Picture" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddImage.svg" ) ), QgsLayoutItemPicture::create ) ); + addLayoutItemType( new QgsLayoutItemMetadata( LayoutLabel, QObject::tr( "Label" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionLabel.svg" ) ), QgsLayoutItemLabel::create ) ); + addLayoutItemType( new QgsLayoutItemMetadata( LayoutLegend, QObject::tr( "Legend" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddLegend.svg" ) ), QgsLayoutItemLegend::create ) ); + addLayoutItemType( new QgsLayoutItemMetadata( LayoutScaleBar, QObject::tr( "Scalebar" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionScaleBar.svg" ) ), QgsLayoutItemScaleBar::create ) ); + addLayoutItemType( new QgsLayoutItemMetadata( LayoutShape, QObject::tr( "Shape" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddBasicRectangle.svg" ) ), []( QgsLayout * layout ) { QgsLayoutItemShape *shape = new QgsLayoutItemShape( layout ); shape->setShapeType( QgsLayoutItemShape::Rectangle ); return shape; } ) ); - addLayoutItemType( new QgsLayoutItemMetadata( LayoutPolygon, QStringLiteral( "Polygon" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddPolygon.svg" ) ), QgsLayoutItemPolygon::create ) ); - addLayoutItemType( new QgsLayoutItemMetadata( LayoutPolyline, QStringLiteral( "Polyline" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddPolyline.svg" ) ), QgsLayoutItemPolyline::create ) ); + addLayoutItemType( new QgsLayoutItemMetadata( LayoutPolygon, QObject::tr( "Polygon" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddPolygon.svg" ) ), QgsLayoutItemPolygon::create ) ); + addLayoutItemType( new QgsLayoutItemMetadata( LayoutPolyline, QObject::tr( "Polyline" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddPolyline.svg" ) ), QgsLayoutItemPolyline::create ) ); - addLayoutMultiFrameType( new QgsLayoutMultiFrameMetadata( LayoutHtml, QStringLiteral( "HTML" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddHtml.svg" ) ), QgsLayoutItemHtml::create ) ); - addLayoutMultiFrameType( new QgsLayoutMultiFrameMetadata( LayoutAttributeTable, QStringLiteral( "Attribute Table" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddTable.svg" ) ), QgsLayoutItemAttributeTable::create ) ); - addLayoutMultiFrameType( new QgsLayoutMultiFrameMetadata( LayoutTextTable, QStringLiteral( "Text Table" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddTable.svg" ) ), QgsLayoutItemTextTable::create ) ); + addLayoutMultiFrameType( new QgsLayoutMultiFrameMetadata( LayoutHtml, QObject::tr( "HTML" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddHtml.svg" ) ), QgsLayoutItemHtml::create ) ); + addLayoutMultiFrameType( new QgsLayoutMultiFrameMetadata( LayoutAttributeTable, QObject::tr( "Attribute Table" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddTable.svg" ) ), QgsLayoutItemAttributeTable::create ) ); + addLayoutMultiFrameType( new QgsLayoutMultiFrameMetadata( LayoutTextTable, QObject::tr( "Text Table" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddTable.svg" ) ), QgsLayoutItemTextTable::create ) ); return true; } From 49eaebbf403b43d14e9e87f0f83ef33d8581ec1e Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 23 Dec 2017 16:31:31 +1000 Subject: [PATCH 027/105] Remove outdated icon support for item metadata Now handled in QgsLayoutItem directly --- python/core/layout/qgslayoutitemregistry.sip | 5 ---- src/core/layout/qgslayoutitemregistry.cpp | 30 ++++++++++---------- src/core/layout/qgslayoutitemregistry.h | 15 ++-------- tests/src/core/testqgslayoutitem.cpp | 2 +- tests/src/core/testqgslayoutmultiframe.cpp | 2 +- tests/src/gui/testqgslayoutview.cpp | 2 +- 6 files changed, 20 insertions(+), 36 deletions(-) diff --git a/python/core/layout/qgslayoutitemregistry.sip b/python/core/layout/qgslayoutitemregistry.sip index 1e68a18323ae..e0f553b45707 100644 --- a/python/core/layout/qgslayoutitemregistry.sip +++ b/python/core/layout/qgslayoutitemregistry.sip @@ -40,11 +40,6 @@ and ``visibleName``. int type() const; %Docstring Returns the unique item type code for the layout item class. -%End - - virtual QIcon icon() const; -%Docstring -Returns an icon representing the layout item type. %End QString visibleName() const; diff --git a/src/core/layout/qgslayoutitemregistry.cpp b/src/core/layout/qgslayoutitemregistry.cpp index 1ac4dd15a8f5..a662e283dbd6 100644 --- a/src/core/layout/qgslayoutitemregistry.cpp +++ b/src/core/layout/qgslayoutitemregistry.cpp @@ -56,29 +56,29 @@ bool QgsLayoutItemRegistry::populate() return new TestLayoutItem( layout ); }; - addLayoutItemType( new QgsLayoutItemMetadata( QgsLayoutItemRegistry::LayoutItem + 1002, QStringLiteral( "temp type" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddLabel.svg" ) ), createTemporaryItem ) ); + addLayoutItemType( new QgsLayoutItemMetadata( QgsLayoutItemRegistry::LayoutItem + 1002, QStringLiteral( "temp type" ), createTemporaryItem ) ); #endif - addLayoutItemType( new QgsLayoutItemMetadata( LayoutGroup, QObject::tr( "Group" ), QIcon(), QgsLayoutItemGroup::create ) ); - addLayoutItemType( new QgsLayoutItemMetadata( LayoutFrame, QObject::tr( "Frame" ), QIcon(), QgsLayoutFrame::create ) ); - addLayoutItemType( new QgsLayoutItemMetadata( LayoutPage, QObject::tr( "Page" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionFileNew.svg" ) ), QgsLayoutItemPage::create ) ); - addLayoutItemType( new QgsLayoutItemMetadata( LayoutMap, QObject::tr( "Map" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddMap.svg" ) ), QgsLayoutItemMap::create ) ); - addLayoutItemType( new QgsLayoutItemMetadata( LayoutPicture, QObject::tr( "Picture" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddImage.svg" ) ), QgsLayoutItemPicture::create ) ); - addLayoutItemType( new QgsLayoutItemMetadata( LayoutLabel, QObject::tr( "Label" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionLabel.svg" ) ), QgsLayoutItemLabel::create ) ); - addLayoutItemType( new QgsLayoutItemMetadata( LayoutLegend, QObject::tr( "Legend" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddLegend.svg" ) ), QgsLayoutItemLegend::create ) ); - addLayoutItemType( new QgsLayoutItemMetadata( LayoutScaleBar, QObject::tr( "Scalebar" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionScaleBar.svg" ) ), QgsLayoutItemScaleBar::create ) ); - addLayoutItemType( new QgsLayoutItemMetadata( LayoutShape, QObject::tr( "Shape" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddBasicRectangle.svg" ) ), []( QgsLayout * layout ) + addLayoutItemType( new QgsLayoutItemMetadata( LayoutGroup, QObject::tr( "Group" ), QgsLayoutItemGroup::create ) ); + addLayoutItemType( new QgsLayoutItemMetadata( LayoutFrame, QObject::tr( "Frame" ), QgsLayoutFrame::create ) ); + addLayoutItemType( new QgsLayoutItemMetadata( LayoutPage, QObject::tr( "Page" ), QgsLayoutItemPage::create ) ); + addLayoutItemType( new QgsLayoutItemMetadata( LayoutMap, QObject::tr( "Map" ), QgsLayoutItemMap::create ) ); + addLayoutItemType( new QgsLayoutItemMetadata( LayoutPicture, QObject::tr( "Picture" ), QgsLayoutItemPicture::create ) ); + addLayoutItemType( new QgsLayoutItemMetadata( LayoutLabel, QObject::tr( "Label" ), QgsLayoutItemLabel::create ) ); + addLayoutItemType( new QgsLayoutItemMetadata( LayoutLegend, QObject::tr( "Legend" ), QgsLayoutItemLegend::create ) ); + addLayoutItemType( new QgsLayoutItemMetadata( LayoutScaleBar, QObject::tr( "Scalebar" ), QgsLayoutItemScaleBar::create ) ); + addLayoutItemType( new QgsLayoutItemMetadata( LayoutShape, QObject::tr( "Shape" ), []( QgsLayout * layout ) { QgsLayoutItemShape *shape = new QgsLayoutItemShape( layout ); shape->setShapeType( QgsLayoutItemShape::Rectangle ); return shape; } ) ); - addLayoutItemType( new QgsLayoutItemMetadata( LayoutPolygon, QObject::tr( "Polygon" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddPolygon.svg" ) ), QgsLayoutItemPolygon::create ) ); - addLayoutItemType( new QgsLayoutItemMetadata( LayoutPolyline, QObject::tr( "Polyline" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddPolyline.svg" ) ), QgsLayoutItemPolyline::create ) ); + addLayoutItemType( new QgsLayoutItemMetadata( LayoutPolygon, QObject::tr( "Polygon" ), QgsLayoutItemPolygon::create ) ); + addLayoutItemType( new QgsLayoutItemMetadata( LayoutPolyline, QObject::tr( "Polyline" ), QgsLayoutItemPolyline::create ) ); - addLayoutMultiFrameType( new QgsLayoutMultiFrameMetadata( LayoutHtml, QObject::tr( "HTML" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddHtml.svg" ) ), QgsLayoutItemHtml::create ) ); - addLayoutMultiFrameType( new QgsLayoutMultiFrameMetadata( LayoutAttributeTable, QObject::tr( "Attribute Table" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddTable.svg" ) ), QgsLayoutItemAttributeTable::create ) ); - addLayoutMultiFrameType( new QgsLayoutMultiFrameMetadata( LayoutTextTable, QObject::tr( "Text Table" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddTable.svg" ) ), QgsLayoutItemTextTable::create ) ); + addLayoutMultiFrameType( new QgsLayoutMultiFrameMetadata( LayoutHtml, QObject::tr( "HTML" ), QgsLayoutItemHtml::create ) ); + addLayoutMultiFrameType( new QgsLayoutMultiFrameMetadata( LayoutAttributeTable, QObject::tr( "Attribute Table" ), QgsLayoutItemAttributeTable::create ) ); + addLayoutMultiFrameType( new QgsLayoutMultiFrameMetadata( LayoutTextTable, QObject::tr( "Text Table" ), QgsLayoutItemTextTable::create ) ); return true; } diff --git a/src/core/layout/qgslayoutitemregistry.h b/src/core/layout/qgslayoutitemregistry.h index dbacd128a11a..1d4906d75c7c 100644 --- a/src/core/layout/qgslayoutitemregistry.h +++ b/src/core/layout/qgslayoutitemregistry.h @@ -62,11 +62,6 @@ class CORE_EXPORT QgsLayoutItemAbstractMetadata */ int type() const { return mType; } - /** - * Returns an icon representing the layout item type. - */ - virtual QIcon icon() const { return QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddBasicRectangle.svg" ) ); } - /** * Returns a translated, user visible name for the layout item class. */ @@ -119,11 +114,10 @@ class CORE_EXPORT QgsLayoutItemMetadata : public QgsLayoutItemAbstractMetadata * Constructor for QgsLayoutItemMetadata with the specified class \a type * and \a visibleName, and function pointers for the various item creation functions. */ - QgsLayoutItemMetadata( int type, const QString &visibleName, const QIcon &icon, + QgsLayoutItemMetadata( int type, const QString &visibleName, QgsLayoutItemCreateFunc pfCreate, QgsLayoutItemPathResolverFunc pfPathResolver = nullptr ) : QgsLayoutItemAbstractMetadata( type, visibleName ) - , mIcon( icon ) , mCreateFunc( pfCreate ) , mPathResolverFunc( pfPathResolver ) {} @@ -138,7 +132,6 @@ class CORE_EXPORT QgsLayoutItemMetadata : public QgsLayoutItemAbstractMetadata */ QgsLayoutItemPathResolverFunc pathResolverFunction() const { return mPathResolverFunc; } - QIcon icon() const override { return mIcon.isNull() ? QgsLayoutItemAbstractMetadata::icon() : mIcon; } QgsLayoutItem *createItem( QgsLayout *layout ) override { return mCreateFunc ? mCreateFunc( layout ) : nullptr; } void resolvePaths( QVariantMap &properties, const QgsPathResolver &pathResolver, bool saving ) override @@ -148,7 +141,6 @@ class CORE_EXPORT QgsLayoutItemMetadata : public QgsLayoutItemAbstractMetadata } protected: - QIcon mIcon; QgsLayoutItemCreateFunc mCreateFunc = nullptr; QgsLayoutItemPathResolverFunc mPathResolverFunc = nullptr; @@ -243,11 +235,10 @@ class CORE_EXPORT QgsLayoutMultiFrameMetadata : public QgsLayoutMultiFrameAbstra * Constructor for QgsLayoutMultiFrameMetadata with the specified class \a type * and \a visibleName, and function pointers for the various item creation functions. */ - QgsLayoutMultiFrameMetadata( int type, const QString &visibleName, const QIcon &icon, + QgsLayoutMultiFrameMetadata( int type, const QString &visibleName, QgsLayoutMultiFrameCreateFunc pfCreate, QgsLayoutMultiFramePathResolverFunc pfPathResolver = nullptr ) : QgsLayoutMultiFrameAbstractMetadata( type, visibleName ) - , mIcon( icon ) , mCreateFunc( pfCreate ) , mPathResolverFunc( pfPathResolver ) {} @@ -262,7 +253,6 @@ class CORE_EXPORT QgsLayoutMultiFrameMetadata : public QgsLayoutMultiFrameAbstra */ QgsLayoutMultiFramePathResolverFunc pathResolverFunction() const { return mPathResolverFunc; } - QIcon icon() const override { return mIcon.isNull() ? QgsLayoutMultiFrameAbstractMetadata::icon() : mIcon; } QgsLayoutMultiFrame *createMultiFrame( QgsLayout *layout ) override { return mCreateFunc ? mCreateFunc( layout ) : nullptr; } void resolvePaths( QVariantMap &properties, const QgsPathResolver &pathResolver, bool saving ) override @@ -272,7 +262,6 @@ class CORE_EXPORT QgsLayoutMultiFrameMetadata : public QgsLayoutMultiFrameAbstra } protected: - QIcon mIcon; QgsLayoutMultiFrameCreateFunc mCreateFunc = nullptr; QgsLayoutMultiFramePathResolverFunc mPathResolverFunc = nullptr; diff --git a/tests/src/core/testqgslayoutitem.cpp b/tests/src/core/testqgslayoutitem.cpp index 481db438ec73..19ab7c1c2bc2 100644 --- a/tests/src/core/testqgslayoutitem.cpp +++ b/tests/src/core/testqgslayoutitem.cpp @@ -257,7 +257,7 @@ void TestQgsLayoutItem::registry() QSignalSpy spyTypeAdded( ®istry, &QgsLayoutItemRegistry::typeAdded ); - QgsLayoutItemMetadata *metadata = new QgsLayoutItemMetadata( 2, QStringLiteral( "my type" ), QIcon(), create, resolve ); + QgsLayoutItemMetadata *metadata = new QgsLayoutItemMetadata( 2, QStringLiteral( "my type" ), create, resolve ); QVERIFY( registry.addLayoutItemType( metadata ) ); QCOMPARE( spyTypeAdded.count(), 1 ); QCOMPARE( spyTypeAdded.value( 0 ).at( 0 ).toInt(), 2 ); diff --git a/tests/src/core/testqgslayoutmultiframe.cpp b/tests/src/core/testqgslayoutmultiframe.cpp index f77a6a782033..ac279f69b56a 100644 --- a/tests/src/core/testqgslayoutmultiframe.cpp +++ b/tests/src/core/testqgslayoutmultiframe.cpp @@ -530,7 +530,7 @@ void TestQgsLayoutMultiFrame::registry() QSignalSpy spyTypeAdded( ®istry, &QgsLayoutItemRegistry::multiFrameTypeAdded ); - QgsLayoutMultiFrameMetadata *metadata = new QgsLayoutMultiFrameMetadata( QgsLayoutItemRegistry::PluginItem + 1, QStringLiteral( "TestMultiFrame" ), QIcon(), create, resolve ); + QgsLayoutMultiFrameMetadata *metadata = new QgsLayoutMultiFrameMetadata( QgsLayoutItemRegistry::PluginItem + 1, QStringLiteral( "TestMultiFrame" ), create, resolve ); QVERIFY( registry.addLayoutMultiFrameType( metadata ) ); QCOMPARE( spyTypeAdded.count(), 1 ); QCOMPARE( spyTypeAdded.value( 0 ).at( 0 ).toInt(), QgsLayoutItemRegistry::PluginItem + 1 ); diff --git a/tests/src/gui/testqgslayoutview.cpp b/tests/src/gui/testqgslayoutview.cpp index 4958a008373d..046b7d34c094 100644 --- a/tests/src/gui/testqgslayoutview.cpp +++ b/tests/src/gui/testqgslayoutview.cpp @@ -320,7 +320,7 @@ void TestQgsLayoutView::guiRegistry() //creating item QgsLayoutItem *item = registry.createItem( uuid, nullptr ); QVERIFY( !item ); - QgsApplication::layoutItemRegistry()->addLayoutItemType( new QgsLayoutItemMetadata( QgsLayoutItemRegistry::LayoutItem + 101, QStringLiteral( "my type" ), QIcon(), []( QgsLayout * layout )->QgsLayoutItem* + QgsApplication::layoutItemRegistry()->addLayoutItemType( new QgsLayoutItemMetadata( QgsLayoutItemRegistry::LayoutItem + 101, QStringLiteral( "my type" ), []( QgsLayout * layout )->QgsLayoutItem* { return new TestItem( layout ); } ) ); From 7c086beb925aeaae08d60f95048306195790d8fc Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 23 Dec 2017 17:00:50 +1000 Subject: [PATCH 028/105] Fix some map item todos --- python/core/layout/qgslayoutcontext.sip | 1 + src/core/layout/qgslayoutatlas.cpp | 16 ++++------------ src/core/layout/qgslayoutcontext.h | 1 + src/core/layout/qgslayoutitemmap.cpp | 17 ++++++----------- tests/src/core/testqgslayoutmap.cpp | 23 +++++++++++++++++++++++ 5 files changed, 35 insertions(+), 23 deletions(-) diff --git a/python/core/layout/qgslayoutcontext.sip b/python/core/layout/qgslayoutcontext.sip index b8fe673de971..f5b9c87978dd 100644 --- a/python/core/layout/qgslayoutcontext.sip +++ b/python/core/layout/qgslayoutcontext.sip @@ -28,6 +28,7 @@ class QgsLayoutContext : QObject FlagAntialiasing, FlagUseAdvancedEffects, FlagForceVectorOutput, + FlagHideCoverageLayer, }; typedef QFlags Flags; diff --git a/src/core/layout/qgslayoutatlas.cpp b/src/core/layout/qgslayoutatlas.cpp index aeb0ef3179b4..edc4dd2be413 100644 --- a/src/core/layout/qgslayoutatlas.cpp +++ b/src/core/layout/qgslayoutatlas.cpp @@ -384,14 +384,8 @@ void QgsLayoutAtlas::setHideCoverage( bool hide ) { mHideCoverage = hide; -#if 0 //TODO - if ( mComposition->atlasMode() == QgsComposition::PreviewAtlas ) - { - //an atlas preview is enabled, so reflect changes in coverage layer visibility immediately - updateAtlasMaps(); - mComposition->update(); - } -#endif + mLayout->context().setFlag( QgsLayoutContext::FlagHideCoverageLayer, hide ); + mLayout->refresh(); } bool QgsLayoutAtlas::setFilenameExpression( const QString &pattern, QString &errorString ) @@ -410,12 +404,10 @@ QgsExpressionContext QgsLayoutAtlas::createExpressionContext() QgsExpressionContext expressionContext; expressionContext << QgsExpressionContextUtils::globalScope(); if ( mLayout ) - expressionContext << QgsExpressionContextUtils::projectScope( mLayout->project() ); -#if 0 //TODO - << QgsExpressionContextUtils::compositionScope( mLayout ); + expressionContext << QgsExpressionContextUtils::projectScope( mLayout->project() ) + << QgsExpressionContextUtils::layoutScope( mLayout ); expressionContext.appendScope( QgsExpressionContextUtils::atlasScope( this ) ); -#endif if ( mCoverageLayer ) expressionContext.lastScope()->setFields( mCoverageLayer->fields() ); diff --git a/src/core/layout/qgslayoutcontext.h b/src/core/layout/qgslayoutcontext.h index 7d5eba142849..9ec3b55c81d6 100644 --- a/src/core/layout/qgslayoutcontext.h +++ b/src/core/layout/qgslayoutcontext.h @@ -46,6 +46,7 @@ class CORE_EXPORT QgsLayoutContext : public QObject FlagAntialiasing = 1 << 3, //!< Use antialiasing when drawing items. FlagUseAdvancedEffects = 1 << 4, //!< Enable advanced effects such as blend modes. FlagForceVectorOutput = 1 << 5, //!< Force output in vector format where possible, even if items require rasterization to keep their correct appearance. + FlagHideCoverageLayer = 1 << 6, //!< Hide coverage layer in outputs }; Q_DECLARE_FLAGS( Flags, Flag ) diff --git a/src/core/layout/qgslayoutitemmap.cpp b/src/core/layout/qgslayoutitemmap.cpp index 18b3b8555e5f..5ea7d9e3d626 100644 --- a/src/core/layout/qgslayoutitemmap.cpp +++ b/src/core/layout/qgslayoutitemmap.cpp @@ -1400,22 +1400,17 @@ QList QgsLayoutItemMap::layersToRender( const QgsExpressionContex } } -#if 0 //TODO //remove atlas coverage layer if required - //TODO - move setting for hiding coverage layer to map item properties - if ( mLayout->atlasMode() != QgsComposition::AtlasOff ) + if ( mLayout->context().flags() & QgsLayoutContext::FlagHideCoverageLayer ) { - if ( mComposition->atlasComposition().hideCoverage() ) + //hiding coverage layer + int removeAt = renderLayers.indexOf( mLayout->context().layer() ); + if ( removeAt != -1 ) { - //hiding coverage layer - int removeAt = renderLayers.indexOf( mComposition->atlasComposition().coverageLayer() ); - if ( removeAt != -1 ) - { - renderLayers.removeAt( removeAt ); - } + renderLayers.removeAt( removeAt ); } } -#endif + return renderLayers; } diff --git a/tests/src/core/testqgslayoutmap.cpp b/tests/src/core/testqgslayoutmap.cpp index 4fce7944437d..1b7de1cbb6d4 100644 --- a/tests/src/core/testqgslayoutmap.cpp +++ b/tests/src/core/testqgslayoutmap.cpp @@ -55,6 +55,7 @@ class TestQgsLayoutMap : public QObject void dataDefinedLayers(); //test data defined layer string void dataDefinedStyles(); //test data defined styles void rasterized(); + void layersToRender(); private: QgsRasterLayer *mRasterLayer = nullptr; @@ -503,5 +504,27 @@ void TestQgsLayoutMap::rasterized() QVERIFY( checker.testLayout( mReport, 0, 0 ) ); } +void TestQgsLayoutMap::layersToRender() +{ + QList layers = QList() << mRasterLayer << mPolysLayer << mPointsLayer << mLinesLayer; + QList layers2 = QList() << mRasterLayer << mPolysLayer << mLinesLayer; + + QgsLayout l( QgsProject::instance() ); + + QgsLayoutItemMap *map = new QgsLayoutItemMap( &l ); + map->setLayers( layers ); + l.addLayoutItem( map ); + + QCOMPARE( map->layersToRender(), layers ); + + // hide coverage layer + l.context().setLayer( mPointsLayer ); + l.context().setFlag( QgsLayoutContext::FlagHideCoverageLayer, true ); + QCOMPARE( map->layersToRender(), layers2 ); + + l.context().setFlag( QgsLayoutContext::FlagHideCoverageLayer, false ); + QCOMPARE( map->layersToRender(), layers ); +} + QGSTEST_MAIN( TestQgsLayoutMap ) #include "testqgslayoutmap.moc" From 3ffdda3e304968cbd8e77e3921a99de8160e0e91 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 23 Dec 2017 17:35:17 +1000 Subject: [PATCH 029/105] Port predefined scale handling to layouts --- python/core/layout/qgslayoutcontext.sip | 15 ++++++++ python/core/layout/qgslayoutitemmap.sip | 1 - src/app/layout/qgslayoutdesignerdialog.cpp | 42 +++++++++++++++------- src/app/layout/qgslayoutdesignerdialog.h | 3 +- src/core/layout/qgslayoutcontext.cpp | 7 ++++ src/core/layout/qgslayoutcontext.h | 16 +++++++++ tests/src/core/testqgslayoutcontext.cpp | 14 ++++++++ 7 files changed, 84 insertions(+), 14 deletions(-) diff --git a/python/core/layout/qgslayoutcontext.sip b/python/core/layout/qgslayoutcontext.sip index f5b9c87978dd..bf5e23e3bf04 100644 --- a/python/core/layout/qgslayoutcontext.sip +++ b/python/core/layout/qgslayoutcontext.sip @@ -233,6 +233,21 @@ and customise their rendering based on the layer. If ``layer`` is -1, all item layers should be rendered. .. seealso:: :py:func:`setCurrentExportLayer()` +%End + + void setPredefinedScales( const QVector &scales ); +%Docstring +Sets the list of predefined ``scales`` to use with the layout. This is used +for maps which are set to the predefined atlas scaling mode. + +.. seealso:: :py:func:`predefinedScales()` +%End + + QVector predefinedScales() const; +%Docstring +Returns the current list of predefined scales for use with the layout. + +.. seealso:: :py:func:`setPredefinedScales()` %End signals: diff --git a/python/core/layout/qgslayoutitemmap.sip b/python/core/layout/qgslayoutitemmap.sip index 3699fbeb5c1a..56fc821697b2 100644 --- a/python/core/layout/qgslayoutitemmap.sip +++ b/python/core/layout/qgslayoutitemmap.sip @@ -480,7 +480,6 @@ Return map settings that will be used for drawing of the map. True if a draw is already in progress %End - virtual QRectF boundingRect() const; diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 25121134987a..d29b0d854135 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -1919,9 +1919,7 @@ void QgsLayoutDesignerDialog::atlasPreviewTriggered( bool checked ) if ( checked ) { -#if 0 //TODO loadAtlasPredefinedScalesFromProject(); -#endif } if ( checked ) @@ -1985,9 +1983,7 @@ void QgsLayoutDesignerDialog::atlasPageComboEditingFinished() else if ( page != atlas->currentFeatureNumber() + 1 ) { QgisApp::instance()->mapCanvas()->stopRendering(); -#if 0 //TODO loadAtlasPredefinedScalesFromProject(); -#endif atlas->seekTo( page - 1 ); #if 0 //TODO emit atlasPreviewFeatureChanged(); @@ -2003,9 +1999,7 @@ void QgsLayoutDesignerDialog::atlasNext() QgisApp::instance()->mapCanvas()->stopRendering(); -#if 0 //TODO loadAtlasPredefinedScalesFromProject(); -#endif if ( printAtlas->next() ) { #if 0 //TODO @@ -2022,9 +2016,7 @@ void QgsLayoutDesignerDialog::atlasPrevious() QgisApp::instance()->mapCanvas()->stopRendering(); -#if 0 //TODO loadAtlasPredefinedScalesFromProject(); -#endif if ( printAtlas->previous() ) { #if 0 //TODO @@ -2041,9 +2033,7 @@ void QgsLayoutDesignerDialog::atlasFirst() QgisApp::instance()->mapCanvas()->stopRendering(); -#if 0 //TODO loadAtlasPredefinedScalesFromProject(); -#endif if ( printAtlas->first() ) { #if 0 //TODO @@ -2060,9 +2050,7 @@ void QgsLayoutDesignerDialog::atlasLast() QgisApp::instance()->mapCanvas()->stopRendering(); -#if 0 //TODO loadAtlasPredefinedScalesFromProject(); -#endif if ( printAtlas->last() ) { #if 0 //TODO @@ -2073,21 +2061,26 @@ void QgsLayoutDesignerDialog::atlasLast() void QgsLayoutDesignerDialog::printAtlas() { + loadAtlasPredefinedScalesFromProject(); //TODO } void QgsLayoutDesignerDialog::exportAtlasToRaster() { + loadAtlasPredefinedScalesFromProject(); //TODO + } void QgsLayoutDesignerDialog::exportAtlasToSvg() { + loadAtlasPredefinedScalesFromProject(); //TODO } void QgsLayoutDesignerDialog::exportAtlasToPdf() { + loadAtlasPredefinedScalesFromProject(); //TODO } @@ -2435,6 +2428,31 @@ void QgsLayoutDesignerDialog::atlasFeatureChanged( const QgsFeature &feature ) mapCanvas->expressionContextScope().addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_geometry" ), QVariant::fromValue( feature.geometry() ), true ) ); } +void QgsLayoutDesignerDialog::loadAtlasPredefinedScalesFromProject() +{ + QVector projectScales; + // first look at project's scales + QStringList scales( mLayout->project()->readListEntry( QStringLiteral( "Scales" ), QStringLiteral( "/ScalesList" ) ) ); + bool hasProjectScales( mLayout->project()->readBoolEntry( QStringLiteral( "Scales" ), QStringLiteral( "/useProjectScales" ) ) ); + if ( !hasProjectScales || scales.isEmpty() ) + { + // default to global map tool scales + QgsSettings settings; + QString scalesStr( settings.value( QStringLiteral( "Map/scales" ), PROJECT_SCALES ).toString() ); + scales = scalesStr.split( ',' ); + } + + for ( auto scaleIt = scales.constBegin(); scaleIt != scales.constEnd(); ++scaleIt ) + { + QStringList parts( scaleIt->split( ':' ) ); + if ( parts.size() == 2 ) + { + projectScales.push_back( parts[1].toDouble() ); + } + } + mLayout->context().setPredefinedScales( projectScales ); +} + QgsLayoutAtlas *QgsLayoutDesignerDialog::atlas() { QgsPrintLayout *layout = qobject_cast< QgsPrintLayout *>( mLayout ); diff --git a/src/app/layout/qgslayoutdesignerdialog.h b/src/app/layout/qgslayoutdesignerdialog.h index ed9d6536edea..a599775525ce 100644 --- a/src/app/layout/qgslayoutdesignerdialog.h +++ b/src/app/layout/qgslayoutdesignerdialog.h @@ -423,7 +423,8 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner void atlasFeatureChanged( const QgsFeature &feature ); - + //! Load predefined scales from the project's properties + void loadAtlasPredefinedScalesFromProject(); QgsLayoutAtlas *atlas(); }; diff --git a/src/core/layout/qgslayoutcontext.cpp b/src/core/layout/qgslayoutcontext.cpp index fd74e234090b..c3131c7e7422 100644 --- a/src/core/layout/qgslayoutcontext.cpp +++ b/src/core/layout/qgslayoutcontext.cpp @@ -160,3 +160,10 @@ void QgsLayoutContext::setPagesVisible( bool visible ) { mPagesVisible = visible; } + +void QgsLayoutContext::setPredefinedScales( const QVector &scales ) +{ + mPredefinedScales = scales; + // make sure the list is sorted + std::sort( mPredefinedScales.begin(), mPredefinedScales.end() ); +} diff --git a/src/core/layout/qgslayoutcontext.h b/src/core/layout/qgslayoutcontext.h index 9ec3b55c81d6..dcdda75caf1a 100644 --- a/src/core/layout/qgslayoutcontext.h +++ b/src/core/layout/qgslayoutcontext.h @@ -233,6 +233,19 @@ class CORE_EXPORT QgsLayoutContext : public QObject */ int currentExportLayer() const { return mCurrentExportLayer; } + /** + * Sets the list of predefined \a scales to use with the layout. This is used + * for maps which are set to the predefined atlas scaling mode. + * \see predefinedScales() + */ + void setPredefinedScales( const QVector &scales ); + + /** + * Returns the current list of predefined scales for use with the layout. + * \see setPredefinedScales() + */ + QVector predefinedScales() const { return mPredefinedScales; } + signals: /** @@ -278,6 +291,9 @@ class CORE_EXPORT QgsLayoutContext : public QObject // projected geometry cache mutable QMap mGeometryCache; + //list of predefined scales + QVector mPredefinedScales; + friend class QgsLayoutExporter; friend class TestQgsLayout; friend class LayoutContextPreviewSettingRestorer; diff --git a/tests/src/core/testqgslayoutcontext.cpp b/tests/src/core/testqgslayoutcontext.cpp index c6699945c246..4f335bce3c77 100644 --- a/tests/src/core/testqgslayoutcontext.cpp +++ b/tests/src/core/testqgslayoutcontext.cpp @@ -1,3 +1,4 @@ + /*************************************************************************** testqgslayoutcontext.cpp ------------------------ @@ -44,6 +45,7 @@ class TestQgsLayoutContext: public QObject void boundingBoxes(); void exportLayer(); void geometry(); + void scales(); private: QString mReport; @@ -242,5 +244,17 @@ void TestQgsLayoutContext::geometry() delete layer; } +void TestQgsLayoutContext::scales() +{ + QVector< qreal > scales; + scales << 1 << 15 << 5 << 10; + + QgsLayoutContext context( nullptr ); + context.setPredefinedScales( scales ); + + // should be sorted + QCOMPARE( context.predefinedScales(), QVector< qreal >() << 1 << 5 << 10 << 15 ); +} + QGSTEST_MAIN( TestQgsLayoutContext ) #include "testqgslayoutcontext.moc" From 69ddc32d0f18667f461d5f3129928df3ccb1e5f1 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 23 Dec 2017 17:50:25 +1000 Subject: [PATCH 030/105] Restore atlas map handling --- python/core/layout/qgslayoutitemmap.sip | 3 + src/app/layout/qgslayoutmapwidget.cpp | 49 ++----- src/core/layout/qgslayoutitemmap.cpp | 162 ++++++++++++++++++++++++ src/core/layout/qgslayoutitemmap.h | 15 +-- tests/src/core/testqgslayoutcontext.cpp | 1 - 5 files changed, 183 insertions(+), 47 deletions(-) diff --git a/python/core/layout/qgslayoutitemmap.sip b/python/core/layout/qgslayoutitemmap.sip index 56fc821697b2..9911374a9664 100644 --- a/python/core/layout/qgslayoutitemmap.sip +++ b/python/core/layout/qgslayoutitemmap.sip @@ -531,6 +531,9 @@ associated legend items know they should update public slots: + virtual void refresh(); + + virtual void invalidateCache(); diff --git a/src/app/layout/qgslayoutmapwidget.cpp b/src/app/layout/qgslayoutmapwidget.cpp index 80a8bfbfe498..35a1c3c514a8 100644 --- a/src/app/layout/qgslayoutmapwidget.cpp +++ b/src/app/layout/qgslayoutmapwidget.cpp @@ -29,6 +29,7 @@ #include "qgslayoutmapgridwidget.h" #include "qgsstyle.h" #include "qgslayoutundostack.h" +#include "qgslayoutatlas.h" #include #include @@ -111,17 +112,14 @@ QgsLayoutMapWidget::QgsLayoutMapWidget( QgsLayoutItemMap *item ) connect( item, &QgsLayoutObject::changed, this, &QgsLayoutMapWidget::updateGuiElements ); -#if 0 //TODO - QgsAtlasComposition *atlas = atlasComposition(); - if ( atlas ) + connect( &item->layout()->context(), &QgsLayoutContext::layerChanged, + this, &QgsLayoutMapWidget::atlasLayerChanged ); + if ( QgsLayoutAtlas *atlas = layoutAtlas() ) { - connect( atlas, &QgsAtlasComposition::coverageLayerChanged, - this, &QgsLayoutMapWidget::atlasLayerChanged ); - connect( atlas, &QgsAtlasComposition::toggled, this, &QgsLayoutMapWidget::compositionAtlasToggled ); - + connect( atlas, &QgsLayoutAtlas::toggled, this, &QgsLayoutMapWidget::compositionAtlasToggled ); compositionAtlasToggled( atlas->enabled() ); } -#endif + mOverviewFrameMapComboBox->setCurrentLayout( item->layout() ); mOverviewFrameMapComboBox->setItemType( QgsLayoutItemRegistry::LayoutMap ); mOverviewFrameStyleButton->registerExpressionContextGenerator( item ); @@ -196,11 +194,9 @@ void QgsLayoutMapWidget::populateDataDefinedButtons() void QgsLayoutMapWidget::compositionAtlasToggled( bool atlasEnabled ) { - Q_UNUSED( atlasEnabled ); -#if 0 //TODO if ( atlasEnabled && - mMapItem && mMapItem->composition() && mMapItem->composition()->atlasComposition().coverageLayer() - && mMapItem->composition()->atlasComposition().coverageLayer()->wkbType() != QgsWkbTypes::NoGeometry ) + mMapItem && mMapItem->layout() && mMapItem->layout()->context().layer() + && mMapItem->layout()->context().layer()->wkbType() != QgsWkbTypes::NoGeometry ) { mAtlasCheckBox->setEnabled( true ); } @@ -209,7 +205,6 @@ void QgsLayoutMapWidget::compositionAtlasToggled( bool atlasEnabled ) mAtlasCheckBox->setEnabled( false ); mAtlasCheckBox->setChecked( false ); } -#endif } void QgsLayoutMapWidget::aboutToShowKeepLayersVisibilityPresetsMenu() @@ -387,31 +382,16 @@ void QgsLayoutMapWidget::mAtlasCheckBox_toggled( bool checked ) void QgsLayoutMapWidget::updateMapForAtlas() { -#if 0 //TODO //update map if in atlas preview mode - QgsComposition *composition = mMapItem->composition(); - if ( !composition ) - { - return; - } - if ( composition->atlasMode() == QgsComposition::AtlasOff ) - { - return; - } - if ( mMapItem->atlasDriven() ) { - //update atlas based extent for map - QgsAtlasComposition *atlas = &composition->atlasComposition(); - //prepareMap causes a redraw - atlas->prepareMap( mMapItem ); + mMapItem->refresh(); } else { //redraw map mMapItem->invalidateCache(); } -#endif } void QgsLayoutMapWidget::mAtlasMarginRadio_toggled( bool checked ) @@ -708,15 +688,14 @@ void QgsLayoutMapWidget::toggleAtlasScalingOptionsByLayerType() return; } -#if 0 //TODO //get atlas coverage layer - QgsVectorLayer *coverageLayer = atlasCoverageLayer(); - if ( !coverageLayer ) + QgsVectorLayer *layer = coverageLayer(); + if ( !layer ) { return; } - switch ( coverageLayer->wkbType() ) + switch ( layer->wkbType() ) { case QgsWkbTypes::Point: case QgsWkbTypes::Point25D: @@ -732,7 +711,6 @@ void QgsLayoutMapWidget::toggleAtlasScalingOptionsByLayerType() mAtlasMarginRadio->setEnabled( true ); mAtlasPredefinedScaleRadio->setEnabled( true ); } -#endif } void QgsLayoutMapWidget::updateComposerExtentFromGui() @@ -1066,8 +1044,6 @@ void QgsLayoutMapWidget::initAnnotationDirectionBox( QComboBox *c, QgsLayoutItem void QgsLayoutMapWidget::atlasLayerChanged( QgsVectorLayer *layer ) { - Q_UNUSED( layer ); -#if 0 //TODO if ( !layer || layer->wkbType() == QgsWkbTypes::NoGeometry ) { //geometryless layer, disable atlas control @@ -1083,7 +1059,6 @@ void QgsLayoutMapWidget::atlasLayerChanged( QgsVectorLayer *layer ) // enable or disable fixed scale control based on layer type if ( mAtlasCheckBox->isChecked() ) toggleAtlasScalingOptionsByLayerType(); -#endif } bool QgsLayoutMapWidget::hasPredefinedScales() const diff --git a/src/core/layout/qgslayoutitemmap.cpp b/src/core/layout/qgslayoutitemmap.cpp index 5ea7d9e3d626..9f7e5ddfb578 100644 --- a/src/core/layout/qgslayoutitemmap.cpp +++ b/src/core/layout/qgslayoutitemmap.cpp @@ -123,6 +123,14 @@ QgsLayoutItemMap *QgsLayoutItemMap::create( QgsLayout *layout ) return new QgsLayoutItemMap( layout ); } +void QgsLayoutItemMap::refresh() +{ + QgsLayoutItem::refresh(); + invalidateCache(); + + updateAtlasFeature(); +} + double QgsLayoutItemMap::scale() const { QgsScaleCalculator calculator; @@ -1799,3 +1807,157 @@ void QgsLayoutItemMap::refreshMapExtents( const QgsExpressionContext *context ) emit mapRotationChanged( mapRotation ); } } + +void QgsLayoutItemMap::updateAtlasFeature() +{ + if ( !atlasDriven() || !mLayout->context().layer() ) + return; // nothing to do + + QgsRectangle bounds = computeAtlasRectangle(); + if ( bounds.isNull() ) + return; + + double xa1 = bounds.xMinimum(); + double xa2 = bounds.xMaximum(); + double ya1 = bounds.yMinimum(); + double ya2 = bounds.yMaximum(); + QgsRectangle newExtent = bounds; + QgsRectangle originalExtent = mExtent; + + //sanity check - only allow fixed scale mode for point layers + bool isPointLayer = false; + switch ( mLayout->context().layer()->wkbType() ) + { + case QgsWkbTypes::Point: + case QgsWkbTypes::Point25D: + case QgsWkbTypes::MultiPoint: + case QgsWkbTypes::MultiPoint25D: + isPointLayer = true; + break; + default: + isPointLayer = false; + break; + } + + if ( mAtlasScalingMode == Fixed || mAtlasScalingMode == Predefined || isPointLayer ) + { + QgsScaleCalculator calc; + calc.setMapUnits( crs().mapUnits() ); + calc.setDpi( 25.4 ); + double originalScale = calc.calculate( originalExtent, rect().width() ); + double geomCenterX = ( xa1 + xa2 ) / 2.0; + double geomCenterY = ( ya1 + ya2 ) / 2.0; + + if ( mAtlasScalingMode == Fixed || isPointLayer ) + { + // only translate, keep the original scale (i.e. width x height) + double xMin = geomCenterX - originalExtent.width() / 2.0; + double yMin = geomCenterY - originalExtent.height() / 2.0; + newExtent = QgsRectangle( xMin, + yMin, + xMin + originalExtent.width(), + yMin + originalExtent.height() ); + + //scale newExtent to match original scale of map + //this is required for geographic coordinate systems, where the scale varies by extent + double newScale = calc.calculate( newExtent, rect().width() ); + newExtent.scale( originalScale / newScale ); + } + else if ( mAtlasScalingMode == Predefined ) + { + // choose one of the predefined scales + double newWidth = originalExtent.width(); + double newHeight = originalExtent.height(); + QVector scales = mLayout->context().predefinedScales(); + for ( int i = 0; i < scales.size(); i++ ) + { + double ratio = scales[i] / originalScale; + newWidth = originalExtent.width() * ratio; + newHeight = originalExtent.height() * ratio; + + // compute new extent, centered on feature + double xMin = geomCenterX - newWidth / 2.0; + double yMin = geomCenterY - newHeight / 2.0; + newExtent = QgsRectangle( xMin, + yMin, + xMin + newWidth, + yMin + newHeight ); + + //scale newExtent to match desired map scale + //this is required for geographic coordinate systems, where the scale varies by extent + double newScale = calc.calculate( newExtent, rect().width() ); + newExtent.scale( scales[i] / newScale ); + + if ( ( newExtent.width() >= bounds.width() ) && ( newExtent.height() >= bounds.height() ) ) + { + // this is the smallest extent that embeds the feature, stop here + break; + } + } + } + } + else if ( mAtlasScalingMode == Auto ) + { + // auto scale + + double geomRatio = bounds.width() / bounds.height(); + double mapRatio = originalExtent.width() / originalExtent.height(); + + // geometry height is too big + if ( geomRatio < mapRatio ) + { + // extent the bbox's width + double adjWidth = ( mapRatio * bounds.height() - bounds.width() ) / 2.0; + xa1 -= adjWidth; + xa2 += adjWidth; + } + // geometry width is too big + else if ( geomRatio > mapRatio ) + { + // extent the bbox's height + double adjHeight = ( bounds.width() / mapRatio - bounds.height() ) / 2.0; + ya1 -= adjHeight; + ya2 += adjHeight; + } + newExtent = QgsRectangle( xa1, ya1, xa2, ya2 ); + + if ( mAtlasMargin > 0.0 ) + { + newExtent.scale( 1 + mAtlasMargin ); + } + } + + // set the new extent (and render) + setExtent( newExtent ); +} + +QgsRectangle QgsLayoutItemMap::computeAtlasRectangle() +{ + // QgsGeometry::boundingBox is expressed in the geometry"s native CRS + // We have to transform the geometry to the destination CRS and ask for the bounding box + // Note: we cannot directly take the transformation of the bounding box, since transformations are not linear + QgsGeometry g = mLayout->context().currentGeometry( crs() ); + // Rotating the geometry, so the bounding box is correct wrt map rotation + if ( mEvaluatedMapRotation != 0.0 ) + { + QgsPointXY prevCenter = g.boundingBox().center(); + g.rotate( mEvaluatedMapRotation, g.boundingBox().center() ); + // Rotation center will be still the bounding box center of an unrotated geometry. + // Which means, if the center of bbox moves after rotation, the viewport will + // also be offset, and part of the geometry will fall out of bounds. + // Here we compensate for that roughly: by extending the rotated bounds + // so that its center is the same as the original. + QgsRectangle bounds = g.boundingBox(); + double dx = std::max( std::abs( prevCenter.x() - bounds.xMinimum() ), + std::abs( prevCenter.x() - bounds.xMaximum() ) ); + double dy = std::max( std::abs( prevCenter.y() - bounds.yMinimum() ), + std::abs( prevCenter.y() - bounds.yMaximum() ) ); + QgsPointXY center = g.boundingBox().center(); + return QgsRectangle( center.x() - dx, center.y() - dy, + center.x() + dx, center.y() + dy ); + } + else + { + return g.boundingBox(); + } +} diff --git a/src/core/layout/qgslayoutitemmap.h b/src/core/layout/qgslayoutitemmap.h index c9ab41926598..dcd01347a484 100644 --- a/src/core/layout/qgslayoutitemmap.h +++ b/src/core/layout/qgslayoutitemmap.h @@ -422,15 +422,6 @@ class CORE_EXPORT QgsLayoutItemMap : public QgsLayoutItem //! True if a draw is already in progress bool isDrawing() const {return mDrawing;} -#if 0 //TODO - - /** - * Sets new Extent for the current atlas preview and changes width, height (and implicitly also scale). - Atlas preview extents are only temporary, and are regenerated whenever the atlas feature changes - */ - void setNewAtlasFeatureExtent( const QgsRectangle &extent ); -#endif - // In case of annotations, the bounding rectangle can be larger than the map item rectangle QRectF boundingRect() const override; @@ -472,6 +463,8 @@ class CORE_EXPORT QgsLayoutItemMap : public QgsLayoutItem public slots: + void refresh() override; + void invalidateCache() override; //! Updates the bounding rect of this item. Call this function before doing any changes related to annotation out of the map rectangle @@ -656,6 +649,10 @@ class CORE_EXPORT QgsLayoutItemMap : public QgsLayoutItem */ void refreshMapExtents( const QgsExpressionContext *context = nullptr ); + void updateAtlasFeature(); + + QgsRectangle computeAtlasRectangle(); + friend class QgsLayoutItemMapGrid; friend class QgsLayoutItemMapOverview; friend class QgsLayoutItemLegend; diff --git a/tests/src/core/testqgslayoutcontext.cpp b/tests/src/core/testqgslayoutcontext.cpp index 4f335bce3c77..807b78702f6c 100644 --- a/tests/src/core/testqgslayoutcontext.cpp +++ b/tests/src/core/testqgslayoutcontext.cpp @@ -1,4 +1,3 @@ - /*************************************************************************** testqgslayoutcontext.cpp ------------------------ From e312d02c2bcba989b9e2844a7e1cdf0796b12d6f Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 23 Dec 2017 17:51:47 +1000 Subject: [PATCH 031/105] Tighten QgsAbstractLayoutIterator interface --- .../core/layout/qgsabstractlayoutiterator.sip | 38 ------------------- python/core/layout/qgslayoutatlas.sip | 34 +++++++++++++++-- src/core/layout/qgsabstractlayoutiterator.h | 26 ------------- src/core/layout/qgslayoutatlas.h | 28 ++++++++++++-- 4 files changed, 56 insertions(+), 70 deletions(-) diff --git a/python/core/layout/qgsabstractlayoutiterator.sip b/python/core/layout/qgsabstractlayoutiterator.sip index a39b121c9bf9..c98eebbffd51 100644 --- a/python/core/layout/qgsabstractlayoutiterator.sip +++ b/python/core/layout/qgsabstractlayoutiterator.sip @@ -49,46 +49,8 @@ Returns the number of features to iterate over. virtual bool next() = 0; %Docstring Iterates to next feature, returning false if no more features exist to iterate over. - -.. seealso:: :py:func:`previous()` - -.. seealso:: :py:func:`last()` - -.. seealso:: :py:func:`first()` -%End - - virtual bool previous() = 0; -%Docstring -Iterates to the previous feature, returning false if no previous feature exists. - -.. seealso:: :py:func:`next()` - -.. seealso:: :py:func:`last()` - -.. seealso:: :py:func:`first()` -%End - - virtual bool last() = 0; -%Docstring -Seeks to the last feature, returning false if no feature was found. - -.. seealso:: :py:func:`next()` - -.. seealso:: :py:func:`previous()` - -.. seealso:: :py:func:`first()` %End - virtual bool first() = 0; -%Docstring -Seeks to the first feature, returning false if no feature was found. - -.. seealso:: :py:func:`next()` - -.. seealso:: :py:func:`previous()` - -.. seealso:: :py:func:`last()` -%End }; diff --git a/python/core/layout/qgslayoutatlas.sip b/python/core/layout/qgslayoutatlas.sip index 085611b42eb8..19686332b580 100644 --- a/python/core/layout/qgslayoutatlas.sip +++ b/python/core/layout/qgslayoutatlas.sip @@ -280,11 +280,39 @@ Returns the current feature number, where a value of 0 corresponds to the first virtual bool next(); - virtual bool previous(); - virtual bool first(); + bool previous(); +%Docstring +Iterates to the previous feature, returning false if no previous feature exists. + +.. seealso:: :py:func:`next()` + +.. seealso:: :py:func:`last()` + +.. seealso:: :py:func:`first()` +%End + + bool last(); +%Docstring +Seeks to the last feature, returning false if no feature was found. + +.. seealso:: :py:func:`next()` - virtual bool last(); +.. seealso:: :py:func:`previous()` + +.. seealso:: :py:func:`first()` +%End + + bool first(); +%Docstring +Seeks to the first feature, returning false if no feature was found. + +.. seealso:: :py:func:`next()` + +.. seealso:: :py:func:`previous()` + +.. seealso:: :py:func:`last()` +%End bool seekTo( int feature ); diff --git a/src/core/layout/qgsabstractlayoutiterator.h b/src/core/layout/qgsabstractlayoutiterator.h index aa0899c26e94..b17de40d31be 100644 --- a/src/core/layout/qgsabstractlayoutiterator.h +++ b/src/core/layout/qgsabstractlayoutiterator.h @@ -45,35 +45,9 @@ class CORE_EXPORT QgsAbstractLayoutIterator /** * Iterates to next feature, returning false if no more features exist to iterate over. - * \see previous() - * \see last() - * \see first() */ virtual bool next() = 0; - /** - * Iterates to the previous feature, returning false if no previous feature exists. - * \see next() - * \see last() - * \see first() - */ - virtual bool previous() = 0; - - /** - * Seeks to the last feature, returning false if no feature was found. - * \see next() - * \see previous() - * \see first() - */ - virtual bool last() = 0; - - /** - * Seeks to the first feature, returning false if no feature was found. - * \see next() - * \see previous() - * \see last() - */ - virtual bool first() = 0; }; #endif //QGSABSTRACTLAYOUTITERATOR_H diff --git a/src/core/layout/qgslayoutatlas.h b/src/core/layout/qgslayoutatlas.h index 3681e7fca37a..00d9a71d340d 100644 --- a/src/core/layout/qgslayoutatlas.h +++ b/src/core/layout/qgslayoutatlas.h @@ -251,9 +251,31 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject, public QgsAbstractLayoutItera public slots: bool next() override; - bool previous() override; - bool first() override; - bool last() override; + + /** + * Iterates to the previous feature, returning false if no previous feature exists. + * \see next() + * \see last() + * \see first() + */ + bool previous(); + + /** + * Seeks to the last feature, returning false if no feature was found. + * \see next() + * \see previous() + * \see first() + */ + bool last(); + + /** + * Seeks to the first feature, returning false if no feature was found. + * \see next() + * \see previous() + * \see last() + */ + bool first(); + bool seekTo( int feature ); /** From b6f1425828b8176dcb277e50c2641534389894e1 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 23 Dec 2017 19:09:05 +1000 Subject: [PATCH 032/105] Flesh out QgsAbstractLayoutIterator interface --- .../core/layout/qgsabstractlayoutiterator.sip | 11 ++++ python/core/layout/qgslayoutatlas.sip | 2 + src/core/layout/qgsabstractlayoutiterator.h | 13 ++++ src/core/layout/qgslayoutatlas.cpp | 66 +++---------------- src/core/layout/qgslayoutatlas.h | 4 +- tests/src/python/test_qgslayoutatlas.py | 7 ++ 6 files changed, 44 insertions(+), 59 deletions(-) diff --git a/python/core/layout/qgsabstractlayoutiterator.sip b/python/core/layout/qgsabstractlayoutiterator.sip index c98eebbffd51..a3f2e6e3f22f 100644 --- a/python/core/layout/qgsabstractlayoutiterator.sip +++ b/python/core/layout/qgsabstractlayoutiterator.sip @@ -28,6 +28,11 @@ the Free Software Foundation; either version 2 of the License, or * virtual ~QgsAbstractLayoutIterator(); + virtual QgsLayout *layout() = 0; +%Docstring +Returns the layout associated with the iterator. +%End + virtual bool beginRender() = 0; %Docstring Called when rendering begins, before iteration commences. Returns true if successful, false if no iteration @@ -49,6 +54,12 @@ Returns the number of features to iterate over. virtual bool next() = 0; %Docstring Iterates to next feature, returning false if no more features exist to iterate over. +%End + + virtual QString filePath( const QString &baseFilePath, const QString &extension ) = 0; +%Docstring +Returns the file path for the current feature, based on a +specified base file path and extension. %End }; diff --git a/python/core/layout/qgslayoutatlas.sip b/python/core/layout/qgslayoutatlas.sip index 19686332b580..a5394840935e 100644 --- a/python/core/layout/qgslayoutatlas.sip +++ b/python/core/layout/qgslayoutatlas.sip @@ -270,6 +270,8 @@ number of matching features. virtual int count() const; + virtual QString filePath( const QString &baseFilePath, const QString &extension ); + int currentFeatureNumber() const; %Docstring diff --git a/src/core/layout/qgsabstractlayoutiterator.h b/src/core/layout/qgsabstractlayoutiterator.h index b17de40d31be..34d7d51de349 100644 --- a/src/core/layout/qgsabstractlayoutiterator.h +++ b/src/core/layout/qgsabstractlayoutiterator.h @@ -17,7 +17,9 @@ #define QGSABSTRACTLAYOUTITERATOR_H #include "qgis_core.h" +#include +class QgsLayout; class CORE_EXPORT QgsAbstractLayoutIterator { @@ -26,6 +28,11 @@ class CORE_EXPORT QgsAbstractLayoutIterator virtual ~QgsAbstractLayoutIterator() = default; + /** + * Returns the layout associated with the iterator. + */ + virtual QgsLayout *layout() = 0; + /** * Called when rendering begins, before iteration commences. Returns true if successful, false if no iteration * is available or required. @@ -48,6 +55,12 @@ class CORE_EXPORT QgsAbstractLayoutIterator */ virtual bool next() = 0; + /** + * Returns the file path for the current feature, based on a + * specified base file path and extension. + */ + virtual QString filePath( const QString &baseFilePath, const QString &extension ) = 0; + }; #endif //QGSABSTRACTLAYOUTITERATOR_H diff --git a/src/core/layout/qgslayoutatlas.cpp b/src/core/layout/qgslayoutatlas.cpp index edc4dd2be413..08b88b48c9fa 100644 --- a/src/core/layout/qgslayoutatlas.cpp +++ b/src/core/layout/qgslayoutatlas.cpp @@ -338,6 +338,15 @@ int QgsLayoutAtlas::count() const return mFeatureIds.size(); } +QString QgsLayoutAtlas::filePath( const QString &baseFilePath, const QString &extension ) +{ + QString base = QDir( baseFilePath ).filePath( mCurrentFilename ); + if ( !extension.startsWith( '.' ) ) + base += '.'; + base += extension; + return base; +} + bool QgsLayoutAtlas::next() { int newFeatureNo = mCurrentFeatureNo + 1; @@ -497,8 +506,6 @@ bool QgsLayoutAtlas::prepareForFeature( const int featureI ) return false; } - mGeometryCache.clear(); - mLayout->context().blockSignals( true ); // setFeature emits changed, we don't want 2 signals mLayout->context().setLayer( mCoverageLayer.get() ); mLayout->context().blockSignals( false ); @@ -507,59 +514,6 @@ bool QgsLayoutAtlas::prepareForFeature( const int featureI ) emit featureChanged( mCurrentFeature ); emit messagePushed( QString( tr( "Atlas feature %1 of %2" ) ).arg( featureI + 1 ).arg( mFeatureIds.size() ) ); - if ( !mCurrentFeature.isValid() ) - { - //bad feature - return false; - } - -#if 0 //TODO - move to map - //update composer maps - - //build a list of atlas-enabled composer maps - QList maps; - QList atlasMaps; - mComposition->composerItems( maps ); - if ( maps.isEmpty() ) - { - return true; - } - for ( QList::iterator mit = maps.begin(); mit != maps.end(); ++mit ) - { - QgsComposerMap *currentMap = ( *mit ); - if ( !currentMap->atlasDriven() ) - { - continue; - } - atlasMaps << currentMap; - } - - if ( !atlasMaps.isEmpty() ) - { - //clear the transformed bounds of the previous feature - mTransformedFeatureBounds = QgsRectangle(); - - // compute extent of current feature in the map CRS. This should be set on a per-atlas map basis, - // but given that it's not currently possible to have maps with different CRSes we can just - // calculate it once based on the first atlas maps' CRS. - computeExtent( atlasMaps[0] ); - } - - for ( QList::iterator mit = maps.begin(); mit != maps.end(); ++mit ) - { - if ( ( *mit )->atlasDriven() ) - { - // map is atlas driven, so update it's bounds (causes a redraw) - prepareMap( *mit ); - } - else - { - // map is not atlas driven, so manually force a redraw (to reflect possibly atlas - // dependent symbology) - ( *mit )->invalidateCache(); - } - } -#endif - return true; + return mCurrentFeature.isValid(); } diff --git a/src/core/layout/qgslayoutatlas.h b/src/core/layout/qgslayoutatlas.h index 00d9a71d340d..a9ad19a3752b 100644 --- a/src/core/layout/qgslayoutatlas.h +++ b/src/core/layout/qgslayoutatlas.h @@ -242,6 +242,7 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject, public QgsAbstractLayoutItera bool beginRender() override; bool endRender() override; int count() const override; + QString filePath( const QString &baseFilePath, const QString &extension ) override; /** * Returns the current feature number, where a value of 0 corresponds to the first feature. @@ -367,9 +368,6 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject, public QgsAbstractLayoutItera int mCurrentFeatureNo = 0; QgsFeature mCurrentFeature; - // projected geometry cache - mutable QMap mGeometryCache; - QgsExpressionContext createExpressionContext(); friend class AtlasFeatureSorter; diff --git a/tests/src/python/test_qgslayoutatlas.py b/tests/src/python/test_qgslayoutatlas.py index 12cbfdabf227..ec29fb28df74 100644 --- a/tests/src/python/test_qgslayoutatlas.py +++ b/tests/src/python/test_qgslayoutatlas.py @@ -198,12 +198,19 @@ def testFileName(self): self.assertEqual(atlas.count(), 4) atlas.first() self.assertEqual(atlas.currentFilename(), 'output_Basse-Normandie') + self.assertEqual(atlas.filePath('/tmp/output', 'png'), '/tmp/output/output_Basse-Normandie.png') + self.assertEqual(atlas.filePath('/tmp/output', '.png'), '/tmp/output/output_Basse-Normandie.png') + self.assertEqual(atlas.filePath('/tmp/output/', 'svg'), '/tmp/output/output_Basse-Normandie.svg') + atlas.next() self.assertEqual(atlas.currentFilename(), 'output_Bretagne') + self.assertEqual(atlas.filePath('/tmp/output', 'png'), '/tmp/output/output_Bretagne.png') atlas.next() self.assertEqual(atlas.currentFilename(), 'output_Pays de la Loire') + self.assertEqual(atlas.filePath('/tmp/output', 'png'), '/tmp/output/output_Pays de la Loire.png') atlas.next() self.assertEqual(atlas.currentFilename(), 'output_Centre') + self.assertEqual(atlas.filePath('/tmp/output', 'png'), '/tmp/output/output_Centre.png') # try changing expression, filename should be updated instantly atlas.setFilenameExpression("'export_' || \"NAME_1\"") From d81bf5d95ab5655741b645b81583a28c26804e1e Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 23 Dec 2017 20:06:51 +1000 Subject: [PATCH 033/105] Add api to export layout atlas as images --- python/core/layout/qgslayoutexporter.sip | 18 +++++++++ src/core/layout/qgslayoutatlas.cpp | 1 + src/core/layout/qgslayoutatlas.h | 2 +- src/core/layout/qgslayoutexporter.cpp | 47 ++++++++++++++++++++++++ src/core/layout/qgslayoutexporter.h | 19 ++++++++++ 5 files changed, 86 insertions(+), 1 deletion(-) diff --git a/python/core/layout/qgslayoutexporter.sip b/python/core/layout/qgslayoutexporter.sip index a6c9d04793ce..2144ff17bf14 100644 --- a/python/core/layout/qgslayoutexporter.sip +++ b/python/core/layout/qgslayoutexporter.sip @@ -120,10 +120,12 @@ Returns the rendered image, or a null QImage if the image does not fit into avai enum ExportResult { Success, + Canceled, MemoryError, FileError, PrintError, SvgLayerError, + IteratorError, }; struct ImageExportSettings @@ -198,6 +200,22 @@ error was encountered. If an error code is returned, errorFile() can be called to determine the filename for the export which encountered the error. %End + + static ExportResult exportToImage( QgsAbstractLayoutIterator *iterator, const QString &baseFilePath, + const QString &extension, const QgsLayoutExporter::ImageExportSettings &settings, + QString &error /Out/, QgsFeedback *feedback = 0 ); +%Docstring +Exports a layout ``iterator`` to raster images, with the specified export ``settings``. + +The ``baseFilePath`` argument gives a base file path, which is modified by the +iterator to obtain file paths for each iterator feature. + +Returns a result code indicating whether the export was successful or an +error was encountered. If an error was obtained then ``error`` will be set +to the error description. +%End + + struct PdfExportSettings { PdfExportSettings(); diff --git a/src/core/layout/qgslayoutatlas.cpp b/src/core/layout/qgslayoutatlas.cpp index 08b88b48c9fa..4c3c4e7389a8 100644 --- a/src/core/layout/qgslayoutatlas.cpp +++ b/src/core/layout/qgslayoutatlas.cpp @@ -206,6 +206,7 @@ class AtlasFeatureSorter int QgsLayoutAtlas::updateFeatures() { + mCurrentFeatureNo = -1; if ( !mCoverageLayer ) { return 0; diff --git a/src/core/layout/qgslayoutatlas.h b/src/core/layout/qgslayoutatlas.h index a9ad19a3752b..4e6bc42c030e 100644 --- a/src/core/layout/qgslayoutatlas.h +++ b/src/core/layout/qgslayoutatlas.h @@ -365,7 +365,7 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject, public QgsAbstractLayoutItera // id of each iterated feature (after filtering and sorting) paired with atlas page name QVector< QPair > mFeatureIds; // current atlas feature number - int mCurrentFeatureNo = 0; + int mCurrentFeatureNo = -1; QgsFeature mCurrentFeature; QgsExpressionContext createExpressionContext(); diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index 7244c38b1a17..b5f861771e5d 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -21,6 +21,8 @@ #include "qgsogrutils.h" #include "qgspaintenginehack.h" #include "qgslayoutguidecollection.h" +#include "qgsabstractlayoutiterator.h" +#include "qgsfeedback.h" #include #include #include @@ -394,6 +396,51 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString return Success; } +QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( QgsAbstractLayoutIterator *iterator, const QString &baseFilePath, const QString &extension, const QgsLayoutExporter::ImageExportSettings &settings, QString &error, QgsFeedback *feedback ) +{ + QgsLayoutExporter exporter( iterator->layout() ); + error.clear(); + + if ( !iterator->beginRender() ) + return IteratorError; + + int total = iterator->count(); + double step = total > 0 ? 100.0 / total : 100.0; + int i = 0; + while ( iterator->next() ) + { + if ( feedback ) + { + feedback->setProperty( "progress", QObject::tr( "Exporting %1 of %2" ).arg( i + 1 ).arg( total ) ); + feedback->setProgress( step * i ); + } + if ( feedback && feedback->isCanceled() ) + { + iterator->endRender(); + return Canceled; + } + + QString filePath = iterator->filePath( baseFilePath, extension ); + ExportResult result = exporter.exportToImage( filePath, settings ); + if ( result != Success ) + { + if ( result == FileError ) + error = QObject::tr( "Cannot write to %1. This file may be open in another application." ).arg( filePath ); + iterator->endRender(); + return result; + } + i++; + } + + if ( feedback ) + { + feedback->setProgress( 100 ); + } + + iterator->endRender(); + return Success; +} + QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &filePath, const QgsLayoutExporter::PdfExportSettings &s ) { if ( !mLayout ) diff --git a/src/core/layout/qgslayoutexporter.h b/src/core/layout/qgslayoutexporter.h index e0bb5c78f1c9..d72dc624cd04 100644 --- a/src/core/layout/qgslayoutexporter.h +++ b/src/core/layout/qgslayoutexporter.h @@ -27,6 +27,7 @@ class QgsLayout; class QPainter; class QgsLayoutItemMap; +class QgsAbstractLayoutIterator; /** * \ingroup core @@ -129,10 +130,12 @@ class CORE_EXPORT QgsLayoutExporter enum ExportResult { Success, //!< Export was successful + Canceled, //!< Export was canceled MemoryError, //!< Unable to allocate memory required to export FileError, //!< Could not write to destination file, likely due to a lock held by another application PrintError, //!< Could not start printing to destination device SvgLayerError, //!< Could not create layered SVG file + IteratorError, //!< Error iterating over layout }; //! Contains settings relating to exporting layouts to raster images @@ -206,6 +209,22 @@ class CORE_EXPORT QgsLayoutExporter */ ExportResult exportToImage( const QString &filePath, const QgsLayoutExporter::ImageExportSettings &settings ); + + /** + * Exports a layout \a iterator to raster images, with the specified export \a settings. + * + * The \a baseFilePath argument gives a base file path, which is modified by the + * iterator to obtain file paths for each iterator feature. + * + * Returns a result code indicating whether the export was successful or an + * error was encountered. If an error was obtained then \a error will be set + * to the error description. + */ + static ExportResult exportToImage( QgsAbstractLayoutIterator *iterator, const QString &baseFilePath, + const QString &extension, const QgsLayoutExporter::ImageExportSettings &settings, + QString &error SIP_OUT, QgsFeedback *feedback = nullptr ); + + //! Contains settings relating to exporting layouts to PDF struct PdfExportSettings { From 5a782f48804f5922bd4286451e3050dff72f6f0a Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 23 Dec 2017 20:06:59 +1000 Subject: [PATCH 034/105] Hookup raster atlas export in gui --- src/app/layout/qgslayoutdesignerdialog.cpp | 309 ++++++++++++++++----- src/app/layout/qgslayoutdesignerdialog.h | 5 + 2 files changed, 247 insertions(+), 67 deletions(-) diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index d29b0d854135..dbb302ad099c 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -41,6 +41,7 @@ #include "qgsmapcanvas.h" #include "qgsmessageviewer.h" #include "qgsgui.h" +#include "qgsfeedback.h" #include "qgslayoutitemguiregistry.h" #include "qgslayoutpropertieswidget.h" #include "qgslayoutruler.h" @@ -69,6 +70,7 @@ #include #include #include +#include #ifdef Q_OS_MACX #include #endif @@ -1490,50 +1492,12 @@ void QgsLayoutDesignerDialog::exportToRaster() if ( containsWmsLayers() ) showWmsPrintingWarning(); - // Image size - double oneInchInLayoutUnits = mLayout->convertToLayoutUnits( QgsLayoutMeasurement( 1, QgsUnitTypes::LayoutInches ) ); - QSizeF maxPageSize = mLayout->pageCollection()->maximumPageSize(); - bool hasUniformPageSizes = mLayout->pageCollection()->hasUniformPageSizes(); - int width = ( int )( mLayout->context().dpi() * maxPageSize.width() / oneInchInLayoutUnits ); - int height = ( int )( mLayout->context().dpi() * maxPageSize.height() / oneInchInLayoutUnits ); - double dpi = mLayout->context().dpi(); - - int memuse = width * height * 3 / 1000000; // pixmap + image - QgsDebugMsg( QString( "Image %1x%2" ).arg( width ).arg( height ) ); - QgsDebugMsg( QString( "memuse = %1" ).arg( memuse ) ); - - if ( memuse > 400 ) // about 4500x4500 - { - int answer = QMessageBox::warning( this, tr( "Export layout" ), - tr( "To create an image of %1x%2 requires about %3 MB of memory. Proceed?" ) - .arg( width ).arg( height ).arg( memuse ), - QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok ); - - raise(); - if ( answer == QMessageBox::Cancel ) - return; - } - - //get some defaults from the composition - bool cropToContents = mLayout->customProperty( QStringLiteral( "imageCropToContents" ), false ).toBool(); - int marginTop = mLayout->customProperty( QStringLiteral( "imageCropMarginTop" ), 0 ).toInt(); - int marginRight = mLayout->customProperty( QStringLiteral( "imageCropMarginRight" ), 0 ).toInt(); - int marginBottom = mLayout->customProperty( QStringLiteral( "imageCropMarginBottom" ), 0 ).toInt(); - int marginLeft = mLayout->customProperty( QStringLiteral( "imageCropMarginLeft" ), 0 ).toInt(); - bool antialias = mLayout->customProperty( QStringLiteral( "imageAntialias" ), true ).toBool(); - - QgsLayoutImageExportOptionsDialog imageDlg( this ); - imageDlg.setImageSize( maxPageSize ); - imageDlg.setResolution( dpi ); - imageDlg.setCropToContents( cropToContents ); - imageDlg.setCropMargins( marginTop, marginRight, marginBottom, marginLeft ); - imageDlg.setGenerateWorldFile( mLayout->customProperty( QStringLiteral( "exportWorldFile" ), false ).toBool() ); - imageDlg.setAntialiasing( antialias ); - - QgsLayoutAtlas *printAtlas = atlas(); + if ( !showFileSizeWarning() ) + return; QgsSettings s; QString outputFileName = QgsFileUtils::stringToSafeFilename( mLayout->name() ); + QgsLayoutAtlas *printAtlas = atlas(); if ( printAtlas && printAtlas->enabled() && mActionAtlasPreview->isChecked() ) { QString lastUsedDir = s.value( QStringLiteral( "UI/lastSaveAsImageDir" ), QDir::homePath() ).toString(); @@ -1552,19 +1516,11 @@ void QgsLayoutDesignerDialog::exportToRaster() return; } - if ( !imageDlg.exec() ) + QgsLayoutExporter::ImageExportSettings settings; + QSize imageSize; + if ( !getRasterExportSettings( settings, imageSize ) ) return; - cropToContents = imageDlg.cropToContents(); - imageDlg.getCropMargins( marginTop, marginRight, marginBottom, marginLeft ); - mLayout->setCustomProperty( QStringLiteral( "imageCropToContents" ), cropToContents ); - mLayout->setCustomProperty( QStringLiteral( "imageCropMarginTop" ), marginTop ); - mLayout->setCustomProperty( QStringLiteral( "imageCropMarginRight" ), marginRight ); - mLayout->setCustomProperty( QStringLiteral( "imageCropMarginBottom" ), marginBottom ); - mLayout->setCustomProperty( QStringLiteral( "imageCropMarginLeft" ), marginLeft ); - - mLayout->setCustomProperty( QStringLiteral( "imageAntialias" ), imageDlg.antialiasing() ); - mView->setPaintingEnabled( false ); QApplication::setOverrideCursor( Qt::BusyCursor ); @@ -1573,19 +1529,6 @@ void QgsLayoutDesignerDialog::exportToRaster() QgsLayoutExporter exporter( mLayout ); - QgsLayoutExporter::ImageExportSettings settings; - settings.cropToContents = cropToContents; - settings.cropMargins = QgsMargins( marginLeft, marginTop, marginRight, marginBottom ); - settings.dpi = imageDlg.resolution(); - if ( hasUniformPageSizes ) - { - settings.imageSize = QSize( imageDlg.imageWidth(), imageDlg.imageHeight() ); - } - settings.generateWorldFile = imageDlg.generateWorldFile(); - settings.flags = QgsLayoutContext::FlagUseAdvancedEffects; - if ( imageDlg.antialiasing() ) - settings.flags |= QgsLayoutContext::FlagAntialiasing; - QFileInfo fi( fileNExt.first ); switch ( exporter.exportToImage( fileNExt.first, settings ) ) { @@ -1597,6 +1540,8 @@ void QgsLayoutDesignerDialog::exportToRaster() case QgsLayoutExporter::PrintError: case QgsLayoutExporter::SvgLayerError: + case QgsLayoutExporter::IteratorError: + case QgsLayoutExporter::Canceled: // no meaning for raster exports, will not be encountered break; @@ -1612,7 +1557,7 @@ void QgsLayoutDesignerDialog::exportToRaster() tr( "Trying to create image %1 (%2×%3 @ %4dpi ) " "resulted in a memory overflow.\n\n" "Please try a lower resolution or a smaller paper size." ) - .arg( exporter.errorFile() ).arg( imageDlg.imageWidth() ).arg( imageDlg.imageHeight() ).arg( settings.dpi ), + .arg( exporter.errorFile() ).arg( imageSize.width() ).arg( imageSize.height() ).arg( settings.dpi ), QMessageBox::Ok, QMessageBox::Ok ); break; @@ -1722,6 +1667,8 @@ void QgsLayoutDesignerDialog::exportToPdf() break; case QgsLayoutExporter::SvgLayerError: + case QgsLayoutExporter::IteratorError: + case QgsLayoutExporter::Canceled: // no meaning for PDF exports, will not be encountered break; } @@ -1873,6 +1820,11 @@ void QgsLayoutDesignerDialog::exportToSvg() "Please try a lower resolution or a smaller paper size." ), QMessageBox::Ok, QMessageBox::Ok ); break; + + case QgsLayoutExporter::IteratorError: + case QgsLayoutExporter::Canceled: + // no meaning here + break; } mView->setPaintingEnabled( true ); @@ -2067,9 +2019,155 @@ void QgsLayoutDesignerDialog::printAtlas() void QgsLayoutDesignerDialog::exportAtlasToRaster() { + QgsLayoutAtlas *printAtlas = atlas(); + if ( !printAtlas || !printAtlas->enabled() ) + return; + loadAtlasPredefinedScalesFromProject(); - //TODO + // else, it has an atlas to render, so a directory must first be selected + if ( printAtlas->filenameExpression().isEmpty() ) + { + int res = QMessageBox::warning( nullptr, tr( "Export Atlas" ), + tr( "The filename expression is empty. A default one will be used instead." ), + QMessageBox::Ok | QMessageBox::Cancel, + QMessageBox::Ok ); + if ( res == QMessageBox::Cancel ) + { + return; + } + QString error; + printAtlas->setFilenameExpression( QStringLiteral( "'output_'||@atlas_featurenumber" ), error ); + } + + QgsSettings s; + QString lastUsedDir = s.value( QStringLiteral( "UI/lastSaveAtlasAsImagesDir" ), QDir::homePath() ).toString(); + + QFileDialog dlg( this, tr( "Export Atlas to Directory" ) ); + dlg.setFileMode( QFileDialog::Directory ); + dlg.setOption( QFileDialog::ShowDirsOnly, true ); + dlg.setDirectory( lastUsedDir ); + if ( !dlg.exec() ) + { + return; + } + + const QStringList files = dlg.selectedFiles(); + if ( files.empty() || files.at( 0 ).isEmpty() ) + { + return; + } + QString dir = files.at( 0 ); +#if 0 //TODO + QString format = printAtlas->fileFormat(); +#endif + QString format = "png"; + QString fileExt = '.' + format; + if ( dir.isEmpty() ) + { + return; + } + s.setValue( QStringLiteral( "UI/lastSaveAtlasAsImagesDir" ), dir ); + + // test directory (if it exists and is writable) + if ( !QDir( dir ).exists() || !QFileInfo( dir ).isWritable() ) + { + QMessageBox::warning( nullptr, tr( "Unable to write into the directory" ), + tr( "The given output directory is not writable. Canceling." ), + QMessageBox::Ok, + QMessageBox::Ok ); + return; + } + + if ( containsWmsLayers() ) + showWmsPrintingWarning(); + + if ( !showFileSizeWarning() ) + return; + +#ifdef Q_OS_MAC + QgisApp::instance()->activateWindow(); + this->raise(); +#endif + + QgsLayoutExporter::ImageExportSettings settings; + QSize imageSize; + if ( !getRasterExportSettings( settings, imageSize ) ) + return; + + mView->setPaintingEnabled( false ); + QApplication::setOverrideCursor( Qt::BusyCursor ); + + // force a refresh, to e.g. update data defined properties, tables, etc + mLayout->refresh(); + + QString error; + std::unique_ptr< QgsFeedback > feedback = qgis::make_unique< QgsFeedback >(); + std::unique_ptr< QProgressDialog > progressDialog = qgis::make_unique< QProgressDialog >( tr( "Rendering maps..." ), tr( "Abort" ), 0, 100, this ); + progressDialog->setWindowTitle( tr( "Exporting Atlas" ) ); + connect( feedback.get(), &QgsFeedback::progressChanged, this, [ & ]( double progress ) + { + progressDialog->setValue( progress ); + progressDialog->setLabelText( feedback->property( "progress" ).toString() ) ; + +#ifdef Q_OS_LINUX + // For some reason on Windows hasPendingEvents() always return true, + // but one iteration is actually enough on Windows to get good interactivity + // whereas on Linux we must allow for far more iterations. + // For safety limit the number of iterations + int nIters = 0; + while ( QCoreApplication::hasPendingEvents() && ++nIters < 100 ) +#endif + { + QCoreApplication::processEvents(); + } + + } ); + connect( progressDialog.get(), &QProgressDialog::canceled, this, [ & ] + { + feedback->cancel(); + } ); + + QgsLayoutExporter::ExportResult result = QgsLayoutExporter::exportToImage( printAtlas, dir, fileExt, settings, error, feedback.get() ); + switch ( result ) + { + case QgsLayoutExporter::Success: + mMessageBar->pushMessage( tr( "Export atlas" ), + tr( "Successfully exported atlas to %2" ).arg( QUrl::fromLocalFile( dir ).toString(), dir ), + QgsMessageBar::SUCCESS, 0 ); + break; + + case QgsLayoutExporter::IteratorError: + QMessageBox::warning( this, tr( "Atlas Export Error" ), + tr( "Error encountered while exporting atlas" ), + QMessageBox::Ok, + QMessageBox::Ok ); + break; + + case QgsLayoutExporter::PrintError: + case QgsLayoutExporter::SvgLayerError: + case QgsLayoutExporter::Canceled: + // no meaning for raster exports, will not be encountered + break; + + case QgsLayoutExporter::FileError: + QMessageBox::warning( this, tr( "Image Export Error" ), + error, + QMessageBox::Ok, + QMessageBox::Ok ); + break; + + case QgsLayoutExporter::MemoryError: + QMessageBox::warning( this, tr( "Memory Allocation Error" ), + tr( "Trying to create image of %2×%3 @ %4dpi " + "resulted in a memory overflow.\n\n" + "Please try a lower resolution or a smaller paper size." ) + .arg( imageSize.width() ).arg( imageSize.height() ).arg( settings.dpi ), + QMessageBox::Ok, QMessageBox::Ok ); + break; + } + QApplication::restoreOverrideCursor(); + mView->setPaintingEnabled( true ); } void QgsLayoutDesignerDialog::exportAtlasToSvg() @@ -2356,6 +2454,83 @@ void QgsLayoutDesignerDialog::showForceVectorWarning() } } +bool QgsLayoutDesignerDialog::showFileSizeWarning() +{ + // Image size + double oneInchInLayoutUnits = mLayout->convertToLayoutUnits( QgsLayoutMeasurement( 1, QgsUnitTypes::LayoutInches ) ); + QSizeF maxPageSize = mLayout->pageCollection()->maximumPageSize(); + int width = ( int )( mLayout->context().dpi() * maxPageSize.width() / oneInchInLayoutUnits ); + int height = ( int )( mLayout->context().dpi() * maxPageSize.height() / oneInchInLayoutUnits ); + int memuse = width * height * 3 / 1000000; // pixmap + image + QgsDebugMsg( QString( "Image %1x%2" ).arg( width ).arg( height ) ); + QgsDebugMsg( QString( "memuse = %1" ).arg( memuse ) ); + + if ( memuse > 400 ) // about 4500x4500 + { + int answer = QMessageBox::warning( this, tr( "Export layout" ), + tr( "To create an image of %1x%2 requires about %3 MB of memory. Proceed?" ) + .arg( width ).arg( height ).arg( memuse ), + QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok ); + + raise(); + if ( answer == QMessageBox::Cancel ) + return false; + } + return true; +} + +bool QgsLayoutDesignerDialog::getRasterExportSettings( QgsLayoutExporter::ImageExportSettings &settings, QSize &imageSize ) +{ + // Image size + QSizeF maxPageSize = mLayout->pageCollection()->maximumPageSize(); + bool hasUniformPageSizes = mLayout->pageCollection()->hasUniformPageSizes(); + double dpi = mLayout->context().dpi(); + + //get some defaults from the composition + bool cropToContents = mLayout->customProperty( QStringLiteral( "imageCropToContents" ), false ).toBool(); + int marginTop = mLayout->customProperty( QStringLiteral( "imageCropMarginTop" ), 0 ).toInt(); + int marginRight = mLayout->customProperty( QStringLiteral( "imageCropMarginRight" ), 0 ).toInt(); + int marginBottom = mLayout->customProperty( QStringLiteral( "imageCropMarginBottom" ), 0 ).toInt(); + int marginLeft = mLayout->customProperty( QStringLiteral( "imageCropMarginLeft" ), 0 ).toInt(); + bool antialias = mLayout->customProperty( QStringLiteral( "imageAntialias" ), true ).toBool(); + + QgsLayoutImageExportOptionsDialog imageDlg( this ); + imageDlg.setImageSize( maxPageSize ); + imageDlg.setResolution( dpi ); + imageDlg.setCropToContents( cropToContents ); + imageDlg.setCropMargins( marginTop, marginRight, marginBottom, marginLeft ); + imageDlg.setGenerateWorldFile( mLayout->customProperty( QStringLiteral( "exportWorldFile" ), false ).toBool() ); + imageDlg.setAntialiasing( antialias ); + + if ( !imageDlg.exec() ) + return false; + + imageSize = QSize( imageDlg.imageWidth(), imageDlg.imageHeight() ); + cropToContents = imageDlg.cropToContents(); + imageDlg.getCropMargins( marginTop, marginRight, marginBottom, marginLeft ); + mLayout->setCustomProperty( QStringLiteral( "imageCropToContents" ), cropToContents ); + mLayout->setCustomProperty( QStringLiteral( "imageCropMarginTop" ), marginTop ); + mLayout->setCustomProperty( QStringLiteral( "imageCropMarginRight" ), marginRight ); + mLayout->setCustomProperty( QStringLiteral( "imageCropMarginBottom" ), marginBottom ); + mLayout->setCustomProperty( QStringLiteral( "imageCropMarginLeft" ), marginLeft ); + + mLayout->setCustomProperty( QStringLiteral( "imageAntialias" ), imageDlg.antialiasing() ); + + settings.cropToContents = cropToContents; + settings.cropMargins = QgsMargins( marginLeft, marginTop, marginRight, marginBottom ); + settings.dpi = imageDlg.resolution(); + if ( hasUniformPageSizes ) + { + settings.imageSize = imageSize; + } + settings.generateWorldFile = imageDlg.generateWorldFile(); + settings.flags = QgsLayoutContext::FlagUseAdvancedEffects; + if ( imageDlg.antialiasing() ) + settings.flags |= QgsLayoutContext::FlagAntialiasing; + + return true; +} + void QgsLayoutDesignerDialog::toggleAtlasControls( bool atlasEnabled ) { //preview defaults to unchecked diff --git a/src/app/layout/qgslayoutdesignerdialog.h b/src/app/layout/qgslayoutdesignerdialog.h index a599775525ce..c5b0df08e58e 100644 --- a/src/app/layout/qgslayoutdesignerdialog.h +++ b/src/app/layout/qgslayoutdesignerdialog.h @@ -19,6 +19,7 @@ #include "ui_qgslayoutdesignerbase.h" #include "qgslayoutdesignerinterface.h" +#include "qgslayoutexporter.h" #include class QgsLayoutDesignerDialog; @@ -408,6 +409,10 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner void showRasterizationWarning(); void showForceVectorWarning(); + bool showFileSizeWarning(); + bool getRasterExportSettings( QgsLayoutExporter::ImageExportSettings &settings, QSize &imageSize ); + + void toggleAtlasActions( bool enabled ); /** From 409d10f43d82982ab90abafc6e9b63f3e7b6b1cc Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 23 Dec 2017 20:19:29 +1000 Subject: [PATCH 035/105] Restore atlas raster format handling --- src/app/layout/qgslayoutatlaswidget.cpp | 10 +++------- src/app/layout/qgslayoutdesignerdialog.cpp | 5 +---- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/app/layout/qgslayoutatlaswidget.cpp b/src/app/layout/qgslayoutatlaswidget.cpp index db61ab845020..e24b14fb256b 100644 --- a/src/app/layout/qgslayoutatlaswidget.cpp +++ b/src/app/layout/qgslayoutatlaswidget.cpp @@ -314,11 +314,9 @@ void QgsLayoutAtlasWidget::mAtlasSortFeatureDirectionButton_clicked() void QgsLayoutAtlasWidget::changeFileFormat() { -#if 0 //TODO - QgsAtlasComposition *atlasMap = mAtlas; - atlasMap->setFileFormat( mAtlasFileFormat->currentText() ); -#endif + mLayout->setCustomProperty( QStringLiteral( "atlasRasterFormat" ), mAtlasFileFormat->currentText() ); } + void QgsLayoutAtlasWidget::updateGuiElements() { blockAllSignals( true ); @@ -353,9 +351,7 @@ void QgsLayoutAtlasWidget::updateGuiElements() mAtlasFeatureFilterEdit->setEnabled( mAtlas->filterFeatures() ); mAtlasFeatureFilterButton->setEnabled( mAtlas->filterFeatures() ); -#if 0 //TODO - mAtlasFileFormat->setCurrentIndex( mAtlasFileFormat->findText( mAtlas->fileFormat() ) ); -#endif + mAtlasFileFormat->setCurrentIndex( mAtlasFileFormat->findText( mLayout->customProperty( QStringLiteral( "atlasRasterFormat" ), QStringLiteral( "png" ) ).toString() ) ); blockAllSignals( false ); } diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index dbb302ad099c..384b2680d7b7 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -2058,10 +2058,7 @@ void QgsLayoutDesignerDialog::exportAtlasToRaster() return; } QString dir = files.at( 0 ); -#if 0 //TODO - QString format = printAtlas->fileFormat(); -#endif - QString format = "png"; + QString format = mLayout->customProperty( QStringLiteral( "atlasRasterFormat" ), QStringLiteral( "png" ) ).toString(); QString fileExt = '.' + format; if ( dir.isEmpty() ) { From 9751c7706371babe7ca922927b7cec025967ad27 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 23 Dec 2017 20:35:08 +1000 Subject: [PATCH 036/105] Restore svg atlas export --- python/core/layout/qgslayoutexporter.sip | 15 ++ src/app/layout/qgslayoutdesignerdialog.cpp | 270 +++++++++++++++++---- src/app/layout/qgslayoutdesignerdialog.h | 2 +- src/core/layout/qgslayoutexporter.cpp | 46 ++++ src/core/layout/qgslayoutexporter.h | 15 ++ 5 files changed, 296 insertions(+), 52 deletions(-) diff --git a/python/core/layout/qgslayoutexporter.sip b/python/core/layout/qgslayoutexporter.sip index 2144ff17bf14..746f9b97da36 100644 --- a/python/core/layout/qgslayoutexporter.sip +++ b/python/core/layout/qgslayoutexporter.sip @@ -315,6 +315,21 @@ Returns a result code indicating whether the export was successful or an error was encountered. %End + static ExportResult exportToSvg( QgsAbstractLayoutIterator *iterator, const QString &baseFilePath, + const QgsLayoutExporter::SvgExportSettings &settings, + QString &error /Out/, QgsFeedback *feedback = 0 ); +%Docstring +Exports a layout ``iterator`` to SVG files, with the specified export ``settings``. + +The ``baseFilePath`` argument gives a base file path, which is modified by the +iterator to obtain file paths for each iterator feature. + +Returns a result code indicating whether the export was successful or an +error was encountered. If an error was obtained then ``error`` will be set +to the error description. +%End + + QString errorFile() const; %Docstring Returns the file name corresponding to the last error encountered during diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 384b2680d7b7..7caddc15b9b8 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -1721,61 +1721,20 @@ void QgsLayoutDesignerDialog::exportToSvg() outputFileName += QLatin1String( ".svg" ); } - settings.setValue( QStringLiteral( "UI/lastSaveAsSvgFile" ), outputFileName ); - - bool groupLayers = false; bool prevSettingLabelsAsOutlines = mLayout->project()->readBoolEntry( QStringLiteral( "PAL" ), QStringLiteral( "/DrawOutlineLabels" ), true ); - bool clipToContent = false; - double marginTop = 0.0; - double marginRight = 0.0; - double marginBottom = 0.0; - double marginLeft = 0.0; - bool previousForceVector = mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool(); - - // open options dialog - QDialog dialog; - Ui::QgsSvgExportOptionsDialog options; - options.setupUi( &dialog ); - options.chkTextAsOutline->setChecked( prevSettingLabelsAsOutlines ); - options.chkMapLayersAsGroup->setChecked( mLayout->customProperty( QStringLiteral( "svgGroupLayers" ), false ).toBool() ); - options.mClipToContentGroupBox->setChecked( mLayout->customProperty( QStringLiteral( "svgCropToContents" ), false ).toBool() ); - options.mForceVectorCheckBox->setChecked( previousForceVector ); - options.mTopMarginSpinBox->setValue( mLayout->customProperty( QStringLiteral( "svgCropMarginTop" ), 0 ).toInt() ); - options.mRightMarginSpinBox->setValue( mLayout->customProperty( QStringLiteral( "svgCropMarginRight" ), 0 ).toInt() ); - options.mBottomMarginSpinBox->setValue( mLayout->customProperty( QStringLiteral( "svgCropMarginBottom" ), 0 ).toInt() ); - options.mLeftMarginSpinBox->setValue( mLayout->customProperty( QStringLiteral( "svgCropMarginLeft" ), 0 ).toInt() ); + settings.setValue( QStringLiteral( "UI/lastSaveAsSvgFile" ), outputFileName ); - if ( dialog.exec() != QDialog::Accepted ) + QgsLayoutExporter::SvgExportSettings svgSettings; + bool exportAsText = false; + if ( !getSvgExportSettings( svgSettings, exportAsText ) ) return; - groupLayers = options.chkMapLayersAsGroup->isChecked(); - clipToContent = options.mClipToContentGroupBox->isChecked(); - marginTop = options.mTopMarginSpinBox->value(); - marginRight = options.mRightMarginSpinBox->value(); - marginBottom = options.mBottomMarginSpinBox->value(); - marginLeft = options.mLeftMarginSpinBox->value(); - - //save dialog settings - mLayout->setCustomProperty( QStringLiteral( "svgGroupLayers" ), groupLayers ); - mLayout->setCustomProperty( QStringLiteral( "svgCropToContents" ), clipToContent ); - mLayout->setCustomProperty( QStringLiteral( "svgCropMarginTop" ), marginTop ); - mLayout->setCustomProperty( QStringLiteral( "svgCropMarginRight" ), marginRight ); - mLayout->setCustomProperty( QStringLiteral( "svgCropMarginBottom" ), marginBottom ); - mLayout->setCustomProperty( QStringLiteral( "svgCropMarginLeft" ), marginLeft ); - //temporarily override label draw outlines setting - mLayout->project()->writeEntry( QStringLiteral( "PAL" ), QStringLiteral( "/DrawOutlineLabels" ), options.chkTextAsOutline->isChecked() ); + mLayout->project()->writeEntry( QStringLiteral( "PAL" ), QStringLiteral( "/DrawOutlineLabels" ), exportAsText ); mView->setPaintingEnabled( false ); QApplication::setOverrideCursor( Qt::BusyCursor ); - QgsLayoutExporter::SvgExportSettings svgSettings; - svgSettings.forceVectorOutput = mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool(); - svgSettings.cropToContents = clipToContent; - svgSettings.cropMargins = QgsMargins( marginLeft, marginTop, marginRight, marginBottom ); - svgSettings.forceVectorOutput = options.mForceVectorCheckBox->isChecked(); - svgSettings.exportAsLayers = groupLayers; - // force a refresh, to e.g. update data defined properties, tables, etc mLayout->refresh(); @@ -2095,9 +2054,6 @@ void QgsLayoutDesignerDialog::exportAtlasToRaster() mView->setPaintingEnabled( false ); QApplication::setOverrideCursor( Qt::BusyCursor ); - // force a refresh, to e.g. update data defined properties, tables, etc - mLayout->refresh(); - QString error; std::unique_ptr< QgsFeedback > feedback = qgis::make_unique< QgsFeedback >(); std::unique_ptr< QProgressDialog > progressDialog = qgis::make_unique< QProgressDialog >( tr( "Rendering maps..." ), tr( "Abort" ), 0, 100, this ); @@ -2126,6 +2082,8 @@ void QgsLayoutDesignerDialog::exportAtlasToRaster() } ); QgsLayoutExporter::ExportResult result = QgsLayoutExporter::exportToImage( printAtlas, dir, fileExt, settings, error, feedback.get() ); + QApplication::restoreOverrideCursor(); + switch ( result ) { case QgsLayoutExporter::Success: @@ -2163,14 +2121,172 @@ void QgsLayoutDesignerDialog::exportAtlasToRaster() QMessageBox::Ok, QMessageBox::Ok ); break; } - QApplication::restoreOverrideCursor(); mView->setPaintingEnabled( true ); } void QgsLayoutDesignerDialog::exportAtlasToSvg() { + QgsLayoutAtlas *printAtlas = atlas(); + if ( !printAtlas || !printAtlas->enabled() ) + return; + loadAtlasPredefinedScalesFromProject(); - //TODO + if ( containsWmsLayers() ) + { + showWmsPrintingWarning(); + } + + showSvgExportWarning(); + + // else, it has an atlas to render, so a directory must first be selected + if ( printAtlas->filenameExpression().isEmpty() ) + { + int res = QMessageBox::warning( nullptr, tr( "Export Atlas" ), + tr( "The filename expression is empty. A default one will be used instead." ), + QMessageBox::Ok | QMessageBox::Cancel, + QMessageBox::Ok ); + if ( res == QMessageBox::Cancel ) + { + return; + } + QString error; + printAtlas->setFilenameExpression( QStringLiteral( "'output_'||@atlas_featurenumber" ), error ); + } + + QgsSettings s; + QString lastUsedDir = s.value( QStringLiteral( "UI/lastSaveAtlasAsSvgDir" ), QDir::homePath() ).toString(); + + QFileDialog dlg( this, tr( "Export Atlas to Directory" ) ); + dlg.setFileMode( QFileDialog::Directory ); + dlg.setOption( QFileDialog::ShowDirsOnly, true ); + dlg.setDirectory( lastUsedDir ); + if ( !dlg.exec() ) + { + return; + } + +#ifdef Q_OS_MAC + QgisApp::instance()->activateWindow(); + this->raise(); +#endif + + const QStringList files = dlg.selectedFiles(); + if ( files.empty() || files.at( 0 ).isEmpty() ) + { + return; + } + QString dir = files.at( 0 ); + if ( dir.isEmpty() ) + { + return; + } + s.setValue( QStringLiteral( "UI/lastSaveAtlasAsSvgDir" ), dir ); + + // test directory (if it exists and is writable) + if ( !QDir( dir ).exists() || !QFileInfo( dir ).isWritable() ) + { + QMessageBox::warning( nullptr, tr( "Unable to write into the directory" ), + tr( "The given output directory is not writable. Canceling." ), + QMessageBox::Ok, + QMessageBox::Ok ); + return; + } + + bool prevSettingLabelsAsOutlines = mLayout->project()->readBoolEntry( QStringLiteral( "PAL" ), QStringLiteral( "/DrawOutlineLabels" ), true ); + QgsLayoutExporter::SvgExportSettings svgSettings; + bool exportAsText = false; + if ( !getSvgExportSettings( svgSettings, exportAsText ) ) + return; + + //temporarily override label draw outlines setting + mLayout->project()->writeEntry( QStringLiteral( "PAL" ), QStringLiteral( "/DrawOutlineLabels" ), exportAsText ); + + mView->setPaintingEnabled( false ); + QApplication::setOverrideCursor( Qt::BusyCursor ); + + QString error; + std::unique_ptr< QgsFeedback > feedback = qgis::make_unique< QgsFeedback >(); + std::unique_ptr< QProgressDialog > progressDialog = qgis::make_unique< QProgressDialog >( tr( "Rendering maps..." ), tr( "Abort" ), 0, 100, this ); + progressDialog->setWindowTitle( tr( "Exporting Atlas" ) ); + connect( feedback.get(), &QgsFeedback::progressChanged, this, [ & ]( double progress ) + { + progressDialog->setValue( progress ); + progressDialog->setLabelText( feedback->property( "progress" ).toString() ) ; + +#ifdef Q_OS_LINUX + // For some reason on Windows hasPendingEvents() always return true, + // but one iteration is actually enough on Windows to get good interactivity + // whereas on Linux we must allow for far more iterations. + // For safety limit the number of iterations + int nIters = 0; + while ( QCoreApplication::hasPendingEvents() && ++nIters < 100 ) +#endif + { + QCoreApplication::processEvents(); + } + + } ); + connect( progressDialog.get(), &QProgressDialog::canceled, this, [ & ] + { + feedback->cancel(); + } ); + + QgsLayoutExporter::ExportResult result = QgsLayoutExporter::exportToSvg( printAtlas, dir, svgSettings, error, feedback.get() ); + + QApplication::restoreOverrideCursor(); + switch ( result ) + { + case QgsLayoutExporter::Success: + { + mMessageBar->pushMessage( tr( "Export atlas" ), + tr( "Successfully exported atlas to %2" ).arg( QUrl::fromLocalFile( dir ).toString(), dir ), + QgsMessageBar::SUCCESS, 0 ); + break; + } + + case QgsLayoutExporter::FileError: + QMessageBox::warning( this, tr( "Export atlas" ), + error, QMessageBox::Ok, + QMessageBox::Ok ); + break; + + case QgsLayoutExporter::SvgLayerError: + QMessageBox::warning( this, tr( "Export atlas" ), + tr( "Cannot create layered SVG file." ), + QMessageBox::Ok, + QMessageBox::Ok ); + break; + + case QgsLayoutExporter::PrintError: + QMessageBox::warning( this, tr( "Export atlas" ), + tr( "Could not create print device." ), + QMessageBox::Ok, + QMessageBox::Ok ); + break; + + + case QgsLayoutExporter::MemoryError: + QMessageBox::warning( this, tr( "Memory Allocation Error" ), + tr( "Exporting the SVG " + "resulted in a memory overflow.\n\n" + "Please try a lower resolution or a smaller paper size." ), + QMessageBox::Ok, QMessageBox::Ok ); + break; + + case QgsLayoutExporter::IteratorError: + QMessageBox::warning( this, tr( "Atlas Export Error" ), + tr( "Error encountered while exporting atlas" ), + QMessageBox::Ok, + QMessageBox::Ok ); + break; + + case QgsLayoutExporter::Canceled: + // no meaning here + break; + } + + mView->setPaintingEnabled( true ); + mLayout->project()->writeEntry( QStringLiteral( "PAL" ), QStringLiteral( "/DrawOutlineLabels" ), prevSettingLabelsAsOutlines ); } void QgsLayoutDesignerDialog::exportAtlasToPdf() @@ -2528,6 +2644,58 @@ bool QgsLayoutDesignerDialog::getRasterExportSettings( QgsLayoutExporter::ImageE return true; } +bool QgsLayoutDesignerDialog::getSvgExportSettings( QgsLayoutExporter::SvgExportSettings &settings, bool &exportAsText ) +{ + bool groupLayers = false; + bool prevSettingLabelsAsOutlines = mLayout->project()->readBoolEntry( QStringLiteral( "PAL" ), QStringLiteral( "/DrawOutlineLabels" ), true ); + bool clipToContent = false; + double marginTop = 0.0; + double marginRight = 0.0; + double marginBottom = 0.0; + double marginLeft = 0.0; + bool previousForceVector = mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool(); + + // open options dialog + QDialog dialog; + Ui::QgsSvgExportOptionsDialog options; + options.setupUi( &dialog ); + options.chkTextAsOutline->setChecked( prevSettingLabelsAsOutlines ); + options.chkMapLayersAsGroup->setChecked( mLayout->customProperty( QStringLiteral( "svgGroupLayers" ), false ).toBool() ); + options.mClipToContentGroupBox->setChecked( mLayout->customProperty( QStringLiteral( "svgCropToContents" ), false ).toBool() ); + options.mForceVectorCheckBox->setChecked( previousForceVector ); + options.mTopMarginSpinBox->setValue( mLayout->customProperty( QStringLiteral( "svgCropMarginTop" ), 0 ).toInt() ); + options.mRightMarginSpinBox->setValue( mLayout->customProperty( QStringLiteral( "svgCropMarginRight" ), 0 ).toInt() ); + options.mBottomMarginSpinBox->setValue( mLayout->customProperty( QStringLiteral( "svgCropMarginBottom" ), 0 ).toInt() ); + options.mLeftMarginSpinBox->setValue( mLayout->customProperty( QStringLiteral( "svgCropMarginLeft" ), 0 ).toInt() ); + + if ( dialog.exec() != QDialog::Accepted ) + return false; + + groupLayers = options.chkMapLayersAsGroup->isChecked(); + clipToContent = options.mClipToContentGroupBox->isChecked(); + marginTop = options.mTopMarginSpinBox->value(); + marginRight = options.mRightMarginSpinBox->value(); + marginBottom = options.mBottomMarginSpinBox->value(); + marginLeft = options.mLeftMarginSpinBox->value(); + + //save dialog settings + mLayout->setCustomProperty( QStringLiteral( "svgGroupLayers" ), groupLayers ); + mLayout->setCustomProperty( QStringLiteral( "svgCropToContents" ), clipToContent ); + mLayout->setCustomProperty( QStringLiteral( "svgCropMarginTop" ), marginTop ); + mLayout->setCustomProperty( QStringLiteral( "svgCropMarginRight" ), marginRight ); + mLayout->setCustomProperty( QStringLiteral( "svgCropMarginBottom" ), marginBottom ); + mLayout->setCustomProperty( QStringLiteral( "svgCropMarginLeft" ), marginLeft ); + + settings.forceVectorOutput = mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool(); + settings.cropToContents = clipToContent; + settings.cropMargins = QgsMargins( marginLeft, marginTop, marginRight, marginBottom ); + settings.forceVectorOutput = options.mForceVectorCheckBox->isChecked(); + settings.exportAsLayers = groupLayers; + + exportAsText = options.chkTextAsOutline->isChecked(); + return true; +} + void QgsLayoutDesignerDialog::toggleAtlasControls( bool atlasEnabled ) { //preview defaults to unchecked diff --git a/src/app/layout/qgslayoutdesignerdialog.h b/src/app/layout/qgslayoutdesignerdialog.h index c5b0df08e58e..05a039ccf1b8 100644 --- a/src/app/layout/qgslayoutdesignerdialog.h +++ b/src/app/layout/qgslayoutdesignerdialog.h @@ -411,7 +411,7 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner bool showFileSizeWarning(); bool getRasterExportSettings( QgsLayoutExporter::ImageExportSettings &settings, QSize &imageSize ); - + bool getSvgExportSettings( QgsLayoutExporter::SvgExportSettings &settings, bool &exportAsText ); void toggleAtlasActions( bool enabled ); diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index b5f861771e5d..b658a7b1de4b 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -652,6 +652,52 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &f return Success; } +QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( QgsAbstractLayoutIterator *iterator, const QString &baseFilePath, const QgsLayoutExporter::SvgExportSettings &settings, QString &error, QgsFeedback *feedback ) +{ + QgsLayoutExporter exporter( iterator->layout() ); + error.clear(); + + if ( !iterator->beginRender() ) + return IteratorError; + + int total = iterator->count(); + double step = total > 0 ? 100.0 / total : 100.0; + int i = 0; + while ( iterator->next() ) + { + if ( feedback ) + { + feedback->setProperty( "progress", QObject::tr( "Exporting %1 of %2" ).arg( i + 1 ).arg( total ) ); + feedback->setProgress( step * i ); + } + if ( feedback && feedback->isCanceled() ) + { + iterator->endRender(); + return Canceled; + } + + QString filePath = iterator->filePath( baseFilePath, QStringLiteral( "svg" ) ); + ExportResult result = exporter.exportToSvg( filePath, settings ); + if ( result != Success ) + { + if ( result == FileError ) + error = QObject::tr( "Cannot write to %1. This file may be open in another application." ).arg( filePath ); + iterator->endRender(); + return result; + } + i++; + } + + if ( feedback ) + { + feedback->setProgress( 100 ); + } + + iterator->endRender(); + return Success; + +} + void QgsLayoutExporter::preparePrintAsPdf( QPrinter &printer, const QString &filePath ) { printer.setOutputFileName( filePath ); diff --git a/src/core/layout/qgslayoutexporter.h b/src/core/layout/qgslayoutexporter.h index d72dc624cd04..26440cd4ec15 100644 --- a/src/core/layout/qgslayoutexporter.h +++ b/src/core/layout/qgslayoutexporter.h @@ -322,6 +322,21 @@ class CORE_EXPORT QgsLayoutExporter */ ExportResult exportToSvg( const QString &filePath, const QgsLayoutExporter::SvgExportSettings &settings ); + /** + * Exports a layout \a iterator to SVG files, with the specified export \a settings. + * + * The \a baseFilePath argument gives a base file path, which is modified by the + * iterator to obtain file paths for each iterator feature. + * + * Returns a result code indicating whether the export was successful or an + * error was encountered. If an error was obtained then \a error will be set + * to the error description. + */ + static ExportResult exportToSvg( QgsAbstractLayoutIterator *iterator, const QString &baseFilePath, + const QgsLayoutExporter::SvgExportSettings &settings, + QString &error SIP_OUT, QgsFeedback *feedback = nullptr ); + + /** * Returns the file name corresponding to the last error encountered during * an export. From 427da5c08174bac266f8bc0b95631887ee471423 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 23 Dec 2017 21:22:32 +1000 Subject: [PATCH 037/105] Restore atlas single file pdf export --- python/core/layout/qgslayoutexporter.sip | 14 ++ src/app/layout/qgslayoutatlaswidget.cpp | 14 +- src/app/layout/qgslayoutdesignerdialog.cpp | 168 ++++++++++++++++++++- src/core/layout/qgslayoutexporter.cpp | 107 ++++++++++--- src/core/layout/qgslayoutexporter.h | 20 ++- 5 files changed, 290 insertions(+), 33 deletions(-) diff --git a/python/core/layout/qgslayoutexporter.sip b/python/core/layout/qgslayoutexporter.sip index 746f9b97da36..75819738bfbb 100644 --- a/python/core/layout/qgslayoutexporter.sip +++ b/python/core/layout/qgslayoutexporter.sip @@ -259,6 +259,20 @@ Returns a result code indicating whether the export was successful or an error was encountered. %End + static ExportResult exportToPdf( QgsAbstractLayoutIterator *iterator, const QString &fileName, + const QgsLayoutExporter::PdfExportSettings &settings, + QString &error /Out/, QgsFeedback *feedback = 0 ); +%Docstring +Exports a layout ``iterator`` to a single PDF file, with the specified export ``settings``. + +The ``fileName`` argument gives the destination file name for the output PDF. + +Returns a result code indicating whether the export was successful or an +error was encountered. If an error was obtained then ``error`` will be set +to the error description. +%End + + struct SvgExportSettings { diff --git a/src/app/layout/qgslayoutatlaswidget.cpp b/src/app/layout/qgslayoutatlaswidget.cpp index e24b14fb256b..d6fcac392426 100644 --- a/src/app/layout/qgslayoutatlaswidget.cpp +++ b/src/app/layout/qgslayoutatlaswidget.cpp @@ -172,9 +172,8 @@ void QgsLayoutAtlasWidget::mAtlasSingleFileCheckBox_stateChanged( int state ) mAtlasFilenamePatternEdit->setEnabled( true ); mAtlasFilenameExpressionButton->setEnabled( true ); } -#if 0 //TODO - mAtlas->setSingleFile( state == Qt::Checked ); -#endif + + mLayout->setCustomProperty( QStringLiteral( "singleFile" ), state == Qt::Checked ); } void QgsLayoutAtlasWidget::mAtlasSortFeatureCheckBox_stateChanged( int state ) @@ -334,11 +333,10 @@ void QgsLayoutAtlasWidget::updateGuiElements() mAtlasFilenamePatternEdit->setText( mAtlas->filenameExpression() ); mAtlasHideCoverageCheckBox->setCheckState( mAtlas->hideCoverage() ? Qt::Checked : Qt::Unchecked ); -#if 0 //TODO - mAtlasSingleFileCheckBox->setCheckState( mAtlas->singleFile() ? Qt::Checked : Qt::Unchecked ); - mAtlasFilenamePatternEdit->setEnabled( !mAtlas->singleFile() ); - mAtlasFilenameExpressionButton->setEnabled( !mAtlas->singleFile() ); -#endif + bool singleFile = mLayout->customProperty( "singleFile", true ).toBool(); + mAtlasSingleFileCheckBox->setCheckState( singleFile ? Qt::Checked : Qt::Unchecked ); + mAtlasFilenamePatternEdit->setEnabled( !singleFile ); + mAtlasFilenameExpressionButton->setEnabled( !singleFile ); mAtlasSortFeatureCheckBox->setCheckState( mAtlas->sortFeatures() ? Qt::Checked : Qt::Unchecked ); mAtlasSortFeatureDirectionButton->setEnabled( mAtlas->sortFeatures() ); diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 7caddc15b9b8..6ae08485d846 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -2291,8 +2291,172 @@ void QgsLayoutDesignerDialog::exportAtlasToSvg() void QgsLayoutDesignerDialog::exportAtlasToPdf() { + QgsLayoutAtlas *printAtlas = atlas(); + if ( !printAtlas || !printAtlas->enabled() ) + return; + loadAtlasPredefinedScalesFromProject(); -//TODO + if ( containsWmsLayers() ) + { + showWmsPrintingWarning(); + } + + if ( requiresRasterization() ) + { + showRasterizationWarning(); + } + + if ( containsAdvancedEffects() && ( mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool() ) ) + { + showForceVectorWarning(); + } + + bool singleFile = mLayout->customProperty( QStringLiteral( "singleFile" ), true ).toBool(); + + QString outputFileName; + QgsSettings settings; + if ( singleFile ) + { + QString lastUsedFile = settings.value( QStringLiteral( "UI/lastSaveAsPdfFile" ), QStringLiteral( "qgis.pdf" ) ).toString(); + QFileInfo file( lastUsedFile ); + + QgsLayoutAtlas *printAtlas = atlas(); + if ( printAtlas && printAtlas->enabled() && mActionAtlasPreview->isChecked() ) + { + outputFileName = QDir( file.path() ).filePath( QgsFileUtils::stringToSafeFilename( printAtlas->currentFilename() ) + QStringLiteral( ".pdf" ) ); + } + else + { + outputFileName = file.path() + '/' + QgsFileUtils::stringToSafeFilename( mLayout->name() ) + QStringLiteral( ".pdf" ); + } + +#ifdef Q_OS_MAC + QgisApp::instance()->activateWindow(); + this->raise(); +#endif + outputFileName = QFileDialog::getSaveFileName( + this, + tr( "Export to PDF" ), + outputFileName, + tr( "PDF Format" ) + " (*.pdf *.PDF)" ); + this->activateWindow(); + if ( outputFileName.isEmpty() ) + { + return; + } + + if ( !outputFileName.endsWith( QLatin1String( ".pdf" ), Qt::CaseInsensitive ) ) + { + outputFileName += QLatin1String( ".pdf" ); + } + settings.setValue( QStringLiteral( "UI/lastSaveAsPdfFile" ), outputFileName ); + } + + mView->setPaintingEnabled( false ); + QApplication::setOverrideCursor( Qt::BusyCursor ); + + QgsLayoutExporter::PdfExportSettings pdfSettings; + pdfSettings.rasterizeWholeImage = mLayout->customProperty( QStringLiteral( "rasterize" ), false ).toBool(); + pdfSettings.forceVectorOutput = mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool(); + + QFileInfo fi( outputFileName ); + + QString error; + std::unique_ptr< QgsFeedback > feedback = qgis::make_unique< QgsFeedback >(); + std::unique_ptr< QProgressDialog > progressDialog = qgis::make_unique< QProgressDialog >( tr( "Rendering maps..." ), tr( "Abort" ), 0, 100, this ); + progressDialog->setWindowTitle( tr( "Exporting Atlas" ) ); + connect( feedback.get(), &QgsFeedback::progressChanged, this, [ & ]( double progress ) + { + progressDialog->setValue( progress ); + progressDialog->setLabelText( feedback->property( "progress" ).toString() ) ; + +#ifdef Q_OS_LINUX + // For some reason on Windows hasPendingEvents() always return true, + // but one iteration is actually enough on Windows to get good interactivity + // whereas on Linux we must allow for far more iterations. + // For safety limit the number of iterations + int nIters = 0; + while ( QCoreApplication::hasPendingEvents() && ++nIters < 100 ) +#endif + { + QCoreApplication::processEvents(); + } + + } ); + connect( progressDialog.get(), &QProgressDialog::canceled, this, [ & ] + { + feedback->cancel(); + } ); + + QgsLayoutExporter::ExportResult result = QgsLayoutExporter::Success; + if ( singleFile ) + { + result = QgsLayoutExporter::exportToPdf( printAtlas, outputFileName, pdfSettings, error, feedback.get() ); + } + else + { + + } + + switch ( result ) + { + case QgsLayoutExporter::Success: + { + if ( singleFile ) + { + mMessageBar->pushMessage( tr( "Export atlas" ), + tr( "Successfully exported atlas to %2" ).arg( QUrl::fromLocalFile( fi.path() ).toString(), outputFileName ), + QgsMessageBar::SUCCESS, 0 ); + } + else + { + mMessageBar->pushMessage( tr( "Export atlas" ), + tr( "Successfully exported atlas to %2" ).arg( QUrl::fromLocalFile( outputFileName ).toString(), outputFileName ), + QgsMessageBar::SUCCESS, 0 ); + } + break; + } + + case QgsLayoutExporter::FileError: + QMessageBox::warning( this, tr( "Export atlas" ), + error, QMessageBox::Ok, + QMessageBox::Ok ); + break; + + case QgsLayoutExporter::SvgLayerError: + // no meaning + break; + + case QgsLayoutExporter::PrintError: + QMessageBox::warning( this, tr( "Export atlas" ), + tr( "Could not create print device." ), + QMessageBox::Ok, + QMessageBox::Ok ); + break; + + + case QgsLayoutExporter::MemoryError: + QMessageBox::warning( this, tr( "Memory Allocation Error" ), + tr( "Exporting the PDF " + "resulted in a memory overflow.\n\n" + "Please try a lower resolution or a smaller paper size." ), + QMessageBox::Ok, QMessageBox::Ok ); + break; + + case QgsLayoutExporter::IteratorError: + QMessageBox::warning( this, tr( "Atlas Export Error" ), + tr( "Error encountered while exporting atlas" ), + QMessageBox::Ok, + QMessageBox::Ok ); + break; + + case QgsLayoutExporter::Canceled: + // no meaning here + break; + } + + mView->setPaintingEnabled( true ); + QApplication::restoreOverrideCursor(); } void QgsLayoutDesignerDialog::paste() @@ -2412,7 +2576,7 @@ void QgsLayoutDesignerDialog::createAtlasWidget() QgsPrintLayout *printLayout = qobject_cast< QgsPrintLayout * >( mLayout ); QgsLayoutAtlas *atlas = printLayout->atlas(); - QgsLayoutAtlasWidget *atlasWidget = new QgsLayoutAtlasWidget( mGeneralDock, printLayout ); + QgsLayoutAtlasWidget *atlasWidget = new QgsLayoutAtlasWidget( mAtlasDock, printLayout ); atlasWidget->setMessageBar( mMessageBar ); mAtlasDock->setWidget( atlasWidget ); mAtlasDock->show(); diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index b658a7b1de4b..876d1e1913f6 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -466,8 +466,8 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f mLayout->context().setFlag( QgsLayoutContext::FlagForceVectorOutput, settings.forceVectorOutput ); QPrinter printer; - preparePrintAsPdf( printer, filePath ); - preparePrint( printer, false ); + preparePrintAsPdf( mLayout, printer, filePath ); + preparePrint( mLayout, printer, false ); QPainter p; if ( !p.begin( &printer ) ) { @@ -485,6 +485,80 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f return result; } +QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( QgsAbstractLayoutIterator *iterator, const QString &fileName, const QgsLayoutExporter::PdfExportSettings &s, QString &error, QgsFeedback *feedback ) +{ + error.clear(); + + if ( !iterator->layout() || !iterator->beginRender() ) + return IteratorError; + + PdfExportSettings settings = s; + if ( settings.dpi <= 0 ) + settings.dpi = iterator->layout()->context().dpi(); + + LayoutContextPreviewSettingRestorer restorer( iterator->layout() ); + ( void )restorer; + LayoutContextSettingsRestorer contextRestorer( iterator->layout() ); + ( void )contextRestorer; + iterator->layout()->context().setDpi( settings.dpi ); + + // If we are not printing as raster, temporarily disable advanced effects + // as QPrinter does not support composition modes and can result + // in items missing from the output + iterator->layout()->context().setFlag( QgsLayoutContext::FlagUseAdvancedEffects, !settings.forceVectorOutput ); + + iterator->layout()->context().setFlag( QgsLayoutContext::FlagForceVectorOutput, settings.forceVectorOutput ); + + QPrinter printer; + preparePrintAsPdf( iterator->layout(), printer, fileName ); + preparePrint( iterator->layout(), printer, false ); + QPainter p; + if ( !p.begin( &printer ) ) + { + //error beginning print + return PrintError; + } + + QgsLayoutExporter exporter( iterator->layout() ); + + int total = iterator->count(); + double step = total > 0 ? 100.0 / total : 100.0; + int i = 0; + bool first = true; + while ( iterator->next() ) + { + if ( feedback ) + { + feedback->setProperty( "progress", QObject::tr( "Exporting %1 of %2" ).arg( i + 1 ).arg( total ) ); + feedback->setProgress( step * i ); + } + if ( feedback && feedback->isCanceled() ) + { + iterator->endRender(); + return Canceled; + } + + ExportResult result = exporter.printPrivate( printer, p, !first, settings.dpi, settings.rasterizeWholeImage ); + if ( result != Success ) + { + if ( result == FileError ) + error = QObject::tr( "Cannot write to %1. This file may be open in another application." ).arg( fileName ); + iterator->endRender(); + return result; + } + first = false; + i++; + } + + if ( feedback ) + { + feedback->setProgress( 100 ); + } + + iterator->endRender(); + return Success; +} + QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &filePath, const QgsLayoutExporter::SvgExportSettings &s ) { if ( !mLayout ) @@ -698,7 +772,7 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( QgsAbstractLayou } -void QgsLayoutExporter::preparePrintAsPdf( QPrinter &printer, const QString &filePath ) +void QgsLayoutExporter::preparePrintAsPdf( QgsLayout *layout, QPrinter &printer, const QString &filePath ) { printer.setOutputFileName( filePath ); // setOutputFormat should come after setOutputFileName, which auto-sets format to QPrinter::PdfFormat. @@ -709,7 +783,7 @@ void QgsLayoutExporter::preparePrintAsPdf( QPrinter &printer, const QString &fil // Also an issue with PDF paper size using QPrinter::NativeFormat on Mac (always outputs portrait letter-size) printer.setOutputFormat( QPrinter::PdfFormat ); - updatePrinterPageSize( printer, 0 ); + updatePrinterPageSize( layout, printer, 0 ); // TODO: add option for this in Composer // May not work on Windows or non-X11 Linux. Works fine on Mac using QPrinter::NativeFormat @@ -718,23 +792,23 @@ void QgsLayoutExporter::preparePrintAsPdf( QPrinter &printer, const QString &fil QgsPaintEngineHack::fixEngineFlags( printer.paintEngine() ); } -void QgsLayoutExporter::preparePrint( QPrinter &printer, bool setFirstPageSize ) +void QgsLayoutExporter::preparePrint( QgsLayout *layout, QPrinter &printer, bool setFirstPageSize ) { printer.setFullPage( true ); printer.setColorMode( QPrinter::Color ); //set user-defined resolution - printer.setResolution( mLayout->context().dpi() ); + printer.setResolution( layout->context().dpi() ); if ( setFirstPageSize ) { - updatePrinterPageSize( printer, 0 ); + updatePrinterPageSize( layout, printer, 0 ); } } QgsLayoutExporter::ExportResult QgsLayoutExporter::print( QPrinter &printer ) { - preparePrint( printer, true ); + preparePrint( mLayout, printer, true ); QPainter p; if ( !p.begin( &printer ) ) { @@ -763,11 +837,7 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::printPrivate( QPrinter &print continue; } - if ( i > 0 ) - { - updatePrinterPageSize( printer, i ); - } - + updatePrinterPageSize( mLayout, printer, i ); if ( ( pageExported && i > fromPage ) || startNewPage ) { printer.newPage(); @@ -795,10 +865,7 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::printPrivate( QPrinter &print continue; } - if ( i > 0 ) - { - updatePrinterPageSize( printer, i ); - } + updatePrinterPageSize( mLayout, printer, i ); if ( ( pageExported && i > fromPage ) || startNewPage ) { @@ -811,13 +878,13 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::printPrivate( QPrinter &print return Success; } -void QgsLayoutExporter::updatePrinterPageSize( QPrinter &printer, int page ) +void QgsLayoutExporter::updatePrinterPageSize( QgsLayout *layout, QPrinter &printer, int page ) { //must set orientation to portrait before setting paper size, otherwise size will be flipped //for landscape sized outputs (#11352) printer.setOrientation( QPrinter::Portrait ); - QgsLayoutSize pageSize = mLayout->pageCollection()->page( page )->sizeWithUnits(); - QgsLayoutSize pageSizeMM = mLayout->context().measurementConverter().convert( pageSize, QgsUnitTypes::LayoutMillimeters ); + QgsLayoutSize pageSize = layout->pageCollection()->page( page )->sizeWithUnits(); + QgsLayoutSize pageSizeMM = layout->context().measurementConverter().convert( pageSize, QgsUnitTypes::LayoutMillimeters ); printer.setPaperSize( pageSizeMM.toQSizeF(), QPrinter::Millimeter ); } diff --git a/src/core/layout/qgslayoutexporter.h b/src/core/layout/qgslayoutexporter.h index 26440cd4ec15..cdf789769963 100644 --- a/src/core/layout/qgslayoutexporter.h +++ b/src/core/layout/qgslayoutexporter.h @@ -267,6 +267,20 @@ class CORE_EXPORT QgsLayoutExporter */ ExportResult exportToPdf( const QString &filePath, const QgsLayoutExporter::PdfExportSettings &settings ); + /** + * Exports a layout \a iterator to a single PDF file, with the specified export \a settings. + * + * The \a fileName argument gives the destination file name for the output PDF. + * + * Returns a result code indicating whether the export was successful or an + * error was encountered. If an error was obtained then \a error will be set + * to the error description. + */ + static ExportResult exportToPdf( QgsAbstractLayoutIterator *iterator, const QString &fileName, + const QgsLayoutExporter::PdfExportSettings &settings, + QString &error SIP_OUT, QgsFeedback *feedback = nullptr ); + + //! Contains settings relating to exporting layouts to SVG struct SvgExportSettings @@ -419,9 +433,9 @@ class CORE_EXPORT QgsLayoutExporter /** * Prepare a \a printer for printing a layout as a PDF, to the destination \a filePath. */ - void preparePrintAsPdf( QPrinter &printer, const QString &filePath ); + static void preparePrintAsPdf( QgsLayout *layout, QPrinter &printer, const QString &filePath ); - void preparePrint( QPrinter &printer, bool setFirstPageSize = false ); + static void preparePrint( QgsLayout *layout, QPrinter &printer, bool setFirstPageSize = false ); /** * Convenience function that prepares the printer and prints. @@ -438,7 +452,7 @@ class CORE_EXPORT QgsLayoutExporter */ ExportResult printPrivate( QPrinter &printer, QPainter &painter, bool startNewPage = false, double dpi = -1, bool rasterize = false ); - void updatePrinterPageSize( QPrinter &printer, int page ); + static void updatePrinterPageSize( QgsLayout *layout, QPrinter &printer, int page ); ExportResult renderToLayeredSvg( const SvgExportSettings &settings, double width, double height, int page, QRectF bounds, const QString &filename, int svgLayerId, const QString &layerName, From 7d8953f1da68b6e7f2785690a799fd70f343b1a8 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 23 Dec 2017 21:32:43 +1000 Subject: [PATCH 038/105] Restore atlas multiple pdf exports --- python/core/layout/qgslayoutexporter.sip | 16 ++++++ src/app/layout/qgslayoutdesignerdialog.cpp | 59 +++++++++++++++++++++- src/core/layout/qgslayoutexporter.cpp | 45 +++++++++++++++++ src/core/layout/qgslayoutexporter.h | 18 ++++++- 4 files changed, 136 insertions(+), 2 deletions(-) diff --git a/python/core/layout/qgslayoutexporter.sip b/python/core/layout/qgslayoutexporter.sip index 75819738bfbb..00481c0f1314 100644 --- a/python/core/layout/qgslayoutexporter.sip +++ b/python/core/layout/qgslayoutexporter.sip @@ -270,9 +270,25 @@ The ``fileName`` argument gives the destination file name for the output PDF. Returns a result code indicating whether the export was successful or an error was encountered. If an error was obtained then ``error`` will be set to the error description. + +.. seealso:: :py:func:`exportToPdfs()` %End + static ExportResult exportToPdfs( QgsAbstractLayoutIterator *iterator, const QString &baseFilePath, + const QgsLayoutExporter::PdfExportSettings &settings, + QString &error /Out/, QgsFeedback *feedback = 0 ); +%Docstring +Exports a layout ``iterator`` to multiple PDF files, with the specified export ``settings``. + +The ``baseFilePath`` argument gives a base file path, which is modified by the +iterator to obtain file paths for each iterator feature. + +Returns a result code indicating whether the export was successful or an +error was encountered. If an error was obtained then ``error`` will be set +to the error description. +.. seealso:: :py:func:`exportToPdf()` +%End struct SvgExportSettings { diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 6ae08485d846..6c20ac0a40e8 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -2351,6 +2351,63 @@ void QgsLayoutDesignerDialog::exportAtlasToPdf() } settings.setValue( QStringLiteral( "UI/lastSaveAsPdfFile" ), outputFileName ); } + else + { + if ( printAtlas->filenameExpression().isEmpty() ) + { + int res = QMessageBox::warning( nullptr, tr( "Export Atlas" ), + tr( "The filename expression is empty. A default one will be used instead." ), + QMessageBox::Ok | QMessageBox::Cancel, + QMessageBox::Ok ); + if ( res == QMessageBox::Cancel ) + { + return; + } + QString error; + printAtlas->setFilenameExpression( QStringLiteral( "'output_'||@atlas_featurenumber" ), error ); + } + + + QString lastUsedDir = settings.value( QStringLiteral( "UI/lastSaveAtlasAsPdfDir" ), QDir::homePath() ).toString(); + + QFileDialog dlg( this, tr( "Export Atlas to Directory" ) ); + dlg.setFileMode( QFileDialog::Directory ); + dlg.setOption( QFileDialog::ShowDirsOnly, true ); + dlg.setDirectory( lastUsedDir ); + if ( !dlg.exec() ) + { + return; + } + +#ifdef Q_OS_MAC + QgisApp::instance()->activateWindow(); + this->raise(); +#endif + + const QStringList files = dlg.selectedFiles(); + if ( files.empty() || files.at( 0 ).isEmpty() ) + { + return; + } + QString dir = files.at( 0 ); + if ( dir.isEmpty() ) + { + return; + } + settings.setValue( QStringLiteral( "UI/lastSaveAtlasAsPdfDir" ), dir ); + + // test directory (if it exists and is writable) + if ( !QDir( dir ).exists() || !QFileInfo( dir ).isWritable() ) + { + QMessageBox::warning( nullptr, tr( "Unable to write into the directory" ), + tr( "The given output directory is not writable. Canceling." ), + QMessageBox::Ok, + QMessageBox::Ok ); + return; + } + + outputFileName = dir; + } mView->setPaintingEnabled( false ); QApplication::setOverrideCursor( Qt::BusyCursor ); @@ -2395,7 +2452,7 @@ void QgsLayoutDesignerDialog::exportAtlasToPdf() } else { - + result = QgsLayoutExporter::exportToPdfs( printAtlas, outputFileName, pdfSettings, error, feedback.get() ); } switch ( result ) diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index 876d1e1913f6..5820ffd87730 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -559,6 +559,51 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( QgsAbstractLayou return Success; } +QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdfs( QgsAbstractLayoutIterator *iterator, const QString &baseFilePath, const QgsLayoutExporter::PdfExportSettings &settings, QString &error, QgsFeedback *feedback ) +{ + QgsLayoutExporter exporter( iterator->layout() ); + error.clear(); + + if ( !iterator->beginRender() ) + return IteratorError; + + int total = iterator->count(); + double step = total > 0 ? 100.0 / total : 100.0; + int i = 0; + while ( iterator->next() ) + { + if ( feedback ) + { + feedback->setProperty( "progress", QObject::tr( "Exporting %1 of %2" ).arg( i + 1 ).arg( total ) ); + feedback->setProgress( step * i ); + } + if ( feedback && feedback->isCanceled() ) + { + iterator->endRender(); + return Canceled; + } + + QString filePath = iterator->filePath( baseFilePath, QStringLiteral( "pdf" ) ); + ExportResult result = exporter.exportToPdf( filePath, settings ); + if ( result != Success ) + { + if ( result == FileError ) + error = QObject::tr( "Cannot write to %1. This file may be open in another application." ).arg( filePath ); + iterator->endRender(); + return result; + } + i++; + } + + if ( feedback ) + { + feedback->setProgress( 100 ); + } + + iterator->endRender(); + return Success; +} + QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &filePath, const QgsLayoutExporter::SvgExportSettings &s ) { if ( !mLayout ) diff --git a/src/core/layout/qgslayoutexporter.h b/src/core/layout/qgslayoutexporter.h index cdf789769963..4ab0da1b8045 100644 --- a/src/core/layout/qgslayoutexporter.h +++ b/src/core/layout/qgslayoutexporter.h @@ -275,12 +275,28 @@ class CORE_EXPORT QgsLayoutExporter * Returns a result code indicating whether the export was successful or an * error was encountered. If an error was obtained then \a error will be set * to the error description. + * + * \see exportToPdfs() */ static ExportResult exportToPdf( QgsAbstractLayoutIterator *iterator, const QString &fileName, const QgsLayoutExporter::PdfExportSettings &settings, QString &error SIP_OUT, QgsFeedback *feedback = nullptr ); - + /** + * Exports a layout \a iterator to multiple PDF files, with the specified export \a settings. + * + * The \a baseFilePath argument gives a base file path, which is modified by the + * iterator to obtain file paths for each iterator feature. + * + * Returns a result code indicating whether the export was successful or an + * error was encountered. If an error was obtained then \a error will be set + * to the error description. + * + * \see exportToPdf() + */ + static ExportResult exportToPdfs( QgsAbstractLayoutIterator *iterator, const QString &baseFilePath, + const QgsLayoutExporter::PdfExportSettings &settings, + QString &error SIP_OUT, QgsFeedback *feedback = nullptr ); //! Contains settings relating to exporting layouts to SVG struct SvgExportSettings From 70e7185cb55d4569fc8f81b8daba4ff4dda65d72 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 23 Dec 2017 21:40:50 +1000 Subject: [PATCH 039/105] Force a refresh after disabling view updates --- python/gui/layout/qgslayoutview.sip | 1 - src/gui/layout/qgslayoutview.cpp | 7 +++++++ src/gui/layout/qgslayoutview.h | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/python/gui/layout/qgslayoutview.sip b/python/gui/layout/qgslayoutview.sip index 161afb7f9ef2..f841e8e105a6 100644 --- a/python/gui/layout/qgslayoutview.sip +++ b/python/gui/layout/qgslayoutview.sip @@ -265,7 +265,6 @@ Returns the delta (in layout coordinates) by which to move items for the given key ``event``. %End - public slots: void zoomFull(); diff --git a/src/gui/layout/qgslayoutview.cpp b/src/gui/layout/qgslayoutview.cpp index fc8bc6a4b19b..54d63db57c0c 100644 --- a/src/gui/layout/qgslayoutview.cpp +++ b/src/gui/layout/qgslayoutview.cpp @@ -446,6 +446,13 @@ QPointF QgsLayoutView::deltaForKeyEvent( QKeyEvent *event ) return QPointF( deltaX, deltaY ); } +void QgsLayoutView::setPaintingEnabled( bool enabled ) +{ + mPaintingEnabled = enabled; + if ( enabled ) + update(); +} + void QgsLayoutView::zoomFull() { fitInView( scene()->sceneRect(), Qt::KeepAspectRatio ); diff --git a/src/gui/layout/qgslayoutview.h b/src/gui/layout/qgslayoutview.h index e42b16bff7f4..66a891762451 100644 --- a/src/gui/layout/qgslayoutview.h +++ b/src/gui/layout/qgslayoutview.h @@ -275,7 +275,7 @@ class GUI_EXPORT QgsLayoutView: public QGraphicsView * used to temporarily halt painting while exporting layouts. * \note Not available in Python bindings. */ - void setPaintingEnabled( bool enabled ) { mPaintingEnabled = enabled; } SIP_SKIP + void setPaintingEnabled( bool enabled ); SIP_SKIP public slots: From 327d311e2135264b089d6327e9f97d2c3ed8bdcc Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 28 Dec 2017 08:39:40 +1000 Subject: [PATCH 040/105] Fix broken atlas handling of z/m layer types --- src/app/layout/qgslayoutmapwidget.cpp | 27 ++++++++++++--------------- src/core/layout/qgslayoutitemmap.cpp | 14 +------------- 2 files changed, 13 insertions(+), 28 deletions(-) diff --git a/src/app/layout/qgslayoutmapwidget.cpp b/src/app/layout/qgslayoutmapwidget.cpp index 35a1c3c514a8..2f654a4b17b4 100644 --- a/src/app/layout/qgslayoutmapwidget.cpp +++ b/src/app/layout/qgslayoutmapwidget.cpp @@ -695,21 +695,18 @@ void QgsLayoutMapWidget::toggleAtlasScalingOptionsByLayerType() return; } - switch ( layer->wkbType() ) - { - case QgsWkbTypes::Point: - case QgsWkbTypes::Point25D: - case QgsWkbTypes::MultiPoint: - case QgsWkbTypes::MultiPoint25D: - //For point layers buffer setting makes no sense, so set "fixed scale" on and disable margin control - mAtlasFixedScaleRadio->setChecked( true ); - mAtlasMarginRadio->setEnabled( false ); - mAtlasPredefinedScaleRadio->setEnabled( false ); - break; - default: - //Not a point layer, so enable changes to fixed scale control - mAtlasMarginRadio->setEnabled( true ); - mAtlasPredefinedScaleRadio->setEnabled( true ); + if ( QgsWkbTypes::geometryType( layer->wkbType() ) == QgsWkbTypes::PointGeometry ) + { + //For point layers buffer setting makes no sense, so set "fixed scale" on and disable margin control + mAtlasFixedScaleRadio->setChecked( true ); + mAtlasMarginRadio->setEnabled( false ); + mAtlasPredefinedScaleRadio->setEnabled( false ); + } + else + { + //Not a point layer, so enable changes to fixed scale control + mAtlasMarginRadio->setEnabled( true ); + mAtlasPredefinedScaleRadio->setEnabled( true ); } } diff --git a/src/core/layout/qgslayoutitemmap.cpp b/src/core/layout/qgslayoutitemmap.cpp index 9f7e5ddfb578..0caf42ca7aa8 100644 --- a/src/core/layout/qgslayoutitemmap.cpp +++ b/src/core/layout/qgslayoutitemmap.cpp @@ -1825,19 +1825,7 @@ void QgsLayoutItemMap::updateAtlasFeature() QgsRectangle originalExtent = mExtent; //sanity check - only allow fixed scale mode for point layers - bool isPointLayer = false; - switch ( mLayout->context().layer()->wkbType() ) - { - case QgsWkbTypes::Point: - case QgsWkbTypes::Point25D: - case QgsWkbTypes::MultiPoint: - case QgsWkbTypes::MultiPoint25D: - isPointLayer = true; - break; - default: - isPointLayer = false; - break; - } + bool isPointLayer = QgsWkbTypes::geometryType( mLayout->context().layer()->wkbType() ) == QgsWkbTypes::PointGeometry; if ( mAtlasScalingMode == Fixed || mAtlasScalingMode == Predefined || isPointLayer ) { From 1b932319a2304048067a23cf20645bbd5f6bb7a7 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 28 Dec 2017 10:11:04 +1000 Subject: [PATCH 041/105] Fix some layout expression context handling, restore some tests --- python/core/layout/qgslayoutitem.sip | 3 + .../core/layout/qgslayoutpagecollection.sip | 1 + python/core/qgsexpressioncontext.sip | 10 +- src/core/layout/qgslayoutitem.cpp | 7 ++ src/core/layout/qgslayoutitem.h | 2 + src/core/layout/qgslayoutpagecollection.cpp | 5 + src/core/layout/qgslayoutpagecollection.h | 10 ++ src/core/qgsexpressioncontext.cpp | 116 ++++++++++++++++-- src/core/qgsexpressioncontext.h | 8 ++ tests/src/core/testqgslayoutitem.cpp | 36 ++++++ tests/src/core/testqgslayoutlabel.cpp | 75 +++++++---- 11 files changed, 241 insertions(+), 32 deletions(-) diff --git a/python/core/layout/qgslayoutitem.sip b/python/core/layout/qgslayoutitem.sip index 2d0685eeecb4..25316904ae04 100644 --- a/python/core/layout/qgslayoutitem.sip +++ b/python/core/layout/qgslayoutitem.sip @@ -979,6 +979,9 @@ Rotates the item by a specified ``angle`` in degrees clockwise around a specifie .. seealso:: :py:func:`itemRotation()` %End + virtual QgsExpressionContext createExpressionContext() const; + + signals: void frameChanged(); diff --git a/python/core/layout/qgslayoutpagecollection.sip b/python/core/layout/qgslayoutpagecollection.sip index 2961056e6a37..aa1b36af7ab6 100644 --- a/python/core/layout/qgslayoutpagecollection.sip +++ b/python/core/layout/qgslayoutpagecollection.sip @@ -59,6 +59,7 @@ A None is returned if an invalid page number is specified. .. seealso:: :py:func:`pages()` %End + int pageNumber( QgsLayoutItemPage *page ) const; %Docstring Returns the page number for the specified ``page``, or -1 if the page diff --git a/python/core/qgsexpressioncontext.sip b/python/core/qgsexpressioncontext.sip index 1be1ff1672fc..736a99bb80ce 100644 --- a/python/core/qgsexpressioncontext.sip +++ b/python/core/qgsexpressioncontext.sip @@ -1055,7 +1055,7 @@ For instance, current page name and number. static QgsExpressionContextScope *atlasScope( const QgsLayoutAtlas *atlas ) /Factory/; %Docstring -Creates a new scope which contains variables and functions relating to a QgsLayoutAtlas. +Creates a new scope which contains variables and functions relating to a :py:class:`QgsLayoutAtlas`. For instance, current page name and number. :param atlas: source atlas. If null, a set of default atlas variables will be added to the scope. @@ -1067,6 +1067,14 @@ Creates a new scope which contains variables and functions relating to a :py:cla For instance, item size and position. :param composerItem: source composer item +%End + + static QgsExpressionContextScope *layoutItemScope( const QgsLayoutItem *item ) /Factory/; +%Docstring +Creates a new scope which contains variables and functions relating to a :py:class:`QgsLayoutItem`. +For instance, item size and position. + +.. versionadded:: 3.0 %End static void setComposerItemVariable( QgsComposerItem *composerItem, const QString &name, const QVariant &value ); diff --git a/src/core/layout/qgslayoutitem.cpp b/src/core/layout/qgslayoutitem.cpp index 110ade203628..d1a5f5333b80 100644 --- a/src/core/layout/qgslayoutitem.cpp +++ b/src/core/layout/qgslayoutitem.cpp @@ -1091,6 +1091,13 @@ void QgsLayoutItem::rotateItem( const double angle, const QPointF &transformOrig refreshItemRotation( &itemTransformOrigin ); } +QgsExpressionContext QgsLayoutItem::createExpressionContext() const +{ + QgsExpressionContext context = QgsLayoutObject::createExpressionContext(); + context.appendScope( QgsExpressionContextUtils::layoutItemScope( this ) ); + return context; +} + void QgsLayoutItem::refresh() { diff --git a/src/core/layout/qgslayoutitem.h b/src/core/layout/qgslayoutitem.h index 691622d3df97..9ab297d820d9 100644 --- a/src/core/layout/qgslayoutitem.h +++ b/src/core/layout/qgslayoutitem.h @@ -886,6 +886,8 @@ class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectIt */ virtual void rotateItem( const double angle, const QPointF &transformOrigin ); + QgsExpressionContext createExpressionContext() const override; + signals: /** diff --git a/src/core/layout/qgslayoutpagecollection.cpp b/src/core/layout/qgslayoutpagecollection.cpp index d069add1417e..7e0cc7537a6b 100644 --- a/src/core/layout/qgslayoutpagecollection.cpp +++ b/src/core/layout/qgslayoutpagecollection.cpp @@ -444,6 +444,11 @@ QgsLayoutItemPage *QgsLayoutPageCollection::page( int pageNumber ) return mPages.value( pageNumber ); } +const QgsLayoutItemPage *QgsLayoutPageCollection::page( int pageNumber ) const +{ + return mPages.value( pageNumber ); +} + int QgsLayoutPageCollection::pageNumber( QgsLayoutItemPage *page ) const { return mPages.indexOf( page ); diff --git a/src/core/layout/qgslayoutpagecollection.h b/src/core/layout/qgslayoutpagecollection.h index 364674551331..08ebf7e03003 100644 --- a/src/core/layout/qgslayoutpagecollection.h +++ b/src/core/layout/qgslayoutpagecollection.h @@ -76,6 +76,16 @@ class CORE_EXPORT QgsLayoutPageCollection : public QObject, public QgsLayoutSeri */ QgsLayoutItemPage *page( int pageNumber ); + /** + * Returns a specific page (by \a pageNumber) from the collection. + * Internal page numbering starts at 0 - so a \a pageNumber of 0 + * corresponds to the first page in the collection. + * A nullptr is returned if an invalid page number is specified. + * \see pages() + * \note Not available in Python bindings. + */ + const QgsLayoutItemPage *page( int pageNumber ) const SIP_SKIP; + /** * Returns the page number for the specified \a page, or -1 if the page * is not contained in the collection. diff --git a/src/core/qgsexpressioncontext.cpp b/src/core/qgsexpressioncontext.cpp index c6e7219dc80a..e8735065b0d2 100644 --- a/src/core/qgsexpressioncontext.cpp +++ b/src/core/qgsexpressioncontext.cpp @@ -33,6 +33,7 @@ #include "qgsprocessingalgorithm.h" #include "qgslayoutatlas.h" #include "qgslayout.h" +#include "qgslayoutpagecollection.h" #include #include @@ -700,6 +701,41 @@ class GetComposerItemVariables : public QgsScopedExpressionFunction }; +class GetLayoutItemVariables : public QgsScopedExpressionFunction +{ + public: + GetLayoutItemVariables( const QgsLayout *c ) + : QgsScopedExpressionFunction( QStringLiteral( "item_variables" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "id" ) ), QStringLiteral( "Layout" ) ) + , mLayout( c ) + {} + + QVariant func( const QVariantList &values, const QgsExpressionContext *, QgsExpression *, const QgsExpressionNodeFunction * ) override + { + if ( !mLayout ) + return QVariant(); + + QString id = values.at( 0 ).toString().toLower(); + + const QgsLayoutItem *item = mLayout->itemByUuid( id ); + if ( !item ) + return QVariant(); + + QgsExpressionContext c = item->createExpressionContext(); + + return c.variablesToMap(); + } + + QgsScopedExpressionFunction *clone() const override + { + return new GetLayoutItemVariables( mLayout ); + } + + private: + + const QgsLayout *mLayout = nullptr; + +}; + class GetLayerVisibility : public QgsScopedExpressionFunction { public: @@ -1079,16 +1115,34 @@ QgsExpressionContextScope *QgsExpressionContextUtils::layoutScope( const QgsLayo //add known layout context variables scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layout_name" ), layout->name(), true ) ); -#if 0 //TODO - scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layout_numpages" ), composition->numPages(), true ) ); - scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layout_pageheight" ), composition->paperHeight(), true ) ); - scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layout_pagewidth" ), composition->paperWidth(), true ) ); + + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layout_numpages" ), layout->pageCollection()->pageCount(), true ) ); + if ( layout->pageCollection()->pageCount() > 0 ) + { + // just take first page size + QSizeF s = layout->pageCollection()->page( 0 )->sizeWithUnits().toQSizeF(); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layout_pageheight" ), s.height(), true ) ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layout_pagewidth" ), s.width(), true ) ); + } scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layout_dpi" ), layout->context().dpi(), true ) ); -#endif -#if 0 //TODO - scope->addFunction( QStringLiteral( "item_variables" ), new GetComposerItemVariables( composition ) ); -#endif + scope->addFunction( QStringLiteral( "item_variables" ), new GetLayoutItemVariables( layout ) ); + + if ( layout->context().layer() ) + { + scope->setFields( layout->context().layer()->fields() ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_layerid" ), layout->context().layer()->id(), true ) ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_layername" ), layout->context().layer()->name(), true ) ); + } + + if ( layout->context().feature().isValid() ) + { + QgsFeature atlasFeature = layout->context().feature(); + scope->setFeature( atlasFeature ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_feature" ), QVariant::fromValue( atlasFeature ), true ) ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_featureid" ), atlasFeature.id(), true ) ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_geometry" ), QVariant::fromValue( atlasFeature.geometry() ), true ) ); + } return scope.release(); } @@ -1233,6 +1287,52 @@ QgsExpressionContextScope *QgsExpressionContextUtils::composerItemScope( const Q scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "item_id" ), composerItem->id(), true ) ); scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "item_uuid" ), composerItem->uuid(), true ) ); scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layout_page" ), composerItem->page(), true ) ); + return scope; +} + +QgsExpressionContextScope *QgsExpressionContextUtils::layoutItemScope( const QgsLayoutItem *item ) +{ + QgsExpressionContextScope *scope = new QgsExpressionContextScope( QObject::tr( "Layout Item" ) ); + if ( !item ) + return scope; + + //add variables defined in composer item properties + const QStringList variableNames = item->customProperty( QStringLiteral( "variableNames" ) ).toStringList(); + const QStringList variableValues = item->customProperty( QStringLiteral( "variableValues" ) ).toStringList(); + + int varIndex = 0; + for ( const QString &variableName : variableNames ) + { + if ( varIndex >= variableValues.length() ) + { + break; + } + + QVariant varValue = variableValues.at( varIndex ); + varIndex++; + scope->setVariable( variableName, varValue ); + } + + //add known composer item context variables + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "item_id" ), item->id(), true ) ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "item_uuid" ), item->uuid(), true ) ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layout_page" ), item->page() + 1, true ) ); + + if ( item->layout() ) + { + const QgsLayoutItemPage *page = item->layout()->pageCollection()->page( item->page() ); + if ( page ) + { + const QSizeF s = page->sizeWithUnits().toQSizeF(); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layout_pageheight" ), s.height(), true ) ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layout_pagewidth" ), s.width(), true ) ); + } + else + { + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layout_pageheight" ), QVariant(), true ) ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layout_pagewidth" ), QVariant(), true ) ); + } + } return scope; } diff --git a/src/core/qgsexpressioncontext.h b/src/core/qgsexpressioncontext.h index e909714e311f..4e2ea313b85e 100644 --- a/src/core/qgsexpressioncontext.h +++ b/src/core/qgsexpressioncontext.h @@ -41,6 +41,7 @@ class QgsSymbol; class QgsProcessingAlgorithm; class QgsProcessingContext; class QgsLayoutAtlas; +class QgsLayoutItem; /** * \ingroup core @@ -943,6 +944,13 @@ class CORE_EXPORT QgsExpressionContextUtils */ static QgsExpressionContextScope *composerItemScope( const QgsComposerItem *composerItem ) SIP_FACTORY; + /** + * Creates a new scope which contains variables and functions relating to a QgsLayoutItem. + * For instance, item size and position. + * \since QGIS 3.0 + */ + static QgsExpressionContextScope *layoutItemScope( const QgsLayoutItem *item ) SIP_FACTORY; + /** * Sets a composer item context variable. This variable will be contained within scopes retrieved via * composerItemScope(). diff --git a/tests/src/core/testqgslayoutitem.cpp b/tests/src/core/testqgslayoutitem.cpp index 19ab7c1c2bc2..9b05cc9e672a 100644 --- a/tests/src/core/testqgslayoutitem.cpp +++ b/tests/src/core/testqgslayoutitem.cpp @@ -168,6 +168,7 @@ class TestQgsLayoutItem: public QObject void excludeFromExports(); void setSceneRect(); void page(); + void itemVariablesFunction(); private: @@ -1386,6 +1387,41 @@ void TestQgsLayoutItem::page() QCOMPARE( item->positionWithUnits(), QgsLayoutPoint( 5, 38, QgsUnitTypes::LayoutCentimeters ) ); } +void TestQgsLayoutItem::itemVariablesFunction() +{ + QgsRectangle extent( 2000, 2800, 2500, 2900 ); + QgsLayout l( QgsProject::instance() ); + + QgsExpression e( QStringLiteral( "map_get( item_variables( 'map_id' ), 'map_scale' )" ) ); + // no map + QgsExpressionContext c = l.createExpressionContext(); + QVariant r = e.evaluate( &c ); + QVERIFY( !r.isValid() ); + + QgsLayoutItemMap *map = new QgsLayoutItemMap( &l ); + map->setExtent( extent ); + map->attemptSetSceneRect( QRectF( 30, 60, 200, 100 ) ); + map->setCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) ); + l.addLayoutItem( map ); + map->setId( QStringLiteral( "map_id" ) ); + + c = l.createExpressionContext(); + r = e.evaluate( &c ); + QGSCOMPARENEAR( r.toDouble(), 1.38916e+08, 100 ); + + QgsExpression e2( QStringLiteral( "map_get( item_variables( 'map_id' ), 'map_crs' )" ) ); + r = e2.evaluate( &c ); + QCOMPARE( r.toString(), QString( "EPSG:4326" ) ); + + QgsExpression e3( QStringLiteral( "map_get( item_variables( 'map_id' ), 'map_crs_definition' )" ) ); + r = e3.evaluate( &c ); + QCOMPARE( r.toString(), QString( "+proj=longlat +datum=WGS84 +no_defs" ) ); + + QgsExpression e4( QStringLiteral( "map_get( item_variables( 'map_id' ), 'map_units' )" ) ); + r = e4.evaluate( &c ); + QCOMPARE( r.toString(), QString( "degrees" ) ); +} + void TestQgsLayoutItem::rotation() { QgsProject proj; diff --git a/tests/src/core/testqgslayoutlabel.cpp b/tests/src/core/testqgslayoutlabel.cpp index 53aa95f7f79b..39beb601e89d 100644 --- a/tests/src/core/testqgslayoutlabel.cpp +++ b/tests/src/core/testqgslayoutlabel.cpp @@ -23,6 +23,9 @@ #include "qgsmultirenderchecker.h" #include "qgsfontutils.h" #include "qgsproject.h" +#include "qgsprintlayout.h" +#include "qgslayoutatlas.h" +#include "qgslayoutpagecollection.h" #include #include "qgstest.h" @@ -44,6 +47,7 @@ class TestQgsLayoutLabel : public QObject void evaluation(); // test expression evaluation when a feature is set void feature_evaluation(); + void feature_evaluation2(); // test page expressions void page_evaluation(); void marginMethods(); //tests getting/setting margins @@ -98,9 +102,7 @@ void TestQgsLayoutLabel::evaluation() QgsLayout l( QgsProject::instance() ); l.initializeDefaults(); -#if 0 //TODO - l.atlasComposition().setCoverageLayer( mVectorLayer ); -#endif + l.context().setLayer( mVectorLayer ); QgsLayoutItemLabel *label = new QgsLayoutItemLabel( &l ); label->setMargin( 1 ); @@ -145,64 +147,91 @@ void TestQgsLayoutLabel::evaluation() void TestQgsLayoutLabel::feature_evaluation() { + QgsPrintLayout l( QgsProject::instance() ); + l.initializeDefaults(); + + QgsLayoutItemLabel *label = new QgsLayoutItemLabel( &l ); + label->setMargin( 1 ); + l.addLayoutItem( label ); + + l.atlas()->setEnabled( true ); + l.atlas()->setCoverageLayer( mVectorLayer ); + QVERIFY( l.atlas()->beginRender() ); + l.atlas()->seekTo( 0 ); + { + // evaluation with a feature + label->setText( QStringLiteral( "[%\"NAME_1\"||'_ok'%]" ) ); + QString evaluated = label->currentText(); + QString expected = QStringLiteral( "Basse-Normandie_ok" ); + QCOMPARE( evaluated, expected ); + } + l.atlas()->seekTo( 1 ); + { + // evaluation with a feature + label->setText( QStringLiteral( "[%\"NAME_1\"||'_ok'%]" ) ); + QString evaluated = label->currentText(); + QString expected = QStringLiteral( "Bretagne_ok" ); + QCOMPARE( evaluated, expected ); + } +} + +void TestQgsLayoutLabel::feature_evaluation2() +{ + // just using context, no atlas QgsLayout l( QgsProject::instance() ); l.initializeDefaults(); -#if 0 //TODO - l.atlasComposition().setCoverageLayer( mVectorLayer ); -#endif QgsLayoutItemLabel *label = new QgsLayoutItemLabel( &l ); label->setMargin( 1 ); l.addLayoutItem( label ); -#if 0 //TODO - l.atlasComposition().setEnabled( true ); - l.setAtlasMode( QgsComposition::ExportAtlas ); - l.atlasComposition().updateFeatures(); - l.atlasComposition().prepareForFeature( 0 ); + QgsFeature f; + QgsFeatureIterator it = mVectorLayer->getFeatures(); + l.context().setLayer( mVectorLayer ); + it.nextFeature( f ); + l.context().setFeature( f ); { // evaluation with a feature label->setText( QStringLiteral( "[%\"NAME_1\"||'_ok'%]" ) ); - QString evaluated = label->displayText(); + QString evaluated = label->currentText(); QString expected = QStringLiteral( "Basse-Normandie_ok" ); QCOMPARE( evaluated, expected ); } - mComposition->atlasComposition().prepareForFeature( 1 ); + it.nextFeature( f ); + l.context().setFeature( f ); { // evaluation with a feature label->setText( QStringLiteral( "[%\"NAME_1\"||'_ok'%]" ) ); - QString evaluated = label->displayText(); + QString evaluated = label->currentText(); QString expected = QStringLiteral( "Bretagne_ok" ); QCOMPARE( evaluated, expected ); } - mComposition->atlasComposition().setEnabled( false ); -#endif } void TestQgsLayoutLabel::page_evaluation() { -#if 0 //TODO QgsLayout l( QgsProject::instance() ); l.initializeDefaults(); - mComposition->atlasComposition().setCoverageLayer( mVectorLayer ); + QgsLayoutItemPage *page2 = new QgsLayoutItemPage( &l ); + page2->setPageSize( "A4", QgsLayoutItemPage::Landscape ); + l.pageCollection()->addPage( page2 ); + l.context().setLayer( mVectorLayer ); QgsLayoutItemLabel *label = new QgsLayoutItemLabel( &l ); label->setMargin( 1 ); l.addLayoutItem( label ); - mComposition->setNumPages( 2 ); { label->setText( QStringLiteral( "[%@layout_page||'/'||@layout_numpages%]" ) ); - QString evaluated = label->displayText(); + QString evaluated = label->currentText(); QString expected = QStringLiteral( "1/2" ); QCOMPARE( evaluated, expected ); // move to the second page and re-evaluate - label->setItemPosition( 0, 320 ); - QCOMPARE( label->displayText(), QString( "2/2" ) ); + label->attemptMove( QgsLayoutPoint( 0, 320 ) ); + QCOMPARE( label->currentText(), QString( "2/2" ) ); } -#endif } void TestQgsLayoutLabel::marginMethods() From 3d03128e4aa78d0989aa817ff88b46503553ff4a Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 28 Dec 2017 10:27:05 +1000 Subject: [PATCH 042/105] Restore some more atlas related tests --- tests/src/core/testqgslayouthtml.cpp | 20 +++---- tests/src/core/testqgslayoutmap.cpp | 20 +++---- tests/src/core/testqgslayouttable.cpp | 81 +++++++++++++-------------- 3 files changed, 56 insertions(+), 65 deletions(-) diff --git a/tests/src/core/testqgslayouthtml.cpp b/tests/src/core/testqgslayouthtml.cpp index ea5e88dff93b..113a4b556a96 100644 --- a/tests/src/core/testqgslayouthtml.cpp +++ b/tests/src/core/testqgslayouthtml.cpp @@ -268,10 +268,10 @@ void TestQgsLayoutHtml::javascriptSetFeature() QgsProject::instance()->addMapLayers( QList() << childLayer << parentLayer ); -#if 0 //TODO //atlas - mComposition->atlasComposition().setCoverageLayer( parentLayer ); - mComposition->atlasComposition().setEnabled( true ); + QgsLayout l( QgsProject::instance() ); + l.initializeDefaults(); + l.context().setLayer( parentLayer ); QgsRelation rel; rel.setId( QStringLiteral( "rel1" ) ); @@ -281,8 +281,6 @@ void TestQgsLayoutHtml::javascriptSetFeature() rel.addFieldPair( QStringLiteral( "y" ), QStringLiteral( "foreignkey" ) ); QgsProject::instance()->relationManager()->addRelation( rel ); - QgsLayout l( QgsProject::instance() ); - l.initializeDefaults(); QgsLayoutItemHtml *htmlItem = new QgsLayoutItemHtml( &l ); QgsLayoutFrame *htmlFrame = new QgsLayoutFrame( &l, htmlItem ); htmlFrame->attemptSetSceneRect( QRectF( 0, 0, 100, 200 ) ); @@ -298,21 +296,19 @@ void TestQgsLayoutHtml::javascriptSetFeature() " feature.properties['relation one'][0].z + ',' + feature.properties['relation one'][1].z;}" "" ) ); - mComposition->setAtlasMode( QgsComposition::ExportAtlas ); - QVERIFY( mComposition->atlasComposition().beginRender() ); - QVERIFY( mComposition->atlasComposition().prepareForFeature( 0 ) ); + QgsFeature f; + QgsFeatureIterator it = parentLayer->getFeatures(); + it.nextFeature( f ); + l.context().setFeature( f ); htmlItem->loadHtml(); - QgsLayoutChecker checker( QStringLiteral( "composerhtml_setfeature" ), mComposition ); + QgsLayoutChecker checker( QStringLiteral( "composerhtml_setfeature" ), &l ); checker.setControlPathPrefix( QStringLiteral( "composer_html" ) ); bool result = checker.testLayout( mReport ); - mComposition->removeMultiFrame( htmlItem ); - delete htmlItem; QVERIFY( result ); QgsProject::instance()->removeMapLayers( QList() << childLayer << parentLayer ); -#endif } diff --git a/tests/src/core/testqgslayoutmap.cpp b/tests/src/core/testqgslayoutmap.cpp index 1b7de1cbb6d4..5589b031f8ba 100644 --- a/tests/src/core/testqgslayoutmap.cpp +++ b/tests/src/core/testqgslayoutmap.cpp @@ -340,7 +340,6 @@ void TestQgsLayoutMap::dataDefinedLayers() result = map->layersToRender(); QVERIFY( result.isEmpty() ); - //test with atlas feature evaluation QgsVectorLayer *atlasLayer = new QgsVectorLayer( QStringLiteral( "Point?field=col1:string" ), QStringLiteral( "atlas" ), QStringLiteral( "memory" ) ); QVERIFY( atlasLayer->isValid() ); @@ -349,23 +348,24 @@ void TestQgsLayoutMap::dataDefinedLayers() QgsFeature f2( atlasLayer->dataProvider()->fields(), 1 ); f2.setAttribute( QStringLiteral( "col1" ), mPointsLayer->name() ); atlasLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 ); -#if 0 //TODO - mComposition->atlasComposition().setCoverageLayer( atlasLayer ); - mComposition->atlasComposition().setEnabled( true ); - mComposition->setAtlasMode( QgsComposition::ExportAtlas ); - mComposition->atlasComposition().beginRender(); - mComposition->atlasComposition().prepareForFeature( 0 ); + + l.context().setLayer( atlasLayer ); + QgsFeature f; + QgsFeatureIterator it = atlasLayer->getFeatures(); + it.nextFeature( f ); + l.context().setFeature( f ); map->dataDefinedProperties().setProperty( QgsLayoutObject::MapLayers, QgsProperty::fromField( QStringLiteral( "col1" ) ) ); result = map->layersToRender(); QCOMPARE( result.count(), 1 ); QCOMPARE( result.at( 0 ), mLinesLayer ); - mComposition->atlasComposition().prepareForFeature( 1 ); + it.nextFeature( f ); + l.context().setFeature( f ); result = map->layersToRender(); QCOMPARE( result.count(), 1 ); QCOMPARE( result.at( 0 ), mPointsLayer ); - mComposition->atlasComposition().setEnabled( false ); -#endif + it.nextFeature( f ); + l.context().setFeature( f ); delete atlasLayer; diff --git a/tests/src/core/testqgslayouttable.cpp b/tests/src/core/testqgslayouttable.cpp index 23a2dd22edae..526e94138217 100644 --- a/tests/src/core/testqgslayouttable.cpp +++ b/tests/src/core/testqgslayouttable.cpp @@ -521,9 +521,8 @@ void TestQgsLayoutTable::attributeTableRepeat() void TestQgsLayoutTable::attributeTableAtlasSource() { -#if 0 //TODO - QgsLayoutItemAttributeTable *table = new QgsLayoutItemAttributeTable( mComposition, false ); - + QgsLayout l( QgsProject::instance() ); + QgsLayoutItemAttributeTable *table = new QgsLayoutItemAttributeTable( &l ); table->setSource( QgsLayoutItemAttributeTable::AtlasFeature ); @@ -534,13 +533,15 @@ void TestQgsLayoutTable::attributeTableAtlasSource() vectorFileInfo.completeBaseName(), QStringLiteral( "ogr" ) ); QgsProject::instance()->addMapLayer( vectorLayer ); - mComposition->atlasComposition().setCoverageLayer( vectorLayer ); - mComposition->atlasComposition().setEnabled( true ); - QVERIFY( mComposition->atlasComposition().beginRender() ); + l.context().setLayer( vectorLayer ); - QVERIFY( mComposition->atlasComposition().prepareForFeature( 0 ) ); - QCOMPARE( table->contents()->length(), 1 ); - QgsComposerTableRow row = table->contents()->at( 0 ); + QgsFeature f; + QgsFeatureIterator it = vectorLayer->getFeatures(); + it.nextFeature( f ); + l.context().setFeature( f ); + + QCOMPARE( table->contents().length(), 1 ); + QgsLayoutTableRow row = table->contents().at( 0 ); //check a couple of results QCOMPARE( row.at( 0 ), QVariant( "Jet" ) ); @@ -551,9 +552,11 @@ void TestQgsLayoutTable::attributeTableAtlasSource() QCOMPARE( row.at( 5 ), QVariant( 2 ) ); //next atlas feature - QVERIFY( mComposition->atlasComposition().prepareForFeature( 1 ) ); - QCOMPARE( table->contents()->length(), 1 ); - row = table->contents()->at( 0 ); + it.nextFeature( f ); + l.context().setFeature( f ); + + QCOMPARE( table->contents().length(), 1 ); + row = table->contents().at( 0 ); QCOMPARE( row.at( 0 ), QVariant( "Biplane" ) ); QCOMPARE( row.at( 1 ), QVariant( 0 ) ); QCOMPARE( row.at( 2 ), QVariant( 1 ) ); @@ -562,9 +565,11 @@ void TestQgsLayoutTable::attributeTableAtlasSource() QCOMPARE( row.at( 5 ), QVariant( 6 ) ); //next atlas feature - QVERIFY( mComposition->atlasComposition().prepareForFeature( 2 ) ); - QCOMPARE( table->contents()->length(), 1 ); - row = table->contents()->at( 0 ); + it.nextFeature( f ); + l.context().setFeature( f ); + + QCOMPARE( table->contents().length(), 1 ); + row = table->contents().at( 0 ); QCOMPARE( row.at( 0 ), QVariant( "Jet" ) ); QCOMPARE( row.at( 1 ), QVariant( 85 ) ); QCOMPARE( row.at( 2 ), QVariant( 3 ) ); @@ -572,15 +577,10 @@ void TestQgsLayoutTable::attributeTableAtlasSource() QCOMPARE( row.at( 4 ), QVariant( 1 ) ); QCOMPARE( row.at( 5 ), QVariant( 2 ) ); - mComposition->atlasComposition().endRender(); - //try for a crash when removing current atlas layer QgsProject::instance()->removeMapLayer( vectorLayer->id() ); table->refreshAttributes(); - mComposition->removeMultiFrame( table ); - delete table; -#endif } @@ -611,10 +611,13 @@ void TestQgsLayoutTable::attributeTableRelationSource() QgsProject::instance()->addMapLayer( atlasLayer ); -#if 0 //TODO //setup atlas - mComposition->atlasComposition().setCoverageLayer( atlasLayer ); - mComposition->atlasComposition().setEnabled( true ); + l.context().setLayer( atlasLayer ); + + QgsFeature f; + QgsFeatureIterator it = atlasLayer->getFeatures(); + it.nextFeature( f ); + l.context().setFeature( f ); //create a relation QgsRelation relation; @@ -624,18 +627,15 @@ void TestQgsLayoutTable::attributeTableRelationSource() relation.addFieldPair( QStringLiteral( "Class" ), QStringLiteral( "Class" ) ); QgsProject::instance()->relationManager()->addRelation( relation ); - QgsLayoutItemAttributeTable *table = new QgsLayoutItemAttributeTable( mComposition, false ); + table = new QgsLayoutItemAttributeTable( &l ); table->setMaximumNumberOfFeatures( 50 ); table->setSource( QgsLayoutItemAttributeTable::RelationChildren ); table->setRelationId( relation.id() ); - QVERIFY( mComposition->atlasComposition().beginRender() ); - QVERIFY( mComposition->atlasComposition().prepareForFeature( 0 ) ); + QCOMPARE( f.attribute( "Class" ).toString(), QString( "Jet" ) ); + QCOMPARE( table->contents().length(), 8 ); - QCOMPARE( mComposition->atlasComposition().feature().attribute( "Class" ).toString(), QString( "Jet" ) ); - QCOMPARE( table->contents()->length(), 8 ); - - QgsComposerTableRow row = table->contents()->at( 0 ); + QgsLayoutTableRow row = table->contents().at( 0 ); //check a couple of results QCOMPARE( row.at( 0 ), QVariant( "Jet" ) ); @@ -644,14 +644,14 @@ void TestQgsLayoutTable::attributeTableRelationSource() QCOMPARE( row.at( 3 ), QVariant( 2 ) ); QCOMPARE( row.at( 4 ), QVariant( 0 ) ); QCOMPARE( row.at( 5 ), QVariant( 2 ) ); - row = table->contents()->at( 1 ); + row = table->contents().at( 1 ); QCOMPARE( row.at( 0 ), QVariant( "Jet" ) ); QCOMPARE( row.at( 1 ), QVariant( 85 ) ); QCOMPARE( row.at( 2 ), QVariant( 3 ) ); QCOMPARE( row.at( 3 ), QVariant( 1 ) ); QCOMPARE( row.at( 4 ), QVariant( 1 ) ); QCOMPARE( row.at( 5 ), QVariant( 2 ) ); - row = table->contents()->at( 2 ); + row = table->contents().at( 2 ); QCOMPARE( row.at( 0 ), QVariant( "Jet" ) ); QCOMPARE( row.at( 1 ), QVariant( 95 ) ); QCOMPARE( row.at( 2 ), QVariant( 3 ) ); @@ -660,17 +660,18 @@ void TestQgsLayoutTable::attributeTableRelationSource() QCOMPARE( row.at( 5 ), QVariant( 2 ) ); //next atlas feature - QVERIFY( mComposition->atlasComposition().prepareForFeature( 1 ) ); - QCOMPARE( mComposition->atlasComposition().feature().attribute( "Class" ).toString(), QString( "Biplane" ) ); - QCOMPARE( table->contents()->length(), 5 ); - row = table->contents()->at( 0 ); + it.nextFeature( f ); + l.context().setFeature( f ); + QCOMPARE( f.attribute( "Class" ).toString(), QString( "Biplane" ) ); + QCOMPARE( table->contents().length(), 5 ); + row = table->contents().at( 0 ); QCOMPARE( row.at( 0 ), QVariant( "Biplane" ) ); QCOMPARE( row.at( 1 ), QVariant( 0 ) ); QCOMPARE( row.at( 2 ), QVariant( 1 ) ); QCOMPARE( row.at( 3 ), QVariant( 3 ) ); QCOMPARE( row.at( 4 ), QVariant( 3 ) ); QCOMPARE( row.at( 5 ), QVariant( 6 ) ); - row = table->contents()->at( 1 ); + row = table->contents().at( 1 ); QCOMPARE( row.at( 0 ), QVariant( "Biplane" ) ); QCOMPARE( row.at( 1 ), QVariant( 340 ) ); QCOMPARE( row.at( 2 ), QVariant( 1 ) ); @@ -678,16 +679,10 @@ void TestQgsLayoutTable::attributeTableRelationSource() QCOMPARE( row.at( 4 ), QVariant( 3 ) ); QCOMPARE( row.at( 5 ), QVariant( 6 ) ); - mComposition->atlasComposition().endRender(); - //try for a crash when removing current atlas layer QgsProject::instance()->removeMapLayer( atlasLayer->id() ); table->refreshAttributes(); - - mComposition->removeMultiFrame( table ); - delete table; -#endif } void TestQgsLayoutTable::contentsContainsRow() From ec67ddfc0e04e28142a8a26563eb93dc5400d3f3 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 28 Dec 2017 11:58:32 +1000 Subject: [PATCH 043/105] Restore more atlas tests --- src/core/layout/qgslayoutitemmap.cpp | 1 + tests/src/core/CMakeLists.txt | 1 + tests/src/core/testqgslayoutatlas.cpp | 428 ++++++++++++++++++ .../expected_atlas_autoscale1_mask.png | Bin 32430 -> 32432 bytes .../expected_atlas_autoscale2_mask.png | Bin 36273 -> 36276 bytes .../expected_atlas_filtering1_mask.png | Bin 35754 -> 35755 bytes .../expected_atlas_fixedscale1_mask.png | Bin 33739 -> 33741 bytes .../expected_atlas_fixedscale2_mask.png | Bin 34884 -> 34885 bytes .../expected_atlas_hiding1_mask.png | Bin 12733 -> 12735 bytes .../expected_atlas_hiding2_mask.png | Bin 10939 -> 10941 bytes .../expected_atlas_predefinedscales1_mask.png | Bin 32178 -> 32180 bytes .../expected_atlas_predefinedscales2_mask.png | Bin 31337 -> 31338 bytes .../expected_atlas_sorting1_mask.png | Bin 35849 -> 35852 bytes .../expected_atlas_sorting2_mask.png | Bin 29206 -> 29211 bytes .../expected_atlas_two_maps1_mask.png | Bin 32732 -> 32734 bytes .../expected_atlas_two_maps2_mask.png | Bin 34750 -> 34752 bytes 16 files changed, 430 insertions(+) create mode 100644 tests/src/core/testqgslayoutatlas.cpp diff --git a/src/core/layout/qgslayoutitemmap.cpp b/src/core/layout/qgslayoutitemmap.cpp index 0caf42ca7aa8..0c8745062b2e 100644 --- a/src/core/layout/qgslayoutitemmap.cpp +++ b/src/core/layout/qgslayoutitemmap.cpp @@ -1917,6 +1917,7 @@ void QgsLayoutItemMap::updateAtlasFeature() // set the new extent (and render) setExtent( newExtent ); + emit preparedForAtlas(); } QgsRectangle QgsLayoutItemMap::computeAtlasRectangle() diff --git a/tests/src/core/CMakeLists.txt b/tests/src/core/CMakeLists.txt index d6546c14cab2..9e4c3a66565f 100755 --- a/tests/src/core/CMakeLists.txt +++ b/tests/src/core/CMakeLists.txt @@ -134,6 +134,7 @@ SET(TESTS testqgslabelingengine.cpp testqgslayertree.cpp testqgslayout.cpp + testqgslayoutatlas.cpp testqgslayoutcontext.cpp testqgslayouthtml.cpp testqgslayoutitem.cpp diff --git a/tests/src/core/testqgslayoutatlas.cpp b/tests/src/core/testqgslayoutatlas.cpp new file mode 100644 index 000000000000..3286e618186f --- /dev/null +++ b/tests/src/core/testqgslayoutatlas.cpp @@ -0,0 +1,428 @@ +/*************************************************************************** + testqgslayoutatlas.cpp + --------------------------- + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsapplication.h" +#include "qgslayout.h" +#include "qgsmultirenderchecker.h" +#include "qgslayoutitemmap.h" +#include "qgslayoutitemmapoverview.h" +#include "qgslayoutatlas.h" +#include "qgslayoutitemlabel.h" +#include "qgsproject.h" +#include "qgsvectorlayer.h" +#include "qgsvectordataprovider.h" +#include "qgssymbol.h" +#include "qgssinglesymbolrenderer.h" +#include "qgsfontutils.h" +#include "qgsprintlayout.h" +#include +#include +#include "qgstest.h" + +class TestQgsLayoutAtlas : public QObject +{ + Q_OBJECT + + public: + TestQgsLayoutAtlas() = default; + + private slots: + void initTestCase();// will be called before the first testfunction is executed. + void cleanupTestCase();// will be called after the last testfunction was executed. + void init();// will be called before each testfunction is executed. + void cleanup();// will be called after every testfunction. + + // test filename pattern evaluation + void filename(); + // test rendering with an autoscale atlas + void autoscale_render(); + // test rendering with a fixed scale atlas + void fixedscale_render(); + // test rendering with predefined scales + void predefinedscales_render(); + // test rendering with two atlas-driven maps + void two_map_autoscale_render(); + // test rendering with a hidden coverage + void hiding_render(); + // test rendering with feature sorting + void sorting_render(); + // test rendering with feature filtering + void filtering_render(); + // test render signals + void test_signals(); + // test removing coverage layer while atlas is enabled + void test_remove_layer(); + + private: + QgsPrintLayout *mLayout = nullptr; + QgsLayoutItemLabel *mLabel1 = nullptr; + QgsLayoutItemLabel *mLabel2 = nullptr; + QgsLayoutItemMap *mAtlasMap = nullptr; + QgsLayoutItemMap *mOverview = nullptr; + QgsVectorLayer *mVectorLayer = nullptr; + QgsVectorLayer *mVectorLayer2 = nullptr; + QgsLayoutAtlas *mAtlas = nullptr; + QString mReport; +}; + +void TestQgsLayoutAtlas::initTestCase() +{ + QgsApplication::init(); + QgsApplication::initQgis(); + + //create maplayers from testdata and add to layer registry + QFileInfo vectorFileInfo( QStringLiteral( TEST_DATA_DIR ) + "/france_parts.shp" ); + mVectorLayer = new QgsVectorLayer( vectorFileInfo.filePath(), + vectorFileInfo.completeBaseName(), + QStringLiteral( "ogr" ) ); + mVectorLayer2 = new QgsVectorLayer( vectorFileInfo.filePath(), + vectorFileInfo.completeBaseName(), + QStringLiteral( "ogr" ) ); + + QgsVectorSimplifyMethod simplifyMethod; + simplifyMethod.setSimplifyHints( QgsVectorSimplifyMethod::NoSimplification ); + mVectorLayer->setSimplifyMethod( simplifyMethod ); + + mReport = QStringLiteral( "

Composer Atlas Tests

\n" ); +} + +void TestQgsLayoutAtlas::cleanupTestCase() +{ + delete mLayout; + delete mVectorLayer; + QgsApplication::exitQgis(); + + QString myReportFile = QDir::tempPath() + "/qgistest.html"; + QFile myFile( myReportFile ); + if ( myFile.open( QIODevice::WriteOnly | QIODevice::Append ) ) + { + QTextStream myQTextStream( &myFile ); + myQTextStream << mReport; + myFile.close(); + } +} + +void TestQgsLayoutAtlas::init() +{ + //create composition with composer map + + // select epsg:2154 + QgsCoordinateReferenceSystem crs; + crs.createFromSrid( 2154 ); + QgsProject::instance()->setCrs( crs ); + mLayout = new QgsPrintLayout( QgsProject::instance() ); + mLayout->initializeDefaults(); + + // fix the renderer, fill with green + QgsStringMap props; + props.insert( QStringLiteral( "color" ), QStringLiteral( "0,127,0" ) ); + QgsFillSymbol *fillSymbol = QgsFillSymbol::createSimple( props ); + QgsSingleSymbolRenderer *renderer = new QgsSingleSymbolRenderer( fillSymbol ); + mVectorLayer->setRenderer( renderer ); + + // the atlas map + mAtlasMap = new QgsLayoutItemMap( mLayout ); + mAtlasMap->attemptSetSceneRect( QRectF( 20, 20, 130, 130 ) ); + mAtlasMap->setFrameEnabled( true ); + mLayout->addLayoutItem( mAtlasMap ); + mAtlasMap->setLayers( QList() << mVectorLayer ); + + mAtlas = mLayout->atlas(); + mAtlas->setCoverageLayer( mVectorLayer ); + mAtlas->setEnabled( true ); + + // an overview + mOverview = new QgsLayoutItemMap( mLayout ); + mOverview->attemptSetSceneRect( QRectF( 180, 20, 50, 50 ) ); + mOverview->setFrameEnabled( true ); + mOverview->overview()->setFrameMap( mAtlasMap ); + mOverview->setLayers( QList() << mVectorLayer ); + mLayout->addLayoutItem( mOverview ); + mOverview->setExtent( QgsRectangle( 49670.718, 6415139.086, 699672.519, 7065140.887 ) ); + + // set the fill symbol of the overview map + QgsStringMap props2; + props2.insert( QStringLiteral( "color" ), QStringLiteral( "127,0,0,127" ) ); + QgsFillSymbol *fillSymbol2 = QgsFillSymbol::createSimple( props2 ); + mOverview->overview()->setFrameSymbol( fillSymbol2 ); + + // header label + mLabel1 = new QgsLayoutItemLabel( mLayout ); + mLayout->addLayoutItem( mLabel1 ); + mLabel1->setText( QStringLiteral( "[% \"NAME_1\" %] area" ) ); + mLabel1->setFont( QgsFontUtils::getStandardTestFont() ); + mLabel1->setMarginX( 1 ); + mLabel1->setMarginY( 1 ); + //need to explicitly set width, since expression hasn't been evaluated against + //an atlas feature yet and will be shorter than required + mLabel1->attemptSetSceneRect( QRectF( 150, 5, 60, 15 ) ); + + // feature number label + mLabel2 = new QgsLayoutItemLabel( mLayout ); + mLayout->addLayoutItem( mLabel2 ); + mLabel2->setText( QStringLiteral( "# [%@atlas_featurenumber || ' / ' || @atlas_totalfeatures%]" ) ); + mLabel2->setFont( QgsFontUtils::getStandardTestFont() ); + mLabel2->attemptSetSceneRect( QRectF( 150, 200, 60, 15 ) ); + mLabel2->setMarginX( 1 ); + mLabel2->setMarginY( 1 ); + + + qDebug() << "header label font: " << mLabel1->font().toString() << " exactMatch:" << mLabel1->font().exactMatch(); + qDebug() << "feature number label font: " << mLabel2->font().toString() << " exactMatch:" << mLabel2->font().exactMatch(); +} + +void TestQgsLayoutAtlas::cleanup() +{ + delete mLayout; + mLayout = nullptr; +} + +void TestQgsLayoutAtlas::filename() +{ + QString error; + mAtlas->setFilenameExpression( QStringLiteral( "'output_' || @atlas_featurenumber" ), error ); + mAtlas->beginRender(); + for ( int fi = 0; fi < mAtlas->count(); ++fi ) + { + mAtlas->seekTo( fi ); + QString expected = QStringLiteral( "output_%1" ).arg( ( int )( fi + 1 ) ); + QCOMPARE( mAtlas->currentFilename(), expected ); + } + mAtlas->endRender(); +} + + +void TestQgsLayoutAtlas::autoscale_render() +{ + mAtlasMap->setExtent( QgsRectangle( 332719.06221504929, 6765214.5887386119, 560957.85090677091, 6993453.3774303338 ) ); + mAtlasMap->setAtlasDriven( true ); + mAtlasMap->setAtlasScalingMode( QgsLayoutItemMap::Auto ); + mAtlasMap->setAtlasMargin( 0.10 ); + + mAtlas->beginRender(); + + for ( int fit = 0; fit < 2; ++fit ) + { + mAtlas->seekTo( fit ); + mLabel1->adjustSizeToText(); + + QgsLayoutChecker checker( QStringLiteral( "atlas_autoscale%1" ).arg( ( ( int )fit ) + 1 ), mLayout ); + checker.setControlPathPrefix( QStringLiteral( "atlas" ) ); + QVERIFY( checker.testLayout( mReport, 0, 100 ) ); + } + mAtlas->endRender(); +} + +void TestQgsLayoutAtlas::fixedscale_render() +{ + //TODO QGIS3.0 - setting the extent AFTER setting atlas driven/fixed scaling mode should + //also update the set fixed scale + mAtlasMap->setExtent( QgsRectangle( 209838.166, 6528781.020, 610491.166, 6920530.620 ) ); + mAtlasMap->setAtlasDriven( true ); + mAtlasMap->setAtlasScalingMode( QgsLayoutItemMap::Fixed ); + + mAtlas->beginRender(); + + for ( int fit = 0; fit < 2; ++fit ) + { + mAtlas->seekTo( fit ); + mLabel1->adjustSizeToText(); + + QgsLayoutChecker checker( QStringLiteral( "atlas_fixedscale%1" ).arg( ( ( int )fit ) + 1 ), mLayout ); + checker.setControlPathPrefix( QStringLiteral( "atlas" ) ); + QVERIFY( checker.testLayout( mReport, 0, 100 ) ); + } + mAtlas->endRender(); +} + +void TestQgsLayoutAtlas::predefinedscales_render() +{ + //TODO QGIS3.0 - setting the extent AFTER setting atlas driven/predefined scaling mode should + //also update the atlas map scale + mAtlasMap->setExtent( QgsRectangle( 209838.166, 6528781.020, 610491.166, 6920530.620 ) ); + mAtlasMap->setAtlasDriven( true ); + mAtlasMap->setAtlasScalingMode( QgsLayoutItemMap::Predefined ); + + QVector scales; + scales << 1800000.0; + scales << 5000000.0; + mLayout->context().setPredefinedScales( scales ); + { + const QVector &setScales = mLayout->context().predefinedScales(); + for ( int i = 0; i < setScales.size(); i++ ) + { + QVERIFY( setScales[i] == scales[i] ); + } + } + + mAtlas->beginRender(); + + for ( int fit = 0; fit < 2; ++fit ) + { + mAtlas->seekTo( fit ); + mLabel1->adjustSizeToText(); + + QgsLayoutChecker checker( QStringLiteral( "atlas_predefinedscales%1" ).arg( ( ( int )fit ) + 1 ), mLayout ); + checker.setControlPathPrefix( QStringLiteral( "atlas" ) ); + QVERIFY( checker.testLayout( mReport, 0, 100 ) ); + } + mAtlas->endRender(); +} + +void TestQgsLayoutAtlas::two_map_autoscale_render() +{ + mAtlasMap->setExtent( QgsRectangle( 332719.06221504929, 6765214.5887386119, 560957.85090677091, 6993453.3774303338 ) ); + mAtlasMap->setAtlasDriven( true ); + mAtlasMap->setAtlasScalingMode( QgsLayoutItemMap::Auto ); + mAtlasMap->setAtlasMargin( 0.10 ); + mOverview->setAtlasDriven( true ); + mOverview->setAtlasScalingMode( QgsLayoutItemMap::Auto ); + mOverview->setAtlasMargin( 2.0 ); + + mAtlas->beginRender(); + + for ( int fit = 0; fit < 2; ++fit ) + { + mAtlas->seekTo( fit ); + mLabel1->adjustSizeToText(); + + QgsLayoutChecker checker( QStringLiteral( "atlas_two_maps%1" ).arg( ( ( int )fit ) + 1 ), mLayout ); + checker.setControlPathPrefix( QStringLiteral( "atlas" ) ); + QVERIFY( checker.testLayout( mReport, 0, 100 ) ); + } + mAtlas->endRender(); +} + +void TestQgsLayoutAtlas::hiding_render() +{ + mAtlasMap->setExtent( QgsRectangle( 209838.166, 6528781.020, 610491.166, 6920530.620 ) ); + mAtlasMap->setAtlasDriven( true ); + mAtlasMap->setAtlasScalingMode( QgsLayoutItemMap::Fixed ); + mAtlas->setHideCoverage( true ); + + mAtlas->beginRender(); + + for ( int fit = 0; fit < 2; ++fit ) + { + mAtlas->seekTo( fit ); + mLabel1->adjustSizeToText(); + + QgsLayoutChecker checker( QStringLiteral( "atlas_hiding%1" ).arg( ( ( int )fit ) + 1 ), mLayout ); + checker.setControlPathPrefix( QStringLiteral( "atlas" ) ); + QVERIFY( checker.testLayout( mReport, 0, 100 ) ); + } + mAtlas->endRender(); +} + +void TestQgsLayoutAtlas::sorting_render() +{ + mAtlasMap->setExtent( QgsRectangle( 209838.166, 6528781.020, 610491.166, 6920530.620 ) ); + mAtlasMap->setAtlasDriven( true ); + mAtlasMap->setAtlasScalingMode( QgsLayoutItemMap::Fixed ); + mAtlas->setHideCoverage( false ); + + mAtlas->setSortFeatures( true ); + mAtlas->setSortExpression( QStringLiteral( "NAME_1" ) ); // departement name + mAtlas->setSortAscending( false ); + + mAtlas->beginRender(); + + for ( int fit = 0; fit < 2; ++fit ) + { + mAtlas->seekTo( fit ); + mLabel1->adjustSizeToText(); + + QgsLayoutChecker checker( QStringLiteral( "atlas_sorting%1" ).arg( ( ( int )fit ) + 1 ), mLayout ); + checker.setControlPathPrefix( QStringLiteral( "atlas" ) ); + QVERIFY( checker.testLayout( mReport, 0, 100 ) ); + } + mAtlas->endRender(); +} + +void TestQgsLayoutAtlas::filtering_render() +{ + mAtlasMap->setExtent( QgsRectangle( 209838.166, 6528781.020, 610491.166, 6920530.620 ) ); + mAtlasMap->setAtlasDriven( true ); + mAtlasMap->setAtlasScalingMode( QgsLayoutItemMap::Fixed ); + mAtlas->setHideCoverage( false ); + + mAtlas->setSortFeatures( false ); + + mAtlas->setFilterFeatures( true ); + QString error; + mAtlas->setFilterExpression( QStringLiteral( "substr(NAME_1,1,1)='P'" ), error ); // select only 'Pays de la Loire' + + mAtlas->beginRender(); + + for ( int fit = 0; fit < 1; ++fit ) + { + mAtlas->seekTo( fit ); + mLabel1->adjustSizeToText(); + + QgsLayoutChecker checker( QStringLiteral( "atlas_filtering%1" ).arg( ( ( int )fit ) + 1 ), mLayout ); + checker.setControlPathPrefix( QStringLiteral( "atlas" ) ); + QVERIFY( checker.testLayout( mReport, 0, 100 ) ); + } + mAtlas->endRender(); +} + +void TestQgsLayoutAtlas::test_signals() +{ + mAtlasMap->setExtent( QgsRectangle( 209838.166, 6528781.020, 610491.166, 6920530.620 ) ); + mAtlasMap->setAtlasDriven( true ); + mAtlasMap->setAtlasScalingMode( QgsLayoutItemMap::Fixed ); + mAtlas->setHideCoverage( false ); + mAtlas->setSortFeatures( false ); + mAtlas->setFilterFeatures( false ); + + QSignalSpy spyRenderBegun( mAtlas, &QgsLayoutAtlas::renderBegun ); + QSignalSpy spyRenderEnded( mAtlas, &QgsLayoutAtlas::renderEnded ); + QSignalSpy spyFeatureChanged( mAtlas, &QgsLayoutAtlas::featureChanged ); + QSignalSpy spyPreparedForAtlas( mAtlasMap, &QgsLayoutItemMap::preparedForAtlas ); + mAtlas->beginRender(); + + QCOMPARE( spyRenderBegun.count(), 1 ); + + for ( int fit = 0; fit < 2; ++fit ) + { + mAtlas->seekTo( fit ); + mLabel1->adjustSizeToText(); + } + QCOMPARE( spyPreparedForAtlas.count(), 2 ); + QCOMPARE( spyFeatureChanged.count(), 2 ); + mAtlas->endRender(); + QCOMPARE( spyRenderEnded.count(), 1 ); +} + +void TestQgsLayoutAtlas::test_remove_layer() +{ + QgsProject::instance()->addMapLayer( mVectorLayer2 ); + mAtlas->setCoverageLayer( mVectorLayer2 ); + mAtlas->setEnabled( true ); + + QSignalSpy spyToggled( mAtlas, SIGNAL( toggled( bool ) ) ); + + //remove coverage layer while atlas is enabled + QgsProject::instance()->removeMapLayer( mVectorLayer2->id() ); + mVectorLayer2 = nullptr; + + QVERIFY( !mAtlas->enabled() ); + QVERIFY( spyToggled.count() == 1 ); +} + +QGSTEST_MAIN( TestQgsLayoutAtlas ) +#include "testqgslayoutatlas.moc" diff --git a/tests/testdata/control_images/atlas/expected_atlas_autoscale1/expected_atlas_autoscale1_mask.png b/tests/testdata/control_images/atlas/expected_atlas_autoscale1/expected_atlas_autoscale1_mask.png index 4123b16682d662bb0c78ac483781d13080fb7de0..920776f72f6c13f75aa06a030fb8898babe60f5b 100644 GIT binary patch delta 1994 zcmcgsYfw{17`@bjow1-+ks?a0VWb67fl7n~a6pP7YNkbv1VgA3K?x7bTSIcG6i^V5 zP(U6@+9DwGD$sxd5#JD!5Fsi;OroPC21pPBgz!q2{_1o({&Z&V?w$R1zWw%m=bVNm zlRv>Ag_?M^goNJM1ji2$4+i+0aCRKthN*0^-kG&=^nQN&r~Y5uzI;Ov`tv%=NK*Wu zU;V|4(d~S1zkyA>jSfzq*x-Cd%fvIqtOzDg43p8py9>IZqq?L9`^a%X+8^Wv;D zM%5WT*g&$ju@MwG1;>IsVaDa9pw0^kZ2Ihw^#rN@xa*!hSH~JUcIoA`2~)id0)eMe zsS#Xa^sUz!TlSsXMrIgBuIN`mN?Xn)6KMH+Gc&VTfm0hLG;~{%EP-Hw@G7S-%#z-> zi^@={X9O}V8huu)8*A*Us;;)jPQA(V?mMFhsALv862jg*&QYt?e6}a8kJn19uL7g3 z4dv_W8`K&Z#H1uACx1sXf#Qns4)zh~O^Zop%lD|jTLmDX;5**cjybviJIXnIXR*TJQS*3e+o)HA4Q?qfedFOBM%V>gp7=g0nqeHT(Odg z+r__mtaj?LO^h9ueO_6TotPWJEmbO&)6>(Gl$6CfszYHwdwcs-uVdr&%E}QTp~}J* zbB#+KRKB{GW(8|fp#9d`^{XHu4ZvW%Ws7iOqKmVa`W)2b#m};Iba8ghU+cZNCoL^) zCHnN~Q@^1uV!g6s;tmdn6O_;d<)S_oT6b?X;w|`u57v5@r>`+C9v9_YFnML<{L`J` z40D@i#?rhUKOgBI`^b{;wx*ihdyGJW3;BHhwVss|bkQ2K#P=CII7O1%QcGeI+z&4OJxI-MSUd-Cz(v;^;-IR2t;b~U85 z_{t2r)&`#58Z9f8btI|gCf@*M39QH|`2fDpEi86m0ac4(J8WQ}Vzwtw(<+?;N*R&HbcyJCSuwSNwt?mnkiTSNj_S83yCY*$$y+p%gq&2yH?&|M?P6kBD~`a;mjNON=Z z6`X;|0No5cd2+R;rsfb{yjcI_55MeKb!9PoCWcGY2+b1<;O?>{1szp=gw^}-;Q?FP zc$4do$}abX`{`?T4+pZK)L5Y3)a-Xa3&M&#uTrZJIQiaS76bvdK+ta+^}w?r=<5R? yYoYYJ1M!Ca^}a>yRo3sJ6S|5q|DF20CdeU2Q%goR&p3kOAgte!0}Vc*7ykxu>!Wc1 delta 1992 zcmcgtYfzF|82+kFXQ#3nSIaJR(Q?bm63fuY-Lw+8N797Qf-NE6`6ya*_twjh&)5qklyQU;mv$>vuMtCj7Gc+ch zq<^3lC3A-$X#cqdv;HT+Un+iu8vF%pU!nbe_%mwEH|`v{x7uQ{98XRTqNS##G3)A( z0rU+h4Hp|lAxX*dTh3@#Yv~41hh$x*89U(hT_uE3y_tjNZt}9^kN&3bxI_!Uo7RsiBD5IK2%f+KT zTsOH~F5$!lsCgqbD%k^H7PhvX*n##^3RqnUHnz6Jev){o#iaJpQ0?vQj}r(1>=5BR zufBp=wswO;_Xp)VD)jI#x&0RCq3?@#@1m0g=f!|P5Z#(?7C!Gi!QP;L#N;y zdI&I1@EeJ085ipfhP4=~dF(lAG^{$9lhE~NV&DBO0)aqMhws3NLs(tGtSy(VkfmjxTcgiyJg!80?W3= zB_?h-dh{s1LMQT)l}`ymSfY`+Fe`-p;^W6(dwP1b5l$K+rkv9$N)%gwb!>k|wHW=qS?D9U&jPD~=fq_~8H^+o<`g3#l^ zk>r81s@_I-g1AOxG#Wqan46c!ymoC>U|`^|THOxt0?|2%5>9wZ>zbw^dYD#Gmb?~I zNij&~q~#@WOX+kvXcQk4^FD_FZs#>O>prk*nOyF;9?P^t?6|@uh!yW1{jmsEWkNqV zsTa>fn6%_q}tgI{#P#qo~p2LB;p`vsd zrMSDh`}G7JRaoJZu6R7(x6N-p`tlw`h6{KXm8JMu0DYY9aE*aZU+l@6x0FM*d~#{M z4sRMR!xQfw!7#jf>RaaNn*Vo7P^GL%ZBSKe3rbkstk$+qy-q<0gZ9CC(c?BRb zhrr|U4ps-hdik=z+H67w0|(^h4snE=0{r~^lK5lK%=*_ss5I4EzQGo9w6h9z3<#Cj z&)eFZu4xCJcnxHAZ!xaU&CPXMvc#We2J-=9XbOb_b@mE~F0B_F+!U!9&L5VVNh+1Y9B ziQ(WoD*M1pWlUQpbif`8>qT3|;f1c15lDEgO$ zR$U{35x`6%H*YSuu_r(Nan`&hd7h643Wcf~@$vTd@bM|DtgKYMz2_X(kcc4=2&JW^ zucXq-(cVTpP#kW8)#Ku*AljP*u9fd>yXBu4Q&G^vX~u3${IuE+g2Imhxk^e(z+49a zIhHJ4ibSI;B$B2F4xxPV$xQ!hbNLvN}Nm6Vsa$@6`w-fdZL3xI+!yZ7y?`6>GMe*t_6o&o>> diff --git a/tests/testdata/control_images/atlas/expected_atlas_autoscale2/expected_atlas_autoscale2_mask.png b/tests/testdata/control_images/atlas/expected_atlas_autoscale2/expected_atlas_autoscale2_mask.png index eed690181d67431a47ae81e609691bdcaf56566a..b8e968eb553b35fca3d1f01a17778d2cfaf6f956 100644 GIT binary patch literal 36276 zcmd3Ohd-6?|NkweB^oN3B}E8Dc$+DECVTJ7&fcR%WP}j1_uj_}MJ1igW1l!lG7gR{ z`}exNKcC-tJbr(`*TcKKbmKB51S#c)#5BgjQWUP@BK&apdnDbr|2&bkojuMTM;p<`WWmc8w+)bNfqqCAp$jC{VpI$G>$7-o| z;t12@o|_wM>+3soQ3uFiga~4Sk5@aq{}aN?E(aq;zg&(6)9(KezU$w^52V0oj!eI|5u3N9Ts!~5F}Ocf7->&WEo*xAKbilHzrqSD>`Qu zTbbin4@stGWnBr2h#>lsyi-PZ=G}=ryT5pZos5I1nfuRb8>9s>`{}%T^@?%mDuN8^ z=Aw@*qL+5BtEhT3pWflU+TFW%>6n=M*L^zIr|vRuzTU<9Z}4my?-Bi{^9zb+zWO#y zijle>iZ5Td&c!w5?!WFXQm%|3r4{$ksikP{&X8RtHtiDg`>YjmaGMT$b4}Bn#v37i zq;)^l!&D4i&YfcJ-K5a4F!`uIlanpPrH%Kt_}EF$xI!<_&FLJTI_^lhudaqen4Zy?$`CJVES#jp5?S+mjVE+#yU9+h z$pMeK8UMYGNgF{n8PTQZDE6A}!pnU+KlQf9OWw$?53m zj+w(l)NRezUr8$XeJw^|^KYv}F-%xf?6{EE*o*Tnq=xkwzLbqnAHo2-D5`!-(FrLj z12Dc7oWu@pb9kUJ+ndxN_~TEK`)D4$eT`(N=rX*TdVKeV@!uDU;#&T@lm5jyDSmpY zqKnT(JJri{dgrPM#msw)%yE{wQJDoLjd~KB84?^ge%Gn4OC#{^OFo*HRnX>Y#bVG-+>_q{$tp$#pKK{ef$z*!tAZ zkI!yWW{`IZBS^!GEP2?A7q{mKyG6EmSe<*~DFf8oAE>8XOT3fp8&>;yC7ow_uldf| z7^9Vt^-o4~zeCWAz=T^!he3eqRf3sQQF^zve z^UQRJkb1Yb-;Zwp5#&r#A-*_o)HOKxc64iWeP*@9zrk&8_{xCmYQn&K8RqSuhgT~k zwkkH!u;l$h?emk|nxAenVsBA`BMv*gNh}%o^y&HTABSBT0$gr&&cRa*47Fn zZ^9P;&70})uln}2(s>#RTkE8OdcuyqvWiNe|1!~NU3d;(t;*v)gngrsRp~$w-}xG` z{W3c{d*wS{=ks-Pnn#G8dk@?7=;%VA;K7u?pV*$N`usZc zv1I#O$wB75zpc*>GyCugc`TTC;14r-Ts-phVhyQr>uFnC+wJ=%v@#39sy-~a8XO-x z_qIC^hmRkOI)E*InzEUfax9!#g6W4F-tI}}Pi?;m?WaRSLqB2l8rj3O8)h^)cXt}s z8+iVP5&nk#Y+>GgeDRu)%hbJv$Os=+4+x=Wg73#pjLkK|HWEZ{3cKOi-5{z%QBTpy zqkMYti1_vx_2GD>tFLL4R8%OAA3y$4R(vV)=NjIDIy)z)rDm_i|1b3uQ`#q)7gIV9 zODtWMIL#p8C;DU5I&P%i{jj;Y`B>A5mLq5eJlwQM-*n1n18YphdT7Ad-*@5fc*NoM z2Yi2jOp2c$ZN(FI*M2&uY#A(UHSKMEboU)_e}VUamotejd?5emzem~=drz_{uDx1n zC-T0%bLU6rAyx4Wiv|6rRs90-aDy6j&J~-CbrKFDh}-*4|J~(Hi2S)yK?R6y{n4d! z`w3=JV$XBLV?2!0es8GLPphJ$A}VulC1p2y7a~Kl`iIl2-HntzT?bFBeltESH1v_R zHOK6ep0(f6szx*|dm9pavg;%6DM~mEQKmRk+Rb0*n{ulgSBs)D(QsK_Ufygz-bP>ff4(B5imZ?6&|w)S>+MRiXDqOq9xZCP|@a&J3jHI3Sr{XLq`az1~S z-rvM04@PxpeiL2#dKpq&&)2Un?d#_-3u^;L-bK4JCA$g(2F_v0UNhHxmlFxS=NlIe zC5d@!!4>(HcREPh;fyg4ekG>FN1CuKM#wO%cPkqF^NOc27p_NA&*;{1+}Q~8 za%kFd+Zis|(R=H%QP8x>dG^?$`0Q%~t}Uz`XyyrG@{(gyO*SYinx3{w67{? zTnuw~jtz$rwfgH7PhfT8yLTE{_q+f8t@-okkIR6e`*bi{a*f50RWIMZ))|OP_|-^O ziT@6x#yHqX zr+jypQwDa{2_+jFo??*c^mKHN(D#IghN5}g^ok1pEmV&KvuvG4Cz} ztxjb`%=DG`t%jhHk@OTrYJ+9{3VL!qBBP5`jN(2*25ZiF_%CKRMqF^po1cxhrFYs1 z+wCDKLu>BhDp~iheI8%rJ=N21E3o>*xpNwqvRkQ;6=e_caQ2n-+)#yVmEQFq9grup zsr?q7ZGO{kO!wca_K#+5AR3Z-K7Ep^DSCF8x|q&Fa|vCRVRBsh)MN!OLB z?pu3}$Ke_cb9N3mzE05$G?sd5IW9nDZND`z1u^oP72sIO=2nUK2oaX_!Gi}Y==N_U z8M`F9!)?&r85i6hx-itZ;szoke?i^%xg#o-ye!+@c3_FIqkh0w@6LK02L&P&_DWe#8!SsU_I2`6D?i_WO;T186VXoEM32A=O|1LR52 z9Dfh{6G8=j8fnKp07V!oM&ZgCOtR;n3u0@7=9?c5i>JbCRM6*|uNkKr@!#9wJS}M> z_FztJ1c$?IW*Ki@G4x&SLr0I6z#T{@`ST6yMq)cFxVo`G`jw4NeIplm8*NKdj_yA843VP%^_BFqV{o|#4?{mhAfk(6bk5eA(N=p8mJh?-8?88MP)gmqN{WXrs>j>#l|Gdm&v#69)i!g?ev+nvnA?s2&pnRwhzK^|ly1Tx& zOAvPYE$2ZG=Ut>~~<2d}~~0FnD|QlLt4kP21R4eguByhKcJA-B_cwn-fm|{1Xm@)K3r1Wfugtm9y@n z<%pl3pB0`CElR6ihG-pgCHSsKhLRz(Li?=K>q85*L?K`vRfz?1iT#j8H{U9H3$!$3 zGH-um?gcbBs+l-{014Q=Q(=T40ZfngcGih;aI;&YqJ?8)rf8{>JUNA;menz8e;s33 zvUI9bKzNa}D1jCGT@UH>mViKZWaKHB5iG{eceTJ+vrH#cm>)s9$o>xp&I7!Te*F(7 zy#I5l`2NUd*)evqj0Z3QlhjBl&M^~zalm!L)2P=JLA>6Ow!T^1X<&Fr{s?UstkeKC3c*q)4%Dcp(vw}B`e2fB3?^#nrVm!tHI>=ONS=o&kg)F$^cXX3HsK^6c zclOwe+!na%)IS>NRVBk@?lzKS8D%V>gi#2eI)osP|MM@;-aY^Npn_Zi1B%d3r~NTV zK5JxP3vuj&`|mtQ*`UiyQ(A?q(fI>JwV7Ar#@z+z z7eIA^k+E-k+o#vm<%LxO%DjI3Fz{fMhcq65HMfHN!)SuXTxDa+pvtgO!ZEzfgfxGf zEfSOrljCTD_$T~w=kf~V9nP7@b=iH^(u@b-1;#9(PkcBBFf4b-R)!Ikbv6&8wL#$8 zxmQ-Erk7@TSt79$=~VEk#wpIVmPCS zj5I)+4_8s;$>fz1uJ`L#Ie2cvg1-A4F(Jb6Zvl;HbOl_uuF^a9^!8zDrcmKG#d=0Y zS|<-`e1QSk1~9t9U$X~+B7x+h7S!z;qkwWp$WAl&&=DU%bAgMTC{u*+b2*KIzTCU; z+bMcPS;KXANJMA4r8Uj@n;m%!v-GyFfyi?4I>X}~opb~z-%MjSkq$g}1 zluzH!>&>rDOil)^q?}7J+v=O+J8s!yhzRrkD7cOsxDa^CmDX?_;5Sj(!7F-c!>2b5 zsksdT7bqIAVO*Ae%g10XuSCnkcNWfSV%ZbLIczHL?o|xI>_m~aAmqY@U64UXN_oj% z$(1vB%zyW>0Vqv*8X>}=az7roobdV;%m(4XQtKbyw-Yw$;C&%5?c&dIf1a^h)Rau< zz8a)>ww<>5cX7O&i*tsT7ojLyvV~$e8)%;*$NIZ&UO<{0c@~VKTE+*ofqF$+y{1wR zfOVDlOmOPsoXYS5vfB9I z?mc8AuFV3dTEFj>Q?2Oh_Es{JdNh(#Tg-rnURrsM3PV~~qfIxCbmbTfSh9*Cw&ZJt zFz>Wsyq$}ps!$e#rC16 z46(Ya7@g_tYiWxg&3*(gywT|OjF#sxQuEH@X6;yXQc?%M@N2rc?3Pfn~nOC2u&XTVngmu9mtuxye=Sk+spzwZJ4lXS@ z!Bf1`E6Vl|Wpbtc@WG}^55NJv!k0=yJw#Yk#}zF*Jm3jBM#edhP*m~+z6Js#L#wG3 zO#ow11076tZ0DpVM@+LnTeQ4xY?fIWt*J)$xW%!l6R^pZG7BHJnKT$_!%An}S37wS zl%lcD*x1-Nos(7X-xppy(){2<@+8Q1)3ZYr^dWX;W_LQpw+d%gy*vzslZ?t#4+Z$O zva!IHumz$7gM)+z(xtHXS|Mmsw-$nFvE`6sK6s)ECC6XAqW*;7lby?zH8r%m^%@*) zyp$k(Ry^#q_VF=*`+&5S6=$SZkrQD`TetL$)Ai5y__Ok~!M=wBl7b#I&`a@MM?T10 zIeipD`-uaGFx2{ujScMRzZ&YTL}kqa%__a36C>2EGrKYsqw{SOPKJgG@%v%&d#h1K zM&@^2@wW-58W%?^E2};Ne){Z<*@}RV=JJ~!5d6{hD!!0iC?QQl%J%Z|YCjVuX+rzu zN>0@ezpXhy!0^4aA2>s{T4g%6eGZ?lW244}On+THasY@?c%QHR^Xf0Wl(og2 z5Vh|XeuT$QlM9KwR0JEU;lOkQ1}-|-zB~DJ;&>j`*P^w&3+`H z9W>wJiiZ*54F8;VWjBm#e*n7UK%r61a)-z&DnSc7|M{Mc0p0g>@N?jITv!OT*4&rV z#Rl9tW@Ur8V5HTNUtGNS44aA+<2TID?E~m85_rjY1u@~gpZDhJ#d}%zkCxDbb}^+8 zkD|~QcT92eZ;v|`&O5N0)fF%^wY0U#w|8{NFn+<9rW~SV`l3yabV<+o9kGLn0nnda z>rCA?WV;rXQSj=sS!)z8B5*ZcX>EJ=A%N&Mi<^)wQ_a1+OO7KSs9@qj^0vbtdwTKx ztVS_0Q2m|~?tL!d_!+f;DWPy24jVPxGwlK?`F1N>8ZI74oUV^P$e)_K`e(>0xm#A~LgjDF4QZQPhD{$T92#zpKA*@8WOrH%$Oze1kbLuWxMUbr z1`bMIY~g*=W??}aNEas4dx?xZ&!~ixDWahJk&ET?)dwf2sX_KY%d5+Nab?jc832J8 zwhjX*3T9G!sbK!tN+0!fgVmpbS8X2TR1-S@giH}#3iM6!DV#rKx?WgZkjU>(^2T;`lD&@~TKFYgj z7&Jz;WunWu>>OeE(az(E+V0-L>6W^_(E)N<_s5pI< z_w(+>97!#Bi(*3R8i|z|auHCZ4j1gJW{xTk(PEgD{b~YLTntVlPV<7Y%@aIVwrRP8nk|8JAjk zn-xW_Q6TvJ>LvDxUm3bV0D^c=nw7of!bX|W!nyzusnS!%*l|KbXZii(>F`O^J_Iy*ZR?+=nJ`F50S>$jNOx2`YV<2naB`%04cE+$GU0Tee;rU77>z1CF_ zbIo?-AKc7P04Vl}m(mydNe&PX4 z$F(C*H92(@vU;);znwew;sj6R&`cB^Vf}1GYjtHw zXhYJm6A1UePiNuisD`$3xZ_?4)0nj0P*x9?x|4NZ{>2^1Sj(bZr=i@r+~gxqO{A$4 z6fa!35NS$l!)*m&0K)e z&zv~}IU8Q<$N&Nk+8S)+%`s`ZM_#?Vi@=mC(2tD$AS#H)cD$KN8;9Hpf^3%VTWtewj=5v?t4 zcKUgzL+%JCQN=y0%F(al4B%F(c_-v();?ExMjt61*@uee(8i+oKcLg(b|7tpIN>uF z_`YDLaDWTwynNJ}#zh>zc34AFz+*%0$ zQC07?+3DN-NGWDi=o`{4=sDcK_{=UW}oR4T{X zoF@gcx#PQqhh;iSQOy&#)ocV3_4BoKRS&eT?`V2Yeljb2?5V*9!lxav&m6B$s(ZY- zVwEKBn;>q8V9ugI+3WSBC$Nb?Q?yM5cl_`+JCE;SN3H=k@f&AJFQ?IB$ji%zHt!Z` zMUM+XXApKV)Pdy5wq;Sk*8*SdBD_bB_D8?8s?sw(pdc)ma(3ABzg_^>Hr`O)qcjJf z*y4@45znFrUx}8wu~BWmV=-k3PPIO2g^6RH$xgLDLRt?Y!fjD2i@DFH*d_&)$q^Hg zLy{+xOAkt>I@NM@E?XpIL)QY5@q>pC-FthUs{H)V0#%l$kK}4?c37k@%NhzMD#vGE z5GZAQv#tec>t;M<;$NddnwlM-O4)}Hpjh!WpdT1JtRq8wb{-H8!aA_v-&?zwRp~`v z2_LFzGuYnh>grGkX6{+o>=(a%Qmi&7f3;I$Z#%>x0zYnpomj&76j08(z)WE5P;Gx7 zs(}mxuwRGkoYs@dxWiOWVb>AX2W;JJbo)c{lq65GpL+#h@rC01;fOStOHtK6Yfbou zWC~4q&RAW*ZFXDwlr2+Vqc}F5$OgGXODK?3YMNJ{HA@^KiVv0g%|&+PX>b%i)z#5? zov9oq#)M|#FqzAR=~fbf#CU_=;=iu|W@>PZtwJ#{D#4Z)@?JXk>T~~(!^K~CL&I0n zfZ+P_Eiv5oPuq7hkhiV-LW)3}z_FIDnL|Vs*SsvgL}d+wzup+TAjRh4K0)Yev|P?h zY|mcW*v>W+CM=G(x=zGsTSL6iF*6gE){xY&j1vgr8*oM{wPF~n4J*auzwIbbh);)2 zcsaay-F6`JGGK-F2NRyj0FQNwTpIg|>wgGfC!Az|d9MvUydn=2nsJB;k=xf7&QoMr zv3Fgc6IYyUsvgF@d0aN9@aYZK5}Gvt*Tb)q9ebKO-d6eI#UU>zvMJhqi^hk&>Lg{0 ztr$z)NR?GxRVS`^CDhj)p9eT`Mb<1_OOvU0s+F!GNR}C|UD0 z@$(a(IT_H{&!j$LT?N(Y_mm z)lkU7{L|vpU>&GB6BCvoPD~rbD6vGo6v`hRFlz2QKAcemJW9yVK)Bk+N@T005`-V@ zXm&$ykv0<_(h#)&$f@0rzB;}GOzHOg;t^oIkc=R7r{)|lWLwD$U;cC77T@31b+1M# z_q;}dCJ+zkRPz9lTw#xe?nT$(VJru54VSgqA<&fE0RMNN5xeK5Tf!=&r4D3lPf%HX z(L>ZgfTpTya|qejFISEd)&V#w)rxeP7HQe^T~T;H(6Q*-#KW<~(0Vj3N!rfKOD{(? z#mF(qlond>sn8g{1t8zzX5P-$di0Tl6eq3pVEvz4>OzTy9gg+|ddCovZ`N!rfa}=S zAcM9)sK_a}i%}O$7_*NdZHYzqJ`zZTstwSbA&%+Q=M``Rh|F5ztgBE@2$sM7iLW8q zvv03I)%%g#;^*U)L=cn5m5TH6UmQId3bybv$xSLMOTeL4?dY$t%JrxLVGq4 z-hL%fsQ#dkK&Z7k!Z!A9)U&7 z-r}Y!MLX%=UNJ{DnH8f!cydIgtdmG#v>hHc>P=;JNannukK@(3&cXL9p=J^<<=sHj z=Z=_Fd=x9Td{g-7T?cxTg@t0&2_~Egse0Y_kmSYA@Q*0rX5+KO=RPTVv%1!I z7g_3>-cL=D`LM44YwFGp#`eUA{xbffAh*ZKvBEyJpp`LBVjz1q)4Xma(soIH)k`eo zwbrK|l@ITlwsQw^SPZ{yoGC_9nNk1K?TEyrq!?lyt7(Avzg0$B{_UJSuMK`JDG%>CDYH)>ZkiiGa~y%Yotnqy#1d!3IoN zo2ykYdn%a8N!}!EZxC>7XS;^DNlSjXWuddnI+3+b(auaip0nj84qprF=>ZcdE@Dko@ zwjNQKp~z|kqk8fnNHxGp61^fY1tyS*Zoeyyh=`z;V)+6&g6IkeQ5X`ol*uHioeUWy z11bt4d&^OJ5H-?!cARP%q#q&NX^>k$4KU!wI=(v*k=wyFbNPZi%~c?;0lcsQ~)+lRK^*oSvvb&Ciw9mQ7;|V zD~g31@SRlbKng$AGr?QUX#=6$puu<^_5BG~Td<{ca;9_!N$@H2&5A%e*V6d5YTlU( zLdlc-mywx36r^C|9NFK+De_mNp>>j~2r=EfSJG!aMg!T6y@eOFGB9Q|PTTbFQw(pc&t_=$Y{#SHZ!nIHtRkvJZS&^y|kBx_TIkg zzz0PHQmNE;6b#qSux9zeOi)T2_<7*c1ald6F9;J{qMNNK@=Afj`S#wC3nz5mwp^!7Kb92iu zDr8a=$=^Wsaey{7fw{2hB?#|_eN^?D3#;eo3t5~!UC~C6M!d^7`Zk-0+I$MjXDd8C+NLEu<^7PN^eP?d<3RUzz=BrqFTiD>xR8G zA~O?W-M3-{ex14UC0UIMu(Dib+w3-BmDEB8>tZn>K+@qVh3%(>g;H1( z?K5`CZg*el6z$i5lt; zNYSt8CLm51^kq`xX2Hye#bR$#J{#|*6Vh52cOz`i7U9F|9%0Tz0r;ncW)K7T*xKC4i z8s)OVXV^5j&fVhUJL6|k&wKRpbUL+d`1o_>16PVRKQ1;?yu8f=txKpHfH0+TipaA( z0J`Oc#hR^vVWE>K|8L{1O3zMz9-RlT`PxbDx<(x?$kLqgO4j!F_WJYm2!=l87vl2H z4qxDxIK9;fDHya_AoP~HL~eRMIG15h(H9nD=;X^QX z&-w;)34IEW?!&@3et10C%BY=Ms~`)$ls~Jp(*P-$s4IpoJwRs*g1e~b`@Ai33>bV_ zC6AcAxRaLZB0SO;xdwVOTkHZx){OjiQ<^S`LhZm&16wmQ7Num`z_K)l$>9^b!^4WP zJ_OR&qdAtksJdrylbC$9+y2f}-MqB2&y(D_%BwN>C#Zz2OZrlkbJcGB?+nImd1vil zbuPe#%h{RTh39ESGb71t=i5&zd1mCvK;}X{ZI0;oyppG#{t?uzSA-d34Rx_^@ZIlafs)fmDv@ih zEU*l)C%#CgMAEzGKf2|8ivwT>ov5nCH9#LR3a%54+~;MR=9#10G7Sd8Ks`5x3?jd5fK0)As9SDjVEr?Fs#Uq#oe--aLX0-;R{ad;bAj%Qe zZ;6R9y0Q~v6|$g-1_)?M_Auo|K$ikVV6Fy_>kO|Jh`xfW-jK2yNyP8*eCKzdKIYtpGk| z3+nlNd?#5$T?)g63u-sx!M-8v`0M%Zs_~vCG>Tseb0Md|O<(H43L{;x(9OwM|Yh>=-ny&yE651aO zdQ%~+r*GW;4cpZfU^u$jfr5I0IiB8Cj{e+1$pJcY`#9jWko|78KIH~(f1bin5So3k zr)q0!bMKOMJp={@?}w{{3eLIi{7=2xT!`@VSD>cdNjnK>!HY7O_96?!{g~YE%d+99 zEDAyt>UghyKR0LJ<(~GSVrZa&6Ie7*7w8f2Y?zh<+GC0!f+ZCAwaNL*pHe|*Li_MA zarCb2$fgoBh#QPZ3WPU(u`4oTzuTgr(1}$^wJpC-)~!2~>P{P#8;M8$(?U;Gg^Y^h zht3t+%eL#e4Jw4ig=eO;zPfHAVq!(`D6VX8zy?ulSnmSI1E56@nGLG4b=B}0+~v~c zO5=wG*ArVlqs1b*sRKkCP@@LGhg^Lf4>b=0DkppoN+E2Ghh}xZLO5Mei>7~v4by-7 zeFB_uSZLR6lviJ)Ii|n+lNTlt(%0j6<8IJ8fgVioOGs#N>}0yETdfVQf8jzmcoJ%| z<{-YH`#V{pEsmHyoEngNNb{dj@KKuU=d5Jj&r9B7VL=5bR8+UqKW%+0))E@HX!pa_ zFiFVqPMgJ2R0^4AJNdJmrbJ=n6i}dyj0|YHQA+%zId%K54P?|Oxc{-Pcm)iFYm*8oD32k&>(PO6(F8ABowoWkG zz8A6!Dq!>a4a8RJN zSr(mYd43_Ng+btDsH;B=6NI{b8Q4y7B7ZJ3a9n6iau;PKK0cyKjqm<(mH`isAwjCD zfJ4Bm{1!id=Hj9w08i+Y8Lq8UGYA3q7BD7$L0O>zB2;(1MLD(t37Hgbp-%+;QgH{%( zi1{%<%4WNvsU@&e`x3ld9y$rn8C&_4;qA~og;uJKo7-Bt!W&4*9-@u&$;ZLu!b>?f zQjZ=$`datv5t}s}i2?dn*V<{nMh~Z41`4}$v8d6v&DF5BxPC0UUW%ogZ=AzhS@5cc z39qX+3td1GmX{K`@z7R)9SD6AC{(XL1A9gjASkNPELhEdQ+Gd`iOy?BN4X0xtWJ>w1p0P8| zDr*~gW&!K~!UpkNcltCKmwK0$%2iUteTVRF-WNJPefm`QOE_g*C{g8C=KX7Z*0(^C zzIX&sG^_{A*3EbWS9wi=#5pxN z%4>MzU(nrm6B9RD{_ykCsS3$ic5fVdxC=#1zVtV4-2$hG$I#7Tx1o7BSE1EnP<_0W z;m@)s2+AsV>UbOHM5m1ls>4iaofv|iw;XaKjTS|-sY2__EjKMKEtzYFbnD+SyY$yb zl-Y@F_hcK}Wz%`o???3oSLA~hwBmueydb&{0J%P^$qfVcYM~!Z1vL{rl+jabDGky; zH0jE83dhSzvKR+e2aHSLxQh*RFu=}wLNTcGp>8bj4J-Rjyl0Ug5F_>&1v@M_v%yvY zpge+M ztoGEmelX)WD6r`@ zw$E#TG8#3KqMz4N64s$}1NNxPnOh&=ZqBNt>a?PNmtVj zzG)h)nl|9lgA;?%Iti7MZ>WlUlLz^rdkdNA5NLnn<_Xzoq5^I%w)=M+pb-=!&|#nE zKABn=mSqF2VQSc>q;H^UcQyC!wEX@)W2QJ!;%Tz*8Z1O^ZwRJ%40y^xaDtH4PKko@ zaPi2$N$~BP=TAV~;KI_q^bID~!2+rUm0d=S`e6<}8B88OpTH&m`Tt!q;X%!(M;Rwr z&G&V0$b~9N!c~Ts4v2BaC;*3L9PzjnuapmTl9%#UVta1OP$?89Fm`|rz286a=6)go z>x1IAmZ|^G9VS1GJH~1u`us2xpgf?C;JZWT<}B>Eqa!~tfp!BWsB50Pz%m=r)BZZJQS0P6$|gPz(6 zdDMn)haYoQgB=DsiqZ$6@C-mZs1c+}&(L)mfE`;sqUD}I;z;p= z02nqQB%!MWCRmEsFF)1+tpOdfE+>u_*~w-;eIw8mtQlYVd3?W??l+f zqyhpZ47a~6To_pi0)s4^a=`Ggr63K3_t?RMk(`hWMGV>~Mr-A9(;Jg1+Ibqs{M~kQ zy;^uHZ3eiZEWx2#=(ZQuw)=>en74-8dCwLA_5|1p#|5CDHQFlx9-z*7@>ieJm2d`S zze6(r0dp}aorQuZ078cAkq_Y&@8AD$#)8o(A5IDX`0*oO%n-5(NVd?r*ZzvJ1Mp?w z%h3T%-jgZe<0hYV7u=f>?)k7+(DAr2=)i zKX-zf`^T*bD4!xFaqerzwkoV8yj)0FrZvv}=c;K9M5`e<}9+q(VEGWQGD;=@;X&~7*;E)Q|b>#8W zzSO~a^IMqno&U)hU6-?E4P5&);dC(29XJhb=y>1l*QcZMJpc$;rpo{mTHpnOX-yI> zDu$91WcpQXZLb{V1K5k6>P64su>!AEfG#p;!YWu(q*%&|rGl*zpa%@>wMdI|R=;A{ zFHg)!IJymxF0T+2$)&W)oV?dMnslo=(rBG|PprZGirnK!{$Uxu;=%Y&;d5onoS-?d&GRG|u%6t{MivW|d!oGfsOV%VOr=<4fR#)cpLsl038p z8E?^@I1$w1%qGzev&&=o4flpvH#|7_`i2Y~;(_C4IAD31)Dqi|vnl3*2bCB4)DKmv zMp){Cdz?@Vl6=4fsAZw&MwAM`b4l6qzV;WK9FNJ)69;40i=<`4&>K#Mf~4-!*&?+M z!ew^)=t;%`|LsV)DLcwknz7ibmo85UdCfq0YhS44J~!uCI)^& zgU!BLq&kY^;7io^+TGr0HYN|d0fY$*98QMo-wBH7$pAe3AJj=G>+(^#xar@Tw{hmQ z=G;Y(p-1V+0JcVW%on>#SebEb-xm@!F>q3})Nkq(UxqYktdfhT3)47H7-%IgX3KmZ4FsT0I0|(-aYb|L&NMEWA71 z57u-(z3R|aIA?ub%y+YrCQRyy>*<3}LT_xsM+)%4CqV2Bghi}Rbzf#=Vv5D2&#eyh zdo>jMHI<0C&rdBZEDQ%f9T_p4Baw)3=BpQ%qdJ)&SFFL|^>-mHS;9Y=EiCkg0!{;F zjk0BX5>^K08irA_9l!f9RIQ+|2ae*`N=({0iY&Uwo$P6ibhrAVjEu|B!(H?!oSU;_ zWMD8LzQr|F!|%b1ttzqO(xGPDvF_`c9H;${Va};?(iUrTiRnoH3_g&++<&6e(_{y_ za_4V;DLs3XT_*?Ey*$}@RyvXi4&8&%wlANGib?_Kgi5Q9cXAFyJHXm?p<4M{Umpu; zBxDa02m_sdYkUyq-imgC41qw;lQ(yEt8&z&)!?+;B*O*Ri6K(b@Ko2##jeL4F0C7$ z!%$IEzoHsX%2s}ugLei1SEbkBc)&{d*4K64)h%CmT0pw!r$TH_!pR2D=Z%kX*)277 zu6hLym;L;81VtyGtlpKT1R-@;!AgPN_JvkHN7;cy zXet4G#A`Q6?L+9?dtX(R1qiITckqv2QzsD`?;ozC?+3+S(a!ehPaoX3*nu<0daU}e zi;HD}Eoor3`SK-@b$Y1c;pPO7zphkn)*Dv&#y%&1e+fc%r9xX6zp~ZvaX6u+2&n4C z=8kwUZR)|zoyH!1Fu9;j_~~q^M^kgNb-0D3`&J4H?dGSAE4_OLHheRCJcLg{15FS^@4qlz7N$c%>mf2?k zNPnO$zJv2vfF@WJU>)H>y3-2ZS+(E=+4ze>*h3!e5AHY&E=y}CeGkGS+ERBDKE{Th zfhvZzO=0x~GRhyX)Js~w#X$9n%5s8%fdQbyOsm9h8Kj=>F#~Mn0Qk=kt@BxAHD^UT zq3r{^Jk~OgK(e_RKSeBQf{&C5NoObYkQEBcV2ufX{$%e?81G8Yx#`1hlaGq=vMaT# zpF$6HJ-!Qr=f;-*uCvecI*qP1ShbA(HnQB@-TR2EFb1wiEp$DG_S~~!^s7v z2rHvw7X^!0L&k+1M)9Qe$3XErAk9>uCwjn|B|WHm+Jfek+wKUci+dEVk?TeUtcqsguA zRq9DdF+ks6JJXv3O@TMiNXK0@1@H)%v7Bo>>}Y`E35sX>H+U)oQT~4(Iqk)efKZbQ2HxVmzy7C=mq2pX7sjI*e04h5% zu#uSm2!-eOakM`Jn?&)GBKaEVE0TQ1?-yw;iysFR^z0)P4|ZkTac1+-NYYk<($$Tf zaxQQY$BECo;#j}$?(UxTr$cHO)7k5mY$lXPn*5E8;S_j$BR|gB!QTEGX3i%>o1sP%l% z1Q}9Lct4LlB>hFiD29uu&=zBr^&MvI3{hGyr)pE+*Lsj^i!+msgvO_zcW{S3(q=R? zh@nZ%4iZMJWkbyK$Xy|({$)|nUsN-?Ylw_co5Go}u zaBO(_8|}2q2=5mF=bp*cQD^cI6WtIMBXS*a4zmYRU%dS0M;i+Gl|jpFgwKy~AC)+A z02Vwxir3rQd&Wh0BQcodGbsW!xwdYE`^pNUrB^ZZz4}>)ew7~BAL9fRn>ZkZLUm^b z?fPfXvWq;%tDSmwXLf$xnwJ)$;@k3jcO#dixaa>k==Z`wX*)({=E-)yoc1d@t$Tpx zf$T%PuLAgavEzf3J`xcT0W!$O#^x;Y$A#aGM%vl?GDEAQ5o5(vkwy(;IhVfmUF%fJ z9{!2DcTB?Wo#MMJo)sSf5fzv3U~3Dwz+j^ckHMQz!MqF&NCsOkaWG`XGg5Dyw{~9uqb4>Op9&oBtC))3?eZpjx>iEu|GA$Lk17F=S1%dB(rK96!u(RlBzjgbf7irZ8TkYOyIiCM07w~Et9*bg z;3h2dm$!WKv4H-B!XfCu2r{P6?@*pJ!=Y0^hTM+tj$%@r*xHRR3A-_=B9Ra`*_qO` zDu%}_w_Q|s@H>GNSnFLF-||cLVWJB1G)tdi4P99~cNtQltJzsb#2z{optg4SklLSP zJw&wYoBBNNlM@N&nh%3{ba+*lBa;zmpL3EpxDQ#@A@F&b%`d!FG?38D3=)44s z0(GeSQKt9$d>(F^J%j}a9?TctdeYiUuTj0T#=6w z+;L@)h4i55$=A>hO#w$A(`b`cM=)JJy=XFg8u>m{IqVpz9`^6VM!t+cb>3YYEH+SN zxQq0z-8p#>QsJ?uKfiy)SuK5bHH4aBU#(v^NcK}eXQfg!cz;Rh#mq`i##&ByB#CTP zhEMxLo|ccI5fJxEFo<8viuY}HPtX3iCvBZv--xl;EfSU8#xrW<@_gpKxI>szo=$6I5?J(r|* z{EQDDKD^ECoBH|lXLIWV-E1x_kT=xTgvW4UQH4vR1_}7*GB`Mqe)z;VF2jl; zAEGyuDM*6aUfYmK0IovagtlBS`G;es*5K9Eac4Cvb)l!HAI9UoiQY}nM<{W94<{L0 zO%Yn85Nu9^X7G86{dC%Qbn1+Em*QsDw>AdW0s55+e%EARVw!=E${QXY{`HFq(Lze# zq`XOz@g$Jp~P^I7FtM>$`Vcs5iLrKvDb<;n}5-1Y7x|LpiP`&3c?d=<_$Qa9#qGJW7z*?Bxz3MwGy*2EZe z6Tq0W@ib|i7EahED}k#9z`BxqxybM9Kd)$zQD-iIqWq2>She^!p<;UYX-8wqoAbk7 zrG2O>SBNnWVenZY~SVU1_IZe-FZ*%`qy2z=lr>*kOLyeb#PLKhXA;$aW0 zpJ#dLs0q&H~5G0qIbK#|%h5;iQ-)4I}O&uUTlMe4ocl(WaWim*|CI%b| zkj_qRNCrvamaWP}WWwso0+9kFCtT%rRB&qmkIBqadld6fAN5g$x#yrusH`O?KZ# zz)R@a2V`Q#Ui!Hp?Tc2Ol|1hsNsh;Q@IpqOy=Xb1#c!KV?&^W(hu?QSSS24=DR5W( zi`T3M6ZLzDb?eL#D+3}Pj6Zg987%orN>0c_3U^LNua)mBioNm{5DE#yk*91lVGvLq zR?N8n1y{t%`GpGvQgS5Jkw%O;(%aIUtSJ;s)i={4d@tpv7lsVdQn5$x)ZD*TUXfto zZpwd9L9^_vK1*Jk&|4n3-%fLQdoAF`p4il3_xAXHz~gW}me!w~9I$(?@0%)1 z5anfC(*3p0+Y5svBX@9zAE(DDxk?hlNFJ~O4ZZxA&L!-4xmVq#I-e*~&uD4@G>Na% z%4iZ-5XRjQw%qimq+Z#s=?vN5y-I>GT+ZpLlYIE8$wn&e-sQ`@d(gV8PYXoBpVaGC z&0GInan^YfD{-6oqgzGB<5Af+6$$2ZG zKyK4-B}X%mUdbHCZ4$4T=0B<*9~>M^jGj+4KEo5{y#0&CI#_oX7ZmWr_iAOPo{|x3 z*H_ohULIfP1^`qljU1qVe$P+6T<)91Br?~{c*jYq!TwugQ&fLdwC(JbS5P<}ndFX+ zqbt3)5_&s1P?daFtd{GPRAF}ZqrAMl*N*Vw&I&PCV|j_jH!}Pm@p}fwQ#={HL3C0i zM}=WOVM}JbXWvz!_(X=K<^3yRdkxOzH+*yvG{WMWHMK#4Z5tJ)G0hc4Eb)(v%fzXw zYxfRwY#oQ7?-AZeq;7JzL$@<$2+qVUVuOP(t&?{NIWJtL)-d|cAcQ1KqN&%>&VedN zE+&jq8#x`tu^;lU1CmJbt*H81@FE;6s#eJm#4_NJBXU=P-0b;*XDl{4>hifILj4n- z3UPX+(ZbXOQU4x?Xetd9F)U|90u9kEXt&$O-eldX-`xr+sK5NX^g!J}=TuMs!}MF@ zi9e!qS zFr|pm_-Fd#emp=X#GndsBG6tv#nLGDD^tnZh+ezx4YU9RYOEq*hp7~VV#+3Dut^-ID$Fxzvm*iT#og@ zAQrd;C0zyS9)kz_bjvvrN&wi${Cx31hQ4D=*mkHqFigE}OsBLD&p&@ohYzUVezLT^ zw4Pov-8yvM$H#b^QBnA$C!s~Qd~|O8KBmByK_MDDd;98}J&j!hGF-cJ80h>);d|VT zWRPR&Z#A6+Lk;KnG905De#|oTHt2K1kqz_2D{}|0uJL%6Ve&ROpnm(y_9CK83|V2s zhJkeET1<&9#PQb$0xh+S6hgSOoYG+@6Ws2Cj*tHKglME^60IC1He+$*C}By_}3XDmiXwp zhZf9;kdu=`m=wvWFvz8KVZl|BamVz^8IJ zZ9^wfLI?dhZ&49#T-s0|%#F_@$L3hBXUjkXQe0dNoRN8t)n(rwOqyfHA#ly#LYAb| z=HiL0w*=A?EgrsnsYyn1Vk^Kj>8k!Q|4st%jkU+dMwS0e)fyYr5Mvi13Gv%6m}VK) z7ti_YXxKK;%l1j7qhf&!yA6Vg!wPFIuc^wyJ(WT&PD$tRu_(|pggEkq$4Dn}Y@^Lw zXjHd`Bs~a_UhKmQ&J4v7x1_Yt)(u^%IDoV)4GXy}F)%RjZ1Z?Upm9di4V^17FF8WRL#w#a8e2Ql%1tKyrRhtzCS93)}r~g7+VI`I|baT6h)Lx zX~9+?OuSGl)1iiN`9e;ug>0LsKQ=SgEx5y zDdOue7F~wLf7XsDLXw8s$nBbl1`%fqSJeTzWV~^ya6Y|?jtFhEqnwvU7Eg4!_Rx7! z#1>roUW)g6HL~)8Nc)7KgB7f}GlQD7%>-=g7r$REts_qqh|x7lW}fE`7HAwoJ__*< zJQE;mkjS2T#BoS1-3(e1q7RTJpxT?hpHmd<7Q6Fy+Le?5?!>{%n}G@%_DT-EmC&&B z*2r(Fw{r9HR`}hm-~OgI&PPexVS%11q*Z}kiXSZ zD$~Hw3`Z4l9YS+1l8G?|HukPyA0u{NZ#eHeFrLLz{oG!dV+aMRvhInx_tKfv0{i8k z&;eG(BGRZIkq!~QL_3(Mk-!Xm`qbCgcQ~-?L)=sKWLse*U7&r#Grf3mUGY`J4Z&?a zB30w#xl=RVBEdt`8m1L#9d5Dj<&{QkEgK(h<3t!{N)z}5{E^aa>npgpE2spsrzMUZ zLEy{~;8OgCifj{)zu3GPXcO`|z;mIP(l_V(KYRdSMaD4a&xBty%~_PUxiPvit=pHq zcSBqpF)5G6whiEH)iRv_fsj0Q)*n<~?fgdzpliKg3e%~%X*pum!4)EHOWKiuEY?G)<1#)(o*Ooa9No@B3vwi9PCqzfB@>~w1g1MvB{#uGRN>l5 znSrMiq8^w?xC)#JhyCvd@84)5^&0Pp*fg~L8+N`F--xxKZHfJ333Y&^WyiKHaWyyO?iv13^ytWeC2I+KP?!(jbI zCDd;-K+zceFmrm>;Q|y?Uj-*FOAALrs1Mz~)6uXUXcW`=x6Z}mzy}V($pY757>3dE zjD3hMn+@|~lEt-_JU5pA0>&~DAFoBbnvbL@DQ>(uCj3MmOO ziH=i9`BfkNowNqu(P2bO4ZV}-^!*l*pr{X@nz{8vZyQ!Zu~AU3Bra3CZW-^LkoWRB z8BN+?IUC5v2Ouho>Ao`9*&HBw`-}pp7~ro1e7^PW#mOvJMI%?n-uOCs?TfrM6;M)G zfXxj}+R|_fAG6yEhoHBReP5#ErmPt>VaQuZAH;^bPGLR3(&~_b4NbkmpwbY@W=rgD z5T)e38lBiZQ`%*iCO&gX|pEu7INvL#mYy~KCH0S z=$l)nBI%vB(F2eRD^*T2M9nPhdYyT6LF-7-_W_Id4A@mMQr}@GIeLvJlOIcp5l{yN zC4$Hh4fbb^p1Xm=0MRd$1_8qA^z{4Kli(!v8zUBl?bH<42h<9X#$d20&+Mby<9cx| zqUYJ+_a7Z#5u+{EEi#$p^tP(|`t@r|P%-k1VC`thvV`No z)Qh2CH5U17Y?4puF#&Xv0TKX94o~PmCsT))@Rf;&FRDhCRE>G|ciY^gW=(FA*4Bw! zIpF-wK}TpdD0U8;$!ucF080SK3B}m1ktbEh0;I+vPSp_tsMhvt-wn~=OtYa+U^B}; zx@gN*Tq?5~1%i6$WTk3KsU3ScxqUB+$DAA-9QwU{M&sA^v$vJjw2%sORfKkKbx_s2*4E-L`IVhts+(_DNd09Z1!)Jeq2&?QvVey$ zcWMnGXDio|;TqB^ZfE>O6vF3nfeN~b=~*B=L5eFBoWz@<2~uPAV|vJVDjW_3!Na-* zZBMLEonG#D7rqGT6HxuN)H%pwAj29UE&FV>>MhU@WTX+K{6nwaBHjpm&Nq9E9_&EU z3LvbG#=Gmy4tiG43xPy-Qtv%-1%92o^TeUw1AB^uiYSf0AU&$R-T5@QaJX}%Vv`;y zzG5#&Z%|brT^iHSL#Pf0H?NtKw2>M@r<|o3+kAejlAbZKv;NoeXV$}&!#DuUQe$)q ziwbTgo=O923E=?@G8qQV9uwH238qAnR*zxobvq}btoi(`eo~$J_S@DnfYET;2-R+N z$-jI1w)19}WzuqT1N%1nap-Izt*s3{!Jyxmqc~kaYUr!OP}?570hS*bUCIli;vcU? zOZGAjkVZz5OfN>wB7sLcv*L#~mz zmTTFDgZ~ixG5ffL@(`S~+&F57O3pJ^xZR0)TBns`wz4LQU)Iq{#G(}mCJq14j z81-iG!YkoCofN3S;4@W^ImI{4A+;}C=n>nb(wyd`U;Y=Z=%9~_!)cfFs_#%G4O<+n z@4se&@?@Jl1gYdCG9r+{FbJ~FYjz8YU?Udf@0qjrw=XWRc)kWVU^zoqRw1kYu_b7_ z$NZxNdvv&){e6H+10C7s-tvfNXmgyWL;u%GspzA3@?JX$fMg-;gb+StW&?MaZ)ZxI z-R+IoG_UQ`D%ruKj_`;8b==Y~sHqSafP5Zm>C#`htys`r_}uT;ci;kdB%<%ZqC}l? zT5t9cyBwq{mKcYK#Wo|Ct)@13HOOkzn6XM;V*O5H^^J{TMkSsPI7yj#mwDoyx7GDK z`I8-62|Y^WSnaiWzAA23NUnEus6b{H=lw{cWJvHyG4?!evGT$X)q+}zLx<+ig&H(J zlZk~2R7gb@Npj{pnCyv}#Ax)dDHiIS%FkLKmnFzZKkmX&+q>_&az;qWT&+z{twADWV%u>~1ypI#e^Y z96C3DFPb%k`Kyv0X)_Rpe6f{T6p_$a!(twfLyov0_!U znL_uJmcVlXw;1}U8?MgQfG9Pviz6X(RfOxl7a{p&Dfb=8@>oW3n?xRlR9<*1qwqBj z=6MJrV0a5{1gsT2&PMc*6Vud=iH^SCH1NW{w!|v0*%}7GyEIOBE)-IT#SrQ$DWsH9 zDuDpVC;AD%Dh~X5M-iB$Js&E~eLuVc4{m^rz;pVwD#i{~2-v*u-l3r?o)7}P$qz~k zF3B3twzkj2Kxj8y9rxnwdWbHjOYlaB3Xov|mcw@T(^#P(T^QsY+;FWp|LNJQJ2&L`D?h?vBz6K$8L@J4Pi^ zUIUJ1-GZJcZ)i;P`drUD&bQX4`+GJS`D8YoiQEDv+`Vkis8m zPxo!w655I;^BZXOwm#DaW79LoPw;&!$v}@Oe?2=7?jvqMGJNZ2BJ~LxdY6Jfat8n+ zvx&fP)oV}?DH0?YOL>5X821iaL=H##7jP}mL6`!=79PD7$~b0d?3ti33HM2{3O}5( z?H@>v>jdx?6rtk+elyg(-Ru?SJfyIyq4I^qO!?-SQs~?*EsLl)}VHK=V`gs(_=d; zAzi?-=A60Z`&h12+=)=>uMuD7_;$f(c7A%)>?8zoEeA*qX66z2?=nfCA2NEqP{bB9 z-pa3f3(Zh^-Kql5|Fyk|;uvq=xV;CdPU6QkvtrqM zJnMW3lj-K z@_kX@rwg!t%p=BZq{z*;pE2OKS@Ykylrrv}iF)GL8~CI(9eIG*gmN;egaUz1yXTfd zHcPmlwbGoUb-zFVLsPNq*L@(vPo?#22$L2SDH|i^a2HiwTRP>aI zG4>=-hmgey$#nszt|kR|C(#!b!mCSVhGU|(d?iD%!-p`XRe2ar>k z^7G|0Dco38ZDPqK6(;6Ha8 znxPMVViZRqtz1JcY+n~Lh_SnQklynVz~c@|sX$){yg9#W=^vyh@@Fyv0ntY608vBm zH(+(odAERegI_3Y1tBZLf&;5nE?ZFtSTHcg6&RLngrauCcnt52&gl}^bW-`?iS|5M zxeKrByAKk1E4VuoFC4Ou;>XeUu_tDP)&L#VZh45#fVosq&@!iyqb5Xu2Mwjx1*pHM zQtD{+;U_x3hM#||F!WkS?YCc$yct#;V$sNuj2Hp$ud+omZ^cvlRPfwv1+M~JI#jK@ zqG}aAjNdo{S$I69LL1J#`e1~y z^V>Mf$B}SJkSL?ZEsTSgq>Q<4oF_!{w_g@rQo#}iDFtqN1hyBjHvxei^eU>ye*e11 z^P3j($S0UoN4J?#FJtKBXiW7yx~W$dCkFfJT|(Xfgu;=677R5?h7ScFN%32(UaxV; z{}0prKf8qJhgltx+|lTRv?1>)#G}YhB0NnzUNBk$%~2H{5kA6xlsrTHLn>(tOWDRLR^epAYI=^z4PVOe2GD$h%r6ZdP-~|z%0ZQFg0%vMz zJtoaE2P=V>f=s(Jf?9*)bTTB^kmw8_ENrqz?1exExj#L)iaPTTd5@c|k;qX}&1JN=PkS0?3nQJ*0MSDrlbm_+PJ7#d297#`tcR*8! z#aI$l8mCKiqn*5BPV2&fpK_g_e)%-~GBEYW{)Xh+ZHMX;n|)x@SzTWrrRUfAPDXclp5a@Y)c z-x)XfUPjg^Hp?CP6UfUPeM_VY(E}h61i%vI9AW{p56UleZE1}dK-eaF&DAAd=u>=8 z^tgbk`i+Hbu9}UTE8_J{@hEmcv9yT$OWnKqWFL3C=3%*od%1XJ7cqWOtVA9L>6)TnB4*;MB*>Dfp z4TQ9AvARx&@qzqju;|1}gr6(cR9uBU5Uzql#Oy;W@`gN7XaE8r(3YIk3=$^31}wLU z)A(L-QhX|B9P6=Rkvu5zqA``eb?|!OcuZBlh2$s6qp(cwLO-bxbEM9+NV^n?Q7ip0 zE{Ti$CG5_k0Ho20yoK7qW>2Z}KqF^F>B+@9lE{R#2IQYiDhL>2G5Tk4646BBURXUT z7ZwH98E7SGg7giUpUKy-0Y3@|jfIcjt)heK4@t79yGz>A#C!Vic_$Ub;wj}iP+Y0!I<*$wjS zFV#OVUqpxqm!Br&+o8zA-hnv=NE!uCdPJYaf4TdAAm_5~0BLMvm}n+W4t;2o2kVZ^ z(ZHquf?6U+nkBkmj_NVwYoR5GP2gGib93z>2LsVE(xXi3Eh;<{G+5|%c^(fZp^B%` zTl;e-n3Q1XIq*zOL7;`7yI{Ui_zA81-lfpafb}(OlH}+JOzx7yx2S1|Oc{Z%N5ZJC z6W1aZBP~v7tp*W=H2_p~DvgU6q@XLqprZdfzz1;m-w!bmz6N9)&O0ICd7-D`B7ksl zIBnm07+!!~P7aNzS%R@tc|fi}EGUjPjE4w$heIk>Va9z8&`yuIGg`l05>f+@$Xm%r zQ$z2X7ukB*_(A3r8IuzsXr8=VcHcWn@a!(O4BRwBKlh8gLQEj6D?E&{AoHA_n~bgdLW8I}a%zX94h^71E-^+JsUvz{$6P_e9@%zB0)(8gACG_{AMU*ZM}$jDjr5eK`0IH^XU_Sc)?mSpQ|TFzO4&ne?vvDP^{eY$}-@X zaMctJ=j-uCB8a%NfPA7Mr@F?3CL{q1Vsp?7cciF5WI28*x|&>MyFsH^E zd0k%esl*Y>3AU|Ntuw6B4JH?QnKfV%z5*hvWGK#~`K(MGjQ*E#{YR(L)KhG6P2ZDH zIN%d-?*8k^s+g%@<;mqgcNnQ!KPe5U6xh{bo^rgjT~|PLWDycegy;&+{n*(WAiqkH zTu0srl{Zi?NDMgb+6CE3)F&G&s~XSm{q~tkoHI?~+GJQY#Z8c6sI0PowwMTi0Y`(J zd8x?FgCNxr3ARUVvO*Sjdqb8a`^>5O$Vt?|#3OAh!6vl@=O5Q9-p0LcgvfgrEV!fs zt-CQsq1?o-y9$97^eK|-w}*tFFziC%WD{sipmQepryGk%asJ%^s>1r(GJ9dAw7I^E z^3J$O5 zrAK0eq?+KE|72rc2`~#p8r195n$>s@8uKs{cDS|$$$Qulk|S43X#zh3aD&pQJz!m5 znmKk#V}ZAXXggsFn!Awd@V2}%HhOb>!24U}TGF4M%S|-#0x&`!5i%i@^GbUq>)1-NvN_-F>7A zgv`lw7pwR_;FO*tFY6lL^*{Im6FJ*WXRMl)^3>-2d8A37u3@yqv6asGNcO!$F!-O1 zhWg8e&eUA$#kl4VlDoHOt7bz{cIQiLH8{_*=b4L;QID?^4c&r<;B^3myL?FDSp&XSf^>^I?a@7P+FU7jgpi2PG#}Q%@J%Lbj`CgS_ z9cChpCC{JAL^;rTMG;b21@l677uFM{^Jz2fB!Mf?m4VD!!)uVLRJcz7deuLCS0H!B z&7LB(xv>avIq7WrZ;fjb4)%(1x$?i&fF6`UL1 zBoCpH_zT@->VEGCd!8MTW`m1$bARe|p5IOz$kNg>g(JAKAy4);xr^`xA6~ierxNq$ zPr)PVb8MuF1?Ncx41%SO=33*9c=(&#AMM#Ab+pwckDI9HUJQev@Jq{jWVA5@de(+7 z$p>v8LTN1?&}Jb!`!EX&E0h&V-Wgasj6tHT8%yWrmD$NB9Oc88EJ#bUc&G5vvG0?P zkDDQMFPB6mw&E*s&ZJpNGqdkq*4v1F3V~gaV)=%WDMkRojM5)Z`m{1DIFgCpHWoKB zf(EMWICv}#GLRp5GCEcE7_}0dS|*N2tmbD}OVmK4>Hhs&7IY{%xTSyOrqK$in$jxn zWrEi85LampC*X~y$kQpEMD>ngLXiZ@mPm4hAS;yYHK|3)6=wnL84vl>B}s+qLkTu& zl1NAAtDd#P;$X;b-m){$?~v#i$9c}`;zIUZ_s9|V>?ZVt(SRL+6AoTQw5CG=8t<6R zbMeVK*)*}X%DDT4`rItNOFl_vrj&(Mzwt$epXQs`ti4s^+TBg+xr_7q-eeyLmNeUH zdcAJ(7|TbnMrg;TgXhd}f(rla#YlO2z+T3W4dy0?)$h(pTw(5#b=Z5}mO=04Y2ZZr z`Mr)xy{EUzzZ~%_jpH4xNuv>uZsHuH#5!~N0dUd5VZZUJG*g6x)O~z>wibNgH@03v zl+j+;77&j@Z{51(iIa=u>?SGh;w-XldM`Km3!a%Cig$q46(DWsF`k~_6 zug)$m=g?Zae`v^dOjKh-TMCcwg&u#3ulCaedB3CE83RPk+XBaPcqwMZgH32V(H5pV zfR*l3y+T_1gSMQ>laGUgFL5e`tb&3<=M()LGe%pH+uK}ggSxZJcMqiIICPZp%udKy z{O8gL^kgj?Qvn5?aC`Jn>hJHT+1lE=dw9s&wiP)!J9p#o0`eVkSa;E|2Lar$M2>b~ z?+KltXlj37-*fWpY#mf$5wj~|($Kb50TCaK2}+~~9mm>TQ<1AYefspS!AA`;bXLG8 zs1H_b5TV10)xr#PBy?i&GrvzOhGA}m6`VxD`bO+>G)p<8@R7{-;oQst?$vE;uSKk% zh~a(2LO+H$RIWvPPxVg@vr+xN)VjuF2l2Z0O8Ayx_8JvSNuwfpnDbXShz;j8z4>Tw zzMuQbpdXV$abmY}oY%NM4nnGb+kYSSK1q-}LBzW%^udyG?9tF=eOdC)C=p3%ot>T0 zj7Qw~gE_ zI4gi=uQwzB$Sb_dA>j?3_~PT__cV*!*dZ@2QZ*O7wbtxMhGnX->?1e zko@l|`QMH6|C3Qjx}TFH+uGXN|LK#}m?%-UeFKbPatNAB?i^^!conK^6hSSR?1^$5 zdAXBxw#$`l;rR9QY);!0k2Ol@XHx|Y?e@lcjlWgI`ONzHcAW`E9uWJz&9|rjfU>#= z2V-tFR8~@o#UaJEZXFlr4+9*Sd3t)*XUruYZC=|&-VUb%Z_6l|I!}5SD>-=MNVA*> zs3MPkD?$r5My1~p{m17|)&j+SYA;pJF?(5k)Mu)}6U@*g4mS^h_QSPe&(zdZwnamd z=R{%qOSF&N-T4j~d>Ql|e38vy%ix+rUPB&kGhGm zqSXbp?`&hUW@v-ar*L6hllgXs>A}3-w7jnwy=b_a zoTObiiF)3=dBe9$zi*T^t9d>(Hqe0Bc$Z_(SxoRGTFHjC&;FPQZu5Ba@T40K@^<*r zQ7)mEX;MMHoPOa2Yv44c$q&I@=?2B_&T#6tF%Zo*g^~#9ZRM=~{u4Mvajo~a&V+Nk z#Jgb5{o8=q@JKt(McW?~!LN#D#+%7Cf*E(fG@g6G+*p8BfObM5yFcgC)jY_u_X6}X zb-)qnjD{r5<>`YMR-}icA6qt=0YN$W%UFjY;19cef>bm`eY@XlT+2&d^ zsI~OtT;KjzP|=pr1|u!gU!2rE9ZH=9Pvd|?e|mrQ`L>_LgJ}M}4Ub?Sw#_&er;u;T zC?0>8F#a8d=#K9!k~w#=#;M#DygZj2Ph1$6|$-X0m{VZn$-z$burQ zjc9w;s{W53tM94a$VT5izNJf#`~YWUE)J+;R}V&T5~XIbIH_9!@La#t+Y2%QpM=7~ z-L}PJI?S^G$28{E#ezq~o?10S4Ns@95*Ds`gDT<*(QLcUiqWyL?yG`E&t+dJI#$@4 zKlzl+mXVQRf+nQuKXI}sK{Fuov7nWmM32lUkIx0?WR#VN*M17jfIRmFK=F^UV7rHW zW<6qkeoT^Kg7Q>d&~VI<=jOaN_ko1i(lgKe6>()GSV#&I&1cvyd=9iWN~d`}+Ij2#$gL zz6p4~GzXMB7YkPh=x~Zy?H>O2{{{fp7o2^;q)B?hyi9(TIJbDCv(>eR29{%UpLuJFqC=^XA@*lET9vTJ|iWQ|IFQ?=A_V>8gzi&59 z=a#!3qh6^R{VevP3 z_39PV2qy|PY?y;QvZT@b+ueng6L}0Sb$4#vx^YT^aCrmvs@Z)+ zmwkNW0+0E2s6S!TUyG84vHNkCv_}siEIeE#`q$iC3vOk*&Ji0o=M`Vz?L}P3=C9$4 zQvQrK9l;c8=VsVf)GQJ(gmk0AJRNS+6@n`JX}Ie7HZK`0Cbnyi!_E{dWiFAdcR8wR zW`jR$XSdNwy3qxT5lZ-0!>o@&5t)#!7ZCWag$av@P>ufn_D1^sR6o7wwaERg$i(F2 zfqb)dZa-pQ>R|5OY`1nMkNMQSh19j()h=9Cl5=mVRl5#%?ApX;gMOsc$_p9Nf|i57 zzggp^Ph)lfX((Wz!EeV6>r`py=%}7b?9X2NNb7$)Qihnbx8bs{rxd|>r%R%Vf;mI#vym;IS9-?Mvsg@%-f97heDsih_wg?uiF>YGidn)L>8=-!4@p|fZnD68e zvME~rD=~?wsY7soYkjh$KH}(5LzWMrUTo}Fvd2U&gL9Q!m((i!HSP3Xkoi`Snv7n+ z-dsRYcB;RTmelfdsjj=F2L1TT$s(5hWtMmw!|06s;szsGV!AAMpD=p9`_j0_q~D64 zISa8nWGy6MD+F1VrSaN|s;V5=0wgFxXqbALJjO1mc&~inS|7HGTp;Z45p?>WvW0fvhLz1_X-`cusgS9Qio`Fn`r0J#XiZ-_3MKI za*r4LulnH~%#pk66q_uN_Su-93SrR`O6aE+8+*6exKA3b^(Yof2s?M~+*)a*pL*)< z43`ZS)3DXXzxZ8}(7(G|H?jLmj3-$W7j)jNdwBTG#LmR#;(BpFy*qxCV+g&TI8>*| zvipN_y)p3#bM^A=CL8>ajA)nH5R_i;zw5ofGs(hNL7Z2T%~R)0NA6R?^Usgas(_zd{y(E5 zhf;UHq(0c+BaPPkRQ4`>aIT&`H}1DNPMl)dvyk5X(-NJb!=0Jr(qOnb8PG_>B2&;3 zLhE2}uS)Ql+z}QMN`rqjcCJ>))luEqBn;I~lAJX(HG>0Iab}wmcxzt2d=Yt_sT zE|W5(&yl+?va+()zFqHnzDbU1(meBUsyXPyc|Mb>-~Am{k<#btAYG45{0SDLNZo4X zdaC97^UTNM-LJ(IEc;vS&nQ`Z1;jo7-1EdzGJCQfeac!zXxMrB<;$0w+V|)c|Gd=l zy_}=N{jqC*w~I1jnj-p8N7>UtFEbe+AC+WQqJC^(U1+nivslReES>n}AnVIMzU>p^K{Wi{1E-EtJEfRv5P42`FJw>!*f*bfhx2l)Nj znvSIGw7tGHHYam_q8&TAxAEg~>W;~uoyPs0j~;$Q9zj@7_;Y5dKTYHx1NI5~(scxf zqH4|(C&{|bF49=nVJ(?$>p#Yg>&E#q5hhj0nyWRM8zfSoQ0{eI0eh=Nh&C?=@ecD6e@J8MQtZ@k%C)7sa8Xe`2gU6mru?eC_pzo+%PQis&D z?6x+AgCssl5&d2FtJKP8HYjnupFh8FuEk^iYz&$C6z(k+@2L)%JPuFsUcBnJnl#ye zzTwZ|WN9BgxT3HI={sRJqJgl};4;vD3Ki(DgtT0$=Ptg}3`Caw>44|^W=2N60i@!6 z-TU9)_S8MmT3e!}_DBkfpKh3V<(#H6G?Q;+dXt`yIo(XKdADZY>{pGRV-$mIMle8#@TZH`gNYYUl5u3BaVw|=EP{mc0*UJu}i(K?1q&r z=Yq+~{44X_G^mVv(px{C85tP->x4xrO|elS#=gbszn!ea%G92B{RDsAI~PBkG0|gx zY0}gU>wbL{6BKk<8p4(MOm^$pE6gfV>m*!%K4%Z=^B=7@VP8n`S?Vb!jTSGkv24GP z+28KM?d=eTpoAjN53T$;wVjfpqT8PzA4^VWPo7@im*b8kOSX3I z>z~V>Z;skIh$eY2RnM(0XayK$YO7RE`qx4_Z2TSpSaHsIuC$%Gk5YC^&Tp2-yr+?b zZB&V#STFLhv$VWCHu&)wrG9v+A4yl=luop|(^{xzAuDOEMRvDE!h3B#F<>&ts4#9~ z15>$>a6hHz7OgbWc^J#Iy$`ne57n~YFN;4>1Ba(usBfczpfC;2H1@3ZVbS@i1qIt4cB+~A=)(w zzH^eu|Gg`_c1tz`cILAHmEW)HsXg(HyR&qa(vzgk$zlY>3?l!vcsG^!8j?{|LtTA7 zLUO5_W%nb?{Dco-KtpVtUB$bN$L#(4_m_~ubns}f;6_N5K$ZvVT&scP;W|JLy#shK zpB`!N?d{!0+WkSh_wLiDPX~<|Sy7ptkuH0}M19R%icj+%59E7ip6dj9$kgEz33-~d zs(_u!0AoNBMrdOMurT}l7WDf;&e9EklZ`x=$Czu#Qse#}-OG0s%;g(7OA8Bjq+Whf zbWsD*9=Ur2JNU2b2k352_!Ou36Ya(ywonsy=VXf=itF_dY^fwmyaOvLrOs|zRd5Uy zm>h_J`{7cE6`rFO3YsJ00H-NO!%_-{T9@`=ii6)JBqeoRkhQ5 zj_&UxD?};thphgWNO2F+|8Tk4DZSW<4bgfM-em~+Ay$=xNzz@tx0<3gk~AE-ptU!n z^`Wk=0D)G$T(a9LNT$@RewSQ_K$wRjm8l3B27rs2mbSPmH6h^w0tbkh=0pzZy{+l5 zbE^mNDrK`yslrG@3&2LGc-JOW5dXGD{xBa~TSZvtozSXO??p(x^>lb2XvM^A&73m7bmAt-8M&u&2c&?XA8KsJxVY9MJnC*CO$CrW|LFG=X`i*h$_CvLM?Lrs`bTbm zCt%-&>Bb}Wx^P^LH?)ERCK%ylxgdt&v?awV6lZWZD3dLGQ5FyDGwRKH== zHtp)So^NjIDl0bj@EL-bV9x;Qn&0L<*M~e}DrqD&)~)CX0b02`(i}?T;^SK;eE{5N z;))y1Qg_DY^c)F^4n;jlle@MzBGNsjipuYIEh5~A&z{QnJw*Dog2Wr%YdL$ryY|!p z5LfQOBFUIYpaI=qJ$i^VYrZ$D(CCtwHL(&`Id8-7lklkeEn|C6pO$PPQq8Cs_Oi6E zjzcg*1B}Mi5Wng-q7@qk!LBS>iwAYnGk))SV4pheP;2ybBn zk!apayZ3{3v7X>Z$kXs3L){E`Z^(~A-K4GB-`iR5fHyKllvs% zpAgvsWXtY^97Uld0z-lI1TwFTLqWaBz|aT%7*+(q&cuP@JOf@uri&=lJnDbZ+~$7v z1DgAP6G8s}nYz25#bVNF$c&p-DN(}<;=NA);%Zi;_!k9GDCw`8el|iE=w7wu+;AQV0mIDfRszc6m8c`MO4ZaWQjy6Vs2|ZLLjx--kNPd-zY0)^By}pE zW_#PZDJ$b(@$WFE(}9^@fLG%X>RpR{g;D<{`69F84)l0V3vA&04Mf$L7if1)qj}HO zYFV0d>)^NjONUV1axE<_z?+9(H=uY`a zKXBILLI3nm!Ryx;xsj{BQ%t`!xtUJGCIn54Jo&{T6sqb!|01XPk9GOT6BaBr3ia^s zJ-m}Mk2up-We;KK-`oq)CDAHC#Sx=2fLqVGwQ#(n3}HI9<-o5pePv|-yu1Q`*ny}s zH$#A^+xz>=EDF>#3u{2BbZ~a&tBYWVLf$l6DhySfro_oOCMM=iNRLRC9gTV~l%#ht zh(1$t7%32p?NM`HM5sG-XmQg%`-(Ig&^X@lM}gFF?}V+bEmZk*8gh6rzo$rdNILtf z?L$e)Ni==b=h;VhLf{{|;}5Q;0$Rx56?O=mNcUo-x?-HLLY*Ha-+HHHLv#JRH z&K$g*LYAGqwKY3DchP`72m=aC@NYNGkws}bS7CGww4G;^w2Dls`q%jRd5#|9wt^cH zZHfTGjdFhq_;0|zBBy1(Ml};_5OTO*zka!G{jsnSO31kdi$3pl5RdPyg>s{!qL6z-8bC28C#Q9y z?+29k9nqv%CoFk&z@b31mTV4q=lQnt=8T{3dqj*Bq@4YvL?_e1tvvy=c=hA(#x%9AUvYRWZM__KCM>PQ?=Ya`DeK?{!b$15n3d# z9G}VU-S*qvay5*jL(|ivo*8&ypc%T~s?=gX{m9fKDUB@fqdbH_AkB5ETZMW&ivpLg zi-~3HH>||++M&F|oUrzyNw@|-iR3uDM7TRFJspaQ`Cud7SkN;_jEuZw_FKDCzdOrs zUViCH;8KUR5C~=PifR+)&%dToCF4kFbz77g@GhS8ZkF9&mF0=~QsPT7f42xH6wQpn zkooArx`!#gne$A^hPEhw`4q$r`!zm3=N#2SqeAXvO&2S9l)#(67kbvfyWk6f+xPxf z$ib!`4*c*wQ>C}MK?vjIa#tAsklwsrE-aoLWuhsRF!txmcg_2WS-z8muYRrtOD+sv zd-QE}t`XCY;)dUR?0uHE4m}Gwh)~|tE{M>smu;47p=92HgdK(@E3|*vt4%?a7AsoxPGr|733E;2pj4)Q9;4?h@QCaEnNrEQ9PV| z=w{>enNyGw7@$gkTn2DxVAlxSt#!v=+>#HoF@zfPLkcLiDV=Yjo5g8dicpuH|^vwl|ZCv7&*=K5s5<9c<2J(yw2ZNVFtk41JhAx{kkE)CK zGQS;!321(7Y;2EA^=BX5FsZ5f@ZrNE0lyYXzhT()qmH1$UKd}l>O-J_`IX9Br0Rh9e*FoSoisl>eViv`I#@T#veJkL4hT;j(tM)~i%BZ=> zPo>6I^uZ=Wu@JSIp3aKtJ-1I%0E~gK2DK5OY0R7##J9uVM3HH2GsSn!R+$s@QaY`U zCoedlnrt)s?C>MyN}#8{lz3g#VO!39?v;Zc|CrZYGO@OCM>6@wW81sfN+;u&^8Y~T zh_iuEv(jmG)8~r^dGYsxeBiYBMHZH!muI-rXl#rh`ja79#LyaX8dX9E(uUkgP&%Iv zQNx#8<~Ia|{`0c@lCDEXXs7eZQ1fZ-vd=|3 z$^i89_Kj7C%s7hreCZizvn;hzp+_;qsB`-G?_UQHZ7p;Gs%5ueef`j3)ICaAcnDLB z(_y)jX71cW=$5Njvh2+I2|+Kf+;n4XUs$kFaawSbfRg;q3Q4<@WL>MmqM}*Qx9#qW zF|A8!rKhKZ4lYD>3xfNl{N>$h8$(GVfzsQsQJoD@D29g(plaV#>SnWjX3%mBHOvPq zGhi!+S2BCfngD>%SKCU#*4Uox>H`7XJ-=qEkFX7ui1WK)6Pz*}FVABxK-kdzCn7 zuBY;ba9?f9A$1~PnfFydN$=R^k?!AGHsS~! zYCdh_b(-0HxZ9M_M>8}i#TptKNI!I=KpNC#QOJ%VG<5Nsl!kGc&|>Kbcmv*ZqWPLI zAIQ{>;{)|msKg) z_^VOMIXw>^xVqj6S41ApF`8lafo(@w^YTi4kvXRnS<@`i-rrH`EU$;Hl0{KPX5D$g!_bK2u8SMR~Zn={$b?7+Z!{=Va?B!`NU^B^QzMy>@hR zlaAsE=oVSfDgW$W2)=}bf%z9}ZDc#WK;d)3^2BX!H3WQ_4aR?}TK6_hnG5R7udSEw zDJy7DxtXDz`^RgZeF&|atCOQj(XFeFV8Rgk;P-4w5%9zU|C$VeIuChe5o;vHtPiVl z&`luAAaX1kqc_S@N=pTG^K~IC0F|Ldnw+?-N7bBb#`7}|@W*=F)|OR!go=cIy^w@N zo?`m@yV;sf)jbc>dmsp=GLz4e2jUO97qewz7Kro!g6NNy8mL175TJs60U%;j$O(fQ zC`MjpRc`I8QeG+N_loAcR5@d&y5{EQIIjNQEKBg`#Eg?7f05!mubg`YnH~LII>1=v zGN|6%%#k=?n9#Q&=B6-Y_aqMhAhRWX@PT%`C=kd5*3#PbH|>g_?Sn>o2hNrr(tsH7c}+@j$$~GKjnd z%A&CMlbbM0(H|?C%zK7fqerpgq;1_9PnVmjXKVOjddchda&$47f#Jc>D4 zPrqypy{a}>?j-r!rVKO()}u2u&YnIUW!6{>Y&^6`*zgQBy@+AV_<27|y=kwMlz_c$ zU6yjV_m?k^d8Kf5l+B1ofP@2eKO*;Y#XUocR)iJ$VULV6A)^UrTo0Uy=-_ zsQ1DafBgrZh_1-EGG^b8zg)->^W}MmlPEFKj3EEMP?LF)BiS=z^Y_T(FL??0CZs_L z;|@bXNCp*IdI~zB0{yA(Nj?!K5acqob02PZ8FjkU7V6~(sfCDwpz#?!-ZA0K#o;Tt zTn9Z923~Hg7f}YrE{rz7ZH@<9e0F72nbTxF5%d%wDr`7Nhv*+oPhPN55MND;Ln(#H z;grthAvD0=t#SiiPL-)g?{a`4UenDl;CeE>vzJ%@*s|21N-8hr=kgY2eX$f@=wT}E zNBjWMq>;*p!iFM1S^D#;)L}W+7J&}|`~L8b_tg9P6RzLa;fqJl#_BK96?-bQMjax( z*USIn%DCMC8gv%)fv(jVuN115qYN{cc4-0yPNnsOQK0}CM{-gi_I7ey5C7svQ6s}1XB{5D`$9e2=|xGMS?YaM z48t^Eee$7fC8!5$`0_g*AM=+PO)rN4wPD$@76K)HRy+bGCqOs|I{?~O`~tAVw5&xGhhGtMaWMioJ=H#n3ONS+LeJgrJC1N++ zL&F6xns3h(uIcYh=;Y+)+m*&*S~x85i%H(~4O3D5 zowj$kef{qWCKI-nr)yTe@>D>)fVsfM%Lmo{sRwwAOw+N;-G=yC9@wc!*}bX^g8Cgu z!c$5^J{X7ID8LG~f2;#sfo z+4(s4)XddMW4xVe)v!B*F%N z(v3HSoPK+eHcLV1A0qK~7%my7`&Q%I+g`vRimJeg@W9*Ks0e5O;K2jz5<}B>L{tit z5s;N-K_9(o?hu*lTww&=X~j?0kqT>H(HyfC!=Fx;1-vm7BY5t$jVh`>VK)4!$5$~<4i6q& zG2!e#0h9xX#|9pI9*Cs(Sv8nn%#-77=zwcT_-r_WrX&gYfBq%&mHGnx(~S8bbxiFi zW#xKGG4nBmjvA31Q3v`Z@i}1!mMCkCX}&}+zhdz;$ArV&R>S0ZIvn?s!vx*MGTOf*t*QQxVC2=-GIJbJiO7}{V&8X z(9H4?9k8hY7x_$DPclnL{168kHY`K@&S$fwkL34!N)TmB=3b($dCEN$smb*%UA-bZ zU{VB-f-=B3X~GxZa#i}-J84ojX%Yz>6BD@sJ}%EqDYa5jbyC~k+P1W2ESmOAW;*tpqSgxD^`8GdhQi`D=ZU!&wD!*L-@l?wqXllJ9(0-gH1==qVs9#(eA?Os!cy|S zEj&*yarvF?FNKnL_P07QAG#R;6G~WNo+40zig2nDLk~rhI)kbG#nO`>mh!3rS45C= zHjr>YJvPdcalpAnClF)JA0w6YQ~M6+=Hhh+gtVx&&Z5s4_4ci$7kmQz0I~*0n0Cd#R_`s!S~3{EUDTsKQXw?u3xB zUOs?we5sm9sbb3gAG?gm9f8&`CW?FuLP#`U9u`E z$wi2*rp&`~B|o3{As~q#DhkeYXaKB4DSjIxFVYWp+uamHz^LXxQjjJ3ye;>IUuH|_J`~%9v+A4_S%9Tv@)#jGq5G^B~&_&v{Z7-+p<7P-0x_Sr|2>uhlzE!fO4LjkBP|85? z0R<^sG2Mk>d=xXjsw;_oDI?hQ<6vaNqLQh-7D9IN_*2$`P(dh|+-flChMIBi{P}bm z4((@y8T6RDF}LsBaWhZ z-QLYc)bV7$O`xRwR`%Ik0!ahg9nuTU6ZBq~GdP7&4JWc56_%AP2K<1;FK7{QadCY1 zV@&HSDa*^QrjI*CMjHbfsag;=_*$J4fV=wYm|Z(qH$l-Xgg=4Wq|yr$FQ7rtoL07d zUk4mQxdm}7Oy8mG(NgcTX5#9e1|K7^YM{3V!a=;L7@?*Jo>Ca(J^i|$bih`C^QoO8 zXAsM|t2p=!^j7TWRk55{3Xnsfb4@P`But4%_)bm^W~zYN_h<(iol+z-b^Jw`ZHOdk zz<7uCX!BeJXi)zC{%r(Iya9f+LJjc%g7p)FC(MhGFd0-$gBPMbhg_Q8iJc)Awe<8f zfIZ1S7A}AJQrD{9+jqZKw{?e7v6w;-oQBX)%^QE#clt2Z7x*;~6o=oaWFSoLVL*^Q zGyHhqEdW%AQLvw2-DR`vxD(V7l8V6#eC+ivg6Xy+c1f9*2a7>aKMd`yJFttC?2%7C z-ctB>q%@skuw7UKlw+{_6(83+C*6gOqx}Xm#)}s&M6n~|&f`IJvMd)b)?B#&xEmyD z=szGuD}=8IC8%#*W9wD}h?qS)YjYv^mH14h%#c(Y#cTbX$;WTFGu-2Hj+F~c3}gY;{6 z66ZCbjal?YWs0FP9@AZR(2aBqf#Im5Xzk*8mSm7vD?A8ha3`RHL##r)j9{2H;@=ag zZ#G^&MgA@wv;wM@su>l_+Ff@`%YS;SGn6v5tLNRpuLg?;c5m*p$H4`-6+E5r_AONm zH=grZJkP$9*ygPK`T9?2(?n%*gG-@icgAx*`@ntjZLezQez2t#N6n+dijQ>PX&@BW zYR|%2!knURo;=wu5grdx8LIA`{DEHh%KCZr>~SCkP$40u9jGxNxHB_P^pnW2xdyz^ ztP~~xyuW>0B6(?1c}F#~_l%k+maUZR-DG!x$FnO>DEMHIa*9)ns%5yl@-hBMfD@iW zt%2Y%^SrN&7g6_w63|*P55z9HnQo?WHr(uYH;Va+Z$c#O0*~p@yD=CaU$k-Uw!hC) zW1Xk0!>8`{LJOf{?){9yz~nRk_LUq1-aNBn7jf4p&1`6<8Q?7f8we=J5q|GLjf8Ir zaOQ+&eZ)bM0$75s0kWkV_&F9rd5Vzv_yz%7Rp3tjtsK^*Si*aZ-UJ2*P!mDg7)S;A z>4{`QF@i_}ztW@p+W!?c@cDgh^q8T?ySp%BbSF#{g3pF~@iw0j489Mh-o@w5eO+%g zN`kfudkrAhzwbVFBa|Hx?}vF$&c}I5X)1h^_>bywBsaQASOf4AfD?+=Ah7#DGGR1? zjIL#w<;dVRhvbT-B2YCUYl4B-7TVau+?HA%6zX9go>r8ooM!}x-dM!fS{OXfhtftNr9b^&6d7YYQpD&BNZrtC`Z^y zOi2OpAFYLXcDm=O4j5i`q#IFSN_nTcsru) z5~IRSwB1WeO`Stin!HPmPj>_moC&jJr~1eZtH%3XDNgmN>md2fYzr z^z+ScbAe{#S`lGkR}$30zL6~M`p-VGcs~yY#qvz&0*`{5po9Y%sGO9r1#c)+Qs7MI zNd@2+x3IYMG)Lew$U;ELVFY@IAcDdc8xNd=$rKtdAgFYXF8L$qp2>exsr|Tq1+|#M5^U;bfI+z-yuUC0fbF$Hy<6cvi%~z;G81 zZj6nY^uI^VUk3&TuZOE^=k~|$RKEo;tO8TB0;p;4ZzC8)I#{0SWYogK!bA3zC^jWT z76l>7#eX%)cw)m{Y`*q(`N(hF1Rbzw==U%qh~E_$0kjv{j{fsQ4)}G+t5cS@VU~%^ z;bGyzCC;M!vfyBbS+Pf^%U63f@AQY@o`pu2Vw47Ye;4ogp>9bkmHy98f&UqycV|OI zwM&F+BeQH#J62F3%ECShgf;NiCa0tfj*X4goKXcE#88o`1bXo`jOd}VfoxY{gnzkk zYS?SExO^b4bIKBF7G37|ZORRiqb}8RDcCqX^gIZtyY2fRa6@Wb7c_NkqdPEZ%Fc<< z{&(J&6x%Ti<2v6FEKH)V1w5pOiD_@Iw)l6~T40edgOQidAJDEa8ebcp&wMU4eDKo_y?^)L;rc3KXcP=MD_rf>|CybG#V-DUVAnTyeS#E35(I zdFPr$jwt1M#4ej2DZ|~h3R?k878V6s6CltzFQ)BZh~*OTG{AyRik-T4_BfMaqo2Qs z2EYFmg#b_ir*`O3qL+^zf_LFg?Sh#jteN!w?kW@3pZ6TD+R6+BOSdJpB4-sSg4umA zJ2Z~+tR6e>3XUW?K?eCxHL`G9J3C;^1n!+wbjVOEsOudNHwVs{z{1bgLXh>qhE|ao zYf9tN1OC!+?gMJyrPl&JbR6`{(G9DWC*+iCV~@L>7X3Xt$)B}HuPx_1o9D{1TXC{Z z6`;BW83i<0-xMADl(qZ!?*cgP!X2v(Cr;|kCc8zmv$EUe1YKt;zu% z6lmL$%|9SQ-=*V()J57@`H0C-NFcaq5HkgBzNaXd-A_|HfJ27C--!$d8=EjuLGyRZ zJzjU%AoRL#p@X>`fb{Acu$x1q!buG8vPkf5fp|Y09CG|OWXiIqNwtY)u*DVTND#;# z415JrwlNPwt)xAZR`7C3@oF?jSS4n`%VBs5qf{_m`rP4Ag_11AZ0^x@xK9`W>&DV} zJ~&pbOeJHh4@s7RKU9rA54&P%lNc?;ysYITwV zP&DiZtX6`0QB4MSQm1=H@mJ01nj*l>I=>PfJM2}92Xj}+n!HQ!r_Z{gg* z^Do16zm}P$*z4Y(e_xESy1%YRPW~LAriSo0iHYD8sdK#GRO{$rVPTPPQG^Y$qFLRp z13{VVk}3V9d&*kz0QQpr)s2W-%~OYJJqUweVz_|KO7?em-`0xT+r0Sqo+WVO?ZX%* zf4jb7(Yw@jrUy|SCV{$_52JWsF3sz;I3U-d-0^V3^;6U?-JoWIbC+XbtSk+re;Cpk z@D45y5A{YIoHh8*NiDFm24}X+mGi0r-w>{x!`JCG0x<$15HkzjY_L_pj|Trdb^3@q z-sEoNk~Xwj*Pz*~yqLgF1tuhb^iib~%m=oF>LQ*N0hsl`Y$8$33HAj@4-RZ)+KBrk zYpI-M5)l!Rr=1Jq@&Zr)O~6Ia6|Vqg*2_mb8NI~SoFAV9zkDVPk4@L!fEEh=fro`2 z+0cU@N^Nd;WCE1}-5XpklK!cZay!oxM5gC6)-}x;+z#@K&G6ybcPzZDj?zXRd)R28TB$H{QVQ zOEJ?XU5f9pNnAJkgv$ND~;nL)61zg~GGU?EmxTy4j9IDYKtWgov9KpF)!w20d$m!~r5O^}$#06irq* z=*yq7kVed9j-m```r%97xsPZ;G)e+N-88;qmR$v83#eX@L>MO?@FD*UM zh(}7Eotv9*9be2BdHPd)b5Oxdao3*zb-LZ$x3cR!az~;Hjd4=zRfx3WjETb4Sgh)6 ziVxZ*X2JTV_uN}hW1$Dn3uM3^IEy(JZ+FkZ)HpHb+aIR?`6RbbJu6z3(t-s$;59%Q z0VS$5n*}%)Tv&-dvNxHzhNtlhEe<^`pW!cfW6334Wzp*A*6ipcp%WEmj4_~tBzD^NPdOSuV*+Cb=+FoA6Y2Py46r{4ezW-By~E?5 z%i?N0f!AP;4YF<@;s}24_{zOZG`?efZSn#Ar_cbHnt<&^ZztIW##HT*ECgH6ct~@Q z=hA7?=Md%~6+kbHAdXw*W{$(4O+F3WWh&8yIy_N&tP}PL1_ldbD;bClAA|o>a|JRC zbQI2ejO}*-?SK&k+zLfCZYwY~AE()j24-Y36b@(-%#Em*^p2k)y6h`1)|&>>jJ)LP z$+82tS#Z4t;$4727_?A;gUVLmxO-$OrXC1|rQLOqv;*}$snc2ClK~8yr(en+_)G7R zE4SVohgJZu9q&`43iR)zeM#RU0M-oa#CRF&VPt>@%bLfw$i>OYXLO|B362MveC=~) zY>3K~-u7*n%X}Db?bDG&u(&6cG)^Prs z*a8MwIOTwikI@P=)4OsOJQ!LUTF}H`jDj4~M9vX|V>C!sGC-%^f_}8ZAs*UNu1+sZ z+Xu&N;ZQ6bFvdu441jG1U@IIKfO(dSJ;O`+wELS+V5SN}r@#rr2;h%)9!TMXY65(%*t3lXEFb{B_^PEo3531kq^^jUbQ0?* zLu6YRC34m!2?`07*;Z)r|3P#W4Y`v5OO%Un8)G=+`2XC$FBV+!F)wl;J>{7Wl~~QL zZs*)GK*q&jM=5<(RIHZ|z0(^xsxA3-1Z@gcz2u+2UMf(RbwkP8N&oc%RF2z$(cStg z0;81WbMa3qy z1D(LVLj67)Y_T?`eHxkJ4#sh2z+?mI<#6u!Z3aQ-9F&S=v0t}~`t7h?TK(gP!A(h!0XYr5rw;eZ2FJfQK{ikNkpXy^P zs#)ZY?XUs50~4Jh@vD-^Svmv|M6DSBCM`KHWy4p;Kc&Gk@|IYw*yzt@&@^ zkGeoa8ax444x|B`)&#>ITCEF?exO9V{uk_Z-zE3rJqG~>xUJbHh6x7r3e(@5;huOJ z+KzM^&WRa@Yx`W7F`CX61D#>va}*mW8~?&Q7=l=@@GJZwj);B63i>Vx*ztwTi2!ZN zN2qoPZkDMRUbPYVTzkwk!AH=-EQHV1sy7H4P{FzuC~p4Qwde+C^B#+j$KaTv_jthW z=2}E7>qmv&2MnE6miJFfV%j676IB-p`C~JD{8wDKV_&__e-xvG3B5b}$T40zN%4C0 z73bleaI?Mh!lP;O+7!p$F|=;p6%n~i=Ef3wGUd}pukVJkYcH;nVKH>UK;radxHqJr z|KZ)iR;dE#izK9^fKwO|tDm4ts;hIkv{KHx?|IjQ7qClRJ~4#1V{IWjl{+-;M7~+p z0FS=wKpEbgw5b2JOx-Z9(@E|7J%KkJu5tLZ6FUo&nNHXaDoR+~%ct%hPw!EN_knk~ zNOi<}`Da_gk@}>hq?*Tb5qrS;Yr5$vpFP1~Fvnk!RTVaD{XF5fKA;bC)zR^B(^0QV z3}%Iy`<4A~Vv)RZG&>PFF`T@eBKYguP|BR0hlfX21_V*W#qi+ZmZ|EPR9jfxc%fN4 z7!*1=I)0{4heJGY+-w0@UN3w+gZN!?58y$DdJW@_quK3cyoPbHF8XlXqG<)xGR0TU zS@3$gV2cTp<*Aks)jpk6Fm^S6-{$*7UtN^6`M_Xf@XEd9VN4-%mT^FKZ(5dbW(zcR zLb;`(-c?Q56Od(- zzC3F`y%~aTdCY78+nUgmw)xN~E>=t%Y9LIWByQX=9NsRn9F4yeF&&GGLKP$~H15OM zL-}OQ`$fiPksOTr%H?R6`a3B}!{S4Gw<#6b#mjW`X{m zo}P*bSHgre^^s@V;h+8c+_>-5jt}zP->N`5J>34c+sKnV}-Km8@iwmBUD)E0X`0B(3tKm?Jvju$%^V_R) zp=rvIEV3zJw6)izq@>IQI+1NvAl=mm(O$;e;q!JZ8sKTcEDxWLafY-CCmW>xF|V@gvoIN#C>*Ff)hJuQkh0oDe1G}K z`7+)V=9>xMj5?u#x#CsBY#1Wj?|WycbH-HbEVHXzZg&;F7|x!o_a>~}iu6mYv3j`_ zPOk*wek6zAY+5=gA>|Mf_Y{Gi1g?a!&Kw~`~@!P&v zICjoUAq08tjZ3GVow#%P*z1-LBTGq8IzGTo*;jB|g=O?*RRU)k0(|ravr5@Oms^olOVs*x`i_i?dr>1}#&!!E9q?bt`)9 z_sDh9N_->l<$i6|X!c3_>c+-Ym%B!pGm42>!uZ2*LW@JW^~XOX|5SOZ+lNU0=5~o7 zW+Fp)w^yBT-zz(Y&3|B8;{B=KfSbcpc9RCE;e+%O@M|Zf18?+hN zM|hC=+j`$WOyQHxBXH5%K}P|DSfus5f)V%(1(KNRNz8R6R|REUf|vm?dkbvq5k0#q)$9^x9!b>8cXqwT0bUA@)n2Y&F6=Y%V_4p4shPS#Tfo9?`SAV1(|ch(6B8*=)OIjXFgnU6RGXCp z7-?}{DWJb(_dEb2{^~V(F~bK>Sy_*f-@}6+Ge}DdC?S9w!kFO8N+-tBDl zXjo-}o@_Gi9Dmvy#Zx%q_zNpH2S>EP{-~CeX&yT*F7$~P{ae&w9ojA>stTX;Lhh2g zhldC~6-33X*Mua0&3Cotx0Tlx;h;1oA~JH-wz2P5bl-FUpm`ws5bp^1Y4(>v8GJ}i zW&$z@0!1~`V~LBBp=s6iry{1siz5uz^ti`p4CAgI zZV$tjKma&;hJRb&j~1hm*HOfmgV%4^F)sA)E~YQ&JVX9gVlOtUXnFIcgY zmMuri3W7BIw&0^+*sm5hkOgAx6PrBso2y40p}BIXXeXvz>w7aTt{2T7IxWr;L@H`e z*1ywY1m|(Th5>|GzKZRv8=?Khjg5Bx2?4vUm&Mxw**C`d1a~zC>_68M#YQyIbOi&zJik3qiOgK_+y6H7>CNM8md=jVxvoiKiW7F*`rll!#v_5c zB86egezICJv>`20f_L0bD|-99f-`VU5~ZOus7g>gfHB^0*tBi7kz(f2=&M|+R91<8 z;NqechbFGw&)^Qka&`zo6nwEx6QEMlXR^e)_gsR?aW*)Vo>v~Q3-qA}!oBrAn-Et3 z+<@SLcQRo|)2FrHzM@VpU66C10JBJSfR$pQ1o(U__z;KNXBCdO2&CpvpbYB7%&|}m z2aicx+O{^V=8uex)w!0<2+5tCNzd=H!eZcpP}t*aRHNnX63s^6J%W2HHmlc8%mTyU zx;IH(mI2k|%j+$9#X$sxx^n%r;PszBm+a4xHo@Bf!2T~9iDv(jv?mb(Cw(UJ>Jlb? z?KJ!pZzxeInHGoSk~=BjbPV;&(FkAlIal%T!Ya3Zd-19^AB+yrWa9M-&t5uuNQ3jt zVY!F4GYi7oKWmCWhpEI~GyRQv$iVAul+t>zrQyti21h%}+~&R~`x>>cHG)5%UFA3f zi?l|O`cAKUz1badlmX+#b+GZEl@z{(ZN0|-3KN8FJPG% zkwge+{C1lRb)pIR_<@q8zpIgCSBV9lSNF%gqwKpzFJNMo-Uq@)2H*;AOGiEk!N6nf z#Yb)EeI5^UJq?5IXRg}dJifI zz*Xp*8h7~?$?gRSh`^Ea0?E^Y1~uW~)Ob9;e!U)tF7;lc5z_mF&}35#SL^}eZGgy$;rvpl@`SlI0Pr7gAm>_9>ShO-WLW~OW?=8TR zoU5SBUhnd^PTc<}r_HNsxSDm3H;WCfryqU#!Tvry94erKzPf4>N>il_K)N?j3y?nu z&jl#_4j)MNvhmZOf4^c16pwx=%KsKgqm%zqx5a5E?0XSXjux9Z0sTg4NLNU5$zb`S z2JN$e#A3&710I~dXT-}y6&F3VMn$Ty)aDcEeCJjRL!7GXk=k3g3WhUTDfMDHAJ-qh z3zlyJMX760+%KZ8HTD#cCL3L)tRLstRJ`W6y(3iZLJQh5An^ZV+vAPt)S=`7*JNpR zpjm!t{scNWa?ABVhJ0J3AgI5*QIq3qHj*H|c}@J?uez zazo4{di*EKq0Fj+w1bOeirHQXQ{ zy25fEr6ys@7!h^SO7)j~TLmcI%FN_=+XEk+SN+Gwc|kr$LICNAgRqBB#8G7h8w>OG z$K$rLNy7v>l&nituMxjxDWo!kYFiG}@wr$0AM>T(c=ZwZ#?`-(P0=%n(%jyH(aUvm zf#?!~3hLN+;ep6VH_NyLnMwFkYDi>Da^olV$vRZ+hy>Gm{mY*3uD~ zv=8WlNCA=yY%fflyR3{#_x>n{VxC_kc=TD7QZQ} zP0~pd?&qSKbp^`-lv&*qKHy8c5JdvU4|>I1wV+A^)NdBzTgg(1-P9rD@$vD|7^6mb zdNkJq2QY#=mj?xrZwD-4Gw?B3F)i{{9t_}!7@qHIwrRz~(YB~iS5y+o0m$(ud%sM0e?&^}m= zOZTYqsKL`W<)}OJP|cF{Twif1vAAj*>5JK$-*5czZZ~$XM;qGptSZqEi$-o~q=Dhz!(NHVg6?ROtV1vP7x z9H}HcU=Lb)sa~2v<#Dj@dMlQbYKd>`wNeyHsM)e0%|6%U)F;?Ol-x1(5yK zVkxFxVRhBfqMb-z989Zy{P=evH1Bc}rhKjU2k*(TS@!peGi3-03VymCf(^4i&@4aI zfY>zZ!n#F&gkEDr7#Dl$;R9dmIZxBsz7n=cB(do<-%7GgJzEoXz=h~~;Af6J5jWJg z8vpU*$B}AIs`mt*u<10meOx!mmd!yTQN%*P>ja5d2V4uk4Z-E1Y+Nn)E*YS||DGEg zc83<7(*4uBq=s)j+)NHUMu%&3?l(ull`A)?YoUdb$6S?VRPqWIR3-0<)v||2JCLFG zj>$CW-VQG=A631phJ)%oCBTi!aNkVqy3qO_C(yBc?t!qMuqEClK3~mHP9yrl13cuPTTTcBCImHXE%YJ@D&2;LLeF1~mLSxguwJB(AXvF4fPJ>cE=FZ#9r zY(7~97FD=)1Y#KofeteLGes2~r}2!&X^9FheO2qv;;E?8&&ShdY9HZMb= z%XzR4Wz~dss}w7NMMc<;3bqli=MJdTBRZ#^jHlwB)|>b9Uqm1<=bY&#vplS7it5;f zrE(;ghFjq&vh_o0*-RUH9U4%4uaB8J3Z}0Smj2~Sy7#m=PblpQtED0`At~)hFaYgY z`N!2>&h!LWs0H9Qje@yQD?mVK7&!E~AxUwO*8QWy-#+YCa{abL1+DwUyqbL`%6+;A z+RK4sfYbnGAhm$ur3V)}&Rr6)nIl;$;UxzQi8*Z`gBX%Kk#|-rC@A1s_z1Aw0Jo8q z4*iI#A)+(t%^-%=d$QN0GE+34_#Xtu3OBh|mb0+q&srZmY?{|0Qo_oS@aVTbN zwB_faRM&FDYlyFK2V(c?>Nw#K7HG7)o9*~vPJsiVCuIR#&hPF;6As|W?J5)^4*Jbe z8Hwum+!y2ZLqIEpN+u2fOHov$l`-=lXZ-P_t#e`Y-ItjMA?ibzi(vEVhjb@9Ay-=t z-Sx%&H)PzZdN=(Oc+9_gB&k6yV)YDP*`^$$&s*s1ae?GDR*bS@MVwExu$AkkKU67TgVm^s*b5_<_EN0&tK*cf~nS=mBY z7;W9qrHTbe+q6;T(RO6tzI~ZC9>>d}y?XcdEhvk*3bTE0N|bfCwcTb}dt1_B^{;v0smH`H^1NLjgNp zRcvOgU4TO5>$YYHn0+efGYp*7CR+LN2)3MQbY8&>d!&f>z*sn>8t?CNi-sf(vdYK= zM1xpJf$iNuE{&fRJ`|+-OTe6CdGBLZb! zPEJm=a)&|9kMZ@gfnd3yeZw<-`m_g~7@uEV<``t=;bAc8trCPbRte%Cv{7mAAQ&^6 zyUn@*3N8e6QZXL50{$qbQf3!!UI;2d>O|}IZy;-rA;9H%>e7;X7Y*P$VTd~RT1 zAfUSSRJPNZGiN$IkTKNTUpHG<+RXnnvpT8THuARtMa0Ebp0}XSvGVcpiEotu8$wcf zIjsfZ*?(&Rl4NFjVLCTlzk*oxhaZ?h#CxE8bkdApIki9lZ=37Ur@gUE*R0`fPL$4> zS5j``>h8d=Gh76^TtENQZK>cYDr;SC`LNw1GN-+eA1-!kusznOP~U!b4C^UB!<94ngv~`S9UR zopi?%uMpJAI$GWH(5g>h)}KU<9m^VFg%DFcLz|QGl{7oG*f$<9w8&V@M@ru28yHuQ zo^}ZBB4|}e2o1mQ_tuH-0+UFl^K{aV`@jd=1GW72z%aO>5WB2p@)HRlsFGV&!=jM=D4n+*VxhH{CrFK?6*vM`*b8OB-X$i z?nf9+r5us?U7@8+?Keukft6sKRzAds+gPCA&LK4i!KB7spj6~A(18#{WsAfP z0qkrRkUSu<5>yQE*IKR}Rz)pZW{N^$g_*Z&EpOc~YJ>y|5@NUVo;=*m%@bt3#@` z;MmyE6aF|Bo02K21*SBpd}slA&fNL^GF0+;HeGZfCeb)8>AkJkgrcqx@lH1+_97S7 z8l#;GHw?RODP7&%=VmJGJNebJ>nzwNwK&e zn7=bXd-fk3^K^6|X*~>1QndHrtje1Sp`w6VW&G&nV&?x*uDZVl$8%8B& zl2&0qWGa9Jz>+7$?pEc{MZc>Z5r<*dI*=t@PtW{Q)}a3D26mEGR-UXJaI>HzLK3W^ z?cJmVt}`OoKrWy=!E>6YFf8DH>>oWEIfNdNSN~C>l=DzUi^n+`TYK``*NJ zlZgvRwU22}|0!CDyak#eK-er;{&IF)W$}{-Y?4qKSr%!h~KYB0444L|F zGQG`iL*=I^6{tB7Gy?ZjB$?u#zQ-(n5%KlL3n)KX!TG3<{*Gfdy6o!G5$$<+IIXw9Z9)#C$E&)i-Vg6%-+O40)H-Db zX9Xm(o}v!HH0D8@drQYHd08crLb8XDq!{qp5o*}%5?D`W$g+MxCj zboM;q8$Bg2k#(bweOTZVKH{41(jSdC!15!}rQm#oet50B^~GXk{K!a>^x~^&B=Bfk z3Qa0a#a}FutWS}X!)%X^mu9ogn^f>yUlr^c;TyBno{`+KQ;Mu{89x*{>faqqK7v!+ms+G36Xg1089nWc$u{ zOtQmz5FGA)?Y&hmvGws@Q?VBm=BK#P{%6$ zB5Eqc1t6bY`UXU=-e|ch7nt?uGvw;1Lwp*w=)9!9MShV~!`HLfAXTx%SVSyoL$Evm z>jS(Rc=Gjh%^2lSC;6pT*478aOp1@Pk}`ih_N^_r(d{I!dI@)FOlic|gYc?F#&(;!jB^;rWHD5fDr%ig(d_Vsjs}z?@i9K~Puy$K6#-QXM5+Vm zjr;(XtLQ{?Wv5)VVfmi~CNt1OtW*tIIlE|s6z!Uo>x(S2^=LemoY|^fWL#q>B2}3QfMLsaK^fbu_J$~McED-i zm4>#cLBs%z(AqDi6h_W8LBK;4P!SuR#MKIGO5PeN`-^{_nLk2GIGTks9p8VZ(N%sv zUI2K6dcmAwiB=s6^+Q`8MRMmZ`*)Nl9JKm3B#ViFo0%gYg_4Ir6b7%k9#YYVHuKkL zJ-H>5DPxn~n>KSH1VxW2Rna^p_9I?HGJKf}rTrc~!%e^+DFFbHY$7mRM_~UrfWur( z|JS|F$Vp49yQx2KW?P_jFwa~2sARvGso})2k5|P2_wN$1$`v@}+}%iyi}@t5$W< zL!dpW2)obnmo?H_5H_F)AnED>sYN){hO^z z<*D0FouQZWPtP1-z|%~7LLg_mmL!gJH^P6ng-MpNqF03$MyS}bE3r@|gI3BR&Wk#M zwb!hV>Np^u-gR-;Y1i82bWTND`{&onSuY=gJd*O+lhq*?mc`q*%JJeYXK4ZCTbi2NGhRxx87dvQ}(=EF7~xK8#shtI~fl48W+0W z0U4g6J+!DYJfK|;z6iS$QQ(|&ZOtXUvlj@#jI=x?<9A>e+qs6PAxnt{)00ypB`PC7 zz;4L}Ha&1sC1{x-*#42$f-UM=Fg)z2&jcEDaeiLjE$RI1fdVmb=-%X{7f2@~R}U7X zLUyDqeEr@WeHPOT#?V2qB=r0>jho+PP&L!*yi!PL^DkIf8jM8jp?ku9!zkz&iy8qX zsVUPI#L`F&D)vWR2}Mv39X|Twd(3%if8{WF_V5$db^gLGT%C4s5+F--3V?qOj*fDg z`773}yFa6ly-)?vmU)isfwgPYwH}<9YJ+|@Q`EQ&Q|No_*URhrvt!Ztaju=K>6nwN zJXY)TQlThiFlExzOIM|{0=f3MqtpedGAOA;T73ur{HnMns~VmC;?8|;RmfmN!=~4kRyb5ASzxlIt<=CFj|25Ke&>bPMX~V>* zRb*Zr`V41f5Q;~lcrVQlq|#$y-gs4kv;fUO^xn{*>yKFB>_cc`NUJ;%K=^K87sL@D zF|Rqj3vYBb0+f^*v5{VW;YR?33ap|?FzCOu$lN+GB0Gh-ebjS(8xWCM3hR185~bC%A&13mPUFO@T4;h3ksuJJ2j>Pt8h~i;i8$u7{_*3-`6{d4aQ@r+ zm2QTF5L8?kqhp?DYj#vup?OJGfyYQH?3_a$?bYGyddH;w9!1U>N=w-J7JRj<>#6&yf)&?3@bld<@hq%mcylZai(*7_ALTZz$>RDOIdN0= zsV`4LzBmk}rWcyzTiD)|X$)xp8KtBvTB)y(W)Uf#Zc@nmPAS6oX6laNMGoPV2lHo% zr>Qpq5_>^d0bsEYv4Gi!OtT`$#C$10*ma4{Cl`w$={wB3$uBnk`S}jEnvI<+;wWt> zGTx42>0_}bOLpGnAL&^(MlrEw#|L?`lSYnYkwudDT>b=(P<7c zC$5zbFZs92k+>U`l{X0t4kHb&P45`Tm>%5=Kn){Ni|htMTE!X>c^WQgJ^_nP%|+Rb z-~11|90b8r!XaYzp%wW|P81>nAe1JM)XZ)qOiH)37mjbm_l)ZgzhRAI4>l~42d%|4 z`YO+Fzbj7;*ROrbx)OcSsPsd)C`u!-TfF6dD-xqQq6b(cF3DfQ?#$YYG`cVI{C&FA ziN&0tksH-TSJZ4L$OP5^&j-{+4m$O{Fz4KDRwCNBjx>}CS1|L3bp~3g_aMGO*M+=> zEcj7CXe@mEoxcQtidy}{?!WN&zDMrp_4`{=zfl%IT(frm8T31s-+!p|(rm~JYlike zAwQ@?CenUO89$N(Q>Xsc8c}U*F z9OJjgz>_A5nq7H3_+QAm?MxXzA|k{NbhK~)xe4n&Yu`oZU!gL4^Y}LLoAJ*4Jysa` zT2v)!)=^h~Z7v})7>JgU9;FBG4@L~cYEzFyM5buiT0Bkif?qp9+yq0JPD(fcL=@HlLX8w{ zHev{et_*{UL_WX=aQEqB7>LpoB>SmDYT$XHr(z?3s-LYB=TcdJMuH9$6V8F{pL^sYInC#r`JbiGJSPF28jMG362 z@qameixB}gohfSIDj21)57rePCT1?Vlt!L%E?6%FF;KJgeTA--XSaYEAL767w1v1N z{lUEm!r}K@?~k(m(_wl)jbGz z3kvf_s^X(uG;;ud=bK5kMP9II<<>%*mbmyh;X33q+4l|MAWs#F$a${a)mY2a_9Xff ze5EZ1FbQArE$oWN2=NWFxT1735y|BR)98pp5xFZ(zKof;!roy( zMR)5TgoEuacd&hU44YfTFV^h>Xn!hX4(s;-y`eoL$#vw7P|t z&FA4gXmlDKcG$ue$$Qul!jZ=}^n#y3gQ~K!i-dJG&&vN6jsPbST$s(qsHRj-kV2vePNjb>bM-Ynr!&C6=rXN9i zdvcuQM(2*Avm}-3mqM3{Z5E)m0?J@r45qawY)HG9-zUMkM6Y%o7bWhJHZt5cPs0515XQCnEZMz|H_%J2 z>075vJe;TLN!?k{v({R&#BZg$$j+)VBARgBz@hT!ZzNfBQu5ADv6f9dyBD9uI(X0zSz)Lb#u!83#}Z;vj^$8umF|KFosKk?Ah-5QlmkJ%!%9t?_K|Lh zq9GC&H_`LLfGb)@K;2!PkB1|wRJhNIp~BeRrG6kG+1XQsHmaAvim@%vT zlukk+oE%MbS(LpQd9k^`omb~`LyEK^<)YfkSa zdP;s9ciw#<%k1o9EWwp5Y1W*62>tPJ@zwLcl$gIh1&=7ovXQr?S>ML2g>Pi$;1}^T zd)XgF3xy}z<{GdP^}oJ`L0CD%c2VWWI=p1EOsY9~(EbsYkLS!i&(NcpVPUDYtC3$O z^(>MEWvN~|XJ9HPSho=#Ihkc?E&zGDPfg^%`+BzaP8%OWxah8>L_b!V%}>{>-I=_& z+<~{;0aC2e3X)<(jDVACjhuvakFz8bj&^43$OsxJ)BE7D5=bELY~=3M_71fYtXd|P zNX+~f3Z*(xNUG#Lp$i>~UhN%IRrMwA$N4*3*|#a%cMT%NyH*e2jV8(Yl=8gEM2wFl zkRD%sK6xc#1BHoPkpO$iW896%NF%ux#voG5`M4tfZKU+NiYN|K3D_^7<76D@IZbZ# zD(mm650QkxWuOVV}Uv)Os| zqjcLP*~@kbyxQ^C+?DR@&po+VwR84vvDYh{jh$NV{kD7KlHP|6=X~CLHeJ%B+EDEi zulDHOT`M!Dv>x;1vZsUB-$^&wdBj}*F_33nJ>prdkl#J>O%ZU3`dS5r#=`&{I~-qY z`L-9?+3R5oId?G8Goz-objQ}B;UgwC^Qcn#%SVRPyNd2^OM<^dO^OQ};_$jYQA#Hg zUV3$v9+a_bDWuWSL zs++zVZ4!adG|zr{i|enEPlh^QRepSa)j+950tW|0!-0L4z)CEsSSnHfNlV5w;d5_q zCBRR~_l&G+c@ZNcqwVNKID|7?mua5$R8mqxM*$5kE-rngIZ~G$w^4sp8!qCs_Y40b za~ECK(TGTaDBsXhNnftv6HdH%H+Ty2$)B3}Yt17)E4ZqTdU<&@mH7BTSiV8Y7F|ms zRD8U1Y?#{8O`odedy@s_>`SMM4o3Xv)^CeET+C~tqoBNxj*hO1QjjewD(dO&Jz!#D z(peJ|)7RIBFRfNM`{f0W>1q7)Qr~WhnkPlyv-9ZZYu9NjSFRj|nQ3LeW^7UeCw0gI z9mMHpa0IQOqU*O6LC&B*A3ah?j5@0?ub_|x{e-r^t3O2X?_dmluU`~7LG2&xpVURK zKF99A?%+sy#+zil6rrAbNTY3bX>qeiDu?@?mi2gc@cMW)hkiTSG1Q4Pl{GTTk>{jL%^g&$q(?*M-S#2_kY)br41=e&jde?%XOy@Z+J}9*{^SpgJ5a2HsGSTdGqJzxC5_+ORnMa6|liO&-M8u$$^H%h_>s_b+WhosQLtFZ#>5P};rPIc zi}O}wo@#h-3zhQ3Srhk*le4!HOy`YkX(tl(5l#V9Q&XqkJ>1kfTJ3VCJwR5&{R_^a zzJa!bCz~=E0xMQjk`miOoJ+mYa~^fOh+6``%&1RL&xh!B2q%1Z<}hucXklief!h-0 zG+ha|3(j=zeFI4#9Ii{9K$xgrFeX5(GHgpFDZ!%$ zdEuz{Oz+9DiJ#>v)o}JXw(s4Czf-;u>hNnCr!bymGNaKmMGyy3qaJbe>8hzetE;QY zJOh>mhHV@R&c4QIq>S=A@dgAgs%tiNFx zoNe8RC7{&melV-43TLb*CF^BvIB;Vt&W>0SOk0IsX{m*S6`qJd>LV9WmMX=lTa@bo zCOK(uFP>I1<(``N(Qjh3#-|!BPHzDx7LIn_6c7}wYHKq^t2gnEZ7O(pQNuiOI~?{1 zTqAfQ zI{7#@TjOV8n%x2Y{4>JNs2^4v0WHlW`R~>T9LfncOc1@YdnYaOa%Va!=yt~=W$lcsRi-Dv zlL<~X^?Q#zD*X0j?&wQR@8Dxe+6+_E)fnlH5OXf-T@V1WuhtU-yP{B3-FIb_3st7% zwzxYjX}zzTWv*_U-x21CLv8f(?2mg-4aIap)=(=O*i`7|h>bcC5VJ-9p|DG{!#?ln z@m7oHeoM)`qnE_*TPuz5A71j2s z;|+~!3;Cbka^+!W=~xgySLvsl?(8e*O;!By*6hbw4vGziH%Fxi z!Q)YFN7wwnLCklPvK&us!MPEhYUO%0?r*Z9zb9KL_5S;R`le)|F-2VY;t(58j diff --git a/tests/testdata/control_images/atlas/expected_atlas_filtering1/expected_atlas_filtering1_mask.png b/tests/testdata/control_images/atlas/expected_atlas_filtering1/expected_atlas_filtering1_mask.png index f83b78a02ab8ad2d8647aa8eb7f3677380aa0e30..7724225763f611b9429e20e0d1b395836d15da31 100644 GIT binary patch delta 1251 zcmV<91RVRSmjbJo0Cnux(|}k7e^pt(etjk;CNelUm?Rk+8_U?(Sk|sxd&zrm zx#gC;_S$P18ym~;@Niyz_0$SCu_`_T;0FKFYdv>(Xd6(rUHR*VmV`XV3oMfBW8e zOXa2OrG%_;?N-I&{$;8#ivu zz`#Jh`s%CPb=O@>dvgH5vbX9=9{>R0avK;JNKa2slBCsY<=nY*x&QwAbK{LSrnk2@ zpM3I3cJADnLe~Ea@E0W;5gCZ0k9ofEpdy?ezXX(?q;f5RX+;h+6 z=9_QM{QP|S`};FAG?aq}59Z^KKW=MgqtRH>8UFmUq@6o==7%4CXnV(JpMAEZJpjP+ zC&T~%D^{b?$mr;3-g)PpoI7_e6B85p`s=UL)zy`$si`E%FTebflMRVEKl|>x^Zxtq zXJ%$54?p~HZoT!^JoC&mOPbl=-~YFdm-6(}PiKC9KHIl%&nvIIvZNgVz{=B^WcQUm z1OUM0lZA;Of3RUg78e&YFffqr?(WRY%;dJ)Zp*^LLe88y^SA5!`uehe|Ne}Rk7wh? zjd}6K7yoh1f9vh-O?P*9Rld+)tDb?Q{QySuYt!-mYx&Sr6O@n3xp0Dvpc z!+HUL6|dQB=H$tfnVg)=Pe1*X9Xod9o_p@e+}vDS5cdxc59h*#3)!=0PhNlh^^-J; zBoy6u-(7OY%*;&Y=jSsrGLq+?e?F6JiWq;c^dSHMF1N0(uKe-GA8EB(>Fn%GS65dS z78Y{hLi;}oty;Az*IjqrMPF;RTKWC=-~al0cz8JPz4u_>Vk>_ZSS2_Ry N002ovPDHLkV1koqyL12m delta 1250 zcmV<81ReXUmjbGn0Cn`((|}k7e@)r2VM8V+CNelUm?Rk+8_U?(Sk|puciDSy zz4g|-@x~h&8ym~;@Niy#{q+nF54XMhrI%jH$*OWbb_T-aKKFRv^>(gj7(rUHR*VmV`XV3oMfBW8g z>#aQT#1k1A8ObxxJd@GU(QMncE#H3oZQHw>&1Q}rJC?O;*XEH&9=YU>ufF;!LqkJ( z^2sN&b?eqV^w2}uuwlcJ-zNaDJP9!Xz%}LQ(WANh?z?mJ=+QjzzympX@?<{x=%dTt zw`Xj-hab-P_;?N-I&{e$n>KCA zz`#Jh{`%|ObI(0XdvgH5vbXwb9{>R0N*fp$NKa2slBCsY<=nY*dGNspbJI;XrMI^? zpMCaOcJADn_urSa0{~e5gctx|MQbz~85|tUJMX-c^XJd!mtTI#yYIf6RjXEIVqzj4 z9UZyhh8wbO-MT#f_~Uv0`RCi3yLRo`C7q+$Y-W6XJe{4L`R=>#{>@tf09F7Y1^`&m z{(L4&Pft&F@7|s1>FG>Pe@H(~&Q6>Z0k9ofEpdy?ezXX(?q@x~kT!V53t zmRoMg{QP|S`};FAG?aq}59ZTPKW%GfqtRH>8UFmUq@6o==Eom@YlfB*eCb?Q{QySuY-TO>gvkE z!a^=yZ2w20)vH(M`s=U11%y0039e%{Skir=EH$d-v|m>C>mL_Hh7!t4xRi0000e;u(|S zf^9miUbU*d7IWdkg|@CU&ph+YGtWHp%rnnC^UO2PJoC&m@6z-B1ADQNW7p;9GXMYp M07*qoM6N<$g4;5_X#fBK diff --git a/tests/testdata/control_images/atlas/expected_atlas_fixedscale1/expected_atlas_fixedscale1_mask.png b/tests/testdata/control_images/atlas/expected_atlas_fixedscale1/expected_atlas_fixedscale1_mask.png index 384a04c9219209f2c959a92c673fca6d3a8ef1fc..c9617a146e61cc53b51994e527331945a5452075 100644 GIT binary patch delta 1626 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 A5C8xG diff --git a/tests/testdata/control_images/atlas/expected_atlas_fixedscale2/expected_atlas_fixedscale2_mask.png b/tests/testdata/control_images/atlas/expected_atlas_fixedscale2/expected_atlas_fixedscale2_mask.png index 81476e821e75ff6d3fc21ccdb2575d76fd625d99..6ba6c62bc9b054749807f83e6fe7573ca71fc956 100644 GIT binary patch delta 15867 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 BCG7wJ delta 15857 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 diff --git a/tests/testdata/control_images/atlas/expected_atlas_hiding1/expected_atlas_hiding1_mask.png b/tests/testdata/control_images/atlas/expected_atlas_hiding1/expected_atlas_hiding1_mask.png index 385c7ecfe0bbb38ae89e051f317de43ca1c31ab1..d49f55a2246e583d68ab0417a3df6e3cf883e955 100644 GIT binary patch delta 1625 zcmZ9Me^8Ql9LJw>nO7~_dDEepZdqMZvx$O;$ja21rrp`8s8g32IW}P9W6B!ZV~?L{;LgGB(B6zln=#X{M6JGft9PCOrPq>t@$qP zIi^KqTx^7l>J}2bLTo)7np|j@Yl?3@`;0+;^Z4stnno%oi@A97`b@2QaBvWkVl}h> zIHin4_=xUkwOT5b8kvs=-akVwiwgjt=m#7QXPB=m_syJa@^db2PiXdY21_?|axzGt znru!Kf);ZVQFxFxO(*3ozpX6Z>7S(&@Fikt*oqE*KGxkBn5wA10=0E`XtwOQ&a~D3C*!(Zg^g=rXMRLjY|9IP#bBK z*Obultx2~$++ScaQ>ADHlcLneHu^UpNBDSr2pAGV?90C#yty_mPU?l#U(@SVW|L-g ztS+3g^8TsEkctlmH{iAu?JVB|SDSDy4eROcg_In!a-t!|U{LX)$wYGc;zYxOMl*Wn za7>P2?k?&1Z3KP(!OhhHhv>))7a0uGL+Mx+YFsYobar+!Kfa0sdoKQc<8w?NxD*ec z$YZV!1z9#ASvQ4LN5jtT9{>QS4iQ6NFB0J>DyigosSDE2@6J>&jWpRr z?4D&sw52}1+NXW)LST`2bY&m6PL@?K6e2|;5dwk0sPDm=mb##om2t_0xVYCfEs?GP zm)s+&yoRpWY?sPG15o7r;ggP5eFFlj^$HMQ;^_VI5TD7SL&|h2)z7VP{U_V)+mZ0w z*49?24HU{oXo`Zn0)YUjuflM=6FX^l`m@S!v z#@ObU_V|rUlm0HKtdn(PCm9SzXO@hm04+E|8(GnxlqXBiAd^Q+(bLuZLgV4^@_pP1 zGw@e&2%N$r38i8CGhYiNo!Ro7jdiWn+Y+Dt7i$aS)_?JUodQ!>9-zvJk3CHWUXo8)=%2hD=c%y&=SYFfnOamGBYDs)ZE;;*&(*<$>p>telYi=`gQbT zg$ynl`l%>G|3UJ$Ba`hR?;?+8cTH94rx`wqLR0n!IGRZmi9dZqJ9x-nC=^0`u#h)w z{HUfZy=nDI?DQJN>EH1M4CBHdrG-U^$emDWVE$ls*bSWjTLh|Pg8uRWB;`6Z@I;i{ee)?Xt3{dE=L!vFDh~Ah`3$ z`sfrmvTQgsBU~&R?sl^Fj2N)$$d5*!*CO59Oad#mvCjR1%r#L9@yUD*S5G|8GcSzr*q7CAF2$tH) zC&JZLl2(q(J2g4#4SfShaw1LEq}jFN2Kpb@YFWotVvbz@D=Jd_8&+ z&OYXpjU9LNia+|Gm|viiU7xJ6!u9`wrlVT`wUeVwlh-$ItXm18Q6~h)Q?LF9CC+E; delta 1625 zcmZ8hdsNbC8vd2tW}V61)65vUYK@pPW2vK#j*4(bCtI>)cG|IHUa&wY#ppzsi26&n z)!hgganlTead33q#7aUk1#GOmaLaMbynt?AP%%kRP!!mo{W<@9=R5EBz3+RT=Y5`6 zj+5fC3yTnu{KB`NRlqKU!2J$3zqmm-fVhRjWqkPAZ?S!` zCBE-n{3Hvuf6rf^y!t7P(OOaxo@2sPgvBTR;Z`KFb|Nq zX&EDfH0?`a3T3jZxH(yL=&F`_I@hS<{=?OMwq4KTyw#})p)M{i?MsghmnRCX+^h16 zii(w$mHMHqnNrV-y*eq~JX4KL%tHYg3(No74?v(YG7d;|`PsX6?YeB$Ue&+-U-yVO zdzf}FMVe1dr@s*&PJI+Cci6yeo6W|Cqa15f*DX0=9?ADJzD{aMt&z!Oi?1F`VTQvw zsxXdol8__ArbZ=9lu+4?uD z;q#?*D`Dur-;c;a&|;i$Hd18kvmB9Jaz=>eo_$iX6XpSW9LHcV$c;sjJhg*rc)4P= z>Wg=K_ddY&T%LPT2k+DKL1a_Rwrfqt{<#eZ%wop-CR82nOCd{dg%Z`%(5rfkEa$eh zwLzK0(T5k>H%*FAXnKgvx7{>TpTB>YL?FO@r#~OgnKu{`=ybZwws04fHas@Q+2@}K z)(@PYZffN9y9Raskk*l#x%~QBj%Jg$jvJ!)` z{~Om+eliP$RQShpz%@paV4!zYoGr>3YpX@jLhN??`fEl|2=4h!2+kpN(fUj^2uBl% z`s=YLh(sbA4>@sPR2+1@qax%kjn_cb>o=fgidDwPNBWyiHU6s8@!<1IOX~T-M$y^s z(wj_Xrrow$nU4<#g2j8j0J@H5Psj?BJ92Z__?~jFcDrTF?AS2sA(5!;)(L)gOZy=V zV)@zQ8lx!=Q{_f8ie^o_{H(t+4rR3gAFI6H*5_jIjs7h=-P4BsLqjSt6X;Poz~Hfs$ne1EiNubtL7FK`XNWR z(#*5tG(ETG%j~2rbM5vi2!{UH<;_NLaZxE?A@4XGj#^})Ey#z-h5P#Yj!4MZ(z3Ep z1fmvNEaO)OPr|&sR1g6nP{j9G`H2N3(DmCV?}P)Xp}WkDsNuy#=C7Qu3c~WDmmc}{ zuzdJ0Rs)ind)LOgz7Asw`@!qIn!`qkWM z3gkQIoO36zX{@QjZ^{ez0M}e|p5KJQJUtWc4jqRqL{Q$Y7#JA1$--<3U#!dzQlPEJ zGn0i$hQ$wZE1ir8qtu$Nu8pjm>S5`Zdwfuy1P50n5sTCAH%4s(sMq&HCFlAB?kPXS zv0|#+u>}LeHvj;#8vy)xgI;+MmJ<~s73Q2fLLiYyu?A%*G?U*S(bi57z@t6_cf^T* Qh7tkv(Zp|BVlztq4_Fp-N&o-= diff --git a/tests/testdata/control_images/atlas/expected_atlas_hiding2/expected_atlas_hiding2_mask.png b/tests/testdata/control_images/atlas/expected_atlas_hiding2/expected_atlas_hiding2_mask.png index a4cfcb66968508454a3cf5554c02738263b505a4..42d3f3a90894c34566791af9dbdc9bab751bda02 100644 GIT binary patch literal 10941 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 NWN=15=eP5J{0kxtt2h7v literal 10939 zcmeHNc{r5&+kZNplR70i2wA46bh0Jc#jlek2}ek>m8p<@U&i3n`DGAF$!^}1Eg{*- zQVeB1l6`F^JHyy!G{e02qxYZp@Ao>(`@YR})#DnT=lOo``*Sbf`|}C^)lir7#{)ki z2*Rm%PU|9qd~b&!+YjvA0pGNZk5$8Em-{&jF9g}!fd1PSC%IP$K@K8%T4zlCQ|5+o z_q%;I7_{De4vQ=IEqC7jSLXdUnVx!eAHSECx*l~ZBK~@U-eTg@V_zqTZ4AAfASic z^x32+KzeJ895+3YZ*&TK<}(vbXJ04|}2f7t_`poKqW2fw_Q-b?-o}o%2^f#_pU#N4qmfD6+{V1xuQ%2P3m0mb zjlb{WAs2TUM(TmDZ^s`}mJ9A93l$YL#V?R98qfszyMZ;r*ox*%tFkc%G zlw%sjZ-Ta+?w*Bi<&0K(Hs`6?^ing~EhA&s|7X zlYj>ag@DD zpBk?6``FW?6E9;;&P;ZVSnkl&Ov6zt{AY5q$RvK-d$4R0SU%mhHdyXP^P{Oho}3NH zRCF0=V9+SyPk|6r9NEkunhh%M;qx*46=Sa@fX(u{U?H>r3N>KC?YbL*{@Ci5l||Mm@%^|J9Ac z>+fS7TwS9%g%lgIjh~WnqfDDQ*bz6k$Ko4~CY8w)p18QU&e{-mf*e){ece%mBwoK! zyx$A!D#c2_1<~1_yshn@ynb$FVN_EmMpT5Gdv$fS(4jNk|I2h1W!P^t3Vpl=bB@O( z^FnwHo~fg)O)_Lya)b^?nmj~8JAThf3p{>4<6^O6*HKm9Ie{!MM3M zS)mTAsrtJ6aa z^`i=RcX!L#H4w7%^P{(Osh==Zn=hW~$+KKn-~!_X(y1aG!tVbvH8WSo6aGM6gKQKrTTok*pdR4K&8^Me+=z^n zttrngD3Fa-oylGb9%W4^MW!E%pi!FmH<)x;v2?~8uHavJwT&;NsaH7k=3B#UWv;^! zjg5_Da-^(`PSgRjm6`5bb$VIt#(X+_2!wQOQ#7gHo@o8zO3CxrBhjVQ;VORXO7EXX z^Ru&$p=Kk0nJthN4V-!cTRL-R2VW$=tc;@Dh}DoslgRj_@~7W`KQ%!Y-tb z(w3qk?^?4yu~cHL=-jujNfhUdCYGD+DS~J3?r|(+&zFsgI`!s95eN$*Y$grTY6F2= zoLmsH;UtWkD;(cI{eBv^CS|+*M)H?0U*>Nzx`>;+@;6%v&CSgY!<9a!rlt)qEQ-kG zl;}tT+^eH?_UzfAfXP%Eh0&;d*&hTewAO<@Z*i@(6cch_Ej=^_~_*0@T7dsfMFV<~0wzd|5AcBM~ ztjYSsg;t314K%G^E_1bRO;(bP055RGJhCWvzveyl#};@?N=n8zIQ9tai_zTlUzzQd zeJo>rK=$?GFI#Kvv2@dhCz@Wm)F>OW-lDwz`Jo>}H}* z7KrgKk3P4Tgm#ZtIeEqjxx7E}PJN`S>)Sw;d!{R?QT`agdOgDl2`6uyx5CYrc+g1v zO%U|hwYmPzKmORaJlPQg;kERGqZ)t@&s>wngcli%*5F%+BfQ+5Sw=*Wnlu+W9ZES0 z3IYMGc^48-j$;?M*@-?9**V%X7m^x>inS`fsjJZWRV-DRdxzo>EdO>kDal zEG_M74(W zIa296f5j%(ZL~%$#ba=TL&W>nb+NMffs)8;^>;}Y_Njc5=J~9?UOKVVx&NmoOh}NF zZH@Xw2NA=K5j|pJVIc^KBl3v;V=b))Ku=YD6Pmjrk-V^}R?Rdb*3{z-RmLA(F1cm) z!nRgpqBYUPW*IV0;1HfZjBfq$ou807y`Wn^AupR$r=LA+uEi+3Urq7;py%;HPlDuE z%N(BS$}&Q;0`yvxCt;$u3Kw+S(oU<%R8FNs@!{BNO9lo8rXV@r6Fsd;t(?uz&do(P zpsm#q56uqRlL-5KG?sJjd#Swg$JwVfH5!2{Q``{39xI)?O`pOrq7RQ6L`Q$PeMVN(Nnykh8!8SZ}UAmC| z{mOfwKw&%6wLPd6lZ|!1ZO4w){U!%*OE``5joi25vCv-{GeZTRSpf*pE=3oL02Y== zD$B%%fsr zjZs4UnqN9&>^{ByccJ^IxBhGW_D=!j9;0|V3F03M$QZ|qi)lbJu(U8cGeaKo>WHe} zt0pB=%UZfz>Fp>S!lHS+-6hdLYrfYqCA{X$X50ZLjxt6oLKF?uPYQLNlBc$*&7WLe1T-zTAp-7*f`i z#iSIB540}Dgk4s)}01eSKq-U zDjbFh_D8$Ey=rqW35>;aVn|_&q=`>BO~PA_v>r+R(j|6$hS+-w8&$m zE(fUPfu49UuM2uNWokLaj)-3i$FKEEj6hvtz*#~-wuWIBKz#oEx$;B;HXTQ&8bfG6 z@`ymwDt|45Byc*=+tkdA1TDxuzmWiY)U#L0Tt&DvS?-aOg6jV65bs*dwRs9O=J%@U zBWeHza;&~gIDt!c42OCLz7(46Es)iu-aorH9LhXN#BZq6t~z+*3khux^RARcK>O5W z=~m4bqrUXcmw(5m5n866-n^7=C0NVeSirIvG}=m+QM^&=^Sr#M^kd{sUCc0*22HZZ zTwzn=HV)x^(BZ{ET_wG`(SrU~+IF!F9MKppA|#^fO@J+;8RRw8g{|1RCn(<_fUGIQ zfwIo^>YLj?K+Aqld1IxE@d@J{4c+FpBJ^QD+Z2yRK{?l2lImVH+VNS)yroW(+i-}Tn4m7sy? zO&wA}QJHb^XVGBBr#?8zW32^gy2W4e5afa$Gy1>Nzy-xIZEv}{t`UzQbB(Nh?CD9Iw1)oq zjM$LnMM28P!(g+&bvB^qXwC&|806LF=HugoJ@~##Kjyo-6RF~a-XL^X$Cx+RVYGgy z6C#Z^r2h{*{Lh%n{5k0TwzvP$7%mnfA3~rsCp+7LCP{SFHr6ucg_sE)j(|f(oh#Nd zlvb~X;Erb45$;p0jT0w}}Fo?H4{XJ6Ge$m*NTl>?X5OS4%_$#yO z5%rJeV84adpAuT*EtlgE$eRDhe&dlMwJKcKP`;rc~-D)|*R^&YY z?2eg!%Qd?UOUtv?meXr$=XZLwV}c4EYlzxcEp)8bXwah@z34vHa1iDFDl|Dp*pi+6 zx^F9w|Hr|vXW=tnd*Rz(q4>jB70QoaH7WnQZyXbAOHz6hcTF|lxpR2@ zMsfS_z(5j}N_`E)iwgeB$`8FphK3U`CT`J}t$q}58?5s1;lnNse&mGwy3$_6-2%?0 z`k-Sd0!BO2^!SaSAa$6ZN*JZ3ER|E5VzP2`C8edMEns|an5LcxEZt||@!Fr94O@bl z^k*Ofh@LtXR)$7KMkomeEW&A%{NWlr?rvCES6^QOjYb;^T5jvF@>}?!V;j=y3?6m2 zn4X?~?@~7WSlY7mGq5+!z&|c`8EINCmcuAn9R|hyWp1{ctmTfw?}24We(>M{%Gg#{ zSDWa>Ud(v}v_NKQsbYr)(^*wjwY{p;N+!!nW->}?g<#NlL@y1ql? zqX1D>OF$HQQKFQN>NPes$*Za+0aGZsn_s3iT|4rX-@*uxby>h~l4Wp_E1#IJ z-iF9OM7R2(X7kG|tlS=$&Vc!n-dDiGc>DQffx$p1a|3Mtc#;^RAucWs>Ii$8TWcV& z@dRbhM8#y9zL8PKBd3;e&;7DCDQjzMd$_pRlq9e?DrRwUadC8TI0s~A>}Sy5Y`s}j z-QSqG9l0rLWbhp#6Y%uuQ$bfAlDJpS&1IQo1Xjv3GksupQX2+ISgi+E6VeNJh4GL+4aY%+ty zQS0UbWs_@OU>GZ`<}0;(ztp>jMjDAu1=WfYhAHpJq{yOM-OuL+N+dyIlGxJr~({KLbZA zc;GpGBJf+8XYfB}@{1ghpb@W*E*6-!Hb z6_rGkR|oCDTW<{wENlY-~8Yq3rP(4Ql_`JxmE7MVA z6c}Q2Tf72r>4F$c2dGBmPn>XnLjf|>6BI>RK-<99O>Ewt0)3B_->?r%3YD1yzz@Yi zdYOQo=)-5njvbz$GjN<2L=pnFY%Uc9!Y2!$Y)f5-lR$QrS{=UNT?|AL(C@7vcSr#F zmii`xQnB2w#xBh)D^r4S4Z-CB&j+9=7svfxIm^Mq74aF*r z2Fb%>CtEp?P284!&1K@f022UO`>R(Ef}IRpT;?B*8m_n4%8f$?dmnp5zdaxXNM=VT zrxwsk1h?Y~YBvL8uS4>JM3d&rfD~#igFdkXkO9m^=g>(+yA0z>pu! zOii!1r>IQgL!6zZfbiE~dYPD*pp0mWiq}zykN$EGO8<#vc0caMjdAVM-Mf&4L|9-b z`w0jxRAV&0IDjo?Mr4rCuSM%oa|jLp%SE(r~=mCdQkl&U|Gmm@5pDc6=k_RFG^pmXEVP5u^zj5axI7z43_|PAn2mWgPyJX;@TvHhpLlBbeW&Qsq+t2t8Yt$WV2i=rLe-#ml;XErmi^3Q66HKeNWgAG;?bhEdw zk5zQkxD(CVcUl-8T!TXk!@E;jKfx5m7)9G(oR^oEEaz=cS7gM`>4qz0+zV=Xd($dl%;YHhZ<2jY&q6i3vL+&GR3t=k~I? z!i1!_!^FW7v)Mc@uT$SkaxD(rT^wj*W8>!L#)#{>vmNCh5D?F|gATw8CvBmp?|%Sw z@cAAnELTQaoSz!ZG7N<^B!}UuLvL9xUme@?==5esN9p|hJVH&=OFZN0wX#IAS6gSX zt2@Y8SXf9i!Wxb`qA98G37EhjZ25$sjEAn?>UGi5{QlPPf~qIHSrlRHgZp!oW&KLqo%i!4OX(O;;y% zj*v2`RDGvw?Zf8gPvQ%YRy+8V@RRyf?_XxyX=%{+2M3*OpwLN>6?XISxn~%LkxB2W zMJ^RI#s*hhY+~YWNBaEboZMV@a9Ubgn$LqPv7+?hEY{rIoV(!;;x5}eU6sA&=o_#~ zavkYM3JPF4SLNRfO${89g(nibyV>4)ldjbT)!xxT zmdCLwg+hd<;>Q@eQN!8dO-T+mSws5Vwbsy{&=C^il7J|g7-}K|Mu4Hxre53O@p#eX zWGX`mB!ptz4y+8jUV^QADzUy7hC8jfVZd^nV4Bas(sQZWM zCc;FMt|go-@yJ-5>QEo5tEtgqko#i?8VIH3<=w{FG5sJ}%7)29Js2drrKKgDs^tSB z9nl--v;%ci!$h*;IRfyFTGP?tAjq=9WY9@#1d1{sS&+eZ!8lgye(2C4 z&SnCE5XOr|QO5teC{)P1#CAeehGJ>8195S3`i5@PPav$A;XUtfb zL~@IXiP^k$t7LRE0T>6dQS(SF7B4lw6ujjWph~+6Mbkf%vKCvV1ajWIWsCTa2LW}L z+j0i!CT%HTw`>+zQ&r!XW^AmhaH}^D$Q8iG0qSJ4+47bwDuBqq8jA!?I-_cEU5aKZ zdGzgl3#_p$gBC6LQ&>yW8Y?LE8Na?>Iiu;Rk|i!-k4B@fUcHK~m6~hb{_2-c(p0j_ zXH)nWwc=G2CfsosP||ZrkG;Ji4%G2y?w3`q2x?5sM?dQ0b3_8y6Q(C z6sfM;MNaa!LEqm1h6wGvxr8RfT$x4mN{~H1Uw3gH7@j1N4HOl>B&+{z@fC*s`S&ah U>y^QA;1z`O3qJYec=WY@0k@i4tpET3 delta 1542 zcmZ8hYfzGD6#iUys#z;`rtRM9R+?s+ty)GvwX3d!H_BMoNX6Bpl!WpcqJGv+)5Xz3 zydwRPD!f16(@?9|jHS*rb2+~h;?5QMbZ=QsYej-pBM8jJg2;#%^inL{Vbk=q- z2P`Z)@>?~IQ_bglqCbv}mCeu3AB}vizaxvvS{k5C=6SDQa=Bc=V)v2HDiuSgK%r2( z_wDONXa)4MYb%p?)as=SMV4Vqp#K<1F;16UM{B+6V} z=)5G#q;yXwB{bonm~Ga8ExM={s$o7TDCmlMkkhK>wk1uxx!rJye@KEdjIs<|tn2PQ zd%9C)ktOE&HtijAa7ajXW#z#poSXOP4WF&{$S3aYt4U(B4Pf^O^9}&4EyII7uUx)VO)~o`^U%Gprdi2(?R)?+!`TP4b=1F z`>XW^!&R+9sEoK=iw&*CdK^Fgm`0;Dgugg|_Beg|bd3yuQa3zy0g6pW9Fs5vG7%!>}s+f|J zGQ|faXAoNXSX+7xjx%$6qy77Z$6X5VFYr<$?HRp9pM@JYus1=K15pd{jI9H`gQuv&CLz?_A|D& zpsjI=j+yb|M;w!=H(QOkk28HtT?gzeD>WE{!9XHqFqwR!jJvvK)}jj*78WAXhU1{d zi1WM^PuSU}!<~;D*s?X?&0qT=@#Tg=I%-ZFX}l^aXPvrvU?AZHK6Akjhr@Xwkt|2Z z8~6gps?hk5{nc1kCv+GWjDXj876w#dU5n;MTHI}IZH*9+q%B(`Z-P@6uP_*l0!NKT z15Z_9*XMvhFf}7B5;biYLOigJI5;#E4j~ph4!5O~N$r+pfpyJnTpl4OTP+49gUIdL zhC%+wD@{yH1WlCvBQ0c!UZ1kw8AxGkAWL0NnJLvTEloqtfP~ahDodXmqM@kES9*%w zTA?8)&kenxlYST1BmYq^iwS6)?SXNO{LFv0jB=$7M^)x0c z#iJBu*`?;|mrf?i3pYAs9*1%)_`#BZDx_()vNo(9q9dg^Kdqi>GUhkQF#Qb}y zRLbINRjL4C6b6sSr_GO+axS-23xz1iouB9CdZFaTY>TNq!eCPKoYBzsS=Nk{blihk zgE3ZjV8V*=DKL#^Z31#~IaO6od_KQ+AR#^>As{d?kU$`W@+20{hXSX-q|a3)-Cp#4I8xj#qW`41WGPPG64 diff --git a/tests/testdata/control_images/atlas/expected_atlas_predefinedscales2/expected_atlas_predefinedscales2_mask.png b/tests/testdata/control_images/atlas/expected_atlas_predefinedscales2/expected_atlas_predefinedscales2_mask.png index 5b96ecc9635227c4c961f4b79257cfe24beec9d3..3280671a537c1f712ae9cfd672dd7fa33eb8b043 100644 GIT binary patch literal 31338 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 literal 31337 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 B(;ol; diff --git a/tests/testdata/control_images/atlas/expected_atlas_sorting1/expected_atlas_sorting1_mask.png b/tests/testdata/control_images/atlas/expected_atlas_sorting1/expected_atlas_sorting1_mask.png index fd82e50a42510952c4575bad69c70ff84a822085..a5699e839990080516895942ab7e08f2ff2296c9 100644 GIT binary patch delta 1598 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 diff --git a/tests/testdata/control_images/atlas/expected_atlas_sorting2/expected_atlas_sorting2_mask.png b/tests/testdata/control_images/atlas/expected_atlas_sorting2/expected_atlas_sorting2_mask.png index 412dd671f4a521c650e4211c261187e6f9240f23..c986bfd62d0ad65f8247c55d2b1f5e81c1d8a4a5 100644 GIT binary patch delta 10265 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 Nq^eDPb>8C6{{a*0$YKBh diff --git a/tests/testdata/control_images/atlas/expected_atlas_two_maps1/expected_atlas_two_maps1_mask.png b/tests/testdata/control_images/atlas/expected_atlas_two_maps1/expected_atlas_two_maps1_mask.png index b643e8396b9208c741f0846b1a46371a3f924150..56f51f7408e17f54c824bd62ba024dbd97d93cf5 100644 GIT binary patch delta 2048 zcmchXX;4#F6vrQfTU)m&R%BFA3zgBbNJL3Ypk)Lb7MU`N27#o?8kT6nmJsvQ)~#C5 zh$0A~RzYbJFoZ1-f)oWo2qK0M2|+86uqJ^7449;se(bbP#}D2Q_q@4h?!D*z&;NJj zW($Sc;z2mUB7q-4_|+OtejfUrpZDP%+h1-%7x8Vr{&~&lA6L&V+V{Ee;{B(D)2qKo z2v3&!aL=4cs%`c1dA?S#W?P@z1>U9JqaAHqIPK(L+_i^m2Oeh?t~SKXYp1&zW7-;- z$2@(Yuyb-{kl4VmaNK*r0)n7_5AgevA0Y?7r?!u9{RQ~HrTL%No4&u{f@g_DqKe8& z2P_JOdfwG__3pUJWL00aNrl7{ms0Ruq>_bXfOksgmAtAb3T3QMwYsbjy`q_x% z4O*?XqN=Jmggta_J)HF9wB^#Np&Yz8-3ua2YA>#XUfD$v2%D*A%Q zI=z0#G_UUD9cv-0p53-@RTjdYY>dSz!9HNJ;Yzr;{&@g-pnKF8SVKOL%`X2ZQ zuIBg;b8~a$gvK*r48sskn0~^Rrn-&RKC(jO9l_(h*pvfoiB6}xTUzRj2mg+grlh7u zlgVz^{TzZk4UsI^WEk(Ob^#@H^!E00W)?xiwwsqgH>JM5zQK5WVRLhHa!N{6Y^)<_ zI<7W3&RHFeZhn3Mg+l4!9?4NC6!dr67DOJd zqy&g+9KA(|XZrYw)2%IredJajmV_wsuP-*Sqfp z#q?Dr@@bK~AlS8f*@BGjZf;EOk?OCoGvl&reMxmr3#f>UGkx;3fs=yay!ffNZ?Zax z(G<#bAtT~RRbrnT5}B(XlPU#D)*fZ;y+Q`6zrSBF^D>?foP;PN4TvyxX=?Fs$1Msl zu2R)?hmUfBT&rvLEr&E&4`8!NP8;LGHtW|v#mvgDfp-E0Fy-fU$>sSD{x>39GB*m| zWbP@CY-J|MYj&(cYrZK3$Te_CL}J^QS!gEEB_B>sx(9G05~k^5cO~>*=_55@fM*c3 zqWXGFcX#*5V}h$D9b-2+kbBmWW|F zVYeYoNS*CZql~^N2?z)n^Xnw;;K7kw;0Is=EDx7LV&#CTEU!S$6 zbC*HDEMWMZ$XW-&Des>?B#=I2<63TLCDHdYulaGl4Hwago^Jo=q#mC(EygxGC%CB} Q1uRsF_1V9-)SGbTFMnmdr2qf` delta 2046 zcmcgseN<9s7{BGJZDk*4o1Rmqt4Jy{XI6xtt&chLBxmJB{KAvj6&m6sQUtxV&DC;! z2&SO|^Y}GqT8dCgVXkGNL8T&KD7H)_1C|)hfY{4_J3HIX&i?40d!F~6_j&L0d*0vg zd7csH9IbQCmE_bpWMN9`51xp&Ms&X9n=1$7+CI+R00+gtzkZ-_&BAxiqH^E+tgLJ9 zstxLniRPk&dGm&ka*)$C{@Lqe#pz4a@QIfQgl{eonp=FmdyP3So}hQX))d(7ZGtdw z%nDdwwpld8k7Zb0095#PAOxPjUyy%uz5yTdFS)$|_(0^Jsrhg4HzRsZf8f`r(+R9r zD=G?x;oy)E;UO8@lEQqc#Ou#sMm}|I1_w{|_+754u1-@hbLjN+)YK2n*DW!~>))T8 zvos5MJh{Xyttd;$TOZJA&pr&p{aS6P-T@sigkR|LA?P?eFE4NE#T_45)0*B8yP-iz z26onXRi|JKHz!Lrhllh16UAfA5>wA>hfSFG@>%GNmPQ+qBs3k)%KAu^oRe$<5Ec_A zFtAuG77vg`+N@iMJ2q={htxjRi#0ERHim{TbA#;Fixw{)A55gmM@LglQk%&uoVDF% zHHMh!6&R}G$mJWQ%0|3iMMU;dsZ{pZ9Ur8!UwUEh+Qzu1p6)X%`X4>22xD3+=@e1I zKJXHW#BWcY6tZTgP_nys?^aspLLQq0r{+ML@+b&}LL!s*vOzXp5e2S{aisx7{slk$ z_{|e;nl{pb?(Xi*+qNAW#OhcUxj}bCG<02Yg{=H((UK+gO-=rSe5(elG7X9R91aI# z^u&Vf=js-(;G^rJre^hm`$uq4>f>bxq2omfwfm!(Oy)@5WK(4@eM5xPP8%t(A;tlv zvbM7`qR7L)=V=*{zc#vb?D0t-1qT<7t#5aCceeybf;)hVKAW7hDDgxdJsFS3*VWbS zZ-|{5IDmUv=$Ud?nr~}8W*qwd+_^s?4oysCEMLC7Z(yLrWHNz#VIX>MsekX#vq;~GgMQWOC4_4QR;1L12Vl90>NSLB4mL>!X-Y_#iZg5(%w zv3F>0mtp}_^4L2Ny19p%o4bA2u3fXEUoUArEH7uhGSm~=v(tUiXv@P$pY)83b->Hz z^Q9l!@Qq5=%y15gLJ7;Ka)UJOG&mfw|8^^M`oS*7SaIZj{EYr8IdfnkEAk8 z{-pl8eX}kOuhlg+`hmItEV{r7oQ@fkOs$UJBzfMtWy_Ya?iEzz9xlRBl6L2;{0IV+ zTIy0Nu6Dl+L>4k!CjjIEk1_cFa#FR(Zy}9lXvT@_i zMNrnm>LzW;y64URPHaL!Ln*$@*%eg@6++L(e0093^eb)LvZR!}piByAMH-7=9>25t)XT{qoE1Pa-3OHg9GZ7Z=}CD8}n5GJB?{kH%DP2OGse zzC$VSe8|IN6_9n}^G2nsrQO`zut?r?#iVi>`p?-A*(t3E$C+9 zAW)0T<$|^zP^s!$S_1p~`?)-xEwSo$k*B^dIw0(cjw~87AS<5xI?rOvH)c{|c)=HB zGFd2?i1oMbAv9}8N5?{IEE@X33uHYyw*w4Ni#qMx)7zfJY0+wE2bdrbwC;A;LDvj_ z7c}|*SSbJFM?w-FbJYV>QBXa7oD1_Ae>FVDNGJ5O&VNFSosM&56}2a3ck|BFul@j^ ClZRLU diff --git a/tests/testdata/control_images/atlas/expected_atlas_two_maps2/expected_atlas_two_maps2_mask.png b/tests/testdata/control_images/atlas/expected_atlas_two_maps2/expected_atlas_two_maps2_mask.png index ce6754b7d657fa463dc9839fb680606cdb62b557..b2b910d2d1b9e3acf39918772151caf64e3832a5 100644 GIT binary patch literal 34752 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 KrR*?2^S=OYob2%c literal 34750 zcmdSB2U}EIvo*Rf2LvNIC;}oNu?doupk!2X&KZ#`8MgwG1q37~Ns_Z<6om~)Y@vmg zBqE^&L^256TJ8Iu^F8*IU|2p=01WP zF-4HWG$)S1PkLr1o8W&=KDw*pf*@oc(ElTe;~=9&kjsd?%q{iDDL=VFZPk9oEv#imX2Op1w(s^P_H86I{G>JZ~+yzFe!S2#oeVSJUv^WHeoxQz3v z)R8M!PntMCIZJV?;%)5NQ=k0Tfrx9VY&( zpNScs6aAz6Ooo*B52>&h&;MS{`rQ9*7yIIO9PY6!CCzWgMtu8wtk?JGJIwMW1Y1+n z+k1O^^-J}HwR&8W%Ea#YW>Q7TBEFXJ@`1|0VREsIm6erT&I<@q#HkL;5pc=D)>a|G zz&){7^1vtO{`K{>wYjV?uE8@xUo-swbohVv-^2Oatp4ax*1^$EY{~hqgc*JQ^5vDp zW?i3vMIwS2TV$dO>>4=M;6b>MzV$5hW0q<8tNq1^edW!sE>RK(+kJN3@i)8{3i2$) z@=8lpV=#Pt-^LPD@+|N*Z-y3578){m|17883~VzAynSV)rr2Jv@OCB-!G)HSaWN z%(L798$a&}CJzbV^s|pM`l!{8dlkyb$*pbo`5PW=`XA(-p%t9_-Oa6K zWRxzkn%FnD=;+_T;5Bz;@28a{_JXQdrfGTp%8FB}Q#*D2d~Whfg`CD$@2AR2nl^Oh z?{Iilj1)<1y(3@fklZh)>long6U)|nSSM@r=NqL)Db8)gB4HkzzF!3&>H2H(R@3g- z^04pbSRTCQquvh-ChI!4hkk7n-#u_Le->znDeM58PqYsMghB zjUBk8rJvcdhi{3q(202s&t~B0*{x@ulw0F~&&5A%P+s>k5c>M)Y4b*PDY_=Qm6j3h z9UXB!mTvpovv&9zhx*@L94zvp?mzq6$R&%=`|Fx+`Dqx-|}jNYSN!^s%F(&ozB# z;l9U7fkoG>xmm)%rANT)-D$&&p+YT=i>KychnmOtH);t@%f6+4f2J9|d7IaZ9X0Co zhm8)_jW`mO7E)Y#2U=SL{C>x#&v~@$rtWWbNGim@mMpa#_+1K*1K!)|fYnTy-)YI~e`H>X9P|M?`l7t_e;lDa=%|sm&QCM79Wn)4|)GI z@EDt^FCmP(;~kbJCo?!Y1xVZWKIciy{8;teJj}QiKoE}P=&a`K_Qf@f&n%5K;F|vY z?lzEL>X6vr-Go3{0wCeJ8aXSz?1}Yj2C(_63d^|^Av8Xx;9To~|IrM;a`O23WvY2j zmisruCZ+!hWeK5vsTyZUc!;m*iTmEXn!4<2fOB(fT#*xEh-!%W3eW9fZ~w`+&wG5@ z(COo)rJZ#`)7yiE()}*Dzqs$YSewlrN59Gjk7Ywg&&sjkBAw}t^>t54+MucCCAU%+ z$?ZaBWUN{x`X$L&BCg;K$9z5B4`3AnqR>%oy!k3~xsNU1`D^t#mAcw1#@@ zF4ht%)Ax#Sv@Efmb2;3;1rM9nHOKsR7ex;KtmcXDD;n*@kkN}U%kMO_?27LFPH6d@ zlVJG92r|NJUeRblyri~POIljmko;v3y{P-A=TpZex1Ky~SQJdmEimR`YvXb4p!b-w z;Py^ro*z`?9{Ts+=gq(4O6O)J59&4>t5+!KHmJk#~bg!>YJ|$<5Qz(EtPcZVU+uW+`6;YJj8HjRi zr?#@5?M>YoTlUK<^_k(Ip{LhiyBK4Ww1e~C(`wooDotDWJ%VIlqtTjbFeTKo9kD+x zx!<{>5^1 zxyLtZYUM?e#OkLbMO{%)Uusw$hT3V5A6~~9m710d+qIb?hzLDON6Ji7t#udBZEZQ& zTV9{_-=F1Yz+21Vc?1P>8-FBxIT72}Fk7Mh=CJa`KQ zZF6n|?}$tF7%u>%9P>ZeqoJi;r5W7V*svuG7aOvuv<(;QHH{=Q+dx6CY))(6)2p!` z`1Pxd*`_V%42tMK!gnX%J+0&-L#%P=mO0m@JZ#>!beL#17_xO6 zowlnUtxC@x$cJ3aMq{A`LdlA=#c2Z9vhQZ#y;!~Tp0YWk&v(gXwAwD=A>_LFTEDc& z>L>oZrYPVb7tu1^8Y-YQcss^$yGzSsLYm$i@qWFF6 zj5>x!gNJJE{FO6t3bLx=>o3uyv#8WD0K9;*q}$+L4D_QmC797q+tGg)@75>2IY;il z^^|#Dpwu(s*s)`c77kDQ( zzO(rL)^z>B{w@oBaSX5W1FICLAO_#Hd-C`F{SS)zS|( zLqkQ?j}n(spfvR}@p`WC7z#0It0}W=aSHQ)R{L;)l!p0GCm*2o8`j@#dnSSKEsT5E zb2c@P_Tk%G|kke7`EPw}utq;mJ77^;YU&P4(b4N_?WkH(vFMZ|W1_lTcJyWzkIm^>!IM z-sIgRGq}>01mylCO5X!6*+GcJytXT;lRNlDevpCEGjfTo43yVhhw7d@@h2BedkywW z%jGfZTnSAF6VBZ?h&KnwxxLeRTp}Y?)MH_W+rQ3mB~)t;{RdS4jh`b|=dBNYb8z%d zMk#}hv-5ovtce^%Xv{UXq<(>2d}l7(Ysn?OaD3d*0bipGJOMjg={O;QminE4??`0b zVepgzf{HFVN|%98EPkzX(M{<tJ`++8FoMqQrhbKNa{-P*BiXmsa!rQr|^8x0R2S zgq03PTyLuAdiue3`e>7P?f!Q9feuQkAaKO!MO^Rp`R}hn7&9M2jB|KhU3Zw#l_b;? zN|HBRs&$9b#J$(hh}Uf!g(}3pGbr`0s__X*7mobrU54D9JL?c9j&f#`(`ZT<4t)Z)vX0jB`i)V$^)RD9 z?`cKQxbJo|@IBa_NMG^6V%7%}n$`2tyxzogz6SPf7A2kwE4^#jHUU(vu(VmXKwTkI z?JnB9DCER&HE)@Y0aIcIq~;()kvc#SHk*8|2LBLCFgoxe2I}@~dTEs3TK3Klp_T%n z#;G&)Ka%V$9qLa5yr1szf|@tlisCZ*nuYoKd#b8YWOTxpac=9QxMoeE@yFRv8zfq! zk06M$lq@u$Qna2^r;YM~;;sXFLo5Of9`{}bR=*;OL6H6<|J$#kc!K`<|3&Bee`Qat zea{}hA$o+91W%5X4LarOWvH>ewyj?9pn}bYj_guYM_i}*AF_{HH4YF-btU-0^%^f< zZ3bque&sED5?97v=hN!Nnm4r%Au7HCt!_cuuoJ{j|=4D1jo_()O!|+A1YeAtwuZPi1B0ZF({YBs|)m z2e7gybVH5u3!X{_!s6z}1QO4rc|NeQjF9@9C!_B&*ZYk=gy4=vB@w9$Zo7}@By$U) zv!7!oRX&7d$dk6IF`vIzp#EF*WqcW@VpCJo8+Gfoo_S> z(GK5(>V`>DF5EUoOzh{_o%3+1$?sH2N8sXLYKj48TH36sv6u$dc?kM`B{ZJ3|3Mqu z$mx9Gp_?rSO%qd9O2wM1qVR%%NE!4oSiS&y!lF)?z|v$$qtRp>edtL*dwcsIvNp<2 z^TFk_@EeiyXrfYF2d=8mwM!qw&6gNHaRq+i%8cG4=OC~!xHI%Ec`44Jq;0=MA?E@j zrP0rjeWO_jGDGf&ZywgR8Xj^6;s8PD^L9(3Wg)iT8>%V+r-6^=^d|nN^>GAL!$9_+UwlN$gudf-+{I zqNROMuDvE29^XB?8Y-6!jkaGftZM%`;wH%h+)Z5B%2H$7qKBVKbzDZbDfauSXN&Z& zj|)SEdt?wo8D{8;WXJ<<**EOU?E&*+rwH4ohYymvLwv^tH#4RmXs=&w8~6AAju3b4 z9$(^`=(BiQpuj+BAmI1Nj>7d8QNErL!W8}dKTqbjgH5+JHkQim%z@vxhCHf9H*|5d zx{xE!x!1BX^rNYPsLMt8l@IZ)o%*;q6Dq&p0cW&$8G?eqk*cyw578%uXCgjAoB|7U zxDdP__ckX=m6HXppFOcfe1RVNf>yqK%|gH6$>b*Q&8`a0mNr;#Yeh6#tohn_)C-4T z6NTE^+K6R2Esdc?8rH#D^nYGZpbjt4LaC05=wej~eHPp{4<+%LsrT+h?yQ&k7e5vo zkBbO{_x(V{!T?qm1Kjf2(c2)R@KC7>(`|^t^90n~BL0rBVIto#T4@!9iYVrM^B!z) zLk{m=e89P%HpaVa^*tj&gzN4yXZc7hdr&!i#3&CD*5ix9T->b8b~Ev1eTT<_Lm{4$ zIMH>ujX!zh&{`;b^jS1wlOCeTaduRtTw5J@E*g}`#NTy{EPWm#!vqq9ge+Eq2v_Lu zoMd77V^MkFov~WZefe!4!|d#A-6pSUG~zS2(G`=17yhE}@uGZgsfz>zo(gV*m@tqm zb_4YaBqo)ew~vI20Pp}eZf9ob7T08~KPp+Wx0iIK-SEuV<-wdp0xUE+DJ*p2Rc_8) zY>{3W>a>ihht>-tESwMT(&xH{uc-!|EeC4@TX{3`Di+g}+Y)a9JIO{7hBAk~%r865e-xdnUwBivwwivRe!bf6XDW3!^xqwE|7OdDCP&EOS zV;YZDaohtw1qcXQ5U_ZVz(5Gw^zN?ETqA&FD@-A7O7WaG1qqlrPZkUv@*lEn7U)=; z3^)B4=;%IYN(U!Z*!K>lMl1$DY?B~&WyCMTEzMcN?&jLC!*a%UZqciBzb)Zt#81@`G=O~6+2dZ`Wk$IZEb7o)2)}^9j0-bdqgXZy{eAph=07M>Ab)l7 z^!3e6(9MVlf#?QUHl3E?zk#$m&*jQBbw~WaV|G+3Q|*OK66pg#Anhq)wPJ-;(#H)! zt1(B+1Z`Gg=3NaknG3m=hQg|L1z>%#L4)QKDdYBKd%UJdYP^Mxgv>Eyp9lc7&|86$ zftEnUAN3{hJaEaKogJWf-&Wqj-}?nGL6x*f$`y1R|LKvB^xwx23@#_xkRb67L{f)k zDGgpP`)xM(1rL_Pe%Cz2`vsZ&>!!6d^VrN=&~Uc=K#(dZ)2m5h(UW7(_oim;;wnOf z)Z*}{C<5H18VY?EksD=$v1eeH2h-8_)R%BzF;?Q^#cP{Kr97^pNR@okXR8cu><`&t zB>qDMXPiRL1qoKD_C7w1vgb&oyomst+-c6|Lra=5J!C8Jae4(_k>1BaM^?+mgjkak z*M=Wt*Q-~r5+B&T;|44VwJq{kAf3_F}41i!6p zRZ%A)8T7=;aE3bQN{16OJ^dSufIyBuSDw`6jQxE-hmnepvnNO(K%^D+8N<9PR^Yg?6H_i>1QGCj0k^oEMBGE$ebT-fm zGO-3dGv+l2Sy+tQ_kYhp-Z^}Id#}&4*TpCDBBD}cSejnOBquUj>8J^;H=A_F@#8m{$6Jgek26~lFhaki7^)%BXdZNvYrr<+S50!Et0z7Forx5L&3%sk!+Sd z7GMb?T=RPMXq`(QH{oc&$X`-j_OoK-XN;wiZjmuhz8c%--|L53^@(vqf|8Ldi1rmF zmz_IEcg01r;7cctz<(3m#)3>Z3BRpi-KxCS7vf7Pkst2^T!2Bff+A>e zv_Y9Z65RM2jRJLuD)=ER4SQTyMUi*O_1px#X}7~tq8Vs~Bs)ub49JMoaVI*f;oTeE z_v`f0-4smL0F;Zl3Og`j?>_QI6EfRzuOv4&H-7C8)NRNWR@uWDcw)6X{Ic5u>Q=uc zz=art77x`+2Rt8X8?7afY!-Q#7YtadhsfLxG68PTBjOF1yY9y=@9XQU9EF1=gQ|V` zY(J19NKuMEf1tM63pS8Olg!BlG3T0SBlw!83ox`SHl+#@@vG$P;Ej^QIi!RDIRTm}Zdnc4x_mw)KQHf23BfDmnOvZ}M0$F(KG0Oy zQ0#fKz}qH<@>KFkDJfz$74IvGZU*y$n3(-LSGO2Sc8}d&cV`9XnKNfPD~e(z1XEQA zzoyz~sHyL9C73-vACQ}&!wobw%dphW+1dHs#E(OB_JFy%vh6-VL0HI-pE~xGpXR5a zrIs!8(@5nd8K!f9Q7HXlinfVkm~qy5BGu|G?pL5$=(RCoW%YGgI1&mNG}!{e!fZG< zsQ-Y&O&xTS(-#rWV|QTOBO)`6pUup?0;H>ql%|p*@OGxnrjRAyZ)pt8`o3dQE-uOv z7){|@C%TS?7JaTlb@oo4a2sCH$Y~ZweVhig!HnEiNkIDKV_;|q`W!@|?HO|NLd`-O zz%!9Ylda*;dZyZf_fA}DJtZCW>ZmE{HKdh1f?O)__Pb@ZQHI%t-u8`TSV9|9)AzOO zn`>*238f&$`7xe2^1Z$UdWGAWFW|p`5K7p!q!U7KO#UP_dkI3Y&@eJ;H5sO_yY*P! z+)zcijv9*d$9a@vnMwO|XnH)OEOdrXdRJk9nZGola$)6w*udRwG%WRQ(xQ>hritTU zRd%oHf>fdF&{5ORRLj5d#uITBB<5;4w zj%l#R-7FNf1r7CBU5!KE=x2{+8tJU~EZ&(-AL!?xgS%mS${w-)TUWw5rZKZe>%V}; z4crd$rt$S%pou_FgZ8V!E@(7kmV}hC4FczdB3R$TyfkteSl6e=egL2tnz(6U=qU^z zOy)3~{oZc0#~Z5gL6^mTKBWIGWowqx8gTs{0fl`M*ILNMz}r`V$HCuAr;chAYB;{S ze=UE=)=)Lx%u2RdO8Q#Q1?-__f0+=HrA^;ja+n&)#1FYC~kQ<3LFO&kW zf{1~&=En%+c0N=}9y8V%u6m}(N$~(i3no<1qlL(PdrdG>A}=v5|H6Bc2U!e68!-p2 zgzjznLji)27#hWz0wN-#FBz^cnqH6a;fM=`JBB4;KED$5klx76g(+-)`&&I+;1y`& zwUe`l<{pL-!l6k(GG-!`KWVaGf{}RD(98ZXh=rIpUNl3u;uI|8+$Pi5L2XL6&KNfnc|{KlvUTFC=MrU+B0AW8U~P0{KmqI^K$l-6W$B#hw9->4)=aKG zt_0u)H&dVvlZ($;I8P{)XUNFBBs)xbBXNkz^Q+MLIxIoE2AfBE?@+xuQ}nMlh5);b z!Fbo0nG|ZSK&#)|f2t+~2u&DLX`qVGoAW6@fIl#&h4q9HI*8-SI^+Ry#731q zz~=swC8%Uj0W=DSE=I^{eGm5Pu88-wR)vVVeD)XU7795m8;AP!lYg%CT+4W-!RHo-zZi_ENnxe6_+_4W06 zm}G!R4)kFVMv0fgK34hdd4Xdfa>5;<=zSq`%AMp@5*gBO3v?0Fle@+{cMMt#TD+rQ zq{AeAh>UuHdZSt3v@ks`_CwSn1Jul@4goQ-uY>CheVCqj(w;;85_ek;A^ksSu!V=D zo?7VekxG{kx_C%IeAbDd@y%F%Nxm6xu{|9A)ott&|DI4lFOeAt;%}yrga3jW$V!mz2!p^6E@~NoSwWGuWxv zg6TbS@JovvkqYJ|m4*ZYdTxBwS{)~_(}8&y!N3=9k?KkvF^i!I!4cd_50n}N&PIJS z#pZ5rU}1(1lKdzW1vFVi26Q1r2AZtaQ$QvXUK9-3LiRe--GN$cD1WHKX_RYH zaDpa;o!vBGscmiO^!Lq+-U#v5e(MjSdgn9l z7krvqiRqqcJQaJt;5Gd9Y8rQ!r<%8R&NeLdkoo6nro7bVR4D_zGggTeC4j;Fq|#H5 z9*^^vfrJeX2Zb0Y96~P(47}T16*M&b{r&N05)PI9AyU*^!1Q2l3;iN}ubTmNP^P^g zfkj)z9F*@17cS68Pg<-0bp1!2U10h8dy*nLB1yhHVEvM{2}#P*=Ij+MC)7XeAkDl^ z5#|@n;|Kvi*QnH8l0N| zA77R+584FR-!0aJARYqNa*6a9uqJ4#Jp3inwKCPzAY|9YXu>{O1JxKDJ$g00pz_#Oi#V|N zxQY~m3yeCd+CdMzHV{kDjbis}w)O6ay3pNO_iA#m;#i^eZvhnoM-8$s6kOoB$!5{h z!VWj%Hacap2RzBi50~))T{6~rh1wPJQQ;Qc3{V9sIPG+Xhp^aeu*sm(Poh%q+Y`x) zcvk2;P6dLYK!U(|b~W+VpQQu^{BDm=bARIA*eh@hu{1_tG5#US;0x}C5qnJt`dphXx3O4OZ5rvsExkJp zI-n6ki~Mi1=Xqk)E8PRY1i2exSm4Kvi>JD4#<<>29)cgj?kcfngQE>vX#+4h$qo_R zKtqG3*%n5`b6DxY}5J%zfu&H2X9hM5)W(P(hMhXWTeJo?EL z!}ST+c%E>TRD66KOnw6mAmji3?pt#$KAp1Klv>cZSd?#X$=K zxseC6i|grqeIkn3m0d-ygjGxFpU?I=m{An%e|B`v9T1WpwP{f5V-Q`zEl+1 zbe?PDp}?5Wo5N>=9CtgjzsgVG8sHn`X6f|UlWCUoAv(L4O+<*o8}24eTWnICa3!ZA z^q0EDP|vxkE44ew6Y9UmC<0kRO$C^%UvsZ)pKxWgR6bP343rBV5I7c8>eCFd^kE;t z@rSaFn~~Y->_b(je}NDIe_>>NgYD#1dIzyr;MxJf6qsVF@5YG8%*zMp_#OJ7`P5|} zs|Th>DE|3yIlw)pZ+1pl1sktbIMg`xu2Q3(6ZpO+6n9i%jOu{V)H)ai)d9qMPEaBg z5uvY+e$%6wRE4$;wPe7YdxyS@6dlzN=QLmf!2Off03Qqt1r(WOrv92m8~I$AA4P{pGA%b_F84kGgs>J)nNF2tD^!LwKo*QoG&D57MZF-KvMDh%?d|y zc7q*>o}YzPGZNe}8=^=vjCKso_RXLPhI(_Cd9cdu#^$F$Krxum>I}Pm(h@xrd#IlV zY7HmMgi#M^t}p+v3dL$>_5A`cS@gX1hhPI|&4XgiX+2DpJ}1R0jrO4cBO3mzy_!zC zY!|_blvH10&NosuL%*D8m3A*vtsg)y-hqDg+^Ik+_fxJ{U;*M^CsG>t(bCbW1J(ZU z;Y0WYnqCnR+h;G;Cz?<}q5yJnEI6KN?ShVWaNhMnJb+l0WlvHrryW<|?OUjZBVq|< zx7wub7?^WXGkc{HKwRA_YH2abOZTfs%ND@@YOiSD@(YSjZeteo-D9VKI)L!%60f1E zqT*csES^9hV1XM11w-LKiu|yEC{xxOE|2(~pjcRV549pfSGqV-c?aAL*7NlwRpg3; z$M?vxVtJbAk}LoeVP*wDw!68EgUf(EM8+u5_}#Z?XkzIzVLg&zq7hO;SxE`{VN{{$ z?I4PgRW?{wbjI!KS`~ONix##*qfis99JaQ$UCWO|j{QXj`<(j)Hn^h^X_)SL^Vxk% z7@C5|EVbOf^6x+;Bap7+9*j&RK4=wcz-K5mXoHTSXsF6>#q|nQO5f|t4ixwrRj(_W z#hTEXtbKl<3X~LxHWYm5@T}2?O|`x70;HlsaEe*m+uIM1%Od>`={jsiKlc~EO&fvH zqY~D>vmUs%=uQ|1N|-+#iY{+AWr>xaS@ncCx^yZ~hjt7CIM-)2Rwb`#hI{tp1P>{s zIoeM%odY}o+T`Sa6*Qri7XNW^V<_M-(odqGICT`g1i_7)dZ<(WdR4kY&h6N_qm%k$ zBVYVdi?ul+SZ!d;22>K-xiCYRiWqL}YXcE?GeGTFxi)Xq*f{a~xVI|8etaaTnDOGd z&SkQf);y%rpml)030w0Q30#&Kb8XV9Mso(z&C4}-BN+HqCHJaezy9{~VhjluI^)7~ zmdsXY+yD{tm9p1=?DlQqPJwz-^{wUG0$e|suhT^ICfOgR;GiN^!FTWo5wWwD&zz4M zgkmhF8q`GDGo^HNboO1_o)X}Md<{heCIOI_xtx^{mo*OH#s!hU)97FNDB!OWD1SX{ zVFgVT+$QXArQe0}vk~UyT2L8Y`860WmxOe5`F?fe`1#QN&|(VwK!r=O<9_YR{BNekGb3P!BSf4<4*+=rL0N>Ja)zBl7sceGeKM$5BOE#imk>Agpk zVISi$OWrJsd03F&HbqH`q<-qHwh$HwR1@ERS{z5?$!*>^$%34^Jw@(YeF)^t9pX z=WS-@nulX=Iy*Z(3lr+!tXW9O@su%A(&-xqu$B?Tgfe1r+@CE-eqZy2v{blkC> zj|uJRYj6^y^~qts;3tA9OcUu@fP(`p>{1;5Wr861^HucM~u8sa`D_x znO)GDt}nsKko#AF3JZvpTiRY>3Njds*RlLa+lKZQ+OVc^N!P*rPyMhXjNF zf^!PG7f##M2!}ej34k(?mu&DJFa#QH@oy2DzE_#cFP5mrhA~S`7IGX0$C+j0wNCT< ziAt88mA86W&Yt*}hZM#5wj3HPZ)9Yopl>S2t?9%36R$H9=g#p<5|fjBEa-obV&=Cs zvvFi;GZajGI`CVgJtzrK6`jLc!EcpRgw6EARM-c)C}C?x(b(c$z~ z%2fL|`O0Nb<`TLGiZ+>bc}wWVa)$Qah+HC;8|hjTFzS$p0G*Pm^Vte;GL4*`Lr>7e zs!H5k^pp5mTU&SDy4933=emc`6cMBU&ZRT&koue>3q}R=4yVRlkpib=MhIq@B!CC5 z$Y!(DuxT{!IS(UDzF`AlJX&;iQN0c(!vPFt$Qm0H+3O1O30rr?-%}JISNi+6 z^qs3I1z)8m>Uq^H)}0xnjOy?WKXI+nV|hG&o#K7h9S|Onj8$HY%@a0yeG$Bbh~3W0 zvKi=$&{Ze3{C;Bg5V&=XLtM8-P_t_Q%=2K5FkjoUS3HrK(FUs6O)I{(^#612uLk!6 zGW-I8knH3She<%>URW2Ae-#U$1vv*1rF3PC1jquAN-lJOxUAMc1@zi*>pkTT>Nr8- z17t{(x}5017J#BQI04|8gSEA_Tt$FP_aM|dAo^Gp(Vk^B9&!LW8U}_m!6H)a{PJ?% zy(x14{%*q!z82KZ(aQAqxl&4a&H7qfQMb?c`lX^}043ephpEuyuKjy83RG4QGGN58 zeO6$Sal;oQ?dVvFS`YVr1xv1pzN`Tu4CXxj4qs-U5W5v&wfztWJOmH0V@Q+Lg$FQ{ zCw6pMhn^t(ESVfYP`WGXziFR86$tI=9+{c!!s6o4M@pk2aAbGG_<`*I6E54=a&5e% z!zo6k_#nLE6hL;M7km7u?)H&UAnq^&Ga87{x*Gi)CJB(cIrmpYQ;vqoWy5R+1+s*0 zs00oPQiSDY9Qd*)OseSrPd5M&(SGL}^ZeU?$~XkbG2E2^an|3R60QR6!}XKMHwFQC)`H5CoG|DU!9>*27Yv{Y0t2r(^(r;^WDvlr6T+CKj0FS5FRwN_2_Nm_WOQmoYt5QK4M-7cY~NX0p)&&n+gv@gQ<|zTyS0x$12?zRiCah2fcWU0>jci5!t=g z*4A(nsbY$#j;2hpi1w+|)T6+|@Fk(>bckpTD?^w~c^m5SfJ(#R0GlWwQfct16sX(wj$olsf*J{>cqlZ1Coh&IR>d5eYakDc=Xv2p z<=UT?QU3^!D!_jR|Jq+ld(}#UWOM}R@a;@By`D6%hyfO$R9YO2&;ZQi%aN&X#(7AK z4V!eq;gQ^fiU2NL{HZE-eQDy|47>IaCD8Lh{{gZ9Wp(%?;jc9vtxBxmvB^7A$7BN* zzkZlIylaGmMSaU>11?!PJ69|QU4zjLWEhOn`bM!ippl;snY-EN36jWgv+u5ouvj}! zI5qznfgsTvLa?;GWTU&4(?7k_q+7 z)CwT2TP}KIBVnGX!AXIy@IsF>##{;6nE~ZW4~`a$#4|?70T*a=6s#O6frI`3vwlIR60CGa$2=Md=Zcomk{)1?_+3blipsn*$zX2#c7z%%iq~6>_F#*X3V< zCy99UYs@Wy*#?f?pgLR9`Y^!_@Ya(>K?w_EZNMJ*YdDcK5~&z5pEJGk7NjINiO@8M zbFwc=WliYzymS{tD$q|sgolPQzRDLMvFvgDYw{g>nQOetmGhL}fkr?d2q1b**&S45 zn+SfrD>M-*yA-riF7$ZwA=LL}v zc05$?*I<@Zz9gCqlzV)9oZ`+k=)|mDU17W5%n**%-vYZ1xU+oq{5dINr-iYWGPcLS z0=9Jk?*PX52WV$AmV2hNbH-U&N2RdG&1B*+_l<5_3tS!9$IM3ZL9CGbsNl_N}+1IYd@0p^f)=!E^j{%DSR572Sd*x z`G&yEg;No<63}1Zra_7W`PlH1ni+MFNlMv!aOk09;ESg~RpTbu*$U7Yx&M!2ZVpU# z-Qq!5jB;tTX_FxiCQ7WVtnm)Ms{?c0AZ3e|+&R>5L*suyY2X?S>SwYD^N&ybeQUXb z?gq6E?b*A>5{b*=Q^eFc&O9cY4> zt!8>0XzTY2n7b=bh1Sf0`Fx`L*Bivj2zUg-9>{xO;*sO*V72%GyZ62Rp&s*eQiR{> z|FzArn^NTC`G(QsO>l+;pB2s4uM9PYw5=c9t?Q_5zq1<;=PSY7fUn6~{9Fqrzi@gqE|7k}=%j?0|%%H>ULR`>)?R-$cKRX=nA}a7sP~GY^VR}xqRf8q@OICb2 z)LZD8YLb|CA4lkffaz8Mj(1M(}#BJ)rj!gMXCeFa*xb6*Kx1M?iY4@4S|og%kn6Ip@L}UWWc(8Et5}-~Z)DLtbBM0f z+<735su*0UdFyuz)Y+8_zQLRg01QxYaNtxdESKb;DRM0l0oW;SR&0Yk4EQb4l^?(g#Qf$xrju2{a+PD# z>wo+=mWpeO*Xj?pmPf1@WJ4;gA29wRxl*6eYRFhDurF;s{&*$hy~ zKDc~$@N!~ecn7~(x5!Fk->!e=i*kE;3YHk}2Y>tm-f1({ZC2b^9ntp|w_E&a?d`1x z!|--0c`Ic{Mscs#a7Ocmji~>(-Tj|9QBhH^Uz6{2BOG8J-N*>e@vm2yO&=Ag2$rUifGW6~u3OV9&^7SD9$b>e$!bir-(Shd8Q7h z@4L;RSs5!gV0jf_=$SwhA?LOI+hW87*^2dRktDc%tLM^-`f;{nr6?;OJ6~ks*6F%S z4X+-i4WZJvp>H2I?0|EYuQdx<@HJMKj<$-xgmmnoR)k#kPqpFRs;R}~3rWMWbRC#Q z@M63@eD}ko0>N+K0KgF#HE8BmG*KO0752qDw-HB$7@4*yH7K3vTc@Tn0c_CY-T8*4 z3G}@YO5S7exyHWtXaYpvt%`Z3fm<1r|Dd+a80G;u+sV?mda+~0Uh#O@64ecV7d#$M zJhbZ8V31?;hO>Kkq3K=CVg>4=ZoLux{9Paj$%Oj21^HiFE2wtt3n#Ef@p!B;k6H|K zXx3dPy1t~BP~$Chaxb5_7<8pak>rySN5~*1RbZK(ESTkvn=fSqFA$zqFW#8B4dLS* zPJ*U(ig1eZ)6s+R@if-h5IB`IKR=&yM#00w!_C$8_2m5f=Fd8A6FwODq7UR=D zrs|h;H)?gNm30c2+4uL$Y_<}Q3zA>recNx&7tNddxquVUUMJp0jKRpWW7LTbZpg2t zG>h%-d*m&M6Zcytj0j<4%<`7_8z`Yjo7)LKJ>v;}1I@0w2wq4?o%rjK(;Trp5RIuS zcVzGVdUvEXPA+>Om1)249)d!z8O!ydAo4l-@ZojF`v@3E(V5Xz5VT0n#^iKMn+b$-9Qr1 za3yluu0?BnIQvf2+Dw8`8bOg?d~;)?28MUaD+*Xw3nzHWBL1ZHUYpn?8)fg(#x#4+ z2Zcj}#yrBCORA7C`SQub#^4jK&QVph#n7B;`=#60v+AQseKob1ww-FE=0@M@ zu=DLpNRh%wryWvwNA^?7zuMfrQ0HIH)0~LSty`&-qPRSsL9ZA!{i&B z8;xw<{z1FG>#HzFQdI5UAdV-iw}yR$`yb)ltB1ViwahWA)hWc$`uigR4UwT#FHIdC zRkH`~?)e#|%{HxO5we&+$>Osl_P5MZQ&K)9?WrUysze>k=cRY8iY+cK&VLM}RNUPo zX{~?*=c@4*^J{CaCLJk}?UB=uO&FhHF{v6IoZcLlmV&a_G)G_1s}RG-`zVZ_c#r@`+ubL z^z?Ri2&Ilbr1+ZBl%fvZGYIk~T;@~C+qVs{?DI@n@5oIy5B1MXeSDY7Y5HREgl)J*n|lxJFBGo|c6-TghGi8cp(JfWVi-I_1) zQ_TIMArO4J?Bx&i-Om!f*Jq14@tTFdmNZh*i?9A{LxX+~2eXZ)?_tDa2=ip6TUhP-uIZJ8yq+qruSQp6r>wcS+ov8oXyE<0rQoma%pSHA}t3&@8pI z+0)5K4j~$_JmI{vF`SF;dNxV6e2M=$r3}_uk6Vfkxs_>x`Gh^2MO?FMbI-+y){HB~ zuZnHLXSGLfqSC=zklK8qr?y<>WXllPwcc{wlduyX*1tmR!~h)2S# zC-g(J&6}3*SxBV;NQPJA0RR@4m%A%^{r*rFoSH1T9(o&MMgF|(J@vYUv-p~B;@KyR z`Um6d^%yMhyV%3Z4u=Ysdp6wx+>~!ey}DVx{Ku=Nw$?F~vrggck;;J~%5n!icy^um z0=82CsA7Gk_G)#I?^>mX*ZW-H%WEtvJxkKhAjZ7$P4q9TX%Ix6I^@+&Cnpc_?tK>k z)nZLWs8x`PP==HCMIx_uSB$@DOqBSs+dPPAs**387XB@!dh0BY!%2j~UU%{BrvkaZ zTYdM#r;xA)f=#uqLd^HBH*tJNZe=jbUp(@zE;2cGy;h zaJ8dI(Qt#8&y7QWgQH!`Y$o)duZ)O2d-u0N;2!pZB|&A*x6bB|*vEt7mvMwrxfp)@ zg?a7yyDsn?bL5>9_t{eGv3$_c(E&_yXMD3c&wo3?U&rJboGIdMe@Q&cRsj0$wT;vX zmFxj1U3ISZ^KgDYA5Z{}b&a^d@((S#ucO}t5SMufM8yJmsOIoR5{VZmJ!-Lw?oc6J z=cfVLUcCkm81uT*yrSw0xa;p!LX9e$pF8(74!A;spzYVu(ZSx{-oPkj)(e3qY-Xep z3?#-82Ab#KFi+X_Et#;mBqi2*|AYO&C;)tV1_r~O(JVIgi-97kk-Ek4P`fO2;3>LK zQ6go=r~|1r_bOc6t`_g;TZ)~ZgH8kIwYRcTe(dR!3eE>HVld?8Vp zGa%9E#DqV=y9K_&;-PCJl+6wkgbFEon#K0T6wbs6(rDe*u0x3$4OwtyDX>3R8^>1! zVG)r*Y@TX$Wo6q(aioZ3ypROaWm@3`M*?#QggY}l_dLi$J8+iWv(U_MXFd-iZF6}d zfe06Nc6R5cfa1YZ^1yA}Z+|sX1Kkx+xP3y)zTK4$2{EzHI3xdJW_ftn3QlZWFgc~O zJ^BWO;@w^LE;7?p;mB-^ayOZoLp9!E#*?eVsaK<&6D+Zq)uRESP6k@?00~=v<8f68k)A3`<0N`gqm-8=U*;_Q)IBUg~tb{B($eURylr=0|zMexu>d>2-M z%e1xwweU>CGJP8kSwN8&Hg_Quq1MAspuEil zmO9qr`Pz$wZIM>>i?ZmmBWl#)rD+$g@+P#^I4u7B3Ax3t{2B7$Xd)ht^;smnT=6w>qU)y07LKG`(SuGPVAhkvm(j8q)di&zQHqum1f z-Dhhc)xQK}GVbAh4^TWw3cUyVts0SrOROK?E|*nCHdv zuD;k|S_Y4^+bKtHB^RSfD-r#TbO4TKlgSox51hY-yks>+x*RKvCqU={$+_Z!om zjf{;A0b8~VNq{oaQ7+cU!661$gr&FzO!IBGyD#YKL$=Ti8*FV=*_g#!6q24s)XwAF zv?|0y{?@ijyxK`(eca0L?^>OFmBmJ7bqb+Di@mOhXY?P9I)nThE0(QFO`#3;_lTFxOW&S(~a$X#(o!G3OO4Jbso}?9w^MG zs#~J}!P~CR3Z>@d1aw!QfHw#ZRlt_}(R$+^9RENb1<8=I7ZcN}BZ@HrP9jfI zpcYjm#R>Sya2?ugrek(j`dq}l*7DI<5|Tlk)U9wHUi-98 z{QR$9YRmAIBxvwHyxh$|C zmE!akZX(7HikUKDIH9HiCCA;XDUBwg{eIuMpvjc0vFwGy59PA57!)58JisSG4J0xmvCkdMS$3noLG4rb_A z;oHNt%`6j}pmd0&_UhxqL$ic8|L~9sP?5UA>7&38aQ|@9Rq|(CD-guTS2`f{WbZ-h zrYa_VwiR#_p+}p4plig=WxKf)qYUCMyt_oq{i3X+^3jm2yRgi+&K8-I3Za+E&;=~e z1=-?Cht2=f-j_#Hx&CiIX+ZO!QW+Yyl2l0Mp^4gwQkhjGBtwJ@nbJt&L=++sDr02I zSRG`DP-MuEw3Co2ncwT)yK}yM-@o7WuJv2%*E;L0bF@ACd7k@oe}?P2?$1Xe6uE@| zL#vEUbm#uhUgTi?H}v1Ba+Iq@@&A1U*DMor9nNmih^=wzazrh^bD{o<_ETaHg!v1U zjqFfWd^m-78-z?Z^RM3% ztIYG!36IGeGhq-ciVaVGYH+fsNqPq-B|%3~-}PM5>arW}pGj-Kc2L?|Gy0L83k6C1 z*+7@v@B6*Gdm~Bk6QL1LsB=Mgmbw=&E*{Tu6dfL+BA%dc01D;du1LYpPT|O3Pl@Sh z`z6J(ii~wg7`OD49>L4z^Sk5ht07r+O!FKkyqrzN=#-Y>%~P!4<;NXAuQC13`R``= zo*D`I%qO&~>BxIcB_~7txiptE~T*bHjv=q-bxP?nE41PX0NtHG)4W-d_+A3WbvMp6$ zDvG!SDDx<=c5`#md+zyH(A{zoblYDH~jqh^Mh!6m3WzBT9ZAx2~-aJzT{C?p*b%lQ0xk3Vb#m0upZGy1Qap4=l>88==_V+A`J;D$op+CWidAUktkrQoPy43zUU z;0U*QO5U?xZB_j84-4U5?2n<)gwOZbzVxbLM*w^|r9ohq%(AQYrj6;iquVi$`{n9@ zD*->mW%AG&G3C|tL+S*t$z5i?npG+>tNr;7fxTm;DF?vbaj?ysHGPHAFDLfZbf6e? z|J%GpX)IedGG9#5O^uS_yST5WK0Z+69jqZzB*@_zcyCU@xQ{3^E3n>xz-IkeuF}{? zXOvw8C%bi)-MoC!8}J4R2jo6`NF$F#0%Z`3A2&&T3~a?%C;gc%mC0-5gViaUkMP#2 zhO<|#VYqeTH{Kdategi%-;sv`yX!fiOT#5R?1I=(| zkX+^2rg+;1A4y*$)L($6@U>PM*?)cT5k)?^rkKq-+}P$ZKA*f#|Jbz5Amk)Uw|E$% zSM0(Wk83(#rwGGWz*{z^Bdg&Q0=0@i_@$5-TmCuPT00k(PnwIb z?3X)h#^`f(-&hO-|8P`!@fh2eR;l^?zmltB=z(28Skn2iXn%F?3ugdL*dD5SQ+b`d z>*!HbtFXlk1&Kq(DAa9uPGrsp@QRAbsd-oW;>2w%!@^VD4Hf;;P#LDC>|dd%cvOiMILl^R zGBDo2-;cP+B*|(~sV{$C*akM6p+HlMn`YHeJnIpG+=(AN3;{v=udt+&l<&2w$%><2 z*8v($K$SxoC>&5_6Um4av_l&^UgX&}_v*tOJHKBt0!+okez3U$8BWf9p9){Le?abM zEp@N(^O(YsyVe^V!(+u&D<>L5GQOd~kpNw6I19PN&>P7X;Zv)~LN_Zj{Ta`%R3K); zWn4;CviJ3%a0>{)Ye&O8XzCIwS~~6Qz`6EZKJ!8x=Hq|0D<6aZB%E;F$y$YfWFqK+~PuEPfy~?n}7|gE-XhCTO&8bTWNq;D^>4h+nYRA)5d}&J7mRwl=&pmrOiSRno;x;d z*s$EKzBWw%$8!wyW(p-}#+3l4p~-kh-Yrz; zDzp%nScgDC1>Nul;&f-8cKB+Jcc;YxkJz=fhch~({1mDI=naDY)V1M3t^IW7H1jna zY>TgKdie(^I-SVAO6-rJGN~UXgxuGGN<{#jKt}TNM!#)>%xH1cj4w475~jKS$u6vA z{17rD0=%GCq@axCqR#qMQyANO@@3;Aw*Dg8n*fNWS}J0ND3ta`)wq;W;uRTQv97{U z0>%h>m(HG(d8^Gj^XHN4_(-x8(3PX8lSq&YXTX>E*SbI4BB7G~vDQGOm9Y$;VZb3C zq632z;TEj4g=+wlWK^EjEV^SUAY@qRRYQT8b$A;Qy6|L=;_EJ)xons@Sn8=6XP?N% ze)n3t6t<~Zh)|_1^JA^M#JgrDFVj@lDPT7(N9~Q`*2o8qf$=Bq!bWZUyoC_Z$P3dzRG0~GF>RdoS47s-`XyZ1Uq+J@Q1-k$uZ{;$6z6E&r(w1HF3zF zq~fmx1S~=L&B%z|a4`I9{?u6hopXFG!$3DpZ))z1S~O3q4mx9iP5{F7XO@M=1PU8+ zmvXPG8~}PA5|hdp*oJF)D2e)KKEDSY17qcNHtpQi9N0p3Pz?Pr>iz!x>#twh)jEgQ zga6>KA^vt!Q=$E#YqYX>xO zIUduX>rSa`Ur6uTA@Z*|6Ru6$ecQ`JJ`vcf%Lhk8(#T0?rK(+u_YN+hA!9)Xr=>7V zI&kOx`gEgSGp3A2k3?qk@#8UGUf#zoz4seq2G36YJez8ga-ex6H8)otAsn`^vy`A( z$vwZ3_frjB({_x2r)cO-S3oQF`fGz-YN=-8E0fonhk1EKre-5^YkIX4l<)2#vy zLwsckH@4&G*QWGfglT}X9ZqT~Z3ju>UQ<(}bJCkn<}N$S)f8z>*EYwlKjAhK4Yo_1 zXFeWf@C7@+{zwR-JaD1a9L>%ay2q9%-2#nbf9@Qz`w9UzBrE8l<0EQYxU%>uP(s28 zx#Ezqf5pn7=ZUg6_)W22KS`PB0(_@>c$nC)M1-zzS;$R+ij)KDT)DQ{hnbLURc&n$ zxGd!3jO@fX?5*Obcwk_845!pR=+IW^cSE~V7w4ZiI^Ci^#- zg;d8*xETDS1z0dkvWVquaNyVcjSWv&c=#FQ#ML@!WucAQ2!TGLh@wdT;X%SC6n#nE zsTRAA4lmFz*W1(;%NZOg2(P~^Mb$)#=SjWAHy|#6zR|s<{ZhMm%#vGp9FOd)EFbtN zs58F=SO^_mirvJ!`ucyg?FJ)`aLD#jdvE_Uc>&!TjFE1QMH_KR%n* zTj};Qb(0^zQq@Ve+)FW0ukni>+1JVD>u@5xwvVWae5JVP50lEfX6_+-{rEuTqliY~ zTy^>($^wF6FXyXFaqP;4&QygSj|)&5ZMOl!HPxmp5j_HK5)c>*J9G+<1H>T~4DX+& zyBT{HDhK%s>pLxk0P_1D&vPN!a$~x3)S@W+q_MzL)&7DyGt=1GJzjP$K=gA#C@U_j^Di*ijFUTnKcBMrG?{JFJs|G@k1GB2I$U0Spj&JZht6awoE zrU;xVkd))wi*1(4T;;)UJz9(Oy%yX)n`=ZTl<)n#BImnq1|a=FZhE!}iwJ$!`ZVcLo_-sKCn=gzaTzXpemB#va~&uLo%bN*v+ujz!#XukdV< zf;fc*j{pgUJdD5O)UIuMt-SxqJsO;Ib*vC!9p#RfoPSUUY?9ohgn-#1Py!_-UNn3^ z_9DI?j!?Q{B%|9Z1I_^c$W&uzdmm{y!r`gIA7*dhV~CmtK1EKiTJa9=-!g`g?1y*} zI}|dn8ETj2R|3bz-fC2^3kIBxv|7rS_$s0(`K#+Wqo;{|r*%&kbNU33kof)$^up&GKJqhg2syDocB5B1BU zQ6`A1Zs(b=#aD)I^-Ewdoxgtl(qGbshFvOR`hzdta<3x+U)`UEiv59J7_AeW(*uyf z1B^;7@N+UToKG~gwzc(~{1+M5Z<_Xg(Ts~o-I@H5W>Q@Z=pW%SA!Gl;?en&nt&xJ-fNpLhzc85S0H<)AA3?|UTl!d{DKyogURhA@4j zcckr`+eoYYEgLO(UmgDMTbWa5ufbKgt0fh^Z1mn-zO?$Tz{wa!=z5%PlfPs4VDYTD z0;bC^-$+2*a|iYw+gkdpxq4alW?;{()5SYDG(f+op< zy4FRd3PWdFcP7N#C+oeG`N{#34!A3)HcMAg3k#l$Ap5ce)i>|jZX88G2I5!OWlyhm zlnb)^Ir+sY{9)>6azNnKU&He8yly7{!C(L(bv*}`hEgnHv$AjdYwq10vwP;U>cYY6 z(>&(=Me{-={5d?_a;7yjW+R=DLIz5YfQ2~JLxs}(Dw*?_G#*JqS!u3EH8yB>x25#_ ziC1^f+sU^5A#r^1e4s!+ZCRD9UBtb#u)gsvqA&_T?yKOE1oobngx8~+_JUZ!Hc#Hn z=`f#_F0h~n?f4s$svq;+PrzjcphMfk7w6Dn0A+rkWBG+u|Jpw$-a`?mwsH3ZIS%VJ{b}mhkIbZ)q?1QO@hKT;*f$kpLvzuTvaX5d*A6|4QbX@Bn+7aGTPt zoaed2pX+Ly*lyi9C@2Xh_U6Kp9~)SaJ~ME3gk2GLp^Zy7u$ly02CszEd_I68(C zK$Sd=^HR7f)r`J89@WVHv*PrSsllJfRq7o++GQ-q*VWp(!;fEa3+X`$$`s&DgdMP8 z5DJq`NwRWsx4WK$VI<`zcUoh2A$#U}*xjs4JDDsRmB}U#_$$xWKpGAO5?)fKgWOJ& zkXR#nFp0M{@wYF75$?Yp37z@tVb_s7_c$Z?%$Z7QZdgnj0zr%lG03Gz$#)n7Xxq z?gsv?Wo*cO#?*8#f(9BYOnxU%cH56n)b?bH^dW^}n|Ei@AaIHlJdA=SsqP5yiy&hd zz40~e5Ns2acgK??L-!!KgAd$dj=Sof3PwuI7H~U^n8&Y&3Un2lkpuU-+2Abul z_(uG7#B6{U9@`O}2U}lEM;;4om@Z>p-t|1upC&!rq#37irsXbzBu4dU13j+;$G*9< za!KOx`*!y4lUCil3hoIBHi<7>(zsx)K@X3C3N4XO7h zsVxF|qzLRNyF;ji-urOwWn#uU=R3{|TWn!jFbnncc+$|^3_9VlMPpv=80tfKL+{Ab zX7rHGeSL!7LFZhI=9X~hTw%Qol$ou*Qs5|=(kHk#ErmMnC?*{O)DtBvBFbb>@1&Js zA4#4%Yhp9hUP2gv(g-!}9DZov(=A)Rk!+^m$ss`WJt@<)*~1}o-=>n8V}%7r(FILZ zLY=(hj~xf^E4(K$zDedPskxxG_;pv*M551Zv_<@Qr$s4BS=_uPYRIYa%Biop!_74} zmT$dnT(4Jbo~Fx`f<3M}{q@I>A1_<@C8q!B{0d~iS%Tw%ajpE7XD(Z*^u~=bgf)Vj z`_$xwtq{&gol&C18R$@tukfb1vC48twCFU-7NQR8|6Bry29pRPA_c+L##i;I67?;(IP6kV4aL0=k+1DO z|K36ZN)|8{uo#+`r3&8DBQ93iMTHuGLV$GCe}z`(`D{4NsH(5kZX*0HKQ1a@pv;w` z52wWk5$v^rO(Oqg#(jL7aj;9v9UOY~D`uq28gBcqXl^kZXDjf~@@R%r%Gme(+!*A2 zhtTpc(K8r;AjB-Fu;hnb(bO_#BWLhDR*d#Zpi>@mfW(!h47Oe4|3*$c4u5!-Wfc5qIV<)Ey2LGYuoT739pY|_UFP;iM>Om!5SeyuiVU)!V80fu@T-v zR>e4KK^0U3!#k|7aJiCb=%jeJ(ZF*k+5?M8-n|dG0i^<2$YaLAnxjyx0&_aY!)P+{ zZNJS&6eki3fVLJ?AAPW(48S4)ZFu%~z)xJQUW(ji%Fc|hK}X^-kEQ(Lp%P&d*nxq( zMS3yvr@SNI0s$YLnB2cX)-dbe7)EJ_RGp>~5nB7^8a^==A(5bY!;-iVZBAtM9?{OO zp6W}9OcEP_FdNuQVB2MB_(YQ-^R`iimdS$|?jv5Fm!!73o{))kINE&hnRwz+YSMC1kiT*lnDn*d*)Y zKr}@s^n#+92=2R1J79AIi`Rh@C^+`=-9u?nuW8hmS7YlnZ{CM9A}Rsm^zjU2{GCVi zY|jWnJ5wH|q&$4rkm#^K6Pr^nxa+RoO=r=-BWCyfuDSJBzH{ zO)jMe%!&homT3NouX~NJk(EEn$BJ@P<_F}GSSJ9FKnMoMTBeRGy;ywdz0scucfosr zwi%6pmi+NzP(MS=QF6=h$bW4f1BD`PK=)F#N8XCY#uSZqq0o2=*l0l6gBdR`$N{~tX=JxChTQx4=!@92OK z=R7t8Rj`X#(b3~kReu>jJ%xKUn1CDxo(nol8>`FDL`{BzqeW$j>Z7{->yR@xygJwB z34QXrVx-BVzR=v=s9|XIEiQ5#Y)a}hqw?yE1-apnlVV+$+BZ`kD#ki!54DAN4e!+l z0QO>Qz%~7>{)D|Z2%|)83oQv{9WCgM%plRF0^O^kHMV6K`k-?X*-oFNj{oYz=R7n> z4`RpIKF9G2hJ~f&=JA{&Zj?rflYM@36v&eGJcRn3|aY;C|cI3-pE(%=H#z^hjvlGl{U=&6ARG*vyHk)HlVVRBIF0}p26 zR1$fZQTfXY^KiFE7ir~C9$7s6{F^EV>gxhZ_Be&32&f;9;S2@qTVZcemU1`fnc*aG z)`KuVAi0ng)O6k()4_UXniHwavb7v&93?v@<7H)7|+#*kQp-3^H+azGae&3 z!PlQ!j=dCGu|r1B^rmR)nY*EUmzcg~DH1qp&agB3a4a<98_$91W&6Sw4BDVR(~!u? zQYCJIdqwRiZ!hxb!H_|h2 z(da;V8oJLDDHJ6PH6C24Q@GZ74(%hT1)zATw9TEGgnkZRo4k;04;4Vk^=k~?phO2W z`V~2+@io|)wK?-IkxoJT?N9*C43cOTzgGtJtMQ7 zGc!T15=z=5Obu2BX~>z3#5fKgzP3mGDAVWt{^`~edBQ|;N=1k=Wp@ zxRl*5Rw9@>g`fDv&C0DvYs^hUAmZfR#(|YocTJZJVq`J(JIt3Uvix@vs?CF>4_cuv zjPcqe%N76FY}Zy$q{FNoX0vHAbqHcVaw5t7*EM%R@(HGLP`K=*445kIIb>0y5u-+1 zrY26r2(=Y4PYxiPExOB$bD4{O8@}*f6r*Fk`AIF~UnKG9+y6Y3G2NpBS{xh(JK;WI zU;n`1CJSvMLWu}5e;S&+436S0FfnP!Wu3wa4K?~RBmd25bNsxCuHV5%3E}jBD}>Oa zWN7PoLGt;RRqIUDzRR*FSHPPH(2>z29Yg9Yu-7gNU^Ed?152>PH@^87G^Zr~#*T)3 z4aZOhF|`kzl{wDIAhjI*)$Ug|zjrgv-~?JUZeg}QFnI6}!V`zG4~mL8{EZC_aM6T2 zb(`oqktCH;hWrOo10a1*z8^|f{*U)s_34Qvk&Er!yB8e&6*87FN#se~QYH10GeE$p z)|fvp4tczwcOu4pRJ}#IML4XbO6SwSB|*c`e8I+&1wfDjbsn;ZhX7j9yFKnb^4<5H zb}%VyowV$1z>cJR#e;RB$D;8yVLGt2Sj7o57JM*=iM<=x`}!d~Aru6Zd~Px-;vUIE zM=bzPfEU%3ub-8cmyg~{NU@%|#MJyNMxS0be8kjmc&-+deHe^mXYQl1?n2CW)LP4u z`%m8?P0nZlyhM3Rwvf~#kw0r!>UI|q|3GH*QUr>Do%L-Io~)Pk6m4dcH8BpgK+m#tu2YInr~7Cn9Xuy zR3Zug1_`J{gNSbTpO0+jU?W2i$o0gqg~{S$REwie4?!X+C;n#`D5_!OzQZ5XHqZyXC`&>zcZ;d|Cvc$ z;7yV-@`ss}Gym)V)x{>=FAOg}X+JS#Gv47N5VbZ+*)A}7(ceoVPqzwn@A|g(FKf?; zWy`YWa?LI=@O-3{MmHBtd(5#bfR=EN>$wx>{)dG}W1{bBKMs&^G>dsRlC=2c%2>tm z=3oWBY-z@TtGlA9<}2HZ;W0SHi0cbUB6?^K^I-(n!eLVs*COV7^5$T^B&EwFMer6}bDrtLY!coX{tB_V&hTz)JTQPzgF!;xDiQCj%miZ}Q4YpkK`E6yq{6`)}{` z9X{GS4S#eVIyak6d36zO|1_TO^4*k{e^#|+QU7_amiT}|#@ym)cV z{W(;o$cW^8%9^h8^rRBeoIFcU$I-@z`T2?n{?J154`!kviZPn6>10|PqT|XPXa!8) zJyOp?m%daG?Ky+hDkGQ`%pO*S>}IoG`sb^wtX^X-`1Y?2M0fbLDk@miNEwYTZQE~3 znqU$dJxzlp`qJM%J1$R7BMcp{r$t!b@;#6&{#__A2l{E zZj(cctAj6(v_E9;;qv^{kCycaNXyee#OS1&7{3YGF6}x>bdCMBdS{~_Ki+uk*s&E7 z7TQHVD*~#b_A;Q|D8~>{iO=o^ldT8GH^5u&QR@yx~ z5%p8CFE38}zsBK?0i5MA#VQ3$+nS)=)b`K0)2QX^5$iG^kz=Xk?v;Icg0+r2383Zb z>o0FYYZ@9t%C_dq9f?(YgCso^G7~wpNt`{O;p@ zTkF6R69fDE`XU{4SFT^hhDJi$(3i0M`l5AWPW_*GWb6bnR&K>8=Hz5XAkipLf++uU z-dIl=S|i)yp|Np0a0r90qM|~Km^OmJt^qU%hIY>uMSGC4;BBVoKEJ+I9ws}Sqy7-t zHgsL}nKnjfv*zBsL0t8|f;^h7nps(?`5y?C*qlG{QxBY_n25+<7z|89bBt)clSJ0;bc`I zQ9Esgr%+7j?CDX%7QTNRr;5rx72X&?Woj}xKH@%h`nw_z{M$jveu!VQFfqCxr|n~t zl6vorz$y9>r#J{kz_zvEqD6C#G`W);r-AB*2AQuJM<##lJum|EBFkgjd?LruMn+cF zwkshce4rV#ctdF9sbLq_c-$zD8+TL!;DB~vbt>*j2TV>*ic3nyMn?;5P`mfKpy2H! z*GlY5<(n&qGJ1!HhC1FoK4R*Q)+cU|yyV_faFs!0S)J#0o%JFEQ}q{)9H~)sezF8^ zZ80*>$!nNdT`C7l)yX;+eIc8oSgB`VjQa(WtWtMo|9?tCUxkKTtv!hc-1gA^3%0Y< g|NXCtp`5AdX;v}Qx`MY6oef1-QrnZb%fR#h0B{w93;+NC From 88a839061d20501432b7138210bc995898967ca8 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 28 Dec 2017 12:33:36 +1000 Subject: [PATCH 044/105] Restore more unit tests --- python/core/layout/qgslayout.sip | 11 + src/core/layout/qgslayout.cpp | 14 + src/core/layout/qgslayout.h | 9 + src/core/qgsexpressioncontext.cpp | 2 +- tests/src/core/testqgslayout.cpp | 24 +- tests/src/core/testqgslayoutitem.cpp | 6 +- tests/src/python/test_qgslayoutatlas.py | 364 +++++++++++++++++++++++- tests/src/python/test_qgslayoutlabel.py | 45 +-- 8 files changed, 447 insertions(+), 28 deletions(-) diff --git a/python/core/layout/qgslayout.sip b/python/core/layout/qgslayout.sip index 899567e42c22..6a02033134d7 100644 --- a/python/core/layout/qgslayout.sip +++ b/python/core/layout/qgslayout.sip @@ -191,6 +191,17 @@ If ``includeTemplateUuids`` is true, then item's :py:func:`QgsLayoutItem.templat will also be tested when trying to match the uuid. .. seealso:: :py:func:`multiFrameByUuid()` + +.. seealso:: :py:func:`itemById()` +%End + + QgsLayoutItem *itemById( const QString &id ) const; +%Docstring +Returns a layout item given its ``id``. +Since item IDs are not necessarely unique, this function returns the first matching +item found. + +.. seealso:: :py:func:`itemByUuid()` %End QgsLayoutMultiFrame *multiFrameByUuid( const QString &uuid ) const; diff --git a/src/core/layout/qgslayout.cpp b/src/core/layout/qgslayout.cpp index db482ffaadf4..1fa93e6453a5 100644 --- a/src/core/layout/qgslayout.cpp +++ b/src/core/layout/qgslayout.cpp @@ -231,6 +231,20 @@ QgsLayoutItem *QgsLayout::itemByUuid( const QString &uuid, bool includeTemplateU return nullptr; } +QgsLayoutItem *QgsLayout::itemById( const QString &id ) const +{ + const QList itemList = items(); + for ( QGraphicsItem *item : itemList ) + { + QgsLayoutItem *layoutItem = dynamic_cast( item ); + if ( layoutItem && layoutItem->id() == id ) + { + return layoutItem; + } + } + return nullptr; +} + QgsLayoutMultiFrame *QgsLayout::multiFrameByUuid( const QString &uuid ) const { for ( QgsLayoutMultiFrame *mf : mMultiFrames ) diff --git a/src/core/layout/qgslayout.h b/src/core/layout/qgslayout.h index 92f731c51cf0..972decb53e0b 100644 --- a/src/core/layout/qgslayout.h +++ b/src/core/layout/qgslayout.h @@ -229,9 +229,18 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext * will also be tested when trying to match the uuid. * * \see multiFrameByUuid() + * \see itemById() */ QgsLayoutItem *itemByUuid( const QString &uuid, bool includeTemplateUuids = false ) const; + /** + * Returns a layout item given its \a id. + * Since item IDs are not necessarely unique, this function returns the first matching + * item found. + * \see itemByUuid() + */ + QgsLayoutItem *itemById( const QString &id ) const; + /** * Returns the layout multiframe with matching \a uuid unique identifier, or a nullptr * if a matching multiframe could not be found. diff --git a/src/core/qgsexpressioncontext.cpp b/src/core/qgsexpressioncontext.cpp index e8735065b0d2..2830a74eb637 100644 --- a/src/core/qgsexpressioncontext.cpp +++ b/src/core/qgsexpressioncontext.cpp @@ -716,7 +716,7 @@ class GetLayoutItemVariables : public QgsScopedExpressionFunction QString id = values.at( 0 ).toString().toLower(); - const QgsLayoutItem *item = mLayout->itemByUuid( id ); + const QgsLayoutItem *item = mLayout->itemById( id ); if ( !item ) return QVariant(); diff --git a/tests/src/core/testqgslayout.cpp b/tests/src/core/testqgslayout.cpp index 42c94c8e49ab..e9cfc816f8ed 100644 --- a/tests/src/core/testqgslayout.cpp +++ b/tests/src/core/testqgslayout.cpp @@ -47,6 +47,7 @@ class TestQgsLayout: public QObject void addItem(); void layoutItems(); void layoutItemByUuid(); + void layoutItemById(); void undoRedoOccurred(); void itemsOnPage(); //test fetching matching items on a set page void shouldExportPage(); @@ -399,7 +400,6 @@ void TestQgsLayout::layoutItemByUuid() { QgsProject p; QgsLayout l( &p ); - l.pageCollection()->deletePage( 0 ); QgsLayoutItemShape *shape1 = new QgsLayoutItemShape( &l ); l.addLayoutItem( shape1 ); @@ -416,6 +416,28 @@ void TestQgsLayout::layoutItemByUuid() QCOMPARE( l.itemByUuid( map1->uuid() ), map1 ); } +void TestQgsLayout::layoutItemById() +{ + QgsProject p; + QgsLayout l( &p ); + + QgsLayoutItemShape *shape1 = new QgsLayoutItemShape( &l ); + l.addLayoutItem( shape1 ); + shape1->setId( QStringLiteral( "shape" ) ); + + QgsLayoutItemShape *shape2 = new QgsLayoutItemShape( &l ); + l.addLayoutItem( shape2 ); + shape2->setId( QStringLiteral( "shape" ) ); + + QgsLayoutItemMap *map1 = new QgsLayoutItemMap( &l ); + l.addLayoutItem( map1 ); + map1->setId( QStringLiteral( "map" ) ); + + QVERIFY( !l.itemById( QStringLiteral( "xxx" ) ) ); + QVERIFY( l.itemById( QStringLiteral( "shape" ) ) == shape1 || l.itemById( QStringLiteral( "shape" ) ) == shape2 ); + QCOMPARE( l.itemById( map1->id() ), map1 ); +} + void TestQgsLayout::undoRedoOccurred() { // test emitting undo/redo occurred signal diff --git a/tests/src/core/testqgslayoutitem.cpp b/tests/src/core/testqgslayoutitem.cpp index 9b05cc9e672a..8daad585bf2c 100644 --- a/tests/src/core/testqgslayoutitem.cpp +++ b/tests/src/core/testqgslayoutitem.cpp @@ -1399,15 +1399,15 @@ void TestQgsLayoutItem::itemVariablesFunction() QVERIFY( !r.isValid() ); QgsLayoutItemMap *map = new QgsLayoutItemMap( &l ); - map->setExtent( extent ); - map->attemptSetSceneRect( QRectF( 30, 60, 200, 100 ) ); map->setCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) ); + map->attemptSetSceneRect( QRectF( 30, 60, 200, 100 ) ); + map->setExtent( extent ); l.addLayoutItem( map ); map->setId( QStringLiteral( "map_id" ) ); c = l.createExpressionContext(); r = e.evaluate( &c ); - QGSCOMPARENEAR( r.toDouble(), 1.38916e+08, 100 ); + QGSCOMPARENEAR( r.toDouble(), 184764103, 100 ); QgsExpression e2( QStringLiteral( "map_get( item_variables( 'map_id' ), 'map_crs' )" ) ); r = e2.evaluate( &c ); diff --git a/tests/src/python/test_qgslayoutatlas.py b/tests/src/python/test_qgslayoutatlas.py index ec29fb28df74..cb309ad4e4b1 100644 --- a/tests/src/python/test_qgslayoutatlas.py +++ b/tests/src/python/test_qgslayoutatlas.py @@ -17,6 +17,7 @@ import tempfile import shutil import os +import glob from qgis.core import (QgsUnitTypes, QgsLayout, @@ -37,19 +38,125 @@ QgsLayoutItemLabel, QgsLayoutSize, QgsLayoutPoint, - QgsVectorLayer) -from qgis.PyQt.QtCore import QFileInfo + QgsVectorLayer, + QgsRectangle, + QgsCoordinateReferenceSystem, + QgsSingleSymbolRenderer, + QgsLayoutItemLabel, + QgsFontUtils, + QgsFeature, + QgsGeometry, + QgsPointXY, + QgsCategorizedSymbolRenderer, + QgsRendererCategory, + QgsMarkerSymbol, + QgsLayoutItemLegend) +from qgis.PyQt.QtCore import QFileInfo, QRectF, QDir from qgis.PyQt.QtTest import QSignalSpy from qgis.PyQt.QtXml import QDomDocument from utilities import unitTestDataPath from qgis.testing import start_app, unittest from qgis.PyQt.QtTest import QSignalSpy +from qgslayoutchecker import QgsLayoutChecker + start_app() class TestQgsLayoutAtlas(unittest.TestCase): + def setUp(self): + self.report = "

Python QgsLayoutAtlas Tests

\n" + + def tearDown(self): + report_file_path = "%s/qgistest.html" % QDir.tempPath() + with open(report_file_path, 'a') as report_file: + report_file.write(self.report) + + def testCase(self): + self.TEST_DATA_DIR = unitTestDataPath() + tmppath = tempfile.mkdtemp() + for file in glob.glob(os.path.join(self.TEST_DATA_DIR, 'france_parts.*')): + shutil.copy(os.path.join(self.TEST_DATA_DIR, file), tmppath) + vectorFileInfo = QFileInfo(tmppath + "/france_parts.shp") + mVectorLayer = QgsVectorLayer(vectorFileInfo.filePath(), vectorFileInfo.completeBaseName(), "ogr") + + QgsProject.instance().addMapLayers([mVectorLayer]) + self.layers = [mVectorLayer] + + # create composition with composer map + + # select epsg:2154 + crs = QgsCoordinateReferenceSystem() + crs.createFromSrid(2154) + QgsProject.instance().setCrs(crs) + + self.layout = QgsPrintLayout(QgsProject.instance()) + self.layout.initializeDefaults() + + # fix the renderer, fill with green + props = {"color": "0,127,0"} + fillSymbol = QgsFillSymbol.createSimple(props) + renderer = QgsSingleSymbolRenderer(fillSymbol) + mVectorLayer.setRenderer(renderer) + + # the atlas map + self.atlas_map = QgsLayoutItemMap(self.layout) + self.atlas_map.attemptSetSceneRect(QRectF(20, 20, 130, 130)) + self.atlas_map.setFrameEnabled(True) + self.atlas_map.setLayers([mVectorLayer]) + self.layout.addLayoutItem(self.atlas_map) + + # the atlas + self.atlas = self.layout.atlas() + self.atlas.setCoverageLayer(mVectorLayer) + self.atlas.setEnabled(True) + + # an overview + self.overview = QgsLayoutItemMap(self.layout) + self.overview.attemptSetSceneRect(QRectF(180, 20, 50, 50)) + self.overview.setFrameEnabled(True) + self.overview.overview().setFrameMap(self.atlas_map) + self.overview.setLayers([mVectorLayer]) + self.layout.addLayoutItem(self.overview) + nextent = QgsRectangle(49670.718, 6415139.086, 699672.519, 7065140.887) + self.overview.setExtent(nextent) + + # set the fill symbol of the overview map + props2 = {"color": "127,0,0,127"} + fillSymbol2 = QgsFillSymbol.createSimple(props2) + self.overview.overview().setFrameSymbol(fillSymbol2) + + # header label + self.mLabel1 = QgsLayoutItemLabel(self.layout) + self.layout.addLayoutItem(self.mLabel1) + self.mLabel1.setText("[% \"NAME_1\" %] area") + self.mLabel1.setFont(QgsFontUtils.getStandardTestFont()) + self.mLabel1.adjustSizeToText() + self.mLabel1.attemptSetSceneRect(QRectF(150, 5, 60, 15)) + self.mLabel1.setMarginX(1) + self.mLabel1.setMarginY(1) + + # feature number label + self.mLabel2 = QgsLayoutItemLabel(self.layout) + self.layout.addLayoutItem(self.mLabel2) + self.mLabel2.setText("# [%@atlas_featurenumber || ' / ' || @atlas_totalfeatures%]") + self.mLabel2.setFont(QgsFontUtils.getStandardTestFont()) + self.mLabel2.adjustSizeToText() + self.mLabel2.attemptSetSceneRect(QRectF(150, 200, 60, 15)) + self.mLabel2.setMarginX(1) + self.mLabel2.setMarginY(1) + + self.filename_test() + self.autoscale_render_test() + self.fixedscale_render_test() + self.predefinedscales_render_test() + self.hidden_render_test() + self.legend_test() + self.rotation_test() + + shutil.rmtree(tmppath, True) + def testReadWriteXml(self): p = QgsProject() vectorFileInfo = QFileInfo(unitTestDataPath() + "/france_parts.shp") @@ -237,6 +344,259 @@ def testNameForPage(self): self.assertEqual(atlas.nameForPage(2), 'Pays de la Loire') self.assertEqual(atlas.nameForPage(3), 'Centre') + def filename_test(self): + self.atlas.setFilenameExpression("'output_' || @atlas_featurenumber") + self.atlas.beginRender() + for i in range(0, self.atlas.count()): + self.atlas.seekTo(i) + expected = "output_%d" % (i + 1) + self.assertEqual(self.atlas.currentFilename(), expected) + self.atlas.endRender() + + def autoscale_render_test(self): + self.atlas_map.setExtent( + QgsRectangle(332719.06221504929, 6765214.5887386119, 560957.85090677091, 6993453.3774303338)) + + self.atlas_map.setAtlasDriven(True) + self.atlas_map.setAtlasScalingMode(QgsLayoutItemMap.Auto) + self.atlas_map.setAtlasMargin(0.10) + + self.atlas.beginRender() + + for i in range(0, 2): + self.atlas.seekTo(i) + self.mLabel1.adjustSizeToText() + + checker = QgsLayoutChecker('atlas_autoscale%d' % (i + 1), self.layout) + checker.setControlPathPrefix("atlas") + myTestResult, myMessage = checker.testLayout(0, 200) + self.report += checker.report() + + self.assertTrue(myTestResult, myMessage) + self.atlas.endRender() + + self.atlas_map.setAtlasDriven(False) + self.atlas_map.setAtlasScalingMode(QgsLayoutItemMap.Fixed) + self.atlas_map.setAtlasMargin(0) + + def fixedscale_render_test(self): + self.atlas_map.setExtent(QgsRectangle(209838.166, 6528781.020, 610491.166, 6920530.620)) + self.atlas_map.setAtlasDriven(True) + self.atlas_map.setAtlasScalingMode(QgsLayoutItemMap.Fixed) + + self.atlas.beginRender() + + for i in range(0, 2): + self.atlas.seekTo(i) + self.mLabel1.adjustSizeToText() + + checker = QgsLayoutChecker('atlas_fixedscale%d' % (i + 1), self.layout) + checker.setControlPathPrefix("atlas") + myTestResult, myMessage = checker.testLayout(0, 200) + self.report += checker.report() + + self.assertTrue(myTestResult, myMessage) + self.atlas.endRender() + + def predefinedscales_render_test(self): + self.atlas_map.setExtent(QgsRectangle(209838.166, 6528781.020, 610491.166, 6920530.620)) + self.atlas_map.setAtlasDriven(True) + self.atlas_map.setAtlasScalingMode(QgsLayoutItemMap.Predefined) + + scales = [1800000, 5000000] + self.layout.context().setPredefinedScales(scales) + for i, s in enumerate(self.layout.context().predefinedScales()): + self.assertEqual(s, scales[i]) + + self.atlas.beginRender() + + for i in range(0, 2): + self.atlas.seekTo(i) + self.mLabel1.adjustSizeToText() + + checker = QgsLayoutChecker('atlas_predefinedscales%d' % (i + 1), self.layout) + checker.setControlPathPrefix("atlas") + myTestResult, myMessage = checker.testLayout(0, 200) + self.report += checker.report() + + self.assertTrue(myTestResult, myMessage) + self.atlas.endRender() + + def hidden_render_test(self): + self.atlas_map.setExtent(QgsRectangle(209838.166, 6528781.020, 610491.166, 6920530.620)) + self.atlas_map.setAtlasScalingMode(QgsLayoutItemMap.Fixed) + self.atlas.setHideCoverage(True) + + self.atlas.beginRender() + + for i in range(0, 2): + self.atlas.seekTo(i) + self.mLabel1.adjustSizeToText() + + checker = QgsLayoutChecker('atlas_hiding%d' % (i + 1), self.layout) + checker.setControlPathPrefix("atlas") + myTestResult, myMessage = checker.testLayout(0, 200) + self.report += checker.report() + + self.assertTrue(myTestResult, myMessage) + self.atlas.endRender() + + self.atlas.setHideCoverage(False) + + def sorting_render_test(self): + self.atlas_map.setExtent(QgsRectangle(209838.166, 6528781.020, 610491.166, 6920530.620)) + self.atlas_map.setAtlasScalingMode(QgsLayoutItemMap.Fixed) + self.atlas.setHideCoverage(False) + + self.atlas.setSortFeatures(True) + self.atlas.setSortKeyAttributeIndex(4) # departement name + self.atlas.setSortAscending(False) + + self.atlas.beginRender() + + for i in range(0, 2): + self.atlas.seekTo(i) + self.mLabel1.adjustSizeToText() + + checker = QgsLayoutChecker('atlas_sorting%d' % (i + 1), self.layout) + checker.setControlPathPrefix("atlas") + myTestResult, myMessage = checker.testLayout(0, 200) + self.report += checker.report() + + self.assertTrue(myTestResult, myMessage) + self.atlas.endRender() + + def filtering_render_test(self): + self.atlas_map.setExtent(QgsRectangle(209838.166, 6528781.020, 610491.166, 6920530.620)) + self.atlas_map.setAtlasScalingMode(QgsLayoutItemMap.Fixed) + self.atlas.setHideCoverage(False) + + self.atlas.setSortFeatures(False) + + self.atlas.setFilterFeatures(True) + self.atlas.setFeatureFilter("substr(NAME_1,1,1)='P'") # select only 'Pays de la loire' + + self.atlas.beginRender() + + for i in range(0, 1): + self.atlas.seekTo(i) + self.mLabel1.adjustSizeToText() + + checker = QgsLayoutChecker('atlas_filtering%d' % (i + 1), self.layout) + checker.setControlPathPrefix("atlas") + myTestResult, myMessage = checker.testLayout(0, 200) + self.report += checker.report() + + self.assertTrue(myTestResult, myMessage) + self.atlas.endRender() + + def legend_test(self): + self.atlas_map.setAtlasDriven(True) + self.atlas_map.setAtlasScalingMode(QgsLayoutItemMap.Auto) + self.atlas_map.setAtlasMargin(0.10) + + # add a point layer + ptLayer = QgsVectorLayer("Point?crs=epsg:4326&field=attr:int(1)&field=label:string(20)", "points", "memory") + + pr = ptLayer.dataProvider() + f1 = QgsFeature(1) + f1.initAttributes(2) + f1.setAttribute(0, 1) + f1.setAttribute(1, "Test label 1") + f1.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(-0.638, 48.954))) + f2 = QgsFeature(2) + f2.initAttributes(2) + f2.setAttribute(0, 2) + f2.setAttribute(1, "Test label 2") + f2.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(-1.682, 48.550))) + pr.addFeatures([f1, f2]) + + # categorized symbology + r = QgsCategorizedSymbolRenderer("attr", [QgsRendererCategory(1, QgsMarkerSymbol.createSimple({"color": "255,0,0"}), "red"), + QgsRendererCategory(2, QgsMarkerSymbol.createSimple({"color": "0,0,255"}), "blue")]) + ptLayer.setRenderer(r) + + QgsProject.instance().addMapLayer(ptLayer) + + # add the point layer to the map settings + layers = self.layers + layers = [ptLayer] + layers + self.atlas_map.setLayers(layers) + self.overview.setLayers(layers) + + # add a legend + legend = QgsLayoutItemLegend(self.layout) + legend.attemptMove(QgsLayoutPoint(200, 100)) + # sets the legend filter parameter + legend.setMap(self.atlas_map) + legend.setLegendFilterOutAtlas(True) + self.layout.addLayoutItem(legend) + + self.atlas.beginRender() + + self.atlas.seekTo(0) + self.mLabel1.adjustSizeToText() + + checker = QgsLayoutChecker('atlas_legend', self.layout) + myTestResult, myMessage = checker.testLayout() + self.report += checker.report() + self.assertTrue(myTestResult, myMessage) + + self.atlas.endRender() + + # restore state + self.atlas_map.setLayers([layers[1]]) + self.layout.removeLayoutItem(legend) + QgsProject.instance().removeMapLayer(ptLayer.id()) + + def rotation_test(self): + # We will create a polygon layer with a rotated rectangle. + # Then we will make it the object layer for the atlas, + # rotate the map and test that the bounding rectangle + # is smaller than the bounds without rotation. + polygonLayer = QgsVectorLayer('Polygon', 'test_polygon', 'memory') + poly = QgsFeature(polygonLayer.pendingFields()) + points = [(10, 15), (15, 10), (45, 40), (40, 45)] + poly.setGeometry(QgsGeometry.fromPolygonXY([[QgsPointXY(x[0], x[1]) for x in points]])) + polygonLayer.dataProvider().addFeatures([poly]) + QgsProject.instance().addMapLayer(polygonLayer) + + # Recreating the composer locally + composition = QgsPrintLayout(QgsProject.instance()) + composition.initializeDefaults() + + # the atlas map + atlasMap = QgsLayoutItemMap(composition) + atlasMap.attemptSetSceneRect(QRectF(20, 20, 130, 130)) + atlasMap.setFrameEnabled(True) + atlasMap.setLayers([polygonLayer]) + atlasMap.setExtent(QgsRectangle(0, 0, 100, 50)) + composition.addLayoutItem(atlasMap) + + # the atlas + atlas = composition.atlas() + atlas.setCoverageLayer(polygonLayer) + atlas.setEnabled(True) + + atlasMap.setAtlasDriven(True) + atlasMap.setAtlasScalingMode(QgsLayoutItemMap.Auto) + atlasMap.setAtlasMargin(0.0) + + # Testing + atlasMap.setMapRotation(0.0) + atlas.beginRender() + atlas.first() + nonRotatedExtent = QgsRectangle(atlasMap.extent()) + + atlasMap.setMapRotation(45.0) + atlas.first() + rotatedExtent = QgsRectangle(atlasMap.extent()) + + self.assertLess(rotatedExtent.width(), nonRotatedExtent.width() * 0.9) + self.assertLess(rotatedExtent.height(), nonRotatedExtent.height() * 0.9) + + QgsProject.instance().removeMapLayer(polygonLayer) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgslayoutlabel.py b/tests/src/python/test_qgslayoutlabel.py index f051f420db48..efda088a4127 100644 --- a/tests/src/python/test_qgslayoutlabel.py +++ b/tests/src/python/test_qgslayoutlabel.py @@ -16,7 +16,13 @@ from qgis.testing import start_app, unittest from qgis.PyQt.QtCore import QFileInfo, QDate, QDateTime -from qgis.core import QgsVectorLayer, QgsLayout, QgsLayoutItemLabel, QgsProject +from qgis.core import (QgsVectorLayer, + QgsPrintLayout, + QgsLayout, + QgsLayoutItemLabel, + QgsProject, + QgsLayoutItemPage, + QgsLayoutPoint) from utilities import unitTestDataPath from test_qgslayoutitem import LayoutItemTestCase @@ -37,7 +43,7 @@ def testCase(self): QgsProject.instance().addMapLayers([mVectorLayer]) - layout = QgsLayout(QgsProject.instance()) + layout = QgsPrintLayout(QgsProject.instance()) layout.initializeDefaults() label = QgsLayoutItemLabel(layout) @@ -68,31 +74,28 @@ def evaluation_test(self, layout, label): assert label.currentText() == "__[NAME_1]42__" def feature_evaluation_test(self, layout, label, mVectorLayer): - pass - # TODO - #atlas = layout.atlasComposition() - #atlas.setCoverageLayer(mVectorLayer) - #atlas.setEnabled(True) - #layout.setAtlasMode(QgsComposition.ExportAtlas) + atlas = layout.atlas() + atlas.setCoverageLayer(mVectorLayer) + atlas.setEnabled(True) - #label.setText("[%\"NAME_1\"||'_ok'%]") - #atlas.beginRender() - #atlas.prepareForFeature(0) - #assert label.currentText() == "Basse-Normandie_ok" + label.setText("[%\"NAME_1\"||'_ok'%]") + atlas.beginRender() + atlas.seekTo(0) + assert label.currentText() == "Basse-Normandie_ok" - #atlas.prepareForFeature(1) - #assert label.currentText() == "Bretagne_ok" + atlas.seekTo(1) + assert label.currentText() == "Bretagne_ok" def page_evaluation_test(self, layout, label, mVectorLayer): - pass - # TODO - #layout.setNumPages(2) - #label.setText("[%@layout_page||'/'||@layout_numpages%]") - #assert label.currentText() == "1/2" + page = QgsLayoutItemPage(layout) + page.setPageSize('A4') + layout.pageCollection().addPage(page) + label.setText("[%@layout_page||'/'||@layout_numpages%]") + assert label.currentText() == "1/2" # move the the second page and re-evaluate - #label.setItemPosition(0, 320) - #assert label.currentText() == "2/2" + label.attemptMove(QgsLayoutPoint(0, 320)) + assert label.currentText() == "2/2" if __name__ == '__main__': From 8de8bb387f2969ea3e2132bb16cf7bfe87acfaba Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 28 Dec 2017 13:15:05 +1000 Subject: [PATCH 045/105] Try (and fail) to avoid a qApp->processEvents() call I just can't find any other reliable way to wait until javascript execution in a web page has completed. --- python/core/layout/qgslayoutitemhtml.sip | 1 + src/core/layout/qgslayoutitemhtml.cpp | 31 +++++++++++++++++++++--- src/core/layout/qgslayoutitemhtml.h | 18 ++++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/python/core/layout/qgslayoutitemhtml.sip b/python/core/layout/qgslayoutitemhtml.sip index 8894a279389c..908912fe61e4 100644 --- a/python/core/layout/qgslayoutitemhtml.sip +++ b/python/core/layout/qgslayoutitemhtml.sip @@ -261,6 +261,7 @@ Recalculates the frame sizes for the current viewport dimensions }; + /************************************************************************ * This file has been generated automatically from * * * diff --git a/src/core/layout/qgslayoutitemhtml.cpp b/src/core/layout/qgslayoutitemhtml.cpp index 5cca26b26294..1b54d90fb57f 100644 --- a/src/core/layout/qgslayoutitemhtml.cpp +++ b/src/core/layout/qgslayoutitemhtml.cpp @@ -197,9 +197,12 @@ void QgsLayoutItemHtml::loadHtml( const bool useCache, const QgsExpressionContex //inject JSON feature if ( !mAtlasFeatureJSON.isEmpty() ) { - mWebPage->mainFrame()->evaluateJavaScript( QStringLiteral( "if ( typeof setFeature === \"function\" ) { setFeature(%1); }" ).arg( mAtlasFeatureJSON ) ); - //needs an extra process events here to give JavaScript a chance to execute - qApp->processEvents(); + JavascriptExecutorLoop jsLoop; + + mWebPage->mainFrame()->addToJavaScriptWindowObject( "loop", &jsLoop ); + mWebPage->mainFrame()->evaluateJavaScript( QStringLiteral( "if ( typeof setFeature === \"function\" ) { setFeature(%1); }; loop.done();" ).arg( mAtlasFeatureJSON ) ); + + jsLoop.execIfNotDone(); } recalculateFrameSizes(); @@ -533,3 +536,25 @@ void QgsLayoutItemHtml::refreshDataDefinedProperty( const QgsLayoutObject::DataD loadHtml( true, &context ); } } + +//JavascriptExecutorLoop +///@cond PRIVATE + +void JavascriptExecutorLoop::done() +{ + mDone = true; + quit(); +} + +void JavascriptExecutorLoop::execIfNotDone() +{ + if ( !mDone ) + exec( QEventLoop::ExcludeUserInputEvents ); + + // gross, but nothing else works, so f*** it.. it's not worth spending a day trying to find a non-hacky way + // to force the web page to update following the js execution + for ( int i = 0; i < 100; i++ ) + qApp->processEvents(); +} + +///@endcond diff --git a/src/core/layout/qgslayoutitemhtml.h b/src/core/layout/qgslayoutitemhtml.h index dceb0746bb6e..51b00211a17d 100644 --- a/src/core/layout/qgslayoutitemhtml.h +++ b/src/core/layout/qgslayoutitemhtml.h @@ -276,4 +276,22 @@ class CORE_EXPORT QgsLayoutItemHtml: public QgsLayoutMultiFrame void refreshExpressionContext(); }; +///@cond PRIVATE +#ifndef SIP_RUN +class JavascriptExecutorLoop : public QEventLoop +{ + Q_OBJECT + public slots: + + void done(); + void execIfNotDone(); + + private: + + bool mDone = false; + +}; +#endif +///@endcond + #endif // QGSLAYOUTITEMHTML_H From be7dae7d520b21550359da2cc1ad839bf234dc7d Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 28 Dec 2017 14:25:25 +1000 Subject: [PATCH 046/105] Expand exporter tests --- tests/src/python/test_qgslayoutexporter.py | 154 +++++++++++++++++- ...pected_layoutexporter_iteratortoimage1.png | Bin 0 -> 22804 bytes ...d_layoutexporter_iteratortoimage1_mask.png | Bin 0 -> 19133 bytes ...pected_layoutexporter_iteratortoimage2.png | Bin 0 -> 20823 bytes ...d_layoutexporter_iteratortoimage2_mask.png | Bin 0 -> 18161 bytes 5 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 tests/testdata/control_images/layout_exporter/expected_layoutexporter_iteratortoimage1/expected_layoutexporter_iteratortoimage1.png create mode 100644 tests/testdata/control_images/layout_exporter/expected_layoutexporter_iteratortoimage1/expected_layoutexporter_iteratortoimage1_mask.png create mode 100644 tests/testdata/control_images/layout_exporter/expected_layoutexporter_iteratortoimage2/expected_layoutexporter_iteratortoimage2.png create mode 100644 tests/testdata/control_images/layout_exporter/expected_layoutexporter_iteratortoimage2/expected_layoutexporter_iteratortoimage2_mask.png diff --git a/tests/src/python/test_qgslayoutexporter.py b/tests/src/python/test_qgslayoutexporter.py index f866c3ca8add..b62584beff38 100644 --- a/tests/src/python/test_qgslayoutexporter.py +++ b/tests/src/python/test_qgslayoutexporter.py @@ -33,14 +33,20 @@ QgsLayoutMeasurement, QgsUnitTypes, QgsSimpleFillSymbolLayer, - QgsFillSymbol) + QgsFillSymbol, + QgsVectorLayer, + QgsCoordinateReferenceSystem, + QgsPrintLayout, + QgsSingleSymbolRenderer) from qgis.PyQt.QtCore import QSize, QSizeF, QDir, QRectF, Qt from qgis.PyQt.QtGui import QImage, QPainter from qgis.PyQt.QtSvg import QSvgRenderer, QSvgGenerator from qgis.testing import start_app, unittest -from utilities import getExecutablePath +from utilities import getExecutablePath, unitTestDataPath + +TEST_DATA_DIR = unitTestDataPath() # PDF-to-image utility # look for Poppler w/ Cairo, then muPDF @@ -576,6 +582,150 @@ def testPageFileName(self): details.page = 2 self.assertEqual(exporter.generateFileName(details), '/tmp/output/my_maps_3.png') + def prepareIteratorLayout(self): + layer_path = os.path.join(TEST_DATA_DIR, 'france_parts.shp') + layer = QgsVectorLayer(layer_path, 'test', "ogr") + + project=QgsProject() + project.addMapLayers([layer]) + # select epsg:2154 + crs = QgsCoordinateReferenceSystem() + crs.createFromSrid(2154) + project.setCrs(crs) + + layout = QgsPrintLayout(project) + layout.initializeDefaults() + + # fix the renderer, fill with green + props = {"color": "0,127,0", "outline_width":"4", "outline_color":'255,255,255'} + fillSymbol = QgsFillSymbol.createSimple(props) + renderer = QgsSingleSymbolRenderer(fillSymbol) + layer.setRenderer(renderer) + + # the atlas map + atlas_map = QgsLayoutItemMap(layout) + atlas_map.attemptSetSceneRect(QRectF(20, 20, 130, 130)) + atlas_map.setFrameEnabled(True) + atlas_map.setLayers([layer]) + layout.addLayoutItem(atlas_map) + + # the atlas + atlas = layout.atlas() + atlas.setCoverageLayer(layer) + atlas.setEnabled(True) + + atlas_map.setExtent( + QgsRectangle(332719.06221504929, 6765214.5887386119, 560957.85090677091, 6993453.3774303338)) + + atlas_map.setAtlasDriven(True) + atlas_map.setAtlasScalingMode(QgsLayoutItemMap.Auto) + atlas_map.setAtlasMargin(0.10) + + return project, layout + + def testIteratorToImages(self): + project, layout = self.prepareIteratorLayout() + atlas = layout.atlas() + atlas.setFilenameExpression("'test_exportiteratortoimage_' || \"NAME_1\"") + + # setup settings + settings = QgsLayoutExporter.ImageExportSettings() + settings.dpi = 80 + + result, error = QgsLayoutExporter.exportToImage(atlas,self.basetestpath, 'png', settings) + self.assertEqual(result, QgsLayoutExporter.Success, error) + + page1_path = os.path.join(self.basetestpath, 'test_exportiteratortoimage_Basse-Normandie.png') + self.assertTrue(self.checkImage('iteratortoimage1', 'iteratortoimage1', page1_path)) + page2_path = os.path.join(self.basetestpath, 'test_exportiteratortoimage_Bretagne.png') + self.assertTrue(self.checkImage('iteratortoimage2', 'iteratortoimage2', page2_path)) + page3_path = os.path.join(self.basetestpath, 'test_exportiteratortoimage_Centre.png') + self.assertTrue(os.path.exists(page3_path)) + page4_path = os.path.join(self.basetestpath, 'test_exportiteratortoimage_Pays de la Loire.png') + self.assertTrue(os.path.exists(page4_path)) + + def testIteratorToSvgs(self): + project, layout = self.prepareIteratorLayout() + atlas = layout.atlas() + atlas.setFilenameExpression("'test_exportiteratortosvg_' || \"NAME_1\"") + + # setup settings + settings = QgsLayoutExporter.SvgExportSettings() + settings.dpi = 80 + settings.forceVectorOutput = False + + result, error = QgsLayoutExporter.exportToSvg(atlas,self.basetestpath, settings) + self.assertEqual(result, QgsLayoutExporter.Success, error) + + page1_path = os.path.join(self.basetestpath, 'test_exportiteratortosvg_Basse-Normandie.svg') + rendered_page_1 = os.path.join(self.basetestpath, 'test_exportiteratortosvg_Basse-Normandie.png') + svgToPng(page1_path, rendered_page_1, width=935) + self.assertTrue(self.checkImage('iteratortosvg1', 'iteratortoimage1', rendered_page_1, size_tolerance=2)) + page2_path = os.path.join(self.basetestpath, 'test_exportiteratortosvg_Bretagne.svg') + rendered_page_2 = os.path.join(self.basetestpath, 'test_exportiteratortosvg_Bretagne.png') + svgToPng(page2_path, rendered_page_2, width=935) + self.assertTrue(self.checkImage('iteratortosvg2', 'iteratortoimage2', rendered_page_2, size_tolerance=2)) + page3_path = os.path.join(self.basetestpath, 'test_exportiteratortosvg_Centre.svg') + self.assertTrue(os.path.exists(page3_path)) + page4_path = os.path.join(self.basetestpath, 'test_exportiteratortosvg_Pays de la Loire.svg') + self.assertTrue(os.path.exists(page4_path)) + + def testIteratorToPdfs(self): + project, layout = self.prepareIteratorLayout() + atlas = layout.atlas() + atlas.setFilenameExpression("'test_exportiteratortopdf_' || \"NAME_1\"") + + # setup settings + settings = QgsLayoutExporter.PdfExportSettings() + settings.dpi = 80 + settings.rasterizeWholeImage = False + settings.forceVectorOutput = False + + result, error = QgsLayoutExporter.exportToPdfs(atlas,self.basetestpath, settings) + self.assertEqual(result, QgsLayoutExporter.Success, error) + + page1_path = os.path.join(self.basetestpath, 'test_exportiteratortopdf_Basse-Normandie.pdf') + rendered_page_1 = os.path.join(self.basetestpath, 'test_exportiteratortopdf_Basse-Normandie.png') + pdfToPng(page1_path, rendered_page_1, dpi=80, page=1) + self.assertTrue(self.checkImage('iteratortopdf1', 'iteratortoimage1', rendered_page_1, size_tolerance=2)) + page2_path = os.path.join(self.basetestpath, 'test_exportiteratortopdf_Bretagne.pdf') + rendered_page_2 = os.path.join(self.basetestpath, 'test_exportiteratortopdf_Bretagne.png') + pdfToPng(page2_path, rendered_page_2, dpi=80, page=1) + self.assertTrue(self.checkImage('iteratortopdf2', 'iteratortoimage2', rendered_page_2, size_tolerance=2)) + page3_path = os.path.join(self.basetestpath, 'test_exportiteratortopdf_Centre.pdf') + self.assertTrue(os.path.exists(page3_path)) + page4_path = os.path.join(self.basetestpath, 'test_exportiteratortopdf_Pays de la Loire.pdf') + self.assertTrue(os.path.exists(page4_path)) + + def testIteratorToPdf(self): + project, layout = self.prepareIteratorLayout() + atlas = layout.atlas() + + # setup settings + settings = QgsLayoutExporter.PdfExportSettings() + settings.dpi = 80 + settings.rasterizeWholeImage = False + settings.forceVectorOutput = False + + pdf_path = os.path.join(self.basetestpath, 'test_exportiteratortopdf_single.pdf') + result, error = QgsLayoutExporter.exportToPdf(atlas,pdf_path, settings) + self.assertEqual(result, QgsLayoutExporter.Success, error) + + rendered_page_1 = os.path.join(self.basetestpath, 'test_exportiteratortopdf_single1.png') + pdfToPng(pdf_path, rendered_page_1, dpi=80, page=1) + self.assertTrue(self.checkImage('iteratortopdfsingle1', 'iteratortoimage1', rendered_page_1, size_tolerance=2)) + + rendered_page_2 = os.path.join(self.basetestpath, 'test_exportiteratortopdf_single2.png') + pdfToPng(pdf_path, rendered_page_2, dpi=80, page=2) + self.assertTrue(self.checkImage('iteratortopdfsingle2', 'iteratortoimage2', rendered_page_2, size_tolerance=2)) + + rendered_page_3 = os.path.join(self.basetestpath, 'test_exportiteratortopdf_single3.png') + pdfToPng(pdf_path, rendered_page_3, dpi=80, page=3) + self.assertTrue(os.path.exists(rendered_page_3)) + rendered_page_4 = os.path.join(self.basetestpath, 'test_exportiteratortopdf_single4.png') + pdfToPng(pdf_path, rendered_page_4, dpi=80, page=4) + self.assertTrue(os.path.exists(rendered_page_4)) + if __name__ == '__main__': unittest.main() diff --git a/tests/testdata/control_images/layout_exporter/expected_layoutexporter_iteratortoimage1/expected_layoutexporter_iteratortoimage1.png b/tests/testdata/control_images/layout_exporter/expected_layoutexporter_iteratortoimage1/expected_layoutexporter_iteratortoimage1.png new file mode 100644 index 0000000000000000000000000000000000000000..16092fef13f63c7b7302f60bea90a5fc85dc81c8 GIT binary patch literal 22804 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

sodxinU5d3Z=b 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 Date: Thu, 28 Dec 2017 14:25:31 +1000 Subject: [PATCH 047/105] Ensure that every iteration through a layout iterator only references the current iteration layout --- src/core/layout/qgslayoutexporter.cpp | 64 +++++++++++++++------------ 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index 5820ffd87730..00ec7c0aef72 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -398,7 +398,6 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( QgsAbstractLayoutIterator *iterator, const QString &baseFilePath, const QString &extension, const QgsLayoutExporter::ImageExportSettings &settings, QString &error, QgsFeedback *feedback ) { - QgsLayoutExporter exporter( iterator->layout() ); error.clear(); if ( !iterator->beginRender() ) @@ -420,6 +419,7 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( QgsAbstractLay return Canceled; } + QgsLayoutExporter exporter( iterator->layout() ); QString filePath = iterator->filePath( baseFilePath, extension ); ExportResult result = exporter.exportToImage( filePath, settings ); if ( result != Success ) @@ -489,37 +489,13 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( QgsAbstractLayou { error.clear(); - if ( !iterator->layout() || !iterator->beginRender() ) + if ( !iterator->beginRender() ) return IteratorError; PdfExportSettings settings = s; - if ( settings.dpi <= 0 ) - settings.dpi = iterator->layout()->context().dpi(); - - LayoutContextPreviewSettingRestorer restorer( iterator->layout() ); - ( void )restorer; - LayoutContextSettingsRestorer contextRestorer( iterator->layout() ); - ( void )contextRestorer; - iterator->layout()->context().setDpi( settings.dpi ); - - // If we are not printing as raster, temporarily disable advanced effects - // as QPrinter does not support composition modes and can result - // in items missing from the output - iterator->layout()->context().setFlag( QgsLayoutContext::FlagUseAdvancedEffects, !settings.forceVectorOutput ); - - iterator->layout()->context().setFlag( QgsLayoutContext::FlagForceVectorOutput, settings.forceVectorOutput ); QPrinter printer; - preparePrintAsPdf( iterator->layout(), printer, fileName ); - preparePrint( iterator->layout(), printer, false ); QPainter p; - if ( !p.begin( &printer ) ) - { - //error beginning print - return PrintError; - } - - QgsLayoutExporter exporter( iterator->layout() ); int total = iterator->count(); double step = total > 0 ? 100.0 / total : 100.0; @@ -538,6 +514,36 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( QgsAbstractLayou return Canceled; } + if ( s.dpi <= 0 ) + settings.dpi = iterator->layout()->context().dpi(); + + LayoutContextPreviewSettingRestorer restorer( iterator->layout() ); + ( void )restorer; + LayoutContextSettingsRestorer contextRestorer( iterator->layout() ); + ( void )contextRestorer; + iterator->layout()->context().setDpi( settings.dpi ); + + // If we are not printing as raster, temporarily disable advanced effects + // as QPrinter does not support composition modes and can result + // in items missing from the output + iterator->layout()->context().setFlag( QgsLayoutContext::FlagUseAdvancedEffects, !settings.forceVectorOutput ); + + iterator->layout()->context().setFlag( QgsLayoutContext::FlagForceVectorOutput, settings.forceVectorOutput ); + + if ( first ) + { + preparePrintAsPdf( iterator->layout(), printer, fileName ); + preparePrint( iterator->layout(), printer, false ); + + if ( !p.begin( &printer ) ) + { + //error beginning print + return PrintError; + } + } + + QgsLayoutExporter exporter( iterator->layout() ); + ExportResult result = exporter.printPrivate( printer, p, !first, settings.dpi, settings.rasterizeWholeImage ); if ( result != Success ) { @@ -561,7 +567,6 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( QgsAbstractLayou QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdfs( QgsAbstractLayoutIterator *iterator, const QString &baseFilePath, const QgsLayoutExporter::PdfExportSettings &settings, QString &error, QgsFeedback *feedback ) { - QgsLayoutExporter exporter( iterator->layout() ); error.clear(); if ( !iterator->beginRender() ) @@ -584,6 +589,8 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdfs( QgsAbstractLayo } QString filePath = iterator->filePath( baseFilePath, QStringLiteral( "pdf" ) ); + + QgsLayoutExporter exporter( iterator->layout() ); ExportResult result = exporter.exportToPdf( filePath, settings ); if ( result != Success ) { @@ -773,7 +780,6 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &f QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( QgsAbstractLayoutIterator *iterator, const QString &baseFilePath, const QgsLayoutExporter::SvgExportSettings &settings, QString &error, QgsFeedback *feedback ) { - QgsLayoutExporter exporter( iterator->layout() ); error.clear(); if ( !iterator->beginRender() ) @@ -796,6 +802,8 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( QgsAbstractLayou } QString filePath = iterator->filePath( baseFilePath, QStringLiteral( "svg" ) ); + + QgsLayoutExporter exporter( iterator->layout() ); ExportResult result = exporter.exportToSvg( filePath, settings ); if ( result != Success ) { From 5bc543af6afaed1debe3471651ac1297694b7d70 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 28 Dec 2017 15:10:47 +1000 Subject: [PATCH 048/105] Refactor layout context Split render context from reporting context --- python/core/core_auto.sip | 3 +- python/core/layout/qgslayout.sip | 13 +- python/core/layout/qgslayoutexporter.sip | 6 +- ...context.sip => qgslayoutrendercontext.sip} | 100 ++--------- python/core/layout/qgslayoutreportcontext.sip | 117 ++++++++++++ src/app/layout/qgslayoutaddpagesdialog.cpp | 2 +- .../layout/qgslayoutattributetablewidget.cpp | 2 +- src/app/layout/qgslayoutdesignerdialog.cpp | 24 +-- src/app/layout/qgslayoutlegendwidget.cpp | 4 +- src/app/layout/qgslayoutmapgridwidget.cpp | 4 +- src/app/layout/qgslayoutmapwidget.cpp | 8 +- .../layout/qgslayoutpagepropertieswidget.cpp | 4 +- src/app/layout/qgslayoutpolygonwidget.cpp | 2 +- src/app/layout/qgslayoutpolylinewidget.cpp | 2 +- src/app/layout/qgslayoutpropertieswidget.cpp | 6 +- src/app/layout/qgslayoutshapewidget.cpp | 4 +- src/core/CMakeLists.txt | 6 +- src/core/layout/qgslayout.cpp | 37 ++-- src/core/layout/qgslayout.h | 30 +++- src/core/layout/qgslayoutatlas.cpp | 12 +- src/core/layout/qgslayoutcontext.cpp | 169 ------------------ src/core/layout/qgslayoutexporter.cpp | 66 +++---- src/core/layout/qgslayoutexporter.h | 15 +- src/core/layout/qgslayoutitem.cpp | 20 +-- .../layout/qgslayoutitemattributetable.cpp | 12 +- src/core/layout/qgslayoutitemhtml.cpp | 10 +- src/core/layout/qgslayoutitemlabel.cpp | 4 +- src/core/layout/qgslayoutitemlegend.cpp | 8 +- src/core/layout/qgslayoutitemmap.cpp | 31 ++-- src/core/layout/qgslayoutitemnodeitem.cpp | 2 +- src/core/layout/qgslayoutitempage.cpp | 10 +- src/core/layout/qgslayoutitempicture.cpp | 23 +-- src/core/layout/qgslayoutitempolygon.cpp | 4 +- src/core/layout/qgslayoutitempolyline.cpp | 4 +- src/core/layout/qgslayoutitemscalebar.cpp | 2 +- src/core/layout/qgslayoutitemshape.cpp | 4 +- src/core/layout/qgslayoutobject.cpp | 5 +- src/core/layout/qgslayoutrendercontext.cpp | 111 ++++++++++++ ...youtcontext.h => qgslayoutrendercontext.h} | 114 ++---------- src/core/layout/qgslayoutreportcontext.cpp | 82 +++++++++ src/core/layout/qgslayoutreportcontext.h | 135 ++++++++++++++ src/core/layout/qgslayouttable.cpp | 2 +- src/core/layout/qgslayoututils.cpp | 4 +- src/core/qgsapplication.cpp | 4 +- src/core/qgsexpressioncontext.cpp | 17 +- src/gui/layout/qgslayoutitemwidget.cpp | 10 +- src/gui/layout/qgslayoutmousehandles.cpp | 6 +- .../qgslayoutnewitempropertiesdialog.cpp | 4 +- tests/src/core/testqgslayout.cpp | 6 +- tests/src/core/testqgslayoutatlas.cpp | 4 +- tests/src/core/testqgslayoutcontext.cpp | 53 +++--- tests/src/core/testqgslayouthtml.cpp | 4 +- tests/src/core/testqgslayoutitem.cpp | 38 ++-- tests/src/core/testqgslayoutlabel.cpp | 10 +- tests/src/core/testqgslayoutmap.cpp | 14 +- tests/src/core/testqgslayoutpage.cpp | 2 +- tests/src/core/testqgslayouttable.cpp | 14 +- tests/src/core/testqgslayoututils.cpp | 12 +- tests/src/python/test_qgslayoutatlas.py | 32 ++-- tests/src/python/test_qgslayoutexporter.py | 14 +- 60 files changed, 813 insertions(+), 654 deletions(-) rename python/core/layout/{qgslayoutcontext.sip => qgslayoutrendercontext.sip} (65%) create mode 100644 python/core/layout/qgslayoutreportcontext.sip delete mode 100644 src/core/layout/qgslayoutcontext.cpp create mode 100644 src/core/layout/qgslayoutrendercontext.cpp rename src/core/layout/{qgslayoutcontext.h => qgslayoutrendercontext.h} (68%) create mode 100644 src/core/layout/qgslayoutreportcontext.cpp create mode 100644 src/core/layout/qgslayoutreportcontext.h diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index fc581abc92c2..79bf38c199d8 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -408,7 +408,6 @@ %Include gps/qgsgpsdconnection.sip %Include layout/qgslayout.sip %Include layout/qgslayoutatlas.sip -%Include layout/qgslayoutcontext.sip %Include layout/qgslayouteffect.sip %Include layout/qgslayoutguidecollection.sip %Include layout/qgslayoutframe.sip @@ -435,6 +434,8 @@ %Include layout/qgslayoutmultiframe.sip %Include layout/qgslayoutpagecollection.sip %Include layout/qgslayoutobject.sip +%Include layout/qgslayoutrendercontext.sip +%Include layout/qgslayoutreportcontext.sip %Include layout/qgslayouttable.sip %Include layout/qgslayouttablecolumn.sip %Include layout/qgslayoutundostack.sip diff --git a/python/core/layout/qgslayout.sip b/python/core/layout/qgslayout.sip index 6a02033134d7..9d4678b48fa1 100644 --- a/python/core/layout/qgslayout.sip +++ b/python/core/layout/qgslayout.sip @@ -309,10 +309,17 @@ Converts a ``point`` from the layout's native units to a specified target ``unit .. seealso:: :py:func:`units()` %End - QgsLayoutContext &context(); + QgsLayoutRenderContext &renderContext(); %Docstring -Returns a reference to the layout's context, which stores information relating to the -current context and rendering settings for the layout. +Returns a reference to the layout's render context, which stores information relating to the +current rendering settings for the layout. +%End + + + QgsLayoutReportContext &reportContext(); +%Docstring +Returns a reference to the layout's report context, which stores information relating to the +current reporting context for the layout. %End diff --git a/python/core/layout/qgslayoutexporter.sip b/python/core/layout/qgslayoutexporter.sip index 00481c0f1314..cd4cadd3ce29 100644 --- a/python/core/layout/qgslayoutexporter.sip +++ b/python/core/layout/qgslayoutexporter.sip @@ -181,7 +181,7 @@ Set to true to generate an external world file alongside exported images. %End - QgsLayoutContext::Flags flags; + QgsLayoutRenderContext::Flags flags; %Docstring Layout context flags, which control how the export will be created. %End @@ -244,7 +244,7 @@ correct appearance in the output. This option is mutually exclusive with rasterizeWholeImage. %End - QgsLayoutContext::Flags flags; + QgsLayoutRenderContext::Flags flags; %Docstring Layout context flags, which control how the export will be created. %End @@ -330,7 +330,7 @@ Note that this option is considered experimental, and the generated SVG may differ from the expected appearance of the layout. %End - QgsLayoutContext::Flags flags; + QgsLayoutRenderContext::Flags flags; %Docstring Layout context flags, which control how the export will be created. %End diff --git a/python/core/layout/qgslayoutcontext.sip b/python/core/layout/qgslayoutrendercontext.sip similarity index 65% rename from python/core/layout/qgslayoutcontext.sip rename to python/core/layout/qgslayoutrendercontext.sip index bf5e23e3bf04..d17a674f2739 100644 --- a/python/core/layout/qgslayoutcontext.sip +++ b/python/core/layout/qgslayoutrendercontext.sip @@ -1,23 +1,23 @@ /************************************************************************ * This file has been generated automatically from * * * - * src/core/layout/qgslayoutcontext.h * + * src/core/layout/qgslayoutrendercontext.h * * * * Do not edit manually ! Edit header and run scripts/sipify.pl again * ************************************************************************/ -class QgsLayoutContext : QObject +class QgsLayoutRenderContext : QObject { %Docstring - Stores information relating to the current context and rendering settings for a layout. + Stores information relating to the current rendering settings for a layout. .. versionadded:: 3.0 %End %TypeHeaderCode -#include "qgslayoutcontext.h" +#include "qgslayoutrendercontext.h" %End public: @@ -30,15 +30,15 @@ class QgsLayoutContext : QObject FlagForceVectorOutput, FlagHideCoverageLayer, }; - typedef QFlags Flags; + typedef QFlags Flags; - QgsLayoutContext( QgsLayout *layout /TransferThis/ ); + QgsLayoutRenderContext( QgsLayout *layout /TransferThis/ ); %Docstring -Constructor for QgsLayoutContext. +Constructor for QgsLayoutRenderContext. %End - void setFlags( const QgsLayoutContext::Flags flags ); + void setFlags( const QgsLayoutRenderContext::Flags flags ); %Docstring Sets the combination of ``flags`` that will be used for rendering the layout. @@ -49,7 +49,7 @@ Sets the combination of ``flags`` that will be used for rendering the layout. .. seealso:: :py:func:`testFlag()` %End - void setFlag( const QgsLayoutContext::Flag flag, const bool on = true ); + void setFlag( const QgsLayoutRenderContext::Flag flag, const bool on = true ); %Docstring Enables or disables a particular rendering ``flag`` for the layout. Other existing flags are not affected. @@ -61,7 +61,7 @@ flags are not affected. .. seealso:: :py:func:`testFlag()` %End - QgsLayoutContext::Flags flags() const; + QgsLayoutRenderContext::Flags flags() const; %Docstring Returns the current combination of flags used for rendering the layout. @@ -86,56 +86,6 @@ Check whether a particular rendering ``flag`` is enabled for the layout. QgsRenderContext::Flags renderContextFlags() const; %Docstring Returns the combination of render context flags matched to the layout context's settings. -%End - - void setFeature( const QgsFeature &feature ); -%Docstring -Sets the current ``feature`` for evaluating the layout. This feature may -be used for altering an item's content and appearance for a report -or atlas layout. - -Emits the changed() signal. - -.. seealso:: :py:func:`feature()` -%End - - QgsFeature feature() const; -%Docstring -Returns the current feature for evaluating the layout. This feature may -be used for altering an item's content and appearance for a report -or atlas layout. - -.. seealso:: :py:func:`currentGeometry()` - -.. seealso:: :py:func:`setFeature()` -%End - - QgsGeometry currentGeometry( const QgsCoordinateReferenceSystem &crs = QgsCoordinateReferenceSystem() ) const; -%Docstring -Returns the current feature() geometry in the given ``crs``. -If no CRS is specified, the original feature geometry is returned. - -Reprojection only works if a valid layer is set for layer(). - -.. seealso:: :py:func:`feature()` - -.. seealso:: :py:func:`layer()` -%End - - QgsVectorLayer *layer() const; -%Docstring -Returns the vector layer associated with the layout's context. - -.. seealso:: :py:func:`setLayer()` -%End - - void setLayer( QgsVectorLayer *layer ); -%Docstring -Sets the vector ``layer`` associated with the layout's context. - -Emits the changed() signal. - -.. seealso:: :py:func:`layer()` %End void setDpi( double dpi ); @@ -233,41 +183,15 @@ and customise their rendering based on the layer. If ``layer`` is -1, all item layers should be rendered. .. seealso:: :py:func:`setCurrentExportLayer()` -%End - - void setPredefinedScales( const QVector &scales ); -%Docstring -Sets the list of predefined ``scales`` to use with the layout. This is used -for maps which are set to the predefined atlas scaling mode. - -.. seealso:: :py:func:`predefinedScales()` -%End - - QVector predefinedScales() const; -%Docstring -Returns the current list of predefined scales for use with the layout. - -.. seealso:: :py:func:`setPredefinedScales()` %End signals: - void flagsChanged( QgsLayoutContext::Flags flags ); + void flagsChanged( QgsLayoutRenderContext::Flags flags ); %Docstring Emitted whenever the context's ``flags`` change. .. seealso:: :py:func:`setFlags()` -%End - - void layerChanged( QgsVectorLayer *layer ); -%Docstring -Emitted when the context's ``layer`` is changed. -%End - - void changed(); -%Docstring -Emitted certain settings in the context is changed, e.g. by setting a new feature or vector layer -for the context. %End void dpiChanged(); @@ -284,7 +208,7 @@ Emitted when the context's DPI is changed. /************************************************************************ * This file has been generated automatically from * * * - * src/core/layout/qgslayoutcontext.h * + * src/core/layout/qgslayoutrendercontext.h * * * * Do not edit manually ! Edit header and run scripts/sipify.pl again * ************************************************************************/ diff --git a/python/core/layout/qgslayoutreportcontext.sip b/python/core/layout/qgslayoutreportcontext.sip new file mode 100644 index 000000000000..2d315a102ae2 --- /dev/null +++ b/python/core/layout/qgslayoutreportcontext.sip @@ -0,0 +1,117 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layout/qgslayoutreportcontext.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + +class QgsLayoutReportContext : QObject +{ +%Docstring + Stores information relating to the current reporting context for a layout. + +.. versionadded:: 3.0 +%End + +%TypeHeaderCode +#include "qgslayoutreportcontext.h" +%End + public: + + QgsLayoutReportContext( QgsLayout *layout /TransferThis/ ); +%Docstring +Constructor for QgsLayoutReportContext. +%End + + void setFeature( const QgsFeature &feature ); +%Docstring +Sets the current ``feature`` for evaluating the layout. This feature may +be used for altering an item's content and appearance for a report +or atlas layout. + +Emits the changed() signal. + +.. seealso:: :py:func:`feature()` +%End + + QgsFeature feature() const; +%Docstring +Returns the current feature for evaluating the layout. This feature may +be used for altering an item's content and appearance for a report +or atlas layout. + +.. seealso:: :py:func:`currentGeometry()` + +.. seealso:: :py:func:`setFeature()` +%End + + QgsGeometry currentGeometry( const QgsCoordinateReferenceSystem &crs = QgsCoordinateReferenceSystem() ) const; +%Docstring +Returns the current feature() geometry in the given ``crs``. +If no CRS is specified, the original feature geometry is returned. + +Reprojection only works if a valid layer is set for layer(). + +.. seealso:: :py:func:`feature()` + +.. seealso:: :py:func:`layer()` +%End + + QgsVectorLayer *layer() const; +%Docstring +Returns the vector layer associated with the layout's context. + +.. seealso:: :py:func:`setLayer()` +%End + + void setLayer( QgsVectorLayer *layer ); +%Docstring +Sets the vector ``layer`` associated with the layout's context. + +Emits the changed() signal. + +.. seealso:: :py:func:`layer()` +%End + + void setPredefinedScales( const QVector &scales ); +%Docstring +Sets the list of predefined ``scales`` to use with the layout. This is used +for maps which are set to the predefined atlas scaling mode. + +.. seealso:: :py:func:`predefinedScales()` +%End + + QVector predefinedScales() const; +%Docstring +Returns the current list of predefined scales for use with the layout. + +.. seealso:: :py:func:`setPredefinedScales()` +%End + + signals: + + void layerChanged( QgsVectorLayer *layer ); +%Docstring +Emitted when the context's ``layer`` is changed. +%End + + void changed(); +%Docstring +Emitted certain settings in the context is changed, e.g. by setting a new feature or vector layer +for the context. +%End + +}; + + + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layout/qgslayoutreportcontext.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/src/app/layout/qgslayoutaddpagesdialog.cpp b/src/app/layout/qgslayoutaddpagesdialog.cpp index 73aa578006c3..f87fa7dfe240 100644 --- a/src/app/layout/qgslayoutaddpagesdialog.cpp +++ b/src/app/layout/qgslayoutaddpagesdialog.cpp @@ -56,7 +56,7 @@ QgsLayoutAddPagesDialog::QgsLayoutAddPagesDialog( QWidget *parent, Qt::WindowFla void QgsLayoutAddPagesDialog::setLayout( QgsLayout *layout ) { - mConverter = layout->context().measurementConverter(); + mConverter = layout->renderContext().measurementConverter(); mSizeUnitsComboBox->setConverter( &mConverter ); mExistingPageSpinBox->setMaximum( layout->pageCollection()->pageCount() ); mSizeUnitsComboBox->setUnit( layout->units() ); diff --git a/src/app/layout/qgslayoutattributetablewidget.cpp b/src/app/layout/qgslayoutattributetablewidget.cpp index e0a48a56cb9f..de82929402d4 100644 --- a/src/app/layout/qgslayoutattributetablewidget.cpp +++ b/src/app/layout/qgslayoutattributetablewidget.cpp @@ -124,7 +124,7 @@ QgsLayoutAttributeTableWidget::QgsLayoutAttributeTableWidget( QgsLayoutFrame *fr connect( mTable, &QgsLayoutMultiFrame::changed, this, &QgsLayoutAttributeTableWidget::updateGuiElements ); // repopulate relations combo box if atlas layer changes - connect( &mTable->layout()->context(), &QgsLayoutContext::layerChanged, + connect( &mTable->layout()->reportContext(), &QgsLayoutReportContext::layerChanged, this, &QgsLayoutAttributeTableWidget::updateRelationsCombo ); if ( QgsLayoutAtlas *atlas = layoutAtlas() ) diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 6c20ac0a40e8..4d4394ea81d1 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -722,13 +722,13 @@ void QgsLayoutDesignerDialog::setCurrentLayout( QgsLayout *layout ) connect( mLayout, &QgsLayout::nameChanged, this, &QgsLayoutDesignerDialog::setWindowTitle ); setWindowTitle( mLayout->name() ); - mActionShowGrid->setChecked( mLayout->context().gridVisible() ); + mActionShowGrid->setChecked( mLayout->renderContext().gridVisible() ); mActionSnapGrid->setChecked( mLayout->snapper().snapToGrid() ); mActionShowGuides->setChecked( mLayout->guides().visible() ); mActionSnapGuides->setChecked( mLayout->snapper().snapToGuides() ); mActionSmartGuides->setChecked( mLayout->snapper().snapToItems() ); - mActionShowBoxes->setChecked( mLayout->context().boundingBoxesVisible() ); - mActionShowPage->setChecked( mLayout->context().pagesVisible() ); + mActionShowBoxes->setChecked( mLayout->renderContext().boundingBoxesVisible() ); + mActionShowPage->setChecked( mLayout->renderContext().pagesVisible() ); mUndoView->setStack( mLayout->undoStack()->stack() ); @@ -850,19 +850,19 @@ void QgsLayoutDesignerDialog::showRulers( bool visible ) void QgsLayoutDesignerDialog::showGrid( bool visible ) { - mLayout->context().setGridVisible( visible ); + mLayout->renderContext().setGridVisible( visible ); mLayout->pageCollection()->redraw(); } void QgsLayoutDesignerDialog::showBoxes( bool visible ) { - mLayout->context().setBoundingBoxesVisible( visible ); + mLayout->renderContext().setBoundingBoxesVisible( visible ); mSelectTool->mouseHandles()->update(); } void QgsLayoutDesignerDialog::showPages( bool visible ) { - mLayout->context().setPagesVisible( visible ); + mLayout->renderContext().setPagesVisible( visible ); mLayout->pageCollection()->redraw(); } @@ -2793,8 +2793,8 @@ bool QgsLayoutDesignerDialog::showFileSizeWarning() // Image size double oneInchInLayoutUnits = mLayout->convertToLayoutUnits( QgsLayoutMeasurement( 1, QgsUnitTypes::LayoutInches ) ); QSizeF maxPageSize = mLayout->pageCollection()->maximumPageSize(); - int width = ( int )( mLayout->context().dpi() * maxPageSize.width() / oneInchInLayoutUnits ); - int height = ( int )( mLayout->context().dpi() * maxPageSize.height() / oneInchInLayoutUnits ); + int width = ( int )( mLayout->renderContext().dpi() * maxPageSize.width() / oneInchInLayoutUnits ); + int height = ( int )( mLayout->renderContext().dpi() * maxPageSize.height() / oneInchInLayoutUnits ); int memuse = width * height * 3 / 1000000; // pixmap + image QgsDebugMsg( QString( "Image %1x%2" ).arg( width ).arg( height ) ); QgsDebugMsg( QString( "memuse = %1" ).arg( memuse ) ); @@ -2818,7 +2818,7 @@ bool QgsLayoutDesignerDialog::getRasterExportSettings( QgsLayoutExporter::ImageE // Image size QSizeF maxPageSize = mLayout->pageCollection()->maximumPageSize(); bool hasUniformPageSizes = mLayout->pageCollection()->hasUniformPageSizes(); - double dpi = mLayout->context().dpi(); + double dpi = mLayout->renderContext().dpi(); //get some defaults from the composition bool cropToContents = mLayout->customProperty( QStringLiteral( "imageCropToContents" ), false ).toBool(); @@ -2858,9 +2858,9 @@ bool QgsLayoutDesignerDialog::getRasterExportSettings( QgsLayoutExporter::ImageE settings.imageSize = imageSize; } settings.generateWorldFile = imageDlg.generateWorldFile(); - settings.flags = QgsLayoutContext::FlagUseAdvancedEffects; + settings.flags = QgsLayoutRenderContext::FlagUseAdvancedEffects; if ( imageDlg.antialiasing() ) - settings.flags |= QgsLayoutContext::FlagAntialiasing; + settings.flags |= QgsLayoutRenderContext::FlagAntialiasing; return true; } @@ -3011,7 +3011,7 @@ void QgsLayoutDesignerDialog::loadAtlasPredefinedScalesFromProject() projectScales.push_back( parts[1].toDouble() ); } } - mLayout->context().setPredefinedScales( projectScales ); + mLayout->reportContext().setPredefinedScales( projectScales ); } QgsLayoutAtlas *QgsLayoutDesignerDialog::atlas() diff --git a/src/app/layout/qgslayoutlegendwidget.cpp b/src/app/layout/qgslayoutlegendwidget.cpp index b4649c88b1ad..d81c9477ad18 100644 --- a/src/app/layout/qgslayoutlegendwidget.cpp +++ b/src/app/layout/qgslayoutlegendwidget.cpp @@ -142,7 +142,7 @@ QgsLayoutLegendWidget::QgsLayoutLegendWidget( QgsLayoutItemLegend *legend ) { connect( layoutAtlas(), &QgsLayoutAtlas::toggled, this, &QgsLayoutLegendWidget::updateFilterLegendByAtlasButton ); } - connect( &legend->layout()->context(), &QgsLayoutContext::layerChanged, this, &QgsLayoutLegendWidget::updateFilterLegendByAtlasButton ); + connect( &legend->layout()->reportContext(), &QgsLayoutReportContext::layerChanged, this, &QgsLayoutLegendWidget::updateFilterLegendByAtlasButton ); registerDataDefinedButton( mLegendTitleDDBtn, QgsLayoutObject::LegendTitle ); registerDataDefinedButton( mColumnsDDBtn, QgsLayoutObject::LegendColumnCount ); @@ -1033,7 +1033,7 @@ void QgsLayoutLegendWidget::updateFilterLegendByAtlasButton() { if ( QgsLayoutAtlas *atlas = layoutAtlas() ) { - mFilterLegendByAtlasCheckBox->setEnabled( atlas->enabled() && mLegend->layout()->context().layer() && mLegend->layout()->context().layer()->geometryType() == QgsWkbTypes::PolygonGeometry ); + mFilterLegendByAtlasCheckBox->setEnabled( atlas->enabled() && mLegend->layout()->reportContext().layer() && mLegend->layout()->reportContext().layer()->geometryType() == QgsWkbTypes::PolygonGeometry ); } } diff --git a/src/app/layout/qgslayoutmapgridwidget.cpp b/src/app/layout/qgslayoutmapgridwidget.cpp index e2b09e119946..7717df6647da 100644 --- a/src/app/layout/qgslayoutmapgridwidget.cpp +++ b/src/app/layout/qgslayoutmapgridwidget.cpp @@ -156,8 +156,8 @@ QgsLayoutMapGridWidget::QgsLayoutMapGridWidget( QgsLayoutItemMapGrid *mapGrid, Q mGridMarkerStyleButton->setLayer( coverageLayer() ); if ( mMap->layout() ) { - connect( &mMap->layout()->context(), &QgsLayoutContext::layerChanged, mGridLineStyleButton, &QgsSymbolButton::setLayer ); - connect( &mMap->layout()->context(), &QgsLayoutContext::layerChanged, mGridMarkerStyleButton, &QgsSymbolButton::setLayer ); + connect( &mMap->layout()->reportContext(), &QgsLayoutReportContext::layerChanged, mGridLineStyleButton, &QgsSymbolButton::setLayer ); + connect( &mMap->layout()->reportContext(), &QgsLayoutReportContext::layerChanged, mGridMarkerStyleButton, &QgsSymbolButton::setLayer ); } } diff --git a/src/app/layout/qgslayoutmapwidget.cpp b/src/app/layout/qgslayoutmapwidget.cpp index 2f654a4b17b4..607c6ece220a 100644 --- a/src/app/layout/qgslayoutmapwidget.cpp +++ b/src/app/layout/qgslayoutmapwidget.cpp @@ -112,7 +112,7 @@ QgsLayoutMapWidget::QgsLayoutMapWidget( QgsLayoutItemMap *item ) connect( item, &QgsLayoutObject::changed, this, &QgsLayoutMapWidget::updateGuiElements ); - connect( &item->layout()->context(), &QgsLayoutContext::layerChanged, + connect( &item->layout()->reportContext(), &QgsLayoutReportContext::layerChanged, this, &QgsLayoutMapWidget::atlasLayerChanged ); if ( QgsLayoutAtlas *atlas = layoutAtlas() ) { @@ -132,7 +132,7 @@ QgsLayoutMapWidget::QgsLayoutMapWidget( QgsLayoutItemMap *item ) mOverviewFrameStyleButton->setLayer( coverageLayer() ); if ( item->layout() ) { - connect( &item->layout()->context(), &QgsLayoutContext::layerChanged, mOverviewFrameStyleButton, &QgsSymbolButton::setLayer ); + connect( &item->layout()->reportContext(), &QgsLayoutReportContext::layerChanged, mOverviewFrameStyleButton, &QgsSymbolButton::setLayer ); } @@ -195,8 +195,8 @@ void QgsLayoutMapWidget::populateDataDefinedButtons() void QgsLayoutMapWidget::compositionAtlasToggled( bool atlasEnabled ) { if ( atlasEnabled && - mMapItem && mMapItem->layout() && mMapItem->layout()->context().layer() - && mMapItem->layout()->context().layer()->wkbType() != QgsWkbTypes::NoGeometry ) + mMapItem && mMapItem->layout() && mMapItem->layout()->reportContext().layer() + && mMapItem->layout()->reportContext().layer()->wkbType() != QgsWkbTypes::NoGeometry ) { mAtlasCheckBox->setEnabled( true ); } diff --git a/src/app/layout/qgslayoutpagepropertieswidget.cpp b/src/app/layout/qgslayoutpagepropertieswidget.cpp index aa342cd51e68..061b93f1042f 100644 --- a/src/app/layout/qgslayoutpagepropertieswidget.cpp +++ b/src/app/layout/qgslayoutpagepropertieswidget.cpp @@ -45,7 +45,7 @@ QgsLayoutPagePropertiesWidget::QgsLayoutPagePropertiesWidget( QWidget *parent, Q mSizeUnitsComboBox->linkToWidget( mWidthSpin ); mSizeUnitsComboBox->linkToWidget( mHeightSpin ); - mSizeUnitsComboBox->setConverter( &mPage->layout()->context().measurementConverter() ); + mSizeUnitsComboBox->setConverter( &mPage->layout()->renderContext().measurementConverter() ); mLockAspectRatio->setWidthSpinBox( mWidthSpin ); mLockAspectRatio->setHeightSpinBox( mHeightSpin ); @@ -80,7 +80,7 @@ QgsLayoutPagePropertiesWidget::QgsLayoutPagePropertiesWidget( QWidget *parent, Q mSymbolButton->setLayer( coverageLayer() ); if ( mPage->layout() ) { - connect( &mPage->layout()->context(), &QgsLayoutContext::layerChanged, mSymbolButton, &QgsSymbolButton::setLayer ); + connect( &mPage->layout()->reportContext(), &QgsLayoutReportContext::layerChanged, mSymbolButton, &QgsSymbolButton::setLayer ); } showCurrentPageSize(); diff --git a/src/app/layout/qgslayoutpolygonwidget.cpp b/src/app/layout/qgslayoutpolygonwidget.cpp index 0a28df41e697..122f0eefebfe 100644 --- a/src/app/layout/qgslayoutpolygonwidget.cpp +++ b/src/app/layout/qgslayoutpolygonwidget.cpp @@ -51,7 +51,7 @@ QgsLayoutPolygonWidget::QgsLayoutPolygonWidget( QgsLayoutItemPolygon *polygon ) mPolygonStyleButton->setLayer( coverageLayer() ); if ( mPolygon->layout() ) { - connect( &mPolygon->layout()->context(), &QgsLayoutContext::layerChanged, mPolygonStyleButton, &QgsSymbolButton::setLayer ); + connect( &mPolygon->layout()->reportContext(), &QgsLayoutReportContext::layerChanged, mPolygonStyleButton, &QgsSymbolButton::setLayer ); } } diff --git a/src/app/layout/qgslayoutpolylinewidget.cpp b/src/app/layout/qgslayoutpolylinewidget.cpp index d1b098c5c3f4..5960a0584a60 100644 --- a/src/app/layout/qgslayoutpolylinewidget.cpp +++ b/src/app/layout/qgslayoutpolylinewidget.cpp @@ -92,7 +92,7 @@ QgsLayoutPolylineWidget::QgsLayoutPolylineWidget( QgsLayoutItemPolyline *polylin mLineStyleButton->setLayer( coverageLayer() ); if ( mPolyline->layout() ) { - connect( &mPolyline->layout()->context(), &QgsLayoutContext::layerChanged, mLineStyleButton, &QgsSymbolButton::setLayer ); + connect( &mPolyline->layout()->reportContext(), &QgsLayoutReportContext::layerChanged, mLineStyleButton, &QgsSymbolButton::setLayer ); } } diff --git a/src/app/layout/qgslayoutpropertieswidget.cpp b/src/app/layout/qgslayoutpropertieswidget.cpp index 8ebd70fd108f..847003985078 100644 --- a/src/app/layout/qgslayoutpropertieswidget.cpp +++ b/src/app/layout/qgslayoutpropertieswidget.cpp @@ -70,7 +70,7 @@ QgsLayoutPropertiesWidget::QgsLayoutPropertiesWidget( QWidget *parent, QgsLayout mLeftMarginSpinBox->setValue( leftMargin ); mMarginUnitsComboBox->linkToWidget( mLeftMarginSpinBox ); mMarginUnitsComboBox->setUnit( marginUnit ); - mMarginUnitsComboBox->setConverter( &mLayout->context().measurementConverter() ); + mMarginUnitsComboBox->setConverter( &mLayout->renderContext().measurementConverter() ); connect( mTopMarginSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutPropertiesWidget::resizeMarginsChanged ); connect( mRightMarginSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutPropertiesWidget::resizeMarginsChanged ); @@ -90,7 +90,7 @@ QgsLayoutPropertiesWidget::QgsLayoutPropertiesWidget( QWidget *parent, QgsLayout void QgsLayoutPropertiesWidget::updateGui() { whileBlocking( mReferenceMapComboBox )->setItem( mLayout->referenceMap() ); - whileBlocking( mResolutionSpinBox )->setValue( mLayout->context().dpi() ); + whileBlocking( mResolutionSpinBox )->setValue( mLayout->renderContext().dpi() ); bool rasterize = mLayout->customProperty( QStringLiteral( "rasterize" ), false ).toBool(); whileBlocking( mRasterizeCheckBox )->setChecked( rasterize ); @@ -199,7 +199,7 @@ void QgsLayoutPropertiesWidget::referenceMapChanged( QgsLayoutItem *item ) void QgsLayoutPropertiesWidget::dpiChanged( int value ) { mLayout->undoStack()->beginCommand( mLayout, tr( "Set Default DPI" ), QgsLayout::UndoLayoutDpi ); - mLayout->context().setDpi( value ); + mLayout->renderContext().setDpi( value ); mLayout->undoStack()->endCommand(); } diff --git a/src/app/layout/qgslayoutshapewidget.cpp b/src/app/layout/qgslayoutshapewidget.cpp index 03173efddf54..e51aeee80007 100644 --- a/src/app/layout/qgslayoutshapewidget.cpp +++ b/src/app/layout/qgslayoutshapewidget.cpp @@ -48,7 +48,7 @@ QgsLayoutShapeWidget::QgsLayoutShapeWidget( QgsLayoutItemShape *shape ) mShapeStyleButton->setSymbolType( QgsSymbol::Fill ); mRadiusUnitsComboBox->linkToWidget( mCornerRadiusSpinBox ); - mRadiusUnitsComboBox->setConverter( &mShape->layout()->context().measurementConverter() ); + mRadiusUnitsComboBox->setConverter( &mShape->layout()->renderContext().measurementConverter() ); setGuiElementValues(); @@ -63,7 +63,7 @@ QgsLayoutShapeWidget::QgsLayoutShapeWidget( QgsLayoutItemShape *shape ) mShapeStyleButton->setLayer( coverageLayer() ); if ( mShape->layout() ) { - connect( &mShape->layout()->context(), &QgsLayoutContext::layerChanged, mShapeStyleButton, &QgsSymbolButton::setLayer ); + connect( &mShape->layout()->reportContext(), &QgsLayoutReportContext::layerChanged, mShapeStyleButton, &QgsSymbolButton::setLayer ); } } diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 8b158ca73f32..2dda74ed1018 100755 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -367,7 +367,6 @@ SET(QGIS_CORE_SRCS layout/qgslayout.cpp layout/qgslayoutaligner.cpp layout/qgslayoutatlas.cpp - layout/qgslayoutcontext.cpp layout/qgslayouteffect.cpp layout/qgslayoutexporter.cpp layout/qgslayoutgridsettings.cpp @@ -401,6 +400,8 @@ SET(QGIS_CORE_SRCS layout/qgslayoutmultiframeundocommand.cpp layout/qgslayoutobject.cpp layout/qgslayoutpagecollection.cpp + layout/qgslayoutrendercontext.cpp + layout/qgslayoutreportcontext.cpp layout/qgslayoutserializableobject.cpp layout/qgslayoutsnapper.cpp layout/qgslayouttable.cpp @@ -740,7 +741,6 @@ SET(QGIS_CORE_MOC_HDRS layout/qgslayout.h layout/qgslayoutatlas.h - layout/qgslayoutcontext.h layout/qgslayouteffect.h layout/qgslayoutguidecollection.h layout/qgslayoutframe.h @@ -768,6 +768,8 @@ SET(QGIS_CORE_MOC_HDRS layout/qgslayoutmultiframe.h layout/qgslayoutpagecollection.h layout/qgslayoutobject.h + layout/qgslayoutrendercontext.h + layout/qgslayoutreportcontext.h layout/qgslayouttable.h layout/qgslayouttablecolumn.h layout/qgslayoutundostack.h diff --git a/src/core/layout/qgslayout.cpp b/src/core/layout/qgslayout.cpp index 1fa93e6453a5..bab4c0b5ad10 100644 --- a/src/core/layout/qgslayout.cpp +++ b/src/core/layout/qgslayout.cpp @@ -30,7 +30,8 @@ QgsLayout::QgsLayout( QgsProject *project ) : mProject( project ) - , mContext( new QgsLayoutContext( this ) ) + , mRenderContext( new QgsLayoutRenderContext( this ) ) + , mReportContext( new QgsLayoutReportContext( this ) ) , mSnapper( QgsLayoutSnapper( this ) ) , mGridSettings( this ) , mPageCollection( new QgsLayoutPageCollection( this ) ) @@ -296,42 +297,52 @@ QgsLayoutItem *QgsLayout::layoutItemAt( QPointF position, const QgsLayoutItem *b double QgsLayout::convertToLayoutUnits( const QgsLayoutMeasurement &measurement ) const { - return mContext->measurementConverter().convert( measurement, mUnits ).length(); + return mRenderContext->measurementConverter().convert( measurement, mUnits ).length(); } QSizeF QgsLayout::convertToLayoutUnits( const QgsLayoutSize &size ) const { - return mContext->measurementConverter().convert( size, mUnits ).toQSizeF(); + return mRenderContext->measurementConverter().convert( size, mUnits ).toQSizeF(); } QPointF QgsLayout::convertToLayoutUnits( const QgsLayoutPoint &point ) const { - return mContext->measurementConverter().convert( point, mUnits ).toQPointF(); + return mRenderContext->measurementConverter().convert( point, mUnits ).toQPointF(); } QgsLayoutMeasurement QgsLayout::convertFromLayoutUnits( const double length, const QgsUnitTypes::LayoutUnit unit ) const { - return mContext->measurementConverter().convert( QgsLayoutMeasurement( length, mUnits ), unit ); + return mRenderContext->measurementConverter().convert( QgsLayoutMeasurement( length, mUnits ), unit ); } QgsLayoutSize QgsLayout::convertFromLayoutUnits( const QSizeF &size, const QgsUnitTypes::LayoutUnit unit ) const { - return mContext->measurementConverter().convert( QgsLayoutSize( size.width(), size.height(), mUnits ), unit ); + return mRenderContext->measurementConverter().convert( QgsLayoutSize( size.width(), size.height(), mUnits ), unit ); } QgsLayoutPoint QgsLayout::convertFromLayoutUnits( const QPointF &point, const QgsUnitTypes::LayoutUnit unit ) const { - return mContext->measurementConverter().convert( QgsLayoutPoint( point.x(), point.y(), mUnits ), unit ); + return mRenderContext->measurementConverter().convert( QgsLayoutPoint( point.x(), point.y(), mUnits ), unit ); } -QgsLayoutContext &QgsLayout::context() +QgsLayoutRenderContext &QgsLayout::renderContext() { - return *mContext; + return *mRenderContext; } -const QgsLayoutContext &QgsLayout::context() const +const QgsLayoutRenderContext &QgsLayout::renderContext() const { - return *mContext; + return *mRenderContext; +} + +QgsLayoutReportContext &QgsLayout::reportContext() +{ + return *mReportContext; +} + +const QgsLayoutReportContext &QgsLayout::reportContext() const +{ + return *mReportContext; } QgsLayoutGuideCollection &QgsLayout::guides() @@ -734,7 +745,7 @@ void QgsLayout::writeXmlLayoutSettings( QDomElement &element, QDomDocument &docu element.setAttribute( QStringLiteral( "name" ), mName ); element.setAttribute( QStringLiteral( "units" ), QgsUnitTypes::encodeUnit( mUnits ) ); element.setAttribute( QStringLiteral( "worldFileMap" ), mWorldFileMapId ); - element.setAttribute( QStringLiteral( "printResolution" ), mContext->dpi() ); + element.setAttribute( QStringLiteral( "printResolution" ), mRenderContext->dpi() ); } QDomElement QgsLayout::writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const @@ -778,7 +789,7 @@ bool QgsLayout::readXmlLayoutSettings( const QDomElement &layoutElement, const Q setName( layoutElement.attribute( QStringLiteral( "name" ) ) ); setUnits( QgsUnitTypes::decodeLayoutUnit( layoutElement.attribute( QStringLiteral( "units" ) ) ) ); mWorldFileMapId = layoutElement.attribute( QStringLiteral( "worldFileMap" ) ); - mContext->setDpi( layoutElement.attribute( QStringLiteral( "printResolution" ), "300" ).toDouble() ); + mRenderContext->setDpi( layoutElement.attribute( QStringLiteral( "printResolution" ), "300" ).toDouble() ); emit changed(); return true; diff --git a/src/core/layout/qgslayout.h b/src/core/layout/qgslayout.h index 972decb53e0b..58589fd360a4 100644 --- a/src/core/layout/qgslayout.h +++ b/src/core/layout/qgslayout.h @@ -29,7 +29,8 @@ class QgsLayoutModel; class QgsLayoutMultiFrame; class QgsLayoutPageCollection; class QgsLayoutUndoStack; -class QgsLayoutContext; +class QgsLayoutRenderContext; +class QgsLayoutReportContext; /** * \ingroup core @@ -324,16 +325,28 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext QgsLayoutPoint convertFromLayoutUnits( const QPointF &point, const QgsUnitTypes::LayoutUnit unit ) const; /** - * Returns a reference to the layout's context, which stores information relating to the - * current context and rendering settings for the layout. + * Returns a reference to the layout's render context, which stores information relating to the + * current rendering settings for the layout. */ - QgsLayoutContext &context(); + QgsLayoutRenderContext &renderContext(); /** - * Returns a reference to the layout's context, which stores information relating to the - * current context and rendering settings for the layout. + * Returns a reference to the layout's render context, which stores information relating to the + * current rendering settings for the layout. */ - SIP_SKIP const QgsLayoutContext &context() const; + SIP_SKIP const QgsLayoutRenderContext &renderContext() const; + + /** + * Returns a reference to the layout's report context, which stores information relating to the + * current reporting context for the layout. + */ + QgsLayoutReportContext &reportContext(); + + /** + * Returns a reference to the layout's report context, which stores information relating to the + * current reporting context for the layout. + */ + SIP_SKIP const QgsLayoutReportContext &reportContext() const; /** * Returns a reference to the layout's snapper, which stores handles layout snap grids and lines @@ -638,7 +651,8 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext QgsObjectCustomProperties mCustomProperties; QgsUnitTypes::LayoutUnit mUnits = QgsUnitTypes::LayoutMillimeters; - QgsLayoutContext *mContext = nullptr; + QgsLayoutRenderContext *mRenderContext = nullptr; + QgsLayoutReportContext *mReportContext = nullptr; QgsLayoutSnapper mSnapper; QgsLayoutGridSettings mGridSettings; diff --git a/src/core/layout/qgslayoutatlas.cpp b/src/core/layout/qgslayoutatlas.cpp index 4c3c4e7389a8..adaf38f5e7e0 100644 --- a/src/core/layout/qgslayoutatlas.cpp +++ b/src/core/layout/qgslayoutatlas.cpp @@ -97,7 +97,7 @@ bool QgsLayoutAtlas::readXml( const QDomElement &atlasElem, const QDomDocument & mCoverageLayer = QgsVectorLayerRef( layerId, layerName, layerSource, layerProvider ); mCoverageLayer.resolveWeakly( mLayout->project() ); - mLayout->context().setLayer( mCoverageLayer.get() ); + mLayout->reportContext().setLayer( mCoverageLayer.get() ); mPageNameExpression = atlasElem.attribute( QStringLiteral( "pageNameExpression" ), QString() ); QString error; @@ -394,7 +394,7 @@ void QgsLayoutAtlas::setHideCoverage( bool hide ) { mHideCoverage = hide; - mLayout->context().setFlag( QgsLayoutContext::FlagHideCoverageLayer, hide ); + mLayout->renderContext().setFlag( QgsLayoutRenderContext::FlagHideCoverageLayer, hide ); mLayout->refresh(); } @@ -507,10 +507,10 @@ bool QgsLayoutAtlas::prepareForFeature( const int featureI ) return false; } - mLayout->context().blockSignals( true ); // setFeature emits changed, we don't want 2 signals - mLayout->context().setLayer( mCoverageLayer.get() ); - mLayout->context().blockSignals( false ); - mLayout->context().setFeature( mCurrentFeature ); + mLayout->reportContext().blockSignals( true ); // setFeature emits changed, we don't want 2 signals + mLayout->reportContext().setLayer( mCoverageLayer.get() ); + mLayout->reportContext().blockSignals( false ); + mLayout->reportContext().setFeature( mCurrentFeature ); emit featureChanged( mCurrentFeature ); emit messagePushed( QString( tr( "Atlas feature %1 of %2" ) ).arg( featureI + 1 ).arg( mFeatureIds.size() ) ); diff --git a/src/core/layout/qgslayoutcontext.cpp b/src/core/layout/qgslayoutcontext.cpp deleted file mode 100644 index c3131c7e7422..000000000000 --- a/src/core/layout/qgslayoutcontext.cpp +++ /dev/null @@ -1,169 +0,0 @@ -/*************************************************************************** - qgslayoutcontext.cpp - -------------------- - begin : July 2017 - copyright : (C) 2017 by Nyall Dawson - email : nyall dot dawson at gmail dot com - ***************************************************************************/ -/*************************************************************************** - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - ***************************************************************************/ - -#include "qgslayoutcontext.h" -#include "qgsfeature.h" -#include "qgslayout.h" - -QgsLayoutContext::QgsLayoutContext( QgsLayout *layout ) - : QObject( layout ) - , mFlags( FlagAntialiasing | FlagUseAdvancedEffects ) - , mLayout( layout ) -{} - -void QgsLayoutContext::setFlags( const QgsLayoutContext::Flags flags ) -{ - if ( flags == mFlags ) - return; - - mFlags = flags; - emit flagsChanged( mFlags ); -} - -void QgsLayoutContext::setFlag( const QgsLayoutContext::Flag flag, const bool on ) -{ - Flags newFlags = mFlags; - if ( on ) - newFlags |= flag; - else - newFlags &= ~flag; - - if ( newFlags == mFlags ) - return; - - mFlags = newFlags; - emit flagsChanged( mFlags ); -} - -QgsLayoutContext::Flags QgsLayoutContext::flags() const -{ - return mFlags; -} - -bool QgsLayoutContext::testFlag( const QgsLayoutContext::Flag flag ) const -{ - return mFlags.testFlag( flag ); -} - -QgsRenderContext::Flags QgsLayoutContext::renderContextFlags() const -{ - QgsRenderContext::Flags flags = nullptr; - if ( mFlags & FlagAntialiasing ) - flags = flags | QgsRenderContext::Antialiasing; - if ( mFlags & FlagUseAdvancedEffects ) - flags = flags | QgsRenderContext::UseAdvancedEffects; - - // TODO - expose as layout context flag? - flags |= QgsRenderContext::ForceVectorOutput; - return flags; -} - -void QgsLayoutContext::setFeature( const QgsFeature &feature ) -{ - mFeature = feature; - mGeometryCache.clear(); - emit changed(); -} - -QgsGeometry QgsLayoutContext::currentGeometry( const QgsCoordinateReferenceSystem &crs ) const -{ - if ( !crs.isValid() ) - { - // no projection, return the native geometry - return mFeature.geometry(); - } - - if ( !mLayer || !mFeature.isValid() || !mFeature.hasGeometry() ) - { - return QgsGeometry(); - } - - if ( mLayer->crs() == crs ) - { - // no projection, return the native geometry - return mFeature.geometry(); - } - - auto it = mGeometryCache.constFind( crs.srsid() ); - if ( it != mGeometryCache.constEnd() ) - { - // we have it in cache, return it - return it.value(); - } - - QgsGeometry transformed = mFeature.geometry(); - transformed.transform( QgsCoordinateTransform( mLayer->crs(), crs, mLayout->project() ) ); - mGeometryCache[crs.srsid()] = transformed; - return transformed; -} - -QgsVectorLayer *QgsLayoutContext::layer() const -{ - return mLayer; -} - -void QgsLayoutContext::setLayer( QgsVectorLayer *layer ) -{ - mLayer = layer; - emit layerChanged( layer ); - emit changed(); -} - -void QgsLayoutContext::setDpi( double dpi ) -{ - if ( dpi == mMeasurementConverter.dpi() ) - return; - - mMeasurementConverter.setDpi( dpi ); - emit dpiChanged(); -} - -double QgsLayoutContext::dpi() const -{ - return mMeasurementConverter.dpi(); -} - -bool QgsLayoutContext::gridVisible() const -{ - return mGridVisible; -} - -void QgsLayoutContext::setGridVisible( bool visible ) -{ - mGridVisible = visible; -} - -bool QgsLayoutContext::boundingBoxesVisible() const -{ - return mBoundingBoxesVisible; -} - -void QgsLayoutContext::setBoundingBoxesVisible( bool visible ) -{ - mBoundingBoxesVisible = visible; -} - -void QgsLayoutContext::setPagesVisible( bool visible ) -{ - mPagesVisible = visible; -} - -void QgsLayoutContext::setPredefinedScales( const QVector &scales ) -{ - mPredefinedScales = scales; - // make sure the list is sorted - std::sort( mPredefinedScales.begin(), mPredefinedScales.end() ); -} diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index 00ec7c0aef72..1e5dae21e892 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -37,14 +37,14 @@ class LayoutContextPreviewSettingRestorer LayoutContextPreviewSettingRestorer( QgsLayout *layout ) : mLayout( layout ) - , mPreviousSetting( layout->context().mIsPreviewRender ) + , mPreviousSetting( layout->renderContext().mIsPreviewRender ) { - mLayout->context().mIsPreviewRender = false; + mLayout->renderContext().mIsPreviewRender = false; } ~LayoutContextPreviewSettingRestorer() { - mLayout->context().mIsPreviewRender = mPreviousSetting; + mLayout->renderContext().mIsPreviewRender = mPreviousSetting; } private: @@ -218,7 +218,7 @@ void QgsLayoutExporter::renderRegion( QPainter *painter, const QRectF ®ion ) LayoutGuideHider guideHider( mLayout ); ( void ) guideHider; - painter->setRenderHint( QPainter::Antialiasing, mLayout->context().flags() & QgsLayoutContext::FlagAntialiasing ); + painter->setRenderHint( QPainter::Antialiasing, mLayout->renderContext().flags() & QgsLayoutRenderContext::FlagAntialiasing ); mLayout->render( painter, QRectF( 0, 0, paintDevice->width(), paintDevice->height() ), region ); } @@ -231,7 +231,7 @@ QImage QgsLayoutExporter::renderRegionToImage( const QRectF ®ion, QSize image LayoutContextPreviewSettingRestorer restorer( mLayout ); ( void )restorer; - double resolution = mLayout->context().dpi(); + double resolution = mLayout->renderContext().dpi(); double oneInchInLayoutUnits = mLayout->convertToLayoutUnits( QgsLayoutMeasurement( 1, QgsUnitTypes::LayoutInches ) ); if ( imageSize.isValid() ) { @@ -273,23 +273,23 @@ class LayoutContextSettingsRestorer LayoutContextSettingsRestorer( QgsLayout *layout ) : mLayout( layout ) - , mPreviousDpi( layout->context().dpi() ) - , mPreviousFlags( layout->context().flags() ) - , mPreviousExportLayer( layout->context().currentExportLayer() ) + , mPreviousDpi( layout->renderContext().dpi() ) + , mPreviousFlags( layout->renderContext().flags() ) + , mPreviousExportLayer( layout->renderContext().currentExportLayer() ) { } ~LayoutContextSettingsRestorer() { - mLayout->context().setDpi( mPreviousDpi ); - mLayout->context().setFlags( mPreviousFlags ); - mLayout->context().setCurrentExportLayer( mPreviousExportLayer ); + mLayout->renderContext().setDpi( mPreviousDpi ); + mLayout->renderContext().setFlags( mPreviousFlags ); + mLayout->renderContext().setCurrentExportLayer( mPreviousExportLayer ); } private: QgsLayout *mLayout = nullptr; double mPreviousDpi = 0; - QgsLayoutContext::Flags mPreviousFlags = 0; + QgsLayoutRenderContext::Flags mPreviousFlags = 0; int mPreviousExportLayer = 0; }; ///@endcond PRIVATE @@ -301,7 +301,7 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString ImageExportSettings settings = s; if ( settings.dpi <= 0 ) - settings.dpi = mLayout->context().dpi(); + settings.dpi = mLayout->renderContext().dpi(); mErrorFileName.clear(); @@ -322,8 +322,8 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( const QString ( void )restorer; LayoutContextSettingsRestorer dpiRestorer( mLayout ); ( void )dpiRestorer; - mLayout->context().setDpi( settings.dpi ); - mLayout->context().setFlags( settings.flags ); + mLayout->renderContext().setDpi( settings.dpi ); + mLayout->renderContext().setFlags( settings.flags ); QList< int > pages; if ( settings.pages.empty() ) @@ -448,7 +448,7 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f PdfExportSettings settings = s; if ( settings.dpi <= 0 ) - settings.dpi = mLayout->context().dpi(); + settings.dpi = mLayout->renderContext().dpi(); mErrorFileName.clear(); @@ -456,14 +456,14 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( const QString &f ( void )restorer; LayoutContextSettingsRestorer contextRestorer( mLayout ); ( void )contextRestorer; - mLayout->context().setDpi( settings.dpi ); + mLayout->renderContext().setDpi( settings.dpi ); // If we are not printing as raster, temporarily disable advanced effects // as QPrinter does not support composition modes and can result // in items missing from the output - mLayout->context().setFlag( QgsLayoutContext::FlagUseAdvancedEffects, !settings.forceVectorOutput ); + mLayout->renderContext().setFlag( QgsLayoutRenderContext::FlagUseAdvancedEffects, !settings.forceVectorOutput ); - mLayout->context().setFlag( QgsLayoutContext::FlagForceVectorOutput, settings.forceVectorOutput ); + mLayout->renderContext().setFlag( QgsLayoutRenderContext::FlagForceVectorOutput, settings.forceVectorOutput ); QPrinter printer; preparePrintAsPdf( mLayout, printer, filePath ); @@ -515,20 +515,20 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( QgsAbstractLayou } if ( s.dpi <= 0 ) - settings.dpi = iterator->layout()->context().dpi(); + settings.dpi = iterator->layout()->renderContext().dpi(); LayoutContextPreviewSettingRestorer restorer( iterator->layout() ); ( void )restorer; LayoutContextSettingsRestorer contextRestorer( iterator->layout() ); ( void )contextRestorer; - iterator->layout()->context().setDpi( settings.dpi ); + iterator->layout()->renderContext().setDpi( settings.dpi ); // If we are not printing as raster, temporarily disable advanced effects // as QPrinter does not support composition modes and can result // in items missing from the output - iterator->layout()->context().setFlag( QgsLayoutContext::FlagUseAdvancedEffects, !settings.forceVectorOutput ); + iterator->layout()->renderContext().setFlag( QgsLayoutRenderContext::FlagUseAdvancedEffects, !settings.forceVectorOutput ); - iterator->layout()->context().setFlag( QgsLayoutContext::FlagForceVectorOutput, settings.forceVectorOutput ); + iterator->layout()->renderContext().setFlag( QgsLayoutRenderContext::FlagForceVectorOutput, settings.forceVectorOutput ); if ( first ) { @@ -618,7 +618,7 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &f SvgExportSettings settings = s; if ( settings.dpi <= 0 ) - settings.dpi = mLayout->context().dpi(); + settings.dpi = mLayout->renderContext().dpi(); mErrorFileName.clear(); @@ -626,9 +626,9 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &f ( void )restorer; LayoutContextSettingsRestorer contextRestorer( mLayout ); ( void )contextRestorer; - mLayout->context().setDpi( settings.dpi ); + mLayout->renderContext().setDpi( settings.dpi ); - mLayout->context().setFlag( QgsLayoutContext::FlagForceVectorOutput, settings.forceVectorOutput ); + mLayout->renderContext().setFlag( QgsLayoutRenderContext::FlagForceVectorOutput, settings.forceVectorOutput ); QFileInfo fi( filePath ); PageExportDetails pageDetails; @@ -707,7 +707,7 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &f if ( layoutItem && layoutItem->numberExportLayers() > 0 ) { layoutItem->show(); - mLayout->context().setCurrentExportLayer( layoutItemLayerIdx ); + mLayout->renderContext().setCurrentExportLayer( layoutItemLayerIdx ); ++layoutItemLayerIdx; } else @@ -733,7 +733,7 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &f if ( layoutItem && layoutItem->numberExportLayers() > 0 && layoutItem->numberExportLayers() == layoutItemLayerIdx ) // restore and pass to next item { - mLayout->context().setCurrentExportLayer( -1 ); + mLayout->renderContext().setCurrentExportLayer( -1 ); layoutItemLayerIdx = 0; ++it; } @@ -851,7 +851,7 @@ void QgsLayoutExporter::preparePrint( QgsLayout *layout, QPrinter &printer, bool printer.setColorMode( QPrinter::Color ); //set user-defined resolution - printer.setResolution( layout->context().dpi() ); + printer.setResolution( layout->renderContext().dpi() ); if ( setFirstPageSize ) { @@ -937,7 +937,7 @@ void QgsLayoutExporter::updatePrinterPageSize( QgsLayout *layout, QPrinter &prin //for landscape sized outputs (#11352) printer.setOrientation( QPrinter::Portrait ); QgsLayoutSize pageSize = layout->pageCollection()->page( page )->sizeWithUnits(); - QgsLayoutSize pageSizeMM = layout->context().measurementConverter().convert( pageSize, QgsUnitTypes::LayoutMillimeters ); + QgsLayoutSize pageSizeMM = layout->renderContext().measurementConverter().convert( pageSize, QgsUnitTypes::LayoutMillimeters ); printer.setPaperSize( pageSizeMM.toQSizeF(), QPrinter::Millimeter ); } @@ -1001,7 +1001,7 @@ std::unique_ptr QgsLayoutExporter::computeGeoTransform( const QgsLayou return nullptr; if ( dpi < 0 ) - dpi = mLayout->context().dpi(); + dpi = mLayout->renderContext().dpi(); // calculate region of composition to export (in mm) QRectF exportRegion = region; @@ -1107,7 +1107,7 @@ bool QgsLayoutExporter::georeferenceOutput( const QString &file, QgsLayoutItemMa return false; // no reference map if ( dpi < 0 ) - dpi = mLayout->context().dpi(); + dpi = mLayout->renderContext().dpi(); std::unique_ptr t = computeGeoTransform( map, exportRegion, dpi ); if ( !t ) @@ -1190,7 +1190,7 @@ void QgsLayoutExporter::computeWorldFileParameters( const QRectF &exportRegion, double Y0 = paperExtent.yMinimum(); if ( dpi < 0 ) - dpi = mLayout->context().dpi(); + dpi = mLayout->renderContext().dpi(); int widthPx = static_cast< int >( dpi * destinationWidth / 25.4 ); int heightPx = static_cast< int >( dpi * destinationHeight / 25.4 ); diff --git a/src/core/layout/qgslayoutexporter.h b/src/core/layout/qgslayoutexporter.h index 4ab0da1b8045..f56e5da13cd7 100644 --- a/src/core/layout/qgslayoutexporter.h +++ b/src/core/layout/qgslayoutexporter.h @@ -18,7 +18,8 @@ #include "qgis_core.h" #include "qgsmargins.h" -#include "qgslayoutcontext.h" +#include "qgslayoutrendercontext.h" +#include "qgslayoutreportcontext.h" #include #include #include @@ -143,7 +144,7 @@ class CORE_EXPORT QgsLayoutExporter { //! Constructor for ImageExportSettings ImageExportSettings() - : flags( QgsLayoutContext::FlagAntialiasing | QgsLayoutContext::FlagUseAdvancedEffects ) + : flags( QgsLayoutRenderContext::FlagAntialiasing | QgsLayoutRenderContext::FlagUseAdvancedEffects ) {} //! Resolution to export layout at. If dpi <= 0 the default layout dpi will be used. @@ -193,7 +194,7 @@ class CORE_EXPORT QgsLayoutExporter /** * Layout context flags, which control how the export will be created. */ - QgsLayoutContext::Flags flags = 0; + QgsLayoutRenderContext::Flags flags = 0; }; @@ -230,7 +231,7 @@ class CORE_EXPORT QgsLayoutExporter { //! Constructor for PdfExportSettings PdfExportSettings() - : flags( QgsLayoutContext::FlagAntialiasing | QgsLayoutContext::FlagUseAdvancedEffects ) + : flags( QgsLayoutRenderContext::FlagAntialiasing | QgsLayoutRenderContext::FlagUseAdvancedEffects ) {} //! Resolution to export layout at. If dpi <= 0 the default layout dpi will be used. @@ -255,7 +256,7 @@ class CORE_EXPORT QgsLayoutExporter /** * Layout context flags, which control how the export will be created. */ - QgsLayoutContext::Flags flags = 0; + QgsLayoutRenderContext::Flags flags = 0; }; @@ -303,7 +304,7 @@ class CORE_EXPORT QgsLayoutExporter { //! Constructor for SvgExportSettings SvgExportSettings() - : flags( QgsLayoutContext::FlagAntialiasing | QgsLayoutContext::FlagUseAdvancedEffects ) + : flags( QgsLayoutRenderContext::FlagAntialiasing | QgsLayoutRenderContext::FlagUseAdvancedEffects ) {} //! Resolution to export layout at. If dpi <= 0 the default layout dpi will be used. @@ -340,7 +341,7 @@ class CORE_EXPORT QgsLayoutExporter /** * Layout context flags, which control how the export will be created. */ - QgsLayoutContext::Flags flags = 0; + QgsLayoutRenderContext::Flags flags = 0; }; diff --git a/src/core/layout/qgslayoutitem.cpp b/src/core/layout/qgslayoutitem.cpp index d1a5f5333b80..9dade563309d 100644 --- a/src/core/layout/qgslayoutitem.cpp +++ b/src/core/layout/qgslayoutitem.cpp @@ -71,10 +71,10 @@ QgsLayoutItem::QgsLayoutItem( QgsLayout *layout, bool manageZValue ) mEffect.reset( new QgsLayoutEffect() ); if ( mLayout ) { - mEffect->setEnabled( mLayout->context().flags() & QgsLayoutContext::FlagUseAdvancedEffects ); - connect( &mLayout->context(), &QgsLayoutContext::flagsChanged, this, [ = ]( QgsLayoutContext::Flags flags ) + mEffect->setEnabled( mLayout->renderContext().flags() & QgsLayoutRenderContext::FlagUseAdvancedEffects ); + connect( &mLayout->renderContext(), &QgsLayoutRenderContext::flagsChanged, this, [ = ]( QgsLayoutRenderContext::Flags flags ) { - mEffect->setEnabled( flags & QgsLayoutContext::FlagUseAdvancedEffects ); + mEffect->setEnabled( flags & QgsLayoutRenderContext::FlagUseAdvancedEffects ); } ); } setGraphicsEffect( mEffect.get() ); @@ -243,10 +243,10 @@ void QgsLayoutItem::paint( QPainter *painter, const QStyleOptionGraphicsItem *it return; } - bool previewRender = !mLayout || mLayout->context().isPreviewRender(); - double destinationDpi = previewRender ? itemStyle->matrix.m11() * 25.4 : mLayout->context().dpi(); + bool previewRender = !mLayout || mLayout->renderContext().isPreviewRender(); + double destinationDpi = previewRender ? itemStyle->matrix.m11() * 25.4 : mLayout->renderContext().dpi(); bool useImageCache = false; - bool forceRasterOutput = containsAdvancedEffects() && ( !mLayout || !( mLayout->context().flags() & QgsLayoutContext::FlagForceVectorOutput ) ); + bool forceRasterOutput = containsAdvancedEffects() && ( !mLayout || !( mLayout->renderContext().flags() & QgsLayoutRenderContext::FlagForceVectorOutput ) ); if ( useImageCache || forceRasterOutput ) { @@ -527,7 +527,7 @@ bool QgsLayoutItem::shouldBlockUndoCommands() const bool QgsLayoutItem::shouldDrawItem() const { - if ( !mLayout || mLayout->context().isPreviewRender() ) + if ( !mLayout || mLayout->renderContext().isPreviewRender() ) { //preview mode so OK to draw item return true; @@ -987,7 +987,7 @@ QgsLayoutSize QgsLayoutItem::applyDataDefinedSize( const QgsLayoutSize &size ) double evaluatedHeight = size.height(); if ( QgsApplication::pageSizeRegistry()->decodePageSize( pageSize, matchedSize ) ) { - QgsLayoutSize convertedSize = mLayout->context().measurementConverter().convert( matchedSize.size, size.units() ); + QgsLayoutSize convertedSize = mLayout->renderContext().measurementConverter().convert( matchedSize.size, size.units() ); evaluatedWidth = convertedSize.width(); evaluatedHeight = convertedSize.height(); } @@ -1271,12 +1271,12 @@ bool QgsLayoutItem::shouldDrawAntialiased() const { return true; } - return mLayout->context().testFlag( QgsLayoutContext::FlagAntialiasing ) && !mLayout->context().testFlag( QgsLayoutContext::FlagDebug ); + return mLayout->renderContext().testFlag( QgsLayoutRenderContext::FlagAntialiasing ) && !mLayout->renderContext().testFlag( QgsLayoutRenderContext::FlagDebug ); } bool QgsLayoutItem::shouldDrawDebugRect() const { - return mLayout && mLayout->context().testFlag( QgsLayoutContext::FlagDebug ); + return mLayout && mLayout->renderContext().testFlag( QgsLayoutRenderContext::FlagDebug ); } QSizeF QgsLayoutItem::applyMinimumSize( const QSizeF &targetSize ) diff --git a/src/core/layout/qgslayoutitemattributetable.cpp b/src/core/layout/qgslayoutitemattributetable.cpp index f209aac4a6f6..242cad39ab73 100644 --- a/src/core/layout/qgslayoutitemattributetable.cpp +++ b/src/core/layout/qgslayoutitemattributetable.cpp @@ -80,7 +80,7 @@ QgsLayoutItemAttributeTable::QgsLayoutItemAttributeTable( QgsLayout *layout ) connect( mLayout->project(), static_cast < void ( QgsProject::* )( const QString & ) >( &QgsProject::layerWillBeRemoved ), this, &QgsLayoutItemAttributeTable::removeLayer ); //coverage layer change = regenerate columns - connect( &mLayout->context(), &QgsLayoutContext::layerChanged, this, &QgsLayoutItemAttributeTable::atlasLayerChanged ); + connect( &mLayout->reportContext(), &QgsLayoutReportContext::layerChanged, this, &QgsLayoutItemAttributeTable::atlasLayerChanged ); } refreshAttributes(); } @@ -436,7 +436,7 @@ bool QgsLayoutItemAttributeTable::getTableContents( QgsLayoutTableContents &cont if ( mSource == QgsLayoutItemAttributeTable::RelationChildren ) { QgsRelation relation = mLayout->project()->relationManager()->relation( mRelationId ); - QgsFeature atlasFeature = mLayout->context().feature(); + QgsFeature atlasFeature = mLayout->reportContext().feature(); req = relation.getRelatedFeaturesRequest( atlasFeature ); } @@ -448,7 +448,7 @@ bool QgsLayoutItemAttributeTable::getTableContents( QgsLayoutTableContents &cont if ( mSource == QgsLayoutItemAttributeTable::AtlasFeature ) { //source mode is current atlas feature - QgsFeature atlasFeature = mLayout->context().feature(); + QgsFeature atlasFeature = mLayout->reportContext().feature(); req.setFilterFid( atlasFeature.id() ); } @@ -476,7 +476,7 @@ bool QgsLayoutItemAttributeTable::getTableContents( QgsLayoutTableContents &cont { continue; } - QgsFeature atlasFeature = mLayout->context().feature(); + QgsFeature atlasFeature = mLayout->reportContext().feature(); if ( !atlasFeature.hasGeometry() || !f.geometry().intersects( atlasFeature.geometry() ) ) { @@ -554,7 +554,7 @@ QgsVectorLayer *QgsLayoutItemAttributeTable::sourceLayer() switch ( mSource ) { case QgsLayoutItemAttributeTable::AtlasFeature: - return mLayout->context().layer(); + return mLayout->reportContext().layer(); case QgsLayoutItemAttributeTable::LayerAttributes: return mVectorLayer.get(); case QgsLayoutItemAttributeTable::RelationChildren: @@ -671,7 +671,7 @@ bool QgsLayoutItemAttributeTable::readPropertiesFromElement( const QDomElement & if ( mSource == QgsLayoutItemAttributeTable::AtlasFeature ) { - mCurrentAtlasLayer = mLayout->context().layer(); + mCurrentAtlasLayer = mLayout->reportContext().layer(); } mShowUniqueRowsOnly = itemElem.attribute( QStringLiteral( "showUniqueRowsOnly" ), QStringLiteral( "0" ) ).toInt(); diff --git a/src/core/layout/qgslayoutitemhtml.cpp b/src/core/layout/qgslayoutitemhtml.cpp index 1b54d90fb57f..fc5e11bc0d22 100644 --- a/src/core/layout/qgslayoutitemhtml.cpp +++ b/src/core/layout/qgslayoutitemhtml.cpp @@ -53,9 +53,9 @@ QgsLayoutItemHtml::QgsLayoutItemHtml( QgsLayout *layout ) //a html item added to a layout needs to have the initial expression context set, //otherwise fields in the html aren't correctly evaluated until atlas preview feature changes (#9457) - setExpressionContext( mLayout->context().feature(), mLayout->context().layer() ); + setExpressionContext( mLayout->reportContext().feature(), mLayout->reportContext().layer() ); - connect( &mLayout->context(), &QgsLayoutContext::changed, this, &QgsLayoutItemHtml::refreshExpressionContext ); + connect( &mLayout->reportContext(), &QgsLayoutReportContext::changed, this, &QgsLayoutItemHtml::refreshExpressionContext ); mFetcher = new QgsNetworkContentFetcher(); } @@ -301,7 +301,7 @@ double QgsLayoutItemHtml::htmlUnitsToLayoutUnits() return 1.0; } - return mLayout->convertToLayoutUnits( QgsLayoutMeasurement( mLayout->context().dpi() / 72.0, QgsUnitTypes::LayoutMillimeters ) ); //webkit seems to assume a standard dpi of 96 + return mLayout->convertToLayoutUnits( QgsLayoutMeasurement( mLayout->renderContext().dpi() / 72.0, QgsUnitTypes::LayoutMillimeters ) ); //webkit seems to assume a standard dpi of 96 } bool candidateSort( QPair c1, QPair c2 ) @@ -518,8 +518,8 @@ void QgsLayoutItemHtml::refreshExpressionContext() if ( mLayout ) { - vl = mLayout->context().layer(); - feature = mLayout->context().feature(); + vl = mLayout->reportContext().layer(); + feature = mLayout->reportContext().feature(); } setExpressionContext( feature, vl ); diff --git a/src/core/layout/qgslayoutitemlabel.cpp b/src/core/layout/qgslayoutitemlabel.cpp index ec70ade9829f..1f42fe46192e 100644 --- a/src/core/layout/qgslayoutitemlabel.cpp +++ b/src/core/layout/qgslayoutitemlabel.cpp @@ -196,7 +196,7 @@ double QgsLayoutItemLabel::htmlUnitsToLayoutUnits() } //TODO : fix this more precisely so that the label's default text size is the same with or without "display as html" - return mLayout->convertToLayoutUnits( QgsLayoutMeasurement( mLayout->context().dpi() / 72.0, QgsUnitTypes::LayoutMillimeters ) ); //webkit seems to assume a standard dpi of 72 + return mLayout->convertToLayoutUnits( QgsLayoutMeasurement( mLayout->renderContext().dpi() / 72.0, QgsUnitTypes::LayoutMillimeters ) ); //webkit seems to assume a standard dpi of 72 } void QgsLayoutItemLabel::setText( const QString &text ) @@ -235,7 +235,7 @@ void QgsLayoutItemLabel::refreshExpressionContext() if ( !mLayout ) return; - QgsVectorLayer *layer = mLayout->context().layer(); + QgsVectorLayer *layer = mLayout->reportContext().layer(); //setup distance area conversion if ( layer ) { diff --git a/src/core/layout/qgslayoutitemlegend.cpp b/src/core/layout/qgslayoutitemlegend.cpp index be32bbe2c60e..916b7ec1cf20 100644 --- a/src/core/layout/qgslayoutitemlegend.cpp +++ b/src/core/layout/qgslayoutitemlegend.cpp @@ -78,7 +78,7 @@ void QgsLayoutItemLegend::paint( QPainter *painter, const QStyleOptionGraphicsIt if ( mLayout ) { - mSettings.setUseAdvancedEffects( mLayout->context().flags() & QgsLayoutContext::FlagUseAdvancedEffects ); + mSettings.setUseAdvancedEffects( mLayout->renderContext().flags() & QgsLayoutRenderContext::FlagUseAdvancedEffects ); mSettings.setDpi( dpi ); } if ( mMap && mLayout ) @@ -754,7 +754,7 @@ void QgsLayoutItemLegend::doUpdateFilterByMap() if ( mMap && ( mLegendFilterByMap || filterByExpression || mInAtlas ) ) { - int dpi = mLayout->context().dpi(); + int dpi = mLayout->renderContext().dpi(); QgsRectangle requestRectangle = mMap->requestedExtent(); @@ -766,7 +766,7 @@ void QgsLayoutItemLegend::doUpdateFilterByMap() QgsGeometry filterPolygon; if ( mInAtlas ) { - filterPolygon = mLayout->context().currentGeometry( mMap->crs() ); + filterPolygon = mLayout->reportContext().currentGeometry( mMap->crs() ); } mLegendModel->setLegendFilter( &ms, /* useExtent */ mInAtlas || mLegendFilterByMap, filterPolygon, /* useExpressions */ true ); } @@ -788,7 +788,7 @@ bool QgsLayoutItemLegend::legendFilterOutAtlas() const void QgsLayoutItemLegend::onAtlasFeature() { - if ( !mLayout->context().feature().isValid() ) + if ( !mLayout->reportContext().feature().isValid() ) return; mInAtlas = mFilterOutAtlas; updateFilterByMap(); diff --git a/src/core/layout/qgslayoutitemmap.cpp b/src/core/layout/qgslayoutitemmap.cpp index 0c8745062b2e..9a466b869e7f 100644 --- a/src/core/layout/qgslayoutitemmap.cpp +++ b/src/core/layout/qgslayoutitemmap.cpp @@ -16,7 +16,8 @@ #include "qgslayoutitemmap.h" #include "qgslayout.h" -#include "qgslayoutcontext.h" +#include "qgslayoutrendercontext.h" +#include "qgslayoutreportcontext.h" #include "qgslayoututils.h" #include "qgslayoutmodel.h" #include "qgsmapthemecollection.h" @@ -759,7 +760,7 @@ void QgsLayoutItemMap::paint( QPainter *painter, const QStyleOptionGraphicsItem //TODO - try to reduce the amount of duplicate code here! - if ( mLayout->context().isPreviewRender() ) + if ( mLayout->renderContext().isPreviewRender() ) { painter->save(); painter->setClipRect( thisPaintRect ); @@ -833,7 +834,7 @@ void QgsLayoutItemMap::paint( QPainter *painter, const QStyleOptionGraphicsItem QgsRectangle cExtent = extent(); QSizeF size( cExtent.width() * mapUnitsToLayoutUnits(), cExtent.height() * mapUnitsToLayoutUnits() ); - if ( containsAdvancedEffects() && ( !mLayout || !( mLayout->context().flags() & QgsLayoutContext::FlagForceVectorOutput ) ) ) + if ( containsAdvancedEffects() && ( !mLayout || !( mLayout->renderContext().flags() & QgsLayoutRenderContext::FlagForceVectorOutput ) ) ) { // rasterize double destinationDpi = style->matrix.m11() * 25.4; @@ -1059,9 +1060,9 @@ QgsMapSettings QgsLayoutItemMap::mapSettings( const QgsRectangle &extent, QSizeF //set layers to render QList layers = layersToRender( &expressionContext ); - if ( mLayout && -1 != mLayout->context().currentExportLayer() ) + if ( mLayout && -1 != mLayout->renderContext().currentExportLayer() ) { - const int layerIdx = mLayout->context().currentExportLayer() - ( hasBackground() ? 1 : 0 ); + const int layerIdx = mLayout->renderContext().currentExportLayer() - ( hasBackground() ? 1 : 0 ); if ( layerIdx >= 0 && layerIdx < layers.length() ) { // exporting with separate layers (e.g., to svg layers), so we only want to render a single map layer @@ -1078,7 +1079,7 @@ QgsMapSettings QgsLayoutItemMap::mapSettings( const QgsRectangle &extent, QSizeF jobMapSettings.setLayers( layers ); jobMapSettings.setLayerStyleOverrides( layerStyleOverridesToRender( expressionContext ) ); - if ( !mLayout->context().isPreviewRender() ) + if ( !mLayout->renderContext().isPreviewRender() ) { //if outputting layout, disable optimisations like layer simplification jobMapSettings.setFlag( QgsMapSettings::UseRenderingOptimization, false ); @@ -1088,10 +1089,10 @@ QgsMapSettings QgsLayoutItemMap::mapSettings( const QgsRectangle &extent, QSizeF // layout-specific overrides of flags jobMapSettings.setFlag( QgsMapSettings::ForceVectorOutput, true ); // force vector output (no caching of marker images etc.) - jobMapSettings.setFlag( QgsMapSettings::Antialiasing, mLayout->context().flags() & QgsLayoutContext::FlagAntialiasing ); + jobMapSettings.setFlag( QgsMapSettings::Antialiasing, mLayout->renderContext().flags() & QgsLayoutRenderContext::FlagAntialiasing ); jobMapSettings.setFlag( QgsMapSettings::DrawEditingInfo, false ); jobMapSettings.setFlag( QgsMapSettings::DrawSelection, false ); - jobMapSettings.setFlag( QgsMapSettings::UseAdvancedEffects, mLayout->context().flags() & QgsLayoutContext::FlagUseAdvancedEffects ); + jobMapSettings.setFlag( QgsMapSettings::UseAdvancedEffects, mLayout->renderContext().flags() & QgsLayoutRenderContext::FlagUseAdvancedEffects ); jobMapSettings.setTransformContext( mLayout->project()->transformContext() ); jobMapSettings.setLabelingEngineSettings( mLayout->project()->labelingEngineSettings() ); @@ -1409,10 +1410,10 @@ QList QgsLayoutItemMap::layersToRender( const QgsExpressionContex } //remove atlas coverage layer if required - if ( mLayout->context().flags() & QgsLayoutContext::FlagHideCoverageLayer ) + if ( mLayout->renderContext().flags() & QgsLayoutRenderContext::FlagHideCoverageLayer ) { //hiding coverage layer - int removeAt = renderLayers.indexOf( mLayout->context().layer() ); + int removeAt = renderLayers.indexOf( mLayout->reportContext().layer() ); if ( removeAt != -1 ) { renderLayers.removeAt( removeAt ); @@ -1630,7 +1631,7 @@ void QgsLayoutItemMap::drawMapBackground( QPainter *p ) bool QgsLayoutItemMap::shouldDrawPart( QgsLayoutItemMap::PartType part ) const { - int currentExportLayer = mLayout->context().currentExportLayer(); + int currentExportLayer = mLayout->renderContext().currentExportLayer(); if ( -1 == currentExportLayer ) { @@ -1810,7 +1811,7 @@ void QgsLayoutItemMap::refreshMapExtents( const QgsExpressionContext *context ) void QgsLayoutItemMap::updateAtlasFeature() { - if ( !atlasDriven() || !mLayout->context().layer() ) + if ( !atlasDriven() || !mLayout->reportContext().layer() ) return; // nothing to do QgsRectangle bounds = computeAtlasRectangle(); @@ -1825,7 +1826,7 @@ void QgsLayoutItemMap::updateAtlasFeature() QgsRectangle originalExtent = mExtent; //sanity check - only allow fixed scale mode for point layers - bool isPointLayer = QgsWkbTypes::geometryType( mLayout->context().layer()->wkbType() ) == QgsWkbTypes::PointGeometry; + bool isPointLayer = QgsWkbTypes::geometryType( mLayout->reportContext().layer()->wkbType() ) == QgsWkbTypes::PointGeometry; if ( mAtlasScalingMode == Fixed || mAtlasScalingMode == Predefined || isPointLayer ) { @@ -1856,7 +1857,7 @@ void QgsLayoutItemMap::updateAtlasFeature() // choose one of the predefined scales double newWidth = originalExtent.width(); double newHeight = originalExtent.height(); - QVector scales = mLayout->context().predefinedScales(); + QVector scales = mLayout->reportContext().predefinedScales(); for ( int i = 0; i < scales.size(); i++ ) { double ratio = scales[i] / originalScale; @@ -1925,7 +1926,7 @@ QgsRectangle QgsLayoutItemMap::computeAtlasRectangle() // QgsGeometry::boundingBox is expressed in the geometry"s native CRS // We have to transform the geometry to the destination CRS and ask for the bounding box // Note: we cannot directly take the transformation of the bounding box, since transformations are not linear - QgsGeometry g = mLayout->context().currentGeometry( crs() ); + QgsGeometry g = mLayout->reportContext().currentGeometry( crs() ); // Rotating the geometry, so the bounding box is correct wrt map rotation if ( mEvaluatedMapRotation != 0.0 ) { diff --git a/src/core/layout/qgslayoutitemnodeitem.cpp b/src/core/layout/qgslayoutitemnodeitem.cpp index 28b8f31bfadf..3ad11a616a9d 100644 --- a/src/core/layout/qgslayoutitemnodeitem.cpp +++ b/src/core/layout/qgslayoutitemnodeitem.cpp @@ -79,7 +79,7 @@ void QgsLayoutNodesItem::draw( QgsRenderContext &context, const QStyleOptionGrap rescaleToFitBoundingBox(); _draw( context ); - if ( mDrawNodes && layout()->context().isPreviewRender() ) + if ( mDrawNodes && layout()->renderContext().isPreviewRender() ) drawNodes( context, style ); } diff --git a/src/core/layout/qgslayoutitempage.cpp b/src/core/layout/qgslayoutitempage.cpp index 7e150689c398..419ee92f4762 100644 --- a/src/core/layout/qgslayoutitempage.cpp +++ b/src/core/layout/qgslayoutitempage.cpp @@ -183,7 +183,7 @@ void QgsLayoutItemPage::redraw() void QgsLayoutItemPage::draw( QgsRenderContext &context, const QStyleOptionGraphicsItem * ) { - if ( !context.painter() || !mLayout || !mLayout->context().pagesVisible() ) + if ( !context.painter() || !mLayout || !mLayout->renderContext().pagesVisible() ) { return; } @@ -196,7 +196,7 @@ void QgsLayoutItemPage::draw( QgsRenderContext &context, const QStyleOptionGraph QPainter *painter = context.painter(); painter->save(); - if ( mLayout->context().isPreviewRender() ) + if ( mLayout->renderContext().isPreviewRender() ) { //if in preview mode, draw page border and shadow so that it's //still possible to tell where pages with a transparent style begin and end @@ -228,7 +228,7 @@ void QgsLayoutItemPage::draw( QgsRenderContext &context, const QStyleOptionGraph //Now subtract 1 pixel to prevent semi-transparent borders at edge of solid page caused by //anti-aliased painting. This may cause a pixel to be cropped from certain edge lines/symbols, //but that can be counteracted by adding a dummy transparent line symbol layer with a wider line width - if ( !mLayout->context().isPreviewRender() || !qgsDoubleNear( maxBleedPixels, 0.0 ) ) + if ( !mLayout->renderContext().isPreviewRender() || !qgsDoubleNear( maxBleedPixels, 0.0 ) ) { maxBleedPixels = std::floor( maxBleedPixels - 2 ); } @@ -276,10 +276,10 @@ void QgsLayoutItemPageGrid::paint( QPainter *painter, const QStyleOptionGraphics if ( !mLayout ) return; - if ( !mLayout->context().isPreviewRender() ) + if ( !mLayout->renderContext().isPreviewRender() ) return; - const QgsLayoutContext &context = mLayout->context(); + const QgsLayoutRenderContext &context = mLayout->renderContext(); const QgsLayoutGridSettings &grid = mLayout->gridSettings(); if ( !context.gridVisible() || grid.resolution().length() <= 0 ) diff --git a/src/core/layout/qgslayoutitempicture.cpp b/src/core/layout/qgslayoutitempicture.cpp index 2d2a86e79002..e0c65f8c6ef0 100644 --- a/src/core/layout/qgslayoutitempicture.cpp +++ b/src/core/layout/qgslayoutitempicture.cpp @@ -18,7 +18,8 @@ #include "qgslayoutitempicture.h" #include "qgslayoutitemregistry.h" #include "qgslayout.h" -#include "qgslayoutcontext.h" +#include "qgslayoutrendercontext.h" +#include "qgslayoutreportcontext.h" #include "qgslayoutitemmap.h" #include "qgslayoututils.h" #include "qgsproject.h" @@ -56,10 +57,10 @@ QgsLayoutItemPicture::QgsLayoutItemPicture( QgsLayout *layout ) //connect to atlas feature changing //to update the picture source expression - connect( &layout->context(), &QgsLayoutContext::changed, this, [ = ] { refreshPicture(); } ); + connect( &layout->reportContext(), &QgsLayoutReportContext::changed, this, [ = ] { refreshPicture(); } ); //connect to layout print resolution changing - connect( &layout->context(), &QgsLayoutContext::dpiChanged, this, &QgsLayoutItemPicture::recalculateSize ); + connect( &layout->renderContext(), &QgsLayoutRenderContext::dpiChanged, this, &QgsLayoutItemPicture::recalculateSize ); connect( this, &QgsLayoutItem::sizePositionChanged, this, &QgsLayoutItemPicture::shapeChanged ); } @@ -117,8 +118,8 @@ void QgsLayoutItemPicture::draw( QgsRenderContext &context, const QStyleOptionGr { boundRectWidthMM = rect().width(); boundRectHeightMM = rect().height(); - imageRect = QRect( 0, 0, mLayout->convertFromLayoutUnits( rect().width(), QgsUnitTypes::LayoutMillimeters ).length() * mLayout->context().dpi() / 25.4, - mLayout->convertFromLayoutUnits( rect().height(), QgsUnitTypes::LayoutMillimeters ).length() * mLayout->context().dpi() / 25.4 ); + imageRect = QRect( 0, 0, mLayout->convertFromLayoutUnits( rect().width(), QgsUnitTypes::LayoutMillimeters ).length() * mLayout->renderContext().dpi() / 25.4, + mLayout->convertFromLayoutUnits( rect().height(), QgsUnitTypes::LayoutMillimeters ).length() * mLayout->renderContext().dpi() / 25.4 ); } //zoom mode - calculate anchor point and rotation @@ -246,8 +247,8 @@ QSizeF QgsLayoutItemPicture::applyItemSizeConstraint( const QSizeF &targetSize ) if ( !( currentPictureSize.isEmpty() ) ) { QgsLayoutSize sizeMM = mLayout->convertFromLayoutUnits( currentPictureSize, QgsUnitTypes::LayoutMillimeters ); - newSize.setWidth( sizeMM.width() * 25.4 / mLayout->context().dpi() ); - newSize.setHeight( sizeMM.height() * 25.4 / mLayout->context().dpi() ); + newSize.setWidth( sizeMM.width() * 25.4 / mLayout->renderContext().dpi() ); + newSize.setHeight( sizeMM.height() * 25.4 / mLayout->renderContext().dpi() ); } } @@ -276,12 +277,12 @@ QSizeF QgsLayoutItemPicture::applyItemSizeConstraint( const QSizeF &targetSize ) QRect QgsLayoutItemPicture::clippedImageRect( double &boundRectWidthMM, double &boundRectHeightMM, QSize imageRectPixels ) { - int boundRectWidthPixels = boundRectWidthMM * mLayout->context().dpi() / 25.4; - int boundRectHeightPixels = boundRectHeightMM * mLayout->context().dpi() / 25.4; + int boundRectWidthPixels = boundRectWidthMM * mLayout->renderContext().dpi() / 25.4; + int boundRectHeightPixels = boundRectHeightMM * mLayout->renderContext().dpi() / 25.4; //update boundRectWidth/Height so that they exactly match pixel bounds - boundRectWidthMM = boundRectWidthPixels * 25.4 / mLayout->context().dpi(); - boundRectHeightMM = boundRectHeightPixels * 25.4 / mLayout->context().dpi(); + boundRectWidthMM = boundRectWidthPixels * 25.4 / mLayout->renderContext().dpi(); + boundRectHeightMM = boundRectHeightPixels * 25.4 / mLayout->renderContext().dpi(); //calculate part of image which fits in bounds int leftClip = 0; diff --git a/src/core/layout/qgslayoutitempolygon.cpp b/src/core/layout/qgslayoutitempolygon.cpp index 1aafd0f44dd7..3cee18d164e3 100644 --- a/src/core/layout/qgslayoutitempolygon.cpp +++ b/src/core/layout/qgslayoutitempolygon.cpp @@ -80,8 +80,8 @@ void QgsLayoutItemPolygon::refreshSymbol() { if ( layout() ) { - QgsRenderContext rc = QgsLayoutUtils::createRenderContextForLayout( layout(), nullptr, layout()->context().dpi() ); - mMaxSymbolBleed = ( 25.4 / layout()->context().dpi() ) * QgsSymbolLayerUtils::estimateMaxSymbolBleed( mPolygonStyleSymbol.get(), rc ); + QgsRenderContext rc = QgsLayoutUtils::createRenderContextForLayout( layout(), nullptr, layout()->renderContext().dpi() ); + mMaxSymbolBleed = ( 25.4 / layout()->renderContext().dpi() ) * QgsSymbolLayerUtils::estimateMaxSymbolBleed( mPolygonStyleSymbol.get(), rc ); } updateSceneRect(); diff --git a/src/core/layout/qgslayoutitempolyline.cpp b/src/core/layout/qgslayoutitempolyline.cpp index 311217859ce2..76d4355c4f21 100644 --- a/src/core/layout/qgslayoutitempolyline.cpp +++ b/src/core/layout/qgslayoutitempolyline.cpp @@ -108,8 +108,8 @@ void QgsLayoutItemPolyline::refreshSymbol() { if ( layout() ) { - QgsRenderContext rc = QgsLayoutUtils::createRenderContextForLayout( layout(), nullptr, layout()->context().dpi() ); - mMaxSymbolBleed = ( 25.4 / layout()->context().dpi() ) * QgsSymbolLayerUtils::estimateMaxSymbolBleed( mPolylineStyleSymbol.get(), rc ); + QgsRenderContext rc = QgsLayoutUtils::createRenderContextForLayout( layout(), nullptr, layout()->renderContext().dpi() ); + mMaxSymbolBleed = ( 25.4 / layout()->renderContext().dpi() ) * QgsSymbolLayerUtils::estimateMaxSymbolBleed( mPolylineStyleSymbol.get(), rc ); } updateSceneRect(); diff --git a/src/core/layout/qgslayoutitemscalebar.cpp b/src/core/layout/qgslayoutitemscalebar.cpp index 18717f6031b1..7e5134b90e5d 100644 --- a/src/core/layout/qgslayoutitemscalebar.cpp +++ b/src/core/layout/qgslayoutitemscalebar.cpp @@ -473,7 +473,7 @@ void QgsLayoutItemScaleBar::resizeToMinimumWidth() double widthMM = mStyle->calculateBoxSize( mSettings, createScaleContext() ).width(); QgsLayoutSize currentSize = sizeWithUnits(); - currentSize.setWidth( mLayout->context().measurementConverter().convert( QgsLayoutMeasurement( widthMM, QgsUnitTypes::LayoutMillimeters ), currentSize.units() ).length() ); + currentSize.setWidth( mLayout->renderContext().measurementConverter().convert( QgsLayoutMeasurement( widthMM, QgsUnitTypes::LayoutMillimeters ), currentSize.units() ).length() ); attemptResize( currentSize ); update(); emit changed(); diff --git a/src/core/layout/qgslayoutitemshape.cpp b/src/core/layout/qgslayoutitemshape.cpp index 6bf7c3ca7453..621d9a6aa2b6 100644 --- a/src/core/layout/qgslayoutitemshape.cpp +++ b/src/core/layout/qgslayoutitemshape.cpp @@ -105,8 +105,8 @@ void QgsLayoutItemShape::refreshSymbol() { if ( layout() ) { - QgsRenderContext rc = QgsLayoutUtils::createRenderContextForLayout( layout(), nullptr, layout()->context().dpi() ); - mMaxSymbolBleed = ( 25.4 / layout()->context().dpi() ) * QgsSymbolLayerUtils::estimateMaxSymbolBleed( mShapeStyleSymbol.get(), rc ); + QgsRenderContext rc = QgsLayoutUtils::createRenderContextForLayout( layout(), nullptr, layout()->renderContext().dpi() ); + mMaxSymbolBleed = ( 25.4 / layout()->renderContext().dpi() ) * QgsSymbolLayerUtils::estimateMaxSymbolBleed( mShapeStyleSymbol.get(), rc ); } updateBoundingRect(); diff --git a/src/core/layout/qgslayoutobject.cpp b/src/core/layout/qgslayoutobject.cpp index 5693b9c6c0ac..255b1c271588 100644 --- a/src/core/layout/qgslayoutobject.cpp +++ b/src/core/layout/qgslayoutobject.cpp @@ -18,7 +18,8 @@ #include #include "qgslayout.h" -#include "qgslayoutcontext.h" +#include "qgslayoutrendercontext.h" +#include "qgslayoutreportcontext.h" #include "qgslayoutobject.h" @@ -93,7 +94,7 @@ QgsLayoutObject::QgsLayoutObject( QgsLayout *layout ) if ( mLayout ) { connect( mLayout, &QgsLayout::refreshed, this, &QgsLayoutObject::refresh ); - connect( &mLayout->context(), &QgsLayoutContext::changed, this, &QgsLayoutObject::refresh ); + connect( &mLayout->reportContext(), &QgsLayoutReportContext::changed, this, &QgsLayoutObject::refresh ); } } diff --git a/src/core/layout/qgslayoutrendercontext.cpp b/src/core/layout/qgslayoutrendercontext.cpp new file mode 100644 index 000000000000..9b6f674e0276 --- /dev/null +++ b/src/core/layout/qgslayoutrendercontext.cpp @@ -0,0 +1,111 @@ +/*************************************************************************** + qgslayoutrendercontext.cpp + -------------------- + begin : July 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgslayoutrendercontext.h" +#include "qgsfeature.h" +#include "qgslayout.h" + +QgsLayoutRenderContext::QgsLayoutRenderContext( QgsLayout *layout ) + : QObject( layout ) + , mFlags( FlagAntialiasing | FlagUseAdvancedEffects ) + , mLayout( layout ) +{} + +void QgsLayoutRenderContext::setFlags( const QgsLayoutRenderContext::Flags flags ) +{ + if ( flags == mFlags ) + return; + + mFlags = flags; + emit flagsChanged( mFlags ); +} + +void QgsLayoutRenderContext::setFlag( const QgsLayoutRenderContext::Flag flag, const bool on ) +{ + Flags newFlags = mFlags; + if ( on ) + newFlags |= flag; + else + newFlags &= ~flag; + + if ( newFlags == mFlags ) + return; + + mFlags = newFlags; + emit flagsChanged( mFlags ); +} + +QgsLayoutRenderContext::Flags QgsLayoutRenderContext::flags() const +{ + return mFlags; +} + +bool QgsLayoutRenderContext::testFlag( const QgsLayoutRenderContext::Flag flag ) const +{ + return mFlags.testFlag( flag ); +} + +QgsRenderContext::Flags QgsLayoutRenderContext::renderContextFlags() const +{ + QgsRenderContext::Flags flags = nullptr; + if ( mFlags & FlagAntialiasing ) + flags = flags | QgsRenderContext::Antialiasing; + if ( mFlags & FlagUseAdvancedEffects ) + flags = flags | QgsRenderContext::UseAdvancedEffects; + + // TODO - expose as layout context flag? + flags |= QgsRenderContext::ForceVectorOutput; + return flags; +} + +void QgsLayoutRenderContext::setDpi( double dpi ) +{ + if ( dpi == mMeasurementConverter.dpi() ) + return; + + mMeasurementConverter.setDpi( dpi ); + emit dpiChanged(); +} + +double QgsLayoutRenderContext::dpi() const +{ + return mMeasurementConverter.dpi(); +} + +bool QgsLayoutRenderContext::gridVisible() const +{ + return mGridVisible; +} + +void QgsLayoutRenderContext::setGridVisible( bool visible ) +{ + mGridVisible = visible; +} + +bool QgsLayoutRenderContext::boundingBoxesVisible() const +{ + return mBoundingBoxesVisible; +} + +void QgsLayoutRenderContext::setBoundingBoxesVisible( bool visible ) +{ + mBoundingBoxesVisible = visible; +} + +void QgsLayoutRenderContext::setPagesVisible( bool visible ) +{ + mPagesVisible = visible; +} diff --git a/src/core/layout/qgslayoutcontext.h b/src/core/layout/qgslayoutrendercontext.h similarity index 68% rename from src/core/layout/qgslayoutcontext.h rename to src/core/layout/qgslayoutrendercontext.h index dcdda75caf1a..c36a6b967db6 100644 --- a/src/core/layout/qgslayoutcontext.h +++ b/src/core/layout/qgslayoutrendercontext.h @@ -1,5 +1,5 @@ /*************************************************************************** - qgslayoutcontext.h + qgslayoutrendercontext.h ------------------- begin : July 2017 copyright : (C) 2017 by Nyall Dawson @@ -13,25 +13,23 @@ * (at your option) any later version. * * * ***************************************************************************/ -#ifndef QGSLAYOUTCONTEXT_H -#define QGSLAYOUTCONTEXT_H +#ifndef QGSLAYOUTRENDERCONTEXT_H +#define QGSLAYOUTRENDERCONTEXT_H #include "qgis_core.h" -#include "qgsfeature.h" -#include "qgsvectorlayer.h" #include "qgslayoutmeasurementconverter.h" +#include "qgsrendercontext.h" #include -class QgsFeature; -class QgsVectorLayer; +class QgsLayout; /** * \ingroup core - * \class QgsLayoutContext - * \brief Stores information relating to the current context and rendering settings for a layout. + * \class QgsLayoutRenderContext + * \brief Stores information relating to the current rendering settings for a layout. * \since QGIS 3.0 */ -class CORE_EXPORT QgsLayoutContext : public QObject +class CORE_EXPORT QgsLayoutRenderContext : public QObject { Q_OBJECT @@ -51,9 +49,9 @@ class CORE_EXPORT QgsLayoutContext : public QObject Q_DECLARE_FLAGS( Flags, Flag ) /** - * Constructor for QgsLayoutContext. + * Constructor for QgsLayoutRenderContext. */ - QgsLayoutContext( QgsLayout *layout SIP_TRANSFERTHIS ); + QgsLayoutRenderContext( QgsLayout *layout SIP_TRANSFERTHIS ); /** * Sets the combination of \a flags that will be used for rendering the layout. @@ -61,7 +59,7 @@ class CORE_EXPORT QgsLayoutContext : public QObject * \see flags() * \see testFlag() */ - void setFlags( const QgsLayoutContext::Flags flags ); + void setFlags( const QgsLayoutRenderContext::Flags flags ); /** * Enables or disables a particular rendering \a flag for the layout. Other existing @@ -70,7 +68,7 @@ class CORE_EXPORT QgsLayoutContext : public QObject * \see flags() * \see testFlag() */ - void setFlag( const QgsLayoutContext::Flag flag, const bool on = true ); + void setFlag( const QgsLayoutRenderContext::Flag flag, const bool on = true ); /** * Returns the current combination of flags used for rendering the layout. @@ -78,7 +76,7 @@ class CORE_EXPORT QgsLayoutContext : public QObject * \see setFlag() * \see testFlag() */ - QgsLayoutContext::Flags flags() const; + QgsLayoutRenderContext::Flags flags() const; /** * Check whether a particular rendering \a flag is enabled for the layout. @@ -93,52 +91,6 @@ class CORE_EXPORT QgsLayoutContext : public QObject */ QgsRenderContext::Flags renderContextFlags() const; - /** - * Sets the current \a feature for evaluating the layout. This feature may - * be used for altering an item's content and appearance for a report - * or atlas layout. - * - * Emits the changed() signal. - * - * \see feature() - */ - void setFeature( const QgsFeature &feature ); - - /** - * Returns the current feature for evaluating the layout. This feature may - * be used for altering an item's content and appearance for a report - * or atlas layout. - * \see currentGeometry() - * \see setFeature() - */ - QgsFeature feature() const { return mFeature; } - - /** - * Returns the current feature() geometry in the given \a crs. - * If no CRS is specified, the original feature geometry is returned. - * - * Reprojection only works if a valid layer is set for layer(). - * - * \see feature() - * \see layer() - */ - QgsGeometry currentGeometry( const QgsCoordinateReferenceSystem &crs = QgsCoordinateReferenceSystem() ) const; - - /** - * Returns the vector layer associated with the layout's context. - * \see setLayer() - */ - QgsVectorLayer *layer() const; - - /** - * Sets the vector \a layer associated with the layout's context. - * - * Emits the changed() signal. - * - * \see layer() - */ - void setLayer( QgsVectorLayer *layer ); - /** * Sets the \a dpi for outputting the layout. This also sets the * corresponding DPI for the context's measurementConverter(). @@ -233,37 +185,13 @@ class CORE_EXPORT QgsLayoutContext : public QObject */ int currentExportLayer() const { return mCurrentExportLayer; } - /** - * Sets the list of predefined \a scales to use with the layout. This is used - * for maps which are set to the predefined atlas scaling mode. - * \see predefinedScales() - */ - void setPredefinedScales( const QVector &scales ); - - /** - * Returns the current list of predefined scales for use with the layout. - * \see setPredefinedScales() - */ - QVector predefinedScales() const { return mPredefinedScales; } - signals: /** * Emitted whenever the context's \a flags change. * \see setFlags() */ - void flagsChanged( QgsLayoutContext::Flags flags ); - - /** - * Emitted when the context's \a layer is changed. - */ - void layerChanged( QgsVectorLayer *layer ); - - /** - * Emitted certain settings in the context is changed, e.g. by setting a new feature or vector layer - * for the context. - */ - void changed(); + void flagsChanged( QgsLayoutRenderContext::Flags flags ); /** * Emitted when the context's DPI is changed. @@ -278,9 +206,6 @@ class CORE_EXPORT QgsLayoutContext : public QObject int mCurrentExportLayer = -1; - QgsFeature mFeature; - QPointer< QgsVectorLayer > mLayer; - QgsLayoutMeasurementConverter mMeasurementConverter; bool mIsPreviewRender = true; @@ -288,22 +213,15 @@ class CORE_EXPORT QgsLayoutContext : public QObject bool mBoundingBoxesVisible = true; bool mPagesVisible = true; - // projected geometry cache - mutable QMap mGeometryCache; - - //list of predefined scales - QVector mPredefinedScales; - friend class QgsLayoutExporter; friend class TestQgsLayout; friend class LayoutContextPreviewSettingRestorer; - }; -Q_DECLARE_METATYPE( QgsLayoutContext::Flags ) +Q_DECLARE_METATYPE( QgsLayoutRenderContext::Flags ) -#endif //QGSLAYOUTCONTEXT_H +#endif //QGSLAYOUTRENDERCONTEXT_H diff --git a/src/core/layout/qgslayoutreportcontext.cpp b/src/core/layout/qgslayoutreportcontext.cpp new file mode 100644 index 000000000000..281098c22825 --- /dev/null +++ b/src/core/layout/qgslayoutreportcontext.cpp @@ -0,0 +1,82 @@ +/*************************************************************************** + qgslayoutreportcontext.cpp + -------------------- + begin : July 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgslayoutreportcontext.h" +#include "qgsfeature.h" +#include "qgslayout.h" + +QgsLayoutReportContext::QgsLayoutReportContext( QgsLayout *layout ) + : QObject( layout ) + , mLayout( layout ) +{} + +void QgsLayoutReportContext::setFeature( const QgsFeature &feature ) +{ + mFeature = feature; + mGeometryCache.clear(); + emit changed(); +} + +QgsGeometry QgsLayoutReportContext::currentGeometry( const QgsCoordinateReferenceSystem &crs ) const +{ + if ( !crs.isValid() ) + { + // no projection, return the native geometry + return mFeature.geometry(); + } + + if ( !mLayer || !mFeature.isValid() || !mFeature.hasGeometry() ) + { + return QgsGeometry(); + } + + if ( mLayer->crs() == crs ) + { + // no projection, return the native geometry + return mFeature.geometry(); + } + + auto it = mGeometryCache.constFind( crs.srsid() ); + if ( it != mGeometryCache.constEnd() ) + { + // we have it in cache, return it + return it.value(); + } + + QgsGeometry transformed = mFeature.geometry(); + transformed.transform( QgsCoordinateTransform( mLayer->crs(), crs, mLayout->project() ) ); + mGeometryCache[crs.srsid()] = transformed; + return transformed; +} + +QgsVectorLayer *QgsLayoutReportContext::layer() const +{ + return mLayer; +} + +void QgsLayoutReportContext::setLayer( QgsVectorLayer *layer ) +{ + mLayer = layer; + emit layerChanged( layer ); + emit changed(); +} + +void QgsLayoutReportContext::setPredefinedScales( const QVector &scales ) +{ + mPredefinedScales = scales; + // make sure the list is sorted + std::sort( mPredefinedScales.begin(), mPredefinedScales.end() ); +} diff --git a/src/core/layout/qgslayoutreportcontext.h b/src/core/layout/qgslayoutreportcontext.h new file mode 100644 index 000000000000..5c30ca5672eb --- /dev/null +++ b/src/core/layout/qgslayoutreportcontext.h @@ -0,0 +1,135 @@ +/*************************************************************************** + qgslayoutreportcontext.h + ------------------- + begin : July 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#ifndef QGSLAYOUTREPORTCONTEXT_H +#define QGSLAYOUTREPORTCONTEXT_H + +#include "qgis_core.h" +#include "qgsfeature.h" +#include "qgsvectorlayer.h" +#include + +/** + * \ingroup core + * \class QgsLayoutReportContext + * \brief Stores information relating to the current reporting context for a layout. + * \since QGIS 3.0 + */ +class CORE_EXPORT QgsLayoutReportContext : public QObject +{ + + Q_OBJECT + + public: + + /** + * Constructor for QgsLayoutReportContext. + */ + QgsLayoutReportContext( QgsLayout *layout SIP_TRANSFERTHIS ); + + /** + * Sets the current \a feature for evaluating the layout. This feature may + * be used for altering an item's content and appearance for a report + * or atlas layout. + * + * Emits the changed() signal. + * + * \see feature() + */ + void setFeature( const QgsFeature &feature ); + + /** + * Returns the current feature for evaluating the layout. This feature may + * be used for altering an item's content and appearance for a report + * or atlas layout. + * \see currentGeometry() + * \see setFeature() + */ + QgsFeature feature() const { return mFeature; } + + /** + * Returns the current feature() geometry in the given \a crs. + * If no CRS is specified, the original feature geometry is returned. + * + * Reprojection only works if a valid layer is set for layer(). + * + * \see feature() + * \see layer() + */ + QgsGeometry currentGeometry( const QgsCoordinateReferenceSystem &crs = QgsCoordinateReferenceSystem() ) const; + + /** + * Returns the vector layer associated with the layout's context. + * \see setLayer() + */ + QgsVectorLayer *layer() const; + + /** + * Sets the vector \a layer associated with the layout's context. + * + * Emits the changed() signal. + * + * \see layer() + */ + void setLayer( QgsVectorLayer *layer ); + + /** + * Sets the list of predefined \a scales to use with the layout. This is used + * for maps which are set to the predefined atlas scaling mode. + * \see predefinedScales() + */ + void setPredefinedScales( const QVector &scales ); + + /** + * Returns the current list of predefined scales for use with the layout. + * \see setPredefinedScales() + */ + QVector predefinedScales() const { return mPredefinedScales; } + + signals: + + /** + * Emitted when the context's \a layer is changed. + */ + void layerChanged( QgsVectorLayer *layer ); + + /** + * Emitted certain settings in the context is changed, e.g. by setting a new feature or vector layer + * for the context. + */ + void changed(); + + private: + + QgsLayout *mLayout = nullptr; + + QgsFeature mFeature; + QPointer< QgsVectorLayer > mLayer; + + // projected geometry cache + mutable QMap mGeometryCache; + + //list of predefined scales + QVector mPredefinedScales; + + friend class QgsLayoutExporter; + friend class TestQgsLayout; + +}; + +#endif //QGSLAYOUTREPORTCONTEXT_H + + + diff --git a/src/core/layout/qgslayouttable.cpp b/src/core/layout/qgslayouttable.cpp index dac33081a9f8..0447d017b3cf 100644 --- a/src/core/layout/qgslayouttable.cpp +++ b/src/core/layout/qgslayouttable.cpp @@ -289,7 +289,7 @@ void QgsLayoutTable::render( QgsRenderContext &context, const QRectF &, const in return; } - if ( !mLayout->context().isPreviewRender() ) + if ( !mLayout->renderContext().isPreviewRender() ) { //exporting composition, so force an attribute refresh //we do this in case vector layer has changed via an external source (e.g., another database user) diff --git a/src/core/layout/qgslayoututils.cpp b/src/core/layout/qgslayoututils.cpp index 2d98aea2c303..7ce50b8c8c23 100644 --- a/src/core/layout/qgslayoututils.cpp +++ b/src/core/layout/qgslayoututils.cpp @@ -128,7 +128,7 @@ QgsRenderContext QgsLayoutUtils::createRenderContextForMap( QgsLayoutItemMap *ma if ( painter ) context.setPainter( painter ); - context.setFlags( map->layout()->context().renderContextFlags() ); + context.setFlags( map->layout()->renderContext().renderContextFlags() ); return context; } } @@ -138,7 +138,7 @@ QgsRenderContext QgsLayoutUtils::createRenderContextForLayout( QgsLayout *layout QgsLayoutItemMap *referenceMap = layout ? layout->referenceMap() : nullptr; QgsRenderContext context = createRenderContextForMap( referenceMap, painter, dpi ); if ( layout ) - context.setFlags( layout->context().renderContextFlags() ); + context.setFlags( layout->renderContext().renderContextFlags() ); return context; } diff --git a/src/core/qgsapplication.cpp b/src/core/qgsapplication.cpp index d5078cd4ed1f..3ea611f07692 100644 --- a/src/core/qgsapplication.cpp +++ b/src/core/qgsapplication.cpp @@ -43,7 +43,7 @@ #include "qgsuserprofilemanager.h" #include "qgsreferencedgeometry.h" #include "qgs3drendererregistry.h" -#include "qgslayoutcontext.h" +#include "qgslayoutrendercontext.h" #include "qgssqliteutils.h" #include "gps/qgsgpsconnectionregistry.h" @@ -154,7 +154,7 @@ void QgsApplication::init( QString profileFolder ) qRegisterMetaType( "QgsMessageLog::MessageLevel" ); qRegisterMetaType( "QgsReferencedRectangle" ); qRegisterMetaType( "QgsReferencedPointXY" ); - qRegisterMetaType( "QgsLayoutContext::Flags" ); + qRegisterMetaType( "QgsLayoutRenderContext::Flags" ); QString prefixPath( getenv( "QGIS_PREFIX_PATH" ) ? getenv( "QGIS_PREFIX_PATH" ) : applicationDirPath() ); // QgsDebugMsg( QString( "prefixPath(): %1" ).arg( prefixPath ) ); diff --git a/src/core/qgsexpressioncontext.cpp b/src/core/qgsexpressioncontext.cpp index 2830a74eb637..2a29cfce3053 100644 --- a/src/core/qgsexpressioncontext.cpp +++ b/src/core/qgsexpressioncontext.cpp @@ -34,6 +34,7 @@ #include "qgslayoutatlas.h" #include "qgslayout.h" #include "qgslayoutpagecollection.h" +#include "qgslayoutreportcontext.h" #include #include @@ -1124,20 +1125,20 @@ QgsExpressionContextScope *QgsExpressionContextUtils::layoutScope( const QgsLayo scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layout_pageheight" ), s.height(), true ) ); scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layout_pagewidth" ), s.width(), true ) ); } - scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layout_dpi" ), layout->context().dpi(), true ) ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layout_dpi" ), layout->renderContext().dpi(), true ) ); scope->addFunction( QStringLiteral( "item_variables" ), new GetLayoutItemVariables( layout ) ); - if ( layout->context().layer() ) + if ( layout->reportContext().layer() ) { - scope->setFields( layout->context().layer()->fields() ); - scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_layerid" ), layout->context().layer()->id(), true ) ); - scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_layername" ), layout->context().layer()->name(), true ) ); + scope->setFields( layout->reportContext().layer()->fields() ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_layerid" ), layout->reportContext().layer()->id(), true ) ); + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_layername" ), layout->reportContext().layer()->name(), true ) ); } - if ( layout->context().feature().isValid() ) + if ( layout->reportContext().feature().isValid() ) { - QgsFeature atlasFeature = layout->context().feature(); + QgsFeature atlasFeature = layout->reportContext().feature(); scope->setFeature( atlasFeature ); scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_feature" ), QVariant::fromValue( atlasFeature ), true ) ); scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_featureid" ), atlasFeature.id(), true ) ); @@ -1250,7 +1251,7 @@ QgsExpressionContextScope *QgsExpressionContextUtils::atlasScope( const QgsLayou if ( atlas->enabled() ) { - QgsFeature atlasFeature = atlas->layout()->context().feature(); + QgsFeature atlasFeature = atlas->layout()->reportContext().feature(); scope->setFeature( atlasFeature ); scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_feature" ), QVariant::fromValue( atlasFeature ), true ) ); scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_featureid" ), atlasFeature.id(), true ) ); diff --git a/src/gui/layout/qgslayoutitemwidget.cpp b/src/gui/layout/qgslayoutitemwidget.cpp index 33e08b4fc635..b5bbd58f6f13 100644 --- a/src/gui/layout/qgslayoutitemwidget.cpp +++ b/src/gui/layout/qgslayoutitemwidget.cpp @@ -31,7 +31,7 @@ QgsLayoutConfigObject::QgsLayoutConfigObject( QWidget *parent, QgsLayoutObject * { if ( mLayoutObject->layout() ) { - connect( &mLayoutObject->layout()->context(), &QgsLayoutContext::layerChanged, + connect( &mLayoutObject->layout()->reportContext(), &QgsLayoutReportContext::layerChanged, this, [ = ] { updateDataDefinedButtons(); } ); } if ( layoutAtlas() ) @@ -122,7 +122,7 @@ QgsVectorLayer *QgsLayoutConfigObject::coverageLayer() const if ( !layout ) return nullptr; - return layout->context().layer(); + return layout->reportContext().layer(); } @@ -201,15 +201,15 @@ QgsLayoutItemPropertiesWidget::QgsLayoutItemPropertiesWidget( QWidget *parent, Q mItemRotationSpinBox->setClearValue( 0 ); mStrokeUnitsComboBox->linkToWidget( mStrokeWidthSpinBox ); - mStrokeUnitsComboBox->setConverter( &item->layout()->context().measurementConverter() ); + mStrokeUnitsComboBox->setConverter( &item->layout()->renderContext().measurementConverter() ); mPosUnitsComboBox->linkToWidget( mXPosSpin ); mPosUnitsComboBox->linkToWidget( mYPosSpin ); mSizeUnitsComboBox->linkToWidget( mWidthSpin ); mSizeUnitsComboBox->linkToWidget( mHeightSpin ); - mPosUnitsComboBox->setConverter( &item->layout()->context().measurementConverter() ); - mSizeUnitsComboBox->setConverter( &item->layout()->context().measurementConverter() ); + mPosUnitsComboBox->setConverter( &item->layout()->renderContext().measurementConverter() ); + mSizeUnitsComboBox->setConverter( &item->layout()->renderContext().measurementConverter() ); mPosLockAspectRatio->setWidthSpinBox( mXPosSpin ); mPosLockAspectRatio->setHeightSpinBox( mYPosSpin ); diff --git a/src/gui/layout/qgslayoutmousehandles.cpp b/src/gui/layout/qgslayoutmousehandles.cpp index 93668d353084..366da40364ba 100644 --- a/src/gui/layout/qgslayoutmousehandles.cpp +++ b/src/gui/layout/qgslayoutmousehandles.cpp @@ -58,20 +58,20 @@ void QgsLayoutMouseHandles::paint( QPainter *painter, const QStyleOptionGraphics Q_UNUSED( itemStyle ); Q_UNUSED( pWidget ); - if ( !mLayout->context().isPreviewRender() ) + if ( !mLayout->renderContext().isPreviewRender() ) { //don't draw selection handles in layout outputs return; } - if ( mLayout->context().boundingBoxesVisible() ) + if ( mLayout->renderContext().boundingBoxesVisible() ) { //draw resize handles around bounds of entire selection double rectHandlerSize = rectHandlerBorderTolerance(); drawHandles( painter, rectHandlerSize ); } - if ( mIsResizing || mIsDragging || mLayout->context().boundingBoxesVisible() ) + if ( mIsResizing || mIsDragging || mLayout->renderContext().boundingBoxesVisible() ) { //draw dotted boxes around selected items drawSelectedItemBounds( painter ); diff --git a/src/gui/layout/qgslayoutnewitempropertiesdialog.cpp b/src/gui/layout/qgslayoutnewitempropertiesdialog.cpp index ffcd56365f56..90066129a4b5 100644 --- a/src/gui/layout/qgslayoutnewitempropertiesdialog.cpp +++ b/src/gui/layout/qgslayoutnewitempropertiesdialog.cpp @@ -176,7 +176,7 @@ void QgsLayoutItemPropertiesDialog::setReferencePoint( QgsLayoutItem::ReferenceP void QgsLayoutItemPropertiesDialog::setLayout( QgsLayout *layout ) { - mSizeUnitsComboBox->setConverter( &layout->context().measurementConverter() ); - mPosUnitsComboBox->setConverter( &layout->context().measurementConverter() ); + mSizeUnitsComboBox->setConverter( &layout->renderContext().measurementConverter() ); + mPosUnitsComboBox->setConverter( &layout->renderContext().measurementConverter() ); mLayout = layout; } diff --git a/tests/src/core/testqgslayout.cpp b/tests/src/core/testqgslayout.cpp index e9cfc816f8ed..37e5d57b2bec 100644 --- a/tests/src/core/testqgslayout.cpp +++ b/tests/src/core/testqgslayout.cpp @@ -111,8 +111,8 @@ void TestQgsLayout::units() //check with dpi conversion layout.setUnits( QgsUnitTypes::LayoutInches ); - layout.context().setDpi( 96.0 ); - QCOMPARE( layout.context().dpi(), 96.0 ); + layout.renderContext().setDpi( 96.0 ); + QCOMPARE( layout.renderContext().dpi(), 96.0 ); QCOMPARE( layout.convertToLayoutUnits( QgsLayoutMeasurement( 96, QgsUnitTypes::LayoutPixels ) ), 1.0 ); QCOMPARE( layout.convertToLayoutUnits( QgsLayoutSize( 96, 96, QgsUnitTypes::LayoutPixels ) ), QSizeF( 1.0, 1.0 ) ); QCOMPARE( layout.convertToLayoutUnits( QgsLayoutPoint( 96, 96, QgsUnitTypes::LayoutPixels ) ), QPointF( 1.0, 1.0 ) ); @@ -635,7 +635,7 @@ void TestQgsLayout::shouldExportPage() QgsLayoutItemPage *page2 = new QgsLayoutItemPage( &l ); page2->setPageSize( "A4" ); l.pageCollection()->addPage( page2 ); - l.context().mIsPreviewRender = false; + l.renderContext().mIsPreviewRender = false; QgsLayoutItemHtml *htmlItem = new QgsLayoutItemHtml( &l ); //frame on page 1 diff --git a/tests/src/core/testqgslayoutatlas.cpp b/tests/src/core/testqgslayoutatlas.cpp index 3286e618186f..6c16151391e8 100644 --- a/tests/src/core/testqgslayoutatlas.cpp +++ b/tests/src/core/testqgslayoutatlas.cpp @@ -260,9 +260,9 @@ void TestQgsLayoutAtlas::predefinedscales_render() QVector scales; scales << 1800000.0; scales << 5000000.0; - mLayout->context().setPredefinedScales( scales ); + mLayout->reportContext().setPredefinedScales( scales ); { - const QVector &setScales = mLayout->context().predefinedScales(); + const QVector &setScales = mLayout->reportContext().predefinedScales(); for ( int i = 0; i < setScales.size(); i++ ) { QVERIFY( setScales[i] == scales[i] ); diff --git a/tests/src/core/testqgslayoutcontext.cpp b/tests/src/core/testqgslayoutcontext.cpp index 807b78702f6c..0b10ac4cb8a6 100644 --- a/tests/src/core/testqgslayoutcontext.cpp +++ b/tests/src/core/testqgslayoutcontext.cpp @@ -15,7 +15,8 @@ * * ***************************************************************************/ -#include "qgslayoutcontext.h" +#include "qgslayoutrendercontext.h" +#include "qgslayoutreportcontext.h" #include "qgis.h" #include "qgsfeature.h" #include "qgsvectorlayer.h" @@ -80,40 +81,40 @@ void TestQgsLayoutContext::cleanup() void TestQgsLayoutContext::creation() { - QgsLayoutContext *context = new QgsLayoutContext( nullptr ); + QgsLayoutRenderContext *context = new QgsLayoutRenderContext( nullptr ); QVERIFY( context ); delete context; } void TestQgsLayoutContext::flags() { - QgsLayoutContext context( nullptr ); - QSignalSpy spyFlagsChanged( &context, &QgsLayoutContext::flagsChanged ); + QgsLayoutRenderContext context( nullptr ); + QSignalSpy spyFlagsChanged( &context, &QgsLayoutRenderContext::flagsChanged ); //test getting and setting flags - context.setFlags( QgsLayoutContext::Flags( QgsLayoutContext::FlagAntialiasing | QgsLayoutContext::FlagUseAdvancedEffects ) ); + context.setFlags( QgsLayoutRenderContext::Flags( QgsLayoutRenderContext::FlagAntialiasing | QgsLayoutRenderContext::FlagUseAdvancedEffects ) ); // default flags, so should be no signal QCOMPARE( spyFlagsChanged.count(), 0 ); - QVERIFY( context.flags() == ( QgsLayoutContext::FlagAntialiasing | QgsLayoutContext::FlagUseAdvancedEffects ) ); - QVERIFY( context.testFlag( QgsLayoutContext::FlagAntialiasing ) ); - QVERIFY( context.testFlag( QgsLayoutContext::FlagUseAdvancedEffects ) ); - QVERIFY( ! context.testFlag( QgsLayoutContext::FlagDebug ) ); - context.setFlag( QgsLayoutContext::FlagDebug ); + QVERIFY( context.flags() == ( QgsLayoutRenderContext::FlagAntialiasing | QgsLayoutRenderContext::FlagUseAdvancedEffects ) ); + QVERIFY( context.testFlag( QgsLayoutRenderContext::FlagAntialiasing ) ); + QVERIFY( context.testFlag( QgsLayoutRenderContext::FlagUseAdvancedEffects ) ); + QVERIFY( ! context.testFlag( QgsLayoutRenderContext::FlagDebug ) ); + context.setFlag( QgsLayoutRenderContext::FlagDebug ); QCOMPARE( spyFlagsChanged.count(), 1 ); - QVERIFY( context.testFlag( QgsLayoutContext::FlagDebug ) ); - context.setFlag( QgsLayoutContext::FlagDebug, false ); + QVERIFY( context.testFlag( QgsLayoutRenderContext::FlagDebug ) ); + context.setFlag( QgsLayoutRenderContext::FlagDebug, false ); QCOMPARE( spyFlagsChanged.count(), 2 ); - QVERIFY( ! context.testFlag( QgsLayoutContext::FlagDebug ) ); - context.setFlag( QgsLayoutContext::FlagDebug, false ); //no change + QVERIFY( ! context.testFlag( QgsLayoutRenderContext::FlagDebug ) ); + context.setFlag( QgsLayoutRenderContext::FlagDebug, false ); //no change QCOMPARE( spyFlagsChanged.count(), 2 ); - context.setFlags( QgsLayoutContext::FlagDebug ); + context.setFlags( QgsLayoutRenderContext::FlagDebug ); QCOMPARE( spyFlagsChanged.count(), 3 ); } void TestQgsLayoutContext::feature() { - QgsLayoutContext context( nullptr ); + QgsLayoutReportContext context( nullptr ); //test removing feature context.setFeature( QgsFeature() ); @@ -129,7 +130,7 @@ void TestQgsLayoutContext::feature() void TestQgsLayoutContext::layer() { - QgsLayoutContext context( nullptr ); + QgsLayoutReportContext context( nullptr ); //test clearing layer context.setLayer( nullptr ); @@ -149,9 +150,9 @@ void TestQgsLayoutContext::layer() void TestQgsLayoutContext::dpi() { - QgsLayoutContext context( nullptr ); + QgsLayoutRenderContext context( nullptr ); - QSignalSpy spyDpiChanged( &context, &QgsLayoutContext::dpiChanged ); + QSignalSpy spyDpiChanged( &context, &QgsLayoutRenderContext::dpiChanged ); context.setDpi( 600 ); QCOMPARE( context.dpi(), 600.0 ); QCOMPARE( context.measurementConverter().dpi(), 600.0 ); @@ -165,20 +166,20 @@ void TestQgsLayoutContext::dpi() void TestQgsLayoutContext::renderContextFlags() { - QgsLayoutContext context( nullptr ); + QgsLayoutRenderContext context( nullptr ); context.setFlags( 0 ); QgsRenderContext::Flags flags = context.renderContextFlags(); QVERIFY( !( flags & QgsRenderContext::Antialiasing ) ); QVERIFY( !( flags & QgsRenderContext::UseAdvancedEffects ) ); QVERIFY( ( flags & QgsRenderContext::ForceVectorOutput ) ); - context.setFlag( QgsLayoutContext::FlagAntialiasing ); + context.setFlag( QgsLayoutRenderContext::FlagAntialiasing ); flags = context.renderContextFlags(); QVERIFY( ( flags & QgsRenderContext::Antialiasing ) ); QVERIFY( !( flags & QgsRenderContext::UseAdvancedEffects ) ); QVERIFY( ( flags & QgsRenderContext::ForceVectorOutput ) ); - context.setFlag( QgsLayoutContext::FlagUseAdvancedEffects ); + context.setFlag( QgsLayoutRenderContext::FlagUseAdvancedEffects ); flags = context.renderContextFlags(); QVERIFY( ( flags & QgsRenderContext::Antialiasing ) ); QVERIFY( ( flags & QgsRenderContext::UseAdvancedEffects ) ); @@ -187,7 +188,7 @@ void TestQgsLayoutContext::renderContextFlags() void TestQgsLayoutContext::boundingBoxes() { - QgsLayoutContext context( nullptr ); + QgsLayoutRenderContext context( nullptr ); context.setBoundingBoxesVisible( false ); QVERIFY( !context.boundingBoxesVisible() ); context.setBoundingBoxesVisible( true ); @@ -196,7 +197,7 @@ void TestQgsLayoutContext::boundingBoxes() void TestQgsLayoutContext::exportLayer() { - QgsLayoutContext context( nullptr ); + QgsLayoutRenderContext context( nullptr ); // must default to -1 QCOMPARE( context.currentExportLayer(), -1 ); context.setCurrentExportLayer( 1 ); @@ -207,7 +208,7 @@ void TestQgsLayoutContext::geometry() { QgsProject p; QgsLayout l( &p ); - QgsLayoutContext context( &l ); + QgsLayoutReportContext context( &l ); // no feature set QVERIFY( context.currentGeometry().isNull() ); @@ -248,7 +249,7 @@ void TestQgsLayoutContext::scales() QVector< qreal > scales; scales << 1 << 15 << 5 << 10; - QgsLayoutContext context( nullptr ); + QgsLayoutReportContext context( nullptr ); context.setPredefinedScales( scales ); // should be sorted diff --git a/tests/src/core/testqgslayouthtml.cpp b/tests/src/core/testqgslayouthtml.cpp index 113a4b556a96..b9768b074036 100644 --- a/tests/src/core/testqgslayouthtml.cpp +++ b/tests/src/core/testqgslayouthtml.cpp @@ -271,7 +271,7 @@ void TestQgsLayoutHtml::javascriptSetFeature() //atlas QgsLayout l( QgsProject::instance() ); l.initializeDefaults(); - l.context().setLayer( parentLayer ); + l.reportContext().setLayer( parentLayer ); QgsRelation rel; rel.setId( QStringLiteral( "rel1" ) ); @@ -299,7 +299,7 @@ void TestQgsLayoutHtml::javascriptSetFeature() QgsFeature f; QgsFeatureIterator it = parentLayer->getFeatures(); it.nextFeature( f ); - l.context().setFeature( f ); + l.reportContext().setFeature( f ); htmlItem->loadHtml(); diff --git a/tests/src/core/testqgslayoutitem.cpp b/tests/src/core/testqgslayoutitem.cpp index 8daad585bf2c..b1318d66c119 100644 --- a/tests/src/core/testqgslayoutitem.cpp +++ b/tests/src/core/testqgslayoutitem.cpp @@ -296,9 +296,9 @@ void TestQgsLayoutItem::shouldDrawDebug() QgsProject p; QgsLayout l( &p ); TestItem *item = new TestItem( &l ); - l.context().setFlag( QgsLayoutContext::FlagDebug, true ); + l.renderContext().setFlag( QgsLayoutRenderContext::FlagDebug, true ); QVERIFY( item->shouldDrawDebugRect() ); - l.context().setFlag( QgsLayoutContext::FlagDebug, false ); + l.renderContext().setFlag( QgsLayoutRenderContext::FlagDebug, false ); QVERIFY( !item->shouldDrawDebugRect() ); delete item; } @@ -308,9 +308,9 @@ void TestQgsLayoutItem::shouldDrawAntialiased() QgsProject p; QgsLayout l( &p ); TestItem *item = new TestItem( &l ); - l.context().setFlag( QgsLayoutContext::FlagAntialiasing, false ); + l.renderContext().setFlag( QgsLayoutRenderContext::FlagAntialiasing, false ); QVERIFY( !item->shouldDrawAntialiased() ); - l.context().setFlag( QgsLayoutContext::FlagAntialiasing, true ); + l.renderContext().setFlag( QgsLayoutRenderContext::FlagAntialiasing, true ); QVERIFY( item->shouldDrawAntialiased() ); delete item; } @@ -327,10 +327,10 @@ void TestQgsLayoutItem::preparePainter() QImage image( QSize( 100, 100 ), QImage::Format_ARGB32 ); QPainter painter; painter.begin( &image ); - l.context().setFlag( QgsLayoutContext::FlagAntialiasing, false ); + l.renderContext().setFlag( QgsLayoutRenderContext::FlagAntialiasing, false ); item->preparePainter( &painter ); QVERIFY( !( painter.renderHints() & QPainter::Antialiasing ) ); - l.context().setFlag( QgsLayoutContext::FlagAntialiasing, true ); + l.renderContext().setFlag( QgsLayoutRenderContext::FlagAntialiasing, true ); item->preparePainter( &painter ); QVERIFY( painter.renderHints() & QPainter::Antialiasing ); delete item; @@ -345,7 +345,7 @@ void TestQgsLayoutItem::debugRect() item->setPos( 100, 100 ); item->setRect( 0, 0, 200, 200 ); l.setSceneRect( 0, 0, 400, 400 ); - l.context().setFlag( QgsLayoutContext::FlagDebug, true ); + l.renderContext().setFlag( QgsLayoutRenderContext::FlagDebug, true ); QImage image( l.sceneRect().size().toSize(), QImage::Format_ARGB32 ); image.fill( 0 ); QPainter painter( &image ); @@ -365,7 +365,7 @@ void TestQgsLayoutItem::draw() item->setPos( 100, 100 ); item->setRect( 0, 0, 200, 200 ); l.setSceneRect( 0, 0, 400, 400 ); - l.context().setFlag( QgsLayoutContext::FlagAntialiasing, false ); //disable antialiasing to limit cross platform differences + l.renderContext().setFlag( QgsLayoutRenderContext::FlagAntialiasing, false ); //disable antialiasing to limit cross platform differences QImage image( l.sceneRect().size().toSize(), QImage::Format_ARGB32 ); image.fill( 0 ); QPainter painter( &image ); @@ -750,7 +750,7 @@ void TestQgsLayoutItem::resize() //test pixel -> page conversion l.setUnits( QgsUnitTypes::LayoutInches ); - l.context().setDpi( 100.0 ); + l.renderContext().setDpi( 100.0 ); item->refresh(); QCOMPARE( spySizeChanged.count(), 6 ); item->setRect( 0, 0, 1, 2 ); @@ -759,7 +759,7 @@ void TestQgsLayoutItem::resize() QCOMPARE( item->rect().height(), 2.8 ); QCOMPARE( spySizeChanged.count(), 7 ); //changing the dpi should resize the item - l.context().setDpi( 200.0 ); + l.renderContext().setDpi( 200.0 ); item->refresh(); QCOMPARE( item->rect().width(), 0.7 ); QCOMPARE( item->rect().height(), 1.4 ); @@ -767,7 +767,7 @@ void TestQgsLayoutItem::resize() //test page -> pixel conversion l.setUnits( QgsUnitTypes::LayoutPixels ); - l.context().setDpi( 100.0 ); + l.renderContext().setDpi( 100.0 ); item->refresh(); item->setRect( 0, 0, 2, 2 ); QCOMPARE( spySizeChanged.count(), 10 ); @@ -776,7 +776,7 @@ void TestQgsLayoutItem::resize() QCOMPARE( item->rect().height(), 300.0 ); QCOMPARE( spySizeChanged.count(), 11 ); //changing dpi results in item resize - l.context().setDpi( 200.0 ); + l.renderContext().setDpi( 200.0 ); item->refresh(); QCOMPARE( item->rect().width(), 200.0 ); QCOMPARE( item->rect().height(), 600.0 ); @@ -1172,28 +1172,28 @@ void TestQgsLayoutItem::move() //test pixel -> page conversion l.setUnits( QgsUnitTypes::LayoutInches ); - l.context().setDpi( 100.0 ); + l.renderContext().setDpi( 100.0 ); item->refresh(); item->setPos( 1, 2 ); item->attemptMove( QgsLayoutPoint( 140, 280, QgsUnitTypes::LayoutPixels ) ); QCOMPARE( item->scenePos().x(), 1.4 ); QCOMPARE( item->scenePos().y(), 2.8 ); //changing the dpi should move the item - l.context().setDpi( 200.0 ); + l.renderContext().setDpi( 200.0 ); item->refresh(); QCOMPARE( item->scenePos().x(), 0.7 ); QCOMPARE( item->scenePos().y(), 1.4 ); //test page -> pixel conversion l.setUnits( QgsUnitTypes::LayoutPixels ); - l.context().setDpi( 100.0 ); + l.renderContext().setDpi( 100.0 ); item->refresh(); item->setPos( 2, 2 ); item->attemptMove( QgsLayoutPoint( 1, 3, QgsUnitTypes::LayoutInches ) ); QCOMPARE( item->scenePos().x(), 100.0 ); QCOMPARE( item->scenePos().y(), 300.0 ); //changing dpi results in item move - l.context().setDpi( 200.0 ); + l.renderContext().setDpi( 200.0 ); item->refresh(); QCOMPARE( item->scenePos().x(), 200.0 ); QCOMPARE( item->scenePos().y(), 600.0 ); @@ -1548,7 +1548,7 @@ void TestQgsLayoutItem::rotation() l.addItem( item ); item->setItemRotation( 45 ); l.setSceneRect( 0, 0, 400, 400 ); - l.context().setFlag( QgsLayoutContext::FlagDebug, true ); + l.renderContext().setFlag( QgsLayoutRenderContext::FlagDebug, true ); QImage image( l.sceneRect().size().toSize(), QImage::Format_ARGB32 ); image.fill( 0 ); QPainter painter( &image ); @@ -1833,9 +1833,9 @@ void TestQgsLayoutItem::blendMode() QCOMPARE( item->blendMode(), QPainter::CompositionMode_Darken ); QVERIFY( item->mEffect->isEnabled() ); - l.context().setFlag( QgsLayoutContext::FlagUseAdvancedEffects, false ); + l.renderContext().setFlag( QgsLayoutRenderContext::FlagUseAdvancedEffects, false ); QVERIFY( !item->mEffect->isEnabled() ); - l.context().setFlag( QgsLayoutContext::FlagUseAdvancedEffects, true ); + l.renderContext().setFlag( QgsLayoutRenderContext::FlagUseAdvancedEffects, true ); QVERIFY( item->mEffect->isEnabled() ); item->dataDefinedProperties().setProperty( QgsLayoutObject::BlendMode, QgsProperty::fromExpression( "'lighten'" ) ); diff --git a/tests/src/core/testqgslayoutlabel.cpp b/tests/src/core/testqgslayoutlabel.cpp index 39beb601e89d..445f7db95e97 100644 --- a/tests/src/core/testqgslayoutlabel.cpp +++ b/tests/src/core/testqgslayoutlabel.cpp @@ -102,7 +102,7 @@ void TestQgsLayoutLabel::evaluation() QgsLayout l( QgsProject::instance() ); l.initializeDefaults(); - l.context().setLayer( mVectorLayer ); + l.reportContext().setLayer( mVectorLayer ); QgsLayoutItemLabel *label = new QgsLayoutItemLabel( &l ); label->setMargin( 1 ); @@ -188,9 +188,9 @@ void TestQgsLayoutLabel::feature_evaluation2() QgsFeature f; QgsFeatureIterator it = mVectorLayer->getFeatures(); - l.context().setLayer( mVectorLayer ); + l.reportContext().setLayer( mVectorLayer ); it.nextFeature( f ); - l.context().setFeature( f ); + l.reportContext().setFeature( f ); { // evaluation with a feature label->setText( QStringLiteral( "[%\"NAME_1\"||'_ok'%]" ) ); @@ -199,7 +199,7 @@ void TestQgsLayoutLabel::feature_evaluation2() QCOMPARE( evaluated, expected ); } it.nextFeature( f ); - l.context().setFeature( f ); + l.reportContext().setFeature( f ); { // evaluation with a feature label->setText( QStringLiteral( "[%\"NAME_1\"||'_ok'%]" ) ); @@ -216,7 +216,7 @@ void TestQgsLayoutLabel::page_evaluation() QgsLayoutItemPage *page2 = new QgsLayoutItemPage( &l ); page2->setPageSize( "A4", QgsLayoutItemPage::Landscape ); l.pageCollection()->addPage( page2 ); - l.context().setLayer( mVectorLayer ); + l.reportContext().setLayer( mVectorLayer ); QgsLayoutItemLabel *label = new QgsLayoutItemLabel( &l ); label->setMargin( 1 ); diff --git a/tests/src/core/testqgslayoutmap.cpp b/tests/src/core/testqgslayoutmap.cpp index 5589b031f8ba..58ae3d0b1874 100644 --- a/tests/src/core/testqgslayoutmap.cpp +++ b/tests/src/core/testqgslayoutmap.cpp @@ -349,23 +349,23 @@ void TestQgsLayoutMap::dataDefinedLayers() f2.setAttribute( QStringLiteral( "col1" ), mPointsLayer->name() ); atlasLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 ); - l.context().setLayer( atlasLayer ); + l.reportContext().setLayer( atlasLayer ); QgsFeature f; QgsFeatureIterator it = atlasLayer->getFeatures(); it.nextFeature( f ); - l.context().setFeature( f ); + l.reportContext().setFeature( f ); map->dataDefinedProperties().setProperty( QgsLayoutObject::MapLayers, QgsProperty::fromField( QStringLiteral( "col1" ) ) ); result = map->layersToRender(); QCOMPARE( result.count(), 1 ); QCOMPARE( result.at( 0 ), mLinesLayer ); it.nextFeature( f ); - l.context().setFeature( f ); + l.reportContext().setFeature( f ); result = map->layersToRender(); QCOMPARE( result.count(), 1 ); QCOMPARE( result.at( 0 ), mPointsLayer ); it.nextFeature( f ); - l.context().setFeature( f ); + l.reportContext().setFeature( f ); delete atlasLayer; @@ -518,11 +518,11 @@ void TestQgsLayoutMap::layersToRender() QCOMPARE( map->layersToRender(), layers ); // hide coverage layer - l.context().setLayer( mPointsLayer ); - l.context().setFlag( QgsLayoutContext::FlagHideCoverageLayer, true ); + l.reportContext().setLayer( mPointsLayer ); + l.renderContext().setFlag( QgsLayoutRenderContext::FlagHideCoverageLayer, true ); QCOMPARE( map->layersToRender(), layers2 ); - l.context().setFlag( QgsLayoutContext::FlagHideCoverageLayer, false ); + l.renderContext().setFlag( QgsLayoutRenderContext::FlagHideCoverageLayer, false ); QCOMPARE( map->layersToRender(), layers ); } diff --git a/tests/src/core/testqgslayoutpage.cpp b/tests/src/core/testqgslayoutpage.cpp index 962a3a1f57a4..8ce9a6e189e3 100644 --- a/tests/src/core/testqgslayoutpage.cpp +++ b/tests/src/core/testqgslayoutpage.cpp @@ -244,7 +244,7 @@ void TestQgsLayoutPage::hiddenPages() simpleFill->setStrokeColor( Qt::transparent ); l.pageCollection()->setPageStyleSymbol( fillSymbol.get() ); - l.context().setPagesVisible( false ); + l.renderContext().setPagesVisible( false ); QgsLayoutChecker checker( QStringLiteral( "composerpaper_hidden" ), &l ); checker.setControlPathPrefix( QStringLiteral( "composer_paper" ) ); diff --git a/tests/src/core/testqgslayouttable.cpp b/tests/src/core/testqgslayouttable.cpp index 526e94138217..af5b188b71f1 100644 --- a/tests/src/core/testqgslayouttable.cpp +++ b/tests/src/core/testqgslayouttable.cpp @@ -533,12 +533,12 @@ void TestQgsLayoutTable::attributeTableAtlasSource() vectorFileInfo.completeBaseName(), QStringLiteral( "ogr" ) ); QgsProject::instance()->addMapLayer( vectorLayer ); - l.context().setLayer( vectorLayer ); + l.reportContext().setLayer( vectorLayer ); QgsFeature f; QgsFeatureIterator it = vectorLayer->getFeatures(); it.nextFeature( f ); - l.context().setFeature( f ); + l.reportContext().setFeature( f ); QCOMPARE( table->contents().length(), 1 ); QgsLayoutTableRow row = table->contents().at( 0 ); @@ -553,7 +553,7 @@ void TestQgsLayoutTable::attributeTableAtlasSource() //next atlas feature it.nextFeature( f ); - l.context().setFeature( f ); + l.reportContext().setFeature( f ); QCOMPARE( table->contents().length(), 1 ); row = table->contents().at( 0 ); @@ -566,7 +566,7 @@ void TestQgsLayoutTable::attributeTableAtlasSource() //next atlas feature it.nextFeature( f ); - l.context().setFeature( f ); + l.reportContext().setFeature( f ); QCOMPARE( table->contents().length(), 1 ); row = table->contents().at( 0 ); @@ -612,12 +612,12 @@ void TestQgsLayoutTable::attributeTableRelationSource() QgsProject::instance()->addMapLayer( atlasLayer ); //setup atlas - l.context().setLayer( atlasLayer ); + l.reportContext().setLayer( atlasLayer ); QgsFeature f; QgsFeatureIterator it = atlasLayer->getFeatures(); it.nextFeature( f ); - l.context().setFeature( f ); + l.reportContext().setFeature( f ); //create a relation QgsRelation relation; @@ -661,7 +661,7 @@ void TestQgsLayoutTable::attributeTableRelationSource() //next atlas feature it.nextFeature( f ); - l.context().setFeature( f ); + l.reportContext().setFeature( f ); QCOMPARE( f.attribute( "Class" ).toString(), QString( "Biplane" ) ); QCOMPARE( table->contents().length(), 5 ); row = table->contents().at( 0 ); diff --git a/tests/src/core/testqgslayoututils.cpp b/tests/src/core/testqgslayoututils.cpp index 2f7336192ca1..fd0e81e29d7b 100644 --- a/tests/src/core/testqgslayoututils.cpp +++ b/tests/src/core/testqgslayoututils.cpp @@ -262,19 +262,19 @@ void TestQgsLayoutUtils::createRenderContextFromLayout() QVERIFY( !rc.painter() ); // check render context flags are correctly set - l.context().setFlags( nullptr ); + l.renderContext().setFlags( nullptr ); rc = QgsLayoutUtils::createRenderContextForLayout( &l, nullptr ); QVERIFY( !( rc.flags() & QgsRenderContext::Antialiasing ) ); QVERIFY( !( rc.flags() & QgsRenderContext::UseAdvancedEffects ) ); QVERIFY( ( rc.flags() & QgsRenderContext::ForceVectorOutput ) ); - l.context().setFlag( QgsLayoutContext::FlagAntialiasing ); + l.renderContext().setFlag( QgsLayoutRenderContext::FlagAntialiasing ); rc = QgsLayoutUtils::createRenderContextForLayout( &l, nullptr ); QVERIFY( ( rc.flags() & QgsRenderContext::Antialiasing ) ); QVERIFY( !( rc.flags() & QgsRenderContext::UseAdvancedEffects ) ); QVERIFY( ( rc.flags() & QgsRenderContext::ForceVectorOutput ) ); - l.context().setFlag( QgsLayoutContext::FlagUseAdvancedEffects ); + l.renderContext().setFlag( QgsLayoutRenderContext::FlagUseAdvancedEffects ); rc = QgsLayoutUtils::createRenderContextForLayout( &l, nullptr ); QVERIFY( ( rc.flags() & QgsRenderContext::Antialiasing ) ); QVERIFY( ( rc.flags() & QgsRenderContext::UseAdvancedEffects ) ); @@ -334,19 +334,19 @@ void TestQgsLayoutUtils::createRenderContextFromMap() QVERIFY( rc.painter() ); // check render context flags are correctly set - l.context().setFlags( 0 ); + l.renderContext().setFlags( 0 ); rc = QgsLayoutUtils::createRenderContextForLayout( &l, nullptr ); QVERIFY( !( rc.flags() & QgsRenderContext::Antialiasing ) ); QVERIFY( !( rc.flags() & QgsRenderContext::UseAdvancedEffects ) ); QVERIFY( ( rc.flags() & QgsRenderContext::ForceVectorOutput ) ); - l.context().setFlag( QgsLayoutContext::FlagAntialiasing ); + l.renderContext().setFlag( QgsLayoutRenderContext::FlagAntialiasing ); rc = QgsLayoutUtils::createRenderContextForLayout( &l, nullptr ); QVERIFY( ( rc.flags() & QgsRenderContext::Antialiasing ) ); QVERIFY( !( rc.flags() & QgsRenderContext::UseAdvancedEffects ) ); QVERIFY( ( rc.flags() & QgsRenderContext::ForceVectorOutput ) ); - l.context().setFlag( QgsLayoutContext::FlagUseAdvancedEffects ); + l.renderContext().setFlag( QgsLayoutRenderContext::FlagUseAdvancedEffects ); rc = QgsLayoutUtils::createRenderContextForLayout( &l, nullptr ); QVERIFY( ( rc.flags() & QgsRenderContext::Antialiasing ) ); QVERIFY( ( rc.flags() & QgsRenderContext::UseAdvancedEffects ) ); diff --git a/tests/src/python/test_qgslayoutatlas.py b/tests/src/python/test_qgslayoutatlas.py index cb309ad4e4b1..79bd859a721a 100644 --- a/tests/src/python/test_qgslayoutatlas.py +++ b/tests/src/python/test_qgslayoutatlas.py @@ -207,52 +207,52 @@ def testIteration(self): atlas.setCoverageLayer(vector_layer) atlas_feature_changed_spy = QSignalSpy(atlas.featureChanged) - context_changed_spy = QSignalSpy(l.context().changed) + context_changed_spy = QSignalSpy(l.reportContext().changed) self.assertTrue(atlas.beginRender()) self.assertTrue(atlas.first()) self.assertEqual(len(atlas_feature_changed_spy), 1) self.assertEqual(len(context_changed_spy), 1) self.assertEqual(atlas.currentFeatureNumber(), 0) - self.assertEqual(l.context().feature()[4], 'Basse-Normandie') - self.assertEqual(l.context().layer(), vector_layer) + self.assertEqual(l.reportContext().feature()[4], 'Basse-Normandie') + self.assertEqual(l.reportContext().layer(), vector_layer) self.assertTrue(atlas.next()) self.assertEqual(len(atlas_feature_changed_spy), 2) self.assertEqual(len(context_changed_spy), 2) self.assertEqual(atlas.currentFeatureNumber(), 1) - self.assertEqual(l.context().feature()[4], 'Bretagne') + self.assertEqual(l.reportContext().feature()[4], 'Bretagne') self.assertTrue(atlas.next()) self.assertEqual(len(atlas_feature_changed_spy), 3) self.assertEqual(len(context_changed_spy), 3) self.assertEqual(atlas.currentFeatureNumber(), 2) - self.assertEqual(l.context().feature()[4], 'Pays de la Loire') + self.assertEqual(l.reportContext().feature()[4], 'Pays de la Loire') self.assertTrue(atlas.next()) self.assertEqual(len(atlas_feature_changed_spy), 4) self.assertEqual(len(context_changed_spy), 4) self.assertEqual(atlas.currentFeatureNumber(), 3) - self.assertEqual(l.context().feature()[4], 'Centre') + self.assertEqual(l.reportContext().feature()[4], 'Centre') self.assertFalse(atlas.next()) self.assertTrue(atlas.seekTo(2)) self.assertEqual(len(atlas_feature_changed_spy), 5) self.assertEqual(len(context_changed_spy), 5) self.assertEqual(atlas.currentFeatureNumber(), 2) - self.assertEqual(l.context().feature()[4], 'Pays de la Loire') + self.assertEqual(l.reportContext().feature()[4], 'Pays de la Loire') self.assertTrue(atlas.last()) self.assertEqual(len(atlas_feature_changed_spy), 6) self.assertEqual(len(context_changed_spy), 6) self.assertEqual(atlas.currentFeatureNumber(), 3) - self.assertEqual(l.context().feature()[4], 'Centre') + self.assertEqual(l.reportContext().feature()[4], 'Centre') self.assertTrue(atlas.previous()) self.assertEqual(len(atlas_feature_changed_spy), 7) self.assertEqual(len(context_changed_spy), 7) self.assertEqual(atlas.currentFeatureNumber(), 2) - self.assertEqual(l.context().feature()[4], 'Pays de la Loire') + self.assertEqual(l.reportContext().feature()[4], 'Pays de la Loire') self.assertTrue(atlas.previous()) self.assertTrue(atlas.previous()) @@ -278,14 +278,14 @@ def testUpdateFeature(self): self.assertTrue(atlas.beginRender()) self.assertTrue(atlas.first()) self.assertEqual(atlas.currentFeatureNumber(), 0) - self.assertEqual(l.context().feature()[4], 'Basse-Normandie') - self.assertEqual(l.context().layer(), vector_layer) + self.assertEqual(l.reportContext().feature()[4], 'Basse-Normandie') + self.assertEqual(l.reportContext().layer(), vector_layer) vector_layer.startEditing() - self.assertTrue(vector_layer.changeAttributeValue(l.context().feature().id(), 4, 'Nah, Canberra mate!')) - self.assertEqual(l.context().feature()[4], 'Basse-Normandie') + self.assertTrue(vector_layer.changeAttributeValue(l.reportContext().feature().id(), 4, 'Nah, Canberra mate!')) + self.assertEqual(l.reportContext().feature()[4], 'Basse-Normandie') l.atlas().refreshCurrentFeature() - self.assertEqual(l.context().feature()[4], 'Nah, Canberra mate!') + self.assertEqual(l.reportContext().feature()[4], 'Nah, Canberra mate!') vector_layer.rollBack() def testFileName(self): @@ -404,8 +404,8 @@ def predefinedscales_render_test(self): self.atlas_map.setAtlasScalingMode(QgsLayoutItemMap.Predefined) scales = [1800000, 5000000] - self.layout.context().setPredefinedScales(scales) - for i, s in enumerate(self.layout.context().predefinedScales()): + self.layout.reportContext().setPredefinedScales(scales) + for i, s in enumerate(self.layout.reportContext().predefinedScales()): self.assertEqual(s, scales[i]) self.atlas.beginRender() diff --git a/tests/src/python/test_qgslayoutexporter.py b/tests/src/python/test_qgslayoutexporter.py index b62584beff38..29162b2f9eca 100644 --- a/tests/src/python/test_qgslayoutexporter.py +++ b/tests/src/python/test_qgslayoutexporter.py @@ -274,7 +274,7 @@ def testRenderRegionToImage(self): self.assertTrue(self.checkImage('rendertoimageregionsize', 'rendertoimageregionsize', rendered_file_path)) # using layout dpi - l.context().setDpi(40) + l.renderContext().setDpi(40) image = exporter.renderRegionToImage(QRectF(5, 10, 110, 100)) self.assertFalse(image.isNull()) @@ -586,7 +586,7 @@ def prepareIteratorLayout(self): layer_path = os.path.join(TEST_DATA_DIR, 'france_parts.shp') layer = QgsVectorLayer(layer_path, 'test', "ogr") - project=QgsProject() + project = QgsProject() project.addMapLayers([layer]) # select epsg:2154 crs = QgsCoordinateReferenceSystem() @@ -597,7 +597,7 @@ def prepareIteratorLayout(self): layout.initializeDefaults() # fix the renderer, fill with green - props = {"color": "0,127,0", "outline_width":"4", "outline_color":'255,255,255'} + props = {"color": "0,127,0", "outline_width": "4", "outline_color": '255,255,255'} fillSymbol = QgsFillSymbol.createSimple(props) renderer = QgsSingleSymbolRenderer(fillSymbol) layer.setRenderer(renderer) @@ -632,7 +632,7 @@ def testIteratorToImages(self): settings = QgsLayoutExporter.ImageExportSettings() settings.dpi = 80 - result, error = QgsLayoutExporter.exportToImage(atlas,self.basetestpath, 'png', settings) + result, error = QgsLayoutExporter.exportToImage(atlas, self.basetestpath, 'png', settings) self.assertEqual(result, QgsLayoutExporter.Success, error) page1_path = os.path.join(self.basetestpath, 'test_exportiteratortoimage_Basse-Normandie.png') @@ -654,7 +654,7 @@ def testIteratorToSvgs(self): settings.dpi = 80 settings.forceVectorOutput = False - result, error = QgsLayoutExporter.exportToSvg(atlas,self.basetestpath, settings) + result, error = QgsLayoutExporter.exportToSvg(atlas, self.basetestpath, settings) self.assertEqual(result, QgsLayoutExporter.Success, error) page1_path = os.path.join(self.basetestpath, 'test_exportiteratortosvg_Basse-Normandie.svg') @@ -681,7 +681,7 @@ def testIteratorToPdfs(self): settings.rasterizeWholeImage = False settings.forceVectorOutput = False - result, error = QgsLayoutExporter.exportToPdfs(atlas,self.basetestpath, settings) + result, error = QgsLayoutExporter.exportToPdfs(atlas, self.basetestpath, settings) self.assertEqual(result, QgsLayoutExporter.Success, error) page1_path = os.path.join(self.basetestpath, 'test_exportiteratortopdf_Basse-Normandie.pdf') @@ -708,7 +708,7 @@ def testIteratorToPdf(self): settings.forceVectorOutput = False pdf_path = os.path.join(self.basetestpath, 'test_exportiteratortopdf_single.pdf') - result, error = QgsLayoutExporter.exportToPdf(atlas,pdf_path, settings) + result, error = QgsLayoutExporter.exportToPdf(atlas, pdf_path, settings) self.assertEqual(result, QgsLayoutExporter.Success, error) rendered_page_1 = os.path.join(self.basetestpath, 'test_exportiteratortopdf_single1.png') From 4d2f0deb1a458e62e1637997ec281be3afb96d61 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 29 Dec 2017 09:38:30 +1000 Subject: [PATCH 049/105] Add a clone method to QgsLayout --- python/core/layout/qgslayout.sip | 6 ++++ python/core/layout/qgsprintlayout.sip | 3 ++ src/core/composer/qgslayoutmanager.cpp | 12 ++------ src/core/layout/qgslayout.cpp | 19 ++++++++++++ src/core/layout/qgslayout.h | 6 ++++ src/core/layout/qgsprintlayout.cpp | 20 +++++++++++++ src/core/layout/qgsprintlayout.h | 2 ++ tests/src/core/testqgslayout.cpp | 41 ++++++++++++++++++++++++++ 8 files changed, 99 insertions(+), 10 deletions(-) diff --git a/python/core/layout/qgslayout.sip b/python/core/layout/qgslayout.sip index 9d4678b48fa1..9a8ea9d8a064 100644 --- a/python/core/layout/qgslayout.sip +++ b/python/core/layout/qgslayout.sip @@ -56,6 +56,12 @@ called on the new layout. ~QgsLayout(); + virtual QgsLayout *clone() const /Factory/; +%Docstring +Creates a clone of the layout. Ownership of the return layout +is transferred to the caller. +%End + void initializeDefaults(); %Docstring Initializes an empty layout, e.g. by adding a default page to the layout. This should be called after creating diff --git a/python/core/layout/qgsprintlayout.sip b/python/core/layout/qgsprintlayout.sip index 474fd4d23457..26faa334e321 100644 --- a/python/core/layout/qgsprintlayout.sip +++ b/python/core/layout/qgsprintlayout.sip @@ -26,6 +26,9 @@ class QgsPrintLayout : QgsLayout Constructor for QgsPrintLayout. %End + virtual QgsPrintLayout *clone() const /Factory/; + + QgsLayoutAtlas *atlas(); %Docstring Returns the print layout's atlas. diff --git a/src/core/composer/qgslayoutmanager.cpp b/src/core/composer/qgslayoutmanager.cpp index 333794a7d75f..77550bd051c8 100644 --- a/src/core/composer/qgslayoutmanager.cpp +++ b/src/core/composer/qgslayoutmanager.cpp @@ -283,16 +283,8 @@ QgsComposition *QgsLayoutManager::duplicateComposition( const QString &name, con QgsLayout *QgsLayoutManager::duplicateLayout( const QgsLayout *layout, const QString &newName ) { - QDomDocument currentDoc; - - QgsReadWriteContext context; - QDomElement elem = layout->writeXml( currentDoc, context ); - currentDoc.appendChild( elem ); - - std::unique_ptr< QgsLayout > newLayout = qgis::make_unique< QgsPrintLayout >( mProject ); - bool ok = false; - newLayout->loadFromTemplate( currentDoc, context, true, &ok ); - if ( !ok ) + std::unique_ptr< QgsLayout > newLayout( layout->clone() ); + if ( !newLayout ) { return nullptr; } diff --git a/src/core/layout/qgslayout.cpp b/src/core/layout/qgslayout.cpp index bab4c0b5ad10..dd8608917e77 100644 --- a/src/core/layout/qgslayout.cpp +++ b/src/core/layout/qgslayout.cpp @@ -75,6 +75,25 @@ QgsLayout::~QgsLayout() mItemsModel.reset(); // manually delete, so we can control order of destruction } +QgsLayout *QgsLayout::clone() const +{ + QDomDocument currentDoc; + + QgsReadWriteContext context; + QDomElement elem = writeXml( currentDoc, context ); + currentDoc.appendChild( elem ); + + std::unique_ptr< QgsLayout > newLayout = qgis::make_unique< QgsLayout >( mProject ); + bool ok = false; + newLayout->loadFromTemplate( currentDoc, context, true, &ok ); + if ( !ok ) + { + return nullptr; + } + + return newLayout.release(); +} + void QgsLayout::initializeDefaults() { // default to a A4 landscape page diff --git a/src/core/layout/qgslayout.h b/src/core/layout/qgslayout.h index 58589fd360a4..8e196d5c0d12 100644 --- a/src/core/layout/qgslayout.h +++ b/src/core/layout/qgslayout.h @@ -83,6 +83,12 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext ~QgsLayout() override; + /** + * Creates a clone of the layout. Ownership of the return layout + * is transferred to the caller. + */ + virtual QgsLayout *clone() const SIP_FACTORY; + /** * Initializes an empty layout, e.g. by adding a default page to the layout. This should be called after creating * a new layout. diff --git a/src/core/layout/qgsprintlayout.cpp b/src/core/layout/qgsprintlayout.cpp index 72afbbc80110..9db82592b352 100644 --- a/src/core/layout/qgsprintlayout.cpp +++ b/src/core/layout/qgsprintlayout.cpp @@ -16,6 +16,7 @@ #include "qgsprintlayout.h" #include "qgslayoutatlas.h" +#include "qgsreadwritecontext.h" QgsPrintLayout::QgsPrintLayout( QgsProject *project ) : QgsLayout( project ) @@ -23,6 +24,25 @@ QgsPrintLayout::QgsPrintLayout( QgsProject *project ) { } +QgsPrintLayout *QgsPrintLayout::clone() const +{ + QDomDocument currentDoc; + + QgsReadWriteContext context; + QDomElement elem = writeXml( currentDoc, context ); + currentDoc.appendChild( elem ); + + std::unique_ptr< QgsPrintLayout > newLayout = qgis::make_unique< QgsPrintLayout >( project() ); + bool ok = false; + newLayout->loadFromTemplate( currentDoc, context, true, &ok ); + if ( !ok ) + { + return nullptr; + } + + return newLayout.release(); +} + QgsLayoutAtlas *QgsPrintLayout::atlas() { return mAtlas; diff --git a/src/core/layout/qgsprintlayout.h b/src/core/layout/qgsprintlayout.h index 69817528afd2..b0471fad98ec 100644 --- a/src/core/layout/qgsprintlayout.h +++ b/src/core/layout/qgsprintlayout.h @@ -38,6 +38,8 @@ class CORE_EXPORT QgsPrintLayout : public QgsLayout */ QgsPrintLayout( QgsProject *project ); + QgsPrintLayout *clone() const override SIP_FACTORY; + /** * Returns the print layout's atlas. */ diff --git a/tests/src/core/testqgslayout.cpp b/tests/src/core/testqgslayout.cpp index 37e5d57b2bec..329d74653a0a 100644 --- a/tests/src/core/testqgslayout.cpp +++ b/tests/src/core/testqgslayout.cpp @@ -26,6 +26,8 @@ #include "qgslayoutitempolyline.h" #include "qgslayoutitemhtml.h" #include "qgslayoutframe.h" +#include "qgsprintlayout.h" +#include "qgslayoutatlas.h" class TestQgsLayout: public QObject { @@ -54,6 +56,7 @@ class TestQgsLayout: public QObject void pageIsEmpty(); void clear(); void georeference(); + void clone(); private: QString mReport; @@ -844,6 +847,44 @@ void TestQgsLayout::georeference() t.reset(); } +void TestQgsLayout::clone() +{ + QgsProject proj; + QgsLayout l( &proj ); + QgsLayoutItemPage *page = new QgsLayoutItemPage( &l ); + page->setPageSize( "A4" ); + l.pageCollection()->addPage( page ); + QgsLayoutItemPage *page2 = new QgsLayoutItemPage( &l ); + page2->setPageSize( "A4" ); + l.pageCollection()->addPage( page2 ); + QgsLayoutItemPage *page3 = new QgsLayoutItemPage( &l ); + page3->setPageSize( "A4" ); + l.pageCollection()->addPage( page3 ); + + //add some items to the composition + QgsLayoutItemShape *label1 = new QgsLayoutItemShape( &l ); + l.addLayoutItem( label1 ); + QgsLayoutItemShape *label2 = new QgsLayoutItemShape( &l ); + l.addLayoutItem( label2 ); + QgsLayoutItemShape *label3 = new QgsLayoutItemShape( &l ); + l.addLayoutItem( label3 ); + + // clone and check a few poperties + std::unique_ptr< QgsLayout > cloned( l.clone() ); + QVERIFY( cloned.get() ); + QCOMPARE( cloned->pageCollection()->pageCount(), 3 ); + QList< QgsLayoutItem * > items; + cloned->layoutItems( items ); + QCOMPARE( items.count(), 6 ); // 3 pages + 3 items + + // clone a print layout + QgsPrintLayout pl( &proj ); + pl.atlas()->setPageNameExpression( QStringLiteral( "not a real expression" ) ); + std::unique_ptr< QgsPrintLayout > plClone( pl.clone() ); + QVERIFY( plClone.get() ); + QCOMPARE( plClone->atlas()->pageNameExpression(), QStringLiteral( "not a real expression" ) ); +} + QGSTEST_MAIN( TestQgsLayout ) #include "testqgslayout.moc" From 811145eb96cb5b2ebc33eb0a2adc7c402fbe7f0c Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 29 Dec 2017 09:39:03 +1000 Subject: [PATCH 050/105] Remove const from count method --- python/core/layout/qgsabstractlayoutiterator.sip | 2 +- python/core/layout/qgslayoutatlas.sip | 2 +- python/core/qgsexpressioncontext.sip | 2 +- src/core/layout/qgsabstractlayoutiterator.h | 2 +- src/core/layout/qgslayoutatlas.cpp | 2 +- src/core/layout/qgslayoutatlas.h | 2 +- src/core/qgsexpressioncontext.cpp | 2 +- src/core/qgsexpressioncontext.h | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/python/core/layout/qgsabstractlayoutiterator.sip b/python/core/layout/qgsabstractlayoutiterator.sip index a3f2e6e3f22f..5a8115bfc760 100644 --- a/python/core/layout/qgsabstractlayoutiterator.sip +++ b/python/core/layout/qgsabstractlayoutiterator.sip @@ -46,7 +46,7 @@ is available or required. Ends the render, performing any required cleanup tasks. %End - virtual int count() const = 0; + virtual int count() = 0; %Docstring Returns the number of features to iterate over. %End diff --git a/python/core/layout/qgslayoutatlas.sip b/python/core/layout/qgslayoutatlas.sip index a5394840935e..2f3248e02bee 100644 --- a/python/core/layout/qgslayoutatlas.sip +++ b/python/core/layout/qgslayoutatlas.sip @@ -268,7 +268,7 @@ number of matching features. virtual bool endRender(); - virtual int count() const; + virtual int count(); virtual QString filePath( const QString &baseFilePath, const QString &extension ); diff --git a/python/core/qgsexpressioncontext.sip b/python/core/qgsexpressioncontext.sip index 736a99bb80ce..d92092a4173f 100644 --- a/python/core/qgsexpressioncontext.sip +++ b/python/core/qgsexpressioncontext.sip @@ -1053,7 +1053,7 @@ For instance, current page name and number. :param atlas: source atlas. If null, a set of default atlas variables will be added to the scope. %End - static QgsExpressionContextScope *atlasScope( const QgsLayoutAtlas *atlas ) /Factory/; + static QgsExpressionContextScope *atlasScope( QgsLayoutAtlas *atlas ) /Factory/; %Docstring Creates a new scope which contains variables and functions relating to a :py:class:`QgsLayoutAtlas`. For instance, current page name and number. diff --git a/src/core/layout/qgsabstractlayoutiterator.h b/src/core/layout/qgsabstractlayoutiterator.h index 34d7d51de349..c2238b6283e8 100644 --- a/src/core/layout/qgsabstractlayoutiterator.h +++ b/src/core/layout/qgsabstractlayoutiterator.h @@ -48,7 +48,7 @@ class CORE_EXPORT QgsAbstractLayoutIterator /** * Returns the number of features to iterate over. */ - virtual int count() const = 0; + virtual int count() = 0; /** * Iterates to next feature, returning false if no more features exist to iterate over. diff --git a/src/core/layout/qgslayoutatlas.cpp b/src/core/layout/qgslayoutatlas.cpp index adaf38f5e7e0..6064f5d20de5 100644 --- a/src/core/layout/qgslayoutatlas.cpp +++ b/src/core/layout/qgslayoutatlas.cpp @@ -334,7 +334,7 @@ bool QgsLayoutAtlas::endRender() return true; } -int QgsLayoutAtlas::count() const +int QgsLayoutAtlas::count() { return mFeatureIds.size(); } diff --git a/src/core/layout/qgslayoutatlas.h b/src/core/layout/qgslayoutatlas.h index 4e6bc42c030e..a1d236ed598e 100644 --- a/src/core/layout/qgslayoutatlas.h +++ b/src/core/layout/qgslayoutatlas.h @@ -241,7 +241,7 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject, public QgsAbstractLayoutItera bool beginRender() override; bool endRender() override; - int count() const override; + int count() override; QString filePath( const QString &baseFilePath, const QString &extension ) override; /** diff --git a/src/core/qgsexpressioncontext.cpp b/src/core/qgsexpressioncontext.cpp index 2a29cfce3053..d37e97703850 100644 --- a/src/core/qgsexpressioncontext.cpp +++ b/src/core/qgsexpressioncontext.cpp @@ -1222,7 +1222,7 @@ QgsExpressionContextScope *QgsExpressionContextUtils::compositionAtlasScope( con return scope; } -QgsExpressionContextScope *QgsExpressionContextUtils::atlasScope( const QgsLayoutAtlas *atlas ) +QgsExpressionContextScope *QgsExpressionContextUtils::atlasScope( QgsLayoutAtlas *atlas ) { QgsExpressionContextScope *scope = new QgsExpressionContextScope( QObject::tr( "Atlas" ) ); if ( !atlas ) diff --git a/src/core/qgsexpressioncontext.h b/src/core/qgsexpressioncontext.h index 4e2ea313b85e..937ae28047ee 100644 --- a/src/core/qgsexpressioncontext.h +++ b/src/core/qgsexpressioncontext.h @@ -935,7 +935,7 @@ class CORE_EXPORT QgsExpressionContextUtils * For instance, current page name and number. * \param atlas source atlas. If null, a set of default atlas variables will be added to the scope. */ - static QgsExpressionContextScope *atlasScope( const QgsLayoutAtlas *atlas ) SIP_FACTORY; + static QgsExpressionContextScope *atlasScope( QgsLayoutAtlas *atlas ) SIP_FACTORY; /** * Creates a new scope which contains variables and functions relating to a QgsComposerItem. From 1ea5a5fb9816b0586d9af2e66a1c672de8f67994 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 29 Dec 2017 11:48:57 +1000 Subject: [PATCH 051/105] [FEATURE] Reporting framework Reports are based on the new layouts engine. They consist of multiple nested sections. Each individual section (and the report itself) can have an optional header and footer (which are themselves layouts, and can consist of multiple pages!). Two different types of sections are implemented so far: - a standard section, which has a single, static body layout. This can be used to embed static layouts mid way through a report - a "field group" section, which repeats its body layout for every feature in a layer. The features are sorted by the selected grouping feature (with an option for ascending/descending sort). If a field group section has child sections (e.g. another field group section with a different field, then only features with unique values for the group feature are iterated over. This allows nested reports, e.g. Report - Country: Australia - State: NSW - Town: Sydney - Town: Woolongong - State: QLD - Town: Beerburrum - Town: Brisbane - Town: Emerald - Country: NZ - State: ... etc In this example country, state or town groups can have their own headers and footers which will be inserted in the report. Reports are configured through a new panel in the layout designer dialog, which is shown when editing a report (created through the Layout Manager Dialog). The organizer allows for adding (and removing) sections to the report, and for selecting which layout (e.g. headers, footers, bodies) to edit within the layout designer. --- python/core/core_auto.sip | 1 + .../core/layout/qgsabstractreportsection.sip | 270 ++++++++++++++++++ src/core/CMakeLists.txt | 2 + src/core/layout/qgsabstractreportsection.cpp | 215 ++++++++++++++ src/core/layout/qgsabstractreportsection.h | 253 ++++++++++++++++ tests/src/python/CMakeLists.txt | 1 + tests/src/python/test_qgsreport.py | 128 +++++++++ 7 files changed, 870 insertions(+) create mode 100644 python/core/layout/qgsabstractreportsection.sip create mode 100644 src/core/layout/qgsabstractreportsection.cpp create mode 100644 src/core/layout/qgsabstractreportsection.h create mode 100644 tests/src/python/test_qgsreport.py diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index 79bf38c199d8..1342a112ef05 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -162,6 +162,7 @@ %Include composer/qgscomposertexttable.sip %Include composer/qgspaperitem.sip %Include layout/qgsabstractlayoutiterator.sip +%Include layout/qgsabstractreportsection.sip %Include layout/qgslayoutaligner.sip %Include layout/qgslayoutexporter.sip %Include layout/qgslayoutgridsettings.sip diff --git a/python/core/layout/qgsabstractreportsection.sip b/python/core/layout/qgsabstractreportsection.sip new file mode 100644 index 000000000000..cd73fb4770ae --- /dev/null +++ b/python/core/layout/qgsabstractreportsection.sip @@ -0,0 +1,270 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layout/qgsabstractreportsection.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + +class QgsAbstractReportSection : QgsAbstractLayoutIterator +{ +%Docstring + An abstract base class for QgsReport subsections. + +.. versionadded:: 3.0 +%End + +%TypeHeaderCode +#include "qgsabstractreportsection.h" +%End + public: + + QgsAbstractReportSection(); +%Docstring +Constructor for QgsAbstractReportSection +%End + + ~QgsAbstractReportSection(); + + + + virtual QgsAbstractReportSection *clone() const = 0 /Factory/; +%Docstring +Clones the report section. Ownership of the returned section is +transferred to the caller. + +Subclasses should call copyCommonProperties() in their clone() +implementations. +%End + + + virtual QgsLayout *layout(); + + virtual bool beginRender(); + + virtual bool next(); + + virtual bool endRender(); + + + bool headerEnabled() const; +%Docstring +Returns true if the header for the section is enabled. + +.. seealso:: :py:func:`setHeaderEnabled()` + +.. seealso:: :py:func:`header()` + +.. seealso:: :py:func:`setHeader()` +%End + + void setHeaderEnabled( bool enabled ); +%Docstring +Sets whether the header for the section is ``enabled``. + +.. seealso:: :py:func:`headerEnabled()` + +.. seealso:: :py:func:`header()` + +.. seealso:: :py:func:`setHeader()` +%End + + QgsLayout *header(); +%Docstring +Returns the header for the section. Note that the header is only +included if headerEnabled() is true. + +.. seealso:: :py:func:`setHeaderEnabled()` + +.. seealso:: :py:func:`headerEnabled()` + +.. seealso:: :py:func:`setHeader()` +%End + + void setHeader( QgsLayout *header /Transfer/ ); +%Docstring +Sets the ``header`` for the section. Note that the header is only +included if headerEnabled() is true. Ownership of ``header`` +is transferred to the report section. + +.. seealso:: :py:func:`setHeaderEnabled()` + +.. seealso:: :py:func:`headerEnabled()` + +.. seealso:: :py:func:`header()` +%End + + bool footerEnabled() const; +%Docstring +Returns true if the footer for the section is enabled. + +.. seealso:: :py:func:`setFooterEnabled()` + +.. seealso:: :py:func:`footer()` + +.. seealso:: :py:func:`setFooter()` +%End + + void setFooterEnabled( bool enabled ); +%Docstring +Sets whether the footer for the section is ``enabled``. + +.. seealso:: :py:func:`footerEnabled()` + +.. seealso:: :py:func:`footer()` + +.. seealso:: :py:func:`setFooter()` +%End + + QgsLayout *footer(); +%Docstring +Returns the footer for the section. Note that the footer is only +included if footerEnabled() is true. + +.. seealso:: :py:func:`setFooterEnabled()` + +.. seealso:: :py:func:`footerEnabled()` + +.. seealso:: :py:func:`setFooter()` +%End + + void setFooter( QgsLayout *footer /Transfer/ ); +%Docstring +Sets the ``footer`` for the section. Note that the footer is only +included if footerEnabled() is true. Ownership of ``footer`` +is transferred to the report section. + +.. seealso:: :py:func:`setFooterEnabled()` + +.. seealso:: :py:func:`footerEnabled()` + +.. seealso:: :py:func:`footer()` +%End + + int childCount() const; +%Docstring +Return the number of child sections for this report section. The child +sections form the body of the report section. + +.. seealso:: :py:func:`children()` +%End + + QList< QgsAbstractReportSection * > children(); +%Docstring +Return all child sections for this report section. The child +sections form the body of the report section. + +.. seealso:: :py:func:`childCount()` + +.. seealso:: :py:func:`child()` + +.. seealso:: :py:func:`appendChild()` + +.. seealso:: :py:func:`insertChild()` + +.. seealso:: :py:func:`removeChild()` +%End + + QgsAbstractReportSection *child( int index ); +%Docstring +Returns the child section at the specified ``index``. + +.. seealso:: :py:func:`children()` +%End + + void appendChild( QgsAbstractReportSection *section /Transfer/ ); +%Docstring +Adds a child ``section``, transferring ownership of the section to this section. + +.. seealso:: :py:func:`children()` + +.. seealso:: :py:func:`insertChild()` +%End + + void insertChild( int index, QgsAbstractReportSection *section /Transfer/ ); +%Docstring +Inserts a child ``section`` at the specified ``index``, transferring ownership of the section to this section. + +.. seealso:: :py:func:`children()` + +.. seealso:: :py:func:`appendChild()` +%End + + void removeChild( QgsAbstractReportSection *section ); +%Docstring +Removes a child ``section``, deleting it. + +.. seealso:: :py:func:`children()` +%End + + void removeChildAt( int index ); +%Docstring +Removes the child section at the specified ``index``, deleting it. + +.. seealso:: :py:func:`children()` +%End + + protected: + + enum SubSection + { + Header, + Body, + Footer, + End, + }; + + void copyCommonProperties( QgsAbstractReportSection *destination ) const; +%Docstring +Copies the common properties of a report section to a ``destination`` section. +This method should be called from clone() implementations. +%End + + private: + QgsAbstractReportSection( const QgsAbstractReportSection &other ); +}; + + +class QgsReport : QgsAbstractReportSection +{ +%Docstring + Represents a report for use with the QgsLayout engine. + +Reports consist of multiple sections, represented by QgsAbstractReportSection +subclasses. + +.. versionadded:: 3.0 +%End + +%TypeHeaderCode +#include "qgsabstractreportsection.h" +%End + public: + + QgsReport(); +%Docstring +Constructor for QgsReport. +%End + + virtual QgsReport *clone() const; + + + virtual int count(); + virtual bool beginRender(); + + virtual bool next(); + + + virtual QString filePath( const QString &baseFilePath, const QString &extension ); + + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layout/qgsabstractreportsection.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 2dda74ed1018..d1a766009b01 100755 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -364,6 +364,7 @@ SET(QGIS_CORE_SRCS dxf/qgsdxfpaintengine.cpp dxf/qgsdxfpallabeling.cpp + layout/qgsabstractreportsection.cpp layout/qgslayout.cpp layout/qgslayoutaligner.cpp layout/qgslayoutatlas.cpp @@ -1029,6 +1030,7 @@ SET(QGIS_CORE_HDRS composer/qgspaperitem.h layout/qgsabstractlayoutiterator.h + layout/qgsabstractreportsection.h layout/qgslayoutaligner.h layout/qgslayoutexporter.h layout/qgslayoutgridsettings.h diff --git a/src/core/layout/qgsabstractreportsection.cpp b/src/core/layout/qgsabstractreportsection.cpp new file mode 100644 index 000000000000..cfbb58ed73b3 --- /dev/null +++ b/src/core/layout/qgsabstractreportsection.cpp @@ -0,0 +1,215 @@ +/*************************************************************************** + qgsabstractreportsection.cpp + -------------------- + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsabstractreportsection.h" +#include "qgslayout.h" + +QgsAbstractReportSection::~QgsAbstractReportSection() +{ + qDeleteAll( mChildren ); +} + +QgsLayout *QgsAbstractReportSection::layout() +{ + return mCurrentLayout; +} + +bool QgsAbstractReportSection::beginRender() +{ + // reset this section + mCurrentLayout = nullptr; + mNextChild = 0; + mNextSection = Header; + + // and all children too + bool result = true; + for ( QgsAbstractReportSection *child : qgis::as_const( mChildren ) ) + { + result = result && child->beginRender(); + } + return result; +} + +bool QgsAbstractReportSection::next() +{ + switch ( mNextSection ) + { + case Header: + { + // regardless of whether we have a header or not, the next section will be the body + mNextSection = Body; + + // if we have a header, then the current section will be the header + if ( mHeaderEnabled && mHeader ) + { + mCurrentLayout = mHeader.get(); + return true; + } + + // but if not, then the current section is the body + FALLTHROUGH; + } + + case Body: + { + + // we iterate through all the section's children... + while ( mNextChild < mChildren.count() ) + { + // ... staying on the current child only while it still has content for us + if ( mChildren.at( mNextChild )->next() ) + { + mCurrentLayout = mChildren.at( mNextChild )->layout(); + return true; + } + else + { + // no more content for this child, so move to next child + mNextChild++; + } + } + + // all children have spent their content, so move to the footer + mNextSection = Footer; + FALLTHROUGH; + } + + case Footer: + { + // regardless of whether we have a footer or not, this is the last section + mNextSection = End; + + // if we have a footer, then the current section will be the footer + if ( mFooterEnabled && mFooter ) + { + mCurrentLayout = mFooter.get(); + return true; + } + + // if not, then we're all done + FALLTHROUGH; + } + + case End: + break; + } + + mCurrentLayout = nullptr; + return false; +} + +bool QgsAbstractReportSection::endRender() +{ + // reset this section + mCurrentLayout = nullptr; + mNextChild = 0; + mNextSection = Header; + + // and all children too + bool result = true; + for ( QgsAbstractReportSection *child : qgis::as_const( mChildren ) ) + { + result = result && child->endRender(); + } + return result; +} + +QgsAbstractReportSection *QgsAbstractReportSection::child( int index ) +{ + return mChildren.value( index ); +} + +void QgsAbstractReportSection::appendChild( QgsAbstractReportSection *section ) +{ + mChildren.append( section ); +} + +void QgsAbstractReportSection::insertChild( int index, QgsAbstractReportSection *section ) +{ + index = std::max( 0, index ); + index = std::min( index, mChildren.count() ); + mChildren.insert( index, section ); +} + +void QgsAbstractReportSection::removeChild( QgsAbstractReportSection *section ) +{ + mChildren.removeAll( section ); + delete section; +} + +void QgsAbstractReportSection::removeChildAt( int index ) +{ + if ( index < 0 || index >= mChildren.count() ) + return; + + QgsAbstractReportSection *section = mChildren.at( index ); + removeChild( section ); +} + +void QgsAbstractReportSection::copyCommonProperties( QgsAbstractReportSection *destination ) const +{ + destination->mHeaderEnabled = mHeaderEnabled; + if ( mHeader ) + destination->mHeader.reset( mHeader->clone() ); + else + destination->mHeader.reset(); + + destination->mFooterEnabled = mFooterEnabled; + if ( mFooter ) + destination->mFooter.reset( mFooter->clone() ); + else + destination->mFooter.reset(); + + qDeleteAll( destination->mChildren ); + destination->mChildren.clear(); + + for ( QgsAbstractReportSection *child : qgis::as_const( mChildren ) ) + { + destination->mChildren.append( child->clone() ); + } +} + + +// QgsReport + +QgsReport *QgsReport::clone() const +{ + std::unique_ptr< QgsReport > copy = qgis::make_unique< QgsReport >(); + copyCommonProperties( copy.get() ); + return copy.release(); +} + +bool QgsReport::beginRender() +{ + mSectionNumber = 0; + return QgsAbstractReportSection::beginRender(); +} + +bool QgsReport::next() +{ + mSectionNumber++; + return QgsAbstractReportSection::next(); +} + +QString QgsReport::filePath( const QString &baseFilePath, const QString &extension ) +{ + QString base = QDir( baseFilePath ).filePath( "report_" ) + QString::number( mSectionNumber ); + if ( !extension.startsWith( '.' ) ) + base += '.'; + base += extension; + return base; + +} diff --git a/src/core/layout/qgsabstractreportsection.h b/src/core/layout/qgsabstractreportsection.h new file mode 100644 index 000000000000..8391ec1de914 --- /dev/null +++ b/src/core/layout/qgsabstractreportsection.h @@ -0,0 +1,253 @@ +/*************************************************************************** + qgsabstractreportsection.h + --------------------------- + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#ifndef QGSABSTRACTREPORTSECTION_H +#define QGSABSTRACTREPORTSECTION_H + +#include "qgis_core.h" +#include "qgsabstractlayoutiterator.h" +#include "qgslayoutreportcontext.h" + +/** + * \ingroup core + * \class QgsAbstractReportSection + * \brief An abstract base class for QgsReport subsections. + * \since QGIS 3.0 + */ +class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator +{ + + public: + + //! Constructor for QgsAbstractReportSection + QgsAbstractReportSection() = default; + + ~QgsAbstractReportSection() override; + + //! QgsAbstractReportSection cannot be copied + QgsAbstractReportSection( const QgsAbstractReportSection &other ) = delete; + + //! QgsAbstractReportSection cannot be copied + QgsAbstractReportSection &operator=( const QgsAbstractReportSection &other ) = delete; + + /** + * Clones the report section. Ownership of the returned section is + * transferred to the caller. + * + * Subclasses should call copyCommonProperties() in their clone() + * implementations. + */ + virtual QgsAbstractReportSection *clone() const = 0 SIP_FACTORY; + +#if 0 //TODO + virtual void setContext( const QgsLayoutReportContext &context ) = 0; +#endif + + QgsLayout *layout() override; + bool beginRender() override; + bool next() override; + bool endRender() override; + + /** + * Returns true if the header for the section is enabled. + * \see setHeaderEnabled() + * \see header() + * \see setHeader() + */ + bool headerEnabled() const { return mHeaderEnabled; } + + /** + * Sets whether the header for the section is \a enabled. + * \see headerEnabled() + * \see header() + * \see setHeader() + */ + void setHeaderEnabled( bool enabled ) { mHeaderEnabled = enabled; } + + /** + * Returns the header for the section. Note that the header is only + * included if headerEnabled() is true. + * \see setHeaderEnabled() + * \see headerEnabled() + * \see setHeader() + */ + QgsLayout *header() { return mHeader.get(); } + + /** + * Sets the \a header for the section. Note that the header is only + * included if headerEnabled() is true. Ownership of \a header + * is transferred to the report section. + * \see setHeaderEnabled() + * \see headerEnabled() + * \see header() + */ + void setHeader( QgsLayout *header SIP_TRANSFER ) { mHeader.reset( header ); } + + /** + * Returns true if the footer for the section is enabled. + * \see setFooterEnabled() + * \see footer() + * \see setFooter() + */ + bool footerEnabled() const { return mFooterEnabled; } + + /** + * Sets whether the footer for the section is \a enabled. + * \see footerEnabled() + * \see footer() + * \see setFooter() + */ + void setFooterEnabled( bool enabled ) { mFooterEnabled = enabled; } + + /** + * Returns the footer for the section. Note that the footer is only + * included if footerEnabled() is true. + * \see setFooterEnabled() + * \see footerEnabled() + * \see setFooter() + */ + QgsLayout *footer() { return mFooter.get(); } + + /** + * Sets the \a footer for the section. Note that the footer is only + * included if footerEnabled() is true. Ownership of \a footer + * is transferred to the report section. + * \see setFooterEnabled() + * \see footerEnabled() + * \see footer() + */ + void setFooter( QgsLayout *footer SIP_TRANSFER ) { mFooter.reset( footer ); } + + /** + * Return the number of child sections for this report section. The child + * sections form the body of the report section. + * \see children() + */ + int childCount() const { return mChildren.count(); } + + /** + * Return all child sections for this report section. The child + * sections form the body of the report section. + * \see childCount() + * \see child() + * \see appendChild() + * \see insertChild() + * \see removeChild() + */ + QList< QgsAbstractReportSection * > children() { return mChildren; } + + /** + * Returns the child section at the specified \a index. + * \see children() + */ + QgsAbstractReportSection *child( int index ); + + /** + * Adds a child \a section, transferring ownership of the section to this section. + * \see children() + * \see insertChild() + */ + void appendChild( QgsAbstractReportSection *section SIP_TRANSFER ); + + /** + * Inserts a child \a section at the specified \a index, transferring ownership of the section to this section. + * \see children() + * \see appendChild() + */ + void insertChild( int index, QgsAbstractReportSection *section SIP_TRANSFER ); + + /** + * Removes a child \a section, deleting it. + * \see children() + */ + void removeChild( QgsAbstractReportSection *section ); + + /** + * Removes the child section at the specified \a index, deleting it. + * \see children() + */ + void removeChildAt( int index ); + + protected: + + //! Report sub-sections + enum SubSection + { + Header, //!< Header for section + Body, //!< Body of section + Footer, //!< Footer for section + End, //!< End of section (i.e. past all available content) + }; + + /** + * Copies the common properties of a report section to a \a destination section. + * This method should be called from clone() implementations. + */ + void copyCommonProperties( QgsAbstractReportSection *destination ) const; + + private: + + SubSection mNextSection = Header; + int mNextChild = 0; + QgsLayout *mCurrentLayout = nullptr; + + bool mHeaderEnabled = false; + bool mFooterEnabled = false; + std::unique_ptr< QgsLayout > mHeader; + std::unique_ptr< QgsLayout > mFooter; + + QList< QgsAbstractReportSection * > mChildren; + +#ifdef SIP_RUN + QgsAbstractReportSection( const QgsAbstractReportSection &other ); +#endif +}; + + +/** + * \ingroup core + * \class QgsReport + * \brief Represents a report for use with the QgsLayout engine. + * + * Reports consist of multiple sections, represented by QgsAbstractReportSection + * subclasses. + * + * \since QGIS 3.0 + */ +class CORE_EXPORT QgsReport : public QgsAbstractReportSection +{ + + public: + + //! Constructor for QgsReport. + QgsReport() = default; + + QgsReport *clone() const override; + + // TODO - how to handle this? + int count() override { return -1; } + bool beginRender() override; + bool next() override; + + //TODO - baseFilePath should be a filename, not directory + QString filePath( const QString &baseFilePath, const QString &extension ) override; + + private: + + int mSectionNumber = 0; + +}; + +#endif //QGSABSTRACTREPORTSECTION_H diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 39c76e0e85e3..3b7099f4cb40 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -154,6 +154,7 @@ ADD_PYTHON_TEST(PyQgsRelation test_qgsrelation.py) ADD_PYTHON_TEST(PyQgsRelationManager test_qgsrelationmanager.py) ADD_PYTHON_TEST(PyQgsRenderContext test_qgsrendercontext.py) ADD_PYTHON_TEST(PyQgsRenderer test_qgsrenderer.py) +ADD_PYTHON_TEST(PyQgsReport test_qgsreport.py) ADD_PYTHON_TEST(PyQgsRulebasedRenderer test_qgsrulebasedrenderer.py) ADD_PYTHON_TEST(PyQgsSingleSymbolRenderer test_qgssinglesymbolrenderer.py) ADD_PYTHON_TEST(PyQgsShapefileProvider test_provider_shapefile.py) diff --git a/tests/src/python/test_qgsreport.py b/tests/src/python/test_qgsreport.py new file mode 100644 index 000000000000..11e3b0641e01 --- /dev/null +++ b/tests/src/python/test_qgsreport.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsReport + +.. note:: This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +""" +__author__ = 'Nyall Dawson' +__date__ = '29/12/2017' +__copyright__ = 'Copyright 2017, The QGIS Project' +# This will get replaced with a git SHA1 when you do a git archive +__revision__ = '$Format:%H$' + +import qgis # NOQA + +from qgis.core import (QgsProject, + QgsLayout, + QgsReport) +from qgis.testing import start_app, unittest + +start_app() + + +class TestQgsReport(unittest.TestCase): + + def testGettersSetters(self): + p = QgsProject() + r = QgsReport() + + r.setHeaderEnabled(True) + self.assertTrue(r.headerEnabled()) + + header = QgsLayout(p) + r.setHeader(header) + self.assertEqual(r.header(), header) + + r.setFooterEnabled(True) + self.assertTrue(r.footerEnabled()) + + footer = QgsLayout(p) + r.setFooter(footer) + self.assertEqual(r.footer(), footer) + + def testChildren(self): + p = QgsProject() + r = QgsReport() + self.assertEqual(r.childCount(), 0) + self.assertEqual(r.children(), []) + self.assertIsNone(r.child(-1)) + self.assertIsNone(r.child(1)) + self.assertIsNone(r.child(0)) + + # try deleting non-existant children + r.removeChildAt(-1) + r.removeChildAt(0) + r.removeChildAt(100) + r.removeChild(None) + + # append child + child1 = QgsReport() + r.appendChild(child1) + self.assertEqual(r.childCount(), 1) + self.assertEqual(r.children(), [child1]) + self.assertEqual(r.child(0), child1) + child2 = QgsReport() + r.appendChild(child2) + self.assertEqual(r.childCount(), 2) + self.assertEqual(r.children(), [child1, child2]) + self.assertEqual(r.child(1), child2) + + def testInsertChild(self): + p = QgsProject() + r = QgsReport() + + child1 = QgsReport() + r.insertChild(11, child1) + self.assertEqual(r.childCount(), 1) + self.assertEqual(r.children(), [child1]) + child2 = QgsReport() + r.insertChild(-1, child2) + self.assertEqual(r.childCount(), 2) + self.assertEqual(r.children(), [child2, child1]) + + def testRemoveChild(self): + p = QgsProject() + r = QgsReport() + + child1 = QgsReport() + r.appendChild(child1) + child2 = QgsReport() + r.appendChild(child2) + + r.removeChildAt(-1) + r.removeChildAt(100) + r.removeChild(None) + self.assertEqual(r.childCount(), 2) + self.assertEqual(r.children(), [child1, child2]) + + r.removeChildAt(1) + self.assertEqual(r.childCount(), 1) + self.assertEqual(r.children(), [child1]) + + r.removeChild(child1) + self.assertEqual(r.childCount(), 0) + self.assertEqual(r.children(), []) + + def testClone(self): + p = QgsProject() + r = QgsReport() + + child1 = QgsReport() + child1.setHeaderEnabled(True) + r.appendChild(child1) + child2 = QgsReport() + child2.setFooterEnabled(True) + r.appendChild(child2) + + cloned = r.clone() + self.assertEqual(cloned.childCount(), 2) + self.assertTrue(cloned.child(0).headerEnabled()) + self.assertFalse(cloned.child(0).footerEnabled()) + self.assertFalse(cloned.child(1).headerEnabled()) + self.assertTrue(cloned.child(1).footerEnabled()) + + +if __name__ == '__main__': + unittest.main() From acb44643e360f6956bae20eebcd7c07e638be464 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 29 Dec 2017 11:50:05 +1000 Subject: [PATCH 052/105] Add missing factory annotation --- python/core/layout/qgsabstractreportsection.sip | 2 +- src/core/layout/qgsabstractreportsection.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/python/core/layout/qgsabstractreportsection.sip b/python/core/layout/qgsabstractreportsection.sip index cd73fb4770ae..8b3307b1a9e1 100644 --- a/python/core/layout/qgsabstractreportsection.sip +++ b/python/core/layout/qgsabstractreportsection.sip @@ -247,7 +247,7 @@ subclasses. Constructor for QgsReport. %End - virtual QgsReport *clone() const; + virtual QgsReport *clone() const /Factory/; virtual int count(); diff --git a/src/core/layout/qgsabstractreportsection.h b/src/core/layout/qgsabstractreportsection.h index 8391ec1de914..cad2b53c71a6 100644 --- a/src/core/layout/qgsabstractreportsection.h +++ b/src/core/layout/qgsabstractreportsection.h @@ -234,7 +234,7 @@ class CORE_EXPORT QgsReport : public QgsAbstractReportSection //! Constructor for QgsReport. QgsReport() = default; - QgsReport *clone() const override; + QgsReport *clone() const override SIP_FACTORY; // TODO - how to handle this? int count() override { return -1; } From cdf5cf21e1c640004e029e09b2a385a11c3bb7eb Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 29 Dec 2017 12:10:36 +1000 Subject: [PATCH 053/105] Add report section subclass with single layout as body --- .../core/layout/qgsabstractreportsection.sip | 54 +++++++++++++++---- src/core/layout/qgsabstractreportsection.cpp | 52 +++++++++++++----- src/core/layout/qgsabstractreportsection.h | 54 ++++++++++++++----- tests/src/python/test_qgsreport.py | 26 +++++---- 4 files changed, 142 insertions(+), 44 deletions(-) diff --git a/python/core/layout/qgsabstractreportsection.sip b/python/core/layout/qgsabstractreportsection.sip index 8b3307b1a9e1..418ec0e4a2c9 100644 --- a/python/core/layout/qgsabstractreportsection.sip +++ b/python/core/layout/qgsabstractreportsection.sip @@ -38,6 +38,12 @@ Subclasses should call copyCommonProperties() in their clone() implementations. %End + virtual int count(); + + virtual QString filePath( const QString &baseFilePath, const QString &extension ); + + + virtual QgsLayout *layout(); @@ -225,13 +231,50 @@ This method should be called from clone() implementations. QgsAbstractReportSection( const QgsAbstractReportSection &other ); }; +class QgsReportSectionLayout : QgsAbstractReportSection +{ +%Docstring + A report section consisting of a single QgsLayout body. + +.. versionadded:: 3.0 +%End + +%TypeHeaderCode +#include "qgsabstractreportsection.h" +%End + public: + + QgsLayout *body(); +%Docstring +Returns the body layout for the section. + +.. seealso:: :py:func:`setBody()` +%End + + void setBody( QgsLayout *body /Transfer/ ); +%Docstring +Sets the ``body`` layout for the section. Ownership of ``body`` +is transferred to the report section. + +.. seealso:: :py:func:`body()` +%End + + virtual QgsReportSectionLayout *clone() const /Factory/; + + virtual bool beginRender(); + + virtual bool next(); + + +}; + class QgsReport : QgsAbstractReportSection { %Docstring Represents a report for use with the QgsLayout engine. -Reports consist of multiple sections, represented by QgsAbstractReportSection +Reports consist of multiple sections, represented by :py:class:`QgsAbstractReportSection` subclasses. .. versionadded:: 3.0 @@ -250,15 +293,6 @@ Constructor for QgsReport. virtual QgsReport *clone() const /Factory/; - virtual int count(); - virtual bool beginRender(); - - virtual bool next(); - - - virtual QString filePath( const QString &baseFilePath, const QString &extension ); - - }; /************************************************************************ diff --git a/src/core/layout/qgsabstractreportsection.cpp b/src/core/layout/qgsabstractreportsection.cpp index cfbb58ed73b3..b971b6e6530b 100644 --- a/src/core/layout/qgsabstractreportsection.cpp +++ b/src/core/layout/qgsabstractreportsection.cpp @@ -22,6 +22,15 @@ QgsAbstractReportSection::~QgsAbstractReportSection() qDeleteAll( mChildren ); } +QString QgsAbstractReportSection::filePath( const QString &baseFilePath, const QString &extension ) +{ + QString base = QDir( baseFilePath ).filePath( "report_" ) + QString::number( mSectionNumber ); + if ( !extension.startsWith( '.' ) ) + base += '.'; + base += extension; + return base; +} + QgsLayout *QgsAbstractReportSection::layout() { return mCurrentLayout; @@ -33,6 +42,7 @@ bool QgsAbstractReportSection::beginRender() mCurrentLayout = nullptr; mNextChild = 0; mNextSection = Header; + mSectionNumber = 0; // and all children too bool result = true; @@ -45,6 +55,8 @@ bool QgsAbstractReportSection::beginRender() bool QgsAbstractReportSection::next() { + mSectionNumber++; + switch ( mNextSection ) { case Header: @@ -192,24 +204,38 @@ QgsReport *QgsReport::clone() const return copy.release(); } -bool QgsReport::beginRender() +// +// QgsReportSectionLayout +// + +QgsReportSectionLayout *QgsReportSectionLayout::clone() const { - mSectionNumber = 0; - return QgsAbstractReportSection::beginRender(); + std::unique_ptr< QgsReportSectionLayout > copy = qgis::make_unique< QgsReportSectionLayout >(); + copyCommonProperties( copy.get() ); + + if ( mBody ) + copy->mBody.reset( mBody->clone() ); + else + copy->mBody.reset(); + + return copy.release(); } -bool QgsReport::next() +bool QgsReportSectionLayout::beginRender() { - mSectionNumber++; - return QgsAbstractReportSection::next(); + mExportedBody = false; + return QgsAbstractReportSection::beginRender(); } -QString QgsReport::filePath( const QString &baseFilePath, const QString &extension ) +bool QgsReportSectionLayout::next() { - QString base = QDir( baseFilePath ).filePath( "report_" ) + QString::number( mSectionNumber ); - if ( !extension.startsWith( '.' ) ) - base += '.'; - base += extension; - return base; - + if ( !mExportedBody ) + { + mExportedBody = true; + return true; + } + else + { + return false; + } } diff --git a/src/core/layout/qgsabstractreportsection.h b/src/core/layout/qgsabstractreportsection.h index cad2b53c71a6..7c6839380da2 100644 --- a/src/core/layout/qgsabstractreportsection.h +++ b/src/core/layout/qgsabstractreportsection.h @@ -51,6 +51,13 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator */ virtual QgsAbstractReportSection *clone() const = 0 SIP_FACTORY; + // TODO - how to handle this? + int count() override { return -1; } + + //TODO - baseFilePath should be a filename, not directory + QString filePath( const QString &baseFilePath, const QString &extension ) override; + + #if 0 //TODO virtual void setContext( const QgsLayoutReportContext &context ) = 0; #endif @@ -199,6 +206,7 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator private: + int mSectionNumber = 0; SubSection mNextSection = Header; int mNextChild = 0; QgsLayout *mCurrentLayout = nullptr; @@ -215,6 +223,40 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator #endif }; +/** + * \ingroup core + * \class QgsReportSectionLayout + * \brief A report section consisting of a single QgsLayout body. + * \since QGIS 3.0 + */ +class CORE_EXPORT QgsReportSectionLayout : public QgsAbstractReportSection +{ + public: + + /** + * Returns the body layout for the section. + * \see setBody() + */ + QgsLayout *body() { return mBody.get(); } + + /** + * Sets the \a body layout for the section. Ownership of \a body + * is transferred to the report section. + * \see body() + */ + void setBody( QgsLayout *body SIP_TRANSFER ) { mBody.reset( body ); } + + QgsReportSectionLayout *clone() const override SIP_FACTORY; + bool beginRender() override; + bool next() override; + + private: + + bool mExportedBody = false; + std::unique_ptr< QgsLayout > mBody; + +}; + /** * \ingroup core @@ -236,18 +278,6 @@ class CORE_EXPORT QgsReport : public QgsAbstractReportSection QgsReport *clone() const override SIP_FACTORY; - // TODO - how to handle this? - int count() override { return -1; } - bool beginRender() override; - bool next() override; - - //TODO - baseFilePath should be a filename, not directory - QString filePath( const QString &baseFilePath, const QString &extension ) override; - - private: - - int mSectionNumber = 0; - }; #endif //QGSABSTRACTREPORTSECTION_H diff --git a/tests/src/python/test_qgsreport.py b/tests/src/python/test_qgsreport.py index 11e3b0641e01..00b8994f119c 100644 --- a/tests/src/python/test_qgsreport.py +++ b/tests/src/python/test_qgsreport.py @@ -16,7 +16,8 @@ from qgis.core import (QgsProject, QgsLayout, - QgsReport) + QgsReport, + QgsReportSectionLayout) from qgis.testing import start_app, unittest start_app() @@ -58,12 +59,12 @@ def testChildren(self): r.removeChild(None) # append child - child1 = QgsReport() + child1 = QgsReportSectionLayout() r.appendChild(child1) self.assertEqual(r.childCount(), 1) self.assertEqual(r.children(), [child1]) self.assertEqual(r.child(0), child1) - child2 = QgsReport() + child2 = QgsReportSectionLayout() r.appendChild(child2) self.assertEqual(r.childCount(), 2) self.assertEqual(r.children(), [child1, child2]) @@ -73,11 +74,11 @@ def testInsertChild(self): p = QgsProject() r = QgsReport() - child1 = QgsReport() + child1 = QgsReportSectionLayout() r.insertChild(11, child1) self.assertEqual(r.childCount(), 1) self.assertEqual(r.children(), [child1]) - child2 = QgsReport() + child2 = QgsReportSectionLayout() r.insertChild(-1, child2) self.assertEqual(r.childCount(), 2) self.assertEqual(r.children(), [child2, child1]) @@ -86,9 +87,9 @@ def testRemoveChild(self): p = QgsProject() r = QgsReport() - child1 = QgsReport() + child1 = QgsReportSectionLayout() r.appendChild(child1) - child2 = QgsReport() + child2 = QgsReportSectionLayout() r.appendChild(child2) r.removeChildAt(-1) @@ -109,10 +110,10 @@ def testClone(self): p = QgsProject() r = QgsReport() - child1 = QgsReport() + child1 = QgsReportSectionLayout() child1.setHeaderEnabled(True) r.appendChild(child1) - child2 = QgsReport() + child2 = QgsReportSectionLayout() child2.setFooterEnabled(True) r.appendChild(child2) @@ -123,6 +124,13 @@ def testClone(self): self.assertFalse(cloned.child(1).headerEnabled()) self.assertTrue(cloned.child(1).footerEnabled()) + def testReportSectionLayout(self): + r = QgsReportSectionLayout() + p = QgsProject() + body = QgsLayout(p) + r.setBody(body) + self.assertEqual(r.body(), body) + if __name__ == '__main__': unittest.main() From 935dfa32d823e4d2d3c9c6266390cdda7fd434ed Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 29 Dec 2017 12:29:03 +1000 Subject: [PATCH 054/105] Add reprt iteration test --- .../core/layout/qgsabstractreportsection.sip | 9 +- src/core/layout/qgsabstractreportsection.cpp | 20 ++- src/core/layout/qgsabstractreportsection.h | 9 +- tests/src/python/test_qgsreport.py | 120 ++++++++++++++++++ 4 files changed, 152 insertions(+), 6 deletions(-) diff --git a/python/core/layout/qgsabstractreportsection.sip b/python/core/layout/qgsabstractreportsection.sip index 418ec0e4a2c9..1ff73074f624 100644 --- a/python/core/layout/qgsabstractreportsection.sip +++ b/python/core/layout/qgsabstractreportsection.sip @@ -54,6 +54,12 @@ implementations. virtual bool endRender(); + virtual QgsLayout *nextBody(); +%Docstring +Returns the next body layout to export, or a None if +no body layouts remain for this section. +%End + bool headerEnabled() const; %Docstring Returns true if the header for the section is enabled. @@ -217,6 +223,7 @@ Removes the child section at the specified ``index``, deleting it. { Header, Body, + Children, Footer, End, }; @@ -263,7 +270,7 @@ is transferred to the report section. virtual bool beginRender(); - virtual bool next(); + virtual QgsLayout *nextBody(); }; diff --git a/src/core/layout/qgsabstractreportsection.cpp b/src/core/layout/qgsabstractreportsection.cpp index b971b6e6530b..3f0c1a65de0b 100644 --- a/src/core/layout/qgsabstractreportsection.cpp +++ b/src/core/layout/qgsabstractreportsection.cpp @@ -77,7 +77,19 @@ bool QgsAbstractReportSection::next() case Body: { + // if we have a body, use it + if ( QgsLayout *body = nextBody() ) + { + mCurrentLayout = body; + return true; + } + mNextSection = Children; + FALLTHROUGH; + } + + case Children: + { // we iterate through all the section's children... while ( mNextChild < mChildren.count() ) { @@ -227,15 +239,15 @@ bool QgsReportSectionLayout::beginRender() return QgsAbstractReportSection::beginRender(); } -bool QgsReportSectionLayout::next() +QgsLayout *QgsReportSectionLayout::nextBody() { - if ( !mExportedBody ) + if ( !mExportedBody && mBody ) { mExportedBody = true; - return true; + return mBody.get(); } else { - return false; + return nullptr; } } diff --git a/src/core/layout/qgsabstractreportsection.h b/src/core/layout/qgsabstractreportsection.h index 7c6839380da2..0634873825f7 100644 --- a/src/core/layout/qgsabstractreportsection.h +++ b/src/core/layout/qgsabstractreportsection.h @@ -67,6 +67,12 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator bool next() override; bool endRender() override; + /** + * Returns the next body layout to export, or a nullptr if + * no body layouts remain for this section. + */ + virtual QgsLayout *nextBody() { return nullptr; } + /** * Returns true if the header for the section is enabled. * \see setHeaderEnabled() @@ -194,6 +200,7 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator { Header, //!< Header for section Body, //!< Body of section + Children, //!< Child sections Footer, //!< Footer for section End, //!< End of section (i.e. past all available content) }; @@ -248,7 +255,7 @@ class CORE_EXPORT QgsReportSectionLayout : public QgsAbstractReportSection QgsReportSectionLayout *clone() const override SIP_FACTORY; bool beginRender() override; - bool next() override; + QgsLayout *nextBody() override; private: diff --git a/tests/src/python/test_qgsreport.py b/tests/src/python/test_qgsreport.py index 00b8994f119c..044f28be7172 100644 --- a/tests/src/python/test_qgsreport.py +++ b/tests/src/python/test_qgsreport.py @@ -131,6 +131,126 @@ def testReportSectionLayout(self): r.setBody(body) self.assertEqual(r.body(), body) + def testIteration(self): + p = QgsProject() + r = QgsReport() + + # empty report + self.assertTrue(r.beginRender()) + self.assertFalse(r.next()) + + # add a header + r.setHeaderEnabled(True) + report_header = QgsLayout(p) + r.setHeader(report_header) + + self.assertTrue(r.beginRender()) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), report_header) + self.assertFalse(r.next()) + + # add a footer + r.setFooterEnabled(True) + report_footer = QgsLayout(p) + r.setFooter(report_footer) + + self.assertTrue(r.beginRender()) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), report_header) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), report_footer) + self.assertFalse(r.next()) + + # add a child + child1 = QgsReportSectionLayout() + child1_body = QgsLayout(p) + child1.setBody(child1_body) + r.appendChild(child1) + self.assertTrue(r.beginRender()) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), report_header) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child1_body) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), report_footer) + self.assertFalse(r.next()) + + # header and footer on child + child1_header = QgsLayout(p) + child1.setHeader(child1_header) + child1.setHeaderEnabled(True) + child1_footer = QgsLayout(p) + child1.setFooter(child1_footer) + child1.setFooterEnabled(True) + self.assertTrue(r.beginRender()) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), report_header) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child1_header) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child1_body) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child1_footer) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), report_footer) + self.assertFalse(r.next()) + + # add another child + child2 = QgsReportSectionLayout() + child2_header = QgsLayout(p) + child2.setHeader(child2_header) + child2.setHeaderEnabled(True) + child2_footer = QgsLayout(p) + child2.setFooter(child2_footer) + child2.setFooterEnabled(True) + r.appendChild(child2) + self.assertTrue(r.beginRender()) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), report_header) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child1_header) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child1_body) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child1_footer) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_header) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_footer) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), report_footer) + self.assertFalse(r.next()) + + # add a child to child2 + child2a = QgsReportSectionLayout() + child2a_header = QgsLayout(p) + child2a.setHeader(child2a_header) + child2a.setHeaderEnabled(True) + child2a_footer = QgsLayout(p) + child2a.setFooter(child2a_footer) + child2a.setFooterEnabled(True) + child2.appendChild(child2a) + self.assertTrue(r.beginRender()) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), report_header) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child1_header) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child1_body) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child1_footer) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_header) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2a_header) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2a_footer) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_footer) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), report_footer) + self.assertFalse(r.next()) + if __name__ == '__main__': unittest.main() From 767075a3d466035f452bf8849581f5a704b67809 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 29 Dec 2017 12:51:04 +1000 Subject: [PATCH 055/105] Hookup filename generation for reports --- python/core/layout/qgsabstractreportsection.sip | 4 +--- src/app/layout/qgslayoutdesignerdialog.cpp | 8 +++++--- src/core/layout/qgsabstractreportsection.cpp | 2 +- src/core/layout/qgsabstractreportsection.h | 5 +---- src/core/layout/qgslayoutatlas.cpp | 4 +++- tests/src/python/test_qgslayoutatlas.py | 10 +++++----- tests/src/python/test_qgslayoutexporter.py | 6 +++--- tests/src/python/test_qgsreport.py | 9 +++++++++ 8 files changed, 28 insertions(+), 20 deletions(-) diff --git a/python/core/layout/qgsabstractreportsection.sip b/python/core/layout/qgsabstractreportsection.sip index 1ff73074f624..353a6720d822 100644 --- a/python/core/layout/qgsabstractreportsection.sip +++ b/python/core/layout/qgsabstractreportsection.sip @@ -40,10 +40,8 @@ implementations. virtual int count(); - virtual QString filePath( const QString &baseFilePath, const QString &extension ); - - + virtual QString filePath( const QString &baseFilePath, const QString &extension ); virtual QgsLayout *layout(); diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 4d4394ea81d1..65b11e3b61ba 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -2081,7 +2081,8 @@ void QgsLayoutDesignerDialog::exportAtlasToRaster() feedback->cancel(); } ); - QgsLayoutExporter::ExportResult result = QgsLayoutExporter::exportToImage( printAtlas, dir, fileExt, settings, error, feedback.get() ); + QString fileName = QDir( dir ).filePath( QStringLiteral( "atlas" ) ); // filename is overridden by atlas + QgsLayoutExporter::ExportResult result = QgsLayoutExporter::exportToImage( printAtlas, fileName, fileExt, settings, error, feedback.get() ); QApplication::restoreOverrideCursor(); switch ( result ) @@ -2231,7 +2232,8 @@ void QgsLayoutDesignerDialog::exportAtlasToSvg() feedback->cancel(); } ); - QgsLayoutExporter::ExportResult result = QgsLayoutExporter::exportToSvg( printAtlas, dir, svgSettings, error, feedback.get() ); + QString filename = QDir( dir ).filePath( QStringLiteral( "atlas" ) ); // filename is overridden by atlas + QgsLayoutExporter::ExportResult result = QgsLayoutExporter::exportToSvg( printAtlas, filename, svgSettings, error, feedback.get() ); QApplication::restoreOverrideCursor(); switch ( result ) @@ -2406,7 +2408,7 @@ void QgsLayoutDesignerDialog::exportAtlasToPdf() return; } - outputFileName = dir; + outputFileName = QDir( dir ).filePath( QStringLiteral( "atlas" ) ); // filename is overridden by atlas } mView->setPaintingEnabled( false ); diff --git a/src/core/layout/qgsabstractreportsection.cpp b/src/core/layout/qgsabstractreportsection.cpp index 3f0c1a65de0b..554039d098c5 100644 --- a/src/core/layout/qgsabstractreportsection.cpp +++ b/src/core/layout/qgsabstractreportsection.cpp @@ -24,7 +24,7 @@ QgsAbstractReportSection::~QgsAbstractReportSection() QString QgsAbstractReportSection::filePath( const QString &baseFilePath, const QString &extension ) { - QString base = QDir( baseFilePath ).filePath( "report_" ) + QString::number( mSectionNumber ); + QString base = QStringLiteral( "%1_%2" ).arg( baseFilePath ).arg( mSectionNumber, 4, 10, QChar( '0' ) ); if ( !extension.startsWith( '.' ) ) base += '.'; base += extension; diff --git a/src/core/layout/qgsabstractreportsection.h b/src/core/layout/qgsabstractreportsection.h index 0634873825f7..d9d1a4baa586 100644 --- a/src/core/layout/qgsabstractreportsection.h +++ b/src/core/layout/qgsabstractreportsection.h @@ -54,14 +54,11 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator // TODO - how to handle this? int count() override { return -1; } - //TODO - baseFilePath should be a filename, not directory - QString filePath( const QString &baseFilePath, const QString &extension ) override; - - #if 0 //TODO virtual void setContext( const QgsLayoutReportContext &context ) = 0; #endif + QString filePath( const QString &baseFilePath, const QString &extension ) override; QgsLayout *layout() override; bool beginRender() override; bool next() override; diff --git a/src/core/layout/qgslayoutatlas.cpp b/src/core/layout/qgslayoutatlas.cpp index 6064f5d20de5..5c8709fd6152 100644 --- a/src/core/layout/qgslayoutatlas.cpp +++ b/src/core/layout/qgslayoutatlas.cpp @@ -341,7 +341,9 @@ int QgsLayoutAtlas::count() QString QgsLayoutAtlas::filePath( const QString &baseFilePath, const QString &extension ) { - QString base = QDir( baseFilePath ).filePath( mCurrentFilename ); + QFileInfo fi( baseFilePath ); + QDir dir = fi.dir(); // ignore everything except the directory + QString base = dir.filePath( mCurrentFilename ); if ( !extension.startsWith( '.' ) ) base += '.'; base += extension; diff --git a/tests/src/python/test_qgslayoutatlas.py b/tests/src/python/test_qgslayoutatlas.py index 79bd859a721a..a75eba56163b 100644 --- a/tests/src/python/test_qgslayoutatlas.py +++ b/tests/src/python/test_qgslayoutatlas.py @@ -305,19 +305,19 @@ def testFileName(self): self.assertEqual(atlas.count(), 4) atlas.first() self.assertEqual(atlas.currentFilename(), 'output_Basse-Normandie') - self.assertEqual(atlas.filePath('/tmp/output', 'png'), '/tmp/output/output_Basse-Normandie.png') - self.assertEqual(atlas.filePath('/tmp/output', '.png'), '/tmp/output/output_Basse-Normandie.png') + self.assertEqual(atlas.filePath('/tmp/output/', 'png'), '/tmp/output/output_Basse-Normandie.png') + self.assertEqual(atlas.filePath('/tmp/output/', '.png'), '/tmp/output/output_Basse-Normandie.png') self.assertEqual(atlas.filePath('/tmp/output/', 'svg'), '/tmp/output/output_Basse-Normandie.svg') atlas.next() self.assertEqual(atlas.currentFilename(), 'output_Bretagne') - self.assertEqual(atlas.filePath('/tmp/output', 'png'), '/tmp/output/output_Bretagne.png') + self.assertEqual(atlas.filePath('/tmp/output/', 'png'), '/tmp/output/output_Bretagne.png') atlas.next() self.assertEqual(atlas.currentFilename(), 'output_Pays de la Loire') - self.assertEqual(atlas.filePath('/tmp/output', 'png'), '/tmp/output/output_Pays de la Loire.png') + self.assertEqual(atlas.filePath('/tmp/output/', 'png'), '/tmp/output/output_Pays de la Loire.png') atlas.next() self.assertEqual(atlas.currentFilename(), 'output_Centre') - self.assertEqual(atlas.filePath('/tmp/output', 'png'), '/tmp/output/output_Centre.png') + self.assertEqual(atlas.filePath('/tmp/output/', 'png'), '/tmp/output/output_Centre.png') # try changing expression, filename should be updated instantly atlas.setFilenameExpression("'export_' || \"NAME_1\"") diff --git a/tests/src/python/test_qgslayoutexporter.py b/tests/src/python/test_qgslayoutexporter.py index 29162b2f9eca..f7dc5be8d241 100644 --- a/tests/src/python/test_qgslayoutexporter.py +++ b/tests/src/python/test_qgslayoutexporter.py @@ -632,7 +632,7 @@ def testIteratorToImages(self): settings = QgsLayoutExporter.ImageExportSettings() settings.dpi = 80 - result, error = QgsLayoutExporter.exportToImage(atlas, self.basetestpath, 'png', settings) + result, error = QgsLayoutExporter.exportToImage(atlas, self.basetestpath + '/', 'png', settings) self.assertEqual(result, QgsLayoutExporter.Success, error) page1_path = os.path.join(self.basetestpath, 'test_exportiteratortoimage_Basse-Normandie.png') @@ -654,7 +654,7 @@ def testIteratorToSvgs(self): settings.dpi = 80 settings.forceVectorOutput = False - result, error = QgsLayoutExporter.exportToSvg(atlas, self.basetestpath, settings) + result, error = QgsLayoutExporter.exportToSvg(atlas, self.basetestpath + '/', settings) self.assertEqual(result, QgsLayoutExporter.Success, error) page1_path = os.path.join(self.basetestpath, 'test_exportiteratortosvg_Basse-Normandie.svg') @@ -681,7 +681,7 @@ def testIteratorToPdfs(self): settings.rasterizeWholeImage = False settings.forceVectorOutput = False - result, error = QgsLayoutExporter.exportToPdfs(atlas, self.basetestpath, settings) + result, error = QgsLayoutExporter.exportToPdfs(atlas, self.basetestpath + '/', settings) self.assertEqual(result, QgsLayoutExporter.Success, error) page1_path = os.path.join(self.basetestpath, 'test_exportiteratortopdf_Basse-Normandie.pdf') diff --git a/tests/src/python/test_qgsreport.py b/tests/src/python/test_qgsreport.py index 044f28be7172..c7d28e999992 100644 --- a/tests/src/python/test_qgsreport.py +++ b/tests/src/python/test_qgsreport.py @@ -233,22 +233,31 @@ def testIteration(self): self.assertTrue(r.beginRender()) self.assertTrue(r.next()) self.assertEqual(r.layout(), report_header) + self.assertEqual(r.filePath('/tmp/myreport', 'png'), '/tmp/myreport_0001.png') self.assertTrue(r.next()) self.assertEqual(r.layout(), child1_header) + self.assertEqual(r.filePath('/tmp/myreport', 'png'), '/tmp/myreport_0002.png') self.assertTrue(r.next()) self.assertEqual(r.layout(), child1_body) + self.assertEqual(r.filePath('/tmp/myreport', '.png'), '/tmp/myreport_0003.png') self.assertTrue(r.next()) self.assertEqual(r.layout(), child1_footer) + self.assertEqual(r.filePath('/tmp/myreport', 'jpg'), '/tmp/myreport_0004.jpg') self.assertTrue(r.next()) self.assertEqual(r.layout(), child2_header) + self.assertEqual(r.filePath('/tmp/myreport', 'png'), '/tmp/myreport_0005.png') self.assertTrue(r.next()) self.assertEqual(r.layout(), child2a_header) + self.assertEqual(r.filePath('/tmp/myreport', 'png'), '/tmp/myreport_0006.png') self.assertTrue(r.next()) self.assertEqual(r.layout(), child2a_footer) + self.assertEqual(r.filePath('/tmp/myreport', 'png'), '/tmp/myreport_0007.png') self.assertTrue(r.next()) self.assertEqual(r.layout(), child2_footer) + self.assertEqual(r.filePath('/tmp/myreport', 'png'), '/tmp/myreport_0008.png') self.assertTrue(r.next()) self.assertEqual(r.layout(), report_footer) + self.assertEqual(r.filePath('/tmp/myreport', 'png'), '/tmp/myreport_0009.png') self.assertFalse(r.next()) From 57628faa01eec0282945fa871230b5d983c09210 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 29 Dec 2017 12:55:47 +1000 Subject: [PATCH 056/105] Add a unit test for exporting reports --- tests/src/python/test_qgslayoutexporter.py | 53 +++++++++++++++++- .../expected_layoutexporter_report_page1.png | Bin 0 -> 3981 bytes .../expected_layoutexporter_report_page2.png | Bin 0 -> 3970 bytes 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 tests/testdata/control_images/layout_exporter/expected_layoutexporter_report_page1/expected_layoutexporter_report_page1.png create mode 100644 tests/testdata/control_images/layout_exporter/expected_layoutexporter_report_page2/expected_layoutexporter_report_page2.png diff --git a/tests/src/python/test_qgslayoutexporter.py b/tests/src/python/test_qgslayoutexporter.py index f7dc5be8d241..937310bac77d 100644 --- a/tests/src/python/test_qgslayoutexporter.py +++ b/tests/src/python/test_qgslayoutexporter.py @@ -37,7 +37,8 @@ QgsVectorLayer, QgsCoordinateReferenceSystem, QgsPrintLayout, - QgsSingleSymbolRenderer) + QgsSingleSymbolRenderer, + QgsReport) from qgis.PyQt.QtCore import QSize, QSizeF, QDir, QRectF, Qt from qgis.PyQt.QtGui import QImage, QPainter from qgis.PyQt.QtSvg import QSvgRenderer, QSvgGenerator @@ -726,6 +727,56 @@ def testIteratorToPdf(self): pdfToPng(pdf_path, rendered_page_4, dpi=80, page=4) self.assertTrue(os.path.exists(rendered_page_4)) + def testExportReport(self): + p = QgsProject() + r = QgsReport() + + # add a header + r.setHeaderEnabled(True) + report_header = QgsLayout(p) + report_header.initializeDefaults() + item1 = QgsLayoutItemShape(report_header) + item1.attemptSetSceneRect(QRectF(10, 20, 100, 150)) + fill = QgsSimpleFillSymbolLayer() + fill_symbol = QgsFillSymbol() + fill_symbol.changeSymbolLayer(0, fill) + fill.setColor(Qt.green) + fill.setStrokeStyle(Qt.NoPen) + item1.setSymbol(fill_symbol) + report_header.addItem(item1) + + r.setHeader(report_header) + + # add a footer + r.setFooterEnabled(True) + report_footer = QgsLayout(p) + report_footer.initializeDefaults() + item2 = QgsLayoutItemShape(report_footer) + item2.attemptSetSceneRect(QRectF(10, 20, 100, 150)) + item2.attemptMove(QgsLayoutPoint(10, 20)) + fill = QgsSimpleFillSymbolLayer() + fill_symbol = QgsFillSymbol() + fill_symbol.changeSymbolLayer(0, fill) + fill.setColor(Qt.cyan) + fill.setStrokeStyle(Qt.NoPen) + item2.setSymbol(fill_symbol) + report_footer.addItem(item2) + + r.setFooter(report_footer) + + # setup settings + settings = QgsLayoutExporter.ImageExportSettings() + settings.dpi = 80 + + report_path = os.path.join(self.basetestpath, 'test_report') + result, error = QgsLayoutExporter.exportToImage(r, report_path, 'png', settings) + self.assertEqual(result, QgsLayoutExporter.Success, error) + + page1_path = os.path.join(self.basetestpath, 'test_report_0001.png') + self.assertTrue(self.checkImage('report_page1', 'report_page1', page1_path)) + page2_path = os.path.join(self.basetestpath, 'test_report_0002.png') + self.assertTrue(self.checkImage('report_page2', 'report_page2', page2_path)) + if __name__ == '__main__': unittest.main() diff --git a/tests/testdata/control_images/layout_exporter/expected_layoutexporter_report_page1/expected_layoutexporter_report_page1.png b/tests/testdata/control_images/layout_exporter/expected_layoutexporter_report_page1/expected_layoutexporter_report_page1.png new file mode 100644 index 0000000000000000000000000000000000000000..4ee4721dee60e50106a01a0c4f4a7324681432d9 GIT binary patch literal 3981 zcmeAS@N?(olHy`uVBq!ia0y~yU|!C^z%-SE4JcxKb8Zrl;wL$v=2);YFes?|F)}cm=nMvm7L5v18Yat&{us~yd2YK6+ zUQ1_ScynvpwWzBU_Bdb~ zEpq^ICI|!jDGX1JXaY&gLSX;J!EaP}G)#s_g4k(4J4sP;g}|96VDe?~boFyt=akR{ E0DKF9mjD0& literal 0 HcmV?d00001 diff --git a/tests/testdata/control_images/layout_exporter/expected_layoutexporter_report_page2/expected_layoutexporter_report_page2.png b/tests/testdata/control_images/layout_exporter/expected_layoutexporter_report_page2/expected_layoutexporter_report_page2.png new file mode 100644 index 0000000000000000000000000000000000000000..e6e4ff33056616f5a017ae8e865a1bb304a3795e GIT binary patch literal 3970 zcmeAS@N?(olHy`uVBq!ia0y~yU|!C^z%-SE4JcxKb8Zrl;w|n5r*tMhlpbvBXW%aikcUuDu z5vW?t52P3hfmS*A&0%6-m>>+aiQ&l+O(1DGDokOR?2NYjXKuXy6GMY=&ABtxwJZz= zj>_)def7unGd1Cl8PqG!omp(h#PHzJp0A@}Kbr7HbKPi>Gg=If)FVdQ&MBb@01bb7Qvd(} literal 0 HcmV?d00001 From 6284f5e36f87841a7786f9c665dda3aedc1c2c02 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 29 Dec 2017 13:12:18 +1000 Subject: [PATCH 057/105] Correct project and parent handling for report sections --- .../core/layout/qgsabstractreportsection.sip | 38 +++++++++++++-- src/core/layout/qgsabstractreportsection.cpp | 39 +++++++++++++-- src/core/layout/qgsabstractreportsection.h | 48 +++++++++++++++++-- tests/src/python/test_qgslayoutexporter.py | 2 +- tests/src/python/test_qgsreport.py | 22 ++++++--- 5 files changed, 131 insertions(+), 18 deletions(-) diff --git a/python/core/layout/qgsabstractreportsection.sip b/python/core/layout/qgsabstractreportsection.sip index 353a6720d822..c1d8f9ce71dc 100644 --- a/python/core/layout/qgsabstractreportsection.sip +++ b/python/core/layout/qgsabstractreportsection.sip @@ -20,9 +20,10 @@ class QgsAbstractReportSection : QgsAbstractLayoutIterator %End public: - QgsAbstractReportSection(); + QgsAbstractReportSection( QgsAbstractReportSection *parent = 0 ); %Docstring -Constructor for QgsAbstractReportSection +Constructor for QgsAbstractReportSection, attached to the specified ``parent`` section. +Note that ownership is not transferred to ``parent``. %End ~QgsAbstractReportSection(); @@ -36,6 +37,16 @@ transferred to the caller. Subclasses should call copyCommonProperties() in their clone() implementations. +%End + + QgsAbstractReportSection *parent(); +%Docstring +Returns the parent report section. +%End + + QgsProject *project(); +%Docstring +Returns the associated project. %End virtual int count(); @@ -230,6 +241,11 @@ Removes the child section at the specified ``index``, deleting it. %Docstring Copies the common properties of a report section to a ``destination`` section. This method should be called from clone() implementations. +%End + + void setParent( QgsAbstractReportSection *parent ); +%Docstring +Sets the ``parent`` report section. %End private: @@ -249,6 +265,12 @@ class QgsReportSectionLayout : QgsAbstractReportSection %End public: + QgsReportSectionLayout( QgsAbstractReportSection *parent = 0 ); +%Docstring +Constructor for QgsReportSectionLayout, attached to the specified ``parent`` section. +Note that ownership is not transferred to ``parent``. +%End + QgsLayout *body(); %Docstring Returns the body layout for the section. @@ -290,9 +312,17 @@ subclasses. %End public: - QgsReport(); + QgsReport( QgsProject *project ); +%Docstring +Constructor for QgsReport, associated with the specified +``project``. + +Note that ownership is not transferred to ``project``. +%End + + QgsProject *project(); %Docstring -Constructor for QgsReport. +Returns the associated project. %End virtual QgsReport *clone() const /Factory/; diff --git a/src/core/layout/qgsabstractreportsection.cpp b/src/core/layout/qgsabstractreportsection.cpp index 554039d098c5..b2aaaf637496 100644 --- a/src/core/layout/qgsabstractreportsection.cpp +++ b/src/core/layout/qgsabstractreportsection.cpp @@ -17,11 +17,31 @@ #include "qgsabstractreportsection.h" #include "qgslayout.h" +QgsAbstractReportSection::QgsAbstractReportSection( QgsAbstractReportSection *parent ) + : mParent( parent ) +{} + QgsAbstractReportSection::~QgsAbstractReportSection() { qDeleteAll( mChildren ); } +QgsProject *QgsAbstractReportSection::project() +{ + QgsAbstractReportSection *current = this; + while ( QgsAbstractReportSection *parent = current->parent() ) + { + if ( !parent ) + return nullptr; + + if ( QgsReport *report = dynamic_cast< QgsReport * >( parent ) ) + return report->project(); + + current = parent; + } + return nullptr; +} + QString QgsAbstractReportSection::filePath( const QString &baseFilePath, const QString &extension ) { QString base = QStringLiteral( "%1_%2" ).arg( baseFilePath ).arg( mSectionNumber, 4, 10, QChar( '0' ) ); @@ -158,11 +178,13 @@ QgsAbstractReportSection *QgsAbstractReportSection::child( int index ) void QgsAbstractReportSection::appendChild( QgsAbstractReportSection *section ) { + section->setParent( this ); mChildren.append( section ); } void QgsAbstractReportSection::insertChild( int index, QgsAbstractReportSection *section ) { + section->setParent( this ); index = std::max( 0, index ); index = std::min( index, mChildren.count() ); mChildren.insert( index, section ); @@ -202,16 +224,21 @@ void QgsAbstractReportSection::copyCommonProperties( QgsAbstractReportSection *d for ( QgsAbstractReportSection *child : qgis::as_const( mChildren ) ) { - destination->mChildren.append( child->clone() ); + destination->appendChild( child->clone() ); } } // QgsReport +QgsReport::QgsReport( QgsProject *project ) + : QgsAbstractReportSection( nullptr ) + , mProject( project ) +{} + QgsReport *QgsReport::clone() const { - std::unique_ptr< QgsReport > copy = qgis::make_unique< QgsReport >(); + std::unique_ptr< QgsReport > copy = qgis::make_unique< QgsReport >( mProject ); copyCommonProperties( copy.get() ); return copy.release(); } @@ -220,13 +247,19 @@ QgsReport *QgsReport::clone() const // QgsReportSectionLayout // +QgsReportSectionLayout::QgsReportSectionLayout( QgsAbstractReportSection *parent ) + : QgsAbstractReportSection( parent ) +{} + QgsReportSectionLayout *QgsReportSectionLayout::clone() const { - std::unique_ptr< QgsReportSectionLayout > copy = qgis::make_unique< QgsReportSectionLayout >(); + std::unique_ptr< QgsReportSectionLayout > copy = qgis::make_unique< QgsReportSectionLayout >( nullptr ); copyCommonProperties( copy.get() ); if ( mBody ) + { copy->mBody.reset( mBody->clone() ); + } else copy->mBody.reset(); diff --git a/src/core/layout/qgsabstractreportsection.h b/src/core/layout/qgsabstractreportsection.h index d9d1a4baa586..532a7c0275bc 100644 --- a/src/core/layout/qgsabstractreportsection.h +++ b/src/core/layout/qgsabstractreportsection.h @@ -31,8 +31,11 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator public: - //! Constructor for QgsAbstractReportSection - QgsAbstractReportSection() = default; + /** + * Constructor for QgsAbstractReportSection, attached to the specified \a parent section. + * Note that ownership is not transferred to \a parent. + */ + QgsAbstractReportSection( QgsAbstractReportSection *parent = nullptr ); ~QgsAbstractReportSection() override; @@ -51,6 +54,16 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator */ virtual QgsAbstractReportSection *clone() const = 0 SIP_FACTORY; + /** + * Returns the parent report section. + */ + QgsAbstractReportSection *parent() { return mParent; } + + /** + * Returns the associated project. + */ + QgsProject *project(); + // TODO - how to handle this? int count() override { return -1; } @@ -208,8 +221,15 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator */ void copyCommonProperties( QgsAbstractReportSection *destination ) const; + /** + * Sets the \a parent report section. + */ + void setParent( QgsAbstractReportSection *parent ) { mParent = parent; } + private: + QgsAbstractReportSection *mParent = nullptr; + int mSectionNumber = 0; SubSection mNextSection = Header; int mNextChild = 0; @@ -237,6 +257,12 @@ class CORE_EXPORT QgsReportSectionLayout : public QgsAbstractReportSection { public: + /** + * Constructor for QgsReportSectionLayout, attached to the specified \a parent section. + * Note that ownership is not transferred to \a parent. + */ + QgsReportSectionLayout( QgsAbstractReportSection *parent = nullptr ); + /** * Returns the body layout for the section. * \see setBody() @@ -277,11 +303,25 @@ class CORE_EXPORT QgsReport : public QgsAbstractReportSection public: - //! Constructor for QgsReport. - QgsReport() = default; + /** + * Constructor for QgsReport, associated with the specified + * \a project. + * + * Note that ownership is not transferred to \a project. + */ + QgsReport( QgsProject *project ); + + /** + * Returns the associated project. + */ + QgsProject *project() { return mProject; } QgsReport *clone() const override SIP_FACTORY; + private: + + QgsProject *mProject = nullptr; + }; #endif //QGSABSTRACTREPORTSECTION_H diff --git a/tests/src/python/test_qgslayoutexporter.py b/tests/src/python/test_qgslayoutexporter.py index 937310bac77d..c1cfb74b3ad5 100644 --- a/tests/src/python/test_qgslayoutexporter.py +++ b/tests/src/python/test_qgslayoutexporter.py @@ -729,7 +729,7 @@ def testIteratorToPdf(self): def testExportReport(self): p = QgsProject() - r = QgsReport() + r = QgsReport(p) # add a header r.setHeaderEnabled(True) diff --git a/tests/src/python/test_qgsreport.py b/tests/src/python/test_qgsreport.py index c7d28e999992..a6b9705231bf 100644 --- a/tests/src/python/test_qgsreport.py +++ b/tests/src/python/test_qgsreport.py @@ -27,7 +27,9 @@ class TestQgsReport(unittest.TestCase): def testGettersSetters(self): p = QgsProject() - r = QgsReport() + r = QgsReport(p) + + self.assertEqual(r.project(), p) r.setHeaderEnabled(True) self.assertTrue(r.headerEnabled()) @@ -45,7 +47,7 @@ def testGettersSetters(self): def testChildren(self): p = QgsProject() - r = QgsReport() + r = QgsReport(p) self.assertEqual(r.childCount(), 0) self.assertEqual(r.children(), []) self.assertIsNone(r.child(-1)) @@ -60,32 +62,38 @@ def testChildren(self): # append child child1 = QgsReportSectionLayout() + self.assertIsNone(child1.project()) r.appendChild(child1) self.assertEqual(r.childCount(), 1) self.assertEqual(r.children(), [child1]) self.assertEqual(r.child(0), child1) + self.assertEqual(child1.parent(), r) + self.assertEqual(child1.project(), p) child2 = QgsReportSectionLayout() r.appendChild(child2) self.assertEqual(r.childCount(), 2) self.assertEqual(r.children(), [child1, child2]) self.assertEqual(r.child(1), child2) + self.assertEqual(child2.parent(), r) def testInsertChild(self): p = QgsProject() - r = QgsReport() + r = QgsReport(p) child1 = QgsReportSectionLayout() r.insertChild(11, child1) self.assertEqual(r.childCount(), 1) self.assertEqual(r.children(), [child1]) + self.assertEqual(child1.parent(), r) child2 = QgsReportSectionLayout() r.insertChild(-1, child2) self.assertEqual(r.childCount(), 2) self.assertEqual(r.children(), [child2, child1]) + self.assertEqual(child2.parent(), r) def testRemoveChild(self): p = QgsProject() - r = QgsReport() + r = QgsReport(p) child1 = QgsReportSectionLayout() r.appendChild(child1) @@ -108,7 +116,7 @@ def testRemoveChild(self): def testClone(self): p = QgsProject() - r = QgsReport() + r = QgsReport(p) child1 = QgsReportSectionLayout() child1.setHeaderEnabled(True) @@ -121,8 +129,10 @@ def testClone(self): self.assertEqual(cloned.childCount(), 2) self.assertTrue(cloned.child(0).headerEnabled()) self.assertFalse(cloned.child(0).footerEnabled()) + self.assertEqual(cloned.child(0).parent(), cloned) self.assertFalse(cloned.child(1).headerEnabled()) self.assertTrue(cloned.child(1).footerEnabled()) + self.assertEqual(cloned.child(1).parent(), cloned) def testReportSectionLayout(self): r = QgsReportSectionLayout() @@ -133,7 +143,7 @@ def testReportSectionLayout(self): def testIteration(self): p = QgsProject() - r = QgsReport() + r = QgsReport(p) # empty report self.assertTrue(r.beginRender()) From 159986fdecc22e97bed6812277d5e1709de81d88 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 29 Dec 2017 15:30:19 +1000 Subject: [PATCH 058/105] Implement QgsReportSectionFieldGroup --- .../core/layout/qgsabstractreportsection.sip | 137 +++++++++++- src/core/layout/qgsabstractreportsection.cpp | 202 ++++++++++++++++-- src/core/layout/qgsabstractreportsection.h | 129 ++++++++++- tests/src/python/test_qgsreport.py | 161 +++++++++++++- 4 files changed, 594 insertions(+), 35 deletions(-) diff --git a/python/core/layout/qgsabstractreportsection.sip b/python/core/layout/qgsabstractreportsection.sip index c1d8f9ce71dc..4b2fe599f111 100644 --- a/python/core/layout/qgsabstractreportsection.sip +++ b/python/core/layout/qgsabstractreportsection.sip @@ -7,11 +7,29 @@ ************************************************************************/ + + + +class QgsReportContext +{ + +%TypeHeaderCode +#include "qgsabstractreportsection.h" +%End + public: + +}; + class QgsAbstractReportSection : QgsAbstractLayoutIterator { %Docstring An abstract base class for QgsReport subsections. +.. warning:: + + This is not considered stable API, and may change in future QGIS releases. It is +exposed to the Python bindings for unit testing purposes only. + .. versionadded:: 3.0 %End @@ -51,7 +69,6 @@ Returns the associated project. virtual int count(); - virtual QString filePath( const QString &baseFilePath, const QString &extension ); virtual QgsLayout *layout(); @@ -63,10 +80,17 @@ Returns the associated project. virtual bool endRender(); - virtual QgsLayout *nextBody(); + virtual void reset(); +%Docstring +Resets the section, ready for a new iteration. +%End + + virtual QgsLayout *nextBody( bool &ok /Out/ ); %Docstring Returns the next body layout to export, or a None if -no body layouts remain for this section. +no body layout is required this iteration. + +``ok`` will be set to false if no bodies remain for this section. %End bool headerEnabled() const; @@ -171,7 +195,7 @@ sections form the body of the report section. .. seealso:: :py:func:`children()` %End - QList< QgsAbstractReportSection * > children(); + QList< QgsAbstractReportSection * > children() const; %Docstring Return all child sections for this report section. The child sections form the body of the report section. @@ -224,6 +248,20 @@ Removes a child ``section``, deleting it. Removes the child section at the specified ``index``, deleting it. .. seealso:: :py:func:`children()` +%End + + void setContext( const QgsReportContext &context ); +%Docstring +Sets the current ``context`` for this section. + +.. seealso:: :py:func:`context()` +%End + + const QgsReportContext &context() const; +%Docstring +Returns the current context for this section. + +.. seealso:: :py:func:`setContext()` %End protected: @@ -257,6 +295,11 @@ class QgsReportSectionLayout : QgsAbstractReportSection %Docstring A report section consisting of a single QgsLayout body. +.. warning:: + + This is not considered stable API, and may change in future QGIS releases. It is +exposed to the Python bindings for unit testing purposes only. + .. versionadded:: 3.0 %End @@ -290,7 +333,85 @@ is transferred to the report section. virtual bool beginRender(); - virtual QgsLayout *nextBody(); + virtual QgsLayout *nextBody( bool &ok ); + + +}; + +class QgsReportSectionFieldGroup : QgsAbstractReportSection +{ +%Docstring + A report section consisting of a features + +.. warning:: + + This is not considered stable API, and may change in future QGIS releases. It is +exposed to the Python bindings for unit testing purposes only. + +.. versionadded:: 3.0 +%End + +%TypeHeaderCode +#include "qgsabstractreportsection.h" +%End + public: + + QgsReportSectionFieldGroup( QgsAbstractReportSection *parent = 0 ); +%Docstring +Constructor for QgsReportSectionFieldGroup, attached to the specified ``parent`` section. +Note that ownership is not transferred to ``parent``. +%End + + QgsLayout *body(); +%Docstring +Returns the body layout for the section. + +.. seealso:: :py:func:`setBody()` +%End + + void setBody( QgsLayout *body /Transfer/ ); +%Docstring +Sets the ``body`` layout for the section. Ownership of ``body`` +is transferred to the report section. + +.. seealso:: :py:func:`body()` +%End + + QgsVectorLayer *layer(); +%Docstring +Returns the vector layer associated with this section. + +.. seealso:: :py:func:`setLayer()` +%End + + void setLayer( QgsVectorLayer *layer ); +%Docstring +Sets the vector ``layer`` associated with this section. + +.. seealso:: :py:func:`layer()` +%End + + QString field() const; +%Docstring +Returns the field associated with this section. + +.. seealso:: :py:func:`setField()` +%End + + void setField( const QString &field ); +%Docstring +Sets the ``field`` associated with this section. + +.. seealso:: :py:func:`field()` +%End + + virtual QgsReportSectionFieldGroup *clone() const /Factory/; + + virtual bool beginRender(); + + virtual QgsLayout *nextBody( bool &ok ); + + virtual void reset(); }; @@ -304,6 +425,11 @@ class QgsReport : QgsAbstractReportSection Reports consist of multiple sections, represented by :py:class:`QgsAbstractReportSection` subclasses. +.. warning:: + + This is not considered stable API, and may change in future QGIS releases. It is +exposed to the Python bindings for unit testing purposes only. + .. versionadded:: 3.0 %End @@ -330,6 +456,7 @@ Returns the associated project. }; + /************************************************************************ * This file has been generated automatically from * * * diff --git a/src/core/layout/qgsabstractreportsection.cpp b/src/core/layout/qgsabstractreportsection.cpp index b2aaaf637496..77ab746205bf 100644 --- a/src/core/layout/qgsabstractreportsection.cpp +++ b/src/core/layout/qgsabstractreportsection.cpp @@ -17,6 +17,8 @@ #include "qgsabstractreportsection.h" #include "qgslayout.h" +///@cond NOT_STABLE + QgsAbstractReportSection::QgsAbstractReportSection( QgsAbstractReportSection *parent ) : mParent( parent ) {} @@ -42,6 +44,15 @@ QgsProject *QgsAbstractReportSection::project() return nullptr; } +void QgsAbstractReportSection::setContext( const QgsReportContext &context ) +{ + mContext = context; + for ( QgsAbstractReportSection *section : qgis::as_const( mChildren ) ) + { + section->setContext( mContext ); + } +} + QString QgsAbstractReportSection::filePath( const QString &baseFilePath, const QString &extension ) { QString base = QStringLiteral( "%1_%2" ).arg( baseFilePath ).arg( mSectionNumber, 4, 10, QChar( '0' ) ); @@ -59,9 +70,7 @@ QgsLayout *QgsAbstractReportSection::layout() bool QgsAbstractReportSection::beginRender() { // reset this section - mCurrentLayout = nullptr; - mNextChild = 0; - mNextSection = Header; + reset(); mSectionNumber = 0; // and all children too @@ -91,42 +100,70 @@ bool QgsAbstractReportSection::next() return true; } - // but if not, then the current section is the body + // but if not, then the current section is a body + mNextSection = Body; FALLTHROUGH; } case Body: { - // if we have a body, use it - if ( QgsLayout *body = nextBody() ) + mNextSection = Children; + + bool ok = false; + // if we have a next body available, use it + QgsLayout *body = nextBody( ok ); + if ( body ) { + mNextChild = 0; mCurrentLayout = body; return true; } - mNextSection = Children; FALLTHROUGH; } case Children: { - // we iterate through all the section's children... - while ( mNextChild < mChildren.count() ) + bool bodiesAvailable = false; + do { - // ... staying on the current child only while it still has content for us - if ( mChildren.at( mNextChild )->next() ) + // we iterate through all the section's children... + while ( mNextChild < mChildren.count() ) { - mCurrentLayout = mChildren.at( mNextChild )->layout(); - return true; + // ... staying on the current child only while it still has content for us + if ( mChildren.at( mNextChild )->next() ) + { + mCurrentLayout = mChildren.at( mNextChild )->layout(); + return true; + } + else + { + // no more content for this child, so move to next child + mNextChild++; + } } - else + + // used up all the children + // if we have a next body available, use it + QgsLayout *body = nextBody( bodiesAvailable ); + if ( bodiesAvailable ) { - // no more content for this child, so move to next child - mNextChild++; + mNextChild = 0; + + for ( QgsAbstractReportSection *section : qgis::as_const( mChildren ) ) + { + section->reset(); + } + } + if ( body ) + { + mCurrentLayout = body; + return true; } } + while ( bodiesAvailable ); - // all children have spent their content, so move to the footer + // all children and bodies have spent their content, so move to the footer mNextSection = Footer; FALLTHROUGH; } @@ -158,9 +195,7 @@ bool QgsAbstractReportSection::next() bool QgsAbstractReportSection::endRender() { // reset this section - mCurrentLayout = nullptr; - mNextChild = 0; - mNextSection = Header; + reset(); // and all children too bool result = true; @@ -171,6 +206,17 @@ bool QgsAbstractReportSection::endRender() return result; } +void QgsAbstractReportSection::reset() +{ + mCurrentLayout = nullptr; + mNextChild = 0; + mNextSection = Header; + for ( QgsAbstractReportSection *section : qgis::as_const( mChildren ) ) + { + section->reset(); + } +} + QgsAbstractReportSection *QgsAbstractReportSection::child( int index ) { return mChildren.value( index ); @@ -272,15 +318,129 @@ bool QgsReportSectionLayout::beginRender() return QgsAbstractReportSection::beginRender(); } -QgsLayout *QgsReportSectionLayout::nextBody() +QgsLayout *QgsReportSectionLayout::nextBody( bool &ok ) { if ( !mExportedBody && mBody ) { mExportedBody = true; + ok = true; return mBody.get(); } else { + ok = false; return nullptr; } } + +// +// QgsReportSectionFieldGroup +// + +QgsReportSectionFieldGroup::QgsReportSectionFieldGroup( QgsAbstractReportSection *parent ) + : QgsAbstractReportSection( parent ) +{ + +} + +QgsReportSectionFieldGroup *QgsReportSectionFieldGroup::clone() const +{ + std::unique_ptr< QgsReportSectionFieldGroup > copy = qgis::make_unique< QgsReportSectionFieldGroup >( nullptr ); + copyCommonProperties( copy.get() ); + + if ( mBody ) + { + copy->mBody.reset( mBody->clone() ); + } + else + copy->mBody.reset(); + + copy->setLayer( mCoverageLayer.get() ); + copy->setField( mField ); + + return copy.release(); +} + +bool QgsReportSectionFieldGroup::beginRender() +{ + if ( !mCoverageLayer.get() ) + return false; + + if ( !mField.isEmpty() ) + { + mFieldIndex = mCoverageLayer->fields().lookupField( mField ); + if ( mFieldIndex < 0 ) + return false; + + if ( mBody ) + mBody->reportContext().setLayer( mCoverageLayer.get() ); + + mFeatures = QgsFeatureIterator(); + } + return QgsAbstractReportSection::beginRender(); +} + +QgsLayout *QgsReportSectionFieldGroup::nextBody( bool &ok ) +{ + if ( !mFeatures.isValid() ) + { + QgsFeatureRequest request; + QString filter = context().layerFilters.value( mCoverageLayer.get() ); + if ( !filter.isEmpty() ) + request.setFilterExpression( filter ); + request.addOrderBy( mField, true ); + mFeatures = mCoverageLayer->getFeatures( request ); + } + + QgsFeature f; + QVariant currentValue; + bool first = true; + while ( first || ( !mBody && mEncounteredValues.contains( currentValue ) ) ) + { + if ( !mFeatures.nextFeature( f ) ) + { + // no features left for this iteration + mFeatures = QgsFeatureIterator(); + ok = false; + return nullptr; + } + + first = false; + currentValue = f.attribute( mFieldIndex ); + } + + mEncounteredValues.insert( currentValue ); + + QgsReportContext c = context(); + QString currentFilter = c.layerFilters.value( mCoverageLayer.get() ); + QString thisFilter = QgsExpression::createFieldEqualityExpression( mField, currentValue ); + QString newFilter = currentFilter.isEmpty() ? thisFilter : QStringLiteral( "(%1) AND (%2)" ).arg( currentFilter, thisFilter ); + c.layerFilters[ mCoverageLayer.get() ] = newFilter; + + const QList< QgsAbstractReportSection * > sections = children(); + for ( QgsAbstractReportSection *section : qgis::as_const( sections ) ) + { + section->setContext( c ); + } + + ok = true; + + if ( mBody ) + { + mBody->reportContext().blockSignals( true ); + mBody->reportContext().setLayer( mCoverageLayer.get() ); + mBody->reportContext().blockSignals( false ); + mBody->reportContext().setFeature( f ); + } + + return mBody.get(); +} + +void QgsReportSectionFieldGroup::reset() +{ + QgsAbstractReportSection::reset(); + mEncounteredValues.clear(); +} + +///@endcond + diff --git a/src/core/layout/qgsabstractreportsection.h b/src/core/layout/qgsabstractreportsection.h index 532a7c0275bc..88340a84c292 100644 --- a/src/core/layout/qgsabstractreportsection.h +++ b/src/core/layout/qgsabstractreportsection.h @@ -19,11 +19,26 @@ #include "qgis_core.h" #include "qgsabstractlayoutiterator.h" #include "qgslayoutreportcontext.h" +#include "qgsvectorlayerref.h" + + +///@cond NOT_STABLE + +// This is not considered stable API - it is exposed to python bindings only for unit testing! + +class CORE_EXPORT QgsReportContext +{ + public: + + QMap< QgsVectorLayer *, QString > layerFilters SIP_SKIP; +}; /** * \ingroup core * \class QgsAbstractReportSection * \brief An abstract base class for QgsReport subsections. + * \warning This is not considered stable API, and may change in future QGIS releases. It is + * exposed to the Python bindings for unit testing purposes only. * \since QGIS 3.0 */ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator @@ -67,21 +82,24 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator // TODO - how to handle this? int count() override { return -1; } -#if 0 //TODO - virtual void setContext( const QgsLayoutReportContext &context ) = 0; -#endif - QString filePath( const QString &baseFilePath, const QString &extension ) override; QgsLayout *layout() override; bool beginRender() override; bool next() override; bool endRender() override; + /** + * Resets the section, ready for a new iteration. + */ + virtual void reset(); + /** * Returns the next body layout to export, or a nullptr if - * no body layouts remain for this section. + * no body layout is required this iteration. + * + * \a ok will be set to false if no bodies remain for this section. */ - virtual QgsLayout *nextBody() { return nullptr; } + virtual QgsLayout *nextBody( bool &ok SIP_OUT ) { ok = false; return nullptr; } /** * Returns true if the header for the section is enabled. @@ -169,7 +187,7 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator * \see insertChild() * \see removeChild() */ - QList< QgsAbstractReportSection * > children() { return mChildren; } + QList< QgsAbstractReportSection * > children() const { return mChildren; } /** * Returns the child section at the specified \a index. @@ -203,6 +221,18 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator */ void removeChildAt( int index ); + /** + * Sets the current \a context for this section. + * \see context() + */ + void setContext( const QgsReportContext &context ); + + /** + * Returns the current context for this section. + * \see setContext() + */ + const QgsReportContext &context() const { return mContext; } + protected: //! Report sub-sections @@ -242,6 +272,8 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator QList< QgsAbstractReportSection * > mChildren; + QgsReportContext mContext; + #ifdef SIP_RUN QgsAbstractReportSection( const QgsAbstractReportSection &other ); #endif @@ -251,6 +283,8 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator * \ingroup core * \class QgsReportSectionLayout * \brief A report section consisting of a single QgsLayout body. + * \warning This is not considered stable API, and may change in future QGIS releases. It is + * exposed to the Python bindings for unit testing purposes only. * \since QGIS 3.0 */ class CORE_EXPORT QgsReportSectionLayout : public QgsAbstractReportSection @@ -278,7 +312,7 @@ class CORE_EXPORT QgsReportSectionLayout : public QgsAbstractReportSection QgsReportSectionLayout *clone() const override SIP_FACTORY; bool beginRender() override; - QgsLayout *nextBody() override; + QgsLayout *nextBody( bool &ok ) override; private: @@ -287,6 +321,80 @@ class CORE_EXPORT QgsReportSectionLayout : public QgsAbstractReportSection }; +/** + * \ingroup core + * \class QgsReportSectionFieldGroup + * \brief A report section consisting of a features + * + * \warning This is not considered stable API, and may change in future QGIS releases. It is + * exposed to the Python bindings for unit testing purposes only. + * + * \since QGIS 3.0 + */ +class CORE_EXPORT QgsReportSectionFieldGroup : public QgsAbstractReportSection +{ + public: + + /** + * Constructor for QgsReportSectionFieldGroup, attached to the specified \a parent section. + * Note that ownership is not transferred to \a parent. + */ + QgsReportSectionFieldGroup( QgsAbstractReportSection *parent = nullptr ); + + /** + * Returns the body layout for the section. + * \see setBody() + */ + QgsLayout *body() { return mBody.get(); } + + /** + * Sets the \a body layout for the section. Ownership of \a body + * is transferred to the report section. + * \see body() + */ + void setBody( QgsLayout *body SIP_TRANSFER ) { mBody.reset( body ); } + + /** + * Returns the vector layer associated with this section. + * \see setLayer() + */ + QgsVectorLayer *layer() { return mCoverageLayer.get(); } + + /** + * Sets the vector \a layer associated with this section. + * \see layer() + */ + void setLayer( QgsVectorLayer *layer ) { mCoverageLayer = layer; } + + /** + * Returns the field associated with this section. + * \see setField() + */ + QString field() const { return mField; } + + /** + * Sets the \a field associated with this section. + * \see field() + */ + void setField( const QString &field ) { mField = field; } + + QgsReportSectionFieldGroup *clone() const override SIP_FACTORY; + bool beginRender() override; + QgsLayout *nextBody( bool &ok ) override; + void reset() override; + + private: + + QgsVectorLayerRef mCoverageLayer; + QString mField; + int mFieldIndex = -1; + QgsFeatureIterator mFeatures; + QSet< QVariant > mEncounteredValues; + + std::unique_ptr< QgsLayout > mBody; + +}; + /** * \ingroup core @@ -296,6 +404,9 @@ class CORE_EXPORT QgsReportSectionLayout : public QgsAbstractReportSection * Reports consist of multiple sections, represented by QgsAbstractReportSection * subclasses. * + * \warning This is not considered stable API, and may change in future QGIS releases. It is + * exposed to the Python bindings for unit testing purposes only. + * * \since QGIS 3.0 */ class CORE_EXPORT QgsReport : public QgsAbstractReportSection @@ -324,4 +435,6 @@ class CORE_EXPORT QgsReport : public QgsAbstractReportSection }; +///@endcond + #endif //QGSABSTRACTREPORTSECTION_H diff --git a/tests/src/python/test_qgsreport.py b/tests/src/python/test_qgsreport.py index a6b9705231bf..37539ad8919f 100644 --- a/tests/src/python/test_qgsreport.py +++ b/tests/src/python/test_qgsreport.py @@ -17,7 +17,11 @@ from qgis.core import (QgsProject, QgsLayout, QgsReport, - QgsReportSectionLayout) + QgsReportSectionLayout, + QgsReportSectionFieldGroup, + QgsVectorLayer, + QgsField, + QgsFeature) from qgis.testing import start_app, unittest start_app() @@ -270,6 +274,161 @@ def testIteration(self): self.assertEqual(r.filePath('/tmp/myreport', 'png'), '/tmp/myreport_0009.png') self.assertFalse(r.next()) + def testFieldGroup(self): + # create a layer + ptLayer = QgsVectorLayer("Point?crs=epsg:4326&field=country:string(20)&field=state:string(20)&field=town:string(20)", "points", "memory") + + attributes = [ + ['Australia', 'QLD', 'Brisbane'], + ['Australia', 'QLD', 'Emerald'], + ['NZ', 'state1', 'town1'], + ['Australia', 'VIC', 'Melbourne'], + ['NZ', 'state1', 'town2'], + ['Australia', 'QLD', 'Beerburrum'], + ['Australia', 'VIC', 'Geelong'], + ['NZ', 'state2', 'town2'], + ['PNG', 'state1', 'town1'], + ['Australia', 'NSW', 'Sydney'] + ] + + pr = ptLayer.dataProvider() + for a in attributes: + f = QgsFeature() + f.initAttributes(3) + f.setAttribute(0, a[0]) + f.setAttribute(1, a[1]) + f.setAttribute(2, a[2]) + self.assertTrue(pr.addFeature(f)) + + p = QgsProject() + r = QgsReport(p) + + # add a child + child1 = QgsReportSectionFieldGroup() + child1_body = QgsLayout(p) + child1.setLayer(ptLayer) + child1.setBody(child1_body) + child1.setField('country') + r.appendChild(child1) + self.assertTrue(r.beginRender()) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child1_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Brisbane']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child1_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Emerald']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child1_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'VIC', 'Melbourne']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child1_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Beerburrum']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child1_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'VIC', 'Geelong']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child1_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'NSW', 'Sydney']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child1_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state1', 'town1']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child1_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state1', 'town2']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child1_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state2', 'town2']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child1_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['PNG', 'state1', 'town1']) + self.assertFalse(r.next()) + + # another group + # remove body from child1 + child1.setBody(None) + + child2 = QgsReportSectionFieldGroup() + child2_body = QgsLayout(p) + child2.setLayer(ptLayer) + child2.setBody(child2_body) + child2.setField('state') + child1.appendChild(child2) + self.assertTrue(r.beginRender()) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'NSW', 'Sydney']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Brisbane']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Emerald']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Beerburrum']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'VIC', 'Melbourne']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'VIC', 'Geelong']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state1', 'town1']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state1', 'town2']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state2', 'town2']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['PNG', 'state1', 'town1']) + self.assertFalse(r.next()) + + # another group + # remove body from child1 + child2.setBody(None) + + child3 = QgsReportSectionFieldGroup() + child3_body = QgsLayout(p) + child3.setLayer(ptLayer) + child3.setBody(child3_body) + child3.setField('town') + child2.appendChild(child3) + self.assertTrue(r.beginRender()) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'NSW', 'Sydney']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Beerburrum']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Brisbane']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Emerald']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'VIC', 'Geelong']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'VIC', 'Melbourne']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state1', 'town1']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state1', 'town2']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state2', 'town2']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['PNG', 'state1', 'town1']) + self.assertFalse(r.next()) + if __name__ == '__main__': unittest.main() From d8af098d837d4dabaf0d4ea4c52121cd16666eca Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 29 Dec 2017 16:03:52 +1000 Subject: [PATCH 059/105] Code shuffle and cleanup --- python/core/core_auto.sip | 3 + .../core/layout/qgsabstractreportsection.sip | 182 ++---------------- python/core/layout/qgsreport.sip | 59 ++++++ .../layout/qgsreportsectionfieldgroup.sip | 99 ++++++++++ python/core/layout/qgsreportsectionlayout.sip | 67 +++++++ src/core/CMakeLists.txt | 6 + src/core/layout/qgsabstractreportsection.cpp | 171 +--------------- src/core/layout/qgsabstractreportsection.h | 173 ++--------------- src/core/layout/qgsreport.cpp | 35 ++++ src/core/layout/qgsreport.h | 68 +++++++ .../layout/qgsreportsectionfieldgroup.cpp | 147 ++++++++++++++ src/core/layout/qgsreportsectionfieldgroup.h | 109 +++++++++++ src/core/layout/qgsreportsectionlayout.cpp | 63 ++++++ src/core/layout/qgsreportsectionlayout.h | 70 +++++++ 14 files changed, 754 insertions(+), 498 deletions(-) create mode 100644 python/core/layout/qgsreport.sip create mode 100644 python/core/layout/qgsreportsectionfieldgroup.sip create mode 100644 python/core/layout/qgsreportsectionlayout.sip create mode 100644 src/core/layout/qgsreport.cpp create mode 100644 src/core/layout/qgsreport.h create mode 100644 src/core/layout/qgsreportsectionfieldgroup.cpp create mode 100644 src/core/layout/qgsreportsectionfieldgroup.h create mode 100644 src/core/layout/qgsreportsectionlayout.cpp create mode 100644 src/core/layout/qgsreportsectionlayout.h diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index 1342a112ef05..8005fc0ec92d 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -175,6 +175,9 @@ %Include layout/qgslayoutsnapper.sip %Include layout/qgslayoutundocommand.sip %Include layout/qgslayoututils.sip +%Include layout/qgsreport.sip +%Include layout/qgsreportsectionfieldgroup.sip +%Include layout/qgsreportsectionlayout.sip %Include metadata/qgslayermetadata.sip %Include metadata/qgslayermetadatavalidator.sip %Include metadata/qgslayermetadataformatter.sip diff --git a/python/core/layout/qgsabstractreportsection.sip b/python/core/layout/qgsabstractreportsection.sip index 4b2fe599f111..34afcbbef2d4 100644 --- a/python/core/layout/qgsabstractreportsection.sip +++ b/python/core/layout/qgsabstractreportsection.sip @@ -10,8 +10,18 @@ -class QgsReportContext +class QgsReportSectionContext { +%Docstring + Current context for a report section. + +.. warning:: + + This is not considered stable API, and may change in future QGIS releases. It is +exposed to the Python bindings for unit testing purposes only. + +.. versionadded:: 3.0 +%End %TypeHeaderCode #include "qgsabstractreportsection.h" @@ -250,14 +260,14 @@ Removes the child section at the specified ``index``, deleting it. .. seealso:: :py:func:`children()` %End - void setContext( const QgsReportContext &context ); + void setContext( const QgsReportSectionContext &context ); %Docstring Sets the current ``context`` for this section. .. seealso:: :py:func:`context()` %End - const QgsReportContext &context() const; + const QgsReportSectionContext &context() const; %Docstring Returns the current context for this section. @@ -290,172 +300,6 @@ Sets the ``parent`` report section. QgsAbstractReportSection( const QgsAbstractReportSection &other ); }; -class QgsReportSectionLayout : QgsAbstractReportSection -{ -%Docstring - A report section consisting of a single QgsLayout body. - -.. warning:: - - This is not considered stable API, and may change in future QGIS releases. It is -exposed to the Python bindings for unit testing purposes only. - -.. versionadded:: 3.0 -%End - -%TypeHeaderCode -#include "qgsabstractreportsection.h" -%End - public: - - QgsReportSectionLayout( QgsAbstractReportSection *parent = 0 ); -%Docstring -Constructor for QgsReportSectionLayout, attached to the specified ``parent`` section. -Note that ownership is not transferred to ``parent``. -%End - - QgsLayout *body(); -%Docstring -Returns the body layout for the section. - -.. seealso:: :py:func:`setBody()` -%End - - void setBody( QgsLayout *body /Transfer/ ); -%Docstring -Sets the ``body`` layout for the section. Ownership of ``body`` -is transferred to the report section. - -.. seealso:: :py:func:`body()` -%End - - virtual QgsReportSectionLayout *clone() const /Factory/; - - virtual bool beginRender(); - - virtual QgsLayout *nextBody( bool &ok ); - - -}; - -class QgsReportSectionFieldGroup : QgsAbstractReportSection -{ -%Docstring - A report section consisting of a features - -.. warning:: - - This is not considered stable API, and may change in future QGIS releases. It is -exposed to the Python bindings for unit testing purposes only. - -.. versionadded:: 3.0 -%End - -%TypeHeaderCode -#include "qgsabstractreportsection.h" -%End - public: - - QgsReportSectionFieldGroup( QgsAbstractReportSection *parent = 0 ); -%Docstring -Constructor for QgsReportSectionFieldGroup, attached to the specified ``parent`` section. -Note that ownership is not transferred to ``parent``. -%End - - QgsLayout *body(); -%Docstring -Returns the body layout for the section. - -.. seealso:: :py:func:`setBody()` -%End - - void setBody( QgsLayout *body /Transfer/ ); -%Docstring -Sets the ``body`` layout for the section. Ownership of ``body`` -is transferred to the report section. - -.. seealso:: :py:func:`body()` -%End - - QgsVectorLayer *layer(); -%Docstring -Returns the vector layer associated with this section. - -.. seealso:: :py:func:`setLayer()` -%End - - void setLayer( QgsVectorLayer *layer ); -%Docstring -Sets the vector ``layer`` associated with this section. - -.. seealso:: :py:func:`layer()` -%End - - QString field() const; -%Docstring -Returns the field associated with this section. - -.. seealso:: :py:func:`setField()` -%End - - void setField( const QString &field ); -%Docstring -Sets the ``field`` associated with this section. - -.. seealso:: :py:func:`field()` -%End - - virtual QgsReportSectionFieldGroup *clone() const /Factory/; - - virtual bool beginRender(); - - virtual QgsLayout *nextBody( bool &ok ); - - virtual void reset(); - - -}; - - -class QgsReport : QgsAbstractReportSection -{ -%Docstring - Represents a report for use with the QgsLayout engine. - -Reports consist of multiple sections, represented by :py:class:`QgsAbstractReportSection` -subclasses. - -.. warning:: - - This is not considered stable API, and may change in future QGIS releases. It is -exposed to the Python bindings for unit testing purposes only. - -.. versionadded:: 3.0 -%End - -%TypeHeaderCode -#include "qgsabstractreportsection.h" -%End - public: - - QgsReport( QgsProject *project ); -%Docstring -Constructor for QgsReport, associated with the specified -``project``. - -Note that ownership is not transferred to ``project``. -%End - - QgsProject *project(); -%Docstring -Returns the associated project. -%End - - virtual QgsReport *clone() const /Factory/; - - -}; - /************************************************************************ * This file has been generated automatically from * diff --git a/python/core/layout/qgsreport.sip b/python/core/layout/qgsreport.sip new file mode 100644 index 000000000000..624bbf5c3d39 --- /dev/null +++ b/python/core/layout/qgsreport.sip @@ -0,0 +1,59 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layout/qgsreport.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + +class QgsReport : QgsAbstractReportSection +{ +%Docstring + Represents a report for use with the QgsLayout engine. + +Reports consist of multiple sections, represented by QgsAbstractReportSection +subclasses. + +.. warning:: + + This is not considered stable API, and may change in future QGIS releases. It is +exposed to the Python bindings for unit testing purposes only. + +.. versionadded:: 3.0 +%End + +%TypeHeaderCode +#include "qgsreport.h" +%End + public: + + QgsReport( QgsProject *project ); +%Docstring +Constructor for QgsReport, associated with the specified +``project``. + +Note that ownership is not transferred to ``project``. +%End + + QgsProject *project(); +%Docstring +Returns the associated project. +%End + + virtual QgsReport *clone() const /Factory/; + + +}; + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layout/qgsreport.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/layout/qgsreportsectionfieldgroup.sip b/python/core/layout/qgsreportsectionfieldgroup.sip new file mode 100644 index 000000000000..977e65f95fe0 --- /dev/null +++ b/python/core/layout/qgsreportsectionfieldgroup.sip @@ -0,0 +1,99 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layout/qgsreportsectionfieldgroup.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + +class QgsReportSectionFieldGroup : QgsAbstractReportSection +{ +%Docstring + A report section consisting of a features + +.. warning:: + + This is not considered stable API, and may change in future QGIS releases. It is +exposed to the Python bindings for unit testing purposes only. + +.. versionadded:: 3.0 +%End + +%TypeHeaderCode +#include "qgsreportsectionfieldgroup.h" +%End + public: + + QgsReportSectionFieldGroup( QgsAbstractReportSection *parent = 0 ); +%Docstring +Constructor for QgsReportSectionFieldGroup, attached to the specified ``parent`` section. +Note that ownership is not transferred to ``parent``. +%End + + QgsLayout *body(); +%Docstring +Returns the body layout for the section. + +.. seealso:: :py:func:`setBody()` +%End + + void setBody( QgsLayout *body /Transfer/ ); +%Docstring +Sets the ``body`` layout for the section. Ownership of ``body`` +is transferred to the report section. + +.. seealso:: :py:func:`body()` +%End + + QgsVectorLayer *layer(); +%Docstring +Returns the vector layer associated with this section. + +.. seealso:: :py:func:`setLayer()` +%End + + void setLayer( QgsVectorLayer *layer ); +%Docstring +Sets the vector ``layer`` associated with this section. + +.. seealso:: :py:func:`layer()` +%End + + QString field() const; +%Docstring +Returns the field associated with this section. + +.. seealso:: :py:func:`setField()` +%End + + void setField( const QString &field ); +%Docstring +Sets the ``field`` associated with this section. + +.. seealso:: :py:func:`field()` +%End + + virtual QgsReportSectionFieldGroup *clone() const /Factory/; + + virtual bool beginRender(); + + virtual QgsLayout *nextBody( bool &ok ); + + virtual void reset(); + + +}; + + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layout/qgsreportsectionfieldgroup.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/layout/qgsreportsectionlayout.sip b/python/core/layout/qgsreportsectionlayout.sip new file mode 100644 index 000000000000..7bfdab56365d --- /dev/null +++ b/python/core/layout/qgsreportsectionlayout.sip @@ -0,0 +1,67 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layout/qgsreportsectionlayout.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsReportSectionLayout : QgsAbstractReportSection +{ +%Docstring + A report section consisting of a single QgsLayout body. + +.. warning:: + + This is not considered stable API, and may change in future QGIS releases. It is +exposed to the Python bindings for unit testing purposes only. + +.. versionadded:: 3.0 +%End + +%TypeHeaderCode +#include "qgsreportsectionlayout.h" +%End + public: + + QgsReportSectionLayout( QgsAbstractReportSection *parent = 0 ); +%Docstring +Constructor for QgsReportSectionLayout, attached to the specified ``parent`` section. +Note that ownership is not transferred to ``parent``. +%End + + QgsLayout *body(); +%Docstring +Returns the body layout for the section. + +.. seealso:: :py:func:`setBody()` +%End + + void setBody( QgsLayout *body /Transfer/ ); +%Docstring +Sets the ``body`` layout for the section. Ownership of ``body`` +is transferred to the report section. + +.. seealso:: :py:func:`body()` +%End + + virtual QgsReportSectionLayout *clone() const /Factory/; + + virtual bool beginRender(); + + virtual QgsLayout *nextBody( bool &ok ); + + +}; + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layout/qgsreportsectionlayout.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index d1a766009b01..ab67925a8817 100755 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -415,6 +415,9 @@ SET(QGIS_CORE_SRCS layout/qgslayoutserializableobject.cpp layout/qgslayoutsize.cpp layout/qgsprintlayout.cpp + layout/qgsreport.cpp + layout/qgsreportsectionfieldgroup.cpp + layout/qgsreportsectionlayout.cpp pal/costcalculator.cpp pal/feature.cpp @@ -1045,6 +1048,9 @@ SET(QGIS_CORE_HDRS layout/qgslayoutsnapper.h layout/qgslayoutundocommand.h layout/qgslayoututils.h + layout/qgsreport.h + layout/qgsreportsectionfieldgroup.h + layout/qgsreportsectionlayout.h metadata/qgslayermetadata.h metadata/qgslayermetadatavalidator.h diff --git a/src/core/layout/qgsabstractreportsection.cpp b/src/core/layout/qgsabstractreportsection.cpp index 77ab746205bf..04b680fe1e58 100644 --- a/src/core/layout/qgsabstractreportsection.cpp +++ b/src/core/layout/qgsabstractreportsection.cpp @@ -16,6 +16,7 @@ #include "qgsabstractreportsection.h" #include "qgslayout.h" +#include "qgsreport.h" ///@cond NOT_STABLE @@ -44,7 +45,7 @@ QgsProject *QgsAbstractReportSection::project() return nullptr; } -void QgsAbstractReportSection::setContext( const QgsReportContext &context ) +void QgsAbstractReportSection::setContext( const QgsReportSectionContext &context ) { mContext = context; for ( QgsAbstractReportSection *section : qgis::as_const( mChildren ) ) @@ -274,173 +275,5 @@ void QgsAbstractReportSection::copyCommonProperties( QgsAbstractReportSection *d } } - -// QgsReport - -QgsReport::QgsReport( QgsProject *project ) - : QgsAbstractReportSection( nullptr ) - , mProject( project ) -{} - -QgsReport *QgsReport::clone() const -{ - std::unique_ptr< QgsReport > copy = qgis::make_unique< QgsReport >( mProject ); - copyCommonProperties( copy.get() ); - return copy.release(); -} - -// -// QgsReportSectionLayout -// - -QgsReportSectionLayout::QgsReportSectionLayout( QgsAbstractReportSection *parent ) - : QgsAbstractReportSection( parent ) -{} - -QgsReportSectionLayout *QgsReportSectionLayout::clone() const -{ - std::unique_ptr< QgsReportSectionLayout > copy = qgis::make_unique< QgsReportSectionLayout >( nullptr ); - copyCommonProperties( copy.get() ); - - if ( mBody ) - { - copy->mBody.reset( mBody->clone() ); - } - else - copy->mBody.reset(); - - return copy.release(); -} - -bool QgsReportSectionLayout::beginRender() -{ - mExportedBody = false; - return QgsAbstractReportSection::beginRender(); -} - -QgsLayout *QgsReportSectionLayout::nextBody( bool &ok ) -{ - if ( !mExportedBody && mBody ) - { - mExportedBody = true; - ok = true; - return mBody.get(); - } - else - { - ok = false; - return nullptr; - } -} - -// -// QgsReportSectionFieldGroup -// - -QgsReportSectionFieldGroup::QgsReportSectionFieldGroup( QgsAbstractReportSection *parent ) - : QgsAbstractReportSection( parent ) -{ - -} - -QgsReportSectionFieldGroup *QgsReportSectionFieldGroup::clone() const -{ - std::unique_ptr< QgsReportSectionFieldGroup > copy = qgis::make_unique< QgsReportSectionFieldGroup >( nullptr ); - copyCommonProperties( copy.get() ); - - if ( mBody ) - { - copy->mBody.reset( mBody->clone() ); - } - else - copy->mBody.reset(); - - copy->setLayer( mCoverageLayer.get() ); - copy->setField( mField ); - - return copy.release(); -} - -bool QgsReportSectionFieldGroup::beginRender() -{ - if ( !mCoverageLayer.get() ) - return false; - - if ( !mField.isEmpty() ) - { - mFieldIndex = mCoverageLayer->fields().lookupField( mField ); - if ( mFieldIndex < 0 ) - return false; - - if ( mBody ) - mBody->reportContext().setLayer( mCoverageLayer.get() ); - - mFeatures = QgsFeatureIterator(); - } - return QgsAbstractReportSection::beginRender(); -} - -QgsLayout *QgsReportSectionFieldGroup::nextBody( bool &ok ) -{ - if ( !mFeatures.isValid() ) - { - QgsFeatureRequest request; - QString filter = context().layerFilters.value( mCoverageLayer.get() ); - if ( !filter.isEmpty() ) - request.setFilterExpression( filter ); - request.addOrderBy( mField, true ); - mFeatures = mCoverageLayer->getFeatures( request ); - } - - QgsFeature f; - QVariant currentValue; - bool first = true; - while ( first || ( !mBody && mEncounteredValues.contains( currentValue ) ) ) - { - if ( !mFeatures.nextFeature( f ) ) - { - // no features left for this iteration - mFeatures = QgsFeatureIterator(); - ok = false; - return nullptr; - } - - first = false; - currentValue = f.attribute( mFieldIndex ); - } - - mEncounteredValues.insert( currentValue ); - - QgsReportContext c = context(); - QString currentFilter = c.layerFilters.value( mCoverageLayer.get() ); - QString thisFilter = QgsExpression::createFieldEqualityExpression( mField, currentValue ); - QString newFilter = currentFilter.isEmpty() ? thisFilter : QStringLiteral( "(%1) AND (%2)" ).arg( currentFilter, thisFilter ); - c.layerFilters[ mCoverageLayer.get() ] = newFilter; - - const QList< QgsAbstractReportSection * > sections = children(); - for ( QgsAbstractReportSection *section : qgis::as_const( sections ) ) - { - section->setContext( c ); - } - - ok = true; - - if ( mBody ) - { - mBody->reportContext().blockSignals( true ); - mBody->reportContext().setLayer( mCoverageLayer.get() ); - mBody->reportContext().blockSignals( false ); - mBody->reportContext().setFeature( f ); - } - - return mBody.get(); -} - -void QgsReportSectionFieldGroup::reset() -{ - QgsAbstractReportSection::reset(); - mEncounteredValues.clear(); -} - ///@endcond diff --git a/src/core/layout/qgsabstractreportsection.h b/src/core/layout/qgsabstractreportsection.h index 88340a84c292..8907bba566cf 100644 --- a/src/core/layout/qgsabstractreportsection.h +++ b/src/core/layout/qgsabstractreportsection.h @@ -26,10 +26,19 @@ // This is not considered stable API - it is exposed to python bindings only for unit testing! -class CORE_EXPORT QgsReportContext +/** + * \ingroup core + * \class QgsReportSectionContext + * \brief Current context for a report section. + * \warning This is not considered stable API, and may change in future QGIS releases. It is + * exposed to the Python bindings for unit testing purposes only. + * \since QGIS 3.0 + */ +class CORE_EXPORT QgsReportSectionContext { public: + //! Current layer filters QMap< QgsVectorLayer *, QString > layerFilters SIP_SKIP; }; @@ -225,13 +234,13 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator * Sets the current \a context for this section. * \see context() */ - void setContext( const QgsReportContext &context ); + void setContext( const QgsReportSectionContext &context ); /** * Returns the current context for this section. * \see setContext() */ - const QgsReportContext &context() const { return mContext; } + const QgsReportSectionContext &context() const { return mContext; } protected: @@ -272,169 +281,13 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator QList< QgsAbstractReportSection * > mChildren; - QgsReportContext mContext; + QgsReportSectionContext mContext; #ifdef SIP_RUN QgsAbstractReportSection( const QgsAbstractReportSection &other ); #endif }; -/** - * \ingroup core - * \class QgsReportSectionLayout - * \brief A report section consisting of a single QgsLayout body. - * \warning This is not considered stable API, and may change in future QGIS releases. It is - * exposed to the Python bindings for unit testing purposes only. - * \since QGIS 3.0 - */ -class CORE_EXPORT QgsReportSectionLayout : public QgsAbstractReportSection -{ - public: - - /** - * Constructor for QgsReportSectionLayout, attached to the specified \a parent section. - * Note that ownership is not transferred to \a parent. - */ - QgsReportSectionLayout( QgsAbstractReportSection *parent = nullptr ); - - /** - * Returns the body layout for the section. - * \see setBody() - */ - QgsLayout *body() { return mBody.get(); } - - /** - * Sets the \a body layout for the section. Ownership of \a body - * is transferred to the report section. - * \see body() - */ - void setBody( QgsLayout *body SIP_TRANSFER ) { mBody.reset( body ); } - - QgsReportSectionLayout *clone() const override SIP_FACTORY; - bool beginRender() override; - QgsLayout *nextBody( bool &ok ) override; - - private: - - bool mExportedBody = false; - std::unique_ptr< QgsLayout > mBody; - -}; - -/** - * \ingroup core - * \class QgsReportSectionFieldGroup - * \brief A report section consisting of a features - * - * \warning This is not considered stable API, and may change in future QGIS releases. It is - * exposed to the Python bindings for unit testing purposes only. - * - * \since QGIS 3.0 - */ -class CORE_EXPORT QgsReportSectionFieldGroup : public QgsAbstractReportSection -{ - public: - - /** - * Constructor for QgsReportSectionFieldGroup, attached to the specified \a parent section. - * Note that ownership is not transferred to \a parent. - */ - QgsReportSectionFieldGroup( QgsAbstractReportSection *parent = nullptr ); - - /** - * Returns the body layout for the section. - * \see setBody() - */ - QgsLayout *body() { return mBody.get(); } - - /** - * Sets the \a body layout for the section. Ownership of \a body - * is transferred to the report section. - * \see body() - */ - void setBody( QgsLayout *body SIP_TRANSFER ) { mBody.reset( body ); } - - /** - * Returns the vector layer associated with this section. - * \see setLayer() - */ - QgsVectorLayer *layer() { return mCoverageLayer.get(); } - - /** - * Sets the vector \a layer associated with this section. - * \see layer() - */ - void setLayer( QgsVectorLayer *layer ) { mCoverageLayer = layer; } - - /** - * Returns the field associated with this section. - * \see setField() - */ - QString field() const { return mField; } - - /** - * Sets the \a field associated with this section. - * \see field() - */ - void setField( const QString &field ) { mField = field; } - - QgsReportSectionFieldGroup *clone() const override SIP_FACTORY; - bool beginRender() override; - QgsLayout *nextBody( bool &ok ) override; - void reset() override; - - private: - - QgsVectorLayerRef mCoverageLayer; - QString mField; - int mFieldIndex = -1; - QgsFeatureIterator mFeatures; - QSet< QVariant > mEncounteredValues; - - std::unique_ptr< QgsLayout > mBody; - -}; - - -/** - * \ingroup core - * \class QgsReport - * \brief Represents a report for use with the QgsLayout engine. - * - * Reports consist of multiple sections, represented by QgsAbstractReportSection - * subclasses. - * - * \warning This is not considered stable API, and may change in future QGIS releases. It is - * exposed to the Python bindings for unit testing purposes only. - * - * \since QGIS 3.0 - */ -class CORE_EXPORT QgsReport : public QgsAbstractReportSection -{ - - public: - - /** - * Constructor for QgsReport, associated with the specified - * \a project. - * - * Note that ownership is not transferred to \a project. - */ - QgsReport( QgsProject *project ); - - /** - * Returns the associated project. - */ - QgsProject *project() { return mProject; } - - QgsReport *clone() const override SIP_FACTORY; - - private: - - QgsProject *mProject = nullptr; - -}; - ///@endcond #endif //QGSABSTRACTREPORTSECTION_H diff --git a/src/core/layout/qgsreport.cpp b/src/core/layout/qgsreport.cpp new file mode 100644 index 000000000000..c1859381893a --- /dev/null +++ b/src/core/layout/qgsreport.cpp @@ -0,0 +1,35 @@ +/*************************************************************************** + qgsreport.cpp + -------------------- + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsreport.h" +#include "qgslayout.h" + +///@cond NOT_STABLE + +QgsReport::QgsReport( QgsProject *project ) + : QgsAbstractReportSection( nullptr ) + , mProject( project ) +{} + +QgsReport *QgsReport::clone() const +{ + std::unique_ptr< QgsReport > copy = qgis::make_unique< QgsReport >( mProject ); + copyCommonProperties( copy.get() ); + return copy.release(); +} + +///@endcond + diff --git a/src/core/layout/qgsreport.h b/src/core/layout/qgsreport.h new file mode 100644 index 000000000000..f912e7d6b958 --- /dev/null +++ b/src/core/layout/qgsreport.h @@ -0,0 +1,68 @@ +/*************************************************************************** + qgsreport.h + --------------------------- + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#ifndef QGSREPORT_H +#define QGSREPORT_H + +#include "qgis_core.h" +#include "qgsabstractreportsection.h" + + +///@cond NOT_STABLE + +// This is not considered stable API - it is exposed to python bindings only for unit testing! + +/** + * \ingroup core + * \class QgsReport + * \brief Represents a report for use with the QgsLayout engine. + * + * Reports consist of multiple sections, represented by QgsAbstractReportSection + * subclasses. + * + * \warning This is not considered stable API, and may change in future QGIS releases. It is + * exposed to the Python bindings for unit testing purposes only. + * + * \since QGIS 3.0 + */ +class CORE_EXPORT QgsReport : public QgsAbstractReportSection +{ + + public: + + /** + * Constructor for QgsReport, associated with the specified + * \a project. + * + * Note that ownership is not transferred to \a project. + */ + QgsReport( QgsProject *project ); + + /** + * Returns the associated project. + */ + QgsProject *project() { return mProject; } + + QgsReport *clone() const override SIP_FACTORY; + + private: + + QgsProject *mProject = nullptr; + +}; + +///@endcond + +#endif //QGSREPORT_H diff --git a/src/core/layout/qgsreportsectionfieldgroup.cpp b/src/core/layout/qgsreportsectionfieldgroup.cpp new file mode 100644 index 000000000000..09477167c1f1 --- /dev/null +++ b/src/core/layout/qgsreportsectionfieldgroup.cpp @@ -0,0 +1,147 @@ +/*************************************************************************** + qgsreportsectionfieldgroup.cpp + -------------------- + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsreportsectionfieldgroup.h" +#include "qgslayout.h" + +///@cond NOT_STABLE + +QgsReportSectionFieldGroup::QgsReportSectionFieldGroup( QgsAbstractReportSection *parent ) + : QgsAbstractReportSection( parent ) +{ + +} + +QgsReportSectionFieldGroup *QgsReportSectionFieldGroup::clone() const +{ + std::unique_ptr< QgsReportSectionFieldGroup > copy = qgis::make_unique< QgsReportSectionFieldGroup >( nullptr ); + copyCommonProperties( copy.get() ); + + if ( mBody ) + { + copy->mBody.reset( mBody->clone() ); + } + else + copy->mBody.reset(); + + copy->setLayer( mCoverageLayer.get() ); + copy->setField( mField ); + + return copy.release(); +} + +bool QgsReportSectionFieldGroup::beginRender() +{ + if ( !mCoverageLayer.get() ) + return false; + + if ( !mField.isEmpty() ) + { + mFieldIndex = mCoverageLayer->fields().lookupField( mField ); + if ( mFieldIndex < 0 ) + return false; + + if ( mBody ) + mBody->reportContext().setLayer( mCoverageLayer.get() ); + + mFeatures = QgsFeatureIterator(); + } + return QgsAbstractReportSection::beginRender(); +} + +QgsLayout *QgsReportSectionFieldGroup::nextBody( bool &ok ) +{ + if ( !mFeatures.isValid() ) + { + mFeatures = mCoverageLayer->getFeatures( buildFeatureRequest() ); + } + + QgsFeature f = getNextFeature(); + if ( !f.isValid() ) + { + // no features left for this iteration + mFeatures = QgsFeatureIterator(); + ok = false; + return nullptr; + } + + updateChildContexts( f ); + + ok = true; + if ( mBody ) + { + mBody->reportContext().blockSignals( true ); + mBody->reportContext().setLayer( mCoverageLayer.get() ); + mBody->reportContext().blockSignals( false ); + mBody->reportContext().setFeature( f ); + } + + return mBody.get(); +} + +void QgsReportSectionFieldGroup::reset() +{ + QgsAbstractReportSection::reset(); + mEncounteredValues.clear(); +} + +QgsFeatureRequest QgsReportSectionFieldGroup::buildFeatureRequest() const +{ + QgsFeatureRequest request; + QString filter = context().layerFilters.value( mCoverageLayer.get() ); + if ( !filter.isEmpty() ) + request.setFilterExpression( filter ); + request.addOrderBy( mField, true ); + return request; +} + +QgsFeature QgsReportSectionFieldGroup::getNextFeature() +{ + QgsFeature f; + QVariant currentValue; + bool first = true; + while ( first || ( !mBody && mEncounteredValues.contains( currentValue ) ) ) + { + if ( !mFeatures.nextFeature( f ) ) + { + return QgsFeature(); + } + + first = false; + currentValue = f.attribute( mFieldIndex ); + } + + mEncounteredValues.insert( currentValue ); + return f; +} + +void QgsReportSectionFieldGroup::updateChildContexts( const QgsFeature &feature ) +{ + QgsReportSectionContext c = context(); + QString currentFilter = c.layerFilters.value( mCoverageLayer.get() ); + QString thisFilter = QgsExpression::createFieldEqualityExpression( mField, feature.attribute( mFieldIndex ) ); + QString newFilter = currentFilter.isEmpty() ? thisFilter : QStringLiteral( "(%1) AND (%2)" ).arg( currentFilter, thisFilter ); + c.layerFilters[ mCoverageLayer.get() ] = newFilter; + + const QList< QgsAbstractReportSection * > sections = children(); + for ( QgsAbstractReportSection *section : qgis::as_const( sections ) ) + { + section->setContext( c ); + } +} + +///@endcond + diff --git a/src/core/layout/qgsreportsectionfieldgroup.h b/src/core/layout/qgsreportsectionfieldgroup.h new file mode 100644 index 000000000000..6075376ac991 --- /dev/null +++ b/src/core/layout/qgsreportsectionfieldgroup.h @@ -0,0 +1,109 @@ +/*************************************************************************** + qgsreportsectionfieldgroup.h + --------------------------- + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#ifndef QGSREPORTSECTIONFIELDGROUP_H +#define QGSREPORTSECTIONFIELDGROUP_H + +#include "qgis_core.h" +#include "qgsabstractreportsection.h" + + +///@cond NOT_STABLE + +// This is not considered stable API - it is exposed to python bindings only for unit testing! + +/** + * \ingroup core + * \class QgsReportSectionFieldGroup + * \brief A report section consisting of a features + * + * \warning This is not considered stable API, and may change in future QGIS releases. It is + * exposed to the Python bindings for unit testing purposes only. + * + * \since QGIS 3.0 + */ +class CORE_EXPORT QgsReportSectionFieldGroup : public QgsAbstractReportSection +{ + public: + + /** + * Constructor for QgsReportSectionFieldGroup, attached to the specified \a parent section. + * Note that ownership is not transferred to \a parent. + */ + QgsReportSectionFieldGroup( QgsAbstractReportSection *parent = nullptr ); + + /** + * Returns the body layout for the section. + * \see setBody() + */ + QgsLayout *body() { return mBody.get(); } + + /** + * Sets the \a body layout for the section. Ownership of \a body + * is transferred to the report section. + * \see body() + */ + void setBody( QgsLayout *body SIP_TRANSFER ) { mBody.reset( body ); } + + /** + * Returns the vector layer associated with this section. + * \see setLayer() + */ + QgsVectorLayer *layer() { return mCoverageLayer.get(); } + + /** + * Sets the vector \a layer associated with this section. + * \see layer() + */ + void setLayer( QgsVectorLayer *layer ) { mCoverageLayer = layer; } + + /** + * Returns the field associated with this section. + * \see setField() + */ + QString field() const { return mField; } + + /** + * Sets the \a field associated with this section. + * \see field() + */ + void setField( const QString &field ) { mField = field; } + + QgsReportSectionFieldGroup *clone() const override SIP_FACTORY; + bool beginRender() override; + QgsLayout *nextBody( bool &ok ) override; + void reset() override; + + private: + + QgsVectorLayerRef mCoverageLayer; + QString mField; + int mFieldIndex = -1; + QgsFeatureIterator mFeatures; + QSet< QVariant > mEncounteredValues; + + std::unique_ptr< QgsLayout > mBody; + + QgsFeatureRequest buildFeatureRequest() const; + + QgsFeature getNextFeature(); + void updateChildContexts( const QgsFeature &feature ); + +}; + + +///@endcond + +#endif //QGSREPORTSECTIONFIELDGROUP_H diff --git a/src/core/layout/qgsreportsectionlayout.cpp b/src/core/layout/qgsreportsectionlayout.cpp new file mode 100644 index 000000000000..fa5d85615b5f --- /dev/null +++ b/src/core/layout/qgsreportsectionlayout.cpp @@ -0,0 +1,63 @@ +/*************************************************************************** + qgsreportsectionlayout.cpp + -------------------- + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsreportsectionlayout.h" +#include "qgslayout.h" + +///@cond NOT_STABLE + +QgsReportSectionLayout::QgsReportSectionLayout( QgsAbstractReportSection *parent ) + : QgsAbstractReportSection( parent ) +{} + +QgsReportSectionLayout *QgsReportSectionLayout::clone() const +{ + std::unique_ptr< QgsReportSectionLayout > copy = qgis::make_unique< QgsReportSectionLayout >( nullptr ); + copyCommonProperties( copy.get() ); + + if ( mBody ) + { + copy->mBody.reset( mBody->clone() ); + } + else + copy->mBody.reset(); + + return copy.release(); +} + +bool QgsReportSectionLayout::beginRender() +{ + mExportedBody = false; + return QgsAbstractReportSection::beginRender(); +} + +QgsLayout *QgsReportSectionLayout::nextBody( bool &ok ) +{ + if ( !mExportedBody && mBody ) + { + mExportedBody = true; + ok = true; + return mBody.get(); + } + else + { + ok = false; + return nullptr; + } +} + +///@endcond + diff --git a/src/core/layout/qgsreportsectionlayout.h b/src/core/layout/qgsreportsectionlayout.h new file mode 100644 index 000000000000..9ddd053c4acd --- /dev/null +++ b/src/core/layout/qgsreportsectionlayout.h @@ -0,0 +1,70 @@ +/*************************************************************************** + qgsreportsectionlayout.h + --------------------------- + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#ifndef QGSREPORTSECTIONLAYOUT_H +#define QGSREPORTSECTIONLAYOUT_H + +#include "qgis_core.h" +#include "qgsabstractreportsection.h" + +///@cond NOT_STABLE + +// This is not considered stable API - it is exposed to python bindings only for unit testing! + +/** + * \ingroup core + * \class QgsReportSectionLayout + * \brief A report section consisting of a single QgsLayout body. + * \warning This is not considered stable API, and may change in future QGIS releases. It is + * exposed to the Python bindings for unit testing purposes only. + * \since QGIS 3.0 + */ +class CORE_EXPORT QgsReportSectionLayout : public QgsAbstractReportSection +{ + public: + + /** + * Constructor for QgsReportSectionLayout, attached to the specified \a parent section. + * Note that ownership is not transferred to \a parent. + */ + QgsReportSectionLayout( QgsAbstractReportSection *parent = nullptr ); + + /** + * Returns the body layout for the section. + * \see setBody() + */ + QgsLayout *body() { return mBody.get(); } + + /** + * Sets the \a body layout for the section. Ownership of \a body + * is transferred to the report section. + * \see body() + */ + void setBody( QgsLayout *body SIP_TRANSFER ) { mBody.reset( body ); } + + QgsReportSectionLayout *clone() const override SIP_FACTORY; + bool beginRender() override; + QgsLayout *nextBody( bool &ok ) override; + + private: + + bool mExportedBody = false; + std::unique_ptr< QgsLayout > mBody; + +}; + +///@endcond + +#endif //QGSREPORTSECTIONLAYOUT_H From 6f2c63f3e59a53f22ffc4646dd8a1defd2d18707 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 29 Dec 2017 17:56:33 +1000 Subject: [PATCH 060/105] Add a common base class for print layouts and reports, and adapt QgsLayoutManager to suit --- python/core/composer/qgslayoutmanager.sip | 12 +-- python/core/core_auto.sip | 3 +- .../core/layout/qgsabstractreportsection.sip | 4 +- python/core/layout/qgslayout.sip | 23 +----- .../core/layout/qgsmasterlayoutinterface.sip | 76 ++++++++++++++++++ python/core/layout/qgsprintlayout.sip | 22 +++++- python/core/layout/qgsreport.sip | 25 ++++-- .../gui/layout/qgslayoutdesignerinterface.sip | 8 +- python/gui/qgisinterface.sip | 2 +- src/app/layout/qgslayoutdesignerdialog.cpp | 57 ++++++++++---- src/app/layout/qgslayoutdesignerdialog.h | 16 ++++ src/app/layout/qgslayoutmanagerdialog.cpp | 45 +++++++---- src/app/layout/qgslayoutmanagerdialog.h | 6 +- src/app/qgisapp.cpp | 16 ++-- src/app/qgisapp.h | 5 +- src/app/qgisappinterface.cpp | 2 +- src/app/qgisappinterface.h | 2 +- src/core/CMakeLists.txt | 3 +- src/core/composer/qgslayoutmanager.cpp | 71 ++++++++++++----- src/core/composer/qgslayoutmanager.h | 16 ++-- src/core/layout/qgsabstractreportsection.cpp | 14 +++- src/core/layout/qgsabstractreportsection.h | 8 +- src/core/layout/qgslayout.cpp | 8 -- src/core/layout/qgslayout.h | 24 +----- src/core/layout/qgslayoutexporter.cpp | 6 +- src/core/layout/qgsmasterlayoutinterface.h | 77 +++++++++++++++++++ src/core/layout/qgsprintlayout.cpp | 25 ++++++ src/core/layout/qgsprintlayout.h | 20 ++++- src/core/layout/qgsreport.cpp | 20 +++++ src/core/layout/qgsreport.h | 23 ++++-- .../layout/qgsreportsectionfieldgroup.cpp | 2 +- src/core/qgsexpressioncontext.cpp | 3 +- src/gui/layout/qgslayoutdesignerinterface.h | 8 +- src/gui/qgisinterface.h | 3 +- tests/src/core/testqgslayout.cpp | 4 +- tests/src/core/testqgslayoutobject.cpp | 3 +- tests/src/python/test_qgslayout.py | 5 +- tests/src/python/test_qgslayoutmanager.py | 42 +++++----- tests/src/python/test_qgsreport.py | 44 +++++------ 39 files changed, 543 insertions(+), 210 deletions(-) create mode 100644 python/core/layout/qgsmasterlayoutinterface.sip create mode 100644 src/core/layout/qgsmasterlayoutinterface.h diff --git a/python/core/composer/qgslayoutmanager.sip b/python/core/composer/qgslayoutmanager.sip index b8248a019181..8fa63d239ce0 100644 --- a/python/core/composer/qgslayoutmanager.sip +++ b/python/core/composer/qgslayoutmanager.sip @@ -50,7 +50,7 @@ as a result of a duplicate composition name). .. seealso:: :py:func:`compositionAdded()` %End - bool addLayout( QgsLayout *layout /Transfer/ ); + bool addLayout( QgsMasterLayoutInterface *layout /Transfer/ ); %Docstring Adds a ``layout`` to the manager. Ownership of the layout is transferred to the manager. Returns true if the addition was successful, or false if the layout could not be added (eg @@ -76,7 +76,7 @@ of removing a composition which is not contained in the manager). .. seealso:: :py:func:`clear()` %End - bool removeLayout( QgsLayout *layout ); + bool removeLayout( QgsMasterLayoutInterface *layout ); %Docstring Removes a ``layout`` from the manager. The layout is deleted. Returns true if the removal was successful, or false if the removal failed (eg as a result @@ -103,7 +103,7 @@ Removes and deletes all layouts from the manager. Returns a list of all compositions contained in the manager. %End - QList< QgsLayout * > layouts() const; + QList< QgsMasterLayoutInterface * > layouts() const; %Docstring Returns a list of all layouts contained in the manager. %End @@ -114,7 +114,7 @@ Returns the composition with a matching name, or None if no matching composition were found. %End - QgsLayout *layoutByName( const QString &name ) const; + QgsMasterLayoutInterface *layoutByName( const QString &name ) const; %Docstring Returns the layout with a matching name, or None if no matching layouts were found. @@ -148,7 +148,7 @@ composition will automatically be stored in the manager. Returns new composition if duplication was successful. %End - QgsLayout *duplicateLayout( const QgsLayout *layout, const QString &newName ); + QgsMasterLayoutInterface *duplicateLayout( const QgsMasterLayoutInterface *layout, const QString &newName ); %Docstring Duplicates an existing ``layout`` from the manager. The new layout will automatically be stored in the manager. @@ -214,7 +214,7 @@ Emitted when a layout is about to be removed from the manager Emitted when a composition is renamed %End - void layoutRenamed( QgsLayout *layout, const QString &newName ); + void layoutRenamed( QgsMasterLayoutInterface *layout, const QString &newName ); %Docstring Emitted when a layout is renamed %End diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index 8005fc0ec92d..cd99afdad444 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -163,6 +163,7 @@ %Include composer/qgspaperitem.sip %Include layout/qgsabstractlayoutiterator.sip %Include layout/qgsabstractreportsection.sip +%Include layout/qgsmasterlayoutinterface.sip %Include layout/qgslayoutaligner.sip %Include layout/qgslayoutexporter.sip %Include layout/qgslayoutgridsettings.sip @@ -175,7 +176,6 @@ %Include layout/qgslayoutsnapper.sip %Include layout/qgslayoutundocommand.sip %Include layout/qgslayoututils.sip -%Include layout/qgsreport.sip %Include layout/qgsreportsectionfieldgroup.sip %Include layout/qgsreportsectionlayout.sip %Include metadata/qgslayermetadata.sip @@ -444,6 +444,7 @@ %Include layout/qgslayouttablecolumn.sip %Include layout/qgslayoutundostack.sip %Include layout/qgsprintlayout.sip +%Include layout/qgsreport.sip %Include symbology/qgscptcityarchive.sip %Include symbology/qgssvgcache.sip %Include symbology/qgsstyle.sip diff --git a/python/core/layout/qgsabstractreportsection.sip b/python/core/layout/qgsabstractreportsection.sip index 34afcbbef2d4..cae2a8fd2f71 100644 --- a/python/core/layout/qgsabstractreportsection.sip +++ b/python/core/layout/qgsabstractreportsection.sip @@ -205,7 +205,7 @@ sections form the body of the report section. .. seealso:: :py:func:`children()` %End - QList< QgsAbstractReportSection * > children() const; + QList< QgsAbstractReportSection * > childSections() const; %Docstring Return all child sections for this report section. The child sections form the body of the report section. @@ -221,7 +221,7 @@ sections form the body of the report section. .. seealso:: :py:func:`removeChild()` %End - QgsAbstractReportSection *child( int index ); + QgsAbstractReportSection *childSection( int index ); %Docstring Returns the child section at the specified ``index``. diff --git a/python/core/layout/qgslayout.sip b/python/core/layout/qgslayout.sip index 9a8ea9d8a064..b532851f7798 100644 --- a/python/core/layout/qgslayout.sip +++ b/python/core/layout/qgslayout.sip @@ -56,7 +56,7 @@ called on the new layout. ~QgsLayout(); - virtual QgsLayout *clone() const /Factory/; + QgsLayout *clone() const /Factory/; %Docstring Creates a clone of the layout. Ownership of the return layout is transferred to the caller. @@ -86,20 +86,6 @@ relations and various other bits. It is never null. Returns the items model attached to the layout. %End - QString name() const; -%Docstring -Returns the layout's name. - -.. seealso:: :py:func:`setName()` -%End - - void setName( const QString &name ); -%Docstring -Sets the layout's name. - -.. seealso:: :py:func:`name()` -%End - QList selectedLayoutItems( const bool includeLockedItems = true ); %Docstring @@ -624,13 +610,6 @@ If None, no item is selected. %Docstring Is emitted when the layout has been refreshed and items should also be refreshed and updated. -%End - - void nameChanged( const QString &name ); -%Docstring -Emitted when the layout's name is changed. - -.. seealso:: :py:func:`setName()` %End }; diff --git a/python/core/layout/qgsmasterlayoutinterface.sip b/python/core/layout/qgsmasterlayoutinterface.sip new file mode 100644 index 000000000000..2ba24278aaf7 --- /dev/null +++ b/python/core/layout/qgsmasterlayoutinterface.sip @@ -0,0 +1,76 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layout/qgsmasterlayoutinterface.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + +class QgsMasterLayoutInterface +{ +%Docstring + Interface for master layout type objects, such as print layouts and reports. + +.. versionadded:: 3.0 +%End + +%TypeHeaderCode +#include "qgsmasterlayoutinterface.h" +%End + public: + + virtual ~QgsMasterLayoutInterface(); + + virtual QgsMasterLayoutInterface *clone() const = 0 /Factory/; +%Docstring +Creates a clone of the layout. Ownership of the returned layout +is transferred to the caller. +%End + + virtual QString name() const = 0; +%Docstring +Returns the layout's name. + +.. seealso:: :py:func:`setName()` +%End + + virtual void setName( const QString &name ) = 0; +%Docstring +Sets the layout's name. + +.. seealso:: :py:func:`name()` +%End + + virtual QgsProject *layoutProject() const = 0; +%Docstring +The project associated with the layout. Used to get access to layers, map themes, +relations and various other bits. It is never null. +%End + + virtual QDomElement writeLayoutXml( QDomDocument &document, const QgsReadWriteContext &context ) const = 0; +%Docstring +Returns the layout's state encapsulated in a DOM element. + +.. seealso:: :py:func:`readLayoutXml()` +%End + + virtual bool readLayoutXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ) = 0; +%Docstring +Sets the layout's state from a DOM element. ``layoutElement`` is the DOM node corresponding to the layout. + +.. seealso:: :py:func:`writeLayoutXml()` +%End + +}; + + + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layout/qgsmasterlayoutinterface.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/layout/qgsprintlayout.sip b/python/core/layout/qgsprintlayout.sip index 26faa334e321..482c36c60dfa 100644 --- a/python/core/layout/qgsprintlayout.sip +++ b/python/core/layout/qgsprintlayout.sip @@ -8,7 +8,7 @@ -class QgsPrintLayout : QgsLayout +class QgsPrintLayout : QgsLayout, QgsMasterLayoutInterface { %Docstring Print layout, a QgsLayout subclass for static or atlas-based layouts. @@ -28,19 +28,39 @@ Constructor for QgsPrintLayout. virtual QgsPrintLayout *clone() const /Factory/; + virtual QgsProject *layoutProject() const; + QgsLayoutAtlas *atlas(); %Docstring Returns the print layout's atlas. %End + virtual QString name() const; + virtual void setName( const QString &name ); + + virtual QDomElement writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const; virtual bool readXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ); + + virtual QDomElement writeLayoutXml( QDomDocument &document, const QgsReadWriteContext &context ) const; + + virtual bool readLayoutXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ); + virtual QgsExpressionContext createExpressionContext() const; + signals: + + void nameChanged( const QString &name ); +%Docstring +Emitted when the layout's name is changed. + +.. seealso:: :py:func:`setName()` +%End + }; /************************************************************************ diff --git a/python/core/layout/qgsreport.sip b/python/core/layout/qgsreport.sip index 624bbf5c3d39..de44b5144263 100644 --- a/python/core/layout/qgsreport.sip +++ b/python/core/layout/qgsreport.sip @@ -9,8 +9,7 @@ - -class QgsReport : QgsAbstractReportSection +class QgsReport : QObject, QgsAbstractReportSection, QgsMasterLayoutInterface { %Docstring Represents a report for use with the QgsLayout engine. @@ -39,13 +38,25 @@ Constructor for QgsReport, associated with the specified Note that ownership is not transferred to ``project``. %End - QgsProject *project(); -%Docstring -Returns the associated project. -%End - + virtual QgsProject *layoutProject() const; virtual QgsReport *clone() const /Factory/; + virtual QString name() const; + virtual void setName( const QString &name ); + + virtual QDomElement writeLayoutXml( QDomDocument &document, const QgsReadWriteContext &context ) const; + + virtual bool readLayoutXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ); + + + signals: + + void nameChanged( const QString &name ); +%Docstring +Emitted when the report's name is changed. + +.. seealso:: :py:func:`setName()` +%End }; diff --git a/python/gui/layout/qgslayoutdesignerinterface.sip b/python/gui/layout/qgslayoutdesignerinterface.sip index bd3dfdda4790..eb224f0d2109 100644 --- a/python/gui/layout/qgslayoutdesignerinterface.sip +++ b/python/gui/layout/qgslayoutdesignerinterface.sip @@ -33,11 +33,17 @@ Constructor for QgsLayoutDesignerInterface. virtual QgsLayout *layout() = 0; %Docstring -Returns the layout displayed in the designer. +Returns the current layout displayed in the designer. .. seealso:: :py:func:`view()` %End + virtual QgsMasterLayoutInterface *masterLayout() = 0; +%Docstring +Returns the master layout displayed in the designer. + +.. seealso:: :py:func:`layout()` +%End virtual QgsLayoutView *view() = 0; %Docstring diff --git a/python/gui/qgisinterface.sip b/python/gui/qgisinterface.sip index 0cf92cba9f74..eafed531769e 100644 --- a/python/gui/qgisinterface.sip +++ b/python/gui/qgisinterface.sip @@ -636,7 +636,7 @@ Opens the layout manager dialog. .. versionadded:: 3.0 %End - virtual QgsLayoutDesignerInterface *openLayoutDesigner( QgsLayout *layout ) = 0; + virtual QgsLayoutDesignerInterface *openLayoutDesigner( QgsMasterLayoutInterface *layout ) = 0; %Docstring Opens a new layout designer dialog for the specified ``layout``, or brings an already open designer window to the foreground if one diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 65b11e3b61ba..eeb2e157ecc9 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -59,6 +59,7 @@ #include "qgslayoutundostack.h" #include "qgslayoutatlaswidget.h" #include "qgslayoutpagecollection.h" +#include "qgsreport.h" #include "ui_qgssvgexportoptions.h" #include #include @@ -96,6 +97,11 @@ QgsLayout *QgsAppLayoutDesignerInterface::layout() return mDesigner->currentLayout(); } +QgsMasterLayoutInterface *QgsAppLayoutDesignerInterface::masterLayout() +{ + return mDesigner->masterLayout(); +} + QgsLayoutView *QgsAppLayoutDesignerInterface::view() { return mDesigner->view(); @@ -692,11 +698,36 @@ QgsLayout *QgsLayoutDesignerDialog::currentLayout() return mLayout; } +void QgsLayoutDesignerDialog::setMasterLayout( QgsMasterLayoutInterface *layout ) +{ + mMasterLayout = layout; + + QObject *obj = dynamic_cast< QObject * >( mMasterLayout ); + if ( obj ) + connect( obj, &QObject::destroyed, this, &QgsLayoutDesignerDialog::close ); + + setWindowTitle( mMasterLayout->name() ); + + if ( QgsPrintLayout *l = dynamic_cast< QgsPrintLayout * >( layout ) ) + { + connect( l, &QgsPrintLayout::nameChanged, this, &QgsLayoutDesignerDialog::setWindowTitle ); + setCurrentLayout( l ); + } + else if ( QgsReport *r = dynamic_cast< QgsReport * >( layout ) ) + { + connect( r, &QgsReport::nameChanged, this, &QgsLayoutDesignerDialog::setWindowTitle ); + } +} + +QgsMasterLayoutInterface *QgsLayoutDesignerDialog::masterLayout() +{ + return mMasterLayout; +} + void QgsLayoutDesignerDialog::setCurrentLayout( QgsLayout *layout ) { layout->deselectAll(); mLayout = layout; - connect( mLayout, &QgsLayout::destroyed, this, &QgsLayoutDesignerDialog::close ); mView->setCurrentLayout( layout ); @@ -719,8 +750,6 @@ void QgsLayoutDesignerDialog::setCurrentLayout( QgsLayout *layout ) { mLayout->guides().clear(); } ); - connect( mLayout, &QgsLayout::nameChanged, this, &QgsLayoutDesignerDialog::setWindowTitle ); - setWindowTitle( mLayout->name() ); mActionShowGrid->setChecked( mLayout->renderContext().gridVisible() ); mActionSnapGrid->setChecked( mLayout->snapper().snapToGrid() ); @@ -1417,7 +1446,7 @@ void QgsLayoutDesignerDialog::addItemsFromTemplate() void QgsLayoutDesignerDialog::duplicate() { QString newTitle; - if ( !QgisApp::instance()->uniqueLayoutTitle( this, newTitle, false, tr( "%1 copy" ).arg( currentLayout()->name() ) ) ) + if ( !QgisApp::instance()->uniqueLayoutTitle( this, newTitle, false, tr( "%1 copy" ).arg( masterLayout()->name() ) ) ) { return; } @@ -1427,7 +1456,7 @@ void QgsLayoutDesignerDialog::duplicate() dlg->setStyleSheet( QgisApp::instance()->styleSheet() ); dlg->show(); - QgsLayoutDesignerDialog *newDialog = QgisApp::instance()->duplicateLayout( currentLayout(), newTitle ); + QgsLayoutDesignerDialog *newDialog = QgisApp::instance()->duplicateLayout( mMasterLayout, newTitle ); dlg->close(); delete dlg; @@ -1468,22 +1497,22 @@ void QgsLayoutDesignerDialog::showManager() void QgsLayoutDesignerDialog::renameLayout() { - QString currentTitle = currentLayout()->name(); + QString currentTitle = masterLayout()->name(); QString newTitle; if ( !QgisApp::instance()->uniqueLayoutTitle( this, newTitle, false, currentTitle ) ) { return; } - currentLayout()->setName( newTitle ); + masterLayout()->setName( newTitle ); } void QgsLayoutDesignerDialog::deleteLayout() { - if ( QMessageBox::question( this, tr( "Delete Layout" ), tr( "Are you sure you want to delete the layout “%1”?" ).arg( currentLayout()->name() ), + if ( QMessageBox::question( this, tr( "Delete Layout" ), tr( "Are you sure you want to delete the layout “%1”?" ).arg( masterLayout()->name() ), QMessageBox::Yes | QMessageBox::No, QMessageBox::No ) != QMessageBox::Yes ) return; - currentLayout()->project()->layoutManager()->removeLayout( currentLayout() ); + masterLayout()->layoutProject()->layoutManager()->removeLayout( masterLayout() ); close(); } @@ -1496,7 +1525,7 @@ void QgsLayoutDesignerDialog::exportToRaster() return; QgsSettings s; - QString outputFileName = QgsFileUtils::stringToSafeFilename( mLayout->name() ); + QString outputFileName = QgsFileUtils::stringToSafeFilename( mMasterLayout->name() ); QgsLayoutAtlas *printAtlas = atlas(); if ( printAtlas && printAtlas->enabled() && mActionAtlasPreview->isChecked() ) { @@ -1596,7 +1625,7 @@ void QgsLayoutDesignerDialog::exportToPdf() } else { - outputFileName = file.path() + '/' + QgsFileUtils::stringToSafeFilename( mLayout->name() ) + QStringLiteral( ".pdf" ); + outputFileName = file.path() + '/' + QgsFileUtils::stringToSafeFilename( mMasterLayout->name() ) + QStringLiteral( ".pdf" ); } #ifdef Q_OS_MAC @@ -1689,7 +1718,7 @@ void QgsLayoutDesignerDialog::exportToSvg() QgsSettings settings; QString lastUsedFile = settings.value( QStringLiteral( "UI/lastSaveAsSvgFile" ), QStringLiteral( "qgis.svg" ) ).toString(); QFileInfo file( lastUsedFile ); - QString outputFileName = QgsFileUtils::stringToSafeFilename( mLayout->name() ); + QString outputFileName = QgsFileUtils::stringToSafeFilename( mMasterLayout->name() ); QgsLayoutAtlas *printAtlas = atlas(); if ( printAtlas && printAtlas->enabled() && mActionAtlasPreview->isChecked() ) @@ -1698,7 +1727,7 @@ void QgsLayoutDesignerDialog::exportToSvg() } else { - outputFileName = file.path() + '/' + QgsFileUtils::stringToSafeFilename( mLayout->name() ) + QStringLiteral( ".svg" ); + outputFileName = file.path() + '/' + QgsFileUtils::stringToSafeFilename( mMasterLayout->name() ) + QStringLiteral( ".svg" ); } #ifdef Q_OS_MAC @@ -2329,7 +2358,7 @@ void QgsLayoutDesignerDialog::exportAtlasToPdf() } else { - outputFileName = file.path() + '/' + QgsFileUtils::stringToSafeFilename( mLayout->name() ) + QStringLiteral( ".pdf" ); + outputFileName = file.path() + '/' + QgsFileUtils::stringToSafeFilename( mMasterLayout->name() ) + QStringLiteral( ".pdf" ); } #ifdef Q_OS_MAC diff --git a/src/app/layout/qgslayoutdesignerdialog.h b/src/app/layout/qgslayoutdesignerdialog.h index 05a039ccf1b8..2d0226167109 100644 --- a/src/app/layout/qgslayoutdesignerdialog.h +++ b/src/app/layout/qgslayoutdesignerdialog.h @@ -46,6 +46,7 @@ class QgsLayoutPropertiesWidget; class QgsMessageBar; class QgsLayoutAtlas; class QgsFeature; +class QgsMasterLayoutInterface; class QgsAppLayoutDesignerInterface : public QgsLayoutDesignerInterface { @@ -54,6 +55,7 @@ class QgsAppLayoutDesignerInterface : public QgsLayoutDesignerInterface public: QgsAppLayoutDesignerInterface( QgsLayoutDesignerDialog *dialog ); QgsLayout *layout() override; + QgsMasterLayoutInterface *masterLayout() override; QgsLayoutView *view() override; QgsMessageBar *messageBar() override; void selectItems( const QList< QgsLayoutItem * > items ) override; @@ -95,6 +97,18 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner */ QgsLayoutView *view(); + /** + * Sets the current master \a layout to edit in the designer. + * \see masterLayout() + */ + void setMasterLayout( QgsMasterLayoutInterface *layout ); + + /** + * Returns the current master layout associated with the designer. + * \see setMasterLayout() + */ + QgsMasterLayoutInterface *masterLayout(); + /** * Sets the current \a layout to edit in the designer. * \see currentLayout() @@ -306,6 +320,8 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner QgsAppLayoutDesignerInterface *mInterface = nullptr; + QgsMasterLayoutInterface *mMasterLayout = nullptr; + QgsLayout *mLayout = nullptr; QgsMessageBar *mMessageBar = nullptr; diff --git a/src/app/layout/qgslayoutmanagerdialog.cpp b/src/app/layout/qgslayoutmanagerdialog.cpp index 7bcea44471cd..4bda29d9a2b1 100644 --- a/src/app/layout/qgslayoutmanagerdialog.cpp +++ b/src/app/layout/qgslayoutmanagerdialog.cpp @@ -27,6 +27,7 @@ #include "qgsproject.h" #include "qgsgui.h" #include "qgsprintlayout.h" +#include "qgsreport.h" #include #include @@ -246,7 +247,7 @@ void QgsLayoutManagerDialog::mAddButton_clicked() title = QgsProject::instance()->layoutManager()->generateUniqueTitle(); } - std::unique_ptr< QgsLayout > layout = qgis::make_unique< QgsPrintLayout >( QgsProject::instance() ); + std::unique_ptr< QgsPrintLayout > layout = qgis::make_unique< QgsPrintLayout >( QgsProject::instance() ); if ( loadingTemplate ) { bool loadedOK = false; @@ -353,11 +354,11 @@ void QgsLayoutManagerDialog::removeClicked() return; } - QList layoutList; + QList layoutList; // Find the layouts that need to be deleted for ( const QModelIndex &index : layoutItems ) { - QgsLayout *l = mModel->layoutFromIndex( index ); + QgsMasterLayoutInterface *l = mModel->layoutFromIndex( index ); if ( l ) { layoutList << l; @@ -365,7 +366,7 @@ void QgsLayoutManagerDialog::removeClicked() } // Once we have the layout list, we can delete all of them ! - for ( QgsLayout *l : qgis::as_const( layoutList ) ) + for ( QgsMasterLayoutInterface *l : qgis::as_const( layoutList ) ) { QgsProject::instance()->layoutManager()->removeLayout( l ); } @@ -376,7 +377,7 @@ void QgsLayoutManagerDialog::showClicked() const QModelIndexList layoutItems = mLayoutListView->selectionModel()->selectedRows(); for ( const QModelIndex &index : layoutItems ) { - if ( QgsLayout *l = mModel->layoutFromIndex( index ) ) + if ( QgsMasterLayoutInterface *l = mModel->layoutFromIndex( index ) ) { QgisApp::instance()->openLayoutDesignerDialog( l ); } @@ -390,7 +391,7 @@ void QgsLayoutManagerDialog::duplicateClicked() return; } - QgsLayout *currentLayout = mModel->layoutFromIndex( mLayoutListView->selectionModel()->selectedRows().at( 0 ) ); + QgsMasterLayoutInterface *currentLayout = mModel->layoutFromIndex( mLayoutListView->selectionModel()->selectedRows().at( 0 ) ); if ( !currentLayout ) return; QString currentTitle = currentLayout->name(); @@ -430,7 +431,7 @@ void QgsLayoutManagerDialog::renameClicked() return; } - QgsLayout *currentLayout = mModel->layoutFromIndex( mLayoutListView->selectionModel()->selectedRows().at( 0 ) ); + QgsMasterLayoutInterface *currentLayout = mModel->layoutFromIndex( mLayoutListView->selectionModel()->selectedRows().at( 0 ) ); if ( !currentLayout ) return; @@ -445,7 +446,7 @@ void QgsLayoutManagerDialog::renameClicked() void QgsLayoutManagerDialog::itemDoubleClicked( const QModelIndex &index ) { - if ( QgsLayout *l = mModel->layoutFromIndex( index ) ) + if ( QgsMasterLayoutInterface *l = mModel->layoutFromIndex( index ) ) { QgisApp::instance()->openLayoutDesignerDialog( l ); } @@ -485,7 +486,14 @@ QVariant QgsLayoutManagerModel::data( const QModelIndex &index, int role ) const return mLayoutManager->layouts().at( index.row() )->name(); case LayoutRole: - return QVariant::fromValue( mLayoutManager->layouts().at( index.row() ) ); + { + if ( QgsLayout *l = dynamic_cast< QgsLayout * >( mLayoutManager->layouts().at( index.row() ) ) ) + return QVariant::fromValue( l ); + else if ( QgsReport *r = dynamic_cast< QgsReport * >( mLayoutManager->layouts().at( index.row() ) ) ) + return QVariant::fromValue( r ); + else + return QVariant(); + } default: return QVariant(); @@ -506,7 +514,7 @@ bool QgsLayoutManagerModel::setData( const QModelIndex &index, const QVariant &v if ( value.toString().isEmpty() ) return false; - QgsLayout *layout = layoutFromIndex( index ); + QgsMasterLayoutInterface *layout = layoutFromIndex( index ); if ( !layout ) return false; @@ -517,8 +525,8 @@ bool QgsLayoutManagerModel::setData( const QModelIndex &index, const QVariant &v //check if name already exists QStringList layoutNames; - const QList< QgsLayout * > layouts = QgsProject::instance()->layoutManager()->layouts(); - for ( QgsLayout *l : layouts ) + const QList< QgsMasterLayoutInterface * > layouts = QgsProject::instance()->layoutManager()->layouts(); + for ( QgsMasterLayoutInterface *l : layouts ) { layoutNames << l->name(); } @@ -549,9 +557,14 @@ Qt::ItemFlags QgsLayoutManagerModel::flags( const QModelIndex &index ) const return flags; } -QgsLayout *QgsLayoutManagerModel::layoutFromIndex( const QModelIndex &index ) const +QgsMasterLayoutInterface *QgsLayoutManagerModel::layoutFromIndex( const QModelIndex &index ) const { - return qobject_cast< QgsLayout * >( qvariant_cast( data( index, LayoutRole ) ) ); + if ( QgsPrintLayout *l = qobject_cast< QgsPrintLayout * >( qvariant_cast( data( index, LayoutRole ) ) ) ) + return l; + else if ( QgsReport *r = qobject_cast< QgsReport * >( qvariant_cast( data( index, LayoutRole ) ) ) ) + return r; + else + return nullptr; } void QgsLayoutManagerModel::layoutAboutToBeAdded( const QString & ) @@ -562,7 +575,7 @@ void QgsLayoutManagerModel::layoutAboutToBeAdded( const QString & ) void QgsLayoutManagerModel::layoutAboutToBeRemoved( const QString &name ) { - QgsLayout *l = mLayoutManager->layoutByName( name ); + QgsMasterLayoutInterface *l = mLayoutManager->layoutByName( name ); int row = mLayoutManager->layouts().indexOf( l ); if ( row >= 0 ) beginRemoveRows( QModelIndex(), row, row ); @@ -578,7 +591,7 @@ void QgsLayoutManagerModel::layoutRemoved( const QString & ) endRemoveRows(); } -void QgsLayoutManagerModel::layoutRenamed( QgsLayout *layout, const QString & ) +void QgsLayoutManagerModel::layoutRenamed( QgsMasterLayoutInterface *layout, const QString & ) { int row = mLayoutManager->layouts().indexOf( layout ); QModelIndex index = createIndex( row, 0 ); diff --git a/src/app/layout/qgslayoutmanagerdialog.h b/src/app/layout/qgslayoutmanagerdialog.h index 30bf80a2a4df..2494d9a24150 100644 --- a/src/app/layout/qgslayoutmanagerdialog.h +++ b/src/app/layout/qgslayoutmanagerdialog.h @@ -23,7 +23,7 @@ class QListWidgetItem; class QgsLayoutDesignerDialog; -class QgsLayout; +class QgsMasterLayoutInterface; class QgsLayoutManager; class QgsLayoutManagerModel : public QAbstractListModel @@ -43,14 +43,14 @@ class QgsLayoutManagerModel : public QAbstractListModel QVariant data( const QModelIndex &index, int role ) const override; bool setData( const QModelIndex &index, const QVariant &value, int role = Qt::EditRole ) override; Qt::ItemFlags flags( const QModelIndex &index ) const override; - QgsLayout *layoutFromIndex( const QModelIndex &index ) const; + QgsMasterLayoutInterface *layoutFromIndex( const QModelIndex &index ) const; private slots: void layoutAboutToBeAdded( const QString &name ); void layoutAboutToBeRemoved( const QString &name ); void layoutAdded( const QString &name ); void layoutRemoved( const QString &name ); - void layoutRenamed( QgsLayout *layout, const QString &newName ); + void layoutRenamed( QgsMasterLayoutInterface *layout, const QString &newName ); private: QgsLayoutManager *mLayoutManager = nullptr; }; diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 26fb38511950..bd4291314df9 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -7352,8 +7352,8 @@ bool QgisApp::uniqueLayoutTitle( QWidget *parent, QString &title, bool acceptEmp QStringList layoutNames; layoutNames << newTitle; - const QList< QgsLayout * > layouts = QgsProject::instance()->layoutManager()->layouts(); - for ( QgsLayout *l : layouts ) + const QList< QgsMasterLayoutInterface * > layouts = QgsProject::instance()->layoutManager()->layouts(); + for ( QgsMasterLayoutInterface *l : layouts ) { layoutNames << l->name(); } @@ -7451,19 +7451,19 @@ QgsLayoutDesignerDialog *QgisApp::createNewLayout( QString title ) title = QgsProject::instance()->layoutManager()->generateUniqueTitle(); } //create new layout object - QgsLayout *layout = new QgsPrintLayout( QgsProject::instance() ); + QgsPrintLayout *layout = new QgsPrintLayout( QgsProject::instance() ); layout->setName( title ); layout->initializeDefaults(); QgsProject::instance()->layoutManager()->addLayout( layout ); return openLayoutDesignerDialog( layout ); } -QgsLayoutDesignerDialog *QgisApp::openLayoutDesignerDialog( QgsLayout *layout ) +QgsLayoutDesignerDialog *QgisApp::openLayoutDesignerDialog( QgsMasterLayoutInterface *layout ) { // maybe a designer already open for this layout Q_FOREACH ( QgsLayoutDesignerDialog *designer, mLayoutDesignerDialogs ) { - if ( designer->currentLayout() == layout ) + if ( designer->masterLayout() == layout ) { designer->show(); designer->activate(); @@ -7474,7 +7474,7 @@ QgsLayoutDesignerDialog *QgisApp::openLayoutDesignerDialog( QgsLayout *layout ) //nope, so make a new one QgsLayoutDesignerDialog *newDesigner = new QgsLayoutDesignerDialog( this ); - newDesigner->setCurrentLayout( layout ); + newDesigner->setMasterLayout( layout ); connect( newDesigner, &QgsLayoutDesignerDialog::aboutToClose, this, [this, newDesigner] { emit layoutDesignerWillBeClosed( newDesigner->iface() ); @@ -7522,7 +7522,7 @@ QgsComposer *QgisApp::duplicateComposer( QgsComposer *currentComposer, QString t return newComposer; } -QgsLayoutDesignerDialog *QgisApp::duplicateLayout( QgsLayout *layout, const QString &t ) +QgsLayoutDesignerDialog *QgisApp::duplicateLayout( QgsMasterLayoutInterface *layout, const QString &t ) { QString title = t; if ( title.isEmpty() ) @@ -7531,7 +7531,7 @@ QgsLayoutDesignerDialog *QgisApp::duplicateLayout( QgsLayout *layout, const QStr title = tr( "%1 copy" ).arg( layout->name() ); } - QgsLayout *newLayout = QgsProject::instance()->layoutManager()->duplicateLayout( layout, title ); + QgsMasterLayoutInterface *newLayout = QgsProject::instance()->layoutManager()->duplicateLayout( layout, title ); QgsLayoutDesignerDialog *dlg = openLayoutDesignerDialog( newLayout ); dlg->activate(); return dlg; diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h index 9fcb35f6f4db..1cb5e92cd7e0 100644 --- a/src/app/qgisapp.h +++ b/src/app/qgisapp.h @@ -62,6 +62,7 @@ class QgsGeometry; class QgsLayerTreeMapCanvasBridge; class QgsLayerTreeView; class QgsLayout; +class QgsMasterLayoutInterface; class QgsLayoutCustomDropHandler; class QgsLayoutDesignerDialog; class QgsLayoutDesignerInterface; @@ -392,7 +393,7 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow * Opens a layout designer dialog for an existing \a layout. * If a designer already exists for this layout then it will be activated. */ - QgsLayoutDesignerDialog *openLayoutDesignerDialog( QgsLayout *layout ); + QgsLayoutDesignerDialog *openLayoutDesignerDialog( QgsMasterLayoutInterface *layout ); //! Deletes a composer and removes entry from Set void deleteComposer( QgsComposer *c ); @@ -408,7 +409,7 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow * If \a title is set, it will be used as the title for the new layout. If it is not set, * and auto-generated title will be used instead. */ - QgsLayoutDesignerDialog *duplicateLayout( QgsLayout *layout, const QString &title = QString() ); + QgsLayoutDesignerDialog *duplicateLayout( QgsMasterLayoutInterface *layout, const QString &title = QString() ); //! Overloaded function used to sort menu entries alphabetically QMenu *createPopupMenu() override; diff --git a/src/app/qgisappinterface.cpp b/src/app/qgisappinterface.cpp index 4b5a61784660..deef4c17f994 100644 --- a/src/app/qgisappinterface.cpp +++ b/src/app/qgisappinterface.cpp @@ -474,7 +474,7 @@ QList QgisAppInterface::openLayoutDesigners() return designerInterfaceList; } -QgsLayoutDesignerInterface *QgisAppInterface::openLayoutDesigner( QgsLayout *layout ) +QgsLayoutDesignerInterface *QgisAppInterface::openLayoutDesigner( QgsMasterLayoutInterface *layout ) { QgsLayoutDesignerDialog *designer = qgis->openLayoutDesignerDialog( layout ); if ( designer ) diff --git a/src/app/qgisappinterface.h b/src/app/qgisappinterface.h index 72613b0a2e23..c776f0977675 100644 --- a/src/app/qgisappinterface.h +++ b/src/app/qgisappinterface.h @@ -238,7 +238,7 @@ class APP_EXPORT QgisAppInterface : public QgisInterface void showLayoutManager() override; QList openLayoutDesigners() override; - QgsLayoutDesignerInterface *openLayoutDesigner( QgsLayout *layout ) override; + QgsLayoutDesignerInterface *openLayoutDesigner( QgsMasterLayoutInterface *layout ) override; void showOptionsDialog( QWidget *parent = nullptr, const QString ¤tPage = QString() ) override; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index ab67925a8817..33906216494b 100755 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -778,6 +778,7 @@ SET(QGIS_CORE_MOC_HDRS layout/qgslayouttablecolumn.h layout/qgslayoutundostack.h layout/qgsprintlayout.h + layout/qgsreport.h symbology/qgscptcityarchive.h symbology/qgssvgcache.h @@ -1034,6 +1035,7 @@ SET(QGIS_CORE_HDRS layout/qgsabstractlayoutiterator.h layout/qgsabstractreportsection.h + layout/qgsmasterlayoutinterface.h layout/qgslayoutaligner.h layout/qgslayoutexporter.h layout/qgslayoutgridsettings.h @@ -1048,7 +1050,6 @@ SET(QGIS_CORE_HDRS layout/qgslayoutsnapper.h layout/qgslayoutundocommand.h layout/qgslayoututils.h - layout/qgsreport.h layout/qgsreportsectionfieldgroup.h layout/qgsreportsectionlayout.h diff --git a/src/core/composer/qgslayoutmanager.cpp b/src/core/composer/qgslayoutmanager.cpp index 77550bd051c8..faeb7157dd01 100644 --- a/src/core/composer/qgslayoutmanager.cpp +++ b/src/core/composer/qgslayoutmanager.cpp @@ -19,6 +19,7 @@ #include "qgslogger.h" #include "qgslayoutundostack.h" #include "qgsprintlayout.h" +#include "qgsreport.h" QgsLayoutManager::QgsLayoutManager( QgsProject *project ) : QObject( project ) @@ -55,22 +56,34 @@ bool QgsLayoutManager::addComposition( QgsComposition *composition ) return true; } -bool QgsLayoutManager::addLayout( QgsLayout *layout ) +bool QgsLayoutManager::addLayout( QgsMasterLayoutInterface *layout ) { if ( !layout ) return false; // check for duplicate name - for ( QgsLayout *l : qgis::as_const( mLayouts ) ) + for ( QgsMasterLayoutInterface *l : qgis::as_const( mLayouts ) ) { if ( l->name() == layout->name() ) return false; } - connect( layout, &QgsLayout::nameChanged, this, [this, layout]( const QString & newName ) + // ugly, but unavoidable for interfaces... + if ( QgsPrintLayout *l = dynamic_cast< QgsPrintLayout * >( layout ) ) { - emit layoutRenamed( layout, newName ); - } ); + connect( l, &QgsPrintLayout::nameChanged, this, [this, l]( const QString & newName ) + { + emit layoutRenamed( l, newName ); + } ); + } + else if ( QgsReport *r = dynamic_cast< QgsReport * >( layout ) ) + { + connect( r, &QgsReport::nameChanged, this, [this, r]( const QString & newName ) + { + emit layoutRenamed( r, newName ); + } ); + } + emit layoutAboutToBeAdded( layout->name() ); mLayouts << layout; emit layoutAdded( layout->name() ); @@ -95,7 +108,7 @@ bool QgsLayoutManager::removeComposition( QgsComposition *composition ) return true; } -bool QgsLayoutManager::removeLayout( QgsLayout *layout ) +bool QgsLayoutManager::removeLayout( QgsMasterLayoutInterface *layout ) { if ( !layout ) return false; @@ -118,8 +131,8 @@ void QgsLayoutManager::clear() { removeComposition( c ); } - const QList< QgsLayout * > layouts = mLayouts; - for ( QgsLayout *l : layouts ) + const QList< QgsMasterLayoutInterface * > layouts = mLayouts; + for ( QgsMasterLayoutInterface *l : layouts ) { removeLayout( l ); } @@ -130,7 +143,7 @@ QList QgsLayoutManager::compositions() const return mCompositions; } -QList QgsLayoutManager::layouts() const +QList QgsLayoutManager::layouts() const { return mLayouts; } @@ -145,9 +158,9 @@ QgsComposition *QgsLayoutManager::compositionByName( const QString &name ) const return nullptr; } -QgsLayout *QgsLayoutManager::layoutByName( const QString &name ) const +QgsMasterLayoutInterface *QgsLayoutManager::layoutByName( const QString &name ) const { - for ( QgsLayout *l : mLayouts ) + for ( QgsMasterLayoutInterface *l : mLayouts ) { if ( l->name() == name ) return l; @@ -194,9 +207,9 @@ bool QgsLayoutManager::readXml( const QDomElement &element, const QDomDocument & const QDomNodeList layoutNodes = element.elementsByTagName( QStringLiteral( "Layout" ) ); for ( int i = 0; i < layoutNodes.size(); ++i ) { - std::unique_ptr< QgsLayout > l = qgis::make_unique< QgsPrintLayout >( mProject ); + std::unique_ptr< QgsPrintLayout > l = qgis::make_unique< QgsPrintLayout >( mProject ); l->undoStack()->blockCommands( true ); - if ( !l->readXml( layoutNodes.at( i ).toElement(), doc, context ) ) + if ( !l->readLayoutXml( layoutNodes.at( i ).toElement(), doc, context ) ) { result = false; continue; @@ -211,6 +224,25 @@ bool QgsLayoutManager::readXml( const QDomElement &element, const QDomDocument & result = false; } } + //reports + const QDomNodeList reportNodes = element.elementsByTagName( QStringLiteral( "Report" ) ); + for ( int i = 0; i < reportNodes.size(); ++i ) + { + std::unique_ptr< QgsReport > r = qgis::make_unique< QgsReport >( mProject ); + if ( !r->readLayoutXml( reportNodes.at( i ).toElement(), doc, context ) ) + { + result = false; + continue; + } + if ( addLayout( r.get() ) ) + { + ( void )r.release(); // ownership was transferred successfully + } + else + { + result = false; + } + } return result; } @@ -226,11 +258,12 @@ QDomElement QgsLayoutManager::writeXml( QDomDocument &doc ) const c->writeXml( composerElem, doc ); c->atlasComposition().writeXml( composerElem, doc ); } + QgsReadWriteContext context; context.setPathResolver( mProject->pathResolver() ); - for ( QgsLayout *l : mLayouts ) + for ( QgsMasterLayoutInterface *l : mLayouts ) { - QDomElement layoutElem = l->writeXml( doc, context ); + QDomElement layoutElem = l->writeLayoutXml( doc, context ); layoutsElem.appendChild( layoutElem ); } return layoutsElem; @@ -281,16 +314,16 @@ QgsComposition *QgsLayoutManager::duplicateComposition( const QString &name, con } } -QgsLayout *QgsLayoutManager::duplicateLayout( const QgsLayout *layout, const QString &newName ) +QgsMasterLayoutInterface *QgsLayoutManager::duplicateLayout( const QgsMasterLayoutInterface *layout, const QString &newName ) { - std::unique_ptr< QgsLayout > newLayout( layout->clone() ); + std::unique_ptr< QgsMasterLayoutInterface > newLayout( layout->clone() ); if ( !newLayout ) { return nullptr; } newLayout->setName( newName ); - QgsLayout *l = newLayout.get(); + QgsMasterLayoutInterface *l = newLayout.get(); if ( !addLayout( l ) ) { return nullptr; @@ -322,7 +355,7 @@ QString QgsLayoutManager::generateUniqueComposerTitle() const QString QgsLayoutManager::generateUniqueTitle() const { QStringList names; - for ( QgsLayout *l : mLayouts ) + for ( QgsMasterLayoutInterface *l : mLayouts ) { names << l->name(); } diff --git a/src/core/composer/qgslayoutmanager.h b/src/core/composer/qgslayoutmanager.h index 0b977f58d5ca..d907078321f7 100644 --- a/src/core/composer/qgslayoutmanager.h +++ b/src/core/composer/qgslayoutmanager.h @@ -19,7 +19,7 @@ #include "qgis_core.h" #include "qgis.h" #include "qgscomposition.h" -#include "qgslayout.h" +#include "qgsmasterlayoutinterface.h" #include class QgsProject; @@ -69,7 +69,7 @@ class CORE_EXPORT QgsLayoutManager : public QObject * \see removeLayout() * \see layoutAdded() */ - bool addLayout( QgsLayout *layout SIP_TRANSFER ); + bool addLayout( QgsMasterLayoutInterface *layout SIP_TRANSFER ); /** * Removes a composition from the manager. The composition is deleted. @@ -91,7 +91,7 @@ class CORE_EXPORT QgsLayoutManager : public QObject * \see layoutAboutToBeRemoved() * \see clear() */ - bool removeLayout( QgsLayout *layout ); + bool removeLayout( QgsMasterLayoutInterface *layout ); /** * Removes and deletes all layouts from the manager. @@ -107,7 +107,7 @@ class CORE_EXPORT QgsLayoutManager : public QObject /** * Returns a list of all layouts contained in the manager. */ - QList< QgsLayout * > layouts() const; + QList< QgsMasterLayoutInterface * > layouts() const; /** * Returns the composition with a matching name, or nullptr if no matching compositions @@ -119,7 +119,7 @@ class CORE_EXPORT QgsLayoutManager : public QObject * Returns the layout with a matching name, or nullptr if no matching layouts * were found. */ - QgsLayout *layoutByName( const QString &name ) const; + QgsMasterLayoutInterface *layoutByName( const QString &name ) const; /** * Reads the manager's state from a DOM element, restoring all layouts @@ -152,7 +152,7 @@ class CORE_EXPORT QgsLayoutManager : public QObject * layout will automatically be stored in the manager. * Returns new the layout if duplication was successful. */ - QgsLayout *duplicateLayout( const QgsLayout *layout, const QString &newName ); + QgsMasterLayoutInterface *duplicateLayout( const QgsMasterLayoutInterface *layout, const QString &newName ); /** * Generates a unique title for a new composition, which does not @@ -196,14 +196,14 @@ class CORE_EXPORT QgsLayoutManager : public QObject void compositionRenamed( QgsComposition *composition, const QString &newName ); //! Emitted when a layout is renamed - void layoutRenamed( QgsLayout *layout, const QString &newName ); + void layoutRenamed( QgsMasterLayoutInterface *layout, const QString &newName ); private: QgsProject *mProject = nullptr; QList< QgsComposition * > mCompositions; - QList< QgsLayout * > mLayouts; + QList< QgsMasterLayoutInterface * > mLayouts; QgsComposition *createCompositionFromXml( const QDomElement &element, const QDomDocument &doc ) const; diff --git a/src/core/layout/qgsabstractreportsection.cpp b/src/core/layout/qgsabstractreportsection.cpp index 04b680fe1e58..b68c61cf2750 100644 --- a/src/core/layout/qgsabstractreportsection.cpp +++ b/src/core/layout/qgsabstractreportsection.cpp @@ -38,7 +38,7 @@ QgsProject *QgsAbstractReportSection::project() return nullptr; if ( QgsReport *report = dynamic_cast< QgsReport * >( parent ) ) - return report->project(); + return report->layoutProject(); current = parent; } @@ -218,7 +218,17 @@ void QgsAbstractReportSection::reset() } } -QgsAbstractReportSection *QgsAbstractReportSection::child( int index ) +void QgsAbstractReportSection::setHeader( QgsLayout *header ) +{ + mHeader.reset( header ); +} + +void QgsAbstractReportSection::setFooter( QgsLayout *footer ) +{ + mFooter.reset( footer ); +} + +QgsAbstractReportSection *QgsAbstractReportSection::childSection( int index ) { return mChildren.value( index ); } diff --git a/src/core/layout/qgsabstractreportsection.h b/src/core/layout/qgsabstractreportsection.h index 8907bba566cf..cb732191340b 100644 --- a/src/core/layout/qgsabstractreportsection.h +++ b/src/core/layout/qgsabstractreportsection.h @@ -143,7 +143,7 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator * \see headerEnabled() * \see header() */ - void setHeader( QgsLayout *header SIP_TRANSFER ) { mHeader.reset( header ); } + void setHeader( QgsLayout *header SIP_TRANSFER ); /** * Returns true if the footer for the section is enabled. @@ -178,7 +178,7 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator * \see footerEnabled() * \see footer() */ - void setFooter( QgsLayout *footer SIP_TRANSFER ) { mFooter.reset( footer ); } + void setFooter( QgsLayout *footer SIP_TRANSFER ); /** * Return the number of child sections for this report section. The child @@ -196,13 +196,13 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator * \see insertChild() * \see removeChild() */ - QList< QgsAbstractReportSection * > children() const { return mChildren; } + QList< QgsAbstractReportSection * > childSections() const { return mChildren; } /** * Returns the child section at the specified \a index. * \see children() */ - QgsAbstractReportSection *child( int index ); + QgsAbstractReportSection *childSection( int index ); /** * Adds a child \a section, transferring ownership of the section to this section. diff --git a/src/core/layout/qgslayout.cpp b/src/core/layout/qgslayout.cpp index dd8608917e77..eb85b5558936 100644 --- a/src/core/layout/qgslayout.cpp +++ b/src/core/layout/qgslayout.cpp @@ -134,12 +134,6 @@ QgsLayoutModel *QgsLayout::itemsModel() return mItemsModel.get(); } -void QgsLayout::setName( const QString &name ) -{ - mName = name; - emit nameChanged( name ); -} - QList QgsLayout::selectedLayoutItems( const bool includeLockedItems ) { QList layoutItemList; @@ -761,7 +755,6 @@ void QgsLayout::refresh() void QgsLayout::writeXmlLayoutSettings( QDomElement &element, QDomDocument &document, const QgsReadWriteContext & ) const { mCustomProperties.writeXml( element, document ); - element.setAttribute( QStringLiteral( "name" ), mName ); element.setAttribute( QStringLiteral( "units" ), QgsUnitTypes::encodeUnit( mUnits ) ); element.setAttribute( QStringLiteral( "worldFileMap" ), mWorldFileMapId ); element.setAttribute( QStringLiteral( "printResolution" ), mRenderContext->dpi() ); @@ -805,7 +798,6 @@ QDomElement QgsLayout::writeXml( QDomDocument &document, const QgsReadWriteConte bool QgsLayout::readXmlLayoutSettings( const QDomElement &layoutElement, const QDomDocument &, const QgsReadWriteContext & ) { mCustomProperties.readXml( layoutElement ); - setName( layoutElement.attribute( QStringLiteral( "name" ) ) ); setUnits( QgsUnitTypes::decodeLayoutUnit( layoutElement.attribute( QStringLiteral( "units" ) ) ) ); mWorldFileMapId = layoutElement.attribute( QStringLiteral( "worldFileMap" ) ); mRenderContext->setDpi( layoutElement.attribute( QStringLiteral( "printResolution" ), "300" ).toDouble() ); diff --git a/src/core/layout/qgslayout.h b/src/core/layout/qgslayout.h index 8e196d5c0d12..c237d91c5e92 100644 --- a/src/core/layout/qgslayout.h +++ b/src/core/layout/qgslayout.h @@ -23,6 +23,7 @@ #include "qgslayoutgridsettings.h" #include "qgslayoutguidecollection.h" #include "qgslayoutexporter.h" +#include "qgsmasterlayoutinterface.h" class QgsLayoutItemMap; class QgsLayoutModel; @@ -48,7 +49,6 @@ class QgsLayoutReportContext; class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContextGenerator, public QgsLayoutUndoObjectInterface { Q_OBJECT - Q_PROPERTY( QString name READ name WRITE setName NOTIFY nameChanged ) public: @@ -87,7 +87,7 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext * Creates a clone of the layout. Ownership of the return layout * is transferred to the caller. */ - virtual QgsLayout *clone() const SIP_FACTORY; + QgsLayout *clone() const SIP_FACTORY; /** * Initializes an empty layout, e.g. by adding a default page to the layout. This should be called after creating @@ -114,18 +114,6 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext */ QgsLayoutModel *itemsModel(); - /** - * Returns the layout's name. - * \see setName() - */ - QString name() const { return mName; } - - /** - * Sets the layout's name. - * \see name() - */ - void setName( const QString &name ); - /** * Returns a list of layout items of a specific type. * \note not available in Python bindings @@ -641,19 +629,11 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext */ void refreshed(); - /** - * Emitted when the layout's name is changed. - * \see setName() - */ - void nameChanged( const QString &name ); - private: QgsProject *mProject = nullptr; std::unique_ptr< QgsLayoutModel > mItemsModel; - QString mName; - QgsObjectCustomProperties mCustomProperties; QgsUnitTypes::LayoutUnit mUnits = QgsUnitTypes::LayoutMillimeters; diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index 1e5dae21e892..eeeece0350a3 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -946,7 +946,11 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::renderToLayeredSvg( const Svg QBuffer svgBuffer; { QSvgGenerator generator; - generator.setTitle( mLayout->name() ); + if ( const QgsMasterLayoutInterface *l = dynamic_cast< const QgsMasterLayoutInterface * >( mLayout.data() ) ) + generator.setTitle( l->name() ); + else if ( mLayout->project() ) + generator.setTitle( mLayout->project()->title() ); + generator.setOutputDevice( &svgBuffer ); generator.setSize( QSize( width, height ) ); generator.setViewBox( QRect( 0, 0, width, height ) ); diff --git a/src/core/layout/qgsmasterlayoutinterface.h b/src/core/layout/qgsmasterlayoutinterface.h new file mode 100644 index 000000000000..f26bdcd7589b --- /dev/null +++ b/src/core/layout/qgsmasterlayoutinterface.h @@ -0,0 +1,77 @@ +/*************************************************************************** + qgslayoutinterface.h + -------------------- + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#ifndef QGSLAYOUTINTERFACE_H +#define QGSLAYOUTINTERFACE_H + +#include "qgis_core.h" +#include "qgis_sip.h" +#include + +/** + * \ingroup core + * \class QgsMasterLayoutInterface + * \brief Interface for master layout type objects, such as print layouts and reports. + * \since QGIS 3.0 + */ +class CORE_EXPORT QgsMasterLayoutInterface +{ + + public: + + virtual ~QgsMasterLayoutInterface() = default; + + /** + * Creates a clone of the layout. Ownership of the returned layout + * is transferred to the caller. + */ + virtual QgsMasterLayoutInterface *clone() const = 0 SIP_FACTORY; + + /** + * Returns the layout's name. + * \see setName() + */ + virtual QString name() const = 0; + + /** + * Sets the layout's name. + * \see name() + */ + virtual void setName( const QString &name ) = 0; + + /** + * The project associated with the layout. Used to get access to layers, map themes, + * relations and various other bits. It is never null. + */ + virtual QgsProject *layoutProject() const = 0; + + /** + * Returns the layout's state encapsulated in a DOM element. + * \see readLayoutXml() + */ + virtual QDomElement writeLayoutXml( QDomDocument &document, const QgsReadWriteContext &context ) const = 0; + + /** + * Sets the layout's state from a DOM element. \a layoutElement is the DOM node corresponding to the layout. + * \see writeLayoutXml() + */ + virtual bool readLayoutXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ) = 0; + +}; + +#endif //QGSLAYOUTINTERFACE_H + + + diff --git a/src/core/layout/qgsprintlayout.cpp b/src/core/layout/qgsprintlayout.cpp index 9db82592b352..a82ca93a2546 100644 --- a/src/core/layout/qgsprintlayout.cpp +++ b/src/core/layout/qgsprintlayout.cpp @@ -43,14 +43,26 @@ QgsPrintLayout *QgsPrintLayout::clone() const return newLayout.release(); } +QgsProject *QgsPrintLayout::layoutProject() const +{ + return project(); +} + QgsLayoutAtlas *QgsPrintLayout::atlas() { return mAtlas; } +void QgsPrintLayout::setName( const QString &name ) +{ + mName = name; + emit nameChanged( name ); +} + QDomElement QgsPrintLayout::writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const { QDomElement layoutElem = QgsLayout::writeXml( document, context ); + layoutElem.setAttribute( QStringLiteral( "name" ), mName ); mAtlas->writeXml( layoutElem, document, context ); return layoutElem; } @@ -62,9 +74,22 @@ bool QgsPrintLayout::readXml( const QDomElement &layoutElement, const QDomDocume QDomElement atlasElem = layoutElement.firstChildElement( QStringLiteral( "Atlas" ) ); mAtlas->readXml( atlasElem, document, context ); + + setName( layoutElement.attribute( QStringLiteral( "name" ) ) ); + return true; } +QDomElement QgsPrintLayout::writeLayoutXml( QDomDocument &document, const QgsReadWriteContext &context ) const +{ + return writeXml( document, context ); +} + +bool QgsPrintLayout::readLayoutXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ) +{ + return readXml( layoutElement, document, context ); +} + QgsExpressionContext QgsPrintLayout::createExpressionContext() const { QgsExpressionContext context = QgsLayout::createExpressionContext(); diff --git a/src/core/layout/qgsprintlayout.h b/src/core/layout/qgsprintlayout.h index b0471fad98ec..08b5dd7a415e 100644 --- a/src/core/layout/qgsprintlayout.h +++ b/src/core/layout/qgsprintlayout.h @@ -27,9 +27,10 @@ class QgsLayoutAtlas; * \brief Print layout, a QgsLayout subclass for static or atlas-based layouts. * \since QGIS 3.0 */ -class CORE_EXPORT QgsPrintLayout : public QgsLayout +class CORE_EXPORT QgsPrintLayout : public QgsLayout, public QgsMasterLayoutInterface { Q_OBJECT + Q_PROPERTY( QString name READ name WRITE setName NOTIFY nameChanged ) public: @@ -39,18 +40,35 @@ class CORE_EXPORT QgsPrintLayout : public QgsLayout QgsPrintLayout( QgsProject *project ); QgsPrintLayout *clone() const override SIP_FACTORY; + QgsProject *layoutProject() const override; /** * Returns the print layout's atlas. */ QgsLayoutAtlas *atlas(); + QString name() const override { return mName; } + void setName( const QString &name ) override; + QDomElement writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const override; bool readXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ) override; + + // QgsLayoutInterface + QDomElement writeLayoutXml( QDomDocument &document, const QgsReadWriteContext &context ) const override; + bool readLayoutXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ) override; QgsExpressionContext createExpressionContext() const override; + signals: + + /** + * Emitted when the layout's name is changed. + * \see setName() + */ + void nameChanged( const QString &name ); + private: + QString mName; QgsLayoutAtlas *mAtlas = nullptr; }; diff --git a/src/core/layout/qgsreport.cpp b/src/core/layout/qgsreport.cpp index c1859381893a..9051791e0b00 100644 --- a/src/core/layout/qgsreport.cpp +++ b/src/core/layout/qgsreport.cpp @@ -31,5 +31,25 @@ QgsReport *QgsReport::clone() const return copy.release(); } +void QgsReport::setName( const QString &name ) +{ + mName = name; + emit nameChanged( mName ); +} + +QDomElement QgsReport::writeLayoutXml( QDomDocument &document, const QgsReadWriteContext &context ) const +{ + QDomElement element = document.createElement( QStringLiteral( "Report" ) ); + + + + return element; +} + +bool QgsReport::readLayoutXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ) +{ + +} + ///@endcond diff --git a/src/core/layout/qgsreport.h b/src/core/layout/qgsreport.h index f912e7d6b958..3c957595e0a8 100644 --- a/src/core/layout/qgsreport.h +++ b/src/core/layout/qgsreport.h @@ -18,7 +18,7 @@ #include "qgis_core.h" #include "qgsabstractreportsection.h" - +#include "qgsmasterlayoutinterface.h" ///@cond NOT_STABLE @@ -37,9 +37,11 @@ * * \since QGIS 3.0 */ -class CORE_EXPORT QgsReport : public QgsAbstractReportSection +class CORE_EXPORT QgsReport : public QObject, public QgsAbstractReportSection, public QgsMasterLayoutInterface { + Q_OBJECT + public: /** @@ -50,16 +52,25 @@ class CORE_EXPORT QgsReport : public QgsAbstractReportSection */ QgsReport( QgsProject *project ); + QgsProject *layoutProject() const override { return mProject; } + QgsReport *clone() const override SIP_FACTORY; + QString name() const override { return mName; } + void setName( const QString &name ) override; + QDomElement writeLayoutXml( QDomDocument &document, const QgsReadWriteContext &context ) const override; + bool readLayoutXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ) override; + + signals: + /** - * Returns the associated project. + * Emitted when the report's name is changed. + * \see setName() */ - QgsProject *project() { return mProject; } - - QgsReport *clone() const override SIP_FACTORY; + void nameChanged( const QString &name ); private: QgsProject *mProject = nullptr; + QString mName; }; diff --git a/src/core/layout/qgsreportsectionfieldgroup.cpp b/src/core/layout/qgsreportsectionfieldgroup.cpp index 09477167c1f1..09a6554c504b 100644 --- a/src/core/layout/qgsreportsectionfieldgroup.cpp +++ b/src/core/layout/qgsreportsectionfieldgroup.cpp @@ -136,7 +136,7 @@ void QgsReportSectionFieldGroup::updateChildContexts( const QgsFeature &feature QString newFilter = currentFilter.isEmpty() ? thisFilter : QStringLiteral( "(%1) AND (%2)" ).arg( currentFilter, thisFilter ); c.layerFilters[ mCoverageLayer.get() ] = newFilter; - const QList< QgsAbstractReportSection * > sections = children(); + const QList< QgsAbstractReportSection * > sections = childSections(); for ( QgsAbstractReportSection *section : qgis::as_const( sections ) ) { section->setContext( c ); diff --git a/src/core/qgsexpressioncontext.cpp b/src/core/qgsexpressioncontext.cpp index d37e97703850..a209f07539f6 100644 --- a/src/core/qgsexpressioncontext.cpp +++ b/src/core/qgsexpressioncontext.cpp @@ -1115,7 +1115,8 @@ QgsExpressionContextScope *QgsExpressionContextUtils::layoutScope( const QgsLayo } //add known layout context variables - scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layout_name" ), layout->name(), true ) ); + if ( const QgsMasterLayoutInterface *l = dynamic_cast< const QgsMasterLayoutInterface * >( layout ) ) + scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layout_name" ), l->name(), true ) ); scope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layout_numpages" ), layout->pageCollection()->pageCount(), true ) ); if ( layout->pageCollection()->pageCount() > 0 ) diff --git a/src/gui/layout/qgslayoutdesignerinterface.h b/src/gui/layout/qgslayoutdesignerinterface.h index 1946f3139087..f1be3d01104c 100644 --- a/src/gui/layout/qgslayoutdesignerinterface.h +++ b/src/gui/layout/qgslayoutdesignerinterface.h @@ -24,6 +24,7 @@ class QgsLayout; class QgsLayoutView; class QgsLayoutItem; class QgsMessageBar; +class QgsMasterLayoutInterface; /** * \ingroup gui @@ -50,11 +51,16 @@ class GUI_EXPORT QgsLayoutDesignerInterface: public QObject {} /** - * Returns the layout displayed in the designer. + * Returns the current layout displayed in the designer. * \see view() */ virtual QgsLayout *layout() = 0; + /** + * Returns the master layout displayed in the designer. + * \see layout() + */ + virtual QgsMasterLayoutInterface *masterLayout() = 0; /** * Returns the layout view utilized by the designer. diff --git a/src/gui/qgisinterface.h b/src/gui/qgisinterface.h index 19c758cc70af..a87beddc4732 100644 --- a/src/gui/qgisinterface.h +++ b/src/gui/qgisinterface.h @@ -34,6 +34,7 @@ class QgsFeature; class QgsLayerTreeMapCanvasBridge; class QgsLayerTreeView; class QgsLayout; +class QgsMasterLayoutInterface; class QgsLayoutDesignerInterface; class QgsMapCanvas; class QgsMapLayer; @@ -560,7 +561,7 @@ class GUI_EXPORT QgisInterface : public QObject * \since QGIS 3.0 * \see closeComposer() */ - virtual QgsLayoutDesignerInterface *openLayoutDesigner( QgsLayout *layout ) = 0; + virtual QgsLayoutDesignerInterface *openLayoutDesigner( QgsMasterLayoutInterface *layout ) = 0; /** * Opens the options dialog. The \a currentPage argument can be used to force diff --git a/tests/src/core/testqgslayout.cpp b/tests/src/core/testqgslayout.cpp index 329d74653a0a..b2b3eb1155d1 100644 --- a/tests/src/core/testqgslayout.cpp +++ b/tests/src/core/testqgslayout.cpp @@ -151,7 +151,7 @@ void TestQgsLayout::units() void TestQgsLayout::name() { QgsProject p; - QgsLayout layout( &p ); + QgsPrintLayout layout( &p ); QString layoutName = QStringLiteral( "test name" ); layout.setName( layoutName ); QCOMPARE( layout.name(), layoutName ); @@ -203,7 +203,7 @@ void TestQgsLayout::variablesEdited() void TestQgsLayout::scope() { QgsProject p; - QgsLayout l( &p ); + QgsPrintLayout l( &p ); // no crash std::unique_ptr< QgsExpressionContextScope > scope( QgsExpressionContextUtils::layoutScope( nullptr ) ); diff --git a/tests/src/core/testqgslayoutobject.cpp b/tests/src/core/testqgslayoutobject.cpp index a76c4de2dade..1f55e17fd817 100644 --- a/tests/src/core/testqgslayoutobject.cpp +++ b/tests/src/core/testqgslayoutobject.cpp @@ -20,6 +20,7 @@ #include "qgstest.h" #include "qgsproject.h" #include "qgsreadwritecontext.h" +#include "qgsprintlayout.h" class TestQgsLayoutObject: public QObject { @@ -126,7 +127,7 @@ void TestQgsLayoutObject::context() { QgsProject p; p.setTitle( QStringLiteral( "my title" ) ); - QgsLayout l( &p ); + QgsPrintLayout l( &p ); l.setName( QStringLiteral( "my layout" ) ); QgsLayoutObject *object = new QgsLayoutObject( nullptr ); diff --git a/tests/src/python/test_qgslayout.py b/tests/src/python/test_qgslayout.py index fac9c8ebb75c..f925a158bc25 100644 --- a/tests/src/python/test_qgslayout.py +++ b/tests/src/python/test_qgslayout.py @@ -24,6 +24,7 @@ QgsLayoutGuide, QgsLayoutObject, QgsProject, + QgsPrintLayout, QgsLayoutItemGroup, QgsLayoutItem, QgsProperty, @@ -58,7 +59,7 @@ def tearDownClass(cls): def testReadWriteXml(self): p = QgsProject() - l = QgsLayout(p) + l = QgsPrintLayout(p) l.setName('my layout') l.setUnits(QgsUnitTypes.LayoutInches) collection = l.pageCollection() @@ -91,7 +92,7 @@ def testReadWriteXml(self): doc = QDomDocument("testdoc") elem = l.writeXml(doc, QgsReadWriteContext()) - l2 = QgsLayout(p) + l2 = QgsPrintLayout(p) self.assertTrue(l2.readXml(elem, doc, QgsReadWriteContext())) self.assertEqual(l2.name(), 'my layout') self.assertEqual(l2.units(), QgsUnitTypes.LayoutInches) diff --git a/tests/src/python/test_qgslayoutmanager.py b/tests/src/python/test_qgslayoutmanager.py index 4f5a360f9eb1..ce59596027fe 100644 --- a/tests/src/python/test_qgslayoutmanager.py +++ b/tests/src/python/test_qgslayoutmanager.py @@ -17,7 +17,7 @@ from qgis.PyQt.QtXml import QDomDocument from qgis.core import (QgsComposition, - QgsLayout, + QgsPrintLayout, QgsLayoutManager, QgsProject) @@ -75,7 +75,7 @@ def testAddComposition(self): def testAddLayout(self): project = QgsProject() - layout = QgsLayout(project) + layout = QgsPrintLayout(project) layout.setName('test layout') manager = QgsLayoutManager(project) @@ -92,7 +92,7 @@ def testAddLayout(self): self.assertFalse(manager.addLayout(layout)) # try adding a second layout - layout2 = QgsLayout(project) + layout2 = QgsPrintLayout(project) layout2.setName('test layout2') self.assertTrue(manager.addLayout(layout2)) self.assertEqual(len(layout_added_spy), 2) @@ -101,7 +101,7 @@ def testAddLayout(self): self.assertEqual(layout_added_spy[1][0], 'test layout2') # adding a layout with duplicate name should fail - layout3 = QgsLayout(project) + layout3 = QgsPrintLayout(project) layout3.setName('test layout2') self.assertFalse(manager.addLayout(layout3)) @@ -125,11 +125,11 @@ def testCompositions(self): def testLayouts(self): project = QgsProject() manager = QgsLayoutManager(project) - layout = QgsLayout(project) + layout = QgsPrintLayout(project) layout.setName('test layout') - layout2 = QgsLayout(project) + layout2 = QgsPrintLayout(project) layout2.setName('test layout2') - layout3 = QgsLayout(project) + layout3 = QgsPrintLayout(project) layout3.setName('test layout3') manager.addLayout(layout) @@ -180,7 +180,7 @@ def layoutAboutToBeRemoved(self, name): def testRemoveLayout(self): project = QgsProject() - layout = QgsLayout(project) + layout = QgsPrintLayout(project) layout.setName('test layout') self.manager = QgsLayoutManager(project) @@ -217,11 +217,11 @@ def testClear(self): composition3 = QgsComposition(project) composition3.setName('test composition3') # add a bunch of layouts - layout = QgsLayout(project) + layout = QgsPrintLayout(project) layout.setName('test layout') - layout2 = QgsLayout(project) + layout2 = QgsPrintLayout(project) layout2.setName('test layout2') - layout3 = QgsLayout(project) + layout3 = QgsPrintLayout(project) layout3.setName('test layout3') manager.addComposition(composition) @@ -269,11 +269,11 @@ def testLayoutsByName(self): manager = QgsLayoutManager(project) # add a bunch of layouts - layout = QgsLayout(project) + layout = QgsPrintLayout(project) layout.setName('test layout') - layout2 = QgsLayout(project) + layout2 = QgsPrintLayout(project) layout2.setName('test layout2') - layout3 = QgsLayout(project) + layout3 = QgsPrintLayout(project) layout3.setName('test layout3') manager.addLayout(layout) @@ -305,11 +305,11 @@ def testReadWriteXml(self): manager.addComposition(composition3) # add a bunch of layouts - layout = QgsLayout(project) + layout = QgsPrintLayout(project) layout.setName('test layout') - layout2 = QgsLayout(project) + layout2 = QgsPrintLayout(project) layout2.setName('test layout2') - layout3 = QgsLayout(project) + layout3 = QgsPrintLayout(project) layout3.setName('test layout3') manager.addLayout(layout) @@ -377,12 +377,12 @@ def testGenerateUniqueTitle(self): manager = QgsLayoutManager(project) self.assertEqual(manager.generateUniqueTitle(), 'Layout 1') - layout = QgsLayout(project) + layout = QgsPrintLayout(project) layout.setName(manager.generateUniqueTitle()) manager.addLayout(layout) self.assertEqual(manager.generateUniqueTitle(), 'Layout 2') - layout2 = QgsLayout(project) + layout2 = QgsPrintLayout(project) layout2.setName(manager.generateUniqueTitle()) manager.addLayout(layout2) @@ -413,10 +413,10 @@ def testRenameSignalCompositions(self): def testRenameSignal(self): project = QgsProject() manager = QgsLayoutManager(project) - layout = QgsLayout(project) + layout = QgsPrintLayout(project) layout.setName('c1') manager.addLayout(layout) - layout2 = QgsLayout(project) + layout2 = QgsPrintLayout(project) layout2.setName('c2') manager.addLayout(layout2) diff --git a/tests/src/python/test_qgsreport.py b/tests/src/python/test_qgsreport.py index 37539ad8919f..7d6b8d461dc9 100644 --- a/tests/src/python/test_qgsreport.py +++ b/tests/src/python/test_qgsreport.py @@ -33,7 +33,7 @@ def testGettersSetters(self): p = QgsProject() r = QgsReport(p) - self.assertEqual(r.project(), p) + self.assertEqual(r.layoutProject(), p) r.setHeaderEnabled(True) self.assertTrue(r.headerEnabled()) @@ -49,16 +49,16 @@ def testGettersSetters(self): r.setFooter(footer) self.assertEqual(r.footer(), footer) - def testChildren(self): + def testchildSections(self): p = QgsProject() r = QgsReport(p) self.assertEqual(r.childCount(), 0) - self.assertEqual(r.children(), []) - self.assertIsNone(r.child(-1)) - self.assertIsNone(r.child(1)) - self.assertIsNone(r.child(0)) + self.assertEqual(r.childSections(), []) + self.assertIsNone(r.childSection(-1)) + self.assertIsNone(r.childSection(1)) + self.assertIsNone(r.childSection(0)) - # try deleting non-existant children + # try deleting non-existant childSections r.removeChildAt(-1) r.removeChildAt(0) r.removeChildAt(100) @@ -69,15 +69,15 @@ def testChildren(self): self.assertIsNone(child1.project()) r.appendChild(child1) self.assertEqual(r.childCount(), 1) - self.assertEqual(r.children(), [child1]) - self.assertEqual(r.child(0), child1) + self.assertEqual(r.childSections(), [child1]) + self.assertEqual(r.childSection(0), child1) self.assertEqual(child1.parent(), r) self.assertEqual(child1.project(), p) child2 = QgsReportSectionLayout() r.appendChild(child2) self.assertEqual(r.childCount(), 2) - self.assertEqual(r.children(), [child1, child2]) - self.assertEqual(r.child(1), child2) + self.assertEqual(r.childSections(), [child1, child2]) + self.assertEqual(r.childSection(1), child2) self.assertEqual(child2.parent(), r) def testInsertChild(self): @@ -87,12 +87,12 @@ def testInsertChild(self): child1 = QgsReportSectionLayout() r.insertChild(11, child1) self.assertEqual(r.childCount(), 1) - self.assertEqual(r.children(), [child1]) + self.assertEqual(r.childSections(), [child1]) self.assertEqual(child1.parent(), r) child2 = QgsReportSectionLayout() r.insertChild(-1, child2) self.assertEqual(r.childCount(), 2) - self.assertEqual(r.children(), [child2, child1]) + self.assertEqual(r.childSections(), [child2, child1]) self.assertEqual(child2.parent(), r) def testRemoveChild(self): @@ -108,15 +108,15 @@ def testRemoveChild(self): r.removeChildAt(100) r.removeChild(None) self.assertEqual(r.childCount(), 2) - self.assertEqual(r.children(), [child1, child2]) + self.assertEqual(r.childSections(), [child1, child2]) r.removeChildAt(1) self.assertEqual(r.childCount(), 1) - self.assertEqual(r.children(), [child1]) + self.assertEqual(r.childSections(), [child1]) r.removeChild(child1) self.assertEqual(r.childCount(), 0) - self.assertEqual(r.children(), []) + self.assertEqual(r.childSections(), []) def testClone(self): p = QgsProject() @@ -131,12 +131,12 @@ def testClone(self): cloned = r.clone() self.assertEqual(cloned.childCount(), 2) - self.assertTrue(cloned.child(0).headerEnabled()) - self.assertFalse(cloned.child(0).footerEnabled()) - self.assertEqual(cloned.child(0).parent(), cloned) - self.assertFalse(cloned.child(1).headerEnabled()) - self.assertTrue(cloned.child(1).footerEnabled()) - self.assertEqual(cloned.child(1).parent(), cloned) + self.assertTrue(cloned.childSection(0).headerEnabled()) + self.assertFalse(cloned.childSection(0).footerEnabled()) + self.assertEqual(cloned.childSection(0).parent(), cloned) + self.assertFalse(cloned.childSection(1).headerEnabled()) + self.assertTrue(cloned.childSection(1).footerEnabled()) + self.assertEqual(cloned.childSection(1).parent(), cloned) def testReportSectionLayout(self): r = QgsReportSectionLayout() From 2654454c0b68615d56c96c3282b6413b6c3d9927 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 29 Dec 2017 20:51:03 +1000 Subject: [PATCH 061/105] Implement serialization of reports --- .../core/layout/qgsabstractreportsection.sip | 47 +++++++- python/core/layout/qgsreport.sip | 1 + .../layout/qgsreportsectionfieldgroup.sip | 11 ++ python/core/layout/qgsreportsectionlayout.sip | 9 ++ src/core/layout/qgsabstractreportsection.cpp | 105 +++++++++++++++++- src/core/layout/qgsabstractreportsection.h | 44 +++++++- src/core/layout/qgsreport.cpp | 13 ++- src/core/layout/qgsreport.h | 1 + .../layout/qgsreportsectionfieldgroup.cpp | 50 +++++++++ src/core/layout/qgsreportsectionfieldgroup.h | 8 ++ src/core/layout/qgsreportsectionlayout.cpp | 24 ++++ src/core/layout/qgsreportsectionlayout.h | 7 ++ tests/src/python/test_qgsreport.py | 64 ++++++++++- 13 files changed, 375 insertions(+), 9 deletions(-) diff --git a/python/core/layout/qgsabstractreportsection.sip b/python/core/layout/qgsabstractreportsection.sip index cae2a8fd2f71..e4ddebd68597 100644 --- a/python/core/layout/qgsabstractreportsection.sip +++ b/python/core/layout/qgsabstractreportsection.sip @@ -45,6 +45,14 @@ exposed to the Python bindings for unit testing purposes only. %TypeHeaderCode #include "qgsabstractreportsection.h" +%End +%ConvertToSubClassCode + if ( dynamic_cast< QgsReportSectionFieldGroup * >( sipCpp ) ) + sipType = sipType_QgsReportSectionFieldGroup; + else if ( dynamic_cast< QgsReportSectionLayout * >( sipCpp ) ) + sipType = sipType_QgsReportSectionLayout; + else + sipType = NULL; %End public: @@ -58,6 +66,11 @@ Note that ownership is not transferred to ``parent``. + virtual QString type() const = 0; +%Docstring +Returns the section subclass type. +%End + virtual QgsAbstractReportSection *clone() const = 0 /Factory/; %Docstring Clones the report section. Ownership of the returned section is @@ -272,6 +285,20 @@ Sets the current ``context`` for this section. Returns the current context for this section. .. seealso:: :py:func:`setContext()` +%End + + bool writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const; +%Docstring +Stores the section state in a DOM element. + +.. seealso:: :py:func:`readXml()` +%End + + bool readXml( const QDomElement §ionElement, const QDomDocument &document, const QgsReadWriteContext &context ); +%Docstring +Sets the item state from a DOM element. + +.. seealso:: :py:func:`writeXml()` %End protected: @@ -291,9 +318,27 @@ Copies the common properties of a report section to a ``destination`` section. This method should be called from clone() implementations. %End - void setParent( QgsAbstractReportSection *parent ); + virtual void setParentSection( QgsAbstractReportSection *parent ); %Docstring Sets the ``parent`` report section. +%End + + virtual bool writePropertiesToElement( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const; +%Docstring +Stores section state within an XML DOM element. + +.. seealso:: :py:func:`writeXml()` + +.. seealso:: :py:func:`readPropertiesFromElement()` +%End + + virtual bool readPropertiesFromElement( const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context ); +%Docstring +Sets section state from a DOM element. + +.. seealso:: :py:func:`writePropertiesToElement()` + +.. seealso:: :py:func:`readXml()` %End private: diff --git a/python/core/layout/qgsreport.sip b/python/core/layout/qgsreport.sip index de44b5144263..a4aeea50f6ba 100644 --- a/python/core/layout/qgsreport.sip +++ b/python/core/layout/qgsreport.sip @@ -38,6 +38,7 @@ Constructor for QgsReport, associated with the specified Note that ownership is not transferred to ``project``. %End + virtual QString type() const; virtual QgsProject *layoutProject() const; virtual QgsReport *clone() const /Factory/; diff --git a/python/core/layout/qgsreportsectionfieldgroup.sip b/python/core/layout/qgsreportsectionfieldgroup.sip index 977e65f95fe0..972ffa1da45e 100644 --- a/python/core/layout/qgsreportsectionfieldgroup.sip +++ b/python/core/layout/qgsreportsectionfieldgroup.sip @@ -34,6 +34,8 @@ Constructor for QgsReportSectionFieldGroup, attached to the specified ``parent`` Note that ownership is not transferred to ``parent``. %End + virtual QString type() const; + QgsLayout *body(); %Docstring Returns the body layout for the section. @@ -85,6 +87,15 @@ Sets the ``field`` associated with this section. virtual void reset(); + virtual void setParentSection( QgsAbstractReportSection *parent ); + + + protected: + + virtual bool writePropertiesToElement( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const; + + virtual bool readPropertiesFromElement( const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context ); + }; diff --git a/python/core/layout/qgsreportsectionlayout.sip b/python/core/layout/qgsreportsectionlayout.sip index 7bfdab56365d..9d01b5740f1f 100644 --- a/python/core/layout/qgsreportsectionlayout.sip +++ b/python/core/layout/qgsreportsectionlayout.sip @@ -33,6 +33,8 @@ Constructor for QgsReportSectionLayout, attached to the specified ``parent`` sec Note that ownership is not transferred to ``parent``. %End + virtual QString type() const; + QgsLayout *body(); %Docstring Returns the body layout for the section. @@ -55,6 +57,13 @@ is transferred to the report section. virtual QgsLayout *nextBody( bool &ok ); + protected: + + virtual bool writePropertiesToElement( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const; + + virtual bool readPropertiesFromElement( const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context ); + + }; diff --git a/src/core/layout/qgsabstractreportsection.cpp b/src/core/layout/qgsabstractreportsection.cpp index b68c61cf2750..9a3356d154eb 100644 --- a/src/core/layout/qgsabstractreportsection.cpp +++ b/src/core/layout/qgsabstractreportsection.cpp @@ -17,6 +17,8 @@ #include "qgsabstractreportsection.h" #include "qgslayout.h" #include "qgsreport.h" +#include "qgsreportsectionfieldgroup.h" +#include "qgsreportsectionlayout.h" ///@cond NOT_STABLE @@ -54,6 +56,95 @@ void QgsAbstractReportSection::setContext( const QgsReportSectionContext &contex } } +bool QgsAbstractReportSection::writeXml( QDomElement &parentElement, QDomDocument &doc, const QgsReadWriteContext &context ) const +{ + QDomElement element = doc.createElement( QStringLiteral( "Section" ) ); + element.setAttribute( QStringLiteral( "type" ), type() ); + + element.setAttribute( QStringLiteral( "headerEnabled" ), mHeaderEnabled ? "1" : "0" ); + if ( mHeader ) + { + QDomElement headerElement = doc.createElement( QStringLiteral( "header" ) ); + headerElement.appendChild( mHeader->writeXml( doc, context ) ); + element.appendChild( headerElement ); + } + element.setAttribute( QStringLiteral( "footerEnabled" ), mFooterEnabled ? "1" : "0" ); + if ( mFooter ) + { + QDomElement footerElement = doc.createElement( QStringLiteral( "footer" ) ); + footerElement.appendChild( mFooter->writeXml( doc, context ) ); + element.appendChild( footerElement ); + } + + for ( QgsAbstractReportSection *section : mChildren ) + { + section->writeXml( element, doc, context ); + } + + writePropertiesToElement( element, doc, context ); + + parentElement.appendChild( element ); + return true; +} + +bool QgsAbstractReportSection::readXml( const QDomElement &element, const QDomDocument &doc, const QgsReadWriteContext &context ) +{ + if ( element.nodeName() != QStringLiteral( "Section" ) ) + { + return false; + } + + mHeaderEnabled = element.attribute( QStringLiteral( "headerEnabled" ), "0" ).toInt(); + mFooterEnabled = element.attribute( QStringLiteral( "footerEnabled" ), "0" ).toInt(); + const QDomElement headerElement = element.firstChildElement( QStringLiteral( "header" ) ); + if ( !headerElement.isNull() ) + { + const QDomElement headerLayoutElem = headerElement.firstChild().toElement(); + std::unique_ptr< QgsLayout > header = qgis::make_unique< QgsLayout >( project() ); + header->readXml( headerLayoutElem, doc, context ); + mHeader = std::move( header ); + } + const QDomElement footerElement = element.firstChildElement( QStringLiteral( "footer" ) ); + if ( !footerElement.isNull() ) + { + const QDomElement footerLayoutElem = footerElement.firstChild().toElement(); + std::unique_ptr< QgsLayout > footer = qgis::make_unique< QgsLayout >( project() ); + footer->readXml( footerLayoutElem, doc, context ); + mFooter = std::move( footer ); + } + + const QDomNodeList sectionItemList = element.childNodes(); + for ( int i = 0; i < sectionItemList.size(); ++i ) + { + const QDomElement currentSectionElem = sectionItemList.at( i ).toElement(); + if ( currentSectionElem.nodeName() != QStringLiteral( "Section" ) ) + continue; + + const QString sectionType = currentSectionElem.attribute( QStringLiteral( "type" ) ); + + //TODO - eventually move this to a registry when there's enough subclasses to warrant it + std::unique_ptr< QgsAbstractReportSection > section; + if ( sectionType == QLatin1String( "SectionFieldGroup" ) ) + { + section = qgis::make_unique< QgsReportSectionFieldGroup >(); + } + else if ( sectionType == QLatin1String( "SectionLayout" ) ) + { + section = qgis::make_unique< QgsReportSectionLayout >(); + } + + if ( section ) + { + appendChild( section.get() ); + section->readXml( currentSectionElem, doc, context ); + ( void )section.release(); //ownership was transferred already + } + } + + bool result = readPropertiesFromElement( element, doc, context ); + return result; +} + QString QgsAbstractReportSection::filePath( const QString &baseFilePath, const QString &extension ) { QString base = QStringLiteral( "%1_%2" ).arg( baseFilePath ).arg( mSectionNumber, 4, 10, QChar( '0' ) ); @@ -235,13 +326,13 @@ QgsAbstractReportSection *QgsAbstractReportSection::childSection( int index ) void QgsAbstractReportSection::appendChild( QgsAbstractReportSection *section ) { - section->setParent( this ); + section->setParentSection( this ); mChildren.append( section ); } void QgsAbstractReportSection::insertChild( int index, QgsAbstractReportSection *section ) { - section->setParent( this ); + section->setParentSection( this ); index = std::max( 0, index ); index = std::min( index, mChildren.count() ); mChildren.insert( index, section ); @@ -285,5 +376,15 @@ void QgsAbstractReportSection::copyCommonProperties( QgsAbstractReportSection *d } } +bool QgsAbstractReportSection::writePropertiesToElement( QDomElement &, QDomDocument &, const QgsReadWriteContext & ) const +{ + return true; +} + +bool QgsAbstractReportSection::readPropertiesFromElement( const QDomElement &, const QDomDocument &, const QgsReadWriteContext & ) +{ + return true; +} + ///@endcond diff --git a/src/core/layout/qgsabstractreportsection.h b/src/core/layout/qgsabstractreportsection.h index cb732191340b..3e51a49b68a8 100644 --- a/src/core/layout/qgsabstractreportsection.h +++ b/src/core/layout/qgsabstractreportsection.h @@ -53,6 +53,17 @@ class CORE_EXPORT QgsReportSectionContext class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator { +#ifdef SIP_RUN + SIP_CONVERT_TO_SUBCLASS_CODE + if ( dynamic_cast< QgsReportSectionFieldGroup * >( sipCpp ) ) + sipType = sipType_QgsReportSectionFieldGroup; + else if ( dynamic_cast< QgsReportSectionLayout * >( sipCpp ) ) + sipType = sipType_QgsReportSectionLayout; + else + sipType = NULL; + SIP_END +#endif + public: /** @@ -69,6 +80,11 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator //! QgsAbstractReportSection cannot be copied QgsAbstractReportSection &operator=( const QgsAbstractReportSection &other ) = delete; + /** + * Returns the section subclass type. + */ + virtual QString type() const = 0; + /** * Clones the report section. Ownership of the returned section is * transferred to the caller. @@ -242,6 +258,18 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator */ const QgsReportSectionContext &context() const { return mContext; } + /** + * Stores the section state in a DOM element. + * \see readXml() + */ + bool writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const; + + /** + * Sets the item state from a DOM element. + * \see writeXml() + */ + bool readXml( const QDomElement §ionElement, const QDomDocument &document, const QgsReadWriteContext &context ); + protected: //! Report sub-sections @@ -263,7 +291,21 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator /** * Sets the \a parent report section. */ - void setParent( QgsAbstractReportSection *parent ) { mParent = parent; } + virtual void setParentSection( QgsAbstractReportSection *parent ) { mParent = parent; } + + /** + * Stores section state within an XML DOM element. + * \see writeXml() + * \see readPropertiesFromElement() + */ + virtual bool writePropertiesToElement( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const; + + /** + * Sets section state from a DOM element. + * \see writePropertiesToElement() + * \see readXml() + */ + virtual bool readPropertiesFromElement( const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context ); private: diff --git a/src/core/layout/qgsreport.cpp b/src/core/layout/qgsreport.cpp index 9051791e0b00..560952ce729e 100644 --- a/src/core/layout/qgsreport.cpp +++ b/src/core/layout/qgsreport.cpp @@ -40,15 +40,20 @@ void QgsReport::setName( const QString &name ) QDomElement QgsReport::writeLayoutXml( QDomDocument &document, const QgsReadWriteContext &context ) const { QDomElement element = document.createElement( QStringLiteral( "Report" ) ); - - - + writeXml( element, document, context ); + element.setAttribute( QStringLiteral( "name" ), mName ); return element; } bool QgsReport::readLayoutXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ) { - + const QDomNodeList sectionList = layoutElement.elementsByTagName( QStringLiteral( "Section" ) ); + if ( sectionList.count() > 0 ) + { + readXml( sectionList.at( 0 ).toElement(), document, context ); + } + setName( layoutElement.attribute( QStringLiteral( "name" ) ) ); + return true; } ///@endcond diff --git a/src/core/layout/qgsreport.h b/src/core/layout/qgsreport.h index 3c957595e0a8..0325b98d5510 100644 --- a/src/core/layout/qgsreport.h +++ b/src/core/layout/qgsreport.h @@ -52,6 +52,7 @@ class CORE_EXPORT QgsReport : public QObject, public QgsAbstractReportSection, p */ QgsReport( QgsProject *project ); + QString type() const override { return QStringLiteral( "SectionReport" ); } QgsProject *layoutProject() const override { return mProject; } QgsReport *clone() const override SIP_FACTORY; QString name() const override { return mName; } diff --git a/src/core/layout/qgsreportsectionfieldgroup.cpp b/src/core/layout/qgsreportsectionfieldgroup.cpp index 09a6554c504b..ce75358440c5 100644 --- a/src/core/layout/qgsreportsectionfieldgroup.cpp +++ b/src/core/layout/qgsreportsectionfieldgroup.cpp @@ -98,6 +98,56 @@ void QgsReportSectionFieldGroup::reset() mEncounteredValues.clear(); } +void QgsReportSectionFieldGroup::setParentSection( QgsAbstractReportSection *parent ) +{ + QgsAbstractReportSection::setParentSection( parent ); + if ( !mCoverageLayer ) + mCoverageLayer.resolveWeakly( project() ); +} + +bool QgsReportSectionFieldGroup::writePropertiesToElement( QDomElement &element, QDomDocument &doc, const QgsReadWriteContext &context ) const +{ + element.setAttribute( QStringLiteral( "field" ), mField ); + + if ( mCoverageLayer ) + { + element.setAttribute( QStringLiteral( "coverageLayer" ), mCoverageLayer.layerId ); + element.setAttribute( QStringLiteral( "coverageLayerName" ), mCoverageLayer.name ); + element.setAttribute( QStringLiteral( "coverageLayerSource" ), mCoverageLayer.source ); + element.setAttribute( QStringLiteral( "coverageLayerProvider" ), mCoverageLayer.provider ); + } + + if ( mBody ) + { + QDomElement bodyElement = doc.createElement( QStringLiteral( "body" ) ); + bodyElement.appendChild( mBody->writeXml( doc, context ) ); + element.appendChild( bodyElement ); + } + return true; +} + +bool QgsReportSectionFieldGroup::readPropertiesFromElement( const QDomElement &element, const QDomDocument &doc, const QgsReadWriteContext &context ) +{ + mField = element.attribute( QStringLiteral( "field" ) ); + + QString layerId = element.attribute( QStringLiteral( "coverageLayer" ) ); + QString layerName = element.attribute( QStringLiteral( "coverageLayerName" ) ); + QString layerSource = element.attribute( QStringLiteral( "coverageLayerSource" ) ); + QString layerProvider = element.attribute( QStringLiteral( "coverageLayerProvider" ) ); + mCoverageLayer = QgsVectorLayerRef( layerId, layerName, layerSource, layerProvider ); + mCoverageLayer.resolveWeakly( project() ); + + const QDomElement bodyElement = element.firstChildElement( QStringLiteral( "body" ) ); + if ( !bodyElement.isNull() ) + { + const QDomElement bodyLayoutElem = bodyElement.firstChild().toElement(); + std::unique_ptr< QgsLayout > body = qgis::make_unique< QgsLayout >( project() ); + body->readXml( bodyLayoutElem, doc, context ); + mBody = std::move( body ); + } + return true; +} + QgsFeatureRequest QgsReportSectionFieldGroup::buildFeatureRequest() const { QgsFeatureRequest request; diff --git a/src/core/layout/qgsreportsectionfieldgroup.h b/src/core/layout/qgsreportsectionfieldgroup.h index 6075376ac991..a6abbda26cd5 100644 --- a/src/core/layout/qgsreportsectionfieldgroup.h +++ b/src/core/layout/qgsreportsectionfieldgroup.h @@ -44,6 +44,8 @@ class CORE_EXPORT QgsReportSectionFieldGroup : public QgsAbstractReportSection */ QgsReportSectionFieldGroup( QgsAbstractReportSection *parent = nullptr ); + QString type() const override { return QStringLiteral( "SectionFieldGroup" ); } + /** * Returns the body layout for the section. * \see setBody() @@ -85,6 +87,12 @@ class CORE_EXPORT QgsReportSectionFieldGroup : public QgsAbstractReportSection bool beginRender() override; QgsLayout *nextBody( bool &ok ) override; void reset() override; + void setParentSection( QgsAbstractReportSection *parent ) override; + + protected: + + bool writePropertiesToElement( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const override; + bool readPropertiesFromElement( const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context ) override; private: diff --git a/src/core/layout/qgsreportsectionlayout.cpp b/src/core/layout/qgsreportsectionlayout.cpp index fa5d85615b5f..97798156aed4 100644 --- a/src/core/layout/qgsreportsectionlayout.cpp +++ b/src/core/layout/qgsreportsectionlayout.cpp @@ -59,5 +59,29 @@ QgsLayout *QgsReportSectionLayout::nextBody( bool &ok ) } } +bool QgsReportSectionLayout::writePropertiesToElement( QDomElement &element, QDomDocument &doc, const QgsReadWriteContext &context ) const +{ + if ( mBody ) + { + QDomElement bodyElement = doc.createElement( QStringLiteral( "body" ) ); + bodyElement.appendChild( mBody->writeXml( doc, context ) ); + element.appendChild( bodyElement ); + } + return true; +} + +bool QgsReportSectionLayout::readPropertiesFromElement( const QDomElement &element, const QDomDocument &doc, const QgsReadWriteContext &context ) +{ + const QDomElement bodyElement = element.firstChildElement( QStringLiteral( "body" ) ); + if ( !bodyElement.isNull() ) + { + const QDomElement bodyLayoutElem = bodyElement.firstChild().toElement(); + std::unique_ptr< QgsLayout > body = qgis::make_unique< QgsLayout >( project() ); + body->readXml( bodyLayoutElem, doc, context ); + mBody = std::move( body ); + } + return true; +} + ///@endcond diff --git a/src/core/layout/qgsreportsectionlayout.h b/src/core/layout/qgsreportsectionlayout.h index 9ddd053c4acd..fcbf6c769588 100644 --- a/src/core/layout/qgsreportsectionlayout.h +++ b/src/core/layout/qgsreportsectionlayout.h @@ -41,6 +41,8 @@ class CORE_EXPORT QgsReportSectionLayout : public QgsAbstractReportSection */ QgsReportSectionLayout( QgsAbstractReportSection *parent = nullptr ); + QString type() const override { return QStringLiteral( "SectionLayout" ); } + /** * Returns the body layout for the section. * \see setBody() @@ -58,6 +60,11 @@ class CORE_EXPORT QgsReportSectionLayout : public QgsAbstractReportSection bool beginRender() override; QgsLayout *nextBody( bool &ok ) override; + protected: + + bool writePropertiesToElement( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const override; + bool readPropertiesFromElement( const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context ) override; + private: bool mExportedBody = false; diff --git a/tests/src/python/test_qgsreport.py b/tests/src/python/test_qgsreport.py index 7d6b8d461dc9..5e317aa4587c 100644 --- a/tests/src/python/test_qgsreport.py +++ b/tests/src/python/test_qgsreport.py @@ -21,8 +21,11 @@ QgsReportSectionFieldGroup, QgsVectorLayer, QgsField, - QgsFeature) + QgsFeature, + QgsReadWriteContext, + QgsUnitTypes) from qgis.testing import start_app, unittest +from qgis.PyQt.QtXml import QDomDocument start_app() @@ -429,6 +432,65 @@ def testFieldGroup(self): self.assertEqual(r.layout().reportContext().feature().attributes(), ['PNG', 'state1', 'town1']) self.assertFalse(r.next()) + def testReadWriteXml(self): + p = QgsProject() + ptLayer = QgsVectorLayer("Point?crs=epsg:4326&field=country:string(20)&field=state:string(20)&field=town:string(20)", "points", "memory") + p.addMapLayer(ptLayer) + + r = QgsReport(p) + r.setName('my report') + # add a header + r.setHeaderEnabled(True) + report_header = QgsLayout(p) + report_header.setUnits(QgsUnitTypes.LayoutInches) + r.setHeader(report_header) + # add a footer + r.setFooterEnabled(True) + report_footer = QgsLayout(p) + report_footer.setUnits(QgsUnitTypes.LayoutMeters) + r.setFooter(report_footer) + + # add some subsections + child1 = QgsReportSectionLayout() + child1_body = QgsLayout(p) + child1_body.setUnits(QgsUnitTypes.LayoutPoints) + child1.setBody(child1_body) + + child2 = QgsReportSectionLayout() + child2_body = QgsLayout(p) + child2_body.setUnits(QgsUnitTypes.LayoutPixels) + child2.setBody(child2_body) + child1.appendChild(child2) + + child2a = QgsReportSectionFieldGroup() + child2a_body = QgsLayout(p) + child2a_body.setUnits(QgsUnitTypes.LayoutInches) + child2a.setBody(child2a_body) + child2a.setField('my field') + child2a.setLayer(ptLayer) + child1.appendChild(child2a) + + r.appendChild(child1) + + doc = QDomDocument("testdoc") + elem = r.writeLayoutXml(doc, QgsReadWriteContext()) + + r2 = QgsReport(p) + self.assertTrue(r2.readLayoutXml(elem, doc, QgsReadWriteContext())) + self.assertEqual(r2.name(), 'my report') + self.assertTrue(r2.headerEnabled()) + self.assertEqual(r2.header().units(), QgsUnitTypes.LayoutInches) + self.assertTrue(r2.footerEnabled()) + self.assertEqual(r2.footer().units(), QgsUnitTypes.LayoutMeters) + + self.assertEqual(r2.childCount(), 1) + self.assertEqual(r2.childSection(0).body().units(), QgsUnitTypes.LayoutPoints) + self.assertEqual(r2.childSection(0).childCount(), 2) + self.assertEqual(r2.childSection(0).childSection(0).body().units(), QgsUnitTypes.LayoutPixels) + self.assertEqual(r2.childSection(0).childSection(1).body().units(), QgsUnitTypes.LayoutInches) + self.assertEqual(r2.childSection(0).childSection(1).field(), 'my field') + self.assertEqual(r2.childSection(0).childSection(1).layer(), ptLayer) + if __name__ == '__main__': unittest.main() From aef0432fdccfa3a7143a0e3c49392e4326d6b1e4 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 29 Dec 2017 20:58:41 +1000 Subject: [PATCH 062/105] Implement sort order customisation for field groups --- .../core/layout/qgsreportsectionfieldgroup.sip | 16 ++++++++++++++++ src/core/layout/qgsreportsectionfieldgroup.cpp | 15 ++++++++++++++- src/core/layout/qgsreportsectionfieldgroup.h | 15 +++++++++++++++ tests/src/python/test_qgsreport.py | 13 +++++++------ 4 files changed, 52 insertions(+), 7 deletions(-) diff --git a/python/core/layout/qgsreportsectionfieldgroup.sip b/python/core/layout/qgsreportsectionfieldgroup.sip index 972ffa1da45e..6280fe7bf302 100644 --- a/python/core/layout/qgsreportsectionfieldgroup.sip +++ b/python/core/layout/qgsreportsectionfieldgroup.sip @@ -77,6 +77,22 @@ Returns the field associated with this section. Sets the ``field`` associated with this section. .. seealso:: :py:func:`field()` +%End + + bool sortAscending() const; +%Docstring +Returns true if the field values should be sorted ascending, +or false for descending sort. + +.. seealso:: :py:func:`setSortAscending()` +%End + + void setSortAscending( bool sortAscending ); +%Docstring +Sets whether the field values should be sorted ascending. Set to true to sort +ascending, or false for descending sort. + +.. seealso:: :py:func:`sortAscending()` %End virtual QgsReportSectionFieldGroup *clone() const /Factory/; diff --git a/src/core/layout/qgsreportsectionfieldgroup.cpp b/src/core/layout/qgsreportsectionfieldgroup.cpp index ce75358440c5..24cdb38360f5 100644 --- a/src/core/layout/qgsreportsectionfieldgroup.cpp +++ b/src/core/layout/qgsreportsectionfieldgroup.cpp @@ -39,6 +39,7 @@ QgsReportSectionFieldGroup *QgsReportSectionFieldGroup::clone() const copy->setLayer( mCoverageLayer.get() ); copy->setField( mField ); + copy->setSortAscending( mSortAscending ); return copy.release(); } @@ -108,6 +109,7 @@ void QgsReportSectionFieldGroup::setParentSection( QgsAbstractReportSection *par bool QgsReportSectionFieldGroup::writePropertiesToElement( QDomElement &element, QDomDocument &doc, const QgsReadWriteContext &context ) const { element.setAttribute( QStringLiteral( "field" ), mField ); + element.setAttribute( QStringLiteral( "ascending" ), mSortAscending ? "1" : "0" ); if ( mCoverageLayer ) { @@ -129,6 +131,7 @@ bool QgsReportSectionFieldGroup::writePropertiesToElement( QDomElement &element, bool QgsReportSectionFieldGroup::readPropertiesFromElement( const QDomElement &element, const QDomDocument &doc, const QgsReadWriteContext &context ) { mField = element.attribute( QStringLiteral( "field" ) ); + mSortAscending = element.attribute( QStringLiteral( "ascending" ) ).toInt(); QString layerId = element.attribute( QStringLiteral( "coverageLayer" ) ); QString layerName = element.attribute( QStringLiteral( "coverageLayerName" ) ); @@ -148,13 +151,23 @@ bool QgsReportSectionFieldGroup::readPropertiesFromElement( const QDomElement &e return true; } +bool QgsReportSectionFieldGroup::sortAscending() const +{ + return mSortAscending; +} + +void QgsReportSectionFieldGroup::setSortAscending( bool sortAscending ) +{ + mSortAscending = sortAscending; +} + QgsFeatureRequest QgsReportSectionFieldGroup::buildFeatureRequest() const { QgsFeatureRequest request; QString filter = context().layerFilters.value( mCoverageLayer.get() ); if ( !filter.isEmpty() ) request.setFilterExpression( filter ); - request.addOrderBy( mField, true ); + request.addOrderBy( mField, mSortAscending ); return request; } diff --git a/src/core/layout/qgsreportsectionfieldgroup.h b/src/core/layout/qgsreportsectionfieldgroup.h index a6abbda26cd5..2a34bd979f43 100644 --- a/src/core/layout/qgsreportsectionfieldgroup.h +++ b/src/core/layout/qgsreportsectionfieldgroup.h @@ -83,6 +83,20 @@ class CORE_EXPORT QgsReportSectionFieldGroup : public QgsAbstractReportSection */ void setField( const QString &field ) { mField = field; } + /** + * Returns true if the field values should be sorted ascending, + * or false for descending sort. + * \see setSortAscending() + */ + bool sortAscending() const; + + /** + * Sets whether the field values should be sorted ascending. Set to true to sort + * ascending, or false for descending sort. + * \see sortAscending() + */ + void setSortAscending( bool sortAscending ); + QgsReportSectionFieldGroup *clone() const override SIP_FACTORY; bool beginRender() override; QgsLayout *nextBody( bool &ok ) override; @@ -98,6 +112,7 @@ class CORE_EXPORT QgsReportSectionFieldGroup : public QgsAbstractReportSection QgsVectorLayerRef mCoverageLayer; QString mField; + bool mSortAscending = true; int mFieldIndex = -1; QgsFeatureIterator mFeatures; QSet< QVariant > mEncounteredValues; diff --git a/tests/src/python/test_qgsreport.py b/tests/src/python/test_qgsreport.py index 5e317aa4587c..a86668ac9c7f 100644 --- a/tests/src/python/test_qgsreport.py +++ b/tests/src/python/test_qgsreport.py @@ -398,6 +398,7 @@ def testFieldGroup(self): child3.setLayer(ptLayer) child3.setBody(child3_body) child3.setField('town') + child3.setSortAscending(False) child2.appendChild(child3) self.assertTrue(r.beginRender()) self.assertTrue(r.next()) @@ -405,27 +406,27 @@ def testFieldGroup(self): self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'NSW', 'Sydney']) self.assertTrue(r.next()) self.assertEqual(r.layout(), child3_body) - self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Beerburrum']) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Emerald']) self.assertTrue(r.next()) self.assertEqual(r.layout(), child3_body) self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Brisbane']) self.assertTrue(r.next()) self.assertEqual(r.layout(), child3_body) - self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Emerald']) - self.assertTrue(r.next()) - self.assertEqual(r.layout(), child3_body) - self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'VIC', 'Geelong']) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Beerburrum']) self.assertTrue(r.next()) self.assertEqual(r.layout(), child3_body) self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'VIC', 'Melbourne']) self.assertTrue(r.next()) self.assertEqual(r.layout(), child3_body) - self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state1', 'town1']) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'VIC', 'Geelong']) self.assertTrue(r.next()) self.assertEqual(r.layout(), child3_body) self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state1', 'town2']) self.assertTrue(r.next()) self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state1', 'town1']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state2', 'town2']) self.assertTrue(r.next()) self.assertEqual(r.layout(), child3_body) From dcf364ffcd48394465dcc49f2e92fbf3122819b6 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 30 Dec 2017 09:05:47 +1000 Subject: [PATCH 063/105] Protect layout view against null layouts --- src/gui/layout/qgslayoutruler.cpp | 3 ++ src/gui/layout/qgslayoutview.cpp | 78 +++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/src/gui/layout/qgslayoutruler.cpp b/src/gui/layout/qgslayoutruler.cpp index f682fd3b0a55..b147c5d1e7e3 100644 --- a/src/gui/layout/qgslayoutruler.cpp +++ b/src/gui/layout/qgslayoutruler.cpp @@ -365,6 +365,9 @@ QPoint QgsLayoutRuler::convertLayoutPointToLocal( QPointF layoutPoint ) const QgsLayoutGuide *QgsLayoutRuler::guideAtPoint( QPoint localPoint ) const { + if ( !mView->currentLayout() ) + return nullptr; + QPointF layoutPoint = convertLocalPointToLayout( localPoint ); QList< QgsLayoutItemPage * > visiblePages = mView->visiblePages(); QList< QgsLayoutGuide * > guides = mView->currentLayout()->guides().guides( mOrientation == Qt::Horizontal ? Qt::Vertical : Qt::Horizontal ); diff --git a/src/gui/layout/qgslayoutview.cpp b/src/gui/layout/qgslayoutview.cpp index 54d63db57c0c..5f6b0cf561fd 100644 --- a/src/gui/layout/qgslayoutview.cpp +++ b/src/gui/layout/qgslayoutview.cpp @@ -190,6 +190,9 @@ void QgsLayoutView::scaleSafe( double scale ) void QgsLayoutView::setZoomLevel( double level ) { + if ( !currentLayout() ) + return; + if ( currentLayout()->units() == QgsUnitTypes::LayoutPixels ) { setTransform( QTransform::fromScale( level, level ) ); @@ -250,6 +253,9 @@ QgsLayoutViewMenuProvider *QgsLayoutView::menuProvider() const QList QgsLayoutView::visiblePages() const { + if ( !currentLayout() ) + return QList< QgsLayoutItemPage *>(); + //get current visible part of scene QRect viewportRect( 0, 0, viewport()->width(), viewport()->height() ); QRectF visibleRect = mapToScene( viewportRect ).boundingRect(); @@ -258,6 +264,9 @@ QList QgsLayoutView::visiblePages() const QList QgsLayoutView::visiblePageNumbers() const { + if ( !currentLayout() ) + return QList< int >(); + //get current visible part of scene QRect viewportRect( 0, 0, viewport()->width(), viewport()->height() ); QRectF visibleRect = mapToScene( viewportRect ).boundingRect(); @@ -266,18 +275,27 @@ QList QgsLayoutView::visiblePageNumbers() const void QgsLayoutView::alignSelectedItems( QgsLayoutAligner::Alignment alignment ) { + if ( !currentLayout() ) + return; + const QList selectedItems = currentLayout()->selectedLayoutItems(); QgsLayoutAligner::alignItems( currentLayout(), selectedItems, alignment ); } void QgsLayoutView::distributeSelectedItems( QgsLayoutAligner::Distribution distribution ) { + if ( !currentLayout() ) + return; + const QList selectedItems = currentLayout()->selectedLayoutItems(); QgsLayoutAligner::distributeItems( currentLayout(), selectedItems, distribution ); } void QgsLayoutView::resizeSelectedItems( QgsLayoutAligner::Resize resize ) { + if ( !currentLayout() ) + return; + const QList selectedItems = currentLayout()->selectedLayoutItems(); QgsLayoutAligner::resizeItems( currentLayout(), selectedItems, resize ); } @@ -289,6 +307,9 @@ void QgsLayoutView::copySelectedItems( QgsLayoutView::ClipboardOperation operati void QgsLayoutView::copyItems( const QList &items, QgsLayoutView::ClipboardOperation operation ) { + if ( !currentLayout() ) + return; + QgsReadWriteContext context; QDomDocument doc; QDomElement documentElement = doc.createElement( QStringLiteral( "LayoutItemClipboard" ) ); @@ -335,6 +356,9 @@ void QgsLayoutView::copyItems( const QList &items, QgsLayoutVie QList< QgsLayoutItem * > QgsLayoutView::pasteItems( QgsLayoutView::PasteMode mode ) { + if ( !currentLayout() ) + return QList< QgsLayoutItem * >(); + QList< QgsLayoutItem * > pastedItems; QDomDocument doc; QClipboard *clipboard = QApplication::clipboard(); @@ -373,6 +397,9 @@ QList< QgsLayoutItem * > QgsLayoutView::pasteItems( QgsLayoutView::PasteMode mod QList QgsLayoutView::pasteItems( QPointF layoutPoint ) { + if ( !currentLayout() ) + return QList(); + QList< QgsLayoutItem * > pastedItems; QDomDocument doc; QClipboard *clipboard = QApplication::clipboard(); @@ -455,6 +482,9 @@ void QgsLayoutView::setPaintingEnabled( bool enabled ) void QgsLayoutView::zoomFull() { + if ( !scene() ) + return; + fitInView( scene()->sceneRect(), Qt::KeepAspectRatio ); viewChanged(); emit zoomLevelChanged(); @@ -462,6 +492,9 @@ void QgsLayoutView::zoomFull() void QgsLayoutView::zoomWidth() { + if ( !scene() ) + return; + //get current visible part of scene QRect viewportRect( 0, 0, viewport()->width(), viewport()->height() ); QRectF visibleRect = mapToScene( viewportRect ).boundingRect(); @@ -623,6 +656,9 @@ void QgsLayoutView::selectNextItemBelow() void QgsLayoutView::raiseSelectedItems() { + if ( !currentLayout() ) + return; + const QList selectedItems = currentLayout()->selectedLayoutItems(); bool itemsRaised = false; for ( QgsLayoutItem *item : selectedItems ) @@ -643,6 +679,9 @@ void QgsLayoutView::raiseSelectedItems() void QgsLayoutView::lowerSelectedItems() { + if ( !currentLayout() ) + return; + const QList selectedItems = currentLayout()->selectedLayoutItems(); bool itemsLowered = false; for ( QgsLayoutItem *item : selectedItems ) @@ -663,6 +702,9 @@ void QgsLayoutView::lowerSelectedItems() void QgsLayoutView::moveSelectedItemsToTop() { + if ( !currentLayout() ) + return; + const QList selectedItems = currentLayout()->selectedLayoutItems(); bool itemsRaised = false; for ( QgsLayoutItem *item : selectedItems ) @@ -683,6 +725,9 @@ void QgsLayoutView::moveSelectedItemsToTop() void QgsLayoutView::moveSelectedItemsToBottom() { + if ( !currentLayout() ) + return; + const QList selectedItems = currentLayout()->selectedLayoutItems(); bool itemsLowered = false; for ( QgsLayoutItem *item : selectedItems ) @@ -703,6 +748,9 @@ void QgsLayoutView::moveSelectedItemsToBottom() void QgsLayoutView::lockSelectedItems() { + if ( !currentLayout() ) + return; + currentLayout()->undoStack()->beginMacro( tr( "Lock Items" ) ); const QList selectionList = currentLayout()->selectedLayoutItems(); for ( QgsLayoutItem *item : selectionList ) @@ -716,6 +764,9 @@ void QgsLayoutView::lockSelectedItems() void QgsLayoutView::unlockAllItems() { + if ( !currentLayout() ) + return; + //unlock all items in layout currentLayout()->undoStack()->beginMacro( tr( "Unlock Items" ) ); @@ -743,11 +794,17 @@ void QgsLayoutView::unlockAllItems() void QgsLayoutView::deleteSelectedItems() { + if ( !currentLayout() ) + return; + deleteItems( currentLayout()->selectedLayoutItems() ); } void QgsLayoutView::deleteItems( const QList &items ) { + if ( !currentLayout() ) + return; + if ( items.empty() ) return; @@ -817,6 +874,9 @@ void QgsLayoutView::ungroupSelectedItems() void QgsLayoutView::mousePressEvent( QMouseEvent *event ) { + if ( !currentLayout() ) + return; + if ( mSnapMarker ) mSnapMarker->setVisible( false ); @@ -853,6 +913,9 @@ void QgsLayoutView::mousePressEvent( QMouseEvent *event ) void QgsLayoutView::mouseReleaseEvent( QMouseEvent *event ) { + if ( !currentLayout() ) + return; + if ( mTool ) { std::unique_ptr me( new QgsLayoutViewMouseEvent( this, event, mTool->flags() & QgsLayoutViewTool::FlagSnaps ) ); @@ -866,6 +929,9 @@ void QgsLayoutView::mouseReleaseEvent( QMouseEvent *event ) void QgsLayoutView::mouseMoveEvent( QMouseEvent *event ) { + if ( !currentLayout() ) + return; + mMouseCurrentXY = event->pos(); QPointF cursorPos = mapToScene( mMouseCurrentXY ); @@ -906,6 +972,9 @@ void QgsLayoutView::mouseMoveEvent( QMouseEvent *event ) void QgsLayoutView::mouseDoubleClickEvent( QMouseEvent *event ) { + if ( !currentLayout() ) + return; + if ( mTool ) { std::unique_ptr me( new QgsLayoutViewMouseEvent( this, event, mTool->flags() & QgsLayoutViewTool::FlagSnaps ) ); @@ -919,6 +988,9 @@ void QgsLayoutView::mouseDoubleClickEvent( QMouseEvent *event ) void QgsLayoutView::wheelEvent( QWheelEvent *event ) { + if ( !currentLayout() ) + return; + if ( mTool ) { mTool->wheelEvent( event ); @@ -933,6 +1005,9 @@ void QgsLayoutView::wheelEvent( QWheelEvent *event ) void QgsLayoutView::keyPressEvent( QKeyEvent *event ) { + if ( !currentLayout() ) + return; + if ( mTool ) { mTool->keyPressEvent( event ); @@ -979,6 +1054,9 @@ void QgsLayoutView::keyPressEvent( QKeyEvent *event ) void QgsLayoutView::keyReleaseEvent( QKeyEvent *event ) { + if ( !currentLayout() ) + return; + if ( mTool ) { mTool->keyReleaseEvent( event ); From 56332b33c427c695c0f5d560993c40a67873d643 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 30 Dec 2017 09:13:40 +1000 Subject: [PATCH 064/105] Fix multiple empty layouts created when reloading project In related news... I find it frustrating that there's no way in Qt to restrict elementsByTagName to direct descendants only, and that all other available API calls for searching only direct descendants are much more fiddly... --- src/core/composer/qgslayoutmanager.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/composer/qgslayoutmanager.cpp b/src/core/composer/qgslayoutmanager.cpp index faeb7157dd01..10f2185ac785 100644 --- a/src/core/composer/qgslayoutmanager.cpp +++ b/src/core/composer/qgslayoutmanager.cpp @@ -204,9 +204,12 @@ bool QgsLayoutManager::readXml( const QDomElement &element, const QDomDocument & context.setPathResolver( mProject->pathResolver() ); // restore layouts - const QDomNodeList layoutNodes = element.elementsByTagName( QStringLiteral( "Layout" ) ); + const QDomNodeList layoutNodes = layoutsElem.childNodes(); for ( int i = 0; i < layoutNodes.size(); ++i ) { + if ( layoutNodes.at( i ).nodeName() != QStringLiteral( "Layout" ) ) + continue; + std::unique_ptr< QgsPrintLayout > l = qgis::make_unique< QgsPrintLayout >( mProject ); l->undoStack()->blockCommands( true ); if ( !l->readLayoutXml( layoutNodes.at( i ).toElement(), doc, context ) ) From 78f2174cfeba9e42995f5226d85dfa6aace3c275 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 30 Dec 2017 09:15:29 +1000 Subject: [PATCH 065/105] Add button in layout manager dialog to create a new report --- src/app/layout/qgslayoutmanagerdialog.cpp | 28 +++++++++++++++++++++++ src/app/layout/qgslayoutmanagerdialog.h | 2 ++ 2 files changed, 30 insertions(+) diff --git a/src/app/layout/qgslayoutmanagerdialog.cpp b/src/app/layout/qgslayoutmanagerdialog.cpp index 4bda29d9a2b1..b28e1cd7c493 100644 --- a/src/app/layout/qgslayoutmanagerdialog.cpp +++ b/src/app/layout/qgslayoutmanagerdialog.cpp @@ -75,6 +75,9 @@ QgsLayoutManagerDialog::QgsLayoutManagerDialog( QWidget *parent, Qt::WindowFlags mShowButton = mButtonBox->addButton( tr( "&Show" ), QDialogButtonBox::ActionRole ); connect( mShowButton, &QAbstractButton::clicked, this, &QgsLayoutManagerDialog::showClicked ); + mCreateReportButton = mButtonBox->addButton( tr( "Create &Report" ), QDialogButtonBox::ActionRole ); + connect( mCreateReportButton, &QAbstractButton::clicked, this, &QgsLayoutManagerDialog::createReport ); + mDuplicateButton = mButtonBox->addButton( tr( "&Duplicate" ), QDialogButtonBox::ActionRole ); connect( mDuplicateButton, &QAbstractButton::clicked, this, &QgsLayoutManagerDialog::duplicateClicked ); @@ -287,6 +290,31 @@ void QgsLayoutManagerDialog::mTemplatesUserDirBtn_pressed() openLocalDirectory( mUserTemplatesDir ); } +void QgsLayoutManagerDialog::createReport() +{ + QString title; + if ( !QgisApp::instance()->uniqueLayoutTitle( this, title, true ) ) + { + return; + } + + if ( title.isEmpty() ) + { + title = QgsProject::instance()->layoutManager()->generateUniqueTitle(); + } + + std::unique_ptr< QgsReport > report = qgis::make_unique< QgsReport >( QgsProject::instance() ); + report->setName( title ); + + std::unique_ptr< QgsLayout > header = qgis::make_unique< QgsLayout >( QgsProject::instance() ); + header->initializeDefaults(); + report->setHeader( header.release() ); + report->setHeaderEnabled( true ); + + QgisApp::instance()->openLayoutDesignerDialog( report.get() ); + QgsProject::instance()->layoutManager()->addLayout( report.release() ); +} + void QgsLayoutManagerDialog::openLocalDirectory( const QString &localDirPath ) { QDir localDir; diff --git a/src/app/layout/qgslayoutmanagerdialog.h b/src/app/layout/qgslayoutmanagerdialog.h index 2494d9a24150..11ddddfa6e5e 100644 --- a/src/app/layout/qgslayoutmanagerdialog.h +++ b/src/app/layout/qgslayoutmanagerdialog.h @@ -92,6 +92,7 @@ class QgsLayoutManagerDialog: public QDialog, private Ui::QgsLayoutManagerBase QPushButton *mRemoveButton = nullptr; QPushButton *mRenameButton = nullptr; QPushButton *mDuplicateButton = nullptr; + QPushButton *mCreateReportButton = nullptr; QgsLayoutManagerModel *mModel = nullptr; #ifdef Q_OS_MAC @@ -112,6 +113,7 @@ class QgsLayoutManagerDialog: public QDialog, private Ui::QgsLayoutManagerBase //! Slot to open user templates dir with user's system void mTemplatesUserDirBtn_pressed(); + void createReport(); void removeClicked(); void showClicked(); //! Duplicate layout From f4bb247c74df6835690d040c4d42706f5893bf1f Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 30 Dec 2017 09:18:21 +1000 Subject: [PATCH 066/105] Disable layout designer actions when no layout is set --- src/app/layout/qgslayoutdesignerdialog.cpp | 176 ++++++++++++++++----- src/app/layout/qgslayoutdesignerdialog.h | 2 + 2 files changed, 138 insertions(+), 40 deletions(-) diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index eeb2e157ecc9..4bcf097cbf71 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -667,6 +667,8 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla tabifyDockWidget( mGeneralDock, mItemDock ); tabifyDockWidget( mItemDock, mItemsDock ); + toggleActions( false ); + //set initial state of atlas controls mActionAtlasPreview->setEnabled( false ); mActionAtlasPreview->setChecked( false ); @@ -717,6 +719,11 @@ void QgsLayoutDesignerDialog::setMasterLayout( QgsMasterLayoutInterface *layout { connect( r, &QgsReport::nameChanged, this, &QgsLayoutDesignerDialog::setWindowTitle ); } + + if ( dynamic_cast< QgsPrintLayout * >( layout ) ) + { + createAtlasWidget(); + } } QgsMasterLayoutInterface *QgsLayoutDesignerDialog::masterLayout() @@ -726,52 +733,55 @@ QgsMasterLayoutInterface *QgsLayoutDesignerDialog::masterLayout() void QgsLayoutDesignerDialog::setCurrentLayout( QgsLayout *layout ) { - layout->deselectAll(); - mLayout = layout; - - mView->setCurrentLayout( layout ); - - // add undo/redo actions which apply to the correct layout undo stack - delete mUndoAction; - delete mRedoAction; - mUndoAction = layout->undoStack()->stack()->createUndoAction( this ); - mUndoAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionUndo.svg" ) ) ); - mUndoAction->setShortcuts( QKeySequence::Undo ); - mRedoAction = layout->undoStack()->stack()->createRedoAction( this ); - mRedoAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionRedo.svg" ) ) ); - mRedoAction->setShortcuts( QKeySequence::Redo ); - menuEdit->insertAction( menuEdit->actions().at( 0 ), mRedoAction ); - menuEdit->insertAction( mRedoAction, mUndoAction ); - mLayoutToolbar->addAction( mUndoAction ); - mLayoutToolbar->addAction( mRedoAction ); - - connect( mLayout->undoStack(), &QgsLayoutUndoStack::undoRedoOccurredForItems, this, &QgsLayoutDesignerDialog::undoRedoOccurredForItems ); - connect( mActionClearGuides, &QAction::triggered, &mLayout->guides(), [ = ] - { - mLayout->guides().clear(); - } ); + if ( !layout ) + { + toggleActions( false ); + } + else + { + layout->deselectAll(); + mLayout = layout; + + mView->setCurrentLayout( layout ); + + // add undo/redo actions which apply to the correct layout undo stack + delete mUndoAction; + delete mRedoAction; + mUndoAction = layout->undoStack()->stack()->createUndoAction( this ); + mUndoAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionUndo.svg" ) ) ); + mUndoAction->setShortcuts( QKeySequence::Undo ); + mRedoAction = layout->undoStack()->stack()->createRedoAction( this ); + mRedoAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionRedo.svg" ) ) ); + mRedoAction->setShortcuts( QKeySequence::Redo ); + menuEdit->insertAction( menuEdit->actions().at( 0 ), mRedoAction ); + menuEdit->insertAction( mRedoAction, mUndoAction ); + mLayoutToolbar->addAction( mUndoAction ); + mLayoutToolbar->addAction( mRedoAction ); + + connect( mLayout->undoStack(), &QgsLayoutUndoStack::undoRedoOccurredForItems, this, &QgsLayoutDesignerDialog::undoRedoOccurredForItems ); + connect( mActionClearGuides, &QAction::triggered, &mLayout->guides(), [ = ] + { + mLayout->guides().clear(); + } ); - mActionShowGrid->setChecked( mLayout->renderContext().gridVisible() ); - mActionSnapGrid->setChecked( mLayout->snapper().snapToGrid() ); - mActionShowGuides->setChecked( mLayout->guides().visible() ); - mActionSnapGuides->setChecked( mLayout->snapper().snapToGuides() ); - mActionSmartGuides->setChecked( mLayout->snapper().snapToItems() ); - mActionShowBoxes->setChecked( mLayout->renderContext().boundingBoxesVisible() ); - mActionShowPage->setChecked( mLayout->renderContext().pagesVisible() ); + mActionShowGrid->setChecked( mLayout->renderContext().gridVisible() ); + mActionSnapGrid->setChecked( mLayout->snapper().snapToGrid() ); + mActionShowGuides->setChecked( mLayout->guides().visible() ); + mActionSnapGuides->setChecked( mLayout->snapper().snapToGuides() ); + mActionSmartGuides->setChecked( mLayout->snapper().snapToItems() ); + mActionShowBoxes->setChecked( mLayout->renderContext().boundingBoxesVisible() ); + mActionShowPage->setChecked( mLayout->renderContext().pagesVisible() ); - mUndoView->setStack( mLayout->undoStack()->stack() ); + mUndoView->setStack( mLayout->undoStack()->stack() ); - mSelectTool->setLayout( layout ); - mItemsTreeView->setCurrentLayout( mLayout ); + mSelectTool->setLayout( layout ); + mItemsTreeView->setCurrentLayout( mLayout ); #ifdef ENABLE_MODELTEST - new ModelTest( mLayout->itemsModel(), this ); + new ModelTest( mLayout->itemsModel(), this ); #endif - createLayoutPropertiesWidget(); - - if ( qobject_cast< QgsPrintLayout * >( layout ) ) - { - createAtlasWidget(); + createLayoutPropertiesWidget(); + toggleActions( true ); } } @@ -1243,6 +1253,9 @@ void QgsLayoutDesignerDialog::sliderZoomChanged( int value ) void QgsLayoutDesignerDialog::updateStatusZoom() { + if ( !currentLayout() ) + return; + double zoomLevel = 0; if ( currentLayout()->units() == QgsUnitTypes::LayoutPixels ) { @@ -3053,6 +3066,89 @@ QgsLayoutAtlas *QgsLayoutDesignerDialog::atlas() return layout->atlas(); } +void QgsLayoutDesignerDialog::toggleActions( bool layoutAvailable ) +{ + mActionPan->setEnabled( layoutAvailable ); + mActionZoomTool->setEnabled( layoutAvailable ); + mActionSelectMoveItem->setEnabled( layoutAvailable ); + mActionZoomAll->setEnabled( layoutAvailable ); + mActionZoomIn->setEnabled( layoutAvailable ); + mActionZoomOut->setEnabled( layoutAvailable ); + mActionZoomActual->setEnabled( layoutAvailable ); + mActionZoomToWidth->setEnabled( layoutAvailable ); + mActionAddPages->setEnabled( layoutAvailable ); + mActionShowGrid->setEnabled( layoutAvailable ); + mActionSnapGrid->setEnabled( layoutAvailable ); + mActionShowGuides->setEnabled( layoutAvailable ); + mActionSnapGuides->setEnabled( layoutAvailable ); + mActionClearGuides->setEnabled( layoutAvailable ); + mActionLayoutProperties->setEnabled( layoutAvailable ); + mActionShowBoxes->setEnabled( layoutAvailable ); + mActionSmartGuides->setEnabled( layoutAvailable ); + mActionDeselectAll->setEnabled( layoutAvailable ); + mActionSelectAll->setEnabled( layoutAvailable ); + mActionInvertSelection->setEnabled( layoutAvailable ); + mActionSelectNextBelow->setEnabled( layoutAvailable ); + mActionSelectNextAbove->setEnabled( layoutAvailable ); + mActionLockItems->setEnabled( layoutAvailable ); + mActionUnlockAll->setEnabled( layoutAvailable ); + mActionRaiseItems->setEnabled( layoutAvailable ); + mActionLowerItems->setEnabled( layoutAvailable ); + mActionMoveItemsToTop->setEnabled( layoutAvailable ); + mActionMoveItemsToBottom->setEnabled( layoutAvailable ); + mActionAlignLeft->setEnabled( layoutAvailable ); + mActionAlignHCenter->setEnabled( layoutAvailable ); + mActionAlignRight->setEnabled( layoutAvailable ); + mActionAlignTop->setEnabled( layoutAvailable ); + mActionAlignVCenter->setEnabled( layoutAvailable ); + mActionAlignBottom->setEnabled( layoutAvailable ); + mActionDistributeLeft->setEnabled( layoutAvailable ); + mActionDistributeHCenter->setEnabled( layoutAvailable ); + mActionDistributeRight->setEnabled( layoutAvailable ); + mActionDistributeTop->setEnabled( layoutAvailable ); + mActionDistributeVCenter->setEnabled( layoutAvailable ); + mActionDistributeBottom->setEnabled( layoutAvailable ); + mActionResizeNarrowest->setEnabled( layoutAvailable ); + mActionResizeWidest->setEnabled( layoutAvailable ); + mActionResizeShortest->setEnabled( layoutAvailable ); + mActionResizeTallest->setEnabled( layoutAvailable ); + mActionDeleteSelection->setEnabled( layoutAvailable ); + mActionResizeToSquare->setEnabled( layoutAvailable ); + mActionShowPage->setEnabled( layoutAvailable ); + mActionGroupItems->setEnabled( layoutAvailable ); + mActionUngroupItems->setEnabled( layoutAvailable ); + mActionRefreshView->setEnabled( layoutAvailable ); + mActionEditNodesItem->setEnabled( layoutAvailable ); + mActionMoveItemContent->setEnabled( layoutAvailable ); + mActionPasteInPlace->setEnabled( layoutAvailable ); + mActionSaveAsTemplate->setEnabled( layoutAvailable ); + mActionLoadFromTemplate->setEnabled( layoutAvailable ); + mActionDuplicateLayout->setEnabled( layoutAvailable ); + mActionExportAsImage->setEnabled( layoutAvailable ); + mActionExportAsPDF->setEnabled( layoutAvailable ); + mActionExportAsSVG->setEnabled( layoutAvailable ); + mActionCut->setEnabled( layoutAvailable ); + mActionCopy->setEnabled( layoutAvailable ); + mActionPaste->setEnabled( layoutAvailable ); + menuAlign_Items->setEnabled( layoutAvailable ); + menu_Distribute_Items->setEnabled( layoutAvailable ); + menuResize->setEnabled( layoutAvailable ); + + const QList itemActions = mToolsActionGroup->actions(); + for ( QAction *action : itemActions ) + { + action->setEnabled( layoutAvailable ); + } + for ( auto it = mItemGroupSubmenus.constBegin(); it != mItemGroupSubmenus.constEnd(); ++it ) + { + it.value()->setEnabled( layoutAvailable ); + } + for ( auto it = mItemGroupToolButtons.constBegin(); it != mItemGroupToolButtons.constEnd(); ++it ) + { + it.value()->setEnabled( layoutAvailable ); + } +} + void QgsLayoutDesignerDialog::selectItems( const QList items ) { for ( QGraphicsItem *item : items ) diff --git a/src/app/layout/qgslayoutdesignerdialog.h b/src/app/layout/qgslayoutdesignerdialog.h index 2d0226167109..6f7eff3b4eb8 100644 --- a/src/app/layout/qgslayoutdesignerdialog.h +++ b/src/app/layout/qgslayoutdesignerdialog.h @@ -448,6 +448,8 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner void loadAtlasPredefinedScalesFromProject(); QgsLayoutAtlas *atlas(); + + void toggleActions( bool layoutAvailable ); }; #endif // QGSLAYOUTDESIGNERDIALOG_H From 66028bcf05644f91740e37408980a25aa7d910b8 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 30 Dec 2017 09:37:33 +1000 Subject: [PATCH 067/105] Cleaner way to hide atlas controls when not using a print layout --- src/app/layout/qgslayoutdesignerdialog.cpp | 32 ++++++++++++---------- src/ui/layout/qgslayoutdesignerbase.ui | 2 +- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 4bcf097cbf71..27ab561ec2c4 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -643,6 +643,9 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla mItemsTreeView = new QgsLayoutItemsListView( mItemsDock, this ); mItemsDock->setWidget( mItemsTreeView ); + mAtlasDock = new QgsDockWidget( tr( "Atlas" ), this ); + mAtlasDock->setObjectName( QStringLiteral( "AtlasDock" ) ); + const QList docks = findChildren(); for ( QDockWidget *dock : docks ) { @@ -654,18 +657,21 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla addDockWidget( Qt::RightDockWidgetArea, mGuideDock ); addDockWidget( Qt::RightDockWidgetArea, mUndoDock ); addDockWidget( Qt::RightDockWidgetArea, mItemsDock ); + addDockWidget( Qt::RightDockWidgetArea, mAtlasDock ); createLayoutPropertiesWidget(); mUndoDock->show(); mItemDock->show(); mGeneralDock->show(); + mAtlasDock->show(); mItemsDock->show(); tabifyDockWidget( mGeneralDock, mUndoDock ); tabifyDockWidget( mItemDock, mUndoDock ); tabifyDockWidget( mGeneralDock, mItemDock ); tabifyDockWidget( mItemDock, mItemsDock ); + tabifyDockWidget( mItemDock, mAtlasDock ); toggleActions( false ); @@ -681,8 +687,6 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla mActionExportAtlasAsImage->setEnabled( false ); mActionExportAtlasAsSVG->setEnabled( false ); mActionExportAtlasAsPDF->setEnabled( false ); - mAtlasToolbar->hide(); - mMenuAtlas->hide(); restoreWindowState(); @@ -724,6 +728,17 @@ void QgsLayoutDesignerDialog::setMasterLayout( QgsMasterLayoutInterface *layout { createAtlasWidget(); } + else + { + // ideally we'd only create mAtlasDock in createAtlasWidget() - + // but if we do that, then it's always brought to the focus + // in tab widgets + mAtlasDock->hide(); + mPanelsMenu->removeAction( mAtlasDock->toggleViewAction() ); + delete mMenuAtlas; + mMenuAtlas = nullptr; + mAtlasToolbar->hide(); + } } QgsMasterLayoutInterface *QgsLayoutDesignerDialog::masterLayout() @@ -2665,25 +2680,14 @@ void QgsLayoutDesignerDialog::createLayoutPropertiesWidget() void QgsLayoutDesignerDialog::createAtlasWidget() { - if ( !mAtlasDock ) - { - mAtlasDock = new QgsDockWidget( tr( "Atlas" ), this ); - mAtlasDock->setObjectName( QStringLiteral( "AtlasDock" ) ); - mPanelsMenu->addAction( mAtlasDock->toggleViewAction() ); - addDockWidget( Qt::RightDockWidgetArea, mAtlasDock ); - tabifyDockWidget( mItemDock, mAtlasDock ); - connect( mAtlasDock, &QDockWidget::visibilityChanged, this, &QgsLayoutDesignerDialog::dockVisibilityChanged ); - } - QgsPrintLayout *printLayout = qobject_cast< QgsPrintLayout * >( mLayout ); QgsLayoutAtlas *atlas = printLayout->atlas(); QgsLayoutAtlasWidget *atlasWidget = new QgsLayoutAtlasWidget( mAtlasDock, printLayout ); atlasWidget->setMessageBar( mMessageBar ); mAtlasDock->setWidget( atlasWidget ); - mAtlasDock->show(); - mMenuAtlas->show(); mAtlasToolbar->show(); + mPanelsMenu->addAction( mAtlasDock->toggleViewAction() ); connect( atlas, &QgsLayoutAtlas::messagePushed, mStatusBar, [ = ]( const QString & message ) { diff --git a/src/ui/layout/qgslayoutdesignerbase.ui b/src/ui/layout/qgslayoutdesignerbase.ui index 436aab2e8163..6745cb5a2559 100644 --- a/src/ui/layout/qgslayoutdesignerbase.ui +++ b/src/ui/layout/qgslayoutdesignerbase.ui @@ -92,7 +92,7 @@ - + 0 From 19b058103bcc0ed5dc6ecff272006e34c04716c9 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 30 Dec 2017 09:52:51 +1000 Subject: [PATCH 068/105] Add crappy inappropriate icons for layout types --- python/core/layout/qgsmasterlayoutinterface.sip | 5 +++++ python/core/layout/qgsprintlayout.sip | 2 ++ python/core/layout/qgsreport.sip | 2 ++ src/app/layout/qgslayoutmanagerdialog.cpp | 5 +++++ src/core/layout/qgsmasterlayoutinterface.h | 6 ++++++ src/core/layout/qgsprintlayout.cpp | 5 +++++ src/core/layout/qgsprintlayout.h | 1 + src/core/layout/qgsreport.cpp | 5 +++++ src/core/layout/qgsreport.h | 1 + 9 files changed, 32 insertions(+) diff --git a/python/core/layout/qgsmasterlayoutinterface.sip b/python/core/layout/qgsmasterlayoutinterface.sip index 2ba24278aaf7..daf4fc002be9 100644 --- a/python/core/layout/qgsmasterlayoutinterface.sip +++ b/python/core/layout/qgsmasterlayoutinterface.sip @@ -33,6 +33,11 @@ is transferred to the caller. Returns the layout's name. .. seealso:: :py:func:`setName()` +%End + + virtual QIcon icon() const = 0; +%Docstring +Returns an icon for the layout. %End virtual void setName( const QString &name ) = 0; diff --git a/python/core/layout/qgsprintlayout.sip b/python/core/layout/qgsprintlayout.sip index 482c36c60dfa..4fba1d2ba7e5 100644 --- a/python/core/layout/qgsprintlayout.sip +++ b/python/core/layout/qgsprintlayout.sip @@ -30,6 +30,8 @@ Constructor for QgsPrintLayout. virtual QgsProject *layoutProject() const; + virtual QIcon icon() const; + QgsLayoutAtlas *atlas(); %Docstring diff --git a/python/core/layout/qgsreport.sip b/python/core/layout/qgsreport.sip index a4aeea50f6ba..306b9eea3b3a 100644 --- a/python/core/layout/qgsreport.sip +++ b/python/core/layout/qgsreport.sip @@ -39,6 +39,8 @@ Note that ownership is not transferred to ``project``. %End virtual QString type() const; + virtual QIcon icon() const; + virtual QgsProject *layoutProject() const; virtual QgsReport *clone() const /Factory/; diff --git a/src/app/layout/qgslayoutmanagerdialog.cpp b/src/app/layout/qgslayoutmanagerdialog.cpp index b28e1cd7c493..d7f991d2badb 100644 --- a/src/app/layout/qgslayoutmanagerdialog.cpp +++ b/src/app/layout/qgslayoutmanagerdialog.cpp @@ -523,6 +523,11 @@ QVariant QgsLayoutManagerModel::data( const QModelIndex &index, int role ) const return QVariant(); } + case Qt::DecorationRole: + { + return mLayoutManager->layouts().at( index.row() )->icon(); + } + default: return QVariant(); } diff --git a/src/core/layout/qgsmasterlayoutinterface.h b/src/core/layout/qgsmasterlayoutinterface.h index f26bdcd7589b..32f0b5961b64 100644 --- a/src/core/layout/qgsmasterlayoutinterface.h +++ b/src/core/layout/qgsmasterlayoutinterface.h @@ -19,6 +19,7 @@ #include "qgis_core.h" #include "qgis_sip.h" #include +#include /** * \ingroup core @@ -45,6 +46,11 @@ class CORE_EXPORT QgsMasterLayoutInterface */ virtual QString name() const = 0; + /** + * Returns an icon for the layout. + */ + virtual QIcon icon() const = 0; + /** * Sets the layout's name. * \see name() diff --git a/src/core/layout/qgsprintlayout.cpp b/src/core/layout/qgsprintlayout.cpp index a82ca93a2546..2b4a914c8ef4 100644 --- a/src/core/layout/qgsprintlayout.cpp +++ b/src/core/layout/qgsprintlayout.cpp @@ -48,6 +48,11 @@ QgsProject *QgsPrintLayout::layoutProject() const return project(); } +QIcon QgsPrintLayout::icon() const +{ + return QgsApplication::getThemeIcon( QStringLiteral( "mActionNewComposer.svg" ) ); +} + QgsLayoutAtlas *QgsPrintLayout::atlas() { return mAtlas; diff --git a/src/core/layout/qgsprintlayout.h b/src/core/layout/qgsprintlayout.h index 08b5dd7a415e..08949125b326 100644 --- a/src/core/layout/qgsprintlayout.h +++ b/src/core/layout/qgsprintlayout.h @@ -41,6 +41,7 @@ class CORE_EXPORT QgsPrintLayout : public QgsLayout, public QgsMasterLayoutInter QgsPrintLayout *clone() const override SIP_FACTORY; QgsProject *layoutProject() const override; + QIcon icon() const override; /** * Returns the print layout's atlas. diff --git a/src/core/layout/qgsreport.cpp b/src/core/layout/qgsreport.cpp index 560952ce729e..805c18b77251 100644 --- a/src/core/layout/qgsreport.cpp +++ b/src/core/layout/qgsreport.cpp @@ -24,6 +24,11 @@ QgsReport::QgsReport( QgsProject *project ) , mProject( project ) {} +QIcon QgsReport::icon() const +{ + return QgsApplication::getThemeIcon( QStringLiteral( "processingResult.svg" ) ); +} + QgsReport *QgsReport::clone() const { std::unique_ptr< QgsReport > copy = qgis::make_unique< QgsReport >( mProject ); diff --git a/src/core/layout/qgsreport.h b/src/core/layout/qgsreport.h index 0325b98d5510..8ead5c779ad5 100644 --- a/src/core/layout/qgsreport.h +++ b/src/core/layout/qgsreport.h @@ -53,6 +53,7 @@ class CORE_EXPORT QgsReport : public QObject, public QgsAbstractReportSection, p QgsReport( QgsProject *project ); QString type() const override { return QStringLiteral( "SectionReport" ); } + QIcon icon() const override; QgsProject *layoutProject() const override { return mProject; } QgsReport *clone() const override SIP_FACTORY; QString name() const override { return mName; } From 3e12ec9dcbefcb1e573dfaccc53487336251d0a8 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 30 Dec 2017 10:02:32 +1000 Subject: [PATCH 069/105] Sort layouts in layout manager --- src/app/layout/qgslayoutmanagerdialog.cpp | 22 ++++++++++++++++------ src/app/layout/qgslayoutmanagerdialog.h | 12 ++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/app/layout/qgslayoutmanagerdialog.cpp b/src/app/layout/qgslayoutmanagerdialog.cpp index d7f991d2badb..ee2cf8f73c43 100644 --- a/src/app/layout/qgslayoutmanagerdialog.cpp +++ b/src/app/layout/qgslayoutmanagerdialog.cpp @@ -65,7 +65,9 @@ QgsLayoutManagerDialog::QgsLayoutManagerDialog( QWidget *parent, Qt::WindowFlags mModel = new QgsLayoutManagerModel( QgsProject::instance()->layoutManager(), this ); - mLayoutListView->setModel( mModel ); + mProxyModel = new QgsLayoutManagerProxyModel( mLayoutListView ); + mProxyModel->setSourceModel( mModel ); + mLayoutListView->setModel( mProxyModel ); connect( mButtonBox, &QDialogButtonBox::rejected, this, &QWidget::close ); connect( mLayoutListView->selectionModel(), &QItemSelectionModel::selectionChanged, @@ -386,7 +388,7 @@ void QgsLayoutManagerDialog::removeClicked() // Find the layouts that need to be deleted for ( const QModelIndex &index : layoutItems ) { - QgsMasterLayoutInterface *l = mModel->layoutFromIndex( index ); + QgsMasterLayoutInterface *l = mModel->layoutFromIndex( mProxyModel->mapToSource( index ) ); if ( l ) { layoutList << l; @@ -405,7 +407,7 @@ void QgsLayoutManagerDialog::showClicked() const QModelIndexList layoutItems = mLayoutListView->selectionModel()->selectedRows(); for ( const QModelIndex &index : layoutItems ) { - if ( QgsMasterLayoutInterface *l = mModel->layoutFromIndex( index ) ) + if ( QgsMasterLayoutInterface *l = mModel->layoutFromIndex( mProxyModel->mapToSource( index ) ) ) { QgisApp::instance()->openLayoutDesignerDialog( l ); } @@ -419,7 +421,7 @@ void QgsLayoutManagerDialog::duplicateClicked() return; } - QgsMasterLayoutInterface *currentLayout = mModel->layoutFromIndex( mLayoutListView->selectionModel()->selectedRows().at( 0 ) ); + QgsMasterLayoutInterface *currentLayout = mModel->layoutFromIndex( mProxyModel->mapToSource( mLayoutListView->selectionModel()->selectedRows().at( 0 ) ) ); if ( !currentLayout ) return; QString currentTitle = currentLayout->name(); @@ -459,7 +461,7 @@ void QgsLayoutManagerDialog::renameClicked() return; } - QgsMasterLayoutInterface *currentLayout = mModel->layoutFromIndex( mLayoutListView->selectionModel()->selectedRows().at( 0 ) ); + QgsMasterLayoutInterface *currentLayout = mModel->layoutFromIndex( mProxyModel->mapToSource( mLayoutListView->selectionModel()->selectedRows().at( 0 ) ) ); if ( !currentLayout ) return; @@ -474,7 +476,7 @@ void QgsLayoutManagerDialog::renameClicked() void QgsLayoutManagerDialog::itemDoubleClicked( const QModelIndex &index ) { - if ( QgsMasterLayoutInterface *l = mModel->layoutFromIndex( index ) ) + if ( QgsMasterLayoutInterface *l = mModel->layoutFromIndex( mProxyModel->mapToSource( index ) ) ) { QgisApp::instance()->openLayoutDesignerDialog( l ); } @@ -630,3 +632,11 @@ void QgsLayoutManagerModel::layoutRenamed( QgsMasterLayoutInterface *layout, con QModelIndex index = createIndex( row, 0 ); emit dataChanged( index, index, QVector() << Qt::DisplayRole ); } + +QgsLayoutManagerProxyModel::QgsLayoutManagerProxyModel( QObject *parent ) + : QSortFilterProxyModel( parent ) +{ + setDynamicSortFilter( true ); + sort( 0 ); + setSortCaseSensitivity( Qt::CaseInsensitive ); +} diff --git a/src/app/layout/qgslayoutmanagerdialog.h b/src/app/layout/qgslayoutmanagerdialog.h index 11ddddfa6e5e..a0adfcb333af 100644 --- a/src/app/layout/qgslayoutmanagerdialog.h +++ b/src/app/layout/qgslayoutmanagerdialog.h @@ -18,6 +18,7 @@ #define QGSLAYOUTMANAGERDIALOG_H #include +#include #include "ui_qgslayoutmanagerbase.h" @@ -55,6 +56,16 @@ class QgsLayoutManagerModel : public QAbstractListModel QgsLayoutManager *mLayoutManager = nullptr; }; +class QgsLayoutManagerProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + + public: + + explicit QgsLayoutManagerProxyModel( QObject *parent ); + +}; + /** * A dialog that allows management of layouts within a project. */ @@ -94,6 +105,7 @@ class QgsLayoutManagerDialog: public QDialog, private Ui::QgsLayoutManagerBase QPushButton *mDuplicateButton = nullptr; QPushButton *mCreateReportButton = nullptr; QgsLayoutManagerModel *mModel = nullptr; + QgsLayoutManagerProxyModel *mProxyModel = nullptr; #ifdef Q_OS_MAC void showEvent( QShowEvent *event ); From b862db06f05f1785cb938ec3703e95945e373a52 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 30 Dec 2017 10:48:34 +1000 Subject: [PATCH 070/105] Fix clashing inherited methods --- python/core/layout/qgsabstractreportsection.sip | 4 ++-- python/core/layout/qgsreportsectionfieldgroup.sip | 4 ++-- python/core/layout/qgsreportsectionlayout.sip | 2 +- src/core/layout/qgsabstractreportsection.cpp | 2 +- src/core/layout/qgsabstractreportsection.h | 4 ++-- src/core/layout/qgsreportsectionfieldgroup.h | 4 ++-- src/core/layout/qgsreportsectionlayout.h | 2 +- tests/src/python/test_qgsreport.py | 12 ++++++------ 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/python/core/layout/qgsabstractreportsection.sip b/python/core/layout/qgsabstractreportsection.sip index e4ddebd68597..323373149636 100644 --- a/python/core/layout/qgsabstractreportsection.sip +++ b/python/core/layout/qgsabstractreportsection.sip @@ -56,7 +56,7 @@ exposed to the Python bindings for unit testing purposes only. %End public: - QgsAbstractReportSection( QgsAbstractReportSection *parent = 0 ); + QgsAbstractReportSection( QgsAbstractReportSection *parentSection = 0 ); %Docstring Constructor for QgsAbstractReportSection, attached to the specified ``parent`` section. Note that ownership is not transferred to ``parent``. @@ -80,7 +80,7 @@ Subclasses should call copyCommonProperties() in their clone() implementations. %End - QgsAbstractReportSection *parent(); + QgsAbstractReportSection *parentSection(); %Docstring Returns the parent report section. %End diff --git a/python/core/layout/qgsreportsectionfieldgroup.sip b/python/core/layout/qgsreportsectionfieldgroup.sip index 6280fe7bf302..c47241fe943d 100644 --- a/python/core/layout/qgsreportsectionfieldgroup.sip +++ b/python/core/layout/qgsreportsectionfieldgroup.sip @@ -28,7 +28,7 @@ exposed to the Python bindings for unit testing purposes only. %End public: - QgsReportSectionFieldGroup( QgsAbstractReportSection *parent = 0 ); + QgsReportSectionFieldGroup( QgsAbstractReportSection *parentSection = 0 ); %Docstring Constructor for QgsReportSectionFieldGroup, attached to the specified ``parent`` section. Note that ownership is not transferred to ``parent``. @@ -103,7 +103,7 @@ ascending, or false for descending sort. virtual void reset(); - virtual void setParentSection( QgsAbstractReportSection *parent ); + virtual void setParentSection( QgsAbstractReportSection *parentSection ); protected: diff --git a/python/core/layout/qgsreportsectionlayout.sip b/python/core/layout/qgsreportsectionlayout.sip index 9d01b5740f1f..b97688db0975 100644 --- a/python/core/layout/qgsreportsectionlayout.sip +++ b/python/core/layout/qgsreportsectionlayout.sip @@ -27,7 +27,7 @@ exposed to the Python bindings for unit testing purposes only. %End public: - QgsReportSectionLayout( QgsAbstractReportSection *parent = 0 ); + QgsReportSectionLayout( QgsAbstractReportSection *parentSection = 0 ); %Docstring Constructor for QgsReportSectionLayout, attached to the specified ``parent`` section. Note that ownership is not transferred to ``parent``. diff --git a/src/core/layout/qgsabstractreportsection.cpp b/src/core/layout/qgsabstractreportsection.cpp index 9a3356d154eb..7e5810c05386 100644 --- a/src/core/layout/qgsabstractreportsection.cpp +++ b/src/core/layout/qgsabstractreportsection.cpp @@ -34,7 +34,7 @@ QgsAbstractReportSection::~QgsAbstractReportSection() QgsProject *QgsAbstractReportSection::project() { QgsAbstractReportSection *current = this; - while ( QgsAbstractReportSection *parent = current->parent() ) + while ( QgsAbstractReportSection *parent = current->parentSection() ) { if ( !parent ) return nullptr; diff --git a/src/core/layout/qgsabstractreportsection.h b/src/core/layout/qgsabstractreportsection.h index 3e51a49b68a8..45c8513fabe7 100644 --- a/src/core/layout/qgsabstractreportsection.h +++ b/src/core/layout/qgsabstractreportsection.h @@ -70,7 +70,7 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator * Constructor for QgsAbstractReportSection, attached to the specified \a parent section. * Note that ownership is not transferred to \a parent. */ - QgsAbstractReportSection( QgsAbstractReportSection *parent = nullptr ); + QgsAbstractReportSection( QgsAbstractReportSection *parentSection = nullptr ); ~QgsAbstractReportSection() override; @@ -97,7 +97,7 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator /** * Returns the parent report section. */ - QgsAbstractReportSection *parent() { return mParent; } + QgsAbstractReportSection *parentSection() { return mParent; } /** * Returns the associated project. diff --git a/src/core/layout/qgsreportsectionfieldgroup.h b/src/core/layout/qgsreportsectionfieldgroup.h index 2a34bd979f43..8cb9f65d08bb 100644 --- a/src/core/layout/qgsreportsectionfieldgroup.h +++ b/src/core/layout/qgsreportsectionfieldgroup.h @@ -42,7 +42,7 @@ class CORE_EXPORT QgsReportSectionFieldGroup : public QgsAbstractReportSection * Constructor for QgsReportSectionFieldGroup, attached to the specified \a parent section. * Note that ownership is not transferred to \a parent. */ - QgsReportSectionFieldGroup( QgsAbstractReportSection *parent = nullptr ); + QgsReportSectionFieldGroup( QgsAbstractReportSection *parentSection = nullptr ); QString type() const override { return QStringLiteral( "SectionFieldGroup" ); } @@ -101,7 +101,7 @@ class CORE_EXPORT QgsReportSectionFieldGroup : public QgsAbstractReportSection bool beginRender() override; QgsLayout *nextBody( bool &ok ) override; void reset() override; - void setParentSection( QgsAbstractReportSection *parent ) override; + void setParentSection( QgsAbstractReportSection *parentSection ) override; protected: diff --git a/src/core/layout/qgsreportsectionlayout.h b/src/core/layout/qgsreportsectionlayout.h index fcbf6c769588..c683c8deecf7 100644 --- a/src/core/layout/qgsreportsectionlayout.h +++ b/src/core/layout/qgsreportsectionlayout.h @@ -39,7 +39,7 @@ class CORE_EXPORT QgsReportSectionLayout : public QgsAbstractReportSection * Constructor for QgsReportSectionLayout, attached to the specified \a parent section. * Note that ownership is not transferred to \a parent. */ - QgsReportSectionLayout( QgsAbstractReportSection *parent = nullptr ); + QgsReportSectionLayout( QgsAbstractReportSection *parentSection = nullptr ); QString type() const override { return QStringLiteral( "SectionLayout" ); } diff --git a/tests/src/python/test_qgsreport.py b/tests/src/python/test_qgsreport.py index a86668ac9c7f..fa78e9f24748 100644 --- a/tests/src/python/test_qgsreport.py +++ b/tests/src/python/test_qgsreport.py @@ -74,14 +74,14 @@ def testchildSections(self): self.assertEqual(r.childCount(), 1) self.assertEqual(r.childSections(), [child1]) self.assertEqual(r.childSection(0), child1) - self.assertEqual(child1.parent(), r) + self.assertEqual(child1.parentSection(), r) self.assertEqual(child1.project(), p) child2 = QgsReportSectionLayout() r.appendChild(child2) self.assertEqual(r.childCount(), 2) self.assertEqual(r.childSections(), [child1, child2]) self.assertEqual(r.childSection(1), child2) - self.assertEqual(child2.parent(), r) + self.assertEqual(child2.parentSection(), r) def testInsertChild(self): p = QgsProject() @@ -91,12 +91,12 @@ def testInsertChild(self): r.insertChild(11, child1) self.assertEqual(r.childCount(), 1) self.assertEqual(r.childSections(), [child1]) - self.assertEqual(child1.parent(), r) + self.assertEqual(child1.parentSection(), r) child2 = QgsReportSectionLayout() r.insertChild(-1, child2) self.assertEqual(r.childCount(), 2) self.assertEqual(r.childSections(), [child2, child1]) - self.assertEqual(child2.parent(), r) + self.assertEqual(child2.parentSection(), r) def testRemoveChild(self): p = QgsProject() @@ -136,10 +136,10 @@ def testClone(self): self.assertEqual(cloned.childCount(), 2) self.assertTrue(cloned.childSection(0).headerEnabled()) self.assertFalse(cloned.childSection(0).footerEnabled()) - self.assertEqual(cloned.childSection(0).parent(), cloned) + self.assertEqual(cloned.childSection(0).parentSection(), cloned) self.assertFalse(cloned.childSection(1).headerEnabled()) self.assertTrue(cloned.childSection(1).footerEnabled()) - self.assertEqual(cloned.childSection(1).parent(), cloned) + self.assertEqual(cloned.childSection(1).parentSection(), cloned) def testReportSectionLayout(self): r = QgsReportSectionLayout() From f4a99b65bbae2a86da0bd3a99e7eb3504341f215 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 30 Dec 2017 16:51:39 +1000 Subject: [PATCH 071/105] Add a row method to QgsAbstractReportSection --- python/core/layout/qgsabstractreportsection.sip | 5 +++++ src/core/layout/qgsabstractreportsection.cpp | 8 ++++++++ src/core/layout/qgsabstractreportsection.h | 5 +++++ tests/src/python/test_qgsreport.py | 5 +++++ 4 files changed, 23 insertions(+) diff --git a/python/core/layout/qgsabstractreportsection.sip b/python/core/layout/qgsabstractreportsection.sip index 323373149636..bdf85bd996c8 100644 --- a/python/core/layout/qgsabstractreportsection.sip +++ b/python/core/layout/qgsabstractreportsection.sip @@ -216,6 +216,11 @@ Return the number of child sections for this report section. The child sections form the body of the report section. .. seealso:: :py:func:`children()` +%End + + int row() const; +%Docstring +Returns the row number of the section within it's parent section. %End QList< QgsAbstractReportSection * > childSections() const; diff --git a/src/core/layout/qgsabstractreportsection.cpp b/src/core/layout/qgsabstractreportsection.cpp index 7e5810c05386..fce63a7f10fd 100644 --- a/src/core/layout/qgsabstractreportsection.cpp +++ b/src/core/layout/qgsabstractreportsection.cpp @@ -319,6 +319,14 @@ void QgsAbstractReportSection::setFooter( QgsLayout *footer ) mFooter.reset( footer ); } +int QgsAbstractReportSection::row() const +{ + if ( mParent ) + return mParent->childSections().indexOf( const_cast( this ) ); + + return 0; +} + QgsAbstractReportSection *QgsAbstractReportSection::childSection( int index ) { return mChildren.value( index ); diff --git a/src/core/layout/qgsabstractreportsection.h b/src/core/layout/qgsabstractreportsection.h index 45c8513fabe7..b350cf2bbc04 100644 --- a/src/core/layout/qgsabstractreportsection.h +++ b/src/core/layout/qgsabstractreportsection.h @@ -203,6 +203,11 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator */ int childCount() const { return mChildren.count(); } + /** + * Returns the row number of the section within it's parent section. + */ + int row() const; + /** * Return all child sections for this report section. The child * sections form the body of the report section. diff --git a/tests/src/python/test_qgsreport.py b/tests/src/python/test_qgsreport.py index fa78e9f24748..b72a0aac9e71 100644 --- a/tests/src/python/test_qgsreport.py +++ b/tests/src/python/test_qgsreport.py @@ -75,6 +75,7 @@ def testchildSections(self): self.assertEqual(r.childSections(), [child1]) self.assertEqual(r.childSection(0), child1) self.assertEqual(child1.parentSection(), r) + self.assertEqual(child1.row(), 0) self.assertEqual(child1.project(), p) child2 = QgsReportSectionLayout() r.appendChild(child2) @@ -82,6 +83,7 @@ def testchildSections(self): self.assertEqual(r.childSections(), [child1, child2]) self.assertEqual(r.childSection(1), child2) self.assertEqual(child2.parentSection(), r) + self.assertEqual(child2.row(), 1) def testInsertChild(self): p = QgsProject() @@ -92,11 +94,14 @@ def testInsertChild(self): self.assertEqual(r.childCount(), 1) self.assertEqual(r.childSections(), [child1]) self.assertEqual(child1.parentSection(), r) + self.assertEqual(child1.row(), 0) child2 = QgsReportSectionLayout() r.insertChild(-1, child2) self.assertEqual(r.childCount(), 2) self.assertEqual(r.childSections(), [child2, child1]) self.assertEqual(child2.parentSection(), r) + self.assertEqual(child2.row(), 0) + self.assertEqual(child1.row(), 1) def testRemoveChild(self): p = QgsProject() From 6db24327f2ec12d27c435b45da49b2fefd5ee1c5 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 30 Dec 2017 16:52:28 +1000 Subject: [PATCH 072/105] Fix project retrieval for QgsReport --- src/core/layout/qgsabstractreportsection.cpp | 3 +++ tests/src/python/test_qgsreport.py | 1 + 2 files changed, 4 insertions(+) diff --git a/src/core/layout/qgsabstractreportsection.cpp b/src/core/layout/qgsabstractreportsection.cpp index fce63a7f10fd..1b159aa7e6f9 100644 --- a/src/core/layout/qgsabstractreportsection.cpp +++ b/src/core/layout/qgsabstractreportsection.cpp @@ -33,6 +33,9 @@ QgsAbstractReportSection::~QgsAbstractReportSection() QgsProject *QgsAbstractReportSection::project() { + if ( QgsReport *report = dynamic_cast< QgsReport * >( this ) ) + return report->layoutProject(); + QgsAbstractReportSection *current = this; while ( QgsAbstractReportSection *parent = current->parentSection() ) { diff --git a/tests/src/python/test_qgsreport.py b/tests/src/python/test_qgsreport.py index b72a0aac9e71..456a521f8ba3 100644 --- a/tests/src/python/test_qgsreport.py +++ b/tests/src/python/test_qgsreport.py @@ -37,6 +37,7 @@ def testGettersSetters(self): r = QgsReport(p) self.assertEqual(r.layoutProject(), p) + self.assertEqual(r.project(), p) r.setHeaderEnabled(True) self.assertTrue(r.headerEnabled()) From c9ddc9fda05b2fb941bb971d51b8439f6cae9f83 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 30 Dec 2017 17:54:41 +1000 Subject: [PATCH 073/105] Start on ui for configuring reports --- .../core/layout/qgsabstractreportsection.sip | 5 + python/core/layout/qgsreport.sip | 1 + .../layout/qgsreportsectionfieldgroup.sip | 2 + python/core/layout/qgsreportsectionlayout.sip | 1 + src/app/CMakeLists.txt | 4 + src/app/layout/qgslayoutdesignerdialog.cpp | 32 ++- src/app/layout/qgslayoutdesignerdialog.h | 3 + src/app/layout/qgsreportorganizerwidget.cpp | 154 +++++++++++++ src/app/layout/qgsreportorganizerwidget.h | 59 +++++ src/app/layout/qgsreportsectionmodel.cpp | 211 ++++++++++++++++++ src/app/layout/qgsreportsectionmodel.h | 63 ++++++ src/core/layout/qgsabstractreportsection.h | 5 + src/core/layout/qgsreport.h | 1 + .../layout/qgsreportsectionfieldgroup.cpp | 5 + src/core/layout/qgsreportsectionfieldgroup.h | 1 + src/core/layout/qgsreportsectionlayout.h | 1 + src/ui/layout/qgsreportorganizerwidgetbase.ui | 150 +++++++++++++ 17 files changed, 697 insertions(+), 1 deletion(-) create mode 100644 src/app/layout/qgsreportorganizerwidget.cpp create mode 100644 src/app/layout/qgsreportorganizerwidget.h create mode 100644 src/app/layout/qgsreportsectionmodel.cpp create mode 100644 src/app/layout/qgsreportsectionmodel.h create mode 100644 src/ui/layout/qgsreportorganizerwidgetbase.ui diff --git a/python/core/layout/qgsabstractreportsection.sip b/python/core/layout/qgsabstractreportsection.sip index bdf85bd996c8..56f330782a33 100644 --- a/python/core/layout/qgsabstractreportsection.sip +++ b/python/core/layout/qgsabstractreportsection.sip @@ -69,6 +69,11 @@ Note that ownership is not transferred to ``parent``. virtual QString type() const = 0; %Docstring Returns the section subclass type. +%End + + virtual QString description() const = 0; +%Docstring +Returns a user-visible, translated description of the section. %End virtual QgsAbstractReportSection *clone() const = 0 /Factory/; diff --git a/python/core/layout/qgsreport.sip b/python/core/layout/qgsreport.sip index 306b9eea3b3a..e8ce2be53a27 100644 --- a/python/core/layout/qgsreport.sip +++ b/python/core/layout/qgsreport.sip @@ -39,6 +39,7 @@ Note that ownership is not transferred to ``project``. %End virtual QString type() const; + virtual QString description() const; virtual QIcon icon() const; virtual QgsProject *layoutProject() const; diff --git a/python/core/layout/qgsreportsectionfieldgroup.sip b/python/core/layout/qgsreportsectionfieldgroup.sip index c47241fe943d..88adc6dcb610 100644 --- a/python/core/layout/qgsreportsectionfieldgroup.sip +++ b/python/core/layout/qgsreportsectionfieldgroup.sip @@ -35,6 +35,8 @@ Note that ownership is not transferred to ``parent``. %End virtual QString type() const; + virtual QString description() const; + QgsLayout *body(); %Docstring diff --git a/python/core/layout/qgsreportsectionlayout.sip b/python/core/layout/qgsreportsectionlayout.sip index b97688db0975..61992e7089cf 100644 --- a/python/core/layout/qgsreportsectionlayout.sip +++ b/python/core/layout/qgsreportsectionlayout.sip @@ -34,6 +34,7 @@ Note that ownership is not transferred to ``parent``. %End virtual QString type() const; + virtual QString description() const; QgsLayout *body(); %Docstring diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 744a4208a10a..852457603ced 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -204,6 +204,8 @@ SET(QGIS_APP_SRCS layout/qgslayoutscalebarwidget.cpp layout/qgslayoutshapewidget.cpp layout/qgslayouttablebackgroundcolorsdialog.cpp + layout/qgsreportorganizerwidget.cpp + layout/qgsreportsectionmodel.cpp locator/qgsinbuiltlocatorfilters.cpp locator/qgslocatoroptionswidget.cpp @@ -423,6 +425,8 @@ SET (QGIS_APP_MOC_HDRS layout/qgslayoutscalebarwidget.h layout/qgslayoutshapewidget.h layout/qgslayouttablebackgroundcolorsdialog.h + layout/qgsreportorganizerwidget.h + layout/qgsreportsectionmodel.h locator/qgsinbuiltlocatorfilters.h locator/qgslocatoroptionswidget.h diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 27ab561ec2c4..d2d0a5958efe 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -60,6 +60,7 @@ #include "qgslayoutatlaswidget.h" #include "qgslayoutpagecollection.h" #include "qgsreport.h" +#include "qgsreportorganizerwidget.h" #include "ui_qgssvgexportoptions.h" #include #include @@ -646,6 +647,9 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla mAtlasDock = new QgsDockWidget( tr( "Atlas" ), this ); mAtlasDock->setObjectName( QStringLiteral( "AtlasDock" ) ); + mReportDock = new QgsDockWidget( tr( "Report" ), this ); + mReportDock->setObjectName( QStringLiteral( "ReportDock" ) ); + const QList docks = findChildren(); for ( QDockWidget *dock : docks ) { @@ -658,6 +662,7 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla addDockWidget( Qt::RightDockWidgetArea, mUndoDock ); addDockWidget( Qt::RightDockWidgetArea, mItemsDock ); addDockWidget( Qt::RightDockWidgetArea, mAtlasDock ); + addDockWidget( Qt::RightDockWidgetArea, mReportDock ); createLayoutPropertiesWidget(); @@ -665,6 +670,7 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla mItemDock->show(); mGeneralDock->show(); mAtlasDock->show(); + mReportDock->show(); mItemsDock->show(); tabifyDockWidget( mGeneralDock, mUndoDock ); @@ -672,6 +678,7 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla tabifyDockWidget( mGeneralDock, mItemDock ); tabifyDockWidget( mItemDock, mItemsDock ); tabifyDockWidget( mItemDock, mAtlasDock ); + tabifyDockWidget( mItemDock, mReportDock ); toggleActions( false ); @@ -739,6 +746,19 @@ void QgsLayoutDesignerDialog::setMasterLayout( QgsMasterLayoutInterface *layout mMenuAtlas = nullptr; mAtlasToolbar->hide(); } + + if ( dynamic_cast< QgsReport * >( layout ) ) + { + createReportWidget(); + } + else + { + // ideally we'd only create mReportDock in createReportWidget() - + // but if we do that, then it's always brought to the focus + // in tab widgets + mReportDock->hide(); + mPanelsMenu->removeAction( mReportDock->toggleViewAction() ); + } } QgsMasterLayoutInterface *QgsLayoutDesignerDialog::masterLayout() @@ -2680,7 +2700,7 @@ void QgsLayoutDesignerDialog::createLayoutPropertiesWidget() void QgsLayoutDesignerDialog::createAtlasWidget() { - QgsPrintLayout *printLayout = qobject_cast< QgsPrintLayout * >( mLayout ); + QgsPrintLayout *printLayout = dynamic_cast< QgsPrintLayout * >( mMasterLayout ); QgsLayoutAtlas *atlas = printLayout->atlas(); QgsLayoutAtlasWidget *atlasWidget = new QgsLayoutAtlasWidget( mAtlasDock, printLayout ); atlasWidget->setMessageBar( mMessageBar ); @@ -2700,6 +2720,16 @@ void QgsLayoutDesignerDialog::createAtlasWidget() toggleAtlasControls( atlas->enabled() && atlas->coverageLayer() ); } +void QgsLayoutDesignerDialog::createReportWidget() +{ + QgsReport *report = dynamic_cast< QgsReport * >( mMasterLayout ); + QgsReportOrganizerWidget *reportWidget = new QgsReportOrganizerWidget( mReportDock, this, report ); + reportWidget->setMessageBar( mMessageBar ); + mReportDock->setWidget( reportWidget ); + + mPanelsMenu->addAction( mReportDock->toggleViewAction() ); +} + void QgsLayoutDesignerDialog::initializeRegistry() { sInitializedRegistry = true; diff --git a/src/app/layout/qgslayoutdesignerdialog.h b/src/app/layout/qgslayoutdesignerdialog.h index 6f7eff3b4eb8..7a651b00cf59 100644 --- a/src/app/layout/qgslayoutdesignerdialog.h +++ b/src/app/layout/qgslayoutdesignerdialog.h @@ -373,6 +373,8 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner QgsDockWidget *mItemsDock = nullptr; QgsLayoutItemsListView *mItemsTreeView = nullptr; + QgsDockWidget *mReportDock = nullptr; + QAction *mUndoAction = nullptr; QAction *mRedoAction = nullptr; //! Copy/cut/paste actions @@ -406,6 +408,7 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner void createLayoutPropertiesWidget(); void createAtlasWidget(); + void createReportWidget(); void initializeRegistry(); diff --git a/src/app/layout/qgsreportorganizerwidget.cpp b/src/app/layout/qgsreportorganizerwidget.cpp new file mode 100644 index 000000000000..48bc3ff3ddfe --- /dev/null +++ b/src/app/layout/qgsreportorganizerwidget.cpp @@ -0,0 +1,154 @@ +/*************************************************************************** + qgsreportorganizerwidget.cpp + ------------------------ + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsreportorganizerwidget.h" +#include "qgsreport.h" +#include "qgsreportsectionmodel.h" +#include "qgsreportsectionlayout.h" +#include "qgsreportsectionfieldgroup.h" +#include "qgslayout.h" +#include "qgslayoutdesignerdialog.h" +#include +#include + +#ifdef ENABLE_MODELTEST +#include "modeltest.h" +#endif + +QgsReportOrganizerWidget::QgsReportOrganizerWidget( QWidget *parent, QgsLayoutDesignerDialog *designer, QgsReport *report ) + : QgsPanelWidget( parent ) + , mReport( report ) + , mDesigner( designer ) +{ + setupUi( this ); + setPanelTitle( tr( "Report" ) ); + + mSectionModel = new QgsReportSectionModel( mReport, mViewSections ); + mViewSections->setModel( mSectionModel ); + +#ifdef ENABLE_MODELTEST + //new ModelTest( mSectionModel, this ); +#endif + + mViewSections->setEditTriggers( QAbstractItemView::AllEditTriggers ); + + QMenu *addMenu = new QMenu( mButtonAddSection ); + QAction *layoutSection = new QAction( tr( "Single section" ), addMenu ); + addMenu->addAction( layoutSection ); + connect( layoutSection, &QAction::triggered, this, &QgsReportOrganizerWidget::addLayoutSection ); + QAction *fieldGroupSection = new QAction( tr( "Field group" ), addMenu ); + addMenu->addAction( fieldGroupSection ); + connect( fieldGroupSection, &QAction::triggered, this, &QgsReportOrganizerWidget::addFieldGroupSection ); + + connect( mCheckShowHeader, &QCheckBox::toggled, this, &QgsReportOrganizerWidget::toggleHeader ); + connect( mCheckShowFooter, &QCheckBox::toggled, this, &QgsReportOrganizerWidget::toggleFooter ); + connect( mButtonEditHeader, &QPushButton::clicked, this, &QgsReportOrganizerWidget::editHeader ); + connect( mButtonEditFooter, &QPushButton::clicked, this, &QgsReportOrganizerWidget::editFooter ); + connect( mViewSections->selectionModel(), &QItemSelectionModel::currentChanged, this, &QgsReportOrganizerWidget::selectionChanged ); + + mButtonAddSection->setMenu( addMenu ); + connect( mButtonRemoveSection, &QPushButton::clicked, this, &QgsReportOrganizerWidget::removeSection ); +} + +void QgsReportOrganizerWidget::setMessageBar( QgsMessageBar *bar ) +{ + mMessageBar = bar; +} + +void QgsReportOrganizerWidget::addLayoutSection() +{ + std::unique_ptr< QgsReportSectionLayout > section = qgis::make_unique< QgsReportSectionLayout >(); + mSectionModel->addSection( mViewSections->currentIndex(), std::move( section ) ); +} + +void QgsReportOrganizerWidget::addFieldGroupSection() +{ + std::unique_ptr< QgsReportSectionFieldGroup > section = qgis::make_unique< QgsReportSectionFieldGroup >(); + mSectionModel->addSection( mViewSections->currentIndex(), std::move( section ) ); +} + +void QgsReportOrganizerWidget::removeSection() +{ + QgsAbstractReportSection *section = mSectionModel->sectionForIndex( mViewSections->currentIndex() ); + if ( dynamic_cast< QgsReport * >( section ) ) + return; //report cannot be removed + + int res = QMessageBox::question( this, tr( "Remove Section" ), + tr( "Are you sure you want to remove the report section?" ), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No ); + if ( res == QMessageBox::No ) + return; + + mSectionModel->removeRow( mViewSections->currentIndex().row(), mViewSections->currentIndex().parent() ); +} + +void QgsReportOrganizerWidget::toggleHeader( bool enabled ) +{ + QgsAbstractReportSection *parent = mSectionModel->sectionForIndex( mViewSections->currentIndex() ); + if ( !parent ) + parent = mReport; + parent->setHeaderEnabled( enabled ); +} + +void QgsReportOrganizerWidget::toggleFooter( bool enabled ) +{ + QgsAbstractReportSection *parent = mSectionModel->sectionForIndex( mViewSections->currentIndex() ); + if ( !parent ) + parent = mReport; + parent->setFooterEnabled( enabled ); +} + +void QgsReportOrganizerWidget::editHeader() +{ + QgsAbstractReportSection *parent = mSectionModel->sectionForIndex( mViewSections->currentIndex() ); + if ( !parent ) + parent = mReport; + + if ( !parent->header() ) + { + std::unique_ptr< QgsLayout > header = qgis::make_unique< QgsLayout >( mReport->layoutProject() ); + header->initializeDefaults(); + parent->setHeader( header.release() ); + } + + mDesigner->setCurrentLayout( parent->header() ); +} + +void QgsReportOrganizerWidget::editFooter() +{ + QgsAbstractReportSection *parent = mSectionModel->sectionForIndex( mViewSections->currentIndex() ); + if ( !parent ) + parent = mReport; + + if ( !parent->footer() ) + { + std::unique_ptr< QgsLayout > footer = qgis::make_unique< QgsLayout >( mReport->layoutProject() ); + footer->initializeDefaults(); + parent->setFooter( footer.release() ); + } + + mDesigner->setCurrentLayout( parent->footer() ); +} + +void QgsReportOrganizerWidget::selectionChanged( const QModelIndex ¤t, const QModelIndex & ) +{ + QgsAbstractReportSection *parent = mSectionModel->sectionForIndex( current ); + if ( !parent ) + parent = mReport; + + whileBlocking( mCheckShowHeader )->setChecked( parent->headerEnabled() ); + whileBlocking( mCheckShowFooter )->setChecked( parent->footerEnabled() ); +} diff --git a/src/app/layout/qgsreportorganizerwidget.h b/src/app/layout/qgsreportorganizerwidget.h new file mode 100644 index 000000000000..28b1ec29a513 --- /dev/null +++ b/src/app/layout/qgsreportorganizerwidget.h @@ -0,0 +1,59 @@ +/*************************************************************************** + qgsreportorganizerwidget.h + ---------------------- + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSREPORTORGANIZERWIDGET_H +#define QGSREPORTORGANIZERWIDGET_H + +#include "ui_qgsreportorganizerwidgetbase.h" +#include "qgspanelwidget.h" +#include + +class QgsReportSectionModel; +class QgsReport; +class QgsMessageBar; +class QgsLayoutDesignerDialog ; + +class QgsReportOrganizerWidget: public QgsPanelWidget, private Ui::QgsReportOrganizerBase +{ + Q_OBJECT + public: + QgsReportOrganizerWidget( QWidget *parent, QgsLayoutDesignerDialog *designer, QgsReport *report ); + + void setMessageBar( QgsMessageBar *bar ); + + private slots: + + void addLayoutSection(); + void addFieldGroupSection(); + void removeSection(); + void toggleHeader( bool enabled ); + void toggleFooter( bool enabled ); + void editHeader(); + void editFooter(); + void selectionChanged( const QModelIndex ¤t, const QModelIndex &previous ); + + private: + + QgsReport *mReport = nullptr; + QgsReportSectionModel *mSectionModel = nullptr; + QgsMessageBar *mMessageBar; + QgsLayoutDesignerDialog *mDesigner = nullptr; + +}; + + + +#endif // QGSREPORTORGANIZERWIDGET_H diff --git a/src/app/layout/qgsreportsectionmodel.cpp b/src/app/layout/qgsreportsectionmodel.cpp new file mode 100644 index 000000000000..e59e89df01e0 --- /dev/null +++ b/src/app/layout/qgsreportsectionmodel.cpp @@ -0,0 +1,211 @@ +/*************************************************************************** + qgsreportsectionmodel.cpp + --------------------- + begin : December 2017 + copyright : (C) 2017 by Nyall Dawso + email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsreportsectionmodel.h" + +#ifdef ENABLE_MODELTEST +#include "modeltest.h" +#endif + +QgsReportSectionModel::QgsReportSectionModel( QgsReport *report, QObject *parent ) + : QAbstractItemModel( parent ) + , mReport( report ) +{ +} + +Qt::ItemFlags QgsReportSectionModel::flags( const QModelIndex &index ) const +{ + if ( !index.isValid() ) + return 0; + + return QAbstractItemModel::flags( index ); +} + +QVariant QgsReportSectionModel::data( const QModelIndex &index, int role ) const +{ + if ( !index.isValid() ) + return QVariant(); + + QgsAbstractReportSection *section = sectionForIndex( index ); + if ( !section ) + return QVariant(); + + switch ( role ) + { + case Qt::DisplayRole: + case Qt::ToolTipRole: + { + switch ( index.column() ) + { + case 0: + return section->description(); + default: + return QVariant(); + } + break; + } + + case Qt::TextAlignmentRole: + { + return ( index.column() == 2 || index.column() == 3 ) ? Qt::AlignRight : Qt::AlignLeft; + } + + case Qt::EditRole: + { + switch ( index.column() ) + { + case 0: + return section->type(); + + default: + return QVariant(); + } + break; + } + + default: + return QVariant(); + } + + return QVariant(); +} + +QVariant QgsReportSectionModel::headerData( int section, Qt::Orientation orientation, int role ) const +{ + if ( orientation == Qt::Horizontal && role == Qt::DisplayRole && section >= 0 && section <= 0 ) + { + QStringList lst; + lst << tr( "Section" ); + return lst[section]; + } + + return QVariant(); +} + +int QgsReportSectionModel::rowCount( const QModelIndex &parent ) const +{ + QgsAbstractReportSection *parentSection = nullptr; + if ( parent.column() > 0 ) + return 0; + + if ( !parent.isValid() ) + parentSection = mReport; + else + parentSection = sectionForIndex( parent ); + + return parentSection->childCount(); +} + +int QgsReportSectionModel::columnCount( const QModelIndex & ) const +{ + return 1; +} + +QModelIndex QgsReportSectionModel::index( int row, int column, const QModelIndex &parent ) const +{ + if ( !hasIndex( row, column, parent ) ) + return QModelIndex(); + + QgsAbstractReportSection *parentSection = nullptr; + + if ( !parent.isValid() ) + parentSection = mReport; + else + parentSection = sectionForIndex( parent ); + + QgsAbstractReportSection *childSection = parentSection->childSection( row ); + if ( childSection ) + return createIndex( row, column, childSection ); + else + return QModelIndex(); +} + +QModelIndex QgsReportSectionModel::parent( const QModelIndex &index ) const +{ + if ( !index.isValid() ) + return QModelIndex(); + + QgsAbstractReportSection *childSection = sectionForIndex( index ); + QgsAbstractReportSection *parentSection = childSection->parentSection(); + + if ( parentSection == mReport ) + return QModelIndex(); + + return createIndex( parentSection->row(), 0, parentSection ); +} + +bool QgsReportSectionModel::setData( const QModelIndex &index, const QVariant &value, int role ) +{ + if ( !index.isValid() ) + return false; + + QgsAbstractReportSection *section = sectionForIndex( index ); + ( void )section; + ( void )value; + + if ( role != Qt::EditRole ) + return false; + + switch ( index.column() ) + { + case 0: + return false; + + default: + return false; + } + + emit dataChanged( index, index ); + return true; +} + +QgsAbstractReportSection *QgsReportSectionModel::sectionForIndex( const QModelIndex &index ) const +{ + return static_cast( index.internalPointer() ); +} + +bool QgsReportSectionModel::removeRows( int row, int count, const QModelIndex &parent ) +{ + QgsAbstractReportSection *parentSection = sectionForIndex( parent ); + + if ( row < 0 || row >= parentSection->childCount() ) + return false; + + beginRemoveRows( parent, row, row + count - 1 ); + + for ( int i = 0; i < count; i++ ) + { + if ( row < parentSection->childCount() ) + { + parentSection->removeChildAt( row ); + } + } + + endRemoveRows(); + + return true; +} + +void QgsReportSectionModel::addSection( const QModelIndex &parent, std::unique_ptr section ) +{ + QgsAbstractReportSection *parentSection = sectionForIndex( parent ); + if ( !parentSection ) + return; + + beginInsertRows( parent, parentSection->childCount(), parentSection->childCount() ); + parentSection->appendChild( section.release() ); + endInsertRows(); +} + diff --git a/src/app/layout/qgsreportsectionmodel.h b/src/app/layout/qgsreportsectionmodel.h new file mode 100644 index 000000000000..09dfcc3d815c --- /dev/null +++ b/src/app/layout/qgsreportsectionmodel.h @@ -0,0 +1,63 @@ +/*************************************************************************** + qgsreportsectionmodel.h + --------------------- + begin : December 2017 + copyright : (C) 2017 by Nyall Dawso + email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSREPORTSECTIONMODEL_H +#define QGSREPORTSECTIONMODEL_H + +#include "qgis.h" +#include "qgsreport.h" +#include + +/** + * \ingroup app + * \class QgsReportSectionModel + * \brief A model for managing the sections in a QgsReport. + * \since QGIS 3.0 + */ +class QgsReportSectionModel : public QAbstractItemModel +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsReportSectionModel, for the specified \a report. + */ + QgsReportSectionModel( QgsReport *report, QObject *parent ); + + Qt::ItemFlags flags( const QModelIndex &index ) const override; + QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const override; + QVariant headerData( int section, Qt::Orientation orientation, + int role = Qt::DisplayRole ) const override; + int rowCount( const QModelIndex &parent = QModelIndex() ) const override; + int columnCount( const QModelIndex & = QModelIndex() ) const override; + + QModelIndex index( int row, int column, const QModelIndex &parent = QModelIndex() ) const override; + QModelIndex parent( const QModelIndex &index ) const override; + bool setData( const QModelIndex &index, const QVariant &value, int role = Qt::EditRole ) override; + bool removeRows( int row, int count, const QModelIndex &parent = QModelIndex() ) override; + + void addSection( const QModelIndex &parent, std::unique_ptr< QgsAbstractReportSection > section ); + + /** + * Returns the report section for the given \a index. + */ + QgsAbstractReportSection *sectionForIndex( const QModelIndex &index ) const; + + private: + QgsReport *mReport = nullptr; +}; + +#endif // QGSREPORTSECTIONMODEL_H diff --git a/src/core/layout/qgsabstractreportsection.h b/src/core/layout/qgsabstractreportsection.h index b350cf2bbc04..6b1d21f59068 100644 --- a/src/core/layout/qgsabstractreportsection.h +++ b/src/core/layout/qgsabstractreportsection.h @@ -85,6 +85,11 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator */ virtual QString type() const = 0; + /** + * Returns a user-visible, translated description of the section. + */ + virtual QString description() const = 0; + /** * Clones the report section. Ownership of the returned section is * transferred to the caller. diff --git a/src/core/layout/qgsreport.h b/src/core/layout/qgsreport.h index 8ead5c779ad5..2fbb7e335ece 100644 --- a/src/core/layout/qgsreport.h +++ b/src/core/layout/qgsreport.h @@ -53,6 +53,7 @@ class CORE_EXPORT QgsReport : public QObject, public QgsAbstractReportSection, p QgsReport( QgsProject *project ); QString type() const override { return QStringLiteral( "SectionReport" ); } + QString description() const override { return QObject::tr( "Report" ); } QIcon icon() const override; QgsProject *layoutProject() const override { return mProject; } QgsReport *clone() const override SIP_FACTORY; diff --git a/src/core/layout/qgsreportsectionfieldgroup.cpp b/src/core/layout/qgsreportsectionfieldgroup.cpp index 24cdb38360f5..14eb9729de6c 100644 --- a/src/core/layout/qgsreportsectionfieldgroup.cpp +++ b/src/core/layout/qgsreportsectionfieldgroup.cpp @@ -25,6 +25,11 @@ QgsReportSectionFieldGroup::QgsReportSectionFieldGroup( QgsAbstractReportSection } +QString QgsReportSectionFieldGroup::description() const +{ + return QObject::tr( "Group: %1" ).arg( mField ); +} + QgsReportSectionFieldGroup *QgsReportSectionFieldGroup::clone() const { std::unique_ptr< QgsReportSectionFieldGroup > copy = qgis::make_unique< QgsReportSectionFieldGroup >( nullptr ); diff --git a/src/core/layout/qgsreportsectionfieldgroup.h b/src/core/layout/qgsreportsectionfieldgroup.h index 8cb9f65d08bb..bb2e248fa866 100644 --- a/src/core/layout/qgsreportsectionfieldgroup.h +++ b/src/core/layout/qgsreportsectionfieldgroup.h @@ -45,6 +45,7 @@ class CORE_EXPORT QgsReportSectionFieldGroup : public QgsAbstractReportSection QgsReportSectionFieldGroup( QgsAbstractReportSection *parentSection = nullptr ); QString type() const override { return QStringLiteral( "SectionFieldGroup" ); } + QString description() const override; /** * Returns the body layout for the section. diff --git a/src/core/layout/qgsreportsectionlayout.h b/src/core/layout/qgsreportsectionlayout.h index c683c8deecf7..e60dbbc5acb9 100644 --- a/src/core/layout/qgsreportsectionlayout.h +++ b/src/core/layout/qgsreportsectionlayout.h @@ -42,6 +42,7 @@ class CORE_EXPORT QgsReportSectionLayout : public QgsAbstractReportSection QgsReportSectionLayout( QgsAbstractReportSection *parentSection = nullptr ); QString type() const override { return QStringLiteral( "SectionLayout" ); } + QString description() const override { return QObject::tr( "Section" ); } /** * Returns the body layout for the section. diff --git a/src/ui/layout/qgsreportorganizerwidgetbase.ui b/src/ui/layout/qgsreportorganizerwidgetbase.ui new file mode 100644 index 000000000000..481f8cc5c415 --- /dev/null +++ b/src/ui/layout/qgsreportorganizerwidgetbase.ui @@ -0,0 +1,150 @@ + + + QgsReportOrganizerBase + + + + 0 + 0 + 723 + 520 + + + + + 425 + 300 + + + + Layout Manager + + + + + + Qt::CustomContextMenu + + + true + + + QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + + + true + + + QAbstractItemView::InternalMove + + + QAbstractItemView::ExtendedSelection + + + true + + + 100 + + + true + + + + + + + + + Add rule + + + + + + + :/images/themes/default/symbologyAdd.svg:/images/themes/default/symbologyAdd.svg + + + + + + + Remove selected rules + + + + + + + :/images/themes/default/symbologyRemove.svg:/images/themes/default/symbologyRemove.svg + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + + + Edit + + + + + + + Show header + + + + + + + Edit + + + + + + + Show footer + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + From fc9a45105c966d2070fa8c96624c5a20ad327b48 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 30 Dec 2017 19:22:00 +1000 Subject: [PATCH 074/105] Add buttons for exporting reports --- src/app/layout/qgslayoutdesignerdialog.cpp | 481 +++++++++++++++++++-- src/app/layout/qgslayoutdesignerdialog.h | 4 + src/ui/layout/qgslayoutdesignerbase.ui | 78 +++- 3 files changed, 527 insertions(+), 36 deletions(-) diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index d2d0a5958efe..74045e5b3b2f 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -222,6 +222,11 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla connect( mActionExportAtlasAsSVG, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportAtlasToSvg ); connect( mActionExportAtlasAsPDF, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportAtlasToPdf ); + connect( mActionReportSettings, &QAction::triggered, this, &QgsLayoutDesignerDialog::showReportSettings ); + connect( mActionExportReportAsImage, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportReportToRaster ); + connect( mActionExportReportAsSVG, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportReportToSvg ); + connect( mActionExportReportAsPDF, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportReportToPdf ); + mView = new QgsLayoutView(); //mView->setMapCanvas( mQgis->mapCanvas() ); mView->setContentsMargins( 0, 0, 0, 0 ); @@ -758,6 +763,9 @@ void QgsLayoutDesignerDialog::setMasterLayout( QgsMasterLayoutInterface *layout // in tab widgets mReportDock->hide(); mPanelsMenu->removeAction( mReportDock->toggleViewAction() ); + delete mMenuReport; + mMenuReport = nullptr; + mReportToolbar->hide(); } } @@ -2595,6 +2603,375 @@ void QgsLayoutDesignerDialog::exportAtlasToPdf() QApplication::restoreOverrideCursor(); } +void QgsLayoutDesignerDialog::exportReportToRaster() +{ + QgsSettings s; + QString outputFileName = QgsFileUtils::stringToSafeFilename( mMasterLayout->name() ); + + QPair fileNExt = QgsGuiUtils::getSaveAsImageName( this, tr( "Save report as" ), outputFileName ); + this->activateWindow(); + + if ( fileNExt.first.isEmpty() ) + { + return; + } + +#ifdef Q_OS_MAC + QgisApp::instance()->activateWindow(); + this->raise(); +#endif + + QgsLayoutExporter::ImageExportSettings settings; + QSize imageSize; + if ( !getRasterExportSettings( settings, imageSize ) ) + return; + + mView->setPaintingEnabled( false ); + QApplication::setOverrideCursor( Qt::BusyCursor ); + + QString error; + std::unique_ptr< QgsFeedback > feedback = qgis::make_unique< QgsFeedback >(); + std::unique_ptr< QProgressDialog > progressDialog = qgis::make_unique< QProgressDialog >( tr( "Rendering report..." ), tr( "Abort" ), 0, 100, this ); + progressDialog->setWindowTitle( tr( "Exporting Report" ) ); + connect( feedback.get(), &QgsFeedback::progressChanged, this, [ & ]( double progress ) + { + progressDialog->setValue( progress ); + progressDialog->setLabelText( feedback->property( "progress" ).toString() ) ; + +#ifdef Q_OS_LINUX + // For some reason on Windows hasPendingEvents() always return true, + // but one iteration is actually enough on Windows to get good interactivity + // whereas on Linux we must allow for far more iterations. + // For safety limit the number of iterations + int nIters = 0; + while ( QCoreApplication::hasPendingEvents() && ++nIters < 100 ) +#endif + { + QCoreApplication::processEvents(); + } + + } ); + connect( progressDialog.get(), &QProgressDialog::canceled, this, [ & ] + { + feedback->cancel(); + } ); + + QFileInfo fi( fileNExt.first ); + QString dir = fi.path(); + QString fileName = dir + '/' + fi.baseName(); + QgsLayoutExporter::ExportResult result = QgsLayoutExporter::exportToImage( dynamic_cast< QgsReport * >( mMasterLayout ), fileName, fileNExt.second, settings, error, feedback.get() ); + QApplication::restoreOverrideCursor(); + + switch ( result ) + { + case QgsLayoutExporter::Success: + mMessageBar->pushMessage( tr( "Export report" ), + tr( "Successfully exported report to %2" ).arg( QUrl::fromLocalFile( dir ).toString(), dir ), + QgsMessageBar::SUCCESS, 0 ); + break; + + case QgsLayoutExporter::IteratorError: + QMessageBox::warning( this, tr( "Report Export Error" ), + tr( "Error encountered while exporting report" ), + QMessageBox::Ok, + QMessageBox::Ok ); + break; + + case QgsLayoutExporter::PrintError: + case QgsLayoutExporter::SvgLayerError: + case QgsLayoutExporter::Canceled: + // no meaning for raster exports, will not be encountered + break; + + case QgsLayoutExporter::FileError: + QMessageBox::warning( this, tr( "Image Export Error" ), + error, + QMessageBox::Ok, + QMessageBox::Ok ); + break; + + case QgsLayoutExporter::MemoryError: + QMessageBox::warning( this, tr( "Memory Allocation Error" ), + tr( "Trying to create image of %2×%3 @ %4dpi " + "resulted in a memory overflow.\n\n" + "Please try a lower resolution or a smaller paper size." ) + .arg( imageSize.width() ).arg( imageSize.height() ).arg( settings.dpi ), + QMessageBox::Ok, QMessageBox::Ok ); + break; + } + mView->setPaintingEnabled( true ); +} + +void QgsLayoutDesignerDialog::exportReportToSvg() +{ + showSvgExportWarning(); + + QgsSettings settings; + QString lastUsedFile = settings.value( QStringLiteral( "UI/lastSaveAsSvgFile" ), QStringLiteral( "qgis.svg" ) ).toString(); + QFileInfo file( lastUsedFile ); + QString outputFileName = file.path() + '/' + QgsFileUtils::stringToSafeFilename( mMasterLayout->name() ) + QStringLiteral( ".svg" ); + + outputFileName = QFileDialog::getSaveFileName( + this, + tr( "Export to SVG" ), + outputFileName, + tr( "SVG Format" ) + " (*.svg *.SVG)" ); + this->activateWindow(); + if ( outputFileName.isEmpty() ) + { + return; + } + + if ( !outputFileName.endsWith( QLatin1String( ".svg" ), Qt::CaseInsensitive ) ) + { + outputFileName += QLatin1String( ".svg" ); + } +#ifdef Q_OS_MAC + QgisApp::instance()->activateWindow(); + this->raise(); +#endif + bool prevSettingLabelsAsOutlines = mMasterLayout->layoutProject()->readBoolEntry( QStringLiteral( "PAL" ), QStringLiteral( "/DrawOutlineLabels" ), true ); + settings.setValue( QStringLiteral( "UI/lastSaveAsSvgFile" ), outputFileName ); + + QgsLayoutExporter::SvgExportSettings svgSettings; + bool exportAsText = false; + if ( !getSvgExportSettings( svgSettings, exportAsText ) ) + return; + + //temporarily override label draw outlines setting + mMasterLayout->layoutProject()->writeEntry( QStringLiteral( "PAL" ), QStringLiteral( "/DrawOutlineLabels" ), exportAsText ); + + mView->setPaintingEnabled( false ); + QApplication::setOverrideCursor( Qt::BusyCursor ); + + QString error; + std::unique_ptr< QgsFeedback > feedback = qgis::make_unique< QgsFeedback >(); + std::unique_ptr< QProgressDialog > progressDialog = qgis::make_unique< QProgressDialog >( tr( "Rendering maps..." ), tr( "Abort" ), 0, 100, this ); + progressDialog->setWindowTitle( tr( "Exporting Report" ) ); + connect( feedback.get(), &QgsFeedback::progressChanged, this, [ & ]( double progress ) + { + progressDialog->setValue( progress ); + progressDialog->setLabelText( feedback->property( "progress" ).toString() ) ; + +#ifdef Q_OS_LINUX + // For some reason on Windows hasPendingEvents() always return true, + // but one iteration is actually enough on Windows to get good interactivity + // whereas on Linux we must allow for far more iterations. + // For safety limit the number of iterations + int nIters = 0; + while ( QCoreApplication::hasPendingEvents() && ++nIters < 100 ) +#endif + { + QCoreApplication::processEvents(); + } + + } ); + connect( progressDialog.get(), &QProgressDialog::canceled, this, [ & ] + { + feedback->cancel(); + } ); + + QFileInfo fi( outputFileName ); + QString outFile = fi.path() + '/' + fi.baseName(); + QString dir = fi.path(); + QgsLayoutExporter::ExportResult result = QgsLayoutExporter::exportToSvg( dynamic_cast< QgsReport * >( mMasterLayout ), outFile, svgSettings, error, feedback.get() ); + + QApplication::restoreOverrideCursor(); + switch ( result ) + { + case QgsLayoutExporter::Success: + { + mMessageBar->pushMessage( tr( "Export report" ), + tr( "Successfully exported report to %2" ).arg( QUrl::fromLocalFile( dir ).toString(), dir ), + QgsMessageBar::SUCCESS, 0 ); + break; + } + + case QgsLayoutExporter::FileError: + QMessageBox::warning( this, tr( "Export report" ), + error, QMessageBox::Ok, + QMessageBox::Ok ); + break; + + case QgsLayoutExporter::SvgLayerError: + QMessageBox::warning( this, tr( "Export report" ), + tr( "Cannot create layered SVG file." ), + QMessageBox::Ok, + QMessageBox::Ok ); + break; + + case QgsLayoutExporter::PrintError: + QMessageBox::warning( this, tr( "Export report" ), + tr( "Could not create print device." ), + QMessageBox::Ok, + QMessageBox::Ok ); + break; + + + case QgsLayoutExporter::MemoryError: + QMessageBox::warning( this, tr( "Memory Allocation Error" ), + tr( "Exporting the SVG " + "resulted in a memory overflow.\n\n" + "Please try a lower resolution or a smaller paper size." ), + QMessageBox::Ok, QMessageBox::Ok ); + break; + + case QgsLayoutExporter::IteratorError: + QMessageBox::warning( this, tr( "Report Export Error" ), + tr( "Error encountered while exporting report" ), + QMessageBox::Ok, + QMessageBox::Ok ); + break; + + case QgsLayoutExporter::Canceled: + // no meaning here + break; + } + + mView->setPaintingEnabled( true ); + mMasterLayout->layoutProject()->writeEntry( QStringLiteral( "PAL" ), QStringLiteral( "/DrawOutlineLabels" ), prevSettingLabelsAsOutlines ); +} + +void QgsLayoutDesignerDialog::exportReportToPdf() +{ + QgsSettings settings; + + QString lastUsedFile = settings.value( QStringLiteral( "UI/lastSaveAsPdfFile" ), QStringLiteral( "qgis.pdf" ) ).toString(); + QFileInfo file( lastUsedFile ); + + QString outputFileName = file.path() + '/' + QgsFileUtils::stringToSafeFilename( mMasterLayout->name() ) + QStringLiteral( ".pdf" ); + +#ifdef Q_OS_MAC + QgisApp::instance()->activateWindow(); + this->raise(); +#endif + outputFileName = QFileDialog::getSaveFileName( + this, + tr( "Export to PDF" ), + outputFileName, + tr( "PDF Format" ) + " (*.pdf *.PDF)" ); + this->activateWindow(); + if ( outputFileName.isEmpty() ) + { + return; + } + + if ( !outputFileName.endsWith( QLatin1String( ".pdf" ), Qt::CaseInsensitive ) ) + { + outputFileName += QLatin1String( ".pdf" ); + } + settings.setValue( QStringLiteral( "UI/lastSaveAsPdfFile" ), outputFileName ); + + mView->setPaintingEnabled( false ); + QApplication::setOverrideCursor( Qt::BusyCursor ); + + bool rasterize = false; + bool forceVectorOutput = false; + if ( mLayout ) + { + rasterize = mLayout->customProperty( QStringLiteral( "rasterize" ), false ).toBool(); + forceVectorOutput = mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool(); + } + QgsLayoutExporter::PdfExportSettings pdfSettings; + pdfSettings.rasterizeWholeImage = rasterize; + pdfSettings.forceVectorOutput = forceVectorOutput; + + QFileInfo fi( outputFileName ); + + QString error; + std::unique_ptr< QgsFeedback > feedback = qgis::make_unique< QgsFeedback >(); + std::unique_ptr< QProgressDialog > progressDialog = qgis::make_unique< QProgressDialog >( tr( "Rendering maps..." ), tr( "Abort" ), 0, 100, this ); + progressDialog->setWindowTitle( tr( "Exporting Report" ) ); + connect( feedback.get(), &QgsFeedback::progressChanged, this, [ & ]( double progress ) + { + progressDialog->setValue( progress ); + progressDialog->setLabelText( feedback->property( "progress" ).toString() ) ; + +#ifdef Q_OS_LINUX + // For some reason on Windows hasPendingEvents() always return true, + // but one iteration is actually enough on Windows to get good interactivity + // whereas on Linux we must allow for far more iterations. + // For safety limit the number of iterations + int nIters = 0; + while ( QCoreApplication::hasPendingEvents() && ++nIters < 100 ) +#endif + { + QCoreApplication::processEvents(); + } + + } ); + connect( progressDialog.get(), &QProgressDialog::canceled, this, [ & ] + { + feedback->cancel(); + } ); + + QgsLayoutExporter::ExportResult result = QgsLayoutExporter::exportToPdf( dynamic_cast< QgsReport * >( mMasterLayout ), outputFileName, pdfSettings, error, feedback.get() ); + + switch ( result ) + { + case QgsLayoutExporter::Success: + { + mMessageBar->pushMessage( tr( "Export report" ), + tr( "Successfully exported report to %2" ).arg( QUrl::fromLocalFile( fi.path() ).toString(), outputFileName ), + QgsMessageBar::SUCCESS, 0 ); + break; + } + + case QgsLayoutExporter::FileError: + QMessageBox::warning( this, tr( "Export report" ), + error, QMessageBox::Ok, + QMessageBox::Ok ); + break; + + case QgsLayoutExporter::SvgLayerError: + // no meaning + break; + + case QgsLayoutExporter::PrintError: + QMessageBox::warning( this, tr( "Export report" ), + tr( "Could not create print device." ), + QMessageBox::Ok, + QMessageBox::Ok ); + break; + + + case QgsLayoutExporter::MemoryError: + QMessageBox::warning( this, tr( "Memory Allocation Error" ), + tr( "Exporting the PDF " + "resulted in a memory overflow.\n\n" + "Please try a lower resolution or a smaller paper size." ), + QMessageBox::Ok, QMessageBox::Ok ); + break; + + case QgsLayoutExporter::IteratorError: + QMessageBox::warning( this, tr( "Report Export Error" ), + tr( "Error encountered while exporting report" ), + QMessageBox::Ok, + QMessageBox::Ok ); + break; + + case QgsLayoutExporter::Canceled: + // no meaning here + break; + } + + mView->setPaintingEnabled( true ); + QApplication::restoreOverrideCursor(); +} + +void QgsLayoutDesignerDialog::showReportSettings() +{ + if ( !mReportDock ) + return; + + if ( !mReportDock->isVisible() ) + { + mReportDock->show(); + } + + mReportDock->raise(); +} + void QgsLayoutDesignerDialog::paste() { QPointF pt = mView->mapFromGlobal( QCursor::pos() ); @@ -2727,6 +3104,8 @@ void QgsLayoutDesignerDialog::createReportWidget() reportWidget->setMessageBar( mMessageBar ); mReportDock->setWidget( reportWidget ); + mReportToolbar->show(); + mPanelsMenu->addAction( mReportDock->toggleViewAction() ); } @@ -2893,25 +3272,39 @@ bool QgsLayoutDesignerDialog::showFileSizeWarning() bool QgsLayoutDesignerDialog::getRasterExportSettings( QgsLayoutExporter::ImageExportSettings &settings, QSize &imageSize ) { + QSizeF maxPageSize; + bool hasUniformPageSizes = false; + double dpi = 300; + bool cropToContents = false; + int marginTop = 0; + int marginRight = 0; + int marginBottom = 0; + int marginLeft = 0; + bool antialias = true; + // Image size - QSizeF maxPageSize = mLayout->pageCollection()->maximumPageSize(); - bool hasUniformPageSizes = mLayout->pageCollection()->hasUniformPageSizes(); - double dpi = mLayout->renderContext().dpi(); + if ( mLayout ) + { + maxPageSize = mLayout->pageCollection()->maximumPageSize(); + hasUniformPageSizes = mLayout->pageCollection()->hasUniformPageSizes(); + dpi = mLayout->renderContext().dpi(); - //get some defaults from the composition - bool cropToContents = mLayout->customProperty( QStringLiteral( "imageCropToContents" ), false ).toBool(); - int marginTop = mLayout->customProperty( QStringLiteral( "imageCropMarginTop" ), 0 ).toInt(); - int marginRight = mLayout->customProperty( QStringLiteral( "imageCropMarginRight" ), 0 ).toInt(); - int marginBottom = mLayout->customProperty( QStringLiteral( "imageCropMarginBottom" ), 0 ).toInt(); - int marginLeft = mLayout->customProperty( QStringLiteral( "imageCropMarginLeft" ), 0 ).toInt(); - bool antialias = mLayout->customProperty( QStringLiteral( "imageAntialias" ), true ).toBool(); + //get some defaults from the composition + cropToContents = mLayout->customProperty( QStringLiteral( "imageCropToContents" ), false ).toBool(); + marginTop = mLayout->customProperty( QStringLiteral( "imageCropMarginTop" ), 0 ).toInt(); + marginRight = mLayout->customProperty( QStringLiteral( "imageCropMarginRight" ), 0 ).toInt(); + marginBottom = mLayout->customProperty( QStringLiteral( "imageCropMarginBottom" ), 0 ).toInt(); + marginLeft = mLayout->customProperty( QStringLiteral( "imageCropMarginLeft" ), 0 ).toInt(); + antialias = mLayout->customProperty( QStringLiteral( "imageAntialias" ), true ).toBool(); + } QgsLayoutImageExportOptionsDialog imageDlg( this ); imageDlg.setImageSize( maxPageSize ); imageDlg.setResolution( dpi ); imageDlg.setCropToContents( cropToContents ); imageDlg.setCropMargins( marginTop, marginRight, marginBottom, marginLeft ); - imageDlg.setGenerateWorldFile( mLayout->customProperty( QStringLiteral( "exportWorldFile" ), false ).toBool() ); + if ( mLayout ) + imageDlg.setGenerateWorldFile( mLayout->customProperty( QStringLiteral( "exportWorldFile" ), false ).toBool() ); imageDlg.setAntialiasing( antialias ); if ( !imageDlg.exec() ) @@ -2920,13 +3313,15 @@ bool QgsLayoutDesignerDialog::getRasterExportSettings( QgsLayoutExporter::ImageE imageSize = QSize( imageDlg.imageWidth(), imageDlg.imageHeight() ); cropToContents = imageDlg.cropToContents(); imageDlg.getCropMargins( marginTop, marginRight, marginBottom, marginLeft ); - mLayout->setCustomProperty( QStringLiteral( "imageCropToContents" ), cropToContents ); - mLayout->setCustomProperty( QStringLiteral( "imageCropMarginTop" ), marginTop ); - mLayout->setCustomProperty( QStringLiteral( "imageCropMarginRight" ), marginRight ); - mLayout->setCustomProperty( QStringLiteral( "imageCropMarginBottom" ), marginBottom ); - mLayout->setCustomProperty( QStringLiteral( "imageCropMarginLeft" ), marginLeft ); - - mLayout->setCustomProperty( QStringLiteral( "imageAntialias" ), imageDlg.antialiasing() ); + if ( mLayout ) + { + mLayout->setCustomProperty( QStringLiteral( "imageCropToContents" ), cropToContents ); + mLayout->setCustomProperty( QStringLiteral( "imageCropMarginTop" ), marginTop ); + mLayout->setCustomProperty( QStringLiteral( "imageCropMarginRight" ), marginRight ); + mLayout->setCustomProperty( QStringLiteral( "imageCropMarginBottom" ), marginBottom ); + mLayout->setCustomProperty( QStringLiteral( "imageCropMarginLeft" ), marginLeft ); + mLayout->setCustomProperty( QStringLiteral( "imageAntialias" ), imageDlg.antialiasing() ); + } settings.cropToContents = cropToContents; settings.cropMargins = QgsMargins( marginLeft, marginTop, marginRight, marginBottom ); @@ -2946,26 +3341,42 @@ bool QgsLayoutDesignerDialog::getRasterExportSettings( QgsLayoutExporter::ImageE bool QgsLayoutDesignerDialog::getSvgExportSettings( QgsLayoutExporter::SvgExportSettings &settings, bool &exportAsText ) { bool groupLayers = false; - bool prevSettingLabelsAsOutlines = mLayout->project()->readBoolEntry( QStringLiteral( "PAL" ), QStringLiteral( "/DrawOutlineLabels" ), true ); + bool prevSettingLabelsAsOutlines = mMasterLayout->layoutProject()->readBoolEntry( QStringLiteral( "PAL" ), QStringLiteral( "/DrawOutlineLabels" ), true ); bool clipToContent = false; double marginTop = 0.0; double marginRight = 0.0; double marginBottom = 0.0; double marginLeft = 0.0; - bool previousForceVector = mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool(); + bool previousForceVector = false; + bool layersAsGroup = false; + bool cropToContents = false; + double topMargin = 0.0; + double rightMargin = 0.0; + double bottomMargin = 0.0; + double leftMargin = 0.0; + if ( mLayout ) + { + mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool(); + layersAsGroup = mLayout->customProperty( QStringLiteral( "svgGroupLayers" ), false ).toBool(); + cropToContents = mLayout->customProperty( QStringLiteral( "svgCropToContents" ), false ).toBool(); + topMargin = mLayout->customProperty( QStringLiteral( "svgCropMarginTop" ), 0 ).toInt(); + rightMargin = mLayout->customProperty( QStringLiteral( "svgCropMarginRight" ), 0 ).toInt(); + bottomMargin = mLayout->customProperty( QStringLiteral( "svgCropMarginBottom" ), 0 ).toInt(); + leftMargin = mLayout->customProperty( QStringLiteral( "svgCropMarginLeft" ), 0 ).toInt(); + } // open options dialog QDialog dialog; Ui::QgsSvgExportOptionsDialog options; options.setupUi( &dialog ); options.chkTextAsOutline->setChecked( prevSettingLabelsAsOutlines ); - options.chkMapLayersAsGroup->setChecked( mLayout->customProperty( QStringLiteral( "svgGroupLayers" ), false ).toBool() ); - options.mClipToContentGroupBox->setChecked( mLayout->customProperty( QStringLiteral( "svgCropToContents" ), false ).toBool() ); + options.chkMapLayersAsGroup->setChecked( layersAsGroup ); + options.mClipToContentGroupBox->setChecked( cropToContents ); options.mForceVectorCheckBox->setChecked( previousForceVector ); - options.mTopMarginSpinBox->setValue( mLayout->customProperty( QStringLiteral( "svgCropMarginTop" ), 0 ).toInt() ); - options.mRightMarginSpinBox->setValue( mLayout->customProperty( QStringLiteral( "svgCropMarginRight" ), 0 ).toInt() ); - options.mBottomMarginSpinBox->setValue( mLayout->customProperty( QStringLiteral( "svgCropMarginBottom" ), 0 ).toInt() ); - options.mLeftMarginSpinBox->setValue( mLayout->customProperty( QStringLiteral( "svgCropMarginLeft" ), 0 ).toInt() ); + options.mTopMarginSpinBox->setValue( topMargin ); + options.mRightMarginSpinBox->setValue( rightMargin ); + options.mBottomMarginSpinBox->setValue( bottomMargin ); + options.mLeftMarginSpinBox->setValue( leftMargin ); if ( dialog.exec() != QDialog::Accepted ) return false; @@ -2977,15 +3388,17 @@ bool QgsLayoutDesignerDialog::getSvgExportSettings( QgsLayoutExporter::SvgExport marginBottom = options.mBottomMarginSpinBox->value(); marginLeft = options.mLeftMarginSpinBox->value(); - //save dialog settings - mLayout->setCustomProperty( QStringLiteral( "svgGroupLayers" ), groupLayers ); - mLayout->setCustomProperty( QStringLiteral( "svgCropToContents" ), clipToContent ); - mLayout->setCustomProperty( QStringLiteral( "svgCropMarginTop" ), marginTop ); - mLayout->setCustomProperty( QStringLiteral( "svgCropMarginRight" ), marginRight ); - mLayout->setCustomProperty( QStringLiteral( "svgCropMarginBottom" ), marginBottom ); - mLayout->setCustomProperty( QStringLiteral( "svgCropMarginLeft" ), marginLeft ); + if ( mLayout ) + { + //save dialog settings + mLayout->setCustomProperty( QStringLiteral( "svgGroupLayers" ), groupLayers ); + mLayout->setCustomProperty( QStringLiteral( "svgCropToContents" ), clipToContent ); + mLayout->setCustomProperty( QStringLiteral( "svgCropMarginTop" ), marginTop ); + mLayout->setCustomProperty( QStringLiteral( "svgCropMarginRight" ), marginRight ); + mLayout->setCustomProperty( QStringLiteral( "svgCropMarginBottom" ), marginBottom ); + mLayout->setCustomProperty( QStringLiteral( "svgCropMarginLeft" ), marginLeft ); + } - settings.forceVectorOutput = mLayout->customProperty( QStringLiteral( "forceVector" ), false ).toBool(); settings.cropToContents = clipToContent; settings.cropMargins = QgsMargins( marginLeft, marginTop, marginRight, marginBottom ); settings.forceVectorOutput = options.mForceVectorCheckBox->isChecked(); diff --git a/src/app/layout/qgslayoutdesignerdialog.h b/src/app/layout/qgslayoutdesignerdialog.h index 7a651b00cf59..8cab9516016c 100644 --- a/src/app/layout/qgslayoutdesignerdialog.h +++ b/src/app/layout/qgslayoutdesignerdialog.h @@ -314,6 +314,10 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner void exportAtlasToSvg(); void exportAtlasToPdf(); + void exportReportToRaster(); + void exportReportToSvg(); + void exportReportToPdf(); + void showReportSettings(); private: static bool sInitializedRegistry; diff --git a/src/ui/layout/qgslayoutdesignerbase.ui b/src/ui/layout/qgslayoutdesignerbase.ui index 6745cb5a2559..c4d129eb8c21 100644 --- a/src/ui/layout/qgslayoutdesignerbase.ui +++ b/src/ui/layout/qgslayoutdesignerbase.ui @@ -6,7 +6,7 @@ 0 0 - 1083 + 1484 609 @@ -97,7 +97,7 @@ 0 0 - 1083 + 1484 42 @@ -269,12 +269,23 @@ + + + Report + + + + + + + + @@ -325,6 +336,21 @@ + + + toolBar + + + TopToolBarArea + + + false + + + + + + &Close @@ -1361,6 +1387,54 @@ Ctrl+Alt+/ + + + + :/images/themes/default/mActionSaveMapAsImage.svg:/images/themes/default/mActionSaveMapAsImage.svg + + + Export Report as &Images... + + + Export Report as Images + + + + + + :/images/themes/default/mActionSaveAsSVG.svg:/images/themes/default/mActionSaveAsSVG.svg + + + Export Report as S&VG... + + + Export Report as SVG + + + + + + :/images/themes/default/mActionSaveAsPDF.svg:/images/themes/default/mActionSaveAsPDF.svg + + + &Export Report as PDF... + + + Export Report as PDF + + + + + + :/images/themes/default/mActionAtlasSettings.svg:/images/themes/default/mActionAtlasSettings.svg + + + Report &Settings + + + Report Settings + + From 8eb6aa9281f3d12ef8e96c3707101ca76a01e51a Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 30 Dec 2017 20:19:26 +1000 Subject: [PATCH 075/105] More UI work on reports --- src/app/CMakeLists.txt | 4 + .../qgsreportfieldgroupsectionwidget.cpp | 77 ++++++++++ .../layout/qgsreportfieldgroupsectionwidget.h | 45 ++++++ .../layout/qgsreportlayoutsectionwidget.cpp | 42 +++++ src/app/layout/qgsreportlayoutsectionwidget.h | 42 +++++ src/app/layout/qgsreportorganizerwidget.cpp | 27 +++- src/app/layout/qgsreportorganizerwidget.h | 1 + src/ui/layout/qgsreportorganizerwidgetbase.ui | 144 ++++++++++++------ .../qgsreportwidgetfieldgroupsectionbase.ui | 106 +++++++++++++ .../qgsreportwidgetlayoutsectionbase.ui | 65 ++++++++ 10 files changed, 509 insertions(+), 44 deletions(-) create mode 100644 src/app/layout/qgsreportfieldgroupsectionwidget.cpp create mode 100644 src/app/layout/qgsreportfieldgroupsectionwidget.h create mode 100644 src/app/layout/qgsreportlayoutsectionwidget.cpp create mode 100644 src/app/layout/qgsreportlayoutsectionwidget.h create mode 100644 src/ui/layout/qgsreportwidgetfieldgroupsectionbase.ui create mode 100644 src/ui/layout/qgsreportwidgetlayoutsectionbase.ui diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 852457603ced..a59758086e9a 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -204,6 +204,8 @@ SET(QGIS_APP_SRCS layout/qgslayoutscalebarwidget.cpp layout/qgslayoutshapewidget.cpp layout/qgslayouttablebackgroundcolorsdialog.cpp + layout/qgsreportfieldgroupsectionwidget.cpp + layout/qgsreportlayoutsectionwidget.cpp layout/qgsreportorganizerwidget.cpp layout/qgsreportsectionmodel.cpp @@ -425,6 +427,8 @@ SET (QGIS_APP_MOC_HDRS layout/qgslayoutscalebarwidget.h layout/qgslayoutshapewidget.h layout/qgslayouttablebackgroundcolorsdialog.h + layout/qgsreportfieldgroupsectionwidget.h + layout/qgsreportlayoutsectionwidget.h layout/qgsreportorganizerwidget.h layout/qgsreportsectionmodel.h diff --git a/src/app/layout/qgsreportfieldgroupsectionwidget.cpp b/src/app/layout/qgsreportfieldgroupsectionwidget.cpp new file mode 100644 index 000000000000..64e055564769 --- /dev/null +++ b/src/app/layout/qgsreportfieldgroupsectionwidget.cpp @@ -0,0 +1,77 @@ +/*************************************************************************** + qgsreportfieldgroupsectionwidget.cpp + ------------------------ + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsreportfieldgroupsectionwidget.h" +#include "qgsreportsectionfieldgroup.h" +#include "qgslayout.h" +#include "qgslayoutdesignerdialog.h" + +QgsReportSectionFieldGroupWidget::QgsReportSectionFieldGroupWidget( QWidget *parent, QgsLayoutDesignerDialog *designer, QgsReportSectionFieldGroup *section ) + : QWidget( parent ) + , mSection( section ) + , mDesigner( designer ) +{ + setupUi( this ); + + mLayerComboBox->setFilters( QgsMapLayerProxyModel::VectorLayer ); + connect( mLayerComboBox, &QgsMapLayerComboBox::layerChanged, mFieldComboBox, &QgsFieldComboBox::setLayer ); + connect( mButtonEditBody, &QPushButton::clicked, this, &QgsReportSectionFieldGroupWidget::editBody ); + + mLayerComboBox->setLayer( section->layer() ); + mFieldComboBox->setField( section->field() ); + mSortAscendingCheckBox->setChecked( section->sortAscending() ); + + connect( mSortAscendingCheckBox, &QCheckBox::toggled, this, &QgsReportSectionFieldGroupWidget::sortAscendingToggled ); + connect( mLayerComboBox, &QgsMapLayerComboBox::layerChanged, this, &QgsReportSectionFieldGroupWidget::setLayer ); + connect( mFieldComboBox, &QgsFieldComboBox::fieldChanged, this, &QgsReportSectionFieldGroupWidget::setField ); +} + +void QgsReportSectionFieldGroupWidget::editBody() +{ + if ( !mSection->body() ) + { + std::unique_ptr< QgsLayout > body = qgis::make_unique< QgsLayout >( mSection->project() ); + body->initializeDefaults(); + mSection->setBody( body.release() ); + } + + if ( mSection->body() ) + { + mSection->body()->reportContext().setLayer( mSection->layer() ); + mDesigner->setCurrentLayout( mSection->body() ); + } +} + +void QgsReportSectionFieldGroupWidget::sortAscendingToggled( bool checked ) +{ + mSection->setSortAscending( checked ); +} + +void QgsReportSectionFieldGroupWidget::setLayer( QgsMapLayer *layer ) +{ + QgsVectorLayer *vl = qobject_cast< QgsVectorLayer * >( layer ); + if ( !vl ) + return; + + mSection->setLayer( vl ); + if ( mSection->body() ) + mSection->body()->reportContext().setLayer( mSection->layer() ); +} + +void QgsReportSectionFieldGroupWidget::setField( const QString &field ) +{ + mSection->setField( field ); +} diff --git a/src/app/layout/qgsreportfieldgroupsectionwidget.h b/src/app/layout/qgsreportfieldgroupsectionwidget.h new file mode 100644 index 000000000000..e75c3f28f3d2 --- /dev/null +++ b/src/app/layout/qgsreportfieldgroupsectionwidget.h @@ -0,0 +1,45 @@ +/*************************************************************************** + qgsreportfieldgroupsectionwidget.h + ---------------------- + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSREPORTFIELDGROUPSECTIONWIDGET_H +#define QGSREPORTFIELDGROUPSECTIONWIDGET_H + +#include "ui_qgsreportwidgetfieldgroupsectionbase.h" + +class QgsLayoutDesignerDialog; +class QgsReportSectionFieldGroup; + +class QgsReportSectionFieldGroupWidget: public QWidget, private Ui::QgsReportWidgetFieldGroupSectionBase +{ + Q_OBJECT + public: + QgsReportSectionFieldGroupWidget( QWidget *parent, QgsLayoutDesignerDialog *designer, QgsReportSectionFieldGroup *section ); + + private slots: + + void editBody(); + void sortAscendingToggled( bool checked ); + void setLayer( QgsMapLayer *layer ); + void setField( const QString &field ); + + private: + + QgsReportSectionFieldGroup *mSection = nullptr; + QgsLayoutDesignerDialog *mDesigner = nullptr; + +}; + +#endif // QGSREPORTFIELDGROUPSECTIONWIDGET_H diff --git a/src/app/layout/qgsreportlayoutsectionwidget.cpp b/src/app/layout/qgsreportlayoutsectionwidget.cpp new file mode 100644 index 000000000000..946e55b26e9c --- /dev/null +++ b/src/app/layout/qgsreportlayoutsectionwidget.cpp @@ -0,0 +1,42 @@ +/*************************************************************************** + qgsreportlayoutsectionwidget.cpp + ------------------------ + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsreportlayoutsectionwidget.h" +#include "qgsreportsectionlayout.h" +#include "qgslayout.h" +#include "qgslayoutdesignerdialog.h" + +QgsReportLayoutSectionWidget::QgsReportLayoutSectionWidget( QWidget *parent, QgsLayoutDesignerDialog *designer, QgsReportSectionLayout *section ) + : QWidget( parent ) + , mSection( section ) + , mDesigner( designer ) +{ + setupUi( this ); + + connect( mButtonEditBody, &QPushButton::clicked, this, &QgsReportLayoutSectionWidget::editBody ); +} + +void QgsReportLayoutSectionWidget::editBody() +{ + if ( !mSection->body() ) + { + std::unique_ptr< QgsLayout > body = qgis::make_unique< QgsLayout >( mSection->project() ); + body->initializeDefaults(); + mSection->setBody( body.release() ); + } + + mDesigner->setCurrentLayout( mSection->body() ); +} diff --git a/src/app/layout/qgsreportlayoutsectionwidget.h b/src/app/layout/qgsreportlayoutsectionwidget.h new file mode 100644 index 000000000000..d1d9c40cafda --- /dev/null +++ b/src/app/layout/qgsreportlayoutsectionwidget.h @@ -0,0 +1,42 @@ +/*************************************************************************** + qgsreportlayoutsectionwidget.h + ---------------------- + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSREPORTLAYOUTSECTIONWIDGET_H +#define QGSREPORTLAYOUTSECTIONWIDGET_H + +#include "ui_qgsreportwidgetlayoutsectionbase.h" + +class QgsLayoutDesignerDialog; +class QgsReportSectionLayout; + +class QgsReportLayoutSectionWidget: public QWidget, private Ui::QgsReportWidgetLayoutSectionBase +{ + Q_OBJECT + public: + QgsReportLayoutSectionWidget( QWidget *parent, QgsLayoutDesignerDialog *designer, QgsReportSectionLayout *section ); + + private slots: + + void editBody(); + + private: + + QgsReportSectionLayout *mSection = nullptr; + QgsLayoutDesignerDialog *mDesigner = nullptr; + +}; + +#endif // QGSREPORTLAYOUTSECTIONWIDGET_H diff --git a/src/app/layout/qgsreportorganizerwidget.cpp b/src/app/layout/qgsreportorganizerwidget.cpp index 48bc3ff3ddfe..c399f2ac5f8d 100644 --- a/src/app/layout/qgsreportorganizerwidget.cpp +++ b/src/app/layout/qgsreportorganizerwidget.cpp @@ -21,6 +21,8 @@ #include "qgsreportsectionfieldgroup.h" #include "qgslayout.h" #include "qgslayoutdesignerdialog.h" +#include "qgsreportlayoutsectionwidget.h" +#include "qgsreportfieldgroupsectionwidget.h" #include #include @@ -40,9 +42,14 @@ QgsReportOrganizerWidget::QgsReportOrganizerWidget( QWidget *parent, QgsLayoutDe mViewSections->setModel( mSectionModel ); #ifdef ENABLE_MODELTEST - //new ModelTest( mSectionModel, this ); + new ModelTest( mSectionModel, this ); #endif + QVBoxLayout *vLayout = new QVBoxLayout(); + vLayout->setMargin( 0 ); + vLayout->setSpacing( 0 ); + mSettingsFrame->setLayout( vLayout ); + mViewSections->setEditTriggers( QAbstractItemView::AllEditTriggers ); QMenu *addMenu = new QMenu( mButtonAddSection ); @@ -151,4 +158,22 @@ void QgsReportOrganizerWidget::selectionChanged( const QModelIndex ¤t, con whileBlocking( mCheckShowHeader )->setChecked( parent->headerEnabled() ); whileBlocking( mCheckShowFooter )->setChecked( parent->footerEnabled() ); + + delete mConfigWidget; + if ( QgsReportSectionLayout *section = dynamic_cast< QgsReportSectionLayout * >( parent ) ) + { + QgsReportLayoutSectionWidget *widget = new QgsReportLayoutSectionWidget( this, mDesigner, section ); + mSettingsFrame->layout()->addWidget( widget ); + mConfigWidget = widget; + } + else if ( QgsReportSectionFieldGroup *section = dynamic_cast< QgsReportSectionFieldGroup * >( parent ) ) + { + QgsReportSectionFieldGroupWidget *widget = new QgsReportSectionFieldGroupWidget( this, mDesigner, section ); + mSettingsFrame->layout()->addWidget( widget ); + mConfigWidget = widget; + } + else + { + mConfigWidget = nullptr; + } } diff --git a/src/app/layout/qgsreportorganizerwidget.h b/src/app/layout/qgsreportorganizerwidget.h index 28b1ec29a513..94788bfd41df 100644 --- a/src/app/layout/qgsreportorganizerwidget.h +++ b/src/app/layout/qgsreportorganizerwidget.h @@ -51,6 +51,7 @@ class QgsReportOrganizerWidget: public QgsPanelWidget, private Ui::QgsReportOrga QgsReportSectionModel *mSectionModel = nullptr; QgsMessageBar *mMessageBar; QgsLayoutDesignerDialog *mDesigner = nullptr; + QWidget *mConfigWidget = nullptr; }; diff --git a/src/ui/layout/qgsreportorganizerwidgetbase.ui b/src/ui/layout/qgsreportorganizerwidgetbase.ui index 481f8cc5c415..8fcf04b4c485 100644 --- a/src/ui/layout/qgsreportorganizerwidgetbase.ui +++ b/src/ui/layout/qgsreportorganizerwidgetbase.ui @@ -97,54 +97,112 @@ - - - - - Edit - - - - - - - Show header - - - - - - - Edit - - - - - - - Show footer - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - + + + QFrame::NoFrame + + + true + + + + + 0 + 0 + 687 + 207 + + + + + + + + + Edit + + + + + + + Show header + + + + + + + Edit + + + + + + + Show footer + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ui/layout/qgsreportwidgetfieldgroupsectionbase.ui b/src/ui/layout/qgsreportwidgetfieldgroupsectionbase.ui new file mode 100644 index 000000000000..0f92ba0798a4 --- /dev/null +++ b/src/ui/layout/qgsreportwidgetfieldgroupsectionbase.ui @@ -0,0 +1,106 @@ + + + QgsReportWidgetFieldGroupSectionBase + + + + 0 + 0 + 705 + 231 + + + + Layout Manager + + + + + + + + Section body + + + + + + + Edit + + + + + + + Layer + + + + + + + + + + Field + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Sort ascending + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + QgsMapLayerComboBox + QComboBox +

qgsmaplayercombobox.h
+
+ + QgsFieldComboBox + QComboBox +
qgsfieldcombobox.h
+
+
+ + + + + diff --git a/src/ui/layout/qgsreportwidgetlayoutsectionbase.ui b/src/ui/layout/qgsreportwidgetlayoutsectionbase.ui new file mode 100644 index 000000000000..292d1080f3fd --- /dev/null +++ b/src/ui/layout/qgsreportwidgetlayoutsectionbase.ui @@ -0,0 +1,65 @@ + + + QgsReportWidgetLayoutSectionBase + + + + 0 + 0 + 723 + 89 + + + + Layout Manager + + + + + + + + Section body + + + + + + + Edit + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + From 17292c12e9f7d3b52f658985712feed9d7c2fc49 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 30 Dec 2017 20:24:36 +1000 Subject: [PATCH 076/105] Fix progress bar with report export --- src/app/layout/qgslayoutdesignerdialog.cpp | 18 +++++++++--------- src/core/layout/qgslayoutexporter.cpp | 21 +++++++++++++++++---- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 74045e5b3b2f..304bd5729041 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -2631,11 +2631,11 @@ void QgsLayoutDesignerDialog::exportReportToRaster() QString error; std::unique_ptr< QgsFeedback > feedback = qgis::make_unique< QgsFeedback >(); - std::unique_ptr< QProgressDialog > progressDialog = qgis::make_unique< QProgressDialog >( tr( "Rendering report..." ), tr( "Abort" ), 0, 100, this ); + std::unique_ptr< QProgressDialog > progressDialog = qgis::make_unique< QProgressDialog >( tr( "Rendering report..." ), tr( "Abort" ), 0, 0, this ); progressDialog->setWindowTitle( tr( "Exporting Report" ) ); - connect( feedback.get(), &QgsFeedback::progressChanged, this, [ & ]( double progress ) + connect( feedback.get(), &QgsFeedback::progressChanged, this, [ & ]( double ) { - progressDialog->setValue( progress ); + //progressDialog->setValue( progress ); progressDialog->setLabelText( feedback->property( "progress" ).toString() ) ; #ifdef Q_OS_LINUX @@ -2746,11 +2746,11 @@ void QgsLayoutDesignerDialog::exportReportToSvg() QString error; std::unique_ptr< QgsFeedback > feedback = qgis::make_unique< QgsFeedback >(); - std::unique_ptr< QProgressDialog > progressDialog = qgis::make_unique< QProgressDialog >( tr( "Rendering maps..." ), tr( "Abort" ), 0, 100, this ); + std::unique_ptr< QProgressDialog > progressDialog = qgis::make_unique< QProgressDialog >( tr( "Rendering maps..." ), tr( "Abort" ), 0, 0, this ); progressDialog->setWindowTitle( tr( "Exporting Report" ) ); - connect( feedback.get(), &QgsFeedback::progressChanged, this, [ & ]( double progress ) + connect( feedback.get(), &QgsFeedback::progressChanged, this, [ & ]( double ) { - progressDialog->setValue( progress ); + //progressDialog->setValue( progress ); progressDialog->setLabelText( feedback->property( "progress" ).toString() ) ; #ifdef Q_OS_LINUX @@ -2880,11 +2880,11 @@ void QgsLayoutDesignerDialog::exportReportToPdf() QString error; std::unique_ptr< QgsFeedback > feedback = qgis::make_unique< QgsFeedback >(); - std::unique_ptr< QProgressDialog > progressDialog = qgis::make_unique< QProgressDialog >( tr( "Rendering maps..." ), tr( "Abort" ), 0, 100, this ); + std::unique_ptr< QProgressDialog > progressDialog = qgis::make_unique< QProgressDialog >( tr( "Rendering maps..." ), tr( "Abort" ), 0, 0, this ); progressDialog->setWindowTitle( tr( "Exporting Report" ) ); - connect( feedback.get(), &QgsFeedback::progressChanged, this, [ & ]( double progress ) + connect( feedback.get(), &QgsFeedback::progressChanged, this, [ & ]( double ) { - progressDialog->setValue( progress ); + //progressDialog->setValue( progress ); progressDialog->setLabelText( feedback->property( "progress" ).toString() ) ; #ifdef Q_OS_LINUX diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index eeeece0350a3..b7ba79deeefe 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -410,7 +410,10 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToImage( QgsAbstractLay { if ( feedback ) { - feedback->setProperty( "progress", QObject::tr( "Exporting %1 of %2" ).arg( i + 1 ).arg( total ) ); + if ( total > 0 ) + feedback->setProperty( "progress", QObject::tr( "Exporting %1 of %2" ).arg( i + 1 ).arg( total ) ); + else + feedback->setProperty( "progress", QObject::tr( "Exporting section %1" ).arg( i + 1 ).arg( total ) ); feedback->setProgress( step * i ); } if ( feedback && feedback->isCanceled() ) @@ -505,7 +508,10 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdf( QgsAbstractLayou { if ( feedback ) { - feedback->setProperty( "progress", QObject::tr( "Exporting %1 of %2" ).arg( i + 1 ).arg( total ) ); + if ( total > 0 ) + feedback->setProperty( "progress", QObject::tr( "Exporting %1 of %2" ).arg( i + 1 ).arg( total ) ); + else + feedback->setProperty( "progress", QObject::tr( "Exporting section %1" ).arg( i + 1 ).arg( total ) ); feedback->setProgress( step * i ); } if ( feedback && feedback->isCanceled() ) @@ -579,7 +585,10 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdfs( QgsAbstractLayo { if ( feedback ) { - feedback->setProperty( "progress", QObject::tr( "Exporting %1 of %2" ).arg( i + 1 ).arg( total ) ); + if ( total > 0 ) + feedback->setProperty( "progress", QObject::tr( "Exporting %1 of %2" ).arg( i + 1 ).arg( total ) ); + else + feedback->setProperty( "progress", QObject::tr( "Exporting section %1" ).arg( i + 1 ).arg( total ) ); feedback->setProgress( step * i ); } if ( feedback && feedback->isCanceled() ) @@ -792,7 +801,11 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( QgsAbstractLayou { if ( feedback ) { - feedback->setProperty( "progress", QObject::tr( "Exporting %1 of %2" ).arg( i + 1 ).arg( total ) ); + if ( total > 0 ) + feedback->setProperty( "progress", QObject::tr( "Exporting %1 of %2" ).arg( i + 1 ).arg( total ) ); + else + feedback->setProperty( "progress", QObject::tr( "Exporting section %1" ).arg( i + 1 ).arg( total ) ); + feedback->setProgress( step * i ); } if ( feedback && feedback->isCanceled() ) From 57cac0100b485fe86c4f6df573e6aa09f4bae16f Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 30 Dec 2017 20:27:08 +1000 Subject: [PATCH 077/105] Expand report sections by default --- src/app/layout/qgsreportorganizerwidget.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/layout/qgsreportorganizerwidget.cpp b/src/app/layout/qgsreportorganizerwidget.cpp index c399f2ac5f8d..7570ae854373 100644 --- a/src/app/layout/qgsreportorganizerwidget.cpp +++ b/src/app/layout/qgsreportorganizerwidget.cpp @@ -40,6 +40,7 @@ QgsReportOrganizerWidget::QgsReportOrganizerWidget( QWidget *parent, QgsLayoutDe mSectionModel = new QgsReportSectionModel( mReport, mViewSections ); mViewSections->setModel( mSectionModel ); + mViewSections->expandAll(); #ifdef ENABLE_MODELTEST new ModelTest( mSectionModel, this ); From 69a225ade373dd42eaf325580b6c8ad30aa1e637 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sun, 31 Dec 2017 13:51:08 +1000 Subject: [PATCH 078/105] Fix missing Report top level item in organizer widget --- src/app/layout/qgsreportsectionmodel.cpp | 57 ++++++++++++++---------- src/app/layout/qgsreportsectionmodel.h | 1 + 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/app/layout/qgsreportsectionmodel.cpp b/src/app/layout/qgsreportsectionmodel.cpp index e59e89df01e0..72720163734c 100644 --- a/src/app/layout/qgsreportsectionmodel.cpp +++ b/src/app/layout/qgsreportsectionmodel.cpp @@ -96,16 +96,20 @@ QVariant QgsReportSectionModel::headerData( int section, Qt::Orientation orienta int QgsReportSectionModel::rowCount( const QModelIndex &parent ) const { - QgsAbstractReportSection *parentSection = nullptr; - if ( parent.column() > 0 ) - return 0; + if ( !parent.isValid() ) + return 1; // report + QgsAbstractReportSection *parentSection = sectionForIndex( parent ); + return parentSection ? parentSection->childCount() : 0; +} + +bool QgsReportSectionModel::hasChildren( const QModelIndex &parent ) const +{ if ( !parent.isValid() ) - parentSection = mReport; - else - parentSection = sectionForIndex( parent ); + return true; // root item: its children are top level items - return parentSection->childCount(); + QgsAbstractReportSection *parentSection = sectionForIndex( parent ); + return parentSection && parentSection->childCount() > 0; } int QgsReportSectionModel::columnCount( const QModelIndex & ) const @@ -118,32 +122,33 @@ QModelIndex QgsReportSectionModel::index( int row, int column, const QModelIndex if ( !hasIndex( row, column, parent ) ) return QModelIndex(); - QgsAbstractReportSection *parentSection = nullptr; - - if ( !parent.isValid() ) - parentSection = mReport; - else - parentSection = sectionForIndex( parent ); - - QgsAbstractReportSection *childSection = parentSection->childSection( row ); - if ( childSection ) - return createIndex( row, column, childSection ); + QgsAbstractReportSection *parentSection = sectionForIndex( parent ); + if ( parentSection ) + { + QgsAbstractReportSection *item = parentSection->childSections().value( row, nullptr ); + return item ? createIndex( row, column, item ) : QModelIndex(); + } else - return QModelIndex(); + { + if ( row == 0 ) + return createIndex( row, column, nullptr ); + else + return QModelIndex(); + } } QModelIndex QgsReportSectionModel::parent( const QModelIndex &index ) const { - if ( !index.isValid() ) + QgsAbstractReportSection *childSection = sectionForIndex( index ); + if ( !childSection ) return QModelIndex(); - QgsAbstractReportSection *childSection = sectionForIndex( index ); QgsAbstractReportSection *parentSection = childSection->parentSection(); - if ( parentSection == mReport ) + if ( !parentSection ) return QModelIndex(); - - return createIndex( parentSection->row(), 0, parentSection ); + else + return createIndex( parentSection->row(), 0, parentSection != mReport ? parentSection : nullptr ); } bool QgsReportSectionModel::setData( const QModelIndex &index, const QVariant &value, int role ) @@ -173,6 +178,12 @@ bool QgsReportSectionModel::setData( const QModelIndex &index, const QVariant &v QgsAbstractReportSection *QgsReportSectionModel::sectionForIndex( const QModelIndex &index ) const { + if ( !index.isValid() ) + return nullptr; + + if ( !index.internalPointer() ) // top level item + return mReport; // IMPORTANT - QgsReport uses multiple inheritance, so cannot static cast the void*! + return static_cast( index.internalPointer() ); } diff --git a/src/app/layout/qgsreportsectionmodel.h b/src/app/layout/qgsreportsectionmodel.h index 09dfcc3d815c..2e25da5e2d20 100644 --- a/src/app/layout/qgsreportsectionmodel.h +++ b/src/app/layout/qgsreportsectionmodel.h @@ -42,6 +42,7 @@ class QgsReportSectionModel : public QAbstractItemModel QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const override; int rowCount( const QModelIndex &parent = QModelIndex() ) const override; + bool hasChildren( const QModelIndex &parent = QModelIndex() ) const override; int columnCount( const QModelIndex & = QModelIndex() ) const override; QModelIndex index( int row, int column, const QModelIndex &parent = QModelIndex() ) const override; From 43aff9b2e3a0b75d61f7e84b4a958566f276241d Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sun, 31 Dec 2017 14:42:11 +1000 Subject: [PATCH 079/105] Auto select new report sections after adding them --- src/app/layout/qgsreportorganizerwidget.cpp | 6 ++++++ src/app/layout/qgsreportsectionmodel.cpp | 24 +++++++++++++++++++++ src/app/layout/qgsreportsectionmodel.h | 2 ++ 3 files changed, 32 insertions(+) diff --git a/src/app/layout/qgsreportorganizerwidget.cpp b/src/app/layout/qgsreportorganizerwidget.cpp index 7570ae854373..d800b1367805 100644 --- a/src/app/layout/qgsreportorganizerwidget.cpp +++ b/src/app/layout/qgsreportorganizerwidget.cpp @@ -79,13 +79,19 @@ void QgsReportOrganizerWidget::setMessageBar( QgsMessageBar *bar ) void QgsReportOrganizerWidget::addLayoutSection() { std::unique_ptr< QgsReportSectionLayout > section = qgis::make_unique< QgsReportSectionLayout >(); + QgsAbstractReportSection *newSection = section.get(); mSectionModel->addSection( mViewSections->currentIndex(), std::move( section ) ); + QModelIndex newIndex = mSectionModel->indexForSection( newSection ); + mViewSections->setCurrentIndex( newIndex ); } void QgsReportOrganizerWidget::addFieldGroupSection() { std::unique_ptr< QgsReportSectionFieldGroup > section = qgis::make_unique< QgsReportSectionFieldGroup >(); + QgsAbstractReportSection *newSection = section.get(); mSectionModel->addSection( mViewSections->currentIndex(), std::move( section ) ); + QModelIndex newIndex = mSectionModel->indexForSection( newSection ); + mViewSections->setCurrentIndex( newIndex ); } void QgsReportOrganizerWidget::removeSection() diff --git a/src/app/layout/qgsreportsectionmodel.cpp b/src/app/layout/qgsreportsectionmodel.cpp index 72720163734c..2e43efdd2b5c 100644 --- a/src/app/layout/qgsreportsectionmodel.cpp +++ b/src/app/layout/qgsreportsectionmodel.cpp @@ -14,6 +14,7 @@ ***************************************************************************/ #include "qgsreportsectionmodel.h" +#include "functional" #ifdef ENABLE_MODELTEST #include "modeltest.h" @@ -187,6 +188,29 @@ QgsAbstractReportSection *QgsReportSectionModel::sectionForIndex( const QModelIn return static_cast( index.internalPointer() ); } +QModelIndex QgsReportSectionModel::indexForSection( QgsAbstractReportSection *section ) const +{ + if ( !section ) + return QModelIndex(); + + std::function< QModelIndex( const QModelIndex &parent, QgsAbstractReportSection *section ) > findIndex = [&]( const QModelIndex & parent, QgsAbstractReportSection * section )->QModelIndex + { + for ( int row = 0; row < rowCount( parent ); ++row ) + { + QModelIndex current = index( row, 0, parent ); + if ( sectionForIndex( current ) == section ) + return current; + + QModelIndex checkChildren = findIndex( current, section ); + if ( checkChildren.isValid() ) + return checkChildren; + } + return QModelIndex(); + }; + + return findIndex( QModelIndex(), section ); +} + bool QgsReportSectionModel::removeRows( int row, int count, const QModelIndex &parent ) { QgsAbstractReportSection *parentSection = sectionForIndex( parent ); diff --git a/src/app/layout/qgsreportsectionmodel.h b/src/app/layout/qgsreportsectionmodel.h index 2e25da5e2d20..95fbe2677b74 100644 --- a/src/app/layout/qgsreportsectionmodel.h +++ b/src/app/layout/qgsreportsectionmodel.h @@ -57,6 +57,8 @@ class QgsReportSectionModel : public QAbstractItemModel */ QgsAbstractReportSection *sectionForIndex( const QModelIndex &index ) const; + QModelIndex indexForSection( QgsAbstractReportSection *section ) const; + private: QgsReport *mReport = nullptr; }; From ea4f61f024476e81f09d2cd23291afeb672f5f8c Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sun, 31 Dec 2017 14:53:14 +1000 Subject: [PATCH 080/105] Dox and spelling --- .../core/layout/qgsabstractlayoutiterator.sip | 11 +++-------- python/core/layout/qgslayoutatlas.sip | 17 +++++++++++++++++ python/core/layout/qgslayoutitemlegend.sip | 3 ++- src/core/layout/qgsabstractlayoutiterator.h | 6 ++++++ src/core/layout/qgslayoutatlas.h | 10 ++++++++++ src/core/layout/qgslayoutitemlegend.h | 2 +- tests/src/python/test_qgsreport.py | 2 +- 7 files changed, 40 insertions(+), 11 deletions(-) diff --git a/python/core/layout/qgsabstractlayoutiterator.sip b/python/core/layout/qgsabstractlayoutiterator.sip index 5a8115bfc760..0661858b21e4 100644 --- a/python/core/layout/qgsabstractlayoutiterator.sip +++ b/python/core/layout/qgsabstractlayoutiterator.sip @@ -11,14 +11,9 @@ class QgsAbstractLayoutIterator { %Docstring -************************************************************************* -* -This program is free software; you can redistribute it and/or modify * -it under the terms of the GNU General Public License as published by * -the Free Software Foundation; either version 2 of the License, or * -(at your option) any later version. * -* -************************************************************************** + An abstract base class for QgsLayout based classes which can be exported by QgsLayoutExporter. + +.. versionadded:: 3.0 %End %TypeHeaderCode diff --git a/python/core/layout/qgslayoutatlas.sip b/python/core/layout/qgslayoutatlas.sip index 2f3248e02bee..821cfd47816b 100644 --- a/python/core/layout/qgslayoutatlas.sip +++ b/python/core/layout/qgslayoutatlas.sip @@ -292,6 +292,8 @@ Iterates to the previous feature, returning false if no previous feature exists. .. seealso:: :py:func:`last()` .. seealso:: :py:func:`first()` + +.. seealso:: :py:func:`seekTo()` %End bool last(); @@ -303,6 +305,8 @@ Seeks to the last feature, returning false if no feature was found. .. seealso:: :py:func:`previous()` .. seealso:: :py:func:`first()` + +.. seealso:: :py:func:`seekTo()` %End bool first(); @@ -314,9 +318,22 @@ Seeks to the first feature, returning false if no feature was found. .. seealso:: :py:func:`previous()` .. seealso:: :py:func:`last()` + +.. seealso:: :py:func:`seekTo()` %End bool seekTo( int feature ); +%Docstring +Seeks to the specified ``feature`` number. + +.. seealso:: :py:func:`first()` + +.. seealso:: :py:func:`previous()` + +.. seealso:: :py:func:`next()` + +.. seealso:: :py:func:`last()` +%End void refreshCurrentFeature(); %Docstring diff --git a/python/core/layout/qgslayoutitemlegend.sip b/python/core/layout/qgslayoutitemlegend.sip index f07428abd5f5..b55ac052e6df 100644 --- a/python/core/layout/qgslayoutitemlegend.sip +++ b/python/core/layout/qgslayoutitemlegend.sip @@ -490,7 +490,8 @@ Returns the legend's renderer settings object. public slots: - void refresh(); + virtual void refresh(); + virtual void refreshDataDefinedProperty( const QgsLayoutObject::DataDefinedProperty property = QgsLayoutObject::AllProperties ); diff --git a/src/core/layout/qgsabstractlayoutiterator.h b/src/core/layout/qgsabstractlayoutiterator.h index c2238b6283e8..4be46a325151 100644 --- a/src/core/layout/qgsabstractlayoutiterator.h +++ b/src/core/layout/qgsabstractlayoutiterator.h @@ -21,6 +21,12 @@ class QgsLayout; +/** + * \ingroup core + * \class QgsAbstractLayoutIterator + * \brief An abstract base class for QgsLayout based classes which can be exported by QgsLayoutExporter. + * \since QGIS 3.0 + */ class CORE_EXPORT QgsAbstractLayoutIterator { diff --git a/src/core/layout/qgslayoutatlas.h b/src/core/layout/qgslayoutatlas.h index a1d236ed598e..f0f5f6cfc117 100644 --- a/src/core/layout/qgslayoutatlas.h +++ b/src/core/layout/qgslayoutatlas.h @@ -258,6 +258,7 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject, public QgsAbstractLayoutItera * \see next() * \see last() * \see first() + * \see seekTo() */ bool previous(); @@ -266,6 +267,7 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject, public QgsAbstractLayoutItera * \see next() * \see previous() * \see first() + * \see seekTo() */ bool last(); @@ -274,9 +276,17 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject, public QgsAbstractLayoutItera * \see next() * \see previous() * \see last() + * \see seekTo() */ bool first(); + /** + * Seeks to the specified \a feature number. + * \see first() + * \see previous() + * \see next() + * \see last() + */ bool seekTo( int feature ); /** diff --git a/src/core/layout/qgslayoutitemlegend.h b/src/core/layout/qgslayoutitemlegend.h index 6b0911f07f00..37aad3844095 100644 --- a/src/core/layout/qgslayoutitemlegend.h +++ b/src/core/layout/qgslayoutitemlegend.h @@ -446,7 +446,7 @@ class CORE_EXPORT QgsLayoutItemLegend : public QgsLayoutItem public slots: - void refresh(); + void refresh() override; void refreshDataDefinedProperty( const QgsLayoutObject::DataDefinedProperty property = QgsLayoutObject::AllProperties ) override; protected: diff --git a/tests/src/python/test_qgsreport.py b/tests/src/python/test_qgsreport.py index 456a521f8ba3..9c8f62e7cf82 100644 --- a/tests/src/python/test_qgsreport.py +++ b/tests/src/python/test_qgsreport.py @@ -62,7 +62,7 @@ def testchildSections(self): self.assertIsNone(r.childSection(1)) self.assertIsNone(r.childSection(0)) - # try deleting non-existant childSections + # try deleting non-existent childSections r.removeChildAt(-1) r.removeChildAt(0) r.removeChildAt(100) From 3332118ec781ce5ecf2278509fc59d45fe55634f Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sun, 31 Dec 2017 15:00:11 +1000 Subject: [PATCH 081/105] Add test mask --- .../expected_composerhtml_setfeature_mask.png | Bin 0 -> 5456 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/testdata/control_images/composer_html/expected_composerhtml_setfeature/travis/expected_composerhtml_setfeature_mask.png diff --git a/tests/testdata/control_images/composer_html/expected_composerhtml_setfeature/travis/expected_composerhtml_setfeature_mask.png b/tests/testdata/control_images/composer_html/expected_composerhtml_setfeature/travis/expected_composerhtml_setfeature_mask.png new file mode 100644 index 0000000000000000000000000000000000000000..33941be4fd4dd2e257701f6ccb6e0fbc6934e44d GIT binary patch literal 5456 zcmeAS@N?(olHy`uVBq!ia0y~yU`b+NV3y)w1B#q|`d}}RVoUONcVYMsf(!O8p9~b? zEbxddW?chfiOZFK}?;jtu>n*~A;&vG>|6R-onskL~;zKI~81QpUhgA*=|pT|wyxkYsRa zkpPkn9RkciN Date: Mon, 1 Jan 2018 18:17:40 +1000 Subject: [PATCH 082/105] Ensure main canvas is refreshed when atlas preview feature changes --- src/app/layout/qgslayoutdesignerdialog.cpp | 36 ++++------------------ src/app/qgisapp.cpp | 3 -- src/gui/qgsmapcanvas.cpp | 1 + 3 files changed, 7 insertions(+), 33 deletions(-) diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 304bd5729041..bc8214a2a48d 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -1938,9 +1938,6 @@ void QgsLayoutDesignerDialog::atlasPreviewTriggered( bool checked ) { QgisApp::instance()->mapCanvas()->stopRendering(); atlas->first(); -#if 0 //TODO - emit atlasPreviewFeatureChanged(); -#endif } } else @@ -1981,9 +1978,6 @@ void QgsLayoutDesignerDialog::atlasPageComboEditingFinished() QgisApp::instance()->mapCanvas()->stopRendering(); loadAtlasPredefinedScalesFromProject(); atlas->seekTo( page - 1 ); -#if 0 //TODO - emit atlasPreviewFeatureChanged(); -#endif } } @@ -1996,12 +1990,7 @@ void QgsLayoutDesignerDialog::atlasNext() QgisApp::instance()->mapCanvas()->stopRendering(); loadAtlasPredefinedScalesFromProject(); - if ( printAtlas->next() ) - { -#if 0 //TODO - emit atlasPreviewFeatureChanged(); -#endif - } + printAtlas->next(); } void QgsLayoutDesignerDialog::atlasPrevious() @@ -2013,12 +2002,7 @@ void QgsLayoutDesignerDialog::atlasPrevious() QgisApp::instance()->mapCanvas()->stopRendering(); loadAtlasPredefinedScalesFromProject(); - if ( printAtlas->previous() ) - { -#if 0 //TODO - emit atlasPreviewFeatureChanged(); -#endif - } + printAtlas->previous(); } void QgsLayoutDesignerDialog::atlasFirst() @@ -2030,12 +2014,7 @@ void QgsLayoutDesignerDialog::atlasFirst() QgisApp::instance()->mapCanvas()->stopRendering(); loadAtlasPredefinedScalesFromProject(); - if ( printAtlas->first() ) - { -#if 0 //TODO - emit atlasPreviewFeatureChanged(); -#endif - } + printAtlas->first(); } void QgsLayoutDesignerDialog::atlasLast() @@ -2047,12 +2026,7 @@ void QgsLayoutDesignerDialog::atlasLast() QgisApp::instance()->mapCanvas()->stopRendering(); loadAtlasPredefinedScalesFromProject(); - if ( printAtlas->last() ) - { -#if 0 //TODO - emit atlasPreviewFeatureChanged(); -#endif - } + printAtlas->last(); } void QgsLayoutDesignerDialog::printAtlas() @@ -3478,6 +3452,8 @@ void QgsLayoutDesignerDialog::atlasFeatureChanged( const QgsFeature &feature ) mapCanvas->expressionContextScope().addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_feature" ), QVariant::fromValue( feature ), true ) ); mapCanvas->expressionContextScope().addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_featureid" ), feature.id(), true ) ); mapCanvas->expressionContextScope().addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "atlas_geometry" ), QVariant::fromValue( feature.geometry() ), true ) ); + mapCanvas->stopRendering(); + mapCanvas->refreshAllLayers(); } void QgsLayoutDesignerDialog::loadAtlasPredefinedScalesFromProject() diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index bd4291314df9..5545f5f224cb 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -7487,9 +7487,6 @@ QgsLayoutDesignerDialog *QgisApp::openLayoutDesignerDialog( QgsMasterLayoutInter newDesigner->open(); emit layoutDesignerOpened( newDesigner->iface() ); -#if 0 //TODO - connect( newDesigner, &QgsLayoutDesignerDialog::atlasPreviewFeatureChanged, this, &QgisApp::refreshMapCanvas ); -#endif return newDesigner; } diff --git a/src/gui/qgsmapcanvas.cpp b/src/gui/qgsmapcanvas.cpp index 43faedda1c63..bcdd0ade8049 100644 --- a/src/gui/qgsmapcanvas.cpp +++ b/src/gui/qgsmapcanvas.cpp @@ -696,6 +696,7 @@ void QgsMapCanvas::stopRendering() mJob->cancelWithoutBlocking(); mJob = nullptr; } + stopPreviewJobs(); } //the format defaults to "PNG" if not specified From f7759b20e1a8eeeae76e8adadeb7b8d4729419aa Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 1 Jan 2018 18:28:50 +1000 Subject: [PATCH 083/105] Fix crash on close project with layout designer open --- src/app/qgisapp.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 5545f5f224cb..d18034e92c07 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -10600,7 +10600,12 @@ void QgisApp::closeProject() deletePrintComposers(); deleteLayoutDesigners(); + + // ensure layout widgets are fully deleted + QgsApplication::sendPostedEvents( nullptr, QEvent::DeferredDelete ); + removeAnnotationItems(); + // clear out any stuff from project mMapCanvas->freeze( true ); mMapCanvas->setLayers( QList() ); From 7450c78606fdb2374b546117d45a53a7cea9a34e Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 1 Jan 2018 18:29:07 +1000 Subject: [PATCH 084/105] Add new icon for atlas exports (courtesy of @nirvn) --- images/images.qrc | 1 + images/themes/default/mActionExport.svg | 192 +++++++++++++++++++++ src/app/layout/qgslayoutdesignerdialog.cpp | 3 +- 3 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 images/themes/default/mActionExport.svg diff --git a/images/images.qrc b/images/images.qrc index 8cb5525d94b6..c1a0a3bc1705 100755 --- a/images/images.qrc +++ b/images/images.qrc @@ -636,6 +636,7 @@ themes/default/mIconPythonFile.svg themes/default/mIconQptFile.svg themes/default/mActionNewPage.svg + themes/default/mActionExport.svg qgis_tips/symbol_levels.png diff --git a/images/themes/default/mActionExport.svg b/images/themes/default/mActionExport.svg new file mode 100644 index 000000000000..09e677f1add3 --- /dev/null +++ b/images/themes/default/mActionExport.svg @@ -0,0 +1,192 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index bc8214a2a48d..c52f1ed2d7f7 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -300,13 +300,14 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla mActionsToolbar->addWidget( resizeToolButton ); QToolButton *atlasExportToolButton = new QToolButton( mAtlasToolbar ); + atlasExportToolButton->setIcon( QgsApplication::getThemeIcon( "mActionExport.svg" ) ); atlasExportToolButton->setPopupMode( QToolButton::InstantPopup ); atlasExportToolButton->setAutoRaise( true ); atlasExportToolButton->setToolButtonStyle( Qt::ToolButtonIconOnly ); atlasExportToolButton->addAction( mActionExportAtlasAsImage ); atlasExportToolButton->addAction( mActionExportAtlasAsSVG ); atlasExportToolButton->addAction( mActionExportAtlasAsPDF ); - atlasExportToolButton->setDefaultAction( mActionExportAtlasAsImage ); + atlasExportToolButton->setToolTip( tr( "Export Atlas" ) ); mAtlasToolbar->insertWidget( mActionAtlasSettings, atlasExportToolButton ); mAtlasPageComboBox = new QComboBox(); mAtlasPageComboBox->setEditable( true ); From 39ae0eef7d3d0d65e092a1e239ec347535a3edc1 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 1 Jan 2018 19:04:58 +1000 Subject: [PATCH 085/105] Add method to seek atlas directly to a QgsFeature --- python/core/layout/qgslayoutatlas.sip | 13 +++++++++++++ src/core/layout/qgslayoutatlas.cpp | 22 ++++++++++++++++++++++ src/core/layout/qgslayoutatlas.h | 9 +++++++++ tests/src/python/test_qgslayoutatlas.py | 15 +++++++++++++++ 4 files changed, 59 insertions(+) diff --git a/python/core/layout/qgslayoutatlas.sip b/python/core/layout/qgslayoutatlas.sip index 821cfd47816b..2a0e02cb10b7 100644 --- a/python/core/layout/qgslayoutatlas.sip +++ b/python/core/layout/qgslayoutatlas.sip @@ -332,6 +332,19 @@ Seeks to the specified ``feature`` number. .. seealso:: :py:func:`next()` +.. seealso:: :py:func:`last()` +%End + + bool seekTo( const QgsFeature &feature ); +%Docstring +Seeks to the specified ``feature``. + +.. seealso:: :py:func:`first()` + +.. seealso:: :py:func:`previous()` + +.. seealso:: :py:func:`next()` + .. seealso:: :py:func:`last()` %End diff --git a/src/core/layout/qgslayoutatlas.cpp b/src/core/layout/qgslayoutatlas.cpp index 5c8709fd6152..5f47d692aef9 100644 --- a/src/core/layout/qgslayoutatlas.cpp +++ b/src/core/layout/qgslayoutatlas.cpp @@ -387,6 +387,28 @@ bool QgsLayoutAtlas::seekTo( int feature ) return prepareForFeature( feature ); } +bool QgsLayoutAtlas::seekTo( const QgsFeature &feature ) +{ + int i = -1; + auto it = mFeatureIds.constBegin(); + for ( int currentIdx = 0; it != mFeatureIds.constEnd(); ++it, ++currentIdx ) + { + if ( ( *it ).first == feature.id() ) + { + i = currentIdx; + break; + } + } + + if ( i < 0 ) + { + //feature not found + return false; + } + + return seekTo( i ); +} + void QgsLayoutAtlas::refreshCurrentFeature() { prepareForFeature( mCurrentFeatureNo ); diff --git a/src/core/layout/qgslayoutatlas.h b/src/core/layout/qgslayoutatlas.h index f0f5f6cfc117..f02f32c6d235 100644 --- a/src/core/layout/qgslayoutatlas.h +++ b/src/core/layout/qgslayoutatlas.h @@ -289,6 +289,15 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject, public QgsAbstractLayoutItera */ bool seekTo( int feature ); + /** + * Seeks to the specified \a feature. + * \see first() + * \see previous() + * \see next() + * \see last() + */ + bool seekTo( const QgsFeature &feature ); + /** * Refreshes the current atlas feature, by refetching its attributes from the vector layer provider */ diff --git a/tests/src/python/test_qgslayoutatlas.py b/tests/src/python/test_qgslayoutatlas.py index a75eba56163b..6f60e337862c 100644 --- a/tests/src/python/test_qgslayoutatlas.py +++ b/tests/src/python/test_qgslayoutatlas.py @@ -20,6 +20,7 @@ import glob from qgis.core import (QgsUnitTypes, + QgsFeature, QgsLayout, QgsPrintLayout, QgsLayoutAtlas, @@ -216,24 +217,28 @@ def testIteration(self): self.assertEqual(atlas.currentFeatureNumber(), 0) self.assertEqual(l.reportContext().feature()[4], 'Basse-Normandie') self.assertEqual(l.reportContext().layer(), vector_layer) + f1 = l.reportContext().feature() self.assertTrue(atlas.next()) self.assertEqual(len(atlas_feature_changed_spy), 2) self.assertEqual(len(context_changed_spy), 2) self.assertEqual(atlas.currentFeatureNumber(), 1) self.assertEqual(l.reportContext().feature()[4], 'Bretagne') + f2 = l.reportContext().feature() self.assertTrue(atlas.next()) self.assertEqual(len(atlas_feature_changed_spy), 3) self.assertEqual(len(context_changed_spy), 3) self.assertEqual(atlas.currentFeatureNumber(), 2) self.assertEqual(l.reportContext().feature()[4], 'Pays de la Loire') + f3 = l.reportContext().feature() self.assertTrue(atlas.next()) self.assertEqual(len(atlas_feature_changed_spy), 4) self.assertEqual(len(context_changed_spy), 4) self.assertEqual(atlas.currentFeatureNumber(), 3) self.assertEqual(l.reportContext().feature()[4], 'Centre') + f4 = l.reportContext().feature() self.assertFalse(atlas.next()) self.assertTrue(atlas.seekTo(2)) @@ -263,6 +268,16 @@ def testIteration(self): self.assertTrue(atlas.endRender()) self.assertEqual(len(atlas_feature_changed_spy), 10) + self.assertTrue(atlas.seekTo(f1)) + self.assertEqual(l.reportContext().feature()[4], 'Basse-Normandie') + self.assertTrue(atlas.seekTo(f4)) + self.assertEqual(l.reportContext().feature()[4], 'Centre') + self.assertTrue(atlas.seekTo(f3)) + self.assertEqual(l.reportContext().feature()[4], 'Pays de la Loire') + self.assertTrue(atlas.seekTo(f2)) + self.assertEqual(l.reportContext().feature()[4], 'Bretagne') + self.assertFalse(atlas.seekTo(QgsFeature(5))) + def testUpdateFeature(self): p = QgsProject() vectorFileInfo = QFileInfo(unitTestDataPath() + "/france_parts.shp") From a730eb7a95b4056c9e9808b772421ee65ec504da Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 1 Jan 2018 19:11:54 +1000 Subject: [PATCH 086/105] Swap 'set atlas feature' action from compositions to layouts Time to start making a permenant switch... --- src/app/layout/qgslayoutdesignerdialog.cpp | 23 ++++++++ src/app/layout/qgslayoutdesignerdialog.h | 5 ++ src/app/qgisapp.cpp | 68 ++++++++++++---------- src/app/qgisapp.h | 7 ++- 4 files changed, 70 insertions(+), 33 deletions(-) diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index c52f1ed2d7f7..104833282339 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -3600,4 +3600,27 @@ QgsMessageBar *QgsLayoutDesignerDialog::messageBar() return mMessageBar; } +void QgsLayoutDesignerDialog::setAtlasFeature( QgsMapLayer *layer, const QgsFeature &feat ) +{ + QgsLayoutAtlas *layoutAtlas = atlas(); + if ( !layoutAtlas || !layoutAtlas->enabled() || layoutAtlas->coverageLayer() != layer ) + { + //either atlas isn't enabled, or layer doesn't match + return; + } + + if ( !mActionAtlasPreview->isChecked() ) + { + //update gui controls + whileBlocking( mActionAtlasPreview )->setChecked( true ); + atlasPreviewTriggered( true ); + } + + //set current preview feature id + layoutAtlas->seekTo( feat ); + + //bring layout window to foreground + activate(); +} + diff --git a/src/app/layout/qgslayoutdesignerdialog.h b/src/app/layout/qgslayoutdesignerdialog.h index 8cab9516016c..96d45d8a3817 100644 --- a/src/app/layout/qgslayoutdesignerdialog.h +++ b/src/app/layout/qgslayoutdesignerdialog.h @@ -138,6 +138,11 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner */ QgsMessageBar *messageBar(); + /** + * Sets the specified feature as the current atlas feature + */ + void setAtlasFeature( QgsMapLayer *layer, const QgsFeature &feat ); + public slots: /** diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index d18034e92c07..4e02ca37b574 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -206,6 +206,7 @@ Q_GUI_EXPORT extern int qt_defaultDpiX(); #include "qgslayertreeview.h" #include "qgslayertreeviewdefaultactions.h" #include "qgslayout.h" +#include "qgslayoutatlas.h" #include "qgslayoutcustomdrophandler.h" #include "qgslayoutdesignerdialog.h" #include "qgslayoutmanager.h" @@ -7560,81 +7561,88 @@ void QgisApp::deleteLayoutDesigners() void QgisApp::setupLayoutManagerConnections() { QgsLayoutManager *manager = QgsProject::instance()->layoutManager(); - connect( manager, &QgsLayoutManager::compositionAdded, this, [ = ]( const QString & name ) + connect( manager, &QgsLayoutManager::layoutAdded, this, [ = ]( const QString & name ) { - QgsComposition *c = QgsProject::instance()->layoutManager()->compositionByName( name ); - if ( !c ) + QgsMasterLayoutInterface *l = QgsProject::instance()->layoutManager()->layoutByName( name ); + if ( !l ) + return; + QgsPrintLayout *pl = dynamic_cast< QgsPrintLayout *>( l ); + if ( !pl ) return; - mAtlasFeatureActions.insert( c, nullptr ); - connect( c, &QgsComposition::nameChanged, this, [this, c]( const QString & name ) + mAtlasFeatureActions.insert( pl, nullptr ); + connect( pl, &QgsPrintLayout::nameChanged, this, [this, pl]( const QString & name ) { - QgsMapLayerAction *action = mAtlasFeatureActions.value( c ); + QgsMapLayerAction *action = mAtlasFeatureActions.value( pl ); if ( action ) { action->setText( QString( tr( "Set as atlas feature for %1" ) ).arg( name ) ); } } ); - connect( &c->atlasComposition(), &QgsAtlasComposition::coverageLayerChanged, this, [this, c]( QgsVectorLayer * coverageLayer ) + connect( pl->atlas(), &QgsLayoutAtlas::coverageLayerChanged, this, [this, pl]( QgsVectorLayer * coverageLayer ) { - setupAtlasMapLayerAction( c, static_cast< bool >( coverageLayer ) ); + setupAtlasMapLayerAction( pl, static_cast< bool >( coverageLayer ) ); } ); - connect( &c->atlasComposition(), &QgsAtlasComposition::toggled, this, [this, c]( bool enabled ) + connect( pl->atlas(), &QgsLayoutAtlas::toggled, this, [this, pl]( bool enabled ) { - setupAtlasMapLayerAction( c, enabled ); + setupAtlasMapLayerAction( pl, enabled ); } ); - setupAtlasMapLayerAction( c, c->atlasComposition().enabled() && c->atlasComposition().coverageLayer() ); + setupAtlasMapLayerAction( pl, pl->atlas()->enabled() && pl->atlas()->coverageLayer() ); } ); - connect( manager, &QgsLayoutManager::compositionAboutToBeRemoved, this, [ = ]( const QString & name ) + connect( manager, &QgsLayoutManager::layoutAboutToBeRemoved, this, [ = ]( const QString & name ) { - QgsComposition *c = QgsProject::instance()->layoutManager()->compositionByName( name ); - if ( c ) + QgsMasterLayoutInterface *l = QgsProject::instance()->layoutManager()->layoutByName( name ); + if ( l ) { - QgsMapLayerAction *action = mAtlasFeatureActions.value( c ); - if ( action ) + QgsPrintLayout *pl = dynamic_cast< QgsPrintLayout * >( l ); + if ( pl ) { - QgsGui::mapLayerActionRegistry()->removeMapLayerAction( action ); - delete action; - mAtlasFeatureActions.remove( c ); + QgsMapLayerAction *action = mAtlasFeatureActions.value( pl ); + if ( action ) + { + QgsGui::mapLayerActionRegistry()->removeMapLayerAction( action ); + delete action; + mAtlasFeatureActions.remove( pl ); + } } } } ); } -void QgisApp::setupAtlasMapLayerAction( QgsComposition *composition, bool enableAction ) +void QgisApp::setupAtlasMapLayerAction( QgsPrintLayout *layout, bool enableAction ) { - QgsMapLayerAction *action = mAtlasFeatureActions.value( composition ); + QgsMapLayerAction *action = mAtlasFeatureActions.value( layout ); if ( action ) { QgsGui::mapLayerActionRegistry()->removeMapLayerAction( action ); delete action; action = nullptr; - mAtlasFeatureActions.remove( composition ); + mAtlasFeatureActions.remove( layout ); } if ( enableAction ) { - action = new QgsMapLayerAction( QString( tr( "Set as atlas feature for %1" ) ).arg( composition->name() ), - this, composition->atlasComposition().coverageLayer(), QgsMapLayerAction::SingleFeature, + action = new QgsMapLayerAction( QString( tr( "Set as atlas feature for %1" ) ).arg( layout->name() ), + this, layout->atlas()->coverageLayer(), QgsMapLayerAction::SingleFeature, QgsApplication::getThemeIcon( QStringLiteral( "/mIconAtlas.svg" ) ) ); - mAtlasFeatureActions.insert( composition, action ); + mAtlasFeatureActions.insert( layout, action ); QgsGui::mapLayerActionRegistry()->addMapLayerAction( action ); - connect( action, &QgsMapLayerAction::triggeredForFeature, this, [this, composition]( QgsMapLayer * layer, const QgsFeature & feat ) + connect( action, &QgsMapLayerAction::triggeredForFeature, this, [this, layout]( QgsMapLayer * layer, const QgsFeature & feat ) { - setCompositionAtlasFeature( composition, layer, feat ); + setLayoutAtlasFeature( layout, layer, feat ); } ); } } -void QgisApp::setCompositionAtlasFeature( QgsComposition *composition, QgsMapLayer *layer, const QgsFeature &feat ) +void QgisApp::setLayoutAtlasFeature( QgsPrintLayout *layout, QgsMapLayer *layer, const QgsFeature &feat ) { - QgsComposer *composer = openComposer( composition ); - composer->setAtlasFeature( layer, feat ); + QgsLayoutDesignerDialog *designer = openLayoutDesignerDialog( layout ); + designer->setAtlasFeature( layer, feat ); } void QgisApp::composerMenuAboutToShow() diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h index 1cb5e92cd7e0..c5c5e394b994 100644 --- a/src/app/qgisapp.h +++ b/src/app/qgisapp.h @@ -81,6 +81,7 @@ class QgsPluginLayer; class QgsPluginLayer; class QgsPluginManager; class QgsPointXY; +class QgsPrintLayout; class QgsProviderRegistry; class QgsPythonUtils; class QgsRasterLayer; @@ -1780,9 +1781,9 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow void setupLayoutManagerConnections(); - void setupAtlasMapLayerAction( QgsComposition *composition, bool enableAction ); + void setupAtlasMapLayerAction( QgsPrintLayout *layout, bool enableAction ); - void setCompositionAtlasFeature( QgsComposition *composition, QgsMapLayer *layer, const QgsFeature &feat ); + void setLayoutAtlasFeature( QgsPrintLayout *layout, QgsMapLayer *layer, const QgsFeature &feat ); void saveAsVectorFileGeneral( QgsVectorLayer *vlayer = nullptr, bool symbologyOption = true ); @@ -2187,7 +2188,7 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow QStackedWidget *mCentralContainer = nullptr; - QHash< QgsComposition *, QgsMapLayerAction * > mAtlasFeatureActions; + QHash< QgsPrintLayout *, QgsMapLayerAction * > mAtlasFeatureActions; int mProjOpen = 0; From 4e45639c45ca0b5758dbe070181bd7a71fb7925b Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 2 Jan 2018 12:47:51 +1000 Subject: [PATCH 087/105] Restore direct print actions --- python/core/layout/qgslayoutexporter.sip | 47 +++ src/app/layout/qgslayoutdesignerdialog.cpp | 394 +++++++++++++++++- src/app/layout/qgslayoutdesignerdialog.h | 16 + .../layout/qgslayoutpagepropertieswidget.cpp | 2 + .../layout/qgslayoutpagepropertieswidget.h | 6 + src/core/layout/qgslayoutexporter.cpp | 113 +++++ src/core/layout/qgslayoutexporter.h | 46 ++ src/ui/layout/qgslayoutdesignerbase.ui | 50 ++- tests/src/python/test_qgslayoutexporter.py | 89 ++++ 9 files changed, 758 insertions(+), 5 deletions(-) diff --git a/python/core/layout/qgslayoutexporter.sip b/python/core/layout/qgslayoutexporter.sip index cd4cadd3ce29..71b6ac7829d4 100644 --- a/python/core/layout/qgslayoutexporter.sip +++ b/python/core/layout/qgslayoutexporter.sip @@ -290,6 +290,53 @@ to the error description. .. seealso:: :py:func:`exportToPdf()` %End + + struct PrintExportSettings + { + PrintExportSettings(); +%Docstring +Constructor for PrintExportSettings +%End + + double dpi; +%Docstring +Resolution to export layout at. If dpi <= 0 the default layout dpi will be used. +%End + + bool rasterizeWholeImage; +%Docstring +Set to true to force whole layout to be rasterized while exporting. + +This option is mutually exclusive with forceVectorOutput. +%End + + QgsLayoutRenderContext::Flags flags; +%Docstring +Layout context flags, which control how the export will be created. +%End + + }; + + ExportResult print( QPrinter &printer, const QgsLayoutExporter::PrintExportSettings &settings ); +%Docstring +Prints the layout to a ``printer``, using the specified export ``settings``. + +Returns a result code indicating whether the export was successful or an +error was encountered. +%End + + static ExportResult print( QgsAbstractLayoutIterator *iterator, QPrinter &printer, + const QgsLayoutExporter::PrintExportSettings &settings, + QString &error /Out/, QgsFeedback *feedback = 0 ); +%Docstring +Exports a layout ``iterator`` to a ``printer``, with the specified export ``settings``. + +Returns a result code indicating whether the export was successful or an +error was encountered. If an error was obtained then ``error`` will be set +to the error description. +%End + + struct SvgExportSettings { SvgExportSettings(); diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 104833282339..e95860cab69f 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -73,6 +73,9 @@ #include #include #include +#include +#include +#include #ifdef Q_OS_MACX #include #endif @@ -195,6 +198,7 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla connect( mActionLayoutManager, &QAction::triggered, this, &QgsLayoutDesignerDialog::showManager ); connect( mActionRemoveLayout, &QAction::triggered, this, &QgsLayoutDesignerDialog::deleteLayout ); + connect( mActionPrint, &QAction::triggered, this, &QgsLayoutDesignerDialog::print ); connect( mActionExportAsImage, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportToRaster ); connect( mActionExportAsPDF, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportToPdf ); connect( mActionExportAsSVG, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportToSvg ); @@ -226,6 +230,9 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla connect( mActionExportReportAsImage, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportReportToRaster ); connect( mActionExportReportAsSVG, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportReportToSvg ); connect( mActionExportReportAsPDF, &QAction::triggered, this, &QgsLayoutDesignerDialog::exportReportToPdf ); + connect( mActionPrintReport, &QAction::triggered, this, &QgsLayoutDesignerDialog::printReport ); + + connect( mActionPageSetup, &QAction::triggered, this, &QgsLayoutDesignerDialog::pageSetup ); mView = new QgsLayoutView(); //mView->setMapCanvas( mQgis->mapCanvas() ); @@ -882,6 +889,10 @@ void QgsLayoutDesignerDialog::showItemOptions( QgsLayoutItem *item, bool bringPa if ( ! widget ) return; + + if ( QgsLayoutPagePropertiesWidget *ppWidget = qobject_cast< QgsLayoutPagePropertiesWidget * >( widget.get() ) ) + connect( ppWidget, &QgsLayoutPagePropertiesWidget::pageOrientationChanged, this, &QgsLayoutDesignerDialog::pageOrientationChanged ); + widget->setDockMode( true ); connect( item, &QgsLayoutItem::destroyed, widget.get(), [this] { @@ -1573,6 +1584,102 @@ void QgsLayoutDesignerDialog::deleteLayout() close(); } +void QgsLayoutDesignerDialog::print() +{ + if ( containsWmsLayers() ) + { + showWmsPrintingWarning(); + } + + if ( requiresRasterization() ) + { + showRasterizationWarning(); + } + + if ( currentLayout()->pageCollection()->pageCount() == 0 ) + return; + + // get orientation from first page + QgsLayoutItemPage::Orientation orientation = currentLayout()->pageCollection()->page( 0 )->orientation(); + + //set printer page orientation + setPrinterPageOrientation( orientation ); + + QPrintDialog printDialog( printer(), nullptr ); + if ( printDialog.exec() != QDialog::Accepted ) + { + return; + } + + + mView->setPaintingEnabled( false ); + QApplication::setOverrideCursor( Qt::BusyCursor ); + + QgsLayoutExporter::PrintExportSettings printSettings; + printSettings.rasterizeWholeImage = mLayout->customProperty( QStringLiteral( "rasterize" ), false ).toBool(); + + // force a refresh, to e.g. update data defined properties, tables, etc + mLayout->refresh(); + + QgsLayoutExporter exporter( mLayout ); + QString printerName = printer()->printerName(); + switch ( exporter.print( *printer(), printSettings ) ) + { + case QgsLayoutExporter::Success: + { + QString message; + if ( !printerName.isEmpty() ) + { + message = tr( "Successfully printed layout to %1" ).arg( printerName ); + } + else + { + message = tr( "Successfully printed layout" ); + } + mMessageBar->pushMessage( tr( "Print layout" ), + message, + QgsMessageBar::SUCCESS, 0 ); + break; + } + + case QgsLayoutExporter::PrintError: + { + QString message; + if ( !printerName.isEmpty() ) + { + message = tr( "Could not create print device for %1" ).arg( printerName ); + } + else + { + message = tr( "Could not create print device" ); + } + QMessageBox::warning( this, tr( "Print layout" ), + message, + QMessageBox::Ok, + QMessageBox::Ok ); + break; + } + + case QgsLayoutExporter::MemoryError: + QMessageBox::warning( this, tr( "Memory Allocation Error" ), + tr( "Printing the layout " + "resulted in a memory overflow.\n\n" + "Please try a lower resolution or a smaller paper size." ), + QMessageBox::Ok, QMessageBox::Ok ); + break; + + case QgsLayoutExporter::FileError: + case QgsLayoutExporter::SvgLayerError: + case QgsLayoutExporter::IteratorError: + case QgsLayoutExporter::Canceled: + // no meaning for PDF exports, will not be encountered + break; + } + + mView->setPaintingEnabled( true ); + QApplication::restoreOverrideCursor(); +} + void QgsLayoutDesignerDialog::exportToRaster() { if ( containsWmsLayers() ) @@ -2032,8 +2139,132 @@ void QgsLayoutDesignerDialog::atlasLast() void QgsLayoutDesignerDialog::printAtlas() { + QgsLayoutAtlas *printAtlas = atlas(); + if ( !printAtlas || !printAtlas->enabled() ) + return; + loadAtlasPredefinedScalesFromProject(); - //TODO + + if ( containsWmsLayers() ) + { + showWmsPrintingWarning(); + } + + if ( requiresRasterization() ) + { + showRasterizationWarning(); + } + + if ( currentLayout()->pageCollection()->pageCount() == 0 ) + return; + + // get orientation from first page + QgsLayoutItemPage::Orientation orientation = currentLayout()->pageCollection()->page( 0 )->orientation(); + + //set printer page orientation + setPrinterPageOrientation( orientation ); + + QPrintDialog printDialog( printer(), nullptr ); + if ( printDialog.exec() != QDialog::Accepted ) + { + return; + } + + mView->setPaintingEnabled( false ); + QApplication::setOverrideCursor( Qt::BusyCursor ); + + QgsLayoutExporter::PrintExportSettings printSettings; + printSettings.rasterizeWholeImage = mLayout->customProperty( QStringLiteral( "rasterize" ), false ).toBool(); + + QString error; + std::unique_ptr< QgsFeedback > feedback = qgis::make_unique< QgsFeedback >(); + std::unique_ptr< QProgressDialog > progressDialog = qgis::make_unique< QProgressDialog >( tr( "Printing maps..." ), tr( "Abort" ), 0, 100, this ); + progressDialog->setWindowTitle( tr( "Printing Atlas" ) ); + connect( feedback.get(), &QgsFeedback::progressChanged, this, [ & ]( double progress ) + { + progressDialog->setValue( progress ); + progressDialog->setLabelText( feedback->property( "progress" ).toString() ) ; + +#ifdef Q_OS_LINUX + // For some reason on Windows hasPendingEvents() always return true, + // but one iteration is actually enough on Windows to get good interactivity + // whereas on Linux we must allow for far more iterations. + // For safety limit the number of iterations + int nIters = 0; + while ( QCoreApplication::hasPendingEvents() && ++nIters < 100 ) +#endif + { + QCoreApplication::processEvents(); + } + + } ); + connect( progressDialog.get(), &QProgressDialog::canceled, this, [ & ] + { + feedback->cancel(); + } ); + + QString printerName = printer()->printerName(); + switch ( QgsLayoutExporter::print( printAtlas, *printer(), printSettings, error, feedback.get() ) ) + { + case QgsLayoutExporter::Success: + { + QString message; + if ( !printerName.isEmpty() ) + { + message = tr( "Successfully printed atlas to %1" ).arg( printerName ); + } + else + { + message = tr( "Successfully printed atlas" ); + } + mMessageBar->pushMessage( tr( "Print atlas" ), + message, + QgsMessageBar::SUCCESS, 0 ); + break; + } + + case QgsLayoutExporter::PrintError: + { + QString message; + if ( !printerName.isEmpty() ) + { + message = tr( "Could not create print device for %1" ).arg( printerName ); + } + else + { + message = tr( "Could not create print device" ); + } + QMessageBox::warning( this, tr( "Print atlas" ), + message, + QMessageBox::Ok, + QMessageBox::Ok ); + break; + } + + case QgsLayoutExporter::MemoryError: + QMessageBox::warning( this, tr( "Memory Allocation Error" ), + tr( "Printing the layout " + "resulted in a memory overflow.\n\n" + "Please try a lower resolution or a smaller paper size." ), + QMessageBox::Ok, QMessageBox::Ok ); + break; + + case QgsLayoutExporter::IteratorError: + QMessageBox::warning( this, tr( "Print Atlas" ), + tr( "Error encountered while printing atlas" ), + QMessageBox::Ok, + QMessageBox::Ok ); + break; + + case QgsLayoutExporter::FileError: + case QgsLayoutExporter::SvgLayerError: + case QgsLayoutExporter::Canceled: + // no meaning for PDF exports, will not be encountered + break; + } + + mView->setPaintingEnabled( true ); + QApplication::restoreOverrideCursor(); } void QgsLayoutDesignerDialog::exportAtlasToRaster() @@ -2934,6 +3165,112 @@ void QgsLayoutDesignerDialog::exportReportToPdf() QApplication::restoreOverrideCursor(); } +void QgsLayoutDesignerDialog::printReport() +{ + QPrintDialog printDialog( printer(), nullptr ); + if ( printDialog.exec() != QDialog::Accepted ) + { + return; + } + + mView->setPaintingEnabled( false ); + QApplication::setOverrideCursor( Qt::BusyCursor ); + + QgsLayoutExporter::PrintExportSettings printSettings; + if ( mLayout ) + printSettings.rasterizeWholeImage = mLayout->customProperty( QStringLiteral( "rasterize" ), false ).toBool(); + + QString error; + std::unique_ptr< QgsFeedback > feedback = qgis::make_unique< QgsFeedback >(); + std::unique_ptr< QProgressDialog > progressDialog = qgis::make_unique< QProgressDialog >( tr( "Printing maps..." ), tr( "Abort" ), 0, 0, this ); + progressDialog->setWindowTitle( tr( "Printing Report" ) ); + connect( feedback.get(), &QgsFeedback::progressChanged, this, [ & ]( double ) + { + //progressDialog->setValue( progress ); + progressDialog->setLabelText( feedback->property( "progress" ).toString() ) ; + +#ifdef Q_OS_LINUX + // For some reason on Windows hasPendingEvents() always return true, + // but one iteration is actually enough on Windows to get good interactivity + // whereas on Linux we must allow for far more iterations. + // For safety limit the number of iterations + int nIters = 0; + while ( QCoreApplication::hasPendingEvents() && ++nIters < 100 ) +#endif + { + QCoreApplication::processEvents(); + } + + } ); + connect( progressDialog.get(), &QProgressDialog::canceled, this, [ & ] + { + feedback->cancel(); + } ); + + QString printerName = printer()->printerName(); + switch ( QgsLayoutExporter::print( dynamic_cast< QgsReport * >( mMasterLayout ), *printer(), printSettings, error, feedback.get() ) ) + { + case QgsLayoutExporter::Success: + { + QString message; + if ( !printerName.isEmpty() ) + { + message = tr( "Successfully printed report to %1" ).arg( printerName ); + } + else + { + message = tr( "Successfully printed report" ); + } + mMessageBar->pushMessage( tr( "Print report" ), + message, + QgsMessageBar::SUCCESS, 0 ); + break; + } + + case QgsLayoutExporter::PrintError: + { + QString message; + if ( !printerName.isEmpty() ) + { + message = tr( "Could not create print device for %1" ).arg( printerName ); + } + else + { + message = tr( "Could not create print device" ); + } + QMessageBox::warning( this, tr( "Print report" ), + message, + QMessageBox::Ok, + QMessageBox::Ok ); + break; + } + + case QgsLayoutExporter::MemoryError: + QMessageBox::warning( this, tr( "Memory Allocation Error" ), + tr( "Printing the report" + "resulted in a memory overflow.\n\n" + "Please try a lower resolution or a smaller paper size." ), + QMessageBox::Ok, QMessageBox::Ok ); + break; + + case QgsLayoutExporter::IteratorError: + QMessageBox::warning( this, tr( "Print Report" ), + tr( "Error encountered while printing report" ), + QMessageBox::Ok, + QMessageBox::Ok ); + break; + + case QgsLayoutExporter::FileError: + case QgsLayoutExporter::SvgLayerError: + case QgsLayoutExporter::Canceled: + // no meaning for PDF exports, will not be encountered + break; + } + + mView->setPaintingEnabled( true ); + QApplication::restoreOverrideCursor(); +} + void QgsLayoutDesignerDialog::showReportSettings() { if ( !mReportDock ) @@ -2947,6 +3284,25 @@ void QgsLayoutDesignerDialog::showReportSettings() mReportDock->raise(); } +void QgsLayoutDesignerDialog::pageSetup() +{ + if ( currentLayout() && currentLayout()->pageCollection()->pageCount() > 0 ) + { + // get orientation from first page + QgsLayoutItemPage::Orientation orientation = currentLayout()->pageCollection()->page( 0 )->orientation(); + //set printer page orientation + setPrinterPageOrientation( orientation ); + } + + QPageSetupDialog pageSetupDialog( printer(), this ); + pageSetupDialog.exec(); +} + +void QgsLayoutDesignerDialog::pageOrientationChanged() +{ + mSetPageOrientation = false; +} + void QgsLayoutDesignerDialog::paste() { QPointF pt = mView->mapFromGlobal( QCursor::pos() ); @@ -3078,6 +3434,8 @@ void QgsLayoutDesignerDialog::createReportWidget() QgsReportOrganizerWidget *reportWidget = new QgsReportOrganizerWidget( mReportDock, this, report ); reportWidget->setMessageBar( mMessageBar ); mReportDock->setWidget( reportWidget ); + mReportDock->show(); + mReportDock->raise(); mReportToolbar->show(); @@ -3087,9 +3445,10 @@ void QgsLayoutDesignerDialog::createReportWidget() void QgsLayoutDesignerDialog::initializeRegistry() { sInitializedRegistry = true; - auto createPageWidget = ( []( QgsLayoutItem * item )->QgsLayoutItemBaseWidget * + auto createPageWidget = ( [this]( QgsLayoutItem * item )->QgsLayoutItemBaseWidget * { - return new QgsLayoutPagePropertiesWidget( nullptr, item ); + std::unique_ptr< QgsLayoutPagePropertiesWidget > newWidget = qgis::make_unique< QgsLayoutPagePropertiesWidget >( nullptr, item ); + return newWidget.release(); } ); QgsGui::layoutItemGuiRegistry()->addLayoutItemGuiMetadata( new QgsLayoutItemGuiMetadata( QgsLayoutItemRegistry::LayoutPage, QObject::tr( "Page" ), QIcon(), createPageWidget, nullptr, QString(), false, QgsLayoutItemAbstractGuiMetadata::FlagNoCreationTools ) ); @@ -3573,6 +3932,35 @@ void QgsLayoutDesignerDialog::toggleActions( bool layoutAvailable ) } } +void QgsLayoutDesignerDialog::setPrinterPageOrientation( QgsLayoutItemPage::Orientation orientation ) +{ + if ( !mSetPageOrientation ) + { + switch ( orientation ) + { + case QgsLayoutItemPage::Landscape: + printer()->setOrientation( QPrinter::Landscape ); + break; + + case QgsLayoutItemPage::Portrait: + printer()->setOrientation( QPrinter::Portrait ); + break; + } + + mSetPageOrientation = true; + } +} + +QPrinter *QgsLayoutDesignerDialog::printer() +{ + //only create the printer on demand - creating a printer object can be very slow + //due to QTBUG-3033 + if ( !mPrinter ) + mPrinter = qgis::make_unique< QPrinter >(); + + return mPrinter.get(); +} + void QgsLayoutDesignerDialog::selectItems( const QList items ) { for ( QGraphicsItem *item : items ) diff --git a/src/app/layout/qgslayoutdesignerdialog.h b/src/app/layout/qgslayoutdesignerdialog.h index 96d45d8a3817..74e7f60d7e85 100644 --- a/src/app/layout/qgslayoutdesignerdialog.h +++ b/src/app/layout/qgslayoutdesignerdialog.h @@ -20,6 +20,7 @@ #include "ui_qgslayoutdesignerbase.h" #include "qgslayoutdesignerinterface.h" #include "qgslayoutexporter.h" +#include "qgslayoutpagecollection.h" #include class QgsLayoutDesignerDialog; @@ -304,6 +305,7 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner void showManager(); void renameLayout(); void deleteLayout(); + void print(); void exportToRaster(); void exportToPdf(); void exportToSvg(); @@ -322,7 +324,14 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner void exportReportToRaster(); void exportReportToSvg(); void exportReportToPdf(); + void printReport(); void showReportSettings(); + + void pageSetup(); + + //! Sets the printer page orientation when the page orientation changes + void pageOrientationChanged(); + private: static bool sInitializedRegistry; @@ -406,6 +415,10 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner QComboBox *mAtlasPageComboBox = nullptr; + //! Page & Printer Setup + std::unique_ptr< QPrinter > mPrinter; + bool mSetPageOrientation = false; + //! Save window state void saveWindowState(); @@ -462,6 +475,9 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner QgsLayoutAtlas *atlas(); void toggleActions( bool layoutAvailable ); + + void setPrinterPageOrientation( QgsLayoutItemPage::Orientation orientation ); + QPrinter *printer(); }; #endif // QGSLAYOUTDESIGNERDIALOG_H diff --git a/src/app/layout/qgslayoutpagepropertieswidget.cpp b/src/app/layout/qgslayoutpagepropertieswidget.cpp index 061b93f1042f..d34f314557a3 100644 --- a/src/app/layout/qgslayoutpagepropertieswidget.cpp +++ b/src/app/layout/qgslayoutpagepropertieswidget.cpp @@ -165,6 +165,8 @@ void QgsLayoutPagePropertiesWidget::updatePageSize() mPage->layout()->pageCollection()->reflow(); mPage->layout()->pageCollection()->endPageSizeChange(); mPage->layout()->undoStack()->endMacro(); + + emit pageOrientationChanged(); } void QgsLayoutPagePropertiesWidget::setToCustomSize() diff --git a/src/app/layout/qgslayoutpagepropertieswidget.h b/src/app/layout/qgslayoutpagepropertieswidget.h index 6ca344c52f8e..a616019289f5 100644 --- a/src/app/layout/qgslayoutpagepropertieswidget.h +++ b/src/app/layout/qgslayoutpagepropertieswidget.h @@ -23,6 +23,7 @@ #include "qgslayoutpoint.h" #include "qgslayoutitemwidget.h" #include "qgslayoutmeasurementconverter.h" +#include "qgslayoutpagecollection.h" class QgsLayoutItem; class QgsLayoutItemPage; @@ -41,6 +42,11 @@ class QgsLayoutPagePropertiesWidget : public QgsLayoutItemBaseWidget, private Ui */ QgsLayoutPagePropertiesWidget( QWidget *parent, QgsLayoutItem *page ); + signals: + + //! Is emitted when page orientation changes + void pageOrientationChanged(); + private slots: void pageSizeChanged( int index ); diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index b7ba79deeefe..5cb7e4d0835d 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -620,6 +620,119 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToPdfs( QgsAbstractLayo return Success; } +QgsLayoutExporter::ExportResult QgsLayoutExporter::print( QPrinter &printer, const QgsLayoutExporter::PrintExportSettings &s ) +{ + if ( !mLayout ) + return PrintError; + + QgsLayoutExporter::PrintExportSettings settings = s; + if ( settings.dpi <= 0 ) + settings.dpi = mLayout->renderContext().dpi(); + + mErrorFileName.clear(); + + LayoutContextPreviewSettingRestorer restorer( mLayout ); + ( void )restorer; + LayoutContextSettingsRestorer contextRestorer( mLayout ); + ( void )contextRestorer; + mLayout->renderContext().setDpi( settings.dpi ); + + // If we are not printing as raster, temporarily disable advanced effects + // as QPrinter does not support composition modes and can result + // in items missing from the output + mLayout->renderContext().setFlag( QgsLayoutRenderContext::FlagUseAdvancedEffects, !settings.rasterizeWholeImage ); + + preparePrint( mLayout, printer, true ); + QPainter p; + if ( !p.begin( &printer ) ) + { + //error beginning print + return PrintError; + } + + ExportResult result = printPrivate( printer, p, false, settings.dpi, settings.rasterizeWholeImage ); + p.end(); + + return result; +} + +QgsLayoutExporter::ExportResult QgsLayoutExporter::print( QgsAbstractLayoutIterator *iterator, QPrinter &printer, const QgsLayoutExporter::PrintExportSettings &s, QString &error, QgsFeedback *feedback ) +{ + error.clear(); + + if ( !iterator->beginRender() ) + return IteratorError; + + PrintExportSettings settings = s; + + QPainter p; + + int total = iterator->count(); + double step = total > 0 ? 100.0 / total : 100.0; + int i = 0; + bool first = true; + while ( iterator->next() ) + { + if ( feedback ) + { + if ( total > 0 ) + feedback->setProperty( "progress", QObject::tr( "Printing %1 of %2" ).arg( i + 1 ).arg( total ) ); + else + feedback->setProperty( "progress", QObject::tr( "Printing section %1" ).arg( i + 1 ).arg( total ) ); + feedback->setProgress( step * i ); + } + if ( feedback && feedback->isCanceled() ) + { + iterator->endRender(); + return Canceled; + } + + if ( s.dpi <= 0 ) + settings.dpi = iterator->layout()->renderContext().dpi(); + + LayoutContextPreviewSettingRestorer restorer( iterator->layout() ); + ( void )restorer; + LayoutContextSettingsRestorer contextRestorer( iterator->layout() ); + ( void )contextRestorer; + iterator->layout()->renderContext().setDpi( settings.dpi ); + + // If we are not printing as raster, temporarily disable advanced effects + // as QPrinter does not support composition modes and can result + // in items missing from the output + iterator->layout()->renderContext().setFlag( QgsLayoutRenderContext::FlagUseAdvancedEffects, !settings.rasterizeWholeImage ); + + if ( first ) + { + preparePrint( iterator->layout(), printer, true ); + + if ( !p.begin( &printer ) ) + { + //error beginning print + return PrintError; + } + } + + QgsLayoutExporter exporter( iterator->layout() ); + + ExportResult result = exporter.printPrivate( printer, p, !first, settings.dpi, settings.rasterizeWholeImage ); + if ( result != Success ) + { + iterator->endRender(); + return result; + } + first = false; + i++; + } + + if ( feedback ) + { + feedback->setProgress( 100 ); + } + + iterator->endRender(); + return Success; +} + QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &filePath, const QgsLayoutExporter::SvgExportSettings &s ) { if ( !mLayout ) diff --git a/src/core/layout/qgslayoutexporter.h b/src/core/layout/qgslayoutexporter.h index f56e5da13cd7..b12a74a6b51d 100644 --- a/src/core/layout/qgslayoutexporter.h +++ b/src/core/layout/qgslayoutexporter.h @@ -299,6 +299,52 @@ class CORE_EXPORT QgsLayoutExporter const QgsLayoutExporter::PdfExportSettings &settings, QString &error SIP_OUT, QgsFeedback *feedback = nullptr ); + + //! Contains settings relating to printing layouts + struct PrintExportSettings + { + //! Constructor for PrintExportSettings + PrintExportSettings() + : flags( QgsLayoutRenderContext::FlagAntialiasing | QgsLayoutRenderContext::FlagUseAdvancedEffects ) + {} + + //! Resolution to export layout at. If dpi <= 0 the default layout dpi will be used. + double dpi = -1; + + /** + * Set to true to force whole layout to be rasterized while exporting. + * + * This option is mutually exclusive with forceVectorOutput. + */ + bool rasterizeWholeImage = false; + + /** + * Layout context flags, which control how the export will be created. + */ + QgsLayoutRenderContext::Flags flags = 0; + + }; + + /** + * Prints the layout to a \a printer, using the specified export \a settings. + * + * Returns a result code indicating whether the export was successful or an + * error was encountered. + */ + ExportResult print( QPrinter &printer, const QgsLayoutExporter::PrintExportSettings &settings ); + + /** + * Exports a layout \a iterator to a \a printer, with the specified export \a settings. + * + * Returns a result code indicating whether the export was successful or an + * error was encountered. If an error was obtained then \a error will be set + * to the error description. + */ + static ExportResult print( QgsAbstractLayoutIterator *iterator, QPrinter &printer, + const QgsLayoutExporter::PrintExportSettings &settings, + QString &error SIP_OUT, QgsFeedback *feedback = nullptr ); + + //! Contains settings relating to exporting layouts to SVG struct SvgExportSettings { diff --git a/src/ui/layout/qgslayoutdesignerbase.ui b/src/ui/layout/qgslayoutdesignerbase.ui index c4d129eb8c21..396ae5425f6b 100644 --- a/src/ui/layout/qgslayoutdesignerbase.ui +++ b/src/ui/layout/qgslayoutdesignerbase.ui @@ -6,7 +6,7 @@ 0 0 - 1484 + 2180 609 @@ -72,6 +72,7 @@ + @@ -97,7 +98,7 @@ 0 0 - 1484 + 2180 42 @@ -123,6 +124,9 @@ + + + @@ -346,6 +350,7 @@ false + @@ -1435,6 +1440,47 @@ Report Settings + + + false + + + + :/images/themes/default/mActionFilePrint.svg:/images/themes/default/mActionFilePrint.svg + + + &Print... + + + Print Layout + + + Ctrl+P + + + + + + :/images/themes/default/mActionFilePrint.svg:/images/themes/default/mActionFilePrint.svg + + + &Print Report... + + + Print Report + + + + + Pa&ge Setup… + + + Page setup + + + Ctrl+Shift+P + + diff --git a/tests/src/python/test_qgslayoutexporter.py b/tests/src/python/test_qgslayoutexporter.py index c1cfb74b3ad5..25c0e635c8e6 100644 --- a/tests/src/python/test_qgslayoutexporter.py +++ b/tests/src/python/test_qgslayoutexporter.py @@ -41,6 +41,7 @@ QgsReport) from qgis.PyQt.QtCore import QSize, QSizeF, QDir, QRectF, Qt from qgis.PyQt.QtGui import QImage, QPainter +from qgis.PyQt.QtPrintSupport import QPrinter from qgis.PyQt.QtSvg import QSvgRenderer, QSvgGenerator from qgis.testing import start_app, unittest @@ -484,6 +485,61 @@ def testExportToSvg(self): self.assertTrue(self.checkImage('exporttosvglayered_page1', 'exporttopdfdpi_page1', rendered_page_1, size_tolerance=1)) self.assertTrue(self.checkImage('exporttosvglayered_page2', 'exporttopdfdpi_page2', rendered_page_2, size_tolerance=1)) + def testPrint(self): + l = QgsLayout(QgsProject.instance()) + l.initializeDefaults() + + # add a second page + page2 = QgsLayoutItemPage(l) + page2.setPageSize('A5') + l.pageCollection().addPage(page2) + + # add some items + item1 = QgsLayoutItemShape(l) + item1.attemptSetSceneRect(QRectF(10, 20, 100, 150)) + fill = QgsSimpleFillSymbolLayer() + fill_symbol = QgsFillSymbol() + fill_symbol.changeSymbolLayer(0, fill) + fill.setColor(Qt.green) + fill.setStrokeStyle(Qt.NoPen) + item1.setSymbol(fill_symbol) + l.addItem(item1) + + item2 = QgsLayoutItemShape(l) + item2.attemptSetSceneRect(QRectF(10, 20, 100, 150)) + item2.attemptMove(QgsLayoutPoint(10, 20), page=1) + fill = QgsSimpleFillSymbolLayer() + fill_symbol = QgsFillSymbol() + fill_symbol.changeSymbolLayer(0, fill) + fill.setColor(Qt.cyan) + fill.setStrokeStyle(Qt.NoPen) + item2.setSymbol(fill_symbol) + l.addItem(item2) + + exporter = QgsLayoutExporter(l) + # setup settings + settings = QgsLayoutExporter.PrintExportSettings() + settings.dpi = 80 + settings.rasterizeWholeImage = False + + pdf_file_path = os.path.join(self.basetestpath, 'test_printdpi.pdf') + # make a qprinter directed to pdf + printer = QPrinter() + printer.setOutputFileName(pdf_file_path) + printer.setOutputFormat(QPrinter.PdfFormat) + + self.assertEqual(exporter.print(printer, settings), QgsLayoutExporter.Success) + self.assertTrue(os.path.exists(pdf_file_path)) + + rendered_page_1 = os.path.join(self.basetestpath, 'test_exporttopdfdpi.png') + dpi = 80 + pdfToPng(pdf_file_path, rendered_page_1, dpi=dpi, page=1) + rendered_page_2 = os.path.join(self.basetestpath, 'test_exporttopdfdpi2.png') + pdfToPng(pdf_file_path, rendered_page_2, dpi=dpi, page=2) + + self.assertTrue(self.checkImage('printdpi_page1', 'exporttopdfdpi_page1', rendered_page_1, size_tolerance=1)) + self.assertTrue(self.checkImage('printdpi_page2', 'exporttopdfdpi_page2', rendered_page_2, size_tolerance=1)) + def testExportWorldFile(self): l = QgsLayout(QgsProject.instance()) l.initializeDefaults() @@ -727,6 +783,39 @@ def testIteratorToPdf(self): pdfToPng(pdf_path, rendered_page_4, dpi=80, page=4) self.assertTrue(os.path.exists(rendered_page_4)) + def testPrintIterator(self): + project, layout = self.prepareIteratorLayout() + atlas = layout.atlas() + + # setup settings + settings = QgsLayoutExporter.PrintExportSettings() + settings.dpi = 80 + settings.rasterizeWholeImage = False + + pdf_path = os.path.join(self.basetestpath, 'test_printiterator.pdf') + # make a qprinter directed to pdf + printer = QPrinter() + printer.setOutputFileName(pdf_path) + printer.setOutputFormat(QPrinter.PdfFormat) + + result, error = QgsLayoutExporter.print(atlas, printer, settings) + self.assertEqual(result, QgsLayoutExporter.Success, error) + + rendered_page_1 = os.path.join(self.basetestpath, 'test_printiterator1.png') + pdfToPng(pdf_path, rendered_page_1, dpi=80, page=1) + self.assertTrue(self.checkImage('printeriterator1', 'iteratortoimage1', rendered_page_1, size_tolerance=2)) + + rendered_page_2 = os.path.join(self.basetestpath, 'test_printiterator2.png') + pdfToPng(pdf_path, rendered_page_2, dpi=80, page=2) + self.assertTrue(self.checkImage('printiterator2', 'iteratortoimage2', rendered_page_2, size_tolerance=2)) + + rendered_page_3 = os.path.join(self.basetestpath, 'test_printiterator3.png') + pdfToPng(pdf_path, rendered_page_3, dpi=80, page=3) + self.assertTrue(os.path.exists(rendered_page_3)) + rendered_page_4 = os.path.join(self.basetestpath, 'test_printiterator4.png') + pdfToPng(pdf_path, rendered_page_4, dpi=80, page=4) + self.assertTrue(os.path.exists(rendered_page_4)) + def testExportReport(self): p = QgsProject() r = QgsReport(p) From 5d64f3cd2265889b3278905da4bb4556fea6337c Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 2 Jan 2018 15:30:31 +1000 Subject: [PATCH 088/105] Fix missing feature contexts for report section header/footers and expand unit tests --- .../core/layout/qgsabstractreportsection.sip | 24 ++ .../layout/qgsreportsectionfieldgroup.sip | 2 + src/core/layout/qgsabstractreportsection.cpp | 18 ++ src/core/layout/qgsabstractreportsection.h | 18 ++ .../layout/qgsreportsectionfieldgroup.cpp | 48 ++- src/core/layout/qgsreportsectionfieldgroup.h | 5 + tests/src/python/test_qgsreport.py | 283 ++++++++++++++++++ 7 files changed, 397 insertions(+), 1 deletion(-) diff --git a/python/core/layout/qgsabstractreportsection.sip b/python/core/layout/qgsabstractreportsection.sip index 56f330782a33..e23876084f31 100644 --- a/python/core/layout/qgsabstractreportsection.sip +++ b/python/core/layout/qgsabstractreportsection.sip @@ -28,6 +28,16 @@ exposed to the Python bindings for unit testing purposes only. %End public: + QgsFeature feature; +%Docstring +Current feature +%End + + QgsVectorLayer *currentLayer; +%Docstring +Current coverage layer +%End + }; class QgsAbstractReportSection : QgsAbstractLayoutIterator @@ -111,6 +121,20 @@ Returns the associated project. virtual void reset(); %Docstring Resets the section, ready for a new iteration. +%End + + virtual void prepareHeader(); +%Docstring +Called just before rendering the section's header. + +.. seealso:: :py:func:`prepareFooter()` +%End + + virtual void prepareFooter(); +%Docstring +Called just before rendering the section's footer. + +.. seealso:: :py:func:`prepareHeader()` %End virtual QgsLayout *nextBody( bool &ok /Out/ ); diff --git a/python/core/layout/qgsreportsectionfieldgroup.sip b/python/core/layout/qgsreportsectionfieldgroup.sip index 88adc6dcb610..d90da6c185da 100644 --- a/python/core/layout/qgsreportsectionfieldgroup.sip +++ b/python/core/layout/qgsreportsectionfieldgroup.sip @@ -101,6 +101,8 @@ ascending, or false for descending sort. virtual bool beginRender(); + virtual void prepareHeader(); + virtual QgsLayout *nextBody( bool &ok ); virtual void reset(); diff --git a/src/core/layout/qgsabstractreportsection.cpp b/src/core/layout/qgsabstractreportsection.cpp index 1b159aa7e6f9..c74e762ede3b 100644 --- a/src/core/layout/qgsabstractreportsection.cpp +++ b/src/core/layout/qgsabstractreportsection.cpp @@ -52,7 +52,23 @@ QgsProject *QgsAbstractReportSection::project() void QgsAbstractReportSection::setContext( const QgsReportSectionContext &context ) { + auto setReportContext = [&context]( QgsLayout * layout ) + { + if ( context.currentLayer ) + { + layout->reportContext().blockSignals( true ); + layout->reportContext().setLayer( context.currentLayer ); + layout->reportContext().blockSignals( false ); + } + layout->reportContext().setFeature( context.feature ); + }; + mContext = context; + if ( mHeader ) + setReportContext( mHeader.get() ); + if ( mFooter ) + setReportContext( mFooter.get() ); + for ( QgsAbstractReportSection *section : qgis::as_const( mChildren ) ) { section->setContext( mContext ); @@ -191,6 +207,7 @@ bool QgsAbstractReportSection::next() // if we have a header, then the current section will be the header if ( mHeaderEnabled && mHeader ) { + prepareHeader(); mCurrentLayout = mHeader.get(); return true; } @@ -271,6 +288,7 @@ bool QgsAbstractReportSection::next() // if we have a footer, then the current section will be the footer if ( mFooterEnabled && mFooter ) { + prepareFooter(); mCurrentLayout = mFooter.get(); return true; } diff --git a/src/core/layout/qgsabstractreportsection.h b/src/core/layout/qgsabstractreportsection.h index 6b1d21f59068..4509b8cc0c95 100644 --- a/src/core/layout/qgsabstractreportsection.h +++ b/src/core/layout/qgsabstractreportsection.h @@ -38,6 +38,12 @@ class CORE_EXPORT QgsReportSectionContext { public: + //! Current feature + QgsFeature feature; + + //! Current coverage layer + QgsVectorLayer *currentLayer = nullptr; + //! Current layer filters QMap< QgsVectorLayer *, QString > layerFilters SIP_SKIP; }; @@ -123,6 +129,18 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator */ virtual void reset(); + /** + * Called just before rendering the section's header. + * \see prepareFooter() + */ + virtual void prepareHeader() {} + + /** + * Called just before rendering the section's footer. + * \see prepareHeader() + */ + virtual void prepareFooter() {} + /** * Returns the next body layout to export, or a nullptr if * no body layout is required this iteration. diff --git a/src/core/layout/qgsreportsectionfieldgroup.cpp b/src/core/layout/qgsreportsectionfieldgroup.cpp index 14eb9729de6c..693188d5f9ce 100644 --- a/src/core/layout/qgsreportsectionfieldgroup.cpp +++ b/src/core/layout/qgsreportsectionfieldgroup.cpp @@ -68,6 +68,24 @@ bool QgsReportSectionFieldGroup::beginRender() return QgsAbstractReportSection::beginRender(); } +void QgsReportSectionFieldGroup::prepareHeader() +{ + if ( !header() ) + return; + + if ( !mFeatures.isValid() ) + { + mFeatures = mCoverageLayer->getFeatures( buildFeatureRequest() ); + } + + mHeaderFeature = getNextFeature(); + header()->reportContext().blockSignals( true ); + header()->reportContext().setLayer( mCoverageLayer.get() ); + header()->reportContext().blockSignals( false ); + header()->reportContext().setFeature( mHeaderFeature ); + mSkipNextRequest = true; +} + QgsLayout *QgsReportSectionFieldGroup::nextBody( bool &ok ) { if ( !mFeatures.isValid() ) @@ -75,15 +93,35 @@ QgsLayout *QgsReportSectionFieldGroup::nextBody( bool &ok ) mFeatures = mCoverageLayer->getFeatures( buildFeatureRequest() ); } - QgsFeature f = getNextFeature(); + QgsFeature f; + if ( !mSkipNextRequest ) + { + f = getNextFeature(); + } + else + { + f = mHeaderFeature; + mSkipNextRequest = false; + } + if ( !f.isValid() ) { // no features left for this iteration mFeatures = QgsFeatureIterator(); + + if ( footer() ) + { + footer()->reportContext().blockSignals( true ); + footer()->reportContext().setLayer( mCoverageLayer.get() ); + footer()->reportContext().blockSignals( false ); + footer()->reportContext().setFeature( mLastFeature ); + } ok = false; return nullptr; } + mLastFeature = f; + updateChildContexts( f ); ok = true; @@ -102,6 +140,10 @@ void QgsReportSectionFieldGroup::reset() { QgsAbstractReportSection::reset(); mEncounteredValues.clear(); + mSkipNextRequest = false; + mHeaderFeature = QgsFeature(); + mLastFeature = QgsFeature(); + mFeatures = QgsFeatureIterator(); } void QgsReportSectionFieldGroup::setParentSection( QgsAbstractReportSection *parent ) @@ -199,6 +241,10 @@ QgsFeature QgsReportSectionFieldGroup::getNextFeature() void QgsReportSectionFieldGroup::updateChildContexts( const QgsFeature &feature ) { QgsReportSectionContext c = context(); + c.feature = feature; + if ( mCoverageLayer ) + c.currentLayer = mCoverageLayer.get(); + QString currentFilter = c.layerFilters.value( mCoverageLayer.get() ); QString thisFilter = QgsExpression::createFieldEqualityExpression( mField, feature.attribute( mFieldIndex ) ); QString newFilter = currentFilter.isEmpty() ? thisFilter : QStringLiteral( "(%1) AND (%2)" ).arg( currentFilter, thisFilter ); diff --git a/src/core/layout/qgsreportsectionfieldgroup.h b/src/core/layout/qgsreportsectionfieldgroup.h index bb2e248fa866..df01219c9bb4 100644 --- a/src/core/layout/qgsreportsectionfieldgroup.h +++ b/src/core/layout/qgsreportsectionfieldgroup.h @@ -100,6 +100,8 @@ class CORE_EXPORT QgsReportSectionFieldGroup : public QgsAbstractReportSection QgsReportSectionFieldGroup *clone() const override SIP_FACTORY; bool beginRender() override; + void prepareHeader() override; + //void prepareFooter() override; QgsLayout *nextBody( bool &ok ) override; void reset() override; void setParentSection( QgsAbstractReportSection *parentSection ) override; @@ -116,6 +118,9 @@ class CORE_EXPORT QgsReportSectionFieldGroup : public QgsAbstractReportSection bool mSortAscending = true; int mFieldIndex = -1; QgsFeatureIterator mFeatures; + bool mSkipNextRequest = false; + QgsFeature mHeaderFeature; + QgsFeature mLastFeature; QSet< QVariant > mEncounteredValues; std::unique_ptr< QgsLayout > mBody; diff --git a/tests/src/python/test_qgsreport.py b/tests/src/python/test_qgsreport.py index 9c8f62e7cf82..8da0ad1bd757 100644 --- a/tests/src/python/test_qgsreport.py +++ b/tests/src/python/test_qgsreport.py @@ -439,6 +439,289 @@ def testFieldGroup(self): self.assertEqual(r.layout().reportContext().feature().attributes(), ['PNG', 'state1', 'town1']) self.assertFalse(r.next()) + # add headers/footers + child3_header = QgsLayout(p) + child3.setHeader(child3_header) + child3.setHeaderEnabled(True) + child3_footer = QgsLayout(p) + child3.setFooter(child3_footer) + child3.setFooterEnabled(True) + + self.assertTrue(r.beginRender()) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_header) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'NSW', 'Sydney']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'NSW', 'Sydney']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_footer) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'NSW', 'Sydney']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_header) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Emerald']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Emerald']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Brisbane']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Beerburrum']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_footer) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Beerburrum']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_header) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'VIC', 'Melbourne']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'VIC', 'Melbourne']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'VIC', 'Geelong']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_footer) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'VIC', 'Geelong']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_header) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state1', 'town2']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state1', 'town2']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state1', 'town1']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_footer) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state1', 'town1']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_header) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state2', 'town2']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state2', 'town2']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_footer) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state2', 'town2']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_header) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['PNG', 'state1', 'town1']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['PNG', 'state1', 'town1']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_footer) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['PNG', 'state1', 'town1']) + self.assertFalse(r.next()) + + # header/footer for section2 + child2_header = QgsLayout(p) + child2.setHeader(child2_header) + child2.setHeaderEnabled(True) + child2_footer = QgsLayout(p) + child2.setFooter(child2_footer) + child2.setFooterEnabled(True) + + self.assertTrue(r.beginRender()) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_header) + self.assertEqual(r.layout().reportContext().feature().attributes()[:2], ['Australia', 'NSW']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_header) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'NSW', 'Sydney']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'NSW', 'Sydney']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_footer) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'NSW', 'Sydney']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_header) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Emerald']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Emerald']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Brisbane']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Beerburrum']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_footer) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Beerburrum']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_header) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'VIC', 'Melbourne']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'VIC', 'Melbourne']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'VIC', 'Geelong']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_footer) + self.assertEqual(r.layout().reportContext().feature().attributes()[:2], ['Australia', 'VIC']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_footer) + self.assertEqual(r.layout().reportContext().feature().attributes()[:2], ['Australia', 'VIC']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_header) + self.assertEqual(r.layout().reportContext().feature().attributes()[:2], ['NZ', 'state1']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_header) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state1', 'town2']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state1', 'town2']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state1', 'town1']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_footer) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state1', 'town1']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_header) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state2', 'town2']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state2', 'town2']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_footer) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state2', 'town2']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_footer) + self.assertEqual(r.layout().reportContext().feature().attributes()[:2], ['NZ', 'state2']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_header) + self.assertEqual(r.layout().reportContext().feature().attributes()[:2], ['PNG', 'state1']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_header) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['PNG', 'state1', 'town1']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['PNG', 'state1', 'town1']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_footer) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['PNG', 'state1', 'town1']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_footer) + self.assertEqual(r.layout().reportContext().feature().attributes()[:2], ['PNG', 'state1']) + self.assertFalse(r.next()) + + # child 1 and report header/footer + child1_header = QgsLayout(p) + child1.setHeader(child1_header) + child1.setHeaderEnabled(True) + child1_footer = QgsLayout(p) + child1.setFooter(child1_footer) + child1.setFooterEnabled(True) + report_header = QgsLayout(p) + r.setHeader(report_header) + r.setHeaderEnabled(True) + report_footer = QgsLayout(p) + r.setFooter(report_footer) + r.setFooterEnabled(True) + + self.assertTrue(r.beginRender()) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), report_header) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child1_header) + self.assertEqual(r.layout().reportContext().feature().attributes()[:1], ['Australia']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_header) + self.assertEqual(r.layout().reportContext().feature().attributes()[:2], ['Australia', 'NSW']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_header) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'NSW', 'Sydney']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'NSW', 'Sydney']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_footer) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'NSW', 'Sydney']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_header) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Emerald']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Emerald']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Brisbane']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Beerburrum']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_footer) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'QLD', 'Beerburrum']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_header) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'VIC', 'Melbourne']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'VIC', 'Melbourne']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'VIC', 'Geelong']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_footer) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['Australia', 'VIC', 'Geelong']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_footer) + self.assertEqual(r.layout().reportContext().feature().attributes()[:2], ['Australia', 'VIC']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_header) + self.assertEqual(r.layout().reportContext().feature().attributes()[:2], ['NZ', 'state1']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_header) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state1', 'town2']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state1', 'town2']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state1', 'town1']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_footer) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state1', 'town1']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_header) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state2', 'town2']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state2', 'town2']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_footer) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['NZ', 'state2', 'town2']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_footer) + self.assertEqual(r.layout().reportContext().feature().attributes()[:2], ['NZ', 'state2']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_header) + self.assertEqual(r.layout().reportContext().feature().attributes()[:2], ['PNG', 'state1']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_header) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['PNG', 'state1', 'town1']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_body) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['PNG', 'state1', 'town1']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child3_footer) + self.assertEqual(r.layout().reportContext().feature().attributes(), ['PNG', 'state1', 'town1']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child2_footer) + self.assertEqual(r.layout().reportContext().feature().attributes()[:2], ['PNG', 'state1']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), child1_footer) + self.assertEqual(r.layout().reportContext().feature().attributes()[:1], ['PNG']) + self.assertTrue(r.next()) + self.assertEqual(r.layout(), report_footer) + self.assertFalse(r.next()) + def testReadWriteXml(self): p = QgsProject() ptLayer = QgsVectorLayer("Point?crs=epsg:4326&field=country:string(20)&field=state:string(20)&field=town:string(20)", "points", "memory") From d2c880af50ca83748089fb4896d95028922b8327 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 2 Jan 2018 16:00:02 +1000 Subject: [PATCH 089/105] Use 'report' text instead of 'atlas' when inside a report designer --- python/gui/layout/qgslayoutitemwidget.sip | 8 ++++++++ src/app/layout/qgslayoutattributetablewidget.cpp | 5 +++++ src/app/layout/qgslayoutattributetablewidget.h | 2 ++ src/app/layout/qgslayoutdesignerdialog.cpp | 9 +++++++++ src/app/layout/qgslayoutdesignerdialog.h | 1 + src/app/layout/qgslayoutlegendwidget.cpp | 6 ++++++ src/app/layout/qgslayoutlegendwidget.h | 1 + src/app/layout/qgslayoutmapwidget.cpp | 6 ++++++ src/app/layout/qgslayoutmapwidget.h | 2 ++ src/gui/layout/qgslayoutitemwidget.cpp | 4 ++++ src/gui/layout/qgslayoutitemwidget.h | 8 ++++++++ 11 files changed, 52 insertions(+) diff --git a/python/gui/layout/qgslayoutitemwidget.sip b/python/gui/layout/qgslayoutitemwidget.sip index fa884256bef9..d217ef4352ca 100644 --- a/python/gui/layout/qgslayoutitemwidget.sip +++ b/python/gui/layout/qgslayoutitemwidget.sip @@ -90,6 +90,14 @@ updated to match ``item``'s properties. If false is returned, then the widget could not be successfully updated to show the properties of ``item``. +%End + + virtual void setReportTypeString( const QString &string ); +%Docstring +Sets the ``string`` to use to describe the current report type (e.g. +"atlas" or "report"). +Subclasses which display this text to users should override this +and update their widget labels accordingly. %End protected: diff --git a/src/app/layout/qgslayoutattributetablewidget.cpp b/src/app/layout/qgslayoutattributetablewidget.cpp index de82929402d4..6fc53b38496e 100644 --- a/src/app/layout/qgslayoutattributetablewidget.cpp +++ b/src/app/layout/qgslayoutattributetablewidget.cpp @@ -145,6 +145,11 @@ QgsLayoutAttributeTableWidget::QgsLayoutAttributeTableWidget( QgsLayoutFrame *fr connect( mContentFontToolButton, &QgsFontButton::changed, this, &QgsLayoutAttributeTableWidget::contentFontChanged ); } +void QgsLayoutAttributeTableWidget::setReportTypeString( const QString &string ) +{ + mIntersectAtlasCheckBox->setText( tr( "Show only features intersecting %1 feature" ).arg( string ) ); +} + bool QgsLayoutAttributeTableWidget::setNewItem( QgsLayoutItem *item ) { QgsLayoutFrame *frame = qobject_cast< QgsLayoutFrame * >( item ); diff --git a/src/app/layout/qgslayoutattributetablewidget.h b/src/app/layout/qgslayoutattributetablewidget.h index c2f8b4bb206b..66569b1b937e 100644 --- a/src/app/layout/qgslayoutattributetablewidget.h +++ b/src/app/layout/qgslayoutattributetablewidget.h @@ -30,6 +30,8 @@ class QgsLayoutAttributeTableWidget: public QgsLayoutItemBaseWidget, private Ui: public: QgsLayoutAttributeTableWidget( QgsLayoutFrame *frame ); + void setReportTypeString( const QString &string ) override; + protected: bool setNewItem( QgsLayoutItem *item ) override; diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index e95860cab69f..10834b02af05 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -889,6 +889,7 @@ void QgsLayoutDesignerDialog::showItemOptions( QgsLayoutItem *item, bool bringPa if ( ! widget ) return; + widget->setReportTypeString( reportTypeString() ); if ( QgsLayoutPagePropertiesWidget *ppWidget = qobject_cast< QgsLayoutPagePropertiesWidget * >( widget.get() ) ) connect( ppWidget, &QgsLayoutPagePropertiesWidget::pageOrientationChanged, this, &QgsLayoutDesignerDialog::pageOrientationChanged ); @@ -3961,6 +3962,14 @@ QPrinter *QgsLayoutDesignerDialog::printer() return mPrinter.get(); } +QString QgsLayoutDesignerDialog::reportTypeString() +{ + if ( atlas() ) + return tr( "atlas" ); + else + return tr( "report" ); +} + void QgsLayoutDesignerDialog::selectItems( const QList items ) { for ( QGraphicsItem *item : items ) diff --git a/src/app/layout/qgslayoutdesignerdialog.h b/src/app/layout/qgslayoutdesignerdialog.h index 74e7f60d7e85..9b47f3c5106a 100644 --- a/src/app/layout/qgslayoutdesignerdialog.h +++ b/src/app/layout/qgslayoutdesignerdialog.h @@ -478,6 +478,7 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner void setPrinterPageOrientation( QgsLayoutItemPage::Orientation orientation ); QPrinter *printer(); + QString reportTypeString(); }; #endif // QGSLAYOUTDESIGNERDIALOG_H diff --git a/src/app/layout/qgslayoutlegendwidget.cpp b/src/app/layout/qgslayoutlegendwidget.cpp index d81c9477ad18..cbf4e74044c5 100644 --- a/src/app/layout/qgslayoutlegendwidget.cpp +++ b/src/app/layout/qgslayoutlegendwidget.cpp @@ -922,6 +922,12 @@ void QgsLayoutLegendWidget::updateLegend() } } +void QgsLayoutLegendWidget::setReportTypeString( const QString &string ) +{ + mFilterLegendByAtlasCheckBox->setText( tr( "Only show items inside current %1 feature" ).arg( string ) ); + mFilterLegendByAtlasCheckBox->setToolTip( tr( "Filter out legend elements that lie outside the current %1 feature." ).arg( string ) ); +} + bool QgsLayoutLegendWidget::setNewItem( QgsLayoutItem *item ) { if ( item->type() != QgsLayoutItemRegistry::LayoutLegend ) diff --git a/src/app/layout/qgslayoutlegendwidget.h b/src/app/layout/qgslayoutlegendwidget.h index ecf17fcd824e..7e9f0b5b6f19 100644 --- a/src/app/layout/qgslayoutlegendwidget.h +++ b/src/app/layout/qgslayoutlegendwidget.h @@ -39,6 +39,7 @@ class QgsLayoutLegendWidget: public QgsLayoutItemBaseWidget, private Ui::QgsLayo void updateLegend(); QgsLayoutItemLegend *legend() { return mLegend; } + void setReportTypeString( const QString &string ) override; protected: diff --git a/src/app/layout/qgslayoutmapwidget.cpp b/src/app/layout/qgslayoutmapwidget.cpp index 607c6ece220a..8746c344af14 100644 --- a/src/app/layout/qgslayoutmapwidget.cpp +++ b/src/app/layout/qgslayoutmapwidget.cpp @@ -155,6 +155,12 @@ QgsLayoutMapWidget::QgsLayoutMapWidget( QgsLayoutItemMap *item ) blockAllSignals( false ); } +void QgsLayoutMapWidget::setReportTypeString( const QString &string ) +{ + mAtlasCheckBox->setTitle( tr( "Controlled by %1" ).arg( string ) ); + mAtlasPredefinedScaleRadio->setToolTip( tr( "Use one of the predefined scales of the project where the %1 feature best fits." ).arg( string ) ); +} + bool QgsLayoutMapWidget::setNewItem( QgsLayoutItem *item ) { if ( item->type() != QgsLayoutItemRegistry::LayoutMap ) diff --git a/src/app/layout/qgslayoutmapwidget.h b/src/app/layout/qgslayoutmapwidget.h index 14f0038903f9..afd189099c76 100644 --- a/src/app/layout/qgslayoutmapwidget.h +++ b/src/app/layout/qgslayoutmapwidget.h @@ -37,6 +37,8 @@ class QgsLayoutMapWidget: public QgsLayoutItemBaseWidget, private Ui::QgsLayoutM public: explicit QgsLayoutMapWidget( QgsLayoutItemMap *item ); + void setReportTypeString( const QString &string ) override; + public slots: void mScaleLineEdit_editingFinished(); void mSetToMapCanvasExtentButton_clicked(); diff --git a/src/gui/layout/qgslayoutitemwidget.cpp b/src/gui/layout/qgslayoutitemwidget.cpp index b5bbd58f6f13..f5512d2edb67 100644 --- a/src/gui/layout/qgslayoutitemwidget.cpp +++ b/src/gui/layout/qgslayoutitemwidget.cpp @@ -154,6 +154,10 @@ bool QgsLayoutItemBaseWidget::setItem( QgsLayoutItem *item ) return false; } +void QgsLayoutItemBaseWidget::setReportTypeString( const QString & ) +{ +} + void QgsLayoutItemBaseWidget::registerDataDefinedButton( QgsPropertyOverrideButton *button, QgsLayoutObject::DataDefinedProperty property ) { mConfigObject->initializeDataDefinedButton( button, property ); diff --git a/src/gui/layout/qgslayoutitemwidget.h b/src/gui/layout/qgslayoutitemwidget.h index 248363f7b0b0..453df6e1d6f6 100644 --- a/src/gui/layout/qgslayoutitemwidget.h +++ b/src/gui/layout/qgslayoutitemwidget.h @@ -134,6 +134,14 @@ class GUI_EXPORT QgsLayoutItemBaseWidget: public QgsPanelWidget */ bool setItem( QgsLayoutItem *item ); + /** + * Sets the \a string to use to describe the current report type (e.g. + * "atlas" or "report"). + * Subclasses which display this text to users should override this + * and update their widget labels accordingly. + */ + virtual void setReportTypeString( const QString &string ); + protected: /** From 51a7efbe4b5f1d940d55f8baa1c7df82ec371385 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 2 Jan 2018 16:33:19 +1000 Subject: [PATCH 090/105] Nicer ui widgets for sections, add missing control for disabling body sections --- .../layout/qgsreportsectionfieldgroup.sip | 31 +++++++ python/core/layout/qgsreportsectionlayout.sip | 30 +++++++ src/app/CMakeLists.txt | 2 + .../qgsreportfieldgroupsectionwidget.cpp | 56 ++++++++++++ .../layout/qgsreportfieldgroupsectionwidget.h | 5 ++ .../layout/qgsreportlayoutsectionwidget.cpp | 55 ++++++++++++ src/app/layout/qgsreportlayoutsectionwidget.h | 5 ++ src/app/layout/qgsreportorganizerwidget.cpp | 62 ++----------- src/app/layout/qgsreportorganizerwidget.h | 4 - src/app/layout/qgsreportsectionwidget.cpp | 78 +++++++++++++++++ src/app/layout/qgsreportsectionwidget.h | 45 ++++++++++ .../layout/qgsreportsectionfieldgroup.cpp | 13 +-- src/core/layout/qgsreportsectionfieldgroup.h | 22 +++++ src/core/layout/qgsreportsectionlayout.cpp | 6 +- src/core/layout/qgsreportsectionlayout.h | 22 ++++- src/ui/layout/qgsreportorganizerwidgetbase.ui | 45 ---------- .../qgsreportwidgetfieldgroupsectionbase.ui | 86 ++++++++++++------- .../qgsreportwidgetlayoutsectionbase.ui | 50 ++++++++--- src/ui/layout/qgsreportwidgetsectionbase.ui | 79 +++++++++++++++++ 19 files changed, 543 insertions(+), 153 deletions(-) create mode 100644 src/app/layout/qgsreportsectionwidget.cpp create mode 100644 src/app/layout/qgsreportsectionwidget.h create mode 100644 src/ui/layout/qgsreportwidgetsectionbase.ui diff --git a/python/core/layout/qgsreportsectionfieldgroup.sip b/python/core/layout/qgsreportsectionfieldgroup.sip index d90da6c185da..7164de4d3774 100644 --- a/python/core/layout/qgsreportsectionfieldgroup.sip +++ b/python/core/layout/qgsreportsectionfieldgroup.sip @@ -43,6 +43,10 @@ Note that ownership is not transferred to ``parent``. Returns the body layout for the section. .. seealso:: :py:func:`setBody()` + +.. seealso:: :py:func:`bodyEnabled()` + +.. seealso:: :py:func:`setBodyEnabled()` %End void setBody( QgsLayout *body /Transfer/ ); @@ -51,8 +55,35 @@ Sets the ``body`` layout for the section. Ownership of ``body`` is transferred to the report section. .. seealso:: :py:func:`body()` + +.. seealso:: :py:func:`bodyEnabled()` + +.. seealso:: :py:func:`setBodyEnabled()` %End + bool bodyEnabled() const; +%Docstring +Returns true if the body for the section is enabled. + +.. seealso:: :py:func:`setBodyEnabled()` + +.. seealso:: :py:func:`body()` + +.. seealso:: :py:func:`setBody()` +%End + + void setBodyEnabled( bool enabled ); +%Docstring +Sets whether the body for the section is ``enabled``. + +.. seealso:: :py:func:`bodyEnabled()` + +.. seealso:: :py:func:`body()` + +.. seealso:: :py:func:`setBody()` +%End + + QgsVectorLayer *layer(); %Docstring Returns the vector layer associated with this section. diff --git a/python/core/layout/qgsreportsectionlayout.sip b/python/core/layout/qgsreportsectionlayout.sip index 61992e7089cf..60c018c4786d 100644 --- a/python/core/layout/qgsreportsectionlayout.sip +++ b/python/core/layout/qgsreportsectionlayout.sip @@ -41,6 +41,10 @@ Note that ownership is not transferred to ``parent``. Returns the body layout for the section. .. seealso:: :py:func:`setBody()` + +.. seealso:: :py:func:`bodyEnabled()` + +.. seealso:: :py:func:`setBodyEnabled()` %End void setBody( QgsLayout *body /Transfer/ ); @@ -49,6 +53,32 @@ Sets the ``body`` layout for the section. Ownership of ``body`` is transferred to the report section. .. seealso:: :py:func:`body()` + +.. seealso:: :py:func:`bodyEnabled()` + +.. seealso:: :py:func:`setBodyEnabled()` +%End + + bool bodyEnabled() const; +%Docstring +Returns true if the body for the section is enabled. + +.. seealso:: :py:func:`setBodyEnabled()` + +.. seealso:: :py:func:`body()` + +.. seealso:: :py:func:`setBody()` +%End + + void setBodyEnabled( bool enabled ); +%Docstring +Sets whether the body for the section is ``enabled``. + +.. seealso:: :py:func:`bodyEnabled()` + +.. seealso:: :py:func:`body()` + +.. seealso:: :py:func:`setBody()` %End virtual QgsReportSectionLayout *clone() const /Factory/; diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index a59758086e9a..a246eeb0c91c 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -208,6 +208,7 @@ SET(QGIS_APP_SRCS layout/qgsreportlayoutsectionwidget.cpp layout/qgsreportorganizerwidget.cpp layout/qgsreportsectionmodel.cpp + layout/qgsreportsectionwidget.cpp locator/qgsinbuiltlocatorfilters.cpp locator/qgslocatoroptionswidget.cpp @@ -431,6 +432,7 @@ SET (QGIS_APP_MOC_HDRS layout/qgsreportlayoutsectionwidget.h layout/qgsreportorganizerwidget.h layout/qgsreportsectionmodel.h + layout/qgsreportsectionwidget.h locator/qgsinbuiltlocatorfilters.h locator/qgslocatoroptionswidget.h diff --git a/src/app/layout/qgsreportfieldgroupsectionwidget.cpp b/src/app/layout/qgsreportfieldgroupsectionwidget.cpp index 64e055564769..32306bca7b4c 100644 --- a/src/app/layout/qgsreportfieldgroupsectionwidget.cpp +++ b/src/app/layout/qgsreportfieldgroupsectionwidget.cpp @@ -29,14 +29,70 @@ QgsReportSectionFieldGroupWidget::QgsReportSectionFieldGroupWidget( QWidget *par mLayerComboBox->setFilters( QgsMapLayerProxyModel::VectorLayer ); connect( mLayerComboBox, &QgsMapLayerComboBox::layerChanged, mFieldComboBox, &QgsFieldComboBox::setLayer ); connect( mButtonEditBody, &QPushButton::clicked, this, &QgsReportSectionFieldGroupWidget::editBody ); + connect( mButtonEditHeader, &QPushButton::clicked, this, &QgsReportSectionFieldGroupWidget::editHeader ); + connect( mButtonEditFooter, &QPushButton::clicked, this, &QgsReportSectionFieldGroupWidget::editFooter ); mLayerComboBox->setLayer( section->layer() ); mFieldComboBox->setField( section->field() ); mSortAscendingCheckBox->setChecked( section->sortAscending() ); + mCheckShowHeader->setChecked( section->headerEnabled() ); + mCheckShowFooter->setChecked( section->footerEnabled() ); + mCheckShowBody->setChecked( section->bodyEnabled() ); + connect( mSortAscendingCheckBox, &QCheckBox::toggled, this, &QgsReportSectionFieldGroupWidget::sortAscendingToggled ); connect( mLayerComboBox, &QgsMapLayerComboBox::layerChanged, this, &QgsReportSectionFieldGroupWidget::setLayer ); connect( mFieldComboBox, &QgsFieldComboBox::fieldChanged, this, &QgsReportSectionFieldGroupWidget::setField ); + connect( mCheckShowHeader, &QCheckBox::toggled, this, &QgsReportSectionFieldGroupWidget::toggleHeader ); + connect( mCheckShowFooter, &QCheckBox::toggled, this, &QgsReportSectionFieldGroupWidget::toggleFooter ); + connect( mCheckShowBody, &QCheckBox::toggled, this, &QgsReportSectionFieldGroupWidget::toggleBody ); +} + +void QgsReportSectionFieldGroupWidget::toggleHeader( bool enabled ) +{ + mSection->setHeaderEnabled( enabled ); +} + +void QgsReportSectionFieldGroupWidget::toggleFooter( bool enabled ) +{ + mSection->setFooterEnabled( enabled ); +} + +void QgsReportSectionFieldGroupWidget::editHeader() +{ + if ( !mSection->header() ) + { + std::unique_ptr< QgsLayout > header = qgis::make_unique< QgsLayout >( mSection->project() ); + header->initializeDefaults(); + mSection->setHeader( header.release() ); + } + + if ( mSection->header() ) + { + mSection->header()->reportContext().setLayer( mSection->layer() ); + mDesigner->setCurrentLayout( mSection->header() ); + } +} + +void QgsReportSectionFieldGroupWidget::editFooter() +{ + if ( !mSection->footer() ) + { + std::unique_ptr< QgsLayout > footer = qgis::make_unique< QgsLayout >( mSection->project() ); + footer->initializeDefaults(); + mSection->setFooter( footer.release() ); + } + + if ( mSection->footer() ) + { + mSection->footer()->reportContext().setLayer( mSection->layer() ); + mDesigner->setCurrentLayout( mSection->footer() ); + } +} + +void QgsReportSectionFieldGroupWidget::toggleBody( bool enabled ) +{ + mSection->setBodyEnabled( enabled ); } void QgsReportSectionFieldGroupWidget::editBody() diff --git a/src/app/layout/qgsreportfieldgroupsectionwidget.h b/src/app/layout/qgsreportfieldgroupsectionwidget.h index e75c3f28f3d2..bbc3d730819c 100644 --- a/src/app/layout/qgsreportfieldgroupsectionwidget.h +++ b/src/app/layout/qgsreportfieldgroupsectionwidget.h @@ -30,6 +30,11 @@ class QgsReportSectionFieldGroupWidget: public QWidget, private Ui::QgsReportWid private slots: + void toggleHeader( bool enabled ); + void toggleFooter( bool enabled ); + void editHeader(); + void editFooter(); + void toggleBody( bool enabled ); void editBody(); void sortAscendingToggled( bool checked ); void setLayer( QgsMapLayer *layer ); diff --git a/src/app/layout/qgsreportlayoutsectionwidget.cpp b/src/app/layout/qgsreportlayoutsectionwidget.cpp index 946e55b26e9c..002a310708f6 100644 --- a/src/app/layout/qgsreportlayoutsectionwidget.cpp +++ b/src/app/layout/qgsreportlayoutsectionwidget.cpp @@ -27,6 +27,61 @@ QgsReportLayoutSectionWidget::QgsReportLayoutSectionWidget( QWidget *parent, Qgs setupUi( this ); connect( mButtonEditBody, &QPushButton::clicked, this, &QgsReportLayoutSectionWidget::editBody ); + connect( mButtonEditHeader, &QPushButton::clicked, this, &QgsReportLayoutSectionWidget::editHeader ); + connect( mButtonEditFooter, &QPushButton::clicked, this, &QgsReportLayoutSectionWidget::editFooter ); + + mCheckShowHeader->setChecked( section->headerEnabled() ); + mCheckShowFooter->setChecked( section->footerEnabled() ); + mCheckShowBody->setChecked( section->bodyEnabled() ); + + connect( mCheckShowHeader, &QCheckBox::toggled, this, &QgsReportLayoutSectionWidget::toggleHeader ); + connect( mCheckShowFooter, &QCheckBox::toggled, this, &QgsReportLayoutSectionWidget::toggleFooter ); + connect( mCheckShowBody, &QCheckBox::toggled, this, &QgsReportLayoutSectionWidget::toggleBody ); +} + +void QgsReportLayoutSectionWidget::toggleHeader( bool enabled ) +{ + mSection->setHeaderEnabled( enabled ); +} + +void QgsReportLayoutSectionWidget::toggleFooter( bool enabled ) +{ + mSection->setFooterEnabled( enabled ); +} + +void QgsReportLayoutSectionWidget::editHeader() +{ + if ( !mSection->header() ) + { + std::unique_ptr< QgsLayout > header = qgis::make_unique< QgsLayout >( mSection->project() ); + header->initializeDefaults(); + mSection->setHeader( header.release() ); + } + + if ( mSection->header() ) + { + mDesigner->setCurrentLayout( mSection->header() ); + } +} + +void QgsReportLayoutSectionWidget::editFooter() +{ + if ( !mSection->footer() ) + { + std::unique_ptr< QgsLayout > footer = qgis::make_unique< QgsLayout >( mSection->project() ); + footer->initializeDefaults(); + mSection->setFooter( footer.release() ); + } + + if ( mSection->footer() ) + { + mDesigner->setCurrentLayout( mSection->footer() ); + } +} + +void QgsReportLayoutSectionWidget::toggleBody( bool enabled ) +{ + mSection->setBodyEnabled( enabled ); } void QgsReportLayoutSectionWidget::editBody() diff --git a/src/app/layout/qgsreportlayoutsectionwidget.h b/src/app/layout/qgsreportlayoutsectionwidget.h index d1d9c40cafda..82aa8bdb6897 100644 --- a/src/app/layout/qgsreportlayoutsectionwidget.h +++ b/src/app/layout/qgsreportlayoutsectionwidget.h @@ -30,6 +30,11 @@ class QgsReportLayoutSectionWidget: public QWidget, private Ui::QgsReportWidgetL private slots: + void toggleHeader( bool enabled ); + void toggleFooter( bool enabled ); + void editHeader(); + void editFooter(); + void toggleBody( bool enabled ); void editBody(); private: diff --git a/src/app/layout/qgsreportorganizerwidget.cpp b/src/app/layout/qgsreportorganizerwidget.cpp index d800b1367805..9a7510d93038 100644 --- a/src/app/layout/qgsreportorganizerwidget.cpp +++ b/src/app/layout/qgsreportorganizerwidget.cpp @@ -22,6 +22,7 @@ #include "qgslayout.h" #include "qgslayoutdesignerdialog.h" #include "qgsreportlayoutsectionwidget.h" +#include "qgsreportsectionwidget.h" #include "qgsreportfieldgroupsectionwidget.h" #include #include @@ -61,10 +62,6 @@ QgsReportOrganizerWidget::QgsReportOrganizerWidget( QWidget *parent, QgsLayoutDe addMenu->addAction( fieldGroupSection ); connect( fieldGroupSection, &QAction::triggered, this, &QgsReportOrganizerWidget::addFieldGroupSection ); - connect( mCheckShowHeader, &QCheckBox::toggled, this, &QgsReportOrganizerWidget::toggleHeader ); - connect( mCheckShowFooter, &QCheckBox::toggled, this, &QgsReportOrganizerWidget::toggleFooter ); - connect( mButtonEditHeader, &QPushButton::clicked, this, &QgsReportOrganizerWidget::editHeader ); - connect( mButtonEditFooter, &QPushButton::clicked, this, &QgsReportOrganizerWidget::editFooter ); connect( mViewSections->selectionModel(), &QItemSelectionModel::currentChanged, this, &QgsReportOrganizerWidget::selectionChanged ); mButtonAddSection->setMenu( addMenu ); @@ -109,63 +106,12 @@ void QgsReportOrganizerWidget::removeSection() mSectionModel->removeRow( mViewSections->currentIndex().row(), mViewSections->currentIndex().parent() ); } -void QgsReportOrganizerWidget::toggleHeader( bool enabled ) -{ - QgsAbstractReportSection *parent = mSectionModel->sectionForIndex( mViewSections->currentIndex() ); - if ( !parent ) - parent = mReport; - parent->setHeaderEnabled( enabled ); -} - -void QgsReportOrganizerWidget::toggleFooter( bool enabled ) -{ - QgsAbstractReportSection *parent = mSectionModel->sectionForIndex( mViewSections->currentIndex() ); - if ( !parent ) - parent = mReport; - parent->setFooterEnabled( enabled ); -} - -void QgsReportOrganizerWidget::editHeader() -{ - QgsAbstractReportSection *parent = mSectionModel->sectionForIndex( mViewSections->currentIndex() ); - if ( !parent ) - parent = mReport; - - if ( !parent->header() ) - { - std::unique_ptr< QgsLayout > header = qgis::make_unique< QgsLayout >( mReport->layoutProject() ); - header->initializeDefaults(); - parent->setHeader( header.release() ); - } - - mDesigner->setCurrentLayout( parent->header() ); -} - -void QgsReportOrganizerWidget::editFooter() -{ - QgsAbstractReportSection *parent = mSectionModel->sectionForIndex( mViewSections->currentIndex() ); - if ( !parent ) - parent = mReport; - - if ( !parent->footer() ) - { - std::unique_ptr< QgsLayout > footer = qgis::make_unique< QgsLayout >( mReport->layoutProject() ); - footer->initializeDefaults(); - parent->setFooter( footer.release() ); - } - - mDesigner->setCurrentLayout( parent->footer() ); -} - void QgsReportOrganizerWidget::selectionChanged( const QModelIndex ¤t, const QModelIndex & ) { QgsAbstractReportSection *parent = mSectionModel->sectionForIndex( current ); if ( !parent ) parent = mReport; - whileBlocking( mCheckShowHeader )->setChecked( parent->headerEnabled() ); - whileBlocking( mCheckShowFooter )->setChecked( parent->footerEnabled() ); - delete mConfigWidget; if ( QgsReportSectionLayout *section = dynamic_cast< QgsReportSectionLayout * >( parent ) ) { @@ -179,6 +125,12 @@ void QgsReportOrganizerWidget::selectionChanged( const QModelIndex ¤t, con mSettingsFrame->layout()->addWidget( widget ); mConfigWidget = widget; } + else if ( QgsReport *section = dynamic_cast< QgsReport * >( parent ) ) + { + QgsReportSectionWidget *widget = new QgsReportSectionWidget( this, mDesigner, section ); + mSettingsFrame->layout()->addWidget( widget ); + mConfigWidget = widget; + } else { mConfigWidget = nullptr; diff --git a/src/app/layout/qgsreportorganizerwidget.h b/src/app/layout/qgsreportorganizerwidget.h index 94788bfd41df..7184d9a7361e 100644 --- a/src/app/layout/qgsreportorganizerwidget.h +++ b/src/app/layout/qgsreportorganizerwidget.h @@ -39,10 +39,6 @@ class QgsReportOrganizerWidget: public QgsPanelWidget, private Ui::QgsReportOrga void addLayoutSection(); void addFieldGroupSection(); void removeSection(); - void toggleHeader( bool enabled ); - void toggleFooter( bool enabled ); - void editHeader(); - void editFooter(); void selectionChanged( const QModelIndex ¤t, const QModelIndex &previous ); private: diff --git a/src/app/layout/qgsreportsectionwidget.cpp b/src/app/layout/qgsreportsectionwidget.cpp new file mode 100644 index 000000000000..237e3bf868c9 --- /dev/null +++ b/src/app/layout/qgsreportsectionwidget.cpp @@ -0,0 +1,78 @@ +/*************************************************************************** + qgsreportsectionwidget.cpp + ------------------------ + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsreportsectionwidget.h" +#include "qgsreport.h" +#include "qgslayout.h" +#include "qgslayoutdesignerdialog.h" + +QgsReportSectionWidget::QgsReportSectionWidget( QWidget *parent, QgsLayoutDesignerDialog *designer, QgsReport *section ) + : QWidget( parent ) + , mSection( section ) + , mDesigner( designer ) +{ + setupUi( this ); + + connect( mButtonEditHeader, &QPushButton::clicked, this, &QgsReportSectionWidget::editHeader ); + connect( mButtonEditFooter, &QPushButton::clicked, this, &QgsReportSectionWidget::editFooter ); + + mCheckShowHeader->setChecked( section->headerEnabled() ); + mCheckShowFooter->setChecked( section->footerEnabled() ); + + connect( mCheckShowHeader, &QCheckBox::toggled, this, &QgsReportSectionWidget::toggleHeader ); + connect( mCheckShowFooter, &QCheckBox::toggled, this, &QgsReportSectionWidget::toggleFooter ); +} + +void QgsReportSectionWidget::toggleHeader( bool enabled ) +{ + mSection->setHeaderEnabled( enabled ); +} + +void QgsReportSectionWidget::toggleFooter( bool enabled ) +{ + mSection->setFooterEnabled( enabled ); +} + +void QgsReportSectionWidget::editHeader() +{ + if ( !mSection->header() ) + { + std::unique_ptr< QgsLayout > header = qgis::make_unique< QgsLayout >( mSection->project() ); + header->initializeDefaults(); + mSection->setHeader( header.release() ); + } + + if ( mSection->header() ) + { + mDesigner->setCurrentLayout( mSection->header() ); + } +} + +void QgsReportSectionWidget::editFooter() +{ + if ( !mSection->footer() ) + { + std::unique_ptr< QgsLayout > footer = qgis::make_unique< QgsLayout >( mSection->project() ); + footer->initializeDefaults(); + mSection->setFooter( footer.release() ); + } + + if ( mSection->footer() ) + { + mDesigner->setCurrentLayout( mSection->footer() ); + } +} + diff --git a/src/app/layout/qgsreportsectionwidget.h b/src/app/layout/qgsreportsectionwidget.h new file mode 100644 index 000000000000..e2c99bba35a2 --- /dev/null +++ b/src/app/layout/qgsreportsectionwidget.h @@ -0,0 +1,45 @@ +/*************************************************************************** + qgsreportsectionwidget.h + ---------------------- + begin : December 2017 + copyright : (C) 2017 by Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSREPORTSECTIONWIDGET_H +#define QGSREPORTSECTIONWIDGET_H + +#include "ui_qgsreportwidgetsectionbase.h" + +class QgsLayoutDesignerDialog; +class QgsReport; + +class QgsReportSectionWidget: public QWidget, private Ui::QgsReportWidgetSectionBase +{ + Q_OBJECT + public: + QgsReportSectionWidget( QWidget *parent, QgsLayoutDesignerDialog *designer, QgsReport *section ); + + private slots: + + void toggleHeader( bool enabled ); + void toggleFooter( bool enabled ); + void editHeader(); + void editFooter(); + + private: + + QgsReport *mSection = nullptr; + QgsLayoutDesignerDialog *mDesigner = nullptr; + +}; + +#endif // QGSREPORTSECTIONWIDGET_H diff --git a/src/core/layout/qgsreportsectionfieldgroup.cpp b/src/core/layout/qgsreportsectionfieldgroup.cpp index 693188d5f9ce..460dd9f572e7 100644 --- a/src/core/layout/qgsreportsectionfieldgroup.cpp +++ b/src/core/layout/qgsreportsectionfieldgroup.cpp @@ -45,6 +45,7 @@ QgsReportSectionFieldGroup *QgsReportSectionFieldGroup::clone() const copy->setLayer( mCoverageLayer.get() ); copy->setField( mField ); copy->setSortAscending( mSortAscending ); + copy->setBodyEnabled( mBodyEnabled ); return copy.release(); } @@ -124,8 +125,8 @@ QgsLayout *QgsReportSectionFieldGroup::nextBody( bool &ok ) updateChildContexts( f ); - ok = true; - if ( mBody ) + ok = mBodyEnabled; + if ( mBody && mBodyEnabled ) { mBody->reportContext().blockSignals( true ); mBody->reportContext().setLayer( mCoverageLayer.get() ); @@ -133,7 +134,7 @@ QgsLayout *QgsReportSectionFieldGroup::nextBody( bool &ok ) mBody->reportContext().setFeature( f ); } - return mBody.get(); + return mBodyEnabled ? mBody.get() : nullptr; } void QgsReportSectionFieldGroup::reset() @@ -157,7 +158,7 @@ bool QgsReportSectionFieldGroup::writePropertiesToElement( QDomElement &element, { element.setAttribute( QStringLiteral( "field" ), mField ); element.setAttribute( QStringLiteral( "ascending" ), mSortAscending ? "1" : "0" ); - + element.setAttribute( QStringLiteral( "bodyEnabled" ), mBodyEnabled ? "1" : "0" ); if ( mCoverageLayer ) { element.setAttribute( QStringLiteral( "coverageLayer" ), mCoverageLayer.layerId ); @@ -179,7 +180,7 @@ bool QgsReportSectionFieldGroup::readPropertiesFromElement( const QDomElement &e { mField = element.attribute( QStringLiteral( "field" ) ); mSortAscending = element.attribute( QStringLiteral( "ascending" ) ).toInt(); - + mBodyEnabled = element.attribute( QStringLiteral( "bodyEnabled" ) ).toInt(); QString layerId = element.attribute( QStringLiteral( "coverageLayer" ) ); QString layerName = element.attribute( QStringLiteral( "coverageLayerName" ) ); QString layerSource = element.attribute( QStringLiteral( "coverageLayerSource" ) ); @@ -223,7 +224,7 @@ QgsFeature QgsReportSectionFieldGroup::getNextFeature() QgsFeature f; QVariant currentValue; bool first = true; - while ( first || ( !mBody && mEncounteredValues.contains( currentValue ) ) ) + while ( first || ( ( !mBody || !mBodyEnabled ) && mEncounteredValues.contains( currentValue ) ) ) { if ( !mFeatures.nextFeature( f ) ) { diff --git a/src/core/layout/qgsreportsectionfieldgroup.h b/src/core/layout/qgsreportsectionfieldgroup.h index df01219c9bb4..01fdde4fd650 100644 --- a/src/core/layout/qgsreportsectionfieldgroup.h +++ b/src/core/layout/qgsreportsectionfieldgroup.h @@ -50,6 +50,8 @@ class CORE_EXPORT QgsReportSectionFieldGroup : public QgsAbstractReportSection /** * Returns the body layout for the section. * \see setBody() + * \see bodyEnabled() + * \see setBodyEnabled() */ QgsLayout *body() { return mBody.get(); } @@ -57,9 +59,28 @@ class CORE_EXPORT QgsReportSectionFieldGroup : public QgsAbstractReportSection * Sets the \a body layout for the section. Ownership of \a body * is transferred to the report section. * \see body() + * \see bodyEnabled() + * \see setBodyEnabled() */ void setBody( QgsLayout *body SIP_TRANSFER ) { mBody.reset( body ); } + /** + * Returns true if the body for the section is enabled. + * \see setBodyEnabled() + * \see body() + * \see setBody() + */ + bool bodyEnabled() const { return mBodyEnabled; } + + /** + * Sets whether the body for the section is \a enabled. + * \see bodyEnabled() + * \see body() + * \see setBody() + */ + void setBodyEnabled( bool enabled ) { mBodyEnabled = enabled; } + + /** * Returns the vector layer associated with this section. * \see setLayer() @@ -123,6 +144,7 @@ class CORE_EXPORT QgsReportSectionFieldGroup : public QgsAbstractReportSection QgsFeature mLastFeature; QSet< QVariant > mEncounteredValues; + bool mBodyEnabled = false; std::unique_ptr< QgsLayout > mBody; QgsFeatureRequest buildFeatureRequest() const; diff --git a/src/core/layout/qgsreportsectionlayout.cpp b/src/core/layout/qgsreportsectionlayout.cpp index 97798156aed4..285f334cd8da 100644 --- a/src/core/layout/qgsreportsectionlayout.cpp +++ b/src/core/layout/qgsreportsectionlayout.cpp @@ -35,6 +35,8 @@ QgsReportSectionLayout *QgsReportSectionLayout::clone() const else copy->mBody.reset(); + copy->mBodyEnabled = mBodyEnabled; + return copy.release(); } @@ -46,7 +48,7 @@ bool QgsReportSectionLayout::beginRender() QgsLayout *QgsReportSectionLayout::nextBody( bool &ok ) { - if ( !mExportedBody && mBody ) + if ( !mExportedBody && mBody && mBodyEnabled ) { mExportedBody = true; ok = true; @@ -67,6 +69,7 @@ bool QgsReportSectionLayout::writePropertiesToElement( QDomElement &element, QDo bodyElement.appendChild( mBody->writeXml( doc, context ) ); element.appendChild( bodyElement ); } + element.setAttribute( QStringLiteral( "bodyEnabled" ), mBodyEnabled ? "1" : "0" ); return true; } @@ -80,6 +83,7 @@ bool QgsReportSectionLayout::readPropertiesFromElement( const QDomElement &eleme body->readXml( bodyLayoutElem, doc, context ); mBody = std::move( body ); } + mBodyEnabled = element.attribute( QStringLiteral( "bodyEnabled" ) ).toInt(); return true; } diff --git a/src/core/layout/qgsreportsectionlayout.h b/src/core/layout/qgsreportsectionlayout.h index e60dbbc5acb9..6dbc51f692b7 100644 --- a/src/core/layout/qgsreportsectionlayout.h +++ b/src/core/layout/qgsreportsectionlayout.h @@ -47,6 +47,8 @@ class CORE_EXPORT QgsReportSectionLayout : public QgsAbstractReportSection /** * Returns the body layout for the section. * \see setBody() + * \see bodyEnabled() + * \see setBodyEnabled() */ QgsLayout *body() { return mBody.get(); } @@ -54,9 +56,27 @@ class CORE_EXPORT QgsReportSectionLayout : public QgsAbstractReportSection * Sets the \a body layout for the section. Ownership of \a body * is transferred to the report section. * \see body() + * \see bodyEnabled() + * \see setBodyEnabled() */ void setBody( QgsLayout *body SIP_TRANSFER ) { mBody.reset( body ); } + /** + * Returns true if the body for the section is enabled. + * \see setBodyEnabled() + * \see body() + * \see setBody() + */ + bool bodyEnabled() const { return mBodyEnabled; } + + /** + * Sets whether the body for the section is \a enabled. + * \see bodyEnabled() + * \see body() + * \see setBody() + */ + void setBodyEnabled( bool enabled ) { mBodyEnabled = enabled; } + QgsReportSectionLayout *clone() const override SIP_FACTORY; bool beginRender() override; QgsLayout *nextBody( bool &ok ) override; @@ -70,7 +90,7 @@ class CORE_EXPORT QgsReportSectionLayout : public QgsAbstractReportSection bool mExportedBody = false; std::unique_ptr< QgsLayout > mBody; - + bool mBodyEnabled = true; }; ///@endcond diff --git a/src/ui/layout/qgsreportorganizerwidgetbase.ui b/src/ui/layout/qgsreportorganizerwidgetbase.ui index 8fcf04b4c485..7960505c73de 100644 --- a/src/ui/layout/qgsreportorganizerwidgetbase.ui +++ b/src/ui/layout/qgsreportorganizerwidgetbase.ui @@ -114,51 +114,6 @@ - - - - - - Edit - - - - - - - Show header - - - - - - - Edit - - - - - - - Show footer - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - diff --git a/src/ui/layout/qgsreportwidgetfieldgroupsectionbase.ui b/src/ui/layout/qgsreportwidgetfieldgroupsectionbase.ui index 0f92ba0798a4..f32ec4649d68 100644 --- a/src/ui/layout/qgsreportwidgetfieldgroupsectionbase.ui +++ b/src/ui/layout/qgsreportwidgetfieldgroupsectionbase.ui @@ -6,8 +6,8 @@ 0 0 - 705 - 231 + 611 + 415 @@ -16,57 +16,85 @@ - - + + - Section body + Edit - - + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + - Edit + Field - + + + + Sort ascending + + + + Layer - + - - + + - Field + Edit - + - - - - Qt::Horizontal + + + + Include footer - - - 40 - 20 - + + + + + + Include header - + - - + + - Sort ascending + Edit + + + + + + + Include body @@ -99,8 +127,6 @@
qgsfieldcombobox.h
- - - + diff --git a/src/ui/layout/qgsreportwidgetlayoutsectionbase.ui b/src/ui/layout/qgsreportwidgetlayoutsectionbase.ui index 292d1080f3fd..afb8fc4ec1b7 100644 --- a/src/ui/layout/qgsreportwidgetlayoutsectionbase.ui +++ b/src/ui/layout/qgsreportwidgetlayoutsectionbase.ui @@ -6,8 +6,8 @@ 0 0 - 723 - 89 + 385 + 237
@@ -15,23 +15,23 @@ - - - + + + - Section body + Edit - - + + - Edit + Include header - - + + Qt::Horizontal @@ -43,6 +43,34 @@ + + + + Include footer + + + + + + + Edit + + + + + + + Edit + + + + + + + Include body + + + diff --git a/src/ui/layout/qgsreportwidgetsectionbase.ui b/src/ui/layout/qgsreportwidgetsectionbase.ui new file mode 100644 index 000000000000..ed0ed25f9765 --- /dev/null +++ b/src/ui/layout/qgsreportwidgetsectionbase.ui @@ -0,0 +1,79 @@ + + + QgsReportWidgetSectionBase + + + + 0 + 0 + 460 + 174 + + + + Layout Manager + + + + + + + + Edit + + + + + + + Include report header + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Include report footer + + + + + + + Edit + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + From ce161e0e7d195cca00052ad71e189847d822cf6e Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 2 Jan 2018 19:16:38 +1000 Subject: [PATCH 091/105] Fix failing unit test --- src/core/layout/qgsreportsectionfieldgroup.cpp | 2 +- tests/src/python/test_qgsreport.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/core/layout/qgsreportsectionfieldgroup.cpp b/src/core/layout/qgsreportsectionfieldgroup.cpp index 460dd9f572e7..624abbb8c079 100644 --- a/src/core/layout/qgsreportsectionfieldgroup.cpp +++ b/src/core/layout/qgsreportsectionfieldgroup.cpp @@ -125,7 +125,7 @@ QgsLayout *QgsReportSectionFieldGroup::nextBody( bool &ok ) updateChildContexts( f ); - ok = mBodyEnabled; + ok = true; if ( mBody && mBodyEnabled ) { mBody->reportContext().blockSignals( true ); diff --git a/tests/src/python/test_qgsreport.py b/tests/src/python/test_qgsreport.py index 8da0ad1bd757..a15478304036 100644 --- a/tests/src/python/test_qgsreport.py +++ b/tests/src/python/test_qgsreport.py @@ -317,6 +317,7 @@ def testFieldGroup(self): child1_body = QgsLayout(p) child1.setLayer(ptLayer) child1.setBody(child1_body) + child1.setBodyEnabled(True) child1.setField('country') r.appendChild(child1) self.assertTrue(r.beginRender()) @@ -354,12 +355,13 @@ def testFieldGroup(self): # another group # remove body from child1 - child1.setBody(None) + child1.setBodyEnabled(False) child2 = QgsReportSectionFieldGroup() child2_body = QgsLayout(p) child2.setLayer(ptLayer) child2.setBody(child2_body) + child2.setBodyEnabled(True) child2.setField('state') child1.appendChild(child2) self.assertTrue(r.beginRender()) @@ -397,12 +399,13 @@ def testFieldGroup(self): # another group # remove body from child1 - child2.setBody(None) + child2.setBodyEnabled(False) child3 = QgsReportSectionFieldGroup() child3_body = QgsLayout(p) child3.setLayer(ptLayer) child3.setBody(child3_body) + child3.setBodyEnabled(True) child3.setField('town') child3.setSortAscending(False) child2.appendChild(child3) From eae4eeb8f5e63441c09480a7ac6a867c9fdb172b Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 5 Jan 2018 11:52:57 +1000 Subject: [PATCH 092/105] Use correct layout type (report/print layout) in new title dialog --- .../core/layout/qgsmasterlayoutinterface.sip | 11 ++++++++++ python/core/layout/qgsprintlayout.sip | 2 ++ python/core/layout/qgsreport.sip | 2 ++ src/app/layout/qgslayoutdesignerdialog.cpp | 6 ++--- src/app/layout/qgslayoutmanagerdialog.cpp | 8 +++---- src/app/qgisapp.cpp | 22 ++++++++++++++----- src/app/qgisapp.h | 3 ++- src/core/layout/qgsmasterlayoutinterface.h | 12 ++++++++++ src/core/layout/qgsprintlayout.cpp | 5 +++++ src/core/layout/qgsprintlayout.h | 1 + src/core/layout/qgsreport.cpp | 6 ++++- src/core/layout/qgsreport.h | 1 + tests/src/app/CMakeLists.txt | 1 + 13 files changed, 66 insertions(+), 14 deletions(-) diff --git a/python/core/layout/qgsmasterlayoutinterface.sip b/python/core/layout/qgsmasterlayoutinterface.sip index daf4fc002be9..fc725e531bab 100644 --- a/python/core/layout/qgsmasterlayoutinterface.sip +++ b/python/core/layout/qgsmasterlayoutinterface.sip @@ -20,12 +20,23 @@ class QgsMasterLayoutInterface %End public: + enum Type + { + PrintLayout, + Report, + }; + virtual ~QgsMasterLayoutInterface(); virtual QgsMasterLayoutInterface *clone() const = 0 /Factory/; %Docstring Creates a clone of the layout. Ownership of the returned layout is transferred to the caller. +%End + + virtual QgsMasterLayoutInterface::Type layoutType() const = 0; +%Docstring +Returns the master layout type. %End virtual QString name() const = 0; diff --git a/python/core/layout/qgsprintlayout.sip b/python/core/layout/qgsprintlayout.sip index 4fba1d2ba7e5..4351baba7cf5 100644 --- a/python/core/layout/qgsprintlayout.sip +++ b/python/core/layout/qgsprintlayout.sip @@ -30,6 +30,8 @@ Constructor for QgsPrintLayout. virtual QgsProject *layoutProject() const; + virtual QgsMasterLayoutInterface::Type layoutType() const; + virtual QIcon icon() const; diff --git a/python/core/layout/qgsreport.sip b/python/core/layout/qgsreport.sip index e8ce2be53a27..efe4641afd6d 100644 --- a/python/core/layout/qgsreport.sip +++ b/python/core/layout/qgsreport.sip @@ -38,6 +38,8 @@ Constructor for QgsReport, associated with the specified Note that ownership is not transferred to ``project``. %End + virtual QgsMasterLayoutInterface::Type layoutType() const; + virtual QString type() const; virtual QString description() const; virtual QIcon icon() const; diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 10834b02af05..aba2dc75c4f6 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -1515,7 +1515,7 @@ void QgsLayoutDesignerDialog::addItemsFromTemplate() void QgsLayoutDesignerDialog::duplicate() { QString newTitle; - if ( !QgisApp::instance()->uniqueLayoutTitle( this, newTitle, false, tr( "%1 copy" ).arg( masterLayout()->name() ) ) ) + if ( !QgisApp::instance()->uniqueLayoutTitle( this, newTitle, false, masterLayout()->layoutType(), tr( "%1 copy" ).arg( masterLayout()->name() ) ) ) { return; } @@ -1546,7 +1546,7 @@ void QgsLayoutDesignerDialog::saveProject() void QgsLayoutDesignerDialog::newLayout() { QString title; - if ( !QgisApp::instance()->uniqueLayoutTitle( this, title, true ) ) + if ( !QgisApp::instance()->uniqueLayoutTitle( this, title, true, QgsMasterLayoutInterface::PrintLayout ) ) { return; } @@ -1568,7 +1568,7 @@ void QgsLayoutDesignerDialog::renameLayout() { QString currentTitle = masterLayout()->name(); QString newTitle; - if ( !QgisApp::instance()->uniqueLayoutTitle( this, newTitle, false, currentTitle ) ) + if ( !QgisApp::instance()->uniqueLayoutTitle( this, newTitle, false, masterLayout()->layoutType(), currentTitle ) ) { return; } diff --git a/src/app/layout/qgslayoutmanagerdialog.cpp b/src/app/layout/qgslayoutmanagerdialog.cpp index ee2cf8f73c43..e587a5db1a07 100644 --- a/src/app/layout/qgslayoutmanagerdialog.cpp +++ b/src/app/layout/qgslayoutmanagerdialog.cpp @@ -242,7 +242,7 @@ void QgsLayoutManagerDialog::mAddButton_clicked() } QString title; - if ( !QgisApp::instance()->uniqueLayoutTitle( this, title, true, storedTitle ) ) + if ( !QgisApp::instance()->uniqueLayoutTitle( this, title, true, QgsMasterLayoutInterface::PrintLayout, storedTitle ) ) { return; } @@ -295,7 +295,7 @@ void QgsLayoutManagerDialog::mTemplatesUserDirBtn_pressed() void QgsLayoutManagerDialog::createReport() { QString title; - if ( !QgisApp::instance()->uniqueLayoutTitle( this, title, true ) ) + if ( !QgisApp::instance()->uniqueLayoutTitle( this, title, true, QgsMasterLayoutInterface::Report ) ) { return; } @@ -427,7 +427,7 @@ void QgsLayoutManagerDialog::duplicateClicked() QString currentTitle = currentLayout->name(); QString newTitle; - if ( !QgisApp::instance()->uniqueLayoutTitle( this, newTitle, false, tr( "%1 copy" ).arg( currentTitle ) ) ) + if ( !QgisApp::instance()->uniqueLayoutTitle( this, newTitle, false, currentLayout->layoutType(), tr( "%1 copy" ).arg( currentTitle ) ) ) { return; } @@ -467,7 +467,7 @@ void QgsLayoutManagerDialog::renameClicked() QString currentTitle = currentLayout->name(); QString newTitle; - if ( !QgisApp::instance()->uniqueLayoutTitle( this, newTitle, false, currentTitle ) ) + if ( !QgisApp::instance()->uniqueLayoutTitle( this, newTitle, false, currentLayout->layoutType(), currentTitle ) ) { return; } diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 4e02ca37b574..487d85457987 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -6002,7 +6002,7 @@ void QgisApp::newPrintComposer() void QgisApp::newPrintLayout() { QString title; - if ( !uniqueLayoutTitle( this, title, true ) ) + if ( !uniqueLayoutTitle( this, title, true, QgsMasterLayoutInterface::PrintLayout ) ) { return; } @@ -7335,7 +7335,7 @@ bool QgisApp::uniqueComposerTitle( QWidget *parent, QString &composerTitle, bool return true; } -bool QgisApp::uniqueLayoutTitle( QWidget *parent, QString &title, bool acceptEmpty, const QString ¤tTitle ) +bool QgisApp::uniqueLayoutTitle( QWidget *parent, QString &title, bool acceptEmpty, QgsMasterLayoutInterface::Type type, const QString ¤tTitle ) { if ( !parent ) { @@ -7344,10 +7344,22 @@ bool QgisApp::uniqueLayoutTitle( QWidget *parent, QString &title, bool acceptEmp bool ok = false; bool titleValid = false; QString newTitle = QString( currentTitle ); - QString chooseMsg = tr( "Create unique print layout title" ); + + QString typeString; + switch ( type ) + { + case QgsMasterLayoutInterface::PrintLayout: + typeString = tr( "print layout" ); + break; + case QgsMasterLayoutInterface::Report: + typeString = tr( "report" ); + break; + } + + QString chooseMsg = tr( "Enter a unique %1 title" ).arg( typeString ); if ( acceptEmpty ) { - chooseMsg += '\n' + tr( "(title generated if left empty)" ); + chooseMsg += '\n' + tr( "(a title will be automatically generated if left empty)" ); } QString titleMsg = chooseMsg; @@ -7361,7 +7373,7 @@ bool QgisApp::uniqueLayoutTitle( QWidget *parent, QString &title, bool acceptEmp while ( !titleValid ) { newTitle = QInputDialog::getText( parent, - tr( "Layout title" ), + tr( "Create %1 title" ).arg( typeString ), titleMsg, QLineEdit::Normal, newTitle, diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h index c5c5e394b994..abb200387210 100644 --- a/src/app/qgisapp.h +++ b/src/app/qgisapp.h @@ -155,6 +155,7 @@ class QgsLayoutQptDropHandler; #include "qgsmaplayeractionregistry.h" #include "qgsoptionswidgetfactory.h" #include "qgsattributetablefiltermodel.h" +#include "qgsmasterlayoutinterface.h" #include "ui_qgisapp.h" #include "qgis_app.h" @@ -376,7 +377,7 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow * * \returns true if user did not cancel the dialog. */ - bool uniqueLayoutTitle( QWidget *parent, QString &title, bool acceptEmpty, const QString ¤tTitle = QString() ); + bool uniqueLayoutTitle( QWidget *parent, QString &title, bool acceptEmpty, QgsMasterLayoutInterface::Type type, const QString ¤tTitle = QString() ); //! Creates a new composer and returns a pointer to it diff --git a/src/core/layout/qgsmasterlayoutinterface.h b/src/core/layout/qgsmasterlayoutinterface.h index 32f0b5961b64..f507108ba6df 100644 --- a/src/core/layout/qgsmasterlayoutinterface.h +++ b/src/core/layout/qgsmasterlayoutinterface.h @@ -32,6 +32,13 @@ class CORE_EXPORT QgsMasterLayoutInterface public: + //! Master layout type + enum Type + { + PrintLayout = 0, //!< Individual print layout (QgsPrintLayout) + Report = 1, //!< Report (QgsReport) + }; + virtual ~QgsMasterLayoutInterface() = default; /** @@ -40,6 +47,11 @@ class CORE_EXPORT QgsMasterLayoutInterface */ virtual QgsMasterLayoutInterface *clone() const = 0 SIP_FACTORY; + /** + * Returns the master layout type. + */ + virtual QgsMasterLayoutInterface::Type layoutType() const = 0; + /** * Returns the layout's name. * \see setName() diff --git a/src/core/layout/qgsprintlayout.cpp b/src/core/layout/qgsprintlayout.cpp index 2b4a914c8ef4..600f82839545 100644 --- a/src/core/layout/qgsprintlayout.cpp +++ b/src/core/layout/qgsprintlayout.cpp @@ -106,3 +106,8 @@ QgsExpressionContext QgsPrintLayout::createExpressionContext() const return context; } + +QgsMasterLayoutInterface::Type QgsPrintLayout::layoutType() const +{ + return QgsMasterLayoutInterface::PrintLayout; +} diff --git a/src/core/layout/qgsprintlayout.h b/src/core/layout/qgsprintlayout.h index 08949125b326..927c004f32aa 100644 --- a/src/core/layout/qgsprintlayout.h +++ b/src/core/layout/qgsprintlayout.h @@ -41,6 +41,7 @@ class CORE_EXPORT QgsPrintLayout : public QgsLayout, public QgsMasterLayoutInter QgsPrintLayout *clone() const override SIP_FACTORY; QgsProject *layoutProject() const override; + QgsMasterLayoutInterface::Type layoutType() const override; QIcon icon() const override; /** diff --git a/src/core/layout/qgsreport.cpp b/src/core/layout/qgsreport.cpp index 805c18b77251..d25a7a2a3b8d 100644 --- a/src/core/layout/qgsreport.cpp +++ b/src/core/layout/qgsreport.cpp @@ -61,5 +61,9 @@ bool QgsReport::readLayoutXml( const QDomElement &layoutElement, const QDomDocum return true; } -///@endcond +QgsMasterLayoutInterface::Type QgsReport::layoutType() const +{ + return QgsMasterLayoutInterface::Report; +} +///@endcond diff --git a/src/core/layout/qgsreport.h b/src/core/layout/qgsreport.h index 2fbb7e335ece..bc26b456f6a4 100644 --- a/src/core/layout/qgsreport.h +++ b/src/core/layout/qgsreport.h @@ -52,6 +52,7 @@ class CORE_EXPORT QgsReport : public QObject, public QgsAbstractReportSection, p */ QgsReport( QgsProject *project ); + QgsMasterLayoutInterface::Type layoutType() const override; QString type() const override { return QStringLiteral( "SectionReport" ); } QString description() const override { return QObject::tr( "Report" ); } QIcon icon() const override; diff --git a/tests/src/app/CMakeLists.txt b/tests/src/app/CMakeLists.txt index 9d5c56a6fc09..68a25b67d52f 100644 --- a/tests/src/app/CMakeLists.txt +++ b/tests/src/app/CMakeLists.txt @@ -8,6 +8,7 @@ INCLUDE_DIRECTORIES( ${CMAKE_SOURCE_DIR}/src/core/composer ${CMAKE_SOURCE_DIR}/src/core/expression ${CMAKE_SOURCE_DIR}/src/core/geometry + ${CMAKE_SOURCE_DIR}/src/core/layout ${CMAKE_SOURCE_DIR}/src/core/metadata ${CMAKE_SOURCE_DIR}/src/core/raster ${CMAKE_SOURCE_DIR}/src/core/symbology From a4f854e2c1764bfe44b23be86fa69957824c69b8 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 5 Jan 2018 12:12:33 +1000 Subject: [PATCH 093/105] Ensure report dock is visible when opening a report designer --- src/app/layout/qgslayoutdesignerdialog.cpp | 27 ++++++++-------------- src/gui/qgsdockwidget.cpp | 3 --- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index aba2dc75c4f6..6cfc385dac8d 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -675,7 +675,7 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla addDockWidget( Qt::RightDockWidgetArea, mUndoDock ); addDockWidget( Qt::RightDockWidgetArea, mItemsDock ); addDockWidget( Qt::RightDockWidgetArea, mAtlasDock ); - addDockWidget( Qt::RightDockWidgetArea, mReportDock ); + addDockWidget( Qt::LeftDockWidgetArea, mReportDock ); createLayoutPropertiesWidget(); @@ -914,6 +914,13 @@ void QgsLayoutDesignerDialog::open() { mView->zoomFull(); // zoomFull() does not work properly until we have called show() } + + if ( mMasterLayout && mMasterLayout->layoutType() == QgsMasterLayoutInterface::Report ) + { + mReportDock->show(); + mReportDock->raise(); + mReportDock->setUserVisible( true ); + } } void QgsLayoutDesignerDialog::activate() @@ -1990,12 +1997,7 @@ void QgsLayoutDesignerDialog::showAtlasSettings() if ( !mAtlasDock ) return; - if ( !mAtlasDock->isVisible() ) - { - mAtlasDock->show(); - } - - mAtlasDock->raise(); + mAtlasDock->setUserVisible( true ); } void QgsLayoutDesignerDialog::atlasPreviewTriggered( bool checked ) @@ -3277,12 +3279,7 @@ void QgsLayoutDesignerDialog::showReportSettings() if ( !mReportDock ) return; - if ( !mReportDock->isVisible() ) - { - mReportDock->show(); - } - - mReportDock->raise(); + mReportDock->setUserVisible( true ); } void QgsLayoutDesignerDialog::pageSetup() @@ -3435,11 +3432,7 @@ void QgsLayoutDesignerDialog::createReportWidget() QgsReportOrganizerWidget *reportWidget = new QgsReportOrganizerWidget( mReportDock, this, report ); reportWidget->setMessageBar( mMessageBar ); mReportDock->setWidget( reportWidget ); - mReportDock->show(); - mReportDock->raise(); - mReportToolbar->show(); - mPanelsMenu->addAction( mReportDock->toggleViewAction() ); } diff --git a/src/gui/qgsdockwidget.cpp b/src/gui/qgsdockwidget.cpp index e23a24c48b20..69fc81e8d1e4 100644 --- a/src/gui/qgsdockwidget.cpp +++ b/src/gui/qgsdockwidget.cpp @@ -35,9 +35,6 @@ void QgsDockWidget::setUserVisible( bool visible ) { if ( visible ) { - if ( mVisibleAndActive ) - return; - show(); raise(); } From 4e6a072e4908154d6a1b6b8ed1be99fb880a6da2 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 5 Jan 2018 12:20:55 +1000 Subject: [PATCH 094/105] Make report/atlas settings actions checkable, so they can also hide the panels --- src/app/layout/qgslayoutdesignerdialog.cpp | 10 ++++++---- src/app/layout/qgslayoutdesignerdialog.h | 4 ++-- src/ui/layout/qgslayoutdesignerbase.ui | 9 ++++++++- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 6cfc385dac8d..5e8518c465cc 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -659,9 +659,11 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla mAtlasDock = new QgsDockWidget( tr( "Atlas" ), this ); mAtlasDock->setObjectName( QStringLiteral( "AtlasDock" ) ); + connect( mAtlasDock, &QDockWidget::visibilityChanged, mActionAtlasSettings, &QAction::setChecked ); mReportDock = new QgsDockWidget( tr( "Report" ), this ); mReportDock->setObjectName( QStringLiteral( "ReportDock" ) ); + connect( mReportDock, &QDockWidget::visibilityChanged, mActionReportSettings, &QAction::setChecked ); const QList docks = findChildren(); for ( QDockWidget *dock : docks ) @@ -1992,12 +1994,12 @@ void QgsLayoutDesignerDialog::exportToSvg() QApplication::restoreOverrideCursor(); } -void QgsLayoutDesignerDialog::showAtlasSettings() +void QgsLayoutDesignerDialog::showAtlasSettings( bool checked ) { if ( !mAtlasDock ) return; - mAtlasDock->setUserVisible( true ); + mAtlasDock->setUserVisible( checked ); } void QgsLayoutDesignerDialog::atlasPreviewTriggered( bool checked ) @@ -3274,12 +3276,12 @@ void QgsLayoutDesignerDialog::printReport() QApplication::restoreOverrideCursor(); } -void QgsLayoutDesignerDialog::showReportSettings() +void QgsLayoutDesignerDialog::showReportSettings( bool checked ) { if ( !mReportDock ) return; - mReportDock->setUserVisible( true ); + mReportDock->setUserVisible( checked ); } void QgsLayoutDesignerDialog::pageSetup() diff --git a/src/app/layout/qgslayoutdesignerdialog.h b/src/app/layout/qgslayoutdesignerdialog.h index 9b47f3c5106a..6b490e3b2eff 100644 --- a/src/app/layout/qgslayoutdesignerdialog.h +++ b/src/app/layout/qgslayoutdesignerdialog.h @@ -309,7 +309,7 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner void exportToRaster(); void exportToPdf(); void exportToSvg(); - void showAtlasSettings(); + void showAtlasSettings( bool checked ); void atlasPreviewTriggered( bool checked ); void atlasPageComboEditingFinished(); void atlasNext(); @@ -325,7 +325,7 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner void exportReportToSvg(); void exportReportToPdf(); void printReport(); - void showReportSettings(); + void showReportSettings( bool checked ); void pageSetup(); diff --git a/src/ui/layout/qgslayoutdesignerbase.ui b/src/ui/layout/qgslayoutdesignerbase.ui index 396ae5425f6b..fff12d65b647 100644 --- a/src/ui/layout/qgslayoutdesignerbase.ui +++ b/src/ui/layout/qgslayoutdesignerbase.ui @@ -99,7 +99,7 @@ 0 0 2180 - 42 + 25 @@ -1369,6 +1369,9 @@ + + true + :/images/themes/default/mActionAtlasSettings.svg:/images/themes/default/mActionAtlasSettings.svg @@ -1429,6 +1432,9 @@ + + true + :/images/themes/default/mActionAtlasSettings.svg:/images/themes/default/mActionAtlasSettings.svg @@ -1510,6 +1516,7 @@ +
From c3f07f62df8da23c89909bc1dd92cdfd4a0ae575 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 5 Jan 2018 12:24:32 +1000 Subject: [PATCH 095/105] Don't tabify report settings with item properties --- src/app/layout/qgslayoutdesignerdialog.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 5e8518c465cc..ec536d1def2c 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -693,7 +693,6 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla tabifyDockWidget( mGeneralDock, mItemDock ); tabifyDockWidget( mItemDock, mItemsDock ); tabifyDockWidget( mItemDock, mAtlasDock ); - tabifyDockWidget( mItemDock, mReportDock ); toggleActions( false ); From 3db9c010213c37d01c0eb33e3ed251ef5c7e0659 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 5 Jan 2018 12:36:30 +1000 Subject: [PATCH 096/105] If no section is selected, add new sections to report itself --- src/app/layout/qgsreportsectionmodel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/layout/qgsreportsectionmodel.cpp b/src/app/layout/qgsreportsectionmodel.cpp index 2e43efdd2b5c..78b910261f7b 100644 --- a/src/app/layout/qgsreportsectionmodel.cpp +++ b/src/app/layout/qgsreportsectionmodel.cpp @@ -237,7 +237,7 @@ void QgsReportSectionModel::addSection( const QModelIndex &parent, std::unique_p { QgsAbstractReportSection *parentSection = sectionForIndex( parent ); if ( !parentSection ) - return; + parentSection = mReport; beginInsertRows( parent, parentSection->childCount(), parentSection->childCount() ); parentSection->appendChild( section.release() ); From 01ce9bccc4987af2fd8134820bc18223b773bd8a Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 5 Jan 2018 12:36:38 +1000 Subject: [PATCH 097/105] Fix crash on report designer close --- src/app/layout/qgsreportorganizerwidget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/layout/qgsreportorganizerwidget.cpp b/src/app/layout/qgsreportorganizerwidget.cpp index 9a7510d93038..8e1012ca3026 100644 --- a/src/app/layout/qgsreportorganizerwidget.cpp +++ b/src/app/layout/qgsreportorganizerwidget.cpp @@ -39,7 +39,7 @@ QgsReportOrganizerWidget::QgsReportOrganizerWidget( QWidget *parent, QgsLayoutDe setupUi( this ); setPanelTitle( tr( "Report" ) ); - mSectionModel = new QgsReportSectionModel( mReport, mViewSections ); + mSectionModel = new QgsReportSectionModel( mReport, this ); mViewSections->setModel( mSectionModel ); mViewSections->expandAll(); From b184c5e102a523bbe7351d3cc091f5ff44852a2b Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 5 Jan 2018 12:40:26 +1000 Subject: [PATCH 098/105] Explicitly disable remove button if no child report section is selected --- src/app/layout/qgsreportorganizerwidget.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/layout/qgsreportorganizerwidget.cpp b/src/app/layout/qgsreportorganizerwidget.cpp index 8e1012ca3026..c8f941d9da98 100644 --- a/src/app/layout/qgsreportorganizerwidget.cpp +++ b/src/app/layout/qgsreportorganizerwidget.cpp @@ -66,6 +66,7 @@ QgsReportOrganizerWidget::QgsReportOrganizerWidget( QWidget *parent, QgsLayoutDe mButtonAddSection->setMenu( addMenu ); connect( mButtonRemoveSection, &QPushButton::clicked, this, &QgsReportOrganizerWidget::removeSection ); + mButtonRemoveSection->setEnabled( false ); //disable until section clicked } void QgsReportOrganizerWidget::setMessageBar( QgsMessageBar *bar ) @@ -112,6 +113,9 @@ void QgsReportOrganizerWidget::selectionChanged( const QModelIndex ¤t, con if ( !parent ) parent = mReport; + // report cannot be deleted + mButtonRemoveSection->setEnabled( parent != mReport ); + delete mConfigWidget; if ( QgsReportSectionLayout *section = dynamic_cast< QgsReportSectionLayout * >( parent ) ) { From 3ac214170fec52ca7e650ff6efb587e5842703b8 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 5 Jan 2018 12:51:52 +1000 Subject: [PATCH 099/105] Auto generated names for reports are 'Report #' --- python/core/composer/qgslayoutmanager.sip | 4 ++-- src/app/layout/qgslayoutmanagerdialog.cpp | 4 ++-- src/app/qgisapp.cpp | 4 ++-- src/core/composer/qgslayoutmanager.cpp | 12 ++++++++++-- src/core/composer/qgslayoutmanager.h | 4 ++-- tests/src/python/test_qgslayoutmanager.py | 15 +++++++++++++-- 6 files changed, 31 insertions(+), 12 deletions(-) diff --git a/python/core/composer/qgslayoutmanager.sip b/python/core/composer/qgslayoutmanager.sip index 8fa63d239ce0..ba08648b354d 100644 --- a/python/core/composer/qgslayoutmanager.sip +++ b/python/core/composer/qgslayoutmanager.sip @@ -161,9 +161,9 @@ Generates a unique title for a new composition, which does not clash with any already contained by the manager. %End - QString generateUniqueTitle() const; + QString generateUniqueTitle( QgsMasterLayoutInterface::Type type = QgsMasterLayoutInterface::PrintLayout ) const; %Docstring -Generates a unique title for a new layout, which does not +Generates a unique title for a new layout of the specified ``type``, which does not clash with any already contained by the manager. %End diff --git a/src/app/layout/qgslayoutmanagerdialog.cpp b/src/app/layout/qgslayoutmanagerdialog.cpp index e587a5db1a07..88ed0bba2592 100644 --- a/src/app/layout/qgslayoutmanagerdialog.cpp +++ b/src/app/layout/qgslayoutmanagerdialog.cpp @@ -249,7 +249,7 @@ void QgsLayoutManagerDialog::mAddButton_clicked() if ( title.isEmpty() ) { - title = QgsProject::instance()->layoutManager()->generateUniqueTitle(); + title = QgsProject::instance()->layoutManager()->generateUniqueTitle( QgsMasterLayoutInterface::PrintLayout ); } std::unique_ptr< QgsPrintLayout > layout = qgis::make_unique< QgsPrintLayout >( QgsProject::instance() ); @@ -302,7 +302,7 @@ void QgsLayoutManagerDialog::createReport() if ( title.isEmpty() ) { - title = QgsProject::instance()->layoutManager()->generateUniqueTitle(); + title = QgsProject::instance()->layoutManager()->generateUniqueTitle( QgsMasterLayoutInterface::Report ); } std::unique_ptr< QgsReport > report = qgis::make_unique< QgsReport >( QgsProject::instance() ); diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 487d85457987..94015aa50463 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -7392,7 +7392,7 @@ bool QgisApp::uniqueLayoutTitle( QWidget *parent, QString &title, bool acceptEmp else { titleValid = true; - newTitle = QgsProject::instance()->layoutManager()->generateUniqueTitle(); + newTitle = QgsProject::instance()->layoutManager()->generateUniqueTitle( type ); } } else if ( layoutNames.indexOf( newTitle, 1 ) >= 0 ) @@ -7461,7 +7461,7 @@ QgsLayoutDesignerDialog *QgisApp::createNewLayout( QString title ) { if ( title.isEmpty() ) { - title = QgsProject::instance()->layoutManager()->generateUniqueTitle(); + title = QgsProject::instance()->layoutManager()->generateUniqueTitle( QgsMasterLayoutInterface::PrintLayout ); } //create new layout object QgsPrintLayout *layout = new QgsPrintLayout( QgsProject::instance() ); diff --git a/src/core/composer/qgslayoutmanager.cpp b/src/core/composer/qgslayoutmanager.cpp index 10f2185ac785..32b9589d6d53 100644 --- a/src/core/composer/qgslayoutmanager.cpp +++ b/src/core/composer/qgslayoutmanager.cpp @@ -355,7 +355,7 @@ QString QgsLayoutManager::generateUniqueComposerTitle() const return name; } -QString QgsLayoutManager::generateUniqueTitle() const +QString QgsLayoutManager::generateUniqueTitle( QgsMasterLayoutInterface::Type type ) const { QStringList names; for ( QgsMasterLayoutInterface *l : mLayouts ) @@ -366,7 +366,15 @@ QString QgsLayoutManager::generateUniqueTitle() const int id = 1; while ( name.isEmpty() || names.contains( name ) ) { - name = tr( "Layout %1" ).arg( id ); + switch ( type ) + { + case QgsMasterLayoutInterface::PrintLayout: + name = tr( "Layout %1" ).arg( id ); + break; + case QgsMasterLayoutInterface::Report: + name = tr( "Report %1" ).arg( id ); + break; + } id++; } return name; diff --git a/src/core/composer/qgslayoutmanager.h b/src/core/composer/qgslayoutmanager.h index d907078321f7..797f82c063e6 100644 --- a/src/core/composer/qgslayoutmanager.h +++ b/src/core/composer/qgslayoutmanager.h @@ -161,10 +161,10 @@ class CORE_EXPORT QgsLayoutManager : public QObject QString generateUniqueComposerTitle() const; /** - * Generates a unique title for a new layout, which does not + * Generates a unique title for a new layout of the specified \a type, which does not * clash with any already contained by the manager. */ - QString generateUniqueTitle() const; + QString generateUniqueTitle( QgsMasterLayoutInterface::Type type = QgsMasterLayoutInterface::PrintLayout ) const; signals: diff --git a/tests/src/python/test_qgslayoutmanager.py b/tests/src/python/test_qgslayoutmanager.py index ce59596027fe..6fd287128229 100644 --- a/tests/src/python/test_qgslayoutmanager.py +++ b/tests/src/python/test_qgslayoutmanager.py @@ -19,7 +19,9 @@ from qgis.core import (QgsComposition, QgsPrintLayout, QgsLayoutManager, - QgsProject) + QgsProject, + QgsReport, + QgsMasterLayoutInterface) from qgis.testing import start_app, unittest from utilities import unitTestDataPath @@ -375,20 +377,29 @@ def testDuplicateComposition(self): def testGenerateUniqueTitle(self): project = QgsProject() manager = QgsLayoutManager(project) - self.assertEqual(manager.generateUniqueTitle(), 'Layout 1') + self.assertEqual(manager.generateUniqueTitle(QgsMasterLayoutInterface.PrintLayout), 'Layout 1') + self.assertEqual(manager.generateUniqueTitle(QgsMasterLayoutInterface.Report), 'Report 1') layout = QgsPrintLayout(project) layout.setName(manager.generateUniqueTitle()) manager.addLayout(layout) self.assertEqual(manager.generateUniqueTitle(), 'Layout 2') + self.assertEqual(manager.generateUniqueTitle(QgsMasterLayoutInterface.Report), 'Report 1') layout2 = QgsPrintLayout(project) layout2.setName(manager.generateUniqueTitle()) manager.addLayout(layout2) self.assertEqual(manager.generateUniqueTitle(), 'Layout 3') + + report1 = QgsReport(project) + report1.setName(manager.generateUniqueTitle(QgsMasterLayoutInterface.Report)) + manager.addLayout(report1) + self.assertEqual(manager.generateUniqueTitle(QgsMasterLayoutInterface.Report), 'Report 2') + manager.clear() self.assertEqual(manager.generateUniqueTitle(), 'Layout 1') + self.assertEqual(manager.generateUniqueTitle(QgsMasterLayoutInterface.Report), 'Report 1') def testRenameSignalCompositions(self): project = QgsProject() From 37f5a3d5d1ebbf12ef17a57fc9d9ad54e4d81151 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 5 Jan 2018 13:08:00 +1000 Subject: [PATCH 100/105] Make some actions apply to reports when a report designer is open --- src/app/layout/qgslayoutdesignerdialog.cpp | 37 ++++++++++++++++++++-- src/app/layout/qgslayoutdesignerdialog.h | 1 + src/app/qgisapp.cpp | 15 +++++++++ src/app/qgisapp.h | 3 ++ 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index ec536d1def2c..ed8d4e0a829c 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -776,6 +776,8 @@ void QgsLayoutDesignerDialog::setMasterLayout( QgsMasterLayoutInterface *layout mMenuReport = nullptr; mReportToolbar->hide(); } + + updateActionNames( mMasterLayout->layoutType() ); } QgsMasterLayoutInterface *QgsLayoutDesignerDialog::masterLayout() @@ -1554,11 +1556,21 @@ void QgsLayoutDesignerDialog::saveProject() void QgsLayoutDesignerDialog::newLayout() { QString title; - if ( !QgisApp::instance()->uniqueLayoutTitle( this, title, true, QgsMasterLayoutInterface::PrintLayout ) ) + if ( !QgisApp::instance()->uniqueLayoutTitle( this, title, true, mMasterLayout->layoutType() ) ) { return; } - QgisApp::instance()->createNewLayout( title ); + + switch ( mMasterLayout->layoutType() ) + { + case QgsMasterLayoutInterface::PrintLayout: + QgisApp::instance()->createNewLayout( title ); + break; + + case QgsMasterLayoutInterface::Report: + QgisApp::instance()->createNewReport( title ); + break; + } } void QgsLayoutDesignerDialog::showManager() @@ -3901,7 +3913,6 @@ void QgsLayoutDesignerDialog::toggleActions( bool layoutAvailable ) mActionPasteInPlace->setEnabled( layoutAvailable ); mActionSaveAsTemplate->setEnabled( layoutAvailable ); mActionLoadFromTemplate->setEnabled( layoutAvailable ); - mActionDuplicateLayout->setEnabled( layoutAvailable ); mActionExportAsImage->setEnabled( layoutAvailable ); mActionExportAsPDF->setEnabled( layoutAvailable ); mActionExportAsSVG->setEnabled( layoutAvailable ); @@ -3964,6 +3975,26 @@ QString QgsLayoutDesignerDialog::reportTypeString() return tr( "report" ); } +void QgsLayoutDesignerDialog::updateActionNames( QgsMasterLayoutInterface::Type type ) +{ + switch ( type ) + { + case QgsMasterLayoutInterface::PrintLayout: + mActionDuplicateLayout->setText( tr( "&Duplicate Layout…" ) ); + mActionRemoveLayout->setText( tr( "Delete Layout…" ) ); + mActionRenameLayout->setText( tr( "Rename Layout…" ) ); + mActionNewLayout->setText( tr( "New Layout…" ) ); + break; + + case QgsMasterLayoutInterface::Report: + mActionDuplicateLayout->setText( tr( "&Duplicate Report…" ) ); + mActionRemoveLayout->setText( tr( "Delete Report…" ) ); + mActionRenameLayout->setText( tr( "Rename Report…" ) ); + mActionNewLayout->setText( tr( "New Report…" ) ); + break; + } +} + void QgsLayoutDesignerDialog::selectItems( const QList items ) { for ( QGraphicsItem *item : items ) diff --git a/src/app/layout/qgslayoutdesignerdialog.h b/src/app/layout/qgslayoutdesignerdialog.h index 6b490e3b2eff..3bd9a98f1921 100644 --- a/src/app/layout/qgslayoutdesignerdialog.h +++ b/src/app/layout/qgslayoutdesignerdialog.h @@ -479,6 +479,7 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner void setPrinterPageOrientation( QgsLayoutItemPage::Orientation orientation ); QPrinter *printer(); QString reportTypeString(); + void updateActionNames( QgsMasterLayoutInterface::Type type ); }; #endif // QGSLAYOUTDESIGNERDIALOG_H diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 94015aa50463..de1253d326de 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -268,6 +268,7 @@ Q_GUI_EXPORT extern int qt_defaultDpiX(); #include "qgsrasterprojector.h" #include "qgsreadwritecontext.h" #include "qgsrectangle.h" +#include "qgsreport.h" #include "qgsscalevisibilitydialog.h" #include "qgsgroupwmsdatadialog.h" #include "qgsselectbyformdialog.h" @@ -7471,6 +7472,20 @@ QgsLayoutDesignerDialog *QgisApp::createNewLayout( QString title ) return openLayoutDesignerDialog( layout ); } +QgsLayoutDesignerDialog *QgisApp::createNewReport( QString title ) +{ + if ( title.isEmpty() ) + { + title = QgsProject::instance()->layoutManager()->generateUniqueTitle( QgsMasterLayoutInterface::Report ); + } + //create new report + std::unique_ptr< QgsReport > report = qgis::make_unique< QgsReport >( QgsProject::instance() ); + report->setName( title ); + QgsMasterLayoutInterface *layout = report.get(); + QgsProject::instance()->layoutManager()->addLayout( report.release() ); + return openLayoutDesignerDialog( layout ); +} + QgsLayoutDesignerDialog *QgisApp::openLayoutDesignerDialog( QgsMasterLayoutInterface *layout ) { // maybe a designer already open for this layout diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h index abb200387210..92f5ccb47475 100644 --- a/src/app/qgisapp.h +++ b/src/app/qgisapp.h @@ -391,6 +391,9 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow //! Creates a new layout and returns a pointer to it QgsLayoutDesignerDialog *createNewLayout( QString title = QString() ); + //! Creates a new report and returns a pointer to it + QgsLayoutDesignerDialog *createNewReport( QString title = QString() ); + /** * Opens a layout designer dialog for an existing \a layout. * If a designer already exists for this layout then it will be activated. From a01d8daf8fc946a964ce7996303564b74cb896d2 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 5 Jan 2018 13:17:27 +1000 Subject: [PATCH 101/105] Fix atlas actions not immediately available for map items when toggling atlas enabled --- src/app/layout/qgslayoutatlaswidget.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/layout/qgslayoutatlaswidget.cpp b/src/app/layout/qgslayoutatlaswidget.cpp index d6fcac392426..79b0444328e5 100644 --- a/src/app/layout/qgslayoutatlaswidget.cpp +++ b/src/app/layout/qgslayoutatlaswidget.cpp @@ -94,6 +94,7 @@ void QgsLayoutAtlasWidget::changeCoverageLayer( QgsMapLayer *layer ) QgsVectorLayer *vl = dynamic_cast( layer ); mLayout->undoStack()->beginCommand( mAtlas, tr( "Change Atlas Layer" ) ); + mLayout->reportContext().setLayer( vl ); if ( !vl ) { mAtlas->setCoverageLayer( nullptr ); From e3daac407bdc4fb8f03dd6b0ffa236d9cb0989e0 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 5 Jan 2018 13:19:26 +1000 Subject: [PATCH 102/105] Rename some test methods --- tests/src/core/testqgslayoutlabel.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/src/core/testqgslayoutlabel.cpp b/tests/src/core/testqgslayoutlabel.cpp index 445f7db95e97..15c517b6a42a 100644 --- a/tests/src/core/testqgslayoutlabel.cpp +++ b/tests/src/core/testqgslayoutlabel.cpp @@ -46,10 +46,10 @@ class TestQgsLayoutLabel : public QObject // test simple expression evaluation void evaluation(); // test expression evaluation when a feature is set - void feature_evaluation(); - void feature_evaluation2(); + void featureEvaluationUsingAtlas(); + void featureEvaluationUsingContext(); // test page expressions - void page_evaluation(); + void pageEvaluation(); void marginMethods(); //tests getting/setting margins void render(); void renderAsHtml(); @@ -145,7 +145,7 @@ void TestQgsLayoutLabel::evaluation() } } -void TestQgsLayoutLabel::feature_evaluation() +void TestQgsLayoutLabel::featureEvaluationUsingAtlas() { QgsPrintLayout l( QgsProject::instance() ); l.initializeDefaults(); @@ -175,7 +175,7 @@ void TestQgsLayoutLabel::feature_evaluation() } } -void TestQgsLayoutLabel::feature_evaluation2() +void TestQgsLayoutLabel::featureEvaluationUsingContext() { // just using context, no atlas QgsLayout l( QgsProject::instance() ); @@ -209,7 +209,7 @@ void TestQgsLayoutLabel::feature_evaluation2() } } -void TestQgsLayoutLabel::page_evaluation() +void TestQgsLayoutLabel::pageEvaluation() { QgsLayout l( QgsProject::instance() ); l.initializeDefaults(); From 612969c230351beafebf7456958dc9211e09a955 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 5 Jan 2018 13:26:36 +1000 Subject: [PATCH 103/105] Use correct QgsSettings keys for layouts --- src/app/layout/qgslayoutdesignerdialog.cpp | 50 +++++++++++----------- src/app/layout/qgslayoutmanagerdialog.cpp | 8 ++-- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index ed8d4e0a829c..90eb03d1fde0 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -174,7 +174,7 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla viewLayout->addWidget( mVerticalRuler, 1, 0 ); //initial state of rulers - bool showRulers = settings.value( QStringLiteral( "LayoutDesigner/showRulers" ), true ).toBool(); + bool showRulers = settings.value( QStringLiteral( "LayoutDesigner/showRulers" ), true, QgsSettings::App ).toBool(); mActionShowRulers->setChecked( showRulers ); mHorizontalRuler->setVisible( showRulers ); mVerticalRuler->setVisible( showRulers ); @@ -950,7 +950,7 @@ void QgsLayoutDesignerDialog::showRulers( bool visible ) mRulerLayoutFix->setVisible( visible ); QgsSettings settings; - settings.setValue( QStringLiteral( "LayoutDesigner/showRulers" ), visible ); + settings.setValue( QStringLiteral( "LayoutDesigner/showRulers" ), visible, QgsSettings::App ); } void QgsLayoutDesignerDialog::showGrid( bool visible ) @@ -1448,7 +1448,7 @@ void QgsLayoutDesignerDialog::saveAsTemplate() { //show file dialog QgsSettings settings; - QString lastSaveDir = settings.value( QStringLiteral( "UI/lastComposerTemplateDir" ), QDir::homePath() ).toString(); + QString lastSaveDir = settings.value( QStringLiteral( "lastComposerTemplateDir" ), QDir::homePath(), QgsSettings::App ).toString(); #ifdef Q_OS_MAC QgisApp::instance()->activateWindow(); this->raise(); @@ -1468,7 +1468,7 @@ void QgsLayoutDesignerDialog::saveAsTemplate() QString saveFileNameWithSuffix = saveFileName.append( ".qpt" ); saveFileInfo = QFileInfo( saveFileNameWithSuffix ); } - settings.setValue( QStringLiteral( "UI/lastComposerTemplateDir" ), saveFileInfo.absolutePath() ); + settings.setValue( QStringLiteral( "lastComposerTemplateDir" ), saveFileInfo.absolutePath(), QgsSettings::App ); QgsReadWriteContext context; context.setPathResolver( QgsProject::instance()->pathResolver() ); @@ -1484,7 +1484,7 @@ void QgsLayoutDesignerDialog::addItemsFromTemplate() return; QgsSettings settings; - QString openFileDir = settings.value( QStringLiteral( "UI/lastComposerTemplateDir" ), QDir::homePath() ).toString(); + QString openFileDir = settings.value( QStringLiteral( "lastComposerTemplateDir" ), QDir::homePath(), QgsSettings::App ).toString(); QString openFileString = QFileDialog::getOpenFileName( nullptr, tr( "Load template" ), openFileDir, tr( "Layout templates" ) + " (*.qpt *.QPT)" ); if ( openFileString.isEmpty() ) @@ -1493,7 +1493,7 @@ void QgsLayoutDesignerDialog::addItemsFromTemplate() } QFileInfo openFileInfo( openFileString ); - settings.setValue( QStringLiteral( "UI/LastComposerTemplateDir" ), openFileInfo.absolutePath() ); + settings.setValue( QStringLiteral( "LastComposerTemplateDir" ), openFileInfo.absolutePath(), QgsSettings::App ); QFile templateFile( openFileString ); if ( !templateFile.open( QIODevice::ReadOnly ) ) @@ -1714,7 +1714,7 @@ void QgsLayoutDesignerDialog::exportToRaster() QgsLayoutAtlas *printAtlas = atlas(); if ( printAtlas && printAtlas->enabled() && mActionAtlasPreview->isChecked() ) { - QString lastUsedDir = s.value( QStringLiteral( "UI/lastSaveAsImageDir" ), QDir::homePath() ).toString(); + QString lastUsedDir = s.value( QStringLiteral( "lastSaveAsImageDir" ), QDir::homePath(), QgsSettings::App ).toString(); outputFileName = QDir( lastUsedDir ).filePath( QgsFileUtils::stringToSafeFilename( printAtlas->currentFilename() ) ); } @@ -1799,7 +1799,7 @@ void QgsLayoutDesignerDialog::exportToPdf() } QgsSettings settings; - QString lastUsedFile = settings.value( QStringLiteral( "UI/lastSaveAsPdfFile" ), QStringLiteral( "qgis.pdf" ) ).toString(); + QString lastUsedFile = settings.value( QStringLiteral( "lastSaveAsPdfFile" ), QStringLiteral( "qgis.pdf" ), QgsSettings::App ).toString(); QFileInfo file( lastUsedFile ); QString outputFileName; @@ -1833,7 +1833,7 @@ void QgsLayoutDesignerDialog::exportToPdf() outputFileName += QLatin1String( ".pdf" ); } - settings.setValue( QStringLiteral( "UI/lastSaveAsPdfFile" ), outputFileName ); + settings.setValue( QStringLiteral( "lastSaveAsPdfFile" ), outputFileName, QgsSettings::App ); mView->setPaintingEnabled( false ); QApplication::setOverrideCursor( Qt::BusyCursor ); @@ -1901,7 +1901,7 @@ void QgsLayoutDesignerDialog::exportToSvg() showSvgExportWarning(); QgsSettings settings; - QString lastUsedFile = settings.value( QStringLiteral( "UI/lastSaveAsSvgFile" ), QStringLiteral( "qgis.svg" ) ).toString(); + QString lastUsedFile = settings.value( QStringLiteral( "lastSaveAsSvgFile" ), QStringLiteral( "qgis.svg" ), QgsSettings::App ).toString(); QFileInfo file( lastUsedFile ); QString outputFileName = QgsFileUtils::stringToSafeFilename( mMasterLayout->name() ); @@ -1936,7 +1936,7 @@ void QgsLayoutDesignerDialog::exportToSvg() } bool prevSettingLabelsAsOutlines = mLayout->project()->readBoolEntry( QStringLiteral( "PAL" ), QStringLiteral( "/DrawOutlineLabels" ), true ); - settings.setValue( QStringLiteral( "UI/lastSaveAsSvgFile" ), outputFileName ); + settings.setValue( QStringLiteral( "lastSaveAsSvgFile" ), outputFileName, QgsSettings::App ); QgsLayoutExporter::SvgExportSettings svgSettings; bool exportAsText = false; @@ -2307,7 +2307,7 @@ void QgsLayoutDesignerDialog::exportAtlasToRaster() } QgsSettings s; - QString lastUsedDir = s.value( QStringLiteral( "UI/lastSaveAtlasAsImagesDir" ), QDir::homePath() ).toString(); + QString lastUsedDir = s.value( QStringLiteral( "lastSaveAtlasAsImagesDir" ), QDir::homePath(), QgsSettings::App ).toString(); QFileDialog dlg( this, tr( "Export Atlas to Directory" ) ); dlg.setFileMode( QFileDialog::Directory ); @@ -2330,7 +2330,7 @@ void QgsLayoutDesignerDialog::exportAtlasToRaster() { return; } - s.setValue( QStringLiteral( "UI/lastSaveAtlasAsImagesDir" ), dir ); + s.setValue( QStringLiteral( "lastSaveAtlasAsImagesDir" ), dir, QgsSettings::App ); // test directory (if it exists and is writable) if ( !QDir( dir ).exists() || !QFileInfo( dir ).isWritable() ) @@ -2462,7 +2462,7 @@ void QgsLayoutDesignerDialog::exportAtlasToSvg() } QgsSettings s; - QString lastUsedDir = s.value( QStringLiteral( "UI/lastSaveAtlasAsSvgDir" ), QDir::homePath() ).toString(); + QString lastUsedDir = s.value( QStringLiteral( "lastSaveAtlasAsSvgDir" ), QDir::homePath(), QgsSettings::App ).toString(); QFileDialog dlg( this, tr( "Export Atlas to Directory" ) ); dlg.setFileMode( QFileDialog::Directory ); @@ -2488,7 +2488,7 @@ void QgsLayoutDesignerDialog::exportAtlasToSvg() { return; } - s.setValue( QStringLiteral( "UI/lastSaveAtlasAsSvgDir" ), dir ); + s.setValue( QStringLiteral( "lastSaveAtlasAsSvgDir" ), dir, QgsSettings::App ); // test directory (if it exists and is writable) if ( !QDir( dir ).exists() || !QFileInfo( dir ).isWritable() ) @@ -2626,7 +2626,7 @@ void QgsLayoutDesignerDialog::exportAtlasToPdf() QgsSettings settings; if ( singleFile ) { - QString lastUsedFile = settings.value( QStringLiteral( "UI/lastSaveAsPdfFile" ), QStringLiteral( "qgis.pdf" ) ).toString(); + QString lastUsedFile = settings.value( QStringLiteral( "lastSaveAsPdfFile" ), QStringLiteral( "qgis.pdf" ), QgsSettings::App ).toString(); QFileInfo file( lastUsedFile ); QgsLayoutAtlas *printAtlas = atlas(); @@ -2658,7 +2658,7 @@ void QgsLayoutDesignerDialog::exportAtlasToPdf() { outputFileName += QLatin1String( ".pdf" ); } - settings.setValue( QStringLiteral( "UI/lastSaveAsPdfFile" ), outputFileName ); + settings.setValue( QStringLiteral( "lastSaveAsPdfFile" ), outputFileName, QgsSettings::App ); } else { @@ -2677,7 +2677,7 @@ void QgsLayoutDesignerDialog::exportAtlasToPdf() } - QString lastUsedDir = settings.value( QStringLiteral( "UI/lastSaveAtlasAsPdfDir" ), QDir::homePath() ).toString(); + QString lastUsedDir = settings.value( QStringLiteral( "lastSaveAtlasAsPdfDir" ), QDir::homePath(), QgsSettings::App ).toString(); QFileDialog dlg( this, tr( "Export Atlas to Directory" ) ); dlg.setFileMode( QFileDialog::Directory ); @@ -2703,7 +2703,7 @@ void QgsLayoutDesignerDialog::exportAtlasToPdf() { return; } - settings.setValue( QStringLiteral( "UI/lastSaveAtlasAsPdfDir" ), dir ); + settings.setValue( QStringLiteral( "lastSaveAtlasAsPdfDir" ), dir, QgsSettings::App ); // test directory (if it exists and is writable) if ( !QDir( dir ).exists() || !QFileInfo( dir ).isWritable() ) @@ -2929,7 +2929,7 @@ void QgsLayoutDesignerDialog::exportReportToSvg() showSvgExportWarning(); QgsSettings settings; - QString lastUsedFile = settings.value( QStringLiteral( "UI/lastSaveAsSvgFile" ), QStringLiteral( "qgis.svg" ) ).toString(); + QString lastUsedFile = settings.value( QStringLiteral( "lastSaveAsSvgFile" ), QStringLiteral( "qgis.svg" ), QgsSettings::App ).toString(); QFileInfo file( lastUsedFile ); QString outputFileName = file.path() + '/' + QgsFileUtils::stringToSafeFilename( mMasterLayout->name() ) + QStringLiteral( ".svg" ); @@ -2953,7 +2953,7 @@ void QgsLayoutDesignerDialog::exportReportToSvg() this->raise(); #endif bool prevSettingLabelsAsOutlines = mMasterLayout->layoutProject()->readBoolEntry( QStringLiteral( "PAL" ), QStringLiteral( "/DrawOutlineLabels" ), true ); - settings.setValue( QStringLiteral( "UI/lastSaveAsSvgFile" ), outputFileName ); + settings.setValue( QStringLiteral( "lastSaveAsSvgFile" ), outputFileName, QgsSettings::App ); QgsLayoutExporter::SvgExportSettings svgSettings; bool exportAsText = false; @@ -3058,7 +3058,7 @@ void QgsLayoutDesignerDialog::exportReportToPdf() { QgsSettings settings; - QString lastUsedFile = settings.value( QStringLiteral( "UI/lastSaveAsPdfFile" ), QStringLiteral( "qgis.pdf" ) ).toString(); + QString lastUsedFile = settings.value( QStringLiteral( "lastSaveAsPdfFile" ), QStringLiteral( "qgis.pdf" ), QgsSettings::App ).toString(); QFileInfo file( lastUsedFile ); QString outputFileName = file.path() + '/' + QgsFileUtils::stringToSafeFilename( mMasterLayout->name() ) + QStringLiteral( ".pdf" ); @@ -3082,7 +3082,7 @@ void QgsLayoutDesignerDialog::exportReportToPdf() { outputFileName += QLatin1String( ".pdf" ); } - settings.setValue( QStringLiteral( "UI/lastSaveAsPdfFile" ), outputFileName ); + settings.setValue( QStringLiteral( "lastSaveAsPdfFile" ), outputFileName, QgsSettings::App ); mView->setPaintingEnabled( false ); QApplication::setOverrideCursor( Qt::BusyCursor ); @@ -3357,9 +3357,9 @@ QgsLayoutView *QgsLayoutDesignerDialog::view() void QgsLayoutDesignerDialog::saveWindowState() { QgsSettings settings; - settings.setValue( QStringLiteral( "LayoutDesigner/geometry" ), saveGeometry() ); + settings.setValue( QStringLiteral( "LayoutDesigner/geometry" ), saveGeometry(), QgsSettings::App ); // store the toolbar/dock widget settings using Qt settings API - settings.setValue( QStringLiteral( "LayoutDesigner/state" ), saveState() ); + settings.setValue( QStringLiteral( "LayoutDesigner/state" ), saveState(), QgsSettings::App ); } void QgsLayoutDesignerDialog::restoreWindowState() diff --git a/src/app/layout/qgslayoutmanagerdialog.cpp b/src/app/layout/qgslayoutmanagerdialog.cpp index 88ed0bba2592..b8241e0b553a 100644 --- a/src/app/layout/qgslayoutmanagerdialog.cpp +++ b/src/app/layout/qgslayoutmanagerdialog.cpp @@ -52,15 +52,15 @@ QgsLayoutManagerDialog::QgsLayoutManagerDialog( QWidget *parent, Qt::WindowFlags mTemplateFileWidget->setDialogTitle( tr( "Select a Template" ) ); mTemplateFileWidget->lineEdit()->setShowClearButton( false ); QgsSettings settings; - mTemplateFileWidget->setDefaultRoot( settings.value( QStringLiteral( "UI/lastComposerTemplateDir" ), QString() ).toString() ); - mTemplateFileWidget->setFilePath( settings.value( QStringLiteral( "UI/ComposerManager/templatePath" ), QString() ).toString() ); + mTemplateFileWidget->setDefaultRoot( settings.value( QStringLiteral( "lastComposerTemplateDir" ), QString(), QgsSettings::App ).toString() ); + mTemplateFileWidget->setFilePath( settings.value( QStringLiteral( "ComposerManager/templatePath" ), QString(), QgsSettings::App ).toString() ); connect( mTemplateFileWidget, &QgsFileWidget::fileChanged, this, [ = ] { QgsSettings settings; - settings.setValue( QStringLiteral( "UI/ComposerManager/templatePath" ), mTemplateFileWidget->filePath() ); + settings.setValue( QStringLiteral( "ComposerManager/templatePath" ), mTemplateFileWidget->filePath(), QgsSettings::App ); QFileInfo tmplFileInfo( mTemplateFileWidget->filePath() ); - settings.setValue( QStringLiteral( "UI/lastComposerTemplateDir" ), tmplFileInfo.absolutePath() ); + settings.setValue( QStringLiteral( "lastComposerTemplateDir" ), tmplFileInfo.absolutePath(), QgsSettings::App ); } ); mModel = new QgsLayoutManagerModel( QgsProject::instance()->layoutManager(), From d9fe0d44076c44e9ac0ab7da31b6392d2dc9ef53 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 5 Jan 2018 13:35:15 +1000 Subject: [PATCH 104/105] Show section name in layout designer title To make it clearer which section is currently being edited --- src/app/layout/qgslayoutdesignerdialog.cpp | 26 ++++++++++++++++--- src/app/layout/qgslayoutdesignerdialog.h | 12 +++++++++ .../qgsreportfieldgroupsectionwidget.cpp | 3 +++ .../layout/qgsreportlayoutsectionwidget.cpp | 3 +++ src/app/layout/qgsreportsectionwidget.cpp | 2 ++ 5 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 90eb03d1fde0..ce60dd805f56 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -733,16 +733,16 @@ void QgsLayoutDesignerDialog::setMasterLayout( QgsMasterLayoutInterface *layout if ( obj ) connect( obj, &QObject::destroyed, this, &QgsLayoutDesignerDialog::close ); - setWindowTitle( mMasterLayout->name() ); + setTitle( mMasterLayout->name() ); if ( QgsPrintLayout *l = dynamic_cast< QgsPrintLayout * >( layout ) ) { - connect( l, &QgsPrintLayout::nameChanged, this, &QgsLayoutDesignerDialog::setWindowTitle ); + connect( l, &QgsPrintLayout::nameChanged, this, &QgsLayoutDesignerDialog::setTitle ); setCurrentLayout( l ); } else if ( QgsReport *r = dynamic_cast< QgsReport * >( layout ) ) { - connect( r, &QgsReport::nameChanged, this, &QgsLayoutDesignerDialog::setWindowTitle ); + connect( r, &QgsReport::nameChanged, this, &QgsLayoutDesignerDialog::setTitle ); } if ( dynamic_cast< QgsPrintLayout * >( layout ) ) @@ -1205,6 +1205,12 @@ void QgsLayoutDesignerDialog::dragEnterEvent( QDragEnterEvent *event ) } } +void QgsLayoutDesignerDialog::setTitle( const QString &title ) +{ + mTitle = title; + updateWindowTitle(); +} + void QgsLayoutDesignerDialog::itemTypeAdded( int id ) { if ( QgsGui::layoutItemGuiRegistry()->itemMetadata( id )->flags() & QgsLayoutItemAbstractGuiMetadata::FlagNoCreationTools ) @@ -3995,6 +4001,14 @@ void QgsLayoutDesignerDialog::updateActionNames( QgsMasterLayoutInterface::Type } } +void QgsLayoutDesignerDialog::updateWindowTitle() +{ + if ( mSectionTitle.isEmpty() ) + setWindowTitle( mTitle ); + else + setWindowTitle( QStringLiteral( "%1 - %2" ).arg( mTitle, mSectionTitle ) ); +} + void QgsLayoutDesignerDialog::selectItems( const QList items ) { for ( QGraphicsItem *item : items ) @@ -4045,4 +4059,10 @@ void QgsLayoutDesignerDialog::setAtlasFeature( QgsMapLayer *layer, const QgsFeat activate(); } +void QgsLayoutDesignerDialog::setSectionTitle( const QString &title ) +{ + mSectionTitle = title; + updateWindowTitle(); +} + diff --git a/src/app/layout/qgslayoutdesignerdialog.h b/src/app/layout/qgslayoutdesignerdialog.h index 3bd9a98f1921..0e0f6982ae6b 100644 --- a/src/app/layout/qgslayoutdesignerdialog.h +++ b/src/app/layout/qgslayoutdesignerdialog.h @@ -144,6 +144,12 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner */ void setAtlasFeature( QgsMapLayer *layer, const QgsFeature &feat ); + /** + * Sets a section \a title, to use to update the dialog title to display + * the currently edited section. + */ + void setSectionTitle( const QString &title ); + public slots: /** @@ -280,6 +286,8 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner private slots: + void setTitle( const QString &title ); + void itemTypeAdded( int id ); void statusZoomCombo_currentIndexChanged( int index ); void statusZoomCombo_zoomEntered(); @@ -419,6 +427,9 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner std::unique_ptr< QPrinter > mPrinter; bool mSetPageOrientation = false; + QString mTitle; + QString mSectionTitle; + //! Save window state void saveWindowState(); @@ -480,6 +491,7 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner QPrinter *printer(); QString reportTypeString(); void updateActionNames( QgsMasterLayoutInterface::Type type ); + void updateWindowTitle(); }; #endif // QGSLAYOUTDESIGNERDIALOG_H diff --git a/src/app/layout/qgsreportfieldgroupsectionwidget.cpp b/src/app/layout/qgsreportfieldgroupsectionwidget.cpp index 32306bca7b4c..b7f8ec959cd3 100644 --- a/src/app/layout/qgsreportfieldgroupsectionwidget.cpp +++ b/src/app/layout/qgsreportfieldgroupsectionwidget.cpp @@ -71,6 +71,7 @@ void QgsReportSectionFieldGroupWidget::editHeader() { mSection->header()->reportContext().setLayer( mSection->layer() ); mDesigner->setCurrentLayout( mSection->header() ); + mDesigner->setSectionTitle( tr( "%1 Header" ).arg( mSection->description() ) ); } } @@ -87,6 +88,7 @@ void QgsReportSectionFieldGroupWidget::editFooter() { mSection->footer()->reportContext().setLayer( mSection->layer() ); mDesigner->setCurrentLayout( mSection->footer() ); + mDesigner->setSectionTitle( tr( "%1 Footer" ).arg( mSection->description() ) ); } } @@ -108,6 +110,7 @@ void QgsReportSectionFieldGroupWidget::editBody() { mSection->body()->reportContext().setLayer( mSection->layer() ); mDesigner->setCurrentLayout( mSection->body() ); + mDesigner->setSectionTitle( tr( "%1 Body" ).arg( mSection->description() ) ); } } diff --git a/src/app/layout/qgsreportlayoutsectionwidget.cpp b/src/app/layout/qgsreportlayoutsectionwidget.cpp index 002a310708f6..51d0de815f4f 100644 --- a/src/app/layout/qgsreportlayoutsectionwidget.cpp +++ b/src/app/layout/qgsreportlayoutsectionwidget.cpp @@ -61,6 +61,7 @@ void QgsReportLayoutSectionWidget::editHeader() if ( mSection->header() ) { mDesigner->setCurrentLayout( mSection->header() ); + mDesigner->setSectionTitle( tr( "%1 Header" ).arg( mSection->description() ) ); } } @@ -76,6 +77,7 @@ void QgsReportLayoutSectionWidget::editFooter() if ( mSection->footer() ) { mDesigner->setCurrentLayout( mSection->footer() ); + mDesigner->setSectionTitle( tr( "%1 Footer" ).arg( mSection->description() ) ); } } @@ -94,4 +96,5 @@ void QgsReportLayoutSectionWidget::editBody() } mDesigner->setCurrentLayout( mSection->body() ); + mDesigner->setSectionTitle( tr( "%1 Body" ).arg( mSection->description() ) ); } diff --git a/src/app/layout/qgsreportsectionwidget.cpp b/src/app/layout/qgsreportsectionwidget.cpp index 237e3bf868c9..fc545bfce6f5 100644 --- a/src/app/layout/qgsreportsectionwidget.cpp +++ b/src/app/layout/qgsreportsectionwidget.cpp @@ -58,6 +58,7 @@ void QgsReportSectionWidget::editHeader() if ( mSection->header() ) { mDesigner->setCurrentLayout( mSection->header() ); + mDesigner->setSectionTitle( tr( "Report Header" ) ); } } @@ -73,6 +74,7 @@ void QgsReportSectionWidget::editFooter() if ( mSection->footer() ) { mDesigner->setCurrentLayout( mSection->footer() ); + mDesigner->setSectionTitle( tr( "Report Footer" ) ); } } From bf6c95d5756a834f443de38f51ce501fd595d516 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Fri, 5 Jan 2018 13:52:22 +1000 Subject: [PATCH 105/105] Add icons for report section types, and show pencil 'editing' icon in report organizer for section currently being edited in the designer This should help indicate to users which section is currently being edited and give more visual hints as to exactly what's happening in the ui. --- .../core/layout/qgsabstractreportsection.sip | 5 ++ .../layout/qgsreportsectionfieldgroup.sip | 2 + python/core/layout/qgsreportsectionlayout.sip | 2 + .../qgsreportfieldgroupsectionwidget.cpp | 7 ++- .../layout/qgsreportfieldgroupsectionwidget.h | 4 +- .../layout/qgsreportlayoutsectionwidget.cpp | 7 ++- src/app/layout/qgsreportlayoutsectionwidget.h | 4 +- src/app/layout/qgsreportorganizerwidget.cpp | 5 ++ src/app/layout/qgsreportorganizerwidget.h | 2 + src/app/layout/qgsreportsectionmodel.cpp | 48 +++++++++++++++++++ src/app/layout/qgsreportsectionmodel.h | 3 ++ src/app/layout/qgsreportsectionwidget.cpp | 6 ++- src/app/layout/qgsreportsectionwidget.h | 4 +- src/core/layout/qgsabstractreportsection.h | 5 ++ .../layout/qgsreportsectionfieldgroup.cpp | 5 ++ src/core/layout/qgsreportsectionfieldgroup.h | 1 + src/core/layout/qgsreportsectionlayout.cpp | 5 ++ src/core/layout/qgsreportsectionlayout.h | 1 + 18 files changed, 110 insertions(+), 6 deletions(-) diff --git a/python/core/layout/qgsabstractreportsection.sip b/python/core/layout/qgsabstractreportsection.sip index e23876084f31..93904b10eb02 100644 --- a/python/core/layout/qgsabstractreportsection.sip +++ b/python/core/layout/qgsabstractreportsection.sip @@ -84,6 +84,11 @@ Returns the section subclass type. virtual QString description() const = 0; %Docstring Returns a user-visible, translated description of the section. +%End + + virtual QIcon icon() const = 0; +%Docstring +Returns an icon representing the section. %End virtual QgsAbstractReportSection *clone() const = 0 /Factory/; diff --git a/python/core/layout/qgsreportsectionfieldgroup.sip b/python/core/layout/qgsreportsectionfieldgroup.sip index 7164de4d3774..0e675ad15345 100644 --- a/python/core/layout/qgsreportsectionfieldgroup.sip +++ b/python/core/layout/qgsreportsectionfieldgroup.sip @@ -37,6 +37,8 @@ Note that ownership is not transferred to ``parent``. virtual QString type() const; virtual QString description() const; + virtual QIcon icon() const; + QgsLayout *body(); %Docstring diff --git a/python/core/layout/qgsreportsectionlayout.sip b/python/core/layout/qgsreportsectionlayout.sip index 60c018c4786d..c366da21470d 100644 --- a/python/core/layout/qgsreportsectionlayout.sip +++ b/python/core/layout/qgsreportsectionlayout.sip @@ -35,6 +35,8 @@ Note that ownership is not transferred to ``parent``. virtual QString type() const; virtual QString description() const; + virtual QIcon icon() const; + QgsLayout *body(); %Docstring diff --git a/src/app/layout/qgsreportfieldgroupsectionwidget.cpp b/src/app/layout/qgsreportfieldgroupsectionwidget.cpp index b7f8ec959cd3..7a497dfdfa3a 100644 --- a/src/app/layout/qgsreportfieldgroupsectionwidget.cpp +++ b/src/app/layout/qgsreportfieldgroupsectionwidget.cpp @@ -18,9 +18,11 @@ #include "qgsreportsectionfieldgroup.h" #include "qgslayout.h" #include "qgslayoutdesignerdialog.h" +#include "qgsreportorganizerwidget.h" -QgsReportSectionFieldGroupWidget::QgsReportSectionFieldGroupWidget( QWidget *parent, QgsLayoutDesignerDialog *designer, QgsReportSectionFieldGroup *section ) +QgsReportSectionFieldGroupWidget::QgsReportSectionFieldGroupWidget( QgsReportOrganizerWidget *parent, QgsLayoutDesignerDialog *designer, QgsReportSectionFieldGroup *section ) : QWidget( parent ) + , mOrganizer( parent ) , mSection( section ) , mDesigner( designer ) { @@ -72,6 +74,7 @@ void QgsReportSectionFieldGroupWidget::editHeader() mSection->header()->reportContext().setLayer( mSection->layer() ); mDesigner->setCurrentLayout( mSection->header() ); mDesigner->setSectionTitle( tr( "%1 Header" ).arg( mSection->description() ) ); + mOrganizer->setEditedSection( mSection ); } } @@ -89,6 +92,7 @@ void QgsReportSectionFieldGroupWidget::editFooter() mSection->footer()->reportContext().setLayer( mSection->layer() ); mDesigner->setCurrentLayout( mSection->footer() ); mDesigner->setSectionTitle( tr( "%1 Footer" ).arg( mSection->description() ) ); + mOrganizer->setEditedSection( mSection ); } } @@ -111,6 +115,7 @@ void QgsReportSectionFieldGroupWidget::editBody() mSection->body()->reportContext().setLayer( mSection->layer() ); mDesigner->setCurrentLayout( mSection->body() ); mDesigner->setSectionTitle( tr( "%1 Body" ).arg( mSection->description() ) ); + mOrganizer->setEditedSection( mSection ); } } diff --git a/src/app/layout/qgsreportfieldgroupsectionwidget.h b/src/app/layout/qgsreportfieldgroupsectionwidget.h index bbc3d730819c..aeb7a7c6f62c 100644 --- a/src/app/layout/qgsreportfieldgroupsectionwidget.h +++ b/src/app/layout/qgsreportfieldgroupsectionwidget.h @@ -21,12 +21,13 @@ class QgsLayoutDesignerDialog; class QgsReportSectionFieldGroup; +class QgsReportOrganizerWidget; class QgsReportSectionFieldGroupWidget: public QWidget, private Ui::QgsReportWidgetFieldGroupSectionBase { Q_OBJECT public: - QgsReportSectionFieldGroupWidget( QWidget *parent, QgsLayoutDesignerDialog *designer, QgsReportSectionFieldGroup *section ); + QgsReportSectionFieldGroupWidget( QgsReportOrganizerWidget *parent, QgsLayoutDesignerDialog *designer, QgsReportSectionFieldGroup *section ); private slots: @@ -42,6 +43,7 @@ class QgsReportSectionFieldGroupWidget: public QWidget, private Ui::QgsReportWid private: + QgsReportOrganizerWidget *mOrganizer = nullptr; QgsReportSectionFieldGroup *mSection = nullptr; QgsLayoutDesignerDialog *mDesigner = nullptr; diff --git a/src/app/layout/qgsreportlayoutsectionwidget.cpp b/src/app/layout/qgsreportlayoutsectionwidget.cpp index 51d0de815f4f..6a9a51cd2e3a 100644 --- a/src/app/layout/qgsreportlayoutsectionwidget.cpp +++ b/src/app/layout/qgsreportlayoutsectionwidget.cpp @@ -18,9 +18,11 @@ #include "qgsreportsectionlayout.h" #include "qgslayout.h" #include "qgslayoutdesignerdialog.h" +#include "qgsreportorganizerwidget.h" -QgsReportLayoutSectionWidget::QgsReportLayoutSectionWidget( QWidget *parent, QgsLayoutDesignerDialog *designer, QgsReportSectionLayout *section ) +QgsReportLayoutSectionWidget::QgsReportLayoutSectionWidget( QgsReportOrganizerWidget *parent, QgsLayoutDesignerDialog *designer, QgsReportSectionLayout *section ) : QWidget( parent ) + , mOrganizer( parent ) , mSection( section ) , mDesigner( designer ) { @@ -62,6 +64,7 @@ void QgsReportLayoutSectionWidget::editHeader() { mDesigner->setCurrentLayout( mSection->header() ); mDesigner->setSectionTitle( tr( "%1 Header" ).arg( mSection->description() ) ); + mOrganizer->setEditedSection( mSection ); } } @@ -78,6 +81,7 @@ void QgsReportLayoutSectionWidget::editFooter() { mDesigner->setCurrentLayout( mSection->footer() ); mDesigner->setSectionTitle( tr( "%1 Footer" ).arg( mSection->description() ) ); + mOrganizer->setEditedSection( mSection ); } } @@ -97,4 +101,5 @@ void QgsReportLayoutSectionWidget::editBody() mDesigner->setCurrentLayout( mSection->body() ); mDesigner->setSectionTitle( tr( "%1 Body" ).arg( mSection->description() ) ); + mOrganizer->setEditedSection( mSection ); } diff --git a/src/app/layout/qgsreportlayoutsectionwidget.h b/src/app/layout/qgsreportlayoutsectionwidget.h index 82aa8bdb6897..3d3d4193d33b 100644 --- a/src/app/layout/qgsreportlayoutsectionwidget.h +++ b/src/app/layout/qgsreportlayoutsectionwidget.h @@ -21,12 +21,13 @@ class QgsLayoutDesignerDialog; class QgsReportSectionLayout; +class QgsReportOrganizerWidget; class QgsReportLayoutSectionWidget: public QWidget, private Ui::QgsReportWidgetLayoutSectionBase { Q_OBJECT public: - QgsReportLayoutSectionWidget( QWidget *parent, QgsLayoutDesignerDialog *designer, QgsReportSectionLayout *section ); + QgsReportLayoutSectionWidget( QgsReportOrganizerWidget *parent, QgsLayoutDesignerDialog *designer, QgsReportSectionLayout *section ); private slots: @@ -39,6 +40,7 @@ class QgsReportLayoutSectionWidget: public QWidget, private Ui::QgsReportWidgetL private: + QgsReportOrganizerWidget *mOrganizer = nullptr; QgsReportSectionLayout *mSection = nullptr; QgsLayoutDesignerDialog *mDesigner = nullptr; diff --git a/src/app/layout/qgsreportorganizerwidget.cpp b/src/app/layout/qgsreportorganizerwidget.cpp index c8f941d9da98..740253e59e2f 100644 --- a/src/app/layout/qgsreportorganizerwidget.cpp +++ b/src/app/layout/qgsreportorganizerwidget.cpp @@ -74,6 +74,11 @@ void QgsReportOrganizerWidget::setMessageBar( QgsMessageBar *bar ) mMessageBar = bar; } +void QgsReportOrganizerWidget::setEditedSection( QgsAbstractReportSection *section ) +{ + mSectionModel->setEditedSection( section ); +} + void QgsReportOrganizerWidget::addLayoutSection() { std::unique_ptr< QgsReportSectionLayout > section = qgis::make_unique< QgsReportSectionLayout >(); diff --git a/src/app/layout/qgsreportorganizerwidget.h b/src/app/layout/qgsreportorganizerwidget.h index 7184d9a7361e..5cad6a0423da 100644 --- a/src/app/layout/qgsreportorganizerwidget.h +++ b/src/app/layout/qgsreportorganizerwidget.h @@ -25,6 +25,7 @@ class QgsReportSectionModel; class QgsReport; class QgsMessageBar; class QgsLayoutDesignerDialog ; +class QgsAbstractReportSection; class QgsReportOrganizerWidget: public QgsPanelWidget, private Ui::QgsReportOrganizerBase { @@ -33,6 +34,7 @@ class QgsReportOrganizerWidget: public QgsPanelWidget, private Ui::QgsReportOrga QgsReportOrganizerWidget( QWidget *parent, QgsLayoutDesignerDialog *designer, QgsReport *report ); void setMessageBar( QgsMessageBar *bar ); + void setEditedSection( QgsAbstractReportSection *section ); private slots: diff --git a/src/app/layout/qgsreportsectionmodel.cpp b/src/app/layout/qgsreportsectionmodel.cpp index 78b910261f7b..8a8dd5cd7099 100644 --- a/src/app/layout/qgsreportsectionmodel.cpp +++ b/src/app/layout/qgsreportsectionmodel.cpp @@ -58,6 +58,34 @@ QVariant QgsReportSectionModel::data( const QModelIndex &index, int role ) const break; } + case Qt::DecorationRole: + switch ( index.column() ) + { + case 0: + { + QIcon icon = section->icon(); + + if ( section == mEditedSection ) + { + QPixmap pixmap( icon.pixmap( 16, 16 ) ); + + QPainter painter( &pixmap ); + painter.drawPixmap( 0, 0, 16, 16, QgsApplication::getThemePixmap( "/mActionToggleEditing.svg" ) ); + painter.end(); + + return QIcon( pixmap ); + } + else + { + return icon; + } + } + + default: + return QVariant(); + } + break; + case Qt::TextAlignmentRole: { return ( index.column() == 2 || index.column() == 3 ) ? Qt::AlignRight : Qt::AlignLeft; @@ -211,6 +239,26 @@ QModelIndex QgsReportSectionModel::indexForSection( QgsAbstractReportSection *se return findIndex( QModelIndex(), section ); } +void QgsReportSectionModel::setEditedSection( QgsAbstractReportSection *section ) +{ + QModelIndex oldSection; + if ( mEditedSection ) + { + oldSection = indexForSection( mEditedSection ); + } + + mEditedSection = section; + if ( oldSection.isValid() ) + emit dataChanged( oldSection, oldSection, QVector() << Qt::DecorationRole ); + + if ( mEditedSection ) + { + QModelIndex newSection = indexForSection( mEditedSection ); + emit dataChanged( newSection, newSection, QVector() << Qt::DecorationRole ); + } + +} + bool QgsReportSectionModel::removeRows( int row, int count, const QModelIndex &parent ) { QgsAbstractReportSection *parentSection = sectionForIndex( parent ); diff --git a/src/app/layout/qgsreportsectionmodel.h b/src/app/layout/qgsreportsectionmodel.h index 95fbe2677b74..c9f5ae1ea892 100644 --- a/src/app/layout/qgsreportsectionmodel.h +++ b/src/app/layout/qgsreportsectionmodel.h @@ -59,8 +59,11 @@ class QgsReportSectionModel : public QAbstractItemModel QModelIndex indexForSection( QgsAbstractReportSection *section ) const; + void setEditedSection( QgsAbstractReportSection *section ); + private: QgsReport *mReport = nullptr; + QgsAbstractReportSection *mEditedSection = nullptr; }; #endif // QGSREPORTSECTIONMODEL_H diff --git a/src/app/layout/qgsreportsectionwidget.cpp b/src/app/layout/qgsreportsectionwidget.cpp index fc545bfce6f5..06a96a23b995 100644 --- a/src/app/layout/qgsreportsectionwidget.cpp +++ b/src/app/layout/qgsreportsectionwidget.cpp @@ -18,9 +18,11 @@ #include "qgsreport.h" #include "qgslayout.h" #include "qgslayoutdesignerdialog.h" +#include "qgsreportorganizerwidget.h" -QgsReportSectionWidget::QgsReportSectionWidget( QWidget *parent, QgsLayoutDesignerDialog *designer, QgsReport *section ) +QgsReportSectionWidget::QgsReportSectionWidget( QgsReportOrganizerWidget *parent, QgsLayoutDesignerDialog *designer, QgsReport *section ) : QWidget( parent ) + , mOrganizer( parent ) , mSection( section ) , mDesigner( designer ) { @@ -59,6 +61,7 @@ void QgsReportSectionWidget::editHeader() { mDesigner->setCurrentLayout( mSection->header() ); mDesigner->setSectionTitle( tr( "Report Header" ) ); + mOrganizer->setEditedSection( mSection ); } } @@ -75,6 +78,7 @@ void QgsReportSectionWidget::editFooter() { mDesigner->setCurrentLayout( mSection->footer() ); mDesigner->setSectionTitle( tr( "Report Footer" ) ); + mOrganizer->setEditedSection( mSection ); } } diff --git a/src/app/layout/qgsreportsectionwidget.h b/src/app/layout/qgsreportsectionwidget.h index e2c99bba35a2..38a558a4e09f 100644 --- a/src/app/layout/qgsreportsectionwidget.h +++ b/src/app/layout/qgsreportsectionwidget.h @@ -21,12 +21,13 @@ class QgsLayoutDesignerDialog; class QgsReport; +class QgsReportOrganizerWidget; class QgsReportSectionWidget: public QWidget, private Ui::QgsReportWidgetSectionBase { Q_OBJECT public: - QgsReportSectionWidget( QWidget *parent, QgsLayoutDesignerDialog *designer, QgsReport *section ); + QgsReportSectionWidget( QgsReportOrganizerWidget *parent, QgsLayoutDesignerDialog *designer, QgsReport *section ); private slots: @@ -37,6 +38,7 @@ class QgsReportSectionWidget: public QWidget, private Ui::QgsReportWidgetSection private: + QgsReportOrganizerWidget *mOrganizer = nullptr; QgsReport *mSection = nullptr; QgsLayoutDesignerDialog *mDesigner = nullptr; diff --git a/src/core/layout/qgsabstractreportsection.h b/src/core/layout/qgsabstractreportsection.h index 4509b8cc0c95..e6e802ab2cb3 100644 --- a/src/core/layout/qgsabstractreportsection.h +++ b/src/core/layout/qgsabstractreportsection.h @@ -96,6 +96,11 @@ class CORE_EXPORT QgsAbstractReportSection : public QgsAbstractLayoutIterator */ virtual QString description() const = 0; + /** + * Returns an icon representing the section. + */ + virtual QIcon icon() const = 0; + /** * Clones the report section. Ownership of the returned section is * transferred to the caller. diff --git a/src/core/layout/qgsreportsectionfieldgroup.cpp b/src/core/layout/qgsreportsectionfieldgroup.cpp index 624abbb8c079..ff8154e104eb 100644 --- a/src/core/layout/qgsreportsectionfieldgroup.cpp +++ b/src/core/layout/qgsreportsectionfieldgroup.cpp @@ -30,6 +30,11 @@ QString QgsReportSectionFieldGroup::description() const return QObject::tr( "Group: %1" ).arg( mField ); } +QIcon QgsReportSectionFieldGroup::icon() const +{ + return QgsApplication::getThemeIcon( QStringLiteral( "/mIconFieldText.svg" ) ); +} + QgsReportSectionFieldGroup *QgsReportSectionFieldGroup::clone() const { std::unique_ptr< QgsReportSectionFieldGroup > copy = qgis::make_unique< QgsReportSectionFieldGroup >( nullptr ); diff --git a/src/core/layout/qgsreportsectionfieldgroup.h b/src/core/layout/qgsreportsectionfieldgroup.h index 01fdde4fd650..8efd08812fc6 100644 --- a/src/core/layout/qgsreportsectionfieldgroup.h +++ b/src/core/layout/qgsreportsectionfieldgroup.h @@ -46,6 +46,7 @@ class CORE_EXPORT QgsReportSectionFieldGroup : public QgsAbstractReportSection QString type() const override { return QStringLiteral( "SectionFieldGroup" ); } QString description() const override; + QIcon icon() const override; /** * Returns the body layout for the section. diff --git a/src/core/layout/qgsreportsectionlayout.cpp b/src/core/layout/qgsreportsectionlayout.cpp index 285f334cd8da..72f9fee6a5ce 100644 --- a/src/core/layout/qgsreportsectionlayout.cpp +++ b/src/core/layout/qgsreportsectionlayout.cpp @@ -23,6 +23,11 @@ QgsReportSectionLayout::QgsReportSectionLayout( QgsAbstractReportSection *parent : QgsAbstractReportSection( parent ) {} +QIcon QgsReportSectionLayout::icon() const +{ + return QgsApplication::getThemeIcon( QStringLiteral( "/mActionNewComposer.svg" ) ); +} + QgsReportSectionLayout *QgsReportSectionLayout::clone() const { std::unique_ptr< QgsReportSectionLayout > copy = qgis::make_unique< QgsReportSectionLayout >( nullptr ); diff --git a/src/core/layout/qgsreportsectionlayout.h b/src/core/layout/qgsreportsectionlayout.h index 6dbc51f692b7..67c75a1e502d 100644 --- a/src/core/layout/qgsreportsectionlayout.h +++ b/src/core/layout/qgsreportsectionlayout.h @@ -43,6 +43,7 @@ class CORE_EXPORT QgsReportSectionLayout : public QgsAbstractReportSection QString type() const override { return QStringLiteral( "SectionLayout" ); } QString description() const override { return QObject::tr( "Section" ); } + QIcon icon() const override; /** * Returns the body layout for the section.