Skip to content

Commit

Permalink
Fix handling of multiframe objects in layout templates
Browse files Browse the repository at this point in the history
  • Loading branch information
nyalldawson committed Jan 22, 2018
1 parent d9b6c8b commit 35a7701
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 13 deletions.
5 changes: 4 additions & 1 deletion python/core/layout/qgslayout.sip.in
Expand Up @@ -197,11 +197,14 @@ item found.
.. seealso:: :py:func:`itemByUuid`
%End

QgsLayoutMultiFrame *multiFrameByUuid( const QString &uuid ) const;
QgsLayoutMultiFrame *multiFrameByUuid( const QString &uuid, bool includeTemplateUuids = false ) const;
%Docstring
Returns the layout multiframe with matching ``uuid`` unique identifier, or a None
if a matching multiframe could not be found.

If ``includeTemplateUuids`` is true, then the multiframe's :py:func:`QgsLayoutMultiFrame.templateUuid()`
will also be tested when trying to match the uuid.

.. seealso:: :py:func:`itemByUuid`
%End

Expand Down
12 changes: 12 additions & 0 deletions python/core/layout/qgslayoutmultiframe.sip.in
Expand Up @@ -95,6 +95,18 @@ upon creation.
.. note::

There is no corresponding setter for the uuid - it's created automatically.

.. seealso:: :py:func:`templateUuid`
%End

QString templateUuid() const;
%Docstring
Returns the multiframe's original identification string. This may differ from the multiframes's uuid()
for multiframes which have been added to an existing layout from a template. In this case
templateUuid() returns the original UUID at the time the template was created,
while uuid() returns the current instance of the multiframes's unique identifier.

.. seealso:: :py:func:`uuid`
%End

virtual QSizeF totalSize() const = 0;
Expand Down
21 changes: 18 additions & 3 deletions src/core/layout/qgslayout.cpp
Expand Up @@ -259,14 +259,14 @@ QgsLayoutItem *QgsLayout::itemById( const QString &id ) const
return nullptr;
}

QgsLayoutMultiFrame *QgsLayout::multiFrameByUuid( const QString &uuid ) const
QgsLayoutMultiFrame *QgsLayout::multiFrameByUuid( const QString &uuid, bool includeTemplateUuids ) const
{
for ( QgsLayoutMultiFrame *mf : mMultiFrames )
{
if ( mf->uuid() == uuid )
{
return mf;
}
else if ( includeTemplateUuids && mf->templateUuid() == uuid )
return mf;
}

return nullptr;
Expand Down Expand Up @@ -608,6 +608,21 @@ QList< QgsLayoutItem * > QgsLayout::loadFromTemplate( const QDomDocument &docume
itemNode.toElement().removeAttribute( QStringLiteral( "uuid" ) );
}
}
QDomNodeList multiFrameNodes = doc.elementsByTagName( QStringLiteral( "LayoutMultiFrame" ) );
for ( int i = 0; i < multiFrameNodes.count(); ++i )
{
QDomNode multiFrameNode = multiFrameNodes.at( i );
if ( multiFrameNode.isElement() )
{
multiFrameNode.toElement().removeAttribute( QStringLiteral( "uuid" ) );
QDomNodeList frameNodes = multiFrameNode.toElement().elementsByTagName( QStringLiteral( "childFrame" ) );
QDomNode itemNode = frameNodes.at( i );
if ( itemNode.isElement() )
{
itemNode.toElement().removeAttribute( QStringLiteral( "uuid" ) );
}
}
}

//read general settings
if ( clearExisting )
Expand Down
6 changes: 5 additions & 1 deletion src/core/layout/qgslayout.h
Expand Up @@ -266,9 +266,13 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext
/**
* Returns the layout multiframe with matching \a uuid unique identifier, or a nullptr
* if a matching multiframe could not be found.
*
* If \a includeTemplateUuids is true, then the multiframe's QgsLayoutMultiFrame::templateUuid()
* will also be tested when trying to match the uuid.
*
* \see itemByUuid()
*/
QgsLayoutMultiFrame *multiFrameByUuid( const QString &uuid ) const;
QgsLayoutMultiFrame *multiFrameByUuid( const QString &uuid, bool includeTemplateUuids = false ) const;

