Skip to content

Commit 447a949

Browse files
committed
Fix items moving after altering page size or inserting/deleting pages
1 parent f649f1f commit 447a949

File tree

7 files changed

+232
-1
lines changed

7 files changed

+232
-1
lines changed

python/core/layout/qgslayoutpagecollection.sip

+16
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,22 @@ Ownership is not transferred, and a copy of the symbol is created internally.
184184
Returns the symbol to use for drawing pages in the collection.
185185

186186
.. seealso:: :py:func:`setPageStyleSymbol()`
187+
%End
188+
189+
void beginPageSizeChange();
190+
%Docstring
191+
Should be called before changing any page item sizes, and followed by a call to
192+
endPageSizeChange(). If page size changes are wrapped in these calls, then items
193+
will maintain their same relative position on pages after the page sizes are updated.
194+
.. seealso:: :py:func:`endPageSizeChange()`
195+
%End
196+
197+
void endPageSizeChange();
198+
%Docstring
199+
Should be called after changing any page item sizes, and preceded by a call to
200+
beginPageSizeChange(). If page size changes are wrapped in these calls, then items
201+
will maintain their same relative position on pages after the page sizes are updated.
202+
.. seealso:: :py:func:`beginPageSizeChange()`
187203
%End
188204

189205
void reflow();

src/app/layout/qgslayoutpagepropertieswidget.cpp

+2
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,12 @@ void QgsLayoutPagePropertiesWidget::orientationChanged( int )
138138

139139
void QgsLayoutPagePropertiesWidget::updatePageSize()
140140
{
141+
mPage->layout()->pageCollection()->beginPageSizeChange();
141142
mPage->layout()->undoStack()->beginCommand( mPage, tr( "Change Page Size" ), 1 + mPage->layout()->pageCollection()->pageNumber( mPage ) );
142143
mPage->setPageSize( QgsLayoutSize( mWidthSpin->value(), mHeightSpin->value(), mSizeUnitsComboBox->unit() ) );
143144
mPage->layout()->undoStack()->endCommand();
144145
mPage->layout()->pageCollection()->reflow();
146+
mPage->layout()->pageCollection()->endPageSizeChange();
145147
}
146148

147149
void QgsLayoutPagePropertiesWidget::setToCustomSize()

src/core/composer/qgslayoutmanager.cpp

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include "qgslayout.h"
1818
#include "qgsproject.h"
1919
#include "qgslogger.h"
20+
#include "qgslayoutundostack.h"
2021

