Skip to content

Commit

Permalink
[FEATURE] Function editor for expression widget.
Browse files Browse the repository at this point in the history
Allows for adding on the fly functions to the expression engine.
Functions are saved in qgis2\python\expressions.

New qgis.user module in Python.

The qgis.user.expressions package points to the qgis2\python\expressions
package in the users home
  • Loading branch information
NathanW2 committed Jan 16, 2015
1 parent 49cf93d commit 59162bc
Show file tree
Hide file tree
Showing 8 changed files with 866 additions and 534 deletions.
1 change: 1 addition & 0 deletions python/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ ENDIF(WITH_CUSTOM_WIDGETS)
SET(PY_FILES
__init__.py
utils.py
user.py
)

ADD_CUSTOM_TARGET(pyutils ALL)
Expand Down
20 changes: 20 additions & 0 deletions python/gui/qgsexpressionbuilderwidget.sip
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,26 @@ class QgsExpressionBuilderWidget : QWidget

void loadRecent( QString key );

/** Create a new file in the function editor
*/
void newFunctionFile( QString fileName = "scratch");

/** Save the current function editor text to the given file.
*/
void saveFunctionFile( QString fileName );

/** Load code from the given file into the function editor
*/
void loadCodeFromFile( QString path );

/** Load code into the function editor
*/
void loadFunctionCode( QString code );

/** Update the list of function files found at the given path
*/
void updateFunctionFileList( QString path );

public slots:
void currentChanged( const QModelIndex &index, const QModelIndex & );
void on_expressionTree_doubleClicked( const QModelIndex &index );
Expand Down
41 changes: 41 additions & 0 deletions python/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import os
import sys
import glob

from qgis.core import QgsApplication

def load_user_expressions(path):
"""
Load all user expressions from the given paths
"""
#Loop all py files and import them
modules = glob.glob(path + "/*.py")
names = [os.path.basename(f)[:-3] for f in modules]
for name in names:
if name == "__init__":
continue
# As user expression functions should be registed with qgsfunction
# just importing the file is enough to get it to load the functions into QGIS
__import__("expressions.{0}".format(name), locals(), globals())


userpythonhome = os.path.join(QgsApplication.qgisSettingsDirPath(), "python")
expressionspath = os.path.join(userpythonhome, "expressions")
startuppy = os.path.join(userpythonhome, "startup.py")

# exec startup script
if os.path.exists(startuppy):
execfile(startuppy, locals(), globals())

if not os.path.exists(expressionspath):
os.makedirs(expressionspath)

initfile = os.path.join(expressionspath, "__init__.py")
if not os.path.exists(initfile):
open(initfile, "w").close()

import expressions

expressions.load = load_user_expressions
expressions.load(expressionspath)

245 changes: 181 additions & 64 deletions src/gui/qgsexpressionbuilderwidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
#include <QFile>
#include <QTextStream>
#include <QSettings>
#include <QDir>
#include <QComboBox>

QgsExpressionBuilderWidget::QgsExpressionBuilderWidget( QWidget *parent )
: QWidget( parent )
Expand Down Expand Up @@ -54,71 +56,27 @@ QgsExpressionBuilderWidget::QgsExpressionBuilderWidget( QWidget *parent )
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", "IN", " IN " );
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() != 0 )
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" ) );

QSettings settings;
splitter->restoreState( settings.value( "/windows/QgsExpressionBuilderWidget/splitter" ).toByteArray() );
// splitter_2->restoreState( settings.value( "/windows/QgsExpressionBuilderWidget/splitter2" ).toByteArray() );

txtExpressionString->setFoldingVisible( false );
// customFunctionBotton->setVisible( QgsPythonRunner::isValid() );
txtPython->setVisible( false );
cgbCustomFunction->setCollapsed( true );
txtPython->setText( "@qgsfunction(args=-1, group='Custom')\n"
"def func(values, feature, parent):\n"
" return str(values)" );

updateFunctionTree();

if ( QgsPythonRunner::isValid() )
{
QgsPythonRunner::eval( "qgis.user.expressionspath", mFunctionsPath );
newFunctionFile();
// The scratch file gets written each time the widget opens.
saveFunctionFile("scratch");
updateFunctionFileList( mFunctionsPath );
}
else
{
tab_2->setEnabled( false );
}
}


