QGIS API Documentation 3.28.0-Firenze (ed3ad0430f)
qgscolorswatchgrid.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgscolorswatchgrid.cpp
3 ------------------
4 Date : July 2014
5 Copyright : (C) 2014 by Nyall Dawson
6 Email : nyall dot dawson at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
16#include "qgscolorswatchgrid.h"
17#include "qgsapplication.h"
18#include "qgssymbollayerutils.h"
19#include "qgslogger.h"
20#include <QPainter>
21#include <QMouseEvent>
22#include <QMenu>
23#include <QBuffer>
24
25#define NUMBER_COLORS_PER_ROW 10 //number of color swatches per row
26
27QgsColorSwatchGrid::QgsColorSwatchGrid( QgsColorScheme *scheme, const QString &context, QWidget *parent )
28 : QWidget( parent )
29 , mScheme( scheme )
30 , mContext( context )
31 , mDrawBoxDepressed( false )
32 , mCurrentHoverBox( -1 )
33 , mFocused( false )
34 , mCurrentFocusBox( 0 )
35 , mPressedOnWidget( false )
36{
37 //need to receive all mouse over events
38 setMouseTracking( true );
39
40 setFocusPolicy( Qt::StrongFocus );
41 setSizePolicy( QSizePolicy::Fixed, QSizePolicy::Fixed );
42
43 mLabelHeight = Qgis::UI_SCALE_FACTOR * fontMetrics().height();
44 mLabelMargin = Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( '.' );
45 mSwatchSize = Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 1.75;
46 mSwatchOutlineSize = std::max( fontMetrics().horizontalAdvance( '.' ) * 0.4, 1.0 );
47
48 mSwatchSpacing = mSwatchSize * 0.3;
49 mSwatchMargin = mLabelMargin;
50
51 //calculate widget width
52 mWidth = NUMBER_COLORS_PER_ROW * mSwatchSize + ( NUMBER_COLORS_PER_ROW - 1 ) * mSwatchSpacing + mSwatchMargin + mSwatchMargin;
53
55}
56
58{
59 return QSize( mWidth, calculateHeight() );
60}
61
63{
64 return QSize( mWidth, calculateHeight() );
65}
66
67void QgsColorSwatchGrid::setContext( const QString &context )
68{
69 mContext = context;
71}
72
73void QgsColorSwatchGrid::setBaseColor( const QColor &baseColor )
74{
75 mBaseColor = baseColor;
77}
78
80{
81 //get colors from scheme
82 mColors = mScheme->fetchColors( mContext, mBaseColor );
83
84 //have to update size of widget in case number of colors has changed
85 updateGeometry();
86 repaint();
87}
88
89void QgsColorSwatchGrid::paintEvent( QPaintEvent *event )
90{
91 Q_UNUSED( event )
92 QPainter painter( this );
93 draw( painter );
94 painter.end();
95}
96
97void QgsColorSwatchGrid::mouseMoveEvent( QMouseEvent *event )
98{
99 //calculate box mouse cursor is over
100 const int newBox = swatchForPosition( event->pos() );
101
102 mDrawBoxDepressed = event->buttons() & Qt::LeftButton;
103 if ( newBox != mCurrentHoverBox )
104 {
105 //only repaint if changes are required
106 mCurrentHoverBox = newBox;
107 repaint();
108
109 updateTooltip( newBox );
110 }
111
112 emit hovered();
113}
114
115void QgsColorSwatchGrid::updateTooltip( const int colorIdx )
116{
117 if ( colorIdx >= 0 && colorIdx < mColors.length() )
118 {
119 const QColor color = mColors.at( colorIdx ).first;
120
121 //if color has an associated name from the color scheme, use that
122 const QString colorName = mColors.at( colorIdx ).second;
123
124 // create very large preview swatch, because the grid itself has only tiny preview icons
125 const int width = static_cast< int >( Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 23 );
126 const int height = static_cast< int >( width / 1.61803398875 ); // golden ratio
127 const int margin = static_cast< int >( height * 0.1 );
128 QImage icon = QImage( width + 2 * margin, height + 2 * margin, QImage::Format_ARGB32 );
129 icon.fill( Qt::transparent );
130
131 QPainter p;
132 p.begin( &icon );
133
134 //start with checkboard pattern
135 const QBrush checkBrush = QBrush( transparentBackground() );
136 p.setPen( Qt::NoPen );
137 p.setBrush( checkBrush );
138 p.drawRect( margin, margin, width, height );
139
140 //draw color over pattern
141 p.setBrush( QBrush( mColors.at( colorIdx ).first ) );
142
143 //draw border
144 p.setPen( QColor( 197, 197, 197 ) );
145 p.drawRect( margin, margin, width, height );
146 p.end();
147
148 QByteArray data;
149 QBuffer buffer( &data );
150 icon.save( &buffer, "PNG", 100 );
151
152 QString info;
153 if ( !colorName.isEmpty() )
154 info += QStringLiteral( "<h3>%1</h3><p>" ).arg( colorName );
155
156 info += QStringLiteral( "<b>HEX</b> %1<br>"
157 "<b>RGB</b> %2<br>"
158 "<b>HSV</b> %3,%4,%5<p>" ).arg( color.name(),
160 .arg( color.hue() ).arg( color.saturation() ).arg( color.value() );
161 info += QStringLiteral( "<img src='data:image/png;base64, %0'>" ).arg( QString( data.toBase64() ) );
162
163 setToolTip( info );
164
165 }
166 else
167 {
168 //clear tooltip
169 setToolTip( QString() );
170 }
171}
172
173void QgsColorSwatchGrid::mousePressEvent( QMouseEvent *event )
174{
175 if ( !mDrawBoxDepressed && event->buttons() & Qt::LeftButton )
176 {
177 mCurrentHoverBox = swatchForPosition( event->pos() );
178 mDrawBoxDepressed = true;
179 repaint();
180 }
181 mPressedOnWidget = true;
182}
183
185{
186 if ( ! mPressedOnWidget )
187 {
188 return;
189 }
190
191 const int box = swatchForPosition( event->pos() );
192 if ( mDrawBoxDepressed && event->button() == Qt::LeftButton )
193 {
194 mCurrentHoverBox = box;
195 mDrawBoxDepressed = false;
196 repaint();
197 }
198
199 if ( box >= 0 && box < mColors.length() && event->button() == Qt::LeftButton )
200 {
201 //color clicked
202 emit colorChanged( mColors.at( box ).first );
203 }
204}
205
206void QgsColorSwatchGrid::keyPressEvent( QKeyEvent *event )
207{
208 //handle keyboard navigation
209 if ( event->key() == Qt::Key_Right )
210 {
211 mCurrentFocusBox = std::min< int >( mCurrentFocusBox + 1, mColors.length() - 1 );
212 }
213 else if ( event->key() == Qt::Key_Left )
214 {
215 mCurrentFocusBox = std::max< int >( mCurrentFocusBox - 1, 0 );
216 }
217 else if ( event->key() == Qt::Key_Up )
218 {
219 int currentRow = mCurrentFocusBox / NUMBER_COLORS_PER_ROW;
220 const int currentColumn = mCurrentFocusBox % NUMBER_COLORS_PER_ROW;
221 currentRow--;
222
223 if ( currentRow >= 0 )
224 {
225 mCurrentFocusBox = currentRow * NUMBER_COLORS_PER_ROW + currentColumn;
226 }
227 else
228 {
229 //moved above first row
230 focusPreviousChild();
231 }
232 }
233 else if ( event->key() == Qt::Key_Down )
234 {
235 int currentRow = mCurrentFocusBox / NUMBER_COLORS_PER_ROW;
236 const int currentColumn = mCurrentFocusBox % NUMBER_COLORS_PER_ROW;
237 currentRow++;
238 const int box = currentRow * NUMBER_COLORS_PER_ROW + currentColumn;
239
240 if ( box < mColors.length() )
241 {
242 mCurrentFocusBox = box;
243 }
244 else
245 {
246 //moved below first row
247 focusNextChild();
248 }
249 }
250 else if ( event->key() == Qt::Key_Enter || event->key() == Qt::Key_Space )
251 {
252 //color clicked
253 emit colorChanged( mColors.at( mCurrentFocusBox ).first );
254 }
255 else
256 {
257 //some other key, pass it on
258 QWidget::keyPressEvent( event );
259 return;
260 }
261
262 repaint();
263}
264
265void QgsColorSwatchGrid::focusInEvent( QFocusEvent *event )
266{
267 Q_UNUSED( event )
268 mFocused = true;
269 repaint();
270}
271
272void QgsColorSwatchGrid::focusOutEvent( QFocusEvent *event )
273{
274 Q_UNUSED( event )
275 mFocused = false;
276 repaint();
277}
278
279int QgsColorSwatchGrid::calculateHeight() const
280{
281 const int numberRows = std::ceil( static_cast<double>( mColors.length() ) / NUMBER_COLORS_PER_ROW );
282 return numberRows * ( mSwatchSize ) + ( numberRows - 1 ) * mSwatchSpacing + mSwatchMargin + mLabelHeight + 0.5 * mLabelMargin + mSwatchMargin;
283}
284
285void QgsColorSwatchGrid::draw( QPainter &painter )
286{
287 const QPalette pal = QPalette( qApp->palette() );
288 const QColor headerBgColor = pal.color( QPalette::Mid );
289 const QColor headerTextColor = pal.color( QPalette::BrightText );
290 const QColor highlight = pal.color( QPalette::Highlight );
291
292 //draw header background
293 painter.setBrush( headerBgColor );
294 painter.setPen( Qt::NoPen );
295 painter.drawRect( QRect( 0, 0, width(), mLabelHeight + 0.5 * mLabelMargin ) );
296
297 //draw header text
298 painter.setPen( headerTextColor );
299 painter.drawText( QRect( mLabelMargin, 0.25 * mLabelMargin, width() - 2 * mLabelMargin, mLabelHeight ),
300 Qt::AlignLeft | Qt::AlignVCenter, mScheme->schemeName() );
301
302 //draw color swatches
303 QgsNamedColorList::const_iterator colorIt = mColors.constBegin();
304 int index = 0;
305 for ( ; colorIt != mColors.constEnd(); ++colorIt )
306 {
307 const int row = index / NUMBER_COLORS_PER_ROW;
308 const int column = index % NUMBER_COLORS_PER_ROW;
309
310 QRect swatchRect = QRect( column * ( mSwatchSize + mSwatchSpacing ) + mSwatchMargin,
311 row * ( mSwatchSize + mSwatchSpacing ) + mSwatchMargin + mLabelHeight + 0.5 * mLabelMargin,
312 mSwatchSize, mSwatchSize );
313
314 if ( mCurrentHoverBox == index )
315 {
316 //hovered boxes are slightly larger
317 swatchRect.adjust( -1, -1, 1, 1 );
318 }
319
320 //start with checkboard pattern for semi-transparent colors
321 if ( ( *colorIt ).first.alpha() != 255 )
322 {
323 const QBrush checkBrush = QBrush( transparentBackground() );
324 painter.setPen( Qt::NoPen );
325 painter.setBrush( checkBrush );
326 painter.drawRect( swatchRect );
327 }
328
329 if ( mCurrentHoverBox == index )
330 {
331 if ( mDrawBoxDepressed )
332 {
333 painter.setPen( QPen( QColor( 100, 100, 100 ), mSwatchOutlineSize ) );
334 }
335 else
336 {
337 //hover color
338 painter.setPen( QPen( QColor( 220, 220, 220 ), mSwatchOutlineSize ) );
339 }
340 }
341 else if ( mFocused && index == mCurrentFocusBox )
342 {
343 painter.setPen( highlight );
344 }
345 else if ( ( *colorIt ).first.name() == mBaseColor.name() )
346 {
347 //currently active color
348 painter.setPen( QPen( QColor( 75, 75, 75 ), mSwatchOutlineSize ) );
349 }
350 else
351 {
352 painter.setPen( QPen( QColor( 197, 197, 197 ), mSwatchOutlineSize ) );
353 }
354
355 painter.setBrush( ( *colorIt ).first );
356 painter.drawRect( swatchRect );
357
358 index++;
359 }
360}
361
362QPixmap QgsColorSwatchGrid::transparentBackground()
363{
364 static QPixmap sTranspBkgrd;
365
366 if ( sTranspBkgrd.isNull() )
367 sTranspBkgrd = QgsApplication::getThemePixmap( QStringLiteral( "/transp-background_8x8.png" ) );
368
369 return sTranspBkgrd;
370}
371
372int QgsColorSwatchGrid::swatchForPosition( QPoint position ) const
373{
374 //calculate box for position
375 int box = -1;
376 const int column = ( position.x() - mSwatchMargin ) / ( mSwatchSize + mSwatchSpacing );
377 const int xRem = ( position.x() - mSwatchMargin ) % ( mSwatchSize + mSwatchSpacing );
378 const int row = ( position.y() - mSwatchMargin - mLabelHeight ) / ( mSwatchSize + mSwatchSpacing );
379 const int yRem = ( position.y() - mSwatchMargin - mLabelHeight ) % ( mSwatchSize + mSwatchSpacing );
380
381 if ( xRem <= mSwatchSize + 1 && yRem <= mSwatchSize + 1 && column < NUMBER_COLORS_PER_ROW )
382 {
383 //if pos is actually inside a valid box, calculate which box
384 box = column + row * NUMBER_COLORS_PER_ROW;
385 }
386 return box;
387}
388
389
390//
391// QgsColorGridAction
392//
393
394
395QgsColorSwatchGridAction::QgsColorSwatchGridAction( QgsColorScheme *scheme, QMenu *menu, const QString &context, QWidget *parent )
396 : QWidgetAction( parent )
397 , mMenu( menu )
398 , mSuppressRecurse( false )
399 , mDismissOnColorSelection( true )
400{
401 mColorSwatchGrid = new QgsColorSwatchGrid( scheme, context, parent );
402
403 setDefaultWidget( mColorSwatchGrid );
404 connect( mColorSwatchGrid, &QgsColorSwatchGrid::colorChanged, this, &QgsColorSwatchGridAction::setColor );
405
406 connect( this, &QAction::hovered, this, &QgsColorSwatchGridAction::onHover );
407 connect( mColorSwatchGrid, &QgsColorSwatchGrid::hovered, this, &QgsColorSwatchGridAction::onHover );
408
409 //hide the action if no colors to be shown
410 setVisible( !mColorSwatchGrid->colors()->isEmpty() );
411}
412
413void QgsColorSwatchGridAction::setBaseColor( const QColor &baseColor )
414{
415 mColorSwatchGrid->setBaseColor( baseColor );
416}
417
419{
420 return mColorSwatchGrid->baseColor();
421}
422
424{
425 return mColorSwatchGrid->context();
426}
427
428void QgsColorSwatchGridAction::setContext( const QString &context )
429{
430 mColorSwatchGrid->setContext( context );
431}
432
434{
435 mColorSwatchGrid->refreshColors();
436 //hide the action if no colors shown
437 setVisible( !mColorSwatchGrid->colors()->isEmpty() );
438}
439
440void QgsColorSwatchGridAction::setColor( const QColor &color )
441{
442 emit colorChanged( color );
443 QAction::trigger();
444 if ( mMenu && mDismissOnColorSelection )
445 {
446 mMenu->hide();
447 }
448}
449
450void QgsColorSwatchGridAction::onHover()
451{
452 //see https://bugreports.qt.io/browse/QTBUG-10427?focusedCommentId=185610&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-185610
453
454 if ( mSuppressRecurse )
455 {
456 return;
457 }
458
459 if ( mMenu )
460 {
461 mSuppressRecurse = true;
462 mMenu->setActiveAction( this );
463 mSuppressRecurse = false;
464 }
465}
static const double UI_SCALE_FACTOR
UI scaling factor.
Definition: qgis.h:2304
static QPixmap getThemePixmap(const QString &name, const QColor &foreColor=QColor(), const QColor &backColor=QColor(), int size=16)
Helper to get a theme icon as a pixmap.
Abstract base class for color schemes.
virtual QString schemeName() const =0
Gets the name for the color scheme.
virtual QgsNamedColorList fetchColors(const QString &context=QString(), const QColor &baseColor=QColor())=0
Gets a list of colors from the scheme.
void setBaseColor(const QColor &baseColor)
Sets the base color for the color grid.
QString context() const
Gets the current context for the color grid.
void colorChanged(const QColor &color)
Emitted when a color has been selected from the widget.
QgsColorSwatchGridAction(QgsColorScheme *scheme, QMenu *menu=nullptr, const QString &context=QString(), QWidget *parent=nullptr)
Construct a new color swatch grid action.
void refreshColors()
Reload colors from scheme and redraws the widget.
QColor baseColor() const
Gets the base color for the color grid.
void setContext(const QString &context)
Sets the current context for the color grid.
A grid of color swatches, which allows for user selection.
void mouseMoveEvent(QMouseEvent *event) override
void mousePressEvent(QMouseEvent *event) override
QColor baseColor() const
Gets the base color for the widget.
QgsColorSwatchGrid(QgsColorScheme *scheme, const QString &context=QString(), QWidget *parent=nullptr)
Construct a new color swatch grid.
void paintEvent(QPaintEvent *event) override
QgsNamedColorList * colors()
Gets the list of colors shown in the grid.
void colorChanged(const QColor &color)
Emitted when a color has been selected from the widget.
void setBaseColor(const QColor &baseColor)
Sets the base color for the widget.
QSize minimumSizeHint() const override
void mouseReleaseEvent(QMouseEvent *event) override
QSize sizeHint() const override
void refreshColors()
Reload colors from scheme and redraws the widget.
void focusInEvent(QFocusEvent *event) override
void keyPressEvent(QKeyEvent *event) override
void focusOutEvent(QFocusEvent *event) override
void hovered()
Emitted when mouse hovers over widget.
void setContext(const QString &context)
Sets the current context for the grid.
QString context() const
Gets the current context for the grid.
static QString encodeColor(const QColor &color)
#define NUMBER_COLORS_PER_ROW