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