1 /***************************************************************************
2                          qgslayoutmodel.cpp
3                          ------------------
4     begin                : October 2017
5     copyright            : (C) 2017 by Nyall Dawson
6     email                : nyall dot dawson at gmail dot com
7  ***************************************************************************/
8 
9 /***************************************************************************
10  *                                                                         *
11  *   This program is free software; you can redistribute it and/or modify  *
12  *   it under the terms of the GNU General Public License as published by  *
13  *   the Free Software Foundation; either version 2 of the License, or     *
14  *   (at your option) any later version.                                   *
15  *                                                                         *
16  ***************************************************************************/
17 
18 #include "qgslayoutmodel.h"
19 #include "qgslayout.h"
20 #include "qgsapplication.h"
21 #include "qgslogger.h"
22 #include "qgslayoutitemgroup.h"
23 #include <QApplication>
24 #include <QGraphicsItem>
25 #include <QDomDocument>
26 #include <QDomElement>
27 #include <QMimeData>
28 #include <QSettings>
29 #include <QMessageBox>
30 #include <QIcon>
31 
QgsLayoutModel(QgsLayout * layout,QObject * parent)32 QgsLayoutModel::QgsLayoutModel( QgsLayout *layout, QObject *parent )
33   : QAbstractItemModel( parent )
34   , mLayout( layout )
35 {
36 
37 }
38 
itemFromIndex(const QModelIndex & index) const39 QgsLayoutItem *QgsLayoutModel::itemFromIndex( const QModelIndex &index ) const
40 {
41   //try to return the QgsLayoutItem corresponding to a QModelIndex
42   if ( !index.isValid() || index.row() == 0 )
43   {
44     return nullptr;
45   }
46 
47   QgsLayoutItem *item = static_cast<QgsLayoutItem *>( index.internalPointer() );
48   return item;
49 }
50 
index(int row,int column,const QModelIndex & parent) const51 QModelIndex QgsLayoutModel::index( int row, int column,
52                                    const QModelIndex &parent ) const
53 {
54   if ( column < 0 || column >= columnCount() )
55   {
56     //column out of bounds
57     return QModelIndex();
58   }
59 
60   if ( !parent.isValid() && row == 0 )
61   {
62     return createIndex( row, column, nullptr );
63   }
64   else if ( !parent.isValid() && row >= 1 && row < mItemsInScene.size() + 1 )
65   {
66     //return an index for the layout item at this position
67     return createIndex( row, column, mItemsInScene.at( row - 1 ) );
68   }
69 
70   //only top level supported for now
71   return QModelIndex();
72 }
73 
refreshItemsInScene()74 void QgsLayoutModel::refreshItemsInScene()
75 {
76   mItemsInScene.clear();
77 
78   const QList< QGraphicsItem * > items = mLayout->items();
79   //filter paper items from list
80   //TODO - correctly handle grouped item z order placement
81   for ( QgsLayoutItem *item : qgis::as_const( mItemZList ) )
82   {
83     if ( item->type() != QgsLayoutItemRegistry::LayoutPage && items.contains( item ) )
84     {
85       mItemsInScene.push_back( item );
86     }
87   }
88 }
89 
parent(const QModelIndex & index) const90 QModelIndex QgsLayoutModel::parent( const QModelIndex &index ) const
91 {
92   Q_UNUSED( index )
93 
94   //all items are top level for now
95   return QModelIndex();
96 }
97 
rowCount(const QModelIndex & parent) const98 int QgsLayoutModel::rowCount( const QModelIndex &parent ) const
99 {
100   if ( !parent.isValid() )
101   {
102     return mItemsInScene.size() + 1;
103   }
104 
105 #if 0
106   QGraphicsItem *parentItem = itemFromIndex( parent );
107 
108   if ( parentItem )
109   {
110     // return child count for item
111     return 0;
112   }
113 #endif
114 
115   //no children for now
116   return 0;
117 }
118 
columnCount(const QModelIndex & parent) const119 int QgsLayoutModel::columnCount( const QModelIndex &parent ) const
120 {
121   Q_UNUSED( parent )
122   return 3;
123 }
124 
data(const QModelIndex & index,int role) const125 QVariant QgsLayoutModel::data( const QModelIndex &index, int role ) const
126 {
127   if ( !index.isValid() )
128     return QVariant();
129 
130   QgsLayoutItem *item = itemFromIndex( index );
131   if ( !item )
132   {
133     return QVariant();
134   }
135 
136   switch ( role )
137   {
138     case Qt::DisplayRole:
139       if ( index.column() == ItemId )
140       {
141         return item->displayName();
142       }
143       else
144       {
145         return QVariant();
146       }
147 
148     case Qt::DecorationRole:
149       if ( index.column() == ItemId )
150       {
151         return item->icon();
152       }
153       else
154       {
155         return QVariant();
156       }
157 
158     case Qt::EditRole:
159       if ( index.column() == ItemId )
160       {
161         return item->id();
162       }
163       else
164       {
165         return QVariant();
166       }
167 
168     case Qt::UserRole:
169       //store item uuid in userrole so we can later get the QModelIndex for a specific item
170       return item->uuid();
171     case Qt::UserRole+1:
172       //user role stores reference in column object
173       return QVariant::fromValue( qobject_cast<QObject *>( item ) );
174 
175     case Qt::TextAlignmentRole:
176       return Qt::AlignLeft & Qt::AlignVCenter;
177 
178     case Qt::CheckStateRole:
179       switch ( index.column() )
180       {
181         case Visibility:
182           //column 0 is visibility of item
183           return item->isVisible() ? Qt::Checked : Qt::Unchecked;
184         case LockStatus:
185           //column 1 is locked state of item
186           return item->isLocked() ? Qt::Checked : Qt::Unchecked;
187         default:
188           return QVariant();
189       }
190 
191     default:
192       return QVariant();
193   }
194 }
195 
setData(const QModelIndex & index,const QVariant & value,int role=Qt::EditRole)196 bool QgsLayoutModel::setData( const QModelIndex &index, const QVariant &value, int role = Qt::EditRole )
197 {
198   Q_UNUSED( role )
199 
200   if ( !index.isValid() )
201     return false;
202 
203   QgsLayoutItem *item = itemFromIndex( index );
204   if ( !item )
205   {
206     return false;
207   }
208 
209   switch ( index.column() )
210   {
211     case Visibility:
212       //first column is item visibility
213       item->setVisibility( value.toBool() );
214       return true;
215 
216     case LockStatus:
217       //second column is item lock state
218       item->setLocked( value.toBool() );
219       return true;
220 
221     case ItemId:
222       //last column is item id
223       item->setId( value.toString() );
224       return true;
225   }
226 
227   return false;
228 }
229 
headerData(int section,Qt::Orientation orientation,int role) const230 QVariant QgsLayoutModel::headerData( int section, Qt::Orientation orientation, int role ) const
231 {
232   switch ( role )
233   {
234     case Qt::DisplayRole:
235     {
236       if ( section == ItemId )
237       {
238         return tr( "Item" );
239       }
240       return QVariant();
241     }
242 
243     case Qt::DecorationRole:
244     {
245       if ( section == Visibility )
246       {
247         return QVariant::fromValue( QgsApplication::getThemeIcon( QStringLiteral( "/mActionShowAllLayersGray.svg" ) ) );
248       }
249       else if ( section == LockStatus )
250       {
251         return QVariant::fromValue( QgsApplication::getThemeIcon( QStringLiteral( "/lockedGray.svg" ) ) );
252       }
253 
254       return QVariant();
255     }
256 
257     case Qt::TextAlignmentRole:
258       return Qt::AlignLeft & Qt::AlignVCenter;
259 
260     default:
261       return QAbstractItemModel::headerData( section, orientation, role );
262   }
263 
264 }
265 
supportedDropActions() const266 Qt::DropActions QgsLayoutModel::supportedDropActions() const
267 {
268   return Qt::MoveAction;
269 }
270 
mimeTypes() const271 QStringList QgsLayoutModel::mimeTypes() const
272 {
273   QStringList types;
274   types << QStringLiteral( "application/x-vnd.qgis.qgis.composeritemid" );
275   return types;
276 }
277 
mimeData(const QModelIndexList & indexes) const278 QMimeData *QgsLayoutModel::mimeData( const QModelIndexList &indexes ) const
279 {
280   QMimeData *mimeData = new QMimeData();
281   QByteArray encodedData;
282 
283   QDataStream stream( &encodedData, QIODevice::WriteOnly );
284 
285   for ( const QModelIndex &index : indexes )
286   {
287     if ( index.isValid() && index.column() == ItemId )
288     {
289       QgsLayoutItem *item = itemFromIndex( index );
290       if ( !item )
291       {
292         continue;
293       }
294       QString text = item->uuid();
295       stream << text;
296     }
297   }
298 
299   mimeData->setData( QStringLiteral( "application/x-vnd.qgis.qgis.composeritemid" ), encodedData );
300   return mimeData;
301 }
302 
zOrderDescending(QgsLayoutItem * item1,QgsLayoutItem * item2)303 bool zOrderDescending( QgsLayoutItem *item1, QgsLayoutItem *item2 )
304 {
305   return item1->zValue() > item2->zValue();
306 }
307 
dropMimeData(const QMimeData * data,Qt::DropAction action,int row,int column,const QModelIndex & parent)308 bool QgsLayoutModel::dropMimeData( const QMimeData *data,
309                                    Qt::DropAction action, int row, int column, const QModelIndex &parent )
310 {
311   if ( column != ItemId && column != -1 )
312   {
313     return false;
314   }
315 
316   if ( action == Qt::IgnoreAction )
317   {
318     return true;
319   }
320 
321   if ( !data->hasFormat( QStringLiteral( "application/x-vnd.qgis.qgis.composeritemid" ) ) )
322   {
323     return false;
324   }
325 
326   if ( parent.isValid() )
327   {
328     return false;
329   }
330 
331   int beginRow = row != -1 ? row : rowCount( QModelIndex() );
332 
333   QByteArray encodedData = data->data( QStringLiteral( "application/x-vnd.qgis.qgis.composeritemid" ) );
334   QDataStream stream( &encodedData, QIODevice::ReadOnly );
335   QList<QgsLayoutItem *> droppedItems;
336   int rows = 0;
337 
338   while ( !stream.atEnd() )
339   {
340     QString text;
341     stream >> text;
342     QgsLayoutItem *item = mLayout->itemByUuid( text );
343     if ( item )
344     {
345       droppedItems << item;
346       ++rows;
347     }
348   }
349 
350   if ( droppedItems.empty() )
351   {
352     //no dropped items
353     return false;
354   }
355 
356   //move dropped items
357 
358   //first sort them by z-order
359   std::sort( droppedItems.begin(), droppedItems.end(), zOrderDescending );
360 
361   //calculate position in z order list to drop items at
362   int destPos = 0;
363   if ( beginRow < rowCount() )
364   {
365     QgsLayoutItem *itemBefore = mItemsInScene.at( beginRow - 1 );
366     destPos = mItemZList.indexOf( itemBefore );
367   }
368   else
369   {
370     //place items at end
371     destPos = mItemZList.size();
372   }
373 
374   //calculate position to insert moved rows to
375   int insertPos = destPos;
376   for ( QgsLayoutItem *item : qgis::as_const( droppedItems ) )
377   {
378     int listPos = mItemZList.indexOf( item );
379     if ( listPos == -1 )
380     {
381       //should be impossible
382       continue;
383     }
384 
385     if ( listPos < destPos )
386     {
387       insertPos--;
388     }
389   }
390 
391   //remove rows from list
392   auto itemIt = droppedItems.begin();
393   for ( ; itemIt != droppedItems.end(); ++itemIt )
394   {
395     mItemZList.removeOne( *itemIt );
396   }
397 
398   //insert items
399   itemIt = droppedItems.begin();
400   for ( ; itemIt != droppedItems.end(); ++itemIt )
401   {
402     mItemZList.insert( insertPos, *itemIt );
403     insertPos++;
404   }
405 
406   rebuildSceneItemList();
407 
408   mLayout->updateZValues( true );
409 
410   return true;
411 }
412 
removeRows(int row,int count,const QModelIndex & parent)413 bool QgsLayoutModel::removeRows( int row, int count, const QModelIndex &parent )
414 {
415   Q_UNUSED( count )
416   if ( parent.isValid() )
417   {
418     return false;
419   }
420 
421   if ( row >= rowCount() )
422   {
423     return false;
424   }
425 
426   //do nothing - moves are handled by the dropMimeData method
427   return true;
428 }
429 
430 ///@cond PRIVATE
clear()431 void QgsLayoutModel::clear()
432 {
433   //totally reset model
434   beginResetModel();
435   mItemZList.clear();
436   refreshItemsInScene();
437   endResetModel();
438 }
439 
zOrderListSize() const440 int QgsLayoutModel::zOrderListSize() const
441 {
442   return mItemZList.size();
443 }
444 
rebuildZList()445 void QgsLayoutModel::rebuildZList()
446 {
447   QList<QgsLayoutItem *> sortedList;
448   //rebuild the item z order list based on the current zValues of items in the scene
449 
450   //get items in descending zValue order
451   const QList<QGraphicsItem *> itemList = mLayout->items( Qt::DescendingOrder );
452   for ( QGraphicsItem *item : itemList )
453   {
454     if ( QgsLayoutItem *layoutItem = dynamic_cast<QgsLayoutItem *>( item ) )
455     {
456       if ( layoutItem->type() != QgsLayoutItemRegistry::LayoutPage )
457       {
458         sortedList.append( layoutItem );
459       }
460     }
461   }
462 
463   mItemZList = sortedList;
464   rebuildSceneItemList();
465 }
466 ///@endcond
467 
rebuildSceneItemList()468 void QgsLayoutModel::rebuildSceneItemList()
469 {
470   //step through the z list and rebuild the items in scene list,
471   //emitting signals as required
472   int row = 0;
473   const QList< QGraphicsItem * > items = mLayout->items();
474   for ( QgsLayoutItem *item : qgis::as_const( mItemZList ) )
475   {
476     if ( item->type() == QgsLayoutItemRegistry::LayoutPage || !items.contains( item ) )
477     {
478       //item not in scene, skip it
479       continue;
480     }
481 
482     int sceneListPos = mItemsInScene.indexOf( item );
483     if ( sceneListPos == row )
484     {
485       //already in list in correct position, nothing to do
486 
487     }
488     else if ( sceneListPos != -1 )
489     {
490       //in list, but in wrong spot
491       beginMoveRows( QModelIndex(), sceneListPos + 1, sceneListPos + 1, QModelIndex(), row + 1 );
492       mItemsInScene.removeAt( sceneListPos );
493       mItemsInScene.insert( row, item );
494       endMoveRows();
495     }
496     else
497     {
498       //needs to be inserted into list
499       beginInsertRows( QModelIndex(), row + 1, row + 1 );
500       mItemsInScene.insert( row, item );
501       endInsertRows();
502     }
503     row++;
504   }
505 }
506 ///@cond PRIVATE
addItemAtTop(QgsLayoutItem * item)507 void QgsLayoutModel::addItemAtTop( QgsLayoutItem *item )
508 {
509   mItemZList.push_front( item );
510   refreshItemsInScene();
511   item->setZValue( mItemZList.size() );
512 }
513 
removeItem(QgsLayoutItem * item)514 void QgsLayoutModel::removeItem( QgsLayoutItem *item )
515 {
516   if ( !item )
517   {
518     //nothing to do
519     return;
520   }
521 
522   int pos = mItemZList.indexOf( item );
523   if ( pos == -1 )
524   {
525     //item not in z list, nothing to do
526     return;
527   }
528 
529   //need to get QModelIndex of item
530   QModelIndex itemIndex = indexForItem( item );
531   if ( !itemIndex.isValid() )
532   {
533     //removing an item not in the scene (e.g., deleted item)
534     //we need to remove it from the list, but don't need to call
535     //beginRemoveRows or endRemoveRows since the item was not used by the model
536     mItemZList.removeAt( pos );
537     refreshItemsInScene();
538     return;
539   }
540 
541   //remove item from model
542   int row = itemIndex.row();
543   beginRemoveRows( QModelIndex(), row, row );
544   mItemZList.removeAt( pos );
545   refreshItemsInScene();
546   endRemoveRows();
547 }
548 
setItemRemoved(QgsLayoutItem * item)549 void QgsLayoutModel::setItemRemoved( QgsLayoutItem *item )
550 {
551   if ( !item )
552   {
553     //nothing to do
554     return;
555   }
556 
557   int pos = mItemZList.indexOf( item );
558   if ( pos == -1 )
559   {
560     //item not in z list, nothing to do
561     return;
562   }
563 
564   //need to get QModelIndex of item
565   QModelIndex itemIndex = indexForItem( item );
566   if ( !itemIndex.isValid() )
567   {
568     return;
569   }
570 
571   //removing item
572   int row = itemIndex.row();
573   beginRemoveRows( QModelIndex(), row, row );
574   mLayout->removeItem( item );
575   refreshItemsInScene();
576   endRemoveRows();
577 }
578 
updateItemDisplayName(QgsLayoutItem * item)579 void QgsLayoutModel::updateItemDisplayName( QgsLayoutItem *item )
580 {
581   if ( !item )
582   {
583     //nothing to do
584     return;
585   }
586 
587   //need to get QModelIndex of item
588   QModelIndex itemIndex = indexForItem( item, ItemId );
589   if ( !itemIndex.isValid() )
590   {
591     return;
592   }
593 
594   //emit signal for item id change
595   emit dataChanged( itemIndex, itemIndex );
596 }
597 
updateItemLockStatus(QgsLayoutItem * item)598 void QgsLayoutModel::updateItemLockStatus( QgsLayoutItem *item )
599 {
600   if ( !item )
601   {
602     //nothing to do
603     return;
604   }
605 
606   //need to get QModelIndex of item
607   QModelIndex itemIndex = indexForItem( item, LockStatus );
608   if ( !itemIndex.isValid() )
609   {
610     return;
611   }
612 
613   //emit signal for item lock status change
614   emit dataChanged( itemIndex, itemIndex );
615 }
616 
updateItemVisibility(QgsLayoutItem * item)617 void QgsLayoutModel::updateItemVisibility( QgsLayoutItem *item )
618 {
619   if ( !item )
620   {
621     //nothing to do
622     return;
623   }
624 
625   //need to get QModelIndex of item
626   QModelIndex itemIndex = indexForItem( item, Visibility );
627   if ( !itemIndex.isValid() )
628   {
629     return;
630   }
631 
632   //emit signal for item visibility change
633   emit dataChanged( itemIndex, itemIndex );
634 }
635 
updateItemSelectStatus(QgsLayoutItem * item)636 void QgsLayoutModel::updateItemSelectStatus( QgsLayoutItem *item )
637 {
638   if ( !item )
639   {
640     //nothing to do
641     return;
642   }
643 
644   //need to get QModelIndex of item
645   QModelIndex itemIndex = indexForItem( item, ItemId );
646   if ( !itemIndex.isValid() )
647   {
648     return;
649   }
650 
651   //emit signal for item visibility change
652   emit dataChanged( itemIndex, itemIndex );
653 }
654 
reorderItemUp(QgsLayoutItem * item)655 bool QgsLayoutModel::reorderItemUp( QgsLayoutItem *item )
656 {
657   if ( !item )
658   {
659     return false;
660   }
661 
662   if ( mItemsInScene.at( 0 ) == item )
663   {
664     //item is already topmost item present in scene, nothing to do
665     return false;
666   }
667 
668   //move item in z list
669   QMutableListIterator<QgsLayoutItem *> it( mItemZList );
670   if ( ! it.findNext( item ) )
671   {
672     //can't find item in z list, nothing to do
673     return false;
674   }
675 
676   const QList< QGraphicsItem * > sceneItems = mLayout->items();
677 
678   it.remove();
679   while ( it.hasPrevious() )
680   {
681     //search through item z list to find previous item which is present in the scene
682     it.previous();
683     if ( it.value() && sceneItems.contains( it.value() ) )
684     {
685       break;
686     }
687   }
688   it.insert( item );
689 
690   //also move item in scene items z list and notify of model changes
691   QModelIndex itemIndex = indexForItem( item );
692   if ( !itemIndex.isValid() )
693   {
694     return true;
695   }
696 
697   //move item up in scene list
698   int row = itemIndex.row();
699   beginMoveRows( QModelIndex(), row, row, QModelIndex(), row - 1 );
700   refreshItemsInScene();
701   endMoveRows();
702   return true;
703 }
704 
reorderItemDown(QgsLayoutItem * item)705 bool QgsLayoutModel::reorderItemDown( QgsLayoutItem *item )
706 {
707   if ( !item )
708   {
709     return false;
710   }
711 
712   if ( mItemsInScene.last() == item )
713   {
714     //item is already lowest item present in scene, nothing to do
715     return false;
716   }
717 
718   //move item in z list
719   QMutableListIterator<QgsLayoutItem *> it( mItemZList );
720   if ( ! it.findNext( item ) )
721   {
722     //can't find item in z list, nothing to do
723     return false;
724   }
725 
726   const QList< QGraphicsItem * > sceneItems = mLayout->items();
727   it.remove();
728   while ( it.hasNext() )
729   {
730     //search through item z list to find next item which is present in the scene
731     //(deleted items still exist in the z list so that they can be restored to their correct stacking order,
732     //but since they are not in the scene they should be ignored here)
733     it.next();
734     if ( it.value() && sceneItems.contains( it.value() ) )
735     {
736       break;
737     }
738   }
739   it.insert( item );
740 
741   //also move item in scene items z list and notify of model changes
742   QModelIndex itemIndex = indexForItem( item );
743   if ( !itemIndex.isValid() )
744   {
745     return true;
746   }
747 
748   //move item down in scene list
749   int row = itemIndex.row();
750   beginMoveRows( QModelIndex(), row, row, QModelIndex(), row + 2 );
751   refreshItemsInScene();
752   endMoveRows();
753   return true;
754 }
755 
reorderItemToTop(QgsLayoutItem * item)756 bool QgsLayoutModel::reorderItemToTop( QgsLayoutItem *item )
757 {
758   if ( !item || !mItemsInScene.contains( item ) )
759   {
760     return false;
761   }
762 
763   if ( mItemsInScene.at( 0 ) == item )
764   {
765     //item is already topmost item present in scene, nothing to do
766     return false;
767   }
768 
769   //move item in z list
770   QMutableListIterator<QgsLayoutItem *> it( mItemZList );
771   if ( it.findNext( item ) )
772   {
773     it.remove();
774   }
775   mItemZList.push_front( item );
776 
777   //also move item in scene items z list and notify of model changes
778   QModelIndex itemIndex = indexForItem( item );
779   if ( !itemIndex.isValid() )
780   {
781     return true;
782   }
783 
784   //move item to top
785   int row = itemIndex.row();
786   beginMoveRows( QModelIndex(), row, row, QModelIndex(), 1 );
787   refreshItemsInScene();
788   endMoveRows();
789   return true;
790 }
791 
reorderItemToBottom(QgsLayoutItem * item)792 bool QgsLayoutModel::reorderItemToBottom( QgsLayoutItem *item )
793 {
794   if ( !item || !mItemsInScene.contains( item ) )
795   {
796     return false;
797   }
798 
799   if ( mItemsInScene.last() == item )
800   {
801     //item is already lowest item present in scene, nothing to do
802     return false;
803   }
804 
805   //move item in z list
806   QMutableListIterator<QgsLayoutItem *> it( mItemZList );
807   if ( it.findNext( item ) )
808   {
809     it.remove();
810   }
811   mItemZList.push_back( item );
812 
813   //also move item in scene items z list and notify of model changes
814   QModelIndex itemIndex = indexForItem( item );
815   if ( !itemIndex.isValid() )
816   {
817     return true;
818   }
819 
820   //move item to bottom
821   int row = itemIndex.row();
822   beginMoveRows( QModelIndex(), row, row, QModelIndex(), rowCount() );
823   refreshItemsInScene();
824   endMoveRows();
825   return true;
826 }
827 
findItemAbove(QgsLayoutItem * item) const828 QgsLayoutItem *QgsLayoutModel::findItemAbove( QgsLayoutItem *item ) const
829 {
830   //search item z list for selected item
831   QListIterator<QgsLayoutItem *> it( mItemZList );
832   it.toBack();
833   if ( it.findPrevious( item ) )
834   {
835     //move position to before selected item
836     while ( it.hasPrevious() )
837     {
838       //now find previous item, since list is sorted from lowest->highest items
839       if ( it.hasPrevious() && !it.peekPrevious()->isGroupMember() )
840       {
841         return it.previous();
842       }
843       it.previous();
844     }
845   }
846   return nullptr;
847 }
848 
findItemBelow(QgsLayoutItem * item) const849 QgsLayoutItem *QgsLayoutModel::findItemBelow( QgsLayoutItem *item ) const
850 {
851   //search item z list for selected item
852   QListIterator<QgsLayoutItem *> it( mItemZList );
853   if ( it.findNext( item ) )
854   {
855     //return next item (list is sorted from lowest->highest items)
856     while ( it.hasNext() )
857     {
858       if ( !it.peekNext()->isGroupMember() )
859       {
860         return it.next();
861       }
862       it.next();
863     }
864   }
865   return nullptr;
866 }
867 
zOrderList()868 QList<QgsLayoutItem *> &QgsLayoutModel::zOrderList()
869 {
870   return mItemZList;
871 }
872 
873 ///@endcond
874 
flags(const QModelIndex & index) const875 Qt::ItemFlags QgsLayoutModel::flags( const QModelIndex &index ) const
876 {
877   Qt::ItemFlags flags = QAbstractItemModel::flags( index );
878 
879   if ( ! index.isValid() )
880   {
881     return flags | Qt::ItemIsDropEnabled;
882   }
883 
884   if ( index.row() == 0 )
885   {
886     return flags | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
887   }
888   else
889   {
890     switch ( index.column() )
891     {
892       case Visibility:
893       case LockStatus:
894         return flags | Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEditable | Qt::ItemIsDragEnabled;
895       case ItemId:
896         return flags | Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsDragEnabled;
897       default:
898         return flags | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
899     }
900   }
901 }
902 
indexForItem(QgsLayoutItem * item,const int column)903 QModelIndex QgsLayoutModel::indexForItem( QgsLayoutItem *item, const int column )
904 {
905   if ( !item )
906   {
907     return QModelIndex();
908   }
909 
910   int row = mItemsInScene.indexOf( item );
911   if ( row == -1 )
912   {
913     //not found
914     return QModelIndex();
915   }
916 
917   return index( row + 1, column );
918 }
919 
920 ///@cond PRIVATE
setSelected(const QModelIndex & index)921 void QgsLayoutModel::setSelected( const QModelIndex &index )
922 {
923   QgsLayoutItem *item = itemFromIndex( index );
924   if ( !item )
925   {
926     return;
927   }
928 
929   // find top level group this item is contained within, and mark the group as selected
930   QgsLayoutItemGroup *group = item->parentGroup();
931   while ( group && group->parentGroup() )
932   {
933     group = group->parentGroup();
934   }
935 
936   // but the actual main selected item is the item itself (allows editing of item properties)
937   mLayout->setSelectedItem( item );
938 
939   if ( group && group != item )
940     group->setSelected( true );
941 }
942 ///@endcond
943 
944 //
945 // QgsLayoutProxyModel
946 //
947 
QgsLayoutProxyModel(QgsLayout * layout,QObject * parent)948 QgsLayoutProxyModel::QgsLayoutProxyModel( QgsLayout *layout, QObject *parent )
949   : QSortFilterProxyModel( parent )
950   , mLayout( layout )
951   , mItemTypeFilter( QgsLayoutItemRegistry::LayoutItem )
952 {
953   if ( mLayout )
954     setSourceModel( mLayout->itemsModel() );
955 
956   setDynamicSortFilter( true );
957   setSortLocaleAware( true );
958   sort( QgsLayoutModel::ItemId );
959 }
960 
lessThan(const QModelIndex & left,const QModelIndex & right) const961 bool QgsLayoutProxyModel::lessThan( const QModelIndex &left, const QModelIndex &right ) const
962 {
963   const QString leftText = sourceModel()->data( left, Qt::DisplayRole ).toString();
964   const QString rightText = sourceModel()->data( right, Qt::DisplayRole ).toString();
965   if ( leftText.isEmpty() )
966     return true;
967   if ( rightText.isEmpty() )
968     return false;
969 
970   //sort by item id
971   const QgsLayoutItem *item1 = itemFromSourceIndex( left );
972   const QgsLayoutItem *item2 = itemFromSourceIndex( right );
973   if ( !item1 )
974     return false;
975 
976   if ( !item2 )
977     return true;
978 
979   return QString::localeAwareCompare( item1->displayName(), item2->displayName() ) < 0;
980 }
981 
itemFromSourceIndex(const QModelIndex & sourceIndex) const982 QgsLayoutItem *QgsLayoutProxyModel::itemFromSourceIndex( const QModelIndex &sourceIndex ) const
983 {
984   if ( !mLayout )
985     return nullptr;
986 
987   //get column corresponding to an index from the source model
988   QVariant itemAsVariant = sourceModel()->data( sourceIndex, Qt::UserRole + 1 );
989   return qobject_cast<QgsLayoutItem *>( itemAsVariant.value<QObject *>() );
990 }
991 
setAllowEmptyItem(bool allowEmpty)992 void QgsLayoutProxyModel::setAllowEmptyItem( bool allowEmpty )
993 {
994   mAllowEmpty = allowEmpty;
995   invalidateFilter();
996 }
997 
allowEmptyItem() const998 bool QgsLayoutProxyModel::allowEmptyItem() const
999 {
1000   return mAllowEmpty;
1001 }
1002 
setItemFlags(QgsLayoutItem::Flags flags)1003 void QgsLayoutProxyModel::setItemFlags( QgsLayoutItem::Flags flags )
1004 {
1005   mItemFlags = flags;
1006   invalidateFilter();
1007 }
1008 
itemFlags() const1009 QgsLayoutItem::Flags QgsLayoutProxyModel::itemFlags() const
1010 {
1011   return mItemFlags;
1012 }
1013 
setFilterType(QgsLayoutItemRegistry::ItemType filter)1014 void QgsLayoutProxyModel::setFilterType( QgsLayoutItemRegistry::ItemType filter )
1015 {
1016   mItemTypeFilter = filter;
1017   invalidate();
1018 }
1019 
setExceptedItemList(const QList<QgsLayoutItem * > & items)1020 void QgsLayoutProxyModel::setExceptedItemList( const QList< QgsLayoutItem *> &items )
1021 {
1022   if ( mExceptedList == items )
1023     return;
1024 
1025   mExceptedList = items;
1026   invalidateFilter();
1027 }
1028 
filterAcceptsRow(int sourceRow,const QModelIndex & sourceParent) const1029 bool QgsLayoutProxyModel::filterAcceptsRow( int sourceRow, const QModelIndex &sourceParent ) const
1030 {
1031   //get QgsComposerItem corresponding to row
1032   QModelIndex index = sourceModel()->index( sourceRow, 0, sourceParent );
1033   QgsLayoutItem *item = itemFromSourceIndex( index );
1034 
1035   if ( !item )
1036     return mAllowEmpty;
1037 
1038   // specific exceptions
1039   if ( mExceptedList.contains( item ) )
1040     return false;
1041 
1042   // filter by type
1043   if ( mItemTypeFilter != QgsLayoutItemRegistry::LayoutItem && item->type() != mItemTypeFilter )
1044     return false;
1045 
1046   if ( mItemFlags && !( item->itemFlags() & mItemFlags ) )
1047   {
1048     return false;
1049   }
1050 
1051   return true;
1052 }
1053