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