File indexing completed on 2025-01-19 03:50:40

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2009-04-22
0007  * Description : Qt model-view for items
0008  *
0009  * SPDX-FileCopyrightText: 2009-2012 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
0010  * SPDX-FileCopyrightText: 2017      by Simon Frei <freisim93 at gmail dot com>
0011  *
0012  * SPDX-License-Identifier: GPL-2.0-or-later
0013  *
0014  * ============================================================ */
0015 
0016 #include "itemcategorizedview.h"
0017 
0018 // Qt includes
0019 
0020 #include <QApplication>
0021 #include <QTimer>
0022 
0023 // Local includes
0024 
0025 #include "digikam_debug.h"
0026 #include "album.h"
0027 #include "albummanager.h"
0028 #include "coredbfields.h"
0029 #include "iccsettings.h"
0030 #include "itemalbummodel.h"
0031 #include "itemalbumfiltermodel.h"
0032 #include "itemcategorydrawer.h"
0033 #include "itemdelegate.h"
0034 #include "itemdelegateoverlay.h"
0035 #include "itemthumbnailmodel.h"
0036 #include "itemselectionoverlay.h"
0037 #include "itemviewtooltip.h"
0038 #include "loadingcacheinterface.h"
0039 #include "thumbnailloadthread.h"
0040 #include "tooltipfiller.h"
0041 #include "itemfacedelegate.h"
0042 
0043 namespace Digikam
0044 {
0045 
0046 class Q_DECL_HIDDEN ItemCategorizedViewToolTip : public ItemViewToolTip
0047 {
0048     Q_OBJECT
0049 
0050 public:
0051 
0052     explicit ItemCategorizedViewToolTip(ItemCategorizedView* const view)
0053         : ItemViewToolTip(view)
0054     {
0055     }
0056 
0057     ItemCategorizedView* view() const
0058     {
0059         return static_cast<ItemCategorizedView*>(ItemViewToolTip::view());
0060     }
0061 
0062 protected:
0063 
0064     QString tipContents() override
0065     {
0066         ItemInfo info = ItemModel::retrieveItemInfo(currentIndex());
0067 
0068         return ToolTipFiller::imageInfoTipContents(info);
0069     }
0070 };
0071 
0072 // -------------------------------------------------------------------------------
0073 
0074 class Q_DECL_HIDDEN ItemCategorizedView::Private
0075 {
0076 public:
0077 
0078     explicit Private()
0079       : model            (nullptr),
0080         filterModel      (nullptr),
0081         delegate         (nullptr),
0082         showToolTip      (false),
0083         scrollToItemId   (0),
0084         delayedEnterTimer(nullptr)
0085     {
0086     }
0087 
0088     ItemModel*            model;
0089     ImageSortFilterModel* filterModel;
0090 
0091     ItemDelegate*         delegate;
0092     bool                  showToolTip;
0093 
0094     qlonglong             scrollToItemId;
0095 
0096     QUrl                  unknownCurrentUrl;
0097 
0098     QTimer*               delayedEnterTimer;
0099 };
0100 
0101 // -------------------------------------------------------------------------------
0102 
0103 ItemCategorizedView::ItemCategorizedView(QWidget* const parent)
0104     : ItemViewCategorized(parent),
0105       d                  (new Private)
0106 {
0107     setToolTip(new ItemCategorizedViewToolTip(this));
0108 
0109     LoadingCacheInterface::connectToSignalFileChanged(this,
0110             SLOT(slotFileChanged(QString)));
0111 
0112     connect(IccSettings::instance(), SIGNAL(signalICCSettingsChanged(ICCSettingsContainer,ICCSettingsContainer)),
0113             this, SLOT(slotIccSettingsChanged(ICCSettingsContainer,ICCSettingsContainer)));
0114 
0115     d->delayedEnterTimer = new QTimer(this);
0116     d->delayedEnterTimer->setInterval(10);
0117     d->delayedEnterTimer->setSingleShot(true);
0118 
0119     connect(d->delayedEnterTimer, SIGNAL(timeout()),
0120             this, SLOT(slotDelayedEnter()));
0121 }
0122 
0123 ItemCategorizedView::~ItemCategorizedView()
0124 {
0125     d->delegate->removeAllOverlays();
0126     delete d;
0127 }
0128 
0129 void ItemCategorizedView::installDefaultModels()
0130 {
0131     ItemAlbumModel* model             = new ItemAlbumModel(this);
0132     ItemAlbumFilterModel* filterModel = new ItemAlbumFilterModel(this);
0133 
0134     filterModel->setSourceItemModel(model);
0135 
0136     filterModel->setSortRole(ItemSortSettings::SortByFileName);
0137     filterModel->setCategorizationMode(ItemSortSettings::CategoryByAlbum);
0138     filterModel->sort(0); // an initial sorting is necessary
0139 
0140     // set flags that we want to get dataChanged() signals for
0141 
0142     model->setWatchFlags(filterModel->suggestedWatchFlags());
0143 
0144     setModels(model, filterModel);
0145 }
0146 
0147 void ItemCategorizedView::setModels(ItemModel* model, ImageSortFilterModel* filterModel)
0148 {
0149     if (d->delegate)
0150     {
0151         d->delegate->setAllOverlaysActive(false);
0152     }
0153 
0154     if (d->filterModel)
0155     {
0156         disconnect(d->filterModel, SIGNAL(layoutAboutToBeChanged()),
0157                    this, SLOT(layoutAboutToBeChanged()));
0158 
0159         disconnect(d->filterModel, SIGNAL(layoutChanged()),
0160                    this, SLOT(layoutWasChanged()));
0161     }
0162 
0163     if (d->model)
0164     {
0165         disconnect(d->model, SIGNAL(imageInfosAdded(QList<ItemInfo>)),
0166                    this, SLOT(slotItemInfosAdded()));
0167     }
0168 
0169     d->model       = model;
0170     d->filterModel = filterModel;
0171 
0172     setModel(d->filterModel);
0173 
0174     connect(d->filterModel, SIGNAL(layoutAboutToBeChanged()),
0175             this, SLOT(layoutAboutToBeChanged()));
0176 
0177     connect(d->filterModel, SIGNAL(layoutChanged()),
0178             this, SLOT(layoutWasChanged()),
0179             Qt::QueuedConnection);
0180 
0181     connect(d->model, SIGNAL(imageInfosAdded(QList<ItemInfo>)),
0182             this, SLOT(slotItemInfosAdded()));
0183 
0184     Q_EMIT modelChanged();
0185 
0186     if (d->delegate)
0187     {
0188         d->delegate->setAllOverlaysActive(true);
0189     }
0190 }
0191 
0192 ItemModel* ItemCategorizedView::imageModel() const
0193 {
0194     return d->model;
0195 }
0196 
0197 ImageSortFilterModel* ItemCategorizedView::imageSortFilterModel() const
0198 {
0199     return d->filterModel;
0200 }
0201 
0202 ItemFilterModel* ItemCategorizedView::imageFilterModel() const
0203 {
0204     return d->filterModel->imageFilterModel();
0205 }
0206 
0207 ItemThumbnailModel* ItemCategorizedView::imageThumbnailModel() const
0208 {
0209     return qobject_cast<ItemThumbnailModel*>(d->model);
0210 }
0211 
0212 ItemAlbumModel* ItemCategorizedView::imageAlbumModel() const
0213 {
0214     return qobject_cast<ItemAlbumModel*>(d->model);
0215 }
0216 
0217 ItemAlbumFilterModel* ItemCategorizedView::imageAlbumFilterModel() const
0218 {
0219     return qobject_cast<ItemAlbumFilterModel*>(d->filterModel->imageFilterModel());
0220 }
0221 
0222 QSortFilterProxyModel* ItemCategorizedView::filterModel() const
0223 {
0224     return d->filterModel;
0225 }
0226 
0227 ItemDelegate* ItemCategorizedView::delegate() const
0228 {
0229     return d->delegate;
0230 }
0231 
0232 void ItemCategorizedView::setItemDelegate(ItemDelegate* delegate)
0233 {
0234     ThumbnailSize oldSize     = thumbnailSize();
0235     ItemDelegate* oldDelegate = d->delegate;
0236 
0237     if (oldDelegate)
0238     {
0239         hideIndexNotification();
0240         d->delegate->setAllOverlaysActive(false);
0241         d->delegate->setViewOnAllOverlays(nullptr);
0242 
0243         // Note: Be precise, no wildcard disconnect!
0244 
0245         disconnect(d->delegate, SIGNAL(requestNotification(QModelIndex,QString)),
0246                    this, SLOT(showIndexNotification(QModelIndex,QString)));
0247 
0248         disconnect(d->delegate, SIGNAL(hideNotification()),
0249                    this, SLOT(hideIndexNotification()));
0250     }
0251 
0252     d->delegate = delegate;
0253     d->delegate->setThumbnailSize(oldSize);
0254 
0255     if (oldDelegate)
0256     {
0257         d->delegate->setSpacing(oldDelegate->spacing());
0258     }
0259 
0260     ItemViewCategorized::setItemDelegate(d->delegate);
0261     setCategoryDrawer(d->delegate->categoryDrawer());
0262     updateDelegateSizes();
0263 
0264     d->delegate->setViewOnAllOverlays(this);
0265     d->delegate->setAllOverlaysActive(true);
0266 
0267     connect(d->delegate, SIGNAL(requestNotification(QModelIndex,QString)),
0268             this, SLOT(showIndexNotification(QModelIndex,QString)));
0269 
0270     connect(d->delegate, SIGNAL(hideNotification()),
0271             this, SLOT(hideIndexNotification()));
0272 }
0273 
0274 Album* ItemCategorizedView::currentAlbum() const
0275 {
0276     ItemAlbumModel* const albumModel = imageAlbumModel();
0277 
0278     // TODO: Change to QList return type
0279 
0280     if (albumModel && !(albumModel->currentAlbums().isEmpty()))
0281     {
0282         return albumModel->currentAlbums().constFirst();
0283     }
0284 
0285     return nullptr;
0286 }
0287 
0288 ItemInfo ItemCategorizedView::currentInfo() const
0289 {
0290     return imageInfo(currentIndex());
0291 }
0292 
0293 QUrl ItemCategorizedView::currentUrl() const
0294 {
0295     return currentInfo().fileUrl();
0296 }
0297 
0298 ItemInfo ItemCategorizedView::imageInfo(const QModelIndex& index) const
0299 {
0300     return d->filterModel->imageInfo(index);
0301 }
0302 
0303 ItemInfoList ItemCategorizedView::imageInfos(const QList<QModelIndex>& indexes) const
0304 {
0305     return ItemInfoList(d->filterModel->imageInfos(indexes));
0306 }
0307 
0308 ItemInfoList ItemCategorizedView::allItemInfos() const
0309 {
0310     return ItemInfoList(d->filterModel->imageInfosSorted());
0311 }
0312 
0313 QList<QUrl> ItemCategorizedView::allUrls() const
0314 {
0315     return allItemInfos().toImageUrlList();
0316 }
0317 
0318 ItemInfoList ItemCategorizedView::selectedItemInfos() const
0319 {
0320     return imageInfos(selectedIndexes());
0321 }
0322 
0323 ItemInfoList ItemCategorizedView::selectedItemInfosCurrentFirst() const
0324 {
0325     QModelIndexList indexes   = selectedIndexes();
0326     const QModelIndex current = currentIndex();
0327 
0328     if (!indexes.isEmpty())
0329     {
0330         if (indexes.first() != current)
0331         {
0332             if (indexes.removeOne(current))
0333             {
0334                 indexes.prepend(current);
0335             }
0336         }
0337     }
0338 
0339     return imageInfos(indexes);
0340 }
0341 
0342 void ItemCategorizedView::toIndex(const QUrl& url)
0343 {
0344     ItemViewCategorized::toIndex(d->filterModel->indexForPath(url.toLocalFile()));
0345 }
0346 
0347 QModelIndex ItemCategorizedView::indexForInfo(const ItemInfo& info) const
0348 {
0349     return d->filterModel->indexForItemInfo(info);
0350 }
0351 
0352 ItemInfo ItemCategorizedView::nextInOrder(const ItemInfo& startingPoint, int nth)
0353 {
0354     QModelIndex index = d->filterModel->indexForItemInfo(startingPoint);
0355 
0356     if (!index.isValid())
0357     {
0358         return ItemInfo();
0359     }
0360 
0361     return imageInfo(d->filterModel->index(index.row() + nth, 0, QModelIndex()));
0362 }
0363 
0364 QModelIndex ItemCategorizedView::nextIndexHint(const QModelIndex& anchor, const QItemSelectionRange& removed) const
0365 {
0366     QModelIndex hint = ItemViewCategorized::nextIndexHint(anchor, removed);
0367     ItemInfo info    = imageInfo(anchor);
0368 /*
0369     qCDebug(DIGIKAM_GENERAL_LOG) << "Having initial hint" << hint << "for" << anchor << d->model->numberOfIndexesForItemInfo(info);
0370 */
0371     // Fixes a special case of multiple (face) entries for the same image.
0372     // If one is removed, any entry of the same image shall be preferred.
0373 
0374     if (d->model->numberOfIndexesForItemInfo(info) > 1)
0375     {
0376         // The hint is for a different info, but we may have a hint for the same info
0377 
0378         if (info != imageInfo(hint))
0379         {
0380             int minDiff                           = d->filterModel->rowCount();
0381             QList<QModelIndex> indexesForItemInfo = d->filterModel->mapListFromSource(d->model->indexesForItemInfo(info));
0382 
0383             Q_FOREACH (const QModelIndex& index, indexesForItemInfo)
0384             {
0385                 if ((index == anchor) || !index.isValid() || removed.contains(index))
0386                 {
0387                     continue;
0388                 }
0389 
0390                 int distance = qAbs(index.row() - anchor.row());
0391 
0392                 if (distance < minDiff)
0393                 {
0394                     minDiff = distance;
0395                     hint    = index;
0396 
0397 /*                  qCDebug(DIGIKAM_GENERAL_LOG) << "Chose index" << hint << "at distance" << minDiff << "to" << anchor;
0398 
0399 */                }
0400             }
0401         }
0402     }
0403 
0404     return hint;
0405 }
0406 
0407 void ItemCategorizedView::openAlbum(const QList<Album*>& albums)
0408 {
0409     ItemAlbumModel* const albumModel = imageAlbumModel();
0410 
0411     if (albumModel)
0412     {
0413         albumModel->openAlbum(albums);
0414     }
0415 }
0416 
0417 ThumbnailSize ItemCategorizedView::thumbnailSize() const
0418 {
0419 /*
0420     ItemThumbnailModel* const thumbModel = imageThumbnailModel();
0421 
0422     if (thumbModel)
0423     {
0424         return thumbModel->thumbnailSize();
0425     }
0426 */
0427     if (d->delegate)
0428     {
0429         return d->delegate->thumbnailSize();
0430     }
0431 
0432     return ThumbnailSize();
0433 }
0434 
0435 void ItemCategorizedView::setThumbnailSize(int size)
0436 {
0437     setThumbnailSize(ThumbnailSize(size));
0438 }
0439 
0440 void ItemCategorizedView::setThumbnailSize(const ThumbnailSize& s)
0441 {
0442     // we abuse this pair of method calls to restore scroll position
0443 
0444     layoutAboutToBeChanged();
0445     d->delegate->setThumbnailSize(s);
0446     layoutWasChanged();
0447 }
0448 
0449 void ItemCategorizedView::setCurrentWhenAvailable(qlonglong imageId)
0450 {
0451     d->scrollToItemId = imageId;
0452 }
0453 
0454 void ItemCategorizedView::setCurrentUrlWhenAvailable(const QUrl& url)
0455 {
0456     if (url.isEmpty())
0457     {
0458         clearSelection();
0459         setCurrentIndex(QModelIndex());
0460         return;
0461     }
0462 
0463     QString path      = url.toLocalFile();
0464     QModelIndex index = d->filterModel->indexForPath(path);
0465 
0466     if (!index.isValid())
0467     {
0468         d->unknownCurrentUrl = url;
0469         return;
0470     }
0471 
0472     clearSelection();
0473     setCurrentIndex(index);
0474     d->unknownCurrentUrl.clear();
0475 }
0476 
0477 void ItemCategorizedView::setCurrentUrl(const QUrl& url)
0478 {
0479     if (url.isEmpty())
0480     {
0481         clearSelection();
0482         setCurrentIndex(QModelIndex());
0483         return;
0484     }
0485 
0486     QString path      = url.toLocalFile();
0487     QModelIndex index = d->filterModel->indexForPath(path);
0488 
0489     if (!index.isValid())
0490     {
0491         return;
0492     }
0493 
0494     clearSelection();
0495     setCurrentIndex(index);
0496 }
0497 
0498 void ItemCategorizedView::setCurrentInfo(const ItemInfo& info)
0499 {
0500     QModelIndex index = d->filterModel->indexForItemInfo(info);
0501     clearSelection();
0502     setCurrentIndex(index);
0503 }
0504 
0505 void ItemCategorizedView::setSelectedUrls(const QList<QUrl>& urlList)
0506 {
0507     QItemSelection mySelection;
0508 
0509     for (QList<QUrl>::const_iterator it = urlList.constBegin() ; it!=urlList.constEnd() ; ++it)
0510     {
0511         const QString path      = it->toLocalFile();
0512         const QModelIndex index = d->filterModel->indexForPath(path);
0513 
0514         if (!index.isValid())
0515         {
0516             qCWarning(DIGIKAM_GENERAL_LOG) << "no QModelIndex found for" << *it;
0517         }
0518         else
0519         {
0520             // TODO: is there a better way?
0521 
0522             mySelection.select(index, index);
0523         }
0524     }
0525 
0526     clearSelection();
0527     selectionModel()->select(mySelection, QItemSelectionModel::Select);
0528 }
0529 
0530 void ItemCategorizedView::setSelectedItemInfos(const QList<ItemInfo>& infos)
0531 {
0532     QItemSelection mySelection;
0533 
0534     Q_FOREACH (const ItemInfo& info, infos)
0535     {
0536         QModelIndex index = d->filterModel->indexForItemInfo(info);
0537         mySelection.select(index, index);
0538     }
0539 
0540     selectionModel()->select(mySelection, QItemSelectionModel::ClearAndSelect);
0541 }
0542 
0543 void ItemCategorizedView::hintAt(const ItemInfo& info)
0544 {
0545     if (info.isNull())
0546     {
0547         return;
0548     }
0549 
0550     QModelIndex index = d->filterModel->indexForItemInfo(info);
0551 
0552     if (!index.isValid())
0553     {
0554         return;
0555     }
0556 
0557     selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate);
0558     scrollTo(index);
0559 }
0560 
0561 void ItemCategorizedView::addOverlay(ItemDelegateOverlay* overlay, ItemDelegate* delegate)
0562 {
0563     if (!delegate)
0564     {
0565         delegate = d->delegate;
0566     }
0567 
0568     delegate->installOverlay(overlay);
0569 
0570     if (delegate == d->delegate)
0571     {
0572         overlay->setView(this);
0573         overlay->setActive(true);
0574     }
0575 }
0576 
0577 void ItemCategorizedView::removeOverlay(ItemDelegateOverlay* overlay)
0578 {
0579     ItemDelegate* const delegate = dynamic_cast<ItemDelegate*>(overlay->delegate());
0580 
0581     if (delegate)
0582     {
0583         delegate->removeOverlay(overlay);
0584     }
0585 
0586     overlay->setView(nullptr);
0587 }
0588 
0589 void ItemCategorizedView::updateGeometries()
0590 {
0591     ItemViewCategorized::updateGeometries();
0592     d->delayedEnterTimer->start();
0593 }
0594 
0595 void ItemCategorizedView::slotDelayedEnter()
0596 {
0597     // re-Q_EMIT entered() for index under mouse (after layout).
0598 
0599     QModelIndex mouseIndex = indexAt(mapFromGlobal(QCursor::pos()));
0600 
0601     if (mouseIndex.isValid())
0602     {
0603         Q_EMIT DCategorizedView::entered(mouseIndex);
0604     }
0605 }
0606 
0607 void ItemCategorizedView::addSelectionOverlay(ItemDelegate* delegate)
0608 {
0609     addOverlay(new ItemSelectionOverlay(this), delegate);
0610 }
0611 
0612 void ItemCategorizedView::scrollToStoredItem()
0613 {
0614     if (d->scrollToItemId)
0615     {
0616         if (d->model->hasImage(d->scrollToItemId))
0617         {
0618             QModelIndex index = d->filterModel->indexForImageId(d->scrollToItemId);
0619             setCurrentIndex(index);
0620             scrollToRelaxed(index, QAbstractItemView::PositionAtCenter);
0621             d->scrollToItemId = 0;
0622         }
0623     }
0624 }
0625 
0626 void ItemCategorizedView::slotItemInfosAdded()
0627 {
0628     if      (d->scrollToItemId)
0629     {
0630         scrollToStoredItem();
0631     }
0632     else if (!d->unknownCurrentUrl.isEmpty())
0633     {
0634         QTimer::singleShot(100, this, SLOT(slotCurrentUrlTimer()));
0635     }
0636 }
0637 
0638 void ItemCategorizedView::slotCurrentUrlTimer()
0639 {
0640     setCurrentUrl(d->unknownCurrentUrl);
0641     d->unknownCurrentUrl.clear();
0642 }
0643 
0644 void ItemCategorizedView::slotFileChanged(const QString& filePath)
0645 {
0646     QModelIndex index = d->filterModel->indexForPath(filePath);
0647 
0648     if (index.isValid())
0649     {
0650         update(index);
0651     }
0652 }
0653 
0654 void ItemCategorizedView::indexActivated(const QModelIndex& index, Qt::KeyboardModifiers modifiers)
0655 {
0656     ItemInfo info = imageInfo(index);
0657 
0658     if (!info.isNull())
0659     {
0660         activated(info, modifiers);
0661         Q_EMIT imageActivated(info);
0662     }
0663 }
0664 
0665 void ItemCategorizedView::currentChanged(const QModelIndex& index, const QModelIndex& previous)
0666 {
0667     ItemViewCategorized::currentChanged(index, previous);
0668 
0669     Q_EMIT currentChanged(imageInfo(index));
0670 }
0671 
0672 void ItemCategorizedView::selectionChanged(const QItemSelection& selectedItems, const QItemSelection& deselectedItems)
0673 {
0674     ItemViewCategorized::selectionChanged(selectedItems, deselectedItems);
0675 
0676     if (!selectedItems.isEmpty())
0677     {
0678         Q_EMIT selected(imageInfos(selectedItems.indexes()));
0679     }
0680 
0681     if (!deselectedItems.isEmpty())
0682     {
0683         Q_EMIT deselected(imageInfos(deselectedItems.indexes()));
0684     }
0685 }
0686 
0687 Album* ItemCategorizedView::albumAt(const QPoint& pos) const
0688 {
0689     if (imageFilterModel()->imageSortSettings().categorizationMode == ItemSortSettings::CategoryByAlbum)
0690     {
0691         QModelIndex categoryIndex = indexForCategoryAt(pos);
0692 
0693         if (categoryIndex.isValid())
0694         {
0695             int albumId = categoryIndex.data(ItemFilterModel::CategoryAlbumIdRole).toInt();
0696 
0697             return AlbumManager::instance()->findPAlbum(albumId);
0698         }
0699     }
0700 
0701     return currentAlbum();
0702 }
0703 
0704 void ItemCategorizedView::activated(const ItemInfo&, Qt::KeyboardModifiers)
0705 {
0706     // implemented in subclass
0707 }
0708 
0709 void ItemCategorizedView::showContextMenuOnIndex(QContextMenuEvent* event, const QModelIndex& index)
0710 {
0711     showContextMenuOnInfo(event, imageInfo(index));
0712 }
0713 
0714 void ItemCategorizedView::showContextMenuOnInfo(QContextMenuEvent*, const ItemInfo&)
0715 {
0716     // implemented in subclass
0717 }
0718 
0719 QItemSelectionModel* ItemCategorizedView::getSelectionModel() const
0720 {
0721     return selectionModel();
0722 }
0723 
0724 AbstractItemDragDropHandler* ItemCategorizedView::dragDropHandler() const
0725 {
0726     return d->model->dragDropHandler();
0727 }
0728 
0729 void ItemCategorizedView::slotIccSettingsChanged(const ICCSettingsContainer&, const ICCSettingsContainer&)
0730 {
0731     viewport()->update();
0732 }
0733 
0734 } // namespace Digikam
0735 
0736 #include "itemcategorizedview.moc"
0737 
0738 #include "moc_itemcategorizedview.cpp"