Perceptual Color

colorpatch.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 "colorpatch.h"
7// Second, the private implementation.
8#include "colorpatch_p.h" // IWYU pragma: associated
9
10#include "constpropagatinguniquepointer.h"
11#include "helper.h"
12#include <algorithm>
13#include <qapplication.h>
14#include <qbrush.h>
15#include <qdrag.h>
16#include <qevent.h>
17#include <qfont.h>
18#include <qframe.h>
19#include <qimage.h>
20#include <qlabel.h>
21#include <qmath.h>
22#include <qmimedata.h>
23#include <qnamespace.h>
24#include <qpainter.h>
25#include <qpalette.h>
26#include <qpen.h>
27#include <qpixmap.h>
28#include <qpoint.h>
29#include <qrect.h>
30#include <qsizepolicy.h>
31#include <qstyle.h>
32#include <qstyleoption.h>
33#include <qvariant.h>
34class QWidget;
35
36namespace PerceptualColor
37{
38/** @brief Constructor
39 * @param parent The parent of the widget, if any */
41 : AbstractDiagram(parent)
42 , d_pointer(new ColorPatchPrivate(this))
43{
44 setAcceptDrops(true);
46 d_pointer->updatePixmap();
47}
48
49/** @brief Destructor */
51{
52}
53
54/** @brief Constructor
55 *
56 * @param backLink Pointer to the object from which <em>this</em> object
57 * is the private implementation. */
58ColorPatchPrivate::ColorPatchPrivate(ColorPatch *backLink)
59 : m_label(new QLabel(backLink))
60 , q_pointer(backLink)
61{
62 m_label->setFrameShape(QFrame::StyledPanel);
63 m_label->setFrameShadow(QFrame::Sunken);
64 m_label->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
65 m_label->setGeometry(0, 0, backLink->width(), backLink->height());
66 // The following alignment is mirrored by Qt on right-to-left layouts:
67 constexpr Qt::Alignment myAlignment{Qt::AlignLeading, Qt::AlignTop};
68 m_label->setAlignment(myAlignment);
69}
70
71/** @brief Provide the size hint.
72 *
73 * Reimplemented from base class.
74 *
75 * @returns the size hint
76 *
77 * @sa @ref minimumSizeHint() */
79{
80 return minimumSizeHint();
81}
82
83/** @brief Provide the minimum size hint.
84 *
85 * Reimplemented from base class.
86 *
87 * @returns the minimum size hint
88 *
89 * @sa @ref sizeHint() */
91{
92 // Use a size similar to a QToolButton with an icon (and without text)
95 option.initFrom(this);
96 option.font = font();
97 const int iconSize = style()->pixelMetric( //
99 nullptr,
100 this);
101 option.iconSize = QSize(iconSize, iconSize);
102 return style()->sizeFromContents( //
104 &option,
105 option.iconSize,
106 this);
107}
108
109/** @brief Updates the pixmap in @ref m_label and its alignment. */
110void ColorPatchPrivate::updatePixmap()
111{
112 const QRect qLabelContentsRect = m_label->contentsRect();
113 const QPixmap pixmap = renderPixmap(qLabelContentsRect.width(), //
114 qLabelContentsRect.height());
115 // NOTE Kvantum was mistakenly scaling the pixmap (even though
116 // QLabel::hasScaledContents() == false) for versions ≤ 1.0.2. This bug
117 // has been fixed: https://github.com/tsujan/Kvantum/issues/804.
118 m_label->setPixmap(pixmap);
119 // There were rendering artefacts under certain QStyle (Breeze, Plastik,
120 // Windows): When selecting in the color dialog a new color with the
121 // screen color picker using “Portal” under 125% scaling, the left and
122 // the top border of the QLabel show a thin line of the previous color.
123 // We can work around this by simply updating the whole widget:
124 m_label->update();
125}
126
127/** @brief Handle resize events.
128 *
129 * Reimplemented from base class.
130 *
131 * @param event The corresponding event */
133{
134 d_pointer->m_label->resize(event->size());
135
136 // NOTE It would be more efficient not to always update the pixmap,
137 // but only when either the height or the width of the new pixmap to
138 // be calculated are larger than those of the current pixmap available
139 // under d_pointer->updatePixmap(). After all, a pixmap that is too
140 // large does not disturb the drawing, while one that is too small does.
141 // Unfortunately, however, resizing QLabel (at least with high-DPI and
142 // RTL layout at the same time) causes the correct alignment (here
143 // Qt::AlignLeading and Qt::AlignTop) to be lost and the image to be
144 // shifted. This error can be worked around by actually each time a new
145 // pixmap is assigned, which is not identical to the old one:
146 d_pointer->updatePixmap();
147}
148
149// No documentation here (documentation of properties
150// and its getters are in the header)
152{
153 return d_pointer->m_color;
154}
155
156/** @brief Setter for the @ref color property.
157 * @param newColor the new color */
158void ColorPatch::setColor(const QColor &newColor)
159{
160 if (newColor != d_pointer->m_color) {
161 d_pointer->m_color = newColor;
162 d_pointer->updatePixmap();
163 Q_EMIT colorChanged(newColor);
164 }
165}
166
167/** @brief Renders the image to be displayed.
168 *
169 * @param width of the requested image, measured in device-independent pixels.
170 *
171 * @param height of the requested image, measured in device-independent pixels.
172 *
173 * @returns An image containing the color of @ref m_color. If the color is
174 * transparent or semi-transparent, background with small gray squares is
175 * visible. If @ref ColorPatch has RTL layout, the image is mirrored. The
176 * device-pixel-ratio is set accordingly to @ref ColorPatch. The size of
177 * the image is equal or (if rounding has to be done because of fractional
178 * scale factors) slightly bigger than necessary to paint the whole
179 * @ref ColorPatch surface at the given device-pixel-ratio. As @ref m_label
180 * does <em>not</em> scale the image by default, it will be displayed with
181 * the correct aspect ratio, while guaranteeing to be big enough whatever
182 * QLabel’s frame size is with the currently used QStyle. */
183QImage ColorPatchPrivate::renderImage(const int width, const int height)
184{
185 // Initialization
186 // Round up to the next integer to be sure to have a big-enough image:
187 const qreal imageWidthF = width * q_pointer->devicePixelRatioF();
188 const int imageWidth = qCeil(imageWidthF);
189 const qreal imageHeightF = height * q_pointer->devicePixelRatioF();
190 const int imageHeight = qCeil(imageHeightF);
191 QImage myImage(imageWidth, //
192 imageHeight, //
194 if ((imageWidth <= 0) || (imageHeight <= 0)) {
195 // Initializing a QPainter on an image of zero size would print
196 // errors. Therefore, returning immediately:
197 myImage.setDevicePixelRatio(q_pointer->devicePixelRatioF());
198 return QImage();
199 }
201 opt.initFrom(q_pointer); // Sets also QStyle::State_MouseOver if appropriate
202
203 // Draw content of an invalid color (and return)
204 if (!m_color.isValid()) {
205 const QPalette::ColorGroup myColorGroup = //
206 (q_pointer->isEnabled()) //
207 ? QPalette::ColorGroup::Normal //
208 : QPalette::ColorGroup::Disabled;
209 myImage.fill( //
211 // An alternative value might be:
212 // q_pointer->palette().color(myColorGroup, QPalette::Window)
213 // but this integrates less nice with styles like QtCurve who
214 // might have background decorations that cover all widgets.
215 // Ultimately, however, it is a matter of taste.
216 );
217 QPen pen( //
218 q_pointer->palette().color(myColorGroup, QPalette::WindowText));
219
220 const int defaultFrameWidth = qMax( //
221 q_pointer->style()->pixelMetric(QStyle::PM_DefaultFrameWidth, &opt),
222 1);
223 const auto lineWidthF = //
224 defaultFrameWidth * q_pointer->devicePixelRatioF();
225 pen.setWidthF(lineWidthF);
226 pen.setCapStyle(Qt::PenCapStyle::SquareCap);
227 {
228 QPainter painter{&myImage};
229 // Because Qt::PenCapStyle::SquareCap will extends beyond the line
230 // end by half the line width, we can use an offset and the line
231 // will still touch the corner pixels of the image. It is a good
232 // idea to do so, because on widgets with an extreme aspect ratio
233 // (for example width 400, height 40, which is a realistic value in
234 // ColorDialog), the lines seem to “shift out of the image”. Using
235 // an offset, it looks nicer. How big should the offset be? To keep
236 // it simple, we use the same offset for both, x and y. The
237 // distance from the offset point to the point where the line
238 // touches the border depends on the angle of the line. The worst
239 // case (that means, the biggest distance) is for 45°. With
240 // Pythagoras, we have, for the offset “a” (identical for x and y):
241 // a² + a² = (½ linewidth)²
242 // 2 a² = ¼ linewidth²
243 // a² = ⅛ linewidth²
244 // a = 1 ÷ (√8) linewidth
245 // a ≈ 0.35 linewidth (Rounding down to be safe)
246 const qreal offset = static_cast<qreal>(lineWidthF * 0.35);
247 const qreal &left = offset; // alias for “offset”
248 const qreal &top = offset; // alias for “offset”
249 const qreal bottom = imageHeightF - offset;
250 const qreal right = imageWidthF - offset;
251 painter.setPen(pen);
252 painter.setRenderHint(QPainter::Antialiasing, true);
253 painter.drawLine(QPointF(left, top), //
254 QPointF(right, bottom));
255 painter.drawLine(QPointF(left, bottom), //
256 QPointF(right, top));
257 }
258 myImage.setDevicePixelRatio(q_pointer->devicePixelRatioF());
259 return myImage;
260 }
261
262 // Draw content of a valid color
263 if (m_color.alphaF() < 1) {
264 // Prepare the image with (semi-)transparent color
265 // Background for colors that are not fully opaque
266 QImage tempBackground = transparencyBackground( //
267 q_pointer->devicePixelRatioF());
268 // Paint the color above
269 QPainter(&tempBackground).fillRect(tempBackground.rect(), m_color);
270 {
271 // Fill a given rectangle with tiles. (QBrush will ignore
272 // the devicePixelRatioF of the image of the tile.)
273 QPainter painter{&myImage};
275 painter.fillRect(myImage.rect(), QBrush(tempBackground));
276 }
277 if (q_pointer->layoutDirection() == Qt::RightToLeft) {
278 // Horizontally mirrored image for right-to-left layout,
279 // so that the “nice” part is the first you see in reading
280 // direction.
281 myImage = myImage.mirrored(true, // horizontally mirrored
282 false // vertically mirrored
283 );
284 }
285 } else {
286 // Prepare the image with plain color
287 myImage.fill(m_color);
288 }
289 myImage.setDevicePixelRatio(q_pointer->devicePixelRatioF());
290 return myImage;
291}
292
293/** @brief Renders the image to be displayed.
294 *
295 * @param width of the requested image, measured in logical pixels.
296 *
297 * @param height of the requested image, measured in logical pixels.
298 *
299 * @returns Same as @ref renderImage but as QPixmap. */
300QPixmap ColorPatchPrivate::renderPixmap(const int width, const int height)
301{
302 QPixmap pixmap = QPixmap::fromImage(renderImage(width, height));
303 pixmap.setDevicePixelRatio(q_pointer->devicePixelRatioF());
304 return pixmap;
305}
306
307/** @brief React on a mouse move event.
308 *
309 * Reimplemented from base class.
310 *
311 * @param event The corresponding mouse event */
313{
314 if (event->button() == Qt::LeftButton)
315 d_pointer->dragStartPosition = event->pos();
317}
318
319/** @brief React on a mouse press event.
320 *
321 * Reimplemented from base class.
322 *
323 * @param event The corresponding mouse event */
325{
326 if (event->buttons() & Qt::LeftButton) {
327 // Distance since the left mouse buttons was originally clicked.
328 const auto vector = event->pos() - d_pointer->dragStartPosition;
329 const auto distanceSquare = vector.x() * vector.x() //
330 + vector.y() * vector.y();
331 const auto refSquare = QApplication::startDragDistance() //
333 if (d_pointer->m_color.isValid() && (distanceSquare >= refSquare)) {
334 QDrag *drag = new QDrag(this); // Mandatory on heap and with parent
335 QMimeData *mimeData = new QMimeData;
336 mimeData->setColorData(d_pointer->m_color);
337 drag->setMimeData(mimeData); // Takes ownership of mime data
338 const auto finalSize = std::max({30, //
339 minimumSizeHint().width(), //
341 drag->setPixmap(d_pointer->renderPixmap(finalSize, finalSize));
342 drag->exec(Qt::CopyAction);
343 }
344 }
345 // NOTE Intentionally not calling the parent’s class’ implementation to
346 // avoid that on Breeze style, instead of drag-and-drop, sometimes
347 // the window gets moved.
348}
349
350/** @brief Accepts drag events for colors.
351 *
352 * Reimplemented from base class.
353 *
354 * @param event The corresponding event */
356{
357 if (event->mimeData()->hasColor()) {
358 const QColor colorToDrop = qvariant_cast<QColor>( //
359 event->mimeData()->colorData());
360 if (colorToDrop.isValid()) {
361 event->acceptProposedAction();
362 return;
363 }
364 }
365}
366
367/** @brief Accepts drag events for colors.
368 *
369 * Reimplemented from base class.
370 *
371 * @param event The corresponding event */
373{
374 if (event->mimeData()->hasColor()) {
375 const QColor colorToDrop = qvariant_cast<QColor>( //
376 event->mimeData()->colorData());
377 if (colorToDrop.isValid()) {
378 setColor(colorToDrop);
379 event->acceptProposedAction();
380 return;
381 }
382 }
383}
384
385} // namespace PerceptualColor
Base class for LCH diagrams.
A color display widget.
Definition colorpatch.h:75
virtual QSize minimumSizeHint() const override
Provide the minimum size hint.
virtual void mouseMoveEvent(QMouseEvent *event) override
React on a mouse press event.
virtual void mousePressEvent(QMouseEvent *event) override
React on a mouse move event.
virtual void dragEnterEvent(QDragEnterEvent *event) override
Accepts drag events for colors.
virtual void resizeEvent(QResizeEvent *event) override
Handle resize events.
void colorChanged(const QColor &color)
Notify signal for property color.
Q_INVOKABLE ColorPatch(QWidget *parent=nullptr)
Constructor.
virtual ~ColorPatch() noexcept override
Destructor.
void setColor(const QColor &newColor)
Setter for the color property.
virtual void dropEvent(QDropEvent *event) override
Accepts drag events for colors.
QColor color
The color that is displayed.
Definition colorpatch.h:95
virtual QSize sizeHint() const override
Provide the size hint.
The namespace of this library.
float alphaF() const const
bool isValid() const const
Qt::DropAction exec(Qt::DropActions supportedActions)
void setMimeData(QMimeData *data)
void setPixmap(const QPixmap &pixmap)
Format_ARGB32_Premultiplied
QRect rect() const const
void setPixmap(const QPixmap &)
void setColorData(const QVariant &color)
Q_EMITQ_EMIT
void fillRect(const QRect &rectangle, QGradient::Preset preset)
void setRenderHint(RenderHint hint, bool on)
QPixmap fromImage(QImage &&image, Qt::ImageConversionFlags flags)
void setDevicePixelRatio(qreal scaleFactor)
int x() const const
int height() const const
int width() const const
int height() const const
int width() const const
PM_ButtonIconSize
virtual int pixelMetric(PixelMetric metric, const QStyleOption *option, const QWidget *widget) const const=0
virtual QSize sizeFromContents(ContentsType type, const QStyleOption *option, const QSize &contentsSize, const QWidget *widget) const const=0
void initFrom(const QWidget *widget)
typedef Alignment
CopyAction
transparent
RightToLeft
LeftButton
QTextStream & left(QTextStream &stream)
QTextStream & right(QTextStream &stream)
void setAcceptDrops(bool on)
QRect contentsRect() const const
void ensurePolished() const const
virtual bool event(QEvent *event) override
virtual void mousePressEvent(QMouseEvent *event)
void resize(const QSize &)
void setSizePolicy(QSizePolicy)
QStyle * style() const const
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.