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