2026-04-21 00:57:14 +08:00
|
|
|
|
#include "ProjectBrowserModel.h"
|
|
|
|
|
|
#include <filesystem>
|
|
|
|
|
|
#include <string>
|
|
|
|
|
|
#include <string_view>
|
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
|
#include <cctype>
|
|
|
|
|
|
#include <cwctype>
|
|
|
|
|
|
#include <initializer_list>
|
|
|
|
|
|
#include <system_error>
|
2026-04-19 00:03:25 +08:00
|
|
|
|
#include <fstream>
|
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
|
namespace XCEngine::UI::Editor::App {
|
2026-04-12 11:12:27 +08:00
|
|
|
|
|
2026-04-21 00:57:14 +08:00
|
|
|
|
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 {
|
|
|
|
|
|
|
2026-04-12 11:12:27 +08:00
|
|
|
|
|
2026-04-19 00:03:25 +08:00
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
|
|
bool IsSameOrDescendantFolderId(
|
|
|
|
|
|
std::string_view candidateId,
|
|
|
|
|
|
std::string_view ancestorId) {
|
|
|
|
|
|
if (candidateId == ancestorId) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (candidateId.size() <= ancestorId.size() ||
|
|
|
|
|
|
candidateId.substr(0u, ancestorId.size()) != ancestorId) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return candidateId[ancestorId.size()] == '/';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string RemapMovedFolderId(
|
|
|
|
|
|
std::string_view itemId,
|
|
|
|
|
|
std::string_view sourceFolderId,
|
|
|
|
|
|
std::string_view destinationFolderId) {
|
|
|
|
|
|
if (!IsSameOrDescendantFolderId(itemId, sourceFolderId)) {
|
|
|
|
|
|
return std::string(itemId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (itemId == sourceFolderId) {
|
|
|
|
|
|
return std::string(destinationFolderId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string remapped = std::string(destinationFolderId);
|
|
|
|
|
|
remapped += std::string(itemId.substr(sourceFolderId.size()));
|
|
|
|
|
|
return remapped;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool IsSameOrDescendantItemId(
|
|
|
|
|
|
std::string_view candidateId,
|
|
|
|
|
|
std::string_view ancestorId) {
|
|
|
|
|
|
if (candidateId == ancestorId) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (candidateId.size() <= ancestorId.size() ||
|
|
|
|
|
|
candidateId.substr(0u, ancestorId.size()) != ancestorId) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return candidateId[ancestorId.size()] == '/';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string RemapMovedItemId(
|
|
|
|
|
|
std::string_view itemId,
|
|
|
|
|
|
std::string_view sourceItemId,
|
|
|
|
|
|
std::string_view destinationItemId) {
|
|
|
|
|
|
if (!IsSameOrDescendantItemId(itemId, sourceItemId)) {
|
|
|
|
|
|
return std::string(itemId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (itemId == sourceItemId) {
|
|
|
|
|
|
return std::string(destinationItemId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string remapped = std::string(destinationItemId);
|
|
|
|
|
|
remapped += std::string(itemId.substr(sourceItemId.size()));
|
|
|
|
|
|
return remapped;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
|
void ProjectBrowserModel::Initialize(const std::filesystem::path& repoRoot) {
|
2026-04-12 11:12:27 +08:00
|
|
|
|
m_assetsRootPath = (repoRoot / "project/Assets").lexically_normal();
|
2026-04-19 00:03:25 +08:00
|
|
|
|
std::error_code errorCode = {};
|
|
|
|
|
|
if (!std::filesystem::exists(m_assetsRootPath, errorCode)) {
|
|
|
|
|
|
std::filesystem::create_directories(m_assetsRootPath / "Scenes", errorCode);
|
|
|
|
|
|
}
|
2026-04-12 11:12:27 +08:00
|
|
|
|
Refresh();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
|
void ProjectBrowserModel::SetFolderIcon(const ::XCEngine::UI::UITextureHandle& icon) {
|
2026-04-12 11:12:27 +08:00
|
|
|
|
m_folderIcon = icon;
|
|
|
|
|
|
if (!m_assetsRootPath.empty()) {
|
|
|
|
|
|
RefreshFolderTree();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
|
void ProjectBrowserModel::Refresh() {
|
2026-04-12 11:12:27 +08:00
|
|
|
|
RefreshFolderTree();
|
|
|
|
|
|
EnsureValidCurrentFolder();
|
|
|
|
|
|
RefreshAssetList();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
|
bool ProjectBrowserModel::Empty() const {
|
2026-04-12 11:12:27 +08:00
|
|
|
|
return m_treeItems.empty();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-19 00:03:25 +08:00
|
|
|
|
std::filesystem::path ProjectBrowserModel::GetProjectRootPath() const {
|
|
|
|
|
|
return m_assetsRootPath.empty()
|
|
|
|
|
|
? std::filesystem::path()
|
|
|
|
|
|
: m_assetsRootPath.parent_path();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
|
const std::filesystem::path& ProjectBrowserModel::GetAssetsRootPath() const {
|
2026-04-12 11:12:27 +08:00
|
|
|
|
return m_assetsRootPath;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
|
const std::vector<ProjectBrowserModel::FolderEntry>& ProjectBrowserModel::GetFolderEntries() const {
|
2026-04-12 11:12:27 +08:00
|
|
|
|
return m_folderEntries;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
|
const std::vector<Widgets::UIEditorTreeViewItem>& ProjectBrowserModel::GetTreeItems() const {
|
2026-04-12 11:12:27 +08:00
|
|
|
|
return m_treeItems;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
|
const std::vector<ProjectBrowserModel::AssetEntry>& ProjectBrowserModel::GetAssetEntries() const {
|
2026-04-12 11:12:27 +08:00
|
|
|
|
return m_assetEntries;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
|
const std::string& ProjectBrowserModel::GetCurrentFolderId() const {
|
2026-04-12 11:12:27 +08:00
|
|
|
|
return m_currentFolderId;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-19 00:03:25 +08:00
|
|
|
|
bool ProjectBrowserModel::IsAssetsRoot(std::string_view itemId) const {
|
|
|
|
|
|
return itemId == kAssetsRootId;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
|
const ProjectBrowserModel::FolderEntry* ProjectBrowserModel::FindFolderEntry(
|
2026-04-12 11:12:27 +08:00
|
|
|
|
std::string_view itemId) const {
|
|
|
|
|
|
for (const FolderEntry& entry : m_folderEntries) {
|
|
|
|
|
|
if (entry.itemId == itemId) {
|
|
|
|
|
|
return &entry;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
|
const ProjectBrowserModel::AssetEntry* ProjectBrowserModel::FindAssetEntry(
|
2026-04-12 11:12:27 +08:00
|
|
|
|
std::string_view itemId) const {
|
|
|
|
|
|
for (const AssetEntry& entry : m_assetEntries) {
|
|
|
|
|
|
if (entry.itemId == itemId) {
|
|
|
|
|
|
return &entry;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-19 00:03:25 +08:00
|
|
|
|
std::optional<std::filesystem::path> ProjectBrowserModel::ResolveItemAbsolutePath(
|
|
|
|
|
|
std::string_view itemId) const {
|
|
|
|
|
|
if (itemId.empty() || m_assetsRootPath.empty()) {
|
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::filesystem::path rootPath = m_assetsRootPath.parent_path();
|
|
|
|
|
|
const std::filesystem::path candidatePath =
|
|
|
|
|
|
(rootPath / std::filesystem::path(std::string(itemId))).lexically_normal();
|
|
|
|
|
|
if (!IsSameOrDescendantPath(candidatePath, m_assetsRootPath)) {
|
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return candidatePath;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string ProjectBrowserModel::BuildProjectRelativePath(std::string_view itemId) const {
|
|
|
|
|
|
const std::optional<std::filesystem::path> absolutePath =
|
|
|
|
|
|
ResolveItemAbsolutePath(itemId);
|
|
|
|
|
|
if (!absolutePath.has_value()) {
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return BuildRelativeProjectPath(absolutePath.value(), GetProjectRootPath());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool ProjectBrowserModel::CreateFolder(
|
|
|
|
|
|
std::string_view parentFolderId,
|
|
|
|
|
|
std::string_view requestedName,
|
|
|
|
|
|
std::string* createdFolderId) {
|
|
|
|
|
|
if (createdFolderId != nullptr) {
|
|
|
|
|
|
createdFolderId->clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const FolderEntry* parentFolder = FindFolderEntry(parentFolderId);
|
|
|
|
|
|
if (parentFolder == nullptr) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const std::string trimmedName = TrimAssetName(requestedName);
|
|
|
|
|
|
if (HasInvalidAssetName(trimmedName)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::filesystem::path newFolderPath =
|
|
|
|
|
|
MakeUniqueFolderPath(parentFolder->absolutePath, trimmedName);
|
|
|
|
|
|
if (!std::filesystem::create_directory(newFolderPath)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
RefreshFolderTree();
|
|
|
|
|
|
EnsureValidCurrentFolder();
|
|
|
|
|
|
RefreshAssetList();
|
|
|
|
|
|
if (createdFolderId != nullptr) {
|
|
|
|
|
|
*createdFolderId = BuildRelativeItemId(newFolderPath, m_assetsRootPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (...) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool ProjectBrowserModel::CreateMaterial(
|
|
|
|
|
|
std::string_view parentFolderId,
|
|
|
|
|
|
std::string_view requestedName,
|
|
|
|
|
|
std::string* createdItemId) {
|
|
|
|
|
|
if (createdItemId != nullptr) {
|
|
|
|
|
|
createdItemId->clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const FolderEntry* parentFolder = FindFolderEntry(parentFolderId);
|
|
|
|
|
|
if (parentFolder == nullptr) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const std::string trimmedName = TrimAssetName(requestedName);
|
|
|
|
|
|
if (HasInvalidAssetName(trimmedName)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::filesystem::path requestedPath =
|
2026-04-19 15:52:28 +08:00
|
|
|
|
BuildPathFromUtf8(trimmedName);
|
2026-04-19 00:03:25 +08:00
|
|
|
|
if (!requestedPath.has_extension()) {
|
2026-04-19 15:52:28 +08:00
|
|
|
|
requestedPath.replace_extension(".mat");
|
2026-04-19 00:03:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::filesystem::path materialPath =
|
|
|
|
|
|
MakeUniqueFilePath(parentFolder->absolutePath, requestedPath);
|
|
|
|
|
|
std::ofstream output(materialPath, std::ios::out | std::ios::trunc);
|
|
|
|
|
|
if (!output.is_open()) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
output <<
|
|
|
|
|
|
"{\n"
|
|
|
|
|
|
" \"renderQueue\": \"geometry\",\n"
|
|
|
|
|
|
" \"renderState\": {\n"
|
|
|
|
|
|
" \"cull\": \"none\",\n"
|
|
|
|
|
|
" \"depthTest\": true,\n"
|
|
|
|
|
|
" \"depthWrite\": true,\n"
|
|
|
|
|
|
" \"depthFunc\": \"less\",\n"
|
|
|
|
|
|
" \"blendEnable\": false,\n"
|
|
|
|
|
|
" \"srcBlend\": \"one\",\n"
|
|
|
|
|
|
" \"dstBlend\": \"zero\",\n"
|
|
|
|
|
|
" \"srcBlendAlpha\": \"one\",\n"
|
|
|
|
|
|
" \"dstBlendAlpha\": \"zero\",\n"
|
|
|
|
|
|
" \"blendOp\": \"add\",\n"
|
|
|
|
|
|
" \"blendOpAlpha\": \"add\",\n"
|
|
|
|
|
|
" \"colorWriteMask\": 15\n"
|
|
|
|
|
|
" }\n"
|
|
|
|
|
|
"}\n";
|
|
|
|
|
|
output.close();
|
|
|
|
|
|
if (!output.good()) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
RefreshFolderTree();
|
|
|
|
|
|
EnsureValidCurrentFolder();
|
|
|
|
|
|
RefreshAssetList();
|
|
|
|
|
|
if (createdItemId != nullptr) {
|
|
|
|
|
|
*createdItemId = BuildRelativeItemId(materialPath, m_assetsRootPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (...) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool ProjectBrowserModel::RenameItem(
|
|
|
|
|
|
std::string_view itemId,
|
|
|
|
|
|
std::string_view newName,
|
|
|
|
|
|
std::string* renamedItemId) {
|
|
|
|
|
|
if (renamedItemId != nullptr) {
|
|
|
|
|
|
renamedItemId->clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (itemId.empty() || IsAssetsRoot(itemId)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::optional<std::filesystem::path> sourcePath =
|
|
|
|
|
|
ResolveItemAbsolutePath(itemId);
|
|
|
|
|
|
if (!sourcePath.has_value()) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const std::string trimmedName = TrimAssetName(newName);
|
|
|
|
|
|
if (HasInvalidAssetName(trimmedName) ||
|
|
|
|
|
|
!std::filesystem::exists(sourcePath.value())) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::string targetName =
|
|
|
|
|
|
BuildRenamedEntryName(sourcePath.value(), trimmedName);
|
|
|
|
|
|
if (HasInvalidAssetName(targetName)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::filesystem::path destinationPath =
|
2026-04-19 15:52:28 +08:00
|
|
|
|
(sourcePath->parent_path() / BuildPathFromUtf8(targetName))
|
2026-04-19 00:03:25 +08:00
|
|
|
|
.lexically_normal();
|
|
|
|
|
|
const std::string destinationItemId =
|
|
|
|
|
|
BuildRelativeItemId(destinationPath, m_assetsRootPath);
|
|
|
|
|
|
|
|
|
|
|
|
if (!RenamePathCaseAware(sourcePath.value(), destinationPath)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
MoveMetaSidecarIfPresent(sourcePath.value(), destinationPath);
|
|
|
|
|
|
|
|
|
|
|
|
if (std::filesystem::is_directory(destinationPath)) {
|
|
|
|
|
|
m_currentFolderId = RemapMovedItemId(
|
|
|
|
|
|
m_currentFolderId,
|
|
|
|
|
|
itemId,
|
|
|
|
|
|
destinationItemId);
|
|
|
|
|
|
}
|
|
|
|
|
|
RefreshFolderTree();
|
|
|
|
|
|
EnsureValidCurrentFolder();
|
|
|
|
|
|
RefreshAssetList();
|
|
|
|
|
|
if (renamedItemId != nullptr) {
|
|
|
|
|
|
*renamedItemId = destinationItemId;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (...) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool ProjectBrowserModel::DeleteItem(std::string_view itemId) {
|
|
|
|
|
|
if (itemId.empty() || IsAssetsRoot(itemId)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::optional<std::filesystem::path> sourcePath =
|
|
|
|
|
|
ResolveItemAbsolutePath(itemId);
|
|
|
|
|
|
if (!sourcePath.has_value()) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!std::filesystem::exists(sourcePath.value())) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (std::filesystem::is_directory(sourcePath.value()) &&
|
|
|
|
|
|
IsSameOrDescendantFolderId(m_currentFolderId, itemId)) {
|
|
|
|
|
|
m_currentFolderId =
|
|
|
|
|
|
BuildRelativeItemId(sourcePath->parent_path(), m_assetsRootPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::filesystem::remove_all(sourcePath.value());
|
|
|
|
|
|
RemoveMetaSidecarIfPresent(sourcePath.value());
|
|
|
|
|
|
RefreshFolderTree();
|
|
|
|
|
|
EnsureValidCurrentFolder();
|
|
|
|
|
|
RefreshAssetList();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (...) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool ProjectBrowserModel::CanMoveItemToFolder(
|
|
|
|
|
|
std::string_view itemId,
|
|
|
|
|
|
std::string_view targetFolderId) const {
|
|
|
|
|
|
if (itemId.empty() || targetFolderId.empty() || IsAssetsRoot(itemId)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::optional<std::filesystem::path> sourcePath =
|
|
|
|
|
|
ResolveItemAbsolutePath(itemId);
|
|
|
|
|
|
const FolderEntry* targetFolder = FindFolderEntry(targetFolderId);
|
|
|
|
|
|
if (!sourcePath.has_value() || targetFolder == nullptr) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::error_code errorCode = {};
|
|
|
|
|
|
if (!std::filesystem::exists(sourcePath.value(), errorCode) || errorCode) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::filesystem::path destinationPath =
|
|
|
|
|
|
(targetFolder->absolutePath / sourcePath->filename()).lexically_normal();
|
|
|
|
|
|
if (destinationPath == sourcePath->lexically_normal()) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (std::filesystem::exists(destinationPath, errorCode) || errorCode) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (std::filesystem::is_directory(sourcePath.value(), errorCode)) {
|
|
|
|
|
|
if (errorCode) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (IsSameOrDescendantPath(targetFolder->absolutePath, sourcePath.value())) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::filesystem::path sourceMetaPath = GetMetaSidecarPath(sourcePath.value());
|
|
|
|
|
|
const std::filesystem::path destinationMetaPath = GetMetaSidecarPath(destinationPath);
|
|
|
|
|
|
const bool sourceMetaExists = std::filesystem::exists(sourceMetaPath, errorCode);
|
|
|
|
|
|
if (errorCode) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (sourceMetaExists && std::filesystem::exists(destinationMetaPath, errorCode)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return !errorCode;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool ProjectBrowserModel::MoveItemToFolder(
|
|
|
|
|
|
std::string_view itemId,
|
|
|
|
|
|
std::string_view targetFolderId,
|
|
|
|
|
|
std::string* movedItemId) {
|
|
|
|
|
|
if (movedItemId != nullptr) {
|
|
|
|
|
|
movedItemId->clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!CanMoveItemToFolder(itemId, targetFolderId)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::optional<std::filesystem::path> sourcePath =
|
|
|
|
|
|
ResolveItemAbsolutePath(itemId);
|
|
|
|
|
|
const FolderEntry* targetFolder = FindFolderEntry(targetFolderId);
|
|
|
|
|
|
if (!sourcePath.has_value() || targetFolder == nullptr) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const std::filesystem::path destinationPath =
|
|
|
|
|
|
(targetFolder->absolutePath / sourcePath->filename()).lexically_normal();
|
|
|
|
|
|
const std::string destinationItemId =
|
|
|
|
|
|
BuildRelativeItemId(destinationPath, m_assetsRootPath);
|
|
|
|
|
|
if (!MovePathWithOptionalMeta(sourcePath.value(), destinationPath)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (std::filesystem::is_directory(destinationPath)) {
|
|
|
|
|
|
m_currentFolderId = RemapMovedItemId(
|
|
|
|
|
|
m_currentFolderId,
|
|
|
|
|
|
itemId,
|
|
|
|
|
|
destinationItemId);
|
|
|
|
|
|
}
|
|
|
|
|
|
RefreshFolderTree();
|
|
|
|
|
|
EnsureValidCurrentFolder();
|
|
|
|
|
|
RefreshAssetList();
|
|
|
|
|
|
if (movedItemId != nullptr) {
|
|
|
|
|
|
*movedItemId = destinationItemId;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (...) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::optional<std::string> ProjectBrowserModel::GetParentFolderId(std::string_view itemId) const {
|
|
|
|
|
|
const FolderEntry* folderEntry = FindFolderEntry(itemId);
|
|
|
|
|
|
if (folderEntry == nullptr || itemId == kAssetsRootId) {
|
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::filesystem::path parentPath = folderEntry->absolutePath.parent_path();
|
|
|
|
|
|
if (parentPath.empty() || parentPath == folderEntry->absolutePath) {
|
|
|
|
|
|
return std::nullopt;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return BuildRelativeItemId(parentPath, m_assetsRootPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool ProjectBrowserModel::CanReparentFolder(
|
|
|
|
|
|
std::string_view sourceFolderId,
|
|
|
|
|
|
std::string_view targetParentId) const {
|
|
|
|
|
|
if (sourceFolderId.empty() ||
|
|
|
|
|
|
targetParentId.empty() ||
|
|
|
|
|
|
sourceFolderId == kAssetsRootId) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const FolderEntry* sourceFolder = FindFolderEntry(sourceFolderId);
|
|
|
|
|
|
const FolderEntry* targetFolder = FindFolderEntry(targetParentId);
|
|
|
|
|
|
if (sourceFolder == nullptr || targetFolder == nullptr) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (IsSameOrDescendantFolderId(targetParentId, sourceFolderId)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::optional<std::string> currentParentId = GetParentFolderId(sourceFolderId);
|
|
|
|
|
|
if (currentParentId.has_value() && currentParentId.value() == targetParentId) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::filesystem::path destinationPath =
|
|
|
|
|
|
targetFolder->absolutePath / sourceFolder->absolutePath.filename();
|
|
|
|
|
|
if (destinationPath.lexically_normal() == sourceFolder->absolutePath.lexically_normal()) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::error_code errorCode = {};
|
|
|
|
|
|
if (std::filesystem::exists(destinationPath, errorCode)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (errorCode) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::filesystem::path sourceMetaPath = GetMetaSidecarPath(sourceFolder->absolutePath);
|
|
|
|
|
|
const std::filesystem::path destinationMetaPath = GetMetaSidecarPath(destinationPath);
|
|
|
|
|
|
const bool sourceMetaExists = std::filesystem::exists(sourceMetaPath, errorCode);
|
|
|
|
|
|
if (errorCode) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (sourceMetaExists && std::filesystem::exists(destinationMetaPath, errorCode)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return !errorCode;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool ProjectBrowserModel::ReparentFolder(
|
|
|
|
|
|
std::string_view sourceFolderId,
|
|
|
|
|
|
std::string_view targetParentId,
|
|
|
|
|
|
std::string* movedFolderId) {
|
|
|
|
|
|
if (!CanReparentFolder(sourceFolderId, targetParentId)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const FolderEntry* sourceFolder = FindFolderEntry(sourceFolderId);
|
|
|
|
|
|
const FolderEntry* targetFolder = FindFolderEntry(targetParentId);
|
|
|
|
|
|
if (sourceFolder == nullptr || targetFolder == nullptr) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::filesystem::path destinationPath =
|
|
|
|
|
|
(targetFolder->absolutePath / sourceFolder->absolutePath.filename()).lexically_normal();
|
|
|
|
|
|
const std::string destinationFolderId =
|
|
|
|
|
|
BuildRelativeItemId(destinationPath, m_assetsRootPath);
|
|
|
|
|
|
|
|
|
|
|
|
if (!MovePathWithOptionalMeta(sourceFolder->absolutePath, destinationPath)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
m_currentFolderId = RemapMovedFolderId(
|
|
|
|
|
|
m_currentFolderId,
|
|
|
|
|
|
sourceFolderId,
|
|
|
|
|
|
destinationFolderId);
|
|
|
|
|
|
RefreshFolderTree();
|
|
|
|
|
|
EnsureValidCurrentFolder();
|
|
|
|
|
|
RefreshAssetList();
|
|
|
|
|
|
|
|
|
|
|
|
if (movedFolderId != nullptr) {
|
|
|
|
|
|
*movedFolderId = destinationFolderId;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool ProjectBrowserModel::MoveFolderToRoot(
|
|
|
|
|
|
std::string_view sourceFolderId,
|
|
|
|
|
|
std::string* movedFolderId) {
|
|
|
|
|
|
if (sourceFolderId.empty() || sourceFolderId == kAssetsRootId) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const FolderEntry* sourceFolder = FindFolderEntry(sourceFolderId);
|
|
|
|
|
|
if (sourceFolder == nullptr) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::optional<std::string> currentParentId = GetParentFolderId(sourceFolderId);
|
|
|
|
|
|
if (!currentParentId.has_value() || currentParentId.value() == kAssetsRootId) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::filesystem::path destinationPath =
|
|
|
|
|
|
(m_assetsRootPath / sourceFolder->absolutePath.filename()).lexically_normal();
|
|
|
|
|
|
if (destinationPath == sourceFolder->absolutePath.lexically_normal()) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::error_code errorCode = {};
|
|
|
|
|
|
if (std::filesystem::exists(destinationPath, errorCode)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (errorCode) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::filesystem::path sourceMetaPath = GetMetaSidecarPath(sourceFolder->absolutePath);
|
|
|
|
|
|
const std::filesystem::path destinationMetaPath = GetMetaSidecarPath(destinationPath);
|
|
|
|
|
|
const bool sourceMetaExists = std::filesystem::exists(sourceMetaPath, errorCode);
|
|
|
|
|
|
if (errorCode) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (sourceMetaExists && std::filesystem::exists(destinationMetaPath, errorCode)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (errorCode) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::string destinationFolderId =
|
|
|
|
|
|
BuildRelativeItemId(destinationPath, m_assetsRootPath);
|
|
|
|
|
|
if (!MovePathWithOptionalMeta(sourceFolder->absolutePath, destinationPath)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
m_currentFolderId = RemapMovedFolderId(
|
|
|
|
|
|
m_currentFolderId,
|
|
|
|
|
|
sourceFolderId,
|
|
|
|
|
|
destinationFolderId);
|
|
|
|
|
|
RefreshFolderTree();
|
|
|
|
|
|
EnsureValidCurrentFolder();
|
|
|
|
|
|
RefreshAssetList();
|
|
|
|
|
|
|
|
|
|
|
|
if (movedFolderId != nullptr) {
|
|
|
|
|
|
*movedFolderId = destinationFolderId;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
|
bool ProjectBrowserModel::NavigateToFolder(std::string_view itemId) {
|
2026-04-12 11:12:27 +08:00
|
|
|
|
if (itemId.empty() || FindFolderEntry(itemId) == nullptr || itemId == m_currentFolderId) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
m_currentFolderId = std::string(itemId);
|
|
|
|
|
|
EnsureValidCurrentFolder();
|
|
|
|
|
|
RefreshAssetList();
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
|
std::vector<ProjectBrowserModel::BreadcrumbSegment> ProjectBrowserModel::BuildBreadcrumbSegments() const {
|
2026-04-12 11:12:27 +08:00
|
|
|
|
std::vector<BreadcrumbSegment> segments = {};
|
|
|
|
|
|
if (m_currentFolderId.empty()) {
|
|
|
|
|
|
segments.push_back(BreadcrumbSegment{ std::string(kAssetsRootId), std::string(kAssetsRootId), true });
|
|
|
|
|
|
return segments;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string cumulativeFolderId = {};
|
|
|
|
|
|
std::size_t segmentStart = 0u;
|
|
|
|
|
|
while (segmentStart < m_currentFolderId.size()) {
|
|
|
|
|
|
const std::size_t separator = m_currentFolderId.find('/', segmentStart);
|
|
|
|
|
|
const std::size_t segmentLength =
|
|
|
|
|
|
separator == std::string_view::npos
|
|
|
|
|
|
? m_currentFolderId.size() - segmentStart
|
|
|
|
|
|
: separator - segmentStart;
|
|
|
|
|
|
if (segmentLength > 0u) {
|
|
|
|
|
|
std::string label = std::string(m_currentFolderId.substr(segmentStart, segmentLength));
|
|
|
|
|
|
if (cumulativeFolderId.empty()) {
|
|
|
|
|
|
cumulativeFolderId = label;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
cumulativeFolderId += "/";
|
|
|
|
|
|
cumulativeFolderId += label;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
segments.push_back(BreadcrumbSegment{
|
|
|
|
|
|
std::move(label),
|
|
|
|
|
|
cumulativeFolderId,
|
|
|
|
|
|
false
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
if (separator == std::string_view::npos) {
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
segmentStart = separator + 1u;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (segments.empty()) {
|
|
|
|
|
|
segments.push_back(BreadcrumbSegment{ std::string(kAssetsRootId), std::string(kAssetsRootId), true });
|
|
|
|
|
|
return segments;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
segments.back().current = true;
|
|
|
|
|
|
return segments;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
|
void ProjectBrowserModel::EnsureValidCurrentFolder() {
|
2026-04-12 11:12:27 +08:00
|
|
|
|
if (m_currentFolderId.empty()) {
|
|
|
|
|
|
m_currentFolderId = std::string(kAssetsRootId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (FindFolderEntry(m_currentFolderId) == nullptr) {
|
|
|
|
|
|
m_currentFolderId = m_treeItems.empty()
|
|
|
|
|
|
? std::string()
|
|
|
|
|
|
: m_treeItems.front().itemId;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 08:24:06 +08:00
|
|
|
|
} // namespace XCEngine::UI::Editor::App
|
2026-04-21 00:57:14 +08:00
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|