Perceptual Color

wheelcolorpicker.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 "wheelcolorpicker.h"
7// Second, the private implementation.
8#include "wheelcolorpicker_p.h" // IWYU pragma: associated
9
10#include "abstractdiagram.h"
11#include "chromalightnessdiagram.h"
12#include "chromalightnessdiagram_p.h" // IWYU pragma: keep // TODO Avoid this pragma by better design: not accessing private parts of other classes.
13#include "cielchd50values.h"
14#include "colorwheel.h"
15#include "colorwheel_p.h" // IWYU pragma: keep // TODO Avoid this pragma by better design: not accessing private parts of other classes.
16#include "constpropagatingrawpointer.h"
17#include "constpropagatinguniquepointer.h"
18#include "helperconstants.h"
19#include "lchdouble.h"
20#include "rgbcolorspace.h"
21#include <math.h>
22#include <qapplication.h>
23#include <qmath.h>
24#include <qobject.h>
25#include <qpoint.h>
26#include <qpointer.h>
27#include <qrect.h>
28#include <qsharedpointer.h>
29#include <utility>
30class QResizeEvent;
31class QWidget;
32
33namespace PerceptualColor
34{
35/** @brief Constructor
36 * @param colorSpace The color space within which this widget should operate.
37 * Can be created with @ref RgbColorSpaceFactory.
38 * @param parent The widget’s parent widget. This parameter will be passed
39 * to the base class’s constructor. */
41 : AbstractDiagram(parent)
42 , d_pointer(new WheelColorPickerPrivate(this))
43{
44 d_pointer->m_rgbColorSpace = colorSpace;
45 d_pointer->m_colorWheel = new ColorWheel(colorSpace, this);
46 d_pointer->m_chromaLightnessDiagram = new ChromaLightnessDiagram(
47 // Same color space for this widget:
48 colorSpace,
49 // This widget is smaller than the color wheel. It will be a child
50 // of the color wheel, so that missed mouse or key events will be
51 // forwarded to the parent widget (color wheel).
52 d_pointer->m_colorWheel);
53 d_pointer->m_colorWheel->setFocusProxy(d_pointer->m_chromaLightnessDiagram);
54 d_pointer->resizeChildWidgets();
55
56 connect(
57 // changes on the color wheel trigger a change in the inner diagram
58 d_pointer->m_colorWheel,
60 this,
61 [this](const qreal newHue) {
62 LchDouble lch = d_pointer->m_chromaLightnessDiagram->currentColor();
63 lch.h = newHue;
64 // We have to be sure that the color is in-gamut also for the
65 // new hue. If it is not, we adjust it:
66 lch = d_pointer->m_rgbColorSpace->reduceCielchD50ChromaToFitIntoGamut(lch);
67 d_pointer->m_chromaLightnessDiagram->setCurrentColor(lch);
68 });
69 connect(d_pointer->m_chromaLightnessDiagram,
70 &ChromaLightnessDiagram::currentColorChanged,
71 this,
72 // As value is stored anyway within ChromaLightnessDiagram member,
73 // it’s enough to just emit the corresponding signal of this class:
75 connect(
76 // QWidget’s constructor requires a QApplication object. As this
77 // is a class derived from QWidget, calling qApp is safe here.
78 qApp,
80 d_pointer.get(), // Without .get() apparently connect() won’t work…
81 &WheelColorPickerPrivate::handleFocusChanged);
82
83 // Initial color
85 // Though CielchD50Values::srgbVersatileInitialColor() is expected to
86 // be in-gamut, its more secure to guarantee this explicitly:
87 d_pointer->m_rgbColorSpace->reduceCielchD50ChromaToFitIntoGamut(
88 // Default sRGB initial color:
89 CielchD50Values::srgbVersatileInitialColor));
90}
91
92/** @brief Default destructor */
96
97/** @brief Constructor
98 *
99 * @param backLink Pointer to the object from which <em>this</em> object
100 * is the private implementation. */
101WheelColorPickerPrivate::WheelColorPickerPrivate(WheelColorPicker *backLink)
102 : q_pointer(backLink)
103{
104}
105
106/** Repaint @ref m_colorWheel when focus changes
107 * on @ref m_chromaLightnessDiagram
108 *
109 * @ref m_chromaLightnessDiagram is the focus proxy of @ref m_colorWheel.
110 * Both show a focus indicator when keyboard focus is active. But
111 * apparently @ref m_colorWheel does not always repaint when focus
112 * changes. Therefore, this slot can be connected to the <tt>qApp</tt>’s
113 * <tt>focusChanged()</tt> signal to make sure that the repaint works.
114 *
115 * @note It might be an alternative to write an event filter
116 * for @ref m_chromaLightnessDiagram to do the same work. The event
117 * filter could be either @ref WheelColorPicker or
118 * @ref WheelColorPickerPrivate (the last case means that
119 * @ref WheelColorPickerPrivate would still have to inherit from
120 * <tt>QObject</tt>). But that would probably be more complicate… */
121void WheelColorPickerPrivate::handleFocusChanged(QWidget *old, QWidget *now)
122{
123 if ((old == m_chromaLightnessDiagram) || (now == m_chromaLightnessDiagram)) {
124 m_colorWheel->update();
125 }
126}
127
128/** @brief React on a resize event.
129 *
130 * Reimplemented from base class.
131 *
132 * @param event The corresponding resize event */
134{
136 d_pointer->resizeChildWidgets();
137}
138
139/** @brief Calculate the optimal size for the inner diagram.
140 *
141 * @returns The maximum possible size of the diagram within the
142 * inner part of the color wheel. With floating point precision.
143 * Measured in <em>device-independent pixels</em>. */
144QSizeF WheelColorPickerPrivate::optimalChromaLightnessDiagramSize() const
145{
146 /** The outer dimensions of the widget are a rectangle within a
147 * circumscribed circled, which is the inner border of the color wheel.
148 *
149 * The widget size is composed by the size of the diagram itself and
150 * the size of the borders. The border size is fixed; only the diagram
151 * size can vary.
152 *
153 * Known variables:
154 * | variable | comment | value |
155 * | :----------- | :------------------------------- | :--------------------------------- |
156 * | r | relation b ÷ a | maximum lightness ÷ maximum chroma |
157 * | h | horizontal shift | left + right diagram border |
158 * | v | vertical shift | top + bottom diagram border |
159 * | d | diameter of circumscribed circle | inner diameter of the color wheel |
160 * | b | diagram height | a × r |
161 * | widgetWidth | widget width | a + h |
162 * | widgetHeight | widget height | b + v |
163 * | a | diagram width | ? |
164 */
165 const qreal r = 100.0 / m_rgbColorSpace->profileMaximumCielchD50Chroma();
166 const qreal h = m_chromaLightnessDiagram->d_pointer->leftBorderPhysical() //
167 + m_chromaLightnessDiagram->d_pointer->defaultBorderPhysical();
168 const qreal v = 2 * m_chromaLightnessDiagram->d_pointer->defaultBorderPhysical();
169 const qreal d = m_colorWheel->d_pointer->innerDiameter();
170
171 /** We can calculate <em>a</em> because right-angled triangle
172 * with <em>a</em> and with <em>b</em> as legs/catheti will have
173 * has hypotenuse the diameter of the circumscribed circle:
174 *
175 * <em>[The following formula requires a working Internet connection
176 * to be displayed.]</em>
177 *
178 * @f[
179 \begin{align}
180 widgetWidth²
181 + widgetHeight²
182 = & d²
183 \\
184 (a+h)²
185 + (b+v)²
186 = & d²
187 \\
188 (a+h)²
189 + (ra+v)²
190 = & d²
191 \\
192
193 + 2ah
194 + h²
195 + r²a²
196 + 2rav
197 + v²
198 = & d²
199 \\
200
201 + r²a²
202 + 2ah
203 + 2rav
204 + h²
205 + v²
206 = & d²
207 \\
208 (1+r²)a²
209 + 2a(h+rv)
210 + (h²+v²)
211 = & d²
212 \\
213
214 + 2a\frac{h+rv}{1+r²}
215 + \frac{h²+v²}{1+r²}
216 = & \frac{d²}{1+r²}
217 \\
218
219 + 2a\frac{h+rv}{1+r²}
220 + \left(\frac{h+rv}{1+r²}\right)^{2}
221 - \left(\frac{h+rv}{1+r²}\right)^{2}
222 + \frac{h²+v²}{1+r²}
223 = & \frac{d²}{1+r²}
224 \\
225 \left(a+\frac{h+rv}{1+r²}\right)^{2}
226 - \left(\frac{h+rv}{1+r²}\right)^{2}
227 + \frac{h²+v²}{1+r²}
228 = & \frac{d²}{1+r²}
229 \\
230 \left(a+\frac{h+rv}{1+r²}\right)^{2}
231 = & \frac{d²}{1+r²}
232 + \left(\frac{h+rv}{1+r²}\right)^{2}
233 - \frac{h²+v²}{1+r²}
234 \\
235 a
236 + \frac{h+rv}{1+r²}
237 = & \sqrt{
238 \frac{d²}{1+r²}
239 + \left(\frac{h+rv}{1+r²}\right)^{2}
240 -\frac{h²+v²}{1+r²}
241 }
242 \\
243 a
244 = & \sqrt{
245 \frac{d²}{1+r²}
246 + \left(\frac{h+rv}{1+r²}\right)^{2}
247 - \frac{h²+v²}{1+r²}
248 }
249 - \frac{h+rv}{1+r²}
250 \end{align}
251 * @f] */
252 const qreal x = (1 + qPow(r, 2)); // x = 1 + r²
253 const qreal a =
254 // The square root:
255 qSqrt(
256 // First fraction:
257 d * d / x
258 // Second fraction:
259 + qPow((h + r * v) / x, 2)
260 // Thierd fraction:
261 - (h * h + v * v) / x)
262 // The part after the square root:
263 - (h + r * v) / x;
264 const qreal b = r * a;
265
266 return QSizeF(a + h, // width
267 b + v // height
268 );
269}
270
271/** @brief Update the geometry of the child widgets.
272 *
273 * This widget does <em>not</em> use layout management for its child widgets.
274 * Therefore, this function should be called on all resize events of this
275 * widget.
276 *
277 * @post The geometry (size and the position) of the child widgets are
278 * adapted according to the current size of <em>this</em> widget itself. */
279void WheelColorPickerPrivate::resizeChildWidgets()
280{
281 // Set new geometry of color wheel. Only the size changes, while the
282 // position (which is 0, 0) remains always unchanged.
283 m_colorWheel->resize(q_pointer->size());
284
285 // Calculate new size for chroma-lightness-diagram
286 const QSizeF widgetSize = optimalChromaLightnessDiagramSize();
287
288 // Calculate new top-left corner position for chroma-lightness-diagram
289 // (relative to parent widget)
290 const qreal radius = m_colorWheel->maximumWidgetSquareSize() / 2.0;
291 const QPointF widgetTopLeftPos(
292 // x position
293 radius - widgetSize.width() / 2.0,
294 // y position:
295 radius - widgetSize.height() / 2.0);
296
297 // Correct the new geometry of chroma-lightness-diagram to fit into
298 // an integer raster.
299 QRectF diagramGeometry(widgetTopLeftPos, widgetSize);
300 // We have to round to full integers, so that our integer-based rectangle
301 // does not exceed the dimensions of the floating-point rectangle.
302 // Round to bigger coordinates for top-left corner:
303 diagramGeometry.setLeft(ceil(diagramGeometry.left()));
304 diagramGeometry.setTop(ceil(diagramGeometry.top()));
305 // Round to smaller coordinates for bottom-right corner:
306 diagramGeometry.setRight(floor(diagramGeometry.right()));
307 diagramGeometry.setBottom(floor(diagramGeometry.bottom()));
308 // TODO The rounding has probably changed the ratio (b ÷ a) of the
309 // diagram itself with the chroma-hue widget. Therefore, maybe a little
310 // bit of gamut is not visible at the right of the diagram. There
311 // might be two possibilities to solve this: Either ChromaLightnessDiagram
312 // gets support for scaling to user-defined maximum chroma (unlikely)
313 // or we implement it here, just by reducing a little bit the height
314 // of the widget until the full gamut gets in (easier).
315
316 // Apply new geometry
317 m_chromaLightnessDiagram->setGeometry(diagramGeometry.toRect());
318}
319
320// No documentation here (documentation of properties
321// and its getters are in the header)
323{
324 return d_pointer->m_chromaLightnessDiagram->currentColor();
325}
326
327/** @brief Setter for the @ref currentColor() property.
328 *
329 * @param newCurrentColor the new color */
331{
332 // The following line will also emit the signal of this class:
333 d_pointer->m_chromaLightnessDiagram->setCurrentColor(newCurrentColor);
334
335 // Avoid that setting the new hue will move the color into gamut.
336 // (As documented, this function accepts happily out-of-gamut colors.)
337 QSignalBlocker myBlocker(d_pointer->m_colorWheel);
338 d_pointer->m_colorWheel->setHue(d_pointer->m_chromaLightnessDiagram->currentColor().h);
339}
340
341/** @brief Recommended size for the widget
342 *
343 * Reimplemented from base class.
344 *
345 * @returns Recommended size for the widget.
346 *
347 * @sa @ref sizeHint() */
349{
350 const QSizeF minimumDiagramSize =
351 // Get the minimum size of the chroma-lightness widget.
352 d_pointer->m_chromaLightnessDiagram->minimumSizeHint()
353 // We have to fit this in a widget pixel raster. But the perfect
354 // position might be between two integer coordinates. We might
355 // have to shift up to 1 pixel at each of the four margins.
356 + QSize(2, 2);
357 const int diameterForMinimumDiagramSize =
358 // The minimum inner diameter of the color wheel has
359 // to be equal (or a little bit bigger) than the
360 // diagonal through the chroma-lightness widget.
361 qCeil(
362 // c = √(a² + b²)
363 qSqrt(qPow(minimumDiagramSize.width(), 2) + qPow(minimumDiagramSize.height(), 2)))
364 // Add size for the color wheel gradient
365 + d_pointer->m_colorWheel->gradientThickness()
366 // Add size for the border around the color wheel gradient
367 + d_pointer->m_colorWheel->d_pointer->border();
368 // Necessary size for this widget so that the diagram fits:
369 const QSize sizeForMinimumDiagramSize(diameterForMinimumDiagramSize, // x
370 diameterForMinimumDiagramSize // y
371 );
372
373 return sizeForMinimumDiagramSize
374 // Expand to the minimumSizeHint() of the color wheel itself
375 .expandedTo(d_pointer->m_colorWheel->minimumSizeHint());
376}
377
378/** @brief Recommended minimum size for the widget.
379 *
380 * Reimplemented from base class.
381 *
382 * @returns Recommended minimum size for the widget.
383 *
384 * @sa @ref minimumSizeHint() */
386{
387 return minimumSizeHint() * scaleFromMinumumSizeHintToSizeHint;
388}
389
390} // namespace PerceptualColor
Base class for LCH diagrams.
A color wheel widget.
Definition colorwheel.h:65
void hueChanged(const qreal newHue)
Notify signal for property hue.
Complete wheel-based color picker widget.
virtual QSize sizeHint() const override
Recommended minimum size for the widget.
void currentColorChanged(const PerceptualColor::LchDouble &newCurrentColor)
Notify signal for property currentColor.
PerceptualColor::LchDouble currentColor
Currently selected color.
virtual ~WheelColorPicker() noexcept override
Default destructor.
void setCurrentColor(const PerceptualColor::LchDouble &newCurrentColor)
Setter for the currentColor() property.
virtual QSize minimumSizeHint() const override
Recommended size for the widget.
Q_INVOKABLE WheelColorPicker(const QSharedPointer< PerceptualColor::RgbColorSpace > &colorSpace, QWidget *parent=nullptr)
Constructor.
virtual void resizeEvent(QResizeEvent *event) override
React on a resize event.
The namespace of this library.
void focusChanged(QWidget *old, QWidget *now)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QSize expandedTo(const QSize &otherSize) const const
qreal height() const const
qreal width() const const
virtual bool event(QEvent *event) override
virtual void resizeEvent(QResizeEvent *event)
A LCH color (Oklch, CielchD50, CielchD65…)
Definition lchdouble.h:50
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:20:36 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.