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