Unify inspector and console panel actions

This commit is contained in:
2026-03-27 00:08:46 +08:00
parent 31675e00c8
commit 3ebad63874
15 changed files with 296 additions and 142 deletions

View File

@@ -105,6 +105,8 @@
- `Edit` 动作解析与菜单绘制 / shortcut 分发已开始从 `MenuBar` 抽成共享 router
- `Hierarchy / Project` 的上下文菜单与创建弹窗也开始下沉到 shared action router
- `Project` 右键菜单目标已不再依赖 panel 内裸索引字段,而是改成 targeted popup state
- `Inspector / Console` 的局部 action 组装也开始继续下沉到 shared router
- `Inspector` 的 component section header 菜单已开始改成 callback/router 驱动,而不是在 widget 层硬编码动作
### 5. Dock / Layout 层
@@ -134,6 +136,8 @@
- panel 的 attach / detach / render 顺序有了统一入口
- 后续继续拆 panel 或补 panel 时,不需要再改一大片壳层代码
- startup scene / dock attach / panel tree 组装已继续从 `EditorLayer` 收口到 `EditorWorkspace`
- `Inspector` 的 selection 订阅已从 `Render()` 懒接线改回生命周期接线
- `Hierarchy` 的事件订阅也已从析构清理改回显式 `OnAttach / OnDetach` 生命周期清理
### 7. Application / ImGui Session 层
@@ -151,6 +155,7 @@
- Win32 window/message pump 已抽成 `Platform/Win32EditorHost.h`
- DX12 swapchain / render target / present / resize 已抽成 `Platform/D3D12WindowRenderer.h`
- scene title 拼装已抽成 `Core/EditorWindowTitle.h`
- crash filter / stderr redirect / logging sink 初始化已继续从 `Application.cpp` 抽离
## 主要面板状态
@@ -212,6 +217,8 @@
- 组件内容编辑大部分已走 property grid
- Add Component 按钮与 popup 项已接 action 层
- Add Component popup 已接 shared popup state
- Add Component popup 菜单项组装已开始从 panel 下沉到 shared inspector action router
- 组件 section header 的移除动作已开始从 widget 层硬编码迁回 inspector action router
仍待完成:
@@ -226,6 +233,7 @@
- 日志行 hover 表现已统一
- `Clear / Filter` 已接 action 层
- console filter 状态已从 panel 裸布尔字段收成独立 state object
- console toolbar action 与日志文本格式化已继续从 panel 下沉到共享层
仍待完成:

View File

@@ -0,0 +1,29 @@
#pragma once
#include "EditorActions.h"
#include "Core/EditorConsoleSink.h"
#include "UI/ConsoleFilterState.h"
#include "UI/UI.h"
namespace XCEngine {
namespace Editor {
namespace Actions {
inline void DrawConsoleToolbarActions(Debug::EditorConsoleSink& sink, UI::ConsoleFilterState& filterState) {
if (DrawToolbarAction(MakeClearConsoleAction())) {
sink.Clear();
}
ImGui::SameLine();
UI::DrawToolbarLabel("Filter");
ImGui::SameLine();
DrawToolbarToggleAction(MakeConsoleInfoFilterAction(filterState.ShowInfo()), filterState.ShowInfo());
ImGui::SameLine();
DrawToolbarToggleAction(MakeConsoleWarningFilterAction(filterState.ShowWarning()), filterState.ShowWarning());
ImGui::SameLine();
DrawToolbarToggleAction(MakeConsoleErrorFilterAction(filterState.ShowError()), filterState.ShowError());
}
} // namespace Actions
} // namespace Editor
} // namespace XCEngine

View File

@@ -161,6 +161,10 @@ inline ActionBinding MakeAddComponentMenuAction(const std::string& label, bool e
return MakeAction(label, nullptr, false, enabled);
}
inline ActionBinding MakeRemoveComponentAction(bool enabled = true) {
return MakeAction("Remove Component", nullptr, false, enabled);
}
inline ActionBinding MakeClearConsoleAction(bool enabled = true) {
return MakeAction("Clear", nullptr, false, enabled);
}

View File

