1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2009-04-19
7  * Description : Qt model-view for items - the delegate
8  *
9  * Copyright (C) 2002-2005 by Renchi Raju <renchi dot raju at gmail dot com>
10  * Copyright (C) 2009-2011 by Andi Clemens <andi dot clemens at gmail dot com>
11  * Copyright (C) 2002-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
12  * Copyright (C) 2006-2011 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
13  *
14  * This program is free software; you can redistribute it
15  * and/or modify it under the terms of the GNU General
16  * Public License as published by the Free Software Foundation;
17  * either version 2, or (at your option)
18  * any later version.
19  *
20  * This program is distributed in the hope that it will be useful,
21  * but WITHOUT ANY WARRANTY; without even the implied warranty of
22  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23  * GNU General Public License for more details.
24  *
25  * ============================================================ */
26 
27 #include "itemdelegate.h"
28 #include "itemdelegate_p.h"
29 
30 // C++ includes
31 
32 #include <cmath>
33 
34 // Qt includes
35 
36 #include <QCache>
37 #include <QPainter>
38 #include <QIcon>
39 #include <QApplication>
40 
41 // KDE includes
42 
43 #include <klocalizedstring.h>
44 
45 // Local includes
46 
47 #include "digikam_debug.h"
48 #include "albummanager.h"
49 #include "itemcategorydrawer.h"
50 #include "itemcategorizedview.h"
51 #include "itemdelegateoverlay.h"
52 #include "itemmodel.h"
53 #include "itemfiltermodel.h"
54 #include "itemthumbnailmodel.h"
55 #include "thumbnailloadthread.h"
56 #include "applicationsettings.h"
57 
58 namespace Digikam
59 {
60 
clearRects()61 void ItemDelegate::ItemDelegatePrivate::clearRects()
62 {
63     ItemViewDelegatePrivate::clearRects();
64     dateRect             = QRect(0, 0, 0, 0);
65     modDateRect          = QRect(0, 0, 0, 0);
66     pixmapRect           = QRect(0, 0, 0, 0);
67     nameRect             = QRect(0, 0, 0, 0);
68     titleRect            = QRect(0, 0, 0, 0);
69     commentsRect         = QRect(0, 0, 0, 0);
70     resolutionRect       = QRect(0, 0, 0, 0);
71     coordinatesRect      = QRect(0, 0, 0, 0);
72     arRect               = QRect(0, 0, 0, 0);
73     sizeRect             = QRect(0, 0, 0, 0);
74     tagRect              = QRect(0, 0, 0, 0);
75     imageInformationRect = QRect(0, 0, 0, 0);
76     pickLabelRect        = QRect(0, 0, 0, 0);
77     groupRect            = QRect(0, 0, 0, 0);
78 }
79 
ItemDelegate(QObject * const parent)80 ItemDelegate::ItemDelegate(QObject* const parent)
81     : ItemViewDelegate(*new ItemDelegatePrivate, parent)
82 {
83 }
84 
ItemDelegate(ItemDelegate::ItemDelegatePrivate & dd,QObject * const parent)85 ItemDelegate::ItemDelegate(ItemDelegate::ItemDelegatePrivate& dd, QObject* const parent)
86     : ItemViewDelegate(dd, parent)
87 {
88 }
89 
~ItemDelegate()90 ItemDelegate::~ItemDelegate()
91 {
92     Q_D(ItemDelegate);
93     // crashes for a lot of people, see bug 230515. Cause unknown.
94     //delete d->categoryDrawer;
95     Q_UNUSED(d); // To please compiler about warnings.
96 }
97 
setView(ItemCategorizedView * view)98 void ItemDelegate::setView(ItemCategorizedView* view)
99 {
100     Q_D(ItemDelegate);
101     setViewOnAllOverlays(view);
102 
103     if (d->currentView)
104     {
105         disconnect(d->currentView, SIGNAL(modelChanged()),
106                    this, SLOT(modelChanged()));
107     }
108 
109     d->currentView = view;
110 
111     setModel(view ? view->model() : nullptr);
112 
113     if (d->currentView)
114     {
115         connect(d->currentView, SIGNAL(modelChanged()),
116                 this, SLOT(modelChanged()));
117     }
118 }
119 
setModel(QAbstractItemModel * model)120 void ItemDelegate::setModel(QAbstractItemModel* model)
121 {
122     Q_D(ItemDelegate);
123 
124     // 1) We only need the model to invalidate model-index based caches on change
125     // 2) We do not need to care for overlays. The view calls setActive() on them on model change
126 
127     if (model == d->currentModel)
128     {
129         return;
130     }
131 
132     if (d->currentModel)
133     {
134         disconnect(d->currentModel, nullptr, this, nullptr);
135     }
136 
137     d->currentModel = model;
138 
139     if (d->currentModel)
140     {
141         connect(d->currentModel, SIGNAL(layoutAboutToBeChanged()),
142                 this, SLOT(modelContentsChanged()));
143 
144         connect(d->currentModel, SIGNAL(modelAboutToBeReset()),
145                 this, SLOT(modelContentsChanged()));
146 
147         connect(d->currentModel, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)),
148                 this, SLOT(modelContentsChanged()));
149 
150         connect(d->currentModel, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)),
151                 this, SLOT(modelContentsChanged()));
152 
153         connect(d->currentModel, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
154                 this, SLOT(modelContentsChanged()));
155     }
156 }
157 
setSpacing(int spacing)158 void ItemDelegate::setSpacing(int spacing)
159 {
160     Q_D(ItemDelegate);
161 
162     if (d->categoryDrawer)
163     {
164         d->categoryDrawer->setLowerSpacing(spacing);
165     }
166 
167     ItemViewDelegate::setSpacing(spacing);
168 }
169 
categoryDrawer() const170 ItemCategoryDrawer* ItemDelegate::categoryDrawer() const
171 {
172     Q_D(const ItemDelegate);
173     return d->categoryDrawer;
174 }
175 
commentsRect() const176 QRect ItemDelegate::commentsRect() const
177 {
178     Q_D(const ItemDelegate);
179     return d->commentsRect;
180 }
181 
tagsRect() const182 QRect ItemDelegate::tagsRect() const
183 {
184     Q_D(const ItemDelegate);
185     return d->tagRect;
186 }
187 
pixmapRect() const188 QRect ItemDelegate::pixmapRect() const
189 {
190     Q_D(const ItemDelegate);
191     return d->pixmapRect;
192 }
193 
imageInformationRect() const194 QRect ItemDelegate::imageInformationRect() const
195 {
196     Q_D(const ItemDelegate);
197     return d->imageInformationRect;
198 }
199 
groupIndicatorRect() const200 QRect ItemDelegate::groupIndicatorRect() const
201 {
202     Q_D(const ItemDelegate);
203     return d->groupRect;
204 }
205 
coordinatesIndicatorRect() const206 QRect ItemDelegate::coordinatesIndicatorRect() const
207 {
208     Q_D(const ItemDelegate);
209     return d->coordinatesRect;
210 }
211 
retrieveThumbnailPixmap(const QModelIndex & index,int thumbnailSize)212 QPixmap ItemDelegate::retrieveThumbnailPixmap(const QModelIndex& index, int thumbnailSize)
213 {
214     // work around constness
215     QAbstractItemModel* const model = const_cast<QAbstractItemModel*>(index.model());
216     // set requested thumbnail size
217     model->setData(index, thumbnailSize, ItemModel::ThumbnailRole);
218     // get data from model
219     QVariant thumbData              = index.data(ItemModel::ThumbnailRole);
220     // reset to default thumbnail size
221     model->setData(index, QVariant(), ItemModel::ThumbnailRole);
222 
223     return thumbData.value<QPixmap>();
224 }
225 
thumbnailPixmap(const QModelIndex & index) const226 QPixmap ItemDelegate::thumbnailPixmap(const QModelIndex& index) const
227 {
228     Q_D(const ItemDelegate);
229     return retrieveThumbnailPixmap(index, d->thumbSize.size());
230 }
231 
paint(QPainter * p,const QStyleOptionViewItem & option,const QModelIndex & index) const232 void ItemDelegate::paint(QPainter* p, const QStyleOptionViewItem& option, const QModelIndex& index) const
233 {
234     Q_D(const ItemDelegate);
235     ItemInfo info = ItemModel::retrieveItemInfo(index);
236 
237     if (info.isNull())
238     {
239         return;
240     }
241 
242     // state of painter must not be changed
243     p->save();
244     p->translate(option.rect.topLeft());
245 
246     bool isSelected = (option.state & QStyle::State_Selected);
247 
248     // Thumbnail
249     QPixmap pix;
250 
251     if (isSelected)
252     {
253         pix = d->selPixmap;
254     }
255     else
256     {
257         pix = d->regPixmap;
258     }
259 
260     bool groupedAndClosed  = (info.hasGroupedImages() &&
261                               !index.data(ItemFilterModel::GroupIsOpenRole).toBool() &&
262                               ApplicationSettings::instance()->getDrawFramesToGrouped());
263 
264     QRect actualPixmapRect = drawThumbnail(p, d->pixmapRect,
265                                            pix, thumbnailPixmap(index),
266                                            groupedAndClosed);
267 
268     if (!actualPixmapRect.isNull())
269     {
270         const_cast<ItemDelegate*>(this)->updateActualPixmapRect(index, actualPixmapRect);
271     }
272 
273     if (!d->ratingRect.isNull())
274     {
275         drawRating(p, index, d->ratingRect, info.rating(), isSelected);
276     }
277 
278     // Draw Color Label rectangle
279     drawColorLabelRect(p, option, isSelected, info.colorLabel());
280 
281     p->setPen(isSelected ? qApp->palette().color(QPalette::HighlightedText)
282                          : qApp->palette().color(QPalette::Text));
283 
284 /*
285     // If there is ImageHistory present, paint a small icon over the thumbnail to indicate that this is derived image
286     if (info.hasImageHistory())
287     {
288         p->drawPixmap(d->pixmapRect.right()-24, d->pixmapRect.bottom()-24, QIcon::fromTheme(QLatin1String("svn_switch")).pixmap(22, 22));
289     }
290 */
291 
292     if (!d->nameRect.isNull())
293     {
294         drawName(p, d->nameRect, info.name());
295     }
296 
297     if (!d->titleRect.isNull())
298     {
299         drawTitle(p, d->titleRect, info.title());
300     }
301 
302     if (!d->commentsRect.isNull())
303     {
304         drawComments(p, d->commentsRect, info.comment());
305     }
306 
307     if (!d->dateRect.isNull() && info.dateTime().isValid())
308     {
309         drawCreationDate(p, d->dateRect, info.dateTime());
310     }
311 
312     if (!d->modDateRect.isNull() && info.modDateTime().isValid())
313     {
314         drawModificationDate(p, d->modDateRect, info.modDateTime());
315     }
316 
317     if (!d->resolutionRect.isNull())
318     {
319         drawImageSize(p, d->resolutionRect, info.dimensions());
320     }
321 
322     if (!d->arRect.isNull())
323     {
324         drawAspectRatio(p, d->arRect, info.dimensions());
325     }
326 
327     if (!d->sizeRect.isNull())
328     {
329         drawFileSize(p, d->sizeRect, info.fileSize());
330     }
331 
332     if (!d->groupRect.isNull())
333     {
334         drawGroupIndicator(p, d->groupRect, info.numberOfGroupedImages(),
335                            index.data(ItemFilterModel::GroupIsOpenRole).toBool());
336     }
337 
338     if (!d->tagRect.isNull())
339     {
340         QStringList tagsList = AlbumManager::instance()->tagNames(info.tagIds());
341         tagsList.sort();
342         QString tags         = tagsList.join(QLatin1String(", "));
343         drawTags(p, d->tagRect, tags, isSelected);
344     }
345 
346     if (!d->pickLabelRect.isNull())
347     {
348         drawPickLabelIcon(p, d->pickLabelRect, info.pickLabel());
349     }
350 
351     bool left  = index.data(ItemModel::LTLeftPanelRole).toBool();
352     bool right = index.data(ItemModel::LTRightPanelRole).toBool();
353     drawPanelSideIcon(p, left, right);
354 
355     if (d->drawImageFormat)
356     {
357         QString frm  = info.format();
358 
359         if (frm.contains(QLatin1Char('-')))
360             frm = frm.section(QLatin1Char('-'), -1);   // For RAW format annotated as "RAW-xxx" => "xxx"
361 
362         drawImageFormat(p, actualPixmapRect, frm, d->drawImageFormatTop);
363     }
364 
365     if (info.id() == info.currentReferenceImage())
366     {
367         drawSpecialInfo(p, actualPixmapRect, i18n("Reference Image"));
368     }
369 
370     if (d->drawCoordinates && info.hasCoordinates())
371     {
372         drawGeolocationIndicator(p, d->coordinatesRect);
373     }
374 
375     if (d->drawFocusFrame)
376     {
377         drawFocusRect(p, option, isSelected);
378     }
379 
380     if (d->drawMouseOverFrame)
381     {
382         drawMouseOverRect(p, option);
383     }
384 
385     p->restore();
386 
387     drawOverlays(p, option, index);
388 }
389 
pixmapForDrag(const QStyleOptionViewItem & option,const QList<QModelIndex> & indexes) const390 QPixmap ItemDelegate::pixmapForDrag(const QStyleOptionViewItem& option, const QList<QModelIndex>& indexes) const
391 {
392     QPixmap icon;
393 
394     if (!indexes.isEmpty())
395     {
396         icon = thumbnailPixmap(indexes.first());
397     }
398 
399     return makeDragPixmap(option, indexes, icon);
400 }
401 
acceptsToolTip(const QPoint & pos,const QRect & visualRect,const QModelIndex & index,QRect * toolTipRect) const402 bool ItemDelegate::acceptsToolTip(const QPoint& pos, const QRect& visualRect, const QModelIndex& index,
403                                    QRect* toolTipRect) const
404 {
405     return onActualPixmapRect(pos, visualRect, index, toolTipRect);
406 }
407 
acceptsActivation(const QPoint & pos,const QRect & visualRect,const QModelIndex & index,QRect * activationRect) const408 bool ItemDelegate::acceptsActivation(const QPoint& pos, const QRect& visualRect, const QModelIndex& index,
409                                       QRect* activationRect) const
410 {
411     return onActualPixmapRect(pos, visualRect, index, activationRect);
412 }
413 
onActualPixmapRect(const QPoint & pos,const QRect & visualRect,const QModelIndex & index,QRect * returnRect) const414 bool ItemDelegate::onActualPixmapRect(const QPoint& pos, const QRect& visualRect, const QModelIndex& index,
415                                        QRect* returnRect) const
416 {
417     QRect actualRect = actualPixmapRect(index);
418 
419     if (actualRect.isNull())
420     {
421         return false;
422     }
423 
424     actualRect.translate(visualRect.topLeft());
425 
426     if (returnRect)
427     {
428         *returnRect = actualRect;
429     }
430 
431     return actualRect.contains(pos);
432 }
433 
setDefaultViewOptions(const QStyleOptionViewItem & option)434 void ItemDelegate::setDefaultViewOptions(const QStyleOptionViewItem& option)
435 {
436     Q_D(ItemDelegate);
437 
438     if (d->categoryDrawer)
439     {
440         d->categoryDrawer->setDefaultViewOptions(option);
441     }
442 
443     ItemViewDelegate::setDefaultViewOptions(option);
444 }
445 
invalidatePaintingCache()446 void ItemDelegate::invalidatePaintingCache()
447 {
448     Q_D(ItemDelegate);
449 
450     if (d->categoryDrawer)
451     {
452         d->categoryDrawer->invalidatePaintingCache();
453     }
454 
455     ItemViewDelegate::invalidatePaintingCache();
456 }
457 
updateContentWidth()458 void ItemDelegate::updateContentWidth()
459 {
460     Q_D(ItemDelegate);
461     d->contentWidth = d->thumbSize.size() + 2*d->radius;
462 }
463 
updateSizeRectsAndPixmaps()464 void ItemDelegate::updateSizeRectsAndPixmaps()
465 {
466     Q_D(ItemDelegate);
467 
468     // ---- Reset rects and prepare fonts ----
469 
470     d->clearRects();
471     prepareFonts();
472 
473     // ---- Fixed sizes and metrics ----
474 
475     updateContentWidth();
476     prepareMetrics(d->contentWidth);
477 
478     // ---- Calculate rects ----
479 
480     updateRects();
481 
482     // ---- Cached pixmaps ----
483 
484     prepareBackground();
485 
486     if (!d->ratingRect.isNull())
487     {
488         // Normally we prepare the pixmaps over the background of the rating rect.
489         // If the rating is drawn over the thumbnail, we can only draw over a transparent pixmap.
490         prepareRatingPixmaps(!d->ratingOverThumbnail);
491     }
492 
493     // ---- Drawing related caches ----
494 
495     clearCaches();
496 }
497 
clearCaches()498 void ItemDelegate::clearCaches()
499 {
500     Q_D(ItemDelegate);
501     ItemViewDelegate::clearCaches();
502     d->actualPixmapRectCache.clear();
503 }
504 
clearModelDataCaches()505 void ItemDelegate::clearModelDataCaches()
506 {
507     Q_D(ItemDelegate);
508     d->actualPixmapRectCache.clear();
509 }
510 
modelChanged()511 void ItemDelegate::modelChanged()
512 {
513     Q_D(ItemDelegate);
514     clearModelDataCaches();
515     setModel(d->currentView ? d->currentView->model() : nullptr);
516 }
517 
modelContentsChanged()518 void ItemDelegate::modelContentsChanged()
519 {
520     clearModelDataCaches();
521 }
522 
actualPixmapRect(const QModelIndex & index) const523 QRect ItemDelegate::actualPixmapRect(const QModelIndex& index) const
524 {
525     Q_D(const ItemDelegate);
526     // We do not recompute if not found. Assumption is cache is always properly updated.
527     QRect* rect = d->actualPixmapRectCache.object(index.row());
528 
529     if (rect)
530     {
531         return *rect;
532     }
533     else
534     {
535         return d->pixmapRect;
536     }
537 }
538 
updateActualPixmapRect(const QModelIndex & index,const QRect & rect)539 void ItemDelegate::updateActualPixmapRect(const QModelIndex& index, const QRect& rect)
540 {
541     Q_D(ItemDelegate);
542     QRect* const old = d->actualPixmapRectCache.object(index.row());
543 
544     if (!old || *old != rect)
545     {
546         d->actualPixmapRectCache.insert(index.row(), new QRect(rect));
547     }
548 }
549 
calculatethumbSizeToFit(int ws)550 int ItemDelegate::calculatethumbSizeToFit(int ws)
551 {
552     Q_D(ItemDelegate);
553 
554     int ts     = thumbnailSize().size();
555     int gs     = gridSize().width();
556     int sp     = spacing();
557     ws         = ws - 2*sp;
558 
559     // Thumbnails size loop to check (upper/lower)
560     int ts1, ts2;
561     // New grid size used in loop
562     int ngs;
563 
564     double rs1 = fmod((double)ws, (double)gs);
565 
566     for (ts1 = ts ; ts1 < ThumbnailSize::maxThumbsSize() ; ++ts1)
567     {
568         ngs        = ts1 + 2*(d->margin + d->radius) + sp;
569         double nrs = fmod((double)ws, (double)ngs);
570 
571         if (nrs <= rs1)
572         {
573             rs1 = nrs;
574         }
575         else
576         {
577             break;
578         }
579     }
580 
581     double rs2 = fmod((double)ws, (double)gs);
582 
583     for (ts2 = ts ; ts2 > ThumbnailSize::Small ; --ts2)
584     {
585         ngs        = ts2 + 2*(d->margin + d->radius) + sp;
586         double nrs = fmod((double)ws, (double)ngs);
587 
588         if (nrs >= rs2)
589         {
590             rs2 = nrs;
591         }
592         else
593         {
594             rs2 = nrs;
595             break;
596         }
597     }
598 
599     if (rs1 > rs2)
600     {
601         return (ts2);
602     }
603 
604     return (ts1);
605 }
606 
607 } // namespace Digikam
608