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