[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)
This commit is contained in:
Nyall Dawson 2014-12-04 23:07:27 +11:00
parent 8a182b7f34
commit 7400106e41
11 changed files with 304 additions and 6 deletions

View File

@ -110,6 +110,15 @@ class QgsExpression
const QgsDistanceArea* distanceArea = 0
);
/**Attempts to evaluate a text string as an expression to a resultant double
* value.
* @param text text to evaluate as expression
* @param fallbackValue value to return if text can not be evaluated as a double
* @returns evaluated double value, or fallback value
* @note added in QGIS 2.7
*/
static double evaluateToDouble( const QString& text, const double fallbackValue );
enum UnaryOperator
{
uoNot,

View File

@ -19,17 +19,31 @@ class QgsDoubleSpinBox : QDoubleSpinBox
void setShowClearButton( const bool showClearButton );
bool showClearButton() const;
/**Sets if the widget will allow entry of simple expressions, which are
* evaluated and then discarded.
* @param enabled set to true to allow expression entry
* @note added in QGIS 2.7
*/
void setExpressionsEnabled( const bool enabled );
/**Returns whether the widget will allow entry of simple expressions, which are
* evaluated and then discarded.
* @returns true if spin box allows expression entry
* @note added in QGIS 2.7
*/
bool expressionsEnabled() const;
//! Set the current value to the value defined by the clear value.
virtual void clear();
/**
* @brief setClearValue defines the clear value as a custom value and will automatically set the clear value mode to CustomValue
* @param defines the numerical value used as the clear value
* @param customValue defines the numerical value used as the clear value
* @param clearValueText is the text displayed when the spin box is at the clear value. If not specified, no special value text is used.
*/
void setClearValue( double customValue, QString clearValueText = QString() );
/**
* @brief setClearValueMode defines if the clear value should be the minimum or maximum values of the widget or a custom value
* @param mode mode to user for clear value
* @param clearValueText is the text displayed when the spin box is at the clear value. If not specified, no special value text is used.
*/
void setClearValueMode( ClearValueMode mode, QString clearValueText = QString() );
@ -37,6 +51,9 @@ class QgsDoubleSpinBox : QDoubleSpinBox
//! returns the value used when clear() is called.
double clearValue() const;
virtual double valueFromText( const QString & text ) const;
virtual QValidator::State validate( QString & input, int & pos ) const;
protected:
virtual void resizeEvent( QResizeEvent* event );
virtual void changeEvent( QEvent* event );

View File

@ -23,13 +23,14 @@ class QgsSpinBox : QSpinBox
virtual void clear();
/**
* @brief setClearValue defines the clear value as a custom value and will automatically set the clear value mode to CustomValue
* @param defines the numerical value used as the clear value
* @brief setClearValue defines the clear value for the widget and will automatically set the clear value mode to CustomValue
* @param customValue defines the numerical value used as the clear value
* @param clearValueText is the text displayed when the spin box is at the clear value. If not specified, no special value text is used.
*/
void setClearValue( int customValue, QString clearValueText = QString() );
/**
* @brief setClearValueMode defines if the clear value should be the minimum or maximum values of the widget or a custom value
* @param mode mode to user for clear value
* @param clearValueText is the text displayed when the spin box is at the clear value. If not specified, no special value text is used.
*/
void setClearValueMode( ClearValueMode mode, QString clearValueText = QString() );

View File

@ -2074,6 +2074,27 @@ QString QgsExpression::replaceExpressionText( const QString &action, const QgsFe
return expr_action;
}
double QgsExpression::evaluateToDouble( const QString &text, const double fallbackValue )
{
bool ok;
//first test if text is directly convertible to double
double convertedValue = text.toDouble( &ok );
if ( ok )
{
return convertedValue;
}
//otherwise try to evalute as expression
QgsExpression expr( text );
QVariant result = expr.evaluate();
convertedValue = result.toDouble( &ok );
if ( expr.hasEvalError() || !ok )
{
return fallbackValue;
}
return convertedValue;
}
///////////////////////////////////////////////
// nodes

View File

@ -200,6 +200,16 @@ class CORE_EXPORT QgsExpression
const QMap<QString, QVariant> *substitutionMap = 0,
const QgsDistanceArea* distanceArea = 0
);
/**Attempts to evaluate a text string as an expression to a resultant double
* value.
* @param text text to evaluate as expression
* @param fallbackValue value to return if text can not be evaluated as a double
* @returns evaluated double value, or fallback value
* @note added in QGIS 2.7
*/
static double evaluateToDouble( const QString& text, const double fallbackValue );
enum UnaryOperator
{
uoNot,

View File

@ -20,7 +20,7 @@
#include <QToolButton>
#include "qgsdoublespinbox.h"
#include "qgsexpression.h"
#include "qgsapplication.h"
#include "qgslogger.h"
@ -29,6 +29,7 @@ QgsDoubleSpinBox::QgsDoubleSpinBox( QWidget *parent )
, mShowClearButton( true )
, mClearValueMode( MinimumValue )
, mCustomClearValue( 0.0 )
, mExpressionsEnabled( true )
{
mClearButton = new QToolButton( this );
mClearButton->setIcon( QgsApplication::getThemeIcon( "/mIconClear.svg" ) );
@ -51,6 +52,11 @@ void QgsDoubleSpinBox::setShowClearButton( const bool showClearButton )
mClearButton->setVisible( shouldShowClearForValue( value() ) );
}
void QgsDoubleSpinBox::setExpressionsEnabled( const bool enabled )
{
mExpressionsEnabled = enabled;
}
void QgsDoubleSpinBox::changeEvent( QEvent *event )
{
QDoubleSpinBox::changeEvent( event );
@ -105,6 +111,63 @@ double QgsDoubleSpinBox::clearValue() const
return mCustomClearValue;
}
QString QgsDoubleSpinBox::stripped( const QString &originalText ) const
{
//adapted from QAbstractSpinBoxPrivate::stripped
//trims whitespace, prefix and suffix from spin box text
QString text = originalText;
if ( specialValueText().size() == 0 || text != specialValueText() )
{
int from = 0;
int size = text.size();
bool changed = false;
if ( prefix().size() && text.startsWith( prefix() ) )
{
from += prefix().size();
size -= from;
changed = true;
}
if ( suffix().size() && text.endsWith( suffix() ) )
{
size -= suffix().size();
changed = true;
}
if ( changed )
text = text.mid( from, size );
}
text = text.trimmed();
return text;
}
double QgsDoubleSpinBox::valueFromText( const QString &text ) const
{
if ( !mExpressionsEnabled )
{
return QDoubleSpinBox::valueFromText( text );
}
QString trimmedText = stripped( text );
if ( trimmedText.isEmpty() )
{
return clearValue();
}
return QgsExpression::evaluateToDouble( trimmedText, value() );
}
QValidator::State QgsDoubleSpinBox::validate( QString &input, int &pos ) const
{
if ( !mExpressionsEnabled )
{
QValidator::State r = QDoubleSpinBox::validate( input, pos );
return r;
}
return QValidator::Acceptable;
}
int QgsDoubleSpinBox::frameWidth() const
{
return style()->pixelMetric( QStyle::PM_DefaultFrameWidth );

View File

@ -28,6 +28,7 @@ class GUI_EXPORT QgsDoubleSpinBox : public QDoubleSpinBox
{
Q_OBJECT
Q_PROPERTY( bool showClearButton READ showClearButton WRITE setShowClearButton )
Q_PROPERTY( bool expressionsEnabled READ expressionsEnabled WRITE setExpressionsEnabled )
public:
enum ClearValueMode
@ -43,17 +44,31 @@ class GUI_EXPORT QgsDoubleSpinBox : public QDoubleSpinBox
void setShowClearButton( const bool showClearButton );
bool showClearButton() const {return mShowClearButton;}
/**Sets if the widget will allow entry of simple expressions, which are
* evaluated and then discarded.
* @param enabled set to true to allow expression entry
* @note added in QGIS 2.7
*/
void setExpressionsEnabled( const bool enabled );
/**Returns whether the widget will allow entry of simple expressions, which are
* evaluated and then discarded.
* @returns true if spin box allows expression entry
* @note added in QGIS 2.7
*/
bool expressionsEnabled() const {return mExpressionsEnabled;}
//! Set the current value to the value defined by the clear value.
virtual void clear();
/**
* @brief setClearValue defines the clear value as a custom value and will automatically set the clear value mode to CustomValue
* @param defines the numerical value used as the clear value
* @param customValue defines the numerical value used as the clear value
* @param clearValueText is the text displayed when the spin box is at the clear value. If not specified, no special value text is used.
*/
void setClearValue( double customValue, QString clearValueText = QString() );
/**
* @brief setClearValueMode defines if the clear value should be the minimum or maximum values of the widget or a custom value
* @param mode mode to user for clear value
* @param clearValueText is the text displayed when the spin box is at the clear value. If not specified, no special value text is used.
*/
void setClearValueMode( ClearValueMode mode, QString clearValueText = QString() );
@ -61,6 +76,9 @@ class GUI_EXPORT QgsDoubleSpinBox : public QDoubleSpinBox
//! returns the value used when clear() is called.
double clearValue() const;
virtual double valueFromText( const QString & text ) const;
virtual QValidator::State validate( QString & input, int & pos ) const;
protected:
virtual void resizeEvent( QResizeEvent* event );
virtual void changeEvent( QEvent* event );
@ -76,7 +94,10 @@ class GUI_EXPORT QgsDoubleSpinBox : public QDoubleSpinBox
ClearValueMode mClearValueMode;
double mCustomClearValue;
bool mExpressionsEnabled;
QToolButton* mClearButton;
QString stripped( const QString &originalText ) const;
};
#endif // QGSDOUBLESPPINBOX_H

View File

@ -48,12 +48,13 @@ class GUI_EXPORT QgsSpinBox : public QSpinBox
/**
* @brief setClearValue defines the clear value for the widget and will automatically set the clear value mode to CustomValue
* @param defines the numerical value used as the clear value
* @param customValue defines the numerical value used as the clear value
* @param clearValueText is the text displayed when the spin box is at the clear value. If not specified, no special value text is used.
*/
void setClearValue( int customValue, QString clearValueText = QString() );
/**
* @brief setClearValueMode defines if the clear value should be the minimum or maximum values of the widget or a custom value
* @param mode mode to user for clear value
* @param clearValueText is the text displayed when the spin box is at the clear value. If not specified, no special value text is used.
*/
void setClearValueMode( ClearValueMode mode, QString clearValueText = QString() );

View File

@ -285,6 +285,7 @@ class TestQgsExpression: public QObject
QTest::newRow( "max(1,3.5,-2.1)" ) << "max(1,3.5,-2.1)" << false << QVariant( 3.5 );
QTest::newRow( "min(-1.5)" ) << "min(-1.5)" << false << QVariant( -1.5 );
QTest::newRow( "min(-16.6,3.5,-2.1)" ) << "min(-16.6,3.5,-2.1)" << false << QVariant( -16.6 );
QTest::newRow( "min(5,3.5,-2.1)" ) << "min(5,3.5,-2.1)" << false << QVariant( -2.1 );
QTest::newRow( "clamp(-2,1,5)" ) << "clamp(-2,1,5)" << false << QVariant( 1.0 );
QTest::newRow( "clamp(-2,-10,5)" ) << "clamp(-2,-10,5)" << false << QVariant( -2.0 );
QTest::newRow( "clamp(-2,100,5)" ) << "clamp(-2,100,5)" << false << QVariant( 5.0 );
@ -327,6 +328,7 @@ class TestQgsExpression: public QObject
QTest::newRow( "substr" ) << "substr('HeLLo', 3,2)" << false << QVariant( "LL" );
QTest::newRow( "substr outside" ) << "substr('HeLLo', -5,2)" << false << QVariant( "" );
QTest::newRow( "regexp_substr" ) << "regexp_substr('abc123','(\\\\d+)')" << false << QVariant( "123" );
QTest::newRow( "regexp_substr no hit" ) << "regexp_substr('abcdef','(\\\\d+)')" << false << QVariant( "" );
QTest::newRow( "regexp_substr invalid" ) << "regexp_substr('abc123','([[[')" << true << QVariant();
QTest::newRow( "strpos" ) << "strpos('Hello World','World')" << false << QVariant( 6 );
QTest::newRow( "strpos outside" ) << "strpos('Hello World','blah')" << false << QVariant( -1 );
@ -345,6 +347,8 @@ class TestQgsExpression: public QObject
QTest::newRow( "wordwrap" ) << "wordwrap('university of qgis',-3,' ')" << false << QVariant( "university\nof qgis" );
QTest::newRow( "wordwrap" ) << "wordwrap('university of qgis\nsupports many multiline',-5,' ')" << false << QVariant( "university\nof qgis\nsupports\nmany multiline" );
QTest::newRow( "format" ) << "format('%1 %2 %3 %1', 'One', 'Two', 'Three')" << false << QVariant( "One Two Three One" );
QTest::newRow( "concat" ) << "concat('a', 'b', 'c', 'd')" << false << QVariant( "abcd" );
QTest::newRow( "concat single" ) << "concat('a')" << false << QVariant( "a" );
// implicit conversions
QTest::newRow( "implicit int->text" ) << "length(123)" << false << QVariant( 3 );
@ -977,6 +981,15 @@ class TestQgsExpression: public QObject
lst << i;
QtConcurrent::blockingMap( lst, _parseAndEvalExpr );
}
void evaluateToDouble()
{
QCOMPARE( QgsExpression::evaluateToDouble( QString( "5" ), 0.0 ), 5.0 );
QCOMPARE( QgsExpression::evaluateToDouble( QString( "5+6" ), 0.0 ), 11.0 );
QCOMPARE( QgsExpression::evaluateToDouble( QString( "5*" ), 7.0 ), 7.0 );
QCOMPARE( QgsExpression::evaluateToDouble( QString( "a" ), 9.0 ), 9.0 );
QCOMPARE( QgsExpression::evaluateToDouble( QString(), 9.0 ), 9.0 );
}
};
QTEST_MAIN( TestQgsExpression )

