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