New QgsExpressionLineEdit widget

Consists of a filter line edit + button to open expression builder

This widget is a bit like the existing QgsFieldExpressionWidget,
but for cases where a combo box does not make sense. Eg, when
no fields are available for the expression to use.

It also has an optional multiline mode, which allows it to be
used in place of the full-blown QgsExpressionBuilderWidget when
space is a problem.
This commit is contained in:
Nyall Dawson 2016-08-18 20:49:19 +10:00
parent 86feb6d376
commit 8f4ca47b70
7 changed files with 545 additions and 0 deletions

View File

@ -70,6 +70,7 @@
%Include qgsexpressionbuilderdialog.sip
%Include qgsexpressionbuilderwidget.sip
%Include qgsexpressionhighlighter.sip
%Include qgsexpressionlineedit.sip
%Include qgsexpressionselectiondialog.sip
%Include qgsextentgroupbox.sip
%Include qgsexternalresourcewidget.sip

View File

@ -0,0 +1,90 @@
/** \ingroup gui
* @class QgsExpressionLineEdit
* @brief The QgsExpressionLineEdit widget includes a line edit for entering expressions
* together with a button to open the expression creation dialog.
*
* This widget is designed for use in contexts where no layer fields are available for
* use in an expression. In contexts where the expression is directly associated with
* a layer and fields can be used, then QgsFieldExpressionWidget is a more appropriate
* choice as it gives users direct access to select fields from a drop down list.
* @note added in QGIS 3.0
*/
class QgsExpressionLineEdit : QWidget
{
%TypeHeaderCode
#include "qgsexpressionlineedit.h"
%End
public:
/**
* Constructor for QgsExpressionLineEdit.
* @param parent parent widget
*/
explicit QgsExpressionLineEdit( QWidget *parent /TransferThis/ = nullptr );
/** Sets the title used in the expression builder dialog
* @param title dialog title
* @see expressionDialogTitle()
*/
void setExpressionDialogTitle( const QString& title );
/** Returns the title used for the expression dialog.
* @see setExpressionDialogTitle()
*/
QString expressionDialogTitle() const;
/** Sets whether the widget should show a multiline text editor.
* @param multiLine set to true to show multiline editor, or false
* to show single line editor (the default).
*/
void setMultiLine( bool multiLine );
/** Set the geometry calculator used in the expression dialog.
* @param distanceArea calculator
*/
void setGeomCalculator( const QgsDistanceArea &distanceArea );
/** Sets a layer associated with the widget. Required in order to get the fields and values
* from the layer.
* @param layer vector layer
*/
void setLayer( QgsVectorLayer* layer );
/** Returns the current expression shown in the widget.
* @see setExpression()
*/
QString expression() const;
/**
* Returns true if the current expression is valid.
* @param expressionError will be set to any generated error message if specified
*/
bool isValidExpression( QString *expressionError /Out/ = nullptr ) const;
/**
* Register an expression context generator class that will be used to retrieve
* an expression context for the widget.
* @param generator A QgsExpressionContextGenerator class that will be used to
* create an expression context when required.
*/
void registerExpressionContextGenerator( const QgsExpressionContextGenerator* generator );
signals:
/** Emitted when the expression is changed.
* @param expression new expression
*/
void expressionChanged( const QString& expression );
public slots:
/** Sets the current expression to show in the widget.
* @param expression expression string
* @see expression()
*/
void setExpression( const QString& expression );
protected:
void changeEvent( QEvent* event );
};

View File

