增加预览页介绍信息显示

This commit is contained in:
2026-04-08 09:56:25 +08:00
parent f53fee8e5a
commit 028ed1b18d
17 changed files with 1059 additions and 23 deletions

View File

@@ -3,14 +3,18 @@
#include "dialogs/CancelableTaskDialog.h"
#include "editor/EditorCanvas.h"
#include "dialogs/ImageCropDialog.h"
#include "core/domain/EntityIntro.h"
#include "core/net/ModelServerClient.h"
#include "params/ParamControls.h"
#include "props/BackgroundPropertySection.h"
#include "props/EntityPropertySection.h"
#include "timeline/TimelineWidget.h"
#include "dialogs/FrameAnimationDialog.h"
#include "dialogs/EntityIntroPopup.h"
#include <QAbstractItemView>
#include <QAbstractSpinBox>
#include <QApplication>
#include <QAction>
#include <QBoxLayout>
#include <QButtonGroup>
@@ -43,7 +47,10 @@
#include <QFile>
#include <QIcon>
#include <QResizeEvent>
#include <QScrollArea>
#include <QShowEvent>
#include <QtGlobal>
#include <QWheelEvent>
#include <QSizePolicy>
#include <QStyle>
#include <QUrl>
@@ -130,6 +137,31 @@ const char* kFloatingModeDockQss = R"(
}
)";
/// 避免滚轮先被 QScrollArea 吃掉,导致内嵌 QDoubleSpinBox 无法用滚轮调节
class SpinFriendlyScrollArea final : public QScrollArea {
public:
explicit SpinFriendlyScrollArea(QWidget* parent = nullptr)
: QScrollArea(parent) {}
protected:
void wheelEvent(QWheelEvent* e) override {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
const QPoint inVp = viewport()->mapFrom(this, e->position().toPoint());
#else
const QPoint inVp = viewport()->mapFrom(this, e->pos());
#endif
if (QWidget* w = viewport()->childAt(inVp)) {
for (QWidget* cur = w; cur; cur = cur->parentWidget()) {
if (qobject_cast<QAbstractSpinBox*>(cur)) {
QApplication::sendEvent(cur, e);
return;
}
}
}
QScrollArea::wheelEvent(e);
}
};
const char* kTimelineBarQss = R"(
#TimelineDockBar QToolButton, #TimelineDockBar QPushButton {
border: 1px solid palette(midlight);
@@ -158,6 +190,7 @@ public:
EditorCanvas* canvas = nullptr;
QWidget* modeDock = nullptr;
QWidget* toolDock = nullptr;
QWidget* previewPlaybackBar = nullptr;
void relayoutFloaters() {
if (canvas) {
@@ -201,6 +234,18 @@ public:
if (toolDock && toolDock->isVisible()) {
toolDock->raise();
}
if (previewPlaybackBar && previewPlaybackBar->isVisible()) {
if (QLayout* lay = previewPlaybackBar->layout()) {
lay->activate();
}
previewPlaybackBar->updateGeometry();
previewPlaybackBar->adjustSize();
const int x = std::max(kMargin, (width() - previewPlaybackBar->width()) / 2);
const int y = std::max(kMargin, height() - previewPlaybackBar->height() - kMargin);
previewPlaybackBar->move(x, y);
previewPlaybackBar->raise();
}
}
protected:
@@ -229,6 +274,21 @@ MainWindow::MainWindow(QWidget* parent)
createMenus();
createProjectTreeDock();
createTimelineDock();
if (m_previewBtnPlay && m_previewBtnPause && m_btnPlay) {
connect(m_previewBtnPlay, &QToolButton::clicked, this, [this]() {
if (m_btnPlay && !m_btnPlay->isChecked()) {
m_btnPlay->setChecked(true);
}
});
connect(m_previewBtnPause, &QToolButton::clicked, this, [this]() {
if (m_btnPlay && m_btnPlay->isChecked()) {
m_btnPlay->setChecked(false);
}
});
}
syncPreviewPlaybackBar();
refreshProjectTree();
updateUiEnabledState();
refreshEditorPage();
@@ -604,6 +664,15 @@ void MainWindow::onTogglePlay(bool on) {
m_playTimer->stop();
}
}
syncPreviewPlaybackBar();
}
void MainWindow::syncPreviewPlaybackBar() {
if (!m_previewBtnPlay || !m_previewBtnPause) {
return;
}
m_previewBtnPlay->setEnabled(!m_playing);
m_previewBtnPause->setEnabled(m_playing);
}
void MainWindow::onInsertCombinedKey() {
@@ -860,7 +929,14 @@ void MainWindow::createProjectTreeDock() {
}
});
connect(m_projectTree, &QTreeWidget::itemClicked, this, &MainWindow::onProjectTreeItemClicked);
dockLayout->addWidget(m_projectTree, 1);
auto* treeScroll = new SpinFriendlyScrollArea(dockContent);
treeScroll->setWidgetResizable(true);
treeScroll->setFrameShape(QFrame::NoFrame);
treeScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
treeScroll->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
treeScroll->setWidget(m_projectTree);
dockLayout->addWidget(treeScroll, 1);
m_dockProjectTree->setWidget(dockContent);
m_dockProjectTree->installEventFilter(this);
@@ -875,7 +951,7 @@ void MainWindow::createProjectTreeDock() {
m_bgPropertySection = new gui::BackgroundPropertySection();
m_entityPropertySection = new gui::EntityPropertySection();
m_propertyStack = new QStackedWidget(m_dockProperties);
m_propertyStack = new QStackedWidget();
m_propertyStack->setContentsMargins(4, 4, 4, 4);
m_propertyStack->addWidget(m_bgPropertySection);
m_propertyStack->addWidget(m_entityPropertySection);
@@ -927,11 +1003,48 @@ void MainWindow::createProjectTreeDock() {
});
connect(m_entityPropertySection, &gui::EntityPropertySection::userScaleEdited, this, [this](double v) {
if (m_selectedEntityId.isEmpty()) return;
if (!m_workspace.setEntityUserScale(m_selectedEntityId, v)) return;
if (!m_workspace.setEntityUserScale(m_selectedEntityId, v, m_currentFrame)) return;
refreshEditorPage();
refreshDopeSheet();
});
connect(m_entityPropertySection, &gui::EntityPropertySection::introContentEdited, this, [this]() {
if (m_selectedEntityId.isEmpty() || !m_entityPropertySection) {
return;
}
const core::EntityIntroContent intro = m_entityPropertySection->introSnapshot();
if (!m_workspace.setEntityIntroContent(m_selectedEntityId, intro)) {
QMessageBox::warning(this, QStringLiteral("介绍"), QStringLiteral("自动保存失败。"));
return;
}
refreshEditorPage();
});
connect(m_entityPropertySection, &gui::EntityPropertySection::introAddImageRequested, this, [this]() {
if (m_selectedEntityId.isEmpty() || !m_entityPropertySection) {
return;
}
const QString path = QFileDialog::getOpenFileName(
this,
QStringLiteral("选择配图"),
{},
QStringLiteral("图片 (*.png *.jpg *.jpeg *.webp *.bmp);;所有文件 (*)"));
if (path.isEmpty()) {
return;
}
QString rel;
if (!m_workspace.importEntityIntroImageFromFile(m_selectedEntityId, path, &rel)) {
QMessageBox::warning(this, QStringLiteral("介绍"), QStringLiteral("导入配图失败。"));
return;
}
m_entityPropertySection->appendIntroImagePath(rel);
});
m_dockProperties->setWidget(m_propertyStack);
auto* propScroll = new SpinFriendlyScrollArea(m_dockProperties);
propScroll->setWidgetResizable(true);
propScroll->setFrameShape(QFrame::NoFrame);
propScroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
propScroll->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
propScroll->setWidget(m_propertyStack);
m_dockProperties->setWidget(propScroll);
addDockWidget(Qt::RightDockWidgetArea, m_dockProjectTree);
splitDockWidget(m_dockProjectTree, m_dockProperties, Qt::Vertical);
@@ -996,10 +1109,12 @@ void MainWindow::refreshPropertyPanel() {
QString displayName;
double userScale = 1.0;
core::EntityIntroContent intro;
for (const auto& e : m_workspace.entities()) {
if (e.id == m_selectedEntityId) {
displayName = e.displayName;
userScale = e.userScale;
intro = e.intro;
break;
}
}
@@ -1013,6 +1128,7 @@ void MainWindow::refreshPropertyPanel() {
st.pivot = m_editorCanvas->selectedAnimatedOriginWorld();
st.centroid = m_editorCanvas->selectedEntityCentroidWorld();
st.userScale = userScale;
st.intro = intro;
m_entityPropertySection->applyState(st);
m_propertyStack->setCurrentWidget(m_entityPropertySection);
m_dockProperties->setWindowTitle(QStringLiteral("属性 — 实体"));
@@ -1323,6 +1439,9 @@ void MainWindow::applyUiMode(UiMode mode) {
if (m_floatingToolDock) {
m_floatingToolDock->setVisible(projectOpen && !preview);
}
if (m_previewPlaybackBar) {
m_previewPlaybackBar->setVisible(projectOpen && preview);
}
if (m_canvasHost) {
m_canvasHost->updateGeometry();
static_cast<CanvasHost*>(m_canvasHost)->relayoutFloaters();
@@ -1500,6 +1619,28 @@ void MainWindow::rebuildCentralPages() {
m_editorCanvas = new EditorCanvas(canvasHost);
canvasHost->canvas = m_editorCanvas;
m_entityIntroPopup = new gui::EntityIntroPopup(this);
m_previewPlaybackBar = new QFrame(canvasHost);
m_previewPlaybackBar->setObjectName(QStringLiteral("PreviewPlaybackBar"));
m_previewPlaybackBar->setStyleSheet(QString::fromUtf8(kTimelineBarQss));
auto* pbl = new QHBoxLayout(m_previewPlaybackBar);
pbl->setContentsMargins(10, 6, 10, 6);
pbl->setSpacing(8);
m_previewBtnPlay = new QToolButton(m_previewPlaybackBar);
m_previewBtnPlay->setText(QStringLiteral("播放"));
m_previewBtnPlay->setToolTip(QStringLiteral("播放时间轴"));
m_previewBtnPause = new QToolButton(m_previewPlaybackBar);
m_previewBtnPause->setText(QStringLiteral("暂停"));
m_previewBtnPause->setToolTip(QStringLiteral("暂停时间轴"));
polishCompactToolButton(m_previewBtnPlay, 36);
polishCompactToolButton(m_previewBtnPause, 36);
pbl->addWidget(m_previewBtnPlay);
pbl->addWidget(m_previewBtnPause);
m_previewPlaybackBar->setParent(canvasHost);
m_previewPlaybackBar->hide();
canvasHost->previewPlaybackBar = m_previewPlaybackBar;
m_floatingModeDock = new QFrame(canvasHost);
m_floatingModeDock->setObjectName(QStringLiteral("FloatingModeDock"));
m_floatingModeDock->setFrameShape(QFrame::NoFrame);
@@ -1685,6 +1826,33 @@ void MainWindow::rebuildCentralPages() {
updateUiEnabledState();
});
connect(m_editorCanvas, &EditorCanvas::presentationEntityIntroRequested, this,
[this](const QString& id, QPointF anchorView) {
if (!m_entityIntroPopup || !m_workspace.isOpen()) {
return;
}
const core::Project::Entity* hit = nullptr;
for (const auto& e : m_workspace.entities()) {
if (e.id == id) {
hit = &e;
break;
}
}
m_entityIntroPopup->setProjectDir(m_workspace.projectDir());
if (hit) {
m_entityIntroPopup->setContent(hit->intro);
} else {
core::EntityIntroContent empty;
m_entityIntroPopup->setContent(empty);
}
m_entityIntroPopup->showNearCanvasPoint(anchorView.toPoint(), m_editorCanvas);
});
connect(m_editorCanvas, &EditorCanvas::presentationInteractionDismissed, this, [this]() {
if (m_entityIntroPopup) {
m_entityIntroPopup->clearAndHide();
}
});
connect(group, &QButtonGroup::idClicked, this, [this](int id) {
if (!m_editorCanvas) {
return;
@@ -1793,6 +1961,12 @@ void MainWindow::setPreviewRequested(bool preview) {
return;
}
m_previewRequested = preview;
if (!preview && m_editorCanvas) {
m_editorCanvas->clearPresentationEntityFocus();
}
if (!preview && m_entityIntroPopup) {
m_entityIntroPopup->clearAndHide();
}
updateUiEnabledState();
if (preview) {
refreshPreviewPage();