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