Skip to content

Commit 59162bc

Browse files
committedJan 16, 2015
[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
1 parent 49cf93d commit 59162bc

File tree

8 files changed

+866
-534
lines changed

8 files changed

+866
-534
lines changed
 

‎python/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ ENDIF(WITH_CUSTOM_WIDGETS)
257257
SET(PY_FILES
258258
__init__.py
259259
utils.py
260+
user.py
260261
)
261262

262263
ADD_CUSTOM_TARGET(pyutils ALL)

‎python/gui/qgsexpressionbuilderwidget.sip

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,26 @@ class QgsExpressionBuilderWidget : QWidget
111111

112112
void loadRecent( QString key );
113113

114+
/** Create a new file in the function editor
115+
*/
116+
void newFunctionFile( QString fileName = "scratch");
117+
118+
/** Save the current function editor text to the given file.
119+
*/
120+
void saveFunctionFile( QString fileName );
121+
122+
/** Load code from the given file into the function editor
123+
*/
124+
void loadCodeFromFile( QString path );
125+
126+
/** Load code into the function editor
127+
*/
128+
void loadFunctionCode( QString code );
129+
130+
/** Update the list of function files found at the given path
131+
*/
132+
void updateFunctionFileList( QString path );
133+
114134
public slots:
115135
void currentChanged( const QModelIndex &index, const QModelIndex & );
116136
void on_expressionTree_doubleClicked( const QModelIndex &index );

‎python/user.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import os
2+
import sys
3+
import glob
4+
5+
from qgis.core import QgsApplication
6+
7+
def load_user_expressions(path):
8+
"""
9+
Load all user expressions from the given paths
10+
"""
11+
#Loop all py files and import them
12+
modules = glob.glob(path + "/*.py")
13+
names = [os.path.basename(f)[:-3] for f in modules]
14+
for name in names:
15+
if name == "__init__":
16+
continue
17+
# As user expression functions should be registed with qgsfunction
18+
# just importing the file is enough to get it to load the functions into QGIS
19+
__import__("expressions.{0}".format(name), locals(), globals())
20+
21+
22+
userpythonhome = os.path.join(QgsApplication.qgisSettingsDirPath(), "python")
23+
expressionspath = os.path.join(userpythonhome, "expressions")
24+
startuppy = os.path.join(userpythonhome, "startup.py")
25+
26+
# exec startup script
27+
if os.path.exists(startuppy):
28+
execfile(startuppy, locals(), globals())
29+
30+
if not os.path.exists(expressionspath):
31+
os.makedirs(expressionspath)
32+
33+
initfile = os.path.join(expressionspath, "__init__.py")
34+
if not os.path.exists(initfile):
35+
open(initfile, "w").close()
36+
37+
import expressions
38+
39+
expressions.load = load_user_expressions
40+
expressions.load(expressionspath)
41+

‎src/gui/qgsexpressionbuilderwidget.cpp

Lines changed: 181 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
#include <QFile>
2626
#include <QTextStream>
2727
#include <QSettings>
28+
#include <QDir>
29+
#include <QComboBox>
2830

2931
QgsExpressionBuilderWidget::QgsExpressionBuilderWidget( QWidget *parent )
3032
: QWidget( parent )
@@ -54,71 +56,27 @@ QgsExpressionBuilderWidget::QgsExpressionBuilderWidget( QWidget *parent )
5456
connect( button, SIGNAL( pressed() ), this, SLOT( operatorButtonClicked() ) );
5557
}
5658

