1056 lines
41 KiB
C++
1056 lines
41 KiB
C++
#include "MetaCoreFoundation/MetaCoreProject.h"
|
|
#include "MetaCorePlatform/MetaCoreWindow.h"
|
|
#include "MetaCoreRender/MetaCoreEditorViewportRenderer.h"
|
|
#include "MetaCoreRender/MetaCoreRenderDevice.h"
|
|
|
|
#include <imgui.h>
|
|
#if defined(_WIN32)
|
|
#include <imgui_impl_win32.h>
|
|
#else
|
|
#include <imgui_impl_glfw.h>
|
|
#include <GLFW/glfw3.h>
|
|
#endif
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <chrono>
|
|
#include <csignal>
|
|
#include <cctype>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iomanip>
|
|
#include <iostream>
|
|
#include <new>
|
|
#include <optional>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <system_error>
|
|
#include <vector>
|
|
|
|
#if defined(_WIN32)
|
|
#ifndef WIN32_LEAN_AND_MEAN
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#endif
|
|
#include <windows.h>
|
|
#else
|
|
#include <limits.h>
|
|
#include <spawn.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#if defined(_WIN32)
|
|
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
|
|
#else
|
|
extern char** environ;
|
|
#endif
|
|
|
|
namespace {
|
|
|
|
struct MetaCoreLauncherProjectRecord {
|
|
std::string Name{};
|
|
std::filesystem::path Root{};
|
|
std::string LastOpened{};
|
|
};
|
|
|
|
enum class MetaCoreLauncherPathPickerTarget {
|
|
NewProjectParent,
|
|
ExistingProject
|
|
};
|
|
|
|
struct MetaCoreLauncherPathPickerEntry {
|
|
std::filesystem::path Path{};
|
|
std::string Label{};
|
|
bool IsDirectory = false;
|
|
bool IsProjectFile = false;
|
|
};
|
|
|
|
[[nodiscard]] std::string MetaCoreToLower(std::string value) {
|
|
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char character) {
|
|
return static_cast<char>(std::tolower(character));
|
|
});
|
|
return value;
|
|
}
|
|
|
|
[[nodiscard]] std::string MetaCoreNowIsoString() {
|
|
const auto now = std::chrono::system_clock::now();
|
|
const auto timeValue = std::chrono::system_clock::to_time_t(now);
|
|
std::tm timeInfo{};
|
|
#if defined(_WIN32)
|
|
localtime_s(&timeInfo, &timeValue);
|
|
#else
|
|
localtime_r(&timeValue, &timeInfo);
|
|
#endif
|
|
|
|
std::ostringstream stream;
|
|
stream << std::put_time(&timeInfo, "%Y-%m-%dT%H:%M:%S");
|
|
return stream.str();
|
|
}
|
|
|
|
[[nodiscard]] std::filesystem::path MetaCoreGetExecutableDirectory() {
|
|
#if defined(_WIN32)
|
|
std::vector<wchar_t> buffer(MAX_PATH, L'\0');
|
|
while (true) {
|
|
const DWORD copied = GetModuleFileNameW(nullptr, buffer.data(), static_cast<DWORD>(buffer.size()));
|
|
if (copied == 0) {
|
|
return {};
|
|
}
|
|
if (copied < buffer.size() - 1) {
|
|
return std::filesystem::path(std::wstring(buffer.data(), copied)).parent_path();
|
|
}
|
|
buffer.resize(buffer.size() * 2, L'\0');
|
|
}
|
|
#else
|
|
std::vector<char> buffer(PATH_MAX, '\0');
|
|
while (true) {
|
|
const ssize_t copied = readlink("/proc/self/exe", buffer.data(), buffer.size());
|
|
if (copied < 0) {
|
|
return {};
|
|
}
|
|
if (static_cast<std::size_t>(copied) < buffer.size()) {
|
|
return std::filesystem::path(std::string(buffer.data(), static_cast<std::size_t>(copied))).parent_path();
|
|
}
|
|
buffer.resize(buffer.size() * 2, '\0');
|
|
}
|
|
#endif
|
|
}
|
|
|
|
[[nodiscard]] std::filesystem::path MetaCoreGetLauncherStatePath() {
|
|
#if defined(_WIN32)
|
|
if (const char* appData = std::getenv("APPDATA"); appData != nullptr && appData[0] != '\0') {
|
|
return std::filesystem::path(appData) / "MetaCore" / "launcher_projects.json";
|
|
}
|
|
#else
|
|
if (const char* home = std::getenv("HOME"); home != nullptr && home[0] != '\0') {
|
|
return std::filesystem::path(home) / ".metacore" / "launcher_projects.json";
|
|
}
|
|
#endif
|
|
return std::filesystem::current_path() / ".metacore" / "launcher_projects.json";
|
|
}
|
|
|
|
[[nodiscard]] std::filesystem::path MetaCoreGetHomeDirectory() {
|
|
#if defined(_WIN32)
|
|
if (const char* userProfile = std::getenv("USERPROFILE"); userProfile != nullptr && userProfile[0] != '\0') {
|
|
return std::filesystem::path(userProfile);
|
|
}
|
|
#else
|
|
if (const char* home = std::getenv("HOME"); home != nullptr && home[0] != '\0') {
|
|
return std::filesystem::path(home);
|
|
}
|
|
#endif
|
|
return std::filesystem::current_path();
|
|
}
|
|
|
|
[[nodiscard]] std::string MetaCoreReadProjectDisplayName(const std::filesystem::path& projectRoot) {
|
|
if (const auto projectDocument = MetaCore::MetaCoreReadProjectFile(MetaCore::MetaCoreGetProjectFilePath(projectRoot));
|
|
projectDocument.has_value() && !projectDocument->Name.empty()) {
|
|
return projectDocument->Name;
|
|
}
|
|
return projectRoot.filename().string();
|
|
}
|
|
|
|
[[nodiscard]] std::string MetaCoreProjectRootKey(const std::filesystem::path& projectRoot) {
|
|
if (const auto resolvedRoot = MetaCore::MetaCoreResolveProjectRootCandidate(projectRoot); resolvedRoot.has_value()) {
|
|
return resolvedRoot->generic_string();
|
|
}
|
|
return projectRoot.lexically_normal().generic_string();
|
|
}
|
|
|
|
void MetaCoreCopyToBuffer(std::array<char, 512>& buffer, const std::string& value) {
|
|
std::memset(buffer.data(), 0, buffer.size());
|
|
std::strncpy(buffer.data(), value.c_str(), buffer.size() - 1);
|
|
}
|
|
|
|
void MetaCoreCopyToBuffer(std::array<char, 128>& buffer, const std::string& value) {
|
|
std::memset(buffer.data(), 0, buffer.size());
|
|
std::strncpy(buffer.data(), value.c_str(), buffer.size() - 1);
|
|
}
|
|
|
|
[[nodiscard]] std::vector<MetaCoreLauncherPathPickerEntry> MetaCoreCollectPathPickerEntries(
|
|
const std::filesystem::path& directory,
|
|
bool includeProjectFile
|
|
) {
|
|
std::vector<MetaCoreLauncherPathPickerEntry> entries;
|
|
std::error_code errorCode;
|
|
const std::filesystem::directory_options options = std::filesystem::directory_options::skip_permission_denied;
|
|
for (auto iterator = std::filesystem::directory_iterator(directory, options, errorCode);
|
|
iterator != std::filesystem::directory_iterator();
|
|
iterator.increment(errorCode)) {
|
|
if (errorCode) {
|
|
errorCode.clear();
|
|
continue;
|
|
}
|
|
|
|
const std::filesystem::path entryPath = iterator->path();
|
|
errorCode.clear();
|
|
const bool isDirectory = iterator->is_directory(errorCode);
|
|
errorCode.clear();
|
|
const bool isProjectFile =
|
|
includeProjectFile &&
|
|
iterator->is_regular_file(errorCode) &&
|
|
entryPath.filename() == "MetaCore.project.json";
|
|
if (!isDirectory && !isProjectFile) {
|
|
continue;
|
|
}
|
|
|
|
MetaCoreLauncherPathPickerEntry entry;
|
|
entry.Path = entryPath;
|
|
entry.Label = entryPath.filename().string();
|
|
entry.IsDirectory = isDirectory;
|
|
entry.IsProjectFile = isProjectFile;
|
|
entries.push_back(std::move(entry));
|
|
}
|
|
|
|
std::sort(entries.begin(), entries.end(), [](const auto& lhs, const auto& rhs) {
|
|
if (lhs.IsDirectory != rhs.IsDirectory) {
|
|
return lhs.IsDirectory;
|
|
}
|
|
return MetaCoreToLower(lhs.Label) < MetaCoreToLower(rhs.Label);
|
|
});
|
|
return entries;
|
|
}
|
|
|
|
[[nodiscard]] std::filesystem::path MetaCoreEditorExecutablePath() {
|
|
std::filesystem::path editorPath = MetaCoreGetExecutableDirectory() / "MetaCoreEditorApp";
|
|
#if defined(_WIN32)
|
|
editorPath += ".exe";
|
|
#endif
|
|
return editorPath;
|
|
}
|
|
|
|
bool MetaCoreLaunchEditorForProject(
|
|
const std::filesystem::path& projectRoot,
|
|
std::string& outError,
|
|
bool removeLauncherOwnedBackend = false
|
|
) {
|
|
const std::filesystem::path editorPath = MetaCoreEditorExecutablePath();
|
|
if (!std::filesystem::exists(editorPath)) {
|
|
outError = "启动器旁边找不到 MetaCoreEditorApp。";
|
|
return false;
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
SetEnvironmentVariableW(L"METACORE_PROJECT_PATH", projectRoot.wstring().c_str());
|
|
std::wstring commandLine =
|
|
L"\"" + editorPath.wstring() + L"\" --project \"" + projectRoot.wstring() + L"\"";
|
|
|
|
STARTUPINFOW startupInfo{};
|
|
startupInfo.cb = sizeof(startupInfo);
|
|
PROCESS_INFORMATION processInfo{};
|
|
std::vector<wchar_t> mutableCommandLine(commandLine.begin(), commandLine.end());
|
|
mutableCommandLine.push_back(L'\0');
|
|
|
|
const BOOL created = CreateProcessW(
|
|
nullptr,
|
|
mutableCommandLine.data(),
|
|
nullptr,
|
|
nullptr,
|
|
FALSE,
|
|
0,
|
|
nullptr,
|
|
editorPath.parent_path().wstring().c_str(),
|
|
&startupInfo,
|
|
&processInfo
|
|
);
|
|
if (!created) {
|
|
outError = "启动 MetaCoreEditorApp 失败。";
|
|
return false;
|
|
}
|
|
CloseHandle(processInfo.hThread);
|
|
CloseHandle(processInfo.hProcess);
|
|
return true;
|
|
#else
|
|
const std::string editorPathString = editorPath.string();
|
|
const std::string editorNameString = editorPath.filename().string();
|
|
const std::string projectRootString = projectRoot.string();
|
|
|
|
std::vector<std::string> environmentStrings;
|
|
for (char** iterator = environ; iterator != nullptr && *iterator != nullptr; ++iterator) {
|
|
const std::string_view entry(*iterator);
|
|
if (entry.rfind("METACORE_PROJECT_PATH=", 0) == 0) {
|
|
continue;
|
|
}
|
|
if (removeLauncherOwnedBackend && entry.rfind("METACORE_FILAMENT_BACKEND=", 0) == 0) {
|
|
continue;
|
|
}
|
|
environmentStrings.emplace_back(*iterator);
|
|
}
|
|
environmentStrings.emplace_back("METACORE_PROJECT_PATH=" + projectRootString);
|
|
|
|
std::vector<char*> environmentPointers;
|
|
environmentPointers.reserve(environmentStrings.size() + 1);
|
|
for (std::string& entry : environmentStrings) {
|
|
environmentPointers.push_back(entry.data());
|
|
}
|
|
environmentPointers.push_back(nullptr);
|
|
|
|
std::array<char*, 4> arguments = {
|
|
const_cast<char*>(editorNameString.c_str()),
|
|
const_cast<char*>("--project"),
|
|
const_cast<char*>(projectRootString.c_str()),
|
|
nullptr
|
|
};
|
|
|
|
pid_t childPid = 0;
|
|
const int result = posix_spawn(
|
|
&childPid,
|
|
editorPathString.c_str(),
|
|
nullptr,
|
|
nullptr,
|
|
arguments.data(),
|
|
environmentPointers.data()
|
|
);
|
|
if (result != 0) {
|
|
outError = "启动 MetaCoreEditorApp 失败。";
|
|
return false;
|
|
}
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
void MetaCoreConfigureLauncherFont() {
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.Fonts->AddFontDefault();
|
|
|
|
std::vector<std::filesystem::path> candidatePaths;
|
|
if (const char* overrideFontPath = std::getenv("METACORE_IMGUI_FONT"); overrideFontPath != nullptr && overrideFontPath[0] != '\0') {
|
|
candidatePaths.emplace_back(overrideFontPath);
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
char* windowsDirectory = nullptr;
|
|
std::size_t windowsDirectoryLength = 0;
|
|
if (_dupenv_s(&windowsDirectory, &windowsDirectoryLength, "WINDIR") == 0 && windowsDirectory != nullptr) {
|
|
const std::filesystem::path fontsPath = std::filesystem::path(windowsDirectory) / "Fonts";
|
|
candidatePaths.push_back(fontsPath / "NotoSansSC-VF.ttf");
|
|
candidatePaths.push_back(fontsPath / "msyh.ttc");
|
|
candidatePaths.push_back(fontsPath / "msyhbd.ttc");
|
|
candidatePaths.push_back(fontsPath / "Deng.ttf");
|
|
candidatePaths.push_back(fontsPath / "simhei.ttf");
|
|
free(windowsDirectory);
|
|
}
|
|
#else
|
|
candidatePaths.emplace_back("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc");
|
|
candidatePaths.emplace_back("/usr/share/fonts/opentype/noto/NotoSansCJK-Medium.ttc");
|
|
candidatePaths.emplace_back("/usr/share/fonts/truetype/wqy/wqy-microhei.ttc");
|
|
candidatePaths.emplace_back("/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc");
|
|
candidatePaths.emplace_back("/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf");
|
|
candidatePaths.emplace_back("/usr/share/fonts/truetype/arphic/uming.ttc");
|
|
#endif
|
|
|
|
for (const auto& candidatePath : candidatePaths) {
|
|
if (!std::filesystem::exists(candidatePath)) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
ImFont* font = io.Fonts->AddFontFromFileTTF(
|
|
candidatePath.string().c_str(),
|
|
17.0F,
|
|
nullptr,
|
|
io.Fonts->GetGlyphRangesChineseSimplifiedCommon()
|
|
);
|
|
if (font != nullptr) {
|
|
io.FontDefault = font;
|
|
return;
|
|
}
|
|
} catch (const std::bad_alloc&) {
|
|
}
|
|
}
|
|
}
|
|
|
|
void MetaCoreApplyLauncherStyle() {
|
|
ImGuiStyle& style = ImGui::GetStyle();
|
|
ImGui::StyleColorsDark();
|
|
style.WindowRounding = 0.0F;
|
|
style.ChildRounding = 6.0F;
|
|
style.PopupRounding = 6.0F;
|
|
style.FrameRounding = 6.0F;
|
|
style.GrabRounding = 6.0F;
|
|
style.WindowBorderSize = 0.0F;
|
|
style.FrameBorderSize = 1.0F;
|
|
style.WindowPadding = ImVec2(0.0F, 0.0F);
|
|
style.FramePadding = ImVec2(10.0F, 7.0F);
|
|
style.ItemSpacing = ImVec2(8.0F, 8.0F);
|
|
style.ScrollbarSize = 8.0F;
|
|
|
|
style.Colors[ImGuiCol_Text] = ImVec4(0.81F, 0.81F, 0.81F, 1.0F);
|
|
style.Colors[ImGuiCol_TextDisabled] = ImVec4(0.42F, 0.42F, 0.42F, 1.0F);
|
|
style.Colors[ImGuiCol_WindowBg] = ImVec4(0.098F, 0.098F, 0.098F, 1.0F);
|
|
style.Colors[ImGuiCol_ChildBg] = ImVec4(0.125F, 0.125F, 0.125F, 1.0F);
|
|
style.Colors[ImGuiCol_PopupBg] = ImVec4(0.125F, 0.125F, 0.125F, 0.98F);
|
|
style.Colors[ImGuiCol_Border] = ImVec4(0.18F, 0.18F, 0.18F, 1.0F);
|
|
style.Colors[ImGuiCol_FrameBg] = ImVec4(0.125F, 0.125F, 0.125F, 1.0F);
|
|
style.Colors[ImGuiCol_FrameBgHovered] = ImVec4(0.165F, 0.165F, 0.165F, 1.0F);
|
|
style.Colors[ImGuiCol_FrameBgActive] = ImVec4(0.20F, 0.20F, 0.20F, 1.0F);
|
|
style.Colors[ImGuiCol_Button] = ImVec4(0.125F, 0.125F, 0.125F, 1.0F);
|
|
style.Colors[ImGuiCol_ButtonHovered] = ImVec4(0.165F, 0.165F, 0.165F, 1.0F);
|
|
style.Colors[ImGuiCol_ButtonActive] = ImVec4(0.20F, 0.20F, 0.20F, 1.0F);
|
|
style.Colors[ImGuiCol_Header] = ImVec4(0.20F, 0.20F, 0.20F, 1.0F);
|
|
style.Colors[ImGuiCol_HeaderHovered] = ImVec4(0.24F, 0.24F, 0.24F, 1.0F);
|
|
style.Colors[ImGuiCol_HeaderActive] = ImVec4(1.0F, 1.0F, 1.0F, 0.18F);
|
|
style.Colors[ImGuiCol_Separator] = ImVec4(0.18F, 0.18F, 0.18F, 1.0F);
|
|
}
|
|
|
|
class MetaCoreLauncherApp {
|
|
public:
|
|
~MetaCoreLauncherApp() {
|
|
Shutdown();
|
|
}
|
|
|
|
bool Initialize() {
|
|
if (std::getenv("METACORE_FILAMENT_BACKEND") == nullptr) {
|
|
#if defined(_WIN32)
|
|
_putenv_s("METACORE_FILAMENT_BACKEND", "opengl");
|
|
#else
|
|
setenv("METACORE_FILAMENT_BACKEND", "opengl", 0);
|
|
#endif
|
|
LauncherSetFilamentBackend_ = true;
|
|
}
|
|
|
|
if (!Window_.Initialize(1080, 720, "MetaCore 启动器")) {
|
|
return false;
|
|
}
|
|
|
|
#if defined(_WIN32)
|
|
Window_.SetNativeWindowMessageHandler([](void* nativeWindowHandle, unsigned int message, std::uintptr_t wparam, std::intptr_t lparam) {
|
|
return ImGui_ImplWin32_WndProcHandler(static_cast<HWND>(nativeWindowHandle), message, static_cast<WPARAM>(wparam), static_cast<LPARAM>(lparam)) != 0;
|
|
});
|
|
#endif
|
|
|
|
if (!RenderDevice_.Initialize(Window_)) {
|
|
return false;
|
|
}
|
|
|
|
IMGUI_CHECKVERSION();
|
|
ImGui::CreateContext();
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.IniFilename = nullptr;
|
|
io.LogFilename = nullptr;
|
|
MetaCoreConfigureLauncherFont();
|
|
MetaCoreApplyLauncherStyle();
|
|
|
|
#if defined(_WIN32)
|
|
if (!ImGui_ImplWin32_InitForOpenGL(Window_.GetNativeWindowHandle())) {
|
|
#else
|
|
if (!ImGui_ImplGlfw_InitForOther(static_cast<GLFWwindow*>(Window_.GetNativeWindowHandle()), true)) {
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
if (!ViewportRenderer_.Initialize(RenderDevice_, Window_, true)) {
|
|
return false;
|
|
}
|
|
|
|
LoadState();
|
|
MetaCoreCopyToBuffer(NewProjectParentBuffer_, std::filesystem::current_path().string());
|
|
MetaCoreCopyToBuffer(NewProjectNameBuffer_, "新项目");
|
|
Initialized_ = true;
|
|
return true;
|
|
}
|
|
|
|
int Run() {
|
|
while (!Window_.ShouldClose()) {
|
|
Window_.BeginFrame();
|
|
#if defined(_WIN32)
|
|
ImGui_ImplWin32_NewFrame();
|
|
#else
|
|
ImGui_ImplGlfw_NewFrame();
|
|
#endif
|
|
ImGui::NewFrame();
|
|
DrawFrame();
|
|
ImGui::Render();
|
|
ViewportRenderer_.RenderAll();
|
|
Window_.EndFrame();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void Shutdown() {
|
|
if (!Initialized_) {
|
|
return;
|
|
}
|
|
ViewportRenderer_.Shutdown();
|
|
#if defined(_WIN32)
|
|
ImGui_ImplWin32_Shutdown();
|
|
#else
|
|
ImGui_ImplGlfw_Shutdown();
|
|
#endif
|
|
ImGui::DestroyContext();
|
|
RenderDevice_.Shutdown();
|
|
Window_.Shutdown();
|
|
Initialized_ = false;
|
|
}
|
|
|
|
private:
|
|
void LoadState() {
|
|
Projects_.clear();
|
|
const std::filesystem::path statePath = MetaCoreGetLauncherStatePath();
|
|
std::ifstream input(statePath);
|
|
if (!input.is_open()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
nlohmann::json json;
|
|
input >> json;
|
|
if (!json.contains("recent_projects") || !json["recent_projects"].is_array()) {
|
|
return;
|
|
}
|
|
for (const auto& item : json["recent_projects"]) {
|
|
if (!item.contains("root") || !item["root"].is_string()) {
|
|
continue;
|
|
}
|
|
MetaCoreLauncherProjectRecord record;
|
|
record.Root = item["root"].get<std::string>();
|
|
if (item.contains("name") && item["name"].is_string()) {
|
|
record.Name = item["name"].get<std::string>();
|
|
}
|
|
if (item.contains("last_opened") && item["last_opened"].is_string()) {
|
|
record.LastOpened = item["last_opened"].get<std::string>();
|
|
}
|
|
if (const auto resolvedRoot = MetaCore::MetaCoreResolveProjectRootCandidate(record.Root); resolvedRoot.has_value()) {
|
|
record.Root = *resolvedRoot;
|
|
record.Name = MetaCoreReadProjectDisplayName(record.Root);
|
|
} else if (record.Name.empty()) {
|
|
record.Name = record.Root.filename().string();
|
|
}
|
|
Projects_.push_back(std::move(record));
|
|
}
|
|
} catch (const std::exception& exceptionObject) {
|
|
StatusMessage_ = std::string("读取启动器状态失败:") + exceptionObject.what();
|
|
}
|
|
}
|
|
|
|
void SaveState() const {
|
|
const std::filesystem::path statePath = MetaCoreGetLauncherStatePath();
|
|
std::error_code errorCode;
|
|
std::filesystem::create_directories(statePath.parent_path(), errorCode);
|
|
if (errorCode) {
|
|
return;
|
|
}
|
|
|
|
nlohmann::json json;
|
|
json["recent_projects"] = nlohmann::json::array();
|
|
for (const auto& project : Projects_) {
|
|
json["recent_projects"].push_back({
|
|
{"name", project.Name},
|
|
{"root", project.Root.generic_string()},
|
|
{"last_opened", project.LastOpened}
|
|
});
|
|
}
|
|
|
|
std::ofstream output(statePath, std::ios::trunc);
|
|
if (output.is_open()) {
|
|
output << json.dump(2);
|
|
}
|
|
}
|
|
|
|
bool UpsertProject(const std::filesystem::path& projectRoot) {
|
|
const auto resolvedRoot = MetaCore::MetaCoreResolveProjectRootCandidate(projectRoot);
|
|
if (!resolvedRoot.has_value()) {
|
|
StatusMessage_ = "该路径不是有效的 MetaCore 项目。";
|
|
return false;
|
|
}
|
|
|
|
const std::string key = MetaCoreProjectRootKey(*resolvedRoot);
|
|
auto iterator = std::find_if(Projects_.begin(), Projects_.end(), [&](const MetaCoreLauncherProjectRecord& record) {
|
|
return MetaCoreProjectRootKey(record.Root) == key;
|
|
});
|
|
|
|
MetaCoreLauncherProjectRecord record;
|
|
record.Root = *resolvedRoot;
|
|
record.Name = MetaCoreReadProjectDisplayName(*resolvedRoot);
|
|
record.LastOpened = MetaCoreNowIsoString();
|
|
|
|
if (iterator != Projects_.end()) {
|
|
*iterator = std::move(record);
|
|
const auto index = static_cast<int>(std::distance(Projects_.begin(), iterator));
|
|
SelectedIndex_ = index;
|
|
std::rotate(Projects_.begin(), Projects_.begin() + index, Projects_.begin() + index + 1);
|
|
SelectedIndex_ = 0;
|
|
} else {
|
|
Projects_.insert(Projects_.begin(), std::move(record));
|
|
SelectedIndex_ = 0;
|
|
}
|
|
SaveState();
|
|
return true;
|
|
}
|
|
|
|
void LaunchSelectedProject() {
|
|
if (SelectedIndex_ < 0 || SelectedIndex_ >= static_cast<int>(Projects_.size())) {
|
|
StatusMessage_ = "请先选择一个项目。";
|
|
return;
|
|
}
|
|
|
|
auto& project = Projects_[static_cast<std::size_t>(SelectedIndex_)];
|
|
const auto resolvedRoot = MetaCore::MetaCoreResolveProjectRootCandidate(project.Root);
|
|
if (!resolvedRoot.has_value()) {
|
|
StatusMessage_ = "项目文件夹缺失或无效。";
|
|
return;
|
|
}
|
|
|
|
std::string error;
|
|
if (!MetaCoreLaunchEditorForProject(*resolvedRoot, error, LauncherSetFilamentBackend_)) {
|
|
StatusMessage_ = error;
|
|
return;
|
|
}
|
|
|
|
project.Root = *resolvedRoot;
|
|
project.Name = MetaCoreReadProjectDisplayName(*resolvedRoot);
|
|
project.LastOpened = MetaCoreNowIsoString();
|
|
SaveState();
|
|
StatusMessage_ = "已启动编辑器:" + project.Name;
|
|
}
|
|
|
|
void RemoveSelectedProjectRecord() {
|
|
if (SelectedIndex_ < 0 || SelectedIndex_ >= static_cast<int>(Projects_.size())) {
|
|
StatusMessage_ = "请先选择一条项目记录。";
|
|
return;
|
|
}
|
|
Projects_.erase(Projects_.begin() + SelectedIndex_);
|
|
if (Projects_.empty()) {
|
|
SelectedIndex_ = -1;
|
|
} else if (SelectedIndex_ >= static_cast<int>(Projects_.size())) {
|
|
SelectedIndex_ = static_cast<int>(Projects_.size()) - 1;
|
|
}
|
|
SaveState();
|
|
}
|
|
|
|
bool SetPathPickerDirectory(const std::filesystem::path& requestedPath) {
|
|
std::filesystem::path directory = requestedPath;
|
|
std::error_code errorCode;
|
|
|
|
if (directory.filename() == "MetaCore.project.json" || std::filesystem::is_regular_file(directory, errorCode)) {
|
|
directory = directory.parent_path();
|
|
}
|
|
|
|
errorCode.clear();
|
|
if (!std::filesystem::exists(directory, errorCode) || !std::filesystem::is_directory(directory, errorCode)) {
|
|
PathPickerError_ = "目录不存在或无法访问。";
|
|
return false;
|
|
}
|
|
|
|
errorCode.clear();
|
|
const std::filesystem::path canonicalDirectory = std::filesystem::weakly_canonical(directory, errorCode);
|
|
PathPickerCurrentDirectory_ = !errorCode && !canonicalDirectory.empty()
|
|
? canonicalDirectory
|
|
: directory.lexically_normal();
|
|
MetaCoreCopyToBuffer(PathPickerPathBuffer_, PathPickerCurrentDirectory_.string());
|
|
PathPickerSelectedPath_.clear();
|
|
PathPickerError_.clear();
|
|
return true;
|
|
}
|
|
|
|
void OpenPathPicker(MetaCoreLauncherPathPickerTarget target) {
|
|
PathPickerTarget_ = target;
|
|
PathPickerSelectedPath_.clear();
|
|
PathPickerError_.clear();
|
|
|
|
std::filesystem::path initialPath = target == MetaCoreLauncherPathPickerTarget::NewProjectParent
|
|
? std::filesystem::path(NewProjectParentBuffer_.data())
|
|
: std::filesystem::path(AddExistingPathBuffer_.data());
|
|
if (initialPath.empty()) {
|
|
initialPath = std::filesystem::current_path();
|
|
}
|
|
|
|
if (!SetPathPickerDirectory(initialPath)) {
|
|
PathPickerError_.clear();
|
|
if (!SetPathPickerDirectory(std::filesystem::current_path())) {
|
|
(void)SetPathPickerDirectory(MetaCoreGetHomeDirectory());
|
|
}
|
|
}
|
|
OpenPathPickerPopup_ = true;
|
|
}
|
|
|
|
void ApplyPickedPath(const std::filesystem::path& pickedPath) {
|
|
if (PathPickerTarget_ == MetaCoreLauncherPathPickerTarget::NewProjectParent) {
|
|
MetaCoreCopyToBuffer(NewProjectParentBuffer_, pickedPath.string());
|
|
} else {
|
|
MetaCoreCopyToBuffer(AddExistingPathBuffer_, pickedPath.string());
|
|
}
|
|
}
|
|
|
|
void DrawFrame() {
|
|
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
|
ImGui::SetNextWindowPos(viewport->Pos);
|
|
ImGui::SetNextWindowSize(viewport->Size);
|
|
ImGui::SetNextWindowViewport(viewport->ID);
|
|
|
|
constexpr ImGuiWindowFlags rootFlags =
|
|
ImGuiWindowFlags_NoDecoration |
|
|
ImGuiWindowFlags_NoMove |
|
|
ImGuiWindowFlags_NoResize |
|
|
ImGuiWindowFlags_NoSavedSettings |
|
|
ImGuiWindowFlags_NoBringToFrontOnFocus;
|
|
|
|
ImGui::Begin("MetaCoreLauncherRoot", nullptr, rootFlags);
|
|
DrawSidebar();
|
|
ImGui::SameLine(0.0F, 0.0F);
|
|
DrawProjectsPage();
|
|
DrawNewProjectPopup();
|
|
DrawAddExistingPopup();
|
|
ImGui::End();
|
|
}
|
|
|
|
void DrawSidebar() {
|
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.078F, 0.078F, 0.078F, 1.0F));
|
|
ImGui::BeginChild("Sidebar", ImVec2(220.0F, 0.0F), false, ImGuiWindowFlags_NoScrollbar);
|
|
ImGui::SetCursorPos(ImVec2(20.0F, 24.0F));
|
|
ImGui::TextUnformatted("MetaCore");
|
|
ImGui::SetCursorPosX(20.0F);
|
|
ImGui::TextDisabled("启动器");
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.145F, 0.145F, 0.145F, 1.0F));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.16F, 0.16F, 0.16F, 1.0F));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.18F, 0.18F, 0.18F, 1.0F));
|
|
ImGui::SetCursorPosX(12.0F);
|
|
if (ImGui::Button("项目", ImVec2(196.0F, 38.0F))) {
|
|
StatusMessage_ = "当前页面:项目";
|
|
}
|
|
ImGui::PopStyleColor(3);
|
|
ImGui::EndChild();
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
void DrawProjectsPage() {
|
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.098F, 0.098F, 0.098F, 1.0F));
|
|
ImGui::BeginChild("ProjectsPage", ImVec2(0.0F, 0.0F), false);
|
|
ImGui::SetCursorPos(ImVec2(28.0F, 24.0F));
|
|
ImGui::BeginGroup();
|
|
|
|
constexpr float pageRightPadding = 28.0F;
|
|
const float headerStartX = ImGui::GetCursorPosX();
|
|
const float pageContentWidth = std::max(1.0F, ImGui::GetContentRegionAvail().x - pageRightPadding);
|
|
const float headerButtonsWidth =
|
|
112.0F + 112.0F + 76.0F + 112.0F + ImGui::GetStyle().ItemSpacing.x * 3.0F;
|
|
ImGui::TextUnformatted("项目");
|
|
ImGui::SameLine(headerStartX + std::max(0.0F, pageContentWidth - headerButtonsWidth));
|
|
if (ImGui::Button("新建项目", ImVec2(112.0F, 36.0F))) {
|
|
MetaCoreCopyToBuffer(NewProjectNameBuffer_, "新项目");
|
|
if (NewProjectParentBuffer_[0] == '\0') {
|
|
MetaCoreCopyToBuffer(NewProjectParentBuffer_, std::filesystem::current_path().string());
|
|
}
|
|
OpenNewProjectPopup_ = true;
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("添加已有", ImVec2(112.0F, 36.0F))) {
|
|
MetaCoreCopyToBuffer(AddExistingPathBuffer_, std::filesystem::current_path().string());
|
|
OpenAddExistingPopup_ = true;
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("启动", ImVec2(76.0F, 36.0F))) {
|
|
LaunchSelectedProject();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("移除记录", ImVec2(112.0F, 36.0F))) {
|
|
RemoveSelectedProjectRecord();
|
|
}
|
|
|
|
ImGui::SetCursorPosX(headerStartX);
|
|
ImGui::SetNextItemWidth(pageContentWidth);
|
|
ImGui::InputTextWithHint("##SearchProjects", "搜索项目...", SearchBuffer_.data(), SearchBuffer_.size());
|
|
|
|
if (!StatusMessage_.empty()) {
|
|
ImGui::SetCursorPosX(headerStartX);
|
|
ImGui::TextDisabled("%s", StatusMessage_.c_str());
|
|
}
|
|
|
|
ImGui::SetCursorPosX(headerStartX);
|
|
ImGui::BeginChild("ProjectCards", ImVec2(pageContentWidth, 0.0F), false);
|
|
DrawProjectCards();
|
|
ImGui::EndChild();
|
|
|
|
ImGui::EndGroup();
|
|
ImGui::EndChild();
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
void DrawProjectCards() {
|
|
const std::string searchNeedle = MetaCoreToLower(SearchBuffer_.data());
|
|
int visibleCount = 0;
|
|
for (std::size_t index = 0; index < Projects_.size(); ++index) {
|
|
const auto& project = Projects_[index];
|
|
const std::string haystack = MetaCoreToLower(project.Name + " " + project.Root.string());
|
|
if (!searchNeedle.empty() && haystack.find(searchNeedle) == std::string::npos) {
|
|
continue;
|
|
}
|
|
|
|
++visibleCount;
|
|
const bool selected = SelectedIndex_ == static_cast<int>(index);
|
|
const bool missing = !MetaCore::MetaCoreResolveProjectRootCandidate(project.Root).has_value();
|
|
const ImVec4 cardColor = selected
|
|
? ImVec4(0.20F, 0.20F, 0.20F, 1.0F)
|
|
: ImVec4(0.125F, 0.125F, 0.125F, 1.0F);
|
|
ImGui::PushID(static_cast<int>(index));
|
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, cardColor);
|
|
ImGui::PushStyleColor(ImGuiCol_Border, selected ? ImVec4(1.0F, 1.0F, 1.0F, 0.65F) : ImVec4(0.18F, 0.18F, 0.18F, 1.0F));
|
|
ImGui::BeginChild("ProjectCard", ImVec2(0.0F, 96.0F), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
|
ImGui::SetCursorPos(ImVec2(14.0F, 12.0F));
|
|
ImGui::BeginGroup();
|
|
if (missing) {
|
|
ImGui::TextDisabled("%s", project.Name.c_str());
|
|
} else {
|
|
ImGui::TextUnformatted(project.Name.c_str());
|
|
}
|
|
ImGui::TextDisabled("%s", project.Root.string().c_str());
|
|
if (missing) {
|
|
ImGui::TextColored(ImVec4(0.92F, 0.34F, 0.34F, 1.0F), "项目缺失");
|
|
} else if (!project.LastOpened.empty()) {
|
|
ImGui::TextDisabled("上次打开 %s", project.LastOpened.c_str());
|
|
}
|
|
ImGui::EndGroup();
|
|
ImGui::EndChild();
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
|
SelectedIndex_ = static_cast<int>(index);
|
|
}
|
|
if (!missing && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
|
|
SelectedIndex_ = static_cast<int>(index);
|
|
LaunchSelectedProject();
|
|
}
|
|
}
|
|
ImGui::PopStyleColor(2);
|
|
ImGui::PopID();
|
|
}
|
|
|
|
if (visibleCount == 0) {
|
|
ImGui::Dummy(ImVec2(1.0F, 48.0F));
|
|
ImGui::TextDisabled("没有项目");
|
|
}
|
|
}
|
|
|
|
void DrawNewProjectPopup() {
|
|
if (OpenNewProjectPopup_) {
|
|
ImGui::OpenPopup("新建项目");
|
|
OpenNewProjectPopup_ = false;
|
|
}
|
|
ImGui::SetNextWindowSize(ImVec2(620.0F, 300.0F), ImGuiCond_Always);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(28.0F, 24.0F));
|
|
if (ImGui::BeginPopupModal("新建项目", nullptr, ImGuiWindowFlags_NoResize)) {
|
|
const float popupInputWidth = ImGui::GetContentRegionAvail().x;
|
|
|
|
ImGui::TextUnformatted("项目名称");
|
|
ImGui::SetNextItemWidth(popupInputWidth);
|
|
ImGui::InputText("##NewProjectName", NewProjectNameBuffer_.data(), NewProjectNameBuffer_.size());
|
|
ImGui::Spacing();
|
|
ImGui::TextUnformatted("父目录");
|
|
const float browseButtonWidth = 88.0F;
|
|
ImGui::SetNextItemWidth(std::max(1.0F, popupInputWidth - browseButtonWidth - ImGui::GetStyle().ItemSpacing.x));
|
|
ImGui::InputText("##NewProjectParent", NewProjectParentBuffer_.data(), NewProjectParentBuffer_.size());
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("选择...", ImVec2(browseButtonWidth, 0.0F))) {
|
|
OpenPathPicker(MetaCoreLauncherPathPickerTarget::NewProjectParent);
|
|
}
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
ImGui::SetCursorPosX(std::max(28.0F, ImGui::GetWindowWidth() - 28.0F - 96.0F * 2.0F - ImGui::GetStyle().ItemSpacing.x));
|
|
if (ImGui::Button("创建", ImVec2(96.0F, 32.0F))) {
|
|
const std::string name = NewProjectNameBuffer_.data();
|
|
const std::filesystem::path parent = NewProjectParentBuffer_.data();
|
|
if (name.empty() || parent.empty()) {
|
|
StatusMessage_ = "项目名称和父目录不能为空。";
|
|
} else {
|
|
const std::filesystem::path projectRoot = parent / name;
|
|
if (MetaCore::MetaCoreCreateProjectSkeleton(projectRoot, name)) {
|
|
if (UpsertProject(projectRoot)) {
|
|
StatusMessage_ = "已创建项目:" + name;
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
} else {
|
|
StatusMessage_ = "创建项目失败。";
|
|
}
|
|
}
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("取消", ImVec2(96.0F, 32.0F))) {
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
DrawPathPickerPopup();
|
|
ImGui::EndPopup();
|
|
}
|
|
ImGui::PopStyleVar();
|
|
}
|
|
|
|
void DrawAddExistingPopup() {
|
|
if (OpenAddExistingPopup_) {
|
|
ImGui::OpenPopup("添加已有项目");
|
|
OpenAddExistingPopup_ = false;
|
|
}
|
|
ImGui::SetNextWindowSize(ImVec2(620.0F, 240.0F), ImGuiCond_Always);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(28.0F, 24.0F));
|
|
if (ImGui::BeginPopupModal("添加已有项目", nullptr, ImGuiWindowFlags_NoResize)) {
|
|
const float popupInputWidth = ImGui::GetContentRegionAvail().x;
|
|
|
|
ImGui::TextUnformatted("项目路径");
|
|
const float browseButtonWidth = 88.0F;
|
|
ImGui::SetNextItemWidth(std::max(1.0F, popupInputWidth - browseButtonWidth - ImGui::GetStyle().ItemSpacing.x));
|
|
ImGui::InputText("##ExistingProjectPath", AddExistingPathBuffer_.data(), AddExistingPathBuffer_.size());
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("选择...", ImVec2(browseButtonWidth, 0.0F))) {
|
|
OpenPathPicker(MetaCoreLauncherPathPickerTarget::ExistingProject);
|
|
}
|
|
ImGui::Spacing();
|
|
ImGui::Separator();
|
|
ImGui::Spacing();
|
|
|
|
ImGui::SetCursorPosX(std::max(28.0F, ImGui::GetWindowWidth() - 28.0F - 96.0F * 2.0F - ImGui::GetStyle().ItemSpacing.x));
|
|
if (ImGui::Button("添加", ImVec2(96.0F, 32.0F))) {
|
|
if (UpsertProject(std::filesystem::path(AddExistingPathBuffer_.data()))) {
|
|
StatusMessage_ = "已添加项目:" + Projects_[static_cast<std::size_t>(SelectedIndex_)].Name;
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("取消", ImVec2(96.0F, 32.0F))) {
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
DrawPathPickerPopup();
|
|
ImGui::EndPopup();
|
|
}
|
|
ImGui::PopStyleVar();
|
|
}
|
|
|
|
void DrawPathPickerPopup() {
|
|
if (OpenPathPickerPopup_) {
|
|
ImGui::OpenPopup("选择路径");
|
|
OpenPathPickerPopup_ = false;
|
|
}
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(760.0F, 520.0F), ImGuiCond_Always);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(28.0F, 24.0F));
|
|
if (ImGui::BeginPopupModal("选择路径", nullptr, ImGuiWindowFlags_NoResize)) {
|
|
const bool pickingNewProjectParent = PathPickerTarget_ == MetaCoreLauncherPathPickerTarget::NewProjectParent;
|
|
ImGui::TextUnformatted(pickingNewProjectParent ? "选择新项目父目录" : "选择已有项目");
|
|
ImGui::TextDisabled(pickingNewProjectParent ? "选择项目将要创建到的父目录。" : "选择项目根目录,或选择 MetaCore.project.json。");
|
|
ImGui::Spacing();
|
|
|
|
const float popupWidth = ImGui::GetContentRegionAvail().x;
|
|
const float actionButtonWidth = 76.0F;
|
|
const float pathInputWidth = std::max(
|
|
1.0F,
|
|
popupWidth - actionButtonWidth * 3.0F - ImGui::GetStyle().ItemSpacing.x * 3.0F
|
|
);
|
|
ImGui::SetNextItemWidth(pathInputWidth);
|
|
ImGui::InputText("##PathPickerPath", PathPickerPathBuffer_.data(), PathPickerPathBuffer_.size());
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("前往", ImVec2(actionButtonWidth, 0.0F))) {
|
|
(void)SetPathPickerDirectory(std::filesystem::path(PathPickerPathBuffer_.data()));
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("上一级", ImVec2(actionButtonWidth, 0.0F))) {
|
|
const std::filesystem::path parentPath = PathPickerCurrentDirectory_.parent_path();
|
|
if (!parentPath.empty() && parentPath != PathPickerCurrentDirectory_) {
|
|
(void)SetPathPickerDirectory(parentPath);
|
|
}
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("主目录", ImVec2(actionButtonWidth, 0.0F))) {
|
|
(void)SetPathPickerDirectory(MetaCoreGetHomeDirectory());
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
ImGui::TextDisabled("%s", PathPickerCurrentDirectory_.string().c_str());
|
|
ImGui::Spacing();
|
|
|
|
ImGui::BeginChild("PathPickerEntries", ImVec2(0.0F, 300.0F), true);
|
|
const std::vector<MetaCoreLauncherPathPickerEntry> entries =
|
|
MetaCoreCollectPathPickerEntries(PathPickerCurrentDirectory_, !pickingNewProjectParent);
|
|
if (entries.empty()) {
|
|
ImGui::TextDisabled("没有可选择的目录或项目文件。");
|
|
}
|
|
for (const auto& entry : entries) {
|
|
const std::string label = entry.IsDirectory
|
|
? std::string("[目录] ") + entry.Label
|
|
: std::string("[项目文件] ") + entry.Label;
|
|
const bool selected = !PathPickerSelectedPath_.empty() && PathPickerSelectedPath_ == entry.Path;
|
|
if (ImGui::Selectable(label.c_str(), selected)) {
|
|
PathPickerSelectedPath_ = entry.Path;
|
|
}
|
|
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
|
|
if (entry.IsDirectory) {
|
|
(void)SetPathPickerDirectory(entry.Path);
|
|
} else {
|
|
ApplyPickedPath(entry.Path);
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
}
|
|
}
|
|
ImGui::EndChild();
|
|
|
|
if (!PathPickerError_.empty()) {
|
|
ImGui::TextColored(ImVec4(0.92F, 0.34F, 0.34F, 1.0F), "%s", PathPickerError_.c_str());
|
|
} else if (!pickingNewProjectParent && std::filesystem::exists(PathPickerCurrentDirectory_ / "MetaCore.project.json")) {
|
|
ImGui::TextDisabled("当前目录包含 MetaCore.project.json。");
|
|
} else {
|
|
ImGui::TextDisabled("双击目录进入,单击目录后可选择该目录。");
|
|
}
|
|
|
|
ImGui::Spacing();
|
|
const float footerButtonWidth = 128.0F;
|
|
ImGui::SetCursorPosX(std::max(
|
|
28.0F,
|
|
ImGui::GetWindowWidth() - 28.0F - footerButtonWidth * 2.0F - ImGui::GetStyle().ItemSpacing.x
|
|
));
|
|
if (ImGui::Button(
|
|
PathPickerSelectedPath_.empty() ? "选择当前目录" : "选择选中项",
|
|
ImVec2(footerButtonWidth, 32.0F)
|
|
)) {
|
|
ApplyPickedPath(PathPickerSelectedPath_.empty() ? PathPickerCurrentDirectory_ : PathPickerSelectedPath_);
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("取消", ImVec2(footerButtonWidth, 32.0F))) {
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
ImGui::PopStyleVar();
|
|
}
|
|
|
|
MetaCore::MetaCoreWindow Window_{};
|
|
MetaCore::MetaCoreRenderDevice RenderDevice_{};
|
|
MetaCore::MetaCoreEditorViewportRenderer ViewportRenderer_{};
|
|
std::vector<MetaCoreLauncherProjectRecord> Projects_{};
|
|
std::array<char, 256> SearchBuffer_{};
|
|
std::array<char, 128> NewProjectNameBuffer_{};
|
|
std::array<char, 512> NewProjectParentBuffer_{};
|
|
std::array<char, 512> AddExistingPathBuffer_{};
|
|
std::array<char, 512> PathPickerPathBuffer_{};
|
|
std::string StatusMessage_{};
|
|
std::string PathPickerError_{};
|
|
std::filesystem::path PathPickerCurrentDirectory_{};
|
|
std::filesystem::path PathPickerSelectedPath_{};
|
|
int SelectedIndex_ = -1;
|
|
bool OpenNewProjectPopup_ = false;
|
|
bool OpenAddExistingPopup_ = false;
|
|
bool OpenPathPickerPopup_ = false;
|
|
MetaCoreLauncherPathPickerTarget PathPickerTarget_ = MetaCoreLauncherPathPickerTarget::NewProjectParent;
|
|
bool LauncherSetFilamentBackend_ = false;
|
|
bool Initialized_ = false;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
#if !defined(_WIN32)
|
|
std::signal(SIGCHLD, SIG_IGN);
|
|
#endif
|
|
|
|
MetaCoreLauncherApp app;
|
|
if (!app.Initialize()) {
|
|
std::cerr << "MetaCoreLauncher initialize failed\n";
|
|
return 1;
|
|
}
|
|
return app.Run();
|
|
}
|