Show UnsavedDocumentsDialog when closing window or quitting application
so user can save and or discard docs more efficiently.
This commit is contained in:
parent
b97adced4c
commit
784ad68758
16 changed files with 544 additions and 164 deletions
|
|
@ -34,6 +34,9 @@ set(PGLAB_SOURCES
|
|||
querytool/QueryExplainModel.cpp
|
||||
querytool/QueryParamListController.cpp
|
||||
querytool/QueryTool.cpp
|
||||
querytool/SqlDocument.cpp
|
||||
querytool/SqlDocumentRegistry.cpp
|
||||
querytool/UnsavedDocumentsDialog.cpp
|
||||
serverinspector/DatabasesPage.cpp
|
||||
serverinspector/RolesPage.cpp
|
||||
serverinspector/ServerInspector.cpp
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ void ConnectionManagerWindow::on_actionQuit_application_triggered()
|
|||
{
|
||||
auto res = QMessageBox::question(this, "pglab",
|
||||
tr("Close all windows?"), QMessageBox::Yes, QMessageBox::No);
|
||||
if (res == QMessageBox::Yes) {
|
||||
if (res == QMessageBox::Yes && m_masterController->confirmQuit(this)) {
|
||||
QApplication::quit();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,10 +20,13 @@
|
|||
#include <QStandardPaths>
|
||||
#include <QStatusBar>
|
||||
#include <QTableView>
|
||||
#include <QApplication>
|
||||
#include <QInputDialog>
|
||||
|
||||
#include "EditTableWidget.h"
|
||||
#include "CodeGenerator.h"
|
||||
#include "querytool/QueryTool.h"
|
||||
#include "querytool/UnsavedDocumentsDialog.h"
|
||||
#include <serverinspector/ServerInspector.h>
|
||||
|
||||
namespace pg = Pgsql;
|
||||
|
|
@ -91,12 +94,18 @@ void DatabaseWindow::closeEvent(QCloseEvent *event)
|
|||
{
|
||||
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();
|
||||
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)
|
||||
|
|
@ -199,14 +208,14 @@ void DatabaseWindow::openSqlFile(QString file_name)
|
|||
{
|
||||
if (!file_name.isEmpty())
|
||||
{
|
||||
auto *ct = new QueryTool(this, this);
|
||||
if (ct->load(file_name))
|
||||
auto document = std::make_shared<SqlDocument>();
|
||||
if (document->load(file_name))
|
||||
{
|
||||
m_masterController->sqlDocumentRegistry().add(document);
|
||||
auto *ct = new QueryTool(this, document, this);
|
||||
ct->addAction(ui->actionExecute_query);
|
||||
addPage(ct, ct->title());
|
||||
}
|
||||
else
|
||||
delete ct;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -391,13 +400,53 @@ void DatabaseWindow::on_actionInspect_server_triggered()
|
|||
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()
|
||||
{
|
||||
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);
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include "OpenDatabase.h"
|
||||
#include "Pgsql_Connection.h"
|
||||
#include "IDatabaseWindow.h"
|
||||
#include "querytool/SqlDocument.h"
|
||||
#include <QMainWindow>
|
||||
#include <memory>
|
||||
|
||||
|
|
@ -38,6 +39,7 @@ public:
|
|||
~DatabaseWindow();
|
||||
|
||||
void setConfig(const ConnectionConfig &config);
|
||||
void openSharedSqlDocument(std::shared_ptr<SqlDocument> document);
|
||||
|
||||
std::shared_ptr<OpenDatabase> getDatabase() { return m_database; }
|
||||
|
||||
|
|
@ -98,6 +100,7 @@ private slots:
|
|||
void on_actionInspect_server_triggered();
|
||||
void on_actionNew_Query_triggered();
|
||||
void on_actionOpen_Query_triggered();
|
||||
void on_actionOpen_in_other_window_triggered();
|
||||
void on_actionSave_Query_triggered();
|
||||
void on_actionPaste_lang_string_triggered();
|
||||
void on_actionRefreshCatalog_triggered();
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
<addaction name="actionSave_Query"/>
|
||||
<addaction name="actionSave_query_as"/>
|
||||
<addaction name="actionSave_copy_of_query_as"/>
|
||||
<addaction name="actionOpen_in_other_window"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionSave_connection"/>
|
||||
<addaction name="actionExport_data"/>
|
||||
|
|
@ -106,6 +107,11 @@
|
|||
<addaction name="menuWindow"/>
|
||||
<addaction name="menuHelp"/>
|
||||
</widget>
|
||||
<action name="actionOpen_in_other_window">
|
||||
<property name="text">
|
||||
<string>Open in other window / Duplicate in window...</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionAbout">
|
||||
<property name="icon">
|
||||
<iconset resource="resources.qrc">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "MasterController.h"
|
||||
#include "ConnectionController.h"
|
||||
#include "querytool/UnsavedDocumentsDialog.h"
|
||||
#include "utils/PostgresqlUrlParser.h"
|
||||
#include <ConnectionConfig.h>
|
||||
#include <QCoreApplication>
|
||||
|
|
@ -59,4 +60,9 @@ SQLiteConnection& MasterController::userConfigDatabase()
|
|||
return m_userConfigDatabase;
|
||||
}
|
||||
|
||||
bool MasterController::confirmQuit(QWidget *parent)
|
||||
{
|
||||
return confirmClose(parent, m_sqlDocumentRegistry.modifiedDocuments());
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
#include <map>
|
||||
#include <memory>
|
||||
#include "sqlite/SQLiteConnection.h"
|
||||
#include "querytool/SqlDocumentRegistry.h"
|
||||
|
||||
|
||||
class ConnectionController;
|
||||
|
|
@ -24,6 +25,10 @@ public:
|
|||
|
||||
ConnectionController* connectionController();
|
||||
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:
|
||||
|
||||
|
|
@ -31,6 +36,7 @@ public slots:
|
|||
|
||||
private:
|
||||
SQLiteConnection m_userConfigDatabase;
|
||||
SqlDocumentRegistry m_sqlDocumentRegistry;
|
||||
ConnectionController* m_connectionController = nullptr;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -21,12 +21,16 @@
|
|||
#include "util.h"
|
||||
#include "UserConfiguration.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)
|
||||
, m_context(context)
|
||||
, ui(new Ui::QueryTab)
|
||||
, m_document(std::move(document))
|
||||
, m_dbConnection()
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
|
@ -40,13 +44,15 @@ QueryTool::QueryTool(IDatabaseWindow *context, QWidget *parent)
|
|||
|
||||
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();
|
||||
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);
|
||||
|
||||
m_queryParamListController = new QueryParamListController(ui->paramTableView, m_context->openDatabase(), this);
|
||||
|
|
@ -60,87 +66,42 @@ QueryTool::QueryTool(IDatabaseWindow *context, QWidget *parent)
|
|||
|
||||
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;
|
||||
}
|
||||
|
||||
bool QueryTool::CanClose(bool prompt_user)
|
||||
{
|
||||
bool can_close;
|
||||
if (m_queryTextChanged) {
|
||||
if (prompt_user)
|
||||
can_close = continueWithoutSavingWarning();
|
||||
else
|
||||
can_close = false;
|
||||
}
|
||||
else {
|
||||
can_close = true;
|
||||
}
|
||||
return can_close;
|
||||
}
|
||||
if (!m_document->isModified())
|
||||
return true;
|
||||
|
||||
void QueryTool::newdoc()
|
||||
{
|
||||
ui->queryEdit->clear();
|
||||
setFileName(tr("new"));
|
||||
setQueryTextChanged(false);
|
||||
m_new = true;
|
||||
}
|
||||
// The document stays open in another editor, so it is not closed with this tool
|
||||
if (m_document->editors().size() > 1)
|
||||
return true;
|
||||
|
||||
bool QueryTool::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();
|
||||
}
|
||||
if (!prompt_user)
|
||||
return false;
|
||||
|
||||
ui->queryEdit->setPlainText(text);
|
||||
setQueryTextChanged(false);
|
||||
setFileName(filename);
|
||||
m_new = false;
|
||||
result = true;
|
||||
}
|
||||
return result;
|
||||
return confirmClose(this, {m_document});
|
||||
}
|
||||
|
||||
bool QueryTool::save()
|
||||
{
|
||||
bool result;
|
||||
if (m_fileName.isEmpty() || m_new) {
|
||||
result = saveAs();
|
||||
}
|
||||
else {
|
||||
result = saveSqlTo(m_fileName);
|
||||
}
|
||||
return result;
|
||||
return m_document->save(this);
|
||||
}
|
||||
|
||||
bool QueryTool::saveAs()
|
||||
{
|
||||
bool result = false;
|
||||
QString filename = promptUserForSaveSqlFilename();
|
||||
if (!filename.isEmpty()) {
|
||||
result = saveSqlTo(filename);
|
||||
if (result) {
|
||||
setFileName(filename);
|
||||
m_new = false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return m_document->saveAs(this);
|
||||
}
|
||||
|
||||
void QueryTool::saveCopyAs()
|
||||
{
|
||||
QString filename = promptUserForSaveSqlFilename();
|
||||
if (!filename.isEmpty()) {
|
||||
saveSqlTo(filename);
|
||||
}
|
||||
m_document->saveCopyAs(this);
|
||||
}
|
||||
|
||||
void QueryTool::execute()
|
||||
|
|
@ -245,81 +206,21 @@ void QueryTool::cancel()
|
|||
|
||||
QString QueryTool::title() const
|
||||
{
|
||||
QFileInfo fileInfo(m_fileName);
|
||||
QFileInfo fileInfo(m_document->fileName());
|
||||
QString fn(fileInfo.fileName());
|
||||
return fn;
|
||||
}
|
||||
|
||||
void QueryTool::setFileName(const QString &filename)
|
||||
{
|
||||
m_fileName = filename;
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
void QueryTool::updateTitle()
|
||||
{
|
||||
QFileInfo fileInfo(m_fileName);
|
||||
QFileInfo fileInfo(m_document->fileName());
|
||||
QString title = fileInfo.fileName();
|
||||
|
||||
if (m_queryTextChanged) {
|
||||
if (m_document->isModified()) {
|
||||
title = "*" + title;
|
||||
}
|
||||
|
||||
m_context->setTitleForWidget(this, title, m_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);
|
||||
m_context->setTitleForWidget(this, title, m_document->fileName());
|
||||
}
|
||||
|
||||
void QueryTool::connectionStateChanged()
|
||||
|
|
@ -618,10 +519,3 @@ void QueryTool::exportData()
|
|||
exportDataToFilename(file_name);
|
||||
}
|
||||
|
||||
void QueryTool::setQueryTextChanged(bool queryTextChanged)
|
||||
{
|
||||
if (m_queryTextChanged != queryTextChanged) {
|
||||
m_queryTextChanged = queryTextChanged;
|
||||
updateTitle();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include "QueryExplainModel.h"
|
||||
#include "stopwatch.h"
|
||||
#include "tuplesresultwidget.h"
|
||||
#include "SqlDocument.h"
|
||||
|
||||
#include <QWidget>
|
||||
#include <memory>
|
||||
|
|
@ -34,12 +35,9 @@ class PgDatabaseCatalog;
|
|||
class QueryTool : public ManagedPage {
|
||||
Q_OBJECT
|
||||
public:
|
||||
QueryTool(IDatabaseWindow *context, QWidget *parent = nullptr);
|
||||
QueryTool(IDatabaseWindow *context, std::shared_ptr<SqlDocument> document, QWidget *parent = nullptr);
|
||||
~QueryTool() override;
|
||||
|
||||
void newdoc();
|
||||
bool load(const QString &filename);
|
||||
|
||||
void explain(bool analyze);
|
||||
|
||||
bool CanClose(bool prompt_user);
|
||||
|
|
@ -47,12 +45,12 @@ public:
|
|||
void generateCode();
|
||||
void exportDataToFilename(const QString &filename);
|
||||
|
||||
QString fileName() const { return m_fileName; }
|
||||
bool isChanged() const { return m_queryTextChanged; }
|
||||
bool isNew() const { return m_new; }
|
||||
QString fileName() const { return m_document->fileName(); }
|
||||
std::shared_ptr<SqlDocument> document() const { return m_document; }
|
||||
void focusEditor();
|
||||
|
||||
QString title() const;
|
||||
void updateTitle();
|
||||
|
||||
|
||||
public slots:
|
||||
|
|
@ -74,15 +72,12 @@ private:
|
|||
using ResultTabContainer = std::vector<TuplesResultWidget*>;
|
||||
IDatabaseWindow *m_context;
|
||||
Ui::QueryTab *ui;
|
||||
SqlSyntaxHighlighter* highlighter;
|
||||
std::shared_ptr<SqlDocument> m_document;
|
||||
ConnectionConfig m_config;
|
||||
StopWatch m_stopwatch;
|
||||
|
||||
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;
|
||||
ASyncDBConnection m_dbConnection;
|
||||
|
|
@ -90,11 +85,6 @@ private:
|
|||
std::unique_ptr<QueryExplainModel> explainModel;
|
||||
ResultTabContainer resultList;
|
||||
|
||||
void setFileName(const QString &filename);
|
||||
bool continueWithoutSavingWarning();
|
||||
bool saveSqlTo(const QString &filename);
|
||||
QString promptUserForSaveSqlFilename();
|
||||
|
||||
void addLog(QString s);
|
||||
|
||||
QString getCommand() const;
|
||||
|
|
@ -102,15 +92,12 @@ private:
|
|||
|
||||
void clearResult();
|
||||
void markError(const Pgsql::ErrorDetails &details);
|
||||
void updateTitle();
|
||||
void setQueryTextChanged(bool queryTextChanged);
|
||||
|
||||
private slots:
|
||||
|
||||
void explain_ready(ExplainRoot::SPtr explain);
|
||||
void query_ready(std::shared_ptr<Pgsql::Result>, qint64 elapsedms);
|
||||
|
||||
void queryTextChanged();
|
||||
void connectionStateChanged();
|
||||
void receiveNotice(Pgsql::ErrorDetails notice);
|
||||
|
||||
|
|
|
|||
143
pglab/querytool/SqlDocument.cpp
Normal file
143
pglab/querytool/SqlDocument.cpp
Normal 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)"));
|
||||
}
|
||||
60
pglab/querytool/SqlDocument.h
Normal file
60
pglab/querytool/SqlDocument.h
Normal 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
|
||||
27
pglab/querytool/SqlDocumentRegistry.cpp
Normal file
27
pglab/querytool/SqlDocumentRegistry.cpp
Normal 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;
|
||||
}
|
||||
27
pglab/querytool/SqlDocumentRegistry.h
Normal file
27
pglab/querytool/SqlDocumentRegistry.h
Normal 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
|
||||
112
pglab/querytool/UnsavedDocumentsDialog.cpp
Normal file
112
pglab/querytool/UnsavedDocumentsDialog.cpp
Normal 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;
|
||||
}
|
||||
48
pglab/querytool/UnsavedDocumentsDialog.h
Normal file
48
pglab/querytool/UnsavedDocumentsDialog.h
Normal 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
|
||||
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue