File indexing completed on 2024-12-22 04:15:04

0001 /*
0002  * SPDX-FileCopyrightText: 2020 Mathias Wein <lynx.mw+kde@gmail.com>
0003  *
0004  * SPDX-License-Identifier: GPL-3.0-or-later
0005  */
0006 
0007 #include "WGShadeSlider.h"
0008 
0009 #include "KoColorDisplayRendererInterface.h"
0010 
0011 #include <QImage>
0012 #include <QMouseEvent>
0013 #include <QPainter>
0014 #include <QVector4D>
0015 #include <QtMath>
0016 
0017 struct WGShadeSlider::Private
0018 {
0019     Private() {}
0020     QImage background;
0021     QVector4D range;
0022     QVector4D offset;
0023     QVector4D baseValues;
0024     qreal handleValue {0};
0025     qreal leftStart {-1};
0026     qreal leftEnd {0};
0027     qreal rightStart {0};
0028     qreal rightEnd {-1};
0029     KisVisualColorModelSP selectorModel;
0030     WGSelectorDisplayConfigSP displayConfig;
0031     int cursorWidth {11};
0032     int lineWidth {1};
0033     int numPatches {9};
0034     bool widgetSizeOk {false};
0035     bool sliderMode {true};
0036     bool imageNeedsUpdate {true};
0037 };
0038 
0039 WGShadeSlider::WGShadeSlider(WGSelectorDisplayConfigSP config, QWidget *parent, KisVisualColorModelSP model)
0040     : QWidget(parent)
0041     , m_d(new Private)
0042 {
0043     m_d->selectorModel = model;
0044     m_d->displayConfig = config;
0045     recalculateParameters();
0046     connect(config.data(), &WGSelectorDisplayConfig::sigDisplayConfigurationChanged,
0047             this, &WGShadeSlider::slotDisplayConfigurationChanged);
0048 }
0049 
0050 WGShadeSlider::~WGShadeSlider()
0051 {}
0052 
0053 void WGShadeSlider::setGradient(const QVector4D &range, const QVector4D &offset)
0054 {
0055     m_d->range = range;
0056     m_d->offset = offset;
0057     m_d->imageNeedsUpdate = true;
0058     resetHandle();
0059 }
0060 
0061 void WGShadeSlider::setDisplayMode(bool slider, int numPatches)
0062 {
0063     if (slider != m_d->sliderMode ||
0064         (!slider && numPatches != m_d->numPatches)) {
0065         m_d->sliderMode = slider;
0066         if (!slider && numPatches > 2) {
0067             m_d->numPatches = numPatches;
0068         }
0069         m_d->widgetSizeOk = sizeRequirementsMet();
0070         m_d->imageNeedsUpdate = true;
0071         resetHandle();
0072     }
0073 }
0074 
0075 void WGShadeSlider::setModel(KisVisualColorModelSP model)
0076 {
0077     m_d->selectorModel = model;
0078     m_d->imageNeedsUpdate = true;
0079     update();
0080 }
0081 
0082 QVector4D WGShadeSlider::channelValues() const
0083 {
0084     return calculateChannelValues(m_d->handleValue);
0085 }
0086 
0087 const QImage *WGShadeSlider::background()
0088 {
0089     if (m_d->imageNeedsUpdate) {
0090         m_d->background = renderBackground();
0091         m_d->imageNeedsUpdate = false;
0092     }
0093     return &m_d->background;
0094 }
0095 
0096 QSize WGShadeSlider::minimumSizeHint() const
0097 {
0098     return QSize(50, 8);
0099 }
0100 
0101 void WGShadeSlider::mousePressEvent(QMouseEvent *event)
0102 {
0103     if (event->button() == Qt::LeftButton) {
0104         emit sigInteraction(true);
0105         if (adjustHandleValue(event->localPos())) {
0106             emit sigChannelValuesChanged(channelValues());
0107             update();
0108         }
0109     } else {
0110         event->ignore();
0111     }
0112 }
0113 
0114 void WGShadeSlider::mouseMoveEvent(QMouseEvent *event)
0115 {
0116     if (event->buttons() & Qt::LeftButton) {
0117         if (adjustHandleValue(event->localPos())) {
0118             emit sigChannelValuesChanged(channelValues());
0119             update();
0120         }
0121     } else {
0122         event->ignore();
0123     }
0124 }
0125 
0126 void WGShadeSlider::mouseReleaseEvent(QMouseEvent *event)
0127 {
0128     if (event->button() == Qt::LeftButton) {
0129         emit sigInteraction(false);
0130     } else {
0131         event->ignore();
0132     }
0133 }
0134 
0135 void WGShadeSlider::paintEvent(QPaintEvent*)
0136 {
0137     if (m_d->imageNeedsUpdate) {
0138         m_d->background = renderBackground();
0139         m_d->imageNeedsUpdate = false;
0140     }
0141     QPainter painter(this);
0142     painter.drawImage(0, 0, m_d->background);
0143     painter.scale(1.0/devicePixelRatioF(), 1.0/devicePixelRatioF());
0144     QRectF handleRect;
0145     if (m_d->sliderMode) {
0146         QPointF sliderPos = convertSliderValueToWidgetCoordinate(m_d->handleValue);
0147         int sliderX = qRound(sliderPos.x());
0148         handleRect = QRectF(sliderX - m_d->cursorWidth/2, 0, m_d->cursorWidth, height());
0149     } else if (m_d->handleValue >= 0) {
0150         handleRect = patchRect(m_d->handleValue);
0151     }
0152     if (handleRect.isValid()) {
0153         QPen pen(QColor(175,175,175), m_d->lineWidth, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin);
0154         painter.setPen(pen);
0155         strokeRect(painter, handleRect, devicePixelRatioF(), 0);
0156         pen.setColor(QColor(75,75,75));
0157         painter.setPen(pen);
0158         strokeRect(painter, handleRect, devicePixelRatioF(), 1);
0159     }
0160 }
0161 
0162 void WGShadeSlider::resizeEvent(QResizeEvent *)
0163 {
0164     recalculateParameters();
0165 }
0166 
0167 void WGShadeSlider::slotSetChannelValues(const QVector4D &values)
0168 {
0169     m_d->baseValues = values;
0170     m_d->imageNeedsUpdate = true;
0171     resetHandle();
0172 }
0173 
0174 void WGShadeSlider::resetHandle()
0175 {
0176     m_d->handleValue = m_d->sliderMode ? 0 : -1;
0177     update();
0178 }
0179 
0180 void WGShadeSlider::slotDisplayConfigurationChanged()
0181 {
0182     m_d->imageNeedsUpdate = true;
0183     update();
0184 }
0185 
0186 bool WGShadeSlider::adjustHandleValue(const QPointF &widgetPos)
0187 {
0188     if (!m_d->widgetSizeOk) {
0189         return false;
0190     }
0191 
0192     if (m_d->sliderMode) {
0193         qreal sliderPos = convertWidgetCoordinateToSliderValue(widgetPos);
0194         if (!qFuzzyIsNull(m_d->handleValue - sliderPos)) {
0195             m_d->handleValue = sliderPos;
0196             return true;
0197         }
0198     } else {
0199         int patchNum = getPatch(widgetPos);
0200         if (patchNum >= 0 && patchNum != (int)m_d->handleValue) {
0201             m_d->handleValue = patchNum;
0202             return true;
0203         }
0204     }
0205     return false;
0206 }
0207 
0208 QPointF WGShadeSlider::convertSliderValueToWidgetCoordinate(qreal value)
0209 {
0210     QPointF pos(0.0, 0.0);
0211     if (value < 0) {
0212         pos.setX(m_d->leftStart - value * (m_d->leftEnd - m_d->leftStart));
0213     }
0214     else if (value > 0) {
0215         pos.setX(m_d->rightStart + value * (m_d->rightEnd - m_d->rightStart));
0216     }
0217     else {
0218         pos.setX((width() - 1) / 2);
0219     }
0220     return pos;
0221 }
0222 
0223 qreal WGShadeSlider::convertWidgetCoordinateToSliderValue(QPointF coordinate)
0224 {
0225     qreal x = coordinate.x();
0226     if (x < m_d->leftEnd) {
0227         return -1.0;
0228     }
0229     else if (x < m_d->leftStart) {
0230         return  (m_d->leftStart - x) / (m_d->leftEnd - m_d->leftStart);
0231     }
0232     else if (x < m_d->rightStart) {
0233         return 0.0;
0234     }
0235     else if (x < m_d->rightEnd) {
0236         return (x - m_d->rightStart) / (m_d->rightEnd - m_d->rightStart);
0237     }
0238     return 1.0;
0239 }
0240 
0241 QVector4D WGShadeSlider::calculateChannelValues(qreal sliderPos) const
0242 {
0243     float delta = 0.0f;
0244     if (m_d->sliderMode) {
0245         delta = (float)sliderPos;
0246     } else if (sliderPos >= 0 || m_d->numPatches > 1) {
0247         delta = 2.0f * float(sliderPos)/(m_d->numPatches - 1.0f) - 1.0f;
0248     }
0249 
0250     QVector4D channelVec = m_d->baseValues + m_d->offset + delta * m_d->range;
0251     // Hue wraps around
0252     if (m_d->selectorModel->isHSXModel()) {
0253         channelVec[0] = (float)fmod(channelVec[0], 1.0);
0254         if (channelVec[0] < 0) {
0255             channelVec[0] += 1.f;
0256         }
0257     }
0258     else {
0259         channelVec[0] = qBound(0.f, channelVec[0], 1.f);
0260     }
0261 
0262     for (int i = 1; i < 3; i++) {
0263         channelVec[i] = qBound(0.f, channelVec[i], 1.f);
0264     }
0265     return channelVec;
0266 }
0267 
0268 int WGShadeSlider::getPatch(const QPointF pos) const
0269 {
0270     int patch = m_d->numPatches * pos.x() / width();
0271     if (patch >= 0 && patch < m_d->numPatches) {
0272         return patch;
0273     }
0274     return -1;
0275 }
0276 
0277 QRectF WGShadeSlider::patchRect(int index) const
0278 {
0279     qreal patchWidth = width() / qreal(m_d->numPatches);
0280     qreal margin = 1.5;
0281     QPointF topLeft(index * patchWidth + margin, 0);
0282     QPointF bottomRight((index+1) * patchWidth - margin, height());
0283     return QRectF(topLeft, bottomRight);
0284 }
0285 
0286 void WGShadeSlider::recalculateParameters()
0287 {
0288     int center = (width() - 1) / 2;
0289     int halfCursor = m_d->cursorWidth / 2;
0290 
0291     m_d->leftEnd = halfCursor;
0292     m_d->leftStart = center - halfCursor;
0293 
0294     m_d->rightStart = center + halfCursor;
0295     m_d->rightEnd = 2 * center  - halfCursor;
0296 
0297     m_d->lineWidth = qRound(devicePixelRatioF() - 0.1);
0298     m_d->widgetSizeOk = sizeRequirementsMet();
0299     m_d->imageNeedsUpdate = true;
0300 }
0301 
0302 bool WGShadeSlider::sizeRequirementsMet() const
0303 {
0304     if (m_d->sliderMode) {
0305         return m_d->leftStart - m_d->leftEnd > 0 &&  m_d->rightEnd - m_d->rightStart > 0;
0306     } else {
0307         return width() > m_d->numPatches;
0308     }
0309 }
0310 
0311 QImage WGShadeSlider::renderBackground()
0312 {
0313     if (!m_d->widgetSizeOk || !m_d->selectorModel || !m_d->selectorModel->colorSpace()) {
0314         return QImage();
0315     }
0316 
0317     // Hi-DPI aware rendering requires that we determine the device pixel dimension;
0318     // actual widget size in device pixels is not accessible unfortunately, it might be 1px smaller...
0319     const qreal deviceDivider = 1.0 / devicePixelRatioF();
0320     const int deviceWidth = qCeil(width() * devicePixelRatioF());
0321     const int deviceHeight = qCeil(height() * devicePixelRatioF());
0322     if (m_d->sliderMode) {
0323         const KoColorSpace *currentCS = m_d->selectorModel->colorSpace();
0324         const quint32 pixelSize = currentCS->pixelSize();
0325         quint32 imageSize = deviceWidth * pixelSize;
0326         QScopedArrayPointer<quint8> raw(new quint8[imageSize] {});
0327         quint8 *dataPtr = raw.data();
0328 
0329         for (int x = 0; x < deviceWidth; x++, dataPtr += pixelSize) {
0330             qreal sliderVal = convertWidgetCoordinateToSliderValue(QPointF(x, 0) * deviceDivider);
0331             QVector4D coordinates = calculateChannelValues(sliderVal);
0332             KoColor c = m_d->selectorModel->convertChannelValuesToKoColor(coordinates);
0333             memcpy(dataPtr, c.data(), pixelSize);
0334         }
0335 
0336         QImage image = m_d->displayConfig->displayConverter()->toQImage(currentCS, raw.data(), {deviceWidth, 1},
0337                                                                         m_d->displayConfig->previewInPaintingCS());
0338         image = image.scaled(QSize(deviceWidth, deviceHeight));
0339 
0340         QPainter painter(&image);
0341         QPen pen(QColor(175,175,175), m_d->lineWidth, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin);
0342         painter.setPen(pen);
0343         strokeRect(painter, QRectF(m_d->leftStart, 0, m_d->cursorWidth, height()), devicePixelRatioF(), 0);
0344         pen.setColor(QColor(75,75,75));
0345         painter.setPen(pen);
0346         strokeRect(painter, QRectF(m_d->leftStart, 0, m_d->cursorWidth, height()), devicePixelRatioF(), 1);
0347 
0348         image.setDevicePixelRatio(devicePixelRatioF());
0349         return image;
0350     } else {
0351         QImage image(deviceWidth, deviceHeight, QImage::Format_ARGB32);
0352         image.fill(Qt::transparent);
0353         image.setDevicePixelRatio(devicePixelRatioF());
0354         QPainter painter(&image);
0355         painter.setPen(Qt::NoPen);
0356 
0357         for (int i = 0; i < m_d->numPatches; i++) {
0358             QVector4D values = calculateChannelValues(i);
0359             KoColor col = m_d->selectorModel->convertChannelValuesToKoColor(values);
0360             QColor qCol = m_d->displayConfig->displayConverter()->toQColor(col, m_d->displayConfig->previewInPaintingCS());
0361             painter.setBrush(qCol);
0362             painter.drawRect(patchRect(i));
0363         }
0364         return image;
0365     }
0366 }
0367 
0368 void WGShadeSlider::strokeRect(QPainter &painter, const QRectF &rect, qreal pixelSize, qreal shrinkX)
0369 {
0370     qreal lineWidth = painter.pen().widthF();
0371     QPointF topLeft(qRound(rect.left() * pixelSize) + (shrinkX + 0.5) * lineWidth,
0372                     qRound(rect.top() * pixelSize) + 0.5 * lineWidth);
0373     QPointF bottomRight(qRound(rect.right() * pixelSize) - (shrinkX + 0.5) * lineWidth,
0374                         qRound(rect.bottom() * pixelSize) - 0.5 * lineWidth);
0375     painter.drawRect(QRectF(topLeft, bottomRight));
0376 }