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