Advance new editor hosted panels and state flow
This commit is contained in:
376
new_editor/app/Project/ProductProjectBrowserModel.cpp
Normal file
376
new_editor/app/Project/ProductProjectBrowserModel.cpp
Normal file
@@ -0,0 +1,376 @@
|
||||
#include "Project/ProductProjectBrowserModel.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace XCEngine::UI::Editor::App::Project {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::string_view kAssetsRootId = "Assets";
|
||||
|
||||
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 WideToUtf8(std::wstring_view value) {
|
||||
if (value.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const int requiredSize = WideCharToMultiByte(
|
||||
CP_UTF8,
|
||||
0,
|
||||
value.data(),
|
||||
static_cast<int>(value.size()),
|
||||
nullptr,
|
||||
0,
|
||||
nullptr,
|
||||
nullptr);
|
||||
if (requiredSize <= 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string result(static_cast<std::size_t>(requiredSize), '\0');
|
||||
WideCharToMultiByte(
|
||||
CP_UTF8,
|
||||
0,
|
||||
value.data(),
|
||||
static_cast<int>(value.size()),
|
||||
result.data(),
|
||||
requiredSize,
|
||||
nullptr,
|
||||
nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string PathToUtf8String(const std::filesystem::path& path) {
|
||||
return WideToUtf8(path.native());
|
||||
}
|
||||
|
||||
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 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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ProductProjectBrowserModel::Initialize(const std::filesystem::path& repoRoot) {
|
||||
m_assetsRootPath = (repoRoot / "project/Assets").lexically_normal();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void ProductProjectBrowserModel::SetFolderIcon(const ::XCEngine::UI::UITextureHandle& icon) {
|
||||
m_folderIcon = icon;
|
||||
if (!m_assetsRootPath.empty()) {
|
||||
RefreshFolderTree();
|
||||
}
|
||||
}
|
||||
|
||||
void ProductProjectBrowserModel::Refresh() {
|
||||
RefreshFolderTree();
|
||||
EnsureValidCurrentFolder();
|
||||
RefreshAssetList();
|
||||
}
|
||||
|
||||
bool ProductProjectBrowserModel::Empty() const {
|
||||
return m_treeItems.empty();
|
||||
}
|
||||
|
||||
const std::filesystem::path& ProductProjectBrowserModel::GetAssetsRootPath() const {
|
||||
return m_assetsRootPath;
|
||||
}
|
||||
|
||||
const std::vector<ProductProjectBrowserModel::FolderEntry>& ProductProjectBrowserModel::GetFolderEntries() const {
|
||||
return m_folderEntries;
|
||||
}
|
||||
|
||||
const std::vector<Widgets::UIEditorTreeViewItem>& ProductProjectBrowserModel::GetTreeItems() const {
|
||||
return m_treeItems;
|
||||
}
|
||||
|
||||
const std::vector<ProductProjectBrowserModel::AssetEntry>& ProductProjectBrowserModel::GetAssetEntries() const {
|
||||
return m_assetEntries;
|
||||
}
|
||||
|
||||
const std::string& ProductProjectBrowserModel::GetCurrentFolderId() const {
|
||||
return m_currentFolderId;
|
||||
}
|
||||
|
||||
const ProductProjectBrowserModel::FolderEntry* ProductProjectBrowserModel::FindFolderEntry(
|
||||
std::string_view itemId) const {
|
||||
for (const FolderEntry& entry : m_folderEntries) {
|
||||
if (entry.itemId == itemId) {
|
||||
return &entry;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const ProductProjectBrowserModel::AssetEntry* ProductProjectBrowserModel::FindAssetEntry(
|
||||
std::string_view itemId) const {
|
||||
for (const AssetEntry& entry : m_assetEntries) {
|
||||
if (entry.itemId == itemId) {
|
||||
return &entry;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool ProductProjectBrowserModel::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<ProductProjectBrowserModel::BreadcrumbSegment> ProductProjectBrowserModel::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;
|
||||
}
|
||||
|
||||
std::vector<std::string> ProductProjectBrowserModel::CollectCurrentFolderAncestorIds() const {
|
||||
return BuildAncestorFolderIds(m_currentFolderId);
|
||||
}
|
||||
|
||||
std::vector<std::string> ProductProjectBrowserModel::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;
|
||||
}
|
||||
|
||||
void ProductProjectBrowserModel::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);
|
||||
}
|
||||
|
||||
void ProductProjectBrowserModel::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.displayName = BuildAssetDisplayName(entry.path(), entry.is_directory());
|
||||
assetEntry.directory = entry.is_directory();
|
||||
m_assetEntries.push_back(std::move(assetEntry));
|
||||
}
|
||||
}
|
||||
|
||||
void ProductProjectBrowserModel::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::Project
|
||||
Reference in New Issue
Block a user