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 }