Show UnsavedDocumentsDialog when closing window or quitting application

so user can save and or discard docs more efficiently.
This commit is contained in:
eelke 2026-10-19 18:16:25 +02:00
parent b97adced4c
commit 784ad68758
16 changed files with 544 additions and 164 deletions

View file

@ -34,6 +34,9 @@ set(PGLAB_SOURCES
querytool/QueryExplainModel.cpp querytool/QueryExplainModel.cpp
querytool/QueryParamListController.cpp querytool/QueryParamListController.cpp
querytool/QueryTool.cpp querytool/QueryTool.cpp
querytool/SqlDocument.cpp
querytool/SqlDocumentRegistry.cpp
querytool/UnsavedDocumentsDialog.cpp
serverinspector/DatabasesPage.cpp serverinspector/DatabasesPage.cpp
serverinspector/RolesPage.cpp serverinspector/RolesPage.cpp
serverinspector/ServerInspector.cpp serverinspector/ServerInspector.cpp

View file

@ -62,7 +62,7 @@ void ConnectionManagerWindow::on_actionQuit_application_triggered()
{ {
auto res = QMessageBox::question(this, "pglab", auto res = QMessageBox::question(this, "pglab",
tr("Close all windows?"), QMessageBox::Yes, QMessageBox::No); tr("Close all windows?"), QMessageBox::Yes, QMessageBox::No);
if (res == QMessageBox::Yes) { if (res == QMessageBox::Yes && m_masterController->confirmQuit(this)) {
QApplication::quit(); QApplication::quit();
} }
} }

View file

@ -20,10 +20,13 @@
#include <QStandardPaths> #include <QStandardPaths>
#include <QStatusBar> #include <QStatusBar>
#include <QTableView> #include <QTableView>
#include <QApplication>
#include <QInputDialog>
#include "EditTableWidget.h" #include "EditTableWidget.h"
#include "CodeGenerator.h" #include "CodeGenerator.h"
#include "querytool/QueryTool.h" #include "querytool/QueryTool.h"
#include "querytool/UnsavedDocumentsDialog.h"
#include <serverinspector/ServerInspector.h> #include <serverinspector/ServerInspector.h>
namespace pg = Pgsql; namespace pg = Pgsql;
@ -91,12 +94,18 @@ void DatabaseWindow::closeEvent(QCloseEvent *event)
{ {
for (int idx = 0; idx < ui->mainTabs->count(); ++idx) for (int idx = 0; idx < ui->mainTabs->count(); ++idx)
{ {
if (!canCloseTab(idx)) auto mp = dynamic_cast<ManagedPage*>(ui->mainTabs->widget(idx));
if (mp && !dynamic_cast<QueryTool*>(mp) && !mp->CanClose(true))
{ {
event->ignore(); event->ignore();
return; return;
} }
} }
auto docs = documentsClosedWith(m_masterController->sqlDocumentRegistry().modifiedDocuments(),
[this](const QWidget *editor) { return editor->window() == this; });
if (!confirmClose(this, docs))
event->ignore();
} }
void DatabaseWindow::setConfig(const ConnectionConfig &config) void DatabaseWindow::setConfig(const ConnectionConfig &config)
@ -199,14 +208,14 @@ void DatabaseWindow::openSqlFile(QString file_name)
{ {
if (!file_name.isEmpty()) if (!file_name.isEmpty())
{ {
auto *ct = new QueryTool(this, this); auto document = std::make_shared<SqlDocument>();
if (ct->load(file_name)) if (document->load(file_name))
{ {
m_masterController->sqlDocumentRegistry().add(document);
auto *ct = new QueryTool(this, document, this);
ct->addAction(ui->actionExecute_query); ct->addAction(ui->actionExecute_query);
addPage(ct, ct->title()); addPage(ct, ct->title());
} }
else
delete ct;
} }
} }
@ -391,13 +400,53 @@ void DatabaseWindow::on_actionInspect_server_triggered()
newServerInspectorPage(); newServerInspectorPage();
} }
void DatabaseWindow::openSharedSqlDocument(std::shared_ptr<SqlDocument> document)
{
auto *ct = new QueryTool(this, std::move(document), this);
ct->addAction(ui->actionExecute_query);
addPage(ct, ct->title());
ct->updateTitle();
}
void DatabaseWindow::on_actionNew_Query_triggered() void DatabaseWindow::on_actionNew_Query_triggered()
{ {
auto *ct = new QueryTool(this, this); auto document = std::make_shared<SqlDocument>();
m_masterController->sqlDocumentRegistry().add(document);
auto *ct = new QueryTool(this, document, this);
ct->addAction(ui->actionExecute_query); ct->addAction(ui->actionExecute_query);
addPage(ct, "new"); addPage(ct, "new");
ct->newdoc(); }
void DatabaseWindow::on_actionOpen_in_other_window_triggered()
{
auto ct = dynamic_cast<QueryTool*>(ui->mainTabs->currentWidget());
if (!ct)
return;
QList<DatabaseWindow*> others;
for (auto *widget : QApplication::topLevelWidgets()) {
auto dw = qobject_cast<DatabaseWindow*>(widget);
if (dw && dw != this && dw->isVisible())
others.append(dw);
}
if (others.isEmpty()) {
QMessageBox::information(this, "pglab", tr("No other window is open."));
return;
}
QStringList names;
for (int i = 0; i < others.size(); ++i)
names.append(QString("%1 (%2)").arg(others[i]->windowTitle()).arg(i + 1));
bool ok = false;
QString selected = QInputDialog::getItem(this, tr("Open in other window"),
tr("Select window:"), names, 0, false, &ok);
if (!ok)
return;
auto target = others[names.indexOf(selected)];
target->openSharedSqlDocument(ct->document());
} }
void DatabaseWindow::on_actionOpen_Query_triggered() void DatabaseWindow::on_actionOpen_Query_triggered()

