431 lines
17 KiB
C++
431 lines
17 KiB
C++
#include "props/EntityPropertySection.h"
|
||
|
||
#include "params/ParamControls.h"
|
||
|
||
#include <QDoubleSpinBox>
|
||
|
||
#include "widgets/CompactNumericSpinBox.h"
|
||
#include "widgets/PropertyPanelControls.h"
|
||
#include <QFormLayout>
|
||
#include <QFrame>
|
||
#include <QHBoxLayout>
|
||
#include <QCheckBox>
|
||
#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);
|
||
polishPropertyVBoxLayout(lay);
|
||
|
||
auto* form = new QFormLayout();
|
||
polishPropertyFormLayout(form);
|
||
|
||
m_name = new PropertyPanelLineEdit(this);
|
||
m_name->setPlaceholderText(QStringLiteral("显示名称"));
|
||
m_name->setToolTip({});
|
||
polishPropertyStretchyTextField(m_name);
|
||
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);
|
||
lab->setWordWrap(true);
|
||
}
|
||
form->addRow(QStringLiteral("深度"), m_depth);
|
||
form->addRow(QStringLiteral("距离缩放"), m_distScale);
|
||
|
||
m_centerLabel = new QLabel(QStringLiteral("中心"), this);
|
||
m_centerPosition = new Vec2DoubleParamControl(this);
|
||
m_centerPosition->setToolTip(
|
||
QStringLiteral("无父级:形心世界坐标。有父级:形心相对父级形心的偏移。"));
|
||
form->addRow(m_centerLabel, m_centerPosition);
|
||
|
||
m_relativeLabel = new QLabel(QStringLiteral("相对位置"), this);
|
||
m_relativeToOwnCenter = new Vec2DoubleParamControl(this);
|
||
m_relativeToOwnCenter->setToolTip(QStringLiteral("变换原点相对自身形心的偏移"));
|
||
form->addRow(m_relativeLabel, m_relativeToOwnCenter);
|
||
|
||
m_priority = new CompactIntSpinBox(this, 72);
|
||
m_priority->setRange(-1000000, 1000000);
|
||
m_priority->setSingleStep(1);
|
||
m_priority->setAccelerated(true);
|
||
m_priority->setToolTip(QStringLiteral("优先级越高越靠上。背景为 0,实体默认 1;同优先级按距离缩放/深度决定覆盖关系。"));
|
||
form->addRow(QStringLiteral("优先级"), m_priority);
|
||
|
||
m_userScale = new CompactDoubleSpinBox(this, 56);
|
||
m_userScale->setRange(0.05, 20.0);
|
||
m_userScale->setDecimals(3);
|
||
m_userScale->setSingleStep(0.05);
|
||
m_userScale->setValue(1.0);
|
||
m_userScale->setToolTip({});
|
||
form->addRow(QStringLiteral("整体缩放"), m_userScale);
|
||
|
||
m_ignoreDistanceScale = new PropertyPanelCheckBox(QStringLiteral("不受距离缩放影响"), this);
|
||
m_ignoreDistanceScale->setToolTip({});
|
||
form->addRow(QStringLiteral("距离缩放"), m_ignoreDistanceScale);
|
||
|
||
m_visible = new PropertyPanelCheckBox(QString(), this);
|
||
m_visible->setChecked(true);
|
||
m_visible->setToolTip({});
|
||
form->addRow(QStringLiteral("可见性"), m_visible);
|
||
|
||
m_spritePreview = new QLabel(this);
|
||
// 贴图预览必须稳定占位:若用 setPixmap(pm.scaled(label->size())),QLabel 的 sizeHint 会被 pixmap 反向撑大,
|
||
// 再触发布局扩张,形成“拖动刷新属性 → 预览越来越大”的正反馈。
|
||
// 因此固定预览区域尺寸,pixmap 仅按该尺寸缩放。
|
||
constexpr int kSpritePreviewW = 200;
|
||
constexpr int kSpritePreviewH = 120;
|
||
m_spritePreview->setFixedSize(kSpritePreviewW, kSpritePreviewH);
|
||
m_spritePreview->setFrameShape(QFrame::StyledPanel);
|
||
m_spritePreview->setAlignment(Qt::AlignCenter);
|
||
m_spritePreview->setText(QStringLiteral("无图像"));
|
||
form->addRow(QStringLiteral("贴图"), m_spritePreview);
|
||
|
||
m_btnEditSprite = new PropertyPanelPushButton(QStringLiteral("编辑"), this);
|
||
m_btnEditSprite->setToolTip(QStringLiteral("编辑当前帧贴图/逐帧动画"));
|
||
form->addRow(QString(), m_btnEditSprite);
|
||
|
||
lay->addLayout(form);
|
||
|
||
auto* sep = new QFrame(this);
|
||
sep->setObjectName(QStringLiteral("PropertySectionSeparator"));
|
||
sep->setFrameShape(QFrame::HLine);
|
||
lay->addWidget(sep);
|
||
|
||
m_introHeader = new QWidget(this);
|
||
polishPropertyStretchyTextField(m_introHeader);
|
||
auto* headLay = new QHBoxLayout(m_introHeader);
|
||
headLay->setContentsMargins(0, 6, 0, 2);
|
||
headLay->setSpacing(4);
|
||
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);
|
||
m_introToggle->setFixedHeight(22);
|
||
auto* introTitleLab = new QLabel(QStringLiteral("介绍"), m_introHeader);
|
||
introTitleLab->setObjectName(QStringLiteral("PropertyPanelHeading"));
|
||
headLay->addWidget(m_introToggle, 0, Qt::AlignVCenter);
|
||
headLay->addWidget(introTitleLab, 1, Qt::AlignVCenter);
|
||
|
||
m_introContent = new QWidget(this);
|
||
polishPropertyStretchyTextField(m_introContent);
|
||
auto* introLay = new QVBoxLayout(m_introContent);
|
||
polishPropertyVBoxLayout(introLay);
|
||
introLay->setContentsMargins(0, 4, 0, 0);
|
||
|
||
m_introTitle = new PropertyPanelLineEdit(m_introContent);
|
||
m_introTitle->setPlaceholderText(QStringLiteral("标题"));
|
||
introLay->addWidget(new QLabel(QStringLiteral("标题"), m_introContent));
|
||
introLay->addWidget(m_introTitle);
|
||
|
||
m_introBody = new PropertyPanelTextEdit(m_introContent);
|
||
m_introBody->setPlaceholderText(QStringLiteral("正文"));
|
||
m_introBody->setMaximumHeight(80);
|
||
introLay->addWidget(new QLabel(QStringLiteral("正文"), m_introContent));
|
||
introLay->addWidget(m_introBody);
|
||
|
||
m_introImages = new QListWidget(m_introContent);
|
||
m_introImages->setObjectName(QStringLiteral("PropertyPanelListWidget"));
|
||
m_introImages->setMinimumHeight(56);
|
||
m_introImages->setToolTip({});
|
||
m_introImages->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||
m_introImages->setWordWrap(true);
|
||
introLay->addWidget(new QLabel(QStringLiteral("配图"), m_introContent));
|
||
introLay->addWidget(m_introImages);
|
||
|
||
m_btnIntroAddImage = new PropertyPanelPushButton(QStringLiteral("添加"), m_introContent);
|
||
m_btnIntroRemoveImage = new PropertyPanelPushButton(QStringLiteral("删除"), m_introContent);
|
||
m_btnIntroAddImage->setFixedWidth(52);
|
||
m_btnIntroRemoveImage->setFixedWidth(52);
|
||
m_btnIntroAddImage->setToolTip(QStringLiteral("添加配图"));
|
||
m_btnIntroRemoveImage->setToolTip(QStringLiteral("删除当前选中的配图"));
|
||
auto* imgRow = new QHBoxLayout();
|
||
imgRow->setContentsMargins(0, 0, 0, 0);
|
||
imgRow->setSpacing(8);
|
||
imgRow->addWidget(m_btnIntroAddImage, 0);
|
||
imgRow->addWidget(m_btnIntroRemoveImage, 0);
|
||
imgRow->addStretch(1);
|
||
introLay->addLayout(imgRow);
|
||
|
||
m_introVideo = new PropertyPanelLineEdit(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_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());
|
||
});
|
||
setIntroSectionExpanded(true);
|
||
|
||
connect(m_name, &QLineEdit::editingFinished, this, [this]() {
|
||
if (m_name) {
|
||
emit displayNameCommitted(m_name->text());
|
||
}
|
||
});
|
||
connect(m_centerPosition, &Vec2DoubleParamControl::valueChanged, this, &EntityPropertySection::centerPositionEdited);
|
||
connect(m_relativeToOwnCenter, &Vec2DoubleParamControl::valueChanged, this,
|
||
&EntityPropertySection::relativeToOwnCenterEdited);
|
||
connect(m_priority, qOverload<int>(&QSpinBox::valueChanged), this, &EntityPropertySection::priorityEdited);
|
||
connect(m_userScale, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &EntityPropertySection::userScaleEdited);
|
||
connect(m_ignoreDistanceScale, &QCheckBox::toggled, this, &EntityPropertySection::ignoreDistanceScaleToggled);
|
||
connect(m_visible, &QCheckBox::toggled, this, &EntityPropertySection::visibleToggled);
|
||
connect(m_btnEditSprite, &QPushButton::clicked, this, &EntityPropertySection::spriteEditRequested);
|
||
|
||
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_centerPosition) m_centerPosition->setValue(0, 0);
|
||
if (m_relativeToOwnCenter) m_relativeToOwnCenter->setValue(0, 0);
|
||
if (m_priority) {
|
||
m_priority->blockSignals(true);
|
||
m_priority->setValue(1);
|
||
m_priority->blockSignals(false);
|
||
}
|
||
if (m_centerLabel) m_centerLabel->setText(QStringLiteral("中心"));
|
||
if (m_relativeLabel) m_relativeLabel->setText(QStringLiteral("相对位置"));
|
||
if (m_userScale) {
|
||
m_userScale->blockSignals(true);
|
||
m_userScale->setValue(1.0);
|
||
m_userScale->blockSignals(false);
|
||
}
|
||
if (m_ignoreDistanceScale) {
|
||
m_ignoreDistanceScale->blockSignals(true);
|
||
m_ignoreDistanceScale->setChecked(false);
|
||
m_ignoreDistanceScale->blockSignals(false);
|
||
}
|
||
if (m_visible) {
|
||
m_visible->blockSignals(true);
|
||
m_visible->setChecked(true);
|
||
m_visible->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();
|
||
if (m_spritePreview) {
|
||
m_spritePreview->setPixmap(QPixmap());
|
||
m_spritePreview->setText(QStringLiteral("无图像"));
|
||
}
|
||
setIntroSectionExpanded(true);
|
||
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_centerLabel) {
|
||
m_centerLabel->setText(s.hasParent ? QStringLiteral("相对中心") : QStringLiteral("世界中心"));
|
||
}
|
||
if (m_relativeLabel) {
|
||
m_relativeLabel->setText(QStringLiteral("相对位置"));
|
||
}
|
||
if (m_centerPosition && !m_centerPosition->isActivelyEditing()) {
|
||
m_centerPosition->setValue(s.centerPosition.x(), s.centerPosition.y());
|
||
}
|
||
if (m_relativeToOwnCenter && !m_relativeToOwnCenter->isActivelyEditing()) {
|
||
m_relativeToOwnCenter->setValue(s.relativeToOwnCenter.x(), s.relativeToOwnCenter.y());
|
||
}
|
||
if (m_priority) {
|
||
m_priority->blockSignals(true);
|
||
m_priority->setValue(s.priority);
|
||
m_priority->blockSignals(false);
|
||
}
|
||
if (m_userScale) {
|
||
m_userScale->blockSignals(true);
|
||
m_userScale->setValue(s.userScale);
|
||
m_userScale->blockSignals(false);
|
||
}
|
||
if (m_ignoreDistanceScale) {
|
||
m_ignoreDistanceScale->blockSignals(true);
|
||
m_ignoreDistanceScale->setChecked(s.ignoreDistanceScale);
|
||
m_ignoreDistanceScale->blockSignals(false);
|
||
}
|
||
if (m_visible) {
|
||
m_visible->blockSignals(true);
|
||
m_visible->setChecked(s.visible);
|
||
m_visible->blockSignals(false);
|
||
}
|
||
if (m_spritePreview && !s.spritePreview.isNull()) {
|
||
const QSize target = m_spritePreview->contentsRect().size().isValid()
|
||
? m_spritePreview->contentsRect().size()
|
||
: m_spritePreview->size();
|
||
const quint64 key = static_cast<quint64>(s.spritePreview.cacheKey());
|
||
if (m_spritePreviewCacheKey != key || m_spritePreviewLastTargetSize != target) {
|
||
const QPixmap pm = QPixmap::fromImage(s.spritePreview);
|
||
m_spritePreview->setPixmap(
|
||
pm.scaled(target, Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||
m_spritePreviewCacheKey = key;
|
||
m_spritePreviewLastTargetSize = target;
|
||
}
|
||
} else if (m_spritePreview) {
|
||
m_spritePreview->setPixmap(QPixmap());
|
||
m_spritePreview->setText(QStringLiteral("无图像"));
|
||
m_spritePreviewCacheKey = 0;
|
||
m_spritePreviewLastTargetSize = {};
|
||
}
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
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_centerPosition) m_centerPosition->setEnabled(on);
|
||
if (m_relativeToOwnCenter) m_relativeToOwnCenter->setEnabled(on);
|
||
if (m_priority) m_priority->setEnabled(on);
|
||
if (m_userScale) m_userScale->setEnabled(on);
|
||
if (m_ignoreDistanceScale) m_ignoreDistanceScale->setEnabled(on);
|
||
if (m_visible) m_visible->setEnabled(on);
|
||
if (m_btnEditSprite) m_btnEditSprite->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
|