mirror of
https://github.com/qgis/QGIS.git
synced 2025-10-17 00:09:36 -04:00
436 lines
15 KiB
C++
436 lines
15 KiB
C++
/***************************************************************************
|
|
qgisexpressionbuilderwidget.cpp - A genric expression string builder widget.
|
|
--------------------------------------
|
|
Date : 29-May-2011
|
|
Copyright : (C) 2011 by Nathan Woodrow
|
|
Email : woodrow.nathan at gmail dot com
|
|
***************************************************************************
|
|
* *
|
|
* This program is free software; you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation; either version 2 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
***************************************************************************/
|
|
|
|
#include "qgsexpressionbuilderwidget.h"
|
|
#include "qgslogger.h"
|
|
#include "qgsexpression.h"
|
|
#include "qgsmessageviewer.h"
|
|
#include "qgsapplication.h"
|
|
|
|
#include <QSettings>
|
|
#include <QMenu>
|
|
#include <QFile>
|
|
#include <QTextStream>
|
|
|
|
QgsExpressionBuilderWidget::QgsExpressionBuilderWidget( QWidget *parent )
|
|
: QWidget( parent )
|
|
{
|
|
setupUi( this );
|
|
|
|
mValueGroupBox->hide();
|
|
btnLoadAll->hide();
|
|
btnLoadSample->hide();
|
|
highlighter = new QgsExpressionHighlighter( txtExpressionString->document() );
|
|
|
|
mModel = new QStandardItemModel( );
|
|
mProxyModel = new QgsExpressionItemSearchProxy();
|
|
mProxyModel->setSourceModel( mModel );
|
|
expressionTree->setModel( mProxyModel );
|
|
|
|
expressionTree->setContextMenuPolicy( Qt::CustomContextMenu );
|
|
connect( this, SIGNAL( expressionParsed( bool ) ), this, SLOT( setExpressionState( bool ) ) );
|
|
connect( expressionTree, SIGNAL( customContextMenuRequested( const QPoint & ) ), this, SLOT( showContextMenu( const QPoint & ) ) );
|
|
connect( expressionTree->selectionModel(), SIGNAL( currentChanged( const QModelIndex &, const QModelIndex & ) ),
|
|
this, SLOT( currentChanged( const QModelIndex &, const QModelIndex & ) ) );
|
|
|
|
connect( btnLoadAll, SIGNAL( pressed() ), this, SLOT( loadAllValues() ) );
|
|
connect( btnLoadSample, SIGNAL( pressed() ), this, SLOT( loadSampleValues() ) );
|
|
|
|
foreach ( QPushButton* button, mOperatorsGroupBox->findChildren<QPushButton *>() )
|
|
{
|
|
connect( button, SIGNAL( pressed() ), this, SLOT( operatorButtonClicked() ) );
|
|
}
|
|
|
|
// TODO Can we move this stuff to QgsExpression, like the functions?
|
|
registerItem( "Operators", "+", " + ", tr( "Addition operator" ) );
|
|
registerItem( "Operators", "-", " -" , tr( "Subtraction operator" ) );
|
|
registerItem( "Operators", "*", " * ", tr( "Multiplication operator" ) );
|
|
registerItem( "Operators", "/", " / ", tr( "Division operator" ) );
|
|
registerItem( "Operators", "%", " % ", tr( "Modulo operator" ) );
|
|
registerItem( "Operators", "^", " ^ ", tr( "Power operator" ) );
|
|
registerItem( "Operators", "=", " = ", tr( "Equal operator" ) );
|
|
registerItem( "Operators", ">", " > ", tr( "Greater as operator" ) );
|
|
registerItem( "Operators", "<", " < ", tr( "Less than operator" ) );
|
|
registerItem( "Operators", "<>", " <> ", tr( "Unequal operator" ) );
|
|
registerItem( "Operators", "<=", " <= ", tr( "Less or equal operator" ) );
|
|
registerItem( "Operators", ">=", " >= ", tr( "Greater or equal operator" ) );
|
|
registerItem( "Operators", "||", " || ",
|
|
QString( "<b>|| %1</b><br><i>%2</i><br><i>%3:</i>%4" )
|
|
.arg( tr( "(String Concatenation)" ) )
|
|
.arg( tr( "Joins two values together into a string" ) )
|
|
.arg( tr( "Usage" ) )
|
|
.arg( tr( "'Dia' || Diameter" ) ) );
|
|
registerItem( "Operators", "LIKE", " LIKE " );
|
|
registerItem( "Operators", "ILIKE", " ILIKE " );
|
|
registerItem( "Operators", "IS", " IS " );
|
|
registerItem( "Operators", "OR", " OR " );
|
|
registerItem( "Operators", "AND", " AND " );
|
|
registerItem( "Operators", "NOT", " NOT " );
|
|
|
|
QString casestring = "CASE WHEN condition THEN result END";
|
|
QString caseelsestring = "CASE WHEN condition THEN result ELSE result END";
|
|
registerItem( "Conditionals", "CASE", casestring );
|
|
registerItem( "Conditionals", "CASE ELSE", caseelsestring );
|
|
|
|
// Load the functions from the QgsExpression class
|
|
int count = QgsExpression::functionCount();
|
|
for ( int i = 0; i < count; i++ )
|
|
{
|
|
QgsExpression::Function* func = QgsExpression::Functions()[i];
|
|
QString name = func->name();
|
|
if ( name.startsWith( "_" ) ) // do not display private functions
|
|
continue;
|
|
if ( func->params() >= 1 )
|
|
name += "(";
|
|
registerItem( func->group(), func->name(), " " + name + " ", func->helptext() );
|
|
}
|
|
|
|
QList<QgsExpression::Function*> specials = QgsExpression::specialColumns();
|
|
for ( int i = 0; i < specials.size(); ++i )
|
|
{
|
|
QString name = specials[i]->name();
|
|
registerItem( specials[i]->group(), name, " " + name + " " );
|
|
}
|
|
|
|
txtSearchEdit->setPlaceholderText( tr( "Search" ) );
|
|
}
|
|
|
|
|
|
QgsExpressionBuilderWidget::~QgsExpressionBuilderWidget()
|
|
{
|
|
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::setLayer( QgsVectorLayer *layer )
|
|
{
|
|
mLayer = layer;
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::currentChanged( const QModelIndex &index, const QModelIndex & )
|
|
{
|
|
// Get the item
|
|
QModelIndex idx = mProxyModel->mapToSource( index );
|
|
QgsExpressionItem* item = dynamic_cast<QgsExpressionItem*>( mModel->itemFromIndex( idx ) );
|
|
if ( !item )
|
|
return;
|
|
|
|
if ( item->getItemType() != QgsExpressionItem::Field )
|
|
{
|
|
mValueListWidget->clear();
|
|
}
|
|
|
|
btnLoadAll->setVisible( item->getItemType() == QgsExpressionItem::Field && mLayer );
|
|
btnLoadSample->setVisible( item->getItemType() == QgsExpressionItem::Field && mLayer );
|
|
mValueGroupBox->setVisible( item->getItemType() == QgsExpressionItem::Field && mLayer );
|
|
|
|
// Show the help for the current item.
|
|
QString help = loadFunctionHelp( item );
|
|
txtHelpText->setText( help );
|
|
txtHelpText->setToolTip( txtHelpText->toPlainText() );
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::on_expressionTree_doubleClicked( const QModelIndex &index )
|
|
{
|
|
QModelIndex idx = mProxyModel->mapToSource( index );
|
|
QgsExpressionItem* item = dynamic_cast<QgsExpressionItem*>( mModel->itemFromIndex( idx ) );
|
|
if ( item == 0 )
|
|
return;
|
|
|
|
// Don't handle the double click it we are on a header node.
|
|
if ( item->getItemType() == QgsExpressionItem::Header )
|
|
return;
|
|
|
|
// Insert the expression text.
|
|
txtExpressionString->insertPlainText( item->getExpressionText() );
|
|
txtExpressionString->setFocus();
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::loadFieldNames()
|
|
{
|
|
// TODO We should really return a error the user of the widget that
|
|
// the there is no layer set.
|
|
if ( !mLayer )
|
|
return;
|
|
|
|
loadFieldNames( mLayer->pendingFields() );
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::loadFieldNames( const QgsFields& fields )
|
|
{
|
|
if ( fields.isEmpty() )
|
|
return;
|
|
|
|
QStringList fieldNames;
|
|
//foreach ( const QgsField& field, fields )
|
|
for ( int i = 0; i < fields.count(); ++i )
|
|
{
|
|
QString fieldName = fields[i].name();
|
|
fieldNames << fieldName;
|
|
registerItem( "Fields and Values", fieldName, " \"" + fieldName + "\" ", "", QgsExpressionItem::Field );
|
|
}
|
|
highlighter->addFields( fieldNames );
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::fillFieldValues( int fieldIndex, int countLimit )
|
|
{
|
|
// TODO We should really return a error the user of the widget that
|
|
// the there is no layer set.
|
|
if ( !mLayer )
|
|
return;
|
|
|
|
// TODO We should thread this so that we don't hold the user up if the layer is massive.
|
|
mValueListWidget->clear();
|
|
mValueListWidget->setUpdatesEnabled( false );
|
|
mValueListWidget->blockSignals( true );
|
|
|
|
QList<QVariant> values;
|
|
mLayer->uniqueValues( fieldIndex, values, countLimit );
|
|
foreach ( QVariant value, values )
|
|
{
|
|
if ( value.isNull() )
|
|
mValueListWidget->addItem( "NULL" );
|
|
else if ( value.type() == QVariant::Int || value.type() == QVariant::Double || value.type() == QVariant::LongLong )
|
|
mValueListWidget->addItem( value.toString() );
|
|
else
|
|
mValueListWidget->addItem( "'" + value.toString().replace( "'", "''" ) + "'" );
|
|
}
|
|
|
|
mValueListWidget->setUpdatesEnabled( true );
|
|
mValueListWidget->blockSignals( false );
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::registerItem( QString group,
|
|
QString label,
|
|
QString expressionText,
|
|
QString helpText,
|
|
QgsExpressionItem::ItemType type )
|
|
{
|
|
QgsExpressionItem* item = new QgsExpressionItem( label, expressionText, helpText, type );
|
|
item->setData( label, Qt::UserRole );
|
|
// Look up the group and insert the new function.
|
|
if ( mExpressionGroups.contains( group ) )
|
|
{
|
|
QgsExpressionItem *groupNode = mExpressionGroups.value( group );
|
|
groupNode->appendRow( item );
|
|
}
|
|
else
|
|
{
|
|
// If the group doesn't exist yet we make it first.
|
|
QgsExpressionItem *newgroupNode = new QgsExpressionItem( QgsExpression::group( group ), "", QgsExpressionItem::Header );
|
|
newgroupNode->setData( group, Qt::UserRole );
|
|
newgroupNode->appendRow( item );
|
|
mModel->appendRow( newgroupNode );
|
|
mExpressionGroups.insert( group, newgroupNode );
|
|
}
|
|
}
|
|
|
|
bool QgsExpressionBuilderWidget::isExpressionValid()
|
|
{
|
|
return mExpressionValid;
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::setGeomCalculator( const QgsDistanceArea & da )
|
|
{
|
|
mDa = da;
|
|
}
|
|
|
|
QString QgsExpressionBuilderWidget::expressionText()
|
|
{
|
|
return txtExpressionString->toPlainText();
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::setExpressionText( const QString& expression )
|
|
{
|
|
txtExpressionString->setPlainText( expression );
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::on_txtExpressionString_textChanged()
|
|
{
|
|
QString text = txtExpressionString->toPlainText();
|
|
|
|
// If the string is empty the expression will still "fail" although
|
|
// we don't show the user an error as it will be confusing.
|
|
if ( text.isEmpty() )
|
|
{
|
|
lblPreview->setText( "" );
|
|
lblPreview->setStyleSheet( "" );
|
|
txtExpressionString->setToolTip( "" );
|
|
lblPreview->setToolTip( "" );
|
|
emit expressionParsed( false );
|
|
return;
|
|
}
|
|
|
|
|
|
|
|
QgsExpression exp( text );
|
|
|
|
if ( mLayer )
|
|
{
|
|
// Only set calculator if we have layer, else use default.
|
|
exp.setGeomCalculator( mDa );
|
|
|
|
if ( !mFeature.isValid() )
|
|
{
|
|
mLayer->getFeatures( QgsFeatureRequest().setFlags(( mLayer->geometryType() != QGis::NoGeometry && exp.needsGeometry() ) ? QgsFeatureRequest::NoFlags : QgsFeatureRequest::NoGeometry ) ).nextFeature( mFeature );
|
|
}
|
|
|
|
if ( mFeature.isValid() )
|
|
{
|
|
QVariant value = exp.evaluate( &mFeature, mLayer->pendingFields() );
|
|
if ( !exp.hasEvalError() )
|
|
lblPreview->setText( value.toString() );
|
|
}
|
|
else
|
|
{
|
|
// The feature is invalid because we don't have one but that doesn't mean user can't
|
|
// build a expression string. They just get no preview.
|
|
lblPreview->setText( "" );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// No layer defined
|
|
QVariant value = exp.evaluate();
|
|
if ( !exp.hasEvalError() )
|
|
{
|
|
lblPreview->setText( value.toString() );
|
|
}
|
|
}
|
|
|
|
if ( exp.hasParserError() || exp.hasEvalError() )
|
|
{
|
|
QString tooltip = QString( "<b>%1:</b><br>%2" ).arg( tr( "Parser Error" ) ).arg( exp.parserErrorString() );
|
|
if ( exp.hasEvalError() )
|
|
tooltip += QString( "<br><br><b>%1:</b><br>%2" ).arg( tr( "Eval Error" ) ).arg( exp.evalErrorString() );
|
|
|
|
lblPreview->setText( tr( "Expression is invalid <a href=""more"">(more info)</a>" ) );
|
|
lblPreview->setStyleSheet( "color: rgba(255, 6, 10, 255);" );
|
|
txtExpressionString->setToolTip( tooltip );
|
|
lblPreview->setToolTip( tooltip );
|
|
emit expressionParsed( false );
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
lblPreview->setStyleSheet( "" );
|
|
txtExpressionString->setToolTip( "" );
|
|
lblPreview->setToolTip( "" );
|
|
emit expressionParsed( true );
|
|
}
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::on_txtSearchEdit_textChanged()
|
|
{
|
|
mProxyModel->setFilterWildcard( txtSearchEdit->text() );
|
|
if ( txtSearchEdit->text().isEmpty() )
|
|
expressionTree->collapseAll();
|
|
else
|
|
expressionTree->expandAll();
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::on_lblPreview_linkActivated( QString link )
|
|
{
|
|
Q_UNUSED( link );
|
|
QgsMessageViewer * mv = new QgsMessageViewer( this );
|
|
mv->setWindowTitle( tr( "More info on expression error" ) );
|
|
mv->setMessageAsHtml( txtExpressionString->toolTip() );
|
|
mv->exec();
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::on_mValueListWidget_itemDoubleClicked( QListWidgetItem *item )
|
|
{
|
|
txtExpressionString->insertPlainText( " " + item->text() + " " );
|
|
txtExpressionString->setFocus();
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::operatorButtonClicked()
|
|
{
|
|
QPushButton* button = dynamic_cast<QPushButton*>( sender() );
|
|
txtExpressionString->insertPlainText( " " + button->text() + " " );
|
|
txtExpressionString->setFocus();
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::showContextMenu( const QPoint & pt )
|
|
{
|
|
QModelIndex idx = expressionTree->indexAt( pt );
|
|
idx = mProxyModel->mapToSource( idx );
|
|
QgsExpressionItem* item = dynamic_cast<QgsExpressionItem*>( mModel->itemFromIndex( idx ) );
|
|
if ( !item )
|
|
return;
|
|
|
|
if ( item->getItemType() == QgsExpressionItem::Field && mLayer )
|
|
{
|
|
QMenu* menu = new QMenu( this );
|
|
menu->addAction( tr( "Load top 10 unique values" ), this, SLOT( loadSampleValues() ) );
|
|
menu->addAction( tr( "Load all unique values" ), this, SLOT( loadAllValues() ) );
|
|
menu->popup( expressionTree->mapToGlobal( pt ) );
|
|
}
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::loadSampleValues()
|
|
{
|
|
QModelIndex idx = mProxyModel->mapToSource( expressionTree->currentIndex() );
|
|
QgsExpressionItem* item = dynamic_cast<QgsExpressionItem*>( mModel->itemFromIndex( idx ) );
|
|
// TODO We should really return a error the user of the widget that
|
|
// the there is no layer set.
|
|
if ( !mLayer )
|
|
return;
|
|
|
|
mValueGroupBox->show();
|
|
int fieldIndex = mLayer->fieldNameIndex( item->text() );
|
|
fillFieldValues( fieldIndex, 10 );
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::loadAllValues()
|
|
{
|
|
QModelIndex idx = mProxyModel->mapToSource( expressionTree->currentIndex() );
|
|
QgsExpressionItem* item = dynamic_cast<QgsExpressionItem*>( mModel->itemFromIndex( idx ) );
|
|
// TODO We should really return a error the user of the widget that
|
|
// the there is no layer set.
|
|
if ( !mLayer )
|
|
return;
|
|
|
|
mValueGroupBox->show();
|
|
int fieldIndex = mLayer->fieldNameIndex( item->text() );
|
|
fillFieldValues( fieldIndex, -1 );
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::setExpressionState( bool state )
|
|
{
|
|
mExpressionValid = state;
|
|
}
|
|
|
|
QString QgsExpressionBuilderWidget::loadFunctionHelp( QgsExpressionItem* expressionItem )
|
|
{
|
|
if ( !expressionItem )
|
|
return "";
|
|
|
|
QString helpContents = expressionItem->getHelpText();
|
|
|
|
// Return the function help that is set for the function if there is one.
|
|
if ( helpContents.isEmpty() )
|
|
{
|
|
QString name = expressionItem->data( Qt::UserRole ).toString();
|
|
|
|
if ( expressionItem->getItemType() == QgsExpressionItem::Field )
|
|
helpContents = QgsExpression::helptext( "Field" );
|
|
else
|
|
helpContents = QgsExpression::helptext( name );
|
|
}
|
|
|
|
QString myStyle = QgsApplication::reportStyleSheet();
|
|
return "<head><style>" + myStyle + "</style></head><body>" + helpContents + "</body>";
|
|
}
|