QGIS API Documentation  3.27.0-Master (11ef3e5184)
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"
22 #include "qgsvectorlayer.h"
24 #include "qgssettings.h"
25 #include "qgsrelationmanager.h"
26 #include "qgsapplication.h"
27 #include "qgsiconutils.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 = std::make_unique<QStandardItemModel>();
115  mProxyModel = std::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->setFilterString( 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( "BETWEEN" ), QStringLiteral( " BETWEEN " ) );
278  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "NOT BETWEEN" ), QStringLiteral( " NOT BETWEEN " ) );
279  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "IN" ), QStringLiteral( " IN " ) );
280  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "LIKE" ), QStringLiteral( " LIKE " ) );
281  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "ILIKE" ), QStringLiteral( " ILIKE " ) );
282  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "IS" ), QStringLiteral( " IS " ) );
283  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "IS NOT" ), QStringLiteral( " IS NOT " ) );
284  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "OR" ), QStringLiteral( " OR " ) );
285  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "AND" ), QStringLiteral( " AND " ) );
286  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "NOT" ), QStringLiteral( " NOT " ) );
287 
288  QString casestring = QStringLiteral( "CASE WHEN condition THEN result END" );
289  registerItem( QStringLiteral( "Conditionals" ), QStringLiteral( "CASE" ), casestring );
290 
291  // use -1 as sort order here -- NULL should always show before the field list
292  registerItem( QStringLiteral( "Fields and Values" ), QStringLiteral( "NULL" ), QStringLiteral( "NULL" ), QString(), QgsExpressionItem::ExpressionNode, false, -1 );
293 
294  // Load the functions from the QgsExpression class
295  int count = QgsExpression::functionCount();
296  for ( int i = 0; i < count; i++ )
297  {
299  QString name = func->name();
300  if ( name.startsWith( '_' ) ) // do not display private functions
301  continue;
302  if ( func->isDeprecated() ) // don't show deprecated functions
303  continue;
304  if ( func->isContextual() )
305  {
306  //don't show contextual functions by default - it's up the the QgsExpressionContext
307  //object to provide them if supported
308  continue;
309  }
310  if ( func->params() != 0 )
311  name += '(';
312  else if ( !name.startsWith( '$' ) )
313  name += QLatin1String( "()" );
314  // this is where the functions are being registered, including functions under "Custom"
315  registerItemForAllGroups( func->groups(), func->name(), ' ' + name + ' ', func->helpText(), QgsExpressionItem::ExpressionNode, mExpressionContext.isHighlightedFunction( func->name() ), 1, QgsExpression::tags( func->name() ) );
316  }
317 
318  // load relation names
319  loadRelations();
320 
321  // load layer IDs
322  loadLayers();
323 
324  loadExpressionContext();
325 }
326 
327 QgsExpressionItem *QgsExpressionTreeView::registerItem( const QString &group,
328  const QString &label,
329  const QString &expressionText,
330  const QString &helpText,
331  QgsExpressionItem::ItemType type, bool highlightedItem, int sortOrder, const QIcon &icon, const QStringList &tags, const QString &name )
332 {
333  QgsExpressionItem *item = new QgsExpressionItem( label, expressionText, helpText, type );
334  item->setData( label, Qt::UserRole );
335  item->setData( sortOrder, QgsExpressionItem::CUSTOM_SORT_ROLE );
336  item->setData( tags, QgsExpressionItem::SEARCH_TAGS_ROLE );
337  item->setData( name, QgsExpressionItem::ITEM_NAME_ROLE );
338  item->setIcon( icon );
339 
340  // Look up the group and insert the new function.
341  if ( mExpressionGroups.contains( group ) )
342  {
343  QgsExpressionItem *groupNode = mExpressionGroups.value( group );
344  groupNode->appendRow( item );
345  }
346  else
347  {
348  // If the group doesn't exist yet we make it first.
349  QgsExpressionItem *newgroupNode = new QgsExpressionItem( QgsExpression::group( group ), QString(), QgsExpressionItem::Header );
350  newgroupNode->setData( group, Qt::UserRole );
351  //Recent group should always be last group
352  newgroupNode->setData( group.startsWith( QLatin1String( "Recent (" ) ) ? 2 : 1, QgsExpressionItem::CUSTOM_SORT_ROLE );
353  newgroupNode->appendRow( item );
354  newgroupNode->setBackground( QBrush( QColor( 150, 150, 150, 150 ) ) );
355  mModel->appendRow( newgroupNode );
356  mExpressionGroups.insert( group, newgroupNode );
357  }
358 
359  if ( highlightedItem )
360  {
361  //insert a copy as a top level item
362  QgsExpressionItem *topLevelItem = new QgsExpressionItem( label, expressionText, helpText, type );
363  topLevelItem->setData( label, Qt::UserRole );
364  item->setData( 0, QgsExpressionItem::CUSTOM_SORT_ROLE );
365  QFont font = topLevelItem->font();
366  font.setBold( true );
367  topLevelItem->setFont( font );
368  mModel->appendRow( topLevelItem );
369  }
370  return item;
371 }
372 
373 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 )
374 {
375  const auto constGroups = groups;
376  for ( const QString &group : constGroups )
377  {
378  registerItem( group, label, expressionText, helpText, type, highlightedItem, sortOrder, QIcon(), tags );
379  }
380 }
381 
382 void QgsExpressionTreeView::loadExpressionContext()
383 {
384  QStringList variableNames = mExpressionContext.filteredVariableNames();
385  const auto constVariableNames = variableNames;
386  for ( const QString &variable : constVariableNames )
387  {
388  registerItem( QStringLiteral( "Variables" ), variable, " @" + variable + ' ',
389  formatVariableHelp( variable, mExpressionContext.description( variable ), true, mExpressionContext.variable( variable ) ),
391  mExpressionContext.isHighlightedVariable( variable ) );
392  }
393 
394  // Load the functions from the expression context
395  QStringList contextFunctions = mExpressionContext.functionNames();
396  const auto constContextFunctions = contextFunctions;
397  for ( const QString &functionName : constContextFunctions )
398  {
399  QgsExpressionFunction *func = mExpressionContext.function( functionName );
400  QString name = func->name();
401  if ( name.startsWith( '_' ) ) // do not display private functions
402  continue;
403  if ( func->params() != 0 )
404  name += '(';
405  registerItemForAllGroups( func->groups(), func->name(), ' ' + name + ' ', func->helpText(), QgsExpressionItem::ExpressionNode, mExpressionContext.isHighlightedFunction( func->name() ), 1, QgsExpression::tags( func->name() ) );
406  }
407 }
408 
409 void QgsExpressionTreeView::loadLayers()
410 {
411  if ( !mProject )
412  return;
413 
414  QMap<QString, QgsMapLayer *> layers = mProject->mapLayers();
415  QMap<QString, QgsMapLayer *>::const_iterator layerIt = layers.constBegin();
416  for ( ; layerIt != layers.constEnd(); ++layerIt )
417  {
418  QIcon icon = QgsIconUtils::iconForLayer( layerIt.value() );
419  QgsExpressionItem *parentItem = registerItem( QStringLiteral( "Map Layers" ), layerIt.value()->name(), QStringLiteral( "'%1'" ).arg( layerIt.key() ), formatLayerHelp( layerIt.value() ), QgsExpressionItem::ExpressionNode, false, 99, icon );
420  loadLayerFields( qobject_cast<QgsVectorLayer *>( layerIt.value() ), parentItem );
421  }
422 
423 }
424 
425 void QgsExpressionTreeView::loadLayerFields( QgsVectorLayer *layer, QgsExpressionItem *parentItem )
426 {
427  if ( !layer )
428  return;
429 
430  const QgsFields fields = layer->fields();
431  for ( int fieldIdx = 0; fieldIdx < fields.count(); ++fieldIdx )
432  {
433  const QgsField field = fields.at( fieldIdx );
434  QIcon icon = fields.iconForField( fieldIdx );
435  const QString label { field.displayNameWithAlias() };
436  QgsExpressionItem *item = new QgsExpressionItem( label, " '" + field.name() + "' ", QString(), QgsExpressionItem::Field );
437  item->setData( label, Qt::UserRole );
438  item->setData( 99, QgsExpressionItem::CUSTOM_SORT_ROLE );
439  item->setData( QStringList(), QgsExpressionItem::SEARCH_TAGS_ROLE );
440  item->setData( field.name(), QgsExpressionItem::ITEM_NAME_ROLE );
441  item->setData( layer->id(), QgsExpressionItem::LAYER_ID_ROLE );
442  item->setIcon( icon );
443  parentItem->appendRow( item );
444  }
445 }
446 
448 {
449  for ( int i = 0; i < fields.count(); ++i )
450  {
451  const QgsField field = fields.at( i );
452  QIcon icon = fields.iconForField( i );
453  registerItem( QStringLiteral( "Fields and Values" ), field.displayNameWithAlias(),
454  " \"" + field.name() + "\" ", QString(), QgsExpressionItem::Field, false, i, icon, QStringList(), field.name() );
455  }
456 }
457 
458 void QgsExpressionTreeView::loadFieldNames()
459 {
460  // Cleanup
461  if ( mExpressionGroups.contains( QStringLiteral( "Fields and Values" ) ) )
462  {
463  QgsExpressionItem *node = mExpressionGroups.value( QStringLiteral( "Fields and Values" ) );
464  node->removeRows( 0, node->rowCount() );
465  // Re-add NULL and feature variables
466  // use -1 as sort order here -- NULL and feature variables should always show before the field list
467  registerItem( QStringLiteral( "Fields and Values" ), QStringLiteral( "NULL" ), QStringLiteral( "NULL" ), QString(), QgsExpressionItem::ExpressionNode, false, -1 );
468  }
469 
470  if ( mLayer )
471  {
472  // Add feature variables to record and attributes group (and highlighted items)
473 
474  const QString currentFeatureHelp = formatVariableHelp( QStringLiteral( "feature" ), QgsExpression::variableHelpText( QStringLiteral( "feature" ) ), false, QVariant() );
475  const QString currentFeatureIdHelp = formatVariableHelp( QStringLiteral( "id" ), QgsExpression::variableHelpText( QStringLiteral( "id" ) ), false, QVariant() );
476  const QString currentGeometryHelp = formatVariableHelp( QStringLiteral( "geometry" ), QgsExpression::variableHelpText( QStringLiteral( "geometry" ) ), false, QVariant() );
477 
478  registerItem( QStringLiteral( "Fields and Values" ), QStringLiteral( "feature" ), QStringLiteral( "@feature" ), currentFeatureHelp, QgsExpressionItem::ExpressionNode, false, -1 );
479  registerItem( QStringLiteral( "Fields and Values" ), QStringLiteral( "id" ), QStringLiteral( "@id" ), currentFeatureIdHelp, QgsExpressionItem::ExpressionNode, false, -1 );
480  registerItem( QStringLiteral( "Fields and Values" ), QStringLiteral( "geometry" ), QStringLiteral( "@geometry" ), currentGeometryHelp, QgsExpressionItem::ExpressionNode, false, -1 );
481 
482  registerItem( tr( "Variables" ), QStringLiteral( "feature" ), QStringLiteral( "@feature" ), currentFeatureHelp, QgsExpressionItem::ExpressionNode );
483  registerItem( tr( "Variables" ), QStringLiteral( "id" ), QStringLiteral( "@id" ), currentFeatureIdHelp, QgsExpressionItem::ExpressionNode );
484  registerItem( tr( "Variables" ), QStringLiteral( "geometry" ), QStringLiteral( "@geometry" ), currentGeometryHelp, QgsExpressionItem::ExpressionNode, false );
485 
486  registerItem( tr( "Record and Attributes" ), QStringLiteral( "feature" ), QStringLiteral( "@feature" ), currentFeatureHelp, QgsExpressionItem::ExpressionNode, true, -1 );
487  registerItem( tr( "Record and Attributes" ), QStringLiteral( "id" ), QStringLiteral( "@id" ), currentFeatureIdHelp, QgsExpressionItem::ExpressionNode, true, -1 );
488  registerItem( tr( "Record and Attributes" ), QStringLiteral( "geometry" ), QStringLiteral( "@geometry" ), currentGeometryHelp, QgsExpressionItem::ExpressionNode, true, -1 );
489  }
490 
491  // this can happen if fields are manually set
492  if ( !mLayer )
493  return;
494 
495  const QgsFields &fields = mLayer->fields();
496 
497  loadFieldNames( fields );
498 }
499 
500 void QgsExpressionTreeView::loadRelations()
501 {
502  if ( !mProject )
503  return;
504 
505  QMap<QString, QgsRelation> relations = mProject->relationManager()->relations();
506  QMap<QString, QgsRelation>::const_iterator relIt = relations.constBegin();
507  for ( ; relIt != relations.constEnd(); ++relIt )
508  {
509  registerItemForAllGroups( QStringList() << tr( "Relations" ), relIt->name(), QStringLiteral( "'%1'" ).arg( relIt->id() ), formatRelationHelp( relIt.value() ) );
510  }
511 }
512 
513 void QgsExpressionTreeView::loadRecent( const QString &collection )
514 {
515  mRecentKey = collection;
516  QString name = tr( "Recent (%1)" ).arg( collection );
517  if ( mExpressionGroups.contains( name ) )
518  {
519  QgsExpressionItem *node = mExpressionGroups.value( name );
520  node->removeRows( 0, node->rowCount() );
521  }
522 
523  QgsSettings settings;
524  const QString location = QStringLiteral( "/expressions/recent/%1" ).arg( collection );
525  const QStringList expressions = settings.value( location ).toStringList();
526  int i = 0;
527  for ( const QString &expression : expressions )
528  {
529  QString help = formatRecentExpressionHelp( expression, expression );
530  QString label = expression;
531  label.replace( '\n', ' ' );
532  registerItem( name, label, expression, help, QgsExpressionItem::ExpressionNode, false, i );
533  i++;
534  }
535 }
536 
537 void QgsExpressionTreeView::saveToRecent( const QString &expressionText, const QString &collection )
538 {
539  QgsSettings settings;
540  QString location = QStringLiteral( "/expressions/recent/%1" ).arg( collection );
541  QStringList expressions = settings.value( location ).toStringList();
542  expressions.removeAll( expressionText );
543 
544  expressions.prepend( expressionText );
545 
546  while ( expressions.count() > 20 )
547  {
548  expressions.pop_back();
549  }
550 
551  settings.setValue( location, expressions );
552  loadRecent( collection );
553 }
554 
555 void QgsExpressionTreeView::saveToUserExpressions( const QString &label, const QString &expression, const QString &helpText )
556 {
557  QgsSettings settings;
558  const QString location = QStringLiteral( "user" );
559  settings.beginGroup( location, QgsSettings::Section::Expressions );
560  settings.beginGroup( label );
561  settings.setValue( QStringLiteral( "expression" ), expression );
562  settings.setValue( QStringLiteral( "helpText" ), helpText );
564  // Scroll
565  const QModelIndexList idxs { mModel->match( mModel->index( 0, 0 ), Qt::DisplayRole, label, 1, Qt::MatchFlag::MatchRecursive ) };
566  if ( ! idxs.isEmpty() )
567  {
568  scrollTo( idxs.first() );
569  }
570 }
571 
573 {
574  QgsSettings settings;
575  settings.remove( QStringLiteral( "user/%1" ).arg( label ), QgsSettings::Section::Expressions );
577 }
578 
579 // this is potentially very slow if there are thousands of user expressions, every time entire cleanup and load
581 {
582  // Cleanup
583  if ( mExpressionGroups.contains( QStringLiteral( "UserGroup" ) ) )
584  {
585  QgsExpressionItem *node = mExpressionGroups.value( QStringLiteral( "UserGroup" ) );
586  node->removeRows( 0, node->rowCount() );
587  }
588 
589  QgsSettings settings;
590  const QString location = QStringLiteral( "user" );
591  settings.beginGroup( location, QgsSettings::Section::Expressions );
592  QString helpText;
593  QString expression;
594  int i = 0;
595  mUserExpressionLabels = settings.childGroups();
596  for ( const auto &label : std::as_const( mUserExpressionLabels ) )
597  {
598  settings.beginGroup( label );
599  expression = settings.value( QStringLiteral( "expression" ) ).toString();
600  helpText = formatUserExpressionHelp( label, expression, settings.value( QStringLiteral( "helpText" ) ).toString() );
601  registerItem( QStringLiteral( "UserGroup" ), label, expression, helpText, QgsExpressionItem::ExpressionNode, false, i++ );
602  settings.endGroup();
603  }
604 }
605 
607 {
608  return mUserExpressionLabels;
609 }
610 
612 {
613  const QString group = QStringLiteral( "user" );
614  QgsSettings settings;
615  QJsonArray exportList;
616  QJsonObject exportObject
617  {
618  {"qgis_version", Qgis::version()},
619  {"exported_at", QDateTime::currentDateTime().toString( Qt::ISODate )},
620  {"author", QgsApplication::userFullName()},
621  {"expressions", exportList}
622  };
623 
624  settings.beginGroup( group, QgsSettings::Section::Expressions );
625 
626  mUserExpressionLabels = settings.childGroups();
627 
628  for ( const QString &label : std::as_const( mUserExpressionLabels ) )
629  {
630  settings.beginGroup( label );
631 
632  const QString expression = settings.value( QStringLiteral( "expression" ) ).toString();
633  const QString helpText = settings.value( QStringLiteral( "helpText" ) ).toString();
634  const QJsonObject expressionObject
635  {
636  {"name", label},
637  {"type", "expression"},
638  {"expression", expression},
639  {"group", group},
640  {"description", helpText}
641  };
642  exportList.push_back( expressionObject );
643 
644  settings.endGroup();
645  }
646 
647  exportObject[QStringLiteral( "expressions" )] = exportList;
648  QJsonDocument exportJson = QJsonDocument( exportObject );
649 
650  return exportJson;
651 }
652 
653 void QgsExpressionTreeView::loadExpressionsFromJson( const QJsonDocument &expressionsDocument )
654 {
655  // if the root of the json document is not an object, it means it's a wrong file
656  if ( ! expressionsDocument.isObject() )
657  return;
658 
659  QJsonObject expressionsObject = expressionsDocument.object();
660 
661  // validate json for manadatory fields
662  if ( ! expressionsObject[QStringLiteral( "qgis_version" )].isString()
663  || ! expressionsObject[QStringLiteral( "exported_at" )].isString()
664  || ! expressionsObject[QStringLiteral( "author" )].isString()
665  || ! expressionsObject[QStringLiteral( "expressions" )].isArray() )
666  return;
667 
668  // validate versions
669  QVersionNumber qgisJsonVersion = QVersionNumber::fromString( expressionsObject[QStringLiteral( "qgis_version" )].toString() );
670  QVersionNumber qgisVersion = QVersionNumber::fromString( Qgis::version() );
671 
672  // if the expressions are from newer version of QGIS, we ask the user to confirm
673  // they want to proceed
674  if ( qgisJsonVersion > qgisVersion )
675  {
676  QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No;
677  switch ( QMessageBox::question( this,
678  tr( "QGIS Version Mismatch" ),
679  tr( "The imported expressions are from newer version of QGIS (%1) "
680  "and some of the expression might not work the current version (%2). "
681  "Are you sure you want to continue?" ).arg( qgisJsonVersion.toString(), qgisVersion.toString() ), buttons ) )
682  {
683  case QMessageBox::No:
684  return;
685 
686  case QMessageBox::Yes:
687  break;
688 
689  default:
690  break;
691  }
692  }
693 
694  // we store the number of
695  QStringList skippedExpressionLabels;
696  bool isApplyToAll = false;
697  bool isOkToOverwrite = false;
698 
699  QgsSettings settings;
700  settings.beginGroup( QStringLiteral( "user" ), QgsSettings::Section::Expressions );
701  mUserExpressionLabels = settings.childGroups();
702 
703  const QJsonArray expressions = expressionsObject[QStringLiteral( "expressions" )].toArray();
704  for ( const QJsonValue && expressionValue : expressions )
705  {
706  // validate the type of the array element, can be anything
707  if ( ! expressionValue.isObject() )
708  {
709  // try to stringify and put and indicator what happened
710  skippedExpressionLabels.append( expressionValue.toString() );
711  continue;
712  }
713 
714  QJsonObject expressionObj = expressionValue.toObject();
715 
716  // make sure the required keys are the correct types
717  if ( ! expressionObj[QStringLiteral( "name" )].isString()
718  || ! expressionObj[QStringLiteral( "type" )].isString()
719  || ! expressionObj[QStringLiteral( "expression" )].isString()
720  || ! expressionObj[QStringLiteral( "group" )].isString()
721  || ! expressionObj[QStringLiteral( "description" )].isString() )
722  {
723  // try to stringify and put an indicator what happened. Try to stringify the name, if fails, go with the expression.
724  if ( ! expressionObj[QStringLiteral( "name" )].toString().isEmpty() )
725  skippedExpressionLabels.append( expressionObj[QStringLiteral( "name" )].toString() );
726  else
727  skippedExpressionLabels.append( expressionObj[QStringLiteral( "expression" )].toString() );
728 
729  continue;
730  }
731 
732  // we want to import only items of type expression for now
733  if ( expressionObj[QStringLiteral( "type" )].toString() != QLatin1String( "expression" ) )
734  {
735  skippedExpressionLabels.append( expressionObj[QStringLiteral( "name" )].toString() );
736  continue;
737  }
738 
739  // we want to import only items of type expression for now
740  if ( expressionObj[QStringLiteral( "group" )].toString() != QLatin1String( "user" ) )
741  {
742  skippedExpressionLabels.append( expressionObj[QStringLiteral( "name" )].toString() );
743  continue;
744  }
745 
746  const QString label = expressionObj[QStringLiteral( "name" )].toString();
747  const QString expression = expressionObj[QStringLiteral( "expression" )].toString();
748  const QString helpText = expressionObj[QStringLiteral( "description" )].toString();
749 
750  // make sure they have valid name
751  if ( label.contains( QLatin1String( "\\" ) ) || label.contains( '/' ) )
752  {
753  skippedExpressionLabels.append( expressionObj[QStringLiteral( "name" )].toString() );
754  continue;
755  }
756 
757  settings.beginGroup( label );
758  const QString oldExpression = settings.value( QStringLiteral( "expression" ) ).toString();
759  settings.endGroup();
760 
761  // TODO would be nice to skip the cases when labels and expressions match
762  if ( mUserExpressionLabels.contains( label ) && expression != oldExpression )
763  {
764  if ( ! isApplyToAll )
765  showMessageBoxConfirmExpressionOverwrite( isApplyToAll, isOkToOverwrite, label, oldExpression, expression );
766 
767  if ( isOkToOverwrite )
768  saveToUserExpressions( label, expression, helpText );
769  else
770  {
771  skippedExpressionLabels.append( label );
772  continue;
773  }
774  }
775  else
776  {
777  saveToUserExpressions( label, expression, helpText );
778  }
779  }
780 
782 
783  if ( ! skippedExpressionLabels.isEmpty() )
784  {
785  QStringList skippedExpressionLabelsQuoted;
786  skippedExpressionLabelsQuoted.reserve( skippedExpressionLabels.size() );
787  for ( const QString &skippedExpressionLabel : skippedExpressionLabels )
788  skippedExpressionLabelsQuoted.append( QStringLiteral( "'%1'" ).arg( skippedExpressionLabel ) );
789 
790  QMessageBox::information( this,
791  tr( "Skipped Expression Imports" ),
792  QStringLiteral( "%1\n%2" ).arg( tr( "The following expressions have been skipped:" ),
793  skippedExpressionLabelsQuoted.join( QLatin1String( ", " ) ) ) );
794  }
795 }
796 
797 const QList<QgsExpressionItem *> QgsExpressionTreeView::findExpressions( const QString &label )
798 {
799  QList<QgsExpressionItem *> result;
800  const QList<QStandardItem *> found { mModel->findItems( label, Qt::MatchFlag::MatchRecursive ) };
801  result.reserve( found.size() );
802  std::transform( found.begin(), found.end(), std::back_inserter( result ),
803  []( QStandardItem * item ) -> QgsExpressionItem* { return static_cast<QgsExpressionItem *>( item ); } );
804  return result;
805 }
806 
807 void QgsExpressionTreeView::showMessageBoxConfirmExpressionOverwrite(
808  bool &isApplyToAll,
809  bool &isOkToOverwrite,
810  const QString &label,
811  const QString &oldExpression,
812  const QString &newExpression )
813 {
814  QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::YesToAll | QMessageBox::No | QMessageBox::NoToAll;
815  switch ( QMessageBox::question( this,
816  tr( "Expression Overwrite" ),
817  tr( "The expression with label '%1' was already defined."
818  "The old expression \"%2\" will be overwritten by \"%3\"."
819  "Are you sure you want to overwrite the expression?" ).arg( label, oldExpression, newExpression ), buttons ) )
820  {
821  case QMessageBox::NoToAll:
822  isApplyToAll = true;
823  isOkToOverwrite = false;
824  break;
825 
826  case QMessageBox::No:
827  isApplyToAll = false;
828  isOkToOverwrite = false;
829  break;
830 
831  case QMessageBox::YesToAll:
832  isApplyToAll = true;
833  isOkToOverwrite = true;
834  break;
835 
836  case QMessageBox::Yes:
837  isApplyToAll = false;
838  isOkToOverwrite = true;
839  break;
840 
841  default:
842  break;
843  }
844 }
845 
846 
847 // ****************************
848 // ****************************
849 // QgsExpressionItemSearchProxy
850 // ****************************
851 
852 
854 {
855  setFilterCaseSensitivity( Qt::CaseInsensitive );
856 }
857 
858 bool QgsExpressionItemSearchProxy::filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const
859 {
860  QModelIndex index = sourceModel()->index( source_row, 0, source_parent );
861  const QgsExpressionItem::ItemType itemType = QgsExpressionItem::ItemType( sourceModel()->data( index, QgsExpressionItem::ITEM_TYPE_ROLE ).toInt() );
862 
863  if ( itemType == QgsExpressionItem::Header )
864  {
865  // show header if any child item matches
866  int count = sourceModel()->rowCount( index );
867  bool matchchild = false;
868  for ( int i = 0; i < count; ++i )
869  {
870  if ( filterAcceptsRow( i, index ) )
871  {
872  matchchild = true;
873  break;
874  }
875  }
876  return matchchild;
877  }
878 
879  // check match of item label or tags
880  const QString name = sourceModel()->data( index, Qt::DisplayRole ).toString();
881  if ( name.contains( mFilterString, Qt::CaseInsensitive ) )
882  {
883  return true;
884  }
885 
886  const QStringList tags = sourceModel()->data( index, QgsExpressionItem::SEARCH_TAGS_ROLE ).toStringList();
887  return std::any_of( tags.begin(), tags.end(), [this]( const QString & tag )
888  {
889  return tag.contains( mFilterString, Qt::CaseInsensitive );
890  } );
891 }
892 
894 {
895  mFilterString = string;
896  invalidate();
897 }
898 
899 bool QgsExpressionItemSearchProxy::lessThan( const QModelIndex &left, const QModelIndex &right ) const
900 {
901  int leftSort = sourceModel()->data( left, QgsExpressionItem::CUSTOM_SORT_ROLE ).toInt();
902  int rightSort = sourceModel()->data( right, QgsExpressionItem::CUSTOM_SORT_ROLE ).toInt();
903  if ( leftSort != rightSort )
904  return leftSort < rightSort;
905 
906  QString leftString = sourceModel()->data( left, Qt::DisplayRole ).toString();
907  QString rightString = sourceModel()->data( right, Qt::DisplayRole ).toString();
908 
909  //ignore $ prefixes when sorting
910  if ( leftString.startsWith( '$' ) )
911  leftString = leftString.mid( 1 );
912  if ( rightString.startsWith( '$' ) )
913  rightString = rightString.mid( 1 );
914 
915  return QString::localeAwareCompare( leftString, rightString ) < 0;
916 }
static QString version()
Version string.
Definition: qgis.cpp:277
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
void setFilterString(const QString &string)
Sets the search filter string.
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override
An expression item that can be used in the QgsExpressionBuilderWidget tree.
static const int LAYER_ID_ROLE
Layer ID role.
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 variableHelpText(const QString &variableName)
Returns the help text for a specified variable.
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:98
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:73
QString name
Definition: qgsmaplayer.h:76
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:104
QString name
Definition: qgsrelation.h:48
Q_GADGET QString id
Definition: qgsrelation.h:45
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.
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:90
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:463