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 <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  registerItem( func->group(), func->name(), " " + name + " ", func->helptext() );
412  }
413 
414  QList<QgsExpression::Function*> specials = QgsExpression::specialColumns();
415  for ( int i = 0; i < specials.size(); ++i )
416  {
417  QString name = specials[i]->name();
418  registerItem( specials[i]->group(), name, " " + name + " " );
419  }
420 }
421 
423 {
424  mDa = da;
425 }
426 
428 {
429  return txtExpressionString->text();
430 }
431 
432 void QgsExpressionBuilderWidget::setExpressionText( const QString& expression )
433 {
434  txtExpressionString->setText( expression );
435 }
436 
438 {
439  QString text = expressionText();
440 
441  // If the string is empty the expression will still "fail" although
442  // we don't show the user an error as it will be confusing.
443  if ( text.isEmpty() )
444  {
445  lblPreview->setText( "" );
446  lblPreview->setStyleSheet( "" );
447  txtExpressionString->setToolTip( "" );
448  lblPreview->setToolTip( "" );
449  emit expressionParsed( false );
450  return;
451  }
452 
453  QgsExpression exp( text );
454 
455  if ( mLayer )
456  {
457  // Only set calculator if we have layer, else use default.
458  exp.setGeomCalculator( mDa );
459 
460  if ( !mFeature.isValid() )
461  {
462  mLayer->getFeatures().nextFeature( mFeature );
463  }
464 
465  if ( mFeature.isValid() )
466  {
467  QVariant value = exp.evaluate( &mFeature, mLayer->pendingFields() );
468  if ( !exp.hasEvalError() )
469  lblPreview->setText( value.toString() );
470  }
471  else
472  {
473  // The feature is invalid because we don't have one but that doesn't mean user can't
474  // build a expression string. They just get no preview.
475  lblPreview->setText( "" );
476  }
477  }
478  else
479  {
480  // No layer defined
481  QVariant value = exp.evaluate();
482  if ( !exp.hasEvalError() )
483  {
484  lblPreview->setText( value.toString() );
485  }
486  }
487 
488  if ( exp.hasParserError() || exp.hasEvalError() )
489  {
490  QString tooltip = QString( "<b>%1:</b><br>%2" ).arg( tr( "Parser Error" ) ).arg( exp.parserErrorString() );
491  if ( exp.hasEvalError() )
492  tooltip += QString( "<br><br><b>%1:</b><br>%2" ).arg( tr( "Eval Error" ) ).arg( exp.evalErrorString() );
493 
494  lblPreview->setText( tr( "Expression is invalid <a href=""more"">(more info)</a>" ) );
495  lblPreview->setStyleSheet( "color: rgba(255, 6, 10, 255);" );
496  txtExpressionString->setToolTip( tooltip );
497  lblPreview->setToolTip( tooltip );
498  emit expressionParsed( false );
499  return;
500  }
501  else
502  {
503  lblPreview->setStyleSheet( "" );
504  txtExpressionString->setToolTip( "" );
505  lblPreview->setToolTip( "" );
506  emit expressionParsed( true );
507  }
508 }
509 
511 {
512  mProxyModel->setFilterWildcard( txtSearchEdit->text() );
513  if ( txtSearchEdit->text().isEmpty() )
514  expressionTree->collapseAll();
515  else
516  expressionTree->expandAll();
517 }
518 
520 {
521  Q_UNUSED( link );
522  QgsMessageViewer * mv = new QgsMessageViewer( this );
523  mv->setWindowTitle( tr( "More info on expression error" ) );
524  mv->setMessageAsHtml( txtExpressionString->toolTip() );
525  mv->exec();
526 }
527 
529 {
530  // Insert the item text or replace selected text
531  txtExpressionString->insertText( " " + item->text() + " " );
532  txtExpressionString->setFocus();
533 }
534 
536 {
537  QPushButton* button = dynamic_cast<QPushButton*>( sender() );
538 
539  // Insert the button text or replace selected text
540  txtExpressionString->insertText( " " + button->text() + " " );
541  txtExpressionString->setFocus();
542 }
543 
545 {
546  QModelIndex idx = expressionTree->indexAt( pt );
547  idx = mProxyModel->mapToSource( idx );
548  QgsExpressionItem* item = dynamic_cast<QgsExpressionItem*>( mModel->itemFromIndex( idx ) );
549  if ( !item )
550  return;
551 
552  if ( item->getItemType() == QgsExpressionItem::Field && mLayer )
553  {
554  QMenu* menu = new QMenu( this );
555  menu->addAction( tr( "Load top 10 unique values" ), this, SLOT( loadSampleValues() ) );
556  menu->addAction( tr( "Load all unique values" ), this, SLOT( loadAllValues() ) );
557  menu->popup( expressionTree->mapToGlobal( pt ) );
558  }
559 }
560 
562 {
563  QModelIndex idx = mProxyModel->mapToSource( expressionTree->currentIndex() );
564  QgsExpressionItem* item = dynamic_cast<QgsExpressionItem*>( mModel->itemFromIndex( idx ) );
565  // TODO We should really return a error the user of the widget that
566  // the there is no layer set.
567  if ( !mLayer || !item )
568  return;
569 
570  mValueGroupBox->show();
571  int fieldIndex = mLayer->fieldNameIndex( item->text() );
572 
573  fillFieldValues( fieldIndex, 10 );
574 }
575 
577 {
578  QModelIndex idx = mProxyModel->mapToSource( expressionTree->currentIndex() );
579  QgsExpressionItem* item = dynamic_cast<QgsExpressionItem*>( mModel->itemFromIndex( idx ) );
580  // TODO We should really return a error the user of the widget that
581  // the there is no layer set.
582  if ( !mLayer || !item )
583  return;
584 
585  mValueGroupBox->show();
586  int fieldIndex = mLayer->fieldNameIndex( item->text() );
587  fillFieldValues( fieldIndex, -1 );
588 }
589 
590 void QgsExpressionBuilderWidget::setExpressionState( bool state )
591 {
592  mExpressionValid = state;
593 }
594 
595 QString QgsExpressionBuilderWidget::loadFunctionHelp( QgsExpressionItem* expressionItem )
596 {
597  if ( !expressionItem )
598  return "";
599 
600  QString helpContents = expressionItem->getHelpText();
601 
602  // Return the function help that is set for the function if there is one.
603  if ( helpContents.isEmpty() )
604  {
605  QString name = expressionItem->data( Qt::UserRole ).toString();
606 
607  if ( expressionItem->getItemType() == QgsExpressionItem::Field )
608  helpContents = QgsExpression::helptext( "Field" );
609  else
610  helpContents = QgsExpression::helptext( name );
611  }
612 
613  QString myStyle = QgsApplication::reportStyleSheet();
614  return "<head><style>" + myStyle + "</style></head><body>" + helpContents + "</body>";
615 }
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
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: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)