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

KDE's Doxygen guidelines are available online.