Skip to content

Commit 4ea5c80

Browse files
committed
[composer] Avoid crash when using redo on multiframe (fix #11351)
Since multiframe changes can remove and create new frame items, it's not safe to directly manipulate frame items in QgsComposerItemCommand. Now, commands which apply to a frame always fetch a reference to the correct frame item directly from the parent multiframe. Also added unit tests for this crash.
1 parent 1ff2ad3 commit 4ea5c80

File tree

6 files changed

+235
-18
lines changed

6 files changed

+235
-18
lines changed

python/core/composer/qgscomposeritemcommand.sip

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ class QgsComposerItemCommand: QUndoCommand
2525
/**Returns true if previous state and after state are valid and different*/
2626
bool containsChange() const;
2727

28-
const QgsComposerItem* item() const;
28+
/**Returns the target item the command applies to.
29+
* @returns target composer item
30+
*/
31+
QgsComposerItem* item() const;
2932

3033
protected:
3134
void saveState( QDomDocument& stateDoc ) const;

src/core/composer/qgscomposerhtml.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,10 @@ bool QgsComposerHtml::writeXML( QDomElement& elem, QDomDocument & doc, bool igno
481481

482482
bool QgsComposerHtml::readXML( const QDomElement& itemElem, const QDomDocument& doc, bool ignoreFrames )
483483
{
484-
deleteFrames();
484+
if ( !ignoreFrames )
485+
{
486+
deleteFrames();
487+
}
485488

486489
//first create the frames
487490
if ( !_readXML( itemElem, doc, ignoreFrames ) )

src/core/composer/qgscomposeritemcommand.cpp

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,26 @@
1717

1818
#include "qgscomposeritemcommand.h"
1919
#include "qgscomposeritem.h"
20+
#include "qgscomposerframe.h"
21+
#include "qgscomposermultiframe.h"
2022
#include "qgsproject.h"
23+
#include "qgslogger.h"
2124

22-
QgsComposerItemCommand::QgsComposerItemCommand( QgsComposerItem* item, const QString& text, QUndoCommand* parent ):
23-
QUndoCommand( text, parent ), mItem( item ), mFirstRun( true )
25+
QgsComposerItemCommand::QgsComposerItemCommand( QgsComposerItem* item, const QString& text, QUndoCommand* parent )
26+
: QUndoCommand( text, parent )
27+
, mItem( item )
28+
, mMultiFrame( 0 )
29+
, mFrameNumber( 0 )
30+
, mFirstRun( true )
2431
{
32+
//is item a frame?
33+
QgsComposerFrame* frame = dynamic_cast<QgsComposerFrame*>( mItem );
34+
if ( frame )
35+
{
36+
//store parent multiframe and frame index
37+
mMultiFrame = frame->multiFrame();
38+
mFrameNumber = mMultiFrame->frameIndex( frame );
39+
}
2540
}
2641

2742
QgsComposerItemCommand::~QgsComposerItemCommand()
@@ -48,6 +63,27 @@ bool QgsComposerItemCommand::containsChange() const
4863
return !( mPreviousState.isNull() || mAfterState.isNull() || mPreviousState.toString() == mAfterState.toString() );
4964
}
5065

66+
QgsComposerItem* QgsComposerItemCommand::item() const
67+
{
68+
QgsComposerItem* item = 0;
69+
if ( mMultiFrame )
70+
{
71+
//item is a frame, so it needs to be handled differently
72+
//in this case the target item is the matching frame number, as subsequent
73+
//changes to the multiframe may have deleted mItem
74+
if ( mMultiFrame->frameCount() > mFrameNumber )
75+
{
76+
item = mMultiFrame->frame( mFrameNumber );
77+
}
78+
}
79+
else if ( mItem )
80+
{
81+
item = mItem;
82+
}
83+
84+
return item;
85+
}
86+
5187
void QgsComposerItemCommand::savePreviousState()
5288
{
5389
saveState( mPreviousState );
@@ -60,26 +96,38 @@ void QgsComposerItemCommand::saveAfterState()
6096

6197
void QgsComposerItemCommand::saveState( QDomDocument& stateDoc ) const
6298
{
63-
if ( mItem )
99+
const QgsComposerItem* source = item();
100+
if ( !source )
64101
{
65-
stateDoc.clear();
66-
QDomElement documentElement = stateDoc.createElement( "ComposerItemState" );
67-
mItem->writeXML( documentElement, stateDoc );
68-
stateDoc.appendChild( documentElement );
102+
return;
69103
}
104+
105+
stateDoc.clear();
106+
QDomElement documentElement = stateDoc.createElement( "ComposerItemState" );
107+
source->writeXML( documentElement, stateDoc );
108+
stateDoc.appendChild( documentElement );
70109
}
71110

72111
void QgsComposerItemCommand::restoreState( QDomDocument& stateDoc ) const
73112
{
74-
if ( mItem )
113+
QgsComposerItem* destItem = item();
114+
if ( !destItem )
75115
{
76-
mItem->readXML( stateDoc.documentElement().firstChild().toElement(), stateDoc );
77-
mItem->repaint();
78-
QgsProject::instance()->dirty( true );
116+
return;
79117
}
118+
119+
destItem->readXML( stateDoc.documentElement().firstChild().toElement(), stateDoc );
120+
destItem->repaint();
121+
QgsProject::instance()->dirty( true );
80122
}
81123

82-
QgsComposerMergeCommand::QgsComposerMergeCommand( Context c, QgsComposerItem* item, const QString& text ): QgsComposerItemCommand( item, text ), mContext( c )
124+
//
125+
//QgsComposerMergeCommand
126+
//
127+
128+
QgsComposerMergeCommand::QgsComposerMergeCommand( Context c, QgsComposerItem* item, const QString& text )
129+
: QgsComposerItemCommand( item, text )
130+
, mContext( c )
83131
{
84132
}
85133

@@ -89,11 +137,18 @@ QgsComposerMergeCommand::~QgsComposerMergeCommand()
89137

90138
bool QgsComposerMergeCommand::mergeWith( const QUndoCommand * command )
91139
{
140+
QgsComposerItem* thisItem = item();
141+
if ( !thisItem )
142+
{
143+
return false;
144+
}
145+
92146
const QgsComposerItemCommand* c = dynamic_cast<const QgsComposerItemCommand*>( command );
93-
if ( !c || mItem != c->item() )
147+
if ( !c || thisItem != c->item() )
94148
{
95149
return false;
96150
}
151+
97152
mAfterState = c->afterState();
98153
return true;
99154
}

src/core/composer/qgscomposeritemcommand.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include <QDomDocument>
2323

2424
class QgsComposerItem;
25+
class QgsComposerMultiFrame;
2526

2627
/**Undo command to undo/redo all composer item related changes*/
2728
class CORE_EXPORT QgsComposerItemCommand: public QUndoCommand
@@ -46,7 +47,10 @@ class CORE_EXPORT QgsComposerItemCommand: public QUndoCommand
4647
/**Returns true if previous state and after state are valid and different*/
4748
bool containsChange() const;
4849

49-
const QgsComposerItem* item() const { return mItem; }
50+
/**Returns the target item the command applies to.
51+
* @returns target composer item
52+
*/
53+
QgsComposerItem *item() const;
5054

5155
protected:
5256
/**Target item of the command*/
@@ -56,6 +60,11 @@ class CORE_EXPORT QgsComposerItemCommand: public QUndoCommand
5660
/**XML containing the state after executing the command*/
5761
QDomDocument mAfterState;
5862

63+
/**Parameters for frame items*/
64+
/**Parent multiframe*/
65+
QgsComposerMultiFrame* mMultiFrame;
66+
int mFrameNumber;
67+
5968
/**Flag to prevent the first redo() if the command is pushed to the undo stack*/
6069
bool mFirstRun;
6170

src/core/composer/qgscomposermultiframe.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,8 @@ void QgsComposerMultiFrame::removeFrame( int i, const bool removeEmptyPages )
307307
{
308308
mIsRecalculatingSize = true;
309309
int pageNumber = frameItem->page();
310-
mComposition->removeComposerItem( frameItem );
310+
//remove item, but don't create undo command
311+
mComposition->removeComposerItem( frameItem, false );
311312
//if frame was the only item on the page, remove the page
312313
if ( removeEmptyPages && mComposition->pageIsEmpty( pageNumber ) )
313314
{
@@ -385,8 +386,10 @@ bool QgsComposerMultiFrame::_readXML( const QDomElement& itemElem, const QDomDoc
385386
QDomElement frameElem = frameList.at( i ).toElement();
386387
QgsComposerFrame* newFrame = new QgsComposerFrame( mComposition, this, 0, 0, 0, 0 );
387388
newFrame->readXML( frameElem, doc );
388-
addFrame( newFrame );
389+
addFrame( newFrame, false );
389390
}
391+
392+
//TODO - think there should be a recalculateFrameSizes() call here
390393
}
391394
return true;
392395
}

tests/src/core/testqgscomposermultiframe.cpp

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ class TestQgsComposerMultiFrame: public QObject
3434
void addFrame(); //test creating new frame inherits all properties of existing frame
3535
void frameIsEmpty(); //test if frame is empty works
3636
void addRemovePage(); //test if page is added and removed for RepeatUntilFinished mode
37+
void undoRedo(); //test that combinations of frame/multiframe undo/redo don't crash
38+
void undoRedoRemovedFrame(); //test that undo doesn't crash with removed frames
3739

3840
private:
3941
QgsComposition* mComposition;
@@ -203,6 +205,148 @@ void TestQgsComposerMultiFrame::addRemovePage()
203205
delete htmlItem;
204206
}
205207

208+
void TestQgsComposerMultiFrame::undoRedo()
209+
{
210+
QgsComposerHtml* htmlItem = new QgsComposerHtml( mComposition, false );
211+
QgsComposerFrame* frame1 = new QgsComposerFrame( mComposition, htmlItem, 0, 0, 100, 200 );
212+
htmlItem->addFrame( frame1 );
213+
htmlItem->setContentMode( QgsComposerHtml::ManualHtml );
214+
htmlItem->setResizeMode( QgsComposerMultiFrame::RepeatUntilFinished );
215+
216+
//short content, so should fit in one frame
217+
htmlItem->setHtml( QString( "<p>Test content</p>" ) );
218+
htmlItem->loadHtml();
219+
220+
//do some combinations of undo/redo commands for both the frame and multiframe
221+
//to try to trigger a crash
222+
frame1->beginCommand( "move" );
223+
frame1->setSceneRect( QRectF( 10, 10, 20, 20 ) );
224+
frame1->endCommand();
225+
frame1->beginCommand( "outline", QgsComposerMergeCommand::ItemOutlineWidth );
226+
frame1->setFrameOutlineWidth( 4.0 );
227+
frame1->endCommand();
228+
frame1->beginCommand( "outline", QgsComposerMergeCommand::ItemOutlineWidth );
229+
frame1->setFrameOutlineWidth( 7.0 );
230+
frame1->endCommand();
231+
232+
//multiframe commands
233+
mComposition->beginMultiFrameCommand( htmlItem, "maxbreak" );
234+
htmlItem->setMaxBreakDistance( 100 );
235+
mComposition->endMultiFrameCommand();
236+
237+
//another frame command
238+
frame1->beginCommand( "bgcolor", QgsComposerMergeCommand::ItemTransparency );
239+
frame1->setBackgroundColor( QColor( 255, 255, 0 ) );
240+
frame1->endCommand();
241+
frame1->beginCommand( "bgcolor", QgsComposerMergeCommand::ItemTransparency );
242+
frame1->setBackgroundColor( QColor( 255, 0, 0 ) );
243+
frame1->endCommand();
244+
245+
//undo changes
246+
247+
//frame bg
248+
mComposition->undoStack()->undo();
249+
//multiframe max break
250+
mComposition->undoStack()->undo();
251+
//frame outline width
252+
mComposition->undoStack()->undo();
253+
//frame move
254+
mComposition->undoStack()->undo();
255+
256+
//check result
257+
QCOMPARE( htmlItem->maxBreakDistance(), 10.0 );
258+
QCOMPARE( htmlItem->frame( 0 )->frameOutlineWidth(), 0.3 );
259+
QCOMPARE( htmlItem->frame( 0 )->pos(), QPointF( 0, 0 ) );
260+
QCOMPARE( htmlItem->frame( 0 )->backgroundColor(), QColor( 255, 255, 255 ) );
261+
262+
//now redo
263+
264+
//frame move
265+
mComposition->undoStack()->redo();
266+
//frame outline width
267+
mComposition->undoStack()->redo();
268+
//multiframe max break
269+
mComposition->undoStack()->redo();
270+
//frame bg color
271+
mComposition->undoStack()->redo();
272+
273+
//check result
274+
QCOMPARE( htmlItem->maxBreakDistance(), 100.0 );
275+
QCOMPARE( htmlItem->frame( 0 )->frameOutlineWidth(), 7.0 );
276+
QCOMPARE( htmlItem->frame( 0 )->pos(), QPointF( 10, 10 ) );
277+
QCOMPARE( htmlItem->frame( 0 )->backgroundColor(), QColor( 255, 0, 0 ) );
278+
279+
mComposition->removeMultiFrame( htmlItem );
280+
delete htmlItem;
281+
}
282+
283+
284+
void TestQgsComposerMultiFrame::undoRedoRemovedFrame()
285+
{
286+
QgsComposerHtml* htmlItem = new QgsComposerHtml( mComposition, false );
287+
QgsComposerFrame* frame1 = new QgsComposerFrame( mComposition, htmlItem, 0, 0, 100, 200 );
288+
htmlItem->addFrame( frame1 );
289+
htmlItem->setContentMode( QgsComposerHtml::ManualHtml );
290+
htmlItem->setResizeMode( QgsComposerMultiFrame::RepeatUntilFinished );
291+
292+
//long content, so should require multiple frames
293+
htmlItem->setHtml( QString( "<p style=\"height: 2000px\">Test content</p>" ) );
294+
htmlItem->loadHtml();
295+
296+
QVERIFY( htmlItem->frameCount() > 1 );
297+
298+
//do a command on the first frame
299+
htmlItem->frame( 0 )->beginCommand( "outline", QgsComposerMergeCommand::ItemOutlineWidth );
300+
htmlItem->frame( 0 )->setFrameOutlineWidth( 4.0 );
301+
htmlItem->frame( 0 )->endCommand();
302+
//do a command on the second frame
303+
htmlItem->frame( 1 )->beginCommand( "outline", QgsComposerMergeCommand::ItemOutlineWidth );
304+
htmlItem->frame( 1 )->setFrameOutlineWidth( 8.0 );
305+
htmlItem->frame( 1 )->endCommand();
306+
307+
//do a multiframe command which removes extra frames
308+
mComposition->beginMultiFrameCommand( htmlItem, "source" );
309+
htmlItem->setHtml( QString( "<p style=\"height: 20px\">Test content</p>" ) );
310+
mComposition->endMultiFrameCommand();
311+
312+
//wipes the second frame
313+
htmlItem->loadHtml();
314+
315+
QCOMPARE( htmlItem->frameCount(), 1 );
316+
317+
//undo changes
318+
319+
//multiframe command
320+
mComposition->undoStack()->undo();
321+
//frame 2 command
322+
mComposition->undoStack()->undo();
323+
//frame 1 command
324+
mComposition->undoStack()->undo();
325+
326+
//check result
327+
QVERIFY( htmlItem->frameCount() > 1 );
328+
QCOMPARE( htmlItem->frame( 0 )->frameOutlineWidth(), 0.3 );
329+
QCOMPARE( htmlItem->frame( 1 )->frameOutlineWidth(), 0.3 );
330+
331+
//now redo
332+
333+
//frame 1 command
334+
mComposition->undoStack()->redo();
335+
//frame 2 command
336+
mComposition->undoStack()->redo();
337+
338+
//check result
339+
QVERIFY( htmlItem->frameCount() > 1 );
340+
QCOMPARE( htmlItem->frame( 0 )->frameOutlineWidth(), 4.0 );
341+
QCOMPARE( htmlItem->frame( 1 )->frameOutlineWidth(), 8.0 );
342+
343+
//multiframe command
344+
mComposition->undoStack()->redo();
345+
QCOMPARE( htmlItem->frameCount(), 1 );
346+
347+
mComposition->removeMultiFrame( htmlItem );
348+
delete htmlItem;
349+
}
206350

207351
QTEST_MAIN( TestQgsComposerMultiFrame )
208352
#include "moc_testqgscomposermultiframe.cxx"

0 commit comments

Comments
 (0)