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