QGIS API Documentation 3.99.0-Master (2fe06baccd8)
Loading...
Searching...
No Matches
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
20#include "qgsapplication.h"
21#include "qgsdoublespinbox.h"
22#include "qgsproject.h"
24#include "qgsrange.h"
25#include "qgsrangeslider.h"
26
27#include <QEvent>
28#include <QHBoxLayout>
29#include <QLabel>
30#include <QMenu>
31#include <QMouseEvent>
32#include <QPainterPath>
33#include <QToolButton>
34#include <QVBoxLayout>
35
36#include "moc_qgselevationcontrollerwidget.cpp"
37
39 : QWidget( parent )
40{
41 QVBoxLayout *vl = new QVBoxLayout();
42 vl->setContentsMargins( 0, 0, 0, 0 );
43
44 mConfigureButton = new QToolButton();
45 mConfigureButton->setPopupMode( QToolButton::InstantPopup );
46 mConfigureButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/propertyicons/settings.svg" ) ) );
47 QHBoxLayout *hl = new QHBoxLayout();
48 hl->setContentsMargins( 0, 0, 0, 0 );
49 hl->addWidget( mConfigureButton );
50 hl->addStretch();
51 vl->addLayout( hl );
52 mMenu = new QMenu( this );
53 mConfigureButton->setMenu( mMenu );
54
55 mSettingsAction = new QgsElevationControllerSettingsAction( mMenu );
56 mMenu->addAction( mSettingsAction );
57 mInvertDirectionAction = new QAction( tr( "Invert Direction" ), this );
58 mInvertDirectionAction->setCheckable( true );
59 mMenu->addAction( mInvertDirectionAction );
60
61 mSettingsAction->sizeSpin()->clear();
62 connect( mSettingsAction->sizeSpin(), qOverload<double>( &QgsDoubleSpinBox::valueChanged ), this, [this]( double size ) {
63 setFixedRangeSize( size < 0 ? -1 : size );
64 } );
65
66 mMenu->addSeparator();
67
68 mSlider = new QgsRangeSlider( Qt::Vertical );
69 mSlider->setFlippedDirection( true );
70 mSlider->setRangeLimits( 0, 100000 );
71 mSliderLabels = new QgsElevationControllerLabels();
72
73 QHBoxLayout *hlSlider = new QHBoxLayout();
74 hlSlider->setContentsMargins( 0, 0, 0, 0 );
75 hlSlider->setSpacing( 2 );
76 hlSlider->addWidget( mSlider );
77 hlSlider->addWidget( mSliderLabels, 1 );
78 hlSlider->addStretch();
79 vl->addLayout( hlSlider );
80
81 setCursor( Qt::ArrowCursor );
82
83 setLayout( vl );
84 updateWidgetMask();
85
87 // if project doesn't have a range, just default to ANY range!
88 setRangeLimits( projectRange.isInfinite() ? QgsDoubleRange( 0, 100 ) : projectRange );
89 connect( QgsProject::instance()->elevationProperties(), &QgsProjectElevationProperties::elevationRangeChanged, this, [this]( const QgsDoubleRange &range ) {
90 if ( !range.isInfinite() )
92 } );
93
94 connect( mSlider, &QgsRangeSlider::rangeChanged, this, [this]( int, int ) {
95 if ( mBlockSliderChanges )
96 return;
97
98 emit rangeChanged( range() );
99 mSliderLabels->setRange( range() );
100 } );
101
102 connect( mInvertDirectionAction, &QAction::toggled, this, [this]() {
103 mSlider->setFlippedDirection( !mInvertDirectionAction->isChecked() );
104 mSliderLabels->setInverted( mInvertDirectionAction->isChecked() );
105
106 emit invertedChanged( mInvertDirectionAction->isChecked() );
107 } );
108
109 // default initial value to full range
111 mSliderLabels->setRange( rangeLimits() );
112}
113
115{
116 QWidget::resizeEvent( event );
117 updateWidgetMask();
118}
119
121{
122 // if the current slider range is just the current range, but snapped to the slider precision, then losslessly return the current range
123 const int snappedLower = static_cast<int>( std::floor( mCurrentRange.lower() * mSliderPrecision ) );
124 const int snappedUpper = static_cast<int>( std::ceil( mCurrentRange.upper() * mSliderPrecision ) );
125 if ( snappedLower == mSlider->lowerValue() && snappedUpper == mSlider->upperValue() )
126 return mCurrentRange;
127
128 const QgsDoubleRange sliderRange( mSlider->lowerValue() / mSliderPrecision, mSlider->upperValue() / mSliderPrecision );
129 if ( mFixedRangeSize >= 0 )
130 {
131 // adjust range so that it has exactly the fixed width (given slider int precision the slider range
132 // will not have the exact fixed width)
133 if ( sliderRange.upper() + mFixedRangeSize <= mRangeLimits.upper() )
134 return QgsDoubleRange( sliderRange.lower(), sliderRange.lower() + mFixedRangeSize );
135 else
136 return QgsDoubleRange( sliderRange.upper() - mFixedRangeSize, sliderRange.upper() );
137 }
138 else
139 {
140 return sliderRange;
141 }
142}
143
145{
146 return mRangeLimits;
147}
148
153
155{
156 return mMenu;
157}
158
160{
161 if ( range == mCurrentRange )
162 return;
163
164 mCurrentRange = range;
165 mBlockSliderChanges = true;
166 mSlider->setRange( static_cast<int>( std::floor( range.lower() * mSliderPrecision ) ), static_cast<int>( std::ceil( range.upper() * mSliderPrecision ) ) );
167 mBlockSliderChanges = false;
168 emit rangeChanged( range );
169
170 mSliderLabels->setRange( mCurrentRange );
171}
172
174{
175 if ( limits.isInfinite() )
176 return;
177
178 mRangeLimits = limits;
179
180 const double limitRange = limits.upper() - limits.lower();
181
182 // pick a reasonable slider precision, given that the slider operates in integer values only
183 mSliderPrecision = std::max( 1000, mSlider->height() ) / limitRange;
184
185 mBlockSliderChanges = true;
186 mSlider->setRangeLimits( static_cast<int>( std::floor( limits.lower() * mSliderPrecision ) ), static_cast<int>( std::ceil( limits.upper() * mSliderPrecision ) ) );
187
188 // clip current range to fit limits
189 const double newCurrentLower = std::max( mCurrentRange.lower(), limits.lower() );
190 const double newCurrentUpper = std::min( mCurrentRange.upper(), limits.upper() );
191 const bool rangeHasChanged = newCurrentLower != mCurrentRange.lower() || newCurrentUpper != mCurrentRange.upper();
192
193 mSlider->setRange( static_cast<int>( std::floor( newCurrentLower * mSliderPrecision ) ), static_cast<int>( std::ceil( newCurrentUpper * mSliderPrecision ) ) );
194 mCurrentRange = QgsDoubleRange( newCurrentLower, newCurrentUpper );
195 mBlockSliderChanges = false;
196 if ( rangeHasChanged )
197 emit rangeChanged( mCurrentRange );
198
199 mSliderLabels->setLimits( mRangeLimits );
200}
201
202void QgsElevationControllerWidget::updateWidgetMask()
203{
204 // we want mouse events from this widgets children to be caught, but events
205 // on the widget itself to be ignored and passed to underlying widgets which are NOT THE DIRECT
206 // PARENT of this widget.
207 // this is definitively *****NOT***** possible with event filters, by overriding mouse events, or
208 // with the WA_TransparentForMouseEvents attribute
209
210 QRegion reg( frameGeometry() );
211 reg -= QRegion( geometry() );
212 reg += childrenRegion();
213 setMask( reg );
214}
215
217{
218 return mFixedRangeSize;
219}
220
222{
223 if ( size == mFixedRangeSize )
224 return;
225
226 mFixedRangeSize = size;
227 if ( mFixedRangeSize < 0 )
228 {
229 mSlider->setFixedRangeSize( -1 );
230 }
231 else
232 {
233 mSlider->setFixedRangeSize( static_cast<int>( std::round( mFixedRangeSize * mSliderPrecision ) ) );
234 }
235 if ( mFixedRangeSize != mSettingsAction->sizeSpin()->value() )
236 mSettingsAction->sizeSpin()->setValue( mFixedRangeSize );
237 emit fixedRangeSizeChanged( mFixedRangeSize );
238}
239
241{
242 mInvertDirectionAction->setChecked( inverted );
243}
244
245void QgsElevationControllerWidget::setSignificantElevations( const QList<double> &elevations )
246{
247 mSliderLabels->setSignificantElevations( elevations );
248}
249
250//
251// QgsElevationControllerLabels
252//
254QgsElevationControllerLabels::QgsElevationControllerLabels( QWidget *parent )
255 : QWidget( parent )
256{
257 // Drop the default widget font size by a couple of points
258 QFont smallerFont = font();
259 int fontSize = smallerFont.pointSize();
260#ifdef Q_OS_WIN
261 fontSize = std::max( fontSize - 1, 8 ); // bit less on windows, due to poor rendering of small point sizes
262#else
263 fontSize = std::max( fontSize - 2, 7 );
264#endif
265 smallerFont.setPointSize( fontSize );
266 setFont( smallerFont );
267
268 const QFontMetrics fm( smallerFont );
269 setMinimumWidth( fm.horizontalAdvance( '0' ) * 5 );
270 setAttribute( Qt::WA_TransparentForMouseEvents );
271}
272
273void QgsElevationControllerLabels::paintEvent( QPaintEvent * )
274{
275 QStyleOptionSlider styleOption;
276 styleOption.initFrom( this );
277
278 const QRect sliderRect = style()->subControlRect( QStyle::CC_Slider, &styleOption, QStyle::SC_SliderHandle, this );
279 const int sliderHeight = sliderRect.height();
280
281 QFont f = font();
282 const QFontMetrics fm( f );
283
284 const int left = rect().left() + 2;
285
286 const double limitRange = mLimits.upper() - mLimits.lower();
287 const double lowerFraction = ( mRange.lower() - mLimits.lower() ) / limitRange;
288 const double upperFraction = ( mRange.upper() - mLimits.lower() ) / limitRange;
289 const int lowerY = !mInverted
290 ? ( std::min( static_cast<int>( std::round( rect().bottom() - sliderHeight * 0.5 - ( rect().height() - sliderHeight ) * lowerFraction + fm.ascent() ) ), rect().bottom() - fm.descent() ) )
291 : ( std::max( static_cast<int>( std::round( rect().top() + sliderHeight * 0.5 + ( rect().height() - sliderHeight ) * lowerFraction - fm.descent() ) ), rect().top() + fm.ascent() ) );
292 const int upperY = !mInverted ? ( std::max( static_cast<int>( std::round( rect().bottom() - sliderHeight * 0.5 - ( rect().height() - sliderHeight ) * upperFraction - fm.descent() ) ), rect().top() + fm.ascent() ) )
293 : ( std::min( static_cast<int>( std::round( rect().top() + sliderHeight * 0.5 + ( rect().height() - sliderHeight ) * upperFraction + fm.ascent() ) ), rect().bottom() - fm.descent() ) );
294
295 const bool lowerIsCloseToLimit = !mInverted
296 ? ( lowerY + fm.height() > rect().bottom() - fm.descent() )
297 : ( lowerY - fm.height() < rect().top() + fm.ascent() );
298 const bool upperIsCloseToLimit = !mInverted
299 ? ( upperY - fm.height() < rect().top() + fm.ascent() )
300 : ( upperY + fm.height() > rect().bottom() - fm.descent() );
301 const bool lowerIsCloseToUpperLimit = !mInverted
302 ? ( lowerY - fm.height() < rect().top() + fm.ascent() )
303 : ( lowerY + fm.height() > rect().bottom() - fm.descent() );
304
305 QLocale locale;
306
307 QPainterPath path;
308
309 for ( double value : std::as_const( mSignificantElevations ) )
310 {
311 const double valueFraction = ( value - mLimits.lower() ) / limitRange;
312 const double verticalCenter = !mInverted
313 ? ( std::min( static_cast<int>( std::round( rect().bottom() - sliderHeight * 0.5 - ( rect().height() - sliderHeight ) * valueFraction + fm.capHeight() * 0.5 ) ), rect().bottom() - fm.descent() ) )
314 : ( std::max( static_cast<int>( std::round( rect().top() + sliderHeight * 0.5 + ( rect().height() - sliderHeight ) * valueFraction + fm.capHeight() * 0.5 ) ), rect().top() + fm.ascent() ) );
315
316 const bool valueIsCloseToLower = verticalCenter + fm.height() > lowerY && verticalCenter - fm.height() < lowerY;
317 if ( valueIsCloseToLower )
318 continue;
319
320 const bool valueIsCloseToUpper = verticalCenter + fm.height() > upperY && verticalCenter - fm.height() < upperY;
321 if ( valueIsCloseToUpper )
322 continue;
323
324 const bool valueIsCloseToLowerLimit = !mInverted
325 ? ( verticalCenter + fm.height() > rect().bottom() - fm.descent() )
326 : ( verticalCenter - fm.height() < rect().top() + fm.ascent() );
327 if ( valueIsCloseToLowerLimit )
328 continue;
329
330 const bool valueIsCloseToUpperLimit = !mInverted
331 ? ( verticalCenter - fm.height() < rect().top() + fm.ascent() )
332 : ( verticalCenter + fm.height() > rect().bottom() - fm.descent() );
333 if ( valueIsCloseToUpperLimit )
334 continue;
335
336 path.addText( left, verticalCenter, f, locale.toString( value ) );
337 }
338
339 if ( mLimits.lower() > std::numeric_limits<double>::lowest() )
340 {
341 if ( lowerIsCloseToLimit )
342 {
343 f.setBold( true );
344 path.addText( left, lowerY, f, locale.toString( mRange.lower() ) );
345 }
346 else
347 {
348 f.setBold( true );
349 path.addText( left, lowerY, f, locale.toString( mRange.lower() ) );
350 f.setBold( false );
351 path.addText( left, !mInverted ? ( rect().bottom() - fm.descent() ) : ( rect().top() + fm.ascent() ), f, locale.toString( mLimits.lower() ) );
352 }
353 }
354
355 if ( mLimits.upper() < std::numeric_limits<double>::max() )
356 {
357 if ( qgsDoubleNear( mRange.upper(), mRange.lower() ) )
358 {
359 if ( !lowerIsCloseToUpperLimit )
360 {
361 f.setBold( false );
362 path.addText( left, !mInverted ? ( rect().top() + fm.ascent() ) : ( rect().bottom() - fm.descent() ), f, locale.toString( mLimits.upper() ) );
363 }
364 }
365 else
366 {
367 if ( upperIsCloseToLimit )
368 {
369 f.setBold( true );
370 path.addText( left, upperY, f, locale.toString( mRange.upper() ) );
371 }
372 else
373 {
374 f.setBold( true );
375 path.addText( left, upperY, f, locale.toString( mRange.upper() ) );
376 f.setBold( false );
377 path.addText( left, !mInverted ? ( rect().top() + fm.ascent() ) : ( rect().bottom() - fm.descent() ), f, locale.toString( mLimits.upper() ) );
378 }
379 }
380 }
381
382 QPainter p( this );
383 p.setRenderHint( QPainter::Antialiasing, true );
384 const QColor bufferColor = palette().color( QPalette::Window );
385 const QColor textColor = palette().color( QPalette::WindowText );
386 QPen pen( bufferColor );
387 pen.setJoinStyle( Qt::RoundJoin );
388 pen.setCapStyle( Qt::RoundCap );
389 pen.setWidthF( 4 );
390 p.setPen( pen );
391 p.setBrush( Qt::NoBrush );
392 p.drawPath( path );
393 p.setPen( Qt::NoPen );
394 p.setBrush( QBrush( textColor ) );
395 p.drawPath( path );
396 p.end();
397}
398
399void QgsElevationControllerLabels::setLimits( const QgsDoubleRange &limits )
400{
401 if ( limits == mLimits )
402 return;
403
404 const QFontMetrics fm( font() );
405 const int maxChars = std::max( QLocale().toString( std::floor( limits.lower() ) ).length(), QLocale().toString( std::floor( limits.upper() ) ).length() ) + 3;
406 setMinimumWidth( fm.horizontalAdvance( '0' ) * maxChars );
407
408 mLimits = limits;
409 update();
410}
411
412void QgsElevationControllerLabels::setRange( const QgsDoubleRange &range )
413{
414 if ( range == mRange )
415 return;
416
417 mRange = range;
418 update();
419}
420
421void QgsElevationControllerLabels::setInverted( bool inverted )
422{
423 if ( inverted == mInverted )
424 return;
425
426 mInverted = inverted;
427 update();
428}
429
430void QgsElevationControllerLabels::setSignificantElevations( const QList<double> &elevations )
431{
432 if ( elevations == mSignificantElevations )
433 return;
434
435 mSignificantElevations = elevations;
436 update();
437}
438
439//
440// QgsElevationControllerSettingsAction
441//
442
443QgsElevationControllerSettingsAction::QgsElevationControllerSettingsAction( QWidget *parent )
444 : QWidgetAction( parent )
445{
446 QGridLayout *gLayout = new QGridLayout();
447 gLayout->setContentsMargins( 3, 2, 3, 2 );
448
449 QLabel *label = new QLabel( tr( "Fixed Range Size" ) );
450 gLayout->addWidget( label, 0, 0 );
451
452 mSizeSpin = new QgsDoubleSpinBox();
453 mSizeSpin->setDecimals( 4 );
454 mSizeSpin->setMinimum( -1.0 );
455 mSizeSpin->setMaximum( 999999999.0 );
456 mSizeSpin->setClearValue( -1, tr( "Not set" ) );
457 mSizeSpin->setKeyboardTracking( false );
458 mSizeSpin->setToolTip( tr( "Limit elevation range to a fixed size" ) );
459
460 gLayout->addWidget( mSizeSpin, 0, 1 );
461
462 QWidget *w = new QWidget();
463 w->setLayout( gLayout );
464 setDefaultWidget( w );
465}
466
467QgsDoubleSpinBox *QgsElevationControllerSettingsAction::sizeSpin()
468{
469 return mSizeSpin;
470}
471
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:233
bool isInfinite() const
Returns true if the range consists of all possible values.
Definition qgsrange.h:287
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 setSignificantElevations(const QList< double > &elevations)
Sets a list of significant elevations to highlight in 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 setInverted(bool inverted)
Sets whether the elevation slider should be inverted.
void setFixedRangeSize(double size)
Sets the fixed range size.
void invertedChanged(bool inverted)
Emitted when the elevation filter slider is inverted.
void fixedRangeSizeChanged(double size)
Emitted when the fixed range size is changed from the widget.
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.
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 rangeChanged(int minimum, int maximum)
Emitted when the range selected in the widget is changed.
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