Start on reflow support for page collections (needs tests)

This commit is contained in:
Nyall Dawson 2017-07-19 19:21:10 +10:00
parent 79a4694177
commit 39bf23a5d5
9 changed files with 389 additions and 2 deletions

View File

@ -29,6 +29,16 @@ class QgsLayout : QGraphicsScene, QgsExpressionContextGenerator
QgsLayout( QgsProject *project );
%Docstring
Construct a new layout linked to the specified ``project``.
If the layout is a "new" layout (as opposed to a layout which will
restore a previous state from XML) then initializeDefaults() should be
called on the new layout.
%End
void initializeDefaults();
%Docstring
Initializes an empty layout, e.g. by adding a default page to the layout. This should be called after creating
a new layout.
%End
QgsProject *project() const;
@ -191,6 +201,29 @@ class QgsLayout : QGraphicsScene, QgsExpressionContextGenerator
:rtype: QgsLayoutPageCollection
%End
QRectF layoutBounds( bool ignorePages = false, double margin = 0.0 ) const;
%Docstring
Calculates the bounds of all non-gui items in the layout. Ignores snap lines, mouse handles
and other cosmetic items.
\param ignorePages set to true to ignore page items
\param margin optional marginal (in percent, e.g., 0.05 = 5% ) to add around items
:return: layout bounds, in layout units.
:rtype: QRectF
%End
void addLayoutItem( QgsLayoutItem *item /Transfer/ );
%Docstring
Adds an ``item`` to the layout. This should be called instead of the base class addItem()
method. Ownership of the item is transferred to the layout.
%End
public slots:
void updateBounds();
%Docstring
Updates the scene bounds of the layout.
%End
signals:
void variablesChanged();

View File

