#include "main_window/MainWindow.h" #include "dialogs/AboutWindow.h" #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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { /// 右侧项目树/属性 dock 水平方向可拖到的最小宽度(须小于 kRightDockAutoHideBelow,否则无法触发自动隐藏) constexpr int kRightDockMinimumWidth = 80; /// 列宽小于此值时自动隐藏右侧两 dock constexpr int kRightDockAutoHideBelow = 92; void polishCompactToolButton(QToolButton* b, int px = 40) { if (!b) return; b->setFixedSize(px, px); b->setFocusPolicy(Qt::NoFocus); b->setAutoRaise(true); b->setIconSize(QSize(px - 14, px - 14)); } void setToolButtonIconOrText(QToolButton* b, const QString& themeName, const QString& text) { if (!b) return; const QIcon ic = QIcon::fromTheme(themeName); if (!ic.isNull()) { b->setIcon(ic); b->setText(QString()); b->setToolButtonStyle(Qt::ToolButtonIconOnly); } else { b->setIcon(QIcon()); b->setText(text); b->setToolButtonStyle(Qt::ToolButtonTextOnly); QFont f = b->font(); f.setPointSize(11); b->setFont(f); } } const char* kEditorToolRailQss = R"( #EditorToolRail { background-color: palette(base); border: 1px solid palette(midlight); border-radius: 10px; } #EditorToolRail QToolButton { border: 1px solid transparent; border-radius: 8px; padding: 2px; background: transparent; } #EditorToolRail QToolButton:hover { background: palette(midlight); } #EditorToolRail QToolButton:checked { background: palette(highlight); color: palette(highlighted-text); } )"; const char* kFloatingModeDockQss = R"( #FloatingModeDock { background-color: palette(base); border: 1px solid palette(midlight); border-radius: 10px; } #FloatingModeDock QComboBox { background-color: palette(button); color: palette(button-text); border: 1px solid palette(mid); border-radius: 6px; padding: 4px 6px; min-height: 22px; } #FloatingModeDock QComboBox:hover { background-color: palette(light); } #FloatingModeDock QComboBox:focus { border: 1px solid palette(highlight); } )"; /// 避免滚轮先被 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(cur)) { QApplication::sendEvent(cur, e); return; } } } QScrollArea::wheelEvent(e); } }; const char* kTimelineBarQss = R"( #TimelineDockBar QToolButton, #TimelineDockBar QPushButton { border: 1px solid palette(midlight); border-radius: 6px; padding: 4px 8px; min-height: 26px; background: palette(button); } #TimelineDockBar QToolButton:hover, #TimelineDockBar QPushButton:hover { background: palette(light); } #TimelineDockBar QToolButton:checked { background: palette(highlight); color: palette(highlighted-text); } #TimelineDockBar QCheckBox { spacing: 6px; } )"; class CanvasHost final : public QWidget { public: explicit CanvasHost(QWidget* parent = nullptr) : QWidget(parent) {} EditorCanvas* canvas = nullptr; QWidget* modeDock = nullptr; QWidget* toolDock = nullptr; QWidget* previewPlaybackBar = nullptr; void relayoutFloaters() { if (canvas) { canvas->setGeometry(0, 0, width(), height()); canvas->lower(); } constexpr int kMargin = 10; constexpr int kGap = 10; if (modeDock && modeDock->isVisible()) { if (QLayout* lay = modeDock->layout()) { lay->activate(); } modeDock->updateGeometry(); modeDock->adjustSize(); const QSize sh = modeDock->sizeHint().expandedTo(modeDock->minimumSizeHint()); if (sh.isValid() && (modeDock->width() < sh.width() || modeDock->height() < sh.height())) { modeDock->resize(std::max(modeDock->width(), sh.width()), std::max(modeDock->height(), sh.height())); } modeDock->move(kMargin, kMargin); modeDock->adjustSize(); } if (toolDock && toolDock->isVisible()) { if (QLayout* lay = toolDock->layout()) { lay->activate(); } toolDock->updateGeometry(); toolDock->adjustSize(); int y = kMargin; if (modeDock && modeDock->isVisible()) { y = modeDock->y() + modeDock->height() + kGap; } toolDock->move(kMargin, y); } // 工具条在上层,避免与模式条叠放时误点不到按钮 if (modeDock && modeDock->isVisible()) { modeDock->raise(); } 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: void resizeEvent(QResizeEvent* e) override { QWidget::resizeEvent(e); relayoutFloaters(); } void showEvent(QShowEvent* e) override { QWidget::showEvent(e); relayoutFloaters(); QTimer::singleShot(0, this, [this]() { relayoutFloaters(); }); } }; } // namespace /// @brief MainWindow 类实现,负责构建和管理主界面,包括菜单、停靠窗口和属性面板。 MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { // 设置窗口大小 resize(1200, 800); rebuildCentralPages(); 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(); m_propertySyncTimer = new QTimer(this); m_propertySyncTimer->setSingleShot(true); connect(m_propertySyncTimer, &QTimer::timeout, this, [this]() { if (m_entityDragging) { refreshEntityPropertyPanelFast(); } else { refreshPropertyPanel(); } }); // 某些平台/窗口管理器下,dock 的初始可见性会在 QMainWindow show() 之后被重新应用一次。 // 这里在事件循环开始后再强制执行一遍“欢迎/编辑”两态策略,保证未打开项目时只显示欢迎页。 QTimer::singleShot(0, this, [this]() { applyUiMode(currentUiMode()); updateUiEnabledState(); refreshEditorPage(); }); setWindowTitle("工具"); statusBar()->showMessage("就绪"); } void MainWindow::createTimelineDock() { m_dockTimeline = new QDockWidget(QStringLiteral("动画"), this); m_dockTimeline->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea); m_dockTimeline->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); auto* dockContent = new QWidget(m_dockTimeline); auto* outer = new QVBoxLayout(dockContent); outer->setContentsMargins(10, 6, 10, 6); outer->setSpacing(6); auto* bar = new QWidget(dockContent); bar->setObjectName(QStringLiteral("TimelineDockBar")); bar->setStyleSheet(QString::fromUtf8(kTimelineBarQss)); auto* layout = new QHBoxLayout(bar); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(8); m_btnPlay = new QToolButton(bar); m_btnPlay->setText(QStringLiteral("▶")); m_btnPlay->setCheckable(true); m_btnPlay->setToolTip(QStringLiteral("播放 / 暂停")); polishCompactToolButton(m_btnPlay, 34); layout->addWidget(m_btnPlay); m_timeline = new TimelineWidget(bar); m_timeline->setFrameRange(0, 600); m_timeline->setCurrentFrame(0); layout->addWidget(m_timeline, 1); m_frameLabel = new QLabel(QStringLiteral("0"), bar); m_frameLabel->setMinimumWidth(40); m_frameLabel->setAlignment(Qt::AlignCenter); { QFont f = m_frameLabel->font(); f.setBold(true); f.setPointSizeF(f.pointSizeF() + 0.5); m_frameLabel->setFont(f); } layout->addWidget(m_frameLabel); // 合并后的关键帧按钮:一次写入位置 + userScale auto* btnKeyCombined = new QToolButton(bar); setToolButtonIconOrText(btnKeyCombined, QStringLiteral("media-record"), QStringLiteral("关键帧")); btnKeyCombined->setToolTip(QStringLiteral("在当前帧记录实体的位置与缩放关键帧")); polishCompactToolButton(btnKeyCombined, 34); layout->addWidget(btnKeyCombined); outer->addWidget(bar); // 旧版 DopeSheet 去除,预留一个占位条,后续可在此添加区间选择或更紧凑的可视化 outer->addStretch(1); m_dockTimeline->setWidget(dockContent); addDockWidget(Qt::BottomDockWidgetArea, m_dockTimeline); connect(m_dockTimeline, &QDockWidget::visibilityChanged, this, [this](bool visible) { if (m_actionToggleTimeline) { m_actionToggleTimeline->blockSignals(true); m_actionToggleTimeline->setChecked(visible); m_actionToggleTimeline->blockSignals(false); } }); m_playTimer = new QTimer(this); connect(m_playTimer, &QTimer::timeout, this, [this]() { if (!m_timeline || !m_workspace.isOpen()) { return; } int v = m_timeline->currentFrame(); if (v >= 600) { v = 0; } else { ++v; } m_timeline->setCurrentFrame(v); }); connect(m_btnPlay, &QToolButton::toggled, this, &MainWindow::onTogglePlay); connect(m_timeline, &TimelineWidget::frameScrubbed, this, [this](int v) { // 轻量实时预览:只更新画布帧,不做 refreshEditorPage 的全量重建 m_currentFrame = v; if (m_frameLabel) { m_frameLabel->setText(QString::number(v)); } if (m_editorCanvas && m_workspace.isOpen()) { // 需要重新求值实体几何/贴图轨道,否则拖动实体与属性变更在非 0 帧会失效 m_timelineScrubbing = true; m_editorCanvas->setCurrentFrame(v); m_editorCanvas->setEntities(m_workspace.entities(), m_workspace.projectDir()); m_timelineScrubbing = false; } else if (m_editorCanvas) { m_editorCanvas->setCurrentFrame(v); } }); connect(m_timeline, &TimelineWidget::frameCommitted, this, [this](int v) { // 松手再做一次较重刷新(如果后续还有需要同步的 UI) m_currentFrame = v; refreshEditorPage(); }); connect(btnKeyCombined, &QToolButton::clicked, this, &MainWindow::onInsertCombinedKey); connect(m_timeline, &TimelineWidget::contextMenuRequested, this, [this](const QPoint& globalPos, int frame) { if (!m_timeline) return; QMenu menu(this); QAction* actDeleteKey = menu.addAction(QStringLiteral("删除关键帧")); QAction* actSetStart = menu.addAction(QStringLiteral("设为区间起点")); QAction* actSetEnd = menu.addAction(QStringLiteral("设为区间终点")); QAction* actClear = menu.addAction(QStringLiteral("清除区间")); menu.addSeparator(); QAction* actAnim = menu.addAction(QStringLiteral("动画…")); // 简单提示当前区间(不占面板空间) const int selA = m_timeline->selectionStart(); const int selB = m_timeline->selectionEnd(); const bool hasRange = (selA >= 0 && selB >= 0); actClear->setEnabled(hasRange); actAnim->setEnabled(hasRange && !m_selectedEntityId.isEmpty() && m_workspace.isOpen()); actDeleteKey->setEnabled(m_workspace.isOpen() && !m_selectedEntityId.isEmpty() && m_timeline->hasSelectedKeyframe()); // 右键命中帧:用鼠标位置对应的 frame m_currentFrame = std::clamp(frame, 0, 600); if (m_frameLabel) m_frameLabel->setText(QString::number(m_currentFrame)); if (m_editorCanvas) m_editorCanvas->setCurrentFrame(m_currentFrame); QAction* chosen = menu.exec(globalPos); if (!chosen) { return; } if (chosen == actDeleteKey) { if (!m_workspace.isOpen() || m_selectedEntityId.isEmpty() || !m_timeline->hasSelectedKeyframe()) { return; } const int f = m_timeline->selectedKeyFrame(); bool ok = false; switch (m_timeline->selectedKeyKind()) { case TimelineWidget::KeyKind::Location: ok = m_workspace.removeEntityLocationKey(m_selectedEntityId, f); break; case TimelineWidget::KeyKind::UserScale: ok = m_workspace.removeEntityUserScaleKey(m_selectedEntityId, f); break; case TimelineWidget::KeyKind::Image: ok = m_workspace.removeEntityImageFrame(m_selectedEntityId, f); break; default: break; } if (ok) { refreshEditorPage(); } return; } if (chosen == actSetStart) { m_timelineRangeStart = m_currentFrame; if (m_timelineRangeEnd < 0) { m_timelineRangeEnd = m_currentFrame; } if (m_timelineRangeEnd < m_timelineRangeStart) { std::swap(m_timelineRangeStart, m_timelineRangeEnd); } if (m_timeline) { m_timeline->setSelectionRange(m_timelineRangeStart, m_timelineRangeEnd); } return; } if (chosen == actSetEnd) { m_timelineRangeEnd = m_currentFrame; if (m_timelineRangeStart < 0) { m_timelineRangeStart = m_currentFrame; } if (m_timelineRangeEnd < m_timelineRangeStart) { std::swap(m_timelineRangeStart, m_timelineRangeEnd); } if (m_timeline) { m_timeline->setSelectionRange(m_timelineRangeStart, m_timelineRangeEnd); } return; } if (chosen == actClear) { m_timelineRangeStart = -1; m_timelineRangeEnd = -1; if (m_timeline) { m_timeline->setSelectionRange(-1, -1); } return; } if (chosen == actAnim) { if (m_selectedEntityId.isEmpty() || !m_workspace.isOpen()) { return; } const int a = std::clamp(std::min(selA, selB), 0, 600); const int b = std::clamp(std::max(selA, selB), 0, 600); if (a > b) { return; } FrameAnimationDialog dlg(m_workspace, m_selectedEntityId, a, b, this); dlg.exec(); refreshEditorPage(); } }); } void MainWindow::updateStatusBarText() { // 坐标系:图片左上角为 (0,0),单位为像素(world 坐标与背景像素一致) if (m_hasSelectedEntity) { statusBar()->showMessage(QStringLiteral("实体中心 (%1, %2, %3) | 鼠标 (%4, %5, %6)") .arg(m_selectedEntityOrigin.x(), 0, 'f', 1) .arg(m_selectedEntityOrigin.y(), 0, 'f', 1) .arg(m_selectedEntityDepth) .arg(m_lastWorldPos.x(), 0, 'f', 1) .arg(m_lastWorldPos.y(), 0, 'f', 1) .arg(m_lastWorldZ)); } else { statusBar()->showMessage(QStringLiteral("鼠标 (%1, %2, %3)") .arg(m_lastWorldPos.x(), 0, 'f', 1) .arg(m_lastWorldPos.y(), 0, 'f', 1) .arg(m_lastWorldZ)); } } void MainWindow::onComputeDepth() { if (!m_workspace.isOpen() || !m_workspace.hasBackground()) { return; } computeDepthAsync(); } void MainWindow::computeDepthAsync() { if (!m_workspace.isOpen() || !m_workspace.hasBackground()) { return; } const auto bgAbs = m_workspace.backgroundAbsolutePath(); if (bgAbs.isEmpty() || !QFileInfo::exists(bgAbs)) { QMessageBox::warning(this, QStringLiteral("深度"), QStringLiteral("背景不存在。")); return; } QFile f(bgAbs); if (!f.open(QIODevice::ReadOnly)) { QMessageBox::warning(this, QStringLiteral("深度"), QStringLiteral("读取背景失败。")); return; } const QByteArray bgBytes = f.readAll(); f.close(); if (bgBytes.isEmpty()) { QMessageBox::warning(this, QStringLiteral("深度"), QStringLiteral("背景文件为空。")); return; } QString base; const QByteArray env = qgetenv("MODEL_SERVER_URL"); base = env.isEmpty() ? QStringLiteral("http://127.0.0.1:8000") : QString::fromUtf8(env); auto* client = new core::ModelServerClient(this); client->setBaseUrl(QUrl(base)); QString immediateErr; QNetworkReply* reply = client->computeDepthPng8Async(bgBytes, &immediateErr); if (!reply) { QMessageBox::warning(this, QStringLiteral("深度"), immediateErr.isEmpty() ? QStringLiteral("无法发起后端请求。") : immediateErr); client->deleteLater(); return; } auto* dlg = new CancelableTaskDialog(QStringLiteral("计算深度"), QStringLiteral("正在请求后端计算深度,请稍候……"), this); dlg->setAttribute(Qt::WA_DeleteOnClose, true); connect(dlg, &CancelableTaskDialog::canceled, this, [reply, dlg]() { if (reply) { reply->abort(); } if (dlg) { dlg->reject(); } }); connect(reply, &QNetworkReply::finished, this, [this, reply, dlg, client]() { const int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); const QByteArray raw = reply->readAll(); const auto netErr = reply->error(); const QString netErrStr = reply->errorString(); reply->deleteLater(); client->deleteLater(); if (dlg) { dlg->close(); } if (netErr != QNetworkReply::NoError) { // 用户取消也会走这里(OperationCanceledError 等) if (netErrStr.contains(QStringLiteral("canceled"), Qt::CaseInsensitive) || netErr == QNetworkReply::OperationCanceledError) { statusBar()->showMessage(QStringLiteral("已取消计算深度")); return; } QMessageBox::warning(this, QStringLiteral("深度"), QStringLiteral("网络错误:%1").arg(netErrStr)); return; } if (httpStatus != 200) { QString detail; const QJsonDocument jd = QJsonDocument::fromJson(raw); if (jd.isObject()) { detail = jd.object().value(QStringLiteral("detail")).toString(); } QMessageBox::warning(this, QStringLiteral("深度"), detail.isEmpty() ? QStringLiteral("后端返回HTTP %1。").arg(httpStatus) : QStringLiteral("后端错误(HTTP %1):%2").arg(httpStatus).arg(detail)); return; } if (raw.isEmpty()) { QMessageBox::warning(this, QStringLiteral("深度"), QStringLiteral("后端返回空数据。")); return; } QString err; if (!m_workspace.saveDepthMapPngBytes(raw, &err)) { QMessageBox::warning(this, QStringLiteral("深度"), err.isEmpty() ? QStringLiteral("保存深度图失败。") : err); return; } statusBar()->showMessage(QStringLiteral("深度已计算")); refreshEditorPage(); refreshProjectTree(); updateUiEnabledState(); refreshPreviewPage(); }); dlg->show(); } void MainWindow::onTogglePlay(bool on) { m_playing = on; if (m_btnPlay) { m_btnPlay->setText(on ? QStringLiteral("⏸") : QStringLiteral("▶")); } if (m_playTimer) { if (on) { const int fps = 30; // 固定 30fps 播放 m_playTimer->start(1000 / fps); } else { 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() { if (m_selectedEntityId.isEmpty() || !m_editorCanvas) { return; } // 位置关键帧:使用当前帧下的动画原点 const QPointF o = m_editorCanvas->selectedAnimatedOriginWorld(); m_workspace.setEntityLocationKey(m_selectedEntityId, m_currentFrame, o); // 缩放关键帧:使用当前帧下的 userScale(而非 depthScale01) const double s = m_editorCanvas->selectedUserScale(); m_workspace.setEntityUserScaleKey(m_selectedEntityId, m_currentFrame, s); refreshEditorPage(); } void MainWindow::createMenus() { createFileMenu(); createEditMenu(); createViewMenu(); createHelpMenu(); } void MainWindow::createFileMenu() { auto m_fileMenu = menuBar()->addMenu(QString()); m_fileMenu->setTitle("文件"); // 示例:新建、打开、保存、退出 auto* newProjectAction = m_fileMenu->addAction(QString()); newProjectAction->setText("新建工程"); newProjectAction->setShortcut(QKeySequence::New); connect(newProjectAction, &QAction::triggered, this, &MainWindow::onNewProject); auto* openProjectAction = m_fileMenu->addAction(QString()); openProjectAction->setText("打开工程"); openProjectAction->setShortcut(QKeySequence::Open); connect(openProjectAction, &QAction::triggered, this, &MainWindow::onOpenProject); auto* closeProjectAction = m_fileMenu->addAction(QString()); closeProjectAction->setText(QStringLiteral("关闭工程")); closeProjectAction->setShortcut(QKeySequence::Close); connect(closeProjectAction, &QAction::triggered, this, &MainWindow::onCloseProject); m_fileMenu->addSeparator(); auto* saveAction = m_fileMenu->addAction(QString()); saveAction->setText("保存"); saveAction->setShortcut(QKeySequence::Save); connect(saveAction, &QAction::triggered, this, &MainWindow::onSaveProject); m_fileMenu->addSeparator(); auto* exitAction = m_fileMenu->addAction(QString()); exitAction->setText("退出"); exitAction->setShortcut(QKeySequence::Quit); connect(exitAction, &QAction::triggered, this, &MainWindow::close); } void MainWindow::createEditMenu() { auto* editMenu = menuBar()->addMenu(QString()); editMenu->setTitle("编辑"); // 撤销/重做 m_actionUndo = editMenu->addAction(QString()); m_actionUndo->setText("撤销"); m_actionUndo->setShortcut(QKeySequence::Undo); connect(m_actionUndo, &QAction::triggered, this, &MainWindow::onUndo); m_actionRedo = editMenu->addAction(QString()); m_actionRedo->setText("重做"); m_actionRedo->setShortcut(QKeySequence::Redo); connect(m_actionRedo, &QAction::triggered, this, &MainWindow::onRedo); editMenu->addSeparator(); // 复制/粘贴/删除 m_actionCopy = editMenu->addAction(QString()); m_actionCopy->setText("复制"); m_actionCopy->setShortcut(QKeySequence::Copy); connect(m_actionCopy, &QAction::triggered, this, &MainWindow::onCopyObject); m_actionPaste = editMenu->addAction(QString()); m_actionPaste->setText("粘贴"); m_actionPaste->setShortcut(QKeySequence::Paste); connect(m_actionPaste, &QAction::triggered, this, &MainWindow::onPasteObject); } void MainWindow::createHelpMenu() { auto* helpMenu = menuBar()->addMenu(QString()); helpMenu->setTitle("帮助"); auto* aboutAction = helpMenu->addAction(QString()); aboutAction->setText("关于"); connect(aboutAction, &QAction::triggered, this, &MainWindow::onAbout); } void MainWindow::createViewMenu() { auto* viewMenu = menuBar()->addMenu(QString()); viewMenu->setTitle("视图"); m_actionToggleProjectTree = viewMenu->addAction(QStringLiteral("项目树")); m_actionToggleProjectTree->setCheckable(true); connect(m_actionToggleProjectTree, &QAction::toggled, this, [this](bool on) { m_rightDocksNarrowHidden = false; if (m_dockProjectTree) { m_dockProjectTree->setVisible(on); } }); m_actionToggleProperties = viewMenu->addAction(QStringLiteral("属性")); m_actionToggleProperties->setCheckable(true); connect(m_actionToggleProperties, &QAction::toggled, this, [this](bool on) { m_rightDocksNarrowHidden = false; if (m_dockProperties) { m_dockProperties->setVisible(on); } }); m_actionToggleTimeline = viewMenu->addAction(QStringLiteral("动画面板")); m_actionToggleTimeline->setCheckable(true); connect(m_actionToggleTimeline, &QAction::toggled, this, [this](bool on) { if (m_dockTimeline) { m_dockTimeline->setVisible(on); } }); auto* canvasMenu = viewMenu->addMenu(QStringLiteral("画布")); m_actionCanvasWorldAxes = canvasMenu->addAction(QStringLiteral("世界坐标轴")); m_actionCanvasWorldAxes->setCheckable(true); m_actionCanvasWorldAxes->setChecked(true); connect(m_actionCanvasWorldAxes, &QAction::toggled, this, [this](bool on) { if (m_editorCanvas) { m_editorCanvas->setWorldAxesVisible(on); } if (m_actionCanvasAxisValues) { m_actionCanvasAxisValues->setEnabled(on); } }); m_actionCanvasAxisValues = canvasMenu->addAction(QStringLiteral("坐标轴数值")); m_actionCanvasAxisValues->setCheckable(true); m_actionCanvasAxisValues->setChecked(true); connect(m_actionCanvasAxisValues, &QAction::toggled, this, [this](bool on) { if (m_editorCanvas) { m_editorCanvas->setAxisLabelsVisible(on); } }); canvasMenu->addSeparator(); m_actionCanvasGrid = canvasMenu->addAction(QStringLiteral("参考网格")); m_actionCanvasGrid->setCheckable(true); m_actionCanvasGrid->setChecked(true); connect(m_actionCanvasGrid, &QAction::toggled, this, [this](bool on) { if (m_editorCanvas) { m_editorCanvas->setGridVisible(on); } }); m_actionCanvasCheckerboard = canvasMenu->addAction(QStringLiteral("棋盘底纹")); m_actionCanvasCheckerboard->setCheckable(true); m_actionCanvasCheckerboard->setChecked(true); connect(m_actionCanvasCheckerboard, &QAction::toggled, this, [this](bool on) { if (m_editorCanvas) { m_editorCanvas->setCheckerboardVisible(on); } }); canvasMenu->addSeparator(); m_actionCanvasDepthOverlay = canvasMenu->addAction(QStringLiteral("深度叠加")); m_actionCanvasDepthOverlay->setCheckable(true); m_actionCanvasDepthOverlay->setChecked(false); connect(m_actionCanvasDepthOverlay, &QAction::toggled, this, [this](bool on) { if (m_editorCanvas) { m_editorCanvas->setDepthOverlayEnabled(on); } if (m_btnToggleDepthOverlay) { m_btnToggleDepthOverlay->blockSignals(true); m_btnToggleDepthOverlay->setChecked(on); m_btnToggleDepthOverlay->blockSignals(false); } if (m_bgPropertySection) { m_bgPropertySection->syncDepthOverlayChecked(on); } }); m_actionCanvasGizmoLabels = canvasMenu->addAction(QStringLiteral("Gizmo 轴标签")); m_actionCanvasGizmoLabels->setCheckable(true); m_actionCanvasGizmoLabels->setChecked(true); connect(m_actionCanvasGizmoLabels, &QAction::toggled, this, [this](bool on) { if (m_editorCanvas) { m_editorCanvas->setGizmoLabelsVisible(on); } }); viewMenu->addSeparator(); m_actionEnterPreview = viewMenu->addAction(QStringLiteral("进入预览展示")); connect(m_actionEnterPreview, &QAction::triggered, this, [this]() { setPreviewRequested(true); }); m_actionBackToEditor = viewMenu->addAction(QStringLiteral("返回编辑模式")); connect(m_actionBackToEditor, &QAction::triggered, this, [this]() { setPreviewRequested(false); }); } void MainWindow::createProjectTreeDock() { m_dockProjectTree = new QDockWidget(QStringLiteral("项目树"), this); m_dockProjectTree->setToolTip(QStringLiteral("右键条目可切换编辑与预览。")); m_dockProjectTree->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea | Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea); m_dockProjectTree->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable); m_dockProjectTree->setMinimumWidth(kRightDockMinimumWidth); auto* dockContent = new QWidget(m_dockProjectTree); auto* dockLayout = new QVBoxLayout(dockContent); dockLayout->setContentsMargins(4, 4, 4, 4); dockLayout->setSpacing(4); m_projectTree = new QTreeWidget(dockContent); m_projectTree->setColumnCount(2); m_projectTree->setHeaderHidden(true); m_projectTree->setRootIsDecorated(false); m_projectTree->setIndentation(0); m_projectTree->setUniformRowHeights(true); m_projectTree->setDragEnabled(false); m_projectTree->setAcceptDrops(false); m_projectTree->setDropIndicatorShown(false); m_projectTree->setDragDropMode(QAbstractItemView::NoDragDrop); if (m_projectTree->header()) { m_projectTree->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); m_projectTree->header()->setSectionResizeMode(1, QHeaderView::Stretch); } m_projectTree->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_projectTree, &QTreeWidget::customContextMenuRequested, this, [this](const QPoint& pos) { if (!m_projectTree) { return; } auto* item = m_projectTree->itemAt(pos); if (!item) { if (m_workspace.isOpen()) { showProjectRootContextMenu(m_projectTree->viewport()->mapToGlobal(pos)); } return; } if (item == m_itemBackground) { showBackgroundContextMenu(m_projectTree->viewport()->mapToGlobal(pos)); } }); connect(m_projectTree, &QTreeWidget::itemClicked, this, &MainWindow::onProjectTreeItemClicked); 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); m_dockProperties = new QDockWidget(QStringLiteral("属性"), this); m_dockProperties->setToolTip(QStringLiteral("在项目树中右键条目可预览或操作背景。")); m_dockProperties->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea | Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea); m_dockProperties->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable); m_dockProperties->setMinimumWidth(kRightDockMinimumWidth); m_bgPropertySection = new gui::BackgroundPropertySection(); m_entityPropertySection = new gui::EntityPropertySection(); m_propertyStack = new QStackedWidget(); m_propertyStack->setContentsMargins(4, 4, 4, 4); m_propertyStack->addWidget(m_bgPropertySection); m_propertyStack->addWidget(m_entityPropertySection); connect(m_bgPropertySection, &gui::BackgroundPropertySection::backgroundVisibleToggled, this, [this](bool on) { if (!m_workspace.isOpen()) return; if (!m_workspace.setBackgroundVisible(on)) return; refreshProjectTree(); refreshEditorPage(); refreshPreviewPage(); }); connect(m_bgPropertySection, &gui::BackgroundPropertySection::depthOverlayToggled, this, [this](bool on) { if (m_editorCanvas) { m_editorCanvas->setDepthOverlayEnabled(on); } if (m_actionCanvasDepthOverlay) { m_actionCanvasDepthOverlay->blockSignals(true); m_actionCanvasDepthOverlay->setChecked(on); m_actionCanvasDepthOverlay->blockSignals(false); } if (m_btnToggleDepthOverlay) { m_btnToggleDepthOverlay->blockSignals(true); m_btnToggleDepthOverlay->setChecked(on); m_btnToggleDepthOverlay->blockSignals(false); } }); connect(m_entityPropertySection, &gui::EntityPropertySection::displayNameCommitted, this, [this](const QString& text) { if (m_selectedEntityId.isEmpty()) return; if (!m_workspace.setEntityDisplayName(m_selectedEntityId, text)) return; refreshProjectTree(); refreshDopeSheet(); }); connect(m_entityPropertySection, &gui::EntityPropertySection::pivotEdited, this, [this](double x, double y) { if (m_selectedEntityId.isEmpty() || !m_editorCanvas) return; const double s = m_editorCanvas->selectedCombinedScale(); if (s <= 1e-9) return; if (!m_workspace.reanchorEntityPivot(m_selectedEntityId, m_currentFrame, QPointF(x, y), s)) return; refreshEditorPage(); refreshDopeSheet(); }); connect(m_entityPropertySection, &gui::EntityPropertySection::centroidEdited, this, [this](double x, double y) { if (m_selectedEntityId.isEmpty() || !m_editorCanvas) return; const double s = m_editorCanvas->selectedCombinedScale(); if (s <= 1e-9) return; const bool autoKey = m_chkAutoKeyframe && m_chkAutoKeyframe->isChecked(); if (!m_workspace.moveEntityCentroidTo(m_selectedEntityId, m_currentFrame, QPointF(x, y), s, autoKey)) return; refreshEditorPage(); refreshDopeSheet(); }); connect(m_entityPropertySection, &gui::EntityPropertySection::userScaleEdited, this, [this](double v) { if (m_selectedEntityId.isEmpty()) 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); }); 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); resizeDocks({m_dockProjectTree, m_dockProperties}, {220, 200}, Qt::Vertical); connect(m_dockProjectTree, &QDockWidget::visibilityChanged, this, [this](bool visible) { if (m_actionToggleProjectTree) { m_actionToggleProjectTree->blockSignals(true); m_actionToggleProjectTree->setChecked(visible); m_actionToggleProjectTree->blockSignals(false); } }); connect(m_dockProperties, &QDockWidget::visibilityChanged, this, [this](bool visible) { if (m_actionToggleProperties) { m_actionToggleProperties->blockSignals(true); m_actionToggleProperties->setChecked(visible); m_actionToggleProperties->blockSignals(false); } }); } void MainWindow::refreshPropertyPanel() { if (!m_bgPropertySection || !m_entityPropertySection || !m_propertyStack) { return; } if (!m_workspace.isOpen()) { m_bgPropertySection->setProjectClosedAppearance(); m_bgAbsCache.clear(); m_bgSizeTextCache = QStringLiteral("-"); } else if (m_workspace.hasBackground()) { const QString bgAbs = m_workspace.backgroundAbsolutePath(); if (bgAbs != m_bgAbsCache) { m_bgAbsCache = bgAbs; if (!bgAbs.isEmpty() && QFileInfo::exists(bgAbs)) { const QImage img(bgAbs); m_bgSizeTextCache = img.isNull() ? QStringLiteral("-") : QStringLiteral("%1 × %2").arg(img.width()).arg(img.height()); } else { m_bgSizeTextCache = QStringLiteral("-"); } } m_bgPropertySection->setBackgroundSizeText(m_bgSizeTextCache); m_bgPropertySection->syncBackgroundVisible(m_workspace.project().backgroundVisible(), true); } else { m_bgPropertySection->setBackgroundSizeText(QStringLiteral("-")); m_bgPropertySection->syncBackgroundVisible(true, false); } if (m_editorCanvas) { m_bgPropertySection->syncDepthOverlayChecked(m_editorCanvas->depthOverlayEnabled()); } const bool entUi = m_hasSelectedEntity && m_workspace.isOpen() && !m_selectedEntityId.isEmpty() && m_editorCanvas; if (!entUi) { m_entityPropertySection->clearDisconnected(); m_propertyStack->setCurrentWidget(m_bgPropertySection); m_dockProperties->setWindowTitle(QStringLiteral("属性 — 背景")); return; } 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; } } m_selectedEntityDisplayNameCache = displayName; gui::EntityPropertyUiState st; st.displayName = displayName.isEmpty() ? m_selectedEntityId : displayName; st.depthZ = m_selectedEntityDepth; st.distanceScaleText = QStringLiteral("%1(自动)").arg(m_editorCanvas->selectedDistanceScaleMultiplier(), 0, 'f', 3); 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("属性 — 实体")); } void MainWindow::refreshEntityPropertyPanelFast() { if (!m_entityPropertySection || !m_propertyStack || !m_editorCanvas) { return; } const bool entUi = m_hasSelectedEntity && m_workspace.isOpen() && !m_selectedEntityId.isEmpty(); if (!entUi) { return; } gui::EntityPropertyUiState st; const QString dn = m_selectedEntityDisplayNameCache; st.displayName = dn.isEmpty() ? m_selectedEntityId : dn; st.depthZ = m_selectedEntityDepth; st.distanceScaleText = QStringLiteral("%1(自动)").arg(m_editorCanvas->selectedDistanceScaleMultiplier(), 0, 'f', 3); st.pivot = m_editorCanvas->selectedAnimatedOriginWorld(); st.centroid = m_editorCanvas->selectedEntityCentroidWorld(); st.userScale = m_editorCanvas->selectedUserScale(); m_entityPropertySection->applyState(st); // 拖动中不切换 stack、不改 dock 标题,避免多余布局开销 } void MainWindow::refreshProjectTree() { if (!m_projectTree) { return; } m_projectTree->clear(); const int iconPm = style()->pixelMetric(QStyle::PM_SmallIconSize); const int eyeSide = std::max(24, iconPm + 8); m_itemBackground = new QTreeWidgetItem(m_projectTree); const bool hasBg = m_workspace.isOpen() && m_workspace.hasBackground(); m_itemBackground->setText(1, hasBg ? QStringLiteral("背景") : QStringLiteral("背景(空白)")); m_itemBackground->setTextAlignment(1, Qt::AlignRight | Qt::AlignVCenter); m_itemBackground->setData(0, Qt::UserRole, QStringLiteral("background")); // “眼睛”按钮(固定尺寸,各行一致) auto makeEye = [this, eyeSide, iconPm](bool visible) -> QToolButton* { auto* btn = new QToolButton(m_projectTree); btn->setFixedSize(eyeSide, eyeSide); btn->setIconSize(QSize(iconPm, iconPm)); btn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); btn->setAutoRaise(true); btn->setCheckable(true); btn->setChecked(visible); btn->setToolTip(visible ? QStringLiteral("隐藏") : QStringLiteral("显示")); // 尽量用主题图标,失败则退化为文本 const QIcon onIcon = QIcon::fromTheme(QStringLiteral("view-visible")); const QIcon offIcon = QIcon::fromTheme(QStringLiteral("view-hidden")); if (!onIcon.isNull() && !offIcon.isNull()) { btn->setIcon(visible ? onIcon : offIcon); } else { btn->setText(visible ? QStringLiteral("👁") : QStringLiteral("×")); } connect(btn, &QToolButton::toggled, this, [btn](bool on) { btn->setToolTip(on ? QStringLiteral("隐藏") : QStringLiteral("显示")); const QIcon visIcon = QIcon::fromTheme(QStringLiteral("view-visible")); const QIcon hidIcon = QIcon::fromTheme(QStringLiteral("view-hidden")); if (!visIcon.isNull() && !hidIcon.isNull()) { btn->setIcon(on ? visIcon : hidIcon); } else { btn->setText(on ? QStringLiteral("👁") : QStringLiteral("×")); } }); return btn; }; const bool bgVisible = m_workspace.isOpen() ? m_workspace.project().backgroundVisible() : true; auto* bgEye = makeEye(bgVisible); bgEye->setEnabled(hasBg); m_projectTree->setItemWidget(m_itemBackground, 0, bgEye); connect(bgEye, &QToolButton::toggled, this, [this](bool on) { if (!m_workspace.isOpen()) return; if (!m_workspace.setBackgroundVisible(on)) return; refreshEditorPage(); refreshPreviewPage(); }); // 实体:与画布一致,按深度由远到近排列(远的在上,近的在下,绘制时近盖远) QVector sortedEnts; if (m_workspace.isOpen()) { sortedEnts = m_workspace.entities(); std::stable_sort(sortedEnts.begin(), sortedEnts.end(), [](const core::Project::Entity& a, const core::Project::Entity& b) { if (a.depth != b.depth) { return a.depth < b.depth; } return a.id < b.id; }); } for (const auto& e : sortedEnts) { auto* it = new QTreeWidgetItem(m_projectTree); it->setText(1, e.displayName.isEmpty() ? e.id : e.displayName); it->setTextAlignment(1, Qt::AlignRight | Qt::AlignVCenter); it->setData(0, Qt::UserRole, QStringLiteral("entity")); it->setData(0, Qt::UserRole + 1, e.id); it->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled); auto* eye = makeEye(e.visible); m_projectTree->setItemWidget(it, 0, eye); connect(eye, &QToolButton::toggled, this, [this, id = e.id](bool on) { if (!m_workspace.setEntityVisible(id, on)) return; refreshEditorPage(); updateUiEnabledState(); }); } if (m_projectTree->header()) { m_projectTree->header()->setSectionResizeMode(0, QHeaderView::Fixed); m_projectTree->setColumnWidth(0, eyeSide + 6); m_projectTree->header()->setSectionResizeMode(1, QHeaderView::Stretch); } syncProjectTreeFromCanvasSelection(); } void MainWindow::syncProjectTreeFromCanvasSelection() { if (!m_projectTree) { return; } m_syncingTreeSelection = true; m_projectTree->blockSignals(true); if (!m_hasSelectedEntity || m_selectedEntityId.isEmpty()) { m_projectTree->clearSelection(); } else { QTreeWidgetItem* found = nullptr; for (QTreeWidgetItemIterator it(m_projectTree); *it; ++it) { QTreeWidgetItem* node = *it; if (node->data(0, Qt::UserRole).toString() == QStringLiteral("entity") && node->data(0, Qt::UserRole + 1).toString() == m_selectedEntityId) { found = node; break; } } if (found) { m_projectTree->setCurrentItem(found); m_projectTree->scrollToItem(found); } else { m_projectTree->clearSelection(); } } m_projectTree->blockSignals(false); m_syncingTreeSelection = false; } void MainWindow::onProjectTreeItemClicked(QTreeWidgetItem* item, int column) { Q_UNUSED(column); if (!item || m_syncingTreeSelection || !m_editorCanvas || !m_workspace.isOpen()) { return; } const QString kind = item->data(0, Qt::UserRole).toString(); if (kind == QStringLiteral("entity")) { const QString id = item->data(0, Qt::UserRole + 1).toString(); if (!id.isEmpty()) { m_editorCanvas->selectEntityById(id); } } else if (kind == QStringLiteral("background")) { m_editorCanvas->clearEntitySelection(); } } void MainWindow::updateUiEnabledState() { const bool projectOpen = m_workspace.isOpen(); const bool hasBg = projectOpen && m_workspace.hasBackground(); const bool hasDepth = projectOpen && m_workspace.hasDepth(); if (!(projectOpen && hasBg) && m_previewRequested) { m_previewRequested = false; } // 背景为空白时:禁止除“设置背景”外的其它操作 if (m_actionUndo) m_actionUndo->setEnabled(projectOpen && hasBg && m_workspace.canUndo()); if (m_actionRedo) m_actionRedo->setEnabled(projectOpen && hasBg && m_workspace.canRedo()); if (m_actionCopy) m_actionCopy->setEnabled(projectOpen && hasBg); if (m_actionPaste) m_actionPaste->setEnabled(projectOpen && hasBg); if (m_actionEnterPreview) m_actionEnterPreview->setEnabled(projectOpen && hasBg && !m_previewRequested); if (m_actionBackToEditor) m_actionBackToEditor->setEnabled(projectOpen && m_previewRequested); if (m_btnToggleDepthOverlay) m_btnToggleDepthOverlay->setEnabled(hasDepth); if (m_bgPropertySection) m_bgPropertySection->setDepthOverlayCheckEnabled(hasDepth); // 创建实体不强依赖深度(无深度时 depth=0),但更推荐先算深度 if (m_btnCreateEntity) m_btnCreateEntity->setEnabled(hasBg); if (m_editorCanvas) { // 门禁:没有深度时只强制关闭叠加;创建实体仍允许 if (!hasDepth && m_editorCanvas->depthOverlayEnabled()) { m_editorCanvas->setDepthOverlayEnabled(false); if (m_btnToggleDepthOverlay) { m_btnToggleDepthOverlay->blockSignals(true); m_btnToggleDepthOverlay->setChecked(false); m_btnToggleDepthOverlay->blockSignals(false); } if (m_bgPropertySection) { m_bgPropertySection->syncDepthOverlayChecked(false); } if (m_actionCanvasDepthOverlay) { m_actionCanvasDepthOverlay->blockSignals(true); m_actionCanvasDepthOverlay->setChecked(false); m_actionCanvasDepthOverlay->blockSignals(false); } } } if (m_modeSelector) { m_modeSelector->setEnabled(projectOpen); m_modeSelector->blockSignals(true); m_modeSelector->setCurrentIndex(m_previewRequested ? 1 : 0); m_modeSelector->setItemData(0, projectOpen ? QVariant() : QVariant(0), Qt::UserRole - 1); m_modeSelector->setItemData(1, (projectOpen && hasBg) ? QVariant() : QVariant(0), Qt::UserRole - 1); m_modeSelector->blockSignals(false); } // 统一套用两态 UI 策略(欢迎/编辑) applyUiMode(currentUiMode()); const bool editorCanvasUi = projectOpen && !m_previewRequested; if (m_actionCanvasWorldAxes) m_actionCanvasWorldAxes->setEnabled(editorCanvasUi); if (m_actionCanvasAxisValues) { m_actionCanvasAxisValues->setEnabled(editorCanvasUi && m_editorCanvas && m_editorCanvas->worldAxesVisible()); } if (m_actionCanvasGrid) m_actionCanvasGrid->setEnabled(editorCanvasUi); if (m_actionCanvasCheckerboard) m_actionCanvasCheckerboard->setEnabled(editorCanvasUi); if (m_actionCanvasDepthOverlay) m_actionCanvasDepthOverlay->setEnabled(editorCanvasUi && hasDepth); if (m_actionCanvasGizmoLabels) m_actionCanvasGizmoLabels->setEnabled(editorCanvasUi); syncCanvasViewMenuFromState(); } void MainWindow::syncCanvasViewMenuFromState() { if (!m_editorCanvas) { return; } const auto syncCheck = [](QAction* a, bool checked) { if (!a) { return; } a->blockSignals(true); a->setChecked(checked); a->blockSignals(false); }; syncCheck(m_actionCanvasWorldAxes, m_editorCanvas->worldAxesVisible()); syncCheck(m_actionCanvasAxisValues, m_editorCanvas->axisLabelsVisible()); syncCheck(m_actionCanvasGrid, m_editorCanvas->gridVisible()); syncCheck(m_actionCanvasCheckerboard, m_editorCanvas->checkerboardVisible()); syncCheck(m_actionCanvasDepthOverlay, m_editorCanvas->depthOverlayEnabled()); syncCheck(m_actionCanvasGizmoLabels, m_editorCanvas->gizmoLabelsVisible()); if (m_bgPropertySection) { m_bgPropertySection->syncDepthOverlayChecked(m_editorCanvas->depthOverlayEnabled()); } } MainWindow::UiMode MainWindow::currentUiMode() const { if (!m_workspace.isOpen()) { return UiMode::Welcome; } if (m_previewRequested && m_workspace.hasBackground()) { return UiMode::Preview; } return UiMode::Editor; } void MainWindow::applyUiMode(UiMode mode) { const bool projectOpen = (mode == UiMode::Editor || mode == UiMode::Preview); const bool preview = (mode == UiMode::Preview); // 中央页面:欢迎 / 工作区(编辑与预览共用同一画布,仅状态不同) if (!projectOpen) { showWelcomePage(); } else { showEditorPage(); refreshEditorPage(); } // Dock 显隐策略: // - Welcome:所有 dock 必须隐藏,确保“未打开项目时只显示欢迎界面” // - Editor:按照默认规则显示(当前只有项目树) // - Preview:默认隐藏 dock,提供“纯展示”视图 if (m_dockProjectTree) { if (!projectOpen || preview) { m_dockProjectTree->setVisible(false); if (!projectOpen) { m_rightDocksNarrowHidden = false; } } else if (m_rightDocksNarrowHidden) { m_dockProjectTree->setVisible(false); } else { m_dockProjectTree->setVisible(true); } } if (m_dockProperties) { if (!projectOpen || preview) { m_dockProperties->setVisible(false); } else if (m_rightDocksNarrowHidden) { m_dockProperties->setVisible(false); } else { m_dockProperties->setVisible(true); } } if (m_dockTimeline) { m_dockTimeline->setVisible(projectOpen && !preview); } if (m_floatingModeDock) { m_floatingModeDock->setVisible(projectOpen); } if (m_floatingToolDock) { m_floatingToolDock->setVisible(projectOpen && !preview); } if (m_previewPlaybackBar) { m_previewPlaybackBar->setVisible(projectOpen && preview); } if (m_canvasHost) { m_canvasHost->updateGeometry(); static_cast(m_canvasHost)->relayoutFloaters(); m_canvasHost->update(); } // 视图菜单开关: // - Welcome:禁用并强制取消勾选(避免用户把 dock 再显示出来) // - Editor:启用,并与 dock 可见性保持一致 // - Preview:仍允许切回编辑(通过预览开关),但 dock 开关禁用以保持展示简洁 if (m_actionToggleProjectTree) { m_actionToggleProjectTree->setEnabled(projectOpen && !preview); m_actionToggleProjectTree->blockSignals(true); m_actionToggleProjectTree->setChecked(projectOpen && !preview && m_dockProjectTree && m_dockProjectTree->isVisible()); m_actionToggleProjectTree->blockSignals(false); } if (m_actionToggleProperties) { m_actionToggleProperties->setEnabled(projectOpen && !preview); m_actionToggleProperties->blockSignals(true); m_actionToggleProperties->setChecked(projectOpen && !preview && m_dockProperties && m_dockProperties->isVisible()); m_actionToggleProperties->blockSignals(false); } if (m_actionToggleTimeline) { m_actionToggleTimeline->setEnabled(projectOpen && !preview); m_actionToggleTimeline->blockSignals(true); m_actionToggleTimeline->setChecked(projectOpen && !preview && m_dockTimeline && m_dockTimeline->isVisible()); m_actionToggleTimeline->blockSignals(false); } } void MainWindow::showProjectRootContextMenu(const QPoint& globalPos) { QMenu menu(this); QAction* actRename = menu.addAction(QStringLiteral("重命名项目…")); menu.addSeparator(); QAction* actPreview = menu.addAction(QStringLiteral("进入预览")); QAction* actBack = menu.addAction(QStringLiteral("返回编辑")); const bool canPreview = m_workspace.isOpen() && m_workspace.hasBackground(); actPreview->setEnabled(canPreview); actBack->setEnabled(m_previewRequested); QAction* chosen = menu.exec(globalPos); if (!chosen) { return; } if (chosen == actRename) { bool ok = false; const QString cur = m_workspace.project().name(); const QString t = QInputDialog::getText(this, QStringLiteral("重命名项目"), QStringLiteral("项目名称:"), QLineEdit::Normal, cur, &ok); if (!ok) { return; } if (!m_workspace.setProjectTitle(t)) { QMessageBox::warning(this, QStringLiteral("重命名"), QStringLiteral("名称无效或保存失败。")); return; } refreshProjectTree(); refreshPropertyPanel(); updateUiEnabledState(); return; } if (chosen == actPreview) { setPreviewRequested(true); } else if (chosen == actBack) { setPreviewRequested(false); } } void MainWindow::rebuildCentralPages() { m_centerStack = new QStackedWidget(this); auto* centerShell = new QWidget(this); auto* shellLayout = new QVBoxLayout(centerShell); shellLayout->setContentsMargins(0, 0, 0, 0); shellLayout->setSpacing(0); shellLayout->addWidget(m_centerStack, 1); // 欢迎页(左:操作说明;右:最近项目,类似 Qt Creator) m_pageWelcome = new QWidget(m_centerStack); auto* welcomeRoot = new QHBoxLayout(m_pageWelcome); welcomeRoot->setContentsMargins(40, 40, 40, 40); welcomeRoot->setSpacing(32); auto* welcomeLeft = new QVBoxLayout(); welcomeLeft->setSpacing(16); auto* title = new QLabel(QStringLiteral("欢迎使用"), m_pageWelcome); QFont f = title->font(); f.setPointSize(f.pointSize() + 6); f.setBold(true); title->setFont(f); welcomeLeft->addWidget(title); auto* desc = new QLabel(QStringLiteral("请创建或打开一个项目。"), m_pageWelcome); desc->setWordWrap(true); welcomeLeft->addWidget(desc); auto* buttonsRow = new QHBoxLayout(); auto* btnCreate = new QPushButton(QStringLiteral("创建项目"), m_pageWelcome); auto* btnOpen = new QPushButton(QStringLiteral("打开项目"), m_pageWelcome); buttonsRow->addWidget(btnCreate); buttonsRow->addWidget(btnOpen); buttonsRow->addStretch(1); welcomeLeft->addLayout(buttonsRow); welcomeLeft->addStretch(1); connect(btnCreate, &QPushButton::clicked, this, &MainWindow::onNewProject); connect(btnOpen, &QPushButton::clicked, this, &MainWindow::onOpenProject); auto* recentFrame = new QFrame(m_pageWelcome); recentFrame->setFrameShape(QFrame::StyledPanel); auto* recentOuter = new QVBoxLayout(recentFrame); recentOuter->setContentsMargins(16, 16, 16, 16); recentOuter->setSpacing(8); auto* recentTitle = new QLabel(QStringLiteral("最近的项目"), recentFrame); QFont rf = recentTitle->font(); rf.setBold(true); recentTitle->setFont(rf); recentOuter->addWidget(recentTitle); m_welcomeRecentEmptyLabel = new QLabel(QStringLiteral("暂无最近打开的项目"), recentFrame); m_welcomeRecentEmptyLabel->setStyleSheet(QStringLiteral("QLabel { color: palette(mid); }")); m_welcomeRecentEmptyLabel->setWordWrap(true); recentOuter->addWidget(m_welcomeRecentEmptyLabel); m_welcomeRecentTree = new QTreeWidget(recentFrame); m_welcomeRecentTree->setHeaderLabels({QStringLiteral("项目名称"), QStringLiteral("路径")}); m_welcomeRecentTree->setRootIsDecorated(false); m_welcomeRecentTree->setAlternatingRowColors(true); m_welcomeRecentTree->setSelectionMode(QAbstractItemView::SingleSelection); m_welcomeRecentTree->setSelectionBehavior(QAbstractItemView::SelectRows); m_welcomeRecentTree->setUniformRowHeights(true); m_welcomeRecentTree->setContextMenuPolicy(Qt::CustomContextMenu); m_welcomeRecentTree->header()->setStretchLastSection(true); m_welcomeRecentTree->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); m_welcomeRecentTree->header()->setSectionResizeMode(1, QHeaderView::Stretch); recentOuter->addWidget(m_welcomeRecentTree, 1); connect(m_welcomeRecentTree, &QTreeWidget::itemDoubleClicked, this, [this](QTreeWidgetItem* item, int) { if (!item) { return; } const QString path = item->data(0, Qt::UserRole).toString(); if (!path.isEmpty()) { openProjectFromPath(path); } }); connect(m_welcomeRecentTree, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) { QTreeWidgetItem* item = m_welcomeRecentTree->itemAt(pos); if (!item) { return; } QMenu menu(this); QAction* actRemove = menu.addAction(QStringLiteral("从列表中移除")); QAction* chosen = menu.exec(m_welcomeRecentTree->viewport()->mapToGlobal(pos)); if (chosen == actRemove) { const QString path = item->data(0, Qt::UserRole).toString(); if (!path.isEmpty()) { m_recentHistory.removeAndSave(path); refreshWelcomeRecentList(); } } }); welcomeRoot->addLayout(welcomeLeft, 1); welcomeRoot->addWidget(recentFrame, 1); // 工作区:全屏画布 + 左上角浮动模式切换 + 左侧浮动工具栏 auto* canvasHost = new CanvasHost(m_centerStack); m_pageEditor = canvasHost; m_canvasHost = canvasHost; 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); m_floatingModeDock->setStyleSheet(QString::fromUtf8(kFloatingModeDockQss)); auto* modeDockLayout = new QHBoxLayout(m_floatingModeDock); modeDockLayout->setContentsMargins(8, 4, 10, 4); modeDockLayout->setSpacing(0); m_modeSelector = new QComboBox(m_floatingModeDock); m_modeSelector->addItem(QStringLiteral("编辑")); m_modeSelector->addItem(QStringLiteral("预览")); { QFontMetrics fm(m_modeSelector->font()); int textW = 0; for (int i = 0; i < m_modeSelector->count(); ++i) { textW = std::max(textW, fm.horizontalAdvance(m_modeSelector->itemText(i))); } const int indicator = m_modeSelector->style()->pixelMetric(QStyle::PM_ScrollBarExtent, nullptr, m_modeSelector); // 文本 + 下拉箭头 + QSS 水平 padding(约 6+6)与边框余量 m_modeSelector->setFixedWidth(textW + indicator + 14); } connect(m_modeSelector, &QComboBox::currentIndexChanged, this, [this](int index) { setPreviewRequested(index == 1); }); modeDockLayout->addWidget(m_modeSelector); canvasHost->modeDock = m_floatingModeDock; m_floatingToolDock = new QFrame(canvasHost); m_floatingToolDock->setObjectName(QStringLiteral("EditorToolRail")); m_floatingToolDock->setFrameShape(QFrame::NoFrame); m_floatingToolDock->setStyleSheet(QString::fromUtf8(kEditorToolRailQss)); m_floatingToolDock->setFixedWidth(52); auto* toolLayout = new QVBoxLayout(m_floatingToolDock); toolLayout->setContentsMargins(6, 8, 6, 8); toolLayout->setSpacing(6); auto* group = new QButtonGroup(m_floatingToolDock); group->setExclusive(true); auto* btnMove = new QToolButton(m_floatingToolDock); btnMove->setCheckable(true); btnMove->setChecked(true); setToolButtonIconOrText(btnMove, QStringLiteral("transform-move"), QStringLiteral("移")); btnMove->setToolTip(QStringLiteral("移动")); polishCompactToolButton(btnMove, 40); toolLayout->addWidget(btnMove, 0, Qt::AlignHCenter); group->addButton(btnMove, static_cast(EditorCanvas::Tool::Move)); auto* btnZoom = new QToolButton(m_floatingToolDock); btnZoom->setCheckable(true); setToolButtonIconOrText(btnZoom, QStringLiteral("zoom-in"), QStringLiteral("放")); btnZoom->setToolTip(QStringLiteral("缩放")); polishCompactToolButton(btnZoom, 40); toolLayout->addWidget(btnZoom, 0, Qt::AlignHCenter); group->addButton(btnZoom, static_cast(EditorCanvas::Tool::Zoom)); m_btnCreateEntity = new QToolButton(m_floatingToolDock); m_btnCreateEntity->setCheckable(true); setToolButtonIconOrText(m_btnCreateEntity, QStringLiteral("draw-brush"), QStringLiteral("创")); m_btnCreateEntity->setToolTip(QStringLiteral("创建实体")); polishCompactToolButton(m_btnCreateEntity, 40); toolLayout->addWidget(m_btnCreateEntity, 0, Qt::AlignHCenter); group->addButton(m_btnCreateEntity, static_cast(EditorCanvas::Tool::CreateEntity)); m_btnToggleDepthOverlay = new QToolButton(m_floatingToolDock); m_btnToggleDepthOverlay->setCheckable(true); m_btnToggleDepthOverlay->setChecked(false); setToolButtonIconOrText(m_btnToggleDepthOverlay, QStringLiteral("color-profile"), QStringLiteral("深")); m_btnToggleDepthOverlay->setToolTip(QStringLiteral("深度叠加")); polishCompactToolButton(m_btnToggleDepthOverlay, 40); toolLayout->addWidget(m_btnToggleDepthOverlay, 0, Qt::AlignHCenter); auto* btnFit = new QToolButton(m_floatingToolDock); setToolButtonIconOrText(btnFit, QStringLiteral("zoom-fit-best"), QStringLiteral("框")); btnFit->setToolTip(QStringLiteral("适配视口")); polishCompactToolButton(btnFit, 40); toolLayout->addWidget(btnFit, 0, Qt::AlignHCenter); toolLayout->addStretch(1); canvasHost->toolDock = m_floatingToolDock; m_floatingModeDock->setParent(canvasHost); m_floatingToolDock->setParent(canvasHost); m_editorCanvas->setParent(canvasHost); m_floatingToolDock->raise(); m_floatingModeDock->raise(); connect(m_editorCanvas, &EditorCanvas::hoveredWorldPosChanged, this, [this](const QPointF& p) { m_lastWorldPos = p; updateStatusBarText(); }); connect(m_editorCanvas, &EditorCanvas::hoveredWorldPosDepthChanged, this, [this](const QPointF& p, int z) { m_lastWorldPos = p; m_lastWorldZ = z; updateStatusBarText(); }); connect(m_editorCanvas, &EditorCanvas::selectedEntityChanged, this, [this](bool hasSel, const QString& id, int depth, const QPointF& origin) { if (m_timelineScrubbing) { return; } m_hasSelectedEntity = hasSel; m_selectedEntityId = id; m_selectedEntityDepth = depth; m_selectedEntityOrigin = origin; updateStatusBarText(); refreshPropertyPanel(); syncProjectTreeFromCanvasSelection(); if (m_timeline && m_workspace.isOpen() && hasSel && !id.isEmpty()) { const core::Project::Entity* hit = nullptr; for (const auto& e : m_workspace.entities()) { if (e.id == id) { hit = &e; m_selectedEntityDisplayNameCache = e.displayName; break; } } m_timeline->setKeyframeTracks(hit); } else if (m_timeline) { m_timeline->setKeyframeTracks(nullptr); } }); connect(m_editorCanvas, &EditorCanvas::entityDragActiveChanged, this, [this](bool on) { m_entityDragging = on; if (on && m_btnPlay && m_btnPlay->isChecked()) { // 拖动实体时自动暂停,避免播放驱动时间轴刷新干扰拖动 m_btnPlay->setChecked(false); } }); connect(m_editorCanvas, &EditorCanvas::selectedEntityPreviewChanged, this, [this](const QString& id, int depth, const QPointF& origin) { if (id.isEmpty() || !m_workspace.isOpen() || !m_entityPropertySection) { return; } // 拖动中低频同步属性面板,不重建控件 m_hasSelectedEntity = true; m_selectedEntityId = id; m_selectedEntityDepth = depth; m_selectedEntityOrigin = origin; if (m_propertySyncTimer) { if (!m_propertySyncTimer->isActive()) { // 属性同步 30Hz:避免拖动时 UI 抢占 m_propertySyncTimer->start(33); } } else { refreshPropertyPanel(); } }); connect(m_editorCanvas, &EditorCanvas::requestAddEntity, this, [this](const core::Project::Entity& e, const QImage& img) { core::Project::Entity ent = e; if (ent.id.isEmpty()) { // 生成稳定且不重复的 id:entity- QSet used; for (const auto& ex : m_workspace.entities()) { used.insert(ex.id); } int n = static_cast(m_workspace.entities().size()) + 1; for (int guard = 0; guard < 100000; ++guard, ++n) { const QString cand = QStringLiteral("entity-%1").arg(n); if (!used.contains(cand)) { ent.id = cand; break; } } } if (!m_workspace.addEntity(ent, img)) { QMessageBox::warning(this, QStringLiteral("实体"), QStringLiteral("保存实体失败。")); return; } refreshEditorPage(); refreshProjectTree(); updateUiEnabledState(); }); connect(m_editorCanvas, &EditorCanvas::requestMoveEntity, this, [this](const QString& id, const QPointF& delta) { // 动画编辑:拖动即写入当前位置关键帧,避免被既有关键帧插值“拉回去” const bool autoKey = true; if (!m_workspace.moveEntityBy(id, delta, m_currentFrame, autoKey)) { return; } refreshEditorPage(); refreshDopeSheet(); 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; } m_editorCanvas->setTool(static_cast(id)); }); connect(m_btnToggleDepthOverlay, &QToolButton::toggled, this, [this](bool on) { if (m_editorCanvas) { m_editorCanvas->setDepthOverlayEnabled(on); } if (m_actionCanvasDepthOverlay) { m_actionCanvasDepthOverlay->blockSignals(true); m_actionCanvasDepthOverlay->setChecked(on); m_actionCanvasDepthOverlay->blockSignals(false); } if (m_bgPropertySection) { m_bgPropertySection->syncDepthOverlayChecked(on); } }); connect(btnFit, &QToolButton::clicked, this, [this]() { if (m_editorCanvas) { m_editorCanvas->zoomToFit(); } }); m_centerStack->addWidget(m_pageWelcome); m_centerStack->addWidget(m_pageEditor); setCentralWidget(centerShell); showWelcomePage(); } void MainWindow::showWelcomePage() { if (m_centerStack && m_pageWelcome) { m_centerStack->setCurrentWidget(m_pageWelcome); } refreshWelcomeRecentList(); } void MainWindow::refreshWelcomeRecentList() { if (!m_welcomeRecentTree || !m_welcomeRecentEmptyLabel) { return; } m_welcomeRecentTree->clear(); const QStringList paths = m_recentHistory.load(); m_welcomeRecentEmptyLabel->setVisible(paths.isEmpty()); m_welcomeRecentTree->setVisible(!paths.isEmpty()); const QFontMetrics fm(m_welcomeRecentTree->font()); const int vw = m_welcomeRecentTree->viewport()->width(); const int elideW = std::max(160, vw - 200); for (const QString& path : paths) { auto* item = new QTreeWidgetItem(m_welcomeRecentTree); item->setText(0, QFileInfo(path).fileName()); item->setText(1, fm.elidedText(path, Qt::ElideMiddle, elideW)); item->setToolTip(0, path); item->setToolTip(1, path); item->setData(0, Qt::UserRole, path); } } void MainWindow::openProjectFromPath(const QString& dir) { if (dir.isEmpty()) { return; } if (m_workspace.isOpen()) { onCloseProject(); } if (!m_workspace.openExisting(dir)) { QMessageBox::warning(this, QStringLiteral("打开项目"), QStringLiteral("打开项目失败(缺少或损坏 project.json)。")); m_recentHistory.removeAndSave(dir); refreshWelcomeRecentList(); return; } m_recentHistory.addAndSave(m_workspace.projectDir()); refreshWelcomeRecentList(); statusBar()->showMessage(QStringLiteral("项目已打开:%1").arg(m_workspace.projectDir())); refreshEditorPage(); refreshProjectTree(); updateUiEnabledState(); refreshPreviewPage(); } void MainWindow::showEditorPage() { if (m_centerStack && m_pageEditor) { m_centerStack->setCurrentWidget(m_pageEditor); } if (m_canvasHost) { m_canvasHost->updateGeometry(); static_cast(m_canvasHost)->relayoutFloaters(); QTimer::singleShot(0, m_canvasHost, [this]() { if (m_canvasHost) { static_cast(m_canvasHost)->relayoutFloaters(); } }); } } void MainWindow::showPreviewPage() { showEditorPage(); } void MainWindow::setPreviewRequested(bool preview) { if (preview && (!m_workspace.isOpen() || !m_workspace.hasBackground())) { return; } m_previewRequested = preview; if (!preview && m_editorCanvas) { m_editorCanvas->clearPresentationEntityFocus(); } if (!preview && m_entityIntroPopup) { m_entityIntroPopup->clearAndHide(); } updateUiEnabledState(); if (preview) { refreshPreviewPage(); } } void MainWindow::refreshPreviewPage() { refreshEditorPage(); } void MainWindow::refreshEditorPage() { if (!m_pageEditor) { return; } const bool open = m_workspace.isOpen(); const auto bgAbs = open ? m_workspace.backgroundAbsolutePath() : QString(); if (m_editorCanvas) { const bool presentation = open && m_previewRequested && m_workspace.hasBackground(); m_editorCanvas->setPresentationPreviewMode(presentation); m_editorCanvas->setBackgroundImagePath(bgAbs); m_editorCanvas->setBackgroundVisible(open ? m_workspace.project().backgroundVisible() : true); m_editorCanvas->setDepthMapPath(open ? m_workspace.depthAbsolutePath() : QString()); if (open) { applyTimelineFromProject(); m_editorCanvas->setEntities(m_workspace.entities(), m_workspace.projectDir()); m_editorCanvas->setCurrentFrame(m_currentFrame); if (m_timeline && !m_selectedEntityId.isEmpty()) { const core::Project::Entity* hit = nullptr; for (const auto& e : m_workspace.entities()) { if (e.id == m_selectedEntityId) { hit = &e; break; } } m_timeline->setKeyframeTracks(hit); } else if (m_timeline) { m_timeline->setKeyframeTracks(nullptr); } } else { m_editorCanvas->setEntities({}, QString()); if (m_timeline) { m_timeline->setKeyframeTracks(nullptr); } } } refreshPropertyPanel(); if (m_canvasHost) { m_canvasHost->updateGeometry(); static_cast(m_canvasHost)->relayoutFloaters(); QTimer::singleShot(0, m_canvasHost, [this]() { if (m_canvasHost) { static_cast(m_canvasHost)->relayoutFloaters(); } }); } refreshDopeSheet(); } void MainWindow::applyTimelineFromProject() { if (!m_timeline || !m_workspace.isOpen()) { return; } const int v = std::clamp(m_currentFrame, 0, 600); m_currentFrame = v; m_timeline->setFrameRange(0, 600); m_timeline->setCurrentFrame(v); if (m_frameLabel) { m_frameLabel->setText(QString::number(v)); } if (m_editorCanvas) { m_editorCanvas->setCurrentFrame(v); } } void MainWindow::refreshDopeSheet() { if (!m_dopeTree) { return; } m_dopeTree->clear(); if (!m_workspace.isOpen()) { return; } const int f = m_currentFrame; const auto& ents = m_workspace.entities(); for (const auto& e : ents) { auto* parent = new QTreeWidgetItem(m_dopeTree); parent->setText(0, e.displayName.isEmpty() ? e.id : e.displayName); parent->setData(0, Qt::UserRole, e.id); parent->setData(0, Qt::UserRole + 1, -1); parent->setText(1, QString()); auto addChannel = [&](const QString& label, int channel, bool hasKey) { auto* ch = new QTreeWidgetItem(parent); ch->setText(0, label); ch->setData(0, Qt::UserRole, e.id); ch->setData(0, Qt::UserRole + 1, channel); ch->setText(1, hasKey ? QStringLiteral("●") : QStringLiteral("—")); }; bool hasLoc = false; for (const auto& k : e.locationKeys) { if (k.frame == f) { hasLoc = true; break; } } bool hasSc = false; for (const auto& k : e.depthScaleKeys) { if (k.frame == f) { hasSc = true; break; } } bool hasIm = false; for (const auto& k : e.imageFrames) { if (k.frame == f) { hasIm = true; break; } } addChannel(QStringLiteral("位置"), 0, hasLoc); addChannel(QStringLiteral("缩放"), 1, hasSc); addChannel(QStringLiteral("图像"), 2, hasIm); } m_dopeTree->expandAll(); } void MainWindow::showBackgroundContextMenu(const QPoint& globalPos) { QMenu menu(this); QAction* actComputeDepth = menu.addAction(QStringLiteral("计算深度(后端)")); actComputeDepth->setEnabled(m_workspace.isOpen() && m_workspace.hasBackground()); QAction* chosen = menu.exec(globalPos); if (!chosen) { return; } if (chosen == actComputeDepth) { computeDepthAsync(); } refreshProjectTree(); updateUiEnabledState(); refreshEditorPage(); refreshPreviewPage(); } void MainWindow::onNewProject() { if (m_workspace.isOpen()) { onCloseProject(); } // 选择父目录:项目会在该目录下自动创建一个新文件夹 const auto parentDir = QFileDialog::getExistingDirectory(this, "选择父目录"); if (parentDir.isEmpty()) { return; } bool ok = false; const auto name = QInputDialog::getText(this, "新项目", QStringLiteral("项目名称:"), QLineEdit::Normal, QStringLiteral("新项目"), &ok); if (!ok) { return; } const auto imagePath = QFileDialog::getOpenFileName( this, QStringLiteral("选择背景图片"), QString(), QStringLiteral("Images (*.png *.jpg *.jpeg *.bmp *.webp);;All Files (*)")); if (imagePath.isEmpty()) { QMessageBox::warning(this, QStringLiteral("新项目"), QStringLiteral("创建项目失败:必须选择背景图片。")); return; } ImageCropDialog crop(imagePath, this); QRect cropRect; // null 表示不裁剪 -> 使用整图 if (crop.exec() == QDialog::Accepted) { // 用户点了“确定”但没有选择裁剪区域:按“不裁剪”处理,使用整图 if (crop.hasValidSelection()) { cropRect = crop.selectedRectInImagePixels(); if (cropRect.isNull()) { QMessageBox::warning(this, QStringLiteral("新项目"), QStringLiteral("创建项目失败:裁剪区域无效。")); return; } } } if (!m_workspace.createNew(parentDir, name, imagePath, cropRect)) { QMessageBox::warning(this, QStringLiteral("新项目"), QStringLiteral("创建项目失败。")); return; } m_recentHistory.addAndSave(m_workspace.projectDir()); refreshWelcomeRecentList(); statusBar()->showMessage(QStringLiteral("项目已创建:%1").arg(m_workspace.projectDir())); refreshEditorPage(); refreshProjectTree(); updateUiEnabledState(); refreshPreviewPage(); } void MainWindow::onOpenProject() { if (m_workspace.isOpen()) { onCloseProject(); } const auto dir = QFileDialog::getExistingDirectory(this, QStringLiteral("打开项目")); if (dir.isEmpty()) { return; } openProjectFromPath(dir); } void MainWindow::onSaveProject() { // TODO: } void MainWindow::onCloseProject() { if (!m_workspace.isOpen()) { return; } m_previewRequested = false; m_playing = false; if (m_playTimer) { m_playTimer->stop(); } if (m_btnPlay) { m_btnPlay->blockSignals(true); m_btnPlay->setChecked(false); m_btnPlay->setText(QStringLiteral("▶")); m_btnPlay->blockSignals(false); } m_workspace.close(); m_rightDocksNarrowHidden = false; m_hasSelectedEntity = false; m_selectedEntityDepth = 0; m_selectedEntityOrigin = QPointF(); m_selectedEntityId.clear(); m_currentFrame = 0; statusBar()->showMessage(QStringLiteral("工程已关闭")); refreshEditorPage(); refreshProjectTree(); updateUiEnabledState(); refreshPreviewPage(); } void MainWindow::onUndo() { if (!m_workspace.undo()) { statusBar()->showMessage(QStringLiteral("无法撤销")); return; } refreshEditorPage(); refreshProjectTree(); updateUiEnabledState(); refreshPreviewPage(); } void MainWindow::onRedo() { if (!m_workspace.redo()) { statusBar()->showMessage(QStringLiteral("无法重做")); return; } refreshEditorPage(); refreshProjectTree(); updateUiEnabledState(); refreshPreviewPage(); } void MainWindow::onCopyObject() { // TODO: } void MainWindow::onPasteObject() { // TODO: } void MainWindow::onAbout() { auto aboutDialog = new AboutWindow(this); aboutDialog->exec(); } bool MainWindow::eventFilter(QObject* watched, QEvent* event) { if (event->type() == QEvent::Resize && watched == m_dockProjectTree) { if (m_dockProjectTree && m_workspace.isOpen() && !m_previewRequested && !m_dockProjectTree->isFloating()) { const int w = m_dockProjectTree->width(); if (w > 0 && w < kRightDockAutoHideBelow && m_dockProjectTree->isVisible()) { m_rightDocksNarrowHidden = true; m_dockProjectTree->hide(); if (m_dockProperties) { m_dockProperties->hide(); } } } } return false; }