Files
hfut-bishe/client/gui/main_window/MainWindow.cpp
2026-04-08 14:37:01 +08:00

2634 lines
102 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "main_window/MainWindow.h"
#include "dialogs/AboutWindow.h"
#include "dialogs/CancelableTaskDialog.h"
#include "dialogs/EntityFinalizeDialog.h"
#include "editor/EditorCanvas.h"
#include "editor/EntityCutoutUtils.h"
#include "dialogs/ImageCropDialog.h"
#include "core/domain/EntityIntro.h"
#include "core/net/ModelServerClient.h"
#include "widgets/ToolOptionPopup.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>
#include <QCheckBox>
#include <QComboBox>
#include <QDockWidget>
#include <QEvent>
#include <QFrame>
#include <QTimer>
#include <QFormLayout>
#include <QInputDialog>
#include <QLabel>
#include <QMenu>
#include <QMenuBar>
#include <QPushButton>
#include <QStackedWidget>
#include <QStatusBar>
#include <QToolButton>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QTreeWidgetItemIterator>
#include <QLineEdit>
#include <QFileDialog>
#include <QFileInfo>
#include <QFontMetrics>
#include <QHeaderView>
#include <QImage>
#include <QMessageBox>
#include <QPixmap>
#include <QFile>
#include <QIcon>
#include <QResizeEvent>
#include <QScrollArea>
#include <QShowEvent>
#include <QtGlobal>
#include <QWheelEvent>
#include <QSizePolicy>
#include <QStyle>
#include <QUrl>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QSet>
#include <QJsonObject>
#include <QJsonArray>
#include <QSet>
#include <algorithm>
namespace {
/// 右侧项目树/属性 dock 水平方向可拖到的最小宽度(须小于 kRightDockAutoHideBelow否则无法触发自动隐藏
constexpr int kRightDockMinimumWidth = 80;
/// 列宽小于此值时自动隐藏右侧两 dock
constexpr int kRightDockAutoHideBelow = 92;
/// 右侧 dock 列最大宽度,避免过宽挤占画布
constexpr int kRightDockMaximumWidth = 288;
/// 属性区表单内容最大宽度dock 仍可略宽,两侧留白,避免 SpinBox 被拉得过开)
constexpr int kPropertyPanelContentMaxWidth = 268;
/// 启动时垂直分割高度:项目树较矮、属性区较高
constexpr int kProjectTreeDockStartupHeight = 148;
constexpr int kPropertiesDockStartupHeight = 392;
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<QAbstractSpinBox*>(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::syncCreateEntityToolButtonTooltip() {
if (!m_btnCreateEntity || !m_editorCanvas) {
return;
}
using Mode = EditorCanvas::EntityCreateSegmentMode;
const Mode m = m_editorCanvas->entityCreateSegmentMode();
if (m == Mode::Manual) {
m_btnCreateEntity->setToolTip(QStringLiteral("创建实体:手动分割\n"
"再次单击本按钮可选择:手动/吸附/模型"));
} else if (m == Mode::Snap) {
m_btnCreateEntity->setToolTip(QStringLiteral("创建实体:吸附分割(前端边缘吸附算法)\n"
"再次单击本按钮可选择:手动/吸附/模型"));
} else {
m_btnCreateEntity->setToolTip(QStringLiteral("创建实体模型分割SAM\n"
"再次单击本按钮可选择:手动/吸附/模型"));
}
}
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(236);
m_bgPropertySection = new gui::BackgroundPropertySection();
m_entityPropertySection = new gui::EntityPropertySection();
m_propertyStack = new QStackedWidget();
m_propertyStack->setContentsMargins(4, 4, 4, 4);
m_propertyStack->setMaximumWidth(kPropertyPanelContentMaxWidth);
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},
{kProjectTreeDockStartupHeight, kPropertiesDockStartupHeight},
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);
}
});
// 停靠时限制右侧列最大宽度;浮动时解除,避免属性/项目树窗口过窄难用
const auto applyRightDockColumnMaxWidth = [](QDockWidget* dock) {
if (!dock) {
return;
}
dock->setMaximumWidth(dock->isFloating() ? QWIDGETSIZE_MAX : kRightDockMaximumWidth);
};
applyRightDockColumnMaxWidth(m_dockProjectTree);
applyRightDockColumnMaxWidth(m_dockProperties);
connect(m_dockProjectTree, &QDockWidget::topLevelChanged, this,
[applyRightDockColumnMaxWidth, this](bool) {
applyRightDockColumnMaxWidth(m_dockProjectTree);
applyRightDockColumnMaxWidth(m_dockProperties);
});
connect(m_dockProperties, &QDockWidget::topLevelChanged, this,
[applyRightDockColumnMaxWidth, this](bool) {
applyRightDockColumnMaxWidth(m_dockProjectTree);
applyRightDockColumnMaxWidth(m_dockProperties);
});
}
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<core::Project::Entity> 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<CanvasHost*>(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<int>(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<int>(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<int>(EditorCanvas::Tool::CreateEntity));
if (!m_createEntityPopup) {
m_createEntityPopup = new ToolOptionPopup(this);
m_createEntityPopup->setOptions({
{static_cast<int>(EditorCanvas::EntityCreateSegmentMode::Manual), QStringLiteral("手动分割")},
{static_cast<int>(EditorCanvas::EntityCreateSegmentMode::Snap), QStringLiteral("吸附分割")},
{static_cast<int>(EditorCanvas::EntityCreateSegmentMode::Sam), QStringLiteral("模型分割")},
});
connect(m_createEntityPopup, &ToolOptionPopup::optionChosen, this, [this](int id) {
if (!m_editorCanvas) return;
m_editorCanvas->setEntityCreateSegmentMode(static_cast<EditorCanvas::EntityCreateSegmentMode>(id));
syncCreateEntityToolButtonTooltip();
statusBar()->showMessage(QStringLiteral("已切换分割方式"));
});
}
connect(m_btnCreateEntity, &QToolButton::clicked, this, [this]() {
if (!m_editorCanvas || !m_btnCreateEntity) return;
if (m_btnCreateEntity->isChecked() && m_editorCanvas->tool() == EditorCanvas::Tool::CreateEntity) {
// 已选中时再次单击:弹出选择面板
if (m_createEntityPopup) {
m_createEntityPopup->popupNearToolButton(m_btnCreateEntity);
}
}
});
syncCreateEntityToolButtonTooltip();
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()) {
// 生成稳定且不重复的 identity-<n>
QSet<QString> used;
for (const auto& ex : m_workspace.entities()) {
used.insert(ex.id);
}
int n = static_cast<int>(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::requestSamSegment, this,
[this](const QByteArray& cropRgbPng,
const QByteArray& overlayPng,
const QPointF& cropTopLeftWorld,
const QJsonArray& pointCoords,
const QJsonArray& pointLabels,
const QJsonArray& boxXyxy) {
if (!m_workspace.isOpen() || !m_workspace.hasBackground()) {
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->segmentSamPromptAsync(
cropRgbPng, overlayPng, pointCoords, pointLabels, boxXyxy, &immediateErr);
if (!reply) {
QMessageBox::warning(this,
QStringLiteral("SAM 分割"),
immediateErr.isEmpty() ? QStringLiteral("无法发起后端请求。") : immediateErr);
client->deleteLater();
return;
}
auto* dlg = new CancelableTaskDialog(QStringLiteral("SAM 分割"),
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, cropTopLeftWorld]() {
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) {
if (netErrStr.contains(QStringLiteral("canceled"), Qt::CaseInsensitive) ||
netErr == QNetworkReply::OperationCanceledError) {
statusBar()->showMessage(QStringLiteral("已取消分割"));
return;
}
QMessageBox::warning(this, QStringLiteral("SAM 分割"),
QStringLiteral("网络错误:%1").arg(netErrStr));
return;
}
if (httpStatus != 200) {
QString detail;
const QJsonDocument jerr = QJsonDocument::fromJson(raw);
if (jerr.isObject()) {
detail = jerr.object().value(QStringLiteral("detail")).toString();
}
QMessageBox::warning(
this,
QStringLiteral("SAM 分割"),
detail.isEmpty() ? QStringLiteral("后端返回 HTTP %1。").arg(httpStatus)
: QStringLiteral("后端错误HTTP %1%2").arg(httpStatus).arg(detail));
return;
}
const QJsonDocument jd = QJsonDocument::fromJson(raw);
if (!jd.isObject()) {
QMessageBox::warning(this, QStringLiteral("SAM 分割"), QStringLiteral("响应不是 JSON。"));
return;
}
const QJsonObject obj = jd.object();
if (!obj.value(QStringLiteral("success")).toBool()) {
const QString err = obj.value(QStringLiteral("error")).toString();
QMessageBox::warning(this, QStringLiteral("SAM 分割"),
err.isEmpty() ? QStringLiteral("分割失败。") : err);
return;
}
const QJsonArray contour = obj.value(QStringLiteral("contour")).toArray();
if (contour.size() < 3) {
QMessageBox::warning(this, QStringLiteral("SAM 分割"), QStringLiteral("轮廓点数不足。"));
return;
}
QVector<QPointF> polyWorld;
polyWorld.reserve(contour.size());
for (const QJsonValue& v : contour) {
if (!v.isArray()) {
continue;
}
const QJsonArray p = v.toArray();
if (p.size() < 2) {
continue;
}
const double x = p.at(0).toDouble();
const double y = p.at(1).toDouble();
polyWorld.append(cropTopLeftWorld + QPointF(x, y));
}
if (polyWorld.size() < 3) {
QMessageBox::warning(this, QStringLiteral("SAM 分割"), QStringLiteral("无效轮廓数据。"));
return;
}
core::Project::Entity ent;
ent.id.clear();
ent.cutoutPolygonWorld = polyWorld;
ent.originWorld = entity_cutout::polygonCentroid(ent.cutoutPolygonWorld);
ent.polygonLocal.clear();
ent.polygonLocal.reserve(ent.cutoutPolygonWorld.size());
for (const auto& pt : ent.cutoutPolygonWorld) {
ent.polygonLocal.push_back(pt - ent.originWorld);
}
QImage depth8;
if (m_workspace.hasDepth()) {
const QString dpath = m_workspace.depthAbsolutePath();
if (!dpath.isEmpty() && QFileInfo::exists(dpath)) {
const QImage dimg(dpath);
if (!dimg.isNull()) {
depth8 = dimg.convertToFormat(QImage::Format_Grayscale8);
}
}
}
const QPointF c = entity_cutout::polygonCentroid(ent.cutoutPolygonWorld);
int z = 0;
if (!depth8.isNull()) {
const int xi = static_cast<int>(std::floor(c.x()));
const int yi = static_cast<int>(std::floor(c.y()));
if (xi >= 0 && yi >= 0 && xi < depth8.width() && yi < depth8.height()) {
z = static_cast<int>(static_cast<const uchar*>(depth8.constScanLine(yi))[xi]);
}
}
ent.depth = z;
{
const double ds01 = static_cast<double>(std::clamp(z, 0, 255)) / 255.0;
ent.distanceScaleCalibMult = 0.5 + ds01 * 1.0;
}
const QString bgAbs = m_workspace.backgroundAbsolutePath();
QImage bg(bgAbs);
if (!bg.isNull() && bg.format() != QImage::Format_ARGB32_Premultiplied) {
bg = bg.convertToFormat(QImage::Format_ARGB32_Premultiplied);
}
QImage cutout;
if (!bg.isNull()) {
QPointF topLeft;
cutout = entity_cutout::extractEntityImage(bg, ent.cutoutPolygonWorld, topLeft);
ent.imageTopLeftWorld = topLeft;
}
QSet<QString> used;
for (const auto& ex : m_workspace.entities()) {
used.insert(ex.id);
}
int n = static_cast<int>(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;
}
}
// 不直接落盘:进入待确认(可微调)
m_editorCanvas->setPendingEntityPolygonWorld(polyWorld);
statusBar()->showMessage(QStringLiteral("分割完成:可拖动顶点微调,回车/点击空白确认"));
});
dlg->show();
});
connect(m_editorCanvas, &EditorCanvas::requestFinalizePendingEntity, this, [this](const QVector<QPointF>& polyWorld) {
if (!m_workspace.isOpen() || !m_workspace.hasBackground()) {
return;
}
if (polyWorld.size() < 3) {
return;
}
// 自动深度
int z = 0;
if (m_workspace.hasDepth()) {
const QString dpath = m_workspace.depthAbsolutePath();
QImage depth8(dpath);
if (!depth8.isNull()) {
depth8 = depth8.convertToFormat(QImage::Format_Grayscale8);
const QPointF c = entity_cutout::polygonCentroid(polyWorld);
const int xi = static_cast<int>(std::floor(c.x()));
const int yi = static_cast<int>(std::floor(c.y()));
if (xi >= 0 && yi >= 0 && xi < depth8.width() && yi < depth8.height()) {
z = static_cast<int>(static_cast<const uchar*>(depth8.constScanLine(yi))[xi]);
}
}
}
// 生成稳定且不重复的 identity-<n>,同时作为“默认名称”
QString newId;
{
QSet<QString> used;
for (const auto& ex : m_workspace.entities()) {
used.insert(ex.id);
}
int n = static_cast<int>(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)) {
newId = cand;
break;
}
}
}
EntityFinalizeDialog dlg(this);
dlg.setDefaultName(newId.isEmpty() ? QStringLiteral("entity-1") : newId);
dlg.setUserScale(1.0);
if (dlg.exec() != QDialog::Accepted) {
return;
}
core::Project::Entity ent;
ent.id = newId;
ent.displayName = dlg.name();
ent.cutoutPolygonWorld = polyWorld;
ent.originWorld = entity_cutout::polygonCentroid(ent.cutoutPolygonWorld);
ent.polygonLocal.clear();
ent.polygonLocal.reserve(ent.cutoutPolygonWorld.size());
for (const auto& pt : ent.cutoutPolygonWorld) {
ent.polygonLocal.push_back(pt - ent.originWorld);
}
ent.depth = std::clamp(z, 0, 255);
ent.userScale = std::max(1e-6, dlg.userScale());
{
const double ds01 = static_cast<double>(ent.depth) / 255.0;
ent.distanceScaleCalibMult = 0.5 + ds01 * 1.0;
}
// 若用户把名称清空,则 displayName 置空UI 会回退显示 id保持原习惯
if (ent.displayName == ent.id) {
// 默认情况保留 displayName=id便于树上直接显示 entity-x
}
if (ent.displayName.isEmpty()) {
// 允许空:界面会用 id 展示
}
QImage bg(m_workspace.backgroundAbsolutePath());
if (!bg.isNull() && bg.format() != QImage::Format_ARGB32_Premultiplied) {
bg = bg.convertToFormat(QImage::Format_ARGB32_Premultiplied);
}
QImage cutout;
if (!bg.isNull()) {
QPointF topLeft;
cutout = entity_cutout::extractEntityImage(bg, ent.cutoutPolygonWorld, topLeft);
ent.imageTopLeftWorld = topLeft;
}
if (!m_workspace.addEntity(ent, cutout)) {
QMessageBox::warning(this, QStringLiteral("实体"), QStringLiteral("保存实体失败。"));
return;
}
if (m_editorCanvas) {
m_editorCanvas->clearPendingEntityPolygon();
}
statusBar()->showMessage(QStringLiteral("实体已创建"));
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<EditorCanvas::Tool>(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<CanvasHost*>(m_canvasHost)->relayoutFloaters();
QTimer::singleShot(0, m_canvasHost, [this]() {
if (m_canvasHost) {
static_cast<CanvasHost*>(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<CanvasHost*>(m_canvasHost)->relayoutFloaters();
QTimer::singleShot(0, m_canvasHost, [this]() {
if (m_canvasHost) {
static_cast<CanvasHost*>(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;
}