QGIS API Documentation 3.41.0-Master (cea29feecf2)
Loading...
Searching...
No Matches
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 "moc_qgslayoutmanager.cpp"
18#include "qgslayout.h"
19#include "qgsproject.h"
20#include "qgslogger.h"
21#include "qgslayoutundostack.h"
22#include "qgsprintlayout.h"
23#include "qgsreport.h"
25#include "qgsreadwritecontext.h"
27#include "qgsruntimeprofiler.h"
28#include <QMessageBox>
29
31 : QObject( project )
32 , mProject( project )
33{
34
35}
36
41
43{
44 if ( !layout || mLayouts.contains( layout ) )
45 return false;
46
47 // check for duplicate name
48 for ( QgsMasterLayoutInterface *l : std::as_const( mLayouts ) )
49 {
50 if ( l->name() == layout->name() )
51 {
52 delete layout;
53 return false;
54 }
55 }
56
57 // ugly, but unavoidable for interfaces...
58 if ( QgsPrintLayout *l = dynamic_cast< QgsPrintLayout * >( layout ) )
59 {
60 connect( l, &QgsPrintLayout::nameChanged, this, [this, l]( const QString & newName )
61 {
62 emit layoutRenamed( l, newName );
63 } );
64 }
65 else if ( QgsReport *r = dynamic_cast< QgsReport * >( layout ) )
66 {
67 connect( r, &QgsReport::nameChanged, this, [this, r]( const QString & newName )
68 {
69 emit layoutRenamed( r, newName );
70 } );
71 }
72
73 emit layoutAboutToBeAdded( layout->name() );
74 mLayouts << layout;
75 emit layoutAdded( layout->name() );
76 mProject->setDirty( true );
77 return true;
78}
79
81{
82 if ( !layout )
83 return false;
84
85 if ( !mLayouts.contains( layout ) )
86 return false;
87
88 QString name = layout->name();
89 emit layoutAboutToBeRemoved( name );
90 mLayouts.removeAll( layout );
91 delete layout;
92 emit layoutRemoved( name );
93 mProject->setDirty( true );
94 return true;
95}
96
98{
99 const QList< QgsMasterLayoutInterface * > layouts = mLayouts;
101 {
102 removeLayout( l );
103 }
104}
105
106QList<QgsMasterLayoutInterface *> QgsLayoutManager::layouts() const
107{
108 return mLayouts;
109}
110
111QList<QgsPrintLayout *> QgsLayoutManager::printLayouts() const
112{
113 QList<QgsPrintLayout *> result;
114 const QList<QgsMasterLayoutInterface *> _layouts( mLayouts );
115 result.reserve( _layouts.size() );
116 for ( const auto &layout : _layouts )
117 {
118 QgsPrintLayout *_item( dynamic_cast<QgsPrintLayout *>( layout ) );
119 if ( _item )
120 result.push_back( _item );
121 }
122 return result;
123}
124
126{
127 for ( QgsMasterLayoutInterface *l : mLayouts )
128 {
129 if ( l->name() == name )
130 return l;
131 }
132 return nullptr;
133}
134
135bool QgsLayoutManager::readXml( const QDomElement &element, const QDomDocument &doc )
136{
137 clear();
138
139 QDomElement layoutsElem = element;
140 if ( element.tagName() != QLatin1String( "Layouts" ) )
141 {
142 layoutsElem = element.firstChildElement( QStringLiteral( "Layouts" ) );
143 }
144 if ( layoutsElem.isNull() )
145 {
146 // handle legacy projects
147 layoutsElem = doc.documentElement();
148 }
149
150 //restore each composer
151 bool result = true;
152 QDomNodeList composerNodes = element.elementsByTagName( QStringLiteral( "Composer" ) );
153 QgsScopedRuntimeProfile profile( tr( "Loading QGIS 2.x compositions" ), QStringLiteral( "projectload" ) );
154 for ( int i = 0; i < composerNodes.size(); ++i )
155 {
156 // This legacy title is the Composer "title" (that can be overridden by the Composition "name")
157 QString legacyTitle = composerNodes.at( i ).toElement().attribute( QStringLiteral( "title" ) );
158 // Convert compositions to layouts
159 QDomNodeList compositionNodes = composerNodes.at( i ).toElement().elementsByTagName( QStringLiteral( "Composition" ) );
160 for ( int j = 0; j < compositionNodes.size(); ++j )
161 {
162 std::unique_ptr< QgsPrintLayout > l( QgsCompositionConverter::createLayoutFromCompositionXml( compositionNodes.at( j ).toElement(), mProject ) );
163 if ( l )
164 {
165 if ( l->name().isEmpty() )
166 l->setName( legacyTitle );
167
168 // some 2.x projects could end in a state where they had duplicated layout names. This is strictly forbidden in 3.x
169 // so check for duplicate name in layouts already added
170 int id = 2;
171 bool isDuplicateName = false;
172 QString originalName = l->name();
173 do
174 {
175 isDuplicateName = false;
176 for ( QgsMasterLayoutInterface *layout : std::as_const( mLayouts ) )
177 {
178 if ( l->name() == layout->name() )
179 {
180 isDuplicateName = true;
181 break;
182 }
183 }
184 if ( isDuplicateName )
185 {
186 l->setName( QStringLiteral( "%1 %2" ).arg( originalName ).arg( id ) );
187 id++;
188 }
189 }
190 while ( isDuplicateName );
191
192 bool added = addLayout( l.release() );
193 result = added && result;
194 }
195 }
196 }
197
198 QgsReadWriteContext context;
199 context.setPathResolver( mProject->pathResolver() );
200
201 profile.switchTask( tr( "Creating layouts" ) );
202
203 // restore layouts
204 const QDomNodeList layoutNodes = layoutsElem.childNodes();
205 for ( int i = 0; i < layoutNodes.size(); ++i )
206 {
207 if ( layoutNodes.at( i ).nodeName() != QLatin1String( "Layout" ) )
208 continue;
209
210 const QString layoutName = layoutNodes.at( i ).toElement().attribute( QStringLiteral( "name" ) );
211 QgsScopedRuntimeProfile profile( layoutName, QStringLiteral( "projectload" ) );
212
213 std::unique_ptr< QgsPrintLayout > l = std::make_unique< QgsPrintLayout >( mProject );
214 l->undoStack()->blockCommands( true );
215 if ( !l->readLayoutXml( layoutNodes.at( i ).toElement(), doc, context ) )
216 {
217 result = false;
218 continue;
219 }
220 l->undoStack()->blockCommands( false );
221 if ( !addLayout( l.release() ) )
222 {
223 result = false;
224 }
225 }
226 //reports
227 profile.switchTask( tr( "Creating reports" ) );
228 const QDomNodeList reportNodes = element.elementsByTagName( QStringLiteral( "Report" ) );
229 for ( int i = 0; i < reportNodes.size(); ++i )
230 {
231 const QString layoutName = reportNodes.at( i ).toElement().attribute( QStringLiteral( "name" ) );
232 QgsScopedRuntimeProfile profile( layoutName, QStringLiteral( "projectload" ) );
233
234 std::unique_ptr< QgsReport > r = std::make_unique< QgsReport >( mProject );
235 if ( !r->readLayoutXml( reportNodes.at( i ).toElement(), doc, context ) )
236 {
237 result = false;
238 continue;
239 }
240 if ( !addLayout( r.release() ) )
241 {
242 result = false;
243 }
244 }
245 return result;
246}
247
248QDomElement QgsLayoutManager::writeXml( QDomDocument &doc ) const
249{
250 QDomElement layoutsElem = doc.createElement( QStringLiteral( "Layouts" ) );
251
252 QgsReadWriteContext context;
253 context.setPathResolver( mProject->pathResolver() );
254 for ( QgsMasterLayoutInterface *l : mLayouts )
255 {
256 QDomElement layoutElem = l->writeLayoutXml( doc, context );
257 layoutsElem.appendChild( layoutElem );
258 }
259 return layoutsElem;
260}
261
263{
264 if ( !layout )
265 return nullptr;
266
267 std::unique_ptr< QgsMasterLayoutInterface > newLayout( layout->clone() );
268 if ( !newLayout )
269 {
270 return nullptr;
271 }
272
273 newLayout->setName( newName );
274 QgsMasterLayoutInterface *l = newLayout.get();
275 if ( !addLayout( newLayout.release() ) )
276 {
277 return nullptr;
278 }
279 else
280 {
281 // cppcheck-suppress returnDanglingLifetime
282 return l;
283 }
284}
285
287{
288 QStringList names;
289 names.reserve( mLayouts.size() );
290 for ( QgsMasterLayoutInterface *l : mLayouts )
291 {
292 names << l->name();
293 }
294 QString name;
295 int id = 1;
296 while ( name.isEmpty() || names.contains( name ) )
297 {
298 switch ( type )
299 {
301 name = tr( "Layout %1" ).arg( id );
302 break;
304 name = tr( "Report %1" ).arg( id );
305 break;
306 }
307 id++;
308 }
309 return name;
310}
311
313{
314 if ( mLayouts.empty() )
315 return true;
316
317 // NOTE: if visitEnter returns false it means "don't visit the layouts", not "abort all further visitations"
318 if ( !visitor->visitEnter( QgsStyleEntityVisitorInterface::Node( QgsStyleEntityVisitorInterface::NodeType::Layouts, QStringLiteral( "layouts" ), tr( "Layouts" ) ) ) )
319 return true;
320
321 for ( QgsMasterLayoutInterface *l : mLayouts )
322 {
323 if ( !l->layoutAccept( visitor ) )
324 return false;
325 }
326
327 if ( !visitor->visitExit( QgsStyleEntityVisitorInterface::Node( QgsStyleEntityVisitorInterface::NodeType::Layouts, QStringLiteral( "layouts" ), tr( "Layouts" ) ) ) )
328 return false;
329
330 return true;
331}
332
333
334
335//
336// QgsLayoutManagerModel
337//
338
340 : QAbstractListModel( parent )
341 , mLayoutManager( manager )
342{
343 connect( mLayoutManager, &QgsLayoutManager::layoutAboutToBeAdded, this, &QgsLayoutManagerModel::layoutAboutToBeAdded );
344 connect( mLayoutManager, &QgsLayoutManager::layoutAdded, this, &QgsLayoutManagerModel::layoutAdded );
345 connect( mLayoutManager, &QgsLayoutManager::layoutAboutToBeRemoved, this, &QgsLayoutManagerModel::layoutAboutToBeRemoved );
346 connect( mLayoutManager, &QgsLayoutManager::layoutRemoved, this, &QgsLayoutManagerModel::layoutRemoved );
347 connect( mLayoutManager, &QgsLayoutManager::layoutRenamed, this, &QgsLayoutManagerModel::layoutRenamed );
348}
349
350int QgsLayoutManagerModel::rowCount( const QModelIndex &parent ) const
351{
352 Q_UNUSED( parent )
353 return ( mLayoutManager ? mLayoutManager->layouts().count() : 0 ) + ( mAllowEmpty ? 1 : 0 );
354}
355
356QVariant QgsLayoutManagerModel::data( const QModelIndex &index, int role ) const
357{
358 if ( index.row() < 0 || index.row() >= rowCount( QModelIndex() ) )
359 return QVariant();
360
361 const bool isEmpty = index.row() == 0 && mAllowEmpty;
362 const int layoutRow = mAllowEmpty ? index.row() - 1 : index.row();
363
364 switch ( role )
365 {
366 case Qt::DisplayRole:
367 case Qt::ToolTipRole:
368 case Qt::EditRole:
369 return !isEmpty && mLayoutManager ? mLayoutManager->layouts().at( layoutRow )->name() : QVariant();
370
371 case static_cast< int >( CustomRole::Layout ):
372 {
373 if ( isEmpty || !mLayoutManager )
374 return QVariant();
375 else if ( QgsLayout *l = dynamic_cast< QgsLayout * >( mLayoutManager->layouts().at( layoutRow ) ) )
376 return QVariant::fromValue( l );
377 else if ( QgsReport *r = dynamic_cast< QgsReport * >( mLayoutManager->layouts().at( layoutRow ) ) )
378 return QVariant::fromValue( r );
379 else
380 return QVariant();
381 }
382
383 case Qt::DecorationRole:
384 {
385 return isEmpty || !mLayoutManager ? QIcon() : mLayoutManager->layouts().at( layoutRow )->icon();
386 }
387
388 default:
389 return QVariant();
390 }
391}
392
393bool QgsLayoutManagerModel::setData( const QModelIndex &index, const QVariant &value, int role )
394{
395 if ( !index.isValid() || role != Qt::EditRole )
396 {
397 return false;
398 }
399 if ( index.row() >= mLayoutManager->layouts().count() )
400 {
401 return false;
402 }
403
404 if ( index.row() == 0 && mAllowEmpty )
405 return false;
406
407 if ( value.toString().isEmpty() )
408 return false;
409
410 QgsMasterLayoutInterface *layout = layoutFromIndex( index );
411 if ( !layout )
412 return false;
413
414 //has name changed?
415 bool changed = layout->name() != value.toString();
416 if ( !changed )
417 return true;
418
419 //check if name already exists
420 QStringList layoutNames;
421 const QList< QgsMasterLayoutInterface * > layouts = QgsProject::instance()->layoutManager()->layouts(); // skip-keyword-check
422 for ( QgsMasterLayoutInterface *l : layouts )
423 {
424 layoutNames << l->name();
425 }
426 if ( layoutNames.contains( value.toString() ) )
427 {
428 //name exists!
429 QMessageBox::warning( nullptr, tr( "Rename Layout" ), tr( "There is already a layout named “%1”." ).arg( value.toString() ) );
430 return false;
431 }
432
433 layout->setName( value.toString() );
434 return true;
435}
436
437Qt::ItemFlags QgsLayoutManagerModel::flags( const QModelIndex &index ) const
438{
439 Qt::ItemFlags flags = QAbstractListModel::flags( index );
440#if 0 // double-click is now used for opening the layout
441 if ( index.isValid() )
442 {
443 return flags | Qt::ItemIsEditable;
444 }
445 else
446 {
447 return flags;
448 }
449#endif
450 return flags;
451}
452
454{
455 if ( index.row() == 0 && mAllowEmpty )
456 return nullptr;
457
458 if ( QgsPrintLayout *l = qobject_cast< QgsPrintLayout * >( qvariant_cast<QObject *>( data( index, static_cast< int >( CustomRole::Layout ) ) ) ) )
459 return l;
460 else if ( QgsReport *r = qobject_cast< QgsReport * >( qvariant_cast<QObject *>( data( index, static_cast< int >( CustomRole::Layout ) ) ) ) )
461 return r;
462 else
463 return nullptr;
464}
465
467{
468 if ( !mLayoutManager )
469 {
470 return QModelIndex();
471 }
472
473 const int r = mLayoutManager->layouts().indexOf( layout );
474 if ( r < 0 )
475 return QModelIndex();
476
477 QModelIndex idx = index( mAllowEmpty ? r + 1 : r, 0, QModelIndex() );
478 if ( idx.isValid() )
479 {
480 return idx;
481 }
482
483 return QModelIndex();
484}
485
487{
488 if ( allowEmpty == mAllowEmpty )
489 return;
490
491 if ( allowEmpty )
492 {
493 beginInsertRows( QModelIndex(), 0, 0 );
494 mAllowEmpty = true;
495 endInsertRows();
496 }
497 else
498 {
499 beginRemoveRows( QModelIndex(), 0, 0 );
500 mAllowEmpty = false;
501 endRemoveRows();
502 }
503}
504
505void QgsLayoutManagerModel::layoutAboutToBeAdded( const QString & )
506{
507 int row = mLayoutManager->layouts().count() + ( mAllowEmpty ? 1 : 0 );
508 beginInsertRows( QModelIndex(), row, row );
509}
510
511void QgsLayoutManagerModel::layoutAboutToBeRemoved( const QString &name )
512{
513 QgsMasterLayoutInterface *l = mLayoutManager->layoutByName( name );
514 int row = mLayoutManager->layouts().indexOf( l ) + ( mAllowEmpty ? 1 : 0 );
515 if ( row >= 0 )
516 beginRemoveRows( QModelIndex(), row, row );
517}
518
519void QgsLayoutManagerModel::layoutAdded( const QString & )
520{
521 endInsertRows();
522}
523
524void QgsLayoutManagerModel::layoutRemoved( const QString & )
525{
526 endRemoveRows();
527}
528
529void QgsLayoutManagerModel::layoutRenamed( QgsMasterLayoutInterface *layout, const QString & )
530{
531 int row = mLayoutManager->layouts().indexOf( layout ) + ( mAllowEmpty ? 1 : 0 );
532 QModelIndex index = createIndex( row, 0 );
533 emit dataChanged( index, index, QVector<int>() << Qt::DisplayRole );
534}
535
536//
537// QgsLayoutManagerProxyModel
538//
539
541 : QSortFilterProxyModel( parent )
542{
543 setDynamicSortFilter( true );
544 sort( 0 );
545 setSortCaseSensitivity( Qt::CaseInsensitive );
546}
547
548bool QgsLayoutManagerProxyModel::lessThan( const QModelIndex &left, const QModelIndex &right ) const
549{
550 const QString leftText = sourceModel()->data( left, Qt::DisplayRole ).toString();
551 const QString rightText = sourceModel()->data( right, Qt::DisplayRole ).toString();
552 if ( leftText.isEmpty() )
553 return true;
554 if ( rightText.isEmpty() )
555 return false;
556
557 return QString::localeAwareCompare( leftText, rightText ) < 0;
558}
559
560bool QgsLayoutManagerProxyModel::filterAcceptsRow( int sourceRow, const QModelIndex &sourceParent ) const
561{
562 QgsLayoutManagerModel *model = qobject_cast< QgsLayoutManagerModel * >( sourceModel() );
563 if ( !model )
564 return false;
565
566 QgsMasterLayoutInterface *layout = model->layoutFromIndex( model->index( sourceRow, 0, sourceParent ) );
567 if ( !layout )
568 return model->allowEmptyLayout();
569
570 if ( !mFilterString.trimmed().isEmpty() )
571 {
572 if ( !layout->name().contains( mFilterString, Qt::CaseInsensitive ) )
573 return false;
574 }
575
576 switch ( layout->layoutType() )
577 {
579 return mFilters & FilterPrintLayouts;
581 return mFilters & FilterReports;
582 }
583 return false;
584}
585
590
592{
593 mFilters = filters;
594 invalidateFilter();
595}
596
598{
599 mFilterString = filter;
600 invalidateFilter();
601}
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.
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).
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.