From 09345b5130c5dd33cad26f34519ca2cae916bcad Mon Sep 17 00:00:00 2001
From: Alessandro Pasotti <elpaso@itopen.it>
Date: Mon, 16 Dec 2019 17:27:32 +0100
Subject: [PATCH] Add QgsExpressionStoreDialog

---
 .../qgsexpressionstoredialog.sip.in           | 60 ++++++++++++++
 python/gui/gui_auto.sip                       |  1 +
 src/app/qgsfieldcalculator.cpp                |  1 +
 src/gui/CMakeLists.txt                        |  3 +
 src/gui/attributetable/qgsdualview.cpp        |  1 +
 src/gui/qgsexpressionbuilderdialog.cpp        |  1 +
 src/gui/qgsexpressionbuilderwidget.cpp        | 66 +++++++++++----
 src/gui/qgsexpressionbuilderwidget.h          |  2 +
 src/gui/qgsexpressionselectiondialog.cpp      |  1 +
 src/gui/qgsexpressionstoredialog.cpp          | 33 ++++++++
 src/gui/qgsexpressionstoredialog.h            | 51 ++++++++++++
 src/ui/qgsexpressionbuilder.ui                | 23 +++++-
 src/ui/qgsexpressionstoredialogbase.ui        | 82 +++++++++++++++++++
 13 files changed, 308 insertions(+), 17 deletions(-)
 create mode 100644 python/gui/auto_generated/qgsexpressionstoredialog.sip.in
 create mode 100644 src/gui/qgsexpressionstoredialog.cpp
 create mode 100644 src/gui/qgsexpressionstoredialog.h
 create mode 100644 src/ui/qgsexpressionstoredialogbase.ui

diff --git a/python/gui/auto_generated/qgsexpressionstoredialog.sip.in b/python/gui/auto_generated/qgsexpressionstoredialog.sip.in
new file mode 100644
index 00000000000..877e8baeda8
--- /dev/null
+++ b/python/gui/auto_generated/qgsexpressionstoredialog.sip.in
@@ -0,0 +1,60 @@
+/************************************************************************
+ * This file has been generated automatically from                      *
+ *                                                                      *
+ * src/gui/qgsexpressionstoredialog.h                                   *
+ *                                                                      *
+ * Do not edit manually ! Edit header and run scripts/sipify.pl again   *
+ ************************************************************************/
+
+
+
+
+class QgsExpressionStoreDialog : QDialog
+{
+%Docstring
+A generic dialog for editing expression text, label and help text.
+
+.. versionadded:: 3.12
+%End
+
+%TypeHeaderCode
+#include "qgsexpressionstoredialog.h"
+%End
+  public:
+
+    QgsExpressionStoreDialog( const QString &label,
+                              const QString &expression,
+                              const QString &helpText,
+                              const QStringList &existingLabels,
+                              QWidget *parent = 0 );
+%Docstring
+Creates a QgsExpressionStoreDialog with given ``label``, ``expression`` and ``helpText``.
+
+:param existingLabels: list of existing labels for unique label validation
+:param parent: optional parent widget
+%End
+
+    QString expression( );
+%Docstring
+Returns the expression text
+%End
+
+    QString label();
+%Docstring
+Returns the label text
+%End
+
+    QString helpText();
+%Docstring
+Returns the help text
+%End
+
+};
+
+/************************************************************************
+ * This file has been generated automatically from                      *
+ *                                                                      *
+ * src/gui/qgsexpressionstoredialog.h                                   *
+ *                                                                      *
+ * Do not edit manually ! Edit header and run scripts/sipify.pl again   *
+ ************************************************************************/
diff --git a/python/gui/gui_auto.sip b/python/gui/gui_auto.sip
index 5a22b375c3f..0d3d3f81b16 100644
--- a/python/gui/gui_auto.sip
+++ b/python/gui/gui_auto.sip
@@ -65,6 +65,7 @@
 %Include auto_generated/qgsencodingfiledialog.sip
 %Include auto_generated/qgserrordialog.sip
 %Include auto_generated/qgsexpressionbuilderdialog.sip
+%Include auto_generated/qgsexpressionstoredialog.sip
 %Include auto_generated/qgsexpressionbuilderwidget.sip
 %Include auto_generated/qgsexpressionhighlighter.sip
 %Include auto_generated/qgsexpressionlineedit.sip
