mirror of
https://github.com/qgis/QGIS.git
synced 2025-03-29 00:05:09 -04:00
1034 lines
29 KiB
C++
1034 lines
29 KiB
C++
/***************************************************************************
|
|
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<QGraphicsItem *> 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<QGraphicsItem *> itemList = items();
|
|
for ( QGraphicsItem *item : itemList )
|
|
{
|
|
QgsLayoutItem *cItem = dynamic_cast<QgsLayoutItem *>( item );
|
|
QgsLayoutItemPage *pItem = dynamic_cast<QgsLayoutItemPage *>( 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<QgsLayoutItem *> QgsLayout::selectedLayoutItems( const bool includeLockedItems )
|
|
{
|
|
QList<QgsLayoutItem *> layoutItemList;
|
|
|
|
const QList<QGraphicsItem *> graphicsItemList = selectedItems();
|
|
for ( QGraphicsItem *item : graphicsItemList )
|
|
{
|
|
QgsLayoutItem *layoutItem = dynamic_cast<QgsLayoutItem *>( 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<QGraphicsItem *> selectedItemList = selectedItems();
|
|
for ( QGraphicsItem *item : selectedItemList )
|
|
{
|
|
if ( QgsLayoutItem *layoutItem = dynamic_cast<QgsLayoutItem *>( 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<QgsLayoutItem *> 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<QGraphicsItem *> itemList = items();
|
|
for ( QGraphicsItem *item : itemList )
|
|
{
|
|
QgsLayoutItem *layoutItem = dynamic_cast<QgsLayoutItem *>( 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<QGraphicsItem *> itemList = items( position, Qt::IntersectsItemShape, Qt::DescendingOrder );
|
|
|
|
bool foundBelowItem = false;
|
|
for ( QGraphicsItem *graphicsItem : itemList )
|
|
{
|
|
QgsLayoutItem *layoutItem = dynamic_cast<QgsLayoutItem *>( graphicsItem );
|
|
QgsLayoutItemPage *paperItem = dynamic_cast<QgsLayoutItemPage *>( 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<const QgsLayoutItem *>( 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<QGraphicsItem *> itemList = items();
|
|
for ( QGraphicsItem *item : itemList )
|
|
{
|
|
const QgsLayoutItem *layoutItem = dynamic_cast<const QgsLayoutItem *>( 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<QgsLayoutMultiFrame *> 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<QgsLayoutItem *> &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<QgsLayoutItem *> QgsLayout::ungroupItems( QgsLayoutItemGroup *group )
|
|
{
|
|
QList<QgsLayoutItem *> 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<QGraphicsItem *> 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<double>::max();
|
|
double minY = std::numeric_limits<double>::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<QgsLayoutItem *> 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 ) );
|
|
}
|