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