diff --git a/python/core/qgsexpression.sip b/python/core/qgsexpression.sip index 9924fd54b1c..39e41fb1791 100644 --- a/python/core/qgsexpression.sip +++ b/python/core/qgsexpression.sip @@ -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, diff --git a/python/gui/editorwidgets/qgsdoublespinbox.sip b/python/gui/editorwidgets/qgsdoublespinbox.sip index ce9f6b9c01e..5e625ef484a 100644 --- a/python/gui/editorwidgets/qgsdoublespinbox.sip +++ b/python/gui/editorwidgets/qgsdoublespinbox.sip @@ -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 ); diff --git a/python/gui/editorwidgets/qgsspinbox.sip b/python/gui/editorwidgets/qgsspinbox.sip index fe2c1ab44c3..25aeea81880 100644 --- a/python/gui/editorwidgets/qgsspinbox.sip +++ b/python/gui/editorwidgets/qgsspinbox.sip @@ -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() ); diff --git a/src/core/qgsexpression.cpp b/src/core/qgsexpression.cpp index 282d46c5d77..4993c092564 100644 --- a/src/core/qgsexpression.cpp +++ b/src/core/qgsexpression.cpp @@ -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 diff --git a/src/core/qgsexpression.h b/src/core/qgsexpression.h index 0358875173d..378d3ade3dd 100644 --- a/src/core/qgsexpression.h +++ b/src/core/qgsexpression.h @@ -200,6 +200,16 @@ class CORE_EXPORT QgsExpression const QMap *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, diff --git a/src/gui/editorwidgets/qgsdoublespinbox.cpp b/src/gui/editorwidgets/qgsdoublespinbox.cpp index 159b8904c13..541527a3aab 100644 --- a/src/gui/editorwidgets/qgsdoublespinbox.cpp +++ b/src/gui/editorwidgets/qgsdoublespinbox.cpp @@ -20,7 +20,7 @@ #include #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 ); diff --git a/src/gui/editorwidgets/qgsdoublespinbox.h b/src/gui/editorwidgets/qgsdoublespinbox.h index 7548da6a68a..c4be7f8e0d2 100644 --- a/src/gui/editorwidgets/qgsdoublespinbox.h +++ b/src/gui/editorwidgets/qgsdoublespinbox.h @@ -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 diff --git a/src/gui/editorwidgets/qgsspinbox.h b/src/gui/editorwidgets/qgsspinbox.h index c195a5c38df..ba066bcc790 100644 --- a/src/gui/editorwidgets/qgsspinbox.h +++ b/src/gui/editorwidgets/qgsspinbox.h @@ -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() ); diff --git a/tests/src/core/testqgsexpression.cpp b/tests/src/core/testqgsexpression.cpp index eb405eb4788..63398401b05 100644 --- a/tests/src/core/testqgsexpression.cpp +++ b/tests/src/core/testqgsexpression.cpp @@ -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 ) diff --git a/tests/src/gui/CMakeLists.txt b/tests/src/gui/CMakeLists.txt index 82660abc1ec..5757d38d148 100644 --- a/tests/src/gui/CMakeLists.txt +++ b/tests/src/gui/CMakeLists.txt @@ -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 ) diff --git a/tests/src/gui/testqgsdoublespinbox.cpp b/tests/src/gui/testqgsdoublespinbox.cpp new file mode 100644 index 00000000000..d71d87d255a --- /dev/null +++ b/tests/src/gui/testqgsdoublespinbox.cpp @@ -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 + +#include + +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"