QGIS API Documentation 4.3.0-Master (0de80482b60)
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 "qgselevationutils.h"
23#include "qgsguiutils.h"
24#include "qgsmapcanvas.h"
25#include "qgsmaplayer.h"
27#include "qgsmathutils.h"
28#include "qgsproject.h"
30#include "qgsrange.h"
31#include "qgsrangeslider.h"
32
33#include <QEvent>
34#include <QHBoxLayout>
35#include <QKeyEvent>
36#include <QLabel>
37#include <QMenu>
38#include <QMouseEvent>
39#include <QPainterPath>
40#include <QString>
41#include <QToolButton>
42#include <QVBoxLayout>
43
44#include "moc_qgselevationcontrollerwidget.cpp"
45
46using namespace Qt::StringLiterals;
47
49 : QWidget( parent )
50{
51 QVBoxLayout *vl = new QVBoxLayout();
52 vl->setContentsMargins( 0, 0, 0, 0 );
53
54 mConfigureButton = new QToolButton();
55 mConfigureButton->setPopupMode( QToolButton::InstantPopup );
56 mConfigureButton->setIcon( QgsApplication::getThemeIcon( u"/propertyicons/settings.svg"_s ) );
57 QHBoxLayout *hl = new QHBoxLayout();
58 hl->setContentsMargins( 0, 0, 0, 0 );
59 hl->addWidget( mConfigureButton );
60 hl->addStretch();
61 vl->addLayout( hl );
62 mMenu = new QMenu( this );
63 mConfigureButton->setMenu( mMenu );
64
65 mSettingsAction = new QgsElevationControllerSettingsAction( mMenu );
66 mMenu->addAction( mSettingsAction );
67
68 mInvertDirectionAction = new QAction( tr( "Invert Direction" ), this );
69 mInvertDirectionAction->setCheckable( true );
70 mMenu->addAction( mInvertDirectionAction );
71
72 QMenu *limitsMenu = mSettingsAction->limitsMenu();
73
74 QAction *limitsFromProjectRangeAction = limitsMenu->addAction( tr( "Project Elevation Range" ) );
75 connect( limitsFromProjectRangeAction, &QAction::triggered, this, [this] {
77 if ( range.isInfinite() )
78 return;
79
80 // an explicitly configured range is applied as it is
82 setRange( range );
83 } );
84
85 QAction *limitsFromProjectLayersAction = limitsMenu->addAction( tr( "Project Layers" ) );
86 connect( limitsFromProjectLayersAction, &QAction::triggered, this, [this] { setLimitsFromLayers( QgsProject::instance()->mapLayers().values() ); } );
87
88 QAction *limitsFromCurrentLayerAction = limitsMenu->addAction( tr( "Current Layer" ) );
89 connect( limitsFromCurrentLayerAction, &QAction::triggered, this, [this] {
90 if ( mMapCanvas )
91 setLimitsFromLayers( { mMapCanvas->currentLayer() } );
92 } );
93
94 // the project and the current layer change at any time, so refresh availability when the menu opens
95 connect( limitsMenu, &QMenu::aboutToShow, this, [this, limitsFromProjectRangeAction, limitsFromProjectLayersAction, limitsFromCurrentLayerAction] {
96 limitsFromProjectRangeAction->setEnabled( !QgsProject::instance()->elevationProperties()->elevationRange().isInfinite() );
97
98 const QMap<QString, QgsMapLayer *> projectLayers = QgsProject::instance()->mapLayers();
99 limitsFromProjectLayersAction->setEnabled( std::any_of( projectLayers.begin(), projectLayers.end(), []( QgsMapLayer *layer ) { return layerHasElevation( layer ); } ) );
100
101 limitsFromCurrentLayerAction->setEnabled( mMapCanvas && layerHasElevation( mMapCanvas->currentLayer() ) );
102 } );
103
104 connect( mSettingsAction, &QgsElevationControllerSettingsAction::limitsChanged, this, &QgsElevationControllerWidget::setRangeLimits );
105 connect( mSettingsAction, &QgsElevationControllerSettingsAction::fixedRangeSizeChanged, this, &QgsElevationControllerWidget::setFixedRangeSize );
106 connect( mSettingsAction, &QgsElevationControllerSettingsAction::fullRangeRequested, this, [this] {
108 mSettingsAction->updateRangeSize( rangeLimits().upper() - rangeLimits().lower() );
109 } );
110
111 mSlider = new QgsRangeSlider( Qt::Vertical );
112 mSlider->setFlippedDirection( true );
113 mSlider->setRangeLimits( 0, 100000 );
114 mSliderLabels = new QgsElevationControllerLabels();
115
116 QHBoxLayout *hlSlider = new QHBoxLayout();
117 hlSlider->setContentsMargins( 0, 0, 0, 0 );
118 hlSlider->setSpacing( 2 );
119 hlSlider->addWidget( mSlider );
120 hlSlider->addWidget( mSliderLabels, 1 );
121 hlSlider->addStretch();
122 vl->addLayout( hlSlider );
123
124 setCursor( Qt::ArrowCursor );
125
126 setLayout( vl );
127 updateWidgetMask();
128
130 // if project doesn't have a range, just default to ANY range!
131 setRangeLimits( projectRange.isInfinite() ? QgsDoubleRange( 0, 100 ) : projectRange );
132 connect( QgsProject::instance()->elevationProperties(), &QgsProjectElevationProperties::elevationRangeChanged, this, [this]( const QgsDoubleRange &range ) {
133 if ( !range.isInfinite() )
135 } );
136
137 connect( mSlider, &QgsRangeSlider::rangeChanged, this, [this]( int, int ) {
138 if ( mBlockSliderChanges )
139 return;
140
141 const QgsDoubleRange snapped = snappedRange( sliderRange() );
142
143 // a drag within one snapping interval leaves the snapped range identical to the current
144 // one, but the handles still have to be moved back onto it, hence before the early return
145 mBlockSliderChanges++;
146 mSlider->setRange( static_cast<int>( std::floor( snapped.lower() * mSliderPrecision ) ), static_cast<int>( std::ceil( snapped.upper() * mSliderPrecision ) ) );
147 mBlockSliderChanges--;
148
149 if ( snapped == mCurrentRange )
150 return;
151
152 mCurrentRange = snapped;
153 emit rangeChanged( mCurrentRange );
154 mSliderLabels->setRange( mCurrentRange );
155 } );
156
157 connect( mMenu, &QMenu::aboutToShow, this, [this]() {
158 mSettingsAction->setLimits( mRangeLimits );
159 mSettingsAction->updateRangeSize( range().upper() - range().lower() );
160 } );
161
162 connect( mInvertDirectionAction, &QAction::toggled, this, [this]( bool inverted ) {
163 mSlider->setFlippedDirection( !inverted );
164 mSliderLabels->setInverted( inverted );
165
166 emit invertedChanged( inverted );
167 } );
168
169 // default initial value to full range
171 mSliderLabels->setRange( rangeLimits() );
172}
173
175{
176 QWidget::resizeEvent( event );
177 updateWidgetMask();
178}
179
181{
182 return mCurrentRange;
183}
184
186{
187 return mRangeLimits;
188}
189
194
196{
197 return mMenu;
198}
199
201{
202 return mMapCanvas;
203}
204
206{
207 mMapCanvas = canvas;
208
209 // a project which defines no elevation range of its own leaves
210 // the widget with an estimated range, the canvas layers give a usable one instead
211 if ( mMapCanvas && QgsProject::instance()->elevationProperties()->elevationRange().isInfinite() )
212 setLimitsFromLayers( mMapCanvas->layers( true ) );
213}
214
215bool QgsElevationControllerWidget::layerHasElevation( QgsMapLayer *layer )
216{
217 return layer && layer->elevationProperties() && layer->elevationProperties()->hasElevation();
218}
219
220void QgsElevationControllerWidget::setLimitsFromLayers( const QList<QgsMapLayer *> &layers )
221{
222 // raster layers gather statistics over their elevation band for this, which is not instant
223 const QgsTemporaryCursorOverride waitCursor( Qt::WaitCursor );
224
225 setLimitsFromRange( QgsElevationUtils::calculateZRangeForLayers( layers ) );
226}
227
228void QgsElevationControllerWidget::setLimitsFromRange( const QgsDoubleRange &range )
229{
230 if ( range.isInfinite() || range.isEmpty() )
231 return;
232
233 const QgsDoubleRange rounded = QgsMathUtils::roundedRange( range );
234 setRangeLimits( rounded );
235 setRange( rounded );
236}
237
239{
240 const QgsDoubleRange newRange = mFixedRangeSize >= 0 ? fixedSizeRangeFrom( range.lower() ) : range;
241 if ( newRange == mCurrentRange )
242 return;
243
244 mCurrentRange = newRange;
245 mBlockSliderChanges = true;
246 mSlider->setRange( static_cast<int>( std::floor( mCurrentRange.lower() * mSliderPrecision ) ), static_cast<int>( std::ceil( mCurrentRange.upper() * mSliderPrecision ) ) );
247 mBlockSliderChanges = false;
248 emit rangeChanged( mCurrentRange );
249
250 mSliderLabels->setRange( mCurrentRange );
251}
252
254{
255 if ( limits.isInfinite() || limits.upper() <= limits.lower() )
256 return;
257
258 mRangeLimits = limits;
259 mSettingsAction->setLimits( mRangeLimits );
260
261 const double limitRange = limits.upper() - limits.lower();
262
263 // pick a reasonable slider precision, given that the slider operates in integer values only
264 mSliderPrecision = std::max( 1000, mSlider->height() ) / limitRange;
265
266 // snap the slider to a tenth of the interval used to round elevation ranges, so that dragging
267 // stays smooth while still selecting round values
268 mSnapInterval = QgsMathUtils::roundingInterval( limitRange, 100 );
269 mSnapDecimals = mSnapInterval > 0 ? std::max( 0, -static_cast<int>( std::floor( std::log10( mSnapInterval ) ) ) ) : 0;
270
271 mBlockSliderChanges = true;
272 mSlider->setRangeLimits( static_cast<int>( std::floor( limits.lower() * mSliderPrecision ) ), static_cast<int>( std::ceil( limits.upper() * mSliderPrecision ) ) );
273
274 // the slider holds the fixed size in its own integer units, which the new precision changed
275 if ( mFixedRangeSize >= 0 )
276 mSlider->setFixedRangeSize( static_cast<int>( std::round( mFixedRangeSize * mSliderPrecision ) ) );
277
278 // clip current range to fit limits
279 double newCurrentLower = std::max( mCurrentRange.lower(), limits.lower() );
280 double newCurrentUpper = std::min( mCurrentRange.upper(), limits.upper() );
281 if ( mFixedRangeSize >= 0 )
282 {
283 // a locked size is kept, the range moves inside the new limits instead of being clipped
284 const QgsDoubleRange fitted = fixedSizeRangeFrom( newCurrentLower );
285 newCurrentLower = fitted.lower();
286 newCurrentUpper = fitted.upper();
287 }
288 const bool rangeHasChanged = newCurrentLower != mCurrentRange.lower() || newCurrentUpper != mCurrentRange.upper();
289
290 mSlider->setRange( static_cast<int>( std::floor( newCurrentLower * mSliderPrecision ) ), static_cast<int>( std::ceil( newCurrentUpper * mSliderPrecision ) ) );
291 mCurrentRange = QgsDoubleRange( newCurrentLower, newCurrentUpper );
292 mBlockSliderChanges = false;
293 if ( rangeHasChanged )
294 {
295 emit rangeChanged( mCurrentRange );
296
297 // the selected range was clipped by the new limits, so the range size shown in the menu is stale
298 mSettingsAction->updateRangeSize( mCurrentRange.upper() - mCurrentRange.lower() );
299 }
300
301 mSliderLabels->setLimits( mRangeLimits );
302 mSliderLabels->setRange( mCurrentRange );
303}
304
305QgsDoubleRange QgsElevationControllerWidget::snappedRange( const QgsDoubleRange &range ) const
306{
307 const double lower = snapValue( range.lower() );
308
309 // snapping must not alter the locked range size
310 if ( mFixedRangeSize >= 0 )
311 return fixedSizeRangeFrom( lower );
312
313 return QgsDoubleRange( lower, std::max( snapValue( range.upper() ), lower ) );
314}
315
316double QgsElevationControllerWidget::snapValue( double value ) const
317{
318 if ( mSnapInterval <= 0 )
319 return std::clamp( value, mRangeLimits.lower(), mRangeLimits.upper() );
320
321 double snapped = std::round( value / mSnapInterval ) * mSnapInterval;
322 if ( mSnapDecimals > 0 )
323 {
324 // strip the noise the multiplication above leaves in fractional values
325 snapped = qgsRound( snapped, mSnapDecimals );
326 }
327
328 // an elevation which is significant for the layers is a better snapping target than a round value
329 for ( double elevation : mSignificantElevations )
330 {
331 if ( std::fabs( elevation - value ) < std::fabs( snapped - value ) )
332 snapped = elevation;
333 }
334
335 return std::clamp( snapped, mRangeLimits.lower(), mRangeLimits.upper() );
336}
337
338QgsDoubleRange QgsElevationControllerWidget::sliderRange() const
339{
340 return QgsDoubleRange( mSlider->lowerValue() / mSliderPrecision, mSlider->upperValue() / mSliderPrecision );
341}
342
343QgsDoubleRange QgsElevationControllerWidget::fixedSizeRangeFrom( double lower ) const
344{
345 double upper = lower + mFixedRangeSize;
346 if ( upper > mRangeLimits.upper() )
347 {
348 upper = mRangeLimits.upper();
349 lower = std::max( upper - mFixedRangeSize, mRangeLimits.lower() );
350 }
351
352 return QgsDoubleRange( lower, upper );
353}
354
355void QgsElevationControllerWidget::updateWidgetMask()
356{
357 // we want mouse events from this widgets children to be caught, but events
358 // on the widget itself to be ignored and passed to underlying widgets which are NOT THE DIRECT
359 // PARENT of this widget.
360 // this is definitively *****NOT***** possible with event filters, by overriding mouse events, or
361 // with the WA_TransparentForMouseEvents attribute
362
363 QRegion reg( frameGeometry() );
364 reg -= QRegion( geometry() );
365 reg += childrenRegion();
366 setMask( reg );
367}
368
370{
371 return mFixedRangeSize;
372}
373
375{
376 if ( size == mFixedRangeSize )
377 return;
378
379 mFixedRangeSize = size;
380 if ( mFixedRangeSize < 0 )
381 {
382 mSlider->setFixedRangeSize( -1 );
383 }
384 else
385 {
386 mSlider->setFixedRangeSize( static_cast<int>( std::round( mFixedRangeSize * mSliderPrecision ) ) );
387 }
388
389 mSettingsAction->setFixedRangeSize( mFixedRangeSize );
390
391 emit fixedRangeSizeChanged( mFixedRangeSize );
392}
393
395{
396 mInvertDirectionAction->setChecked( inverted );
397}
398
399void QgsElevationControllerWidget::setSignificantElevations( const QList<double> &elevations )
400{
401 mSignificantElevations = elevations;
402 mSliderLabels->setSignificantElevations( elevations );
403}
404
405//
406// QgsElevationControllerLabels
407//
409QgsElevationControllerLabels::QgsElevationControllerLabels( QWidget *parent )
410 : QWidget( parent )
411{
412 // Drop the default widget font size by a couple of points
413 QFont smallerFont = font();
414 int fontSize = smallerFont.pointSize();
415#ifdef Q_OS_WIN
416 fontSize = std::max( fontSize - 1, 8 ); // bit less on windows, due to poor rendering of small point sizes
417#else
418 fontSize = std::max( fontSize - 2, 7 );
419#endif
420 smallerFont.setPointSize( fontSize );
421 setFont( smallerFont );
422
423 const QFontMetrics fm( smallerFont );
424 setMinimumWidth( fm.horizontalAdvance( '0' ) * 5 );
425 setAttribute( Qt::WA_TransparentForMouseEvents );
426}
427
428void QgsElevationControllerLabels::paintEvent( QPaintEvent * )
429{
430 QStyleOptionSlider styleOption;
431 styleOption.initFrom( this );
432
433 const QRect sliderRect = style()->subControlRect( QStyle::CC_Slider, &styleOption, QStyle::SC_SliderHandle, this );
434 const int sliderHeight = sliderRect.height();
435
436 QFont f = font();
437 const QFontMetrics fm( f );
438
439 const int left = rect().left() + 2;
440
441 const double limitRange = mLimits.upper() - mLimits.lower();
442 const double lowerFraction = ( mRange.lower() - mLimits.lower() ) / limitRange;
443 const double upperFraction = ( mRange.upper() - mLimits.lower() ) / limitRange;
444 const int lowerY
445 = !mInverted
446 ? ( std::min( static_cast<int>( std::round( rect().bottom() - sliderHeight * 0.5 - ( rect().height() - sliderHeight ) * lowerFraction + fm.ascent() ) ), rect().bottom() - fm.descent() ) )
447 : ( std::max( static_cast<int>( std::round( rect().top() + sliderHeight * 0.5 + ( rect().height() - sliderHeight ) * lowerFraction - fm.descent() ) ), rect().top() + fm.ascent() ) );
448 const int upperY
449 = !mInverted
450 ? ( std::max( static_cast<int>( std::round( rect().bottom() - sliderHeight * 0.5 - ( rect().height() - sliderHeight ) * upperFraction - fm.descent() ) ), rect().top() + fm.ascent() ) )
451 : ( std::min( static_cast<int>( std::round( rect().top() + sliderHeight * 0.5 + ( rect().height() - sliderHeight ) * upperFraction + fm.ascent() ) ), rect().bottom() - fm.descent() ) );
452
453 const bool lowerIsCloseToLimit = !mInverted ? ( lowerY + fm.height() > rect().bottom() - fm.descent() ) : ( lowerY - fm.height() < rect().top() + fm.ascent() );
454 const bool upperIsCloseToLimit = !mInverted ? ( upperY - fm.height() < rect().top() + fm.ascent() ) : ( upperY + fm.height() > rect().bottom() - fm.descent() );
455 const bool lowerIsCloseToUpperLimit = !mInverted ? ( lowerY - fm.height() < rect().top() + fm.ascent() ) : ( lowerY + fm.height() > rect().bottom() - fm.descent() );
456
457 QLocale locale;
458
459 QPainterPath path;
460
461 for ( double value : std::as_const( mSignificantElevations ) )
462 {
463 const double valueFraction = ( value - mLimits.lower() ) / limitRange;
464 const double verticalCenter
465 = !mInverted
466 ? ( std::min( static_cast<int>( std::round( rect().bottom() - sliderHeight * 0.5 - ( rect().height() - sliderHeight ) * valueFraction + fm.capHeight() * 0.5 ) ), rect().bottom() - fm.descent() ) )
467 : ( std::max( static_cast<int>( std::round( rect().top() + sliderHeight * 0.5 + ( rect().height() - sliderHeight ) * valueFraction + fm.capHeight() * 0.5 ) ), rect().top() + fm.ascent() ) );
468
469 const bool valueIsCloseToLower = verticalCenter + fm.height() > lowerY && verticalCenter - fm.height() < lowerY;
470 if ( valueIsCloseToLower )
471 continue;
472
473 const bool valueIsCloseToUpper = verticalCenter + fm.height() > upperY && verticalCenter - fm.height() < upperY;
474 if ( valueIsCloseToUpper )
475 continue;
476
477 const bool valueIsCloseToLowerLimit = !mInverted ? ( verticalCenter + fm.height() > rect().bottom() - fm.descent() ) : ( verticalCenter - fm.height() < rect().top() + fm.ascent() );
478 if ( valueIsCloseToLowerLimit )
479 continue;
480
481 const bool valueIsCloseToUpperLimit = !mInverted ? ( verticalCenter - fm.height() < rect().top() + fm.ascent() ) : ( verticalCenter + fm.height() > rect().bottom() - fm.descent() );
482 if ( valueIsCloseToUpperLimit )
483 continue;
484
485 path.addText( left, verticalCenter, f, locale.toString( value ) );
486 }
487
488 if ( mLimits.lower() > std::numeric_limits<double>::lowest() )
489 {
490 if ( lowerIsCloseToLimit )
491 {
492 f.setBold( true );
493 path.addText( left, lowerY, f, locale.toString( mRange.lower() ) );
494 }
495 else
496 {
497 f.setBold( true );
498 path.addText( left, lowerY, f, locale.toString( mRange.lower() ) );
499 f.setBold( false );
500 path.addText( left, !mInverted ? ( rect().bottom() - fm.descent() ) : ( rect().top() + fm.ascent() ), f, locale.toString( mLimits.lower() ) );
501 }
502 }
503
504 if ( mLimits.upper() < std::numeric_limits<double>::max() )
505 {
506 if ( qgsDoubleNear( mRange.upper(), mRange.lower() ) )
507 {
508 if ( !lowerIsCloseToUpperLimit )
509 {
510 f.setBold( false );
511 path.addText( left, !mInverted ? ( rect().top() + fm.ascent() ) : ( rect().bottom() - fm.descent() ), f, locale.toString( mLimits.upper() ) );
512 }
513 }
514 else
515 {
516 if ( upperIsCloseToLimit )
517 {
518 f.setBold( true );
519 path.addText( left, upperY, f, locale.toString( mRange.upper() ) );
520 }
521 else
522 {
523 f.setBold( true );
524 path.addText( left, upperY, f, locale.toString( mRange.upper() ) );
525 f.setBold( false );
526 path.addText( left, !mInverted ? ( rect().top() + fm.ascent() ) : ( rect().bottom() - fm.descent() ), f, locale.toString( mLimits.upper() ) );
527 }
528 }
529 }
530
531 QPainter p( this );
532 p.setRenderHint( QPainter::Antialiasing, true );
533 const QColor bufferColor = palette().color( QPalette::Window );
534 const QColor textColor = palette().color( QPalette::WindowText );
535 QPen pen( bufferColor );
536 pen.setJoinStyle( Qt::RoundJoin );
537 pen.setCapStyle( Qt::RoundCap );
538 pen.setWidthF( 4 );
539 p.setPen( pen );
540 p.setBrush( Qt::NoBrush );
541 p.drawPath( path );
542 p.setPen( Qt::NoPen );
543 p.setBrush( QBrush( textColor ) );
544 p.drawPath( path );
545 p.end();
546}
547
548void QgsElevationControllerLabels::setLimits( const QgsDoubleRange &limits )
549{
550 if ( limits == mLimits )
551 return;
552
553 const QFontMetrics fm( font() );
554 const int maxChars = std::max( QLocale().toString( std::floor( limits.lower() ) ).length(), QLocale().toString( std::floor( limits.upper() ) ).length() ) + 3;
555 setMinimumWidth( fm.horizontalAdvance( '0' ) * maxChars );
556
557 mLimits = limits;
558 update();
559}
560
561void QgsElevationControllerLabels::setRange( const QgsDoubleRange &range )
562{
563 if ( range == mRange )
564 return;
565
566 mRange = range;
567 update();
568}
569
570void QgsElevationControllerLabels::setInverted( bool inverted )
571{
572 if ( inverted == mInverted )
573 return;
574
575 mInverted = inverted;
576 update();
577}
578
579void QgsElevationControllerLabels::setSignificantElevations( const QList<double> &elevations )
580{
581 if ( elevations == mSignificantElevations )
582 return;
583
584 mSignificantElevations = elevations;
585 update();
586}
587
588//
589// QgsElevationControllerSettingsAction
590//
591
592QgsElevationControllerSettingsAction::QgsElevationControllerSettingsAction( QWidget *parent )
593 : QWidgetAction( parent )
594 , mMenu( qobject_cast<QMenu *>( parent ) )
595{
596 QGridLayout *gLayout = new QGridLayout();
597 gLayout->setContentsMargins( 3, 2, 3, 2 );
598
599 QLabel *limitsLabel = new QLabel( tr( "Limits" ) );
600 limitsLabel->setToolTip( tr( "Lowest and highest elevations which can be selected with the slider" ) );
601 gLayout->addWidget( limitsLabel, 0, 0 );
602
603 mLowerSpin = new QgsDoubleSpinBox();
604 mLowerSpin->setDecimals( 4 );
605 mLowerSpin->setMinimum( -999999999.0 );
606 mLowerSpin->setMaximum( 999999999.0 );
607 mLowerSpin->setShowClearButton( false );
608 mLowerSpin->setKeyboardTracking( false );
609 mLowerSpin->setToolTip( tr( "Lowest elevation which can be selected" ) );
610
611 gLayout->addWidget( mLowerSpin, 0, 1 );
612
613 QLabel *toLabel = new QLabel( tr( "to" ) );
614 gLayout->addWidget( toLabel, 0, 2 );
615
616 mUpperSpin = new QgsDoubleSpinBox();
617 mUpperSpin->setDecimals( 4 );
618 mUpperSpin->setMinimum( -999999999.0 );
619 mUpperSpin->setMaximum( 999999999.0 );
620 mUpperSpin->setShowClearButton( false );
621 mUpperSpin->setKeyboardTracking( false );
622 mUpperSpin->setToolTip( tr( "Highest elevation which can be selected" ) );
623
624 gLayout->addWidget( mUpperSpin, 0, 3 );
625
626 mLimitsButton = new QToolButton();
627 mLimitsButton->setIcon( QgsApplication::getThemeIcon( u"/mActionRefresh.svg"_s ) );
628 mLimitsButton->setPopupMode( QToolButton::InstantPopup );
629 mLimitsButton->setToolTip( tr( "Take the elevation limits from the project or the layers" ) );
630 mLimitsMenu = new QMenu( mLimitsButton );
631 mLimitsButton->setMenu( mLimitsMenu );
632
633 gLayout->addWidget( mLimitsButton, 0, 4 );
634
635 const QString rangeToolTip = tr(
636 "Size of the elevation range shown in the map, i.e. the difference between "
637 "the upper and the lower handle of the slider. Lock it to keep this size while "
638 "moving the slider, or clear it to show the full range."
639 );
640
641 QLabel *rangeLabel = new QLabel( tr( "Range" ) );
642 rangeLabel->setToolTip( rangeToolTip );
643 gLayout->addWidget( rangeLabel, 1, 0 );
644
645 mSizeSpin = new QgsDoubleSpinBox();
646 mSizeSpin->setDecimals( 4 );
647 mSizeSpin->setMinimum( 0.0 );
648 mSizeSpin->setMaximum( 999999999.0 );
649 mSizeSpin->setClearValue( 0, tr( "Full range" ) );
650 mSizeSpin->setKeyboardTracking( false );
651 mSizeSpin->setToolTip( rangeToolTip );
652
653 gLayout->addWidget( mSizeSpin, 1, 1, 1, 3 );
654
655 mLockButton = new QToolButton();
656 mLockButton->setIcon( QgsApplication::getThemeIcon( u"/cadtools/lock.svg"_s ) );
657 mLockButton->setCheckable( true );
658 mLockButton->setToolTip( tr( "Lock the elevation range to a fixed size" ) );
659
660 gLayout->addWidget( mLockButton, 1, 4 );
661
662 // the spin boxes take the extra width, not the labels or the buttons
663 gLayout->setColumnStretch( 1, 1 );
664 gLayout->setColumnStretch( 3, 1 );
665
666 const auto emitLimits = [this] { emit limitsChanged( QgsDoubleRange( mLowerSpin->value(), mUpperSpin->value() ) ); };
667 connect( mLowerSpin, qOverload<double>( &QgsDoubleSpinBox::valueChanged ), this, emitLimits );
668 connect( mUpperSpin, qOverload<double>( &QgsDoubleSpinBox::valueChanged ), this, emitLimits );
669
670 connect( mSizeSpin, qOverload<double>( &QgsDoubleSpinBox::valueChanged ), this, [this]( double size ) {
671 if ( mSizeSpin->isCleared() )
672 {
673 emit fixedRangeSizeChanged( -1 );
674 emit fullRangeRequested();
675 }
676 else if ( mLockButton->isChecked() )
677 {
678 emit fixedRangeSizeChanged( size );
679 }
680 } );
681 connect( mLockButton, &QToolButton::toggled, this, [this]( bool locked ) { emit fixedRangeSizeChanged( locked ? mSizeSpin->value() : -1 ); } );
682
683 QWidget *w = new QWidget();
684 w->setLayout( gLayout );
685
686 // watch the whole panel, so that hovering any of its widgets marks this action as the
687 // active one and key presses are never handed over to the menu
688 w->installEventFilter( this );
689 const QList<QWidget *> children = w->findChildren<QWidget *>();
690 for ( QWidget *child : children )
691 child->installEventFilter( this );
692
693 connect( this, &QAction::hovered, this, &QgsElevationControllerSettingsAction::onHover );
694
695 setDefaultWidget( w );
696}
697
698QMenu *QgsElevationControllerSettingsAction::limitsMenu()
699{
700 return mLimitsMenu;
701}
702
703void QgsElevationControllerSettingsAction::setLimits( const QgsDoubleRange &limits )
704{
705 const QSignalBlocker blockLower( mLowerSpin );
706 const QSignalBlocker blockUpper( mUpperSpin );
707 // the limits can always be widened, but never crossed over
708 mLowerSpin->setMaximum( limits.upper() );
709 mUpperSpin->setMinimum( limits.lower() );
710 mLowerSpin->setValue( limits.lower() );
711 mUpperSpin->setValue( limits.upper() );
712}
713
714void QgsElevationControllerSettingsAction::setFixedRangeSize( double size )
715{
716 {
717 const QSignalBlocker blockLock( mLockButton );
718 mLockButton->setChecked( size >= 0 );
719 }
720
721 if ( size >= 0 )
722 {
723 const QSignalBlocker blockSpin( mSizeSpin );
724 mSizeSpin->setValue( size );
725 }
726}
727
728void QgsElevationControllerSettingsAction::updateRangeSize( double size )
729{
730 // a locked size is the one the user asked for, it must not be overwritten
731 if ( mLockButton->isChecked() )
732 return;
733
734 const QSignalBlocker blockSpin( mSizeSpin );
735 mSizeSpin->setValue( size );
736}
737
738bool QgsElevationControllerSettingsAction::eventFilter( QObject *watched, QEvent *event )
739{
740 switch ( event->type() )
741 {
742 case QEvent::Enter:
743 onHover();
744 break;
745
746 case QEvent::KeyPress:
747 {
748 const QKeyEvent *keyEvent = static_cast<QKeyEvent *>( event );
749 if ( keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter )
750 {
751 // spin boxes leave the return key for their parents to handle, which would make the
752 // menu activate its highlighted action and close. Apply the typed value ourselves instead.
753 QgsDoubleSpinBox *spin = qobject_cast<QgsDoubleSpinBox *>( watched );
754 if ( !spin )
755 spin = qobject_cast<QgsDoubleSpinBox *>( watched->parent() );
756
757 if ( spin )
758 {
759 spin->interpretText();
760 if ( spin == mSizeSpin && !mSizeSpin->isCleared() )
761 mLockButton->setChecked( true );
762 return true;
763 }
764 }
765 break;
766 }
767
768 default:
769 break;
770 }
771 return QWidgetAction::eventFilter( watched, event );
772}
773
774void QgsElevationControllerSettingsAction::onHover()
775{
776 // see https://bugreports.qt.io/browse/QTBUG-10427
777 // the menu keeps highlighting the action the mouse last passed over, and would trigger it
778 // when the user hits enter while interacting with this widget
779 if ( mSuppressRecurse || !mMenu )
780 return;
781
782 mSuppressRecurse = true;
783 mMenu->setActiveAction( this );
784 mSuppressRecurse = false;
785}
786
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 setMapCanvas(QgsMapCanvas *canvas)
Sets the map canvas the widget takes its layers from.
QgsMapCanvas * mapCanvas() const
Returns the map canvas the widget takes its layers from.
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.
static QgsDoubleRange calculateZRangeForLayers(const QList< QgsMapLayer * > &layers)
Calculates the elevation range for the specified layers.
Map canvas is a class for displaying all GIS data types on a canvas.
virtual bool hasElevation() const
Returns true if the layer has an elevation or z component.
Base class for all map layer types.
Definition qgsmaplayer.h:83
virtual QgsMapLayerElevationProperties * elevationProperties()
Returns the layer's elevation properties.
static QgsDoubleRange roundedRange(const QgsDoubleRange &range)
Expands a range outward to round values, e.g.
static double roundingInterval(double span, int divisions=10)
Returns a round interval, a power of ten, which splits the given span into at least divisions parts.
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.
QMap< QString, QgsMapLayer * > mapLayers(const bool validOnly=false) const
Returns a map of all registered layers by layer ID.
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
int ANALYSIS_EXPORT lower(int n, int i)
Lower function.
double qgsRound(double number, int places)
Returns a double number, rounded (as close as possible) to the specified number of places.
Definition qgis.h:7651