feat: add editor project switching workflow
This commit is contained in:
78
docs/issues/Editor模块_CMake直链Release版XCEngine库破坏配置一致性3.28.md
Normal file
78
docs/issues/Editor模块_CMake直链Release版XCEngine库破坏配置一致性3.28.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Editor模块 CMake直链Release版XCEngine库破坏配置一致性
|
||||
## 1. 问题定义
|
||||
|
||||
当前 editor 可执行目标的链接方式仍然是硬编码文件路径:
|
||||
|
||||
- [`editor/CMakeLists.txt`](D:\Xuanchi\Main\XCEngine\editor\CMakeLists.txt)
|
||||
|
||||
当前写法:
|
||||
|
||||
- `target_link_libraries(... ${CMAKE_CURRENT_SOURCE_DIR}/../build/engine/Release/XCEngine.lib)`
|
||||
|
||||
这不是正常的 CMake target 依赖,而是直接绑死到一个磁盘上的 `Release` 静态库文件。
|
||||
|
||||
---
|
||||
|
||||
## 2. 当前影响
|
||||
|
||||
这会导致几个非常实际的问题:
|
||||
|
||||
1. Debug editor 也可能链接到旧的 Release 版引擎库
|
||||
2. editor 对 `XCEngine` 目标没有真实依赖关系
|
||||
3. 引擎库变更后,editor 可能不会按正确配置自动重建
|
||||
4. Debug / Release 混链风险被隐藏
|
||||
|
||||
当前本地状态就能看到:
|
||||
|
||||
- `build/engine/Debug/XCEngine.lib`
|
||||
- `build/engine/Release/XCEngine.lib`
|
||||
|
||||
两者时间戳和体积明显不同,但 editor 目标仍然硬连 Release 文件。
|
||||
|
||||
---
|
||||
|
||||
## 3. 为什么这是重大缺陷
|
||||
|
||||
这条会直接污染后续 Viewport 对接的开发过程:
|
||||
|
||||
- 你以为 editor 正在吃最新的 Renderer / RHI 改动
|
||||
- 实际上它可能仍在链接旧的 Release 库
|
||||
|
||||
这样一来:
|
||||
|
||||
- viewport 接入问题很难调
|
||||
- Debug 行为和 Release 行为可能不一致
|
||||
- CI / 新机器 / 干净构建环境也更容易炸
|
||||
|
||||
它不是“代码风格问题”,而是构建依赖关系本身不正确。
|
||||
|
||||
---
|
||||
|
||||
## 4. 建议方案
|
||||
|
||||
应改成标准 CMake target 依赖:
|
||||
|
||||
1. editor 直接链接 `XCEngine`
|
||||
2. 不再手写 `../build/engine/Release/XCEngine.lib`
|
||||
3. 由 CMake 自己处理 Debug / Release / RelWithDebInfo 的库选择
|
||||
4. editor/include 路径和 link 关系都从 target 传播,而不是继续手写 build 输出路径
|
||||
|
||||
---
|
||||
|
||||
## 5. 验收标准
|
||||
|
||||
完成后至少应满足:
|
||||
|
||||
1. `editor` 目标通过 `target_link_libraries(... XCEngine)` 链接引擎
|
||||
2. Debug editor 自动吃 Debug `XCEngine`
|
||||
3. Release editor 自动吃 Release `XCEngine`
|
||||
4. 修改 engine 后重新构建 editor 时,依赖关系正确生效
|
||||
5. 干净构建环境不依赖预先存在的某个磁盘库文件
|
||||
|
||||
---
|
||||
|
||||
## 6. 优先级
|
||||
|
||||
中高。
|
||||
|
||||
它未必第一时间阻塞面板 UI 开发,但会显著污染后续所有 viewport / renderer 联调结果,建议尽快修。
|
||||
@@ -0,0 +1,94 @@
|
||||
# Editor模块 宿主渲染与EngineRHI未统一导致Viewport纹理无法接入
|
||||
## 1. 问题定义
|
||||
|
||||
当前 editor 的窗口宿主渲染仍然是一套独立的原生 D3D12 路径:
|
||||
|
||||
- [`editor/src/Application.h`](D:\Xuanchi\Main\XCEngine\editor\src\Application.h)
|
||||
- [`editor/src/Application.cpp`](D:\Xuanchi\Main\XCEngine\editor\src\Application.cpp)
|
||||
- [`editor/src/Platform/D3D12WindowRenderer.h`](D:\Xuanchi\Main\XCEngine\editor\src\Platform\D3D12WindowRenderer.h)
|
||||
|
||||
这套路径只负责:
|
||||
|
||||
- 创建 Win32 窗口交换链
|
||||
- 创建独立的 `ID3D12Device / ID3D12CommandQueue`
|
||||
- 渲染 ImGui 主界面
|
||||
|
||||
而当前引擎的 RHI / Renderer 则是另一套独立设备与上下文体系。
|
||||
|
||||
这意味着后续即使 Renderer 能把 `SceneView/GameView` 渲染到离屏目标,editor 侧也没有稳定的统一设备桥接层把那张纹理安全地贴到 ImGui。
|
||||
|
||||
---
|
||||
|
||||
## 2. 当前现状
|
||||
|
||||
当前 `Application::InitializeWindowRenderer()` 直接创建原生 D3D12 宿主:
|
||||
|
||||
- editor 只知道 `Platform::D3D12WindowRenderer`
|
||||
- editor 并不持有 `RHIDevice / RenderContext / RenderSurface`
|
||||
- `SceneViewPanel / GameViewPanel` 也没有可复用的 viewport host 对象
|
||||
|
||||
结果是:
|
||||
|
||||
- editor 主界面渲染和引擎 Renderer 渲染仍然分裂
|
||||
- 未来 viewport 要么走不通
|
||||
- 要么被迫写一层高风险的 D3D12 私有纹理互拷/句柄桥接
|
||||
- 要么反向污染 Renderer,使其去适配 editor 私有宿主
|
||||
|
||||
---
|
||||
|
||||
## 3. 为什么这是重大缺陷
|
||||
|
||||
这不是“面板里还没把图显示出来”的小缺口,而是 viewport 接入的根部边界问题。
|
||||
|
||||
如果不先统一宿主层,后面很容易走成错误路线:
|
||||
|
||||
- Editor 继续维护一套私有 D3D12 设备
|
||||
- Renderer 再维护一套引擎 RHI 设备
|
||||
- `SceneView` 和 `GameView` 为了显示纹理被迫做后端专用互操作
|
||||
- Vulkan / OpenGL 路径在 editor 中天然失去接入可能
|
||||
|
||||
这会直接破坏:
|
||||
|
||||
- RHI 抽象边界
|
||||
- Renderer 的后端无关性
|
||||
- 后续 editor viewport 与 runtime 共用同一渲染链路的目标
|
||||
|
||||
---
|
||||
|
||||
## 4. 建议方案
|
||||
|
||||
正确方向应该是:
|
||||
|
||||
1. editor 宿主层只保留“窗口 + ImGui 宿主”职责
|
||||
2. viewport 输出统一来自引擎 Renderer 的 `RenderSurface`
|
||||
3. editor 增加专门的 viewport bridge / host 层,而不是把渲染实现塞进 panel
|
||||
4. 该 bridge 层需要明确处理:
|
||||
- 使用哪个 RHI backend
|
||||
- 如何创建离屏 color/depth 目标
|
||||
- 如何把离屏结果暴露为 ImGui 可显示纹理
|
||||
- resize 生命周期
|
||||
- device/context 所有权
|
||||
|
||||
建议不要继续扩张 `D3D12WindowRenderer` 的职责。
|
||||
|
||||
它可以继续作为 editor 主窗口 UI 宿主,但不应成为 viewport 真实渲染实现本体。
|
||||
|
||||
---
|
||||
|
||||
## 5. 验收标准
|
||||
|
||||
完成后至少应满足:
|
||||
|
||||
1. editor 可以不依赖私有 D3D12 纹理路径来显示 viewport
|
||||
2. `SceneView` 和 `GameView` 都走统一的 viewport host 接口
|
||||
3. viewport 输出来自引擎 `RenderSurface`
|
||||
4. editor 不需要因为切换 OpenGL / D3D12 / Vulkan 而重写面板逻辑
|
||||
5. Renderer 不需要反向依赖 editor 平台实现
|
||||
|
||||
---
|
||||
|
||||
## 6. 优先级
|
||||
|
||||
高。
|
||||
|
||||
在开始正式做 Scene/Game Viewport 之前必须先收敛这个边界,否则后面的接入实现会天然带着架构债。
|
||||
87
docs/issues/Editor模块_项目根路径仍绑定可执行目录阻塞真实场景与资源加载3.28.md
Normal file
87
docs/issues/Editor模块_项目根路径仍绑定可执行目录阻塞真实场景与资源加载3.28.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Editor模块 项目根路径仍绑定可执行目录阻塞真实场景与资源加载
|
||||
## 1. 问题定义
|
||||
|
||||
当前 editor 初始化 `EditorContext` 时,把 project path 直接设成了可执行文件目录:
|
||||
|
||||
- [`editor/src/Application.cpp`](D:\Xuanchi\Main\XCEngine\editor\src\Application.cpp)
|
||||
|
||||
具体行为是:
|
||||
|
||||
- 通过 `GetExecutableDirectoryUtf8()` 拿 exe 目录
|
||||
- 用这个目录初始化 `EditorContext`
|
||||
- `ProjectPanel` 和 `SceneManager::LoadStartupScene()` 都基于这个路径工作
|
||||
|
||||
这意味着 editor 当前没有“真实工程根目录”的概念。
|
||||
|
||||
---
|
||||
|
||||
## 2. 当前影响
|
||||
|
||||
基于当前实现:
|
||||
|
||||
- `ProjectManager::Initialize()` 会把 `<projectPath>/Assets` 当作项目资源根
|
||||
- [`editor/src/Managers/ProjectManager.cpp`](D:\Xuanchi\Main\XCEngine\editor\src\Managers\ProjectManager.cpp)
|
||||
- [`editor/src/Core/EditorWorkspace.h`](D:\Xuanchi\Main\XCEngine\editor\src\Core\EditorWorkspace.h)
|
||||
|
||||
但 `projectPath` 现在是 `editor/bin/...`
|
||||
|
||||
结果就是:
|
||||
|
||||
- Project 面板看到的是 exe 目录下的 `Assets`
|
||||
- Startup Scene 也是从 exe 目录下找 `Assets/Scenes/Main.xc`
|
||||
- viewport 后面如果要加载真实 scene / material / texture / mesh,也会默认走错根目录
|
||||
|
||||
对“真正接引擎工程内容”来说,这是实打实的阻塞项。
|
||||
|
||||
---
|
||||
|
||||
## 3. 为什么这是重大缺陷
|
||||
|
||||
Scene/Game Viewport 一旦接 Renderer,就不再只是“显示一个空测试图”。
|
||||
|
||||
它需要基于当前工程:
|
||||
|
||||
- 加载场景
|
||||
- 找到 mesh / material / texture / shader 资产
|
||||
- 正确解析 `Assets/...` 相对路径
|
||||
|
||||
如果 editor 的项目根仍然绑定在 exe 目录:
|
||||
|
||||
- 资源加载结果会和实际工程目录脱钩
|
||||
- Editor 和运行时看到的资产树不是同一棵
|
||||
- 后续 Project 面板、Scene 保存、Viewport 渲染都会形成伪项目环境
|
||||
|
||||
---
|
||||
|
||||
## 4. 建议方案
|
||||
|
||||
建议尽快引入明确的工程根路径入口,而不是继续默认 exe 目录:
|
||||
|
||||
1. `Application` 启动时明确解析 editor project root
|
||||
2. 最少先支持:
|
||||
- 命令行传入工程根
|
||||
- 或固定读取 workspace/project 配置
|
||||
3. `EditorContext / ProjectManager / SceneManager` 统一只认这一份 project root
|
||||
4. `ProjectPanel`、startup scene、future viewport asset loading 全部复用同一来源
|
||||
|
||||
在没有真实 project root 之前,不建议开始做 viewport 里的正式资源接入。
|
||||
|
||||
---
|
||||
|
||||
## 5. 验收标准
|
||||
|
||||
完成后至少应满足:
|
||||
|
||||
1. editor 能明确知道当前工程根目录,而不是推断 exe 目录
|
||||
2. Project 面板显示的是工程真实 `Assets`
|
||||
3. Startup Scene 从真实工程根加载
|
||||
4. Scene/Game Viewport 后续使用的资源路径与 Project 面板一致
|
||||
5. Debug / Release / editor/bin 变化不会改变 editor 看到的项目内容
|
||||
|
||||
---
|
||||
|
||||
## 6. 优先级
|
||||
|
||||
高。
|
||||
|
||||
这条不解决,Viewport 接入后只会跑在一个“伪项目目录”里,后面越做越难回收。
|
||||
@@ -55,19 +55,24 @@ target_include_directories(${PROJECT_NAME} PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
${imgui_SOURCE_DIR}
|
||||
${imgui_SOURCE_DIR}/backends
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../engine/include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../engine/third_party/stb
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../tests/OpenGL/package/glm
|
||||
)
|
||||
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE UNICODE _UNICODE)
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE /utf-8 /MD)
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE /utf-8)
|
||||
|
||||
if(MSVC)
|
||||
set_property(TARGET ${PROJECT_NAME} PROPERTY
|
||||
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||
endif()
|
||||
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE
|
||||
XCEngine
|
||||
d3d12.lib
|
||||
dxgi.lib
|
||||
d3dcompiler.lib
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../build/engine/Release/XCEngine.lib
|
||||
Ole32.lib
|
||||
Shell32.lib
|
||||
Uuid.lib
|
||||
)
|
||||
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES
|
||||
|
||||
@@ -16,6 +16,18 @@ namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace Actions {
|
||||
|
||||
inline ActionBinding MakeNewProjectAction() {
|
||||
return MakeAction("New Project...");
|
||||
}
|
||||
|
||||
inline ActionBinding MakeOpenProjectAction() {
|
||||
return MakeAction("Open Project...");
|
||||
}
|
||||
|
||||
inline ActionBinding MakeSaveProjectAction() {
|
||||
return MakeAction("Save Project");
|
||||
}
|
||||
|
||||
inline ActionBinding MakeNewSceneAction() {
|
||||
return MakeAction("New Scene", "Ctrl+N", false, true, true, Shortcut(ImGuiKey_N, true));
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "EditActionRouter.h"
|
||||
#include "EditorActions.h"
|
||||
#include "Commands/ProjectCommands.h"
|
||||
#include "Commands/SceneCommands.h"
|
||||
#include "Core/EditorEvents.h"
|
||||
#include "Core/EventBus.h"
|
||||
@@ -17,6 +18,18 @@ inline void ExecuteNewScene(IEditorContext& context) {
|
||||
Commands::NewScene(context);
|
||||
}
|
||||
|
||||
inline void ExecuteNewProject(IEditorContext& context) {
|
||||
Commands::NewProjectWithDialog(context);
|
||||
}
|
||||
|
||||
inline void ExecuteOpenProject(IEditorContext& context) {
|
||||
Commands::OpenProjectWithDialog(context);
|
||||
}
|
||||
|
||||
inline void ExecuteSaveProject(IEditorContext& context) {
|
||||
Commands::SaveProject(context);
|
||||
}
|
||||
|
||||
inline void ExecuteOpenScene(IEditorContext& context) {
|
||||
Commands::OpenSceneWithDialog(context);
|
||||
}
|
||||
@@ -60,6 +73,10 @@ inline void HandleMainMenuShortcuts(IEditorContext& context, const ShortcutConte
|
||||
}
|
||||
|
||||
inline void DrawFileMenuActions(IEditorContext& context) {
|
||||
DrawMenuAction(MakeNewProjectAction(), [&]() { ExecuteNewProject(context); });
|
||||
DrawMenuAction(MakeOpenProjectAction(), [&]() { ExecuteOpenProject(context); });
|
||||
DrawMenuAction(MakeSaveProjectAction(), [&]() { ExecuteSaveProject(context); });
|
||||
DrawMenuSeparator();
|
||||
DrawMenuAction(MakeNewSceneAction(), [&]() { ExecuteNewScene(context); });
|
||||
DrawMenuAction(MakeOpenSceneAction(), [&]() { ExecuteOpenScene(context); });
|
||||
DrawMenuAction(MakeSaveSceneAction(), [&]() { ExecuteSaveScene(context); });
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "Application.h"
|
||||
#include "Core/EditorLoggingSetup.h"
|
||||
#include "Core/ProjectRootResolver.h"
|
||||
#include "Core/EditorWindowTitle.h"
|
||||
#include "Layers/EditorLayer.h"
|
||||
#include "Core/EditorContext.h"
|
||||
@@ -8,6 +9,7 @@
|
||||
#include "UI/BuiltInIcons.h"
|
||||
#include "Platform/Win32Utf8.h"
|
||||
#include "Platform/WindowsProcessDiagnostics.h"
|
||||
#include <XCEngine/Debug/Logger.h>
|
||||
#include <windows.h>
|
||||
|
||||
namespace XCEngine {
|
||||
@@ -105,13 +107,25 @@ bool Application::Initialize(HWND hwnd) {
|
||||
const std::string exeDir = Platform::GetExecutableDirectoryUtf8();
|
||||
ConfigureEditorLogging(exeDir);
|
||||
|
||||
const std::string projectRoot = ResolveEditorProjectRootUtf8();
|
||||
const bool workingDirectoryChanged = SetEditorWorkingDirectory(projectRoot);
|
||||
|
||||
auto& logger = Debug::Logger::Get();
|
||||
const std::string projectRootMessage = "Editor project root: " + projectRoot;
|
||||
logger.Info(Debug::LogCategory::General, projectRootMessage.c_str());
|
||||
|
||||
if (!workingDirectoryChanged) {
|
||||
const std::string warningMessage = "Failed to switch editor working directory to project root: " + projectRoot;
|
||||
logger.Warning(Debug::LogCategory::General, warningMessage.c_str());
|
||||
}
|
||||
|
||||
m_hwnd = hwnd;
|
||||
|
||||
if (!InitializeWindowRenderer(hwnd)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
InitializeEditorContext(exeDir);
|
||||
InitializeEditorContext(projectRoot);
|
||||
InitializeImGui(hwnd);
|
||||
AttachEditorLayer();
|
||||
m_renderReady = true;
|
||||
@@ -151,5 +165,31 @@ void Application::OnResize(int width, int height) {
|
||||
m_windowRenderer.Resize(width, height);
|
||||
}
|
||||
|
||||
bool Application::SwitchProject(const std::string& projectPath) {
|
||||
if (!m_editorContext || projectPath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_imguiSession.SetProjectPath(projectPath);
|
||||
m_editorContext->SetProjectPath(projectPath);
|
||||
|
||||
auto& logger = Debug::Logger::Get();
|
||||
if (!SetEditorWorkingDirectory(projectPath)) {
|
||||
const std::string warningMessage = "Failed to switch editor working directory to project root: " + projectPath;
|
||||
logger.Warning(Debug::LogCategory::General, warningMessage.c_str());
|
||||
}
|
||||
|
||||
const std::string infoMessage = "Switched editor project root: " + projectPath;
|
||||
logger.Info(Debug::LogCategory::General, infoMessage.c_str());
|
||||
|
||||
m_lastWindowTitle.clear();
|
||||
UpdateWindowTitle();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Application::SaveProjectState() {
|
||||
m_imguiSession.SaveSettings();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ public:
|
||||
void Shutdown();
|
||||
void Render();
|
||||
void OnResize(int width, int height);
|
||||
bool SwitchProject(const std::string& projectPath);
|
||||
void SaveProjectState();
|
||||
bool IsRenderReady() const { return m_renderReady; }
|
||||
HWND GetWindowHandle() const { return m_hwnd; }
|
||||
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "Application.h"
|
||||
#include "Core/AssetItem.h"
|
||||
#include "Core/IEditorContext.h"
|
||||
#include "Core/IProjectManager.h"
|
||||
#include "Core/ISelectionManager.h"
|
||||
#include "Core/ISceneManager.h"
|
||||
#include "Core/IUndoManager.h"
|
||||
#include "SceneCommands.h"
|
||||
#include "Utils/FileDialogUtils.h"
|
||||
#include "Utils/ProjectFileUtils.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine {
|
||||
@@ -68,6 +75,115 @@ inline bool MoveAssetToFolder(
|
||||
return projectManager.MoveItem(sourceFullPath, targetFolder->fullPath);
|
||||
}
|
||||
|
||||
inline std::string BuildProjectFallbackScenePath(const std::string& projectPath) {
|
||||
return (std::filesystem::path(projectPath) / "Assets" / "Scenes" / "Main.xc").string();
|
||||
}
|
||||
|
||||
inline bool EnsureProjectStructure(const std::string& projectPath) {
|
||||
if (projectPath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
std::error_code ec;
|
||||
fs::create_directories(fs::path(projectPath) / "Assets" / "Scenes", ec);
|
||||
ec.clear();
|
||||
fs::create_directories(fs::path(projectPath) / ".xceditor", ec);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline ProjectFileUtils::ProjectDescriptor BuildProjectDescriptor(IEditorContext& context) {
|
||||
auto& sceneManager = context.GetSceneManager();
|
||||
ProjectFileUtils::ProjectDescriptor descriptor;
|
||||
descriptor.name = ProjectFileUtils::GetProjectName(context.GetProjectPath());
|
||||
|
||||
const std::string currentScenePath = sceneManager.GetCurrentScenePath();
|
||||
if (!currentScenePath.empty()) {
|
||||
descriptor.startupScene = ProjectFileUtils::MakeProjectRelativePath(
|
||||
context.GetProjectPath(),
|
||||
currentScenePath);
|
||||
}
|
||||
|
||||
if (descriptor.startupScene.empty()) {
|
||||
descriptor.startupScene = "Assets/Scenes/Main.xc";
|
||||
}
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
inline bool SaveProjectDescriptor(IEditorContext& context) {
|
||||
return ProjectFileUtils::SaveProjectDescriptor(
|
||||
context.GetProjectPath(),
|
||||
BuildProjectDescriptor(context));
|
||||
}
|
||||
|
||||
inline bool SaveProject(IEditorContext& context) {
|
||||
if (!EnsureProjectStructure(context.GetProjectPath())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SaveDirtySceneWithFallback(context, BuildProjectFallbackScenePath(context.GetProjectPath()))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
context.GetProjectManager().RefreshCurrentFolder();
|
||||
Application::Get().SaveProjectState();
|
||||
return SaveProjectDescriptor(context);
|
||||
}
|
||||
|
||||
inline bool SwitchProject(IEditorContext& context, const std::string& projectPath) {
|
||||
if (projectPath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SceneEditorUtils::ConfirmSceneSwitch(context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EnsureProjectStructure(projectPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Application::Get().SwitchProject(projectPath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
context.SetProjectPath(projectPath);
|
||||
context.GetProjectManager().Initialize(projectPath);
|
||||
const bool loaded = context.GetSceneManager().LoadStartupScene(projectPath);
|
||||
context.GetSelectionManager().ClearSelection();
|
||||
context.GetUndoManager().ClearHistory();
|
||||
context.GetProjectManager().RefreshCurrentFolder();
|
||||
|
||||
if (loaded) {
|
||||
SaveProjectDescriptor(context);
|
||||
}
|
||||
|
||||
return loaded;
|
||||
}
|
||||
|
||||
inline bool NewProjectWithDialog(IEditorContext& context) {
|
||||
const std::string projectPath = FileDialogUtils::PickFolderDialog(
|
||||
L"Select New Project Folder",
|
||||
context.GetProjectPath());
|
||||
if (projectPath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SwitchProject(context, projectPath);
|
||||
}
|
||||
|
||||
inline bool OpenProjectWithDialog(IEditorContext& context) {
|
||||
const std::string projectPath = FileDialogUtils::PickFolderDialog(
|
||||
L"Open Project Folder",
|
||||
context.GetProjectPath());
|
||||
if (projectPath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SwitchProject(context, projectPath);
|
||||
}
|
||||
|
||||
} // namespace Commands
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
|
||||
165
editor/src/Core/ProjectRootResolver.h
Normal file
165
editor/src/Core/ProjectRootResolver.h
Normal file
@@ -0,0 +1,165 @@
|
||||
#pragma once
|
||||
|
||||
#include "Platform/Win32Utf8.h"
|
||||
|
||||
#include <shellapi.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace detail {
|
||||
|
||||
inline fs::path NormalizePath(const fs::path& path, const fs::path& basePath = {}) {
|
||||
if (path.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
fs::path absolutePath = path;
|
||||
std::error_code ec;
|
||||
if (absolutePath.is_relative()) {
|
||||
absolutePath = basePath.empty()
|
||||
? fs::absolute(absolutePath, ec)
|
||||
: fs::absolute(basePath / absolutePath, ec);
|
||||
if (ec) {
|
||||
absolutePath = basePath.empty() ? absolutePath : (basePath / path);
|
||||
}
|
||||
}
|
||||
|
||||
ec.clear();
|
||||
const fs::path canonicalPath = fs::weakly_canonical(absolutePath, ec);
|
||||
if (!ec) {
|
||||
return canonicalPath.lexically_normal();
|
||||
}
|
||||
|
||||
return absolutePath.lexically_normal();
|
||||
}
|
||||
|
||||
inline bool IsWorkspaceRoot(const fs::path& candidate) {
|
||||
std::error_code ec;
|
||||
return fs::exists(candidate / L"CMakeLists.txt", ec) &&
|
||||
fs::is_directory(candidate / L"editor", ec) &&
|
||||
fs::is_directory(candidate / L"engine", ec);
|
||||
}
|
||||
|
||||
inline bool IsEditorProjectRoot(const fs::path& candidate) {
|
||||
std::error_code ec;
|
||||
return fs::exists(candidate / L"Project.xcproject", ec) ||
|
||||
fs::is_directory(candidate / L"Assets", ec);
|
||||
}
|
||||
|
||||
inline fs::path ResolveWorkspaceDefaultProjectRoot(const fs::path& workspaceRoot) {
|
||||
if (workspaceRoot.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const fs::path preferredProjectRoot = workspaceRoot / L"project";
|
||||
if (IsEditorProjectRoot(preferredProjectRoot)) {
|
||||
return NormalizePath(preferredProjectRoot);
|
||||
}
|
||||
|
||||
if (IsEditorProjectRoot(workspaceRoot)) {
|
||||
return NormalizePath(workspaceRoot);
|
||||
}
|
||||
|
||||
return NormalizePath(preferredProjectRoot);
|
||||
}
|
||||
|
||||
inline std::optional<fs::path> FindWorkspaceRoot(fs::path start) {
|
||||
if (start.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
start = NormalizePath(start);
|
||||
for (fs::path current = start; !current.empty(); ) {
|
||||
if (IsWorkspaceRoot(current)) {
|
||||
return current;
|
||||
}
|
||||
|
||||
const fs::path parent = current.parent_path();
|
||||
if (parent == current) {
|
||||
break;
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
inline std::optional<fs::path> ParseCommandLineProjectOverride(const fs::path& workingDirectory) {
|
||||
int argc = 0;
|
||||
LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
|
||||
if (!argv || argc <= 1) {
|
||||
if (argv) {
|
||||
LocalFree(argv);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<fs::path> projectOverride;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
const std::wstring arg = argv[i] ? argv[i] : L"";
|
||||
if ((arg == L"--project" || arg == L"-p") && i + 1 < argc) {
|
||||
projectOverride = NormalizePath(fs::path(argv[++i]), workingDirectory);
|
||||
break;
|
||||
}
|
||||
|
||||
static const std::wstring kProjectArgPrefix = L"--project=";
|
||||
if (arg.rfind(kProjectArgPrefix, 0) == 0) {
|
||||
projectOverride = NormalizePath(fs::path(arg.substr(kProjectArgPrefix.size())), workingDirectory);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
LocalFree(argv);
|
||||
return projectOverride;
|
||||
}
|
||||
|
||||
inline std::string PathToUtf8(const fs::path& path) {
|
||||
return Platform::WideToUtf8(path.wstring());
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
inline std::string ResolveEditorProjectRootUtf8() {
|
||||
std::error_code ec;
|
||||
const fs::path workingDirectory = detail::NormalizePath(fs::current_path(ec));
|
||||
const fs::path executableDirectory = detail::NormalizePath(fs::path(Platform::Utf8ToWide(Platform::GetExecutableDirectoryUtf8())));
|
||||
|
||||
if (const auto projectOverride = detail::ParseCommandLineProjectOverride(workingDirectory.empty() ? executableDirectory : workingDirectory)) {
|
||||
return detail::PathToUtf8(*projectOverride);
|
||||
}
|
||||
|
||||
if (const auto workspaceRoot = detail::FindWorkspaceRoot(workingDirectory)) {
|
||||
return detail::PathToUtf8(detail::ResolveWorkspaceDefaultProjectRoot(*workspaceRoot));
|
||||
}
|
||||
|
||||
if (const auto workspaceRoot = detail::FindWorkspaceRoot(executableDirectory)) {
|
||||
return detail::PathToUtf8(detail::ResolveWorkspaceDefaultProjectRoot(*workspaceRoot));
|
||||
}
|
||||
|
||||
if (!workingDirectory.empty()) {
|
||||
return detail::PathToUtf8(workingDirectory);
|
||||
}
|
||||
|
||||
return detail::PathToUtf8(executableDirectory);
|
||||
}
|
||||
|
||||
inline bool SetEditorWorkingDirectory(const std::string& projectRootUtf8) {
|
||||
if (projectRootUtf8.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
fs::current_path(fs::path(Platform::Utf8ToWide(projectRootUtf8)), ec);
|
||||
return !ec;
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "SceneManager.h"
|
||||
#include "Core/EventBus.h"
|
||||
#include "Core/EditorEvents.h"
|
||||
#include "Utils/ProjectFileUtils.h"
|
||||
#include <XCEngine/Components/ComponentFactoryRegistry.h>
|
||||
#include <XCEngine/Components/CameraComponent.h>
|
||||
#include <XCEngine/Components/LightComponent.h>
|
||||
@@ -387,6 +388,14 @@ std::string SceneManager::GetScenesDirectory(const std::string& projectPath) {
|
||||
std::string SceneManager::ResolveDefaultScenePath(const std::string& projectPath) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
if (const auto descriptor = ProjectFileUtils::LoadProjectDescriptor(projectPath)) {
|
||||
const std::string configuredScenePath =
|
||||
ProjectFileUtils::ResolveProjectPath(projectPath, descriptor->startupScene);
|
||||
if (IsSceneFileUsable(configuredScenePath)) {
|
||||
return configuredScenePath;
|
||||
}
|
||||
}
|
||||
|
||||
const fs::path scenesDir = GetScenesDirectory(projectPath);
|
||||
const fs::path mainXC = scenesDir / "Main.xc";
|
||||
if (fs::exists(mainXC)) {
|
||||
|
||||
@@ -48,6 +48,17 @@ public:
|
||||
return m_iniPath;
|
||||
}
|
||||
|
||||
void SetProjectPath(const std::string& projectPath) {
|
||||
if (ImGui::GetCurrentContext() == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
SaveSettings();
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
ConfigureIniFile(projectPath, io);
|
||||
ImGui::LoadIniSettingsFromDisk(m_iniPath.c_str());
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr float kUiFontSize = 18.0f;
|
||||
static constexpr const char* kPrimaryUiFontPath = "C:/Windows/Fonts/segoeui.ttf";
|
||||
|
||||
@@ -86,6 +86,30 @@ inline void DrawComboPreviewFrame(
|
||||
textColor);
|
||||
}
|
||||
|
||||
inline void DrawControlFrameChrome(
|
||||
ImDrawList* drawList,
|
||||
const ImVec2& min,
|
||||
const ImVec2& max,
|
||||
bool hovered,
|
||||
bool active = false) {
|
||||
if (!drawList) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ImGuiStyle& style = ImGui::GetStyle();
|
||||
const ImU32 frameColor = ImGui::GetColorU32(
|
||||
active ? ImGuiCol_FrameBgActive : (hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg));
|
||||
const ImU32 borderColor = ImGui::GetColorU32(ImGuiCol_Border);
|
||||
drawList->AddRectFilled(min, max, frameColor, style.FrameRounding);
|
||||
drawList->AddRect(
|
||||
ImVec2(min.x + 0.5f, min.y + 0.5f),
|
||||
ImVec2(max.x - 0.5f, max.y - 0.5f),
|
||||
borderColor,
|
||||
style.FrameRounding,
|
||||
0,
|
||||
style.FrameBorderSize);
|
||||
}
|
||||
|
||||
inline bool DrawFloat(
|
||||
const char* label,
|
||||
float& value,
|
||||
@@ -113,7 +137,8 @@ inline bool DrawLinearSlider(
|
||||
const bool active = ImGui::IsItemActive();
|
||||
const ImVec2 min = ImGui::GetItemRectMin();
|
||||
const ImVec2 max = ImGui::GetItemRectMax();
|
||||
const float trackPadding = LinearSliderHorizontalPadding();
|
||||
const ImGuiStyle& style = ImGui::GetStyle();
|
||||
const float trackPadding = (std::max)(LinearSliderHorizontalPadding(), style.FramePadding.x);
|
||||
const float trackMinX = min.x + trackPadding;
|
||||
const float trackMaxX = max.x - trackPadding;
|
||||
const float centerY = (min.y + max.y) * 0.5f;
|
||||
@@ -121,6 +146,7 @@ inline bool DrawLinearSlider(
|
||||
const float trackHalfThickness = LinearSliderTrackThickness() * 0.5f;
|
||||
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
DrawControlFrameChrome(drawList, min, max, hovered, active);
|
||||
drawList->AddRectFilled(
|
||||
ImVec2(trackMinX, centerY - trackHalfThickness),
|
||||
ImVec2(trackMaxX, centerY + trackHalfThickness),
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "StyleTokens.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <imgui.h>
|
||||
|
||||
namespace XCEngine {
|
||||
@@ -51,14 +53,22 @@ inline SplitterResult DrawSplitter(
|
||||
const ImVec4 color = active
|
||||
? PanelSplitterActiveColor()
|
||||
: (hovered ? PanelSplitterHoveredColor() : PanelSplitterIdleColor());
|
||||
const float thickness = PanelSplitterVisibleThickness();
|
||||
const float thickness = hitThickness > 0.0f
|
||||
? (std::min)( (std::max)(PanelSplitterVisibleThickness(), 1.0f), hitThickness)
|
||||
: (std::max)(PanelSplitterVisibleThickness(), 1.0f);
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
if (vertical) {
|
||||
const float centerX = min.x + (size.x * 0.5f);
|
||||
drawList->AddLine(ImVec2(centerX, min.y), ImVec2(centerX, min.y + size.y), ImGui::GetColorU32(color), thickness);
|
||||
const float visualMinX = std::floor(min.x + (size.x - thickness) * 0.5f);
|
||||
drawList->AddRectFilled(
|
||||
ImVec2(visualMinX, min.y),
|
||||
ImVec2(visualMinX + thickness, min.y + size.y),
|
||||
ImGui::GetColorU32(color));
|
||||
} else {
|
||||
const float centerY = min.y + (size.y * 0.5f);
|
||||
drawList->AddLine(ImVec2(min.x, centerY), ImVec2(min.x + size.x, centerY), ImGui::GetColorU32(color), thickness);
|
||||
const float visualMinY = std::floor(min.y + (size.y - thickness) * 0.5f);
|
||||
drawList->AddRectFilled(
|
||||
ImVec2(min.x, visualMinY),
|
||||
ImVec2(min.x + size.x, visualMinY + thickness),
|
||||
ImGui::GetColorU32(color));
|
||||
}
|
||||
|
||||
return SplitterResult{ hovered, active, delta };
|
||||
|
||||
@@ -199,7 +199,7 @@ inline float PanelSplitterHitThickness() {
|
||||
}
|
||||
|
||||
inline float PanelSplitterVisibleThickness() {
|
||||
return 1.0f;
|
||||
return 2.0f;
|
||||
}
|
||||
|
||||
inline ImVec4 PanelSplitterIdleColor() {
|
||||
@@ -235,7 +235,7 @@ inline float CompactNavigationTreeIndentSpacing() {
|
||||
}
|
||||
|
||||
inline float NavigationTreeIconSize() {
|
||||
return 17.0f;
|
||||
return 18.0f;
|
||||
}
|
||||
|
||||
inline float NavigationTreePrefixWidth() {
|
||||
|
||||
167
editor/src/Utils/FileDialogUtils.h
Normal file
167
editor/src/Utils/FileDialogUtils.h
Normal file
@@ -0,0 +1,167 @@
|
||||
#pragma once
|
||||
|
||||
#include "Platform/Win32Utf8.h"
|
||||
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
|
||||
#include <windows.h>
|
||||
#include <commdlg.h>
|
||||
#include <shobjidl.h>
|
||||
#include <wrl/client.h>
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace FileDialogUtils {
|
||||
|
||||
inline HWND GetDialogOwnerWindow() {
|
||||
HWND owner = GetActiveWindow();
|
||||
if (!owner) {
|
||||
owner = GetForegroundWindow();
|
||||
}
|
||||
return owner;
|
||||
}
|
||||
|
||||
inline std::string OpenFileDialog(
|
||||
const wchar_t* filter,
|
||||
const std::string& initialDirectory = {},
|
||||
const std::string& initialPath = {},
|
||||
DWORD flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR) {
|
||||
std::array<wchar_t, 1024> fileBuffer{};
|
||||
if (!initialPath.empty()) {
|
||||
const std::wstring initialFile = Platform::Utf8ToWide(initialPath);
|
||||
wcsncpy_s(fileBuffer.data(), fileBuffer.size(), initialFile.c_str(), _TRUNCATE);
|
||||
}
|
||||
|
||||
const std::wstring initialDir = Platform::Utf8ToWide(initialDirectory);
|
||||
OPENFILENAMEW dialog{};
|
||||
dialog.lStructSize = sizeof(dialog);
|
||||
dialog.hwndOwner = GetDialogOwnerWindow();
|
||||
dialog.lpstrFilter = filter;
|
||||
dialog.lpstrFile = fileBuffer.data();
|
||||
dialog.nMaxFile = static_cast<DWORD>(fileBuffer.size());
|
||||
dialog.lpstrInitialDir = initialDir.empty() ? nullptr : initialDir.c_str();
|
||||
dialog.Flags = flags;
|
||||
|
||||
if (!GetOpenFileNameW(&dialog)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Platform::WideToUtf8(fileBuffer.data());
|
||||
}
|
||||
|
||||
inline std::string SaveFileDialog(
|
||||
const wchar_t* filter,
|
||||
const std::string& initialDirectory = {},
|
||||
const std::string& suggestedPath = {},
|
||||
const wchar_t* defaultExtension = nullptr,
|
||||
DWORD flags = OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR) {
|
||||
std::array<wchar_t, 1024> fileBuffer{};
|
||||
if (!suggestedPath.empty()) {
|
||||
const std::wstring suggestedWide = Platform::Utf8ToWide(suggestedPath);
|
||||
wcsncpy_s(fileBuffer.data(), fileBuffer.size(), suggestedWide.c_str(), _TRUNCATE);
|
||||
}
|
||||
|
||||
const std::wstring initialDir = Platform::Utf8ToWide(initialDirectory);
|
||||
OPENFILENAMEW dialog{};
|
||||
dialog.lStructSize = sizeof(dialog);
|
||||
dialog.hwndOwner = GetDialogOwnerWindow();
|
||||
dialog.lpstrFilter = filter;
|
||||
dialog.lpstrFile = fileBuffer.data();
|
||||
dialog.nMaxFile = static_cast<DWORD>(fileBuffer.size());
|
||||
dialog.lpstrInitialDir = initialDir.empty() ? nullptr : initialDir.c_str();
|
||||
dialog.lpstrDefExt = defaultExtension;
|
||||
dialog.Flags = flags;
|
||||
|
||||
if (!GetSaveFileNameW(&dialog)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return Platform::WideToUtf8(fileBuffer.data());
|
||||
}
|
||||
|
||||
inline std::string PickFolderDialog(const wchar_t* title, const std::string& initialDirectory = {}) {
|
||||
const HRESULT initHr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
|
||||
if (FAILED(initHr) && initHr != RPC_E_CHANGED_MODE) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const bool shouldUninitialize = SUCCEEDED(initHr);
|
||||
Microsoft::WRL::ComPtr<IFileDialog> dialog;
|
||||
HRESULT hr = CoCreateInstance(CLSID_FileOpenDialog, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&dialog));
|
||||
if (FAILED(hr)) {
|
||||
if (shouldUninitialize) {
|
||||
CoUninitialize();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
DWORD options = 0;
|
||||
dialog->GetOptions(&options);
|
||||
dialog->SetOptions(options | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST);
|
||||
|
||||
if (title && title[0] != L'\0') {
|
||||
dialog->SetTitle(title);
|
||||
}
|
||||
|
||||
if (!initialDirectory.empty()) {
|
||||
Microsoft::WRL::ComPtr<IShellItem> initialFolder;
|
||||
hr = SHCreateItemFromParsingName(
|
||||
Platform::Utf8ToWide(initialDirectory).c_str(),
|
||||
nullptr,
|
||||
IID_PPV_ARGS(&initialFolder));
|
||||
if (SUCCEEDED(hr) && initialFolder) {
|
||||
dialog->SetFolder(initialFolder.Get());
|
||||
dialog->SetDefaultFolder(initialFolder.Get());
|
||||
}
|
||||
}
|
||||
|
||||
hr = dialog->Show(GetDialogOwnerWindow());
|
||||
if (hr == HRESULT_FROM_WIN32(ERROR_CANCELLED)) {
|
||||
if (shouldUninitialize) {
|
||||
CoUninitialize();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
if (FAILED(hr)) {
|
||||
if (shouldUninitialize) {
|
||||
CoUninitialize();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<IShellItem> result;
|
||||
hr = dialog->GetResult(&result);
|
||||
if (FAILED(hr) || !result) {
|
||||
if (shouldUninitialize) {
|
||||
CoUninitialize();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
PWSTR selectedPath = nullptr;
|
||||
hr = result->GetDisplayName(SIGDN_FILESYSPATH, &selectedPath);
|
||||
if (FAILED(hr) || !selectedPath) {
|
||||
if (shouldUninitialize) {
|
||||
CoUninitialize();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::string folderPath = Platform::WideToUtf8(selectedPath);
|
||||
CoTaskMemFree(selectedPath);
|
||||
|
||||
if (shouldUninitialize) {
|
||||
CoUninitialize();
|
||||
}
|
||||
|
||||
return folderPath;
|
||||
}
|
||||
|
||||
} // namespace FileDialogUtils
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
119
editor/src/Utils/ProjectFileUtils.h
Normal file
119
editor/src/Utils/ProjectFileUtils.h
Normal file
@@ -0,0 +1,119 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace ProjectFileUtils {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
struct ProjectDescriptor {
|
||||
std::string name;
|
||||
std::string startupScene;
|
||||
};
|
||||
|
||||
inline fs::path GetProjectFilePath(const std::string& projectRoot) {
|
||||
return fs::path(projectRoot) / "Project.xcproject";
|
||||
}
|
||||
|
||||
inline std::string GetProjectName(const std::string& projectRoot) {
|
||||
const fs::path rootPath(projectRoot);
|
||||
const fs::path folderName = rootPath.filename().empty() ? rootPath.parent_path().filename() : rootPath.filename();
|
||||
return folderName.empty() ? "XCEngineProject" : folderName.string();
|
||||
}
|
||||
|
||||
inline std::string Trim(const std::string& value) {
|
||||
const size_t begin = value.find_first_not_of(" \t\r\n");
|
||||
if (begin == std::string::npos) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const size_t end = value.find_last_not_of(" \t\r\n");
|
||||
return value.substr(begin, end - begin + 1);
|
||||
}
|
||||
|
||||
inline bool SaveProjectDescriptor(const std::string& projectRoot, const ProjectDescriptor& descriptor) {
|
||||
std::error_code ec;
|
||||
fs::create_directories(fs::path(projectRoot), ec);
|
||||
|
||||
std::ofstream output(GetProjectFilePath(projectRoot), std::ios::out | std::ios::trunc);
|
||||
if (!output.is_open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
output << "version=1\n";
|
||||
output << "name=" << descriptor.name << "\n";
|
||||
output << "startup_scene=" << descriptor.startupScene << "\n";
|
||||
return output.good();
|
||||
}
|
||||
|
||||
inline std::optional<ProjectDescriptor> LoadProjectDescriptor(const std::string& projectRoot) {
|
||||
std::ifstream input(GetProjectFilePath(projectRoot));
|
||||
if (!input.is_open()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
ProjectDescriptor descriptor;
|
||||
descriptor.name = GetProjectName(projectRoot);
|
||||
|
||||
std::string line;
|
||||
while (std::getline(input, line)) {
|
||||
const size_t equalsPos = line.find('=');
|
||||
if (equalsPos == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string key = Trim(line.substr(0, equalsPos));
|
||||
const std::string value = Trim(line.substr(equalsPos + 1));
|
||||
if (key == "name") {
|
||||
descriptor.name = value;
|
||||
} else if (key == "startup_scene") {
|
||||
descriptor.startupScene = value;
|
||||
}
|
||||
}
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
inline std::string MakeProjectRelativePath(const std::string& projectRoot, const std::string& fullPath) {
|
||||
if (projectRoot.empty() || fullPath.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
const fs::path root = fs::weakly_canonical(fs::path(projectRoot), ec);
|
||||
ec.clear();
|
||||
const fs::path target = fs::weakly_canonical(fs::path(fullPath), ec);
|
||||
if (ec) {
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
ec.clear();
|
||||
const fs::path relative = fs::relative(target, root, ec);
|
||||
if (ec) {
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
return relative.lexically_normal().generic_string();
|
||||
}
|
||||
|
||||
inline std::string ResolveProjectPath(const std::string& projectRoot, const std::string& pathValue) {
|
||||
if (pathValue.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const fs::path path(pathValue);
|
||||
if (path.is_absolute()) {
|
||||
return path.lexically_normal().string();
|
||||
}
|
||||
|
||||
return (fs::path(projectRoot) / path).lexically_normal().string();
|
||||
}
|
||||
|
||||
} // namespace ProjectFileUtils
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
@@ -3,15 +3,10 @@
|
||||
#include "Core/IEditorContext.h"
|
||||
#include "Core/IProjectManager.h"
|
||||
#include "Core/ISceneManager.h"
|
||||
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include "Utils/FileDialogUtils.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <commdlg.h>
|
||||
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
@@ -19,36 +14,6 @@ namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace SceneEditorUtils {
|
||||
|
||||
inline std::wstring Utf8ToWide(const std::string& value) {
|
||||
if (value.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const int length = MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, nullptr, 0);
|
||||
if (length <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::wstring result(length - 1, L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, &result[0], length);
|
||||
return result;
|
||||
}
|
||||
|
||||
inline std::string WideToUtf8(const std::wstring& value) {
|
||||
if (value.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const int length = WideCharToMultiByte(CP_UTF8, 0, value.c_str(), -1, nullptr, 0, nullptr, nullptr);
|
||||
if (length <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string result(length - 1, '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, value.c_str(), -1, &result[0], length, nullptr, nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
inline std::string SanitizeSceneFileName(const std::string& value) {
|
||||
std::string result = value.empty() ? "Untitled Scene" : value;
|
||||
for (char& ch : result) {
|
||||
@@ -74,14 +39,6 @@ inline std::string SanitizeSceneFileName(const std::string& value) {
|
||||
return result;
|
||||
}
|
||||
|
||||
inline HWND GetDialogOwnerWindow() {
|
||||
HWND owner = GetActiveWindow();
|
||||
if (!owner) {
|
||||
owner = GetForegroundWindow();
|
||||
}
|
||||
return owner;
|
||||
}
|
||||
|
||||
inline const wchar_t* GetSceneFilter() {
|
||||
static const wchar_t filter[] =
|
||||
L"XCEngine Scene (*.xc)\0*.xc\0Legacy Scene (*.scene;*.unity)\0*.scene;*.unity\0All Files (*.*)\0*.*\0\0";
|
||||
@@ -92,28 +49,10 @@ inline std::string OpenSceneFileDialog(const std::string& projectPath, const std
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
const fs::path scenesDir = fs::path(projectPath) / "Assets" / "Scenes";
|
||||
const std::wstring initialDir = Utf8ToWide(scenesDir.string());
|
||||
|
||||
std::array<wchar_t, 1024> fileBuffer{};
|
||||
if (!initialPath.empty()) {
|
||||
const std::wstring initialFile = Utf8ToWide(initialPath);
|
||||
wcsncpy_s(fileBuffer.data(), fileBuffer.size(), initialFile.c_str(), _TRUNCATE);
|
||||
}
|
||||
|
||||
OPENFILENAMEW dialog{};
|
||||
dialog.lStructSize = sizeof(dialog);
|
||||
dialog.hwndOwner = GetDialogOwnerWindow();
|
||||
dialog.lpstrFilter = GetSceneFilter();
|
||||
dialog.lpstrFile = fileBuffer.data();
|
||||
dialog.nMaxFile = static_cast<DWORD>(fileBuffer.size());
|
||||
dialog.lpstrInitialDir = initialDir.empty() ? nullptr : initialDir.c_str();
|
||||
dialog.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
|
||||
|
||||
if (!GetOpenFileNameW(&dialog)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return WideToUtf8(fileBuffer.data());
|
||||
return FileDialogUtils::OpenFileDialog(
|
||||
GetSceneFilter(),
|
||||
scenesDir.string(),
|
||||
initialPath);
|
||||
}
|
||||
|
||||
inline std::string SaveSceneFileDialog(
|
||||
@@ -127,26 +66,11 @@ inline std::string SaveSceneFileDialog(
|
||||
? scenesDir / (SanitizeSceneFileName(currentSceneName) + ".xc")
|
||||
: fs::path(currentScenePath).replace_extension(".xc");
|
||||
|
||||
std::array<wchar_t, 1024> fileBuffer{};
|
||||
const std::wstring suggestedWide = Utf8ToWide(suggestedPath.string());
|
||||
wcsncpy_s(fileBuffer.data(), fileBuffer.size(), suggestedWide.c_str(), _TRUNCATE);
|
||||
|
||||
const std::wstring initialDir = Utf8ToWide(suggestedPath.parent_path().string());
|
||||
OPENFILENAMEW dialog{};
|
||||
dialog.lStructSize = sizeof(dialog);
|
||||
dialog.hwndOwner = GetDialogOwnerWindow();
|
||||
dialog.lpstrFilter = GetSceneFilter();
|
||||
dialog.lpstrFile = fileBuffer.data();
|
||||
dialog.nMaxFile = static_cast<DWORD>(fileBuffer.size());
|
||||
dialog.lpstrInitialDir = initialDir.empty() ? nullptr : initialDir.c_str();
|
||||
dialog.lpstrDefExt = L"xc";
|
||||
dialog.Flags = OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
|
||||
|
||||
if (!GetSaveFileNameW(&dialog)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return WideToUtf8(fileBuffer.data());
|
||||
return FileDialogUtils::SaveFileDialog(
|
||||
GetSceneFilter(),
|
||||
suggestedPath.parent_path().string(),
|
||||
suggestedPath.string(),
|
||||
L"xc");
|
||||
}
|
||||
|
||||
inline bool SaveCurrentScene(IEditorContext& context) {
|
||||
@@ -177,7 +101,7 @@ inline bool ConfirmSceneSwitch(IEditorContext& context) {
|
||||
}
|
||||
|
||||
const int result = MessageBoxW(
|
||||
GetDialogOwnerWindow(),
|
||||
FileDialogUtils::GetDialogOwnerWindow(),
|
||||
L"Save changes to the current scene before continuing?",
|
||||
L"Unsaved Scene Changes",
|
||||
MB_YESNOCANCEL | MB_ICONWARNING);
|
||||
|
||||
Reference in New Issue
Block a user