Perceptual Color

chromahuediagram.cpp
1// SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
2// SPDX-License-Identifier: BSD-2-Clause OR MIT
3
4// Own headers
5// First the interface, which forces the header to be self-contained.
6#include "chromahuediagram.h"
7// Second, the private implementation.
8#include "chromahuediagram_p.h" // IWYU pragma: associated
9
10#include "abstractdiagram.h"
11#include "asyncimageprovider.h"
12#include "chromahueimageparameters.h"
13#include "cielchd50values.h"
14#include "colorwheelimage.h"
15#include "constpropagatingrawpointer.h"
16#include "constpropagatinguniquepointer.h"
17#include "helper.h"
18#include "helperconstants.h"
19#include "helperconversion.h"
20#include "lchdouble.h"
21#include "polarpointf.h"
22#include "rgbcolorspace.h"
23#include <lcms2.h>
24#include <qbrush.h>
25#include <qcolor.h>
26#include <qevent.h>
27#include <qimage.h>
28#include <qnamespace.h>
29#include <qpainter.h>
30#include <qpen.h>
31#include <qpoint.h>
32#include <qsharedpointer.h>
33#include <qwidget.h>
34
35namespace PerceptualColor
36{
37/** @brief The constructor.
38 * @param colorSpace The color space within which this widget should operate.
39 * Can be created with @ref RgbColorSpaceFactory.
40 * @param parent The widget’s parent widget. This parameter will be passed
41 * to the base class’s constructor. */
43 : AbstractDiagram(parent)
44 , d_pointer(new ChromaHueDiagramPrivate(this, colorSpace))
45{
46 // Setup LittleCMS. This is the first thing to do, because other
47 // operations rely on a working LittleCMS.
48 d_pointer->m_rgbColorSpace = colorSpace;
49
50 // Set focus policy
51 // In Qt, usually focus (QWidget::hasFocus()) by mouse click is either
52 // not accepted at all or accepted always for the hole rectangular
53 // widget, depending on QWidget::focusPolicy(). This is not convenient
54 // and intuitive for big, circular-shaped widgets like this one. It
55 // would be nicer if the focus would only be accepted by mouse clicks
56 // <em>within the circle itself</em>. Qt does not provide a build-in
57 // way to do this. But a workaround to implement this behavior is
58 // possible: Set QWidget::focusPolicy() to <em>not</em> accept focus
59 // by mouse click. Then, reimplement mousePressEvent() and call
60 // setFocus(Qt::MouseFocusReason) if the mouse click is within the
61 // circle. Therefore, this class simply defaults to
62 // Qt::FocusPolicy::TabFocus for QWidget::focusPolicy().
63 setFocusPolicy(Qt::FocusPolicy::TabFocus);
64
65 // Connections
66 connect(&d_pointer->m_chromaHueImage, //
67 &AsyncImageProvider<ChromaHueImageParameters>::interlacingPassCompleted, //
68 this,
70
71 // Initialize the color
72 setCurrentColor(CielchD50Values::srgbVersatileInitialColor);
73}
74
75/** @brief Default destructor */
79
80/** @brief Constructor
81 *
82 * @param backLink Pointer to the object from which <em>this</em> object
83 * is the private implementation.
84 * @param colorSpace The color space within which this widget
85 * should operate. */
86ChromaHueDiagramPrivate::ChromaHueDiagramPrivate(ChromaHueDiagram *backLink, const QSharedPointer<PerceptualColor::RgbColorSpace> &colorSpace)
87 : m_currentColor{0, 0, 0} // dummy value
88 , m_wheelImage(colorSpace)
89 , q_pointer(backLink)
90{
91}
92
93/** @brief React on a mouse press event.
94 *
95 * Reimplemented from base class.
96 *
97 * @internal
98 * @post
99 * - If the mouse is clicked with the circular diagram (inside or
100 * outside of the visible gamut), than this widget gets the focus
101 * and and @ref ChromaHueDiagramPrivate::m_isMouseEventActive is
102 * set to <tt>true</tt> to track mouse movements from now on.
103 * Reacts on all clicks (left, middle, right). If the mouse was
104 * within the gamut, the diagram’s handle is displaced there. If
105 * the mouse was outside the gamut, the diagram’s handle always stays
106 * within the gamut: The hue value is correctly retained, while the chroma
107 * value is the highest possible chroma within the gamut at this hue.
108 * @endinternal
109 *
110 * @param event The corresponding mouse event */
112{
113 // TODO Also accept out-of-gamut clicks when they are covered by the
114 // current handle.
115 const bool isWithinCircle = //
116 d_pointer->isWidgetPixelPositionWithinMouseSensibleCircle(event->pos());
117 if (isWithinCircle) {
118 event->accept();
119 // Mouse focus is handled manually because so we can accept
120 // focus only on mouse clicks within the displayed gamut, while
121 // rejecting focus otherwise. In the constructor, therefore
122 // Qt::FocusPolicy::TabFocus is specified, so that manual handling
123 // of mouse focus is up to this code here.
125 // Enable mouse tracking from now on:
126 d_pointer->m_isMouseEventActive = true;
127 // As clicks are only accepted within the visible gamut, the mouse
128 // cursor is made invisible. Its function is taken over by the
129 // handle itself within the displayed gamut.
131 // Set the color property
132 d_pointer->setColorFromWidgetPixelPosition(event->pos());
133 // Schedule a paint event, so that the wheel handle will show. It’s
134 // not enough to hope setColorFromWidgetCoordinates() would do this,
135 // because setColorFromWidgetCoordinates() would not update the
136 // widget if the mouse click was done at the same position as the
137 // current color handle.
138 update();
139 } else {
140 // Make sure default behavior like drag-window in KDE’s
141 // “Breeze” widget style works if this widget does not
142 // actually react itself on a mouse event.
143 event->ignore();
144 }
145}
146
147/** @brief React on a mouse move event.
148 *
149 * Reimplemented from base class.
150 *
151 * @internal
152 * @post Reacts only on mouse move events if
153 * @ref ChromaHueDiagramPrivate::m_isMouseEventActive is <tt>true</tt>:
154 * - If the mouse moves within the gamut, the diagram’s handle is displaced
155 * there. The mouse cursor is invisible; only the diagram’ handle is
156 * visible.
157 * - If the mouse moves outside the gamut, the diagram’s handle always stays
158 * within the gamut: The hue value is correctly retained, while the chroma
159 * value is the highest possible chroma within the gamut at this hue.
160 * Both, the diagram’s handle <em>and</em> the mouse cursor are
161 * visible.
162 * @endinternal
163 *
164 * @param event The corresponding mouse event */
166{
167 if (d_pointer->m_isMouseEventActive) {
168 event->accept();
169 const cmsCIELab cielabD50 = //
170 d_pointer->fromWidgetPixelPositionToLab(event->pos());
171 const bool isWithinCircle = //
172 d_pointer->isWidgetPixelPositionWithinMouseSensibleCircle( //
173 event->pos());
174 if (isWithinCircle && d_pointer->m_rgbColorSpace->isCielabD50InGamut(cielabD50)) {
176 } else {
177 unsetCursor();
178 }
179 d_pointer->setColorFromWidgetPixelPosition(event->pos());
180 } else {
181 // Make sure default behavior like drag-window in KDE’s
182 // Breeze widget style works.
183 event->ignore();
184 }
185}
186
187/** @brief React on a mouse release event.
188 *
189 * Reimplemented from base class. Reacts on all clicks (left, middle, right).
190 *
191 * @param event The corresponding mouse event
192 *
193 * @internal
194 *
195 * @post If @ref ChromaHueDiagramPrivate::m_isMouseEventActive is
196 * <tt>true</tt> then:
197 * - If the mouse is within the gamut, the diagram’s handle is displaced
198 * there.
199 * - If the mouse moves outside the gamut, the diagram’s handle always stays
200 * within the gamut: The hue value is correctly retained, while the chroma
201 * value is the highest possible chroma within the gamut at this hue.
202 * - The mouse cursor is made visible (if he wasn’t yet visible anyway).
203 * - @ref ChromaHueDiagramPrivate::m_isMouseEventActive is set
204 * to <tt>false</tt>.
205 *
206 * @todo What if the widget displays a gamut that has no L*=0.1 because its
207 * blackpoint is lighter.? Sacrificing chroma alone does not help? How to
208 * react (for mouse input, keyboard input, but also API functions like
209 * setColor()? */
211{
212 if (d_pointer->m_isMouseEventActive) {
213 event->accept();
214 unsetCursor();
215 d_pointer->m_isMouseEventActive = false;
216 d_pointer->setColorFromWidgetPixelPosition(event->pos());
217 // Schedule a paint event, so that the wheel handle will be hidden.
218 // It’s not enough to hope setColorFromWidgetCoordinates() would do
219 // this, because setColorFromWidgetCoordinates() would not update the
220 // widget if the mouse click was done at the same position as the
221 // current color handle.
222 update();
223 } else {
224 // Make sure default behavior like drag-window in KDE’s
225 // Breeze widget style works
226 event->ignore();
227 }
228}
229
230/** @brief React on a mouse wheel event.
231 *
232 * Reimplemented from base class.
233 *
234 * Scrolling up raises the hue value, scrolling down lowers the hue value.
235 * Of course, at the point at 0°/360° wrapping applies.
236 *
237 * @param event The corresponding mouse event */
239{
240 // Though QWheelEvent::position() returns a floating point
241 // value, this value seems to corresponds to a pixel position
242 // and not a coordinate point. Therefore, we convert to QPoint.
243 const bool isWithinCircle = //
244 d_pointer->isWidgetPixelPositionWithinMouseSensibleCircle( //
245 event->position().toPoint());
246 if (
247 // Do nothing while a the mouse is clicked and the mouse movement is
248 // tracked anyway because this would be confusing for the user.
249 (!d_pointer->m_isMouseEventActive)
250 // Only react on good old vertical wheels,
251 // and not on horizontal wheels.
252 && (event->angleDelta().y() != 0)
253 // Only react on wheel events when then happen in the appropriate
254 // area.
255 // Though QWheelEvent::position() returns a floating point
256 // value, this value seems to corresponds to a pixel position
257 // and not a coordinate point. Therefore, we convert to QPoint.
258 && isWithinCircle
259 // then:
260 ) {
261 event->accept();
262 // Calculate the new hue.
263 // This may result in a hue smaller then 0° or bigger then 360°.
264 // This should not make any problems.
265 LchDouble newColor = d_pointer->m_currentColor;
266 newColor.h += standardWheelStepCount(event) * singleStepHue;
268 d_pointer->m_rgbColorSpace->reduceCielchD50ChromaToFitIntoGamut(newColor));
269 } else {
270 event->ignore();
271 }
272}
273
274/** @brief React on key press events.
275 *
276 * Reimplemented from base class.
277 *
278 * The keys do not react in form of up, down, left and right like in
279 * Cartesian coordinate systems. The keys change radius and angel like
280 * in polar coordinate systems, because our color model is also based
281 * on a polar coordinate system.
282 *
283 * For chroma changes: Moves the handle as much as possible into the
284 * desired direction as long as this is still in the gamut.
285 * - Qt::Key_Up increments chroma a small step
286 * - Qt::Key_Down decrements chroma a small step
287 * - Qt::Key_PageUp increments chroma a big step
288 * - Qt::Key_PageDown decrements chroma a big step
289 *
290 * For hue changes: If necessary, the chroma value is reduced to get an
291 * in-gamut color with the new hue.
292 * - Qt::Key_Left increments hue a small step
293 * - Qt::Key_Right decrements hue a small step
294 * - Qt::Key_Home increments hue a big step
295 * - Qt::Key_End decrements hue a big step
296 *
297 * @param event the event
298 *
299 * @internal
300 *
301 * @todo Is this behavior really a good user experience? Or is it confusing
302 * that left, right, up and down don’t do what was expected? What could be
303 * more intuitive keys for changing radius and angle? At least the arrow keys
304 * are likely that the user tries them out by trial-and-error. */
306{
307 LchDouble newColor = currentColor();
308 switch (event->key()) {
309 case Qt::Key_Up:
310 newColor.c += singleStepChroma;
311 break;
312 case Qt::Key_Down:
313 newColor.c -= singleStepChroma;
314 break;
315 case Qt::Key_Left:
316 newColor.h += singleStepHue;
317 break;
318 case Qt::Key_Right:
319 newColor.h -= singleStepHue;
320 break;
321 case Qt::Key_PageUp:
322 newColor.c += pageStepChroma;
323 break;
324 case Qt::Key_PageDown:
325 newColor.c -= pageStepChroma;
326 break;
327 case Qt::Key_Home:
328 newColor.h += pageStepHue;
329 break;
330 case Qt::Key_End:
331 newColor.h -= pageStepHue;
332 break;
333 default:
334 // Quote from Qt documentation:
335 //
336 // “If you reimplement this handler, it is very important that
337 // you call the base class implementation if you do not act
338 // upon the key.
339 //
340 // The default implementation closes popup widgets if the
341 // user presses the key sequence for QKeySequence::Cancel
342 // (typically the Escape key). Otherwise the event is
343 // ignored, so that the widget’s parent can interpret it.“
345 return;
346 }
347 // Here we reach only if the key has been recognized. If not, in the
348 // default branch of the switch statement, we would have passed the
349 // keyPressEvent yet to the parent and returned.
350 if (newColor.c < 0) {
351 // Do not allow negative chroma values.
352 // (Doing so would be counter-intuitive.)
353 newColor.c = 0;
354 }
355 // Move the value into gamut (if necessary):
356 newColor = d_pointer->m_rgbColorSpace->reduceCielchD50ChromaToFitIntoGamut(newColor);
357 // Apply the new value:
358 setCurrentColor(newColor);
359}
360
361/** @brief Recommended size for the widget.
362 *
363 * Reimplemented from base class.
364 *
365 * @returns Recommended size for the widget.
366 *
367 * @sa @ref minimumSizeHint() */
369{
370 return minimumSizeHint() * scaleFromMinumumSizeHintToSizeHint;
371}
372
373/** @brief Recommended minimum size for the widget
374 *
375 * Reimplemented from base class.
376 *
377 * @returns Recommended minimum size for the widget.
378 *
379 * @sa @ref sizeHint() */
381{
382 const int mySize =
383 // Considering the gradient length two times, as the diagram
384 // shows the center of the coordinate system in the middle,
385 // and each side of the center should be well visible.
386 2 * d_pointer->diagramBorder() + 2 * gradientMinimumLength();
387 // Expand to the global minimum size for GUI elements
388 return QSize(mySize, mySize);
389}
390
391// No documentation here (documentation of properties
392// and its getters are in the header)
394{
395 return d_pointer->m_currentColor;
396}
397
398/** @brief Setter for the @ref currentColor property.
399 *
400 * @param newCurrentColor the new color */
402{
403 if (newCurrentColor.hasSameCoordinates(d_pointer->m_currentColor)) {
404 return;
405 }
406
407 const LchDouble oldColor = d_pointer->m_currentColor;
408
409 d_pointer->m_currentColor = newCurrentColor;
410
411 // Update, if necessary, the diagram.
412 if (d_pointer->m_currentColor.l != oldColor.l) {
413 const qreal temp = qBound(static_cast<qreal>(0), //
414 d_pointer->m_currentColor.l, //
415 static_cast<qreal>(100));
416 d_pointer->m_chromaHueImageParameters.lightness = temp;
417 // TODO xxx Enable this line one the performance problem is solved.
418 // This is meant to free memory in the cache if the widget is
419 // not currently visible.
420 // d_pointer->m_chromaHueImage.setImageParameters(d_pointer->m_chromaHueImageParameters);
421 }
422
423 // Schedule a paint event:
424 update();
425
426 // Emit notify signal
427 Q_EMIT currentColorChanged(newCurrentColor);
428}
429
430/** @brief The point that is the center of the diagram coordinate system.
431 *
432 * @returns The point that is the center of the diagram coordinate system,
433 * measured in <em>device-independent pixels</em> relative to the widget
434 * coordinate system.
435 *
436 * @sa @ref diagramOffset provides a one-dimensional
437 * representation of this very same fact. */
438QPointF ChromaHueDiagramPrivate::diagramCenter() const
439{
440 const qreal tempOffset{diagramOffset()};
441 return QPointF(tempOffset, tempOffset);
442}
443
444/** @brief The point that is the center of the diagram coordinate system.
445 *
446 * @returns The offset between the center of the widget coordinate system
447 * and the center of the diagram coordinate system. The value is measured in
448 * <em>device-independent pixels</em> relative to the widget’s coordinate
449 * system. The value is identical for both, x axis and y axis.
450 *
451 * @sa @ref diagramCenter provides a two-dimensional
452 * representation of this very same fact. */
453qreal ChromaHueDiagramPrivate::diagramOffset() const
454{
455 return q_pointer->maximumWidgetSquareSize() / 2.0;
456}
457
458/** @brief React on a resize event.
459 *
460 * Reimplemented from base class.
461 *
462 * @param event The corresponding resize event */
464{
465 Q_UNUSED(event)
466
467 // Update the widget content
468 d_pointer->m_wheelImage.setImageSize(maximumPhysicalSquareSize());
469 d_pointer->m_chromaHueImageParameters.imageSizePhysical =
470 // Guaranteed to be ≥ 0:
472 // TODO xxx Enable this line once the performance problem is solved.
473 // This is meant to free memory in the cache if the widget is
474 // not currently visible.
475 // d_pointer->m_chromaHueImage.setImageParameters(d_pointer->m_chromaHueImageParameters);
476
477 // As Qt documentation says:
478 // “The widget will be erased and receive a paint event
479 // immediately after processing the resize event. No
480 // drawing need be (or should be) done inside this handler.”
481}
482
483/** @brief Widget coordinate point corresponding to the
484 * @ref ChromaHueDiagram::currentColor property
485 *
486 * @returns Widget coordinate point corresponding to the
487 * @ref ChromaHueDiagram::currentColor property. This is the position
488 * of @ref ChromaHueDiagram::currentColor in the gamut diagram, but measured
489 * and expressed as widget coordinate point.
490 *
491 * @sa @ref ChromaHueMeasurement "Measurement details" */
492QPointF ChromaHueDiagramPrivate::widgetCoordinatesFromCurrentColor() const
493{
494 const qreal scaleFactor = //
495 (q_pointer->maximumWidgetSquareSize() - 2.0 * diagramBorder()) //
496 / (2.0 * m_rgbColorSpace->profileMaximumCielchD50Chroma());
497 QPointF currentColor = //
498 PolarPointF(m_currentColor.c, m_currentColor.h).toCartesian();
499 return QPointF(
500 // x:
501 currentColor.x() * scaleFactor + diagramOffset(),
502 // y:
503 diagramOffset() - currentColor.y() * scaleFactor);
504}
505
506/** @brief Converts widget pixel positions to Lab coordinates
507 *
508 * @param position The position of a pixel of the widget coordinate
509 * system. The given value does not necessarily need to
510 * be within the actual displayed diagram or even the gamut itself. It
511 * might even be negative.
512 *
513 * @returns The Lab coordinates of the currently displayed gamut diagram
514 * for the (center of the) given pixel position.
515 * @sa @ref ChromaHueMeasurement "Measurement details" */
516cmsCIELab ChromaHueDiagramPrivate::fromWidgetPixelPositionToLab(const QPoint position) const
517{
518 const qreal scaleFactor = //
519 (2.0 * m_rgbColorSpace->profileMaximumCielchD50Chroma()) //
520 / (q_pointer->maximumWidgetSquareSize() - 2.0 * diagramBorder());
521 // The pixel at position 0 0 has its top left border at position 0 0
522 // and its bottom right border at position 1 1 and its center at
523 // position 0.5 0.5. Its the center of the pixel that is our reference
524 // for conversion, therefore we have to ship by 0.5 widget pixels.
525 constexpr qreal pixelValueShift = 0.5;
526 cmsCIELab lab;
527 lab.L = m_currentColor.l;
528 lab.a = //
529 (position.x() + pixelValueShift - diagramOffset()) * scaleFactor;
530 lab.b = //
531 (position.y() + pixelValueShift - diagramOffset()) * scaleFactor * (-1);
532 return lab;
533}
534
535/** @brief Sets the @ref ChromaHueDiagram::currentColor property corresponding
536 * to a given widget pixel position.
537 *
538 * @param position The position of a pixel of the widget coordinate
539 * system. The given value does not necessarily need to be within the
540 * actual displayed diagram or even the gamut itself. It might even be
541 * negative.
542 *
543 * @post If the <em>center</em> of the widget pixel is within the represented
544 * gamut, then the @ref ChromaHueDiagram::currentColor property is
545 * set correspondingly. If the center of the widget pixel is outside
546 * the gamut, then the chroma value is reduced (while the hue is
547 * maintained) until arriving at the outer shell of the gamut; the
548 * @ref ChromaHueDiagram::currentColor property is than set to this adapted
549 * color.
550 *
551 * @note This function works independently of the actually displayed color
552 * gamut diagram. So if parts of the gamut (the high chroma parts) are cut
553 * off in the visible diagram, this does not influence this function.
554 *
555 * @sa @ref ChromaHueMeasurement "Measurement details"
556 *
557 * @internal
558 *
559 * @todo What when the mouse goes outside the gray circle, but more gamut
560 * is available outside (because @ref RgbColorSpace::profileMaximumCielchD50Chroma()
561 * was chosen too small)? For consistency, the handle of the diagram should
562 * stay within the gray circle, and this should be interpreted also actually
563 * as the value at the position of the handle. */
564void ChromaHueDiagramPrivate::setColorFromWidgetPixelPosition(const QPoint position)
565{
566 const cmsCIELab lab = fromWidgetPixelPositionToLab(position);
567 const auto myColor = //
568 m_rgbColorSpace->reduceCielchD50ChromaToFitIntoGamut(toLchDouble(lab));
569 q_pointer->setCurrentColor(myColor);
570}
571
572/** @brief Tests if a widget pixel position is within the mouse sensible circle.
573 *
574 * The mouse sensible circle contains the inner gray circle (on which the
575 * gamut diagram is painted).
576 * @param position The position of a pixel of the widget coordinate
577 * system. The given value does not necessarily need to be within the
578 * actual displayed diagram or even the gamut itself. It might even be
579 * negative.
580 * @returns <tt>true</tt> if the (center of the) pixel at the given position
581 * is within the circle, <tt>false</tt> otherwise. */
582bool ChromaHueDiagramPrivate::isWidgetPixelPositionWithinMouseSensibleCircle(const QPoint position) const
583{
584 const qreal radius = PolarPointF(
585 // Position relative to
586 // polar coordinate system center:
587 position
588 - diagramCenter()
589 // Apply the offset between
590 // - a pixel position on one hand and
591 // - a coordinate point in the middle of this very
592 // same pixel on the other:
593 + QPointF(0.5, 0.5))
594 .radius();
595
596 const qreal diagramCircleRadius = //
597 q_pointer->maximumWidgetSquareSize() / 2.0 - diagramBorder();
598
599 return (radius <= diagramCircleRadius);
600}
601
602/** @brief Paint the widget.
603 *
604 * Reimplemented from base class.
605 *
606 * @param event the paint event
607 *
608 * @internal
609 *
610 * @post
611 * - Paints the widget. Takes the existing
612 * @ref ChromaHueDiagramPrivate::m_chromaHueImage and
613 * @ref ChromaHueDiagramPrivate::m_wheelImage and paints them on the widget.
614 * If their cache is up-to-date, this operation is fast, otherwise
615 * considerably slower.
616 * - Paints the handles.
617 * - If the widget has focus, it also paints the focus indicator. As the
618 * widget is round, we cannot use <tt>QStyle::PE_FrameFocusRect</tt> for
619 * painting this, neither does <tt>QStyle</tt> provide build-in support
620 * for round widgets. Therefore, we draw the focus indicator ourself,
621 * which means its form is not controlled by <tt>QStyle</tt>.
622 *
623 * @todo Show the indicator on the color wheel not only while a mouse button
624 * is pressed, but also while a keyboard button is pressed.
625 *
626 * @todo What when @ref ChromaHueDiagramPrivate::m_currentColor has a valid
627 * in-gamut color, but this color is out of the <em>displayed</em> diagram?
628 * How to handle that? */
630{
631 Q_UNUSED(event)
632
633 // We do not paint directly on the widget, but on a QImage buffer first:
634 // Render anti-aliased looks better. But as Qt documentation says:
635 //
636 // “Renderhints are used to specify flags to QPainter that may or
637 // may not be respected by any given engine.”
638 //
639 // Painting here directly on the widget might lead to different
640 // anti-aliasing results depending on the underlying window system. This
641 // is especially problematic as anti-aliasing might shift or not a pixel
642 // to the left or to the right. So we paint on a QImage first. As QImage
643 // (at difference to QPixmap and a QWidget) is independent of native
644 // platform rendering, it guarantees identical anti-aliasing results on
645 // all platforms. Here the quote from QPainter class documentation:
646 //
647 // “To get the optimal rendering result using QPainter, you should
648 // use the platform independent QImage as paint device; i.e. using
649 // QImage will ensure that the result has an identical pixel
650 // representation on any platform.”
651 QImage buffer(maximumPhysicalSquareSize(), // width
652 maximumPhysicalSquareSize(), // height
654 );
655 buffer.fill(Qt::transparent);
657
658 // Other initialization
659 QPainter bufferPainter(&buffer);
660 QPen pen;
661 const QBrush transparentBrush{Qt::transparent};
662 // Set color of the handle: Black or white, depending on the lightness of
663 // the currently selected color.
664 const QColor handleColor //
665 {handleColorFromBackgroundLightness(d_pointer->m_currentColor.l)};
666 const QPointF widgetCoordinatesFromCurrentColor //
667 {d_pointer->widgetCoordinatesFromCurrentColor()};
668
669 // Paint the gamut itself as available in the cache.
670 bufferPainter.setRenderHint(QPainter::Antialiasing, false);
671 // As devicePixelRatioF() might have changed, we make sure everything
672 // that might depend on devicePixelRatioF() is updated before painting.
673 d_pointer->m_chromaHueImageParameters.borderPhysical =
674 // TODO It might be useful to reduce this border to (near to) zero, and
675 // than paint with an offset (if this is possible with drawEllipse?).
676 // Then the memory consumption would be reduced somewhat.
677 d_pointer->diagramBorder() * devicePixelRatioF();
678 d_pointer->m_chromaHueImageParameters.imageSizePhysical =
679 // Guaranteed to be ≥ 0:
681 const qreal temp = qBound(static_cast<qreal>(0), //
682 d_pointer->m_currentColor.l, //
683 static_cast<qreal>(100));
684 d_pointer->m_chromaHueImageParameters.lightness = temp;
685 d_pointer->m_chromaHueImageParameters.devicePixelRatioF = //
687 d_pointer->m_chromaHueImageParameters.rgbColorSpace = //
688 d_pointer->m_rgbColorSpace;
689 d_pointer->m_chromaHueImage.setImageParameters( //
690 d_pointer->m_chromaHueImageParameters);
691 d_pointer->m_chromaHueImage.refreshAsync();
692 const qreal circleRadius = //
693 (maximumWidgetSquareSize() - 2 * d_pointer->diagramBorder()) / 2.0;
694 bufferPainter.setRenderHint(QPainter::Antialiasing, true);
695 bufferPainter.setPen(QPen(Qt::NoPen));
696 bufferPainter.setBrush(d_pointer->m_chromaHueImage.getCache());
697 bufferPainter.drawEllipse(
698 // center:
701 // width:
702 circleRadius,
703 // height:
704 circleRadius);
705
706 // Paint a color wheel around
707 bufferPainter.setRenderHint(QPainter::Antialiasing, false);
708 // As devicePixelRatioF() might have changed, we make sure everything
709 // that might depend on devicePixelRatioF() is updated before painting.
710 d_pointer->m_wheelImage.setBorder( //
712 d_pointer->m_wheelImage.setDevicePixelRatioF(devicePixelRatioF());
713 d_pointer->m_wheelImage.setImageSize(maximumPhysicalSquareSize());
714 d_pointer->m_wheelImage.setWheelThickness( //
716 bufferPainter.drawImage( //
717 QPoint(0, 0), // position of the image
718 d_pointer->m_wheelImage.getImage() // the image itself
719 );
720
721 // Paint a handle on the color wheel (only if a mouse event is
722 // currently active).
723 if (d_pointer->m_isMouseEventActive) {
724 // The radius of the outer border of the color wheel
725 const qreal radius = //
727 // Get widget coordinate point for the handle
728 QPointF myHandleInner = PolarPointF(radius - gradientThickness(), //
729 d_pointer->m_currentColor.h)
730 .toCartesian();
731 myHandleInner.ry() *= -1; // Transform to Widget coordinate points
732 myHandleInner += d_pointer->diagramCenter();
733 QPointF myHandleOuter = //
734 PolarPointF(radius, d_pointer->m_currentColor.h).toCartesian();
735 myHandleOuter.ry() *= -1; // Transform to Widget coordinate points
736 myHandleOuter += d_pointer->diagramCenter();
737 // Draw the line
738 pen = QPen();
740 // TODO Instead of Qt::FlatCap, we could really paint a handle
741 // that does match perfectly the round inner and outer border
742 // of the wheel. But: Is it really worth the complexity?
744 pen.setColor(handleColor);
745 bufferPainter.setPen(pen);
746 bufferPainter.setRenderHint(QPainter::Antialiasing, true);
747 bufferPainter.drawLine(myHandleInner, myHandleOuter);
748 }
749
750 // Paint the handle within the gamut
751 bufferPainter.setRenderHint(QPainter::Antialiasing, true);
752 pen = QPen();
754 pen.setColor(handleColor);
756 bufferPainter.setPen(pen);
757 bufferPainter.setBrush(transparentBrush);
758 bufferPainter.drawEllipse(widgetCoordinatesFromCurrentColor, // center
759 handleRadius(), // x radius
760 handleRadius() // y radius
761 );
762 const auto diagramOffset = d_pointer->diagramOffset();
763 const QPointF diagramCartesianCoordinatesFromCurrentColor(
764 // x:
765 widgetCoordinatesFromCurrentColor.x() - diagramOffset,
766 // y:
767 (widgetCoordinatesFromCurrentColor.y() - diagramOffset) * (-1));
768 PolarPointF diagramPolarCoordinatesFromCurrentColor( //
769 diagramCartesianCoordinatesFromCurrentColor);
770 // lineRadius will be a point at the middle of the line thickness
771 // of the circular handle.
772 qreal lineRadius = //
773 diagramPolarCoordinatesFromCurrentColor.radius() - handleRadius();
774 if (lineRadius > 0) {
775 QPointF lineEndWidgetCoordinates = //
776 PolarPointF(
777 // radius:
778 lineRadius,
779 // angle:
780 diagramPolarCoordinatesFromCurrentColor.angleDegree() //
781 )
782 .toCartesian();
783 lineEndWidgetCoordinates.ry() *= (-1);
784 lineEndWidgetCoordinates += d_pointer->diagramCenter();
785 bufferPainter.drawLine(
786 // point 1 (center of the diagram):
787 d_pointer->diagramCenter(),
788 // point 2:
789 lineEndWidgetCoordinates);
790 }
791
792 // Paint a focus indicator.
793 //
794 // We could paint a focus indicator (round or rectangular) around the
795 // handle. Depending on the currently selected hue for the diagram, it
796 // looks ugly because the colors of focus indicator and diagram do not
797 // harmonize, or it is mostly invisible if the colors are similar. So
798 // this approach does not work well.
799 //
800 // It seems better to paint a focus indicator for the whole widget.
801 // We could use the style primitives to paint a rectangular focus
802 // indicator around the whole widget:
803 //
804 // style()->drawPrimitive(
805 // QStyle::PE_FrameFocusRect,
806 // &option,
807 // &painter,
808 // this
809 // );
810 //
811 // However, this does not work well because this widget does not have a
812 // rectangular form.
813 //
814 // Then we have to design the line that we want to display. It is better
815 // to do that ourselves instead of relying on generic QStyle::PE_Frame or
816 // similar solutions as their result seems to be quite unpredictable
817 // across various styles. So we use handleOutlineThickness as line width
818 // and paint it at the left-most possible position. As m_wheelBorder
819 // accommodates also to handleRadius(), the distance of the focus line to
820 // the real widget also does, which looks nice.
821 if (hasFocus()) {
822 bufferPainter.setRenderHint(QPainter::Antialiasing, true);
823 pen = QPen();
826 bufferPainter.setPen(pen);
827 bufferPainter.setBrush(transparentBrush);
828 bufferPainter.drawEllipse(
829 // center:
830 d_pointer->diagramCenter(),
831 // x radius:
832 diagramOffset - handleOutlineThickness() / 2.0,
833 // y radius:
834 diagramOffset - handleOutlineThickness() / 2.0);
835 }
836
837 // Paint the buffer to the actual widget
838 QPainter widgetPainter(this);
839 widgetPainter.setRenderHint(QPainter::Antialiasing, false);
840 widgetPainter.drawImage(QPoint(0, 0), buffer);
841}
842
843/** @brief The border around the round diagram.
844 *
845 * Measured in <em>device-independent pixels</em>.
846 *
847 * @returns The border. This is the space where the surrounding color wheel
848 * and the focus indicator are painted. */
849int ChromaHueDiagramPrivate::diagramBorder() const
850{
851 return
852 // The space outside the wheel:
853 q_pointer->spaceForFocusIndicator()
854 // Add space for the wheel itself:
855 + q_pointer->gradientThickness()
856 // Add extra space between wheel and diagram:
857 + 2 * q_pointer->handleOutlineThickness();
858}
859
860} // namespace PerceptualColor
Base class for LCH diagrams.
qreal handleRadius() const
The radius of a circular handle.
QColor handleColorFromBackgroundLightness(qreal lightness) const
An appropriate color for a handle, depending on the background lightness.
int gradientMinimumLength() const
The minimum length of a color gradient.
int spaceForFocusIndicator() const
The empty space around diagrams reserved for the focus indicator.
void callUpdate()
An alternative to QWidget::update().
int handleOutlineThickness() const
The outline thickness of a handle.
int gradientThickness() const
The thickness of a color gradient.
QColor focusIndicatorColor() const
The color for painting focus indicators.
int maximumPhysicalSquareSize() const
The maximum possible size of a square within the widget, measured in physical pixels.
qreal maximumWidgetSquareSize() const
The maximum possible size of a square within the widget, measured in device-independent pixels.
A widget for selecting chroma and hue in LCH color space.
virtual void mouseMoveEvent(QMouseEvent *event) override
React on a mouse move event.
virtual void wheelEvent(QWheelEvent *event) override
React on a mouse wheel event.
void setCurrentColor(const PerceptualColor::LchDouble &newCurrentColor)
Setter for the currentColor property.
LchDouble currentColor
Currently selected color.
virtual QSize minimumSizeHint() const override
Recommended minimum size for the widget.
void currentColorChanged(const PerceptualColor::LchDouble &newCurrentColor)
Notify signal for property currentColor.
virtual void keyPressEvent(QKeyEvent *event) override
React on key press events.
virtual ~ChromaHueDiagram() noexcept override
Default destructor.
virtual void paintEvent(QPaintEvent *event) override
Paint the widget.
virtual void mousePressEvent(QMouseEvent *event) override
React on a mouse press event.
virtual void mouseReleaseEvent(QMouseEvent *event) override
React on a mouse release event.
virtual void resizeEvent(QResizeEvent *event) override
React on a resize event.
Q_INVOKABLE ChromaHueDiagram(const QSharedPointer< PerceptualColor::RgbColorSpace > &colorSpace, QWidget *parent=nullptr)
The constructor.
virtual QSize sizeHint() const override
Recommended size for the widget.
The namespace of this library.
Format_ARGB32_Premultiplied
void fill(Qt::GlobalColor color)
void setDevicePixelRatio(qreal scaleFactor)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
qreal devicePixelRatioF() const const
void drawEllipse(const QPoint &center, int rx, int ry)
void drawImage(const QPoint &point, const QImage &image)
void drawLine(const QLine &line)
void setBrush(Qt::BrushStyle style)
void setPen(Qt::PenStyle style)
void setRenderHint(RenderHint hint, bool on)
void setCapStyle(Qt::PenCapStyle style)
void setColor(const QColor &color)
void setWidth(int width)
int x() const const
int y() const const
qreal & ry()
qreal x() const const
qreal y() const const
BlankCursor
MouseFocusReason
transparent
void setCursor(const QCursor &)
virtual bool event(QEvent *event) override
bool hasFocus() const const
void setFocusPolicy(Qt::FocusPolicy policy)
virtual void keyPressEvent(QKeyEvent *event)
void setFocus()
void update()
A LCH color (Oklch, CielchD50, CielchD65…)
Definition lchdouble.h:50
bool hasSameCoordinates(const LchDouble &other) const
Compares coordinates with another object.
Definition lchdouble.cpp:75
double l
Lightness, mesured in percent.
Definition lchdouble.h:55
double h
Hue, measured in degree.
Definition lchdouble.h:67
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri May 24 2024 11:48:43 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.