QGIS API Documentation  3.9.0-Master (d9ef585e47)
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  const auto constMStops = mStops;
126  for ( const QgsGradientStop &stop : constMStops )
127  {
128  int x = stop.offset * box.width() + box.left();
129  drawStopMarker( painter, QPoint( x, markerTop ), stop.color, mSelectedStop == i );
130  ++i;
131  }
132 
133  painter.end();
134 }
135 
137 {
138  if ( index > 0 && index < mGradient.count() - 1 )
139  {
140  // need to map original stop index across to cached, possibly out of order stop index
141  QgsGradientStop selectedStop = mGradient.stops().at( index - 1 );
142  index = 1;
143  const auto constMStops = mStops;
144  for ( const QgsGradientStop &stop : constMStops )
145  {
146  if ( stop == selectedStop )
147  {
148  break;
149  }
150  index++;
151  }
152  }
153 
154  mSelectedStop = index;
156  update();
157 }
158 
160 {
161  if ( mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1 )
162  {
163  return mStops.at( mSelectedStop - 1 );
164  }
165  else if ( mSelectedStop == 0 )
166  {
167  return QgsGradientStop( 0.0, mGradient.color1() );
168  }
169  else
170  {
171  return QgsGradientStop( 1.0, mGradient.color2() );
172  }
173 }
174 
176 {
177  if ( mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1 )
178  {
179  mStops[ mSelectedStop - 1 ].color = color;
180  mGradient.setStops( mStops );
181  }
182  else if ( mSelectedStop == 0 )
183  {
184  mGradient.setColor1( color );
185  }
186  else
187  {
188  mGradient.setColor2( color );
189  }
190  update();
191  emit changed();
192 }
193 
195 {
196  if ( mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1 )
197  {
198  mStops[ mSelectedStop - 1 ].offset = offset;
199  mGradient.setStops( mStops );
200  update();
201  emit changed();
202  }
203 }
204 
205 void QgsGradientStopEditor::setSelectedStopDetails( const QColor &color, double offset )
206 {
207  if ( mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1 )
208  {
209  mStops[ mSelectedStop - 1 ].color = color;
210  mStops[ mSelectedStop - 1 ].offset = offset;
211  mGradient.setStops( mStops );
212  }
213  else if ( mSelectedStop == 0 )
214  {
215  mGradient.setColor1( color );
216  }
217  else
218  {
219  mGradient.setColor2( color );
220  }
221 
222  update();
223  emit changed();
224 }
225 
227 {
228  if ( selectedStopIsMovable() )
229  {
230  //delete stop
231  double stopOffset = mStops.at( mSelectedStop - 1 ).offset;
232  mStops.removeAt( mSelectedStop - 1 );
233  mGradient.setStops( mStops );
234 
235  int closest = findClosestStop( relativePositionToPoint( stopOffset ) );
236  if ( closest >= 0 )
237  selectStop( closest );
238  update();
239  emit changed();
240  }
241 }
242 
243 void QgsGradientStopEditor::setColor1( const QColor &color )
244 {
245  mGradient.setColor1( color );
246  update();
247  emit changed();
248 }
249 
250 void QgsGradientStopEditor::setColor2( const QColor &color )
251 {
252  mGradient.setColor2( color );
253  update();
254  emit changed();
255 }
256 
258 {
259  if ( e->buttons() & Qt::LeftButton )
260  {
261  if ( selectedStopIsMovable() )
262  {
263  double offset = pointToRelativePosition( e->pos().x() );
264 
265  // have to edit the temporary stop list, as setting stops on the gradient will reorder them
266  // and change which stop corresponds to the selected one;
267  mStops[ mSelectedStop - 1 ].offset = offset;
268 
269  mGradient.setStops( mStops );
270  update();
271  emit changed();
272  }
273  }
274  e->accept();
275 }
276 
277 int QgsGradientStopEditor::findClosestStop( int x, int threshold ) const
278 {
279  int closestStop = -1;
280  int closestDiff = std::numeric_limits<int>::max();
281  int currentDiff = std::numeric_limits<int>::max();
282 
283  // check for matching stops first, so that they take precedence
284  // otherwise it's impossible to select a stop which sits above the first/last stop, making
285  // it impossible to move or delete these
286  int i = 1;
287  const auto constStops = mGradient.stops();
288  for ( const QgsGradientStop &stop : constStops )
289  {
290  currentDiff = std::abs( relativePositionToPoint( stop.offset ) + 1 - x );
291  if ( ( threshold < 0 || currentDiff < threshold ) && currentDiff < closestDiff )
292  {
293  closestStop = i;
294  closestDiff = currentDiff;
295  }
296  i++;
297  }
298 
299  //first stop
300  currentDiff = std::abs( relativePositionToPoint( 0.0 ) + 1 - x );
301  if ( ( threshold < 0 || currentDiff < threshold ) && currentDiff < closestDiff )
302  {
303  closestStop = 0;
304  closestDiff = currentDiff;
305  }
306 
307  //last stop
308  currentDiff = std::abs( relativePositionToPoint( 1.0 ) + 1 - x );
309  if ( ( threshold < 0 || currentDiff < threshold ) && currentDiff < closestDiff )
310  {
311  closestStop = mGradient.count() - 1;
312  }
313 
314  return closestStop;
315 }
316 
318 {
319  if ( e->pos().y() >= rect().height() - MARGIN_BOTTOM - 1 )
320  {
321  // find closest point
322  int closestStop = findClosestStop( e->pos().x(), CLICK_THRESHOLD );
323  if ( closestStop >= 0 )
324  {
325  selectStop( closestStop );
326  }
327  update();
328  }
329  e->accept();
330 }
331 
333 {
334  if ( e->buttons() & Qt::LeftButton )
335  {
336  // add a new stop
337  double offset = pointToRelativePosition( e->pos().x() );
338  mStops << QgsGradientStop( offset, mGradient.color( offset ) );
339  mSelectedStop = mStops.length();
340  mGradient.setStops( mStops );
341  update();
342  emit changed();
343  }
344  e->accept();
345 }
346 
348 {
349  if ( ( e->key() == Qt::Key_Backspace || e->key() == Qt::Key_Delete ) )
350  {
352  e->accept();
353  return;
354  }
355  else if ( e->key() == Qt::Key_Left || e->key() == Qt::Key_Right )
356  {
357  if ( selectedStopIsMovable() )
358  {
359  // calculate offset corresponding to 1 px
360  double offsetDiff = pointToRelativePosition( rect().x() + MARGIN_X + FRAME_MARGIN + 2 ) - pointToRelativePosition( rect().x() + MARGIN_X + FRAME_MARGIN + 1 );
361 
362  if ( e->modifiers() & Qt::ShiftModifier )
363  offsetDiff *= 10.0;
364 
365  if ( e->key() == Qt::Key_Left )
366  offsetDiff *= -1;
367 
368  mStops[ mSelectedStop - 1 ].offset = qBound( 0.0, mStops[ mSelectedStop - 1 ].offset + offsetDiff, 1.0 );
369  mGradient.setStops( mStops );
370  update();
371  e->accept();
372  emit changed();
373  return;
374  }
375  }
376 
377  QWidget::keyPressEvent( e );
378 }
379 
380 QPixmap QgsGradientStopEditor::transparentBackground()
381 {
382  static QPixmap sTranspBkgrd;
383 
384  if ( sTranspBkgrd.isNull() )
385  sTranspBkgrd = QgsApplication::getThemePixmap( QStringLiteral( "/transp-background_8x8.png" ) );
386 
387  return sTranspBkgrd;
388 }
389 
390 void QgsGradientStopEditor::drawStopMarker( QPainter &painter, QPoint topMiddle, const QColor &color, bool selected )
391 {
392  painter.save();
393  painter.setRenderHint( QPainter::Antialiasing );
394  painter.setBrush( selected ? QColor( 150, 150, 150 ) : Qt::white );
395  painter.setPen( selected ? Qt::black : QColor( 150, 150, 150 ) );
396  // 0.5 offsets to make edges pixel grid aligned
397  painter.translate( std::round( topMiddle.x() - MARKER_WIDTH / 2.0 ) + 0.5, topMiddle.y() + 0.5 );
398  painter.drawPolygon( sOuterTriangle );
399 
400  // draw the checkerboard background for marker
401  painter.setBrush( QBrush( transparentBackground() ) );
402  painter.setPen( Qt::NoPen );
403  painter.drawPolygon( sInnerTriangle );
404 
405  // draw color on top
406  painter.setBrush( color );
407  painter.drawPolygon( sInnerTriangle );
408  painter.restore();
409 }
410 
411 double QgsGradientStopEditor::pointToRelativePosition( int x ) const
412 {
413  int left = rect().x() + MARGIN_X + FRAME_MARGIN;
414  int right = left + rect().width() - 2 * MARGIN_X - 2 * FRAME_MARGIN;
415 
416  if ( x <= left )
417  return 0;
418  else if ( x >= right )
419  return 1.0;
420 
421  return static_cast< double >( x - left ) / ( right - left );
422 }
423 
424 int QgsGradientStopEditor::relativePositionToPoint( double position ) const
425 {
426  int left = rect().x() + MARGIN_X + FRAME_MARGIN;
427  int right = left + rect().width() - 2 * MARGIN_X - 2 * FRAME_MARGIN;
428 
429  if ( position <= 0 )
430  return left;
431  else if ( position >= 1.0 )
432  return right;
433 
434  return left + ( right - left ) * position;
435 }
436 
437 bool QgsGradientStopEditor::selectedStopIsMovable() const
438 {
439  // first and last stop can't be moved or deleted
440  return mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1;
441 }
442 
443 
444 void QgsGradientStopEditor::dragEnterEvent( QDragEnterEvent *e )
445 {
446  //is dragged data valid color data?
447  bool hasAlpha;
448  QColor mimeColor = QgsSymbolLayerUtils::colorFromMimeData( e->mimeData(), hasAlpha );
449 
450  if ( mimeColor.isValid() )
451  {
452  //if so, we accept the drag
453  e->acceptProposedAction();
454  }
455 }
456 
457 void QgsGradientStopEditor::dropEvent( QDropEvent *e )
458 {
459  //is dropped data valid color data?
460  bool hasAlpha = false;
461  QColor mimeColor = QgsSymbolLayerUtils::colorFromMimeData( e->mimeData(), hasAlpha );
462 
463  if ( mimeColor.isValid() )
464  {
465  //accept drop and set new color
466  e->acceptProposedAction();
467 
468  // add a new stop here
469  double offset = pointToRelativePosition( e->pos().x() );
470  mStops << QgsGradientStop( offset, mimeColor );
471  mSelectedStop = mStops.length();
472  mGradient.setStops( mStops );
473  update();
474  emit changed();
475  }
476 
477  //could not get color from mime data
478 }
479 
480 
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.
#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
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.