@ -65,6 +65,9 @@ class QgsLayoutPageCollection : QObject
to the collection, and the page will automatically be added to the collection's
layout() (there is no need to manually add the page item to the layout).
The page will be added after all pages currently contained in the collection.
Calling addPage() automatically triggers a reflow() of pages.
.. seealso:: insertPage()
%End
@ -80,6 +83,8 @@ class QgsLayoutPageCollection : QObject
(Page numbers in collections begin at 0 - so a ``beforePage`` of 0 will insert
the page before all existing pages).
Calling insertPage() automatically triggers a reflow() of pages.
.. seealso:: addPage()
%End
@ -90,6 +95,8 @@ class QgsLayoutPageCollection : QObject
Page numbers in collections begin at 0 - so a ``pageNumber`` of 0 will delete
the first page in the collection.
Calling deletePage() automatically triggers a reflow() of pages.
%End
void setPageStyleSymbol( QgsFillSymbol *symbol );
@ -107,6 +114,19 @@ class QgsLayoutPageCollection : QObject
:rtype: QgsFillSymbol
%End
void reflow();
%Docstring
Forces the page collection to reflow the arrangement of pages, e.g. to account
for page size/orientation change.
%End
double maximumPageWidth() const;
%Docstring
Returns the maximum width of pages in the collection. The returned value is
in layout units.
:rtype: float
%End
};
/************************************************************************

View File

@ -25,6 +25,14 @@ QgsLayout::QgsLayout( QgsProject *project )
setBackgroundBrush( QColor( 215, 215, 215 ) );
}
void QgsLayout::initializeDefaults()
{
// default to a A4 landscape page
QgsLayoutItemPage *page = new QgsLayoutItemPage( this );
page->setPageSize( QgsLayoutSize( 297, 210, QgsUnitTypes::LayoutMillimeters ) );
mPageCollection->addPage( page );
}
QgsProject *QgsLayout::project() const
{
return mProject;
@ -112,3 +120,48 @@ QgsLayoutPageCollection *QgsLayout::pageCollection()
{
return mPageCollection.get();
}
QRectF QgsLayout::layoutBounds( bool ignorePages, double margin ) const
{
//start with an empty rectangle
QRectF bounds;
//add all QgsComposerItems and QgsPaperItems which are in the composition
Q_FOREACH ( const QGraphicsItem *item, items() )
{
const QgsLayoutItem *layoutItem = dynamic_cast<const QgsLayoutItem *>( item );
if ( !layoutItem )
continue;
bool isPage = layoutItem->type() == QgsLayoutItemRegistry::LayoutPage;
if ( !isPage || !ignorePages )
{
//expand bounds with current item's bounds
if ( bounds.isValid() )
bounds = bounds.united( item->sceneBoundingRect() );
else
bounds = item->sceneBoundingRect();
}
}
if ( bounds.isValid() && margin > 0.0 )
{
//finally, expand bounds out by specified margin of page size
double maxWidth = mPageCollection->maximumPageWidth();
bounds.adjust( -maxWidth * margin, -maxWidth * margin, maxWidth * margin, maxWidth * margin );
}
return bounds;
}
void QgsLayout::addLayoutItem( QgsLayoutItem *item )
{
addItem( item );
updateBounds();
}
void QgsLayout::updateBounds()
{
setSceneRect( layoutBounds( false, 0.05 ) );
}

View File

@ -45,9 +45,19 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext
/**
* Construct a new layout linked to the specified \a project.
*
* If the layout is a "new" layout (as opposed to a layout which will
* restore a previous state from XML) then initializeDefaults() should be
* called on the new layout.
*/
QgsLayout( QgsProject *project );
/**
* Initializes an empty layout, e.g. by adding a default page to the layout. This should be called after creating
* a new layout.
*/
void initializeDefaults();
/**
* The project associated with the layout. Used to get access to layers, map themes,
* relations and various other bits. It is never null.
@ -211,6 +221,28 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext
*/
QgsLayoutPageCollection *pageCollection();
/**
* Calculates the bounds of all non-gui items in the layout. Ignores snap lines, mouse handles
* and other cosmetic items.
* \param ignorePages set to true to ignore page items
* \param margin optional marginal (in percent, e.g., 0.05 = 5% ) to add around items
* \returns layout bounds, in layout units.
*/
QRectF layoutBounds( bool ignorePages = false, double margin = 0.0 ) const;
/**
* Adds an \a item to the layout. This should be called instead of the base class addItem()
* method. Ownership of the item is transferred to the layout.
*/
void addLayoutItem( QgsLayoutItem *item SIP_TRANSFER );
public slots:
/**
* Updates the scene bounds of the layout.
*/
void updateBounds();
signals:
/**

View File

@ -17,6 +17,8 @@
#include "qgslayoutpagecollection.h"
#include "qgslayout.h"
#define SPACE_BETWEEN_PAGES 10
QgsLayoutPageCollection::QgsLayoutPageCollection( QgsLayout *layout )
: QObject( layout )
, mLayout( layout )
@ -41,6 +43,29 @@ void QgsLayoutPageCollection::setPageStyleSymbol( QgsFillSymbol *symbol )
mPageStyleSymbol.reset( static_cast<QgsFillSymbol *>( symbol->clone() ) );
}
void QgsLayoutPageCollection::reflow()
{
double currentY = 0;
QgsLayoutPoint p( 0, 0, mLayout->units() );
Q_FOREACH ( QgsLayoutItemPage *page, mPages )
{
page->attemptMove( p );
currentY += mLayout->convertToLayoutUnits( page->pageSize() ).height() + SPACE_BETWEEN_PAGES;
p.setY( currentY );
}
mLayout->updateBounds();
}
double QgsLayoutPageCollection::maximumPageWidth() const
{
double maxWidth = 0;
Q_FOREACH ( QgsLayoutItemPage *page, mPages )
{
maxWidth = qMax( maxWidth, mLayout->convertToLayoutUnits( page->pageSize() ).width() );
}
return maxWidth;
}
QgsLayout *QgsLayoutPageCollection::layout() const
{
return mLayout;
@ -65,6 +90,7 @@ void QgsLayoutPageCollection::addPage( QgsLayoutItemPage *page )
{
mPages.append( page );
mLayout->addItem( page );
reflow();
}
void QgsLayoutPageCollection::insertPage( QgsLayoutItemPage *page, int beforePage )
@ -81,6 +107,7 @@ void QgsLayoutPageCollection::insertPage( QgsLayoutItemPage *page, int beforePag
mPages.insert( beforePage, page );
}
mLayout->addItem( page );
reflow();
}
void QgsLayoutPageCollection::deletePage( int pageNumber )
@ -91,6 +118,7 @@ void QgsLayoutPageCollection::deletePage( int pageNumber )
QgsLayoutItemPage *page = mPages.takeAt( pageNumber );
mLayout->removeItem( page );
page->deleteLater();
reflow();
}
void QgsLayoutPageCollection::createDefaultPageStyleSymbol()

View File

@ -78,6 +78,9 @@ class CORE_EXPORT QgsLayoutPageCollection : public QObject
* to the collection, and the page will automatically be added to the collection's
* layout() (there is no need to manually add the page item to the layout).
* The page will be added after all pages currently contained in the collection.
*
* Calling addPage() automatically triggers a reflow() of pages.
*
* \see insertPage()
*/
void addPage( QgsLayoutItemPage *page SIP_TRANSFER );
@ -93,6 +96,8 @@ class CORE_EXPORT QgsLayoutPageCollection : public QObject
* (Page numbers in collections begin at 0 - so a \a beforePage of 0 will insert
* the page before all existing pages).
*
* Calling insertPage() automatically triggers a reflow() of pages.
*
* \see addPage()
*/
void insertPage( QgsLayoutItemPage *page SIP_TRANSFER, int beforePage );
@ -103,6 +108,8 @@ class CORE_EXPORT QgsLayoutPageCollection : public QObject
*
* Page numbers in collections begin at 0 - so a \a pageNumber of 0 will delete
* the first page in the collection.
*
* Calling deletePage() automatically triggers a reflow() of pages.
*/
void deletePage( int pageNumber );
@ -120,6 +127,18 @@ class CORE_EXPORT QgsLayoutPageCollection : public QObject
*/
const QgsFillSymbol *pageStyleSymbol() const { return mPageStyleSymbol.get(); }
/**
* Forces the page collection to reflow the arrangement of pages, e.g. to account
* for page size/orientation change.
*/
void reflow();
/**
* Returns the maximum width of pages in the collection. The returned value is
* in layout units.
*/
double maximumPageWidth() const;
private:
QgsLayout *mLayout = nullptr;