Expand Down Expand Up @@ -156,6 +114,113 @@ void QgsExpressionBuilderWidget::currentChanged( const QModelIndex &index, const
txtHelpText->setToolTip( txtHelpText->toPlainText() );
}

void QgsExpressionBuilderWidget::on_btnRun_pressed()
{
saveFunctionFile( cmbFileNames->currentText() );
runPythonCode( txtPython->text() );
}

void QgsExpressionBuilderWidget::runPythonCode( QString code )
{
if ( QgsPythonRunner::isValid() )
{
QString pythontext = code;
QgsPythonRunner::run( pythontext );
}
updateFunctionTree();
}

void QgsExpressionBuilderWidget::saveFunctionFile( QString fileName )
{
QDir myDir( mFunctionsPath );
if ( !myDir.exists() )
{
myDir.mkpath( mFunctionsPath );
}

if ( !fileName.endsWith( ".py" ) )
{
fileName.append( ".py" );
}

fileName = mFunctionsPath + QDir::separator() + fileName;
QFile myFile( fileName );
if ( myFile.open( QIODevice::WriteOnly | QIODevice::Text ) )
{
QTextStream myFileStream( &myFile );
myFileStream << txtPython->text() << endl;
myFile.close();
}
}

void QgsExpressionBuilderWidget::updateFunctionFileList( QString path )
{
mFunctionsPath = path;
QDir dir( path );
dir.setNameFilters( QStringList() << "*.py" );
QStringList files = dir.entryList( QDir::Files );
cmbFileNames->clear();
foreach ( QString name, files )
{
QFileInfo info( mFunctionsPath + QDir::separator() + name );
if ( info.baseName() == "__init__" ) continue;
cmbFileNames->addItem( info.baseName() );
}
}

void QgsExpressionBuilderWidget::newFunctionFile( QString fileName )
{
txtPython->setText( "from qgis.core import *\n"
"from qgis.gui import *\n\n"
"@qgsfunction(args=-1, group='Custom')\n"
"def func(values, feature, parent):\n"
" return str(values)" );
int index = cmbFileNames->findText( fileName );
if ( index == -1 )
cmbFileNames->setEditText( fileName );
else
cmbFileNames->setCurrentIndex( index );
}

void QgsExpressionBuilderWidget::on_btnNewFile_pressed()
{
newFunctionFile();
}

void QgsExpressionBuilderWidget::on_cmbFileNames_currentIndexChanged( int index )
{
if ( index == -1 )
return;

QString path = mFunctionsPath + QDir::separator() + cmbFileNames->currentText();
loadCodeFromFile( path );
}

void QgsExpressionBuilderWidget::loadCodeFromFile( QString path )
{
if ( !path.endsWith( ".py" ) )
path.append( ".py" );

txtPython->loadScript( path );
}

void QgsExpressionBuilderWidget::loadFunctionCode( QString code )
{
txtPython->setText( code );
}

void QgsExpressionBuilderWidget::on_btnSaveFile_pressed()
{
QString name = cmbFileNames->currentText();
saveFunctionFile( name );
int index = cmbFileNames->findText( name );
if ( index == -1 )
{
cmbFileNames->addItem( name );
cmbFileNames->setCurrentIndex( cmbFileNames->count() - 1 );
}
}

void QgsExpressionBuilderWidget::on_expressionTree_doubleClicked( const QModelIndex &index )
{
QModelIndex idx = mProxyModel->mapToSource( index );
Expand Down Expand Up @@ -292,18 +357,70 @@ void QgsExpressionBuilderWidget::loadRecent( QString key )
}
}

void QgsExpressionBuilderWidget::updateFunctionTree()
{
mModel->clear();
mExpressionGroups.clear();
// 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", "IN", " IN " );
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() != 0 )
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 + " " );
}
}

void QgsExpressionBuilderWidget::setGeomCalculator( const QgsDistanceArea & da )
{
mDa = da;
}

QString QgsExpressionBuilderWidget::expressionText()
{
if ( QgsPythonRunner::isValid() )
{
QString pythontext = txtPython->text();
QgsPythonRunner::run( pythontext );
}
return txtExpressionString->text();
}

Expand Down
Loading

0 comments on commit 59162bc

Please sign in to comment.