diff --git a/src/app/qgsfieldcalculator.cpp b/src/app/qgsfieldcalculator.cpp
index 47d78cfbeff..78dc2a82c7c 100644
--- a/src/app/qgsfieldcalculator.cpp
+++ b/src/app/qgsfieldcalculator.cpp
@@ -153,6 +153,7 @@ QgsFieldCalculator::QgsFieldCalculator( QgsVectorLayer *vl, QWidget *parent )
   mOnlyUpdateSelectedCheckBox->setText( tr( "Only update %1 selected features" ).arg( vl->selectedFeatureCount() ) );
 
   builder->loadRecent( QStringLiteral( "fieldcalc" ) );
+  builder->loadStored( QStringLiteral( "fieldcalc" ) );
 
   mInfoIcon->setPixmap( style()->standardPixmap( QStyle::SP_MessageBoxInformation ) );
 
diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt
index b91c3b1fb49..105d7de4cec 100644
--- a/src/gui/CMakeLists.txt
+++ b/src/gui/CMakeLists.txt
@@ -292,6 +292,7 @@ SET(QGIS_GUI_SRCS
   qgsexpressionhighlighter.cpp
   qgsexpressionlineedit.cpp
   qgsexpressionselectiondialog.cpp
+  qgsexpressionstoredialog.cpp
   qgsextentgroupbox.cpp
   qgsexternalresourcewidget.cpp
   qgsfeatureselectiondlg.cpp
@@ -487,6 +488,7 @@ SET(QGIS_GUI_HDRS
   qgsencodingfiledialog.h
   qgserrordialog.h
   qgsexpressionbuilderdialog.h
+  qgsexpressionstoredialog.h
   qgsexpressionbuilderwidget.h
   qgsexpressionhighlighter.h
   qgsexpressionlineedit.h
@@ -919,6 +921,7 @@ SET(QGIS_GUI_UI_HDRS
   ${CMAKE_CURRENT_BINARY_DIR}/../ui/ui_qgscredentialdialog.h
   ${CMAKE_CURRENT_BINARY_DIR}/../ui/ui_qgsdetaileditemwidgetbase.h
   ${CMAKE_CURRENT_BINARY_DIR}/../ui/ui_qgsexpressionbuilderdialogbase.h
+  ${CMAKE_CURRENT_BINARY_DIR}/../ui/ui_qgsexpressionstoredialogbase.h
   ${CMAKE_CURRENT_BINARY_DIR}/../ui/ui_qgsexpressionbuilder.h
   ${CMAKE_CURRENT_BINARY_DIR}/../ui/ui_qgsexpressionselectiondialogbase.h
   ${CMAKE_CURRENT_BINARY_DIR}/../ui/ui_qgsfeaturefilterwidget.h
diff --git a/src/gui/attributetable/qgsdualview.cpp b/src/gui/attributetable/qgsdualview.cpp
index b9b4bc16936..9d6d36ec548 100644
--- a/src/gui/attributetable/qgsdualview.cpp
+++ b/src/gui/attributetable/qgsdualview.cpp
@@ -861,6 +861,7 @@ void QgsDualView::modifySort()
   expressionBuilder->setLayer( mLayer );
   expressionBuilder->loadFieldNames();
   expressionBuilder->loadRecent( QStringLiteral( "generic" ) );
+  expressionBuilder->loadStored( QStringLiteral( "generic" ) );
   expressionBuilder->setExpressionText( sortExpression().isEmpty() ? mLayer->displayExpression() : sortExpression() );
 
   sortingGroupBox->layout()->addWidget( expressionBuilder );
diff --git a/src/gui/qgsexpressionbuilderdialog.cpp b/src/gui/qgsexpressionbuilderdialog.cpp
index fd3a3b7a237..c44bb55db2e 100644
--- a/src/gui/qgsexpressionbuilderdialog.cpp
+++ b/src/gui/qgsexpressionbuilderdialog.cpp
@@ -33,6 +33,7 @@ QgsExpressionBuilderDialog::QgsExpressionBuilderDialog( QgsVectorLayer *layer, c
   builder->setExpressionText( startText );
   builder->loadFieldNames();
   builder->loadRecent( mRecentKey );
+  builder->loadStored( mRecentKey );
 
   connect( buttonBox, &QDialogButtonBox::helpRequested, this, &QgsExpressionBuilderDialog::showHelp );
 }
diff --git a/src/gui/qgsexpressionbuilderwidget.cpp b/src/gui/qgsexpressionbuilderwidget.cpp
index 5c1cd88b502..16e8088d614 100644
--- a/src/gui/qgsexpressionbuilderwidget.cpp
+++ b/src/gui/qgsexpressionbuilderwidget.cpp
@@ -32,6 +32,7 @@
 #include "qgsexpressioncontextutils.h"
 #include "qgsfieldformatterregistry.h"
 #include "qgsfieldformatter.h"
+#include "qgsexpressionstoredialog.h"
 
 #include <QMenu>
 #include <QFile>
@@ -41,7 +42,7 @@
 #include <QComboBox>
 #include <QGraphicsOpacityEffect>
 #include <QPropertyAnimation>
-
+#include <QMessageBox>
 
 QgsExpressionBuilderWidget::QgsExpressionBuilderWidget( QWidget *parent )
   : QWidget( parent )
@@ -61,6 +62,8 @@ QgsExpressionBuilderWidget::QgsExpressionBuilderWidget( QWidget *parent )
   connect( mValuesListView, &QListView::doubleClicked, this, &QgsExpressionBuilderWidget::mValuesListView_doubleClicked );
   connect( btnSaveExpression, &QPushButton::pressed, this, &QgsExpressionBuilderWidget::storeCurrentExpression );
   connect( btnRemoveExpression, &QPushButton::pressed, this, &QgsExpressionBuilderWidget::removeSelectedExpression );
+  connect( btnClearEditor, &QPushButton::pressed, txtExpressionString, &QgsCodeEditorExpression::clear );
+
   txtHelpText->setOpenExternalLinks( true );
 
   mValueGroupBox->hide();
@@ -74,6 +77,13 @@ QgsExpressionBuilderWidget::QgsExpressionBuilderWidget( QWidget *parent )
   expressionTree->setSortingEnabled( true );
   expressionTree->sortByColumn( 0, Qt::AscendingOrder );
 
+  expressionTree->setSelectionMode( QAbstractItemView::SelectionMode::SingleSelection );
+
+  // Set icons for tool buttons
+  btnSaveExpression->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionFileSave.svg" ) ) );
+  btnRemoveExpression->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionDeleteSelected.svg" ) ) );
+  btnClearEditor->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionFileNew.svg" ) ) );
+
   expressionTree->setContextMenuPolicy( Qt::CustomContextMenu );
   connect( this, &QgsExpressionBuilderWidget::expressionParsed, this, &QgsExpressionBuilderWidget::setExpressionState );
   connect( expressionTree, &QWidget::customContextMenuRequested, this, &QgsExpressionBuilderWidget::showContextMenu );
@@ -250,6 +260,10 @@ void QgsExpressionBuilderWidget::currentChanged( const QModelIndex &index, const
   // Show the help for the current item.
   QString help = loadFunctionHelp( item );
   txtHelpText->setText( help );
+
+  btnRemoveExpression->setEnabled( item->parent() &&
+                                   item->parent()->text() == mStoredGroupName );
+
 }
 
 void QgsExpressionBuilderWidget::btnRun_pressed()
@@ -594,12 +608,12 @@ void QgsExpressionBuilderWidget::loadRecent( const QString &collection )
 void QgsExpressionBuilderWidget::loadStored( const QString &collection )
 {
   mRecentKey = collection;
-  const QString groupName = tr( "Stored (%1)" ).arg( collection );
+  mStoredGroupName = tr( "Stored (%1)" ).arg( collection );
 
   // Cleanup
-  if ( mExpressionGroups.contains( groupName ) )
+  if ( mExpressionGroups.contains( mStoredGroupName ) )
   {
-    QgsExpressionItem *node = mExpressionGroups.value( groupName );
+    QgsExpressionItem *node = mExpressionGroups.value( mStoredGroupName );
     node->removeRows( 0, node->rowCount() );
   }
 
@@ -610,12 +624,13 @@ void QgsExpressionBuilderWidget::loadStored( const QString &collection )
   QString helpText;
   QString expression;
   int i = 0;
-  for ( const auto &label : settings.childGroups() )
+  mStoredLabels = settings.childGroups();
+  for ( const auto &label : qgis::as_const( mStoredLabels ) )
   {
     settings.beginGroup( label );
     expression = settings.value( QStringLiteral( "expression" ) ).toString();
     helpText = settings.value( QStringLiteral( "helpText" ) ).toString();
-    this->registerItem( groupName, label, expression, helpText, QgsExpressionItem::ExpressionNode, false, i++ );
+    this->registerItem( mStoredGroupName, label, expression, helpText, QgsExpressionItem::ExpressionNode, false, i++ );
     settings.endGroup();
   }
 }
@@ -629,6 +644,14 @@ void QgsExpressionBuilderWidget::saveToStored( const QString &label, const QStri
   settings.setValue( QStringLiteral( "expression" ), expression );
   settings.setValue( QStringLiteral( "helpText" ), helpText );
   this->loadStored( collection );
+  // Scroll
+  const QModelIndexList idxs { expressionTree->model()->match( expressionTree->model()->index( 0, 0 ),
+                               Qt::DisplayRole, label, 1,
+                               Qt::MatchFlag::MatchRecursive ) };
+  if ( ! idxs.isEmpty() )
+  {
+    expressionTree->scrollTo( idxs.first() );
+  }
 }
 
 void QgsExpressionBuilderWidget::removeFromStored( const QString &name, const QString &collection )
@@ -771,6 +794,9 @@ void QgsExpressionBuilderWidget::txtExpressionString_textChanged()
   QString text = expressionText();
   clearErrors();
 
+  btnClearEditor->setEnabled( ! txtExpressionString->text().isEmpty() );
+  btnSaveExpression->setEnabled( false );
+
   // If the string is empty the expression will still "fail" although
   // we don't show the user an error as it will be confusing.
   if ( text.isEmpty() )
@@ -838,6 +864,7 @@ void QgsExpressionBuilderWidget::txtExpressionString_textChanged()
     setParserError( false );
     setEvalError( false );
     createMarkers( exp.rootNode() );
+    btnSaveExpression->setEnabled( true );
   }
 
 }
@@ -1201,21 +1228,34 @@ void QgsExpressionBuilderWidget::autosave()
 void QgsExpressionBuilderWidget::storeCurrentExpression()
 {
   const QString expression { this->expressionText() };
-  saveToStored( expression, expression, expression, mRecentKey );
+  QgsExpressionStoreDialog dlg { expression, expression, QString( ), mStoredLabels };
+  if ( dlg.exec() == QDialog::DialogCode::Accepted )
+  {
+    saveToStored( dlg.label(), dlg.expression(), dlg.helpText(), mRecentKey );
+  }
 }
 
 void QgsExpressionBuilderWidget::removeSelectedExpression()
 {
-  QModelIndex idx { expressionTree->currentIndex() };
+
+// Get the item
+  QModelIndex idx = mProxyModel->mapToSource( expressionTree->currentIndex() );
   QgsExpressionItem *item = dynamic_cast<QgsExpressionItem *>( mModel->itemFromIndex( idx ) );
   if ( !item )
     return;
 
-  // Don't handle the double-click if we are on a header node.
-  if ( item->getItemType() == QgsExpressionItem::Header )
+  // Don't handle remove if we are on a header node or the parent
+  // is not the stored group
+  if ( item->getItemType() == QgsExpressionItem::Header ||
+       ( item->parent() && item->parent()->text() != mStoredGroupName ) )
     return;
 
-  removeFromStored( item->text(), mRecentKey );
+  if ( QMessageBox::Yes == QMessageBox::question( this, tr( "Remove Stored Expression" ),
+       tr( "Do you really want to remove stored expressions '%1'?" ).arg( item->text() ),
+       QMessageBox::Yes | QMessageBox::No ) )
+  {
+    removeFromStored( item->text(), mRecentKey );
+  }
 
 }
 
@@ -1281,10 +1321,6 @@ QString QgsExpressionBuilderWidget::loadFunctionHelp( QgsExpressionItem *express
   return "<head><style>" + helpStylesheet() + "</style></head><body>" + helpContents + "</body>";
 }
 
-
-
-
-
 QgsExpressionItemSearchProxy::QgsExpressionItemSearchProxy()
 {
   setFilterCaseSensitivity( Qt::CaseInsensitive );
diff --git a/src/gui/qgsexpressionbuilderwidget.h b/src/gui/qgsexpressionbuilderwidget.h
index f9aa38bb090..4074a9001d8 100644
--- a/src/gui/qgsexpressionbuilderwidget.h
+++ b/src/gui/qgsexpressionbuilderwidget.h
@@ -497,6 +497,8 @@ class GUI_EXPORT QgsExpressionBuilderWidget : public QWidget, private Ui::QgsExp
     QPointer< QgsProject > mProject;
     bool mEvalError = true;
     bool mParserError = true;
+    QString mStoredGroupName;
+    QStringList mStoredLabels;
 };
 
 // clazy:excludeall=qstring-allocations
diff --git a/src/gui/qgsexpressionselectiondialog.cpp b/src/gui/qgsexpressionselectiondialog.cpp
index a19dd9d5ead..d14ff39c141 100644
--- a/src/gui/qgsexpressionselectiondialog.cpp
+++ b/src/gui/qgsexpressionselectiondialog.cpp
@@ -59,6 +59,7 @@ QgsExpressionSelectionDialog::QgsExpressionSelectionDialog( QgsVectorLayer *laye
   mExpressionBuilder->setExpressionText( startText );
   mExpressionBuilder->loadFieldNames();
   mExpressionBuilder->loadRecent( QStringLiteral( "Selection" ) );
+  mExpressionBuilder->loadStored( QStringLiteral( "Selection" ) );
 
   QgsExpressionContext context( QgsExpressionContextUtils::globalProjectLayerScopes( mLayer ) );
   mExpressionBuilder->setExpressionContext( context );
diff --git a/src/gui/qgsexpressionstoredialog.cpp b/src/gui/qgsexpressionstoredialog.cpp
new file mode 100644
index 00000000000..c8418ad075a
--- /dev/null
+++ b/src/gui/qgsexpressionstoredialog.cpp
@@ -0,0 +1,33 @@
+#include "qgsexpressionstoredialog.h"
+#include <QPushButton>
+#include <QStyle>
+
+QgsExpressionStoreDialog::QgsExpressionStoreDialog( const QString &label, const QString &expression, const QString &helpText, const QStringList &existingLabels, QWidget *parent )
+  : QDialog( parent )
+  , mExistingLabels( existingLabels )
+{
+  setupUi( this );
+  mExpression->setText( expression );
+  mHelpText->setText( helpText );
+  connect( buttonBox, &QDialogButtonBox::accepted, this, &QgsExpressionStoreDialog::accept );
+  connect( buttonBox, &QDialogButtonBox::rejected, this, &QgsExpressionStoreDialog::reject );
+  mValidationError->hide();
+  mValidationError->setStyleSheet( QStringLiteral( "QLabel { color : red; }" ) );
+  QPushButton *saveBtn { buttonBox->button( QDialogButtonBox::StandardButton::Save ) };
+  saveBtn->setEnabled( false );
+  connect( mLabel, &QLineEdit::textChanged, this, [ = ]( const QString & text )
+  {
+    if ( mExistingLabels.contains( text ) )
+    {
+      mValidationError->show();
+      saveBtn->setEnabled( false );
+    }
+    else
+    {
+      mValidationError->hide();
+      saveBtn->setEnabled( true );
+    }
+  } );
+  mLabel->setText( label );
+}
+
diff --git a/src/gui/qgsexpressionstoredialog.h b/src/gui/qgsexpressionstoredialog.h
new file mode 100644
index 00000000000..0fe4767b039
--- /dev/null
+++ b/src/gui/qgsexpressionstoredialog.h
@@ -0,0 +1,51 @@
+#ifndef QGSEXPRESSIONSTOREDIALOG_H
+#define QGSEXPRESSIONSTOREDIALOG_H
+
+#include "qgis_gui.h"
+#include <QDialog>
+#include "ui_qgsexpressionstoredialogbase.h"
+
+
+
+/**
+ * \ingroup gui
+ * A generic dialog for editing expression text, label and help text.
+ * \since QGIS 3.12
+ */
+class GUI_EXPORT QgsExpressionStoreDialog : public QDialog, private Ui::QgsExpressionStoreDialogBase
+{
+  public:
+
+    /**
+     * Creates a QgsExpressionStoreDialog with given \a label, \a expression and \a helpText.
+     * \param existingLabels list of existing labels for unique label validation
+     * \param parent optional parent widget
+     */
+    QgsExpressionStoreDialog( const QString &label,
+                              const QString &expression,
+                              const QString &helpText,
+                              const QStringList &existingLabels,
+                              QWidget *parent = nullptr );
+
+    /**
+     * Returns the expression text
+     */
+    QString expression( ) { return mExpression->text( ); }
+
+    /**
+     * Returns the label text
+     */
+    QString label() { return  mLabel->text(); }
+
+    /**
+     * Returns the help text
+     */
+    QString helpText() { return  mHelpText->toHtml(); }
+
+  private:
+
+    QStringList mExistingLabels;
+
+};
+
+#endif // QGSEXPRESSIONSTOREDIALOG_H
diff --git a/src/ui/qgsexpressionbuilder.ui b/src/ui/qgsexpressionbuilder.ui
index f95a73c16c9..58b3b4dea00 100644
--- a/src/ui/qgsexpressionbuilder.ui
+++ b/src/ui/qgsexpressionbuilder.ui
@@ -325,9 +325,25 @@
                     <number>0</number>
                    </property>
                    <item>
-                    <widget class="QToolButton" name="btnSaveExpression">
+                    <widget class="QToolButton" name="btnClearEditor">
+                     <property name="enabled">
+                      <bool>false</bool>
+                     </property>
                      <property name="toolTip">
-                      <string>Add the selected expression to the stored expressions</string>
+                      <string>Clear the expression editor</string>
+                     </property>
+                     <property name="text">
+                      <string>Clear</string>
+                     </property>
+                    </widget>
+                   </item>
+                   <item>
+                    <widget class="QToolButton" name="btnSaveExpression">
+                     <property name="enabled">
+                      <bool>false</bool>
+                     </property>
+                     <property name="toolTip">
+                      <string>Add current expression to stored expressions</string>
                      </property>
                      <property name="text">
                       <string>Save</string>
@@ -336,6 +352,9 @@
                    </item>
                    <item>
                     <widget class="QToolButton" name="btnRemoveExpression">
+                     <property name="enabled">
+                      <bool>false</bool>
+                     </property>
                      <property name="toolTip">
                       <string>Removes selected expression from the stored expressions</string>
                      </property>
diff --git a/src/ui/qgsexpressionstoredialogbase.ui b/src/ui/qgsexpressionstoredialogbase.ui
new file mode 100644
index 00000000000..99266052cb2
--- /dev/null
+++ b/src/ui/qgsexpressionstoredialogbase.ui
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>QgsExpressionStoreDialogBase</class>
+ <widget class="QDialog" name="QgsExpressionStoreDialogBase">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>395</width>
+    <height>211</height>
+   </rect>
+  </property>
+  <property name="sizePolicy">
+   <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
+    <horstretch>0</horstretch>
+    <verstretch>0</verstretch>
+   </sizepolicy>
+  </property>
+  <property name="windowTitle">
+   <string>Store expression</string>
+  </property>
+  <layout class="QFormLayout" name="formLayout">
+   <item row="0" column="0">
+    <widget class="QLabel" name="lblLabel">
+     <property name="text">
+      <string>Label</string>
+     </property>
+    </widget>
+   </item>
+   <item row="6" column="1">
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Cancel|QDialogButtonBox::Save</set>
+     </property>
+    </widget>
+   </item>
+   <item row="0" column="1">
+    <widget class="QLineEdit" name="mLabel"/>
+   </item>
+   <item row="4" column="1">
+    <widget class="QTextEdit" name="mHelpText"/>
+   </item>
+   <item row="4" column="0">
+    <widget class="QLabel" name="lblHelpText">
+     <property name="text">
+      <string>Help text</string>
+     </property>
+    </widget>
+   </item>
+   <item row="2" column="1">
+    <widget class="QgsCodeEditorExpression" name="mExpression" native="true"/>
+   </item>
+   <item row="2" column="0">
+    <widget class="QLabel" name="lblExpression">
+     <property name="text">
+      <string>Expression</string>
+     </property>
+    </widget>
+   </item>
+   <item row="1" column="1">
+    <widget class="QLabel" name="mValidationError">
+     <property name="text">
+      <string>A stored expression with this name already exists!</string>
+     </property>
+     <property name="wordWrap">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <customwidgets>
+  <customwidget>
+   <class>QgsCodeEditorExpression</class>
+   <extends>QWidget</extends>
+   <header>qgscodeeditorexpression.h</header>
+   <container>1</container>
+  </customwidget>
+ </customwidgets>
+ <resources/>
+ <connections/>
+</ui>