QGIS API Documentation  3.20.0-Odense (decaadbb31)
qgsexpressiontreeview.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgsexpressiontreeview.cpp
3  --------------------------------------
4  Date : march 2020 - quarantine day 9
5  Copyright : (C) 2020 by Denis Rouzaud
6  Email : [email protected]
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 <QMenu>
17 #include <QMessageBox>
18 #include <QVersionNumber>
19 
20 #include "qgsexpressiontreeview.h"
21 #include "qgis.h"
23 #include "qgsvectorlayer.h"
25 #include "qgssettings.h"
26 #include "qgsrelationmanager.h"
27 #include "qgsapplication.h"
28 #include "qgsiconutils.h"
29 
30 
32 QString formatRelationHelp( const QgsRelation &relation )
33 {
34  QString text = QStringLiteral( "<h3>%1</h3>\n<div class=\"description\"><p>%2</p></div>" )
35  .arg( QCoreApplication::translate( "relation_help", "relation %1" ).arg( relation.name() ),
36  QObject::tr( "Inserts the relation ID for the relation named '%1'." ).arg( relation.name() ) );
37 
38  text += QStringLiteral( "<h4>%1</h4><div class=\"description\"><pre>%2</pre></div>" )
39  .arg( QObject::tr( "Current value" ), relation.id() );
40 
41  return text;
42 }
43 
44 
46 QString formatLayerHelp( const QgsMapLayer *layer )
47 {
48  QString text = QStringLiteral( "<h3>%1</h3>\n<div class=\"description\"><p>%2</p></div>" )
49  .arg( QCoreApplication::translate( "layer_help", "map layer %1" ).arg( layer->name() ),
50  QObject::tr( "Inserts the layer ID for the layer named '%1'." ).arg( layer->name() ) );
51 
52  text += QStringLiteral( "<h4>%1</h4><div class=\"description\"><pre>%2</pre></div>" )
53  .arg( QObject::tr( "Current value" ), layer->id() );
54 
55  return text;
56 }
57 
59 QString formatRecentExpressionHelp( const QString &label, const QString &expression )
60 {
61  QString text = QStringLiteral( "<h3>%1</h3>\n<div class=\"description\"><p>%2</p></div>" )
62  .arg( QCoreApplication::translate( "recent_expression_help", "expression %1" ).arg( label ),
63  QCoreApplication::translate( "recent_expression_help", "Recently used expression." ) );
64 
65  text += QStringLiteral( "<h4>%1</h4><div class=\"description\"><pre>%2</pre></div>" )
66  .arg( QObject::tr( "Expression" ), expression );
67 
68  return text;
69 }
70 
72 QString formatUserExpressionHelp( const QString &label, const QString &expression, const QString &description )
73 {
74  QString text = QStringLiteral( "<h3>%1</h3>\n<div class=\"description\"><p>%2</p></div>" )
75  .arg( QCoreApplication::translate( "user_expression_help", "expression %1" ).arg( label ), description );
76 
77  text += QStringLiteral( "<h4>%1</h4><div class=\"description\"><pre>%2</pre></div>" )
78  .arg( QObject::tr( "Expression" ), expression );
79 
80  return text;
81 }
82 
84 QString formatVariableHelp( const QString &variable, const QString &description, bool showValue, const QVariant &value )
85 {
86  QString text = QStringLiteral( "<h3>%1</h3>\n<div class=\"description\"><p>%2</p></div>" )
87  .arg( QCoreApplication::translate( "variable_help", "variable %1" ).arg( variable ), description );
88 
89  if ( showValue )
90  {
91  QString valueString = !value.isValid()
92  ? QCoreApplication::translate( "variable_help", "not set" )
93  : QStringLiteral( "<pre>%1</pre>" ).arg( QgsExpression::formatPreviewString( value ) );
94 
95  text += QStringLiteral( "<h4>%1</h4><div class=\"description\"><p>%2</p></div>" )
96  .arg( QObject::tr( "Current value" ), valueString );
97  }
98 
99  return text;
100 }
101 
102 
103 // ****************************
104 // ****************************
105 // QgsExpressionTreeView
106 // ****************************
107 
108 
110  : QTreeView( parent )
111  , mProject( QgsProject::instance() )
112 {
113  connect( this, &QTreeView::doubleClicked, this, &QgsExpressionTreeView::onDoubleClicked );
114 
115  mModel = std::make_unique<QStandardItemModel>();
116  mProxyModel = std::make_unique<QgsExpressionItemSearchProxy>();
117  mProxyModel->setDynamicSortFilter( true );
118  mProxyModel->setSourceModel( mModel.get() );
119  setModel( mProxyModel.get() );
120  setSortingEnabled( true );
121  sortByColumn( 0, Qt::AscendingOrder );
122 
123  setSelectionMode( QAbstractItemView::SelectionMode::SingleSelection );
124 
125  setContextMenuPolicy( Qt::CustomContextMenu );
126  connect( this, &QWidget::customContextMenuRequested, this, &QgsExpressionTreeView::showContextMenu );
127  connect( selectionModel(), &QItemSelectionModel::currentChanged, this, &QgsExpressionTreeView::currentItemChanged );
128 
129  updateFunctionTree();
131 
132  // select the first item in the function list
133  // in order to avoid a blank help widget
134  QModelIndex firstItem = mProxyModel->index( 0, 0, QModelIndex() );
135  setCurrentIndex( firstItem );
136 }
137 
139 {
140  mLayer = layer;
141 
142  //TODO - remove existing layer scope from context
143 
144  if ( mLayer )
145  mExpressionContext << QgsExpressionContextUtils::layerScope( mLayer );
146 
147  loadFieldNames();
148 }
149 
151 {
152  mExpressionContext = context;
153  updateFunctionTree();
154  loadFieldNames();
155  loadRecent( mRecentKey );
157 }
158 
160 {
161  mMenuProvider = provider;
162 }
163 
165 {
166  updateFunctionTree();
167  loadFieldNames();
168  loadRecent( mRecentKey );
170 }
171 
173 {
174  QModelIndex idx = mProxyModel->mapToSource( currentIndex() );
175  QgsExpressionItem *item = static_cast<QgsExpressionItem *>( mModel->itemFromIndex( idx ) );
176  return item;
177 }
178 
179 QStandardItemModel *QgsExpressionTreeView::model()
180 {
181  return mModel.get();
182 }
183 
185 {
186  return mProject;
187 }
188 
190 {
191  mProject = project;
192  updateFunctionTree();
193 }
194 
195 
196 void QgsExpressionTreeView::setSearchText( const QString &text )
197 {
198  mProxyModel->setFilterWildcard( text );
199  if ( text.isEmpty() )
200  {
201  collapseAll();
202  }
203  else
204  {
205  expandAll();
206  QModelIndex index = mProxyModel->index( 0, 0 );
207  if ( mProxyModel->hasChildren( index ) )
208  {
209  QModelIndex child = mProxyModel->index( 0, 0, index );
210  selectionModel()->setCurrentIndex( child, QItemSelectionModel::ClearAndSelect );
211  }
212  }
213 }
214 
215 void QgsExpressionTreeView::onDoubleClicked( const QModelIndex &index )
216 {
217  QModelIndex idx = mProxyModel->mapToSource( index );
218  QgsExpressionItem *item = static_cast<QgsExpressionItem *>( mModel->itemFromIndex( idx ) );
219  if ( !item )
220  return;
221 
222  // Don't handle the double-click if we are on a header node.
223  if ( item->getItemType() == QgsExpressionItem::Header )
224  return;
225 
227 }
228 
229 void QgsExpressionTreeView::showContextMenu( QPoint pt )
230 {
231  QModelIndex idx = indexAt( pt );
232  idx = mProxyModel->mapToSource( idx );
233  QgsExpressionItem *item = static_cast<QgsExpressionItem *>( mModel->itemFromIndex( idx ) );
234  if ( !item )
235  return;
236 
237  if ( !mMenuProvider )
238  return;
239 
240  QMenu *menu = mMenuProvider->createContextMenu( item );
241 
242  if ( menu )
243  menu->popup( mapToGlobal( pt ) );
244 }
245 
246 void QgsExpressionTreeView::currentItemChanged( const QModelIndex &index, const QModelIndex & )
247 {
248  // Get the item
249  QModelIndex idx = mProxyModel->mapToSource( index );
250  QgsExpressionItem *item = static_cast<QgsExpressionItem *>( mModel->itemFromIndex( idx ) );
251  if ( !item )
252  return;
253 
254  emit currentExpressionItemChanged( item );
255 }
256 
257 void QgsExpressionTreeView::updateFunctionTree()
258 {
259  mModel->clear();
260  mExpressionGroups.clear();
261 
262  // TODO Can we move this stuff to QgsExpression, like the functions?
263  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "+" ), QStringLiteral( " + " ) );
264  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "-" ), QStringLiteral( " - " ) );
265  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "*" ), QStringLiteral( " * " ) );
266  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "/" ), QStringLiteral( " / " ) );
267  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "%" ), QStringLiteral( " % " ) );
268  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "^" ), QStringLiteral( " ^ " ) );
269  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "=" ), QStringLiteral( " = " ) );
270  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "~" ), QStringLiteral( " ~ " ) );
271  registerItem( QStringLiteral( "Operators" ), QStringLiteral( ">" ), QStringLiteral( " > " ) );
272  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "<" ), QStringLiteral( " < " ) );
273  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "<>" ), QStringLiteral( " <> " ) );
274  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "<=" ), QStringLiteral( " <= " ) );
275  registerItem( QStringLiteral( "Operators" ), QStringLiteral( ">=" ), QStringLiteral( " >= " ) );
276  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "[]" ), QStringLiteral( "[ ]" ) );
277  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "||" ), QStringLiteral( " || " ) );
278  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "IN" ), QStringLiteral( " IN " ) );
279  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "LIKE" ), QStringLiteral( " LIKE " ) );
280  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "ILIKE" ), QStringLiteral( " ILIKE " ) );
281  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "IS" ), QStringLiteral( " IS " ) );
282  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "IS NOT" ), QStringLiteral( " IS NOT " ) );
283  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "OR" ), QStringLiteral( " OR " ) );
284  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "AND" ), QStringLiteral( " AND " ) );
285  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "NOT" ), QStringLiteral( " NOT " ) );
286 
287  QString casestring = QStringLiteral( "CASE WHEN condition THEN result END" );
288  registerItem( QStringLiteral( "Conditionals" ), QStringLiteral( "CASE" ), casestring );
289 
290  // use -1 as sort order here -- NULL should always show before the field list
291  registerItem( QStringLiteral( "Fields and Values" ), QStringLiteral( "NULL" ), QStringLiteral( "NULL" ), QString(), QgsExpressionItem::ExpressionNode, false, -1 );
292 
293  // Load the functions from the QgsExpression class
294  int count = QgsExpression::functionCount();
295  for ( int i = 0; i < count; i++ )
296  {
298  QString name = func->name();
299  if ( name.startsWith( '_' ) ) // do not display private functions
300  continue;
301  if ( func->isDeprecated() ) // don't show deprecated functions
302  continue;
303  if ( func->isContextual() )
304  {
305  //don't show contextual functions by default - it's up the the QgsExpressionContext
306  //object to provide them if supported
307  continue;
308  }
309  if ( func->params() != 0 )
310  name += '(';
311  else if ( !name.startsWith( '$' ) )
312  name += QLatin1String( "()" );
313  // this is where the functions are being registered, including functions under "Custom"
314  registerItemForAllGroups( func->groups(), func->name(), ' ' + name + ' ', func->helpText(), QgsExpressionItem::ExpressionNode, mExpressionContext.isHighlightedFunction( func->name() ), 1, QgsExpression::tags( func->name() ) );
315  }
316 
317  // load relation names
318  loadRelations();
319 
320  // load layer IDs
321  loadLayers();
322 
323  loadExpressionContext();
324 }
325 
326 void QgsExpressionTreeView::registerItem( const QString &group,
327  const QString &label,
328  const QString &expressionText,
329  const QString &helpText,
330  QgsExpressionItem::ItemType type, bool highlightedItem, int sortOrder, QIcon icon, const QStringList &tags, const QString &name )
331 {
332  QgsExpressionItem *item = new QgsExpressionItem( label, expressionText, helpText, type );
333  item->setData( label, Qt::UserRole );
334  item->setData( sortOrder, QgsExpressionItem::CUSTOM_SORT_ROLE );
335  item->setData( tags, QgsExpressionItem::SEARCH_TAGS_ROLE );
336  item->setData( name, QgsExpressionItem::ITEM_NAME_ROLE );
337  item->setIcon( icon );
338 
339  // Look up the group and insert the new function.
340  if ( mExpressionGroups.contains( group ) )
341  {
342  QgsExpressionItem *groupNode = mExpressionGroups.value( group );
343  groupNode->appendRow( item );
344  }
345  else
346  {
347  // If the group doesn't exist yet we make it first.
348  QgsExpressionItem *newgroupNode = new QgsExpressionItem( QgsExpression::group( group ), QString(), QgsExpressionItem::Header );
349  newgroupNode->setData( group, Qt::UserRole );
350  //Recent group should always be last group
351  newgroupNode->setData( group.startsWith( QLatin1String( "Recent (" ) ) ? 2 : 1, QgsExpressionItem::CUSTOM_SORT_ROLE );
352  newgroupNode->appendRow( item );
353  newgroupNode->setBackground( QBrush( QColor( 150, 150, 150, 150 ) ) );
354  mModel->appendRow( newgroupNode );
355  mExpressionGroups.insert( group, newgroupNode );
356  }
357 
358  if ( highlightedItem )
359  {
360  //insert a copy as a top level item
361  QgsExpressionItem *topLevelItem = new QgsExpressionItem( label, expressionText, helpText, type );
362  topLevelItem->setData( label, Qt::UserRole );
363  item->setData( 0, QgsExpressionItem::CUSTOM_SORT_ROLE );
364  QFont font = topLevelItem->font();
365  font.setBold( true );
366  topLevelItem->setFont( font );
367  mModel->appendRow( topLevelItem );
368  }
369 }
370 
371 void QgsExpressionTreeView::registerItemForAllGroups( const QStringList &groups, const QString &label, const QString &expressionText, const QString &helpText, QgsExpressionItem::ItemType type, bool highlightedItem, int sortOrder, const QStringList &tags )
372 {
373  const auto constGroups = groups;
374  for ( const QString &group : constGroups )
375  {
376  registerItem( group, label, expressionText, helpText, type, highlightedItem, sortOrder, QIcon(), tags );
377  }
378 }
379 
380 void QgsExpressionTreeView::loadExpressionContext()
381 {
382  QStringList variableNames = mExpressionContext.filteredVariableNames();
383  const auto constVariableNames = variableNames;
384  for ( const QString &variable : constVariableNames )
385  {
386  registerItem( QStringLiteral( "Variables" ), variable, " @" + variable + ' ',
387  formatVariableHelp( variable, mExpressionContext.description( variable ), true, mExpressionContext.variable( variable ) ),
389  mExpressionContext.isHighlightedVariable( variable ) );
390  }
391 
392  // Load the functions from the expression context
393  QStringList contextFunctions = mExpressionContext.functionNames();
394  const auto constContextFunctions = contextFunctions;
395  for ( const QString &functionName : constContextFunctions )
396  {
397  QgsExpressionFunction *func = mExpressionContext.function( functionName );
398  QString name = func->name();
399  if ( name.startsWith( '_' ) ) // do not display private functions
400  continue;
401  if ( func->params() != 0 )
402  name += '(';
403  registerItemForAllGroups( func->groups(), func->name(), ' ' + name + ' ', func->helpText(), QgsExpressionItem::ExpressionNode, mExpressionContext.isHighlightedFunction( func->name() ), 1, QgsExpression::tags( func->name() ) );
404  }
405 }
406 
407 void QgsExpressionTreeView::loadLayers()
408 {
409  if ( !mProject )
410  return;
411 
412  QMap<QString, QgsMapLayer *> layers = mProject->mapLayers();
413  QMap<QString, QgsMapLayer *>::const_iterator layerIt = layers.constBegin();
414  for ( ; layerIt != layers.constEnd(); ++layerIt )
415  {
416  QIcon icon = QgsIconUtils::iconForLayer( layerIt.value() );
417  registerItem( QStringLiteral( "Map Layers" ), layerIt.value()->name(), QStringLiteral( "'%1'" ).arg( layerIt.key() ), formatLayerHelp( layerIt.value() ), QgsExpressionItem::ExpressionNode, false, 99, icon );
418  }
419 }
420 
422 {
423  for ( int i = 0; i < fields.count(); ++i )
424  {
425  const QgsField field = fields.at( i );
426  QIcon icon = fields.iconForField( i );
427  registerItem( QStringLiteral( "Fields and Values" ), field.displayNameWithAlias(),
428  " \"" + field.name() + "\" ", QString(), QgsExpressionItem::Field, false, i, icon, QStringList(), field.name() );
429  }
430 }
431 
432 void QgsExpressionTreeView::loadFieldNames()
433 {
434  // Cleanup
435  if ( mExpressionGroups.contains( QStringLiteral( "Fields and Values" ) ) )
436  {
437  QgsExpressionItem *node = mExpressionGroups.value( QStringLiteral( "Fields and Values" ) );
438  node->removeRows( 0, node->rowCount() );
439  // Re-add NULL
440  // use -1 as sort order here -- NULL should always show before the field list
441  registerItem( QStringLiteral( "Fields and Values" ), QStringLiteral( "NULL" ), QStringLiteral( "NULL" ), QString(), QgsExpressionItem::ExpressionNode, false, -1 );
442  }
443 
444  // this can happen if fields are manually set
445  if ( !mLayer )
446  return;
447 
448  const QgsFields &fields = mLayer->fields();
449 
450  loadFieldNames( fields );
451 }
452 
453 void QgsExpressionTreeView::loadRelations()
454 {
455  if ( !mProject )
456  return;
457 
458  QMap<QString, QgsRelation> relations = mProject->relationManager()->relations();
459  QMap<QString, QgsRelation>::const_iterator relIt = relations.constBegin();
460  for ( ; relIt != relations.constEnd(); ++relIt )
461  {
462  registerItemForAllGroups( QStringList() << tr( "Relations" ), relIt->name(), QStringLiteral( "'%1'" ).arg( relIt->id() ), formatRelationHelp( relIt.value() ) );
463  }
464 }
465 
466 void QgsExpressionTreeView::loadRecent( const QString &collection )
467 {
468  mRecentKey = collection;
469  QString name = tr( "Recent (%1)" ).arg( collection );
470  if ( mExpressionGroups.contains( name ) )
471  {
472  QgsExpressionItem *node = mExpressionGroups.value( name );
473  node->removeRows( 0, node->rowCount() );
474  }
475 
476  QgsSettings settings;
477  const QString location = QStringLiteral( "/expressions/recent/%1" ).arg( collection );
478  const QStringList expressions = settings.value( location ).toStringList();
479  int i = 0;
480  for ( const QString &expression : expressions )
481  {
482  QString help = formatRecentExpressionHelp( expression, expression );
483  QString label = expression;
484  label.replace( '\n', ' ' );
485  registerItem( name, label, expression, help, QgsExpressionItem::ExpressionNode, false, i );
486  i++;
487  }
488 }
489 
490 void QgsExpressionTreeView::saveToRecent( const QString &expressionText, const QString &collection )
491 {
492  QgsSettings settings;
493  QString location = QStringLiteral( "/expressions/recent/%1" ).arg( collection );
494  QStringList expressions = settings.value( location ).toStringList();
495  expressions.removeAll( expressionText );
496 
497  expressions.prepend( expressionText );
498 
499  while ( expressions.count() > 20 )
500  {
501  expressions.pop_back();
502  }
503 
504  settings.setValue( location, expressions );
505  loadRecent( collection );
506 }
507 
508 void QgsExpressionTreeView::saveToUserExpressions( const QString &label, const QString expression, const QString &helpText )
509 {
510  QgsSettings settings;
511  const QString location = QStringLiteral( "user" );
512  settings.beginGroup( location, QgsSettings::Section::Expressions );
513  settings.beginGroup( label );
514  settings.setValue( QStringLiteral( "expression" ), expression );
515  settings.setValue( QStringLiteral( "helpText" ), helpText );
517  // Scroll
518  const QModelIndexList idxs { mModel->match( mModel->index( 0, 0 ), Qt::DisplayRole, label, 1, Qt::MatchFlag::MatchRecursive ) };
519  if ( ! idxs.isEmpty() )
520  {
521  scrollTo( idxs.first() );
522  }
523 }
524 
526 {
527  QgsSettings settings;
528  settings.remove( QStringLiteral( "user/%1" ).arg( label ), QgsSettings::Section::Expressions );
530 }
531 
532 // this is potentially very slow if there are thousands of user expressions, every time entire cleanup and load
534 {
535  // Cleanup
536  if ( mExpressionGroups.contains( QStringLiteral( "UserGroup" ) ) )
537  {
538  QgsExpressionItem *node = mExpressionGroups.value( QStringLiteral( "UserGroup" ) );
539  node->removeRows( 0, node->rowCount() );
540  }
541 
542  QgsSettings settings;
543  const QString location = QStringLiteral( "user" );
544  settings.beginGroup( location, QgsSettings::Section::Expressions );
545  QString label;
546  QString helpText;
547  QString expression;
548  int i = 0;
549  mUserExpressionLabels = settings.childGroups();
550  for ( const auto &label : std::as_const( mUserExpressionLabels ) )
551  {
552  settings.beginGroup( label );
553  expression = settings.value( QStringLiteral( "expression" ) ).toString();
554  helpText = formatUserExpressionHelp( label, expression, settings.value( QStringLiteral( "helpText" ) ).toString() );
555  registerItem( QStringLiteral( "UserGroup" ), label, expression, helpText, QgsExpressionItem::ExpressionNode, false, i++ );
556  settings.endGroup();
557  }
558 }
559 
561 {
562  return mUserExpressionLabels;
563 }
564 
566 {
567  const QString group = QStringLiteral( "user" );
568  QgsSettings settings;
569  QJsonArray exportList;
570  QJsonObject exportObject
571  {
572  {"qgis_version", Qgis::version()},
573  {"exported_at", QDateTime::currentDateTime().toString( Qt::ISODate )},
574  {"author", QgsApplication::userFullName()},
575  {"expressions", exportList}
576  };
577 
578  settings.beginGroup( group, QgsSettings::Section::Expressions );
579 
580  mUserExpressionLabels = settings.childGroups();
581 
582  for ( const QString &label : std::as_const( mUserExpressionLabels ) )
583  {
584  settings.beginGroup( label );
585 
586  const QString expression = settings.value( QStringLiteral( "expression" ) ).toString();
587  const QString helpText = settings.value( QStringLiteral( "helpText" ) ).toString();
588  const QJsonObject expressionObject
589  {
590  {"name", label},
591  {"type", "expression"},
592  {"expression", expression},
593  {"group", group},
594  {"description", helpText}
595  };
596  exportList.push_back( expressionObject );
597 
598  settings.endGroup();
599  }
600 
601  exportObject["expressions"] = exportList;
602  QJsonDocument exportJson = QJsonDocument( exportObject );
603 
604  return exportJson;
605 }
606 
607 void QgsExpressionTreeView::loadExpressionsFromJson( const QJsonDocument &expressionsDocument )
608 {
609  // if the root of the json document is not an object, it means it's a wrong file
610  if ( ! expressionsDocument.isObject() )
611  return;
612 
613  QJsonObject expressionsObject = expressionsDocument.object();
614 
615  // validate json for manadatory fields
616  if ( ! expressionsObject["qgis_version"].isString()
617  || ! expressionsObject["exported_at"].isString()
618  || ! expressionsObject["author"].isString()
619  || ! expressionsObject["expressions"].isArray() )
620  return;
621 
622  // validate versions
623  QVersionNumber qgisJsonVersion = QVersionNumber::fromString( expressionsObject["qgis_version"].toString() );
624  QVersionNumber qgisVersion = QVersionNumber::fromString( Qgis::version() );
625 
626  // if the expressions are from newer version of QGIS, we ask the user to confirm
627  // they want to proceed
628  if ( qgisJsonVersion > qgisVersion )
629  {
630  QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No;
631  switch ( QMessageBox::question( this,
632  tr( "QGIS Version Mismatch" ),
633  tr( "The imported expressions are from newer version of QGIS (%1) "
634  "and some of the expression might not work the current version (%2). "
635  "Are you sure you want to continue?" ).arg( qgisJsonVersion.toString(), qgisVersion.toString() ), buttons ) )
636  {
637  case QMessageBox::No:
638  return;
639 
640  case QMessageBox::Yes:
641  break;
642 
643  default:
644  break;
645  }
646  }
647 
648  // we store the number of
649  QStringList skippedExpressionLabels;
650  bool isApplyToAll = false;
651  bool isOkToOverwrite = false;
652 
653  QgsSettings settings;
654  settings.beginGroup( QStringLiteral( "user" ), QgsSettings::Section::Expressions );
655  mUserExpressionLabels = settings.childGroups();
656 
657  for ( const QJsonValue && expressionValue : expressionsObject["expressions"].toArray() )
658  {
659  // validate the type of the array element, can be anything
660  if ( ! expressionValue.isObject() )
661  {
662  // try to stringify and put and indicator what happened
663  skippedExpressionLabels.append( expressionValue.toString() );
664  continue;
665  }
666 
667  QJsonObject expressionObj = expressionValue.toObject();
668 
669  // make sure the required keys are the correct types
670  if ( ! expressionObj["name"].isString()
671  || ! expressionObj["type"].isString()
672  || ! expressionObj["expression"].isString()
673  || ! expressionObj["group"].isString()
674  || ! expressionObj["description"].isString() )
675  {
676  // try to stringify and put an indicator what happened. Try to stringify the name, if fails, go with the expression.
677  if ( ! expressionObj["name"].toString().isEmpty() )
678  skippedExpressionLabels.append( expressionObj["name"].toString() );
679  else
680  skippedExpressionLabels.append( expressionObj["expression"].toString() );
681 
682  continue;
683  }
684 
685  // we want to import only items of type expression for now
686  if ( expressionObj["type"].toString() != QLatin1String( "expression" ) )
687  {
688  skippedExpressionLabels.append( expressionObj["name"].toString() );
689  continue;
690  }
691 
692  // we want to import only items of type expression for now
693  if ( expressionObj["group"].toString() != QLatin1String( "user" ) )
694  {
695  skippedExpressionLabels.append( expressionObj["name"].toString() );
696  continue;
697  }
698 
699  const QString label = expressionObj["name"].toString();
700  const QString expression = expressionObj["expression"].toString();
701  const QString helpText = expressionObj["description"].toString();
702 
703  // make sure they have valid name
704  if ( label.contains( "\\" ) || label.contains( '/' ) )
705  {
706  skippedExpressionLabels.append( expressionObj["name"].toString() );
707  continue;
708  }
709 
710  settings.beginGroup( label );
711  const QString oldExpression = settings.value( QStringLiteral( "expression" ) ).toString();
712  settings.endGroup();
713 
714  // TODO would be nice to skip the cases when labels and expressions match
715  if ( mUserExpressionLabels.contains( label ) && expression != oldExpression )
716  {
717  if ( ! isApplyToAll )
718  showMessageBoxConfirmExpressionOverwrite( isApplyToAll, isOkToOverwrite, label, oldExpression, expression );
719 
720  if ( isOkToOverwrite )
721  saveToUserExpressions( label, expression, helpText );
722  else
723  {
724  skippedExpressionLabels.append( label );
725  continue;
726  }
727  }
728  else
729  {
730  saveToUserExpressions( label, expression, helpText );
731  }
732  }
733 
735 
736  if ( ! skippedExpressionLabels.isEmpty() )
737  {
738  QStringList skippedExpressionLabelsQuoted;
739  for ( const QString &skippedExpressionLabel : skippedExpressionLabels )
740  skippedExpressionLabelsQuoted.append( QStringLiteral( "'%1'" ).arg( skippedExpressionLabel ) );
741 
742  QMessageBox::information( this,
743  tr( "Skipped Expression Imports" ),
744  QStringLiteral( "%1\n%2" ).arg( tr( "The following expressions have been skipped:" ),
745  skippedExpressionLabelsQuoted.join( ", " ) ) );
746  }
747 }
748 
749 const QList<QgsExpressionItem *> QgsExpressionTreeView::findExpressions( const QString &label )
750 {
751  QList<QgsExpressionItem *> result;
752  const QList<QStandardItem *> found { mModel->findItems( label, Qt::MatchFlag::MatchRecursive ) };
753  for ( const auto &item : std::as_const( found ) )
754  {
755  result.push_back( static_cast<QgsExpressionItem *>( item ) );
756  }
757  return result;
758 }
759 
760 void QgsExpressionTreeView::showMessageBoxConfirmExpressionOverwrite(
761  bool &isApplyToAll,
762  bool &isOkToOverwrite,
763  const QString &label,
764  const QString &oldExpression,
765  const QString &newExpression )
766 {
767  QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::YesToAll | QMessageBox::No | QMessageBox::NoToAll;
768  switch ( QMessageBox::question( this,
769  tr( "Expression Overwrite" ),
770  tr( "The expression with label '%1' was already defined."
771  "The old expression \"%2\" will be overwritten by \"%3\"."
772  "Are you sure you want to overwrite the expression?" ).arg( label, oldExpression, newExpression ), buttons ) )
773  {
774  case QMessageBox::NoToAll:
775  isApplyToAll = true;
776  isOkToOverwrite = false;
777  break;
778 
779  case QMessageBox::No:
780  isApplyToAll = false;
781  isOkToOverwrite = false;
782  break;
783 
784  case QMessageBox::YesToAll:
785  isApplyToAll = true;
786  isOkToOverwrite = true;
787  break;
788 
789  case QMessageBox::Yes:
790  isApplyToAll = false;
791  isOkToOverwrite = true;
792  break;
793 
794  default:
795  break;
796  }
797 }
798 
799 
800 // ****************************
801 // ****************************
802 // QgsExpressionItemSearchProxy
803 // ****************************
804 
805 
807 {
808  setFilterCaseSensitivity( Qt::CaseInsensitive );
809 }
810 
811 bool QgsExpressionItemSearchProxy::filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const
812 {
813  QModelIndex index = sourceModel()->index( source_row, 0, source_parent );
814  QgsExpressionItem::ItemType itemType = QgsExpressionItem::ItemType( sourceModel()->data( index, QgsExpressionItem::ITEM_TYPE_ROLE ).toInt() );
815 
816  int count = sourceModel()->rowCount( index );
817  bool matchchild = false;
818  for ( int i = 0; i < count; ++i )
819  {
820  if ( filterAcceptsRow( i, index ) )
821  {
822  matchchild = true;
823  break;
824  }
825  }
826 
827  if ( itemType == QgsExpressionItem::Header && matchchild )
828  return true;
829 
830  if ( itemType == QgsExpressionItem::Header )
831  return false;
832 
833  // check match of item label or tags
834  if ( QSortFilterProxyModel::filterAcceptsRow( source_row, source_parent ) )
835  {
836  return true;
837  }
838  else
839  {
840  const QStringList tags = sourceModel()->data( index, QgsExpressionItem::SEARCH_TAGS_ROLE ).toStringList();
841  for ( const QString &tag : tags )
842  {
843  if ( tag.contains( filterRegExp() ) )
844  return true;
845  }
846  }
847  return false;
848 }
849 
850 bool QgsExpressionItemSearchProxy::lessThan( const QModelIndex &left, const QModelIndex &right ) const
851 {
852  int leftSort = sourceModel()->data( left, QgsExpressionItem::CUSTOM_SORT_ROLE ).toInt();
853  int rightSort = sourceModel()->data( right, QgsExpressionItem::CUSTOM_SORT_ROLE ).toInt();
854  if ( leftSort != rightSort )
855  return leftSort < rightSort;
856 
857  QString leftString = sourceModel()->data( left, Qt::DisplayRole ).toString();
858  QString rightString = sourceModel()->data( right, Qt::DisplayRole ).toString();
859 
860  //ignore $ prefixes when sorting
861  if ( leftString.startsWith( '$' ) )
862  leftString = leftString.mid( 1 );
863  if ( rightString.startsWith( '$' ) )
864  rightString = rightString.mid( 1 );
865 
866  return QString::localeAwareCompare( leftString, rightString ) < 0;
867 }
static QString version()
Version string.
Definition: qgis.cpp:285
static QString userFullName()
Returns the user's operating system login account full display name.
static QgsExpressionContextScope * layerScope(const QgsMapLayer *layer)
Creates a new scope which contains variables and functions relating to a QgsMapLayer.
Expression contexts are used to encapsulate the parameters around which a QgsExpression should be eva...
QString description(const QString &name) const
Returns a translated description string for the variable with specified name.
QStringList functionNames() const
Retrieves a list of function names contained in the context.
bool isHighlightedFunction(const QString &name) const
Returns true if the specified function name is intended to be highlighted to the user.
QStringList filteredVariableNames() const
Returns a filtered list of variables names set by all scopes in the context.
bool isHighlightedVariable(const QString &name) const
Returns true if the specified variable name is intended to be highlighted to the user.
QgsExpressionFunction * function(const QString &name) const
Fetches a matching function from the context.
QVariant variable(const QString &name) const
Fetches a matching variable from the context.
A abstract base class for defining QgsExpression functions.
bool isContextual() const
Returns whether the function is only available if provided by a QgsExpressionContext object.
int params() const
The number of parameters this function takes.
QStringList groups() const
Returns a list of the groups the function belongs to.
virtual bool isDeprecated() const
Returns true if the function is deprecated and should not be presented as a valid option to users in ...
QString name() const
The name of the function.
const QString helpText() const
The help text for the function.
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override
An expression item that can be used in the QgsExpressionBuilderWidget tree.
QString getExpressionText() const
static const int SEARCH_TAGS_ROLE
Search tags role.
static const int ITEM_TYPE_ROLE
Item type role.
static const int CUSTOM_SORT_ROLE
Custom sort order role.
QgsExpressionItem::ItemType getItemType() const
Gets the type of expression item, e.g., header, field, ExpressionNode.
static const int ITEM_NAME_ROLE
Item name role.
Implementation of this interface can be implemented to allow QgsExpressionTreeView instance to provid...
virtual QMenu * createContextMenu(QgsExpressionItem *item)
Returns a newly created menu instance.
QgsProject * project()
Returns the project currently associated with the widget.
void refresh()
Refreshes the content of the tree.
void setProject(QgsProject *project)
Sets the project currently associated with the widget.
void saveToUserExpressions(const QString &label, const QString expression, const QString &helpText)
Stores the user expression with given label and helpText.
QgsExpressionItem * currentItem() const
Returns the current item or a nullptr.
void setLayer(QgsVectorLayer *layer)
Sets layer in order to get the fields and values.
void setMenuProvider(MenuProvider *provider)
Sets the menu provider.
QStringList userExpressionLabels() const
Returns the user expression labels.
void expressionItemDoubleClicked(const QString &text)
Emitted when a expression item is double clicked.
void currentExpressionItemChanged(QgsExpressionItem *item)
Emitter when the current expression item changed.
QJsonDocument exportUserExpressions()
Create the expressions JSON document storing all the user expressions to be exported.
QgsExpressionTreeView(QWidget *parent=nullptr)
Constructor.
void loadExpressionsFromJson(const QJsonDocument &expressionsDocument)
Load and permanently store the expressions from the expressions JSON document.
void saveToRecent(const QString &expressionText, const QString &collection="generic")
Adds the current expression to the given collection.
void setSearchText(const QString &text)
Sets the text to filter the expression tree.
void setExpressionContext(const QgsExpressionContext &context)
Sets the expression context for the tree view.
const QList< QgsExpressionItem * > findExpressions(const QString &label)
Returns the list of expression items matching a label.
void removeFromUserExpressions(const QString &label)
Removes the expression label from the user stored expressions.
void loadRecent(const QString &collection=QStringLiteral("generic"))
Loads the recent expressions from the given collection.
void loadUserExpressions()
Loads the user expressions.
Q_DECL_DEPRECATED QStandardItemModel * model()
Returns a pointer to the dialog's function item model.
void loadFieldNames(const QgsFields &fields)
This allows loading fields without specifying a layer.
static const QList< QgsExpressionFunction * > & Functions()
static int functionCount()
Returns the number of functions defined in the parser.
static QString formatPreviewString(const QVariant &value, bool htmlOutput=true, int maximumPreviewLength=60)
Formats an expression result for friendly display to the user.
static QStringList tags(const QString &name)
Returns a string list of search tags for a specified function.
static QString group(const QString &group)
Returns the translated name for a function group.
Encapsulate a field in an attribute table or data source.
Definition: qgsfield.h:51
QString name
Definition: qgsfield.h:60
QString displayNameWithAlias() const
Returns the name to use when displaying this field and adds the alias in parenthesis if it is defined...
Definition: qgsfield.cpp:96
Container of fields for a vector layer.
Definition: qgsfields.h:45
int count() const
Returns number of items.
Definition: qgsfields.cpp:133
QgsField at(int i) const
Returns the field at particular index (must be in range 0..N-1).
Definition: qgsfields.cpp:163
QIcon iconForField(int fieldIdx, bool considerOrigin=false) const
Returns an icon corresponding to a field index, based on the field's type and source.
Definition: qgsfields.cpp:275
static QIcon iconForLayer(const QgsMapLayer *layer)
Returns the icon corresponding to a specified map layer.
Base class for all map layer types.
Definition: qgsmaplayer.h:70
QString name
Definition: qgsmaplayer.h:73
QString id() const
Returns the layer's unique ID, which is used to access this layer from QgsProject.
Encapsulates a QGIS project, including sets of map layers and their styles, layouts,...
Definition: qgsproject.h:99
QString name
Definition: qgsrelation.h:49
Q_GADGET QString id
Definition: qgsrelation.h:46
Represents a vector layer which manages a vector based data sets.
QgsFields fields() const FINAL
Returns the list of fields of this layer.
QString formatLayerHelp(const QgsMapLayer *layer)
Returns a HTML formatted string for use as a layer item help.
QString formatUserExpressionHelp(const QString &label, const QString &expression, const QString &description)
Returns a HTML formatted string for use as a user expression item help.
QString formatVariableHelp(const QString &variable, const QString &description, bool showValue, const QVariant &value)
Returns a HTML formatted string for use as a variable item help.
QString formatRecentExpressionHelp(const QString &label, const QString &expression)
Returns a HTML formatted string for use as a recent expression item help.
QString formatRelationHelp(const QgsRelation &relation)
Returns a HTML formatted string for use as a relation item help.
const QgsField & field
Definition: qgsfield.h:463