/**
* Returns the topmost layout item at a specified \a position. Ignores paper items.
Expand Down
5 changes: 5 additions & 0 deletions src/core/layout/qgslayoutframe.cpp
Expand Up @@ -187,6 +187,7 @@ void QgsLayoutFrame::drawBackground( QgsRenderContext &context )
bool QgsLayoutFrame::writePropertiesToElement( QDomElement &parentElement, QDomDocument &, const QgsReadWriteContext & ) const
{
parentElement.setAttribute( QStringLiteral( "multiFrame" ), mMultiFrameUuid );
parentElement.setAttribute( QStringLiteral( "multiFrameTemplateUuid" ), mMultiFrameUuid );
parentElement.setAttribute( QStringLiteral( "sectionX" ), QString::number( mSection.x() ) );
parentElement.setAttribute( QStringLiteral( "sectionY" ), QString::number( mSection.y() ) );
parentElement.setAttribute( QStringLiteral( "sectionWidth" ), QString::number( mSection.width() ) );
Expand All @@ -207,6 +208,10 @@ bool QgsLayoutFrame::readPropertiesFromElement( const QDomElement &itemElem, con
mHideBackgroundIfEmpty = itemElem.attribute( QStringLiteral( "hideBackgroundIfEmpty" ), QStringLiteral( "0" ) ).toInt();

mMultiFrameUuid = itemElem.attribute( QStringLiteral( "multiFrame" ) );
if ( mMultiFrameUuid.isEmpty( ) )
{
mMultiFrameUuid = itemElem.attribute( QStringLiteral( "multiFrameTemplateUuid" ) );
}
mMultiFrame = mLayout->multiFrameByUuid( mMultiFrameUuid );
return true;
}
9 changes: 8 additions & 1 deletion src/core/layout/qgslayoutmultiframe.cpp
Expand Up @@ -301,8 +301,9 @@ void QgsLayoutMultiFrame::cancelCommand()

void QgsLayoutMultiFrame::finalizeRestoreFromXml()
{
for ( const QString &uuid : qgis::as_const( mFrameUuids ) )
for ( int i = 0; i < mFrameUuids.count(); ++i )
{
const QString uuid = mFrameUuids.at( i ).isEmpty() ? mFrameTemplateUuids.at( i ) : mFrameUuids.at( i );
QgsLayoutItem *item = mLayout->itemByUuid( uuid, true );
if ( QgsLayoutFrame *frame = qobject_cast< QgsLayoutFrame * >( item ) )
{
Expand Down Expand Up @@ -458,6 +459,7 @@ bool QgsLayoutMultiFrame::writeXml( QDomElement &parentElement, QDomDocument &do
QDomElement element = doc.createElement( QStringLiteral( "LayoutMultiFrame" ) );
element.setAttribute( QStringLiteral( "resizeMode" ), mResizeMode );
element.setAttribute( QStringLiteral( "uuid" ), mUuid );
element.setAttribute( QStringLiteral( "templateUuid" ), mUuid );
element.setAttribute( QStringLiteral( "type" ), type() );

for ( QgsLayoutFrame *frame : mFrameItems )
Expand All @@ -467,6 +469,7 @@ bool QgsLayoutMultiFrame::writeXml( QDomElement &parentElement, QDomDocument &do

QDomElement childItem = doc.createElement( QStringLiteral( "childFrame" ) );
childItem.setAttribute( QStringLiteral( "uuid" ), frame->uuid() );
childItem.setAttribute( QStringLiteral( "templateUuid" ), frame->uuid() );

if ( includeFrames )
{
Expand Down Expand Up @@ -495,10 +498,12 @@ bool QgsLayoutMultiFrame::readXml( const QDomElement &element, const QDomDocumen
readObjectPropertiesFromElement( element, doc, context );

mUuid = element.attribute( QStringLiteral( "uuid" ), QUuid::createUuid().toString() );
mTemplateUuid = element.attribute( QStringLiteral( "templateUuid" ), QUuid::createUuid().toString() );
mResizeMode = static_cast< ResizeMode >( element.attribute( QStringLiteral( "resizeMode" ), QStringLiteral( "0" ) ).toInt() );

deleteFrames();
mFrameUuids.clear();
mFrameTemplateUuids.clear();
QDomNodeList elementNodes = element.elementsByTagName( QStringLiteral( "childFrame" ) );
for ( int i = 0; i < elementNodes.count(); ++i )
{
Expand All @@ -510,6 +515,8 @@ bool QgsLayoutMultiFrame::readXml( const QDomElement &element, const QDomDocumen

QString uuid = frameElement.attribute( QStringLiteral( "uuid" ) );
mFrameUuids << uuid;
QString templateUuid = frameElement.attribute( QStringLiteral( "templateUuid" ) );
mFrameTemplateUuids << templateUuid;

if ( includeFrames )
{
Expand Down
14 changes: 13 additions & 1 deletion src/core/layout/qgslayoutmultiframe.h
Expand Up @@ -128,9 +128,19 @@ class CORE_EXPORT QgsLayoutMultiFrame: public QgsLayoutObject, public QgsLayoutU
* Returns the multiframe identification string. This is a unique random string set for the multiframe
* upon creation.
* \note There is no corresponding setter for the uuid - it's created automatically.
* \see templateUuid()
*/
QString uuid() const { return mUuid; }

