/*************************************************************************** qgslayout.cpp ------------------- begin : June 2017 copyright : (C) 2017 by Nyall Dawson email : nyall dot dawson at gmail dot com ***************************************************************************/ /*************************************************************************** * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * ***************************************************************************/ #include "qgslayout.h" #include "qgslayoutitem.h" #include "qgslayoutmodel.h" #include "qgslayoutpagecollection.h" #include "qgslayoutguidecollection.h" #include "qgsreadwritecontext.h" #include "qgsproject.h" #include "qgslayoutitemundocommand.h" #include "qgslayoutitemgroup.h" #include "qgslayoutitemgroupundocommand.h" #include "qgslayoutmultiframe.h" #include "qgslayoutitemmap.h" #include "qgslayoutundostack.h" QgsLayout::QgsLayout( QgsProject *project ) : mProject( project ) , mRenderContext( new QgsLayoutRenderContext( this ) ) , mReportContext( new QgsLayoutReportContext( this ) ) , mSnapper( QgsLayoutSnapper( this ) ) , mGridSettings( this ) , mPageCollection( new QgsLayoutPageCollection( this ) ) , mUndoStack( new QgsLayoutUndoStack( this ) ) { // just to make sure - this should be the default, but maybe it'll change in some future Qt version... setBackgroundBrush( Qt::NoBrush ); mItemsModel.reset( new QgsLayoutModel( this ) ); } QgsLayout::~QgsLayout() { // no need for undo commands when we're destroying the layout mUndoStack->blockCommands( true ); deleteAndRemoveMultiFrames(); // make sure that all layout items are removed before // this class is deconstructed - to avoid segfaults // when layout items access in destructor layout that isn't valid anymore // since deletion of some item types (e.g. groups) trigger deletion // of other items, we have to do this careful, one at a time... QList itemList = items(); bool deleted = true; while ( deleted ) { deleted = false; for ( QGraphicsItem *item : qgis::as_const( itemList ) ) { if ( dynamic_cast< QgsLayoutItem * >( item ) && !dynamic_cast< QgsLayoutItemPage *>( item ) ) { delete item; deleted = true; break; } } itemList = items(); } mItemsModel.reset(); // manually delete, so we can control order of destruction } QgsLayout *QgsLayout::clone() const { QDomDocument currentDoc; QgsReadWriteContext context; QDomElement elem = writeXml( currentDoc, context ); currentDoc.appendChild( elem ); std::unique_ptr< QgsLayout > newLayout = qgis::make_unique< QgsLayout >( mProject ); bool ok = false; newLayout->loadFromTemplate( currentDoc, context, true, &ok ); if ( !ok ) { return nullptr; } return newLayout.release(); } void QgsLayout::initializeDefaults() { // default to a A4 landscape page QgsLayoutItemPage *page = new QgsLayoutItemPage( this ); page->setPageSize( QgsLayoutSize( 297, 210, QgsUnitTypes::LayoutMillimeters ) ); mPageCollection->addPage( page ); mUndoStack->stack()->clear(); } void QgsLayout::clear() { deleteAndRemoveMultiFrames(); //delete all non paper items const QList itemList = items(); for ( QGraphicsItem *item : itemList ) { QgsLayoutItem *cItem = dynamic_cast( item ); QgsLayoutItemPage *pItem = dynamic_cast( item ); if ( cItem && !pItem ) { removeLayoutItemPrivate( cItem ); } } mItemsModel->clear(); mPageCollection->clear(); mUndoStack->stack()->clear(); } QgsProject *QgsLayout::project() const { return mProject; } QgsLayoutModel *QgsLayout::itemsModel() { return mItemsModel.get(); } QList QgsLayout::selectedLayoutItems( const bool includeLockedItems ) { QList layoutItemList; const QList graphicsItemList = selectedItems(); for ( QGraphicsItem *item : graphicsItemList ) { QgsLayoutItem *layoutItem = dynamic_cast( item ); if ( layoutItem && ( includeLockedItems || !layoutItem->isLocked() ) ) { layoutItemList.push_back( layoutItem ); } } return layoutItemList; } void QgsLayout::setSelectedItem( QgsLayoutItem *item ) { whileBlocking( this )->deselectAll(); if ( item ) { item->setSelected( true ); } emit selectedItemChanged( item ); } void QgsLayout::deselectAll() { //we can't use QGraphicsScene::clearSelection, as that emits no signals //and we don't know which items are being deselected //accordingly, we can't inform the layout model of selection changes //instead, do the clear selection manually... const QList selectedItemList = selectedItems(); for ( QGraphicsItem *item : selectedItemList ) { if ( QgsLayoutItem *layoutItem = dynamic_cast( item ) ) { layoutItem->setSelected( false ); } } emit selectedItemChanged( nullptr ); } bool QgsLayout::raiseItem( QgsLayoutItem *item, bool deferUpdate ) { //model handles reordering items bool result = mItemsModel->reorderItemUp( item ); if ( result && !deferUpdate ) { //update all positions updateZValues(); update(); } return result; } bool QgsLayout::lowerItem( QgsLayoutItem *item, bool deferUpdate ) { //model handles reordering items bool result = mItemsModel->reorderItemDown( item ); if ( result && !deferUpdate ) { //update all positions updateZValues(); update(); } return result; } bool QgsLayout::moveItemToTop( QgsLayoutItem *item, bool deferUpdate ) { //model handles reordering items bool result = mItemsModel->reorderItemToTop( item ); if ( result && !deferUpdate ) { //update all positions updateZValues(); update(); } return result; } bool QgsLayout::moveItemToBottom( QgsLayoutItem *item, bool deferUpdate ) { //model handles reordering items bool result = mItemsModel->reorderItemToBottom( item ); if ( result && !deferUpdate ) { //update all positions updateZValues(); update(); } return result; } QgsLayoutItem *QgsLayout::itemByUuid( const QString &uuid, bool includeTemplateUuids ) const { QList itemList; layoutItems( itemList ); for ( QgsLayoutItem *item : qgis::as_const( itemList ) ) { if ( item->uuid() == uuid ) return item; else if ( includeTemplateUuids && item->templateUuid() == uuid ) return item; } return nullptr; } QgsLayoutItem *QgsLayout::itemById( const QString &id ) const { const QList itemList = items(); for ( QGraphicsItem *item : itemList ) { QgsLayoutItem *layoutItem = dynamic_cast( item ); if ( layoutItem && layoutItem->id() == id ) { return layoutItem; } } return nullptr; } QgsLayoutMultiFrame *QgsLayout::multiFrameByUuid( const QString &uuid ) const { for ( QgsLayoutMultiFrame *mf : mMultiFrames ) { if ( mf->uuid() == uuid ) { return mf; } } return nullptr; } QgsLayoutItem *QgsLayout::layoutItemAt( QPointF position, const bool ignoreLocked ) const { return layoutItemAt( position, nullptr, ignoreLocked ); } QgsLayoutItem *QgsLayout::layoutItemAt( QPointF position, const QgsLayoutItem *belowItem, const bool ignoreLocked ) const { //get a list of items which intersect the specified position, in descending z order const QList itemList = items( position, Qt::IntersectsItemShape, Qt::DescendingOrder ); bool foundBelowItem = false; for ( QGraphicsItem *graphicsItem : itemList ) { QgsLayoutItem *layoutItem = dynamic_cast( graphicsItem ); QgsLayoutItemPage *paperItem = dynamic_cast( layoutItem ); if ( layoutItem && !paperItem ) { // If we are not checking for a an item below a specified item, or if we've // already found that item, then we've found our target if ( ( ! belowItem || foundBelowItem ) && ( !ignoreLocked || !layoutItem->isLocked() ) ) { return layoutItem; } else { if ( layoutItem == belowItem ) { //Target item is next in list foundBelowItem = true; } } } } return nullptr; } double QgsLayout::convertToLayoutUnits( const QgsLayoutMeasurement &measurement ) const { return mRenderContext->measurementConverter().convert( measurement, mUnits ).length(); } QSizeF QgsLayout::convertToLayoutUnits( const QgsLayoutSize &size ) const { return mRenderContext->measurementConverter().convert( size, mUnits ).toQSizeF(); } QPointF QgsLayout::convertToLayoutUnits( const QgsLayoutPoint &point ) const { return mRenderContext->measurementConverter().convert( point, mUnits ).toQPointF(); } QgsLayoutMeasurement QgsLayout::convertFromLayoutUnits( const double length, const QgsUnitTypes::LayoutUnit unit ) const { return mRenderContext->measurementConverter().convert( QgsLayoutMeasurement( length, mUnits ), unit ); } QgsLayoutSize QgsLayout::convertFromLayoutUnits( const QSizeF &size, const QgsUnitTypes::LayoutUnit unit ) const { return mRenderContext->measurementConverter().convert( QgsLayoutSize( size.width(), size.height(), mUnits ), unit ); } QgsLayoutPoint QgsLayout::convertFromLayoutUnits( const QPointF &point, const QgsUnitTypes::LayoutUnit unit ) const { return mRenderContext->measurementConverter().convert( QgsLayoutPoint( point.x(), point.y(), mUnits ), unit ); } QgsLayoutRenderContext &QgsLayout::renderContext() { return *mRenderContext; } const QgsLayoutRenderContext &QgsLayout::renderContext() const { return *mRenderContext; } QgsLayoutReportContext &QgsLayout::reportContext() { return *mReportContext; } const QgsLayoutReportContext &QgsLayout::reportContext() const { return *mReportContext; } QgsLayoutGuideCollection &QgsLayout::guides() { return mPageCollection->guides(); } const QgsLayoutGuideCollection &QgsLayout::guides() const { return mPageCollection->guides(); } QgsExpressionContext QgsLayout::createExpressionContext() const { QgsExpressionContext context = QgsExpressionContext(); context.appendScope( QgsExpressionContextUtils::globalScope() ); context.appendScope( QgsExpressionContextUtils::projectScope( mProject ) ); context.appendScope( QgsExpressionContextUtils::layoutScope( this ) ); return context; } void QgsLayout::setCustomProperty( const QString &key, const QVariant &value ) { mCustomProperties.setValue( key, value ); if ( key.startsWith( QLatin1String( "variable" ) ) ) emit variablesChanged(); } QVariant QgsLayout::customProperty( const QString &key, const QVariant &defaultValue ) const { return mCustomProperties.value( key, defaultValue ); } void QgsLayout::removeCustomProperty( const QString &key ) { mCustomProperties.remove( key ); } QStringList QgsLayout::customProperties() const { return mCustomProperties.keys(); } QgsLayoutItemMap *QgsLayout::referenceMap() const { // prefer explicitly set reference map if ( QgsLayoutItemMap *map = qobject_cast< QgsLayoutItemMap * >( itemByUuid( mWorldFileMapId ) ) ) return map; // else try to find largest map QList< QgsLayoutItemMap * > maps; layoutItems( maps ); QgsLayoutItemMap *largestMap = nullptr; double largestMapArea = 0; for ( QgsLayoutItemMap *map : qgis::as_const( maps ) ) { double area = map->rect().width() * map->rect().height(); if ( area > largestMapArea ) { largestMapArea = area; largestMap = map; } } return largestMap; } void QgsLayout::setReferenceMap( QgsLayoutItemMap *map ) { mWorldFileMapId = map ? map->uuid() : QString(); mProject->setDirty( true ); } QgsLayoutPageCollection *QgsLayout::pageCollection() { return mPageCollection.get(); } const QgsLayoutPageCollection *QgsLayout::pageCollection() const { 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( item ); if ( !layoutItem ) continue; bool isPage = layoutItem->type() == QgsLayoutItemRegistry::LayoutPage; if ( !isPage || !ignorePages ) { //expand bounds with current item's bounds QRectF itemBounds; if ( isPage ) { // for pages we only consider the item's rect - not the bounding rect // as the bounding rect contains extra padding itemBounds = layoutItem->mapToScene( layoutItem->rect() ).boundingRect(); } else itemBounds = item->sceneBoundingRect(); if ( bounds.isValid() ) bounds = bounds.united( itemBounds ); else bounds = itemBounds; } } 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; } QRectF QgsLayout::pageItemBounds( int page, bool visibleOnly ) const { //start with an empty rectangle QRectF bounds; //add all QgsLayoutItems on page const QList itemList = items(); for ( QGraphicsItem *item : itemList ) { const QgsLayoutItem *layoutItem = dynamic_cast( item ); if ( layoutItem && layoutItem->type() != QgsLayoutItemRegistry::LayoutPage && layoutItem->page() == page ) { if ( visibleOnly && !layoutItem->isVisible() ) continue; //expand bounds with current item's bounds if ( bounds.isValid() ) bounds = bounds.united( item->sceneBoundingRect() ); else bounds = item->sceneBoundingRect(); } } return bounds; } void QgsLayout::addLayoutItem( QgsLayoutItem *item ) { addLayoutItemPrivate( item ); QString undoText; if ( QgsLayoutItemAbstractMetadata *metadata = QgsApplication::layoutItemRegistry()->itemMetadata( item->type() ) ) { undoText = tr( "Create %1" ).arg( metadata->visibleName() ); } else { undoText = tr( "Create Item" ); } if ( !mUndoStack->isBlocked() ) mUndoStack->push( new QgsLayoutItemAddItemCommand( item, undoText ) ); } void QgsLayout::removeLayoutItem( QgsLayoutItem *item ) { std::unique_ptr< QgsLayoutItemDeleteUndoCommand > deleteCommand; if ( !mUndoStack->isBlocked() ) { mUndoStack->beginMacro( tr( "Delete Items" ) ); deleteCommand.reset( new QgsLayoutItemDeleteUndoCommand( item, tr( "Delete Item" ) ) ); } removeLayoutItemPrivate( item ); if ( deleteCommand ) { mUndoStack->push( deleteCommand.release() ); mUndoStack->endMacro(); } } void QgsLayout::addMultiFrame( QgsLayoutMultiFrame *multiFrame ) { if ( !multiFrame ) return; if ( !mMultiFrames.contains( multiFrame ) ) mMultiFrames << multiFrame; } void QgsLayout::removeMultiFrame( QgsLayoutMultiFrame *multiFrame ) { mMultiFrames.removeAll( multiFrame ); } QList QgsLayout::multiFrames() const { return mMultiFrames; } bool QgsLayout::saveAsTemplate( const QString &path, const QgsReadWriteContext &context ) const { QFile templateFile( path ); if ( !templateFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) ) { return false; } QDomDocument saveDocument; QDomElement elem = writeXml( saveDocument, context ); saveDocument.appendChild( elem ); if ( templateFile.write( saveDocument.toByteArray() ) == -1 ) return false; return true; } QList< QgsLayoutItem * > QgsLayout::loadFromTemplate( const QDomDocument &document, const QgsReadWriteContext &context, bool clearExisting, bool *ok ) { if ( ok ) *ok = false; QList< QgsLayoutItem * > result; if ( clearExisting ) { clear(); } QDomDocument doc = document; // remove all uuid attributes since we don't want duplicates UUIDS QDomNodeList itemsNodes = doc.elementsByTagName( QStringLiteral( "LayoutItem" ) ); for ( int i = 0; i < itemsNodes.count(); ++i ) { QDomNode itemNode = itemsNodes.at( i ); if ( itemNode.isElement() ) { itemNode.toElement().removeAttribute( QStringLiteral( "uuid" ) ); } } //read general settings if ( clearExisting ) { QDomElement layoutElem = doc.documentElement(); if ( layoutElem.isNull() ) { return result; } bool loadOk = readXml( layoutElem, doc, context ); if ( !loadOk ) { return result; } layoutItems( result ); } else { result = addItemsFromXml( doc.documentElement(), doc, context ); } if ( ok ) *ok = true; return result; } QgsLayoutUndoStack *QgsLayout::undoStack() { return mUndoStack.get(); } const QgsLayoutUndoStack *QgsLayout::undoStack() const { return mUndoStack.get(); } ///@cond PRIVATE class QgsLayoutUndoCommand: public QgsAbstractLayoutUndoCommand { public: QgsLayoutUndoCommand( QgsLayout *layout, const QString &text, int id, QUndoCommand *parent SIP_TRANSFERTHIS = nullptr ) : QgsAbstractLayoutUndoCommand( text, id, parent ) , mLayout( layout ) {} protected: void saveState( QDomDocument &stateDoc ) const override { stateDoc.clear(); QDomElement documentElement = stateDoc.createElement( QStringLiteral( "UndoState" ) ); mLayout->writeXmlLayoutSettings( documentElement, stateDoc, QgsReadWriteContext() ); stateDoc.appendChild( documentElement ); } void restoreState( QDomDocument &stateDoc ) override { if ( !mLayout ) { return; } mLayout->readXmlLayoutSettings( stateDoc.documentElement(), stateDoc, QgsReadWriteContext() ); mLayout->project()->setDirty( true ); } private: QgsLayout *mLayout = nullptr; }; ///@endcond QgsAbstractLayoutUndoCommand *QgsLayout::createCommand( const QString &text, int id, QUndoCommand *parent ) { return new QgsLayoutUndoCommand( this, text, id, parent ); } QgsLayoutItemGroup *QgsLayout::groupItems( const QList &items ) { if ( items.size() < 2 ) { //not enough items for a group return nullptr; } mUndoStack->beginMacro( tr( "Group Items" ) ); std::unique_ptr< QgsLayoutItemGroup > itemGroup( new QgsLayoutItemGroup( this ) ); for ( QgsLayoutItem *item : items ) { itemGroup->addItem( item ); } QgsLayoutItemGroup *returnGroup = itemGroup.get(); addLayoutItem( itemGroup.release() ); std::unique_ptr< QgsLayoutItemGroupUndoCommand > c( new QgsLayoutItemGroupUndoCommand( QgsLayoutItemGroupUndoCommand::Grouped, returnGroup, this, tr( "Group Items" ) ) ); mUndoStack->push( c.release() ); mProject->setDirty( true ); #if 0 emit composerItemGroupAdded( itemGroup ); #endif mUndoStack->endMacro(); return returnGroup; } QList QgsLayout::ungroupItems( QgsLayoutItemGroup *group ) { QList ungroupedItems; if ( !group ) { return ungroupedItems; } mUndoStack->beginMacro( tr( "Ungroup Items" ) ); // Call this before removing group items so it can keep note // of contents std::unique_ptr< QgsLayoutItemGroupUndoCommand > c( new QgsLayoutItemGroupUndoCommand( QgsLayoutItemGroupUndoCommand::Ungrouped, group, this, tr( "Ungroup Items" ) ) ); mUndoStack->push( c.release() ); mProject->setDirty( true ); ungroupedItems = group->items(); group->removeItems(); removeLayoutItem( group ); mUndoStack->endMacro(); #if 0 //TODO removeComposerItem( group, false, false ); #endif return ungroupedItems; } void QgsLayout::refresh() { mUndoStack->blockCommands( true ); mPageCollection->beginPageSizeChange(); emit refreshed(); mPageCollection->reflow(); mPageCollection->endPageSizeChange(); mUndoStack->blockCommands( false ); update(); } void QgsLayout::writeXmlLayoutSettings( QDomElement &element, QDomDocument &document, const QgsReadWriteContext & ) const { mCustomProperties.writeXml( element, document ); element.setAttribute( QStringLiteral( "units" ), QgsUnitTypes::encodeUnit( mUnits ) ); element.setAttribute( QStringLiteral( "worldFileMap" ), mWorldFileMapId ); element.setAttribute( QStringLiteral( "printResolution" ), mRenderContext->dpi() ); } QDomElement QgsLayout::writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const { QDomElement element = document.createElement( QStringLiteral( "Layout" ) ); auto save = [&]( const QgsLayoutSerializableObject * object )->bool { return object->writeXml( element, document, context ); }; save( &mSnapper ); save( &mGridSettings ); save( mPageCollection.get() ); //save items except paper items and frame items (they are saved with the corresponding multiframe) const QList itemList = items(); for ( const QGraphicsItem *graphicsItem : itemList ) { if ( const QgsLayoutItem *item = dynamic_cast< const QgsLayoutItem *>( graphicsItem ) ) { if ( item->type() == QgsLayoutItemRegistry::LayoutPage ) continue; item->writeXml( element, document, context ); } } //save multiframes for ( QgsLayoutMultiFrame *mf : mMultiFrames ) { mf->writeXml( element, document, context ); } writeXmlLayoutSettings( element, document, context ); return element; } bool QgsLayout::readXmlLayoutSettings( const QDomElement &layoutElement, const QDomDocument &, const QgsReadWriteContext & ) { mCustomProperties.readXml( layoutElement ); setUnits( QgsUnitTypes::decodeLayoutUnit( layoutElement.attribute( QStringLiteral( "units" ) ) ) ); mWorldFileMapId = layoutElement.attribute( QStringLiteral( "worldFileMap" ) ); mRenderContext->setDpi( layoutElement.attribute( QStringLiteral( "printResolution" ), "300" ).toDouble() ); emit changed(); return true; } void QgsLayout::addLayoutItemPrivate( QgsLayoutItem *item ) { addItem( item ); updateBounds(); mItemsModel->rebuildZList(); } void QgsLayout::removeLayoutItemPrivate( QgsLayoutItem *item ) { mItemsModel->setItemRemoved( item ); // small chance that item is still in a scene - the model may have // rejected the removal for some reason. This is probably not necessary, // but can't hurt... if ( item->scene() ) removeItem( item ); #if 0 //TODO emit itemRemoved( item ); #endif item->cleanup(); item->deleteLater(); } void QgsLayout::deleteAndRemoveMultiFrames() { qDeleteAll( mMultiFrames ); mMultiFrames.clear(); } QPointF QgsLayout::minPointFromXml( const QDomElement &elem ) const { double minX = std::numeric_limits::max(); double minY = std::numeric_limits::max(); const QDomNodeList itemList = elem.elementsByTagName( QStringLiteral( "LayoutItem" ) ); bool found = false; for ( int i = 0; i < itemList.size(); ++i ) { const QDomElement currentItemElem = itemList.at( i ).toElement(); QgsLayoutPoint pos = QgsLayoutPoint::decodePoint( currentItemElem.attribute( QStringLiteral( "position" ) ) ); QPointF layoutPoint = convertToLayoutUnits( pos ); minX = std::min( minX, layoutPoint.x() ); minY = std::min( minY, layoutPoint.y() ); found = true; } return found ? QPointF( minX, minY ) : QPointF( 0, 0 ); } void QgsLayout::updateZValues( const bool addUndoCommands ) { int counter = mItemsModel->zOrderListSize(); const QList zOrderList = mItemsModel->zOrderList(); if ( addUndoCommands ) { mUndoStack->beginMacro( tr( "Change Item Stacking" ) ); } for ( QgsLayoutItem *currentItem : zOrderList ) { if ( currentItem ) { if ( addUndoCommands ) { mUndoStack->beginCommand( currentItem, QString() ); } currentItem->setZValue( counter ); if ( addUndoCommands ) { mUndoStack->endCommand(); } } --counter; } if ( addUndoCommands ) { mUndoStack->endMacro(); } } bool QgsLayout::readXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ) { if ( layoutElement.nodeName() != QStringLiteral( "Layout" ) ) { return false; } auto restore = [&]( QgsLayoutSerializableObject * object )->bool { return object->readXml( layoutElement, document, context ); }; blockSignals( true ); // defer changed signal to end readXmlLayoutSettings( layoutElement, document, context ); blockSignals( false ); restore( mPageCollection.get() ); restore( &mSnapper ); restore( &mGridSettings ); addItemsFromXml( layoutElement, document, context ); emit changed(); return true; } QList< QgsLayoutItem * > QgsLayout::addItemsFromXml( const QDomElement &parentElement, const QDomDocument &document, const QgsReadWriteContext &context, QPointF *position, bool pasteInPlace ) { QList< QgsLayoutItem * > newItems; QList< QgsLayoutMultiFrame * > newMultiFrames; //if we are adding items to a layout which already contains items, we need to make sure //these items are placed at the top of the layout and that zValues are not duplicated //so, calculate an offset which needs to be added to the zValue of created items int zOrderOffset = mItemsModel->zOrderListSize(); QPointF pasteShiftPos; int pageNumber = -1; if ( position ) { //If we are placing items relative to a certain point, then calculate how much we need //to shift the items by so that they are placed at this point //First, calculate the minimum position from the xml QPointF minItemPos = minPointFromXml( parentElement ); //next, calculate how much each item needs to be shifted from its original position //so that it's placed at the correct relative position pasteShiftPos = *position - minItemPos; if ( pasteInPlace ) { pageNumber = mPageCollection->pageNumberForPoint( *position ); } } // multiframes //TODO - fix this. pasting multiframe frame items has no effect const QDomNodeList multiFrameList = parentElement.elementsByTagName( QStringLiteral( "LayoutMultiFrame" ) ); for ( int i = 0; i < multiFrameList.size(); ++i ) { const QDomElement multiFrameElem = multiFrameList.at( i ).toElement(); const int itemType = multiFrameElem.attribute( QStringLiteral( "type" ) ).toInt(); std::unique_ptr< QgsLayoutMultiFrame > mf( QgsApplication::layoutItemRegistry()->createMultiFrame( itemType, this ) ); if ( !mf ) { // e.g. plugin based item which is no longer available continue; } mf->readXml( multiFrameElem, document, context ); #if 0 //TODO? mf->setCreateUndoCommands( true ); #endif QgsLayoutMultiFrame *m = mf.get(); this->addMultiFrame( mf.release() ); //offset z values for frames //TODO - fix this after fixing multiframe item paste /*for ( int frameIdx = 0; frameIdx < mf->frameCount(); ++frameIdx ) { QgsLayoutItemFrame * frame = mf->frame( frameIdx ); frame->setZValue( frame->zValue() + zOrderOffset ); // also need to shift frames according to position/pasteInPlacePt }*/ newMultiFrames << m; } const QDomNodeList layoutItemList = parentElement.childNodes(); for ( int i = 0; i < layoutItemList.size(); ++i ) { const QDomElement currentItemElem = layoutItemList.at( i ).toElement(); if ( currentItemElem.nodeName() != QStringLiteral( "LayoutItem" ) ) continue; const int itemType = currentItemElem.attribute( QStringLiteral( "type" ) ).toInt(); std::unique_ptr< QgsLayoutItem > item( QgsApplication::layoutItemRegistry()->createItem( itemType, this ) ); if ( !item ) { // e.g. plugin based item which is no longer available continue; } item->readXml( currentItemElem, document, context ); if ( position ) { if ( pasteInPlace ) { QgsLayoutPoint posOnPage = QgsLayoutPoint::decodePoint( currentItemElem.attribute( QStringLiteral( "positionOnPage" ) ) ); item->attemptMove( posOnPage, true, false, pageNumber ); } else { item->attemptMoveBy( pasteShiftPos.x(), pasteShiftPos.y() ); } } QgsLayoutItem *layoutItem = item.get(); addLayoutItem( item.release() ); layoutItem->setZValue( layoutItem->zValue() + zOrderOffset ); newItems << layoutItem; } // we now allow items to "post-process", e.g. if they need to setup connections // to other items in the layout, which may not have existed at the time the // item's state was restored. E.g. a scalebar may have been restored before the map // it is linked to for ( QgsLayoutItem *item : qgis::as_const( newItems ) ) { item->finalizeRestoreFromXml(); } for ( QgsLayoutMultiFrame *mf : qgis::as_const( newMultiFrames ) ) { mf->finalizeRestoreFromXml(); } //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. mItemsModel->rebuildZList(); return newItems; } void QgsLayout::updateBounds() { setSceneRect( layoutBounds( false, 0.05 ) ); }