From a9896eac05c14f6ff7ffbe5873ea5fa10fc29adf Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 4 Dec 2017 16:58:52 +1000 Subject: [PATCH] Port a bunch of QgsLayoutManager methods to use QgsLayouts --- python/core/layout/qgslayout.sip | 6 + src/app/layout/qgslayoutdesignerdialog.cpp | 1 + src/app/qgisapp.cpp | 6 +- src/core/composer/qgslayoutmanager.cpp | 90 ++++++++++-- src/core/composer/qgslayoutmanager.h | 66 ++++++++- src/core/layout/qgslayout.cpp | 6 + src/core/layout/qgslayout.h | 9 +- tests/src/python/test_qgslayoutmanager.py | 160 +++++++++++++++++++-- 8 files changed, 315 insertions(+), 29 deletions(-) diff --git a/python/core/layout/qgslayout.sip b/python/core/layout/qgslayout.sip index 2694819e390c..78168e95b635 100644 --- a/python/core/layout/qgslayout.sip +++ b/python/core/layout/qgslayout.sip @@ -537,6 +537,12 @@ class QgsLayout : QGraphicsScene, QgsExpressionContextGenerator, QgsLayoutUndoOb and updated. %End + void nameChanged( const QString &name ); +%Docstring + Emitted when the layout's name is changed. +.. seealso:: setName() +%End + }; diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index 1af2ece448f9..1cce9a73a005 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -633,6 +633,7 @@ void QgsLayoutDesignerDialog::setCurrentLayout( QgsLayout *layout ) { mLayout->guides().clear(); } ); + connect( mLayout, &QgsLayout::nameChanged, this, &QgsLayoutDesignerDialog::setWindowTitle ); mActionShowGrid->setChecked( mLayout->context().gridVisible() ); mActionSnapGrid->setChecked( mLayout->snapper().snapToGrid() ); diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 04514bcc90d4..4a1b9c07759f 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -7315,7 +7315,7 @@ bool QgisApp::uniqueComposerTitle( QWidget *parent, QString &composerTitle, bool else { titleValid = true; - newTitle = QgsProject::instance()->layoutManager()->generateUniqueTitle(); + newTitle = QgsProject::instance()->layoutManager()->generateUniqueComposerTitle(); } } else if ( cNames.indexOf( newTitle, 1 ) >= 0 ) @@ -7380,7 +7380,7 @@ bool QgisApp::uniqueLayoutTitle( QWidget *parent, QString &title, bool acceptEmp else { titleValid = true; - newTitle = QgsProject::instance()->layoutManager()->generateUniqueTitle(); + newTitle = QgsProject::instance()->layoutManager()->generateUniqueComposerTitle(); } } else if ( cNames.indexOf( newTitle, 1 ) >= 0 ) @@ -7403,7 +7403,7 @@ QgsComposer *QgisApp::createNewComposer( QString title ) { if ( title.isEmpty() ) { - title = QgsProject::instance()->layoutManager()->generateUniqueTitle(); + title = QgsProject::instance()->layoutManager()->generateUniqueComposerTitle(); } //create new composition object QgsComposition *composition = new QgsComposition( QgsProject::instance() ); diff --git a/src/core/composer/qgslayoutmanager.cpp b/src/core/composer/qgslayoutmanager.cpp index cc33b2fff56e..16c7108cae0d 100644 --- a/src/core/composer/qgslayoutmanager.cpp +++ b/src/core/composer/qgslayoutmanager.cpp @@ -53,6 +53,29 @@ bool QgsLayoutManager::addComposition( QgsComposition *composition ) return true; } +bool QgsLayoutManager::addLayout( QgsLayout *layout ) +{ + if ( !layout ) + return false; + + // check for duplicate name + for ( QgsLayout *l : qgis::as_const( mLayouts ) ) + { + if ( l->name() == layout->name() ) + return false; + } + + connect( layout, &QgsLayout::nameChanged, this, [this, layout]( const QString & newName ) + { + emit layoutRenamed( layout, newName ); + } ); + emit layoutAboutToBeAdded( layout->name() ); + mLayouts << layout; + emit layoutAdded( layout->name() ); + mProject->setDirty( true ); + return true; +} + bool QgsLayoutManager::removeComposition( QgsComposition *composition ) { if ( !composition ) @@ -70,12 +93,34 @@ bool QgsLayoutManager::removeComposition( QgsComposition *composition ) return true; } +bool QgsLayoutManager::removeLayout( QgsLayout *layout ) +{ + if ( !layout ) + return false; + + if ( !mLayouts.contains( layout ) ) + return false; + + QString name = layout->name(); + emit layoutAboutToBeRemoved( name ); + mLayouts.removeAll( layout ); + delete layout; + emit layoutRemoved( name ); + mProject->setDirty( true ); + return true; +} + void QgsLayoutManager::clear() { Q_FOREACH ( QgsComposition *c, mCompositions ) { removeComposition( c ); } + const QList< QgsLayout * > layouts = mLayouts; + for ( QgsLayout *l : layouts ) + { + removeLayout( l ); + } } QList QgsLayoutManager::compositions() const @@ -83,6 +128,11 @@ QList QgsLayoutManager::compositions() const return mCompositions; } +QList QgsLayoutManager::layouts() const +{ + return mLayouts; +} + QgsComposition *QgsLayoutManager::compositionByName( const QString &name ) const { Q_FOREACH ( QgsComposition *c, mCompositions ) @@ -93,6 +143,16 @@ QgsComposition *QgsLayoutManager::compositionByName( const QString &name ) const return nullptr; } +QgsLayout *QgsLayoutManager::layoutByName( const QString &name ) const +{ + for ( QgsLayout *l : mLayouts ) + { + if ( l->name() == name ) + return l; + } + return nullptr; +} + bool QgsLayoutManager::readXml( const QDomElement &element, const QDomDocument &doc ) { clear(); @@ -204,22 +264,19 @@ QgsLayout *QgsLayoutManager::duplicateLayout( const QgsLayout *layout, const QSt } newLayout->setName( newName ); -#if 0 //TODO - if ( !addComposition( newComposition ) ) + QgsLayout *l = newLayout.get(); + if ( !addLayout( l ) ) { - delete newComposition; return nullptr; } else { - return newComposition; + ( void )newLayout.release(); //ownership was transferred successfully + return l; } -#endif - - return newLayout.release(); } -QString QgsLayoutManager::generateUniqueTitle() const +QString QgsLayoutManager::generateUniqueComposerTitle() const { QStringList names; Q_FOREACH ( QgsComposition *c, mCompositions ) @@ -236,6 +293,23 @@ QString QgsLayoutManager::generateUniqueTitle() const return name; } +QString QgsLayoutManager::generateUniqueTitle() const +{ + QStringList names; + for ( QgsLayout *l : mLayouts ) + { + names << l->name(); + } + QString name; + int id = 1; + while ( name.isEmpty() || names.contains( name ) ) + { + name = tr( "Layout %1" ).arg( id ); + id++; + } + return name; +} + QgsComposition *QgsLayoutManager::createCompositionFromXml( const QDomElement &element, const QDomDocument &doc ) const { QDomNodeList compositionNodeList = element.elementsByTagName( QStringLiteral( "Composition" ) ); diff --git a/src/core/composer/qgslayoutmanager.h b/src/core/composer/qgslayoutmanager.h index da7378cb1118..5a70b897ce41 100644 --- a/src/core/composer/qgslayoutmanager.h +++ b/src/core/composer/qgslayoutmanager.h @@ -19,6 +19,7 @@ #include "qgis_core.h" #include "qgis.h" #include "qgscomposition.h" +#include "qgslayout.h" #include class QgsProject; @@ -28,13 +29,13 @@ class QgsProject; * \class QgsLayoutManager * \since QGIS 3.0 * - * \brief Manages storage of a set of compositions. + * \brief Manages storage of a set of layouts. * * QgsLayoutManager handles the storage, serializing and deserializing - * of QgsCompositions. Usually this class is not constructed directly, but + * of QgsLayouts. Usually this class is not constructed directly, but * rather accessed through a QgsProject via QgsProject::layoutManager(). * - * QgsLayoutManager retains ownership of all the compositions contained + * QgsLayoutManager retains ownership of all the layouts contained * in the manager. */ @@ -61,6 +62,15 @@ class CORE_EXPORT QgsLayoutManager : public QObject */ bool addComposition( QgsComposition *composition SIP_TRANSFER ); + /** + * Adds a \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 + * as a result of a duplicate layout name). + * \see removeLayout() + * \see layoutAdded() + */ + bool addLayout( QgsLayout *layout SIP_TRANSFER ); + /** * Removes a composition from the manager. The composition is deleted. * Returns true if the removal was successful, or false if the removal failed (eg as a result @@ -73,8 +83,19 @@ class CORE_EXPORT QgsLayoutManager : public QObject bool removeComposition( QgsComposition *composition ); /** - * Removes and deletes all compositions from the manager. - * \see removeComposition() + * Removes a \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 + * of removing a layout which is not contained in the manager). + * \see addLayout() + * \see layoutRemoved() + * \see layoutAboutToBeRemoved() + * \see clear() + */ + bool removeLayout( QgsLayout *layout ); + + /** + * Removes and deletes all layouts from the manager. + * \see removeLayout() */ void clear(); @@ -83,6 +104,11 @@ class CORE_EXPORT QgsLayoutManager : public QObject */ QList< QgsComposition * > compositions() const; + /** + * Returns a list of all layouts contained in the manager. + */ + QList< QgsLayout * > layouts() const; + /** * Returns the composition with a matching name, or nullptr if no matching compositions * were found. @@ -90,7 +116,13 @@ class CORE_EXPORT QgsLayoutManager : public QObject QgsComposition *compositionByName( const QString &name ) const; /** - * Reads the manager's state from a DOM element, restoring all compositions + * Returns the layout with a matching name, or nullptr if no matching layouts + * were found. + */ + QgsLayout *layoutByName( const QString &name ) const; + + /** + * Reads the manager's state from a DOM element, restoring all layouts * present in the XML document. * \see writeXml() */ @@ -126,6 +158,12 @@ class CORE_EXPORT QgsLayoutManager : public QObject * Generates a unique title for a new composition, which does not * clash with any already contained by the manager. */ + QString generateUniqueComposerTitle() const; + + /** + * Generates a unique title for a new layout, which does not + * clash with any already contained by the manager. + */ QString generateUniqueTitle() const; signals: @@ -133,23 +171,39 @@ class CORE_EXPORT QgsLayoutManager : public QObject //! Emitted when a composition is about to be added to the manager void compositionAboutToBeAdded( const QString &name ); + //! Emitted when a layout is about to be added to the manager + void layoutAboutToBeAdded( const QString &name ); + //! Emitted when a composition has been added to the manager void compositionAdded( const QString &name ); + //! Emitted when a layout has been added to the manager + void layoutAdded( const QString &name ); + //! Emitted when a composition was removed from the manager void compositionRemoved( const QString &name ); + //! Emitted when a layout was removed from the manager + void layoutRemoved( const QString &name ); + //! Emitted when a composition is about to be removed from the manager void compositionAboutToBeRemoved( const QString &name ); + //! Emitted when a layout is about to be removed from the manager + void layoutAboutToBeRemoved( const QString &name ); + //! Emitted when a composition is renamed void compositionRenamed( QgsComposition *composition, const QString &newName ); + //! Emitted when a layout is renamed + void layoutRenamed( QgsLayout *layout, const QString &newName ); + private: QgsProject *mProject = nullptr; QList< QgsComposition * > mCompositions; + QList< QgsLayout * > mLayouts; QgsComposition *createCompositionFromXml( const QDomElement &element, const QDomDocument &doc ) const; diff --git a/src/core/layout/qgslayout.cpp b/src/core/layout/qgslayout.cpp index 62ad3e59e3f5..f26243fe1271 100644 --- a/src/core/layout/qgslayout.cpp +++ b/src/core/layout/qgslayout.cpp @@ -117,6 +117,12 @@ QgsLayoutExporter &QgsLayout::exporter() return mExporter; } +void QgsLayout::setName( const QString &name ) +{ + mName = name; + emit nameChanged( name ); +} + QList QgsLayout::selectedLayoutItems( const bool includeLockedItems ) { QList layoutItemList; diff --git a/src/core/layout/qgslayout.h b/src/core/layout/qgslayout.h index 76a5a413f22b..3c964494a34b 100644 --- a/src/core/layout/qgslayout.h +++ b/src/core/layout/qgslayout.h @@ -40,6 +40,7 @@ class QgsLayoutMultiFrame; class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContextGenerator, public QgsLayoutUndoObjectInterface { Q_OBJECT + Q_PROPERTY( QString name READ name WRITE setName NOTIFY nameChanged ) public: @@ -108,7 +109,7 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext * Sets the layout's name. * \see name() */ - void setName( const QString &name ) { mName = name; } + void setName( const QString &name ); /** * Returns a list of layout items of a specific type. @@ -585,6 +586,12 @@ 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; diff --git a/tests/src/python/test_qgslayoutmanager.py b/tests/src/python/test_qgslayoutmanager.py index b2e8fb66e241..899621a3834b 100644 --- a/tests/src/python/test_qgslayoutmanager.py +++ b/tests/src/python/test_qgslayoutmanager.py @@ -18,6 +18,7 @@ from qgis.PyQt.QtXml import QDomDocument from qgis.core import (QgsComposition, + QgsLayout, QgsLayoutManager, QgsProject) @@ -73,6 +74,38 @@ def testAddComposition(self): composition3.setName('test composition2') self.assertFalse(manager.addComposition(composition3)) + def testAddLayout(self): + project = QgsProject() + layout = QgsLayout(project) + layout.setName('test layout') + + manager = QgsLayoutManager(project) + + layout_about_to_be_added_spy = QSignalSpy(manager.layoutAboutToBeAdded) + layout_added_spy = QSignalSpy(manager.layoutAdded) + self.assertTrue(manager.addLayout(layout)) + self.assertEqual(len(layout_about_to_be_added_spy), 1) + self.assertEqual(layout_about_to_be_added_spy[0][0], 'test layout') + self.assertEqual(len(layout_added_spy), 1) + self.assertEqual(layout_added_spy[0][0], 'test layout') + + # adding it again should fail + self.assertFalse(manager.addLayout(layout)) + + # try adding a second layout + layout2 = QgsLayout(project) + layout2.setName('test layout2') + self.assertTrue(manager.addLayout(layout2)) + self.assertEqual(len(layout_added_spy), 2) + self.assertEqual(layout_about_to_be_added_spy[1][0], 'test layout2') + self.assertEqual(len(layout_about_to_be_added_spy), 2) + self.assertEqual(layout_added_spy[1][0], 'test layout2') + + # adding a layout with duplicate name should fail + layout3 = QgsLayout(project) + layout3.setName('test layout2') + self.assertFalse(manager.addLayout(layout3)) + def testCompositions(self): project = QgsProject() manager = QgsLayoutManager(project) @@ -90,6 +123,23 @@ def testCompositions(self): manager.addComposition(composition3) self.assertEqual(set(manager.compositions()), {composition, composition2, composition3}) + def testLayouts(self): + project = QgsProject() + manager = QgsLayoutManager(project) + layout = QgsLayout(project) + layout.setName('test layout') + layout2 = QgsLayout(project) + layout2.setName('test layout2') + layout3 = QgsLayout(project) + layout3.setName('test layout3') + + manager.addLayout(layout) + self.assertEqual(manager.layouts(), [layout]) + manager.addLayout(layout2) + self.assertEqual(set(manager.layouts()), {layout, layout2}) + manager.addLayout(layout3) + self.assertEqual(set(manager.layouts()), {layout, layout2, layout3}) + def aboutToBeRemoved(self, name): # composition should still exist at this time self.assertEqual(name, 'test composition') @@ -123,6 +173,39 @@ def testRemoveComposition(self): self.assertTrue(self.aboutFired) self.manager = None + def layoutAboutToBeRemoved(self, name): + # layout should still exist at this time + self.assertEqual(name, 'test layout') + self.assertTrue(self.manager.layoutByName('test layout')) + self.aboutFired = True + + def testRemoveLayout(self): + project = QgsProject() + layout = QgsLayout(project) + layout.setName('test layout') + + self.manager = QgsLayoutManager(project) + layout_removed_spy = QSignalSpy(self.manager.layoutRemoved) + layout_about_to_be_removed_spy = QSignalSpy(self.manager.layoutAboutToBeRemoved) + # tests that layout still exists when layoutAboutToBeRemoved is fired + self.manager.layoutAboutToBeRemoved.connect(self.layoutAboutToBeRemoved) + + # not added, should fail + self.assertFalse(self.manager.removeLayout(layout)) + self.assertEqual(len(layout_removed_spy), 0) + self.assertEqual(len(layout_about_to_be_removed_spy), 0) + + self.assertTrue(self.manager.addLayout(layout)) + self.assertEqual(self.manager.layouts(), [layout]) + self.assertTrue(self.manager.removeLayout(layout)) + self.assertEqual(len(self.manager.layouts()), 0) + self.assertEqual(len(layout_removed_spy), 1) + self.assertEqual(layout_removed_spy[0][0], 'test layout') + self.assertEqual(len(layout_about_to_be_removed_spy), 1) + self.assertEqual(layout_about_to_be_removed_spy[0][0], 'test layout') + self.assertTrue(self.aboutFired) + self.manager = None + def testClear(self): project = QgsProject() manager = QgsLayoutManager(project) @@ -134,17 +217,32 @@ def testClear(self): composition2.setName('test composition2') composition3 = QgsComposition(project) composition3.setName('test composition3') + # add a bunch of layouts + layout = QgsLayout(project) + layout.setName('test layout') + layout2 = QgsLayout(project) + layout2.setName('test layout2') + layout3 = QgsLayout(project) + layout3.setName('test layout3') manager.addComposition(composition) manager.addComposition(composition2) manager.addComposition(composition3) + manager.addLayout(layout) + manager.addLayout(layout2) + manager.addLayout(layout3) composition_removed_spy = QSignalSpy(manager.compositionRemoved) composition_about_to_be_removed_spy = QSignalSpy(manager.compositionAboutToBeRemoved) + layout_removed_spy = QSignalSpy(manager.layoutRemoved) + layout_about_to_be_removed_spy = QSignalSpy(manager.layoutAboutToBeRemoved) manager.clear() self.assertEqual(len(manager.compositions()), 0) self.assertEqual(len(composition_removed_spy), 3) self.assertEqual(len(composition_about_to_be_removed_spy), 3) + self.assertEqual(len(manager.layouts()), 0) + self.assertEqual(len(layout_removed_spy), 3) + self.assertEqual(len(layout_about_to_be_removed_spy), 3) def testCompositionByName(self): project = QgsProject() @@ -167,6 +265,27 @@ def testCompositionByName(self): self.assertEqual(manager.compositionByName('test composition2'), composition2) self.assertEqual(manager.compositionByName('test composition3'), composition3) + def testLayoutsByName(self): + project = QgsProject() + manager = QgsLayoutManager(project) + + # add a bunch of layouts + layout = QgsLayout(project) + layout.setName('test layout') + layout2 = QgsLayout(project) + layout2.setName('test layout2') + layout3 = QgsLayout(project) + layout3.setName('test layout3') + + manager.addLayout(layout) + manager.addLayout(layout2) + manager.addLayout(layout3) + + self.assertFalse(manager.layoutByName('asdf')) + self.assertEqual(manager.layoutByName('test layout'), layout) + self.assertEqual(manager.layoutByName('test layout2'), layout2) + self.assertEqual(manager.layoutByName('test layout3'), layout3) + def testReadWriteXml(self): """ Test reading and writing layout manager state to XML @@ -241,22 +360,22 @@ def testDuplicateComposition(self): def testGenerateUniqueTitle(self): project = QgsProject() manager = QgsLayoutManager(project) - self.assertEqual(manager.generateUniqueTitle(), 'Composer 1') + self.assertEqual(manager.generateUniqueTitle(), 'Layout 1') - composition = QgsComposition(project) - composition.setName(manager.generateUniqueTitle()) - manager.addComposition(composition) + layout = QgsLayout(project) + layout.setName(manager.generateUniqueTitle()) + manager.addLayout(layout) - self.assertEqual(manager.generateUniqueTitle(), 'Composer 2') - composition2 = QgsComposition(project) - composition2.setName(manager.generateUniqueTitle()) - manager.addComposition(composition2) + self.assertEqual(manager.generateUniqueTitle(), 'Layout 2') + layout2 = QgsLayout(project) + layout2.setName(manager.generateUniqueTitle()) + manager.addLayout(layout2) - self.assertEqual(manager.generateUniqueTitle(), 'Composer 3') + self.assertEqual(manager.generateUniqueTitle(), 'Layout 3') manager.clear() - self.assertEqual(manager.generateUniqueTitle(), 'Composer 1') + self.assertEqual(manager.generateUniqueTitle(), 'Layout 1') - def testRenameSignal(self): + def testRenameSignalCompositions(self): project = QgsProject() manager = QgsLayoutManager(project) composition = QgsComposition(project) @@ -276,6 +395,25 @@ def testRenameSignal(self): self.assertEqual(composition_renamed_spy[1][0], composition2) self.assertEqual(composition_renamed_spy[1][1], 'd2') + def testRenameSignal(self): + project = QgsProject() + manager = QgsLayoutManager(project) + layout = QgsLayout(project) + layout.setName('c1') + manager.addLayout(layout) + layout2 = QgsLayout(project) + layout2.setName('c2') + manager.addLayout(layout2) + + layout_renamed_spy = QSignalSpy(manager.layoutRenamed) + layout.setName('d1') + self.assertEqual(len(layout_renamed_spy), 1) + #self.assertEqual(layout_renamed_spy[0][0], layout) + self.assertEqual(layout_renamed_spy[0][1], 'd1') + layout2.setName('d2') + self.assertEqual(len(layout_renamed_spy), 2) + #self.assertEqual(layout_renamed_spy[1][0], layout2) + self.assertEqual(layout_renamed_spy[1][1], 'd2') if __name__ == '__main__': unittest.main()