QGIS API Documentation  3.12.1-București (121cc00ff0)
qgsexpressionbuilderwidget.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgisexpressionbuilderwidget.cpp - A generic expression string builder widget.
3  --------------------------------------
4  Date : 29-May-2011
5  Copyright : (C) 2011 by Nathan Woodrow
6  Email : woodrow.nathan at gmail dot com
7  ***************************************************************************
8  * *
9  * This program is free software; you can redistribute it and/or modify *
10  * it under the terms of the GNU General Public License as published by *
11  * the Free Software Foundation; either version 2 of the License, or *
12  * (at your option) any later version. *
13  * *
14  ***************************************************************************/
15 
17 #include "qgslogger.h"
18 #include "qgsexpression.h"
19 #include "qgsexpressionfunction.h"
20 #include "qgsexpressionnodeimpl.h"
21 #include "qgsmessageviewer.h"
22 #include "qgsapplication.h"
23 #include "qgspythonrunner.h"
24 #include "qgsgeometry.h"
25 #include "qgsfeature.h"
26 #include "qgsfeatureiterator.h"
27 #include "qgsvectorlayer.h"
28 #include "qgssettings.h"
29 #include "qgsproject.h"
30 #include "qgsrelationmanager.h"
31 #include "qgsrelation.h"
34 #include "qgsfieldformatter.h"
36 
37 #include <QMenu>
38 #include <QFile>
39 #include <QTextStream>
40 #include <QDir>
41 #include <QInputDialog>
42 #include <QComboBox>
43 #include <QGraphicsOpacityEffect>
44 #include <QPropertyAnimation>
45 #include <QMessageBox>
46 
48  : QWidget( parent )
49  , mProject( QgsProject::instance() )
50 {
51  setupUi( this );
52 
53  connect( btnRun, &QToolButton::pressed, this, &QgsExpressionBuilderWidget::btnRun_pressed );
54  connect( btnNewFile, &QToolButton::pressed, this, &QgsExpressionBuilderWidget::btnNewFile_pressed );
55  connect( cmbFileNames, &QListWidget::currentItemChanged, this, &QgsExpressionBuilderWidget::cmbFileNames_currentItemChanged );
56  connect( expressionTree, &QTreeView::doubleClicked, this, &QgsExpressionBuilderWidget::expressionTree_doubleClicked );
57  connect( txtExpressionString, &QgsCodeEditorExpression::textChanged, this, &QgsExpressionBuilderWidget::txtExpressionString_textChanged );
58  connect( txtPython, &QgsCodeEditorPython::textChanged, this, &QgsExpressionBuilderWidget::txtPython_textChanged );
59  connect( txtSearchEditValues, &QgsFilterLineEdit::textChanged, this, &QgsExpressionBuilderWidget::txtSearchEditValues_textChanged );
60  connect( txtSearchEdit, &QgsFilterLineEdit::textChanged, this, &QgsExpressionBuilderWidget::txtSearchEdit_textChanged );
61  connect( lblPreview, &QLabel::linkActivated, this, &QgsExpressionBuilderWidget::lblPreview_linkActivated );
62  connect( mValuesListView, &QListView::doubleClicked, this, &QgsExpressionBuilderWidget::mValuesListView_doubleClicked );
63  connect( btnSaveExpression, &QPushButton::pressed, this, &QgsExpressionBuilderWidget::storeCurrentUserExpression );
64  connect( btnRemoveExpression, &QPushButton::pressed, this, &QgsExpressionBuilderWidget::removeSelectedUserExpression );
65  connect( btnClearEditor, &QPushButton::pressed, txtExpressionString, &QgsCodeEditorExpression::clear );
66 
67  txtHelpText->setOpenExternalLinks( true );
68  mValueGroupBox->hide();
69 // highlighter = new QgsExpressionHighlighter( txtExpressionString->document() );
70 
71  mModel = qgis::make_unique<QStandardItemModel>();
72  mProxyModel = qgis::make_unique<QgsExpressionItemSearchProxy>();
73  mProxyModel->setDynamicSortFilter( true );
74  mProxyModel->setSourceModel( mModel.get() );
75  expressionTree->setModel( mProxyModel.get() );
76  expressionTree->setSortingEnabled( true );
77  expressionTree->sortByColumn( 0, Qt::AscendingOrder );
78 
79  expressionTree->setSelectionMode( QAbstractItemView::SelectionMode::SingleSelection );
80 
81  // Note: must be in sync with the json help file for UserGroup
82  mUserExpressionsGroupName = QgsExpression::group( QStringLiteral( "UserGroup" ) );
83 
84  // Set icons for tool buttons
85  btnSaveExpression->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionFileSave.svg" ) ) );
86  btnRemoveExpression->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionDeleteSelected.svg" ) ) );
87  btnClearEditor->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionFileNew.svg" ) ) );
88 
89  expressionTree->setContextMenuPolicy( Qt::CustomContextMenu );
90  connect( this, &QgsExpressionBuilderWidget::expressionParsed, this, &QgsExpressionBuilderWidget::setExpressionState );
91  connect( expressionTree, &QWidget::customContextMenuRequested, this, &QgsExpressionBuilderWidget::showContextMenu );
92  connect( expressionTree->selectionModel(), &QItemSelectionModel::currentChanged,
93  this, &QgsExpressionBuilderWidget::currentChanged );
94 
95  connect( btnLoadAll, &QAbstractButton::pressed, this, &QgsExpressionBuilderWidget::loadAllValues );
96  connect( btnLoadSample, &QAbstractButton::pressed, this, &QgsExpressionBuilderWidget::loadSampleValues );
97 
98  const auto pushButtons { mOperatorsGroupBox->findChildren<QPushButton *>() };
99  for ( QPushButton *button : pushButtons )
100  {
101  connect( button, &QAbstractButton::pressed, this, &QgsExpressionBuilderWidget::operatorButtonClicked );
102  }
103 
104  txtSearchEdit->setShowSearchIcon( true );
105  txtSearchEdit->setPlaceholderText( tr( "Search…" ) );
106 
107  mValuesModel = qgis::make_unique<QStandardItemModel>();
108  mProxyValues = qgis::make_unique<QSortFilterProxyModel>();
109  mProxyValues->setSourceModel( mValuesModel.get() );
110  mValuesListView->setModel( mProxyValues.get() );
111  txtSearchEditValues->setShowSearchIcon( true );
112  txtSearchEditValues->setPlaceholderText( tr( "Search…" ) );
113 
114  editorSplit->setSizes( QList<int>( {175, 300} ) );
115 
116  functionsplit->setCollapsible( 0, false );
117  connect( mShowHelpButton, &QPushButton::clicked, this, [ = ]()
118  {
119  functionsplit->setSizes( QList<int>( {mOperationListGroup->width() - mHelpAndValuesWidget->minimumWidth(),
120  mHelpAndValuesWidget->minimumWidth()
121  } ) );
122  mShowHelpButton->setEnabled( false );
123  } );
124  connect( functionsplit, &QSplitter::splitterMoved, this, [ = ]( int, int )
125  {
126  mShowHelpButton->setEnabled( functionsplit->sizes().at( 1 ) == 0 );
127  } );
128 
129 
130  QgsSettings settings;
131  splitter->restoreState( settings.value( QStringLiteral( "Windows/QgsExpressionBuilderWidget/splitter" ) ).toByteArray() );
132  editorSplit->restoreState( settings.value( QStringLiteral( "Windows/QgsExpressionBuilderWidget/editorsplitter" ) ).toByteArray() );
133  functionsplit->restoreState( settings.value( QStringLiteral( "Windows/QgsExpressionBuilderWidget/functionsplitter" ) ).toByteArray() );
134  mShowHelpButton->setEnabled( functionsplit->sizes().at( 1 ) == 0 );
135 
136  txtExpressionString->setFoldingVisible( false );
137 
138  updateFunctionTree();
140 
141  if ( QgsPythonRunner::isValid() )
142  {
143  QgsPythonRunner::eval( QStringLiteral( "qgis.user.expressionspath" ), mFunctionsPath );
144  updateFunctionFileList( mFunctionsPath );
145  }
146  else
147  {
148  tab_2->hide();
149  }
150 
151  // select the first item in the function list
152  // in order to avoid a blank help widget
153  QModelIndex firstItem = mProxyModel->index( 0, 0, QModelIndex() );
154  expressionTree->setCurrentIndex( firstItem );
155 
156  txtExpressionString->setWrapMode( QsciScintilla::WrapWord );
157  lblAutoSave->clear();
158 
159  // Note: If you add a indicator here you should add it to clearErrors method if you need to clear it on text parse.
160  txtExpressionString->indicatorDefine( QgsCodeEditor::SquiggleIndicator, QgsExpression::ParserError::FunctionUnknown );
161  txtExpressionString->indicatorDefine( QgsCodeEditor::SquiggleIndicator, QgsExpression::ParserError::FunctionWrongArgs );
162  txtExpressionString->indicatorDefine( QgsCodeEditor::SquiggleIndicator, QgsExpression::ParserError::FunctionInvalidParams );
163  txtExpressionString->indicatorDefine( QgsCodeEditor::SquiggleIndicator, QgsExpression::ParserError::FunctionNamedArgsError );
164 #if defined(QSCINTILLA_VERSION) && QSCINTILLA_VERSION >= 0x20a00
165  txtExpressionString->indicatorDefine( QgsCodeEditor::TriangleIndicator, QgsExpression::ParserError::Unknown );
166 #else
167  txtExpressionString->indicatorDefine( QgsCodeEditor::SquiggleIndicator, QgsExpression::ParserError::Unknown );
168 #endif
169 
170  // Set all the error markers as red. -1 is all.
171  txtExpressionString->setIndicatorForegroundColor( QColor( Qt::red ), -1 );
172  txtExpressionString->setIndicatorHoverForegroundColor( QColor( Qt::red ), -1 );
173  txtExpressionString->setIndicatorOutlineColor( QColor( Qt::red ), -1 );
174 
175  // Hidden function markers.
176  txtExpressionString->indicatorDefine( QgsCodeEditor::HiddenIndicator, FUNCTION_MARKER_ID );
177  txtExpressionString->setIndicatorForegroundColor( QColor( Qt::blue ), FUNCTION_MARKER_ID );
178  txtExpressionString->setIndicatorHoverForegroundColor( QColor( Qt::blue ), FUNCTION_MARKER_ID );
179  txtExpressionString->setIndicatorHoverStyle( QgsCodeEditor::DotsIndicator, FUNCTION_MARKER_ID );
180 
181  connect( txtExpressionString, &QgsCodeEditorExpression::indicatorClicked, this, &QgsExpressionBuilderWidget::indicatorClicked );
182  txtExpressionString->setAutoCompletionCaseSensitivity( true );
183  txtExpressionString->setAutoCompletionSource( QsciScintilla::AcsAPIs );
184  txtExpressionString->setCallTipsVisible( 0 );
185 
186  setExpectedOutputFormat( QString() );
187  mFunctionBuilderHelp->setMarginVisible( false );
188  mFunctionBuilderHelp->setEdgeMode( QsciScintilla::EdgeNone );
189  mFunctionBuilderHelp->setEdgeColumn( 0 );
190  mFunctionBuilderHelp->setReadOnly( true );
191  mFunctionBuilderHelp->setText( tr( "\"\"\"Define a new function using the @qgsfunction decorator.\n\
192 \n\
193  The function accepts the following parameters\n\
194 \n\
195  : param [any]: Define any parameters you want to pass to your function before\n\
196  the following arguments.\n\
197  : param feature: The current feature\n\
198  : param parent: The QgsExpression object\n\
199  : param context: If there is an argument called ``context`` found at the last\n\
200  position, this variable will contain a ``QgsExpressionContext``\n\
201  object, that gives access to various additional information like\n\
202  expression variables. E.g. ``context.variable( 'layer_id' )``\n\
203  : returns: The result of the expression.\n\
204 \n\
205 \n\
206 \n\
207  The @qgsfunction decorator accepts the following arguments:\n\
208 \n\
209 \n\
210  : param args: Defines the number of arguments. With ``args = 'auto'`` the number of\n\
211  arguments will automatically be extracted from the signature.\n\
212  With ``args = -1``, any number of arguments are accepted.\n\
213  : param group: The name of the group under which this expression function will\n\
214  be listed.\n\
215  : param handlesnull: Set this to True if your function has custom handling for NULL values.\n\
216  If False, the result will always be NULL as soon as any parameter is NULL.\n\
217  Defaults to False.\n\
218  : param usesgeometry : Set this to True if your function requires access to\n\
219  feature.geometry(). Defaults to False.\n\
220  : param referenced_columns: An array of attribute names that are required to run\n\
221  this function. Defaults to [QgsFeatureRequest.ALL_ATTRIBUTES].\n\
222  \"\"\"" ) );
223 }
224 
225 
227 {
228  QgsSettings settings;
229  settings.setValue( QStringLiteral( "Windows/QgsExpressionBuilderWidget/splitter" ), splitter->saveState() );
230  settings.setValue( QStringLiteral( "Windows/QgsExpressionBuilderWidget/editorsplitter" ), editorSplit->saveState() );
231  settings.setValue( QStringLiteral( "Windows/QgsExpressionBuilderWidget/functionsplitter" ), functionsplit->saveState() );
232 }
233 
235 {
236  mLayer = layer;
237 
238  //TODO - remove existing layer scope from context
239 
240  if ( mLayer )
241  mExpressionContext << QgsExpressionContextUtils::layerScope( mLayer );
242 }
243 
244 void QgsExpressionBuilderWidget::currentChanged( const QModelIndex &index, const QModelIndex & )
245 {
246  txtSearchEditValues->clear();
247 
248  // Get the item
249  QModelIndex idx = mProxyModel->mapToSource( index );
250  QgsExpressionItem *item = dynamic_cast<QgsExpressionItem *>( mModel->itemFromIndex( idx ) );
251  if ( !item )
252  return;
253 
254  bool isField = mLayer && item->getItemType() == QgsExpressionItem::Field;
255  if ( isField )
256  {
257  loadFieldValues( mFieldValues.value( item->text() ) );
258 
259  cbxValuesInUse->setVisible( formatterCanProvideAvailableValues( item->text() ) );
260  cbxValuesInUse->setChecked( false );
261  }
262  mValueGroupBox->setVisible( isField );
263 
264  mShowHelpButton->setText( isField ? tr( "Show Values" ) : tr( "Show Help" ) );
265 
266  // Show the help for the current item.
267  QString help = loadFunctionHelp( item );
268  txtHelpText->setText( help );
269 
270  btnRemoveExpression->setEnabled( item->parent() &&
271  item->parent()->text() == mUserExpressionsGroupName );
272 
273 }
274 
275 void QgsExpressionBuilderWidget::btnRun_pressed()
276 {
277  if ( !cmbFileNames->currentItem() )
278  return;
279 
280  QString file = cmbFileNames->currentItem()->text();
281  saveFunctionFile( file );
282  runPythonCode( txtPython->text() );
283 }
284 
285 void QgsExpressionBuilderWidget::runPythonCode( const QString &code )
286 {
287  if ( QgsPythonRunner::isValid() )
288  {
289  QString pythontext = code;
290  QgsPythonRunner::run( pythontext );
291  }
292  updateFunctionTree();
293  loadFieldNames();
294  loadRecent( mRecentKey );
296 }
297 
299 {
300  QDir myDir( mFunctionsPath );
301  if ( !myDir.exists() )
302  {
303  myDir.mkpath( mFunctionsPath );
304  }
305 
306  if ( !fileName.endsWith( QLatin1String( ".py" ) ) )
307  {
308  fileName.append( ".py" );
309  }
310 
311  fileName = mFunctionsPath + QDir::separator() + fileName;
312  QFile myFile( fileName );
313  if ( myFile.open( QIODevice::WriteOnly | QFile::Truncate ) )
314  {
315  QTextStream myFileStream( &myFile );
316  myFileStream << txtPython->text() << endl;
317  myFile.close();
318  }
319 }
320 
322 {
323  mFunctionsPath = path;
324  QDir dir( path );
325  dir.setNameFilters( QStringList() << QStringLiteral( "*.py" ) );
326  QStringList files = dir.entryList( QDir::Files );
327  cmbFileNames->clear();
328  const auto constFiles = files;
329  for ( const QString &name : constFiles )
330  {
331  QFileInfo info( mFunctionsPath + QDir::separator() + name );
332  if ( info.baseName() == QLatin1String( "__init__" ) ) continue;
333  QListWidgetItem *item = new QListWidgetItem( QgsApplication::getThemeIcon( QStringLiteral( "console/iconTabEditorConsole.svg" ) ), info.baseName() );
334  cmbFileNames->addItem( item );
335  }
336  if ( !cmbFileNames->currentItem() )
337  {
338  cmbFileNames->setCurrentRow( 0 );
339  }
340 
341  if ( cmbFileNames->count() == 0 )
342  {
343  // Create default sample entry.
344  newFunctionFile( "default" );
345  txtPython->setText( QString( "'''\n#Sample custom function file\n "
346  "(uncomment to use and customize or Add button to create a new file) \n%1 \n '''" ).arg( txtPython->text() ) );
347  saveFunctionFile( "default" );
348  }
349 }
350 
351 void QgsExpressionBuilderWidget::newFunctionFile( const QString &fileName )
352 {
353  QList<QListWidgetItem *> items = cmbFileNames->findItems( fileName, Qt::MatchExactly );
354  if ( !items.isEmpty() )
355  return;
356 
357  QListWidgetItem *item = new QListWidgetItem( QgsApplication::getThemeIcon( QStringLiteral( "console/iconTabEditorConsole.svg" ) ), fileName );
358  cmbFileNames->insertItem( 0, item );
359  cmbFileNames->setCurrentRow( 0 );
360 
361  QString templatetxt;
362  QgsPythonRunner::eval( QStringLiteral( "qgis.user.default_expression_template" ), templatetxt );
363  txtPython->setText( templatetxt );
364  saveFunctionFile( fileName );
365 }
366 
367 void QgsExpressionBuilderWidget::btnNewFile_pressed()
368 {
369  bool ok;
370  QString text = QInputDialog::getText( this, tr( "New File" ),
371  tr( "New file name:" ), QLineEdit::Normal,
372  QString(), &ok );
373  if ( ok && !text.isEmpty() )
374  {
375  newFunctionFile( text );
376  }
377 }
378 
379 void QgsExpressionBuilderWidget::cmbFileNames_currentItemChanged( QListWidgetItem *item, QListWidgetItem *lastitem )
380 {
381  if ( lastitem )
382  {
383  QString filename = lastitem->text();
384  saveFunctionFile( filename );
385  }
386  QString path = mFunctionsPath + QDir::separator() + item->text();
387  loadCodeFromFile( path );
388 }
389 
391 {
392  if ( !path.endsWith( QLatin1String( ".py" ) ) )
393  path.append( ".py" );
394 
395  txtPython->loadScript( path );
396 }
397 
399 {
400  txtPython->setText( code );
401 }
402 
403 void QgsExpressionBuilderWidget::expressionTree_doubleClicked( const QModelIndex &index )
404 {
405  QModelIndex idx = mProxyModel->mapToSource( index );
406  QgsExpressionItem *item = dynamic_cast<QgsExpressionItem *>( mModel->itemFromIndex( idx ) );
407  if ( !item )
408  return;
409 
410  // Don't handle the double-click if we are on a header node.
411  if ( item->getItemType() == QgsExpressionItem::Header )
412  return;
413 
414  // Insert the expression text or replace selected text
415  txtExpressionString->insertText( item->getExpressionText() );
416  txtExpressionString->setFocus();
417 }
418 
420 {
421  // TODO We should really return a error the user of the widget that
422  // the there is no layer set.
423  if ( !mLayer )
424  return;
425 
426  loadFieldNames( mLayer->fields() );
427 }
428 
429 
431 {
432  if ( fields.isEmpty() )
433  return;
434 
435  txtExpressionString->setFields( fields );
436 
437  for ( int i = 0; i < fields.count(); ++i )
438  {
439  const QgsField field = fields.at( i );
440  QIcon icon = fields.iconForField( i );
441  registerItem( QStringLiteral( "Fields and Values" ), field.displayNameWithAlias(),
442  " \"" + field.name() + "\" ", QString(), QgsExpressionItem::Field, false, i, icon );
443  }
444 }
445 
446 void QgsExpressionBuilderWidget::loadFieldsAndValues( const QMap<QString, QStringList> &fieldValues )
447 {
448  mFieldValues.clear();
449  QgsFields fields;
450  for ( auto it = fieldValues.constBegin(); it != fieldValues.constEnd(); ++it )
451  {
452  fields.append( QgsField( it.key() ) );
453  const QStringList values = it.value();
454  QVariantMap map;
455  for ( const QString &value : values )
456  {
457  map.insert( value, value );
458  }
459  mFieldValues.insert( it.key(), map );
460  }
461  loadFieldNames( fields );
462 }
463 
464 void QgsExpressionBuilderWidget::loadFieldsAndValues( const QMap<QString, QVariantMap> &fieldValues )
465 {
466  QgsFields fields;
467  for ( auto it = fieldValues.constBegin(); it != fieldValues.constEnd(); ++it )
468  {
469  fields.append( QgsField( it.key() ) );
470  }
471  loadFieldNames( fields );
472  mFieldValues = fieldValues;
473 }
474 
475 void QgsExpressionBuilderWidget::fillFieldValues( const QString &fieldName, int countLimit, bool forceUsedValues )
476 {
477  // TODO We should really return a error the user of the widget that
478  // the there is no layer set.
479  if ( !mLayer )
480  return;
481 
482  // TODO We should thread this so that we don't hold the user up if the layer is massive.
483 
484  const QgsFields fields = mLayer->fields();
485  int fieldIndex = fields.lookupField( fieldName );
486 
487  if ( fieldIndex < 0 )
488  return;
489 
490  const QgsEditorWidgetSetup setup = fields.at( fieldIndex ).editorWidgetSetup();
492 
493  QVariantList values;
494  if ( cbxValuesInUse->isVisible() && !cbxValuesInUse->isChecked() && !forceUsedValues )
495  {
496  QgsFieldFormatterContext fieldFormatterContext;
497  fieldFormatterContext.setProject( mProject );
498  values = formatter->availableValues( setup.config(), countLimit, fieldFormatterContext );
499  }
500  else
501  {
502  values = mLayer->uniqueValues( fieldIndex, countLimit ).toList();
503  }
504  std::sort( values.begin(), values.end() );
505 
506  mValuesModel->clear();
507  for ( const QVariant &value : qgis::as_const( values ) )
508  {
509  QString strValue;
510  if ( value.isNull() )
511  strValue = QStringLiteral( "NULL" );
512  else if ( value.type() == QVariant::Int || value.type() == QVariant::Double || value.type() == QVariant::LongLong )
513  strValue = value.toString();
514  else
515  strValue = '\'' + value.toString().replace( '\'', QLatin1String( "''" ) ) + '\'';
516 
517  QString representedValue = formatter->representValue( mLayer, fieldIndex, setup.config(), QVariant(), value );
518  if ( representedValue != value.toString() )
519  representedValue = representedValue + QStringLiteral( " [" ) + strValue + ']';
520 
521  QStandardItem *item = new QStandardItem( representedValue );
522  item->setData( strValue );
523  mValuesModel->appendRow( item );
524  }
525 }
526 
527 bool QgsExpressionBuilderWidget::formatterCanProvideAvailableValues( const QString &fieldName )
528 {
529  const QgsFields fields = mLayer->fields();
530  int fieldIndex = fields.lookupField( fieldName );
531  if ( fieldIndex != -1 )
532  {
533  const QgsEditorWidgetSetup setup = fields.at( fieldIndex ).editorWidgetSetup();
535 
536  return ( formatter->flags() & QgsFieldFormatter::CanProvideAvailableValues );
537  }
538  return false;
539 }
540 
541 QString QgsExpressionBuilderWidget::getFunctionHelp( QgsExpressionFunction *function )
542 {
543  if ( !function )
544  return QString();
545 
546  QString helpContents = QgsExpression::helpText( function->name() );
547 
548  return QStringLiteral( "<head><style>" ) + helpStylesheet() + QStringLiteral( "</style></head><body>" ) + helpContents + QStringLiteral( "</body>" );
549 
550 }
551 
552 void QgsExpressionBuilderWidget::registerItem( const QString &group,
553  const QString &label,
554  const QString &expressionText,
555  const QString &helpText,
556  QgsExpressionItem::ItemType type, bool highlightedItem, int sortOrder, QIcon icon, const QStringList &tags )
557 {
558  QgsExpressionItem *item = new QgsExpressionItem( label, expressionText, helpText, type );
559  item->setData( label, Qt::UserRole );
560  item->setData( sortOrder, QgsExpressionItem::CUSTOM_SORT_ROLE );
561  item->setData( tags, QgsExpressionItem::SEARCH_TAGS_ROLE );
562  item->setIcon( icon );
563 
564  // Look up the group and insert the new function.
565  if ( mExpressionGroups.contains( group ) )
566  {
567  QgsExpressionItem *groupNode = mExpressionGroups.value( group );
568  groupNode->appendRow( item );
569  }
570  else
571  {
572  // If the group doesn't exist yet we make it first.
573  QgsExpressionItem *newgroupNode = new QgsExpressionItem( QgsExpression::group( group ), QString(), QgsExpressionItem::Header );
574  newgroupNode->setData( group, Qt::UserRole );
575  //Recent group should always be last group
576  newgroupNode->setData( group.startsWith( QLatin1String( "Recent (" ) ) ? 2 : 1, QgsExpressionItem::CUSTOM_SORT_ROLE );
577  newgroupNode->appendRow( item );
578  newgroupNode->setBackground( QBrush( QColor( 238, 238, 238 ) ) );
579  mModel->appendRow( newgroupNode );
580  mExpressionGroups.insert( group, newgroupNode );
581  }
582 
583  if ( highlightedItem )
584  {
585  //insert a copy as a top level item
586  QgsExpressionItem *topLevelItem = new QgsExpressionItem( label, expressionText, helpText, type );
587  topLevelItem->setData( label, Qt::UserRole );
588  item->setData( 0, QgsExpressionItem::CUSTOM_SORT_ROLE );
589  QFont font = topLevelItem->font();
590  font.setBold( true );
591  topLevelItem->setFont( font );
592  mModel->appendRow( topLevelItem );
593  }
594 
595 }
596 
598 {
599  return mExpressionValid;
600 }
601 
602 void QgsExpressionBuilderWidget::saveToRecent( const QString &collection )
603 {
604  QgsSettings settings;
605  QString location = QStringLiteral( "/expressions/recent/%1" ).arg( collection );
606  QStringList expressions = settings.value( location ).toStringList();
607  expressions.removeAll( this->expressionText() );
608 
609  expressions.prepend( this->expressionText() );
610 
611  while ( expressions.count() > 20 )
612  {
613  expressions.pop_back();
614  }
615 
616  settings.setValue( location, expressions );
617  loadRecent( collection );
618 }
619 
620 void QgsExpressionBuilderWidget::loadRecent( const QString &collection )
621 {
622  mRecentKey = collection;
623  QString name = tr( "Recent (%1)" ).arg( collection );
624  if ( mExpressionGroups.contains( name ) )
625  {
626  QgsExpressionItem *node = mExpressionGroups.value( name );
627  node->removeRows( 0, node->rowCount() );
628  }
629 
630  QgsSettings settings;
631  const QString location = QStringLiteral( "/expressions/recent/%1" ).arg( collection );
632  const QStringList expressions = settings.value( location ).toStringList();
633  int i = 0;
634  for ( const QString &expression : expressions )
635  {
636  registerItem( name, expression, expression, expression, QgsExpressionItem::ExpressionNode, false, i );
637  i++;
638  }
639 }
640 
642 {
643  // Cleanup
644  if ( mExpressionGroups.contains( QStringLiteral( "UserGroup" ) ) )
645  {
646  QgsExpressionItem *node = mExpressionGroups.value( QStringLiteral( "UserGroup" ) );
647  node->removeRows( 0, node->rowCount() );
648  }
649 
650  QgsSettings settings;
651  const QString location = QStringLiteral( "user" );
652  settings.beginGroup( location, QgsSettings::Section::Expressions );
653  QString label;
654  QString helpText;
655  QString expression;
656  int i = 0;
657  mUserExpressionLabels = settings.childGroups();
658  for ( const auto &label : qgis::as_const( mUserExpressionLabels ) )
659  {
660  settings.beginGroup( label );
661  expression = settings.value( QStringLiteral( "expression" ) ).toString();
662  helpText = settings.value( QStringLiteral( "helpText" ) ).toString();
663  registerItem( QStringLiteral( "UserGroup" ), label, expression, helpText, QgsExpressionItem::ExpressionNode, false, i++ );
664  settings.endGroup();
665  }
666 }
667 
668 void QgsExpressionBuilderWidget::saveToUserExpressions( const QString &label, const QString expression, const QString &helpText )
669 {
670  QgsSettings settings;
671  const QString location = QStringLiteral( "user" );
672  settings.beginGroup( location, QgsSettings::Section::Expressions );
673  settings.beginGroup( label );
674  settings.setValue( QStringLiteral( "expression" ), expression );
675  settings.setValue( QStringLiteral( "helpText" ), helpText );
677  // Scroll
678  const QModelIndexList idxs { expressionTree->model()->match( expressionTree->model()->index( 0, 0 ),
679  Qt::DisplayRole, label, 1,
680  Qt::MatchFlag::MatchRecursive ) };
681  if ( ! idxs.isEmpty() )
682  {
683  expressionTree->scrollTo( idxs.first() );
684  }
685 }
686 
688 {
689  QgsSettings settings;
690  settings.remove( QStringLiteral( "user/%1" ).arg( label ), QgsSettings::Section::Expressions );
692 }
693 
694 void QgsExpressionBuilderWidget::loadLayers()
695 {
696  if ( !mProject )
697  return;
698 
699  QMap<QString, QgsMapLayer *> layers = mProject->mapLayers();
700  QMap<QString, QgsMapLayer *>::const_iterator layerIt = layers.constBegin();
701  for ( ; layerIt != layers.constEnd(); ++layerIt )
702  {
703  registerItemForAllGroups( QStringList() << tr( "Map Layers" ), layerIt.value()->name(), QStringLiteral( "'%1'" ).arg( layerIt.key() ), formatLayerHelp( layerIt.value() ) );
704  }
705 }
706 
707 void QgsExpressionBuilderWidget::loadRelations()
708 {
709  if ( !mProject )
710  return;
711 
712  QMap<QString, QgsRelation> relations = mProject->relationManager()->relations();
713  QMap<QString, QgsRelation>::const_iterator relIt = relations.constBegin();
714  for ( ; relIt != relations.constEnd(); ++relIt )
715  {
716  registerItemForAllGroups( QStringList() << tr( "Relations" ), relIt->name(), QStringLiteral( "'%1'" ).arg( relIt->id() ), formatRelationHelp( relIt.value() ) );
717  }
718 }
719 
720 void QgsExpressionBuilderWidget::updateFunctionTree()
721 {
722  mModel->clear();
723  mExpressionGroups.clear();
724  // TODO Can we move this stuff to QgsExpression, like the functions?
725  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "+" ), QStringLiteral( " + " ) );
726  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "-" ), QStringLiteral( " - " ) );
727  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "*" ), QStringLiteral( " * " ) );
728  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "/" ), QStringLiteral( " / " ) );
729  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "%" ), QStringLiteral( " % " ) );
730  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "^" ), QStringLiteral( " ^ " ) );
731  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "=" ), QStringLiteral( " = " ) );
732  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "~" ), QStringLiteral( " ~ " ) );
733  registerItem( QStringLiteral( "Operators" ), QStringLiteral( ">" ), QStringLiteral( " > " ) );
734  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "<" ), QStringLiteral( " < " ) );
735  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "<>" ), QStringLiteral( " <> " ) );
736  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "<=" ), QStringLiteral( " <= " ) );
737  registerItem( QStringLiteral( "Operators" ), QStringLiteral( ">=" ), QStringLiteral( " >= " ) );
738  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "[]" ), QStringLiteral( "[ ]" ) );
739  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "||" ), QStringLiteral( " || " ) );
740  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "IN" ), QStringLiteral( " IN " ) );
741  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "LIKE" ), QStringLiteral( " LIKE " ) );
742  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "ILIKE" ), QStringLiteral( " ILIKE " ) );
743  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "IS" ), QStringLiteral( " IS " ) );
744  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "OR" ), QStringLiteral( " OR " ) );
745  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "AND" ), QStringLiteral( " AND " ) );
746  registerItem( QStringLiteral( "Operators" ), QStringLiteral( "NOT" ), QStringLiteral( " NOT " ) );
747 
748  QString casestring = QStringLiteral( "CASE WHEN condition THEN result END" );
749  registerItem( QStringLiteral( "Conditionals" ), QStringLiteral( "CASE" ), casestring );
750 
751  // use -1 as sort order here -- NULL should always show before the field list
752  registerItem( QStringLiteral( "Fields and Values" ), QStringLiteral( "NULL" ), QStringLiteral( "NULL" ), QString(), QgsExpressionItem::ExpressionNode, false, -1 );
753 
754  // Load the functions from the QgsExpression class
755  int count = QgsExpression::functionCount();
756  for ( int i = 0; i < count; i++ )
757  {
759  QString name = func->name();
760  if ( name.startsWith( '_' ) ) // do not display private functions
761  continue;
762  if ( func->isDeprecated() ) // don't show deprecated functions
763  continue;
764  if ( func->isContextual() )
765  {
766  //don't show contextual functions by default - it's up the the QgsExpressionContext
767  //object to provide them if supported
768  continue;
769  }
770  if ( func->params() != 0 )
771  name += '(';
772  else if ( !name.startsWith( '$' ) )
773  name += QLatin1String( "()" );
774  registerItemForAllGroups( func->groups(), func->name(), ' ' + name + ' ', func->helpText(), QgsExpressionItem::ExpressionNode, mExpressionContext.isHighlightedFunction( func->name() ), 1, QgsExpression::tags( func->name() ) );
775  }
776 
777  // load relation names
778  loadRelations();
779 
780  // load layer IDs
781  loadLayers();
782 
783  loadExpressionContext();
784 }
785 
787 {
788  mDa = da;
789 }
790 
792 {
793  return txtExpressionString->text();
794 }
795 
796 void QgsExpressionBuilderWidget::setExpressionText( const QString &expression )
797 {
798  txtExpressionString->setText( expression );
799 }
800 
802 {
803  return lblExpected->text();
804 }
805 
807 {
808  lblExpected->setText( expected );
809  mExpectedOutputFrame->setVisible( !expected.isNull() );
810 }
811 
813 {
814  mExpressionContext = context;
815  updateFunctionTree();
816  loadFieldNames();
817  loadRecent( mRecentKey );
819 }
820 
821 void QgsExpressionBuilderWidget::txtExpressionString_textChanged()
822 {
823  QString text = expressionText();
824  clearErrors();
825 
826  btnClearEditor->setEnabled( ! txtExpressionString->text().isEmpty() );
827  btnSaveExpression->setEnabled( false );
828 
829  // If the string is empty the expression will still "fail" although
830  // we don't show the user an error as it will be confusing.
831  if ( text.isEmpty() )
832  {
833  lblPreview->clear();
834  lblPreview->setStyleSheet( QString() );
835  txtExpressionString->setToolTip( QString() );
836  lblPreview->setToolTip( QString() );
837  emit expressionParsed( false );
838  setParserError( true );
839  setEvalError( true );
840  return;
841  }
842 
843 
844  QgsExpression exp( text );
845 
846  if ( mLayer )
847  {
848  // Only set calculator if we have layer, else use default.
849  exp.setGeomCalculator( &mDa );
850 
851  if ( !mExpressionContext.feature().isValid() )
852  {
853  // no feature passed yet, try to get from layer
854  QgsFeature f;
855  mLayer->getFeatures( QgsFeatureRequest().setLimit( 1 ) ).nextFeature( f );
856  mExpressionContext.setFeature( f );
857  }
858  }
859 
860  QVariant value = exp.evaluate( &mExpressionContext );
861  if ( !exp.hasEvalError() )
862  {
863  lblPreview->setText( QgsExpression::formatPreviewString( value ) );
864  }
865 
866  if ( exp.hasParserError() || exp.hasEvalError() )
867  {
868  QString errorString = exp.parserErrorString().replace( "\n", "<br>" );
869  QString tooltip;
870  if ( exp.hasParserError() )
871  tooltip = QStringLiteral( "<b>%1:</b>"
872  "%2" ).arg( tr( "Parser Errors" ), errorString );
873  // Only show the eval error if there is no parser error.
874  if ( !exp.hasParserError() && exp.hasEvalError() )
875  tooltip += QStringLiteral( "<b>%1:</b> %2" ).arg( tr( "Eval Error" ), exp.evalErrorString() );
876 
877  lblPreview->setText( tr( "Expression is invalid <a href=""more"">(more info)</a>" ) );
878  lblPreview->setStyleSheet( QStringLiteral( "color: rgba(255, 6, 10, 255);" ) );
879  txtExpressionString->setToolTip( tooltip );
880  lblPreview->setToolTip( tooltip );
881  emit expressionParsed( false );
882  setParserError( exp.hasParserError() );
883  setEvalError( exp.hasEvalError() );
884  createErrorMarkers( exp.parserErrors() );
885  return;
886  }
887  else
888  {
889  lblPreview->setStyleSheet( QString() );
890  txtExpressionString->setToolTip( QString() );
891  lblPreview->setToolTip( QString() );
892  emit expressionParsed( true );
893  setParserError( false );
894  setEvalError( false );
895  createMarkers( exp.rootNode() );
896  btnSaveExpression->setEnabled( true );
897  }
898 
899 }
900 
901 void QgsExpressionBuilderWidget::loadExpressionContext()
902 {
903  txtExpressionString->setExpressionContext( mExpressionContext );
904  QStringList variableNames = mExpressionContext.filteredVariableNames();
905  const auto constVariableNames = variableNames;
906  for ( const QString &variable : constVariableNames )
907  {
908  registerItem( QStringLiteral( "Variables" ), variable, " @" + variable + ' ',
909  QgsExpression::formatVariableHelp( mExpressionContext.description( variable ), true, mExpressionContext.variable( variable ) ),
911  mExpressionContext.isHighlightedVariable( variable ) );
912  }
913 
914  // Load the functions from the expression context
915  QStringList contextFunctions = mExpressionContext.functionNames();
916  const auto constContextFunctions = contextFunctions;
917  for ( const QString &functionName : constContextFunctions )
918  {
919  QgsExpressionFunction *func = mExpressionContext.function( functionName );
920  QString name = func->name();
921  if ( name.startsWith( '_' ) ) // do not display private functions
922  continue;
923  if ( func->params() != 0 )
924  name += '(';
925  registerItemForAllGroups( func->groups(), func->name(), ' ' + name + ' ', func->helpText(), QgsExpressionItem::ExpressionNode, mExpressionContext.isHighlightedFunction( func->name() ), 1, QgsExpression::tags( func->name() ) );
926  }
927 }
928 
929 void QgsExpressionBuilderWidget::registerItemForAllGroups( const QStringList &groups, const QString &label, const QString &expressionText, const QString &helpText, QgsExpressionItem::ItemType type, bool highlightedItem, int sortOrder, const QStringList &tags )
930 {
931  const auto constGroups = groups;
932  for ( const QString &group : constGroups )
933  {
934  registerItem( group, label, expressionText, helpText, type, highlightedItem, sortOrder, QIcon(), tags );
935  }
936 }
937 
938 QString QgsExpressionBuilderWidget::formatRelationHelp( const QgsRelation &relation ) const
939 {
940  QString text = QStringLiteral( "<p>%1</p>" ).arg( tr( "Inserts the relation ID for the relation named '%1'." ).arg( relation.name() ) );
941  text.append( QStringLiteral( "<p>%1</p>" ).arg( tr( "Current value: '%1'" ).arg( relation.id() ) ) );
942  return text;
943 }
944 
945 QString QgsExpressionBuilderWidget::formatLayerHelp( const QgsMapLayer *layer ) const
946 {
947  QString text = QStringLiteral( "<p>%1</p>" ).arg( tr( "Inserts the layer ID for the layer named '%1'." ).arg( layer->name() ) );
948  text.append( QStringLiteral( "<p>%1</p>" ).arg( tr( "Current value: '%1'" ).arg( layer->id() ) ) );
949  return text;
950 }
951 
953 {
954  return mParserError;
955 }
956 
957 void QgsExpressionBuilderWidget::setParserError( bool parserError )
958 {
959  if ( parserError == mParserError )
960  return;
961 
962  mParserError = parserError;
963  emit parserErrorChanged();
964 }
965 
966 void QgsExpressionBuilderWidget::loadFieldValues( const QVariantMap &values )
967 {
968  mValuesModel->clear();
969  for ( QVariantMap::ConstIterator it = values.constBegin(); it != values.constEnd(); ++ it )
970  {
971  QStandardItem *item = new QStandardItem( it.key() );
972  item->setData( it.value() );
973  mValuesModel->appendRow( item );
974  }
975 }
976 
978 {
979  return mEvalError;
980 }
981 
982 void QgsExpressionBuilderWidget::setEvalError( bool evalError )
983 {
984  if ( evalError == mEvalError )
985  return;
986 
987  mEvalError = evalError;
988  emit evalErrorChanged();
989 }
990 
992 {
993  return mModel.get();
994 }
995 
997 {
998  return mProject;
999 }
1000 
1002 {
1003  mProject = project;
1004  updateFunctionTree();
1005 }
1006 
1008 {
1009  QWidget::showEvent( e );
1010  txtExpressionString->setFocus();
1011 }
1012 
1013 void QgsExpressionBuilderWidget::createErrorMarkers( QList<QgsExpression::ParserError> errors )
1014 {
1015  clearErrors();
1016  for ( const QgsExpression::ParserError &error : errors )
1017  {
1018  int errorFirstLine = error.firstLine - 1 ;
1019  int errorFirstColumn = error.firstColumn - 1;
1020  int errorLastColumn = error.lastColumn - 1;
1021  int errorLastLine = error.lastLine - 1;
1022 
1023  // If we have a unknown error we just mark the point that hit the error for now
1024  // until we can handle others more.
1025  if ( error.errorType == QgsExpression::ParserError::Unknown )
1026  {
1027  errorFirstLine = errorLastLine;
1028  errorFirstColumn = errorLastColumn - 1;
1029  }
1030  txtExpressionString->fillIndicatorRange( errorFirstLine,
1031  errorFirstColumn,
1032  errorLastLine,
1033  errorLastColumn, error.errorType );
1034  }
1035 }
1036 
1037 void QgsExpressionBuilderWidget::createMarkers( const QgsExpressionNode *inNode )
1038 {
1039  switch ( inNode->nodeType() )
1040  {
1041  case QgsExpressionNode::NodeType::ntFunction:
1042  {
1043  const QgsExpressionNodeFunction *node = static_cast<const QgsExpressionNodeFunction *>( inNode );
1044  txtExpressionString->SendScintilla( QsciScintilla::SCI_SETINDICATORCURRENT, FUNCTION_MARKER_ID );
1045  txtExpressionString->SendScintilla( QsciScintilla::SCI_SETINDICATORVALUE, node->fnIndex() );
1046  int start = inNode->parserFirstColumn - 1;
1047  int end = inNode->parserLastColumn - 1;
1048  int start_pos = txtExpressionString->positionFromLineIndex( inNode->parserFirstLine - 1, start );
1049  txtExpressionString->SendScintilla( QsciScintilla::SCI_INDICATORFILLRANGE, start_pos, end - start );
1050  if ( node->args() )
1051  {
1052  const QList< QgsExpressionNode * > nodeList = node->args()->list();
1053  for ( QgsExpressionNode *n : nodeList )
1054  {
1055  createMarkers( n );
1056  }
1057  }
1058  break;
1059  }
1060  case QgsExpressionNode::NodeType::ntLiteral:
1061  {
1062  break;
1063  }
1064  case QgsExpressionNode::NodeType::ntUnaryOperator:
1065  {
1066  const QgsExpressionNodeUnaryOperator *node = static_cast<const QgsExpressionNodeUnaryOperator *>( inNode );
1067  createMarkers( node->operand() );
1068  break;
1069  }
1070  case QgsExpressionNode::NodeType::ntBinaryOperator:
1071  {
1072  const QgsExpressionNodeBinaryOperator *node = static_cast<const QgsExpressionNodeBinaryOperator *>( inNode );
1073  createMarkers( node->opLeft() );
1074  createMarkers( node->opRight() );
1075  break;
1076  }
1077  case QgsExpressionNode::NodeType::ntColumnRef:
1078  {
1079  break;
1080  }
1081  case QgsExpressionNode::NodeType::ntInOperator:
1082  {
1083  const QgsExpressionNodeInOperator *node = static_cast<const QgsExpressionNodeInOperator *>( inNode );
1084  if ( node->list() )
1085  {
1086  const QList< QgsExpressionNode * > nodeList = node->list()->list();
1087  for ( QgsExpressionNode *n : nodeList )
1088  {
1089  createMarkers( n );
1090  }
1091  }
1092  break;
1093  }
1094  case QgsExpressionNode::NodeType::ntCondition:
1095  {
1096  const QgsExpressionNodeCondition *node = static_cast<const QgsExpressionNodeCondition *>( inNode );
1097  for ( QgsExpressionNodeCondition::WhenThen *cond : node->conditions() )
1098  {
1099  createMarkers( cond->whenExp() );
1100  createMarkers( cond->thenExp() );
1101  }
1102  if ( node->elseExp() )
1103  {
1104  createMarkers( node->elseExp() );
1105  }
1106  break;
1107  }
1108  case QgsExpressionNode::NodeType::ntIndexOperator:
1109  {
1110  break;
1111  }
1112  }
1113 }
1114 
1115 void QgsExpressionBuilderWidget::clearFunctionMarkers()
1116 {
1117  int lastLine = txtExpressionString->lines() - 1;
1118  txtExpressionString->clearIndicatorRange( 0, 0, lastLine, txtExpressionString->text( lastLine ).length() - 1, FUNCTION_MARKER_ID );
1119 }
1120 
1121 void QgsExpressionBuilderWidget::clearErrors()
1122 {
1123  int lastLine = txtExpressionString->lines() - 1;
1124  // Note: -1 here doesn't seem to do the clear all like the other functions. Will need to make this a bit smarter.
1125  txtExpressionString->clearIndicatorRange( 0, 0, lastLine, txtExpressionString->text( lastLine ).length(), QgsExpression::ParserError::Unknown );
1126  txtExpressionString->clearIndicatorRange( 0, 0, lastLine, txtExpressionString->text( lastLine ).length(), QgsExpression::ParserError::FunctionInvalidParams );
1127  txtExpressionString->clearIndicatorRange( 0, 0, lastLine, txtExpressionString->text( lastLine ).length(), QgsExpression::ParserError::FunctionUnknown );
1128  txtExpressionString->clearIndicatorRange( 0, 0, lastLine, txtExpressionString->text( lastLine ).length(), QgsExpression::ParserError::FunctionWrongArgs );
1129  txtExpressionString->clearIndicatorRange( 0, 0, lastLine, txtExpressionString->text( lastLine ).length(), QgsExpression::ParserError::FunctionNamedArgsError );
1130 }
1131 
1132 void QgsExpressionBuilderWidget::txtSearchEdit_textChanged()
1133 {
1134  mProxyModel->setFilterWildcard( txtSearchEdit->text() );
1135  if ( txtSearchEdit->text().isEmpty() )
1136  {
1137  expressionTree->collapseAll();
1138  }
1139  else
1140  {
1141  expressionTree->expandAll();
1142  QModelIndex index = mProxyModel->index( 0, 0 );
1143  if ( mProxyModel->hasChildren( index ) )
1144  {
1145  QModelIndex child = mProxyModel->index( 0, 0, index );
1146  expressionTree->selectionModel()->setCurrentIndex( child, QItemSelectionModel::ClearAndSelect );
1147  }
1148  }
1149 }
1150 
1151 void QgsExpressionBuilderWidget::txtSearchEditValues_textChanged()
1152 {
1153  mProxyValues->setFilterCaseSensitivity( Qt::CaseInsensitive );
1154  mProxyValues->setFilterWildcard( txtSearchEditValues->text() );
1155 }
1156 
1157 void QgsExpressionBuilderWidget::lblPreview_linkActivated( const QString &link )
1158 {
1159  Q_UNUSED( link )
1160  QgsMessageViewer *mv = new QgsMessageViewer( this );
1161  mv->setWindowTitle( tr( "More Info on Expression Error" ) );
1162  mv->setMessageAsHtml( txtExpressionString->toolTip() );
1163  mv->exec();
1164 }
1165 
1166 void QgsExpressionBuilderWidget::mValuesListView_doubleClicked( const QModelIndex &index )
1167 {
1168  // Insert the item text or replace selected text
1169  txtExpressionString->insertText( ' ' + index.data( Qt::UserRole + 1 ).toString() + ' ' );
1170  txtExpressionString->setFocus();
1171 }
1172 
1173 void QgsExpressionBuilderWidget::operatorButtonClicked()
1174 {
1175  QPushButton *button = qobject_cast<QPushButton *>( sender() );
1176 
1177  // Insert the button text or replace selected text
1178  txtExpressionString->insertText( ' ' + button->text() + ' ' );
1179  txtExpressionString->setFocus();
1180 }
1181 
1182 void QgsExpressionBuilderWidget::showContextMenu( QPoint pt )
1183 {
1184  QModelIndex idx = expressionTree->indexAt( pt );
1185  idx = mProxyModel->mapToSource( idx );
1186  QgsExpressionItem *item = dynamic_cast<QgsExpressionItem *>( mModel->itemFromIndex( idx ) );
1187  if ( !item )
1188  return;
1189 
1190  if ( item->getItemType() == QgsExpressionItem::Field && mLayer )
1191  {
1192  QMenu *menu = new QMenu( this );
1193  menu->addAction( tr( "Load First 10 Unique Values" ), this, SLOT( loadSampleValues() ) );
1194  menu->addAction( tr( "Load All Unique Values" ), this, SLOT( loadAllValues() ) );
1195 
1196  if ( formatterCanProvideAvailableValues( item->text() ) )
1197  {
1198  menu->addAction( tr( "Load First 10 Unique Used Values" ), this, SLOT( loadSampleUsedValues() ) );
1199  menu->addAction( tr( "Load All Unique Used Values" ), this, SLOT( loadAllUsedValues() ) );
1200  }
1201  menu->popup( expressionTree->mapToGlobal( pt ) );
1202  }
1203 }
1204 
1206 {
1207  QModelIndex idx = mProxyModel->mapToSource( expressionTree->currentIndex() );
1208  QgsExpressionItem *item = dynamic_cast<QgsExpressionItem *>( mModel->itemFromIndex( idx ) );
1209  // TODO We should really return a error the user of the widget that
1210  // the there is no layer set.
1211  if ( !mLayer || !item )
1212  return;
1213 
1214  mValueGroupBox->show();
1215  fillFieldValues( item->text(), 10 );
1216 }
1217 
1219 {
1220  QModelIndex idx = mProxyModel->mapToSource( expressionTree->currentIndex() );
1221  QgsExpressionItem *item = dynamic_cast<QgsExpressionItem *>( mModel->itemFromIndex( idx ) );
1222  // TODO We should really return a error the user of the widget that
1223  // the there is no layer set.
1224  if ( !mLayer || !item )
1225  return;
1226 
1227  mValueGroupBox->show();
1228  fillFieldValues( item->text(), -1 );
1229 }
1230 
1232 {
1233  QModelIndex idx = mProxyModel->mapToSource( expressionTree->currentIndex() );
1234  QgsExpressionItem *item = dynamic_cast<QgsExpressionItem *>( mModel->itemFromIndex( idx ) );
1235  // TODO We should really return a error the user of the widget that
1236  // the there is no layer set.
1237  if ( !mLayer || !item )
1238  return;
1239 
1240  mValueGroupBox->show();
1241  fillFieldValues( item->text(), 10, true );
1242 }
1243 
1245 {
1246  QModelIndex idx = mProxyModel->mapToSource( expressionTree->currentIndex() );
1247  QgsExpressionItem *item = dynamic_cast<QgsExpressionItem *>( mModel->itemFromIndex( idx ) );
1248  // TODO We should really return a error the user of the widget that
1249  // the there is no layer set.
1250  if ( !mLayer || !item )
1251  return;
1252 
1253  mValueGroupBox->show();
1254  fillFieldValues( item->text(), -1, true );
1255 }
1256 
1257 void QgsExpressionBuilderWidget::txtPython_textChanged()
1258 {
1259  lblAutoSave->setText( tr( "Saving…" ) );
1260  if ( mAutoSave )
1261  {
1262  autosave();
1263  }
1264 }
1265 
1267 {
1268  // Don't auto save if not on function editor that would be silly.
1269  if ( tabWidget->currentIndex() != 1 )
1270  return;
1271 
1272  QListWidgetItem *item = cmbFileNames->currentItem();
1273  if ( !item )
1274  return;
1275 
1276  QString file = item->text();
1277  saveFunctionFile( file );
1278  lblAutoSave->setText( QStringLiteral( "Saved" ) );
1279  QGraphicsOpacityEffect *effect = new QGraphicsOpacityEffect();
1280  lblAutoSave->setGraphicsEffect( effect );
1281  QPropertyAnimation *anim = new QPropertyAnimation( effect, "opacity" );
1282  anim->setDuration( 2000 );
1283  anim->setStartValue( 1.0 );
1284  anim->setEndValue( 0.0 );
1285  anim->setEasingCurve( QEasingCurve::OutQuad );
1286  anim->start( QAbstractAnimation::DeleteWhenStopped );
1287 }
1288 
1290 {
1291  const QString expression { this->expressionText() };
1292  QgsExpressionStoreDialog dlg { expression, expression, QString( ), mUserExpressionLabels };
1293  if ( dlg.exec() == QDialog::DialogCode::Accepted )
1294  {
1295  saveToUserExpressions( dlg.label(), dlg.expression(), dlg.helpText() );
1296  }
1297 }
1298 
1300 {
1301 
1302 // Get the item
1303  QModelIndex idx = mProxyModel->mapToSource( expressionTree->currentIndex() );
1304  QgsExpressionItem *item = dynamic_cast<QgsExpressionItem *>( mModel->itemFromIndex( idx ) );
1305  if ( !item )
1306  return;
1307 
1308  // Don't handle remove if we are on a header node or the parent
1309  // is not the user group
1310  if ( item->getItemType() == QgsExpressionItem::Header ||
1311  ( item->parent() && item->parent()->text() != mUserExpressionsGroupName ) )
1312  return;
1313 
1314  if ( QMessageBox::Yes == QMessageBox::question( this, tr( "Remove Stored Expression" ),
1315  tr( "Do you really want to remove stored expressions '%1'?" ).arg( item->text() ),
1316  QMessageBox::Yes | QMessageBox::No ) )
1317  {
1318  removeFromUserExpressions( item->text() );
1319  }
1320 
1321 }
1322 
1323 const QList<QgsExpressionItem *> QgsExpressionBuilderWidget::findExpressions( const QString &label )
1324 {
1325  QList<QgsExpressionItem *> result;
1326  const QList<QStandardItem *> found { mModel->findItems( label, Qt::MatchFlag::MatchRecursive ) };
1327  for ( const auto &item : qgis::as_const( found ) )
1328  {
1329  result.push_back( dynamic_cast<QgsExpressionItem *>( item ) );
1330  }
1331  return result;
1332 }
1333 
1334 void QgsExpressionBuilderWidget::indicatorClicked( int line, int index, Qt::KeyboardModifiers state )
1335 {
1336  if ( state & Qt::ControlModifier )
1337  {
1338  int position = txtExpressionString->positionFromLineIndex( line, index );
1339  long fncIndex = txtExpressionString->SendScintilla( QsciScintilla::SCI_INDICATORVALUEAT, FUNCTION_MARKER_ID, static_cast<long int>( position ) );
1340  QgsExpressionFunction *func = QgsExpression::Functions()[fncIndex];
1341  QString help = getFunctionHelp( func );
1342  txtHelpText->setText( help );
1343  }
1344 }
1345 
1346 void QgsExpressionBuilderWidget::setExpressionState( bool state )
1347 {
1348  mExpressionValid = state;
1349 }
1350 
1351 QString QgsExpressionBuilderWidget::helpStylesheet() const
1352 {
1353  //start with default QGIS report style
1354  QString style = QgsApplication::reportStyleSheet();
1355 
1356  //add some tweaks
1357  style += " .functionname {color: #0a6099; font-weight: bold;} "
1358  " .argument {font-family: monospace; color: #bf0c0c; font-style: italic; } "
1359  " td.argument { padding-right: 10px; }";
1360 
1361  return style;
1362 }
1363 
1364 QString QgsExpressionBuilderWidget::loadFunctionHelp( QgsExpressionItem *expressionItem )
1365 {
1366  if ( !expressionItem )
1367  return QString();
1368 
1369  QString helpContents = expressionItem->getHelpText();
1370 
1371  // Return the function help that is set for the function if there is one.
1372  if ( helpContents.isEmpty() )
1373  {
1374  QString name = expressionItem->data( Qt::UserRole ).toString();
1375 
1376  if ( expressionItem->getItemType() == QgsExpressionItem::Field )
1377  helpContents = QgsExpression::helpText( QStringLiteral( "Field" ) );
1378  else
1379  helpContents = QgsExpression::helpText( name );
1380  }
1381 
1382  return "<head><style>" + helpStylesheet() + "</style></head><body>" + helpContents + "</body>";
1383 }
1384 
1386 {
1387  setFilterCaseSensitivity( Qt::CaseInsensitive );
1388 }
1389 
1390 bool QgsExpressionItemSearchProxy::filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const
1391 {
1392  QModelIndex index = sourceModel()->index( source_row, 0, source_parent );
1393  QgsExpressionItem::ItemType itemType = QgsExpressionItem::ItemType( sourceModel()->data( index, QgsExpressionItem::ITEM_TYPE_ROLE ).toInt() );
1394 
1395  int count = sourceModel()->rowCount( index );
1396  bool matchchild = false;
1397  for ( int i = 0; i < count; ++i )
1398  {
1399  if ( filterAcceptsRow( i, index ) )
1400  {
1401  matchchild = true;
1402  break;
1403  }
1404  }
1405 
1406  if ( itemType == QgsExpressionItem::Header && matchchild )
1407  return true;
1408 
1409  if ( itemType == QgsExpressionItem::Header )
1410  return false;
1411 
1412  // check match of item label or tags
1413  if ( QSortFilterProxyModel::filterAcceptsRow( source_row, source_parent ) )
1414  {
1415  return true;
1416  }
1417  else
1418  {
1419  const QStringList tags = sourceModel()->data( index, QgsExpressionItem::SEARCH_TAGS_ROLE ).toStringList();
1420  for ( const QString &tag : tags )
1421  {
1422  if ( tag.contains( filterRegExp() ) )
1423  return true;
1424  }
1425  }
1426  return false;
1427 }
1428 
1429 bool QgsExpressionItemSearchProxy::lessThan( const QModelIndex &left, const QModelIndex &right ) const
1430 {
1431  int leftSort = sourceModel()->data( left, QgsExpressionItem::CUSTOM_SORT_ROLE ).toInt();
1432  int rightSort = sourceModel()->data( right, QgsExpressionItem::CUSTOM_SORT_ROLE ).toInt();
1433  if ( leftSort != rightSort )
1434  return leftSort < rightSort;
1435 
1436  QString leftString = sourceModel()->data( left, Qt::DisplayRole ).toString();
1437  QString rightString = sourceModel()->data( right, Qt::DisplayRole ).toString();
1438 
1439  //ignore $ prefixes when sorting
1440  if ( leftString.startsWith( '$' ) )
1441  leftString = leftString.mid( 1 );
1442  if ( rightString.startsWith( '$' ) )
1443  rightString = rightString.mid( 1 );
1444 
1445  return QString::localeAwareCompare( leftString, rightString ) < 0;
1446 }
int lookupField(const QString &fieldName) const
Looks up field&#39;s index from the field name.
Definition: qgsfields.cpp:324
bool isValid() const
Returns the validity of this feature.
Definition: qgsfeature.cpp:183
QgsProject * project()
Returns the project currently associated with the widget.
Class for parsing and evaluation of expressions (formerly called "search strings").
QString name
Definition: qgsrelation.h:48
int parserFirstColumn
First column in the parser this node was found.
void saveToUserExpressions(const QString &label, const QString expression, const QString &helpText)
Stores the user expression with given label and helpText.
bool hasParserError() const
Returns true if an error occurred when parsing the input expression.
static QString formatPreviewString(const QVariant &value, bool htmlOutput=true)
Formats an expression result for friendly display to the user.
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
void setProject(QgsProject *project)
Sets the project currently associated with the widget.
void saveFunctionFile(QString fileName)
Saves the current function editor text to the given file.
Base class for all map layer types.
Definition: qgsmaplayer.h:79
bool isHighlightedFunction(const QString &name) const
Returns true if the specified function name is intended to be highlighted to the user.
QgsExpressionNode::NodeList * args() const
Returns a list of arguments specified for the function.
QVariantMap config() const
static QgsFieldFormatterRegistry * fieldFormatterRegistry()
Gets the registry of available field formatters.
QStandardItemModel * model()
Returns a pointer to the dialog&#39;s function item model.
QStringList filteredVariableNames() const
Returns a filtered list of variables names set by all scopes in the context.
QString name
Definition: qgsfield.h:59
int params() const
The number of parameters this function takes.
QStringList groups() const
Returns a list of the groups the function belongs to.
This class is a composition of two QSettings instances:
Definition: qgssettings.h:58
const QList< QgsExpressionItem * > findExpressions(const QString &label)
Returns the list of expression items matching a label.
static QString group(const QString &group)
Returns the translated name for a function group.
void loadSampleUsedValues()
Load used sample values into the sample value area.
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
QIcon iconForField(int fieldIdx) const
Returns an icon corresponding to a field index, based on the field&#39;s type and source.
Definition: qgsfields.cpp:275
void setFeature(const QgsFeature &feature)
Convenience function for setting a feature for the context.
QgsExpressionNode * opRight() const
Returns the node to the right of the operator.
WhenThenList conditions() const
The list of WHEN THEN expression parts of the expression.
void setGeomCalculator(const QgsDistanceArea &da)
Sets geometry calculator used in distance/area calculations.
Details about any parser errors that were found when parsing the expression.
void registerItem(const QString &group, const QString &label, const QString &expressionText, const QString &helpText=QString(), QgsExpressionItem::ItemType type=QgsExpressionItem::ExpressionNode, bool highlightedItem=false, int sortOrder=1, QIcon icon=QIcon(), const QStringList &tags=QStringList())
Registers a node item for the expression builder.
void removeSelectedUserExpression()
Removes the selected expression from the stored user expressions, the selected expression must be a u...
A context for field formatter containing information like the project.
void evalErrorChanged()
Will be set to true if the current expression text reported an eval error with the context...
void loadSampleValues()
Load sample values into the sample value area.
void setLayer(QgsVectorLayer *layer)
Sets layer in order to get the fields and values.
QString id
Definition: qgsrelation.h:45
QVariant evaluate()
Evaluate the feature and return the result.
An expression node for CASE WHEN clauses.
void remove(const QString &key, QgsSettings::Section section=QgsSettings::NoSection)
Removes the setting key and any sub-settings of key in a section.
QString evalErrorString() const
Returns evaluation error.
static QString reportStyleSheet(QgsApplication::StyleSheetType styleSheetType=QgsApplication::StyleSheetType::Qt)
Returns a css style sheet for reports, the styleSheetType argument determines what type of stylesheet...
Container of fields for a vector layer.
Definition: qgsfields.h:42
static QIcon getThemeIcon(const QString &name)
Helper to get a theme icon.
QgsExpressionNode::NodeList * list() const
Returns the list of nodes to search for matching values within.
QList< QgsExpression::ParserError > parserErrors() const
Returns parser error details including location of error.
QgsEditorWidgetSetup editorWidgetSetup() const
Gets the editor widget setup for the field.
Definition: qgsfield.cpp:473
QgsExpressionNode * opLeft() const
Returns the node to the left of the operator.
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override
static QStringList tags(const QString &name)
Returns a string list of search tags for a specified function.
The feature class encapsulates a single feature including its id, geometry and a list of field/values...
Definition: qgsfeature.h:55
void loadRecent(const QString &collection=QStringLiteral("generic"))
Loads the recent expressions from the given collection.
int count() const
Returns number of items.
Definition: qgsfields.cpp:133
QString parserErrorString() const
Returns parser error.
void loadFunctionCode(const QString &code)
Loads code into the function editor.
QVariant variable(const QString &name) const
Fetches a matching variable from the context.
Function was called with invalid args.
void loadCodeFromFile(QString path)
Loads code from the given file into the function editor.
void newFunctionFile(const QString &fileName="scratch")
Creates a new file in the function editor.
Non named function arg used after named arg.
QString description(const QString &name) const
Returns a translated description string for the variable with specified name.
QgsField at(int i) const
Gets field at particular index (must be in range 0..N-1)
Definition: qgsfields.cpp:163
void setExpectedOutputFormat(const QString &expected)
The set expected format string.
void updateFunctionFileList(const QString &path)
Updates the list of function files found at the given path.
static int functionCount()
Returns the number of functions defined in the parser.
QString id() const
Returns the layer&#39;s unique ID, which is used to access this layer from QgsProject.
QgsFields fields() const FINAL
Returns the list of fields of this layer.
bool parserError() const
Will be set to true if the current expression text reports a parser error with the context...
bool isHighlightedVariable(const QString &name) const
Returns true if the specified variable name is intended to be highlighted to the user.
QString expectedOutputFormat()
The set expected format string.
Function was called with the wrong number of args.
static const int SEARCH_TAGS_ROLE
Search tags role.
Expression contexts are used to encapsulate the parameters around which a QgsExpression should be eva...
An expression node for value IN or NOT IN clauses.
This class wraps a request for features to a vector layer (or directly its vector data provider)...
bool append(const QgsField &field, FieldOrigin origin=OriginProvider, int originIndex=-1)
Appends a field. The field must have unique name, otherwise it is rejected (returns false) ...
Definition: qgsfields.cpp:59
Encapsulates a QGIS project, including sets of map layers and their styles, layouts, annotations, canvases, etc.
Definition: qgsproject.h:91
void loadFieldNames()
Loads all the field names from the layer.
Abstract base class for all nodes that can appear in an expression.
static const int ITEM_TYPE_ROLE
Item type role.
Encapsulate a field in an attribute table or data source.
Definition: qgsfield.h:49
void showEvent(QShowEvent *e) override
void removeFromUserExpressions(const QString &label)
Removes the expression label from the user stored expressions.
An expression node for expression functions.
virtual QVariantList availableValues(const QVariantMap &config, int countLimit, const QgsFieldFormatterContext &context) const
Returns a list of the values that would be possible to select with this widget type On a RelationRefe...
void autosave()
Auto save the current Python function code.
QgsExpressionNode * elseExp() const
The ELSE expression used for the condition.
static const QList< QgsExpressionFunction * > & Functions()
static QString formatVariableHelp(const QString &description, bool showValue=true, const QVariant &value=QVariant())
Returns formatted help text for a variable.
QgsFieldFormatter * fieldFormatter(const QString &id) const
Gets a field formatter by its id.
void setExpressionContext(const QgsExpressionContext &context)
Sets the expression context for the widget.
static bool eval(const QString &command, QString &result)
Eval a Python statement.
bool evalError() const
Will be set to true if the current expression text reported an eval error with the context...
A field formatter helps to handle and display values for a field.
QgsExpressionItem::ItemType getItemType() const
Gets the type of expression item, e.g., header, field, ExpressionNode.
static const int CUSTOM_SORT_ROLE
Custom sort order role.
QString name() const
The name of the function.
void beginGroup(const QString &prefix, QgsSettings::Section section=QgsSettings::NoSection)
Appends prefix to the current group.
Definition: qgssettings.cpp:87
A general purpose distance and area calculator, capable of performing ellipsoid based calculations...
A abstract base class for defining QgsExpression functions.
An expression item that can be used in the QgsExpressionBuilderWidget tree.
void setGeomCalculator(const QgsDistanceArea *calc)
Sets the geometry calculator used for distance and area calculations in expressions.
QList< QgsExpressionNode * > list()
Gets a list of all the nodes.
int parserFirstLine
First line in the parser this node was found.
QStringList functionNames() const
Retrieves a list of function names contained in the context.
virtual QString representValue(QgsVectorLayer *layer, int fieldIndex, const QVariantMap &config, const QVariant &cache, const QVariant &value) const
Create a pretty String representation of the value.
QSet< QVariant > uniqueValues(int fieldIndex, int limit=-1) const FINAL
Calculates a list of unique values contained within an attribute in the layer.
const QString helpText() const
The help text for the function.
static bool run(const QString &command, const QString &messageOnError=QString())
Execute a Python statement.
void setValue(const QString &key, const QVariant &value, QgsSettings::Section section=QgsSettings::NoSection)
Sets the value of setting key to value.
QString getExpressionText() const
const QgsExpressionNode * rootNode() const
Returns the root node of the expression.
Holder for the widget type and its configuration for a field.
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override
A unary node is either negative as in boolean (not) or as in numbers (minus).
void loadFieldsAndValues(const QMap< QString, QStringList > &fieldValues)
Loads field names and values from the specified map.
A binary expression operator, which operates on two values.
bool isEmpty() const
Checks whether the container is empty.
Definition: qgsfields.cpp:128
QString expressionText()
Gets the expression string that has been set in the expression area.
static QString helpText(QString name)
Returns the help text for a specified function.
QgsFeature feature() const
Convenience function for retrieving the feature for the context, if set.
void setExpressionText(const QString &expression)
Sets the expression string for the widget.
void parserErrorChanged()
Will be set to true if the current expression text reported a parser error with the context...
QString getHelpText() const
Gets the help text that is associated with this expression item.
QgsFeatureIterator getFeatures(const QgsFeatureRequest &request=QgsFeatureRequest()) const FINAL
Queries the layer for features specified in request.
QString name
Definition: qgsmaplayer.h:83
QgsExpressionBuilderWidget(QWidget *parent=nullptr)
Create a new expression builder widget with an optional parent.
A generic dialog for editing expression text, label and help text.
static QgsExpressionContextScope * layerScope(const QgsMapLayer *layer)
Creates a new scope which contains variables and functions relating to a QgsMapLayer.
bool nextFeature(QgsFeature &f)
void setProject(QgsProject *project)
Sets the project used in field formatter.
bool hasEvalError() const
Returns true if an error occurred when evaluating last input.
Represents a vector layer which manages a vector based data sets.
void expressionParsed(bool isValid)
Emitted when the user changes the expression in the widget.
void loadAllUsedValues()
Load all unique values from the set layer into the sample area.
virtual bool isDeprecated() const
Returns true if the function is deprecated and should not be presented as a valid option to users in ...
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:97
virtual QgsExpressionNode::NodeType nodeType() const =0
Gets the type of this node.
QgsExpressionNode * operand() const
Returns the node the operator will operate upon.
void saveToRecent(const QString &collection="generic")
Adds the current expression to the given collection.
static bool isValid()
Returns true if the runner has an instance (and thus is able to run commands)
void storeCurrentUserExpression()
Adds the current expressions to the stored user expressions.
Represents a "WHEN... THEN..." portation of a CASE WHEN clause in an expression.
void loadUserExpressions()
Loads the user expressions.
void loadAllValues()
Load all unique values from the set layer into the sample area.
QgsExpressionFunction * function(const QString &name) const
Fetches a matching function from the context.
int parserLastColumn
Last column in the parser this node was found.
bool isContextual() const
Returns whether the function is only available if provided by a QgsExpressionContext object...
Flags flags() const
Returns the flags.
int fnIndex() const
Returns the index of the node&#39;s function.