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 }