File indexing completed on 2025-12-07 04:08:35

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2005-08-15
0007  * Description : a widget to draw stars rating
0008  *
0009  * SPDX-FileCopyrightText: 2005      by Owen Hirst <n8rider@sbcglobal.net>
0010  * SPDX-FileCopyrightText: 2006-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0011  *
0012  * SPDX-License-Identifier: GPL-2.0-or-later
0013  *
0014  * ============================================================ */
0015 
0016 #include "ratingwidget.h"
0017 
0018 // C++ includes
0019 
0020 #include <cmath>
0021 
0022 // Qt includes
0023 
0024 #include <QApplication>
0025 #include <QPainter>
0026 #include <QPalette>
0027 #include <QPixmap>
0028 #include <QTimeLine>
0029 #include <QFont>
0030 #include <QAction>
0031 #include <QWidgetAction>
0032 
0033 // KDE includes
0034 
0035 #if !defined(Q_OS_DARWIN) && defined(Q_CC_GNU)
0036 #   pragma GCC diagnostic push
0037 #   pragma GCC diagnostic ignored "-Wdeprecated-declarations"
0038 #endif
0039 
0040 #if defined(Q_CC_CLANG)
0041 #   pragma clang diagnostic push
0042 #   pragma clang diagnostic ignored "-Wdeprecated-declarations"
0043 #endif
0044 
0045 #include <klocalizedstring.h>
0046 #include <kactioncollection.h>
0047 
0048 // Restore warnings
0049 #if !defined(Q_OS_DARWIN) && defined(Q_CC_GNU)
0050 #   pragma GCC diagnostic pop
0051 #endif
0052 
0053 #if defined(Q_CC_CLANG)
0054 #   pragma clang diagnostic pop
0055 #endif
0056 
0057 // Local includes
0058 
0059 #include "digikam_debug.h"
0060 #include "digikam_globals.h"
0061 #include "thememanager.h"
0062 #include "dxmlguiwindow.h"
0063 #include "dexpanderbox.h"
0064 
0065 namespace Digikam
0066 {
0067 
0068 class Q_DECL_HIDDEN RatingWidget::Private
0069 {
0070 public:
0071 
0072     explicit Private()
0073       : tracking        (true),
0074         isHovered       (false),
0075         fading          (false),
0076         rating          (0),
0077         fadingValue     (0),
0078         duration        (600),
0079         offset          (0),
0080         width           (15),
0081         fadingTimeLine  (nullptr)
0082     {
0083     }
0084 
0085     bool       tracking;
0086     bool       isHovered;
0087     bool       fading;
0088 
0089     int        rating;
0090     int        fadingValue;
0091     int        duration;       ///< in milliseconds
0092     int        offset;
0093     int        width;
0094 
0095     QTimeLine* fadingTimeLine;
0096 
0097     QPixmap    selPixmap;      ///< Selected star.
0098     QPixmap    regPixmap;      ///< Regular star.
0099     QPixmap    disPixmap;      ///< Disable star.
0100 };
0101 
0102 RatingWidget::RatingWidget(QWidget* const parent)
0103     : QWidget(parent),
0104       d      (new Private)
0105 {
0106     slotThemeChanged();
0107 
0108     connect(ThemeManager::instance(), SIGNAL(signalThemeChanged()),
0109             this, SLOT(slotThemeChanged()));
0110 }
0111 
0112 RatingWidget::~RatingWidget()
0113 {
0114     delete d;
0115 }
0116 
0117 void RatingWidget::setupTimeLine()
0118 {
0119     delete d->fadingTimeLine;
0120     d->fadingTimeLine = new QTimeLine(d->duration, this);
0121     d->fadingTimeLine->setFrameRange(0, 255);
0122 
0123     connect(d->fadingTimeLine, SIGNAL(frameChanged(int)),
0124             this, SLOT(setFadingValue(int)));
0125 
0126     d->fadingTimeLine->start();
0127 }
0128 
0129 int RatingWidget::regPixmapWidth() const
0130 {
0131     return d->width;
0132 }
0133 
0134 void RatingWidget::setRating(int val)
0135 {
0136     if (((val < RatingMin) || (val > RatingMax)) && (val != NoRating))
0137     {
0138         return;
0139     }
0140 
0141     d->rating = val;
0142 
0143     if (d->tracking)
0144     {
0145         Q_EMIT signalRatingChanged(d->rating);
0146     }
0147 
0148     Q_EMIT signalRatingModified(d->rating);
0149     update();
0150 }
0151 
0152 int RatingWidget::rating() const
0153 {
0154     return d->rating;
0155 }
0156 
0157 void RatingWidget::setTracking(bool tracking)
0158 {
0159     d->tracking = tracking;
0160 }
0161 
0162 bool RatingWidget::hasTracking() const
0163 {
0164     return d->tracking;
0165 }
0166 
0167 void RatingWidget::setFading(bool fading)
0168 {
0169     d->fading = fading;
0170 }
0171 
0172 bool RatingWidget::hasFading() const
0173 {
0174     return d->fading;
0175 }
0176 
0177 void RatingWidget::setFadingValue(int value)
0178 {
0179     d->fadingValue = value;
0180 
0181     if ((d->fadingValue >= 255) && d->fadingTimeLine)
0182     {
0183         d->fadingTimeLine->stop();
0184     }
0185 
0186     update();
0187 }
0188 
0189 void RatingWidget::setVisible(bool visible)
0190 {
0191     QWidget::setVisible(visible);
0192 
0193     if (visible)
0194     {
0195         startFading();
0196     }
0197     else
0198     {
0199         stopFading();
0200     }
0201 }
0202 
0203 int RatingWidget::maximumVisibleWidth() const
0204 {
0205     return RatingMax * (d->width + 1);
0206 }
0207 
0208 void RatingWidget::startFading()
0209 {
0210     if (!hasFading())
0211     {
0212         return;
0213     }
0214 
0215     if (!d->isHovered)
0216     {
0217         d->isHovered   = true;
0218         d->fadingValue = 0;
0219         setupTimeLine();
0220     }
0221 }
0222 
0223 void RatingWidget::stopFading()
0224 {
0225     if (!hasFading())
0226     {
0227         return;
0228     }
0229 
0230     if (d->fadingTimeLine)
0231     {
0232         d->fadingTimeLine->stop();
0233     }
0234 
0235     d->isHovered   = false;
0236     d->fadingValue = 0;
0237     update();
0238 }
0239 
0240 void RatingWidget::setVisibleImmediately()
0241 {
0242     setFadingValue(255);
0243 }
0244 
0245 QPixmap RatingWidget::starPixmapDisabled() const
0246 {
0247     return d->disPixmap;
0248 }
0249 
0250 QPixmap RatingWidget::starPixmapFilled() const
0251 {
0252     return d->selPixmap;
0253 }
0254 
0255 QPixmap RatingWidget::starPixmap() const
0256 {
0257     return d->regPixmap;
0258 }
0259 
0260 void RatingWidget::regeneratePixmaps()
0261 {
0262     slotThemeChanged();
0263 }
0264 
0265 void RatingWidget::mousePressEvent(QMouseEvent* e)
0266 {
0267     if (e->button() != Qt::LeftButton)
0268     {
0269         return;
0270     }
0271 
0272     if (hasFading() && (d->fadingValue < 255))
0273     {
0274         return;
0275     }
0276 
0277 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
0278 
0279     int pos = (e->position().toPoint().x() - d->offset) / d->width + 1;
0280 
0281 #else
0282 
0283     int pos = (e->x() - d->offset) / d->width + 1;
0284 
0285 #endif
0286 
0287     if (d->rating == pos)
0288     {
0289         d->rating--;
0290     }
0291     else
0292     {
0293         d->rating = pos;
0294     }
0295 
0296     if (d->rating > RatingMax)
0297     {
0298         d->rating = RatingMax;
0299     }
0300 
0301     if (d->rating < RatingMin)
0302     {
0303         d->rating = RatingMin;
0304     }
0305 
0306     if (d->tracking)
0307     {
0308         Q_EMIT signalRatingChanged(d->rating);
0309     }
0310 
0311     Q_EMIT signalRatingModified(d->rating);
0312     update();
0313 }
0314 
0315 void RatingWidget::mouseMoveEvent(QMouseEvent* e)
0316 {
0317     if (!(e->buttons() & Qt::LeftButton))
0318     {
0319         return;
0320     }
0321 
0322     if (hasFading() && (d->fadingValue < 255))
0323     {
0324         return;
0325     }
0326 
0327 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
0328 
0329     int pos = (e->position().toPoint().x() - d->offset) / d->width + 1;
0330 
0331 #else
0332 
0333     int pos = (e->x() - d->offset) / d->width + 1;
0334 
0335 #endif
0336 
0337     if (d->rating != pos)
0338     {
0339         if (pos > RatingMax)       // NOTE: bug. #151357
0340         {
0341             pos = RatingMax;
0342         }
0343 
0344         if (pos < RatingMin)
0345         {
0346             pos = RatingMin;
0347         }
0348 
0349         d->rating = pos;
0350 
0351         if (d->tracking)
0352         {
0353             Q_EMIT signalRatingChanged(d->rating);
0354         }
0355 
0356         Q_EMIT signalRatingModified(d->rating);
0357         update();
0358     }
0359 }
0360 
0361 void RatingWidget::mouseReleaseEvent(QMouseEvent* e)
0362 {
0363     if (e->button() != Qt::LeftButton)
0364     {
0365         return;
0366     }
0367 
0368     if (hasFading() && (d->fadingValue < 255))
0369     {
0370         return;
0371     }
0372 
0373     Q_EMIT signalRatingChanged(d->rating);
0374 }
0375 
0376 void RatingWidget::slotThemeChanged()
0377 {
0378     qreal dpr    = devicePixelRatio();
0379 
0380     d->regPixmap = QPixmap(d->width * dpr, d->width * dpr);
0381     d->regPixmap.fill(Qt::transparent);
0382     d->selPixmap = QPixmap(d->width * dpr, d->width * dpr);
0383     d->selPixmap.fill(Qt::transparent);
0384     d->disPixmap = QPixmap(d->width * dpr, d->width * dpr);
0385     d->disPixmap.fill(Qt::transparent);
0386 
0387     d->regPixmap.setDevicePixelRatio(dpr);
0388     d->selPixmap.setDevicePixelRatio(dpr);
0389     d->disPixmap.setDevicePixelRatio(dpr);
0390 
0391     QPainter p1(&d->regPixmap);
0392     p1.setRenderHint(QPainter::Antialiasing, true);
0393     p1.setBrush(palette().color(QPalette::Active, backgroundRole()));
0394     p1.setPen(palette().color(QPalette::Active, foregroundRole()));
0395     p1.drawPolygon(starPolygon(), Qt::WindingFill);
0396     p1.end();
0397 
0398     QPainter p2(&d->selPixmap);
0399     p2.setRenderHint(QPainter::Antialiasing, true);
0400     p2.setBrush(qApp->palette().color(QPalette::Link));
0401     p2.setPen(palette().color(QPalette::Active, foregroundRole()));
0402     p2.drawPolygon(starPolygon(), Qt::WindingFill);
0403     p2.end();
0404 
0405     QPainter p3(&d->disPixmap);
0406     p3.setRenderHint(QPainter::Antialiasing, true);
0407     p3.setBrush(palette().color(QPalette::Disabled, backgroundRole()));
0408     p3.setPen(palette().color(QPalette::Disabled, foregroundRole()));
0409     p3.drawPolygon(starPolygon(), Qt::WindingFill);
0410     p3.end();
0411 
0412     setMinimumSize(QSize((d->width + 1) * RatingMax, d->width));
0413     update();
0414 }
0415 
0416 QPolygon RatingWidget::starPolygon()
0417 {
0418     QPolygon star;
0419     star << QPoint(0,  6);
0420     star << QPoint(5,  5);
0421     star << QPoint(7,  0);
0422     star << QPoint(9,  5);
0423     star << QPoint(14, 6);
0424     star << QPoint(10, 9);
0425     star << QPoint(11, 14);
0426     star << QPoint(7,  11);
0427     star << QPoint(3,  14);
0428     star << QPoint(4,  9);
0429 
0430     return star;
0431 }
0432 
0433 QIcon RatingWidget::buildIcon(int rate, int size)
0434 {
0435     QPixmap pix(size, size);
0436     pix.fill(Qt::transparent);
0437     QPainter p(&pix);
0438     QTransform transform;
0439     transform.scale(size/15.0, size/15.0);
0440     p.setTransform(transform);
0441     p.setRenderHint(QPainter::Antialiasing, true);
0442     p.setPen(qApp->palette().color(QPalette::Active, QPalette::ButtonText));
0443 
0444     if (rate > 0)
0445     {
0446         p.setBrush(qApp->palette().color(QPalette::Link));
0447     }
0448 
0449     p.drawPolygon(starPolygon(), Qt::WindingFill);
0450     p.end();
0451 
0452     return QIcon(pix);
0453 }
0454 
0455 void RatingWidget::paintEvent(QPaintEvent*)
0456 {
0457     QPainter p(this);
0458 
0459     d->offset = (width() - RatingMax * (d->width + 1)) / 2;
0460 
0461     // Widget is disable : drawing grayed frame.
0462 
0463     if (!isEnabled())
0464     {
0465         int x = d->offset;
0466 
0467         for (int i = 0 ; i < RatingMax ; ++i)
0468         {
0469             p.drawPixmap(x, 0, d->disPixmap);
0470             x += d->width + 1;
0471         }
0472     }
0473     else
0474     {
0475         int x       = d->offset;
0476         int rate    = d->rating != NoRating ? d->rating : 0;
0477         QPixmap sel = d->selPixmap;
0478         applyFading(sel);
0479 
0480         for (int i = 0 ; i < rate ; ++i)
0481         {
0482             p.drawPixmap(x, 0, sel);
0483             x += d->width + 1;
0484         }
0485 
0486         QPixmap reg = d->regPixmap;
0487         applyFading(reg);
0488 
0489         for (int i = rate ; i < RatingMax ; ++i)
0490         {
0491             p.drawPixmap(x, 0, reg);
0492             x += d->width + 1;
0493         }
0494     }
0495 
0496     p.end();
0497 }
0498 
0499 void RatingWidget::applyFading(QPixmap& pix)
0500 {
0501     if (hasFading() && d->fadingValue < 255)
0502     {
0503         QPainter p(&pix);
0504         p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
0505         p.fillRect(pix.rect(), QColor(0, 0, 0, d->fadingValue));
0506         p.end();
0507     }
0508 }
0509 
0510 // -------------------------------------------------------------------------------
0511 
0512 class Q_DECL_HIDDEN RatingBox::Private
0513 {
0514 
0515 public:
0516 
0517     explicit Private()
0518       : shortcut    (nullptr),
0519         ratingWidget(nullptr)
0520     {
0521     }
0522 
0523     DAdjustableLabel* shortcut;
0524 
0525     RatingWidget*     ratingWidget;
0526 };
0527 
0528 RatingBox::RatingBox(QWidget* const parent)
0529     : DVBox(parent),
0530       d    (new Private)
0531 {
0532     setAttribute(Qt::WA_DeleteOnClose);
0533     setFocusPolicy(Qt::NoFocus);
0534 
0535     d->ratingWidget = new RatingWidget(this);
0536     d->ratingWidget->setTracking(false);
0537 
0538     d->shortcut     = new DAdjustableLabel(this);
0539     QFont fnt       = d->shortcut->font();
0540     fnt.setItalic(true);
0541     d->shortcut->setFont(fnt);
0542     d->shortcut->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
0543     d->shortcut->setWordWrap(false);
0544 
0545     setContentsMargins(QMargins());
0546     setSpacing(0);
0547 
0548     // -------------------------------------------------------------
0549 
0550     connect(d->ratingWidget, SIGNAL(signalRatingModified(int)),
0551             this, SLOT(slotUpdateDescription(int)));
0552 
0553     connect(d->ratingWidget, SIGNAL(signalRatingChanged(int)),
0554             this, SIGNAL(signalRatingChanged(int)));
0555 }
0556 
0557 RatingBox::~RatingBox()
0558 {
0559     delete d;
0560 }
0561 
0562 void RatingBox::slotUpdateDescription(int rating)
0563 {
0564     DXmlGuiWindow* const app = dynamic_cast<DXmlGuiWindow*>(qApp->activeWindow());
0565 
0566     if (app)
0567     {
0568         QAction* const ac = app->actionCollection()->action(QString::fromLatin1("rateshortcut-%1").arg(rating));
0569 
0570         if (ac)
0571         {
0572             d->shortcut->setAdjustedText(ac->shortcut().toString());
0573         }
0574     }
0575 }
0576 
0577 // -------------------------------------------------------------------------------
0578 
0579 RatingMenuAction::RatingMenuAction(QMenu* const parent)
0580     : QMenu(parent)
0581 {
0582     setTitle(i18n("Rating"));
0583     QWidgetAction* const wa = new QWidgetAction(this);
0584     RatingBox* const rb     = new RatingBox(parent);
0585     wa->setDefaultWidget(rb);
0586     addAction(wa);
0587 
0588     connect(rb, SIGNAL(signalRatingChanged(int)),
0589             this, SIGNAL(signalRatingChanged(int)));
0590 
0591     connect(rb, SIGNAL(signalRatingChanged(int)),
0592             parent, SLOT(close()));
0593 }
0594 
0595 RatingMenuAction::~RatingMenuAction()
0596 {
0597 }
0598 
0599 } // namespace Digikam
0600 
0601 #include "moc_ratingwidget.cpp"