Restore ability to save layouts to templates and add items from template

This commit is contained in:
Nyall Dawson 2017-12-04 14:30:19 +10:00
parent 59b6bf62ab
commit a4dea9935f
8 changed files with 306 additions and 1 deletions

View File

@ -417,6 +417,28 @@ class QgsLayout : QGraphicsScene, QgsExpressionContextGenerator, QgsLayoutUndoOb
:rtype: list of QgsLayoutMultiFrame
%End
bool saveAsTemplate( const QString &path, const QgsReadWriteContext &context ) const;
%Docstring
Saves the layout as a template at the given file ``path``.
Returns true if save was successful.
.. seealso:: loadFromTemplate()
:rtype: bool
%End
QList< QgsLayoutItem * > loadFromTemplate( const QDomDocument &document, const QgsReadWriteContext &context, bool clearExisting = true, bool *ok /Out/ = 0 );
%Docstring
Load a layout template ``document``.
By default this method will clear all items from the existing layout and real all layout
settings from the template. Setting ``clearExisting`` to false will only add new items
from the template, without overwriting the existing items or layout settings.
If ``ok`` is specified, it will be set to true if the load was successful.
Returns a list of loaded items.
:rtype: list of QgsLayoutItem
%End
QDomElement writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const;
%Docstring
Returns the layout's state encapsulated in a DOM element.

View File

