QGIS API Documentation  2.18.21-Las Palmas (9fba24a)
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 <QPainter>
19 #include <QStyleOptionFrameV3>
20 #include <QMouseEvent>
21 
22 #define MARKER_WIDTH 11
23 #define MARKER_HEIGHT 14
24 #define MARKER_GAP 1.5
25 #define MARGIN_BOTTOM ( MARKER_HEIGHT + 2 )
26 #define MARGIN_X ( MARKER_WIDTH / 2 )
27 #define FRAME_MARGIN 2
28 #define CLICK_THRESHOLD ( MARKER_WIDTH / 2 + 3 )
29 
31  : QWidget( parent )
32  , mSelectedStop( 0 )
33 {
34  if ( ramp )
35  mGradient = *ramp;
36  mStops = mGradient.stops();
37 
38  if ( sOuterTriangle.isEmpty() )
39  {
40  sOuterTriangle << QPointF( 0, MARKER_HEIGHT ) << QPointF( MARKER_WIDTH, MARKER_HEIGHT )
41  << QPointF( MARKER_WIDTH, MARKER_WIDTH / 2.0 )
42  << QPointF( MARKER_WIDTH / 2.0, 0 )
43  << QPointF( 0, MARKER_WIDTH / 2.0 )
44  << QPointF( 0, MARKER_HEIGHT );
45  }
46  if ( sInnerTriangle.isEmpty() )
47  {
49  << QPointF( MARKER_WIDTH - MARKER_GAP, MARKER_WIDTH / 2.0 + 1 )
50  << QPointF( MARKER_WIDTH / 2.0, MARKER_GAP )
51  << QPointF( MARKER_GAP, MARKER_WIDTH / 2.0 + 1 )
53  }
54 
55  setFocusPolicy( Qt::StrongFocus );
56  setAcceptDrops( true );
57 }
58 
60 {
61  mGradient = ramp;
62  mStops = mGradient.stops();
63  mSelectedStop = 0;
64  update();
65  emit changed();
66 }
67 
69 {
70  //horizontal
71  return QSize( 200, 80 );
72 }
73 
75 {
76  Q_UNUSED( event );
77  QPainter painter( this );
78 
79  QRect frameRect( rect().x() + MARGIN_X, rect().y(),
80  rect().width() - 2 * MARGIN_X,
81  rect().height() - MARGIN_BOTTOM );
82 
83  //draw frame
84  QStyleOptionFrameV3 option;
85  option.initFrom( this );
86  option.state = hasFocus() ? QStyle::State_KeyboardFocusChange : QStyle::State_None;
87  option.rect = frameRect;
88  style()->drawPrimitive( QStyle::PE_Frame, &option, &painter );
89 
90  if ( hasFocus() )
91  {
92  //draw focus rect
93  QStyleOptionFocusRect option;
94  option.initFrom( this );
95  option.state = QStyle::State_KeyboardFocusChange;
96  option.rect = frameRect;
97  style()->drawPrimitive( QStyle::PE_FrameFocusRect, &option, &painter );
98  }
99 
100  //start with the checkboard pattern
101  QBrush checkBrush = QBrush( transparentBackground() );
102  painter.setBrush( checkBrush );
103  painter.setPen( Qt::NoPen );
104 
105  QRect box( frameRect.x() + FRAME_MARGIN, frameRect.y() + FRAME_MARGIN,
106  frameRect.width() - 2 * FRAME_MARGIN,
107  frameRect.height() - 2 * FRAME_MARGIN );
108 
109  painter.drawRect( box );
110 
111  // draw gradient preview on top of checkerboard
112  for ( int i = 0; i < box.width() + 1; ++i )
113  {
114  QPen pen( mGradient.color( static_cast< double >( i ) / box.width() ) );
115  painter.setPen( pen );
116  painter.drawLine( box.left() + i, box.top(), box.left() + i, box.height() + 1 );
117  }
118 
119  // draw stop markers
120  int markerTop = frameRect.bottom() + 1;
121  drawStopMarker( painter, QPoint( box.left(), markerTop ), mGradient.color1(), mSelectedStop == 0 );
122  drawStopMarker( painter, QPoint( box.right(), markerTop ), mGradient.color2(), mSelectedStop == mGradient.count() - 1 );
123  int i = 1;
124  Q_FOREACH ( const QgsGradientStop& stop, mStops )
125  {
126  int x = stop.offset * box.width() + box.left();
127  drawStopMarker( painter, QPoint( x, markerTop ), stop.color, mSelectedStop == i );
128  ++i;
129  }
130 
131  painter.end();
132 }
133 
135 {
136  if ( index > 0 && index < mGradient.count() - 1 )
137  {
138  // need to map original stop index across to cached, possibly out of order stop index
139  QgsGradientStop selectedStop = mGradient.stops().at( index - 1 );
140  index = 1;
141  Q_FOREACH ( const QgsGradientStop& stop, mStops )
142  {
143  if ( stop == selectedStop )
144  {
145  break;
146  }
147  index++;
148  }
149  }
150 
151  mSelectedStop = index;
153  update();
154 }
155 
157 {
158  if ( mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1 )
159  {
160  return mStops.at( mSelectedStop - 1 );
161  }
162  else if ( mSelectedStop == 0 )
163  {
164  return QgsGradientStop( 0.0, mGradient.color1() );
165  }
166  else
167  {
168  return QgsGradientStop( 1.0, mGradient.color2() );
169  }
170 }
171 
173 {
174  if ( mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1 )
175  {
176  mStops[ mSelectedStop - 1 ].color = color;
177  mGradient.setStops( mStops );
178  }
179  else if ( mSelectedStop == 0 )
180  {
181  mGradient.setColor1( color );
182  }
183  else
184  {
185  mGradient.setColor2( color );
186  }
187  update();
188  emit changed();
189 }
190 
192 {
193  if ( mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1 )
194  {
195  mStops[ mSelectedStop - 1 ].offset = offset;
196  mGradient.setStops( mStops );
197  update();
198  emit changed();
199  }
200 }
201 
202 void QgsGradientStopEditor::setSelectedStopDetails( const QColor &color, double offset )
203 {
204  if ( mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1 )
205  {
206  mStops[ mSelectedStop - 1 ].color = color;
207  mStops[ mSelectedStop - 1 ].offset = offset;
208  mGradient.setStops( mStops );
209  }
210  else if ( mSelectedStop == 0 )
211  {
212  mGradient.setColor1( color );
213  }
214  else
215  {
216  mGradient.setColor2( color );
217  }
218 
219  update();
220  emit changed();
221 }
222 
224 {
225  if ( selectedStopIsMovable() )
226  {
227  //delete stop
228  double stopOffset = mStops.at( mSelectedStop - 1 ).offset;
229  mStops.removeAt( mSelectedStop - 1 );
230  mGradient.setStops( mStops );
231 
232  int closest = findClosestStop( relativePositionToPoint( stopOffset ) );
233  if ( closest >= 0 )
234  selectStop( closest );
235  update();
236  emit changed();
237  }
238 }
239 
241 {
242  mGradient.setColor1( color );
243  update();
244  emit changed();
245 }
246 
248 {
249  mGradient.setColor2( color );
250  update();
251  emit changed();
252 }
253 
255 {
256  if ( e->buttons() & Qt::LeftButton )
257  {
258  if ( selectedStopIsMovable() )
259  {
260  double offset = pointToRelativePosition( e->pos().x() );
261 
262  // have to edit the temporary stop list, as setting stops on the gradient will reorder them
263  // and change which stop corresponds to the selected one;
264  mStops[ mSelectedStop - 1 ].offset = offset;
265 
266  mGradient.setStops( mStops );
267  update();
268  emit changed();
269  }
270  }
271  e->accept();
272 }
273 
274 int QgsGradientStopEditor::findClosestStop( int x, int threshold ) const
275 {
276  int closestStop = -1;
277  int closestDiff = INT_MAX;
278  int currentDiff = INT_MAX;
279 
280  // check for matching stops first, so that they take precedence
281  // otherwise it's impossible to select a stop which sits above the first/last stop, making
282  // it impossible to move or delete these
283  int i = 1;
284  Q_FOREACH ( const QgsGradientStop& stop, mGradient.stops() )
285  {
286  currentDiff = qAbs( relativePositionToPoint( stop.offset ) + 1 - x );
287  if (( threshold < 0 || currentDiff < threshold ) && currentDiff < closestDiff )
288  {
289  closestStop = i;
290  closestDiff = currentDiff;
291  }
292  i++;
293  }
294 
295  //first stop
296  currentDiff = qAbs( relativePositionToPoint( 0.0 ) + 1 - x );
297  if (( threshold < 0 || currentDiff < threshold ) && currentDiff < closestDiff )
298  {
299  closestStop = 0;
300  closestDiff = currentDiff;
301  }
302 
303  //last stop
304  currentDiff = qAbs( relativePositionToPoint( 1.0 ) + 1 - x );
305  if (( threshold < 0 || currentDiff < threshold ) && currentDiff < closestDiff )
306  {
307  closestStop = mGradient.count() - 1;
308  closestDiff = currentDiff;
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 
375 }
376 
377 const QPixmap& QgsGradientStopEditor::transparentBackground()
378 {
379  static QPixmap transpBkgrd;
380 
381  if ( transpBkgrd.isNull() )
382  transpBkgrd = QgsApplication::getThemePixmap( "/transp-background_8x8.png" );
383 
384  return transpBkgrd;
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( qRound( 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 
442 {
443  //is dragged data valid color data?
444  bool hasAlpha;
445  QColor mimeColor = QgsSymbolLayerV2Utils::colorFromMimeData( e->mimeData(), hasAlpha );
446 
447  if ( mimeColor.isValid() )
448  {
449  //if so, we accept the drag
451  }
452 }
453 
455 {
456  //is dropped data valid color data?
457  bool hasAlpha = false;
458  QColor mimeColor = QgsSymbolLayerV2Utils::colorFromMimeData( e->mimeData(), hasAlpha );
459 
460  if ( mimeColor.isValid() )
461  {
462  //accept drop and set new color
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 
static unsigned index
Qt::KeyboardModifiers modifiers() const
bool end()
const QMimeData * mimeData() const
Represents a color stop within a gradient color ramp.
void setRenderHint(RenderHint hint, bool on)
void changed()
Emitted when the gradient ramp is changed by a user.
int length() const
void setFocusPolicy(Qt::FocusPolicy policy)
QStyle * style() const
QColor color2() const
Returns the gradient end color.
const T & at(int i) const
void removeAt(int i)
const QPoint & pos() const
static QColor colorFromMimeData(const QMimeData *data, bool &hasAlpha)
Attempts to parse mime data as a color.
int y() const
void save()
QColor color1() const
Returns the gradient start color.
void setSelectedStopDetails(const QColor &color, double offset)
Sets the color and offset for the current selected stop.
void drawPolygon(const QPointF *points, int pointCount, Qt::FillRule fillRule)
#define MARGIN_BOTTOM
int height() const
int x() const
int y() const
bool hasFocus() const
Qt::MouseButtons buttons() const
void drawLine(const QLineF &line)
void setColor1(const QColor &color)
Sets the color for the first stop.
void acceptProposedAction()
#define MARKER_HEIGHT
QgsGradientStopsList stops() const
Returns the list of intermediate gradient stops for the ramp.
void setColor2(const QColor &color)
Sets the gradient end color.
static QPixmap getThemePixmap(const QString &theName)
Helper to get a theme icon as a pixmap.
void update()
int x() const
int y() const
void setColor1(const QColor &color)
Sets the gradient start color.
void initFrom(const QWidget *widget)
int width() const
void drawRect(const QRectF &rectangle)
void deleteSelectedStop()
Deletes the current selected stop.
virtual int count() const override
Returns number of defined colors, or -1 if undefined.
virtual void mouseMoveEvent(QMouseEvent *event) override
int x() const
void setPen(const QColor &color)
double offset
Relative positional offset, between 0 and 1.
void setStops(const QgsGradientStopsList &stops)
Sets the list of intermediate gradient stops for the ramp.
#define MARKER_GAP
void setBrush(const QBrush &brush)
QgsGradientStopEditor(QWidget *parent=nullptr, QgsVectorGradientColorRampV2 *ramp=nullptr)
Constructor for QgsGradientStopEditor.
QRect rect() const
void setAcceptDrops(bool on)
int key() const
void accept()
bool isNull() const
void setColor2(const QColor &color)
Sets the color for the last stop.
#define MARKER_WIDTH
virtual QColor color(double value) const override
Returns the color corresponding to a specified value.
void restore()
void dropEvent(QDropEvent *e) override
void selectedStopChanged(const QgsGradientStop &stop)
Emitted when the current selected stop changes.
int width() const
void setGradientRamp(const QgsVectorGradientColorRampV2 &ramp)
Sets the current ramp shown in the editor.
bool isEmpty() const
void paintEvent(QPaintEvent *event) override
virtual void keyPressEvent(QKeyEvent *event)
int bottom() const
void setSelectedStopColor(const QColor &color)
Sets the color for the current selected stop.
virtual QSize sizeHint() const override
void translate(const QPointF &offset)
virtual void drawPrimitive(PrimitiveElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget) const=0
#define CLICK_THRESHOLD
virtual void mousePressEvent(QMouseEvent *event) override
void dragEnterEvent(QDragEnterEvent *e) override
const QPoint & pos() const
#define MARGIN_X
QgsGradientStop selectedStop() const
Returns details about the currently selected stop.
virtual void mouseDoubleClickEvent(QMouseEvent *event) override
virtual bool event(QEvent *event)
Gradient color ramp, which smoothly interpolates between two colors and also supports optional extra ...
#define FRAME_MARGIN
int height() const
virtual void keyPressEvent(QKeyEvent *event) override
void setSelectedStopOffset(double offset)
Sets the offset for the current selected stop.
bool isValid() const
QColor color
Gradient color at stop.
void selectStop(int index)
Sets the currently selected stop.