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 // Own headers
0005 // First the interface, which forces the header to be self-contained.
0006 #include "swatchbook.h"
0007 // Second, the private implementation.
0008 #include "swatchbook_p.h" // IWYU pragma: associated
0009 
0010 #include "abstractdiagram.h"
0011 #include "constpropagatingrawpointer.h"
0012 #include "constpropagatinguniquepointer.h"
0013 #include "helper.h"
0014 #include "helpermath.h"
0015 #include "initializetranslation.h"
0016 #include "lchdouble.h"
0017 #include "rgbcolorspace.h"
0018 #include <algorithm>
0019 #include <optional>
0020 #include <qapplication.h>
0021 #include <qcoreapplication.h>
0022 #include <qcoreevent.h>
0023 #include <qevent.h>
0024 #include <qfontmetrics.h>
0025 #include <qline.h>
0026 #include <qlist.h>
0027 #include <qmargins.h>
0028 #include <qnamespace.h>
0029 #include <qpainter.h>
0030 #include <qpainterpath.h>
0031 #include <qpen.h>
0032 #include <qpoint.h>
0033 #include <qrect.h>
0034 #include <qsharedpointer.h>
0035 #include <qsizepolicy.h>
0036 #include <qstringliteral.h>
0037 #include <qstyle.h>
0038 #include <qstyleoption.h>
0039 #include <qtransform.h>
0040 #include <qwidget.h>
0041 
0042 #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
0043 #include <qcontainerfwd.h>
0044 #else
0045 #include <qstringlist.h>
0046 #include <qvector.h>
0047 #endif
0048 
0049 namespace PerceptualColor
0050 {
0051 
0052 /** @brief Retranslate the UI with all user-visible strings.
0053  *
0054  * This function updates all user-visible strings by using
0055  * <tt>Qt::tr()</tt> to get up-to-date translations.
0056  *
0057  * This function is meant to be called at the end of the constructor and
0058  * additionally after each <tt>QEvent::LanguageChange</tt> event.
0059  *
0060  * @note This is the same concept as
0061  * <a href="https://doc.qt.io/qt-5/designer-using-a-ui-file.html">
0062  * Qt Designer, which also provides a function of the same name in
0063  * uic-generated code</a>. */
0064 void SwatchBookPrivate::retranslateUi()
0065 {
0066     // Which symbol is appropriate as selection mark? This might depend on
0067     // culture and language. For more information, see also
0068     // https://en.wikipedia.org/w/index.php?title=Check_mark&oldid=1030853305#International_differences
0069     // Therefore, we provide translation support for the selection mark.
0070 
0071     // NOTE Some candidates for “translations” of this character might be
0072     // emoji characters that might render colorful on some systems and
0073     // some fonts. It would be great to disable color fonts and only
0074     // accept black fonts. However, this seems to be impossible with Qt.
0075     // There is a command-line option named “nocolorfonts”, documented at
0076     // https://doc.qt.io/qt-6/qguiapplication.html#QGuiApplication
0077     // However, this is only available for DirectWrite font rendering
0078     // on Windows. There does not seem to be a cross-platform solution
0079     // currently.
0080     /*: @item Indicate the selected color in the swatch book. This symbol
0081     should be translated to whatever symbol is most appropriate for “selected”
0082     in the translation language. Example symbols: ✓ U+2713 CHECK MARK.
0083     ✗ U+2717 BALLOT X. ✘ U+2718 HEAVY BALLOT X. ○ U+25CB WHITE CIRCLE.
0084     ◯ U+25EF LARGE CIRCLE. Do not use emoji characters as they may render
0085     colorful on some systems, so they will ignore the automatically chosen
0086     color which is used get best contrast with the background. (Also
0087     U+FE0E VARIATION SELECTOR-15 does not prevent colorful rendering.) */
0088     const QString translation = tr("✓");
0089 
0090     // Test if all characters of the translated string are actually
0091     // available in the given font.
0092     auto ucs4 = translation.toUcs4();
0093     bool okay = true;
0094     QFontMetricsF myFontMetrics(q_pointer->font());
0095     for (int i = 0; okay && (i < ucs4.count()); ++i) {
0096         okay = myFontMetrics.inFontUcs4(ucs4.at(i));
0097     }
0098 
0099     // Return
0100     if (okay) {
0101         m_selectionMark = translation;
0102     } else {
0103         m_selectionMark = QString();
0104     }
0105 
0106     // Schedule a paint event to make the changes visible.
0107     q_pointer->update();
0108 }
0109 
0110 /** @brief Constructor
0111  *
0112  * @param colorSpace The color space of the swatches.
0113  * @param swatches The colors in the given color space. The first dimension
0114  *        (@ref Array2D::iCount()) is interpreted as horizontal axis from
0115  *        left to right on LTR layouts, and the other way around on RTL
0116  *        layouts. The second dimension of the array (@ref Array2D::jCount())
0117  *        is interpreted as vertical axis, from top to bottom.
0118  * @param wideSpacing Set of axis where the spacing should be wider than
0119  *        normal. This is useful to give some visual structure: When your
0120  *        swatches are organized logically in columns, set
0121  *        <tt>Qt::Orientation::Horizontal</tt> here. To use normal spacing
0122  *        everywhere, simply set this parameter to <tt>{}</tt>.
0123  * @param parent The parent of the widget, if any */
0124 SwatchBook::SwatchBook(const QSharedPointer<PerceptualColor::RgbColorSpace> &colorSpace,
0125                        const Array2D<QColor> &swatches,
0126                        Qt::Orientations wideSpacing,
0127                        QWidget *parent)
0128     : AbstractDiagram(parent)
0129     , d_pointer(new SwatchBookPrivate(this, swatches, wideSpacing))
0130 {
0131     d_pointer->m_rgbColorSpace = colorSpace;
0132 
0133     setFocusPolicy(Qt::FocusPolicy::StrongFocus);
0134 
0135     setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
0136 
0137     // Trigger paint events whenever the mouse enters or leaves the widget.
0138     // (Important on some QStyle who might paint widgets different then.)
0139     setAttribute(Qt::WA_Hover);
0140 
0141     // Initialize the selection (and implicitly the currentColor property):
0142     d_pointer->selectSwatch(8, 0); // Same default as in QColorDialog
0143 
0144     initializeTranslation(QCoreApplication::instance(),
0145                           // An empty std::optional means: If in initialization
0146                           // had been done yet, repeat this initialization.
0147                           // If not, do a new initialization now with default
0148                           // values.
0149                           std::optional<QStringList>());
0150     d_pointer->retranslateUi();
0151 }
0152 
0153 /** @brief Destructor */
0154 SwatchBook::~SwatchBook() noexcept
0155 {
0156 }
0157 
0158 /** @brief Constructor
0159  *
0160  * @param backLink Pointer to the object from which <em>this</em> object
0161  * is the private implementation.
0162  * @param swatches The swatches.
0163  * @param wideSpacing Set of axis using @ref widePatchSpacing instead
0164  *        of @ref normalPatchSpacing. */
0165 SwatchBookPrivate::SwatchBookPrivate(SwatchBook *backLink, const Array2D<QColor> &swatches, Qt::Orientations wideSpacing)
0166     : m_swatches(swatches)
0167     , m_wideSpacing(wideSpacing)
0168     , q_pointer(backLink)
0169 {
0170 }
0171 
0172 /** @brief Recommended size for the widget.
0173  *
0174  * Reimplemented from base class.
0175  *
0176  * @returns Recommended size for the widget.
0177  *
0178  * @sa @ref minimumSizeHint() */
0179 QSize SwatchBook::sizeHint() const
0180 {
0181     return minimumSizeHint();
0182 }
0183 
0184 /** @brief Recommended minimum size for the widget.
0185  *
0186  * Reimplemented from base class.
0187  *
0188  * @returns Recommended minimum size for the widget.
0189  *
0190  * @sa @ref sizeHint() */
0191 QSize SwatchBook::minimumSizeHint() const
0192 {
0193     ensurePolished();
0194     QStyleOptionFrame myOption;
0195     d_pointer->initStyleOption(&myOption);
0196     const auto contentSize = d_pointer->colorPatchesSizeWithMargin();
0197     const auto styleSize = style()->sizeFromContents(QStyle::CT_LineEdit, //
0198                                                      &myOption,
0199                                                      contentSize,
0200                                                      this);
0201 
0202     // On some style like (for example the MacOS style), sizeFromContents()
0203     // for line edits returns a value that is less height than the content
0204     // size. Therefore, here comes some safety code:
0205     const auto lineWidth = myOption.lineWidth;
0206     QMargins margins{lineWidth, lineWidth, lineWidth, lineWidth};
0207     const QSize minimumSize = contentSize.grownBy(margins);
0208 
0209     return minimumSize.expandedTo(styleSize);
0210 }
0211 
0212 // No documentation here (documentation of properties
0213 // and its getters are in the header)
0214 QColor SwatchBook::currentColor() const
0215 {
0216     return d_pointer->m_currentColor;
0217 }
0218 
0219 /** @brief Setter for the @ref currentColor property.
0220  *
0221  * @param newCurrentColor the new color */
0222 void SwatchBook::setCurrentColor(const QColor &newCurrentColor)
0223 {
0224     // Convert to RGB:
0225     QColor temp = newCurrentColor;
0226     if (!temp.isValid()) {
0227         temp = Qt::black; // Conformance with QColorDialog
0228     }
0229     if (temp.spec() != QColor::Spec::Rgb) {
0230         // Make sure that the QColor::spec() is QColor::Spec::Rgb.
0231         // QColorDialog apparently calls QColor.rgb() within its
0232         // setCurrentColor function; this will however round to 8 bit
0233         // per channel. We prefer a more exact conversion to RGB:
0234         temp = QColor::fromRgbF( //
0235             temp.redF(),
0236             temp.greenF(),
0237             temp.blueF(),
0238             temp.alphaF());
0239     }
0240 
0241     if (temp == d_pointer->m_currentColor) {
0242         return;
0243     }
0244 
0245     d_pointer->m_currentColor = temp;
0246 
0247     bool colorFound = false;
0248     const qsizetype myColumnCount = d_pointer->m_swatches.iCount();
0249     const qsizetype myRowCount = d_pointer->m_swatches.jCount();
0250     int columnIndex = 0;
0251     int rowIndex = 0;
0252     for (columnIndex = 0; //
0253          columnIndex < myColumnCount; //
0254          ++columnIndex) {
0255         for (rowIndex = 0; rowIndex < myRowCount; ++rowIndex) {
0256             if (d_pointer->m_swatches.value(columnIndex, rowIndex) == temp) {
0257                 colorFound = true;
0258                 break;
0259             }
0260         }
0261         if (colorFound) {
0262             break;
0263         }
0264     }
0265     if (colorFound) {
0266         d_pointer->m_selectedColumn = columnIndex;
0267         d_pointer->m_selectedRow = rowIndex;
0268     } else {
0269         d_pointer->m_selectedColumn = -1;
0270         d_pointer->m_selectedRow = -1;
0271     }
0272 
0273     Q_EMIT currentColorChanged(temp);
0274 
0275     update();
0276 }
0277 
0278 /** @brief Selects a swatch from the book.
0279  *
0280  * @pre Both parameters are valid indexes within @ref m_swatches.
0281  * (Otherwise there will likely be a crash.)
0282  *
0283  * @param newCurrentColomn Index of the column.
0284  *
0285  * @param newCurrentRow Index of the row.
0286  *
0287  * @post If the given swatch is empty, nothing happens. Otherwise, it is
0288  * selected and the selection mark is visible and @ref SwatchBook::currentColor
0289  * has the value of this color. */
0290 void SwatchBookPrivate::selectSwatch(QListSizeType newCurrentColomn, QListSizeType newCurrentRow)
0291 {
0292     const auto newColor = m_swatches.value(newCurrentColomn, newCurrentRow);
0293     if (!newColor.isValid()) {
0294         return;
0295     }
0296     m_selectedColumn = newCurrentColomn;
0297     m_selectedRow = newCurrentRow;
0298     if (newColor != m_currentColor) {
0299         m_currentColor = newColor;
0300         Q_EMIT q_pointer->currentColorChanged(newColor);
0301     }
0302     q_pointer->update();
0303 }
0304 
0305 /** @brief Horizontal spacing between color patches.
0306  *
0307  * @returns Horizontal spacing between color patches, measured in
0308  * device-independent pixel. The value depends on the
0309  * current <tt>QStyle</tt>.
0310  *
0311  * @sa @ref verticalPatchSpacing */
0312 int SwatchBookPrivate::horizontalPatchSpacing() const
0313 {
0314     if (m_wideSpacing.testFlag(Qt::Orientation::Horizontal)) {
0315         return widePatchSpacing();
0316     }
0317     return normalPatchSpacing();
0318 }
0319 
0320 /** @brief Value for a wide spacing between swatches.
0321  *
0322  * @returns Wide spacing between color patches, measured in
0323  * device-independent pixel. The value depends on the
0324  * current <tt>QStyle</tt>.
0325  *
0326  * @sa @ref normalPatchSpacing */
0327 int SwatchBookPrivate::widePatchSpacing() const
0328 {
0329     // NOTE The value is derived from the current QStyle’s values for some
0330     // horizontal spacings. This seems reasonable because some styles might
0331     // have tighter metrics for vertical spacing, which might not look good
0332     // here. The derived value is actually useful for both, horizontal and
0333     // vertical metrics.
0334 
0335     int temp = q_pointer->style()->pixelMetric( //
0336         QStyle::PM_LayoutHorizontalSpacing,
0337         nullptr,
0338         q_pointer.toPointerToConstObject());
0339     if (temp <= 0) {
0340         // Some styles like Qt’s build-in “Plastique” style or the external
0341         // “QtCurve” style return 0 here. If so, we fall back to the left
0342         // margin. (We do not use qMax() because this workaround should
0343         // really only apply when the returned value is 0, because under
0344         // normal circumstances, it might be intentional that the left
0345         // margin is bigger than the horizontal spacing.)
0346         temp = q_pointer->style()->pixelMetric( //
0347             QStyle::PM_LayoutLeftMargin,
0348             nullptr,
0349             q_pointer.toPointerToConstObject());
0350     }
0351     // Another fallback (if also PM_LayoutLeftMargin fails):
0352     if (temp <= 0) {
0353         temp = q_pointer->style()->pixelMetric( //
0354             QStyle::PM_DefaultFrameWidth,
0355             nullptr,
0356             q_pointer.toPointerToConstObject());
0357     }
0358     // A last-resort fallback:
0359     return qMax(temp, 2);
0360 }
0361 
0362 /** @brief Vertical spacing between color patches.
0363  *
0364  * @returns Vertical spacing between color patches, measured in
0365  * device-independent pixel. The value is typically smaller than
0366  * @ref horizontalPatchSpacing(), to symbolize that the binding
0367  * between patches is vertically stronger than horizontally. */
0368 int SwatchBookPrivate::verticalPatchSpacing() const
0369 {
0370     if (m_wideSpacing.testFlag(Qt::Orientation::Vertical)) {
0371         return widePatchSpacing();
0372     }
0373     return normalPatchSpacing();
0374 }
0375 
0376 /** @brief Normal spacing between color swatches.
0377  *
0378  * @returns Normal spacing between color patches, measured in
0379  * device-independent pixel. The value is typically smaller than
0380  * @ref widePatchSpacing(), to symbolize that the binding
0381  * between patches is stronger. */
0382 int SwatchBookPrivate::normalPatchSpacing() const
0383 {
0384     return qMax(widePatchSpacing() / 3, // ⅓ looks nice
0385                 1 // minimal useful value for a line visible as all scales
0386     );
0387 }
0388 
0389 /** @brief Initializes a <tt>QStyleOptionFrame</tt> object for this widget
0390  * in its current state.
0391  *
0392  * This function is provided analogous to many Qt widgets that also
0393  * provide a function of that name with this purpose.
0394  *
0395  * @param option The object that will be initialized
0396  *
0397  * @note The value in QStyleOptionFrame::rect is not initialized. */
0398 void SwatchBookPrivate::initStyleOption(QStyleOptionFrame *option) const
0399 {
0400     if (option == nullptr) {
0401         return;
0402     }
0403     option->initFrom(q_pointer.toPointerToConstObject());
0404     option->lineWidth = q_pointer->style()->pixelMetric( //
0405         QStyle::PM_DefaultFrameWidth,
0406         option,
0407         q_pointer.toPointerToConstObject());
0408     option->midLineWidth = 0;
0409     option->state |= QStyle::State_Sunken;
0410     // The following option is not set because this widget
0411     // currently has no read-only mode:
0412     // option->state |= QStyle::State_ReadOnly;
0413     option->features = QStyleOptionFrame::None;
0414 }
0415 
0416 /** @brief Offset between top-left of the widget and top-left of the content.
0417  *
0418  * @param styleOptionFrame The options that will be passed to <tt>QStyle</tt>
0419  * to calculate correctly the offset.
0420  *
0421  * @returns The pixel position of the top-left pixel of the content area
0422  * which can be used for the color patches. */
0423 QPoint SwatchBookPrivate::offset(const QStyleOptionFrame &styleOptionFrame) const
0424 {
0425     const QPoint innerMarginOffset = QPoint( //
0426         q_pointer->style()->pixelMetric(QStyle::PM_LayoutLeftMargin),
0427         q_pointer->style()->pixelMetric(QStyle::PM_LayoutTopMargin));
0428 
0429     QStyleOptionFrame temp = styleOptionFrame; // safety copy
0430     const QRectF frameContentRectangle = q_pointer->style()->subElementRect( //
0431         QStyle::SE_LineEditContents,
0432         &temp, // Risk of changes, therefore using the safety copy
0433         q_pointer.toPointerToConstObject());
0434     const QSizeF swatchbookContentSize = colorPatchesSizeWithMargin();
0435 
0436     // Some styles, such as the Fusion style, regularly return a slightly
0437     // larger rectangle through QStyle::subElementRect() than the one
0438     // requested in SwatchBook::minimumSizeHint(), which we need to draw
0439     // the color patches. In the case of the Kvantum style,
0440     // QStyle::subElementRect().height() is even greater than
0441     // SwatchBook::height(). It extends beyond the widget's own dimensions,
0442     // both at the top and bottom. QStyle::subElementRect().y() is
0443     // negative. Please see https://github.com/tsujan/Kvantum/issues/676
0444     // for more information. To ensure a visually pleasing rendering, we
0445     // implement centering within QStyle::subElementRect().
0446     QPointF frameOffset = frameContentRectangle.center();
0447     frameOffset.rx() -= swatchbookContentSize.width() / 2.;
0448     frameOffset.ry() -= swatchbookContentSize.height() / 2.;
0449 
0450     return (frameOffset + innerMarginOffset).toPoint();
0451 }
0452 
0453 /** @brief React on a mouse press event.
0454  *
0455  * Reimplemented from base class.
0456  *
0457  * @param event The corresponding mouse event */
0458 void SwatchBook::mousePressEvent(QMouseEvent *event)
0459 {
0460     // NOTE We will not actively ignore the event, even if we didn’t actually
0461     // react on it. Therefore, Breeze and other styles cannot move
0462     // the window when clicking in the middle between two patches.
0463     // This is intentional, because allowing it would be confusing:
0464     // - The space between the patches is quite limited anyway, so
0465     //   it’s not worth the pain and could be surprising because somebody
0466     //   can click there by mistake.
0467     // - We use the same background as QLineEdit, which at its turn also
0468     //   does not allow moving the window with a left-click within the
0469     //   field. We should be consistent with this behaviour.
0470 
0471     const QSize myColorPatchSize = d_pointer->patchSizeOuter();
0472     const int myPatchWidth = myColorPatchSize.width();
0473     const int myPatchHeight = myColorPatchSize.height();
0474     QStyleOptionFrame myFrameStyleOption;
0475     d_pointer->initStyleOption(&myFrameStyleOption);
0476     const QPoint temp = event->pos() - d_pointer->offset(myFrameStyleOption);
0477 
0478     if ((temp.x() < 0) || (temp.y() < 0)) {
0479         return; // Click position too much leftwards or upwards.
0480     }
0481 
0482     const auto columnWidth = myPatchWidth + d_pointer->horizontalPatchSpacing();
0483     const int xWithinPatch = temp.x() % columnWidth;
0484     if (xWithinPatch >= myPatchWidth) {
0485         return; // Click position horizontally between two patch columns
0486     }
0487 
0488     const auto rowHeight = myPatchHeight + d_pointer->verticalPatchSpacing();
0489     const int yWithinPatch = temp.y() % rowHeight;
0490     if (yWithinPatch >= myPatchHeight) {
0491         return; // Click position vertically between two patch rows
0492     }
0493 
0494     const int rowIndex = temp.y() / rowHeight;
0495     if (!isInRange<qsizetype>(0, rowIndex, d_pointer->m_swatches.jCount() - 1)) {
0496         // The index is out of range. This might happen when the user
0497         // clicks very near to the border, where is no other patch
0498         // anymore, but which is still part of the widget.
0499         return;
0500     }
0501 
0502     const int visualColumnIndex = temp.x() / columnWidth;
0503     QListSizeType columnIndex;
0504     if (layoutDirection() == Qt::LayoutDirection::LeftToRight) {
0505         columnIndex = visualColumnIndex;
0506     } else {
0507         columnIndex = //
0508             d_pointer->m_swatches.iCount() - 1 - visualColumnIndex;
0509     }
0510     if (!isInRange<qsizetype>(0, columnIndex, d_pointer->m_swatches.iCount() - 1)) {
0511         // The index is out of range. This might happen when the user
0512         // clicks very near to the border, where is no other patch
0513         // anymore, but which is still part of the widget.
0514         return;
0515     }
0516 
0517     // If we reached here, the click must have been within a patch
0518     // and we have valid indexes.
0519     d_pointer->selectSwatch(columnIndex, rowIndex);
0520 }
0521 
0522 /** @brief The size of the color patches.
0523  *
0524  * This is the bounding box around the outer limit.
0525  *
0526  * @returns The size of the color patches, measured in device-independent
0527  * pixel.
0528  *
0529  * @sa @ref patchSizeInner */
0530 QSize SwatchBookPrivate::patchSizeOuter() const
0531 {
0532     q_pointer->ensurePolished();
0533     const QSize mySize = patchSizeInner();
0534     QStyleOptionToolButton myOptions;
0535     myOptions.initFrom(q_pointer.toPointerToConstObject());
0536     myOptions.rect.setSize(mySize);
0537     return q_pointer->style()->sizeFromContents( //
0538         QStyle::CT_ToolButton,
0539         &myOptions,
0540         mySize,
0541         q_pointer.toPointerToConstObject());
0542 }
0543 
0544 /** @brief Size of the inner space of a color patch.
0545  *
0546  * This is typically smaller than @ref patchSizeOuter.
0547  *
0548  * @returns Size of the inner space of a color patch, measured in
0549  * device-independent pixel. */
0550 QSize SwatchBookPrivate::patchSizeInner() const
0551 {
0552     const int metric = q_pointer->style()->pixelMetric( //
0553         QStyle::PM_ButtonIconSize,
0554         nullptr,
0555         q_pointer.toPointerToConstObject());
0556     const int size = std::max({metric, //
0557                                horizontalPatchSpacing(), //
0558                                verticalPatchSpacing()});
0559     return QSize(size, size);
0560 }
0561 
0562 /** @brief Paint the widget.
0563  *
0564  * Reimplemented from base class.
0565  *
0566  * @param event the paint event */
0567 void SwatchBook::paintEvent(QPaintEvent *event)
0568 {
0569     Q_UNUSED(event)
0570 
0571     // Initialization
0572     QPainter widgetPainter(this);
0573     widgetPainter.setRenderHint(QPainter::Antialiasing);
0574     QStyleOptionFrame frameStyleOption;
0575     d_pointer->initStyleOption(&frameStyleOption);
0576     const int horizontalSpacing = d_pointer->horizontalPatchSpacing();
0577     const int verticalSpacing = d_pointer->verticalPatchSpacing();
0578     const QSize patchSizeOuter = d_pointer->patchSizeOuter();
0579     const int patchWidthOuter = patchSizeOuter.width();
0580     const int patchHeightOuter = patchSizeOuter.height();
0581 
0582     // Draw the background
0583     {
0584         // We draw the frame slightly shrunk on windowsvista style. Otherwise,
0585         // when the windowsvista style is used on 125% scale factor and with
0586         // a multi-monitor setup, the frame would sometimes not render on some
0587         // of the borders on some of the screens.
0588         const bool vistaStyle = QString::compare( //
0589                                     QApplication::style()->objectName(),
0590                                     QStringLiteral("windowsvista"),
0591                                     Qt::CaseInsensitive)
0592             == 0;
0593         const int shrink = vistaStyle ? 1 : 0;
0594         const QMargins margins(shrink, shrink, shrink, shrink);
0595         auto shrunkFrameStyleOption = frameStyleOption;
0596         shrunkFrameStyleOption.rect = frameStyleOption.rect - margins;
0597         // NOTE On ukui style, drawing this primitive results in strange
0598         // rendering on mouse hover. Actual behaviour: The whole panel
0599         // background is drawn blue. Expected behaviour: Only the frame is
0600         // drawn blue (as ukui actually does when rendering a QLineEdit).
0601         // Playing around with PE_FrameLineEdit instead of or additional to
0602         // PE_PanelLineEdit did not give better results either.
0603         // As ukui has many, many graphical glitches and bugs (up to
0604         // crashed unfixed for years), we assume that this is a problem of
0605         // ukui, and not of our code. Furthermore, while the behavior is
0606         // unexpected, the rendering doesn’t look completely terrible; we
0607         // can live with that.
0608         style()->drawPrimitive(QStyle::PE_PanelLineEdit, //
0609                                &shrunkFrameStyleOption,
0610                                &widgetPainter,
0611                                this);
0612     }
0613 
0614     // Draw the color patches
0615     const QPoint offset = d_pointer->offset(frameStyleOption);
0616     const QListSizeType columnCount = d_pointer->m_swatches.iCount();
0617     QListSizeType visualColumn;
0618     for (int columnIndex = 0; columnIndex < columnCount; ++columnIndex) {
0619         for (int row = 0; //
0620              row < d_pointer->m_swatches.jCount(); //
0621              ++row //
0622         ) {
0623             const auto swatchColor = //
0624                 d_pointer->m_swatches.value(columnIndex, row);
0625             if (swatchColor.isValid()) {
0626                 widgetPainter.setBrush(swatchColor);
0627                 widgetPainter.setPen(Qt::NoPen);
0628                 if (layoutDirection() == Qt::LayoutDirection::LeftToRight) {
0629                     visualColumn = columnIndex;
0630                 } else {
0631                     visualColumn = columnCount - 1 - columnIndex;
0632                 }
0633                 widgetPainter.drawRect( //
0634                     offset.x() //
0635                         + (static_cast<int>(visualColumn) //
0636                            * (patchWidthOuter + horizontalSpacing)),
0637                     offset.y() //
0638                         + row * (patchHeightOuter + verticalSpacing),
0639                     patchWidthOuter,
0640                     patchHeightOuter);
0641             }
0642         }
0643     }
0644 
0645     // If there is no selection mark to draw, nothing more to do: Return!
0646     if (d_pointer->m_selectedColumn < 0 || d_pointer->m_selectedRow < 0) {
0647         return;
0648     }
0649 
0650     // Draw the selection mark (if any)
0651     const QListSizeType visualSelectedColumnIndex = //
0652         (layoutDirection() == Qt::LayoutDirection::LeftToRight) //
0653         ? d_pointer->m_selectedColumn //
0654         : d_pointer->m_swatches.iCount() - 1 - d_pointer->m_selectedColumn;
0655     const LchDouble colorCielchD50 = //
0656         d_pointer->m_rgbColorSpace->toCielchD50Double( //
0657             d_pointer //
0658                 ->m_swatches //
0659                 .value(d_pointer->m_selectedColumn, d_pointer->m_selectedRow) //
0660                 .rgba64() //
0661         );
0662     const QColor selectionMarkColor = //
0663         handleColorFromBackgroundLightness(colorCielchD50.l);
0664     const QPointF selectedPatchOffset = QPointF( //
0665         offset.x() //
0666             + (static_cast<int>(visualSelectedColumnIndex) //
0667                * (patchWidthOuter + horizontalSpacing)), //
0668         offset.y() //
0669             + (static_cast<int>(d_pointer->m_selectedRow) //
0670                * (patchHeightOuter + verticalSpacing)));
0671     const QSize patchSizeInner = d_pointer->patchSizeInner();
0672     const int patchWidthInner = patchSizeInner.width();
0673     const int patchHeightInner = patchSizeInner.height();
0674     if (d_pointer->m_selectionMark.isEmpty()) {
0675         // If no selection mark is available for the current translation in
0676         // the current font, we will draw a hard-coded fallback mark.
0677         const QSize sizeDifference = patchSizeOuter - patchSizeInner;
0678         // Offset of the selection mark to the border of the patch:
0679         QPointF selectionMarkOffset = QPointF( //
0680             sizeDifference.width() / 2.0,
0681             sizeDifference.height() / 2.0);
0682         if (patchWidthInner > patchHeightInner) {
0683             selectionMarkOffset.rx() += //
0684                 ((patchWidthInner - patchHeightInner) / 2.0);
0685         }
0686         if (patchHeightInner > patchWidthInner) {
0687             selectionMarkOffset.ry() += //
0688                 ((patchHeightInner - patchWidthInner) / 2.0);
0689         }
0690         const int effectiveSquareSize = qMin( //
0691             patchHeightInner,
0692             patchWidthInner);
0693         qreal penWidth = effectiveSquareSize * 0.08;
0694         QPen pen;
0695         pen.setColor(selectionMarkColor);
0696         pen.setCapStyle(Qt::PenCapStyle::RoundCap);
0697         pen.setWidthF(penWidth);
0698         widgetPainter.setPen(pen);
0699         QPointF point1 = QPointF(penWidth, //
0700                                  0.7 * effectiveSquareSize);
0701         point1 += selectedPatchOffset + selectionMarkOffset;
0702         QPointF point2(0.35 * effectiveSquareSize, //
0703                        1 * effectiveSquareSize - penWidth);
0704         point2 += selectedPatchOffset + selectionMarkOffset;
0705         QPointF point3(1 * effectiveSquareSize - penWidth, //
0706                        penWidth);
0707         point3 += selectedPatchOffset + selectionMarkOffset;
0708         widgetPainter.drawLine(QLineF(point1, point2));
0709         widgetPainter.drawLine(QLineF(point2, point3));
0710     } else {
0711         QPainterPath textPath;
0712         // Render the selection mark string in the path
0713         textPath.addText(0, 0, font(), d_pointer->m_selectionMark);
0714         // Align the path top-left to the path’s virtual coordinate system
0715         textPath.translate(textPath.boundingRect().x() * (-1), //
0716                            textPath.boundingRect().y() * (-1));
0717         // QPainterPath::boundingRect() might be slow. Cache the result:
0718         const QSizeF boundingRectangleSize = textPath.boundingRect().size();
0719 
0720         if (!boundingRectangleSize.isEmpty()) { // Prevent division by 0
0721             QTransform textTransform;
0722 
0723             // Offset for the current patch
0724             textTransform.translate(
0725                 // x:
0726                 selectedPatchOffset.x() //
0727                     + (patchWidthOuter - patchWidthInner) / 2,
0728                 // y:
0729                 selectedPatchOffset.y() //
0730                     + (patchHeightOuter - patchHeightInner) / 2);
0731 
0732             // Scale to maximum and center within the margins
0733             const qreal scaleFactor = qMin(
0734                 // Horizontal scale factor:
0735                 patchWidthInner / boundingRectangleSize.width(),
0736                 // Vertical scale factor:
0737                 patchHeightInner / boundingRectangleSize.height());
0738             QSizeF scaledSelectionMarkSize = //
0739                 boundingRectangleSize * scaleFactor;
0740             const QSizeF temp = //
0741                 (patchSizeInner - scaledSelectionMarkSize) / 2;
0742             textTransform.translate(temp.width(), temp.height());
0743             textTransform.scale(scaleFactor, scaleFactor);
0744 
0745             // Draw
0746             widgetPainter.setTransform(textTransform);
0747             widgetPainter.setPen(Qt::NoPen);
0748             widgetPainter.setBrush(selectionMarkColor);
0749             widgetPainter.drawPath(textPath);
0750         }
0751     }
0752 }
0753 
0754 /** @brief React on key press events.
0755  *
0756  * Reimplemented from base class.
0757  *
0758  * When the arrow keys are pressed, it moves the selection mark
0759  * into the desired direction.
0760  * When <tt>Qt::Key_PageUp</tt>, <tt>Qt::Key_PageDown</tt>,
0761  * <tt>Qt::Key_Home</tt> or <tt>Qt::Key_End</tt> are pressed, it moves the
0762  * handle a big step into the desired direction.
0763  *
0764  * Other key events are forwarded to the base class.
0765  *
0766  * @param event the event */
0767 void SwatchBook::keyPressEvent(QKeyEvent *event)
0768 {
0769     QListSizeType columnShift = 0;
0770     QListSizeType rowShift = 0;
0771     const int writingDirection = //
0772         (layoutDirection() == Qt::LeftToRight) //
0773         ? 1 //
0774         : -1;
0775     switch (event->key()) {
0776     case Qt::Key_Up:
0777         rowShift = -1;
0778         break;
0779     case Qt::Key_Down:
0780         rowShift = 1;
0781         break;
0782     case Qt::Key_Left:
0783         columnShift = -1 * writingDirection;
0784         break;
0785     case Qt::Key_Right:
0786         columnShift = 1 * writingDirection;
0787         break;
0788     case Qt::Key_PageUp:
0789         rowShift = (-1) * d_pointer->m_swatches.jCount();
0790         break;
0791     case Qt::Key_PageDown:
0792         rowShift = d_pointer->m_swatches.jCount();
0793         break;
0794     case Qt::Key_Home:
0795         columnShift = (-1) * d_pointer->m_swatches.iCount();
0796         break;
0797     case Qt::Key_End:
0798         columnShift = d_pointer->m_swatches.iCount();
0799         break;
0800     default:
0801         // Quote from Qt documentation:
0802         //
0803         //     “If you reimplement this handler, it is very important that
0804         //      you call the base class implementation if you do not act
0805         //      upon the key.
0806         //
0807         //      The default implementation closes popup widgets if the
0808         //      user presses the key sequence for QKeySequence::Cancel
0809         //      (typically the Escape key). Otherwise the event is
0810         //      ignored, so that the widget’s parent can interpret it.“
0811         QWidget::keyPressEvent(event);
0812         return;
0813     }
0814     // Here we reach only if the key has been recognized. If not, in the
0815     // default branch of the switch statement, we would have passed the
0816     // keyPressEvent yet to the parent and returned.
0817 
0818     // If currently no color of the swatch book is selected, select the
0819     // first color as default.
0820     if ((d_pointer->m_selectedColumn < 0) && (d_pointer->m_selectedRow < 0)) {
0821         d_pointer->selectSwatch(0, 0);
0822         return;
0823     }
0824 
0825     const int accelerationFactor = 2;
0826     if (event->modifiers().testFlag(Qt::ControlModifier)) {
0827         columnShift *= accelerationFactor;
0828         rowShift *= accelerationFactor;
0829     }
0830 
0831     d_pointer->selectSwatch( //
0832         qBound<QListSizeType>(0, //
0833                               d_pointer->m_selectedColumn + columnShift,
0834                               d_pointer->m_swatches.iCount() - 1),
0835         qBound<QListSizeType>(0, //
0836                               d_pointer->m_selectedRow + rowShift, //
0837                               d_pointer->m_swatches.jCount() - 1));
0838 }
0839 
0840 /** @brief Handle state changes.
0841  *
0842  * Implements reaction on <tt>QEvent::LanguageChange</tt>.
0843  *
0844  * Reimplemented from base class.
0845  *
0846  * @param event The event. */
0847 void SwatchBook::changeEvent(QEvent *event)
0848 {
0849     if (event->type() == QEvent::LanguageChange) {
0850         // From QCoreApplication documentation:
0851         //     “Installing or removing a QTranslator, or changing an installed
0852         //      QTranslator generates a LanguageChange event for the
0853         //      QCoreApplication instance. A QApplication instance will
0854         //      propagate the event to all toplevel widgets […].
0855         // Retranslate this widget itself:
0856         d_pointer->retranslateUi();
0857     }
0858 
0859     QWidget::changeEvent(event);
0860 }
0861 
0862 /** @brief Size necessary to render the color patches, including a margin.
0863  *
0864  * @returns Size necessary to render the color patches, including a margin.
0865  * Measured in device-independent pixels. */
0866 QSize SwatchBookPrivate::colorPatchesSizeWithMargin() const
0867 {
0868     q_pointer->ensurePolished();
0869     const QSize patchSize = patchSizeOuter();
0870     const int columnCount = static_cast<int>(m_swatches.iCount());
0871     const int rowCount = static_cast<int>(m_swatches.jCount());
0872     const int width = //
0873         q_pointer->style()->pixelMetric(QStyle::PM_LayoutLeftMargin) //
0874         + columnCount * patchSize.width() //
0875         + (columnCount - 1) * horizontalPatchSpacing() //
0876         + q_pointer->style()->pixelMetric(QStyle::PM_LayoutRightMargin);
0877     const int height = //
0878         q_pointer->style()->pixelMetric(QStyle::PM_LayoutTopMargin) //
0879         + rowCount * patchSize.height() //
0880         + (rowCount - 1) * verticalPatchSpacing() //
0881         + q_pointer->style()->pixelMetric(QStyle::PM_LayoutBottomMargin);
0882     return QSize(width, height);
0883 }
0884 
0885 } // namespace PerceptualColor