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