diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index 79bf38c199d..1342a112ef0 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 00000000000..cd73fb4770a --- /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 2dda74ed101..d1a766009b0 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 00000000000..cfbb58ed73b --- /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 00000000000..8391ec1de91 --- /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 39c76e0e85e..3b7099f4cb4 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 00000000000..11e3b0641e0 --- /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()