refactor(new_editor): snapshot hosted editor restructuring

This commit is contained in:
2026-04-21 00:57:14 +08:00
parent e123e584c8
commit 9b7b369007
248 changed files with 21152 additions and 14397 deletions

View File

@@ -1,11 +1,484 @@
#include "ProjectBrowserModel.h"
#include "ProjectBrowserModelInternal.h"
#include "ProjectBrowserModel.h"
#include <filesystem>
#include <string>
#include <string_view>
#include <vector>
#include <algorithm>
#include <cctype>
#include <cwctype>
#include <initializer_list>
#include <system_error>
#include <fstream>
namespace XCEngine::UI::Editor::App {
using namespace ProjectBrowserModelInternal;
inline constexpr std::string_view kAssetsRootId = "Assets";
std::string ToLowerCopy(std::string value);
std::string PathToUtf8String(const std::filesystem::path& path);
std::filesystem::path BuildPathFromUtf8(std::string_view value);
std::string NormalizePathSeparators(std::string value);
std::string BuildRelativeItemId(
const std::filesystem::path& path,
const std::filesystem::path& assetsRoot);
std::string BuildRelativeProjectPath(
const std::filesystem::path& path,
const std::filesystem::path& projectRoot);
std::string BuildAssetDisplayName(const std::filesystem::path& path, bool directory);
std::string BuildAssetNameWithExtension(const std::filesystem::path& path, bool directory);
bool IsMetaFile(const std::filesystem::path& path);
bool HasChildDirectories(const std::filesystem::path& folderPath);
std::vector<std::filesystem::path> CollectSortedChildDirectories(
const std::filesystem::path& folderPath);
std::wstring MakePathKey(const std::filesystem::path& path);
bool IsSameOrDescendantPath(
const std::filesystem::path& path,
const std::filesystem::path& ancestor);
std::string TrimAssetName(std::string_view name);
bool HasInvalidAssetName(std::string_view name);
std::filesystem::path MakeUniqueFolderPath(
const std::filesystem::path& parentPath,
std::string_view preferredName);
std::filesystem::path MakeUniqueFilePath(
const std::filesystem::path& parentPath,
const std::filesystem::path& preferredFileName);
std::string BuildRenamedEntryName(
const std::filesystem::path& sourcePath,
std::string_view requestedName);
std::filesystem::path GetMetaSidecarPath(const std::filesystem::path& assetPath);
void RemoveMetaSidecarIfPresent(const std::filesystem::path& assetPath);
bool RenamePathCaseAware(
const std::filesystem::path& sourcePath,
const std::filesystem::path& destPath);
void MoveMetaSidecarIfPresent(
const std::filesystem::path& sourcePath,
const std::filesystem::path& destPath);
bool MovePathWithOptionalMeta(
const std::filesystem::path& sourcePath,
const std::filesystem::path& destinationPath);
ProjectBrowserModel::ItemKind ResolveItemKind(
const std::filesystem::path& path,
bool directory);
bool CanOpenItemKind(ProjectBrowserModel::ItemKind kind);
bool CanPreviewItem(
const std::filesystem::path& path,
ProjectBrowserModel::ItemKind kind,
bool directory);
} // namespace XCEngine::UI::Editor::App
namespace XCEngine::UI::Editor::App {
namespace {
std::string BuildUtf8String(std::u8string_view value) {
std::string result = {};
result.reserve(value.size());
for (const char8_t character : value) {
result.push_back(static_cast<char>(character));
}
return result;
}
std::u8string BuildU8String(std::string_view value) {
std::u8string result = {};
result.reserve(value.size());
for (const char character : value) {
result.push_back(static_cast<char8_t>(character));
}
return result;
}
} // namespace
std::string ToLowerCopy(std::string value) {
std::transform(
value.begin(),
value.end(),
value.begin(),
[](unsigned char character) {
return static_cast<char>(std::tolower(character));
});
return value;
}
std::string PathToUtf8String(const std::filesystem::path& path) {
return BuildUtf8String(path.u8string());
}
std::filesystem::path BuildPathFromUtf8(std::string_view value) {
return std::filesystem::path(BuildU8String(value));
}
std::string NormalizePathSeparators(std::string value) {
std::replace(value.begin(), value.end(), '\\', '/');
return value;
}
std::string BuildRelativeItemId(
const std::filesystem::path& path,
const std::filesystem::path& assetsRoot) {
const std::filesystem::path relative =
std::filesystem::relative(path, assetsRoot.parent_path());
const std::string normalized =
NormalizePathSeparators(PathToUtf8String(relative.lexically_normal()));
return normalized.empty() ? std::string(kAssetsRootId) : normalized;
}
std::string BuildRelativeProjectPath(
const std::filesystem::path& path,
const std::filesystem::path& projectRoot) {
if (projectRoot.empty() || path.empty()) {
return {};
}
const std::filesystem::path relative =
std::filesystem::relative(path, projectRoot);
const std::string normalized =
NormalizePathSeparators(PathToUtf8String(relative.lexically_normal()));
return normalized;
}
std::string BuildAssetDisplayName(const std::filesystem::path& path, bool directory) {
if (directory) {
return PathToUtf8String(path.filename());
}
const std::string filename = PathToUtf8String(path.filename());
const std::size_t extensionOffset = filename.find_last_of('.');
if (extensionOffset == std::string::npos || extensionOffset == 0u) {
return filename;
}
return filename.substr(0u, extensionOffset);
}
std::string BuildAssetNameWithExtension(const std::filesystem::path& path, bool directory) {
return directory
? PathToUtf8String(path.filename())
: PathToUtf8String(path.filename());
}
bool IsMetaFile(const std::filesystem::path& path) {
return ToLowerCopy(path.extension().string()) == ".meta";
}
bool HasChildDirectories(const std::filesystem::path& folderPath) {
std::error_code errorCode = {};
const std::filesystem::directory_iterator end = {};
for (std::filesystem::directory_iterator iterator(folderPath, errorCode);
!errorCode && iterator != end;
iterator.increment(errorCode)) {
if (iterator->is_directory(errorCode)) {
return true;
}
}
return false;
}
std::vector<std::filesystem::path> CollectSortedChildDirectories(
const std::filesystem::path& folderPath) {
std::vector<std::filesystem::path> paths = {};
std::error_code errorCode = {};
const std::filesystem::directory_iterator end = {};
for (std::filesystem::directory_iterator iterator(folderPath, errorCode);
!errorCode && iterator != end;
iterator.increment(errorCode)) {
if (iterator->is_directory(errorCode)) {
paths.push_back(iterator->path());
}
}
std::sort(
paths.begin(),
paths.end(),
[](const std::filesystem::path& lhs, const std::filesystem::path& rhs) {
return ToLowerCopy(PathToUtf8String(lhs.filename())) <
ToLowerCopy(PathToUtf8String(rhs.filename()));
});
return paths;
}
std::wstring MakePathKey(const std::filesystem::path& path) {
std::wstring key = path.lexically_normal().generic_wstring();
std::transform(key.begin(), key.end(), key.begin(), ::towlower);
return key;
}
bool IsSameOrDescendantPath(
const std::filesystem::path& path,
const std::filesystem::path& ancestor) {
const std::wstring pathKey = MakePathKey(path);
std::wstring ancestorKey = MakePathKey(ancestor);
if (pathKey.empty() || ancestorKey.empty()) {
return false;
}
if (pathKey == ancestorKey) {
return true;
}
if (ancestorKey.back() != L'/') {
ancestorKey += L'/';
}
return pathKey.rfind(ancestorKey, 0) == 0;
}
std::string TrimAssetName(std::string_view name) {
const auto first =
std::find_if_not(name.begin(), name.end(), [](unsigned char character) {
return std::isspace(character) != 0;
});
if (first == name.end()) {
return {};
}
const auto last =
std::find_if_not(name.rbegin(), name.rend(), [](unsigned char character) {
return std::isspace(character) != 0;
}).base();
return std::string(first, last);
}
bool HasInvalidAssetName(std::string_view name) {
if (name.empty() || name == "." || name == "..") {
return true;
}
return name.find_first_of("\\/:*?\"<>|") != std::string_view::npos;
}
std::filesystem::path MakeUniqueFolderPath(
const std::filesystem::path& parentPath,
std::string_view preferredName) {
std::filesystem::path candidatePath =
parentPath / BuildPathFromUtf8(preferredName);
if (!std::filesystem::exists(candidatePath)) {
return candidatePath;
}
for (std::size_t suffix = 1u;; ++suffix) {
candidatePath =
parentPath / BuildPathFromUtf8(
std::string(preferredName) + " " + std::to_string(suffix));
if (!std::filesystem::exists(candidatePath)) {
return candidatePath;
}
}
}
std::filesystem::path MakeUniqueFilePath(
const std::filesystem::path& parentPath,
const std::filesystem::path& preferredFileName) {
std::filesystem::path candidatePath = parentPath / preferredFileName;
if (!std::filesystem::exists(candidatePath)) {
return candidatePath;
}
const std::wstring stem = preferredFileName.stem().wstring();
const std::wstring extension = preferredFileName.extension().wstring();
for (std::size_t suffix = 1u;; ++suffix) {
candidatePath =
parentPath /
std::filesystem::path(
stem + L" " + std::to_wstring(suffix) + extension);
if (!std::filesystem::exists(candidatePath)) {
return candidatePath;
}
}
}
std::string BuildRenamedEntryName(
const std::filesystem::path& sourcePath,
std::string_view requestedName) {
if (std::filesystem::is_directory(sourcePath)) {
return std::string(requestedName);
}
const std::filesystem::path requestedPath =
BuildPathFromUtf8(requestedName);
if (requestedPath.has_extension()) {
return PathToUtf8String(requestedPath.filename());
}
return std::string(requestedName) + PathToUtf8String(sourcePath.extension());
}
std::filesystem::path GetMetaSidecarPath(const std::filesystem::path& assetPath) {
return std::filesystem::path(assetPath.native() + std::filesystem::path(L".meta").native());
}
void RemoveMetaSidecarIfPresent(const std::filesystem::path& assetPath) {
std::error_code errorCode = {};
std::filesystem::remove_all(GetMetaSidecarPath(assetPath), errorCode);
}
namespace {
std::filesystem::path MakeCaseOnlyRenameTempPath(const std::filesystem::path& sourcePath) {
const std::filesystem::path parentPath = sourcePath.parent_path();
const std::wstring sourceStem = sourcePath.filename().wstring();
for (std::size_t suffix = 0u;; ++suffix) {
std::wstring tempName = sourceStem + L".xc_tmp_rename";
if (suffix > 0u) {
tempName += std::to_wstring(suffix);
}
const std::filesystem::path tempPath = parentPath / tempName;
if (!std::filesystem::exists(tempPath)) {
return tempPath;
}
}
}
bool MatchesExtension(
std::wstring_view extension,
std::initializer_list<std::wstring_view> candidates) {
for (const std::wstring_view candidate : candidates) {
if (extension == candidate) {
return true;
}
}
return false;
}
} // namespace
bool RenamePathCaseAware(
const std::filesystem::path& sourcePath,
const std::filesystem::path& destPath) {
if (MakePathKey(sourcePath) != MakePathKey(destPath)) {
if (std::filesystem::exists(destPath)) {
return false;
}
std::filesystem::rename(sourcePath, destPath);
return true;
}
if (sourcePath.filename() == destPath.filename()) {
return true;
}
const std::filesystem::path tempPath = MakeCaseOnlyRenameTempPath(sourcePath);
std::filesystem::rename(sourcePath, tempPath);
std::filesystem::rename(tempPath, destPath);
return true;
}
void MoveMetaSidecarIfPresent(
const std::filesystem::path& sourcePath,
const std::filesystem::path& destPath) {
const std::filesystem::path sourceMetaPath = GetMetaSidecarPath(sourcePath);
if (!std::filesystem::exists(sourceMetaPath)) {
return;
}
const std::filesystem::path destMetaPath = GetMetaSidecarPath(destPath);
RenamePathCaseAware(sourceMetaPath, destMetaPath);
}
bool MovePathWithOptionalMeta(
const std::filesystem::path& sourcePath,
const std::filesystem::path& destinationPath) {
std::error_code errorCode = {};
const std::filesystem::path sourceMetaPath = GetMetaSidecarPath(sourcePath);
const std::filesystem::path destinationMetaPath = GetMetaSidecarPath(destinationPath);
const bool moveMeta = std::filesystem::exists(sourceMetaPath, errorCode);
if (errorCode) {
return false;
}
std::filesystem::rename(sourcePath, destinationPath, errorCode);
if (errorCode) {
return false;
}
if (!moveMeta) {
return true;
}
std::filesystem::rename(sourceMetaPath, destinationMetaPath, errorCode);
if (!errorCode) {
return true;
}
std::error_code rollbackError = {};
std::filesystem::rename(destinationPath, sourcePath, rollbackError);
return false;
}
ProjectBrowserModel::ItemKind ResolveItemKind(
const std::filesystem::path& path,
bool directory) {
if (directory) {
return ProjectBrowserModel::ItemKind::Folder;
}
std::wstring extension = path.extension().wstring();
std::transform(extension.begin(), extension.end(), extension.begin(), ::towlower);
if (MatchesExtension(extension, { L".xc", L".unity", L".scene" })) {
return ProjectBrowserModel::ItemKind::Scene;
}
if (MatchesExtension(extension, { L".fbx", L".obj", L".gltf", L".glb" })) {
return ProjectBrowserModel::ItemKind::Model;
}
if (extension == L".mat") {
return ProjectBrowserModel::ItemKind::Material;
}
if (MatchesExtension(extension, {
L".png", L".jpg", L".jpeg", L".tga", L".bmp", L".gif",
L".psd", L".hdr", L".pic", L".ppm", L".pgm", L".pbm",
L".pnm", L".dds", L".ktx", L".ktx2", L".webp" })) {
return ProjectBrowserModel::ItemKind::Texture;
}
if (MatchesExtension(extension, { L".cs", L".cpp", L".c", L".h", L".hpp" })) {
return ProjectBrowserModel::ItemKind::Script;
}
return ProjectBrowserModel::ItemKind::File;
}
bool CanOpenItemKind(ProjectBrowserModel::ItemKind kind) {
return kind == ProjectBrowserModel::ItemKind::Folder ||
kind == ProjectBrowserModel::ItemKind::Scene;
}
bool CanPreviewItem(
const std::filesystem::path& path,
ProjectBrowserModel::ItemKind kind,
bool directory) {
if (directory || kind != ProjectBrowserModel::ItemKind::Texture) {
return false;
}
std::wstring extension = path.extension().wstring();
std::transform(extension.begin(), extension.end(), extension.begin(), ::towlower);
return MatchesExtension(extension, {
L".png",
L".jpg",
L".jpeg",
L".tga",
L".bmp",
L".gif",
L".psd",
L".hdr",
L".pic",
L".ppm",
L".pgm",
L".pbm",
L".pnm"
});
}
} // namespace XCEngine::UI::Editor::App
namespace XCEngine::UI::Editor::App {
namespace {
@@ -708,3 +1181,133 @@ void ProjectBrowserModel::EnsureValidCurrentFolder() {
}
} // namespace XCEngine::UI::Editor::App
namespace XCEngine::UI::Editor::App {
void ProjectBrowserModel::RefreshAssetList() {
EnsureValidCurrentFolder();
m_assetEntries.clear();
const FolderEntry* currentFolder = FindFolderEntry(m_currentFolderId);
if (currentFolder == nullptr) {
return;
}
std::vector<std::filesystem::directory_entry> entries = {};
std::error_code errorCode = {};
const std::filesystem::directory_iterator end = {};
for (std::filesystem::directory_iterator iterator(currentFolder->absolutePath, errorCode);
!errorCode && iterator != end;
iterator.increment(errorCode)) {
if (!iterator->exists(errorCode) || IsMetaFile(iterator->path())) {
continue;
}
if (!iterator->is_directory(errorCode) && !iterator->is_regular_file(errorCode)) {
continue;
}
entries.push_back(*iterator);
}
std::sort(
entries.begin(),
entries.end(),
[](const std::filesystem::directory_entry& lhs, const std::filesystem::directory_entry& rhs) {
const bool lhsDirectory = lhs.is_directory();
const bool rhsDirectory = rhs.is_directory();
if (lhsDirectory != rhsDirectory) {
return lhsDirectory && !rhsDirectory;
}
return ToLowerCopy(PathToUtf8String(lhs.path().filename())) <
ToLowerCopy(PathToUtf8String(rhs.path().filename()));
});
for (const std::filesystem::directory_entry& entry : entries) {
AssetEntry assetEntry = {};
assetEntry.itemId = BuildRelativeItemId(entry.path(), m_assetsRootPath);
assetEntry.absolutePath = entry.path();
assetEntry.nameWithExtension =
BuildAssetNameWithExtension(entry.path(), entry.is_directory());
assetEntry.displayName = BuildAssetDisplayName(entry.path(), entry.is_directory());
assetEntry.extensionLower = ToLowerCopy(PathToUtf8String(entry.path().extension()));
assetEntry.kind = ResolveItemKind(entry.path(), entry.is_directory());
assetEntry.directory = entry.is_directory();
assetEntry.canOpen = CanOpenItemKind(assetEntry.kind);
assetEntry.canPreview =
CanPreviewItem(entry.path(), assetEntry.kind, assetEntry.directory);
m_assetEntries.push_back(std::move(assetEntry));
}
}
} // namespace XCEngine::UI::Editor::App
namespace XCEngine::UI::Editor::App {
void ProjectBrowserModel::RefreshFolderTree() {
m_folderEntries.clear();
m_treeItems.clear();
if (m_assetsRootPath.empty() || !std::filesystem::exists(m_assetsRootPath)) {
return;
}
const auto appendFolderRecursive =
[&](auto&& self, const std::filesystem::path& folderPath, std::uint32_t depth) -> void {
const std::string itemId = BuildRelativeItemId(folderPath, m_assetsRootPath);
FolderEntry folderEntry = {};
folderEntry.itemId = itemId;
folderEntry.absolutePath = folderPath;
folderEntry.label = PathToUtf8String(folderPath.filename());
m_folderEntries.push_back(folderEntry);
Widgets::UIEditorTreeViewItem item = {};
item.itemId = itemId;
item.label = folderEntry.label;
item.depth = depth;
item.forceLeaf = !HasChildDirectories(folderPath);
item.leadingIcon = m_folderIcon;
m_treeItems.push_back(std::move(item));
const std::vector<std::filesystem::path> childFolders =
CollectSortedChildDirectories(folderPath);
for (const std::filesystem::path& childPath : childFolders) {
self(self, childPath, depth + 1u);
}
};
appendFolderRecursive(appendFolderRecursive, m_assetsRootPath, 0u);
}
std::vector<std::string> ProjectBrowserModel::CollectCurrentFolderAncestorIds() const {
return BuildAncestorFolderIds(m_currentFolderId);
}
std::vector<std::string> ProjectBrowserModel::BuildAncestorFolderIds(
std::string_view itemId) const {
std::vector<std::string> ancestors = {};
const FolderEntry* folderEntry = FindFolderEntry(itemId);
if (folderEntry == nullptr) {
return ancestors;
}
std::filesystem::path path = folderEntry->absolutePath;
while (true) {
ancestors.push_back(BuildRelativeItemId(path, m_assetsRootPath));
if (path == m_assetsRootPath) {
break;
}
path = path.parent_path();
}
std::reverse(ancestors.begin(), ancestors.end());
return ancestors;
}
} // namespace XCEngine::UI::Editor::App