QGIS API Documentation  3.2.0-Bonn (bc43194)
qgsgradientstopeditor.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgsgradientstopeditor.cpp
3  -------------------------
4  begin : April 2016
5  copyright : (C) 2016 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 "qgsgradientstopeditor.h"
17 #include "qgsapplication.h"
18 #include "qgssymbollayerutils.h"
19 
20 #include <QPainter>
21 #include <QStyleOptionFrameV3>
22 #include <QMouseEvent>
23 
24 #define MARKER_WIDTH 11
25 #define MARKER_HEIGHT 14
26 #define MARKER_GAP 1.5
27 #define MARGIN_BOTTOM ( MARKER_HEIGHT + 2 )
28 #define MARGIN_X ( MARKER_WIDTH / 2 )
29 #define FRAME_MARGIN 2
30 #define CLICK_THRESHOLD ( MARKER_WIDTH / 2 + 3 )
31 
33  : QWidget( parent )
34 {
35  if ( ramp )
36  mGradient = *ramp;
37  mStops = mGradient.stops();
38 
39  if ( sOuterTriangle.isEmpty() )
40  {
41  sOuterTriangle << QPointF( 0, MARKER_HEIGHT ) << QPointF( MARKER_WIDTH, MARKER_HEIGHT )
42  << QPointF( MARKER_WIDTH, MARKER_WIDTH / 2.0 )
43  << QPointF( MARKER_WIDTH / 2.0, 0 )
44  << QPointF( 0, MARKER_WIDTH / 2.0 )
45  << QPointF( 0, MARKER_HEIGHT );
46  }
47  if ( sInnerTriangle.isEmpty() )
48  {
49  sInnerTriangle << QPointF( MARKER_GAP, MARKER_HEIGHT - MARKER_GAP ) << QPointF( MARKER_WIDTH - MARKER_GAP, MARKER_HEIGHT - MARKER_GAP )
50  << QPointF( MARKER_WIDTH - MARKER_GAP, MARKER_WIDTH / 2.0 + 1 )
51  << QPointF( MARKER_WIDTH / 2.0, MARKER_GAP )
52  << QPointF( MARKER_GAP, MARKER_WIDTH / 2.0 + 1 )
53  << QPointF( MARKER_GAP, MARKER_HEIGHT - MARKER_GAP );
54  }
55 
56  setFocusPolicy( Qt::StrongFocus );
57  setAcceptDrops( true );
58 }
59 
61 {
62  mGradient = ramp;
63  mStops = mGradient.stops();
64  mSelectedStop = 0;
65  update();
66  emit changed();
67 }
68 
70 {
71  //horizontal
72  return QSize( 200, 80 );
73 }
74 
75 void QgsGradientStopEditor::paintEvent( QPaintEvent *event )
76 {
77  Q_UNUSED( event );
78  QPainter painter( this );
79 
80  QRect frameRect( rect().x() + MARGIN_X, rect().y(),
81  rect().width() - 2 * MARGIN_X,
82  rect().height() - MARGIN_BOTTOM );
83 
84  //draw frame
85  QStyleOptionFrame option;
86  option.initFrom( this );
87  option.state = hasFocus() ? QStyle::State_KeyboardFocusChange : QStyle::State_None;
88  option.rect = frameRect;
89  style()->drawPrimitive( QStyle::PE_Frame, &option, &painter );
90 
91  if ( hasFocus() )
92  {
93  //draw focus rect
94  QStyleOptionFocusRect option;
95  option.initFrom( this );
96  option.state = QStyle::State_KeyboardFocusChange;
97  option.rect = frameRect;
98  style()->drawPrimitive( QStyle::PE_FrameFocusRect, &option, &painter );
99  }
100 
101  //start with the checkboard pattern
102  QBrush checkBrush = QBrush( transparentBackground() );
103  painter.setBrush( checkBrush );
104  painter.setPen( Qt::NoPen );
105 
106  QRect box( frameRect.x() + FRAME_MARGIN, frameRect.y() + FRAME_MARGIN,
107  frameRect.width() - 2 * FRAME_MARGIN,
108  frameRect.height() - 2 * FRAME_MARGIN );
109 
110  painter.drawRect( box );
111 
112  // draw gradient preview on top of checkerboard
113  for ( int i = 0; i < box.width() + 1; ++i )
114  {
115  QPen pen( mGradient.color( static_cast< double >( i ) / box.width() ) );
116  painter.setPen( pen );
117  painter.drawLine( box.left() + i, box.top(), box.left() + i, box.height() + 1 );
118  }
119 
120  // draw stop markers
121  int markerTop = frameRect.bottom() + 1;
122  drawStopMarker( painter, QPoint( box.left(), markerTop ), mGradient.color1(), mSelectedStop == 0 );
123  drawStopMarker( painter, QPoint( box.right(), markerTop ), mGradient.color2(), mSelectedStop == mGradient.count() - 1 );
124  int i = 1;
125  Q_FOREACH ( const QgsGradientStop &stop, mStops )
126  {
127  int x = stop.offset * box.width() + box.left();
128  drawStopMarker( painter, QPoint( x, markerTop ), stop.color, mSelectedStop == i );
129  ++i;
130  }
131 
132  painter.end();
133 }
134 
136 {
137  if ( index > 0 && index < mGradient.count() - 1 )
138  {
139  // need to map original stop index across to cached, possibly out of order stop index
140  QgsGradientStop selectedStop = mGradient.stops().at( index - 1 );
141  index = 1;
142  Q_FOREACH ( const QgsGradientStop &stop, mStops )
143  {
144  if ( stop == selectedStop )
145  {
146  break;
147  }
148  index++;
149  }
150  }
151 
152  mSelectedStop = index;
154  update();
155 }
156 
158 {
159  if ( mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1 )
160  {
161  return mStops.at( mSelectedStop - 1 );
162  }
163  else if ( mSelectedStop == 0 )
164  {
165  return QgsGradientStop( 0.0, mGradient.color1() );
166  }
167  else
168  {
169  return QgsGradientStop( 1.0, mGradient.color2() );
170  }
171 }
172 
174 {
175  if ( mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1 )
176  {
177  mStops[ mSelectedStop - 1 ].color = color;
178  mGradient.setStops( mStops );
179  }
180  else if ( mSelectedStop == 0 )
181  {
182  mGradient.setColor1( color );
183  }
184  else
185  {
186  mGradient.setColor2( color );
187  }
188  update();
189  emit changed();
190 }
191 
193 {
194  if ( mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1 )
195  {
196  mStops[ mSelectedStop - 1 ].offset = offset;
197  mGradient.setStops( mStops );
198  update();
199  emit changed();
200  }
201 }
202 
203 void QgsGradientStopEditor::setSelectedStopDetails( const QColor &color, double offset )
204 {
205  if ( mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1 )
206  {
207  mStops[ mSelectedStop - 1 ].color = color;
208  mStops[ mSelectedStop - 1 ].offset = offset;
209  mGradient.setStops( mStops );
210  }
211  else if ( mSelectedStop == 0 )
212  {
213  mGradient.setColor1( color );
214  }
215  else
216  {
217  mGradient.setColor2( color );
218  }
219 
220  update();
221  emit changed();
222 }
223 
225 {
226  if ( selectedStopIsMovable() )
227  {
228  //delete stop
229  double stopOffset = mStops.at( mSelectedStop - 1 ).offset;
230  mStops.removeAt( mSelectedStop - 1 );
231  mGradient.setStops( mStops );
232 
233  int closest = findClosestStop( relativePositionToPoint( stopOffset ) );
234  if ( closest >= 0 )
235  selectStop( closest );
236  update();
237  emit changed();
238  }
239 }
240 
241 void QgsGradientStopEditor::setColor1( const QColor &color )
242 {
243  mGradient.setColor1( color );
244  update();
245  emit changed();
246 }
247 
248 void QgsGradientStopEditor::setColor2( const QColor &color )
249 {
250  mGradient.setColor2( color );
251  update();
252  emit changed();
253 }
254 
256 {
257  if ( e->buttons() & Qt::LeftButton )
258  {
259  if ( selectedStopIsMovable() )
260  {
261  double offset = pointToRelativePosition( e->pos().x() );
262 
263  // have to edit the temporary stop list, as setting stops on the gradient will reorder them
264  // and change which stop corresponds to the selected one;
265  mStops[ mSelectedStop - 1 ].offset = offset;
266 
267  mGradient.setStops( mStops );
268  update();
269  emit changed();
270  }
271  }
272  e->accept();
273 }
274 
275 int QgsGradientStopEditor::findClosestStop( int x, int threshold ) const
276 {
277  int closestStop = -1;
278  int closestDiff = std::numeric_limits<int>::max();
279  int currentDiff = std::numeric_limits<int>::max();
280 
281  // check for matching stops first, so that they take precedence
282  // otherwise it's impossible to select a stop which sits above the first/last stop, making
283  // it impossible to move or delete these
284  int i = 1;
285  Q_FOREACH ( const QgsGradientStop &stop, mGradient.stops() )
286  {
287  currentDiff = std::abs( relativePositionToPoint( stop.offset ) + 1 - x );
288  if ( ( threshold < 0 || currentDiff < threshold ) && currentDiff < closestDiff )
289  {
290  closestStop = i;
291  closestDiff = currentDiff;
292  }
293  i++;
294  }
295 
296  //first stop
297  currentDiff = std::abs( relativePositionToPoint( 0.0 ) + 1 - x );
298  if ( ( threshold < 0 || currentDiff < threshold ) && currentDiff < closestDiff )
299  {
300  closestStop = 0;
301  closestDiff = currentDiff;
302  }
303 
304  //last stop
305  currentDiff = std::abs( relativePositionToPoint( 1.0 ) + 1 - x );
306  if ( ( threshold < 0 || currentDiff < threshold ) && currentDiff < closestDiff )
307  {
308  closestStop = mGradient.count() - 1;
309  }
310 
311  return closestStop;
312 }
313 
315 {
316  if ( e->pos().y() >= rect().height() - MARGIN_BOTTOM - 1 )
317  {
318  // find closest point
319  int closestStop = findClosestStop( e->pos().x(), CLICK_THRESHOLD );
320  if ( closestStop >= 0 )
321  {
322  selectStop( closestStop );
323  }
324  update();
325  }
326  e->accept();
327 }
328 
330 {
331  if ( e->buttons() & Qt::LeftButton )
332  {
333  // add a new stop
334  double offset = pointToRelativePosition( e->pos().x() );
335  mStops << QgsGradientStop( offset, mGradient.color( offset ) );
336  mSelectedStop = mStops.length();
337  mGradient.setStops( mStops );
338  update();
339  emit changed();
340  }
341  e->accept();
342 }
343 
345 {
346  if ( ( e->key() == Qt::Key_Backspace || e->key() == Qt::Key_Delete ) )
347  {
349  e->accept();
350  return;
351  }
352  else if ( e->key() == Qt::Key_Left || e->key() == Qt::Key_Right )
353  {
354  if ( selectedStopIsMovable() )
355  {
356  // calculate offset corresponding to 1 px
357  double offsetDiff = pointToRelativePosition( rect().x() + MARGIN_X + FRAME_MARGIN + 2 ) - pointToRelativePosition( rect().x() + MARGIN_X + FRAME_MARGIN + 1 );
358 
359  if ( e->modifiers() & Qt::ShiftModifier )
360  offsetDiff *= 10.0;
361 
362  if ( e->key() == Qt::Key_Left )
363  offsetDiff *= -1;
364 
365  mStops[ mSelectedStop - 1 ].offset = qBound( 0.0, mStops[ mSelectedStop - 1 ].offset + offsetDiff, 1.0 );
366  mGradient.setStops( mStops );
367  update();
368  e->accept();
369  emit changed();
370  return;
371  }
372  }
373 
374  QWidget::keyPressEvent( e );
375 }
376 
377 QPixmap QgsGradientStopEditor::transparentBackground()
378 {
379  static QPixmap sTranspBkgrd;
380 
381  if ( sTranspBkgrd.isNull() )
382  sTranspBkgrd = QgsApplication::getThemePixmap( QStringLiteral( "/transp-background_8x8.png" ) );
383 
384  return sTranspBkgrd;
385 }
386 
387 void QgsGradientStopEditor::drawStopMarker( QPainter &painter, QPoint topMiddle, const QColor &color, bool selected )
388 {
389  painter.save();
390  painter.setRenderHint( QPainter::Antialiasing );
391  painter.setBrush( selected ? QColor( 150, 150, 150 ) : Qt::white );
392  painter.setPen( selected ? Qt::black : QColor( 150, 150, 150 ) );
393  // 0.5 offsets to make edges pixel grid aligned
394  painter.translate( std::round( topMiddle.x() - MARKER_WIDTH / 2.0 ) + 0.5, topMiddle.y() + 0.5 );
395  painter.drawPolygon( sOuterTriangle );
396 
397  // draw the checkerboard background for marker
398  painter.setBrush( QBrush( transparentBackground() ) );
399  painter.setPen( Qt::NoPen );
400  painter.drawPolygon( sInnerTriangle );
401 
402  // draw color on top
403  painter.setBrush( color );
404  painter.drawPolygon( sInnerTriangle );
405  painter.restore();
406 }
407 
408 double QgsGradientStopEditor::pointToRelativePosition( int x ) const
409 {
410  int left = rect().x() + MARGIN_X + FRAME_MARGIN;
411  int right = left + rect().width() - 2 * MARGIN_X - 2 * FRAME_MARGIN;
412 
413  if ( x <= left )
414  return 0;
415  else if ( x >= right )
416  return 1.0;
417 
418  return static_cast< double >( x - left ) / ( right - left );
419 }
420 
421 int QgsGradientStopEditor::relativePositionToPoint( double position ) const
422 {
423  int left = rect().x() + MARGIN_X + FRAME_MARGIN;
424  int right = left + rect().width() - 2 * MARGIN_X - 2 * FRAME_MARGIN;
425 
426  if ( position <= 0 )
427  return left;
428  else if ( position >= 1.0 )
429  return right;
430 
431  return left + ( right - left ) * position;
432 }
433 
434 bool QgsGradientStopEditor::selectedStopIsMovable() const
435 {
436  // first and last stop can't be moved or deleted
437  return mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1;
438 }
439 
440 
441 void QgsGradientStopEditor::dragEnterEvent( QDragEnterEvent *e )
442 {
443  //is dragged data valid color data?
444  bool hasAlpha;
445  QColor mimeColor = QgsSymbolLayerUtils::colorFromMimeData( e->mimeData(), hasAlpha );
446 
447  if ( mimeColor.isValid() )
448  {
449  //if so, we accept the drag
450  e->acceptProposedAction();
451  }
452 }
453 
454 void QgsGradientStopEditor::dropEvent( QDropEvent *e )
455 {
456  //is dropped data valid color data?
457  bool hasAlpha = false;
458  QColor mimeColor = QgsSymbolLayerUtils::colorFromMimeData( e->mimeData(), hasAlpha );
459 
460  if ( mimeColor.isValid() )
461  {
462  //accept drop and set new color
463  e->acceptProposedAction();
464 
465  // add a new stop here
466  double offset = pointToRelativePosition( e->pos().x() );
467  mStops << QgsGradientStop( offset, mimeColor );
468  mSelectedStop = mStops.length();
469  mGradient.setStops( mStops );
470  update();
471  emit changed();
472  }
473 
474  //could not get color from mime data
475 }
476 
477 
QgsGradientStopEditor(QWidget *parent=nullptr, QgsGradientColorRamp *ramp=nullptr)
Constructor for QgsGradientStopEditor.
void setColor2(const QColor &color)
Sets the gradient end color.
Definition: qgscolorramp.h:195
Represents a color stop within a QgsGradientColorRamp color ramp.
Definition: qgscolorramp.h:101
void changed()
Emitted when the gradient ramp is changed by a user.
QColor color2() const
Returns the gradient end color.
Definition: qgscolorramp.h:179
void setSelectedStopDetails(const QColor &color, double offset)
Sets the color and offset for the current selected stop.
#define MARGIN_BOTTOM
void setColor1(const QColor &color)
Sets the color for the first stop.
#define MARKER_HEIGHT
static QPixmap getThemePixmap(const QString &name)
Helper to get a theme icon as a pixmap.
void deleteSelectedStop()
Deletes the current selected stop.
void mouseMoveEvent(QMouseEvent *event) override
void setGradientRamp(const QgsGradientColorRamp &ramp)
Sets the current ramp shown in the editor.
double offset
Relative positional offset, between 0 and 1.
Definition: qgscolorramp.h:116
#define MARKER_GAP
void setStops(const QgsGradientStopsList &stops)
Sets the list of intermediate gradient stops for the ramp.
QgsGradientStopsList stops() const
Returns the list of intermediate gradient stops for the ramp.
Definition: qgscolorramp.h:235
void setColor2(const QColor &color)
Sets the color for the last stop.
#define MARKER_WIDTH
void dropEvent(QDropEvent *e) override
void selectedStopChanged(const QgsGradientStop &stop)
Emitted when the current selected stop changes.
void paintEvent(QPaintEvent *event) override
void setSelectedStopColor(const QColor &color)
Sets the color for the current selected stop.
QSize sizeHint() const override
#define CLICK_THRESHOLD
void mousePressEvent(QMouseEvent *event) override
void dragEnterEvent(QDragEnterEvent *e) override
QColor color(double value) const override
Returns the color corresponding to a specified value.
#define MARGIN_X
QgsGradientStop selectedStop() const
Returns details about the currently selected stop.
void mouseDoubleClickEvent(QMouseEvent *event) override
Gradient color ramp, which smoothly interpolates between two colors and also supports optional extra ...
Definition: qgscolorramp.h:139
QColor color1() const
Returns the gradient start color.
Definition: qgscolorramp.h:172
#define FRAME_MARGIN
void keyPressEvent(QKeyEvent *event) override
void setSelectedStopOffset(double offset)
Sets the offset for the current selected stop.
static QColor colorFromMimeData(const QMimeData *data, bool &hasAlpha)
Attempts to parse mime data as a color.
void setColor1(const QColor &color)
Sets the gradient start color.
Definition: qgscolorramp.h:187
QColor color
Gradient color at stop.
Definition: qgscolorramp.h:118
int count() const override
Returns number of defined colors, or -1 if undefined.
Definition: qgscolorramp.h:159
void selectStop(int index)
Sets the currently selected stop.