diff --git a/.idea/QtSettings.xml b/.idea/QtSettings.xml
new file mode 100644
index 0000000..0248d0c
--- /dev/null
+++ b/.idea/QtSettings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..0b76fe5
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/pgLab.iml b/.idea/pgLab.iml
index 6102194..4c94235 100644
--- a/.idea/pgLab.iml
+++ b/.idea/pgLab.iml
@@ -1,8 +1,2 @@
-
-
-
-
-
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/pglab/crud/CrudModel.cpp b/pglab/crud/CrudModel.cpp
index a8bcae1..c30a1bc 100644
--- a/pglab/crud/CrudModel.cpp
+++ b/pglab/crud/CrudModel.cpp
@@ -244,6 +244,7 @@ void CrudModel::loadData()
void CrudModel::loadIntoModel(std::shared_ptr data)
{
beginResetModel();
+ bool was_dirty = hasPendingChanges();
m_pendingRowList.clear();
m_roData = data;
initializeColumnList();
@@ -253,6 +254,8 @@ void CrudModel::loadIntoModel(std::shared_ptr data)
if (m_wholeTable)
appendNewRowInternal();
endResetModel();
+ if (was_dirty)
+ emit pendingChangesChanged(false);
}
void CrudModel::initRowMapping()
@@ -321,7 +324,10 @@ bool CrudModel::setData(const QModelIndex &index, const QVariant &value, int rol
s.clear();
val = s;
}
+ bool was_dirty = hasPendingChanges();
m_pendingRowList.setValue(col, row_mapping.rowKey, val);
+ if (!was_dirty)
+ emit pendingChangesChanged(true);
emit dataChanged(index, index, QVector() << role);
return true;
@@ -611,14 +617,27 @@ void CrudModel::initializeColumnList()
}
}
-bool CrudModel::submit()
+bool CrudModel::saveChanges()
{
- return savePendingChanges();
+ bool was_dirty = hasPendingChanges();
+ bool ok = savePendingChanges();
+ if (was_dirty != hasPendingChanges())
+ emit pendingChangesChanged(hasPendingChanges());
+ return ok;
}
-void CrudModel::revert()
+void CrudModel::revertChanges()
{
+ if (!hasPendingChanges())
+ return;
+ m_pendingRowList.clear();
+ for (auto &row_mapping : m_rowMapping)
+ row_mapping.pending = false;
+
+ if (rowCount() > 0)
+ emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1));
+ emit pendingChangesChanged(false);
}
void CrudModel::appendNewRow()
@@ -678,7 +697,10 @@ std::tuple CrudModel::removeRows(const std::set
// If something goes wrong after this commit we should reload contents of model
}
// Then from model
+ bool was_dirty = hasPendingChanges();
RemoveRangesOfRowsFromModel(row_ranges);
+ if (was_dirty != hasPendingChanges())
+ emit pendingChangesChanged(hasPendingChanges());
return { true, "" };
}
catch (const Pgsql::PgResultError &error)
@@ -713,6 +735,8 @@ void CrudModel::RemoveRangeOfRowsFromModel(IntegerRange range)
beginRemoveRows(QModelIndex(), range.start(), range.end() - 1);
SCOPE_EXIT { endRemoveRows(); };
auto first = m_rowMapping.begin() + range.start();
+ for (auto iter = first; iter != first + range.length(); ++iter)
+ m_pendingRowList.m_rows.erase(iter->rowKey);
m_rowMapping.erase(first, first + range.length());
}
diff --git a/pglab/crud/CrudModel.h b/pglab/crud/CrudModel.h
index d05b6c0..485b6c2 100644
--- a/pglab/crud/CrudModel.h
+++ b/pglab/crud/CrudModel.h
@@ -46,12 +46,21 @@ public:
void loadData();
+ /// True when there are edits that have not been saved to the database
+ bool hasPendingChanges() const { return !m_pendingRowList.m_rows.empty(); }
+ /// Writes all pending rows to the database, rows that fail stay pending
+ bool saveChanges();
+ /// Discards all pending edits, the grid shows the last saved values again
+ void revertChanges();
+
std::tuple removeRows(const std::set> &row_ranges);
bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override;
+signals:
+ /// Emitted when the model goes from clean to having pending changes or back
+ void pendingChangesChanged(bool hasChanges);
+
public slots:
- virtual bool submit() override;
- virtual void revert() override;
private:
diff --git a/pglab/crud/CrudTab.cpp b/pglab/crud/CrudTab.cpp
index 88b0ac2..8bb44b0 100644
--- a/pglab/crud/CrudTab.cpp
+++ b/pglab/crud/CrudTab.cpp
@@ -60,6 +60,13 @@ void CrudTab::setConfig(Oid oid) //std::shared_ptr db, const PgCla
void CrudTab::refresh()
{
+ if (m_crudModel->hasPendingChanges()) {
+ auto res = QMessageBox::question(this, "pgLab",
+ tr("There are unsaved changes. Refreshing discards them. Do you want to continue?"),
+ QMessageBox::Yes, QMessageBox::No);
+ if (res != QMessageBox::Yes)
+ return;
+ }
m_crudModel->loadData();
}
diff --git a/pglab/crud/DataGridWidget.cpp b/pglab/crud/DataGridWidget.cpp
index 64f19a8..65708f7 100644
--- a/pglab/crud/DataGridWidget.cpp
+++ b/pglab/crud/DataGridWidget.cpp
@@ -1,9 +1,13 @@
#include "DataGridWidget.h"
+#include "CrudModel.h"
#include "CustomDataRole.h"
#include "ResultTableModelUtil.h"
#include "util/PgLabItemDelegate.h"
+#include
#include
+#include
#include
+#include
#include
@@ -13,6 +17,25 @@ DataGridWidget::DataGridWidget(QWidget *parent)
auto layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
+ m_toolbar = new QToolBar(this);
+ m_toolbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ layout->addWidget(m_toolbar);
+
+ m_actSave = new QAction(style()->standardIcon(QStyle::SP_DialogSaveButton), tr("Save"), this);
+ m_actSave->setShortcut(QKeySequence::Save);
+ m_actSave->setShortcutContext(Qt::WidgetWithChildrenShortcut);
+ m_actSave->setToolTip(tr("Save all pending changes to the database (Ctrl+S)"));
+ m_actSave->setEnabled(false);
+ addAction(m_actSave);
+ m_toolbar->addAction(m_actSave);
+ connect(m_actSave, &QAction::triggered, this, &DataGridWidget::onSave);
+
+ m_actRevert = new QAction(style()->standardIcon(QStyle::SP_DialogResetButton), tr("Revert"), this);
+ m_actRevert->setToolTip(tr("Discard all pending changes"));
+ m_actRevert->setEnabled(false);
+ m_toolbar->addAction(m_actRevert);
+ connect(m_actRevert, &QAction::triggered, this, &DataGridWidget::onRevert);
+
m_view = new QTableView(this);
layout->addWidget(m_view);
@@ -27,8 +50,52 @@ void DataGridWidget::setSourceModel(QAbstractItemModel *model)
m_proxy = new QSortFilterProxyModel(this);
m_proxy->setSortRole(CustomSortRole);
}
+ disconnect(m_pendingChangesConnection);
m_proxy->setSourceModel(model);
m_view->setModel(m_proxy);
m_view->setSortingEnabled(true);
m_proxy->sort(0, Qt::AscendingOrder);
+
+ auto crud = qobject_cast(model);
+ m_toolbar->setVisible(crud != nullptr);
+ if (crud) {
+ m_pendingChangesConnection = connect(crud, &CrudModel::pendingChangesChanged, this, &DataGridWidget::onPendingChangesChanged);
+ onPendingChangesChanged(crud->hasPendingChanges());
+ }
+ else {
+ onPendingChangesChanged(false);
+ }
+}
+
+void DataGridWidget::onPendingChangesChanged(bool hasChanges)
+{
+ m_actSave->setEnabled(hasChanges);
+ m_actRevert->setEnabled(hasChanges);
+}
+
+void DataGridWidget::onSave()
+{
+ auto crud = qobject_cast(m_proxy->sourceModel());
+ if (!crud)
+ return;
+
+ commitOpenEditor();
+ crud->saveChanges();
+}
+
+void DataGridWidget::onRevert()
+{
+ auto crud = qobject_cast(m_proxy->sourceModel());
+ if (!crud)
+ return;
+
+ commitOpenEditor();
+ crud->revertChanges();
+}
+
+void DataGridWidget::commitOpenEditor()
+{
+ // Moving focus away from the editor makes the delegate commit its value, a toolbar click does not take focus
+ if (m_view->indexWidget(m_view->currentIndex()))
+ m_view->setFocus();
}
diff --git a/pglab/crud/DataGridWidget.h b/pglab/crud/DataGridWidget.h
index 11d6745..d605053 100644
--- a/pglab/crud/DataGridWidget.h
+++ b/pglab/crud/DataGridWidget.h
@@ -4,8 +4,10 @@
#include
class QAbstractItemModel;
+class QAction;
class QSortFilterProxyModel;
class QTableView;
+class QToolBar;
/** Sortable and editable grid shared by the CRUD tab and the query result tab.
*/
@@ -20,9 +22,21 @@ public:
QTableView *view() const { return m_view; }
QSortFilterProxyModel *proxy() const { return m_proxy; }
+private slots:
+ void onPendingChangesChanged(bool hasChanges);
+ void onSave();
+ void onRevert();
+
private:
+ /// Closes an open cell editor so its value is written to the model before saving or reverting
+ void commitOpenEditor();
+
QTableView *m_view = nullptr;
QSortFilterProxyModel *m_proxy = nullptr;
+ QToolBar *m_toolbar = nullptr;
+ QAction *m_actSave = nullptr;
+ QAction *m_actRevert = nullptr;
+ QMetaObject::Connection m_pendingChangesConnection;
};
#endif // DATAGRIDWIDGET_H
diff --git a/releasenotes/notes/datagrid-save-revert-toolbar-3f9c1a7e5b2d8604.yaml b/releasenotes/notes/datagrid-save-revert-toolbar-3f9c1a7e5b2d8604.yaml
new file mode 100644
index 0000000..ebfdb20
--- /dev/null
+++ b/releasenotes/notes/datagrid-save-revert-toolbar-3f9c1a7e5b2d8604.yaml
@@ -0,0 +1,13 @@
+---
+features:
+ - |
+ Data grids in the table editor and in query results now have a toolbar
+ with :menuselection:`Save` and :menuselection:`Revert` buttons, Save can
+ also be triggered with :kbd:`Ctrl+S`. Both buttons are only enabled while
+ there are unsaved changes.
+other:
+ - |
+ Edits in data grids are no longer saved automatically when a cell editor
+ is closed or when :kbd:`Escape` is pressed. Use the :menuselection:`Save`
+ button to write changes to the database, or :menuselection:`Revert` to
+ discard them. Refreshing a table with unsaved changes asks for confirmation.