57-
// TODO Can we move this stuff to QgsExpression, like the functions?
58-
registerItem( "Operators", "+", " + ", tr( "Addition operator" ) );
59-
registerItem( "Operators", "-", " - ", tr( "Subtraction operator" ) );
60-
registerItem( "Operators", "*", " * ", tr( "Multiplication operator" ) );
61-
registerItem( "Operators", "/", " / ", tr( "Division operator" ) );
62-
registerItem( "Operators", "%", " % ", tr( "Modulo operator" ) );
63-
registerItem( "Operators", "^", " ^ ", tr( "Power operator" ) );
64-
registerItem( "Operators", "=", " = ", tr( "Equal operator" ) );
65-
registerItem( "Operators", ">", " > ", tr( "Greater as operator" ) );
66-
registerItem( "Operators", "<", " < ", tr( "Less than operator" ) );
67-
registerItem( "Operators", "<>", " <> ", tr( "Unequal operator" ) );
68-
registerItem( "Operators", "<=", " <= ", tr( "Less or equal operator" ) );
69-
registerItem( "Operators", ">=", " >= ", tr( "Greater or equal operator" ) );
70-
registerItem( "Operators", "||", " || ",
71-
QString( "<b>|| %1</b><br><i>%2</i><br><i>%3:</i>%4" )
72-
.arg( tr( "(String Concatenation)" ) )
73-
.arg( tr( "Joins two values together into a string" ) )
74-
.arg( tr( "Usage" ) )
75-
.arg( tr( "'Dia' || Diameter" ) ) );
76-
registerItem( "Operators", "IN", " IN " );
77-
registerItem( "Operators", "LIKE", " LIKE " );
78-
registerItem( "Operators", "ILIKE", " ILIKE " );
79-
registerItem( "Operators", "IS", " IS " );
80-
registerItem( "Operators", "OR", " OR " );
81-
registerItem( "Operators", "AND", " AND " );
82-
registerItem( "Operators", "NOT", " NOT " );
83-
84-
QString casestring = "CASE WHEN condition THEN result END";
85-
QString caseelsestring = "CASE WHEN condition THEN result ELSE result END";
86-
registerItem( "Conditionals", "CASE", casestring );
87-
registerItem( "Conditionals", "CASE ELSE", caseelsestring );
88-
89-
// Load the functions from the QgsExpression class
90-
int count = QgsExpression::functionCount();
91-
for ( int i = 0; i < count; i++ )
92-
{
93-
QgsExpression::Function* func = QgsExpression::Functions()[i];
94-
QString name = func->name();
95-
if ( name.startsWith( "_" ) ) // do not display private functions
96-
continue;
97-
if ( func->params() != 0 )
98-
name += "(";
99-
registerItem( func->group(), func->name(), " " + name + " ", func->helptext() );
100-
}
101-
102-
QList<QgsExpression::Function*> specials = QgsExpression::specialColumns();
103-
for ( int i = 0; i < specials.size(); ++i )
104-
{
105-
QString name = specials[i]->name();
106-
registerItem( specials[i]->group(), name, " " + name + " " );
107-
}
108-
10959
txtSearchEdit->setPlaceholderText( tr( "Search" ) );
11060

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

11564
txtExpressionString->setFoldingVisible( false );
116-
// customFunctionBotton->setVisible( QgsPythonRunner::isValid() );
117-
txtPython->setVisible( false );
118-
cgbCustomFunction->setCollapsed( true );
119-
txtPython->setText( "@qgsfunction(args=-1, group='Custom')\n"
120-
"def func(values, feature, parent):\n"
121-
" return str(values)" );
65+
66+
updateFunctionTree();
67+
68+
if ( QgsPythonRunner::isValid() )
69+
{
70+
QgsPythonRunner::eval( "qgis.user.expressionspath", mFunctionsPath );
71+
newFunctionFile();
72+
// The scratch file gets written each time the widget opens.
73+
saveFunctionFile("scratch");
74+
updateFunctionFileList( mFunctionsPath );
75+
}
76+
else
77+
{
78+
tab_2->setEnabled( false );
79+
}
12280
}
12381

12482

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

