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