@ -208,6 +208,7 @@ SET(QGIS_GUI_SRCS
qgsexpressionbuilderdialog.cpp
qgsexpressionbuilderwidget.cpp
qgsexpressionhighlighter.cpp
qgsexpressionlineedit.cpp
qgsexpressionselectiondialog.cpp
qgsextentgroupbox.cpp
qgsexternalresourcewidget.cpp
@ -365,6 +366,7 @@ SET(QGIS_GUI_MOC_HDRS
qgsexpressionbuilderdialog.h
qgsexpressionbuilderwidget.h
qgsexpressionhighlighter.h
qgsexpressionlineedit.h
qgsexpressionselectiondialog.h
qgsextentgroupbox.h
qgsexternalresourcewidget.h

View File

@ -0,0 +1,221 @@
/***************************************************************************
qgsexpressionlineedit.cpp
------------------------
Date : 18.08.2016
Copyright : (C) 2016 Nyall Dawson
Email : nyall dot dawson 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 "qgsexpressionlineedit.h"
#include "qgsfilterlineedit.h"
#include "qgsexpressioncontext.h"
#include "qgsapplication.h"
#include "qgsexpressionbuilderdialog.h"
#include "qgsexpressioncontextgenerator.h"
#include "qgscodeeditorsql.h"
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QToolButton>
QgsExpressionLineEdit::QgsExpressionLineEdit( QWidget *parent )
: QWidget( parent )
, mLineEdit( nullptr )
, mCodeEditor( nullptr )
, mExpressionDialogTitle( tr( "Expression dialog" ) )
, mDa( nullptr )
, mExpressionContextGenerator( nullptr )
, mLayer( nullptr )
{
mButton = new QToolButton();
mButton->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Minimum );
mButton->setIcon( QgsApplication::getThemeIcon( "/mIconExpression.svg" ) );
connect( mButton, SIGNAL( clicked() ), this, SLOT( editExpression() ) );
//sets up layout
setMultiLine( false );
mExpressionContext = QgsExpressionContext();
mExpressionContext << QgsExpressionContextUtils::globalScope()
<< QgsExpressionContextUtils::projectScope();
}
void QgsExpressionLineEdit::setExpressionDialogTitle( const QString& title )
{
mExpressionDialogTitle = title;
}
void QgsExpressionLineEdit::setMultiLine( bool multiLine )
{
QString exp = expression();
if ( multiLine && !mCodeEditor )
{
mCodeEditor = new QgsCodeEditorSQL();
mCodeEditor->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding );
delete mLineEdit;
mLineEdit = nullptr;
QHBoxLayout* newLayout = new QHBoxLayout();
newLayout->setContentsMargins( 0, 0, 0, 0 );
newLayout->addWidget( mCodeEditor );
QVBoxLayout* vLayout = new QVBoxLayout();
vLayout->addWidget( mButton );
vLayout->addStretch();
newLayout->addLayout( vLayout );
delete layout();
setLayout( newLayout );
setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding );
setFocusProxy( mCodeEditor );
connect( mCodeEditor, SIGNAL( textChanged() ), this, SLOT( expressionEdited() ) );
setExpression( exp );
}
else if ( !multiLine && !mLineEdit )
{
delete mCodeEditor;
mCodeEditor = nullptr;
mLineEdit = new QgsFilterLineEdit();
mLineEdit->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Minimum );
QHBoxLayout* newLayout = new QHBoxLayout();
newLayout->setContentsMargins( 0, 0, 0, 0 );
newLayout->addWidget( mLineEdit );
newLayout->addWidget( mButton );
delete layout();
setLayout( newLayout );
setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Minimum );
setFocusProxy( mLineEdit );
connect( mLineEdit, SIGNAL( textChanged( QString ) ), this, SLOT( expressionEdited( QString ) ) );
setExpression( exp );
}
}
void QgsExpressionLineEdit::setGeomCalculator( const QgsDistanceArea &da )
{
mDa.reset( new QgsDistanceArea( da ) );
}
void QgsExpressionLineEdit::setLayer( QgsVectorLayer* layer )
{
mLayer = layer;
}
QString QgsExpressionLineEdit::expression() const
{
if ( mLineEdit )
return mLineEdit->text();
else if ( mCodeEditor )
return mCodeEditor->text();
return QString();
}
bool QgsExpressionLineEdit::isValidExpression( QString *expressionError ) const
{
QString temp;
return QgsExpression::isValid( expression(), &mExpressionContext, expressionError ? *expressionError : temp );
}
void QgsExpressionLineEdit::registerExpressionContextGenerator( const QgsExpressionContextGenerator* generator )
{
mExpressionContextGenerator = generator;
}
void QgsExpressionLineEdit::setExpression( const QString& newExpression )
{
if ( mLineEdit )
mLineEdit->setText( newExpression );
else if ( mCodeEditor )
mCodeEditor->setText( newExpression );
}
void QgsExpressionLineEdit::editExpression()
{
QString currentExpression = expression();
QgsExpressionContext context = mExpressionContextGenerator ? mExpressionContextGenerator->createExpressionContext() : mExpressionContext;
QgsExpressionBuilderDialog dlg( mLayer, currentExpression, this, "generic", context );
if ( !mDa.isNull() )
{
dlg.setGeomCalculator( *mDa );
}
dlg.setWindowTitle( mExpressionDialogTitle );
if ( dlg.exec() )
{
QString newExpression = dlg.expressionText();
setExpression( newExpression );
}
}
void QgsExpressionLineEdit::expressionEdited()
{
emit expressionChanged( expression() );
}
void QgsExpressionLineEdit::expressionEdited( const QString& expression )
{
updateLineEditStyle( expression );
emit expressionChanged( expression );
}
void QgsExpressionLineEdit::changeEvent( QEvent* event )
{
if ( event->type() == QEvent::EnabledChange )
{
updateLineEditStyle( expression() );
}
}
void QgsExpressionLineEdit::updateLineEditStyle( const QString& expression )
{
if ( !mLineEdit )
return;
QPalette palette;
if ( !isEnabled() )
{
palette.setColor( QPalette::Text, Qt::gray );
}
else
{
bool isValid = true;
if ( !expression.isEmpty() )
{
isValid = isExpressionValid( expression );
}
if ( !isValid )
{
palette.setColor( QPalette::Text, Qt::red );
}
else
{
palette.setColor( QPalette::Text, Qt::black );
}
}
mLineEdit->setPalette( palette );
}
bool QgsExpressionLineEdit::isExpressionValid( const QString& expressionStr )
{
QgsExpression expression( expressionStr );
expression.prepare( &mExpressionContext );
return !expression.hasParserError();
}

View File

@ -0,0 +1,152 @@
/***************************************************************************
qgsexpressionlineedit.h
----------------------
Date : 18.08.2016
Copyright : (C) 2016 Nyall Dawson
Email : nyall dot dawson 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. *
* *
***************************************************************************/
#ifndef QGSEXPRESSIONLINEEDIT_H
#define QGSEXPRESSIONLINEEDIT_H
#include <QWidget>
#include "qgsexpressioncontext.h"
#include "qgsdistancearea.h"
class QgsFilterLineEdit;
class QToolButton;
class QgsDistanceArea;
class QgsExpressionContextGenerator;
class QgsCodeEditorSQL;
/** \ingroup gui
* @class QgsExpressionLineEdit
* @brief The QgsExpressionLineEdit widget includes a line edit for entering expressions
* together with a button to open the expression creation dialog.
*
* This widget is designed for use in contexts where no layer fields are available for
* use in an expression. In contexts where the expression is directly associated with
* a layer and fields can be used, then QgsFieldExpressionWidget is a more appropriate
* choice as it gives users direct access to select fields from a drop down list.
*
* QgsExpressionLineEdit also supports a multiline editor mode which is useful where
* more space is available for the widget, but where QgsExpressionBuilderWidget
* is too complex or large for use.
*
* @note added in QGIS 3.0
*/
class GUI_EXPORT QgsExpressionLineEdit : public QWidget
{
Q_OBJECT
public:
/**
* Constructor for QgsExpressionLineEdit.
* @param parent parent widget
*/
explicit QgsExpressionLineEdit( QWidget *parent = nullptr );
/** Sets the title used in the expression builder dialog
* @param title dialog title
* @see expressionDialogTitle()
*/
void setExpressionDialogTitle( const QString& title );
/** Returns the title used for the expression dialog.
* @see setExpressionDialogTitle()
*/
QString expressionDialogTitle() const { return mExpressionDialogTitle; }
/** Sets whether the widget should show a multiline text editor.
* @param multiLine set to true to show multiline editor, or false
* to show single line editor (the default).
*/
void setMultiLine( bool multiLine );
/** Set the geometry calculator used in the expression dialog.
* @param distanceArea calculator
*/
void setGeomCalculator( const QgsDistanceArea &distanceArea );
/** Sets a layer associated with the widget. Required in order to get the fields and values
* from the layer.
* @param layer vector layer
*/
void setLayer( QgsVectorLayer* layer );
/** Returns the current expression shown in the widget.
* @see setExpression()
*/
QString expression() const;
/**
* Returns true if the current expression is valid.
* @param expressionError will be set to any generated error message if specified
*/
bool isValidExpression( QString *expressionError = nullptr ) const;
/**
* Register an expression context generator class that will be used to retrieve
* an expression context for the widget.
* @param generator A QgsExpressionContextGenerator class that will be used to
* create an expression context when required.
*/
void registerExpressionContextGenerator( const QgsExpressionContextGenerator* generator );
signals:
/** Emitted when the expression is changed.
* @param expression new expression
*/
void expressionChanged( const QString& expression );
public slots:
/** Sets the current expression to show in the widget.
* @param expression expression string
* @see expression()
*/
void setExpression( const QString& expression );
protected:
void changeEvent( QEvent* event ) override;
private slots:
//! When the expression is edited by the user in the line edit, it will be checked for validity
void expressionEdited( const QString& expression );
void expressionEdited();
//! Opens the expression editor dialog to edit the current expression or add a new expression
void editExpression();
/**
* @brief updateLineEditStyle will re-style (color/font) the line edit depending on content and status
* @param expression if expression is given it will be evaluated for the given string, otherwise it takes
* current expression from the model
*/
void updateLineEditStyle( const QString& expression = QString() );
private:
QgsFilterLineEdit* mLineEdit;
QgsCodeEditorSQL* mCodeEditor;
QToolButton* mButton;
QString mExpressionDialogTitle;
QScopedPointer<QgsDistanceArea> mDa;
QgsExpressionContext mExpressionContext;
const QgsExpressionContextGenerator* mExpressionContextGenerator;
QgsVectorLayer* mLayer;
bool isExpressionValid( const QString& expressionStr );
friend class TestQgsFieldExpressionWidget;
};
#endif // QGSEXPRESSIONLINEEDIT_H

View File

@ -41,6 +41,7 @@ ADD_PYTHON_TEST(PyQgsDelimitedTextProvider test_qgsdelimitedtextprovider.py)
ADD_PYTHON_TEST(PyQgsDistanceArea test_qgsdistancearea.py)
ADD_PYTHON_TEST(PyQgsEditWidgets test_qgseditwidgets.py)
ADD_PYTHON_TEST(PyQgsExpression test_qgsexpression.py)
ADD_PYTHON_TEST(PyQgsExpressionLineEdit test_qgsexpressionlineedit.py)
ADD_PYTHON_TEST(PyQgsFeature test_qgsfeature.py)
ADD_PYTHON_TEST(PyQgsProject test_qgsproject.py)
ADD_PYTHON_TEST(PyQgsFeatureIterator test_qgsfeatureiterator.py)

View File

@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
"""QGIS Unit tests for QgsExpressionLineEdit
.. note:: 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.
"""
__author__ = 'Nyall Dawson'
__date__ = '20/08/2016'
__copyright__ = 'Copyright 2016, The QGIS Project'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'
import qgis # NOQA
import os
from qgis.gui import QgsExpressionLineEdit
try:
from qgis.PyQt.QtTest import QSignalSpy
use_signal_spy = True
except:
use_signal_spy = False
from qgis.testing import start_app, unittest
start_app()
class TestQgsExpressionLineEdit(unittest.TestCase):
def testDialog(self):
""" test dialog related methods """
w = qgis.gui.QgsExpressionLineEdit()
w.setExpressionDialogTitle('test')
self.assertEqual(w.expressionDialogTitle(), 'test')
def testSetGetExpression(self):
""" test setting and getting expression """
w = qgis.gui.QgsExpressionLineEdit()
self.assertFalse(w.expression())
w.setExpression('1+2')
self.assertEqual(w.expression(), '1+2')
result, error = w.isValidExpression()
self.assertTrue(result)
w.setExpression('1+')
self.assertEqual(w.expression(), '1+')
result, error = w.isValidExpression()
self.assertFalse(result)
self.assertTrue(error)
# try with a multiline widget too
w.setMultiLine(True)
self.assertEqual(w.expression(), '1+')
w.setExpression('1+3')
self.assertEqual(w.expression(), '1+3')
# and flip back again...
w.setMultiLine(False)
self.assertEqual(w.expression(), '1+3')
@unittest.skipIf(not use_signal_spy, "No QSignalSpy available")
def test_ChangedSignals(self):
""" test that signals are correctly emitted when changing expressions"""
w = qgis.gui.QgsExpressionLineEdit()
expression_changed_spy = QSignalSpy(w.expressionChanged)
w.setExpression('1+1')
self.assertEqual(len(expression_changed_spy), 1)
self.assertEqual(expression_changed_spy[0][0], '1+1')
if __name__ == '__main__':
unittest.main()