1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2009-03-05
7  * Description : Qt item model for database entries
8  *
9  * Copyright (C) 2009-2011 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
10  * Copyright (C) 2012-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
11  *
12  * This program is free software; you can redistribute it
13  * and/or modify it under the terms of the GNU General
14  * Public License as published by the Free Software Foundation;
15  * either version 2, or (at your option)
16  * any later version.
17  *
18  * This program is distributed in the hope that it will be useful,
19  * but WITHOUT ANY WARRANTY; without even the implied warranty of
20  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21  * GNU General Public License for more details.
22  *
23  * ============================================================ */
24 
25 #include "itemmodel.h"
26 
27 // Qt includes
28 
29 #include <QHash>
30 #include <QItemSelection>
31 
32 // Local includes
33 
34 #include "digikam_debug.h"
35 #include "coredbchangesets.h"
36 #include "coredbfields.h"
37 #include "coredbwatch.h"
38 #include "iteminfo.h"
39 #include "iteminfolist.h"
40 #include "abstractitemdragdrophandler.h"
41 
42 namespace Digikam
43 {
44 
45 class Q_DECL_HIDDEN ItemModel::Private
46 {
47 public:
48 
Private()49     explicit Private()
50       : keepFilePathCache          (false),
51         sendRemovalSignals         (false),
52         preprocessor               (nullptr),
53         refreshing                 (false),
54         reAdding                   (false),
55         incrementalRefreshRequested(false),
56         incrementalUpdater         (nullptr)
57     {
58     }
59 
60 public:
61 
62     ItemInfoList                       infos;
63     ItemInfo                           itemInfo;
64 
65     QList<QVariant>                    extraValues;
66     QMultiHash<qlonglong, int>         idHash;
67 
68     bool                               keepFilePathCache;
69     QHash<QString, qlonglong>          filePathHash;
70 
71     bool                               sendRemovalSignals;
72 
73     QObject*                           preprocessor;
74     bool                               refreshing;
75     bool                               reAdding;
76     bool                               incrementalRefreshRequested;
77 
78     DatabaseFields::Set                watchFlags;
79 
80     class ItemModelIncrementalUpdater* incrementalUpdater;
81 
82     ItemInfoList                       pendingInfos;
83     QList<QVariant>                    pendingExtraValues;
84 
85 public:
86 
isValid(const QModelIndex & index)87     inline bool isValid(const QModelIndex& index)
88     {
89         if (!index.isValid())
90         {
91             return false;
92         }
93 
94         if ((index.row() < 0) || (index.row() >= infos.size()))
95         {
96             qCDebug(DIGIKAM_GENERAL_LOG) << "Invalid index" << index;
97             return false;
98         }
99 
100         return true;
101     }
102 
extraValueValid(const QModelIndex & index)103     inline bool extraValueValid(const QModelIndex& index)
104     {
105         // we assume isValid() being called before, no duplicate checks
106 
107         if      (extraValues.size() == 0)
108         {
109             return false;
110         }
111         else if (index.row() >= extraValues.size())
112         {
113             qCDebug(DIGIKAM_GENERAL_LOG) << "Invalid index for extraData" << index;
114             return false;
115         }
116 
117         return true;
118     }
119 };
120 
121 // -------------------------------------------------------------------------------------
122 
123 typedef QPair<int, int> IntPair; // to make foreach macro happy
124 typedef QList<IntPair>  IntPairList;
125 
126 class Q_DECL_HIDDEN ItemModelIncrementalUpdater
127 {
128 public:
129 
130     explicit ItemModelIncrementalUpdater(ItemModel::Private* d);
131 
132     void                  appendInfos(const QList<ItemInfo>& infos, const QList<QVariant>& extraValues);
133     void                  aboutToBeRemovedInModel(const IntPairList& aboutToBeRemoved);
134     QList<IntPair>        oldIndexes();
135 
136     static QList<IntPair> toContiguousPairs(const QList<int>& ids);
137 
138 public:
139 
140     QHash<qlonglong, int> oldIds;
141     QList<QVariant>       oldExtraValues;
142     QList<ItemInfo>       newInfos;
143     QList<QVariant>       newExtraValues;
144     QList<IntPairList>    modelRemovals;
145 };
146 
147 // -------------------------------------------------------------------------------------
148 
ItemModel(QObject * const parent)149 ItemModel::ItemModel(QObject* const parent)
150     : QAbstractListModel(parent),
151       d                 (new Private)
152 {
153     // --- NOTE: use dynamic binding as slotImageChange() is a virtual slot which can be re-implemented in derived classes.
154     connect(CoreDbAccess::databaseWatch(), static_cast<void (CoreDbWatch::*)(const ImageChangeset&)>(&CoreDbWatch::imageChange),
155             this, &ItemModel::slotImageChange);
156 
157     // --- NOTE: use dynamic binding as slotImageTagChange() is a virtual slot which can be re-implemented in derived classes.
158     connect(CoreDbAccess::databaseWatch(), static_cast<void (CoreDbWatch::*)(const ImageTagChangeset&)>(&CoreDbWatch::imageTagChange),
159             this, &ItemModel::slotImageTagChange);
160 }
161 
~ItemModel()162 ItemModel::~ItemModel()
163 {
164     delete d->incrementalUpdater;
165     delete d;
166 }
167 
168 // ------------ Access methods -------------
169 
setKeepsFilePathCache(bool keepCache)170 void ItemModel::setKeepsFilePathCache(bool keepCache)
171 {
172     d->keepFilePathCache = keepCache;
173 }
174 
keepsFilePathCache() const175 bool ItemModel::keepsFilePathCache() const
176 {
177     return d->keepFilePathCache;
178 }
179 
isEmpty() const180 bool ItemModel::isEmpty() const
181 {
182     return d->infos.isEmpty();
183 }
184 
itemCount() const185 int ItemModel::itemCount() const
186 {
187     return d->infos.count();
188 }
189 
setWatchFlags(const DatabaseFields::Set & set)190 void ItemModel::setWatchFlags(const DatabaseFields::Set& set)
191 {
192     d->watchFlags = set;
193 }
194 
imageInfo(const QModelIndex & index) const195 ItemInfo ItemModel::imageInfo(const QModelIndex& index) const
196 {
197     if (!d->isValid(index))
198     {
199         return ItemInfo();
200     }
201 
202     return d->infos.at(index.row());
203 }
204 
imageInfoRef(const QModelIndex & index) const205 ItemInfo& ItemModel::imageInfoRef(const QModelIndex& index) const
206 {
207     if (!d->isValid(index))
208     {
209         return d->itemInfo;
210     }
211 
212     return d->infos[index.row()];
213 }
214 
imageId(const QModelIndex & index) const215 qlonglong ItemModel::imageId(const QModelIndex& index) const
216 {
217     if (!d->isValid(index))
218     {
219         return 0;
220     }
221 
222     return d->infos.at(index.row()).id();
223 }
224 
imageInfos(const QList<QModelIndex> & indexes) const225 QList<ItemInfo> ItemModel::imageInfos(const QList<QModelIndex>& indexes) const
226 {
227     QList<ItemInfo> infos;
228 
229     foreach (const QModelIndex& index, indexes)
230     {
231         infos << imageInfo(index);
232     }
233 
234     return infos;
235 }
236 
imageIds(const QList<QModelIndex> & indexes) const237 QList<qlonglong> ItemModel::imageIds(const QList<QModelIndex>& indexes) const
238 {
239     QList<qlonglong> ids;
240 
241     foreach (const QModelIndex& index, indexes)
242     {
243         ids << imageId(index);
244     }
245 
246     return ids;
247 }
248 
imageInfo(int row) const249 ItemInfo ItemModel::imageInfo(int row) const
250 {
251     if ((row < 0) || (row >= d->infos.size()))
252     {
253         return ItemInfo();
254     }
255 
256     return d->infos.at(row);
257 }
258 
imageInfoRef(int row) const259 ItemInfo& ItemModel::imageInfoRef(int row) const
260 {
261     if ((row < 0) || (row >= d->infos.size()))
262     {
263         return d->itemInfo;
264     }
265 
266     return d->infos[row];
267 }
268 
imageId(int row) const269 qlonglong ItemModel::imageId(int row) const
270 {
271     if ((row < 0) || (row >= d->infos.size()))
272     {
273         return -1;
274     }
275 
276     return d->infos.at(row).id();
277 }
278 
indexForItemInfo(const ItemInfo & info) const279 QModelIndex ItemModel::indexForItemInfo(const ItemInfo& info) const
280 {
281     return indexForImageId(info.id());
282 }
283 
indexForItemInfo(const ItemInfo & info,const QVariant & extraValue) const284 QModelIndex ItemModel::indexForItemInfo(const ItemInfo& info, const QVariant& extraValue) const
285 {
286     return indexForImageId(info.id(), extraValue);
287 }
288 
indexesForItemInfo(const ItemInfo & info) const289 QList<QModelIndex> ItemModel::indexesForItemInfo(const ItemInfo& info) const
290 {
291     return indexesForImageId(info.id());
292 }
293 
indexForImageId(qlonglong id) const294 QModelIndex ItemModel::indexForImageId(qlonglong id) const
295 {
296     int index = d->idHash.value(id, -1);
297 
298     if (index != -1)
299     {
300         return createIndex(index, 0);
301     }
302 
303     return QModelIndex();
304 }
305 
indexForImageId(qlonglong id,const QVariant & extraValue) const306 QModelIndex ItemModel::indexForImageId(qlonglong id, const QVariant& extraValue) const
307 {
308     if (d->extraValues.isEmpty())
309     {
310         return indexForImageId(id);
311     }
312 
313     QMultiHash<qlonglong, int>::const_iterator it;
314 
315     for (it = d->idHash.constFind(id) ; it != d->idHash.constEnd() && it.key() == id ; ++it)
316     {
317         if (d->extraValues.at(it.value()) == extraValue)
318         {
319             return createIndex(it.value(), 0);
320         }
321     }
322 
323     return QModelIndex();
324 }
325 
indexesForImageId(qlonglong id) const326 QList<QModelIndex> ItemModel::indexesForImageId(qlonglong id) const
327 {
328     QList<QModelIndex> indexes;
329     QMultiHash<qlonglong, int>::const_iterator it;
330 
331     for (it = d->idHash.constFind(id) ; it != d->idHash.constEnd() && it.key() == id ; ++it)
332     {
333         indexes << createIndex(it.value(), 0);
334     }
335 
336     return indexes;
337 }
338 
numberOfIndexesForItemInfo(const ItemInfo & info) const339 int ItemModel::numberOfIndexesForItemInfo(const ItemInfo& info) const
340 {
341     return numberOfIndexesForImageId(info.id());
342 }
343 
numberOfIndexesForImageId(qlonglong id) const344 int ItemModel::numberOfIndexesForImageId(qlonglong id) const
345 {
346     if (d->extraValues.isEmpty())
347     {
348         return 0;
349     }
350 
351     int count = 0;
352     QMultiHash<qlonglong, int>::const_iterator it;
353 
354     for (it = d->idHash.constFind(id) ; it != d->idHash.constEnd() && it.key() == id ; ++it)
355     {
356         ++count;
357     }
358 
359     return count;
360 }
361 
362 // static method
retrieveItemInfo(const QModelIndex & index)363 ItemInfo ItemModel::retrieveItemInfo(const QModelIndex& index)
364 {
365     if (!index.isValid())
366     {
367         return ItemInfo();
368     }
369 
370     ItemModel* const model = index.data(ItemModelPointerRole).value<ItemModel*>();
371     int row                = index.data(ItemModelInternalId).toInt();
372 
373     if (!model)
374     {
375         return ItemInfo();
376     }
377 
378     return model->imageInfo(row);
379 }
380 
381 // static method
retrieveImageId(const QModelIndex & index)382 qlonglong ItemModel::retrieveImageId(const QModelIndex& index)
383 {
384     if (!index.isValid())
385     {
386         return 0;
387     }
388 
389     ItemModel* const model = index.data(ItemModelPointerRole).value<ItemModel*>();
390     int row                = index.data(ItemModelInternalId).toInt();
391 
392     if (!model)
393     {
394         return 0;
395     }
396 
397     return model->imageId(row);
398 }
399 
indexForPath(const QString & filePath) const400 QModelIndex ItemModel::indexForPath(const QString& filePath) const
401 {
402     if (d->keepFilePathCache)
403     {
404         return indexForImageId(d->filePathHash.value(filePath));
405     }
406     else
407     {
408         const int size = d->infos.size();
409 
410         for (int i = 0 ; i < size ; ++i)
411         {
412             if (d->infos.at(i).filePath() == filePath)
413             {
414                 return createIndex(i, 0);
415             }
416         }
417     }
418 
419     return QModelIndex();
420 }
421 
indexesForPath(const QString & filePath) const422 QList<QModelIndex> ItemModel::indexesForPath(const QString& filePath) const
423 {
424     if (d->keepFilePathCache)
425     {
426         return indexesForImageId(d->filePathHash.value(filePath));
427     }
428     else
429     {
430         QList<QModelIndex> indexes;
431         const int size = d->infos.size();
432 
433         for (int i = 0 ; i < size ; ++i)
434         {
435             if (d->infos.at(i).filePath() == filePath)
436             {
437                 indexes << createIndex(i, 0);
438             }
439         }
440 
441         return indexes;
442     }
443 }
444 
imageInfo(const QString & filePath) const445 ItemInfo ItemModel::imageInfo(const QString& filePath) const
446 {
447     if (d->keepFilePathCache)
448     {
449         qlonglong id = d->filePathHash.value(filePath, -1);
450 
451         if (id != -1)
452         {
453             int index = d->idHash.value(id, -1);
454 
455             if (index != -1)
456             {
457                 return d->infos.at(index);
458             }
459         }
460     }
461     else
462     {
463         foreach (const ItemInfo& info, d->infos)
464         {
465             if (info.filePath() == filePath)
466             {
467                 return info;
468             }
469         }
470     }
471 
472     return ItemInfo();
473 }
474 
imageInfos(const QString & filePath) const475 QList<ItemInfo> ItemModel::imageInfos(const QString& filePath) const
476 {
477     QList<ItemInfo> infos;
478 
479     if (d->keepFilePathCache)
480     {
481         qlonglong id = d->filePathHash.value(filePath, -1);
482 
483         if (id != -1)
484         {
485             foreach (int index, d->idHash.values(id))
486             {
487                 infos << d->infos.at(index);
488             }
489         }
490     }
491     else
492     {
493         foreach (const ItemInfo& info, d->infos)
494         {
495             if (info.filePath() == filePath)
496             {
497                 infos << info;
498             }
499         }
500     }
501 
502     return infos;
503 }
504 
addItemInfo(const ItemInfo & info)505 void ItemModel::addItemInfo(const ItemInfo& info)
506 {
507     addItemInfos(QList<ItemInfo>() << info, QList<QVariant>());
508 }
509 
addItemInfos(const QList<ItemInfo> & infos)510 void ItemModel::addItemInfos(const QList<ItemInfo>& infos)
511 {
512     addItemInfos(infos, QList<QVariant>());
513 }
514 
addItemInfos(const QList<ItemInfo> & infos,const QList<QVariant> & extraValues)515 void ItemModel::addItemInfos(const QList<ItemInfo>& infos, const QList<QVariant>& extraValues)
516 {
517     if (infos.isEmpty())
518     {
519         return;
520     }
521 
522     if (d->incrementalUpdater)
523     {
524         d->incrementalUpdater->appendInfos(infos, extraValues);
525     }
526     else
527     {
528         appendInfos(infos, extraValues);
529     }
530 }
531 
addItemInfoSynchronously(const ItemInfo & info)532 void ItemModel::addItemInfoSynchronously(const ItemInfo& info)
533 {
534     addItemInfosSynchronously(QList<ItemInfo>() << info, QList<QVariant>());
535 }
536 
addItemInfosSynchronously(const QList<ItemInfo> & infos)537 void ItemModel::addItemInfosSynchronously(const QList<ItemInfo>& infos)
538 {
539     addItemInfos(infos, QList<QVariant>());
540 }
541 
addItemInfosSynchronously(const QList<ItemInfo> & infos,const QList<QVariant> & extraValues)542 void ItemModel::addItemInfosSynchronously(const QList<ItemInfo>& infos, const QList<QVariant>& extraValues)
543 {
544     if (infos.isEmpty())
545     {
546         return;
547     }
548 
549     publiciseInfos(infos, extraValues);
550     emit processAdded(infos, extraValues);
551 }
552 
ensureHasItemInfo(const ItemInfo & info)553 void ItemModel::ensureHasItemInfo(const ItemInfo& info)
554 {
555     ensureHasItemInfos(QList<ItemInfo>() << info, QList<QVariant>());
556 }
557 
ensureHasItemInfos(const QList<ItemInfo> & infos)558 void ItemModel::ensureHasItemInfos(const QList<ItemInfo>& infos)
559 {
560     ensureHasItemInfos(infos, QList<QVariant>());
561 }
562 
ensureHasItemInfos(const QList<ItemInfo> & infos,const QList<QVariant> & extraValues)563 void ItemModel::ensureHasItemInfos(const QList<ItemInfo>& infos, const QList<QVariant>& extraValues)
564 {
565     if (extraValues.isEmpty())
566     {
567         if (!d->pendingExtraValues.isEmpty())
568         {
569             qCDebug(DIGIKAM_GENERAL_LOG) << "ExtraValue / No Extra Value mismatch. Ignoring added infos.";
570             return;
571         }
572     }
573     else
574     {
575         if (d->pendingInfos.size() != d->pendingExtraValues.size())
576         {
577             qCDebug(DIGIKAM_GENERAL_LOG) << "ExtraValue / No Extra Value mismatch. Ignoring added infos.";
578             return;
579         }
580     }
581 
582     d->pendingInfos << infos;
583     d->pendingExtraValues << extraValues;
584     cleanSituationChecks();
585 }
586 
clearItemInfos()587 void ItemModel::clearItemInfos()
588 {
589     beginResetModel();
590 
591     d->infos.clear();
592     d->extraValues.clear();
593     d->idHash.clear();
594     d->filePathHash.clear();
595 
596     delete d->incrementalUpdater;
597 
598     d->incrementalUpdater          = nullptr;
599     d->pendingInfos.clear();
600     d->pendingExtraValues.clear();
601     d->refreshing                  = false;
602     d->reAdding                    = false;
603     d->incrementalRefreshRequested = false;
604 
605     imageInfosCleared();
606     endResetModel();
607 }
608 
setItemInfos(const QList<ItemInfo> & infos)609 void ItemModel::setItemInfos(const QList<ItemInfo>& infos)
610 {
611     clearItemInfos();
612     addItemInfos(infos);
613 }
614 
imageInfos() const615 QList<ItemInfo> ItemModel::imageInfos() const
616 {
617     return d->infos;
618 }
619 
imageIds() const620 QList<qlonglong> ItemModel::imageIds() const
621 {
622     return d->idHash.keys();
623 }
624 
hasImage(qlonglong id) const625 bool ItemModel::hasImage(qlonglong id) const
626 {
627     return d->idHash.contains(id);
628 }
629 
hasImage(const ItemInfo & info) const630 bool ItemModel::hasImage(const ItemInfo& info) const
631 {
632     return d->idHash.contains(info.id());
633 }
634 
hasImage(const ItemInfo & info,const QVariant & extraValue) const635 bool ItemModel::hasImage(const ItemInfo& info, const QVariant& extraValue) const
636 {
637     return hasImage(info.id(), extraValue);
638 }
639 
hasImage(qlonglong id,const QVariant & extraValue) const640 bool ItemModel::hasImage(qlonglong id, const QVariant& extraValue) const
641 {
642     if (d->extraValues.isEmpty())
643     {
644         return hasImage(id);
645     }
646 
647     QMultiHash<qlonglong, int>::const_iterator it;
648 
649     for (it = d->idHash.constFind(id) ; it != d->idHash.constEnd() && it.key() == id ; ++it)
650     {
651         if (d->extraValues.at(it.value()) == extraValue)
652         {
653             return true;
654         }
655     }
656 
657     return false;;
658 }
659 
uniqueItemInfos() const660 QList<ItemInfo> ItemModel::uniqueItemInfos() const
661 {
662     if (d->extraValues.isEmpty())
663     {
664         return d->infos;
665     }
666 
667     QList<ItemInfo> uniqueInfos;
668     const int size = d->infos.size();
669 
670     for (int i = 0 ; i < size ; ++i)
671     {
672         const ItemInfo& info = d->infos.at(i);
673 
674         if (d->idHash.value(info.id()) == i)
675         {
676             uniqueInfos << info;
677         }
678     }
679 
680     return uniqueInfos;
681 }
682 
emitDataChangedForAll()683 void ItemModel::emitDataChangedForAll()
684 {
685     if (d->infos.isEmpty())
686     {
687         return;
688     }
689 
690     QModelIndex first = createIndex(0, 0);
691     QModelIndex last  = createIndex(d->infos.size() - 1, 0);
692     emit dataChanged(first, last);
693 }
694 
emitDataChangedForSelection(const QItemSelection & selection)695 void ItemModel::emitDataChangedForSelection(const QItemSelection& selection)
696 {
697     if (!selection.isEmpty())
698     {
699         foreach (const QItemSelectionRange& range, selection)
700         {
701             emit dataChanged(range.topLeft(), range.bottomRight());
702         }
703     }
704 }
705 
ensureHasGroupedImages(const ItemInfo & groupLeader)706 void ItemModel::ensureHasGroupedImages(const ItemInfo& groupLeader)
707 {
708     ensureHasItemInfos(groupLeader.groupedImages());
709 }
710 
711 // ------------ Preprocessing -------------
712 
setPreprocessor(QObject * const preprocessor)713 void ItemModel::setPreprocessor(QObject* const preprocessor)
714 {
715     unsetPreprocessor(d->preprocessor);
716     d->preprocessor = preprocessor;
717 }
718 
unsetPreprocessor(QObject * const preprocessor)719 void ItemModel::unsetPreprocessor(QObject* const preprocessor)
720 {
721     if (preprocessor && (d->preprocessor == preprocessor))
722     {
723         disconnect(this, SIGNAL(preprocess(QList<ItemInfo>,QList<QVariant>)),
724                    nullptr, nullptr);
725 
726         disconnect(d->preprocessor, nullptr,
727                    this, SLOT(reAddItemInfos(QList<ItemInfo>,QList<QVariant>)));
728 
729         disconnect(d->preprocessor, nullptr,
730                    this, SLOT(reAddingFinished()));
731     }
732 }
733 
appendInfos(const QList<ItemInfo> & infos,const QList<QVariant> & extraValues)734 void ItemModel::appendInfos(const QList<ItemInfo>& infos, const QList<QVariant>& extraValues)
735 {
736     if (infos.isEmpty())
737     {
738         return;
739     }
740 
741     if (d->preprocessor)
742     {
743         d->reAdding = true;
744         emit preprocess(infos, extraValues);
745     }
746     else
747     {
748         publiciseInfos(infos, extraValues);
749     }
750 }
751 
appendInfosChecked(const QList<ItemInfo> & infos,const QList<QVariant> & extraValues)752 void ItemModel::appendInfosChecked(const QList<ItemInfo>& infos, const QList<QVariant>& extraValues)
753 {
754     // This method does deduplication. It is private because in context of readding or refreshing it is of no use.
755 
756     if (extraValues.isEmpty())
757     {
758         QList<ItemInfo> checkedInfos;
759 
760         foreach (const ItemInfo& info, infos)
761         {
762             if (!hasImage(info))
763             {
764                 checkedInfos << info;
765             }
766         }
767 
768         appendInfos(checkedInfos, QList<QVariant>());
769     }
770     else
771     {
772         QList<ItemInfo> checkedInfos;
773         QList<QVariant> checkedExtraValues;
774         const int size = infos.size();
775 
776         for (int i = 0 ; i < size ; ++i)
777         {
778             if (!hasImage(infos[i], extraValues[i]))
779             {
780                 checkedInfos       << infos[i];
781                 checkedExtraValues << extraValues[i];
782             }
783         }
784 
785         appendInfos(checkedInfos, checkedExtraValues);
786     }
787 }
788 
reAddItemInfos(const QList<ItemInfo> & infos,const QList<QVariant> & extraValues)789 void ItemModel::reAddItemInfos(const QList<ItemInfo>& infos, const QList<QVariant>& extraValues)
790 {
791     // addItemInfos -> appendInfos -> preprocessor -> reAddItemInfos
792 
793     publiciseInfos(infos, extraValues);
794 }
795 
reAddingFinished()796 void ItemModel::reAddingFinished()
797 {
798     d->reAdding = false;
799     cleanSituationChecks();
800 }
801 
startRefresh()802 void ItemModel::startRefresh()
803 {
804     d->refreshing = true;
805 }
806 
finishRefresh()807 void ItemModel::finishRefresh()
808 {
809     d->refreshing = false;
810     cleanSituationChecks();
811 }
812 
isRefreshing() const813 bool ItemModel::isRefreshing() const
814 {
815     return d->refreshing;
816 }
817 
cleanSituationChecks()818 void ItemModel::cleanSituationChecks()
819 {
820     // For starting an incremental refresh we want a clear situation:
821     // Any remaining batches from non-incremental refreshing subclasses have been received in appendInfos(),
822     // any batches sent to preprocessor for re-adding have been re-added.
823 
824     if (d->refreshing || d->reAdding)
825     {
826         return;
827     }
828 
829     if (!d->pendingInfos.isEmpty())
830     {
831         appendInfosChecked(d->pendingInfos, d->pendingExtraValues);
832         d->pendingInfos.clear();
833         d->pendingExtraValues.clear();
834         cleanSituationChecks();
835         return;
836     }
837 
838     if (d->incrementalRefreshRequested)
839     {
840         d->incrementalRefreshRequested = false;
841         emit readyForIncrementalRefresh();
842     }
843     else
844     {
845         emit allRefreshingFinished();
846     }
847 }
848 
publiciseInfos(const QList<ItemInfo> & infos,const QList<QVariant> & extraValues)849 void ItemModel::publiciseInfos(const QList<ItemInfo>& infos, const QList<QVariant>& extraValues)
850 {
851     if (infos.isEmpty())
852     {
853         return;
854     }
855 
856     Q_ASSERT(
857              (infos.size() == extraValues.size()) ||
858              (extraValues.isEmpty() && d->extraValues.isEmpty())
859             );
860 
861     emit imageInfosAboutToBeAdded(infos);
862     const int firstNewIndex = d->infos.size();
863     const int lastNewIndex  = d->infos.size() + infos.size() - 1;
864     beginInsertRows(QModelIndex(), firstNewIndex, lastNewIndex);
865     d->infos       << infos;
866     d->extraValues << extraValues;
867 
868     for (int i = firstNewIndex ; i <= lastNewIndex ; ++i)
869     {
870         const ItemInfo& info = d->infos.at(i);
871         qlonglong id         = info.id();
872         d->idHash.insert(id, i);
873 
874         if (d->keepFilePathCache)
875         {
876             d->filePathHash[info.filePath()] = id;
877         }
878     }
879 
880     endInsertRows();
881     emit imageInfosAdded(infos);
882 }
883 
requestIncrementalRefresh()884 void ItemModel::requestIncrementalRefresh()
885 {
886     if (d->reAdding)
887     {
888         d->incrementalRefreshRequested = true;
889     }
890     else
891     {
892         emit readyForIncrementalRefresh();
893     }
894 }
895 
hasIncrementalRefreshPending() const896 bool ItemModel::hasIncrementalRefreshPending() const
897 {
898     return d->incrementalRefreshRequested;
899 }
900 
startIncrementalRefresh()901 void ItemModel::startIncrementalRefresh()
902 {
903     delete d->incrementalUpdater;
904 
905     d->incrementalUpdater = new ItemModelIncrementalUpdater(d);
906 }
907 
finishIncrementalRefresh()908 void ItemModel::finishIncrementalRefresh()
909 {
910     if (!d->incrementalUpdater)
911     {
912         return;
913     }
914 
915     // remove old entries
916 
917     QList<QPair<int, int> > pairs = d->incrementalUpdater->oldIndexes();
918     removeRowPairs(pairs);
919 
920     // add new indexes
921 
922     appendInfos(d->incrementalUpdater->newInfos, d->incrementalUpdater->newExtraValues);
923 
924     delete d->incrementalUpdater;
925     d->incrementalUpdater = nullptr;
926 }
927 
removeIndex(const QModelIndex & index)928 void ItemModel::removeIndex(const QModelIndex& index)
929 {
930     removeIndexes(QList<QModelIndex>() << index);
931 }
932 
removeIndexes(const QList<QModelIndex> & indexes)933 void ItemModel::removeIndexes(const QList<QModelIndex>& indexes)
934 {
935     QList<int> listIndexes;
936 
937     foreach (const QModelIndex& index, indexes)
938     {
939         if (d->isValid(index))
940         {
941             listIndexes << index.row();
942         }
943     }
944 
945     if (listIndexes.isEmpty())
946     {
947         return;
948     }
949 
950     removeRowPairsWithCheck(ItemModelIncrementalUpdater::toContiguousPairs(listIndexes));
951 }
952 
removeItemInfo(const ItemInfo & info)953 void ItemModel::removeItemInfo(const ItemInfo& info)
954 {
955     removeItemInfos(QList<ItemInfo>() << info);
956 }
957 
removeItemInfos(const QList<ItemInfo> & infos)958 void ItemModel::removeItemInfos(const QList<ItemInfo>& infos)
959 {
960     QList<int> listIndexes;
961 
962     foreach (const ItemInfo& info, infos)
963     {
964         QModelIndex index = indexForImageId(info.id());
965 
966         if (index.isValid())
967         {
968             listIndexes << index.row();
969         }
970     }
971 
972     removeRowPairsWithCheck(ItemModelIncrementalUpdater::toContiguousPairs(listIndexes));
973 }
974 
removeItemInfos(const QList<ItemInfo> & infos,const QList<QVariant> & extraValues)975 void ItemModel::removeItemInfos(const QList<ItemInfo>& infos, const QList<QVariant>& extraValues)
976 {
977     if (extraValues.isEmpty())
978     {
979         removeItemInfos(infos);
980         return;
981     }
982 
983     QList<int> listIndexes;
984 
985     for (int i = 0 ; i < infos.size() ; ++i)
986     {
987         QModelIndex index = indexForImageId(infos.at(i).id(), extraValues.at(i));
988 
989         if (index.isValid())
990         {
991             listIndexes << index.row();
992         }
993     }
994 
995     removeRowPairsWithCheck(ItemModelIncrementalUpdater::toContiguousPairs(listIndexes));
996 }
997 
setSendRemovalSignals(bool send)998 void ItemModel::setSendRemovalSignals(bool send)
999 {
1000     d->sendRemovalSignals = send;
1001 }
1002 /*
1003 template <class List, typename T>
1004 static bool pairsContain(const List& list, T value)
1005 {
1006     typename List::const_iterator middle;
1007     typename List::const_iterator begin = list.begin();
1008     typename List::const_iterator end   = list.end();
1009     int n                               = int(end - begin);
1010     int half;
1011 
1012     while (n > 0)
1013     {
1014         half   = n >> 1;
1015         middle = begin + half;
1016 
1017         if      ((middle->first <= value) && (middle->second >= value))
1018         {
1019             return true;
1020         }
1021         else if (middle->second < value)
1022         {
1023             begin = middle + 1;
1024             n    -= half   + 1;
1025         }
1026         else
1027         {
1028             n = half;
1029         }
1030     }
1031 
1032     return false;
1033 }
1034 */
removeRowPairsWithCheck(const QList<QPair<int,int>> & toRemove)1035 void ItemModel::removeRowPairsWithCheck(const QList<QPair<int, int> >& toRemove)
1036 {
1037     if (d->incrementalUpdater)
1038     {
1039         d->incrementalUpdater->aboutToBeRemovedInModel(toRemove);
1040     }
1041 
1042     removeRowPairs(toRemove);
1043 }
1044 
removeRowPairs(const QList<QPair<int,int>> & toRemove)1045 void ItemModel::removeRowPairs(const QList<QPair<int, int> >& toRemove)
1046 {
1047     if (toRemove.isEmpty())
1048     {
1049         return;
1050     }
1051 
1052     // Remove old indexes
1053     // Keep in mind that when calling beginRemoveRows all structures announced to be removed
1054     // must still be valid, and this includes our hashes as well, which limits what we can optimize
1055 
1056     int                     removedRows = 0;
1057     int                     offset      = 0;
1058     QList<qlonglong>        removeFilePaths;
1059     typedef QPair<int, int> IntPair;            // to make foreach macro happy
1060 
1061 
1062     foreach (const IntPair& pair, toRemove)
1063     {
1064         const int begin = pair.first  - offset;
1065         const int end   = pair.second - offset; // inclusive
1066         removedRows     = end - begin + 1;
1067 
1068         // when removing from the list, all subsequent indexes are affected
1069 
1070         offset         += removedRows;
1071 
1072         QList<ItemInfo> removedInfos;
1073 
1074         if (d->sendRemovalSignals)
1075         {
1076             // cppcheck-suppress knownEmptyContainer
1077             std::copy(d->infos.begin() + begin, d->infos.begin() + end, removedInfos.begin());
1078             emit imageInfosAboutToBeRemoved(removedInfos);
1079         }
1080 
1081         imageInfosAboutToBeRemoved(begin, end);
1082         beginRemoveRows(QModelIndex(), begin, end);
1083 
1084         // update idHash - which points to indexes of d->infos, and these change now!
1085 
1086         QMultiHash<qlonglong, int>::iterator it;
1087 
1088         for (it = d->idHash.begin() ; it != d->idHash.end() ; )
1089         {
1090             if (it.value() >= begin)
1091             {
1092                 if (it.value() > end)
1093                 {
1094                     // after the removed interval: adjust index
1095 
1096                     it.value() -= removedRows;
1097                 }
1098                 else
1099                 {
1100                     // in the removed interval
1101 
1102                     removeFilePaths << it.key();
1103                     it = d->idHash.erase(it);
1104                     continue;
1105                 }
1106             }
1107 
1108             ++it;
1109         }
1110 
1111         // remove from list
1112 
1113         d->infos.erase(d->infos.begin() + begin, d->infos.begin() + (end + 1));
1114 
1115         if (!d->extraValues.isEmpty())
1116         {
1117             d->extraValues.erase(d->extraValues.begin() + begin, d->extraValues.begin() + (end + 1));
1118         }
1119 
1120         endRemoveRows();
1121 
1122         if (d->sendRemovalSignals)
1123         {
1124             emit imageInfosRemoved(removedInfos);
1125         }
1126     }
1127 
1128     // tidy up: remove old indexes from file path hash now
1129 
1130     if (d->keepFilePathCache)
1131     {
1132         QHash<QString, qlonglong>::iterator it;
1133 
1134         for (it = d->filePathHash.begin() ; it != d->filePathHash.end() ; )
1135         {
1136             if (removeFilePaths.contains(it.value()))
1137             {
1138                 it = d->filePathHash.erase(it);
1139             }
1140             else
1141             {
1142                 ++it;
1143             }
1144         }
1145     }
1146 }
1147 
1148 // ---------------------------------------------------------------------------------
1149 
ItemModelIncrementalUpdater(ItemModel::Private * d)1150 ItemModelIncrementalUpdater::ItemModelIncrementalUpdater(ItemModel::Private* d)
1151     : oldIds        (d->idHash),
1152       oldExtraValues(d->extraValues)
1153 {
1154 }
1155 
appendInfos(const QList<ItemInfo> & infos,const QList<QVariant> & extraValues)1156 void ItemModelIncrementalUpdater::appendInfos(const QList<ItemInfo>& infos, const QList<QVariant>& extraValues)
1157 {
1158     if (extraValues.isEmpty())
1159     {
1160         foreach (const ItemInfo& info, infos)
1161         {
1162             QHash<qlonglong, int>::iterator it = oldIds.find(info.id());
1163 
1164             if (it != oldIds.end())
1165             {
1166                 oldIds.erase(it);
1167             }
1168             else
1169             {
1170                 newInfos << info;
1171             }
1172         }
1173     }
1174     else
1175     {
1176         for (int i = 0 ; i < infos.size() ; ++i)
1177         {
1178             const ItemInfo& info = infos.at(i);
1179             bool found           = false;
1180             QHash<qlonglong, int>::iterator it;
1181 
1182             for (it = oldIds.find(info.id()) ; it != oldIds.end() && it.key() == info.id() ; ++it)
1183             {
1184                 // first check is for bug #262596. Not sure if needed.
1185 
1186                 if (it.value() < oldExtraValues.size() && extraValues.at(i) == oldExtraValues.at(it.value()))
1187                 {
1188                     found = true;
1189                     break;
1190                 }
1191             }
1192 
1193             if (found)
1194             {
1195                 oldIds.erase(it);
1196 
1197                 // do not erase from oldExtraValues - oldIds is a hash id -> index.
1198             }
1199             else
1200             {
1201                 newInfos << info;
1202                 newExtraValues << extraValues.at(i);
1203             }
1204         }
1205     }
1206 }
1207 
aboutToBeRemovedInModel(const IntPairList & toRemove)1208 void ItemModelIncrementalUpdater::aboutToBeRemovedInModel(const IntPairList& toRemove)
1209 {
1210     modelRemovals << toRemove;
1211 }
1212 
oldIndexes()1213 QList<QPair<int, int> > ItemModelIncrementalUpdater::oldIndexes()
1214 {
1215     // first, apply all changes to indexes by direct removal in model
1216     // while the updater was active
1217 
1218     foreach (const IntPairList& list, modelRemovals)
1219     {
1220         int removedRows = 0;
1221         int offset      = 0;
1222 
1223         foreach (const IntPair& pair, list)
1224         {
1225             const int begin = pair.first - offset;
1226             const int end   = pair.second - offset; // inclusive
1227             removedRows     = end - begin + 1;
1228 
1229             // when removing from the list, all subsequent indexes are affected
1230 
1231             offset         += removedRows;
1232 
1233             // update idHash - which points to indexes of d->infos, and these change now!
1234 
1235             QHash<qlonglong, int>::iterator it;
1236 
1237             for (it = oldIds.begin() ; it != oldIds.end() ; )
1238             {
1239                 if (it.value() >= begin)
1240                 {
1241                     if (it.value() > end)
1242                     {
1243                         // after the removed interval: adjust index
1244 
1245                         it.value() -= removedRows;
1246                     }
1247                     else
1248                     {
1249                         // in the removed interval
1250 
1251                         it = oldIds.erase(it);
1252                         continue;
1253                     }
1254                 }
1255 
1256                 ++it;
1257             }
1258         }
1259     }
1260 
1261     modelRemovals.clear();
1262 
1263     return toContiguousPairs(oldIds.values());
1264 }
1265 
toContiguousPairs(const QList<int> & unsorted)1266 QList<QPair<int, int> > ItemModelIncrementalUpdater::toContiguousPairs(const QList<int>& unsorted)
1267 {
1268     // Take the given indices and return them as contiguous pairs [begin, end]
1269 
1270     QList<QPair<int, int> > pairs;
1271 
1272     if (unsorted.isEmpty())
1273     {
1274         return pairs;
1275     }
1276 
1277     QList<int> indices(unsorted);
1278     std::sort(indices.begin(), indices.end());
1279 
1280     QPair<int, int> pair(indices.first(), indices.first());
1281 
1282     for (int i = 1 ; i < indices.size() ; ++i)
1283     {
1284         const int& index = indices.at(i);
1285 
1286         if (index == (pair.second + 1))
1287         {
1288             pair.second = index;
1289             continue;
1290         }
1291 
1292         pairs << pair;          // insert last pair
1293         pair.first  = index;
1294         pair.second = index;
1295     }
1296 
1297     pairs << pair;
1298 
1299     return pairs;
1300 }
1301 
1302 // ------------ QAbstractItemModel implementation -------------
1303 
data(const QModelIndex & index,int role) const1304 QVariant ItemModel::data(const QModelIndex& index, int role) const
1305 {
1306     if (!d->isValid(index))
1307     {
1308         return QVariant();
1309     }
1310 
1311     switch (role)
1312     {
1313         case Qt::DisplayRole:
1314         case Qt::ToolTipRole:
1315         {
1316             return d->infos.at(index.row()).name();
1317         }
1318 
1319         case ItemModelPointerRole:
1320         {
1321             return QVariant::fromValue(const_cast<ItemModel*>(this));
1322         }
1323 
1324         case ItemModelInternalId:
1325         {
1326             return index.row();
1327         }
1328 
1329         case CreationDateRole:
1330         {
1331             return d->infos.at(index.row()).dateTime();
1332         }
1333 
1334         case ExtraDataRole:
1335         {
1336             if (d->extraValueValid(index))
1337             {
1338                 return d->extraValues.at(index.row());
1339             }
1340             else
1341             {
1342                 return QVariant();
1343             }
1344         }
1345 
1346         case ExtraDataDuplicateCount:
1347         {
1348             qlonglong id = d->infos.at(index.row()).id();
1349 
1350             return numberOfIndexesForImageId(id);
1351         }
1352     }
1353 
1354     return QVariant();
1355 }
1356 
headerData(int section,Qt::Orientation orientation,int role) const1357 QVariant ItemModel::headerData(int section, Qt::Orientation orientation, int role) const
1358 {
1359     Q_UNUSED(section)
1360     Q_UNUSED(orientation)
1361     Q_UNUSED(role)
1362 
1363     return QVariant();
1364 }
1365 
rowCount(const QModelIndex & parent) const1366 int ItemModel::rowCount(const QModelIndex& parent) const
1367 {
1368     if (parent.isValid())
1369     {
1370         return 0;
1371     }
1372 
1373     return d->infos.size();
1374 }
1375 
flags(const QModelIndex & index) const1376 Qt::ItemFlags ItemModel::flags(const QModelIndex& index) const
1377 {
1378     if (!d->isValid(index))
1379     {
1380         return Qt::NoItemFlags;
1381     }
1382 
1383     Qt::ItemFlags f = Qt::ItemIsSelectable | Qt::ItemIsEnabled;
1384     f              |= dragDropFlags(index);
1385 
1386     return f;
1387 }
1388 
index(int row,int column,const QModelIndex & parent) const1389 QModelIndex ItemModel::index(int row, int column, const QModelIndex& parent) const
1390 {
1391     if ((column != 0) || (row < 0) || parent.isValid() || (row >= d->infos.size()))
1392     {
1393         return QModelIndex();
1394     }
1395 
1396     return createIndex(row, 0);
1397 }
1398 
1399 // ------------ Database watch -------------
1400 
slotImageChange(const ImageChangeset & changeset)1401 void ItemModel::slotImageChange(const ImageChangeset& changeset)
1402 {
1403     if (d->infos.isEmpty())
1404     {
1405         return;
1406     }
1407 
1408     if (d->watchFlags & changeset.changes())
1409     {
1410         QItemSelection items;
1411 
1412         foreach (const qlonglong& id, changeset.ids())
1413         {
1414             QModelIndex index = indexForImageId(id);
1415 
1416             if (index.isValid())
1417             {
1418                 items.select(index, index);
1419             }
1420         }
1421 
1422         if (!items.isEmpty())
1423         {
1424             emitDataChangedForSelection(items);
1425             emit imageChange(changeset, items);
1426         }
1427     }
1428 }
1429 
slotImageTagChange(const ImageTagChangeset & changeset)1430 void ItemModel::slotImageTagChange(const ImageTagChangeset& changeset)
1431 {
1432     if (d->infos.isEmpty())
1433     {
1434         return;
1435     }
1436 
1437     QItemSelection items;
1438 
1439     foreach (const qlonglong& id, changeset.ids())
1440     {
1441         QModelIndex index = indexForImageId(id);
1442 
1443         if (index.isValid())
1444         {
1445             items.select(index, index);
1446         }
1447     }
1448 
1449     if (!items.isEmpty())
1450     {
1451         emitDataChangedForSelection(items);
1452         emit imageTagChange(changeset, items);
1453     }
1454 }
1455 
1456 } // namespace Digikam
1457