View File

@ -115,7 +115,7 @@ void QgsLayoutViewToolAddItem::layoutReleaseEvent( QgsLayoutViewMouseEvent *even
settings.setValue( QStringLiteral( "LayoutDesigner/lastItemHeight" ), item->sizeWithUnits().height() );
settings.setValue( QStringLiteral( "LayoutDesigner/lastSizeUnit" ), static_cast< int >( item->sizeWithUnits().units() ) );
layout()->addItem( item );
layout()->addLayoutItem( item );
}
void QgsLayoutViewToolAddItem::deactivate()

View File

@ -19,6 +19,8 @@
#include "qgstest.h"
#include "qgsproject.h"
#include "qgslayoutitemmap.h"
#include "qgslayoutitemshape.h"
#include "qgstestutils.h"
class TestQgsLayout: public QObject
{
@ -36,6 +38,8 @@ class TestQgsLayout: public QObject
void variablesEdited();
void scope();
void referenceMap();
void bounds();
void addItem();
private:
QString mReport;
@ -246,6 +250,117 @@ void TestQgsLayout::referenceMap()
}
void TestQgsLayout::bounds()
{
//add some items to a layout
QgsProject p;
QgsLayout l( &p );
l.initializeDefaults();
QgsLayoutItemRectangularShape *shape1 = new QgsLayoutItemRectangularShape( &l );
shape1->attemptResize( QgsLayoutSize( 90, 50 ) );
shape1->attemptMove( QgsLayoutPoint( 90, 50 ) );
shape1->setItemRotation( 45 );
l.addLayoutItem( shape1 );
QgsLayoutItemRectangularShape *shape2 = new QgsLayoutItemRectangularShape( &l );
shape2->attemptResize( QgsLayoutSize( 110, 50 ) );
shape2->attemptMove( QgsLayoutPoint( 100, 150 ) );
l.addLayoutItem( shape2 );
#if 0
QgsLayoutItemRectangularShape *shape3 = new QgsLayoutItemRectangularShape( &l );
l.addLayoutItem( shape3 );
shape3->setItemPosition( 210, 30, 50, 100, QgsComposerItem::UpperLeft, false, 2 );
QgsLayoutItemRectangularShape *shape4 = new QgsLayoutItemRectangularShape( &l );
l.addLayoutItem( shape4 );
shape4->setItemPosition( 10, 120, 50, 30, QgsComposerItem::UpperLeft, false, 2 );
shape4->setVisibility( false );
#endif
//check bounds
QRectF layoutBounds = l.layoutBounds( false );
#if 0 // correct values when 2nd page items are added back in
QGSCOMPARENEAR( layoutBounds.height(), 372.15, 0.01 );
QGSCOMPARENEAR( layoutBounds.width(), 301.00, 0.01 );
QGSCOMPARENEAR( layoutBounds.left(), -2, 0.01 );
QGSCOMPARENEAR( layoutBounds.top(), -2, 0.01 );
QRectF compositionBoundsNoPage = l.layoutBounds( true );
QGSCOMPARENEAR( compositionBoundsNoPage.height(), 320.36, 0.01 );
QGSCOMPARENEAR( compositionBoundsNoPage.width(), 250.30, 0.01 );
QGSCOMPARENEAR( compositionBoundsNoPage.left(), 9.85, 0.01 );
QGSCOMPARENEAR( compositionBoundsNoPage.top(), 49.79, 0.01 );
#endif
QGSCOMPARENEAR( layoutBounds.height(), 211.000000, 0.01 );
QGSCOMPARENEAR( layoutBounds.width(), 298.000000, 0.01 );
QGSCOMPARENEAR( layoutBounds.left(), -0.500000, 0.01 );
QGSCOMPARENEAR( layoutBounds.top(), -0.500000, 0.01 );
QRectF compositionBoundsNoPage = l.layoutBounds( true );
QGSCOMPARENEAR( compositionBoundsNoPage.height(), 175.704581, 0.01 );
QGSCOMPARENEAR( compositionBoundsNoPage.width(), 125.704581, 0.01 );
QGSCOMPARENEAR( compositionBoundsNoPage.left(), 84.795419, 0.01 );
QGSCOMPARENEAR( compositionBoundsNoPage.top(), 24.795419, 0.01 );
#if 0
QRectF page1Bounds = composition->pageItemBounds( 0, true );
QGSCOMPARENEAR( page1Bounds.height(), 150.36, 0.01 );
QGSCOMPARENEAR( page1Bounds.width(), 155.72, 0.01 );
QGSCOMPARENEAR( page1Bounds.left(), 54.43, 0.01 );
QGSCOMPARENEAR( page1Bounds.top(), 49.79, 0.01 );
QRectF page2Bounds = composition->pageItemBounds( 1, true );
QGSCOMPARENEAR( page2Bounds.height(), 100.30, 0.01 );
QGSCOMPARENEAR( page2Bounds.width(), 50.30, 0.01 );
QGSCOMPARENEAR( page2Bounds.left(), 209.85, 0.01 );
QGSCOMPARENEAR( page2Bounds.top(), 249.85, 0.01 );
QRectF page2BoundsWithHidden = composition->pageItemBounds( 1, false );
QGSCOMPARENEAR( page2BoundsWithHidden.height(), 120.30, 0.01 );
QGSCOMPARENEAR( page2BoundsWithHidden.width(), 250.30, 0.01 );
QGSCOMPARENEAR( page2BoundsWithHidden.left(), 9.85, 0.01 );
QGSCOMPARENEAR( page2BoundsWithHidden.top(), 249.85, 0.01 );
#endif
}
void TestQgsLayout::addItem()
{
QgsProject p;
QgsLayout l( &p );
l.pageCollection()->deletePage( 0 );
QgsLayoutItemRectangularShape *shape1 = new QgsLayoutItemRectangularShape( &l );
shape1->attemptResize( QgsLayoutSize( 140, 70 ) );
shape1->attemptMove( QgsLayoutPoint( 90, 50 ) );
l.addLayoutItem( shape1 );
QVERIFY( l.items().contains( shape1 ) );
// bounds should be updated to include item
QGSCOMPARENEAR( l.sceneRect().left(), 89.5, 0.001 );
QGSCOMPARENEAR( l.sceneRect().top(), 49.5, 0.001 );
QGSCOMPARENEAR( l.sceneRect().width(), 141, 0.001 );
QGSCOMPARENEAR( l.sceneRect().height(), 71, 0.001 );
QgsLayoutItemRectangularShape *shape2 = new QgsLayoutItemRectangularShape( &l );
shape2->attemptResize( QgsLayoutSize( 240, 170 ) );
shape2->attemptMove( QgsLayoutPoint( 30, 20 ) );
// don't use addLayoutItem - we want to manually trigger a bounds update
l.addItem( shape2 );
QGSCOMPARENEAR( l.sceneRect().left(), 89.5, 0.001 );
QGSCOMPARENEAR( l.sceneRect().top(), 49.5, 0.001 );
QGSCOMPARENEAR( l.sceneRect().width(), 141, 0.001 );
QGSCOMPARENEAR( l.sceneRect().height(), 71, 0.001 );
l.updateBounds();
// bounds should be updated to include item
QGSCOMPARENEAR( l.sceneRect().left(), 29.5, 0.001 );
QGSCOMPARENEAR( l.sceneRect().top(), 19.5, 0.001 );
QGSCOMPARENEAR( l.sceneRect().width(), 241, 0.001 );
QGSCOMPARENEAR( l.sceneRect().height(), 171, 0.001 );
}
QGSTEST_MAIN( TestQgsLayout )
#include "testqgslayout.moc"