117+
void QgsExpressionBuilderWidget::on_btnRun_pressed()
118+
{
119+
saveFunctionFile( cmbFileNames->currentText() );
120+
runPythonCode( txtPython->text() );
121+
}
122+
123+
void QgsExpressionBuilderWidget::runPythonCode( QString code )
124+
{
125+
if ( QgsPythonRunner::isValid() )
126+
{
127+
QString pythontext = code;
128+
QgsPythonRunner::run( pythontext );
129+
}
130+
updateFunctionTree();
131+
}
132+
133+
void QgsExpressionBuilderWidget::saveFunctionFile( QString fileName )
134+
{
135+
QDir myDir( mFunctionsPath );
136+
if ( !myDir.exists() )
137+
{
138+
myDir.mkpath( mFunctionsPath );
139+
}
140+
141+
if ( !fileName.endsWith( ".py" ) )
142+
{
143+
fileName.append( ".py" );
144+
}
145+
146+
fileName = mFunctionsPath + QDir::separator() + fileName;
147+
QFile myFile( fileName );
148+
if ( myFile.open( QIODevice::WriteOnly | QIODevice::Text ) )
149+
{
150+
QTextStream myFileStream( &myFile );
151+
myFileStream << txtPython->text() << endl;
152+
myFile.close();
153+
}
154+
}
155+
156+
void QgsExpressionBuilderWidget::updateFunctionFileList( QString path )
157+
{
158+
mFunctionsPath = path;
159+
QDir dir( path );
160+
dir.setNameFilters( QStringList() << "*.py" );
161+
QStringList files = dir.entryList( QDir::Files );
162+
cmbFileNames->clear();
163+
foreach ( QString name, files )
164+
{
165+
QFileInfo info( mFunctionsPath + QDir::separator() + name );
166+
if ( info.baseName() == "__init__" ) continue;
167+
cmbFileNames->addItem( info.baseName() );
168+
}
169+
}
170+
171+
void QgsExpressionBuilderWidget::newFunctionFile( QString fileName )
172+
{
173+
txtPython->setText( "from qgis.core import *\n"
174+
"from qgis.gui import *\n\n"
175+
"@qgsfunction(args=-1, group='Custom')\n"
176+
"def func(values, feature, parent):\n"
177+
" return str(values)" );
178+
int index = cmbFileNames->findText( fileName );
179+
if ( index == -1 )
180+
cmbFileNames->setEditText( fileName );
181+
else
182+
cmbFileNames->setCurrentIndex( index );
183+
}
184+
185+
void QgsExpressionBuilderWidget::on_btnNewFile_pressed()
186+
{
187+
newFunctionFile();
188+
}
189+
190+
void QgsExpressionBuilderWidget::on_cmbFileNames_currentIndexChanged( int index )
191+
{
192+
if ( index == -1 )
193+
return;
194+
195+
QString path = mFunctionsPath + QDir::separator() + cmbFileNames->currentText();
196+
loadCodeFromFile( path );
197+
}
198+
199+
void QgsExpressionBuilderWidget::loadCodeFromFile( QString path )
200+
{
201+
if ( !path.endsWith( ".py" ) )
202+
path.append( ".py" );
203+
204+
txtPython->loadScript( path );
205+
}
206+
207+
void QgsExpressionBuilderWidget::loadFunctionCode( QString code )
208+
{
209+
txtPython->setText( code );
210+
}
211+
212+
void QgsExpressionBuilderWidget::on_btnSaveFile_pressed()
213+
{
214+
QString name = cmbFileNames->currentText();
215+
saveFunctionFile( name );
216+
int index = cmbFileNames->findText( name );
217+
if ( index == -1 )
218+
{
219+
cmbFileNames->addItem( name );
220+
cmbFileNames->setCurrentIndex( cmbFileNames->count() - 1 );
221+
}
222+
}
223+
159224
void QgsExpressionBuilderWidget::on_expressionTree_doubleClicked( const QModelIndex &index )
160225
{
161226
QModelIndex idx = mProxyModel->mapToSource( index );
@@ -292,18 +357,70 @@ void QgsExpressionBuilderWidget::loadRecent( QString key )
292357
}
293358
}
294359

360+
void QgsExpressionBuilderWidget::updateFunctionTree()
361+
{
362+
mModel->clear();
363+
mExpressionGroups.clear();
364+
// TODO Can we move this stuff to QgsExpression, like the functions?
365+
registerItem( "Operators", "+", " + ", tr( "Addition operator" ) );
366+
registerItem( "Operators", "-", " - ", tr( "Subtraction operator" ) );
367+
registerItem( "Operators", "*", " * ", tr( "Multiplication operator" ) );
368+
registerItem( "Operators", "/", " / ", tr( "Division operator" ) );
369+
registerItem( "Operators", "%", " % ", tr( "Modulo operator" ) );
370+
registerItem( "Operators", "^", " ^ ", tr( "Power operator" ) );
371+
registerItem( "Operators", "=", " = ", tr( "Equal operator" ) );
372+
registerItem( "Operators", ">", " > ", tr( "Greater as operator" ) );
373+
registerItem( "Operators", "<", " < ", tr( "Less than operator" ) );
374+
registerItem( "Operators", "<>", " <> ", tr( "Unequal operator" ) );
375+
registerItem( "Operators", "<=", " <= ", tr( "Less or equal operator" ) );
376+
registerItem( "Operators", ">=", " >= ", tr( "Greater or equal operator" ) );
377+
registerItem( "Operators", "||", " || ",
378+
QString( "<b>|| %1</b><br><i>%2</i><br><i>%3:</i>%4" )
379+
.arg( tr( "(String Concatenation)" ) )
380+
.arg( tr( "Joins two values together into a string" ) )
381+
.arg( tr( "Usage" ) )
382+
.arg( tr( "'Dia' || Diameter" ) ) );
383+
registerItem( "Operators", "IN", " IN " );
384+
registerItem( "Operators", "LIKE", " LIKE " );
385+
registerItem( "Operators", "ILIKE", " ILIKE " );
386+
registerItem( "Operators", "IS", " IS " );
387+
registerItem( "Operators", "OR", " OR " );
388+
registerItem( "Operators", "AND", " AND " );
389+
registerItem( "Operators", "NOT", " NOT " );
390+
391+
QString casestring = "CASE WHEN condition THEN result END";
392+
QString caseelsestring = "CASE WHEN condition THEN result ELSE result END";
393+
registerItem( "Conditionals", "CASE", casestring );
394+
registerItem( "Conditionals", "CASE ELSE", caseelsestring );
395+
396+
// Load the functions from the QgsExpression class
397+
int count = QgsExpression::functionCount();
398+
for ( int i = 0; i < count; i++ )
399+
{
400+
QgsExpression::Function* func = QgsExpression::Functions()[i];
401+
QString name = func->name();
402+
if ( name.startsWith( "_" ) ) // do not display private functions
403+
continue;
404+
if ( func->params() != 0 )
405+
name += "(";
406+
registerItem( func->group(), func->name(), " " + name + " ", func->helptext() );
407+
}
408+
409+
QList<QgsExpression::Function*> specials = QgsExpression::specialColumns();
410+
for ( int i = 0; i < specials.size(); ++i )
411+
{
412+
QString name = specials[i]->name();
413+
registerItem( specials[i]->group(), name, " " + name + " " );
414+
}
415+
}
416+
295417
void QgsExpressionBuilderWidget::setGeomCalculator( const QgsDistanceArea & da )
296418
{
297419
mDa = da;
298420
}
299421

