File indexing completed on 2024-11-24 04:34:22

0001 /***************************************************************************
0002  *   SPDX-License-Identifier: GPL-2.0-or-later
0003  *                                                                         *
0004  *   SPDX-FileCopyrightText: 2004-2023 Thomas Fischer <fischer@unix-ag.uni-kl.de>
0005  *                                                                         *
0006  *   This program is free software; you can redistribute it and/or modify  *
0007  *   it under the terms of the GNU General Public License as published by  *
0008  *   the Free Software Foundation; either version 2 of the License, or     *
0009  *   (at your option) any later version.                                   *
0010  *                                                                         *
0011  *   This program is distributed in the hope that it will be useful,       *
0012  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0013  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0014  *   GNU General Public License for more details.                          *
0015  *                                                                         *
0016  *   You should have received a copy of the GNU General Public License     *
0017  *   along with this program; if not, see <https://www.gnu.org/licenses/>. *
0018  ***************************************************************************/
0019 
0020 #include "starrating.h"
0021 
0022 #include <QHBoxLayout>
0023 #include <QLabel>
0024 #include <QFontMetrics>
0025 #include <QPaintEvent>
0026 #include <QMouseEvent>
0027 #include <QPainter>
0028 #include <QTimer>
0029 #include <QPushButton>
0030 #include <QDebug>
0031 
0032 #include <KLocalizedString>
0033 
0034 const int StarRatingPainter::numberOfStars = 8;
0035 
0036 StarRatingPainter::StarRatingPainter()
0037 {
0038     setHalfStepsEnabled(true);
0039     setMaxRating(numberOfStars * 2 /** times two because of setHalfStepsEnabled(true) */);
0040     setAlignment(Qt::AlignVCenter | Qt::AlignLeft);
0041     setLayoutDirection(Qt::LeftToRight);
0042 }
0043 
0044 void StarRatingPainter::paint(QPainter *painter, const QRect &rect, double percent, double hoverPercent)
0045 {
0046     const int rating {percent >= 0.0 ? static_cast<int>(percent *numberOfStars / 50.0 + 0.5) : 0};
0047     const int hoverRating {hoverPercent >= 0.0 ? static_cast<int>(hoverPercent *numberOfStars / 50.0 + 0.5) : 0};
0048     KRatingPainter::paint(painter, rect, rating, hoverRating);
0049 }
0050 
0051 double StarRatingPainter::roundToNearestHalfStarPercent(double percent)
0052 {
0053     const double result {percent >= 0.0 ? static_cast<int>(percent *numberOfStars / 50.0 + 0.5) * 50.0 / numberOfStars : -1.0};
0054     return result;
0055 }
0056 
0057 class StarRating::Private
0058 {
0059 private:
0060     StarRating *p;
0061 
0062 public:
0063     static const int paintMargin;
0064     StarRatingPainter ratingPainter;
0065 
0066     bool isReadOnly;
0067     double percent;
0068     int starHeight;
0069     int spacing;
0070     static const QString unsetStarsText;
0071     QLabel *labelPercent;
0072     QPushButton *clearButton;
0073     QPoint mouseLocation;
0074 
0075     Private(StarRating *parent)
0076             : p(parent), isReadOnly(false), percent(-1.0)
0077     {
0078         QHBoxLayout *layout = new QHBoxLayout(p);
0079         spacing = qMax(layout->spacing(), 8);
0080         layout->setContentsMargins(0, 0, 0, 0);
0081 
0082         labelPercent = new QLabel(p);
0083         layout->addWidget(labelPercent, 0, Qt::AlignRight | Qt::AlignVCenter);
0084         const QFontMetrics fm(labelPercent->fontMetrics());
0085 #if QT_VERSION >= 0x050b00
0086         labelPercent->setFixedWidth(fm.horizontalAdvance(unsetStarsText));
0087 #else // QT_VERSION >= 0x050b00
0088         labelPercent->setFixedWidth(fm.width(unsetStarsText));
0089 #endif // QT_VERSION >= 0x050b00
0090         labelPercent->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
0091         labelPercent->setText(unsetStarsText);
0092         labelPercent->installEventFilter(parent);
0093 
0094         layout->addStretch(1);
0095 
0096         clearButton = new QPushButton(QIcon::fromTheme(QStringLiteral("edit-clear-locationbar-rtl")), QString(), p);
0097         layout->addWidget(clearButton, 0, Qt::AlignRight | Qt::AlignVCenter);
0098         connect(clearButton, &QPushButton::clicked, p, &StarRating::clear);
0099         clearButton->installEventFilter(parent);
0100 
0101         starHeight = qMin(labelPercent->height() * 4 / 3, clearButton->height());
0102         parent->setMinimumHeight(starHeight);
0103     }
0104 
0105     inline QRect starsInside() const
0106     {
0107         return QRect(QPoint(labelPercent->width() + spacing, (p->height() - starHeight) / 2), QSize(p->width() - 2 * spacing - clearButton->width() - labelPercent->width(), starHeight));
0108     }
0109 
0110     double percentFromPosition(const QRect &inside, const QPoint &pos)
0111     {
0112         const int selectedStars = ratingPainter.ratingFromPosition(inside, pos);
0113         const double percent {qMax(0, qMin(StarRatingPainter::numberOfStars * 2, selectedStars)) * 50.0 / StarRatingPainter::numberOfStars};
0114         return percent;
0115     }
0116 };
0117 
0118 const int StarRating::Private::paintMargin = 2;
0119 const QString StarRating::Private::unsetStarsText {i18n("Not set")};
0120 
0121 StarRating::StarRating(QWidget *parent)
0122         : QWidget(parent), d(new Private(this))
0123 {
0124     QTimer::singleShot(250, this, &StarRating::buttonHeight);
0125 
0126     setMouseTracking(true);
0127 }
0128 
0129 StarRating::~StarRating()
0130 {
0131     delete d;
0132 }
0133 
0134 void StarRating::paintEvent(QPaintEvent *ev)
0135 {
0136     QWidget::paintEvent(ev);
0137     QPainter p(this);
0138 
0139     const QRect r = d->starsInside();
0140     const double hoverPercent {d->mouseLocation.isNull() ? -1.0 : d->percentFromPosition(r, d->mouseLocation)};
0141     const double labelPercent {hoverPercent >= 0.0 ? hoverPercent : d->percent};
0142 
0143     if (labelPercent >= 0.0) {
0144         d->ratingPainter.paint(&p, d->starsInside(), d->percent, hoverPercent);
0145         if (StarRatingPainter::numberOfStars < 10)
0146             d->labelPercent->setText(QString::number(labelPercent * StarRatingPainter::numberOfStars / 100.0, 'f', 1));
0147         else
0148             d->labelPercent->setText(QString::number(labelPercent * StarRatingPainter::numberOfStars / 100));
0149     } else {
0150         p.setOpacity(0.5);
0151         d->ratingPainter.paint(&p, d->starsInside(), -1.0);
0152         d->labelPercent->setText(d->unsetStarsText);
0153     }
0154 
0155     ev->accept();
0156 }
0157 
0158 void StarRating::mouseReleaseEvent(QMouseEvent *ev)
0159 {
0160     QWidget::mouseReleaseEvent(ev);
0161 
0162     if (!d->isReadOnly && ev->button() == Qt::LeftButton) {
0163         d->mouseLocation = QPoint();
0164         const double newPercent = d->percentFromPosition(d->starsInside(), ev->pos());
0165         setValue(newPercent);
0166         Q_EMIT modified();
0167         ev->accept();
0168     }
0169 }
0170 
0171 void StarRating::mouseMoveEvent(QMouseEvent *ev)
0172 {
0173     QWidget::mouseMoveEvent(ev);
0174 
0175     if (!d->isReadOnly) {
0176         d->mouseLocation = ev->pos();
0177         if (d->mouseLocation.x() < d->labelPercent->width() || d->mouseLocation.x() > width() - d->clearButton->width())
0178             d->mouseLocation = QPoint();
0179         update();
0180         ev->accept();
0181     }
0182 }
0183 
0184 void StarRating::leaveEvent(QEvent *ev)
0185 {
0186     QWidget::leaveEvent(ev);
0187 
0188     if (!d->isReadOnly) {
0189         d->mouseLocation = QPoint();
0190         update();
0191         ev->accept();
0192     }
0193 }
0194 
0195 bool StarRating::eventFilter(QObject *obj, QEvent *event)
0196 {
0197     if (obj != d->labelPercent && obj != d->clearButton)
0198         return false;
0199 
0200     if ((event->type() == QEvent::MouseMove || event->type() == QEvent::Enter) && d->mouseLocation != QPoint()) {
0201         d->mouseLocation = QPoint();
0202         update();
0203     }
0204     return false;
0205 }
0206 
0207 double StarRating::value() const
0208 {
0209     return d->percent;
0210 }
0211 
0212 void StarRating::setValue(double percent)
0213 {
0214     if (d->isReadOnly) return; ///< disallow modifications if read-only
0215 
0216     if (percent >= 0.0 && percent <= 100.0) {
0217         // Round given percent value to a value matching the number of stars,
0218         // in steps of half-stars
0219         d->percent = StarRatingPainter::roundToNearestHalfStarPercent(percent);
0220         update();
0221     }
0222 }
0223 
0224 void StarRating::unsetValue() {
0225     if (d->isReadOnly) return; ///< disallow modifications if read-only
0226 
0227     d->mouseLocation = QPoint();
0228     d->percent = -1.0;
0229     update();
0230 }
0231 
0232 void StarRating::setReadOnly(bool isReadOnly)
0233 {
0234     d->isReadOnly = isReadOnly;
0235     d->clearButton->setEnabled(!isReadOnly);
0236     setMouseTracking(!isReadOnly);
0237 }
0238 
0239 void StarRating::clear()
0240 {
0241     if (d->isReadOnly) return; ///< disallow modifications if read-only
0242 
0243     unsetValue();
0244     Q_EMIT modified();
0245 }
0246 
0247 void StarRating::buttonHeight()
0248 {
0249     const QSizePolicy sp = d->clearButton->sizePolicy();
0250     // Allow clear button to take as much vertical space as available
0251     d->clearButton->setSizePolicy(sp.horizontalPolicy(), QSizePolicy::MinimumExpanding);
0252 
0253     // Update this widget's height behaviour
0254     d->starHeight = qMin(d->labelPercent->height() * 4 / 3, d->clearButton->height());
0255     setMinimumHeight(d->starHeight);
0256 }
0257 
0258 bool StarRatingFieldInput::reset(const Value &value)
0259 {
0260     bool result = false;
0261     const QString text = PlainTextValue::text(value);
0262     if (text.isEmpty()) {
0263         unsetValue();
0264         result = true;
0265     } else {
0266         const double number = text.toDouble(&result);
0267         if (result && number >= 0.0 && number <= 100.0) {
0268             setValue(number);
0269             result = true;
0270         } else {
0271             // Some value provided that cannot be interpreted or is out of range
0272             unsetValue();
0273             result = false;
0274         }
0275     }
0276     return result;
0277 }
0278 
0279 bool StarRatingFieldInput::apply(Value &v) const
0280 {
0281     v.clear();
0282     const double percent = value();
0283     if (percent >= 0.0 && percent <= 100)
0284         v.append(QSharedPointer<PlainText>(new PlainText(QString::number(percent, 'f', 2))));
0285     return true;
0286 }
0287 
0288 bool StarRatingFieldInput::validate(QWidget **, QString &) const
0289 {
0290     // Star rating widget has always a valid value (even no rating is valid)
0291     return true;
0292 }