Add a fixed range width option to QgsRangeSlider

Allows forcing the widget to have a specific fixed range width,
so that interactions with the lower or upper slider automatically
force the other slider to move to keep a constant width
This commit is contained in:
Nyall Dawson 2024-03-14 14:21:47 +10:00
parent a5b4d9743e
commit a91f5bf7c2
5 changed files with 276 additions and 4 deletions

View File

@ -168,6 +168,32 @@ This corresponds to the larger increment or decrement applied when the user pres
.. seealso:: :py:func:`setPageStep`
.. seealso:: :py:func:`singleStep`
%End
int fixedRangeWidth() const;
%Docstring
Returns the slider's fixed range width, or -1 if not set.
If a fixed range width is set then moving either the lower or upper slider will automatically
move the other slider accordingly, in order to keep the selected range at the specified
fixed width.
.. seealso:: :py:func:`setFixedRangeWidth`
.. versionadded:: 3.38
%End
void setFixedRangeWidth( int width );
%Docstring
Sets the slider's fixed range ``width``. Set to -1 if no fixed width is desired.
If a fixed range width is set then moving either the lower or upper slider will automatically
move the other slider accordingly, in order to keep the selected range at the specified
fixed width.
.. seealso:: :py:func:`fixedRangeWidth`
.. versionadded:: 3.38
%End
public slots:
@ -265,6 +291,17 @@ Emitted when the range selected in the widget is changed.
void rangeLimitsChanged( int minimum, int maximum );
%Docstring
Emitted when the limits of values allowed in the widget is changed.
%End
void fixedRangeWidthChanged( int width );
%Docstring
Emitted when the widget's fixed range width is changed.
.. seealso:: :py:func:`fixedRangeWidth`
.. seealso:: :py:func:`setFixedRangeWidth`
.. versionadded:: 3.38
%End
};

View File

@ -168,6 +168,32 @@ This corresponds to the larger increment or decrement applied when the user pres
.. seealso:: :py:func:`setPageStep`
.. seealso:: :py:func:`singleStep`
%End
int fixedRangeWidth() const;
%Docstring
Returns the slider's fixed range width, or -1 if not set.
If a fixed range width is set then moving either the lower or upper slider will automatically
move the other slider accordingly, in order to keep the selected range at the specified
fixed width.
.. seealso:: :py:func:`setFixedRangeWidth`
.. versionadded:: 3.38
%End
void setFixedRangeWidth( int width );
%Docstring
Sets the slider's fixed range ``width``. Set to -1 if no fixed width is desired.
If a fixed range width is set then moving either the lower or upper slider will automatically
move the other slider accordingly, in order to keep the selected range at the specified
fixed width.
.. seealso:: :py:func:`fixedRangeWidth`
.. versionadded:: 3.38
%End
public slots:
@ -265,6 +291,17 @@ Emitted when the range selected in the widget is changed.
void rangeLimitsChanged( int minimum, int maximum );
%Docstring
Emitted when the limits of values allowed in the widget is changed.
%End
void fixedRangeWidthChanged( int width );
%Docstring
Emitted when the widget's fixed range width is changed.
.. seealso:: :py:func:`fixedRangeWidth`
.. seealso:: :py:func:`setFixedRangeWidth`
.. versionadded:: 3.38
%End
};

View File

