1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2007-04-11
7  * Description : light table thumbs bar
8  *
9  * Copyright (C) 2007-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
10  *
11  * This program is free software; you can redistribute it
12  * and/or modify it under the terms of the GNU General
13  * Public License as published by the Free Software Foundation;
14  * either version 2, or (at your option)
15  * any later version.
16  *
17  * This program is distributed in the hope that it will be useful,
18  * but WITHOUT ANY WARRANTY; without even the implied warranty of
19  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20  * GNU General Public License for more details.
21  *
22  * ============================================================ */
23 
24 #include "lighttablethumbbar.h"
25 
26 // Qt includes
27 
28 #include <QAction>
29 #include <QList>
30 #include <QPixmap>
31 #include <QPainter>
32 #include <QContextMenuEvent>
33 #include <QApplication>
34 #include <QMenu>
35 #include <QIcon>
36 
37 // KDE includes
38 
39 #include <klocalizedstring.h>
40 
41 // Local includes
42 
43 #include "digikam_debug.h"
44 #include "coredb.h"
45 #include "applicationsettings.h"
46 #include "contextmenuhelper.h"
47 #include "itemfiltermodel.h"
48 #include "itemdragdrop.h"
49 #include "fileactionmngr.h"
50 #include "thumbnailloadthread.h"
51 
52 namespace Digikam
53 {
54 
55 template <typename T, class Container>
removeAnyInInterval(Container & list,const T & begin,const T & end)56 void removeAnyInInterval(Container& list, const T& begin, const T& end)
57 {
58     typename Container::iterator it;
59 
60     for (it = list.begin() ; it != list.end() ; )
61     {
62         if (((*it) >= begin) && ((*it) <= end))
63         {
64             it = list.erase(it);
65         }
66         else
67         {
68             ++it;
69         }
70     }
71 }
72 
73 class Q_DECL_HIDDEN LightTableItemListModel : public ItemListModel
74 {
75     Q_OBJECT
76 
77 public:
78 
LightTableItemListModel(QObject * const parent=nullptr)79     explicit LightTableItemListModel(QObject* const parent = nullptr)
80         : ItemListModel(parent),
81           m_exclusive  (false)
82     {
83     }
84 
clearLightTableState()85     void clearLightTableState()
86     {
87         m_leftIndexes.clear();
88         m_rightIndexes.clear();
89     }
90 
setExclusiveLightTableState(bool exclusive)91     void setExclusiveLightTableState(bool exclusive)
92     {
93         m_exclusive = exclusive;
94     }
95 
data(const QModelIndex & index,int role=Qt::DisplayRole) const96     QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override
97     {
98         if      (role == LTLeftPanelRole)
99         {
100             return m_leftIndexes.contains(index.row());
101         }
102         else if (role == LTRightPanelRole)
103         {
104             return m_rightIndexes.contains(index.row());
105         }
106 
107         return ItemListModel::data(index, role);
108     }
109 
setData(const QModelIndex & index,const QVariant & value,int role=Qt::DisplayRole)110     bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::DisplayRole) override
111     {
112         if (!index.isValid())
113         {
114             return false;
115         }
116 
117         if      (role == LTLeftPanelRole)
118         {
119             if (m_exclusive)
120             {
121                 m_leftIndexes.clear();
122             }
123 
124             m_leftIndexes << index.row();
125 
126             return true;
127         }
128         else if (role == LTRightPanelRole)
129         {
130             if (m_exclusive)
131             {
132                 m_rightIndexes.clear();
133             }
134 
135             m_rightIndexes << index.row();
136 
137             return true;
138         }
139 
140         return ItemListModel::setData(index, value, role);
141     }
142 
imageInfosAboutToBeRemoved(int begin,int end)143     void imageInfosAboutToBeRemoved(int begin, int end) override
144     {
145         removeAnyInInterval(m_leftIndexes, begin, end);
146         removeAnyInInterval(m_rightIndexes, begin, end);
147     }
148 
imageInfosCleared()149     void imageInfosCleared() override
150     {
151         clearLightTableState();
152     }
153 
154 protected:
155 
156     QSet<int> m_leftIndexes;
157     QSet<int> m_rightIndexes;
158     bool      m_exclusive;
159 };
160 
161 class Q_DECL_HIDDEN LightTableThumbBar::Private
162 {
163 
164 public:
165 
Private()166     explicit Private()
167       : navigateByPair  (false),
168         imageInfoModel  (nullptr),
169         imageFilterModel(nullptr),
170         dragDropHandler (nullptr)
171     {
172     }
173 
174     bool                     navigateByPair;
175 
176     LightTableItemListModel* imageInfoModel;
177     ItemFilterModel*         imageFilterModel;
178     ItemDragDropHandler*     dragDropHandler;
179 };
180 
LightTableThumbBar(QWidget * const parent)181 LightTableThumbBar::LightTableThumbBar(QWidget* const parent)
182     : ItemThumbnailBar(parent),
183       d               (new Private)
184 {
185     d->imageInfoModel   = new LightTableItemListModel(this);
186 
187     // only one is left, only one is right at a time
188 
189     d->imageInfoModel->setExclusiveLightTableState(true);
190 
191     d->imageFilterModel = new ItemFilterModel(this);
192     d->imageFilterModel->setSourceItemModel(d->imageInfoModel);
193 
194     d->imageInfoModel->setWatchFlags(d->imageFilterModel->suggestedWatchFlags());
195     d->imageInfoModel->setThumbnailLoadThread(ThumbnailLoadThread::defaultIconViewThread());
196 
197     d->imageFilterModel->setCategorizationMode(ItemSortSettings::NoCategories);
198     d->imageFilterModel->setStringTypeNatural(ApplicationSettings::instance()->isStringTypeNatural());
199     d->imageFilterModel->setSortRole((ItemSortSettings::SortRole)ApplicationSettings::instance()->getImageSortOrder());
200     d->imageFilterModel->setSortOrder((ItemSortSettings::SortOrder)ApplicationSettings::instance()->getImageSorting());
201     d->imageFilterModel->setAllGroupsOpen(true); // disable filtering out by group, see bug #308948
202     d->imageFilterModel->sort(0);                // an initial sorting is necessary
203 
204     d->dragDropHandler = new ItemDragDropHandler(d->imageInfoModel);
205     d->dragDropHandler->setReadOnlyDrop(true);
206     d->imageInfoModel->setDragDropHandler(d->dragDropHandler);
207 
208     setModels(d->imageInfoModel, d->imageFilterModel);
209     setSelectionMode(QAbstractItemView::SingleSelection);
210 
211     connect(d->dragDropHandler, SIGNAL(itemInfosDropped(QList<ItemInfo>)),
212             this, SIGNAL(signalDroppedItems(QList<ItemInfo>)));
213 
214     connect(d->imageInfoModel, SIGNAL(imageInfosAdded(QList<ItemInfo>)),
215             this, SIGNAL(signalContentChanged()));
216 
217     connect(d->imageInfoModel, SIGNAL(rowsRemoved(QModelIndex,int,int)),
218             this, SIGNAL(signalContentChanged()));
219 
220     connect(ApplicationSettings::instance(), SIGNAL(setupChanged()),
221             this, SLOT(slotSetupChanged()));
222 }
223 
~LightTableThumbBar()224 LightTableThumbBar::~LightTableThumbBar()
225 {
226     delete d;
227 }
228 
setItems(const ItemInfoList & list)229 void LightTableThumbBar::setItems(const ItemInfoList& list)
230 {
231     foreach (const ItemInfo& info, list)
232     {
233         if (!d->imageInfoModel->hasImage(info))
234         {
235             d->imageInfoModel->addItemInfo(info);
236         }
237     }
238 }
239 
slotDockLocationChanged(Qt::DockWidgetArea area)240 void LightTableThumbBar::slotDockLocationChanged(Qt::DockWidgetArea area)
241 {
242     if ((area == Qt::LeftDockWidgetArea) || (area == Qt::RightDockWidgetArea))
243     {
244         setFlow(TopToBottom);
245     }
246     else
247     {
248         setFlow(LeftToRight);
249     }
250 
251     scrollTo(currentIndex());
252 }
253 
clear()254 void LightTableThumbBar::clear()
255 {
256     d->imageInfoModel->clearItemInfos();
257     emit signalContentChanged();
258 }
259 
setNavigateByPair(bool b)260 void LightTableThumbBar::setNavigateByPair(bool b)
261 {
262     d->navigateByPair = b;
263 }
264 
showContextMenuOnInfo(QContextMenuEvent * e,const ItemInfo & info)265 void LightTableThumbBar::showContextMenuOnInfo(QContextMenuEvent* e, const ItemInfo& info)
266 {
267     // temporary actions ----------------------------------
268 
269     QAction* const leftPanelAction  = new QAction(QIcon::fromTheme(QLatin1String("go-previous")),   i18n("Show on left panel"),  this);
270     QAction* const rightPanelAction = new QAction(QIcon::fromTheme(QLatin1String("go-next")),       i18n("Show on right panel"), this);
271     QAction* const editAction       = new QAction(QIcon::fromTheme(QLatin1String("document-edit")), i18n("Edit"),                this);
272     QAction* const removeAction     = new QAction(QIcon::fromTheme(QLatin1String("window-close")),  i18n("Remove item"),         this);
273     QAction* const clearAllAction   = new QAction(QIcon::fromTheme(QLatin1String("edit-delete")),   i18n("Clear all"),           this);
274 
275     leftPanelAction->setEnabled(d->navigateByPair  ? false : true);
276     rightPanelAction->setEnabled(d->navigateByPair ? false : true);
277     clearAllAction->setEnabled(countItems()        ? true  : false);
278 
279     // ----------------------------------------------------
280 
281     QMenu popmenu(this);
282     ContextMenuHelper cmhelper(&popmenu);
283     cmhelper.addAction(leftPanelAction, true);
284     cmhelper.addAction(rightPanelAction, true);
285     cmhelper.addSeparator();
286     cmhelper.addAction(editAction);
287     cmhelper.addServicesMenu(QList<QUrl>() << info.fileUrl());
288     cmhelper.addSeparator();
289     cmhelper.addLabelsAction();
290     cmhelper.addSeparator();
291     cmhelper.addAction(removeAction);
292     cmhelper.addAction(clearAllAction, true);
293 
294     // special action handling --------------------------------
295 
296     connect(&cmhelper, SIGNAL(signalAssignPickLabel(int)),
297             this, SLOT(slotAssignPickLabel(int)));
298 
299     connect(&cmhelper, SIGNAL(signalAssignColorLabel(int)),
300             this, SLOT(slotAssignColorLabel(int)));
301 
302     connect(&cmhelper, SIGNAL(signalAssignRating(int)),
303             this, SLOT(slotAssignRating(int)));
304 
305     QAction* const choice = cmhelper.exec(e->globalPos());
306 
307     if (choice)
308     {
309         if      (choice == leftPanelAction)
310         {
311             emit signalSetItemOnLeftPanel(info);
312         }
313         else if (choice == rightPanelAction)
314         {
315             emit signalSetItemOnRightPanel(info);
316         }
317         else if (choice == editAction)
318         {
319             emit signalEditItem(info);
320         }
321         else if (choice == removeAction)
322         {
323             emit signalRemoveItem(info);
324         }
325         else if (choice == clearAllAction)
326         {
327             emit signalClearAll();
328         }
329     }
330 }
331 
slotColorLabelChanged(const QUrl & url,int color)332 void LightTableThumbBar::slotColorLabelChanged(const QUrl& url, int color)
333 {
334     assignColorLabel(ItemInfo::fromUrl(url), color);
335 }
336 
slotPickLabelChanged(const QUrl & url,int pick)337 void LightTableThumbBar::slotPickLabelChanged(const QUrl& url, int pick)
338 {
339     assignPickLabel(ItemInfo::fromUrl(url), pick);
340 }
341 
slotAssignPickLabel(int pickId)342 void LightTableThumbBar::slotAssignPickLabel(int pickId)
343 {
344     assignPickLabel(currentInfo(), pickId);
345 }
346 
slotAssignColorLabel(int colorId)347 void LightTableThumbBar::slotAssignColorLabel(int colorId)
348 {
349     assignColorLabel(currentInfo(), colorId);
350 }
351 
slotRatingChanged(const QUrl & url,int rating)352 void LightTableThumbBar::slotRatingChanged(const QUrl& url, int rating)
353 {
354     assignRating(ItemInfo::fromUrl(url), rating);
355 }
356 
slotAssignRating(int rating)357 void LightTableThumbBar::slotAssignRating(int rating)
358 {
359     assignRating(currentInfo(), rating);
360 }
361 
assignPickLabel(const ItemInfo & info,int pickId)362 void LightTableThumbBar::assignPickLabel(const ItemInfo& info, int pickId)
363 {
364     FileActionMngr::instance()->assignPickLabel(info, pickId);
365 }
366 
assignRating(const ItemInfo & info,int rating)367 void LightTableThumbBar::assignRating(const ItemInfo& info, int rating)
368 {
369     rating = qMin(RatingMax, qMax(RatingMin, rating));
370     FileActionMngr::instance()->assignRating(info, rating);
371 }
372 
assignColorLabel(const ItemInfo & info,int colorId)373 void LightTableThumbBar::assignColorLabel(const ItemInfo& info, int colorId)
374 {
375     FileActionMngr::instance()->assignColorLabel(info, colorId);
376 }
377 
slotToggleTag(const QUrl & url,int tagID)378 void LightTableThumbBar::slotToggleTag(const QUrl& url, int tagID)
379 {
380     toggleTag(ItemInfo::fromUrl(url), tagID);
381 }
382 
toggleTag(int tagID)383 void LightTableThumbBar::toggleTag(int tagID)
384 {
385     toggleTag(currentInfo(), tagID);
386 }
387 
toggleTag(const ItemInfo & info,int tagID)388 void LightTableThumbBar::toggleTag(const ItemInfo& info, int tagID)
389 {
390     if (!info.isNull())
391     {
392         if (!info.tagIds().contains(tagID))
393         {
394             FileActionMngr::instance()->assignTag(info, tagID);
395         }
396         else
397         {
398             FileActionMngr::instance()->removeTag(info, tagID);
399         }
400     }
401 }
402 
setOnLeftPanel(const ItemInfo & info)403 void LightTableThumbBar::setOnLeftPanel(const ItemInfo& info)
404 {
405     QModelIndex index = d->imageInfoModel->indexForItemInfo(info);
406 
407     // model has exclusiveLightTableState, so any previous index will be reset
408 
409     d->imageInfoModel->setData(index, true, ItemModel::LTLeftPanelRole);
410     viewport()->update();
411 }
412 
setOnRightPanel(const ItemInfo & info)413 void LightTableThumbBar::setOnRightPanel(const ItemInfo& info)
414 {
415     QModelIndex index = d->imageInfoModel->indexForItemInfo(info);
416 
417     // model has exclusiveLightTableState, so any previous index will be reset
418 
419     d->imageInfoModel->setData(index, true, ItemModel::LTRightPanelRole);
420     viewport()->update();
421 }
422 
isOnLeftPanel(const ItemInfo & info) const423 bool LightTableThumbBar::isOnLeftPanel(const ItemInfo& info) const
424 {
425     return d->imageInfoModel->indexForItemInfo(info).data(ItemModel::LTLeftPanelRole).toBool();
426 }
427 
isOnRightPanel(const ItemInfo & info) const428 bool LightTableThumbBar::isOnRightPanel(const ItemInfo& info) const
429 {
430     return d->imageInfoModel->indexForItemInfo(info).data(ItemModel::LTRightPanelRole).toBool();
431 }
432 
findItemByInfo(const ItemInfo & info) const433 QModelIndex LightTableThumbBar::findItemByInfo(const ItemInfo& info) const
434 {
435     if (!info.isNull())
436     {
437         return d->imageInfoModel->indexForItemInfo(info);
438     }
439 
440     return QModelIndex();
441 }
442 
findItemByIndex(const QModelIndex & index) const443 ItemInfo LightTableThumbBar::findItemByIndex(const QModelIndex& index) const
444 {
445     if (index.isValid())
446     {
447         return d->imageInfoModel->imageInfo(index);
448     }
449 
450     return ItemInfo();
451 }
452 
removeItemByInfo(const ItemInfo & info)453 void LightTableThumbBar::removeItemByInfo(const ItemInfo& info)
454 {
455     if (info.isNull())
456     {
457         return;
458     }
459 
460     d->imageInfoModel->removeItemInfo(info);
461 }
462 
countItems() const463 int LightTableThumbBar::countItems() const
464 {
465     return d->imageInfoModel->rowCount();
466 }
467 
paintEvent(QPaintEvent * e)468 void LightTableThumbBar::paintEvent(QPaintEvent* e)
469 {
470     if (!countItems())
471     {
472         QPainter p(viewport());
473         p.setPen(QPen(qApp->palette().color(QPalette::Text)));
474         p.drawText(0, 0, width(), height(),
475                    Qt::AlignCenter | Qt::TextWordWrap,
476                    i18n("Drag and drop images here"));
477         p.end();
478 
479         return;
480     }
481 
482     ItemThumbnailBar::paintEvent(e);
483 }
484 
slotSetupChanged()485 void LightTableThumbBar::slotSetupChanged()
486 {
487     d->imageFilterModel->setStringTypeNatural(ApplicationSettings::instance()->isStringTypeNatural());
488 }
489 
490 } // namespace Digikam
491 
492 #include "lighttablethumbbar.moc"
493