QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
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 // cppcheck-suppress returnDanglingLifetime
281 return l;
282 }
283}
284
286{
287 QStringList names;
288 names.reserve( mLayouts.size() );
289 for ( QgsMasterLayoutInterface *l : mLayouts )
290 {
291 names << l->name();
292 }
293 QString name;
294 int id = 1;
295 while ( name.isEmpty() || names.contains( name ) )
296 {
297 switch ( type )
298 {
300 name = tr( "Layout %1" ).arg( id );
301 break;
303 name = tr( "Report %1" ).arg( id );
304 break;
305 }
306 id++;
307 }
308 return name;
309}
310
312{
313 if ( mLayouts.empty() )
314 return true;
315
316 // NOTE: if visitEnter returns false it means "don't visit the layouts", not "abort all further visitations"
317 if ( !visitor->visitEnter( QgsStyleEntityVisitorInterface::Node( QgsStyleEntityVisitorInterface::NodeType::Layouts, QStringLiteral( "layouts" ), tr( "Layouts" ) ) ) )
318 return true;
319
320 for ( QgsMasterLayoutInterface *l : mLayouts )
321 {
322 if ( !l->layoutAccept( visitor ) )
323 return false;
324 }
325
326 if ( !visitor->visitExit( QgsStyleEntityVisitorInterface::Node( QgsStyleEntityVisitorInterface::NodeType::Layouts, QStringLiteral( "layouts" ), tr( "Layouts" ) ) ) )
327 return false;
328
329 return true;
330}
331
332
333
334//
335// QgsLayoutManagerModel
336//
337
339 : QAbstractListModel( parent )
340 , mLayoutManager( manager )
341{
342 connect( mLayoutManager, &QgsLayoutManager::layoutAboutToBeAdded, this, &QgsLayoutManagerModel::layoutAboutToBeAdded );
343 connect( mLayoutManager, &QgsLayoutManager::layoutAdded, this, &QgsLayoutManagerModel::layoutAdded );
344 connect( mLayoutManager, &QgsLayoutManager::layoutAboutToBeRemoved, this, &QgsLayoutManagerModel::layoutAboutToBeRemoved );
345 connect( mLayoutManager, &QgsLayoutManager::layoutRemoved, this, &QgsLayoutManagerModel::layoutRemoved );
346 connect( mLayoutManager, &QgsLayoutManager::layoutRenamed, this, &QgsLayoutManagerModel::layoutRenamed );
347}
348
349int QgsLayoutManagerModel::rowCount( const QModelIndex &parent ) const
350{
351 Q_UNUSED( parent )
352 return ( mLayoutManager ? mLayoutManager->layouts().count() : 0 ) + ( mAllowEmpty ? 1 : 0 );
353}
354
355QVariant QgsLayoutManagerModel::data( const QModelIndex &index, int role ) const
356{
357 if ( index.row() < 0 || index.row() >= rowCount( QModelIndex() ) )
358 return QVariant();
359
360 const bool isEmpty = index.row() == 0 && mAllowEmpty;
361 const int layoutRow = mAllowEmpty ? index.row() - 1 : index.row();
362
363 switch ( role )
364 {
365 case Qt::DisplayRole:
366 case Qt::ToolTipRole:
367 case Qt::EditRole:
368 return !isEmpty && mLayoutManager ? mLayoutManager->layouts().at( layoutRow )->name() : QVariant();
369
370 case static_cast< int >( CustomRole::Layout ):
371 {
372 if ( isEmpty || !mLayoutManager )
373 return QVariant();
374 else if ( QgsLayout *l = dynamic_cast< QgsLayout * >( mLayoutManager->layouts().at( layoutRow ) ) )
375 return QVariant::fromValue( l );
376 else if ( QgsReport *r = dynamic_cast< QgsReport * >( mLayoutManager->layouts().at( layoutRow ) ) )
377 return QVariant::fromValue( r );
378 else
379 return QVariant();
380 }
381
382 case Qt::DecorationRole:
383 {
384 return isEmpty || !mLayoutManager ? QIcon() : mLayoutManager->layouts().at( layoutRow )->icon();
385 }
386
387 default:
388 return QVariant();
389 }
390}
391
392bool QgsLayoutManagerModel::setData( const QModelIndex &index, const QVariant &value, int role )
393{
394 if ( !index.isValid() || role != Qt::EditRole )
395 {
396 return false;
397 }
398 if ( index.row() >= mLayoutManager->layouts().count() )
399 {
400 return false;
401 }
402
403 if ( index.row() == 0 && mAllowEmpty )
404 return false;
405
406 if ( value.toString().isEmpty() )
407 return false;
408
409 QgsMasterLayoutInterface *layout = layoutFromIndex( index );
410 if ( !layout )
411 return false;
412
413 //has name changed?
414 bool changed = layout->name() != value.toString();
415 if ( !changed )
416 return true;
417
418 //check if name already exists
419 QStringList layoutNames;
420 const QList< QgsMasterLayoutInterface * > layouts = QgsProject::instance()->layoutManager()->layouts();
421 for ( QgsMasterLayoutInterface *l : layouts )
422 {
423 layoutNames << l->name();
424 }
425 if ( layoutNames.contains( value.toString() ) )
426 {
427 //name exists!
428 QMessageBox::warning( nullptr, tr( "Rename Layout" ), tr( "There is already a layout named “%1”." ).arg( value.toString() ) );
429 return false;
430 }
431
432 layout->setName( value.toString() );
433 return true;
434}
435
436Qt::ItemFlags QgsLayoutManagerModel::flags( const QModelIndex &index ) const
437{
438 Qt::ItemFlags flags = QAbstractListModel::flags( index );
439#if 0 // double-click is now used for opening the layout
440 if ( index.isValid() )
441 {
442 return flags | Qt::ItemIsEditable;
443 }
444 else
445 {
446 return flags;
447 }
448#endif
449 return flags;
450}
451
453{
454 if ( index.row() == 0 && mAllowEmpty )
455 return nullptr;
456
457 if ( QgsPrintLayout *l = qobject_cast< QgsPrintLayout * >( qvariant_cast<QObject *>( data( index, static_cast< int >( CustomRole::Layout ) ) ) ) )
458 return l;
459 else if ( QgsReport *r = qobject_cast< QgsReport * >( qvariant_cast<QObject *>( data( index, static_cast< int >( CustomRole::Layout ) ) ) ) )
460 return r;
461 else
462 return nullptr;
463}
464
466{
467 if ( !mLayoutManager )
468 {
469 return QModelIndex();
470 }
471
472 const int r = mLayoutManager->layouts().indexOf( layout );
473 if ( r < 0 )
474 return QModelIndex();
475
476 QModelIndex idx = index( mAllowEmpty ? r + 1 : r, 0, QModelIndex() );
477 if ( idx.isValid() )
478 {
479 return idx;
480 }
481
482 return QModelIndex();
483}
484
486{
487 if ( allowEmpty == mAllowEmpty )
488 return;
489
490 if ( allowEmpty )
491 {
492 beginInsertRows( QModelIndex(), 0, 0 );
493 mAllowEmpty = true;
494 endInsertRows();
495 }
496 else
497 {
498 beginRemoveRows( QModelIndex(), 0, 0 );
499 mAllowEmpty = false;
500 endRemoveRows();
501 }
502}
503
504void QgsLayoutManagerModel::layoutAboutToBeAdded( const QString & )
505{
506 int row = mLayoutManager->layouts().count() + ( mAllowEmpty ? 1 : 0 );
507 beginInsertRows( QModelIndex(), row, row );
508}
509
510void QgsLayoutManagerModel::layoutAboutToBeRemoved( const QString &name )
511{
512 QgsMasterLayoutInterface *l = mLayoutManager->layoutByName( name );
513 int row = mLayoutManager->layouts().indexOf( l ) + ( mAllowEmpty ? 1 : 0 );
514 if ( row >= 0 )
515 beginRemoveRows( QModelIndex(), row, row );
516}
517
518void QgsLayoutManagerModel::layoutAdded( const QString & )
519{
520 endInsertRows();
521}
522
523void QgsLayoutManagerModel::layoutRemoved( const QString & )
524{
525 endRemoveRows();
526}
527
528void QgsLayoutManagerModel::layoutRenamed( QgsMasterLayoutInterface *layout, const QString & )
529{
530 int row = mLayoutManager->layouts().indexOf( layout ) + ( mAllowEmpty ? 1 : 0 );
531 QModelIndex index = createIndex( row, 0 );
532 emit dataChanged( index, index, QVector<int>() << Qt::DisplayRole );
533}
534
535//
536// QgsLayoutManagerProxyModel
537//
538
540 : QSortFilterProxyModel( parent )
541{
542 setDynamicSortFilter( true );
543 sort( 0 );
544 setSortCaseSensitivity( Qt::CaseInsensitive );
545}
546
547bool QgsLayoutManagerProxyModel::lessThan( const QModelIndex &left, const QModelIndex &right ) const
548{
549 const QString leftText = sourceModel()->data( left, Qt::DisplayRole ).toString();
550 const QString rightText = sourceModel()->data( right, Qt::DisplayRole ).toString();
551 if ( leftText.isEmpty() )
552 return true;
553 if ( rightText.isEmpty() )
554 return false;
555
556 return QString::localeAwareCompare( leftText, rightText ) < 0;
557}
558
559bool QgsLayoutManagerProxyModel::filterAcceptsRow( int sourceRow, const QModelIndex &sourceParent ) const
560{
561 QgsLayoutManagerModel *model = qobject_cast< QgsLayoutManagerModel * >( sourceModel() );
562 if ( !model )
563 return false;
564
565 QgsMasterLayoutInterface *layout = model->layoutFromIndex( model->index( sourceRow, 0, sourceParent ) );
566 if ( !layout )
567 return model->allowEmptyLayout();
568
569 if ( !mFilterString.trimmed().isEmpty() )
570 {
571 if ( !layout->name().contains( mFilterString, Qt::CaseInsensitive ) )
572 return false;
573 }
574
575 switch ( layout->layoutType() )
576 {
578 return mFilters & FilterPrintLayouts;
580 return mFilters & FilterReports;
581 }
582 return false;
583}
584
586{
587 return mFilters;
588}
589
591{
592 mFilters = filters;
593 invalidateFilter();
594}
595
597{
598 mFilterString = filter;
599 invalidateFilter();
600}
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.
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:49
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:107
static QgsProject * instance()
Returns the QgsProject singleton instance.
Definition: qgsproject.cpp:481
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:599
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.