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