/**
* Returns the multiframe's original identification string. This may differ from the multiframes's uuid()
* for multiframes which have been added to an existing layout from a template. In this case
* templateUuid() returns the original UUID at the time the template was created,
* while uuid() returns the current instance of the multiframes's unique identifier.
* \see uuid()
*/
QString templateUuid() const { return mTemplateUuid; }

/**
* Returns the total size of the multiframe's content, in layout units.
*/
Expand Down Expand Up @@ -431,9 +441,11 @@ class CORE_EXPORT QgsLayoutMultiFrame: public QgsLayoutObject, public QgsLayoutU
bool mBlockUndoCommands = false;

QList< QString > mFrameUuids;
QList< QString > mFrameTemplateUuids;

//! Unique id
//! Unique id
QString mUuid;
QString mTemplateUuid;
friend class QgsLayoutFrame;
};

Expand Down
121 changes: 115 additions & 6 deletions tests/src/python/test_qgslayout.py
Expand Up @@ -27,9 +27,11 @@
QgsPrintLayout,
QgsLayoutItemGroup,
QgsLayoutItem,
QgsLayoutItemHtml,
QgsProperty,
QgsLayoutPageCollection,
QgsLayoutMeasurement,
QgsLayoutFrame,
QgsFillSymbol,
QgsReadWriteContext,
QgsLayoutItemMap,
Expand Down Expand Up @@ -240,8 +242,27 @@ def testSaveLoadTemplate(self):
item2.attemptResize(QgsLayoutSize(2.8, 2.2, QgsUnitTypes.LayoutCentimeters))
l.addItem(item2)

uuids = {item1.uuid(), item2.uuid()}
original_uuids = {item1.uuid(), item2.uuid()}
# multiframe
multiframe1 = QgsLayoutItemHtml(l)
multiframe1.setHtml('mf1')
l.addMultiFrame(multiframe1)
frame1 = QgsLayoutFrame(l, multiframe1)
frame1.setId('frame1')
frame1.attemptMove(QgsLayoutPoint(4, 8, QgsUnitTypes.LayoutMillimeters))
frame1.attemptResize(QgsLayoutSize(18, 12, QgsUnitTypes.LayoutMillimeters))
multiframe1.addFrame(frame1)

multiframe2 = QgsLayoutItemHtml(l)
multiframe2.setHtml('mf2')
l.addMultiFrame(multiframe2)
frame2 = QgsLayoutFrame(l, multiframe2)
frame2.setId('frame2')
frame2.attemptMove(QgsLayoutPoint(1.4, 1.8, QgsUnitTypes.LayoutCentimeters))
frame2.attemptResize(QgsLayoutSize(2.8, 2.2, QgsUnitTypes.LayoutCentimeters))
multiframe2.addFrame(frame2)

uuids = {item1.uuid(), item2.uuid(), frame1.uuid(), frame2.uuid(), multiframe1.uuid(), multiframe2.uuid()}
original_uuids = {item1.uuid(), item2.uuid(), frame1.uuid(), frame2.uuid()}

self.assertTrue(l.saveAsTemplate(tmpfile, QgsReadWriteContext()))

Expand All @@ -254,52 +275,140 @@ def testSaveLoadTemplate(self):
# adding to existing items
new_items, ok = l2.loadFromTemplate(doc, QgsReadWriteContext(), False)
self.assertTrue(ok)
self.assertEqual(len(new_items), 2)
self.assertEqual(len(new_items), 4)
items = l2.items()
multiframes = l2.multiFrames()
self.assertEqual(len(multiframes), 2)
self.assertTrue([i for i in items if i.id() == 'xxyyxx'])
self.assertTrue([i for i in items if i.id() == 'zzyyzz'])
self.assertTrue([i for i in items if i.id() == 'frame1'])
self.assertTrue([i for i in items if i.id() == 'frame2'])
self.assertTrue([i for i in multiframes if i.html() == 'mf1'])
self.assertTrue([i for i in multiframes if i.html() == 'mf2'])
self.assertTrue(new_items[0] in l2.items())
self.assertTrue(new_items[1] in l2.items())
self.assertTrue(new_items[2] in l2.items())
self.assertTrue(new_items[3] in l2.items())

