QGIS API Documentation  3.22.4-Białowieża (ce8e65e95e)
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  setColorFromPos( event->pos() );
606 }
607 
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 
627 void QgsColorWheel::mouseReleaseEvent( QMouseEvent *event )
628 {
629  Q_UNUSED( event )
630  mClickedPart = QgsColorWheel::None;
631 }
632 
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 
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 
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 
752 {
753  delete mBoxImage;
754 }
755 
757 {
758  const int size = Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 22;
759  return QSize( size, size );
760 }
761 
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 
797 {
798  if ( component != mComponent )
799  {
800  //need to redraw
801  mDirty = true;
802  }
804 }
805 
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 
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 
844 void QgsColorBox::mouseMoveEvent( QMouseEvent *event )
845 {
846  setColorFromPoint( event->pos() );
848 }
849 
850 void QgsColorBox::mousePressEvent( QMouseEvent *event )
851 {
852  setColorFromPoint( event->pos() );
853 }
854 
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 
880 int QgsColorBox::valueRangeX() const
881 {
882  return componentRange( xComponent() );
883 }
884 
885 int QgsColorBox::valueRangeY() const
886 {
887  return componentRange( yComponent() );
888 }
889 
890 QgsColorWidget::ColorComponent QgsColorBox::yComponent() const
891 {
892  switch ( mComponent )
893  {
894  case QgsColorWidget::Red:
895  return QgsColorWidget::Green;
898  return QgsColorWidget::Red;
899  case QgsColorWidget::Hue:
903  return QgsColorWidget::Hue;
904  default:
905  //should not occur
906  return QgsColorWidget::Red;
907  }
908 }
909 
910 int QgsColorBox::yComponentValue() const
911 {
912  return componentValue( yComponent() );
913 }
914 
915 QgsColorWidget::ColorComponent QgsColorBox::xComponent() const
916 {
917  switch ( mComponent )
918  {
919  case QgsColorWidget::Red:
921  return QgsColorWidget::Blue;
923  return QgsColorWidget::Green;
924  case QgsColorWidget::Hue:
926  return QgsColorWidget:: Value;
929  default:
930  //should not occur
931  return QgsColorWidget::Red;
932  }
933 }
934 
935 int QgsColorBox::xComponentValue() const
936 {
937  return componentValue( xComponent() );
938 }
939 
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 
963  update();
964  emit colorChanged( color );
965 }
966 
967 
968 //
969 // QgsColorRampWidget
970 //
971 
973  const QgsColorWidget::ColorComponent component,
974  const Orientation orientation )
975  : QgsColorWidget( parent, component )
976 {
977  setFocusPolicy( Qt::StrongFocus );
979 
980  //create triangle polygons
981  setMarkerSize( 5 );
982 }
983 
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 
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 
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 
1119 {
1120  mOrientation = orientation;
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 
1135 {
1136  if ( margin == mMargin )
1137  {
1138  return;
1139  }
1140  mMargin = margin;
1141  update();
1142 }
1143 
1144 void QgsColorRampWidget::setShowFrame( const bool showFrame )
1145 {
1146  if ( showFrame == mShowFrame )
1147  {
1148  return;
1149  }
1150  mShowFrame = showFrame;
1151  update();
1152 }
1153 
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 
1162 void QgsColorRampWidget::mouseMoveEvent( QMouseEvent *event )
1163 {
1164  setColorFromPoint( event->pos() );
1166 }
1167 
1168 void QgsColorRampWidget::wheelEvent( QWheelEvent *event )
1169 {
1170  const int oldValue = componentValue();
1171 
1172  if ( event->angleDelta().y() > 0 )
1173  {
1175  }
1176  else
1177  {
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 
1191 void QgsColorRampWidget::mousePressEvent( QMouseEvent *event )
1192 {
1193  setColorFromPoint( event->pos() );
1194 }
1195 
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  {
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  {
1208  }
1209  else if ( ( mOrientation == QgsColorRampWidget::Horizontal && event->key() == Qt::Key_PageDown )
1210  || ( mOrientation == QgsColorRampWidget::Vertical && event->key() == Qt::Key_PageUp ) )
1211  {
1213  }
1214  else if ( ( mOrientation == QgsColorRampWidget::Horizontal && event->key() == Qt::Key_PageUp )
1215  || ( mOrientation == QgsColorRampWidget::Vertical && event->key() == Qt::Key_PageDown ) )
1216  {
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
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 
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 
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  }
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 
1308 {
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  }
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 
1330 {
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 
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 
1349 void QgsColorSliderWidget::rampColorChanged( const QColor &color )
1350 {
1351  emit colorChanged( color );
1352 }
1353 
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 
1362 void QgsColorSliderWidget::rampChanged( int value )
1363 {
1364  mSpinBox->blockSignals( true );
1365  mSpinBox->setValue( convertRealToDisplay( value ) );
1366  mSpinBox->blockSignals( false );
1367 }
1368 
1369 
1370 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!
1375  {
1376  return std::round( 100.0 * realValue / 255.0 );
1377  }
1378 
1379  //leave all other values intact
1380  return realValue;
1381 }
1382 
1383 int QgsColorSliderWidget::convertDisplayToReal( const int displayValue ) const
1384 {
1385  //scale saturation, value or alpha from 0->100 range (see note in convertRealToDisplay)
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 
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 
1431 void QgsColorTextWidget::setColor( const QColor &color, const bool emitSignals )
1432 {
1433  QgsColorWidget::setColor( color, emitSignals );
1434  updateText();
1435 }
1436 
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 
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 
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 
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 
1536  : QgsColorWidget( parent )
1537  , mColor2( QColor() )
1538 {
1539 
1540 }
1541 
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 
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 
1595 {
1596  return QSize( Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 22, Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 22 * 0.75 );
1597 }
1598 
1599 void QgsColorPreviewWidget::setColor2( const QColor &color )
1600 {
1601  if ( color == mColor2 )
1602  {
1603  return;
1604  }
1605  mColor2 = color;
1606  update();
1607 }
1608 
1610 {
1611  if ( e->button() == Qt::LeftButton )
1612  {
1613  mDragStartPosition = e->pos();
1614  }
1616 }
1617 
1619 {
1620  if ( ( e->pos() - mDragStartPosition ).manhattanLength() >= QApplication::startDragDistance() )
1621  {
1622  //mouse moved, so a drag. nothing to do here
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 
1643 {
1644  //handle dragging colors from button
1645 
1646  if ( !( e->buttons() & Qt::LeftButton ) )
1647  {
1648  //left button not depressed, so not a drag
1650  return;
1651  }
1652 
1653  if ( ( e->pos() - mDragStartPosition ).manhattanLength() < QApplication::startDragDistance() )
1654  {
1655  //mouse not moved, so not a drag
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 
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 
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 
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 }
static const double UI_SCALE_FACTOR
UI scaling factor.
Definition: qgis.h:1052
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 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 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:305
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:253
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