QGIS API Documentation 4.3.0-Master (91d4a333d5d)
Loading...
Searching...
No Matches
qgscolorpickerhelper.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgscolorpickerhelper.cpp
3 ---------------
4 Date : November 2026
5 Copyright : (C) 2026 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
17
18#include "qgis.h"
19#include "qgsapplication.h"
20
21#include <QApplication>
22#include <QEvent>
23#include <QMouseEvent>
24#include <QTimer>
25#include <QWidget>
26#include <QWindow>
27
28#include "moc_qgscolorpickerhelper.cpp"
29
31 : QObject( parent )
32 , mSampleTimer( new QTimer( this ) )
33{
34 // avoid spamming signals when sampling and mouse moves quickly
35 // defer actual color sampling to a small timeout
36 mSampleTimer->setSingleShot( true );
37 mSampleTimer->setInterval( 100 );
38 connect( mSampleTimer, &QTimer::timeout, this, &QgsColorPickerHelper::hoverSampleTimeout );
39}
40
42{
43 if ( mPickingColor )
44 {
45 stopPicking();
46 }
47}
48
49void QgsColorPickerHelper::activate( int sampleRadius )
50{
51 if ( mPickingColor )
52 {
53 // already active
54 return;
55 }
56
57 mSampleRadius = sampleRadius;
58 mPickingColor = true;
59 mCurrentPickingColor = QColor();
60 mFinalColor = QColor();
61 QApplication::setOverrideCursor( QgsApplication::getThemeCursor( QgsApplication::Cursor::Sampler ) );
62
63 // set all top level widgets (windows) as mouse tracking, so user can sample colors
64 // from eg a different layout designer window or from the main canvas from the layout designer
65 const QList<QWidget *> topLevelWidgets = QApplication::topLevelWidgets();
66 for ( QWidget *widget : topLevelWidgets )
67 {
68 if ( !widget->hasMouseTracking() )
69 {
70 widget->setMouseTracking( true );
71 mWidgetsToCleanup.append( QPointer( widget ) );
72 }
73 }
74
75 // event filter gets installed for the entire application
76 qApp->installEventFilter( this );
77}
78
79bool QgsColorPickerHelper::eventFilter( QObject *watched, QEvent *event )
80{
81 if ( !mPickingColor )
82 {
83 return QObject::eventFilter( watched, event );
84 }
85
86 switch ( event->type() )
87 {
88 case QEvent::MouseMove:
89 case QEvent::HoverMove:
90 {
91 const QPoint pos = static_cast<QSinglePointEvent *>( event )->position().toPoint();
92 QPoint localPos;
93 if ( QWidget *target = findEventWindowWidgetAndLocalPos( watched, pos, localPos ) )
94 {
95 mPendingTargetWidget = target;
96 mPendingLocalPos = localPos;
97 // avoid spamming signals when sampling and mouse moves quickly
98 if ( !mSampleTimer->isActive() )
99 {
100 mSampleTimer->start();
101 }
102 }
103 // consume hover events -- otherwise eg toolbuttons will show in a raised state
104 // when the cursor passes over them as though they are interactable
105 return true;
106 }
107
108 case QEvent::MouseButtonPress:
109 {
110 QMouseEvent *mouseEvent = static_cast<QMouseEvent *>( event );
111 if ( mouseEvent->button() == Qt::LeftButton )
112 {
113 // left mouse button down = sample
114 mSampleTimer->stop();
115 QColor finalColor;
116 QPoint localPos;
117 if ( QWidget *target = findEventWindowWidgetAndLocalPos( watched, mouseEvent->position(), localPos ) )
118 {
119 finalColor = sampleColor( target, localPos );
120 }
121 mFinalColor = finalColor.isValid() ? finalColor : mCurrentPickingColor;
122 return true;
123 }
124 else if ( mouseEvent->button() == Qt::RightButton )
125 {
126 return true;
127 }
128 break;
129 }
130
131 case QEvent::MouseButtonRelease:
132 {
133 QMouseEvent *mouseEvent = static_cast<QMouseEvent *>( event );
134 if ( mouseEvent->button() == Qt::LeftButton )
135 {
136 if ( mFinalColor.isValid() )
137 onColorPicked( mFinalColor );
138 else
139 cancelPicking();
140 return true;
141 }
142 else if ( mouseEvent->button() == Qt::RightButton )
143 {
144 // right click = cancel picking
145 cancelPicking();
146 return true;
147 }
148 break;
149 }
150
151 case QEvent::KeyPress:
152 {
153 QKeyEvent *keyEvent = static_cast<QKeyEvent *>( event );
154 switch ( keyEvent->key() )
155 {
156 case Qt::Key_Escape:
157 cancelPicking();
158 return true;
159
160 case Qt::Key_Space:
161 case Qt::Key_Return:
162 case Qt::Key_Enter:
163 mSampleTimer->stop();
164 if ( mPendingTargetWidget )
165 {
166 const QColor sampled = sampleColor( mPendingTargetWidget, mPendingLocalPos );
167 if ( sampled.isValid() )
168 mCurrentPickingColor = sampled;
169 }
170 onColorPicked( mCurrentPickingColor );
171 return true;
172 return true;
173
174 default:
175 if ( keyEvent->text().isEmpty() )
176 return false; // don't swallow modifier keys
177 return true;
178 }
179 break;
180 }
181
182 default:
183 break;
184 }
185
186 return QObject::eventFilter( watched, event );
187}
188
189void QgsColorPickerHelper::hoverSampleTimeout()
190{
191 if ( !mPickingColor || !mPendingTargetWidget )
192 return;
193
194 const QColor sampled = sampleColor( mPendingTargetWidget, mPendingLocalPos );
195 if ( sampled.isValid() )
196 {
197 mCurrentPickingColor = sampled;
198 emit colorHovered( mCurrentPickingColor );
199 }
200}
201
202QWidget *QgsColorPickerHelper::findEventWindowWidgetAndLocalPos( QObject *watched, const QPointF &eventPos, QPoint &outPos )
203{
204 // object may already be a widget
205 if ( QWidget *w = qobject_cast<QWidget *>( watched ) )
206 {
207 // if so, we use the top level window associated with this widget -- that ensures that
208 // sampled colors correctly handle semi-transparent widgets, by returning the ACTUAL
209 // visible pixel color for the entire window at that point
210 QWidget *topLevel = w->window();
211 outPos = w->mapTo( topLevel, eventPos.toPoint() );
212 return topLevel;
213 }
214
215 // or it might be a QWindow, in which case we need to match it to a top-level widget
216 if ( QWindow *window = qobject_cast<QWindow *>( watched ) )
217 {
218 const QList<QWidget *> topLevelWidgets = QApplication::topLevelWidgets();
219 for ( QWidget *widget : topLevelWidgets )
220 {
221 if ( widget->windowHandle() == window )
222 {
223 outPos = eventPos.toPoint();
224 return widget;
225 }
226 }
227 }
228
229 return nullptr;
230}
231
232void QgsColorPickerHelper::onColorPicked( const QColor &color )
233{
234 stopPicking();
235 emit colorPicked( color );
236}
237
238void QgsColorPickerHelper::cancelPicking()
239{
240 stopPicking();
241 emit canceled();
242}
243
244void QgsColorPickerHelper::stopPicking()
245{
246 mSampleTimer->stop();
247 mPendingTargetWidget = nullptr;
248
249 qApp->removeEventFilter( this );
250
251 for ( const QPointer< QWidget > &widget : std::as_const( mWidgetsToCleanup ) )
252 {
253 if ( widget )
254 {
255 widget->setMouseTracking( false );
256 }
257 }
258 mWidgetsToCleanup.clear();
259
260 QgsApplication::restoreOverrideCursor();
261 mPickingColor = false;
262}
263
264QColor QgsColorPickerHelper::sampleColor( QWidget *widget, const QPoint &pos ) const
265{
266 if ( !widget )
267 return QColor();
268
269 const int radius = std::max( 0, mSampleRadius );
270 QRect grabRect( pos.x() - radius, pos.y() - radius, 1 + radius * 2, 1 + radius * 2 );
271 // don't try to grab stuff outside window bounds
272 grabRect = grabRect.intersected( widget->rect() );
273 if ( grabRect.isEmpty() )
274 return QColor();
275
276 const QPixmap pixmap = widget->grab( grabRect );
277 if ( pixmap.isNull() )
278 return QColor();
279
280 const QImage image = pixmap.toImage();
281 if ( radius == 0 )
282 {
283 return image.pixelColor( 0, 0 );
284 }
285 else
286 {
287 return averageColor( image );
288 }
289}
290
291QColor QgsColorPickerHelper::averageColor( const QImage &image )
292{
293 QRgb tmpRgb;
294 int colorCount = 0;
295 quint64 sumRed = 0;
296 quint64 sumBlue = 0;
297 quint64 sumGreen = 0;
298 //scan through image and sum rgb components
299 for ( int heightIndex = 0; heightIndex < image.height(); ++heightIndex )
300 {
301 const QRgb *scanLine = reinterpret_cast<const QRgb *>( image.constScanLine( heightIndex ) );
302 for ( int widthIndex = 0; widthIndex < image.width(); ++widthIndex )
303 {
304 tmpRgb = scanLine[widthIndex];
305 sumRed += qRed( tmpRgb );
306 sumBlue += qBlue( tmpRgb );
307 sumGreen += qGreen( tmpRgb );
308 colorCount++;
309 }
310 }
311 //calculate average components as floats
312 const double avgRed = static_cast<double>( sumRed ) / ( 255.0 * colorCount );
313 const double avgGreen = static_cast<double>( sumGreen ) / ( 255.0 * colorCount );
314 const double avgBlue = static_cast<double>( sumBlue ) / ( 255.0 * colorCount );
315
316 //create a new color representing the average
317 return QColor::fromRgbF( avgRed, avgGreen, avgBlue );
318}
static QCursor getThemeCursor(Cursor cursor)
Helper to get a theme cursor.
@ Sampler
Color/Value picker.
void colorPicked(const QColor &color)
Emitted when the user has finished picking a color from the screen.
void activate(int sampleRadius=0)
Activates the color picking operation.
void colorHovered(const QColor &color)
Emitted when the user hovers over a color from the screen.
QgsColorPickerHelper(QObject *parent)
Constructor for QgsColorPickerHelper for the specified parent widget.
bool eventFilter(QObject *watched, QEvent *event) override
void canceled()
Emitted when the user cancels the picking operation.