# double check that new items have a unique uid
self.assertNotIn(new_items[0].uuid(), uuids)
self.assertIn(new_items[0].templateUuid(), original_uuids)
uuids.add(new_items[0].uuid())
self.assertNotIn(new_items[1].uuid(), uuids)
self.assertIn(new_items[1].templateUuid(), original_uuids)
uuids.add(new_items[1].uuid())
self.assertNotIn(new_items[2].uuid(), uuids)
self.assertIn(new_items[2].templateUuid(), original_uuids)
uuids.add(new_items[2].uuid())
self.assertNotIn(new_items[3].uuid(), uuids)
self.assertIn(new_items[3].templateUuid(), original_uuids)
uuids.add(new_items[3].uuid())

self.assertNotIn(multiframes[0].uuid(), [multiframe1.uuid(), multiframe2.uuid()])
self.assertIn(multiframes[0].templateUuid(), [multiframe1.uuid(), multiframe2.uuid()])
self.assertNotIn(multiframes[1].uuid(), [multiframe1.uuid(), multiframe2.uuid()])
self.assertIn(multiframes[1].templateUuid(), [multiframe1.uuid(), multiframe2.uuid()])
new_multiframe1 = [i for i in multiframes if i.html() == 'mf1'][0]
self.assertEqual(new_multiframe1.layout(), l2)
new_multiframe2 = [i for i in multiframes if i.html() == 'mf2'][0]
self.assertEqual(new_multiframe2.layout(), l2)
new_frame1 = sip.cast([i for i in items if i.id() == 'frame1'][0], QgsLayoutFrame)
new_frame2 = sip.cast([i for i in items if i.id() == 'frame2'][0], QgsLayoutFrame)
self.assertEqual(new_frame1.multiFrame(), new_multiframe1)
self.assertEqual(new_multiframe1.frames()[0].uuid(), new_frame1.uuid())
self.assertEqual(new_frame2.multiFrame(), new_multiframe2)
self.assertEqual(new_multiframe2.frames()[0].uuid(), new_frame2.uuid())

# adding to existing items
new_items2, ok = l2.loadFromTemplate(doc, QgsReadWriteContext(), False)
self.assertTrue(ok)
self.assertEqual(len(new_items2), 2)
self.assertEqual(len(new_items2), 4)
items = l2.items()
self.assertEqual(len(items), 4)
self.assertEqual(len(items), 8)
multiframes2 = l2.multiFrames()
self.assertEqual(len(multiframes2), 4)
multiframes2 = [m for m in l2.multiFrames() if not m.uuid() in [new_multiframe1.uuid(), new_multiframe2.uuid()]]
self.assertEqual(len(multiframes2), 2)
self.assertTrue([i for i in items if i.id() == 'xxyyxx'])
self.assertTrue([i for i in items if i.id() == 'zzyyzz'])
self.assertTrue([i for i in items if i.id() == 'frame1'])
self.assertTrue([i for i in items if i.id() == 'frame2'])
self.assertTrue([i for i in multiframes2 if i.html() == 'mf1'])
self.assertTrue([i for i in multiframes2 if i.html() == 'mf2'])
self.assertTrue(new_items[0] in l2.items())
self.assertTrue(new_items[1] in l2.items())
self.assertTrue(new_items[2] in l2.items())
self.assertTrue(new_items[3] in l2.items())
self.assertTrue(new_items2[0] in l2.items())
self.assertTrue(new_items2[1] in l2.items())
self.assertTrue(new_items2[2] in l2.items())
self.assertTrue(new_items2[3] in l2.items())
self.assertNotIn(new_items2[0].uuid(), uuids)
self.assertIn(new_items2[0].templateUuid(), original_uuids)
uuids.add(new_items[0].uuid())
self.assertNotIn(new_items2[1].uuid(), uuids)
self.assertIn(new_items2[1].templateUuid(), original_uuids)
uuids.add(new_items[1].uuid())
self.assertNotIn(new_items2[2].uuid(), uuids)
self.assertIn(new_items2[2].templateUuid(), original_uuids)
uuids.add(new_items[2].uuid())
self.assertNotIn(new_items2[3].uuid(), uuids)
self.assertIn(new_items2[3].templateUuid(), original_uuids)
uuids.add(new_items[3].uuid())

