File indexing completed on 2024-05-12 04:44:31

0001 // SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
0002 // SPDX-License-Identifier: BSD-2-Clause OR MIT
0003 
0004 // Own headers
0005 // First the interface, which forces the header to be self-contained.
0006 #include "colordialog.h"
0007 // Second, the private implementation.
0008 #include "colordialog_p.h" // IWYU pragma: associated
0009 
0010 #include "absolutecolor.h"
0011 #include "chromahuediagram.h"
0012 #include "cielchd50values.h"
0013 #include "colorpatch.h"
0014 #include "constpropagatingrawpointer.h"
0015 #include "constpropagatinguniquepointer.h"
0016 #include "gradientslider.h"
0017 #include "helper.h"
0018 #include "helperconstants.h"
0019 #include "helperconversion.h"
0020 #include "helperqttypes.h"
0021 #include "initializetranslation.h"
0022 #include "lchadouble.h"
0023 #include "lchdouble.h"
0024 #include "multispinbox.h"
0025 #include "multispinboxsection.h"
0026 #include "oklchvalues.h"
0027 #include "rgbcolor.h"
0028 #include "rgbcolorspace.h"
0029 #include "rgbcolorspacefactory.h"
0030 #include "screencolorpicker.h"
0031 #include "setting.h"
0032 #include "swatchbook.h"
0033 #include "wheelcolorpicker.h"
0034 #include <algorithm>
0035 #include <lcms2.h>
0036 #include <optional>
0037 #include <qaction.h>
0038 #include <qapplication.h>
0039 #include <qboxlayout.h>
0040 #include <qbytearray.h>
0041 #include <qchar.h>
0042 #include <qcoreapplication.h>
0043 #include <qcoreevent.h>
0044 #include <qdatetime.h>
0045 #include <qdebug.h>
0046 #include <qdialogbuttonbox.h>
0047 #include <qfontmetrics.h>
0048 #include <qformlayout.h>
0049 #include <qgroupbox.h>
0050 #include <qguiapplication.h>
0051 #include <qicon.h>
0052 #include <qkeysequence.h>
0053 #include <qlabel.h>
0054 #include <qlineedit.h>
0055 #include <qlist.h>
0056 #include <qlocale.h>
0057 #include <qobject.h>
0058 #include <qpair.h>
0059 #include <qpointer.h>
0060 #include <qpushbutton.h>
0061 #include <qregularexpression.h>
0062 #include <qscopedpointer.h>
0063 #include <qscreen.h>
0064 #include <qsharedpointer.h>
0065 #include <qshortcut.h>
0066 #include <qsize.h>
0067 #include <qsizepolicy.h>
0068 #include <qspinbox.h>
0069 #include <qstringbuilder.h>
0070 #include <qstringliteral.h>
0071 #include <qtabwidget.h>
0072 #include <qtoolbutton.h>
0073 #include <qvalidator.h>
0074 #include <qversionnumber.h>
0075 #include <qwidget.h>
0076 #include <utility>
0077 class QShowEvent;
0078 
0079 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
0080 #include <qcontainerfwd.h>
0081 #include <qobjectdefs.h>
0082 #else
0083 #include <qstringlist.h>
0084 #endif
0085 
0086 #if (QT_VERSION >= QT_VERSION_CHECK(6, 5, 0))
0087 #include <qstylehints.h>
0088 #endif
0089 
0090 namespace PerceptualColor
0091 {
0092 
0093 /** @brief A text with the name of the color model.
0094  *
0095  * @param model The signature of the color model.
0096  *
0097  * @returns A text with the name of the color model, or an empty
0098  * QString if the model is unknown. If a translation is available,
0099  * the translation is returned instead of the original English text. */
0100 QString ColorDialogPrivate::translateColorModel(cmsColorSpaceSignature model)
0101 {
0102     switch (model) {
0103     case cmsSigXYZData:
0104         /*: @item A color model: X, Y, Z. */
0105         return tr("XYZ");
0106     case cmsSigLabData:
0107         /*: @item A color model: Lightness, a, b. */
0108         return tr("Lab");
0109     case cmsSigRgbData:
0110         /*: @item A color model: red, green, blue. */
0111         return tr("RGB");
0112     case cmsSigLuvData:
0113         // return tr("Luv"); // Currently not supported.
0114         return QString();
0115     case cmsSigYCbCrData:
0116         // return tr("YCbCr"); // Currently not supported.
0117         return QString();
0118     case cmsSigYxyData:
0119         // return tr("Yxy"); // Currently not supported.
0120         return QString();
0121     case cmsSigGrayData:
0122         // return tr("Grayscale"); // Currently not supported.
0123         return QString();
0124     case cmsSigHsvData:
0125         // return tr("HSV"); // Currently not supported.
0126         return QString();
0127     case cmsSigHlsData:
0128         // return tr("HSL"); // Currently not supported.
0129         return QString();
0130     case cmsSigCmykData:
0131         // return tr("CMYK"); // Currently not supported.
0132         return QString();
0133     case cmsSigCmyData:
0134         // return tr("CMY"); // Currently not supported.
0135         return QString();
0136     case cmsSigNamedData: // Does not exist in ICC 4.4.
0137     case cmsSig2colorData:
0138     case cmsSig3colorData:
0139     case cmsSig4colorData:
0140     case cmsSig5colorData:
0141     case cmsSig6colorData:
0142     case cmsSig7colorData:
0143     case cmsSig8colorData:
0144     case cmsSig9colorData:
0145     case cmsSig10colorData:
0146     case cmsSig11colorData:
0147     case cmsSig12colorData:
0148     case cmsSig13colorData:
0149     case cmsSig14colorData:
0150     case cmsSig15colorData:
0151         // return tr("Named color"); // Currently not supported.
0152         return QString();
0153     case cmsSig1colorData:
0154     case cmsSigLuvKData:
0155     case cmsSigMCH1Data:
0156     case cmsSigMCH2Data:
0157     case cmsSigMCH3Data:
0158     case cmsSigMCH4Data:
0159     case cmsSigMCH5Data:
0160     case cmsSigMCH6Data:
0161     case cmsSigMCH7Data:
0162     case cmsSigMCH8Data:
0163     case cmsSigMCH9Data:
0164     case cmsSigMCHAData:
0165     case cmsSigMCHBData:
0166     case cmsSigMCHCData:
0167     case cmsSigMCHDData:
0168     case cmsSigMCHEData:
0169     case cmsSigMCHFData:
0170     // Unhandeled: These values do not exist in ICC 4.4 standard as
0171     // published at https://www.color.org/specification/ICC.1-2022-05.pdf
0172     // page 35, table 19 — Data colour space signatures.
0173     default:
0174         break;
0175     }
0176     return QString();
0177 }
0178 
0179 /** @brief Retranslate the UI with all user-visible strings.
0180  *
0181  * This function updates all user-visible strings by using
0182  * <tt>Qt::tr()</tt> to get up-to-date translations.
0183  *
0184  * This function is meant to be called at the end of the constructor and
0185  * additionally after each <tt>QEvent::LanguageChange</tt> event.
0186  *
0187  * @note This is the same concept as
0188  * <a href="https://doc.qt.io/qt-5/designer-using-a-ui-file.html">
0189  * Qt Designer, which also provides a function of the same name in
0190  * uic-generated code</a>.
0191  *
0192  * @internal
0193  *
0194  * @todo Add to the color-space tooltip information about available rendering
0195  * intents (we have yet RgbColorSpacePrivate::intentList but do not use it
0196  * anywhere) and the RGB profile illuminant? (This would have to be implemented
0197  * in @ref RgbColorSpace first.)
0198  *
0199  * @todo As the tooltip for color-space information is quite big, would
0200  * it be better to do what systemsettings does in globaldesign/fonts? They
0201  * have a small button with an “i” symbol (for information), which does
0202  * nothing when it’s clicked, but when hovering with the mouse, it shows
0203  * the tooltip?
0204  *
0205  * @todo How to make tooltip information available for touch-screen users? */
0206 void ColorDialogPrivate::retranslateUi()
0207 {
0208     /*: @item/plain Percentage value in a spinbox. Range: 0%–100%. */
0209     const QPair<QString, QString> percentageInSpinbox = //
0210         getPrefixSuffix(tr("%1%"));
0211 
0212     /*: @item/plain Arc-degree value in a spinbox. Range: 0°–360°. */
0213     const QPair<QString, QString> arcDegreeInSpinbox = //
0214         getPrefixSuffix(tr("%1°"));
0215 
0216     QStringList profileInfo;
0217     const QString name = //
0218         m_rgbColorSpace->profileName().toHtmlEscaped();
0219     if (!name.isEmpty()) {
0220         /*: @item:intext An information from the color profile to be added
0221         to the info text about current color space. */
0222         profileInfo.append(tableRow.arg(tr("Name:"), name));
0223     }
0224     /*: @item:intext The maximum chroma. */
0225     const QString maximumCielchD50Chroma = //
0226         tr("%L1 (estimated)")
0227             .arg(m_rgbColorSpace->profileMaximumCielchD50Chroma(), //
0228                  0, //
0229                  'f', //
0230                  decimals);
0231     /*: @item:intext An information from the color profile to be added
0232     to the info text about current color space. */
0233     profileInfo.append( //
0234         tableRow.arg(tr("Maximum CIELCh-D50 chroma:"), maximumCielchD50Chroma));
0235     /*: @item:intext The maximum chroma. */
0236     const QString maximumOklchChroma = //
0237         tr("%L1 (estimated)")
0238             .arg(m_rgbColorSpace->profileMaximumOklchChroma(), //
0239                  0, //
0240                  'f', //
0241                  okdecimals);
0242     /*: @item:intext An information from the color profile to be added
0243     to the info text about current color space. */
0244     profileInfo.append( //
0245         tableRow.arg(tr("Maximum Oklch chroma:"), maximumOklchChroma));
0246     QString profileClass;
0247     switch (m_rgbColorSpace->profileClass()) {
0248     case cmsSigDisplayClass:
0249         /*: @item:intext The class of an ICC profile. */
0250         profileClass = tr("Display profile");
0251         break;
0252     case cmsSigAbstractClass: // Image effect profile (Abstract profile)
0253                               // This ICC profile class is called "abstract
0254                               // profile" in the official standard. However,
0255                               // the name is misleading. The actual function of
0256                               // these ICC profiles is to apply image effects.
0257     case cmsSigColorSpaceClass: // Color space conversion profile
0258     case cmsSigInputClass: // Input profile
0259     case cmsSigLinkClass: // Device link profile
0260     case cmsSigNamedColorClass: // Named color profile
0261     case cmsSigOutputClass: // Output profile
0262         // These profile classes are currently not supported.
0263         break;
0264     }
0265     if (!profileClass.isEmpty()) {
0266         /*: @item:intext An information from the color profile to be added
0267         to the info text about current color space. */
0268         profileInfo.append( //
0269             tableRow.arg(tr("Profile class:"), profileClass));
0270     }
0271     const QString colorModel = //
0272         translateColorModel(m_rgbColorSpace->profileColorModel());
0273     if (!colorModel.isEmpty()) {
0274         /*: @item:intext An information from the color profile to be added
0275         to the info text about current color space.
0276         The color model of the color space which is described by this
0277         profile. */
0278         profileInfo.append(tableRow.arg(tr("Color model:"), colorModel));
0279     }
0280     const QString manufacturer = //
0281         m_rgbColorSpace->profileManufacturer().toHtmlEscaped();
0282     if (!manufacturer.isEmpty()) {
0283         /*: @item:intext An information from the color profile to be added
0284         to the info text about current color space.
0285         This is usually the manufacturer of the device to which
0286         the colour profile applies. */
0287         profileInfo.append(tableRow.arg(tr("Manufacturer:"), manufacturer));
0288     }
0289     const QString model = //
0290         m_rgbColorSpace->profileModel().toHtmlEscaped();
0291     if (!model.isEmpty()) {
0292         /*: @item:intext An information from the color profile to be added to
0293         the info text about current color space.
0294         This is usually the model identifier of the device to which
0295         the colour profile applies. */
0296         profileInfo.append(tableRow.arg(tr("Device model:"), (model)));
0297     }
0298     const QDateTime creationDateTime = //
0299         m_rgbColorSpace->profileCreationDateTime();
0300     if (!creationDateTime.isNull()) {
0301         const auto creationDateTimeString = QLocale().toString(
0302             // Date and time:
0303             creationDateTime,
0304             // Format:
0305             QLocale::LongFormat);
0306         /*: @item:intext An information from the color profile to be added to
0307         the info text about current color space.
0308         This is the date and time of the creation of the profile. */
0309         profileInfo.append( //
0310             tableRow.arg(tr("Created:"), (creationDateTimeString)));
0311     }
0312     const QVersionNumber iccVersion = m_rgbColorSpace->profileIccVersion();
0313     /*: @item:intext An information from the color profile to be added to
0314     the info text about current color space.
0315     This is the version number of the ICC file format that is used. */
0316     profileInfo.append( //
0317         tableRow.arg(tr("ICC format:"), (iccVersion.toString())));
0318     const bool hasMatrixShaper = //
0319         m_rgbColorSpace->profileHasMatrixShaper();
0320     const bool hasClut = //
0321         m_rgbColorSpace->profileHasClut();
0322     if (hasMatrixShaper || hasClut) {
0323         const QString matrixShaperString = tableRow.arg(
0324             /*: @item:intext An information from the color profile to be added
0325             to the info text about current color space.
0326             Wether the profile has a matrix shaper or a color lookup table
0327             (CLUT) or both. */
0328             tr("Implementation:"));
0329         if (hasMatrixShaper && hasClut) {
0330             /*: @item:intext An information from the color profile to be added
0331             to the info text about current color space.
0332             Wether the profile has a matrix shaper or a color lookup table
0333             (CLUT) or both. */
0334             profileInfo.append( //
0335                 matrixShaperString.arg(tr("Matrices and color lookup tables")));
0336         } else if (hasMatrixShaper) {
0337             /*: @item:intext An information from the color profile to be added
0338             to the info text about current color space.
0339             Wether the profile has a matrix shaper or a color lookup table
0340             (CLUT) or both. */
0341             profileInfo.append(matrixShaperString.arg(tr("Matrices")));
0342         } else if (hasClut) {
0343             /*: @item:intext An information from the color profile to be added
0344             to the info text about current color space.
0345             Wether the profile has a matrix shaper or a color lookup table
0346             (CLUT) or both. */
0347             profileInfo.append( //
0348                 matrixShaperString.arg(tr("Color lookup tables")));
0349         }
0350     }
0351     const QString pcsColorModelText = //
0352         translateColorModel(m_rgbColorSpace->profilePcsColorModel());
0353     if (!pcsColorModelText.isEmpty()) {
0354         /*: @item:intext An information from the color profile to be added
0355         to the info text about current color space.
0356         The color model of the PCS (profile connection space) which is used
0357         internally by this profile. */
0358         profileInfo.append( //
0359             tableRow.arg(tr("PCS color model:"), pcsColorModelText));
0360     }
0361     const QString copyright = m_rgbColorSpace->profileCopyright();
0362     if (!copyright.isEmpty()) {
0363         /*: @item:intext An information from the color profile to be added
0364         to the info text about current color space.
0365         The copyright of this profile. */
0366         profileInfo.append(tableRow.arg(tr("Copyright:"), copyright));
0367     }
0368     const qint64 fileSize = //
0369         m_rgbColorSpace->profileFileSize();
0370     if (fileSize >= 0) {
0371         /*: @item:intext An information from the color profile to be added to
0372         the info text about current color space.
0373         This is the size of the ICC file that was read in. */
0374         profileInfo.append(tableRow.arg(tr("File size:"), //
0375                                         QLocale().formattedDataSize(fileSize)));
0376     }
0377     const QString fileName = //
0378         m_rgbColorSpace->profileAbsoluteFilePath();
0379     if (!fileName.isEmpty()) {
0380         /*: @item:intext An information from the color profile to be added to
0381         the info text about current color space. */
0382         profileInfo.append(tableRow.arg(tr("File name:"), fileName));
0383     }
0384     if (profileInfo.isEmpty()) {
0385         m_rgbGroupBox->setToolTip(QString());
0386     } else {
0387         const QString tableString = QStringLiteral(
0388             "<b>%1</b><br/>"
0389             "<table border=\"0\" cellpadding=\"2\" cellspacing=\"0\">"
0390             "%2"
0391             "</table>");
0392         m_rgbGroupBox->setToolTip(richTextMarker
0393                                   + tableString.arg(
0394                                       /*: @info:intext Title of info text about
0395                                       current color space (will be followed by
0396                                       other information as available
0397                                       in the color profile. */
0398                                       tr("Color space information"), //
0399                                       profileInfo.join(QString())));
0400     }
0401 
0402     /*: @label:spinbox Label for CIE’s CIEHLC color model, based on Hue,
0403     Lightness, Chroma, and using the D50 illuminant as white point.*/
0404     m_ciehlcD50SpinBoxLabel->setText(tr("CIEHL&C D50:"));
0405 
0406     /*: @label:spinbox Label for Oklch color model, based on Lightness, Chroma,
0407     Hue, and using the D65 illuminant as white point. */
0408     m_oklchSpinBoxLabel->setText(tr("O&klch:"));
0409 
0410     /*: @label:spinbox Label for RGB color model, based on Red, Green, Blue. */
0411     m_rgbSpinBoxLabel->setText(tr("&RGB:"));
0412 
0413     /*: @label:textbox Label for hexadecimal RGB representation like #12ab45 */
0414     m_rgbLineEditLabel->setText(tr("He&x:"));
0415 
0416     const int swatchBookIndex = m_tabWidget->indexOf(m_swatchBookWrapperWidget);
0417     if (swatchBookIndex >= 0) {
0418         /*: @title:tab
0419         The tab contains a swatch book showing the basic colors like yellow,
0420         orange, red… Same text as in QColorDialog */
0421         const auto mnemonic = tr("&Basic colors");
0422         m_tabWidget->setTabToolTip( //
0423             swatchBookIndex, //
0424             richTextMarker + fromMnemonicToRichText(mnemonic));
0425         m_swatchBookTabShortcut->setKey(QKeySequence::mnemonic(mnemonic));
0426     }
0427     const int hueFirstIndex = m_tabWidget->indexOf(m_hueFirstWrapperWidget);
0428     if (hueFirstIndex >= 0) {
0429         /*: @title:tab
0430         The tab contains a visual UI to choose first the hue, and in a
0431         second step chroma and lightness. */
0432         const auto mnemonic = tr("&Hue-based");
0433         m_tabWidget->setTabToolTip( //
0434             hueFirstIndex, //
0435             richTextMarker + fromMnemonicToRichText(mnemonic));
0436         m_hueFirstTabShortcut->setKey(QKeySequence::mnemonic(mnemonic));
0437     }
0438     const int lightnessFirstIndex = //
0439         m_tabWidget->indexOf(m_lightnessFirstWrapperWidget);
0440     if (lightnessFirstIndex >= 0) {
0441         /*: @title:tab
0442         The tab contains a visual UI to choose first the lightness, and in a
0443         second step chroma and hue.
0444         “Lightness” is different from “brightness”/“value”
0445         and should therefore get a different translation. */
0446         const auto mnemonic = tr("&Lightness-based");
0447         m_tabWidget->setTabToolTip( //
0448             lightnessFirstIndex, //
0449             richTextMarker + fromMnemonicToRichText(mnemonic));
0450         m_lightnessFirstTabShortcut->setKey(QKeySequence::mnemonic(mnemonic));
0451     }
0452     const int numericIndex = //
0453         m_tabWidget->indexOf(m_numericalWidget);
0454     if (numericIndex >= 0) {
0455         /*: @title:tab
0456         The tab contains a UI to describe the color with numbers: Spin boxes
0457         and line edits containing values like “#2A7845” or “RGB 85 45 12”. */
0458         const auto mnemonic = tr("&Numeric");
0459         m_tabWidget->setTabToolTip( //
0460             numericIndex, //
0461             richTextMarker + fromMnemonicToRichText(mnemonic));
0462         m_numericalTabShortcut->setKey(QKeySequence::mnemonic(mnemonic));
0463     }
0464 
0465     /*: @label:spinbox HSL (hue, saturation, lightness) */
0466     m_hslSpinBoxLabel->setText(tr("HS&L:"));
0467 
0468     /*: @label:spinbox HSV (hue, saturation, value) and HSB (hue, saturation,
0469     brightness) are two different names for the very same color model. */
0470     m_hsvSpinBoxLabel->setText(tr("HS&V/HSB:"));
0471 
0472     /*: @label:spinbox HWB (hue, whiteness, blackness) */
0473     m_hwbSpinBoxLabel->setText(tr("H&WB:"));
0474 
0475     /*: @action:button */
0476     m_buttonOK->setText(tr("&OK"));
0477 
0478     /*: @action:button */
0479     m_buttonCancel->setText(tr("&Cancel"));
0480     /*: @info:tooltip Help text for RGB spinbox. */
0481     m_rgbSpinBox->setToolTip( //
0482         richTextMarker
0483         + tr("<p>Red: 0⁠–⁠255</p>"
0484              "<p>Green: 0⁠–⁠255</p>"
0485              "<p>Blue: 0⁠–⁠255</p>"));
0486 
0487     /*: @info:tooltip Help text for hexadecimal code. */
0488     m_rgbLineEdit->setToolTip( //
0489         richTextMarker
0490         + tr("<p>Hexadecimal color code, as used in HTML: #RRGGBB</p>"
0491              "<p>RR: two-digit code for red: 00⁠–⁠FF</p>"
0492              "<p>GG: two-digit code for green: 00⁠–⁠FF</p>"
0493              "<p>BB: two-digit code for blue: 00⁠–⁠FF</p>"));
0494 
0495     /*: @info:tooltip Help text for HSL (hue, saturation, lightness).
0496     Saturation: 0 means something on the grey axis; 255 means something
0497     between the grey axis and the most colorful color. This is different
0498     from “chroma” and should therefore get a different translation.
0499     Lightness: 0 means always black; 255 means always white. This is
0500     different from “brightness” and should therefore get a different
0501     translation. */
0502     m_hslSpinBox->setToolTip(richTextMarker
0503                              + tr("<p>Hue: 0°⁠–⁠360°</p>"
0504                                   "<p>HSL-Saturation: 0%⁠–⁠100%</p>"
0505                                   "<p>Lightness: 0%⁠–⁠100%</p>"));
0506 
0507     /*: @info:tooltip Help text for HWB (hue, whiteness, blackness).
0508     The idea behind is that the hue defines the pure (maximum colorful) color.
0509     Than, white color can be added, creating a “tint”. Or black color
0510     can be added, creating a “shade”. Or both can be added, creating a “tone“.
0511     See https://en.wikipedia.org/wiki/Tint,_shade_and_tone for more
0512     information. 0% white + 0% black = pure color. 100% white
0513     + 0% black = white. 0% white + 100% black = black. 50% white + 50% black
0514     = gray. 50% white + 0% black = tint. 25% white + 25% black = tone.
0515     0% white + 50% black = shade. */
0516     m_hwbSpinBox->setToolTip(richTextMarker
0517                              + tr("<p>Hue: 0°⁠–⁠360°</p>"
0518                                   "<p>Whiteness: 0%⁠–⁠100%</p>"
0519                                   "<p>Blackness: 0%⁠–⁠100%</p>"));
0520 
0521     /*: @info:tooltip Help text for HSV/HSB. HSV (hue, saturation, value)
0522     and HSB (hue, saturation, brightness) are two different names for the
0523     very same color model. Saturation: 0 means something between black and
0524     white; 255 means something between black and the most colorful color.
0525     This is different from “chroma” and should therefore get a different
0526     translation. Brightness/value: 0 means always black; 255 means something
0527     between white and the most colorful color. This is different from
0528     “lightness” and should therefore get a different translation. */
0529     m_hsvSpinBox->setToolTip(richTextMarker
0530                              + tr("<p>Hue: 0°⁠–⁠360°</p>"
0531                                   "<p>HSV/HSB-Saturation: 0%⁠–⁠100%</p>"
0532                                   "<p>Brightness/Value: 0%⁠–⁠100%</p>"));
0533 
0534     m_alphaSpinBox->setPrefix(percentageInSpinbox.first);
0535     m_alphaSpinBox->setSuffix(percentageInSpinbox.second);
0536 
0537     /*: @label:slider Accessible name for lightness slider. This is different
0538     from “brightness”/“value” and should therefore get a different
0539     translation. */
0540     m_lchLightnessSelector->setAccessibleName(tr("Lightness"));
0541 
0542     /*: @info:tooltip Help text for CIEHLC. “lightness” is different from
0543     “brightness”/“value” and should therefore get a different translation. */
0544     m_ciehlcD50SpinBox->setToolTip(richTextMarker
0545                                    + tr("<p>Hue: 0°⁠–⁠360°</p>"
0546                                         "<p>Lightness: 0%⁠–⁠100%</p>"
0547                                         "<p>Chroma: 0⁠–⁠%L1</p>")
0548                                          .arg(CielchD50Values::maximumChroma));
0549 
0550     constexpr double maxOklchChroma = OklchValues::maximumChroma;
0551     /*: @info:tooltip Help text for Oklch. “lightness” is different from
0552     “brightness”/“value” and should therefore get a different translation. */
0553     m_oklchSpinBox->setToolTip(richTextMarker
0554                                + tr("<p>Lightness: %L1⁠–⁠%L2</p>"
0555                                     "<p>Chroma: %L3⁠–⁠%L4</p>"
0556                                     "<p>Hue: 0°⁠–⁠360°</p>"
0557                                     "<p>Whitepoint: D65</p>")
0558                                      .arg(0., 0, 'f', okdecimals)
0559                                      .arg(1., 0, 'f', okdecimals)
0560                                      .arg(0., 0, 'f', okdecimals)
0561                                      .arg(maxOklchChroma, 0, 'f', okdecimals));
0562 
0563     /*: @label:slider An opacity of 0 means completely
0564     transparent. The higher the opacity value increases, the
0565     more opaque the colour becomes,  until it finally becomes
0566     completely opaque at the highest possible opacity value. */
0567     const QString opacityLabel = tr("Op&acity:");
0568     m_alphaGradientSlider->setAccessibleName(opacityLabel);
0569     m_alphaLabel->setText(opacityLabel);
0570 
0571     // HSL spin box
0572     QList<MultiSpinBoxSection> hslSections = //
0573         m_hslSpinBox->sectionConfigurations();
0574     if (hslSections.count() != 3) {
0575         qWarning() //
0576             << "Expected 3 sections in HSV MultiSpinBox, but got" //
0577             << hslSections.count() //
0578             << "instead. This is a bug in libperceptualcolor.";
0579     } else {
0580         hslSections[0].setPrefix(arcDegreeInSpinbox.first);
0581         hslSections[0].setSuffix( //
0582             arcDegreeInSpinbox.second + m_multispinboxSectionSeparator);
0583         hslSections[1].setPrefix( //
0584             m_multispinboxSectionSeparator + percentageInSpinbox.first);
0585         hslSections[1].setSuffix( //
0586             percentageInSpinbox.second + m_multispinboxSectionSeparator);
0587         hslSections[2].setPrefix( //
0588             m_multispinboxSectionSeparator + percentageInSpinbox.first);
0589         hslSections[2].setSuffix(percentageInSpinbox.second);
0590         m_hslSpinBox->setSectionConfigurations(hslSections);
0591     }
0592 
0593     // HWB spin box
0594     QList<MultiSpinBoxSection> hwbSections = //
0595         m_hwbSpinBox->sectionConfigurations();
0596     if (hwbSections.count() != 3) {
0597         qWarning() //
0598             << "Expected 3 sections in HSV MultiSpinBox, but got" //
0599             << hwbSections.count() //
0600             << "instead. This is a bug in libperceptualcolor.";
0601     } else {
0602         hwbSections[0].setPrefix(arcDegreeInSpinbox.first);
0603         hwbSections[0].setSuffix( //
0604             arcDegreeInSpinbox.second + m_multispinboxSectionSeparator);
0605         hwbSections[1].setPrefix( //
0606             m_multispinboxSectionSeparator + percentageInSpinbox.first);
0607         hwbSections[1].setSuffix( //
0608             percentageInSpinbox.second + m_multispinboxSectionSeparator);
0609         hwbSections[2].setPrefix( //
0610             m_multispinboxSectionSeparator + percentageInSpinbox.first);
0611         hwbSections[2].setSuffix( //
0612             percentageInSpinbox.second);
0613         m_hwbSpinBox->setSectionConfigurations(hwbSections);
0614     }
0615 
0616     // HSV spin box
0617     QList<MultiSpinBoxSection> hsvSections = //
0618         m_hsvSpinBox->sectionConfigurations();
0619     if (hsvSections.count() != 3) {
0620         qWarning() //
0621             << "Expected 3 sections in HSV MultiSpinBox, but got" //
0622             << hsvSections.count() //
0623             << "instead. This is a bug in libperceptualcolor.";
0624     } else {
0625         hsvSections[0].setPrefix(arcDegreeInSpinbox.first);
0626         hsvSections[0].setSuffix( //
0627             arcDegreeInSpinbox.second + m_multispinboxSectionSeparator);
0628         hsvSections[1].setPrefix( //
0629             m_multispinboxSectionSeparator + percentageInSpinbox.first);
0630         hsvSections[1].setSuffix( //
0631             percentageInSpinbox.second + m_multispinboxSectionSeparator);
0632         hsvSections[2].setPrefix( //
0633             m_multispinboxSectionSeparator + percentageInSpinbox.first);
0634         hsvSections[2].setSuffix(percentageInSpinbox.second);
0635         m_hsvSpinBox->setSectionConfigurations(hsvSections);
0636     }
0637 
0638     // CIEHLC-D50 spin box
0639     QList<MultiSpinBoxSection> ciehlcD50Sections = //
0640         m_ciehlcD50SpinBox->sectionConfigurations();
0641     if (ciehlcD50Sections.count() != 3) {
0642         qWarning() //
0643             << "Expected 3 sections in HLC MultiSpinBox, but got" //
0644             << ciehlcD50Sections.count() //
0645             << "instead. This is a bug in libperceptualcolor.";
0646     } else {
0647         ciehlcD50Sections[0].setPrefix(arcDegreeInSpinbox.first);
0648         ciehlcD50Sections[0].setSuffix( //
0649             arcDegreeInSpinbox.second + m_multispinboxSectionSeparator);
0650         ciehlcD50Sections[1].setPrefix( //
0651             m_multispinboxSectionSeparator + percentageInSpinbox.first);
0652         ciehlcD50Sections[1].setSuffix( //
0653             percentageInSpinbox.second + m_multispinboxSectionSeparator);
0654         ciehlcD50Sections[2].setPrefix(m_multispinboxSectionSeparator);
0655         ciehlcD50Sections[2].setSuffix(QString());
0656         m_ciehlcD50SpinBox->setSectionConfigurations(ciehlcD50Sections);
0657     }
0658 
0659     // Oklch spin box
0660     QList<MultiSpinBoxSection> oklchSections = //
0661         m_oklchSpinBox->sectionConfigurations();
0662     if (oklchSections.count() != 3) {
0663         qWarning() //
0664             << "Expected 3 sections in HLC MultiSpinBox, but got" //
0665             << oklchSections.count() //
0666             << "instead. This is a bug in libperceptualcolor.";
0667     } else {
0668         oklchSections[0].setPrefix(QString());
0669         oklchSections[0].setSuffix(m_multispinboxSectionSeparator);
0670         oklchSections[1].setPrefix(m_multispinboxSectionSeparator);
0671         oklchSections[1].setSuffix(m_multispinboxSectionSeparator);
0672         oklchSections[2].setPrefix( //
0673             m_multispinboxSectionSeparator + arcDegreeInSpinbox.first);
0674         oklchSections[2].setSuffix(arcDegreeInSpinbox.second);
0675         m_oklchSpinBox->setSectionConfigurations(oklchSections);
0676     }
0677 
0678     if (m_screenColorPickerButton) {
0679         /*: @action:button (eye dropper/pipette).
0680         A click on the button transforms the mouse cursor to a cross and lets
0681         the user choose a color from the screen by doing a left-click.
0682         Same text as in QColorDialog */
0683         const auto mnemonic = tr("&Pick screen color");
0684         m_screenColorPickerButton->setToolTip( //
0685             richTextMarker + fromMnemonicToRichText(mnemonic));
0686         m_screenColorPickerButton->setShortcut( //
0687             QKeySequence::mnemonic(mnemonic));
0688     }
0689 
0690     /*: @info:tooltip Tooltip for the gamut-correction action.
0691     The icon for this action is only visible in the UI while the
0692     color value within the corresponding spinbox is an out-of-gamut
0693     value. A click on the icon will change the spinbox’s values to
0694     the nearest in-gamut color (and make the icon disappear). */
0695     const auto gamutMnemonic = //
0696         tr("Click to snap to nearest in-&gamut color");
0697     const QString gamutTooltip = //
0698         richTextMarker + fromMnemonicToRichText(gamutMnemonic);
0699     const auto gamutShortcut = QKeySequence::mnemonic(gamutMnemonic);
0700     m_ciehlcD50SpinBoxGamutAction->setToolTip(gamutTooltip);
0701     m_ciehlcD50SpinBoxGamutAction->setShortcut(gamutShortcut);
0702     m_oklchSpinBoxGamutAction->setToolTip(gamutTooltip);
0703     m_oklchSpinBoxGamutAction->setShortcut(gamutShortcut);
0704 
0705     // NOTE No need to call
0706     //
0707     // q_pointer->adjustSize();
0708     //
0709     // because our layout adopts automatically to the
0710     // new size of the strings. Indeed, calling
0711     //
0712     // q_pointer->adjustSize();
0713     //
0714     // would change the height (!) of the widget: While it might seem
0715     // reasonable that the width changes when the strings change, the
0716     // height should not. We didn’t find the reason and didn’t manage
0717     // to reproduce this behaviour within the unit tests. But anyway
0718     // the call is not necessary, as mentioned earlier.
0719 }
0720 
0721 /** @brief Reloads all icons, adapting to the current color schema and
0722  * widget style. */
0723 void ColorDialogPrivate::reloadIcons()
0724 {
0725     QScopedPointer<QLabel> label{new QLabel(q_pointer)};
0726     label->setText(QStringLiteral("abc"));
0727     label->resize(label->sizeHint()); // Smaller size means faster guess.
0728     ColorSchemeType newType = guessColorSchemeTypeFromWidget(label.data()) //
0729                                   .value_or(newType);
0730 
0731     m_currentIconThemeType = newType;
0732 
0733     static const QStringList swatchBookIcons //
0734         {QStringLiteral("paint-swatch"),
0735          // For “symbolic” (monochromatic) vs “full-color” icons, see
0736          // https://pointieststick.com/2023/08/12/how-all-this-icon-stuff-is-going-to-work-in-plasma-6/
0737          QStringLiteral("palette"),
0738          QStringLiteral("palette-symbolic")};
0739     const int swatchBookIndex = //
0740         m_tabWidget->indexOf(m_swatchBookWrapperWidget);
0741     if (swatchBookIndex >= 0) {
0742         m_tabWidget->setTabIcon(swatchBookIndex, //
0743                                 qIconFromTheme(swatchBookIcons, //
0744                                                QStringLiteral("color-swatch"),
0745                                                newType));
0746     }
0747 
0748     static const QStringList hueFirstIcons //
0749         {
0750             QStringLiteral("color-mode-hue-shift-positive"),
0751         };
0752     const int hueFirstIndex = //
0753         m_tabWidget->indexOf(m_hueFirstWrapperWidget);
0754     if (hueFirstIndex >= 0) {
0755         m_tabWidget->setTabIcon(hueFirstIndex, //
0756                                 qIconFromTheme(hueFirstIcons, //
0757                                                QStringLiteral("steering-wheel"),
0758                                                newType));
0759     }
0760 
0761     static const QStringList lightnessFirstIcons //
0762         {
0763             QStringLiteral("brightness-high"),
0764         };
0765     const int lightnessFirstIndex = //
0766         m_tabWidget->indexOf(m_lightnessFirstWrapperWidget);
0767     if (lightnessFirstIndex >= 0) {
0768         m_tabWidget->setTabIcon(lightnessFirstIndex, //
0769                                 qIconFromTheme(lightnessFirstIcons, //
0770                                                QStringLiteral("brightness-2"),
0771                                                newType));
0772     }
0773 
0774     static const QStringList numericIcons //
0775         {
0776             QStringLiteral("black_sum"),
0777         };
0778     const int numericIndex = //
0779         m_tabWidget->indexOf(m_numericalWidget);
0780     if (numericIndex >= 0) {
0781         m_tabWidget->setTabIcon(numericIndex, //
0782                                 qIconFromTheme(numericIcons, //
0783                                                QStringLiteral("123"),
0784                                                newType));
0785     }
0786 
0787     // Gamut button for some spin boxes
0788     static const QStringList gamutIconNames //
0789         {
0790             QStringLiteral("data-warning"),
0791             QStringLiteral("dialog-warning-symbolic"),
0792         };
0793     const QIcon gamutIcon = qIconFromTheme(gamutIconNames, //
0794                                            QStringLiteral("eye-exclamation"),
0795                                            newType);
0796     m_ciehlcD50SpinBoxGamutAction->setIcon(gamutIcon);
0797     m_oklchSpinBoxGamutAction->setIcon(gamutIcon);
0798 
0799     static const QStringList candidates //
0800         {
0801             QStringLiteral("color-picker"), //
0802             QStringLiteral("gtk-color-picker"), //
0803             QStringLiteral("tool_color_picker"), //
0804         };
0805     if (!m_screenColorPickerButton.isNull()) {
0806         m_screenColorPickerButton->setIcon( //
0807             qIconFromTheme(candidates, //
0808                            QStringLiteral("color-picker"),
0809                            newType));
0810     }
0811 }
0812 
0813 /** @brief Basic initialization.
0814  *
0815  * @param colorSpace The color space within which this widget should operate.
0816  * Can be created with @ref RgbColorSpaceFactory.
0817  *
0818  * Code that is shared between the various overloaded constructors.
0819  *
0820  * @todo The RTL layout is broken for @ref SwatchBook. Thought a stretch
0821  * is added in the layout, the @ref SwatchBook stays left-aligned
0822  * instead of right-aligned if there is too much space. Why doesn’t this
0823  * right-align? For @ref m_wheelColorPicker and @ref m_chromaHueDiagram
0824  * the same code works fine! */
0825 void ColorDialogPrivate::initialize(const QSharedPointer<PerceptualColor::RgbColorSpace> &colorSpace)
0826 {
0827     // Do not show the “?” button in the window title. This button is displayed
0828     // by default on widgets that inherit from QDialog. But we do not want the
0829     // button because we do not provide What’s-This-help anyway, so having
0830     // the button would be confusing.
0831     q_pointer->setWindowFlag(Qt::WindowContextHelpButtonHint, false);
0832 
0833     // initialize color space and its dependencies
0834     m_rgbColorSpace = colorSpace;
0835     m_wcsBasicColors = wcsBasicColors(colorSpace);
0836     m_wcsBasicDefaultColor = m_wcsBasicColors.value(4, 2);
0837 
0838     // create the graphical selectors
0839     m_swatchBookBasicColors = new SwatchBook(m_rgbColorSpace, //
0840                                              m_wcsBasicColors, //
0841                                              Qt::Orientation::Horizontal);
0842     QHBoxLayout *swatchBookInnerLayout = new QHBoxLayout();
0843     swatchBookInnerLayout->addWidget(m_swatchBookBasicColors);
0844     swatchBookInnerLayout->addStretch();
0845     QVBoxLayout *swatchBookOuterLayout = new QVBoxLayout();
0846     swatchBookOuterLayout->addLayout(swatchBookInnerLayout);
0847     swatchBookOuterLayout->addStretch();
0848     m_swatchBookWrapperWidget = new QWidget();
0849     m_swatchBookWrapperWidget->setLayout(swatchBookOuterLayout);
0850 
0851     m_wheelColorPicker = new WheelColorPicker(m_rgbColorSpace);
0852     m_hueFirstWrapperWidget = new QWidget;
0853     QHBoxLayout *tempHueFirstLayout = new QHBoxLayout;
0854     tempHueFirstLayout->addWidget(m_wheelColorPicker);
0855     m_hueFirstWrapperWidget->setLayout(tempHueFirstLayout);
0856 
0857     m_lchLightnessSelector = new GradientSlider(m_rgbColorSpace);
0858     LchaDouble black;
0859     black.l = 0;
0860     black.c = 0;
0861     black.h = 0;
0862     black.a = 1;
0863     LchaDouble white;
0864     white.l = 100;
0865     white.c = 0;
0866     white.h = 0;
0867     white.a = 1;
0868     m_lchLightnessSelector->setColors(black, white);
0869     m_chromaHueDiagram = new ChromaHueDiagram(m_rgbColorSpace);
0870     QHBoxLayout *tempLightnesFirstLayout = new QHBoxLayout();
0871     tempLightnesFirstLayout->addWidget(m_lchLightnessSelector);
0872     tempLightnesFirstLayout->addWidget(m_chromaHueDiagram);
0873     m_lightnessFirstWrapperWidget = new QWidget();
0874     m_lightnessFirstWrapperWidget->setLayout(tempLightnesFirstLayout);
0875 
0876     initializeScreenColorPicker();
0877 
0878     m_tabWidget = new QTabWidget;
0879     // It would be good to have bigger icons. Via QStyle::pixelMetrics()
0880     // we could get values for this. QStyle::PM_LargeIconSize seems to large,
0881     // be we could use std::max() with QStyle::PM_ToolBarIconSize,
0882     // QStyle::PM_SmallIconSize, QStyle::PM_TabBarIconSize,
0883     // QStyle::PM_ButtonIconSize. But the problem is a regression in Qt6
0884     // (compared to Qt5) that breaks rendering of bigger icons via
0885     // QTabWidget::iconSize(): https://bugreports.qt.io/browse/QTBUG-114849
0886     // Furthermore, it appears that the MacOS style does not adjust the height
0887     // of the tab bar to match the icon height. This causes larger icons to
0888     // simply overflow, which looks like a rendering issue. Therefore,
0889     // currently we stick with the default icons size for tab bars.
0890     m_tabWidget->addTab(m_swatchBookWrapperWidget, QString());
0891     m_swatchBookTabShortcut = new QShortcut(q_pointer);
0892     connect(m_swatchBookTabShortcut, //
0893             &QShortcut::activated,
0894             this,
0895             [this]() {
0896                 m_tabWidget->setCurrentIndex( //
0897                     m_tabWidget->indexOf(m_swatchBookWrapperWidget));
0898             });
0899     connect(m_swatchBookTabShortcut, //
0900             &QShortcut::activatedAmbiguously,
0901             this,
0902             [this]() {
0903                 m_tabWidget->setCurrentIndex( //
0904                     m_tabWidget->indexOf(m_swatchBookWrapperWidget));
0905             });
0906 
0907     m_tabWidget->addTab(m_hueFirstWrapperWidget, QString());
0908     m_hueFirstTabShortcut = new QShortcut(q_pointer);
0909     connect(m_hueFirstTabShortcut, //
0910             &QShortcut::activated,
0911             this,
0912             [this]() {
0913                 m_tabWidget->setCurrentIndex( //
0914                     m_tabWidget->indexOf(m_hueFirstWrapperWidget));
0915             });
0916     connect(m_hueFirstTabShortcut, //
0917             &QShortcut::activatedAmbiguously,
0918             this,
0919             [this]() {
0920                 m_tabWidget->setCurrentIndex( //
0921                     m_tabWidget->indexOf(m_hueFirstWrapperWidget));
0922             });
0923 
0924     m_tabWidget->addTab(m_lightnessFirstWrapperWidget, QString());
0925     m_lightnessFirstTabShortcut = new QShortcut(q_pointer);
0926     connect(m_lightnessFirstTabShortcut, //
0927             &QShortcut::activated,
0928             this,
0929             [this]() {
0930                 m_tabWidget->setCurrentIndex( //
0931                     m_tabWidget->indexOf(m_lightnessFirstWrapperWidget));
0932             });
0933     connect(m_lightnessFirstTabShortcut, //
0934             &QShortcut::activatedAmbiguously,
0935             this,
0936             [this]() {
0937                 m_tabWidget->setCurrentIndex( //
0938                     m_tabWidget->indexOf(m_lightnessFirstWrapperWidget));
0939             });
0940 
0941     m_tabTable.insert(&m_swatchBookWrapperWidget, //
0942                       QStringLiteral("swatch"));
0943     m_tabTable.insert(&m_hueFirstWrapperWidget, //
0944                       QStringLiteral("hue-based"));
0945     m_tabTable.insert(&m_lightnessFirstWrapperWidget, //
0946                       QStringLiteral("lightness-based"));
0947     m_tabTable.insert(&m_numericalWidget, //
0948                       QStringLiteral("numerical"));
0949     connect(m_tabWidget, //
0950             &QTabWidget::currentChanged, //
0951             this, //
0952             &ColorDialogPrivate::saveCurrentTab);
0953 
0954     // Create the ColorPatch
0955     m_colorPatch = new ColorPatch();
0956     m_colorPatch->setMinimumSize(m_colorPatch->minimumSizeHint() * 1.5);
0957 
0958     QHBoxLayout *headerLayout = new QHBoxLayout();
0959     headerLayout->addWidget(m_colorPatch, 1);
0960     m_screenColorPickerButton->setSizePolicy(QSizePolicy::Minimum, // horizontal
0961                                              QSizePolicy::Minimum); // vertical
0962     headerLayout->addWidget(m_screenColorPickerButton,
0963                             // Do not grow the cell in the direction
0964                             // of the QBoxLayout:
0965                             0,
0966                             // No alignment: Fill the entire cell.
0967                             Qt::Alignment());
0968 
0969     // Create widget for the numerical values
0970     m_numericalWidget = initializeNumericPage();
0971     m_numericalTabShortcut = new QShortcut(q_pointer);
0972     connect(m_numericalTabShortcut, //
0973             &QShortcut::activated,
0974             this,
0975             [this]() {
0976                 m_tabWidget->setCurrentIndex( //
0977                     m_tabWidget->indexOf(m_numericalWidget));
0978             });
0979     connect(m_numericalTabShortcut, //
0980             &QShortcut::activatedAmbiguously,
0981             this,
0982             [this]() {
0983                 m_tabWidget->setCurrentIndex( //
0984                     m_tabWidget->indexOf(m_numericalWidget));
0985             });
0986 
0987     // Create layout for graphical and numerical widgets
0988     m_selectorLayout = new QHBoxLayout();
0989     m_selectorLayout->addWidget(m_tabWidget);
0990     m_selectorLayout->addWidget(m_numericalWidget);
0991 
0992     // Create widgets for alpha value
0993     QHBoxLayout *m_alphaLayout = new QHBoxLayout();
0994     m_alphaGradientSlider = new GradientSlider(m_rgbColorSpace, //
0995                                                Qt::Orientation::Horizontal);
0996     m_alphaGradientSlider->setSingleStep(singleStepAlpha);
0997     m_alphaGradientSlider->setPageStep(pageStepAlpha);
0998     m_alphaSpinBox = new QDoubleSpinBox();
0999     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
1046             &PerceptualColor::ColorDialog::accept); // slot
1047     connect(m_buttonBox, // sender
1048             &QDialogButtonBox::rejected, // signal
1049             q_pointer, // receiver
1050             &PerceptualColor::ColorDialog::reject); // slot
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
1072             &MultiSpinBox::sectionValuesChanged, // signal
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
1087             &MultiSpinBox::sectionValuesChanged, // signal
1088             this, // receiver
1089             &ColorDialogPrivate::readHslNumericValues // slot
1090     );
1091     connect(m_hwbSpinBox, // sender
1092             &MultiSpinBox::sectionValuesChanged, // signal
1093             this, // receiver
1094             &ColorDialogPrivate::readHwbNumericValues // slot
1095     );
1096     connect(m_hsvSpinBox, // sender
1097             &MultiSpinBox::sectionValuesChanged, // signal
1098             this, // receiver
1099             &ColorDialogPrivate::readHsvNumericValues // slot
1100     );
1101     connect(m_ciehlcD50SpinBox, // sender
1102             &MultiSpinBox::sectionValuesChanged, // signal
1103             this, // receiver
1104             &ColorDialogPrivate::readHlcNumericValues // slot
1105     );
1106     connect(m_ciehlcD50SpinBox, // sender
1107             &MultiSpinBox::editingFinished, // signal
1108             this, // receiver
1109             &ColorDialogPrivate::updateHlcButBlockSignals // slot
1110     );
1111     connect(m_oklchSpinBox, // sender
1112             &MultiSpinBox::sectionValuesChanged, // signal
1113             this, // receiver
1114             &ColorDialogPrivate::readOklchNumericValues // slot
1115     );
1116     connect(m_oklchSpinBox, // sender
1117             &MultiSpinBox::editingFinished, // signal
1118             this, // receiver
1119             &ColorDialogPrivate::updateOklchButBlockSignals // slot
1120     );
1121     connect(m_lchLightnessSelector, // sender
1122             &GradientSlider::valueChanged, // signal
1123             this, // receiver
1124             &ColorDialogPrivate::readLightnessValue // slot
1125     );
1126     connect(m_wheelColorPicker, // sender
1127             &WheelColorPicker::currentColorChanged, // signal
1128             this, // receiver
1129             &ColorDialogPrivate::readWheelColorPickerValues // slot
1130     );
1131     connect(m_chromaHueDiagram, // sender
1132             &ChromaHueDiagram::currentColorChanged, // signal
1133             this, // receiver
1134             &ColorDialogPrivate::readChromaHueDiagramValue // slot
1135     );
1136     connect(m_alphaGradientSlider, // sender
1137             &GradientSlider::valueChanged, // signal
1138             this, // receiver
1139             &ColorDialogPrivate::updateColorPatch // slot
1140     );
1141     connect(m_alphaGradientSlider, // sender
1142             &GradientSlider::valueChanged, // signal
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
1221             &QStyleHints::colorSchemeChanged, // signal
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. */
1231 ColorDialog::ColorDialog(QWidget *parent)
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. */
1249 ColorDialog::ColorDialog(const QColor &initial, QWidget *parent)
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. */
1268 ColorDialog::ColorDialog(const QSharedPointer<PerceptualColor::RgbColorSpace> &colorSpace, QWidget *parent)
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. */
1288 ColorDialog::ColorDialog(const QSharedPointer<PerceptualColor::RgbColorSpace> &colorSpace, const QColor &initial, QWidget *parent)
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 */
1302 ColorDialog::~ColorDialog() noexcept
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. */
1315 ColorDialogPrivate::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)
1322 QColor ColorDialog::currentColor() const
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. */
1344 void ColorDialog::setCurrentColor(const QColor &color)
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 */
1376 void 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. */
1392 void 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. */
1401 void 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. */
1414 void 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. */
1446 void 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. */
1564 void 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. */
1582 void 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. */
1595 void 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. */
1608 void 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. */
1621 void 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. */
1634 void 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. */
1651 void 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. */
1668 void 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. */
1682 void 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. */
1696 void 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. */
1720 void 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. */
1763 void 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. */
1779 void 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. */
1789 void 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. */
1815 void 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>. */
1847 void 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,
1856             &QPushButton::clicked,
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. */
1885 QWidget *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}"));
1917         QRegularExpressionValidator *validator = new QRegularExpressionValidator( //
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);
2091     tempWidget->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
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)
2109 QColorDialog::ColorDialogOptions ColorDialog::options() const
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 */
2119 void ColorDialog::setOption(PerceptualColor::ColorDialog::ColorDialogOption option, bool on)
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. */
2130 void ColorDialog::setOptions(PerceptualColor::ColorDialog::ColorDialogOptions newOptions)
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  */
2166 bool ColorDialog::testOption(PerceptualColor::ColorDialog::ColorDialogOption option) const
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. */
2183 QColor ColorDialog::getColor(const QSharedPointer<PerceptualColor::RgbColorSpace> &colorSpace,
2184                              const QColor &initial,
2185                              QWidget *parent,
2186                              const QString &title,
2187                              QColorDialog::ColorDialogOptions options)
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. */
2212 QColor ColorDialog::getColor(const QColor &initial, QWidget *parent, const QString &title, QColorDialog::ColorDialogOptions options)
2213 {
2214     return getColor(RgbColorSpaceFactory::createSrgb(), //
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. */
2238 QColor ColorDialog::selectedColor() const
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 */
2251 void 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     }
2259     QDialog::setVisible(visible);
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 */
2272 void 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     }
2280     QDialog::done(result);
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)
2299 ColorDialog::DialogLayoutDimensions ColorDialog::layoutDimensions() const
2300 {
2301     return d_pointer->m_layoutDimensions;
2302 }
2303 
2304 /** @brief Setter for property @ref layoutDimensions
2305  * @param newLayoutDimensions the new layout dimensions */
2306 void ColorDialog::setLayoutDimensions(const ColorDialog::DialogLayoutDimensions newLayoutDimensions)
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. */
2321 void 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 = //
2327         ColorDialog::DialogLayoutDimensions::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. */
2411 void ColorDialog::changeEvent(QEvent *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 
2445     QDialog::changeEvent(event);
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. */
2458 void ColorDialog::showEvent(QShowEvent *event)
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     }
2478     QDialog::showEvent(event);
2479 }
2480 
2481 /** @brief Saves the current tab of @ref m_tabWidget to @ref m_settings. */
2482 void 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