300422
QString QgsExpressionBuilderWidget::expressionText()
301423
{
302-
if ( QgsPythonRunner::isValid() )
303-
{
304-
QString pythontext = txtPython->text();
305-
QgsPythonRunner::run( pythontext );
306-
}
307424
return txtExpressionString->text();
308425
}
309426

‎src/gui/qgsexpressionbuilderwidget.h

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,32 @@ class GUI_EXPORT QgsExpressionBuilderWidget : public QWidget, private Ui::QgsExp
150150

151151
void loadRecent( QString key );
152152

153+
/** Create a new file in the function editor
154+
*/
155+
void newFunctionFile( QString fileName = "scratch");
156+
157+
/** Save the current function editor text to the given file.
158+
*/
159+
void saveFunctionFile( QString fileName );
160+
161+
/** Load code from the given file into the function editor
162+
*/
163+
void loadCodeFromFile( QString path );
164+
165+
/** Load code into the function editor
166+
*/
167+
void loadFunctionCode( QString code );
168+
169+
/** Update the list of function files found at the given path
170+
*/
171+
void updateFunctionFileList( QString path );
172+
153173
public slots:
154174
void currentChanged( const QModelIndex &index, const QModelIndex & );
175+
void on_btnRun_pressed();
176+
void on_btnNewFile_pressed();
177+
void on_cmbFileNames_currentIndexChanged( int index );
178+
void on_btnSaveFile_pressed();
155179
void on_expressionTree_doubleClicked( const QModelIndex &index );
156180
void on_txtExpressionString_textChanged();
157181
void on_txtSearchEdit_textChanged();
@@ -174,9 +198,12 @@ class GUI_EXPORT QgsExpressionBuilderWidget : public QWidget, private Ui::QgsExp
174198
void expressionParsed( bool isValid );
175199

176200
private:
201+
void runPythonCode( QString code );
202+
void updateFunctionTree();
177203
void fillFieldValues( int fieldIndex, int countLimit );
178204
QString loadFunctionHelp( QgsExpressionItem* functionName );
179205

206+
QString mFunctionsPath;
180207
QgsVectorLayer *mLayer;
181208
QStandardItemModel *mModel;
182209
QgsExpressionItemSearchProxy *mProxyModel;

‎src/gui/symbology-ng/qgsdatadefinedsymboldialog.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "qgslogger.h"
66

77
#include <QCheckBox>
8+
#include <QSettings>
89

910

1011
QgsDataDefinedSymbolDialog::QgsDataDefinedSymbolDialog( const QList< DataDefinedSymbolEntry >& entries, const QgsVectorLayer* vl, QWidget * parent, Qt::WindowFlags f )

‎src/python/qgspythonutilsimpl.cpp

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ void QgsPythonUtilsImpl::initPython( QgisInterface* interface )
157157
return;
158158
}
159159

160+
160161
// tell the utils script where to look for the plugins
161162
runString( "qgis.utils.plugin_paths = [" + pluginpaths.join( "," ) + "]" );
162163
runString( "qgis.utils.sys_plugin_path = \"" + pluginsPath() + "\"" );
@@ -169,8 +170,14 @@ void QgsPythonUtilsImpl::initPython( QgisInterface* interface )
169170
// initialize 'iface' object
170171
runString( "qgis.utils.initInterface(" + QString::number(( unsigned long ) interface ) + ")" );
171172

172-
QString startuppath = homePythonPath() + " + \"/startup.py\"";
173-
runString( "if os.path.exists(" + startuppath + "): from startup import *\n" );
173+
// import QGIS user
174+
error_msg = QObject::tr( "Couldn't load QGIS user." ) + "\n" + QObject::tr( "Python support will be disabled." );
175+
if ( !runString( "import qgis.user", error_msg ) )
176+
{
177+
// Should we really bail because of this?!
178+
exitPython();
179+
return;
180+
}
174181

175182
// release GIL!
176183
// Later on, we acquire GIL just before doing some Python calls and

‎src/ui/qgsexpressionbuilder.ui

Lines changed: 586 additions & 468 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.