[feature] Saving/loading SQL queries from Execute SQL/Update SQL dialogs

This adds support for saving and loading SQL queries to a .sql text
file to the Execute SQL dialog and Update SQL dialogs.

Effectively, it ports this functionality from the DB Manager plugin
over to the core browser-based database connection facilities.

The UX has been designed to mimic the same functionality from
other standard parts of QGIS, eg the Processing Script Editor. Toolbar
actions are used accordingly, instead of the old text button approach
used in DB Manager.

Sponsored by City of Canning
This commit is contained in:
Nyall Dawson 2025-02-25 12:28:33 +10:00
parent feb0dd789d
commit 090e9fb671
7 changed files with 554 additions and 41 deletions

View File

@ -35,6 +35,7 @@ be used in different contexts like when updating the SQL of an existing query la
#include "qgsqueryresultwidget.h"
%End
public:
enum class QueryWidgetMode /BaseType=IntFlag/
{
SqlQueryMode,
@ -68,6 +69,7 @@ Sets the connection to ``connection``, ownership is transferred to the widget.
Convenience method to set the SQL editor text to ``sql``.
%End
public slots:
void notify( const QString &title, const QString &text, Qgis::MessageLevel level = Qgis::MessageLevel::Info );
@ -125,6 +127,79 @@ Emitted when the first batch of results has been fetched.
If the query returns no results this signal is not emitted.
%End
};
class QgsQueryResultDialog : QDialog
{
%Docstring(signature="appended")
A dialog which allows users to enter and run an SQL query on a
DB connection (an instance of :py:class:`QgsAbstractDatabaseProviderConnection`).
.. note::
the ownership of the connection is transferred to the dialog.
.. seealso:: :py:class:`QgsQueryResultWidget`
.. versionadded:: 3.44
%End
%TypeHeaderCode
#include "qgsqueryresultwidget.h"
%End
public:
QgsQueryResultDialog( QgsAbstractDatabaseProviderConnection *connection /Transfer/ = 0, QWidget *parent = 0 );
%Docstring
Constructor for QgsQueryResultDialog.
Ownership of the ``connection`` is transferred to the dialog.
%End
QgsQueryResultWidget *resultWidget();
%Docstring
Returns the :py:class:`QgsQueryResultWidget` shown in the dialog.
%End
virtual void closeEvent( QCloseEvent *event );
};
class QgsQueryResultMainWindow : QMainWindow
{
%Docstring(signature="appended")
A main window which allows users to enter and run an SQL query on a
DB connection (an instance of :py:class:`QgsAbstractDatabaseProviderConnection`).
.. note::
the ownership of the connection is transferred to the window.
.. seealso:: :py:class:`QgsQueryResultWidget`
.. versionadded:: 3.44
%End
%TypeHeaderCode
#include "qgsqueryresultwidget.h"
%End
public:
QgsQueryResultMainWindow( QgsAbstractDatabaseProviderConnection *connection /Transfer/ = 0, const QString &identifierName = QString() );
%Docstring
Constructor for QgsQueryResultMainWindow.
Ownership of the ``connection`` is transferred to the window.
%End
QgsQueryResultWidget *resultWidget();
%Docstring
Returns the :py:class:`QgsQueryResultWidget` shown in the window.
%End
virtual void closeEvent( QCloseEvent *event );
};
/************************************************************************

View File

@ -35,6 +35,7 @@ be used in different contexts like when updating the SQL of an existing query la
#include "qgsqueryresultwidget.h"
%End
public:
enum class QueryWidgetMode
{
SqlQueryMode,
@ -68,6 +69,7 @@ Sets the connection to ``connection``, ownership is transferred to the widget.
Convenience method to set the SQL editor text to ``sql``.
%End
public slots:
void notify( const QString &title, const QString &text, Qgis::MessageLevel level = Qgis::MessageLevel::Info );
@ -125,6 +127,79 @@ Emitted when the first batch of results has been fetched.
If the query returns no results this signal is not emitted.
%End
};
class QgsQueryResultDialog : QDialog
{
%Docstring(signature="appended")
A dialog which allows users to enter and run an SQL query on a
DB connection (an instance of :py:class:`QgsAbstractDatabaseProviderConnection`).
.. note::
the ownership of the connection is transferred to the dialog.
.. seealso:: :py:class:`QgsQueryResultWidget`
.. versionadded:: 3.44
%End
%TypeHeaderCode
#include "qgsqueryresultwidget.h"
%End
public:
QgsQueryResultDialog( QgsAbstractDatabaseProviderConnection *connection /Transfer/ = 0, QWidget *parent = 0 );
%Docstring
Constructor for QgsQueryResultDialog.
Ownership of the ``connection`` is transferred to the dialog.
%End
QgsQueryResultWidget *resultWidget();
%Docstring
Returns the :py:class:`QgsQueryResultWidget` shown in the dialog.
%End
virtual void closeEvent( QCloseEvent *event );
};
class QgsQueryResultMainWindow : QMainWindow
{
%Docstring(signature="appended")
A main window which allows users to enter and run an SQL query on a
DB connection (an instance of :py:class:`QgsAbstractDatabaseProviderConnection`).
.. note::
the ownership of the connection is transferred to the window.
.. seealso:: :py:class:`QgsQueryResultWidget`
.. versionadded:: 3.44
%End
%TypeHeaderCode
#include "qgsqueryresultwidget.h"
%End
public:
QgsQueryResultMainWindow( QgsAbstractDatabaseProviderConnection *connection /Transfer/ = 0, const QString &identifierName = QString() );
%Docstring
Constructor for QgsQueryResultMainWindow.
Ownership of the ``connection`` is transferred to the window.
%End
QgsQueryResultWidget *resultWidget();
%Docstring
Returns the :py:class:`QgsQueryResultWidget` shown in the window.
%End
virtual void closeEvent( QCloseEvent *event );
};
/************************************************************************

View File

@ -2010,25 +2010,13 @@ void QgsDatabaseItemGuiProvider::openSqlDialog( const QString &connectionUri, co
std::unique_ptr<QgsAbstractDatabaseProviderConnection> conn( qgis::down_cast<QgsAbstractDatabaseProviderConnection *>( md->createConnection( connectionUri, QVariantMap() ) ) );
// Create the SQL dialog: this might become an independent class dialog in the future, for now
// we are still prototyping the features that this dialog will have.
QMainWindow *dialog = new QMainWindow();
dialog->setObjectName( QStringLiteral( "SQLCommandsDialog" ) );
if ( !identifierName.isEmpty() )
dialog->setWindowTitle( tr( "%1 — Execute SQL" ).arg( identifierName ) );
else
dialog->setWindowTitle( tr( "Execute SQL" ) );
QgsGui::enableAutoGeometryRestore( dialog );
QgsQueryResultMainWindow *dialog = new QgsQueryResultMainWindow( conn.release(), identifierName );
dialog->setAttribute( Qt::WA_DeleteOnClose );
dialog->setStyleSheet( QgisApp::instance()->styleSheet() );
QgsQueryResultWidget *widget { new QgsQueryResultWidget( nullptr, conn.release() ) };
widget->setQuery( query );
dialog->setCentralWidget( widget );
dialog->resultWidget()->setQuery( query );
connect( widget, &QgsQueryResultWidget::createSqlVectorLayer, widget, [provider, connectionUri, context]( const QString &, const QString &, const QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions &options ) {
connect( dialog->resultWidget(), &QgsQueryResultWidget::createSqlVectorLayer, dialog, [provider, connectionUri, context]( const QString &, const QString &, const QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions &options ) {
QgsProviderMetadata *md { QgsProviderRegistry::instance()->providerMetadata( provider ) };
if ( !md )
return;

View File

@ -253,20 +253,32 @@ QMenu *QgsAppLayerTreeViewMenuProvider::createContextMenu()
std::unique_ptr<QgsAbstractDatabaseProviderConnection> conn2 { QgsMapLayerUtils::databaseConnection( layer ) };
if ( conn2 )
{
QgsDialog dialog;
dialog.setObjectName( QStringLiteral( "SqlUpdateDialog" ) );
dialog.setWindowTitle( tr( "%1 — Update SQL" ).arg( layer->name() ) );
QgsGui::enableAutoGeometryRestore( &dialog );
QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions options { conn2->sqlOptions( layer->source() ) };
options.layerName = layer->name();
QgsQueryResultWidget *queryResultWidget { new QgsQueryResultWidget( &dialog, conn2.release() ) };
queryResultWidget->setWidgetMode( QgsQueryResultWidget::QueryWidgetMode::QueryLayerUpdateMode );
queryResultWidget->setSqlVectorLayerOptions( options );
queryResultWidget->executeQuery();
queryResultWidget->layout()->setContentsMargins( 0, 0, 0, 0 );
dialog.layout()->addWidget( queryResultWidget );
connect( queryResultWidget, &QgsQueryResultWidget::createSqlVectorLayer, queryResultWidget, [queryResultWidget, layer, this]( const QString &, const QString &, const QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions &options ) {
QgsQueryResultDialog dialog( conn2.release() );
dialog.setObjectName( QStringLiteral( "SqlUpdateDialog" ) );
dialog.setStyleSheet( QgisApp::instance()->styleSheet() );
const QString layerName = layer->name();
dialog.setWindowTitle( tr( "%1 — Update SQL" ).arg( layerName ) );
QgsGui::enableAutoGeometryRestore( &dialog );
dialog.resultWidget()->setWidgetMode( QgsQueryResultWidget::QueryWidgetMode::QueryLayerUpdateMode );
dialog.resultWidget()->setSqlVectorLayerOptions( options );
dialog.resultWidget()->executeQuery();
connect( dialog.resultWidget(), &QgsQueryResultWidget::requestDialogTitleUpdate, &dialog, [&dialog, layerName]( const QString &fileName ) {
if ( fileName.isEmpty() )
{
dialog.setWindowTitle( tr( "%1 — Update SQL" ).arg( layerName ) );
}
else
{
dialog.setWindowTitle( tr( "%1 — %2 — Update SQL" ).arg( fileName, layerName ) );
}
} );
connect( dialog.resultWidget(), &QgsQueryResultWidget::createSqlVectorLayer, &dialog, [&dialog, layer, this]( const QString &, const QString &, const QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions &options ) {
( void ) this;
std::unique_ptr<QgsAbstractDatabaseProviderConnection> conn3 { QgsMapLayerUtils::databaseConnection( layer ) };
if ( conn3 )
@ -277,7 +289,7 @@ QMenu *QgsAppLayerTreeViewMenuProvider::createContextMenu()
if ( sqlLayer->isValid() )
{
layer->setDataSource( sqlLayer->source(), sqlLayer->name(), sqlLayer->dataProvider()->name(), QgsDataProvider::ProviderOptions() );
queryResultWidget->notify( QObject::tr( "Layer Update Success" ), QObject::tr( "The SQL layer was updated successfully" ), Qgis::MessageLevel::Success );
dialog.resultWidget()->notify( QObject::tr( "Layer Update Success" ), QObject::tr( "The SQL layer was updated successfully" ), Qgis::MessageLevel::Success );
}
else
{
@ -286,12 +298,12 @@ QMenu *QgsAppLayerTreeViewMenuProvider::createContextMenu()
{
error = QObject::tr( "layer is not valid, check the log messages for more information" );
}
queryResultWidget->notify( QObject::tr( "Layer Update Error" ), QObject::tr( "Error updating the SQL layer: %1" ).arg( error ), Qgis::MessageLevel::Critical );
dialog.resultWidget()->notify( QObject::tr( "Layer Update Error" ), QObject::tr( "Error updating the SQL layer: %1" ).arg( error ), Qgis::MessageLevel::Critical );
}
}
catch ( QgsProviderConnectionException &ex )
{
queryResultWidget->notify( QObject::tr( "Layer Update Error" ), QObject::tr( "Error updating the SQL layer: %1" ).arg( ex.what() ), Qgis::MessageLevel::Critical );
dialog.resultWidget()->notify( QObject::tr( "Layer Update Error" ), QObject::tr( "Error updating the SQL layer: %1" ).arg( ex.what() ), Qgis::MessageLevel::Critical );
}
}
} );
@ -306,19 +318,28 @@ QMenu *QgsAppLayerTreeViewMenuProvider::createContextMenu()
std::unique_ptr<QgsAbstractDatabaseProviderConnection> conn2 { QgsMapLayerUtils::databaseConnection( layer ) };
if ( conn2 )
{
QgsDialog dialog;
QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions options { conn2->sqlOptions( layer->source() ) };
QgsQueryResultDialog dialog( conn2.release() );
dialog.setObjectName( QStringLiteral( "SqlExecuteDialog" ) );
dialog.setStyleSheet( QgisApp::instance()->styleSheet() );
dialog.setWindowTitle( tr( "Execute SQL" ) );
QgsGui::enableAutoGeometryRestore( &dialog );
QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions options { conn2->sqlOptions( layer->source() ) };
QgsQueryResultWidget *queryResultWidget { new QgsQueryResultWidget( &dialog, conn2.release() ) };
queryResultWidget->setSqlVectorLayerOptions( options );
queryResultWidget->executeQuery();
queryResultWidget->layout()->setContentsMargins( 0, 0, 0, 0 );
dialog.layout()->addWidget( queryResultWidget );
dialog.setStyleSheet( QgisApp::instance()->styleSheet() );
dialog.resultWidget()->setSqlVectorLayerOptions( options );
dialog.resultWidget()->executeQuery();
connect( queryResultWidget, &QgsQueryResultWidget::createSqlVectorLayer, queryResultWidget, [queryResultWidget, layer, this]( const QString &, const QString &, const QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions &options ) {
connect( dialog.resultWidget(), &QgsQueryResultWidget::requestDialogTitleUpdate, &dialog, [&dialog]( const QString &fileName ) {
if ( fileName.isEmpty() )
{
dialog.setWindowTitle( tr( "Execute SQL" ) );
}
else
{
dialog.setWindowTitle( tr( "%1 — Execute SQL" ).arg( fileName ) );
}
} );
connect( dialog.resultWidget(), &QgsQueryResultWidget::createSqlVectorLayer, &dialog, [&dialog, layer, this]( const QString &, const QString &, const QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions &options ) {
( void ) this;
std::unique_ptr<QgsAbstractDatabaseProviderConnection> conn3 { QgsMapLayerUtils::databaseConnection( layer ) };
if ( conn3 )
@ -330,7 +351,7 @@ QMenu *QgsAppLayerTreeViewMenuProvider::createContextMenu()
}
catch ( QgsProviderConnectionException &ex )
{
queryResultWidget->notify( QObject::tr( "New SQL Layer Creation Error" ), QObject::tr( "Error creating the SQL layer: %1" ).arg( ex.what() ), Qgis::MessageLevel::Critical );
dialog.resultWidget()->notify( QObject::tr( "New SQL Layer Creation Error" ), QObject::tr( "Error creating the SQL layer: %1" ).arg( ex.what() ), Qgis::MessageLevel::Critical );
}
}
} );

View File

@ -28,9 +28,16 @@
#include "qgsproviderregistry.h"
#include "qgsprovidermetadata.h"
#include "qgscodeeditorwidget.h"
#include "qgsfileutils.h"
#include <QClipboard>
#include <QShortcut>
#include <QFileDialog>
#include <QMessageBox>
///@cond PRIVATE
const QgsSettingsEntryString *QgsQueryResultWidget::settingLastSourceFolder = new QgsSettingsEntryString( QStringLiteral( "last-source-folder" ), sTreeSqlQueries, QString(), QStringLiteral( "Last used folder for SQL source files" ) );
///@endcond PRIVATE
QgsQueryResultWidget::QgsQueryResultWidget( QWidget *parent, QgsAbstractDatabaseProviderConnection *connection )
: QWidget( parent )
@ -56,6 +63,10 @@ QgsQueryResultWidget::QgsQueryResultWidget( QWidget *parent, QgsAbstractDatabase
vl->addWidget( mCodeEditorWidget );
mSqlEditorContainer->setLayout( vl );
connect( mActionOpenQuery, &QAction::triggered, this, &QgsQueryResultWidget::openQuery );
connect( mActionSaveQuery, &QAction::triggered, this, [this] { saveQuery( false ); } );
connect( mActionSaveQueryAs, &QAction::triggered, this, [this] { saveQuery( true ); } );
connect( mActionCut, &QAction::triggered, mSqlEditor, &QgsCodeEditor::cut );
connect( mActionCopy, &QAction::triggered, mSqlEditor, &QgsCodeEditor::copy );
connect( mActionPaste, &QAction::triggered, mSqlEditor, &QgsCodeEditor::paste );
@ -66,6 +77,7 @@ QgsQueryResultWidget::QgsQueryResultWidget( QWidget *parent, QgsAbstractDatabase
connect( mActionFindReplace, &QAction::toggled, mCodeEditorWidget, &QgsCodeEditorWidget::setSearchBarVisible );
connect( mCodeEditorWidget, &QgsCodeEditorWidget::searchBarToggled, mActionFindReplace, &QAction::setChecked );
connect( mSqlEditor, &QgsCodeEditor::modificationChanged, this, &QgsQueryResultWidget::setHasChanged );
connect( mExecuteButton, &QPushButton::pressed, this, &QgsQueryResultWidget::executeQuery );
@ -139,6 +151,7 @@ QgsQueryResultWidget::QgsQueryResultWidget( QWidget *parent, QgsAbstractDatabase
connect( copySelection, &QShortcut::activated, this, &QgsQueryResultWidget::copySelection );
setConnection( connection );
setHasChanged( false );
}
QgsQueryResultWidget::~QgsQueryResultWidget()
@ -517,9 +530,72 @@ void QgsQueryResultWidget::copyResults( int fromRow, int toRow, int fromColumn,
}
}
void QgsQueryResultWidget::openQuery()
{
if ( !mCodeEditorWidget->filePath().isEmpty() && mHasChangedFileContents )
{
if ( QMessageBox::warning( this, tr( "Unsaved Changes" ), tr( "There are unsaved changes in the query. Continue?" ), QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No, QMessageBox::StandardButton::No ) == QMessageBox::StandardButton::No )
return;
}
QString initialDir = settingLastSourceFolder->value();
if ( initialDir.isEmpty() )
initialDir = QDir::homePath();
const QString fileName = QFileDialog::getOpenFileName( this, tr( "Open Query" ), initialDir, tr( "SQL queries (*.sql *.SQL)" ) + QStringLiteral( ";;" ) + QObject::tr( "All files" ) + QStringLiteral( " (*.*)" ) );
if ( fileName.isEmpty() )
return;
QFileInfo fi( fileName );
settingLastSourceFolder->setValue( fi.path() );
QgsTemporaryCursorOverride cursor( Qt::CursorShape::WaitCursor );
mCodeEditorWidget->loadFile( fileName );
setHasChanged( false );
}
void QgsQueryResultWidget::saveQuery( bool saveAs )
{
if ( mCodeEditorWidget->filePath().isEmpty() || saveAs )
{
QString selectedFilter;
QString initialDir = settingLastSourceFolder->value();
if ( initialDir.isEmpty() )
initialDir = QDir::homePath();
QString newPath = QFileDialog::getSaveFileName(
this,
tr( "Save Query" ),
initialDir,
tr( "SQL queries (*.sql *.SQL)" ) + QStringLiteral( ";;" ) + QObject::tr( "All files" ) + QStringLiteral( " (*.*)" ),
&selectedFilter
);
if ( !newPath.isEmpty() )
{
QFileInfo fi( newPath );
settingLastSourceFolder->setValue( fi.path() );
if ( !selectedFilter.contains( QStringLiteral( "*.*)" ) ) )
newPath = QgsFileUtils::ensureFileNameHasExtension( newPath, { QStringLiteral( "sql" ) } );
mCodeEditorWidget->save( newPath );
setHasChanged( false );
}
}
else if ( !mCodeEditorWidget->filePath().isEmpty() )
{
mCodeEditorWidget->save();
setHasChanged( false );
}
}
QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions QgsQueryResultWidget::sqlVectorLayerOptions() const
{
mSqlVectorLayerOptions.sql = mSqlEditor->text().replace( QRegularExpression( ";\\s*$" ), QString() );
const thread_local QRegularExpression rx( QStringLiteral( ";\\s*$" ) );
mSqlVectorLayerOptions.sql = mSqlEditor->text().replace( rx, QString() );
mSqlVectorLayerOptions.filter = mFilterLineEdit->text();
mSqlVectorLayerOptions.primaryKeyColumns = mPkColumnsComboBox->checkedItems();
mSqlVectorLayerOptions.geometryColumn = mGeometryColumnComboBox->currentText();
@ -586,12 +662,73 @@ void QgsQueryResultWidget::setQuery( const QString &sql )
mActionRedo->setEnabled( false );
}
bool QgsQueryResultWidget::promptUnsavedChanges()
{
if ( !mCodeEditorWidget->filePath().isEmpty() && mHasChangedFileContents )
{
const QMessageBox::StandardButton ret = QMessageBox::question(
this,
tr( "Save Query?" ),
tr(
"There are unsaved changes in this query. Do you want to save those?"
),
QMessageBox::StandardButton::Save
| QMessageBox::StandardButton::Cancel
| QMessageBox::StandardButton::Discard,
QMessageBox::StandardButton::Cancel
);
if ( ret == QMessageBox::StandardButton::Save )
{
saveQuery( false );
return true;
}
else if ( ret == QMessageBox::StandardButton::Discard )
{
return true;
}
else
{
return false;
}
}
else
{
return true;
}
}
void QgsQueryResultWidget::notify( const QString &title, const QString &text, Qgis::MessageLevel level )
{
mMessageBar->pushMessage( title, text, level );
}
void QgsQueryResultWidget::setHasChanged( bool hasChanged )
{
mHasChangedFileContents = hasChanged;
mActionSaveQuery->setEnabled( hasChanged );
updateDialogTitle();
}
void QgsQueryResultWidget::updateDialogTitle()
{
QString fileName;
if ( !mCodeEditorWidget->filePath().isEmpty() )
{
const QFileInfo fi( mCodeEditorWidget->filePath() );
fileName = fi.fileName();
if ( mHasChangedFileContents )
{
fileName.prepend( '*' );
}
}
emit requestDialogTitleUpdate( fileName );
}
///@cond private
void QgsConnectionsApiFetcher::fetchTokens()
@ -732,3 +869,81 @@ QString QgsQueryResultItemDelegate::displayText( const QVariant &value, const QL
}
///@endcond private
//
// QgsQueryResultDialog
//
QgsQueryResultDialog::QgsQueryResultDialog( QgsAbstractDatabaseProviderConnection *connection, QWidget *parent )
: QDialog( parent )
{
setObjectName( QStringLiteral( "QgsQueryResultDialog" ) );
QgsGui::enableAutoGeometryRestore( this );
mWidget = new QgsQueryResultWidget( this, connection );
QVBoxLayout *l = new QVBoxLayout();
l->setContentsMargins( 0, 0, 0, 0 );
l->addWidget( mWidget );
setLayout( l );
}
void QgsQueryResultDialog::closeEvent( QCloseEvent *event )
{
if ( !mWidget->promptUnsavedChanges() )
{
event->ignore();
}
else
{
event->accept();
}
}
//
// QgsQueryResultMainWindow
//
QgsQueryResultMainWindow::QgsQueryResultMainWindow( QgsAbstractDatabaseProviderConnection *connection, const QString &identifierName )
: mIdentifierName( identifierName )
{
setObjectName( QStringLiteral( "SQLCommandsDialog" ) );
QgsGui::enableAutoGeometryRestore( this );
mWidget = new QgsQueryResultWidget( nullptr, connection );
setCentralWidget( mWidget );
connect( mWidget, &QgsQueryResultWidget::requestDialogTitleUpdate, this, &QgsQueryResultMainWindow::updateWindowTitle );
updateWindowTitle( QString() );
}
void QgsQueryResultMainWindow::closeEvent( QCloseEvent *event )
{
if ( !mWidget->promptUnsavedChanges() )
{
event->ignore();
}
else
{
event->accept();
}
}
void QgsQueryResultMainWindow::updateWindowTitle( const QString &fileName )
{
if ( fileName.isEmpty() )
{
if ( !mIdentifierName.isEmpty() )
setWindowTitle( tr( "%1 — Execute SQL" ).arg( mIdentifierName ) );
else
setWindowTitle( tr( "Execute SQL" ) );
}
else
{
if ( !mIdentifierName.isEmpty() )
setWindowTitle( tr( "%1 — %2 — Execute SQL" ).arg( fileName, mIdentifierName ) );
else
setWindowTitle( tr( "%1 — Execute SQL" ).arg( fileName ) );
}
}

View File

@ -27,6 +27,8 @@
#include <QThread>
#include <QtConcurrent>
#include <QStyledItemDelegate>
#include <QDialog>
#include <QMainWindow>
class QgsCodeEditorWidget;
@ -108,6 +110,13 @@ class GUI_EXPORT QgsQueryResultWidget : public QWidget, private Ui::QgsQueryResu
Q_OBJECT
public:
#ifndef SIP_RUN
///@cond PRIVATE
static inline QgsSettingsTreeNode *sTreeSqlQueries = QgsSettingsTree::sTreeGui->createChildNode( QStringLiteral( "sql-queries" ) );
static const QgsSettingsEntryString *settingLastSourceFolder;
///@endcond PRIVATE
#endif
/**
* \brief The QueryWidgetMode enum represents various modes for the widget appearance.
*/
@ -145,6 +154,8 @@ class GUI_EXPORT QgsQueryResultWidget : public QWidget, private Ui::QgsQueryResu
*/
void setQuery( const QString &sql );
SIP_SKIP bool promptUnsavedChanges();
public slots:
/**
@ -198,6 +209,8 @@ class GUI_EXPORT QgsQueryResultWidget : public QWidget, private Ui::QgsQueryResu
*/
void firstResultBatchFetched();
SIP_SKIP void requestDialogTitleUpdate( const QString &filename );
private slots:
/**
@ -206,8 +219,10 @@ class GUI_EXPORT QgsQueryResultWidget : public QWidget, private Ui::QgsQueryResu
void updateButtons();
void showCellContextMenu( QPoint point );
void copySelection();
void openQuery();
void saveQuery( bool saveAs );
void setHasChanged( bool hasChanged );
private:
QgsCodeEditorWidget *mCodeEditorWidget = nullptr;
@ -229,6 +244,8 @@ class GUI_EXPORT QgsQueryResultWidget : public QWidget, private Ui::QgsQueryResu
QueryWidgetMode mQueryWidgetMode = QueryWidgetMode::SqlQueryMode;
long long mCurrentHistoryEntryId = -1;
bool mHasChangedFileContents = false;
/**
* Updates SQL layer columns.
*/
@ -254,8 +271,81 @@ class GUI_EXPORT QgsQueryResultWidget : public QWidget, private Ui::QgsQueryResu
*/
QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions sqlVectorLayerOptions() const;
void updateDialogTitle();
friend class TestQgsQueryResultWidget;
};
/**
* \ingroup gui
* \brief A dialog which allows users to enter and run an SQL query on a
* DB connection (an instance of QgsAbstractDatabaseProviderConnection).
*
* \note the ownership of the connection is transferred to the dialog.
*
* \see QgsQueryResultWidget
*
* \since QGIS 3.44
*/
class GUI_EXPORT QgsQueryResultDialog : public QDialog
{
Q_OBJECT
public:
/**
* Constructor for QgsQueryResultDialog.
*
* Ownership of the \a connection is transferred to the dialog.
*/
QgsQueryResultDialog( QgsAbstractDatabaseProviderConnection *connection SIP_TRANSFER = nullptr, QWidget *parent = nullptr );
/**
* Returns the QgsQueryResultWidget shown in the dialog.
*/
QgsQueryResultWidget *resultWidget() { return mWidget; }
void closeEvent( QCloseEvent *event ) override;
private:
QgsQueryResultWidget *mWidget = nullptr;
};
/**
* \ingroup gui
* \brief A main window which allows users to enter and run an SQL query on a
* DB connection (an instance of QgsAbstractDatabaseProviderConnection).
*
* \note the ownership of the connection is transferred to the window.
*
* \see QgsQueryResultWidget
*
* \since QGIS 3.44
*/
class GUI_EXPORT QgsQueryResultMainWindow : public QMainWindow
{
Q_OBJECT
public:
/**
* Constructor for QgsQueryResultMainWindow.
*
* Ownership of the \a connection is transferred to the window.
*/
QgsQueryResultMainWindow( QgsAbstractDatabaseProviderConnection *connection SIP_TRANSFER = nullptr, const QString &identifierName = QString() );
/**
* Returns the QgsQueryResultWidget shown in the window.
*/
QgsQueryResultWidget *resultWidget() { return mWidget; }
void closeEvent( QCloseEvent *event ) override;
private:
QgsQueryResultWidget *mWidget = nullptr;
QString mIdentifierName;
void updateWindowTitle( const QString &fileName );
};
#endif // QGSQUERYRESULTWIDGET_H

