269 lines
7.1 KiB
C++
269 lines
7.1 KiB
C++
|
|
#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);
|
||
|
|
}
|