[FEATURE] Function editor for expression widget.

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
This commit is contained in:
Nathan Woodrow 2015-01-12 18:14:30 +10:00
parent 49cf93dafb
commit 59162bc178
8 changed files with 899 additions and 567 deletions

View File

@ -257,6 +257,7 @@ ENDIF(WITH_CUSTOM_WIDGETS)
SET(PY_FILES
__init__.py
utils.py
user.py
)
ADD_CUSTOM_TARGET(pyutils ALL)

View File

@ -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 );

41
python/user.py Normal file
View File

@ -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)

View File

@ -25,6 +25,8 @@
#include <QFile>
#include <QTextStream>
#include <QSettings>
#include <QDir>
#include <QComboBox>
QgsExpressionBuilderWidget::QgsExpressionBuilderWidget( QWidget *parent )
: QWidget( parent )
@ -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 );
}
}
@ -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 );
@ -292,6 +357,63 @@ 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;
@ -299,11 +421,6 @@ void QgsExpressionBuilderWidget::setGeomCalculator( const QgsDistanceArea & da )
QString QgsExpressionBuilderWidget::expressionText()
{
if ( QgsPythonRunner::isValid() )
{
QString pythontext = txtPython->text();
QgsPythonRunner::run( pythontext );
}
return txtExpressionString->text();
}

View File

@ -150,8 +150,32 @@ class GUI_EXPORT QgsExpressionBuilderWidget : public QWidget, private Ui::QgsExp
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_btnRun_pressed();
void on_btnNewFile_pressed();
void on_cmbFileNames_currentIndexChanged( int index );
void on_btnSaveFile_pressed();
void on_expressionTree_doubleClicked( const QModelIndex &index );
void on_txtExpressionString_textChanged();
void on_txtSearchEdit_textChanged();
@ -174,9 +198,12 @@ class GUI_EXPORT QgsExpressionBuilderWidget : public QWidget, private Ui::QgsExp
void expressionParsed( bool isValid );
private:
void runPythonCode( QString code );
void updateFunctionTree();
void fillFieldValues( int fieldIndex, int countLimit );
QString loadFunctionHelp( QgsExpressionItem* functionName );
QString mFunctionsPath;
QgsVectorLayer *mLayer;
QStandardItemModel *mModel;
QgsExpressionItemSearchProxy *mProxyModel;

View File

@ -5,6 +5,7 @@
#include "qgslogger.h"
#include <QCheckBox>
#include <QSettings>
QgsDataDefinedSymbolDialog::QgsDataDefinedSymbolDialog( const QList< DataDefinedSymbolEntry >& entries, const QgsVectorLayer* vl, QWidget * parent, Qt::WindowFlags f )

View File

@ -157,6 +157,7 @@ void QgsPythonUtilsImpl::initPython( QgisInterface* interface )
return;
}
// tell the utils script where to look for the plugins
runString( "qgis.utils.plugin_paths = [" + pluginpaths.join( "," ) + "]" );
runString( "qgis.utils.sys_plugin_path = \"" + pluginsPath() + "\"" );
@ -169,8 +170,14 @@ void QgsPythonUtilsImpl::initPython( QgisInterface* interface )
// initialize 'iface' object
runString( "qgis.utils.initInterface(" + QString::number(( unsigned long ) interface ) + ")" );
QString startuppath = homePythonPath() + " + \"/startup.py\"";
runString( "if os.path.exists(" + startuppath + "): from startup import *\n" );
// import QGIS user
error_msg = QObject::tr( "Couldn't load QGIS user." ) + "\n" + QObject::tr( "Python support will be disabled." );
if ( !runString( "import qgis.user", error_msg ) )
{
// Should we really bail because of this?!
exitPython();
return;
}
// release GIL!
// Later on, we acquire GIL just before doing some Python calls and

File diff suppressed because it is too large Load Diff