Perceptual Color

swatchbook.cpp
1// SPDX-FileCopyrightText: Lukas Sommer <sommerluk@gmail.com>
2// SPDX-License-Identifier: BSD-2-Clause OR MIT
3
4// Own headers
5// First the interface, which forces the header to be self-contained.
6#include "swatchbook.h"
7// Second, the private implementation.
8#include "swatchbook_p.h" // IWYU pragma: associated
9
10#include "abstractdiagram.h"
11#include "constpropagatingrawpointer.h"
12#include "constpropagatinguniquepointer.h"
13#include "genericcolor.h"
14#include "helper.h"
15#include "helpermath.h"
16#include "initializetranslation.h"
17#include "rgbcolorspace.h"
18#include <algorithm>
19#include <optional>
20#include <qapplication.h>
21#include <qcoreapplication.h>
22#include <qcoreevent.h>
23#include <qevent.h>
24#include <qfontmetrics.h>
25#include <qline.h>
26#include <qlist.h>
27#include <qmargins.h>
28#include <qmetatype.h>
29#include <qnamespace.h>
30#include <qpainter.h>
31#include <qpainterpath.h>
32#include <qpen.h>
33#include <qpoint.h>
34#include <qrect.h>
35#include <qsharedpointer.h>
36#include <qsizepolicy.h>
37#include <qstringliteral.h>
38#include <qstyle.h>
39#include <qstyleoption.h>
40#include <qtransform.h>
41#include <qwidget.h>
42
43#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
44#include <qcontainerfwd.h>
45#else
46#include <qstringlist.h>
47#include <qvector.h>
48#endif
49
50namespace PerceptualColor
51{
52
53/** @brief Retranslate the UI with all user-visible strings.
54 *
55 * This function updates all user-visible strings by using
56 * <tt>Qt::tr()</tt> to get up-to-date translations.
57 *
58 * This function is meant to be called at the end of the constructor and
59 * additionally after each <tt>QEvent::LanguageChange</tt> event.
60 *
61 * @note This is the same concept as
62 * <a href="https://doc.qt.io/qt-5/designer-using-a-ui-file.html">
63 * Qt Designer, which also provides a function of the same name in
64 * uic-generated code</a>. */
65void SwatchBookPrivate::retranslateUi()
66{
67 // Which symbol is appropriate as selection mark? This might depend on
68 // culture and language. For more information, see also
69 // https://en.wikipedia.org/w/index.php?title=Check_mark&oldid=1030853305#International_differences
70 // Therefore, we provide translation support for the selection mark.
71
72 // NOTE Some candidates for “translations” of this character might be
73 // emoji characters that might render colorful on some systems and
74 // some fonts. It would be great to disable color fonts and only
75 // accept black fonts. However, this seems to be impossible with Qt.
76 // There is a command-line option named “nocolorfonts”, documented at
77 // https://doc.qt.io/qt-6/qguiapplication.html#QGuiApplication
78 // However, this is only available for DirectWrite font rendering
79 // on Windows. There does not seem to be a cross-platform solution
80 // currently.
81 /*: @item Indicate the selected color in the swatch book. This symbol
82 should be translated to whatever symbol is most appropriate for “selected”
83 in the translation language. Example symbols: ✓ U+2713 CHECK MARK.
84 ✗ U+2717 BALLOT X. ✘ U+2718 HEAVY BALLOT X. ○ U+25CB WHITE CIRCLE.
85 ◯ U+25EF LARGE CIRCLE. Do not use emoji characters as they may render
86 colorful on some systems, so they will ignore the automatically chosen
87 color which is used get best contrast with the background. (Also
88 U+FE0E VARIATION SELECTOR-15 does not prevent colorful rendering.) */
89 const QString translation = tr("✓");
90
91 // Test if all characters of the translated string are actually
92 // available in the given font.
93 auto ucs4 = translation.toUcs4();
94 bool okay = true;
95 QFontMetricsF myFontMetrics(q_pointer->font());
96 for (int i = 0; okay && (i < ucs4.count()); ++i) {
97 okay = myFontMetrics.inFontUcs4(ucs4.at(i));
98 }
99
100 // Return
101 if (okay) {
102 m_selectionMark = translation;
103 } else {
104 m_selectionMark = QString();
105 }
106
107 // Schedule a paint event to make the changes visible.
108 q_pointer->update();
109}
110
111/** @brief Constructor
112 *
113 * @param colorSpace The color space of the swatches.
114 * @param swatchGrid Initial value for property @ref swatchGrid
115 * @param wideSpacing Set of axis where the spacing should be wider than
116 * normal. This is useful to give some visual structure: When your
117 * swatches are organized logically in columns, set
118 * <tt>Qt::Orientation::Horizontal</tt> here. To use normal spacing
119 * everywhere, simply set this parameter to <tt>{}</tt>.
120 * @param parent The parent of the widget, if any */
121SwatchBook::SwatchBook(const QSharedPointer<PerceptualColor::RgbColorSpace> &colorSpace,
122 const PerceptualColor::Swatches &swatchGrid,
123 Qt::Orientations wideSpacing,
124 QWidget *parent)
125 : AbstractDiagram(parent)
126 , d_pointer(new SwatchBookPrivate(this, swatchGrid, wideSpacing))
127{
128 qRegisterMetaType<Swatches>();
129
130 d_pointer->m_rgbColorSpace = colorSpace;
131
132 setFocusPolicy(Qt::FocusPolicy::StrongFocus);
133
135
136 // Trigger paint events whenever the mouse enters or leaves the widget.
137 // (Important on some QStyle who might paint widgets different then.)
138 setAttribute(Qt::WA_Hover);
139
140 // Initialize the selection (and implicitly the currentColor property):
141 d_pointer->selectSwatch(8, 0); // Same default as in QColorDialog
142
143 initializeTranslation(QCoreApplication::instance(),
144 // An empty std::optional means: If in initialization
145 // had been done yet, repeat this initialization.
146 // If not, do a new initialization now with default
147 // values.
148 std::optional<QStringList>());
149 d_pointer->retranslateUi();
150}
151
152/** @brief Destructor */
153SwatchBook::~SwatchBook() noexcept
154{
155}
156
157/** @brief Constructor
158 *
159 * @param backLink Pointer to the object from which <em>this</em> object
160 * is the private implementation.
161 * @param swatchGrid The swatches.
162 * @param wideSpacing Set of axis using @ref widePatchSpacing instead
163 * of @ref normalPatchSpacing. */
164SwatchBookPrivate::SwatchBookPrivate(SwatchBook *backLink, const PerceptualColor::Swatches &swatchGrid, Qt::Orientations wideSpacing)
165 : m_swatchGrid(swatchGrid)
166 , m_wideSpacing(wideSpacing)
167 , q_pointer(backLink)
168{
169}
170
171/** @brief Recommended size for the widget.
172 *
173 * Reimplemented from base class.
174 *
175 * @returns Recommended size for the widget.
176 *
177 * @sa @ref minimumSizeHint() */
178QSize SwatchBook::sizeHint() const
179{
180 return minimumSizeHint();
181}
182
183/** @brief Recommended minimum size for the widget.
184 *
185 * Reimplemented from base class.
186 *
187 * @returns Recommended minimum size for the widget.
188 *
189 * @sa @ref sizeHint() */
190QSize SwatchBook::minimumSizeHint() const
191{
192 ensurePolished();
193 QStyleOptionFrame myOption;
194 d_pointer->initStyleOption(&myOption);
195 const auto contentSize = d_pointer->colorPatchesSizeWithMargin();
196 const auto styleSize = style()->sizeFromContents(QStyle::CT_LineEdit, //
197 &myOption,
198 contentSize,
199 this);
200
201 // On some style like (for example the MacOS style), sizeFromContents()
202 // for line edits returns a value that is less height than the content
203 // size. Therefore, here comes some safety code:
204 const auto lineWidth = myOption.lineWidth;
205 QMargins margins{lineWidth, lineWidth, lineWidth, lineWidth};
206 const QSize minimumSize = contentSize.grownBy(margins);
207
208 return minimumSize.expandedTo(styleSize);
209}
210
211// No documentation here (documentation of properties
212// and its getters are in the header)
213QColor SwatchBook::currentColor() const
214{
215 return d_pointer->m_currentColor;
216}
217
218/** @brief Setter for the @ref currentColor property.
219 *
220 * @param newCurrentColor the new color */
221void SwatchBook::setCurrentColor(const QColor &newCurrentColor)
222{
223 // Convert to RGB:
224 QColor temp = newCurrentColor;
225 if (!temp.isValid()) {
226 temp = Qt::black; // Conformance with QColorDialog
227 }
228 if (temp.spec() != QColor::Spec::Rgb) {
229 // Make sure that the QColor::spec() is QColor::Spec::Rgb.
230 // QColorDialog apparently calls QColor.rgb() within its
231 // setCurrentColor function; this will however round to 8 bit
232 // per channel. We prefer a more exact conversion to RGB:
233 temp = QColor::fromRgbF( //
234 temp.redF(),
235 temp.greenF(),
236 temp.blueF(),
237 temp.alphaF());
238 }
239
240 if (temp == d_pointer->m_currentColor) {
241 return;
242 }
243
244 d_pointer->m_currentColor = temp;
245
246 d_pointer->selectSwatchFromCurrentColor();
247
248 Q_EMIT currentColorChanged(temp);
249
250 update();
251}
252
253// No documentation here (documentation of properties
254// and its getters are in the header)
255Swatches SwatchBook::swatchGrid() const
256{
257 return d_pointer->m_swatchGrid;
258}
259
260/** @brief Setter for the @ref swatchGrid property.
261 *
262 * @param newSwatchGrid the new value */
263void SwatchBook::setSwatchGrid(const PerceptualColor::Swatches &newSwatchGrid)
264{
265 if (newSwatchGrid == d_pointer->m_swatchGrid) {
266 return;
267 }
268
269 d_pointer->m_swatchGrid = newSwatchGrid;
270
271 d_pointer->selectSwatchFromCurrentColor();
272
273 Q_EMIT swatchGridChanged(newSwatchGrid);
274
275 // As of Qt documentation:
276 // “Notifies the layout system that this widget has changed and may
277 // need to change geometry.”
278 updateGeometry();
279
280 update();
281}
282
283/** @brief Selects a swatch from the book.
284 *
285 * @pre Both parameters are valid indexes within @ref m_swatchGrid.
286 * (Otherwise there will likely be a crash.)
287 *
288 * @param newCurrentColomn Index of the column.
289 *
290 * @param newCurrentRow Index of the row.
291 *
292 * @post If the given swatch is empty, nothing happens. Otherwise, it is
293 * selected and the selection mark is visible and @ref SwatchBook::currentColor
294 * has the value of this color. */
295void SwatchBookPrivate::selectSwatch(QListSizeType newCurrentColomn, QListSizeType newCurrentRow)
296{
297 const auto newColor = m_swatchGrid.value(newCurrentColomn, newCurrentRow);
298 if (!newColor.isValid()) {
299 return;
300 }
301 m_selectedColumn = newCurrentColomn;
302 m_selectedRow = newCurrentRow;
303 if (newColor != m_currentColor) {
304 m_currentColor = newColor;
305 Q_EMIT q_pointer->currentColorChanged(newColor);
306 }
307 q_pointer->update();
308}
309
310/** @brief Selects a swatch from the grid.
311 *
312 * @post If the currently selected swatch corresponds to
313 * @ref SwatchBook::currentColor nothing happens. Otherwise, a swatch if
314 * selected if there is one that corresponds to @ref SwatchBook::currentColor,
315 * or none if there is no corresponding swatch. */
316void SwatchBookPrivate::selectSwatchFromCurrentColor()
317{
318 if (m_selectedColumn > 0 && m_selectedRow > 0) {
319 if (m_swatchGrid.value(m_selectedColumn, m_selectedRow) == m_currentColor) {
320 return;
321 }
322 }
323
324 bool colorFound = false;
325 const qsizetype myColumnCount = m_swatchGrid.iCount();
326 const qsizetype myRowCount = m_swatchGrid.jCount();
327 int columnIndex = 0;
328 int rowIndex = 0;
329 for (columnIndex = 0; //
330 columnIndex < myColumnCount; //
331 ++columnIndex) {
332 for (rowIndex = 0; rowIndex < myRowCount; ++rowIndex) {
333 if (m_swatchGrid.value(columnIndex, rowIndex) == m_currentColor) {
334 colorFound = true;
335 break;
336 }
337 }
338 if (colorFound) {
339 break;
340 }
341 }
342 if (colorFound) {
343 m_selectedColumn = columnIndex;
344 m_selectedRow = rowIndex;
345 } else {
346 m_selectedColumn = -1;
347 m_selectedRow = -1;
348 }
349}
350
351/** @brief Horizontal spacing between color patches.
352 *
353 * @returns Horizontal spacing between color patches, measured in
354 * device-independent pixel. The value depends on the
355 * current <tt>QStyle</tt>.
356 *
357 * @sa @ref verticalPatchSpacing */
358int SwatchBookPrivate::horizontalPatchSpacing() const
359{
360 if (m_wideSpacing.testFlag(Qt::Orientation::Horizontal)) {
361 return widePatchSpacing();
362 }
363 return normalPatchSpacing();
364}
365
366/** @brief Value for a wide spacing between swatches.
367 *
368 * @returns Wide spacing between color patches, measured in
369 * device-independent pixel. The value depends on the
370 * current <tt>QStyle</tt>.
371 *
372 * @sa @ref normalPatchSpacing */
373int SwatchBookPrivate::widePatchSpacing() const
374{
375 // NOTE The value is derived from the current QStyle’s values for some
376 // horizontal spacings. This seems reasonable because some styles might
377 // have tighter metrics for vertical spacing, which might not look good
378 // here. The derived value is actually useful for both, horizontal and
379 // vertical metrics.
380
381 int temp = q_pointer->style()->pixelMetric( //
383 nullptr,
384 q_pointer.toPointerToConstObject());
385 if (temp <= 0) {
386 // Some styles like Qt’s build-in “Plastique” style or the external
387 // “QtCurve” style return 0 here. If so, we fall back to the left
388 // margin. (We do not use qMax() because this workaround should
389 // really only apply when the returned value is 0, because under
390 // normal circumstances, it might be intentional that the left
391 // margin is bigger than the horizontal spacing.)
392 temp = q_pointer->style()->pixelMetric( //
394 nullptr,
395 q_pointer.toPointerToConstObject());
396 }
397 // Another fallback (if also PM_LayoutLeftMargin fails):
398 if (temp <= 0) {
399 temp = q_pointer->style()->pixelMetric( //
401 nullptr,
402 q_pointer.toPointerToConstObject());
403 }
404 // A last-resort fallback:
405 return qMax(temp, 2);
406}
407
408/** @brief Vertical spacing between color patches.
409 *
410 * @returns Vertical spacing between color patches, measured in
411 * device-independent pixel. The value is typically smaller than
412 * @ref horizontalPatchSpacing(), to symbolize that the binding
413 * between patches is vertically stronger than horizontally. */
414int SwatchBookPrivate::verticalPatchSpacing() const
415{
416 if (m_wideSpacing.testFlag(Qt::Orientation::Vertical)) {
417 return widePatchSpacing();
418 }
419 return normalPatchSpacing();
420}
421
422/** @brief Normal spacing between color swatches.
423 *
424 * @returns Normal spacing between color patches, measured in
425 * device-independent pixel. The value is typically smaller than
426 * @ref widePatchSpacing(), to symbolize that the binding
427 * between patches is stronger. */
428int SwatchBookPrivate::normalPatchSpacing() const
429{
430 return qMax(widePatchSpacing() / 3, // ⅓ looks nice
431 1 // minimal useful value for a line visible as all scales
432 );
433}
434
435/** @brief Initializes a <tt>QStyleOptionFrame</tt> object for this widget
436 * in its current state.
437 *
438 * This function is provided analogous to many Qt widgets that also
439 * provide a function of that name with this purpose.
440 *
441 * @param option The object that will be initialized
442 *
443 * @note The value in QStyleOptionFrame::rect is not initialized. */
444void SwatchBookPrivate::initStyleOption(QStyleOptionFrame *option) const
445{
446 if (option == nullptr) {
447 return;
448 }
449 option->initFrom(q_pointer.toPointerToConstObject());
450 option->lineWidth = q_pointer->style()->pixelMetric( //
452 option,
453 q_pointer.toPointerToConstObject());
454 option->midLineWidth = 0;
455 option->state |= QStyle::State_Sunken;
456 // The following option is not set because this widget
457 // currently has no read-only mode:
458 // option->state |= QStyle::State_ReadOnly;
459 option->features = QStyleOptionFrame::None;
460}
461
462/** @brief Offset between top-left of the widget and top-left of the content.
463 *
464 * @param styleOptionFrame The options that will be passed to <tt>QStyle</tt>
465 * to calculate correctly the offset.
466 *
467 * @returns The pixel position of the top-left pixel of the content area
468 * which can be used for the color patches. */
469QPoint SwatchBookPrivate::offset(const QStyleOptionFrame &styleOptionFrame) const
470{
471 const QPoint innerMarginOffset = QPoint( //
472 q_pointer->style()->pixelMetric(QStyle::PM_LayoutLeftMargin),
473 q_pointer->style()->pixelMetric(QStyle::PM_LayoutTopMargin));
474
475 QStyleOptionFrame temp = styleOptionFrame; // safety copy
476 const QRectF frameContentRectangle = q_pointer->style()->subElementRect( //
478 &temp, // Risk of changes, therefore using the safety copy
479 q_pointer.toPointerToConstObject());
480 const QSizeF swatchbookContentSize = colorPatchesSizeWithMargin();
481
482 // Some styles, such as the Fusion style, regularly return a slightly
483 // larger rectangle through QStyle::subElementRect() than the one
484 // requested in SwatchBook::minimumSizeHint(), which we need to draw
485 // the color patches. In the case of the Kvantum style,
486 // QStyle::subElementRect().height() is even greater than
487 // SwatchBook::height(). It extends beyond the widget's own dimensions,
488 // both at the top and bottom. QStyle::subElementRect().y() is
489 // negative. Please see https://github.com/tsujan/Kvantum/issues/676
490 // for more information. To ensure a visually pleasing rendering, we
491 // implement centering within QStyle::subElementRect().
492 QPointF frameOffset = frameContentRectangle.center();
493 frameOffset.rx() -= swatchbookContentSize.width() / 2.;
494 frameOffset.ry() -= swatchbookContentSize.height() / 2.;
495
496 return (frameOffset + innerMarginOffset).toPoint();
497}
498
499/** @brief React on a mouse press event.
500 *
501 * Reimplemented from base class.
502 *
503 * @param event The corresponding mouse event */
504void SwatchBook::mousePressEvent(QMouseEvent *event)
505{
506 // NOTE We will not actively ignore the event, even if we didn’t actually
507 // react on it. Therefore, Breeze and other styles cannot move
508 // the window when clicking in the middle between two patches.
509 // This is intentional, because allowing it would be confusing:
510 // - The space between the patches is quite limited anyway, so
511 // it’s not worth the pain and could be surprising because somebody
512 // can click there by mistake.
513 // - We use the same background as QLineEdit, which at its turn also
514 // does not allow moving the window with a left-click within the
515 // field. We should be consistent with this behaviour.
516
517 const QSize myColorPatchSize = d_pointer->patchSizeOuter();
518 const int myPatchWidth = myColorPatchSize.width();
519 const int myPatchHeight = myColorPatchSize.height();
520 QStyleOptionFrame myFrameStyleOption;
521 d_pointer->initStyleOption(&myFrameStyleOption);
522 const QPoint temp = event->pos() - d_pointer->offset(myFrameStyleOption);
523
524 if ((temp.x() < 0) || (temp.y() < 0)) {
525 return; // Click position too much leftwards or upwards.
526 }
527
528 const auto columnWidth = myPatchWidth + d_pointer->horizontalPatchSpacing();
529 const int xWithinPatch = temp.x() % columnWidth;
530 if (xWithinPatch >= myPatchWidth) {
531 return; // Click position horizontally between two patch columns
532 }
533
534 const auto rowHeight = myPatchHeight + d_pointer->verticalPatchSpacing();
535 const int yWithinPatch = temp.y() % rowHeight;
536 if (yWithinPatch >= myPatchHeight) {
537 return; // Click position vertically between two patch rows
538 }
539
540 const int rowIndex = temp.y() / rowHeight;
541 if (!isInRange<qsizetype>(0, rowIndex, d_pointer->m_swatchGrid.jCount() - 1)) {
542 // The index is out of range. This might happen when the user
543 // clicks very near to the border, where is no other patch
544 // anymore, but which is still part of the widget.
545 return;
546 }
547
548 const int visualColumnIndex = temp.x() / columnWidth;
549 QListSizeType columnIndex;
550 if (layoutDirection() == Qt::LayoutDirection::LeftToRight) {
551 columnIndex = visualColumnIndex;
552 } else {
553 columnIndex = //
554 d_pointer->m_swatchGrid.iCount() - 1 - visualColumnIndex;
555 }
556 if (!isInRange<qsizetype>(0, columnIndex, d_pointer->m_swatchGrid.iCount() - 1)) {
557 // The index is out of range. This might happen when the user
558 // clicks very near to the border, where is no other patch
559 // anymore, but which is still part of the widget.
560 return;
561 }
562
563 // If we reached here, the click must have been within a patch
564 // and we have valid indexes.
565 d_pointer->selectSwatch(columnIndex, rowIndex);
566}
567
568/** @brief The size of the color patches.
569 *
570 * This is the bounding box around the outer limit.
571 *
572 * @returns The size of the color patches, measured in device-independent
573 * pixel.
574 *
575 * @sa @ref patchSizeInner */
576QSize SwatchBookPrivate::patchSizeOuter() const
577{
578 q_pointer->ensurePolished();
579 const QSize myInnerSize = patchSizeInner();
580 QStyleOptionToolButton myOptions;
581 myOptions.initFrom(q_pointer.toPointerToConstObject());
582 myOptions.rect.setSize(myInnerSize);
583 const auto myStyledOuterSize = q_pointer->style()->sizeFromContents( //
585 &myOptions,
586 myInnerSize,
587 q_pointer.toPointerToConstObject());
588 // Ensure that the difference between patchInnerSize and patchOuterSize is
589 // large enough to accommodate twice the cornerRadius. This accounts for
590 // one cornerRadius on each border of the rectangle.
591 const int extra = 2 * cornerRadius();
592 return myStyledOuterSize.expandedTo(myInnerSize + QSize(extra, extra));
593}
594
595/** @brief Size of the inner space of a color patch.
596 *
597 * This is typically smaller than @ref patchSizeOuter.
598 *
599 * @returns Size of the inner space of a color patch, measured in
600 * device-independent pixel. */
601QSize SwatchBookPrivate::patchSizeInner() const
602{
603 const int metric = q_pointer->style()->pixelMetric( //
605 nullptr,
606 q_pointer.toPointerToConstObject());
607 const int size = std::max({metric, //
608 horizontalPatchSpacing(), //
609 verticalPatchSpacing()});
610 return QSize(size, size);
611}
612
613/** @brief Corner radius for drawing rounded color patch rectangles
614 *
615 * Tries to guess a radius that matches well with the current QStyle.
616 *
617 * @returns Corner radius for drawing rounded color patch rectangles.
618 * Guarantied to be ≥ 0. */
619int SwatchBookPrivate::cornerRadius() const
620{
621 const auto defaultFrameWidth = //
622 q_pointer->style()->pixelMetric(QStyle::PM_DefaultFrameWidth);
623 return qMax(defaultFrameWidth, 0);
624}
625
626/** @brief Paint the widget.
627 *
628 * Reimplemented from base class.
629 *
630 * @param event the paint event */
631void SwatchBook::paintEvent(QPaintEvent *event)
632{
633 Q_UNUSED(event)
634
635 // Initialization
636 QPainter widgetPainter(this);
637 widgetPainter.setRenderHint(QPainter::Antialiasing);
638 QStyleOptionFrame frameStyleOption;
639 d_pointer->initStyleOption(&frameStyleOption);
640 const int horizontalSpacing = d_pointer->horizontalPatchSpacing();
641 const int verticalSpacing = d_pointer->verticalPatchSpacing();
642 const QSize patchSizeOuter = d_pointer->patchSizeOuter();
643 const int patchWidthOuter = patchSizeOuter.width();
644 const int patchHeightOuter = patchSizeOuter.height();
645
646 // Draw the background
647 {
648 // We draw the frame slightly shrunk on windowsvista style. Otherwise,
649 // when the windowsvista style is used on 125% scale factor and with
650 // a multi-monitor setup, the frame would sometimes not render on some
651 // of the borders on some of the screens.
652 const bool vistaStyle = QString::compare( //
653 QApplication::style()->objectName(),
654 QStringLiteral("windowsvista"),
656 == 0;
657 const int shrink = vistaStyle ? 1 : 0;
658 const QMargins margins(shrink, shrink, shrink, shrink);
659 auto shrunkFrameStyleOption = frameStyleOption;
660 shrunkFrameStyleOption.rect = frameStyleOption.rect - margins;
661 // NOTE On ukui style, drawing this primitive results in strange
662 // rendering on mouse hover. Actual behaviour: The whole panel
663 // background is drawn blue. Expected behaviour: Only the frame is
664 // drawn blue (as ukui actually does when rendering a QLineEdit).
665 // Playing around with PE_FrameLineEdit instead of or additional to
666 // PE_PanelLineEdit did not give better results either.
667 // As ukui has many, many graphical glitches and bugs (up to
668 // crashed unfixed for years), we assume that this is a problem of
669 // ukui, and not of our code. Furthermore, while the behavior is
670 // unexpected, the rendering doesn’t look completely terrible; we
671 // can live with that.
672 style()->drawPrimitive(QStyle::PE_PanelLineEdit, //
673 &shrunkFrameStyleOption,
674 &widgetPainter,
675 this);
676 }
677
678 // Draw the color patches
679 const QPoint offset = d_pointer->offset(frameStyleOption);
680 const QListSizeType columnCount = d_pointer->m_swatchGrid.iCount();
681 const int myCornerRadius = d_pointer->cornerRadius();
682 QListSizeType visualColumn;
683 for (int columnIndex = 0; columnIndex < columnCount; ++columnIndex) {
684 for (int row = 0; //
685 row < d_pointer->m_swatchGrid.jCount(); //
686 ++row //
687 ) {
688 const auto swatchColor = //
689 d_pointer->m_swatchGrid.value(columnIndex, row);
690 if (swatchColor.isValid()) {
691 widgetPainter.setBrush(swatchColor);
692 widgetPainter.setPen(Qt::NoPen);
693 if (layoutDirection() == Qt::LayoutDirection::LeftToRight) {
694 visualColumn = columnIndex;
695 } else {
696 visualColumn = columnCount - 1 - columnIndex;
697 }
698 widgetPainter.drawRoundedRect( //
699 offset.x() //
700 + (static_cast<int>(visualColumn) //
701 * (patchWidthOuter + horizontalSpacing)),
702 offset.y() //
703 + row * (patchHeightOuter + verticalSpacing),
704 patchWidthOuter,
705 patchHeightOuter,
706 myCornerRadius,
707 myCornerRadius);
708 }
709 }
710 }
711
712 // If there is no selection mark to draw, nothing more to do: Return!
713 if (d_pointer->m_selectedColumn < 0 || d_pointer->m_selectedRow < 0) {
714 return;
715 }
716
717 // Draw the selection mark (if any)
718 const QListSizeType visualSelectedColumnIndex = //
719 (layoutDirection() == Qt::LayoutDirection::LeftToRight) //
720 ? d_pointer->m_selectedColumn //
721 : d_pointer->m_swatchGrid.iCount() - 1 - d_pointer->m_selectedColumn;
722 const auto colorCielchD50 = //
723 d_pointer->m_rgbColorSpace->toCielchD50( //
724 d_pointer //
725 ->m_swatchGrid //
726 .value(d_pointer->m_selectedColumn, d_pointer->m_selectedRow) //
727 .rgba64() //
728 );
729 const QColor selectionMarkColor = //
730 handleColorFromBackgroundLightness(colorCielchD50.first);
731 const QPointF selectedPatchOffset = QPointF( //
732 offset.x() //
733 + (static_cast<int>(visualSelectedColumnIndex) //
734 * (patchWidthOuter + horizontalSpacing)), //
735 offset.y() //
736 + (static_cast<int>(d_pointer->m_selectedRow) //
737 * (patchHeightOuter + verticalSpacing)));
738 const QSize patchSizeInner = d_pointer->patchSizeInner();
739 const int patchWidthInner = patchSizeInner.width();
740 const int patchHeightInner = patchSizeInner.height();
741 if (d_pointer->m_selectionMark.isEmpty()) {
742 // If no selection mark is available for the current translation in
743 // the current font, we will draw a hard-coded fallback mark.
744 const QSize sizeDifference = patchSizeOuter - patchSizeInner;
745 // Offset of the selection mark to the border of the patch:
746 QPointF selectionMarkOffset = QPointF( //
747 sizeDifference.width() / 2.0,
748 sizeDifference.height() / 2.0);
749 if (patchWidthInner > patchHeightInner) {
750 selectionMarkOffset.rx() += //
751 ((patchWidthInner - patchHeightInner) / 2.0);
752 }
753 if (patchHeightInner > patchWidthInner) {
754 selectionMarkOffset.ry() += //
755 ((patchHeightInner - patchWidthInner) / 2.0);
756 }
757 const int effectiveSquareSize = qMin( //
758 patchHeightInner,
759 patchWidthInner);
760 qreal penWidth = effectiveSquareSize * 0.08;
761 QPen pen;
762 pen.setColor(selectionMarkColor);
763 pen.setCapStyle(Qt::PenCapStyle::RoundCap);
764 pen.setWidthF(penWidth);
765 widgetPainter.setPen(pen);
766 QPointF point1 = QPointF(penWidth, //
767 0.7 * effectiveSquareSize);
768 point1 += selectedPatchOffset + selectionMarkOffset;
769 QPointF point2(0.35 * effectiveSquareSize, //
770 1 * effectiveSquareSize - penWidth);
771 point2 += selectedPatchOffset + selectionMarkOffset;
772 QPointF point3(1 * effectiveSquareSize - penWidth, //
773 penWidth);
774 point3 += selectedPatchOffset + selectionMarkOffset;
775 widgetPainter.drawLine(QLineF(point1, point2));
776 widgetPainter.drawLine(QLineF(point2, point3));
777 } else {
778 QPainterPath textPath;
779 // Render the selection mark string in the path
780 textPath.addText(0, 0, font(), d_pointer->m_selectionMark);
781 // Align the path top-left to the path’s virtual coordinate system
782 textPath.translate(textPath.boundingRect().x() * (-1), //
783 textPath.boundingRect().y() * (-1));
784 // QPainterPath::boundingRect() might be slow. Cache the result:
785 const QSizeF boundingRectangleSize = textPath.boundingRect().size();
786
787 if (!boundingRectangleSize.isEmpty()) { // Prevent division by 0
788 QTransform textTransform;
789
790 // Offset for the current patch
791 textTransform.translate(
792 // x:
793 selectedPatchOffset.x() //
794 + (patchWidthOuter - patchWidthInner) / 2,
795 // y:
796 selectedPatchOffset.y() //
797 + (patchHeightOuter - patchHeightInner) / 2);
798
799 // Scale to maximum and center within the margins
800 const qreal scaleFactor = qMin(
801 // Horizontal scale factor:
802 patchWidthInner / boundingRectangleSize.width(),
803 // Vertical scale factor:
804 patchHeightInner / boundingRectangleSize.height());
805 QSizeF scaledSelectionMarkSize = //
806 boundingRectangleSize * scaleFactor;
807 const QSizeF temp = //
808 (patchSizeInner - scaledSelectionMarkSize) / 2;
809 textTransform.translate(temp.width(), temp.height());
810 textTransform.scale(scaleFactor, scaleFactor);
811
812 // Draw
813 widgetPainter.setTransform(textTransform);
814 widgetPainter.setPen(Qt::NoPen);
815 widgetPainter.setBrush(selectionMarkColor);
816 widgetPainter.drawPath(textPath);
817 }
818 }
819}
820
821/** @brief React on key press events.
822 *
823 * Reimplemented from base class.
824 *
825 * When the arrow keys are pressed, it moves the selection mark
826 * into the desired direction.
827 * When <tt>Qt::Key_PageUp</tt>, <tt>Qt::Key_PageDown</tt>,
828 * <tt>Qt::Key_Home</tt> or <tt>Qt::Key_End</tt> are pressed, it moves the
829 * handle a big step into the desired direction.
830 *
831 * Other key events are forwarded to the base class.
832 *
833 * @param event the event */
834void SwatchBook::keyPressEvent(QKeyEvent *event)
835{
836 QListSizeType columnShift = 0;
837 QListSizeType rowShift = 0;
838 const int writingDirection = //
839 (layoutDirection() == Qt::LeftToRight) //
840 ? 1 //
841 : -1;
842 switch (event->key()) {
843 case Qt::Key_Up:
844 rowShift = -1;
845 break;
846 case Qt::Key_Down:
847 rowShift = 1;
848 break;
849 case Qt::Key_Left:
850 columnShift = -1 * writingDirection;
851 break;
852 case Qt::Key_Right:
853 columnShift = 1 * writingDirection;
854 break;
855 case Qt::Key_PageUp:
856 rowShift = (-1) * d_pointer->m_swatchGrid.jCount();
857 break;
858 case Qt::Key_PageDown:
859 rowShift = d_pointer->m_swatchGrid.jCount();
860 break;
861 case Qt::Key_Home:
862 columnShift = (-1) * d_pointer->m_swatchGrid.iCount();
863 break;
864 case Qt::Key_End:
865 columnShift = d_pointer->m_swatchGrid.iCount();
866 break;
867 default:
868 // Quote from Qt documentation:
869 //
870 // “If you reimplement this handler, it is very important that
871 // you call the base class implementation if you do not act
872 // upon the key.
873 //
874 // The default implementation closes popup widgets if the
875 // user presses the key sequence for QKeySequence::Cancel
876 // (typically the Escape key). Otherwise the event is
877 // ignored, so that the widget’s parent can interpret it.“
879 return;
880 }
881 // Here we reach only if the key has been recognized. If not, in the
882 // default branch of the switch statement, we would have passed the
883 // keyPressEvent yet to the parent and returned.
884
885 // If currently no color of the swatch book is selected, select the
886 // first color as default.
887 if ((d_pointer->m_selectedColumn < 0) && (d_pointer->m_selectedRow < 0)) {
888 d_pointer->selectSwatch(0, 0);
889 return;
890 }
891
892 const int accelerationFactor = 2;
893 if (event->modifiers().testFlag(Qt::ControlModifier)) {
894 columnShift *= accelerationFactor;
895 rowShift *= accelerationFactor;
896 }
897
898 d_pointer->selectSwatch( //
899 qBound<QListSizeType>(0, //
900 d_pointer->m_selectedColumn + columnShift,
901 d_pointer->m_swatchGrid.iCount() - 1),
902 qBound<QListSizeType>(0, //
903 d_pointer->m_selectedRow + rowShift, //
904 d_pointer->m_swatchGrid.jCount() - 1));
905}
906
907/** @brief Handle state changes.
908 *
909 * Implements reaction on <tt>QEvent::LanguageChange</tt>.
910 *
911 * Reimplemented from base class.
912 *
913 * @param event The event. */
914void SwatchBook::changeEvent(QEvent *event)
915{
916 if (event->type() == QEvent::LanguageChange) {
917 // From QCoreApplication documentation:
918 // “Installing or removing a QTranslator, or changing an installed
919 // QTranslator generates a LanguageChange event for the
920 // QCoreApplication instance. A QApplication instance will
921 // propagate the event to all toplevel widgets […].
922 // Retranslate this widget itself:
923 d_pointer->retranslateUi();
924 }
925
927}
928
929/** @brief Size necessary to render the color patches, including a margin.
930 *
931 * @returns Size necessary to render the color patches, including a margin.
932 * Measured in device-independent pixels. */
933QSize SwatchBookPrivate::colorPatchesSizeWithMargin() const
934{
935 q_pointer->ensurePolished();
936 const QSize patchSize = patchSizeOuter();
937 const int columnCount = static_cast<int>(m_swatchGrid.iCount());
938 const int rowCount = static_cast<int>(m_swatchGrid.jCount());
939 const int width = //
940 q_pointer->style()->pixelMetric(QStyle::PM_LayoutLeftMargin) //
941 + columnCount * patchSize.width() //
942 + (columnCount - 1) * horizontalPatchSpacing() //
943 + q_pointer->style()->pixelMetric(QStyle::PM_LayoutRightMargin);
944 const int height = //
945 q_pointer->style()->pixelMetric(QStyle::PM_LayoutTopMargin) //
946 + rowCount * patchSize.height() //
947 + (rowCount - 1) * verticalPatchSpacing() //
948 + q_pointer->style()->pixelMetric(QStyle::PM_LayoutBottomMargin);
949 return QSize(width, height);
950}
951
952} // namespace PerceptualColor
AKONADI_CALENDAR_EXPORT KCalendarCore::Event::Ptr event(const Akonadi::Item &item)
void update(Part *part, const QByteArray &data, qint64 dataSize)
The namespace of this library.
Array2D< QColor > Swatches
Swatches organized in a grid.
Definition helper.h:242
QStyle * style()
float alphaF() const const
float blueF() const const
QColor fromRgbF(float r, float g, float b, float a)
float greenF() const const
bool isValid() const const
float redF() const const
Spec spec() const const
QCoreApplication * instance()
QString tr(const char *sourceText, const char *disambiguation, int n)
void addText(const QPointF &point, const QFont &font, const QString &text)
QRectF boundingRect() const const
void translate(const QPointF &offset)
void setCapStyle(Qt::PenCapStyle style)
void setColor(const QColor &color)
void setWidthF(qreal width)
int x() const const
int y() const const
qreal & rx()
qreal & ry()
QPointF center() const const
QSizeF size() const const
qreal x() const const
qreal y() const const
QSize expandedTo(const QSize &otherSize) const const
QSize grownBy(QMargins margins) const const
int height() const const
int width() const const
qreal height() const const
bool isEmpty() const const
qreal width() const const
int compare(QLatin1StringView s1, const QString &s2, Qt::CaseSensitivity cs)
QList< uint > toUcs4() const const
PM_LayoutHorizontalSpacing
PE_PanelLineEdit
SE_LineEditContents
void initFrom(const QWidget *widget)
CaseInsensitive
ControlModifier
LeftToRight
typedef Orientations
WA_Hover
QTransform & scale(qreal sx, qreal sy)
QTransform & translate(qreal dx, qreal dy)
virtual void changeEvent(QEvent *event)
virtual void keyPressEvent(QKeyEvent *event)
void update()
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Nov 22 2024 12:00:47 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.