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