2122
QgsLayoutManager::QgsLayoutManager( QgsProject *project )
2223
: QObject( project )
@@ -193,11 +194,13 @@ bool QgsLayoutManager::readXml( const QDomElement &element, const QDomDocument &
193194
for ( int i = 0; i < layoutNodes.size(); ++i )
194195
{
195196
std::unique_ptr< QgsLayout > l = qgis::make_unique< QgsLayout >( mProject );
197+
l->undoStack()->blockCommands( true );
196198
if ( !l->readXml( layoutNodes.at( i ).toElement(), doc, context ) )
197199
{
198200
result = false;
199201
continue;
200202
}
203+
l->undoStack()->blockCommands( false );
201204
if ( addLayout( l.get() ) )
202205
{
203206
( void )l.release(); // ownership was transferred successfully

src/core/layout/qgslayout.cpp

+3
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,9 @@ QList<QgsLayoutItem *> QgsLayout::ungroupItems( QgsLayoutItemGroup *group )
701701
void QgsLayout::refresh()
702702
{
703703
emit refreshed();
704+
mPageCollection->beginPageSizeChange();
705+
mPageCollection->reflow();
706+
mPageCollection->endPageSizeChange();
704707
update();
705708
}
706709

src/core/layout/qgslayoutpagecollection.cpp

+75-1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,37 @@ void QgsLayoutPageCollection::setPageStyleSymbol( QgsFillSymbol *symbol )
5454

5555
}
5656

57+
void QgsLayoutPageCollection::beginPageSizeChange()
58+
{
59+
mPreviousItemPositions.clear();
60+
QList< QgsLayoutItem * > items;
61+
mLayout->layoutItems( items );
62+
63+
for ( QgsLayoutItem *item : qgis::as_const( items ) )
64+
{
65+
if ( item->type() == QgsLayoutItemRegistry::LayoutPage )
66+
continue;
67+
68+
mPreviousItemPositions.insert( item->uuid(), qMakePair( item->page(), item->pagePositionWithUnits() ) );
69+
}
70+
}
71+
72+
void QgsLayoutPageCollection::endPageSizeChange()
73+
{
74+
for ( auto it = mPreviousItemPositions.constBegin(); it != mPreviousItemPositions.constEnd(); ++it )
75+
{
76+
if ( QgsLayoutItem *item = mLayout->itemByUuid( it.key() ) )
77+
{
78+
if ( !mBlockUndoCommands )
79+
item->beginCommand( QString() );
80+
item->attemptMove( it.value().second, true, false, it.value().first );
81+
if ( !mBlockUndoCommands )
82+
item->endCommand();
83+
}
84+
}
85+
mPreviousItemPositions.clear();
86+
}
87+
5788
void QgsLayoutPageCollection::reflow()
5889
{
5990
double currentY = 0;
@@ -526,11 +557,15 @@ QgsLayoutItemPage *QgsLayoutPageCollection::extendByNewPage()
526557
void QgsLayoutPageCollection::insertPage( QgsLayoutItemPage *page, int beforePage )
527558
{
528559
if ( !mBlockUndoCommands )
560+
{
561+
mLayout->undoStack()->beginMacro( tr( "Add Page" ) );
529562
mLayout->undoStack()->beginCommand( this, tr( "Add Page" ) );
563+
}
530564

531565
if ( beforePage < 0 )
532566
beforePage = 0;
533567

568+
beginPageSizeChange();
534569
if ( beforePage >= mPages.count() )
535570
{
536571
mPages.append( page );
@@ -541,8 +576,22 @@ void QgsLayoutPageCollection::insertPage( QgsLayoutItemPage *page, int beforePag
541576
}
542577
mLayout->addItem( page );
543578
reflow();
579+
580+
// bump up stored page numbers to account
581+
for ( auto it = mPreviousItemPositions.begin(); it != mPreviousItemPositions.end(); ++it )
582+
{
583+
if ( it.value().first < beforePage )
584+
continue;
585+
586+
it.value().first = it.value().first + 1;
587+
}
588+
589+
endPageSizeChange();
544590
if ( ! mBlockUndoCommands )
591+
{
545592
mLayout->undoStack()->endCommand();
593+
mLayout->undoStack()->endMacro();
594+
}
546595
}
547596

548597
void QgsLayoutPageCollection::deletePage( int pageNumber )
@@ -556,10 +605,22 @@ void QgsLayoutPageCollection::deletePage( int pageNumber )
556605
mLayout->undoStack()->beginCommand( this, tr( "Remove Page" ) );
557606
}
558607
emit pageAboutToBeRemoved( pageNumber );
608+
beginPageSizeChange();
559609
QgsLayoutItemPage *page = mPages.takeAt( pageNumber );
560610
mLayout->removeItem( page );
561611
page->deleteLater();
562612
reflow();
613+
614+
// bump stored page numbers to account
615+
for ( auto it = mPreviousItemPositions.begin(); it != mPreviousItemPositions.end(); ++it )
616+
{
617+
if ( it.value().first <= pageNumber )
618+
continue;
619+
620+
it.value().first = it.value().first - 1;
621+
}
622+
623+
endPageSizeChange();
563624
if ( ! mBlockUndoCommands )
564625
{
565626
mLayout->undoStack()->endCommand();
@@ -577,10 +638,23 @@ void QgsLayoutPageCollection::deletePage( QgsLayoutItemPage *page )
577638
mLayout->undoStack()->beginMacro( tr( "Remove Page" ) );
578639
mLayout->undoStack()->beginCommand( this, tr( "Remove Page" ) );
579640
}
580-
emit pageAboutToBeRemoved( mPages.indexOf( page ) );
641+
int pageIndex = mPages.indexOf( page );
642+
emit pageAboutToBeRemoved( pageIndex );
643+
beginPageSizeChange();
581644
mPages.removeAll( page );
582645
page->deleteLater();
583646
reflow();
647+
648+
// bump stored page numbers to account
649+
for ( auto it = mPreviousItemPositions.begin(); it != mPreviousItemPositions.end(); ++it )
650+
{
651+
if ( it.value().first <= pageIndex )
652+
continue;
653+
654+
it.value().first = it.value().first - 1;
655+
}
656+
657+
endPageSizeChange();
584658
if ( !mBlockUndoCommands )
585659
{
586660
mLayout->undoStack()->endCommand();

src/core/layout/qgslayoutpagecollection.h

+19
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include "qgslayoutitempage.h"
2525
#include "qgslayoutitem.h"
2626
#include "qgslayoutserializableobject.h"
27+
#include "qgslayoutpoint.h"
2728
#include <QObject>
2829
#include <memory>
2930

@@ -220,6 +221,22 @@ class CORE_EXPORT QgsLayoutPageCollection : public QObject, public QgsLayoutSeri
220221
*/
221222
const QgsFillSymbol *pageStyleSymbol() const { return mPageStyleSymbol.get(); }
222223

224+
/**
225+
* Should be called before changing any page item sizes, and followed by a call to
226+
* endPageSizeChange(). If page size changes are wrapped in these calls, then items
227+
* will maintain their same relative position on pages after the page sizes are updated.
228+
* \see endPageSizeChange()
229+
*/
230+
void beginPageSizeChange();
231+
232+
/**
233+
* Should be called after changing any page item sizes, and preceded by a call to
234+
* beginPageSizeChange(). If page size changes are wrapped in these calls, then items
235+
* will maintain their same relative position on pages after the page sizes are updated.
236+
* \see beginPageSizeChange()
237+
*/
238+
void endPageSizeChange();
239+
223240
/**
224241
* Forces the page collection to reflow the arrangement of pages, e.g. to account
225242
* for page size/orientation change.
@@ -391,6 +408,8 @@ class CORE_EXPORT QgsLayoutPageCollection : public QObject, public QgsLayoutSeri
391408

392409
bool mBlockUndoCommands = false;
393410

411+
QMap< QString, QPair< int, QgsLayoutPoint > > mPreviousItemPositions;
412+
394413
void createDefaultPageStyleSymbol();
395414

396415
friend class QgsLayoutPageCollectionUndoCommand;

tests/src/python/test_qgslayoutpagecollection.py

+114
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,120 @@ def testReflow(self):
365365
self.assertEqual(page3.pos().x(), 0)
366366
self.assertEqual(page3.pos().y(), 130)
367367

368+
def testInsertPageWithItems(self):
369+
p = QgsProject()
370+
l = QgsLayout(p)
371+
collection = l.pageCollection()
372+
373+
# add a page
374+
page = QgsLayoutItemPage(l)
375+
page.setPageSize('A4')
376+
collection.addPage(page)
377+
page2 = QgsLayoutItemPage(l)
378+
page2.setPageSize('A5')
379+
collection.addPage(page2)
380+
381+
# item on pages
382+
shape1 = QgsLayoutItemShape(l)
383+
shape1.attemptResize(QgsLayoutSize(90, 50))
384+
shape1.attemptMove(QgsLayoutPoint(90, 50), page=0)
385+
l.addLayoutItem(shape1)
386+
387+
shape2 = QgsLayoutItemShape(l)
388+
shape2.attemptResize(QgsLayoutSize(110, 50))
389+
shape2.attemptMove(QgsLayoutPoint(100, 150), page=1)
390+
l.addLayoutItem(shape2)
391+
392+
self.assertEqual(shape1.page(), 0)
393+
self.assertEqual(shape2.page(), 1)
394+
395+
# third page, slotted in middle
396+
page3 = QgsLayoutItemPage(l)
397+
page3.setPageSize('A3')
398+
collection.insertPage(page3, 0)
399+
400+
# check item position
401+
self.assertEqual(shape1.page(), 1)
402+
self.assertEqual(shape1.pagePositionWithUnits(), QgsLayoutPoint(90, 50))
403+
self.assertEqual(shape2.page(), 2)
404+
self.assertEqual(shape2.pagePositionWithUnits(), QgsLayoutPoint(100, 150))
405+
406+
def testDeletePageWithItems(self):
407+
p = QgsProject()
408+
l = QgsLayout(p)
409+
collection = l.pageCollection()
410+
411+
# add a page
412+
page = QgsLayoutItemPage(l)
413+
page.setPageSize('A4')
414+
collection.addPage(page)
415+
page2 = QgsLayoutItemPage(l)
416+
page2.setPageSize('A4')
417+
collection.addPage(page2)
418+
page3 = QgsLayoutItemPage(l)
419+
page3.setPageSize('A4')
420+
collection.addPage(page3)
421+
422+
# item on pages
423+
shape1 = QgsLayoutItemShape(l)
424+
shape1.attemptResize(QgsLayoutSize(90, 50))
425+
shape1.attemptMove(QgsLayoutPoint(90, 50), page=0)
426+
l.addLayoutItem(shape1)
427+
428+
shape2 = QgsLayoutItemShape(l)
429+
shape2.attemptResize(QgsLayoutSize(110, 50))
430+
shape2.attemptMove(QgsLayoutPoint(100, 150), page=2)
431+
l.addLayoutItem(shape2)
432+
433+
self.assertEqual(shape1.page(), 0)
434+
self.assertEqual(shape2.page(), 2)
435+
436+
collection.deletePage(1)
437+
438+
# check item position
439+
self.assertEqual(shape1.page(), 0)
440+
self.assertEqual(shape1.pagePositionWithUnits(), QgsLayoutPoint(90, 50))
441+
self.assertEqual(shape2.page(), 1)
442+
self.assertEqual(shape2.pagePositionWithUnits(), QgsLayoutPoint(100, 150))
443+
444+
def testDeletePageWithItems2(self):
445+
p = QgsProject()
446+
l = QgsLayout(p)
447+
collection = l.pageCollection()
448+
449+
# add a page
450+
page = QgsLayoutItemPage(l)
451+
page.setPageSize('A4')
452+
collection.addPage(page)
453+
page2 = QgsLayoutItemPage(l)
454+
page2.setPageSize('A4')
455+
collection.addPage(page2)
456+
page3 = QgsLayoutItemPage(l)
457+
page3.setPageSize('A4')
458+
collection.addPage(page3)
459+
460+
# item on pages
461+
shape1 = QgsLayoutItemShape(l)
462+
shape1.attemptResize(QgsLayoutSize(90, 50))
463+
shape1.attemptMove(QgsLayoutPoint(90, 50), page=0)
464+
l.addLayoutItem(shape1)
465+
466+
shape2 = QgsLayoutItemShape(l)
467+
shape2.attemptResize(QgsLayoutSize(110, 50))
468+
shape2.attemptMove(QgsLayoutPoint(100, 150), page=2)
469+
l.addLayoutItem(shape2)
470+
471+
self.assertEqual(shape1.page(), 0)
472+
self.assertEqual(shape2.page(), 2)
473+
474+
collection.deletePage(page2)
475+
476+
# check item position
477+
self.assertEqual(shape1.page(), 0)
478+
self.assertEqual(shape1.pagePositionWithUnits(), QgsLayoutPoint(90, 50))
479+
self.assertEqual(shape2.page(), 1)
480+
self.assertEqual(shape2.pagePositionWithUnits(), QgsLayoutPoint(100, 150))
481+
368482
def testDataDefinedSize(self):
369483
p = QgsProject()
370484
l = QgsLayout(p)

0 commit comments

Comments
 (0)