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