1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2009-03-25
7  * Description : Tree View for album models
8  *
9  * Copyright (C) 2009-2011 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
10  * Copyright (C) 2010-2011 by Andi Clemens <andi dot clemens at gmail dot com>
11  * Copyright (C) 2014      by Mohamed_Anwer <m_dot_anwer at gmx dot com>
12  * Copyright (C) 2014      by Michael G. Hansen <mike at mghansen dot de>
13  * Copyright (C) 2009-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
14  *
15  * This program is free software; you can redistribute it
16  * and/or modify it under the terms of the GNU General
17  * Public License as published by the Free Software Foundation;
18  * either version 2, or (at your option)
19  * any later version.
20  *
21  * This program is distributed in the hope that it will be useful,
22  * but WITHOUT ANY WARRANTY; without even the implied warranty of
23  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24  * GNU General Public License for more details.
25  *
26  * ============================================================ */
27 
28 #include "abstractalbumtreeview_p.h"
29 
30 // KDE includes
31 
32 #include <kconfiggroup.h>
33 
34 namespace Digikam
35 {
36 
AbstractAlbumTreeView(QWidget * const parent,Flags flags)37 AbstractAlbumTreeView::AbstractAlbumTreeView(QWidget* const parent, Flags flags)
38     : QTreeView             (parent),
39       StateSavingObject     (this),
40       m_albumModel          (nullptr),
41       m_albumFilterModel    (nullptr),
42       m_dragDropHandler     (nullptr),
43       m_lastScrollBarValue  (0),
44       m_checkOnMiddleClick  (false),
45       m_restoreCheckState   (false),
46       m_flags               (flags),
47       d                     (new Private)
48 {
49     if (flags & CreateDefaultDelegate)
50     {
51         d->delegate = new AlbumTreeViewDelegate(this);
52         setItemDelegate(d->delegate);
53         setUniformRowHeights(true);
54     }
55 
56     d->resizeColumnsTimer = new QTimer(this);
57     d->resizeColumnsTimer->setInterval(200);
58     d->resizeColumnsTimer->setSingleShot(true);
59 
60     d->contextMenuIcon  = QIcon::fromTheme(QLatin1String("digikam")).pixmap(style()->pixelMetric(QStyle::PM_SmallIconSize));
61     d->contextMenuTitle = i18n("Context menu");
62 
63     connect(d->resizeColumnsTimer, SIGNAL(timeout()),
64             this, SLOT(adaptColumnsToContent()));
65 
66     connect(ApplicationSettings::instance(), SIGNAL(setupChanged()),
67             this, SLOT(albumSettingsChanged()));
68 
69     connect(this, SIGNAL(currentAlbumChanged(Album*)),
70             this, SLOT(currentAlbumChangedForBackupSelection(Album*)));
71 
72     if (flags & CreateDefaultFilterModel)
73     {
74         setAlbumFilterModel(new AlbumFilterModel(this));
75     }
76 
77     setSortingEnabled(true);
78     albumSettingsChanged();
79 }
80 
~AbstractAlbumTreeView()81 AbstractAlbumTreeView::~AbstractAlbumTreeView()
82 {
83     delete d;
84 }
85 
setAlbumModel(AbstractSpecificAlbumModel * const model)86 void AbstractAlbumTreeView::setAlbumModel(AbstractSpecificAlbumModel* const model)
87 {
88     if (m_albumModel == model)
89     {
90         return;
91     }
92 
93     if (m_albumModel)
94     {
95         disconnect(m_albumModel, nullptr, this, nullptr);
96     }
97 
98     m_albumModel = model;
99 
100     if (m_albumFilterModel)
101     {
102         m_albumFilterModel->setSourceAlbumModel(m_albumModel);
103     }
104 
105     if (m_albumModel)
106     {
107         if (!m_albumModel->rootAlbum())
108         {
109             connect(m_albumModel, SIGNAL(rootAlbumAvailable()),
110                     this, SLOT(slotRootAlbumAvailable()));
111         }
112 
113         if (m_albumFilterModel)
114         {
115             expand(m_albumFilterModel->rootAlbumIndex());
116         }
117     }
118 }
119 
setAlbumFilterModel(AlbumFilterModel * const filterModel)120 void AbstractAlbumTreeView::setAlbumFilterModel(AlbumFilterModel* const filterModel)
121 {
122     if (filterModel == m_albumFilterModel)
123     {
124         return;
125     }
126 
127     if (m_albumFilterModel)
128     {
129         disconnect(m_albumFilterModel);
130     }
131 
132     if (selectionModel())
133     {
134         disconnect(selectionModel());
135     }
136 
137     m_albumFilterModel = filterModel;
138     setModel(m_albumFilterModel);
139 
140     if (m_albumFilterModel)
141     {
142         m_albumFilterModel->setSourceAlbumModel(m_albumModel);
143 
144         connect(m_albumFilterModel, SIGNAL(searchTextSettingsAboutToChange(bool,bool)),
145                 this, SLOT(slotSearchTextSettingsAboutToChange(bool,bool)));
146 
147         connect(m_albumFilterModel, SIGNAL(searchTextSettingsChanged(bool,bool)),
148                 this, SLOT(slotSearchTextSettingsChanged(bool,bool)));
149 
150         // NOTE: When only single selection was available, everything was
151         //       implemented using currentAlbum() which was equal with selectedAlbum()
152         //       after enabling multiple selection they are no longer the same
153         //       and some options must use selected others only currentAlbum
154         //       Now AlbumManager implementation is a little bit of mess
155         //       because selected are now currentAlbums()...
156 
157         connect(selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)),
158                 this, SLOT(slotCurrentChanged()));
159 
160         connect(selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)),
161                 this, SLOT(slotSelectionChanged()));
162 
163         connect(m_albumFilterModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
164                 this, SLOT(adaptColumnsOnDataChange(QModelIndex,QModelIndex)));
165 
166         connect(m_albumFilterModel, SIGNAL(rowsInserted(QModelIndex,int,int)),
167                 this, SLOT(adaptColumnsOnRowChange(QModelIndex,int,int)));
168 
169         connect(m_albumFilterModel, SIGNAL(rowsRemoved(QModelIndex,int,int)),
170                 this, SLOT(adaptColumnsOnRowChange(QModelIndex,int,int)));
171 
172         connect(m_albumFilterModel, SIGNAL(layoutChanged()),
173                 this, SLOT(adaptColumnsOnLayoutChange()));
174 
175         connect(horizontalScrollBar(), SIGNAL(valueChanged(int)),
176                 this, SLOT(slotScrollBarValueChanged(int)));
177 
178         connect(horizontalScrollBar(), SIGNAL(actionTriggered(int)),
179                 this, SLOT(slotScrollBarActionTriggered(int)));
180 
181         adaptColumnsToContent();
182 
183         if (m_albumModel)
184         {
185             expand(m_albumFilterModel->rootAlbumIndex());
186         }
187 /*
188         m_albumFilterModel->setDynamicSortFilter(true);
189 */
190     }
191 }
192 
albumModel() const193 AbstractSpecificAlbumModel* AbstractAlbumTreeView::albumModel() const
194 {
195     return m_albumModel;
196 }
197 
albumFilterModel() const198 AlbumFilterModel* AbstractAlbumTreeView::albumFilterModel() const
199 {
200     return m_albumFilterModel;
201 }
202 
setExpandOnSingleClick(const bool doThat)203 void AbstractAlbumTreeView::setExpandOnSingleClick(const bool doThat)
204 {
205     d->expandOnSingleClick = doThat;
206 }
207 
setExpandNewCurrentItem(const bool doThat)208 void AbstractAlbumTreeView::setExpandNewCurrentItem(const bool doThat)
209 {
210     d->expandNewCurrent = doThat;
211 }
212 
setSelectAlbumOnClick(const bool selectOnClick)213 void AbstractAlbumTreeView::setSelectAlbumOnClick(const bool selectOnClick)
214 {
215     d->selectAlbumOnClick = selectOnClick;
216 }
217 
indexVisuallyAt(const QPoint & p)218 QModelIndex AbstractAlbumTreeView::indexVisuallyAt(const QPoint& p)
219 {
220     if (viewport()->rect().contains(p))
221     {
222         const QModelIndex index = indexAt(p);
223 
224         if (index.isValid() && visualRect(index).contains(p))
225         {
226             return index;
227         }
228     }
229 
230     return QModelIndex();
231 }
232 
233 template<class A>
currentAlbums()234 QList<A*> AbstractAlbumTreeView::currentAlbums()
235 {
236     QList<A*> albums;
237     const QList<Album*> currentAl = AlbumManager::instance()->currentAlbums();
238 
239     for (QList<Album*>::const_iterator it = currentAl.constBegin() ; it != currentAl.constEnd() ; ++it)
240     {
241         A* const item = dynamic_cast<A*>(*it);
242 
243         if (item)
244         {
245             albums.append(item);
246         }
247     }
248 
249     return albums;
250 }
251 
slotSearchTextSettingsAboutToChange(bool searched,bool willSearch)252 void AbstractAlbumTreeView::slotSearchTextSettingsAboutToChange(bool searched, bool willSearch)
253 {
254     // backup before we begin searching
255 
256     if (!searched && willSearch && d->searchBackup.isEmpty())
257     {
258         qCDebug(DIGIKAM_GENERAL_LOG) << "Searching started, backing up state";
259 
260         QList<int> selection, expansion;
261         saveStateRecursive(QModelIndex(), selection, expansion);
262 
263         // selection is ignored here because the user may have changed this
264         // while searching
265 
266         foreach (const int& expandedId, expansion)
267         {
268             d->searchBackup[expandedId].expanded = true;
269         }
270 
271         // also backup the last selected album in case this didn't work via the slot
272 
273         const QList<Album*> selList = selectedAlbums<Album>(selectionModel(),
274                                                             m_albumFilterModel);
275         if (!selList.isEmpty())
276         {
277             d->lastSelectedAlbum = selList.first();
278         }
279     }
280 }
281 
slotSearchTextSettingsChanged(bool wasSearching,bool searched)282 void AbstractAlbumTreeView::slotSearchTextSettingsChanged(bool wasSearching, bool searched)
283 {
284     // ensure that all search results are visible if there is currently a search working
285 
286     if (searched)
287     {
288         qCDebug(DIGIKAM_GENERAL_LOG) << "Searched, expanding all results";
289         expandMatches(QModelIndex());
290     }
291 
292     // restore the tree view state if searching finished
293 
294     if (wasSearching && !searched && !d->searchBackup.isEmpty())
295     {
296 
297         qCDebug(DIGIKAM_GENERAL_LOG) << "Searching finished, restoring tree view state";
298 
299         collapseAll();
300         restoreStateForHierarchy(QModelIndex(), d->searchBackup);
301         d->searchBackup.clear();
302 
303         if (d->lastSelectedAlbum)
304         {
305             setCurrentAlbums(QList<Album*>() << d->lastSelectedAlbum, false);
306 
307             // doing this twice somehow ensures that all parents are expanded
308             // and we are at the right position. Maybe a hack... ;)
309 
310             scrollTo(m_albumFilterModel->indexForAlbum(d->lastSelectedAlbum));
311             scrollTo(m_albumFilterModel->indexForAlbum(d->lastSelectedAlbum));
312         }
313     }
314 }
315 
currentAlbumChangedForBackupSelection(Album * currentAlbum)316 void AbstractAlbumTreeView::currentAlbumChangedForBackupSelection(Album* currentAlbum)
317 {
318     d->lastSelectedAlbum = currentAlbum;
319 }
320 
slotRootAlbumAvailable()321 void AbstractAlbumTreeView::slotRootAlbumAvailable()
322 {
323     expand(m_albumFilterModel->rootAlbumIndex());
324 }
325 
expandMatches(const QModelIndex & index)326 bool AbstractAlbumTreeView::expandMatches(const QModelIndex& index)
327 {
328     bool anyMatch = false;
329 
330     // expand index if a child matches
331 
332     const QModelIndex source_index             = m_albumFilterModel->mapToSource(index);
333     const AlbumFilterModel::MatchResult result = m_albumFilterModel->matchResult(source_index);
334 
335     switch (result)
336     {
337         case AlbumFilterModel::NoMatch:
338 
339             if (index != rootIndex())
340             {
341                 return false;
342             }
343 
344             break;
345 
346         case AlbumFilterModel::ParentMatch:
347 
348             // Does not rule out additional child match, return value is unknown
349 
350             break;
351 
352         case AlbumFilterModel::DirectMatch:
353 
354             // Does not rule out additional child match, but we know we will return true
355 
356             anyMatch = true;
357             break;
358 
359         case AlbumFilterModel::ChildMatch:
360         case AlbumFilterModel::SpecialMatch:
361 
362             // We know already to expand, and we know already we will return true.
363 
364             anyMatch = true;
365             expand(index);
366 
367             break;
368     }
369 
370     // Recurse. Expand if children if have an (indirect) match
371 
372     const int rows = m_albumFilterModel->rowCount(index);
373 
374     for (int i = 0 ; i < rows ; ++i)
375     {
376         const QModelIndex child = m_albumFilterModel->index(i, 0, index);
377         const bool childResult  = expandMatches(child);
378 
379         if (childResult)
380         {
381             anyMatch = true;
382 
383             // if there is a direct match _and_ a child match, do not forget to expand the parent
384 
385             expand(index);
386         }
387     }
388 
389     return anyMatch;
390 }
391 
setSearchTextSettings(const SearchTextSettings & settings)392 void AbstractAlbumTreeView::setSearchTextSettings(const SearchTextSettings& settings)
393 {
394     m_albumFilterModel->setSearchTextSettings(settings);
395 }
396 
setAlbumManagerCurrentAlbum(const bool set)397 void AbstractAlbumTreeView::setAlbumManagerCurrentAlbum(const bool set)
398 {
399     d->setInAlbumManager = set;
400 }
401 
setCurrentAlbums(const QList<Album * > & albums,bool selectInAlbumManager)402 void AbstractAlbumTreeView::setCurrentAlbums(const QList<Album*>& albums, bool selectInAlbumManager)
403 {
404     if (!model())
405     {
406         return;
407     }
408 
409     if (selectInAlbumManager && d->setInAlbumManager)
410     {
411         AlbumManager::instance()->setCurrentAlbums(albums);
412     }
413 
414     setCurrentIndex(albumFilterModel()->indexForAlbum(albums.first()));
415 
416     QItemSelectionModel* const model = selectionModel();
417     model->clearSelection();
418 
419     for (int it = 0 ; it < albums.size() ; ++it)
420     {
421         model->select(albumFilterModel()->indexForAlbum(albums.at(it)),
422                       model->Select);
423     }
424 }
425 
slotCurrentChanged()426 void AbstractAlbumTreeView::slotCurrentChanged()
427 {
428     // It seems that QItemSelectionModel::selectedIndexes() has not been updated at this point
429     // and returns the previously selected items. Therefore the line below did not work.
430 /*
431     QList<Album*> selected = selectedAlbums<Album>(selectionModel(),
432                                                    m_albumFilterModel);
433 */
434     // Instead, we call QItemSelectionModel::currentIndex to get the current index.
435 
436     const QModelIndex cIndex = selectionModel()->currentIndex();
437 
438     if (!cIndex.isValid())
439     {
440         return;
441     }
442 
443     Album* const cAlbum = m_albumFilterModel->albumForIndex(cIndex);
444 
445     if (!cAlbum)
446     {
447         return;
448     }
449 
450     emit currentAlbumChanged(cAlbum);
451 }
452 
slotSelectionChanged()453 void AbstractAlbumTreeView::slotSelectionChanged()
454 {
455     // FIXME: Dead signal? Nobody listens to it
456 /*
457     emit selectedAlbumsChanged(selectedAlbums<Album>(selectionModel(), m_albumFilterModel));
458 */
459     if (d->selectAlbumOnClick)
460     {
461         AlbumManager::instance()->setCurrentAlbums(selectedAlbums<Album>(selectionModel(),
462                                                                          m_albumFilterModel));
463     }
464 }
465 
mousePressEvent(QMouseEvent * e)466 void AbstractAlbumTreeView::mousePressEvent(QMouseEvent* e)
467 {
468     const QModelIndex currentBefor = currentIndex();
469 
470     QTreeView::mousePressEvent(e);
471 
472     if ((d->expandOnSingleClick || d->expandNewCurrent) && (e->button() == Qt::LeftButton))
473     {
474         const QModelIndex index = indexVisuallyAt(e->pos());
475 
476         if (index.isValid())
477         {
478             if (d->expandOnSingleClick)
479             {
480                 // See bug #126871: collapse/expand treeview using left mouse button single click.
481                 // Exception: If a newly selected item is already expanded, do not collapse on selection.
482 
483                 const bool expanded = isExpanded(index);
484 
485                 if ((index == currentIndex()) || !expanded)
486                 {
487                     setExpanded(index, !expanded);
488                 }
489             }
490             else
491             {
492                 if (currentBefor != currentIndex())
493                 {
494                     expand(index);
495                 }
496             }
497         }
498     }
499     else if (m_checkOnMiddleClick && (e->button() == Qt::MidButton))
500     {
501         Album* const a = m_albumFilterModel->albumForIndex(indexAt(e->pos()));
502 
503         if (a)
504         {
505             middleButtonPressed(a);
506         }
507     }
508 }
509 
middleButtonPressed(Album *)510 void AbstractAlbumTreeView::middleButtonPressed(Album*)
511 {
512     // reimplement if needed
513 }
514 
startDrag(Qt::DropActions supportedActions)515 void AbstractAlbumTreeView::startDrag(Qt::DropActions supportedActions)
516 {
517     const QModelIndexList indexes = selectedIndexes();
518 
519     if (indexes.count() > 0)
520     {
521         QMimeData* const data = m_albumFilterModel->mimeData(indexes);
522 
523         if (!data)
524         {
525             return;
526         }
527 
528         QStyleOptionViewItem option = viewOptions();
529         option.rect                 = viewport()->rect();
530         const QPixmap pixmap        = /*m_delegate->*/pixmapForDrag(option, indexes);
531         QDrag* const drag           = new QDrag(this);
532         drag->setPixmap(pixmap);
533         drag->setMimeData(data);
534         drag->exec(supportedActions, Qt::CopyAction);
535     }
536 }
537 
538 /**
539  *TODO: Move to delegate, when we have one.
540  *      Copy code from image delegate for creating icons when dragging multiple items
541  */
pixmapForDrag(const QStyleOptionViewItem &,QList<QModelIndex> indexes)542 QPixmap AbstractAlbumTreeView::pixmapForDrag(const QStyleOptionViewItem&, QList<QModelIndex> indexes)
543 {
544     if (indexes.isEmpty())
545     {
546         return QPixmap();
547     }
548 
549     const QVariant decoration = indexes.first().data(Qt::DecorationRole);
550 
551     return decoration.value<QPixmap>();
552 }
553 
dragEnterEvent(QDragEnterEvent * e)554 void AbstractAlbumTreeView::dragEnterEvent(QDragEnterEvent* e)
555 {
556     AlbumModelDragDropHandler* const handler = m_albumModel->dragDropHandler();
557 
558     if (handler && handler->acceptsMimeData(e->mimeData()))
559     {
560         setState(DraggingState);
561         e->accept();
562     }
563     else
564     {
565         e->ignore();
566     }
567 }
568 
dragMoveEvent(QDragMoveEvent * e)569 void AbstractAlbumTreeView::dragMoveEvent(QDragMoveEvent* e)
570 {
571     QTreeView::dragMoveEvent(e);
572     AlbumModelDragDropHandler* const handler = m_albumModel->dragDropHandler();
573 
574     if (handler)
575     {
576         const QModelIndex index     = indexVisuallyAt(e->pos());
577         const Qt::DropAction action = handler->accepts(e, m_albumFilterModel->mapToSourceAlbumModel(index));
578 
579         if (action == Qt::IgnoreAction)
580         {
581             e->ignore();
582         }
583         else
584         {
585             e->setDropAction(action);
586             e->accept();
587         }
588     }
589 }
590 
dragLeaveEvent(QDragLeaveEvent * e)591 void AbstractAlbumTreeView::dragLeaveEvent(QDragLeaveEvent* e)
592 {
593     QTreeView::dragLeaveEvent(e);
594 }
595 
dropEvent(QDropEvent * e)596 void AbstractAlbumTreeView::dropEvent(QDropEvent* e)
597 {
598     QTreeView::dropEvent(e);
599     AlbumModelDragDropHandler* const handler = m_albumModel->dragDropHandler();
600 
601     if (handler)
602     {
603         const QModelIndex index = indexVisuallyAt(e->pos());
604 
605         if (handler->dropEvent(this, e, m_albumFilterModel->mapToSourceAlbumModel(index)))
606         {
607             e->accept();
608         }
609     }
610 }
611 
viewportEvent(QEvent * event)612 bool AbstractAlbumTreeView::viewportEvent(QEvent* event)
613 {
614     return QTreeView::viewportEvent(event);
615 }
616 
selectedItems()617 QList<Album*> AbstractAlbumTreeView::selectedItems()
618 {
619     return selectedAlbums<Album>(selectionModel(), m_albumFilterModel);
620 }
621 
doLoadState()622 void AbstractAlbumTreeView::doLoadState()
623 {
624     KConfigGroup configGroup = getConfigGroup();
625 /*
626     qCDebug(DIGIKAM_GENERAL_LOG) << "Loading view state from " << this << configGroup.name() << objectName();
627 */
628     // extract the selection from the config
629 
630     const QStringList selection = configGroup.readEntry(entryName(d->configSelectionEntry), QStringList());
631 /*
632     qCDebug(DIGIKAM_GENERAL_LOG) << "selection: " << selection;
633 */
634     foreach (const QString& key, selection)
635     {
636         bool validId;
637         const int id = key.toInt(&validId);
638 
639         if (validId)
640         {
641             d->statesByAlbumId[id].selected = true;
642         }
643     }
644 
645     // extract expansion state from config
646 
647     const QStringList expansion = configGroup.readEntry(entryName(d->configExpansionEntry), QStringList());
648 /*
649     qCDebug(DIGIKAM_GENERAL_LOG) << "expansion: " << expansion;
650 */
651     // If no expansion was done, at least expand the root albums
652 
653     if (expansion.isEmpty())
654     {
655         QList<AlbumRootInfo> roots = CoreDbAccess().db()->getAlbumRoots();
656 
657         foreach (const AlbumRootInfo& info, roots)
658         {
659             int albumId = CoreDbAccess().db()->getAlbumForPath(info.id, QLatin1String("/"), false);
660 
661             if (albumId != -1)
662             {
663                 d->statesByAlbumId[albumId].expanded = true;
664             }
665         }
666     }
667     else
668     {
669         foreach (const QString& key, expansion)
670         {
671             bool validId;
672             const int id = key.toInt(&validId);
673 
674             if (validId)
675             {
676                 d->statesByAlbumId[id].expanded = true;
677             }
678         }
679     }
680 
681     // extract current index from config
682 
683     const QString key = configGroup.readEntry(entryName(d->configCurrentIndexEntry), QString());
684 /*
685     qCDebug(DIGIKAM_GENERAL_LOG) << "currentIndex: " << key;
686 */
687     bool validId;
688     const int id      = key.toInt(&validId);
689 
690     if (validId)
691     {
692         d->statesByAlbumId[id].currentIndex = true;
693     }
694 
695 /*
696     for (QMap<int, Digikam::State>::iterator it = d->statesByAlbumId.begin() ; it
697         != d->statesByAlbumId.end() ; ++it)
698     {
699         qCDebug(DIGIKAM_GENERAL_LOG) << "id = " << it.key() << ": recovered state (selected = "
700         << it.value().selected << ", expanded = "
701         << it.value().expanded << ", currentIndex = "
702         << it.value().currentIndex << ")";
703     }
704 */
705 
706     // initial restore run, for everything already loaded
707 /*
708     qCDebug(DIGIKAM_GENERAL_LOG) << "initial restore run with " << model()->rowCount() << " rows";
709 */
710     restoreStateForHierarchy(QModelIndex(), d->statesByAlbumId);
711 
712     // also restore the sorting order
713 
714     sortByColumn(configGroup.readEntry(entryName(d->configSortColumnEntry), 0),
715                  (Qt::SortOrder) configGroup.readEntry(entryName(d->configSortOrderEntry), (int)Qt::AscendingOrder));
716 
717     // use a timer to scroll to the first possible selected album
718 
719     QTimer::singleShot(200, this, SLOT(scrollToSelectedAlbum()));
720 }
721 
restoreStateForHierarchy(const QModelIndex & index,const QMap<int,Digikam::State> & stateStore)722 void AbstractAlbumTreeView::restoreStateForHierarchy(const QModelIndex& index, const QMap<int, Digikam::State>& stateStore)
723 {
724     restoreState(index, stateStore);
725 
726     // do a recursive call of the state restoration
727 
728     for (int i = 0 ; i < model()->rowCount(index) ; ++i)
729     {
730         const QModelIndex child = model()->index(i, 0, index);
731         restoreStateForHierarchy(child, stateStore);
732     }
733 }
734 
restoreState(const QModelIndex & index,const QMap<int,Digikam::State> & stateStore)735 void AbstractAlbumTreeView::restoreState(const QModelIndex& index, const QMap<int, Digikam::State>& stateStore)
736 {
737     Album* const album = albumFilterModel()->albumForIndex(index);
738 
739     if (album && stateStore.contains(album->id()))
740     {
741 
742         Digikam::State state = stateStore.value(album->id());
743 
744 /*
745         qCDebug(DIGIKAM_GENERAL_LOG) << "Trying to restore state of album " << album->title() << "(" <<album->id() << ")"
746                  << ": state(selected = " << state.selected
747                  << ", expanded = " << state.expanded
748                  << ", currentIndex = " << state.currentIndex << ")" << this;
749 */
750         // Block signals to prevent that the searches started when the last
751         // selected index is restored when loading the GUI
752 
753         selectionModel()->blockSignals(true);
754 
755         if (state.selected)
756         {
757 /*
758             qCDebug(DIGIKAM_GENERAL_LOG) << "Selecting" << album->title();
759 */
760             selectionModel()->select(index, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
761         }
762 
763         // Restore expansion state but ensure that the root album is always expanded
764 
765         if (!album->isRoot())
766         {
767             setExpanded(index, state.expanded);
768         }
769         else
770         {
771             setExpanded(index, true);
772         }
773 
774         // restore the current index
775 
776         if (state.currentIndex)
777         {
778 /*
779             qCDebug(DIGIKAM_GENERAL_LOG) << "Setting current index" << album->title() << "(" << album->id() << ")";
780 */
781             selectionModel()->setCurrentIndex(index, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
782         }
783 
784         selectionModel()->blockSignals(false);
785     }
786 }
787 
rowsInserted(const QModelIndex & parent,int start,int end)788 void AbstractAlbumTreeView::rowsInserted(const QModelIndex& parent, int start, int end)
789 {
790     QTreeView::rowsInserted(parent, start, end);
791 
792     if (!d->statesByAlbumId.isEmpty())
793     {
794 /*
795         qCDebug(DIGIKAM_GENERAL_LOG) << "slot rowInserted called with index = " << index
796                                      << ", start = " << start << ", end = " << end
797                                      << "remaining ids" << d->statesByAlbumId.keys();
798 */
799         // Restore state for parent a second time - expansion can only be restored if there are children
800 
801         restoreState(parent, d->statesByAlbumId);
802 
803         for (int i = start ; i <= end ; ++i)
804         {
805             const QModelIndex child = model()->index(i, 0, parent);
806             restoreState(child, d->statesByAlbumId);
807         }
808     }
809 }
810 
rowsAboutToBeRemoved(const QModelIndex & parent,int start,int end)811 void AbstractAlbumTreeView::rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end)
812 {
813     QTreeView::rowsAboutToBeRemoved(parent, start, end);
814 
815     // Clean up map if album id is reused for a new album
816 
817     if (!d->statesByAlbumId.isEmpty())
818     {
819         for (int i = start ; i <= end ; ++i)
820         {
821             const QModelIndex child = model()->index(i, 0, parent);
822             Album* const album      = albumModel()->albumForIndex(child);
823 
824             if (album)
825             {
826                 d->statesByAlbumId.remove(album->id());
827             }
828         }
829     }
830 }
831 
adaptColumnsToContent()832 void AbstractAlbumTreeView::adaptColumnsToContent()
833 {
834     resizeColumnToContents(0);
835 }
836 
scrollToSelectedAlbum()837 void AbstractAlbumTreeView::scrollToSelectedAlbum()
838 {
839     const QModelIndexList selected = selectedIndexes();
840 
841     if (!selected.isEmpty())
842     {
843         scrollTo(selected.first(), PositionAtCenter);
844         horizontalScrollBar()->setValue(0);
845     }
846 }
847 
expandEverything(const QModelIndex & index)848 void AbstractAlbumTreeView::expandEverything(const QModelIndex& index)
849 {
850     for (int row = 0 ; row < albumFilterModel()->rowCount(index) ; ++row)
851     {
852         const QModelIndex rowIndex = albumFilterModel()->index(row, 0, index);
853         expand(rowIndex);
854         expandEverything(rowIndex);
855     }
856 }
857 
slotExpandNode()858 void AbstractAlbumTreeView::slotExpandNode()
859 {
860     QItemSelectionModel* const model = selectionModel();
861     QModelIndexList selected         = model->selectedIndexes();
862 
863     QQueue<QModelIndex> greyNodes;
864 
865     foreach (const QModelIndex& index, selected)
866     {
867         greyNodes.append(index);
868         expand(index);
869     }
870 
871     while (!greyNodes.isEmpty())
872     {
873         QModelIndex current = greyNodes.dequeue();
874 
875         if (!current.isValid())
876         {
877             continue;
878         }
879 
880         int it            = 0;
881         QModelIndex child = current.model()->index(it++, 0, current);
882 
883         while (child.isValid())
884         {
885             expand(child);
886             greyNodes.enqueue(child);
887             child = current.model()->index(it++, 0, current);
888         }
889     }
890 }
891 
slotCollapseNode()892 void AbstractAlbumTreeView::slotCollapseNode()
893 {
894     QItemSelectionModel* const model = selectionModel();
895     QModelIndexList selected         = model->selectedIndexes();
896 
897     QQueue<QModelIndex> greyNodes;
898 
899     foreach (const QModelIndex& index, selected)
900     {
901         greyNodes.append(index);
902         collapse(index);
903     }
904 
905     while (!greyNodes.isEmpty())
906     {
907         QModelIndex current = greyNodes.dequeue();
908 
909         if (!current.isValid())
910         {
911             continue;
912         }
913 
914         int it              = 0;
915         QModelIndex child   = current.model()->index(it++, 0, current);
916 
917         while (child.isValid())
918         {
919             collapse(child);
920             greyNodes.enqueue(child);
921             child = current.model()->index(it++, 0, current);
922         }
923     }
924 }
925 
adaptColumnsOnDataChange(const QModelIndex & topLeft,const QModelIndex & bottomRight)926 void AbstractAlbumTreeView::adaptColumnsOnDataChange(const QModelIndex& topLeft, const QModelIndex& bottomRight)
927 {
928     Q_UNUSED(topLeft);
929     Q_UNUSED(bottomRight);
930 
931     if (!d->resizeColumnsTimer->isActive())
932     {
933         d->resizeColumnsTimer->start();
934     }
935 }
936 
adaptColumnsOnRowChange(const QModelIndex & parent,int start,int end)937 void AbstractAlbumTreeView::adaptColumnsOnRowChange(const QModelIndex& parent, int start, int end)
938 {
939     Q_UNUSED(parent);
940     Q_UNUSED(start);
941     Q_UNUSED(end);
942 
943     if (!d->resizeColumnsTimer->isActive())
944     {
945         d->resizeColumnsTimer->start();
946     }
947 }
948 
adaptColumnsOnLayoutChange()949 void AbstractAlbumTreeView::adaptColumnsOnLayoutChange()
950 {
951     if (!d->resizeColumnsTimer->isActive())
952     {
953         d->resizeColumnsTimer->start();
954     }
955 }
956 
doSaveState()957 void AbstractAlbumTreeView::doSaveState()
958 {
959     KConfigGroup configGroup = getConfigGroup();
960 
961     QList<int> selection, expansion;
962 
963     for (int i = 0 ; i < model()->rowCount() ; ++i)
964     {
965         const QModelIndex index = model()->index(i, 0);
966         saveStateRecursive(index, selection, expansion);
967     }
968 
969     Album* const selectedAlbum = albumFilterModel()->albumForIndex(selectionModel()->currentIndex());
970     QString currentIndex;
971 
972     if (selectedAlbum)
973     {
974         currentIndex = QString::number(selectedAlbum->id());
975     }
976 
977     configGroup.writeEntry(entryName(d->configSelectionEntry),    selection);
978     configGroup.writeEntry(entryName(d->configExpansionEntry),    expansion);
979     configGroup.writeEntry(entryName(d->configCurrentIndexEntry), currentIndex);
980     configGroup.writeEntry(entryName(d->configSortColumnEntry),   albumFilterModel()->sortColumn());
981 
982     // A dummy way to force the tree view to resort if the album sort role changed
983 
984     if (ApplicationSettings::instance()->getAlbumSortChanged())
985     {
986         if (int(albumFilterModel()->sortOrder()) == 0)
987         {
988             configGroup.writeEntry(entryName(d->configSortOrderEntry), 1);
989         }
990         else
991         {
992             configGroup.writeEntry(entryName(d->configSortOrderEntry), 0);
993         }
994     }
995     else
996     {
997         configGroup.writeEntry(entryName(d->configSortOrderEntry), int(albumFilterModel()->sortOrder()));
998     }
999 }
1000 
saveStateRecursive(const QModelIndex & index,QList<int> & selection,QList<int> & expansion)1001 void AbstractAlbumTreeView::saveStateRecursive(const QModelIndex& index, QList<int>& selection, QList<int>& expansion)
1002 {
1003     Album* const album = albumFilterModel()->albumForIndex(index);
1004 
1005     if (album)
1006     {
1007         const int id = album->id();
1008 
1009         if (selectionModel()->isSelected(index))
1010         {
1011             selection.append(id);
1012         }
1013 
1014         if (isExpanded(index))
1015         {
1016             expansion.append(id);
1017         }
1018     }
1019 
1020     for (int i = 0 ; i < model()->rowCount(index) ; ++i)
1021     {
1022         const QModelIndex child = model()->index(i, 0, index);
1023         saveStateRecursive(child, selection, expansion);
1024     }
1025 }
1026 
setEnableContextMenu(const bool enable)1027 void AbstractAlbumTreeView::setEnableContextMenu(const bool enable)
1028 {
1029     d->enableContextMenu = enable;
1030 }
1031 
showContextMenuAt(QContextMenuEvent * event,Album * albumForEvent)1032 bool AbstractAlbumTreeView::showContextMenuAt(QContextMenuEvent* event, Album* albumForEvent)
1033 {
1034     Q_UNUSED(event);
1035     return albumForEvent;
1036 }
1037 
setContextMenuIcon(const QPixmap & pixmap)1038 void AbstractAlbumTreeView::setContextMenuIcon(const QPixmap& pixmap)
1039 {
1040     d->contextMenuIcon = pixmap;
1041 }
1042 
setContextMenuTitle(const QString & title)1043 void AbstractAlbumTreeView::setContextMenuTitle(const QString& title)
1044 {
1045     d->contextMenuTitle = title;
1046 }
1047 
contextMenuIcon() const1048 QPixmap AbstractAlbumTreeView::contextMenuIcon() const
1049 {
1050     return d->contextMenuIcon;
1051 }
1052 
contextMenuTitle() const1053 QString AbstractAlbumTreeView::contextMenuTitle() const
1054 {
1055     return d->contextMenuTitle;
1056 }
1057 
addContextMenuElement(ContextMenuElement * element)1058 void AbstractAlbumTreeView::addContextMenuElement(ContextMenuElement* element)
1059 {
1060     d->contextMenuElements << element;
1061 }
1062 
removeContextMenuElement(ContextMenuElement * element)1063 void AbstractAlbumTreeView::removeContextMenuElement(ContextMenuElement* element)
1064 {
1065     d->contextMenuElements.removeAll(element);
1066 }
1067 
contextMenuElements() const1068 QList<AbstractAlbumTreeView::ContextMenuElement*> AbstractAlbumTreeView::contextMenuElements() const
1069 {
1070     return d->contextMenuElements;
1071 }
1072 
contextMenuEvent(QContextMenuEvent * event)1073 void AbstractAlbumTreeView::contextMenuEvent(QContextMenuEvent* event)
1074 {
1075     if (!d->enableContextMenu)
1076     {
1077         return;
1078     }
1079 
1080     Album* const album = albumFilterModel()->albumForIndex(indexAt(event->pos()));
1081 
1082     if (!album)
1083     {
1084         return;
1085     }
1086 
1087     if (album->isTrashAlbum())
1088     {
1089         // For the moment, disabling context menu for trash.
1090         // TODO : customize it.
1091 
1092         return;
1093     }
1094 
1095     if (!showContextMenuAt(event, album))
1096     {
1097         return;
1098     }
1099 
1100     // switch to the selected album if need
1101 
1102     if (d->selectOnContextMenu)
1103     {
1104         setCurrentAlbums(QList<Album*>() << album);
1105     }
1106 
1107     // --------------------------------------------------------
1108 
1109     QMenu* const popmenu = new QMenu(this);
1110     popmenu->addSection(contextMenuIcon(), contextMenuTitle());
1111     ContextMenuHelper cmhelper(popmenu);
1112 
1113     addCustomContextMenuActions(cmhelper, album);
1114 
1115     foreach (ContextMenuElement* const element, d->contextMenuElements)
1116     {
1117         element->addActions(this, cmhelper, album);
1118     }
1119 
1120     AlbumPointer<Album> albumPointer(album);
1121     QAction* const choice = cmhelper.exec(QCursor::pos());
1122     handleCustomContextMenuAction(choice, albumPointer);
1123 }
1124 
setSelectOnContextMenu(const bool select)1125 void AbstractAlbumTreeView::setSelectOnContextMenu(const bool select)
1126 {
1127     d->selectOnContextMenu = select;
1128 }
1129 
addCustomContextMenuActions(ContextMenuHelper & cmh,Album * album)1130 void AbstractAlbumTreeView::addCustomContextMenuActions(ContextMenuHelper& cmh, Album* album)
1131 {
1132     Q_UNUSED(cmh);
1133     Q_UNUSED(album);
1134 }
1135 
handleCustomContextMenuAction(QAction * action,const AlbumPointer<Album> & album)1136 void AbstractAlbumTreeView::handleCustomContextMenuAction(QAction* action, const AlbumPointer<Album>& album)
1137 {
1138     Q_UNUSED(action);
1139     Q_UNUSED(album);
1140 }
1141 
albumSettingsChanged()1142 void AbstractAlbumTreeView::albumSettingsChanged()
1143 {
1144     setFont(ApplicationSettings::instance()->getTreeViewFont());
1145 
1146     if (d->delegate)
1147     {
1148         d->delegate->updateHeight();
1149     }
1150 }
1151 
slotScrollBarValueChanged(int value)1152 void AbstractAlbumTreeView::slotScrollBarValueChanged(int value)
1153 {
1154     if (m_lastScrollBarValue == -1)
1155     {
1156         m_lastScrollBarValue = value;
1157     }
1158 
1159     if ((value == 0) && (m_lastScrollBarValue > 0))
1160     {
1161         horizontalScrollBar()->setValue(m_lastScrollBarValue);
1162     }
1163 }
1164 
slotScrollBarActionTriggered(int action)1165 void AbstractAlbumTreeView::slotScrollBarActionTriggered(int action)
1166 {
1167     if ((action == QAbstractSlider::SliderMove)        ||
1168         (action == QAbstractSlider::SliderToMinimum)   ||
1169         (action == QAbstractSlider::SliderPageStepSub) ||
1170         (action == QAbstractSlider::SliderSingleStepSub))
1171     {
1172         m_lastScrollBarValue = -1;
1173     }
1174 }
1175 
1176 } // namespace Digikam
1177