@ -120,7 +120,15 @@ void QgsRangeSlider::setLowerValue( int lowerValue )
return;
mLowerValue = std::min( mStyleOption.maximum, std::max( mStyleOption.minimum, lowerValue ) );
mUpperValue = std::max( mLowerValue, mUpperValue );
if ( mFixedRangeWidth >= 0 )
{
mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum );
mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth );
}
else
{
mUpperValue = std::max( mLowerValue, mUpperValue );
}
emit rangeChanged( mLowerValue, mUpperValue );
update();
}
@ -137,7 +145,16 @@ void QgsRangeSlider::setUpperValue( int upperValue )
return;
mUpperValue = std::max( mStyleOption.minimum, std::min( mStyleOption.maximum, upperValue ) );
mLowerValue = std::min( mLowerValue, mUpperValue );
if ( mFixedRangeWidth >= 0 )
{
mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth );
mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum );
}
else
{
mLowerValue = std::min( mLowerValue, mUpperValue );
}
emit rangeChanged( mLowerValue, mUpperValue );
update();
}
@ -152,6 +169,15 @@ void QgsRangeSlider::setRange( int lower, int upper )
mLowerValue = std::min( mStyleOption.maximum, std::max( mStyleOption.minimum, lower ) );
mUpperValue = std::min( mStyleOption.maximum, std::max( mStyleOption.minimum, upper ) );
if ( mFixedRangeWidth >= 0 )
{
mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum );
mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth );
}
else
{
mUpperValue = std::min( mStyleOption.maximum, std::max( mStyleOption.minimum, upper ) );
}
emit rangeChanged( mLowerValue, mUpperValue );
update();
}
@ -317,6 +343,24 @@ QRect QgsRangeSlider::selectedRangeRect()
return selectionRect.adjusted( -1, 1, 1, -1 );
}
int QgsRangeSlider::fixedRangeWidth() const
{
return mFixedRangeWidth;
}
void QgsRangeSlider::setFixedRangeWidth( int width )
{
if ( width == mFixedRangeWidth )
return;
mFixedRangeWidth = width;
if ( mFixedRangeWidth >= 0 )
setUpperValue( mLowerValue + mFixedRangeWidth );
emit fixedRangeWidthChanged( mFixedRangeWidth );
}
void QgsRangeSlider::applyStep( int step )
{
switch ( mFocusControl )
@ -327,6 +371,11 @@ void QgsRangeSlider::applyStep( int step )
if ( newLowerValue != mLowerValue )
{
mLowerValue = newLowerValue;
if ( mFixedRangeWidth >= 0 )
{
mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum );
mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth );
}
emit rangeChanged( mLowerValue, mUpperValue );
update();
}
@ -339,6 +388,11 @@ void QgsRangeSlider::applyStep( int step )
if ( newUpperValue != mUpperValue )
{
mUpperValue = newUpperValue;
if ( mFixedRangeWidth >= 0 )
{
mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth );
mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum );
}
emit rangeChanged( mLowerValue, mUpperValue );
update();
}
@ -354,7 +408,15 @@ void QgsRangeSlider::applyStep( int step )
if ( newLowerValue != mLowerValue )
{
mLowerValue = newLowerValue;
mUpperValue = std::min( mStyleOption.maximum, mLowerValue + previousWidth );
if ( mFixedRangeWidth >= 0 )
{
mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum );
mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth );
}
else
{
mUpperValue = std::min( mStyleOption.maximum, mLowerValue + previousWidth );
}
emit rangeChanged( mLowerValue, mUpperValue );
update();
}
@ -366,7 +428,15 @@ void QgsRangeSlider::applyStep( int step )
if ( newUpperValue != mUpperValue )
{
mUpperValue = newUpperValue;
mLowerValue = std::max( mStyleOption.minimum, mUpperValue - previousWidth );
if ( mFixedRangeWidth >= 0 )
{
mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth );
mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum );
}
else
{
mLowerValue = std::max( mStyleOption.minimum, mUpperValue - previousWidth );
}
emit rangeChanged( mLowerValue, mUpperValue );
update();
}
@ -604,6 +674,12 @@ void QgsRangeSlider::mouseMoveEvent( QMouseEvent *event )
{
changed = true;
mUpperValue = mPreDragUpperValue;
if ( mFixedRangeWidth >= 0 )
{
// don't permit fixed width drags if it pushes the other value out of range
mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth );
mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum );
}
}
}
else if ( newPosition > mStartDragPos )
@ -614,6 +690,12 @@ void QgsRangeSlider::mouseMoveEvent( QMouseEvent *event )
{
changed = true;
mLowerValue = mPreDragLowerValue;
if ( mFixedRangeWidth >= 0 )
{
// don't permit fixed width drags if it pushes the other value out of range
mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum );
mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth );
}
}
}
else
@ -623,11 +705,23 @@ void QgsRangeSlider::mouseMoveEvent( QMouseEvent *event )
{
changed = true;
mUpperValue = mPreDragUpperValue;
if ( mFixedRangeWidth >= 0 )
{
// don't permit fixed width drags if it pushes the other value out of range
mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth );
mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum );
}
}
if ( mLowerValue != mPreDragLowerValue )
{
changed = true;
mLowerValue = mPreDragLowerValue;
if ( mFixedRangeWidth >= 0 )
{
// don't permit fixed width drags if it pushes the other value out of range
mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum );
mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth );
}
}
}
}
@ -645,6 +739,13 @@ void QgsRangeSlider::mouseMoveEvent( QMouseEvent *event )
if ( mLowerValue != newPosition )
{
mLowerValue = newPosition;
if ( mFixedRangeWidth >= 0 )
{
// don't permit fixed width drags if it pushes the other value out of range
mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum );
mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth );
}
changed = true;
}
break;
@ -657,6 +758,13 @@ void QgsRangeSlider::mouseMoveEvent( QMouseEvent *event )
if ( mUpperValue != newPosition )
{
mUpperValue = newPosition;
if ( mFixedRangeWidth >= 0 )
{
// don't permit fixed width drags if it pushes the other value out of range
mLowerValue = std::max( mStyleOption.minimum, mUpperValue - mFixedRangeWidth );
mUpperValue = std::min( mLowerValue + mFixedRangeWidth, mStyleOption.maximum );
}
changed = true;
}
break;

