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