QGIS API Documentation  2.9.0-Master
qgsexpressionbuilderwidget.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgisexpressionbuilderwidget.cpp - A genric 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 "qgsmessageviewer.h"
20 #include "qgsapplication.h"
21 #include "qgspythonrunner.h"
22 
23 #include <QSettings>
24 #include <QMenu>
25 #include <QFile>
26 #include <QTextStream>
27 #include <QDir>
28 #include <QComboBox>
29 
31  : QWidget( parent )
32  , mLayer( NULL )
33  , highlighter( NULL )
34  , mExpressionValid( false )
35 {
36  setupUi( this );
37 
38  mValueGroupBox->hide();
39  mLoadGroupBox->hide();
40 // highlighter = new QgsExpressionHighlighter( txtExpressionString->document() );
41 
42  mModel = new QStandardItemModel();
43  mProxyModel = new QgsExpressionItemSearchProxy();
44  mProxyModel->setSourceModel( mModel );
45  expressionTree->setModel( mProxyModel );
46 
47  expressionTree->setContextMenuPolicy( Qt::CustomContextMenu );
48  connect( this, SIGNAL( expressionParsed( bool ) ), this, SLOT( setExpressionState( bool ) ) );
49  connect( expressionTree, SIGNAL( customContextMenuRequested( const QPoint & ) ), this, SLOT( showContextMenu( const QPoint & ) ) );
50  connect( expressionTree->selectionModel(), SIGNAL( currentChanged( const QModelIndex &, const QModelIndex & ) ),
51  this, SLOT( currentChanged( const QModelIndex &, const QModelIndex & ) ) );
52 
53  connect( btnLoadAll, SIGNAL( pressed() ), this, SLOT( loadAllValues() ) );
54  connect( btnLoadSample, SIGNAL( pressed() ), this, SLOT( loadSampleValues() ) );
55 
56  foreach ( QPushButton* button, mOperatorsGroupBox->findChildren<QPushButton *>() )
57  {
58  connect( button, SIGNAL( pressed() ), this, SLOT( operatorButtonClicked() ) );
59  }
60 
61  txtSearchEdit->setPlaceholderText( tr( "Search" ) );
62 
63  QSettings settings;
64  splitter->restoreState( settings.value( "/windows/QgsExpressionBuilderWidget/splitter" ).toByteArray() );
65  functionsplit->restoreState( settings.value( "/windows/QgsExpressionBuilderWidget/functionsplitter" ).toByteArray() );
66 
67  txtExpressionString->setFoldingVisible( false );
68 
69  updateFunctionTree();
70 
72  {
73  QgsPythonRunner::eval( "qgis.user.expressionspath", mFunctionsPath );
75  // The scratch file gets written each time the widget opens.
76  saveFunctionFile( "scratch" );
77  updateFunctionFileList( mFunctionsPath );
78  }
79  else
80  {
81  tab_2->setEnabled( false );
82  }
83 }
84 
85 
87 {
88  QSettings settings;
89  settings.setValue( "/windows/QgsExpressionBuilderWidget/splitter", splitter->saveState() );
90  settings.setValue( "/windows/QgsExpressionBuilderWidget/functionsplitter", functionsplit->saveState() );
91 }
92 
94 {
95  mLayer = layer;
96 }
97 
98 void QgsExpressionBuilderWidget::currentChanged( const QModelIndex &index, const QModelIndex & )
99 {
100  // Get the item
101  QModelIndex idx = mProxyModel->mapToSource( index );
102  QgsExpressionItem* item = dynamic_cast<QgsExpressionItem*>( mModel->itemFromIndex( idx ) );
103  if ( !item )
104  return;
105 
106  if ( item->getItemType() != QgsExpressionItem::Field )
107  {
108  mValueListWidget->clear();
109  }
110 
111  mLoadGroupBox->setVisible( item->getItemType() == QgsExpressionItem::Field && mLayer );
112  mValueGroupBox->setVisible( item->getItemType() == QgsExpressionItem::Field && mLayer );
113 
114  // Show the help for the current item.
115  QString help = loadFunctionHelp( item );
116  txtHelpText->setText( help );
117  txtHelpText->setToolTip( txtHelpText->toPlainText() );
118 }
119 
121 {
122  saveFunctionFile( cmbFileNames->currentText() );
123  runPythonCode( txtPython->text() );
124 }
125 
126 void QgsExpressionBuilderWidget::runPythonCode( QString code )
127 {
128  if ( QgsPythonRunner::isValid() )
129  {
130  QString pythontext = code;
131  QgsPythonRunner::run( pythontext );
132  }
133  updateFunctionTree();
134 }
135 
137 {
138  QDir myDir( mFunctionsPath );
139  if ( !myDir.exists() )
140  {
141  myDir.mkpath( mFunctionsPath );
142  }
143 
144  if ( !fileName.endsWith( ".py" ) )
145  {
146  fileName.append( ".py" );
147  }
148 
149  fileName = mFunctionsPath + QDir::separator() + fileName;
150  QFile myFile( fileName );
151  if ( myFile.open( QIODevice::WriteOnly ) )
152  {
153  QTextStream myFileStream( &myFile );
154  myFileStream << txtPython->text() << endl;
155  myFile.close();
156  }
157 }
158 
160 {
161  mFunctionsPath = path;
162  QDir dir( path );
163  dir.setNameFilters( QStringList() << "*.py" );
164  QStringList files = dir.entryList( QDir::Files );
165  cmbFileNames->clear();
166  foreach ( QString name, files )
167  {
168  QFileInfo info( mFunctionsPath + QDir::separator() + name );
169  if ( info.baseName() == "__init__" ) continue;
170  cmbFileNames->addItem( info.baseName() );
171  }
172 }
173 
175 {
176  QString templatetxt;
177  QgsPythonRunner::eval( "qgis.user.expressions.template", templatetxt );
178  txtPython->setText( templatetxt );
179  int index = cmbFileNames->findText( fileName );
180  if ( index == -1 )
181  cmbFileNames->setEditText( fileName );
182  else
183  cmbFileNames->setCurrentIndex( index );
184 }
185 
187 {
188  newFunctionFile();
189 }
190 
192 {
193  if ( index == -1 )
194  return;
195 
196  QString path = mFunctionsPath + QDir::separator() + cmbFileNames->currentText();
197  loadCodeFromFile( path );
198 }
199 
201 {
202  if ( !path.endsWith( ".py" ) )
203  path.append( ".py" );
204 
205  txtPython->loadScript( path );
206 }
207 
209 {
210  txtPython->setText( code );
211 }
212 
214 {
215  QString name = cmbFileNames->currentText();
216  saveFunctionFile( name );
217  int index = cmbFileNames->findText( name );
218  if ( index == -1 )
219  {
220  cmbFileNames->addItem( name );
221  cmbFileNames->setCurrentIndex( cmbFileNames->count() - 1 );
222  }
223 }
224 
226 {
227  QModelIndex idx = mProxyModel->mapToSource( index );
228  QgsExpressionItem* item = dynamic_cast<QgsExpressionItem*>( mModel->itemFromIndex( idx ) );
229  if ( item == 0 )
230  return;
231 
232  // Don't handle the double click it we are on a header node.
233  if ( item->getItemType() == QgsExpressionItem::Header )
234  return;
235 
236  // Insert the expression text or replace selected text
237  txtExpressionString->insertText( item->getExpressionText() );
238  txtExpressionString->setFocus();
239 }
240 
242 {
243  // TODO We should really return a error the user of the widget that
244  // the there is no layer set.
245  if ( !mLayer )
246  return;
247 
248  loadFieldNames( mLayer->pendingFields() );
249 }
250 
252 {
253  if ( fields.isEmpty() )
254  return;
255 
256  QStringList fieldNames;
257  //foreach ( const QgsField& field, fields )
258  for ( int i = 0; i < fields.count(); ++i )
259  {
260  QString fieldName = fields[i].name();
261  fieldNames << fieldName;
262  registerItem( "Fields and Values", fieldName, " \"" + fieldName + "\" ", "", QgsExpressionItem::Field );
263  }
264 // highlighter->addFields( fieldNames );
265 }
266 
267 void QgsExpressionBuilderWidget::fillFieldValues( int fieldIndex, int countLimit )
268 {
269  // TODO We should really return a error the user of the widget that
270  // the there is no layer set.
271  if ( !mLayer )
272  return;
273 
274  // TODO We should thread this so that we don't hold the user up if the layer is massive.
275  mValueListWidget->clear();
276 
277  if ( fieldIndex < 0 )
278  return;
279 
280  mValueListWidget->setUpdatesEnabled( false );
281  mValueListWidget->blockSignals( true );
282 
283  QList<QVariant> values;
284  mLayer->uniqueValues( fieldIndex, values, countLimit );
285  foreach ( QVariant value, values )
286  {
287  if ( value.isNull() )
288  mValueListWidget->addItem( "NULL" );
289  else if ( value.type() == QVariant::Int || value.type() == QVariant::Double || value.type() == QVariant::LongLong )
290  mValueListWidget->addItem( value.toString() );
291  else
292  mValueListWidget->addItem( "'" + value.toString().replace( "'", "''" ) + "'" );
293  }
294 
295  mValueListWidget->setUpdatesEnabled( true );
296  mValueListWidget->blockSignals( false );
297 }
298 
300  QString label,
301  QString expressionText,
302  QString helpText,
304 {
305  QgsExpressionItem* item = new QgsExpressionItem( label, expressionText, helpText, type );
306  item->setData( label, Qt::UserRole );
307  // Look up the group and insert the new function.
308  if ( mExpressionGroups.contains( group ) )
309  {
310  QgsExpressionItem *groupNode = mExpressionGroups.value( group );
311  groupNode->appendRow( item );
312  }
313  else
314  {
315  // If the group doesn't exist yet we make it first.
317  newgroupNode->setData( group, Qt::UserRole );
318  newgroupNode->appendRow( item );
319  mModel->appendRow( newgroupNode );
320  mExpressionGroups.insert( group, newgroupNode );
321  }
322 }
323 
325 {
326  return mExpressionValid;
327 }
328 
330 {
331  QSettings settings;
332  QString location = QString( "/expressions/recent/%1" ).arg( key );
333  QStringList expressions = settings.value( location ).toStringList();
334  expressions.removeAll( this->expressionText() );
335 
336  expressions.prepend( this->expressionText() );
337 
338  while ( expressions.count() > 20 )
339  {
340  expressions.pop_back();
341  }
342 
343  settings.setValue( location, expressions );
344  this->loadRecent( key );
345 }
346 
348 {
349  QString name = tr( "Recent (%1)" ).arg( key );
350  if ( mExpressionGroups.contains( name ) )
351  {
352  QgsExpressionItem* node = mExpressionGroups.value( name );
353  node->removeRows( 0, node->rowCount() );
354  }
355 
356  QSettings settings;
357  QString location = QString( "/expressions/recent/%1" ).arg( key );
358  QStringList expressions = settings.value( location ).toStringList();
359  foreach ( QString expression, expressions )
360  {
361  this->registerItem( name, expression, expression, expression );
362  }
363 }
364 
365 void QgsExpressionBuilderWidget::updateFunctionTree()
366 {
367  mModel->clear();
368  mExpressionGroups.clear();
369  // TODO Can we move this stuff to QgsExpression, like the functions?
370  registerItem( "Operators", "+", " + ", tr( "Addition operator" ) );
371  registerItem( "Operators", "-", " - ", tr( "Subtraction operator" ) );
372  registerItem( "Operators", "*", " * ", tr( "Multiplication operator" ) );
373  registerItem( "Operators", "/", " / ", tr( "Division operator" ) );
374  registerItem( "Operators", "%", " % ", tr( "Modulo operator" ) );
375  registerItem( "Operators", "^", " ^ ", tr( "Power operator" ) );
376  registerItem( "Operators", "=", " = ", tr( "Equal operator" ) );
377  registerItem( "Operators", ">", " > ", tr( "Greater as operator" ) );
378  registerItem( "Operators", "<", " < ", tr( "Less than operator" ) );
379  registerItem( "Operators", "<>", " <> ", tr( "Unequal operator" ) );
380  registerItem( "Operators", "<=", " <= ", tr( "Less or equal operator" ) );
381  registerItem( "Operators", ">=", " >= ", tr( "Greater or equal operator" ) );
382  registerItem( "Operators", "||", " || ",
383  QString( "<b>|| %1</b><br><i>%2</i><br><i>%3:</i>%4" )
384  .arg( tr( "(String Concatenation)" ) )
385  .arg( tr( "Joins two values together into a string" ) )
386  .arg( tr( "Usage" ) )
387  .arg( tr( "'Dia' || Diameter" ) ) );
388  registerItem( "Operators", "IN", " IN " );
389  registerItem( "Operators", "LIKE", " LIKE " );
390  registerItem( "Operators", "ILIKE", " ILIKE " );
391  registerItem( "Operators", "IS", " IS " );
392  registerItem( "Operators", "OR", " OR " );
393  registerItem( "Operators", "AND", " AND " );
394  registerItem( "Operators", "NOT", " NOT " );
395 
396  QString casestring = "CASE WHEN condition THEN result END";
397  QString caseelsestring = "CASE WHEN condition THEN result ELSE result END";
398  registerItem( "Conditionals", "CASE", casestring );
399  registerItem( "Conditionals", "CASE ELSE", caseelsestring );
400 
401  // Load the functions from the QgsExpression class
402  int count = QgsExpression::functionCount();
403  for ( int i = 0; i < count; i++ )
404  {
406  QString name = func->name();
407  if ( name.startsWith( "_" ) ) // do not display private functions
408  continue;
409  if ( func->params() != 0 )
410  name += "(";
411  else if ( !name.startsWith( "$" ) )
412  name += "()";
413  registerItem( func->group(), func->name(), " " + name + " ", func->helptext() );
414  }
415 
416  QList<QgsExpression::Function*> specials = QgsExpression::specialColumns();
417  for ( int i = 0; i < specials.size(); ++i )
418  {
419  QString name = specials[i]->name();
420  registerItem( specials[i]->group(), name, " " + name + " " );
421  }
422 }
423 
425 {
426  mDa = da;
427 }
428 
430 {
431  return txtExpressionString->text();
432 }
433 
434 void QgsExpressionBuilderWidget::setExpressionText( const QString& expression )
435 {
436  txtExpressionString->setText( expression );
437 }
438 
440 {
441  QString text = expressionText();
442 
443  // If the string is empty the expression will still "fail" although
444  // we don't show the user an error as it will be confusing.
445  if ( text.isEmpty() )
446  {
447  lblPreview->setText( "" );
448  lblPreview->setStyleSheet( "" );
449  txtExpressionString->setToolTip( "" );
450  lblPreview->setToolTip( "" );
451  emit expressionParsed( false );
452  return;
453  }
454 
455  QgsExpression exp( text );
456 
457  if ( mLayer )
458  {
459  // Only set calculator if we have layer, else use default.
460  exp.setGeomCalculator( mDa );
461 
462  if ( !mFeature.isValid() )
463  {
464  mLayer->getFeatures().nextFeature( mFeature );
465  }
466 
467  if ( mFeature.isValid() )
468  {
469  QVariant value = exp.evaluate( &mFeature, mLayer->pendingFields() );
470  if ( !exp.hasEvalError() )
471  lblPreview->setText( formatPreviewString( value.toString() ) );
472  }
473  else
474  {
475  // The feature is invalid because we don't have one but that doesn't mean user can't
476  // build a expression string. They just get no preview.
477  lblPreview->setText( "" );
478  }
479  }
480  else
481  {
482  // No layer defined
483  QVariant value = exp.evaluate();
484  if ( !exp.hasEvalError() )
485  {
486  lblPreview->setText( formatPreviewString( value.toString() ) );
487  }
488  }
489 
490  if ( exp.hasParserError() || exp.hasEvalError() )
491  {
492  QString tooltip = QString( "<b>%1:</b><br>%2" ).arg( tr( "Parser Error" ) ).arg( exp.parserErrorString() );
493  if ( exp.hasEvalError() )
494  tooltip += QString( "<br><br><b>%1:</b><br>%2" ).arg( tr( "Eval Error" ) ).arg( exp.evalErrorString() );
495 
496  lblPreview->setText( tr( "Expression is invalid <a href=""more"">(more info)</a>" ) );
497  lblPreview->setStyleSheet( "color: rgba(255, 6, 10, 255);" );
498  txtExpressionString->setToolTip( tooltip );
499  lblPreview->setToolTip( tooltip );
500  emit expressionParsed( false );
501  return;
502  }
503  else
504  {
505  lblPreview->setStyleSheet( "" );
506  txtExpressionString->setToolTip( "" );
507  lblPreview->setToolTip( "" );
508  emit expressionParsed( true );
509  }
510 }
511 
512 QString QgsExpressionBuilderWidget::formatPreviewString( const QString& previewString ) const
513 {
514  if ( previewString.length() > 63 )
515  {
516  return QString( tr( "%1..." ) ).arg( previewString.left( 60 ) );
517  }
518  else
519  {
520  return previewString;
521  }
522 }
523 
525 {
526  mProxyModel->setFilterWildcard( txtSearchEdit->text() );
527  if ( txtSearchEdit->text().isEmpty() )
528  expressionTree->collapseAll();
529  else
530  expressionTree->expandAll();
531 }
532 
534 {
535  Q_UNUSED( link );
536  QgsMessageViewer * mv = new QgsMessageViewer( this );
537  mv->setWindowTitle( tr( "More info on expression error" ) );
538  mv->setMessageAsHtml( txtExpressionString->toolTip() );
539  mv->exec();
540 }
541 
543 {
544  // Insert the item text or replace selected text
545  txtExpressionString->insertText( " " + item->text() + " " );
546  txtExpressionString->setFocus();
547 }
548 
550 {
551  QPushButton* button = dynamic_cast<QPushButton*>( sender() );
552 
553  // Insert the button text or replace selected text
554  txtExpressionString->insertText( " " + button->text() + " " );
555  txtExpressionString->setFocus();
556 }
557 
559 {
560  QModelIndex idx = expressionTree->indexAt( pt );
561  idx = mProxyModel->mapToSource( idx );
562  QgsExpressionItem* item = dynamic_cast<QgsExpressionItem*>( mModel->itemFromIndex( idx ) );
563  if ( !item )
564  return;
565 
566  if ( item->getItemType() == QgsExpressionItem::Field && mLayer )
567  {
568  QMenu* menu = new QMenu( this );
569  menu->addAction( tr( "Load top 10 unique values" ), this, SLOT( loadSampleValues() ) );
570  menu->addAction( tr( "Load all unique values" ), this, SLOT( loadAllValues() ) );
571  menu->popup( expressionTree->mapToGlobal( pt ) );
572  }
573 }
574 
576 {
577  QModelIndex idx = mProxyModel->mapToSource( expressionTree->currentIndex() );
578  QgsExpressionItem* item = dynamic_cast<QgsExpressionItem*>( mModel->itemFromIndex( idx ) );
579  // TODO We should really return a error the user of the widget that
580  // the there is no layer set.
581  if ( !mLayer || !item )
582  return;
583 
584  mValueGroupBox->show();
585  int fieldIndex = mLayer->fieldNameIndex( item->text() );
586 
587  fillFieldValues( fieldIndex, 10 );
588 }
589 
591 {
592  QModelIndex idx = mProxyModel->mapToSource( expressionTree->currentIndex() );
593  QgsExpressionItem* item = dynamic_cast<QgsExpressionItem*>( mModel->itemFromIndex( idx ) );
594  // TODO We should really return a error the user of the widget that
595  // the there is no layer set.
596  if ( !mLayer || !item )
597  return;
598 
599  mValueGroupBox->show();
600  int fieldIndex = mLayer->fieldNameIndex( item->text() );
601  fillFieldValues( fieldIndex, -1 );
602 }
603 
604 void QgsExpressionBuilderWidget::setExpressionState( bool state )
605 {
606  mExpressionValid = state;
607 }
608 
609 QString QgsExpressionBuilderWidget::loadFunctionHelp( QgsExpressionItem* expressionItem )
610 {
611  if ( !expressionItem )
612  return "";
613 
614  QString helpContents = expressionItem->getHelpText();
615 
616  // Return the function help that is set for the function if there is one.
617  if ( helpContents.isEmpty() )
618  {
619  QString name = expressionItem->data( Qt::UserRole ).toString();
620 
621  if ( expressionItem->getItemType() == QgsExpressionItem::Field )
622  helpContents = QgsExpression::helptext( "Field" );
623  else
624  helpContents = QgsExpression::helptext( name );
625  }
626 
627  QString myStyle = QgsApplication::reportStyleSheet();
628  return "<head><style>" + myStyle + "</style></head><body>" + helpContents + "</body>";
629 }
Class for parsing and evaluation of expressions (formerly called "search strings").
Definition: qgsexpression.h:86
bool hasEvalError() const
Returns true if an error occurred when evaluating last input.
static unsigned index
void saveFunctionFile(QString fileName)
Save the current function editor text to the given file.
bool hasParserError() const
Returns true if an error occurred when parsing the input expression.
Definition: qgsexpression.h:93
bool isValid() const
Returns the validity of this feature.
Definition: qgsfeature.cpp:168
QVariant evaluate(const QgsFeature *f=NULL)
Evaluate the feature and return the result.
A abstract base class for defining QgsExpression functions.
void setGeomCalculator(const QgsDistanceArea &da)
Sets geometry calculator used in distance/area calculations.
void uniqueValues(int index, QList< QVariant > &uniqueValues, int limit=-1)
Returns unique values for column.
QgsFeatureIterator getFeatures(const QgsFeatureRequest &request=QgsFeatureRequest())
Query the provider for features specified in request.
void setLayer(QgsVectorLayer *layer)
Sets layer in order to get the fields and values.
static QString helptext(QString name)
static bool eval(QString command, QString &result)
Eval a python statement.
void updateFunctionFileList(QString path)
Update the list of function files found at the given path.
Container of fields for a vector layer.
Definition: qgsfield.h:173
static QString group(QString group)
static QString reportStyleSheet()
get a standard css style sheet for reports.
void loadCodeFromFile(QString path)
Load code from the given file into the function editor.
static const QList< Function * > & Functions()
void setMessageAsHtml(const QString &msg)
static int functionCount()
Returns the number of functions defined in the parser.
Search proxy used to filter the QgsExpressionBuilderWidget tree.
static bool run(QString command, QString messageOnError=QString())
execute a python statement
void on_mValueListWidget_itemDoubleClicked(QListWidgetItem *item)
void loadFunctionCode(QString code)
Load code into the function editor.
int count() const
Return number of items.
Definition: qgsfield.cpp:279
void on_expressionTree_doubleClicked(const QModelIndex &index)
void loadFieldNames()
Loads all the field names from the layer.
QString name()
The name of the function.
void newFunctionFile(QString fileName="scratch")
Create a new file in the function editor.
QgsExpressionItem::ItemType getItemType()
Get the type of expression item eg header, field, ExpressionNode.
General purpose distance and area calculator.
An expression item that can be used in the QgsExpressionBuilderWidget tree.
void currentChanged(const QModelIndex &index, const QModelIndex &)
QString group()
The group the function belongs to.
static QList< Function * > specialColumns()
Returns a list of special Column definitions.
int params()
The number of parameters this function takes.
A generic message view for displaying QGIS messages.
const QString helptext()
The help text for the function.
QString expressionText()
Gets the expression string that has been set in the expression area.
void registerItem(QString group, QString label, QString expressionText, QString helpText="", QgsExpressionItem::ItemType type=QgsExpressionItem::ExpressionNode)
Registers a node item for the expression builder.
void setGeomCalculator(const QgsDistanceArea &calc)
Sets the geometry calculator used in evaluation of expressions,.
void setExpressionText(const QString &expression)
Sets the expression string for the widget.
const QgsFields & pendingFields() const
returns field list in the to-be-committed state
bool nextFeature(QgsFeature &f)
Represents a vector layer which manages a vector based data sets.
int fieldNameIndex(const QString &fieldName) const
Returns the index of a field name or -1 if the field does not exist.
void expressionParsed(bool isValid)
Emitted when the user changes the expression in the widget.
bool isEmpty() const
Check whether the container is empty.
Definition: qgsfield.cpp:274
QString parserErrorString() const
Returns parser error.
Definition: qgsexpression.h:95
QString getHelpText()
Get the help text that is associated with this expression item.
QString evalErrorString() const
Returns evaluation error.
static bool isValid()
returns true if the runner has an instance (and thus is able to run commands)
#define tr(sourceText)