1 /***************************************************************************
2 qgscolorwidgets.cpp - color selection widgets
3 ---------------------
4 begin : September 2014
5 copyright : (C) 2014 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 "qgscolorwidgets.h"
17 #include "qgsapplication.h"
18 #include "qgssymbollayerutils.h"
19 #include "qgssettings.h"
20 #include "qgslogger.h"
21 #include "qgsguiutils.h"
22
23 #include <QResizeEvent>
24
25 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
26 #include <QStyleOptionFrameV3>
27 #else
28 #include <QStyleOptionFrame>
29 #endif
30 #include <QPainter>
31 #include <QHBoxLayout>
32 #include <QSpinBox>
33 #include <QLineEdit>
34 #include <QFontMetrics>
35 #include <QToolButton>
36 #include <QMenu>
37 #include <QDrag>
38 #include <QRectF>
39 #include <QLineF>
40
41 #include <cmath>
42
43
44 //
45 // QgsColorWidget
46 //
47
QgsColorWidget(QWidget * parent,const ColorComponent component)48 QgsColorWidget::QgsColorWidget( QWidget *parent, const ColorComponent component )
49 : QWidget( parent )
50 , mCurrentColor( Qt::red )
51 , mComponent( component )
52 {
53 setAcceptDrops( true );
54 }
55
componentValue() const56 int QgsColorWidget::componentValue() const
57 {
58 return componentValue( mComponent );
59 }
60
createDragIcon(const QColor & color)61 QPixmap QgsColorWidget::createDragIcon( const QColor &color )
62 {
63 //craft a pixmap for the drag icon
64 const int iconSize = QgsGuiUtils::scaleIconSize( 50 );
65 QPixmap pixmap( iconSize, iconSize );
66 pixmap.fill( Qt::transparent );
67 QPainter painter;
68 painter.begin( &pixmap );
69 //start with a light gray background
70 painter.fillRect( QRect( 0, 0, iconSize, iconSize ), QBrush( QColor( 200, 200, 200 ) ) );
71 //draw rect with white border, filled with current color
72 QColor pixmapColor = color;
73 pixmapColor.setAlpha( 255 );
74 painter.setBrush( QBrush( pixmapColor ) );
75 painter.setPen( QPen( Qt::white ) );
76 painter.drawRect( QRect( 1, 1, iconSize - 2, iconSize - 2 ) );
77 painter.end();
78 return pixmap;
79 }
80
componentValue(const QgsColorWidget::ColorComponent component) const81 int QgsColorWidget::componentValue( const QgsColorWidget::ColorComponent component ) const
82 {
83 if ( !mCurrentColor.isValid() )
84 {
85 return -1;
86 }
87
88 switch ( component )
89 {
90 case QgsColorWidget::Red:
91 return mCurrentColor.red();
92 case QgsColorWidget::Green:
93 return mCurrentColor.green();
94 case QgsColorWidget::Blue:
95 return mCurrentColor.blue();
96 case QgsColorWidget::Hue:
97 //hue is treated specially, to avoid -1 hues values from QColor for ambiguous hues
98 return hue();
99 case QgsColorWidget::Saturation:
100 return mCurrentColor.hsvSaturation();
101 case QgsColorWidget::Value:
102 return mCurrentColor.value();
103 case QgsColorWidget::Alpha:
104 return mCurrentColor.alpha();
105 default:
106 return -1;
107 }
108 }
109
componentRange() const110 int QgsColorWidget::componentRange() const
111 {
112 return componentRange( mComponent );
113 }
114
componentRange(const QgsColorWidget::ColorComponent component) const115 int QgsColorWidget::componentRange( const QgsColorWidget::ColorComponent component ) const
116 {
117 if ( component == QgsColorWidget::Multiple )
118 {
119 //no component
120 return -1;
121 }
122
123 if ( component == QgsColorWidget::Hue )
124 {
125 //hue ranges to 359
126 return 359;
127 }
128 else
129 {
130 //all other components range to 255
131 return 255;
132 }
133 }
134
hue() const135 int QgsColorWidget::hue() const
136 {
137 if ( mCurrentColor.hue() >= 0 )
138 {
139 return mCurrentColor.hue();
140 }
141 else
142 {
143 return mExplicitHue;
144 }
145 }
146
alterColor(QColor & color,const QgsColorWidget::ColorComponent component,const int newValue) const147 void QgsColorWidget::alterColor( QColor &color, const QgsColorWidget::ColorComponent component, const int newValue ) const
148 {
149 int h, s, v, a;
150 color.getHsv( &h, &s, &v, &a );
151
152 //clip value to sensible range
153 const int clippedValue = std::min( std::max( 0, newValue ), componentRange( component ) );
154
155 switch ( component )
156 {
157 case QgsColorWidget::Red:
158 color.setRed( clippedValue );
159 return;
160 case QgsColorWidget::Green:
161 color.setGreen( clippedValue );
162 return;
163 case QgsColorWidget::Blue:
164 color.setBlue( clippedValue );
165 return;
166 case QgsColorWidget::Hue:
167 color.setHsv( clippedValue, s, v, a );
168 return;
169 case QgsColorWidget::Saturation:
170 color.setHsv( h, clippedValue, v, a );
171 return;
172 case QgsColorWidget::Value:
173 color.setHsv( h, s, clippedValue, a );
174 return;
175 case QgsColorWidget::Alpha:
176 color.setAlpha( clippedValue );
177 return;
178 default:
179 return;
180 }
181 }
182
transparentBackground()183 const QPixmap &QgsColorWidget::transparentBackground()
184 {
185 static QPixmap sTranspBkgrd;
186
187 if ( sTranspBkgrd.isNull() )
188 sTranspBkgrd = QgsApplication::getThemePixmap( QStringLiteral( "/transp-background_8x8.png" ) );
189
190 return sTranspBkgrd;
191 }
192
dragEnterEvent(QDragEnterEvent * e)193 void QgsColorWidget::dragEnterEvent( QDragEnterEvent *e )
194 {
195 //is dragged data valid color data?
196 bool hasAlpha;
197 const QColor mimeColor = QgsSymbolLayerUtils::colorFromMimeData( e->mimeData(), hasAlpha );
198
199 if ( mimeColor.isValid() )
200 {
201 //if so, we accept the drag
202 e->acceptProposedAction();
203 }
204 }
205
dropEvent(QDropEvent * e)206 void QgsColorWidget::dropEvent( QDropEvent *e )
207 {
208 //is dropped data valid color data?
209 bool hasAlpha = false;
210 QColor mimeColor = QgsSymbolLayerUtils::colorFromMimeData( e->mimeData(), hasAlpha );
211
212 if ( mimeColor.isValid() )
213 {
214 //accept drop and set new color
215 e->acceptProposedAction();
216
217 if ( !hasAlpha )
218 {
219 //mime color has no explicit alpha component, so keep existing alpha
220 mimeColor.setAlpha( mCurrentColor.alpha() );
221 }
222
223 setColor( mimeColor );
224 emit colorChanged( mCurrentColor );
225 }
226
227 //could not get color from mime data
228 }
229
mouseMoveEvent(QMouseEvent * e)230 void QgsColorWidget::mouseMoveEvent( QMouseEvent *e )
231 {
232 emit hovered();
233 e->accept();
234 //don't pass to QWidget::mouseMoveEvent, causes issues with widget used in QWidgetAction
235 }
236
mousePressEvent(QMouseEvent * e)237 void QgsColorWidget::mousePressEvent( QMouseEvent *e )
238 {
239 e->accept();
240 //don't pass to QWidget::mousePressEvent, causes issues with widget used in QWidgetAction
241 }
242
mouseReleaseEvent(QMouseEvent * e)243 void QgsColorWidget::mouseReleaseEvent( QMouseEvent *e )
244 {
245 e->accept();
246 //don't pass to QWidget::mouseReleaseEvent, causes issues with widget used in QWidgetAction
247 }
248
color() const249 QColor QgsColorWidget::color() const
250 {
251 return mCurrentColor;
252 }
253
setComponent(const QgsColorWidget::ColorComponent component)254 void QgsColorWidget::setComponent( const QgsColorWidget::ColorComponent component )
255 {
256 if ( component == mComponent )
257 {
258 return;
259 }
260
261 mComponent = component;
262 update();
263 }
264
setComponentValue(const int value)265 void QgsColorWidget::setComponentValue( const int value )
266 {
267 if ( mComponent == QgsColorWidget::Multiple )
268 {
269 return;
270 }
271
272 //clip value to valid range
273 int valueClipped = std::min( value, componentRange() );
274 valueClipped = std::max( valueClipped, 0 );
275
276 int r, g, b, a;
277 mCurrentColor.getRgb( &r, &g, &b, &a );
278 int h, s, v;
279 mCurrentColor.getHsv( &h, &s, &v );
280 //overwrite hue with explicit hue if required
281 h = hue();
282
283 switch ( mComponent )
284 {
285 case QgsColorWidget::Red:
286 if ( r == valueClipped )
287 {
288 return;
289 }
290 mCurrentColor.setRed( valueClipped );
291 break;
292 case QgsColorWidget::Green:
293 if ( g == valueClipped )
294 {
295 return;
296 }
297 mCurrentColor.setGreen( valueClipped );
298 break;
299 case QgsColorWidget::Blue:
300 if ( b == valueClipped )
301 {
302 return;
303 }
304 mCurrentColor.setBlue( valueClipped );
305 break;
306 case QgsColorWidget::Hue:
307 if ( h == valueClipped )
308 {
309 return;
310 }
311 mCurrentColor.setHsv( valueClipped, s, v, a );
312 break;
313 case QgsColorWidget::Saturation:
314 if ( s == valueClipped )
315 {
316 return;
317 }
318 mCurrentColor.setHsv( h, valueClipped, v, a );
319 break;
320 case QgsColorWidget::Value:
321 if ( v == valueClipped )
322 {
323 return;
324 }
325 mCurrentColor.setHsv( h, s, valueClipped, a );
326 break;
327 case QgsColorWidget::Alpha:
328 if ( a == valueClipped )
329 {
330 return;
331 }
332 mCurrentColor.setAlpha( valueClipped );
333 break;
334 default:
335 return;
336 }
337
338 //update recorded hue
339 if ( mCurrentColor.hue() >= 0 )
340 {
341 mExplicitHue = mCurrentColor.hue();
342 }
343
344 update();
345 }
346
setColor(const QColor & color,const bool emitSignals)347 void QgsColorWidget::setColor( const QColor &color, const bool emitSignals )
348 {
349 if ( color == mCurrentColor )
350 {
351 return;
352 }
353
354 mCurrentColor = color;
355
356 //update recorded hue
357 if ( color.hue() >= 0 )
358 {
359 mExplicitHue = color.hue();
360 }
361
362 if ( emitSignals )
363 {
364 emit colorChanged( mCurrentColor );
365 }
366
367 update();
368 }
369
370
371 //
372 // QgsColorWheel
373 //
374
QgsColorWheel(QWidget * parent)375 QgsColorWheel::QgsColorWheel( QWidget *parent )
376 : QgsColorWidget( parent )
377 {
378 //create wheel hue brush - only do this once
379 QConicalGradient wheelGradient = QConicalGradient( 0, 0, 0 );
380 const int wheelStops = 20;
381 QColor gradColor = QColor::fromHsvF( 1.0, 1.0, 1.0 );
382 for ( int pos = 0; pos <= wheelStops; ++pos )
383 {
384 const double relativePos = static_cast<double>( pos ) / wheelStops;
385 gradColor.setHsvF( relativePos, 1, 1 );
386 wheelGradient.setColorAt( relativePos, gradColor );
387 }
388 mWheelBrush = QBrush( wheelGradient );
389 }
390
~QgsColorWheel()391 QgsColorWheel::~QgsColorWheel()
392 {
393 delete mWheelImage;
394 delete mTriangleImage;
395 delete mWidgetImage;
396 }
397
sizeHint() const398 QSize QgsColorWheel::sizeHint() const
399 {
400 const int size = Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 22;
401 return QSize( size, size );
402 }
403
paintEvent(QPaintEvent * event)404 void QgsColorWheel::paintEvent( QPaintEvent *event )
405 {
406 Q_UNUSED( event )
407 QPainter painter( this );
408
409 if ( !mWidgetImage || !mWheelImage || !mTriangleImage )
410 {
411 createImages( size() );
412 }
413
414 //draw everything in an image
415 mWidgetImage->fill( Qt::transparent );
416 QPainter imagePainter( mWidgetImage );
417 imagePainter.setRenderHint( QPainter::Antialiasing );
418
419 if ( mWheelDirty )
420 {
421 //need to redraw the wheel image
422 createWheel();
423 }
424
425 //draw wheel centered on widget
426 const QPointF center = QPointF( width() / 2.0, height() / 2.0 );
427 imagePainter.drawImage( QPointF( center.x() - ( mWheelImage->width() / 2.0 ), center.y() - ( mWheelImage->height() / 2.0 ) ), *mWheelImage );
428
429 //draw hue marker
430 const int h = hue();
431 const double length = mWheelImage->width() / 2.0;
432 QLineF hueMarkerLine = QLineF( center.x(), center.y(), center.x() + length, center.y() );
433 hueMarkerLine.setAngle( h );
434 imagePainter.save();
435 //use sourceIn mode for nicer antialiasing
436 imagePainter.setCompositionMode( QPainter::CompositionMode_SourceIn );
437 QPen pen;
438 pen.setWidth( 2 );
439 //adapt pen color for hue
440 pen.setColor( h > 20 && h < 200 ? Qt::black : Qt::white );
441 imagePainter.setPen( pen );
442 imagePainter.drawLine( hueMarkerLine );
443 imagePainter.restore();
444
445 //draw triangle
446 if ( mTriangleDirty )
447 {
448 createTriangle();
449 }
450 imagePainter.drawImage( QPointF( center.x() - ( mWheelImage->width() / 2.0 ), center.y() - ( mWheelImage->height() / 2.0 ) ), *mTriangleImage );
451
452 //draw current color marker
453 const double triangleRadius = length - mWheelThickness - 1;
454
455 //adapted from equations at https://github.com/timjb/colortriangle/blob/master/colortriangle.js by Tim Baumann
456 const double lightness = mCurrentColor.lightnessF();
457 const double hueRadians = ( h * M_PI / 180.0 );
458 const double hx = std::cos( hueRadians ) * triangleRadius;
459 const double hy = -std::sin( hueRadians ) * triangleRadius;
460 const double sx = -std::cos( -hueRadians + ( M_PI / 3.0 ) ) * triangleRadius;
461 const double sy = -std::sin( -hueRadians + ( M_PI / 3.0 ) ) * triangleRadius;
462 const double vx = -std::cos( hueRadians + ( M_PI / 3.0 ) ) * triangleRadius;
463 const double vy = std::sin( hueRadians + ( M_PI / 3.0 ) ) * triangleRadius;
464 const double mx = ( sx + vx ) / 2.0;
465 const double my = ( sy + vy ) / 2.0;
466
467 const double a = ( 1 - 2.0 * std::fabs( lightness - 0.5 ) ) * mCurrentColor.hslSaturationF();
468 const double x = sx + ( vx - sx ) * lightness + ( hx - mx ) * a;
469 const double y = sy + ( vy - sy ) * lightness + ( hy - my ) * a;
470
471 //adapt pen color for lightness
472 pen.setColor( lightness > 0.7 ? Qt::black : Qt::white );
473 imagePainter.setPen( pen );
474 imagePainter.setBrush( Qt::NoBrush );
475 imagePainter.drawEllipse( QPointF( x + center.x(), y + center.y() ), 4.0, 4.0 );
476 imagePainter.end();
477
478 //draw image onto widget
479 painter.drawImage( QPoint( 0, 0 ), *mWidgetImage );
480 painter.end();
481 }
482
setColor(const QColor & color,const bool emitSignals)483 void QgsColorWheel::setColor( const QColor &color, const bool emitSignals )
484 {
485 if ( color.hue() >= 0 && color.hue() != hue() )
486 {
487 //hue has changed, need to redraw the triangle
488 mTriangleDirty = true;
489 }
490
491 QgsColorWidget::setColor( color, emitSignals );
492 }
493
createImages(const QSizeF size)494 void QgsColorWheel::createImages( const QSizeF size )
495 {
496 const double wheelSize = std::min( size.width(), size.height() ) - mMargin * 2.0;
497 mWheelThickness = wheelSize / 15.0;
498
499 //recreate cache images at correct size
500 delete mWheelImage;
501 mWheelImage = new QImage( wheelSize, wheelSize, QImage::Format_ARGB32 );
502 delete mTriangleImage;
503 mTriangleImage = new QImage( wheelSize, wheelSize, QImage::Format_ARGB32 );
504 delete mWidgetImage;
505 mWidgetImage = new QImage( size.width(), size.height(), QImage::Format_ARGB32 );
506
507 //trigger a redraw for the images
508 mWheelDirty = true;
509 mTriangleDirty = true;
510 }
511
resizeEvent(QResizeEvent * event)512 void QgsColorWheel::resizeEvent( QResizeEvent *event )
513 {
514 //recreate images for new size
515 createImages( event->size() );
516 QgsColorWidget::resizeEvent( event );
517 }
518
setColorFromPos(const QPointF pos)519 void QgsColorWheel::setColorFromPos( const QPointF pos )
520 {
521 const QPointF center = QPointF( width() / 2.0, height() / 2.0 );
522 //line from center to mouse position
523 const QLineF line = QLineF( center.x(), center.y(), pos.x(), pos.y() );
524
525 QColor newColor = QColor();
526
527 int h, s, l, alpha;
528 mCurrentColor.getHsl( &h, &s, &l, &alpha );
529 //override hue with explicit hue, so we don't get -1 values from QColor for hue
530 h = hue();
531
532 if ( mClickedPart == QgsColorWheel::Triangle )
533 {
534 //adapted from equations at https://github.com/timjb/colortriangle/blob/master/colortriangle.js by Tim Baumann
535
536 //position of event relative to triangle center
537 const double x = pos.x() - center.x();
538 const double y = pos.y() - center.y();
539
540 double eventAngleRadians = line.angle() * M_PI / 180.0;
541 const double hueRadians = h * M_PI / 180.0;
542 double rad0 = std::fmod( eventAngleRadians + 2.0 * M_PI - hueRadians, 2.0 * M_PI );
543 double rad1 = std::fmod( rad0, ( ( 2.0 / 3.0 ) * M_PI ) ) - ( M_PI / 3.0 );
544 const double length = mWheelImage->width() / 2.0;
545 const double triangleLength = length - mWheelThickness - 1;
546
547 const double a = 0.5 * triangleLength;
548 double b = std::tan( rad1 ) * a;
549 double r = std::sqrt( x * x + y * y );
550 const double maxR = std::sqrt( a * a + b * b );
551
552 if ( r > maxR )
553 {
554 const double dx = std::tan( rad1 ) * r;
555 double rad2 = std::atan( dx / maxR );
556 rad2 = std::min( rad2, M_PI / 3.0 );
557 rad2 = std::max( rad2, -M_PI / 3.0 );
558 eventAngleRadians += rad2 - rad1;
559 rad0 = std::fmod( eventAngleRadians + 2.0 * M_PI - hueRadians, 2.0 * M_PI );
560 rad1 = std::fmod( rad0, ( ( 2.0 / 3.0 ) * M_PI ) ) - ( M_PI / 3.0 );
561 b = std::tan( rad1 ) * a;
562 r = std::sqrt( a * a + b * b );
563 }
564
565 const double triangleSideLength = std::sqrt( 3.0 ) * triangleLength;
566 const double newL = ( ( -std::sin( rad0 ) * r ) / triangleSideLength ) + 0.5;
567 const double widthShare = 1.0 - ( std::fabs( newL - 0.5 ) * 2.0 );
568 const double newS = ( ( ( std::cos( rad0 ) * r ) + ( triangleLength / 2.0 ) ) / ( 1.5 * triangleLength ) ) / widthShare;
569 s = std::min( static_cast< int >( std::round( std::max( 0.0, newS ) * 255.0 ) ), 255 );
570 l = std::min( static_cast< int >( std::round( std::max( 0.0, newL ) * 255.0 ) ), 255 );
571 newColor = QColor::fromHsl( h, s, l );
572 //explicitly set the hue again, so that it's exact
573 newColor.setHsv( h, newColor.hsvSaturation(), newColor.value(), alpha );
574 }
575 else if ( mClickedPart == QgsColorWheel::Wheel )
576 {
577 //use hue angle
578 s = mCurrentColor.hsvSaturation();
579 const int v = mCurrentColor.value();
580 const int newHue = line.angle();
581 newColor = QColor::fromHsv( newHue, s, v, alpha );
582 //hue has changed, need to redraw triangle
583 mTriangleDirty = true;
584 }
585
586 if ( newColor.isValid() && newColor != mCurrentColor )
587 {
588 //color has changed
589 mCurrentColor = QColor( newColor );
590
591 if ( mCurrentColor.hue() >= 0 )
592 {
593 //color has a valid hue, so update the QgsColorWidget's explicit hue
594 mExplicitHue = mCurrentColor.hue();
595 }
596
597 update();
598 emit colorChanged( mCurrentColor );
599 }
600 }
601
mouseMoveEvent(QMouseEvent * event)602 void QgsColorWheel::mouseMoveEvent( QMouseEvent *event )
603 {
604 setColorFromPos( event->pos() );
605 QgsColorWidget::mouseMoveEvent( event );
606 }
607
mousePressEvent(QMouseEvent * event)608 void QgsColorWheel::mousePressEvent( QMouseEvent *event )
609 {
610 //calculate where the event occurred -- on the wheel or inside the triangle?
611
612 //create a line from the widget's center to the event
613 const QLineF line = QLineF( width() / 2.0, height() / 2.0, event->pos().x(), event->pos().y() );
614
615 const double innerLength = mWheelImage->width() / 2.0 - mWheelThickness;
616 if ( line.length() < innerLength )
617 {
618 mClickedPart = QgsColorWheel::Triangle;
619 }
620 else
621 {
622 mClickedPart = QgsColorWheel::Wheel;
623 }
624 setColorFromPos( event->pos() );
625 }
626
mouseReleaseEvent(QMouseEvent * event)627 void QgsColorWheel::mouseReleaseEvent( QMouseEvent *event )
628 {
629 Q_UNUSED( event )
630 mClickedPart = QgsColorWheel::None;
631 }
632
createWheel()633 void QgsColorWheel::createWheel()
634 {
635 if ( !mWheelImage )
636 {
637 return;
638 }
639
640 const int maxSize = std::min( mWheelImage->width(), mWheelImage->height() );
641 const double wheelRadius = maxSize / 2.0;
642
643 mWheelImage->fill( Qt::transparent );
644 QPainter p( mWheelImage );
645 p.setRenderHint( QPainter::Antialiasing );
646 p.setBrush( mWheelBrush );
647 p.setPen( Qt::NoPen );
648
649 //draw hue wheel as a circle
650 p.translate( wheelRadius, wheelRadius );
651 p.drawEllipse( QPointF( 0.0, 0.0 ), wheelRadius, wheelRadius );
652
653 //cut hole in center of circle to make a ring
654 p.setCompositionMode( QPainter::CompositionMode_DestinationOut );
655 p.setBrush( QBrush( Qt::black ) );
656 p.drawEllipse( QPointF( 0.0, 0.0 ), wheelRadius - mWheelThickness, wheelRadius - mWheelThickness );
657 p.end();
658
659 mWheelDirty = false;
660 }
661
createTriangle()662 void QgsColorWheel::createTriangle()
663 {
664 if ( !mWheelImage || !mTriangleImage )
665 {
666 return;
667 }
668
669 const QPointF center = QPointF( mWheelImage->width() / 2.0, mWheelImage->height() / 2.0 );
670 mTriangleImage->fill( Qt::transparent );
671
672 QPainter imagePainter( mTriangleImage );
673 imagePainter.setRenderHint( QPainter::Antialiasing );
674
675 const int angle = hue();
676 const double wheelRadius = mWheelImage->width() / 2.0;
677 const double triangleRadius = wheelRadius - mWheelThickness - 1;
678
679 //pure version of hue (at full saturation and value)
680 const QColor pureColor = QColor::fromHsv( angle, 255, 255 );
681 //create copy of color but with 0 alpha
682 QColor alphaColor = QColor( pureColor );
683 alphaColor.setAlpha( 0 );
684
685 //some rather ugly shortcuts to obtain corners and midpoints of triangle
686 QLineF line1 = QLineF( center.x(), center.y(), center.x() - triangleRadius * std::cos( M_PI / 3.0 ), center.y() - triangleRadius * std::sin( M_PI / 3.0 ) );
687 QLineF line2 = QLineF( center.x(), center.y(), center.x() + triangleRadius, center.y() );
688 QLineF line3 = QLineF( center.x(), center.y(), center.x() - triangleRadius * std::cos( M_PI / 3.0 ), center.y() + triangleRadius * std::sin( M_PI / 3.0 ) );
689 QLineF line4 = QLineF( center.x(), center.y(), center.x() - triangleRadius * std::cos( M_PI / 3.0 ), center.y() );
690 QLineF line5 = QLineF( center.x(), center.y(), ( line2.p2().x() + line1.p2().x() ) / 2.0, ( line2.p2().y() + line1.p2().y() ) / 2.0 );
691 line1.setAngle( line1.angle() + angle );
692 line2.setAngle( line2.angle() + angle );
693 line3.setAngle( line3.angle() + angle );
694 line4.setAngle( line4.angle() + angle );
695 line5.setAngle( line5.angle() + angle );
696 const QPointF p1 = line1.p2();
697 const QPointF p2 = line2.p2();
698 const QPointF p3 = line3.p2();
699 const QPointF p4 = line4.p2();
700 const QPointF p5 = line5.p2();
701
702 //inspired by Tim Baumann's work at https://github.com/timjb/colortriangle/blob/master/colortriangle.js
703 QLinearGradient colorGrad = QLinearGradient( p4.x(), p4.y(), p2.x(), p2.y() );
704 colorGrad.setColorAt( 0, alphaColor );
705 colorGrad.setColorAt( 1, pureColor );
706 QLinearGradient whiteGrad = QLinearGradient( p3.x(), p3.y(), p5.x(), p5.y() );
707 whiteGrad.setColorAt( 0, QColor( 255, 255, 255, 255 ) );
708 whiteGrad.setColorAt( 1, QColor( 255, 255, 255, 0 ) );
709
710 QPolygonF triangle;
711 triangle << p2 << p1 << p3 << p2;
712 imagePainter.setPen( Qt::NoPen );
713 //start with a black triangle
714 imagePainter.setBrush( QBrush( Qt::black ) );
715 imagePainter.drawPolygon( triangle );
716 //draw a gradient from transparent to the pure color at the triangle's tip
717 imagePainter.setBrush( QBrush( colorGrad ) );
718 imagePainter.drawPolygon( triangle );
719 //draw a white gradient using additive composition mode
720 imagePainter.setCompositionMode( QPainter::CompositionMode_Plus );
721 imagePainter.setBrush( QBrush( whiteGrad ) );
722 imagePainter.drawPolygon( triangle );
723
724 //above process results in some small artifacts on the edge of the triangle. Let's clear these up
725 //use source composition mode and draw an outline using a transparent pen
726 //this clears the edge pixels and leaves a nice smooth image
727 imagePainter.setCompositionMode( QPainter::CompositionMode_Source );
728 imagePainter.setBrush( Qt::NoBrush );
729 imagePainter.setPen( QPen( Qt::transparent ) );
730 imagePainter.drawPolygon( triangle );
731
732 imagePainter.end();
733 mTriangleDirty = false;
734 }
735
736
737
738 //
739 // QgsColorBox
740 //
741
QgsColorBox(QWidget * parent,const ColorComponent component)742 QgsColorBox::QgsColorBox( QWidget *parent, const ColorComponent component )
743 : QgsColorWidget( parent, component )
744 {
745 setFocusPolicy( Qt::StrongFocus );
746 setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding );
747
748 mBoxImage = new QImage( width() - mMargin * 2, height() - mMargin * 2, QImage::Format_RGB32 );
749 }
750
~QgsColorBox()751 QgsColorBox::~QgsColorBox()
752 {
753 delete mBoxImage;
754 }
755
sizeHint() const756 QSize QgsColorBox::sizeHint() const
757 {
758 const int size = Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 22;
759 return QSize( size, size );
760 }
761
paintEvent(QPaintEvent * event)762 void QgsColorBox::paintEvent( QPaintEvent *event )
763 {
764 Q_UNUSED( event )
765 QPainter painter( this );
766
767 QStyleOptionFrame option;
768 option.initFrom( this );
769 option.state = hasFocus() ? QStyle::State_Active : QStyle::State_None;
770 style()->drawPrimitive( QStyle::PE_Frame, &option, &painter );
771
772 if ( mDirty )
773 {
774 createBox();
775 }
776
777 //draw background image
778 painter.drawImage( QPoint( mMargin, mMargin ), *mBoxImage );
779
780 //draw cross lines
781 const double xPos = mMargin + ( width() - 2 * mMargin - 1 ) * static_cast<double>( xComponentValue() ) / static_cast<double>( valueRangeX() );
782 const double yPos = mMargin + ( height() - 2 * mMargin - 1 ) - ( height() - 2 * mMargin - 1 ) * static_cast<double>( yComponentValue() ) / static_cast<double>( valueRangeY() );
783
784 painter.setBrush( Qt::white );
785 painter.setPen( Qt::NoPen );
786
787 painter.drawRect( xPos - 1, mMargin, 3, height() - 2 * mMargin - 1 );
788 painter.drawRect( mMargin, yPos - 1, width() - 2 * mMargin - 1, 3 );
789 painter.setPen( Qt::black );
790 painter.drawLine( xPos, mMargin, xPos, height() - mMargin - 1 );
791 painter.drawLine( mMargin, yPos, width() - mMargin - 1, yPos );
792
793 painter.end();
794 }
795
setComponent(const QgsColorWidget::ColorComponent component)796 void QgsColorBox::setComponent( const QgsColorWidget::ColorComponent component )
797 {
798 if ( component != mComponent )
799 {
800 //need to redraw
801 mDirty = true;
802 }
803 QgsColorWidget::setComponent( component );
804 }
805
setColor(const QColor & color,const bool emitSignals)806 void QgsColorBox::setColor( const QColor &color, const bool emitSignals )
807 {
808 //check if we need to redraw the box image
809 if ( mComponent == QgsColorWidget::Red && mCurrentColor.red() != color.red() )
810 {
811 mDirty = true;
812 }
813 else if ( mComponent == QgsColorWidget::Green && mCurrentColor.green() != color.green() )
814 {
815 mDirty = true;
816 }
817 else if ( mComponent == QgsColorWidget::Blue && mCurrentColor.blue() != color.blue() )
818 {
819 mDirty = true;
820 }
821 else if ( mComponent == QgsColorWidget::Hue && color.hsvHue() >= 0 && hue() != color.hsvHue() )
822 {
823 mDirty = true;
824 }
825 else if ( mComponent == QgsColorWidget::Saturation && mCurrentColor.hsvSaturation() != color.hsvSaturation() )
826 {
827 mDirty = true;
828 }
829 else if ( mComponent == QgsColorWidget::Value && mCurrentColor.value() != color.value() )
830 {
831 mDirty = true;
832 }
833 QgsColorWidget::setColor( color, emitSignals );
834 }
835
resizeEvent(QResizeEvent * event)836 void QgsColorBox::resizeEvent( QResizeEvent *event )
837 {
838 mDirty = true;
839 delete mBoxImage;
840 mBoxImage = new QImage( event->size().width() - mMargin * 2, event->size().height() - mMargin * 2, QImage::Format_RGB32 );
841 QgsColorWidget::resizeEvent( event );
842 }
843
mouseMoveEvent(QMouseEvent * event)844 void QgsColorBox::mouseMoveEvent( QMouseEvent *event )
845 {
846 setColorFromPoint( event->pos() );
847 QgsColorWidget::mouseMoveEvent( event );
848 }
849
mousePressEvent(QMouseEvent * event)850 void QgsColorBox::mousePressEvent( QMouseEvent *event )
851 {
852 setColorFromPoint( event->pos() );
853 }
854
createBox()855 void QgsColorBox::createBox()
856 {
857 const int maxValueX = mBoxImage->width();
858 const int maxValueY = mBoxImage->height();
859
860 //create a temporary color object
861 QColor currentColor = QColor( mCurrentColor );
862 int colorComponentValue;
863
864 for ( int y = 0; y < maxValueY; ++y )
865 {
866 QRgb *scanLine = ( QRgb * )mBoxImage->scanLine( y );
867
868 colorComponentValue = int( valueRangeY() - valueRangeY() * ( double( y ) / maxValueY ) );
869 alterColor( currentColor, yComponent(), colorComponentValue );
870 for ( int x = 0; x < maxValueX; ++x )
871 {
872 colorComponentValue = int( valueRangeX() * ( double( x ) / maxValueX ) );
873 alterColor( currentColor, xComponent(), colorComponentValue );
874 scanLine[x] = currentColor.rgb();
875 }
876 }
877 mDirty = false;
878 }
879
valueRangeX() const880 int QgsColorBox::valueRangeX() const
881 {
882 return componentRange( xComponent() );
883 }
884
valueRangeY() const885 int QgsColorBox::valueRangeY() const
886 {
887 return componentRange( yComponent() );
888 }
889
yComponent() const890 QgsColorWidget::ColorComponent QgsColorBox::yComponent() const
891 {
892 switch ( mComponent )
893 {
894 case QgsColorWidget::Red:
895 return QgsColorWidget::Green;
896 case QgsColorWidget:: Green:
897 case QgsColorWidget:: Blue:
898 return QgsColorWidget::Red;
899 case QgsColorWidget::Hue:
900 return QgsColorWidget:: Saturation;
901 case QgsColorWidget:: Saturation:
902 case QgsColorWidget:: Value:
903 return QgsColorWidget::Hue;
904 default:
905 //should not occur
906 return QgsColorWidget::Red;
907 }
908 }
909
yComponentValue() const910 int QgsColorBox::yComponentValue() const
911 {
912 return componentValue( yComponent() );
913 }
914
xComponent() const915 QgsColorWidget::ColorComponent QgsColorBox::xComponent() const
916 {
917 switch ( mComponent )
918 {
919 case QgsColorWidget::Red:
920 case QgsColorWidget:: Green:
921 return QgsColorWidget::Blue;
922 case QgsColorWidget:: Blue:
923 return QgsColorWidget::Green;
924 case QgsColorWidget::Hue:
925 case QgsColorWidget:: Saturation:
926 return QgsColorWidget:: Value;
927 case QgsColorWidget:: Value:
928 return QgsColorWidget::Saturation;
929 default:
930 //should not occur
931 return QgsColorWidget::Red;
932 }
933 }
934
xComponentValue() const935 int QgsColorBox::xComponentValue() const
936 {
937 return componentValue( xComponent() );
938 }
939
setColorFromPoint(QPoint point)940 void QgsColorBox::setColorFromPoint( QPoint point )
941 {
942 int valX = valueRangeX() * ( point.x() - mMargin ) / ( width() - 2 * mMargin - 1 );
943 valX = std::min( std::max( valX, 0 ), valueRangeX() );
944
945 int valY = valueRangeY() - valueRangeY() * ( point.y() - mMargin ) / ( height() - 2 * mMargin - 1 );
946 valY = std::min( std::max( valY, 0 ), valueRangeY() );
947
948 QColor color = QColor( mCurrentColor );
949 alterColor( color, xComponent(), valX );
950 alterColor( color, yComponent(), valY );
951
952 if ( color == mCurrentColor )
953 {
954 return;
955 }
956
957 if ( color.hue() >= 0 )
958 {
959 mExplicitHue = color.hue();
960 }
961
962 mCurrentColor = color;
963 update();
964 emit colorChanged( color );
965 }
966
967
968 //
969 // QgsColorRampWidget
970 //
971
QgsColorRampWidget(QWidget * parent,const QgsColorWidget::ColorComponent component,const Orientation orientation)972 QgsColorRampWidget::QgsColorRampWidget( QWidget *parent,
973 const QgsColorWidget::ColorComponent component,
974 const Orientation orientation )
975 : QgsColorWidget( parent, component )
976 {
977 setFocusPolicy( Qt::StrongFocus );
978 setOrientation( orientation );
979
980 //create triangle polygons
981 setMarkerSize( 5 );
982 }
983
sizeHint() const984 QSize QgsColorRampWidget::sizeHint() const
985 {
986 if ( mOrientation == QgsColorRampWidget::Horizontal )
987 {
988 //horizontal
989 return QSize( Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 22, Qgis::UI_SCALE_FACTOR * fontMetrics().height() * 1.3 );
990 }
991 else
992 {
993 //vertical
994 return QSize( Qgis::UI_SCALE_FACTOR * fontMetrics().height() * 1.3, Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 22 );
995 }
996 }
997
paintEvent(QPaintEvent * event)998 void QgsColorRampWidget::paintEvent( QPaintEvent *event )
999 {
1000 Q_UNUSED( event )
1001 QPainter painter( this );
1002
1003 if ( mShowFrame )
1004 {
1005 //draw frame
1006 QStyleOptionFrame option;
1007 option.initFrom( this );
1008 option.state = hasFocus() ? QStyle::State_KeyboardFocusChange : QStyle::State_None;
1009 style()->drawPrimitive( QStyle::PE_Frame, &option, &painter );
1010 }
1011
1012 if ( hasFocus() )
1013 {
1014 //draw focus rect
1015 QStyleOptionFocusRect option;
1016 option.initFrom( this );
1017 option.state = QStyle::State_KeyboardFocusChange;
1018 style()->drawPrimitive( QStyle::PE_FrameFocusRect, &option, &painter );
1019 }
1020
1021 if ( mComponent != QgsColorWidget::Alpha )
1022 {
1023 const int maxValue = ( mOrientation == QgsColorRampWidget::Horizontal ? width() : height() ) - 1 - 2 * mMargin;
1024 QColor color = QColor( mCurrentColor );
1025 color.setAlpha( 255 );
1026 QPen pen;
1027 // we need to set pen width to 1,
1028 // since on retina displays
1029 // pen.setWidth(0) <=> pen.width = 0.5
1030 // see https://github.com/qgis/QGIS/issues/23900
1031 pen.setWidth( 1 );
1032 painter.setPen( pen );
1033 painter.setBrush( Qt::NoBrush );
1034
1035 //draw background ramp
1036 for ( int c = 0; c <= maxValue; ++c )
1037 {
1038 int colorVal = static_cast<int>( componentRange() * static_cast<double>( c ) / maxValue );
1039 //vertical sliders are reversed
1040 if ( mOrientation == QgsColorRampWidget::Vertical )
1041 {
1042 colorVal = componentRange() - colorVal;
1043 }
1044 alterColor( color, mComponent, colorVal );
1045 if ( color.hue() < 0 )
1046 {
1047 color.setHsv( hue(), color.saturation(), color.value() );
1048 }
1049 pen.setColor( color );
1050 painter.setPen( pen );
1051 if ( mOrientation == QgsColorRampWidget::Horizontal )
1052 {
1053 //horizontal
1054 painter.drawLine( QLineF( c + mMargin, mMargin, c + mMargin, height() - mMargin - 1 ) );
1055 }
1056 else
1057 {
1058 //vertical
1059 painter.drawLine( QLineF( mMargin, c + mMargin, width() - mMargin - 1, c + mMargin ) );
1060 }
1061 }
1062 }
1063 else
1064 {
1065 //alpha ramps are drawn differently
1066 //start with the checkboard pattern
1067 const QBrush checkBrush = QBrush( transparentBackground() );
1068 painter.setBrush( checkBrush );
1069 painter.setPen( Qt::NoPen );
1070 painter.drawRect( QRectF( mMargin, mMargin, width() - 2 * mMargin - 1, height() - 2 * mMargin - 1 ) );
1071 QLinearGradient colorGrad;
1072 if ( mOrientation == QgsColorRampWidget::Horizontal )
1073 {
1074 //horizontal
1075 colorGrad = QLinearGradient( mMargin, 0, width() - mMargin - 1, 0 );
1076 }
1077 else
1078 {
1079 //vertical
1080 colorGrad = QLinearGradient( 0, mMargin, 0, height() - mMargin - 1 );
1081 }
1082 QColor transparent = QColor( mCurrentColor );
1083 transparent.setAlpha( 0 );
1084 colorGrad.setColorAt( 0, transparent );
1085 QColor opaque = QColor( mCurrentColor );
1086 opaque.setAlpha( 255 );
1087 colorGrad.setColorAt( 1, opaque );
1088 const QBrush colorBrush = QBrush( colorGrad );
1089 painter.setBrush( colorBrush );
1090 painter.drawRect( QRectF( mMargin, mMargin, width() - 2 * mMargin - 1, height() - 2 * mMargin - 1 ) );
1091 }
1092
1093 if ( mOrientation == QgsColorRampWidget::Horizontal )
1094 {
1095 //draw marker triangles for horizontal ramps
1096 painter.setRenderHint( QPainter::Antialiasing );
1097 painter.setBrush( QBrush( Qt::black ) );
1098 painter.setPen( Qt::NoPen );
1099 painter.translate( mMargin + ( width() - 2 * mMargin ) * static_cast<double>( componentValue() ) / componentRange(), mMargin - 1 );
1100 painter.drawPolygon( mTopTriangle );
1101 painter.translate( 0, height() - mMargin - 2 );
1102 painter.setBrush( QBrush( Qt::white ) );
1103 painter.drawPolygon( mBottomTriangle );
1104 painter.end();
1105 }
1106 else
1107 {
1108 //draw cross lines for vertical ramps
1109 const double ypos = mMargin + ( height() - 2 * mMargin - 1 ) - ( height() - 2 * mMargin - 1 ) * static_cast<double>( componentValue() ) / componentRange();
1110 painter.setBrush( Qt::white );
1111 painter.setPen( Qt::NoPen );
1112 painter.drawRect( QRectF( mMargin, ypos - 1, width() - 2 * mMargin - 1, 3 ) );
1113 painter.setPen( Qt::black );
1114 painter.drawLine( QLineF( mMargin, ypos, width() - mMargin - 1, ypos ) );
1115 }
1116 }
1117
setOrientation(const QgsColorRampWidget::Orientation orientation)1118 void QgsColorRampWidget::setOrientation( const QgsColorRampWidget::Orientation orientation )
1119 {
1120 mOrientation = orientation;
1121 if ( orientation == QgsColorRampWidget::Horizontal )
1122 {
1123 //horizontal
1124 setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Fixed );
1125 }
1126 else
1127 {
1128 //vertical
1129 setSizePolicy( QSizePolicy::Fixed, QSizePolicy::MinimumExpanding );
1130 }
1131 updateGeometry();
1132 }
1133
setInteriorMargin(const int margin)1134 void QgsColorRampWidget::setInteriorMargin( const int margin )
1135 {
1136 if ( margin == mMargin )
1137 {
1138 return;
1139 }
1140 mMargin = margin;
1141 update();
1142 }
1143
setShowFrame(const bool showFrame)1144 void QgsColorRampWidget::setShowFrame( const bool showFrame )
1145 {
1146 if ( showFrame == mShowFrame )
1147 {
1148 return;
1149 }
1150 mShowFrame = showFrame;
1151 update();
1152 }
1153
setMarkerSize(const int markerSize)1154 void QgsColorRampWidget::setMarkerSize( const int markerSize )
1155 {
1156 //create triangle polygons
1157 mTopTriangle << QPoint( -markerSize, 0 ) << QPoint( markerSize, 0 ) << QPoint( 0, markerSize );
1158 mBottomTriangle << QPoint( -markerSize, 0 ) << QPoint( markerSize, 0 ) << QPoint( 0, -markerSize );
1159 update();
1160 }
1161
mouseMoveEvent(QMouseEvent * event)1162 void QgsColorRampWidget::mouseMoveEvent( QMouseEvent *event )
1163 {
1164 setColorFromPoint( event->pos() );
1165 QgsColorWidget::mouseMoveEvent( event );
1166 }
1167
wheelEvent(QWheelEvent * event)1168 void QgsColorRampWidget::wheelEvent( QWheelEvent *event )
1169 {
1170 const int oldValue = componentValue();
1171
1172 if ( event->angleDelta().y() > 0 )
1173 {
1174 setComponentValue( componentValue() + 1 );
1175 }
1176 else
1177 {
1178 setComponentValue( componentValue() - 1 );
1179 }
1180
1181 if ( componentValue() != oldValue )
1182 {
1183 //value has changed
1184 emit colorChanged( mCurrentColor );
1185 emit valueChanged( componentValue() );
1186 }
1187
1188 event->accept();
1189 }
1190
mousePressEvent(QMouseEvent * event)1191 void QgsColorRampWidget::mousePressEvent( QMouseEvent *event )
1192 {
1193 setColorFromPoint( event->pos() );
1194 }
1195
keyPressEvent(QKeyEvent * event)1196 void QgsColorRampWidget::keyPressEvent( QKeyEvent *event )
1197 {
1198 const int oldValue = componentValue();
1199 if ( ( mOrientation == QgsColorRampWidget::Horizontal && ( event->key() == Qt::Key_Right || event->key() == Qt::Key_Up ) )
1200 || ( mOrientation == QgsColorRampWidget::Vertical && ( event->key() == Qt::Key_Left || event->key() == Qt::Key_Up ) ) )
1201 {
1202 setComponentValue( componentValue() + 1 );
1203 }
1204 else if ( ( mOrientation == QgsColorRampWidget::Horizontal && ( event->key() == Qt::Key_Left || event->key() == Qt::Key_Down ) )
1205 || ( mOrientation == QgsColorRampWidget::Vertical && ( event->key() == Qt::Key_Right || event->key() == Qt::Key_Down ) ) )
1206 {
1207 setComponentValue( componentValue() - 1 );
1208 }
1209 else if ( ( mOrientation == QgsColorRampWidget::Horizontal && event->key() == Qt::Key_PageDown )
1210 || ( mOrientation == QgsColorRampWidget::Vertical && event->key() == Qt::Key_PageUp ) )
1211 {
1212 setComponentValue( componentValue() + 10 );
1213 }
1214 else if ( ( mOrientation == QgsColorRampWidget::Horizontal && event->key() == Qt::Key_PageUp )
1215 || ( mOrientation == QgsColorRampWidget::Vertical && event->key() == Qt::Key_PageDown ) )
1216 {
1217 setComponentValue( componentValue() - 10 );
1218 }
1219 else if ( ( mOrientation == QgsColorRampWidget::Horizontal && event->key() == Qt::Key_Home )
1220 || ( mOrientation == QgsColorRampWidget::Vertical && event->key() == Qt::Key_End ) )
1221 {
1222 setComponentValue( 0 );
1223 }
1224 else if ( ( mOrientation == QgsColorRampWidget::Horizontal && event->key() == Qt::Key_End )
1225 || ( mOrientation == QgsColorRampWidget::Vertical && event->key() == Qt::Key_Home ) )
1226 {
1227 //set to maximum value
1228 setComponentValue( componentRange() );
1229 }
1230 else
1231 {
1232 QgsColorWidget::keyPressEvent( event );
1233 return;
1234 }
1235
1236 if ( componentValue() != oldValue )
1237 {
1238 //value has changed
1239 emit colorChanged( mCurrentColor );
1240 emit valueChanged( componentValue() );
1241 }
1242 }
1243
setColorFromPoint(QPointF point)1244 void QgsColorRampWidget::setColorFromPoint( QPointF point )
1245 {
1246 const int oldValue = componentValue();
1247 int val;
1248 if ( mOrientation == QgsColorRampWidget::Horizontal )
1249 {
1250 val = componentRange() * ( point.x() - mMargin ) / ( width() - 2 * mMargin );
1251 }
1252 else
1253 {
1254 val = componentRange() - componentRange() * ( point.y() - mMargin ) / ( height() - 2 * mMargin );
1255 }
1256 val = std::max( 0, std::min( val, componentRange() ) );
1257 setComponentValue( val );
1258
1259 if ( componentValue() != oldValue )
1260 {
1261 //value has changed
1262 emit colorChanged( mCurrentColor );
1263 emit valueChanged( componentValue() );
1264 }
1265 }
1266
1267 //
1268 // QgsColorSliderWidget
1269 //
1270
QgsColorSliderWidget(QWidget * parent,const ColorComponent component)1271 QgsColorSliderWidget::QgsColorSliderWidget( QWidget *parent, const ColorComponent component )
1272 : QgsColorWidget( parent, component )
1273
1274 {
1275 QHBoxLayout *hLayout = new QHBoxLayout();
1276 hLayout->setContentsMargins( 0, 0, 0, 0 );
1277 hLayout->setSpacing( 5 );
1278
1279 mRampWidget = new QgsColorRampWidget( nullptr, component );
1280 mRampWidget->setColor( mCurrentColor );
1281 hLayout->addWidget( mRampWidget, 1 );
1282
1283 mSpinBox = new QSpinBox();
1284 //set spinbox to a reasonable width
1285 const int largestCharWidth = mSpinBox->fontMetrics().horizontalAdvance( QStringLiteral( "888%" ) );
1286 mSpinBox->setMinimumWidth( largestCharWidth + 35 );
1287 mSpinBox->setMinimum( 0 );
1288 mSpinBox->setMaximum( convertRealToDisplay( componentRange() ) );
1289 mSpinBox->setValue( convertRealToDisplay( componentValue() ) );
1290 if ( component == QgsColorWidget::Hue )
1291 {
1292 //degrees suffix for hue
1293 mSpinBox->setSuffix( QChar( 176 ) );
1294 }
1295 else if ( component == QgsColorWidget::Saturation || component == QgsColorWidget::Value || component == QgsColorWidget::Alpha )
1296 {
1297 mSpinBox->setSuffix( tr( "%" ) );
1298 }
1299 hLayout->addWidget( mSpinBox );
1300 setLayout( hLayout );
1301
1302 connect( mRampWidget, &QgsColorRampWidget::valueChanged, this, &QgsColorSliderWidget::rampChanged );
1303 connect( mRampWidget, &QgsColorWidget::colorChanged, this, &QgsColorSliderWidget::rampColorChanged );
1304 connect( mSpinBox, static_cast < void ( QSpinBox::* )( int ) > ( &QSpinBox::valueChanged ), this, &QgsColorSliderWidget::spinChanged );
1305 }
1306
setComponent(const QgsColorWidget::ColorComponent component)1307 void QgsColorSliderWidget::setComponent( const QgsColorWidget::ColorComponent component )
1308 {
1309 QgsColorWidget::setComponent( component );
1310 mRampWidget->setComponent( component );
1311 mSpinBox->setMaximum( convertRealToDisplay( componentRange() ) );
1312 if ( component == QgsColorWidget::Hue )
1313 {
1314 //degrees suffix for hue
1315 mSpinBox->setSuffix( QChar( 176 ) );
1316 }
1317 else if ( component == QgsColorWidget::Saturation || component == QgsColorWidget::Value || component == QgsColorWidget::Alpha )
1318 {
1319 //saturation, value and alpha are in %
1320 mSpinBox->setSuffix( tr( "%" ) );
1321 }
1322 else
1323 {
1324 //clear suffix
1325 mSpinBox->setSuffix( QString() );
1326 }
1327 }
1328
setComponentValue(const int value)1329 void QgsColorSliderWidget::setComponentValue( const int value )
1330 {
1331 QgsColorWidget::setComponentValue( value );
1332 mRampWidget->blockSignals( true );
1333 mRampWidget->setComponentValue( value );
1334 mRampWidget->blockSignals( false );
1335 mSpinBox->blockSignals( true );
1336 mSpinBox->setValue( convertRealToDisplay( value ) );
1337 mSpinBox->blockSignals( false );
1338 }
1339
setColor(const QColor & color,bool emitSignals)1340 void QgsColorSliderWidget::setColor( const QColor &color, bool emitSignals )
1341 {
1342 QgsColorWidget::setColor( color, emitSignals );
1343 mRampWidget->setColor( color );
1344 mSpinBox->blockSignals( true );
1345 mSpinBox->setValue( convertRealToDisplay( componentValue() ) );
1346 mSpinBox->blockSignals( false );
1347 }
1348
rampColorChanged(const QColor & color)1349 void QgsColorSliderWidget::rampColorChanged( const QColor &color )
1350 {
1351 emit colorChanged( color );
1352 }
1353
spinChanged(int value)1354 void QgsColorSliderWidget::spinChanged( int value )
1355 {
1356 const int convertedValue = convertDisplayToReal( value );
1357 QgsColorWidget::setComponentValue( convertedValue );
1358 mRampWidget->setComponentValue( convertedValue );
1359 emit colorChanged( mCurrentColor );
1360 }
1361
rampChanged(int value)1362 void QgsColorSliderWidget::rampChanged( int value )
1363 {
1364 mSpinBox->blockSignals( true );
1365 mSpinBox->setValue( convertRealToDisplay( value ) );
1366 mSpinBox->blockSignals( false );
1367 }
1368
1369
convertRealToDisplay(const int realValue) const1370 int QgsColorSliderWidget::convertRealToDisplay( const int realValue ) const
1371 {
1372 //scale saturation, value or alpha to 0->100 range. This makes more sense for users
1373 //for whom "255" is a totally arbitrary value!
1374 if ( mComponent == QgsColorWidget::Saturation || mComponent == QgsColorWidget::Value || mComponent == QgsColorWidget::Alpha )
1375 {
1376 return std::round( 100.0 * realValue / 255.0 );
1377 }
1378
1379 //leave all other values intact
1380 return realValue;
1381 }
1382
convertDisplayToReal(const int displayValue) const1383 int QgsColorSliderWidget::convertDisplayToReal( const int displayValue ) const
1384 {
1385 //scale saturation, value or alpha from 0->100 range (see note in convertRealToDisplay)
1386 if ( mComponent == QgsColorWidget::Saturation || mComponent == QgsColorWidget::Value || mComponent == QgsColorWidget::Alpha )
1387 {
1388 return std::round( 255.0 * displayValue / 100.0 );
1389 }
1390
1391 //leave all other values intact
1392 return displayValue;
1393 }
1394
1395 //
1396 // QgsColorTextWidget
1397 //
1398
QgsColorTextWidget(QWidget * parent)1399 QgsColorTextWidget::QgsColorTextWidget( QWidget *parent )
1400 : QgsColorWidget( parent )
1401 {
1402 QHBoxLayout *hLayout = new QHBoxLayout();
1403 hLayout->setContentsMargins( 0, 0, 0, 0 );
1404 hLayout->setSpacing( 0 );
1405
1406 mLineEdit = new QLineEdit( nullptr );
1407 hLayout->addWidget( mLineEdit );
1408
1409 mMenuButton = new QToolButton( mLineEdit );
1410 mMenuButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mIconDropDownMenu.svg" ) ) );
1411 mMenuButton->setCursor( Qt::ArrowCursor );
1412 mMenuButton->setFocusPolicy( Qt::NoFocus );
1413 mMenuButton->setStyleSheet( QStringLiteral( "QToolButton { border: none; padding: 0px; }" ) );
1414
1415 setLayout( hLayout );
1416
1417 const int frameWidth = mLineEdit->style()->pixelMetric( QStyle::PM_DefaultFrameWidth );
1418 mLineEdit->setStyleSheet( QStringLiteral( "QLineEdit { padding-right: %1px; } " )
1419 .arg( mMenuButton->sizeHint().width() + frameWidth + 1 ) );
1420
1421 connect( mLineEdit, &QLineEdit::editingFinished, this, &QgsColorTextWidget::textChanged );
1422 connect( mMenuButton, &QAbstractButton::clicked, this, &QgsColorTextWidget::showMenu );
1423
1424 //restore format setting
1425 QgsSettings settings;
1426 mFormat = settings.enumValue( QStringLiteral( "ColorWidgets/textWidgetFormat" ), HexRgb );
1427
1428 updateText();
1429 }
1430
setColor(const QColor & color,const bool emitSignals)1431 void QgsColorTextWidget::setColor( const QColor &color, const bool emitSignals )
1432 {
1433 QgsColorWidget::setColor( color, emitSignals );
1434 updateText();
1435 }
1436
resizeEvent(QResizeEvent * event)1437 void QgsColorTextWidget::resizeEvent( QResizeEvent *event )
1438 {
1439 Q_UNUSED( event )
1440 const QSize sz = mMenuButton->sizeHint();
1441 const int frameWidth = style()->pixelMetric( QStyle::PM_DefaultFrameWidth );
1442 mMenuButton->move( mLineEdit->rect().right() - frameWidth - sz.width(),
1443 ( mLineEdit->rect().bottom() + 1 - sz.height() ) / 2 );
1444 }
1445
updateText()1446 void QgsColorTextWidget::updateText()
1447 {
1448 switch ( mFormat )
1449 {
1450 case HexRgb:
1451 mLineEdit->setText( mCurrentColor.name() );
1452 break;
1453 case HexRgbA:
1454 mLineEdit->setText( mCurrentColor.name() + QStringLiteral( "%1" ).arg( mCurrentColor.alpha(), 2, 16, QChar( '0' ) ) );
1455 break;
1456 case Rgb:
1457 mLineEdit->setText( tr( "rgb( %1, %2, %3 )" ).arg( mCurrentColor.red() ).arg( mCurrentColor.green() ).arg( mCurrentColor.blue() ) );
1458 break;
1459 case Rgba:
1460 mLineEdit->setText( tr( "rgba( %1, %2, %3, %4 )" ).arg( mCurrentColor.red() ).arg( mCurrentColor.green() ).arg( mCurrentColor.blue() ).arg( QString::number( mCurrentColor.alphaF(), 'f', 2 ) ) );
1461 break;
1462 }
1463 }
1464
textChanged()1465 void QgsColorTextWidget::textChanged()
1466 {
1467 const QString testString = mLineEdit->text();
1468 bool containsAlpha;
1469 QColor color = QgsSymbolLayerUtils::parseColorWithAlpha( testString, containsAlpha );
1470 if ( !color.isValid() )
1471 {
1472 //bad color string
1473 updateText();
1474 return;
1475 }
1476
1477 //good color string
1478 if ( color != mCurrentColor )
1479 {
1480 //retain alpha if no explicit alpha set
1481 if ( !containsAlpha )
1482 {
1483 color.setAlpha( mCurrentColor.alpha() );
1484 }
1485 //color has changed
1486 mCurrentColor = color;
1487 emit colorChanged( mCurrentColor );
1488 }
1489 updateText();
1490 }
1491
showMenu()1492 void QgsColorTextWidget::showMenu()
1493 {
1494 QMenu colorContextMenu;
1495
1496 QAction *hexRgbAction = new QAction( tr( "#RRGGBB" ), nullptr );
1497 colorContextMenu.addAction( hexRgbAction );
1498 QAction *hexRgbaAction = new QAction( tr( "#RRGGBBAA" ), nullptr );
1499 colorContextMenu.addAction( hexRgbaAction );
1500 QAction *rgbAction = new QAction( tr( "rgb( r, g, b )" ), nullptr );
1501 colorContextMenu.addAction( rgbAction );
1502 QAction *rgbaAction = new QAction( tr( "rgba( r, g, b, a )" ), nullptr );
1503 colorContextMenu.addAction( rgbaAction );
1504
1505 QAction *selectedAction = colorContextMenu.exec( QCursor::pos() );
1506 if ( selectedAction == hexRgbAction )
1507 {
1508 mFormat = QgsColorTextWidget::HexRgb;
1509 }
1510 else if ( selectedAction == hexRgbaAction )
1511 {
1512 mFormat = QgsColorTextWidget::HexRgbA;
1513 }
1514 else if ( selectedAction == rgbAction )
1515 {
1516 mFormat = QgsColorTextWidget::Rgb;
1517 }
1518 else if ( selectedAction == rgbaAction )
1519 {
1520 mFormat = QgsColorTextWidget::Rgba;
1521 }
1522
1523 //save format setting
1524 QgsSettings settings;
1525 settings.setEnumValue( QStringLiteral( "ColorWidgets/textWidgetFormat" ), mFormat );
1526
1527 updateText();
1528 }
1529
1530
1531 //
1532 // QgsColorPreviewWidget
1533 //
1534
QgsColorPreviewWidget(QWidget * parent)1535 QgsColorPreviewWidget::QgsColorPreviewWidget( QWidget *parent )
1536 : QgsColorWidget( parent )
1537 , mColor2( QColor() )
1538 {
1539
1540 }
1541
drawColor(const QColor & color,QRect rect,QPainter & painter)1542 void QgsColorPreviewWidget::drawColor( const QColor &color, QRect rect, QPainter &painter )
1543 {
1544 painter.setPen( Qt::NoPen );
1545 //if color has an alpha, start with a checkboard pattern
1546 if ( color.alpha() < 255 )
1547 {
1548 const QBrush checkBrush = QBrush( transparentBackground() );
1549 painter.setBrush( checkBrush );
1550 painter.drawRect( rect );
1551
1552 //draw half of widget showing solid color, the other half showing color with alpha
1553
1554 //ensure at least a 1px overlap to avoid artifacts
1555 const QBrush colorBrush = QBrush( color );
1556 painter.setBrush( colorBrush );
1557 painter.drawRect( std::floor( rect.width() / 2.0 ) + rect.left(), rect.top(), rect.width() - std::floor( rect.width() / 2.0 ), rect.height() );
1558
1559 QColor opaqueColor = QColor( color );
1560 opaqueColor.setAlpha( 255 );
1561 const QBrush opaqueBrush = QBrush( opaqueColor );
1562 painter.setBrush( opaqueBrush );
1563 painter.drawRect( rect.left(), rect.top(), std::ceil( rect.width() / 2.0 ), rect.height() );
1564 }
1565 else
1566 {
1567 //no alpha component, just draw a solid rectangle
1568 const QBrush brush = QBrush( color );
1569 painter.setBrush( brush );
1570 painter.drawRect( rect );
1571 }
1572 }
1573
paintEvent(QPaintEvent * event)1574 void QgsColorPreviewWidget::paintEvent( QPaintEvent *event )
1575 {
1576 Q_UNUSED( event )
1577 QPainter painter( this );
1578
1579 if ( mColor2.isValid() )
1580 {
1581 //drawing with two color sections
1582 const int verticalSplit = std::round( height() / 2.0 );
1583 drawColor( mCurrentColor, QRect( 0, 0, width(), verticalSplit ), painter );
1584 drawColor( mColor2, QRect( 0, verticalSplit, width(), height() - verticalSplit ), painter );
1585 }
1586 else if ( mCurrentColor.isValid() )
1587 {
1588 drawColor( mCurrentColor, QRect( 0, 0, width(), height() ), painter );
1589 }
1590
1591 painter.end();
1592 }
1593
sizeHint() const1594 QSize QgsColorPreviewWidget::sizeHint() const
1595 {
1596 return QSize( Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 22, Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 22 * 0.75 );
1597 }
1598
setColor2(const QColor & color)1599 void QgsColorPreviewWidget::setColor2( const QColor &color )
1600 {
1601 if ( color == mColor2 )
1602 {
1603 return;
1604 }
1605 mColor2 = color;
1606 update();
1607 }
1608
mousePressEvent(QMouseEvent * e)1609 void QgsColorPreviewWidget::mousePressEvent( QMouseEvent *e )
1610 {
1611 if ( e->button() == Qt::LeftButton )
1612 {
1613 mDragStartPosition = e->pos();
1614 }
1615 QgsColorWidget::mousePressEvent( e );
1616 }
1617
mouseReleaseEvent(QMouseEvent * e)1618 void QgsColorPreviewWidget::mouseReleaseEvent( QMouseEvent *e )
1619 {
1620 if ( ( e->pos() - mDragStartPosition ).manhattanLength() >= QApplication::startDragDistance() )
1621 {
1622 //mouse moved, so a drag. nothing to do here
1623 QgsColorWidget::mouseReleaseEvent( e );
1624 return;
1625 }
1626
1627 //work out which color was clicked
1628 QColor clickedColor = mCurrentColor;
1629 if ( mColor2.isValid() )
1630 {
1631 //two color sections, check if dragged color was the second color
1632 const int verticalSplit = std::round( height() / 2.0 );
1633 if ( mDragStartPosition.y() >= verticalSplit )
1634 {
1635 clickedColor = mColor2;
1636 }
1637 }
1638 emit colorChanged( clickedColor );
1639
1640 }
1641
mouseMoveEvent(QMouseEvent * e)1642 void QgsColorPreviewWidget::mouseMoveEvent( QMouseEvent *e )
1643 {
1644 //handle dragging colors from button
1645
1646 if ( !( e->buttons() & Qt::LeftButton ) )
1647 {
1648 //left button not depressed, so not a drag
1649 QgsColorWidget::mouseMoveEvent( e );
1650 return;
1651 }
1652
1653 if ( ( e->pos() - mDragStartPosition ).manhattanLength() < QApplication::startDragDistance() )
1654 {
1655 //mouse not moved, so not a drag
1656 QgsColorWidget::mouseMoveEvent( e );
1657 return;
1658 }
1659
1660 //user is dragging color
1661
1662 //work out which color is being dragged
1663 QColor dragColor = mCurrentColor;
1664 if ( mColor2.isValid() )
1665 {
1666 //two color sections, check if dragged color was the second color
1667 const int verticalSplit = std::round( height() / 2.0 );
1668 if ( mDragStartPosition.y() >= verticalSplit )
1669 {
1670 dragColor = mColor2;
1671 }
1672 }
1673
1674 QDrag *drag = new QDrag( this );
1675 drag->setMimeData( QgsSymbolLayerUtils::colorToMimeData( dragColor ) );
1676 drag->setPixmap( createDragIcon( dragColor ) );
1677 drag->exec( Qt::CopyAction );
1678 }
1679
1680
1681 //
1682 // QgsColorWidgetAction
1683 //
1684
QgsColorWidgetAction(QgsColorWidget * colorWidget,QMenu * menu,QWidget * parent)1685 QgsColorWidgetAction::QgsColorWidgetAction( QgsColorWidget *colorWidget, QMenu *menu, QWidget *parent )
1686 : QWidgetAction( parent )
1687 , mMenu( menu )
1688 , mColorWidget( colorWidget )
1689 , mSuppressRecurse( false )
1690 , mDismissOnColorSelection( true )
1691 {
1692 setDefaultWidget( mColorWidget );
1693 connect( mColorWidget, &QgsColorWidget::colorChanged, this, &QgsColorWidgetAction::setColor );
1694
1695 connect( this, &QAction::hovered, this, &QgsColorWidgetAction::onHover );
1696 connect( mColorWidget, &QgsColorWidget::hovered, this, &QgsColorWidgetAction::onHover );
1697 }
1698
onHover()1699 void QgsColorWidgetAction::onHover()
1700 {
1701 //see https://bugreports.qt.io/browse/QTBUG-10427?focusedCommentId=185610&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-185610
1702 if ( mSuppressRecurse )
1703 {
1704 return;
1705 }
1706
1707 if ( mMenu )
1708 {
1709 mSuppressRecurse = true;
1710 mMenu->setActiveAction( this );
1711 mSuppressRecurse = false;
1712 }
1713 }
1714
setColor(const QColor & color)1715 void QgsColorWidgetAction::setColor( const QColor &color )
1716 {
1717 emit colorChanged( color );
1718 if ( mMenu && mDismissOnColorSelection )
1719 {
1720 QAction::trigger();
1721 mMenu->hide();
1722 }
1723 }
1724