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