QGIS API Documentation  2.8.2-Wien
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Groups 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 }