View file

@ -6,6 +6,7 @@
#include "OpenDatabase.h" #include "OpenDatabase.h"
#include "Pgsql_Connection.h" #include "Pgsql_Connection.h"
#include "IDatabaseWindow.h" #include "IDatabaseWindow.h"
#include "querytool/SqlDocument.h"
#include <QMainWindow> #include <QMainWindow>
#include <memory> #include <memory>
@ -38,6 +39,7 @@ public:
~DatabaseWindow(); ~DatabaseWindow();
void setConfig(const ConnectionConfig &config); void setConfig(const ConnectionConfig &config);
void openSharedSqlDocument(std::shared_ptr<SqlDocument> document);
std::shared_ptr<OpenDatabase> getDatabase() { return m_database; } std::shared_ptr<OpenDatabase> getDatabase() { return m_database; }
@ -98,6 +100,7 @@ private slots:
void on_actionInspect_server_triggered(); void on_actionInspect_server_triggered();
void on_actionNew_Query_triggered(); void on_actionNew_Query_triggered();
void on_actionOpen_Query_triggered(); void on_actionOpen_Query_triggered();
void on_actionOpen_in_other_window_triggered();
void on_actionSave_Query_triggered(); void on_actionSave_Query_triggered();
void on_actionPaste_lang_string_triggered(); void on_actionPaste_lang_string_triggered();
void on_actionRefreshCatalog_triggered(); void on_actionRefreshCatalog_triggered();

View file

@ -43,6 +43,7 @@
<addaction name="actionSave_Query"/> <addaction name="actionSave_Query"/>
<addaction name="actionSave_query_as"/> <addaction name="actionSave_query_as"/>
<addaction name="actionSave_copy_of_query_as"/> <addaction name="actionSave_copy_of_query_as"/>
<addaction name="actionOpen_in_other_window"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionSave_connection"/> <addaction name="actionSave_connection"/>
<addaction name="actionExport_data"/> <addaction name="actionExport_data"/>
@ -106,6 +107,11 @@
<addaction name="menuWindow"/> <addaction name="menuWindow"/>
<addaction name="menuHelp"/> <addaction name="menuHelp"/>
</widget> </widget>
<action name="actionOpen_in_other_window">
<property name="text">
<string>Open in other window / Duplicate in window...</string>
</property>
</action>
<action name="actionAbout"> <action name="actionAbout">
<property name="icon"> <property name="icon">
<iconset resource="resources.qrc"> <iconset resource="resources.qrc">

View file

