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