View File

@ -119,6 +119,7 @@ ADD_QGIS_TEST(zoomtest testqgsmaptoolzoom.cpp)
ADD_QGIS_TEST(projectionissues testprojectionissues.cpp)
ADD_QGIS_TEST(scalecombobox testqgsscalecombobox.cpp)
ADD_QGIS_TEST(dualviewtest testqgsdualview.cpp )
ADD_QGIS_TEST(doublespinbox testqgsdoublespinbox.cpp)
ADD_QGIS_TEST(rubberbandtest testqgsrubberband.cpp )
ADD_QGIS_TEST(mapcanvastest testqgsmapcanvas.cpp )

View File

@ -0,0 +1,141 @@
/***************************************************************************
testqgsdoublespinbox.cpp
--------------------------------------
Date : December 2014
Copyright : (C) 2014 Nyall Dawson
Email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include <QtTest/QtTest>
#include <editorwidgets/qgsdoublespinbox.h>
class TestQgsDoubleSpinBox: public QObject
{
Q_OBJECT
private slots:
void initTestCase(); // will be called before the first testfunction is executed.
void cleanupTestCase(); // will be called after the last testfunction was executed.
void init(); // will be called before each testfunction is executed.
void cleanup(); // will be called after every testfunction.
void clear();
void expression();
private:
};
void TestQgsDoubleSpinBox::initTestCase()
{
}
void TestQgsDoubleSpinBox::cleanupTestCase()
{
}
void TestQgsDoubleSpinBox::init()
{
}
void TestQgsDoubleSpinBox::cleanup()
{
}
void TestQgsDoubleSpinBox::clear()
{
QgsDoubleSpinBox* spinBox = new QgsDoubleSpinBox();
spinBox->setMaximum( 10.0 );
spinBox->setMinimum( 1.0 );
spinBox->setValue( 5.0 );
spinBox->setClearValueMode( QgsDoubleSpinBox::MinimumValue );
spinBox->clear();
QCOMPARE( spinBox->value(), 1.0 );
QCOMPARE( spinBox->clearValue(), 1.0 );
spinBox->setClearValueMode( QgsDoubleSpinBox::MaximumValue );
spinBox->clear();
QCOMPARE( spinBox->value(), 10.0 );
QCOMPARE( spinBox->clearValue(), 10.0 );
spinBox->setClearValue( 7.0 );
spinBox->clear();
QCOMPARE( spinBox->value(), 7.0 );
QCOMPARE( spinBox->clearValue(), 7.0 );
delete spinBox;
}
void TestQgsDoubleSpinBox::expression()
{
QgsDoubleSpinBox* spinBox = new QgsDoubleSpinBox();
spinBox->setMinimum( -10.0 );
spinBox->setMaximum( 10.0 );
spinBox->setValue( 1.0 );
spinBox->setExpressionsEnabled( false );
QCOMPARE( spinBox->valueFromText( QString( "5" ) ), 5.0 );
QCOMPARE( spinBox->valueFromText( QString( "5+2" ) ), -10.0 );
spinBox->setExpressionsEnabled( true );
QCOMPARE( spinBox->valueFromText( QString( "5" ) ), 5.0 );
QCOMPARE( spinBox->valueFromText( QString( "5+2" ) ), 7.0 );
spinBox->setClearValue( 3.0 );
QCOMPARE( spinBox->valueFromText( QString( "" ) ), 3.0 ); //clearing should set to clearValue
spinBox->setValue( 4.0 );
QCOMPARE( spinBox->valueFromText( QString( "5/" ) ), 4.0 ); //invalid expression should reset to previous value
//suffix tests
spinBox->setSuffix( QString( "mm" ) );
spinBox->setExpressionsEnabled( false );
QCOMPARE( spinBox->valueFromText( QString( "5mm" ) ), 5.0 );
QCOMPARE( spinBox->valueFromText( QString( "5+2mm" ) ), -10.0 );
spinBox->setExpressionsEnabled( true );
QCOMPARE( spinBox->valueFromText( QString( "5 mm" ) ), 5.0 );
QCOMPARE( spinBox->valueFromText( QString( "5+2 mm" ) ), 7.0 );
QCOMPARE( spinBox->valueFromText( QString( "5mm" ) ), 5.0 );
QCOMPARE( spinBox->valueFromText( QString( "5+2mm" ) ), 7.0 );
spinBox->setClearValue( 3.0 );
QCOMPARE( spinBox->valueFromText( QString( "mm" ) ), 3.0 ); //clearing should set to clearValue
QCOMPARE( spinBox->valueFromText( QString( "" ) ), 3.0 );
spinBox->setValue( 4.0 );
QCOMPARE( spinBox->valueFromText( QString( "5/mm" ) ), 4.0 ); //invalid expression should reset to previous value
//prefix tests
spinBox->setSuffix( QString() );
spinBox->setPrefix( QString( "mm" ) );
spinBox->setExpressionsEnabled( false );
QCOMPARE( spinBox->valueFromText( QString( "mm5" ) ), 5.0 );
QCOMPARE( spinBox->valueFromText( QString( "mm5+2" ) ), -10.0 );
spinBox->setExpressionsEnabled( true );
QCOMPARE( spinBox->valueFromText( QString( "mm 5" ) ), 5.0 );
QCOMPARE( spinBox->valueFromText( QString( "mm 5+2" ) ), 7.0 );
QCOMPARE( spinBox->valueFromText( QString( "mm5" ) ), 5.0 );
QCOMPARE( spinBox->valueFromText( QString( "mm5+2" ) ), 7.0 );
spinBox->setClearValue( 3.0 );
QCOMPARE( spinBox->valueFromText( QString( "mm" ) ), 3.0 ); //clearing should set to clearValue
QCOMPARE( spinBox->valueFromText( QString( "" ) ), 3.0 );
spinBox->setValue( 4.0 );
QCOMPARE( spinBox->valueFromText( QString( "mm5/" ) ), 4.0 ); //invalid expression should reset to previous value
//both suffix and prefix
spinBox->setSuffix( QString( "ll" ) );
spinBox->setPrefix( QString( "mm" ) );
spinBox->setExpressionsEnabled( true );
QCOMPARE( spinBox->valueFromText( QString( "mm 5 ll" ) ), 5.0 );
QCOMPARE( spinBox->valueFromText( QString( "mm 5+2 ll" ) ), 7.0 );
QCOMPARE( spinBox->valueFromText( QString( "mm5ll" ) ), 5.0 );
QCOMPARE( spinBox->valueFromText( QString( "mm5+2ll" ) ), 7.0 );
spinBox->setClearValue( 3.0 );
QCOMPARE( spinBox->valueFromText( QString( "mmll" ) ), 3.0 ); //clearing should set to clearValue
QCOMPARE( spinBox->valueFromText( QString( "" ) ), 3.0 );
spinBox->setValue( 4.0 );
QCOMPARE( spinBox->valueFromText( QString( "mm5/ll" ) ), 4.0 ); //invalid expression should reset to previous value
}
QTEST_MAIN( TestQgsDoubleSpinBox )
#include "testqgsdoublespinbox.moc"