View File

@ -169,6 +169,30 @@ class GUI_EXPORT QgsRangeSlider : public QWidget
*/
int pageStep() const;
/**
* Returns the slider's fixed range width, or -1 if not set.
*
* If a fixed range width is set then moving either the lower or upper slider will automatically
* move the other slider accordingly, in order to keep the selected range at the specified
* fixed width.
*
* \see setFixedRangeWidth()
* \since QGIS 3.38
*/
int fixedRangeWidth() const;
/**
* Sets the slider's fixed range \a width. Set to -1 if no fixed width is desired.
*
* If a fixed range width is set then moving either the lower or upper slider will automatically
* move the other slider accordingly, in order to keep the selected range at the specified
* fixed width.
*
* \see fixedRangeWidth()
* \since QGIS 3.38
*/
void setFixedRangeWidth( int width );
public slots:
/**
@ -255,6 +279,16 @@ class GUI_EXPORT QgsRangeSlider : public QWidget
*/
void rangeLimitsChanged( int minimum, int maximum );
/**
* Emitted when the widget's fixed range width is changed.
*
* \see fixedRangeWidth()
* \see setFixedRangeWidth()
*
* \since QGIS 3.38
*/
void fixedRangeWidthChanged( int width );
private:
int pick( const QPoint &pt ) const;
@ -270,6 +304,8 @@ class GUI_EXPORT QgsRangeSlider : public QWidget
int mSingleStep = 1;
int mPageStep = 10;
int mFixedRangeWidth = -1;
QStyleOptionSlider mStyleOption;
enum Control
{

View File

@ -41,6 +41,10 @@ class TestQgsRangeSlider(QgisTestCase):
w.setPageStep(5)
self.assertEqual(w.pageStep(), 5)
self.assertEqual(w.fixedRangeWidth(), -1)
w.setFixedRangeWidth(5)
self.assertEqual(w.fixedRangeWidth(), 5)
def testLimits(self):
w = QgsRangeSlider()
spy = QSignalSpy(w.rangeLimitsChanged)
@ -268,6 +272,56 @@ class TestQgsRangeSlider(QgisTestCase):
self.assertEqual(len(spy), 6)
self.assertEqual(spy[-1], [3, 7])
def test_fixed_range_width(self):
"""
Test interactions with fixed range widths
"""
w = QgsRangeSlider()
w.setRangeLimits(0, 100)
w.setFixedRangeWidth(10)
self.assertEqual(w.upperValue() - w.lowerValue(), 10)
w.setUpperValue(70)
self.assertEqual(w.upperValue(), 70)
self.assertEqual(w.lowerValue(), 60)
w.setLowerValue(5)
self.assertEqual(w.upperValue(), 15)
self.assertEqual(w.lowerValue(), 5)
# try to force value outside range
w.setUpperValue(5)
self.assertEqual(w.upperValue(), 10)
self.assertEqual(w.lowerValue(), 0)
w.setLowerValue(95)
self.assertEqual(w.upperValue(), 100)
self.assertEqual(w.lowerValue(), 90)
w.setRange(0, 5)
self.assertEqual(w.upperValue(), 10)
self.assertEqual(w.lowerValue(), 0)
w.setRange(95, 100)
self.assertEqual(w.upperValue(), 100)
self.assertEqual(w.lowerValue(), 90)
# with zero width fixed range
w.setFixedRangeWidth(0)
self.assertEqual(w.upperValue() - w.lowerValue(), 0)
w.setUpperValue(70)
self.assertEqual(w.upperValue(), 70)
self.assertEqual(w.lowerValue(), 70)
w.setLowerValue(5)
self.assertEqual(w.upperValue(), 5)
self.assertEqual(w.lowerValue(), 5)
w.setRange(0, 5)
self.assertEqual(w.upperValue(), 0)
self.assertEqual(w.lowerValue(), 0)
if __name__ == '__main__':
unittest.main()