View File

@ -15,7 +15,14 @@ __revision__ = '$Format:%H$'
import qgis # NOQA
import sip
from qgis.core import QgsUnitTypes, QgsLayout, QgsLayoutItemPage, QgsProject, QgsLayoutPageCollection, QgsSimpleFillSymbolLayer, QgsFillSymbol
from qgis.core import (QgsUnitTypes,
QgsLayout,
QgsLayoutItemPage,
QgsLayoutSize,
QgsProject,
QgsLayoutPageCollection,
QgsSimpleFillSymbolLayer,
QgsFillSymbol)
from qgis.PyQt.QtCore import Qt, QCoreApplication, QEvent
from qgis.testing import start_app, unittest
@ -120,6 +127,86 @@ class TestQgsLayoutPageCollection(unittest.TestCase):
self.assertTrue(sip.isdeleted(page))
self.assertTrue(sip.isdeleted(page2))
def testMaxPageWidth(self):
"""
Test calculating maximum page width
"""
p = QgsProject()
l = QgsLayout(p)
collection = l.pageCollection()
# add a page
page = QgsLayoutItemPage(l)
page.setPageSize('A4')
collection.addPage(page)
self.assertEqual(collection.maximumPageWidth(), 210.0)
# add a second page
page2 = QgsLayoutItemPage(l)
page2.setPageSize('A3')
collection.addPage(page2)
self.assertEqual(collection.maximumPageWidth(), 297.0)
# add a page with other units
page3 = QgsLayoutItemPage(l)
page3.setPageSize(QgsLayoutSize(100, 100, QgsUnitTypes.LayoutMeters))
collection.addPage(page3)
self.assertEqual(collection.maximumPageWidth(), 100000.0)
def testReflow(self):
"""
Test reflowing pages
"""
p = QgsProject()
l = QgsLayout(p)
collection = l.pageCollection()
#add a page
page = QgsLayoutItemPage(l)
page.setPageSize('A4')
collection.addPage(page)
#should be positioned at origin
self.assertEqual(page.pos().x(), 0)
self.assertEqual(page.pos().y(), 0)
#second page
page2 = QgsLayoutItemPage(l)
page2.setPageSize('A5')
collection.addPage(page2)
self.assertEqual(page.pos().x(), 0)
self.assertEqual(page.pos().y(), 0)
self.assertEqual(page2.pos().x(), 0)
self.assertEqual(page2.pos().y(), 307)
#third page, slotted in middle
page3 = QgsLayoutItemPage(l)
page3.setPageSize('A3')
collection.insertPage(page3, 1)
self.assertEqual(page.pos().x(), 0)
self.assertEqual(page.pos().y(), 0)
self.assertEqual(page2.pos().x(), 0)
self.assertEqual(page2.pos().y(), 737)
self.assertEqual(page3.pos().x(), 0)
self.assertEqual(page3.pos().y(), 307)
page.setPageSize(QgsLayoutSize(100, 120))
# no update until reflow is called
self.assertEqual(page.pos().x(), 0)
self.assertEqual(page.pos().y(), 0)
self.assertEqual(page2.pos().x(), 0)
self.assertEqual(page2.pos().y(), 737)
self.assertEqual(page3.pos().x(), 0)
self.assertEqual(page3.pos().y(), 307)
collection.reflow()
self.assertEqual(page.pos().x(), 0)
self.assertEqual(page.pos().y(), 0)
self.assertEqual(page2.pos().x(), 0)
self.assertEqual(page2.pos().y(), 560)
self.assertEqual(page3.pos().x(), 0)
self.assertEqual(page3.pos().y(), 130)
if __name__ == '__main__':
unittest.main()