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

KDE's Doxygen guidelines are available online.