mirror of
https://github.com/qgis/QGIS.git
synced 2025-04-17 00:04:02 -04:00
Restore ability to save layouts to templates and add items from template
This commit is contained in:
parent
59b6bf62ab
commit
a4dea9935f
@ -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.
|
||||
|
@ -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() );
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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()
|
||||
|
@ -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 )
|
||||
|
@ -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 &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>&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>
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user