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