QGIS API Documentation 3.43.0-Master (c67cf405802)
qgsvaluemapconfigdlg.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsvaluemapconfigdlg.cpp
3 --------------------------------------
4 Date : 5.1.2014
5 Copyright : (C) 2014 Matthias Kuhn
6 Email : matthias at opengis dot ch
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
17#include "moc_qgsvaluemapconfigdlg.cpp"
18
21#include "qgsapplication.h"
22#include "qgssettings.h"
23
24#include <QFileDialog>
25#include <QMessageBox>
26#include <QTextStream>
27#include <QClipboard>
28#include <QKeyEvent>
29#include <QMimeData>
30#include <QRegularExpression>
31
33 : QgsEditorConfigWidget( vl, fieldIdx, parent )
34{
35 setupUi( this );
36
37 mValueMapErrorsLabel->setVisible( false );
38 mValueMapErrorsLabel->setStyleSheet( QStringLiteral( "QLabel { color : red; }" ) );
39
40 tableWidget->insertRow( 0 );
41
42 tableWidget->horizontalHeader()->setSectionsClickable( true );
43 tableWidget->setSortingEnabled( true );
44
45 connect( addNullButton, &QAbstractButton::clicked, this, &QgsValueMapConfigDlg::addNullButtonPushed );
46 connect( removeSelectedButton, &QAbstractButton::clicked, this, &QgsValueMapConfigDlg::removeSelectedButtonPushed );
47 connect( loadFromLayerButton, &QAbstractButton::clicked, this, &QgsValueMapConfigDlg::loadFromLayerButtonPushed );
48 connect( loadFromCSVButton, &QAbstractButton::clicked, this, &QgsValueMapConfigDlg::loadFromCSVButtonPushed );
49 connect( tableWidget, &QTableWidget::cellChanged, this, &QgsValueMapConfigDlg::vCellChanged );
50 tableWidget->installEventFilter( this );
51}
52
54{
55 QList<QVariant> valueList;
56
57 //store data to map
58 for ( int i = 0; i < tableWidget->rowCount() - 1; i++ )
59 {
60 QTableWidgetItem *ki = tableWidget->item( i, 0 );
61 QTableWidgetItem *vi = tableWidget->item( i, 1 );
62
63 if ( !ki )
64 continue;
65
66 QString ks = ki->text();
67 if ( ( ks == QgsApplication::nullRepresentation() ) && !( ki->flags() & Qt::ItemIsEditable ) )
69
70 QVariantMap value;
71
72 if ( !vi || vi->text().isNull() )
73 {
74 value.insert( ks, ks );
75 }
76 else
77 {
78 value.insert( vi->text(), ks );
79 }
80 valueList.append( value );
81 }
82
83 QVariantMap cfg;
84 cfg.insert( QStringLiteral( "map" ), valueList );
85 return cfg;
86}
87
88void QgsValueMapConfigDlg::setConfig( const QVariantMap &config )
89{
90 tableWidget->clearContents();
91 for ( int i = tableWidget->rowCount() - 1; i > 0; i-- )
92 {
93 tableWidget->removeRow( i );
94 }
95
96 QList<QVariant> valueList = config.value( QStringLiteral( "map" ) ).toList();
97 QList<QPair<QString, QVariant>> orderedList;
98
99 if ( valueList.count() > 0 )
100 {
101 for ( int i = 0, row = 0; i < valueList.count(); i++, row++ )
102 {
103 orderedList.append( qMakePair( valueList[i].toMap().constBegin().value().toString(), valueList[i].toMap().constBegin().key() ) );
104 }
105 }
106 else
107 {
108 int row = 0;
109 const QVariantMap values = config.value( QStringLiteral( "map" ) ).toMap();
110 for ( QVariantMap::ConstIterator mit = values.constBegin(); mit != values.constEnd(); mit++, row++ )
111 {
112 if ( QgsVariantUtils::isNull( mit.value() ) )
113 orderedList.append( qMakePair( mit.key(), QVariant() ) );
114 else
115 orderedList.append( qMakePair( mit.value().toString(), mit.key() ) );
116 }
117 }
118
119 updateMap( orderedList, false );
120}
121
122void QgsValueMapConfigDlg::vCellChanged( int row, int column )
123{
124 Q_UNUSED( column )
125 if ( row == tableWidget->rowCount() - 1 )
126 {
127 tableWidget->insertRow( row + 1 );
128 } //else check type
129
130 if ( layer()->fields().exists( field() ) )
131 {
132 // check cell value
133 QTableWidgetItem *item = tableWidget->item( row, 0 );
134 if ( item )
135 {
136 const QString validValue = checkValueLength( item->text() );
137 if ( validValue.length() != item->text().length() )
138 {
139 const QString errorMessage = tr( "Value '%1' has been trimmed (maximum field length: %2)" )
140 .arg( item->text(), QString::number( layer()->fields().field( field() ).length() ) );
141 item->setText( validValue );
142 mValueMapErrorsLabel->setVisible( true );
143 mValueMapErrorsLabel->setText( QStringLiteral( "%1<br>%2" ).arg( errorMessage, mValueMapErrorsLabel->text() ) );
144 }
145 }
146 }
147
148 emit changed();
149}
150
151void QgsValueMapConfigDlg::removeSelectedButtonPushed()
152{
153 QList<QTableWidgetItem *> list = tableWidget->selectedItems();
154 QSet<int> rowsToRemove;
155 int removed = 0;
156 int i;
157 for ( i = 0; i < list.size(); i++ )
158 {
159 if ( list[i]->column() == 0 )
160 {
161 const int row = list[i]->row();
162 if ( !rowsToRemove.contains( row ) )
163 {
164 rowsToRemove.insert( row );
165 }
166 }
167 }
168 for ( const int rowToRemoved : rowsToRemove )
169 {
170 tableWidget->removeRow( rowToRemoved - removed );
171 removed++;
172 }
173 emit changed();
174}
175
176void QgsValueMapConfigDlg::updateMap( const QMap<QString, QVariant> &map, bool insertNull )
177{
178 QList<QPair<QString, QVariant>> orderedMap;
179 const auto end = map.constEnd();
180 for ( auto it = map.constBegin(); it != end; ++it )
181 {
182 orderedMap.append( qMakePair( it.key(), it.value() ) );
183 }
184
185 updateMap( orderedMap, insertNull );
186}
187
188void QgsValueMapConfigDlg::updateMap( const QList<QPair<QString, QVariant>> &list, bool insertNull )
189{
190 tableWidget->clearContents();
191 mValueMapErrorsLabel->setVisible( false );
192
193 for ( int i = tableWidget->rowCount() - 1; i > 0; i-- )
194 {
195 tableWidget->removeRow( i );
196 }
197 int row = 0;
198
199 if ( insertNull )
200 {
201 setRow( row, QgsValueMapFieldFormatter::NULL_VALUE, QStringLiteral( "<NULL>" ) );
202 ++row;
203 }
204
205 constexpr int maxOverflowErrors { 5 };
206 QStringList reportedErrors;
207 const bool hasField { layer()->fields().exists( field() ) };
208 const QgsField mappedField { hasField ? layer()->fields().field( field() ) : QgsField() };
209
210 for ( const auto &pair : list )
211 {
212 if ( QgsVariantUtils::isNull( pair.second ) )
213 setRow( row, pair.first, QString() );
214 else
215 {
216 const QString value { pair.first };
217 // Check value
218 const QString validValue = checkValueLength( value );
219
220 if ( validValue.length() != value.length() )
221 {
222 if ( reportedErrors.length() < maxOverflowErrors )
223 {
224 reportedErrors.push_back( tr( "Value '%1' has been trimmed (maximum field length: %2)" )
225 .arg( value, QString::number( mappedField.length() ) ) );
226 }
227 else if ( reportedErrors.length() == maxOverflowErrors )
228 {
229 reportedErrors.push_back( tr( "Only first %1 errors have been reported." )
230 .arg( maxOverflowErrors ) );
231 }
232 }
233
234 setRow( row, validValue, pair.second.toString() );
235
236 // Show errors if any
237 if ( !reportedErrors.isEmpty() )
238 {
239 mValueMapErrorsLabel->setVisible( true );
240 mValueMapErrorsLabel->setText( reportedErrors.join( QLatin1String( "<br>" ) ) );
241 }
242 }
243 ++row;
244 }
245}
246
247QString QgsValueMapConfigDlg::checkValueLength( const QString &value )
248{
250 {
251 return value;
252 }
253
254 if ( layer()->fields().exists( field() ) )
255 {
256 const QgsField mappedField { layer()->fields().field( field() ) };
257 if ( mappedField.length() > 0 && value.length() > mappedField.length() )
258 {
259 return value.mid( 0, mappedField.length() );
260 }
261 }
262 return value;
263}
264
265void QgsValueMapConfigDlg::populateComboBox( QComboBox *comboBox, const QVariantMap &config, bool skipNull )
266{
267 const QList<QVariant> valueList = config.value( QStringLiteral( "map" ) ).toList();
268
269 if ( !valueList.empty() )
270 {
271 for ( const QVariant &value : valueList )
272 {
273 const QVariantMap valueMap = value.toMap();
274
275 if ( skipNull && valueMap.constBegin().value() == QgsValueMapFieldFormatter::NULL_VALUE )
276 continue;
277
278 comboBox->addItem( valueMap.constBegin().key(), valueMap.constBegin().value() );
279 }
280 }
281 else
282 {
283 const QVariantMap map = config.value( QStringLiteral( "map" ) ).toMap();
284 for ( auto it = map.constBegin(); it != map.constEnd(); ++it )
285 {
286 if ( skipNull && it.value() == QgsValueMapFieldFormatter::NULL_VALUE )
287 continue;
288
289 comboBox->addItem( it.key(), it.value() );
290 }
291 }
292}
293
294bool QgsValueMapConfigDlg::eventFilter( QObject *watched, QEvent *event )
295{
296 Q_UNUSED( watched )
297 if ( event->type() == QEvent::KeyPress )
298 {
299 QKeyEvent *keyEvent = static_cast<QKeyEvent *>( event );
300 if ( keyEvent->matches( QKeySequence::Copy ) )
301 {
302 copySelectionToClipboard();
303 event->accept();
304 return true;
305 }
306 }
307 return false;
308}
309
310void QgsValueMapConfigDlg::setRow( int row, const QString &value, const QString &description )
311{
312 const QgsSettings settings;
313 QTableWidgetItem *valueCell = nullptr;
314 QTableWidgetItem *descriptionCell = new QTableWidgetItem( description );
315 tableWidget->insertRow( row );
317 {
318 QFont cellFont;
319 cellFont.setItalic( true );
320 valueCell = new QTableWidgetItem( QgsApplication::nullRepresentation() );
321 valueCell->setFont( cellFont );
322 valueCell->setFlags( Qt::ItemIsSelectable | Qt::ItemIsEnabled );
323 descriptionCell->setFont( cellFont );
324 }
325 else
326 {
327 valueCell = new QTableWidgetItem( value );
328 }
329 tableWidget->setItem( row, 0, valueCell );
330 tableWidget->setItem( row, 1, descriptionCell );
331}
332
333void QgsValueMapConfigDlg::copySelectionToClipboard()
334{
335 QAbstractItemModel *model = tableWidget->model();
336 QItemSelectionModel *selection = tableWidget->selectionModel();
337 const QModelIndexList indexes = selection->selectedIndexes();
338
339 QString clipboardText;
340 QModelIndex previous = indexes.first();
341 auto mimeData = std::make_unique<QMimeData>();
342 for ( const QModelIndex &current : indexes )
343 {
344 const QString text = model->data( current ).toString();
345 if ( current.row() != previous.row() )
346 {
347 clipboardText.append( '\n' );
348 }
349 else if ( current.column() != previous.column() )
350 {
351 clipboardText.append( '\t' );
352 }
353 clipboardText.append( text );
354 previous = current;
355 }
356 mimeData->setData( QStringLiteral( "text/plain" ), clipboardText.toUtf8() );
357 QApplication::clipboard()->setMimeData( mimeData.release() );
358}
359
360void QgsValueMapConfigDlg::addNullButtonPushed()
361{
362 setRow( tableWidget->rowCount() - 1, QgsValueMapFieldFormatter::NULL_VALUE, QStringLiteral( "<NULL>" ) );
363}
364
365void QgsValueMapConfigDlg::loadFromLayerButtonPushed()
366{
367 QgsAttributeTypeLoadDialog layerDialog( layer() );
368 if ( !layerDialog.exec() )
369 return;
370
371 updateMap( layerDialog.valueMap(), layerDialog.insertNull() );
372}
373
374void QgsValueMapConfigDlg::loadFromCSVButtonPushed()
375{
376 const QgsSettings settings;
377
378 const QString fileName = QFileDialog::getOpenFileName( nullptr, tr( "Load Value Map from File" ), QDir::homePath() );
379 if ( fileName.isNull() )
380 return;
381 loadMapFromCSV( fileName );
382}
383
384void QgsValueMapConfigDlg::loadMapFromCSV( const QString &filePath )
385{
386 QFile f( filePath );
387
388 if ( !f.open( QIODevice::ReadOnly ) )
389 {
390 QMessageBox::information( nullptr, tr( "Load Value Map from File" ), tr( "Could not open file %1\nError was: %2" ).arg( filePath, f.errorString() ), QMessageBox::Cancel );
391 return;
392 }
393
394 QTextStream s( &f );
395 s.setAutoDetectUnicode( true );
396
397 const thread_local QRegularExpression re( "(?:^\"|[;,]\")(\"\"|[\\w\\W]*?)(?=\"[;,]|\"$)|(?:^(?!\")|[;,](?!\"))([^;,]*?)(?=$|[;,])|(\\r\\n|\\n)" );
398 QList<QPair<QString, QVariant>> map;
399 while ( !s.atEnd() )
400 {
401 const QString l = s.readLine().trimmed();
402 QRegularExpressionMatchIterator matches = re.globalMatch( l );
403 QStringList ceils;
404 while ( matches.hasNext() && ceils.size() < 2 )
405 {
406 const QRegularExpressionMatch match = matches.next();
407 ceils << match.capturedTexts().last().trimmed().replace( QLatin1String( "\"\"" ), QLatin1String( "\"" ) );
408 }
409
410 if ( ceils.size() != 2 )
411 continue;
412
413 QString key = ceils[0];
414 QString val = ceils[1];
417 map.append( qMakePair( key, val ) );
418 }
419
420 updateMap( map, false );
421}
static QString nullRepresentation()
Returns the string used to represent the value NULL throughout QGIS.
A dialog which allows populating value maps using features present in a vector layer.
Base class for widgets which configure editor widget types.
int field()
Returns the field for which this configuration widget applies.
QgsVectorLayer * layer()
Returns the layer for which this configuration widget applies.
void changed()
Emitted when the configuration of the widget is changed.
Encapsulate a field in an attribute table or data source.
Definition qgsfield.h:53
QgsField field(int fieldIdx) const
Returns the field at particular index (must be in range 0..N-1).
Q_INVOKABLE bool exists(int i) const
Returns if a field index is valid.
Stores settings for use within QGIS.
Definition qgssettings.h:66
void loadMapFromCSV(const QString &filePath)
Updates the displayed table with the values from a CSV file.
void setConfig(const QVariantMap &config) override
Update the configuration widget to represent the given configuration.
bool eventFilter(QObject *watched, QEvent *event) override
QgsValueMapConfigDlg(QgsVectorLayer *vl, int fieldIdx, QWidget *parent)
void updateMap(const QMap< QString, QVariant > &map, bool insertNull)
Updates the displayed table with the values from map.
static void populateComboBox(QComboBox *comboBox, const QVariantMap &configuration, bool skipNull)
Populates a comboBox with the appropriate entries based on a value map configuration.
QVariantMap config() override
Create a configuration from the current GUI state.
static const QString NULL_VALUE
Will be saved in the configuration when a value is NULL.
static bool isNull(const QVariant &variant, bool silenceNullWarnings=false)
Returns true if the specified variant should be considered a NULL value.
Represents a vector layer which manages a vector based dataset.