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

KDE's Doxygen guidelines are available online.