QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
qgselevationcontrollerwidget.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgselevationcontrollerwidget.cpp
3 ---------------
4 begin : March 2024
5 copyright : (C) 2024 by Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
19#include "qgsrangeslider.h"
20#include "qgsrange.h"
21#include "qgsproject.h"
23#include "qgsapplication.h"
24#include "qgsdoublespinbox.h"
25
26#include <QVBoxLayout>
27#include <QHBoxLayout>
28#include <QToolButton>
29#include <QEvent>
30#include <QMouseEvent>
31#include <QMenu>
32#include <QPainterPath>
33#include <QLabel>
34
36 : QWidget( parent )
37{
38 QVBoxLayout *vl = new QVBoxLayout();
39 vl->setContentsMargins( 0, 0, 0, 0 );
40
41 mConfigureButton = new QToolButton();
42 mConfigureButton->setPopupMode( QToolButton::InstantPopup );
43 mConfigureButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/propertyicons/settings.svg" ) ) );
44 QHBoxLayout *hl = new QHBoxLayout();
45 hl->setContentsMargins( 0, 0, 0, 0 );
46 hl->addWidget( mConfigureButton );
47 hl->addStretch();
48 vl->addLayout( hl );
49 mMenu = new QMenu( this );
50 mConfigureButton->setMenu( mMenu );
51
52 QgsElevationControllerSettingsAction *settingsAction = new QgsElevationControllerSettingsAction( mMenu );
53 mMenu->addAction( settingsAction );
54
55 settingsAction->sizeSpin()->clear();
56 connect( settingsAction->sizeSpin(), qOverload< double >( &QgsDoubleSpinBox::valueChanged ), this, [this]( double size )
57 {
58 setFixedRangeSize( size < 0 ? -1 : size );
59 } );
60
61 mMenu->addSeparator();
62
63 mSlider = new QgsRangeSlider( Qt::Vertical );
64 mSlider->setFlippedDirection( true );
65 mSlider->setRangeLimits( 0, 100000 );
66 mSliderLabels = new QgsElevationControllerLabels();
67
68 QHBoxLayout *hlSlider = new QHBoxLayout();
69 hlSlider->setContentsMargins( 0, 0, 0, 0 );
70 hlSlider->setSpacing( 2 );
71 hlSlider->addWidget( mSlider );
72 hlSlider->addWidget( mSliderLabels, 1 );
73 hlSlider->addStretch();
74 vl->addLayout( hlSlider );
75
76 setCursor( Qt::ArrowCursor );
77
78 setLayout( vl );
79 updateWidgetMask();
80
82 // if project doesn't have a range, just default to ANY range!
83 setRangeLimits( projectRange.isInfinite() ? QgsDoubleRange( 0, 100 ) : projectRange );
84 connect( QgsProject::instance()->elevationProperties(), &QgsProjectElevationProperties::elevationRangeChanged, this, [this]( const QgsDoubleRange & range )
85 {
86 if ( !range.isInfinite() )
88 } );
89
90 connect( mSlider, &QgsRangeSlider::rangeChanged, this, [this]( int, int )
91 {
92 if ( mBlockSliderChanges )
93 return;
94
95 emit rangeChanged( range() );
96 mSliderLabels->setRange( range() );
97 } );
98
99 // default initial value to full range
101 mSliderLabels->setRange( rangeLimits() );
102}
103
105{
106 QWidget::resizeEvent( event );
107 updateWidgetMask();
108}
109
111{
112 // if the current slider range is just the current range, but snapped to the slider precision, then losslessly return the current range
113 const int snappedLower = static_cast< int >( std::floor( mCurrentRange.lower() * mSliderPrecision ) );
114 const int snappedUpper = static_cast< int >( std::ceil( mCurrentRange.upper() * mSliderPrecision ) );
115 if ( snappedLower == mSlider->lowerValue() && snappedUpper == mSlider->upperValue() )
116 return mCurrentRange;
117
118 const QgsDoubleRange sliderRange( mSlider->lowerValue() / mSliderPrecision, mSlider->upperValue() / mSliderPrecision );
119 if ( mFixedRangeSize >= 0 )
120 {
121 // adjust range so that it has exactly the fixed width (given slider int precision the slider range
122 // will not have the exact fixed width)
123 if ( sliderRange.upper() + mFixedRangeSize <= mRangeLimits.upper() )
124 return QgsDoubleRange( sliderRange.lower(), sliderRange.lower() + mFixedRangeSize );
125 else
126 return QgsDoubleRange( sliderRange.upper() - mFixedRangeSize, sliderRange.upper() );
127 }
128 else
129 {
130 return sliderRange;
131 }
132}
133
135{
136 return mRangeLimits;
137}
138
140{
141 return mSlider;
142}
143
145{
146 return mMenu;
147}
148
150{
151 if ( range == mCurrentRange )
152 return;
153
154 mCurrentRange = range;
155 mBlockSliderChanges = true;
156 mSlider->setRange( static_cast< int >( std::floor( range.lower() * mSliderPrecision ) ),
157 static_cast< int >( std::ceil( range.upper() * mSliderPrecision ) ) );
158 mBlockSliderChanges = false;
159 emit rangeChanged( range );
160
161 mSliderLabels->setRange( mCurrentRange );
162}
163
165{
166 if ( limits.isInfinite() )
167 return;
168
169 mRangeLimits = limits;
170
171 const double limitRange = limits.upper() - limits.lower();
172
173 // pick a reasonable slider precision, given that the slider operates in integer values only
174 mSliderPrecision = std::max( 1000, mSlider->height() ) / limitRange;
175
176 mBlockSliderChanges = true;
177 mSlider->setRangeLimits( static_cast< int >( std::floor( limits.lower() * mSliderPrecision ) ),
178 static_cast< int >( std::ceil( limits.upper() * mSliderPrecision ) ) );
179
180 // clip current range to fit limits
181 const double newCurrentLower = std::max( mCurrentRange.lower(), limits.lower() );
182 const double newCurrentUpper = std::min( mCurrentRange.upper(), limits.upper() );
183 const bool rangeHasChanged = newCurrentLower != mCurrentRange.lower() || newCurrentUpper != mCurrentRange.upper();
184
185 mSlider->setRange( static_cast< int >( std::floor( newCurrentLower * mSliderPrecision ) ),
186 static_cast< int >( std::ceil( newCurrentUpper * mSliderPrecision ) ) );
187 mCurrentRange = QgsDoubleRange( newCurrentLower, newCurrentUpper );
188 mBlockSliderChanges = false;
189 if ( rangeHasChanged )
190 emit rangeChanged( mCurrentRange );
191
192 mSliderLabels->setLimits( mRangeLimits );
193}
194
195void QgsElevationControllerWidget::updateWidgetMask()
196{
197 // we want mouse events from this widgets children to be caught, but events
198 // on the widget itself to be ignored and passed to underlying widgets which are NOT THE DIRECT
199 // PARENT of this widget.
200 // this is definitively *****NOT***** possible with event filters, by overriding mouse events, or
201 // with the WA_TransparentForMouseEvents attribute
202
203 QRegion reg( frameGeometry() );
204 reg -= QRegion( geometry() );
205 reg += childrenRegion();
206 setMask( reg );
207}
208
210{
211 return mFixedRangeSize;
212}
213
215{
216 if ( size == mFixedRangeSize )
217 return;
218
219 mFixedRangeSize = size;
220 if ( mFixedRangeSize < 0 )
221 {
222 mSlider->setFixedRangeSize( -1 );
223 }
224 else
225 {
226 mSlider->setFixedRangeSize( static_cast< int >( std::round( mFixedRangeSize * mSliderPrecision ) ) );
227 }
228}
229
230//
231// QgsElevationControllerLabels
232//
234QgsElevationControllerLabels::QgsElevationControllerLabels( QWidget *parent )
235 : QWidget( parent )
236{
237 // Drop the default widget font size by a couple of points
238 QFont smallerFont = font();
239 int fontSize = smallerFont.pointSize();
240#ifdef Q_OS_WIN
241 fontSize = std::max( fontSize - 1, 8 ); // bit less on windows, due to poor rendering of small point sizes
242#else
243 fontSize = std::max( fontSize - 2, 7 );
244#endif
245 smallerFont.setPointSize( fontSize );
246 setFont( smallerFont );
247
248 const QFontMetrics fm( smallerFont );
249 setMinimumWidth( fm.horizontalAdvance( '0' ) * 5 );
250 setAttribute( Qt::WA_TransparentForMouseEvents );
251}
252
253void QgsElevationControllerLabels::paintEvent( QPaintEvent * )
254{
255 QStyleOptionSlider styleOption;
256 styleOption.initFrom( this );
257
258 const QRect sliderRect = style()->subControlRect( QStyle::CC_Slider, &styleOption, QStyle::SC_SliderHandle, this );
259 const int sliderHeight = sliderRect.height();
260
261 QFont f = font();
262 const QFontMetrics fm( f );
263
264 const int left = rect().left() + 2;
265
266 const double limitRange = mLimits.upper() - mLimits.lower();
267 const double lowerFraction = ( mRange.lower() - mLimits.lower() ) / limitRange;
268 const double upperFraction = ( mRange.upper() - mLimits.lower() ) / limitRange;
269 const int lowerY = std::min( static_cast< int >( std::round( rect().bottom() - sliderHeight * 0.5 - ( rect().height() - sliderHeight ) * lowerFraction + fm.ascent() ) ),
270 rect().bottom() - fm.descent() );
271 const int upperY = std::max( static_cast< int >( std::round( rect().bottom() - sliderHeight * 0.5 - ( rect().height() - sliderHeight ) * upperFraction - fm.descent() ) ),
272 rect().top() + fm.ascent() );
273
274 const bool lowerIsCloseToLimit = lowerY + fm.height() > rect().bottom() - fm.descent();
275 const bool upperIsCloseToLimit = upperY - fm.height() < rect().top() + fm.ascent();
276 const bool lowerIsCloseToUpperLimit = lowerY - fm.height() < rect().top() + fm.ascent();
277
278 QLocale locale;
279
280 QPainterPath path;
281 if ( mLimits.lower() > std::numeric_limits< double >::lowest() )
282 {
283 if ( lowerIsCloseToLimit )
284 {
285 f.setBold( true );
286 path.addText( left, lowerY, f, locale.toString( mRange.lower() ) );
287 }
288 else
289 {
290 f.setBold( true );
291 path.addText( left, lowerY, f, locale.toString( mRange.lower() ) );
292 f.setBold( false );
293 path.addText( left, rect().bottom() - fm.descent(), f, locale.toString( mLimits.lower() ) );
294 }
295 }
296
297 if ( mLimits.upper() < std::numeric_limits< double >::max() )
298 {
299 if ( qgsDoubleNear( mRange.upper(), mRange.lower() ) )
300 {
301 if ( !lowerIsCloseToUpperLimit )
302 {
303 f.setBold( false );
304 path.addText( left, rect().top() + fm.ascent(), f, locale.toString( mLimits.upper() ) );
305 }
306 }
307 else
308 {
309 if ( upperIsCloseToLimit )
310 {
311 f.setBold( true );
312 path.addText( left, upperY, f, locale.toString( mRange.upper() ) );
313 }
314 else
315 {
316 f.setBold( true );
317 path.addText( left, upperY, f, locale.toString( mRange.upper() ) );
318 f.setBold( false );
319 path.addText( left, rect().top() + fm.ascent(), f, locale.toString( mLimits.upper() ) );
320 }
321 }
322 }
323
324 QPainter p( this );
325 p.setRenderHint( QPainter::Antialiasing, true );
326 const QColor bufferColor = palette().color( QPalette::Window );
327 const QColor textColor = palette().color( QPalette::WindowText );
328 QPen pen( bufferColor );
329 pen.setJoinStyle( Qt::RoundJoin );
330 pen.setCapStyle( Qt::RoundCap );
331 pen.setWidthF( 4 );
332 p.setPen( pen );
333 p.setBrush( Qt::NoBrush );
334 p.drawPath( path );
335 p.setPen( Qt::NoPen );
336 p.setBrush( QBrush( textColor ) );
337 p.drawPath( path );
338 p.end();
339}
340
341void QgsElevationControllerLabels::setLimits( const QgsDoubleRange &limits )
342{
343 if ( limits == mLimits )
344 return;
345
346 const QFontMetrics fm( font() );
347 const int maxChars = std::max( QLocale().toString( std::floor( limits.lower() ) ).length(),
348 QLocale().toString( std::floor( limits.upper() ) ).length() ) + 3;
349 setMinimumWidth( fm.horizontalAdvance( '0' ) * maxChars );
350
351 mLimits = limits;
352 update();
353}
354
355void QgsElevationControllerLabels::setRange( const QgsDoubleRange &range )
356{
357 if ( range == mRange )
358 return;
359
360 mRange = range;
361 update();
362}
363
364//
365// QgsElevationControllerSettingsAction
366//
367
368QgsElevationControllerSettingsAction::QgsElevationControllerSettingsAction( QWidget *parent )
369 : QWidgetAction( parent )
370{
371 QGridLayout *gLayout = new QGridLayout();
372 gLayout->setContentsMargins( 3, 2, 3, 2 );
373
374 QLabel *label = new QLabel( tr( "Fixed Range Size" ) );
375 gLayout->addWidget( label, 0, 0 );
376
377 mSizeSpin = new QgsDoubleSpinBox();
378 mSizeSpin->setDecimals( 4 );
379 mSizeSpin->setMinimum( -1.0 );
380 mSizeSpin->setMaximum( 999999999.0 );
381 mSizeSpin->setClearValue( -1, tr( "Not set" ) );
382 mSizeSpin->setKeyboardTracking( false );
383 mSizeSpin->setToolTip( tr( "Limit elevation range to a fixed size" ) );
384
385 gLayout->addWidget( mSizeSpin, 0, 1 );
386
387 QWidget *w = new QWidget();
388 w->setLayout( gLayout );
389 setDefaultWidget( w );
390}
391
392QgsDoubleSpinBox *QgsElevationControllerSettingsAction::sizeSpin()
393{
394 return mSizeSpin;
395}
396
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
QgsRange which stores a range of double values.
Definition: qgsrange.h:231
bool isInfinite() const
Returns true if the range consists of all possible values.
Definition: qgsrange.h:285
The QgsSpinBox is a spin box with a clear button that will set the value to the defined clear value.
double fixedRangeSize() const
Returns the fixed range size, or -1 if no fixed size is set.
QgsRangeSlider * slider()
Returns a reference to the slider component of the widget.
void resizeEvent(QResizeEvent *event) override
void setRange(const QgsDoubleRange &range)
Sets the current visible range for the widget.
QgsDoubleRange rangeLimits() const
Returns the limits of the elevation range which can be selected by the widget.
void setFixedRangeSize(double size)
Sets the fixed range size.
void setRangeLimits(const QgsDoubleRange &limits)
Sets the limits of the elevation range which can be selected by the widget.
void rangeChanged(const QgsDoubleRange &range)
Emitted when the visible range from the widget is changed.
QgsElevationControllerWidget(QWidget *parent=nullptr)
Constructor for QgsElevationControllerWidget, with the specified parent widget.
QgsDoubleRange range() const
Returns the current visible range from the widget.
QMenu * menu()
Returns a reference to the widget's configuration menu, which can be used to add actions to the menu.
QgsDoubleRange elevationRange() const
Returns the project's elevation range, which indicates the upper and lower elevation limits associate...
void elevationRangeChanged(const QgsDoubleRange &range)
Emitted when the project's elevation is changed.
static QgsProject * instance()
Returns the QgsProject singleton instance.
Definition: qgsproject.cpp:481
const QgsProjectElevationProperties * elevationProperties() const
Returns the project's elevation properties, which contains the project's elevation related settings.
A slider control with two interactive endpoints, for interactive selection of a range of values.
void setRangeLimits(int minimum, int maximum)
Sets the minimum and maximum range limits for values allowed in the widget.
int upperValue() const
Returns the upper value for the range selected in the widget.
void rangeChanged(int minimum, int maximum)
Emitted when the range selected in the widget is changed.
void setFlippedDirection(bool flipped)
Sets whether the slider has its values flipped.
void setRange(int lower, int upper)
Sets the current range selected in the widget.
int lowerValue() const
Returns the lower value for the range selected in the widget.
void setFixedRangeSize(int size)
Sets the slider's fixed range size.
T lower() const
Returns the lower bound of the range.
Definition: qgsrange.h:78
T upper() const
Returns the upper bound of the range.
Definition: qgsrange.h:85
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:5207