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 "polarpointf.h"
21#include "rgbcolorspace.h"
22#include <lcms2.h>
23#include <qbrush.h>
24#include <qcolor.h>
25#include <qevent.h>
26#include <qimage.h>
27#include <qnamespace.h>
28#include <qpainter.h>
29#include <qpen.h>
30#include <qpoint.h>
31#include <qsharedpointer.h>
32#include <qwidget.h>
33
34namespace PerceptualColor
35{
36/** @brief The constructor.
37 * @param colorSpace The color space within which this widget should operate.
38 * Can be created with @ref RgbColorSpaceFactory.
39 * @param parent The widget’s parent widget. This parameter will be passed
40 * to the base class’s constructor. */
42 : AbstractDiagram(parent)
43 , d_pointer(new ChromaHueDiagramPrivate(this, colorSpace))
44{
45 // Setup LittleCMS. This is the first thing to do, because other
46 // operations rely on a working LittleCMS.
47 d_pointer->m_rgbColorSpace = colorSpace;
48
49 // Set focus policy
50 // In Qt, usually focus (QWidget::hasFocus()) by mouse click is either
51 // not accepted at all or accepted always for the hole rectangular
52 // widget, depending on QWidget::focusPolicy(). This is not convenient
53 // and intuitive for big, circular-shaped widgets like this one. It
54 // would be nicer if the focus would only be accepted by mouse clicks
55 // <em>within the circle itself</em>. Qt does not provide a build-in
56 // way to do this. But a workaround to implement this behavior is
57 // possible: Set QWidget::focusPolicy() to <em>not</em> accept focus
58 // by mouse click. Then, reimplement mousePressEvent() and call
59 // setFocus(Qt::MouseFocusReason) if the mouse click is within the
60 // circle. Therefore, this class simply defaults to
61 // Qt::FocusPolicy::TabFocus for QWidget::focusPolicy().
62 setFocusPolicy(Qt::FocusPolicy::TabFocus);
63
64 // Connections
65 connect(&d_pointer->m_chromaHueImage, //
66 &AsyncImageProvider<ChromaHueImageParameters>::interlacingPassCompleted, //
67 this,
69
70 // Initialize the color
71 setCurrentColorCielchD50(CielchD50Values::srgbVersatileInitialColor);
72}
73
74/** @brief Default destructor */
78
79/** @brief Constructor
80 *
81 * @param backLink Pointer to the object from which <em>this</em> object
82 * is the private implementation.
83 * @param colorSpace The color space within which this widget
84 * should operate. */
85ChromaHueDiagramPrivate::ChromaHueDiagramPrivate(ChromaHueDiagram *backLink, const QSharedPointer<PerceptualColor::RgbColorSpace> &colorSpace)
86 : m_currentColorCielchD50{0, 0, 0} // dummy value
87 , m_wheelImage(colorSpace)
88 , q_pointer(backLink)
89{
90}
91
92/** @brief React on a mouse press event.
93 *
94 * Reimplemented from base class.
95 *
96 * @internal
97 * @post
98 * - If the mouse is clicked with the circular diagram (inside or
99 * outside of the visible gamut), than this widget gets the focus
100 * and and @ref ChromaHueDiagramPrivate::m_isMouseEventActive is
101 * set to <tt>true</tt> to track mouse movements from now on.
102 * Reacts on all clicks (left, middle, right). If the mouse was
103 * within the gamut, the diagram’s handle is displaced there. If
104 * the mouse was outside the gamut, the diagram’s handle always stays
105 * within the gamut: The hue value is correctly retained, while the chroma
106 * value is the highest possible chroma within the gamut at this hue.
107 * @endinternal
108 *
109 * @param event The corresponding mouse event */
111{
112 // TODO Also accept out-of-gamut clicks when they are covered by the
113 // current handle.
114 const bool isWithinCircle = //
115 d_pointer->isWidgetPixelPositionWithinMouseSensibleCircle(event->pos());
116 if (isWithinCircle) {
117 event->accept();
118 // Mouse focus is handled manually because so we can accept
119 // focus only on mouse clicks within the displayed gamut, while
120 // rejecting focus otherwise. In the constructor, therefore
121 // Qt::FocusPolicy::TabFocus is specified, so that manual handling
122 // of mouse focus is up to this code here.
124 // Enable mouse tracking from now on:
125 d_pointer->m_isMouseEventActive = true;
126 // As clicks are only accepted within the visible gamut, the mouse
127 // cursor is made invisible. Its function is taken over by the
128 // handle itself within the displayed gamut.
130 // Set the color property
131 d_pointer->setColorFromWidgetPixelPosition(event->pos());
132 // Schedule a paint event, so that the wheel handle will show. It’s
133 // not enough to hope setColorFromWidgetCoordinates() would do this,
134 // because setColorFromWidgetCoordinates() would not update the
135 // widget if the mouse click was done at the same position as the
136 // current color handle.
137 update();
138 } else {
139 // Make sure default behavior like drag-window in KDE’s
140 // “Breeze” widget style works if this widget does not
141 // actually react itself on a mouse event.
142 event->ignore();
143 }
144}
145
146/** @brief React on a mouse move event.
147 *
148 * Reimplemented from base class.
149 *
150 * @internal
151 * @post Reacts only on mouse move events if
152 * @ref ChromaHueDiagramPrivate::m_isMouseEventActive is <tt>true</tt>:
153 * - If the mouse moves within the gamut, the diagram’s handle is displaced
154 * there. The mouse cursor is invisible; only the diagram’ handle is
155 * visible.
156 * - If the mouse moves outside the gamut, the diagram’s handle always stays
157 * within the gamut: The hue value is correctly retained, while the chroma
158 * value is the highest possible chroma within the gamut at this hue.
159 * Both, the diagram’s handle <em>and</em> the mouse cursor are
160 * visible.
161 * @endinternal
162 *
163 * @param event The corresponding mouse event */
165{
166 if (d_pointer->m_isMouseEventActive) {
167 event->accept();
168 const cmsCIELab cielabD50 = //
169 d_pointer->fromWidgetPixelPositionToLab(event->pos());
170 const bool isWithinCircle = //
171 d_pointer->isWidgetPixelPositionWithinMouseSensibleCircle( //
172 event->pos());
173 if (isWithinCircle && d_pointer->m_rgbColorSpace->isCielabD50InGamut(cielabD50)) {
175 } else {
176 unsetCursor();
177 }
178 d_pointer->setColorFromWidgetPixelPosition(event->pos());
179 } else {
180 // Make sure default behavior like drag-window in KDE’s
181 // Breeze widget style works.
182 event->ignore();
183 }
184}
185
186/** @brief React on a mouse release event.
187 *
188 * Reimplemented from base class. Reacts on all clicks (left, middle, right).
189 *
190 * @param event The corresponding mouse event
191 *
192 * @internal
193 *
194 * @post If @ref ChromaHueDiagramPrivate::m_isMouseEventActive is
195 * <tt>true</tt> then:
196 * - If the mouse is within the gamut, the diagram’s handle is displaced
197 * there.
198 * - If the mouse moves outside the gamut, the diagram’s handle always stays
199 * within the gamut: The hue value is correctly retained, while the chroma
200 * value is the highest possible chroma within the gamut at this hue.
201 * - The mouse cursor is made visible (if he wasn’t yet visible anyway).
202 * - @ref ChromaHueDiagramPrivate::m_isMouseEventActive is set
203 * to <tt>false</tt>.
204 *
205 * @todo What if the widget displays a gamut that has no L*=0.1 because its
206 * blackpoint is lighter.? Sacrificing chroma alone does not help? How to
207 * react (for mouse input, keyboard input, but also API functions like
208 * setColor()? */
210{
211 if (d_pointer->m_isMouseEventActive) {
212 event->accept();
213 unsetCursor();
214 d_pointer->m_isMouseEventActive = false;
215 d_pointer->setColorFromWidgetPixelPosition(event->pos());
216 // Schedule a paint event, so that the wheel handle will be hidden.
217 // It’s not enough to hope setColorFromWidgetCoordinates() would do
218 // this, because setColorFromWidgetCoordinates() would not update the
219 // widget if the mouse click was done at the same position as the
220 // current color handle.
221 update();
222 } else {
223 // Make sure default behavior like drag-window in KDE’s
224 // Breeze widget style works
225 event->ignore();
226 }
227}
228
229/** @brief React on a mouse wheel event.
230 *
231 * Reimplemented from base class.
232 *
233 * Scrolling up raises the hue value, scrolling down lowers the hue value.
234 * Of course, at the point at 0°/360° wrapping applies.
235 *
236 * @param event The corresponding mouse event */
238{
239 // Though QWheelEvent::position() returns a floating point
240 // value, this value seems to corresponds to a pixel position
241 // and not a coordinate point. Therefore, we convert to QPoint.
242 const bool isWithinCircle = //
243 d_pointer->isWidgetPixelPositionWithinMouseSensibleCircle( //
244 event->position().toPoint());
245 if (
246 // Do nothing while a the mouse is clicked and the mouse movement is
247 // tracked anyway because this would be confusing for the user.
248 (!d_pointer->m_isMouseEventActive)
249 // Only react on good old vertical wheels,
250 // and not on horizontal wheels.
251 && (event->angleDelta().y() != 0)
252 // Only react on wheel events when then happen in the appropriate
253 // area.
254 // Though QWheelEvent::position() returns a floating point
255 // value, this value seems to corresponds to a pixel position
256 // and not a coordinate point. Therefore, we convert to QPoint.
257 && isWithinCircle
258 // then:
259 ) {
260 event->accept();
261 // Calculate the new hue.
262 // This may result in a hue smaller then 0° or bigger then 360°.
263 // This should not make any problems.
264 GenericColor newColor = d_pointer->m_currentColorCielchD50;
265 newColor.third += standardWheelStepCount(event) * singleStepHue;
267 d_pointer->m_rgbColorSpace->reduceCielchD50ChromaToFitIntoGamut(newColor));
268 } else {
269 event->ignore();
270 }
271}
272
273/** @brief React on key press events.
274 *
275 * Reimplemented from base class.
276 *
277 * The keys do not react in form of up, down, left and right like in
278 * Cartesian coordinate systems. The keys change radius and angel like
279 * in polar coordinate systems, because our color model is also based
280 * on a polar coordinate system.
281 *
282 * For chroma changes: Moves the handle as much as possible into the
283 * desired direction as long as this is still in the gamut.
284 * - Qt::Key_Up increments chroma a small step
285 * - Qt::Key_Down decrements chroma a small step
286 * - Qt::Key_PageUp increments chroma a big step
287 * - Qt::Key_PageDown decrements chroma a big step
288 *
289 * For hue changes: If necessary, the chroma value is reduced to get an
290 * in-gamut color with the new hue.
291 * - Qt::Key_Left increments hue a small step
292 * - Qt::Key_Right decrements hue a small step
293 * - Qt::Key_Home increments hue a big step
294 * - Qt::Key_End decrements hue a big step
295 *
296 * @param event the event
297 *
298 * @internal
299 *
300 * @todo Is this behavior really a good user experience? Or is it confusing
301 * that left, right, up and down don’t do what was expected? What could be
302 * more intuitive keys for changing radius and angle? At least the arrow keys
303 * are likely that the user tries them out by trial-and-error. */
305{
306 GenericColor newColor = currentColorCielchD50();
307 switch (event->key()) {
308 case Qt::Key_Up:
309 newColor.second += singleStepChroma;
310 break;
311 case Qt::Key_Down:
312 newColor.second -= singleStepChroma;
313 break;
314 case Qt::Key_Left:
315 newColor.third += singleStepHue;
316 break;
317 case Qt::Key_Right:
318 newColor.third -= singleStepHue;
319 break;
320 case Qt::Key_PageUp:
321 newColor.second += pageStepChroma;
322 break;
323 case Qt::Key_PageDown:
324 newColor.second -= pageStepChroma;
325 break;
326 case Qt::Key_Home:
327 newColor.third += pageStepHue;
328 break;
329 case Qt::Key_End:
330 newColor.third -= pageStepHue;
331 break;
332 default:
333 // Quote from Qt documentation:
334 //
335 // “If you reimplement this handler, it is very important that
336 // you call the base class implementation if you do not act
337 // upon the key.
338 //
339 // The default implementation closes popup widgets if the
340 // user presses the key sequence for QKeySequence::Cancel
341 // (typically the Escape key). Otherwise the event is
342 // ignored, so that the widget’s parent can interpret it.“
344 return;
345 }
346 // Here we reach only if the key has been recognized. If not, in the
347 // default branch of the switch statement, we would have passed the
348 // keyPressEvent yet to the parent and returned.
349 if (newColor.second < 0) {
350 // Do not allow negative chroma values.
351 // (Doing so would be counter-intuitive.)
352 newColor.second = 0;
353 }
354 // Move the value into gamut (if necessary):
355 newColor = d_pointer->m_rgbColorSpace->reduceCielchD50ChromaToFitIntoGamut(newColor);
356 // Apply the new value:
357 setCurrentColorCielchD50(newColor);
358}
359
360/** @brief Recommended size for the widget.
361 *
362 * Reimplemented from base class.
363 *
364 * @returns Recommended size for the widget.
365 *
366 * @sa @ref minimumSizeHint() */
368{
369 return minimumSizeHint() * scaleFromMinumumSizeHintToSizeHint;
370}
371
372/** @brief Recommended minimum size for the widget
373 *
374 * Reimplemented from base class.
375 *
376 * @returns Recommended minimum size for the widget.
377 *
378 * @sa @ref sizeHint() */
380{
381 const int mySize =
382 // Considering the gradient length two times, as the diagram
383 // shows the center of the coordinate system in the middle,
384 // and each side of the center should be well visible.
385 2 * d_pointer->diagramBorder() + 2 * gradientMinimumLength();
386 // Expand to the global minimum size for GUI elements
387 return QSize(mySize, mySize);
388}
389
390// No documentation here (documentation of properties
391// and its getters are in the header)
393{
394 return d_pointer->m_currentColorCielchD50;
395}
396
397/** @brief Setter for the @ref currentColorCielchD50 property.
398 *
399 * @param newCurrentColorCielchD50 the new color */
400void ChromaHueDiagram::setCurrentColorCielchD50(const GenericColor &newCurrentColorCielchD50)
401{
402 if (newCurrentColorCielchD50 == d_pointer->m_currentColorCielchD50) {
403 return;
404 }
405
406 const GenericColor oldColor = d_pointer->m_currentColorCielchD50;
407
408 d_pointer->m_currentColorCielchD50 = newCurrentColorCielchD50;
409
410 // Update, if necessary, the diagram.
411 if (d_pointer->m_currentColorCielchD50.first != oldColor.first) {
412 const qreal temp = qBound(static_cast<qreal>(0), //
413 d_pointer->m_currentColorCielchD50.first, //
414 static_cast<qreal>(100));
415 d_pointer->m_chromaHueImageParameters.lightness = temp;
416 // TODO xxx Enable this line one the performance problem is solved.
417 // This is meant to free memory in the cache if the widget is
418 // not currently visible.
419 // d_pointer->m_chromaHueImage.setImageParameters(d_pointer->m_chromaHueImageParameters);
420 }
421
422 // Schedule a paint event:
423 update();
424
425 // Emit notify signal
426 Q_EMIT currentColorCielchD50Changed(newCurrentColorCielchD50);
427}
428
429/** @brief The point that is the center of the diagram coordinate system.
430 *
431 * @returns The point that is the center of the diagram coordinate system,
432 * measured in <em>device-independent pixels</em> relative to the widget
433 * coordinate system.
434 *
435 * @sa @ref diagramOffset provides a one-dimensional
436 * representation of this very same fact. */
437QPointF ChromaHueDiagramPrivate::diagramCenter() const
438{
439 const qreal tempOffset{diagramOffset()};
440 return QPointF(tempOffset, tempOffset);
441}
442
443/** @brief The point that is the center of the diagram coordinate system.
444 *
445 * @returns The offset between the center of the widget coordinate system
446 * and the center of the diagram coordinate system. The value is measured in
447 * <em>device-independent pixels</em> relative to the widget’s coordinate
448 * system. The value is identical for both, x axis and y axis.
449 *
450 * @sa @ref diagramCenter provides a two-dimensional
451 * representation of this very same fact. */
452qreal ChromaHueDiagramPrivate::diagramOffset() const
453{
454 return q_pointer->maximumWidgetSquareSize() / 2.0;
455}
456
457/** @brief React on a resize event.
458 *
459 * Reimplemented from base class.
460 *
461 * @param event The corresponding resize event */
463{
464 Q_UNUSED(event)
465
466 // Update the widget content
467 d_pointer->m_wheelImage.setImageSize(maximumPhysicalSquareSize());
468 d_pointer->m_chromaHueImageParameters.imageSizePhysical =
469 // Guaranteed to be ≥ 0:
471 // TODO xxx Enable this line once the performance problem is solved.
472 // This is meant to free memory in the cache if the widget is
473 // not currently visible.
474 // d_pointer->m_chromaHueImage.setImageParameters(d_pointer->m_chromaHueImageParameters);
475
476 // As Qt documentation says:
477 // “The widget will be erased and receive a paint event
478 // immediately after processing the resize event. No
479 // drawing need be (or should be) done inside this handler.”
480}
481
482/** @brief Widget coordinate point corresponding to the
483 * @ref ChromaHueDiagram::currentColorCielchD50 property
484 *
485 * @returns Widget coordinate point corresponding to the
486 * @ref ChromaHueDiagram::currentColorCielchD50 property. This is the position
487 * of @ref ChromaHueDiagram::currentColorCielchD50 in the gamut diagram, but measured
488 * and expressed as widget coordinate point.
489 *
490 * @sa @ref ChromaHueMeasurement "Measurement details" */
491QPointF ChromaHueDiagramPrivate::widgetCoordinatesFromCurrentColorCielchD50() const
492{
493 const qreal scaleFactor = //
494 (q_pointer->maximumWidgetSquareSize() - 2.0 * diagramBorder()) //
495 / (2.0 * m_rgbColorSpace->profileMaximumCielchD50Chroma());
496 QPointF currentColor = //
497 PolarPointF(m_currentColorCielchD50.second, m_currentColorCielchD50.third).toCartesian();
498 return QPointF(
499 // x:
500 currentColor.x() * scaleFactor + diagramOffset(),
501 // y:
502 diagramOffset() - currentColor.y() * scaleFactor);
503}
504
505/** @brief Converts widget pixel positions to Lab coordinates
506 *
507 * @param position The position of a pixel of the widget coordinate
508 * system. The given value does not necessarily need to
509 * be within the actual displayed diagram or even the gamut itself. It
510 * might even be negative.
511 *
512 * @returns The Lab coordinates of the currently displayed gamut diagram
513 * for the (center of the) given pixel position.
514 * @sa @ref ChromaHueMeasurement "Measurement details" */
515cmsCIELab ChromaHueDiagramPrivate::fromWidgetPixelPositionToLab(const QPoint position) const
516{
517 const qreal scaleFactor = //
518 (2.0 * m_rgbColorSpace->profileMaximumCielchD50Chroma()) //
519 / (q_pointer->maximumWidgetSquareSize() - 2.0 * diagramBorder());
520 // The pixel at position 0 0 has its top left border at position 0 0
521 // and its bottom right border at position 1 1 and its center at
522 // position 0.5 0.5. Its the center of the pixel that is our reference
523 // for conversion, therefore we have to ship by 0.5 widget pixels.
524 constexpr qreal pixelValueShift = 0.5;
525 cmsCIELab lab;
526 lab.L = m_currentColorCielchD50.first;
527 lab.a = //
528 (position.x() + pixelValueShift - diagramOffset()) * scaleFactor;
529 lab.b = //
530 (position.y() + pixelValueShift - diagramOffset()) * scaleFactor * (-1);
531 return lab;
532}
533
534/** @brief Sets the @ref ChromaHueDiagram::currentColorCielchD50 property corresponding
535 * to a given widget pixel position.
536 *
537 * @param position The position of a pixel of the widget coordinate
538 * system. The given value does not necessarily need to be within the
539 * actual displayed diagram or even the gamut itself. It might even be
540 * negative.
541 *
542 * @post If the <em>center</em> of the widget pixel is within the represented
543 * gamut, then the @ref ChromaHueDiagram::currentColorCielchD50 property is
544 * set correspondingly. If the center of the widget pixel is outside
545 * the gamut, then the chroma value is reduced (while the hue is
546 * maintained) until arriving at the outer shell of the gamut; the
547 * @ref ChromaHueDiagram::currentColorCielchD50 property is than set to this adapted
548 * color.
549 *
550 * @note This function works independently of the actually displayed color
551 * gamut diagram. So if parts of the gamut (the high chroma parts) are cut
552 * off in the visible diagram, this does not influence this function.
553 *
554 * @sa @ref ChromaHueMeasurement "Measurement details"
555 *
556 * @internal
557 *
558 * @todo What when the mouse goes outside the gray circle, but more gamut
559 * is available outside (because @ref RgbColorSpace::profileMaximumCielchD50Chroma()
560 * was chosen too small)? For consistency, the handle of the diagram should
561 * stay within the gray circle, and this should be interpreted also actually
562 * as the value at the position of the handle. */
563void ChromaHueDiagramPrivate::setColorFromWidgetPixelPosition(const QPoint position)
564{
565 const cmsCIELab lab = fromWidgetPixelPositionToLab(position);
566 const auto myColor = //
567 m_rgbColorSpace->reduceCielchD50ChromaToFitIntoGamut( //
568 toGenericColorCielabD50(lab));
569 q_pointer->setCurrentColorCielchD50(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_currentColorCielchD50 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_currentColorCielchD50.first)};
666 const QPointF widgetCoordinatesFromCurrentColorCielchD50 //
667 {d_pointer->widgetCoordinatesFromCurrentColorCielchD50()};
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_currentColorCielchD50.first, //
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_currentColorCielchD50.third)
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_currentColorCielchD50.third).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(widgetCoordinatesFromCurrentColorCielchD50, // center
759 handleRadius(), // x radius
760 handleRadius() // y radius
761 );
762 const auto diagramOffset = d_pointer->diagramOffset();
763 const QPointF diagramCartesianCoordinatesFromCurrentColorCielchD50(
764 // x:
765 widgetCoordinatesFromCurrentColorCielchD50.x() - diagramOffset,
766 // y:
767 (widgetCoordinatesFromCurrentColorCielchD50.y() - diagramOffset) * (-1));
768 PolarPointF diagramPolarCoordinatesFromCurrentColorCielchD50( //
769 diagramCartesianCoordinatesFromCurrentColorCielchD50);
770 // lineRadius will be a point at the middle of the line thickness
771 // of the circular handle.
772 qreal lineRadius = //
773 diagramPolarCoordinatesFromCurrentColorCielchD50.radius() - handleRadius();
774 if (lineRadius > 0) {
775 QPointF lineEndWidgetCoordinates = //
776 PolarPointF(
777 // radius:
778 lineRadius,
779 // angle:
780 diagramPolarCoordinatesFromCurrentColorCielchD50.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 currentColorCielchD50Changed(const PerceptualColor::GenericColor &newCurrentColorCielchD50)
Notify signal for property currentColorCielchD50.
virtual QSize minimumSizeHint() const override
Recommended minimum size for the widget.
GenericColor currentColorCielchD50
Currently selected color.
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.
void setCurrentColorCielchD50(const PerceptualColor::GenericColor &newCurrentColorCielchD50)
Setter for the currentColorCielchD50 property.
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()
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Mon Nov 18 2024 12:18:38 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.