@@ -0,0 +1,65 @@
#pragma once
#include "EditorActions.h"
#include "Commands/ComponentCommands.h"
#include "ComponentEditors/ComponentEditorRegistry.h"
#include "Core/IEditorContext.h"
#include "UI/UI.h"
#include <string>
namespace XCEngine {
namespace Editor {
namespace Actions {
inline std::string BuildAddComponentMenuLabel(const IComponentEditor& editor, ::XCEngine::Components::GameObject* gameObject) {
std::string label = editor.GetDisplayName();
if (Commands::CanAddComponent(editor, gameObject)) {
return label;
}
const char* reason = editor.GetAddDisabledReason(gameObject);
if (reason && reason[0] != '\0') {
label += " (";
label += reason;
label += ")";
}
return label;
}
inline bool DrawInspectorAddComponentMenu(IEditorContext& context, ::XCEngine::Components::GameObject* gameObject) {
bool drewAnyEntry = false;
for (const auto& editor : ComponentEditorRegistry::Get().GetEditors()) {
if (!editor || !editor->ShowInAddComponentMenu()) {
continue;
}
drewAnyEntry = true;
const bool canAdd = Commands::CanAddComponent(*editor, gameObject);
const std::string label = BuildAddComponentMenuLabel(*editor, gameObject);
DrawMenuAction(MakeAddComponentMenuAction(label, canAdd), [&]() {
if (Commands::AddComponent(context, *editor, gameObject)) {
ImGui::CloseCurrentPopup();
}
});
}
return drewAnyEntry;
}
inline bool DrawInspectorComponentMenu(
IEditorContext& context,
::XCEngine::Components::Component* component,
::XCEngine::Components::GameObject* gameObject,
const IComponentEditor* editor) {
const bool canRemove = Commands::CanRemoveComponent(component, editor);
return DrawMenuAction(MakeRemoveComponentAction(canRemove), [&]() {
Commands::RemoveComponent(context, component, gameObject, editor);
});
}
} // namespace Actions
} // namespace Editor
} // namespace XCEngine

View File

