QGIS API Documentation  3.27.0-Master (0e23467727)
qgscolorwidgets.cpp
Go to the documentation of this file.
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 
48 QgsColorWidget::QgsColorWidget( QWidget *parent, const ColorComponent component )
49  : QWidget( parent )
50  , mCurrentColor( Qt::red )
51  , mComponent( component )
52 {
53  setAcceptDrops( true );
54 }
55 
57 {
58  return componentValue( mComponent );
59 }
60 
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 
82 {
83  if ( !mCurrentColor.isValid() )
84  {
85  return -1;
86  }
87 
88  switch ( component )
89  {
91  return mCurrentColor.red();
93  return mCurrentColor.green();
95  return mCurrentColor.blue();
97  //hue is treated specially, to avoid -1 hues values from QColor for ambiguous hues
98  return hue();
100  return mCurrentColor.hsvSaturation();
102  return mCurrentColor.value();
104  return mCurrentColor.alpha();
105  default:
106  return -1;
107  }
108 }
109 
111 {
112  return componentRange( mComponent );
113 }
114 
116 {
118  {
119  //no component
120  return -1;
121  }
122 
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 
136 {
137  if ( mCurrentColor.hue() >= 0 )
138  {
139  return mCurrentColor.hue();
140  }
141  else
142  {
143  return mExplicitHue;
144  }
145 }
146 
147 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;
161  color.setGreen( clippedValue );
162  return;
164  color.setBlue( clippedValue );
165  return;
166  case QgsColorWidget::Hue:
167  color.setHsv( clippedValue, s, v, a );
168  return;
170  color.setHsv( h, clippedValue, v, a );
171  return;
173  color.setHsv( h, s, clippedValue, a );
174  return;
176  color.setAlpha( clippedValue );
177  return;
178  default:
179  return;
180  }
181 }
182 
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 
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 
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 
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 
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 
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 
249 QColor QgsColorWidget::color() const
250 {
251  return mCurrentColor;
252 }
253 
255 {
256  if ( component == mComponent )
257  {
258  return;
259  }
260 
262  update();
263 }
264 
265 void QgsColorWidget::setComponentValue( const int value )
266 {
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;
293  if ( g == valueClipped )
294  {
295  return;
296  }
297  mCurrentColor.setGreen( valueClipped );
298  break;
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;
314  if ( s == valueClipped )
315  {
316  return;
317  }
318  mCurrentColor.setHsv( h, valueClipped, v, a );
319  break;
321  if ( v == valueClipped )
322  {
323  return;
324  }
325  mCurrentColor.setHsv( h, s, valueClipped, a );
326  break;
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 
347 void QgsColorWidget::setColor( const QColor &color, const bool emitSignals )
348 {
349  if ( color == mCurrentColor )
350  {
351  return;
352  }
353 
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 
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 
392 {
393  delete mWheelImage;
394  delete mTriangleImage;
395  delete mWidgetImage;
396 }
397 
399 {
400  const int size = Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 22;
401  return QSize( size, size );
402 }
403 
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 
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 
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 
512 void QgsColorWheel::resizeEvent( QResizeEvent *event )
513 {
514  //recreate images for new size
515  createImages( event->size() );
516  QgsColorWidget::resizeEvent( event );
517 }
518 
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 
602 void QgsColorWheel::mouseMoveEvent( QMouseEvent *event )
603 {
604  if ( mIsDragging )
605  setColorFromPos( event->pos() );
606 
608 }
609 
610 void QgsColorWheel::mousePressEvent( QMouseEvent *event )
611 {
612  if ( event->button() == Qt::LeftButton )
613  {
614  mIsDragging = true;
615  //calculate where the event occurred -- on the wheel or inside the triangle?
616 
617  //create a line from the widget's center to the event
618  const QLineF line = QLineF( width() / 2.0, height() / 2.0, event->pos().x(), event->pos().y() );
619 
620  const double innerLength = mWheelImage->width() / 2.0 - mWheelThickness;
621  if ( line.length() < innerLength )
622  {
623  mClickedPart = QgsColorWheel::Triangle;
624  }
625  else
626  {
627  mClickedPart = QgsColorWheel::Wheel;
628  }
629  setColorFromPos( event->pos() );
630  }
631  else
632  {
634  }
635 }
636 
637 void QgsColorWheel::mouseReleaseEvent( QMouseEvent *event )
638 {
639  if ( event->button() == Qt::LeftButton )
640  {
641  mIsDragging = false;
642  mClickedPart = QgsColorWheel::None;
643  }
644  else
645  {
647  }
648 }
649 
650 void QgsColorWheel::createWheel()
651 {
652  if ( !mWheelImage )
653  {
654  return;
655  }
656 
657  const int maxSize = std::min( mWheelImage->width(), mWheelImage->height() );
658  const double wheelRadius = maxSize / 2.0;
659 
660  mWheelImage->fill( Qt::transparent );
661  QPainter p( mWheelImage );
662  p.setRenderHint( QPainter::Antialiasing );
663  p.setBrush( mWheelBrush );
664  p.setPen( Qt::NoPen );
665 
666  //draw hue wheel as a circle
667  p.translate( wheelRadius, wheelRadius );
668  p.drawEllipse( QPointF( 0.0, 0.0 ), wheelRadius, wheelRadius );
669 
670  //cut hole in center of circle to make a ring
671  p.setCompositionMode( QPainter::CompositionMode_DestinationOut );
672  p.setBrush( QBrush( Qt::black ) );
673  p.drawEllipse( QPointF( 0.0, 0.0 ), wheelRadius - mWheelThickness, wheelRadius - mWheelThickness );
674  p.end();
675 
676  mWheelDirty = false;
677 }
678 
679 void QgsColorWheel::createTriangle()
680 {
681  if ( !mWheelImage || !mTriangleImage )
682  {
683  return;
684  }
685 
686  const QPointF center = QPointF( mWheelImage->width() / 2.0, mWheelImage->height() / 2.0 );
687  mTriangleImage->fill( Qt::transparent );
688 
689  QPainter imagePainter( mTriangleImage );
690  imagePainter.setRenderHint( QPainter::Antialiasing );
691 
692  const int angle = hue();
693  const double wheelRadius = mWheelImage->width() / 2.0;
694  const double triangleRadius = wheelRadius - mWheelThickness - 1;
695 
696  //pure version of hue (at full saturation and value)
697  const QColor pureColor = QColor::fromHsv( angle, 255, 255 );
698  //create copy of color but with 0 alpha
699  QColor alphaColor = QColor( pureColor );
700  alphaColor.setAlpha( 0 );
701 
702  //some rather ugly shortcuts to obtain corners and midpoints of triangle
703  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 ) );
704  QLineF line2 = QLineF( center.x(), center.y(), center.x() + triangleRadius, center.y() );
705  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 ) );
706  QLineF line4 = QLineF( center.x(), center.y(), center.x() - triangleRadius * std::cos( M_PI / 3.0 ), center.y() );
707  QLineF line5 = QLineF( center.x(), center.y(), ( line2.p2().x() + line1.p2().x() ) / 2.0, ( line2.p2().y() + line1.p2().y() ) / 2.0 );
708  line1.setAngle( line1.angle() + angle );
709  line2.setAngle( line2.angle() + angle );
710  line3.setAngle( line3.angle() + angle );
711  line4.setAngle( line4.angle() + angle );
712  line5.setAngle( line5.angle() + angle );
713  const QPointF p1 = line1.p2();
714  const QPointF p2 = line2.p2();
715  const QPointF p3 = line3.p2();
716  const QPointF p4 = line4.p2();
717  const QPointF p5 = line5.p2();
718 
719  //inspired by Tim Baumann's work at https://github.com/timjb/colortriangle/blob/master/colortriangle.js
720  QLinearGradient colorGrad = QLinearGradient( p4.x(), p4.y(), p2.x(), p2.y() );
721  colorGrad.setColorAt( 0, alphaColor );
722  colorGrad.setColorAt( 1, pureColor );
723  QLinearGradient whiteGrad = QLinearGradient( p3.x(), p3.y(), p5.x(), p5.y() );
724  whiteGrad.setColorAt( 0, QColor( 255, 255, 255, 255 ) );
725  whiteGrad.setColorAt( 1, QColor( 255, 255, 255, 0 ) );
726 
727  QPolygonF triangle;
728  triangle << p2 << p1 << p3 << p2;
729  imagePainter.setPen( Qt::NoPen );
730  //start with a black triangle
731  imagePainter.setBrush( QBrush( Qt::black ) );
732  imagePainter.drawPolygon( triangle );
733  //draw a gradient from transparent to the pure color at the triangle's tip
734  imagePainter.setBrush( QBrush( colorGrad ) );
735  imagePainter.drawPolygon( triangle );
736  //draw a white gradient using additive composition mode
737  imagePainter.setCompositionMode( QPainter::CompositionMode_Plus );
738  imagePainter.setBrush( QBrush( whiteGrad ) );
739  imagePainter.drawPolygon( triangle );
740 
741  //above process results in some small artifacts on the edge of the triangle. Let's clear these up
742  //use source composition mode and draw an outline using a transparent pen
743  //this clears the edge pixels and leaves a nice smooth image
744  imagePainter.setCompositionMode( QPainter::CompositionMode_Source );
745  imagePainter.setBrush( Qt::NoBrush );
746  imagePainter.setPen( QPen( Qt::transparent ) );
747  imagePainter.drawPolygon( triangle );
748 
749  imagePainter.end();
750  mTriangleDirty = false;
751 }
752 
753 
754 
755 //
756 // QgsColorBox
757 //
758 
759 QgsColorBox::QgsColorBox( QWidget *parent, const ColorComponent component )
760  : QgsColorWidget( parent, component )
761 {
762  setFocusPolicy( Qt::StrongFocus );
763  setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding );
764 
765  mBoxImage = new QImage( width() - mMargin * 2, height() - mMargin * 2, QImage::Format_RGB32 );
766 }
767 
769 {
770  delete mBoxImage;
771 }
772 
774 {
775  const int size = Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 22;
776  return QSize( size, size );
777 }
778 
779 void QgsColorBox::paintEvent( QPaintEvent *event )
780 {
781  Q_UNUSED( event )
782  QPainter painter( this );
783 
784  QStyleOptionFrame option;
785  option.initFrom( this );
786  option.state = hasFocus() ? QStyle::State_Active : QStyle::State_None;
787  style()->drawPrimitive( QStyle::PE_Frame, &option, &painter );
788 
789  if ( mDirty )
790  {
791  createBox();
792  }
793 
794  //draw background image
795  painter.drawImage( QPoint( mMargin, mMargin ), *mBoxImage );
796 
797  //draw cross lines
798  const double xPos = mMargin + ( width() - 2 * mMargin - 1 ) * static_cast<double>( xComponentValue() ) / static_cast<double>( valueRangeX() );
799  const double yPos = mMargin + ( height() - 2 * mMargin - 1 ) - ( height() - 2 * mMargin - 1 ) * static_cast<double>( yComponentValue() ) / static_cast<double>( valueRangeY() );
800 
801  painter.setBrush( Qt::white );
802  painter.setPen( Qt::NoPen );
803 
804  painter.drawRect( xPos - 1, mMargin, 3, height() - 2 * mMargin - 1 );
805  painter.drawRect( mMargin, yPos - 1, width() - 2 * mMargin - 1, 3 );
806  painter.setPen( Qt::black );
807  painter.drawLine( xPos, mMargin, xPos, height() - mMargin - 1 );
808  painter.drawLine( mMargin, yPos, width() - mMargin - 1, yPos );
809 
810  painter.end();
811 }
812 
814 {
815  if ( component != mComponent )
816  {
817  //need to redraw
818  mDirty = true;
819  }
821 }
822 
823 void QgsColorBox::setColor( const QColor &color, const bool emitSignals )
824 {
825  //check if we need to redraw the box image
826  if ( mComponent == QgsColorWidget::Red && mCurrentColor.red() != color.red() )
827  {
828  mDirty = true;
829  }
830  else if ( mComponent == QgsColorWidget::Green && mCurrentColor.green() != color.green() )
831  {
832  mDirty = true;
833  }
834  else if ( mComponent == QgsColorWidget::Blue && mCurrentColor.blue() != color.blue() )
835  {
836  mDirty = true;
837  }
838  else if ( mComponent == QgsColorWidget::Hue && color.hsvHue() >= 0 && hue() != color.hsvHue() )
839  {
840  mDirty = true;
841  }
842  else if ( mComponent == QgsColorWidget::Saturation && mCurrentColor.hsvSaturation() != color.hsvSaturation() )
843  {
844  mDirty = true;
845  }
846  else if ( mComponent == QgsColorWidget::Value && mCurrentColor.value() != color.value() )
847  {
848  mDirty = true;
849  }
850  QgsColorWidget::setColor( color, emitSignals );
851 }
852 
853 void QgsColorBox::resizeEvent( QResizeEvent *event )
854 {
855  mDirty = true;
856  delete mBoxImage;
857  mBoxImage = new QImage( event->size().width() - mMargin * 2, event->size().height() - mMargin * 2, QImage::Format_RGB32 );
858  QgsColorWidget::resizeEvent( event );
859 }
860 
861 void QgsColorBox::mouseMoveEvent( QMouseEvent *event )
862 {
863  if ( mIsDragging )
864  {
865  setColorFromPoint( event->pos() );
866  }
868 }
869 
870 void QgsColorBox::mousePressEvent( QMouseEvent *event )
871 {
872  if ( event->button() == Qt::LeftButton )
873  {
874  mIsDragging = true;
875  setColorFromPoint( event->pos() );
876  }
877  else
878  {
880  }
881 }
882 
883 void QgsColorBox::mouseReleaseEvent( QMouseEvent *event )
884 {
885  if ( event->button() == Qt::LeftButton )
886  {
887  mIsDragging = false;
888  }
889  else
890  {
892  }
893 }
894 
895 void QgsColorBox::createBox()
896 {
897  const int maxValueX = mBoxImage->width();
898  const int maxValueY = mBoxImage->height();
899 
900  //create a temporary color object
901  QColor currentColor = QColor( mCurrentColor );
902  int colorComponentValue;
903 
904  for ( int y = 0; y < maxValueY; ++y )
905  {
906  QRgb *scanLine = ( QRgb * )mBoxImage->scanLine( y );
907 
908  colorComponentValue = int( valueRangeY() - valueRangeY() * ( double( y ) / maxValueY ) );
909  alterColor( currentColor, yComponent(), colorComponentValue );
910  for ( int x = 0; x < maxValueX; ++x )
911  {
912  colorComponentValue = int( valueRangeX() * ( double( x ) / maxValueX ) );
913  alterColor( currentColor, xComponent(), colorComponentValue );
914  scanLine[x] = currentColor.rgb();
915  }
916  }
917  mDirty = false;
918 }
919 
920 int QgsColorBox::valueRangeX() const
921 {
922  return componentRange( xComponent() );
923 }
924 
925 int QgsColorBox::valueRangeY() const
926 {
927  return componentRange( yComponent() );
928 }
929 
930 QgsColorWidget::ColorComponent QgsColorBox::yComponent() const
931 {
932  switch ( mComponent )
933  {
934  case QgsColorWidget::Red:
935  return QgsColorWidget::Green;
938  return QgsColorWidget::Red;
939  case QgsColorWidget::Hue:
943  return QgsColorWidget::Hue;
944  default:
945  //should not occur
946  return QgsColorWidget::Red;
947  }
948 }
949 
950 int QgsColorBox::yComponentValue() const
951 {
952  return componentValue( yComponent() );
953 }
954 
955 QgsColorWidget::ColorComponent QgsColorBox::xComponent() const
956 {
957  switch ( mComponent )
958  {
959  case QgsColorWidget::Red:
961  return QgsColorWidget::Blue;
963  return QgsColorWidget::Green;
964  case QgsColorWidget::Hue:
966  return QgsColorWidget:: Value;
969  default:
970  //should not occur
971  return QgsColorWidget::Red;
972  }
973 }
974 
975 int QgsColorBox::xComponentValue() const
976 {
977  return componentValue( xComponent() );
978 }
979 
980 void QgsColorBox::setColorFromPoint( QPoint point )
981 {
982  int valX = valueRangeX() * ( point.x() - mMargin ) / ( width() - 2 * mMargin - 1 );
983  valX = std::min( std::max( valX, 0 ), valueRangeX() );
984 
985  int valY = valueRangeY() - valueRangeY() * ( point.y() - mMargin ) / ( height() - 2 * mMargin - 1 );
986  valY = std::min( std::max( valY, 0 ), valueRangeY() );
987 
988  QColor color = QColor( mCurrentColor );
989  alterColor( color, xComponent(), valX );
990  alterColor( color, yComponent(), valY );
991 
992  if ( color == mCurrentColor )
993  {
994  return;
995  }
996 
997  if ( color.hue() >= 0 )
998  {
999  mExplicitHue = color.hue();
1000  }
1001 
1002  mCurrentColor = color;
1003  update();
1004  emit colorChanged( color );
1005 }
1006 
1007 
1008 //
1009 // QgsColorRampWidget
1010 //
1011 
1013  const QgsColorWidget::ColorComponent component,
1014  const Orientation orientation )
1015  : QgsColorWidget( parent, component )
1016 {
1017  setFocusPolicy( Qt::StrongFocus );
1019 
1020  //create triangle polygons
1021  setMarkerSize( 5 );
1022 }
1023 
1025 {
1026  if ( mOrientation == QgsColorRampWidget::Horizontal )
1027  {
1028  //horizontal
1029  return QSize( Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 22, Qgis::UI_SCALE_FACTOR * fontMetrics().height() * 1.3 );
1030  }
1031  else
1032  {
1033  //vertical
1034  return QSize( Qgis::UI_SCALE_FACTOR * fontMetrics().height() * 1.3, Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 22 );
1035  }
1036 }
1037 
1038 void QgsColorRampWidget::paintEvent( QPaintEvent *event )
1039 {
1040  Q_UNUSED( event )
1041  QPainter painter( this );
1042 
1043  if ( mShowFrame )
1044  {
1045  //draw frame
1046  QStyleOptionFrame option;
1047  option.initFrom( this );
1048  option.state = hasFocus() ? QStyle::State_KeyboardFocusChange : QStyle::State_None;
1049  style()->drawPrimitive( QStyle::PE_Frame, &option, &painter );
1050  }
1051 
1052  if ( hasFocus() )
1053  {
1054  //draw focus rect
1055  QStyleOptionFocusRect option;
1056  option.initFrom( this );
1057  option.state = QStyle::State_KeyboardFocusChange;
1058  style()->drawPrimitive( QStyle::PE_FrameFocusRect, &option, &painter );
1059  }
1060 
1062  {
1063  const int maxValue = ( mOrientation == QgsColorRampWidget::Horizontal ? width() : height() ) - 1 - 2 * mMargin;
1064  QColor color = QColor( mCurrentColor );
1065  color.setAlpha( 255 );
1066  QPen pen;
1067  // we need to set pen width to 1,
1068  // since on retina displays
1069  // pen.setWidth(0) <=> pen.width = 0.5
1070  // see https://github.com/qgis/QGIS/issues/23900
1071  pen.setWidth( 1 );
1072  painter.setPen( pen );
1073  painter.setBrush( Qt::NoBrush );
1074 
1075  //draw background ramp
1076  for ( int c = 0; c <= maxValue; ++c )
1077  {
1078  int colorVal = static_cast<int>( componentRange() * static_cast<double>( c ) / maxValue );
1079  //vertical sliders are reversed
1080  if ( mOrientation == QgsColorRampWidget::Vertical )
1081  {
1082  colorVal = componentRange() - colorVal;
1083  }
1084  alterColor( color, mComponent, colorVal );
1085  if ( color.hue() < 0 )
1086  {
1087  color.setHsv( hue(), color.saturation(), color.value() );
1088  }
1089  pen.setColor( color );
1090  painter.setPen( pen );
1091  if ( mOrientation == QgsColorRampWidget::Horizontal )
1092  {
1093  //horizontal
1094  painter.drawLine( QLineF( c + mMargin, mMargin, c + mMargin, height() - mMargin - 1 ) );
1095  }
1096  else
1097  {
1098  //vertical
1099  painter.drawLine( QLineF( mMargin, c + mMargin, width() - mMargin - 1, c + mMargin ) );
1100  }
1101  }
1102  }
1103  else
1104  {
1105  //alpha ramps are drawn differently
1106  //start with the checkboard pattern
1107  const QBrush checkBrush = QBrush( transparentBackground() );
1108  painter.setBrush( checkBrush );
1109  painter.setPen( Qt::NoPen );
1110  painter.drawRect( QRectF( mMargin, mMargin, width() - 2 * mMargin - 1, height() - 2 * mMargin - 1 ) );
1111  QLinearGradient colorGrad;
1112  if ( mOrientation == QgsColorRampWidget::Horizontal )
1113  {
1114  //horizontal
1115  colorGrad = QLinearGradient( mMargin, 0, width() - mMargin - 1, 0 );
1116  }
1117  else
1118  {
1119  //vertical
1120  colorGrad = QLinearGradient( 0, mMargin, 0, height() - mMargin - 1 );
1121  }
1122  QColor transparent = QColor( mCurrentColor );
1123  transparent.setAlpha( 0 );
1124  colorGrad.setColorAt( 0, transparent );
1125  QColor opaque = QColor( mCurrentColor );
1126  opaque.setAlpha( 255 );
1127  colorGrad.setColorAt( 1, opaque );
1128  const QBrush colorBrush = QBrush( colorGrad );
1129  painter.setBrush( colorBrush );
1130  painter.drawRect( QRectF( mMargin, mMargin, width() - 2 * mMargin - 1, height() - 2 * mMargin - 1 ) );
1131  }
1132 
1133  if ( mOrientation == QgsColorRampWidget::Horizontal )
1134  {
1135  //draw marker triangles for horizontal ramps
1136  painter.setRenderHint( QPainter::Antialiasing );
1137  painter.setBrush( QBrush( Qt::black ) );
1138  painter.setPen( Qt::NoPen );
1139  painter.translate( mMargin + ( width() - 2 * mMargin ) * static_cast<double>( componentValue() ) / componentRange(), mMargin - 1 );
1140  painter.drawPolygon( mTopTriangle );
1141  painter.translate( 0, height() - mMargin - 2 );
1142  painter.setBrush( QBrush( Qt::white ) );
1143  painter.drawPolygon( mBottomTriangle );
1144  painter.end();
1145  }
1146  else
1147  {
1148  //draw cross lines for vertical ramps
1149  const double ypos = mMargin + ( height() - 2 * mMargin - 1 ) - ( height() - 2 * mMargin - 1 ) * static_cast<double>( componentValue() ) / componentRange();
1150  painter.setBrush( Qt::white );
1151  painter.setPen( Qt::NoPen );
1152  painter.drawRect( QRectF( mMargin, ypos - 1, width() - 2 * mMargin - 1, 3 ) );
1153  painter.setPen( Qt::black );
1154  painter.drawLine( QLineF( mMargin, ypos, width() - mMargin - 1, ypos ) );
1155  }
1156 }
1157 
1159 {
1160  mOrientation = orientation;
1162  {
1163  //horizontal
1164  setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Fixed );
1165  }
1166  else
1167  {
1168  //vertical
1169  setSizePolicy( QSizePolicy::Fixed, QSizePolicy::MinimumExpanding );
1170  }
1171  updateGeometry();
1172 }
1173 
1175 {
1176  if ( margin == mMargin )
1177  {
1178  return;
1179  }
1180  mMargin = margin;
1181  update();
1182 }
1183 
1184 void QgsColorRampWidget::setShowFrame( const bool showFrame )
1185 {
1186  if ( showFrame == mShowFrame )
1187  {
1188  return;
1189  }
1190  mShowFrame = showFrame;
1191  update();
1192 }
1193 
1194 void QgsColorRampWidget::setMarkerSize( const int markerSize )
1195 {
1196  //create triangle polygons
1197  mTopTriangle << QPoint( -markerSize, 0 ) << QPoint( markerSize, 0 ) << QPoint( 0, markerSize );
1198  mBottomTriangle << QPoint( -markerSize, 0 ) << QPoint( markerSize, 0 ) << QPoint( 0, -markerSize );
1199  update();
1200 }
1201 
1202 void QgsColorRampWidget::mouseMoveEvent( QMouseEvent *event )
1203 {
1204  if ( mIsDragging )
1205  {
1206  setColorFromPoint( event->pos() );
1207  }
1208 
1210 }
1211 
1212 void QgsColorRampWidget::wheelEvent( QWheelEvent *event )
1213 {
1214  const int oldValue = componentValue();
1215 
1216  if ( event->angleDelta().y() > 0 )
1217  {
1219  }
1220  else
1221  {
1223  }
1224 
1225  if ( componentValue() != oldValue )
1226  {
1227  //value has changed
1228  emit colorChanged( mCurrentColor );
1229  emit valueChanged( componentValue() );
1230  }
1231 
1232  event->accept();
1233 }
1234 
1235 void QgsColorRampWidget::mousePressEvent( QMouseEvent *event )
1236 {
1237  if ( event->button() == Qt::LeftButton )
1238  {
1239  mIsDragging = true;
1240  setColorFromPoint( event->pos() );
1241  }
1242  else
1243  {
1245  }
1246 }
1247 
1248 void QgsColorRampWidget::mouseReleaseEvent( QMouseEvent *event )
1249 {
1250  if ( event->button() == Qt::LeftButton )
1251  {
1252  mIsDragging = false;
1253  }
1254  else
1255  {
1257  }
1258 }
1259 
1260 void QgsColorRampWidget::keyPressEvent( QKeyEvent *event )
1261 {
1262  const int oldValue = componentValue();
1263  if ( ( mOrientation == QgsColorRampWidget::Horizontal && ( event->key() == Qt::Key_Right || event->key() == Qt::Key_Up ) )
1264  || ( mOrientation == QgsColorRampWidget::Vertical && ( event->key() == Qt::Key_Left || event->key() == Qt::Key_Up ) ) )
1265  {
1267  }
1268  else if ( ( mOrientation == QgsColorRampWidget::Horizontal && ( event->key() == Qt::Key_Left || event->key() == Qt::Key_Down ) )
1269  || ( mOrientation == QgsColorRampWidget::Vertical && ( event->key() == Qt::Key_Right || event->key() == Qt::Key_Down ) ) )
1270  {
1272  }
1273  else if ( ( mOrientation == QgsColorRampWidget::Horizontal && event->key() == Qt::Key_PageDown )
1274  || ( mOrientation == QgsColorRampWidget::Vertical && event->key() == Qt::Key_PageUp ) )
1275  {
1277  }
1278  else if ( ( mOrientation == QgsColorRampWidget::Horizontal && event->key() == Qt::Key_PageUp )
1279  || ( mOrientation == QgsColorRampWidget::Vertical && event->key() == Qt::Key_PageDown ) )
1280  {
1282  }
1283  else if ( ( mOrientation == QgsColorRampWidget::Horizontal && event->key() == Qt::Key_Home )
1284  || ( mOrientation == QgsColorRampWidget::Vertical && event->key() == Qt::Key_End ) )
1285  {
1286  setComponentValue( 0 );
1287  }
1288  else if ( ( mOrientation == QgsColorRampWidget::Horizontal && event->key() == Qt::Key_End )
1289  || ( mOrientation == QgsColorRampWidget::Vertical && event->key() == Qt::Key_Home ) )
1290  {
1291  //set to maximum value
1293  }
1294  else
1295  {
1296  QgsColorWidget::keyPressEvent( event );
1297  return;
1298  }
1299 
1300  if ( componentValue() != oldValue )
1301  {
1302  //value has changed
1303  emit colorChanged( mCurrentColor );
1304  emit valueChanged( componentValue() );
1305  }
1306 }
1307 
1308 void QgsColorRampWidget::setColorFromPoint( QPointF point )
1309 {
1310  const int oldValue = componentValue();
1311  int val;
1312  if ( mOrientation == QgsColorRampWidget::Horizontal )
1313  {
1314  val = componentRange() * ( point.x() - mMargin ) / ( width() - 2 * mMargin );
1315  }
1316  else
1317  {
1318  val = componentRange() - componentRange() * ( point.y() - mMargin ) / ( height() - 2 * mMargin );
1319  }
1320  val = std::max( 0, std::min( val, componentRange() ) );
1321  setComponentValue( val );
1322 
1323  if ( componentValue() != oldValue )
1324  {
1325  //value has changed
1326  emit colorChanged( mCurrentColor );
1327  emit valueChanged( componentValue() );
1328  }
1329 }
1330 
1331 //
1332 // QgsColorSliderWidget
1333 //
1334 
1336  : QgsColorWidget( parent, component )
1337 
1338 {
1339  QHBoxLayout *hLayout = new QHBoxLayout();
1340  hLayout->setContentsMargins( 0, 0, 0, 0 );
1341  hLayout->setSpacing( 5 );
1342 
1343  mRampWidget = new QgsColorRampWidget( nullptr, component );
1344  mRampWidget->setColor( mCurrentColor );
1345  hLayout->addWidget( mRampWidget, 1 );
1346 
1347  mSpinBox = new QSpinBox();
1348  //set spinbox to a reasonable width
1349  const int largestCharWidth = mSpinBox->fontMetrics().horizontalAdvance( QStringLiteral( "888%" ) );
1350  mSpinBox->setMinimumWidth( largestCharWidth + 35 );
1351  mSpinBox->setMinimum( 0 );
1352  mSpinBox->setMaximum( convertRealToDisplay( componentRange() ) );
1353  mSpinBox->setValue( convertRealToDisplay( componentValue() ) );
1354  if ( component == QgsColorWidget::Hue )
1355  {
1356  //degrees suffix for hue
1357  mSpinBox->setSuffix( QChar( 176 ) );
1358  }
1360  {
1361  mSpinBox->setSuffix( tr( "%" ) );
1362  }
1363  hLayout->addWidget( mSpinBox );
1364  setLayout( hLayout );
1365 
1366  connect( mRampWidget, &QgsColorRampWidget::valueChanged, this, &QgsColorSliderWidget::rampChanged );
1367  connect( mRampWidget, &QgsColorWidget::colorChanged, this, &QgsColorSliderWidget::rampColorChanged );
1368  connect( mSpinBox, static_cast < void ( QSpinBox::* )( int ) > ( &QSpinBox::valueChanged ), this, &QgsColorSliderWidget::spinChanged );
1369 }
1370 
1372 {
1374  mRampWidget->setComponent( component );
1375  mSpinBox->setMaximum( convertRealToDisplay( componentRange() ) );
1376  if ( component == QgsColorWidget::Hue )
1377  {
1378  //degrees suffix for hue
1379  mSpinBox->setSuffix( QChar( 176 ) );
1380  }
1382  {
1383  //saturation, value and alpha are in %
1384  mSpinBox->setSuffix( tr( "%" ) );
1385  }
1386  else
1387  {
1388  //clear suffix
1389  mSpinBox->setSuffix( QString() );
1390  }
1391 }
1392 
1394 {
1396  mRampWidget->blockSignals( true );
1397  mRampWidget->setComponentValue( value );
1398  mRampWidget->blockSignals( false );
1399  mSpinBox->blockSignals( true );
1400  mSpinBox->setValue( convertRealToDisplay( value ) );
1401  mSpinBox->blockSignals( false );
1402 }
1403 
1404 void QgsColorSliderWidget::setColor( const QColor &color, bool emitSignals )
1405 {
1406  QgsColorWidget::setColor( color, emitSignals );
1407  mRampWidget->setColor( color );
1408  mSpinBox->blockSignals( true );
1409  mSpinBox->setValue( convertRealToDisplay( componentValue() ) );
1410  mSpinBox->blockSignals( false );
1411 }
1412 
1413 void QgsColorSliderWidget::rampColorChanged( const QColor &color )
1414 {
1415  emit colorChanged( color );
1416 }
1417 
1418 void QgsColorSliderWidget::spinChanged( int value )
1419 {
1420  const int convertedValue = convertDisplayToReal( value );
1421  QgsColorWidget::setComponentValue( convertedValue );
1422  mRampWidget->setComponentValue( convertedValue );
1423  emit colorChanged( mCurrentColor );
1424 }
1425 
1426 void QgsColorSliderWidget::rampChanged( int value )
1427 {
1428  mSpinBox->blockSignals( true );
1429  mSpinBox->setValue( convertRealToDisplay( value ) );
1430  mSpinBox->blockSignals( false );
1431 }
1432 
1433 
1434 int QgsColorSliderWidget::convertRealToDisplay( const int realValue ) const
1435 {
1436  //scale saturation, value or alpha to 0->100 range. This makes more sense for users
1437  //for whom "255" is a totally arbitrary value!
1439  {
1440  return std::round( 100.0 * realValue / 255.0 );
1441  }
1442 
1443  //leave all other values intact
1444  return realValue;
1445 }
1446 
1447 int QgsColorSliderWidget::convertDisplayToReal( const int displayValue ) const
1448 {
1449  //scale saturation, value or alpha from 0->100 range (see note in convertRealToDisplay)
1451  {
1452  return std::round( 255.0 * displayValue / 100.0 );
1453  }
1454 
1455  //leave all other values intact
1456  return displayValue;
1457 }
1458 
1459 //
1460 // QgsColorTextWidget
1461 //
1462 
1464  : QgsColorWidget( parent )
1465 {
1466  QHBoxLayout *hLayout = new QHBoxLayout();
1467  hLayout->setContentsMargins( 0, 0, 0, 0 );
1468  hLayout->setSpacing( 0 );
1469 
1470  mLineEdit = new QLineEdit( nullptr );
1471  hLayout->addWidget( mLineEdit );
1472 
1473  mMenuButton = new QToolButton( mLineEdit );
1474  mMenuButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mIconDropDownMenu.svg" ) ) );
1475  mMenuButton->setCursor( Qt::ArrowCursor );
1476  mMenuButton->setFocusPolicy( Qt::NoFocus );
1477  mMenuButton->setStyleSheet( QStringLiteral( "QToolButton { border: none; padding: 0px; }" ) );
1478 
1479  setLayout( hLayout );
1480 
1481  const int frameWidth = mLineEdit->style()->pixelMetric( QStyle::PM_DefaultFrameWidth );
1482  mLineEdit->setStyleSheet( QStringLiteral( "QLineEdit { padding-right: %1px; } " )
1483  .arg( mMenuButton->sizeHint().width() + frameWidth + 1 ) );
1484 
1485  connect( mLineEdit, &QLineEdit::editingFinished, this, &QgsColorTextWidget::textChanged );
1486  connect( mMenuButton, &QAbstractButton::clicked, this, &QgsColorTextWidget::showMenu );
1487 
1488  //restore format setting
1489  QgsSettings settings;
1490  mFormat = settings.enumValue( QStringLiteral( "ColorWidgets/textWidgetFormat" ), HexRgb );
1491 
1492  updateText();
1493 }
1494 
1495 void QgsColorTextWidget::setColor( const QColor &color, const bool emitSignals )
1496 {
1497  QgsColorWidget::setColor( color, emitSignals );
1498  updateText();
1499 }
1500 
1501 void QgsColorTextWidget::resizeEvent( QResizeEvent *event )
1502 {
1503  Q_UNUSED( event )
1504  const QSize sz = mMenuButton->sizeHint();
1505  const int frameWidth = style()->pixelMetric( QStyle::PM_DefaultFrameWidth );
1506  mMenuButton->move( mLineEdit->rect().right() - frameWidth - sz.width(),
1507  ( mLineEdit->rect().bottom() + 1 - sz.height() ) / 2 );
1508 }
1509 
1510 void QgsColorTextWidget::updateText()
1511 {
1512  switch ( mFormat )
1513  {
1514  case HexRgb:
1515  mLineEdit->setText( mCurrentColor.name() );
1516  break;
1517  case HexRgbA:
1518  mLineEdit->setText( mCurrentColor.name() + QStringLiteral( "%1" ).arg( mCurrentColor.alpha(), 2, 16, QChar( '0' ) ) );
1519  break;
1520  case Rgb:
1521  mLineEdit->setText( tr( "rgb( %1, %2, %3 )" ).arg( mCurrentColor.red() ).arg( mCurrentColor.green() ).arg( mCurrentColor.blue() ) );
1522  break;
1523  case Rgba:
1524  mLineEdit->setText( tr( "rgba( %1, %2, %3, %4 )" ).arg( mCurrentColor.red() ).arg( mCurrentColor.green() ).arg( mCurrentColor.blue() ).arg( QString::number( mCurrentColor.alphaF(), 'f', 2 ) ) );
1525  break;
1526  }
1527 }
1528 
1529 void QgsColorTextWidget::textChanged()
1530 {
1531  const QString testString = mLineEdit->text();
1532  bool containsAlpha;
1533  QColor color = QgsSymbolLayerUtils::parseColorWithAlpha( testString, containsAlpha );
1534  if ( !color.isValid() )
1535  {
1536  //bad color string
1537  updateText();
1538  return;
1539  }
1540 
1541  //good color string
1542  if ( color != mCurrentColor )
1543  {
1544  //retain alpha if no explicit alpha set
1545  if ( !containsAlpha )
1546  {
1547  color.setAlpha( mCurrentColor.alpha() );
1548  }
1549  //color has changed
1550  mCurrentColor = color;
1551  emit colorChanged( mCurrentColor );
1552  }
1553  updateText();
1554 }
1555 
1556 void QgsColorTextWidget::showMenu()
1557 {
1558  QMenu colorContextMenu;
1559 
1560  QAction *hexRgbAction = new QAction( tr( "#RRGGBB" ), nullptr );
1561  colorContextMenu.addAction( hexRgbAction );
1562  QAction *hexRgbaAction = new QAction( tr( "#RRGGBBAA" ), nullptr );
1563  colorContextMenu.addAction( hexRgbaAction );
1564  QAction *rgbAction = new QAction( tr( "rgb( r, g, b )" ), nullptr );
1565  colorContextMenu.addAction( rgbAction );
1566  QAction *rgbaAction = new QAction( tr( "rgba( r, g, b, a )" ), nullptr );
1567  colorContextMenu.addAction( rgbaAction );
1568 
1569  QAction *selectedAction = colorContextMenu.exec( QCursor::pos() );
1570  if ( selectedAction == hexRgbAction )
1571  {
1572  mFormat = QgsColorTextWidget::HexRgb;
1573  }
1574  else if ( selectedAction == hexRgbaAction )
1575  {
1576  mFormat = QgsColorTextWidget::HexRgbA;
1577  }
1578  else if ( selectedAction == rgbAction )
1579  {
1580  mFormat = QgsColorTextWidget::Rgb;
1581  }
1582  else if ( selectedAction == rgbaAction )
1583  {
1584  mFormat = QgsColorTextWidget::Rgba;
1585  }
1586 
1587  //save format setting
1588  QgsSettings settings;
1589  settings.setEnumValue( QStringLiteral( "ColorWidgets/textWidgetFormat" ), mFormat );
1590 
1591  updateText();
1592 }
1593 
1594 
1595 //
1596 // QgsColorPreviewWidget
1597 //
1598 
1600  : QgsColorWidget( parent )
1601  , mColor2( QColor() )
1602 {
1603 
1604 }
1605 
1606 void QgsColorPreviewWidget::drawColor( const QColor &color, QRect rect, QPainter &painter )
1607 {
1608  painter.setPen( Qt::NoPen );
1609  //if color has an alpha, start with a checkboard pattern
1610  if ( color.alpha() < 255 )
1611  {
1612  const QBrush checkBrush = QBrush( transparentBackground() );
1613  painter.setBrush( checkBrush );
1614  painter.drawRect( rect );
1615 
1616  //draw half of widget showing solid color, the other half showing color with alpha
1617 
1618  //ensure at least a 1px overlap to avoid artifacts
1619  const QBrush colorBrush = QBrush( color );
1620  painter.setBrush( colorBrush );
1621  painter.drawRect( std::floor( rect.width() / 2.0 ) + rect.left(), rect.top(), rect.width() - std::floor( rect.width() / 2.0 ), rect.height() );
1622 
1623  QColor opaqueColor = QColor( color );
1624  opaqueColor.setAlpha( 255 );
1625  const QBrush opaqueBrush = QBrush( opaqueColor );
1626  painter.setBrush( opaqueBrush );
1627  painter.drawRect( rect.left(), rect.top(), std::ceil( rect.width() / 2.0 ), rect.height() );
1628  }
1629  else
1630  {
1631  //no alpha component, just draw a solid rectangle
1632  const QBrush brush = QBrush( color );
1633  painter.setBrush( brush );
1634  painter.drawRect( rect );
1635  }
1636 }
1637 
1638 void QgsColorPreviewWidget::paintEvent( QPaintEvent *event )
1639 {
1640  Q_UNUSED( event )
1641  QPainter painter( this );
1642 
1643  if ( mColor2.isValid() )
1644  {
1645  //drawing with two color sections
1646  const int verticalSplit = std::round( height() / 2.0 );
1647  drawColor( mCurrentColor, QRect( 0, 0, width(), verticalSplit ), painter );
1648  drawColor( mColor2, QRect( 0, verticalSplit, width(), height() - verticalSplit ), painter );
1649  }
1650  else if ( mCurrentColor.isValid() )
1651  {
1652  drawColor( mCurrentColor, QRect( 0, 0, width(), height() ), painter );
1653  }
1654 
1655  painter.end();
1656 }
1657 
1659 {
1660  return QSize( Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 22, Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 22 * 0.75 );
1661 }
1662 
1663 void QgsColorPreviewWidget::setColor2( const QColor &color )
1664 {
1665  if ( color == mColor2 )
1666  {
1667  return;
1668  }
1669  mColor2 = color;
1670  update();
1671 }
1672 
1674 {
1675  if ( e->button() == Qt::LeftButton )
1676  {
1677  mDragStartPosition = e->pos();
1678  }
1680 }
1681 
1683 {
1684  if ( ( e->pos() - mDragStartPosition ).manhattanLength() >= QApplication::startDragDistance() )
1685  {
1686  //mouse moved, so a drag. nothing to do here
1688  return;
1689  }
1690 
1691  //work out which color was clicked
1692  QColor clickedColor = mCurrentColor;
1693  if ( mColor2.isValid() )
1694  {
1695  //two color sections, check if dragged color was the second color
1696  const int verticalSplit = std::round( height() / 2.0 );
1697  if ( mDragStartPosition.y() >= verticalSplit )
1698  {
1699  clickedColor = mColor2;
1700  }
1701  }
1702  emit colorChanged( clickedColor );
1703 
1704 }
1705 
1707 {
1708  //handle dragging colors from button
1709 
1710  if ( !( e->buttons() & Qt::LeftButton ) )
1711  {
1712  //left button not depressed, so not a drag
1714  return;
1715  }
1716 
1717  if ( ( e->pos() - mDragStartPosition ).manhattanLength() < QApplication::startDragDistance() )
1718  {
1719  //mouse not moved, so not a drag
1721  return;
1722  }
1723 
1724  //user is dragging color
1725 
1726  //work out which color is being dragged
1727  QColor dragColor = mCurrentColor;
1728  if ( mColor2.isValid() )
1729  {
1730  //two color sections, check if dragged color was the second color
1731  const int verticalSplit = std::round( height() / 2.0 );
1732  if ( mDragStartPosition.y() >= verticalSplit )
1733  {
1734  dragColor = mColor2;
1735  }
1736  }
1737 
1738  QDrag *drag = new QDrag( this );
1739  drag->setMimeData( QgsSymbolLayerUtils::colorToMimeData( dragColor ) );
1740  drag->setPixmap( createDragIcon( dragColor ) );
1741  drag->exec( Qt::CopyAction );
1742 }
1743 
1744 
1745 //
1746 // QgsColorWidgetAction
1747 //
1748 
1749 QgsColorWidgetAction::QgsColorWidgetAction( QgsColorWidget *colorWidget, QMenu *menu, QWidget *parent )
1750  : QWidgetAction( parent )
1751  , mMenu( menu )
1752  , mColorWidget( colorWidget )
1753  , mSuppressRecurse( false )
1754  , mDismissOnColorSelection( true )
1755 {
1756  setDefaultWidget( mColorWidget );
1757  connect( mColorWidget, &QgsColorWidget::colorChanged, this, &QgsColorWidgetAction::setColor );
1758 
1759  connect( this, &QAction::hovered, this, &QgsColorWidgetAction::onHover );
1760  connect( mColorWidget, &QgsColorWidget::hovered, this, &QgsColorWidgetAction::onHover );
1761 }
1762 
1763 void QgsColorWidgetAction::onHover()
1764 {
1765  //see https://bugreports.qt.io/browse/QTBUG-10427?focusedCommentId=185610&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-185610
1766  if ( mSuppressRecurse )
1767  {
1768  return;
1769  }
1770 
1771  if ( mMenu )
1772  {
1773  mSuppressRecurse = true;
1774  mMenu->setActiveAction( this );
1775  mSuppressRecurse = false;
1776  }
1777 }
1778 
1779 void QgsColorWidgetAction::setColor( const QColor &color )
1780 {
1781  emit colorChanged( color );
1782  if ( mMenu && mDismissOnColorSelection )
1783  {
1784  QAction::trigger();
1785  mMenu->hide();
1786  }
1787 }
static const double UI_SCALE_FACTOR
UI scaling factor.
Definition: qgis.h:2039
static QPixmap getThemePixmap(const QString &name, const QColor &foreColor=QColor(), const QColor &backColor=QColor(), int size=16)
Helper to get a theme icon as a pixmap.
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
QSize sizeHint() const override
void resizeEvent(QResizeEvent *event) override
void mouseReleaseEvent(QMouseEvent *event) override
void mousePressEvent(QMouseEvent *event) override
void setComponent(ColorComponent component) override
Sets the color component which the widget controls.
void setColor(const QColor &color, bool emitSignals=false) override
void mouseMoveEvent(QMouseEvent *event) override
void paintEvent(QPaintEvent *event) override
QgsColorBox(QWidget *parent=nullptr, ColorComponent component=Value)
Construct a new color box widget.
~QgsColorBox() override
virtual void setColor2(const QColor &color)
Sets the second color for the widget.
void mouseMoveEvent(QMouseEvent *e) override
QSize sizeHint() const override
void paintEvent(QPaintEvent *event) override
void mouseReleaseEvent(QMouseEvent *e) override
void mousePressEvent(QMouseEvent *e) override
QgsColorPreviewWidget(QWidget *parent=nullptr)
Construct a new color preview widget.
A color ramp widget.
void setMarkerSize(int markerSize)
Sets the size for drawing the triangular markers on the ramp.
void setInteriorMargin(int margin)
Sets the margin between the edge of the widget and the ramp.
void paintEvent(QPaintEvent *event) override
void keyPressEvent(QKeyEvent *event) override
void mousePressEvent(QMouseEvent *event) override
Orientation orientation() const
Fetches the orientation for the color ramp.
void wheelEvent(QWheelEvent *event) override
QgsColorRampWidget(QWidget *parent=nullptr, ColorComponent component=QgsColorWidget::Red, Orientation orientation=QgsColorRampWidget::Horizontal)
Construct a new color ramp widget.
void valueChanged(int value)
Emitted when the widget's color component value changes.
QSize sizeHint() const override
void mouseMoveEvent(QMouseEvent *event) override
void mouseReleaseEvent(QMouseEvent *event) override
void setOrientation(Orientation orientation)
Sets the orientation for the color ramp.
void setShowFrame(bool showFrame)
Sets whether the ramp should be drawn within a frame.
Orientation
Specifies the orientation of a color ramp.
@ Horizontal
Horizontal ramp.
@ Vertical
Vertical ramp.
bool showFrame() const
Fetches whether the ramp is drawn within a frame.
void setColor(const QColor &color, bool emitSignals=false) override
Sets the color for the widget.
void setComponent(ColorComponent component) override
Sets the color component which the widget controls.
void setComponentValue(int value) override
Alters the widget's color by setting the value for the widget's color component.
QgsColorSliderWidget(QWidget *parent=nullptr, ColorComponent component=QgsColorWidget::Red)
Construct a new color slider widget.
QgsColorTextWidget(QWidget *parent=nullptr)
Construct a new color line edit widget.
@ Rgba
Rgba( r, g, b, a ) format, with alpha.
@ Rgb
Rgb( r, g, b ) format.
@ HexRgbA
#RRGGBBAA in hexadecimal, with alpha
@ HexRgb
#RRGGBB in hexadecimal
void setColor(const QColor &color, bool emitSignals=false) override
Sets the color for the widget.
void resizeEvent(QResizeEvent *event) override
void paintEvent(QPaintEvent *event) override
QgsColorWheel(QWidget *parent=nullptr)
Constructs a new color wheel widget.
void mousePressEvent(QMouseEvent *event) override
QSize sizeHint() const override
void mouseReleaseEvent(QMouseEvent *event) override
void mouseMoveEvent(QMouseEvent *event) override
void resizeEvent(QResizeEvent *event) override
void setColor(const QColor &color, bool emitSignals=false) override
~QgsColorWheel() override
void colorChanged(const QColor &color)
Emitted when a color has been selected from the widget.
QgsColorWidgetAction(QgsColorWidget *colorWidget, QMenu *menu=nullptr, QWidget *parent=nullptr)
Construct a new color widget action.
A base class for interactive color widgets.
void mousePressEvent(QMouseEvent *e) override
void hovered()
Emitted when mouse hovers over widget.
QgsColorWidget(QWidget *parent=nullptr, ColorComponent component=Multiple)
Construct a new color widget.
ColorComponent component() const
Returns the color component which the widget controls.
QColor color() const
Returns the current color for the widget.
void colorChanged(const QColor &color)
Emitted when the widget's color changes.
int mExplicitHue
QColor wipes the hue information when it is ambiguous (e.g., for saturation = 0).
void mouseReleaseEvent(QMouseEvent *e) override
virtual void setComponentValue(int value)
Alters the widget's color by setting the value for the widget's color component.
void alterColor(QColor &color, QgsColorWidget::ColorComponent component, int newValue) const
Alters a color by modifying the value of a specific color component.
void mouseMoveEvent(QMouseEvent *e) override
int componentValue() const
Returns the current value of the widget's color component.
static QPixmap createDragIcon(const QColor &color)
Create an icon for dragging colors.
void dropEvent(QDropEvent *e) override
virtual void setComponent(QgsColorWidget::ColorComponent component)
Sets the color component which the widget controls.
ColorComponent mComponent
int componentRange() const
Returns the range of valid values for the color widget's component.
int hue() const
Returns the hue for the widget.
virtual void setColor(const QColor &color, bool emitSignals=false)
Sets the color for the widget.
static const QPixmap & transparentBackground()
Generates a checkboard pattern pixmap for use as a background to transparent colors.
ColorComponent
Specifies the color component which the widget alters.
@ Hue
Hue component of color (based on HSV model)
@ Alpha
Alpha component (opacity) of color.
@ Green
Green component of color.
@ Red
Red component of color.
@ Saturation
Saturation component of color (based on HSV model)
@ Blue
Blue component of color.
@ Value
Value component of color (based on HSV model)
@ Multiple
Widget alters multiple color components.
void dragEnterEvent(QDragEnterEvent *e) override
This class is a composition of two QSettings instances:
Definition: qgssettings.h:62
void setEnumValue(const QString &key, const T &value, const Section section=NoSection)
Set the value of a setting based on an enum.
Definition: qgssettings.h:335
T enumValue(const QString &key, const T &defaultValue, const Section section=NoSection)
Returns the setting value for a setting based on an enum.
Definition: qgssettings.h:283
static QColor parseColorWithAlpha(const QString &colorStr, bool &containsAlpha, bool strictEval=false)
Attempts to parse a string as a color using a variety of common formats, including hex codes,...
static QColor colorFromMimeData(const QMimeData *data, bool &hasAlpha)
Attempts to parse mime data as a color.
static QMimeData * colorToMimeData(const QColor &color)
Creates mime data from a color.
double ANALYSIS_EXPORT angle(QgsPoint *p1, QgsPoint *p2, QgsPoint *p3, QgsPoint *p4)
Calculates the angle between two segments (in 2 dimension, z-values are ignored)
Definition: MathUtils.cpp:786
QSize iconSize(bool dockableToolbar)
Returns the user-preferred size of a window's toolbar icons.
int scaleIconSize(int standardSize)
Scales an icon size to compensate for display pixel density, making the icon size hi-dpi friendly,...
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c