1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2005-08-15
7  * Description : a widget to draw stars rating
8  *
9  * Copyright (C) 2005      by Owen Hirst <n8rider@sbcglobal.net>
10  * Copyright (C) 2006-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 "ratingwidget.h"
26 
27 // C++ includes
28 
29 #include <cmath>
30 
31 // Qt includes
32 
33 #include <QApplication>
34 #include <QPainter>
35 #include <QPalette>
36 #include <QPixmap>
37 #include <QTimeLine>
38 #include <QFont>
39 #include <QAction>
40 #include <QWidgetAction>
41 
42 // KDE includes
43 
44 #include <klocalizedstring.h>
45 #include <kactioncollection.h>
46 
47 // Local includes
48 
49 #include "digikam_debug.h"
50 #include "digikam_globals.h"
51 #include "thememanager.h"
52 #include "dxmlguiwindow.h"
53 #include "dexpanderbox.h"
54 
55 namespace Digikam
56 {
57 
58 class Q_DECL_HIDDEN RatingWidget::Private
59 {
60 public:
61 
Private()62     explicit Private()
63       : tracking        (true),
64         isHovered       (false),
65         fading          (false),
66         rating          (0),
67         fadingValue     (0),
68         duration        (600),
69         offset          (0),
70         fadingTimeLine  (nullptr)
71     {
72     }
73 
74     bool       tracking;
75     bool       isHovered;
76     bool       fading;
77 
78     int        rating;
79     int        fadingValue;
80     int        duration;       ///< in milliseconds
81     int        offset;
82 
83     QTimeLine* fadingTimeLine;
84 
85     QPixmap    selPixmap;      ///< Selected star.
86     QPixmap    regPixmap;      ///< Regular star.
87     QPixmap    disPixmap;      ///< Disable star.
88 };
89 
RatingWidget(QWidget * const parent)90 RatingWidget::RatingWidget(QWidget* const parent)
91     : QWidget(parent),
92       d      (new Private)
93 {
94     slotThemeChanged();
95 
96     connect(ThemeManager::instance(), SIGNAL(signalThemeChanged()),
97             this, SLOT(slotThemeChanged()));
98 }
99 
~RatingWidget()100 RatingWidget::~RatingWidget()
101 {
102     delete d;
103 }
104 
setupTimeLine()105 void RatingWidget::setupTimeLine()
106 {
107     delete d->fadingTimeLine;
108     d->fadingTimeLine = new QTimeLine(d->duration, this);
109     d->fadingTimeLine->setFrameRange(0, 255);
110 
111     connect(d->fadingTimeLine, SIGNAL(frameChanged(int)),
112             this, SLOT(setFadingValue(int)));
113 
114     d->fadingTimeLine->start();
115 }
116 
regPixmapWidth() const117 int RatingWidget::regPixmapWidth() const
118 {
119     return d->regPixmap.width();
120 }
121 
setRating(int val)122 void RatingWidget::setRating(int val)
123 {
124     if (((val < RatingMin) || (val > RatingMax)) && (val != NoRating))
125     {
126         return;
127     }
128 
129     d->rating = val;
130 
131     if (d->tracking)
132     {
133         emit signalRatingChanged(d->rating);
134     }
135 
136     emit signalRatingModified(d->rating);
137     update();
138 }
139 
rating() const140 int RatingWidget::rating() const
141 {
142     return d->rating;
143 }
144 
setTracking(bool tracking)145 void RatingWidget::setTracking(bool tracking)
146 {
147     d->tracking = tracking;
148 }
149 
hasTracking() const150 bool RatingWidget::hasTracking() const
151 {
152     return d->tracking;
153 }
154 
setFading(bool fading)155 void RatingWidget::setFading(bool fading)
156 {
157     d->fading = fading;
158 }
159 
hasFading() const160 bool RatingWidget::hasFading() const
161 {
162     return d->fading;
163 }
164 
setFadingValue(int value)165 void RatingWidget::setFadingValue(int value)
166 {
167     d->fadingValue = value;
168 
169     if ((d->fadingValue >= 255) && d->fadingTimeLine)
170     {
171         d->fadingTimeLine->stop();
172     }
173 
174     update();
175 }
176 
setVisible(bool visible)177 void RatingWidget::setVisible(bool visible)
178 {
179     QWidget::setVisible(visible);
180 
181     if (visible)
182     {
183         startFading();
184     }
185     else
186     {
187         stopFading();
188     }
189 }
190 
maximumVisibleWidth() const191 int RatingWidget::maximumVisibleWidth() const
192 {
193     return RatingMax * (d->disPixmap.width()+1);
194 }
195 
startFading()196 void RatingWidget::startFading()
197 {
198     if (!hasFading())
199     {
200         return;
201     }
202 
203     if (!d->isHovered)
204     {
205         d->isHovered   = true;
206         d->fadingValue = 0;
207         setupTimeLine();
208     }
209 }
210 
stopFading()211 void RatingWidget::stopFading()
212 {
213     if (!hasFading())
214     {
215         return;
216     }
217 
218     if (d->fadingTimeLine)
219     {
220         d->fadingTimeLine->stop();
221     }
222 
223     d->isHovered   = false;
224     d->fadingValue = 0;
225     update();
226 }
227 
setVisibleImmediately()228 void RatingWidget::setVisibleImmediately()
229 {
230     setFadingValue(255);
231 }
232 
starPixmapDisabled() const233 QPixmap RatingWidget::starPixmapDisabled() const
234 {
235     return d->disPixmap;
236 }
237 
starPixmapFilled() const238 QPixmap RatingWidget::starPixmapFilled() const
239 {
240     return d->selPixmap;
241 }
242 
starPixmap() const243 QPixmap RatingWidget::starPixmap() const
244 {
245     return d->regPixmap;
246 }
247 
regeneratePixmaps()248 void RatingWidget::regeneratePixmaps()
249 {
250     slotThemeChanged();
251 }
252 
mousePressEvent(QMouseEvent * e)253 void RatingWidget::mousePressEvent(QMouseEvent* e)
254 {
255     if (e->button() != Qt::LeftButton)
256     {
257         return;
258     }
259 
260     if (hasFading() && (d->fadingValue < 255))
261     {
262         return;
263     }
264 
265     int pos = (e->x() - d->offset) / d->regPixmap.width() +1;
266 
267     if (d->rating == pos)
268     {
269         d->rating--;
270     }
271     else
272     {
273         d->rating = pos;
274     }
275 
276     if (d->rating > RatingMax)
277     {
278         d->rating = RatingMax;
279     }
280 
281     if (d->rating < RatingMin)
282     {
283         d->rating = RatingMin;
284     }
285 
286     if (d->tracking)
287     {
288         emit signalRatingChanged(d->rating);
289     }
290 
291     emit signalRatingModified(d->rating);
292     update();
293 }
294 
mouseMoveEvent(QMouseEvent * e)295 void RatingWidget::mouseMoveEvent(QMouseEvent* e)
296 {
297     if (!(e->buttons() & Qt::LeftButton))
298     {
299         return;
300     }
301 
302     if (hasFading() && (d->fadingValue < 255))
303     {
304         return;
305     }
306 
307     int pos = (e->x() - d->offset) / d->regPixmap.width() +1;
308 
309     if (d->rating != pos)
310     {
311         if (pos > RatingMax)       // NOTE: bug. #151357
312         {
313             pos = RatingMax;
314         }
315 
316         if (pos < RatingMin)
317         {
318             pos = RatingMin;
319         }
320 
321         d->rating = pos;
322 
323         if (d->tracking)
324         {
325             emit signalRatingChanged(d->rating);
326         }
327 
328         emit signalRatingModified(d->rating);
329         update();
330     }
331 }
332 
mouseReleaseEvent(QMouseEvent * e)333 void RatingWidget::mouseReleaseEvent(QMouseEvent* e)
334 {
335     if (e->button() != Qt::LeftButton)
336     {
337         return;
338     }
339 
340     if (hasFading() && (d->fadingValue < 255))
341     {
342         return;
343     }
344 
345     emit signalRatingChanged(d->rating);
346 }
347 
slotThemeChanged()348 void RatingWidget::slotThemeChanged()
349 {
350     d->regPixmap = QPixmap(15, 15);
351     d->regPixmap.fill(Qt::transparent);
352     d->selPixmap = QPixmap(15, 15);
353     d->selPixmap.fill(Qt::transparent);
354     d->disPixmap = QPixmap(15, 15);
355     d->disPixmap.fill(Qt::transparent);
356 
357     QPainter p1(&d->regPixmap);
358     p1.setRenderHint(QPainter::Antialiasing, true);
359     p1.setBrush(palette().color(QPalette::Active, backgroundRole()));
360     p1.setPen(palette().color(QPalette::Active, foregroundRole()));
361     p1.drawPolygon(starPolygon(), Qt::WindingFill);
362     p1.end();
363 
364     QPainter p2(&d->selPixmap);
365     p2.setRenderHint(QPainter::Antialiasing, true);
366     p2.setBrush(qApp->palette().color(QPalette::Link));
367     p2.setPen(palette().color(QPalette::Active, foregroundRole()));
368     p2.drawPolygon(starPolygon(), Qt::WindingFill);
369     p2.end();
370 
371     QPainter p3(&d->disPixmap);
372     p3.setRenderHint(QPainter::Antialiasing, true);
373     p3.setBrush(palette().color(QPalette::Disabled, backgroundRole()));
374     p3.setPen(palette().color(QPalette::Disabled, foregroundRole()));
375     p3.drawPolygon(starPolygon(), Qt::WindingFill);
376     p3.end();
377 
378     setMinimumSize(QSize((d->regPixmap.width()+1)*RatingMax, d->regPixmap.height()));
379     update();
380 }
381 
starPolygon()382 QPolygon RatingWidget::starPolygon()
383 {
384     QPolygon star;
385     star << QPoint(0,  6);
386     star << QPoint(5,  5);
387     star << QPoint(7,  0);
388     star << QPoint(9,  5);
389     star << QPoint(14, 6);
390     star << QPoint(10, 9);
391     star << QPoint(11, 14);
392     star << QPoint(7,  11);
393     star << QPoint(3,  14);
394     star << QPoint(4,  9);
395 
396     return star;
397 }
398 
buildIcon(int rate,int size)399 QIcon RatingWidget::buildIcon(int rate, int size)
400 {
401     QPixmap pix(size, size);
402     pix.fill(Qt::transparent);
403     QPainter p(&pix);
404     QTransform transform;
405     transform.scale(size/15.0, size/15.0);
406     p.setTransform(transform);
407     p.setRenderHint(QPainter::Antialiasing, true);
408     p.setPen(qApp->palette().color(QPalette::Active, QPalette::ButtonText));
409 
410     if (rate > 0)
411     {
412         p.setBrush(qApp->palette().color(QPalette::Link));
413     }
414 
415     p.drawPolygon(starPolygon(), Qt::WindingFill);
416     p.end();
417 
418     return QIcon(pix);
419 }
420 
paintEvent(QPaintEvent *)421 void RatingWidget::paintEvent(QPaintEvent*)
422 {
423     QPainter p(this);
424 
425     d->offset = (width() - RatingMax * (d->disPixmap.width()+1)) / 2;
426 
427     // Widget is disable : drawing grayed frame.
428 
429     if (!isEnabled())
430     {
431         int x = d->offset;
432 
433         for (int i = 0 ; i < RatingMax ; ++i)
434         {
435             p.drawPixmap(x, 0, d->disPixmap);
436             x += d->disPixmap.width()+1;
437         }
438     }
439     else
440     {
441         int x       = d->offset;
442         int rate    = d->rating != NoRating ? d->rating : 0;
443         QPixmap sel = d->selPixmap;
444         applyFading(sel);
445 
446         for (int i = 0 ; i < rate ; ++i)
447         {
448             p.drawPixmap(x, 0, sel);
449             x += sel.width()+1;
450         }
451 
452         QPixmap reg = d->regPixmap;
453         applyFading(reg);
454 
455         for (int i = rate ; i < RatingMax ; ++i)
456         {
457             p.drawPixmap(x, 0, reg);
458             x += reg.width()+1;
459         }
460     }
461 
462     p.end();
463 }
464 
applyFading(QPixmap & pix)465 void RatingWidget::applyFading(QPixmap& pix)
466 {
467     if (hasFading() && d->fadingValue < 255)
468     {
469         QPainter p(&pix);
470         p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
471         p.fillRect(pix.rect(), QColor(0, 0, 0, d->fadingValue));
472         p.end();
473     }
474 }
475 
476 // -------------------------------------------------------------------------------
477 
478 class Q_DECL_HIDDEN RatingBox::Private
479 {
480 
481 public:
482 
Private()483     explicit Private()
484       : shortcut    (nullptr),
485         ratingWidget(nullptr)
486     {
487     }
488 
489     DAdjustableLabel* shortcut;
490 
491     RatingWidget*     ratingWidget;
492 };
493 
RatingBox(QWidget * const parent)494 RatingBox::RatingBox(QWidget* const parent)
495     : DVBox(parent),
496       d    (new Private)
497 {
498     setAttribute(Qt::WA_DeleteOnClose);
499     setFocusPolicy(Qt::NoFocus);
500 
501     d->ratingWidget = new RatingWidget(this);
502     d->ratingWidget->setTracking(false);
503 
504     d->shortcut     = new DAdjustableLabel(this);
505     QFont fnt       = d->shortcut->font();
506     fnt.setItalic(true);
507     d->shortcut->setFont(fnt);
508     d->shortcut->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
509     d->shortcut->setWordWrap(false);
510 
511     setContentsMargins(QMargins());
512     setSpacing(0);
513 
514     // -------------------------------------------------------------
515 
516     connect(d->ratingWidget, SIGNAL(signalRatingModified(int)),
517             this, SLOT(slotUpdateDescription(int)));
518 
519     connect(d->ratingWidget, SIGNAL(signalRatingChanged(int)),
520             this, SIGNAL(signalRatingChanged(int)));
521 }
522 
~RatingBox()523 RatingBox::~RatingBox()
524 {
525     delete d;
526 }
527 
slotUpdateDescription(int rating)528 void RatingBox::slotUpdateDescription(int rating)
529 {
530     DXmlGuiWindow* const app = dynamic_cast<DXmlGuiWindow*>(qApp->activeWindow());
531 
532     if (app)
533     {
534         QAction* const ac = app->actionCollection()->action(QString::fromLatin1("rateshortcut-%1").arg(rating));
535 
536         if (ac)
537         {
538             d->shortcut->setAdjustedText(ac->shortcut().toString());
539         }
540     }
541 }
542 
543 // -------------------------------------------------------------------------------
544 
RatingMenuAction(QMenu * const parent)545 RatingMenuAction::RatingMenuAction(QMenu* const parent)
546     : QMenu(parent)
547 {
548     setTitle(i18n("Rating"));
549     QWidgetAction* const wa = new QWidgetAction(this);
550     RatingBox* const rb     = new RatingBox(parent);
551     wa->setDefaultWidget(rb);
552     addAction(wa);
553 
554     connect(rb, SIGNAL(signalRatingChanged(int)),
555             this, SIGNAL(signalRatingChanged(int)));
556 
557     connect(rb, SIGNAL(signalRatingChanged(int)),
558             parent, SLOT(close()));
559 }
560 
~RatingMenuAction()561 RatingMenuAction::~RatingMenuAction()
562 {
563 }
564 
565 } // namespace Digikam
566