| 1 | /* |
| 2 | This file is part of the KDE libraries |
| 3 | SPDX-FileCopyrightText: 2007-2008 Sebastian Trueg <trueg@kde.org> |
| 4 | |
| 5 | SPDX-License-Identifier: LGPL-2.0-or-later |
| 6 | */ |
| 7 | |
| 8 | #include "kratingpainter.h" |
| 9 | |
| 10 | #include <QIcon> |
| 11 | #include <QPainter> |
| 12 | #include <QPixmap> |
| 13 | #include <QPoint> |
| 14 | #include <QRect> |
| 15 | |
| 16 | class KRatingPainterPrivate |
| 17 | { |
| 18 | public: |
| 19 | QPixmap getPixmap(int size, qreal dpr, QIcon::State state = QIcon::On); |
| 20 | |
| 21 | int maxRating = 10; |
| 22 | int spacing = 0; |
| 23 | QIcon icon; |
| 24 | bool isEnabled = true; |
| 25 | bool bHalfSteps = true; |
| 26 | Qt::Alignment alignment = Qt::AlignCenter; |
| 27 | Qt::LayoutDirection direction = Qt::LeftToRight; |
| 28 | QPixmap customPixmap; |
| 29 | }; |
| 30 | |
| 31 | static void imageToGrayScale(QImage &img, float value); |
| 32 | static void imageToSemiTransparent(QImage &img); |
| 33 | |
| 34 | QPixmap KRatingPainterPrivate::getPixmap(int size, qreal dpr, QIcon::State state) |
| 35 | { |
| 36 | bool transformToOffState = (state == QIcon::Off); |
| 37 | QPixmap p; |
| 38 | |
| 39 | if (!customPixmap.isNull()) { |
| 40 | p = customPixmap.scaled(s: QSize(size, size)); |
| 41 | } else { |
| 42 | QIcon _icon(icon); |
| 43 | if (_icon.isNull()) { |
| 44 | if (state == QIcon::On) { |
| 45 | _icon = QIcon::fromTheme(QStringLiteral("rating" )); |
| 46 | } else if (QIcon::hasThemeIcon(QStringLiteral("rating-unrated" ))) { |
| 47 | _icon = QIcon::fromTheme(QStringLiteral("rating-unrated" )); |
| 48 | transformToOffState = false; // no need because we already have the perfect icon |
| 49 | } else { |
| 50 | _icon = QIcon::fromTheme(QStringLiteral("rating" )); // will be transformed to the "off" state |
| 51 | } |
| 52 | } |
| 53 | p = _icon.pixmap(size: QSize(size, size), devicePixelRatio: dpr); |
| 54 | } |
| 55 | |
| 56 | if (transformToOffState) { |
| 57 | QImage img = p.toImage().convertToFormat(f: QImage::Format_ARGB32); |
| 58 | imageToGrayScale(img, value: 1.0); |
| 59 | // The icon might have already been monochrome, so we also need to make it semi-transparent to see a difference. |
| 60 | imageToSemiTransparent(img); |
| 61 | return QPixmap::fromImage(image: img); |
| 62 | } |
| 63 | return p; |
| 64 | } |
| 65 | |
| 66 | KRatingPainter::KRatingPainter() |
| 67 | : d(new KRatingPainterPrivate()) |
| 68 | { |
| 69 | } |
| 70 | |
| 71 | KRatingPainter::~KRatingPainter() = default; |
| 72 | |
| 73 | int KRatingPainter::maxRating() const |
| 74 | { |
| 75 | return d->maxRating; |
| 76 | } |
| 77 | |
| 78 | bool KRatingPainter::halfStepsEnabled() const |
| 79 | { |
| 80 | return d->bHalfSteps; |
| 81 | } |
| 82 | |
| 83 | Qt::Alignment KRatingPainter::alignment() const |
| 84 | { |
| 85 | return d->alignment; |
| 86 | } |
| 87 | |
| 88 | Qt::LayoutDirection KRatingPainter::layoutDirection() const |
| 89 | { |
| 90 | return d->direction; |
| 91 | } |
| 92 | |
| 93 | QIcon KRatingPainter::icon() const |
| 94 | { |
| 95 | return d->icon; |
| 96 | } |
| 97 | |
| 98 | bool KRatingPainter::isEnabled() const |
| 99 | { |
| 100 | return d->isEnabled; |
| 101 | } |
| 102 | |
| 103 | QPixmap KRatingPainter::customPixmap() const |
| 104 | { |
| 105 | return d->customPixmap; |
| 106 | } |
| 107 | |
| 108 | int KRatingPainter::spacing() const |
| 109 | { |
| 110 | return d->spacing; |
| 111 | } |
| 112 | |
| 113 | void KRatingPainter::setMaxRating(int max) |
| 114 | { |
| 115 | d->maxRating = max; |
| 116 | } |
| 117 | |
| 118 | void KRatingPainter::setHalfStepsEnabled(bool enabled) |
| 119 | { |
| 120 | d->bHalfSteps = enabled; |
| 121 | } |
| 122 | |
| 123 | void KRatingPainter::setAlignment(Qt::Alignment align) |
| 124 | { |
| 125 | d->alignment = align; |
| 126 | } |
| 127 | |
| 128 | void KRatingPainter::setLayoutDirection(Qt::LayoutDirection direction) |
| 129 | { |
| 130 | d->direction = direction; |
| 131 | } |
| 132 | |
| 133 | void KRatingPainter::setIcon(const QIcon &icon) |
| 134 | { |
| 135 | d->icon = icon; |
| 136 | } |
| 137 | |
| 138 | void KRatingPainter::setEnabled(bool enabled) |
| 139 | { |
| 140 | d->isEnabled = enabled; |
| 141 | } |
| 142 | |
| 143 | void KRatingPainter::setCustomPixmap(const QPixmap &pixmap) |
| 144 | { |
| 145 | d->customPixmap = pixmap; |
| 146 | } |
| 147 | |
| 148 | void KRatingPainter::setSpacing(int s) |
| 149 | { |
| 150 | d->spacing = qMax(a: 0, b: s); |
| 151 | } |
| 152 | |
| 153 | static void imageToGrayScale(QImage &img, float value) |
| 154 | { |
| 155 | QRgb *data = (QRgb *)img.bits(); |
| 156 | QRgb *end = data + img.width() * img.height(); |
| 157 | |
| 158 | unsigned char gray; |
| 159 | unsigned char val = (unsigned char)(255.0 * value); |
| 160 | while (data != end) { |
| 161 | gray = qGray(rgb: *data); |
| 162 | *data = qRgba(r: (val * gray + (255 - val) * qRed(rgb: *data)) >> 8, |
| 163 | g: (val * gray + (255 - val) * qGreen(rgb: *data)) >> 8, |
| 164 | b: (val * gray + (255 - val) * qBlue(rgb: *data)) >> 8, |
| 165 | a: qAlpha(rgb: *data)); |
| 166 | ++data; |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | static void imageToSemiTransparent(QImage &img) |
| 171 | { |
| 172 | QRgb *data = (QRgb *)img.bits(); |
| 173 | QRgb *end = data + img.width() * img.height(); |
| 174 | |
| 175 | while (data != end) { |
| 176 | *data = qRgba(r: qRed(rgb: *data), g: qGreen(rgb: *data), b: qBlue(rgb: *data), a: qAlpha(rgb: *data) >> 1); |
| 177 | ++data; |
| 178 | } |
| 179 | } |
| 180 | |
| 181 | void KRatingPainter::paint(QPainter *painter, const QRect &rect, int rating, int hoverRating) const |
| 182 | { |
| 183 | const qreal dpr = painter->device()->devicePixelRatio(); |
| 184 | rating = qMin(a: rating, b: d->maxRating); |
| 185 | hoverRating = qMin(a: hoverRating, b: d->maxRating); |
| 186 | |
| 187 | int numUsedStars = d->bHalfSteps ? d->maxRating / 2 : d->maxRating; |
| 188 | |
| 189 | if (hoverRating >= 0 && hoverRating < rating) { |
| 190 | int tmp = hoverRating; |
| 191 | hoverRating = rating; |
| 192 | rating = tmp; |
| 193 | } |
| 194 | |
| 195 | int usedSpacing = d->spacing; |
| 196 | |
| 197 | // get the rating pixmaps |
| 198 | int maxHSizeOnePix = (rect.width() - (numUsedStars - 1) * usedSpacing) / numUsedStars; |
| 199 | QPixmap ratingPix = d->getPixmap(size: qMin(a: rect.height(), b: maxHSizeOnePix), dpr, state: QIcon::On); |
| 200 | |
| 201 | QSize ratingPixSize = ratingPix.size() / ratingPix.devicePixelRatio(); |
| 202 | |
| 203 | QPixmap disabledRatingPix = d->getPixmap(size: qMin(a: rect.height(), b: maxHSizeOnePix), dpr, state: QIcon::Off); |
| 204 | QImage disabledRatingImage = disabledRatingPix.toImage().convertToFormat(f: QImage::Format_ARGB32); |
| 205 | QPixmap hoverPix; |
| 206 | |
| 207 | // if we are disabled we become gray and more transparent |
| 208 | if (!d->isEnabled) { |
| 209 | ratingPix = disabledRatingPix; |
| 210 | |
| 211 | imageToSemiTransparent(img&: disabledRatingImage); |
| 212 | disabledRatingPix = QPixmap::fromImage(image: disabledRatingImage); |
| 213 | } |
| 214 | |
| 215 | bool half = d->bHalfSteps && rating % 2; |
| 216 | int numRatingStars = d->bHalfSteps ? rating / 2 : rating; |
| 217 | |
| 218 | int numHoverStars = 0; |
| 219 | bool halfHover = false; |
| 220 | if (hoverRating >= 0 && rating != hoverRating && d->isEnabled) { |
| 221 | numHoverStars = d->bHalfSteps ? hoverRating / 2 : hoverRating; |
| 222 | halfHover = d->bHalfSteps && hoverRating % 2; |
| 223 | |
| 224 | disabledRatingImage = ratingPix.toImage().convertToFormat(f: QImage::Format_ARGB32); |
| 225 | imageToGrayScale(img&: disabledRatingImage, value: 0.5); |
| 226 | |
| 227 | hoverPix = QPixmap::fromImage(image: disabledRatingImage); |
| 228 | } |
| 229 | |
| 230 | if (d->alignment & Qt::AlignJustify && numUsedStars > 1) { |
| 231 | int w = rect.width(); |
| 232 | w -= numUsedStars * ratingPixSize.width(); |
| 233 | usedSpacing = w / (numUsedStars - 1); |
| 234 | } |
| 235 | |
| 236 | int ratingAreaWidth = ratingPixSize.width() * numUsedStars + usedSpacing * (numUsedStars - 1); |
| 237 | |
| 238 | int i = 0; |
| 239 | int x = rect.x(); |
| 240 | if (d->alignment & Qt::AlignRight) { |
| 241 | x += (rect.width() - ratingAreaWidth); |
| 242 | } else if (d->alignment & Qt::AlignHCenter) { |
| 243 | x += (rect.width() - ratingAreaWidth) / 2; |
| 244 | } |
| 245 | |
| 246 | int xInc = ratingPixSize.width() + usedSpacing; |
| 247 | if (d->direction == Qt::RightToLeft) { |
| 248 | x = rect.width() - ratingPixSize.width() - x; |
| 249 | xInc = -xInc; |
| 250 | } |
| 251 | |
| 252 | int y = rect.y(); |
| 253 | if (d->alignment & Qt::AlignVCenter) { |
| 254 | y += (rect.height() / 2 - ratingPixSize.height() / 2); |
| 255 | } else if (d->alignment & Qt::AlignBottom) { |
| 256 | y += (rect.height() - ratingPixSize.height()); |
| 257 | } |
| 258 | for (; i < numRatingStars; ++i) { |
| 259 | painter->drawPixmap(x, y, pm: ratingPix); |
| 260 | x += xInc; |
| 261 | } |
| 262 | if (half) { |
| 263 | painter->drawPixmap(x, |
| 264 | y, |
| 265 | w: ratingPixSize.width() / 2, |
| 266 | h: ratingPixSize.height(), |
| 267 | pm: d->direction == Qt::RightToLeft ? (numHoverStars > 0 ? hoverPix : disabledRatingPix) : ratingPix, |
| 268 | sx: 0, |
| 269 | sy: 0, |
| 270 | sw: ratingPix.width() / 2, |
| 271 | sh: ratingPix.height()); // source sizes are deliberately not device independent |
| 272 | painter->drawPixmap(x: x + ratingPixSize.width() / 2, |
| 273 | y, |
| 274 | w: ratingPixSize.width() / 2, |
| 275 | h: ratingPixSize.height(), |
| 276 | pm: d->direction == Qt::RightToLeft ? ratingPix : (numHoverStars > 0 ? hoverPix : disabledRatingPix), |
| 277 | sx: ratingPix.width() / 2, |
| 278 | sy: 0, |
| 279 | sw: ratingPix.width() / 2, |
| 280 | sh: ratingPix.height()); |
| 281 | x += xInc; |
| 282 | ++i; |
| 283 | } |
| 284 | for (; i < numHoverStars; ++i) { |
| 285 | painter->drawPixmap(x, y, pm: hoverPix); |
| 286 | x += xInc; |
| 287 | } |
| 288 | if (halfHover) { |
| 289 | painter->drawPixmap(x, |
| 290 | y, |
| 291 | w: ratingPixSize.width() / 2, |
| 292 | h: ratingPixSize.height(), |
| 293 | pm: d->direction == Qt::RightToLeft ? disabledRatingPix : hoverPix, |
| 294 | sx: 0, |
| 295 | sy: 0, |
| 296 | sw: ratingPix.width() / 2, |
| 297 | sh: ratingPix.height()); |
| 298 | painter->drawPixmap(x: x + ratingPixSize.width() / 2, |
| 299 | y, |
| 300 | w: ratingPixSize.width() / 2, |
| 301 | h: ratingPixSize.height(), |
| 302 | pm: d->direction == Qt::RightToLeft ? hoverPix : disabledRatingPix, |
| 303 | sx: ratingPix.width() / 2, |
| 304 | sy: 0, |
| 305 | sw: ratingPix.width() / 2, |
| 306 | sh: ratingPix.height()); |
| 307 | x += xInc; |
| 308 | ++i; |
| 309 | } |
| 310 | for (; i < numUsedStars; ++i) { |
| 311 | painter->drawPixmap(x, y, pm: disabledRatingPix); |
| 312 | x += xInc; |
| 313 | } |
| 314 | } |
| 315 | |
| 316 | int KRatingPainter::ratingFromPosition(const QRect &rect, const QPoint &pos) const |
| 317 | { |
| 318 | int usedSpacing = d->spacing; |
| 319 | int numUsedStars = d->bHalfSteps ? d->maxRating / 2 : d->maxRating; |
| 320 | int maxHSizeOnePix = (rect.width() - (numUsedStars - 1) * usedSpacing) / numUsedStars; |
| 321 | QPixmap ratingPix = d->getPixmap(size: qMin(a: rect.height(), b: maxHSizeOnePix), dpr: 1.0); |
| 322 | QSize ratingPixSize = ratingPix.deviceIndependentSize().toSize(); |
| 323 | |
| 324 | int ratingAreaWidth = ratingPixSize.width() * numUsedStars + usedSpacing * (numUsedStars - 1); |
| 325 | |
| 326 | QRect usedRect(rect); |
| 327 | if (d->alignment & Qt::AlignRight) { |
| 328 | usedRect.setLeft(rect.right() - ratingAreaWidth); |
| 329 | } else if (d->alignment & Qt::AlignHCenter) { |
| 330 | int x = (rect.width() - ratingAreaWidth) / 2; |
| 331 | usedRect.setLeft(rect.left() + x); |
| 332 | usedRect.setRight(rect.right() - x); |
| 333 | } else { // d->alignment & Qt::AlignLeft |
| 334 | usedRect.setRight(rect.left() + ratingAreaWidth - 1); |
| 335 | } |
| 336 | |
| 337 | if (d->alignment & Qt::AlignBottom) { |
| 338 | usedRect.setTop(rect.bottom() - ratingPixSize.height() + 1); |
| 339 | } else if (d->alignment & Qt::AlignVCenter) { |
| 340 | int x = (rect.height() - ratingPixSize.height()) / 2; |
| 341 | usedRect.setTop(rect.top() + x); |
| 342 | usedRect.setBottom(rect.bottom() - x); |
| 343 | } else { // d->alignment & Qt::AlignTop |
| 344 | usedRect.setBottom(rect.top() + ratingPixSize.height() - 1); |
| 345 | } |
| 346 | |
| 347 | if (usedRect.contains(p: pos)) { |
| 348 | int x = 0; |
| 349 | if (d->direction == Qt::RightToLeft) { |
| 350 | x = usedRect.right() - pos.x(); |
| 351 | } else { |
| 352 | x = pos.x() - usedRect.left(); |
| 353 | } |
| 354 | |
| 355 | double one = (double)usedRect.width() / (double)d->maxRating; |
| 356 | |
| 357 | // qCDebug(KWidgetsAddonsLog) << "rating:" << ( int )( ( double )x/one + 0.5 ); |
| 358 | |
| 359 | return (int)((double)x / one + 0.5); |
| 360 | } else { |
| 361 | return -1; |
| 362 | } |
| 363 | } |
| 364 | |
| 365 | void KRatingPainter::paintRating(QPainter *painter, const QRect &rect, Qt::Alignment align, int rating, int hoverRating) |
| 366 | { |
| 367 | KRatingPainter rp; |
| 368 | rp.setAlignment(align); |
| 369 | rp.setLayoutDirection(painter->layoutDirection()); |
| 370 | rp.paint(painter, rect, rating, hoverRating); |
| 371 | } |
| 372 | |
| 373 | int KRatingPainter::getRatingFromPosition(const QRect &rect, Qt::Alignment align, Qt::LayoutDirection direction, const QPoint &pos) |
| 374 | { |
| 375 | KRatingPainter rp; |
| 376 | rp.setAlignment(align); |
| 377 | rp.setLayoutDirection(direction); |
| 378 | return rp.ratingFromPosition(rect, pos); |
| 379 | } |
| 380 | |