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