Skip to content

Commit d2c9863

Browse files
dgoedkoopm-kuhn
authored andcommitted
Add null handling to value map edit widget (fixes #15215) (#3274)
* Add null handling to value map edit widget (fixes #15215) * Return QVariant type * Use hardcoded value for 'null' representation * Detect "null" value when loading value map from csv; use null QString constructor * Use configured "null" representation for display in value map * Use single definition for value map null representation guid * Added unit test for value map widget and fixed value displaying bug
1 parent 07da9b1 commit d2c9863

File tree

6 files changed

+154
-43
lines changed

6 files changed

+154
-43
lines changed

src/gui/editorwidgets/qgsvaluemapconfigdlg.cpp

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ QgsValueMapConfigDlg::QgsValueMapConfigDlg( QgsVectorLayer* vl, int fieldIdx, QW
2929

3030
tableWidget->insertRow( 0 );
3131

32+
connect( addNullButton, SIGNAL( clicked() ), this, SLOT( addNullButtonPushed() ) );
3233
connect( removeSelectedButton, SIGNAL( clicked() ), this, SLOT( removeSelectedButtonPushed() ) );
3334
connect( loadFromLayerButton, SIGNAL( clicked() ), this, SLOT( loadFromLayerButtonPushed() ) );
3435
connect( loadFromCSVButton, SIGNAL( clicked() ), this, SLOT( loadFromCSVButtonPushed() ) );
@@ -38,6 +39,7 @@ QgsValueMapConfigDlg::QgsValueMapConfigDlg( QgsVectorLayer* vl, int fieldIdx, QW
3839
QgsEditorWidgetConfig QgsValueMapConfigDlg::config()
3940
{
4041
QgsEditorWidgetConfig cfg;
42+
QSettings settings;
4143

4244
//store data to map
4345
for ( int i = 0; i < tableWidget->rowCount() - 1; i++ )
@@ -48,13 +50,17 @@ QgsEditorWidgetConfig QgsValueMapConfigDlg::config()
4850
if ( !ki )
4951
continue;
5052

53+
QString ks = ki->text();
54+
if (( ks == settings.value( "qgis/nullValue", "NULL" ).toString() ) && !( ki->flags() & Qt::ItemIsEditable ) )
55+
ks = VALUEMAP_NULL_TEXT;
56+
5157
if ( !vi || vi->text().isNull() )
5258
{
53-
cfg.insert( ki->text(), ki->text() );
59+
cfg.insert( ks, ks );
5460
}
5561
else
5662
{
57-
cfg.insert( vi->text(), ki->text() );
63+
cfg.insert( vi->text(), ks );
5864
}
5965
}
6066

@@ -72,16 +78,10 @@ void QgsValueMapConfigDlg::setConfig( const QgsEditorWidgetConfig& config )
7278
int row = 0;
7379
for ( QgsEditorWidgetConfig::ConstIterator mit = config.begin(); mit != config.end(); mit++, row++ )
7480
{
75-
tableWidget->insertRow( row );
7681
if ( mit.value().isNull() )
77-
{
78-
tableWidget->setItem( row, 0, new QTableWidgetItem( mit.key() ) );
79-
}
82+
setRow( row, mit.key(), QString() );
8083
else
81-
{
82-
tableWidget->setItem( row, 0, new QTableWidgetItem( mit.value().toString() ) );
83-
tableWidget->setItem( row, 1, new QTableWidgetItem( mit.key() ) );
84-
}
84+
setRow( row, mit.value().toString(), mit.key() );
8585
}
8686
}
8787

@@ -129,27 +129,47 @@ void QgsValueMapConfigDlg::updateMap( const QMap<QString, QVariant> &map, bool i
129129

130130
if ( insertNull )
131131
{
132-
QSettings settings;
133-
tableWidget->setItem( row, 0, new QTableWidgetItem( settings.value( "qgis/nullValue", "NULL" ).toString() ) );
134-
tableWidget->setItem( row, 1, new QTableWidgetItem( "<NULL>" ) );
132+
setRow( row, VALUEMAP_NULL_TEXT, "<NULL>" );
135133
++row;
136134
}
137135

138136
for ( QMap<QString, QVariant>::const_iterator mit = map.begin(); mit != map.end(); ++mit, ++row )
139137
{
140-
tableWidget->insertRow( row );
141138
if ( mit.value().isNull() )
142-
{
143-
tableWidget->setItem( row, 0, new QTableWidgetItem( mit.key() ) );
144-
}
139+
setRow( row, mit.key(), QString() );
145140
else
146-
{
147-
tableWidget->setItem( row, 0, new QTableWidgetItem( mit.key() ) );
148-
tableWidget->setItem( row, 1, new QTableWidgetItem( mit.value().toString() ) );
149-
}
141+
setRow( row, mit.key(), mit.value().toString() );
150142
}
151143
}
152144

145+
void QgsValueMapConfigDlg::setRow( int row, const QString value, const QString description )
146+
{
147+
QSettings settings;
148+
QTableWidgetItem* valueCell;
149+
QTableWidgetItem* descriptionCell = new QTableWidgetItem( description );
150+
tableWidget->insertRow( row );
151+
if ( value == QString( VALUEMAP_NULL_TEXT ) )
152+
{
153+
QFont cellFont;
154+
cellFont.setItalic( true );
155+
valueCell = new QTableWidgetItem( settings.value( "qgis/nullValue", "NULL" ).toString() );
156+
valueCell->setFont( cellFont );
157+
valueCell->setFlags( Qt::ItemIsSelectable | Qt::ItemIsEnabled );
158+
descriptionCell->setFont( cellFont );
159+
}
160+
else
161+
{
162+
valueCell = new QTableWidgetItem( value );
163+
}
164+
tableWidget->setItem( row, 0, valueCell );
165+
tableWidget->setItem( row, 1, descriptionCell );
166+
}
167+
168+
void QgsValueMapConfigDlg::addNullButtonPushed()
169+
{
170+
setRow( tableWidget->rowCount() - 1, VALUEMAP_NULL_TEXT, "<NULL>" );
171+
}
172+
153173
void QgsValueMapConfigDlg::loadFromLayerButtonPushed()
154174
{
155175
QgsAttributeTypeLoadDialog layerDialog( layer() );
@@ -161,6 +181,8 @@ void QgsValueMapConfigDlg::loadFromLayerButtonPushed()
161181

162182
void QgsValueMapConfigDlg::loadFromCSVButtonPushed()
163183
{
184+
QSettings settings;
185+
164186
QString fileName = QFileDialog::getOpenFileName( nullptr, tr( "Select a file" ), QDir::homePath() );
165187
if ( fileName.isNull() )
166188
return;
@@ -217,6 +239,9 @@ void QgsValueMapConfigDlg::loadFromCSVButtonPushed()
217239
val = val.mid( 1, val.length() - 2 );
218240
}
219241

242+
if ( key == settings.value( "qgis/nullValue", "NULL" ).toString() )
243+
key = QString( VALUEMAP_NULL_TEXT );
244+
220245
map[ key ] = val;
221246
}
222247

src/gui/editorwidgets/qgsvaluemapconfigdlg.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
#include "qgseditorconfigwidget.h"
2222

23+
#define VALUEMAP_NULL_TEXT "{2839923C-8B7D-419E-B84B-CA2FE9B80EC7}"
24+
2325
/** \ingroup gui
2426
* \class QgsValueMapConfigDlg
2527
* \note not available in Python bindings
@@ -36,8 +38,12 @@ class GUI_EXPORT QgsValueMapConfigDlg : public QgsEditorConfigWidget, private Ui
3638

3739
void updateMap( const QMap<QString, QVariant> &map, bool insertNull );
3840

41+
private:
42+
void setRow( int row, const QString value, const QString description );
43+
3944
private slots:
4045
void vCellChanged( int row, int column );
46+
void addNullButtonPushed();
4147
void removeSelectedButtonPushed();
4248
void loadFromLayerButtonPushed();
4349
void loadFromCSVButtonPushed();

src/gui/editorwidgets/qgsvaluemapwidgetfactory.cpp

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
#include "qgsdefaultsearchwidgetwrapper.h"
2121
#include "qgsvaluemapconfigdlg.h"
2222

23+
#include <QSettings>
24+
2325
QgsValueMapWidgetFactory::QgsValueMapWidgetFactory( const QString& name )
2426
: QgsEditorWidgetFactory( name )
2527
{
@@ -82,11 +84,17 @@ void QgsValueMapWidgetFactory::writeConfig( const QgsEditorWidgetConfig& config,
8284

8385
QString QgsValueMapWidgetFactory::representValue( QgsVectorLayer* vl, int fieldIdx, const QgsEditorWidgetConfig& config, const QVariant& cache, const QVariant& value ) const
8486
{
85-
Q_UNUSED( vl )
86-
Q_UNUSED( fieldIdx )
8787
Q_UNUSED( cache )
8888

89-
return config.key( value, QVariant( QString( "(%1)" ).arg( value.toString() ) ).toString() );
89+
QString valueInternalText;
90+
QString valueDisplayText;
91+
QSettings settings;
92+
if ( value.isNull() )
93+
valueInternalText = QString( VALUEMAP_NULL_TEXT );
94+
else
95+
valueInternalText = value.toString();
96+
97+
return config.key( valueInternalText, QVariant( QString( "(%1)" ).arg( vl->fields().at( fieldIdx ).displayString( value ) ) ).toString() );
9098
}
9199

92100
QVariant QgsValueMapWidgetFactory::sortValue( QgsVectorLayer* vl, int fieldIdx, const QgsEditorWidgetConfig& config, const QVariant& cache, const QVariant& value ) const

src/gui/editorwidgets/qgsvaluemapwidgetwrapper.cpp

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
***************************************************************************/
1515

1616
#include "qgsvaluemapwidgetwrapper.h"
17+
#include "qgsvaluemapconfigdlg.h"
18+
19+
#include <QSettings>
1720

1821
QgsValueMapWidgetWrapper::QgsValueMapWidgetWrapper( QgsVectorLayer* vl, int fieldIdx, QWidget* editor, QWidget* parent )
1922
: QgsEditorWidgetWrapper( vl, fieldIdx, editor, parent )
@@ -29,6 +32,9 @@ QVariant QgsValueMapWidgetWrapper::value() const
2932
if ( mComboBox )
3033
v = mComboBox->itemData( mComboBox->currentIndex() );
3134

35+
if ( v == QString( VALUEMAP_NULL_TEXT ) )
36+
v = QVariant( field().type() );
37+
3238
return v;
3339
}
3440

@@ -70,6 +76,12 @@ bool QgsValueMapWidgetWrapper::valid() const
7076

7177
void QgsValueMapWidgetWrapper::setValue( const QVariant& value )
7278
{
79+
QString v;
80+
if ( value.isNull() )
81+
v = QString( VALUEMAP_NULL_TEXT );
82+
else
83+
v = value.toString();
84+
7385
if ( mComboBox )
74-
mComboBox->setCurrentIndex( mComboBox->findData( value ) );
86+
mComboBox->setCurrentIndex( mComboBox->findData( v ) );
7587
}

src/ui/editorwidgets/qgsvaluemapconfigdlgbase.ui

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<string>Form</string>
1515
</property>
1616
<layout class="QGridLayout" name="gridLayout">
17-
<item row="0" column="0" colspan="3">
17+
<item row="0" column="0" colspan="5">
1818
<widget class="QLabel" name="valueMapLabel">
1919
<property name="text">
2020
<string>Combo box with predefined items. Value is stored in the attribute, description is shown in the combo box.</string>
@@ -31,14 +31,7 @@
3131
</property>
3232
</widget>
3333
</item>
34-
<item row="1" column="1">
35-
<widget class="QPushButton" name="loadFromCSVButton">
36-
<property name="text">
37-
<string>Load Data from CSV File</string>
38-
</property>
39-
</widget>
40-
</item>
41-
<item row="1" column="2">
34+
<item row="1" column="4">
4235
<spacer name="horizontalSpacer">
4336
<property name="orientation">
4437
<enum>Qt::Horizontal</enum>
@@ -51,7 +44,7 @@
5144
</property>
5245
</spacer>
5346
</item>
54-
<item row="2" column="0" colspan="3">
47+
<item row="2" column="0" colspan="5">
5548
<widget class="QTableWidget" name="tableWidget">
5649
<column>
5750
<property name="text">
@@ -65,14 +58,7 @@
6558
</column>
6659
</widget>
6760
</item>
68-
<item row="3" column="0">
69-
<widget class="QPushButton" name="removeSelectedButton">
70-
<property name="text">
71-
<string>Remove Selected</string>
72-
</property>
73-
</widget>
74-
</item>
75-
<item row="3" column="1" colspan="2">
61+
<item row="3" column="3" colspan="2">
7662
<spacer name="horizontalSpacer_2">
7763
<property name="orientation">
7864
<enum>Qt::Horizontal</enum>
@@ -85,6 +71,27 @@
8571
</property>
8672
</spacer>
8773
</item>
74+
<item row="3" column="0">
75+
<widget class="QPushButton" name="addNullButton">
76+
<property name="text">
77+
<string>Add &quot;NULL&quot; value</string>
78+
</property>
79+
</widget>
80+
</item>
81+
<item row="3" column="1">
82+
<widget class="QPushButton" name="removeSelectedButton">
83+
<property name="text">
84+
<string>Remove Selected</string>
85+
</property>
86+
</widget>
87+
</item>
88+
<item row="1" column="1">
89+
<widget class="QPushButton" name="loadFromCSVButton">
90+
<property name="text">
91+
<string>Load Data from CSV File</string>
92+
</property>
93+
</widget>
94+
</item>
8895
</layout>
8996
</widget>
9097
<resources/>

tests/src/python/test_qgseditwidgets.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
class TestQgsTextEditWidget(unittest.TestCase):
2626

27+
VALUEMAP_NULL_TEXT = "{2839923C-8B7D-419E-B84B-CA2FE9B80EC7}"
28+
2729
@classmethod
2830
def setUpClass(cls):
2931
QgsEditorWidgetRegistry.initEditors()
@@ -63,6 +65,57 @@ def test_SetValue(self):
6365
self.doAttributeTest(0, ['value', '123', NULL, NULL])
6466
self.doAttributeTest(1, [NULL, 123, NULL, NULL])
6567

68+
def test_ValueMap_representValue(self):
69+
layer = QgsVectorLayer("none?field=number1:integer&field=number2:double&field=text1:string&field=number3:integer&field=number4:double&field=text2:string",
70+
"layer", "memory")
71+
assert layer.isValid()
72+
QgsMapLayerRegistry.instance().addMapLayer(layer)
73+
f = QgsFeature()
74+
f.setAttributes([2, 2.5, 'NULL', None, None, None])
75+
assert layer.dataProvider().addFeatures([f])
76+
reg = QgsEditorWidgetRegistry.instance()
77+
factory = reg.factory("ValueMap")
78+
self.assertIsNotNone(factory)
79+
80+
# Tests with different value types occuring in the value map
81+
config = {'two': '2', 'twoandhalf': '2.5', 'NULL text': 'NULL',
82+
'nothing': self.VALUEMAP_NULL_TEXT}
83+
self.assertEqual(factory.representValue(layer, 0, config, None, 2), 'two')
84+
self.assertEqual(factory.representValue(layer, 1, config, None, 2.5), 'twoandhalf')
85+
self.assertEqual(factory.representValue(layer, 2, config, None, 'NULL'), 'NULL text')
86+
# Tests with null values of different types, if value map contains null
87+
self.assertEqual(factory.representValue(layer, 3, config, None, None), 'nothing')
88+
self.assertEqual(factory.representValue(layer, 4, config, None, None), 'nothing')
89+
self.assertEqual(factory.representValue(layer, 5, config, None, None), 'nothing')
90+
# Tests with fallback display for different value types
91+
config = {}
92+
self.assertEqual(factory.representValue(layer, 0, config, None, 2), '(2)')
93+
self.assertEqual(factory.representValue(layer, 1, config, None, 2.5), '(2.50000)')
94+
self.assertEqual(factory.representValue(layer, 2, config, None, 'NULL'), '(NULL)')
95+
# Tests with fallback display for null in different types of fields
96+
self.assertEqual(factory.representValue(layer, 3, config, None, None), '(NULL)')
97+
self.assertEqual(factory.representValue(layer, 4, config, None, None), '(NULL)')
98+
self.assertEqual(factory.representValue(layer, 5, config, None, None), '(NULL)')
99+
100+
QgsMapLayerRegistry.instance().removeAllMapLayers()
101+
102+
def test_ValueMap_set_get(self):
103+
layer = QgsVectorLayer("none?field=number:integer", "layer", "memory")
104+
assert layer.isValid()
105+
QgsMapLayerRegistry.instance().addMapLayer(layer)
106+
reg = QgsEditorWidgetRegistry.instance()
107+
configWdg = reg.createConfigWidget('ValueMap', layer, 0, None)
108+
109+
config = {'two': '2', 'twoandhalf': '2.5', 'NULL text': 'NULL',
110+
'nothing': self.VALUEMAP_NULL_TEXT}
111+
112+
# Set a configuration containing values and NULL and check if it
113+
# is returned intact.
114+
configWdg.setConfig(config)
115+
self.assertEqual(configWdg.config(), config)
116+
117+
QgsMapLayerRegistry.instance().removeAllMapLayers()
118+
66119
def test_ValueRelation_representValue(self):
67120

68121
first_layer = QgsVectorLayer("none?field=foreign_key:integer",

0 commit comments

Comments
 (0)