@ -1,5 +1,6 @@
#include "MasterController.h" #include "MasterController.h"
#include "ConnectionController.h" #include "ConnectionController.h"
#include "querytool/UnsavedDocumentsDialog.h"
#include "utils/PostgresqlUrlParser.h" #include "utils/PostgresqlUrlParser.h"
#include <ConnectionConfig.h> #include <ConnectionConfig.h>
#include <QCoreApplication> #include <QCoreApplication>
@ -59,4 +60,9 @@ SQLiteConnection& MasterController::userConfigDatabase()
return m_userConfigDatabase; return m_userConfigDatabase;
} }
bool MasterController::confirmQuit(QWidget *parent)
{
return confirmClose(parent, m_sqlDocumentRegistry.modifiedDocuments());
}

View file

@ -7,6 +7,7 @@
#include <map> #include <map>
#include <memory> #include <memory>
#include "sqlite/SQLiteConnection.h" #include "sqlite/SQLiteConnection.h"
#include "querytool/SqlDocumentRegistry.h"
class ConnectionController; class ConnectionController;
@ -25,12 +26,17 @@ public:
ConnectionController* connectionController(); ConnectionController* connectionController();
SQLiteConnection& userConfigDatabase(); SQLiteConnection& userConfigDatabase();
SqlDocumentRegistry& sqlDocumentRegistry() { return m_sqlDocumentRegistry; }
/// Asks once about all modified documents of all windows. Returns false when quitting must be aborted.
bool confirmQuit(QWidget *parent);
signals: signals:
public slots: public slots:
private: private:
SQLiteConnection m_userConfigDatabase; SQLiteConnection m_userConfigDatabase;
SqlDocumentRegistry m_sqlDocumentRegistry;
ConnectionController* m_connectionController = nullptr; ConnectionController* m_connectionController = nullptr;
}; };

View file

