mirror of
https://github.com/qgis/QGIS.git
synced 2025-04-14 00:07:35 -04:00
[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:
parent
49cf93dafb
commit
59162bc178
@ -257,6 +257,7 @@ ENDIF(WITH_CUSTOM_WIDGETS)
|
||||
SET(PY_FILES
|
||||
__init__.py
|
||||
utils.py
|
||||
user.py
|
||||
)
|
||||
|
||||
ADD_CUSTOM_TARGET(pyutils ALL)
|
||||
|
@ -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
41
python/user.py
Normal 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)
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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 )
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user