self.assertNotIn(multiframes2[0].uuid(), [multiframe1.uuid(), multiframe2.uuid(), new_multiframe1.uuid(), new_multiframe2.uuid()])
self.assertIn(multiframes2[0].templateUuid(), [multiframe1.uuid(), multiframe2.uuid()])
self.assertNotIn(multiframes2[1].uuid(), [multiframe1.uuid(), multiframe2.uuid(), new_multiframe1.uuid(), new_multiframe2.uuid()])
self.assertIn(multiframes2[1].templateUuid(), [multiframe1.uuid(), multiframe2.uuid()])

new_multiframe1b = [i for i in multiframes2 if i.html() == 'mf1'][0]
self.assertEqual(new_multiframe1b.layout(), l2)
new_multiframe2b = [i for i in multiframes2 if i.html() == 'mf2'][0]
self.assertEqual(new_multiframe2b.layout(), l2)
new_frame1b = sip.cast([i for i in items if i.id() == 'frame1' and i.uuid() != new_frame1.uuid()][0], QgsLayoutFrame)
new_frame2b = sip.cast([i for i in items if i.id() == 'frame2' and i.uuid() != new_frame2.uuid()][0], QgsLayoutFrame)
self.assertEqual(new_frame1b.multiFrame(), new_multiframe1b)
self.assertEqual(new_multiframe1b.frames()[0].uuid(), new_frame1b.uuid())
self.assertEqual(new_frame2b.multiFrame(), new_multiframe2b)
self.assertEqual(new_multiframe2b.frames()[0].uuid(), new_frame2b.uuid())

# clearing existing items
new_items3, ok = l2.loadFromTemplate(doc, QgsReadWriteContext(), True)
new_multiframes = l2.multiFrames()
self.assertTrue(ok)
self.assertEqual(len(new_items3), 3) # includes page
self.assertEqual(len(new_items3), 5) # includes page
self.assertEqual(len(new_multiframes), 2)
items = l2.items()
self.assertTrue([i for i in items if isinstance(i, QgsLayoutItem) and i.id() == 'xxyyxx'])
self.assertTrue([i for i in items if isinstance(i, QgsLayoutItem) and i.id() == 'zzyyzz'])
self.assertTrue([i for i in items if isinstance(i, QgsLayoutItem) and i.id() == 'frame1'])
self.assertTrue([i for i in items if isinstance(i, QgsLayoutItem) and i.id() == 'frame2'])
self.assertTrue(new_items3[0] in l2.items())
self.assertTrue(new_items3[1] in l2.items())
self.assertTrue(new_items3[2] in l2.items())
self.assertTrue(new_items3[3] in l2.items())
self.assertIn(new_items3[0].templateUuid(), original_uuids)
self.assertIn(new_items3[1].templateUuid(), original_uuids)
self.assertIn(new_items3[2].templateUuid(), original_uuids)
self.assertIn(new_items3[3].templateUuid(), original_uuids)
new_multiframe1 = [i for i in new_multiframes if i.html() == 'mf1'][0]
self.assertEqual(new_multiframe1.templateUuid(), multiframe1.uuid())
new_multiframe2 = [i for i in new_multiframes if i.html() == 'mf2'][0]
self.assertEqual(new_multiframe2.templateUuid(), multiframe2.uuid())

self.assertEqual(l2.itemByUuid(new_items3[0].templateUuid(), True), new_items3[0])
self.assertEqual(l2.itemByUuid(new_items3[1].templateUuid(), True), new_items3[1])
self.assertEqual(l2.itemByUuid(new_items3[2].templateUuid(), True), new_items3[2])
self.assertEqual(l2.itemByUuid(new_items3[3].templateUuid(), True), new_items3[3])
self.assertEqual(l2.multiFrameByUuid(new_multiframe1.templateUuid(), True), new_multiframe1)
self.assertEqual(l2.multiFrameByUuid(new_multiframe2.templateUuid(), True), new_multiframe2)

new_frame1 = sip.cast([i for i in items if isinstance(i, QgsLayoutItem) and i.id() == 'frame1'][0], QgsLayoutFrame)
new_frame2 = sip.cast([i for i in items if isinstance(i, QgsLayoutItem) and i.id() == 'frame2'][0], QgsLayoutFrame)
self.assertEqual(new_frame1.multiFrame(), new_multiframe1)
self.assertEqual(new_multiframe1.frames()[0].uuid(), new_frame1.uuid())
self.assertEqual(new_frame2.multiFrame(), new_multiframe2)
self.assertEqual(new_multiframe2.frames()[0].uuid(), new_frame2.uuid())

def testSelectedItems(self):
p = QgsProject()
Expand Down

0 comments on commit 35a7701

Please sign in to comment.