Merge pull request #5119 from nyalldawson/layout_next3

[layouts] Undo/redo framework
This commit is contained in:
Nyall Dawson 2017-09-10 11:38:59 +10:00 committed by GitHub
commit cea7eb8be5
51 changed files with 2491 additions and 60 deletions

View File

@ -30,6 +30,7 @@ from qgis.core import QgsSettings
class optionsDialog(QDialog, Ui_SettingsDialogPythonConsole):
def __init__(self, parent):
QDialog.__init__(self, parent)
self.setWindowTitle(QCoreApplication.translate(

View File

@ -160,8 +160,11 @@
%Include layout/qgslayoutmeasurementconverter.sip
%Include layout/qgspagesizeregistry.sip
%Include layout/qgslayoutpoint.sip
%Include layout/qgslayoutserializableobject.sip
%Include layout/qgslayoutsize.sip
%Include layout/qgslayoutsnapper.sip
%Include layout/qgslayoutundocommand.sip
%Include layout/qgslayoutundostack.sip
%Include layout/qgslayoututils.sip
%Include metadata/qgslayermetadata.sip
%Include metadata/qgslayermetadatavalidator.sip

View File

@ -8,7 +8,7 @@
class QgsLayout : QGraphicsScene, QgsExpressionContextGenerator
class QgsLayout : QGraphicsScene, QgsExpressionContextGenerator, QgsLayoutUndoObjectInterface
{
%Docstring
Base class for layouts, which can contain items such as maps, labels, scalebars, etc.
@ -39,8 +39,6 @@ class QgsLayout : QGraphicsScene, QgsExpressionContextGenerator
called on the new layout.
%End
~QgsLayout();
void initializeDefaults();
%Docstring
Initializes an empty layout, e.g. by adding a default page to the layout. This should be called after creating
@ -67,6 +65,14 @@ class QgsLayout : QGraphicsScene, QgsExpressionContextGenerator
.. seealso:: name()
%End
QgsLayoutItem *itemByUuid( const QString &uuid );
%Docstring
Returns the layout item with matching ``uuid`` unique identifier, or a None
if a matching item could not be found.
:rtype: QgsLayoutItem
%End
void setUnits( QgsUnitTypes::LayoutUnit units );
%Docstring
Sets the native measurement ``units`` for the layout. These also form the default unit
@ -247,6 +253,32 @@ class QgsLayout : QGraphicsScene, QgsExpressionContextGenerator
method. Ownership of the item is transferred to the layout.
%End
QDomElement writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const;
%Docstring
Returns the layout's state encapsulated in a DOM element.
.. seealso:: readXml()
:rtype: QDomElement
%End
bool readXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context );
%Docstring
Sets the collection's state from a DOM element. ``layoutElement`` is the DOM node corresponding to the layout.
.. seealso:: writeXml()
:rtype: bool
%End
QgsLayoutUndoStack *undoStack();
%Docstring
Returns a pointer to the layout's undo stack, which manages undo/redo states for the layout
and it's associated objects.
:rtype: QgsLayoutUndoStack
%End
virtual QgsAbstractLayoutUndoCommand *createCommand( const QString &text, int id = 0, QUndoCommand *parent = 0 ) /Factory/;
public slots:
void updateBounds();

View File

@ -8,7 +8,7 @@
class QgsLayoutGridSettings
class QgsLayoutGridSettings : QgsLayoutSerializableObject
{
%Docstring
Contains settings relating to the appearance, spacing and offset for layout grids.
@ -27,11 +27,15 @@ class QgsLayoutGridSettings
StyleCrosses
};
QgsLayoutGridSettings();
QgsLayoutGridSettings( QgsLayout *layout );
%Docstring
Constructor for QgsLayoutGridSettings.
%End
virtual QString stringType() const;
virtual QgsLayout *layout();
void setResolution( const QgsLayoutMeasurement &resolution );
%Docstring
Sets the page/snap grid ``resolution``.
@ -92,6 +96,22 @@ class QgsLayoutGridSettings
:rtype: Style
%End
virtual bool writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const;
%Docstring
Stores the grid's state in a DOM element. The ``parentElement`` should refer to the parent layout's DOM element.
.. seealso:: readXml()
:rtype: bool
%End
virtual bool readXml( const QDomElement &gridElement, const QDomDocument &document, const QgsReadWriteContext &context );
%Docstring
Sets the grid's state from a DOM element. gridElement is the DOM node corresponding to the grid.
.. seealso:: writeXml()
:rtype: bool
%End
};
/************************************************************************

View File

@ -132,7 +132,7 @@ class QgsLayoutGuide : QObject
};
class QgsLayoutGuideCollection : QAbstractTableModel
class QgsLayoutGuideCollection : QAbstractTableModel, QgsLayoutSerializableObject
{
%Docstring
Stores and manages the snap guides used by a layout.
@ -160,6 +160,10 @@ class QgsLayoutGuideCollection : QAbstractTableModel
%End
~QgsLayoutGuideCollection();
virtual QString stringType() const;
virtual QgsLayout *layout();
virtual int rowCount( const QModelIndex & ) const;
virtual int columnCount( const QModelIndex & ) const;
@ -188,6 +192,11 @@ class QgsLayoutGuideCollection : QAbstractTableModel
.. seealso:: clear()
%End
void setGuideLayoutPosition( QgsLayoutGuide *guide, double position );
%Docstring
Sets the absolute ``position`` (in layout coordinates) for ``guide`` within the layout.
%End
void clear();
%Docstring
Removes all guides from the collection.
@ -233,6 +242,22 @@ class QgsLayoutGuideCollection : QAbstractTableModel
.. seealso:: visible()
%End
virtual bool writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const;
%Docstring
Stores the collection's state in a DOM element. The ``parentElement`` should refer to the parent layout's DOM element.
.. seealso:: readXml()
:rtype: bool
%End
virtual bool readXml( const QDomElement &collectionElement, const QDomDocument &document, const QgsReadWriteContext &context );
%Docstring
Sets the collection's state from a DOM element. collectionElement is the DOM node corresponding to the collection.
.. seealso:: writeXml()
:rtype: bool
%End
};

View File

@ -9,7 +9,7 @@
class QgsLayoutItem : QgsLayoutObject, QGraphicsRectItem
class QgsLayoutItem : QgsLayoutObject, QGraphicsRectItem, QgsLayoutUndoObjectInterface
{
%Docstring
Base class for graphical items within a QgsLayout.
@ -219,6 +219,9 @@ class QgsLayoutItem : QgsLayoutObject, QGraphicsRectItem
:rtype: bool
%End
virtual QgsAbstractLayoutUndoCommand *createCommand( const QString &text, int id, QUndoCommand *parent = 0 ) /Factory/;
public slots:
virtual void refresh();

View File

@ -32,6 +32,16 @@ class QgsLayoutItemPage : QgsLayoutItem
%Docstring
Constructor for QgsLayoutItemPage, with the specified parent ``layout``.
%End
static QgsLayoutItemPage *create( QgsLayout *layout, const QVariantMap &settings ) /Factory/;
%Docstring
Returns a new page item for the specified ``layout``.
The caller takes responsibility for deleting the returned object.
:rtype: QgsLayoutItemPage
%End
virtual int type() const;
virtual QString stringType() const;
@ -80,6 +90,9 @@ class QgsLayoutItemPage : QgsLayoutItem
virtual void attemptResize( const QgsLayoutSize &size );
virtual QgsAbstractLayoutUndoCommand *createCommand( const QString &text, int id, QUndoCommand *parent = 0 ) /Factory/;
public slots:
virtual void redraw();

View File

@ -9,7 +9,7 @@
class QgsLayoutPageCollection : QObject
class QgsLayoutPageCollection : QObject, QgsLayoutSerializableObject
{
%Docstring
A manager for a collection of pages in a layout.
@ -28,11 +28,9 @@ class QgsLayoutPageCollection : QObject
~QgsLayoutPageCollection();
QgsLayout *layout() const;
%Docstring
Returns the layout this collection belongs to.
:rtype: QgsLayout
%End
virtual QString stringType() const;
virtual QgsLayout *layout();
QList< QgsLayoutItemPage * > pages();
%Docstring
@ -130,6 +128,12 @@ class QgsLayoutPageCollection : QObject
Calling deletePage() automatically triggers a reflow() of pages.
%End
QgsLayoutItemPage *takePage( QgsLayoutItemPage *page ) /TransferBack/;
%Docstring
Takes a ``page`` from the collection, returning ownership of the page to the caller.
:rtype: QgsLayoutItemPage
%End
void setPageStyleSymbol( QgsFillSymbol *symbol );
%Docstring
Sets the ``symbol`` to use for drawing pages in the collection.
@ -211,6 +215,29 @@ class QgsLayoutPageCollection : QObject
:rtype: float
%End
virtual bool writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const;
%Docstring
Stores the collection's state in a DOM element. The ``parentElement`` should refer to the parent layout's DOM element.
.. seealso:: readXml()
:rtype: bool
%End
virtual bool readXml( const QDomElement &collectionElement, const QDomDocument &document, const QgsReadWriteContext &context );
%Docstring
Sets the collection's state from a DOM element. collectionElement is the DOM node corresponding to the collection.
.. seealso:: writeXml()
:rtype: bool
%End
QgsLayoutGuideCollection &guides();
%Docstring
Returns a reference to the collection's guide collection, which manages page snap guides.
:rtype: QgsLayoutGuideCollection
%End
public slots:
void redraw();

View File

@ -0,0 +1,66 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/layout/qgslayoutserializableobject.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/
class QgsLayoutSerializableObject : QgsLayoutUndoObjectInterface
{
%Docstring
An interface for layout objects which can be stored and read from DOM elements.
.. versionadded:: 3.0
%End
%TypeHeaderCode
#include "qgslayoutserializableobject.h"
%End
public:
virtual ~QgsLayoutSerializableObject();
virtual QString stringType() const = 0;
%Docstring
Return the object type as a string.
This string must be a unique, single word, character only representation of the item type, eg "LayoutScaleBar"
:rtype: str
%End
virtual QgsLayout *layout() = 0;
%Docstring
Returns the layout the object belongs to.
:rtype: QgsLayout
%End
virtual bool writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const = 0;
%Docstring
Stores the objects's state in a DOM element. The ``parentElement`` should refer to the parent layout's DOM element.
.. seealso:: readXml()
:rtype: bool
%End
virtual bool readXml( const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context ) = 0;
%Docstring
Sets the objects's state from a DOM element. ``element`` is the DOM node corresponding to the object.
.. seealso:: writeXml()
:rtype: bool
%End
virtual QgsAbstractLayoutUndoCommand *createCommand( const QString &text, int id, QUndoCommand *parent = 0 ) /Factory/;
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/layout/qgslayoutserializableobject.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/

View File

@ -8,7 +8,7 @@
class QgsLayoutSnapper
class QgsLayoutSnapper: QgsLayoutSerializableObject
{
%Docstring
Manages snapping grids and preset snap lines in a layout, and handles
@ -26,6 +26,10 @@ class QgsLayoutSnapper
Constructor for QgsLayoutSnapper, attached to the specified ``layout``.
%End
virtual QString stringType() const;
virtual QgsLayout *layout();
void setSnapTolerance( const int snapTolerance );
%Docstring
Sets the snap ``tolerance`` (in pixels) to use when snapping.
@ -106,6 +110,22 @@ class QgsLayoutSnapper
:rtype: float
%End
virtual bool writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const;
%Docstring
Stores the snapper's state in a DOM element. The ``parentElement`` should refer to the parent layout's DOM element.
.. seealso:: readXml()
:rtype: bool
%End
virtual bool readXml( const QDomElement &gridElement, const QDomDocument &document, const QgsReadWriteContext &context );
%Docstring
Sets the snapper's state from a DOM element. snapperElement is the DOM node corresponding to the snapper.
.. seealso:: writeXml()
:rtype: bool
%End
};
/************************************************************************

View File

@ -0,0 +1,132 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/layout/qgslayoutundocommand.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/
class QgsAbstractLayoutUndoCommand: QUndoCommand
{
%Docstring
Base class for commands to undo/redo layout and layout object changes.
.. versionadded:: 3.0
%End
%TypeHeaderCode
#include "qgslayoutundocommand.h"
%End
public:
QgsAbstractLayoutUndoCommand( const QString &text, int id = 0, QUndoCommand *parent /TransferThis/ = 0 );
%Docstring
Constructor for QgsLayoutUndoCommand.
The ``id`` argument can be used to specify an id number for the source event - this is used to determine whether QUndoCommand
command compression can apply to the command.
%End
virtual void undo();
virtual void redo();
virtual int id() const;
void saveBeforeState();
%Docstring
Saves current layout state as before state.
.. seealso:: beforeState()
.. seealso:: saveAfterState()
%End
void saveAfterState();
%Docstring
Saves current layout state as after state.
.. seealso:: afterState()
.. seealso:: saveBeforeState()
%End
QDomDocument beforeState() const;
%Docstring
Returns the before state for the layout.
.. seealso:: saveBeforeState()
.. seealso:: afterState()
:rtype: QDomDocument
%End
QDomDocument afterState() const;
%Docstring
Returns the after state for the layout.
.. seealso:: saveAfterState()
.. seealso:: beforeState()
:rtype: QDomDocument
%End
virtual bool containsChange() const;
%Docstring
Returns true if both the before and after states are valid and different.
:rtype: bool
%End
protected:
virtual void saveState( QDomDocument &stateDoc ) const = 0;
%Docstring
Saves the state of the object to the specified ``stateDoc``.
Subclasses must implement this to handle encapsulating their current state into a DOM document.
.. seealso:: restoreState()
%End
virtual void restoreState( QDomDocument &stateDoc ) = 0;
%Docstring
Restores the state of the object from the specified ``stateDoc``.
Subclasses must implement this to handle restoring their current state from the encapsulated state.
.. seealso:: saveState()
%End
void setAfterState( const QDomDocument &stateDoc );
%Docstring
Manually sets the after state for the command. Generally this should not be called directly.
%End
};
class QgsLayoutUndoObjectInterface
{
%Docstring
Interface for layout objects which support undo/redo commands.
.. versionadded:: 3.0
%End
%TypeHeaderCode
#include "qgslayoutundocommand.h"
%End
public:
virtual QgsAbstractLayoutUndoCommand *createCommand( const QString &text, int id = 0, QUndoCommand *parent = 0 ) = 0 /Factory/;
%Docstring
Creates a new layout undo command with the specified ``text`` and ``parent``.
The ``id`` argument can be used to specify an id number for the source event - this is used to determine whether QUndoCommand
command compression can apply to the command.
:rtype: QgsAbstractLayoutUndoCommand
%End
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/layout/qgslayoutundocommand.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/

View File

@ -0,0 +1,99 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/layout/qgslayoutundostack.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/
class QgsLayoutUndoStack
{
%Docstring
An undo stack for QgsLayouts.
.. versionadded:: 3.0
%End
%TypeHeaderCode
#include "qgslayoutundostack.h"
%End
public:
QgsLayoutUndoStack( QgsLayout *layout );
%Docstring
Constructor for QgsLayoutUndoStack, for the specified parent ``layout``.
%End
void beginMacro( const QString &commandText );
%Docstring
Starts a macro command, with the given descriptive ``commandText``.
Any commands added to the stack (either via direct manipulation of
stack() or via beginCommand()/endCommand() calls) between a
beginMacro() and endMacro() block are collapsed into a single
undo command, which will be applied or rolled back in a single step.
.. seealso:: endMacro()
%End
void endMacro();
%Docstring
Ends a macro command. This must be called after beginMacro(), when
all child undo commands which form part of the macro have been completed.
Any commands added to the stack (either via direct manipulation of
stack() or via beginCommand()/endCommand() calls) between a
beginMacro() and endMacro() block are collapsed into a single
undo command, which will be applied or rolled back in a single step.
.. seealso:: beginMacro()
%End
void beginCommand( QgsLayoutUndoObjectInterface *object, const QString &commandText, int id = 0 );
%Docstring
Begins a new undo command for the specified ``object``.
This must be followed by a call to endCommand() or cancelCommand() after the desired changes
have been made to ``object``.
The ``id`` argument can be used to specify an id number for the source event - this is used to determine whether QUndoCommand
command compression can apply to the command.
.. seealso:: endCommand()
.. seealso:: cancelCommand()
%End
void endCommand();
%Docstring
Saves final state of an object and pushes the active command to the undo history.
.. seealso:: beginCommand()
.. seealso:: cancelCommand()
%End
void cancelCommand();
%Docstring
Cancels the active command, discarding it without pushing to the undo history.
.. seealso:: endCommand()
.. seealso:: cancelCommand()
%End
QUndoStack *stack();
%Docstring
Returns a pointer to the internal QUndoStack.
:rtype: QUndoStack
%End
private:
QgsLayoutUndoStack( const QgsLayoutUndoStack &other );
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/core/layout/qgslayoutundostack.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/

View File

@ -44,6 +44,7 @@
#include <QDesktopWidget>
#include <QSlider>
#include <QLabel>
#include <QUndoView>
//add some nice zoom levels for zoom comboboxes
@ -286,15 +287,28 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla
mGuideDock->setUserVisible( true );
} );
mUndoDock = new QgsDockWidget( tr( "Command history" ), this );
mUndoDock->setObjectName( QStringLiteral( "CommandDock" ) );
mPanelsMenu->addAction( mUndoDock->toggleViewAction() );
mUndoView = new QUndoView( this );
mUndoDock->setWidget( mUndoView );
addDockWidget( Qt::RightDockWidgetArea, mItemDock );
addDockWidget( Qt::RightDockWidgetArea, mGeneralDock );
addDockWidget( Qt::RightDockWidgetArea, mGuideDock );
addDockWidget( Qt::RightDockWidgetArea, mUndoDock );
createLayoutPropertiesWidget();
mUndoDock->show();
mItemDock->show();
mGeneralDock->show();
mActionUndo->setEnabled( false );
mActionRedo->setEnabled( false );
tabifyDockWidget( mGeneralDock, mUndoDock );
tabifyDockWidget( mItemDock, mUndoDock );
tabifyDockWidget( mGeneralDock, mItemDock );
restoreWindowState();
@ -325,6 +339,12 @@ void QgsLayoutDesignerDialog::setCurrentLayout( QgsLayout *layout )
mActionShowGuides->setChecked( mLayout->guides().visible() );
mActionSnapGuides->setChecked( mLayout->snapper().snapToGuides() );
connect( mLayout->undoStack()->stack(), &QUndoStack::canUndoChanged, mActionUndo, &QAction::setEnabled );
connect( mLayout->undoStack()->stack(), &QUndoStack::canRedoChanged, mActionRedo, &QAction::setEnabled );
connect( mActionUndo, &QAction::triggered, mLayout->undoStack()->stack(), &QUndoStack::undo );
connect( mActionRedo, &QAction::triggered, mLayout->undoStack()->stack(), &QUndoStack::redo );
mUndoView->setStack( mLayout->undoStack()->stack() );
createLayoutPropertiesWidget();
}
@ -357,8 +377,14 @@ void QgsLayoutDesignerDialog::showItemOptions( QgsLayoutItem *item )
delete mItemPropertiesStack->takeMainPanel();
widget->setDockMode( true );
connect( item, &QgsLayoutItem::destroyed, widget.get(), [this]
{
delete mItemPropertiesStack->takeMainPanel();
} );
mItemPropertiesStack->setMainPanel( widget.release() );
mItemDock->setUserVisible( true );
}
void QgsLayoutDesignerDialog::open()
@ -612,12 +638,17 @@ void QgsLayoutDesignerDialog::addPages()
}
if ( dlg.numberPages() > 1 )
mLayout->undoStack()->beginMacro( tr( "Add pages" ) );
for ( int i = 0; i < dlg.numberPages(); ++i )
{
QgsLayoutItemPage *page = new QgsLayoutItemPage( mLayout );
page->setPageSize( dlg.pageSize() );
mLayout->pageCollection()->insertPage( page, firstPagePosition + i );
}
if ( dlg.numberPages() > 1 )
mLayout->undoStack()->endMacro();
}
}

View File

@ -35,6 +35,7 @@ class QgsLayoutAppMenuProvider;
class QgsLayoutItem;
class QgsPanelWidgetStack;
class QgsDockWidget;
class QUndoView;
class QgsAppLayoutDesignerInterface : public QgsLayoutDesignerInterface
{
@ -206,6 +207,9 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner
QgsDockWidget *mGuideDock = nullptr;
QgsPanelWidgetStack *mGuideStack = nullptr;
QUndoView *mUndoView = nullptr;
QgsDockWidget *mUndoDock = nullptr;
//! Save window state
void saveWindowState();

View File

@ -72,22 +72,26 @@ void QgsLayoutGuideWidget::addVerticalGuide()
void QgsLayoutGuideWidget::deleteHorizontalGuide()
{
mLayout->undoStack()->beginMacro( tr( "Remove horizontal guides" ) );
Q_FOREACH ( const QModelIndex &index, mHozGuidesTableView->selectionModel()->selectedIndexes() )
{
mHozGuidesTableView->closePersistentEditor( index );
if ( index.column() == 0 )
mHozProxyModel->removeRow( index.row() );
}
mLayout->undoStack()->endMacro();
}
void QgsLayoutGuideWidget::deleteVerticalGuide()
{
mLayout->undoStack()->beginMacro( tr( "Remove vertical guides" ) );
Q_FOREACH ( const QModelIndex &index, mVertGuidesTableView->selectionModel()->selectedIndexes() )
{
mVertGuidesTableView->closePersistentEditor( index );
if ( index.column() == 0 )
mVertProxyModel->removeRow( index.row() );
}
mLayout->undoStack()->endMacro();
}
void QgsLayoutGuideWidget::pageChanged( int page )
@ -123,8 +127,10 @@ void QgsLayoutGuideWidget::clearAll()
mVertGuidesTableView->closePersistentEditor( index );
}
mLayout->undoStack()->beginMacro( tr( "Remove all guides" ) );
mVertProxyModel->removeRows( 0, mVertProxyModel->rowCount() );
mHozProxyModel->removeRows( 0, mHozProxyModel->rowCount() );
mLayout->undoStack()->endMacro();
}
void QgsLayoutGuideWidget::applyToAll()

View File

@ -128,7 +128,9 @@ void QgsLayoutPagePropertiesWidget::orientationChanged( int )
void QgsLayoutPagePropertiesWidget::updatePageSize()
{
mPage->layout()->undoStack()->beginCommand( mPage, tr( "Changed page size" ), 1 + mPage->layout()->pageCollection()->pageNumber( mPage ) );
mPage->setPageSize( QgsLayoutSize( mWidthSpin->value(), mHeightSpin->value(), mSizeUnitsComboBox->unit() ) );
mPage->layout()->undoStack()->endCommand();
mPage->layout()->pageCollection()->reflow();
}
@ -147,8 +149,6 @@ void QgsLayoutPagePropertiesWidget::showCurrentPageSize()
if ( !pageSize.isEmpty() )
{
mPageSizeComboBox->setCurrentIndex( mPageSizeComboBox->findData( pageSize ) );
mWidthSpin->setEnabled( false );
mHeightSpin->setEnabled( false );
mLockAspectRatio->setEnabled( false );
mLockAspectRatio->setLocked( false );
mSizeUnitsComboBox->setEnabled( false );
@ -158,8 +158,6 @@ void QgsLayoutPagePropertiesWidget::showCurrentPageSize()
{
// custom
mPageSizeComboBox->setCurrentIndex( mPageSizeComboBox->count() - 1 );
mWidthSpin->setEnabled( true );
mHeightSpin->setEnabled( true );
mLockAspectRatio->setEnabled( true );
mSizeUnitsComboBox->setEnabled( true );
mPageOrientationComboBox->setEnabled( false );

View File

@ -359,14 +359,19 @@ SET(QGIS_CORE_SRCS
layout/qgslayoutitempage.cpp
layout/qgslayoutitemregistry.cpp
layout/qgslayoutitemshape.cpp
layout/qgslayoutitemundocommand.cpp
layout/qgslayoutmeasurement.cpp
layout/qgslayoutmeasurementconverter.cpp
layout/qgslayoutobject.cpp
layout/qgslayoutpagecollection.cpp
layout/qgslayoutserializableobject.cpp
layout/qgslayoutsnapper.cpp
layout/qgslayoutundocommand.cpp
layout/qgslayoutundostack.cpp
layout/qgslayoututils.cpp
layout/qgspagesizeregistry.cpp
layout/qgslayoutpoint.cpp
layout/qgslayoutserializableobject.cpp
layout/qgslayoutsize.cpp
pal/costcalculator.cpp
@ -935,12 +940,16 @@ SET(QGIS_CORE_HDRS
layout/qgslayoutcontext.h
layout/qgslayoutgridsettings.h
layout/qgslayoutitemundocommand.h
layout/qgslayoutmeasurement.h
layout/qgslayoutmeasurementconverter.h
layout/qgspagesizeregistry.h
layout/qgslayoutpoint.h
layout/qgslayoutserializableobject.h
layout/qgslayoutsize.h
layout/qgslayoutsnapper.h
layout/qgslayoutundocommand.h
layout/qgslayoutundostack.h
layout/qgslayoututils.h
metadata/qgslayermetadata.h

View File

@ -49,7 +49,7 @@ QgsComposerLabel::QgsComposerLabel( QgsComposition *composition )
, mMarginX( 1.0 )
, mMarginY( 1.0 )
, mFontColor( QColor( 0, 0, 0 ) )
, mHAlignment( Qt::AlignLeft )
, mHAlignment( Qt::AlignJustify )
, mVAlignment( Qt::AlignTop )
, mDistanceArea( nullptr )
{

View File

@ -71,7 +71,7 @@ class CORE_EXPORT QgsComposerLabel: public QgsComposerItem
* \param a alignment
* \returns void
*/
void setHAlign( Qt::AlignmentFlag a ) {mHAlignment = a;}
void setHAlign( Qt::AlignmentFlag a ) { mHAlignment = a; }
/** Mutator for the vertical alignment of the label
* \param a alignment

View File

@ -17,30 +17,28 @@
#include "qgslayout.h"
#include "qgslayoutpagecollection.h"
#include "qgslayoutguidecollection.h"
#include "qgsreadwritecontext.h"
#include "qgsproject.h"
QgsLayout::QgsLayout( QgsProject *project )
: QGraphicsScene()
, mProject( project )
, mSnapper( QgsLayoutSnapper( this ) )
, mGridSettings( this )
, mPageCollection( new QgsLayoutPageCollection( this ) )
, mGuideCollection( new QgsLayoutGuideCollection( this, mPageCollection.get() ) )
, 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 );
}
QgsLayout::~QgsLayout()
{
// delete guide collection FIRST, since it depends on the page collection
mGuideCollection.reset();
}
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();
}
QgsProject *QgsLayout::project() const
@ -48,6 +46,21 @@ QgsProject *QgsLayout::project() const
return mProject;
}
QgsLayoutItem *QgsLayout::itemByUuid( const QString &uuid )
{
QList<QgsLayoutItem *> itemList;
layoutItems( itemList );
Q_FOREACH ( QgsLayoutItem *item, itemList )
{
if ( item->uuid() == uuid )
{
return item;
}
}
return nullptr;
}
double QgsLayout::convertToLayoutUnits( const QgsLayoutMeasurement &measurement ) const
{
return mContext.measurementConverter().convert( measurement, mUnits ).length();
@ -80,12 +93,12 @@ QgsLayoutPoint QgsLayout::convertFromLayoutUnits( const QPointF &point, const Qg
QgsLayoutGuideCollection &QgsLayout::guides()
{
return *mGuideCollection;
return mPageCollection->guides();
}
const QgsLayoutGuideCollection &QgsLayout::guides() const
{
return *mGuideCollection;
return mPageCollection->guides();
}
QgsExpressionContext QgsLayout::createExpressionContext() const
@ -196,6 +209,109 @@ void QgsLayout::addLayoutItem( QgsLayoutItem *item )
updateBounds();
}
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().firstChild().toElement(), 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 );
}
void QgsLayout::writeXmlLayoutSettings( QDomElement &element, QDomDocument &document, const QgsReadWriteContext & ) const
{
mCustomProperties.writeXml( element, document );
element.setAttribute( QStringLiteral( "name" ), mName );
element.setAttribute( QStringLiteral( "units" ), QgsUnitTypes::encodeUnit( mUnits ) );
}
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() );
writeXmlLayoutSettings( element, document, context );
return element;
}
bool QgsLayout::readXmlLayoutSettings( const QDomElement &layoutElement, const QDomDocument &, const QgsReadWriteContext & )
{
mCustomProperties.readXml( layoutElement );
setName( layoutElement.attribute( QStringLiteral( "name" ) ) );
setUnits( QgsUnitTypes::decodeLayoutUnit( layoutElement.attribute( QStringLiteral( "units" ) ) ) );
return true;
}
bool QgsLayout::readXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context )
{
if ( layoutElement.nodeName() != QString( "Layout" ) )
{
return false;
}
auto restore = [&]( QgsLayoutSerializableObject * object )->bool
{
return object->readXml( layoutElement, document, context );
};
readXmlLayoutSettings( layoutElement, document, context );
restore( mPageCollection.get() );
restore( &mSnapper );
restore( &mGridSettings );
return true;
}
void QgsLayout::updateBounds()
{
setSceneRect( layoutBounds( false, 0.05 ) );

View File

@ -24,6 +24,7 @@
#include "qgslayoutpagecollection.h"
#include "qgslayoutgridsettings.h"
#include "qgslayoutguidecollection.h"
#include "qgslayoutundostack.h"
class QgsLayoutItemMap;
@ -33,7 +34,7 @@ class QgsLayoutItemMap;
* \brief Base class for layouts, which can contain items such as maps, labels, scalebars, etc.
* \since QGIS 3.0
*/
class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContextGenerator
class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContextGenerator, public QgsLayoutUndoObjectInterface
{
Q_OBJECT
@ -59,8 +60,6 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext
*/
QgsLayout( QgsProject *project );
~QgsLayout();
/**
* Initializes an empty layout, e.g. by adding a default page to the layout. This should be called after creating
* a new layout.
@ -86,6 +85,31 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext
*/
void setName( const QString &name ) { mName = name; }
/**
* Returns a list of layout items of a specific type.
* \note not available in Python bindings
*/
template<class T> void layoutItems( QList<T *> &itemList ) SIP_SKIP
{
itemList.clear();
QList<QGraphicsItem *> graphicsItemList = items();
QList<QGraphicsItem *>::iterator itemIt = graphicsItemList.begin();
for ( ; itemIt != graphicsItemList.end(); ++itemIt )
{
T *item = dynamic_cast<T *>( *itemIt );
if ( item )
{
itemList.push_back( item );
}
}
}
/**
* Returns the layout item with matching \a uuid unique identifier, or a nullptr
* if a matching item could not be found.
*/
QgsLayoutItem *itemByUuid( const QString &uuid );
/**
* Sets the native measurement \a units for the layout. These also form the default unit
* for measurements for the layout.
@ -285,6 +309,33 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext
*/
void addLayoutItem( QgsLayoutItem *item SIP_TRANSFER );
/**
* Returns the layout's state encapsulated in a DOM element.
* \see readXml()
*/
QDomElement writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const;
/**
* Sets the collection's state from a DOM element. \a layoutElement is the DOM node corresponding to the layout.
* \see writeXml()
*/
bool readXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context );
/**
* Returns a pointer to the layout's undo stack, which manages undo/redo states for the layout
* and it's associated objects.
*/
QgsLayoutUndoStack *undoStack();
/**
* Returns a pointer to the layout's undo stack, which manages undo/redo states for the layout
* and it's associated objects.
*/
SIP_SKIP const QgsLayoutUndoStack *undoStack() const;
QgsAbstractLayoutUndoCommand *createCommand( const QString &text, int id = 0, QUndoCommand *parent = nullptr ) SIP_FACTORY override;
public slots:
/**
@ -313,9 +364,14 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext
QgsLayoutGridSettings mGridSettings;
std::unique_ptr< QgsLayoutPageCollection > mPageCollection;
std::unique_ptr< QgsLayoutUndoStack > mUndoStack;
std::unique_ptr< QgsLayoutGuideCollection > mGuideCollection;
//! 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
bool readXmlLayoutSettings( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context );
friend class QgsLayoutUndoCommand;
};
#endif //QGSLAYOUT_H

View File

@ -15,11 +15,76 @@
***************************************************************************/
#include "qgslayoutgridsettings.h"
#include "qgsreadwritecontext.h"
#include "qgslayout.h"
#include "qgsproject.h"
QgsLayoutGridSettings::QgsLayoutGridSettings()
QgsLayoutGridSettings::QgsLayoutGridSettings( QgsLayout *layout )
: mGridResolution( QgsLayoutMeasurement( 10 ) )
, mLayout( layout )
{
mGridPen = QPen( QColor( 190, 190, 190, 100 ), 0 );
mGridPen.setCosmetic( true );
}
QgsLayout *QgsLayoutGridSettings::layout()
{
return mLayout;
}
void QgsLayoutGridSettings::setResolution( const QgsLayoutMeasurement &resolution )
{
mLayout->undoStack()->beginCommand( this, QObject::tr( "Grid resolution changed" ), UndoGridResolution );
mGridResolution = resolution;
mLayout->undoStack()->endCommand();
}
void QgsLayoutGridSettings::setOffset( const QgsLayoutPoint offset )
{
mLayout->undoStack()->beginCommand( this, QObject::tr( "Grid offset changed" ), UndoGridOffset );
mGridOffset = offset;
mLayout->undoStack()->endCommand();
}
bool QgsLayoutGridSettings::writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext & ) const
{
QDomElement element = document.createElement( QStringLiteral( "Grid" ) );
element.setAttribute( QStringLiteral( "resolution" ), mGridResolution.length() );
element.setAttribute( QStringLiteral( "resUnits" ), QgsUnitTypes::encodeUnit( mGridResolution.units() ) );
element.setAttribute( QStringLiteral( "offsetX" ), mGridOffset.x() );
element.setAttribute( QStringLiteral( "offsetY" ), mGridOffset.y() );
element.setAttribute( QStringLiteral( "offsetUnits" ), QgsUnitTypes::encodeUnit( mGridOffset.units() ) );
parentElement.appendChild( element );
return true;
}
bool QgsLayoutGridSettings::readXml( const QDomElement &e, const QDomDocument &, const QgsReadWriteContext & )
{
QDomElement element = e;
if ( element.nodeName() != QStringLiteral( "Grid" ) )
{
element = element.firstChildElement( QStringLiteral( "Grid" ) );
}
if ( element.nodeName() != QStringLiteral( "Grid" ) )
{
return false;
}
double res = element.attribute( QStringLiteral( "resolution" ), QStringLiteral( "10" ) ).toDouble();
QgsUnitTypes::LayoutUnit resUnit = QgsUnitTypes::decodeLayoutUnit( element.attribute( QStringLiteral( "resUnits" ) ) );
mGridResolution = QgsLayoutMeasurement( res, resUnit );
double offsetX = element.attribute( QStringLiteral( "offsetX" ) ).toDouble();
double offsetY = element.attribute( QStringLiteral( "offsetY" ) ).toDouble();
QgsUnitTypes::LayoutUnit offsetUnit = QgsUnitTypes::decodeLayoutUnit( element.attribute( QStringLiteral( "offsetUnits" ) ) );
mGridOffset = QgsLayoutPoint( offsetX, offsetY, offsetUnit );
mLayout->pageCollection()->redraw();
return true;
}

