1 /***************************************************************************
2 qgsprocessingaggregatewidgets.cpp
3 ---------------------
4 Date : June 2020
5 Copyright : (C) 2020 by Nyall Dawson
6 Email : nyall dot dawson at gmail dot com
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
16 #include "qgsprocessingaggregatewidgets.h"
17 #include "qgsexpressioncontextutils.h"
18 #include "qgsfieldexpressionwidget.h"
19 #include "qgsfieldmappingwidget.h"
20
21 #include <QBoxLayout>
22 #include <QLineEdit>
23 #include <QMessageBox>
24 #include <QPushButton>
25 #include <QStandardItemModel>
26 #include <QToolButton>
27 #include <QTableView>
28 #include <mutex>
29
30 //
31 // QgsAggregateMappingModel
32 //
33
QgsAggregateMappingModel(const QgsFields & sourceFields,QObject * parent)34 QgsAggregateMappingModel::QgsAggregateMappingModel( const QgsFields &sourceFields,
35 QObject *parent )
36 : QAbstractTableModel( parent )
37 , mExpressionContextGenerator( new QgsFieldMappingModel::ExpressionContextGenerator( sourceFields ) )
38 {
39 setSourceFields( sourceFields );
40 }
41
headerData(int section,Qt::Orientation orientation,int role) const42 QVariant QgsAggregateMappingModel::headerData( int section, Qt::Orientation orientation, int role ) const
43 {
44 if ( role == Qt::DisplayRole )
45 {
46 switch ( orientation )
47 {
48 case Qt::Horizontal:
49 {
50 switch ( static_cast<ColumnDataIndex>( section ) )
51 {
52 case ColumnDataIndex::SourceExpression:
53 {
54 return tr( "Source Expression" );
55 }
56 case ColumnDataIndex::Aggregate:
57 {
58 return tr( "Aggregate Function" );
59 }
60 case ColumnDataIndex::Delimiter:
61 {
62 return tr( "Delimiter" );
63 }
64 case ColumnDataIndex::DestinationName:
65 {
66 return tr( "Name" );
67 }
68 case ColumnDataIndex::DestinationType:
69 {
70 return tr( "Type" );
71 }
72 case ColumnDataIndex::DestinationLength:
73 {
74 return tr( "Length" );
75 }
76 case ColumnDataIndex::DestinationPrecision:
77 {
78 return tr( "Precision" );
79 }
80 }
81 break;
82 }
83 case Qt::Vertical:
84 {
85 return section;
86 }
87 }
88 }
89 return QVariant();
90 }
91
sourceFields() const92 QgsFields QgsAggregateMappingModel::sourceFields() const
93 {
94 return mSourceFields;
95 }
96
rowCount(const QModelIndex & parent) const97 int QgsAggregateMappingModel::rowCount( const QModelIndex &parent ) const
98 {
99 if ( parent.isValid() )
100 return 0;
101 return mMapping.count();
102 }
103
columnCount(const QModelIndex & parent) const104 int QgsAggregateMappingModel::columnCount( const QModelIndex &parent ) const
105 {
106 if ( parent.isValid() )
107 return 0;
108 return 7;
109 }
110
data(const QModelIndex & index,int role) const111 QVariant QgsAggregateMappingModel::data( const QModelIndex &index, int role ) const
112 {
113 if ( index.isValid() )
114 {
115 const ColumnDataIndex col { static_cast<ColumnDataIndex>( index.column() ) };
116 const Aggregate &agg { mMapping.at( index.row() ) };
117
118 switch ( role )
119 {
120 case Qt::DisplayRole:
121 case Qt::EditRole:
122 {
123 switch ( col )
124 {
125 case ColumnDataIndex::SourceExpression:
126 {
127 return agg.source;
128 }
129 case ColumnDataIndex::Aggregate:
130 {
131 return agg.aggregate;
132 }
133 case ColumnDataIndex::Delimiter:
134 {
135 return agg.delimiter;
136 }
137 case ColumnDataIndex::DestinationName:
138 {
139 return agg.field.displayName();
140 }
141 case ColumnDataIndex::DestinationType:
142 {
143 return static_cast<int>( agg.field.type() );
144 }
145 case ColumnDataIndex::DestinationLength:
146 {
147 return agg.field.length();
148 }
149 case ColumnDataIndex::DestinationPrecision:
150 {
151 return agg.field.precision();
152 }
153 }
154 break;
155 }
156 }
157 }
158 return QVariant();
159 }
160
flags(const QModelIndex & index) const161 Qt::ItemFlags QgsAggregateMappingModel::flags( const QModelIndex &index ) const
162 {
163 if ( index.isValid() )
164 {
165 return Qt::ItemFlags( Qt::ItemIsSelectable |
166 Qt::ItemIsEditable |
167 Qt::ItemIsEnabled );
168 }
169 return Qt::ItemFlags();
170 }
171
setData(const QModelIndex & index,const QVariant & value,int role)172 bool QgsAggregateMappingModel::setData( const QModelIndex &index, const QVariant &value, int role )
173 {
174 if ( index.isValid() )
175 {
176 if ( role == Qt::EditRole )
177 {
178 Aggregate &f = mMapping[index.row()];
179 switch ( static_cast<ColumnDataIndex>( index.column() ) )
180 {
181 case ColumnDataIndex::SourceExpression:
182 {
183 const QgsExpression exp { value.toString() };
184 f.source = exp;
185 break;
186 }
187 case ColumnDataIndex::Aggregate:
188 {
189 f.aggregate = value.toString();
190 break;
191 }
192 case ColumnDataIndex::Delimiter:
193 {
194 f.delimiter = value.toString();
195 break;
196 }
197 case ColumnDataIndex::DestinationName:
198 {
199 f.field.setName( value.toString() );
200 break;
201 }
202 case ColumnDataIndex::DestinationType:
203 {
204 f.field.setType( static_cast<QVariant::Type>( value.toInt( ) ) );
205 break;
206 }
207 case ColumnDataIndex::DestinationLength:
208 {
209 bool ok;
210 const int length { value.toInt( &ok ) };
211 if ( ok )
212 f.field.setLength( length );
213 break;
214 }
215 case ColumnDataIndex::DestinationPrecision:
216 {
217 bool ok;
218 const int precision { value.toInt( &ok ) };
219 if ( ok )
220 f.field.setPrecision( precision );
221 break;
222 }
223 }
224 emit dataChanged( index, index );
225 }
226 return true;
227 }
228 else
229 {
230 return false;
231 }
232 }
233
234
moveUpOrDown(const QModelIndex & index,bool up)235 bool QgsAggregateMappingModel::moveUpOrDown( const QModelIndex &index, bool up )
236 {
237 if ( ! index.isValid() && index.model() == this )
238 return false;
239
240 // Always swap down
241 const int row { up ? index.row() - 1 : index.row() };
242 // Range checking
243 if ( row < 0 || row + 1 >= rowCount( QModelIndex() ) )
244 {
245 return false;
246 }
247 beginMoveRows( QModelIndex( ), row, row, QModelIndex(), row + 2 );
248 #if QT_VERSION < QT_VERSION_CHECK(5, 13, 0)
249 mMapping.swap( row, row + 1 );
250 #else
251 mMapping.swapItemsAt( row, row + 1 );
252 #endif
253 endMoveRows();
254 return true;
255 }
256
setSourceFields(const QgsFields & sourceFields)257 void QgsAggregateMappingModel::setSourceFields( const QgsFields &sourceFields )
258 {
259 mSourceFields = sourceFields;
260 if ( mExpressionContextGenerator )
261 mExpressionContextGenerator->setSourceFields( mSourceFields );
262
263 const QStringList usedFields;
264 beginResetModel();
265 mMapping.clear();
266
267 for ( const QgsField &f : sourceFields )
268 {
269 Aggregate aggregate;
270 aggregate.field = f;
271 aggregate.source = QgsExpression::quotedColumnRef( f.name() );
272
273 if ( f.isNumeric() )
274 aggregate.aggregate = QStringLiteral( "sum" );
275 else if ( f.type() == QVariant::String || ( f.type() == QVariant::List && f.subType() == QVariant::String ) )
276 aggregate.aggregate = QStringLiteral( "concatenate" );
277
278 aggregate.delimiter = ',';
279
280 mMapping.push_back( aggregate );
281 }
282 endResetModel();
283 }
284
contextGenerator() const285 QgsExpressionContextGenerator *QgsAggregateMappingModel::contextGenerator() const
286 {
287 return mExpressionContextGenerator.get();
288 }
289
setBaseExpressionContextGenerator(const QgsExpressionContextGenerator * generator)290 void QgsAggregateMappingModel::setBaseExpressionContextGenerator( const QgsExpressionContextGenerator *generator )
291 {
292 mExpressionContextGenerator->setBaseExpressionContextGenerator( generator );
293 }
294
mapping() const295 QList<QgsAggregateMappingModel::Aggregate> QgsAggregateMappingModel::mapping() const
296 {
297 return mMapping;
298 }
299
setMapping(const QList<QgsAggregateMappingModel::Aggregate> & mapping)300 void QgsAggregateMappingModel::setMapping( const QList<QgsAggregateMappingModel::Aggregate> &mapping )
301 {
302 beginResetModel();
303 mMapping = mapping;
304 endResetModel();
305 }
306
appendField(const QgsField & field,const QString & source,const QString & aggregate)307 void QgsAggregateMappingModel::appendField( const QgsField &field, const QString &source, const QString &aggregate )
308 {
309 const int lastRow { rowCount( QModelIndex( ) ) };
310 beginInsertRows( QModelIndex(), lastRow, lastRow );
311 Aggregate agg;
312 agg.field = field;
313 agg.source = source;
314 agg.aggregate = aggregate;
315 agg.delimiter = ',';
316 mMapping.push_back( agg );
317 endInsertRows( );
318 }
319
removeField(const QModelIndex & index)320 bool QgsAggregateMappingModel::removeField( const QModelIndex &index )
321 {
322 if ( index.isValid() && index.model() == this && index.row() < rowCount( QModelIndex() ) )
323 {
324 beginRemoveRows( QModelIndex(), index.row(), index.row() );
325 mMapping.removeAt( index.row() );
326 endRemoveRows();
327 return true;
328 }
329 else
330 {
331 return false;
332 }
333 }
334
moveUp(const QModelIndex & index)335 bool QgsAggregateMappingModel::moveUp( const QModelIndex &index )
336 {
337 return moveUpOrDown( index );
338 }
339
moveDown(const QModelIndex & index)340 bool QgsAggregateMappingModel::moveDown( const QModelIndex &index )
341 {
342 return moveUpOrDown( index, false );
343 }
344
345
346 //
347 // QgsAggregateMappingWidget
348 //
349
QgsAggregateMappingWidget(QWidget * parent,const QgsFields & sourceFields)350 QgsAggregateMappingWidget::QgsAggregateMappingWidget( QWidget *parent,
351 const QgsFields &sourceFields )
352 : QgsPanelWidget( parent )
353 {
354 QVBoxLayout *verticalLayout = new QVBoxLayout();
355 verticalLayout->setContentsMargins( 0, 0, 0, 0 );
356 mTableView = new QTableView();
357 verticalLayout->addWidget( mTableView );
358 setLayout( verticalLayout );
359
360 mModel = new QgsAggregateMappingModel( sourceFields, this );
361 mTableView->setModel( mModel );
362 mTableView->setItemDelegateForColumn( static_cast<int>( QgsAggregateMappingModel::ColumnDataIndex::SourceExpression ), new QgsFieldMappingWidget::ExpressionDelegate( this ) );
363 mTableView->setItemDelegateForColumn( static_cast<int>( QgsAggregateMappingModel::ColumnDataIndex::Aggregate ), new QgsAggregateMappingWidget::AggregateDelegate( mTableView ) );
364 mTableView->setItemDelegateForColumn( static_cast<int>( QgsAggregateMappingModel::ColumnDataIndex::DestinationType ), new QgsFieldMappingWidget::TypeDelegate( mTableView ) );
365 updateColumns();
366 // Make sure columns are updated when rows are added
367 connect( mModel, &QgsAggregateMappingModel::rowsInserted, this, [ = ] { updateColumns(); } );
368 connect( mModel, &QgsAggregateMappingModel::modelReset, this, [ = ] { updateColumns(); } );
369 connect( mModel, &QgsAggregateMappingModel::dataChanged, this, &QgsAggregateMappingWidget::changed );
370 connect( mModel, &QgsAggregateMappingModel::rowsInserted, this, &QgsAggregateMappingWidget::changed );
371 connect( mModel, &QgsAggregateMappingModel::rowsRemoved, this, &QgsAggregateMappingWidget::changed );
372 connect( mModel, &QgsAggregateMappingModel::modelReset, this, &QgsAggregateMappingWidget::changed );
373 }
374
model() const375 QgsAggregateMappingModel *QgsAggregateMappingWidget::model() const
376 {
377 return qobject_cast<QgsAggregateMappingModel *>( mModel );
378 }
379
mapping() const380 QList<QgsAggregateMappingModel::Aggregate> QgsAggregateMappingWidget::mapping() const
381 {
382 return model()->mapping();
383 }
384
setMapping(const QList<QgsAggregateMappingModel::Aggregate> & mapping)385 void QgsAggregateMappingWidget::setMapping( const QList<QgsAggregateMappingModel::Aggregate> &mapping )
386 {
387 model()->setMapping( mapping );
388 }
389
selectionModel()390 QItemSelectionModel *QgsAggregateMappingWidget::selectionModel()
391 {
392 return mTableView->selectionModel();
393 }
394
setSourceFields(const QgsFields & sourceFields)395 void QgsAggregateMappingWidget::setSourceFields( const QgsFields &sourceFields )
396 {
397 model()->setSourceFields( sourceFields );
398 }
399
setSourceLayer(QgsVectorLayer * layer)400 void QgsAggregateMappingWidget::setSourceLayer( QgsVectorLayer *layer )
401 {
402 mSourceLayer = layer;
403 }
404
sourceLayer()405 QgsVectorLayer *QgsAggregateMappingWidget::sourceLayer()
406 {
407 return mSourceLayer;
408 }
409
scrollTo(const QModelIndex & index) const410 void QgsAggregateMappingWidget::scrollTo( const QModelIndex &index ) const
411 {
412 mTableView->scrollTo( index );
413 }
414
registerExpressionContextGenerator(const QgsExpressionContextGenerator * generator)415 void QgsAggregateMappingWidget::registerExpressionContextGenerator( const QgsExpressionContextGenerator *generator )
416 {
417 model()->setBaseExpressionContextGenerator( generator );
418 }
419
appendField(const QgsField & field,const QString & source,const QString & aggregate)420 void QgsAggregateMappingWidget::appendField( const QgsField &field, const QString &source, const QString &aggregate )
421 {
422 model()->appendField( field, source, aggregate );
423 }
424
removeSelectedFields()425 bool QgsAggregateMappingWidget::removeSelectedFields()
426 {
427 if ( ! mTableView->selectionModel()->hasSelection() )
428 return false;
429
430 std::list<int> rowsToRemove { selectedRows() };
431 rowsToRemove.reverse();
432 for ( const int row : rowsToRemove )
433 {
434 if ( ! model()->removeField( model()->index( row, 0, QModelIndex() ) ) )
435 {
436 return false;
437 }
438 }
439 return true;
440 }
441
moveSelectedFieldsUp()442 bool QgsAggregateMappingWidget::moveSelectedFieldsUp()
443 {
444 if ( ! mTableView->selectionModel()->hasSelection() )
445 return false;
446
447 const std::list<int> rowsToMoveUp { selectedRows() };
448 for ( const int row : rowsToMoveUp )
449 {
450 if ( ! model()->moveUp( model()->index( row, 0, QModelIndex() ) ) )
451 {
452 return false;
453 }
454 }
455 return true;
456 }
457
moveSelectedFieldsDown()458 bool QgsAggregateMappingWidget::moveSelectedFieldsDown()
459 {
460 if ( ! mTableView->selectionModel()->hasSelection() )
461 return false;
462
463 std::list<int> rowsToMoveDown { selectedRows() };
464 rowsToMoveDown.reverse();
465 for ( const int row : rowsToMoveDown )
466 {
467 if ( ! model()->moveDown( model()->index( row, 0, QModelIndex() ) ) )
468 {
469 return false;
470 }
471 }
472 return true;
473 }
474
updateColumns()475 void QgsAggregateMappingWidget::updateColumns()
476 {
477 for ( int i = 0; i < mModel->rowCount(); ++i )
478 {
479 mTableView->openPersistentEditor( mModel->index( i, static_cast<int>( QgsAggregateMappingModel::ColumnDataIndex::SourceExpression ) ) );
480 mTableView->openPersistentEditor( mModel->index( i, static_cast<int>( QgsAggregateMappingModel::ColumnDataIndex::DestinationType ) ) );
481 mTableView->openPersistentEditor( mModel->index( i, static_cast<int>( QgsAggregateMappingModel::ColumnDataIndex::Aggregate ) ) );
482 }
483
484 for ( int i = 0; i < mModel->columnCount(); ++i )
485 {
486 mTableView->resizeColumnToContents( i );
487 }
488 }
489
selectedRows()490 std::list<int> QgsAggregateMappingWidget::selectedRows()
491 {
492 std::list<int> rows;
493 if ( mTableView->selectionModel()->hasSelection() )
494 {
495 const QModelIndexList constSelection { mTableView->selectionModel()->selectedIndexes() };
496 for ( const QModelIndex &index : constSelection )
497 {
498 rows.push_back( index.row() );
499 }
500 rows.sort();
501 rows.unique();
502 }
503 return rows;
504 }
505
506
507
508 //
509 // AggregateDelegate
510 //
511
AggregateDelegate(QObject * parent)512 QgsAggregateMappingWidget::AggregateDelegate::AggregateDelegate( QObject *parent )
513 : QStyledItemDelegate( parent )
514 {
515 }
516
createEditor(QWidget * parent,const QStyleOptionViewItem & option,const QModelIndex &) const517 QWidget *QgsAggregateMappingWidget::AggregateDelegate::createEditor( QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex & ) const
518 {
519 Q_UNUSED( option )
520 QComboBox *editor = new QComboBox( parent );
521
522 const QStringList aggregateList { QgsAggregateMappingWidget::AggregateDelegate::aggregates() };
523 int i = 0;
524 for ( const QString &aggregate : aggregateList )
525 {
526 editor->addItem( aggregate );
527 editor->setItemData( i, aggregate, Qt::UserRole );
528 ++i;
529 }
530
531 connect( editor,
532 qOverload<int >( &QComboBox::currentIndexChanged ),
533 this,
534 [ = ]( int currentIndex )
535 {
536 Q_UNUSED( currentIndex )
537 const_cast< QgsAggregateMappingWidget::AggregateDelegate *>( this )->emit commitData( editor );
538 } );
539
540 return editor;
541 }
542
setEditorData(QWidget * editor,const QModelIndex & index) const543 void QgsAggregateMappingWidget::AggregateDelegate::setEditorData( QWidget *editor, const QModelIndex &index ) const
544 {
545 QComboBox *editorWidget { qobject_cast<QComboBox *>( editor ) };
546 if ( ! editorWidget )
547 return;
548
549 const QVariant value = index.model()->data( index, Qt::EditRole );
550 editorWidget->setCurrentIndex( editorWidget->findData( value ) );
551 }
552
setModelData(QWidget * editor,QAbstractItemModel * model,const QModelIndex & index) const553 void QgsAggregateMappingWidget::AggregateDelegate::setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const
554 {
555 QComboBox *editorWidget { qobject_cast<QComboBox *>( editor ) };
556 if ( ! editorWidget )
557 return;
558
559 const QVariant currentValue = editorWidget->currentData( );
560 model->setData( index, currentValue, Qt::EditRole );
561 }
562
aggregates()563 const QStringList QgsAggregateMappingWidget::AggregateDelegate::aggregates()
564 {
565 static QStringList sAggregates;
566 static std::once_flag initialized;
567 std::call_once( initialized, [ = ]( )
568 {
569 sAggregates << QStringLiteral( "first_value" )
570 << QStringLiteral( "last_value" );
571
572 const QList<QgsExpressionFunction *> functions = QgsExpression::Functions();
573 for ( const QgsExpressionFunction *function : functions )
574 {
575 if ( !function || function->isDeprecated() || function->name().isEmpty() || function->name().at( 0 ) == '_' )
576 continue;
577
578 if ( function->groups().contains( QLatin1String( "Aggregates" ) ) )
579 {
580 if ( function->name() == QLatin1String( "aggregate" )
581 || function->name() == QLatin1String( "relation_aggregate" ) )
582 continue;
583
584 sAggregates.append( function->name() );
585 }
586
587 std::sort( sAggregates.begin(), sAggregates.end() );
588 }
589 } );
590
591 return sAggregates;
592 }
593
594