Perceptual Color

colordialog.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 "colordialog.h"
7// Second, the private implementation.
8#include "colordialog_p.h" // IWYU pragma: associated
9
10#include "absolutecolor.h"
11#include "chromahuediagram.h"
12#include "cielchd50values.h"
13#include "colorpatch.h"
14#include "constpropagatingrawpointer.h"
15#include "constpropagatinguniquepointer.h"
16#include "gradientslider.h"
17#include "helper.h"
18#include "helperconstants.h"
19#include "helperconversion.h"
20#include "helperqttypes.h"
21#include "initializetranslation.h"
22#include "multispinbox.h"
23#include "multispinboxsection.h"
24#include "oklchvalues.h"
25#include "rgbcolor.h"
26#include "rgbcolorspace.h"
27#include "rgbcolorspacefactory.h"
28#include "screencolorpicker.h"
29#include "setting.h"
30#include "swatchbook.h"
31#include "wheelcolorpicker.h"
32#include <algorithm>
33#include <lcms2.h>
34#include <optional>
35#include <qaction.h>
36#include <qapplication.h>
37#include <qboxlayout.h>
38#include <qbytearray.h>
39#include <qchar.h>
40#include <qcombobox.h>
41#include <qcoreapplication.h>
42#include <qcoreevent.h>
43#include <qdatetime.h>
44#include <qdebug.h>
45#include <qdialogbuttonbox.h>
46#include <qfontmetrics.h>
47#include <qformlayout.h>
48#include <qgridlayout.h>
49#include <qgroupbox.h>
50#include <qguiapplication.h>
51#include <qicon.h>
52#include <qkeysequence.h>
53#include <qlabel.h>
54#include <qlineedit.h>
55#include <qlist.h>
56#include <qlocale.h>
57#include <qobject.h>
58#include <qpair.h>
59#include <qpointer.h>
60#include <qpushbutton.h>
61#include <qregularexpression.h>
62#include <qscopedpointer.h>
63#include <qscreen.h>
64#include <qsharedpointer.h>
65#include <qshortcut.h>
66#include <qsize.h>
67#include <qsizepolicy.h>
68#include <qspinbox.h>
69#include <qstackedlayout.h>
70#include <qstringbuilder.h>
71#include <qstringliteral.h>
72#include <qtabwidget.h>
73#include <qtoolbutton.h>
74#include <qvalidator.h>
75#include <qversionnumber.h>
76#include <qwidget.h>
77#include <utility>
78class QShowEvent;
79
80#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
81#include <qcontainerfwd.h>
82#include <qobjectdefs.h>
83#else
84#include <qstringlist.h>
85#endif
86
87#if (QT_VERSION >= QT_VERSION_CHECK(6, 5, 0))
88#include <qstylehints.h>
89#endif
90
91namespace PerceptualColor
92{
93
94/** @brief A text with the name of the color model.
95 *
96 * @param model The signature of the color model.
97 *
98 * @returns A text with the name of the color model, or an empty
99 * QString if the model is unknown. If a translation is available,
100 * the translation is returned instead of the original English text. */
101QString ColorDialogPrivate::translateColorModel(cmsColorSpaceSignature model)
102{
103 switch (model) {
104 case cmsSigXYZData:
105 /*: @item A color model: X, Y, Z. */
106 return tr("XYZ");
107 case cmsSigLabData:
108 /*: @item A color model: Lightness, a, b. */
109 return tr("Lab");
110 case cmsSigRgbData:
111 /*: @item A color model: red, green, blue. */
112 return tr("RGB");
113 case cmsSigLuvData:
114 // return tr("Luv"); // Currently not supported.
115 return QString();
116 case cmsSigYCbCrData:
117 // return tr("YCbCr"); // Currently not supported.
118 return QString();
119 case cmsSigYxyData:
120 // return tr("Yxy"); // Currently not supported.
121 return QString();
122 case cmsSigGrayData:
123 // return tr("Grayscale"); // Currently not supported.
124 return QString();
125 case cmsSigHsvData:
126 // return tr("HSV"); // Currently not supported.
127 return QString();
128 case cmsSigHlsData:
129 // return tr("HSL"); // Currently not supported.
130 return QString();
131 case cmsSigCmykData:
132 // return tr("CMYK"); // Currently not supported.
133 return QString();
134 case cmsSigCmyData:
135 // return tr("CMY"); // Currently not supported.
136 return QString();
137 case cmsSigNamedData: // Does not exist in ICC 4.4.
138 case cmsSig2colorData:
139 case cmsSig3colorData:
140 case cmsSig4colorData:
141 case cmsSig5colorData:
142 case cmsSig6colorData:
143 case cmsSig7colorData:
144 case cmsSig8colorData:
145 case cmsSig9colorData:
146 case cmsSig10colorData:
147 case cmsSig11colorData:
148 case cmsSig12colorData:
149 case cmsSig13colorData:
150 case cmsSig14colorData:
151 case cmsSig15colorData:
152 // return tr("Named color"); // Currently not supported.
153 return QString();
154 case cmsSig1colorData:
155 case cmsSigLuvKData:
156 case cmsSigMCH1Data:
157 case cmsSigMCH2Data:
158 case cmsSigMCH3Data:
159 case cmsSigMCH4Data:
160 case cmsSigMCH5Data:
161 case cmsSigMCH6Data:
162 case cmsSigMCH7Data:
163 case cmsSigMCH8Data:
164 case cmsSigMCH9Data:
165 case cmsSigMCHAData:
166 case cmsSigMCHBData:
167 case cmsSigMCHCData:
168 case cmsSigMCHDData:
169 case cmsSigMCHEData:
170 case cmsSigMCHFData:
171 // Unhandeled: These values do not exist in ICC 4.4 standard as
172 // published at https://www.color.org/specification/ICC.1-2022-05.pdf
173 // page 35, table 19 — Data colour space signatures.
174 default:
175 break;
176 }
177 return QString();
178}
179
180/** @brief Retranslate the UI with all user-visible strings.
181 *
182 * This function updates all user-visible strings by using
183 * <tt>Qt::tr()</tt> to get up-to-date translations.
184 *
185 * This function is meant to be called at the end of the constructor and
186 * additionally after each <tt>QEvent::LanguageChange</tt> event.
187 *
188 * @note This is the same concept as
189 * <a href="https://doc.qt.io/qt-5/designer-using-a-ui-file.html">
190 * Qt Designer, which also provides a function of the same name in
191 * uic-generated code</a>.
192 *
193 * @internal
194 *
195 * @todo Add to the color-space tooltip information about available rendering
196 * intents (we have yet RgbColorSpacePrivate::intentList but do not use it
197 * anywhere) and the RGB profile illuminant? (This would have to be implemented
198 * in @ref RgbColorSpace first.)
199 *
200 * @todo As the tooltip for color-space information is quite big, would
201 * it be better to do what systemsettings does in globaldesign/fonts? They
202 * have a small button with an “i” symbol (for information), which does
203 * nothing when it’s clicked, but when hovering with the mouse, it shows
204 * the tooltip?
205 *
206 * @todo How to make tooltip information available for touch-screen users?
207 *
208 * @todo Provide the information
209 * of @ref RgbColorSpace::profileRenderingIntentDirections() in the tooltip of the
210 * color space. Example: Rendering intents: Perceptual (input, output, proof)
211 * [br] Relative (input, proof) [br] Saturation (output) [br]
212 * Absolute colorimetric (proof). Missing rendering intents are not displayed.
213 * Limiting ourself to these four standard rendering intents, leaving LittleCMS
214 * extensions out of scope. Providing i18n instead of using the rendering
215 * intent descriptions of LittleCMS.
216 */
217void ColorDialogPrivate::retranslateUi()
218{
219 /*: @item/plain Percentage value in a spinbox. Range: 0%–100%. */
220 const QString percentageInSpinbox = tr("%1%");
221
222 /*: @item/plain Arc-degree value in a spinbox. Range: 0°–360°. */
223 const QString arcDegreeInSpinbox = tr("%1°");
224
225 QStringList profileInfo;
226 const QString name = //
227 m_rgbColorSpace->profileName().toHtmlEscaped();
228 if (!name.isEmpty()) {
229 /*: @item:intext An information from the color profile to be added
230 to the info text about current color space. */
231 profileInfo.append(tableRow.arg(tr("Name:"), name));
232 }
233 /*: @item:intext The maximum chroma. */
234 const QString maximumCielchD50Chroma = //
235 tr("%L1 (estimated)")
236 .arg(m_rgbColorSpace->profileMaximumCielchD50Chroma(), //
237 0, //
238 'f', //
239 decimals);
240 /*: @item:intext An information from the color profile to be added
241 to the info text about current color space. */
242 profileInfo.append( //
243 tableRow.arg(tr("Maximum CIELCh-D50 chroma:"), maximumCielchD50Chroma));
244 /*: @item:intext The maximum chroma. */
245 const QString maximumOklchChroma = //
246 tr("%L1 (estimated)")
247 .arg(m_rgbColorSpace->profileMaximumOklchChroma(), //
248 0, //
249 'f', //
250 okdecimals);
251 /*: @item:intext An information from the color profile to be added
252 to the info text about current color space. */
253 profileInfo.append( //
254 tableRow.arg(tr("Maximum Oklch chroma:"), maximumOklchChroma));
255 QString profileClass;
256 switch (m_rgbColorSpace->profileClass()) {
257 case cmsSigDisplayClass:
258 /*: @item:intext The class of an ICC profile. */
259 profileClass = tr("Display profile");
260 break;
261 case cmsSigAbstractClass: // Image effect profile (Abstract profile)
262 // This ICC profile class is called "abstract
263 // profile" in the official standard. However,
264 // the name is misleading. The actual function of
265 // these ICC profiles is to apply image effects.
266 case cmsSigColorSpaceClass: // Color space conversion profile
267 case cmsSigInputClass: // Input profile
268 case cmsSigLinkClass: // Device link profile
269 case cmsSigNamedColorClass: // Named color profile
270 case cmsSigOutputClass: // Output profile
271 // These profile classes are currently not supported.
272 break;
273 }
274 if (!profileClass.isEmpty()) {
275 /*: @item:intext An information from the color profile to be added
276 to the info text about current color space. */
277 profileInfo.append( //
278 tableRow.arg(tr("Profile class:"), profileClass));
279 }
280 const QString colorModel = //
281 translateColorModel(m_rgbColorSpace->profileColorModel());
282 if (!colorModel.isEmpty()) {
283 /*: @item:intext An information from the color profile to be added
284 to the info text about current color space.
285 The color model of the color space which is described by this
286 profile. */
287 profileInfo.append(tableRow.arg(tr("Color model:"), colorModel));
288 }
289 const QString manufacturer = //
290 m_rgbColorSpace->profileManufacturer().toHtmlEscaped();
291 if (!manufacturer.isEmpty()) {
292 /*: @item:intext An information from the color profile to be added
293 to the info text about current color space.
294 This is usually the manufacturer of the device to which
295 the colour profile applies. */
296 profileInfo.append(tableRow.arg(tr("Manufacturer:"), manufacturer));
297 }
298 const QString model = //
299 m_rgbColorSpace->profileModel().toHtmlEscaped();
300 if (!model.isEmpty()) {
301 /*: @item:intext An information from the color profile to be added to
302 the info text about current color space.
303 This is usually the model identifier of the device to which
304 the colour profile applies. */
305 profileInfo.append(tableRow.arg(tr("Device model:"), (model)));
306 }
307 const QDateTime creationDateTime = //
308 m_rgbColorSpace->profileCreationDateTime();
309 if (!creationDateTime.isNull()) {
310 const auto creationDateTimeString = QLocale().toString(
311 // Date and time:
312 creationDateTime,
313 // Format:
315 /*: @item:intext An information from the color profile to be added to
316 the info text about current color space.
317 This is the date and time of the creation of the profile. */
318 profileInfo.append( //
319 tableRow.arg(tr("Created:"), (creationDateTimeString)));
320 }
321 const QVersionNumber iccVersion = m_rgbColorSpace->profileIccVersion();
322 /*: @item:intext An information from the color profile to be added to
323 the info text about current color space.
324 This is the version number of the ICC file format that is used. */
325 profileInfo.append( //
326 tableRow.arg(tr("ICC format:"), (iccVersion.toString())));
327 const bool hasMatrixShaper = //
328 m_rgbColorSpace->profileHasMatrixShaper();
329 const bool hasClut = //
330 m_rgbColorSpace->profileHasClut();
331 if (hasMatrixShaper || hasClut) {
332 const QString matrixShaperString = tableRow.arg(
333 /*: @item:intext An information from the color profile to be added
334 to the info text about current color space.
335 Wether the profile has a matrix shaper or a color lookup table
336 (CLUT) or both. */
337 tr("Implementation:"));
338 if (hasMatrixShaper && hasClut) {
339 /*: @item:intext An information from the color profile to be added
340 to the info text about current color space.
341 Wether the profile has a matrix shaper or a color lookup table
342 (CLUT) or both. */
343 profileInfo.append( //
344 matrixShaperString.arg(tr("Matrices and color lookup tables")));
345 } else if (hasMatrixShaper) {
346 /*: @item:intext An information from the color profile to be added
347 to the info text about current color space.
348 Wether the profile has a matrix shaper or a color lookup table
349 (CLUT) or both. */
350 profileInfo.append(matrixShaperString.arg(tr("Matrices")));
351 } else if (hasClut) {
352 /*: @item:intext An information from the color profile to be added
353 to the info text about current color space.
354 Wether the profile has a matrix shaper or a color lookup table
355 (CLUT) or both. */
356 profileInfo.append( //
357 matrixShaperString.arg(tr("Color lookup tables")));
358 }
359 }
360 const QString pcsColorModelText = //
361 translateColorModel(m_rgbColorSpace->profilePcsColorModel());
362 if (!pcsColorModelText.isEmpty()) {
363 /*: @item:intext An information from the color profile to be added
364 to the info text about current color space.
365 The color model of the PCS (profile connection space) which is used
366 internally by this profile. */
367 profileInfo.append( //
368 tableRow.arg(tr("PCS color model:"), pcsColorModelText));
369 }
370 const QString copyright = m_rgbColorSpace->profileCopyright();
371 if (!copyright.isEmpty()) {
372 /*: @item:intext An information from the color profile to be added
373 to the info text about current color space.
374 The copyright of this profile. */
375 profileInfo.append(tableRow.arg(tr("Copyright:"), copyright));
376 }
377 const qint64 fileSize = //
378 m_rgbColorSpace->profileFileSize();
379 if (fileSize >= 0) {
380 /*: @item:intext An information from the color profile to be added to
381 the info text about current color space.
382 This is the size of the ICC file that was read in. */
383 profileInfo.append(tableRow.arg(tr("File size:"), //
384 QLocale().formattedDataSize(fileSize)));
385 }
386 const QString fileName = //
387 m_rgbColorSpace->profileAbsoluteFilePath();
388 if (!fileName.isEmpty()) {
389 /*: @item:intext An information from the color profile to be added to
390 the info text about current color space. */
391 profileInfo.append(tableRow.arg(tr("File name:"), fileName));
392 }
393 if (profileInfo.isEmpty()) {
394 m_rgbGroupBox->setToolTip(QString());
395 } else {
396 const QString tableString = QStringLiteral(
397 "<b>%1</b><br/>"
398 "<table border=\"0\" cellpadding=\"2\" cellspacing=\"0\">"
399 "%2"
400 "</table>");
401 m_rgbGroupBox->setToolTip(richTextMarker
402 + tableString.arg(
403 /*: @info:intext Title of info text about
404 current color space (will be followed by
405 other information as available
406 in the color profile. */
407 tr("Color space information"), //
408 profileInfo.join(QString())));
409 }
410
411 /*: @label:spinbox Label for CIE’s CIELCH color model, based on Lightness,
412 * Chroma and Hue, and using the D50 illuminant as white point.*/
413 m_cielchD50SpinBoxLabel->setText(tr("&CIELCH D50:"));
414
415 /*: @label:spinbox Label for Oklch color model, based on Lightness, Chroma,
416 Hue, and using the D65 illuminant as white point. */
417 m_oklchSpinBoxLabel->setText(tr("O&klch:"));
418
419 /*: @label:spinbox Label for RGB color model, based on Red, Green, Blue. */
420 m_rgbSpinBoxLabel->setText(tr("&RGB:"));
421
422 /*: @label:textbox Label for hexadecimal RGB representation like #12ab45 */
423 m_rgbLineEditLabel->setText(tr("He&x:"));
424
425 const int swatchBookIndex = m_tabWidget->indexOf(m_swatchBookWrapperWidget);
426 if (swatchBookIndex >= 0) {
427 /*: @title:tab
428 The tab contains swatch books showing colors. */
429 const auto mnemonic = tr("&Swatch book");
430 m_tabWidget->setTabToolTip( //
431 swatchBookIndex, //
432 richTextMarker + fromMnemonicToRichText(mnemonic));
433 m_swatchBookTabShortcut->setKey(QKeySequence::mnemonic(mnemonic));
434 }
435
436 const int hueFirstIndex = m_tabWidget->indexOf(m_hueFirstWrapperWidget);
437 if (hueFirstIndex >= 0) {
438 /*: @title:tab
439 The tab contains a visual UI to choose first the hue, and in a
440 second step chroma and lightness. */
441 const auto mnemonic = tr("&Hue-based");
442 m_tabWidget->setTabToolTip( //
443 hueFirstIndex, //
444 richTextMarker + fromMnemonicToRichText(mnemonic));
445 m_hueFirstTabShortcut->setKey(QKeySequence::mnemonic(mnemonic));
446 }
447 const int lightnessFirstIndex = //
448 m_tabWidget->indexOf(m_lightnessFirstWrapperWidget);
449 if (lightnessFirstIndex >= 0) {
450 /*: @title:tab
451 The tab contains a visual UI to choose first the lightness, and in a
452 second step chroma and hue.
453 “Lightness” is different from “brightness”/“value”
454 and should therefore get a different translation. */
455 const auto mnemonic = tr("&Lightness-based");
456 m_tabWidget->setTabToolTip( //
457 lightnessFirstIndex, //
458 richTextMarker + fromMnemonicToRichText(mnemonic));
459 m_lightnessFirstTabShortcut->setKey(QKeySequence::mnemonic(mnemonic));
460 }
461 const int numericIndex = //
462 m_tabWidget->indexOf(m_numericalWidget);
463 if (numericIndex >= 0) {
464 /*: @title:tab
465 The tab contains a UI to describe the color with numbers: Spin boxes
466 and line edits containing values like “#2A7845” or “RGB 85 45 12”. */
467 const auto mnemonic = tr("&Numeric");
468 m_tabWidget->setTabToolTip( //
469 numericIndex, //
470 richTextMarker + fromMnemonicToRichText(mnemonic));
471 m_numericalTabShortcut->setKey(QKeySequence::mnemonic(mnemonic));
472 }
473
474 /*: @label:spinbox HSL (hue, saturation, lightness) */
475 m_hslSpinBoxLabel->setText(tr("HS&L:"));
476
477 /*: @label:spinbox HSV (hue, saturation, value) and HSB (hue, saturation,
478 brightness) are two different names for the very same color model. */
479 m_hsvSpinBoxLabel->setText(tr("HS&V/HSB:"));
480
481 /*: @label:spinbox HWB (hue, whiteness, blackness) */
482 m_hwbSpinBoxLabel->setText(tr("H&WB:"));
483
484 /*: @action:button */
485 m_buttonOK->setText(tr("&OK"));
486
487 /*: @action:button */
488 m_buttonCancel->setText(tr("&Cancel"));
489 /*: @info:tooltip Help text for RGB spinbox. */
490 m_rgbSpinBox->setToolTip( //
491 richTextMarker
492 + tr("<p>Red: 0⁠–⁠255</p>"
493 "<p>Green: 0⁠–⁠255</p>"
494 "<p>Blue: 0⁠–⁠255</p>"));
495
496 /*: @info:tooltip Help text for hexadecimal code. */
497 m_rgbLineEdit->setToolTip( //
498 richTextMarker
499 + tr("<p>Hexadecimal color code, as used in HTML: #RRGGBB</p>"
500 "<p>RR: two-digit code for red: 00⁠–⁠FF</p>"
501 "<p>GG: two-digit code for green: 00⁠–⁠FF</p>"
502 "<p>BB: two-digit code for blue: 00⁠–⁠FF</p>"));
503
504 /*: @info:tooltip Help text for HSL (hue, saturation, lightness).
505 Saturation: 0 means something on the grey axis; 255 means something
506 between the grey axis and the most colorful color. This is different
507 from “chroma” and should therefore get a different translation.
508 Lightness: 0 means always black; 255 means always white. This is
509 different from “brightness” and should therefore get a different
510 translation. */
511 m_hslSpinBox->setToolTip(richTextMarker
512 + tr("<p>Hue: 0°⁠–⁠360°</p>"
513 "<p>HSL-Saturation: 0%⁠–⁠100%</p>"
514 "<p>Lightness: 0%⁠–⁠100%</p>"));
515
516 /*: @info:tooltip Help text for HWB (hue, whiteness, blackness).
517 The idea behind is that the hue defines the pure (maximum colorful) color.
518 Than, white color can be added, creating a “tint”. Or black color
519 can be added, creating a “shade”. Or both can be added, creating a “tone“.
520 See https://en.wikipedia.org/wiki/Tint,_shade_and_tone for more
521 information. 0% white + 0% black = pure color. 100% white
522 + 0% black = white. 0% white + 100% black = black. 50% white + 50% black
523 = gray. 50% white + 0% black = tint. 25% white + 25% black = tone.
524 0% white + 50% black = shade. */
525 m_hwbSpinBox->setToolTip(richTextMarker
526 + tr("<p>Hue: 0°⁠–⁠360°</p>"
527 "<p>Whiteness: 0%⁠–⁠100%</p>"
528 "<p>Blackness: 0%⁠–⁠100%</p>"));
529
530 /*: @info:tooltip Help text for HSV/HSB. HSV (hue, saturation, value)
531 and HSB (hue, saturation, brightness) are two different names for the
532 very same color model. Saturation: 0 means something between black and
533 white; 255 means something between black and the most colorful color.
534 This is different from “chroma” and should therefore get a different
535 translation. Brightness/value: 0 means always black; 255 means something
536 between white and the most colorful color. This is different from
537 “lightness” and should therefore get a different translation. */
538 m_hsvSpinBox->setToolTip(richTextMarker
539 + tr("<p>Hue: 0°⁠–⁠360°</p>"
540 "<p>HSV/HSB-Saturation: 0%⁠–⁠100%</p>"
541 "<p>Brightness/Value: 0%⁠–⁠100%</p>"));
542
543 const auto myPercentagePrefixSuffix = getPrefixSuffix(percentageInSpinbox);
544 m_alphaSpinBox->setPrefix(myPercentagePrefixSuffix.first);
545 m_alphaSpinBox->setSuffix(myPercentagePrefixSuffix.second);
546
547 /*: @label:slider Accessible name for lightness slider. This is different
548 from “brightness”/“value” and should therefore get a different
549 translation. */
550 m_lchLightnessSelector->setAccessibleName(tr("Lightness"));
551
552 /*: @info:tooltip Help text for CIELCH. “lightness” is different from
553 “brightness”/“value” and should therefore get a different translation. */
554 m_cielchD50SpinBox->setToolTip(richTextMarker
555 + tr("<p>Lightness: 0%⁠–⁠100%</p>"
556 "<p>Chroma: 0⁠–⁠%L1</p>"
557 "<p>Hue: 0°⁠–⁠360°</p>")
558 .arg(CielchD50Values::maximumChroma));
559
560 constexpr double maxOklchChroma = OklchValues::maximumChroma;
561 /*: @info:tooltip Help text for Oklch. “lightness” is different from
562 “brightness”/“value” and should therefore get a different translation. */
563 m_oklchSpinBox->setToolTip(richTextMarker
564 + tr("<p>Lightness: %L1⁠–⁠%L2</p>"
565 "<p>Chroma: %L3⁠–⁠%L4</p>"
566 "<p>Hue: 0°⁠–⁠360°</p>"
567 "<p>Whitepoint: D65</p>")
568 .arg(0., 0, 'f', okdecimals)
569 .arg(1., 0, 'f', okdecimals)
570 .arg(0., 0, 'f', okdecimals)
571 .arg(maxOklchChroma, 0, 'f', okdecimals));
572
573 /*: @label:slider An opacity of 0 means completely
574 transparent. The higher the opacity value increases, the
575 more opaque the colour becomes, until it finally becomes
576 completely opaque at the highest possible opacity value. */
577 const QString opacityLabel = tr("Op&acity:");
578 m_alphaGradientSlider->setAccessibleName(opacityLabel);
579 m_alphaLabel->setText(opacityLabel);
580
581 // HSL spin box
582 QList<MultiSpinBoxSection> hslSections = //
583 m_hslSpinBox->sectionConfigurations();
584 if (hslSections.count() != 3) {
585 qWarning() //
586 << "Expected 3 sections in HSV MultiSpinBox, but got" //
587 << hslSections.count() //
588 << "instead. This is a bug in libperceptualcolor.";
589 } else {
590 hslSections[0].setFormatString( //
591 arcDegreeInSpinbox + m_multispinboxSectionSeparator);
592 hslSections[1].setFormatString( //
593 m_multispinboxSectionSeparator + percentageInSpinbox //
594 + m_multispinboxSectionSeparator);
595 hslSections[2].setFormatString( //
596 m_multispinboxSectionSeparator + percentageInSpinbox);
597 m_hslSpinBox->setSectionConfigurations(hslSections);
598 }
599
600 // HWB spin box
601 QList<MultiSpinBoxSection> hwbSections = //
602 m_hwbSpinBox->sectionConfigurations();
603 if (hwbSections.count() != 3) {
604 qWarning() //
605 << "Expected 3 sections in HSV MultiSpinBox, but got" //
606 << hwbSections.count() //
607 << "instead. This is a bug in libperceptualcolor.";
608 } else {
609 hwbSections[0].setFormatString( //
610 arcDegreeInSpinbox + m_multispinboxSectionSeparator);
611 hwbSections[1].setFormatString( //
612 m_multispinboxSectionSeparator + percentageInSpinbox //
613 + m_multispinboxSectionSeparator);
614 hwbSections[2].setFormatString( //
615 m_multispinboxSectionSeparator + percentageInSpinbox);
616 m_hwbSpinBox->setSectionConfigurations(hwbSections);
617 }
618
619 // HSV spin box
620 QList<MultiSpinBoxSection> hsvSections = //
621 m_hsvSpinBox->sectionConfigurations();
622 if (hsvSections.count() != 3) {
623 qWarning() //
624 << "Expected 3 sections in HSV MultiSpinBox, but got" //
625 << hsvSections.count() //
626 << "instead. This is a bug in libperceptualcolor.";
627 } else {
628 hsvSections[0].setFormatString( //
629 arcDegreeInSpinbox + m_multispinboxSectionSeparator);
630 hsvSections[1].setFormatString( //
631 m_multispinboxSectionSeparator //
632 + percentageInSpinbox //
633 + m_multispinboxSectionSeparator);
634 hsvSections[2].setFormatString( //
635 m_multispinboxSectionSeparator + percentageInSpinbox);
636 m_hsvSpinBox->setSectionConfigurations(hsvSections);
637 }
638
639 // CIELCH-D50 spin box
640 QList<MultiSpinBoxSection> cielchD50Sections = //
641 m_cielchD50SpinBox->sectionConfigurations();
642 if (cielchD50Sections.count() != 3) {
643 qWarning() //
644 << "Expected 3 sections in CIELCH-D50 MultiSpinBox, but got" //
645 << cielchD50Sections.count() //
646 << "instead. This is a bug in libperceptualcolor.";
647 } else {
648 cielchD50Sections[0].setFormatString( //
649 percentageInSpinbox + m_multispinboxSectionSeparator);
650 cielchD50Sections[1].setFormatString( //
651 m_multispinboxSectionSeparator //
652 + QStringLiteral("%1") //
653 + m_multispinboxSectionSeparator);
654 cielchD50Sections[2].setFormatString( //
655 m_multispinboxSectionSeparator + arcDegreeInSpinbox);
656 m_cielchD50SpinBox->setSectionConfigurations(cielchD50Sections);
657 }
658
659 // Oklch spin box
660 QList<MultiSpinBoxSection> oklchSections = //
661 m_oklchSpinBox->sectionConfigurations();
662 if (oklchSections.count() != 3) {
663 qWarning() //
664 << "Expected 3 sections in Oklch MultiSpinBox, but got" //
665 << oklchSections.count() //
666 << "instead. This is a bug in libperceptualcolor.";
667 } else {
668 oklchSections[0].setFormatString( //
669 QStringLiteral("%1") + m_multispinboxSectionSeparator);
670 oklchSections[1].setFormatString( //
671 m_multispinboxSectionSeparator + QStringLiteral("%1") //
672 + m_multispinboxSectionSeparator);
673 oklchSections[2].setFormatString( //
674 m_multispinboxSectionSeparator + arcDegreeInSpinbox);
675 m_oklchSpinBox->setSectionConfigurations(oklchSections);
676 }
677
678 if (m_screenColorPickerButton) {
679 /*: @action:button (eye dropper/pipette).
680 A click on the button transforms the mouse cursor to a cross and lets
681 the user choose a color from the screen by doing a left-click.
682 Same text as in QColorDialog */
683 const auto mnemonic = tr("&Pick screen color");
684 m_screenColorPickerButton->setToolTip( //
685 richTextMarker + fromMnemonicToRichText(mnemonic));
686 m_screenColorPickerButton->setShortcut( //
687 QKeySequence::mnemonic(mnemonic));
688 }
689
690 /*: @info:tooltip Tooltip for the gamut-correction action.
691 The icon for this action is only visible in the UI while the
692 color value within the corresponding spinbox is an out-of-gamut
693 value. A click on the icon will change the spinbox’s values to
694 the nearest in-gamut color (and make the icon disappear). */
695 const auto gamutMnemonic = //
696 tr("Click to snap to nearest in-&gamut color");
697 const QString gamutTooltip = //
698 richTextMarker + fromMnemonicToRichText(gamutMnemonic);
699 const auto gamutShortcut = QKeySequence::mnemonic(gamutMnemonic);
700 m_cielchD50SpinBoxGamutAction->setToolTip(gamutTooltip);
701 m_cielchD50SpinBoxGamutAction->setShortcut(gamutShortcut);
702 m_oklchSpinBoxGamutAction->setToolTip(gamutTooltip);
703 m_oklchSpinBoxGamutAction->setShortcut(gamutShortcut);
704
705 /*: @item:inlistbox
706 The swatch grid showing the basic colors like yellow,
707 orange, red… Same text as in QColorDialog */
708 m_swatchBookSelector->setItemText(0, tr("Basic colors")); // TODO xxx Short cut? /plain or /richtext or how is the correct context marker?
709
710 /*: @item:inlistbox
711 The swatch grid showing the history of
712 previously selected colors. */
713 m_swatchBookSelector->setItemText(1, tr("History")); // TODO xxx Short cut? /plain or /richtext or how is the correct context marker?
714
715 /*: @item:inlistbox
716 The swatch grid showing custom colors, allowing users to add or remove
717 colors as desired. */
718 m_swatchBookSelector->setItemText(2, tr("Custom Colors")); // TODO xxx Short cut? /plain or /richtext or how is the correct context marker?
719
720 // NOTE No need to call
721 //
722 // q_pointer->adjustSize();
723 //
724 // because our layout adopts automatically to the
725 // new size of the strings. Indeed, calling
726 //
727 // q_pointer->adjustSize();
728 //
729 // would change the height (!) of the widget: While it might seem
730 // reasonable that the width changes when the strings change, the
731 // height should not. We didn’t find the reason and didn’t manage
732 // to reproduce this behaviour within the unit tests. But anyway
733 // the call is not necessary, as mentioned earlier.
734}
735
736/** @brief Reloads all icons, adapting to the current color schema and
737 * widget style. */
738void ColorDialogPrivate::reloadIcons()
739{
740 m_currentIconThemeType = guessColorSchemeTypeFromWidget(q_pointer);
741
742 static const QStringList swatchBookIcons //
743 {QStringLiteral("paint-swatch"),
744 // For “symbolic” (monochromatic) vs “full-color” icons, see
745 // https://pointieststick.com/2023/08/12/how-all-this-icon-stuff-is-going-to-work-in-plasma-6/
746 QStringLiteral("palette"),
747 QStringLiteral("palette-symbolic")};
748 const int swatchBookIndex = //
749 m_tabWidget->indexOf(m_swatchBookWrapperWidget);
750 if (swatchBookIndex >= 0) {
751 m_tabWidget->setTabIcon(swatchBookIndex, //
752 qIconFromTheme(swatchBookIcons, //
753 QStringLiteral("color-swatch"),
754 m_currentIconThemeType));
755 }
756
757 static const QStringList hueFirstIcons //
758 {
759 QStringLiteral("color-mode-hue-shift-positive"),
760 };
761 const int hueFirstIndex = //
762 m_tabWidget->indexOf(m_hueFirstWrapperWidget);
763 if (hueFirstIndex >= 0) {
764 m_tabWidget->setTabIcon(hueFirstIndex, //
765 qIconFromTheme(hueFirstIcons, //
766 QStringLiteral("steering-wheel"),
767 m_currentIconThemeType));
768 }
769
770 static const QStringList lightnessFirstIcons //
771 {
772 QStringLiteral("brightness-high"),
773 };
774 const int lightnessFirstIndex = //
775 m_tabWidget->indexOf(m_lightnessFirstWrapperWidget);
776 if (lightnessFirstIndex >= 0) {
777 m_tabWidget->setTabIcon(lightnessFirstIndex, //
778 qIconFromTheme(lightnessFirstIcons, //
779 QStringLiteral("brightness-2"),
780 m_currentIconThemeType));
781 }
782
783 static const QStringList numericIcons //
784 {
785 QStringLiteral("black_sum"),
786 };
787 const int numericIndex = //
788 m_tabWidget->indexOf(m_numericalWidget);
789 if (numericIndex >= 0) {
790 m_tabWidget->setTabIcon(numericIndex, //
791 qIconFromTheme(numericIcons, //
792 QStringLiteral("123"),
793 m_currentIconThemeType));
794 }
795
796 // Gamut button for some spin boxes
797 static const QStringList gamutIconNames //
798 {
799 QStringLiteral("data-warning"),
800 QStringLiteral("dialog-warning-symbolic"),
801 };
802 const QIcon gamutIcon = qIconFromTheme(gamutIconNames, //
803 QStringLiteral("eye-exclamation"),
804 m_currentIconThemeType);
805 m_cielchD50SpinBoxGamutAction->setIcon(gamutIcon);
806 m_oklchSpinBoxGamutAction->setIcon(gamutIcon);
807
808 static const QStringList candidates //
809 {
810 QStringLiteral("color-picker"), //
811 QStringLiteral("gtk-color-picker"), //
812 QStringLiteral("tool_color_picker"), //
813 };
814 if (!m_screenColorPickerButton.isNull()) {
815 m_screenColorPickerButton->setIcon( //
816 qIconFromTheme(candidates, //
817 QStringLiteral("color-picker"),
818 m_currentIconThemeType));
819 }
820}
821
822/** @brief Basic initialization.
823 *
824 * @param colorSpace The color space within which this widget should operate.
825 * Can be created with @ref RgbColorSpaceFactory.
826 *
827 * Code that is shared between the various overloaded constructors.
828 *
829 * @todo The RTL layout is broken for @ref SwatchBook. Thought a stretch
830 * is added in the layout, the @ref SwatchBook stays left-aligned
831 * instead of right-aligned if there is too much space. Why doesn’t this
832 * right-align? For @ref m_wheelColorPicker and @ref m_chromaHueDiagram
833 * the same code works fine! */
834void ColorDialogPrivate::initialize(const QSharedPointer<PerceptualColor::RgbColorSpace> &colorSpace)
835{
836 // Do not show the “?” button in the window title. This button is displayed
837 // by default on widgets that inherit from QDialog. But we do not want the
838 // button because we do not provide What’s-This-help anyway, so having
839 // the button would be confusing.
840 q_pointer->setWindowFlag(Qt::WindowContextHelpButtonHint, false);
841
842 // initialize color space and its dependencies
843 m_rgbColorSpace = colorSpace;
844 m_wcsBasicColors = wcsBasicColors(colorSpace);
845 m_wcsBasicDefaultColor = m_wcsBasicColors.value(4, 2);
846
847 // create the graphical selectors
848 m_swatchBookBasicColors = new SwatchBook(m_rgbColorSpace, //
849 m_wcsBasicColors, //
850 Qt::Orientation::Horizontal);
851 auto swatchBookBasicColorsLayout = new QGridLayout();
852 auto swatchBookBasicColorsLayoutWidget = new QWidget();
853 swatchBookBasicColorsLayoutWidget->setLayout(swatchBookBasicColorsLayout);
854 swatchBookBasicColorsLayoutWidget->setContentsMargins(0, 0, 0, 0);
855 swatchBookBasicColorsLayout->setContentsMargins(0, 0, 0, 0);
856 swatchBookBasicColorsLayout->addWidget(m_swatchBookBasicColors);
857 swatchBookBasicColorsLayout->setRowStretch(1, 1);
858 swatchBookBasicColorsLayout->setColumnStretch(1, 1);
859 m_swatchBookHistory = new SwatchBook(m_rgbColorSpace, //
860 Swatches(), //
861 Qt::Orientation::Vertical);
862 auto swatchBookHistoryLayout = new QGridLayout();
863 auto swatchBookHistoryLayoutWidget = new QWidget();
864 swatchBookHistoryLayoutWidget->setLayout(swatchBookHistoryLayout);
865 swatchBookHistoryLayoutWidget->setContentsMargins(0, 0, 0, 0);
866 swatchBookHistoryLayout->setContentsMargins(0, 0, 0, 0);
867 swatchBookHistoryLayout->addWidget(m_swatchBookHistory);
868 swatchBookHistoryLayout->setRowStretch(1, 1);
869 swatchBookHistoryLayout->setColumnStretch(1, 1);
870 loadHistoryFromSettingsToSwatchBook();
871 m_swatchBookCustomColors = new SwatchBook( //
872 m_rgbColorSpace, //
873 Swatches(), //
874 Qt::Orientation::Horizontal | Qt::Orientation::Vertical);
875 m_swatchBookCustomColors->setEditable(true);
876 auto swatchBookCustomColorsLayout = new QGridLayout();
877 auto swatchBookCustomColorsLayoutWidget = new QWidget();
878 swatchBookCustomColorsLayoutWidget->setLayout(swatchBookCustomColorsLayout);
879 swatchBookCustomColorsLayoutWidget->setContentsMargins(0, 0, 0, 0);
880 swatchBookCustomColorsLayout->setContentsMargins(0, 0, 0, 0);
881 swatchBookCustomColorsLayout->addWidget(m_swatchBookCustomColors);
882 swatchBookCustomColorsLayout->setRowStretch(1, 1);
883 swatchBookCustomColorsLayout->setColumnStretch(1, 1);
884 loadCustomColorsFromSettingsToSwatchBook();
885 connect(m_swatchBookCustomColors, // sender
886 &SwatchBook::swatchGridChanged, // signal
887 this, // receiver
888 [this](const Swatches &newSwatches) {
889 m_settings.customColors.setValue(newSwatches.toQList());
890 });
891 m_swatchBookStack = new QStackedLayout();
892 m_swatchBookStack->addWidget(swatchBookBasicColorsLayoutWidget);
893 m_swatchBookStack->addWidget(swatchBookHistoryLayoutWidget);
894 m_swatchBookStack->addWidget(swatchBookCustomColorsLayoutWidget);
895 QHBoxLayout *swatchBookInnerLayout = new QHBoxLayout();
896 swatchBookInnerLayout->addLayout(m_swatchBookStack);
897 swatchBookInnerLayout->addStretch();
898 QVBoxLayout *swatchBookOuterLayout = new QVBoxLayout();
899 m_swatchBookSelector = new QComboBox();
900 m_swatchBookSelector->addItem(QString());
901 m_swatchBookSelector->addItem(QString());
902 m_swatchBookSelector->addItem(QString());
903 connect(m_swatchBookSelector, //
905 this,
906 [this](int i) {
907 switch (i) {
908 case 0:
909 m_swatchBookStack->setCurrentIndex(0);
910 m_settings.swatchBookPage.setValue( //
911 PerceptualSettings::SwatchBookPage::BasicColors);
912 break;
913 case 1:
914 m_swatchBookStack->setCurrentIndex(1);
915 m_settings.swatchBookPage.setValue( //
916 PerceptualSettings::SwatchBookPage::History);
917 break;
918 case 2:
919 m_swatchBookStack->setCurrentIndex(2);
920 m_settings.swatchBookPage.setValue( //
921 PerceptualSettings::SwatchBookPage::CustomColors);
922 break;
923 };
924 });
925 swatchBookOuterLayout->addWidget(m_swatchBookSelector);
926 swatchBookOuterLayout->addLayout(swatchBookInnerLayout);
927 swatchBookOuterLayout->addStretch();
928 m_swatchBookWrapperWidget = new QWidget();
929 m_swatchBookWrapperWidget->setLayout(swatchBookOuterLayout);
930 connect(&m_settings.history, //
931 &Setting<PerceptualSettings::ColorList>::valueChanged,
932 this,
933 &ColorDialogPrivate::loadHistoryFromSettingsToSwatchBook);
934 connect(&m_settings.customColors, //
935 &Setting<PerceptualSettings::ColorList>::valueChanged,
936 this,
937 &ColorDialogPrivate::loadCustomColorsFromSettingsToSwatchBook);
938 m_wheelColorPicker = new WheelColorPicker(m_rgbColorSpace);
939 m_hueFirstWrapperWidget = new QWidget;
940 QHBoxLayout *tempHueFirstLayout = new QHBoxLayout;
941 tempHueFirstLayout->addWidget(m_wheelColorPicker);
942 m_hueFirstWrapperWidget->setLayout(tempHueFirstLayout);
943
944 m_lchLightnessSelector = new GradientSlider(m_rgbColorSpace);
945 GenericColor blackCielchD50;
946 blackCielchD50.first = 0;
947 blackCielchD50.second = 0;
948 blackCielchD50.third = 0;
949 blackCielchD50.fourth = 1;
950 GenericColor whiteCielchD50;
951 whiteCielchD50.first = 100;
952 whiteCielchD50.second = 0;
953 whiteCielchD50.third = 0;
954 whiteCielchD50.fourth = 1;
955 m_lchLightnessSelector->setColors(blackCielchD50, whiteCielchD50);
956 m_chromaHueDiagram = new ChromaHueDiagram(m_rgbColorSpace);
957 QHBoxLayout *tempLightnesFirstLayout = new QHBoxLayout();
958 tempLightnesFirstLayout->addWidget(m_lchLightnessSelector);
959 tempLightnesFirstLayout->addWidget(m_chromaHueDiagram);
960 m_lightnessFirstWrapperWidget = new QWidget();
961 m_lightnessFirstWrapperWidget->setLayout(tempLightnesFirstLayout);
962
963 initializeScreenColorPicker();
964
965 m_tabWidget = new QTabWidget;
966 // It would be good to have bigger icons. Via QStyle::pixelMetrics()
967 // we could get values for this. QStyle::PM_LargeIconSize seems to large,
968 // be we could use std::max() with QStyle::PM_ToolBarIconSize,
969 // QStyle::PM_SmallIconSize, QStyle::PM_TabBarIconSize,
970 // QStyle::PM_ButtonIconSize. But the problem is a regression in Qt6
971 // (compared to Qt5) that breaks rendering of bigger icons via
972 // QTabWidget::iconSize(): https://bugreports.qt.io/browse/QTBUG-114849
973 // Furthermore, it appears that the MacOS style does not adjust the height
974 // of the tab bar to match the icon height. This causes larger icons to
975 // simply overflow, which looks like a rendering issue. Therefore,
976 // currently we stick with the default icons size for tab bars.
977 m_tabWidget->addTab(m_swatchBookWrapperWidget, QString());
978 m_swatchBookTabShortcut = new QShortcut(q_pointer);
979 connect(m_swatchBookTabShortcut, //
981 this,
982 [this]() {
983 m_tabWidget->setCurrentIndex( //
984 m_tabWidget->indexOf(m_swatchBookWrapperWidget));
985 });
986 connect(m_swatchBookTabShortcut, //
988 this,
989 [this]() {
990 m_tabWidget->setCurrentIndex( //
991 m_tabWidget->indexOf(m_swatchBookWrapperWidget));
992 });
993
994 m_tabWidget->addTab(m_hueFirstWrapperWidget, QString());
995 m_hueFirstTabShortcut = new QShortcut(q_pointer);
996 connect(m_hueFirstTabShortcut, //
998 this,
999 [this]() {
1000 m_tabWidget->setCurrentIndex( //
1001 m_tabWidget->indexOf(m_hueFirstWrapperWidget));
1002 });
1003 connect(m_hueFirstTabShortcut, //
1005 this,
1006 [this]() {
1007 m_tabWidget->setCurrentIndex( //
1008 m_tabWidget->indexOf(m_hueFirstWrapperWidget));
1009 });
1010
1011 m_tabWidget->addTab(m_lightnessFirstWrapperWidget, QString());
1012 m_lightnessFirstTabShortcut = new QShortcut(q_pointer);
1013 connect(m_lightnessFirstTabShortcut, //
1015 this,
1016 [this]() {
1017 m_tabWidget->setCurrentIndex( //
1018 m_tabWidget->indexOf(m_lightnessFirstWrapperWidget));
1019 });
1020 connect(m_lightnessFirstTabShortcut, //
1022 this,
1023 [this]() {
1024 m_tabWidget->setCurrentIndex( //
1025 m_tabWidget->indexOf(m_lightnessFirstWrapperWidget));
1026 });
1027
1028 m_tabTable.insert(&m_swatchBookWrapperWidget, //
1029 QStringLiteral("swatch"));
1030 m_tabTable.insert(&m_hueFirstWrapperWidget, //
1031 QStringLiteral("hue-based"));
1032 m_tabTable.insert(&m_lightnessFirstWrapperWidget, //
1033 QStringLiteral("lightness-based"));
1034 m_tabTable.insert(&m_numericalWidget, //
1035 QStringLiteral("numerical"));
1036 connect(m_tabWidget, //
1038 this, //
1039 &ColorDialogPrivate::saveCurrentTab);
1040
1041 // Create the ColorPatch
1042 m_colorPatch = new ColorPatch();
1043 m_colorPatch->setMinimumSize(m_colorPatch->minimumSizeHint() * 1.5);
1044
1045 QHBoxLayout *headerLayout = new QHBoxLayout();
1046 headerLayout->addWidget(m_colorPatch, 1);
1047 m_screenColorPickerButton->setSizePolicy(QSizePolicy::Minimum, // horizontal
1048 QSizePolicy::Minimum); // vertical
1049 headerLayout->addWidget(m_screenColorPickerButton,
1050 // Do not grow the cell in the direction
1051 // of the QBoxLayout:
1052 0,
1053 // No alignment: Fill the entire cell.
1054 Qt::Alignment());
1055
1056 // Create widget for the numerical values
1057 m_numericalWidget = initializeNumericPage();
1058 m_numericalTabShortcut = new QShortcut(q_pointer);
1059 connect(m_numericalTabShortcut, //
1061 this,
1062 [this]() {
1063 m_tabWidget->setCurrentIndex( //
1064 m_tabWidget->indexOf(m_numericalWidget));
1065 });
1066 connect(m_numericalTabShortcut, //
1068 this,
1069 [this]() {
1070 m_tabWidget->setCurrentIndex( //
1071 m_tabWidget->indexOf(m_numericalWidget));
1072 });
1073
1074 // Create layout for graphical and numerical widgets
1075 m_selectorLayout = new QHBoxLayout();
1076 m_selectorLayout->addWidget(m_tabWidget);
1077 m_selectorLayout->addWidget(m_numericalWidget);
1078
1079 // Create widgets for alpha value
1080 QHBoxLayout *m_alphaLayout = new QHBoxLayout();
1081 m_alphaGradientSlider = new GradientSlider(m_rgbColorSpace, //
1082 Qt::Orientation::Horizontal);
1083 m_alphaGradientSlider->setSingleStep(singleStepAlpha);
1084 m_alphaGradientSlider->setPageStep(pageStepAlpha);
1085 m_alphaSpinBox = new QDoubleSpinBox();
1086 m_alphaSpinBox->setAlignment(Qt::AlignmentFlag::AlignRight);
1087 m_alphaSpinBox->setMinimum(0);
1088 m_alphaSpinBox->setMaximum(100);
1089 // The suffix is set in retranslateUi.
1090 m_alphaSpinBox->setDecimals(decimals);
1091 m_alphaSpinBox->setSingleStep(singleStepAlpha * 100);
1092 // m_alphaSpinBox is of type QDoubleSpinBox which does not allow to
1093 // configure the pageStep.
1094 m_alphaLabel = new QLabel();
1095 m_alphaLabel->setBuddy(m_alphaSpinBox);
1096 m_alphaLayout->addWidget(m_alphaLabel);
1097 m_alphaLayout->addWidget(m_alphaGradientSlider);
1098 m_alphaLayout->addWidget(m_alphaSpinBox);
1099
1100 // Create the default buttons
1101 // We use standard buttons, because these standard buttons are
1102 // created by Qt and have automatically the correct icons and so on
1103 // (as designated in the current platform and widget style).
1104 // Though we use standard buttons, (later) we set the text manually to
1105 // get full control over the translation. Otherwise, loading a
1106 // different translation files than the user’s QLocale::system()
1107 // default locale would not update the standard button texts.
1108 m_buttonBox = new QDialogButtonBox();
1109 // NOTE We start with the OK button, and not with the Cancel button.
1110 // This is because apparently, the first button becomes the default
1111 // one (though Qt documentation says differently). If Cancel would
1112 // be the first, it would become the default button, which is not
1113 // what we want. (Even QPushButton::setDefault() will not change this
1114 // afterwards.)
1115 m_buttonOK = m_buttonBox->addButton(QDialogButtonBox::Ok);
1116 m_buttonCancel = m_buttonBox->addButton(QDialogButtonBox::Cancel);
1117 // The Qt documentation at
1118 // https://doc.qt.io/qt-5/qcoreapplication.html#installTranslator
1119 // says that Qt::LanguageChange events are only send to top-level
1120 // widgets. However, our experience is that also the QDialogButtonBox
1121 // receives Qt::LanguageChange events and reacts on it by updating
1122 // the user-visible string of all standard buttons. We do not want
1123 // to use custom buttons because of the advantages of standard
1124 // buttons that are described above. On the other hand, we do not
1125 // want Qt to change our string because we use our own translation
1126 // here.
1127 m_buttonBox->installEventFilter(&m_languageChangeEventFilter);
1128 m_buttonOK->installEventFilter(&m_languageChangeEventFilter);
1129 m_buttonCancel->installEventFilter(&m_languageChangeEventFilter);
1130 connect(m_buttonBox, // sender
1131 &QDialogButtonBox::accepted, // signal
1132 q_pointer, // receiver
1134 connect(m_buttonBox, // sender
1135 &QDialogButtonBox::rejected, // signal
1136 q_pointer, // receiver
1138
1139 // Create the main layout
1140 QVBoxLayout *tempMainLayout = new QVBoxLayout();
1141 tempMainLayout->addLayout(headerLayout);
1142 tempMainLayout->addLayout(m_selectorLayout);
1143 tempMainLayout->addLayout(m_alphaLayout);
1144 tempMainLayout->addWidget(m_buttonBox);
1145 q_pointer->setLayout(tempMainLayout);
1146
1147 // initialize signal-slot-connections
1148 connect(m_colorPatch, // sender
1149 &ColorPatch::colorChanged, // signal
1150 this, // receiver
1151 &ColorDialogPrivate::readColorPatchValue // slot
1152 );
1153 connect(m_swatchBookBasicColors, // sender
1154 &SwatchBook::currentColorChanged, // signal
1155 this, // receiver
1156 &ColorDialogPrivate::readSwatchBookBasicColorsValue // slot
1157 );
1158 connect(m_swatchBookHistory, // sender
1159 &SwatchBook::currentColorChanged, // signal
1160 this, // receiver
1161 &ColorDialogPrivate::readSwatchBookHistoryValue // slot
1162 );
1163 connect(m_swatchBookCustomColors, // sender
1164 &SwatchBook::currentColorChanged, // signal
1165 this, // receiver
1166 &ColorDialogPrivate::readSwatchBookCustomColorsValue // slot
1167 );
1168 connect(m_rgbSpinBox, // sender
1170 this, // receiver
1171 &ColorDialogPrivate::readRgbNumericValues // slot
1172 );
1173 connect(m_rgbLineEdit, // sender
1174 &QLineEdit::textChanged, // signal
1175 this, // receiver
1176 &ColorDialogPrivate::readRgbHexValues // slot
1177 );
1178 connect(m_rgbLineEdit, // sender
1179 &QLineEdit::editingFinished, // signal
1180 this, // receiver
1181 &ColorDialogPrivate::updateRgbHexButBlockSignals // slot
1182 );
1183 connect(m_hslSpinBox, // sender
1185 this, // receiver
1186 &ColorDialogPrivate::readHslNumericValues // slot
1187 );
1188 connect(m_hwbSpinBox, // sender
1190 this, // receiver
1191 &ColorDialogPrivate::readHwbNumericValues // slot
1192 );
1193 connect(m_hsvSpinBox, // sender
1195 this, // receiver
1196 &ColorDialogPrivate::readHsvNumericValues // slot
1197 );
1198 connect(m_cielchD50SpinBox, // sender
1200 this, // receiver
1201 &ColorDialogPrivate::readLchNumericValues // slot
1202 );
1203 connect(m_cielchD50SpinBox, // sender
1205 this, // receiver
1206 &ColorDialogPrivate::updateLchButBlockSignals // slot
1207 );
1208 connect(m_oklchSpinBox, // sender
1210 this, // receiver
1211 &ColorDialogPrivate::readOklchNumericValues // slot
1212 );
1213 connect(m_oklchSpinBox, // sender
1215 this, // receiver
1216 &ColorDialogPrivate::updateOklchButBlockSignals // slot
1217 );
1218 connect(m_lchLightnessSelector, // sender
1220 this, // receiver
1221 &ColorDialogPrivate::readLightnessValue // slot
1222 );
1223 connect(m_wheelColorPicker, // sender
1225 this, // receiver
1226 &ColorDialogPrivate::readWheelColorPickerValues // slot
1227 );
1228 connect(m_chromaHueDiagram, // sender
1230 this, // receiver
1231 &ColorDialogPrivate::readChromaHueDiagramValue // slot
1232 );
1233 connect(m_alphaGradientSlider, // sender
1235 this, // receiver
1236 &ColorDialogPrivate::updateColorPatch // slot
1237 );
1238 connect(m_alphaGradientSlider, // sender
1240 this, // receiver
1241 [this](const qreal newFraction) { // lambda
1242 const QSignalBlocker blocker(m_alphaSpinBox);
1243 m_alphaSpinBox->setValue(newFraction * 100);
1244 });
1245 connect(m_alphaSpinBox, // sender
1246 QOverload<double>::of(&QDoubleSpinBox::valueChanged), // signal
1247 this, // receiver
1248 [this](const double newValue) { // lambda
1249 // m_alphaGradientSlider has range [0, 1], while the signal
1250 // has range [0, 100]. This has to be adapted:
1251 m_alphaGradientSlider->setValue(newValue / 100);
1252 });
1253
1254 // Initialize the options
1255 q_pointer->setOptions(QColorDialog::ColorDialogOption::DontUseNativeDialog);
1256
1257 // We are setting the translated default window title here instead
1258 // of setting it within retranslateUi(). This is because also QColorDialog
1259 // does not update the window title on LanguageChange events (probably
1260 // to avoid confusion, because it’s difficult to tell exactly if the
1261 // library user did or did not explicitly change the window title.
1262 /*: @title:window Default window title. Same text as in QColorDialog */
1263 q_pointer->setWindowTitle(tr("Select color"));
1264
1265 // Enable size grip
1266 // As this dialog can indeed be resized, the size grip should
1267 // be enabled. So, users can see the little triangle at the
1268 // right bottom of the dialog (or the left bottom on a
1269 // right-to-left layout). So, the user will be aware
1270 // that he can indeed resize this dialog, which is
1271 // important as the users are used to the default
1272 // platform dialog, which often do not allow resizing. Therefore,
1273 // by default, QDialog::isSizeGripEnabled() should be true.
1274 // NOTE: Some widget styles like Oxygen or Breeze leave the size grip
1275 // widget invisible; nevertheless it reacts on mouse events. Other
1276 // widget styles indeed show the size grip widget, like Fusion or
1277 // QtCurve.
1278 q_pointer->setSizeGripEnabled(true);
1279
1280 // The q_pointer’s object is still not fully initialized at this point,
1281 // but it’s base class constructor has fully run; this should be enough
1282 // to use functionality based on QWidget, so we can use it as parent.
1283 m_cielchD50SpinBoxGamutAction = new QAction(q_pointer);
1284 connect(m_cielchD50SpinBoxGamutAction, // sender
1285 &QAction::triggered, // signal
1286 this, // receiver
1287 &ColorDialogPrivate::updateLchButBlockSignals // slot
1288 );
1289 m_oklchSpinBoxGamutAction = new QAction(q_pointer);
1290 connect(m_oklchSpinBoxGamutAction, // sender
1291 &QAction::triggered, // signal
1292 this, // receiver
1293 &ColorDialogPrivate::updateOklchButBlockSignals // slot
1294 );
1295 // However, here we hide the action because initially the
1296 // current color should be in-gamut, so no need for the gamut action
1297 // to be visible.
1298 m_cielchD50SpinBoxGamutAction->setVisible(false);
1299 m_cielchD50SpinBox->addActionButton( //
1300 m_cielchD50SpinBoxGamutAction, //
1301 QLineEdit::ActionPosition::TrailingPosition);
1302 m_oklchSpinBoxGamutAction->setVisible(false);
1303 m_oklchSpinBox->addActionButton( //
1304 m_oklchSpinBoxGamutAction, //
1305 QLineEdit::ActionPosition::TrailingPosition);
1306
1307 initializeTranslation(QCoreApplication::instance(),
1308 // An empty std::optional means: If in initialization
1309 // had been done yet, repeat this initialization.
1310 // If not, do a new initialization now with default
1311 // values.
1312 std::optional<QStringList>());
1313 retranslateUi();
1314
1315 reloadIcons();
1316#if (QT_VERSION >= QT_VERSION_CHECK(6, 5, 0))
1317 connect(qGuiApp->styleHints(), // sender
1319 this, // receiver
1320 &ColorDialogPrivate::reloadIcons);
1321#endif
1322}
1323
1324/** @brief Constructor
1325 *
1326 * @param parent pointer to the parent widget, if any
1327 * @post The @ref currentColor property is set to a default value. */
1329 : QDialog(parent)
1330 , d_pointer(new ColorDialogPrivate(this))
1331{
1332 d_pointer->initialize(RgbColorSpaceFactory::createSrgb());
1333 setCurrentColor(d_pointer->defaultColor());
1334}
1335
1336/** @brief Constructor
1337 *
1338 * @param initial the initially chosen color of the dialog
1339 * @param parent pointer to the parent widget, if any
1340 * @post The object is constructed and @ref setCurrentColor() is called
1341 * with <em>initial</em>. See @ref setCurrentColor() for the modifications
1342 * that will be applied before setting the current color. Especially, as
1343 * this dialog is constructed by default without alpha support, the
1344 * alpha channel of <em>initial</em> is ignored and a fully opaque color is
1345 * used. */
1347 : QDialog(parent)
1348 , d_pointer(new ColorDialogPrivate(this))
1349{
1350 d_pointer->initialize(RgbColorSpaceFactory::createSrgb());
1351 // Calling setCurrentColor() guaranties to update all widgets
1352 // because it always sets a valid color, even when the color
1353 // parameter was invalid. As m_currentOpaqueColor is invalid
1354 // be default, and therefor different, setCurrentColor()
1355 // guaranties to update all widgets.
1356 setCurrentColor(initial);
1357}
1358
1359/** @brief Constructor
1360 *
1361 * @param colorSpace The color space within which this widget should operate.
1362 * Can be created with @ref RgbColorSpaceFactory.
1363 * @param parent pointer to the parent widget, if any
1364 * @post The @ref currentColor property is set to a default value. */
1366 : QDialog(parent)
1367 , d_pointer(new ColorDialogPrivate(this))
1368{
1369 d_pointer->initialize(colorSpace);
1370 setCurrentColor(d_pointer->defaultColor());
1371}
1372
1373/** @brief Returns the default value for @ref ColorDialog::currentColor.
1374 *
1375 * @returns the default value for @ref ColorDialog::currentColor. Comes from
1376 * the history (if any), or otherwise from @ref m_wcsBasicDefaultColor. */
1377QColor ColorDialogPrivate::defaultColor() const
1378{
1379 const auto history = m_settings.history.value();
1380 return history.value(0, m_wcsBasicDefaultColor);
1381}
1382
1383/** @brief Constructor
1384 *
1385 * @param colorSpace The color space within which this widget should operate.
1386 * Can be created with @ref RgbColorSpaceFactory.
1387 * @param initial the initially chosen color of the dialog
1388 * @param parent pointer to the parent widget, if any
1389 * @post The object is constructed and @ref setCurrentColor() is called
1390 * with <em>initial</em>. See @ref setCurrentColor() for the modifications
1391 * that will be applied before setting the current color. Especially, as
1392 * this dialog is constructed by default without alpha support, the
1393 * alpha channel of <em>initial</em> is ignored and a fully opaque color is
1394 * used. */
1396 : QDialog(parent)
1397 , d_pointer(new ColorDialogPrivate(this))
1398{
1399 d_pointer->initialize(colorSpace);
1400 // Calling setCurrentColor() guaranties to update all widgets
1401 // because it always sets a valid color, even when the color
1402 // parameter was invalid. As m_currentOpaqueColor is invalid
1403 // be default, and therefor different, setCurrentColor()
1404 // guaranties to update all widgets.
1405 setCurrentColor(initial);
1406}
1407
1408/** @brief Destructor */
1410{
1411 // All the layouts and widgets used here are automatically child widgets
1412 // of this dialog widget. Therefor they are deleted automatically.
1413 // Also m_rgbColorSpace is of type RgbColorSpace(), which
1414 // inherits from QObject, and is a child of this dialog widget, does
1415 // not need to be deleted manually.
1416}
1417
1418/** @brief Constructor
1419 *
1420 * @param backLink Pointer to the object from which <em>this</em> object
1421 * is the private implementation. */
1422ColorDialogPrivate::ColorDialogPrivate(ColorDialog *backLink)
1423 : q_pointer(backLink)
1424{
1425}
1426
1427// No documentation here (documentation of properties
1428// and its getters are in the header)
1430{
1431 QColor temp = d_pointer->m_currentOpaqueColorRgb.rgbQColor;
1432 temp.setAlphaF( //
1433 static_cast<QColorFloatType>( //
1434 d_pointer->m_alphaGradientSlider->value()));
1435 return temp;
1436}
1437
1438/** @brief Setter for @ref currentColor property.
1439 *
1440 * @param color the new color
1441 * @post The property @ref currentColor is adapted as follows:
1442 * - If <em>color</em> is not valid, <tt>Qt::black</tt> is used instead.
1443 * - If <em>color</em>’s <tt>QColor::Spec</tt> is <em>not</em>
1444 * <tt>QColor::Spec::Rgb</tt> then it will be converted silently
1445 * to <tt>QColor::Spec::Rgb</tt>
1446 * - The RGB part of @ref currentColor will be the RGB part of <tt>color</tt>.
1447 * - The alpha channel of @ref currentColor will be the alpha channel
1448 * of <tt>color</tt> if at the moment of the function call
1449 * the <tt>QColorDialog::ColorDialogOption::ShowAlphaChannel</tt> option is
1450 * set. It will be fully opaque otherwise. */
1452{
1453 QColor temp;
1454 if (color.isValid()) {
1455 // Make sure that the QColor::spec() is QColor::Spec::Rgb.
1456 temp = color.toRgb();
1457 } else {
1458 // For invalid colors same behavior as QColorDialog
1459 temp = QColor(Qt::black);
1460 }
1461 if (testOption(ColorDialog::ColorDialogOption::ShowAlphaChannel)) {
1462 d_pointer->m_alphaGradientSlider->setValue( //
1463 static_cast<double>(temp.alphaF()));
1464 } else {
1465 d_pointer->m_alphaGradientSlider->setValue(1);
1466 }
1467 // No need to update m_alphaSpinBox as this is done
1468 // automatically by signals emitted by m_alphaGradientSlider.
1469 const RgbColor myRgbColor = RgbColor::fromRgbQColor(temp);
1470 d_pointer->setCurrentOpaqueColor(myRgbColor, nullptr);
1471}
1472
1473/** @brief Opens the dialog and connects its @ref colorSelected() signal to
1474 * the slot specified by receiver and member.
1475 *
1476 * The signal will be disconnected from the slot when the dialog is closed.
1477 *
1478 * Example:
1479 * @snippet testcolordialog.cpp ColorDialog Open
1480 *
1481 * @param receiver the object that will receive the @ref colorSelected() signal
1482 * @param member the slot that will receive the @ref colorSelected() signal */
1483void ColorDialog::open(QObject *receiver, const char *member)
1484{
1485 connect(this, // sender
1486 SIGNAL(colorSelected(QColor)), // signal
1487 receiver, // receiver
1488 member); // slot
1489 d_pointer->m_receiverToBeDisconnected = receiver;
1490 d_pointer->m_memberToBeDisconnected = member;
1491 QDialog::open();
1492}
1493
1494/** @brief Updates the color patch widget
1495 *
1496 * @post The color patch widget will show the color
1497 * of @ref m_currentOpaqueColorRgb and the alpha
1498 * value of @ref m_alphaGradientSlider. */
1499void ColorDialogPrivate::updateColorPatch()
1500{
1501 QColor tempRgbQColor = m_currentOpaqueColorRgb.rgbQColor;
1502 tempRgbQColor.setAlphaF( //
1503 static_cast<QColorFloatType>(m_alphaGradientSlider->value()));
1504 m_colorPatch->setColor(tempRgbQColor);
1505}
1506
1507/** @brief Overloaded function. */
1508void ColorDialogPrivate::setCurrentOpaqueColor(const QHash<PerceptualColor::ColorModel, PerceptualColor::GenericColor> &abs, QWidget *const ignoreWidget)
1509{
1510 const auto cielchD50 = abs.value(ColorModel::CielchD50);
1511 const auto rgb1 = m_rgbColorSpace->fromCielchD50ToRgb1(cielchD50);
1512 const auto rgb255 = GenericColor(rgb1.first * 255, //
1513 rgb1.second * 255,
1514 rgb1.third * 255);
1515 const auto rgbColor = RgbColor::fromRgb255(rgb255);
1516 setCurrentOpaqueColor(abs, rgbColor, ignoreWidget);
1517}
1518
1519/** @brief Overloaded function. */
1520void ColorDialogPrivate::setCurrentOpaqueColor(const PerceptualColor::RgbColor &rgb, QWidget *const ignoreWidget)
1521{
1522 const auto temp = rgb.rgb255;
1523 const QColor myQColor = QColor::fromRgbF( //
1524 static_cast<QColorFloatType>(temp.first / 255.), //
1525 static_cast<QColorFloatType>(temp.second / 255.), //
1526 static_cast<QColorFloatType>(temp.third / 255.));
1527 const auto cielchD50 = GenericColor( //
1528 m_rgbColorSpace->toCielchD50(myQColor.rgba64()));
1529 setCurrentOpaqueColor( //
1530 AbsoluteColor::allConversions(ColorModel::CielchD50, cielchD50),
1531 rgb,
1532 ignoreWidget);
1533}
1534
1535/** @brief Updates @ref m_currentOpaqueColorAbs, @ref m_currentOpaqueColorRgb
1536 * and affected widgets.
1537 *
1538 * @param abs The new color in absolute color models
1539 * @param rgb The new color in RGB and RGB-derived models (profile-dependant)
1540 *
1541 * @param ignoreWidget A widget that should <em>not</em> be updated. Or
1542 * <tt>nullptr</tt> to update <em>all</em> widgets.
1543 *
1544 * @post If this function is called recursively, nothing happens. Else
1545 * the color is moved into the gamut, then @ref m_currentOpaqueColorAbs and
1546 * @ref m_currentOpaqueColorRgb are updated, and the corresponding widgets
1547 * are updated (except the widget specified to be ignored – if any).
1548 *
1549 * @note Recursive functions calls are ignored. This is useful, because you
1550 * can connect signals from various widgets to this slot without having to
1551 * worry about infinite recursions. */
1552void ColorDialogPrivate::setCurrentOpaqueColor(const QHash<PerceptualColor::ColorModel, PerceptualColor::GenericColor> &abs,
1553 const PerceptualColor::RgbColor &rgb,
1554 QWidget *const ignoreWidget)
1555{
1556 const bool isIdentical = //
1557 (abs == m_currentOpaqueColorAbs) && (rgb == m_currentOpaqueColorRgb);
1558 if (m_isColorChangeInProgress || isIdentical) {
1559 // Nothing to do!
1560 return;
1561 }
1562
1563 // If we have really some work to do, block recursive calls
1564 // of this function
1565 m_isColorChangeInProgress = true;
1566
1567 // Save currentColor() for later comparison
1568 // Using currentColor() makes sure correct alpha treatment!
1569 QColor oldQColor = q_pointer->currentColor();
1570
1571 // Update m_currentOpaqueColor
1572 m_currentOpaqueColorAbs = abs;
1573 m_currentOpaqueColorRgb = rgb;
1574
1575 // Update basic colors swatch book
1576 if (m_swatchBookBasicColors != ignoreWidget) {
1577 m_swatchBookBasicColors->setCurrentColor( //
1578 m_currentOpaqueColorRgb.rgbQColor);
1579 }
1580
1581 // Update history swatch book
1582 if (m_swatchBookHistory != ignoreWidget) {
1583 m_swatchBookHistory->setCurrentColor(m_currentOpaqueColorRgb.rgbQColor);
1584 }
1585
1586 // Update custom colors swatch book
1587 if (m_swatchBookCustomColors != ignoreWidget) {
1588 m_swatchBookCustomColors->setCurrentColor(m_currentOpaqueColorRgb.rgbQColor);
1589 }
1590
1591 // Update RGB widget
1592 if (m_rgbSpinBox != ignoreWidget) {
1593 m_rgbSpinBox->setSectionValues( //
1594 m_currentOpaqueColorRgb.rgb255.toQList3());
1595 }
1596
1597 // Update HSL widget
1598 if (m_hslSpinBox != ignoreWidget) {
1599 m_hslSpinBox->setSectionValues( //
1600 m_currentOpaqueColorRgb.hsl.toQList3());
1601 }
1602
1603 // Update HWB widget
1604 if (m_hwbSpinBox != ignoreWidget) {
1605 m_hwbSpinBox->setSectionValues( //
1606 m_currentOpaqueColorRgb.hwb.toQList3());
1607 }
1608
1609 // Update HSV widget
1610 if (m_hsvSpinBox != ignoreWidget) {
1611 m_hsvSpinBox->setSectionValues( //
1612 m_currentOpaqueColorRgb.hsv.toQList3());
1613 }
1614
1615 // Update CIELCH-D50 widget
1616 const GenericColor cielchD50 = m_currentOpaqueColorAbs.value(ColorModel::CielchD50);
1617 if (m_cielchD50SpinBox != ignoreWidget) {
1618 m_cielchD50SpinBox->setSectionValues(cielchD50.toQList3());
1619 }
1620
1621 // Update Oklch widget
1622 const auto oklch = m_currentOpaqueColorAbs.value(ColorModel::OklchD65);
1623 if (m_oklchSpinBox != ignoreWidget) {
1624 m_oklchSpinBox->setSectionValues(oklch.toQList3());
1625 }
1626
1627 // Update RGB hex widget
1628 if (m_rgbLineEdit != ignoreWidget) {
1629 updateRgbHexButBlockSignals();
1630 }
1631
1632 // Update lightness selector
1633 if (m_lchLightnessSelector != ignoreWidget) {
1634 m_lchLightnessSelector->setValue( //
1635 cielchD50.first / static_cast<qreal>(100));
1636 }
1637
1638 // Update chroma-hue diagram
1639 if (m_chromaHueDiagram != ignoreWidget) {
1640 m_chromaHueDiagram->setCurrentColorCielchD50(cielchD50);
1641 }
1642
1643 // Update wheel color picker
1644 if (m_wheelColorPicker != ignoreWidget) {
1645 m_wheelColorPicker->setCurrentColorCielchD50(cielchD50);
1646 }
1647
1648 // Update alpha gradient slider
1649 if (m_alphaGradientSlider != ignoreWidget) {
1650 GenericColor tempColor;
1651 tempColor.first = cielchD50.first;
1652 tempColor.second = cielchD50.second;
1653 tempColor.third = cielchD50.third;
1654 tempColor.fourth = 0;
1655 m_alphaGradientSlider->setFirstColorCieLchD50A(tempColor);
1656 tempColor.fourth = 1;
1657 m_alphaGradientSlider->setSecondColorCieLchD50A(tempColor);
1658 }
1659
1660 // Update widgets that take alpha information
1661 if (m_colorPatch != ignoreWidget) {
1662 updateColorPatch();
1663 }
1664
1665 // Emit signal currentColorChanged() only if necessary
1666 if (q_pointer->currentColor() != oldQColor) {
1667 Q_EMIT q_pointer->currentColorChanged(q_pointer->currentColor());
1668 }
1669
1670 // End of this function. Unblock recursive
1671 // function calls before returning.
1672 m_isColorChangeInProgress = false;
1673}
1674
1675/** @brief Reads the value from the lightness selector in the dialog and
1676 * updates the dialog accordingly. */
1677void ColorDialogPrivate::readLightnessValue()
1678{
1679 if (m_isColorChangeInProgress) {
1680 // Nothing to do!
1681 return;
1682 }
1683 auto cielchD50 = m_currentOpaqueColorAbs.value(ColorModel::CielchD50);
1684 cielchD50.first = m_lchLightnessSelector->value() * 100;
1685 cielchD50 = GenericColor( //
1686 m_rgbColorSpace->reduceCielchD50ChromaToFitIntoGamut(cielchD50));
1687 setCurrentOpaqueColor( //
1688 AbsoluteColor::allConversions(ColorModel::CielchD50, cielchD50), //
1689 m_lchLightnessSelector);
1690}
1691
1692/** @brief Reads the HSL numbers in the dialog and
1693 * updates the dialog accordingly. */
1694void ColorDialogPrivate::readHslNumericValues()
1695{
1696 if (m_isColorChangeInProgress) {
1697 // Nothing to do!
1698 return;
1699 }
1700 const auto temp = RgbColor::fromHsl( //
1701 GenericColor(m_hslSpinBox->sectionValues()));
1702 setCurrentOpaqueColor(temp, m_hslSpinBox);
1703}
1704
1705/** @brief Reads the HWB numbers in the dialog and
1706 * updates the dialog accordingly. */
1707void ColorDialogPrivate::readHwbNumericValues()
1708{
1709 if (m_isColorChangeInProgress) {
1710 // Nothing to do!
1711 return;
1712 }
1713 const auto temp = RgbColor::fromHwb( //
1714 GenericColor(m_hwbSpinBox->sectionValues()));
1715 setCurrentOpaqueColor(temp, m_hwbSpinBox);
1716}
1717
1718/** @brief Reads the HSV numbers in the dialog and
1719 * updates the dialog accordingly. */
1720void ColorDialogPrivate::readHsvNumericValues()
1721{
1722 if (m_isColorChangeInProgress) {
1723 // Nothing to do!
1724 return;
1725 }
1726 const auto temp = RgbColor::fromHsv( //
1727 GenericColor(m_hsvSpinBox->sectionValues()));
1728 setCurrentOpaqueColor(temp, m_hsvSpinBox);
1729}
1730
1731/** @brief Reads the decimal RGB numbers in the dialog and
1732 * updates the dialog accordingly. */
1733void ColorDialogPrivate::readRgbNumericValues()
1734{
1735 if (m_isColorChangeInProgress) {
1736 // Nothing to do!
1737 return;
1738 }
1739 const auto temp = RgbColor::fromRgb255( //
1740 GenericColor(m_rgbSpinBox->sectionValues()));
1741 setCurrentOpaqueColor(temp, m_rgbSpinBox);
1742}
1743
1744/** @brief Reads the color of the color patch, and
1745 * updates the dialog accordingly. */
1746void ColorDialogPrivate::readColorPatchValue()
1747{
1748 if (m_isColorChangeInProgress) {
1749 // Nothing to do!
1750 return;
1751 }
1752 const QColor temp = m_colorPatch->color();
1753 if (!temp.isValid()) {
1754 // No color is currently selected!
1755 return;
1756 }
1757 const auto myRgbColor = RgbColor::fromRgbQColor(temp);
1758 setCurrentOpaqueColor(myRgbColor, m_colorPatch);
1759}
1760
1761/** @brief Reads the color of the basic colors widget, and (if any)
1762 * updates the dialog accordingly. */
1763void ColorDialogPrivate::readSwatchBookBasicColorsValue()
1764{
1765 if (m_isColorChangeInProgress) {
1766 // Nothing to do!
1767 return;
1768 }
1769 const QColor temp = m_swatchBookBasicColors->currentColor();
1770 if (!temp.isValid()) {
1771 // No color is currently selected!
1772 return;
1773 }
1774 const auto myRgbColor = RgbColor::fromRgbQColor(temp);
1775 setCurrentOpaqueColor(myRgbColor, m_swatchBookBasicColors);
1776}
1777
1778/** @brief Reads the color of the history widget, and (if any)
1779 * updates the dialog accordingly. */
1780void ColorDialogPrivate::readSwatchBookHistoryValue()
1781{
1782 if (m_isColorChangeInProgress) {
1783 // Nothing to do!
1784 return;
1785 }
1786 const QColor temp = m_swatchBookHistory->currentColor();
1787 if (!temp.isValid()) {
1788 // No color is currently selected!
1789 return;
1790 }
1791 const auto myRgbColor = RgbColor::fromRgbQColor(temp);
1792 setCurrentOpaqueColor(myRgbColor, m_swatchBookHistory);
1793}
1794
1795/**
1796 * @brief Reads the color of the custom colors widget, and (if any)
1797 * updates the dialog accordingly.
1798 */
1799void ColorDialogPrivate::readSwatchBookCustomColorsValue()
1800{
1801 if (m_isColorChangeInProgress) {
1802 // Nothing to do!
1803 return;
1804 }
1805 const QColor temp = m_swatchBookCustomColors->currentColor();
1806 if (!temp.isValid()) {
1807 // No color is currently selected!
1808 return;
1809 }
1810 const auto myRgbColor = RgbColor::fromRgbQColor(temp);
1811 setCurrentOpaqueColor(myRgbColor, m_swatchBookCustomColors);
1812}
1813
1814/** @brief Reads the color of the @ref WheelColorPicker in the dialog and
1815 * updates the dialog accordingly. */
1816void ColorDialogPrivate::readWheelColorPickerValues()
1817{
1818 if (m_isColorChangeInProgress) {
1819 // Nothing to do!
1820 return;
1821 }
1822 const auto cielchD50 = GenericColor(m_wheelColorPicker->currentColorCielchD50());
1823 setCurrentOpaqueColor( //
1824 AbsoluteColor::allConversions(ColorModel::CielchD50, cielchD50),
1825 m_wheelColorPicker);
1826}
1827
1828/** @brief Reads the color of the @ref ChromaHueDiagram in the dialog and
1829 * updates the dialog accordingly. */
1830void ColorDialogPrivate::readChromaHueDiagramValue()
1831{
1832 if (m_isColorChangeInProgress) {
1833 // Nothing to do!
1834 return;
1835 }
1836 const auto cielchD50 = GenericColor(m_chromaHueDiagram->currentColorCielchD50());
1837 setCurrentOpaqueColor( //
1838 AbsoluteColor::allConversions(ColorModel::CielchD50, cielchD50),
1839 m_chromaHueDiagram);
1840}
1841
1842/** @brief Reads the hexadecimal RGB numbers in the dialog and
1843 * updates the dialog accordingly. */
1844void ColorDialogPrivate::readRgbHexValues()
1845{
1846 if (m_isColorChangeInProgress) {
1847 // Nothing to do!
1848 return;
1849 }
1850 QString temp = m_rgbLineEdit->text();
1851 if (!temp.startsWith(QStringLiteral(u"#"))) {
1852 temp = QStringLiteral(u"#") + temp;
1853 }
1854 QColor rgb;
1855 rgb.setNamedColor(temp);
1856 if (rgb.isValid()) {
1857 const auto myRgbColor = RgbColor::fromRgbQColor(rgb);
1858 setCurrentOpaqueColor(myRgbColor, m_rgbLineEdit);
1859 } else {
1860 m_isDirtyRgbLineEdit = true;
1861 }
1862}
1863
1864/** @brief Updates the RGB Hex widget to @ref m_currentOpaqueColorRgb.
1865 *
1866 * @post The @ref m_rgbLineEdit gets the value of @ref m_currentOpaqueColorRgb.
1867 * During this operation, all signals of @ref m_rgbLineEdit are blocked. */
1868void ColorDialogPrivate::updateRgbHexButBlockSignals()
1869{
1870 QSignalBlocker mySignalBlocker(m_rgbLineEdit);
1871
1872 // m_currentOpaqueColor is supposed to be always in-gamut. However,
1873 // because of rounding issues, a conversion to an unbounded RGB
1874 // color could result in an invalid color. Therefore, we must
1875 // use a conversion to a _bounded_ RGB color.
1876 const auto &rgbFloat = m_currentOpaqueColorRgb.rgb255;
1877
1878 // We cannot rely on the convenient QColor.name() because this function
1879 // seems to use floor() instead of round(), which does not make sense in
1880 // our dialog, and it would be inconsistent with the other widgets
1881 // of the dialog. Therefore, we have to round explicitly (to integers):
1882 // This format string provides a non-localized format!
1883 // Format of the numbers:
1884 // 1) The number itself
1885 // 2) The minimal field width (2 digits)
1886 // 3) The base of the number representation (16, hexadecimal)
1887 // 4) The fill character (leading zero)
1888 const QString hexString = //
1889 QStringLiteral(u"#%1%2%3")
1890 .arg(qBound(0, qRound(rgbFloat.first), 255), //
1891 2, //
1892 16, //
1893 QChar::fromLatin1('0'))
1894 .arg(qBound(0, qRound(rgbFloat.second), 255), //
1895 2, //
1896 16, //
1897 QChar::fromLatin1('0'))
1898 .arg(qBound(0, qRound(rgbFloat.third), 255), //
1899 2, //
1900 16, //
1901 QChar::fromLatin1('0'))
1902 .toUpper(); // Convert to upper case
1903 m_rgbLineEdit->setText(hexString);
1904}
1905
1906/** @brief Updates the LCH spin box to @ref m_currentOpaqueColorAbs.
1907 *
1908 * @post The @ref m_cielchD50SpinBox gets the value of
1909 * @ref m_currentOpaqueColorAbs. During this operation, all signals of
1910 * @ref m_cielchD50SpinBox are blocked. */
1911void ColorDialogPrivate::updateLchButBlockSignals()
1912{
1913 QSignalBlocker mySignalBlocker(m_cielchD50SpinBox);
1914 const auto cielchD50 = m_currentOpaqueColorAbs.value(ColorModel::CielchD50);
1915 m_cielchD50SpinBox->setSectionValues(cielchD50.toQList3());
1916 m_cielchD50SpinBoxGamutAction->setVisible(false);
1917}
1918
1919/** @brief Updates the Oklch spin box to @ref m_currentOpaqueColorAbs.
1920 *
1921 * @post The @ref m_oklchSpinBox gets the value
1922 * of @ref m_currentOpaqueColorAbs. During this operation,
1923 * all signals of @ref m_oklchSpinBox are blocked. */
1924void ColorDialogPrivate::updateOklchButBlockSignals()
1925{
1926 QSignalBlocker mySignalBlocker(m_oklchSpinBox);
1927 const auto oklch = m_currentOpaqueColorAbs.value(ColorModel::OklchD65);
1928 m_oklchSpinBox->setSectionValues(oklch.toQList3());
1929 m_oklchSpinBoxGamutAction->setVisible(false);
1930}
1931
1932/** @brief If no @ref m_isColorChangeInProgress, reads the LCH numbers
1933 * in the dialog and updates the dialog accordingly. */
1934void ColorDialogPrivate::readLchNumericValues()
1935{
1936 if (m_isColorChangeInProgress) {
1937 // Nothing to do!
1938 return;
1939 }
1940 const GenericColor lchValues = GenericColor(m_cielchD50SpinBox->sectionValues());
1941 if (m_rgbColorSpace->isCielchD50InGamut(lchValues)) {
1942 m_cielchD50SpinBoxGamutAction->setVisible(false);
1943 } else {
1944 m_cielchD50SpinBoxGamutAction->setVisible(true);
1945 }
1946 const auto myColor = GenericColor( //
1947 m_rgbColorSpace->reduceCielchD50ChromaToFitIntoGamut(lchValues));
1948 setCurrentOpaqueColor( //
1949 AbsoluteColor::allConversions(ColorModel::CielchD50, myColor),
1950 // widget that will ignored during updating:
1951 m_cielchD50SpinBox);
1952}
1953
1954/** @brief If no @ref m_isColorChangeInProgress, reads the Oklch numbers
1955 * in the dialog and updates the dialog accordingly. */
1956void ColorDialogPrivate::readOklchNumericValues()
1957{
1958 if (m_isColorChangeInProgress) {
1959 // Nothing to do!
1960 return;
1961 }
1962 // Get final color (in necessary moving the original color into gamut).
1963 // TODO xxx This code moves into gamut based on the Cielch-D50 instead of
1964 // the Oklch gamut. This leads to wrong results, because Oklch hue is not
1965 // guaranteed to be respected. Use actually Oklch to move into gamut!
1966 GenericColor originalOklch;
1967 originalOklch.first = m_oklchSpinBox->sectionValues().value(0);
1968 originalOklch.second = m_oklchSpinBox->sectionValues().value(1);
1969 originalOklch.third = m_oklchSpinBox->sectionValues().value(2);
1970 if (m_rgbColorSpace->isOklchInGamut(originalOklch)) {
1971 m_oklchSpinBoxGamutAction->setVisible(false);
1972 } else {
1973 m_oklchSpinBoxGamutAction->setVisible(true);
1974 }
1975 const auto inGamutOklch = GenericColor( //
1976 m_rgbColorSpace->reduceOklchChromaToFitIntoGamut(originalOklch));
1977 const auto inGamutColor = //
1978 AbsoluteColor::allConversions(ColorModel::OklchD65, inGamutOklch);
1979 setCurrentOpaqueColor(inGamutColor,
1980 // widget that will ignored during updating:
1981 m_oklchSpinBox);
1982}
1983
1984/** @brief Try to initialize the screen color picker feature.
1985 *
1986 * @post If supported, @ref m_screenColorPickerButton
1987 * is created. Otherwise, it stays <tt>nullptr</tt>. */
1988void ColorDialogPrivate::initializeScreenColorPicker()
1989{
1990 auto screenPicker = new ScreenColorPicker(q_pointer);
1991 if (!screenPicker->isAvailable()) {
1992 return;
1993 }
1994 m_screenColorPickerButton = new QToolButton;
1995 screenPicker->setParent(m_screenColorPickerButton); // For better support
1996 connect(m_screenColorPickerButton,
1998 screenPicker,
1999 // Default capture by reference, but screenPicker by value
2000 [&, screenPicker]() {
2001 const auto myColor = q_pointer->currentColor();
2002 // TODO Restore QColor exactly, but could potentially produce
2003 // rounding errors: If original MultiColor was derived form
2004 // LCH, it is not guaranteed that the new MultiColor derived
2005 // from this QColor will not have rounding errors for LCH.
2006 screenPicker->startPicking( //
2007 fromFloatingToEightBit(myColor.redF()), //
2008 fromFloatingToEightBit(myColor.greenF()), //
2009 fromFloatingToEightBit(myColor.blueF()));
2010 });
2011 connect(screenPicker, //
2012 &ScreenColorPicker::newColor, //
2013 q_pointer, //
2014 [this](const double red, const double green, const double blue, const bool isSRgbGuaranteed) {
2015 Q_UNUSED(isSRgbGuaranteed) // BUG Currently, there is no color
2016 // management for the result of the screen color picking.
2017 // Instead, we should assume probably that the value is sRGB
2018 // and convert it into the actual working color space of this
2019 // widget (which might happen to be also sRGB, but could also
2020 // be different).
2021 const GenericColor rgb255 //
2022 {qBound<double>(0, red * 255, 255), //
2023 qBound<double>(0, green * 255, 255),
2024 qBound<double>(0, blue * 255, 255)};
2025 setCurrentOpaqueColor(RgbColor::fromRgb255(rgb255), nullptr);
2026 });
2027}
2028
2029/** @brief Initialize the numeric input widgets of this dialog.
2030 * @returns A pointer to a new widget that has the other, numeric input
2031 * widgets as child widgets. */
2032QWidget *ColorDialogPrivate::initializeNumericPage()
2033{
2034 // Create RGB MultiSpinBox
2035 {
2036 m_rgbSpinBox = new MultiSpinBox();
2037 QList<MultiSpinBoxSection> rgbSections;
2038 MultiSpinBoxSection mySection;
2039 mySection.setDecimals(decimals);
2040 mySection.setMinimum(0);
2041 mySection.setMaximum(255);
2042 // R
2043 mySection.setFormatString( //
2044 QStringLiteral("%1") + m_multispinboxSectionSeparator);
2045 rgbSections.append(mySection);
2046 // G
2047 mySection.setFormatString( //
2048 m_multispinboxSectionSeparator + QStringLiteral("%1") //
2049 + m_multispinboxSectionSeparator);
2050 rgbSections.append(mySection);
2051 // B
2052 mySection.setFormatString( //
2053 m_multispinboxSectionSeparator + QStringLiteral("%1"));
2054 rgbSections.append(mySection);
2055 // Not setting prefix/suffix here. This will be done in retranslateUi()…
2056 m_rgbSpinBox->setSectionConfigurations(rgbSections);
2057 }
2058
2059 // Create widget for the hex style color representation
2060 {
2061 m_rgbLineEdit = new QLineEdit();
2062 m_rgbLineEdit->setMaxLength(7);
2063 QRegularExpression tempRegularExpression( //
2064 QStringLiteral(u"#?[0-9A-Fa-f]{0,6}"));
2065 QRegularExpressionValidator *validator = //
2066 new QRegularExpressionValidator(tempRegularExpression, q_pointer);
2067 m_rgbLineEdit->setValidator(validator);
2068 }
2069
2070 // Create HSL spin box
2071 {
2072 m_hslSpinBox = new MultiSpinBox();
2073 QList<MultiSpinBoxSection> hslSections;
2074 MultiSpinBoxSection mySection;
2075 mySection.setDecimals(decimals);
2076 // H
2077 mySection.setMinimum(0);
2078 mySection.setMaximum(360);
2079 mySection.setWrapping(true);
2080 hslSections.append(mySection);
2081 // S
2082 mySection.setMinimum(0);
2083 mySection.setMaximum(100);
2084 mySection.setWrapping(false);
2085 hslSections.append(mySection);
2086 // L
2087 mySection.setMinimum(0);
2088 mySection.setMaximum(100);
2089 mySection.setWrapping(false);
2090 hslSections.append(mySection);
2091 // Not setting prefix/suffix here. This will be done in retranslateUi()…
2092 m_hslSpinBox->setSectionConfigurations(hslSections);
2093 }
2094
2095 // Create HWB spin box
2096 {
2097 m_hwbSpinBox = new MultiSpinBox();
2098 QList<MultiSpinBoxSection> hwbSections;
2099 MultiSpinBoxSection mySection;
2100 mySection.setDecimals(decimals);
2101 // H
2102 mySection.setMinimum(0);
2103 mySection.setMaximum(360);
2104 mySection.setWrapping(true);
2105 hwbSections.append(mySection);
2106 // W
2107 mySection.setMinimum(0);
2108 mySection.setMaximum(100);
2109 mySection.setWrapping(false);
2110 hwbSections.append(mySection);
2111 // B
2112 mySection.setMinimum(0);
2113 mySection.setMaximum(100);
2114 mySection.setWrapping(false);
2115 hwbSections.append(mySection);
2116 // Not setting prefix/suffix here. This will be done in retranslateUi()…
2117 m_hwbSpinBox->setSectionConfigurations(hwbSections);
2118 }
2119
2120 // Create HSV spin box
2121 {
2122 m_hsvSpinBox = new MultiSpinBox();
2123 QList<MultiSpinBoxSection> hsvSections;
2124 MultiSpinBoxSection mySection;
2125 mySection.setDecimals(decimals);
2126 // H
2127 mySection.setMinimum(0);
2128 mySection.setMaximum(360);
2129 mySection.setWrapping(true);
2130 hsvSections.append(mySection);
2131 // S
2132 mySection.setMinimum(0);
2133 mySection.setMaximum(100);
2134 mySection.setWrapping(false);
2135 hsvSections.append(mySection);
2136 // V
2137 mySection.setMinimum(0);
2138 mySection.setMaximum(100);
2139 mySection.setWrapping(false);
2140 hsvSections.append(mySection);
2141 // Not setting prefix/suffix here. This will be done in retranslateUi()…
2142 m_hsvSpinBox->setSectionConfigurations(hsvSections);
2143 }
2144
2145 // Create RGB layout
2146 {
2147 QFormLayout *tempRgbFormLayout = new QFormLayout();
2148 m_rgbSpinBoxLabel = new QLabel();
2149 m_rgbSpinBoxLabel->setBuddy(m_rgbSpinBox);
2150 tempRgbFormLayout->addRow(m_rgbSpinBoxLabel, m_rgbSpinBox);
2151 m_rgbLineEditLabel = new QLabel();
2152 m_rgbLineEditLabel->setBuddy(m_rgbLineEdit);
2153 tempRgbFormLayout->addRow(m_rgbLineEditLabel, m_rgbLineEdit);
2154 m_hslSpinBoxLabel = new QLabel();
2155 m_hslSpinBoxLabel->setBuddy(m_hslSpinBox);
2156 tempRgbFormLayout->addRow(m_hslSpinBoxLabel, m_hslSpinBox);
2157 m_hwbSpinBoxLabel = new QLabel();
2158 m_hwbSpinBoxLabel->setBuddy(m_hwbSpinBox);
2159 tempRgbFormLayout->addRow(m_hwbSpinBoxLabel, m_hwbSpinBox);
2160 m_hsvSpinBoxLabel = new QLabel();
2161 m_hsvSpinBoxLabel->setBuddy(m_hsvSpinBox);
2162 tempRgbFormLayout->addRow(m_hsvSpinBoxLabel, m_hsvSpinBox);
2163 m_rgbGroupBox = new QGroupBox();
2164 m_rgbGroupBox->setLayout(tempRgbFormLayout);
2165 // Using the profile name as QGroupBox title. But on some styles, the
2166 // title is always shown completely, even if the text is extremly
2167 // long. As the text is out of our control, and some profiles
2168 // like Krita’s ITUR_2100_PQ_FULL.ICC have actually extremly
2169 // long names, we use eliding.
2170 const QFontMetricsF fontMetrics(m_rgbGroupBox->font());
2171 const auto elidedProfileName = fontMetrics.elidedText( //
2172 m_rgbColorSpace->profileName(),
2173 Qt::TextElideMode::ElideRight,
2174 // width (in device-independent pixels!):
2175 tempRgbFormLayout->minimumSize().width());
2176 m_rgbGroupBox->setTitle(elidedProfileName);
2177 }
2178
2179 // Create widget for the CIELCH-D50 color representation
2180 {
2181 QList<MultiSpinBoxSection> cielchD50Sections;
2182 m_cielchD50SpinBox = new MultiSpinBox;
2183 MultiSpinBoxSection mySection;
2184 mySection.setDecimals(decimals);
2185 // L
2186 mySection.setMinimum(0);
2187 mySection.setMaximum(100);
2188 mySection.setWrapping(false);
2189 cielchD50Sections.append(mySection);
2190 // C
2191 mySection.setMinimum(0);
2192 mySection.setMaximum(CielchD50Values::maximumChroma);
2193 mySection.setWrapping(false);
2194 cielchD50Sections.append(mySection);
2195 // H
2196 mySection.setMinimum(0);
2197 mySection.setMaximum(360);
2198 mySection.setWrapping(true);
2199 cielchD50Sections.append(mySection);
2200 // Not setting prefix/suffix here. This will be done in retranslateUi()…
2201 m_cielchD50SpinBox->setSectionConfigurations(cielchD50Sections);
2202 }
2203
2204 // Create widget for the Oklch color representation
2205 {
2206 QList<MultiSpinBoxSection> oklchSections;
2207 MultiSpinBoxSection mySection;
2208 m_oklchSpinBox = new MultiSpinBox;
2209 // L
2210 mySection.setMinimum(0);
2211 mySection.setMaximum(1);
2212 mySection.setSingleStep(singleStepOklabc);
2213 mySection.setWrapping(false);
2214 mySection.setDecimals(okdecimals);
2215 oklchSections.append(mySection);
2216 // C
2217 mySection.setMinimum(0);
2218 mySection.setMaximum(OklchValues::maximumChroma);
2219 mySection.setSingleStep(singleStepOklabc);
2220 mySection.setWrapping(false);
2221 mySection.setDecimals(okdecimals);
2222 oklchSections.append(mySection);
2223 // H
2224 mySection.setMinimum(0);
2225 mySection.setMaximum(360);
2226 mySection.setSingleStep(1);
2227 mySection.setWrapping(true);
2228 mySection.setDecimals(decimals);
2229 oklchSections.append(mySection);
2230 // Not setting the suffix here. This will be done in retranslateUi()…
2231 m_oklchSpinBox->setSectionConfigurations(oklchSections);
2232 }
2233
2234 // Create a global widget
2235 QWidget *tempWidget = new QWidget;
2236 QVBoxLayout *tempMainLayout = new QVBoxLayout;
2237 tempWidget->setLayout(tempMainLayout);
2239 QFormLayout *cielabFormLayout = new QFormLayout;
2240 m_oklchSpinBoxLabel = new QLabel();
2241 m_oklchSpinBoxLabel->setBuddy(m_oklchSpinBox);
2242 cielabFormLayout->addRow(m_oklchSpinBoxLabel, m_oklchSpinBox);
2243 m_cielchD50SpinBoxLabel = new QLabel();
2244 m_cielchD50SpinBoxLabel->setBuddy(m_cielchD50SpinBox);
2245 cielabFormLayout->addRow(m_cielchD50SpinBoxLabel, m_cielchD50SpinBox);
2246 tempMainLayout->addLayout(cielabFormLayout);
2247 tempMainLayout->addWidget(m_rgbGroupBox);
2248 tempMainLayout->addStretch();
2249
2250 // Return
2251 return tempWidget;
2252}
2253
2254// No documentation here (documentation of properties
2255// and its getters are in the header)
2257{
2258 return d_pointer->m_options;
2259}
2260
2261/** @brief Setter for @ref options.
2262 *
2263 * Sets a value for just one single option within @ref options.
2264 * @param option the option to set
2265 * @param on the new value of the option */
2267{
2268 QColorDialog::ColorDialogOptions temp = d_pointer->m_options;
2269 temp.setFlag(option, on);
2270 setOptions(temp);
2271}
2272
2273/** @brief Setter for @ref options
2274 * @param newOptions the new options
2275 * @post <em>All</em> options of the widget have the same state
2276 * (enabled/disabled) as in the given parameter. */
2278{
2279 if (newOptions == d_pointer->m_options) {
2280 return;
2281 }
2282
2283 // Save the new options
2284 d_pointer->m_options = newOptions;
2285 // Correct QColorDialog::ColorDialogOption::DontUseNativeDialog
2286 // which must be always on
2287 d_pointer->m_options.setFlag( //
2288 QColorDialog::ColorDialogOption::DontUseNativeDialog,
2289 true);
2290
2291 // Apply the new options (alpha value)
2292 const bool alphaVisibility = d_pointer->m_options.testFlag( //
2293 QColorDialog::ColorDialogOption::ShowAlphaChannel);
2294 d_pointer->m_alphaLabel->setVisible(alphaVisibility);
2295 d_pointer->m_alphaGradientSlider->setVisible(alphaVisibility);
2296 d_pointer->m_alphaSpinBox->setVisible(alphaVisibility);
2297
2298 // Apply the new options (buttons)
2299 d_pointer->m_buttonBox->setVisible(!d_pointer->m_options.testFlag( //
2300 QColorDialog::ColorDialogOption::NoButtons));
2301
2302 // Notify
2303 Q_EMIT optionsChanged(d_pointer->m_options);
2304}
2305
2306/** @brief Getter for @ref options
2307 *
2308 * Gets the value of just one single option within @ref options.
2309 *
2310 * @param option the requested option
2311 * @returns the value of the requested option
2312 */
2314{
2315 return d_pointer->m_options.testFlag(option);
2316}
2317
2318/** @brief Pops up a modal color dialog, lets the user choose a color, and
2319 * returns that color.
2320 *
2321 * @param colorSpace The color space within which this widget should operate.
2322 * @param initial initial value for currentColor()
2323 * @param parent parent widget of the dialog (or 0 for no parent)
2324 * @param title window title (or an empty string for the default window
2325 * title)
2326 * @param options the options() for customizing the look and feel of the
2327 * dialog
2328 * @returns selectedColor(): The color the user has selected; or an
2329 * invalid color if the user has canceled the dialog. */
2331 const QColor &initial,
2332 QWidget *parent,
2333 const QString &title,
2335{
2336 ColorDialog temp(colorSpace, parent);
2337 if (!title.isEmpty()) {
2338 temp.setWindowTitle(title);
2339 }
2340 temp.setOptions(options);
2341 // setCurrentColor() must be after setOptions()
2342 // to allow alpha channel support
2343 temp.setCurrentColor(initial);
2344 temp.exec();
2345 return temp.selectedColor();
2346}
2347
2348/** @brief Pops up a modal color dialog, lets the user choose a color, and
2349 * returns that color.
2350 *
2351 * @param initial initial value for currentColor()
2352 * @param parent parent widget of the dialog (or 0 for no parent)
2353 * @param title window title (or an empty string for the default window
2354 * title)
2355 * @param options the options() for customizing the look and feel of the
2356 * dialog
2357 * @returns selectedColor(): The color the user has selected; or an
2358 * invalid color if the user has canceled the dialog. */
2360{
2362 initial, //
2363 parent, //
2364 title, //
2365 options);
2366}
2367
2368/** @brief The color that was actually selected by the user.
2369 *
2370 * At difference to the @ref currentColor property, this function provides
2371 * the color that was actually selected by the user by clicking the OK button
2372 * or pressing the return key or another equivalent action.
2373 *
2374 * This function most useful to get the actually selected color <em>after</em>
2375 * that the dialog has been closed.
2376 *
2377 * When a dialog that had been closed or hidden is shown again,
2378 * this function returns to an invalid QColor().
2379 *
2380 * @returns Just after showing the dialog, the value is an invalid QColor. If
2381 * the user selects a color by clicking the OK button or another equivalent
2382 * action, the value is the selected color. If the user cancels the dialog
2383 * (Cancel button, or by pressing the Escape key), the value remains an
2384 * invalid QColor. */
2386{
2387 return d_pointer->m_selectedColor;
2388}
2389
2390/** @brief Setter for property <em>visible</em>
2391 *
2392 * Reimplemented from base class.
2393 *
2394 * When a dialog, that wasn't formerly visible, gets visible,
2395 * it’s @ref selectedColor value is cleared.
2396 *
2397 * @param visible holds whether or not the dialog should be visible */
2399{
2400 if (visible && (!isVisible())) {
2401 // Only delete the selected color if the dialog wasn’t visible before
2402 // and will be made visible now.
2403 d_pointer->m_selectedColor = QColor();
2404 d_pointer->applyLayoutDimensions();
2405 }
2407 // HACK If there is a QColorDialog as helper widget for the
2408 // screen color picker feature, QDialog::setVisible() sometimes
2409 // changes which is default button; however, this has only been
2410 // observed running the unit tests on KDE’s CI system running, but
2411 // not when running the unit tests locally. Force correct default button:
2412 d_pointer->m_buttonOK->setDefault(true);
2413}
2414
2415/** @brief Various updates when closing the dialog.
2416 *
2417 * Reimplemented from base class.
2418 * @param result The result with which the dialog has been closed */
2420{
2421 if (result == QDialog::DialogCode::Accepted) {
2422 d_pointer->m_selectedColor = currentColor();
2423 auto history = d_pointer->m_settings.history.value();
2424 const auto maxHistoryLenght = std::max(d_pointer->historySwatchCount, //
2425 history.count());
2426 // Remove duplicates of the new value that might exist yet in the list.
2427 history.removeAll(d_pointer->m_selectedColor);
2428 // Add the new value at the very beginning.
2429 history.prepend(d_pointer->m_selectedColor);
2430 // Adapt list length.
2431 while (history.count() > maxHistoryLenght) {
2432 history.removeLast();
2433 }
2434 d_pointer->m_settings.history.setValue(history);
2435 Q_EMIT colorSelected(d_pointer->m_selectedColor);
2436 } else {
2437 d_pointer->m_selectedColor = QColor();
2438 }
2440 if (d_pointer->m_receiverToBeDisconnected) {
2441 // This “disconnect” uses the old-style syntax, which does not
2442 // detect errors on compile time. However, we do not see a
2443 // possibility how to substitute it with the better new-style
2444 // syntax, given that d_pointer->m_memberToBeDisconnected
2445 // can contain different classes, which would be difficult
2446 // it typing the class name directly in the new syntax.
2447 disconnect(this, // sender
2448 SIGNAL(colorSelected(QColor)), // signal
2449 d_pointer->m_receiverToBeDisconnected, // receiver
2450 d_pointer->m_memberToBeDisconnected.constData() // slot
2451 );
2452 d_pointer->m_receiverToBeDisconnected = nullptr;
2453 }
2454}
2455
2456// No documentation here (documentation of properties
2457// and its getters are in the header)
2459{
2460 return d_pointer->m_layoutDimensions;
2461}
2462
2463/** @brief Setter for property @ref layoutDimensions
2464 * @param newLayoutDimensions the new layout dimensions */
2466{
2467 if (newLayoutDimensions == d_pointer->m_layoutDimensions) {
2468 return;
2469 }
2470 d_pointer->m_layoutDimensions = newLayoutDimensions;
2471 d_pointer->applyLayoutDimensions();
2472 Q_EMIT layoutDimensionsChanged(d_pointer->m_layoutDimensions);
2473}
2474
2475/** @brief Arranges the layout conforming to @ref ColorDialog::layoutDimensions
2476 *
2477 * If @ref ColorDialog::layoutDimensions is DialogLayoutDimensions::automatic
2478 * than it is first evaluated again if for the current display the collapsed
2479 * or the expanded layout is used. */
2480void ColorDialogPrivate::applyLayoutDimensions()
2481{
2482 constexpr auto collapsed = ColorDialog::DialogLayoutDimensions::Collapsed;
2483 constexpr auto expanded = ColorDialog::DialogLayoutDimensions::Expanded;
2484 // cppcheck-suppress unreadVariable // false positive
2485 constexpr auto screenSizeDependent = //
2487 int effectivelyAvailableScreenWidth;
2488 int widthThreeshold;
2489 switch (m_layoutDimensions) {
2490 case collapsed:
2491 m_layoutDimensionsEffective = collapsed;
2492 break;
2493 case expanded:
2494 m_layoutDimensionsEffective = expanded;
2495 break;
2496 case screenSizeDependent:
2497 // Note: The following code works correctly on scaled
2498 // devices (high-DPI…).
2499
2500 // We should not use more than 70% of the screen for a dialog.
2501 // That’s roughly the same as the default maximum sizes for
2502 // a QDialog.
2503 effectivelyAvailableScreenWidth = qRound( //
2504 QGuiApplication::primaryScreen()->availableSize().width() * 0.7);
2505
2506 // Now we calculate the space we need for displaying the
2507 // graphical selectors and the numerical selector at their
2508 // preferred size in an expanded layout.
2509 // Start with the size of the graphical selectors.
2510 widthThreeshold = qMax( //
2511 m_wheelColorPicker->sizeHint().width(), //
2512 m_lightnessFirstWrapperWidget->sizeHint().width());
2513 // Add the size of the numerical selector.
2514 widthThreeshold += m_numericalWidget->sizeHint().width();
2515 // Add some space for margins.
2516 widthThreeshold = qRound(widthThreeshold * 1.2);
2517
2518 // Now decide between collapsed layout and expanded layout
2519 if (effectivelyAvailableScreenWidth < widthThreeshold) {
2520 m_layoutDimensionsEffective = collapsed;
2521 } else {
2522 m_layoutDimensionsEffective = expanded;
2523 }
2524 break;
2525 default:
2526 // We should never reach this point, because we treat all possible
2527 // enum values in the switch statement.
2528 throw 0;
2529 }
2530
2531 if (m_layoutDimensionsEffective == collapsed) {
2532 if (m_selectorLayout->indexOf(m_numericalWidget) >= 0) {
2533 // Indeed we have expanded layout and have to switch to
2534 // collapsed layout…
2535 const bool oldUpdatesEnabled = m_tabWidget->updatesEnabled();
2536 m_tabWidget->setUpdatesEnabled(false);
2537 // According to the documentation of QTabWidget::addTab it is
2538 // recommended to disable visual updates during adding new
2539 // tabs. This should avoid flickering.
2540 m_tabWidget->addTab(m_numericalWidget, QString());
2541 m_tabWidget->setUpdatesEnabled(oldUpdatesEnabled);
2542 retranslateUi(); // Will put a label for the recently inserted tab.
2543 reloadIcons(); // Will put an icon for the recently inserted tab.
2544 // We don’t call m_numericalWidget->show(); because this
2545 // is controlled by the QTabWidget.
2546 // Adopt size of dialog to new layout’s size hint:
2547 q_pointer->adjustSize();
2548 }
2549 } else {
2550 if (m_selectorLayout->indexOf(m_numericalWidget) < 0) {
2551 // Indeed we have collapsed layout and have to switch to
2552 // expanded layout…
2553 m_selectorLayout->addWidget(m_numericalWidget);
2554 // We call show because the widget is hidden by removing it
2555 // from its old parent, and needs to be shown explicitly.
2556 m_numericalWidget->show();
2557 // Adopt size of dialog to new layout’s size hint:
2558 q_pointer->adjustSize();
2559 }
2560 }
2561}
2562
2563/** @brief Handle state changes.
2564 *
2565 * Implements reaction on <tt>QEvent::LanguageChange</tt>.
2566 *
2567 * Reimplemented from base class.
2568 *
2569 * @param event The event. */
2571{
2572 const auto type = event->type();
2573
2574 if (type == QEvent::LanguageChange) {
2575 // From QCoreApplication documentation:
2576 // “Installing or removing a QTranslator, or changing an installed
2577 // QTranslator generates a LanguageChange event for the
2578 // QCoreApplication instance. A QApplication instance will
2579 // propagate the event to all toplevel widgets […].
2580 // Retranslate this widget itself:
2581 d_pointer->retranslateUi();
2582 // Retranslate all child widgets that actually need to be retranslated:
2583 {
2584 QEvent eventForSwatchBookBasicColors(QEvent::LanguageChange);
2585 QApplication::sendEvent(d_pointer->m_swatchBookBasicColors, //
2586 &eventForSwatchBookBasicColors);
2587 }
2588 {
2589 QEvent eventForSwatchBookHistory(QEvent::LanguageChange);
2590 QApplication::sendEvent(d_pointer->m_swatchBookHistory, //
2591 &eventForSwatchBookHistory);
2592 }
2593 {
2594 QEvent eventForSwatchBookCustomColors(QEvent::LanguageChange);
2595 QApplication::sendEvent(d_pointer->m_swatchBookCustomColors, //
2596 &eventForSwatchBookCustomColors);
2597 }
2598 {
2599 QEvent eventForButtonOk(QEvent::LanguageChange);
2600 QApplication::sendEvent(d_pointer->m_buttonOK, //
2601 &eventForButtonOk);
2602 }
2603 {
2604 QEvent eventForButtonCancel(QEvent::LanguageChange);
2605 QApplication::sendEvent(d_pointer->m_buttonOK, //
2606 &eventForButtonCancel);
2607 }
2608 }
2609
2610 if ((type == QEvent::PaletteChange) || (type == QEvent::StyleChange)) {
2611 d_pointer->reloadIcons();
2612 }
2613
2615}
2616
2617/** @brief Handle show events.
2618 *
2619 * Reimplemented from base class.
2620 *
2621 * @param event The event.
2622 *
2623 * @internal
2624 *
2625 * On the first show event, make @ref ColorDialogPrivate::m_tabWidget use
2626 * the current tab corresponding to @ref ColorDialogPrivate::m_settings. */
2628{
2629 if (!d_pointer->everShown) {
2630 constexpr auto expValue = ColorDialog::DialogLayoutDimensions::Expanded;
2631 const bool exp = d_pointer->m_layoutDimensionsEffective == expValue;
2632 const auto tabString = exp //
2633 ? d_pointer->m_settings.tabExpanded.value() //
2634 : d_pointer->m_settings.tab.value();
2635 const auto key = d_pointer->m_tabTable.key(tabString, nullptr);
2636 if (key != nullptr) {
2637 d_pointer->m_tabWidget->setCurrentWidget(*key);
2638 }
2639 // Save the new tab explicitly. If setCurrentWidget() is not
2640 // different from the default value, it does not trigger the
2641 // QTabWidget::currentChanged() signal, resulting in the tab
2642 // not being saved. However, we want to ensure that the tab
2643 // is saved whenever the user has first seen it.
2644 d_pointer->saveCurrentTab();
2645
2646 switch (d_pointer->m_settings.swatchBookPage.value()) {
2647 case PerceptualSettings::SwatchBookPage::BasicColors:
2648 default:
2649 d_pointer->m_settings.swatchBookPage.setValue( //
2650 PerceptualSettings::SwatchBookPage::BasicColors);
2651 d_pointer->m_swatchBookSelector->setCurrentIndex(0);
2652 d_pointer->m_swatchBookStack->setCurrentIndex(0);
2653 break;
2654 case PerceptualSettings::SwatchBookPage::History:
2655 d_pointer->m_settings.swatchBookPage.setValue( //
2656 PerceptualSettings::SwatchBookPage::History);
2657 d_pointer->m_swatchBookSelector->setCurrentIndex(1);
2658 d_pointer->m_swatchBookStack->setCurrentIndex(1);
2659 break;
2660 case PerceptualSettings::SwatchBookPage::CustomColors:
2661 d_pointer->m_settings.swatchBookPage.setValue( //
2662 PerceptualSettings::SwatchBookPage::History);
2663 d_pointer->m_swatchBookSelector->setCurrentIndex(2);
2664 d_pointer->m_swatchBookStack->setCurrentIndex(2);
2665 break;
2666 }
2667
2668 d_pointer->everShown = true;
2669 }
2671}
2672
2673/** @brief Saves the current tab of @ref m_tabWidget to @ref m_settings. */
2674void ColorDialogPrivate::saveCurrentTab()
2675{
2676 const auto currentIndex = m_tabWidget->currentIndex();
2677 QWidget const *const widget = m_tabWidget->widget(currentIndex);
2678 const auto keyList = m_tabTable.keys();
2679 auto it = std::find_if( //
2680 keyList.begin(),
2681 keyList.end(),
2682 [widget](const auto &key) {
2683 return ((*key) == widget);
2684 } //
2685 );
2686 if (it != keyList.end()) {
2687 const auto tabString = m_tabTable.value(*it);
2688 constexpr auto expValue = ColorDialog::DialogLayoutDimensions::Expanded;
2689 if (m_layoutDimensionsEffective == expValue) {
2690 m_settings.tabExpanded.setValue(tabString);
2691 } else {
2692 m_settings.tab.setValue(tabString);
2693 }
2694 }
2695}
2696
2697/** @brief Loads @ref PerceptualSettings::history into
2698 * @ref m_swatchBookHistory. */
2699void ColorDialogPrivate::loadHistoryFromSettingsToSwatchBook()
2700{
2701 const Swatches historyArray(historyHSwatchCount, //
2702 historyVSwatchCount, //
2703 m_settings.history.value());
2704 m_swatchBookHistory->setSwatchGrid(historyArray);
2705}
2706
2707/** @brief Loads @ref PerceptualSettings::customColors into
2708 * @ref m_swatchBookCustomColors. */
2709void ColorDialogPrivate::loadCustomColorsFromSettingsToSwatchBook()
2710{
2711 const Swatches customColorsArray(customColorsHSwatchCount, //
2712 customColorsVSwatchCount, //
2713 m_settings.customColors.value());
2714 m_swatchBookCustomColors->setSwatchGrid(customColorsArray);
2715}
2716
2717} // namespace PerceptualColor
void currentColorCielchD50Changed(const PerceptualColor::GenericColor &newCurrentColorCielchD50)
Notify signal for property currentColorCielchD50.
A perceptually uniform color picker dialog.
void colorSelected(const QColor &color)
This signal is emitted just after the user has clicked OK to select a color to use.
static QColor getColor(const QColor &initial=Qt::white, QWidget *parent=nullptr, const QString &title=QString(), ColorDialogOptions options=ColorDialogOptions())
Pops up a modal color dialog, lets the user choose a color, and returns that color.
virtual void showEvent(QShowEvent *event) override
Handle show events.
virtual void setVisible(bool visible) override
Setter for property visible
Q_INVOKABLE void open(QObject *receiver, const char *member)
Opens the dialog and connects its colorSelected() signal to the slot specified by receiver and member...
QColorDialog::ColorDialogOptions ColorDialogOptions
Local alias for QColorDialog::ColorDialogOptions.
Q_INVOKABLE ColorDialog(QWidget *parent=nullptr)
Constructor.
ColorDialogOptions options
Various options that affect the look and feel of the dialog.
Q_INVOKABLE bool testOption(PerceptualColor::ColorDialog::ColorDialogOption option) const
Getter for KConfig Entry Options.
Q_INVOKABLE void setOption(PerceptualColor::ColorDialog::ColorDialogOption option, bool on=true)
Setter for KConfig Entry Options.
QColor currentColor
Currently selected color in the dialog.
void setCurrentColor(const QColor &color)
Setter for currentColor property.
virtual void done(int result) override
Various updates when closing the dialog.
virtual ~ColorDialog() noexcept override
Destructor.
void optionsChanged(const PerceptualColor::ColorDialog::ColorDialogOptions newOptions)
Notify signal for property KConfig Entry Options.
void setOptions(PerceptualColor::ColorDialog::ColorDialogOptions newOptions)
Setter for KConfig Entry Options.
void setLayoutDimensions(const PerceptualColor::ColorDialog::DialogLayoutDimensions newLayoutDimensions)
Setter for property layoutDimensions.
DialogLayoutDimensions
Layout dimensions.
@ Expanded
Use the large, “expanded” layout of this dialog.
@ Collapsed
Use the small, “collapsed“ layout of this dialog.
@ ScreenSizeDependent
Decide automatically between collapsed and expanded layout: collapsed is used on small screens,...
virtual void changeEvent(QEvent *event) override
Handle state changes.
void layoutDimensionsChanged(const PerceptualColor::ColorDialog::DialogLayoutDimensions newLayoutDimensions)
Notify signal for property layoutDimensions.
Q_INVOKABLE QColor selectedColor() const
The color that was actually selected by the user.
QColorDialog::ColorDialogOption ColorDialogOption
Local alias for QColorDialog::ColorDialogOption.
DialogLayoutDimensions layoutDimensions
Layout dimensions.
void colorChanged(const QColor &color)
Notify signal for property color.
void valueChanged(const qreal newValue)
Signal for value property.
void sectionValuesChanged(const QList< double > &newSectionValues)
Notify signal for property sectionValues.
static QSharedPointer< PerceptualColor::RgbColorSpace > createSrgb()
Create an sRGB color space object.
void currentColorCielchD50Changed(const PerceptualColor::GenericColor &newCurrentColorCielchD50)
Notify signal for property currentColorCielchD50.
QString name(StandardAction id)
The namespace of this library.
Array2D< QColor > Swatches
Swatches organized in a grid.
Definition helper.h:262
Swatches wcsBasicColors(const QSharedPointer< PerceptualColor::RgbColorSpace > &colorSpace)
Swatch grid derived from the basic colors as by WCS (World color survey).
Definition helper.cpp:491
@ OklchD65
The absolute Oklch color space, which by definition always and exclusively uses a D65 illuminant.
@ CielchD50
The absolute Cielch color space using a D50 illuminant.
void clicked(bool checked)
void editingFinished()
void triggered(bool checked)
void addLayout(QLayout *layout, int stretch)
void addStretch(int stretch)
void addWidget(QWidget *widget, int stretch, Qt::Alignment alignment)
QChar fromLatin1(char c)
void setNamedColor(QLatin1StringView name)
float alphaF() const const
QColor fromRgbF(float r, float g, float b, float a)
bool isValid() const const
QRgba64 rgba64() const const
void setAlphaF(float alpha)
QColor toRgb() const const
typedef ColorDialogOptions
void currentIndexChanged(int index)
QCoreApplication * instance()
bool sendEvent(QObject *receiver, QEvent *event)
bool isNull() const const
QDialog(QWidget *parent, Qt::WindowFlags f)
virtual void accept()
virtual void done(int r)
virtual int exec()
virtual void open()
virtual void reject()
int result() const const
virtual void setVisible(bool visible) override
virtual void showEvent(QShowEvent *event) override
void valueChanged(double d)
void addRow(QLayout *layout)
virtual QSize minimumSize() const const override
QList< Key > keys() const const
T value(const Key &key) const const
QKeySequence mnemonic(const QString &text)
void editingFinished()
void textChanged(const QString &text)
void append(QList< T > &&value)
qsizetype count() const const
bool isEmpty() const const
QObject(QObject *parent)
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
QObject * parent() const const
QString tr(const char *sourceText, const char *disambiguation, int n)
void activated()
void activatedAmbiguously()
int width() const const
QString arg(Args &&... args) const const
bool isEmpty() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QString toHtmlEscaped() const const
QString join(QChar separator) const const
void colorSchemeChanged(Qt::ColorScheme colorScheme)
typedef Alignment
WindowContextHelpButtonHint
void currentChanged(int index)
QString toString() const const
QWidget(QWidget *parent, Qt::WindowFlags f)
virtual void changeEvent(QEvent *event)
virtual bool event(QEvent *event) override
void setLayout(QLayout *layout)
void setSizePolicy(QSizePolicy)
void setWindowTitle(const QString &)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri May 2 2025 12:04:47 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.