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>