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 a² 0193 + 2ah 0194 + h² 0195 + r²a² 0196 + 2rav 0197 + v² 0198 = & d² 0199 \\ 0200 a² 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 a² 0214 + 2a\frac{h+rv}{1+r²} 0215 + \frac{h²+v²}{1+r²} 0216 = & \frac{d²}{1+r²} 0217 \\ 0218 a² 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