Start of multiframe porting

This commit is contained in:
Nyall Dawson 2017-10-27 13:08:20 +10:00
parent 6278245949
commit 714920ff25
15 changed files with 2064 additions and 0 deletions

View File

@ -406,6 +406,7 @@
%Include layout/qgslayoutcontext.sip
%Include layout/qgslayouteffect.sip
%Include layout/qgslayoutguidecollection.sip
%Include layout/qgslayoutframe.sip
%Include layout/qgslayoutitem.sip
%Include layout/qgslayoutitemgroup.sip
%Include layout/qgslayoutitemlabel.sip
@ -422,6 +423,7 @@
%Include layout/qgslayoutitemregistry.sip
%Include layout/qgslayoutitemshape.sip
%Include layout/qgslayoutmodel.sip
%Include layout/qgslayoutmultiframe.sip
%Include layout/qgslayoutpagecollection.sip
%Include layout/qgslayoutobject.sip
%Include layout/qgslayoutundostack.sip

View File

@ -379,6 +379,28 @@ class QgsLayout : QGraphicsScene, QgsExpressionContextGenerator, QgsLayoutUndoOb
The item will also be deleted.
%End
void addMultiFrame( QgsLayoutMultiFrame *multiFrame /Transfer/ );
%Docstring
Adds a ``multiFrame`` to the layout. The object is owned by the layout until removeMultiFrame() is called.
.. seealso:: removeMultiFrame()
.. seealso:: multiFrames()
%End
void removeMultiFrame( QgsLayoutMultiFrame *multiFrame );
%Docstring
Removes a ``multiFrame`` from the layout (but does not delete it).
.. seealso:: addMultiFrame()
.. seealso:: multiFrames()
%End
QSet< QgsLayoutMultiFrame * > multiFrames() const;
%Docstring
Returns a list of multi frames contained in the layout.
.. seealso:: addMultiFrame()
.. seealso:: removeMultiFrame()
:rtype: set of QgsLayoutMultiFrame
%End
QDomElement writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const;
%Docstring
Returns the layout's state encapsulated in a DOM element.

View File

