QGIS API Documentation 4.1.0-Master (3b8ef1f72a3)
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"
25#include "qgssettingstree.h"
26#include "qgssymbollayerutils.h"
27
28#include <QDrag>
29#include <QFontMetrics>
30#include <QHBoxLayout>
31#include <QLineEdit>
32#include <QLineF>
33#include <QMenu>
34#include <QPainter>
35#include <QRectF>
36#include <QResizeEvent>
37#include <QString>
38#include <QStyleOptionFrame>
39#include <QToolButton>
40
41#include "moc_qgscolorwidgets.cpp"
42
43using namespace Qt::StringLiterals;
44
47
48#define HUE_MAX 360
49
50
51// TODO QGIS 5 remove typedef, QColor was qreal (double) and is now float
52typedef float float_type;
53
54
55//
56// QgsColorWidget
57//
58
60 : QWidget( parent )
61 , mCurrentColor( Qt::red )
63{
64 setAcceptDrops( true );
65}
66
68{
69 return static_cast<int>( std::round( componentValueF( mComponent ) * static_cast<float>( componentRange() ) ) );
70}
71
76
77QPixmap QgsColorWidget::createDragIcon( const QColor &color )
78{
79 //craft a pixmap for the drag icon
80 const int iconSize = QgsGuiUtils::scaleIconSize( 50 );
81 QPixmap pixmap( iconSize, iconSize );
82 pixmap.fill( Qt::transparent );
83 QPainter painter;
84 painter.begin( &pixmap );
85 //start with a light gray background
86 painter.fillRect( QRect( 0, 0, iconSize, iconSize ), QBrush( QColor( 200, 200, 200 ) ) );
87 //draw rect with white border, filled with current color
88 QColor pixmapColor = color;
89 pixmapColor.setAlpha( 255 );
90 painter.setBrush( QBrush( pixmapColor ) );
91 painter.setPen( QPen( Qt::white ) );
92 painter.drawRect( QRect( 1, 1, iconSize - 2, iconSize - 2 ) );
93 painter.end();
94 return pixmap;
95}
96
121
123{
124 return static_cast<int>( std::round( componentValueF( component ) * static_cast<float>( componentRange( component ) ) ) );
125}
126
128{
129 if ( !mCurrentColor.isValid() )
130 {
131 return -1;
132 }
133
134 // TODO QGIS 5 remove the nolint instructions, QColor was qreal (double) and is now float
135 // NOLINTBEGIN(bugprone-narrowing-conversions)
136 switch ( component )
137 {
139 return mCurrentColor.redF();
141 return mCurrentColor.greenF();
143 return mCurrentColor.blueF();
145 //hue is treated specially, to avoid -1 hues values from QColor for ambiguous hues
146 return hueF();
148 return mCurrentColor.hsvSaturationF();
150 return mCurrentColor.valueF();
152 return mCurrentColor.alphaF();
154 return mCurrentColor.cyanF();
156 return mCurrentColor.yellowF();
158 return mCurrentColor.magentaF();
160 return mCurrentColor.blackF();
161 default:
162 return -1;
163 }
164 // NOLINTEND(bugprone-narrowing-conversions)
165}
166
168{
169 return componentRange( mComponent );
170}
171
173{
175 {
176 //no component
177 return -1;
178 }
179
181 {
182 //hue ranges to HUE_MAX
183 return HUE_MAX;
184 }
185 else
186 {
187 //all other components range to 255
188 return 255;
189 }
190}
191
193{
194 return static_cast<int>( std::round( hueF() * HUE_MAX ) );
195}
196
198{
199 if ( mCurrentColor.hueF() >= 0 )
200 {
201 return mCurrentColor.hueF(); // NOLINT(bugprone-narrowing-conversions): TODO QGIS 5 remove the nolint instructions, QColor was qreal (double) and is now float
202 }
203 else
204 {
205 return mExplicitHue;
206 }
207}
208
210{
211 //clip value to sensible range
212 const float clippedValue = static_cast<float>( std::clamp( newValue, 0, componentRange( component ) ) ) / static_cast<float>( componentRange( component ) );
213 alterColorF( color, component, clippedValue );
214}
215
217{
218 float_type clippedValue = std::clamp( newValue, 0.f, 1.f );
219
220 if ( colorSpec( component ) == QColor::Spec::Cmyk )
221 {
222 float_type c, m, y, k, a;
223 color.getCmykF( &c, &m, &y, &k, &a );
224
225 switch ( component )
226 {
228 color.setCmykF( clippedValue, m, y, k, a );
229 break;
231 color.setCmykF( c, clippedValue, y, k, a );
232 break;
234 color.setCmykF( c, m, clippedValue, k, a );
235 break;
237 color.setCmykF( c, m, y, clippedValue, a );
238 break;
239 default:
240 return;
241 }
242 }
243 else
244 {
245 float_type r, g, b, a;
246 color.getRgbF( &r, &g, &b, &a );
247 float_type h, s, v;
248 color.getHsvF( &h, &s, &v );
249
250 switch ( component )
251 {
253 color.setRedF( clippedValue );
254 break;
256 color.setGreenF( clippedValue );
257 break;
259 color.setBlueF( clippedValue );
260 break;
262 color.setHsvF( clippedValue, s, v, a );
263 break;
265 color.setHsvF( h, clippedValue, v, a );
266 break;
268 color.setHsvF( h, s, clippedValue, a );
269 break;
271 color.setAlphaF( clippedValue );
272 break;
273 default:
274 return;
275 }
276 }
277}
278
280{
281 switch ( component )
282 {
283 case Red:
284 case Green:
285 case Blue:
286 return QColor::Spec::Rgb;
287
288 case Hue:
289 case Saturation:
290 case Value:
291 return QColor::Spec::Hsv;
292
293 case Cyan:
294 case Magenta:
295 case Yellow:
296 case Black:
297 return QColor::Spec::Cmyk;
298
299 default:
300 return QColor::Spec::Invalid;
301 }
302}
303
304QColor::Spec QgsColorWidget::colorSpec() const
305{
306 return colorSpec( mComponent );
307}
308
310{
311 static QPixmap sTranspBkgrd;
312
313 if ( sTranspBkgrd.isNull() )
314 sTranspBkgrd = QgsApplication::getThemePixmap( u"/transp-background_8x8.png"_s );
315
316 return sTranspBkgrd;
317}
318
319void QgsColorWidget::dragEnterEvent( QDragEnterEvent *e )
320{
321 //is dragged data valid color data?
322 bool hasAlpha;
323 const QColor mimeColor = QgsSymbolLayerUtils::colorFromMimeData( e->mimeData(), hasAlpha );
324
325 if ( mimeColor.isValid() )
326 {
327 //if so, we accept the drag
328 e->acceptProposedAction();
329 }
330}
331
332void QgsColorWidget::dropEvent( QDropEvent *e )
333{
334 //is dropped data valid color data?
335 bool hasAlpha = false;
336 QColor mimeColor = QgsSymbolLayerUtils::colorFromMimeData( e->mimeData(), hasAlpha );
337
338 if ( mimeColor.isValid() )
339 {
340 //accept drop and set new color
341 e->acceptProposedAction();
342
343 if ( !hasAlpha )
344 {
345 //mime color has no explicit alpha component, so keep existing alpha
346 mimeColor.setAlpha( mCurrentColor.alpha() );
347 }
348
349 setColor( mimeColor );
351 }
352
353 //could not get color from mime data
354}
355
356void QgsColorWidget::mouseMoveEvent( QMouseEvent *e )
357{
358 emit hovered();
359 e->accept();
360 //don't pass to QWidget::mouseMoveEvent, causes issues with widget used in QWidgetAction
361}
362
364{
365 e->accept();
366 //don't pass to QWidget::mousePressEvent, causes issues with widget used in QWidgetAction
367}
368
370{
371 e->accept();
372 //don't pass to QWidget::mouseReleaseEvent, causes issues with widget used in QWidgetAction
373}
374
376{
377 return mCurrentColor;
378}
379
381{
382 if ( component == mComponent )
383 {
384 return;
385 }
386
388 update();
389}
390
392{
393 setComponentValueF( static_cast<float>( value ) / static_cast< float >( componentRange( mComponent ) ) );
394}
395
396void QgsColorWidget::setComponentValueF( const float value )
397{
399 {
400 return;
401 }
402
403 //overwrite hue with explicit hue if required
405 {
406 float_type h, s, v, a;
407 mCurrentColor.getHsvF( &h, &s, &v, &a );
408
409 h = hueF();
410
411 mCurrentColor.setHsvF( h, s, v, a );
412 }
413
415
416 //update recorded hue
417 if ( mCurrentColor.hue() >= 0 )
418 {
419 mExplicitHue = mCurrentColor.hueF(); // NOLINT(bugprone-narrowing-conversions): TODO QGIS 5 remove the nolint instructions, QColor was qreal (double) and is now float
420 }
421
422 update();
423}
424
425void QgsColorWidget::setColor( const QColor &color, const bool emitSignals )
426{
427 if ( color == mCurrentColor )
428 {
429 return;
430 }
431
433
434 //update recorded hue
435 if ( color.hue() >= 0 )
436 {
437 mExplicitHue = color.hueF(); // NOLINT(bugprone-narrowing-conversions): TODO QGIS 5 remove the nolint instructions, QColor was qreal (double) and is now float
438 }
439
440 if ( emitSignals )
441 {
443 }
444
445 update();
446}
447
448
449//
450// QgsColorWheel
451//
452
454 : QgsColorWidget( parent )
455{
456 //create wheel hue brush - only do this once
457 QConicalGradient wheelGradient = QConicalGradient( 0, 0, 0 );
458 const int wheelStops = 20;
459 QColor gradColor = QColor::fromHsvF( 1.0, 1.0, 1.0 );
460 for ( int pos = 0; pos <= wheelStops; ++pos )
461 {
462 const double relativePos = static_cast<double>( pos ) / wheelStops;
463 gradColor.setHsvF( relativePos, 1, 1 );
464 wheelGradient.setColorAt( relativePos, gradColor );
465 }
466 mWheelBrush = QBrush( wheelGradient );
467}
468
470
472{
473 const int size = Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 22;
474 return QSize( size, size );
475}
476
477void QgsColorWheel::paintEvent( QPaintEvent *event )
478{
479 Q_UNUSED( event )
480 QPainter painter( this );
481
482 if ( mWidgetImage.isNull() || mWheelImage.isNull() || mTriangleImage.isNull() )
483 {
484 createImages( size() );
485 }
486
487 //draw everything in an image
488 mWidgetImage.fill( Qt::transparent );
489 QPainter imagePainter( &mWidgetImage );
490 imagePainter.setRenderHint( QPainter::Antialiasing );
491
492 if ( mWheelDirty )
493 {
494 //need to redraw the wheel image
495 createWheel();
496 }
497
498 //draw wheel centered on widget
499 const QPointF center = QPointF( mWidgetImage.width() / 2.0, mWidgetImage.height() / 2.0 );
500 imagePainter.drawImage( QPointF( center.x() - ( mWheelImage.width() / 2.0 ), center.y() - ( mWheelImage.height() / 2.0 ) ), mWheelImage );
501
502 //draw hue marker
503 const float h = hueF() * HUE_MAX;
504 const double length = mWheelImage.width() / 2.0;
505 QLineF hueMarkerLine = QLineF( center.x(), center.y(), center.x() + length, center.y() );
506 hueMarkerLine.setAngle( h );
507 imagePainter.save();
508 //use sourceIn mode for nicer antialiasing
509 imagePainter.setCompositionMode( QPainter::CompositionMode_SourceIn );
510 QPen pen;
511 pen.setWidthF( 2 * devicePixelRatioF() );
512 //adapt pen color for hue
513 pen.setColor( h > 20 && h < 200 ? Qt::black : Qt::white );
514 imagePainter.setPen( pen );
515 imagePainter.drawLine( hueMarkerLine );
516 imagePainter.restore();
517
518 //draw triangle
519 if ( mTriangleDirty )
520 {
521 createTriangle();
522 }
523 imagePainter.drawImage( QPointF( center.x() - ( mWheelImage.width() / 2.0 ), center.y() - ( mWheelImage.height() / 2.0 ) ), mTriangleImage );
524
525 //draw current color marker
526 const double triangleRadius = length - mWheelThickness * devicePixelRatioF() - 1;
527
528 //adapted from equations at https://github.com/timjb/colortriangle/blob/master/colortriangle.js by Tim Baumann
529 const double lightness = mCurrentColor.lightnessF();
530 const double hueRadians = ( h * M_PI / 180.0 );
531 const double hx = std::cos( hueRadians ) * triangleRadius;
532 const double hy = -std::sin( hueRadians ) * triangleRadius;
533 const double sx = -std::cos( -hueRadians + ( M_PI / 3.0 ) ) * triangleRadius;
534 const double sy = -std::sin( -hueRadians + ( M_PI / 3.0 ) ) * triangleRadius;
535 const double vx = -std::cos( hueRadians + ( M_PI / 3.0 ) ) * triangleRadius;
536 const double vy = std::sin( hueRadians + ( M_PI / 3.0 ) ) * triangleRadius;
537 const double mx = ( sx + vx ) / 2.0;
538 const double my = ( sy + vy ) / 2.0;
539
540 const double a = ( 1 - 2.0 * std::fabs( lightness - 0.5 ) ) * mCurrentColor.hslSaturationF();
541 const double x = sx + ( vx - sx ) * lightness + ( hx - mx ) * a;
542 const double y = sy + ( vy - sy ) * lightness + ( hy - my ) * a;
543
544 //adapt pen color for lightness
545 pen.setColor( lightness > 0.7 ? Qt::black : Qt::white );
546 imagePainter.setPen( pen );
547 imagePainter.setBrush( Qt::NoBrush );
548 imagePainter.drawEllipse( QPointF( x + center.x(), y + center.y() ), 4.0 * devicePixelRatioF(), 4.0 * devicePixelRatioF() );
549 imagePainter.end();
550
551 //draw image onto widget
552 painter.drawImage( QRectF( 0, 0, width(), height() ), mWidgetImage );
553 painter.end();
554}
555
556void QgsColorWheel::setColor( const QColor &color, const bool emitSignals )
557{
558 if ( color.hue() >= 0 && !qgsDoubleNear( color.hue(), hueF() ) )
559 {
560 //hue has changed, need to redraw the triangle
561 mTriangleDirty = true;
562 }
563
564 QgsColorWidget::setColor( color, emitSignals );
565}
566
567void QgsColorWheel::createImages( const QSizeF size )
568{
569 const double wheelSize = std::min( size.width(), size.height() ) - mMargin * 2.0;
570 mWheelThickness = wheelSize / 15.0;
571
572 //recreate cache images at correct size
573 const double pixelRatio = devicePixelRatioF();
574 mWheelImage = QImage( wheelSize * pixelRatio, wheelSize * pixelRatio, QImage::Format_ARGB32 );
575 mTriangleImage = QImage( wheelSize * pixelRatio, wheelSize * pixelRatio, QImage::Format_ARGB32 );
576 mWidgetImage = QImage( size.width() * pixelRatio, size.height() * pixelRatio, QImage::Format_ARGB32 );
577
578 //trigger a redraw for the images
579 mWheelDirty = true;
580 mTriangleDirty = true;
581}
582
583void QgsColorWheel::resizeEvent( QResizeEvent *event )
584{
585 QgsColorWidget::resizeEvent( event );
586#ifdef Q_OS_WIN
587 // For some reason the first reported size than that of the parent widget, leading to a cut-off color wheel
588 if ( event->size().width() > parentWidget()->size().width() )
589 {
590 QSize newSize( std::min( event->size().width(), parentWidget()->size().width() - 2 ), std::min( event->size().height(), parentWidget()->size().height() - 2 ) );
591 resize( newSize );
592 createImages( newSize );
593 }
594 else
595 {
596 createImages( event->size() );
597 }
598#else
599 //recreate images for new size
600 createImages( event->size() );
601#endif
602}
603
604void QgsColorWheel::setColorFromPos( const QPointF pos )
605{
606 const QPointF center = QPointF( width() / 2.0, height() / 2.0 );
607 //line from center to mouse position
608 const QLineF line = QLineF( center.x(), center.y(), pos.x(), pos.y() );
609
610 QColor newColor = QColor();
611
612 float_type h, s, l, alpha;
613 mCurrentColor.getHslF( &h, &s, &l, &alpha );
614 //override hue with explicit hue, so we don't get -1 values from QColor for hue
615 h = hueF();
616
617 if ( mClickedPart == QgsColorWheel::Triangle )
618 {
619 //adapted from equations at https://github.com/timjb/colortriangle/blob/master/colortriangle.js by Tim Baumann
620
621 //position of event relative to triangle center
622 const double x = pos.x() - center.x();
623 const double y = pos.y() - center.y();
624
625 double eventAngleRadians = line.angle() * M_PI / 180.0;
626 const double hueRadians = h * 2 * M_PI;
627 double rad0 = std::fmod( eventAngleRadians + 2.0 * M_PI - hueRadians, 2.0 * M_PI );
628 double rad1 = std::fmod( rad0, ( ( 2.0 / 3.0 ) * M_PI ) ) - ( M_PI / 3.0 );
629 const double length = mWheelImage.width() / 2.0 / devicePixelRatioF();
630 const double triangleLength = length - mWheelThickness - 1;
631
632 const double a = 0.5 * triangleLength;
633 double b = std::tan( rad1 ) * a;
634 double r = std::sqrt( x * x + y * y );
635 const double maxR = std::sqrt( a * a + b * b );
636
637 if ( r > maxR )
638 {
639 const double dx = std::tan( rad1 ) * r;
640 double rad2 = std::atan( dx / maxR );
641 rad2 = std::min( rad2, M_PI / 3.0 );
642 rad2 = std::max( rad2, -M_PI / 3.0 );
643 eventAngleRadians += rad2 - rad1;
644 rad0 = std::fmod( eventAngleRadians + 2.0 * M_PI - hueRadians, 2.0 * M_PI );
645 rad1 = std::fmod( rad0, ( ( 2.0 / 3.0 ) * M_PI ) ) - ( M_PI / 3.0 );
646 b = std::tan( rad1 ) * a;
647 r = std::sqrt( a * a + b * b );
648 }
649
650 const double triangleSideLength = std::sqrt( 3.0 ) * triangleLength;
651 const double newL = ( ( -std::sin( rad0 ) * r ) / triangleSideLength ) + 0.5;
652 const double widthShare = 1.0 - ( std::fabs( newL - 0.5 ) * 2.0 );
653 const double newS = ( ( ( std::cos( rad0 ) * r ) + ( triangleLength / 2.0 ) ) / ( 1.5 * triangleLength ) ) / widthShare;
654 s = std::min( std::max( 0.f, static_cast<float>( newS ) ), 1.f );
655 l = std::min( std::max( 0.f, static_cast<float>( newL ) ), 1.f );
656 newColor = QColor::fromHslF( h, s, l );
657 //explicitly set the hue again, so that it's exact
658 newColor.setHsvF( h, newColor.hsvSaturationF(), newColor.valueF(), alpha );
659 }
660 else if ( mClickedPart == QgsColorWheel::Wheel )
661 {
662 //use hue angle
663 s = mCurrentColor.hsvSaturationF();
664 const float v = mCurrentColor.valueF(); // NOLINT(bugprone-narrowing-conversions): TODO QGIS 5 remove the nolint instructions, QColor was qreal (double) and is now float
665 const qreal newHue = line.angle() / HUE_MAX;
666 newColor = QColor::fromHsvF( static_cast<float>( newHue ), s, v, alpha );
667 //hue has changed, need to redraw triangle
668 mTriangleDirty = true;
669 }
670
671 if ( newColor.isValid() && newColor != mCurrentColor )
672 {
673 //color has changed
674 mCurrentColor = QColor( newColor );
675
676 if ( mCurrentColor.hueF() >= 0 )
677 {
678 //color has a valid hue, so update the QgsColorWidget's explicit hue
679 mExplicitHue = mCurrentColor.hueF(); // NOLINT(bugprone-narrowing-conversions): TODO QGIS 5 remove the nolint instructions, QColor was qreal (double) and is now float
680 }
681
682 update();
684 }
685}
686
687void QgsColorWheel::mouseMoveEvent( QMouseEvent *event )
688{
689 if ( mIsDragging )
690 setColorFromPos( event->pos() );
691
693}
694
695void QgsColorWheel::mousePressEvent( QMouseEvent *event )
696{
697 if ( event->button() == Qt::LeftButton )
698 {
699 mIsDragging = true;
700 //calculate where the event occurred -- on the wheel or inside the triangle?
701
702 //create a line from the widget's center to the event
703 const QLineF line = QLineF( width() / 2.0, height() / 2.0, event->pos().x(), event->pos().y() );
704
705 const double innerLength = mWheelImage.width() / 2.0 / devicePixelRatioF() - mWheelThickness;
706 if ( line.length() < innerLength )
707 {
708 mClickedPart = QgsColorWheel::Triangle;
709 }
710 else
711 {
712 mClickedPart = QgsColorWheel::Wheel;
713 }
714 setColorFromPos( event->pos() );
715 }
716 else
717 {
719 }
720}
721
722void QgsColorWheel::mouseReleaseEvent( QMouseEvent *event )
723{
724 if ( event->button() == Qt::LeftButton )
725 {
726 mIsDragging = false;
727 mClickedPart = QgsColorWheel::None;
728 }
729 else
730 {
732 }
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;
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, 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 * devicePixelRatioF(), wheelRadius - mWheelThickness * 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.height() / 2.0 );
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;
780 const double triangleRadius = wheelRadius - mWheelThickness * 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%"_s );
1443 mSpinBox->setMinimumWidth( 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 ) );
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:6690
static QPixmap getThemePixmap(const QString &name, const QColor &foreColor=QColor(), const QColor &backColor=QColor(), int size=16)
Helper to get a theme icon as a pixmap.
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
QSize sizeHint() const override
void resizeEvent(QResizeEvent *event) override
void mouseReleaseEvent(QMouseEvent *event) override
void mousePressEvent(QMouseEvent *event) override
void setComponent(ColorComponent component) override
Sets the color component which the widget controls.
void setColor(const QColor &color, bool emitSignals=false) override
void mouseMoveEvent(QMouseEvent *event) override
void paintEvent(QPaintEvent *event) override
QgsColorBox(QWidget *parent=nullptr, ColorComponent component=Value)
Construct a new color box widget.
~QgsColorBox() override
virtual void setColor2(const QColor &color)
Sets the second color for the widget.
void mouseMoveEvent(QMouseEvent *e) override
QSize sizeHint() const override
void paintEvent(QPaintEvent *event) override
void mouseReleaseEvent(QMouseEvent *e) override
void mousePressEvent(QMouseEvent *e) override
QgsColorPreviewWidget(QWidget *parent=nullptr)
Construct a new color preview widget.
A color ramp widget.
void setMarkerSize(int markerSize)
Sets the size for drawing the triangular markers on the ramp.
void setInteriorMargin(int margin)
Sets the margin between the edge of the widget and the ramp.
Q_DECL_DEPRECATED void valueChanged(int value)
Emitted when the widget's color component value changes.
void paintEvent(QPaintEvent *event) override
void keyPressEvent(QKeyEvent *event) override
void mousePressEvent(QMouseEvent *event) override
Orientation orientation() const
Fetches the orientation for the color ramp.
void valueChangedF(float value)
Emitted when the widget's color component value changes.
void wheelEvent(QWheelEvent *event) override
QgsColorRampWidget(QWidget *parent=nullptr, ColorComponent component=QgsColorWidget::Red, Orientation orientation=QgsColorRampWidget::Horizontal)
Construct a new color ramp widget.
QSize sizeHint() const override
void mouseMoveEvent(QMouseEvent *event) override
void mouseReleaseEvent(QMouseEvent *event) override
void setOrientation(Orientation orientation)
Sets the orientation for the color ramp.
void setShowFrame(bool showFrame)
Sets whether the ramp should be drawn within a frame.
Orientation
Specifies the orientation of a color ramp.
@ Horizontal
Horizontal ramp.
@ Vertical
Vertical ramp.
bool showFrame() const
Fetches whether the ramp is drawn within a frame.
void setColor(const QColor &color, bool emitSignals=false) override
Sets the color for the widget.
void setComponent(ColorComponent component) override
Sets the color component which the widget controls.
void setComponentValueF(float value) override
Alters the widget's color by setting the value for the widget's color component.
QgsColorSliderWidget(QWidget *parent=nullptr, ColorComponent component=QgsColorWidget::Red)
Construct a new color slider widget.
QgsColorTextWidget(QWidget *parent=nullptr)
Construct a new color line edit widget.
@ Rgba
Rgba( r, g, b, a ) format, with alpha.
@ Rgb
Rgb( r, g, b ) format.
@ HexRgbA
#RRGGBBAA in hexadecimal, with alpha
@ HexRgb
#RRGGBB in hexadecimal
void setColor(const QColor &color, bool emitSignals=false) override
Sets the color for the widget.
void setAllowOpacity(bool allowOpacity)
Sets whether opacity modification (transparency) is permitted.
static const QgsSettingsEntryEnumFlag< ColorTextFormat > * settingsTextFormat
void resizeEvent(QResizeEvent *event) override
void paintEvent(QPaintEvent *event) override
QgsColorWheel(QWidget *parent=nullptr)
Constructs a new color wheel widget.
void mousePressEvent(QMouseEvent *event) override
QSize sizeHint() const override
void mouseReleaseEvent(QMouseEvent *event) override
void mouseMoveEvent(QMouseEvent *event) override
void resizeEvent(QResizeEvent *event) override
void setColor(const QColor &color, bool emitSignals=false) override
~QgsColorWheel() override
QgsColorWidget * colorWidget()
Returns the color widget contained in the widget action.
void colorChanged(const QColor &color)
Emitted when a color has been selected from the widget.
QgsColorWidgetAction(QgsColorWidget *colorWidget, QMenu *menu=nullptr, QWidget *parent=nullptr)
Construct a new color widget action.
A base class for interactive color widgets.
static void alterColorF(QColor &color, QgsColorWidget::ColorComponent component, float newValue)
Alters a color by modifying the value of a specific color component.
static Q_DECL_DEPRECATED void alterColor(QColor &color, QgsColorWidget::ColorComponent component, int newValue)
Alters a color by modifying the value of a specific color component.
void mousePressEvent(QMouseEvent *e) override
void hovered()
Emitted when mouse hovers over widget.
QgsColorWidget(QWidget *parent=nullptr, ColorComponent component=Multiple)
Construct a new color widget.
virtual void setComponentValueF(float value)
Alters the widget's color by setting the value for the widget's color component.
ColorComponent component() const
Returns the color component which the widget controls.
QColor color() const
Returns the current color for the widget.
ComponentUnit
Specified the color component unit.
@ Degree
Degree values in the range 0-359.
@ Percent
Percent values in the range 0-100.
@ Scaled0to255
Values in the range 0-255.
void colorChanged(const QColor &color)
Emitted when the widget's color changes.
void mouseReleaseEvent(QMouseEvent *e) override
virtual Q_DECL_DEPRECATED void setComponentValue(int value)
Alters the widget's color by setting the value for the widget's color component.
void mouseMoveEvent(QMouseEvent *e) override
Q_DECL_DEPRECATED int componentValue() const
Returns the current value of the widget's color component.
static QPixmap createDragIcon(const QColor &color)
Create an icon for dragging colors.
float hueF() const
Returns the hue for the widget.
float mExplicitHue
QColor wipes the hue information when it is ambiguous (e.g., for saturation = 0).
void dropEvent(QDropEvent *e) override
float componentValueF() const
Returns the current value of the widget's color component.
static ComponentUnit componentUnit(ColorComponent component)
Returns color component unit.
virtual void setComponent(QgsColorWidget::ColorComponent component)
Sets the color component which the widget controls.
ColorComponent mComponent
int componentRange() const
Returns the range of valid values for the color widget's component.
QColor::Spec colorSpec() const
Returns color widget type of color, either RGB, HSV, CMYK, or Invalid if this component value is Mult...
Q_DECL_DEPRECATED int hue() const
Returns the hue for the widget.
virtual void setColor(const QColor &color, bool emitSignals=false)
Sets the color for the widget.
static const QPixmap & transparentBackground()
Generates a checkboard pattern pixmap for use as a background to transparent colors.
ColorComponent
Specifies the color component which the widget alters.
@ Hue
Hue component of color (based on HSV model).
@ Alpha
Alpha component (opacity) of color.
@ Green
Green component of color.
@ Red
Red component of color.
@ Saturation
Saturation component of color (based on HSV model).
@ Magenta
Magenta component (based on CMYK model) of color.
@ Yellow
Yellow component (based on CMYK model) of color.
@ Black
Black component (based on CMYK model) of color.
@ Cyan
Cyan component (based on CMYK model) of color.
@ Blue
Blue component of color.
@ Value
Value component of color (based on HSV model).
@ Multiple
Widget alters multiple color components.
void dragEnterEvent(QDragEnterEvent *e) override
The QgsSpinBox is a spin box with a clear button that will set the value to the defined clear value.
A template class for enum and flag settings entry.
static QgsSettingsTreeNode * sTreeColorWidgets
static QColor parseColorWithAlpha(const QString &colorStr, bool &containsAlpha, bool strictEval=false)
Attempts to parse a string as a color using a variety of common formats, including hex codes,...
static QColor colorFromMimeData(const QMimeData *data, bool &hasAlpha)
Attempts to parse mime data as a color.
static QMimeData * colorToMimeData(const QColor &color)
Creates mime data from a color.
double ANALYSIS_EXPORT angle(QgsPoint *p1, QgsPoint *p2, QgsPoint *p3, QgsPoint *p4)
Calculates the angle between two segments (in 2 dimension, z-values are ignored).
int scaleIconSize(int standardSize)
Scales an icon size to compensate for display pixel density, making the icon size hi-dpi friendly,...
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c
#define Q_NOWARN_DEPRECATED_POP
Definition qgis.h:7621
#define BUILTIN_UNREACHABLE
Definition qgis.h:7657
#define Q_NOWARN_DEPRECATED_PUSH
Definition qgis.h:7620
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:7077
#define HUE_MAX
float float_type