QGIS API Documentation 3.28.0-Firenze (ed3ad0430f)
qgslayoutmanager.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgslayoutmanager.cpp
3 --------------------
4 Date : January 2017
5 Copyright : (C) 2017 Nyall Dawson
6 Email : nyall dot dawson at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
16#include "qgslayoutmanager.h"
17#include "qgslayout.h"
18#include "qgsproject.h"
19#include "qgslogger.h"
20#include "qgslayoutundostack.h"
21#include "qgsprintlayout.h"
22#include "qgsreport.h"
24#include "qgsreadwritecontext.h"
26#include "qgsruntimeprofiler.h"
27#include <QMessageBox>
28
30 : QObject( project )
31 , mProject( project )
32{
33
34}
35
37{
38 clear();
39}
40
42{
43 if ( !layout || mLayouts.contains( layout ) )
44 return false;
45
46 // check for duplicate name
47 for ( QgsMasterLayoutInterface *l : std::as_const( mLayouts ) )
48 {
49 if ( l->name() == layout->name() )
50 {
51 delete layout;
52 return false;
53 }
54 }
55
56 // ugly, but unavoidable for interfaces...
57 if ( QgsPrintLayout *l = dynamic_cast< QgsPrintLayout * >( layout ) )
58 {
59 connect( l, &QgsPrintLayout::nameChanged, this, [this, l]( const QString & newName )
60 {
61 emit layoutRenamed( l, newName );
62 } );
63 }
64 else if ( QgsReport *r = dynamic_cast< QgsReport * >( layout ) )
65 {
66 connect( r, &QgsReport::nameChanged, this, [this, r]( const QString & newName )
67 {
68 emit layoutRenamed( r, newName );
69 } );
70 }
71
72 emit layoutAboutToBeAdded( layout->name() );
73 mLayouts << layout;
74 emit layoutAdded( layout->name() );
75 mProject->setDirty( true );
76 return true;
77}
78
80{
81 if ( !layout )
82 return false;
83
84 if ( !mLayouts.contains( layout ) )
85 return false;
86
87 QString name = layout->name();
88 emit layoutAboutToBeRemoved( name );
89 mLayouts.removeAll( layout );
90 delete layout;
91 emit layoutRemoved( name );
92 mProject->setDirty( true );
93 return true;
94}
95
97{
98 const QList< QgsMasterLayoutInterface * > layouts = mLayouts;
100 {
101 removeLayout( l );
102 }
103}
104
105QList<QgsMasterLayoutInterface *> QgsLayoutManager::layouts() const
106{
107 return mLayouts;
108}
109
110QList<QgsPrintLayout *> QgsLayoutManager::printLayouts() const
111{
112 QList<QgsPrintLayout *> result;
113 const QList<QgsMasterLayoutInterface *> _layouts( mLayouts );
114 result.reserve( _layouts.size() );
115 for ( const auto &layout : _layouts )
116 {
117 QgsPrintLayout *_item( dynamic_cast<QgsPrintLayout *>( layout ) );
118 if ( _item )
119 result.push_back( _item );
120 }
121 return result;
122}
123
125{
126 for ( QgsMasterLayoutInterface *l : mLayouts )
127 {
128 if ( l->name() == name )
129 return l;
130 }
131 return nullptr;
132}
133
134bool QgsLayoutManager::readXml( const QDomElement &element, const QDomDocument &doc )
135{
136 clear();
137
138 QDomElement layoutsElem = element;
139 if ( element.tagName() != QLatin1String( "Layouts" ) )
140 {
141 layoutsElem = element.firstChildElement( QStringLiteral( "Layouts" ) );
142 }
143 if ( layoutsElem.isNull() )
144 {
145 // handle legacy projects
146 layoutsElem = doc.documentElement();
147 }
148
149 //restore each composer
150 bool result = true;
151 QDomNodeList composerNodes = element.elementsByTagName( QStringLiteral( "Composer" ) );
152 QgsScopedRuntimeProfile profile( tr( "Loading QGIS 2.x compositions" ), QStringLiteral( "projectload" ) );
153 for ( int i = 0; i < composerNodes.size(); ++i )
154 {
155 // This legacy title is the Composer "title" (that can be overridden by the Composition "name")
156 QString legacyTitle = composerNodes.at( i ).toElement().attribute( QStringLiteral( "title" ) );
157 // Convert compositions to layouts
158 QDomNodeList compositionNodes = composerNodes.at( i ).toElement().elementsByTagName( QStringLiteral( "Composition" ) );
159 for ( int j = 0; j < compositionNodes.size(); ++j )
160 {
161 std::unique_ptr< QgsPrintLayout > l( QgsCompositionConverter::createLayoutFromCompositionXml( compositionNodes.at( j ).toElement(), mProject ) );
162 if ( l )
163 {
164 if ( l->name().isEmpty() )
165 l->setName( legacyTitle );
166
167 // some 2.x projects could end in a state where they had duplicated layout names. This is strictly forbidden in 3.x
168 // so check for duplicate name in layouts already added
169 int id = 2;
170 bool isDuplicateName = false;
171 QString originalName = l->name();
172 do
173 {
174 isDuplicateName = false;
175 for ( QgsMasterLayoutInterface *layout : std::as_const( mLayouts ) )
176 {
177 if ( l->name() == layout->name() )
178 {
179 isDuplicateName = true;
180 break;
181 }
182 }
183 if ( isDuplicateName )
184 {
185 l->setName( QStringLiteral( "%1 %2" ).arg( originalName ).arg( id ) );
186 id++;
187 }
188 }
189 while ( isDuplicateName );
190
191 bool added = addLayout( l.release() );
192 result = added && result;
193 }
194 }
195 }
196
197 QgsReadWriteContext context;
198 context.setPathResolver( mProject->pathResolver() );
199
200 profile.switchTask( tr( "Creating layouts" ) );
201
202 // restore layouts
203 const QDomNodeList layoutNodes = layoutsElem.childNodes();
204 for ( int i = 0; i < layoutNodes.size(); ++i )
205 {
206 if ( layoutNodes.at( i ).nodeName() != QLatin1String( "Layout" ) )
207 continue;
208
209 const QString layoutName = layoutNodes.at( i ).toElement().attribute( QStringLiteral( "name" ) );
210 QgsScopedRuntimeProfile profile( layoutName, QStringLiteral( "projectload" ) );
211
212 std::unique_ptr< QgsPrintLayout > l = std::make_unique< QgsPrintLayout >( mProject );
213 l->undoStack()->blockCommands( true );
214 if ( !l->readLayoutXml( layoutNodes.at( i ).toElement(), doc, context ) )
215 {
216 result = false;
217 continue;
218 }
219 l->undoStack()->blockCommands( false );
220 if ( !addLayout( l.release() ) )
221 {
222 result = false;
223 }
224 }
225 //reports
226 profile.switchTask( tr( "Creating reports" ) );
227 const QDomNodeList reportNodes = element.elementsByTagName( QStringLiteral( "Report" ) );
228 for ( int i = 0; i < reportNodes.size(); ++i )
229 {
230 const QString layoutName = reportNodes.at( i ).toElement().attribute( QStringLiteral( "name" ) );
231 QgsScopedRuntimeProfile profile( layoutName, QStringLiteral( "projectload" ) );
232
233 std::unique_ptr< QgsReport > r = std::make_unique< QgsReport >( mProject );
234 if ( !r->readLayoutXml( reportNodes.at( i ).toElement(), doc, context ) )
235 {
236 result = false;
237 continue;
238 }
239 if ( !addLayout( r.release() ) )
240 {
241 result = false;
242 }
243 }
244 return result;
245}
246
247QDomElement QgsLayoutManager::writeXml( QDomDocument &doc ) const
248{
249 QDomElement layoutsElem = doc.createElement( QStringLiteral( "Layouts" ) );
250
251 QgsReadWriteContext context;
252 context.setPathResolver( mProject->pathResolver() );
253 for ( QgsMasterLayoutInterface *l : mLayouts )
254 {
255 QDomElement layoutElem = l->writeLayoutXml( doc, context );
256 layoutsElem.appendChild( layoutElem );
257 }
258 return layoutsElem;
259}
260
262{
263 if ( !layout )
264 return nullptr;
265
266 std::unique_ptr< QgsMasterLayoutInterface > newLayout( layout->clone() );
267 if ( !newLayout )
268 {
269 return nullptr;
270 }
271
272 newLayout->setName( newName );
273 QgsMasterLayoutInterface *l = newLayout.get();
274 if ( !addLayout( newLayout.release() ) )
275 {
276 return nullptr;
277 }
278 else
279 {
280 return l;
281 }
282}
283
285{
286 QStringList names;
287 names.reserve( mLayouts.size() );
288 for ( QgsMasterLayoutInterface *l : mLayouts )
289 {
290 names << l->name();
291 }
292 QString name;
293 int id = 1;
294 while ( name.isEmpty() || names.contains( name ) )
295 {
296 switch ( type )
297 {
299 name = tr( "Layout %1" ).arg( id );
300 break;
302 name = tr( "Report %1" ).arg( id );
303 break;
304 }
305 id++;
306 }
307 return name;
308}
309
311{
312 if ( mLayouts.empty() )
313 return true;
314
315 // NOTE: if visitEnter returns false it means "don't visit the layouts", not "abort all further visitations"
316 if ( !visitor->visitEnter( QgsStyleEntityVisitorInterface::Node( QgsStyleEntityVisitorInterface::NodeType::Layouts, QStringLiteral( "layouts" ), tr( "Layouts" ) ) ) )
317 return true;
318
319 for ( QgsMasterLayoutInterface *l : mLayouts )
320 {
321 if ( !l->layoutAccept( visitor ) )
322 return false;
323 }
324
325 if ( !visitor->visitExit( QgsStyleEntityVisitorInterface::Node( QgsStyleEntityVisitorInterface::NodeType::Layouts, QStringLiteral( "layouts" ), tr( "Layouts" ) ) ) )
326 return false;
327
328 return true;
329}
330
331
332
333//
334// QgsLayoutManagerModel
335//
336
338 : QAbstractListModel( parent )
339 , mLayoutManager( manager )
340{
341 connect( mLayoutManager, &QgsLayoutManager::layoutAboutToBeAdded, this, &QgsLayoutManagerModel::layoutAboutToBeAdded );
342 connect( mLayoutManager, &QgsLayoutManager::layoutAdded, this, &QgsLayoutManagerModel::layoutAdded );
343 connect( mLayoutManager, &QgsLayoutManager::layoutAboutToBeRemoved, this, &QgsLayoutManagerModel::layoutAboutToBeRemoved );
344 connect( mLayoutManager, &QgsLayoutManager::layoutRemoved, this, &QgsLayoutManagerModel::layoutRemoved );
345 connect( mLayoutManager, &QgsLayoutManager::layoutRenamed, this, &QgsLayoutManagerModel::layoutRenamed );
346}
347
348int QgsLayoutManagerModel::rowCount( const QModelIndex &parent ) const
349{
350 Q_UNUSED( parent )
351 return ( mLayoutManager ? mLayoutManager->layouts().count() : 0 ) + ( mAllowEmpty ? 1 : 0 );
352}
353
354QVariant QgsLayoutManagerModel::data( const QModelIndex &index, int role ) const
355{
356 if ( index.row() < 0 || index.row() >= rowCount( QModelIndex() ) )
357 return QVariant();
358
359 const bool isEmpty = index.row() == 0 && mAllowEmpty;
360 const int layoutRow = mAllowEmpty ? index.row() - 1 : index.row();
361
362 switch ( role )
363 {
364 case Qt::DisplayRole:
365 case Qt::ToolTipRole:
366 case Qt::EditRole:
367 return !isEmpty && mLayoutManager ? mLayoutManager->layouts().at( layoutRow )->name() : QVariant();
368
369 case LayoutRole:
370 {
371 if ( isEmpty || !mLayoutManager )
372 return QVariant();
373 else if ( QgsLayout *l = dynamic_cast< QgsLayout * >( mLayoutManager->layouts().at( layoutRow ) ) )
374 return QVariant::fromValue( l );
375 else if ( QgsReport *r = dynamic_cast< QgsReport * >( mLayoutManager->layouts().at( layoutRow ) ) )
376 return QVariant::fromValue( r );
377 else
378 return QVariant();
379 }
380
381 case Qt::DecorationRole:
382 {
383 return isEmpty || !mLayoutManager ? QIcon() : mLayoutManager->layouts().at( layoutRow )->icon();
384 }
385
386 default:
387 return QVariant();
388 }
389}
390
391bool QgsLayoutManagerModel::setData( const QModelIndex &index, const QVariant &value, int role )
392{
393 if ( !index.isValid() || role != Qt::EditRole )
394 {
395 return false;
396 }
397 if ( index.row() >= mLayoutManager->layouts().count() )
398 {
399 return false;
400 }
401
402 if ( index.row() == 0 && mAllowEmpty )
403 return false;
404
405 if ( value.toString().isEmpty() )
406 return false;
407
408 QgsMasterLayoutInterface *layout = layoutFromIndex( index );
409 if ( !layout )
410 return false;
411
412 //has name changed?
413 bool changed = layout->name() != value.toString();
414 if ( !changed )
415 return true;
416
417 //check if name already exists
418 QStringList layoutNames;
419 const QList< QgsMasterLayoutInterface * > layouts = QgsProject::instance()->layoutManager()->layouts();
420 for ( QgsMasterLayoutInterface *l : layouts )
421 {
422 layoutNames << l->name();
423 }
424 if ( layoutNames.contains( value.toString() ) )
425 {
426 //name exists!
427 QMessageBox::warning( nullptr, tr( "Rename Layout" ), tr( "There is already a layout named “%1”." ).arg( value.toString() ) );
428 return false;
429 }
430
431 layout->setName( value.toString() );
432 return true;
433}
434
435Qt::ItemFlags QgsLayoutManagerModel::flags( const QModelIndex &index ) const
436{
437 Qt::ItemFlags flags = QAbstractListModel::flags( index );
438#if 0 // double-click is now used for opening the layout
439 if ( index.isValid() )
440 {
441 return flags | Qt::ItemIsEditable;
442 }
443 else
444 {
445 return flags;
446 }
447#endif
448 return flags;
449}
450
452{
453 if ( index.row() == 0 && mAllowEmpty )
454 return nullptr;
455
456 if ( QgsPrintLayout *l = qobject_cast< QgsPrintLayout * >( qvariant_cast<QObject *>( data( index, LayoutRole ) ) ) )
457 return l;
458 else if ( QgsReport *r = qobject_cast< QgsReport * >( qvariant_cast<QObject *>( data( index, LayoutRole ) ) ) )
459 return r;
460 else
461 return nullptr;
462}
463
465{
466 if ( !mLayoutManager )
467 {
468 return QModelIndex();
469 }
470
471 const int r = mLayoutManager->layouts().indexOf( layout );
472 if ( r < 0 )
473 return QModelIndex();
474
475 QModelIndex idx = index( mAllowEmpty ? r + 1 : r, 0, QModelIndex() );
476 if ( idx.isValid() )
477 {
478 return idx;
479 }
480
481 return QModelIndex();
482}
483
485{
486 if ( allowEmpty == mAllowEmpty )
487 return;
488
489 if ( allowEmpty )
490 {
491 beginInsertRows( QModelIndex(), 0, 0 );
492 mAllowEmpty = true;
493 endInsertRows();
494 }
495 else
496 {
497 beginRemoveRows( QModelIndex(), 0, 0 );
498 mAllowEmpty = false;
499 endRemoveRows();
500 }
501}
502
503void QgsLayoutManagerModel::layoutAboutToBeAdded( const QString & )
504{
505 int row = mLayoutManager->layouts().count() + ( mAllowEmpty ? 1 : 0 );
506 beginInsertRows( QModelIndex(), row, row );
507}
508
509void QgsLayoutManagerModel::layoutAboutToBeRemoved( const QString &name )
510{
511 QgsMasterLayoutInterface *l = mLayoutManager->layoutByName( name );
512 int row = mLayoutManager->layouts().indexOf( l ) + ( mAllowEmpty ? 1 : 0 );
513 if ( row >= 0 )
514 beginRemoveRows( QModelIndex(), row, row );
515}
516
517void QgsLayoutManagerModel::layoutAdded( const QString & )
518{
519 endInsertRows();
520}
521
522void QgsLayoutManagerModel::layoutRemoved( const QString & )
523{
524 endRemoveRows();
525}
526
527void QgsLayoutManagerModel::layoutRenamed( QgsMasterLayoutInterface *layout, const QString & )
528{
529 int row = mLayoutManager->layouts().indexOf( layout ) + ( mAllowEmpty ? 1 : 0 );
530 QModelIndex index = createIndex( row, 0 );
531 emit dataChanged( index, index, QVector<int>() << Qt::DisplayRole );
532}
533
534//
535// QgsLayoutManagerProxyModel
536//
537
539 : QSortFilterProxyModel( parent )
540{
541 setDynamicSortFilter( true );
542 sort( 0 );
543 setSortCaseSensitivity( Qt::CaseInsensitive );
544}
545
546bool QgsLayoutManagerProxyModel::lessThan( const QModelIndex &left, const QModelIndex &right ) const
547{
548 const QString leftText = sourceModel()->data( left, Qt::DisplayRole ).toString();
549 const QString rightText = sourceModel()->data( right, Qt::DisplayRole ).toString();
550 if ( leftText.isEmpty() )
551 return true;
552 if ( rightText.isEmpty() )
553 return false;
554
555 return QString::localeAwareCompare( leftText, rightText ) < 0;
556}
557
558bool QgsLayoutManagerProxyModel::filterAcceptsRow( int sourceRow, const QModelIndex &sourceParent ) const
559{
560 QgsLayoutManagerModel *model = qobject_cast< QgsLayoutManagerModel * >( sourceModel() );
561 if ( !model )
562 return false;
563
564 QgsMasterLayoutInterface *layout = model->layoutFromIndex( model->index( sourceRow, 0, sourceParent ) );
565 if ( !layout )
566 return model->allowEmptyLayout();
567
568 if ( !mFilterString.trimmed().isEmpty() )
569 {
570 if ( !layout->name().contains( mFilterString, Qt::CaseInsensitive ) )
571 return false;
572 }
573
574 switch ( layout->layoutType() )
575 {
577 return mFilters & FilterPrintLayouts;
579 return mFilters & FilterReports;
580 }
581 return false;
582}
583
584QgsLayoutManagerProxyModel::Filters QgsLayoutManagerProxyModel::filters() const
585{
586 return mFilters;
587}
588
590{
591 mFilters = filters;
592 invalidateFilter();
593}
594
596{
597 mFilterString = filter;
598 invalidateFilter();
599}
static std::unique_ptr< QgsPrintLayout > createLayoutFromCompositionXml(const QDomElement &composerElement, QgsProject *project)
createLayoutFromCompositionXml is a factory that creates layout instances from a QGIS 2....
List model representing the print layouts and reports available in a layout manager.
bool setData(const QModelIndex &index, const QVariant &value, int role=Qt::EditRole) override
QModelIndex indexFromLayout(QgsMasterLayoutInterface *layout) const
Returns the model index corresponding to a layout.
@ LayoutRole
Layout object.
Qt::ItemFlags flags(const QModelIndex &index) const override
QgsMasterLayoutInterface * layoutFromIndex(const QModelIndex &index) const
Returns the layout at the corresponding index.
void setAllowEmptyLayout(bool allowEmpty)
Sets whether an optional empty layout ("not set") option is present in the model.
QVariant data(const QModelIndex &index, int role) const override
bool allowEmptyLayout() const
Returns true if the model allows the empty layout ("not set") choice.
QgsLayoutManagerModel(QgsLayoutManager *manager, QObject *parent=nullptr)
Constructor for QgsLayoutManagerModel, showing the layouts from the specified manager.
int rowCount(const QModelIndex &parent) const override
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override
void setFilterString(const QString &filter)
Sets a filter string, such that only layouts with names containing the specified string will be shown...
void setFilters(QgsLayoutManagerProxyModel::Filters filters)
Sets the current filters used for filtering available layouts.
QgsLayoutManagerProxyModel::Filters filters() const
Returns the current filters used for filtering available layouts.
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override
@ FilterReports
Includes reports.
@ FilterPrintLayouts
Includes print layouts.
QgsLayoutManagerProxyModel(QObject *parent=nullptr)
Constructor for QgsLayoutManagerProxyModel.
Manages storage of a set of layouts.
bool readXml(const QDomElement &element, const QDomDocument &doc)
Reads the manager's state from a DOM element, restoring all layouts present in the XML document.
QgsLayoutManager(QgsProject *project=nullptr)
Constructor for QgsLayoutManager.
QList< QgsMasterLayoutInterface * > layouts() const
Returns a list of all layouts contained in the manager.
QList< QgsPrintLayout * > printLayouts() const
Returns a list of all print layouts contained in the manager.
void layoutAboutToBeRemoved(const QString &name)
Emitted when a layout is about to be removed from the manager.
~QgsLayoutManager() override
bool removeLayout(QgsMasterLayoutInterface *layout)
Removes a layout from the manager.
QgsMasterLayoutInterface * layoutByName(const QString &name) const
Returns the layout with a matching name, or nullptr if no matching layouts were found.
void clear()
Removes and deletes all layouts from the manager.
bool addLayout(QgsMasterLayoutInterface *layout)
Adds a layout to the manager.
void layoutAboutToBeAdded(const QString &name)
Emitted when a layout is about to be added to the manager.
bool accept(QgsStyleEntityVisitorInterface *visitor) const
Accepts the specified style entity visitor, causing it to visit all style entities associated within ...
void layoutRenamed(QgsMasterLayoutInterface *layout, const QString &newName)
Emitted when a layout is renamed.
void layoutRemoved(const QString &name)
Emitted when a layout was removed from the manager.
QDomElement writeXml(QDomDocument &doc) const
Returns a DOM element representing the state of the manager.
QString generateUniqueTitle(QgsMasterLayoutInterface::Type type=QgsMasterLayoutInterface::PrintLayout) const
Generates a unique title for a new layout of the specified type, which does not clash with any alread...
void layoutAdded(const QString &name)
Emitted when a layout has been added to the manager.
QgsMasterLayoutInterface * duplicateLayout(const QgsMasterLayoutInterface *layout, const QString &newName)
Duplicates an existing layout from the manager.
Base class for layouts, which can contain items such as maps, labels, scalebars, etc.
Definition: qgslayout.h:51
Interface for master layout type objects, such as print layouts and reports.
virtual QString name() const =0
Returns the layout's name.
virtual QgsMasterLayoutInterface::Type layoutType() const =0
Returns the master layout type.
@ PrintLayout
Individual print layout (QgsPrintLayout)
virtual QgsMasterLayoutInterface * clone() const =0
Creates a clone of the layout.
virtual void setName(const QString &name)=0
Sets the layout's name.
Print layout, a QgsLayout subclass for static or atlas-based layouts.
void nameChanged(const QString &name)
Emitted when the layout's name is changed.
Encapsulates a QGIS project, including sets of map layers and their styles, layouts,...
Definition: qgsproject.h:104
static QgsProject * instance()
Returns the QgsProject singleton instance.
Definition: qgsproject.cpp:477
QgsPathResolver pathResolver() const
Returns path resolver object with considering whether the project uses absolute or relative paths and...
const QgsLayoutManager * layoutManager() const
Returns the project's layout manager, which manages print layouts, atlases and reports within the pro...
void setDirty(bool b=true)
Flag the project as dirty (modified).
Definition: qgsproject.cpp:573
The class is used as a container of context for various read/write operations on other objects.
void setPathResolver(const QgsPathResolver &resolver)
Sets up path resolver for conversion between relative and absolute paths.
Scoped object for logging of the runtime for a single operation or group of operations.
void switchTask(const QString &name)
Switches the current task managed by the scoped profile to a new task with the given name.
An interface for classes which can visit style entity (e.g.
virtual bool visitExit(const QgsStyleEntityVisitorInterface::Node &node)
Called when the visitor stops visiting a node.
virtual bool visitEnter(const QgsStyleEntityVisitorInterface::Node &node)
Called when the visitor starts visiting a node.
Contains information relating to a node (i.e.