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