QGIS API Documentation 4.1.0-Master (60fea48833c)
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 ) / static_cast< float >( componentRange( mComponent ) ) );
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( std::min( event->size().width(), parentWidget()->size().width() - 2 ), std::min( event->size().height(), parentWidget()->size().height() - 2 ) );
587 resize( newSize );
588 createImages( newSize );
589 }
590 else
591 {
592 createImages( event->size() );
593 }
594#else
595 //recreate images for new size
596 createImages( event->size() );
597#endif
598}
599
600void QgsColorWheel::setColorFromPos( const QPointF pos )
601{
602 const QPointF center = QPointF( width() / 2.0, height() / 2.0 );
603 //line from center to mouse position
604 const QLineF line = QLineF( center.x(), center.y(), pos.x(), pos.y() );
605
606 QColor newColor = QColor();
607
608 float_type h, s, l, alpha;
609 mCurrentColor.getHslF( &h, &s, &l, &alpha );
610 //override hue with explicit hue, so we don't get -1 values from QColor for hue
611 h = hueF();
612
613 if ( mClickedPart == QgsColorWheel::Triangle )
614 {
615 //adapted from equations at https://github.com/timjb/colortriangle/blob/master/colortriangle.js by Tim Baumann
616
617 //position of event relative to triangle center
618 const double x = pos.x() - center.x();
619 const double y = pos.y() - center.y();
620
621 double eventAngleRadians = line.angle() * M_PI / 180.0;
622 const double hueRadians = h * 2 * M_PI;
623 double rad0 = std::fmod( eventAngleRadians + 2.0 * M_PI - hueRadians, 2.0 * M_PI );
624 double rad1 = std::fmod( rad0, ( ( 2.0 / 3.0 ) * M_PI ) ) - ( M_PI / 3.0 );
625 const double length = mWheelImage.width() / 2.0 / devicePixelRatioF();
626 const double triangleLength = length - mWheelThickness - 1;
627
628 const double a = 0.5 * triangleLength;
629 double b = std::tan( rad1 ) * a;
630 double r = std::sqrt( x * x + y * y );
631 const double maxR = std::sqrt( a * a + b * b );
632
633 if ( r > maxR )
634 {
635 const double dx = std::tan( rad1 ) * r;
636 double rad2 = std::atan( dx / maxR );
637 rad2 = std::min( rad2, M_PI / 3.0 );
638 rad2 = std::max( rad2, -M_PI / 3.0 );
639 eventAngleRadians += rad2 - rad1;
640 rad0 = std::fmod( eventAngleRadians + 2.0 * M_PI - hueRadians, 2.0 * M_PI );
641 rad1 = std::fmod( rad0, ( ( 2.0 / 3.0 ) * M_PI ) ) - ( M_PI / 3.0 );
642 b = std::tan( rad1 ) * a;
643 r = std::sqrt( a * a + b * b );
644 }
645
646 const double triangleSideLength = std::sqrt( 3.0 ) * triangleLength;
647 const double newL = ( ( -std::sin( rad0 ) * r ) / triangleSideLength ) + 0.5;
648 const double widthShare = 1.0 - ( std::fabs( newL - 0.5 ) * 2.0 );
649 const double newS = ( ( ( std::cos( rad0 ) * r ) + ( triangleLength / 2.0 ) ) / ( 1.5 * triangleLength ) ) / widthShare;
650 s = std::min( std::max( 0.f, static_cast<float>( newS ) ), 1.f );
651 l = std::min( std::max( 0.f, static_cast<float>( newL ) ), 1.f );
652 newColor = QColor::fromHslF( h, s, l );
653 //explicitly set the hue again, so that it's exact
654 newColor.setHsvF( h, newColor.hsvSaturationF(), newColor.valueF(), alpha );
655 }
656 else if ( mClickedPart == QgsColorWheel::Wheel )
657 {
658 //use hue angle
659 s = mCurrentColor.hsvSaturationF();
660 const float v = mCurrentColor.valueF(); // NOLINT(bugprone-narrowing-conversions): TODO QGIS 5 remove the nolint instructions, QColor was qreal (double) and is now float
661 const qreal newHue = line.angle() / HUE_MAX;
662 newColor = QColor::fromHsvF( static_cast<float>( newHue ), s, v, alpha );
663 //hue has changed, need to redraw triangle
664 mTriangleDirty = true;
665 }
666
667 if ( newColor.isValid() && newColor != mCurrentColor )
668 {
669 //color has changed
670 mCurrentColor = QColor( newColor );
671
672 if ( mCurrentColor.hueF() >= 0 )
673 {
674 //color has a valid hue, so update the QgsColorWidget's explicit hue
675 mExplicitHue = mCurrentColor.hueF(); // NOLINT(bugprone-narrowing-conversions): TODO QGIS 5 remove the nolint instructions, QColor was qreal (double) and is now float
676 }
677
678 update();
680 }
681}
682
683void QgsColorWheel::mouseMoveEvent( QMouseEvent *event )
684{
685 if ( mIsDragging )
686 setColorFromPos( event->pos() );
687
689}
690
691void QgsColorWheel::mousePressEvent( QMouseEvent *event )
692{
693 if ( event->button() == Qt::LeftButton )
694 {
695 mIsDragging = true;
696 //calculate where the event occurred -- on the wheel or inside the triangle?
697
698 //create a line from the widget's center to the event
699 const QLineF line = QLineF( width() / 2.0, height() / 2.0, event->pos().x(), event->pos().y() );
700
701 const double innerLength = mWheelImage.width() / 2.0 / devicePixelRatioF() - mWheelThickness;
702 if ( line.length() < innerLength )
703 {
704 mClickedPart = QgsColorWheel::Triangle;
705 }
706 else
707 {
708 mClickedPart = QgsColorWheel::Wheel;
709 }
710 setColorFromPos( event->pos() );
711 }
712 else
713 {
715 }
716}
717
718void QgsColorWheel::mouseReleaseEvent( QMouseEvent *event )
719{
720 if ( event->button() == Qt::LeftButton )
721 {
722 mIsDragging = false;
723 mClickedPart = QgsColorWheel::None;
724 }
725 else
726 {
728 }
729}
730
731void QgsColorWheel::createWheel()
732{
733 if ( mWheelImage.isNull() )
734 {
735 return;
736 }
737
738 const int maxSize = std::min( mWheelImage.width(), mWheelImage.height() );
739 const double wheelRadius = maxSize / 2.0;
740
741 mWheelImage.fill( Qt::transparent );
742 QPainter p( &mWheelImage );
743 p.setRenderHint( QPainter::Antialiasing );
744 p.setBrush( mWheelBrush );
745 p.setPen( Qt::NoPen );
746
747 //draw hue wheel as a circle
748 p.translate( wheelRadius, wheelRadius );
749 p.drawEllipse( QPointF( 0.0, 0.0 ), wheelRadius, wheelRadius );
750
751 //cut hole in center of circle to make a ring
752 p.setCompositionMode( QPainter::CompositionMode_DestinationOut );
753 p.setBrush( QBrush( Qt::black ) );
754 p.drawEllipse( QPointF( 0, 0 ), wheelRadius - mWheelThickness * devicePixelRatioF(), wheelRadius - mWheelThickness * devicePixelRatioF() );
755 p.end();
756
757 mWheelDirty = false;
758}
759
760void QgsColorWheel::createTriangle()
761{
762 if ( mWheelImage.isNull() || mTriangleImage.isNull() )
763 {
764 return;
765 }
766
767 const QPointF center = QPointF( mWheelImage.width() / 2.0, mWheelImage.height() / 2.0 );
768 mTriangleImage.fill( Qt::transparent );
769
770 QPainter imagePainter( &mTriangleImage );
771 imagePainter.setRenderHint( QPainter::Antialiasing );
772
773 const float angle = hueF();
774 const float angleDegree = angle * HUE_MAX;
775 const double wheelRadius = mWheelImage.width() / 2.0;
776 const double triangleRadius = wheelRadius - mWheelThickness * devicePixelRatioF() - 1;
777
778 //pure version of hue (at full saturation and value)
779 const QColor pureColor = QColor::fromHsvF( angle, 1., 1. );
780 //create copy of color but with 0 alpha
781 QColor alphaColor = QColor( pureColor );
782 alphaColor.setAlpha( 0 );
783
784 //some rather ugly shortcuts to obtain corners and midpoints of triangle
785 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 ) );
786 QLineF line2 = QLineF( center.x(), center.y(), center.x() + triangleRadius, center.y() );
787 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 ) );
788 QLineF line4 = QLineF( center.x(), center.y(), center.x() - triangleRadius * std::cos( M_PI / 3.0 ), center.y() );
789 QLineF line5 = QLineF( center.x(), center.y(), ( line2.p2().x() + line1.p2().x() ) / 2.0, ( line2.p2().y() + line1.p2().y() ) / 2.0 );
790 line1.setAngle( line1.angle() + angleDegree );
791 line2.setAngle( line2.angle() + angleDegree );
792 line3.setAngle( line3.angle() + angleDegree );
793 line4.setAngle( line4.angle() + angleDegree );
794 line5.setAngle( line5.angle() + angleDegree );
795 const QPointF p1 = line1.p2();
796 const QPointF p2 = line2.p2();
797 const QPointF p3 = line3.p2();
798 const QPointF p4 = line4.p2();
799 const QPointF p5 = line5.p2();
800
801 //inspired by Tim Baumann's work at https://github.com/timjb/colortriangle/blob/master/colortriangle.js
802 QLinearGradient colorGrad = QLinearGradient( p4.x(), p4.y(), p2.x(), p2.y() );
803 colorGrad.setColorAt( 0, alphaColor );
804 colorGrad.setColorAt( 1, pureColor );
805 QLinearGradient whiteGrad = QLinearGradient( p3.x(), p3.y(), p5.x(), p5.y() );
806 whiteGrad.setColorAt( 0, QColor( 255, 255, 255, 255 ) );
807 whiteGrad.setColorAt( 1, QColor( 255, 255, 255, 0 ) );
808
809 QPolygonF triangle;
810 triangle << p2 << p1 << p3 << p2;
811 imagePainter.setPen( Qt::NoPen );
812 //start with a black triangle
813 imagePainter.setBrush( QBrush( Qt::black ) );
814 imagePainter.drawPolygon( triangle );
815 //draw a gradient from transparent to the pure color at the triangle's tip
816 imagePainter.setBrush( QBrush( colorGrad ) );
817 imagePainter.drawPolygon( triangle );
818 //draw a white gradient using additive composition mode
819 imagePainter.setCompositionMode( QPainter::CompositionMode_Plus );
820 imagePainter.setBrush( QBrush( whiteGrad ) );
821 imagePainter.drawPolygon( triangle );
822
823 //above process results in some small artifacts on the edge of the triangle. Let's clear these up
824 //use source composition mode and draw an outline using a transparent pen
825 //this clears the edge pixels and leaves a nice smooth image
826 imagePainter.setCompositionMode( QPainter::CompositionMode_Source );
827 imagePainter.setBrush( Qt::NoBrush );
828 imagePainter.setPen( QPen( Qt::transparent ) );
829 imagePainter.drawPolygon( triangle );
830
831 imagePainter.end();
832 mTriangleDirty = false;
833}
834
835
836//
837// QgsColorBox
838//
839
841 : QgsColorWidget( parent, component )
842{
843 setFocusPolicy( Qt::StrongFocus );
844 setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding );
845
846 mBoxImage = new QImage( width() - mMargin * 2, height() - mMargin * 2, QImage::Format_RGB32 );
847}
848
850{
851 delete mBoxImage;
852}
853
855{
856 const int size = Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 22;
857 return QSize( size, size );
858}
859
860void QgsColorBox::paintEvent( QPaintEvent *event )
861{
862 Q_UNUSED( event )
863 QPainter painter( this );
864
865 QStyleOptionFrame option;
866 option.initFrom( this );
867 option.state = hasFocus() ? QStyle::State_Active : QStyle::State_None;
868 style()->drawPrimitive( QStyle::PE_Frame, &option, &painter );
869
870 if ( mDirty )
871 {
872 createBox();
873 }
874
875 //draw background image
876 painter.drawImage( QPoint( mMargin, mMargin ), *mBoxImage );
877
878 //draw cross lines
879 const double h = height();
880 const double w = width();
881 const double margin = mMargin;
882 const double xPos = ( mMargin + ( w - 2 * mMargin - 1 ) * xComponentValue() );
883 const double yPos = ( mMargin + ( h - 2 * mMargin - 1 ) - ( h - 2 * mMargin - 1 ) * yComponentValue() );
884
885 painter.setBrush( Qt::white );
886 painter.setPen( Qt::NoPen );
887
888 painter.drawRect( QRectF( xPos - 1, mMargin, 3, height() - 2 * margin - 1 ) );
889 painter.drawRect( QRectF( mMargin, yPos - 1, width() - 2 * margin - 1, 3 ) );
890 painter.setPen( Qt::black );
891 painter.drawLine( QLineF( xPos, mMargin, xPos, height() - margin - 1 ) );
892 painter.drawLine( QLineF( mMargin, yPos, width() - margin - 1, yPos ) );
893
894 painter.end();
895}
896
898{
899 if ( component != mComponent )
900 {
901 //need to redraw
902 mDirty = true;
903 }
905}
906
907void QgsColorBox::setColor( const QColor &color, const bool emitSignals )
908{
909 //check if we need to redraw the box image
910 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() ) ) );
911
912 QgsColorWidget::setColor( color, emitSignals );
913}
914
915void QgsColorBox::resizeEvent( QResizeEvent *event )
916{
917 mDirty = true;
918 delete mBoxImage;
919 mBoxImage = new QImage( event->size().width() - mMargin * 2, event->size().height() - mMargin * 2, QImage::Format_RGB32 );
920 QgsColorWidget::resizeEvent( event );
921}
922
923void QgsColorBox::mouseMoveEvent( QMouseEvent *event )
924{
925 if ( mIsDragging )
926 {
927 setColorFromPoint( event->pos() );
928 }
930}
931
932void QgsColorBox::mousePressEvent( QMouseEvent *event )
933{
934 if ( event->button() == Qt::LeftButton )
935 {
936 mIsDragging = true;
937 setColorFromPoint( event->pos() );
938 }
939 else
940 {
942 }
943}
944
945void QgsColorBox::mouseReleaseEvent( QMouseEvent *event )
946{
947 if ( event->button() == Qt::LeftButton )
948 {
949 mIsDragging = false;
950 }
951 else
952 {
954 }
955}
956
957void QgsColorBox::createBox()
958{
959 const int maxValueX = mBoxImage->width();
960 const int maxValueY = mBoxImage->height();
961
962 //create a temporary color object
963 QColor currentColor = QColor( mCurrentColor );
964 float colorComponentValue;
965
966 for ( int y = 0; y < maxValueY; ++y )
967 {
968 QRgb *scanLine = ( QRgb * ) mBoxImage->scanLine( y );
969
970 colorComponentValue = 1.f - static_cast<float>( y ) / static_cast<float>( maxValueY );
971 alterColorF( currentColor, yComponent(), colorComponentValue );
972 for ( int x = 0; x < maxValueX; ++x )
973 {
974 colorComponentValue = static_cast<float>( x ) / static_cast<float>( maxValueY );
975 alterColorF( currentColor, xComponent(), colorComponentValue );
976 scanLine[x] = currentColor.rgb();
977 }
978 }
979 mDirty = false;
980}
981
982float QgsColorBox::valueRangeX() const
983{
984 return static_cast<float>( componentRange( xComponent() ) );
985}
986
987float QgsColorBox::valueRangeY() const
988{
989 return static_cast<float>( componentRange( yComponent() ) );
990}
991
992QgsColorWidget::ColorComponent QgsColorBox::yComponent() const
993{
994 switch ( mComponent )
995 {
1000 return QgsColorWidget::Red;
1001
1006 return QgsColorWidget::Hue;
1007
1013
1014 default:
1015 //should not occur
1016 return QgsColorWidget::Red;
1017 }
1018}
1019
1020float QgsColorBox::yComponentValue() const
1021{
1022 return componentValueF( yComponent() );
1023}
1024
1025QgsColorWidget::ColorComponent QgsColorBox::xComponent() const
1026{
1027 switch ( mComponent )
1028 {
1031 return QgsColorWidget::Blue;
1033 return QgsColorWidget::Green;
1034
1037 return QgsColorWidget::Value;
1040
1043 return QgsColorWidget::Cyan;
1046
1047 default:
1048 //should not occur
1049 return QgsColorWidget::Red;
1050 }
1051}
1052
1053float QgsColorBox::xComponentValue() const
1054{
1055 return componentValueF( xComponent() );
1056}
1057
1058void QgsColorBox::setColorFromPoint( QPoint point )
1059{
1060 const float x = static_cast<float>( point.x() );
1061 const float y = static_cast<float>( point.y() );
1062 const float w = static_cast<float>( width() );
1063 const float h = static_cast<float>( height() );
1064
1065 float valX = ( x - mMargin ) / ( w - 2 * mMargin - 1 );
1066 float valY = 1.f - ( y - mMargin ) / ( h - 2 * mMargin - 1 );
1067
1068 QColor color = QColor( mCurrentColor );
1069 alterColorF( color, xComponent(), valX );
1070 alterColorF( color, yComponent(), valY );
1071
1072 if ( color == mCurrentColor )
1073 {
1074 return;
1075 }
1076
1077 if ( color.hueF() >= 0 )
1078 {
1079 mExplicitHue = color.hueF(); // NOLINT(bugprone-narrowing-conversions): TODO QGIS 5 remove the nolint instructions, QColor was qreal (double) and is now float
1080 }
1081
1083 update();
1084 emit colorChanged( color );
1085}
1086
1087
1088//
1089// QgsColorRampWidget
1090//
1091
1093 : QgsColorWidget( parent, component )
1094{
1095 setFocusPolicy( Qt::StrongFocus );
1097
1098 //create triangle polygons
1099 setMarkerSize( 5 );
1100}
1101
1103{
1104 if ( mOrientation == QgsColorRampWidget::Horizontal )
1105 {
1106 //horizontal
1107 return QSize( Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 22, Qgis::UI_SCALE_FACTOR * fontMetrics().height() * 1.3 );
1108 }
1109 else
1110 {
1111 //vertical
1112 return QSize( Qgis::UI_SCALE_FACTOR * fontMetrics().height() * 1.3, Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 22 );
1113 }
1114}
1115
1116void QgsColorRampWidget::paintEvent( QPaintEvent *event )
1117{
1118 Q_UNUSED( event )
1119 QPainter painter( this );
1120
1121 if ( mShowFrame )
1122 {
1123 //draw frame
1124 QStyleOptionFrame option;
1125 option.initFrom( this );
1126 option.state = hasFocus() ? QStyle::State_KeyboardFocusChange : QStyle::State_None;
1127 style()->drawPrimitive( QStyle::PE_Frame, &option, &painter );
1128 }
1129
1130 if ( hasFocus() )
1131 {
1132 //draw focus rect
1133 QStyleOptionFocusRect option;
1134 option.initFrom( this );
1135 option.state = QStyle::State_KeyboardFocusChange;
1136 style()->drawPrimitive( QStyle::PE_FrameFocusRect, &option, &painter );
1137 }
1138
1139 float w = static_cast<float>( width() );
1140 float h = static_cast<float>( height() );
1141 float margin = static_cast<float>( mMargin );
1143 {
1144 const int maxValue = ( mOrientation == QgsColorRampWidget::Horizontal ? width() : height() ) - 1 - 2 * mMargin;
1145 QColor color = QColor( mCurrentColor );
1146 color.setAlphaF( 1.f );
1147 QPen pen;
1148 // we need to set pen width to 1,
1149 // since on retina displays
1150 // pen.setWidth(0) <=> pen.width = 0.5
1151 // see https://github.com/qgis/QGIS/issues/23900
1152 pen.setWidth( 1 );
1153 painter.setPen( pen );
1154 painter.setBrush( Qt::NoBrush );
1155
1156 //draw background ramp
1157 for ( int c = 0; c <= maxValue; ++c )
1158 {
1159 float colorVal = static_cast<float>( c ) / static_cast<float>( maxValue );
1160 //vertical sliders are reversed
1161 if ( mOrientation == QgsColorRampWidget::Vertical )
1162 {
1163 colorVal = 1.f - colorVal;
1164 }
1165 alterColorF( color, mComponent, colorVal );
1166 if ( color.hueF() < 0 )
1167 {
1168 color.setHsvF( hueF(), color.saturationF(), color.valueF() );
1169 }
1170 pen.setColor( color );
1171 painter.setPen( pen );
1172 if ( mOrientation == QgsColorRampWidget::Horizontal )
1173 {
1174 //horizontal
1175 painter.drawLine( QLineF( c + mMargin, mMargin, c + mMargin, height() - mMargin - 1 ) );
1176 }
1177 else
1178 {
1179 //vertical
1180 painter.drawLine( QLineF( mMargin, c + mMargin, width() - mMargin - 1, c + mMargin ) );
1181 }
1182 }
1183 }
1184 else
1185 {
1186 //alpha ramps are drawn differently
1187 //start with the checkboard pattern
1188 const QBrush checkBrush = QBrush( transparentBackground() );
1189 painter.setBrush( checkBrush );
1190 painter.setPen( Qt::NoPen );
1191 painter.drawRect( QRectF( margin, margin, w - 2 * margin - 1, h - 2 * margin - 1 ) );
1192 QLinearGradient colorGrad;
1193 if ( mOrientation == QgsColorRampWidget::Horizontal )
1194 {
1195 //horizontal
1196 colorGrad = QLinearGradient( margin, 0, w - margin - 1, 0 );
1197 }
1198 else
1199 {
1200 //vertical
1201 colorGrad = QLinearGradient( 0, margin, 0, h - margin - 1 );
1202 }
1203 QColor transparent = QColor( mCurrentColor );
1204 transparent.setAlpha( 0 );
1205 colorGrad.setColorAt( 0, transparent );
1206 QColor opaque = QColor( mCurrentColor );
1207 opaque.setAlpha( 255 );
1208 colorGrad.setColorAt( 1, opaque );
1209 const QBrush colorBrush = QBrush( colorGrad );
1210 painter.setBrush( colorBrush );
1211 painter.drawRect( QRectF( margin, margin, w - 2 * margin - 1, h - 2 * margin - 1 ) );
1212 }
1213
1214 if ( mOrientation == QgsColorRampWidget::Horizontal )
1215 {
1216 //draw marker triangles for horizontal ramps
1217 painter.setRenderHint( QPainter::Antialiasing );
1218 painter.setBrush( QBrush( Qt::black ) );
1219 painter.setPen( Qt::NoPen );
1220 painter.translate( margin + ( w - 2 * margin ) * componentValueF(), margin - 1 );
1221 painter.drawPolygon( mTopTriangle );
1222 painter.translate( 0, h - margin - 2 );
1223 painter.setBrush( QBrush( Qt::white ) );
1224 painter.drawPolygon( mBottomTriangle );
1225 painter.end();
1226 }
1227 else
1228 {
1229 //draw cross lines for vertical ramps
1230 const double ypos = margin + ( h - 2 * margin - 1 ) - ( h - 2 * margin - 1 ) * componentValueF();
1231 painter.setBrush( Qt::white );
1232 painter.setPen( Qt::NoPen );
1233 painter.drawRect( QRectF( margin, ypos - 1, w - 2 * margin - 1, 3 ) );
1234 painter.setPen( Qt::black );
1235 painter.drawLine( QLineF( margin, ypos, w - margin - 1, ypos ) );
1236 }
1237}
1238
1240{
1241 mOrientation = orientation;
1243 {
1244 //horizontal
1245 setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Fixed );
1246 }
1247 else
1248 {
1249 //vertical
1250 setSizePolicy( QSizePolicy::Fixed, QSizePolicy::MinimumExpanding );
1251 }
1252 updateGeometry();
1253}
1254
1256{
1257 if ( margin == mMargin )
1258 {
1259 return;
1260 }
1261 mMargin = margin;
1262 update();
1263}
1264
1266{
1267 if ( showFrame == mShowFrame )
1268 {
1269 return;
1270 }
1271 mShowFrame = showFrame;
1272 update();
1273}
1274
1275void QgsColorRampWidget::setMarkerSize( const int markerSize )
1276{
1277 //create triangle polygons
1278 mTopTriangle << QPoint( -markerSize, 0 ) << QPoint( markerSize, 0 ) << QPoint( 0, markerSize );
1279 mBottomTriangle << QPoint( -markerSize, 0 ) << QPoint( markerSize, 0 ) << QPoint( 0, -markerSize );
1280 update();
1281}
1282
1283void QgsColorRampWidget::mouseMoveEvent( QMouseEvent *event )
1284{
1285 if ( mIsDragging )
1286 {
1287 setColorFromPoint( event->pos() );
1288 }
1289
1291}
1292
1293void QgsColorRampWidget::wheelEvent( QWheelEvent *event )
1294{
1295 const float oldValue = componentValueF();
1296 const float delta = 1.f / static_cast<float>( componentRange() );
1297 if ( event->angleDelta().y() > 0 )
1298 {
1299 setComponentValueF( oldValue + delta );
1300 }
1301 else
1302 {
1303 setComponentValueF( oldValue - delta );
1304 }
1305
1306 if ( !qgsDoubleNear( componentValueF(), oldValue ) )
1307 {
1308 //value has changed
1311 emit valueChanged( componentValue() );
1314 }
1315
1316 event->accept();
1317}
1318
1319void QgsColorRampWidget::mousePressEvent( QMouseEvent *event )
1320{
1321 if ( event->button() == Qt::LeftButton )
1322 {
1323 mIsDragging = true;
1324 setColorFromPoint( event->pos() );
1325 }
1326 else
1327 {
1329 }
1330}
1331
1333{
1334 if ( event->button() == Qt::LeftButton )
1335 {
1336 mIsDragging = false;
1337 }
1338 else
1339 {
1341 }
1342}
1343
1345{
1346 const float oldValue = componentValueF();
1347 const float delta = 1.f / static_cast<float>( componentRange() );
1348 if ( ( mOrientation == QgsColorRampWidget::Horizontal && ( event->key() == Qt::Key_Right || event->key() == Qt::Key_Up ) )
1349 || ( mOrientation == QgsColorRampWidget::Vertical && ( event->key() == Qt::Key_Left || event->key() == Qt::Key_Up ) ) )
1350 {
1351 setComponentValueF( oldValue + delta );
1352 }
1353 else if ( ( mOrientation == QgsColorRampWidget::Horizontal && ( event->key() == Qt::Key_Left || event->key() == Qt::Key_Down ) )
1354 || ( mOrientation == QgsColorRampWidget::Vertical && ( event->key() == Qt::Key_Right || event->key() == Qt::Key_Down ) ) )
1355 {
1356 setComponentValueF( oldValue - delta );
1357 }
1358 else if ( ( mOrientation == QgsColorRampWidget::Horizontal && event->key() == Qt::Key_PageDown ) || ( mOrientation == QgsColorRampWidget::Vertical && event->key() == Qt::Key_PageUp ) )
1359 {
1360 setComponentValueF( oldValue + 10 * delta );
1361 }
1362 else if ( ( mOrientation == QgsColorRampWidget::Horizontal && event->key() == Qt::Key_PageUp ) || ( mOrientation == QgsColorRampWidget::Vertical && event->key() == Qt::Key_PageDown ) )
1363 {
1364 setComponentValueF( oldValue - 10 * delta );
1365 }
1366 else if ( ( mOrientation == QgsColorRampWidget::Horizontal && event->key() == Qt::Key_Home ) || ( mOrientation == QgsColorRampWidget::Vertical && event->key() == Qt::Key_End ) )
1367 {
1368 setComponentValueF( 0 );
1369 }
1370 else if ( ( mOrientation == QgsColorRampWidget::Horizontal && event->key() == Qt::Key_End ) || ( mOrientation == QgsColorRampWidget::Vertical && event->key() == Qt::Key_Home ) )
1371 {
1372 //set to maximum value
1373 setComponentValueF( 1.f );
1374 }
1375 else
1376 {
1377 QgsColorWidget::keyPressEvent( event );
1378 return;
1379 }
1380
1381 if ( !qgsDoubleNear( componentValueF(), oldValue ) )
1382 {
1383 //value has changed
1386 emit valueChanged( componentValue() );
1389 }
1390}
1391
1392void QgsColorRampWidget::setColorFromPoint( QPointF point )
1393{
1394 const float oldValue = componentValueF();
1395 float val;
1396 const float margin = static_cast<float>( mMargin );
1397 const float w = static_cast<float>( width() );
1398 const float h = static_cast<float>( height() );
1399
1400 if ( mOrientation == QgsColorRampWidget::Horizontal )
1401 {
1402 val = ( static_cast<float>( point.x() ) - margin ) / ( w - 2 * margin );
1403 }
1404 else
1405 {
1406 val = 1.f - ( static_cast<float>( point.y() ) - margin ) / ( h - 2 * margin );
1407 }
1408 setComponentValueF( val );
1409
1410 if ( !qgsDoubleNear( componentValueF(), oldValue ) )
1411 {
1412 //value has changed
1415 emit valueChanged( componentValue() );
1418 }
1419}
1420
1421//
1422// QgsColorSliderWidget
1423//
1424
1426 : QgsColorWidget( parent, component )
1427
1428{
1429 QHBoxLayout *hLayout = new QHBoxLayout();
1430 hLayout->setContentsMargins( 0, 0, 0, 0 );
1431 hLayout->setSpacing( 5 );
1432
1433 mRampWidget = new QgsColorRampWidget( nullptr, component );
1434 mRampWidget->setColor( mCurrentColor );
1435 hLayout->addWidget( mRampWidget, 1 );
1436
1437 mSpinBox = new QgsDoubleSpinBox();
1438 mSpinBox->setShowClearButton( false );
1439 //set spinbox to a reasonable width
1440 const int largestCharWidth = mSpinBox->fontMetrics().horizontalAdvance( u"888%"_s );
1441 mSpinBox->setMinimumWidth( largestCharWidth + 35 );
1442 mSpinBox->setMinimum( 0 );
1443 mSpinBox->setMaximum( convertRealToDisplay( 1.f ) );
1444 mSpinBox->setValue( convertRealToDisplay( componentValueF() ) );
1445 hLayout->addWidget( mSpinBox );
1446 setLayout( hLayout );
1447
1448 connect( mRampWidget, &QgsColorRampWidget::valueChangedF, this, &QgsColorSliderWidget::rampChanged );
1449 connect( mRampWidget, &QgsColorWidget::colorChanged, this, &QgsColorSliderWidget::rampColorChanged );
1450 connect( mSpinBox, static_cast<void ( QDoubleSpinBox::* )( double )>( &QDoubleSpinBox::valueChanged ), this, &QgsColorSliderWidget::spinChanged );
1451}
1452
1454{
1456 mRampWidget->setComponent( component );
1457 mSpinBox->setMaximum( convertRealToDisplay( static_cast<float>( componentRange() ) ) );
1458
1459 switch ( componentUnit( component ) )
1460 {
1462 mSpinBox->setSuffix( QChar( 176 ) );
1463 break;
1464
1466 mSpinBox->setSuffix( tr( "%" ) );
1467 break;
1468
1470 //clear suffix
1471 mSpinBox->setSuffix( QString() );
1472 }
1473}
1474
1476{
1478 mRampWidget->blockSignals( true );
1479 mRampWidget->setComponentValueF( value );
1480 mRampWidget->blockSignals( false );
1481 mSpinBox->blockSignals( true );
1482 mSpinBox->setValue( convertRealToDisplay( value ) );
1483 mSpinBox->blockSignals( false );
1484}
1485
1486void QgsColorSliderWidget::setColor( const QColor &color, bool emitSignals )
1487{
1488 QgsColorWidget::setColor( color, emitSignals );
1489 mRampWidget->setColor( color );
1490 mSpinBox->blockSignals( true );
1491 mSpinBox->setValue( convertRealToDisplay( componentValueF() ) );
1492 mSpinBox->blockSignals( false );
1493}
1494
1495void QgsColorSliderWidget::rampColorChanged( const QColor &color )
1496{
1497 emit colorChanged( color );
1498}
1499
1500void QgsColorSliderWidget::spinChanged( double value )
1501{
1502 const float convertedValue = convertDisplayToReal( static_cast<float>( value ) );
1503 QgsColorWidget::setComponentValueF( convertedValue );
1504 mRampWidget->setComponentValueF( convertedValue );
1506}
1507
1508void QgsColorSliderWidget::rampChanged( float value )
1509{
1510 mSpinBox->blockSignals( true );
1511 mSpinBox->setValue( convertRealToDisplay( value ) );
1512 mSpinBox->blockSignals( false );
1513}
1514
1515
1516float QgsColorSliderWidget::convertRealToDisplay( const float realValue ) const
1517{
1518 switch ( componentUnit( mComponent ) )
1519 {
1521 return realValue * 100.f;
1522
1524 return realValue * HUE_MAX;
1525
1527 return realValue * 255.f;
1528 }
1529
1531}
1532
1533float QgsColorSliderWidget::convertDisplayToReal( const float displayValue ) const
1534{
1535 switch ( componentUnit( mComponent ) )
1536 {
1538 return displayValue / 100.f;
1539
1541 return displayValue / HUE_MAX;
1542
1544 return displayValue / 255.f;
1545 }
1546
1548}
1549
1550//
1551// QgsColorTextWidget
1552//
1553
1555 : QgsColorWidget( parent )
1556{
1557 QHBoxLayout *hLayout = new QHBoxLayout();
1558 hLayout->setContentsMargins( 0, 0, 0, 0 );
1559 hLayout->setSpacing( 0 );
1560
1561 mLineEdit = new QLineEdit( nullptr );
1562 hLayout->addWidget( mLineEdit );
1563
1564 mMenuButton = new QToolButton( mLineEdit );
1565 mMenuButton->setIcon( QgsApplication::getThemeIcon( u"/mIconDropDownMenu.svg"_s ) );
1566 mMenuButton->setCursor( Qt::ArrowCursor );
1567 mMenuButton->setFocusPolicy( Qt::NoFocus );
1568 mMenuButton->setStyleSheet( u"QToolButton { border: none; padding: 0px; }"_s );
1569
1570 setLayout( hLayout );
1571
1572 const int frameWidth = mLineEdit->style()->pixelMetric( QStyle::PM_DefaultFrameWidth );
1573 mLineEdit->setStyleSheet( u"QLineEdit { padding-right: %1px; } "_s.arg( mMenuButton->sizeHint().width() + frameWidth + 1 ) );
1574
1575 connect( mLineEdit, &QLineEdit::editingFinished, this, &QgsColorTextWidget::textChanged );
1576 connect( mMenuButton, &QAbstractButton::clicked, this, &QgsColorTextWidget::showMenu );
1577
1578 //restore format setting
1579 QgsSettings settings;
1580 mFormat = settings.enumValue( u"ColorWidgets/textWidgetFormat"_s, HexRgb );
1581
1582 updateText();
1583}
1584
1585void QgsColorTextWidget::setColor( const QColor &color, const bool emitSignals )
1586{
1587 QgsColorWidget::setColor( color, emitSignals );
1588 updateText();
1589}
1590
1591void QgsColorTextWidget::resizeEvent( QResizeEvent *event )
1592{
1593 Q_UNUSED( event )
1594 const QSize sz = mMenuButton->sizeHint();
1595 const int frameWidth = style()->pixelMetric( QStyle::PM_DefaultFrameWidth );
1596 mMenuButton->move( mLineEdit->rect().right() - frameWidth - sz.width(), ( mLineEdit->rect().bottom() + 1 - sz.height() ) / 2 );
1597}
1598
1599void QgsColorTextWidget::updateText()
1600{
1601 switch ( mFormat )
1602 {
1603 case HexRgb:
1604 mLineEdit->setText( mCurrentColor.name() );
1605 break;
1606 case HexRgbA:
1607 mLineEdit->setText( mCurrentColor.name() + u"%1"_s.arg( mCurrentColor.alpha(), 2, 16, QChar( '0' ) ) );
1608 break;
1609 case Rgb:
1610 mLineEdit->setText( tr( "rgb( %1, %2, %3 )" ).arg( mCurrentColor.red() ).arg( mCurrentColor.green() ).arg( mCurrentColor.blue() ) );
1611 break;
1612 case Rgba:
1613 mLineEdit->setText( tr( "rgba( %1, %2, %3, %4 )" ).arg( mCurrentColor.red() ).arg( mCurrentColor.green() ).arg( mCurrentColor.blue() ).arg( QString::number( mCurrentColor.alphaF(), 'f', 2 ) ) );
1614 break;
1615 }
1616}
1617
1618void QgsColorTextWidget::textChanged()
1619{
1620 const QString testString = mLineEdit->text();
1621 bool containsAlpha;
1622 QColor color = QgsSymbolLayerUtils::parseColorWithAlpha( testString, containsAlpha );
1623 if ( !color.isValid() )
1624 {
1625 //bad color string
1626 updateText();
1627 return;
1628 }
1629
1630 //good color string
1631 if ( color != mCurrentColor )
1632 {
1633 //retain alpha if no explicit alpha set
1634 if ( !containsAlpha )
1635 {
1636 color.setAlpha( mCurrentColor.alpha() );
1637 }
1638 //color has changed
1641 }
1642 updateText();
1643}
1644
1645void QgsColorTextWidget::showMenu()
1646{
1647 QMenu colorContextMenu;
1648 QAction *hexRgbaAction = nullptr;
1649 QAction *rgbaAction = nullptr;
1650
1651 QAction *hexRgbAction = new QAction( tr( "#RRGGBB" ), nullptr );
1652 colorContextMenu.addAction( hexRgbAction );
1653 if ( mAllowAlpha )
1654 {
1655 hexRgbaAction = new QAction( tr( "#RRGGBBAA" ), nullptr );
1656 colorContextMenu.addAction( hexRgbaAction );
1657 }
1658 QAction *rgbAction = new QAction( tr( "rgb( r, g, b )" ), nullptr );
1659 colorContextMenu.addAction( rgbAction );
1660 if ( mAllowAlpha )
1661 {
1662 rgbaAction = new QAction( tr( "rgba( r, g, b, a )" ), nullptr );
1663 colorContextMenu.addAction( rgbaAction );
1664 }
1665
1666 QAction *selectedAction = colorContextMenu.exec( QCursor::pos() );
1667 if ( selectedAction == hexRgbAction )
1668 {
1670 }
1671 else if ( hexRgbaAction && selectedAction == hexRgbaAction )
1672 {
1674 }
1675 else if ( selectedAction == rgbAction )
1676 {
1677 mFormat = QgsColorTextWidget::Rgb;
1678 }
1679 else if ( rgbaAction && selectedAction == rgbaAction )
1680 {
1681 mFormat = QgsColorTextWidget::Rgba;
1682 }
1683
1684 //save format setting
1685 QgsSettings settings;
1686 settings.setEnumValue( u"ColorWidgets/textWidgetFormat"_s, mFormat );
1687
1688 updateText();
1689}
1690
1691void QgsColorTextWidget::setAllowOpacity( const bool allowOpacity )
1692{
1693 mAllowAlpha = allowOpacity;
1694}
1695
1696//
1697// QgsColorPreviewWidget
1698//
1699
1701 : QgsColorWidget( parent )
1702 , mColor2( QColor() )
1703{}
1704
1705void QgsColorPreviewWidget::drawColor( const QColor &color, QRect rect, QPainter &painter )
1706{
1707 painter.setPen( Qt::NoPen );
1708 //if color has an alpha, start with a checkboard pattern
1709 if ( color.alpha() < 255 )
1710 {
1711 const QBrush checkBrush = QBrush( transparentBackground() );
1712 painter.setBrush( checkBrush );
1713 painter.drawRect( rect );
1714
1715 //draw half of widget showing solid color, the other half showing color with alpha
1716
1717 //ensure at least a 1px overlap to avoid artifacts
1718 const QBrush colorBrush = QBrush( color );
1719 painter.setBrush( colorBrush );
1720 painter.drawRect( std::floor( rect.width() / 2.0 ) + rect.left(), rect.top(), rect.width() - std::floor( rect.width() / 2.0 ), rect.height() );
1721
1722 QColor opaqueColor = QColor( color );
1723 opaqueColor.setAlpha( 255 );
1724 const QBrush opaqueBrush = QBrush( opaqueColor );
1725 painter.setBrush( opaqueBrush );
1726 painter.drawRect( rect.left(), rect.top(), std::ceil( rect.width() / 2.0 ), rect.height() );
1727 }
1728 else
1729 {
1730 //no alpha component, just draw a solid rectangle
1731 const QBrush brush = QBrush( color );
1732 painter.setBrush( brush );
1733 painter.drawRect( rect );
1734 }
1735}
1736
1737void QgsColorPreviewWidget::paintEvent( QPaintEvent *event )
1738{
1739 Q_UNUSED( event )
1740 QPainter painter( this );
1741
1742 if ( mColor2.isValid() )
1743 {
1744 //drawing with two color sections
1745 const int verticalSplit = std::round( height() / 2.0 );
1746 drawColor( mCurrentColor, QRect( 0, 0, width(), verticalSplit ), painter );
1747 drawColor( mColor2, QRect( 0, verticalSplit, width(), height() - verticalSplit ), painter );
1748 }
1749 else if ( mCurrentColor.isValid() )
1750 {
1751 drawColor( mCurrentColor, QRect( 0, 0, width(), height() ), painter );
1752 }
1753
1754 painter.end();
1755}
1756
1758{
1759 return QSize( Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 22, Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 22 * 0.75 );
1760}
1761
1763{
1764 if ( color == mColor2 )
1765 {
1766 return;
1767 }
1768 mColor2 = color;
1769 update();
1770}
1771
1773{
1774 if ( e->button() == Qt::LeftButton )
1775 {
1776 mDragStartPosition = e->pos();
1777 }
1779}
1780
1782{
1783 if ( ( e->pos() - mDragStartPosition ).manhattanLength() >= QApplication::startDragDistance() )
1784 {
1785 //mouse moved, so a drag. nothing to do here
1787 return;
1788 }
1789
1790 //work out which color was clicked
1791 QColor clickedColor = mCurrentColor;
1792 if ( mColor2.isValid() )
1793 {
1794 //two color sections, check if dragged color was the second color
1795 const int verticalSplit = std::round( height() / 2.0 );
1796 if ( mDragStartPosition.y() >= verticalSplit )
1797 {
1798 clickedColor = mColor2;
1799 }
1800 }
1801 emit colorChanged( clickedColor );
1802}
1803
1805{
1806 //handle dragging colors from button
1807
1808 if ( !( e->buttons() & Qt::LeftButton ) )
1809 {
1810 //left button not depressed, so not a drag
1812 return;
1813 }
1814
1815 if ( ( e->pos() - mDragStartPosition ).manhattanLength() < QApplication::startDragDistance() )
1816 {
1817 //mouse not moved, so not a drag
1819 return;
1820 }
1821
1822 //user is dragging color
1823
1824 //work out which color is being dragged
1825 QColor dragColor = mCurrentColor;
1826 if ( mColor2.isValid() )
1827 {
1828 //two color sections, check if dragged color was the second color
1829 const int verticalSplit = std::round( height() / 2.0 );
1830 if ( mDragStartPosition.y() >= verticalSplit )
1831 {
1832 dragColor = mColor2;
1833 }
1834 }
1835
1836 QDrag *drag = new QDrag( this );
1837 drag->setMimeData( QgsSymbolLayerUtils::colorToMimeData( dragColor ) );
1838 drag->setPixmap( createDragIcon( dragColor ) );
1839 drag->exec( Qt::CopyAction );
1840}
1841
1842
1843//
1844// QgsColorWidgetAction
1845//
1846
1848 : QWidgetAction( parent )
1849 , mMenu( menu )
1850 , mColorWidget( colorWidget )
1851{
1852 setDefaultWidget( mColorWidget );
1853 connect( mColorWidget, &QgsColorWidget::colorChanged, this, &QgsColorWidgetAction::setColor );
1854
1855 connect( this, &QAction::hovered, this, &QgsColorWidgetAction::onHover );
1856 connect( mColorWidget, &QgsColorWidget::hovered, this, &QgsColorWidgetAction::onHover );
1857}
1858
1859void QgsColorWidgetAction::onHover()
1860{
1861 //see https://bugreports.qt.io/browse/QTBUG-10427?focusedCommentId=185610&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-185610
1862 if ( mSuppressRecurse )
1863 {
1864 return;
1865 }
1866
1867 if ( mMenu )
1868 {
1869 mSuppressRecurse = true;
1870 mMenu->setActiveAction( this );
1871 mSuppressRecurse = false;
1872 }
1873}
1874
1875void QgsColorWidgetAction::setColor( const QColor &color )
1876{
1877 emit colorChanged( color );
1878 if ( mMenu && mDismissOnColorSelection )
1879 {
1880 QAction::trigger();
1881 mMenu->hide();
1882 }
1883}
static const double UI_SCALE_FACTOR
UI scaling factor.
Definition qgis.h:6591
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:7504
#define BUILTIN_UNREACHABLE
Definition qgis.h:7540
#define Q_NOWARN_DEPRECATED_PUSH
Definition qgis.h:7503
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:6975
#define HUE_MAX
float float_type