QGIS API Documentation 3.99.0-Master (d270888f95f)
Loading...
Searching...
No Matches
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
18#include "qgsapplication.h"
19#include "qgssymbollayerutils.h"
20
21#include <QMouseEvent>
22#include <QPainter>
23#include <QString>
24#include <QStyleOptionFrame>
25
26#include "moc_qgsgradientstopeditor.cpp"
27
28using namespace Qt::StringLiterals;
29
31 : QWidget( parent )
32{
33 if ( ramp )
34 mGradient = *ramp;
35 mStops = mGradient.stops();
36
37 if ( sOuterTriangle.isEmpty() )
38 {
39 sOuterTriangle << QPointF( 0, MARKER_HEIGHT ) << QPointF( MARKER_WIDTH, MARKER_HEIGHT )
40 << QPointF( MARKER_WIDTH, MARKER_WIDTH / 2.0 )
41 << QPointF( MARKER_WIDTH / 2.0, 0 )
42 << QPointF( 0, MARKER_WIDTH / 2.0 )
43 << QPointF( 0, MARKER_HEIGHT );
44 }
45 if ( sInnerTriangle.isEmpty() )
46 {
47 sInnerTriangle << QPointF( MARKER_GAP, MARKER_HEIGHT - MARKER_GAP ) << QPointF( MARKER_WIDTH - MARKER_GAP, MARKER_HEIGHT - MARKER_GAP )
48 << QPointF( MARKER_WIDTH - MARKER_GAP, MARKER_WIDTH / 2.0 + 1 )
49 << QPointF( MARKER_WIDTH / 2.0, MARKER_GAP )
50 << QPointF( MARKER_GAP, MARKER_WIDTH / 2.0 + 1 )
51 << QPointF( MARKER_GAP, MARKER_HEIGHT - MARKER_GAP );
52 }
53
54 setFocusPolicy( Qt::StrongFocus );
55 setAcceptDrops( true );
56}
57
59{
60 mGradient = ramp;
61 mStops = mGradient.stops();
62 mSelectedStop = 0;
63 update();
64 emit changed();
65}
66
68{
69 //horizontal
70 return QSize( 200, 80 );
71}
72
73void QgsGradientStopEditor::paintEvent( QPaintEvent *event )
74{
75 Q_UNUSED( event )
76 QPainter painter( this );
77
78 QRect frameRect( rect().x() + MARGIN_X, rect().y(), rect().width() - 2 * MARGIN_X, rect().height() - MARGIN_BOTTOM );
79
80 //draw frame
81 QStyleOptionFrame option;
82 option.initFrom( this );
83 option.state = hasFocus() ? QStyle::State_KeyboardFocusChange : QStyle::State_None;
84 option.rect = frameRect;
85 style()->drawPrimitive( QStyle::PE_Frame, &option, &painter );
86
87 if ( hasFocus() )
88 {
89 //draw focus rect
90 QStyleOptionFocusRect option;
91 option.initFrom( this );
92 option.state = QStyle::State_KeyboardFocusChange;
93 option.rect = frameRect;
94 style()->drawPrimitive( QStyle::PE_FrameFocusRect, &option, &painter );
95 }
96
97 //start with the checkboard pattern
98 QBrush checkBrush = QBrush( transparentBackground() );
99 painter.setBrush( checkBrush );
100 painter.setPen( Qt::NoPen );
101
102 QRect box( frameRect.x() + FRAME_MARGIN_PX, frameRect.y() + FRAME_MARGIN_PX, frameRect.width() - 2 * FRAME_MARGIN_PX, frameRect.height() - 2 * FRAME_MARGIN_PX );
103
104 painter.drawRect( box );
105
106 // draw gradient preview on top of checkerboard
107 for ( int i = 0; i < box.width() + 1; ++i )
108 {
109 QPen pen( mGradient.color( static_cast<double>( i ) / box.width() ) );
110 painter.setPen( pen );
111 painter.drawLine( box.left() + i, box.top(), box.left() + i, box.height() + 1 );
112 }
113
114 // draw stop markers
115 int markerTop = frameRect.bottom() + 1;
116 drawStopMarker( painter, QPoint( box.left(), markerTop ), mGradient.color1(), mSelectedStop == 0 );
117 drawStopMarker( painter, QPoint( box.right(), markerTop ), mGradient.color2(), mSelectedStop == mGradient.count() - 1 );
118 int i = 1;
119 const auto constMStops = mStops;
120 for ( const QgsGradientStop &stop : constMStops )
121 {
122 int x = stop.offset * box.width() + box.left();
123 drawStopMarker( painter, QPoint( x, markerTop ), stop.color, mSelectedStop == i );
124 ++i;
125 }
126
127 painter.end();
128}
129
131{
132 if ( index > 0 && index < mGradient.count() - 1 )
133 {
134 // need to map original stop index across to cached, possibly out of order stop index
135 QgsGradientStop selectedStop = mGradient.stops().at( index - 1 );
136 index = 1;
137 const auto constMStops = mStops;
138 for ( const QgsGradientStop &stop : constMStops )
139 {
140 if ( stop == selectedStop )
141 {
142 break;
143 }
144 index++;
145 }
146 }
147
148 mSelectedStop = index;
150 update();
151}
152
154{
155 if ( mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1 )
156 {
157 return mStops.at( mSelectedStop - 1 );
158 }
159 else if ( mSelectedStop == 0 )
160 {
161 return QgsGradientStop( 0.0, mGradient.color1() );
162 }
163 else
164 {
165 QgsGradientStop stop( 1.0, mGradient.color2() );
166 stop.setColorSpec( mGradient.colorSpec() );
167 stop.setDirection( mGradient.direction() );
168 return stop;
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
203{
204 if ( mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1 )
205 {
206 mStops[mSelectedStop - 1].setColorSpec( spec );
207 mGradient.setStops( mStops );
208 update();
209 emit changed();
210 }
211 else if ( mSelectedStop == mGradient.count() - 1 )
212 {
213 mGradient.setColorSpec( spec );
214 update();
215 emit changed();
216 }
217}
218
220{
221 if ( mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1 )
222 {
223 mStops[mSelectedStop - 1].setDirection( direction );
224 mGradient.setStops( mStops );
225 update();
226 emit changed();
227 }
228 else if ( mSelectedStop == mGradient.count() - 1 )
229 {
230 mGradient.setDirection( direction );
231 update();
232 emit changed();
233 }
234}
235
236void QgsGradientStopEditor::setSelectedStopDetails( const QColor &color, double offset )
237{
238 if ( mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1 )
239 {
240 mStops[mSelectedStop - 1].color = color;
241 mStops[mSelectedStop - 1].offset = offset;
242 mGradient.setStops( mStops );
243 }
244 else if ( mSelectedStop == 0 )
245 {
246 mGradient.setColor1( color );
247 }
248 else
249 {
250 mGradient.setColor2( color );
251 }
252
253 update();
254 emit changed();
255}
256
258{
259 if ( selectedStopIsMovable() )
260 {
261 //delete stop
262 double stopOffset = mStops.at( mSelectedStop - 1 ).offset;
263 mStops.removeAt( mSelectedStop - 1 );
264 mGradient.setStops( mStops );
265
266 int closest = findClosestStop( relativePositionToPoint( stopOffset ) );
267 if ( closest >= 0 )
268 selectStop( closest );
269 update();
270 emit changed();
271 }
272}
273
274void QgsGradientStopEditor::setColor1( const QColor &color )
275{
276 mGradient.setColor1( color );
277 update();
278 emit changed();
279}
280
281void QgsGradientStopEditor::setColor2( const QColor &color )
282{
283 mGradient.setColor2( color );
284 update();
285 emit changed();
286}
287
289{
290 if ( e->buttons() & Qt::LeftButton )
291 {
292 if ( selectedStopIsMovable() )
293 {
294 double offset = pointToRelativePosition( e->pos().x() );
295
296 // have to edit the temporary stop list, as setting stops on the gradient will reorder them
297 // and change which stop corresponds to the selected one;
298 mStops[mSelectedStop - 1].offset = offset;
299
300 mGradient.setStops( mStops );
301 update();
302 emit changed();
303 }
304 }
305 e->accept();
306}
307
308int QgsGradientStopEditor::findClosestStop( int x, int threshold ) const
309{
310 int closestStop = -1;
311 int closestDiff = std::numeric_limits<int>::max();
312 int currentDiff = std::numeric_limits<int>::max();
313
314 // check for matching stops first, so that they take precedence
315 // otherwise it's impossible to select a stop which sits above the first/last stop, making
316 // it impossible to move or delete these
317 int i = 1;
318 const auto constStops = mGradient.stops();
319 for ( const QgsGradientStop &stop : constStops )
320 {
321 currentDiff = std::abs( relativePositionToPoint( stop.offset ) + 1 - x );
322 if ( ( threshold < 0 || currentDiff < threshold ) && currentDiff < closestDiff )
323 {
324 closestStop = i;
325 closestDiff = currentDiff;
326 }
327 i++;
328 }
329
330 //first stop
331 currentDiff = std::abs( relativePositionToPoint( 0.0 ) + 1 - x );
332 if ( ( threshold < 0 || currentDiff < threshold ) && currentDiff < closestDiff )
333 {
334 closestStop = 0;
335 closestDiff = currentDiff;
336 }
337
338 //last stop
339 currentDiff = std::abs( relativePositionToPoint( 1.0 ) + 1 - x );
340 if ( ( threshold < 0 || currentDiff < threshold ) && currentDiff < closestDiff )
341 {
342 closestStop = mGradient.count() - 1;
343 }
344
345 return closestStop;
346}
347
349{
350 if ( e->pos().y() >= rect().height() - MARGIN_BOTTOM - 1 )
351 {
352 // find closest point
353 int closestStop = findClosestStop( e->pos().x(), CLICK_THRESHOLD );
354 if ( closestStop >= 0 )
355 {
356 selectStop( closestStop );
357 }
358 update();
359 }
360 e->accept();
361}
362
364{
365 if ( e->buttons() & Qt::LeftButton )
366 {
367 // add a new stop
368 double offset = pointToRelativePosition( e->pos().x() );
369 mStops << QgsGradientStop( offset, mGradient.color( offset ) );
370 mSelectedStop = mStops.length();
371 mGradient.setStops( mStops );
372 update();
373 emit changed();
374 }
375 e->accept();
376}
377
379{
380 if ( ( e->key() == Qt::Key_Backspace || e->key() == Qt::Key_Delete ) )
381 {
383 e->accept();
384 return;
385 }
386 else if ( e->key() == Qt::Key_Left || e->key() == Qt::Key_Right )
387 {
388 if ( selectedStopIsMovable() )
389 {
390 // calculate offset corresponding to 1 px
391 double offsetDiff = pointToRelativePosition( rect().x() + MARGIN_X + FRAME_MARGIN_PX + 2 ) - pointToRelativePosition( rect().x() + MARGIN_X + FRAME_MARGIN_PX + 1 );
392
393 if ( e->modifiers() & Qt::ShiftModifier )
394 offsetDiff *= 10.0;
395
396 if ( e->key() == Qt::Key_Left )
397 offsetDiff *= -1;
398
399 mStops[mSelectedStop - 1].offset = std::clamp( mStops[mSelectedStop - 1].offset + offsetDiff, 0.0, 1.0 );
400 mGradient.setStops( mStops );
401 update();
402 e->accept();
403 emit changed();
404 return;
405 }
406 }
407
408 QWidget::keyPressEvent( e );
409}
410
411QPixmap QgsGradientStopEditor::transparentBackground()
412{
413 static QPixmap sTranspBkgrd;
414
415 if ( sTranspBkgrd.isNull() )
416 sTranspBkgrd = QgsApplication::getThemePixmap( u"/transp-background_8x8.png"_s );
417
418 return sTranspBkgrd;
419}
420
421void QgsGradientStopEditor::drawStopMarker( QPainter &painter, QPoint topMiddle, const QColor &color, bool selected )
422{
423 QgsScopedQPainterState painterState( &painter );
424 painter.setRenderHint( QPainter::Antialiasing );
425 painter.setBrush( selected ? QColor( 150, 150, 150 ) : Qt::white );
426 painter.setPen( selected ? Qt::black : QColor( 150, 150, 150 ) );
427 // 0.5 offsets to make edges pixel grid aligned
428 painter.translate( std::round( topMiddle.x() - MARKER_WIDTH / 2.0 ) + 0.5, topMiddle.y() + 0.5 );
429 painter.drawPolygon( sOuterTriangle );
430
431 // draw the checkerboard background for marker
432 painter.setBrush( QBrush( transparentBackground() ) );
433 painter.setPen( Qt::NoPen );
434 painter.drawPolygon( sInnerTriangle );
435
436 // draw color on top
437 painter.setBrush( color );
438 painter.drawPolygon( sInnerTriangle );
439}
440
441double QgsGradientStopEditor::pointToRelativePosition( int x ) const
442{
443 int left = rect().x() + MARGIN_X + FRAME_MARGIN_PX;
444 int right = left + rect().width() - 2 * MARGIN_X - 2 * FRAME_MARGIN_PX;
445
446 if ( x <= left )
447 return 0;
448 else if ( x >= right )
449 return 1.0;
450
451 return static_cast<double>( x - left ) / ( right - left );
452}
453
454int QgsGradientStopEditor::relativePositionToPoint( double position ) const
455{
456 int left = rect().x() + MARGIN_X + FRAME_MARGIN_PX;
457 int right = left + rect().width() - 2 * MARGIN_X - 2 * FRAME_MARGIN_PX;
458
459 if ( position <= 0 )
460 return left;
461 else if ( position >= 1.0 )
462 return right;
463
464 return left + ( right - left ) * position;
465}
466
467bool QgsGradientStopEditor::selectedStopIsMovable() const
468{
469 // first and last stop can't be moved or deleted
470 return mSelectedStop > 0 && mSelectedStop < mGradient.count() - 1;
471}
472
473
475{
476 //is dragged data valid color data?
477 bool hasAlpha;
478 QColor mimeColor = QgsSymbolLayerUtils::colorFromMimeData( e->mimeData(), hasAlpha );
479
480 if ( mimeColor.isValid() )
481 {
482 //if so, we accept the drag
483 e->acceptProposedAction();
484 }
485}
486
488{
489 //is dropped data valid color data?
490 bool hasAlpha = false;
491 QColor mimeColor = QgsSymbolLayerUtils::colorFromMimeData( e->mimeData(), hasAlpha );
492
493 if ( mimeColor.isValid() )
494 {
495 //accept drop and set new color
496 e->acceptProposedAction();
497
498 // add a new stop here
499 double offset = pointToRelativePosition( e->pos().x() );
500 mStops << QgsGradientStop( offset, mimeColor );
501 mSelectedStop = mStops.length();
502 mGradient.setStops( mStops );
503 update();
504 emit changed();
505 }
506
507 //could not get color from mime data
508}
AngularDirection
Angular directions.
Definition qgis.h:3491
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 ...
QgsGradientStopsList stops() const
Returns the list of intermediate gradient stops for the ramp.
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...
static QColor colorFromMimeData(const QMimeData *data, bool &hasAlpha)
Attempts to parse mime data as a color.