mirror of
https://github.com/qgis/QGIS.git
synced 2025-04-12 00:06:43 -04:00
948 lines
32 KiB
C++
948 lines
32 KiB
C++
/***************************************************************************
|
|
qgisexpressionbuilderwidget.cpp - A generic 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 "qgsexpressionfunction.h"
|
|
#include "qgsmessageviewer.h"
|
|
#include "qgsapplication.h"
|
|
#include "qgspythonrunner.h"
|
|
#include "qgsgeometry.h"
|
|
#include "qgsfeature.h"
|
|
#include "qgsfeatureiterator.h"
|
|
#include "qgsvectorlayer.h"
|
|
#include "qgssettings.h"
|
|
#include "qgsproject.h"
|
|
#include "qgsrelationmanager.h"
|
|
#include "qgsrelation.h"
|
|
|
|
#include <QMenu>
|
|
#include <QFile>
|
|
#include <QTextStream>
|
|
#include <QDir>
|
|
#include <QInputDialog>
|
|
#include <QComboBox>
|
|
#include <QGraphicsOpacityEffect>
|
|
#include <QPropertyAnimation>
|
|
|
|
|
|
QgsExpressionBuilderWidget::QgsExpressionBuilderWidget( QWidget *parent )
|
|
: QWidget( parent )
|
|
, mProject( QgsProject::instance() )
|
|
{
|
|
setupUi( this );
|
|
connect( btnRun, &QToolButton::pressed, this, &QgsExpressionBuilderWidget::btnRun_pressed );
|
|
connect( btnNewFile, &QToolButton::pressed, this, &QgsExpressionBuilderWidget::btnNewFile_pressed );
|
|
connect( cmbFileNames, &QListWidget::currentItemChanged, this, &QgsExpressionBuilderWidget::cmbFileNames_currentItemChanged );
|
|
connect( expressionTree, &QTreeView::doubleClicked, this, &QgsExpressionBuilderWidget::expressionTree_doubleClicked );
|
|
connect( txtExpressionString, &QgsCodeEditorSQL::textChanged, this, &QgsExpressionBuilderWidget::txtExpressionString_textChanged );
|
|
connect( txtPython, &QgsCodeEditorPython::textChanged, this, &QgsExpressionBuilderWidget::txtPython_textChanged );
|
|
connect( txtSearchEditValues, &QgsFilterLineEdit::textChanged, this, &QgsExpressionBuilderWidget::txtSearchEditValues_textChanged );
|
|
connect( txtSearchEdit, &QgsFilterLineEdit::textChanged, this, &QgsExpressionBuilderWidget::txtSearchEdit_textChanged );
|
|
connect( lblPreview, &QLabel::linkActivated, this, &QgsExpressionBuilderWidget::lblPreview_linkActivated );
|
|
connect( mValuesListView, &QListView::doubleClicked, this, &QgsExpressionBuilderWidget::mValuesListView_doubleClicked );
|
|
|
|
mValueGroupBox->hide();
|
|
mLoadGroupBox->hide();
|
|
// highlighter = new QgsExpressionHighlighter( txtExpressionString->document() );
|
|
|
|
mModel = new QStandardItemModel();
|
|
mProxyModel = new QgsExpressionItemSearchProxy();
|
|
mProxyModel->setDynamicSortFilter( true );
|
|
mProxyModel->setSourceModel( mModel );
|
|
expressionTree->setModel( mProxyModel );
|
|
expressionTree->setSortingEnabled( true );
|
|
expressionTree->sortByColumn( 0, Qt::AscendingOrder );
|
|
|
|
expressionTree->setContextMenuPolicy( Qt::CustomContextMenu );
|
|
connect( this, &QgsExpressionBuilderWidget::expressionParsed, this, &QgsExpressionBuilderWidget::setExpressionState );
|
|
connect( expressionTree, &QWidget::customContextMenuRequested, this, &QgsExpressionBuilderWidget::showContextMenu );
|
|
connect( expressionTree->selectionModel(), &QItemSelectionModel::currentChanged,
|
|
this, &QgsExpressionBuilderWidget::currentChanged );
|
|
|
|
connect( btnLoadAll, &QAbstractButton::pressed, this, &QgsExpressionBuilderWidget::loadAllValues );
|
|
connect( btnLoadSample, &QAbstractButton::pressed, this, &QgsExpressionBuilderWidget::loadSampleValues );
|
|
|
|
Q_FOREACH ( QPushButton *button, mOperatorsGroupBox->findChildren<QPushButton *>() )
|
|
{
|
|
connect( button, &QAbstractButton::pressed, this, &QgsExpressionBuilderWidget::operatorButtonClicked );
|
|
}
|
|
|
|
txtSearchEdit->setPlaceholderText( tr( "Search" ) );
|
|
|
|
mValuesModel = new QStringListModel();
|
|
mProxyValues = new QSortFilterProxyModel();
|
|
mProxyValues->setSourceModel( mValuesModel );
|
|
mValuesListView->setModel( mProxyValues );
|
|
txtSearchEditValues->setPlaceholderText( tr( "Search" ) );
|
|
|
|
QgsSettings settings;
|
|
splitter->restoreState( settings.value( QStringLiteral( "Windows/QgsExpressionBuilderWidget/splitter" ) ).toByteArray() );
|
|
editorSplit->restoreState( settings.value( QStringLiteral( "Windows/QgsExpressionBuilderWidget/editorsplitter" ) ).toByteArray() );
|
|
functionsplit->restoreState( settings.value( QStringLiteral( "Windows/QgsExpressionBuilderWidget/functionsplitter" ) ).toByteArray() );
|
|
|
|
txtExpressionString->setFoldingVisible( false );
|
|
|
|
updateFunctionTree();
|
|
|
|
if ( QgsPythonRunner::isValid() )
|
|
{
|
|
QgsPythonRunner::eval( QStringLiteral( "qgis.user.expressionspath" ), mFunctionsPath );
|
|
updateFunctionFileList( mFunctionsPath );
|
|
}
|
|
else
|
|
{
|
|
tab_2->hide();
|
|
}
|
|
|
|
// select the first item in the function list
|
|
// in order to avoid a blank help widget
|
|
QModelIndex firstItem = mProxyModel->index( 0, 0, QModelIndex() );
|
|
expressionTree->setCurrentIndex( firstItem );
|
|
|
|
lblAutoSave->clear();
|
|
txtExpressionString->setWrapMode( QsciScintilla::WrapWord );
|
|
}
|
|
|
|
|
|
QgsExpressionBuilderWidget::~QgsExpressionBuilderWidget()
|
|
{
|
|
QgsSettings settings;
|
|
settings.setValue( QStringLiteral( "Windows/QgsExpressionBuilderWidget/splitter" ), splitter->saveState() );
|
|
settings.setValue( QStringLiteral( "Windows/QgsExpressionBuilderWidget/editorsplitter" ), editorSplit->saveState() );
|
|
settings.setValue( QStringLiteral( "Windows/QgsExpressionBuilderWidget/functionsplitter" ), functionsplit->saveState() );
|
|
|
|
delete mModel;
|
|
delete mProxyModel;
|
|
delete mValuesModel;
|
|
delete mProxyValues;
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::setLayer( QgsVectorLayer *layer )
|
|
{
|
|
mLayer = layer;
|
|
|
|
//TODO - remove existing layer scope from context
|
|
|
|
if ( mLayer )
|
|
mExpressionContext << QgsExpressionContextUtils::layerScope( mLayer );
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::currentChanged( const QModelIndex &index, const QModelIndex & )
|
|
{
|
|
txtSearchEditValues->clear();
|
|
|
|
// Get the item
|
|
QModelIndex idx = mProxyModel->mapToSource( index );
|
|
QgsExpressionItem *item = dynamic_cast<QgsExpressionItem *>( mModel->itemFromIndex( idx ) );
|
|
if ( !item )
|
|
return;
|
|
|
|
if ( item->getItemType() == QgsExpressionItem::Field && mFieldValues.contains( item->text() ) )
|
|
{
|
|
const QStringList &values = mFieldValues[item->text()];
|
|
mValuesModel->setStringList( values );
|
|
}
|
|
|
|
mLoadGroupBox->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 );
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::btnRun_pressed()
|
|
{
|
|
if ( !cmbFileNames->currentItem() )
|
|
return;
|
|
|
|
QString file = cmbFileNames->currentItem()->text();
|
|
saveFunctionFile( file );
|
|
runPythonCode( txtPython->text() );
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::runPythonCode( const QString &code )
|
|
{
|
|
if ( QgsPythonRunner::isValid() )
|
|
{
|
|
QString pythontext = code;
|
|
QgsPythonRunner::run( pythontext );
|
|
}
|
|
updateFunctionTree();
|
|
loadFieldNames();
|
|
loadRecent( mRecentKey );
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::saveFunctionFile( QString fileName )
|
|
{
|
|
QDir myDir( mFunctionsPath );
|
|
if ( !myDir.exists() )
|
|
{
|
|
myDir.mkpath( mFunctionsPath );
|
|
}
|
|
|
|
if ( !fileName.endsWith( QLatin1String( ".py" ) ) )
|
|
{
|
|
fileName.append( ".py" );
|
|
}
|
|
|
|
fileName = mFunctionsPath + QDir::separator() + fileName;
|
|
QFile myFile( fileName );
|
|
if ( myFile.open( QIODevice::WriteOnly | QFile::Truncate ) )
|
|
{
|
|
QTextStream myFileStream( &myFile );
|
|
myFileStream << txtPython->text() << endl;
|
|
myFile.close();
|
|
}
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::updateFunctionFileList( const QString &path )
|
|
{
|
|
mFunctionsPath = path;
|
|
QDir dir( path );
|
|
dir.setNameFilters( QStringList() << QStringLiteral( "*.py" ) );
|
|
QStringList files = dir.entryList( QDir::Files );
|
|
cmbFileNames->clear();
|
|
Q_FOREACH ( const QString &name, files )
|
|
{
|
|
QFileInfo info( mFunctionsPath + QDir::separator() + name );
|
|
if ( info.baseName() == QLatin1String( "__init__" ) ) continue;
|
|
QListWidgetItem *item = new QListWidgetItem( QgsApplication::getThemeIcon( QStringLiteral( "console/iconTabEditorConsole.png" ) ), info.baseName() );
|
|
cmbFileNames->addItem( item );
|
|
}
|
|
if ( !cmbFileNames->currentItem() )
|
|
cmbFileNames->setCurrentRow( 0 );
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::newFunctionFile( const QString &fileName )
|
|
{
|
|
QList<QListWidgetItem *> items = cmbFileNames->findItems( fileName, Qt::MatchExactly );
|
|
if ( !items.isEmpty() )
|
|
return;
|
|
|
|
QListWidgetItem *item = new QListWidgetItem( QgsApplication::getThemeIcon( QStringLiteral( "console/iconTabEditorConsole.png" ) ), fileName );
|
|
cmbFileNames->insertItem( 0, item );
|
|
cmbFileNames->setCurrentRow( 0 );
|
|
|
|
QString templatetxt;
|
|
QgsPythonRunner::eval( QStringLiteral( "qgis.user.expressions.template" ), templatetxt );
|
|
txtPython->setText( templatetxt );
|
|
saveFunctionFile( fileName );
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::btnNewFile_pressed()
|
|
{
|
|
bool ok;
|
|
QString text = QInputDialog::getText( this, tr( "Enter new file name" ),
|
|
tr( "File name:" ), QLineEdit::Normal,
|
|
QLatin1String( "" ), &ok );
|
|
if ( ok && !text.isEmpty() )
|
|
{
|
|
newFunctionFile( text );
|
|
}
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::cmbFileNames_currentItemChanged( QListWidgetItem *item, QListWidgetItem *lastitem )
|
|
{
|
|
if ( lastitem )
|
|
{
|
|
QString filename = lastitem->text();
|
|
saveFunctionFile( filename );
|
|
}
|
|
QString path = mFunctionsPath + QDir::separator() + item->text();
|
|
loadCodeFromFile( path );
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::loadCodeFromFile( QString path )
|
|
{
|
|
if ( !path.endsWith( QLatin1String( ".py" ) ) )
|
|
path.append( ".py" );
|
|
|
|
txtPython->loadScript( path );
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::loadFunctionCode( const QString &code )
|
|
{
|
|
txtPython->setText( code );
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::expressionTree_doubleClicked( const QModelIndex &index )
|
|
{
|
|
QModelIndex idx = mProxyModel->mapToSource( index );
|
|
QgsExpressionItem *item = dynamic_cast<QgsExpressionItem *>( mModel->itemFromIndex( idx ) );
|
|
if ( !item )
|
|
return;
|
|
|
|
// Don't handle the double-click if we are on a header node.
|
|
if ( item->getItemType() == QgsExpressionItem::Header )
|
|
return;
|
|
|
|
// Insert the expression text or replace selected text
|
|
txtExpressionString->insertText( 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->fields() );
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::loadFieldNames( const QgsFields &fields )
|
|
{
|
|
if ( fields.isEmpty() )
|
|
return;
|
|
|
|
QStringList fieldNames;
|
|
//Q_FOREACH ( const QgsField& field, fields )
|
|
fieldNames.reserve( fields.count() );
|
|
for ( int i = 0; i < fields.count(); ++i )
|
|
{
|
|
QString fieldName = fields.at( i ).name();
|
|
fieldNames << fieldName;
|
|
registerItem( QStringLiteral( "Fields and Values" ), fieldName, " \"" + fieldName + "\" ", QLatin1String( "" ), QgsExpressionItem::Field, false, i );
|
|
}
|
|
// highlighter->addFields( fieldNames );
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::loadFieldsAndValues( const QMap<QString, QStringList> &fieldValues )
|
|
{
|
|
QgsFields fields;
|
|
for ( auto it = fieldValues.constBegin(); it != fieldValues.constEnd(); ++it )
|
|
{
|
|
fields.append( QgsField( it.key() ) );
|
|
}
|
|
loadFieldNames( fields );
|
|
mFieldValues = fieldValues;
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::fillFieldValues( const QString &fieldName, 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.
|
|
|
|
int fieldIndex = mLayer->fields().lookupField( fieldName );
|
|
|
|
if ( fieldIndex < 0 )
|
|
return;
|
|
|
|
QStringList strValues;
|
|
QList<QVariant> values = mLayer->uniqueValues( fieldIndex, countLimit ).toList();
|
|
std::sort( values.begin(), values.end() );
|
|
Q_FOREACH ( const QVariant &value, values )
|
|
{
|
|
QString strValue;
|
|
if ( value.isNull() )
|
|
strValue = QStringLiteral( "NULL" );
|
|
else if ( value.type() == QVariant::Int || value.type() == QVariant::Double || value.type() == QVariant::LongLong )
|
|
strValue = value.toString();
|
|
else
|
|
strValue = '\'' + value.toString().replace( '\'', QLatin1String( "''" ) ) + '\'';
|
|
strValues.append( strValue );
|
|
}
|
|
mValuesModel->setStringList( strValues );
|
|
mFieldValues[fieldName] = strValues;
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::registerItem( const QString &group,
|
|
const QString &label,
|
|
const QString &expressionText,
|
|
const QString &helpText,
|
|
QgsExpressionItem::ItemType type, bool highlightedItem, int sortOrder )
|
|
{
|
|
QgsExpressionItem *item = new QgsExpressionItem( label, expressionText, helpText, type );
|
|
item->setData( label, Qt::UserRole );
|
|
item->setData( sortOrder, QgsExpressionItem::CUSTOM_SORT_ROLE );
|
|
|
|
// 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 ), QLatin1String( "" ), QgsExpressionItem::Header );
|
|
newgroupNode->setData( group, Qt::UserRole );
|
|
//Recent group should always be last group
|
|
newgroupNode->setData( group.startsWith( QLatin1String( "Recent (" ) ) ? 2 : 1, QgsExpressionItem::CUSTOM_SORT_ROLE );
|
|
newgroupNode->appendRow( item );
|
|
newgroupNode->setBackground( QBrush( QColor( 238, 238, 238 ) ) );
|
|
mModel->appendRow( newgroupNode );
|
|
mExpressionGroups.insert( group, newgroupNode );
|
|
}
|
|
|
|
if ( highlightedItem )
|
|
{
|
|
//insert a copy as a top level item
|
|
QgsExpressionItem *topLevelItem = new QgsExpressionItem( label, expressionText, helpText, type );
|
|
topLevelItem->setData( label, Qt::UserRole );
|
|
item->setData( 0, QgsExpressionItem::CUSTOM_SORT_ROLE );
|
|
QFont font = topLevelItem->font();
|
|
font.setBold( true );
|
|
topLevelItem->setFont( font );
|
|
mModel->appendRow( topLevelItem );
|
|
}
|
|
|
|
}
|
|
|
|
bool QgsExpressionBuilderWidget::isExpressionValid()
|
|
{
|
|
return mExpressionValid;
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::saveToRecent( const QString &collection )
|
|
{
|
|
QgsSettings settings;
|
|
QString location = QStringLiteral( "/expressions/recent/%1" ).arg( collection );
|
|
QStringList expressions = settings.value( location ).toStringList();
|
|
expressions.removeAll( this->expressionText() );
|
|
|
|
expressions.prepend( this->expressionText() );
|
|
|
|
while ( expressions.count() > 20 )
|
|
{
|
|
expressions.pop_back();
|
|
}
|
|
|
|
settings.setValue( location, expressions );
|
|
this->loadRecent( collection );
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::loadRecent( const QString &collection )
|
|
{
|
|
mRecentKey = collection;
|
|
QString name = tr( "Recent (%1)" ).arg( collection );
|
|
if ( mExpressionGroups.contains( name ) )
|
|
{
|
|
QgsExpressionItem *node = mExpressionGroups.value( name );
|
|
node->removeRows( 0, node->rowCount() );
|
|
}
|
|
|
|
QgsSettings settings;
|
|
QString location = QStringLiteral( "/expressions/recent/%1" ).arg( collection );
|
|
QStringList expressions = settings.value( location ).toStringList();
|
|
int i = 0;
|
|
Q_FOREACH ( const QString &expression, expressions )
|
|
{
|
|
this->registerItem( name, expression, expression, expression, QgsExpressionItem::ExpressionNode, false, i );
|
|
i++;
|
|
}
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::loadLayers()
|
|
{
|
|
if ( !mProject )
|
|
return;
|
|
|
|
QMap<QString, QgsMapLayer *> layers = mProject->mapLayers();
|
|
QMap<QString, QgsMapLayer *>::const_iterator layerIt = layers.constBegin();
|
|
for ( ; layerIt != layers.constEnd(); ++layerIt )
|
|
{
|
|
registerItemForAllGroups( QStringList() << tr( "Map Layers" ), layerIt.value()->name(), QStringLiteral( "'%1'" ).arg( layerIt.key() ), formatLayerHelp( layerIt.value() ) );
|
|
}
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::loadRelations()
|
|
{
|
|
if ( !mProject )
|
|
return;
|
|
|
|
QMap<QString, QgsRelation> relations = mProject->relationManager()->relations();
|
|
QMap<QString, QgsRelation>::const_iterator relIt = relations.constBegin();
|
|
for ( ; relIt != relations.constEnd(); ++relIt )
|
|
{
|
|
registerItemForAllGroups( QStringList() << tr( "Relations" ), relIt->name(), QStringLiteral( "'%1'" ).arg( relIt->id() ), formatRelationHelp( relIt.value() ) );
|
|
}
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::updateFunctionTree()
|
|
{
|
|
mModel->clear();
|
|
mExpressionGroups.clear();
|
|
// TODO Can we move this stuff to QgsExpression, like the functions?
|
|
registerItem( QStringLiteral( "Operators" ), QStringLiteral( "+" ), QStringLiteral( " + " ) );
|
|
registerItem( QStringLiteral( "Operators" ), QStringLiteral( "-" ), QStringLiteral( " - " ) );
|
|
registerItem( QStringLiteral( "Operators" ), QStringLiteral( "*" ), QStringLiteral( " * " ) );
|
|
registerItem( QStringLiteral( "Operators" ), QStringLiteral( "/" ), QStringLiteral( " / " ) );
|
|
registerItem( QStringLiteral( "Operators" ), QStringLiteral( "%" ), QStringLiteral( " % " ) );
|
|
registerItem( QStringLiteral( "Operators" ), QStringLiteral( "^" ), QStringLiteral( " ^ " ) );
|
|
registerItem( QStringLiteral( "Operators" ), QStringLiteral( "=" ), QStringLiteral( " = " ) );
|
|
registerItem( QStringLiteral( "Operators" ), QStringLiteral( "~" ), QStringLiteral( " ~ " ) );
|
|
registerItem( QStringLiteral( "Operators" ), QStringLiteral( ">" ), QStringLiteral( " > " ) );
|
|
registerItem( QStringLiteral( "Operators" ), QStringLiteral( "<" ), QStringLiteral( " < " ) );
|
|
registerItem( QStringLiteral( "Operators" ), QStringLiteral( "<>" ), QStringLiteral( " <> " ) );
|
|
registerItem( QStringLiteral( "Operators" ), QStringLiteral( "<=" ), QStringLiteral( " <= " ) );
|
|
registerItem( QStringLiteral( "Operators" ), QStringLiteral( ">=" ), QStringLiteral( " >= " ) );
|
|
registerItem( QStringLiteral( "Operators" ), QStringLiteral( "||" ), QStringLiteral( " || " ) );
|
|
registerItem( QStringLiteral( "Operators" ), QStringLiteral( "IN" ), QStringLiteral( " IN " ) );
|
|
registerItem( QStringLiteral( "Operators" ), QStringLiteral( "LIKE" ), QStringLiteral( " LIKE " ) );
|
|
registerItem( QStringLiteral( "Operators" ), QStringLiteral( "ILIKE" ), QStringLiteral( " ILIKE " ) );
|
|
registerItem( QStringLiteral( "Operators" ), QStringLiteral( "IS" ), QStringLiteral( " IS " ) );
|
|
registerItem( QStringLiteral( "Operators" ), QStringLiteral( "OR" ), QStringLiteral( " OR " ) );
|
|
registerItem( QStringLiteral( "Operators" ), QStringLiteral( "AND" ), QStringLiteral( " AND " ) );
|
|
registerItem( QStringLiteral( "Operators" ), QStringLiteral( "NOT" ), QStringLiteral( " NOT " ) );
|
|
|
|
QString casestring = QStringLiteral( "CASE WHEN condition THEN result END" );
|
|
registerItem( QStringLiteral( "Conditionals" ), QStringLiteral( "CASE" ), casestring );
|
|
|
|
registerItem( QStringLiteral( "Fields and Values" ), QStringLiteral( "NULL" ), QStringLiteral( "NULL" ) );
|
|
|
|
// Load the functions from the QgsExpression class
|
|
int count = QgsExpression::functionCount();
|
|
for ( int i = 0; i < count; i++ )
|
|
{
|
|
QgsExpressionFunction *func = QgsExpression::Functions()[i];
|
|
QString name = func->name();
|
|
if ( name.startsWith( '_' ) ) // do not display private functions
|
|
continue;
|
|
if ( func->isDeprecated() ) // don't show deprecated functions
|
|
continue;
|
|
if ( func->isContextual() )
|
|
{
|
|
//don't show contextual functions by default - it's up the the QgsExpressionContext
|
|
//object to provide them if supported
|
|
continue;
|
|
}
|
|
if ( func->params() != 0 )
|
|
name += '(';
|
|
else if ( !name.startsWith( '$' ) )
|
|
name += QLatin1String( "()" );
|
|
registerItemForAllGroups( func->groups(), func->name(), ' ' + name + ' ', func->helpText() );
|
|
}
|
|
|
|
// load relation names
|
|
loadRelations();
|
|
|
|
// load layer IDs
|
|
loadLayers();
|
|
|
|
loadExpressionContext();
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::setGeomCalculator( const QgsDistanceArea &da )
|
|
{
|
|
mDa = da;
|
|
}
|
|
|
|
QString QgsExpressionBuilderWidget::expressionText()
|
|
{
|
|
return txtExpressionString->text();
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::setExpressionText( const QString &expression )
|
|
{
|
|
txtExpressionString->setText( expression );
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::setExpressionContext( const QgsExpressionContext &context )
|
|
{
|
|
mExpressionContext = context;
|
|
updateFunctionTree();
|
|
loadFieldNames();
|
|
loadRecent( mRecentKey );
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::txtExpressionString_textChanged()
|
|
{
|
|
QString text = expressionText();
|
|
|
|
// 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->clear();
|
|
lblPreview->setStyleSheet( QLatin1String( "" ) );
|
|
txtExpressionString->setToolTip( QLatin1String( "" ) );
|
|
lblPreview->setToolTip( QLatin1String( "" ) );
|
|
emit expressionParsed( false );
|
|
setParserError( true );
|
|
setEvalError( true );
|
|
return;
|
|
}
|
|
|
|
QgsExpression exp( text );
|
|
|
|
if ( mLayer )
|
|
{
|
|
// Only set calculator if we have layer, else use default.
|
|
exp.setGeomCalculator( &mDa );
|
|
|
|
if ( !mExpressionContext.feature().isValid() )
|
|
{
|
|
// no feature passed yet, try to get from layer
|
|
QgsFeature f;
|
|
mLayer->getFeatures( QgsFeatureRequest().setLimit( 1 ) ).nextFeature( f );
|
|
mExpressionContext.setFeature( f );
|
|
}
|
|
}
|
|
|
|
QVariant value = exp.evaluate( &mExpressionContext );
|
|
if ( !exp.hasEvalError() )
|
|
{
|
|
lblPreview->setText( QgsExpression::formatPreviewString( value ) );
|
|
}
|
|
|
|
if ( exp.hasParserError() || exp.hasEvalError() )
|
|
{
|
|
QString tooltip = QStringLiteral( "<b>%1:</b><br>%2" ).arg( tr( "Parser Error" ), exp.parserErrorString() );
|
|
if ( exp.hasEvalError() )
|
|
tooltip += QStringLiteral( "<br><br><b>%1:</b><br>%2" ).arg( tr( "Eval Error" ), exp.evalErrorString() );
|
|
|
|
lblPreview->setText( tr( "Expression is invalid <a href=""more"">(more info)</a>" ) );
|
|
lblPreview->setStyleSheet( QStringLiteral( "color: rgba(255, 6, 10, 255);" ) );
|
|
txtExpressionString->setToolTip( tooltip );
|
|
lblPreview->setToolTip( tooltip );
|
|
emit expressionParsed( false );
|
|
setParserError( exp.hasParserError() );
|
|
setEvalError( exp.hasEvalError() );
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
lblPreview->setStyleSheet( QString() );
|
|
txtExpressionString->setToolTip( QString() );
|
|
lblPreview->setToolTip( QString() );
|
|
emit expressionParsed( true );
|
|
setParserError( false );
|
|
setEvalError( false );
|
|
}
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::loadExpressionContext()
|
|
{
|
|
QStringList variableNames = mExpressionContext.filteredVariableNames();
|
|
Q_FOREACH ( const QString &variable, variableNames )
|
|
{
|
|
registerItem( QStringLiteral( "Variables" ), variable, " @" + variable + ' ',
|
|
QgsExpression::formatVariableHelp( mExpressionContext.description( variable ), true, mExpressionContext.variable( variable ) ),
|
|
QgsExpressionItem::ExpressionNode,
|
|
mExpressionContext.isHighlightedVariable( variable ) );
|
|
}
|
|
|
|
// Load the functions from the expression context
|
|
QStringList contextFunctions = mExpressionContext.functionNames();
|
|
Q_FOREACH ( const QString &functionName, contextFunctions )
|
|
{
|
|
QgsExpressionFunction *func = mExpressionContext.function( functionName );
|
|
QString name = func->name();
|
|
if ( name.startsWith( '_' ) ) // do not display private functions
|
|
continue;
|
|
if ( func->params() != 0 )
|
|
name += '(';
|
|
registerItemForAllGroups( func->groups(), func->name(), ' ' + name + ' ', func->helpText() );
|
|
}
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::registerItemForAllGroups( const QStringList &groups, const QString &label, const QString &expressionText, const QString &helpText, QgsExpressionItem::ItemType type, bool highlightedItem, int sortOrder )
|
|
{
|
|
Q_FOREACH ( const QString &group, groups )
|
|
{
|
|
registerItem( group, label, expressionText, helpText, type, highlightedItem, sortOrder );
|
|
}
|
|
}
|
|
|
|
QString QgsExpressionBuilderWidget::formatRelationHelp( const QgsRelation &relation ) const
|
|
{
|
|
QString text = QStringLiteral( "<p>%1</p>" ).arg( tr( "Inserts the relation ID for the relation named '%1'." ).arg( relation.name() ) );
|
|
text.append( QStringLiteral( "<p>%1</p>" ).arg( tr( "Current value: '%1'" ).arg( relation.id() ) ) );
|
|
return text;
|
|
}
|
|
|
|
QString QgsExpressionBuilderWidget::formatLayerHelp( const QgsMapLayer *layer ) const
|
|
{
|
|
QString text = QStringLiteral( "<p>%1</p>" ).arg( tr( "Inserts the layer ID for the layer named '%1'." ).arg( layer->name() ) );
|
|
text.append( QStringLiteral( "<p>%1</p>" ).arg( tr( "Current value: '%1'" ).arg( layer->id() ) ) );
|
|
return text;
|
|
}
|
|
|
|
bool QgsExpressionBuilderWidget::parserError() const
|
|
{
|
|
return mParserError;
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::setParserError( bool parserError )
|
|
{
|
|
if ( parserError == mParserError )
|
|
return;
|
|
|
|
mParserError = parserError;
|
|
emit parserErrorChanged();
|
|
}
|
|
|
|
bool QgsExpressionBuilderWidget::evalError() const
|
|
{
|
|
return mEvalError;
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::setEvalError( bool evalError )
|
|
{
|
|
if ( evalError == mEvalError )
|
|
return;
|
|
|
|
mEvalError = evalError;
|
|
emit evalErrorChanged();
|
|
}
|
|
|
|
QStandardItemModel *QgsExpressionBuilderWidget::model()
|
|
{
|
|
return mModel;
|
|
}
|
|
|
|
QgsProject *QgsExpressionBuilderWidget::project()
|
|
{
|
|
return mProject;
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::setProject( QgsProject *project )
|
|
{
|
|
mProject = project;
|
|
updateFunctionTree();
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::showEvent( QShowEvent *e )
|
|
{
|
|
QWidget::showEvent( e );
|
|
txtExpressionString->setFocus();
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::txtSearchEdit_textChanged()
|
|
{
|
|
mProxyModel->setFilterWildcard( txtSearchEdit->text() );
|
|
if ( txtSearchEdit->text().isEmpty() )
|
|
{
|
|
expressionTree->collapseAll();
|
|
}
|
|
else
|
|
{
|
|
expressionTree->expandAll();
|
|
QModelIndex index = mProxyModel->index( 0, 0 );
|
|
if ( mProxyModel->hasChildren( index ) )
|
|
{
|
|
QModelIndex child = mProxyModel->index( 0, 0, index );
|
|
expressionTree->selectionModel()->setCurrentIndex( child, QItemSelectionModel::ClearAndSelect );
|
|
}
|
|
}
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::txtSearchEditValues_textChanged()
|
|
{
|
|
mProxyValues->setFilterCaseSensitivity( Qt::CaseInsensitive );
|
|
mProxyValues->setFilterWildcard( txtSearchEditValues->text() );
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::lblPreview_linkActivated( const 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::mValuesListView_doubleClicked( const QModelIndex &index )
|
|
{
|
|
// Insert the item text or replace selected text
|
|
txtExpressionString->insertText( ' ' + index.data( Qt::DisplayRole ).toString() + ' ' );
|
|
txtExpressionString->setFocus();
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::operatorButtonClicked()
|
|
{
|
|
QPushButton *button = dynamic_cast<QPushButton *>( sender() );
|
|
|
|
// Insert the button text or replace selected text
|
|
txtExpressionString->insertText( ' ' + button->text() + ' ' );
|
|
txtExpressionString->setFocus();
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::showContextMenu( 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 || !item )
|
|
return;
|
|
|
|
mValueGroupBox->show();
|
|
fillFieldValues( item->text(), 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 || !item )
|
|
return;
|
|
|
|
mValueGroupBox->show();
|
|
fillFieldValues( item->text(), -1 );
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::txtPython_textChanged()
|
|
{
|
|
lblAutoSave->setText( tr( "Saving…" ) );
|
|
if ( mAutoSave )
|
|
{
|
|
autosave();
|
|
}
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::autosave()
|
|
{
|
|
// Don't auto save if not on function editor that would be silly.
|
|
if ( tabWidget->currentIndex() != 1 )
|
|
return;
|
|
|
|
QListWidgetItem *item = cmbFileNames->currentItem();
|
|
if ( !item )
|
|
return;
|
|
|
|
QString file = item->text();
|
|
saveFunctionFile( file );
|
|
lblAutoSave->setText( QStringLiteral( "Saved" ) );
|
|
QGraphicsOpacityEffect *effect = new QGraphicsOpacityEffect();
|
|
lblAutoSave->setGraphicsEffect( effect );
|
|
QPropertyAnimation *anim = new QPropertyAnimation( effect, "opacity" );
|
|
anim->setDuration( 2000 );
|
|
anim->setStartValue( 1.0 );
|
|
anim->setEndValue( 0.0 );
|
|
anim->setEasingCurve( QEasingCurve::OutQuad );
|
|
anim->start( QAbstractAnimation::DeleteWhenStopped );
|
|
}
|
|
|
|
void QgsExpressionBuilderWidget::setExpressionState( bool state )
|
|
{
|
|
mExpressionValid = state;
|
|
}
|
|
|
|
QString QgsExpressionBuilderWidget::helpStylesheet() const
|
|
{
|
|
//start with default QGIS report style
|
|
QString style = QgsApplication::reportStyleSheet();
|
|
|
|
//add some tweaks
|
|
style += " .functionname {color: #0a6099; font-weight: bold;} "
|
|
" .argument {font-family: monospace; color: #bf0c0c; font-style: italic; } "
|
|
" td.argument { padding-right: 10px; }";
|
|
|
|
return style;
|
|
}
|
|
|
|
QString QgsExpressionBuilderWidget::loadFunctionHelp( QgsExpressionItem *expressionItem )
|
|
{
|
|
if ( !expressionItem )
|
|
return QLatin1String( "" );
|
|
|
|
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( QStringLiteral( "Field" ) );
|
|
else
|
|
helpContents = QgsExpression::helpText( name );
|
|
}
|
|
|
|
return "<head><style>" + helpStylesheet() + "</style></head><body>" + helpContents + "</body>";
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
QgsExpressionItemSearchProxy::QgsExpressionItemSearchProxy()
|
|
{
|
|
setFilterCaseSensitivity( Qt::CaseInsensitive );
|
|
}
|
|
|
|
bool QgsExpressionItemSearchProxy::filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const
|
|
{
|
|
QModelIndex index = sourceModel()->index( source_row, 0, source_parent );
|
|
QgsExpressionItem::ItemType itemType = QgsExpressionItem::ItemType( sourceModel()->data( index, QgsExpressionItem::ITEM_TYPE_ROLE ).toInt() );
|
|
|
|
int count = sourceModel()->rowCount( index );
|
|
bool matchchild = false;
|
|
for ( int i = 0; i < count; ++i )
|
|
{
|
|
if ( filterAcceptsRow( i, index ) )
|
|
{
|
|
matchchild = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( itemType == QgsExpressionItem::Header && matchchild )
|
|
return true;
|
|
|
|
if ( itemType == QgsExpressionItem::Header )
|
|
return false;
|
|
|
|
return QSortFilterProxyModel::filterAcceptsRow( source_row, source_parent );
|
|
}
|
|
|
|
bool QgsExpressionItemSearchProxy::lessThan( const QModelIndex &left, const QModelIndex &right ) const
|
|
{
|
|
int leftSort = sourceModel()->data( left, QgsExpressionItem::CUSTOM_SORT_ROLE ).toInt();
|
|
int rightSort = sourceModel()->data( right, QgsExpressionItem::CUSTOM_SORT_ROLE ).toInt();
|
|
if ( leftSort != rightSort )
|
|
return leftSort < rightSort;
|
|
|
|
QString leftString = sourceModel()->data( left, Qt::DisplayRole ).toString();
|
|
QString rightString = sourceModel()->data( right, Qt::DisplayRole ).toString();
|
|
|
|
//ignore $ prefixes when sorting
|
|
if ( leftString.startsWith( '$' ) )
|
|
leftString = leftString.mid( 1 );
|
|
if ( rightString.startsWith( '$' ) )
|
|
rightString = rightString.mid( 1 );
|
|
|
|
return QString::localeAwareCompare( leftString, rightString ) < 0;
|
|
}
|