#include "RecordPanel.h" #include "CustomDataRole.h" #include "Pgsql_oids.h" #include #include #include #include #include #include #include #include #include #include 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(editor)) return line->text(); if (auto plain = qobject_cast(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(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 outer = new QVBoxLayout(content); outer->setContentsMargins(0, 0, 0, 0); auto form = new QWidget(content); auto layout = new QFormLayout(form); 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(form); 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(form); 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(form); 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); } outer->addWidget(form); outer->addStretch(1); setWidget(content); } void RecordPanel::showValue(FieldRow &field, const QString &text, bool editable) { QWidget *editor = field.editor; if (auto line = qobject_cast(editor)) line->setReadOnly(!editable); else if (auto plain = qobject_cast(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(editor)) line->setText(text); else if (auto plain = qobject_cast(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(watched)) commitEditor(edit); } return QScrollArea::eventFilter(watched, event); }