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

KDE's Doxygen guidelines are available online.