@ -21,12 +21,16 @@
#include "util.h" #include "util.h"
#include "UserConfiguration.h" #include "UserConfiguration.h"
#include "IDatabaseWindow.h" #include "IDatabaseWindow.h"
#include "UnsavedDocumentsDialog.h"
#include <QPlainTextDocumentLayout>
#include <QTextDocument>
QueryTool::QueryTool(IDatabaseWindow *context, QWidget *parent) QueryTool::QueryTool(IDatabaseWindow *context, std::shared_ptr<SqlDocument> document, QWidget *parent)
: ManagedPage(parent) : ManagedPage(parent)
, m_context(context) , m_context(context)
, ui(new Ui::QueryTab) , ui(new Ui::QueryTab)
, m_document(std::move(document))
, m_dbConnection() , m_dbConnection()
{ {
ui->setupUi(this); ui->setupUi(this);
@ -40,13 +44,15 @@ QueryTool::QueryTool(IDatabaseWindow *context, QWidget *parent)
ui->queryEdit->setFont(UserConfiguration::instance()->codeFont()); ui->queryEdit->setFont(UserConfiguration::instance()->codeFont());
highlighter = new SqlSyntaxHighlighter(ui->queryEdit->document()); ui->queryEdit->setDocument(m_document->textDocument());
m_document->addEditor(this);
auto types = m_catalog->types(); auto types = m_catalog->types();
if (types) { if (types) {
highlighter->setTypes(*types); m_document->highlighter()->setTypes(*types);
} }
connect(ui->queryEdit, &QPlainTextEdit::textChanged, this, &QueryTool::queryTextChanged); connect(m_document.get(), &SqlDocument::modifiedChanged, this, &QueryTool::updateTitle);
connect(m_document.get(), &SqlDocument::fileNameChanged, this, &QueryTool::updateTitle);
ui->queryEdit->setAcceptDrops(false); ui->queryEdit->setAcceptDrops(false);
m_queryParamListController = new QueryParamListController(ui->paramTableView, m_context->openDatabase(), this); m_queryParamListController = new QueryParamListController(ui->paramTableView, m_context->openDatabase(), this);
@ -60,87 +66,42 @@ QueryTool::QueryTool(IDatabaseWindow *context, QWidget *parent)
QueryTool::~QueryTool() QueryTool::~QueryTool()
{ {
// Detach the editor from the shared document, it may be destroyed together with this tool
auto detached = new QTextDocument(ui->queryEdit);
detached->setDocumentLayout(new QPlainTextDocumentLayout(detached));
ui->queryEdit->setDocument(detached);
m_document->removeEditor(this);
delete ui; delete ui;
} }
bool QueryTool::CanClose(bool prompt_user) bool QueryTool::CanClose(bool prompt_user)
{ {
bool can_close; if (!m_document->isModified())
if (m_queryTextChanged) { return true;
if (prompt_user)
can_close = continueWithoutSavingWarning();
else
can_close = false;
}
else {
can_close = true;
}
return can_close;
}
void QueryTool::newdoc() // The document stays open in another editor, so it is not closed with this tool
{ if (m_document->editors().size() > 1)
ui->queryEdit->clear(); return true;
setFileName(tr("new"));
setQueryTextChanged(false);
m_new = true;
}
bool QueryTool::load(const QString &filename) if (!prompt_user)
{ return false;
bool result = false;
QFile file(filename);
if (file.open(QIODevice::ReadOnly)) {
QByteArray ba = file.readAll();
auto toUtf16 = QStringDecoder(QStringDecoder::Utf8);
QString text = toUtf16(ba);
if (toUtf16.hasError()) {
file.reset();
QTextStream stream(&file);
text = stream.readAll();
}
ui->queryEdit->setPlainText(text); return confirmClose(this, {m_document});
setQueryTextChanged(false);
setFileName(filename);
m_new = false;
result = true;
}
return result;
} }
bool QueryTool::save() bool QueryTool::save()
{ {
bool result; return m_document->save(this);
if (m_fileName.isEmpty() || m_new) {
result = saveAs();
}
else {
result = saveSqlTo(m_fileName);
}
return result;
} }
bool QueryTool::saveAs() bool QueryTool::saveAs()
{ {
bool result = false; return m_document->saveAs(this);
QString filename = promptUserForSaveSqlFilename();
if (!filename.isEmpty()) {
result = saveSqlTo(filename);
if (result) {
setFileName(filename);
m_new = false;
}
}
return result;
} }
void QueryTool::saveCopyAs() void QueryTool::saveCopyAs()
{ {
QString filename = promptUserForSaveSqlFilename(); m_document->saveCopyAs(this);
if (!filename.isEmpty()) {
saveSqlTo(filename);
}
} }
void QueryTool::execute() void QueryTool::execute()
@ -245,81 +206,21 @@ void QueryTool::cancel()
QString QueryTool::title() const QString QueryTool::title() const
{ {
QFileInfo fileInfo(m_fileName); QFileInfo fileInfo(m_document->fileName());
QString fn(fileInfo.fileName()); QString fn(fileInfo.fileName());
return fn; return fn;
} }
void QueryTool::setFileName(const QString &filename)
{
m_fileName = filename;
updateTitle();
}
void QueryTool::updateTitle() void QueryTool::updateTitle()
{ {
QFileInfo fileInfo(m_fileName); QFileInfo fileInfo(m_document->fileName());
QString title = fileInfo.fileName(); QString title = fileInfo.fileName();
if (m_queryTextChanged) { if (m_document->isModified()) {
title = "*" + title; title = "*" + title;
} }
m_context->setTitleForWidget(this, title, m_fileName); m_context->setTitleForWidget(this, title, m_document->fileName());
}
bool QueryTool::continueWithoutSavingWarning()
{
QMessageBox msgBox;
msgBox.setIcon(QMessageBox::Warning);
msgBox.setText(QString("Save changes in document \"%1\" before closing?").arg(m_fileName));
msgBox.setInformativeText("The changes will be lost when you choose Discard.");
msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
msgBox.setDefaultButton(QMessageBox::Cancel);
int ret = msgBox.exec();
if (ret == QMessageBox::Save) {
if (!save()) {
// save failed or was a saveAs and was cancelled, don't close!
ret = QMessageBox::Cancel;
}
}
return ret != QMessageBox::Cancel;
}
bool QueryTool::saveSqlTo(const QString &filename)
{
bool result = false;
QFileInfo fileinfo(filename);
QFile file(filename);
if (file.open(QIODevice::WriteOnly)) {
QTextStream stream(&file);
//stream.setCodec("utf-8");
stream.setEncoding(QStringConverter::Utf8);
QString text = ui->queryEdit->toPlainText();
stream << text;
stream.flush();
if (stream.status() == QTextStream::Ok) {
setQueryTextChanged(false);
result = true;
}
}
return result;
}
QString QueryTool::promptUserForSaveSqlFilename()
{
QString home_dir = QStandardPaths::locate(QStandardPaths::HomeLocation, "", QStandardPaths::LocateDirectory);
return QFileDialog::getSaveFileName(this, tr("Save query"), home_dir, tr("SQL file (*.sql)"));
}
void QueryTool::queryTextChanged()
{
setQueryTextChanged(true);
} }
void QueryTool::connectionStateChanged() void QueryTool::connectionStateChanged()
@ -618,10 +519,3 @@ void QueryTool::exportData()
exportDataToFilename(file_name); exportDataToFilename(file_name);
} }
void QueryTool::setQueryTextChanged(bool queryTextChanged)
{
if (m_queryTextChanged != queryTextChanged) {
m_queryTextChanged = queryTextChanged;
updateTitle();
}
}