@ -0,0 +1,115 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/layout/qgslayoutframe.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/
class QgsLayoutFrame: QgsLayoutItem
{
%Docstring
Base class for frame items, which form a layout multiframe item.
.. versionadded:: 3.0
%End
%TypeHeaderCode
#include "qgslayoutframe.h"
%End
public:
QgsLayoutFrame( QgsLayout *layout, QgsLayoutMultiFrame *multiFrame );
%Docstring
Constructor for QgsLayoutFrame, with the specified parent ``layout``
and belonging to a ``multiFrame``.
%End
virtual int type() const;
virtual QString stringType() const;
virtual QString displayName() const;
void setContentSection( const QRectF &section );
%Docstring
Sets the visible part of the multiframe's content which is visible within
this frame (relative to the total multiframe extent in layout units).
.. seealso:: extent()
%End
QgsLayoutMultiFrame *multiFrame() const;
%Docstring
Returns the parent multiframe for the frame.
:rtype: QgsLayoutMultiFrame
%End
QRectF extent() const;
%Docstring
Returns the visible portion of the multi frame's content which
is shown in this frame, in layout units.
.. seealso:: setContentSection()
:rtype: QRectF
%End
bool hidePageIfEmpty() const;
%Docstring
Returns whether the page should be hidden (ie, not included in layout exports) if this frame is empty
:return: true if page should be hidden if frame is empty
.. seealso:: setHidePageIfEmpty()
:rtype: bool
%End
void setHidePageIfEmpty( const bool hidePageIfEmpty );
%Docstring
Sets whether the page should be hidden (ie, not included in layout exports) if this frame is empty
\param hidePageIfEmpty set to true if page should be hidden if frame is empty
.. seealso:: hidePageIfEmpty()
%End
bool hideBackgroundIfEmpty() const;
%Docstring
Returns whether the background and frame stroke should be hidden if this frame is empty
:return: true if background and stroke should be hidden if frame is empty
.. seealso:: setHideBackgroundIfEmpty()
:rtype: bool
%End
void setHideBackgroundIfEmpty( const bool hideBackgroundIfEmpty );
%Docstring
Sets whether the background and frame stroke should be hidden if this frame is empty
\param hideBackgroundIfEmpty set to true if background and stroke should be hidden if frame is empty
.. seealso:: hideBackgroundIfEmpty()
%End
bool isEmpty() const;
%Docstring
Returns whether the frame is empty.
.. seealso:: hidePageIfEmpty()
:rtype: bool
%End
virtual QgsExpressionContext createExpressionContext() const;
protected:
virtual void draw( QgsRenderContext &context, const QStyleOptionGraphicsItem *itemStyle = 0 );
void drawFrame( QgsRenderContext &context );
void drawBackground( QgsRenderContext &context );
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/layout/qgslayoutframe.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/

View File

@ -109,6 +109,7 @@ class QgsLayoutItemRegistry : QObject
LayoutShape,
LayoutPolygon,
LayoutPolyline,
LayoutFrame,
// item
PluginItem,

View File

@ -0,0 +1,281 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/layout/qgslayoutmultiframe.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/
class QgsLayoutMultiFrame: QgsLayoutObject
{
%Docstring
Abstract base class for layout items with the ability to distribute the content to
several frames (QgsLayoutFrame items).
.. versionadded:: 3.0
%End
%TypeHeaderCode
#include "qgslayoutmultiframe.h"
%End
public:
enum ResizeMode
{
UseExistingFrames,
ExtendToNextPage,
RepeatOnEveryPage,
RepeatUntilFinished
};
QgsLayoutMultiFrame( QgsLayout *layout /TransferThis/ );
%Docstring
Construct a new multiframe item, attached to the specified ``layout``.
%End
~QgsLayoutMultiFrame();
virtual QSizeF totalSize() const = 0;
%Docstring
Returns the total size of the multiframe's content, in layout units.
:rtype: QSizeF
%End
virtual QSizeF fixedFrameSize( const int frameIndex = -1 ) const;
%Docstring
Returns the fixed size for a frame, if desired. If the fixed frame size changes,
the sizes of all frames can be recalculated by calling recalculateFrameRects().
\param frameIndex frame number
:return: fixed size for frame. If the size has a width or height of 0, then
the frame size is not fixed in that direction and frames can have variable width
or height accordingly.
.. seealso:: minFrameSize()
.. seealso:: recalculateFrameRects()
:rtype: QSizeF
%End
virtual QSizeF minFrameSize( const int frameIndex = -1 ) const;
%Docstring
Returns the minimum size for a frames, if desired. If the minimum
size changes, the sizes of all frames can be recalculated by calling
recalculateFrameRects().
\param frameIndex frame number
:return: minimum size for frame. If the size has a width or height of 0, then
the frame size has no minimum in that direction.
.. seealso:: fixedFrameSize()
.. seealso:: recalculateFrameRects()
:rtype: QSizeF
%End
virtual void render( QgsRenderContext &context, const QRectF &renderExtent, const int frameIndex,
const QStyleOptionGraphicsItem *itemStyle = 0 ) = 0;
%Docstring
Renders a portion of the multiframe's content into a render ``context``.
\param context destination render painter
\param renderExtent visible extent of content to render into the painter.
\param frameIndex frame number for content
\param itemStyle item style options for graphics item rendering
%End
virtual void addFrame( QgsLayoutFrame *frame /Transfer/, bool recalcFrameSizes = true );
%Docstring
Adds a ``frame`` to the multiframe.
If ``recalcFrameSizes`` is set to true, then a recalculation of all existing frame sizes will be forced.
.. seealso:: removeFrame()
%End
virtual double findNearbyPageBreak( double yPos );
%Docstring
Finds the optimal position to break a frame at.
\param yPos maximum vertical position for break, in layout units.
:return: the optimal breakable position which occurs in the multi frame close
to and before the specified yPos
:rtype: float
%End
void removeFrame( int index, bool removeEmptyPages = false );
%Docstring
Removes a frame by ``index`` from the multiframe. This method automatically removes the frame from the
layout too.
If ``removeEmptyPages`` is set to true, then pages which are empty after the frame is removed will
also be removed from the layout.
.. seealso:: addFrame()
.. seealso:: deleteFrames()
%End
void deleteFrames();
%Docstring
Removes and deletes all child frames.
.. seealso:: removeFrame()
%End
void setResizeMode( ResizeMode mode );
%Docstring
Sets the resize ``mode`` for the multiframe, and recalculates frame sizes to match.
.. seealso:: resizeMode()
%End
ResizeMode resizeMode() const;
%Docstring
Returns the resize mode for the multiframe.
.. seealso:: setResizeMode()
:rtype: ResizeMode
%End
virtual bool writeXml( QDomElement &elem, QDomDocument &doc, bool ignoreFrames = false ) const = 0;
%Docstring
Stores state information about multiframe in DOM element. Implementations of writeXml
should also call the _writeXML method to save general multiframe properties.
\param elem is DOM element
\param doc is the DOM document
\param ignoreFrames set to false to avoid writing state information about child frames into DOM
.. seealso:: _writeXML
:rtype: bool
%End
bool _writeXml( QDomElement &elem, QDomDocument &doc, bool ignoreFrames = false ) const;
%Docstring
Stores state information about base multiframe object in DOM element. Implementations of writeXml
should call this method.
\param elem is DOM element
\param doc is the DOM document
\param ignoreFrames set to false to avoid writing state information about child frames into DOM
.. seealso:: writeXml
:rtype: bool
%End
virtual bool readXml( const QDomElement &itemElem, const QDomDocument &doc, bool ignoreFrames = false ) = 0;
%Docstring
Reads multiframe state information from a DOM element. Implementations of readXml
should also call the _readXML method to restore general multiframe properties.
\param itemElem is DOM element
\param doc is the DOM document
\param ignoreFrames set to false to avoid read state information about child frames from DOM
.. seealso:: _readXML
:rtype: bool
%End
bool _readXml( const QDomElement &itemElem, const QDomDocument &doc, bool ignoreFrames = false );
%Docstring
Restores state information about base multiframe object from a DOM element. Implementations of readXml
should call this method.
\param itemElem is DOM element
\param doc is the DOM document
\param ignoreFrames set to false to avoid reading state information about child frames from DOM
.. seealso:: readXml
:rtype: bool
%End
int frameCount() const;
%Docstring
Returns the number of frames associated with this multiframe.
.. seealso:: frames()
:rtype: int
%End
QgsLayoutFrame *frame( int index ) const;
%Docstring
Returns the child frame at a specified ``index`` from the multiframe.
.. seealso:: frameIndex()
:rtype: QgsLayoutFrame
%End
int frameIndex( QgsLayoutFrame *frame ) const;
%Docstring
Returns the index of a ``frame`` within the multiframe.
:return: index for frame if found, -1 if frame not found in multiframe
.. seealso:: frame()
:rtype: int
%End
QgsLayoutFrame *createNewFrame( QgsLayoutFrame *currentFrame, QPointF pos, QSizeF size );
%Docstring
Creates a new frame and adds it to the multi frame and layout.
\param currentFrame an existing QgsLayoutFrame from which to copy the size
and general frame properties (e.g., frame style, background, rendering settings).
\param pos position of top-left corner of the new frame, in layout units
\param size size of the new frame, in layout units
:rtype: QgsLayoutFrame
%End
virtual QString displayName() const;
%Docstring
Returns the multiframe display name.
:rtype: str
%End
public slots:
void update();
%Docstring
Forces a redraw of all child frames.
%End
virtual void recalculateFrameSizes();
%Docstring
Recalculates the portion of the multiframe item which is shown in each of its
component frames. If the resize mode is set to anything but UseExistingFrames then
this may cause new frames to be added or frames to be removed, in order to fit
the current size of the multiframe's content.
.. seealso:: recalculateFrameRects()
%End
void recalculateFrameRects();
%Docstring
Forces a recalculation of all the associated frame's scene rectangles. This
method is useful for multiframes which implement a minFrameSize() or
fixedFrameSize() method.
.. seealso:: minFrameSize()
.. seealso:: fixedFrameSize()
.. seealso:: recalculateFrameSizes
%End
signals:
void changed();
%Docstring
Emitted when the properties of a multi frame have changed, and the GUI item widget
must be updated.
%End
void contentsChanged();
%Docstring
Emitted when the contents of the multi frame have changed and the frames
must be redrawn.
%End
protected:
protected slots:
void handlePageChange();
%Docstring
Adapts to changed number of layout pages if resize type is RepeatOnEveryPage.
%End
void handleFrameRemoval();
%Docstring
Called when a frame is removed. Updates frame list and recalculates
content of remaining frames.
%End
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/layout/qgslayoutmultiframe.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/

View File

@ -369,6 +369,7 @@ SET(QGIS_CORE_SRCS
layout/qgslayoutexporter.cpp
layout/qgslayoutgridsettings.cpp
layout/qgslayoutguidecollection.cpp
layout/qgslayoutframe.cpp
layout/qgslayoutitem.cpp
layout/qgslayoutitemgroup.cpp
layout/qgslayoutitemgroupundocommand.cpp
@ -389,6 +390,7 @@ SET(QGIS_CORE_SRCS
layout/qgslayoutmeasurement.cpp
layout/qgslayoutmeasurementconverter.cpp
layout/qgslayoutmodel.cpp
layout/qgslayoutmultiframe.cpp
layout/qgslayoutobject.cpp
layout/qgslayoutpagecollection.cpp
layout/qgslayoutserializableobject.cpp
@ -729,6 +731,7 @@ SET(QGIS_CORE_MOC_HDRS
layout/qgslayoutcontext.h
layout/qgslayouteffect.h
layout/qgslayoutguidecollection.h
layout/qgslayoutframe.h
layout/qgslayoutitem.h
layout/qgslayoutitemgroup.h
layout/qgslayoutitemgroupundocommand.h
@ -746,6 +749,7 @@ SET(QGIS_CORE_MOC_HDRS
layout/qgslayoutitemregistry.h
layout/qgslayoutitemshape.h
layout/qgslayoutmodel.h
layout/qgslayoutmultiframe.h
layout/qgslayoutpagecollection.h
layout/qgslayoutobject.h
layout/qgslayoutundostack.h

View File

@ -43,6 +43,8 @@ QgsLayout::~QgsLayout()
// no need for undo commands when we're destroying the layout
mBlockUndoCommands = 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
@ -413,6 +415,24 @@ void QgsLayout::removeLayoutItem( QgsLayoutItem *item )
}
}
void QgsLayout::addMultiFrame( QgsLayoutMultiFrame *multiFrame )
{
if ( !multiFrame )
return;
mMultiFrames.insert( multiFrame );
}
void QgsLayout::removeMultiFrame( QgsLayoutMultiFrame *multiFrame )
{
mMultiFrames.remove( multiFrame );
}
QSet<QgsLayoutMultiFrame *> QgsLayout::multiFrames() const
{
return mMultiFrames;
}
QgsLayoutUndoStack *QgsLayout::undoStack()
{
return mUndoStack.get();
@ -581,6 +601,12 @@ void QgsLayout::removeLayoutItemPrivate( QgsLayoutItem *item )
delete item;
}
void QgsLayout::deleteAndRemoveMultiFrames()
{
qDeleteAll( mMultiFrames );
mMultiFrames.clear();
}
void QgsLayout::updateZValues( const bool addUndoCommands )
{
int counter = mItemsModel->zOrderListSize();

View File

@ -29,6 +29,7 @@
class QgsLayoutItemMap;
class QgsLayoutModel;
class QgsLayoutMultiFrame;
/**
* \ingroup core
@ -428,6 +429,27 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext
*/
void removeLayoutItem( QgsLayoutItem *item );
/**
* Adds a \a multiFrame to the layout. The object is owned by the layout until removeMultiFrame() is called.
* \see removeMultiFrame()
* \see multiFrames()
*/
void addMultiFrame( QgsLayoutMultiFrame *multiFrame SIP_TRANSFER );
/**
* Removes a \a multiFrame from the layout (but does not delete it).
* \see addMultiFrame()
* \see multiFrames()
*/
void removeMultiFrame( QgsLayoutMultiFrame *multiFrame );
/**
* Returns a list of multi frames contained in the layout.
* \see addMultiFrame()
* \see removeMultiFrame()
*/
QSet< QgsLayoutMultiFrame * > multiFrames() const;
/**
* Returns the layout's state encapsulated in a DOM element.
* \see readXml()
@ -528,6 +550,9 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext
bool mBlockUndoCommands = false;
//! List of multiframe objects
QSet<QgsLayoutMultiFrame *> mMultiFrames;
//! Writes only the layout settings (not member settings like grid settings, etc) to XML
void writeXmlLayoutSettings( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const;
//! Reads only the layout settings (not member settings like grid settings, etc) from XML
@ -543,6 +568,8 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext
*/
void removeLayoutItemPrivate( QgsLayoutItem *item );
void deleteAndRemoveMultiFrames();
friend class QgsLayoutItemAddItemCommand;
friend class QgsLayoutItemDeleteUndoCommand;
friend class QgsLayoutItemUndoCommand;

View File

@ -0,0 +1,230 @@
/***************************************************************************
qgslayoutframe.cpp
------------------
begin : October 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 "qgslayoutframe.h"
#include "qgslayoutmultiframe.h"
#include "qgslayoutitemregistry.h"
#include "qgslayout.h"
QgsLayoutFrame::QgsLayoutFrame( QgsLayout *layout, QgsLayoutMultiFrame *multiFrame )
: QgsLayoutItem( layout )
, mMultiFrame( multiFrame )
{
//default to no background
setBackgroundEnabled( false );
if ( multiFrame )
{
//repaint frame when multiframe content changes
connect( multiFrame, &QgsLayoutMultiFrame::contentsChanged, this, [ = ]
{
update();
} );
#if 0 //TODO
//force recalculation of rect, so that multiframe specified sizes can be applied
setSceneRect( QRectF( pos().x(), pos().y(), rect().width(), rect().height() ) );
#endif
}
}
QgsLayoutMultiFrame *QgsLayoutFrame::multiFrame() const
{
return mMultiFrame;
}
#if 0// TODO
bool QgsLayoutFrame::writeXml( QDomElement &elem, QDomDocument &doc ) const
{
QDomElement frameElem = doc.createElement( QStringLiteral( "ComposerFrame" ) );
frameElem.setAttribute( QStringLiteral( "sectionX" ), QString::number( mSection.x() ) );
frameElem.setAttribute( QStringLiteral( "sectionY" ), QString::number( mSection.y() ) );
frameElem.setAttribute( QStringLiteral( "sectionWidth" ), QString::number( mSection.width() ) );
frameElem.setAttribute( QStringLiteral( "sectionHeight" ), QString::number( mSection.height() ) );
frameElem.setAttribute( QStringLiteral( "hidePageIfEmpty" ), mHidePageIfEmpty );
frameElem.setAttribute( QStringLiteral( "hideBackgroundIfEmpty" ), mHideBackgroundIfEmpty );
elem.appendChild( frameElem );
return _writeXml( frameElem, doc );
}
bool QgsLayoutFrame::readXml( const QDomElement &itemElem, const QDomDocument &doc )
{
double x = itemElem.attribute( QStringLiteral( "sectionX" ) ).toDouble();
double y = itemElem.attribute( QStringLiteral( "sectionY" ) ).toDouble();
double width = itemElem.attribute( QStringLiteral( "sectionWidth" ) ).toDouble();
double height = itemElem.attribute( QStringLiteral( "sectionHeight" ) ).toDouble();
mSection = QRectF( x, y, width, height );
mHidePageIfEmpty = itemElem.attribute( QStringLiteral( "hidePageIfEmpty" ), QStringLiteral( "0" ) ).toInt();
mHideBackgroundIfEmpty = itemElem.attribute( QStringLiteral( "hideBackgroundIfEmpty" ), QStringLiteral( "0" ) ).toInt();
QDomElement composerItem = itemElem.firstChildElement( QStringLiteral( "ComposerItem" ) );
if ( composerItem.isNull() )
{
return false;
}
return _readXml( composerItem, doc );
}
#endif
int QgsLayoutFrame::type() const
{
return QgsLayoutItemRegistry::LayoutFrame;
}
QString QgsLayoutFrame::stringType() const
{
return QStringLiteral( "ItemFrame" );
}
void QgsLayoutFrame::setHidePageIfEmpty( const bool hidePageIfEmpty )
{
mHidePageIfEmpty = hidePageIfEmpty;
}
void QgsLayoutFrame::setHideBackgroundIfEmpty( const bool hideBackgroundIfEmpty )
{
if ( hideBackgroundIfEmpty == mHideBackgroundIfEmpty )
{
return;
}
mHideBackgroundIfEmpty = hideBackgroundIfEmpty;
update();
}
bool QgsLayoutFrame::isEmpty() const
{
if ( !mMultiFrame )
{
return true;
}
double multiFrameHeight = mMultiFrame->totalSize().height();
if ( multiFrameHeight <= mSection.top() )
{
//multiframe height is less than top of this frame's visible portion
return true;
}
return false;
}
QgsExpressionContext QgsLayoutFrame::createExpressionContext() const
{
if ( !mMultiFrame )
return QgsLayoutItem::createExpressionContext();
//start with multiframe's context
QgsExpressionContext context = mMultiFrame->createExpressionContext();
#if 0 //TODO
//add frame's individual context
context.appendScope( QgsExpressionContextUtils::layoutItemScope( this ) );
#endif
return context;
}
QString QgsLayoutFrame::displayName() const
{
if ( !id().isEmpty() )
{
return id();
}
if ( mMultiFrame )
{
return mMultiFrame->displayName();
}
return tr( "<Frame>" );
}
#if 0 //TODO
void QgsLayoutFrame::setSceneRect( const QRectF &rectangle )
{
QRectF fixedRect = rectangle;
if ( mMultiFrame )
{
//calculate index of frame
int frameIndex = mMultiFrame->frameIndex( this );
QSizeF fixedSize = mMultiFrame->fixedFrameSize( frameIndex );
if ( fixedSize.width() > 0 )
{
fixedRect.setWidth( fixedSize.width() );
}
if ( fixedSize.height() > 0 )
{
fixedRect.setHeight( fixedSize.height() );
}
//check minimum size
QSizeF minSize = mMultiFrame->minFrameSize( frameIndex );
fixedRect.setWidth( std::max( minSize.width(), fixedRect.width() ) );
fixedRect.setHeight( std::max( minSize.height(), fixedRect.height() ) );
}
QgsComposerItem::setSceneRect( fixedRect );
}
#endif
void QgsLayoutFrame::draw( QgsRenderContext &context, const QStyleOptionGraphicsItem *itemStyle )
{
if ( mMultiFrame )
{
//calculate index of frame
int frameIndex = mMultiFrame->frameIndex( this );
mMultiFrame->render( context, mSection, frameIndex, itemStyle );
}
}
void QgsLayoutFrame::drawFrame( QgsRenderContext &context )
{
if ( !isEmpty() || !mHideBackgroundIfEmpty )
{
QgsLayoutItem::drawFrame( context );
}
}
void QgsLayoutFrame::drawBackground( QgsRenderContext &context )
{
if ( !isEmpty() || !mHideBackgroundIfEmpty )
{
QgsLayoutItem::drawBackground( context );
}
}
#if 0 //TODO
void QgsLayoutFrame::beginItemCommand( const QString &text )
{
if ( mComposition )
{
mComposition->beginMultiFrameCommand( multiFrame(), text );
}
}
void QgsLayoutFrame::endItemCommand()
{
if ( mComposition )
{
mComposition->endMultiFrameCommand();
}
}
#endif

View File

@ -0,0 +1,135 @@
/***************************************************************************
qgslayoutframe.cpp
------------------
begin : October 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. *
* *
***************************************************************************/
#ifndef QGSLAYOUTFRAME_H
#define QGSLAYOUTFRAME_H
#include "qgis_core.h"
#include "qgis.h"
#include "qgslayoutitem.h"
class QgsLayout;
class QgsLayoutMultiFrame;
/**
* \ingroup core
* Base class for frame items, which form a layout multiframe item.
* \since QGIS 3.0
*/
class CORE_EXPORT QgsLayoutFrame: public QgsLayoutItem
{
Q_OBJECT
public:
/**
* Constructor for QgsLayoutFrame, with the specified parent \a layout
* and belonging to a \a multiFrame.
*/
QgsLayoutFrame( QgsLayout *layout, QgsLayoutMultiFrame *multiFrame );
int type() const override;
QString stringType() const override;
//Overridden to allow multiframe to set display name
QString displayName() const override;
/**
* Sets the visible part of the multiframe's content which is visible within
* this frame (relative to the total multiframe extent in layout units).
* \see extent()
*/
void setContentSection( const QRectF &section ) { mSection = section; }
/**
* Returns the parent multiframe for the frame.
*/
QgsLayoutMultiFrame *multiFrame() const;
#if 0 //TODO
//Overridden to handle fixed frame sizes set by multi frame
void setSceneRect( const QRectF &rectangle ) override;
void paint( QPainter *painter, const QStyleOptionGraphicsItem *itemStyle, QWidget *pWidget ) override;
void beginItemCommand( const QString &text ) override;
void endItemCommand() override;
bool writeXml( QDomElement &elem, QDomDocument &doc ) const override;
bool readXml( const QDomElement &itemElem, const QDomDocument &doc ) override;
#endif
/**
* Returns the visible portion of the multi frame's content which
* is shown in this frame, in layout units.
* \see setContentSection()
*/
QRectF extent() const { return mSection; }
/**
* Returns whether the page should be hidden (ie, not included in layout exports) if this frame is empty
* \returns true if page should be hidden if frame is empty
* \see setHidePageIfEmpty()
*/
bool hidePageIfEmpty() const { return mHidePageIfEmpty; }
/**
* Sets whether the page should be hidden (ie, not included in layout exports) if this frame is empty
* \param hidePageIfEmpty set to true if page should be hidden if frame is empty
* \see hidePageIfEmpty()
*/
void setHidePageIfEmpty( const bool hidePageIfEmpty );
/**
* Returns whether the background and frame stroke should be hidden if this frame is empty
* \returns true if background and stroke should be hidden if frame is empty
* \see setHideBackgroundIfEmpty()
*/
bool hideBackgroundIfEmpty() const { return mHideBackgroundIfEmpty; }
/**
* Sets whether the background and frame stroke should be hidden if this frame is empty
* \param hideBackgroundIfEmpty set to true if background and stroke should be hidden if frame is empty
* \see hideBackgroundIfEmpty()
*/
void setHideBackgroundIfEmpty( const bool hideBackgroundIfEmpty );
/**
* Returns whether the frame is empty.
* \see hidePageIfEmpty()
*/
bool isEmpty() const;
QgsExpressionContext createExpressionContext() const override;
protected:
void draw( QgsRenderContext &context, const QStyleOptionGraphicsItem *itemStyle = nullptr ) override;
void drawFrame( QgsRenderContext &context );
void drawBackground( QgsRenderContext &context );
private:
QgsLayoutFrame() = delete;
QgsLayoutMultiFrame *mMultiFrame = nullptr;
QRectF mSection;
//! If true, layout will not export page if this frame is empty
bool mHidePageIfEmpty = false;
//! If true, background and outside frame will not be drawn if frame is empty
bool mHideBackgroundIfEmpty = false;
friend class QgsLayoutMultiFrame;
};
#endif // QGSLAYOUTFRAME_H

View File

@ -191,6 +191,7 @@ class CORE_EXPORT QgsLayoutItemRegistry : public QObject
LayoutShape, //!< Shape item
LayoutPolygon, //!< Polygon shape item
LayoutPolyline, //!< Polyline shape item
LayoutFrame, //!< Frame item, part of a QgsLayoutMultiFrame object
// item types provided by plugins
PluginItem, //!< Starting point for plugin item types

View File

@ -0,0 +1,431 @@
/***************************************************************************
qgslayoutmultiframe.cpp
-----------------------
begin : October 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 "qgslayoutmultiframe.h"
#include "qgslayoutframe.h"
#include "qgslayout.h"
#include <QtCore>
QgsLayoutMultiFrame::QgsLayoutMultiFrame( QgsLayout *layout )
: QgsLayoutObject( layout )
{
mLayout->addMultiFrame( this );
#if 0 //TODO
connect( mLayout, &QgsLayout::nPagesChanged, this, &QgsLayoutMultiFrame::handlePageChange );
#endif
}
QgsLayoutMultiFrame::~QgsLayoutMultiFrame()
{
deleteFrames();
}
QSizeF QgsLayoutMultiFrame::fixedFrameSize( const int frameIndex ) const
{
Q_UNUSED( frameIndex );
return QSizeF( 0, 0 );
}
QSizeF QgsLayoutMultiFrame::minFrameSize( const int frameIndex ) const
{
Q_UNUSED( frameIndex );
return QSizeF( 0, 0 );
}
double QgsLayoutMultiFrame::findNearbyPageBreak( double yPos )
{
return yPos;
}
void QgsLayoutMultiFrame::addFrame( QgsLayoutFrame *frame, bool recalcFrameSizes )
{
if ( !frame )
return;
mFrameItems.push_back( frame );
frame->mMultiFrame = this;
connect( frame, &QgsLayoutFrame::destroyed, this, &QgsLayoutMultiFrame::handleFrameRemoval );
if ( mLayout )
{
mLayout->addLayoutItem( frame );
}
if ( recalcFrameSizes )
{
recalculateFrameSizes();
}
}
void QgsLayoutMultiFrame::setResizeMode( ResizeMode mode )
{
if ( mode != mResizeMode )
{
mResizeMode = mode;
recalculateFrameSizes();
emit changed();
}
}
QList<QgsLayoutFrame *> QgsLayoutMultiFrame::frames() const
{
return mFrameItems;
}
void QgsLayoutMultiFrame::recalculateFrameSizes()
{
#if 0 //TODO
if ( mFrameItems.empty() )
{
return;
}
QSizeF size = totalSize();
double totalHeight = size.height();
if ( totalHeight < 1 )
{
return;
}
double currentY = 0;
double currentHeight = 0;
QgsLayoutFrame *currentItem = nullptr;
for ( int i = 0; i < mFrameItems.size(); ++i )
{
if ( mResizeMode != RepeatOnEveryPage && currentY >= totalHeight )
{
if ( mResizeMode == RepeatUntilFinished || mResizeMode == ExtendToNextPage ) //remove unneeded frames in extent mode
{
bool removingPages = true;
for ( int j = mFrameItems.size(); j > i; --j )
{
int numPagesBefore = mLayout->pageCollection()->pageCount();
removeFrame( j - 1, removingPages );
//if removing the frame didn't also remove the page, then stop removing pages
removingPages = removingPages && ( mLayout->pageCollection()->pageCount() < numPagesBefore );
}
return;
}
}
currentItem = mFrameItems.value( i );
currentHeight = currentItem->rect().height();
if ( mResizeMode == RepeatOnEveryPage )
{
currentItem->setContentSection( QRectF( 0, 0, currentItem->rect().width(), currentHeight ) );
}
else
{
currentHeight = findNearbyPageBreak( currentY + currentHeight ) - currentY;
currentItem->setContentSection( QRectF( 0, currentY, currentItem->rect().width(), currentHeight ) );
}
currentItem->update();
currentY += currentHeight;
}
//at end of frames but there is still content left. Add other pages if ResizeMode ==
if ( mResizeMode != UseExistingFrames )
{
while ( ( mResizeMode == RepeatOnEveryPage ) || currentY < totalHeight )
{
//find out on which page the lower left point of the last frame is
int page = std::floor( ( currentItem->pos().y() + currentItem->rect().height() ) / ( mLayout->paperHeight() + mComposition->spaceBetweenPages() ) ) + 1;
if ( mResizeMode == RepeatOnEveryPage )
{
if ( page >= mComposition->numPages() )
{
break;
}
}
else
{
//add an extra page if required
if ( mComposition->numPages() < ( page + 1 ) )
{
mComposition->setNumPages( page + 1 );
}
}
double frameHeight = 0;
if ( mResizeMode == RepeatUntilFinished || mResizeMode == RepeatOnEveryPage )
{
frameHeight = currentItem->rect().height();
}
else //mResizeMode == ExtendToNextPage
{
frameHeight = ( currentY + mComposition->paperHeight() ) > totalHeight ? totalHeight - currentY : mComposition->paperHeight();
}
double newFrameY = page * ( mComposition->paperHeight() + mComposition->spaceBetweenPages() );
if ( mResizeMode == RepeatUntilFinished || mResizeMode == RepeatOnEveryPage )
{
newFrameY += currentItem->pos().y() - ( page - 1 ) * ( mComposition->paperHeight() + mComposition->spaceBetweenPages() );
}
//create new frame
QgsLayoutFrame *newFrame = createNewFrame( currentItem,
QPointF( currentItem->pos().x(), newFrameY ),
QSizeF( currentItem->rect().width(), frameHeight ) );
if ( mResizeMode == RepeatOnEveryPage )
{
newFrame->setContentSection( QRectF( 0, 0, newFrame->rect().width(), newFrame->rect().height() ) );
currentY += frameHeight;
}
else
{
double contentHeight = findNearbyPageBreak( currentY + newFrame->rect().height() ) - currentY;
newFrame->setContentSection( QRectF( 0, currentY, newFrame->rect().width(), contentHeight ) );
currentY += contentHeight;
}
currentItem = newFrame;
}
}
#endif
}
void QgsLayoutMultiFrame::recalculateFrameRects()
{
#if 0 //TODO
if ( mFrameItems.empty() )
{
//no frames, nothing to do
return;
}
const QList< QgsLayoutFrame * > frames = mFrameItems;
for ( QgsLayoutFrame *frame : frames )
{
frame->setSceneRect( QRectF( frame->scenePos().x(), frame->scenePos().y(),
frame->rect().width(), frame->rect().height() ) );
}
#endif
}
QgsLayoutFrame *QgsLayoutMultiFrame::createNewFrame( QgsLayoutFrame *currentFrame, QPointF pos, QSizeF size )
{
if ( !currentFrame )
{
return nullptr;
}
QgsLayoutFrame *newFrame = new QgsLayoutFrame( mLayout, this );
newFrame->attemptSetSceneRect( QRectF( pos.x(), pos.y(), size.width(), size.height() ) );
//copy some settings from the parent frame
newFrame->setBackgroundColor( currentFrame->backgroundColor() );
newFrame->setBackgroundEnabled( currentFrame->hasBackground() );
newFrame->setBlendMode( currentFrame->blendMode() );
newFrame->setFrameEnabled( currentFrame->hasFrame() );
newFrame->setFrameStrokeColor( currentFrame->frameStrokeColor() );
newFrame->setFrameJoinStyle( currentFrame->frameJoinStyle() );
newFrame->setFrameStrokeWidth( currentFrame->frameStrokeWidth() );
newFrame->setItemOpacity( currentFrame->itemOpacity() );
newFrame->setHideBackgroundIfEmpty( currentFrame->hideBackgroundIfEmpty() );
addFrame( newFrame, false );
return newFrame;
}
QString QgsLayoutMultiFrame::displayName() const
{
return tr( "<Multiframe>" );
}
void QgsLayoutMultiFrame::handleFrameRemoval()
{
if ( mBlockUpdates )
return;
QgsLayoutFrame *frame = qobject_cast<QgsLayoutFrame *>( sender() );
if ( !frame )
{
return;
}
int index = mFrameItems.indexOf( frame );
if ( index == -1 )
{
return;
}
mFrameItems.removeAt( index );
if ( !mFrameItems.isEmpty() )
{
if ( resizeMode() != QgsLayoutMultiFrame::RepeatOnEveryPage && !mIsRecalculatingSize )
{
//removing a frame forces the multi frame to UseExistingFrames resize mode
//otherwise the frame may not actually be removed, leading to confusing ui behavior
mResizeMode = QgsLayoutMultiFrame::UseExistingFrames;
emit changed();
recalculateFrameSizes();
}
}
}
void QgsLayoutMultiFrame::handlePageChange()
{
#if 0 //TODO
if ( mLayout->pageCollection()->pageCount() < 1 )
{
return;
}
if ( mResizeMode != RepeatOnEveryPage )
{
return;
}
//remove items beginning on non-existing pages
for ( int i = mFrameItems.size() - 1; i >= 0; --i )
{
QgsLayoutFrame *frame = mFrameItems.at( i );
int page = frame->pos().y() / ( mComposition->paperHeight() + mComposition->spaceBetweenPages() );
if ( page > ( mComposition->numPages() - 1 ) )
{
removeFrame( i );
}
}
//page number of the last item
QgsLayoutFrame *lastFrame = mFrameItems.last();
int lastItemPage = lastFrame->pos().y() / ( mLayout->paperHeight() + mLayout->spaceBetweenPages() );
for ( int i = lastItemPage + 1; i < mLayout->pageCollection()->pageCount(); ++i )
{
//copy last frame to current page
QgsLayoutFrame *newFrame = new QgsLayoutFrame( mLayout, this, lastFrame->pos().x(),
lastFrame->pos().y() + mLayout->paperHeight() + mLayout->spaceBetweenPages(),
lastFrame->rect().width(), lastFrame->rect().height() );
addFrame( newFrame, false );
lastFrame = newFrame;
}
recalculateFrameSizes();
update();
#endif
}
void QgsLayoutMultiFrame::removeFrame( int i, const bool removeEmptyPages )
{
if ( i >= mFrameItems.count() )
{
return;
}
QgsLayoutFrame *frameItem = mFrameItems.at( i );
if ( mLayout )
{
mIsRecalculatingSize = true;
#if 0 //TODO
int pageNumber = frameItem->page();
//remove item, but don't create undo command
#if 0 //TODO - block undo commands
#endif
mLayout->removeLayoutItem( frameItem );
//if frame was the only item on the page, remove the page
if ( removeEmptyPages && mComposition->pageIsEmpty( pageNumber ) )
{
mComposition->setNumPages( mComposition->numPages() - 1 );
}
#endif
mIsRecalculatingSize = false;
}
mFrameItems.removeAt( i );
}
void QgsLayoutMultiFrame::update()
{
for ( QgsLayoutFrame *frame : qgis::as_const( mFrameItems ) )
{
frame->update();
}
}
void QgsLayoutMultiFrame::deleteFrames()
{
mBlockUpdates = true;
ResizeMode bkResizeMode = mResizeMode;
mResizeMode = UseExistingFrames;
for ( QgsLayoutFrame *frame : qgis::as_const( mFrameItems ) )
{
#if 0 //TODO -block undo commands
#endif
mLayout->removeLayoutItem( frame );
}
mFrameItems.clear();
mResizeMode = bkResizeMode;
mBlockUpdates = false;
}
QgsLayoutFrame *QgsLayoutMultiFrame::frame( int i ) const
{
if ( i < 0 || i >= mFrameItems.size() )
{
return nullptr;
}
return mFrameItems.at( i );
}
int QgsLayoutMultiFrame::frameIndex( QgsLayoutFrame *frame ) const
{
return mFrameItems.indexOf( frame );
}
bool QgsLayoutMultiFrame::_writeXml( QDomElement &elem, QDomDocument &doc, bool ignoreFrames ) const
{
#if 0 //TODO
elem.setAttribute( QStringLiteral( "resizeMode" ), mResizeMode );
if ( !ignoreFrames )
{
QList<QgsComposerFrame *>::const_iterator frameIt = mFrameItems.constBegin();
for ( ; frameIt != mFrameItems.constEnd(); ++frameIt )
{
( *frameIt )->writeXml( elem, doc );
}
}
QgsComposerObject::writeXml( elem, doc );
#endif
return true;
}
bool QgsLayoutMultiFrame::_readXml( const QDomElement &itemElem, const QDomDocument &doc, bool ignoreFrames )
{
#if 0 //TODO
QgsComposerObject::readXml( itemElem, doc );
mResizeMode = static_cast< ResizeMode >( itemElem.attribute( QStringLiteral( "resizeMode" ), QStringLiteral( "0" ) ).toInt() );
if ( !ignoreFrames )
{
QDomNodeList frameList = itemElem.elementsByTagName( QStringLiteral( "ComposerFrame" ) );
for ( int i = 0; i < frameList.size(); ++i )
{
QDomElement frameElem = frameList.at( i ).toElement();
QgsComposerFrame *newFrame = new QgsComposerFrame( mComposition, this, 0, 0, 0, 0 );
newFrame->readXml( frameElem, doc );
addFrame( newFrame, false );
}
//TODO - think there should be a recalculateFrameSizes() call here
}
#endif
return true;
}

View File

@ -0,0 +1,304 @@
/***************************************************************************
qgslayoutmultiframe.h
--------------------
begin : October 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. *
* *
***************************************************************************/
#ifndef QGSLAYOUTMULTIFRAME_H
#define QGSLAYOUTMULTIFRAME_H
#include "qgis_core.h"
#include "qgis.h"
#include "qgslayoutobject.h"
#include <QObject>
#include <QSizeF>
#include <QPointF>
class QgsLayoutFrame;
class QgsLayoutItem;
class QgsLayout;
class QDomDocument;
class QDomElement;
class QRectF;
class QPainter;
class QStyleOptionGraphicsItem;
class QgsRenderContext;
/**
* \ingroup core
* \class QgsLayoutMultiFrame
* Abstract base class for layout items with the ability to distribute the content to
* several frames (QgsLayoutFrame items).
* \since QGIS 3.0
*/
class CORE_EXPORT QgsLayoutMultiFrame: public QgsLayoutObject
{
Q_OBJECT
public:
/**
* Specifies the behavior for creating new frames to fit the multiframe's content
*/
enum ResizeMode
{
UseExistingFrames = 0, //!< Don't automatically create new frames, just use existing frames
ExtendToNextPage, //!< Creates new full page frames on the following page(s) until the entire multiframe content is visible
RepeatOnEveryPage, //!< Repeats the same frame on every page
RepeatUntilFinished /*!< creates new frames with the same position and dimensions as the existing frame on the following page(s),
until the entire multiframe content is visible */
};
/**
* Construct a new multiframe item, attached to the specified \a layout.
*/
QgsLayoutMultiFrame( QgsLayout *layout SIP_TRANSFERTHIS );
~QgsLayoutMultiFrame();
/**
* Returns the total size of the multiframe's content, in layout units.
*/
virtual QSizeF totalSize() const = 0;
/**
* Returns the fixed size for a frame, if desired. If the fixed frame size changes,
* the sizes of all frames can be recalculated by calling recalculateFrameRects().
* \param frameIndex frame number
* \returns fixed size for frame. If the size has a width or height of 0, then
* the frame size is not fixed in that direction and frames can have variable width
* or height accordingly.
* \see minFrameSize()
* \see recalculateFrameRects()
*/
virtual QSizeF fixedFrameSize( const int frameIndex = -1 ) const;
/**
* Returns the minimum size for a frames, if desired. If the minimum
* size changes, the sizes of all frames can be recalculated by calling
* recalculateFrameRects().
* \param frameIndex frame number
* \returns minimum size for frame. If the size has a width or height of 0, then
* the frame size has no minimum in that direction.
* \see fixedFrameSize()
* \see recalculateFrameRects()
*/
virtual QSizeF minFrameSize( const int frameIndex = -1 ) const;
/**
* Renders a portion of the multiframe's content into a render \a context.
* \param context destination render painter
* \param renderExtent visible extent of content to render into the painter.
* \param frameIndex frame number for content
* \param itemStyle item style options for graphics item rendering
*/
virtual void render( QgsRenderContext &context, const QRectF &renderExtent, const int frameIndex,
const QStyleOptionGraphicsItem *itemStyle = nullptr ) = 0;
/**
* Adds a \a frame to the multiframe.
*
* If \a recalcFrameSizes is set to true, then a recalculation of all existing frame sizes will be forced.
*
* \see removeFrame()
*/
virtual void addFrame( QgsLayoutFrame *frame SIP_TRANSFER, bool recalcFrameSizes = true );
/**
* Finds the optimal position to break a frame at.
* \param yPos maximum vertical position for break, in layout units.
* \returns the optimal breakable position which occurs in the multi frame close
* to and before the specified yPos
*/
virtual double findNearbyPageBreak( double yPos );
/**
* Removes a frame by \a index from the multiframe. This method automatically removes the frame from the
* layout too.
*
* If \a removeEmptyPages is set to true, then pages which are empty after the frame is removed will
* also be removed from the layout.
*
* \see addFrame()
* \see deleteFrames()
*/
void removeFrame( int index, bool removeEmptyPages = false );
/**
* Removes and deletes all child frames.
* \see removeFrame()
*/
void deleteFrames();
/**
* Sets the resize \a mode for the multiframe, and recalculates frame sizes to match.
* \see resizeMode()
*/
void setResizeMode( ResizeMode mode );
/**
* Returns the resize mode for the multiframe.
* \see setResizeMode()
*/
ResizeMode resizeMode() const { return mResizeMode; }
/**
* Stores state information about multiframe in DOM element. Implementations of writeXml
* should also call the _writeXML method to save general multiframe properties.
* \param elem is DOM element
* \param doc is the DOM document
* \param ignoreFrames set to false to avoid writing state information about child frames into DOM
* \see _writeXML
*/
virtual bool writeXml( QDomElement &elem, QDomDocument &doc, bool ignoreFrames = false ) const = 0;
/**
* Stores state information about base multiframe object in DOM element. Implementations of writeXml
* should call this method.
* \param elem is DOM element
* \param doc is the DOM document
* \param ignoreFrames set to false to avoid writing state information about child frames into DOM
* \see writeXml
*/
bool _writeXml( QDomElement &elem, QDomDocument &doc, bool ignoreFrames = false ) const;
/**
* Reads multiframe state information from a DOM element. Implementations of readXml
* should also call the _readXML method to restore general multiframe properties.
* \param itemElem is DOM element
* \param doc is the DOM document
* \param ignoreFrames set to false to avoid read state information about child frames from DOM
* \see _readXML
*/
virtual bool readXml( const QDomElement &itemElem, const QDomDocument &doc, bool ignoreFrames = false ) = 0;
/**
* Restores state information about base multiframe object from a DOM element. Implementations of readXml
* should call this method.
* \param itemElem is DOM element
* \param doc is the DOM document
* \param ignoreFrames set to false to avoid reading state information about child frames from DOM
* \see readXml
*/
bool _readXml( const QDomElement &itemElem, const QDomDocument &doc, bool ignoreFrames = false );
/**
* Returns a list of all child frames for this multiframe.
* \see frameCount()
* \note Not available in Python bindings
*/
QList<QgsLayoutFrame *> frames() const SIP_SKIP;
/**
* Returns the number of frames associated with this multiframe.
* \see frames()
*/
int frameCount() const { return mFrameItems.size(); }
/**
* Returns the child frame at a specified \a index from the multiframe.
* \see frameIndex()
*/
QgsLayoutFrame *frame( int index ) const;
/**
* Returns the index of a \a frame within the multiframe.
* \returns index for frame if found, -1 if frame not found in multiframe
* \see frame()
*/
int frameIndex( QgsLayoutFrame *frame ) const;
/**
* Creates a new frame and adds it to the multi frame and layout.
* \param currentFrame an existing QgsLayoutFrame from which to copy the size
* and general frame properties (e.g., frame style, background, rendering settings).
* \param pos position of top-left corner of the new frame, in layout units
* \param size size of the new frame, in layout units
*/
QgsLayoutFrame *createNewFrame( QgsLayoutFrame *currentFrame, QPointF pos, QSizeF size );
/**
* Returns the multiframe display name.
*/
virtual QString displayName() const;
public slots:
/**
* Forces a redraw of all child frames.
*/
void update();
/**
* Recalculates the portion of the multiframe item which is shown in each of its
* component frames. If the resize mode is set to anything but UseExistingFrames then
* this may cause new frames to be added or frames to be removed, in order to fit
* the current size of the multiframe's content.
* \see recalculateFrameRects()
*/
virtual void recalculateFrameSizes();
/**
* Forces a recalculation of all the associated frame's scene rectangles. This
* method is useful for multiframes which implement a minFrameSize() or
* fixedFrameSize() method.
* \see minFrameSize()
* \see fixedFrameSize()
* \see recalculateFrameSizes
*/
void recalculateFrameRects();
signals:
/**
* Emitted when the properties of a multi frame have changed, and the GUI item widget
* must be updated.
*/
void changed();
/**
* Emitted when the contents of the multi frame have changed and the frames
* must be redrawn.
*/
void contentsChanged();
protected:
QList<QgsLayoutFrame *> mFrameItems;
ResizeMode mResizeMode = UseExistingFrames;
protected slots:
/**
* Adapts to changed number of layout pages if resize type is RepeatOnEveryPage.
*/
void handlePageChange();
/**
* Called when a frame is removed. Updates frame list and recalculates
* content of remaining frames.
*/
void handleFrameRemoval();
private:
QgsLayoutMultiFrame() = delete;
bool mIsRecalculatingSize = false;
bool mBlockUpdates = false;
};
#endif // QGSLAYOUTMULTIFRAME_H

View File

@ -140,6 +140,7 @@ SET(TESTS
testqgslayoutmapgrid.cpp
testqgslayoutmapoverview.cpp
testqgslayoutmodel.cpp
testqgslayoutmultiframe.cpp
testqgslayoutobject.cpp
testqgslayoutpage.cpp
testqgslayoutpicture.cpp

View File

@ -0,0 +1,484 @@
/***************************************************************************
testqgslayoutmultiframe.cpp
---------------------------
begin : October 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 "qgslayoutframe.h"
#include "qgslayoutmultiframe.h"
#include "qgslayoutitemlabel.h"
#include "qgslayout.h"
#include "qgsmultirenderchecker.h"
#include "qgsapplication.h"
#include "qgsproject.h"
#include <QObject>
#include "qgstest.h"
class TestQgsLayoutMultiFrame : public QObject
{
Q_OBJECT
private slots:
void initTestCase();// will be called before the first testfunction is executed.
void cleanupTestCase();// will be called after the last testfunction was executed.
void init();// will be called before each testfunction is executed.
void cleanup();// will be called after every testfunction.
void layoutMethods();
void addFrame(); //test creating new frame inherits all properties of existing frame
void displayName();
void frameIsEmpty(); //test if frame is empty works
void addRemovePage(); //test if page is added and removed for RepeatUntilFinished mode
void undoRedo(); //test that combinations of frame/multiframe undo/redo don't crash
void undoRedoRemovedFrame(); //test that undo doesn't crash with removed frames
private:
QgsLayout *mLayout = nullptr;
QString mReport;
};
class TestMultiFrame : public QgsLayoutMultiFrame
{
Q_OBJECT
public:
TestMultiFrame( QgsLayout *layout )
: QgsLayoutMultiFrame( layout )
{
}
void render( QgsRenderContext &, const QRectF &, const int,
const QStyleOptionGraphicsItem * ) override
{
}
QSizeF totalSize() const override
{
return QSizeF();
}
bool writeXml( QDomElement &, QDomDocument &, bool ) const override
{
return true;
}
bool readXml( const QDomElement &, const QDomDocument &, bool ) override
{
return true;
}
};
void TestQgsLayoutMultiFrame::initTestCase()
{
QgsApplication::init();
QgsApplication::initQgis();
mLayout = new QgsLayout( QgsProject::instance() );
mLayout->initializeDefaults();
mReport = QStringLiteral( "<h1>Layout MultiFrame Tests</h1>\n" );
}
void TestQgsLayoutMultiFrame::cleanupTestCase()
{
delete mLayout;
QString myReportFile = QDir::tempPath() + "/qgistest.html";
QFile myFile( myReportFile );
if ( myFile.open( QIODevice::WriteOnly | QIODevice::Append ) )
{
QTextStream myQTextStream( &myFile );
myQTextStream << mReport;
myFile.close();
}
QgsApplication::exitQgis();
}
void TestQgsLayoutMultiFrame::init()
{
}
void TestQgsLayoutMultiFrame::cleanup()
{
}
void TestQgsLayoutMultiFrame::layoutMethods()
{
QgsLayout *l = new QgsLayout( QgsProject::instance() );
QVERIFY( l->multiFrames().empty() );
l->addMultiFrame( nullptr );
QVERIFY( l->multiFrames().empty() );
TestMultiFrame *mF = new TestMultiFrame( l );
QCOMPARE( l->multiFrames().count(), 1 );
QVERIFY( l->multiFrames().contains( mF ) );
// try readding
l->addMultiFrame( mF );
QCOMPARE( l->multiFrames().count(), 1 );
l->removeMultiFrame( nullptr );
QCOMPARE( l->multiFrames().count(), 1 );
QVERIFY( l->multiFrames().contains( mF ) );
TestMultiFrame *mF2 = new TestMultiFrame( l );
QPointer< TestMultiFrame > pMF2( mF2 );
l->addMultiFrame( mF2 );
QCOMPARE( l->multiFrames().count(), 2 );
QVERIFY( l->multiFrames().contains( mF ) );
QVERIFY( l->multiFrames().contains( mF2 ) );
l->removeMultiFrame( mF2 );
QCOMPARE( l->multiFrames().count(), 1 );
QVERIFY( l->multiFrames().contains( mF ) );
// should not be deleted
QVERIFY( pMF2 );
delete mF2;
QVERIFY( !pMF2 );
delete l;
}
void TestQgsLayoutMultiFrame::addFrame()
{
TestMultiFrame *multiframe = new TestMultiFrame( mLayout );
QCOMPARE( multiframe->frameCount(), 0 );
QVERIFY( multiframe->frames().empty() );
QVERIFY( !multiframe->frame( -1 ) );
QVERIFY( !multiframe->frame( 0 ) );
QVERIFY( !multiframe->frame( 1 ) );
QCOMPARE( multiframe->frameIndex( nullptr ), -1 );
multiframe->addFrame( nullptr );
QCOMPARE( multiframe->frameCount(), 0 );
QVERIFY( multiframe->frames().empty() );
QgsLayoutFrame *frame1 = new QgsLayoutFrame( mLayout, multiframe );
QCOMPARE( multiframe->frameIndex( frame1 ), -1 );
frame1->attemptSetSceneRect( QRectF( 0, 0, 100, 200 ) );
multiframe->addFrame( frame1 );
QCOMPARE( multiframe->frameCount(), 1 );
QCOMPARE( multiframe->frames().count(), 1 );
QVERIFY( multiframe->frames().contains( frame1 ) );
QVERIFY( !multiframe->frame( -1 ) );
QCOMPARE( multiframe->frame( 0 ), frame1 );
QVERIFY( !multiframe->frame( 1 ) );
QCOMPARE( multiframe->frameIndex( frame1 ), 0 );
QCOMPARE( frame1->multiFrame(), multiframe );
// deferred set multiframe
QgsLayoutFrame *frame1a = new QgsLayoutFrame( mLayout, nullptr );
QVERIFY( !frame1a->multiFrame() );
multiframe->addFrame( frame1a );
QCOMPARE( frame1a->multiFrame(), multiframe );
//should not be inherited
frame1->setHidePageIfEmpty( true );
//should be inherited
frame1->setHideBackgroundIfEmpty( true );
frame1->setFrameStrokeWidth( QgsLayoutMeasurement( 5.0 ) );
frame1->setFrameJoinStyle( Qt::RoundJoin );
frame1->setFrameEnabled( true );
frame1->setFrameStrokeColor( QColor( Qt::red ) );
frame1->setBackgroundEnabled( true );
frame1->setBackgroundColor( QColor( Qt::green ) );
frame1->setBlendMode( QPainter::CompositionMode_ColorBurn );
frame1->setItemOpacity( 0.5 );
QgsLayoutFrame *frame2 = multiframe->createNewFrame( frame1, QPointF( 50, 55 ), QSizeF( 70, 120 ) );
//check frame created in correct place
QCOMPARE( frame2->rect().height(), 120.0 );
QCOMPARE( frame2->rect().width(), 70.0 );
QCOMPARE( frame2->scenePos().x(), 50.0 );
QCOMPARE( frame2->scenePos().y(), 55.0 );
//check frame properties
QCOMPARE( frame2->frameStrokeWidth(), frame1->frameStrokeWidth() );
QCOMPARE( frame2->frameStrokeColor(), frame1->frameStrokeColor() );
QCOMPARE( frame2->frameJoinStyle(), frame1->frameJoinStyle() );
QCOMPARE( frame2->hasBackground(), frame1->hasBackground() );
QCOMPARE( frame2->backgroundColor(), frame1->backgroundColor() );
QCOMPARE( frame2->blendMode(), frame1->blendMode() );
QCOMPARE( frame2->itemOpacity(), frame1->itemOpacity() );
//check non-inherited properties
QVERIFY( !frame2->hidePageIfEmpty() );
mLayout->removeMultiFrame( multiframe );
delete multiframe;
}
void TestQgsLayoutMultiFrame::displayName()
{
TestMultiFrame *multiframe = new TestMultiFrame( mLayout );
QCOMPARE( multiframe->displayName(), QStringLiteral( "<Multiframe>" ) );
QgsLayoutFrame *frame1 = new QgsLayoutFrame( mLayout, nullptr );
QCOMPARE( frame1->displayName(), QStringLiteral( "<Frame>" ) );
multiframe->addFrame( frame1 );
QCOMPARE( frame1->displayName(), QStringLiteral( "<Multiframe>" ) );
frame1->setId( "my frame" );
QCOMPARE( frame1->displayName(), QStringLiteral( "my frame" ) );
}
void TestQgsLayoutMultiFrame::frameIsEmpty()
{
#if 0 //TODO
QgsComposerHtml *htmlItem = new QgsComposerHtml( mLayout, false );
QgsComposerFrame *frame1 = new QgsComposerFrame( mLayout, htmlItem, 0, 0, 100, 200 );
QgsComposerFrame *frame2 = new QgsComposerFrame( mLayout, htmlItem, 0, 0, 100, 200 );
htmlItem->addFrame( frame1 );
htmlItem->addFrame( frame2 );
htmlItem->setContentMode( QgsComposerHtml::ManualHtml );
//short content, so frame 2 should be empty
htmlItem->setHtml( QStringLiteral( "<p><i>Test manual <b>html</b></i></p>" ) );
htmlItem->loadHtml();
QCOMPARE( frame1->isEmpty(), false );
QCOMPARE( frame2->isEmpty(), true );
//long content, so frame 2 should not be empty
htmlItem->setHtml( QStringLiteral( "<p style=\"height: 10000px\"><i>Test manual <b>html</b></i></p>" ) );
htmlItem->loadHtml();
QCOMPARE( frame1->isEmpty(), false );
QCOMPARE( frame2->isEmpty(), false );
//..and back again..
htmlItem->setHtml( QStringLiteral( "<p><i>Test manual <b>html</b></i></p>" ) );
htmlItem->loadHtml();
QCOMPARE( frame1->isEmpty(), false );
QCOMPARE( frame2->isEmpty(), true );
mLayout->removeMultiFrame( htmlItem );
delete htmlItem;
#endif
}
void TestQgsLayoutMultiFrame::addRemovePage()
{
#if 0 //TODO
QgsComposerHtml *htmlItem = new QgsComposerHtml( mLayout, false );
QgsComposerFrame *frame1 = new QgsComposerFrame( mLayout, htmlItem, 0, 0, 100, 200 );
htmlItem->addFrame( frame1 );
htmlItem->setContentMode( QgsComposerHtml::ManualHtml );
htmlItem->setResizeMode( QgsComposerMultiFrame::RepeatUntilFinished );
//short content, so should fit in one frame
htmlItem->setHtml( QStringLiteral( "<p><i>Test manual <b>html</b></i></p>" ) );
htmlItem->loadHtml();
//should be one page
QCOMPARE( htmlItem->frameCount(), 1 );
QCOMPARE( mLayout->numPages(), 1 );
//long content, so we require 3 frames
htmlItem->setHtml( QStringLiteral( "<p style=\"height: 2000px\"><i>Test manual <b>html</b></i></p>" ) );
htmlItem->loadHtml();
QCOMPARE( htmlItem->frameCount(), 3 );
QCOMPARE( mLayout->numPages(), 3 );
//..and back again..
htmlItem->setHtml( QStringLiteral( "<p><i>Test manual <b>html</b></i></p>" ) );
htmlItem->loadHtml();
QCOMPARE( htmlItem->frameCount(), 1 );
QCOMPARE( mLayout->numPages(), 1 );
//get a bit more complicated - add another item to page 3
QgsComposerLabel *label1 = new QgsComposerLabel( mLayout );
mLayout->addComposerLabel( label1 );
label1->setItemPosition( 10, 10, 50, 50, QgsComposerItem::UpperLeft, false, 3 );
//long content, so we require 4 pages
htmlItem->setHtml( QStringLiteral( "<p style=\"height: 3000px\"><i>Test manual <b>html</b></i></p>" ) );
htmlItem->loadHtml();
QCOMPARE( htmlItem->frameCount(), 4 );
QCOMPARE( mLayout->numPages(), 4 );
//..and back again. Since there's an item on page 3, only page 4 should be removed
htmlItem->setHtml( QStringLiteral( "<p><i>Test manual <b>html</b></i></p>" ) );
htmlItem->loadHtml();
QCOMPARE( htmlItem->frameCount(), 1 );
QCOMPARE( mLayout->numPages(), 3 );
mLayout->removeMultiFrame( htmlItem );
delete htmlItem;
#endif
}
void TestQgsLayoutMultiFrame::undoRedo()
{
#if 0 //TODO
QgsComposerHtml *htmlItem = new QgsComposerHtml( mLayout, false );
QgsComposerFrame *frame1 = new QgsComposerFrame( mLayout, htmlItem, 0, 0, 100, 200 );
htmlItem->addFrame( frame1 );
htmlItem->setContentMode( QgsComposerHtml::ManualHtml );
htmlItem->setResizeMode( QgsComposerMultiFrame::RepeatUntilFinished );
//short content, so should fit in one frame
htmlItem->setHtml( QStringLiteral( "<p>Test content</p>" ) );
htmlItem->loadHtml();
//do some combinations of undo/redo commands for both the frame and multiframe
//to try to trigger a crash
frame1->beginCommand( QStringLiteral( "move" ) );
frame1->setSceneRect( QRectF( 10, 10, 20, 20 ) );
frame1->endCommand();
frame1->beginCommand( QStringLiteral( "stroke" ), QgsComposerMergeCommand::ItemStrokeWidth );
frame1->setFrameStrokeWidth( 4.0 );
frame1->endCommand();
frame1->beginCommand( QStringLiteral( "stroke" ), QgsComposerMergeCommand::ItemStrokeWidth );
frame1->setFrameStrokeWidth( 7.0 );
frame1->endCommand();
//multiframe commands
mLayout->beginMultiFrameCommand( htmlItem, QStringLiteral( "maxbreak" ) );
htmlItem->setMaxBreakDistance( 100 );
mLayout->endMultiFrameCommand();
//another frame command
frame1->beginCommand( QStringLiteral( "bgcolor" ), QgsComposerMergeCommand::ItemOpacity );
frame1->setBackgroundColor( QColor( 255, 255, 0 ) );
frame1->endCommand();
frame1->beginCommand( QStringLiteral( "bgcolor" ), QgsComposerMergeCommand::ItemOpacity );
frame1->setBackgroundColor( QColor( 255, 0, 0 ) );
frame1->endCommand();
//undo changes
//frame bg
mLayout->undoStack()->undo();
//multiframe max break
mLayout->undoStack()->undo();
//frame stroke width
mLayout->undoStack()->undo();
//frame move
mLayout->undoStack()->undo();
//check result
QCOMPARE( htmlItem->maxBreakDistance(), 10.0 );
QCOMPARE( htmlItem->frame( 0 )->frameStrokeWidth(), 0.3 );
QCOMPARE( htmlItem->frame( 0 )->pos(), QPointF( 0, 0 ) );
QCOMPARE( htmlItem->frame( 0 )->backgroundColor(), QColor( 255, 255, 255 ) );
//now redo
//frame move
mLayout->undoStack()->redo();
//frame stroke width
mLayout->undoStack()->redo();
//multiframe max break
mLayout->undoStack()->redo();
//frame bg color
mLayout->undoStack()->redo();
//check result
QCOMPARE( htmlItem->maxBreakDistance(), 100.0 );
QCOMPARE( htmlItem->frame( 0 )->frameStrokeWidth(), 7.0 );
QCOMPARE( htmlItem->frame( 0 )->pos(), QPointF( 10, 10 ) );
QCOMPARE( htmlItem->frame( 0 )->backgroundColor(), QColor( 255, 0, 0 ) );
mLayout->removeMultiFrame( htmlItem );
delete htmlItem;
#endif
}
void TestQgsLayoutMultiFrame::undoRedoRemovedFrame()
{
#if 0 //TODO
QgsComposerHtml *htmlItem = new QgsComposerHtml( mLayout, false );
QgsComposerFrame *frame1 = new QgsComposerFrame( mLayout, htmlItem, 0, 0, 100, 200 );
htmlItem->addFrame( frame1 );
htmlItem->setContentMode( QgsComposerHtml::ManualHtml );
htmlItem->setResizeMode( QgsComposerMultiFrame::RepeatUntilFinished );
//long content, so should require multiple frames
htmlItem->setHtml( QStringLiteral( "<p style=\"height: 2000px\">Test content</p>" ) );
htmlItem->loadHtml();
QVERIFY( htmlItem->frameCount() > 1 );
//do a command on the first frame
htmlItem->frame( 0 )->beginCommand( QStringLiteral( "stroke" ), QgsComposerMergeCommand::ItemStrokeWidth );
htmlItem->frame( 0 )->setFrameStrokeWidth( 4.0 );
htmlItem->frame( 0 )->endCommand();
//do a command on the second frame
htmlItem->frame( 1 )->beginCommand( QStringLiteral( "stroke" ), QgsComposerMergeCommand::ItemStrokeWidth );
htmlItem->frame( 1 )->setFrameStrokeWidth( 8.0 );
htmlItem->frame( 1 )->endCommand();
//do a multiframe command which removes extra frames
mLayout->beginMultiFrameCommand( htmlItem, QStringLiteral( "source" ) );
htmlItem->setHtml( QStringLiteral( "<p style=\"height: 20px\">Test content</p>" ) );
mLayout->endMultiFrameCommand();
//wipes the second frame
htmlItem->loadHtml();
QCOMPARE( htmlItem->frameCount(), 1 );
//undo changes
//multiframe command
mLayout->undoStack()->undo();
//frame 2 command
mLayout->undoStack()->undo();
//frame 1 command
mLayout->undoStack()->undo();
//check result
QVERIFY( htmlItem->frameCount() > 1 );
QCOMPARE( htmlItem->frame( 0 )->frameStrokeWidth(), 0.3 );
QCOMPARE( htmlItem->frame( 1 )->frameStrokeWidth(), 0.3 );
//now redo
//frame 1 command
mLayout->undoStack()->redo();
//frame 2 command
mLayout->undoStack()->redo();
//check result
QVERIFY( htmlItem->frameCount() > 1 );
QCOMPARE( htmlItem->frame( 0 )->frameStrokeWidth(), 4.0 );
QCOMPARE( htmlItem->frame( 1 )->frameStrokeWidth(), 8.0 );
//multiframe command
mLayout->undoStack()->redo();
QCOMPARE( htmlItem->frameCount(), 1 );
mLayout->removeMultiFrame( htmlItem );
delete htmlItem;
#endif
}
QGSTEST_MAIN( TestQgsLayoutMultiFrame )
#include "testqgslayoutmultiframe.moc"