Skip to content

Commit 7400106

Browse files
committed
[FEATURE] Evaluate expressions entered in QgsDoubleSpinBox
Allows entry of QGIS expressions into the spin box. The expression is evaluated on enter or loss of focus and then discarded. (refs #10544)
1 parent 8a182b7 commit 7400106

File tree

11 files changed

+304
-6
lines changed

11 files changed

+304
-6
lines changed

python/core/qgsexpression.sip

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,15 @@ class QgsExpression
110110
const QgsDistanceArea* distanceArea = 0
111111
);
112112

113+
/**Attempts to evaluate a text string as an expression to a resultant double
114+
* value.
115+
* @param text text to evaluate as expression
116+
* @param fallbackValue value to return if text can not be evaluated as a double
117+
* @returns evaluated double value, or fallback value
118+
* @note added in QGIS 2.7
119+
*/
120+
static double evaluateToDouble( const QString& text, const double fallbackValue );
121+
113122
enum UnaryOperator
114123
{
115124
uoNot,

python/gui/editorwidgets/qgsdoublespinbox.sip

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,41 @@ class QgsDoubleSpinBox : QDoubleSpinBox
1919
void setShowClearButton( const bool showClearButton );
2020
bool showClearButton() const;
2121

22+
/**Sets if the widget will allow entry of simple expressions, which are
23+
* evaluated and then discarded.
24+
* @param enabled set to true to allow expression entry
25+
* @note added in QGIS 2.7
26+
*/
27+
void setExpressionsEnabled( const bool enabled );
28+
/**Returns whether the widget will allow entry of simple expressions, which are
29+
* evaluated and then discarded.
30+
* @returns true if spin box allows expression entry
31+
* @note added in QGIS 2.7
32+
*/
33+
bool expressionsEnabled() const;
34+
2235
//! Set the current value to the value defined by the clear value.
2336
virtual void clear();
2437

2538
/**
2639
* @brief setClearValue defines the clear value as a custom value and will automatically set the clear value mode to CustomValue
27-
* @param defines the numerical value used as the clear value
40+
* @param customValue defines the numerical value used as the clear value
2841
* @param clearValueText is the text displayed when the spin box is at the clear value. If not specified, no special value text is used.
2942
*/
3043
void setClearValue( double customValue, QString clearValueText = QString() );
3144
/**
3245
* @brief setClearValueMode defines if the clear value should be the minimum or maximum values of the widget or a custom value
46+
* @param mode mode to user for clear value
3347
* @param clearValueText is the text displayed when the spin box is at the clear value. If not specified, no special value text is used.
3448
*/
3549
void setClearValueMode( ClearValueMode mode, QString clearValueText = QString() );
3650

3751
//! returns the value used when clear() is called.
3852
double clearValue() const;
3953

54+
virtual double valueFromText( const QString & text ) const;
55+
virtual QValidator::State validate( QString & input, int & pos ) const;
56+
4057
protected:
4158
virtual void resizeEvent( QResizeEvent* event );
4259
virtual void changeEvent( QEvent* event );

python/gui/editorwidgets/qgsspinbox.sip

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ class QgsSpinBox : QSpinBox
2323
virtual void clear();
2424

2525
/**
26-
* @brief setClearValue defines the clear value as a custom value and will automatically set the clear value mode to CustomValue
27-
* @param defines the numerical value used as the clear value
26+
* @brief setClearValue defines the clear value for the widget and will automatically set the clear value mode to CustomValue
27+
* @param customValue defines the numerical value used as the clear value
2828
* @param clearValueText is the text displayed when the spin box is at the clear value. If not specified, no special value text is used.
2929
*/
3030
void setClearValue( int customValue, QString clearValueText = QString() );
3131
/**
3232
* @brief setClearValueMode defines if the clear value should be the minimum or maximum values of the widget or a custom value
33+
* @param mode mode to user for clear value
3334
* @param clearValueText is the text displayed when the spin box is at the clear value. If not specified, no special value text is used.
3435
*/
3536
void setClearValueMode( ClearValueMode mode, QString clearValueText = QString() );

src/core/qgsexpression.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2074,6 +2074,27 @@ QString QgsExpression::replaceExpressionText( const QString &action, const QgsFe
20742074
return expr_action;
20752075
}
20762076

2077+
double QgsExpression::evaluateToDouble( const QString &text, const double fallbackValue )
2078+
{
2079+
bool ok;
2080+
//first test if text is directly convertible to double
2081+
double convertedValue = text.toDouble( &ok );
2082+
if ( ok )
2083+
{
2084+
return convertedValue;
2085+
}
2086+
2087+
//otherwise try to evalute as expression
2088+
QgsExpression expr( text );
2089+
QVariant result = expr.evaluate();
2090+
convertedValue = result.toDouble( &ok );
2091+
if ( expr.hasEvalError() || !ok )
2092+
{
2093+
return fallbackValue;
2094+
}
2095+
return convertedValue;
2096+
}
2097+
20772098

20782099
///////////////////////////////////////////////
20792100
// nodes

src/core/qgsexpression.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,16 @@ class CORE_EXPORT QgsExpression
200200
const QMap<QString, QVariant> *substitutionMap = 0,
201201
const QgsDistanceArea* distanceArea = 0
202202
);
203+
204+
/**Attempts to evaluate a text string as an expression to a resultant double
205+
* value.
206+
* @param text text to evaluate as expression
207+
* @param fallbackValue value to return if text can not be evaluated as a double
208+
* @returns evaluated double value, or fallback value
209+
* @note added in QGIS 2.7
210+
*/
211+
static double evaluateToDouble( const QString& text, const double fallbackValue );
212+
203213
enum UnaryOperator
204214
{
205215
uoNot,

src/gui/editorwidgets/qgsdoublespinbox.cpp

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
#include <QToolButton>
2121

2222
#include "qgsdoublespinbox.h"
23-
23+
#include "qgsexpression.h"
2424
#include "qgsapplication.h"
2525
#include "qgslogger.h"
2626

@@ -29,6 +29,7 @@ QgsDoubleSpinBox::QgsDoubleSpinBox( QWidget *parent )
2929
, mShowClearButton( true )
3030
, mClearValueMode( MinimumValue )
3131
, mCustomClearValue( 0.0 )
32+
, mExpressionsEnabled( true )
3233
{
3334
mClearButton = new QToolButton( this );
3435
mClearButton->setIcon( QgsApplication::getThemeIcon( "/mIconClear.svg" ) );
@@ -51,6 +52,11 @@ void QgsDoubleSpinBox::setShowClearButton( const bool showClearButton )
5152
mClearButton->setVisible( shouldShowClearForValue( value() ) );
5253
}
5354

55+
void QgsDoubleSpinBox::setExpressionsEnabled( const bool enabled )
56+
{
57+
mExpressionsEnabled = enabled;
58+
}
59+
5460
void QgsDoubleSpinBox::changeEvent( QEvent *event )
5561
{
5662
QDoubleSpinBox::changeEvent( event );
@@ -105,6 +111,63 @@ double QgsDoubleSpinBox::clearValue() const
105111
return mCustomClearValue;
106112
}
107113

114+
QString QgsDoubleSpinBox::stripped( const QString &originalText ) const
115+
{
116+
//adapted from QAbstractSpinBoxPrivate::stripped
117+
//trims whitespace, prefix and suffix from spin box text
118+
QString text = originalText;
119+
if ( specialValueText().size() == 0 || text != specialValueText() )
120+
{
121+
int from = 0;
122+
int size = text.size();
123+
bool changed = false;
124+
if ( prefix().size() && text.startsWith( prefix() ) )
125+
{
126+
from += prefix().size();
127+
size -= from;
128+
changed = true;
129+
}
130+
if ( suffix().size() && text.endsWith( suffix() ) )
131+
{
132+
size -= suffix().size();
133+
changed = true;
134+
}
135+
if ( changed )
136+
text = text.mid( from, size );
137+
}
138+
139+
text = text.trimmed();
140+
141+
return text;
142+
}
143+
144+
double QgsDoubleSpinBox::valueFromText( const QString &text ) const
145+
{
146+
if ( !mExpressionsEnabled )
147+
{
148+
return QDoubleSpinBox::valueFromText( text );
149+
}
150+
151+
QString trimmedText = stripped( text );
152+
if ( trimmedText.isEmpty() )
153+
{
154+
return clearValue();
155+
}
156+
157+
return QgsExpression::evaluateToDouble( trimmedText, value() );
158+
}
159+
160+
QValidator::State QgsDoubleSpinBox::validate( QString &input, int &pos ) const
161+
{
162+
if ( !mExpressionsEnabled )
163+
{
164+
QValidator::State r = QDoubleSpinBox::validate( input, pos );
165+
return r;
166+
}
167+
168+
return QValidator::Acceptable;
169+
}
170+
108171
int QgsDoubleSpinBox::frameWidth() const
109172
{
110173
return style()->pixelMetric( QStyle::PM_DefaultFrameWidth );

src/gui/editorwidgets/qgsdoublespinbox.h

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class GUI_EXPORT QgsDoubleSpinBox : public QDoubleSpinBox
2828
{
2929
Q_OBJECT
3030
Q_PROPERTY( bool showClearButton READ showClearButton WRITE setShowClearButton )
31+
Q_PROPERTY( bool expressionsEnabled READ expressionsEnabled WRITE setExpressionsEnabled )
3132

3233
public:
3334
enum ClearValueMode
@@ -43,24 +44,41 @@ class GUI_EXPORT QgsDoubleSpinBox : public QDoubleSpinBox
4344
void setShowClearButton( const bool showClearButton );
4445
bool showClearButton() const {return mShowClearButton;}
4546

47+
/**Sets if the widget will allow entry of simple expressions, which are
48+
* evaluated and then discarded.
49+
* @param enabled set to true to allow expression entry
50+
* @note added in QGIS 2.7
51+
*/
52+
void setExpressionsEnabled( const bool enabled );
53+
/**Returns whether the widget will allow entry of simple expressions, which are
54+
* evaluated and then discarded.
55+
* @returns true if spin box allows expression entry
56+
* @note added in QGIS 2.7
57+
*/
58+
bool expressionsEnabled() const {return mExpressionsEnabled;}
59+
4660
//! Set the current value to the value defined by the clear value.
4761
virtual void clear();
4862

4963
/**
5064
* @brief setClearValue defines the clear value as a custom value and will automatically set the clear value mode to CustomValue
51-
* @param defines the numerical value used as the clear value
65+
* @param customValue defines the numerical value used as the clear value
5266
* @param clearValueText is the text displayed when the spin box is at the clear value. If not specified, no special value text is used.
5367
*/
5468
void setClearValue( double customValue, QString clearValueText = QString() );
5569
/**
5670
* @brief setClearValueMode defines if the clear value should be the minimum or maximum values of the widget or a custom value
71+
* @param mode mode to user for clear value
5772
* @param clearValueText is the text displayed when the spin box is at the clear value. If not specified, no special value text is used.
5873
*/
5974
void setClearValueMode( ClearValueMode mode, QString clearValueText = QString() );
6075

6176
//! returns the value used when clear() is called.
6277
double clearValue() const;
6378

79+
virtual double valueFromText( const QString & text ) const;
80+
virtual QValidator::State validate( QString & input, int & pos ) const;
81+
6482
protected:
6583
virtual void resizeEvent( QResizeEvent* event );
6684
virtual void changeEvent( QEvent* event );
@@ -76,7 +94,10 @@ class GUI_EXPORT QgsDoubleSpinBox : public QDoubleSpinBox
7694
ClearValueMode mClearValueMode;
7795
double mCustomClearValue;
7896

97+
bool mExpressionsEnabled;
98+
7999
QToolButton* mClearButton;
100+
QString stripped( const QString &originalText ) const;
80101
};
81102

82103
#endif // QGSDOUBLESPPINBOX_H

src/gui/editorwidgets/qgsspinbox.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,13 @@ class GUI_EXPORT QgsSpinBox : public QSpinBox
4848

4949
/**
5050
* @brief setClearValue defines the clear value for the widget and will automatically set the clear value mode to CustomValue
51-
* @param defines the numerical value used as the clear value
51+
* @param customValue defines the numerical value used as the clear value
5252
* @param clearValueText is the text displayed when the spin box is at the clear value. If not specified, no special value text is used.
5353
*/
5454
void setClearValue( int customValue, QString clearValueText = QString() );
5555
/**
5656
* @brief setClearValueMode defines if the clear value should be the minimum or maximum values of the widget or a custom value
57+
* @param mode mode to user for clear value
5758
* @param clearValueText is the text displayed when the spin box is at the clear value. If not specified, no special value text is used.
5859
*/
5960
void setClearValueMode( ClearValueMode mode, QString clearValueText = QString() );

tests/src/core/testqgsexpression.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ class TestQgsExpression: public QObject
285285
QTest::newRow( "max(1,3.5,-2.1)" ) << "max(1,3.5,-2.1)" << false << QVariant( 3.5 );
286286
QTest::newRow( "min(-1.5)" ) << "min(-1.5)" << false << QVariant( -1.5 );
287287
QTest::newRow( "min(-16.6,3.5,-2.1)" ) << "min(-16.6,3.5,-2.1)" << false << QVariant( -16.6 );
288+
QTest::newRow( "min(5,3.5,-2.1)" ) << "min(5,3.5,-2.1)" << false << QVariant( -2.1 );
288289
QTest::newRow( "clamp(-2,1,5)" ) << "clamp(-2,1,5)" << false << QVariant( 1.0 );
289290
QTest::newRow( "clamp(-2,-10,5)" ) << "clamp(-2,-10,5)" << false << QVariant( -2.0 );
290291
QTest::newRow( "clamp(-2,100,5)" ) << "clamp(-2,100,5)" << false << QVariant( 5.0 );
@@ -327,6 +328,7 @@ class TestQgsExpression: public QObject
327328
QTest::newRow( "substr" ) << "substr('HeLLo', 3,2)" << false << QVariant( "LL" );
328329
QTest::newRow( "substr outside" ) << "substr('HeLLo', -5,2)" << false << QVariant( "" );
329330
QTest::newRow( "regexp_substr" ) << "regexp_substr('abc123','(\\\\d+)')" << false << QVariant( "123" );
331+
QTest::newRow( "regexp_substr no hit" ) << "regexp_substr('abcdef','(\\\\d+)')" << false << QVariant( "" );
330332
QTest::newRow( "regexp_substr invalid" ) << "regexp_substr('abc123','([[[')" << true << QVariant();
331333
QTest::newRow( "strpos" ) << "strpos('Hello World','World')" << false << QVariant( 6 );
332334
QTest::newRow( "strpos outside" ) << "strpos('Hello World','blah')" << false << QVariant( -1 );
@@ -345,6 +347,8 @@ class TestQgsExpression: public QObject
345347
QTest::newRow( "wordwrap" ) << "wordwrap('university of qgis',-3,' ')" << false << QVariant( "university\nof qgis" );
346348
QTest::newRow( "wordwrap" ) << "wordwrap('university of qgis\nsupports many multiline',-5,' ')" << false << QVariant( "university\nof qgis\nsupports\nmany multiline" );
347349
QTest::newRow( "format" ) << "format('%1 %2 %3 %1', 'One', 'Two', 'Three')" << false << QVariant( "One Two Three One" );
350+
QTest::newRow( "concat" ) << "concat('a', 'b', 'c', 'd')" << false << QVariant( "abcd" );
351+
QTest::newRow( "concat single" ) << "concat('a')" << false << QVariant( "a" );
348352

349353
// implicit conversions
350354
QTest::newRow( "implicit int->text" ) << "length(123)" << false << QVariant( 3 );
@@ -977,6 +981,15 @@ class TestQgsExpression: public QObject
977981
lst << i;
978982
QtConcurrent::blockingMap( lst, _parseAndEvalExpr );
979983
}
984+
985+
void evaluateToDouble()
986+
{
987+
QCOMPARE( QgsExpression::evaluateToDouble( QString( "5" ), 0.0 ), 5.0 );
988+
QCOMPARE( QgsExpression::evaluateToDouble( QString( "5+6" ), 0.0 ), 11.0 );
989+
QCOMPARE( QgsExpression::evaluateToDouble( QString( "5*" ), 7.0 ), 7.0 );
990+
QCOMPARE( QgsExpression::evaluateToDouble( QString( "a" ), 9.0 ), 9.0 );
991+
QCOMPARE( QgsExpression::evaluateToDouble( QString(), 9.0 ), 9.0 );
992+
}
980993
};
981994

982995
QTEST_MAIN( TestQgsExpression )

tests/src/gui/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ ADD_QGIS_TEST(zoomtest testqgsmaptoolzoom.cpp)
119119
ADD_QGIS_TEST(projectionissues testprojectionissues.cpp)
120120
ADD_QGIS_TEST(scalecombobox testqgsscalecombobox.cpp)
121121
ADD_QGIS_TEST(dualviewtest testqgsdualview.cpp )
122+
ADD_QGIS_TEST(doublespinbox testqgsdoublespinbox.cpp)
122123
ADD_QGIS_TEST(rubberbandtest testqgsrubberband.cpp )
123124
ADD_QGIS_TEST(mapcanvastest testqgsmapcanvas.cpp )
124125

0 commit comments

Comments
 (0)