Quantum GIS API Documentation
1.8
|
00001 /*************************************************************************** 00002 qgisexpressionbuilderwidget.cpp - A genric expression string builder widget. 00003 -------------------------------------- 00004 Date : 29-May-2011 00005 Copyright : (C) 2011 by Nathan Woodrow 00006 Email : woodrow.nathan at gmail dot com 00007 *************************************************************************** 00008 * * 00009 * This program is free software; you can redistribute it and/or modify * 00010 * it under the terms of the GNU General Public License as published by * 00011 * the Free Software Foundation; either version 2 of the License, or * 00012 * (at your option) any later version. * 00013 * * 00014 ***************************************************************************/ 00015 00016 #include "qgsexpressionbuilderwidget.h" 00017 #include "qgslogger.h" 00018 #include "qgsexpression.h" 00019 #include "qgsmessageviewer.h" 00020 #include "qgsapplication.h" 00021 00022 #include <QSettings> 00023 #include <QMenu> 00024 #include <QFile> 00025 #include <QTextStream> 00026 00027 QgsExpressionBuilderWidget::QgsExpressionBuilderWidget( QWidget *parent ) 00028 : QWidget( parent ) 00029 { 00030 setupUi( this ); 00031 00032 mValueGroupBox->hide(); 00033 // The open and save button are for future. 00034 btnOpen->hide(); 00035 btnSave->hide(); 00036 highlighter = new QgsExpressionHighlighter( txtExpressionString->document() ); 00037 00038 mModel = new QStandardItemModel( ); 00039 mProxyModel = new QgsExpressionItemSearchProxy(); 00040 mProxyModel->setSourceModel( mModel ); 00041 expressionTree->setModel( mProxyModel ); 00042 00043 expressionTree->setContextMenuPolicy( Qt::CustomContextMenu ); 00044 connect( this, SIGNAL( expressionParsed( bool ) ), this, SLOT( setExpressionState( bool ) ) ); 00045 connect( expressionTree, SIGNAL( customContextMenuRequested( const QPoint & ) ), this, SLOT( showContextMenu( const QPoint & ) ) ); 00046 connect( expressionTree->selectionModel(), SIGNAL( currentChanged( const QModelIndex &, const QModelIndex & ) ), 00047 this, SLOT( currentChanged( const QModelIndex &, const QModelIndex & ) ) ); 00048 00049 foreach( QPushButton* button, mOperatorsGroupBox->findChildren<QPushButton *>() ) 00050 { 00051 connect( button, SIGNAL( pressed() ), this, SLOT( operatorButtonClicked() ) ); 00052 } 00053 00054 // TODO Can we move this stuff to QgsExpression, like the functions? 00055 registerItem( tr( "Operators" ), "+", " + " ); 00056 registerItem( tr( "Operators" ), "-", " -" ); 00057 registerItem( tr( "Operators" ), "*", " * " ); 00058 registerItem( tr( "Operators" ), "/", " / " ); 00059 registerItem( tr( "Operators" ), "%", " % " ); 00060 registerItem( tr( "Operators" ), "^", " ^ " ); 00061 registerItem( tr( "Operators" ), "=", " = " ); 00062 registerItem( tr( "Operators" ), ">", " > " ); 00063 registerItem( tr( "Operators" ), "<", " < " ); 00064 registerItem( tr( "Operators" ), "<>", " <> " ); 00065 registerItem( tr( "Operators" ), "<=", " <= " ); 00066 registerItem( tr( "Operators" ), ">=", " >= " ); 00067 registerItem( tr( "Operators" ), "||", " || ", 00068 QString( "<b>|| %1</b><br><i>%2</i><br><i>%3:</i>%4" ) 00069 .arg( tr( "(String Concatenation)" ) ) 00070 .arg( tr( "Joins two values together into a string" ) ) 00071 .arg( tr( "Usage" ) ) 00072 .arg( tr( "'Dia' || Diameter" ) ) ); 00073 registerItem( tr( "Operators" ), "LIKE", " LIKE " ); 00074 registerItem( tr( "Operators" ), "ILIKE", " ILIKE " ); 00075 registerItem( tr( "Operators" ), "IS", " IS NOT " ); 00076 registerItem( tr( "Operators" ), "OR", " OR " ); 00077 registerItem( tr( "Operators" ), "AND", " AND " ); 00078 registerItem( tr( "Operators" ), "NOT", " NOT " ); 00079 00080 00081 // Load the functions from the QgsExpression class 00082 int count = QgsExpression::functionCount(); 00083 for ( int i = 0; i < count; i++ ) 00084 { 00085 QgsExpression::FunctionDef func = QgsExpression::BuiltinFunctions()[i]; 00086 QString name = func.mName; 00087 if ( func.mParams >= 1 ) 00088 name += "("; 00089 registerItem( func.mGroup, func.mName, " " + name + " " ); 00090 }; 00091 } 00092 00093 00094 QgsExpressionBuilderWidget::~QgsExpressionBuilderWidget() 00095 { 00096 00097 } 00098 00099 void QgsExpressionBuilderWidget::setLayer( QgsVectorLayer *layer ) 00100 { 00101 mLayer = layer; 00102 } 00103 00104 void QgsExpressionBuilderWidget::currentChanged( const QModelIndex &index, const QModelIndex & ) 00105 { 00106 // Get the item 00107 QModelIndex idx = mProxyModel->mapToSource( index ); 00108 QgsExpressionItem* item = dynamic_cast<QgsExpressionItem*>( mModel->itemFromIndex( idx ) ); 00109 if ( item == 0 ) 00110 return; 00111 00112 // Loading field values are handled with a 00113 // right click so we just show the help. 00114 if ( item->getItemType() != QgsExpressionItem::Field ) 00115 { 00116 00117 mValueGroupBox->hide(); 00118 mValueListWidget->clear(); 00119 } 00120 // Show the help for the current item. 00121 QString help = loadFunctionHelp( item ); 00122 txtHelpText->setText( help ); 00123 txtHelpText->setToolTip( txtHelpText->toPlainText() ); 00124 } 00125 00126 void QgsExpressionBuilderWidget::on_expressionTree_doubleClicked( const QModelIndex &index ) 00127 { 00128 QModelIndex idx = mProxyModel->mapToSource( index ); 00129 QgsExpressionItem* item = dynamic_cast<QgsExpressionItem*>( mModel->itemFromIndex( idx ) ); 00130 if ( item == 0 ) 00131 return; 00132 00133 // Don't handle the double click it we are on a header node. 00134 if ( item->getItemType() == QgsExpressionItem::Header ) 00135 return; 00136 00137 // Insert the expression text. 00138 txtExpressionString->insertPlainText( item->getExpressionText() ); 00139 } 00140 00141 void QgsExpressionBuilderWidget::loadFieldNames() 00142 { 00143 // TODO We should really return a error the user of the widget that 00144 // the there is no layer set. 00145 if ( !mLayer ) 00146 return; 00147 00148 const QgsFieldMap fieldMap = mLayer->pendingFields(); 00149 loadFieldNames( fieldMap ); 00150 } 00151 00152 void QgsExpressionBuilderWidget::loadFieldNames( QgsFieldMap fields ) 00153 { 00154 if ( fields.isEmpty() ) 00155 return; 00156 00157 QStringList fieldNames; 00158 foreach( QgsField field, fields ) 00159 { 00160 QString fieldName = field.name(); 00161 fieldNames << fieldName; 00162 registerItem( tr( "Fields and Values" ), fieldName, " \"" + fieldName + "\" ", "", QgsExpressionItem::Field ); 00163 } 00164 highlighter->addFields( fieldNames ); 00165 } 00166 00167 void QgsExpressionBuilderWidget::fillFieldValues( int fieldIndex, int countLimit ) 00168 { 00169 // TODO We should really return a error the user of the widget that 00170 // the there is no layer set. 00171 if ( !mLayer ) 00172 return; 00173 00174 // TODO We should thread this so that we don't hold the user up if the layer is massive. 00175 mValueListWidget->clear(); 00176 mValueListWidget->setUpdatesEnabled( false ); 00177 mValueListWidget->blockSignals( true ); 00178 00179 QList<QVariant> values; 00180 mLayer->uniqueValues( fieldIndex, values, countLimit ); 00181 foreach( QVariant value, values ) 00182 { 00183 if ( value.isNull() ) 00184 mValueListWidget->addItem( "NULL" ); 00185 else if ( value.type() == QVariant::Int || value.type() == QVariant::Double || value.type() == QVariant::LongLong ) 00186 mValueListWidget->addItem( value.toString() ); 00187 else 00188 mValueListWidget->addItem( "'" + value.toString().replace( "'", "''" ) + "'" ); 00189 } 00190 00191 mValueListWidget->setUpdatesEnabled( true ); 00192 mValueListWidget->blockSignals( false ); 00193 } 00194 00195 void QgsExpressionBuilderWidget::registerItem( QString group, 00196 QString label, 00197 QString expressionText, 00198 QString helpText, 00199 QgsExpressionItem::ItemType type ) 00200 { 00201 QgsExpressionItem* item = new QgsExpressionItem( label, expressionText, helpText, type ); 00202 // Look up the group and insert the new function. 00203 if ( mExpressionGroups.contains( group ) ) 00204 { 00205 QgsExpressionItem* groupNode = mExpressionGroups.value( group ); 00206 groupNode->appendRow( item ); 00207 } 00208 else 00209 { 00210 // If the group doesn't exist yet we make it first. 00211 QgsExpressionItem* newgroupNode = new QgsExpressionItem( group, "", QgsExpressionItem::Header ); 00212 newgroupNode->appendRow( item ); 00213 mModel->appendRow( newgroupNode ); 00214 mExpressionGroups.insert( group , newgroupNode ); 00215 } 00216 } 00217 00218 bool QgsExpressionBuilderWidget::isExpressionValid() 00219 { 00220 return mExpressionValid; 00221 } 00222 00223 QString QgsExpressionBuilderWidget::expressionText() 00224 { 00225 return txtExpressionString->toPlainText(); 00226 } 00227 00228 void QgsExpressionBuilderWidget::setExpressionText( const QString& expression ) 00229 { 00230 txtExpressionString->setPlainText( expression ); 00231 } 00232 00233 void QgsExpressionBuilderWidget::on_txtExpressionString_textChanged() 00234 { 00235 QString text = txtExpressionString->toPlainText(); 00236 00237 // If the string is empty the expression will still "fail" although 00238 // we don't show the user an error as it will be confusing. 00239 if ( text.isEmpty() ) 00240 { 00241 lblPreview->setText( "" ); 00242 lblPreview->setStyleSheet( "" ); 00243 txtExpressionString->setToolTip( "" ); 00244 lblPreview->setToolTip( "" ); 00245 emit expressionParsed( false ); 00246 return; 00247 } 00248 00249 QgsExpression exp( text ); 00250 00251 // TODO We could do this without a layer. 00252 // Maybe just calling exp.evaluate()? 00253 if ( mLayer ) 00254 { 00255 if ( !mFeature.isValid() ) 00256 { 00257 mLayer->select( mLayer->pendingAllAttributesList(), QgsRectangle(), mLayer->geometryType() != QGis::NoGeometry && exp.needsGeometry() ); 00258 mLayer->nextFeature( mFeature ); 00259 } 00260 00261 if ( mFeature.isValid() ) 00262 { 00263 QVariant value = exp.evaluate( &mFeature, mLayer->pendingFields() ); 00264 if ( !exp.hasEvalError() ) 00265 lblPreview->setText( value.toString() ); 00266 } 00267 else 00268 { 00269 // The feature is invalid because we don't have one but that doesn't mean user can't 00270 // build a expression string. They just get no preview. 00271 lblPreview->setText( "" ); 00272 } 00273 } 00274 00275 if ( exp.hasParserError() || exp.hasEvalError() ) 00276 { 00277 QString tooltip = QString( "<b>%1:</b><br>%2" ).arg( tr( "Parser Error" ) ).arg( exp.parserErrorString() ); 00278 if ( exp.hasEvalError() ) 00279 tooltip += QString( "<br><br><b>%1:</b><br>%2" ).arg( tr( "Eval Error" ) ).arg( exp.evalErrorString() ); 00280 00281 lblPreview->setText( tr( "Expression is invalid <a href=""more"">(more info)</a>" ) ); 00282 lblPreview->setStyleSheet( "color: rgba(255, 6, 10, 255);" ); 00283 txtExpressionString->setToolTip( tooltip ); 00284 lblPreview->setToolTip( tooltip ); 00285 emit expressionParsed( false ); 00286 return; 00287 } 00288 else 00289 { 00290 lblPreview->setStyleSheet( "" ); 00291 txtExpressionString->setToolTip( "" ); 00292 lblPreview->setToolTip( "" ); 00293 emit expressionParsed( true ); 00294 } 00295 } 00296 00297 void QgsExpressionBuilderWidget::on_txtSearchEdit_textChanged() 00298 { 00299 mProxyModel->setFilterWildcard( txtSearchEdit->text() ); 00300 if ( txtSearchEdit->text().isEmpty() ) 00301 expressionTree->collapseAll(); 00302 else 00303 expressionTree->expandAll(); 00304 } 00305 00306 void QgsExpressionBuilderWidget::on_lblPreview_linkActivated( QString link ) 00307 { 00308 Q_UNUSED( link ); 00309 QgsMessageViewer * mv = new QgsMessageViewer( this ); 00310 mv->setWindowTitle( tr( "More info on expression error" ) ); 00311 mv->setMessageAsHtml( txtExpressionString->toolTip() ); 00312 mv->exec(); 00313 } 00314 00315 void QgsExpressionBuilderWidget::on_mValueListWidget_itemDoubleClicked( QListWidgetItem *item ) 00316 { 00317 txtExpressionString->insertPlainText( " " + item->text() + " " ); 00318 } 00319 00320 void QgsExpressionBuilderWidget::operatorButtonClicked() 00321 { 00322 QPushButton* button = dynamic_cast<QPushButton*>( sender() ); 00323 txtExpressionString->insertPlainText( " " + button->text() + " " ); 00324 } 00325 00326 void QgsExpressionBuilderWidget::showContextMenu( const QPoint & pt ) 00327 { 00328 QModelIndex idx = expressionTree->indexAt( pt ); 00329 idx = mProxyModel->mapToSource( idx ); 00330 QgsExpressionItem* item = dynamic_cast<QgsExpressionItem*>( mModel->itemFromIndex( idx ) ); 00331 if ( !item ) 00332 return; 00333 00334 if ( item->getItemType() == QgsExpressionItem::Field ) 00335 { 00336 QMenu* menu = new QMenu( this ); 00337 menu->addAction( tr( "Load top 10 unique values" ), this, SLOT( loadSampleValues() ) ); 00338 menu->addAction( tr( "Load all unique values" ), this, SLOT( loadAllValues() ) ); 00339 menu->popup( expressionTree->mapToGlobal( pt ) ); 00340 } 00341 } 00342 00343 void QgsExpressionBuilderWidget::loadSampleValues() 00344 { 00345 QModelIndex idx = mProxyModel->mapToSource( expressionTree->currentIndex() ); 00346 QgsExpressionItem* item = dynamic_cast<QgsExpressionItem*>( mModel->itemFromIndex( idx ) ); 00347 // TODO We should really return a error the user of the widget that 00348 // the there is no layer set. 00349 if ( !mLayer ) 00350 return; 00351 00352 mValueGroupBox->show(); 00353 int fieldIndex = mLayer->fieldNameIndex( item->text() ); 00354 fillFieldValues( fieldIndex, 10 ); 00355 } 00356 00357 void QgsExpressionBuilderWidget::loadAllValues() 00358 { 00359 QModelIndex idx = mProxyModel->mapToSource( expressionTree->currentIndex() ); 00360 QgsExpressionItem* item = dynamic_cast<QgsExpressionItem*>( mModel->itemFromIndex( idx ) ); 00361 // TODO We should really return a error the user of the widget that 00362 // the there is no layer set. 00363 if ( !mLayer ) 00364 return; 00365 00366 mValueGroupBox->show(); 00367 int fieldIndex = mLayer->fieldNameIndex( item->text() ); 00368 fillFieldValues( fieldIndex, -1 ); 00369 } 00370 00371 void QgsExpressionBuilderWidget::setExpressionState( bool state ) 00372 { 00373 mExpressionValid = state; 00374 } 00375 00376 QString QgsExpressionBuilderWidget::loadFunctionHelp( QgsExpressionItem* functionName ) 00377 { 00378 if ( functionName == NULL ) 00379 return ""; 00380 00381 // set up the path to the help file 00382 QString helpFilesPath = QgsApplication::pkgDataPath() + "/resources/function_help/"; 00383 /* 00384 * determine the locale and create the file name from 00385 * the context id 00386 */ 00387 QString lang = QLocale::system().name(); 00388 00389 QSettings settings; 00390 if ( settings.value( "locale/overrideFlag", false ).toBool() ) 00391 { 00392 QLocale l( settings.value( "locale/userLocale", "en_US" ).toString() ); 00393 lang = l.name(); 00394 } 00395 /* 00396 * If the language isn't set on the system, assume en_US, 00397 * otherwise we get the banner at the top of the help file 00398 * saying it isn't available in "your" language. Some systems 00399 * may be installed without the LANG environment being set. 00400 */ 00401 if ( lang.length() == 0 || lang == "C" || lang.startsWith( "en_" ) ) 00402 { 00403 lang = "en_US"; 00404 } 00405 00406 QString name = functionName->text(); 00407 00408 if ( functionName->getItemType() == QgsExpressionItem::Field ) 00409 name = "Field"; 00410 00411 QString fullHelpPath = helpFilesPath + name + "-" + lang; 00412 // get the help content and title from the localized file 00413 QString helpContents; 00414 QFile file( fullHelpPath ); 00415 00416 QString missingError = tr( "<h3>Oops! QGIS can't find help for this function.</h3>" 00417 "The help file for %1 was not found.<br>" 00418 ).arg( Qt::escape( name ) ); 00419 00420 if ( !lang.startsWith( "en_" ) ) 00421 { 00422 // try en_US version if localized version is unavailable 00423 if ( !file.exists() ) 00424 { 00425 helpContents = tr( "(Showing English version as there was no help available in your language (%1). If you would like to create it, contact the QGIS translation team).<br>" ).arg( lang ); 00426 00427 missingError += tr( "It was neither available in your language (%1) nor English." ).arg( lang ); 00428 00429 // try en_US next 00430 fullHelpPath = helpFilesPath + functionName->text() + "-en_US"; 00431 file.setFileName( fullHelpPath ); 00432 } 00433 } 00434 00435 missingError += tr( "<br>If you would like to create it, contact the QGIS development team." ); 00436 00437 if ( !file.open( QIODevice::ReadOnly | QIODevice::Text ) ) 00438 { 00439 helpContents = missingError; 00440 } 00441 else 00442 { 00443 QTextStream in( &file ); 00444 in.setCodec( "UTF-8" ); // Help files must be in Utf-8 00445 while ( !in.atEnd() ) 00446 { 00447 QString line = in.readLine(); 00448 helpContents += line; 00449 } 00450 } 00451 00452 file.close(); 00453 00454 // Set the browser text to the help contents 00455 QString myStyle = QgsApplication::reportStyleSheet(); 00456 helpContents = "<head><style>" + myStyle + "</style></head><body>" + helpContents + "</body>"; 00457 return helpContents; 00458 }