View File

@ -39,6 +39,10 @@
</property>
<item>
<widget class="QToolBar" name="mToolBar">
<addaction name="mActionOpenQuery"/>
<addaction name="mActionSaveQuery"/>
<addaction name="mActionSaveQueryAs"/>
<addaction name="separator"/>
<addaction name="mActionCut"/>
<addaction name="mActionCopy"/>
<addaction name="mActionPaste"/>
@ -350,6 +354,51 @@
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="mActionOpenQuery">
<property name="icon">
<iconset resource="../../images/images.qrc">
<normaloff>:/images/themes/default/mActionFileOpen.svg</normaloff>:/images/themes/default/mActionFileOpen.svg</iconset>
</property>
<property name="text">
<string>Open Query…</string>
</property>
<property name="toolTip">
<string>Open Query</string>
</property>
<property name="shortcut">
<string>Ctrl+O</string>
</property>
</action>
<action name="mActionSaveQuery">
<property name="icon">
<iconset resource="../../images/images.qrc">
<normaloff>:/images/themes/default/mActionFileSave.svg</normaloff>:/images/themes/default/mActionFileSave.svg</iconset>
</property>
<property name="text">
<string>Save Query…</string>
</property>
<property name="toolTip">
<string>Save Query</string>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
</property>
</action>
<action name="mActionSaveQueryAs">
<property name="icon">
<iconset resource="../../images/images.qrc">
<normaloff>:/images/themes/default/mActionFileSaveAs.svg</normaloff>:/images/themes/default/mActionFileSaveAs.svg</iconset>
</property>
<property name="text">
<string>Save Query as…</string>
</property>
<property name="toolTip">
<string>Save Query as</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+S</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>