View File

@ -19,9 +19,12 @@
#include "qgis_core.h"
#include "qgslayoutmeasurement.h"
#include "qgslayoutpoint.h"
#include "qgslayoutundocommand.h"
#include "qgslayoutserializableobject.h"
#include <QPen>
class QgsLayout;
class QgsReadWriteContext;
/**
* \ingroup core
@ -29,7 +32,7 @@ class QgsLayout;
* \brief Contains settings relating to the appearance, spacing and offset for layout grids.
* \since QGIS 3.0
*/
class CORE_EXPORT QgsLayoutGridSettings
class CORE_EXPORT QgsLayoutGridSettings : public QgsLayoutSerializableObject
{
public:
@ -45,14 +48,17 @@ class CORE_EXPORT QgsLayoutGridSettings
/**
* Constructor for QgsLayoutGridSettings.
*/
QgsLayoutGridSettings();
QgsLayoutGridSettings( QgsLayout *layout );
QString stringType() const override { return QStringLiteral( "LayoutGrid" ); }
QgsLayout *layout() override;
/**
* Sets the page/snap grid \a resolution.
* \see resolution()
* \see setOffset()
*/
void setResolution( const QgsLayoutMeasurement &resolution ) { mGridResolution = resolution; }
void setResolution( const QgsLayoutMeasurement &resolution );
/**
* Returns the page/snap grid resolution.
@ -66,7 +72,7 @@ class CORE_EXPORT QgsLayoutGridSettings
* \see offset()
* \see setResolution()
*/
void setOffset( const QgsLayoutPoint offset ) { mGridOffset = offset; }
void setOffset( const QgsLayoutPoint offset );
/**
* Returns the offset of the page/snap grid.
@ -103,12 +109,33 @@ class CORE_EXPORT QgsLayoutGridSettings
*/
Style style() const { return mGridStyle; }
/**
* Stores the grid's state in a DOM element. The \a parentElement should refer to the parent layout's DOM element.
* \see readXml()
*/
bool writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const override;
/**
* Sets the grid's state from a DOM element. gridElement is the DOM node corresponding to the grid.
* \see writeXml()
*/
bool readXml( const QDomElement &gridElement, const QDomDocument &document, const QgsReadWriteContext &context ) override;
private:
// Used for 'collapsing' undo commands
enum UndoCommand
{
UndoGridResolution = 1,
UndoGridOffset,
};
QgsLayoutMeasurement mGridResolution;
QgsLayoutPoint mGridOffset;
QPen mGridPen;
Style mGridStyle = StyleLines;
QgsLayout *mLayout = nullptr;
};
#endif //QGSLAYOUTGRIDSETTINGS_H

View File

@ -16,6 +16,8 @@
#include "qgslayoutguidecollection.h"
#include "qgslayout.h"
#include "qgsproject.h"
#include "qgsreadwritecontext.h"
#include <QGraphicsLineItem>
@ -205,6 +207,11 @@ QgsLayoutGuideCollection::~QgsLayoutGuideCollection()
qDeleteAll( mGuides );
}
QgsLayout *QgsLayoutGuideCollection::layout()
{
return mLayout;
}
int QgsLayoutGuideCollection::rowCount( const QModelIndex & ) const
{
return mGuides.count();
@ -279,8 +286,10 @@ bool QgsLayoutGuideCollection::setData( const QModelIndex &index, const QVariant
QgsLayoutMeasurement m = guide->position();
m.setLength( newPos );
mLayout->undoStack()->beginCommand( mPageCollection, tr( "Guide moved" ), Move + index.row() );
whileBlocking( guide )->setPosition( m );
guide->update();
mLayout->undoStack()->endCommand();
emit dataChanged( index, index, QVector<int>() << role );
return true;
}
@ -293,11 +302,28 @@ bool QgsLayoutGuideCollection::setData( const QModelIndex &index, const QVariant
QgsLayoutMeasurement m = guide->position();
m.setLength( newPos );
mLayout->undoStack()->beginCommand( mPageCollection, tr( "Guide moved" ), Move + index.row() );
whileBlocking( guide )->setPosition( m );
guide->update();
mLayout->undoStack()->endCommand();
emit dataChanged( index, index, QVector<int>() << role );
return true;
}
case LayoutPositionRole:
{
bool ok = false;
double newPos = value.toDouble( &ok );
if ( !ok )
return false;
mLayout->undoStack()->beginCommand( mPageCollection, tr( "Guide moved" ), Move + index.row() );
whileBlocking( guide )->setLayoutPosition( newPos );
mLayout->undoStack()->endCommand();
emit dataChanged( index, index, QVector<int>() << role );
return true;
}
case UnitsRole:
{
bool ok = false;
@ -307,8 +333,10 @@ bool QgsLayoutGuideCollection::setData( const QModelIndex &index, const QVariant
QgsLayoutMeasurement m = guide->position();
m.setUnits( static_cast< QgsUnitTypes::LayoutUnit >( units ) );
mLayout->undoStack()->beginCommand( mPageCollection, tr( "Guide moved" ), Move + index.row() );
whileBlocking( guide )->setPosition( m );
guide->update();
mLayout->undoStack()->endCommand();
emit dataChanged( index, index, QVector<int>() << role );
return true;
}
@ -340,12 +368,16 @@ bool QgsLayoutGuideCollection::removeRows( int row, int count, const QModelIndex
if ( parent.isValid() )
return false;
if ( !mBlockUndoCommands )
mLayout->undoStack()->beginCommand( mPageCollection, tr( "Guide(s) removed" ), Remove + row );
beginRemoveRows( parent, row, row + count - 1 );
for ( int i = 0; i < count; ++ i )
{
delete mGuides.takeAt( row );
}
endRemoveRows();
if ( !mBlockUndoCommands )
mLayout->undoStack()->endCommand();
return true;
}
@ -353,9 +385,13 @@ void QgsLayoutGuideCollection::addGuide( QgsLayoutGuide *guide )
{
guide->setLayout( mLayout );
if ( !mBlockUndoCommands )
mLayout->undoStack()->beginCommand( mPageCollection, tr( "Guide created" ) );
beginInsertRows( QModelIndex(), mGuides.count(), mGuides.count() );
mGuides.append( guide );
endInsertRows();
if ( !mBlockUndoCommands )
mLayout->undoStack()->endCommand();
QModelIndex index = createIndex( mGuides.length() - 1, 0 );
connect( guide, &QgsLayoutGuide::positionChanged, this, [ this, index ]
@ -373,16 +409,29 @@ void QgsLayoutGuideCollection::removeGuide( QgsLayoutGuide *guide )
removeRow( row );
}
void QgsLayoutGuideCollection::setGuideLayoutPosition( QgsLayoutGuide *guide, double position )
{
int row = mGuides.indexOf( guide );
if ( row < 0 )
return;
setData( index( row, 0 ), position, LayoutPositionRole );
}
void QgsLayoutGuideCollection::clear()
{
mLayout->undoStack()->beginCommand( mPageCollection, tr( "Guides cleared" ) );
beginResetModel();
qDeleteAll( mGuides );
mGuides.clear();
endResetModel();
mLayout->undoStack()->endCommand();
}
void QgsLayoutGuideCollection::applyGuidesToAllOtherPages( int sourcePage )
{
mLayout->undoStack()->beginCommand( mPageCollection, tr( "Guides applied" ) );
mBlockUndoCommands = true;
QgsLayoutItemPage *page = mPageCollection->page( sourcePage );
// remove other page's guides
Q_FOREACH ( QgsLayoutGuide *guide, mGuides )
@ -408,6 +457,8 @@ void QgsLayoutGuideCollection::applyGuidesToAllOtherPages( int sourcePage )
}
}
}
mLayout->undoStack()->endCommand();
mBlockUndoCommands = false;
}
void QgsLayoutGuideCollection::update()
@ -448,19 +499,76 @@ bool QgsLayoutGuideCollection::visible() const
void QgsLayoutGuideCollection::setVisible( bool visible )
{
mLayout->undoStack()->beginCommand( mPageCollection, tr( "Guide visibility changed" ) );
mGuidesVisible = visible;
mLayout->undoStack()->endCommand();
update();
}
void QgsLayoutGuideCollection::pageAboutToBeRemoved( int pageNumber )
{
mBlockUndoCommands = true;
Q_FOREACH ( QgsLayoutGuide *guide, guidesOnPage( pageNumber ) )
{
removeGuide( guide );
}
mBlockUndoCommands = false;
}
bool QgsLayoutGuideCollection::writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext & ) const
{
QDomElement element = document.createElement( QStringLiteral( "GuideCollection" ) );
element.setAttribute( QStringLiteral( "visible" ), mGuidesVisible );
Q_FOREACH ( QgsLayoutGuide *guide, mGuides )
{
QDomElement guideElement = document.createElement( QStringLiteral( "Guide" ) );
guideElement.setAttribute( QStringLiteral( "orientation" ), guide->orientation() );
guideElement.setAttribute( QStringLiteral( "page" ), mPageCollection->pageNumber( guide->page() ) );
guideElement.setAttribute( QStringLiteral( "position" ), guide->position().length() );
guideElement.setAttribute( QStringLiteral( "units" ), QgsUnitTypes::encodeUnit( guide->position().units() ) );
element.appendChild( guideElement );
}
parentElement.appendChild( element );
return true;
}
bool QgsLayoutGuideCollection::readXml( const QDomElement &e, const QDomDocument &, const QgsReadWriteContext & )
{
QDomElement element = e;
if ( element.nodeName() != QStringLiteral( "GuideCollection" ) )
{
element = element.firstChildElement( QStringLiteral( "GuideCollection" ) );
}
if ( element.nodeName() != QStringLiteral( "GuideCollection" ) )
{
return false;
}
mBlockUndoCommands = true;
beginResetModel();
qDeleteAll( mGuides );
mGuides.clear();
mGuidesVisible = element.attribute( QStringLiteral( "visible" ), QStringLiteral( "0" ) ) != QLatin1String( "0" );
QDomNodeList guideNodeList = element.elementsByTagName( QStringLiteral( "Guide" ) );
for ( int i = 0; i < guideNodeList.size(); ++i )
{
QDomElement element = guideNodeList.at( i ).toElement();
QgsLayoutGuide::Orientation orientation = static_cast< QgsLayoutGuide::Orientation >( element.attribute( QStringLiteral( "orientation" ), QStringLiteral( "0" ) ).toInt() );
double pos = element.attribute( QStringLiteral( "position" ), QStringLiteral( "0" ) ).toDouble();
QgsUnitTypes::LayoutUnit unit = QgsUnitTypes::decodeLayoutUnit( element.attribute( QStringLiteral( "units" ) ) );
int page = element.attribute( QStringLiteral( "page" ), QStringLiteral( "0" ) ).toInt();
std::unique_ptr< QgsLayoutGuide > guide( new QgsLayoutGuide( orientation, QgsLayoutMeasurement( pos, unit ), mPageCollection->page( page ) ) );
guide->update();
addGuide( guide.release() );
}
endResetModel();
mBlockUndoCommands = false;
return true;
}
//
// QgsLayoutGuideProxyModel

