File indexing completed on 2024-05-12 04:44:36

0001 // SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
0002 // SPDX-License-Identifier: BSD-2-Clause OR MIT
0003 
0004 // Own headers
0005 // First the interface, which forces the header to be self-contained.
0006 #include "wheelcolorpicker.h"
0007 // Second, the private implementation.
0008 #include "wheelcolorpicker_p.h" // IWYU pragma: associated
0009 
0010 #include "abstractdiagram.h"
0011 #include "chromalightnessdiagram.h"
0012 #include "chromalightnessdiagram_p.h" // IWYU pragma: keep // TODO Avoid this pragma by better design: not accessing private parts of other classes.
0013 #include "cielchd50values.h"
0014 #include "colorwheel.h"
0015 #include "colorwheel_p.h" // IWYU pragma: keep // TODO Avoid this pragma by better design: not accessing private parts of other classes.
0016 #include "constpropagatingrawpointer.h"
0017 #include "constpropagatinguniquepointer.h"
0018 #include "helperconstants.h"
0019 #include "lchdouble.h"
0020 #include "rgbcolorspace.h"
0021 #include <math.h>
0022 #include <qapplication.h>
0023 #include <qmath.h>
0024 #include <qobject.h>
0025 #include <qpoint.h>
0026 #include <qpointer.h>
0027 #include <qrect.h>
0028 #include <qsharedpointer.h>
0029 #include <utility>
0030 class QResizeEvent;
0031 class QWidget;
0032 
0033 namespace PerceptualColor
0034 {
0035 /** @brief Constructor
0036  * @param colorSpace The color space within which this widget should operate.
0037  * Can be created with @ref RgbColorSpaceFactory.
0038  * @param parent The widget’s parent widget. This parameter will be passed
0039  * to the base class’s constructor. */
0040 WheelColorPicker::WheelColorPicker(const QSharedPointer<PerceptualColor::RgbColorSpace> &colorSpace, QWidget *parent)
0041     : AbstractDiagram(parent)
0042     , d_pointer(new WheelColorPickerPrivate(this))
0043 {
0044     d_pointer->m_rgbColorSpace = colorSpace;
0045     d_pointer->m_colorWheel = new ColorWheel(colorSpace, this);
0046     d_pointer->m_chromaLightnessDiagram = new ChromaLightnessDiagram(
0047         // Same color space for this widget:
0048         colorSpace,
0049         // This widget is smaller than the color wheel. It will be a child
0050         // of the color wheel, so that missed mouse or key events will be
0051         // forwarded to the parent widget (color wheel).
0052         d_pointer->m_colorWheel);
0053     d_pointer->m_colorWheel->setFocusProxy(d_pointer->m_chromaLightnessDiagram);
0054     d_pointer->resizeChildWidgets();
0055 
0056     connect(
0057         // changes on the color wheel trigger a change in the inner diagram
0058         d_pointer->m_colorWheel,
0059         &ColorWheel::hueChanged,
0060         this,
0061         [this](const qreal newHue) {
0062             LchDouble lch = d_pointer->m_chromaLightnessDiagram->currentColor();
0063             lch.h = newHue;
0064             // We have to be sure that the color is in-gamut also for the
0065             // new hue. If it is not, we adjust it:
0066             lch = d_pointer->m_rgbColorSpace->reduceCielchD50ChromaToFitIntoGamut(lch);
0067             d_pointer->m_chromaLightnessDiagram->setCurrentColor(lch);
0068         });
0069     connect(d_pointer->m_chromaLightnessDiagram,
0070             &ChromaLightnessDiagram::currentColorChanged,
0071             this,
0072             // As value is stored anyway within ChromaLightnessDiagram member,
0073             // it’s enough to just emit the corresponding signal of this class:
0074             &WheelColorPicker::currentColorChanged);
0075     connect(
0076         // QWidget’s constructor requires a QApplication object. As this
0077         // is a class derived from QWidget, calling qApp is safe here.
0078         qApp,
0079         &QApplication::focusChanged,
0080         d_pointer.get(), // Without .get() apparently connect() won’t work…
0081         &WheelColorPickerPrivate::handleFocusChanged);
0082 
0083     // Initial color
0084     setCurrentColor(
0085         // Though CielchD50Values::srgbVersatileInitialColor() is expected to
0086         // be in-gamut, its more secure to guarantee this explicitly:
0087         d_pointer->m_rgbColorSpace->reduceCielchD50ChromaToFitIntoGamut(
0088             // Default sRGB initial color:
0089             CielchD50Values::srgbVersatileInitialColor));
0090 }
0091 
0092 /** @brief Default destructor */
0093 WheelColorPicker::~WheelColorPicker() noexcept
0094 {
0095 }
0096 
0097 /** @brief Constructor
0098  *
0099  * @param backLink Pointer to the object from which <em>this</em> object
0100  * is the private implementation. */
0101 WheelColorPickerPrivate::WheelColorPickerPrivate(WheelColorPicker *backLink)
0102     : q_pointer(backLink)
0103 {
0104 }
0105 
0106 /** Repaint @ref m_colorWheel when focus changes
0107  * on @ref m_chromaLightnessDiagram
0108  *
0109  * @ref m_chromaLightnessDiagram is the focus proxy of @ref m_colorWheel.
0110  * Both show a focus indicator when keyboard focus is active. But
0111  * apparently @ref m_colorWheel does not always repaint when focus
0112  * changes. Therefore, this slot can be connected to the <tt>qApp</tt>’s
0113  * <tt>focusChanged()</tt> signal to make sure that the repaint works.
0114  *
0115  * @note It might be an alternative to write an event filter
0116  * for @ref m_chromaLightnessDiagram to do the same work. The event
0117  * filter could be either @ref WheelColorPicker or
0118  * @ref WheelColorPickerPrivate (the last case means that
0119  * @ref WheelColorPickerPrivate would still have to inherit from
0120  * <tt>QObject</tt>). But that would probably be more complicate… */
0121 void WheelColorPickerPrivate::handleFocusChanged(QWidget *old, QWidget *now)
0122 {
0123     if ((old == m_chromaLightnessDiagram) || (now == m_chromaLightnessDiagram)) {
0124         m_colorWheel->update();
0125     }
0126 }
0127 
0128 /** @brief React on a resize event.
0129  *
0130  * Reimplemented from base class.
0131  *
0132  * @param event The corresponding resize event */
0133 void WheelColorPicker::resizeEvent(QResizeEvent *event)
0134 {
0135     AbstractDiagram::resizeEvent(event);
0136     d_pointer->resizeChildWidgets();
0137 }
0138 
0139 /** @brief Calculate the optimal size for the inner diagram.
0140  *
0141  * @returns The maximum possible size of the diagram within the
0142  * inner part of the color wheel. With floating point precision.
0143  * Measured in <em>device-independent pixels</em>. */
0144 QSizeF WheelColorPickerPrivate::optimalChromaLightnessDiagramSize() const
0145 {
0146     /** The outer dimensions of the widget are a rectangle within a
0147      * circumscribed circled, which is the inner border of the color wheel.
0148      *
0149      * The widget size is composed by the size of the diagram itself and
0150      * the size of the borders. The border size is fixed; only the diagram
0151      * size can vary.
0152      *
0153      * Known variables:
0154      * | variable     | comment                          | value                              |
0155      * | :----------- | :------------------------------- | :--------------------------------- |
0156      * | r            | relation b ÷ a                   | maximum lightness ÷ maximum chroma |
0157      * | h            | horizontal shift                 | left + right diagram border        |
0158      * | v            | vertical shift                   | top + bottom diagram border        |
0159      * | d            | diameter of circumscribed circle | inner diameter of the color wheel  |
0160      * | b            | diagram height                   | a × r                              |
0161      * | widgetWidth  | widget width                     | a + h                              |
0162      * | widgetHeight | widget height                    | b + v                              |
0163      * | a            | diagram width                    | ?                                  |
0164      */
0165     const qreal r = 100.0 / m_rgbColorSpace->profileMaximumCielchD50Chroma();
0166     const qreal h = m_chromaLightnessDiagram->d_pointer->leftBorderPhysical() //
0167         + m_chromaLightnessDiagram->d_pointer->defaultBorderPhysical();
0168     const qreal v = 2 * m_chromaLightnessDiagram->d_pointer->defaultBorderPhysical();
0169     const qreal d = m_colorWheel->d_pointer->innerDiameter();
0170 
0171     /** We can calculate <em>a</em> because right-angled triangle
0172      * with <em>a</em> and with <em>b</em> as legs/catheti will have
0173      * has hypotenuse the diameter of the circumscribed circle:
0174      *
0175      * <em>[The following formula requires a working Internet connection
0176      * to be displayed.]</em>
0177      *
0178      * @f[
0179         \begin{align}
0180             widgetWidth²
0181             + widgetHeight²
0182             = & d²
0183         \\
0184             (a+h)²
0185             + (b+v)²
0186             = & d²
0187         \\
0188             (a+h)²
0189             + (ra+v)²
0190             = & d²
0191         \\
0192 
0193             + 2ah
0194             + h²
0195             + r²a²
0196             + 2rav
0197             + v²
0198             = & d²
0199         \\
0200 
0201             + r²a²
0202             + 2ah
0203             + 2rav
0204             + h²
0205             + v²
0206             = & d²
0207         \\
0208             (1+r²)a²
0209             + 2a(h+rv)
0210             + (h²+v²)
0211             = & d²
0212         \\
0213 
0214             + 2a\frac{h+rv}{1+r²}
0215             + \frac{h²+v²}{1+r²}
0216             = & \frac{d²}{1+r²}
0217         \\
0218 
0219             + 2a\frac{h+rv}{1+r²}
0220             + \left(\frac{h+rv}{1+r²}\right)^{2}
0221             - \left(\frac{h+rv}{1+r²}\right)^{2}
0222             + \frac{h²+v²}{1+r²}
0223             = &  \frac{d²}{1+r²}
0224         \\
0225             \left(a+\frac{h+rv}{1+r²}\right)^{2}
0226             - \left(\frac{h+rv}{1+r²}\right)^{2}
0227             + \frac{h²+v²}{1+r²}
0228             = & \frac{d²}{1+r²}
0229         \\
0230             \left(a+\frac{h+rv}{1+r²}\right)^{2}
0231             = & \frac{d²}{1+r²}
0232             + \left(\frac{h+rv}{1+r²}\right)^{2}
0233             - \frac{h²+v²}{1+r²}
0234         \\
0235             a
0236             + \frac{h+rv}{1+r²}
0237             = & \sqrt{
0238                 \frac{d²}{1+r²}
0239                 + \left(\frac{h+rv}{1+r²}\right)^{2}
0240                 -\frac{h²+v²}{1+r²}
0241             }
0242         \\
0243             a
0244             = & \sqrt{
0245                 \frac{d²}{1+r²}
0246                 + \left(\frac{h+rv}{1+r²}\right)^{2}
0247                 - \frac{h²+v²}{1+r²}
0248             }
0249             - \frac{h+rv}{1+r²}
0250         \end{align}
0251      * @f] */
0252     const qreal x = (1 + qPow(r, 2)); // x = 1 + r²
0253     const qreal a =
0254         // The square root:
0255         qSqrt(
0256             // First fraction:
0257             d * d / x
0258             // Second fraction:
0259             + qPow((h + r * v) / x, 2)
0260             // Thierd fraction:
0261             - (h * h + v * v) / x)
0262         // The part after the square root:
0263         - (h + r * v) / x;
0264     const qreal b = r * a;
0265 
0266     return QSizeF(a + h, // width
0267                   b + v // height
0268     );
0269 }
0270 
0271 /** @brief Update the geometry of the child widgets.
0272  *
0273  * This widget does <em>not</em> use layout management for its child widgets.
0274  * Therefore, this function should be called on all resize events of this
0275  * widget.
0276  *
0277  * @post The geometry (size and the position) of the child widgets are
0278  * adapted according to the current size of <em>this</em> widget itself. */
0279 void WheelColorPickerPrivate::resizeChildWidgets()
0280 {
0281     // Set new geometry of color wheel. Only the size changes, while the
0282     // position (which is 0, 0) remains always unchanged.
0283     m_colorWheel->resize(q_pointer->size());
0284 
0285     // Calculate new size for chroma-lightness-diagram
0286     const QSizeF widgetSize = optimalChromaLightnessDiagramSize();
0287 
0288     // Calculate new top-left corner position for chroma-lightness-diagram
0289     // (relative to parent widget)
0290     const qreal radius = m_colorWheel->maximumWidgetSquareSize() / 2.0;
0291     const QPointF widgetTopLeftPos(
0292         // x position
0293         radius - widgetSize.width() / 2.0,
0294         // y position:
0295         radius - widgetSize.height() / 2.0);
0296 
0297     // Correct the new geometry of chroma-lightness-diagram to fit into
0298     // an integer raster.
0299     QRectF diagramGeometry(widgetTopLeftPos, widgetSize);
0300     // We have to round to full integers, so that our integer-based rectangle
0301     // does not exceed the dimensions of the floating-point rectangle.
0302     // Round to bigger coordinates for top-left corner:
0303     diagramGeometry.setLeft(ceil(diagramGeometry.left()));
0304     diagramGeometry.setTop(ceil(diagramGeometry.top()));
0305     // Round to smaller coordinates for bottom-right corner:
0306     diagramGeometry.setRight(floor(diagramGeometry.right()));
0307     diagramGeometry.setBottom(floor(diagramGeometry.bottom()));
0308     // TODO The rounding has probably changed the ratio (b ÷ a) of the
0309     // diagram itself with the chroma-hue widget. Therefore, maybe a little
0310     // bit of gamut is not visible at the right of the diagram. There
0311     // might be two possibilities to solve this: Either ChromaLightnessDiagram
0312     // gets support for scaling to user-defined maximum chroma (unlikely)
0313     // or we implement it here, just by reducing a little bit the height
0314     // of the widget until the full gamut gets in (easier).
0315 
0316     // Apply new geometry
0317     m_chromaLightnessDiagram->setGeometry(diagramGeometry.toRect());
0318 }
0319 
0320 // No documentation here (documentation of properties
0321 // and its getters are in the header)
0322 LchDouble WheelColorPicker::currentColor() const
0323 {
0324     return d_pointer->m_chromaLightnessDiagram->currentColor();
0325 }
0326 
0327 /** @brief Setter for the @ref currentColor() property.
0328  *
0329  * @param newCurrentColor the new color */
0330 void WheelColorPicker::setCurrentColor(const LchDouble &newCurrentColor)
0331 {
0332     // The following line will also emit the signal of this class:
0333     d_pointer->m_chromaLightnessDiagram->setCurrentColor(newCurrentColor);
0334 
0335     // Avoid that setting the new hue will move the color into gamut.
0336     // (As documented, this function accepts happily out-of-gamut colors.)
0337     QSignalBlocker myBlocker(d_pointer->m_colorWheel);
0338     d_pointer->m_colorWheel->setHue(d_pointer->m_chromaLightnessDiagram->currentColor().h);
0339 }
0340 
0341 /** @brief Recommended size for the widget
0342  *
0343  * Reimplemented from base class.
0344  *
0345  * @returns Recommended size for the widget.
0346  *
0347  * @sa @ref sizeHint() */
0348 QSize WheelColorPicker::minimumSizeHint() const
0349 {
0350     const QSizeF minimumDiagramSize =
0351         // Get the minimum size of the chroma-lightness widget.
0352         d_pointer->m_chromaLightnessDiagram->minimumSizeHint()
0353         // We have to fit this in a widget pixel raster. But the perfect
0354         // position might be between two integer coordinates. We might
0355         // have to shift up to 1 pixel at each of the four margins.
0356         + QSize(2, 2);
0357     const int diameterForMinimumDiagramSize =
0358         // The minimum inner diameter of the color wheel has
0359         // to be equal (or a little bit bigger) than the
0360         // diagonal through the chroma-lightness widget.
0361         qCeil(
0362             // c = √(a² + b²)
0363             qSqrt(qPow(minimumDiagramSize.width(), 2) + qPow(minimumDiagramSize.height(), 2)))
0364         // Add size for the color wheel gradient
0365         + d_pointer->m_colorWheel->gradientThickness()
0366         // Add size for the border around the color wheel gradient
0367         + d_pointer->m_colorWheel->d_pointer->border();
0368     // Necessary size for this widget so that the diagram fits:
0369     const QSize sizeForMinimumDiagramSize(diameterForMinimumDiagramSize, // x
0370                                           diameterForMinimumDiagramSize // y
0371     );
0372 
0373     return sizeForMinimumDiagramSize
0374         // Expand to the minimumSizeHint() of the color wheel itself
0375         .expandedTo(d_pointer->m_colorWheel->minimumSizeHint());
0376 }
0377 
0378 /** @brief Recommended minimum size for the widget.
0379  *
0380  * Reimplemented from base class.
0381  *
0382  * @returns Recommended minimum size for the widget.
0383  *
0384  * @sa @ref minimumSizeHint() */
0385 QSize WheelColorPicker::sizeHint() const
0386 {
0387     return minimumSizeHint() * scaleFromMinumumSizeHintToSizeHint;
0388 }
0389 
0390 } // namespace PerceptualColor