QGIS API Documentation  3.20.0-Odense (decaadbb31)
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 = std::clamp( mStops[ mSelectedStop - 1 ].offset + offsetDiff, 0.0, 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  QgsScopedQPainterState painterState( &painter );
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 }
409 
410 double QgsGradientStopEditor::pointToRelativePosition( int x ) const
411 {
412  int left = rect().x() + MARGIN_X + FRAME_MARGIN;
413  int right = left + rect().width() - 2 * MARGIN_X - 2 * FRAME_MARGIN;
414 
415  if ( x <= left )
416  return 0;
417  else if ( x >= right )
418  return 1.0;
419 
420  return static_cast< double >( x - left ) / ( right - left );
421 }
422 
423 int QgsGradientStopEditor::relativePositionToPoint( double position ) const
424 {
425  int left = rect().x() + MARGIN_X + FRAME_MARGIN;
426  int right = left + rect().width() - 2 * MARGIN_X - 2 * FRAME_MARGIN;
427 
428  if ( position <= 0 )
429  return left;
430  else if ( position >= 1.0 )
431  return right;
432 
433  return left + ( right - left ) * position;
434 }
435 
436 bool QgsGradientStopEditor::selectedStopIsMovable() const
437 {
438  // first and last stop can't be moved or deleted
439  return mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1;
440 }
441 
442 
443 void QgsGradientStopEditor::dragEnterEvent( QDragEnterEvent *e )
444 {
445  //is dragged data valid color data?
446  bool hasAlpha;
447  QColor mimeColor = QgsSymbolLayerUtils::colorFromMimeData( e->mimeData(), hasAlpha );
448 
449  if ( mimeColor.isValid() )
450  {
451  //if so, we accept the drag
452  e->acceptProposedAction();
453  }
454 }
455 
456 void QgsGradientStopEditor::dropEvent( QDropEvent *e )
457 {
458  //is dropped data valid color data?
459  bool hasAlpha = false;
460  QColor mimeColor = QgsSymbolLayerUtils::colorFromMimeData( e->mimeData(), hasAlpha );
461 
462  if ( mimeColor.isValid() )
463  {
464  //accept drop and set new color
465  e->acceptProposedAction();
466 
467  // add a new stop here
468  double offset = pointToRelativePosition( e->pos().x() );
469  mStops << QgsGradientStop( offset, mimeColor );
470  mSelectedStop = mStops.length();
471  mGradient.setStops( mStops );
472  update();
473  emit changed();
474  }
475 
476  //could not get color from mime data
477 }
478 
479 
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 ...
Definition: qgscolorramp.h:151
void setColor1(const QColor &color)
Sets the gradient start color.
Definition: qgscolorramp.h:206
void setColor2(const QColor &color)
Sets the gradient end color.
Definition: qgscolorramp.h:214
int count() const override
Returns number of defined colors, or -1 if undefined.
Definition: qgscolorramp.h:170
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.
Definition: qgscolorramp.h:191
QgsGradientStopsList stops() const
Returns the list of intermediate gradient stops for the ramp.
Definition: qgscolorramp.h:254
QColor color2() const
Returns the gradient end color.
Definition: qgscolorramp.h:198
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
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 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.
Definition: qgscolorramp.h:113
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