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