Compare commits
No commits in common. "70f4601aa9c3704f69fc22fb83611da6eb291a7b" and "784ad68758931ca54247127a936dbc3c1f437c09" have entirely different histories.
70f4601aa9
...
784ad68758
18 changed files with 177 additions and 540 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -13,4 +13,3 @@ pglabAll.pro.user.4.8-pre1
|
||||||
/pglabAll.pro.user*
|
/pglabAll.pro.user*
|
||||||
/public/
|
/public/
|
||||||
/Testing/Temporary
|
/Testing/Temporary
|
||||||
.junie/
|
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ set(PGLAB_SOURCES
|
||||||
crud/CrudModel.cpp
|
crud/CrudModel.cpp
|
||||||
crud/CrudTab.cpp
|
crud/CrudTab.cpp
|
||||||
crud/DataGridWidget.cpp
|
crud/DataGridWidget.cpp
|
||||||
crud/RecordPanel.cpp
|
|
||||||
querytool/QueryExplainModel.cpp
|
querytool/QueryExplainModel.cpp
|
||||||
querytool/QueryParamListController.cpp
|
querytool/QueryParamListController.cpp
|
||||||
querytool/QueryTool.cpp
|
querytool/QueryTool.cpp
|
||||||
|
|
@ -72,6 +71,8 @@ set(PGLAB_SOURCES
|
||||||
EditTableWidget.cpp
|
EditTableWidget.cpp
|
||||||
EditColumnTableModel.cpp
|
EditColumnTableModel.cpp
|
||||||
DatabaseWindow.cpp
|
DatabaseWindow.cpp
|
||||||
|
widgets/SingleRecordModel.cpp
|
||||||
|
widgets/SingleRecordWidget.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set(PGLAB_FORMS
|
set(PGLAB_FORMS
|
||||||
|
|
@ -83,6 +84,7 @@ set(PGLAB_FORMS
|
||||||
ProcessStdioWidget.ui
|
ProcessStdioWidget.ui
|
||||||
crud/CrudTab.ui
|
crud/CrudTab.ui
|
||||||
CodeGenerator.ui
|
CodeGenerator.ui
|
||||||
|
widgets/SingleRecordWidget.ui
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(pglab
|
add_executable(pglab
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,6 @@ public:
|
||||||
/// Show an arbitrary query result, cells are editable where the result maps them to a table with a primary key
|
/// Show an arbitrary query result, cells are editable where the result maps them to a table with a primary key
|
||||||
void setResult(std::shared_ptr<OpenDatabase> db, std::shared_ptr<Pgsql::Result> result);
|
void setResult(std::shared_ptr<OpenDatabase> db, std::shared_ptr<Pgsql::Result> result);
|
||||||
|
|
||||||
/// Number of grid columns in front of the result columns (the row number column)
|
|
||||||
static constexpr int PreColumnCount = 1;
|
|
||||||
|
|
||||||
|
|
||||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||||
|
|
||||||
|
|
@ -65,6 +62,10 @@ signals:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
|
/// Number of grid columns in front of the result columns (the row number column)
|
||||||
|
static constexpr int PreColumnCount = 1;
|
||||||
|
|
||||||
|
|
||||||
class ColumnSort {
|
class ColumnSort {
|
||||||
public:
|
public:
|
||||||
enum Direction { Ascending, Descending };
|
enum Direction { Ascending, Descending };
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
#include "DataGridWidget.h"
|
#include "DataGridWidget.h"
|
||||||
#include "CrudModel.h"
|
#include "CrudModel.h"
|
||||||
#include "CustomDataRole.h"
|
#include "CustomDataRole.h"
|
||||||
#include "RecordPanel.h"
|
|
||||||
#include "ResultTableModelUtil.h"
|
#include "ResultTableModelUtil.h"
|
||||||
#include "util/PgLabItemDelegate.h"
|
#include "util/PgLabItemDelegate.h"
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QItemSelectionModel>
|
|
||||||
#include <QSortFilterProxyModel>
|
#include <QSortFilterProxyModel>
|
||||||
#include <QSplitter>
|
|
||||||
#include <QStyle>
|
#include <QStyle>
|
||||||
#include <QTableView>
|
#include <QTableView>
|
||||||
#include <QToolBar>
|
#include <QToolBar>
|
||||||
|
|
@ -39,28 +36,12 @@ DataGridWidget::DataGridWidget(QWidget *parent)
|
||||||
m_toolbar->addAction(m_actRevert);
|
m_toolbar->addAction(m_actRevert);
|
||||||
connect(m_actRevert, &QAction::triggered, this, &DataGridWidget::onRevert);
|
connect(m_actRevert, &QAction::triggered, this, &DataGridWidget::onRevert);
|
||||||
|
|
||||||
m_actRecord = new QAction(tr("Record"), this);
|
|
||||||
m_actRecord->setCheckable(true);
|
|
||||||
m_actRecord->setChecked(true);
|
|
||||||
m_actRecord->setToolTip(tr("Show the values of the current row next to the grid"));
|
|
||||||
m_toolbar->addAction(m_actRecord);
|
|
||||||
|
|
||||||
m_view = new QTableView(this);
|
m_view = new QTableView(this);
|
||||||
|
layout->addWidget(m_view);
|
||||||
|
|
||||||
SetTableViewDefault(m_view);
|
SetTableViewDefault(m_view);
|
||||||
auto delegate = new PgLabItemDelegate(m_view);
|
auto delegate = new PgLabItemDelegate(m_view);
|
||||||
m_view->setItemDelegate(delegate);
|
m_view->setItemDelegate(delegate);
|
||||||
|
|
||||||
m_splitter = new QSplitter(Qt::Horizontal, this);
|
|
||||||
layout->addWidget(m_splitter);
|
|
||||||
m_splitter->addWidget(m_view);
|
|
||||||
|
|
||||||
m_recordPanel = new RecordPanel(this);
|
|
||||||
m_recordPanel->setFirstDataColumn(CrudModel::PreColumnCount);
|
|
||||||
m_splitter->addWidget(m_recordPanel);
|
|
||||||
m_splitter->setStretchFactor(0, 1);
|
|
||||||
m_splitter->setStretchFactor(1, 0);
|
|
||||||
|
|
||||||
connect(m_actRecord, &QAction::toggled, m_recordPanel, &QWidget::setVisible);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DataGridWidget::setSourceModel(QAbstractItemModel *model)
|
void DataGridWidget::setSourceModel(QAbstractItemModel *model)
|
||||||
|
|
@ -70,18 +51,13 @@ void DataGridWidget::setSourceModel(QAbstractItemModel *model)
|
||||||
m_proxy->setSortRole(CustomSortRole);
|
m_proxy->setSortRole(CustomSortRole);
|
||||||
}
|
}
|
||||||
disconnect(m_pendingChangesConnection);
|
disconnect(m_pendingChangesConnection);
|
||||||
disconnect(m_currentRowConnection);
|
|
||||||
m_proxy->setSourceModel(model);
|
m_proxy->setSourceModel(model);
|
||||||
m_view->setModel(m_proxy);
|
m_view->setModel(m_proxy);
|
||||||
m_view->setSortingEnabled(true);
|
m_view->setSortingEnabled(true);
|
||||||
m_proxy->sort(0, Qt::AscendingOrder);
|
m_proxy->sort(0, Qt::AscendingOrder);
|
||||||
|
|
||||||
m_recordPanel->setModel(m_proxy);
|
|
||||||
m_currentRowConnection = connect(m_view->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &DataGridWidget::onCurrentRowChanged);
|
|
||||||
|
|
||||||
auto crud = qobject_cast<CrudModel *>(model);
|
auto crud = qobject_cast<CrudModel *>(model);
|
||||||
m_actSave->setVisible(crud != nullptr);
|
m_toolbar->setVisible(crud != nullptr);
|
||||||
m_actRevert->setVisible(crud != nullptr);
|
|
||||||
if (crud) {
|
if (crud) {
|
||||||
m_pendingChangesConnection = connect(crud, &CrudModel::pendingChangesChanged, this, &DataGridWidget::onPendingChangesChanged);
|
m_pendingChangesConnection = connect(crud, &CrudModel::pendingChangesChanged, this, &DataGridWidget::onPendingChangesChanged);
|
||||||
onPendingChangesChanged(crud->hasPendingChanges());
|
onPendingChangesChanged(crud->hasPendingChanges());
|
||||||
|
|
@ -91,11 +67,6 @@ void DataGridWidget::setSourceModel(QAbstractItemModel *model)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DataGridWidget::onCurrentRowChanged(const QModelIndex ¤t)
|
|
||||||
{
|
|
||||||
m_recordPanel->setCurrentRow(current.row());
|
|
||||||
}
|
|
||||||
|
|
||||||
void DataGridWidget::onPendingChangesChanged(bool hasChanges)
|
void DataGridWidget::onPendingChangesChanged(bool hasChanges)
|
||||||
{
|
{
|
||||||
m_actSave->setEnabled(hasChanges);
|
m_actSave->setEnabled(hasChanges);
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,9 @@
|
||||||
|
|
||||||
class QAbstractItemModel;
|
class QAbstractItemModel;
|
||||||
class QAction;
|
class QAction;
|
||||||
class QModelIndex;
|
|
||||||
class QSortFilterProxyModel;
|
class QSortFilterProxyModel;
|
||||||
class QSplitter;
|
|
||||||
class QTableView;
|
class QTableView;
|
||||||
class QToolBar;
|
class QToolBar;
|
||||||
class RecordPanel;
|
|
||||||
|
|
||||||
/** Sortable and editable grid shared by the CRUD tab and the query result tab.
|
/** Sortable and editable grid shared by the CRUD tab and the query result tab.
|
||||||
*/
|
*/
|
||||||
|
|
@ -29,7 +26,6 @@ private slots:
|
||||||
void onPendingChangesChanged(bool hasChanges);
|
void onPendingChangesChanged(bool hasChanges);
|
||||||
void onSave();
|
void onSave();
|
||||||
void onRevert();
|
void onRevert();
|
||||||
void onCurrentRowChanged(const QModelIndex ¤t);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/// Closes an open cell editor so its value is written to the model before saving or reverting
|
/// Closes an open cell editor so its value is written to the model before saving or reverting
|
||||||
|
|
@ -37,14 +33,10 @@ private:
|
||||||
|
|
||||||
QTableView *m_view = nullptr;
|
QTableView *m_view = nullptr;
|
||||||
QSortFilterProxyModel *m_proxy = nullptr;
|
QSortFilterProxyModel *m_proxy = nullptr;
|
||||||
QSplitter *m_splitter = nullptr;
|
|
||||||
RecordPanel *m_recordPanel = nullptr;
|
|
||||||
QToolBar *m_toolbar = nullptr;
|
QToolBar *m_toolbar = nullptr;
|
||||||
QAction *m_actSave = nullptr;
|
QAction *m_actSave = nullptr;
|
||||||
QAction *m_actRevert = nullptr;
|
QAction *m_actRevert = nullptr;
|
||||||
QAction *m_actRecord = nullptr;
|
|
||||||
QMetaObject::Connection m_pendingChangesConnection;
|
QMetaObject::Connection m_pendingChangesConnection;
|
||||||
QMetaObject::Connection m_currentRowConnection;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // DATAGRIDWIDGET_H
|
#endif // DATAGRIDWIDGET_H
|
||||||
|
|
|
||||||
|
|
@ -1,268 +0,0 @@
|
||||||
#include "RecordPanel.h"
|
|
||||||
#include "CustomDataRole.h"
|
|
||||||
#include "Pgsql_oids.h"
|
|
||||||
#include <QEvent>
|
|
||||||
#include <QFont>
|
|
||||||
#include <QFontDatabase>
|
|
||||||
#include <QFormLayout>
|
|
||||||
#include <QHBoxLayout>
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QLineEdit>
|
|
||||||
#include <QPlainTextEdit>
|
|
||||||
#include <QToolButton>
|
|
||||||
#include <QVBoxLayout>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
constexpr int CollapsedTextLines = 2;
|
|
||||||
constexpr int ExpandedTextLines = 10;
|
|
||||||
|
|
||||||
bool isLongTextType(Oid oid)
|
|
||||||
{
|
|
||||||
switch (oid) {
|
|
||||||
case Pgsql::text_oid:
|
|
||||||
case Pgsql::varchar_oid:
|
|
||||||
case Pgsql::bpchar_oid:
|
|
||||||
case Pgsql::json_oid:
|
|
||||||
case Pgsql::jsonb_oid:
|
|
||||||
case Pgsql::xml_oid:
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int textEditHeight(const QPlainTextEdit *edit, int lines)
|
|
||||||
{
|
|
||||||
return edit->fontMetrics().lineSpacing() * lines + 2 * edit->frameWidth() + 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
QString editorText(const QWidget *editor)
|
|
||||||
{
|
|
||||||
if (auto line = qobject_cast<const QLineEdit *>(editor))
|
|
||||||
return line->text();
|
|
||||||
if (auto plain = qobject_cast<const QPlainTextEdit *>(editor))
|
|
||||||
return plain->toPlainText();
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
RecordPanel::RecordPanel(QWidget *parent)
|
|
||||||
: QScrollArea(parent)
|
|
||||||
{
|
|
||||||
setWidgetResizable(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RecordPanel::setModel(QAbstractItemModel *model)
|
|
||||||
{
|
|
||||||
if (m_model)
|
|
||||||
m_model->disconnect(this);
|
|
||||||
|
|
||||||
m_model = model;
|
|
||||||
if (m_model) {
|
|
||||||
connect(m_model, &QAbstractItemModel::dataChanged, this, &RecordPanel::refresh);
|
|
||||||
connect(m_model, &QAbstractItemModel::layoutChanged, this, &RecordPanel::refresh);
|
|
||||||
connect(m_model, &QAbstractItemModel::rowsInserted, this, &RecordPanel::refresh);
|
|
||||||
connect(m_model, &QAbstractItemModel::rowsRemoved, this, &RecordPanel::refresh);
|
|
||||||
connect(m_model, &QAbstractItemModel::modelReset, this, &RecordPanel::onModelReset);
|
|
||||||
}
|
|
||||||
onModelReset();
|
|
||||||
}
|
|
||||||
|
|
||||||
void RecordPanel::setFirstDataColumn(int column)
|
|
||||||
{
|
|
||||||
m_firstDataColumn = column;
|
|
||||||
clearFields();
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
void RecordPanel::setCurrentRow(int row)
|
|
||||||
{
|
|
||||||
if (m_model && row >= 0 && row < m_model->rowCount())
|
|
||||||
m_rowIndex = QPersistentModelIndex(m_model->index(row, 0));
|
|
||||||
else
|
|
||||||
m_rowIndex = QPersistentModelIndex();
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
void RecordPanel::onModelReset()
|
|
||||||
{
|
|
||||||
m_rowIndex = QPersistentModelIndex();
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
void RecordPanel::refresh()
|
|
||||||
{
|
|
||||||
if (!m_model || !m_rowIndex.isValid()) {
|
|
||||||
clearFields();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int columns = m_model->columnCount() - m_firstDataColumn;
|
|
||||||
if (columns < 0)
|
|
||||||
columns = 0;
|
|
||||||
if (m_fields.size() != static_cast<size_t>(columns))
|
|
||||||
rebuild();
|
|
||||||
|
|
||||||
for (auto &field : m_fields) {
|
|
||||||
QModelIndex index = m_model->index(m_rowIndex.row(), field.column);
|
|
||||||
bool editable = m_model->flags(index).testFlag(Qt::ItemIsEditable);
|
|
||||||
QVariant value = index.data(Qt::EditRole);
|
|
||||||
QString text;
|
|
||||||
if (value.isValid() && !value.isNull()) {
|
|
||||||
text = value.toString();
|
|
||||||
// An empty string is shown as '' so it can be told apart from NULL, the placeholder shows NULL
|
|
||||||
if (text.isEmpty())
|
|
||||||
text = QStringLiteral("''");
|
|
||||||
}
|
|
||||||
showValue(field, text, editable);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void RecordPanel::clearFields()
|
|
||||||
{
|
|
||||||
m_fields.clear();
|
|
||||||
if (QWidget *old = takeWidget())
|
|
||||||
old->deleteLater();
|
|
||||||
}
|
|
||||||
|
|
||||||
void RecordPanel::rebuild()
|
|
||||||
{
|
|
||||||
clearFields();
|
|
||||||
|
|
||||||
auto content = new QWidget;
|
|
||||||
auto layout = new QFormLayout(content);
|
|
||||||
layout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
|
|
||||||
|
|
||||||
int row = m_rowIndex.row();
|
|
||||||
for (int column = m_firstDataColumn; column < m_model->columnCount(); ++column) {
|
|
||||||
QString header = m_model->headerData(column, Qt::Horizontal, Qt::DisplayRole).toString();
|
|
||||||
int newline = header.indexOf('\n');
|
|
||||||
QString name = newline < 0 ? header : header.left(newline);
|
|
||||||
QString typeName = newline < 0 ? QString() : header.mid(newline + 1);
|
|
||||||
|
|
||||||
auto caption = new QWidget(content);
|
|
||||||
auto captionLayout = new QVBoxLayout(caption);
|
|
||||||
captionLayout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
auto nameLabel = new QLabel(name, caption);
|
|
||||||
QFont nameFont = nameLabel->font();
|
|
||||||
nameFont.setBold(true);
|
|
||||||
nameLabel->setFont(nameFont);
|
|
||||||
auto typeLabel = new QLabel(typeName, caption);
|
|
||||||
captionLayout->addWidget(nameLabel);
|
|
||||||
captionLayout->addWidget(typeLabel);
|
|
||||||
|
|
||||||
Oid oid = m_model->index(row, column).data(CustomDataTypeRole).toUInt();
|
|
||||||
FieldRow field;
|
|
||||||
field.column = column;
|
|
||||||
QWidget *fieldWidget = nullptr;
|
|
||||||
|
|
||||||
if (isLongTextType(oid)) {
|
|
||||||
auto container = new QWidget(content);
|
|
||||||
auto containerLayout = new QVBoxLayout(container);
|
|
||||||
containerLayout->setContentsMargins(0, 0, 0, 0);
|
|
||||||
|
|
||||||
auto edit = new QPlainTextEdit(container);
|
|
||||||
edit->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
|
|
||||||
edit->setPlaceholderText(tr("null"));
|
|
||||||
edit->setFixedHeight(textEditHeight(edit, CollapsedTextLines));
|
|
||||||
edit->installEventFilter(this);
|
|
||||||
|
|
||||||
auto expand = new QToolButton(container);
|
|
||||||
expand->setCheckable(true);
|
|
||||||
expand->setArrowType(Qt::DownArrow);
|
|
||||||
expand->setToolTip(tr("Expand or collapse the value"));
|
|
||||||
connect(expand, &QToolButton::toggled, edit, [edit, expand](bool expanded) {
|
|
||||||
edit->setFixedHeight(textEditHeight(edit, expanded ? ExpandedTextLines : CollapsedTextLines));
|
|
||||||
expand->setArrowType(expanded ? Qt::UpArrow : Qt::DownArrow);
|
|
||||||
});
|
|
||||||
|
|
||||||
auto buttonRow = new QHBoxLayout;
|
|
||||||
buttonRow->addStretch();
|
|
||||||
buttonRow->addWidget(expand);
|
|
||||||
containerLayout->addWidget(edit);
|
|
||||||
containerLayout->addLayout(buttonRow);
|
|
||||||
|
|
||||||
field.editor = edit;
|
|
||||||
field.expandButton = expand;
|
|
||||||
fieldWidget = container;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
auto edit = new QLineEdit(content);
|
|
||||||
edit->setPlaceholderText(tr("null"));
|
|
||||||
connect(edit, &QLineEdit::editingFinished, this, [this, edit]() { commitEditor(edit); });
|
|
||||||
|
|
||||||
field.editor = edit;
|
|
||||||
fieldWidget = edit;
|
|
||||||
}
|
|
||||||
|
|
||||||
layout->addRow(caption, fieldWidget);
|
|
||||||
m_fields.push_back(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
setWidget(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RecordPanel::showValue(FieldRow &field, const QString &text, bool editable)
|
|
||||||
{
|
|
||||||
QWidget *editor = field.editor;
|
|
||||||
if (auto line = qobject_cast<QLineEdit *>(editor))
|
|
||||||
line->setReadOnly(!editable);
|
|
||||||
else if (auto plain = qobject_cast<QPlainTextEdit *>(editor))
|
|
||||||
plain->setReadOnly(!editable);
|
|
||||||
|
|
||||||
// Do not replace the text while the user is typing in it
|
|
||||||
if (editor->hasFocus())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (auto line = qobject_cast<QLineEdit *>(editor))
|
|
||||||
line->setText(text);
|
|
||||||
else if (auto plain = qobject_cast<QPlainTextEdit *>(editor))
|
|
||||||
plain->setPlainText(text);
|
|
||||||
field.original = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RecordPanel::commitEditor(QWidget *editor)
|
|
||||||
{
|
|
||||||
if (m_updating || !m_model || !m_rowIndex.isValid())
|
|
||||||
return;
|
|
||||||
|
|
||||||
FieldRow *field = findField(editor);
|
|
||||||
if (!field)
|
|
||||||
return;
|
|
||||||
|
|
||||||
QString text = editorText(editor);
|
|
||||||
if (text == field->original)
|
|
||||||
return;
|
|
||||||
|
|
||||||
QModelIndex index = m_model->index(m_rowIndex.row(), field->column);
|
|
||||||
if (!m_model->flags(index).testFlag(Qt::ItemIsEditable))
|
|
||||||
return;
|
|
||||||
|
|
||||||
m_updating = true;
|
|
||||||
m_model->setData(index, text, Qt::EditRole);
|
|
||||||
m_updating = false;
|
|
||||||
|
|
||||||
// The model may have rebuilt the fields while storing the value, so look the field up again
|
|
||||||
if (FieldRow *updated = findField(editor))
|
|
||||||
updated->original = text;
|
|
||||||
}
|
|
||||||
|
|
||||||
RecordPanel::FieldRow *RecordPanel::findField(QWidget *editor)
|
|
||||||
{
|
|
||||||
for (auto &field : m_fields) {
|
|
||||||
if (field.editor == editor)
|
|
||||||
return &field;
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RecordPanel::eventFilter(QObject *watched, QEvent *event)
|
|
||||||
{
|
|
||||||
if (event->type() == QEvent::FocusOut) {
|
|
||||||
if (auto edit = qobject_cast<QPlainTextEdit *>(watched))
|
|
||||||
commitEditor(edit);
|
|
||||||
}
|
|
||||||
return QScrollArea::eventFilter(watched, event);
|
|
||||||
}
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
#ifndef RECORDPANEL_H
|
|
||||||
#define RECORDPANEL_H
|
|
||||||
|
|
||||||
#include <QScrollArea>
|
|
||||||
#include <QPersistentModelIndex>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
class QAbstractItemModel;
|
|
||||||
class QEvent;
|
|
||||||
class QToolButton;
|
|
||||||
|
|
||||||
/** Shows the values of one row of a model as a scrollable list, one line per column.
|
|
||||||
* Values are edited through the model so the pending change logic of the model is reused.
|
|
||||||
*/
|
|
||||||
class RecordPanel : public QScrollArea {
|
|
||||||
Q_OBJECT
|
|
||||||
public:
|
|
||||||
explicit RecordPanel(QWidget *parent = nullptr);
|
|
||||||
|
|
||||||
/// Sets the model to show, the columns from firstDataColumn on are shown
|
|
||||||
void setModel(QAbstractItemModel *model);
|
|
||||||
/// Columns before this one are skipped, e.g. the row number column of the CRUD model
|
|
||||||
void setFirstDataColumn(int column);
|
|
||||||
/// Shows the values of the given row of the model, -1 shows nothing
|
|
||||||
void setCurrentRow(int row);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void refresh();
|
|
||||||
void onModelReset();
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct FieldRow {
|
|
||||||
int column = -1;
|
|
||||||
QString original; ///< Text the editor shows, only a different text is written back
|
|
||||||
QWidget *editor = nullptr;
|
|
||||||
QToolButton *expandButton = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
void clearFields();
|
|
||||||
void rebuild();
|
|
||||||
void showValue(FieldRow &field, const QString &text, bool editable);
|
|
||||||
void commitEditor(QWidget *editor);
|
|
||||||
FieldRow *findField(QWidget *editor);
|
|
||||||
|
|
||||||
QAbstractItemModel *m_model = nullptr;
|
|
||||||
int m_firstDataColumn = 0;
|
|
||||||
QPersistentModelIndex m_rowIndex;
|
|
||||||
std::vector<FieldRow> m_fields;
|
|
||||||
bool m_updating = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // RECORDPANEL_H
|
|
||||||
|
|
@ -31,12 +31,6 @@
|
||||||
</property>
|
</property>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QWidget" name="widget" native="true">
|
<widget class="QWidget" name="widget" native="true">
|
||||||
<property name="sizePolicy">
|
|
||||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
|
||||||
<horstretch>0</horstretch>
|
|
||||||
<verstretch>0</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
<property name="leftMargin">
|
<property name="leftMargin">
|
||||||
<number>4</number>
|
<number>4</number>
|
||||||
|
|
|
||||||
61
pglab/widgets/SingleRecordModel.cpp
Normal file
61
pglab/widgets/SingleRecordModel.cpp
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
#include "SingleRecordModel.h"
|
||||||
|
#include "CustomDataRole.h"
|
||||||
|
|
||||||
|
SingleRecordModel::SingleRecordModel(QObject *parent)
|
||||||
|
: QAbstractTableModel(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant SingleRecordModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||||
|
{
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
int SingleRecordModel::rowCount(const QModelIndex &parent) const
|
||||||
|
{
|
||||||
|
if (parent.isValid())
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return elems.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
int SingleRecordModel::columnCount(const QModelIndex &parent) const
|
||||||
|
{
|
||||||
|
if (parent.isValid())
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return ColCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant SingleRecordModel::data(const QModelIndex &index, int role) const
|
||||||
|
{
|
||||||
|
if (!index.isValid())
|
||||||
|
return QVariant();
|
||||||
|
|
||||||
|
const int row = index.row();
|
||||||
|
if (role == Qt::DisplayRole)
|
||||||
|
{
|
||||||
|
switch (index.column())
|
||||||
|
{
|
||||||
|
case NameCol:
|
||||||
|
return elems[row].name;
|
||||||
|
case ValueCol:
|
||||||
|
return elems[row].value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (role == CustomDataTypeRole)
|
||||||
|
{
|
||||||
|
switch (index.column())
|
||||||
|
{
|
||||||
|
case NameCol:
|
||||||
|
return Pgsql::text_oid;
|
||||||
|
case ValueCol:
|
||||||
|
return elems[row].oid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (role == CustomDataMeaningRole)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
return QVariant();
|
||||||
|
}
|
||||||
39
pglab/widgets/SingleRecordModel.h
Normal file
39
pglab/widgets/SingleRecordModel.h
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QAbstractTableModel>
|
||||||
|
#include "Pgsql_oids.h"
|
||||||
|
|
||||||
|
class SingleRecordModel : public QAbstractTableModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit SingleRecordModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
// Header:
|
||||||
|
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||||
|
|
||||||
|
// Basic functionality:
|
||||||
|
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
|
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||||
|
|
||||||
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum Columns {
|
||||||
|
NameCol,
|
||||||
|
ValueCol,
|
||||||
|
|
||||||
|
ColCount
|
||||||
|
};
|
||||||
|
|
||||||
|
class Elem {
|
||||||
|
public:
|
||||||
|
QString name;
|
||||||
|
QString value;
|
||||||
|
Oid oid;
|
||||||
|
};
|
||||||
|
using Elems = std::vector<Elem>;
|
||||||
|
Elems elems;
|
||||||
|
};
|
||||||
|
|
||||||
14
pglab/widgets/SingleRecordWidget.cpp
Normal file
14
pglab/widgets/SingleRecordWidget.cpp
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#include "SingleRecordWidget.h"
|
||||||
|
#include "ui_SingleRecordWidget.h"
|
||||||
|
|
||||||
|
SingleRecordWidget::SingleRecordWidget(QWidget *parent) :
|
||||||
|
QWidget(parent),
|
||||||
|
ui(new Ui::SingleRecordWidget)
|
||||||
|
{
|
||||||
|
ui->setupUi(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
SingleRecordWidget::~SingleRecordWidget()
|
||||||
|
{
|
||||||
|
delete ui;
|
||||||
|
}
|
||||||
20
pglab/widgets/SingleRecordWidget.h
Normal file
20
pglab/widgets/SingleRecordWidget.h
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class SingleRecordWidget;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SingleRecordWidget : public QWidget
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit SingleRecordWidget(QWidget *parent = nullptr);
|
||||||
|
~SingleRecordWidget();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Ui::SingleRecordWidget *ui;
|
||||||
|
};
|
||||||
|
|
||||||
31
pglab/widgets/SingleRecordWidget.ui
Normal file
31
pglab/widgets/SingleRecordWidget.ui
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>SingleRecordWidget</class>
|
||||||
|
<widget class="QWidget" name="SingleRecordWidget">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>500</width>
|
||||||
|
<height>679</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Form</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="PgLabTableView" name="properties"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>PgLabTableView</class>
|
||||||
|
<extends>QTableView</extends>
|
||||||
|
<header>util/PgLabTableView.h</header>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
---
|
|
||||||
features:
|
|
||||||
- |
|
|
||||||
Data grids in the table editor and in query results now have a record
|
|
||||||
panel next to the grid. It shows the column name, type and value of the
|
|
||||||
current row, long text, varchar, json, jsonb and xml values get an
|
|
||||||
expandable multi-line box. Values are editable exactly when the grid cell
|
|
||||||
is editable. The panel can be hidden with the :menuselection:`Record`
|
|
||||||
button.
|
|
||||||
|
|
@ -8,7 +8,6 @@ add_executable(IntegrationTests
|
||||||
tst_CrudModelBehavior.cpp
|
tst_CrudModelBehavior.cpp
|
||||||
${CMAKE_SOURCE_DIR}/pglab/crud/CrudModel.cpp
|
${CMAKE_SOURCE_DIR}/pglab/crud/CrudModel.cpp
|
||||||
${CMAKE_SOURCE_DIR}/pglab/crud/DataGridWidget.cpp
|
${CMAKE_SOURCE_DIR}/pglab/crud/DataGridWidget.cpp
|
||||||
${CMAKE_SOURCE_DIR}/pglab/crud/RecordPanel.cpp
|
|
||||||
${CMAKE_SOURCE_DIR}/pglab/OpenDatabase.cpp
|
${CMAKE_SOURCE_DIR}/pglab/OpenDatabase.cpp
|
||||||
${CMAKE_SOURCE_DIR}/pglab/util/PgLabItemDelegate.cpp
|
${CMAKE_SOURCE_DIR}/pglab/util/PgLabItemDelegate.cpp
|
||||||
${CMAKE_SOURCE_DIR}/pglab/util/Colors.cpp
|
${CMAKE_SOURCE_DIR}/pglab/util/Colors.cpp
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,6 @@ add_executable(pglabtests
|
||||||
tst_TypeMappings.cpp
|
tst_TypeMappings.cpp
|
||||||
tst_Range.cpp
|
tst_Range.cpp
|
||||||
ResultSourceMappingTest.cpp
|
ResultSourceMappingTest.cpp
|
||||||
tst_RecordPanel.cpp
|
|
||||||
${CMAKE_SOURCE_DIR}/pglab/crud/RecordPanel.cpp
|
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(pglabtests PRIVATE
|
target_link_libraries(pglabtests PRIVATE
|
||||||
|
|
@ -34,8 +32,6 @@ target_link_libraries(pglabtests PRIVATE
|
||||||
|
|
||||||
target_include_directories(pglabtests PRIVATE
|
target_include_directories(pglabtests PRIVATE
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}
|
${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
${CMAKE_SOURCE_DIR}/pglab
|
|
||||||
${CMAKE_SOURCE_DIR}/pglab/crud
|
|
||||||
)
|
)
|
||||||
|
|
||||||
set_target_properties(pglabtests PROPERTIES
|
set_target_properties(pglabtests PROPERTIES
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,7 @@
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
#include <QApplication>
|
|
||||||
#include <QtGlobal>
|
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
if (!qEnvironmentVariableIsSet("QT_QPA_PLATFORM"))
|
|
||||||
qputenv("QT_QPA_PLATFORM", "offscreen");
|
|
||||||
QApplication app(argc, argv);
|
|
||||||
::testing::InitGoogleTest(&argc, argv);
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
return RUN_ALL_TESTS();
|
return RUN_ALL_TESTS();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,144 +0,0 @@
|
||||||
#include <gtest/gtest.h>
|
|
||||||
|
|
||||||
#include <QLabel>
|
|
||||||
#include <QLineEdit>
|
|
||||||
#include <QPlainTextEdit>
|
|
||||||
#include <QStandardItemModel>
|
|
||||||
#include <QToolButton>
|
|
||||||
|
|
||||||
#include "CustomDataRole.h"
|
|
||||||
#include "Pgsql_oids.h"
|
|
||||||
#include "RecordPanel.h"
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
/// Column 0 is the row number column, columns 1 to 3 are data columns
|
|
||||||
/// id (int4, editable), body (text, editable), ro (int4, read-only)
|
|
||||||
void fillModel(QStandardItemModel &model)
|
|
||||||
{
|
|
||||||
model.setColumnCount(4);
|
|
||||||
model.setRowCount(1);
|
|
||||||
model.setHeaderData(0, Qt::Horizontal, QStringLiteral("#"), Qt::DisplayRole);
|
|
||||||
model.setHeaderData(1, Qt::Horizontal, QStringLiteral("id\nint4"), Qt::DisplayRole);
|
|
||||||
model.setHeaderData(2, Qt::Horizontal, QStringLiteral("body\ntext"), Qt::DisplayRole);
|
|
||||||
model.setHeaderData(3, Qt::Horizontal, QStringLiteral("ro\nint4"), Qt::DisplayRole);
|
|
||||||
model.setData(model.index(0, 1), Pgsql::int4_oid, CustomDataTypeRole);
|
|
||||||
model.setData(model.index(0, 2), Pgsql::text_oid, CustomDataTypeRole);
|
|
||||||
model.setData(model.index(0, 3), Pgsql::int4_oid, CustomDataTypeRole);
|
|
||||||
model.setData(model.index(0, 1), QStringLiteral("1"));
|
|
||||||
model.setData(model.index(0, 2), QStringLiteral("hello"));
|
|
||||||
model.setData(model.index(0, 3), QStringLiteral("7"));
|
|
||||||
model.itemFromIndex(model.index(0, 3))->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class RecordPanelTest : public ::testing::Test {
|
|
||||||
protected:
|
|
||||||
void SetUp() override
|
|
||||||
{
|
|
||||||
model.reset(new QStandardItemModel);
|
|
||||||
fillModel(*model);
|
|
||||||
panel.setModel(model.get());
|
|
||||||
panel.setFirstDataColumn(1);
|
|
||||||
panel.setCurrentRow(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unique_ptr<QStandardItemModel> model;
|
|
||||||
RecordPanel panel;
|
|
||||||
};
|
|
||||||
|
|
||||||
TEST_F(RecordPanelTest, OneFieldPerDataColumnSkippingRowNumber)
|
|
||||||
{
|
|
||||||
ASSERT_NE(panel.widget(), nullptr);
|
|
||||||
EXPECT_EQ(panel.widget()->findChildren<QLabel *>().size(), 6);
|
|
||||||
EXPECT_EQ(panel.widget()->findChildren<QPlainTextEdit *>().size(), 1);
|
|
||||||
EXPECT_EQ(panel.widget()->findChildren<QLineEdit *>().size(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(RecordPanelTest, ShowsValuesOfCurrentRow)
|
|
||||||
{
|
|
||||||
auto lines = panel.widget()->findChildren<QLineEdit *>();
|
|
||||||
auto texts = panel.widget()->findChildren<QPlainTextEdit *>();
|
|
||||||
ASSERT_EQ(lines.size(), 2);
|
|
||||||
ASSERT_EQ(texts.size(), 1);
|
|
||||||
EXPECT_EQ(lines[0]->text(), QStringLiteral("1"));
|
|
||||||
EXPECT_EQ(texts[0]->toPlainText(), QStringLiteral("hello"));
|
|
||||||
EXPECT_EQ(lines[1]->text(), QStringLiteral("7"));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(RecordPanelTest, ReadOnlyWhenCellNotEditable)
|
|
||||||
{
|
|
||||||
auto lines = panel.widget()->findChildren<QLineEdit *>();
|
|
||||||
ASSERT_EQ(lines.size(), 2);
|
|
||||||
EXPECT_FALSE(lines[0]->isReadOnly());
|
|
||||||
EXPECT_TRUE(lines[1]->isReadOnly());
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(RecordPanelTest, NullShowsEmptyTextWithNullPlaceholder)
|
|
||||||
{
|
|
||||||
model->setData(model->index(0, 1), QVariant(), Qt::EditRole);
|
|
||||||
panel.setCurrentRow(0);
|
|
||||||
|
|
||||||
auto lines = panel.widget()->findChildren<QLineEdit *>();
|
|
||||||
ASSERT_EQ(lines.size(), 2);
|
|
||||||
EXPECT_TRUE(lines[0]->text().isEmpty());
|
|
||||||
EXPECT_EQ(lines[0]->placeholderText(), QStringLiteral("null"));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(RecordPanelTest, EmptyStringShownAsQuotedEmpty)
|
|
||||||
{
|
|
||||||
model->setData(model->index(0, 2), QString(""), Qt::EditRole);
|
|
||||||
panel.setCurrentRow(0);
|
|
||||||
|
|
||||||
auto texts = panel.widget()->findChildren<QPlainTextEdit *>();
|
|
||||||
ASSERT_EQ(texts.size(), 1);
|
|
||||||
EXPECT_EQ(texts[0]->toPlainText(), QStringLiteral("''"));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(RecordPanelTest, EditingFinishedWritesThroughModel)
|
|
||||||
{
|
|
||||||
auto lines = panel.widget()->findChildren<QLineEdit *>();
|
|
||||||
ASSERT_EQ(lines.size(), 2);
|
|
||||||
lines[0]->setText(QStringLiteral("42"));
|
|
||||||
emit lines[0]->editingFinished();
|
|
||||||
|
|
||||||
EXPECT_EQ(model->index(0, 1).data(Qt::EditRole).toString(), QStringLiteral("42"));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(RecordPanelTest, DataChangedRefreshesEditor)
|
|
||||||
{
|
|
||||||
model->setData(model->index(0, 1), QStringLiteral("5"), Qt::EditRole);
|
|
||||||
|
|
||||||
auto lines = panel.widget()->findChildren<QLineEdit *>();
|
|
||||||
ASSERT_EQ(lines.size(), 2);
|
|
||||||
EXPECT_EQ(lines[0]->text(), QStringLiteral("5"));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(RecordPanelTest, NoCurrentRowShowsNothing)
|
|
||||||
{
|
|
||||||
panel.setCurrentRow(-1);
|
|
||||||
EXPECT_EQ(panel.widget(), nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(RecordPanelTest, ModelResetClearsPanel)
|
|
||||||
{
|
|
||||||
model->clear();
|
|
||||||
EXPECT_EQ(panel.widget(), nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_F(RecordPanelTest, ExpandButtonGrowsTextBox)
|
|
||||||
{
|
|
||||||
auto texts = panel.widget()->findChildren<QPlainTextEdit *>();
|
|
||||||
auto buttons = panel.widget()->findChildren<QToolButton *>();
|
|
||||||
ASSERT_EQ(texts.size(), 1);
|
|
||||||
ASSERT_EQ(buttons.size(), 1);
|
|
||||||
|
|
||||||
int collapsed = texts[0]->maximumHeight();
|
|
||||||
buttons[0]->setChecked(true);
|
|
||||||
int expanded = texts[0]->maximumHeight();
|
|
||||||
EXPECT_GT(expanded, collapsed);
|
|
||||||
|
|
||||||
buttons[0]->setChecked(false);
|
|
||||||
EXPECT_EQ(texts[0]->maximumHeight(), collapsed);
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue