302 lines
11 KiB
C++
302 lines
11 KiB
C++
#include "props/EntityPropertySection.h"
|
||
|
||
#include "params/ParamControls.h"
|
||
|
||
#include <QDoubleSpinBox>
|
||
#include <QFormLayout>
|
||
#include <QHBoxLayout>
|
||
#include <QLabel>
|
||
#include <QLineEdit>
|
||
#include <QListWidget>
|
||
#include <QPushButton>
|
||
#include <QTextEdit>
|
||
#include <QTimer>
|
||
#include <QToolButton>
|
||
#include <QVBoxLayout>
|
||
|
||
namespace gui {
|
||
|
||
EntityPropertySection::EntityPropertySection(QWidget* parent)
|
||
: PropertySectionWidget(parent) {
|
||
auto* lay = new QVBoxLayout(this);
|
||
lay->setContentsMargins(0, 0, 0, 0);
|
||
lay->setSpacing(6);
|
||
|
||
auto* form = new QFormLayout();
|
||
form->setContentsMargins(0, 0, 0, 0);
|
||
form->setSpacing(6);
|
||
|
||
m_name = new QLineEdit(this);
|
||
m_name->setPlaceholderText(QStringLiteral("显示名称"));
|
||
m_name->setToolTip(QStringLiteral("仅显示用;内部 id 不变"));
|
||
form->addRow(QStringLiteral("名称"), m_name);
|
||
|
||
m_depth = new QLabel(QStringLiteral("-"), this);
|
||
m_distScale = new QLabel(QStringLiteral("-"), this);
|
||
for (QLabel* lab : {m_depth, m_distScale}) {
|
||
lab->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||
}
|
||
form->addRow(QStringLiteral("深度"), m_depth);
|
||
form->addRow(QStringLiteral("距离缩放"), m_distScale);
|
||
|
||
m_pivot = new Vec2ParamControl(this);
|
||
m_pivot->setToolTip(QStringLiteral("枢轴在世界坐标中的位置(限制在轮廓包络内),用于重定位局部原点"));
|
||
form->addRow(QStringLiteral("中心坐标"), m_pivot);
|
||
|
||
m_centroid = new Vec2ParamControl(this);
|
||
m_centroid->setToolTip(QStringLiteral("实体几何质心的世界坐标;修改将整体平移实体"));
|
||
form->addRow(QStringLiteral("位置"), m_centroid);
|
||
|
||
m_userScale = new QDoubleSpinBox(this);
|
||
m_userScale->setRange(0.05, 20.0);
|
||
m_userScale->setDecimals(3);
|
||
m_userScale->setSingleStep(0.05);
|
||
m_userScale->setValue(1.0);
|
||
m_userScale->setToolTip(QStringLiteral("人为整体缩放,与深度距离缩放相乘"));
|
||
form->addRow(QStringLiteral("整体缩放"), m_userScale);
|
||
|
||
lay->addLayout(form);
|
||
|
||
m_introHeader = new QWidget(this);
|
||
auto* headLay = new QHBoxLayout(m_introHeader);
|
||
headLay->setContentsMargins(0, 2, 0, 2);
|
||
headLay->setSpacing(6);
|
||
m_introToggle = new QToolButton(m_introHeader);
|
||
m_introToggle->setAutoRaise(true);
|
||
m_introToggle->setToolButtonStyle(Qt::ToolButtonTextOnly);
|
||
m_introToggle->setCursor(Qt::PointingHandCursor);
|
||
m_introToggle->setToolTip(QStringLiteral("展开或折叠介绍设置"));
|
||
m_introToggle->setText(QStringLiteral("▶"));
|
||
m_introToggle->setFixedWidth(22);
|
||
auto* introTitleLab = new QLabel(QStringLiteral("介绍"), m_introHeader);
|
||
introTitleLab->setStyleSheet(QStringLiteral("QLabel { font-weight: 600; }"));
|
||
headLay->addWidget(m_introToggle, 0, Qt::AlignVCenter);
|
||
headLay->addWidget(introTitleLab, 1, Qt::AlignVCenter);
|
||
|
||
m_introContent = new QWidget(this);
|
||
auto* introLay = new QVBoxLayout(m_introContent);
|
||
introLay->setContentsMargins(8, 4, 0, 0);
|
||
introLay->setSpacing(6);
|
||
|
||
m_introTitle = new QLineEdit(m_introContent);
|
||
m_introTitle->setPlaceholderText(QStringLiteral("标题"));
|
||
introLay->addWidget(new QLabel(QStringLiteral("标题"), m_introContent));
|
||
introLay->addWidget(m_introTitle);
|
||
|
||
m_introBody = new QTextEdit(m_introContent);
|
||
m_introBody->setPlaceholderText(QStringLiteral("正文(纯文本)"));
|
||
m_introBody->setMaximumHeight(100);
|
||
introLay->addWidget(new QLabel(QStringLiteral("正文"), m_introContent));
|
||
introLay->addWidget(m_introBody);
|
||
|
||
m_introImages = new QListWidget(m_introContent);
|
||
m_introImages->setMinimumHeight(72);
|
||
m_introImages->setToolTip(QStringLiteral("配图相对路径列表;使用下方按钮从文件添加"));
|
||
introLay->addWidget(new QLabel(QStringLiteral("配图"), m_introContent));
|
||
introLay->addWidget(m_introImages);
|
||
|
||
auto* imgRow = new QHBoxLayout();
|
||
m_btnIntroAddImage = new QPushButton(QStringLiteral("添加配图…"), m_introContent);
|
||
m_btnIntroRemoveImage = new QPushButton(QStringLiteral("移除选中"), m_introContent);
|
||
imgRow->addWidget(m_btnIntroAddImage);
|
||
imgRow->addWidget(m_btnIntroRemoveImage);
|
||
introLay->addLayout(imgRow);
|
||
|
||
m_introVideo = new QLineEdit(m_introContent);
|
||
m_introVideo->setPlaceholderText(QStringLiteral("可选:视频相对路径(如 assets/entities/xxx.mp4)"));
|
||
introLay->addWidget(new QLabel(QStringLiteral("视频路径(预留)"), m_introContent));
|
||
introLay->addWidget(m_introVideo);
|
||
|
||
lay->addWidget(m_introHeader);
|
||
lay->addWidget(m_introContent);
|
||
lay->addStretch(1);
|
||
|
||
m_introContent->setVisible(false);
|
||
|
||
m_introSaveTimer = new QTimer(this);
|
||
m_introSaveTimer->setSingleShot(true);
|
||
connect(m_introSaveTimer, &QTimer::timeout, this, [this]() {
|
||
if (!m_introBulkUpdate) {
|
||
emit introContentEdited();
|
||
}
|
||
});
|
||
|
||
connect(m_introToggle, &QToolButton::clicked, this, [this]() {
|
||
setIntroSectionExpanded(!m_introContent->isVisible());
|
||
});
|
||
|
||
connect(m_name, &QLineEdit::editingFinished, this, [this]() {
|
||
if (m_name) {
|
||
emit displayNameCommitted(m_name->text());
|
||
}
|
||
});
|
||
connect(m_pivot, &Vec2ParamControl::valueChanged, this, &EntityPropertySection::pivotEdited);
|
||
connect(m_centroid, &Vec2ParamControl::valueChanged, this, &EntityPropertySection::centroidEdited);
|
||
connect(m_userScale, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &EntityPropertySection::userScaleEdited);
|
||
|
||
connect(m_introTitle, &QLineEdit::textChanged, this, [this](const QString&) { scheduleIntroPersist(); });
|
||
connect(m_introBody, &QTextEdit::textChanged, this, [this]() { scheduleIntroPersist(); });
|
||
connect(m_introVideo, &QLineEdit::textChanged, this, [this](const QString&) { scheduleIntroPersist(); });
|
||
|
||
connect(m_btnIntroAddImage, &QPushButton::clicked, this, &EntityPropertySection::introAddImageRequested);
|
||
connect(m_btnIntroRemoveImage, &QPushButton::clicked, this, [this]() {
|
||
if (!m_introImages || !m_introImages->currentItem()) {
|
||
return;
|
||
}
|
||
delete m_introImages->takeItem(m_introImages->currentRow());
|
||
scheduleIntroPersist();
|
||
});
|
||
}
|
||
|
||
void EntityPropertySection::setIntroSectionExpanded(bool expanded) {
|
||
if (!m_introContent || !m_introToggle) {
|
||
return;
|
||
}
|
||
m_introContent->setVisible(expanded);
|
||
m_introToggle->setText(expanded ? QStringLiteral("▼") : QStringLiteral("▶"));
|
||
}
|
||
|
||
void EntityPropertySection::scheduleIntroPersist() {
|
||
if (m_introBulkUpdate || !m_introSaveTimer) {
|
||
return;
|
||
}
|
||
m_introSaveTimer->start(400);
|
||
}
|
||
|
||
void EntityPropertySection::clearDisconnected() {
|
||
if (m_introSaveTimer) {
|
||
m_introSaveTimer->stop();
|
||
}
|
||
m_introBulkUpdate = true;
|
||
setEditingEnabled(false);
|
||
if (m_name) {
|
||
m_name->blockSignals(true);
|
||
m_name->clear();
|
||
m_name->blockSignals(false);
|
||
}
|
||
if (m_depth) m_depth->setText(QStringLiteral("-"));
|
||
if (m_distScale) m_distScale->setText(QStringLiteral("-"));
|
||
if (m_pivot) m_pivot->setValue(0.0, 0.0);
|
||
if (m_centroid) m_centroid->setValue(0.0, 0.0);
|
||
if (m_userScale) {
|
||
m_userScale->blockSignals(true);
|
||
m_userScale->setValue(1.0);
|
||
m_userScale->blockSignals(false);
|
||
}
|
||
if (m_introTitle) {
|
||
m_introTitle->blockSignals(true);
|
||
m_introTitle->clear();
|
||
m_introTitle->blockSignals(false);
|
||
}
|
||
if (m_introBody) {
|
||
m_introBody->blockSignals(true);
|
||
m_introBody->clear();
|
||
m_introBody->blockSignals(false);
|
||
}
|
||
if (m_introVideo) {
|
||
m_introVideo->blockSignals(true);
|
||
m_introVideo->clear();
|
||
m_introVideo->blockSignals(false);
|
||
}
|
||
if (m_introImages) m_introImages->clear();
|
||
setIntroSectionExpanded(false);
|
||
m_introBulkUpdate = false;
|
||
}
|
||
|
||
void EntityPropertySection::applyState(const EntityPropertyUiState& s) {
|
||
if (m_introSaveTimer) {
|
||
m_introSaveTimer->stop();
|
||
}
|
||
m_introBulkUpdate = true;
|
||
|
||
setEditingEnabled(true);
|
||
if (m_name) {
|
||
m_name->blockSignals(true);
|
||
m_name->setText(s.displayName);
|
||
m_name->blockSignals(false);
|
||
}
|
||
if (m_depth) m_depth->setText(QString::number(s.depthZ));
|
||
if (m_distScale) m_distScale->setText(s.distanceScaleText);
|
||
if (m_pivot) m_pivot->setValue(s.pivot.x(), s.pivot.y());
|
||
if (m_centroid) m_centroid->setValue(s.centroid.x(), s.centroid.y());
|
||
if (m_userScale) {
|
||
m_userScale->blockSignals(true);
|
||
m_userScale->setValue(s.userScale);
|
||
m_userScale->blockSignals(false);
|
||
}
|
||
if (m_introTitle) {
|
||
m_introTitle->blockSignals(true);
|
||
m_introTitle->setText(s.intro.title);
|
||
m_introTitle->blockSignals(false);
|
||
}
|
||
if (m_introBody) {
|
||
m_introBody->blockSignals(true);
|
||
m_introBody->setPlainText(s.intro.bodyText);
|
||
m_introBody->blockSignals(false);
|
||
}
|
||
if (m_introVideo) {
|
||
m_introVideo->blockSignals(true);
|
||
m_introVideo->setText(s.intro.videoPathRelative);
|
||
m_introVideo->blockSignals(false);
|
||
}
|
||
if (m_introImages) {
|
||
m_introImages->clear();
|
||
for (const QString& p : s.intro.imagePathsRelative) {
|
||
if (!p.isEmpty()) {
|
||
auto* it = new QListWidgetItem(p, m_introImages);
|
||
it->setData(Qt::UserRole, p);
|
||
}
|
||
}
|
||
}
|
||
setIntroSectionExpanded(false);
|
||
m_introBulkUpdate = false;
|
||
}
|
||
|
||
void EntityPropertySection::appendIntroImagePath(const QString& relativePath) {
|
||
if (!m_introImages || relativePath.isEmpty()) {
|
||
return;
|
||
}
|
||
auto* it = new QListWidgetItem(relativePath, m_introImages);
|
||
it->setData(Qt::UserRole, relativePath);
|
||
scheduleIntroPersist();
|
||
}
|
||
|
||
core::EntityIntroContent EntityPropertySection::introSnapshot() const {
|
||
core::EntityIntroContent out;
|
||
if (m_introTitle) {
|
||
out.title = m_introTitle->text();
|
||
}
|
||
if (m_introBody) {
|
||
out.bodyText = m_introBody->toPlainText();
|
||
}
|
||
if (m_introVideo) {
|
||
out.videoPathRelative = m_introVideo->text().trimmed();
|
||
}
|
||
if (m_introImages) {
|
||
for (int i = 0; i < m_introImages->count(); ++i) {
|
||
const QString p = m_introImages->item(i)->data(Qt::UserRole).toString();
|
||
if (!p.isEmpty()) {
|
||
out.imagePathsRelative.push_back(p);
|
||
}
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
void EntityPropertySection::setEditingEnabled(bool on) {
|
||
if (m_name) m_name->setEnabled(on);
|
||
if (m_pivot) m_pivot->setEnabled(on);
|
||
if (m_centroid) m_centroid->setEnabled(on);
|
||
if (m_userScale) m_userScale->setEnabled(on);
|
||
if (m_introHeader) m_introHeader->setEnabled(on);
|
||
if (m_introToggle) m_introToggle->setEnabled(on);
|
||
if (m_introTitle) m_introTitle->setEnabled(on);
|
||
if (m_introBody) m_introBody->setEnabled(on);
|
||
if (m_introVideo) m_introVideo->setEnabled(on);
|
||
if (m_introImages) m_introImages->setEnabled(on);
|
||
if (m_btnIntroAddImage) m_btnIntroAddImage->setEnabled(on);
|
||
if (m_btnIntroRemoveImage) m_btnIntroRemoveImage->setEnabled(on);
|
||
}
|
||
|
||
} // namespace gui
|