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