Compare commits

..

3 commits

Author SHA1 Message Date
eelke
70f4601aa9 Remove old code 2026-10-20 19:06:38 +02:00
eelke
04ce099adf Add recordpanel beside datagrid 2026-10-20 19:06:18 +02:00
eelke
d178d5191c ignore .junie folder 2026-10-20 18:06:03 +02:00
18 changed files with 540 additions and 177 deletions

1
.gitignore vendored
View file

@ -13,3 +13,4 @@ pglabAll.pro.user.4.8-pre1
/pglabAll.pro.user*
/public/
/Testing/Temporary
.junie/

View file

@ -31,6 +31,7 @@ set(PGLAB_SOURCES
crud/CrudModel.cpp
crud/CrudTab.cpp
crud/DataGridWidget.cpp
crud/RecordPanel.cpp
querytool/QueryExplainModel.cpp
querytool/QueryParamListController.cpp
querytool/QueryTool.cpp
@ -71,8 +72,6 @@ set(PGLAB_SOURCES
EditTableWidget.cpp
EditColumnTableModel.cpp
DatabaseWindow.cpp
widgets/SingleRecordModel.cpp
widgets/SingleRecordWidget.cpp
)
set(PGLAB_FORMS
@ -84,7 +83,6 @@ set(PGLAB_FORMS
ProcessStdioWidget.ui
crud/CrudTab.ui
CodeGenerator.ui
widgets/SingleRecordWidget.ui
)
add_executable(pglab

View file

@ -31,6 +31,9 @@ public:
/// 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);
/// 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;
@ -62,10 +65,6 @@ signals:
private:
/// Number of grid columns in front of the result columns (the row number column)
static constexpr int PreColumnCount = 1;
class ColumnSort {
public:
enum Direction { Ascending, Descending };

View file

@ -1,10 +1,13 @@
#include "DataGridWidget.h"
#include "CrudModel.h"
#include "CustomDataRole.h"
#include "RecordPanel.h"
#include "ResultTableModelUtil.h"
#include "util/PgLabItemDelegate.h"
#include <QAction>
#include <QItemSelectionModel>
#include <QSortFilterProxyModel>
#include <QSplitter>
#include <QStyle>
#include <QTableView>
#include <QToolBar>
@ -36,12 +39,28 @@ DataGridWidget::DataGridWidget(QWidget *parent)
m_toolbar->addAction(m_actRevert);
connect(m_actRevert, &QAction::triggered, this, &DataGridWidget::onRevert);
m_view = new QTableView(this);
layout->addWidget(m_view);
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);
SetTableViewDefault(m_view);
auto delegate = new PgLabItemDelegate(m_view);
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)
@ -51,13 +70,18 @@ void DataGridWidget::setSourceModel(QAbstractItemModel *model)
m_proxy->setSortRole(CustomSortRole);
}
disconnect(m_pendingChangesConnection);
disconnect(m_currentRowConnection);
m_proxy->setSourceModel(model);
m_view->setModel(m_proxy);
m_view->setSortingEnabled(true);
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);
m_toolbar->setVisible(crud != nullptr);
m_actSave->setVisible(crud != nullptr);
m_actRevert->setVisible(crud != nullptr);
if (crud) {
m_pendingChangesConnection = connect(crud, &CrudModel::pendingChangesChanged, this, &DataGridWidget::onPendingChangesChanged);
onPendingChangesChanged(crud->hasPendingChanges());
@ -67,6 +91,11 @@ void DataGridWidget::setSourceModel(QAbstractItemModel *model)
}
}
void DataGridWidget::onCurrentRowChanged(const QModelIndex &current)
{
m_recordPanel->setCurrentRow(current.row());
}
void DataGridWidget::onPendingChangesChanged(bool hasChanges)
{
m_actSave->setEnabled(hasChanges);

View file

@ -5,9 +5,12 @@
class QAbstractItemModel;
class QAction;
class QModelIndex;
class QSortFilterProxyModel;
class QSplitter;
class QTableView;
class QToolBar;
class RecordPanel;
/** Sortable and editable grid shared by the CRUD tab and the query result tab.
*/
@ -26,6 +29,7 @@ private slots:
void onPendingChangesChanged(bool hasChanges);
void onSave();
void onRevert();
void onCurrentRowChanged(const QModelIndex &current);
private:
/// Closes an open cell editor so its value is written to the model before saving or reverting
@ -33,10 +37,14 @@ private:
QTableView *m_view = nullptr;
QSortFilterProxyModel *m_proxy = nullptr;
QSplitter *m_splitter = nullptr;
RecordPanel *m_recordPanel = nullptr;
QToolBar *m_toolbar = nullptr;
QAction *m_actSave = nullptr;
QAction *m_actRevert = nullptr;
QAction *m_actRecord = nullptr;
QMetaObject::Connection m_pendingChangesConnection;
QMetaObject::Connection m_currentRowConnection;
};
#endif // DATAGRIDWIDGET_H

268
pglab/crud/RecordPanel.cpp Normal file
View file

@ -0,0 +1,268 @@
#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);
}

55
pglab/crud/RecordPanel.h Normal file
View file

@ -0,0 +1,55 @@
#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

View file

@ -31,6 +31,12 @@
</property>
<item>
<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">
<property name="leftMargin">
<number>4</number>

View file

@ -1,61 +0,0 @@
#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();
}

View file

@ -1,39 +0,0 @@
#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;
};

View file

@ -1,14 +0,0 @@
#include "SingleRecordWidget.h"
#include "ui_SingleRecordWidget.h"
SingleRecordWidget::SingleRecordWidget(QWidget *parent) :
QWidget(parent),
ui(new Ui::SingleRecordWidget)
{
ui->setupUi(this);
}
SingleRecordWidget::~SingleRecordWidget()
{
delete ui;
}

View file

@ -1,20 +0,0 @@
#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;
};

View file

@ -1,31 +0,0 @@
<?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>

View file

@ -0,0 +1,9 @@
---
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.

View file

@ -8,6 +8,7 @@ add_executable(IntegrationTests
tst_CrudModelBehavior.cpp
${CMAKE_SOURCE_DIR}/pglab/crud/CrudModel.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/util/PgLabItemDelegate.cpp
${CMAKE_SOURCE_DIR}/pglab/util/Colors.cpp

View file

@ -18,6 +18,8 @@ add_executable(pglabtests
tst_TypeMappings.cpp
tst_Range.cpp
ResultSourceMappingTest.cpp
tst_RecordPanel.cpp
${CMAKE_SOURCE_DIR}/pglab/crud/RecordPanel.cpp
)
target_link_libraries(pglabtests PRIVATE
@ -32,6 +34,8 @@ target_link_libraries(pglabtests PRIVATE
target_include_directories(pglabtests PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/pglab
${CMAKE_SOURCE_DIR}/pglab/crud
)
set_target_properties(pglabtests PROPERTIES

View file

@ -1,7 +1,13 @@
#include <gtest/gtest.h>
#include <gtest/gtest.h>
#include <QApplication>
#include <QtGlobal>
int main(int argc, char *argv[])
{
if (!qEnvironmentVariableIsSet("QT_QPA_PLATFORM"))
qputenv("QT_QPA_PLATFORM", "offscreen");
QApplication app(argc, argv);
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View file

@ -0,0 +1,144 @@
#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);
}