@ -44,6 +44,7 @@
#include "qgslayoutmousehandles.h"
#include "qgslayoutmodel.h"
#include "qgslayoutitemslistview.h"
#include "qgsproject.h"
#include <QShortcut>
#include <QComboBox>
#include <QLineEdit>
@ -52,6 +53,8 @@
#include <QLabel>
#include <QUndoView>
#include <QTreeView>
#include <QFileDialog>
#include <QMessageBox>
#ifdef ENABLE_MODELTEST
#include "modeltest.h"
@ -299,6 +302,9 @@ QgsLayoutDesignerDialog::QgsLayoutDesignerDialog( QWidget *parent, Qt::WindowFla
mActionPreviewProtanope->setActionGroup( previewGroup );
mActionPreviewDeuteranope->setActionGroup( previewGroup );
connect( mActionSaveAsTemplate, &QAction::triggered, this, &QgsLayoutDesignerDialog::saveAsTemplate );
connect( mActionLoadFromTemplate, &QAction::triggered, this, &QgsLayoutDesignerDialog::addItemsFromTemplate );
connect( mActionZoomIn, &QAction::triggered, mView, &QgsLayoutView::zoomIn );
connect( mActionZoomOut, &QAction::triggered, mView, &QgsLayoutView::zoomOut );
connect( mActionZoomAll, &QAction::triggered, mView, &QgsLayoutView::zoomFull );
@ -1130,6 +1136,84 @@ void QgsLayoutDesignerDialog::undoRedoOccurredForItems( const QSet<QString> item
showItemOptions( focusItem );
}
void QgsLayoutDesignerDialog::saveAsTemplate()
{
//show file dialog
QgsSettings settings;
QString lastSaveDir = settings.value( QStringLiteral( "UI/lastComposerTemplateDir" ), QDir::homePath() ).toString();
#ifdef Q_OS_MAC
mQgis->activateWindow();
this->raise();
#endif
QString saveFileName = QFileDialog::getSaveFileName(
this,
tr( "Save template" ),
lastSaveDir,
tr( "Layout templates" ) + " (*.qpt *.QPT)" );
if ( saveFileName.isEmpty() )
return;
QFileInfo saveFileInfo( saveFileName );
//check if suffix has been added
if ( saveFileInfo.suffix().isEmpty() )
{
QString saveFileNameWithSuffix = saveFileName.append( ".qpt" );
saveFileInfo = QFileInfo( saveFileNameWithSuffix );
}
settings.setValue( QStringLiteral( "UI/lastComposerTemplateDir" ), saveFileInfo.absolutePath() );
QgsReadWriteContext context;
context.setPathResolver( QgsProject::instance()->pathResolver() );
if ( !currentLayout()->saveAsTemplate( saveFileName, context ) )
{
QMessageBox::warning( nullptr, tr( "Save template" ), tr( "Error creating template file." ) );
}
}
void QgsLayoutDesignerDialog::addItemsFromTemplate()
{
if ( !currentLayout() )
return;
QgsSettings settings;
QString openFileDir = settings.value( QStringLiteral( "UI/lastComposerTemplateDir" ), QDir::homePath() ).toString();
QString openFileString = QFileDialog::getOpenFileName( nullptr, tr( "Load template" ), openFileDir, tr( "Layout templates" ) + " (*.qpt *.QPT)" );
if ( openFileString.isEmpty() )
{
return; //canceled by the user
}
QFileInfo openFileInfo( openFileString );
settings.setValue( QStringLiteral( "UI/LastComposerTemplateDir" ), openFileInfo.absolutePath() );
QFile templateFile( openFileString );
if ( !templateFile.open( QIODevice::ReadOnly ) )
{
QMessageBox::warning( this, tr( "Load from template" ), tr( "Could not read template file." ) );
return;
}
QDomDocument templateDoc;
QgsReadWriteContext context;
context.setPathResolver( QgsProject::instance()->pathResolver() );
if ( templateDoc.setContent( &templateFile ) )
{
bool ok = false;
QList< QgsLayoutItem * > items = currentLayout()->loadFromTemplate( templateDoc, context, false, &ok );
if ( !ok )
{
QMessageBox::warning( this, tr( "Load from template" ), tr( "Could not read template file." ) );
return;
}
else
{
whileBlocking( currentLayout() )->deselectAll();
selectItems( items );
}
}
}
void QgsLayoutDesignerDialog::paste()
{
QPointF pt = mView->mapFromGlobal( QCursor::pos() );

View File

@ -258,6 +258,8 @@ class QgsLayoutDesignerDialog: public QMainWindow, private Ui::QgsLayoutDesigner
void statusMessageReceived( const QString &message );
void dockVisibilityChanged( bool visible );
void undoRedoOccurredForItems( const QSet< QString > itemUuids );
void saveAsTemplate();
void addItemsFromTemplate();
private:

View File

@ -470,6 +470,77 @@ QList<QgsLayoutMultiFrame *> QgsLayout::multiFrames() const
return mMultiFrames;
}
bool QgsLayout::saveAsTemplate( const QString &path, const QgsReadWriteContext &context ) const
{
QFile templateFile( path );
if ( !templateFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
{
return false;
}
QDomDocument saveDocument;
QDomElement elem = writeXml( saveDocument, context );
saveDocument.appendChild( elem );
if ( templateFile.write( saveDocument.toByteArray() ) == -1 )
return false;
return true;
}
QList< QgsLayoutItem * > QgsLayout::loadFromTemplate( const QDomDocument &document, const QgsReadWriteContext &context, bool clearExisting, bool *ok )
{
if ( ok )
*ok = false;
QList< QgsLayoutItem * > result;
if ( clearExisting )
{
clear();
}
QDomDocument doc = document;
// remove all uuid attributes since we don't want duplicates UUIDS
QDomNodeList composerItemsNodes = doc.elementsByTagName( QStringLiteral( "ComposerItem" ) );
for ( int i = 0; i < composerItemsNodes.count(); ++i )
{
QDomNode composerItemNode = composerItemsNodes.at( i );
if ( composerItemNode.isElement() )
{
composerItemNode.toElement().setAttribute( QStringLiteral( "templateUuid" ), composerItemNode.toElement().attribute( QStringLiteral( "uuid" ) ) );
composerItemNode.toElement().removeAttribute( QStringLiteral( "uuid" ) );
}
}
//read general settings
if ( clearExisting )
{
QDomElement layoutElem = doc.documentElement();
if ( layoutElem.isNull() )
{
return result;
}
bool loadOk = readXml( layoutElem, doc, context );
if ( !loadOk )
{
return result;
}
layoutItems( result );
}
else
{
result = addItemsFromXml( doc.documentElement(), doc, context );
}
if ( ok )
*ok = true;
return result;
}
QgsLayoutUndoStack *QgsLayout::undoStack()
{
return mUndoStack.get();

View File

@ -465,6 +465,26 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext
*/
QList< QgsLayoutMultiFrame * > multiFrames() const;
/**
* Saves the layout as a template at the given file \a path.
* Returns true if save was successful.
* \see loadFromTemplate()
*/
bool saveAsTemplate( const QString &path, const QgsReadWriteContext &context ) const;
/**
* Load a layout template \a document.
*
* By default this method will clear all items from the existing layout and real all layout
* settings from the template. Setting \a clearExisting to false will only add new items
* from the template, without overwriting the existing items or layout settings.
*
* If \a ok is specified, it will be set to true if the load was successful.
*
* Returns a list of loaded items.
*/
QList< QgsLayoutItem * > loadFromTemplate( const QDomDocument &document, const QgsReadWriteContext &context, bool clearExisting = true, bool *ok SIP_OUT = nullptr );
/**
* Returns the layout's state encapsulated in a DOM element.
* \see readXml()

View File

@ -719,6 +719,9 @@ void QgsLayoutView::deleteSelectedItems()
void QgsLayoutView::deleteItems( const QList<QgsLayoutItem *> &items )
{
if ( items.empty() )
return;
currentLayout()->undoStack()->beginMacro( tr( "Delete Items" ) );
//delete selected items
for ( QgsLayoutItem *item : items )

View File

@ -62,6 +62,8 @@
<attribute name="toolBarBreak">
<bool>false</bool>
</attribute>
<addaction name="mActionLoadFromTemplate"/>
<addaction name="mActionSaveAsTemplate"/>
</widget>
<widget class="QToolBar" name="mToolsToolbar">
<property name="windowTitle">
@ -85,7 +87,7 @@
<x>0</x>
<y>0</y>
<width>1083</width>
<height>42</height>
<height>25</height>
</rect>
</property>
<widget class="QMenu" name="mLayoutMenu">
@ -95,6 +97,9 @@
<addaction name="mActionLayoutProperties"/>
<addaction name="mActionAddPages"/>
<addaction name="separator"/>
<addaction name="mActionLoadFromTemplate"/>
<addaction name="mActionSaveAsTemplate"/>
<addaction name="separator"/>
<addaction name="mActionClose"/>
</widget>
<widget class="QMenu" name="mItemMenu">
@ -1057,6 +1062,30 @@
<string>Ctrl+Shift+V</string>
</property>
</action>
<action name="mActionSaveAsTemplate">
<property name="icon">
<iconset resource="../../../images/images.qrc">
<normaloff>:/images/themes/default/mActionFileSaveAs.svg</normaloff>:/images/themes/default/mActionFileSaveAs.svg</iconset>
</property>
<property name="text">
<string>Save as &amp;Template...</string>
</property>
<property name="toolTip">
<string>Save as template</string>
</property>
</action>
<action name="mActionLoadFromTemplate">
<property name="icon">
<iconset resource="../../../images/images.qrc">
<normaloff>:/images/themes/default/mActionFileOpen.svg</normaloff>:/images/themes/default/mActionFileOpen.svg</iconset>
</property>
<property name="text">
<string>&amp;Add Items from Template...</string>
</property>
<property name="toolTip">
<string>Add items from template</string>
</property>
</action>
</widget>
<resources>
<include location="../../../images/images.qrc"/>
@ -1086,6 +1115,7 @@
<include location="../../../images/images.qrc"/>
<include location="../../../images/images.qrc"/>
<include location="../../../images/images.qrc"/>
<include location="../../../images/images.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -14,6 +14,9 @@ __revision__ = '$Format:%H$'
import qgis # NOQA
import sip
import tempfile
import shutil
import os
from qgis.core import (QgsUnitTypes,
QgsLayout,
@ -43,6 +46,16 @@ start_app()
class TestQgsLayout(unittest.TestCase):
@classmethod
def setUpClass(cls):
"""Run before all tests"""
cls.basetestpath = tempfile.mkdtemp()
@classmethod
def tearDownClass(cls):
"""Run after all tests"""
shutil.rmtree(cls.basetestpath, True)
def testReadWriteXml(self):
p = QgsProject()
l = QgsLayout(p)
@ -204,6 +217,66 @@ class TestQgsLayout(unittest.TestCase):
#TODO - test restoring multiframe
def testSaveLoadTemplate(self):
tmpfile = os.path.join(self.basetestpath, 'testTemplate.qpt')
p = QgsProject()
l = QgsLayout(p)
# add some items
item1 = QgsLayoutItemLabel(l)
item1.setId('xxyyxx')
item1.attemptMove(QgsLayoutPoint(4, 8, QgsUnitTypes.LayoutMillimeters))
item1.attemptResize(QgsLayoutSize(18, 12, QgsUnitTypes.LayoutMillimeters))
l.addItem(item1)
item2 = QgsLayoutItemLabel(l)
item2.setId('zzyyzz')
item2.attemptMove(QgsLayoutPoint(1.4, 1.8, QgsUnitTypes.LayoutCentimeters))
item2.attemptResize(QgsLayoutSize(2.8, 2.2, QgsUnitTypes.LayoutCentimeters))
l.addItem(item2)
self.assertTrue(l.saveAsTemplate(tmpfile, QgsReadWriteContext()))
l2 = QgsLayout(p)
with open(tmpfile) as f:
template_content = f.read()
doc = QDomDocument()
doc.setContent(template_content)
# adding to existing items
new_items, ok = l2.loadFromTemplate(doc, QgsReadWriteContext(), False)
self.assertTrue(ok)
self.assertEqual(len(new_items), 2)
items = l2.items()
self.assertTrue([i for i in items if i.id() == 'xxyyxx'])
self.assertTrue([i for i in items if i.id() == 'zzyyzz'])
self.assertTrue(new_items[0] in l2.items())
self.assertTrue(new_items[1] in l2.items())
# adding to existing items
new_items2, ok = l2.loadFromTemplate(doc, QgsReadWriteContext(), False)
self.assertTrue(ok)
self.assertEqual(len(new_items2), 2)
items = l2.items()
self.assertEqual(len(items), 4)
self.assertTrue([i for i in items if i.id() == 'xxyyxx'])
self.assertTrue([i for i in items if i.id() == 'zzyyzz'])
self.assertTrue(new_items[0] in l2.items())
self.assertTrue(new_items[1] in l2.items())
self.assertTrue(new_items2[0] in l2.items())
self.assertTrue(new_items2[1] in l2.items())
# clearing existing items
new_items3, ok = l2.loadFromTemplate(doc, QgsReadWriteContext(), True)
self.assertTrue(ok)
self.assertEqual(len(new_items3), 2)
items = l2.items()
self.assertEqual(len(items), 2)
self.assertTrue([i for i in items if i.id() == 'xxyyxx'])
self.assertTrue([i for i in items if i.id() == 'zzyyzz'])
self.assertTrue(new_items3[0] in l2.items())
self.assertTrue(new_items3[1] in l2.items())
def testSelectedItems(self):
p = QgsProject()
l = QgsLayout(p)