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