diff --git a/python/gui/auto_generated/editorwidgets/qgsdoublespinbox.sip.in b/python/gui/auto_generated/editorwidgets/qgsdoublespinbox.sip.in index dff7644775b..8cc28a56547 100644 --- a/python/gui/auto_generated/editorwidgets/qgsdoublespinbox.sip.in +++ b/python/gui/auto_generated/editorwidgets/qgsdoublespinbox.sip.in @@ -124,6 +124,13 @@ Returns the value used when clear() is called. Set alignment in the embedded line edit widget :param alignment: +%End + + void setSpecialValueText( const QString &txt ); +%Docstring +Set the special-value text to be ``txt`` +If set, the spin box will display this text instead of a numeric value whenever the current value +is equal to minimum(). Typical use is to indicate that this choice has a special (default) meaning. %End virtual double valueFromText( const QString &text ) const; diff --git a/python/gui/auto_generated/editorwidgets/qgsspinbox.sip.in b/python/gui/auto_generated/editorwidgets/qgsspinbox.sip.in index 8bc50661cee..97b4f9cf538 100644 --- a/python/gui/auto_generated/editorwidgets/qgsspinbox.sip.in +++ b/python/gui/auto_generated/editorwidgets/qgsspinbox.sip.in @@ -124,6 +124,13 @@ Returns the value used when clear() is called. Set alignment in the embedded line edit widget :param alignment: +%End + + void setSpecialValueText( const QString &txt ); +%Docstring +Set the special-value text to be ``txt`` +If set, the spin box will display this text instead of a numeric value whenever the current value +is equal to minimum(). Typical use is to indicate that this choice has a special (default) meaning. %End virtual int valueFromText( const QString &text ) const; diff --git a/src/gui/editorwidgets/qgsdoublespinbox.cpp b/src/gui/editorwidgets/qgsdoublespinbox.cpp index 7955aa87c28..b89230b0934 100644 --- a/src/gui/editorwidgets/qgsdoublespinbox.cpp +++ b/src/gui/editorwidgets/qgsdoublespinbox.cpp @@ -26,6 +26,12 @@ #define CLEAR_ICON_SIZE 16 +// This is required because private implementation of +// QAbstractSpinBoxPrivate checks for specialText emptiness +// and skips specialText handling if it's empty +QString QgsDoubleSpinBox::SPECIAL_TEXT_WHEN_EMPTY = QChar( 0x2063 ); + + QgsDoubleSpinBox::QgsDoubleSpinBox( QWidget *parent ) : QDoubleSpinBox( parent ) { @@ -140,6 +146,14 @@ void QgsDoubleSpinBox::setLineEditAlignment( Qt::Alignment alignment ) mLineEdit->setAlignment( alignment ); } +void QgsDoubleSpinBox::setSpecialValueText( const QString &txt ) +{ + if ( txt.isEmpty() ) + QDoubleSpinBox::setSpecialValueText( SPECIAL_TEXT_WHEN_EMPTY ); + else + QDoubleSpinBox::setSpecialValueText( txt ); +} + QString QgsDoubleSpinBox::stripped( const QString &originalText ) const { //adapted from QAbstractSpinBoxPrivate::stripped @@ -147,6 +161,9 @@ QString QgsDoubleSpinBox::stripped( const QString &originalText ) const QString text = originalText; if ( specialValueText().isEmpty() || text != specialValueText() ) { + // Strip SPECIAL_TEXT_WHEN_EMPTY + if ( text.contains( SPECIAL_TEXT_WHEN_EMPTY ) ) + text = text.replace( SPECIAL_TEXT_WHEN_EMPTY, QString() ); int from = 0; int size = text.size(); bool changed = false; diff --git a/src/gui/editorwidgets/qgsdoublespinbox.h b/src/gui/editorwidgets/qgsdoublespinbox.h index a1bc5e1defd..a323fd3f2e5 100644 --- a/src/gui/editorwidgets/qgsdoublespinbox.h +++ b/src/gui/editorwidgets/qgsdoublespinbox.h @@ -132,6 +132,13 @@ class GUI_EXPORT QgsDoubleSpinBox : public QDoubleSpinBox */ void setLineEditAlignment( Qt::Alignment alignment ); + /** + * Set the special-value text to be \a txt + * If set, the spin box will display this text instead of a numeric value whenever the current value + * is equal to minimum(). Typical use is to indicate that this choice has a special (default) meaning. + */ + void setSpecialValueText( const QString &txt ); + double valueFromText( const QString &text ) const override; QValidator::State validate( QString &input, int &pos ) const override; void paintEvent( QPaintEvent *e ) override; @@ -156,6 +163,13 @@ class GUI_EXPORT QgsDoubleSpinBox : public QDoubleSpinBox bool mExpressionsEnabled = true; QString stripped( const QString &originalText ) const; + + // This is required because private implementation of + // QAbstractSpinBoxPrivate checks for specialText emptiness + // and skips specialText handling if it's empty + static QString SPECIAL_TEXT_WHEN_EMPTY; + + friend class TestQgsRangeWidgetWrapper; }; #endif // QGSDOUBLESPINBOX_H diff --git a/src/gui/editorwidgets/qgsrangewidgetwrapper.cpp b/src/gui/editorwidgets/qgsrangewidgetwrapper.cpp index 9442b5e0a68..6f298e8c9d3 100644 --- a/src/gui/editorwidgets/qgsrangewidgetwrapper.cpp +++ b/src/gui/editorwidgets/qgsrangewidgetwrapper.cpp @@ -22,6 +22,8 @@ #include "qgsdial.h" #include "qgsslider.h" + + QgsRangeWidgetWrapper::QgsRangeWidgetWrapper( QgsVectorLayer *vl, int fieldIdx, QWidget *editor, QWidget *parent ) : QgsEditorWidgetWrapper( vl, fieldIdx, editor, parent ) @@ -119,7 +121,11 @@ void QgsRangeWidgetWrapper::initWidget( QWidget *editor ) // Note: call setMinimum here or setValue won't work mDoubleSpinBox->setMinimum( minval ); mDoubleSpinBox->setValue( minval ); - mDoubleSpinBox->setSpecialValueText( QgsApplication::nullRepresentation() ); + QgsDoubleSpinBox *doubleSpinBox( qobject_cast( mDoubleSpinBox ) ); + if ( doubleSpinBox ) + doubleSpinBox->setSpecialValueText( QgsApplication::nullRepresentation() ); + else + mDoubleSpinBox->setSpecialValueText( QgsApplication::nullRepresentation() ); } mDoubleSpinBox->setMinimum( minval ); mDoubleSpinBox->setMaximum( maxval ); @@ -141,7 +147,11 @@ void QgsRangeWidgetWrapper::initWidget( QWidget *editor ) int stepval = step.isValid() ? step.toInt() : 1; minval -= stepval; mIntSpinBox->setValue( minval ); - mIntSpinBox->setSpecialValueText( QgsApplication::nullRepresentation() ); + QgsSpinBox *intSpinBox( qobject_cast( mIntSpinBox ) ); + if ( intSpinBox ) + intSpinBox->setSpecialValueText( QgsApplication::nullRepresentation() ); + else + mIntSpinBox->setSpecialValueText( QgsApplication::nullRepresentation() ); } setupIntEditor( minval, max, step, mIntSpinBox, this ); if ( config( QStringLiteral( "Suffix" ) ).isValid() ) diff --git a/src/gui/editorwidgets/qgsspinbox.cpp b/src/gui/editorwidgets/qgsspinbox.cpp index aeb42ed8644..f783a85f8ff 100644 --- a/src/gui/editorwidgets/qgsspinbox.cpp +++ b/src/gui/editorwidgets/qgsspinbox.cpp @@ -26,6 +26,12 @@ #define CLEAR_ICON_SIZE 16 +// This is required because private implementation of +// QAbstractSpinBoxPrivate checks for specialText emptiness +// and skips specialText handling if it's empty +QString QgsSpinBox::SPECIAL_TEXT_WHEN_EMPTY = QChar( 0x2063 ); + + QgsSpinBox::QgsSpinBox( QWidget *parent ) : QSpinBox( parent ) { @@ -137,6 +143,14 @@ void QgsSpinBox::setLineEditAlignment( Qt::Alignment alignment ) mLineEdit->setAlignment( alignment ); } +void QgsSpinBox::setSpecialValueText( const QString &txt ) +{ + if ( txt.isEmpty() ) + QSpinBox::setSpecialValueText( SPECIAL_TEXT_WHEN_EMPTY ); + else + QSpinBox::setSpecialValueText( txt ); +} + int QgsSpinBox::valueFromText( const QString &text ) const { if ( !mExpressionsEnabled ) @@ -185,6 +199,9 @@ QString QgsSpinBox::stripped( const QString &originalText ) const QString text = originalText; if ( specialValueText().isEmpty() || text != specialValueText() ) { + // Strip SPECIAL_TEXT_WHEN_EMPTY + if ( text.contains( SPECIAL_TEXT_WHEN_EMPTY ) ) + text = text.replace( SPECIAL_TEXT_WHEN_EMPTY, QString() ); int from = 0; int size = text.size(); bool changed = false; diff --git a/src/gui/editorwidgets/qgsspinbox.h b/src/gui/editorwidgets/qgsspinbox.h index 17e9f2ed432..cff822144f1 100644 --- a/src/gui/editorwidgets/qgsspinbox.h +++ b/src/gui/editorwidgets/qgsspinbox.h @@ -132,6 +132,13 @@ class GUI_EXPORT QgsSpinBox : public QSpinBox */ void setLineEditAlignment( Qt::Alignment alignment ); + /** + * Set the special-value text to be \a txt + * If set, the spin box will display this text instead of a numeric value whenever the current value + * is equal to minimum(). Typical use is to indicate that this choice has a special (default) meaning. + */ + void setSpecialValueText( const QString &txt ); + int valueFromText( const QString &text ) const override; QValidator::State validate( QString &input, int &pos ) const override; @@ -156,7 +163,15 @@ class GUI_EXPORT QgsSpinBox : public QSpinBox bool mExpressionsEnabled = true; + // This is required because private implementation of + // QAbstractSpinBoxPrivate checks for specialText emptiness + // and skips specialText handling if it's empty + static QString SPECIAL_TEXT_WHEN_EMPTY; + QString stripped( const QString &originalText ) const; + + friend class TestQgsRangeWidgetWrapper; + }; #endif // QGSSPINBOX_H diff --git a/tests/src/gui/testqgsrangewidgetwrapper.cpp b/tests/src/gui/testqgsrangewidgetwrapper.cpp index 4da02dc0eda..e0b1e0f8fb0 100644 --- a/tests/src/gui/testqgsrangewidgetwrapper.cpp +++ b/tests/src/gui/testqgsrangewidgetwrapper.cpp @@ -20,12 +20,14 @@ #include "qgsrangewidgetwrapper.h" #include "qgsrangeconfigdlg.h" #include "qgsdoublespinbox.h" +#include "qgsspinbox.h" #include "qgsapplication.h" #include "qgslogger.h" #include "qgsvectorlayer.h" #include "qgsdataprovider.h" +#include "qgsfilterlineedit.h" - +#include #include #include @@ -48,14 +50,22 @@ class TestQgsRangeWidgetWrapper : public QObject void test_setDoubleRange(); void test_setDoubleSmallerRange(); void test_setDoubleLimits(); + void test_nulls(); + private: - std::unique_ptr widget; // For field 1 + std::unique_ptr widget0; // For field 0 + std::unique_ptr widget1; // For field 1 std::unique_ptr widget2; // For field 2 std::unique_ptr vl; }; void TestQgsRangeWidgetWrapper::initTestCase() { + // Set up the QgsSettings environment + QCoreApplication::setOrganizationName( QStringLiteral( "QGIS" ) ); + QCoreApplication::setOrganizationDomain( QStringLiteral( "qgis.org" ) ); + QCoreApplication::setApplicationName( QStringLiteral( "QGIS-TEST-RANGE-WIDGET" ) ); + QgsApplication::init(); QgsApplication::initQgis(); } @@ -108,9 +118,10 @@ void TestQgsRangeWidgetWrapper::init() QCOMPARE( vl->featureCount( ), ( long )3 ); QgsFeature _feat1( vl->getFeature( 1 ) ); QCOMPARE( _feat1, feat1 ); - widget = qgis::make_unique( vl.get(), 1, nullptr, nullptr ); + widget0 = qgis::make_unique( vl.get(), 0, nullptr, nullptr ); + widget1 = qgis::make_unique( vl.get(), 1, nullptr, nullptr ); widget2 = qgis::make_unique( vl.get(), 2, nullptr, nullptr ); - QVERIFY( widget.get() ); + QVERIFY( widget1.get() ); } void TestQgsRangeWidgetWrapper::cleanup() @@ -123,9 +134,9 @@ void TestQgsRangeWidgetWrapper::test_setDoubleRange() // See https://issues.qgis.org/issues/17878 // QGIS 3 Vector Layer Fields Garbled when Clicking the Toggle Editing Icon - QgsDoubleSpinBox *editor = qobject_cast( widget->createWidget( nullptr ) ); + QgsDoubleSpinBox *editor = qobject_cast( widget1->createWidget( nullptr ) ); QVERIFY( editor ); - widget->initWidget( editor ); + widget1->initWidget( editor ); QgsDoubleSpinBox *editor2 = qobject_cast( widget2->createWidget( nullptr ) ); QVERIFY( editor2 ); widget2->initWidget( editor2 ); @@ -133,7 +144,7 @@ void TestQgsRangeWidgetWrapper::test_setDoubleRange() QgsFeature feat( vl->getFeature( 1 ) ); QVERIFY( feat.isValid() ); QCOMPARE( feat.attribute( 1 ).toDouble(), 123.123456789 ); - widget->setFeature( vl->getFeature( 1 ) ); + widget1->setFeature( vl->getFeature( 1 ) ); widget2->setFeature( vl->getFeature( 1 ) ); QCOMPARE( vl->fields().at( 1 ).precision(), 9 ); // Default is 0 !!! for double, really ? @@ -151,12 +162,12 @@ void TestQgsRangeWidgetWrapper::test_setDoubleRange() QCOMPARE( editor->maximum( ), std::numeric_limits::max() ); QCOMPARE( editor2->maximum( ), std::numeric_limits::max() ); - widget->setFeature( vl->getFeature( 2 ) ); + widget1->setFeature( vl->getFeature( 2 ) ); widget2->setFeature( vl->getFeature( 2 ) ); QCOMPARE( editor->value( ), editor->minimum() ); QCOMPARE( editor2->value( ), editor->minimum() ); - widget->setFeature( vl->getFeature( 3 ) ); + widget1->setFeature( vl->getFeature( 3 ) ); widget2->setFeature( vl->getFeature( 3 ) ); QCOMPARE( editor->value( ), -123.123456789 ); QCOMPARE( editor2->value( ), -123.0 ); @@ -169,10 +180,10 @@ void TestQgsRangeWidgetWrapper::test_setDoubleSmallerRange() cfg.insert( QStringLiteral( "Min" ), -100.0 ); cfg.insert( QStringLiteral( "Max" ), 100.0 ); cfg.insert( QStringLiteral( "Step" ), 1 ); - widget->setConfig( cfg ); - QgsDoubleSpinBox *editor = qobject_cast( widget->createWidget( nullptr ) ); + widget1->setConfig( cfg ); + QgsDoubleSpinBox *editor = qobject_cast( widget1->createWidget( nullptr ) ); QVERIFY( editor ); - widget->initWidget( editor ); + widget1->initWidget( editor ); widget2->setConfig( cfg ); QgsDoubleSpinBox *editor2 = qobject_cast( widget2->createWidget( nullptr ) ); @@ -182,7 +193,7 @@ void TestQgsRangeWidgetWrapper::test_setDoubleSmallerRange() QgsFeature feat( vl->getFeature( 1 ) ); QVERIFY( feat.isValid() ); QCOMPARE( feat.attribute( 1 ).toDouble(), 123.123456789 ); - widget->setFeature( vl->getFeature( 1 ) ); + widget1->setFeature( vl->getFeature( 1 ) ); widget2->setFeature( vl->getFeature( 1 ) ); QCOMPARE( vl->fields().at( 1 ).precision(), 9 ); @@ -202,13 +213,13 @@ void TestQgsRangeWidgetWrapper::test_setDoubleSmallerRange() QCOMPARE( editor2->maximum( ), ( double )100 ); // NULL, NULL - widget->setFeature( vl->getFeature( 2 ) ); + widget1->setFeature( vl->getFeature( 2 ) ); widget2->setFeature( vl->getFeature( 2 ) ); QCOMPARE( editor->value( ), editor->minimum() ); QCOMPARE( editor2->value( ), editor2->minimum() ); // negative, negative - widget->setFeature( vl->getFeature( 3 ) ); + widget1->setFeature( vl->getFeature( 3 ) ); widget2->setFeature( vl->getFeature( 3 ) ); // value was changed to the minimum QCOMPARE( editor->value( ), editor->minimum() ); @@ -223,10 +234,10 @@ void TestQgsRangeWidgetWrapper::test_setDoubleLimits() cfg.insert( QStringLiteral( "Min" ), std::numeric_limits::lowest() ); cfg.insert( QStringLiteral( "Max" ), std::numeric_limits::max() ); cfg.insert( QStringLiteral( "Step" ), 1 ); - widget->setConfig( cfg ); - QgsDoubleSpinBox *editor = qobject_cast( widget->createWidget( nullptr ) ); + widget1->setConfig( cfg ); + QgsDoubleSpinBox *editor = qobject_cast( widget1->createWidget( nullptr ) ); QVERIFY( editor ); - widget->initWidget( editor ); + widget1->initWidget( editor ); widget2->setConfig( cfg ); QgsDoubleSpinBox *editor2 = qobject_cast( widget2->createWidget( nullptr ) ); @@ -241,7 +252,7 @@ void TestQgsRangeWidgetWrapper::test_setDoubleLimits() QgsFeature feat( vl->getFeature( 1 ) ); QVERIFY( feat.isValid() ); QCOMPARE( feat.attribute( 1 ).toDouble(), 123.123456789 ); - widget->setFeature( vl->getFeature( 1 ) ); + widget1->setFeature( vl->getFeature( 1 ) ); widget2->setFeature( vl->getFeature( 1 ) ); QCOMPARE( vl->fields().at( 1 ).precision(), 9 ); @@ -253,13 +264,13 @@ void TestQgsRangeWidgetWrapper::test_setDoubleLimits() QCOMPARE( editor2->value( ), 123.0 ); // NULL, NULL - widget->setFeature( vl->getFeature( 2 ) ); + widget1->setFeature( vl->getFeature( 2 ) ); widget2->setFeature( vl->getFeature( 2 ) ); QCOMPARE( editor->value( ), editor->minimum() ); QCOMPARE( editor2->value( ), editor2->minimum() ); // negative, negative - widget->setFeature( vl->getFeature( 3 ) ); + widget1->setFeature( vl->getFeature( 3 ) ); widget2->setFeature( vl->getFeature( 3 ) ); // value was changed to the minimum QCOMPARE( editor->value( ), -123.123456789 ); @@ -267,6 +278,56 @@ void TestQgsRangeWidgetWrapper::test_setDoubleLimits() } +void TestQgsRangeWidgetWrapper::test_nulls() +{ + + QgsApplication::setNullRepresentation( QString( "" ) ); + + QVariantMap cfg; + cfg.insert( QStringLiteral( "Min" ), 100.00 ); + cfg.insert( QStringLiteral( "Max" ), 200.00 ); + cfg.insert( QStringLiteral( "Step" ), 1 ); + cfg.insert( QStringLiteral( "Precision" ), 0 ); + widget1->setConfig( cfg ); + QgsDoubleSpinBox *editor1 = qobject_cast( widget1->createWidget( nullptr ) ); + QVERIFY( editor1 ); + widget1->initWidget( editor1 ); + // Out of range + widget1->setFeature( vl->getFeature( 3 ) ); + QCOMPARE( editor1->value( ), editor1->minimum() ); + QCOMPARE( widget1->value( ), QVariant( QVariant::Double ) ); + widget1->setFeature( QgsFeature( vl->fields() ) ); + // Null + QCOMPARE( editor1->value( ), editor1->minimum() ); + QCOMPARE( widget1->value( ), QVariant( QVariant::Double ) ); + QCOMPARE( editor1->mLineEdit->text(), QgsDoubleSpinBox::SPECIAL_TEXT_WHEN_EMPTY ); + editor1->mLineEdit->setText( QString( "151%1" ).arg( QgsDoubleSpinBox::SPECIAL_TEXT_WHEN_EMPTY ) ); + QCOMPARE( widget1->value( ), 151 ); + editor1->mLineEdit->setText( QString( QgsDoubleSpinBox::SPECIAL_TEXT_WHEN_EMPTY ).append( QStringLiteral( "161" ) ) ); + QCOMPARE( widget1->value( ), 161 ); + + + QgsSpinBox *editor0 = qobject_cast( widget0->createWidget( nullptr ) ); + QVERIFY( editor0 ); + widget0->setConfig( cfg ); + widget0->initWidget( editor0 ); + // Out of range + widget0->setFeature( vl->getFeature( 3 ) ); + QCOMPARE( editor0->value( ), editor0->minimum() ); + QCOMPARE( widget0->value( ), QVariant( QVariant::Int ) ); + widget0->setFeature( QgsFeature( vl->fields() ) ); + // Null + QCOMPARE( editor0->value( ), editor0->minimum() ); + QCOMPARE( widget0->value( ), QVariant( QVariant::Int ) ); + QCOMPARE( editor0->mLineEdit->text(), QgsDoubleSpinBox::SPECIAL_TEXT_WHEN_EMPTY ); + + editor0->mLineEdit->setText( QString( "150%1" ).arg( QgsDoubleSpinBox::SPECIAL_TEXT_WHEN_EMPTY ) ); + QCOMPARE( widget0->value( ), 150 ); + editor0->mLineEdit->setText( QString( QgsDoubleSpinBox::SPECIAL_TEXT_WHEN_EMPTY ).append( QStringLiteral( "160" ) ) ); + QCOMPARE( widget0->value( ), 160 ); + +} + QGSTEST_MAIN( TestQgsRangeWidgetWrapper )