feat: expand editor scripting asset and viewport flow

This commit is contained in:
2026-04-03 13:22:30 +08:00
parent ed8c27fde2
commit a05d0b80a2
124 changed files with 10397 additions and 1737 deletions

View File

@@ -5,11 +5,16 @@
#include "Core/IEditorContext.h"
#include "Core/IProjectManager.h"
#include "Core/AssetItem.h"
#include "Platform/Win32Utf8.h"
#include "Utils/ProjectFileUtils.h"
#include "UI/UI.h"
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <imgui.h>
#include <shellapi.h>
#include <vector>
namespace XCEngine {
namespace Editor {
@@ -86,6 +91,52 @@ UI::AssetTileOptions MakeProjectAssetTileOptions() {
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") {
@@ -186,6 +237,93 @@ 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 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::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()) {
@@ -265,7 +403,6 @@ void ProjectPanel::RenderToolbar() {
}
void ProjectPanel::RenderFolderTreePane(IProjectManager& manager) {
auto* managerPtr = &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);
@@ -287,13 +424,7 @@ void ProjectPanel::RenderFolderTreePane(IProjectManager& manager) {
}
if (UI::BeginContextMenuForWindow("##ProjectFolderTreeContext")) {
Actions::DrawMenuAction(Actions::MakeCreateFolderAction(), [&]() {
QueueDeferredAction(m_deferredContextAction, [this, managerPtr]() {
if (AssetItemPtr createdFolder = Commands::CreateFolder(*managerPtr, "New Folder")) {
BeginRename(createdFolder);
}
});
});
DrawProjectContextMenu(manager, BuildContextMenuTarget(manager, nullptr));
UI::EndContextMenu();
}
@@ -351,7 +482,6 @@ void ProjectPanel::RenderFolderTreeNode(
}
void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
auto* managerPtr = &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);
@@ -393,7 +523,6 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
}
const float tileWidth = UI::AssetTileSize().x;
const float tileHeight = UI::AssetTileSize().y;
const float spacing = UI::AssetGridSpacing().x;
const float rowSpacing = UI::AssetGridSpacing().y;
const float panelWidth = ImGui::GetContentRegionAvail().x;
@@ -402,19 +531,41 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
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),
gridOrigin.y + row * (tileHeight + rowSpacing)));
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;
}
@@ -427,9 +578,14 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
}
}
if (!visibleItems.empty()) {
const int rowCount = (static_cast<int>(visibleItems.size()) + columns - 1) / columns;
ImGui::SetCursorPosY(gridOrigin.y + rowCount * tileHeight + (rowCount - 1) * rowSpacing);
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()) {
@@ -449,21 +605,7 @@ void ProjectPanel::RenderBrowserPane(IProjectManager& manager) {
}
if (UI::BeginContextMenuForWindow("##ProjectBrowserContext")) {
Actions::DrawMenuAction(Actions::MakeCreateFolderAction(), [&]() {
QueueDeferredAction(m_deferredContextAction, [this, managerPtr]() {
if (AssetItemPtr createdFolder = Commands::CreateFolder(*managerPtr, "New Folder")) {
BeginRename(createdFolder);
}
});
});
if (manager.CanNavigateBack()) {
Actions::DrawMenuSeparator();
Actions::DrawMenuAction(Actions::MakeNavigateBackAction(true), [&]() {
QueueDeferredAction(m_deferredContextAction, [managerPtr]() {
managerPtr->NavigateBack();
});
});
}
DrawProjectContextMenu(manager, BuildContextMenuTarget(manager, nullptr));
UI::EndContextMenu();
}
@@ -570,21 +712,7 @@ ProjectPanel::AssetItemInteraction ProjectPanel::RenderAssetItem(const AssetItem
Actions::BeginProjectAssetDrag(item, iconKind);
if (UI::BeginContextMenuForLastItem("##ProjectItemContext")) {
Actions::DrawMenuAction(Actions::MakeOpenAssetAction(Commands::CanOpenAsset(item)), [&]() {
QueueDeferredAction(m_deferredContextAction, [this, item]() {
Actions::OpenProjectAsset(*m_context, item);
});
});
Actions::DrawMenuAction(Actions::MakeAction("Rename", nullptr, false, item != nullptr), [&]() {
QueueDeferredAction(m_deferredContextAction, [this, item]() {
BeginRename(item);
});
});
Actions::DrawMenuAction(Actions::MakeDeleteAssetAction(item != nullptr), [&]() {
QueueDeferredAction(m_deferredContextAction, [this, item]() {
Commands::DeleteAsset(m_context->GetProjectManager(), item);
});
});
DrawProjectContextMenu(m_context->GetProjectManager(), BuildContextMenuTarget(m_context->GetProjectManager(), item));
UI::EndContextMenu();
}