File indexing completed on 2024-05-12 04:44:29

0001 // SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
0002 // SPDX-License-Identifier: BSD-2-Clause OR MIT
0003 
0004 // Own headers
0005 // First the interface, which forces the header to be self-contained.
0006 #include "chromahuediagram.h"
0007 // Second, the private implementation.
0008 #include "chromahuediagram_p.h" // IWYU pragma: associated
0009 
0010 #include "abstractdiagram.h"
0011 #include "asyncimageprovider.h"
0012 #include "chromahueimageparameters.h"
0013 #include "cielchd50values.h"
0014 #include "colorwheelimage.h"
0015 #include "constpropagatingrawpointer.h"
0016 #include "constpropagatinguniquepointer.h"
0017 #include "helper.h"
0018 #include "helperconstants.h"
0019 #include "helperconversion.h"
0020 #include "lchdouble.h"
0021 #include "polarpointf.h"
0022 #include "rgbcolorspace.h"
0023 #include <lcms2.h>
0024 #include <qbrush.h>
0025 #include <qcolor.h>
0026 #include <qevent.h>
0027 #include <qimage.h>
0028 #include <qnamespace.h>
0029 #include <qpainter.h>
0030 #include <qpen.h>
0031 #include <qpoint.h>
0032 #include <qsharedpointer.h>
0033 #include <qwidget.h>
0034 
0035 namespace PerceptualColor
0036 {
0037 /** @brief The constructor.
0038  * @param colorSpace The color space within which this widget should operate.
0039  * Can be created with @ref RgbColorSpaceFactory.
0040  * @param parent The widget’s parent widget. This parameter will be passed
0041  * to the base class’s constructor. */
0042 ChromaHueDiagram::ChromaHueDiagram(const QSharedPointer<PerceptualColor::RgbColorSpace> &colorSpace, QWidget *parent)
0043     : AbstractDiagram(parent)
0044     , d_pointer(new ChromaHueDiagramPrivate(this, colorSpace))
0045 {
0046     // Setup LittleCMS. This is the first thing to do, because other
0047     // operations rely on a working LittleCMS.
0048     d_pointer->m_rgbColorSpace = colorSpace;
0049 
0050     // Set focus policy
0051     // In Qt, usually focus (QWidget::hasFocus()) by mouse click is either
0052     // not accepted at all or accepted always for the hole rectangular
0053     // widget, depending on QWidget::focusPolicy(). This is not convenient
0054     // and intuitive for big, circular-shaped widgets like this one. It
0055     // would be nicer if the focus would only be accepted by mouse clicks
0056     // <em>within the circle itself</em>. Qt does not provide a build-in
0057     // way to do this. But a workaround to implement this behavior is
0058     // possible: Set QWidget::focusPolicy() to <em>not</em> accept focus
0059     // by mouse click. Then, reimplement mousePressEvent() and call
0060     // setFocus(Qt::MouseFocusReason) if the mouse click is within the
0061     // circle. Therefore, this class simply defaults to
0062     // Qt::FocusPolicy::TabFocus for QWidget::focusPolicy().
0063     setFocusPolicy(Qt::FocusPolicy::TabFocus);
0064 
0065     // Connections
0066     connect(&d_pointer->m_chromaHueImage, //
0067             &AsyncImageProvider<ChromaHueImageParameters>::interlacingPassCompleted, //
0068             this,
0069             &ChromaHueDiagram::callUpdate);
0070 
0071     // Initialize the color
0072     setCurrentColor(CielchD50Values::srgbVersatileInitialColor);
0073 }
0074 
0075 /** @brief Default destructor */
0076 ChromaHueDiagram::~ChromaHueDiagram() noexcept
0077 {
0078 }
0079 
0080 /** @brief Constructor
0081  *
0082  * @param backLink Pointer to the object from which <em>this</em> object
0083  *                 is the private implementation.
0084  * @param colorSpace The color space within which this widget
0085  *                   should operate. */
0086 ChromaHueDiagramPrivate::ChromaHueDiagramPrivate(ChromaHueDiagram *backLink, const QSharedPointer<PerceptualColor::RgbColorSpace> &colorSpace)
0087     : m_currentColor{0, 0, 0} // dummy value
0088     , m_wheelImage(colorSpace)
0089     , q_pointer(backLink)
0090 {
0091 }
0092 
0093 /** @brief React on a mouse press event.
0094  *
0095  * Reimplemented from base class.
0096  *
0097  * @internal
0098  * @post
0099  * - If the mouse is clicked with the circular diagram (inside or
0100  *   outside of the visible gamut), than this widget gets the focus
0101  *   and and @ref ChromaHueDiagramPrivate::m_isMouseEventActive is
0102  *   set to <tt>true</tt> to track mouse movements from now on.
0103  *   Reacts on all clicks (left, middle, right). If the mouse was
0104  *   within the gamut, the diagram’s handle is displaced there. If
0105  *   the mouse was outside the gamut, the diagram’s handle always stays
0106  *   within the gamut: The hue value is correctly retained, while the chroma
0107  *   value is the highest possible chroma within the gamut at this hue.
0108  * @endinternal
0109  *
0110  * @param event The corresponding mouse event */
0111 void ChromaHueDiagram::mousePressEvent(QMouseEvent *event)
0112 {
0113     // TODO Also accept out-of-gamut clicks when they are covered by the
0114     // current handle.
0115     const bool isWithinCircle = //
0116         d_pointer->isWidgetPixelPositionWithinMouseSensibleCircle(event->pos());
0117     if (isWithinCircle) {
0118         event->accept();
0119         // Mouse focus is handled manually because so we can accept
0120         // focus only on mouse clicks within the displayed gamut, while
0121         // rejecting focus otherwise. In the constructor, therefore
0122         // Qt::FocusPolicy::TabFocus is specified, so that manual handling
0123         // of mouse focus is up to this code here.
0124         setFocus(Qt::MouseFocusReason);
0125         // Enable mouse tracking from now on:
0126         d_pointer->m_isMouseEventActive = true;
0127         // As clicks are only accepted within the visible gamut, the mouse
0128         // cursor is made invisible. Its function is taken over by the
0129         // handle itself within the displayed gamut.
0130         setCursor(Qt::BlankCursor);
0131         // Set the color property
0132         d_pointer->setColorFromWidgetPixelPosition(event->pos());
0133         // Schedule a paint event, so that the wheel handle will show. It’s
0134         // not enough to hope setColorFromWidgetCoordinates() would do this,
0135         // because setColorFromWidgetCoordinates() would not update the
0136         // widget if the mouse click was done at the same position as the
0137         // current color handle.
0138         update();
0139     } else {
0140         // Make sure default behavior like drag-window in KDE’s
0141         // “Breeze” widget style works if this widget does not
0142         // actually react itself on a mouse event.
0143         event->ignore();
0144     }
0145 }
0146 
0147 /** @brief React on a mouse move event.
0148  *
0149  * Reimplemented from base class.
0150  *
0151  * @internal
0152  * @post Reacts only on mouse move events if
0153  * @ref ChromaHueDiagramPrivate::m_isMouseEventActive is <tt>true</tt>:
0154  * - If the mouse moves within the gamut, the diagram’s handle is displaced
0155  *   there. The mouse cursor is invisible; only the diagram’ handle is
0156  *   visible.
0157  * - If the mouse moves outside the gamut, the diagram’s handle always stays
0158  *   within the gamut: The hue value is correctly retained, while the chroma
0159  *   value is the highest possible chroma within the gamut at this hue.
0160  *   Both, the diagram’s handle <em>and</em> the mouse cursor are
0161  *   visible.
0162  * @endinternal
0163  *
0164  * @param event The corresponding mouse event */
0165 void ChromaHueDiagram::mouseMoveEvent(QMouseEvent *event)
0166 {
0167     if (d_pointer->m_isMouseEventActive) {
0168         event->accept();
0169         const cmsCIELab cielabD50 = //
0170             d_pointer->fromWidgetPixelPositionToLab(event->pos());
0171         const bool isWithinCircle = //
0172             d_pointer->isWidgetPixelPositionWithinMouseSensibleCircle( //
0173                 event->pos());
0174         if (isWithinCircle && d_pointer->m_rgbColorSpace->isCielabD50InGamut(cielabD50)) {
0175             setCursor(Qt::BlankCursor);
0176         } else {
0177             unsetCursor();
0178         }
0179         d_pointer->setColorFromWidgetPixelPosition(event->pos());
0180     } else {
0181         // Make sure default behavior like drag-window in KDE’s
0182         // Breeze widget style works.
0183         event->ignore();
0184     }
0185 }
0186 
0187 /** @brief React on a mouse release event.
0188  *
0189  * Reimplemented from base class. Reacts on all clicks (left, middle, right).
0190  *
0191  * @param event The corresponding mouse event
0192  *
0193  * @internal
0194  *
0195  * @post If @ref ChromaHueDiagramPrivate::m_isMouseEventActive is
0196  * <tt>true</tt> then:
0197  * - If the mouse is within the gamut, the diagram’s handle is displaced
0198  *   there.
0199  * - If the mouse moves outside the gamut, the diagram’s handle always stays
0200  *   within the gamut: The hue value is correctly retained, while the chroma
0201  *   value is the highest possible chroma within the gamut at this hue.
0202  * - The mouse cursor is made visible (if he wasn’t yet visible anyway).
0203  * - @ref ChromaHueDiagramPrivate::m_isMouseEventActive is set
0204  *   to <tt>false</tt>.
0205  *
0206  * @todo What if the widget displays a gamut that has no L*=0.1 because its
0207  * blackpoint is lighter.? Sacrificing chroma alone does not help? How to
0208  * react (for mouse input, keyboard input, but also API functions like
0209  * setColor()? */
0210 void ChromaHueDiagram::mouseReleaseEvent(QMouseEvent *event)
0211 {
0212     if (d_pointer->m_isMouseEventActive) {
0213         event->accept();
0214         unsetCursor();
0215         d_pointer->m_isMouseEventActive = false;
0216         d_pointer->setColorFromWidgetPixelPosition(event->pos());
0217         // Schedule a paint event, so that the wheel handle will be hidden.
0218         // It’s not enough to hope setColorFromWidgetCoordinates() would do
0219         // this, because setColorFromWidgetCoordinates() would not update the
0220         // widget if the mouse click was done at the same position as the
0221         // current color handle.
0222         update();
0223     } else {
0224         // Make sure default behavior like drag-window in KDE’s
0225         // Breeze widget style works
0226         event->ignore();
0227     }
0228 }
0229 
0230 /** @brief React on a mouse wheel event.
0231  *
0232  * Reimplemented from base class.
0233  *
0234  * Scrolling up raises the hue value, scrolling down lowers the hue value.
0235  * Of course, at the point at 0°/360° wrapping applies.
0236  *
0237  * @param event The corresponding mouse event */
0238 void ChromaHueDiagram::wheelEvent(QWheelEvent *event)
0239 {
0240     // Though QWheelEvent::position() returns a floating point
0241     // value, this value seems to corresponds to a pixel position
0242     // and not a coordinate point. Therefore, we convert to QPoint.
0243     const bool isWithinCircle = //
0244         d_pointer->isWidgetPixelPositionWithinMouseSensibleCircle( //
0245             event->position().toPoint());
0246     if (
0247         // Do nothing while a the mouse is clicked and the mouse movement is
0248         // tracked anyway because this would be confusing for the user.
0249         (!d_pointer->m_isMouseEventActive)
0250         // Only react on good old vertical wheels,
0251         // and not on horizontal wheels.
0252         && (event->angleDelta().y() != 0)
0253         // Only react on wheel events when then happen in the appropriate
0254         // area.
0255         // Though QWheelEvent::position() returns a floating point
0256         // value, this value seems to corresponds to a pixel position
0257         // and not a coordinate point. Therefore, we convert to QPoint.
0258         && isWithinCircle
0259         // then:
0260     ) {
0261         event->accept();
0262         // Calculate the new hue.
0263         // This may result in a hue smaller then 0° or bigger then 360°.
0264         // This should not make any problems.
0265         LchDouble newColor = d_pointer->m_currentColor;
0266         newColor.h += standardWheelStepCount(event) * singleStepHue;
0267         setCurrentColor( //
0268             d_pointer->m_rgbColorSpace->reduceCielchD50ChromaToFitIntoGamut(newColor));
0269     } else {
0270         event->ignore();
0271     }
0272 }
0273 
0274 /** @brief React on key press events.
0275  *
0276  * Reimplemented from base class.
0277  *
0278  * The keys do not react in form of up, down, left and right like in
0279  * Cartesian coordinate systems. The keys change radius and angel like
0280  * in polar coordinate systems, because our color model is also based
0281  * on a polar coordinate system.
0282  *
0283  * For chroma changes: Moves the handle as much as possible into the
0284  * desired direction as long as this is still in the gamut.
0285  * - Qt::Key_Up increments chroma a small step
0286  * - Qt::Key_Down decrements chroma a small step
0287  * - Qt::Key_PageUp increments chroma a big step
0288  * - Qt::Key_PageDown decrements chroma a big step
0289  *
0290  * For hue changes: If necessary, the chroma value is reduced to get an
0291  * in-gamut color with the new hue.
0292  * - Qt::Key_Left increments hue a small step
0293  * - Qt::Key_Right decrements hue a small step
0294  * - Qt::Key_Home increments hue a big step
0295  * - Qt::Key_End decrements hue a big step
0296  *
0297  * @param event the event
0298  *
0299  * @internal
0300  *
0301  * @todo Is this behavior really a good user experience? Or is it confusing
0302  * that left, right, up and down don’t do what was expected? What could be
0303  * more intuitive keys for changing radius and angle? At least the arrow keys
0304  * are likely that the user tries them out by trial-and-error. */
0305 void ChromaHueDiagram::keyPressEvent(QKeyEvent *event)
0306 {
0307     LchDouble newColor = currentColor();
0308     switch (event->key()) {
0309     case Qt::Key_Up:
0310         newColor.c += singleStepChroma;
0311         break;
0312     case Qt::Key_Down:
0313         newColor.c -= singleStepChroma;
0314         break;
0315     case Qt::Key_Left:
0316         newColor.h += singleStepHue;
0317         break;
0318     case Qt::Key_Right:
0319         newColor.h -= singleStepHue;
0320         break;
0321     case Qt::Key_PageUp:
0322         newColor.c += pageStepChroma;
0323         break;
0324     case Qt::Key_PageDown:
0325         newColor.c -= pageStepChroma;
0326         break;
0327     case Qt::Key_Home:
0328         newColor.h += pageStepHue;
0329         break;
0330     case Qt::Key_End:
0331         newColor.h -= pageStepHue;
0332         break;
0333     default:
0334         // Quote from Qt documentation:
0335         //
0336         //     “If you reimplement this handler, it is very important that
0337         //      you call the base class implementation if you do not act
0338         //      upon the key.
0339         //
0340         //      The default implementation closes popup widgets if the
0341         //      user presses the key sequence for QKeySequence::Cancel
0342         //      (typically the Escape key). Otherwise the event is
0343         //      ignored, so that the widget’s parent can interpret it.“
0344         QWidget::keyPressEvent(event);
0345         return;
0346     }
0347     // Here we reach only if the key has been recognized. If not, in the
0348     // default branch of the switch statement, we would have passed the
0349     // keyPressEvent yet to the parent and returned.
0350     if (newColor.c < 0) {
0351         // Do not allow negative chroma values.
0352         // (Doing so would be counter-intuitive.)
0353         newColor.c = 0;
0354     }
0355     // Move the value into gamut (if necessary):
0356     newColor = d_pointer->m_rgbColorSpace->reduceCielchD50ChromaToFitIntoGamut(newColor);
0357     // Apply the new value:
0358     setCurrentColor(newColor);
0359 }
0360 
0361 /** @brief Recommended size for the widget.
0362  *
0363  * Reimplemented from base class.
0364  *
0365  * @returns Recommended size for the widget.
0366  *
0367  * @sa @ref minimumSizeHint() */
0368 QSize ChromaHueDiagram::sizeHint() const
0369 {
0370     return minimumSizeHint() * scaleFromMinumumSizeHintToSizeHint;
0371 }
0372 
0373 /** @brief Recommended minimum size for the widget
0374  *
0375  * Reimplemented from base class.
0376  *
0377  * @returns Recommended minimum size for the widget.
0378  *
0379  * @sa @ref sizeHint() */
0380 QSize ChromaHueDiagram::minimumSizeHint() const
0381 {
0382     const int mySize =
0383         // Considering the gradient length two times, as the diagram
0384         // shows the center of the coordinate system in the middle,
0385         // and each side of the center should be well visible.
0386         2 * d_pointer->diagramBorder() + 2 * gradientMinimumLength();
0387     // Expand to the global minimum size for GUI elements
0388     return QSize(mySize, mySize);
0389 }
0390 
0391 // No documentation here (documentation of properties
0392 // and its getters are in the header)
0393 LchDouble ChromaHueDiagram::currentColor() const
0394 {
0395     return d_pointer->m_currentColor;
0396 }
0397 
0398 /** @brief Setter for the @ref currentColor property.
0399  *
0400  * @param newCurrentColor the new color */
0401 void ChromaHueDiagram::setCurrentColor(const LchDouble &newCurrentColor)
0402 {
0403     if (newCurrentColor.hasSameCoordinates(d_pointer->m_currentColor)) {
0404         return;
0405     }
0406 
0407     const LchDouble oldColor = d_pointer->m_currentColor;
0408 
0409     d_pointer->m_currentColor = newCurrentColor;
0410 
0411     // Update, if necessary, the diagram.
0412     if (d_pointer->m_currentColor.l != oldColor.l) {
0413         const qreal temp = qBound(static_cast<qreal>(0), //
0414                                   d_pointer->m_currentColor.l, //
0415                                   static_cast<qreal>(100));
0416         d_pointer->m_chromaHueImageParameters.lightness = temp;
0417         // TODO xxx Enable this line one the performance problem is solved.
0418         // This is meant to free memory in the cache if the widget is
0419         // not currently visible.
0420         // d_pointer->m_chromaHueImage.setImageParameters(d_pointer->m_chromaHueImageParameters);
0421     }
0422 
0423     // Schedule a paint event:
0424     update();
0425 
0426     // Emit notify signal
0427     Q_EMIT currentColorChanged(newCurrentColor);
0428 }
0429 
0430 /** @brief The point that is the center of the diagram coordinate system.
0431  *
0432  * @returns The point that is the center of the diagram coordinate system,
0433  * measured in <em>device-independent pixels</em> relative to the widget
0434  * coordinate system.
0435  *
0436  * @sa @ref diagramOffset provides a one-dimensional
0437  * representation of this very same fact. */
0438 QPointF ChromaHueDiagramPrivate::diagramCenter() const
0439 {
0440     const qreal tempOffset{diagramOffset()};
0441     return QPointF(tempOffset, tempOffset);
0442 }
0443 
0444 /** @brief The point that is the center of the diagram coordinate system.
0445  *
0446  * @returns The offset between the center of the widget coordinate system
0447  * and the center of the diagram coordinate system. The value is measured in
0448  * <em>device-independent pixels</em> relative to the widget’s coordinate
0449  * system. The value is identical for both, x axis and y axis.
0450  *
0451  * @sa @ref diagramCenter provides a two-dimensional
0452  * representation of this very same fact. */
0453 qreal ChromaHueDiagramPrivate::diagramOffset() const
0454 {
0455     return q_pointer->maximumWidgetSquareSize() / 2.0;
0456 }
0457 
0458 /** @brief React on a resize event.
0459  *
0460  * Reimplemented from base class.
0461  *
0462  * @param event The corresponding resize event */
0463 void ChromaHueDiagram::resizeEvent(QResizeEvent *event)
0464 {
0465     Q_UNUSED(event)
0466 
0467     // Update the widget content
0468     d_pointer->m_wheelImage.setImageSize(maximumPhysicalSquareSize());
0469     d_pointer->m_chromaHueImageParameters.imageSizePhysical =
0470         // Guaranteed to be ≥ 0:
0471         maximumPhysicalSquareSize();
0472     // TODO xxx Enable this line once the performance problem is solved.
0473     // This is meant to free memory in the cache if the widget is
0474     // not currently visible.
0475     // d_pointer->m_chromaHueImage.setImageParameters(d_pointer->m_chromaHueImageParameters);
0476 
0477     // As Qt documentation says:
0478     //     “The widget will be erased and receive a paint event
0479     //      immediately after processing the resize event. No
0480     //      drawing need be (or should be) done inside this handler.”
0481 }
0482 
0483 /** @brief  Widget coordinate point corresponding to the
0484  * @ref ChromaHueDiagram::currentColor property
0485  *
0486  * @returns Widget coordinate point corresponding to the
0487  * @ref ChromaHueDiagram::currentColor property. This is the position
0488  * of @ref ChromaHueDiagram::currentColor in the gamut diagram, but measured
0489  * and expressed as widget coordinate point.
0490  *
0491  * @sa @ref ChromaHueMeasurement "Measurement details" */
0492 QPointF ChromaHueDiagramPrivate::widgetCoordinatesFromCurrentColor() const
0493 {
0494     const qreal scaleFactor = //
0495         (q_pointer->maximumWidgetSquareSize() - 2.0 * diagramBorder()) //
0496         / (2.0 * m_rgbColorSpace->profileMaximumCielchD50Chroma());
0497     QPointF currentColor = //
0498         PolarPointF(m_currentColor.c, m_currentColor.h).toCartesian();
0499     return QPointF(
0500         // x:
0501         currentColor.x() * scaleFactor + diagramOffset(),
0502         // y:
0503         diagramOffset() - currentColor.y() * scaleFactor);
0504 }
0505 
0506 /** @brief Converts widget pixel positions to Lab coordinates
0507  *
0508  * @param position The position of a pixel of the widget coordinate
0509  * system. The given value  does not necessarily need to
0510  * be within the actual displayed diagram or even the gamut itself. It
0511  * might even be negative.
0512  *
0513  * @returns The Lab coordinates of the currently displayed gamut diagram
0514  * for the (center of the) given pixel position.
0515  * @sa @ref ChromaHueMeasurement "Measurement details" */
0516 cmsCIELab ChromaHueDiagramPrivate::fromWidgetPixelPositionToLab(const QPoint position) const
0517 {
0518     const qreal scaleFactor = //
0519         (2.0 * m_rgbColorSpace->profileMaximumCielchD50Chroma()) //
0520         / (q_pointer->maximumWidgetSquareSize() - 2.0 * diagramBorder());
0521     // The pixel at position 0 0 has its top left border at position 0 0
0522     // and its bottom right border at position 1 1 and its center at
0523     // position 0.5 0.5. Its the center of the pixel that is our reference
0524     // for conversion, therefore we have to ship by 0.5 widget pixels.
0525     constexpr qreal pixelValueShift = 0.5;
0526     cmsCIELab lab;
0527     lab.L = m_currentColor.l;
0528     lab.a = //
0529         (position.x() + pixelValueShift - diagramOffset()) * scaleFactor;
0530     lab.b = //
0531         (position.y() + pixelValueShift - diagramOffset()) * scaleFactor * (-1);
0532     return lab;
0533 }
0534 
0535 /** @brief Sets the @ref ChromaHueDiagram::currentColor property corresponding
0536  * to a given widget pixel position.
0537  *
0538  * @param position The position of a pixel of the widget coordinate
0539  * system. The given value  does not necessarily need to be within the
0540  * actual displayed diagram or even the gamut itself. It might even be
0541  * negative.
0542  *
0543  * @post If the <em>center</em> of the widget pixel is within the represented
0544  * gamut, then the @ref ChromaHueDiagram::currentColor property is
0545  * set correspondingly. If the center of the widget pixel is outside
0546  * the gamut, then the chroma value is reduced (while the hue is
0547  * maintained) until arriving at the outer shell of the gamut; the
0548  * @ref ChromaHueDiagram::currentColor property is than set to this adapted
0549  * color.
0550  *
0551  * @note This function works independently of the actually displayed color
0552  * gamut diagram. So if parts of the gamut (the high chroma parts) are cut
0553  * off in the visible diagram, this does not influence this function.
0554  *
0555  * @sa @ref ChromaHueMeasurement "Measurement details"
0556  *
0557  * @internal
0558  *
0559  * @todo What when the mouse goes outside the gray circle, but more gamut
0560  * is available outside (because @ref RgbColorSpace::profileMaximumCielchD50Chroma()
0561  * was chosen too small)? For consistency, the handle of the diagram should
0562  * stay within the gray circle, and this should be interpreted also actually
0563  * as the value at the position of the handle. */
0564 void ChromaHueDiagramPrivate::setColorFromWidgetPixelPosition(const QPoint position)
0565 {
0566     const cmsCIELab lab = fromWidgetPixelPositionToLab(position);
0567     const auto myColor = //
0568         m_rgbColorSpace->reduceCielchD50ChromaToFitIntoGamut(toLchDouble(lab));
0569     q_pointer->setCurrentColor(myColor);
0570 }
0571 
0572 /** @brief Tests if a widget pixel position is within the mouse sensible circle.
0573  *
0574  * The mouse sensible circle contains the inner gray circle (on which the
0575  * gamut diagram is painted).
0576  * @param position The position of a pixel of the widget coordinate
0577  * system. The given value  does not necessarily need to be within the
0578  * actual displayed diagram or even the gamut itself. It might even be
0579  * negative.
0580  * @returns <tt>true</tt> if the (center of the) pixel at the given position
0581  * is within the circle, <tt>false</tt> otherwise. */
0582 bool ChromaHueDiagramPrivate::isWidgetPixelPositionWithinMouseSensibleCircle(const QPoint position) const
0583 {
0584     const qreal radius = PolarPointF(
0585                              // Position relative to
0586                              // polar coordinate system center:
0587                              position
0588                              - diagramCenter()
0589                              // Apply the offset between
0590                              // - a pixel position on one hand and
0591                              // - a coordinate point in the middle of this very
0592                              //   same pixel on the other:
0593                              + QPointF(0.5, 0.5))
0594                              .radius();
0595 
0596     const qreal diagramCircleRadius = //
0597         q_pointer->maximumWidgetSquareSize() / 2.0 - diagramBorder();
0598 
0599     return (radius <= diagramCircleRadius);
0600 }
0601 
0602 /** @brief Paint the widget.
0603  *
0604  * Reimplemented from base class.
0605  *
0606  * @param event the paint event
0607  *
0608  * @internal
0609  *
0610  * @post
0611  * - Paints the widget. Takes the existing
0612  *   @ref ChromaHueDiagramPrivate::m_chromaHueImage and
0613  *   @ref ChromaHueDiagramPrivate::m_wheelImage and paints them on the widget.
0614  *   If their cache is up-to-date, this operation is fast, otherwise
0615  *   considerably slower.
0616  * - Paints the handles.
0617  * - If the widget has focus, it also paints the focus indicator. As the
0618  *   widget is round, we cannot use <tt>QStyle::PE_FrameFocusRect</tt> for
0619  *   painting this, neither does <tt>QStyle</tt> provide build-in support
0620  *   for round widgets. Therefore, we draw the focus indicator ourself,
0621  *   which means its form is not controlled by <tt>QStyle</tt>.
0622  *
0623  * @todo Show the indicator on the color wheel not only while a mouse button
0624  * is pressed, but also while a keyboard button is pressed.
0625  *
0626  * @todo What when @ref ChromaHueDiagramPrivate::m_currentColor has a valid
0627  * in-gamut color, but this color is out of the <em>displayed</em> diagram?
0628  * How to handle that? */
0629 void ChromaHueDiagram::paintEvent(QPaintEvent *event)
0630 {
0631     Q_UNUSED(event)
0632 
0633     // We do not paint directly on the widget, but on a QImage buffer first:
0634     // Render anti-aliased looks better. But as Qt documentation says:
0635     //
0636     //      “Renderhints are used to specify flags to QPainter that may or
0637     //       may not be respected by any given engine.”
0638     //
0639     // Painting here directly on the widget might lead to different
0640     // anti-aliasing results depending on the underlying window system. This
0641     // is especially problematic as anti-aliasing might shift or not a pixel
0642     // to the left or to the right. So we paint on a QImage first. As QImage
0643     // (at difference to QPixmap and a QWidget) is independent of native
0644     // platform rendering, it guarantees identical anti-aliasing results on
0645     // all platforms. Here the quote from QPainter class documentation:
0646     //
0647     //      “To get the optimal rendering result using QPainter, you should
0648     //       use the platform independent QImage as paint device; i.e. using
0649     //       QImage will ensure that the result has an identical pixel
0650     //       representation on any platform.”
0651     QImage buffer(maximumPhysicalSquareSize(), // width
0652                   maximumPhysicalSquareSize(), // height
0653                   QImage::Format_ARGB32_Premultiplied // format
0654     );
0655     buffer.fill(Qt::transparent);
0656     buffer.setDevicePixelRatio(devicePixelRatioF());
0657 
0658     // Other initialization
0659     QPainter bufferPainter(&buffer);
0660     QPen pen;
0661     const QBrush transparentBrush{Qt::transparent};
0662     // Set color of the handle: Black or white, depending on the lightness of
0663     // the currently selected color.
0664     const QColor handleColor //
0665         {handleColorFromBackgroundLightness(d_pointer->m_currentColor.l)};
0666     const QPointF widgetCoordinatesFromCurrentColor //
0667         {d_pointer->widgetCoordinatesFromCurrentColor()};
0668 
0669     // Paint the gamut itself as available in the cache.
0670     bufferPainter.setRenderHint(QPainter::Antialiasing, false);
0671     // As devicePixelRatioF() might have changed, we make sure everything
0672     // that might depend on devicePixelRatioF() is updated before painting.
0673     d_pointer->m_chromaHueImageParameters.borderPhysical =
0674         // TODO It might be useful to reduce this border to (near to) zero, and
0675         // than paint with an offset (if this is possible with drawEllipse?).
0676         // Then the memory consumption would be reduced somewhat.
0677         d_pointer->diagramBorder() * devicePixelRatioF();
0678     d_pointer->m_chromaHueImageParameters.imageSizePhysical =
0679         // Guaranteed to be ≥ 0:
0680         maximumPhysicalSquareSize();
0681     const qreal temp = qBound(static_cast<qreal>(0), //
0682                               d_pointer->m_currentColor.l, //
0683                               static_cast<qreal>(100));
0684     d_pointer->m_chromaHueImageParameters.lightness = temp;
0685     d_pointer->m_chromaHueImageParameters.devicePixelRatioF = //
0686         devicePixelRatioF();
0687     d_pointer->m_chromaHueImageParameters.rgbColorSpace = //
0688         d_pointer->m_rgbColorSpace;
0689     d_pointer->m_chromaHueImage.setImageParameters( //
0690         d_pointer->m_chromaHueImageParameters);
0691     d_pointer->m_chromaHueImage.refreshAsync();
0692     const qreal circleRadius = //
0693         (maximumWidgetSquareSize() - 2 * d_pointer->diagramBorder()) / 2.0;
0694     bufferPainter.setRenderHint(QPainter::Antialiasing, true);
0695     bufferPainter.setPen(QPen(Qt::NoPen));
0696     bufferPainter.setBrush(d_pointer->m_chromaHueImage.getCache());
0697     bufferPainter.drawEllipse(
0698         // center:
0699         QPointF(maximumWidgetSquareSize() / 2.0, //
0700                 maximumWidgetSquareSize() / 2.0),
0701         // width:
0702         circleRadius,
0703         // height:
0704         circleRadius);
0705 
0706     // Paint a color wheel around
0707     bufferPainter.setRenderHint(QPainter::Antialiasing, false);
0708     // As devicePixelRatioF() might have changed, we make sure everything
0709     // that might depend on devicePixelRatioF() is updated before painting.
0710     d_pointer->m_wheelImage.setBorder( //
0711         spaceForFocusIndicator() * devicePixelRatioF());
0712     d_pointer->m_wheelImage.setDevicePixelRatioF(devicePixelRatioF());
0713     d_pointer->m_wheelImage.setImageSize(maximumPhysicalSquareSize());
0714     d_pointer->m_wheelImage.setWheelThickness( //
0715         gradientThickness() * devicePixelRatioF());
0716     bufferPainter.drawImage( //
0717         QPoint(0, 0), // position of the image
0718         d_pointer->m_wheelImage.getImage() // the image itself
0719     );
0720 
0721     // Paint a handle on the color wheel (only if a mouse event is
0722     // currently active).
0723     if (d_pointer->m_isMouseEventActive) {
0724         // The radius of the outer border of the color wheel
0725         const qreal radius = //
0726             maximumWidgetSquareSize() / 2.0 - spaceForFocusIndicator();
0727         // Get widget coordinate point for the handle
0728         QPointF myHandleInner = PolarPointF(radius - gradientThickness(), //
0729                                             d_pointer->m_currentColor.h)
0730                                     .toCartesian();
0731         myHandleInner.ry() *= -1; // Transform to Widget coordinate points
0732         myHandleInner += d_pointer->diagramCenter();
0733         QPointF myHandleOuter = //
0734             PolarPointF(radius, d_pointer->m_currentColor.h).toCartesian();
0735         myHandleOuter.ry() *= -1; // Transform to Widget coordinate points
0736         myHandleOuter += d_pointer->diagramCenter();
0737         // Draw the line
0738         pen = QPen();
0739         pen.setWidth(handleOutlineThickness());
0740         // TODO Instead of Qt::FlatCap, we could really paint a handle
0741         // that does match perfectly the round inner and outer border
0742         // of the wheel. But: Is it really worth the complexity?
0743         pen.setCapStyle(Qt::FlatCap);
0744         pen.setColor(handleColor);
0745         bufferPainter.setPen(pen);
0746         bufferPainter.setRenderHint(QPainter::Antialiasing, true);
0747         bufferPainter.drawLine(myHandleInner, myHandleOuter);
0748     }
0749 
0750     // Paint the handle within the gamut
0751     bufferPainter.setRenderHint(QPainter::Antialiasing, true);
0752     pen = QPen();
0753     pen.setWidth(handleOutlineThickness());
0754     pen.setColor(handleColor);
0755     pen.setCapStyle(Qt::RoundCap);
0756     bufferPainter.setPen(pen);
0757     bufferPainter.setBrush(transparentBrush);
0758     bufferPainter.drawEllipse(widgetCoordinatesFromCurrentColor, // center
0759                               handleRadius(), // x radius
0760                               handleRadius() // y radius
0761     );
0762     const auto diagramOffset = d_pointer->diagramOffset();
0763     const QPointF diagramCartesianCoordinatesFromCurrentColor(
0764         // x:
0765         widgetCoordinatesFromCurrentColor.x() - diagramOffset,
0766         // y:
0767         (widgetCoordinatesFromCurrentColor.y() - diagramOffset) * (-1));
0768     PolarPointF diagramPolarCoordinatesFromCurrentColor( //
0769         diagramCartesianCoordinatesFromCurrentColor);
0770     // lineRadius will be a point at the middle of the line thickness
0771     // of the circular handle.
0772     qreal lineRadius = //
0773         diagramPolarCoordinatesFromCurrentColor.radius() - handleRadius();
0774     if (lineRadius > 0) {
0775         QPointF lineEndWidgetCoordinates = //
0776             PolarPointF(
0777                 // radius:
0778                 lineRadius,
0779                 // angle:
0780                 diagramPolarCoordinatesFromCurrentColor.angleDegree() //
0781                 )
0782                 .toCartesian();
0783         lineEndWidgetCoordinates.ry() *= (-1);
0784         lineEndWidgetCoordinates += d_pointer->diagramCenter();
0785         bufferPainter.drawLine(
0786             // point 1 (center of the diagram):
0787             d_pointer->diagramCenter(),
0788             // point 2:
0789             lineEndWidgetCoordinates);
0790     }
0791 
0792     // Paint a focus indicator.
0793     //
0794     // We could paint a focus indicator (round or rectangular) around the
0795     // handle. Depending on the currently selected hue for the diagram, it
0796     // looks ugly because the colors of focus indicator and diagram do not
0797     // harmonize, or it is mostly invisible if the colors are similar. So
0798     // this approach does not work well.
0799     //
0800     // It seems better to paint a focus indicator for the whole widget.
0801     // We could use the style primitives to paint a rectangular focus
0802     // indicator around the whole widget:
0803     //
0804     // style()->drawPrimitive(
0805     //     QStyle::PE_FrameFocusRect,
0806     //     &option,
0807     //     &painter,
0808     //     this
0809     // );
0810     //
0811     // However, this does not work well because this widget does not have a
0812     // rectangular form.
0813     //
0814     // Then we have to design the line that we want to display. It is better
0815     // to do that ourselves instead of relying on generic QStyle::PE_Frame or
0816     // similar solutions as their result seems to be quite unpredictable
0817     // across various styles. So we use handleOutlineThickness as line width
0818     // and paint it at the left-most possible position. As m_wheelBorder
0819     // accommodates also to handleRadius(), the distance of the focus line to
0820     // the real widget also does, which looks nice.
0821     if (hasFocus()) {
0822         bufferPainter.setRenderHint(QPainter::Antialiasing, true);
0823         pen = QPen();
0824         pen.setWidth(handleOutlineThickness());
0825         pen.setColor(focusIndicatorColor());
0826         bufferPainter.setPen(pen);
0827         bufferPainter.setBrush(transparentBrush);
0828         bufferPainter.drawEllipse(
0829             // center:
0830             d_pointer->diagramCenter(),
0831             // x radius:
0832             diagramOffset - handleOutlineThickness() / 2.0,
0833             // y radius:
0834             diagramOffset - handleOutlineThickness() / 2.0);
0835     }
0836 
0837     // Paint the buffer to the actual widget
0838     QPainter widgetPainter(this);
0839     widgetPainter.setRenderHint(QPainter::Antialiasing, false);
0840     widgetPainter.drawImage(QPoint(0, 0), buffer);
0841 }
0842 
0843 /** @brief The border around the round diagram.
0844  *
0845  * Measured in <em>device-independent pixels</em>.
0846  *
0847  * @returns The border. This is the space where the surrounding color wheel
0848  * and the focus indicator are painted. */
0849 int ChromaHueDiagramPrivate::diagramBorder() const
0850 {
0851     return
0852         // The space outside the wheel:
0853         q_pointer->spaceForFocusIndicator()
0854         // Add space for the wheel itself:
0855         + q_pointer->gradientThickness()
0856         // Add extra space between wheel and diagram:
0857         + 2 * q_pointer->handleOutlineThickness();
0858 }
0859 
0860 } // namespace PerceptualColor