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