QGIS API Documentation  3.20.0-Odense (decaadbb31)
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 
27 QgsColorSwatchGrid::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 
54  refreshColors();
55 }
56 
58 {
59  return QSize( mWidth, calculateHeight() );
60 }
61 
63 {
64  return QSize( mWidth, calculateHeight() );
65 }
66 
67 void QgsColorSwatchGrid::setContext( const QString &context )
68 {
69  mContext = context;
70  refreshColors();
71 }
72 
73 void QgsColorSwatchGrid::setBaseColor( const QColor &baseColor )
74 {
75  mBaseColor = baseColor;
76  refreshColors();
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 
89 void QgsColorSwatchGrid::paintEvent( QPaintEvent *event )
90 {
91  Q_UNUSED( event )
92  QPainter painter( this );
93  draw( painter );
94  painter.end();
95 }
96 
97 void QgsColorSwatchGrid::mouseMoveEvent( QMouseEvent *event )
98 {
99  //calculate box mouse cursor is over
100  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 
115 void QgsColorSwatchGrid::updateTooltip( const int colorIdx )
116 {
117  if ( colorIdx >= 0 && colorIdx < mColors.length() )
118  {
119  QColor color = mColors.at( colorIdx ).first;
120 
121  //if color has an associated name from the color scheme, use that
122  QString colorName = mColors.at( colorIdx ).second;
123 
124  // create very large preview swatch, because the grid itself has only tiny preview icons
125  int width = static_cast< int >( Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 23 );
126  int height = static_cast< int >( width / 1.61803398875 ); // golden ratio
127  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  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 
173 void 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 
184 void QgsColorSwatchGrid::mouseReleaseEvent( QMouseEvent *event )
185 {
186  if ( ! mPressedOnWidget )
187  {
188  return;
189  }
190 
191  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 
206 void QgsColorSwatchGrid::keyPressEvent( QKeyEvent *event )
207 {
208  //handle keyboard navigation
209  if ( event->key() == Qt::Key_Right )
210  {
211  mCurrentFocusBox = std::min( mCurrentFocusBox + 1, mColors.length() - 1 );
212  }
213  else if ( event->key() == Qt::Key_Left )
214  {
215  mCurrentFocusBox = std::max( mCurrentFocusBox - 1, 0 );
216  }
217  else if ( event->key() == Qt::Key_Up )
218  {
219  int currentRow = mCurrentFocusBox / NUMBER_COLORS_PER_ROW;
220  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  int currentColumn = mCurrentFocusBox % NUMBER_COLORS_PER_ROW;
237  currentRow++;
238  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 
265 void QgsColorSwatchGrid::focusInEvent( QFocusEvent *event )
266 {
267  Q_UNUSED( event )
268  mFocused = true;
269  repaint();
270 }
271 
272 void QgsColorSwatchGrid::focusOutEvent( QFocusEvent *event )
273 {
274  Q_UNUSED( event )
275  mFocused = false;
276  repaint();
277 }
278 
279 int QgsColorSwatchGrid::calculateHeight() const
280 {
281  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 
285 void QgsColorSwatchGrid::draw( QPainter &painter )
286 {
287  QPalette pal = QPalette( qApp->palette() );
288  QColor headerBgColor = pal.color( QPalette::Mid );
289  QColor headerTextColor = pal.color( QPalette::BrightText );
290  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  int row = index / NUMBER_COLORS_PER_ROW;
308  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  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 
362 QPixmap 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 
372 int QgsColorSwatchGrid::swatchForPosition( QPoint position ) const
373 {
374  //calculate box for position
375  int box = -1;
376  int column = ( position.x() - mSwatchMargin ) / ( mSwatchSize + mSwatchSpacing );
377  int xRem = ( position.x() - mSwatchMargin ) % ( mSwatchSize + mSwatchSpacing );
378  int row = ( position.y() - mSwatchMargin - mLabelHeight ) / ( mSwatchSize + mSwatchSpacing );
379  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 
395 QgsColorSwatchGridAction::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 
413 void 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 
428 void 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 
440 void QgsColorSwatchGridAction::setColor( const QColor &color )
441 {
442  emit colorChanged( color );
443  QAction::trigger();
444  if ( mMenu && mDismissOnColorSelection )
445  {
446  mMenu->hide();
447  }
448 }
449 
450 void 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:416
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
QgsNamedColorList * colors()
Gets the list of colors shown in the grid.
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
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