#include "props/EntityPropertySection.h" #include "params/ParamControls.h" #include #include "widgets/CompactNumericSpinBox.h" #include "widgets/PropertyPanelControls.h" #include #include #include #include #include #include #include #include #include #include #include #include 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(&QSpinBox::valueChanged), this, &EntityPropertySection::priorityEdited); connect(m_userScale, qOverload(&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(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