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