View file

@ -6,6 +6,7 @@
#include "QueryExplainModel.h" #include "QueryExplainModel.h"
#include "stopwatch.h" #include "stopwatch.h"
#include "tuplesresultwidget.h" #include "tuplesresultwidget.h"
#include "SqlDocument.h"
#include <QWidget> #include <QWidget>
#include <memory> #include <memory>
@ -34,12 +35,9 @@ class PgDatabaseCatalog;
class QueryTool : public ManagedPage { class QueryTool : public ManagedPage {
Q_OBJECT Q_OBJECT
public: public:
QueryTool(IDatabaseWindow *context, QWidget *parent = nullptr); QueryTool(IDatabaseWindow *context, std::shared_ptr<SqlDocument> document, QWidget *parent = nullptr);
~QueryTool() override; ~QueryTool() override;
void newdoc();
bool load(const QString &filename);
void explain(bool analyze); void explain(bool analyze);
bool CanClose(bool prompt_user); bool CanClose(bool prompt_user);
@ -47,12 +45,12 @@ public:
void generateCode(); void generateCode();
void exportDataToFilename(const QString &filename); void exportDataToFilename(const QString &filename);
QString fileName() const { return m_fileName; } QString fileName() const { return m_document->fileName(); }
bool isChanged() const { return m_queryTextChanged; } std::shared_ptr<SqlDocument> document() const { return m_document; }
bool isNew() const { return m_new; }
void focusEditor(); void focusEditor();
QString title() const; QString title() const;
void updateTitle();
public slots: public slots:
@ -74,15 +72,12 @@ private:
using ResultTabContainer = std::vector<TuplesResultWidget*>; using ResultTabContainer = std::vector<TuplesResultWidget*>;
IDatabaseWindow *m_context; IDatabaseWindow *m_context;
Ui::QueryTab *ui; Ui::QueryTab *ui;
SqlSyntaxHighlighter* highlighter; std::shared_ptr<SqlDocument> m_document;
ConnectionConfig m_config; ConnectionConfig m_config;
StopWatch m_stopwatch; StopWatch m_stopwatch;
QueryParamListController *m_queryParamListController = nullptr; QueryParamListController *m_queryParamListController = nullptr;
bool m_new = true;
QString m_fileName; ///< use setFileName function to set
bool m_queryTextChanged = false;
std::shared_ptr<PgDatabaseCatalog> m_catalog; std::shared_ptr<PgDatabaseCatalog> m_catalog;
ASyncDBConnection m_dbConnection; ASyncDBConnection m_dbConnection;
@ -90,11 +85,6 @@ private:
std::unique_ptr<QueryExplainModel> explainModel; std::unique_ptr<QueryExplainModel> explainModel;
ResultTabContainer resultList; ResultTabContainer resultList;
void setFileName(const QString &filename);
bool continueWithoutSavingWarning();
bool saveSqlTo(const QString &filename);
QString promptUserForSaveSqlFilename();
void addLog(QString s); void addLog(QString s);
QString getCommand() const; QString getCommand() const;
@ -102,15 +92,12 @@ private:
void clearResult(); void clearResult();
void markError(const Pgsql::ErrorDetails &details); void markError(const Pgsql::ErrorDetails &details);
void updateTitle();
void setQueryTextChanged(bool queryTextChanged);
private slots: private slots:
void explain_ready(ExplainRoot::SPtr explain); void explain_ready(ExplainRoot::SPtr explain);
void query_ready(std::shared_ptr<Pgsql::Result>, qint64 elapsedms); void query_ready(std::shared_ptr<Pgsql::Result>, qint64 elapsedms);
void queryTextChanged();
void connectionStateChanged(); void connectionStateChanged();
void receiveNotice(Pgsql::ErrorDetails notice); void receiveNotice(Pgsql::ErrorDetails notice);

View file

@ -0,0 +1,143 @@
#include "SqlDocument.h"
#include "util/SqlSyntaxHighlighter.h"
#include <QFile>
#include <QFileDialog>
#include <QFileInfo>
#include <QStandardPaths>
#include <QStringConverter>
#include <QPlainTextDocumentLayout>
#include <QTextDocument>
#include <QTextStream>
SqlDocument::SqlDocument(QObject *parent)
: QObject(parent)
, m_document(new QTextDocument(this))
, m_highlighter(new SqlSyntaxHighlighter(m_document))
{
// QPlainTextEdit only accepts documents that use a QPlainTextDocumentLayout
m_document->setDocumentLayout(new QPlainTextDocumentLayout(m_document));
connect(m_document, &QTextDocument::modificationChanged, this, &SqlDocument::modifiedChanged);
setFileName(tr("new"));
}
SqlDocument::~SqlDocument() = default;
bool SqlDocument::isModified() const
{
return m_document->isModified();
}
void SqlDocument::setModified(bool modified)
{
m_document->setModified(modified);
}
void SqlDocument::clear()
{
m_document->clear();
setFileName(tr("new"));
m_new = true;
setModified(false);
}
bool SqlDocument::load(const QString &filename)
{
bool result = false;
QFile file(filename);
if (file.open(QIODevice::ReadOnly)) {
QByteArray ba = file.readAll();
auto toUtf16 = QStringDecoder(QStringDecoder::Utf8);
QString text = toUtf16(ba);
if (toUtf16.hasError()) {
file.reset();
QTextStream stream(&file);
text = stream.readAll();
}
m_document->setPlainText(text);
setModified(false);
setFileName(filename);
m_new = false;
result = true;
}
return result;
}
bool SqlDocument::save(QWidget *parent)
{
bool result;
if (m_fileName.isEmpty() || m_new) {
result = saveAs(parent);
}
else {
result = saveSqlTo(m_fileName);
}
return result;
}
bool SqlDocument::saveAs(QWidget *parent)
{
bool result = false;
QString filename = promptUserForSaveSqlFilename(parent);
if (!filename.isEmpty()) {
result = saveSqlTo(filename);
if (result) {
setFileName(filename);
m_new = false;
}
}
return result;
}
bool SqlDocument::saveCopyAs(QWidget *parent)
{
bool result = false;
QString filename = promptUserForSaveSqlFilename(parent);
if (!filename.isEmpty()) {
result = saveSqlTo(filename);
}
return result;
}
void SqlDocument::addEditor(QWidget *editor)
{
if (!m_editors.contains(editor))
m_editors.append(editor);
}
void SqlDocument::removeEditor(QWidget *editor)
{
m_editors.removeAll(editor);
}
void SqlDocument::setFileName(const QString &filename)
{
m_fileName = filename;
emit fileNameChanged();
}
bool SqlDocument::saveSqlTo(const QString &filename)
{
bool result = false;
QFile file(filename);
if (file.open(QIODevice::WriteOnly)) {
QTextStream stream(&file);
stream.setEncoding(QStringConverter::Utf8);
QString text = m_document->toPlainText();
stream << text;
stream.flush();
if (stream.status() == QTextStream::Ok) {
setModified(false);
result = true;
}
}
return result;
}
QString SqlDocument::promptUserForSaveSqlFilename(QWidget *parent)
{
QString home_dir = QStandardPaths::locate(QStandardPaths::HomeLocation, "", QStandardPaths::LocateDirectory);
return QFileDialog::getSaveFileName(parent, tr("Save query"), home_dir, tr("SQL file (*.sql)"));
}

View file

@ -0,0 +1,60 @@
#ifndef SQLDOCUMENT_H
#define SQLDOCUMENT_H
#include <QList>
#include <QObject>
#include <QString>
class QTextDocument;
class QWidget;
class SqlSyntaxHighlighter;
/** Owns the text of a SQL script together with its file state.
* Several query tools (editors) can show the same document, they all
* use the same QTextDocument.
*/
class SqlDocument : public QObject {
Q_OBJECT
public:
explicit SqlDocument(QObject *parent = nullptr);
~SqlDocument() override;
QTextDocument *textDocument() const { return m_document; }
SqlSyntaxHighlighter *highlighter() const { return m_highlighter; }
QString fileName() const { return m_fileName; }
bool isNew() const { return m_new; }
bool isModified() const;
void setModified(bool modified);
/// Empties the document and makes it a new unnamed document.
void clear();
bool load(const QString &filename);
/// Save the document under its current name, a file save dialog will be shown if this is a new document
bool save(QWidget *parent);
/// Saves the document under a new name and continue editing the document under this new name.
bool saveAs(QWidget *parent);
/// Save the document under a new name but continue editing the document under its old name.
bool saveCopyAs(QWidget *parent);
void addEditor(QWidget *editor);
void removeEditor(QWidget *editor);
const QList<QWidget*> &editors() const { return m_editors; }
signals:
void modifiedChanged(bool modified);
void fileNameChanged();
private:
QTextDocument *m_document;
SqlSyntaxHighlighter *m_highlighter;
QString m_fileName;
bool m_new = true;
QList<QWidget*> m_editors;
void setFileName(const QString &filename);
bool saveSqlTo(const QString &filename);
QString promptUserForSaveSqlFilename(QWidget *parent);
};
#endif // SQLDOCUMENT_H

View file

@ -0,0 +1,27 @@
#include "SqlDocumentRegistry.h"
#include "SqlDocument.h"
void SqlDocumentRegistry::add(const std::shared_ptr<SqlDocument> &document)
{
m_documents.push_back(document);
}
QList<std::shared_ptr<SqlDocument>> SqlDocumentRegistry::documents() const
{
QList<std::shared_ptr<SqlDocument>> result;
for (const auto &weak : m_documents) {
if (auto doc = weak.lock())
result.append(doc);
}
return result;
}
QList<std::shared_ptr<SqlDocument>> SqlDocumentRegistry::modifiedDocuments() const
{
QList<std::shared_ptr<SqlDocument>> result;
for (auto &doc : documents()) {
if (doc->isModified())
result.append(doc);
}
return result;
}

View file

@ -0,0 +1,27 @@
#ifndef SQLDOCUMENTREGISTRY_H
#define SQLDOCUMENTREGISTRY_H
#include <QList>
#include <memory>
#include <vector>
class SqlDocument;
/** Keeps track of all SQL documents that are still in use, without
* extending their lifetime. A document lives as long as at least one
* query tool holds a shared pointer to it.
*/
class SqlDocumentRegistry {
public:
void add(const std::shared_ptr<SqlDocument> &document);
/// All documents that are still alive.
QList<std::shared_ptr<SqlDocument>> documents() const;
/// All documents that are alive and have unsaved changes.
QList<std::shared_ptr<SqlDocument>> modifiedDocuments() const;
private:
std::vector<std::weak_ptr<SqlDocument>> m_documents;
};
#endif // SQLDOCUMENTREGISTRY_H

View file

@ -0,0 +1,112 @@
#include "UnsavedDocumentsDialog.h"
#include <QDialogButtonBox>
#include <QListWidget>
#include <QPushButton>
#include <QVBoxLayout>
#include <QLabel>
#include <algorithm>
UnsavedDocumentsDialog::UnsavedDocumentsDialog(const QList<std::shared_ptr<SqlDocument>> &docs, QWidget *parent)
: QDialog(parent)
, m_docs(docs)
, m_list(new QListWidget(this))
{
setWindowTitle(tr("Unsaved changes"));
auto layout = new QVBoxLayout(this);
layout->addWidget(new QLabel(tr("The following documents have unsaved changes. Select the documents to save or discard:"), this));
for (const auto &doc : m_docs) {
QStringList windows;
for (const QWidget *editor : doc->editors()) {
QString title = editor->window()->windowTitle();
if (!windows.contains(title))
windows.append(title);
}
auto item = new QListWidgetItem(QString("%1 (%2)").arg(doc->fileName(), windows.join(", ")), m_list);
item->setToolTip(doc->fileName());
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(Qt::Checked);
}
layout->addWidget(m_list);
auto buttons = new QDialogButtonBox(this);
auto save_button = buttons->addButton(tr("Save selected"), QDialogButtonBox::AcceptRole);
auto discard_button = buttons->addButton(tr("Discard selected"), QDialogButtonBox::DestructiveRole);
buttons->addButton(QDialogButtonBox::Cancel);
layout->addWidget(buttons);
connect(save_button, &QPushButton::clicked, this, [this]() {
m_choice = Choice::Save;
accept();
});
connect(discard_button, &QPushButton::clicked, this, [this]() {
m_choice = Choice::Discard;
accept();
});
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
}
QList<std::shared_ptr<SqlDocument>> UnsavedDocumentsDialog::checkedDocuments() const
{
QList<std::shared_ptr<SqlDocument>> result;
for (int i = 0; i < m_docs.size(); ++i) {
if (m_list->item(i)->checkState() == Qt::Checked)
result.append(m_docs[i]);
}
return result;
}
QList<std::shared_ptr<SqlDocument>> UnsavedDocumentsDialog::uncheckedDocuments() const
{
QList<std::shared_ptr<SqlDocument>> result;
for (int i = 0; i < m_docs.size(); ++i) {
if (m_list->item(i)->checkState() != Qt::Checked)
result.append(m_docs[i]);
}
return result;
}
QList<std::shared_ptr<SqlDocument>> documentsClosedWith(const QList<std::shared_ptr<SqlDocument>> &docs,
const std::function<bool(const QWidget*)> &isClosing)
{
QList<std::shared_ptr<SqlDocument>> result;
for (const auto &doc : docs) {
const auto &editors = doc->editors();
if (!doc->isModified() || editors.isEmpty())
continue;
bool all_closing = std::all_of(editors.begin(), editors.end(),
[&isClosing](const QWidget *editor) { return isClosing(editor); });
if (all_closing)
result.append(doc);
}
return result;
}
bool confirmClose(QWidget *parent, const QList<std::shared_ptr<SqlDocument>> &docs)
{
auto remaining = docs;
while (!remaining.isEmpty()) {
UnsavedDocumentsDialog dlg(remaining, parent);
if (dlg.exec() != QDialog::Accepted || dlg.choice() == UnsavedDocumentsDialog::Choice::Cancel)
return false;
bool save = dlg.choice() == UnsavedDocumentsDialog::Choice::Save;
for (const auto &doc : dlg.checkedDocuments()) {
if (save) {
if (!doc->save(parent))
return false;
}
else {
doc->setModified(false);
}
}
// Show the dialog again for the documents the user did not select.
remaining = dlg.uncheckedDocuments();
}
return true;
}

View file

@ -0,0 +1,48 @@
#ifndef UNSAVEDDOCUMENTSDIALOG_H
#define UNSAVEDDOCUMENTSDIALOG_H
#include "SqlDocument.h"
#include <QDialog>
#include <QList>
#include <functional>
#include <memory>
class QListWidget;
/** Lists modified documents with a checkbox per document. The user picks
* whether the checked documents should be saved or discarded.
*/
class UnsavedDocumentsDialog : public QDialog {
Q_OBJECT
public:
enum class Choice {
Cancel,
Save,
Discard
};
UnsavedDocumentsDialog(const QList<std::shared_ptr<SqlDocument>> &docs, QWidget *parent = nullptr);
Choice choice() const { return m_choice; }
QList<std::shared_ptr<SqlDocument>> checkedDocuments() const;
QList<std::shared_ptr<SqlDocument>> uncheckedDocuments() const;
private:
QList<std::shared_ptr<SqlDocument>> m_docs;
QListWidget *m_list;
Choice m_choice = Choice::Cancel;
};
/** Selects the documents that are closed when every editor for which
* isClosing returns true is closed. Only modified documents are returned.
*/
QList<std::shared_ptr<SqlDocument>> documentsClosedWith(const QList<std::shared_ptr<SqlDocument>> &docs,
const std::function<bool(const QWidget*)> &isClosing);
/** Asks the user what to do with the modified documents. Returns true when
* the close may continue, i.e. nothing was cancelled, no save failed and
* every listed document was checked.
*/
bool confirmClose(QWidget *parent, const QList<std::shared_ptr<SqlDocument>> &docs);
#endif // UNSAVEDDOCUMENTSDIALOG_H

View file

@ -0,0 +1,9 @@
---
features:
- |
Closing a window or quitting the application now shows a single "Unsaved changes" dialog listing all modified SQL documents. Select the documents to save or discard; after the action has completed the dialog is shown again for the remaining documents, until all are handled or you cancel.
- |
A SQL document can be opened in another window with its own connection via the new "Open in other window / Duplicate in window..." action in the File menu. Both windows share the same text, and the document is only prompted for when its last editor closes.
fixes:
- |
Quitting the application from the connection manager no longer discards unsaved SQL changes silently.