@@ -1,43 +1,14 @@
#include "Application.h"
#include "Core/EditorLoggingSetup.h"
#include "Core/EditorWindowTitle.h"
#include "Layers/EditorLayer.h"
#include "Core/EditorContext.h"
#include "Core/EditorConsoleSink.h"
#include "Core/EditorEvents.h"
#include "Core/EventBus.h"
#include "Platform/Win32Utf8.h"
#include <XCEngine/Debug/Logger.h>
#include <XCEngine/Debug/FileLogSink.h>
#include <XCEngine/Debug/ConsoleLogSink.h>
#include <stdio.h>
#include "Platform/WindowsProcessDiagnostics.h"
#include <windows.h>
namespace {
std::string GetExecutableLogPath(const char* fileName) {
return XCEngine::Editor::Platform::GetExecutableDirectoryUtf8() + "\\" + fileName;
}
} // namespace
static LONG WINAPI GlobalExceptionFilter(EXCEPTION_POINTERS* exceptionPointers) {
const std::string logPath = GetExecutableLogPath("crash.log");
FILE* f = nullptr;
fopen_s(&f, logPath.c_str(), "a");
if (f) {
fprintf(f, "[CRASH] ExceptionCode=0x%08X, Address=0x%p\n",
exceptionPointers->ExceptionRecord->ExceptionCode,
exceptionPointers->ExceptionRecord->ExceptionAddress);
fclose(f);
}
fprintf(stderr, "[CRASH] ExceptionCode=0x%08X, Address=0x%p\n",
exceptionPointers->ExceptionRecord->ExceptionCode,
exceptionPointers->ExceptionRecord->ExceptionAddress);
return EXCEPTION_EXECUTE_HANDLER;
}
namespace XCEngine {
namespace Editor {
@@ -47,21 +18,11 @@ Application& Application::Get() {
}
bool Application::Initialize(HWND hwnd) {
SetUnhandledExceptionFilter(GlobalExceptionFilter);
{
const std::string stderrPath = GetExecutableLogPath("stderr.log");
freopen(stderrPath.c_str(), "w", stderr);
}
Debug::Logger::Get().AddSink(std::make_unique<Debug::ConsoleLogSink>());
Debug::Logger::Get().AddSink(std::make_unique<Debug::EditorConsoleSink>());
Platform::InstallCrashExceptionFilter();
Platform::RedirectStderrToExecutableLog();
const std::string exeDir = Platform::GetExecutableDirectoryUtf8();
std::string logPath = exeDir + "\\editor.log";
Debug::Logger::Get().AddSink(std::make_unique<Debug::FileLogSink>(logPath.c_str()));
Debug::Logger::Get().Info(Debug::LogCategory::General, "Editor Application starting...");
Debug::Logger::Get().Info(Debug::LogCategory::General, ("Log file: " + logPath).c_str());
ConfigureEditorLogging(exeDir);
m_hwnd = hwnd;

View File

@@ -0,0 +1,27 @@
#pragma once
#include "Core/EditorConsoleSink.h"
#include <XCEngine/Debug/ConsoleLogSink.h>
#include <XCEngine/Debug/FileLogSink.h>
#include <XCEngine/Debug/Logger.h>
#include <memory>
#include <string>
namespace XCEngine {
namespace Editor {
inline void ConfigureEditorLogging(const std::string& executableDirectory) {
auto& logger = Debug::Logger::Get();
logger.AddSink(std::make_unique<Debug::ConsoleLogSink>());
logger.AddSink(std::make_unique<Debug::EditorConsoleSink>());
const std::string logPath = executableDirectory + "\\editor.log";
logger.AddSink(std::make_unique<Debug::FileLogSink>(logPath.c_str()));
logger.Info(Debug::LogCategory::General, "Editor Application starting...");
logger.Info(Debug::LogCategory::General, ("Log file: " + logPath).c_str());
}
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,51 @@
#pragma once
#include "Platform/Win32Utf8.h"
#include <stdio.h>
#include <string>
#include <windows.h>
namespace XCEngine {
namespace Editor {
namespace Platform {
inline std::string GetExecutableLogPath(const char* fileName) {
return GetExecutableDirectoryUtf8() + "\\" + fileName;
}
inline LONG WINAPI CrashExceptionFilter(EXCEPTION_POINTERS* exceptionPointers) {
const std::string logPath = GetExecutableLogPath("crash.log");
FILE* file = nullptr;
fopen_s(&file, logPath.c_str(), "a");
if (file) {
fprintf(
file,
"[CRASH] ExceptionCode=0x%08X, Address=0x%p\n",
exceptionPointers->ExceptionRecord->ExceptionCode,
exceptionPointers->ExceptionRecord->ExceptionAddress);
fclose(file);
}
fprintf(
stderr,
"[CRASH] ExceptionCode=0x%08X, Address=0x%p\n",
exceptionPointers->ExceptionRecord->ExceptionCode,
exceptionPointers->ExceptionRecord->ExceptionAddress);
return EXCEPTION_EXECUTE_HANDLER;
}
inline void InstallCrashExceptionFilter() {
SetUnhandledExceptionFilter(CrashExceptionFilter);
}
inline void RedirectStderrToExecutableLog() {
const std::string stderrPath = GetExecutableLogPath("stderr.log");
freopen(stderrPath.c_str(), "w", stderr);
}
} // namespace Platform
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,36 @@
#pragma once
#include <XCEngine/Debug/LogCategory.h>
#include <XCEngine/Debug/LogEntry.h>
#include <XCEngine/Debug/LogLevel.h>
#include <string>
namespace XCEngine {
namespace Editor {
namespace UI {
inline const char* ConsoleLogPrefix(::XCEngine::Debug::LogLevel level) {
switch (level) {
case ::XCEngine::Debug::LogLevel::Verbose:
case ::XCEngine::Debug::LogLevel::Debug:
case ::XCEngine::Debug::LogLevel::Info:
return "[INFO] ";
case ::XCEngine::Debug::LogLevel::Warning:
return "[WARN] ";
case ::XCEngine::Debug::LogLevel::Error:
case ::XCEngine::Debug::LogLevel::Fatal:
return "[ERROR] ";
}
return "[LOG] ";
}
inline std::string BuildConsoleLogText(const ::XCEngine::Debug::LogEntry& log) {
const char* category = ::XCEngine::Debug::LogCategoryToString(log.category);
return std::string(ConsoleLogPrefix(log.level)) + "[" + category + "] " + log.message.CStr();
}
} // namespace UI
} // namespace Editor
} // namespace XCEngine

View File

@@ -2,6 +2,7 @@
#include "BaseTheme.h"
#include "ConsoleFilterState.h"
#include "ConsoleLogFormatter.h"
#include "Core.h"
#include "DockHostStyle.h"
#include "PanelChrome.h"

View File

@@ -11,7 +11,6 @@ namespace UI {
struct ComponentSectionResult {
bool open = false;
bool removeRequested = false;
};
struct HierarchyNodeResult {
@@ -281,10 +280,11 @@ inline void DrawAssetIcon(ImDrawList* drawList, const ImVec2& min, const ImVec2&
drawList->AddLine(foldA, foldB, lineColor);
}
template <typename DrawMenuFn>
inline ComponentSectionResult BeginComponentSection(
const void* id,
const char* label,
bool canRemove,
DrawMenuFn&& drawMenu,
bool defaultOpen = true) {
const ImGuiStyle& style = ImGui::GetStyle();
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, InspectorSectionFramePadding());
@@ -302,15 +302,19 @@ inline ComponentSectionResult BeginComponentSection(
const bool open = ImGui::TreeNodeEx(id, flags, "%s", label);
ImGui::PopStyleVar(2);
bool removeRequested = false;
if (BeginPopupContextItem("##ComponentSettings")) {
DrawMenuCommand(MenuCommand::Action("Remove Component", nullptr, false, canRemove), [&]() {
removeRequested = true;
});
drawMenu();
EndPopup();
}
return ComponentSectionResult{ open, removeRequested };
return ComponentSectionResult{ open };
}
inline ComponentSectionResult BeginComponentSection(
const void* id,
const char* label,
bool defaultOpen = true) {
return BeginComponentSection(id, label, []() {}, defaultOpen);
}
inline void EndComponentSection() {

View File

@@ -1,10 +1,8 @@
#include "Actions/ConsoleActionRouter.h"
#include "Actions/ActionRouting.h"
#include "Actions/EditorActions.h"
#include "ConsolePanel.h"
#include "Core/EditorConsoleSink.h"
#include "UI/UI.h"
#include <XCEngine/Debug/LogCategory.h>
#include <XCEngine/Debug/Logger.h>
#include <imgui.h>
namespace XCEngine {
@@ -26,18 +24,7 @@ void ConsolePanel::Render() {
{
UI::PanelToolbarScope toolbar("ConsoleToolbar", UI::StandardPanelToolbarHeight());
if (toolbar.IsOpen()) {
if (Actions::DrawToolbarAction(Actions::MakeClearConsoleAction())) {
sink->Clear();
}
ImGui::SameLine();
UI::DrawToolbarLabel("Filter");
ImGui::SameLine();
Actions::DrawToolbarToggleAction(Actions::MakeConsoleInfoFilterAction(m_filterState.ShowInfo()), m_filterState.ShowInfo());
ImGui::SameLine();
Actions::DrawToolbarToggleAction(Actions::MakeConsoleWarningFilterAction(m_filterState.ShowWarning()), m_filterState.ShowWarning());
ImGui::SameLine();
Actions::DrawToolbarToggleAction(Actions::MakeConsoleErrorFilterAction(m_filterState.ShowError()), m_filterState.ShowError());
Actions::DrawConsoleToolbarActions(*sink, m_filterState);
}
}
@@ -53,27 +40,9 @@ void ConsolePanel::Render() {
continue;
}
const char* prefix;
switch (log.level) {
case ::XCEngine::Debug::LogLevel::Verbose:
case ::XCEngine::Debug::LogLevel::Debug:
case ::XCEngine::Debug::LogLevel::Info:
prefix = "[INFO] ";
break;
case ::XCEngine::Debug::LogLevel::Warning:
prefix = "[WARN] ";
break;
case ::XCEngine::Debug::LogLevel::Error:
case ::XCEngine::Debug::LogLevel::Fatal:
prefix = "[ERROR] ";
break;
}
ImGui::PushID(static_cast<int>(logIndex));
const char* category = ::XCEngine::Debug::LogCategoryToString(log.category);
std::string fullMessage = std::string(prefix) + "[" + category + "] " + log.message.CStr();
const std::string fullMessage = UI::BuildConsoleLogText(log);
if (UI::DrawConsoleLogRow(fullMessage.c_str())) {
ImGui::SetClipboardText(fullMessage.c_str());
}

View File

@@ -16,14 +16,11 @@ namespace Editor {
HierarchyPanel::HierarchyPanel() : Panel("Hierarchy") {
}
HierarchyPanel::~HierarchyPanel() {
if (m_context) {
m_context->GetEventBus().Unsubscribe<SelectionChangedEvent>(m_selectionHandlerId);
m_context->GetEventBus().Unsubscribe<EntityRenameRequestedEvent>(m_renameRequestHandlerId);
}
}
void HierarchyPanel::OnAttach() {
if (!m_context || m_selectionHandlerId || m_renameRequestHandlerId) {
return;
}
m_selectionHandlerId = m_context->GetEventBus().Subscribe<SelectionChangedEvent>(
[this](const SelectionChangedEvent& event) {
OnSelectionChanged(event);
@@ -36,6 +33,21 @@ void HierarchyPanel::OnAttach() {
);
}
void HierarchyPanel::OnDetach() {
if (!m_context) {
return;
}
if (m_selectionHandlerId) {
m_context->GetEventBus().Unsubscribe<SelectionChangedEvent>(m_selectionHandlerId);
m_selectionHandlerId = 0;
}
if (m_renameRequestHandlerId) {
m_context->GetEventBus().Unsubscribe<EntityRenameRequestedEvent>(m_renameRequestHandlerId);
m_renameRequestHandlerId = 0;
}
}
void HierarchyPanel::OnSelectionChanged(const SelectionChangedEvent& event) {
if (m_renameState.IsActive() && event.primarySelection != m_renameState.Item()) {
CancelRename();

View File

@@ -13,9 +13,9 @@ enum class SortMode { Name, ComponentCount, TransformFirst };
class HierarchyPanel : public Panel {
public:
HierarchyPanel();
~HierarchyPanel();
void OnAttach() override;
void OnDetach() override;
void Render() override;
private:

View File

@@ -1,6 +1,6 @@
#include "Actions/InspectorActionRouter.h"
#include "Actions/ActionRouting.h"
#include "Actions/EditorActions.h"
#include "Commands/ComponentCommands.h"
#include "InspectorPanel.h"
#include "Core/IEditorContext.h"
#include "Core/ISceneManager.h"
@@ -19,9 +19,28 @@ namespace Editor {
InspectorPanel::InspectorPanel() : Panel("Inspector") {}
InspectorPanel::~InspectorPanel() {
if (m_context) {
m_context->GetEventBus().Unsubscribe<SelectionChangedEvent>(m_selectionHandlerId);
}
void InspectorPanel::OnAttach() {
if (!m_context || m_selectionHandlerId) {
return;
}
m_selectionHandlerId = m_context->GetEventBus().Subscribe<SelectionChangedEvent>(
[this](const SelectionChangedEvent& event) {
OnSelectionChanged(event);
}
);
m_selectedEntityId = m_context->GetSelectionManager().GetSelectedEntity();
}
void InspectorPanel::OnDetach() {
if (!m_context || !m_selectionHandlerId) {
return;
}
m_context->GetEventBus().Unsubscribe<SelectionChangedEvent>(m_selectionHandlerId);
m_selectionHandlerId = 0;
}
void InspectorPanel::OnSelectionChanged(const SelectionChangedEvent& event) {
@@ -40,16 +59,6 @@ void InspectorPanel::Render() {
Actions::ObserveInactiveActionRoute(*m_context);
if (!m_selectionHandlerId && m_context) {
m_selectionHandlerId = m_context->GetEventBus().Subscribe<SelectionChangedEvent>(
[this](const SelectionChangedEvent& event) {
OnSelectionChanged(event);
}
);
}
m_selectedEntityId = m_context->GetSelectionManager().GetSelectedEntity();
if (m_selectedEntityId) {
auto& sceneManager = m_context->GetSceneManager();
auto* gameObject = sceneManager.GetEntity(m_selectedEntityId);
@@ -107,32 +116,7 @@ void InspectorPanel::RenderAddComponentPopup(::XCEngine::Components::GameObject*
return;
}
bool drewAnyEntry = false;
for (const auto& editor : ComponentEditorRegistry::Get().GetEditors()) {
if (!editor || !editor->ShowInAddComponentMenu()) {
continue;
}
drewAnyEntry = true;
const bool canAdd = Commands::CanAddComponent(*editor, gameObject);
std::string label = editor->GetDisplayName();
if (!canAdd) {
const char* reason = editor->GetAddDisabledReason(gameObject);
if (reason && reason[0] != '\0') {
label += " (";
label += reason;
label += ")";
}
}
Actions::DrawMenuAction(Actions::MakeAddComponentMenuAction(label, canAdd), [&]() {
if (Commands::AddComponent(*m_context, *editor, gameObject)) {
ImGui::CloseCurrentPopup();
}
});
}
if (!drewAnyEntry) {
if (!Actions::DrawInspectorAddComponentMenu(*m_context, gameObject)) {
UI::DrawHintText("No registered component editors");
}
@@ -145,14 +129,15 @@ void InspectorPanel::RenderComponent(::XCEngine::Components::Component* componen
IComponentEditor* editor = ComponentEditorRegistry::Get().FindEditor(component);
const std::string name = component->GetName();
const UI::ComponentSectionResult section =
UI::BeginComponentSection(
(void*)typeid(*component).hash_code(),
name.c_str(),
Commands::CanRemoveComponent(component, editor));
bool removed = false;
const UI::ComponentSectionResult section = UI::BeginComponentSection(
(void*)typeid(*component).hash_code(),
name.c_str(),
[&]() {
removed = Actions::DrawInspectorComponentMenu(*m_context, component, gameObject, editor);
});
if (section.removeRequested) {
Commands::RemoveComponent(*m_context, component, gameObject, editor);
if (removed) {
return;
}

View File

@@ -18,6 +18,8 @@ public:
InspectorPanel();
~InspectorPanel();
void OnAttach() override;
void OnDetach() override;
void Render() override;
private: