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.