[layouts] Fix multiframe items (tables, html) cannot be pasted

Fixes #10456, #17882
This commit is contained in:
Nyall Dawson 2018-01-22 17:14:35 +10:00
parent cbe6416278
commit 7a2ab1cc7c
11 changed files with 186 additions and 84 deletions

View File

@ -180,8 +180,33 @@ which deferred z-order updates.
Returns the layout item with matching ``uuid`` unique identifier, or a None
if a matching item could not be found.
If ``includeTemplateUuids`` is true, then item's :py:func:`QgsLayoutItem.templateUuid()`
will also be tested when trying to match the uuid.
If ``includeTemplateUuids`` is true, then item's template UUID
will also be tested when trying to match the uuid. This may differ from the item's UUID
for items which have been added to an existing layout from a template. In this case
the template UUID returns the original item UUID at the time the template was created,
vs the item's uuid() which returns the current instance of the item's unique identifier.
Note that template UUIDs are only available while a layout is being restored from XML.
.. seealso:: :py:func:`itemByTemplateUuid`
.. seealso:: :py:func:`multiFrameByUuid`
.. seealso:: :py:func:`itemById`
%End
QgsLayoutItem *itemByTemplateUuid( const QString &uuid ) const;
%Docstring
Returns the layout item with matching template ``uuid`` unique identifier, or a None
if a matching item could not be found. Unlike itemByUuid(), this method ONLY checks
template UUIDs for a match.
Template UUIDs are valid only for items which have been added to an existing layout from a template. In this case
the template UUID is the original item UUID at the time the template was created,
vs the item's uuid() which returns the current instance of the item's unique identifier.
Note that template UUIDs are only available while a layout is being restored from XML.
.. seealso:: :py:func:`itemByUuid`
.. seealso:: :py:func:`multiFrameByUuid`
@ -203,7 +228,11 @@ Returns the layout multiframe with matching ``uuid`` unique identifier, or a Non
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.
will also be tested when trying to match the uuid. Template UUIDs are valid only for items
which have been added to an existing layout from a template. In this case
the template UUID is the original item UUID at the time the template was created,
vs the item's uuid() which returns the current instance of the item's unique identifier.
Note that template UUIDs are only available while a layout is being restored from XML.
.. seealso:: :py:func:`itemByUuid`
%End

View File

@ -217,18 +217,6 @@ upon creation.
.. seealso:: :py:func:`id`
.. seealso:: :py:func:`setId`
.. seealso:: :py:func:`templateUuid`
%End
QString templateUuid() const;
%Docstring
Returns the item's original identification string. This may differ from the item's uuid()
for items which have been added to an existing layout from a template. In this case
templateUuid() returns the original item UUID at the time the template was created,
while uuid() returns the current instance of the item's unique identifier.
.. seealso:: :py:func:`uuid`
%End
QString id() const;

View File

@ -95,18 +95,6 @@ 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;

View File

@ -238,7 +238,20 @@ QgsLayoutItem *QgsLayout::itemByUuid( const QString &uuid, bool includeTemplateU
{
if ( item->uuid() == uuid )
return item;
else if ( includeTemplateUuids && item->templateUuid() == uuid )
else if ( includeTemplateUuids && item->mTemplateUuid == uuid )
return item;
}
return nullptr;
}
QgsLayoutItem *QgsLayout::itemByTemplateUuid( const QString &uuid ) const
{
QList<QgsLayoutItem *> itemList;
layoutItems( itemList );
for ( QgsLayoutItem *item : qgis::as_const( itemList ) )
{
if ( item->mTemplateUuid == uuid )
return item;
}
@ -265,7 +278,7 @@ QgsLayoutMultiFrame *QgsLayout::multiFrameByUuid( const QString &uuid, bool incl
{
if ( mf->uuid() == uuid )
return mf;
else if ( includeTemplateUuids && mf->templateUuid() == uuid )
else if ( includeTemplateUuids && mf->mTemplateUuid == uuid )
return mf;
}
@ -1040,6 +1053,15 @@ QList< QgsLayoutItem * > QgsLayout::addItemsFromXml( const QDomElement &parentEl
mf->finalizeRestoreFromXml();
}
for ( QgsLayoutItem *item : qgis::as_const( newItems ) )
{
item->mTemplateUuid.clear();
}
for ( QgsLayoutMultiFrame *mf : qgis::as_const( newMultiFrames ) )
{
mf->mTemplateUuid.clear();
}
//Since this function adds items in an order which isn't the z-order, and each item is added to end of
//z order list in turn, it will now be inconsistent with the actual order of items in the scene.
//Make sure z order list matches the actual order of items in the scene.

View File

@ -247,14 +247,36 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext
* Returns the layout item with matching \a uuid unique identifier, or a nullptr
* if a matching item could not be found.
*
* If \a includeTemplateUuids is true, then item's QgsLayoutItem::templateUuid()
* will also be tested when trying to match the uuid.
* If \a includeTemplateUuids is true, then item's template UUID
* will also be tested when trying to match the uuid. This may differ from the item's UUID
* for items which have been added to an existing layout from a template. In this case
* the template UUID returns the original item UUID at the time the template was created,
* vs the item's uuid() which returns the current instance of the item's unique identifier.
* Note that template UUIDs are only available while a layout is being restored from XML.
*
* \see itemByTemplateUuid()
* \see multiFrameByUuid()
* \see itemById()
*/
QgsLayoutItem *itemByUuid( const QString &uuid, bool includeTemplateUuids = false ) const;
/**
* Returns the layout item with matching template \a uuid unique identifier, or a nullptr
* if a matching item could not be found. Unlike itemByUuid(), this method ONLY checks
* template UUIDs for a match.
*
* Template UUIDs are valid only for items which have been added to an existing layout from a template. In this case
* the template UUID is the original item UUID at the time the template was created,
* vs the item's uuid() which returns the current instance of the item's unique identifier.
*
* Note that template UUIDs are only available while a layout is being restored from XML.
*
* \see itemByUuid()
* \see multiFrameByUuid()
* \see itemById()
*/
QgsLayoutItem *itemByTemplateUuid( const QString &uuid ) const;
/**
* Returns a layout item given its \a id.
* Since item IDs are not necessarely unique, this function returns the first matching
@ -268,7 +290,11 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext
* 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.
* will also be tested when trying to match the uuid. Template UUIDs are valid only for items
* which have been added to an existing layout from a template. In this case
* the template UUID is the original item UUID at the time the template was created,
* vs the item's uuid() which returns the current instance of the item's unique identifier.
* Note that template UUIDs are only available while a layout is being restored from XML.
*
* \see itemByUuid()
*/

View File

@ -243,19 +243,9 @@ class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectIt
* \note There is no corresponding setter for the uuid - it's created automatically.
* \see id()
* \see setId()
* \see templateUuid()
*/
virtual QString uuid() const { return mUuid; }
/**
* Returns the item's original identification string. This may differ from the item's uuid()
* for items which have been added to an existing layout from a template. In this case
* templateUuid() returns the original item UUID at the time the template was created,
* while uuid() returns the current instance of the item's unique identifier.
* \see uuid()
*/
QString templateUuid() const { return mTemplateUuid; }
/**
* Returns the item's ID name. This is not necessarily unique, and duplicate ID names may exist
* for a layout.
@ -1083,6 +1073,7 @@ class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectIt
friend class TestQgsLayoutItem;
friend class TestQgsLayoutView;
friend class QgsLayout;
friend class QgsLayoutItemGroup;
friend class QgsCompositionConverter;
};

View File

@ -303,9 +303,24 @@ void QgsLayoutMultiFrame::finalizeRestoreFromXml()
{
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 ) )
QgsLayoutFrame *frame = nullptr;
const QString uuid = mFrameUuids.at( i );
if ( !uuid.isEmpty() )
{
QgsLayoutItem *item = mLayout->itemByUuid( uuid, true );
frame = qobject_cast< QgsLayoutFrame * >( item );
}
if ( !frame )
{
const QString templateUuid = mFrameTemplateUuids.at( i );
if ( !templateUuid.isEmpty() )
{
QgsLayoutItem *item = mLayout->itemByTemplateUuid( templateUuid );
frame = qobject_cast< QgsLayoutFrame * >( item );
}
}
if ( frame )
{
addFrame( frame );
}

View File

@ -128,19 +128,9 @@ 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.
*/
@ -447,6 +437,7 @@ class CORE_EXPORT QgsLayoutMultiFrame: public QgsLayoutObject, public QgsLayoutU
QString mUuid;
QString mTemplateUuid;
friend class QgsLayoutFrame;
friend class QgsLayout;
};

View File

@ -17,6 +17,8 @@
#include "qgslayoutview.h"
#include "qgslayout.h"
#include "qgslayoutframe.h"
#include "qgslayoutmultiframe.h"
#include "qgslayoutviewtool.h"
#include "qgslayoutviewmouseevent.h"
#include "qgslayoutviewtooltemporarykeypan.h"
@ -149,7 +151,6 @@ void QgsLayoutView::setTool( QgsLayoutViewTool *tool )
tool->activate();
mTool = tool;
connect( mTool, &QgsLayoutViewTool::itemFocused, this, &QgsLayoutView::itemFocused );
emit toolSet( mTool );
}
@ -320,6 +321,9 @@ void QgsLayoutView::copyItems( const QList<QgsLayoutItem *> &items, QgsLayoutVie
QDomElement documentElement = doc.createElement( QStringLiteral( "LayoutItemClipboard" ) );
if ( operation == ClipboardCut )
currentLayout()->undoStack()->beginMacro( tr( "Cut Items" ) );
QSet< QgsLayoutMultiFrame * > copiedMultiFrames;
for ( QgsLayoutItem *item : items )
{
// copy every child from a group
@ -331,6 +335,15 @@ void QgsLayoutView::copyItems( const QList<QgsLayoutItem *> &items, QgsLayoutVie
groupedItem->writeXml( documentElement, doc, context );
}
}
else if ( QgsLayoutFrame *frame = qobject_cast<QgsLayoutFrame *>( item ) )
{
// copy multiframe too
if ( !copiedMultiFrames.contains( frame->multiFrame() ) )
{
frame->multiFrame()->writeXml( documentElement, doc, context );
copiedMultiFrames.insert( frame->multiFrame() );
}
}
item->writeXml( documentElement, doc, context );
if ( operation == ClipboardCut )
currentLayout()->removeLayoutItem( item );
@ -352,6 +365,24 @@ void QgsLayoutView::copyItems( const QList<QgsLayoutItem *> &items, QgsLayoutVie
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" ) );
for ( int j = 0; j < frameNodes.count(); ++j )
{
QDomNode itemNode = frameNodes.at( j );
if ( itemNode.isElement() )
{
itemNode.toElement().removeAttribute( QStringLiteral( "uuid" ) );
}
}
}
}
QMimeData *mimeData = new QMimeData;
mimeData->setData( QStringLiteral( "text/xml" ), doc.toByteArray() );

