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 
QgsGradientStopEditor(QWidget * parent,QgsGradientColorRamp * ramp)36 QgsGradientStopEditor::QgsGradientStopEditor( QWidget *parent, QgsGradientColorRamp *ramp )
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 
setGradientRamp(const QgsGradientColorRamp & ramp)64 void QgsGradientStopEditor::setGradientRamp( const QgsGradientColorRamp &ramp )
65 {
66   mGradient = ramp;
67   mStops = mGradient.stops();
68   mSelectedStop = 0;
69   update();
70   emit changed();
71 }
72 
sizeHint() const73 QSize QgsGradientStopEditor::sizeHint() const
74 {
75   //horizontal
76   return QSize( 200, 80 );
77 }
78 
paintEvent(QPaintEvent * event)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 
selectStop(int index)140 void QgsGradientStopEditor::selectStop( int index )
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;
159   emit selectedStopChanged( selectedStop() );
160   update();
161 }
162 
selectedStop() const163 QgsGradientStop QgsGradientStopEditor::selectedStop() const
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 
setSelectedStopColor(const QColor & color)179 void QgsGradientStopEditor::setSelectedStopColor( const QColor &color )
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 
setSelectedStopOffset(double offset)198 void QgsGradientStopEditor::setSelectedStopOffset( double offset )
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 
setSelectedStopDetails(const QColor & color,double offset)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 
deleteSelectedStop()230 void QgsGradientStopEditor::deleteSelectedStop()
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 
setColor1(const QColor & color)247 void QgsGradientStopEditor::setColor1( const QColor &color )
248 {
249   mGradient.setColor1( color );
250   update();
251   emit changed();
252 }
253 
setColor2(const QColor & color)254 void QgsGradientStopEditor::setColor2( const QColor &color )
255 {
256   mGradient.setColor2( color );
257   update();
258   emit changed();
259 }
260 
mouseMoveEvent(QMouseEvent * e)261 void QgsGradientStopEditor::mouseMoveEvent( QMouseEvent *e )
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 
findClosestStop(int x,int threshold) const281 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 
mousePressEvent(QMouseEvent * e)321 void QgsGradientStopEditor::mousePressEvent( QMouseEvent *e )
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 
mouseDoubleClickEvent(QMouseEvent * e)336 void QgsGradientStopEditor::mouseDoubleClickEvent( QMouseEvent *e )
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 
keyPressEvent(QKeyEvent * e)351 void QgsGradientStopEditor::keyPressEvent( QKeyEvent *e )
352 {
353   if ( ( e->key() == Qt::Key_Backspace || e->key() == Qt::Key_Delete ) )
354   {
355     deleteSelectedStop();
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 
transparentBackground()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 
drawStopMarker(QPainter & painter,QPoint topMiddle,const QColor & color,bool selected)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 
pointToRelativePosition(int x) const414 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 
relativePositionToPoint(double position) const427 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 
selectedStopIsMovable() const440 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 
dragEnterEvent(QDragEnterEvent * e)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 
dropEvent(QDropEvent * e)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 
484