Files
XCEngine/new_editor/app/Features/Project/ProjectBrowserModel.cpp

713 lines
21 KiB
C++

#include "ProjectBrowserModel.h"
#include "ProjectBrowserModelInternal.h"
#include "Internal/StringEncoding.h"
#include <fstream>
namespace XCEngine::UI::Editor::App {
using namespace ProjectBrowserModelInternal;
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
void ProjectBrowserModel::Initialize(const std::filesystem::path& repoRoot) {
m_assetsRootPath = (repoRoot / "project/Assets").lexically_normal();
std::error_code errorCode = {};
if (!std::filesystem::exists(m_assetsRootPath, errorCode)) {
std::filesystem::create_directories(m_assetsRootPath / "Scenes", errorCode);
}
Refresh();
}
void ProjectBrowserModel::SetFolderIcon(const ::XCEngine::UI::UITextureHandle& icon) {
m_folderIcon = icon;
if (!m_assetsRootPath.empty()) {
RefreshFolderTree();
}
}
void ProjectBrowserModel::Refresh() {
RefreshFolderTree();
EnsureValidCurrentFolder();
RefreshAssetList();
}
bool ProjectBrowserModel::Empty() const {
return m_treeItems.empty();
}
std::filesystem::path ProjectBrowserModel::GetProjectRootPath() const {
return m_assetsRootPath.empty()
? std::filesystem::path()
: m_assetsRootPath.parent_path();
}
const std::filesystem::path& ProjectBrowserModel::GetAssetsRootPath() const {
return m_assetsRootPath;
}
const std::vector<ProjectBrowserModel::FolderEntry>& ProjectBrowserModel::GetFolderEntries() const {
return m_folderEntries;
}
const std::vector<Widgets::UIEditorTreeViewItem>& ProjectBrowserModel::GetTreeItems() const {
return m_treeItems;
}
const std::vector<ProjectBrowserModel::AssetEntry>& ProjectBrowserModel::GetAssetEntries() const {
return m_assetEntries;
}
const std::string& ProjectBrowserModel::GetCurrentFolderId() const {
return m_currentFolderId;
}
bool ProjectBrowserModel::IsAssetsRoot(std::string_view itemId) const {
return itemId == kAssetsRootId;
}
const ProjectBrowserModel::FolderEntry* ProjectBrowserModel::FindFolderEntry(
std::string_view itemId) const {
for (const FolderEntry& entry : m_folderEntries) {
if (entry.itemId == itemId) {
return &entry;
}
}
return nullptr;
}
const ProjectBrowserModel::AssetEntry* ProjectBrowserModel::FindAssetEntry(
std::string_view itemId) const {
for (const AssetEntry& entry : m_assetEntries) {
if (entry.itemId == itemId) {
return &entry;
}
}
return nullptr;
}
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 =
App::Internal::Utf8ToWide(trimmedName);
if (!requestedPath.has_extension()) {
requestedPath += L".mat";
}
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 =
(sourcePath->parent_path() / App::Internal::Utf8ToWide(targetName))
.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;
}
bool ProjectBrowserModel::NavigateToFolder(std::string_view itemId) {
if (itemId.empty() || FindFolderEntry(itemId) == nullptr || itemId == m_currentFolderId) {
return false;
}
m_currentFolderId = std::string(itemId);
EnsureValidCurrentFolder();
RefreshAssetList();
return true;
}
std::vector<ProjectBrowserModel::BreadcrumbSegment> ProjectBrowserModel::BuildBreadcrumbSegments() const {
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;
}
void ProjectBrowserModel::EnsureValidCurrentFolder() {
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;
}
}
} // namespace XCEngine::UI::Editor::App