View File

@ -292,22 +292,16 @@ class TestQgsLayout(unittest.TestCase):
# 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]
@ -344,22 +338,16 @@ class TestQgsLayout(unittest.TestCase):
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)
@ -387,21 +375,8 @@ class TestQgsLayout(unittest.TestCase):
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)

View File

@ -19,6 +19,9 @@ from qgis.core import (QgsProject,
QgsUnitTypes,
QgsLayoutItemPicture,
QgsLayoutItemLabel,
QgsLayoutItemHtml,
QgsLayoutItemRegistry,
QgsLayoutFrame,
QgsLayoutPoint,
QgsLayoutSize,
QgsLayoutAligner)
@ -686,6 +689,30 @@ class TestQgsLayoutView(unittest.TestCase):
l.addLayoutItem(item2)
item2.setSelected(True)
# multiframes
multiframe1 = QgsLayoutItemHtml(l)
multiframe1.setHtml('mf1')
l.addMultiFrame(multiframe1)
frame1 = QgsLayoutFrame(l, multiframe1)
frame1.setId('frame1a')
multiframe1.addFrame(frame1)
frame1b = QgsLayoutFrame(l, multiframe1)
frame1b.setId('frame1b')
multiframe1.addFrame(frame1b) # not selected
frame1c = QgsLayoutFrame(l, multiframe1)
frame1c.setId('frame1b')
multiframe1.addFrame(frame1c) # not selected
multiframe2 = QgsLayoutItemHtml(l)
multiframe2.setHtml('mf2')
l.addMultiFrame(multiframe2)
frame2 = QgsLayoutFrame(l, multiframe2)
frame2.setId('frame2')
multiframe2.addFrame(frame2)
frame1.setSelected(True)
frame2.setSelected(True)
view = QgsLayoutView()
view.setCurrentLayout(l)
self.assertFalse(view.hasItemsInClipboard())
@ -694,11 +721,30 @@ class TestQgsLayoutView(unittest.TestCase):
self.assertTrue(view.hasItemsInClipboard())
pasted = view.pasteItems(QgsLayoutView.PasteModeCursor)
self.assertEqual(len(pasted), 2)
self.assertEqual(len(pasted), 4)
new_multiframes = [m for m in l.multiFrames() if m not in [multiframe1, multiframe2]]
self.assertEqual(len(new_multiframes), 2)
self.assertIn(pasted[0], l.items())
self.assertIn(pasted[1], l.items())
self.assertIn(sip.cast(pasted[0], QgsLayoutItemLabel).text(), ('label 1', 'label 2'))
self.assertIn(sip.cast(pasted[1], QgsLayoutItemLabel).text(), ('label 1', 'label 2'))
labels = [p for p in pasted if p.type() == QgsLayoutItemRegistry.LayoutLabel]
self.assertIn(sip.cast(labels[0], QgsLayoutItemLabel).text(), ('label 1', 'label 2'))
self.assertIn(sip.cast(labels[1], QgsLayoutItemLabel).text(), ('label 1', 'label 2'))
frames = [p for p in pasted if p.type() == QgsLayoutItemRegistry.LayoutFrame]
pasted_frame1 = sip.cast(frames[0], QgsLayoutFrame)
pasted_frame2 = sip.cast(frames[1], QgsLayoutFrame)
self.assertIn(pasted_frame1.multiFrame(), new_multiframes)
self.assertIn(new_multiframes[0].frames()[0].uuid(), (pasted_frame1.uuid(), pasted_frame2.uuid()))
self.assertIn(pasted_frame2.multiFrame(), new_multiframes)
self.assertIn(new_multiframes[1].frames()[0].uuid(), (pasted_frame1.uuid(), pasted_frame2.uuid()))
self.assertEqual(frame1.multiFrame(), multiframe1)
self.assertCountEqual(multiframe1.frames(), [frame1, frame1b, frame1c])
self.assertEqual(frame1b.multiFrame(), multiframe1)
self.assertEqual(frame1c.multiFrame(), multiframe1)
self.assertEqual(frame2.multiFrame(), multiframe2)
self.assertCountEqual(multiframe2.frames(), [frame2])
# copy specific item
view.copyItems([item2], QgsLayoutView.ClipboardCopy)