diff --git a/python/PyQt6/gui/auto_generated/qgsqueryresultwidget.sip.in b/python/PyQt6/gui/auto_generated/qgsqueryresultwidget.sip.in index cb0f981d3538..75f26653a8dc 100644 --- a/python/PyQt6/gui/auto_generated/qgsqueryresultwidget.sip.in +++ b/python/PyQt6/gui/auto_generated/qgsqueryresultwidget.sip.in @@ -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 ); + + }; /************************************************************************ diff --git a/python/gui/auto_generated/qgsqueryresultwidget.sip.in b/python/gui/auto_generated/qgsqueryresultwidget.sip.in index 8f5f73fca333..1a54a1bdbd4e 100644 --- a/python/gui/auto_generated/qgsqueryresultwidget.sip.in +++ b/python/gui/auto_generated/qgsqueryresultwidget.sip.in @@ -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 ); + + }; /************************************************************************ diff --git a/src/app/browser/qgsinbuiltdataitemproviders.cpp b/src/app/browser/qgsinbuiltdataitemproviders.cpp index 724fdfb39757..7bcf14c417b8 100644 --- a/src/app/browser/qgsinbuiltdataitemproviders.cpp +++ b/src/app/browser/qgsinbuiltdataitemproviders.cpp @@ -2010,25 +2010,13 @@ void QgsDatabaseItemGuiProvider::openSqlDialog( const QString &connectionUri, co std::unique_ptr conn( qgis::down_cast( 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; diff --git a/src/app/qgsapplayertreeviewmenuprovider.cpp b/src/app/qgsapplayertreeviewmenuprovider.cpp index 96ae2c52b97d..4f1a40c83fdf 100644 --- a/src/app/qgsapplayertreeviewmenuprovider.cpp +++ b/src/app/qgsapplayertreeviewmenuprovider.cpp @@ -253,20 +253,32 @@ QMenu *QgsAppLayerTreeViewMenuProvider::createContextMenu() std::unique_ptr 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 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 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( 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( queryResultWidget, &QgsQueryResultWidget::createSqlVectorLayer, queryResultWidget, [queryResultWidget, layer, this]( const QString &, const QString &, const QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions &options ) { + connect( dialog.resultWidget(), &QgsQueryResultWidget::createSqlVectorLayer, &dialog, [&dialog, layer, this]( const QString &, const QString &, const QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions &options ) { ( void ) this; std::unique_ptr 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 ); } } } ); diff --git a/src/gui/qgsqueryresultwidget.cpp b/src/gui/qgsqueryresultwidget.cpp index 087cb6e3ca23..561674569449 100644 --- a/src/gui/qgsqueryresultwidget.cpp +++ b/src/gui/qgsqueryresultwidget.cpp @@ -28,9 +28,16 @@ #include "qgsproviderregistry.h" #include "qgsprovidermetadata.h" #include "qgscodeeditorwidget.h" +#include "qgsfileutils.h" #include #include +#include +#include + +///@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 ) ); + } +} diff --git a/src/gui/qgsqueryresultwidget.h b/src/gui/qgsqueryresultwidget.h index f5fbb9564062..aeefb21a76a9 100644 --- a/src/gui/qgsqueryresultwidget.h +++ b/src/gui/qgsqueryresultwidget.h @@ -27,6 +27,8 @@ #include #include #include +#include +#include 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 diff --git a/src/ui/qgsqueryresultwidgetbase.ui b/src/ui/qgsqueryresultwidgetbase.ui index 854f97191659..b2861583d421 100644 --- a/src/ui/qgsqueryresultwidgetbase.ui +++ b/src/ui/qgsqueryresultwidgetbase.ui @@ -39,6 +39,10 @@ + + + + @@ -350,6 +354,51 @@ QAction::NoRole + + + + :/images/themes/default/mActionFileOpen.svg:/images/themes/default/mActionFileOpen.svg + + + Open Query… + + + Open Query + + + Ctrl+O + + + + + + :/images/themes/default/mActionFileSave.svg:/images/themes/default/mActionFileSave.svg + + + Save Query… + + + Save Query + + + Ctrl+S + + + + + + :/images/themes/default/mActionFileSaveAs.svg:/images/themes/default/mActionFileSaveAs.svg + + + Save Query as… + + + Save Query as + + + Ctrl+Shift+S + +