View File

@ -20,6 +20,7 @@
#include "qgslayoutmeasurement.h"
#include "qgslayoutpoint.h"
#include "qgslayoutitempage.h"
#include "qgslayoutserializableobject.h"
#include <QPen>
#include <QAbstractListModel>
#include <QSortFilterProxyModel>
@ -28,6 +29,9 @@
class QgsLayout;
class QgsLayoutPageCollection;
class QDomElement;
class QDomDocument;
class QgsReadWriteContext;
/**
* \ingroup core
@ -168,7 +172,7 @@ class CORE_EXPORT QgsLayoutGuide : public QObject
* \brief Stores and manages the snap guides used by a layout.
* \since QGIS 3.0
*/
class CORE_EXPORT QgsLayoutGuideCollection : public QAbstractTableModel
class CORE_EXPORT QgsLayoutGuideCollection : public QAbstractTableModel, public QgsLayoutSerializableObject
{
Q_OBJECT
@ -192,6 +196,9 @@ class CORE_EXPORT QgsLayoutGuideCollection : public QAbstractTableModel
QgsLayoutGuideCollection( QgsLayout *layout, QgsLayoutPageCollection *pageCollection );
~QgsLayoutGuideCollection();
QString stringType() const override { return QStringLiteral( "LayoutGuideCollection" ); }
QgsLayout *layout() override;
int rowCount( const QModelIndex & ) const override;
int columnCount( const QModelIndex & ) const override;
QVariant data( const QModelIndex &index, int role ) const override;
@ -214,6 +221,11 @@ class CORE_EXPORT QgsLayoutGuideCollection : public QAbstractTableModel
*/
void removeGuide( QgsLayoutGuide *guide );
/**
* Sets the absolute \a position (in layout coordinates) for \a guide within the layout.
*/
void setGuideLayoutPosition( QgsLayoutGuide *guide, double position );
/**
* Removes all guides from the collection.
* \see removeGuide()
@ -256,12 +268,30 @@ class CORE_EXPORT QgsLayoutGuideCollection : public QAbstractTableModel
*/
void setVisible( bool visible );
/**
* Stores the collection's state in a DOM element. The \a parentElement should refer to the parent layout's DOM element.
* \see readXml()
*/
bool writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const override;
/**
* Sets the collection's state from a DOM element. collectionElement is the DOM node corresponding to the collection.
* \see writeXml()
*/
bool readXml( const QDomElement &collectionElement, const QDomDocument &document, const QgsReadWriteContext &context ) override;
private slots:
void pageAboutToBeRemoved( int pageNumber );
private:
enum UndoRoles
{
Move = 10000,
Remove = 20000,
};
QgsLayout *mLayout = nullptr;
QgsLayoutPageCollection *mPageCollection = nullptr;
@ -269,6 +299,9 @@ class CORE_EXPORT QgsLayoutGuideCollection : public QAbstractTableModel
int mHeaderSize = 0;
bool mGuidesVisible = true;
bool mBlockUndoCommands = false;
friend class QgsLayoutGuideCollectionUndoCommand;
};

View File

