1014 lines
35 KiB
C++
1014 lines
35 KiB
C++
#include "Actions/ActionRouting.h"
|
|
#include "Actions/ProjectActionRouter.h"
|
|
#include "Commands/ProjectCommands.h"
|
|
#include "ProjectPanel.h"
|
|
#include "Core/IEditorContext.h"
|
|
#include "Core/IProjectManager.h"
|
|
#include "Core/ISceneManager.h"
|
|
#include "Core/AssetItem.h"
|
|
#include "Platform/Win32Utf8.h"
|
|
#include "Utils/ProjectFileUtils.h"
|
|
#include "UI/UI.h"
|
|
|
|
#include <XCEngine/Core/Asset/ResourceManager.h>
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cctype>
|
|
#include <cstdint>
|
|
#include <filesystem>
|
|
#include <imgui.h>
|
|
#include <shellapi.h>
|
|
#include <vector>
|
|
|
|
namespace XCEngine {
|
|
namespace Editor {
|
|
|
|
namespace {
|
|
|
|
constexpr float kProjectToolbarHeight = 26.0f;
|
|
constexpr float kProjectToolbarPaddingY = 3.0f;
|
|
|
|
std::uint64_t ResolveImportStatusElapsedMs(
|
|
const XCEngine::Resources::AssetImportService::ImportStatusSnapshot& status) {
|
|
if (!status.HasValue() || status.startedAtMs == 0) {
|
|
return 0;
|
|
}
|
|
|
|
if (!status.inProgress) {
|
|
return static_cast<std::uint64_t>(status.durationMs);
|
|
}
|
|
|
|
const auto nowMs = static_cast<std::uint64_t>(
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now().time_since_epoch()).count());
|
|
return nowMs >= status.startedAtMs
|
|
? nowMs - static_cast<std::uint64_t>(status.startedAtMs)
|
|
: 0;
|
|
}
|
|
|
|
ImVec4 ResolveImportStatusColor(const XCEngine::Resources::AssetImportService::ImportStatusSnapshot& status) {
|
|
if (!status.HasValue()) {
|
|
return XCEngine::Editor::UI::ConsoleSecondaryTextColor();
|
|
}
|
|
|
|
if (status.inProgress) {
|
|
return XCEngine::Editor::UI::ConsoleWarningColor();
|
|
}
|
|
|
|
return status.success
|
|
? XCEngine::Editor::UI::ConsoleSecondaryTextColor()
|
|
: XCEngine::Editor::UI::ConsoleErrorColor();
|
|
}
|
|
|
|
std::uint64_t ResolveSceneLoadElapsedMs(const XCEngine::Editor::SceneLoadProgressSnapshot& status) {
|
|
if (!status.HasValue() || status.startedAtMs == 0) {
|
|
return 0;
|
|
}
|
|
|
|
if (status.inProgress) {
|
|
const auto nowMs = static_cast<std::uint64_t>(
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now().time_since_epoch()).count());
|
|
return nowMs >= status.startedAtMs ? nowMs - status.startedAtMs : 0;
|
|
}
|
|
|
|
if (status.streamingCompletedAtMs >= status.startedAtMs && status.streamingCompletedAtMs != 0) {
|
|
return status.streamingCompletedAtMs - status.startedAtMs;
|
|
}
|
|
|
|
if (status.firstFrameAtMs >= status.startedAtMs && status.firstFrameAtMs != 0) {
|
|
return status.firstFrameAtMs - status.startedAtMs;
|
|
}
|
|
|
|
if (status.structureReadyAtMs >= status.startedAtMs && status.structureReadyAtMs != 0) {
|
|
return status.structureReadyAtMs - status.startedAtMs;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
std::uint64_t ResolveSceneLoadMarkerDeltaMs(std::uint64_t startMs, std::uint64_t endMs) {
|
|
return endMs >= startMs && startMs != 0 ? endMs - startMs : 0;
|
|
}
|
|
|
|
ImVec4 ResolveSceneLoadStatusColor(const XCEngine::Editor::SceneLoadProgressSnapshot& status) {
|
|
if (!status.HasValue()) {
|
|
return XCEngine::Editor::UI::ConsoleSecondaryTextColor();
|
|
}
|
|
|
|
if (status.inProgress) {
|
|
return XCEngine::Editor::UI::ConsoleWarningColor();
|
|
}
|
|
|
|
return status.success
|
|
? XCEngine::Editor::UI::ConsoleSecondaryTextColor()
|
|
: XCEngine::Editor::UI::ConsoleErrorColor();
|
|
}
|
|
|
|
std::string BuildImportStatusText(const XCEngine::Resources::AssetImportService::ImportStatusSnapshot& status) {
|
|
if (!status.HasValue()) {
|
|
return "Library: ready";
|
|
}
|
|
|
|
std::string text = "Library: ";
|
|
text += status.message.CStr();
|
|
const std::uint64_t elapsedMs = ResolveImportStatusElapsedMs(status);
|
|
if (elapsedMs > 0) {
|
|
text += " (";
|
|
text += std::to_string(elapsedMs);
|
|
text += " ms)";
|
|
}
|
|
return text;
|
|
}
|
|
|
|
std::string BuildSceneLoadStatusText(const XCEngine::Editor::SceneLoadProgressSnapshot& status) {
|
|
if (!status.HasValue()) {
|
|
return "Scene: ready";
|
|
}
|
|
|
|
std::string text = "Scene: ";
|
|
if (!status.success) {
|
|
text += status.message.empty() ? "failed" : status.message;
|
|
return text;
|
|
}
|
|
|
|
if (status.inProgress) {
|
|
text += status.message.empty() ? "loading..." : status.message;
|
|
} else {
|
|
text += "ready";
|
|
}
|
|
|
|
const std::uint64_t elapsedMs = ResolveSceneLoadElapsedMs(status);
|
|
if (elapsedMs > 0) {
|
|
text += " (";
|
|
text += std::to_string(elapsedMs);
|
|
text += " ms)";
|
|
}
|
|
return text;
|
|
}
|
|
|
|
std::string BuildImportStatusTooltipText(const XCEngine::Resources::AssetImportService::ImportStatusSnapshot& status) {
|
|
if (!status.HasValue()) {
|
|
return "No import operations have been recorded in this session.";
|
|
}
|
|
|
|
std::string tooltip;
|
|
tooltip += "Operation: ";
|
|
tooltip += status.operation.CStr();
|
|
tooltip += "\nState: ";
|
|
tooltip += status.inProgress ? "Running" : (status.success ? "Succeeded" : "Failed");
|
|
const std::uint64_t elapsedMs = ResolveImportStatusElapsedMs(status);
|
|
if (elapsedMs > 0) {
|
|
tooltip += "\nDuration: ";
|
|
tooltip += std::to_string(elapsedMs);
|
|
tooltip += " ms";
|
|
}
|
|
if (!status.targetPath.Empty()) {
|
|
tooltip += "\nTarget: ";
|
|
tooltip += status.targetPath.CStr();
|
|
}
|
|
if (status.importedAssetCount > 0) {
|
|
tooltip += "\nImported: ";
|
|
tooltip += std::to_string(status.importedAssetCount);
|
|
}
|
|
if (status.removedArtifactCount > 0) {
|
|
tooltip += "\nRemoved orphans: ";
|
|
tooltip += std::to_string(status.removedArtifactCount);
|
|
}
|
|
return tooltip;
|
|
}
|
|
|
|
std::string BuildSceneLoadStatusTooltipText(const XCEngine::Editor::SceneLoadProgressSnapshot& status) {
|
|
if (!status.HasValue()) {
|
|
return "No scene load operations have been recorded in this session.";
|
|
}
|
|
|
|
std::string tooltip;
|
|
tooltip += "Operation: ";
|
|
tooltip += status.operation.empty() ? "Scene Load" : status.operation;
|
|
if (!status.scenePath.empty()) {
|
|
tooltip += "\nScene: ";
|
|
tooltip += status.scenePath;
|
|
}
|
|
tooltip += "\nState: ";
|
|
tooltip += status.inProgress ? "Running" : (status.success ? "Succeeded" : "Failed");
|
|
|
|
const std::uint64_t structureMs =
|
|
ResolveSceneLoadMarkerDeltaMs(status.startedAtMs, status.structureReadyAtMs);
|
|
if (structureMs > 0) {
|
|
tooltip += "\nStructure restore: ";
|
|
tooltip += std::to_string(structureMs);
|
|
tooltip += " ms";
|
|
}
|
|
|
|
const std::uint64_t firstFrameMs =
|
|
ResolveSceneLoadMarkerDeltaMs(status.startedAtMs, status.firstFrameAtMs);
|
|
if (firstFrameMs > 0) {
|
|
tooltip += "\nFirst viewport frame: ";
|
|
tooltip += std::to_string(firstFrameMs);
|
|
tooltip += " ms";
|
|
}
|
|
|
|
const std::uint64_t interactiveMs =
|
|
ResolveSceneLoadMarkerDeltaMs(status.startedAtMs, status.interactiveAtMs);
|
|
if (interactiveMs > 0) {
|
|
tooltip += "\nFirst interactive frame: ";
|
|
tooltip += std::to_string(interactiveMs);
|
|
tooltip += " ms";
|
|
}
|
|
|
|
const std::uint64_t streamingMs =
|
|
ResolveSceneLoadMarkerDeltaMs(status.startedAtMs, status.streamingCompletedAtMs);
|
|
if (streamingMs > 0) {
|
|
tooltip += "\nRuntime streaming complete: ";
|
|
tooltip += std::to_string(streamingMs);
|
|
tooltip += " ms";
|
|
}
|
|
|
|
if (status.peakPendingAsyncLoads > 0) {
|
|
tooltip += "\nPeak async loads: ";
|
|
tooltip += std::to_string(status.peakPendingAsyncLoads);
|
|
}
|
|
if (status.inProgress && status.currentPendingAsyncLoads > 0) {
|
|
tooltip += "\nPending async loads: ";
|
|
tooltip += std::to_string(status.currentPendingAsyncLoads);
|
|
}
|
|
if (!status.message.empty()) {
|
|
tooltip += "\nStatus: ";
|
|
tooltip += status.message;
|
|
}
|
|
return tooltip;
|
|
}
|
|
|
|
template <typename Fn>
|
|
void QueueDeferredAction(std::function<void()>& pendingAction, Fn&& fn) {
|
|
if (!pendingAction) {
|
|
pendingAction = std::forward<Fn>(fn);
|
|
}
|
|
}
|
|
|
|
void DrawProjectFolderTreePrefix(const UI::TreeNodePrefixContext& context) {
|
|
if (!context.drawList) {
|
|
return;
|
|
}
|
|
|
|
const float width = context.max.x - context.min.x;
|
|
const float height = context.max.y - context.min.y;
|
|
const float iconExtent = UI::NavigationTreeIconSize();
|
|
const float minX = context.min.x + (width - iconExtent) * 0.5f;
|
|
const float minY = context.min.y + (height - iconExtent) * 0.5f;
|
|
UI::DrawAssetIcon(
|
|
context.drawList,
|
|
ImVec2(minX, minY),
|
|
ImVec2(minX + iconExtent, minY + iconExtent),
|
|
UI::AssetIconKind::Folder);
|
|
}
|
|
|
|
UI::AssetIconKind ResolveProjectAssetIconKind(const AssetItemPtr& item) {
|
|
if (!item) {
|
|
return UI::AssetIconKind::File;
|
|
}
|
|
|
|
if (item->isFolder) {
|
|
return UI::AssetIconKind::Folder;
|
|
}
|
|
|
|
if (item->type == "Scene") {
|
|
return UI::AssetIconKind::Scene;
|
|
}
|
|
|
|
return UI::AssetIconKind::File;
|
|
}
|
|
|
|
std::string GetProjectAssetDisplayName(const AssetItemPtr& item) {
|
|
if (!item) {
|
|
return {};
|
|
}
|
|
|
|
if (item->isFolder) {
|
|
return item->name;
|
|
}
|
|
|
|
const size_t extensionPos = item->name.find_last_of('.');
|
|
if (extensionPos == std::string::npos || extensionPos == 0) {
|
|
return item->name;
|
|
}
|
|
|
|
return item->name.substr(0, extensionPos);
|
|
}
|
|
|
|
UI::AssetTileOptions MakeProjectAssetTileOptions() {
|
|
UI::AssetTileOptions options;
|
|
options.drawIdleFrame = false;
|
|
options.drawSelectionBorder = false;
|
|
options.iconOffset = UI::ProjectAssetTileIconOffset();
|
|
options.iconSize = UI::ProjectAssetTileIconSize();
|
|
|
|
return options;
|
|
}
|
|
|
|
std::string BuildProjectRelativeAssetPath(const std::string& projectPath, const std::string& fullPath) {
|
|
if (projectPath.empty() || fullPath.empty()) {
|
|
return {};
|
|
}
|
|
|
|
return ProjectFileUtils::MakeProjectRelativePath(projectPath, fullPath);
|
|
}
|
|
|
|
bool ShowPathInExplorer(const std::string& fullPath, bool selectTarget) {
|
|
if (fullPath.empty()) {
|
|
return false;
|
|
}
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
std::error_code ec;
|
|
const fs::path targetPath = fs::path(Platform::Utf8ToWide(fullPath)).lexically_normal();
|
|
if (targetPath.empty() || !fs::exists(targetPath, ec)) {
|
|
return false;
|
|
}
|
|
|
|
HINSTANCE result = nullptr;
|
|
if (selectTarget) {
|
|
const std::wstring parameters = L"/select,\"" + targetPath.native() + L"\"";
|
|
const std::wstring workingDirectory = targetPath.parent_path().native();
|
|
result = ShellExecuteW(
|
|
nullptr,
|
|
L"open",
|
|
L"explorer.exe",
|
|
parameters.c_str(),
|
|
workingDirectory.empty() ? nullptr : workingDirectory.c_str(),
|
|
SW_SHOWNORMAL);
|
|
} else {
|
|
const std::wstring workingDirectory = targetPath.parent_path().native();
|
|
result = ShellExecuteW(
|
|
nullptr,
|
|
L"open",
|
|
targetPath.c_str(),
|
|
nullptr,
|
|
workingDirectory.empty() ? nullptr : workingDirectory.c_str(),
|
|
SW_SHOWNORMAL);
|
|
}
|
|
|
|
return reinterpret_cast<INT_PTR>(result) > 32;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
ProjectPanel::ProjectPanel() : Panel("Project") {
|
|
}
|
|
|
|
void ProjectPanel::Initialize(const std::string& projectPath) {
|
|
m_context->GetProjectManager().Initialize(projectPath);
|
|
}
|
|
|
|
void ProjectPanel::BeginAssetDragDropFrame() {
|
|
m_assetDragDropState.Reset();
|
|
if (const char* draggedPath = Actions::GetDraggedProjectAssetPath()) {
|
|
m_assetDragDropState.dragging = true;
|
|
m_assetDragDropState.sourcePath = draggedPath;
|
|
}
|
|
}
|
|
|
|
void ProjectPanel::RegisterFolderDropTarget(IProjectManager& manager, const AssetItemPtr& folder) {
|
|
if (!m_assetDragDropState.dragging || !folder || !folder->isFolder || !ImGui::BeginDragDropTarget()) {
|
|
return;
|
|
}
|
|
|
|
const ImGuiPayload* payload = ImGui::GetDragDropPayload();
|
|
if (!payload || !payload->IsDataType(Actions::ProjectAssetPayloadType())) {
|
|
ImGui::EndDragDropTarget();
|
|
return;
|
|
}
|
|
|
|
m_assetDragDropState.hoveredTarget = true;
|
|
const bool canDrop = Commands::CanMoveAssetToFolder(manager, m_assetDragDropState.sourcePath, folder);
|
|
if (canDrop) {
|
|
m_assetDragDropState.hoveredValidTarget = true;
|
|
if (const ImGuiPayload* accepted = ImGui::AcceptDragDropPayload(
|
|
Actions::ProjectAssetPayloadType(),
|
|
ImGuiDragDropFlags_AcceptNoDrawDefaultRect))
|
|
{
|
|
if (accepted->Delivery) {
|
|
m_assetDragDropState.deliveredSourcePath = m_assetDragDropState.sourcePath;
|
|
m_assetDragDropState.deliveredTarget = folder;
|
|
}
|
|
}
|
|
}
|
|
|
|
ImGui::EndDragDropTarget();
|
|
}
|
|
|
|
void ProjectPanel::FinalizeAssetDragDrop(IProjectManager& manager) {
|
|
if (m_assetDragDropState.dragging) {
|
|
ImGui::SetMouseCursor(
|
|
m_assetDragDropState.hoveredValidTarget ? ImGuiMouseCursor_Arrow : ImGuiMouseCursor_NotAllowed);
|
|
}
|
|
|
|
if (!m_assetDragDropState.deliveredSourcePath.empty() && m_assetDragDropState.deliveredTarget) {
|
|
Commands::MoveAssetToFolder(
|
|
manager,
|
|
m_assetDragDropState.deliveredSourcePath,
|
|
m_assetDragDropState.deliveredTarget);
|
|
}
|
|
}
|
|
|
|
void ProjectPanel::BeginRename(const AssetItemPtr& item) {
|
|
if (!item) {
|
|
CancelRename();
|
|
return;
|
|
}
|
|
|
|
m_renameState.Begin(item->fullPath, GetProjectAssetDisplayName(item).c_str());
|
|
}
|
|
|
|
bool ProjectPanel::CommitRename(IProjectManager& manager) {
|
|
if (!m_renameState.IsActive()) {
|
|
return false;
|
|
}
|
|
|
|
const std::string sourcePath = m_renameState.Item();
|
|
std::string currentDisplayName;
|
|
for (const auto& item : manager.GetCurrentItems()) {
|
|
if (item && item->fullPath == sourcePath) {
|
|
currentDisplayName = GetProjectAssetDisplayName(item);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!currentDisplayName.empty() && currentDisplayName == m_renameState.Buffer()) {
|
|
CancelRename();
|
|
return true;
|
|
}
|
|
|
|
if (!Commands::RenameAsset(manager, sourcePath, m_renameState.Buffer())) {
|
|
return false;
|
|
}
|
|
|
|
CancelRename();
|
|
return true;
|
|
}
|
|
|
|
void ProjectPanel::CancelRename() {
|
|
m_renameState.Cancel();
|
|
}
|
|
|
|
ProjectPanel::ContextMenuTarget ProjectPanel::BuildContextMenuTarget(
|
|
IProjectManager& manager,
|
|
const AssetItemPtr& item) const {
|
|
ContextMenuTarget target;
|
|
target.item = item;
|
|
if (item) {
|
|
target.subjectPath = item->fullPath;
|
|
target.createFolderPath = item->isFolder ? item->fullPath : std::string();
|
|
target.showInExplorerSelect = true;
|
|
return target;
|
|
}
|
|
|
|
if (const AssetItemPtr currentFolder = manager.GetCurrentFolder()) {
|
|
target.subjectPath = currentFolder->fullPath;
|
|
target.createFolderPath = currentFolder->fullPath;
|
|
}
|
|
|
|
return target;
|
|
}
|
|
|
|
void ProjectPanel::DrawProjectContextMenu(IProjectManager& manager, const ContextMenuTarget& target) {
|
|
auto* managerPtr = &manager;
|
|
const bool canCreate = !target.createFolderPath.empty();
|
|
const bool canShowInExplorer = !target.subjectPath.empty();
|
|
const bool canOpen = target.item != nullptr && Commands::CanOpenAsset(target.item);
|
|
const bool canInstantiateModel =
|
|
target.item != nullptr &&
|
|
m_context != nullptr &&
|
|
Commands::CanInstantiateModelAsset(*m_context, target.item);
|
|
const bool canDelete = target.item != nullptr;
|
|
const bool canRename = target.item != nullptr;
|
|
const std::string copyPath = BuildProjectRelativeAssetPath(
|
|
m_context ? m_context->GetProjectPath() : std::string(),
|
|
target.subjectPath);
|
|
const bool canCopyPath = !copyPath.empty();
|
|
|
|
const auto queueCreateAsset = [this, managerPtr, target](auto createFn) {
|
|
QueueDeferredAction(m_deferredContextAction, [this, managerPtr, target, createFn]() {
|
|
if (!target.createFolderPath.empty() && target.item && target.item->isFolder) {
|
|
managerPtr->NavigateToFolder(target.item);
|
|
}
|
|
|
|
if (AssetItemPtr createdItem = createFn(*managerPtr)) {
|
|
BeginRename(createdItem);
|
|
}
|
|
});
|
|
};
|
|
|
|
UI::DrawContextSubmenu("Create", [&]() {
|
|
Actions::DrawMenuAction(Actions::MakeAction("Folder", nullptr, false, canCreate), [&]() {
|
|
queueCreateAsset([](IProjectManager& createManager) {
|
|
return Commands::CreateFolder(createManager, "New Folder");
|
|
});
|
|
});
|
|
|
|
Actions::DrawMenuAction(Actions::MakeAction("Material", nullptr, false, canCreate), [&]() {
|
|
queueCreateAsset([](IProjectManager& createManager) {
|
|
return Commands::CreateMaterial(createManager, "New Material");
|
|
});
|
|
});
|
|
}, canCreate);
|
|
|
|
Actions::DrawMenuAction(Actions::MakeAction("Show in Explore", nullptr, false, canShowInExplorer), [&]() {
|
|
QueueDeferredAction(m_deferredContextAction, [target]() {
|
|
ShowPathInExplorer(target.subjectPath, target.showInExplorerSelect);
|
|
});
|
|
});
|
|
|
|
Actions::DrawMenuAction(Actions::MakeAction("Instantiate In Scene", nullptr, false, canInstantiateModel), [&]() {
|
|
QueueDeferredAction(m_deferredContextAction, [this, target]() {
|
|
Commands::InstantiateModelAsset(*m_context, target.item);
|
|
});
|
|
});
|
|
|
|
Actions::DrawMenuAction(Actions::MakeOpenAssetAction(canOpen), [&]() {
|
|
QueueDeferredAction(m_deferredContextAction, [this, target]() {
|
|
Actions::OpenProjectAsset(*m_context, target.item);
|
|
});
|
|
});
|
|
|
|
Actions::DrawMenuAction(Actions::MakeDeleteAssetAction(canDelete), [&]() {
|
|
QueueDeferredAction(m_deferredContextAction, [this, target]() {
|
|
Commands::DeleteAsset(m_context->GetProjectManager(), target.item);
|
|
});
|
|
});
|
|
|
|
Actions::DrawMenuAction(Actions::MakeAction("Rename", nullptr, false, canRename), [&]() {
|
|
QueueDeferredAction(m_deferredContextAction, [this, target]() {
|
|
BeginRename(target.item);
|
|
});
|
|
});
|
|
|
|
Actions::DrawMenuAction(Actions::MakeAction("Copy Path", nullptr, false, canCopyPath), [copyPath]() {
|
|
ImGui::SetClipboardText(copyPath.c_str());
|
|
});
|
|
}
|
|
|
|
void ProjectPanel::Render() {
|
|
UI::PanelWindowScope panel(m_name.c_str());
|
|
if (!panel.IsOpen()) {
|
|
return;
|
|
}
|
|
|
|
Actions::ObserveFocusedActionRoute(*m_context, EditorActionRoute::Project);
|
|
|
|
auto& manager = m_context->GetProjectManager();
|
|
BeginAssetDragDropFrame();
|
|
m_deferredContextAction = {};
|
|
RenderToolbar();
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserSurfaceColor());
|
|
UI::PanelContentScope content("ProjectContent", ImVec2(0.0f, 0.0f));
|
|
if (!content.IsOpen()) {
|
|
ImGui::PopStyleColor();
|
|
return;
|
|
}
|
|
|
|
const float totalHeight = ImGui::GetContentRegionAvail().y;
|
|
const float splitterWidth = UI::PanelSplitterHitThickness();
|
|
const float availableWidth = ImGui::GetContentRegionAvail().x;
|
|
const float clampedNavigationWidth = std::clamp(
|
|
m_navigationWidth,
|
|
UI::ProjectNavigationMinWidth(),
|
|
(std::max)(UI::ProjectNavigationMinWidth(), availableWidth - UI::ProjectBrowserMinWidth() - splitterWidth));
|
|
m_navigationWidth = clampedNavigationWidth;
|
|
|
|
RenderFolderTreePane(manager);
|
|
ImGui::SameLine(0.0f, 0.0f);
|
|
const UI::SplitterResult splitter = UI::DrawSplitter("##ProjectPaneSplitter", UI::SplitterAxis::Vertical, totalHeight);
|
|
if (splitter.active) {
|
|
m_navigationWidth += splitter.delta;
|
|
}
|
|
ImGui::SameLine(0.0f, 0.0f);
|
|
RenderBrowserPane(manager);
|
|
|
|
FinalizeAssetDragDrop(manager);
|
|
ImGui::PopStyleColor();
|
|
|
|
if (m_deferredContextAction) {
|
|
auto deferredAction = std::move(m_deferredContextAction);
|
|
m_deferredContextAction = {};
|
|
deferredAction();
|
|
}
|
|
}
|
|
|
|
void ProjectPanel::RenderToolbar() {
|
|
const auto importStatus = ::XCEngine::Resources::ResourceManager::Get().GetProjectAssetImportStatus();
|
|
const SceneLoadProgressSnapshot sceneLoadStatus =
|
|
m_context != nullptr ? m_context->GetSceneManager().GetSceneLoadProgress() : SceneLoadProgressSnapshot{};
|
|
const std::string importStatusText = BuildImportStatusText(importStatus);
|
|
const std::string importStatusTooltip = BuildImportStatusTooltipText(importStatus);
|
|
const std::string sceneLoadStatusText = BuildSceneLoadStatusText(sceneLoadStatus);
|
|
const std::string sceneLoadStatusTooltip = BuildSceneLoadStatusTooltipText(sceneLoadStatus);
|
|
|
|
UI::PanelToolbarScope toolbar(
|
|
"ProjectToolbar",
|
|
kProjectToolbarHeight,
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse,
|
|
true,
|
|
ImVec2(UI::ToolbarPadding().x, kProjectToolbarPaddingY),
|
|
UI::ToolbarItemSpacing(),
|
|
UI::ProjectPanelToolbarBackgroundColor());
|
|
if (!toolbar.IsOpen()) {
|
|
return;
|
|
}
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0.0f, 0.0f));
|
|
if (ImGui::BeginTable("##ProjectToolbarLayout", 3, ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_SizingStretchProp)) {
|
|
ImGui::TableSetupColumn("##ImportStatus", ImGuiTableColumnFlags_WidthStretch);
|
|
ImGui::TableSetupColumn("##SceneLoadStatus", ImGuiTableColumnFlags_WidthStretch);
|
|
ImGui::TableSetupColumn("##Search", ImGuiTableColumnFlags_WidthFixed, 220.0f);
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
ImGui::TableNextColumn();
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ResolveImportStatusColor(importStatus));
|
|
ImGui::TextUnformatted(importStatusText.c_str());
|
|
ImGui::PopStyleColor();
|
|
if (ImGui::IsItemHovered() && !importStatusTooltip.empty()) {
|
|
ImGui::BeginTooltip();
|
|
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 36.0f);
|
|
ImGui::TextUnformatted(importStatusTooltip.c_str());
|
|
ImGui::PopTextWrapPos();
|
|
ImGui::EndTooltip();
|
|
}
|
|
|
|
ImGui::TableNextColumn();
|
|
ImGui::AlignTextToFramePadding();
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ResolveSceneLoadStatusColor(sceneLoadStatus));
|
|
ImGui::TextUnformatted(sceneLoadStatusText.c_str());
|
|
ImGui::PopStyleColor();
|
|
if (ImGui::IsItemHovered() && !sceneLoadStatusTooltip.empty()) {
|
|
ImGui::BeginTooltip();
|
|
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 36.0f);
|
|
ImGui::TextUnformatted(sceneLoadStatusTooltip.c_str());
|
|
ImGui::PopTextWrapPos();
|
|
ImGui::EndTooltip();
|
|
}
|
|
|
|
ImGui::TableNextColumn();
|
|
UI::ToolbarSearchField("##Search", "Search assets", m_searchBuffer, sizeof(m_searchBuffer));
|
|
|
|
ImGui::EndTable();
|
|
}
|
|
ImGui::PopStyleVar();
|
|
}
|
|
|
|
void ProjectPanel::RenderFolderTreePane(IProjectManager& manager) {
|
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectNavigationPaneBackgroundColor());
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, UI::ProjectNavigationPanePadding());
|
|
const bool open = ImGui::BeginChild("ProjectFolderTree", ImVec2(m_navigationWidth, 0.0f), false);
|
|
ImGui::PopStyleVar();
|
|
ImGui::PopStyleColor();
|
|
if (!open) {
|
|
ImGui::EndChild();
|
|
return;
|
|
}
|
|
|
|
const AssetItemPtr rootFolder = manager.GetRootFolder();
|
|
const AssetItemPtr currentFolder = manager.GetCurrentFolder();
|
|
const std::string currentFolderPath = currentFolder ? currentFolder->fullPath : std::string();
|
|
UI::ResetTreeLayout();
|
|
if (rootFolder) {
|
|
RenderFolderTreeNode(manager, rootFolder, currentFolderPath);
|
|
} else {
|
|
UI::DrawEmptyState("No Assets Folder");
|
|
}
|
|
|
|
if (UI::BeginContextMenuForWindow("##ProjectFolderTreeContext")) {
|
|
DrawProjectContextMenu(manager, BuildContextMenuTarget(manager, nullptr));
|
|
UI::EndContextMenu();
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
}
|
|
|
|
void ProjectPanel::RenderFolderTreeNode(
|
|
IProjectManager& manager,
|
|
const AssetItemPtr& folder,
|
|
const std::string& currentFolderPath) {
|
|
if (!folder || !folder->isFolder) {
|
|
return;
|
|
}
|
|
|
|
bool hasChildFolders = false;
|
|
for (const auto& child : folder->children) {
|
|
if (child && child->isFolder) {
|
|
hasChildFolders = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
UI::TreeNodeDefinition nodeDefinition;
|
|
nodeDefinition.options.selected = folder->fullPath == currentFolderPath;
|
|
nodeDefinition.options.leaf = !hasChildFolders;
|
|
nodeDefinition.options.defaultOpen = IsCurrentTreeBranch(currentFolderPath, folder->fullPath);
|
|
nodeDefinition.persistenceKey = folder->fullPath;
|
|
nodeDefinition.style = UI::ProjectFolderTreeStyle();
|
|
nodeDefinition.prefix.width = UI::NavigationTreePrefixWidth();
|
|
nodeDefinition.prefix.draw = DrawProjectFolderTreePrefix;
|
|
nodeDefinition.callbacks.onInteraction = [this, &manager, folder](const UI::TreeNodeResult& node) {
|
|
if (node.clicked) {
|
|
manager.NavigateToFolder(folder);
|
|
}
|
|
|
|
};
|
|
|
|
const UI::TreeNodeResult node = UI::DrawTreeNode(
|
|
&m_folderTreeState,
|
|
(void*)folder.get(),
|
|
folder->name.c_str(),
|
|
nodeDefinition);
|
|
|
|
RegisterFolderDropTarget(manager, folder);
|
|
|
|
if (node.open) {
|
|
for (const auto& child : folder->children) {
|
|
if (!child || !child->isFolder) {
|
|
continue;
|
|
}
|
|
RenderFolderTreeNode(manager, child, currentFolderPath);
|
|
}
|
|
UI::EndTreeNode();
|
|
}
|
|
}
|
|
|
|
void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
|
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserPaneBackgroundColor());
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
|
const bool open = ImGui::BeginChild("ProjectBrowser", ImVec2(0.0f, 0.0f), false);
|
|
ImGui::PopStyleVar();
|
|
ImGui::PopStyleColor();
|
|
if (!open) {
|
|
ImGui::EndChild();
|
|
return;
|
|
}
|
|
|
|
std::vector<AssetItemPtr> visibleItems;
|
|
const auto& items = manager.GetCurrentItems();
|
|
const UI::SearchQuery searchQuery(m_searchBuffer);
|
|
if (m_renameState.IsActive() && manager.FindCurrentItemIndex(m_renameState.Item()) < 0) {
|
|
CancelRename();
|
|
}
|
|
visibleItems.reserve(items.size());
|
|
for (const auto& item : items) {
|
|
if ((m_renameState.IsActive() && item && item->fullPath == m_renameState.Item()) || MatchesSearch(item, searchQuery)) {
|
|
visibleItems.push_back(item);
|
|
}
|
|
}
|
|
|
|
RenderBrowserHeader(manager);
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserPaneBackgroundColor());
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, UI::ProjectBrowserPanePadding());
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, UI::AssetGridSpacing());
|
|
const bool bodyOpen = ImGui::BeginChild(
|
|
"ProjectBrowserBody",
|
|
ImVec2(0.0f, 0.0f),
|
|
ImGuiChildFlags_AlwaysUseWindowPadding);
|
|
ImGui::PopStyleVar(2);
|
|
ImGui::PopStyleColor();
|
|
if (!bodyOpen) {
|
|
ImGui::EndChild();
|
|
ImGui::EndChild();
|
|
return;
|
|
}
|
|
|
|
const float tileWidth = UI::AssetTileSize().x;
|
|
const float spacing = UI::AssetGridSpacing().x;
|
|
const float rowSpacing = UI::AssetGridSpacing().y;
|
|
const float panelWidth = ImGui::GetContentRegionAvail().x;
|
|
int columns = static_cast<int>((panelWidth + spacing) / (tileWidth + spacing));
|
|
if (columns < 1) {
|
|
columns = 1;
|
|
}
|
|
|
|
const int rowCount = visibleItems.empty() ? 0 : (static_cast<int>(visibleItems.size()) + columns - 1) / columns;
|
|
std::vector<float> rowHeights(static_cast<size_t>(rowCount), UI::AssetTileSize().y);
|
|
|
|
AssetItemPtr pendingSelection;
|
|
AssetItemPtr pendingOpenTarget;
|
|
const std::string selectedItemPath = manager.GetSelectedItemPath();
|
|
const ImVec2 gridOrigin = ImGui::GetCursorPos();
|
|
for (int visibleIndex = 0; visibleIndex < static_cast<int>(visibleItems.size()); ++visibleIndex) {
|
|
const AssetItemPtr& item = visibleItems[visibleIndex];
|
|
const bool isRenaming = item && m_renameState.IsEditing(item->fullPath);
|
|
UI::AssetTileOptions tileOptions = MakeProjectAssetTileOptions();
|
|
tileOptions.drawLabel = !isRenaming;
|
|
const ImVec2 tileSize = UI::ComputeAssetTileSize(GetProjectAssetDisplayName(item).c_str(), tileOptions);
|
|
const int row = visibleIndex / columns;
|
|
rowHeights[static_cast<size_t>(row)] = (std::max)(rowHeights[static_cast<size_t>(row)], tileSize.y);
|
|
}
|
|
|
|
std::vector<float> rowOffsets(static_cast<size_t>(rowCount), gridOrigin.y);
|
|
float nextRowY = gridOrigin.y;
|
|
for (int row = 0; row < rowCount; ++row) {
|
|
rowOffsets[static_cast<size_t>(row)] = nextRowY;
|
|
nextRowY += rowHeights[static_cast<size_t>(row)] + rowSpacing;
|
|
}
|
|
|
|
int renderedItemCount = 0;
|
|
for (int visibleIndex = 0; visibleIndex < static_cast<int>(visibleItems.size()); ++visibleIndex) {
|
|
const int column = visibleIndex % columns;
|
|
const int row = visibleIndex / columns;
|
|
ImGui::SetCursorPos(ImVec2(
|
|
gridOrigin.x + column * (tileWidth + spacing),
|
|
rowOffsets[static_cast<size_t>(row)]));
|
|
|
|
const AssetItemPtr& item = visibleItems[visibleIndex];
|
|
const AssetItemInteraction interaction = RenderAssetItem(item, selectedItemPath == item->fullPath);
|
|
++renderedItemCount;
|
|
if (interaction.clicked) {
|
|
pendingSelection = item;
|
|
}
|
|
if (interaction.openRequested) {
|
|
pendingOpenTarget = item;
|
|
break;
|
|
}
|
|
if (m_deferredContextAction) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (renderedItemCount > 0) {
|
|
const int renderedRowCount = (renderedItemCount + columns - 1) / columns;
|
|
float contentBottom = gridOrigin.y;
|
|
for (int row = 0; row < renderedRowCount; ++row) {
|
|
contentBottom = rowOffsets[static_cast<size_t>(row)] + rowHeights[static_cast<size_t>(row)];
|
|
}
|
|
ImGui::SetCursorPos(ImVec2(gridOrigin.x, contentBottom));
|
|
ImGui::Dummy(ImVec2(0.0f, 0.0f));
|
|
}
|
|
|
|
if (visibleItems.empty() && !searchQuery.Empty()) {
|
|
UI::DrawEmptyState(
|
|
"No Search Results",
|
|
"No assets match the current search");
|
|
}
|
|
|
|
if (!m_deferredContextAction) {
|
|
Actions::HandleProjectBackgroundPrimaryClick(manager, m_renameState);
|
|
if (pendingSelection) {
|
|
manager.SetSelectedItem(pendingSelection);
|
|
}
|
|
if (pendingOpenTarget) {
|
|
Actions::OpenProjectAsset(*m_context, pendingOpenTarget);
|
|
}
|
|
}
|
|
|
|
if (UI::BeginContextMenuForWindow("##ProjectBrowserContext")) {
|
|
DrawProjectContextMenu(manager, BuildContextMenuTarget(manager, nullptr));
|
|
UI::EndContextMenu();
|
|
}
|
|
|
|
ImGui::EndChild();
|
|
ImGui::EndChild();
|
|
}
|
|
|
|
void ProjectPanel::RenderBrowserHeader(IProjectManager& manager) {
|
|
auto* managerPtr = &manager;
|
|
ImGui::PushStyleColor(ImGuiCol_ChildBg, UI::ProjectBrowserHeaderBackgroundColor());
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(10.0f, 5.0f));
|
|
const bool open = ImGui::BeginChild(
|
|
"ProjectBrowserHeader",
|
|
ImVec2(0.0f, UI::ProjectBrowserHeaderHeight()),
|
|
false,
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
|
ImGui::PopStyleVar();
|
|
ImGui::PopStyleColor();
|
|
if (!open) {
|
|
ImGui::EndChild();
|
|
return;
|
|
}
|
|
|
|
const float rowHeight = UI::BreadcrumbItemHeight();
|
|
const float startY = ImGui::GetCursorPosY();
|
|
const float availableHeight = ImGui::GetContentRegionAvail().y;
|
|
if (availableHeight > rowHeight) {
|
|
ImGui::SetCursorPosY(startY + (availableHeight - rowHeight) * 0.5f - 1.0f);
|
|
}
|
|
|
|
UI::DrawToolbarBreadcrumbs(
|
|
"Assets",
|
|
manager.GetPathDepth(),
|
|
[&](size_t index) { return manager.GetPathName(index); },
|
|
[&](size_t index) {
|
|
QueueDeferredAction(m_deferredContextAction, [managerPtr, index]() {
|
|
managerPtr->NavigateToIndex(index);
|
|
});
|
|
});
|
|
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
const ImVec2 windowMin = ImGui::GetWindowPos();
|
|
const ImVec2 windowMax(windowMin.x + ImGui::GetWindowSize().x, windowMin.y + ImGui::GetWindowSize().y);
|
|
UI::DrawHorizontalDivider(drawList, windowMin.x, windowMax.x, windowMax.y - 0.5f);
|
|
|
|
ImGui::EndChild();
|
|
}
|
|
|
|
ProjectPanel::AssetItemInteraction ProjectPanel::RenderAssetItem(const AssetItemPtr& item, bool isSelected) {
|
|
AssetItemInteraction interaction;
|
|
|
|
ImGui::PushID(item ? item->fullPath.c_str() : "ProjectItem");
|
|
const bool isRenaming = item && m_renameState.IsEditing(item->fullPath);
|
|
const bool isDraggingThisItem = !isRenaming && Actions::IsProjectAssetBeingDragged(item);
|
|
const UI::AssetIconKind iconKind = ResolveProjectAssetIconKind(item);
|
|
const std::string displayName = GetProjectAssetDisplayName(item);
|
|
UI::AssetTileOptions tileOptions = MakeProjectAssetTileOptions();
|
|
tileOptions.drawLabel = !isRenaming;
|
|
|
|
const UI::AssetTileResult tile = UI::DrawAssetTile(
|
|
displayName.c_str(),
|
|
isSelected,
|
|
isDraggingThisItem,
|
|
[&](ImDrawList* drawList, const ImVec2& iconMin, const ImVec2& iconMax) {
|
|
if (item && item->canUseImagePreview &&
|
|
UI::DrawTextureAssetPreview(
|
|
drawList,
|
|
iconMin,
|
|
iconMax,
|
|
item->fullPath,
|
|
m_context ? m_context->GetProjectPath() : std::string())) {
|
|
return;
|
|
}
|
|
UI::DrawAssetIcon(drawList, iconMin, iconMax, iconKind);
|
|
},
|
|
tileOptions);
|
|
const bool secondaryClicked = !isRenaming && ImGui::IsItemClicked(ImGuiMouseButton_Right);
|
|
|
|
if (isRenaming) {
|
|
const float renameWidth = tile.labelMax.x - tile.labelMin.x;
|
|
const float renameOffsetY = (std::max)(0.0f, (tile.labelMax.y - tile.labelMin.y - UI::InlineRenameFieldHeight()) * 0.5f);
|
|
const UI::InlineRenameFieldResult renameField = UI::DrawInlineRenameFieldAt(
|
|
"##Rename",
|
|
ImVec2(tile.labelMin.x, tile.labelMin.y + renameOffsetY),
|
|
m_renameState.Buffer(),
|
|
m_renameState.BufferSize(),
|
|
renameWidth,
|
|
m_renameState.ConsumeFocusRequest());
|
|
|
|
if (renameField.cancelRequested) {
|
|
CancelRename();
|
|
} else if (renameField.submitted || renameField.deactivated) {
|
|
CommitRename(m_context->GetProjectManager());
|
|
}
|
|
} else {
|
|
if (tile.clicked) {
|
|
interaction.clicked = true;
|
|
}
|
|
if (secondaryClicked && item) {
|
|
m_context->GetProjectManager().SetSelectedItem(item);
|
|
}
|
|
|
|
RegisterFolderDropTarget(m_context->GetProjectManager(), item);
|
|
Actions::BeginProjectAssetDrag(item, iconKind);
|
|
|
|
if (UI::BeginContextMenuForLastItem("##ProjectItemContext")) {
|
|
DrawProjectContextMenu(m_context->GetProjectManager(), BuildContextMenuTarget(m_context->GetProjectManager(), item));
|
|
UI::EndContextMenu();
|
|
}
|
|
|
|
if (tile.openRequested) {
|
|
interaction.openRequested = true;
|
|
}
|
|
}
|
|
|
|
ImGui::PopID();
|
|
return interaction;
|
|
}
|
|
|
|
bool ProjectPanel::MatchesSearch(const AssetItemPtr& item, const UI::SearchQuery& searchQuery) {
|
|
if (!item) {
|
|
return false;
|
|
}
|
|
if (searchQuery.Empty()) {
|
|
return true;
|
|
}
|
|
|
|
return searchQuery.Matches(item->name);
|
|
}
|
|
|
|
bool ProjectPanel::IsCurrentTreeBranch(const std::string& currentFolderPath, const std::string& folderPath) {
|
|
if (currentFolderPath.empty() || folderPath.empty()) {
|
|
return false;
|
|
}
|
|
if (currentFolderPath == folderPath) {
|
|
return true;
|
|
}
|
|
|
|
const std::string withForwardSlash = folderPath + "/";
|
|
const std::string withBackwardSlash = folderPath + "\\";
|
|
return currentFolderPath.rfind(withForwardSlash, 0) == 0 || currentFolderPath.rfind(withBackwardSlash, 0) == 0;
|
|
}
|
|
|
|
}
|
|
}
|