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