@ -18,6 +18,7 @@
#include "qgslayout.h"
#include "qgslayoututils.h"
#include "qgspagesizeregistry.h"
#include "qgslayoutitemundocommand.h"
#include <QPainter>
#include <QStyleOptionGraphicsItem>
#include <QUuid>
@ -260,6 +261,11 @@ bool QgsLayoutItem::readXml( const QDomElement &itemElem, const QDomDocument &do
return readPropertiesFromElement( itemElem, doc, context );
}
QgsAbstractLayoutUndoCommand *QgsLayoutItem::createCommand( const QString &text, int id, QUndoCommand *parent )
{
return new QgsLayoutItemUndoCommand( this, text, id, parent );
}
QgsLayoutPoint QgsLayoutItem::applyDataDefinedPosition( const QgsLayoutPoint &position )
{
if ( !mLayout )

View File

@ -22,6 +22,7 @@
#include "qgslayoutsize.h"
#include "qgslayoutpoint.h"
#include "qgsrendercontext.h"
#include "qgslayoutundocommand.h"
#include <QGraphicsRectItem>
class QgsLayout;
@ -33,7 +34,7 @@ class QPainter;
* \brief Base class for graphical items within a QgsLayout.
* \since QGIS 3.0
*/
class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectItem
class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectItem, public QgsLayoutUndoObjectInterface
{
#ifdef SIP_RUN
#include <qgslayoutitemshape.h>
@ -227,6 +228,8 @@ class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectIt
*/
virtual bool readXml( const QDomElement &itemElement, const QDomDocument &document, const QgsReadWriteContext &context );
QgsAbstractLayoutUndoCommand *createCommand( const QString &text, int id, QUndoCommand *parent = nullptr ) override SIP_FACTORY;
public slots:
/**

View File

@ -19,6 +19,7 @@
#include "qgslayoututils.h"
#include "qgspagesizeregistry.h"
#include "qgssymbollayerutils.h"
#include "qgslayoutitemundocommand.h"
#include <QPainter>
#include <QStyleOptionGraphicsItem>
@ -42,6 +43,12 @@ QgsLayoutItemPage::QgsLayoutItemPage( QgsLayout *layout )
mGrid->setParentItem( this );
}
QgsLayoutItemPage *QgsLayoutItemPage::create( QgsLayout *layout, const QVariantMap &settings )
{
Q_UNUSED( settings );
return new QgsLayoutItemPage( layout );
}
void QgsLayoutItemPage::setPageSize( const QgsLayoutSize &size )
{
attemptResize( size );
@ -119,6 +126,37 @@ void QgsLayoutItemPage::attemptResize( const QgsLayoutSize &size )
mLayout->guides().update();
}
///@cond PRIVATE
class QgsLayoutItemPageUndoCommand: public QgsLayoutItemUndoCommand
{
public:
QgsLayoutItemPageUndoCommand( QgsLayoutItemPage *page, const QString &text, int id = 0, QUndoCommand *parent SIP_TRANSFERTHIS = nullptr )
: QgsLayoutItemUndoCommand( page, text, id, parent )
{}
void restoreState( QDomDocument &stateDoc ) override
{
QgsLayoutItemUndoCommand::restoreState( stateDoc );
layout()->pageCollection()->reflow();
}
protected:
QgsLayoutItem *recreateItem( int, QgsLayout *layout ) override
{
QgsLayoutItemPage *page = new QgsLayoutItemPage( layout );
layout->pageCollection()->addPage( page );
return page;
}
};
///@endcond
QgsAbstractLayoutUndoCommand *QgsLayoutItemPage::createCommand( const QString &text, int id, QUndoCommand *parent )
{
return new QgsLayoutItemPageUndoCommand( this, text, id, parent );
}
void QgsLayoutItemPage::redraw()
{
QgsLayoutItem::redraw();

View File

@ -69,6 +69,15 @@ class CORE_EXPORT QgsLayoutItemPage : public QgsLayoutItem
* Constructor for QgsLayoutItemPage, with the specified parent \a layout.
*/
explicit QgsLayoutItemPage( QgsLayout *layout SIP_TRANSFERTHIS );
/**
* Returns a new page item for the specified \a layout.
*
* The caller takes responsibility for deleting the returned object.
*/
static QgsLayoutItemPage *create( QgsLayout *layout, const QVariantMap &settings ) SIP_FACTORY;
int type() const override { return QgsLayoutItemRegistry::LayoutPage; }
QString stringType() const override { return QStringLiteral( "ItemPaper" ); }
@ -110,6 +119,8 @@ class CORE_EXPORT QgsLayoutItemPage : public QgsLayoutItem
void attemptResize( const QgsLayoutSize &size ) override;
QgsAbstractLayoutUndoCommand *createCommand( const QString &text, int id, QUndoCommand *parent = nullptr ) override SIP_FACTORY;
public slots:
void redraw() override;

View File

@ -16,6 +16,7 @@
#include "qgslayoutitemregistry.h"
#include "qgslayoutitemshape.h"
#include "qgslayoutitempage.h"
#include "qgsgloweffect.h"
#include "qgseffectstack.h"
#include <QPainter>
@ -42,7 +43,7 @@ bool QgsLayoutItemRegistry::populate()
};
addLayoutItemType( new QgsLayoutItemMetadata( 101, QStringLiteral( "temp type" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddLabel.svg" ) ), createTemporaryItem ) );
addLayoutItemType( new QgsLayoutItemMetadata( LayoutPage, QStringLiteral( "Page" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionFileNew.svg" ) ), nullptr ) );
addLayoutItemType( new QgsLayoutItemMetadata( LayoutPage, QStringLiteral( "Page" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionFileNew.svg" ) ), QgsLayoutItemPage::create ) );
addLayoutItemType( new QgsLayoutItemMetadata( LayoutRectangle, QStringLiteral( "Rectangle" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddBasicRectangle.svg" ) ), QgsLayoutItemRectangularShape::create ) );
addLayoutItemType( new QgsLayoutItemMetadata( LayoutEllipse, QStringLiteral( "Ellipse" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddBasicCircle.svg" ) ), QgsLayoutItemEllipseShape::create ) );

View File

@ -0,0 +1,123 @@
/***************************************************************************
qgslayoutitemundocommand.cpp
------------------------
begin : July 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 "qgslayoutitemundocommand.h"
#include "qgslayoutitem.h"
#include "qgsreadwritecontext.h"
#include "qgslayout.h"
#include "qgsproject.h"
///@cond PRIVATE
QgsLayoutItemUndoCommand::QgsLayoutItemUndoCommand( QgsLayoutItem *item, const QString &text, int id, QUndoCommand *parent )
: QgsAbstractLayoutUndoCommand( text, id, parent )
, mItemUuid( item->uuid() )
, mLayout( item->layout() )
, mItemType( item->type() )
{
}
bool QgsLayoutItemUndoCommand::mergeWith( const QUndoCommand *command )
{
if ( command->id() == 0 )
return false;
const QgsLayoutItemUndoCommand *c = dynamic_cast<const QgsLayoutItemUndoCommand *>( command );
if ( !c )
{
return false;
}
setAfterState( c->afterState() );
return true;
}
void QgsLayoutItemUndoCommand::saveState( QDomDocument &stateDoc ) const
{
stateDoc.clear();
QDomElement documentElement = stateDoc.createElement( QStringLiteral( "ItemState" ) );
QgsLayoutItem *item = mLayout->itemByUuid( mItemUuid );
Q_ASSERT_X( item, "QgsLayoutItemUndoCommand::saveState", "could not retrieve item for saving state" );
item->writeXml( documentElement, stateDoc, QgsReadWriteContext() );
stateDoc.appendChild( documentElement );
}
void QgsLayoutItemUndoCommand::restoreState( QDomDocument &stateDoc )
{
// find item by uuid...
QgsLayoutItem *item = mLayout->itemByUuid( mItemUuid );
if ( !item )
{
// uh oh - it's been deleted! we need to create a new instance
item = recreateItem( mItemType, mLayout );
}
item->readXml( stateDoc.documentElement().firstChild().toElement(), stateDoc, QgsReadWriteContext() );
mLayout->project()->setDirty( true );
}
QgsLayoutItem *QgsLayoutItemUndoCommand::recreateItem( int itemType, QgsLayout *layout )
{
QgsLayoutItem *item = QgsApplication::layoutItemRegistry()->createItem( itemType, layout );
mLayout->addLayoutItem( item );
return item;
}
QString QgsLayoutItemUndoCommand::itemUuid() const
{
return mItemUuid;
}
QgsLayout *QgsLayoutItemUndoCommand::layout() const
{
return mLayout;
}
//
// QgsLayoutItemDeleteUndoCommand
//
QgsLayoutItemDeleteUndoCommand::QgsLayoutItemDeleteUndoCommand( QgsLayoutItem *item, const QString &text, int id, QUndoCommand *parent )
: QgsLayoutItemUndoCommand( item, text, id, parent )
{
saveBeforeState();
}
bool QgsLayoutItemDeleteUndoCommand::mergeWith( const QUndoCommand * )
{
return false;
}
void QgsLayoutItemDeleteUndoCommand::redo()
{
if ( mFirstRun )
{
mFirstRun = false;
return;
}
QgsLayoutItem *item = layout()->itemByUuid( itemUuid() );
Q_ASSERT_X( item, "QgsLayoutItemDeleteUndoCommand::redo", "could not find item to re-delete!" );
layout()->removeItem( item );
item->deleteLater();
}
///@endcond

View File

@ -0,0 +1,112 @@
/***************************************************************************
qgslayoutitemundocommand.h
----------------------
begin : July 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 QGSLAYOUTITEMUNDOCOMMAND_H
#define QGSLAYOUTITEMUNDOCOMMAND_H
#include "qgslayoutundocommand.h"
#include "qgis_core.h"
class QgsLayout;
class QgsLayoutItem;
SIP_NO_FILE
///@cond PRIVATE
/**
* \ingroup core
* An undo command subclass for layout item undo commands.
*
* QgsLayoutItemUndoCommand is a specific layout undo command which is
* designed for use with QgsLayoutItems. It automatically handles
* recreating a deleted item when the undo stack rolls back past
* the item deletion command.
*
* \since QGIS 3.0
*/
class CORE_EXPORT QgsLayoutItemUndoCommand: public QgsAbstractLayoutUndoCommand
{
public:
/**
* Constructor for QgsLayoutItemUndoCommand.
* \param item associated layout item
* \param text undo command descriptive text
* \param id optional undo command id, used for automatic command merging
* \param parent command
*/
QgsLayoutItemUndoCommand( QgsLayoutItem *item, const QString &text, int id = 0, QUndoCommand *parent SIP_TRANSFERTHIS = nullptr );
bool mergeWith( const QUndoCommand *command ) override;
/**
* Returns the layout associated with this command.
*/
QgsLayout *layout() const;
/**
* Returns the associated item's UUID, which uniquely identifies the item
* within the layout.
*/
QString itemUuid() const;
protected:
void saveState( QDomDocument &stateDoc ) const override;
void restoreState( QDomDocument &stateDoc ) override;
virtual QgsLayoutItem *recreateItem( int itemType, QgsLayout *layout ) SIP_FACTORY;
private:
QString mItemUuid;
QgsLayout *mLayout = nullptr;
int mItemType = 0;
};
/**
* \ingroup core
* An undo command subclass for layout item deletion undo commands.
*
* QgsLayoutItemDeleteUndoCommand is a specific layout undo command which handles
* layout item deletion. When applied (e.g. as a result of a 'redo' action),
* the associated layout item is deleted and removed from the layout.
*
* \since QGIS 3.0
*/
class CORE_EXPORT QgsLayoutItemDeleteUndoCommand: public QgsLayoutItemUndoCommand
{
public:
/**
* Constructor for QgsLayoutItemDeleteUndoCommand.
* \param item associated layout item
* \param text undo command descriptive text
* \param id optional undo command id, used for automatic command merging
* \param parent command
*/
QgsLayoutItemDeleteUndoCommand( QgsLayoutItem *item, const QString &text, int id = 0, QUndoCommand *parent SIP_TRANSFERTHIS = nullptr );
bool mergeWith( const QUndoCommand *command ) override;
void redo() override;
};
///@endcond
#endif

View File

@ -16,10 +16,15 @@
#include "qgslayoutpagecollection.h"
#include "qgslayout.h"
#include "qgsreadwritecontext.h"
#include "qgsproject.h"
#include "qgslayoutitemundocommand.h"
#include "qgssymbollayerutils.h"
QgsLayoutPageCollection::QgsLayoutPageCollection( QgsLayout *layout )
: QObject( layout )
, mLayout( layout )
, mGuideCollection( new QgsLayoutGuideCollection( layout, this ) )
{
createDefaultPageStyleSymbol();
}
@ -135,6 +140,83 @@ double QgsLayoutPageCollection::pageShadowWidth() const
return spaceBetweenPages() / 2;
}
bool QgsLayoutPageCollection::writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const
{
QDomElement element = document.createElement( QStringLiteral( "PageCollection" ) );
QDomElement pageStyleElem = QgsSymbolLayerUtils::saveSymbol( QString(), mPageStyleSymbol.get(), document, context );
element.appendChild( pageStyleElem );
for ( const QgsLayoutItemPage *page : mPages )
{
page->writeXml( element, document, context );
}
mGuideCollection->writeXml( element, document, context );
parentElement.appendChild( element );
return true;
}
bool QgsLayoutPageCollection::readXml( const QDomElement &e, const QDomDocument &document, const QgsReadWriteContext &context )
{
QDomElement element = e;
if ( element.nodeName() != QStringLiteral( "PageCollection" ) )
{
element = element.firstChildElement( QStringLiteral( "PageCollection" ) );
}
if ( element.nodeName() != QStringLiteral( "PageCollection" ) )
{
return false;
}
mBlockUndoCommands = true;
int i = 0;
for ( QgsLayoutItemPage *page : qgsAsConst( mPages ) )
{
emit pageAboutToBeRemoved( i );
mLayout->removeItem( page );
page->deleteLater();
++i;
}
mPages.clear();
QDomElement pageStyleSymbolElem = element.firstChildElement( QStringLiteral( "symbol" ) );
if ( !pageStyleSymbolElem.isNull() )
{
mPageStyleSymbol.reset( QgsSymbolLayerUtils::loadSymbol<QgsFillSymbol>( pageStyleSymbolElem, context ) );
}
QDomNodeList pageList = element.elementsByTagName( QStringLiteral( "LayoutItem" ) );
for ( int i = 0; i < pageList.size(); ++i )
{
QDomElement pageElement = pageList.at( i ).toElement();
std::unique_ptr< QgsLayoutItemPage > page( new QgsLayoutItemPage( mLayout ) );
page->readXml( pageElement, document, context );
mPages.append( page.get() );
mLayout->addItem( page.release() );
}
reflow();
mGuideCollection->readXml( element, document, context );
mBlockUndoCommands = false;
return true;
}
QgsLayoutGuideCollection &QgsLayoutPageCollection::guides()
{
return *mGuideCollection;
}
const QgsLayoutGuideCollection &QgsLayoutPageCollection::guides() const
{
return *mGuideCollection;
}
void QgsLayoutPageCollection::redraw()
{
Q_FOREACH ( QgsLayoutItemPage *page, mPages )
@ -143,7 +225,7 @@ void QgsLayoutPageCollection::redraw()
}
}
QgsLayout *QgsLayoutPageCollection::layout() const
QgsLayout *QgsLayoutPageCollection::layout()
{
return mLayout;
}
@ -194,13 +276,20 @@ QList<int> QgsLayoutPageCollection::visiblePageNumbers( QRectF region ) const
void QgsLayoutPageCollection::addPage( QgsLayoutItemPage *page )
{
if ( !mBlockUndoCommands )
mLayout->undoStack()->beginCommand( this, tr( "Add page" ) );
mPages.append( page );
mLayout->addItem( page );
reflow();
if ( !mBlockUndoCommands )
mLayout->undoStack()->endCommand();
}
void QgsLayoutPageCollection::insertPage( QgsLayoutItemPage *page, int beforePage )
{
if ( !mBlockUndoCommands )
mLayout->undoStack()->beginCommand( this, tr( "Add page" ) );
if ( beforePage < 0 )
beforePage = 0;
@ -214,6 +303,8 @@ void QgsLayoutPageCollection::insertPage( QgsLayoutItemPage *page, int beforePag
}
mLayout->addItem( page );
reflow();
if ( ! mBlockUndoCommands )
mLayout->undoStack()->endCommand();
}
void QgsLayoutPageCollection::deletePage( int pageNumber )
@ -221,11 +312,21 @@ void QgsLayoutPageCollection::deletePage( int pageNumber )
if ( pageNumber < 0 || pageNumber >= mPages.count() )
return;
if ( !mBlockUndoCommands )
{
mLayout->undoStack()->beginMacro( tr( "Remove page" ) );
mLayout->undoStack()->beginCommand( this, tr( "Remove page" ) );
}
emit pageAboutToBeRemoved( pageNumber );
QgsLayoutItemPage *page = mPages.takeAt( pageNumber );
mLayout->removeItem( page );
page->deleteLater();
reflow();
if ( ! mBlockUndoCommands )
{
mLayout->undoStack()->endCommand();
mLayout->undoStack()->endMacro();
}
}
void QgsLayoutPageCollection::deletePage( QgsLayoutItemPage *page )
@ -233,10 +334,26 @@ void QgsLayoutPageCollection::deletePage( QgsLayoutItemPage *page )
if ( !mPages.contains( page ) )
return;
if ( !mBlockUndoCommands )
{
mLayout->undoStack()->beginMacro( tr( "Remove page" ) );
mLayout->undoStack()->beginCommand( this, tr( "Remove page" ) );
}
emit pageAboutToBeRemoved( mPages.indexOf( page ) );
mPages.removeAll( page );
page->deleteLater();
reflow();
if ( !mBlockUndoCommands )
{
mLayout->undoStack()->endCommand();
mLayout->undoStack()->endMacro();
}
}
QgsLayoutItemPage *QgsLayoutPageCollection::takePage( QgsLayoutItemPage *page )
{
mPages.removeAll( page );
return page;
}
void QgsLayoutPageCollection::createDefaultPageStyleSymbol()

View File

@ -21,10 +21,12 @@
#include "qgis_sip.h"
#include "qgssymbol.h"
#include "qgslayoutitempage.h"
#include "qgslayoutserializableobject.h"
#include <QObject>
#include <memory>
class QgsLayout;
class QgsLayoutGuideCollection;
/**
* \ingroup core
@ -32,7 +34,7 @@ class QgsLayout;
* \brief A manager for a collection of pages in a layout.
* \since QGIS 3.0
*/
class CORE_EXPORT QgsLayoutPageCollection : public QObject
class CORE_EXPORT QgsLayoutPageCollection : public QObject, public QgsLayoutSerializableObject
{
Q_OBJECT
@ -46,10 +48,8 @@ class CORE_EXPORT QgsLayoutPageCollection : public QObject
~QgsLayoutPageCollection();
/**
* Returns the layout this collection belongs to.
*/
QgsLayout *layout() const;
QString stringType() const override { return QStringLiteral( "LayoutPageCollection" ); }
QgsLayout *layout() override;
/**
* Returns a list of pages in the collection.
@ -141,6 +141,11 @@ class CORE_EXPORT QgsLayoutPageCollection : public QObject
*/
void deletePage( QgsLayoutItemPage *page );
/**
* Takes a \a page from the collection, returning ownership of the page to the caller.
*/
QgsLayoutItemPage *takePage( QgsLayoutItemPage *page ) SIP_TRANSFERBACK;
/**
* Sets the \a symbol to use for drawing pages in the collection.
*
@ -211,6 +216,28 @@ class CORE_EXPORT QgsLayoutPageCollection : public QObject
*/
double pageShadowWidth() const;
/**
* Stores the collection's state in a DOM element. The \a parentElement should refer to the parent layout's DOM element.
* \see readXml()
*/
bool writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const override;
/**
* Sets the collection's state from a DOM element. collectionElement is the DOM node corresponding to the collection.
* \see writeXml()
*/
bool readXml( const QDomElement &collectionElement, const QDomDocument &document, const QgsReadWriteContext &context ) override;
/**
* Returns a reference to the collection's guide collection, which manages page snap guides.
*/
QgsLayoutGuideCollection &guides();
/**
* Returns a reference to the collection's guide collection, which manages page snap guides.
*/
SIP_SKIP const QgsLayoutGuideCollection &guides() const;
public slots:
/**
@ -237,12 +264,18 @@ class CORE_EXPORT QgsLayoutPageCollection : public QObject
QgsLayout *mLayout = nullptr;
std::unique_ptr< QgsLayoutGuideCollection > mGuideCollection;
//! Symbol for drawing pages
std::unique_ptr< QgsFillSymbol > mPageStyleSymbol;
QList< QgsLayoutItemPage * > mPages;
bool mBlockUndoCommands = false;
void createDefaultPageStyleSymbol();
friend class QgsLayoutPageCollectionUndoCommand;
};
#endif //QGSLAYOUTPAGECOLLECTION_H

View File

@ -0,0 +1,81 @@
/***************************************************************************
qgslayoutserializableobject.cpp
-------------------------------
begin : July 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 "qgslayoutserializableobject.h"
#include "qgsreadwritecontext.h"
#include "qgslayout.h"
#include "qgsproject.h"
///@cond PRIVATE
class QgsLayoutSerializableObjectUndoCommand: public QgsAbstractLayoutUndoCommand
{
public:
QgsLayoutSerializableObjectUndoCommand( QgsLayoutSerializableObject *object, const QString &text, int id, QUndoCommand *parent SIP_TRANSFERTHIS = nullptr )
: QgsAbstractLayoutUndoCommand( text, id, parent )
, mObject( object )
{}
bool mergeWith( const QUndoCommand *command ) override
{
if ( command->id() == 0 )
return false;
const QgsLayoutSerializableObjectUndoCommand *c = dynamic_cast<const QgsLayoutSerializableObjectUndoCommand *>( command );
if ( !c )
{
return false;
}
if ( mObject->stringType() != c->mObject->stringType() )
return false;
setAfterState( c->afterState() );
return true;
}
protected:
void saveState( QDomDocument &stateDoc ) const override
{
stateDoc.clear();
QDomElement documentElement = stateDoc.createElement( QStringLiteral( "UndoState" ) );
mObject->writeXml( documentElement, stateDoc, QgsReadWriteContext() );
stateDoc.appendChild( documentElement );
}
void restoreState( QDomDocument &stateDoc ) override
{
if ( !mObject )
{
return;
}
mObject->readXml( stateDoc.documentElement().firstChild().toElement(), stateDoc, QgsReadWriteContext() );
mObject->layout()->project()->setDirty( true );
}
private:
QgsLayoutSerializableObject *mObject = nullptr;
};
///@endcond
QgsAbstractLayoutUndoCommand *QgsLayoutSerializableObject::createCommand( const QString &text, int id, QUndoCommand *parent )
{
return new QgsLayoutSerializableObjectUndoCommand( this, text, id, parent );
}

View File

@ -0,0 +1,73 @@
/***************************************************************************
qgslayoutserializableobject.h
-----------------------------
begin : July 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 QGSLAYOUTSERIALIZABLEOBJECT_H
#define QGSLAYOUTSERIALIZABLEOBJECT_H
#include "qgis.h"
#include "qgis_core.h"
#include "qgslayoutundocommand.h"
class QDomElement;
class QDomDocument;
class QgsReadWriteContext;
class QgsAbstractLayoutUndoCommand;
/**
* \ingroup core
* An interface for layout objects which can be stored and read from DOM elements.
* \since QGIS 3.0
*/
class CORE_EXPORT QgsLayoutSerializableObject : public QgsLayoutUndoObjectInterface
{
public:
virtual ~QgsLayoutSerializableObject() = default;
/**
* Return the object type as a string.
*
* This string must be a unique, single word, character only representation of the item type, eg "LayoutScaleBar"
*/
virtual QString stringType() const = 0;
/**
* Returns the layout the object belongs to.
*/
virtual QgsLayout *layout() = 0;
/**
* Stores the objects's state in a DOM element. The \a parentElement should refer to the parent layout's DOM element.
* \see readXml()
*/
virtual bool writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const = 0;
/**
* Sets the objects's state from a DOM element. \a element is the DOM node corresponding to the object.
* \see writeXml()
*/
virtual bool readXml( const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context ) = 0;
QgsAbstractLayoutUndoCommand *createCommand( const QString &text, int id, QUndoCommand *parent = nullptr ) override SIP_FACTORY;
private:
friend class QgsLayoutSerializableObjectUndoCommand;
};
#endif // QGSLAYOUTSERIALIZABLEOBJECT_H

View File

@ -16,12 +16,34 @@
#include "qgslayoutsnapper.h"
#include "qgslayout.h"
#include "qgsreadwritecontext.h"
#include "qgsproject.h"
QgsLayoutSnapper::QgsLayoutSnapper( QgsLayout *layout )
: mLayout( layout )
{
}
QgsLayout *QgsLayoutSnapper::layout()
{
return mLayout;
}
void QgsLayoutSnapper::setSnapTolerance( const int snapTolerance )
{
mTolerance = snapTolerance;
}
void QgsLayoutSnapper::setSnapToGrid( bool enabled )
{
mSnapToGrid = enabled;
}
void QgsLayoutSnapper::setSnapToGuides( bool enabled )
{
mSnapToGuides = enabled;
}
QPointF QgsLayoutSnapper::snapPoint( QPointF point, double scaleFactor, bool &snapped ) const
{
snapped = false;
@ -146,3 +168,36 @@ double QgsLayoutSnapper::snapPointToGuides( double original, QgsLayoutGuide::Ori
return original;
}
}
bool QgsLayoutSnapper::writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext & ) const
{
QDomElement element = document.createElement( QStringLiteral( "Snapper" ) );
element.setAttribute( QStringLiteral( "tolerance" ), mTolerance );
element.setAttribute( QStringLiteral( "snapToGrid" ), mSnapToGrid );
element.setAttribute( QStringLiteral( "snapToGuides" ), mSnapToGuides );
parentElement.appendChild( element );
return true;
}
bool QgsLayoutSnapper::readXml( const QDomElement &e, const QDomDocument &, const QgsReadWriteContext & )
{
QDomElement element = e;
if ( element.nodeName() != QStringLiteral( "Snapper" ) )
{
element = element.firstChildElement( QStringLiteral( "Snapper" ) );
}
if ( element.nodeName() != QStringLiteral( "Snapper" ) )
{
return false;
}
mTolerance = element.attribute( QStringLiteral( "tolerance" ), QStringLiteral( "5" ) ).toInt();
mSnapToGrid = element.attribute( QStringLiteral( "snapToGrid" ), QStringLiteral( "0" ) ) != QLatin1String( "0" );
mSnapToGuides = element.attribute( QStringLiteral( "snapToGuides" ), QStringLiteral( "0" ) ) != QLatin1String( "0" );
return true;
}

View File

@ -20,9 +20,11 @@
#include "qgslayoutmeasurement.h"
#include "qgslayoutpoint.h"
#include "qgslayoutguidecollection.h"
#include "qgslayoutserializableobject.h"
#include <QPen>
class QgsLayout;
class QgsReadWriteContext;
/**
* \ingroup core
@ -31,7 +33,7 @@ class QgsLayout;
* snapping points to the nearest grid coordinate/snap line when possible.
* \since QGIS 3.0
*/
class CORE_EXPORT QgsLayoutSnapper
class CORE_EXPORT QgsLayoutSnapper: public QgsLayoutSerializableObject
{
public:
@ -41,11 +43,14 @@ class CORE_EXPORT QgsLayoutSnapper
*/
QgsLayoutSnapper( QgsLayout *layout );
QString stringType() const override { return QStringLiteral( "LayoutSnapper" ); }
QgsLayout *layout() override;
/**
* Sets the snap \a tolerance (in pixels) to use when snapping.
* \see snapTolerance()
*/
void setSnapTolerance( const int snapTolerance ) { mTolerance = snapTolerance; }
void setSnapTolerance( const int snapTolerance );
/**
* Returns the snap tolerance (in pixels) to use when snapping.
@ -63,7 +68,7 @@ class CORE_EXPORT QgsLayoutSnapper
* Sets whether snapping to grid is \a enabled.
* \see snapToGrid()
*/
void setSnapToGrid( bool enabled ) { mSnapToGrid = enabled; }
void setSnapToGrid( bool enabled );
/**
* Returns true if snapping to guides is enabled.
@ -75,7 +80,7 @@ class CORE_EXPORT QgsLayoutSnapper
* Sets whether snapping to guides is \a enabled.
* \see snapToGuides()
*/
void setSnapToGuides( bool enabled ) { mSnapToGuides = enabled; }
void setSnapToGuides( bool enabled );
/**
* Snaps a layout coordinate \a point. If \a point was snapped, \a snapped will be set to true.
@ -115,14 +120,36 @@ class CORE_EXPORT QgsLayoutSnapper
*/
double snapPointToGuides( double original, QgsLayoutGuide::Orientation orientation, double scaleFactor, bool &snapped SIP_OUT ) const;
/**
* Stores the snapper's state in a DOM element. The \a parentElement should refer to the parent layout's DOM element.
* \see readXml()
*/
bool writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const override;
/**
* Sets the snapper's state from a DOM element. snapperElement is the DOM node corresponding to the snapper.
* \see writeXml()
*/
bool readXml( const QDomElement &gridElement, const QDomDocument &document, const QgsReadWriteContext &context ) override;
private:
// Used for 'collapsing' undo commands
enum UndoCommand
{
UndoTolerance = 1,
UndoSnapToGrid,
UndoSnapToGuides,
};
QgsLayout *mLayout = nullptr;
int mTolerance = 5;
bool mSnapToGrid = false;
bool mSnapToGuides = true;
friend class QgsLayoutSnapperUndoCommand;
};
#endif //QGSLAYOUTSNAPPER_H

View File

@ -0,0 +1,59 @@
/***************************************************************************
qgslayoutundocommand.cpp
------------------------
begin : July 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 "qgslayoutundocommand.h"
QgsAbstractLayoutUndoCommand::QgsAbstractLayoutUndoCommand( const QString &text, int id, QUndoCommand *parent )
: QUndoCommand( text, parent )
, mId( id )
{}
void QgsAbstractLayoutUndoCommand::undo()
{
restoreState( mBeforeState );
}
void QgsAbstractLayoutUndoCommand::redo()
{
if ( mFirstRun )
{
mFirstRun = false;
return;
}
restoreState( mAfterState );
}
void QgsAbstractLayoutUndoCommand::saveBeforeState()
{
saveState( mBeforeState );
}
void QgsAbstractLayoutUndoCommand::saveAfterState()
{
saveState( mAfterState );
}
bool QgsAbstractLayoutUndoCommand::containsChange() const
{
return !( mBeforeState.isNull() || mAfterState.isNull() || mBeforeState.toString() == mAfterState.toString() );
}
void QgsAbstractLayoutUndoCommand::setAfterState( const QDomDocument &stateDoc )
{
mAfterState = stateDoc;
}

View File

@ -0,0 +1,142 @@
/***************************************************************************
qgslayoutundocommand.h
----------------------
begin : July 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 QGSLAYOUTUNDOCOMMAND_H
#define QGSLAYOUTUNDOCOMMAND_H
#include <QUndoCommand>
#include "qgis.h"
#include <QDomDocument>
#include "qgis_core.h"
class QgsLayout;
/**
* \ingroup core
* Base class for commands to undo/redo layout and layout object changes.
* \since QGIS 3.0
*/
class CORE_EXPORT QgsAbstractLayoutUndoCommand: public QUndoCommand
{
public:
/**
* Constructor for QgsLayoutUndoCommand.
* The \a id argument can be used to specify an id number for the source event - this is used to determine whether QUndoCommand
* command compression can apply to the command.
*/
QgsAbstractLayoutUndoCommand( const QString &text, int id = 0, QUndoCommand *parent SIP_TRANSFERTHIS = nullptr );
void undo() override;
void redo() override;
int id() const override { return mId; }
/**
* Saves current layout state as before state.
* \see beforeState()
* \see saveAfterState()
*/
void saveBeforeState();
/**
* Saves current layout state as after state.
* \see afterState()
* \see saveBeforeState()
*/
void saveAfterState();
/**
* Returns the before state for the layout.
* \see saveBeforeState()
* \see afterState()
*/
QDomDocument beforeState() const { return mBeforeState.cloneNode().toDocument(); }
/**
* Returns the after state for the layout.
* \see saveAfterState()
* \see beforeState()
*/
QDomDocument afterState() const { return mAfterState.cloneNode().toDocument(); }
/**
* Returns true if both the before and after states are valid and different.
*/
virtual bool containsChange() const;
protected:
/**
* Saves the state of the object to the specified \a stateDoc.
*
* Subclasses must implement this to handle encapsulating their current state into a DOM document.
*
* \see restoreState()
*/
virtual void saveState( QDomDocument &stateDoc ) const = 0;
/**
* Restores the state of the object from the specified \a stateDoc.
*
* Subclasses must implement this to handle restoring their current state from the encapsulated state.
*
* \see saveState()
*/
virtual void restoreState( QDomDocument &stateDoc ) = 0;
/**
* Manually sets the after state for the command. Generally this should not be called directly.
*/
void setAfterState( const QDomDocument &stateDoc );
//! Flag to prevent the first redo() if the command is pushed to the undo stack
bool mFirstRun = true;
private:
//! XML that saves the state before executing the command
QDomDocument mBeforeState;
//! XML containing the state after executing the command
QDomDocument mAfterState;
//! Command id
int mId = 0;
};
/**
* \ingroup core
* Interface for layout objects which support undo/redo commands.
* \since QGIS 3.0
*/
class CORE_EXPORT QgsLayoutUndoObjectInterface
{
public:
/**
* Creates a new layout undo command with the specified \a text and \a parent.
*
* The \a id argument can be used to specify an id number for the source event - this is used to determine whether QUndoCommand
* command compression can apply to the command.
*/
virtual QgsAbstractLayoutUndoCommand *createCommand( const QString &text, int id = 0, QUndoCommand *parent = nullptr ) = 0 SIP_FACTORY;
};
#endif // QGSLAYOUTUNDOCOMMAND_H

View File

@ -0,0 +1,74 @@
/***************************************************************************
qgslayoutundostack.cpp
------------------------
begin : July 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 "qgslayoutundostack.h"
#include "qgslayout.h"
#include "qgsproject.h"
#include <QUndoStack>
QgsLayoutUndoStack::QgsLayoutUndoStack( QgsLayout *layout )
: mLayout( layout )
, mUndoStack( new QUndoStack( layout ) )
{
}
void QgsLayoutUndoStack::beginMacro( const QString &commandText )
{
mUndoStack->beginMacro( commandText );
}
void QgsLayoutUndoStack::endMacro()
{
mUndoStack->endMacro();
}
void QgsLayoutUndoStack::beginCommand( QgsLayoutUndoObjectInterface *object, const QString &commandText, int id )
{
if ( !object )
{
return;
}
mActiveCommand.reset( object->createCommand( commandText, id, nullptr ) );
mActiveCommand->saveBeforeState();
}
void QgsLayoutUndoStack::endCommand()
{
if ( !mActiveCommand )
return;
mActiveCommand->saveAfterState();
if ( mActiveCommand->containsChange() ) //protect against empty commands
{
mUndoStack->push( mActiveCommand.release() );
mLayout->project()->setDirty( true );
}
}
void QgsLayoutUndoStack::cancelCommand()
{
mActiveCommand.reset();
}
QUndoStack *QgsLayoutUndoStack::stack()
{
return mUndoStack.get();
}

View File

@ -0,0 +1,114 @@
/***************************************************************************
qgslayoutundostack.h
----------------------
begin : July 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 QGSLAYOUTUNDOSTACK_H
#define QGSLAYOUTUNDOSTACK_H
#include "qgis.h"
#include "qgis_core.h"
#include "qgslayoutundocommand.h"
#include <memory>
class QgsLayout;
class QUndoStack;
/**
* \ingroup core
* An undo stack for QgsLayouts.
* \since QGIS 3.0
*/
class CORE_EXPORT QgsLayoutUndoStack
{
public:
/**
* Constructor for QgsLayoutUndoStack, for the specified parent \a layout.
*/
QgsLayoutUndoStack( QgsLayout *layout );
/**
* Starts a macro command, with the given descriptive \a commandText.
*
* Any commands added to the stack (either via direct manipulation of
* stack() or via beginCommand()/endCommand() calls) between a
* beginMacro() and endMacro() block are collapsed into a single
* undo command, which will be applied or rolled back in a single step.
*
* \see endMacro()
*/
void beginMacro( const QString &commandText );
/**
* Ends a macro command. This must be called after beginMacro(), when
* all child undo commands which form part of the macro have been completed.
*
* Any commands added to the stack (either via direct manipulation of
* stack() or via beginCommand()/endCommand() calls) between a
* beginMacro() and endMacro() block are collapsed into a single
* undo command, which will be applied or rolled back in a single step.
*
* \see beginMacro()
*/
void endMacro();
/**
* Begins a new undo command for the specified \a object.
*
* This must be followed by a call to endCommand() or cancelCommand() after the desired changes
* have been made to \a object.
*
* The \a id argument can be used to specify an id number for the source event - this is used to determine whether QUndoCommand
* command compression can apply to the command.
*
* \see endCommand()
* \see cancelCommand()
*/
void beginCommand( QgsLayoutUndoObjectInterface *object, const QString &commandText, int id = 0 );
/**
* Saves final state of an object and pushes the active command to the undo history.
* \see beginCommand()
* \see cancelCommand()
*/
void endCommand();
/**
* Cancels the active command, discarding it without pushing to the undo history.
* \see endCommand()
* \see cancelCommand()
*/
void cancelCommand();
/**
* Returns a pointer to the internal QUndoStack.
*/
QUndoStack *stack();
private:
QgsLayout *mLayout = nullptr;
std::unique_ptr< QUndoStack > mUndoStack;
std::unique_ptr< QgsAbstractLayoutUndoCommand > mActiveCommand;
#ifdef SIP_RUN
QgsLayoutUndoStack( const QgsLayoutUndoStack &other );
#endif
};
#endif // QGSLAYOUTUNDOCOMMAND_H

View File

@ -624,13 +624,13 @@ void QgsLayoutRuler::mouseMoveEvent( QMouseEvent *event )
{
case Qt::Horizontal:
{
mDraggingGuide->setLayoutPosition( displayPos.x() );
mView->currentLayout()->guides().setGuideLayoutPosition( mDraggingGuide, displayPos.x() );
displayPos.setY( 0 );
break;
}
case Qt::Vertical:
{
mDraggingGuide->setLayoutPosition( displayPos.y() );
mView->currentLayout()->guides().setGuideLayoutPosition( mDraggingGuide, displayPos.y() );
displayPos.setX( 0 );
break;
}
@ -679,8 +679,11 @@ void QgsLayoutRuler::mousePressEvent( QMouseEvent *event )
if ( !mDraggingGuide )
{
// if no guide at the point, then we're creating one
mCreatingGuide = true;
createTemporaryGuideItem();
if ( mView->currentLayout()->pageCollection()->pageCount() > 0 )
{
mCreatingGuide = true;
createTemporaryGuideItem();
}
}
switch ( mOrientation )
{

View File

@ -62,6 +62,8 @@
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="mActionUndo"/>
<addaction name="mActionRedo"/>
</widget>
<widget class="QToolBar" name="mToolsToolbar">
<property name="windowTitle">
@ -134,7 +136,15 @@
<addaction name="mPanelsMenu"/>
<addaction name="mActionToggleFullScreen"/>
</widget>
<widget class="QMenu" name="menuEdit">
<property name="title">
<string>&amp;Edit</string>
</property>
<addaction name="mActionUndo"/>
<addaction name="mActionRedo"/>
</widget>
<addaction name="mLayoutMenu"/>
<addaction name="menuEdit"/>
<addaction name="mMenuView"/>
<addaction name="mItemMenu"/>
</widget>
@ -395,6 +405,37 @@
<string>Layout Properties…</string>
</property>
</action>
<action name="mActionUndo">
<property name="icon">
<iconset>
<normalon>:/images/themes/default/mActionUndo.svg</normalon>
</iconset>
</property>
<property name="text">
<string>&amp;Undo</string>
</property>
<property name="toolTip">
<string>Revert last change</string>
</property>
<property name="shortcut">
<string>Ctrl+Z</string>
</property>
</action>
<action name="mActionRedo">
<property name="icon">
<iconset resource="../../../images/images.qrc">
<normaloff>:/images/themes/default/mActionRedo.svg</normaloff>:/images/themes/default/mActionRedo.svg</iconset>
</property>
<property name="text">
<string>&amp;Redo</string>
</property>
<property name="toolTip">
<string>Restore last change</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+Z</string>
</property>
</action>
</widget>
<resources>
<include location="../../../images/images.qrc"/>

View File

@ -39,6 +39,8 @@ class TestQgsLayout: public QObject
void referenceMap();
void bounds();
void addItem();
void layoutItems();
void layoutItemByUuid();
private:
QString mReport;
@ -360,6 +362,61 @@ void TestQgsLayout::addItem()
QGSCOMPARENEAR( l.sceneRect().height(), 171, 0.001 );
}
void TestQgsLayout::layoutItems()
{
QgsProject p;
QgsLayout l( &p );
l.pageCollection()->deletePage( 0 );
QgsLayoutItemRectangularShape *shape1 = new QgsLayoutItemRectangularShape( &l );
l.addLayoutItem( shape1 );
QgsLayoutItemRectangularShape *shape2 = new QgsLayoutItemRectangularShape( &l );
l.addLayoutItem( shape2 );
QgsLayoutItemMap *map1 = new QgsLayoutItemMap( &l );
l.addLayoutItem( map1 );
QList< QgsLayoutItem * > items;
l.layoutItems( items );
QCOMPARE( items.count(), 3 );
QVERIFY( items.contains( shape1 ) );
QVERIFY( items.contains( shape2 ) );
QVERIFY( items.contains( map1 ) );
QList< QgsLayoutItemRectangularShape * > shapes;
l.layoutItems( shapes );
QCOMPARE( shapes.count(), 2 );
QVERIFY( shapes.contains( shape1 ) );
QVERIFY( shapes.contains( shape2 ) );
QList< QgsLayoutItemMap * > maps;
l.layoutItems( maps );
QCOMPARE( maps.count(), 1 );
QVERIFY( maps.contains( map1 ) );
}
void TestQgsLayout::layoutItemByUuid()
{
QgsProject p;
QgsLayout l( &p );
l.pageCollection()->deletePage( 0 );
QgsLayoutItemRectangularShape *shape1 = new QgsLayoutItemRectangularShape( &l );
l.addLayoutItem( shape1 );
QgsLayoutItemRectangularShape *shape2 = new QgsLayoutItemRectangularShape( &l );
l.addLayoutItem( shape2 );
QgsLayoutItemMap *map1 = new QgsLayoutItemMap( &l );
l.addLayoutItem( map1 );
QVERIFY( !l.itemByUuid( QStringLiteral( "xxx" ) ) );
QCOMPARE( l.itemByUuid( shape1->uuid() ), shape1 );
QCOMPARE( l.itemByUuid( shape2->uuid() ), shape2 );
QCOMPARE( l.itemByUuid( map1->uuid() ), map1 );
}
QGSTEST_MAIN( TestQgsLayout )
#include "testqgslayout.moc"

View File

@ -75,6 +75,7 @@ ADD_PYTHON_TEST(PyQgsJsonUtils test_qgsjsonutils.py)
ADD_PYTHON_TEST(PyQgsLayerMetadata test_qgslayermetadata.py)
ADD_PYTHON_TEST(PyQgsLayerTreeMapCanvasBridge test_qgslayertreemapcanvasbridge.py)
ADD_PYTHON_TEST(PyQgsLayerTree test_qgslayertree.py)
ADD_PYTHON_TEST(PyQgsLayout test_qgslayout.py)
ADD_PYTHON_TEST(PyQgsLayoutManager test_qgslayoutmanager.py)
ADD_PYTHON_TEST(PyQgsLayoutPageCollection test_qgslayoutpagecollection.py)
ADD_PYTHON_TEST(PyQgsLayoutView test_qgslayoutview.py)

View File

@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for QgsLayout
.. note:: 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.
"""
__author__ = 'Nyall Dawson'
__date__ = '18/07/2017'
__copyright__ = 'Copyright 2017, The QGIS Project'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'
import qgis # NOQA
import sip
from qgis.core import (QgsUnitTypes,
QgsLayout,
QgsLayoutItemPage,
QgsLayoutGuide,
QgsLayoutObject,
QgsProject,
QgsProperty,
QgsLayoutPageCollection,
QgsLayoutMeasurement,
QgsFillSymbol,
QgsReadWriteContext)
from qgis.PyQt.QtCore import Qt, QCoreApplication, QEvent, QPointF, QRectF
from qgis.PyQt.QtTest import QSignalSpy
from qgis.PyQt.QtXml import QDomDocument
from qgis.testing import start_app, unittest
start_app()
class TestQgsLayout(unittest.TestCase):
def testReadWriteXml(self):
p = QgsProject()
l = QgsLayout(p)
l.setName('my layout')
l.setUnits(QgsUnitTypes.LayoutInches)
collection = l.pageCollection()
# add a page
page = QgsLayoutItemPage(l)
page.setPageSize('A6')
collection.addPage(page)
grid = l.gridSettings()
grid.setResolution(QgsLayoutMeasurement(5, QgsUnitTypes.LayoutPoints))
g1 = QgsLayoutGuide(QgsLayoutGuide.Horizontal, QgsLayoutMeasurement(5, QgsUnitTypes.LayoutCentimeters),
l.pageCollection().page(0))
l.guides().addGuide(g1)
snapper = l.snapper()
snapper.setSnapTolerance(7)
doc = QDomDocument("testdoc")
elem = l.writeXml(doc, QgsReadWriteContext())
l2 = QgsLayout(p)
self.assertTrue(l2.readXml(elem, doc, QgsReadWriteContext()))
self.assertEqual(l2.name(), 'my layout')
self.assertEqual(l2.units(), QgsUnitTypes.LayoutInches)
collection2 = l2.pageCollection()
self.assertEqual(collection2.pageCount(), 1)
self.assertAlmostEqual(collection2.page(0).pageSize().width(), 105, 4)
self.assertEqual(collection2.page(0).pageSize().height(), 148)
self.assertEqual(l2.gridSettings().resolution().length(), 5.0)
self.assertEqual(l2.gridSettings().resolution().units(), QgsUnitTypes.LayoutPoints)
self.assertEqual(l2.guides().guidesOnPage(0)[0].orientation(), QgsLayoutGuide.Horizontal)
self.assertEqual(l2.guides().guidesOnPage(0)[0].position().length(), 5.0)
self.assertEqual(l2.guides().guidesOnPage(0)[0].position().units(), QgsUnitTypes.LayoutCentimeters)
self.assertEqual(l2.snapper().snapTolerance(), 7)
if __name__ == '__main__':
unittest.main()

View File

@ -20,9 +20,11 @@ from qgis.core import (QgsProject,
QgsLayoutMeasurement,
QgsUnitTypes,
QgsLayoutPoint,
QgsLayoutItemPage)
QgsLayoutItemPage,
QgsReadWriteContext)
from qgis.PyQt.QtGui import (QPen,
QColor)
from qgis.PyQt.QtXml import QDomDocument
from qgis.testing import start_app, unittest
@ -34,7 +36,7 @@ class TestQgsLayoutGridSettings(unittest.TestCase):
def testGettersSetters(self):
p = QgsProject()
l = QgsLayout(p)
s = QgsLayoutGridSettings()
s = QgsLayoutGridSettings(l)
s.setResolution(QgsLayoutMeasurement(5, QgsUnitTypes.LayoutPoints))
self.assertEqual(s.resolution().length(), 5.0)
self.assertEqual(s.resolution().units(), QgsUnitTypes.LayoutPoints)
@ -50,6 +52,74 @@ class TestQgsLayoutGridSettings(unittest.TestCase):
s.setStyle(QgsLayoutGridSettings.StyleDots)
self.assertEqual(s.style(), QgsLayoutGridSettings.StyleDots)
def testReadWriteXml(self):
p = QgsProject()
l = QgsLayout(p)
s = QgsLayoutGridSettings(l)
s.setResolution(QgsLayoutMeasurement(5, QgsUnitTypes.LayoutPoints))
s.setOffset(QgsLayoutPoint(6, 7, QgsUnitTypes.LayoutPixels))
doc = QDomDocument("testdoc")
elem = doc.createElement("test")
self.assertTrue(s.writeXml(elem, doc, QgsReadWriteContext()))
s2 = QgsLayoutGridSettings(l)
self.assertTrue(s2.readXml(elem.firstChildElement(), doc, QgsReadWriteContext()))
self.assertEqual(s2.resolution().length(), 5.0)
self.assertEqual(s2.resolution().units(), QgsUnitTypes.LayoutPoints)
self.assertEqual(s2.offset().x(), 6.0)
self.assertEqual(s2.offset().y(), 7.0)
self.assertEqual(s2.offset().units(), QgsUnitTypes.LayoutPixels)
def testUndoRedo(self):
p = QgsProject()
l = QgsLayout(p)
g = l.gridSettings()
g.setResolution(QgsLayoutMeasurement(15, QgsUnitTypes.LayoutPoints))
# these two commands should be 'collapsed'
g.setOffset(QgsLayoutPoint(555, 10, QgsUnitTypes.LayoutPoints))
g.setOffset(QgsLayoutPoint(5, 10, QgsUnitTypes.LayoutPoints))
# these two commands should be 'collapsed'
g.setResolution(QgsLayoutMeasurement(45, QgsUnitTypes.LayoutInches))
g.setResolution(QgsLayoutMeasurement(35, QgsUnitTypes.LayoutInches))
self.assertEqual(g.offset().x(), 5.0)
self.assertEqual(g.offset().y(), 10.0)
self.assertEqual(g.offset().units(), QgsUnitTypes.LayoutPoints)
self.assertEqual(g.resolution().length(), 35.0)
self.assertEqual(g.resolution().units(), QgsUnitTypes.LayoutInches)
l.undoStack().stack().undo()
self.assertEqual(g.offset().x(), 5.0)
self.assertEqual(g.offset().y(), 10.0)
self.assertEqual(g.offset().units(), QgsUnitTypes.LayoutPoints)
self.assertEqual(g.resolution().length(), 15.0)
self.assertEqual(g.resolution().units(), QgsUnitTypes.LayoutPoints)
l.undoStack().stack().undo()
self.assertEqual(g.offset().x(), 0.0)
self.assertEqual(g.offset().y(), 0.0)
self.assertEqual(g.offset().units(), QgsUnitTypes.LayoutMillimeters)
self.assertEqual(g.resolution().length(), 15.0)
self.assertEqual(g.resolution().units(), QgsUnitTypes.LayoutPoints)
l.undoStack().stack().redo()
self.assertEqual(g.offset().x(), 5.0)
self.assertEqual(g.offset().y(), 10.0)
self.assertEqual(g.offset().units(), QgsUnitTypes.LayoutPoints)
self.assertEqual(g.resolution().length(), 15.0)
self.assertEqual(g.resolution().units(), QgsUnitTypes.LayoutPoints)
l.undoStack().stack().redo()
self.assertEqual(g.offset().x(), 5.0)
self.assertEqual(g.offset().y(), 10.0)
self.assertEqual(g.offset().units(), QgsUnitTypes.LayoutPoints)
self.assertEqual(g.resolution().length(), 35.0)
self.assertEqual(g.resolution().units(), QgsUnitTypes.LayoutInches)
if __name__ == '__main__':
unittest.main()

View File

@ -19,7 +19,7 @@ from qgis.core import (QgsProject,
QgsLayoutGuide,
QgsLayoutMeasurement,
QgsUnitTypes,
QgsLayoutPoint,
QgsReadWriteContext,
QgsLayoutItemPage,
QgsLayoutGuideCollection,
QgsLayoutGuideProxyModel)
@ -27,6 +27,7 @@ from qgis.PyQt.QtCore import (QModelIndex)
from qgis.PyQt.QtGui import (QPen,
QColor)
from qgis.PyQt.QtTest import QSignalSpy
from qgis.PyQt.QtXml import QDomDocument
from qgis.testing import start_app, unittest
@ -315,6 +316,55 @@ class TestQgsLayoutGuide(unittest.TestCase):
self.assertTrue(g1.item().isVisible())
self.assertTrue(g2.item().isVisible())
def testReadWriteXml(self):
p = QgsProject()
l = QgsLayout(p)
l.initializeDefaults()
guides = l.guides()
# add some guides
g1 = QgsLayoutGuide(QgsLayoutGuide.Horizontal, QgsLayoutMeasurement(5, QgsUnitTypes.LayoutCentimeters), l.pageCollection().page(0))
guides.addGuide(g1)
g2 = QgsLayoutGuide(QgsLayoutGuide.Vertical, QgsLayoutMeasurement(6, QgsUnitTypes.LayoutInches), l.pageCollection().page(0))
guides.addGuide(g2)
guides.setVisible(False)
doc = QDomDocument("testdoc")
elem = doc.createElement("test")
self.assertTrue(guides.writeXml(elem, doc, QgsReadWriteContext()))
l2 = QgsLayout(p)
l2.initializeDefaults()
guides2 = l2.guides()
self.assertTrue(guides2.readXml(elem.firstChildElement(), doc, QgsReadWriteContext()))
guide_list = guides2.guidesOnPage(0)
self.assertEqual(len(guide_list), 2)
self.assertEqual(guide_list[0].orientation(), QgsLayoutGuide.Horizontal)
self.assertEqual(guide_list[0].position().length(), 5.0)
self.assertEqual(guide_list[0].position().units(), QgsUnitTypes.LayoutCentimeters)
self.assertEqual(guide_list[1].orientation(), QgsLayoutGuide.Vertical)
self.assertEqual(guide_list[1].position().length(), 6.0)
self.assertEqual(guide_list[1].position().units(), QgsUnitTypes.LayoutInches)
def testGuideLayoutPosition(self):
p = QgsProject()
l = QgsLayout(p)
l.initializeDefaults()
guides = l.guides()
# add some guides
g1 = QgsLayoutGuide(QgsLayoutGuide.Horizontal, QgsLayoutMeasurement(1, QgsUnitTypes.LayoutCentimeters), l.pageCollection().page(0))
guides.addGuide(g1)
# set position in layout units (mm)
guides.setGuideLayoutPosition(g1, 50)
self.assertEqual(g1.position().length(), 5.0)
self.assertEqual(g1.position().units(), QgsUnitTypes.LayoutCentimeters)
if __name__ == '__main__':
unittest.main()

View File

@ -24,9 +24,11 @@ from qgis.core import (QgsUnitTypes,
QgsProperty,
QgsLayoutPageCollection,
QgsSimpleFillSymbolLayer,
QgsFillSymbol)
QgsFillSymbol,
QgsReadWriteContext)
from qgis.PyQt.QtCore import Qt, QCoreApplication, QEvent, QPointF, QRectF
from qgis.PyQt.QtTest import QSignalSpy
from qgis.PyQt.QtXml import QDomDocument
from qgis.testing import start_app, unittest
@ -426,6 +428,130 @@ class TestQgsLayoutPageCollection(unittest.TestCase):
self.assertEqual(collection.visiblePages(QRectF(100, 310, 115, 615)), [page2])
self.assertEqual(collection.visiblePageNumbers(QRectF(100, 310, 115, 115)), [1])
def testTakePage(self):
p = QgsProject()
l = QgsLayout(p)
collection = l.pageCollection()
# add some pages
page = QgsLayoutItemPage(l)
page.setPageSize('A4')
collection.addPage(page)
page2 = QgsLayoutItemPage(l)
page2.setPageSize('A5')
collection.addPage(page2)
self.assertEqual(collection.pageCount(), 2)
self.assertFalse(collection.takePage(None))
self.assertEqual(collection.takePage(page), page)
self.assertFalse(sip.isdeleted(page))
self.assertEqual(collection.pageCount(), 1)
self.assertEqual(collection.pages(), [page2])
self.assertEqual(collection.page(0), page2)
self.assertEqual(collection.takePage(page2), page2)
self.assertFalse(sip.isdeleted(page2))
self.assertEqual(collection.pageCount(), 0)
self.assertEqual(collection.pages(), [])
self.assertFalse(collection.page(0))
def testReadWriteXml(self):
p = QgsProject()
l = QgsLayout(p)
collection = l.pageCollection()
fill = QgsSimpleFillSymbolLayer()
fill_symbol = QgsFillSymbol()
fill_symbol.changeSymbolLayer(0, fill)
fill.setColor(Qt.green)
fill.setStrokeColor(Qt.red)
fill.setStrokeWidth(6)
collection.setPageStyleSymbol(fill_symbol)
# add a page
page = QgsLayoutItemPage(l)
page.setPageSize('A4')
self.assertEqual(collection.pageNumber(page), -1)
collection.addPage(page)
# add a second page
page2 = QgsLayoutItemPage(l)
page2.setPageSize('A5')
collection.addPage(page2)
doc = QDomDocument("testdoc")
elem = doc.createElement("test")
self.assertTrue(collection.writeXml(elem, doc, QgsReadWriteContext()))
l2 = QgsLayout(p)
collection2 = l2.pageCollection()
self.assertTrue(collection2.readXml(elem.firstChildElement(), doc, QgsReadWriteContext()))
self.assertEqual(collection2.pageCount(), 2)
self.assertEqual(collection2.page(0).pageSize().width(), 210)
self.assertEqual(collection2.page(0).pageSize().height(), 297)
self.assertEqual(collection2.page(1).pageSize().width(), 148)
self.assertEqual(collection2.page(1).pageSize().height(), 210)
self.assertEqual(collection2.pageStyleSymbol().symbolLayer(0).color().name(), '#00ff00')
self.assertEqual(collection2.pageStyleSymbol().symbolLayer(0).strokeColor().name(), '#ff0000')
def testUndoRedo(self):
p = QgsProject()
l = QgsLayout(p)
collection = l.pageCollection()
# add a page
page = QgsLayoutItemPage(l)
page.setPageSize('A4')
collection.addPage(page)
self.assertEqual(collection.pageCount(), 1)
l.undoStack().stack().undo()
self.assertEqual(collection.pageCount(), 0)
l.undoStack().stack().redo()
self.assertEqual(collection.pageCount(), 1)
# make sure page is accessible
self.assertEqual(collection.page(0).pageSize().width(), 210)
# add a second page
page2 = QgsLayoutItemPage(l)
page2.setPageSize('A5')
collection.addPage(page2)
# delete page
collection.deletePage(collection.page(0))
self.assertEqual(collection.pageCount(), 1)
l.undoStack().stack().undo()
self.assertEqual(collection.pageCount(), 2)
# make sure pages are accessible
self.assertEqual(collection.page(0).pageSize().width(), 210)
self.assertEqual(collection.page(1).pageSize().width(), 148)
l.undoStack().stack().undo()
self.assertEqual(collection.pageCount(), 1)
l.undoStack().stack().undo()
self.assertEqual(collection.pageCount(), 0)
l.undoStack().stack().redo()
self.assertEqual(collection.pageCount(), 1)
self.assertEqual(collection.page(0).pageSize().width(), 210)
l.undoStack().stack().redo()
self.assertEqual(collection.pageCount(), 2)
self.assertEqual(collection.page(0).pageSize().width(), 210)
self.assertEqual(collection.page(1).pageSize().width(), 148)
l.undoStack().stack().redo()
self.assertEqual(collection.pageCount(), 1)
self.assertEqual(collection.page(0).pageSize().width(), 148)
if __name__ == '__main__':
unittest.main()

View File

@ -22,8 +22,10 @@ from qgis.core import (QgsProject,
QgsUnitTypes,
QgsLayoutPoint,
QgsLayoutItemPage,
QgsLayoutGuide)
QgsLayoutGuide,
QgsReadWriteContext)
from qgis.PyQt.QtCore import QPointF
from qgis.PyQt.QtXml import QDomDocument
from qgis.testing import start_app, unittest
@ -193,6 +195,41 @@ class TestQgsLayoutSnapper(unittest.TestCase):
self.assertTrue(snapped)
self.assertEqual(point, QPointF(0, 0.5))
def testReadWriteXml(self):
p = QgsProject()
l = QgsLayout(p)
l.initializeDefaults()
snapper = l.snapper()
snapper.setSnapToGrid(True)
snapper.setSnapTolerance(1)
snapper.setSnapToGuides(True)
doc = QDomDocument("testdoc")
elem = doc.createElement("test")
self.assertTrue(snapper.writeXml(elem, doc, QgsReadWriteContext()))
l2 = QgsLayout(p)
l2.initializeDefaults()
snapper2 = l2.snapper()
self.assertTrue(snapper2.readXml(elem.firstChildElement(), doc, QgsReadWriteContext()))
self.assertTrue(snapper2.snapToGrid())
self.assertEqual(snapper2.snapTolerance(), 1)
self.assertTrue(snapper2.snapToGuides())
snapper.setSnapToGrid(False)
snapper.setSnapTolerance(1)
snapper.setSnapToGuides(False)
doc = QDomDocument("testdoc")
elem = doc.createElement("test")
self.assertTrue(snapper.writeXml(elem, doc, QgsReadWriteContext()))
self.assertTrue(snapper2.readXml(elem.firstChildElement(), doc, QgsReadWriteContext()))
self.assertFalse(snapper2.snapToGrid())
self.assertFalse(snapper2.snapToGuides())
if __name__ == '__main__':
unittest.main()