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

KDE's Doxygen guidelines are available online.