QGIS API Documentation 4.3.0-Master (7c7bd4d7018)
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 "qgsscreenhelper.h"
26#include "qgssettingstree.h"
27#include "qgssymbollayerutils.h"
28
29#include <QDrag>
30#include <QFontMetrics>
31#include <QHBoxLayout>
32#include <QLineEdit>
33#include <QLineF>
34#include <QMenu>
35#include <QPainter>
36#include <QRectF>
37#include <QResizeEvent>
38#include <QString>
39#include <QStyleOptionFrame>
40#include <QToolButton>
41
42#include "moc_qgscolorwidgets.cpp"
43
44using namespace Qt::StringLiterals;
45
48
49#define HUE_MAX 360
50
51
52// TODO QGIS 5 remove typedef, QColor was qreal (double) and is now float
53typedef float float_type;
54
55
56//
57// QgsColorWidget
58//
59
61 : QWidget( parent )
62 , mCurrentColor( Qt::red )
64{
65 setAcceptDrops( true );
66}
67
69{
70 return static_cast<int>( std::round( componentValueF( mComponent ) * static_cast<float>( componentRange() ) ) );
71}
72
77
78QPixmap QgsColorWidget::createDragIcon( const QColor &color )
79{
80 //craft a pixmap for the drag icon
81 const int iconSize = QgsGuiUtils::scaleIconSize( 50 );
82 QPixmap pixmap( iconSize, iconSize );
83 pixmap.fill( Qt::transparent );
84 QPainter painter;
85 painter.begin( &pixmap );
86 //start with a light gray background
87 painter.fillRect( QRect( 0, 0, iconSize, iconSize ), QBrush( QColor( 200, 200, 200 ) ) );
88 //draw rect with white border, filled with current color
89 QColor pixmapColor = color;
90 pixmapColor.setAlpha( 255 );
91 painter.setBrush( QBrush( pixmapColor ) );
92 painter.setPen( QPen( Qt::white ) );
93 painter.drawRect( QRect( 1, 1, iconSize - 2, iconSize - 2 ) );
94 painter.end();
95 return pixmap;
96}
97
122
124{
125 return static_cast<int>( std::round( componentValueF( component ) * static_cast<float>( componentRange( component ) ) ) );
126}
127
129{
130 if ( !mCurrentColor.isValid() )
131 {
132 return -1;
133 }
134
135 // TODO QGIS 5 remove the nolint instructions, QColor was qreal (double) and is now float
136 // NOLINTBEGIN(bugprone-narrowing-conversions)
137 switch ( component )
138 {
140 return mCurrentColor.redF();
142 return mCurrentColor.greenF();
144 return mCurrentColor.blueF();
146 //hue is treated specially, to avoid -1 hues values from QColor for ambiguous hues
147 return hueF();
149 return mCurrentColor.hsvSaturationF();
151 return mCurrentColor.valueF();
153 return mCurrentColor.alphaF();
155 return mCurrentColor.cyanF();
157 return mCurrentColor.yellowF();
159 return mCurrentColor.magentaF();
161 return mCurrentColor.blackF();
162 default:
163 return -1;
164 }
165 // NOLINTEND(bugprone-narrowing-conversions)
166}
167
169{
170 return componentRange( mComponent );
171}
172
174{
176 {
177 //no component
178 return -1;
179 }
180
182 {
183 //hue ranges to HUE_MAX
184 return HUE_MAX;
185 }
186 else
187 {
188 //all other components range to 255
189 return 255;
190 }
191}
192
194{
195 return static_cast<int>( std::round( hueF() * HUE_MAX ) );
196}
197
199{
200 if ( mCurrentColor.hueF() >= 0 )
201 {
202 return mCurrentColor.hueF(); // NOLINT(bugprone-narrowing-conversions): TODO QGIS 5 remove the nolint instructions, QColor was qreal (double) and is now float
203 }
204 else
205 {
206 return mExplicitHue;
207 }
208}
209
211{
212 //clip value to sensible range
213 const float clippedValue = static_cast<float>( std::clamp( newValue, 0, componentRange( component ) ) ) / static_cast<float>( componentRange( component ) );
214 alterColorF( color, component, clippedValue );
215}
216
218{
219 float_type clippedValue = std::clamp( newValue, 0.f, 1.f );
220
221 if ( colorSpec( component ) == QColor::Spec::Cmyk )
222 {
223 float_type c, m, y, k, a;
224 color.getCmykF( &c, &m, &y, &k, &a );
225
226 switch ( component )
227 {
229 color.setCmykF( clippedValue, m, y, k, a );
230 break;
232 color.setCmykF( c, clippedValue, y, k, a );
233 break;
235 color.setCmykF( c, m, clippedValue, k, a );
236 break;
238 color.setCmykF( c, m, y, clippedValue, a );
239 break;
240 default:
241 return;
242 }
243 }
244 else
245 {
246 float_type r, g, b, a;
247 color.getRgbF( &r, &g, &b, &a );
248 float_type h, s, v;
249 color.getHsvF( &h, &s, &v );
250
251 switch ( component )
252 {
254 color.setRedF( clippedValue );
255 break;
257 color.setGreenF( clippedValue );
258 break;
260 color.setBlueF( clippedValue );
261 break;
263 color.setHsvF( clippedValue, s, v, a );
264 break;
266 color.setHsvF( h, clippedValue, v, a );
267 break;
269 color.setHsvF( h, s, clippedValue, a );
270 break;
272 color.setAlphaF( clippedValue );
273 break;
274 default:
275 return;
276 }
277 }
278}
279
281{
282 switch ( component )
283 {
284 case Red:
285 case Green:
286 case Blue:
287 return QColor::Spec::Rgb;
288
289 case Hue:
290 case Saturation:
291 case Value:
292 return QColor::Spec::Hsv;
293
294 case Cyan:
295 case Magenta:
296 case Yellow:
297 case Black:
298 return QColor::Spec::Cmyk;
299
300 default:
301 return QColor::Spec::Invalid;
302 }
303}
304
305QColor::Spec QgsColorWidget::colorSpec() const
306{
307 return colorSpec( mComponent );
308}
309
311{
312 static QPixmap sTranspBkgrd;
313
314 if ( sTranspBkgrd.isNull() )
315 sTranspBkgrd = QgsApplication::getThemePixmap( u"/transp-background_8x8.png"_s );
316
317 return sTranspBkgrd;
318}
319
320void QgsColorWidget::dragEnterEvent( QDragEnterEvent *e )
321{
322 //is dragged data valid color data?
323 bool hasAlpha;
324 const QColor mimeColor = QgsSymbolLayerUtils::colorFromMimeData( e->mimeData(), hasAlpha );
325
326 if ( mimeColor.isValid() )
327 {
328 //if so, we accept the drag
329 e->acceptProposedAction();
330 }
331}
332
333void QgsColorWidget::dropEvent( QDropEvent *e )
334{
335 //is dropped data valid color data?
336 bool hasAlpha = false;
337 QColor mimeColor = QgsSymbolLayerUtils::colorFromMimeData( e->mimeData(), hasAlpha );
338
339 if ( mimeColor.isValid() )
340 {
341 //accept drop and set new color
342 e->acceptProposedAction();
343
344 if ( !hasAlpha )
345 {
346 //mime color has no explicit alpha component, so keep existing alpha
347 mimeColor.setAlpha( mCurrentColor.alpha() );
348 }
349
350 setColor( mimeColor );
352 }
353
354 //could not get color from mime data
355}
356
357void QgsColorWidget::mouseMoveEvent( QMouseEvent *e )
358{
359 emit hovered();
360 e->accept();
361 //don't pass to QWidget::mouseMoveEvent, causes issues with widget used in QWidgetAction
362}
363
365{
366 e->accept();
367 //don't pass to QWidget::mousePressEvent, causes issues with widget used in QWidgetAction
368}
369
371{
372 e->accept();
373 //don't pass to QWidget::mouseReleaseEvent, causes issues with widget used in QWidgetAction
374}
375
377{
378 return mCurrentColor;
379}
380
382{
383 if ( component == mComponent )
384 {
385 return;
386 }
387
389 update();
390}
391
393{
394 setComponentValueF( static_cast<float>( value ) / static_cast< float >( componentRange( mComponent ) ) );
395}
396
397void QgsColorWidget::setComponentValueF( const float value )
398{
400 {
401 return;
402 }
403
404 //overwrite hue with explicit hue if required
406 {
407 float_type h, s, v, a;
408 mCurrentColor.getHsvF( &h, &s, &v, &a );
409
410 h = hueF();
411
412 mCurrentColor.setHsvF( h, s, v, a );
413 }
414
416
417 //update recorded hue
418 if ( mCurrentColor.hue() >= 0 )
419 {
420 mExplicitHue = mCurrentColor.hueF(); // NOLINT(bugprone-narrowing-conversions): TODO QGIS 5 remove the nolint instructions, QColor was qreal (double) and is now float
421 }
422
423 update();
424}
425
426void QgsColorWidget::setColor( const QColor &color, const bool emitSignals )
427{
428 if ( color == mCurrentColor )
429 {
430 return;
431 }
432
434
435 //update recorded hue
436 if ( color.hue() >= 0 )
437 {
438 mExplicitHue = color.hueF(); // NOLINT(bugprone-narrowing-conversions): TODO QGIS 5 remove the nolint instructions, QColor was qreal (double) and is now float
439 }
440
441 if ( emitSignals )
442 {
444 }
445
446 update();
447}
448
449
450//
451// QgsColorWheel
452//
453
455 : QgsColorWidget( parent )
456{
457 //create wheel hue brush - only do this once
458 QConicalGradient wheelGradient = QConicalGradient( 0, 0, 0 );
459 const int wheelStops = 20;
460 QColor gradColor = QColor::fromHsvF( 1.0, 1.0, 1.0 );
461 for ( int pos = 0; pos <= wheelStops; ++pos )
462 {
463 const double relativePos = static_cast<double>( pos ) / wheelStops;
464 gradColor.setHsvF( relativePos, 1, 1 );
465 wheelGradient.setColorAt( relativePos, gradColor );
466 }
467 mWheelBrush = QBrush( wheelGradient );
468
469 auto screenHelper = new QgsScreenHelper( this );
470 connect( screenHelper, &QgsScreenHelper::screenDpiChanged, this, &QgsColorWheel::invalidateImages );
471}
472
474
476{
477 const int size = Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 22;
478 return QSize( size, size );
479}
480
481void QgsColorWheel::paintEvent( QPaintEvent *event )
482{
483 Q_UNUSED( event )
484 QPainter painter( this );
485
486 if ( mWidgetImage.isNull() || mWheelImage.isNull() || mTriangleImage.isNull() )
487 {
488 createImages( size() );
489 }
490
491 //draw everything in an image
492 mWidgetImage.fill( Qt::transparent );
493 QPainter imagePainter( &mWidgetImage );
494 imagePainter.setRenderHint( QPainter::Antialiasing );
495
496 if ( mWheelDirty )
497 {
498 //need to redraw the wheel image
499 createWheel();
500 }
501
502 //draw wheel centered on widget
503 const QPointF center = QPointF( mWidgetImage.width() / 2.0 / mWidgetImage.devicePixelRatioF(), mWidgetImage.height() / 2.0 / mWidgetImage.devicePixelRatioF() );
504 imagePainter.drawImage( QPointF( center.x() - ( mWheelImage.width() / 2.0 / mWidgetImage.devicePixelRatioF() ), center.y() - ( mWheelImage.height() / 2.0 / mWidgetImage.devicePixelRatioF() ) ), mWheelImage );
505
506 //draw hue marker
507 const float h = hueF() * HUE_MAX;
508 const double length = mWheelImage.width() / 2.0 / mWidgetImage.devicePixelRatioF();
509 QLineF hueMarkerLine = QLineF( center.x(), center.y(), center.x() + length, center.y() );
510 hueMarkerLine.setAngle( h );
511 imagePainter.save();
512 //use sourceIn mode for nicer antialiasing
513 imagePainter.setCompositionMode( QPainter::CompositionMode_SourceIn );
514 QPen pen;
515 pen.setWidthF( 2 * mWidgetImage.devicePixelRatioF() );
516 //adapt pen color for hue
517 pen.setColor( h > 20 && h < 200 ? Qt::black : Qt::white );
518 imagePainter.setPen( pen );
519 imagePainter.drawLine( hueMarkerLine );
520 imagePainter.restore();
521
522 //draw triangle
523 if ( mTriangleDirty )
524 {
525 createTriangle();
526 }
527 imagePainter.drawImage( QPointF( center.x() - ( mWheelImage.width() / 2.0 / mWidgetImage.devicePixelRatioF() ), center.y() - ( mWheelImage.height() / 2.0 / mWidgetImage.devicePixelRatioF() ) ), mTriangleImage );
528
529 //draw current color marker
530 const double triangleRadius = length - ( mWheelThickness + 1 ) * mWheelImage.devicePixelRatioF();
531
532 //adapted from equations at https://github.com/timjb/colortriangle/blob/master/colortriangle.js by Tim Baumann
533 const double lightness = mCurrentColor.lightnessF();
534 const double hueRadians = ( h * M_PI / 180.0 );
535 const double hx = std::cos( hueRadians ) * triangleRadius;
536 const double hy = -std::sin( hueRadians ) * triangleRadius;
537 const double sx = -std::cos( -hueRadians + ( M_PI / 3.0 ) ) * triangleRadius;
538 const double sy = -std::sin( -hueRadians + ( M_PI / 3.0 ) ) * triangleRadius;
539 const double vx = -std::cos( hueRadians + ( M_PI / 3.0 ) ) * triangleRadius;
540 const double vy = std::sin( hueRadians + ( M_PI / 3.0 ) ) * triangleRadius;
541 const double mx = ( sx + vx ) / 2.0;
542 const double my = ( sy + vy ) / 2.0;
543
544 const double a = ( 1 - 2.0 * std::fabs( lightness - 0.5 ) ) * mCurrentColor.hslSaturationF();
545 const double x = sx + ( vx - sx ) * lightness + ( hx - mx ) * a;
546 const double y = sy + ( vy - sy ) * lightness + ( hy - my ) * a;
547
548 //adapt pen color for lightness
549 pen.setColor( lightness > 0.7 ? Qt::black : Qt::white );
550 imagePainter.setPen( pen );
551 imagePainter.setBrush( Qt::NoBrush );
552 imagePainter.drawEllipse( QPointF( x + center.x(), y + center.y() ), 4.0 * mWheelImage.devicePixelRatioF(), 4.0 * mWheelImage.devicePixelRatioF() );
553 imagePainter.end();
554
555 //draw image onto widget
556 painter.drawImage( QRectF( 0, 0, width(), height() ), mWidgetImage );
557 painter.end();
558}
559
560void QgsColorWheel::setColor( const QColor &color, const bool emitSignals )
561{
562 if ( color.hue() >= 0 && !qgsDoubleNear( color.hue(), hueF() ) )
563 {
564 //hue has changed, need to redraw the triangle
565 mTriangleDirty = true;
566 }
567
568 QgsColorWidget::setColor( color, emitSignals );
569}
570
571void QgsColorWheel::createImages( const QSizeF size )
572{
573 const double wheelSize = std::min( size.width(), size.height() ) - mMargin * 2.0;
574 mWheelThickness = wheelSize / 15.0;
575
576 //recreate cache images at correct size
577 const double pixelRatio = devicePixelRatioF();
578 mWheelImage = QImage( wheelSize * pixelRatio, wheelSize * pixelRatio, QImage::Format_ARGB32 );
579 mWheelImage.setDevicePixelRatio( pixelRatio );
580 mTriangleImage = QImage( wheelSize * pixelRatio, wheelSize * pixelRatio, QImage::Format_ARGB32 );
581 mTriangleImage.setDevicePixelRatio( pixelRatio );
582 mWidgetImage = QImage( size.width() * pixelRatio, size.height() * pixelRatio, QImage::Format_ARGB32 );
583 mWidgetImage.setDevicePixelRatio( pixelRatio );
584
585 //trigger a redraw for the images
586 mWheelDirty = true;
587 mTriangleDirty = true;
588}
589
590void QgsColorWheel::resizeEvent( QResizeEvent * )
591{
592 // force a recreation on next paint
593 invalidateImages();
594}
595
596void QgsColorWheel::setColorFromPos( const QPointF pos )
597{
598 const QPointF center = QPointF( width() / 2.0, height() / 2.0 );
599 //line from center to mouse position
600 const QLineF line = QLineF( center.x(), center.y(), pos.x(), pos.y() );
601
602 QColor newColor = QColor();
603
604 float_type h, s, l, alpha;
605 mCurrentColor.getHslF( &h, &s, &l, &alpha );
606 //override hue with explicit hue, so we don't get -1 values from QColor for hue
607 h = hueF();
608
609 if ( mClickedPart == QgsColorWheel::Triangle )
610 {
611 //adapted from equations at https://github.com/timjb/colortriangle/blob/master/colortriangle.js by Tim Baumann
612
613 //position of event relative to triangle center
614 const double x = pos.x() - center.x();
615 const double y = pos.y() - center.y();
616
617 double eventAngleRadians = line.angle() * M_PI / 180.0;
618 const double hueRadians = h * 2 * M_PI;
619 double rad0 = std::fmod( eventAngleRadians + 2.0 * M_PI - hueRadians, 2.0 * M_PI );
620 double rad1 = std::fmod( rad0, ( ( 2.0 / 3.0 ) * M_PI ) ) - ( M_PI / 3.0 );
621 const double length = mWheelImage.width() / 2.0 / mWheelImage.devicePixelRatioF();
622 const double triangleLength = length - mWheelThickness - 1;
623
624 const double a = 0.5 * triangleLength;
625 double b = std::tan( rad1 ) * a;
626 double r = std::sqrt( x * x + y * y );
627 const double maxR = std::sqrt( a * a + b * b );
628
629 if ( r > maxR )
630 {
631 const double dx = std::tan( rad1 ) * r;
632 double rad2 = std::atan( dx / maxR );
633 rad2 = std::min( rad2, M_PI / 3.0 );
634 rad2 = std::max( rad2, -M_PI / 3.0 );
635 eventAngleRadians += rad2 - rad1;
636 rad0 = std::fmod( eventAngleRadians + 2.0 * M_PI - hueRadians, 2.0 * M_PI );
637 rad1 = std::fmod( rad0, ( ( 2.0 / 3.0 ) * M_PI ) ) - ( M_PI / 3.0 );
638 b = std::tan( rad1 ) * a;
639 r = std::sqrt( a * a + b * b );
640 }
641
642 const double triangleSideLength = std::sqrt( 3.0 ) * triangleLength;
643 const double newL = ( ( -std::sin( rad0 ) * r ) / triangleSideLength ) + 0.5;
644 const double widthShare = 1.0 - ( std::fabs( newL - 0.5 ) * 2.0 );
645 const double newS = ( ( ( std::cos( rad0 ) * r ) + ( triangleLength / 2.0 ) ) / ( 1.5 * triangleLength ) ) / widthShare;
646 s = std::min( std::max( 0.f, static_cast<float>( newS ) ), 1.f );
647 l = std::min( std::max( 0.f, static_cast<float>( newL ) ), 1.f );
648 newColor = QColor::fromHslF( h, s, l );
649 //explicitly set the hue again, so that it's exact
650 newColor.setHsvF( h, newColor.hsvSaturationF(), newColor.valueF(), alpha );
651 }
652 else if ( mClickedPart == QgsColorWheel::Wheel )
653 {
654 //use hue angle
655 s = mCurrentColor.hsvSaturationF();
656 const float v = mCurrentColor.valueF(); // NOLINT(bugprone-narrowing-conversions): TODO QGIS 5 remove the nolint instructions, QColor was qreal (double) and is now float
657 const qreal newHue = line.angle() / HUE_MAX;
658 newColor = QColor::fromHsvF( static_cast<float>( newHue ), s, v, alpha );
659 //hue has changed, need to redraw triangle
660 mTriangleDirty = true;
661 }
662
663 if ( newColor.isValid() && newColor != mCurrentColor )
664 {
665 //color has changed
666 mCurrentColor = QColor( newColor );
667
668 if ( mCurrentColor.hueF() >= 0 )
669 {
670 //color has a valid hue, so update the QgsColorWidget's explicit hue
671 mExplicitHue = mCurrentColor.hueF(); // NOLINT(bugprone-narrowing-conversions): TODO QGIS 5 remove the nolint instructions, QColor was qreal (double) and is now float
672 }
673
674 update();
676 }
677}
678
679void QgsColorWheel::mouseMoveEvent( QMouseEvent *event )
680{
681 if ( mIsDragging )
682 setColorFromPos( event->pos() );
683
685}
686
687void QgsColorWheel::mousePressEvent( QMouseEvent *event )
688{
689 if ( event->button() == Qt::LeftButton )
690 {
691 mIsDragging = true;
692 //calculate where the event occurred -- on the wheel or inside the triangle?
693
694 //create a line from the widget's center to the event
695 const QLineF line = QLineF( width() / 2.0, height() / 2.0, event->pos().x(), event->pos().y() );
696
697 const double innerLength = mWheelImage.width() / 2.0 / mWheelImage.devicePixelRatioF() - mWheelThickness * mWheelImage.devicePixelRatioF();
698 if ( line.length() < innerLength )
699 {
700 mClickedPart = QgsColorWheel::Triangle;
701 }
702 else
703 {
704 mClickedPart = QgsColorWheel::Wheel;
705 }
706 setColorFromPos( event->pos() );
707 }
708 else
709 {
711 }
712}
713
714void QgsColorWheel::mouseReleaseEvent( QMouseEvent *event )
715{
716 if ( event->button() == Qt::LeftButton )
717 {
718 mIsDragging = false;
719 mClickedPart = QgsColorWheel::None;
720 }
721 else
722 {
724 }
725}
726
727void QgsColorWheel::invalidateImages()
728{
729 // force a recreation on next paint
730 mWidgetImage = QImage();
731}
732
733void QgsColorWheel::createWheel()
734{
735 if ( mWheelImage.isNull() )
736 {
737 return;
738 }
739
740 const int maxSize = std::min( mWheelImage.width(), mWheelImage.height() );
741 const double wheelRadius = maxSize / 2.0 / mWheelImage.devicePixelRatioF();
742
743 mWheelImage.fill( Qt::transparent );
744 QPainter p( &mWheelImage );
745 p.setRenderHint( QPainter::Antialiasing );
746 p.setBrush( mWheelBrush );
747 p.setPen( Qt::NoPen );
748
749 //draw hue wheel as a circle
750 p.translate( wheelRadius, wheelRadius );
751 p.drawEllipse( QPointF( 0, 0 ), wheelRadius, wheelRadius );
752
753 //cut hole in center of circle to make a ring
754 p.setCompositionMode( QPainter::CompositionMode_DestinationOut );
755 p.setBrush( QBrush( Qt::black ) );
756 p.drawEllipse( QPointF( 0, 0 ), wheelRadius - mWheelThickness * mWheelImage.devicePixelRatioF(), wheelRadius - mWheelThickness * mWheelImage.devicePixelRatioF() );
757 p.end();
758
759 mWheelDirty = false;
760}
761
762void QgsColorWheel::createTriangle()
763{
764 if ( mWheelImage.isNull() || mTriangleImage.isNull() )
765 {
766 return;
767 }
768
769 const QPointF center = QPointF( mWheelImage.width() / 2.0 / mWheelImage.devicePixelRatioF(), mWheelImage.height() / 2.0 / mWheelImage.devicePixelRatioF() );
770 mTriangleImage.fill( Qt::transparent );
771
772 QPainter imagePainter( &mTriangleImage );
773 imagePainter.setRenderHint( QPainter::Antialiasing );
774
775 const float angle = hueF();
776 const float angleDegree = angle * HUE_MAX;
777 const double wheelRadius = mWheelImage.width() / 2.0 / mWheelImage.devicePixelRatioF();
778 const double triangleRadius = wheelRadius - mWheelThickness * mWheelImage.devicePixelRatioF() - 1;
779
780 //pure version of hue (at full saturation and value)
781 const QColor pureColor = QColor::fromHsvF( angle, 1., 1. );
782 //create copy of color but with 0 alpha
783 QColor alphaColor = QColor( pureColor );
784 alphaColor.setAlpha( 0 );
785
786 //some rather ugly shortcuts to obtain corners and midpoints of triangle
787 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 ) );
788 QLineF line2 = QLineF( center.x(), center.y(), center.x() + triangleRadius, center.y() );
789 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 ) );
790 QLineF line4 = QLineF( center.x(), center.y(), center.x() - triangleRadius * std::cos( M_PI / 3.0 ), center.y() );
791 QLineF line5 = QLineF( center.x(), center.y(), ( line2.p2().x() + line1.p2().x() ) / 2.0, ( line2.p2().y() + line1.p2().y() ) / 2.0 );
792 line1.setAngle( line1.angle() + angleDegree );
793 line2.setAngle( line2.angle() + angleDegree );
794 line3.setAngle( line3.angle() + angleDegree );
795 line4.setAngle( line4.angle() + angleDegree );
796 line5.setAngle( line5.angle() + angleDegree );
797 const QPointF p1 = line1.p2();
798 const QPointF p2 = line2.p2();
799 const QPointF p3 = line3.p2();
800 const QPointF p4 = line4.p2();
801 const QPointF p5 = line5.p2();
802
803 //inspired by Tim Baumann's work at https://github.com/timjb/colortriangle/blob/master/colortriangle.js
804 QLinearGradient colorGrad = QLinearGradient( p4.x(), p4.y(), p2.x(), p2.y() );
805 colorGrad.setColorAt( 0, alphaColor );
806 colorGrad.setColorAt( 1, pureColor );
807 QLinearGradient whiteGrad = QLinearGradient( p3.x(), p3.y(), p5.x(), p5.y() );
808 whiteGrad.setColorAt( 0, QColor( 255, 255, 255, 255 ) );
809 whiteGrad.setColorAt( 1, QColor( 255, 255, 255, 0 ) );
810
811 QPolygonF triangle;
812 triangle << p2 << p1 << p3 << p2;
813 imagePainter.setPen( Qt::NoPen );
814 //start with a black triangle
815 imagePainter.setBrush( QBrush( Qt::black ) );
816 imagePainter.drawPolygon( triangle );
817 //draw a gradient from transparent to the pure color at the triangle's tip
818 imagePainter.setBrush( QBrush( colorGrad ) );
819 imagePainter.drawPolygon( triangle );
820 //draw a white gradient using additive composition mode
821 imagePainter.setCompositionMode( QPainter::CompositionMode_Plus );
822 imagePainter.setBrush( QBrush( whiteGrad ) );
823 imagePainter.drawPolygon( triangle );
824
825 //above process results in some small artifacts on the edge of the triangle. Let's clear these up
826 //use source composition mode and draw an outline using a transparent pen
827 //this clears the edge pixels and leaves a nice smooth image
828 imagePainter.setCompositionMode( QPainter::CompositionMode_Source );
829 imagePainter.setBrush( Qt::NoBrush );
830 imagePainter.setPen( QPen( Qt::transparent ) );
831 imagePainter.drawPolygon( triangle );
832
833 imagePainter.end();
834 mTriangleDirty = false;
835}
836
837
838//
839// QgsColorBox
840//
841
843 : QgsColorWidget( parent, component )
844{
845 setFocusPolicy( Qt::StrongFocus );
846 setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding );
847
848 mBoxImage = std::make_unique<QImage>( width() - static_cast<int>( mMargin * 2 ), height() - static_cast<int>( mMargin * 2 ), QImage::Format_RGB32 );
849}
850
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 mBoxImage = std::make_unique<QImage>( event->size().width() - static_cast<int>( mMargin * 2 ), event->size().height() - static_cast<int>( mMargin * 2 ), QImage::Format_RGB32 );
919
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.88%"_s );
1441 mSpinBox->setFixedWidth( 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 mFormat = settingsTextFormat->value();
1580
1581 updateText();
1582}
1583
1584void QgsColorTextWidget::setColor( const QColor &color, const bool emitSignals )
1585{
1586 QgsColorWidget::setColor( color, emitSignals );
1587 updateText();
1588}
1589
1590void QgsColorTextWidget::resizeEvent( QResizeEvent *event )
1591{
1592 Q_UNUSED( event )
1593 const QSize sz = mMenuButton->sizeHint();
1594 const int frameWidth = style()->pixelMetric( QStyle::PM_DefaultFrameWidth );
1595 mMenuButton->move( mLineEdit->rect().right() - frameWidth - sz.width(), ( mLineEdit->rect().bottom() + 1 - sz.height() ) / 2 );
1596}
1597
1598void QgsColorTextWidget::updateText()
1599{
1600 switch ( mFormat )
1601 {
1602 case HexRgb:
1603 mLineEdit->setText( mCurrentColor.name() );
1604 break;
1605 case HexRgbA:
1606 mLineEdit->setText( mCurrentColor.name() + u"%1"_s.arg( mCurrentColor.alpha(), 2, 16, QChar( '0' ) ) );
1607 break;
1608 case Rgb:
1609 mLineEdit->setText( tr( "rgb( %1, %2, %3 )" ).arg( mCurrentColor.red() ).arg( mCurrentColor.green() ).arg( mCurrentColor.blue() ) );
1610 break;
1611 case Rgba:
1612 mLineEdit->setText( tr( "rgba( %1, %2, %3, %4 )" ).arg( mCurrentColor.red() ).arg( mCurrentColor.green() ).arg( mCurrentColor.blue() ).arg( QString::number( mCurrentColor.alphaF(), 'f', 2 ) ) );
1613 break;
1614 }
1615}
1616
1617void QgsColorTextWidget::textChanged()
1618{
1619 const QString testString = mLineEdit->text();
1620 bool containsAlpha;
1621 QColor color = QgsSymbolLayerUtils::parseColorWithAlpha( testString, containsAlpha );
1622 if ( !color.isValid() )
1623 {
1624 //bad color string
1625 updateText();
1626 return;
1627 }
1628
1629 //good color string
1630 if ( color != mCurrentColor )
1631 {
1632 //retain alpha if no explicit alpha set
1633 if ( !containsAlpha )
1634 {
1635 color.setAlpha( mCurrentColor.alpha() );
1636 }
1637 //color has changed
1640 }
1641 updateText();
1642}
1643
1644void QgsColorTextWidget::showMenu()
1645{
1646 QMenu colorContextMenu;
1647 QAction *hexRgbaAction = nullptr;
1648 QAction *rgbaAction = nullptr;
1649
1650 QAction *hexRgbAction = new QAction( tr( "#RRGGBB" ), nullptr );
1651 colorContextMenu.addAction( hexRgbAction );
1652 if ( mAllowAlpha )
1653 {
1654 hexRgbaAction = new QAction( tr( "#RRGGBBAA" ), nullptr );
1655 colorContextMenu.addAction( hexRgbaAction );
1656 }
1657 QAction *rgbAction = new QAction( tr( "rgb( r, g, b )" ), nullptr );
1658 colorContextMenu.addAction( rgbAction );
1659 if ( mAllowAlpha )
1660 {
1661 rgbaAction = new QAction( tr( "rgba( r, g, b, a )" ), nullptr );
1662 colorContextMenu.addAction( rgbaAction );
1663 }
1664
1665 QAction *selectedAction = colorContextMenu.exec( QCursor::pos() );
1666 if ( selectedAction == hexRgbAction )
1667 {
1669 }
1670 else if ( hexRgbaAction && selectedAction == hexRgbaAction )
1671 {
1673 }
1674 else if ( selectedAction == rgbAction )
1675 {
1676 mFormat = QgsColorTextWidget::Rgb;
1677 }
1678 else if ( rgbaAction && selectedAction == rgbaAction )
1679 {
1680 mFormat = QgsColorTextWidget::Rgba;
1681 }
1682
1683 //save format setting
1684 settingsTextFormat->setValue( mFormat );
1685
1686 updateText();
1687}
1688
1689void QgsColorTextWidget::setAllowOpacity( const bool allowOpacity )
1690{
1691 mAllowAlpha = allowOpacity;
1692}
1693
1694//
1695// QgsColorPreviewWidget
1696//
1697
1699 : QgsColorWidget( parent )
1700 , mColor2( QColor() )
1701{}
1702
1703void QgsColorPreviewWidget::drawColor( const QColor &color, QRect rect, QPainter &painter )
1704{
1705 painter.setPen( Qt::NoPen );
1706 //if color has an alpha, start with a checkboard pattern
1707 if ( color.alpha() < 255 )
1708 {
1709 const QBrush checkBrush = QBrush( transparentBackground() );
1710 painter.setBrush( checkBrush );
1711 painter.drawRect( rect );
1712
1713 //draw half of widget showing solid color, the other half showing color with alpha
1714
1715 //ensure at least a 1px overlap to avoid artifacts
1716 const QBrush colorBrush = QBrush( color );
1717 painter.setBrush( colorBrush );
1718 painter.drawRect( std::floor( rect.width() / 2.0 ) + rect.left(), rect.top(), rect.width() - std::floor( rect.width() / 2.0 ), rect.height() );
1719
1720 QColor opaqueColor = QColor( color );
1721 opaqueColor.setAlpha( 255 );
1722 const QBrush opaqueBrush = QBrush( opaqueColor );
1723 painter.setBrush( opaqueBrush );
1724 painter.drawRect( rect.left(), rect.top(), std::ceil( rect.width() / 2.0 ), rect.height() );
1725 }
1726 else
1727 {
1728 //no alpha component, just draw a solid rectangle
1729 const QBrush brush = QBrush( color );
1730 painter.setBrush( brush );
1731 painter.drawRect( rect );
1732 }
1733}
1734
1735void QgsColorPreviewWidget::paintEvent( QPaintEvent *event )
1736{
1737 Q_UNUSED( event )
1738 QPainter painter( this );
1739
1740 if ( mColor2.isValid() )
1741 {
1742 //drawing with two color sections
1743 const int verticalSplit = std::round( height() / 2.0 );
1744 drawColor( mCurrentColor, QRect( 0, 0, width(), verticalSplit ), painter );
1745 drawColor( mColor2, QRect( 0, verticalSplit, width(), height() - verticalSplit ), painter );
1746 }
1747 else if ( mCurrentColor.isValid() )
1748 {
1749 drawColor( mCurrentColor, QRect( 0, 0, width(), height() ), painter );
1750 }
1751
1752 painter.end();
1753}
1754
1756{
1757 return QSize( Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 22, Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 22 * 0.75 );
1758}
1759
1761{
1762 if ( color == mColor2 )
1763 {
1764 return;
1765 }
1766 mColor2 = color;
1767 update();
1768}
1769
1771{
1772 if ( e->button() == Qt::LeftButton )
1773 {
1774 mDragStartPosition = e->pos();
1775 }
1777}
1778
1780{
1781 if ( ( e->pos() - mDragStartPosition ).manhattanLength() >= QApplication::startDragDistance() )
1782 {
1783 //mouse moved, so a drag. nothing to do here
1785 return;
1786 }
1787
1788 //work out which color was clicked
1789 QColor clickedColor = mCurrentColor;
1790 if ( mColor2.isValid() )
1791 {
1792 //two color sections, check if dragged color was the second color
1793 const int verticalSplit = std::round( height() / 2.0 );
1794 if ( mDragStartPosition.y() >= verticalSplit )
1795 {
1796 clickedColor = mColor2;
1797 }
1798 }
1799 emit colorChanged( clickedColor );
1800}
1801
1803{
1804 //handle dragging colors from button
1805
1806 if ( !( e->buttons() & Qt::LeftButton ) )
1807 {
1808 //left button not depressed, so not a drag
1810 return;
1811 }
1812
1813 if ( ( e->pos() - mDragStartPosition ).manhattanLength() < QApplication::startDragDistance() )
1814 {
1815 //mouse not moved, so not a drag
1817 return;
1818 }
1819
1820 //user is dragging color
1821
1822 //work out which color is being dragged
1823 QColor dragColor = mCurrentColor;
1824 if ( mColor2.isValid() )
1825 {
1826 //two color sections, check if dragged color was the second color
1827 const int verticalSplit = std::round( height() / 2.0 );
1828 if ( mDragStartPosition.y() >= verticalSplit )
1829 {
1830 dragColor = mColor2;
1831 }
1832 }
1833
1834 QDrag *drag = new QDrag( this );
1835 drag->setMimeData( QgsSymbolLayerUtils::colorToMimeData( dragColor ) );
1836 drag->setPixmap( createDragIcon( dragColor ) );
1837 drag->exec( Qt::CopyAction );
1838}
1839
1840
1841//
1842// QgsColorWidgetAction
1843//
1844
1846 : QWidgetAction( parent )
1847 , mMenu( menu )
1848 , mColorWidget( colorWidget )
1849{
1850 setDefaultWidget( mColorWidget );
1851 connect( mColorWidget, &QgsColorWidget::colorChanged, this, &QgsColorWidgetAction::setColor );
1852
1853 connect( this, &QAction::hovered, this, &QgsColorWidgetAction::onHover );
1854 connect( mColorWidget, &QgsColorWidget::hovered, this, &QgsColorWidgetAction::onHover );
1855}
1856
1857void QgsColorWidgetAction::onHover()
1858{
1859 //see https://bugreports.qt.io/browse/QTBUG-10427?focusedCommentId=185610&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-185610
1860 if ( mSuppressRecurse )
1861 {
1862 return;
1863 }
1864
1865 if ( mMenu )
1866 {
1867 mSuppressRecurse = true;
1868 mMenu->setActiveAction( this );
1869 mSuppressRecurse = false;
1870 }
1871}
1872
1873void QgsColorWidgetAction::setColor( const QColor &color )
1874{
1875 emit colorChanged( color );
1876 if ( mMenu && mDismissOnColorSelection )
1877 {
1878 QAction::trigger();
1879 mMenu->hide();
1880 }
1881}
static const double UI_SCALE_FACTOR
UI scaling factor.
Definition qgis.h:6938
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.
static const QgsSettingsEntryEnumFlag< ColorTextFormat > * settingsTextFormat
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.
A utility class for dynamic handling of changes to screen properties.
void screenDpiChanged(double dpi)
Emitted whenever the screen dpi associated with the widget is changed.
A template class for enum and flag settings entry.
static QgsSettingsTreeNode * sTreeColorWidgets
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:7938
#define BUILTIN_UNREACHABLE
Definition qgis.h:7974
#define Q_NOWARN_DEPRECATED_PUSH
Definition qgis.h:7937
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:7340
#define HUE_MAX
float float_type