File indexing completed on 2024-05-12 04:44:36
0001 // SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com> 0002 // SPDX-License-Identifier: BSD-2-Clause OR MIT 0003 0004 #include "chromahuediagram.h" 0005 #include "chromalightnessdiagram.h" 0006 #include "colordialog.h" 0007 #include "colorpatch.h" 0008 #include "colorwheel.h" 0009 #include "constpropagatinguniquepointer.h" 0010 #include "gradientslider.h" 0011 #include "helper.h" 0012 #include "lchdouble.h" 0013 #include "multispinbox.h" 0014 #include "multispinboxsection.h" 0015 #include "rgbcolorspace.h" 0016 #include "rgbcolorspacefactory.h" 0017 #include "settranslation.h" 0018 #include "swatchbook.h" 0019 #include "version.h" 0020 #include "wheelcolorpicker.h" 0021 #include <cstdlib> 0022 #include <qaction.h> 0023 #include <qapplication.h> 0024 #include <qcolor.h> 0025 #include <qcommandlineoption.h> 0026 #include <qcommandlineparser.h> 0027 #include <qcoreapplication.h> 0028 #include <qdebug.h> 0029 #include <qfont.h> 0030 #include <qfontdatabase.h> 0031 #include <qfontinfo.h> 0032 #include <qglobal.h> 0033 #include <qicon.h> 0034 #include <qlineedit.h> 0035 #include <qlist.h> 0036 #include <qlocale.h> 0037 #include <qnamespace.h> 0038 #include <qobjectdefs.h> 0039 #include <qpalette.h> 0040 #include <qpixmap.h> 0041 #include <qscopedpointer.h> 0042 #include <qsharedpointer.h> 0043 #include <qstring.h> 0044 #include <qstringbuilder.h> 0045 #include <qstringliteral.h> 0046 #include <qstyle.h> 0047 #include <qstylefactory.h> 0048 #include <qtabwidget.h> 0049 #include <qversionnumber.h> 0050 #include <qwidget.h> 0051 #include <type_traits> 0052 #include <utility> 0053 0054 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) 0055 #include <qcontainerfwd.h> 0056 #else 0057 #include <qstringlist.h> 0058 #endif 0059 0060 using namespace PerceptualColor; 0061 0062 // Force a font for a widget and all direct or indirect children widgets. 0063 // 0064 // The given font is set on the widget and all its direct or indirect 0065 // children which are subclasses of <tt>QWidget</tt>. If the widget is 0066 // a <tt>nullptr</tt>, nothing happens. 0067 // 0068 // Use case: QApplication::setFont() occasionally does not work on all 0069 // child widgets, so a special enforcement is needed. 0070 static void forceFont(QWidget *widget, const QFont &font = qApp->font()) 0071 { 0072 if (widget == nullptr) { 0073 return; 0074 } 0075 widget->setFont(font); 0076 for (auto child : std::as_const(widget->children())) { 0077 forceFont(qobject_cast<QWidget *>(child), font); 0078 } 0079 } 0080 0081 static void screenshotInternal(QWidget *widget, const QString &comment = QString()) 0082 { 0083 // Get fully qualified class name 0084 QString className = QString::fromUtf8(widget->metaObject()->className()); 0085 // Strip all the qualifiers 0086 className = className.split(QStringLiteral("::")).last(); 0087 widget->grab().save( 0088 // File name: 0089 className + comment + QStringLiteral(".png"), 0090 // File format: nullptr means: The file format will be chosen 0091 // from file name’s suffix. 0092 nullptr, 0093 // Compression: 0094 // 0 means: The compression is slow and results in a small file size. 0095 // 100 means: The compression is fast and results in a big file size. 0096 0); 0097 } 0098 0099 // Screenshots of widgets with asynchronous image processing. 0100 // 0101 // This function is not deterministic! If the delays are enough to 0102 // get the full-quality screenshot depends on the speed of your 0103 // hardware and on how much other applications are running on 0104 // your system! 0105 static void screenshotDelayed(QWidget *widget, const QString &comment = QString()) 0106 { 0107 QWidget parent; 0108 const auto oldParent = widget->parentWidget(); 0109 widget->setParent(&parent); 0110 parent.show(); 0111 // forceFont influences the metrics. Therefore, calling it before 0112 // QWidget::resize() and QWidget::show(). 0113 forceFont(widget); 0114 // Set an acceptable widget size (important 0115 // standalone-widgets without layout management): 0116 widget->resize(widget->sizeHint()); 0117 widget->show(); // Necessary to receive and process events like paintEvent() 0118 delayedEventProcessing(); 0119 screenshotInternal(widget, comment); 0120 widget->hide(); 0121 widget->setParent(oldParent); 0122 } 0123 0124 // A message handler that does not print any messages. 0125 static void voidMessageHandler(QtMsgType, const QMessageLogContext &, const QString &) 0126 { 0127 // dummy message handler that does not print messages 0128 } 0129 0130 // This function tries to set as many settings as possible to hard-coded 0131 // values: The widget style, the translation, the icon set and many more. 0132 // This makes it more likely to get the same screenshots on different 0133 // computers with different settings. 0134 static void initWidgetAppearance(QApplication *app) 0135 { 0136 // We prefer the Fusion style because he is the most cross-platform 0137 // style, so generating the screenshots does not depend on the 0138 // current system. Furthermore, it has support for fraction 0139 // scale factors such as 1.25 or 1.5. 0140 // 0141 // Possible styles (not all available in all setups): 0142 // "Breeze", "dsemilight", "dsemidark", "dlight", "ddark", "kvantum-dark", 0143 // "kvantum", "cleanlooks", "gtk2", "cde", "motif", "plastique", "Oxygen", 0144 // "QtCurve", "Windows", "Fusion" 0145 const QStringList styleNames{QStringLiteral("Fusion"), // 0146 QStringLiteral("Breeze"), // 0147 QStringLiteral("Oxygen")}; 0148 QStyle *style = nullptr; 0149 for (const QString &styleName : styleNames) { 0150 style = QStyleFactory::create(styleName); 0151 if (style != nullptr) { 0152 break; 0153 } 0154 } 0155 QApplication::setStyle(style); // This call is safe even if style==nullptr. 0156 0157 // Fusion uses by default the system’s palette, but 0158 // we want something system-independent to make the screenshots 0159 // look always the same. Therefore, we explicitly set Fusion’s 0160 // standard palette. 0161 { 0162 QScopedPointer<QStyle> tempStyle( // 0163 QStyleFactory::create(QStringLiteral("Fusion"))); 0164 QPalette tempPalette = tempStyle->standardPalette(); 0165 // The following colors are missing in Fusion’s standard palette: 0166 // They appear in Qt’s documentation of QPalette::ColorRole, 0167 // but do not appear when passing Fusion’s standard palette to 0168 // qDebug. Therefore, we set them explicitly to the default values 0169 // that are mentioned in the documentation of QPalette::ColorRole. 0170 tempPalette.setColor(QPalette::Link, Qt::blue); 0171 tempPalette.setColor(QPalette::Link, Qt::magenta); 0172 QApplication::setPalette(tempPalette); 0173 } 0174 0175 // By default, the icons of the system are made available by 0176 // QPlatformTheme. However, we want to make screenshots that 0177 // are independent of the currently selected icon theme on 0178 // the computer that produces the screenshots. Therefore, we 0179 // choose an invalid search path for icon themes to avoid 0180 // that missing icons are found in other themes that are 0181 // available on the current computer: 0182 QIcon::setThemeSearchPaths(QStringList(QStringLiteral("invalid"))); 0183 // Now, we change the standard icon theme to an invalid value. 0184 // As the search path has also been set to an invalid, missing 0185 // icons cannot be replaced by fallback icons. 0186 QIcon::setThemeName(QStringLiteral("invalid")); 0187 qInstallMessageHandler(voidMessageHandler); // Suppress warnings 0188 { 0189 // Trigger a call to the new, invalid icon theme. This call 0190 // will produce a message on the console: 0191 // “Icon theme "invalid" not found.” 0192 // Here, we trigger it intentionally while having the message 0193 // suppressed. The message appears only at the first call 0194 // to the invalid icon theme, but not on subsequent calls. 0195 // Therefore, we will not get more messages for this in the 0196 // rest of this program. 0197 QWidget widget; 0198 widget.repaint(); 0199 QCoreApplication::processEvents(); 0200 } 0201 qInstallMessageHandler(nullptr); // Do not suppress warnings anymore 0202 0203 // Other initializations 0204 app->setApplicationName(QStringLiteral("Perceptual color picker")); 0205 app->setLayoutDirection(Qt::LeftToRight); 0206 QLocale::setDefault(QLocale::English); 0207 PerceptualColor::setTranslation(app, // 0208 QLocale(QLocale::English).uiLanguages()); 0209 } 0210 0211 // We try to be as explicit as possible about the fonts. 0212 // std::exit() is called if one of the fontfiles couldn’t be loaded. 0213 static void initFonts(QApplication *app, const QStringList &fontfiles) 0214 { 0215 // NOTE It would even be possible to bundle a font as Qt resource 0216 // to become completely independent from the fonts that are 0217 // installed on the system: https://stackoverflow.com/a/30973961 0218 0219 QStringList fontFamilies; 0220 for (const QString &fontfile : std::as_const(fontfiles)) { 0221 const int id = QFontDatabase::addApplicationFont(fontfile); 0222 if (id == -1) { 0223 qWarning() << "Font file could not be loaded:" << fontfile; 0224 std::exit(-1); 0225 } 0226 fontFamilies.append(QFontDatabase::applicationFontFamilies(id)); 0227 } 0228 fontFamilies.append(QStringLiteral("Noto Sans")); // Fallback 0229 fontFamilies.append(QStringLiteral("Noto Sans Symbols2")); // Fallback 0230 // NOTE The font size is defined in “point”, whatever “point” is. 0231 // Actually, the size of a “point” depends on the scale factor, 0232 // which is set elsewhere yet. So, when the scale factor is 0233 // correct, than using a fixed “point” size should give us 0234 // identical results also on different systems. 0235 QFont myFont = QFont(fontFamilies.first(), // 0236 10, // 0237 QFont::Weight::Normal, // 0238 QFont::Style::StyleNormal); 0239 // Anti-alias might be different on different systems. Disabling it 0240 // entirely would look too ugly, but we disable subpixel antialias to make 0241 // the results between different systems at least smaller. 0242 constexpr auto styleStrategy = static_cast<QFont::StyleStrategy>( // 0243 QFont::PreferAntialias | QFont::NoSubpixelAntialias); 0244 myFont.setStyleStrategy(styleStrategy); 0245 myFont.setStyleHint(QFont::SansSerif, styleStrategy); 0246 myFont.setFamilies(fontFamilies); 0247 // It seems QFont::exactMatch() and QFontInfo::exactMatch() do not 0248 // work reliable on the X Window System, because this systems does 0249 // not provide the required functionality. Workaround: Compare 0250 // the actually used family (available via QFontInfo) with the 0251 // originally requested family (available via QFont): 0252 if (QFontInfo(myFont).family() != myFont.family()) { 0253 qWarning() << "Could not load font correctly:" << myFont.family(); 0254 } 0255 app->setFont(myFont); 0256 } 0257 0258 static void setCurrentTab(ColorDialog *dialog, int index) 0259 { 0260 QList<QTabWidget *> tabWidgets = dialog->findChildren<QTabWidget *>(); 0261 if (tabWidgets.count() != 1) { 0262 throw 0; 0263 } 0264 QTabWidget *myTabWidget = tabWidgets.first(); 0265 const auto count = myTabWidget->count(); 0266 if (count > 1) { 0267 // It seems that QTabWidget::setCurrentIndex() does not always repaint 0268 // correctly when no event loop is running. Workaround: 0269 // First, set a different index, and later the the actual index. 0270 if (index == 0) { 0271 myTabWidget->setCurrentIndex(1); 0272 } else { 0273 myTabWidget->setCurrentIndex(0); 0274 } 0275 } 0276 myTabWidget->setCurrentIndex(index); 0277 } 0278 0279 static void makeScreenshots() 0280 { 0281 // Variables 0282 QSharedPointer<RgbColorSpace> m_colorSpace = // 0283 RgbColorSpaceFactory::createSrgb(); 0284 // Chose a default color: 0285 // — that is present in the basic colors (to show the selection mark) 0286 // — is quite chromatic (which looks nice on screenshots) 0287 // — has nevertheless a little bit of distance to the outer 0288 // hull (which puts the marker somewhere in the inner of 0289 // the gamut, which makes the screenshots easier to understand). 0290 const QColor defaultColorRgb = QColor::fromRgb(50, 127, 206); 0291 const LchDouble defaultColorCielchD50 = // 0292 m_colorSpace->toCielchD50Double(defaultColorRgb.rgba64()); 0293 QColor myColor; 0294 0295 { 0296 ChromaHueDiagram m_chromaHueDiagram(m_colorSpace); 0297 m_chromaHueDiagram.setCurrentColor(defaultColorCielchD50); 0298 screenshotDelayed(&m_chromaHueDiagram); 0299 } 0300 0301 { 0302 ChromaLightnessDiagram m_chromaLightnessDiagram(m_colorSpace); 0303 m_chromaLightnessDiagram.setCurrentColor(defaultColorCielchD50); 0304 screenshotDelayed(&m_chromaLightnessDiagram); 0305 } 0306 0307 { 0308 ColorDialog m_colorDialog(m_colorSpace); 0309 m_colorDialog.setLayoutDimensions( // 0310 ColorDialog::DialogLayoutDimensions::Expanded); 0311 m_colorDialog.setCurrentColor(defaultColorRgb); 0312 setCurrentTab(&m_colorDialog, 0); 0313 screenshotDelayed(&m_colorDialog); 0314 } 0315 0316 { 0317 ColorDialog m_colorDialog(m_colorSpace); 0318 m_colorDialog.setLayoutDimensions( // 0319 ColorDialog::DialogLayoutDimensions::Expanded); 0320 m_colorDialog.setCurrentColor(defaultColorRgb); 0321 setCurrentTab(&m_colorDialog, 1); 0322 screenshotDelayed(&m_colorDialog, QStringLiteral("Tab1")); 0323 } 0324 0325 { 0326 ColorDialog m_colorDialog(m_colorSpace); 0327 m_colorDialog.setLayoutDimensions( // 0328 ColorDialog::DialogLayoutDimensions::Expanded); 0329 m_colorDialog.setCurrentColor(defaultColorRgb); 0330 setCurrentTab(&m_colorDialog, 2); 0331 screenshotDelayed(&m_colorDialog, QStringLiteral("Tab2")); 0332 } 0333 0334 { 0335 ColorDialog m_colorDialog(m_colorSpace); 0336 m_colorDialog.setLayoutDimensions( // 0337 ColorDialog::DialogLayoutDimensions::Expanded); 0338 m_colorDialog.setCurrentColor(defaultColorRgb); 0339 setCurrentTab(&m_colorDialog, 1); 0340 m_colorDialog.setOption( // 0341 ColorDialog::ColorDialogOption::ShowAlphaChannel); 0342 myColor = m_colorDialog.currentColor(); 0343 myColor.setAlphaF(0.5); 0344 m_colorDialog.setCurrentColor(myColor); 0345 screenshotDelayed(&m_colorDialog, QStringLiteral("Alpha")); 0346 } 0347 0348 { 0349 ColorDialog m_colorDialog(m_colorSpace); 0350 m_colorDialog.setLayoutDimensions( // 0351 ColorDialog::DialogLayoutDimensions::Expanded); 0352 m_colorDialog.setCurrentColor(defaultColorRgb); 0353 setCurrentTab(&m_colorDialog, 1); 0354 m_colorDialog.setOption( // 0355 ColorDialog::ColorDialogOption::ShowAlphaChannel); 0356 myColor = m_colorDialog.currentColor(); 0357 myColor.setAlphaF(0.5); 0358 m_colorDialog.setCurrentColor(myColor); 0359 screenshotDelayed(&m_colorDialog, QStringLiteral("Expanded")); 0360 } 0361 0362 { 0363 ColorDialog m_colorDialog(m_colorSpace); 0364 m_colorDialog.setLayoutDimensions( // 0365 ColorDialog::DialogLayoutDimensions::Collapsed); 0366 m_colorDialog.setCurrentColor(defaultColorRgb); 0367 setCurrentTab(&m_colorDialog, 1); 0368 m_colorDialog.setOption( // 0369 ColorDialog::ColorDialogOption::ShowAlphaChannel); 0370 myColor = m_colorDialog.currentColor(); 0371 myColor.setAlphaF(0.5); 0372 m_colorDialog.setCurrentColor(myColor); 0373 screenshotDelayed(&m_colorDialog, QStringLiteral("Collapsed")); 0374 } 0375 0376 { 0377 ColorPatch m_colorPatch; 0378 myColor = defaultColorRgb; 0379 m_colorPatch.setColor(myColor); 0380 screenshotDelayed(&m_colorPatch); 0381 myColor.setAlphaF(0.5); 0382 m_colorPatch.setColor(myColor); 0383 screenshotDelayed(&m_colorPatch, QStringLiteral("SemiTransparent")); 0384 m_colorPatch.setColor(QColor()); 0385 screenshotDelayed(&m_colorPatch, QStringLiteral("Invalid")); 0386 } 0387 0388 { 0389 ColorWheel m_colorWheel(m_colorSpace); 0390 m_colorWheel.setHue(defaultColorCielchD50.h); 0391 screenshotDelayed(&m_colorWheel); 0392 } 0393 0394 { 0395 GradientSlider m_gradientSlider(m_colorSpace); 0396 m_gradientSlider.setOrientation(Qt::Horizontal); 0397 screenshotDelayed(&m_gradientSlider); 0398 } 0399 0400 { 0401 MultiSpinBox m_multiSpinBox; 0402 PerceptualColor::MultiSpinBoxSection mySection; 0403 QList<PerceptualColor::MultiSpinBoxSection> hsvSectionConfigurations; 0404 QList<double> values; 0405 mySection.setDecimals(1); 0406 mySection.setPrefix(QString()); 0407 mySection.setMinimum(0); 0408 mySection.setWrapping(true); 0409 mySection.setMaximum(360); 0410 mySection.setSuffix(QStringLiteral(u"° ")); 0411 hsvSectionConfigurations.append(mySection); 0412 values.append(310); 0413 mySection.setPrefix(QStringLiteral(u" ")); 0414 mySection.setMinimum(0); 0415 mySection.setMaximum(255); 0416 mySection.setWrapping(false); 0417 mySection.setSuffix(QStringLiteral(u" ")); 0418 hsvSectionConfigurations.append(mySection); 0419 values.append(200); 0420 mySection.setSuffix(QString()); 0421 hsvSectionConfigurations.append(mySection); 0422 values.append(100); 0423 m_multiSpinBox.setSectionConfigurations(hsvSectionConfigurations); 0424 m_multiSpinBox.setSectionValues(values); 0425 screenshotDelayed(&m_multiSpinBox); 0426 0427 // Out-of-gamut button for the HLC spin box 0428 QAction *myAction = new QAction( 0429 // Icon: 0430 qIconFromTheme( // 0431 QStringList(), 0432 QStringLiteral("eye-exclamation"), 0433 ColorSchemeType::Light), 0434 // Text: 0435 QString(), 0436 // Parent object: 0437 &m_multiSpinBox); 0438 MultiSpinBox m_multiSpinBoxWithButton; 0439 m_multiSpinBoxWithButton.setSectionConfigurations( // 0440 hsvSectionConfigurations); 0441 m_multiSpinBoxWithButton.setSectionValues(values); 0442 m_multiSpinBoxWithButton.addActionButton( // 0443 myAction, // 0444 QLineEdit::ActionPosition::TrailingPosition); 0445 screenshotDelayed(&m_multiSpinBoxWithButton, QStringLiteral("WithButton")); 0446 } 0447 0448 { 0449 WheelColorPicker m_wheelColorPicker(m_colorSpace); 0450 m_wheelColorPicker.setCurrentColor(defaultColorCielchD50); 0451 screenshotDelayed(&m_wheelColorPicker); 0452 } 0453 0454 { 0455 SwatchBook m_swatchBook(m_colorSpace, // 0456 wcsBasicColors(m_colorSpace), 0457 Qt::Orientation::Horizontal); 0458 m_swatchBook.setCurrentColor(defaultColorRgb); 0459 screenshotDelayed(&m_swatchBook); 0460 } 0461 } 0462 0463 // Creates a set of screenshots of the library and saves these 0464 // screenshots as .png files in the working directory. 0465 int main(int argc, char *argv[]) 0466 { 0467 // Adjust the scale factor before constructing our real QApplication 0468 // object: 0469 { 0470 // See https://doc.qt.io/qt-6/highdpi.html for documentation 0471 // about QT_SCALE_FACTOR. In short: For testing purpose, it 0472 // can be used to adjust the current system-default scale 0473 // factor. This affects both, widget painting and font 0474 // rendering (font DPI). 0475 // 0476 // We choose a small factor, because the actual default size 0477 // of dialog and top-level widgets in Qt is: smaller or 0478 // than ⅔ of the screen. This affects our color dialog, which 0479 // allows small sizes, but recommends bigger ones. As the 0480 // screen size of the computer running this program is not 0481 // known in advance, we try to minimize the effects be choosing 0482 // the smallest possible scale factor, which is 1. (Values 0483 // smaller than 1 are working: They break the layout.) 0484 constexpr qreal screenshotScaleFactor = 1; 0485 // Create a temporary QApplication object within this block scope. 0486 // Necessary to get the system’s scale factor. 0487 QApplication app(argc, argv); 0488 const qreal systemScaleFactor = QWidget().devicePixelRatioF(); 0489 bool conversionOkay; 0490 double qtScaleFactor = // 0491 qEnvironmentVariable("QT_SCALE_FACTOR").toDouble(&conversionOkay); 0492 if (!conversionOkay) { 0493 qtScaleFactor = 1; 0494 } 0495 qtScaleFactor = // 0496 qtScaleFactor / systemScaleFactor * screenshotScaleFactor; 0497 // Set QT_SCALE_FACTOR to a corrected factor. This will only 0498 // take effect when the current QApplication object has been 0499 // destroyed and a new one has been created. 0500 qputenv("QT_SCALE_FACTOR", QString::number(qtScaleFactor).toUtf8()); 0501 } 0502 0503 // Prepare configuration before instantiating the application object 0504 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 0505 QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); 0506 #endif 0507 0508 // Instantiate the application object 0509 QApplication app(argc, argv); 0510 app.setApplicationName(QStringLiteral("generatescreenshots")); 0511 app.setApplicationVersion(perceptualColorRunTimeVersion().toString()); 0512 QCommandLineParser parser; 0513 const QString description = QStringLiteral( // 0514 "Generate screenshots of PerceptualColor widgets for documentation.\n" 0515 "\n" 0516 "The generated screenshots are similar also when this application\n" 0517 "is used on different operation systems. The used QStyle() and\n" 0518 "color schema and scaling factor are hard-coded. However, fonts\n" 0519 "render slightly different on different systems. You can explicitly\n" 0520 "specify the font files to use; this might reduce the differences,\n" 0521 "but will not eliminate them entirely."); 0522 parser.setApplicationDescription(description); 0523 parser.addHelpOption(); 0524 parser.addVersionOption(); 0525 const QCommandLineOption native // 0526 {QStringLiteral("native"), 0527 QStringLiteral("Use the current environment’s default style instead " 0528 "of a hard-coded style. Also, “fontfiles” will be " 0529 "ignored.")}; 0530 parser.addOption(native); 0531 parser.addPositionalArgument( // 0532 QStringLiteral("fontfiles"), // 0533 QStringLiteral("Zero or more font files (preferred fonts first).")); 0534 parser.process(app); 0535 if (!parser.isSet(native)) { 0536 initWidgetAppearance(&app); 0537 initFonts(&app, parser.positionalArguments()); 0538 } 0539 0540 // Do the actual work 0541 makeScreenshots(); 0542 0543 // Return 0544 return EXIT_SUCCESS; 0545 }