Advance new editor hosted panels and state flow
This commit is contained in:
@@ -150,11 +150,16 @@ if(XCENGINE_BUILD_XCUI_EDITOR_APP)
|
|||||||
app/Commands/ProductEditorHostCommandBridge.cpp
|
app/Commands/ProductEditorHostCommandBridge.cpp
|
||||||
app/Core/ProductEditorContext.cpp
|
app/Core/ProductEditorContext.cpp
|
||||||
app/Core/ProductEditorSession.cpp
|
app/Core/ProductEditorSession.cpp
|
||||||
|
app/Panels/ProductConsolePanel.cpp
|
||||||
|
app/Hierarchy/ProductHierarchyModel.cpp
|
||||||
app/Icons/ProductBuiltInIcons.cpp
|
app/Icons/ProductBuiltInIcons.cpp
|
||||||
app/Panels/ProductHierarchyPanel.cpp
|
app/Panels/ProductHierarchyPanel.cpp
|
||||||
|
app/Panels/ProductInspectorPanel.cpp
|
||||||
app/Panels/ProductProjectPanel.cpp
|
app/Panels/ProductProjectPanel.cpp
|
||||||
|
app/Project/ProductProjectBrowserModel.cpp
|
||||||
app/Shell/ProductShellAsset.cpp
|
app/Shell/ProductShellAsset.cpp
|
||||||
app/Workspace/ProductEditorWorkspace.cpp
|
app/Workspace/ProductEditorWorkspace.cpp
|
||||||
|
app/Workspace/ProductEditorWorkspaceEventRouter.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(XCUIEditorApp PRIVATE
|
target_include_directories(XCUIEditorApp PRIVATE
|
||||||
|
|||||||
@@ -344,6 +344,39 @@ std::string DescribeProjectPanelEvent(const App::ProductProjectPanel::Event& eve
|
|||||||
return stream.str();
|
return stream.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string DescribeHierarchyPanelEvent(const App::ProductHierarchyPanel::Event& event) {
|
||||||
|
std::ostringstream stream = {};
|
||||||
|
switch (event.kind) {
|
||||||
|
case App::ProductHierarchyPanel::EventKind::SelectionChanged:
|
||||||
|
stream << "SelectionChanged";
|
||||||
|
break;
|
||||||
|
case App::ProductHierarchyPanel::EventKind::Reparented:
|
||||||
|
stream << "Reparented";
|
||||||
|
break;
|
||||||
|
case App::ProductHierarchyPanel::EventKind::MovedToRoot:
|
||||||
|
stream << "MovedToRoot";
|
||||||
|
break;
|
||||||
|
case App::ProductHierarchyPanel::EventKind::RenameRequested:
|
||||||
|
stream << "RenameRequested";
|
||||||
|
break;
|
||||||
|
case App::ProductHierarchyPanel::EventKind::None:
|
||||||
|
default:
|
||||||
|
stream << "None";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!event.itemId.empty()) {
|
||||||
|
stream << " item=" << event.itemId;
|
||||||
|
}
|
||||||
|
if (!event.targetItemId.empty()) {
|
||||||
|
stream << " target=" << event.targetItemId;
|
||||||
|
}
|
||||||
|
if (!event.label.empty()) {
|
||||||
|
stream << " label=" << event.label;
|
||||||
|
}
|
||||||
|
return stream.str();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int Application::Run(HINSTANCE hInstance, int nCmdShow) {
|
int Application::Run(HINSTANCE hInstance, int nCmdShow) {
|
||||||
@@ -534,9 +567,8 @@ void Application::RenderFrame() {
|
|||||||
frameTrace.str());
|
frameTrace.str());
|
||||||
}
|
}
|
||||||
ApplyHostCaptureRequests(shellFrame.result);
|
ApplyHostCaptureRequests(shellFrame.result);
|
||||||
for (const App::ProductProjectPanel::Event& event : m_editorWorkspace.GetProjectPanelEvents()) {
|
for (const App::ProductEditorWorkspaceTraceEntry& entry : m_editorWorkspace.GetTraceEntries()) {
|
||||||
LogRuntimeTrace("project", DescribeProjectPanelEvent(event));
|
LogRuntimeTrace(entry.channel, entry.message);
|
||||||
m_editorContext.SetStatus("Project", DescribeProjectPanelEvent(event));
|
|
||||||
}
|
}
|
||||||
ApplyHostedContentCaptureRequests();
|
ApplyHostedContentCaptureRequests();
|
||||||
ApplyCurrentCursor();
|
ApplyCurrentCursor();
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace XCEngine::UI::Editor::App {
|
|||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
using ::XCEngine::UI::Editor::BuildEditorShellShortcutManager;
|
using ::XCEngine::UI::Editor::BuildEditorShellShortcutManager;
|
||||||
|
constexpr std::size_t kMaxConsoleEntries = 256u;
|
||||||
|
|
||||||
std::string ComposeStatusText(
|
std::string ComposeStatusText(
|
||||||
std::string_view status,
|
std::string_view status,
|
||||||
@@ -82,6 +83,14 @@ const ProductEditorSession& ProductEditorContext::GetSession() const {
|
|||||||
return m_session;
|
return m_session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ProductEditorContext::SetSelection(ProductEditorSelectionState selection) {
|
||||||
|
m_session.selection = std::move(selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProductEditorContext::ClearSelection() {
|
||||||
|
m_session.selection = {};
|
||||||
|
}
|
||||||
|
|
||||||
UIEditorWorkspaceController& ProductEditorContext::GetWorkspaceController() {
|
UIEditorWorkspaceController& ProductEditorContext::GetWorkspaceController() {
|
||||||
return m_workspaceController;
|
return m_workspaceController;
|
||||||
}
|
}
|
||||||
@@ -110,6 +119,9 @@ void ProductEditorContext::SetReadyStatus() {
|
|||||||
void ProductEditorContext::SetStatus(
|
void ProductEditorContext::SetStatus(
|
||||||
std::string status,
|
std::string status,
|
||||||
std::string message) {
|
std::string message) {
|
||||||
|
if (m_lastStatus != status || m_lastMessage != message) {
|
||||||
|
AppendConsoleEntry(status, message);
|
||||||
|
}
|
||||||
m_lastStatus = std::move(status);
|
m_lastStatus = std::move(status);
|
||||||
m_lastMessage = std::move(message);
|
m_lastMessage = std::move(message);
|
||||||
}
|
}
|
||||||
@@ -172,6 +184,18 @@ void ProductEditorContext::UpdateStatusFromShellResult(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ProductEditorContext::AppendConsoleEntry(
|
||||||
|
std::string channel,
|
||||||
|
std::string message) {
|
||||||
|
ProductEditorConsoleEntry entry = {};
|
||||||
|
entry.channel = std::move(channel);
|
||||||
|
entry.message = std::move(message);
|
||||||
|
m_session.consoleEntries.push_back(std::move(entry));
|
||||||
|
if (m_session.consoleEntries.size() > kMaxConsoleEntries) {
|
||||||
|
m_session.consoleEntries.erase(m_session.consoleEntries.begin());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::string ProductEditorContext::DescribeWorkspaceState(
|
std::string ProductEditorContext::DescribeWorkspaceState(
|
||||||
const UIEditorShellInteractionState& interactionState) const {
|
const UIEditorShellInteractionState& interactionState) const {
|
||||||
std::ostringstream stream = {};
|
std::ostringstream stream = {};
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ public:
|
|||||||
const std::string& GetValidationMessage() const;
|
const std::string& GetValidationMessage() const;
|
||||||
const EditorShellAsset& GetShellAsset() const;
|
const EditorShellAsset& GetShellAsset() const;
|
||||||
const ProductEditorSession& GetSession() const;
|
const ProductEditorSession& GetSession() const;
|
||||||
|
void SetSelection(ProductEditorSelectionState selection);
|
||||||
|
void ClearSelection();
|
||||||
|
|
||||||
UIEditorWorkspaceController& GetWorkspaceController();
|
UIEditorWorkspaceController& GetWorkspaceController();
|
||||||
const UIEditorWorkspaceController& GetWorkspaceController() const;
|
const UIEditorWorkspaceController& GetWorkspaceController() const;
|
||||||
@@ -41,6 +43,8 @@ public:
|
|||||||
const UIEditorShellInteractionState& interactionState) const;
|
const UIEditorShellInteractionState& interactionState) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void AppendConsoleEntry(std::string channel, std::string message);
|
||||||
|
|
||||||
EditorShellAsset m_shellAsset = {};
|
EditorShellAsset m_shellAsset = {};
|
||||||
EditorShellAssetValidationResult m_shellValidation = {};
|
EditorShellAssetValidationResult m_shellValidation = {};
|
||||||
UIEditorWorkspaceController m_workspaceController = {};
|
UIEditorWorkspaceController m_workspaceController = {};
|
||||||
|
|||||||
@@ -35,6 +35,18 @@ std::string_view GetProductEditorActionRouteName(ProductEditorActionRoute route)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string_view GetProductEditorSelectionKindName(ProductEditorSelectionKind kind) {
|
||||||
|
switch (kind) {
|
||||||
|
case ProductEditorSelectionKind::HierarchyNode:
|
||||||
|
return "HierarchyNode";
|
||||||
|
case ProductEditorSelectionKind::ProjectItem:
|
||||||
|
return "ProjectItem";
|
||||||
|
case ProductEditorSelectionKind::None:
|
||||||
|
default:
|
||||||
|
return "None";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ProductEditorActionRoute ResolveProductEditorActionRoute(std::string_view panelId) {
|
ProductEditorActionRoute ResolveProductEditorActionRoute(std::string_view panelId) {
|
||||||
if (panelId == "hierarchy") {
|
if (panelId == "hierarchy") {
|
||||||
return ProductEditorActionRoute::Hierarchy;
|
return ProductEditorActionRoute::Hierarchy;
|
||||||
|
|||||||
@@ -25,16 +25,38 @@ enum class ProductEditorActionRoute : std::uint8_t {
|
|||||||
Game
|
Game
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum class ProductEditorSelectionKind : std::uint8_t {
|
||||||
|
None = 0,
|
||||||
|
HierarchyNode,
|
||||||
|
ProjectItem
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ProductEditorSelectionState {
|
||||||
|
ProductEditorSelectionKind kind = ProductEditorSelectionKind::None;
|
||||||
|
std::string itemId = {};
|
||||||
|
std::string displayName = {};
|
||||||
|
std::filesystem::path absolutePath = {};
|
||||||
|
bool directory = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ProductEditorConsoleEntry {
|
||||||
|
std::string channel = {};
|
||||||
|
std::string message = {};
|
||||||
|
};
|
||||||
|
|
||||||
struct ProductEditorSession {
|
struct ProductEditorSession {
|
||||||
std::filesystem::path repoRoot = {};
|
std::filesystem::path repoRoot = {};
|
||||||
std::filesystem::path projectRoot = {};
|
std::filesystem::path projectRoot = {};
|
||||||
std::string activePanelId = {};
|
std::string activePanelId = {};
|
||||||
ProductEditorRuntimeMode runtimeMode = ProductEditorRuntimeMode::Edit;
|
ProductEditorRuntimeMode runtimeMode = ProductEditorRuntimeMode::Edit;
|
||||||
ProductEditorActionRoute activeRoute = ProductEditorActionRoute::None;
|
ProductEditorActionRoute activeRoute = ProductEditorActionRoute::None;
|
||||||
|
ProductEditorSelectionState selection = {};
|
||||||
|
std::vector<ProductEditorConsoleEntry> consoleEntries = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string_view GetProductEditorRuntimeModeName(ProductEditorRuntimeMode mode);
|
std::string_view GetProductEditorRuntimeModeName(ProductEditorRuntimeMode mode);
|
||||||
std::string_view GetProductEditorActionRouteName(ProductEditorActionRoute route);
|
std::string_view GetProductEditorActionRouteName(ProductEditorActionRoute route);
|
||||||
|
std::string_view GetProductEditorSelectionKindName(ProductEditorSelectionKind kind);
|
||||||
|
|
||||||
ProductEditorActionRoute ResolveProductEditorActionRoute(std::string_view panelId);
|
ProductEditorActionRoute ResolveProductEditorActionRoute(std::string_view panelId);
|
||||||
|
|
||||||
|
|||||||
293
new_editor/app/Hierarchy/ProductHierarchyModel.cpp
Normal file
293
new_editor/app/Hierarchy/ProductHierarchyModel.cpp
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
#include "Hierarchy/ProductHierarchyModel.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <functional>
|
||||||
|
#include <optional>
|
||||||
|
#include <sstream>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace XCEngine::UI::Editor::App {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
const ProductHierarchyNode* FindNodeRecursive(
|
||||||
|
const std::vector<ProductHierarchyNode>& nodes,
|
||||||
|
std::string_view nodeId) {
|
||||||
|
for (const ProductHierarchyNode& node : nodes) {
|
||||||
|
if (node.nodeId == nodeId) {
|
||||||
|
return &node;
|
||||||
|
}
|
||||||
|
if (const ProductHierarchyNode* child =
|
||||||
|
FindNodeRecursive(node.children, nodeId);
|
||||||
|
child != nullptr) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProductHierarchyNode* FindNodeRecursive(
|
||||||
|
std::vector<ProductHierarchyNode>& nodes,
|
||||||
|
std::string_view nodeId) {
|
||||||
|
for (ProductHierarchyNode& node : nodes) {
|
||||||
|
if (node.nodeId == nodeId) {
|
||||||
|
return &node;
|
||||||
|
}
|
||||||
|
if (ProductHierarchyNode* child =
|
||||||
|
FindNodeRecursive(node.children, nodeId);
|
||||||
|
child != nullptr) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FindNodeParentRecursive(
|
||||||
|
const std::vector<ProductHierarchyNode>& nodes,
|
||||||
|
std::string_view nodeId,
|
||||||
|
const ProductHierarchyNode*& parent) {
|
||||||
|
for (const ProductHierarchyNode& node : nodes) {
|
||||||
|
for (const ProductHierarchyNode& child : node.children) {
|
||||||
|
if (child.nodeId == nodeId) {
|
||||||
|
parent = &node;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FindNodeParentRecursive(node.children, nodeId, parent)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ExtractNodeRecursive(
|
||||||
|
std::vector<ProductHierarchyNode>& nodes,
|
||||||
|
std::string_view nodeId,
|
||||||
|
ProductHierarchyNode& extractedNode) {
|
||||||
|
for (std::size_t index = 0u; index < nodes.size(); ++index) {
|
||||||
|
if (nodes[index].nodeId == nodeId) {
|
||||||
|
extractedNode = std::move(nodes[index]);
|
||||||
|
nodes.erase(nodes.begin() + static_cast<std::ptrdiff_t>(index));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ExtractNodeRecursive(nodes[index].children, nodeId, extractedNode)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildTreeItemsRecursive(
|
||||||
|
const std::vector<ProductHierarchyNode>& nodes,
|
||||||
|
std::uint32_t depth,
|
||||||
|
const ::XCEngine::UI::UITextureHandle& icon,
|
||||||
|
std::vector<Widgets::UIEditorTreeViewItem>& items) {
|
||||||
|
for (const ProductHierarchyNode& node : nodes) {
|
||||||
|
Widgets::UIEditorTreeViewItem item = {};
|
||||||
|
item.itemId = node.nodeId;
|
||||||
|
item.label = node.label;
|
||||||
|
item.depth = depth;
|
||||||
|
item.forceLeaf = node.children.empty();
|
||||||
|
item.leadingIcon = icon;
|
||||||
|
items.push_back(std::move(item));
|
||||||
|
BuildTreeItemsRecursive(node.children, depth + 1u, icon, items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ProductHierarchyModel ProductHierarchyModel::BuildDefault() {
|
||||||
|
ProductHierarchyModel model = {};
|
||||||
|
model.m_roots = {
|
||||||
|
ProductHierarchyNode{ "main_camera", "Main Camera", {} },
|
||||||
|
ProductHierarchyNode{ "directional_light", "Directional Light", {} },
|
||||||
|
ProductHierarchyNode{
|
||||||
|
"player",
|
||||||
|
"Player",
|
||||||
|
{
|
||||||
|
ProductHierarchyNode{ "camera_pivot", "Camera Pivot", {} },
|
||||||
|
ProductHierarchyNode{ "player_mesh", "Mesh", {} }
|
||||||
|
} },
|
||||||
|
ProductHierarchyNode{
|
||||||
|
"environment",
|
||||||
|
"Environment",
|
||||||
|
{
|
||||||
|
ProductHierarchyNode{ "ground", "Ground", {} },
|
||||||
|
ProductHierarchyNode{
|
||||||
|
"props",
|
||||||
|
"Props",
|
||||||
|
{
|
||||||
|
ProductHierarchyNode{ "crate_01", "Crate_01", {} },
|
||||||
|
ProductHierarchyNode{ "barrel_01", "Barrel_01", {} }
|
||||||
|
} }
|
||||||
|
} }
|
||||||
|
};
|
||||||
|
model.m_nextGeneratedNodeId = 1u;
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProductHierarchyModel::Empty() const {
|
||||||
|
return m_roots.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProductHierarchyModel::ContainsNode(std::string_view nodeId) const {
|
||||||
|
return FindNode(nodeId) != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductHierarchyNode* ProductHierarchyModel::FindNode(std::string_view nodeId) const {
|
||||||
|
return FindNodeRecursive(m_roots, nodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProductHierarchyNode* ProductHierarchyModel::FindNode(std::string_view nodeId) {
|
||||||
|
return FindNodeRecursive(m_roots, nodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> ProductHierarchyModel::GetParentId(std::string_view nodeId) const {
|
||||||
|
const ProductHierarchyNode* parent = nullptr;
|
||||||
|
if (!FindNodeParentRecursive(m_roots, nodeId, parent) || parent == nullptr) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent->nodeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProductHierarchyModel::RenameNode(std::string_view nodeId, std::string label) {
|
||||||
|
ProductHierarchyNode* node = FindNode(nodeId);
|
||||||
|
if (node == nullptr || label.empty() || node->label == label) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
node->label = std::move(label);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ProductHierarchyModel::CreateChild(
|
||||||
|
std::string_view parentId,
|
||||||
|
std::string_view label) {
|
||||||
|
ProductHierarchyNode* parent = FindNode(parentId);
|
||||||
|
if (parent == nullptr) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
ProductHierarchyNode node = {};
|
||||||
|
node.nodeId = AllocateNodeId();
|
||||||
|
node.label = label.empty() ? std::string("GameObject") : std::string(label);
|
||||||
|
parent->children.push_back(std::move(node));
|
||||||
|
return parent->children.back().nodeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProductHierarchyModel::DeleteNode(std::string_view nodeId) {
|
||||||
|
if (nodeId.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProductHierarchyNode removed = {};
|
||||||
|
return ExtractNodeRecursive(m_roots, nodeId, removed);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProductHierarchyModel::CanReparent(
|
||||||
|
std::string_view sourceNodeId,
|
||||||
|
std::string_view targetParentId) const {
|
||||||
|
if (sourceNodeId.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductHierarchyNode* source = FindNode(sourceNodeId);
|
||||||
|
if (source == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetParentId.empty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductHierarchyNode* targetParent = FindNode(targetParentId);
|
||||||
|
return CanAdopt(sourceNodeId, targetParent);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProductHierarchyModel::Reparent(
|
||||||
|
std::string_view sourceNodeId,
|
||||||
|
std::string_view targetParentId) {
|
||||||
|
if (!CanReparent(sourceNodeId, targetParentId) || targetParentId.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::optional<std::string> currentParentId = GetParentId(sourceNodeId);
|
||||||
|
if (currentParentId.has_value() && currentParentId.value() == targetParentId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProductHierarchyNode movedNode = {};
|
||||||
|
if (!ExtractNodeRecursive(m_roots, sourceNodeId, movedNode)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProductHierarchyNode* targetParent = FindNode(targetParentId);
|
||||||
|
if (targetParent == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetParent->children.push_back(std::move(movedNode));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProductHierarchyModel::MoveToRoot(std::string_view sourceNodeId) {
|
||||||
|
if (sourceNodeId.empty() || !ContainsNode(sourceNodeId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!GetParentId(sourceNodeId).has_value()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProductHierarchyNode movedNode = {};
|
||||||
|
if (!ExtractNodeRecursive(m_roots, sourceNodeId, movedNode)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_roots.push_back(std::move(movedNode));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Widgets::UIEditorTreeViewItem> ProductHierarchyModel::BuildTreeItems(
|
||||||
|
const ::XCEngine::UI::UITextureHandle& icon) const {
|
||||||
|
std::vector<Widgets::UIEditorTreeViewItem> items = {};
|
||||||
|
BuildTreeItemsRecursive(m_roots, 0u, icon, items);
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ProductHierarchyModel::AllocateNodeId() {
|
||||||
|
std::ostringstream stream = {};
|
||||||
|
stream << "generated_node_" << m_nextGeneratedNodeId++;
|
||||||
|
return stream.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProductHierarchyModel::CanAdopt(
|
||||||
|
std::string_view sourceNodeId,
|
||||||
|
const ProductHierarchyNode* targetParent) const {
|
||||||
|
if (targetParent == nullptr || sourceNodeId == targetParent->nodeId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductHierarchyNode* source = FindNode(sourceNodeId);
|
||||||
|
if (source == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !ContainsDescendant(*source, targetParent->nodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProductHierarchyModel::ContainsDescendant(
|
||||||
|
const ProductHierarchyNode& node,
|
||||||
|
std::string_view candidateId) const {
|
||||||
|
for (const ProductHierarchyNode& child : node.children) {
|
||||||
|
if (child.nodeId == candidateId || ContainsDescendant(child, candidateId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace XCEngine::UI::Editor::App
|
||||||
54
new_editor/app/Hierarchy/ProductHierarchyModel.h
Normal file
54
new_editor/app/Hierarchy/ProductHierarchyModel.h
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <XCEditor/Collections/UIEditorTreeView.h>
|
||||||
|
|
||||||
|
#include <XCEngine/UI/Types.h>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace XCEngine::UI::Editor::App {
|
||||||
|
|
||||||
|
struct ProductHierarchyNode {
|
||||||
|
std::string nodeId = {};
|
||||||
|
std::string label = {};
|
||||||
|
std::vector<ProductHierarchyNode> children = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
class ProductHierarchyModel {
|
||||||
|
public:
|
||||||
|
static ProductHierarchyModel BuildDefault();
|
||||||
|
|
||||||
|
bool Empty() const;
|
||||||
|
bool ContainsNode(std::string_view nodeId) const;
|
||||||
|
const ProductHierarchyNode* FindNode(std::string_view nodeId) const;
|
||||||
|
ProductHierarchyNode* FindNode(std::string_view nodeId);
|
||||||
|
|
||||||
|
std::optional<std::string> GetParentId(std::string_view nodeId) const;
|
||||||
|
bool RenameNode(std::string_view nodeId, std::string label);
|
||||||
|
std::string CreateChild(std::string_view parentId, std::string_view label);
|
||||||
|
bool DeleteNode(std::string_view nodeId);
|
||||||
|
|
||||||
|
bool CanReparent(std::string_view sourceNodeId, std::string_view targetParentId) const;
|
||||||
|
bool Reparent(std::string_view sourceNodeId, std::string_view targetParentId);
|
||||||
|
bool MoveToRoot(std::string_view sourceNodeId);
|
||||||
|
|
||||||
|
std::vector<Widgets::UIEditorTreeViewItem> BuildTreeItems(
|
||||||
|
const ::XCEngine::UI::UITextureHandle& icon) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string AllocateNodeId();
|
||||||
|
bool CanAdopt(
|
||||||
|
std::string_view sourceNodeId,
|
||||||
|
const ProductHierarchyNode* targetParent) const;
|
||||||
|
bool ContainsDescendant(
|
||||||
|
const ProductHierarchyNode& node,
|
||||||
|
std::string_view candidateId) const;
|
||||||
|
|
||||||
|
std::vector<ProductHierarchyNode> m_roots = {};
|
||||||
|
std::uint64_t m_nextGeneratedNodeId = 1u;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace XCEngine::UI::Editor::App
|
||||||
109
new_editor/app/Panels/ProductConsolePanel.cpp
Normal file
109
new_editor/app/Panels/ProductConsolePanel.cpp
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
#include "ProductConsolePanel.h"
|
||||||
|
|
||||||
|
#include <XCEditor/Foundation/UIEditorTheme.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace XCEngine::UI::Editor::App {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using ::XCEngine::UI::UIColor;
|
||||||
|
using ::XCEngine::UI::UIDrawList;
|
||||||
|
using ::XCEngine::UI::UIPoint;
|
||||||
|
using ::XCEngine::UI::UIRect;
|
||||||
|
|
||||||
|
constexpr std::string_view kConsolePanelId = "console";
|
||||||
|
constexpr float kPadding = 8.0f;
|
||||||
|
constexpr float kLineHeight = 18.0f;
|
||||||
|
constexpr float kFontSize = 11.0f;
|
||||||
|
|
||||||
|
constexpr UIColor kSurfaceColor(0.205f, 0.205f, 0.205f, 1.0f);
|
||||||
|
constexpr UIColor kTextColor(0.840f, 0.840f, 0.840f, 1.0f);
|
||||||
|
constexpr UIColor kChannelColor(0.640f, 0.640f, 0.640f, 1.0f);
|
||||||
|
constexpr UIColor kEmptyColor(0.580f, 0.580f, 0.580f, 1.0f);
|
||||||
|
|
||||||
|
float ResolveTextTop(float rectY, float rectHeight, float fontSize) {
|
||||||
|
const float lineHeight = fontSize * 1.6f;
|
||||||
|
return rectY + std::floor((rectHeight - lineHeight) * 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
const UIEditorPanelContentHostPanelState* ProductConsolePanel::FindMountedConsolePanel(
|
||||||
|
const UIEditorPanelContentHostFrame& contentHostFrame) const {
|
||||||
|
for (const UIEditorPanelContentHostPanelState& panelState : contentHostFrame.panelStates) {
|
||||||
|
if (panelState.panelId == kConsolePanelId && panelState.mounted) {
|
||||||
|
return &panelState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProductConsolePanel::Update(
|
||||||
|
const ProductEditorSession& session,
|
||||||
|
const UIEditorPanelContentHostFrame& contentHostFrame) {
|
||||||
|
const UIEditorPanelContentHostPanelState* panelState =
|
||||||
|
FindMountedConsolePanel(contentHostFrame);
|
||||||
|
if (panelState == nullptr) {
|
||||||
|
m_visible = false;
|
||||||
|
m_bounds = {};
|
||||||
|
m_entries = nullptr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_visible = true;
|
||||||
|
m_bounds = panelState->bounds;
|
||||||
|
m_entries = &session.consoleEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProductConsolePanel::Append(UIDrawList& drawList) const {
|
||||||
|
if (!m_visible || m_bounds.width <= 0.0f || m_bounds.height <= 0.0f) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawList.AddFilledRect(m_bounds, kSurfaceColor);
|
||||||
|
|
||||||
|
const UIRect contentRect(
|
||||||
|
m_bounds.x + kPadding,
|
||||||
|
m_bounds.y + kPadding,
|
||||||
|
(std::max)(m_bounds.width - kPadding * 2.0f, 0.0f),
|
||||||
|
(std::max)(m_bounds.height - kPadding * 2.0f, 0.0f));
|
||||||
|
|
||||||
|
if (m_entries == nullptr || m_entries->empty()) {
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(contentRect.x, ResolveTextTop(contentRect.y, kLineHeight, kFontSize)),
|
||||||
|
"Console is empty.",
|
||||||
|
kEmptyColor,
|
||||||
|
kFontSize);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t maxVisibleLines = (std::max)(
|
||||||
|
static_cast<std::size_t>(1),
|
||||||
|
static_cast<std::size_t>(contentRect.height / kLineHeight));
|
||||||
|
const std::size_t entryCount = m_entries->size();
|
||||||
|
const std::size_t firstVisible =
|
||||||
|
entryCount > maxVisibleLines ? entryCount - maxVisibleLines : 0u;
|
||||||
|
|
||||||
|
drawList.PushClipRect(contentRect);
|
||||||
|
float nextY = contentRect.y;
|
||||||
|
for (std::size_t index = firstVisible; index < entryCount; ++index) {
|
||||||
|
const ProductEditorConsoleEntry& entry = (*m_entries)[index];
|
||||||
|
const std::string line = entry.channel.empty()
|
||||||
|
? entry.message
|
||||||
|
: "[" + entry.channel + "] " + entry.message;
|
||||||
|
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(contentRect.x, ResolveTextTop(nextY, kLineHeight, kFontSize)),
|
||||||
|
line,
|
||||||
|
entry.channel.empty() ? kTextColor : kChannelColor,
|
||||||
|
kFontSize);
|
||||||
|
nextY += kLineHeight;
|
||||||
|
}
|
||||||
|
drawList.PopClipRect();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace XCEngine::UI::Editor::App
|
||||||
27
new_editor/app/Panels/ProductConsolePanel.h
Normal file
27
new_editor/app/Panels/ProductConsolePanel.h
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Core/ProductEditorSession.h"
|
||||||
|
|
||||||
|
#include <XCEditor/Shell/UIEditorPanelContentHost.h>
|
||||||
|
|
||||||
|
#include <XCEngine/UI/DrawData.h>
|
||||||
|
|
||||||
|
namespace XCEngine::UI::Editor::App {
|
||||||
|
|
||||||
|
class ProductConsolePanel {
|
||||||
|
public:
|
||||||
|
void Update(
|
||||||
|
const ProductEditorSession& session,
|
||||||
|
const UIEditorPanelContentHostFrame& contentHostFrame);
|
||||||
|
void Append(::XCEngine::UI::UIDrawList& drawList) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const UIEditorPanelContentHostPanelState* FindMountedConsolePanel(
|
||||||
|
const UIEditorPanelContentHostFrame& contentHostFrame) const;
|
||||||
|
|
||||||
|
bool m_visible = false;
|
||||||
|
::XCEngine::UI::UIRect m_bounds = {};
|
||||||
|
const std::vector<ProductEditorConsoleEntry>* m_entries = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace XCEngine::UI::Editor::App
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
#include <XCEditor/Collections/UIEditorTreeView.h>
|
#include <XCEditor/Collections/UIEditorTreeView.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
@@ -12,15 +14,25 @@ namespace XCEngine::UI::Editor::App {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
using ::XCEngine::UI::UIColor;
|
||||||
using ::XCEngine::UI::UIDrawList;
|
using ::XCEngine::UI::UIDrawList;
|
||||||
using ::XCEngine::UI::UIInputEvent;
|
using ::XCEngine::UI::UIInputEvent;
|
||||||
using ::XCEngine::UI::UIInputEventType;
|
using ::XCEngine::UI::UIInputEventType;
|
||||||
using ::XCEngine::UI::UIPoint;
|
using ::XCEngine::UI::UIPoint;
|
||||||
|
using ::XCEngine::UI::UIPointerButton;
|
||||||
using ::XCEngine::UI::UIRect;
|
using ::XCEngine::UI::UIRect;
|
||||||
using Widgets::AppendUIEditorTreeViewBackground;
|
using Widgets::AppendUIEditorTreeViewBackground;
|
||||||
using Widgets::AppendUIEditorTreeViewForeground;
|
using Widgets::AppendUIEditorTreeViewForeground;
|
||||||
|
using Widgets::DoesUIEditorTreeViewItemHaveChildren;
|
||||||
|
using Widgets::HitTestUIEditorTreeView;
|
||||||
|
using Widgets::IsUIEditorTreeViewPointInside;
|
||||||
|
using Widgets::UIEditorTreeViewHitTarget;
|
||||||
|
using Widgets::UIEditorTreeViewHitTargetKind;
|
||||||
|
using Widgets::UIEditorTreeViewInvalidIndex;
|
||||||
|
|
||||||
constexpr std::string_view kHierarchyPanelId = "hierarchy";
|
constexpr std::string_view kHierarchyPanelId = "hierarchy";
|
||||||
|
constexpr float kDragThreshold = 4.0f;
|
||||||
|
constexpr UIColor kDragPreviewColor(0.82f, 0.82f, 0.82f, 0.55f);
|
||||||
|
|
||||||
bool ContainsPoint(const UIRect& rect, const UIPoint& point) {
|
bool ContainsPoint(const UIRect& rect, const UIPoint& point) {
|
||||||
return point.x >= rect.x &&
|
return point.x >= rect.x &&
|
||||||
@@ -29,12 +41,26 @@ bool ContainsPoint(const UIRect& rect, const UIPoint& point) {
|
|||||||
point.y <= rect.y + rect.height;
|
point.y <= rect.y + rect.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float ComputeSquaredDistance(const UIPoint& lhs, const UIPoint& rhs) {
|
||||||
|
const float dx = lhs.x - rhs.x;
|
||||||
|
const float dy = lhs.y - rhs.y;
|
||||||
|
return dx * dx + dy * dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
::XCEngine::UI::UITextureHandle ResolveGameObjectIcon(
|
||||||
|
const ProductBuiltInIcons* icons) {
|
||||||
|
return icons != nullptr
|
||||||
|
? icons->Resolve(ProductBuiltInIconKind::GameObject)
|
||||||
|
: ::XCEngine::UI::UITextureHandle {};
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<UIInputEvent> FilterHierarchyInputEvents(
|
std::vector<UIInputEvent> FilterHierarchyInputEvents(
|
||||||
const UIRect& bounds,
|
const UIRect& bounds,
|
||||||
const std::vector<UIInputEvent>& inputEvents,
|
const std::vector<UIInputEvent>& inputEvents,
|
||||||
bool allowInteraction,
|
bool allowInteraction,
|
||||||
bool panelActive) {
|
bool panelActive,
|
||||||
if (!allowInteraction) {
|
bool captureActive) {
|
||||||
|
if (!allowInteraction && !captureActive) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +72,7 @@ std::vector<UIInputEvent> FilterHierarchyInputEvents(
|
|||||||
case UIInputEventType::PointerButtonDown:
|
case UIInputEventType::PointerButtonDown:
|
||||||
case UIInputEventType::PointerButtonUp:
|
case UIInputEventType::PointerButtonUp:
|
||||||
case UIInputEventType::PointerWheel:
|
case UIInputEventType::PointerWheel:
|
||||||
if (ContainsPoint(bounds, event.position)) {
|
if (captureActive || ContainsPoint(bounds, event.position)) {
|
||||||
filteredEvents.push_back(event);
|
filteredEvents.push_back(event);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -55,6 +81,10 @@ std::vector<UIInputEvent> FilterHierarchyInputEvents(
|
|||||||
break;
|
break;
|
||||||
case UIInputEventType::FocusGained:
|
case UIInputEventType::FocusGained:
|
||||||
case UIInputEventType::FocusLost:
|
case UIInputEventType::FocusLost:
|
||||||
|
if (panelActive || captureActive) {
|
||||||
|
filteredEvents.push_back(event);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case UIInputEventType::KeyDown:
|
case UIInputEventType::KeyDown:
|
||||||
case UIInputEventType::KeyUp:
|
case UIInputEventType::KeyUp:
|
||||||
case UIInputEventType::Character:
|
case UIInputEventType::Character:
|
||||||
@@ -70,16 +100,41 @@ std::vector<UIInputEvent> FilterHierarchyInputEvents(
|
|||||||
return filteredEvents;
|
return filteredEvents;
|
||||||
}
|
}
|
||||||
|
|
||||||
::XCEngine::UI::UITextureHandle ResolveGameObjectIcon(
|
const Widgets::UIEditorTreeViewItem* ResolveHitItem(
|
||||||
const ProductBuiltInIcons* icons) {
|
const Widgets::UIEditorTreeViewLayout& layout,
|
||||||
return icons != nullptr
|
const std::vector<Widgets::UIEditorTreeViewItem>& items,
|
||||||
? icons->Resolve(ProductBuiltInIconKind::GameObject)
|
const UIPoint& point,
|
||||||
: ::XCEngine::UI::UITextureHandle {};
|
UIEditorTreeViewHitTarget* hitTargetOutput = nullptr) {
|
||||||
|
const UIEditorTreeViewHitTarget hitTarget = HitTestUIEditorTreeView(layout, point);
|
||||||
|
if (hitTargetOutput != nullptr) {
|
||||||
|
*hitTargetOutput = hitTarget;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hitTarget.itemIndex >= items.size()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return &items[hitTarget.itemIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t FindVisibleIndexForItemId(
|
||||||
|
const Widgets::UIEditorTreeViewLayout& layout,
|
||||||
|
const std::vector<Widgets::UIEditorTreeViewItem>& items,
|
||||||
|
std::string_view itemId) {
|
||||||
|
for (std::size_t visibleIndex = 0u; visibleIndex < layout.visibleItemIndices.size(); ++visibleIndex) {
|
||||||
|
const std::size_t itemIndex = layout.visibleItemIndices[visibleIndex];
|
||||||
|
if (itemIndex < items.size() && items[itemIndex].itemId == itemId) {
|
||||||
|
return visibleIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return UIEditorTreeViewInvalidIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void ProductHierarchyPanel::Initialize() {
|
void ProductHierarchyPanel::Initialize() {
|
||||||
|
m_model = ProductHierarchyModel::BuildDefault();
|
||||||
RebuildItems();
|
RebuildItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,52 +154,321 @@ const UIEditorPanelContentHostPanelState* ProductHierarchyPanel::FindMountedHier
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ProductHierarchyPanel::ResetTransientState() {
|
||||||
|
m_frameEvents.clear();
|
||||||
|
m_dragState.requestPointerCapture = false;
|
||||||
|
m_dragState.requestPointerRelease = false;
|
||||||
|
}
|
||||||
|
|
||||||
void ProductHierarchyPanel::RebuildItems() {
|
void ProductHierarchyPanel::RebuildItems() {
|
||||||
const auto icon = ResolveGameObjectIcon(m_icons);
|
const auto icon = ResolveGameObjectIcon(m_icons);
|
||||||
const std::string previousSelection =
|
const std::string previousSelection =
|
||||||
m_selection.HasSelection() ? m_selection.GetSelectedId() : std::string();
|
m_selection.HasSelection() ? m_selection.GetSelectedId() : std::string();
|
||||||
|
|
||||||
m_treeItems = {
|
m_treeItems = m_model.BuildTreeItems(icon);
|
||||||
{ "main-camera", "Main Camera", 0u, true, 0.0f, icon },
|
|
||||||
{ "directional-light", "Directional Light", 0u, true, 0.0f, icon },
|
|
||||||
{ "player", "Player", 0u, false, 0.0f, icon },
|
|
||||||
{ "player/camera-pivot", "Camera Pivot", 1u, true, 0.0f, icon },
|
|
||||||
{ "player/mesh", "Mesh", 1u, true, 0.0f, icon },
|
|
||||||
{ "environment", "Environment", 0u, false, 0.0f, icon },
|
|
||||||
{ "environment/ground", "Ground", 1u, true, 0.0f, icon },
|
|
||||||
{ "environment/props", "Props", 1u, false, 0.0f, icon },
|
|
||||||
{ "environment/props/crate-01", "Crate_01", 2u, true, 0.0f, icon },
|
|
||||||
{ "environment/props/barrel-01", "Barrel_01", 2u, true, 0.0f, icon }
|
|
||||||
};
|
|
||||||
|
|
||||||
m_expansion.Expand("player");
|
m_expansion.Expand("player");
|
||||||
m_expansion.Expand("environment");
|
m_expansion.Expand("environment");
|
||||||
m_expansion.Expand("environment/props");
|
m_expansion.Expand("props");
|
||||||
|
|
||||||
if (!previousSelection.empty()) {
|
if (!previousSelection.empty() && m_model.ContainsNode(previousSelection)) {
|
||||||
for (const Widgets::UIEditorTreeViewItem& item : m_treeItems) {
|
|
||||||
if (item.itemId == previousSelection) {
|
|
||||||
m_selection.SetSelection(previousSelection);
|
m_selection.SetSelection(previousSelection);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!m_treeItems.empty()) {
|
if (!m_treeItems.empty()) {
|
||||||
m_selection.SetSelection(m_treeItems.front().itemId);
|
m_selection.SetSelection(m_treeItems.front().itemId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ProductHierarchyPanel::EmitSelectionEvent() {
|
||||||
|
if (!m_selection.HasSelection()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductHierarchyNode* node = m_model.FindNode(m_selection.GetSelectedId());
|
||||||
|
if (node == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Event event = {};
|
||||||
|
event.kind = EventKind::SelectionChanged;
|
||||||
|
event.itemId = node->nodeId;
|
||||||
|
event.label = node->label;
|
||||||
|
m_frameEvents.push_back(std::move(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProductHierarchyPanel::EmitReparentEvent(
|
||||||
|
EventKind kind,
|
||||||
|
std::string itemId,
|
||||||
|
std::string targetItemId) {
|
||||||
|
Event event = {};
|
||||||
|
event.kind = kind;
|
||||||
|
event.itemId = std::move(itemId);
|
||||||
|
event.targetItemId = std::move(targetItemId);
|
||||||
|
if (const ProductHierarchyNode* node = m_model.FindNode(event.itemId); node != nullptr) {
|
||||||
|
event.label = node->label;
|
||||||
|
}
|
||||||
|
m_frameEvents.push_back(std::move(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<UIInputEvent> ProductHierarchyPanel::BuildInteractionInputEvents(
|
||||||
|
const std::vector<UIInputEvent>& inputEvents,
|
||||||
|
const UIRect& bounds,
|
||||||
|
bool allowInteraction,
|
||||||
|
bool panelActive) const {
|
||||||
|
const std::vector<UIInputEvent> rawEvents = FilterHierarchyInputEvents(
|
||||||
|
bounds,
|
||||||
|
inputEvents,
|
||||||
|
allowInteraction,
|
||||||
|
panelActive,
|
||||||
|
HasActivePointerCapture());
|
||||||
|
|
||||||
|
struct DragPreviewState {
|
||||||
|
std::string armedItemId = {};
|
||||||
|
UIPoint pressPosition = {};
|
||||||
|
bool armed = false;
|
||||||
|
bool dragging = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Widgets::UIEditorTreeViewLayout layout =
|
||||||
|
m_treeFrame.layout.bounds.width > 0.0f ? m_treeFrame.layout : Widgets::BuildUIEditorTreeViewLayout(
|
||||||
|
bounds,
|
||||||
|
m_treeItems,
|
||||||
|
m_expansion,
|
||||||
|
BuildProductTreeViewMetrics());
|
||||||
|
DragPreviewState preview = {};
|
||||||
|
preview.armed = m_dragState.armed;
|
||||||
|
preview.armedItemId = m_dragState.armedItemId;
|
||||||
|
preview.pressPosition = m_dragState.pressPosition;
|
||||||
|
preview.dragging = m_dragState.dragging;
|
||||||
|
|
||||||
|
std::vector<UIInputEvent> filteredEvents = {};
|
||||||
|
filteredEvents.reserve(rawEvents.size());
|
||||||
|
for (const UIInputEvent& event : rawEvents) {
|
||||||
|
bool suppress = false;
|
||||||
|
|
||||||
|
switch (event.type) {
|
||||||
|
case UIInputEventType::PointerButtonDown:
|
||||||
|
if (event.pointerButton == UIPointerButton::Left) {
|
||||||
|
UIEditorTreeViewHitTarget hitTarget = {};
|
||||||
|
const Widgets::UIEditorTreeViewItem* hitItem =
|
||||||
|
ResolveHitItem(layout, m_treeItems, event.position, &hitTarget);
|
||||||
|
if (hitItem != nullptr &&
|
||||||
|
hitTarget.kind == UIEditorTreeViewHitTargetKind::Row) {
|
||||||
|
preview.armed = true;
|
||||||
|
preview.armedItemId = hitItem->itemId;
|
||||||
|
preview.pressPosition = event.position;
|
||||||
|
} else {
|
||||||
|
preview.armed = false;
|
||||||
|
preview.armedItemId.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (preview.dragging) {
|
||||||
|
suppress = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UIInputEventType::PointerMove:
|
||||||
|
if (preview.dragging) {
|
||||||
|
suppress = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preview.armed &&
|
||||||
|
ComputeSquaredDistance(event.position, preview.pressPosition) >=
|
||||||
|
kDragThreshold * kDragThreshold) {
|
||||||
|
preview.dragging = true;
|
||||||
|
suppress = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UIInputEventType::PointerButtonUp:
|
||||||
|
if (event.pointerButton == UIPointerButton::Left) {
|
||||||
|
if (preview.dragging) {
|
||||||
|
suppress = true;
|
||||||
|
preview.dragging = false;
|
||||||
|
}
|
||||||
|
preview.armed = false;
|
||||||
|
preview.armedItemId.clear();
|
||||||
|
} else if (preview.dragging) {
|
||||||
|
suppress = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UIInputEventType::PointerLeave:
|
||||||
|
if (preview.dragging) {
|
||||||
|
suppress = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UIInputEventType::FocusLost:
|
||||||
|
preview.armed = false;
|
||||||
|
preview.dragging = false;
|
||||||
|
preview.armedItemId.clear();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!suppress) {
|
||||||
|
filteredEvents.push_back(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProductHierarchyPanel::ProcessDragAndFrameEvents(
|
||||||
|
const std::vector<UIInputEvent>& inputEvents,
|
||||||
|
const UIRect& bounds,
|
||||||
|
bool allowInteraction,
|
||||||
|
bool panelActive) {
|
||||||
|
const std::vector<UIInputEvent> filteredEvents = FilterHierarchyInputEvents(
|
||||||
|
bounds,
|
||||||
|
inputEvents,
|
||||||
|
allowInteraction,
|
||||||
|
panelActive,
|
||||||
|
HasActivePointerCapture());
|
||||||
|
|
||||||
|
if (m_treeFrame.result.selectionChanged) {
|
||||||
|
EmitSelectionEvent();
|
||||||
|
}
|
||||||
|
if (m_treeFrame.result.renameRequested &&
|
||||||
|
!m_treeFrame.result.renameItemId.empty()) {
|
||||||
|
Event event = {};
|
||||||
|
event.kind = EventKind::RenameRequested;
|
||||||
|
event.itemId = m_treeFrame.result.renameItemId;
|
||||||
|
if (const ProductHierarchyNode* node = m_model.FindNode(event.itemId); node != nullptr) {
|
||||||
|
event.label = node->label;
|
||||||
|
}
|
||||||
|
m_frameEvents.push_back(std::move(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const UIInputEvent& event : filteredEvents) {
|
||||||
|
switch (event.type) {
|
||||||
|
case UIInputEventType::PointerButtonDown:
|
||||||
|
if (event.pointerButton == UIPointerButton::Left) {
|
||||||
|
UIEditorTreeViewHitTarget hitTarget = {};
|
||||||
|
const Widgets::UIEditorTreeViewItem* hitItem =
|
||||||
|
ResolveHitItem(m_treeFrame.layout, m_treeItems, event.position, &hitTarget);
|
||||||
|
if (hitItem != nullptr &&
|
||||||
|
hitTarget.kind == UIEditorTreeViewHitTargetKind::Row) {
|
||||||
|
m_dragState.armed = true;
|
||||||
|
m_dragState.armedItemId = hitItem->itemId;
|
||||||
|
m_dragState.pressPosition = event.position;
|
||||||
|
} else {
|
||||||
|
m_dragState.armed = false;
|
||||||
|
m_dragState.armedItemId.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UIInputEventType::PointerMove:
|
||||||
|
if (m_dragState.armed && !m_dragState.dragging &&
|
||||||
|
ComputeSquaredDistance(event.position, m_dragState.pressPosition) >=
|
||||||
|
kDragThreshold * kDragThreshold) {
|
||||||
|
m_dragState.dragging = !m_dragState.armedItemId.empty();
|
||||||
|
m_dragState.draggedItemId = m_dragState.armedItemId;
|
||||||
|
m_dragState.dropTargetItemId.clear();
|
||||||
|
m_dragState.dropToRoot = false;
|
||||||
|
m_dragState.validDropTarget = false;
|
||||||
|
if (m_dragState.dragging) {
|
||||||
|
m_dragState.requestPointerCapture = true;
|
||||||
|
if (!m_selection.IsSelected(m_dragState.draggedItemId)) {
|
||||||
|
m_selection.SetSelection(m_dragState.draggedItemId);
|
||||||
|
EmitSelectionEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_dragState.dragging) {
|
||||||
|
UIEditorTreeViewHitTarget hitTarget = {};
|
||||||
|
const Widgets::UIEditorTreeViewItem* hitItem =
|
||||||
|
ResolveHitItem(m_treeFrame.layout, m_treeItems, event.position, &hitTarget);
|
||||||
|
|
||||||
|
m_dragState.dropTargetItemId.clear();
|
||||||
|
m_dragState.dropToRoot = false;
|
||||||
|
m_dragState.validDropTarget = false;
|
||||||
|
|
||||||
|
if (hitItem != nullptr &&
|
||||||
|
(hitTarget.kind == UIEditorTreeViewHitTargetKind::Row ||
|
||||||
|
hitTarget.kind == UIEditorTreeViewHitTargetKind::Disclosure)) {
|
||||||
|
m_dragState.dropTargetItemId = hitItem->itemId;
|
||||||
|
m_dragState.validDropTarget =
|
||||||
|
m_model.CanReparent(
|
||||||
|
m_dragState.draggedItemId,
|
||||||
|
m_dragState.dropTargetItemId);
|
||||||
|
} else if (ContainsPoint(bounds, event.position)) {
|
||||||
|
m_dragState.dropToRoot = true;
|
||||||
|
m_dragState.validDropTarget =
|
||||||
|
m_model.GetParentId(m_dragState.draggedItemId).has_value();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UIInputEventType::PointerButtonUp:
|
||||||
|
if (event.pointerButton != UIPointerButton::Left) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_dragState.dragging) {
|
||||||
|
if (m_dragState.validDropTarget) {
|
||||||
|
const std::string draggedItemId = m_dragState.draggedItemId;
|
||||||
|
const std::string dropTargetItemId = m_dragState.dropTargetItemId;
|
||||||
|
const bool changed =
|
||||||
|
m_dragState.dropToRoot
|
||||||
|
? m_model.MoveToRoot(draggedItemId)
|
||||||
|
: m_model.Reparent(draggedItemId, dropTargetItemId);
|
||||||
|
if (changed) {
|
||||||
|
RebuildItems();
|
||||||
|
EmitReparentEvent(
|
||||||
|
m_dragState.dropToRoot ? EventKind::MovedToRoot : EventKind::Reparented,
|
||||||
|
draggedItemId,
|
||||||
|
dropTargetItemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_dragState.armed = false;
|
||||||
|
m_dragState.dragging = false;
|
||||||
|
m_dragState.armedItemId.clear();
|
||||||
|
m_dragState.draggedItemId.clear();
|
||||||
|
m_dragState.dropTargetItemId.clear();
|
||||||
|
m_dragState.dropToRoot = false;
|
||||||
|
m_dragState.validDropTarget = false;
|
||||||
|
m_dragState.requestPointerRelease = true;
|
||||||
|
} else {
|
||||||
|
m_dragState.armed = false;
|
||||||
|
m_dragState.armedItemId.clear();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UIInputEventType::FocusLost:
|
||||||
|
if (m_dragState.dragging) {
|
||||||
|
m_dragState.requestPointerRelease = true;
|
||||||
|
}
|
||||||
|
m_dragState = {};
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ProductHierarchyPanel::Update(
|
void ProductHierarchyPanel::Update(
|
||||||
const UIEditorPanelContentHostFrame& contentHostFrame,
|
const UIEditorPanelContentHostFrame& contentHostFrame,
|
||||||
const std::vector<UIInputEvent>& inputEvents,
|
const std::vector<UIInputEvent>& inputEvents,
|
||||||
bool allowInteraction,
|
bool allowInteraction,
|
||||||
bool panelActive) {
|
bool panelActive) {
|
||||||
|
ResetTransientState();
|
||||||
|
|
||||||
const UIEditorPanelContentHostPanelState* panelState =
|
const UIEditorPanelContentHostPanelState* panelState =
|
||||||
FindMountedHierarchyPanel(contentHostFrame);
|
FindMountedHierarchyPanel(contentHostFrame);
|
||||||
if (panelState == nullptr) {
|
if (panelState == nullptr) {
|
||||||
m_visible = false;
|
m_visible = false;
|
||||||
m_treeFrame = {};
|
m_treeFrame = {};
|
||||||
|
m_dragState = {};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,14 +477,25 @@ void ProductHierarchyPanel::Update(
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_visible = true;
|
m_visible = true;
|
||||||
|
const std::vector<UIInputEvent> interactionEvents =
|
||||||
|
BuildInteractionInputEvents(
|
||||||
|
inputEvents,
|
||||||
|
panelState->bounds,
|
||||||
|
allowInteraction,
|
||||||
|
panelActive);
|
||||||
m_treeFrame = UpdateUIEditorTreeViewInteraction(
|
m_treeFrame = UpdateUIEditorTreeViewInteraction(
|
||||||
m_treeInteractionState,
|
m_treeInteractionState,
|
||||||
m_selection,
|
m_selection,
|
||||||
m_expansion,
|
m_expansion,
|
||||||
panelState->bounds,
|
panelState->bounds,
|
||||||
m_treeItems,
|
m_treeItems,
|
||||||
FilterHierarchyInputEvents(panelState->bounds, inputEvents, allowInteraction, panelActive),
|
interactionEvents,
|
||||||
BuildProductTreeViewMetrics());
|
BuildProductTreeViewMetrics());
|
||||||
|
ProcessDragAndFrameEvents(
|
||||||
|
inputEvents,
|
||||||
|
panelState->bounds,
|
||||||
|
allowInteraction,
|
||||||
|
panelActive);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProductHierarchyPanel::Append(UIDrawList& drawList) const {
|
void ProductHierarchyPanel::Append(UIDrawList& drawList) const {
|
||||||
@@ -184,6 +519,50 @@ void ProductHierarchyPanel::Append(UIDrawList& drawList) const {
|
|||||||
m_treeItems,
|
m_treeItems,
|
||||||
palette,
|
palette,
|
||||||
metrics);
|
metrics);
|
||||||
|
|
||||||
|
if (!m_dragState.dragging || !m_dragState.validDropTarget) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_dragState.dropToRoot) {
|
||||||
|
drawList.AddRectOutline(
|
||||||
|
m_treeFrame.layout.bounds,
|
||||||
|
kDragPreviewColor,
|
||||||
|
1.0f,
|
||||||
|
0.0f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t visibleIndex = FindVisibleIndexForItemId(
|
||||||
|
m_treeFrame.layout,
|
||||||
|
m_treeItems,
|
||||||
|
m_dragState.dropTargetItemId);
|
||||||
|
if (visibleIndex == UIEditorTreeViewInvalidIndex ||
|
||||||
|
visibleIndex >= m_treeFrame.layout.rowRects.size()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawList.AddRectOutline(
|
||||||
|
m_treeFrame.layout.rowRects[visibleIndex],
|
||||||
|
kDragPreviewColor,
|
||||||
|
1.0f,
|
||||||
|
0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProductHierarchyPanel::WantsHostPointerCapture() const {
|
||||||
|
return m_dragState.requestPointerCapture;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProductHierarchyPanel::WantsHostPointerRelease() const {
|
||||||
|
return m_dragState.requestPointerRelease;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProductHierarchyPanel::HasActivePointerCapture() const {
|
||||||
|
return m_dragState.dragging;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<ProductHierarchyPanel::Event>& ProductHierarchyPanel::GetFrameEvents() const {
|
||||||
|
return m_frameEvents;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace XCEngine::UI::Editor::App
|
} // namespace XCEngine::UI::Editor::App
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "Hierarchy/ProductHierarchyModel.h"
|
||||||
|
|
||||||
#include <XCEditor/Collections/UIEditorTreeViewInteraction.h>
|
#include <XCEditor/Collections/UIEditorTreeViewInteraction.h>
|
||||||
#include <XCEditor/Shell/UIEditorPanelContentHost.h>
|
#include <XCEditor/Shell/UIEditorPanelContentHost.h>
|
||||||
|
|
||||||
@@ -7,6 +9,8 @@
|
|||||||
#include <XCEngine/UI/Widgets/UIExpansionModel.h>
|
#include <XCEngine/UI/Widgets/UIExpansionModel.h>
|
||||||
#include <XCEngine/UI/Widgets/UISelectionModel.h>
|
#include <XCEngine/UI/Widgets/UISelectionModel.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace XCEngine::UI::Editor::App {
|
namespace XCEngine::UI::Editor::App {
|
||||||
@@ -15,6 +19,21 @@ class ProductBuiltInIcons;
|
|||||||
|
|
||||||
class ProductHierarchyPanel {
|
class ProductHierarchyPanel {
|
||||||
public:
|
public:
|
||||||
|
enum class EventKind : std::uint8_t {
|
||||||
|
None = 0,
|
||||||
|
SelectionChanged,
|
||||||
|
Reparented,
|
||||||
|
MovedToRoot,
|
||||||
|
RenameRequested
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Event {
|
||||||
|
EventKind kind = EventKind::None;
|
||||||
|
std::string itemId = {};
|
||||||
|
std::string targetItemId = {};
|
||||||
|
std::string label = {};
|
||||||
|
};
|
||||||
|
|
||||||
void Initialize();
|
void Initialize();
|
||||||
void SetBuiltInIcons(const ProductBuiltInIcons* icons);
|
void SetBuiltInIcons(const ProductBuiltInIcons* icons);
|
||||||
void Update(
|
void Update(
|
||||||
@@ -23,18 +42,54 @@ public:
|
|||||||
bool allowInteraction,
|
bool allowInteraction,
|
||||||
bool panelActive);
|
bool panelActive);
|
||||||
void Append(::XCEngine::UI::UIDrawList& drawList) const;
|
void Append(::XCEngine::UI::UIDrawList& drawList) const;
|
||||||
|
bool WantsHostPointerCapture() const;
|
||||||
|
bool WantsHostPointerRelease() const;
|
||||||
|
bool HasActivePointerCapture() const;
|
||||||
|
const std::vector<Event>& GetFrameEvents() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
struct DragState {
|
||||||
|
std::string armedItemId = {};
|
||||||
|
std::string draggedItemId = {};
|
||||||
|
std::string dropTargetItemId = {};
|
||||||
|
::XCEngine::UI::UIPoint pressPosition = {};
|
||||||
|
bool armed = false;
|
||||||
|
bool dragging = false;
|
||||||
|
bool dropToRoot = false;
|
||||||
|
bool validDropTarget = false;
|
||||||
|
bool requestPointerCapture = false;
|
||||||
|
bool requestPointerRelease = false;
|
||||||
|
};
|
||||||
|
|
||||||
const UIEditorPanelContentHostPanelState* FindMountedHierarchyPanel(
|
const UIEditorPanelContentHostPanelState* FindMountedHierarchyPanel(
|
||||||
const UIEditorPanelContentHostFrame& contentHostFrame) const;
|
const UIEditorPanelContentHostFrame& contentHostFrame) const;
|
||||||
|
void ResetTransientState();
|
||||||
void RebuildItems();
|
void RebuildItems();
|
||||||
|
void ProcessDragAndFrameEvents(
|
||||||
|
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
|
||||||
|
const ::XCEngine::UI::UIRect& bounds,
|
||||||
|
bool allowInteraction,
|
||||||
|
bool panelActive);
|
||||||
|
void EmitSelectionEvent();
|
||||||
|
void EmitReparentEvent(
|
||||||
|
EventKind kind,
|
||||||
|
std::string itemId,
|
||||||
|
std::string targetItemId);
|
||||||
|
std::vector<::XCEngine::UI::UIInputEvent> BuildInteractionInputEvents(
|
||||||
|
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
|
||||||
|
const ::XCEngine::UI::UIRect& bounds,
|
||||||
|
bool allowInteraction,
|
||||||
|
bool panelActive) const;
|
||||||
|
|
||||||
const ProductBuiltInIcons* m_icons = nullptr;
|
const ProductBuiltInIcons* m_icons = nullptr;
|
||||||
|
ProductHierarchyModel m_model = {};
|
||||||
std::vector<Widgets::UIEditorTreeViewItem> m_treeItems = {};
|
std::vector<Widgets::UIEditorTreeViewItem> m_treeItems = {};
|
||||||
::XCEngine::UI::Widgets::UISelectionModel m_selection = {};
|
::XCEngine::UI::Widgets::UISelectionModel m_selection = {};
|
||||||
::XCEngine::UI::Widgets::UIExpansionModel m_expansion = {};
|
::XCEngine::UI::Widgets::UIExpansionModel m_expansion = {};
|
||||||
UIEditorTreeViewInteractionState m_treeInteractionState = {};
|
UIEditorTreeViewInteractionState m_treeInteractionState = {};
|
||||||
UIEditorTreeViewInteractionFrame m_treeFrame = {};
|
UIEditorTreeViewInteractionFrame m_treeFrame = {};
|
||||||
|
std::vector<Event> m_frameEvents = {};
|
||||||
|
DragState m_dragState = {};
|
||||||
bool m_visible = false;
|
bool m_visible = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
224
new_editor/app/Panels/ProductInspectorPanel.cpp
Normal file
224
new_editor/app/Panels/ProductInspectorPanel.cpp
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
#include "ProductInspectorPanel.h"
|
||||||
|
|
||||||
|
#include <XCEditor/Foundation/UIEditorTheme.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace XCEngine::UI::Editor::App {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using ::XCEngine::UI::UIColor;
|
||||||
|
using ::XCEngine::UI::UIDrawList;
|
||||||
|
using ::XCEngine::UI::UIPoint;
|
||||||
|
using ::XCEngine::UI::UIRect;
|
||||||
|
|
||||||
|
constexpr std::string_view kInspectorPanelId = "inspector";
|
||||||
|
constexpr float kPanelPadding = 10.0f;
|
||||||
|
constexpr float kHeaderHeight = 22.0f;
|
||||||
|
constexpr float kSectionGap = 10.0f;
|
||||||
|
constexpr float kRowHeight = 21.0f;
|
||||||
|
constexpr float kLabelWidth = 88.0f;
|
||||||
|
constexpr float kTitleFontSize = 13.0f;
|
||||||
|
constexpr float kSubtitleFontSize = 11.0f;
|
||||||
|
constexpr float kSectionTitleFontSize = 11.0f;
|
||||||
|
constexpr float kRowFontSize = 11.0f;
|
||||||
|
|
||||||
|
constexpr UIColor kSurfaceColor(0.205f, 0.205f, 0.205f, 1.0f);
|
||||||
|
constexpr UIColor kSectionHeaderColor(0.225f, 0.225f, 0.225f, 1.0f);
|
||||||
|
constexpr UIColor kSectionBodyColor(0.215f, 0.215f, 0.215f, 1.0f);
|
||||||
|
constexpr UIColor kTitleColor(0.910f, 0.910f, 0.910f, 1.0f);
|
||||||
|
constexpr UIColor kSubtitleColor(0.650f, 0.650f, 0.650f, 1.0f);
|
||||||
|
constexpr UIColor kLabelColor(0.700f, 0.700f, 0.700f, 1.0f);
|
||||||
|
constexpr UIColor kValueColor(0.860f, 0.860f, 0.860f, 1.0f);
|
||||||
|
|
||||||
|
float ResolveTextTop(float rectY, float rectHeight, float fontSize) {
|
||||||
|
const float lineHeight = fontSize * 1.6f;
|
||||||
|
return rectY + std::floor((rectHeight - lineHeight) * 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string PathToUtf8String(const std::filesystem::path& path) {
|
||||||
|
const std::u8string value = path.u8string();
|
||||||
|
return std::string(value.begin(), value.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
const UIEditorPanelContentHostPanelState* ProductInspectorPanel::FindMountedInspectorPanel(
|
||||||
|
const UIEditorPanelContentHostFrame& contentHostFrame) const {
|
||||||
|
for (const UIEditorPanelContentHostPanelState& panelState : contentHostFrame.panelStates) {
|
||||||
|
if (panelState.panelId == kInspectorPanelId && panelState.mounted) {
|
||||||
|
return &panelState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProductInspectorPanel::BuildPresentation(const ProductEditorSession& session) {
|
||||||
|
m_sections.clear();
|
||||||
|
m_title.clear();
|
||||||
|
m_subtitle.clear();
|
||||||
|
m_hasSelection = false;
|
||||||
|
|
||||||
|
switch (session.selection.kind) {
|
||||||
|
case ProductEditorSelectionKind::HierarchyNode: {
|
||||||
|
m_hasSelection = true;
|
||||||
|
m_title = session.selection.displayName.empty()
|
||||||
|
? std::string("GameObject")
|
||||||
|
: session.selection.displayName;
|
||||||
|
m_subtitle = "GameObject";
|
||||||
|
|
||||||
|
Section identity = {};
|
||||||
|
identity.title = "Identity";
|
||||||
|
identity.rows = {
|
||||||
|
{ "Type", "GameObject" },
|
||||||
|
{ "Name", m_title },
|
||||||
|
{ "Id", session.selection.itemId }
|
||||||
|
};
|
||||||
|
m_sections.push_back(std::move(identity));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ProductEditorSelectionKind::ProjectItem: {
|
||||||
|
m_hasSelection = true;
|
||||||
|
m_title = session.selection.displayName.empty()
|
||||||
|
? (session.selection.directory ? std::string("Folder") : std::string("Asset"))
|
||||||
|
: session.selection.displayName;
|
||||||
|
m_subtitle = session.selection.directory ? "Folder" : "Asset";
|
||||||
|
|
||||||
|
Section identity = {};
|
||||||
|
identity.title = "Identity";
|
||||||
|
identity.rows = {
|
||||||
|
{ "Type", session.selection.directory ? std::string("Folder") : std::string("Asset") },
|
||||||
|
{ "Name", m_title },
|
||||||
|
{ "Id", session.selection.itemId }
|
||||||
|
};
|
||||||
|
m_sections.push_back(std::move(identity));
|
||||||
|
|
||||||
|
Section location = {};
|
||||||
|
location.title = "Location";
|
||||||
|
location.rows = {
|
||||||
|
{ "Path", PathToUtf8String(session.selection.absolutePath) }
|
||||||
|
};
|
||||||
|
m_sections.push_back(std::move(location));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ProductEditorSelectionKind::None:
|
||||||
|
default:
|
||||||
|
m_title = "Nothing selected";
|
||||||
|
m_subtitle = "Select a hierarchy item or project asset.";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProductInspectorPanel::Update(
|
||||||
|
const ProductEditorSession& session,
|
||||||
|
const UIEditorPanelContentHostFrame& contentHostFrame) {
|
||||||
|
const UIEditorPanelContentHostPanelState* panelState =
|
||||||
|
FindMountedInspectorPanel(contentHostFrame);
|
||||||
|
if (panelState == nullptr) {
|
||||||
|
m_visible = false;
|
||||||
|
m_bounds = {};
|
||||||
|
m_sections.clear();
|
||||||
|
m_title.clear();
|
||||||
|
m_subtitle.clear();
|
||||||
|
m_hasSelection = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_visible = true;
|
||||||
|
m_bounds = panelState->bounds;
|
||||||
|
BuildPresentation(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProductInspectorPanel::Append(UIDrawList& drawList) const {
|
||||||
|
if (!m_visible || m_bounds.width <= 0.0f || m_bounds.height <= 0.0f) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UIColor borderColor = ResolveUIEditorDockHostPalette().splitterColor;
|
||||||
|
drawList.AddFilledRect(m_bounds, kSurfaceColor);
|
||||||
|
|
||||||
|
const float contentX = m_bounds.x + kPanelPadding;
|
||||||
|
const float contentWidth = (std::max)(m_bounds.width - kPanelPadding * 2.0f, 0.0f);
|
||||||
|
float nextY = m_bounds.y + kPanelPadding;
|
||||||
|
|
||||||
|
const UIRect titleRect(contentX, nextY, contentWidth, 18.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(titleRect.x, ResolveTextTop(titleRect.y, titleRect.height, kTitleFontSize)),
|
||||||
|
m_title,
|
||||||
|
kTitleColor,
|
||||||
|
kTitleFontSize);
|
||||||
|
nextY += titleRect.height;
|
||||||
|
|
||||||
|
const UIRect subtitleRect(contentX, nextY, contentWidth, 16.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(subtitleRect.x, ResolveTextTop(subtitleRect.y, subtitleRect.height, kSubtitleFontSize)),
|
||||||
|
m_subtitle,
|
||||||
|
kSubtitleColor,
|
||||||
|
kSubtitleFontSize);
|
||||||
|
nextY += subtitleRect.height + kSectionGap;
|
||||||
|
|
||||||
|
if (!m_hasSelection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const Section& section : m_sections) {
|
||||||
|
const float bodyHeight = static_cast<float>(section.rows.size()) * kRowHeight;
|
||||||
|
const UIRect headerRect(contentX, nextY, contentWidth, kHeaderHeight);
|
||||||
|
const UIRect bodyRect(contentX, headerRect.y + headerRect.height, contentWidth, bodyHeight);
|
||||||
|
|
||||||
|
drawList.AddFilledRect(headerRect, kSectionHeaderColor);
|
||||||
|
drawList.AddFilledRect(bodyRect, kSectionBodyColor);
|
||||||
|
drawList.AddRectOutline(
|
||||||
|
UIRect(headerRect.x, headerRect.y, headerRect.width, headerRect.height + bodyRect.height),
|
||||||
|
borderColor,
|
||||||
|
1.0f,
|
||||||
|
0.0f);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(headerRect.x + 8.0f, ResolveTextTop(headerRect.y, headerRect.height, kSectionTitleFontSize)),
|
||||||
|
section.title,
|
||||||
|
kTitleColor,
|
||||||
|
kSectionTitleFontSize);
|
||||||
|
|
||||||
|
float rowY = bodyRect.y;
|
||||||
|
for (std::size_t rowIndex = 0u; rowIndex < section.rows.size(); ++rowIndex) {
|
||||||
|
const SectionRow& row = section.rows[rowIndex];
|
||||||
|
const UIRect rowRect(contentX, rowY, contentWidth, kRowHeight);
|
||||||
|
const UIRect labelRect(rowRect.x + 8.0f, rowRect.y, kLabelWidth, rowRect.height);
|
||||||
|
const UIRect valueRect(
|
||||||
|
labelRect.x + labelRect.width + 8.0f,
|
||||||
|
rowRect.y,
|
||||||
|
(std::max)(rowRect.width - (labelRect.width + 24.0f), 0.0f),
|
||||||
|
rowRect.height);
|
||||||
|
|
||||||
|
if (rowIndex > 0u) {
|
||||||
|
drawList.AddFilledRect(
|
||||||
|
UIRect(rowRect.x, rowRect.y, rowRect.width, 1.0f),
|
||||||
|
borderColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(labelRect.x, ResolveTextTop(labelRect.y, labelRect.height, kRowFontSize)),
|
||||||
|
row.label,
|
||||||
|
kLabelColor,
|
||||||
|
kRowFontSize);
|
||||||
|
|
||||||
|
drawList.PushClipRect(valueRect);
|
||||||
|
drawList.AddText(
|
||||||
|
UIPoint(valueRect.x, ResolveTextTop(valueRect.y, valueRect.height, kRowFontSize)),
|
||||||
|
row.value,
|
||||||
|
kValueColor,
|
||||||
|
kRowFontSize);
|
||||||
|
drawList.PopClipRect();
|
||||||
|
|
||||||
|
rowY += kRowHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextY = bodyRect.y + bodyRect.height + kSectionGap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace XCEngine::UI::Editor::App
|
||||||
44
new_editor/app/Panels/ProductInspectorPanel.h
Normal file
44
new_editor/app/Panels/ProductInspectorPanel.h
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Core/ProductEditorSession.h"
|
||||||
|
|
||||||
|
#include <XCEditor/Shell/UIEditorPanelContentHost.h>
|
||||||
|
|
||||||
|
#include <XCEngine/UI/DrawData.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace XCEngine::UI::Editor::App {
|
||||||
|
|
||||||
|
class ProductInspectorPanel {
|
||||||
|
public:
|
||||||
|
void Update(
|
||||||
|
const ProductEditorSession& session,
|
||||||
|
const UIEditorPanelContentHostFrame& contentHostFrame);
|
||||||
|
void Append(::XCEngine::UI::UIDrawList& drawList) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct SectionRow {
|
||||||
|
std::string label = {};
|
||||||
|
std::string value = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Section {
|
||||||
|
std::string title = {};
|
||||||
|
std::vector<SectionRow> rows = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const UIEditorPanelContentHostPanelState* FindMountedInspectorPanel(
|
||||||
|
const UIEditorPanelContentHostFrame& contentHostFrame) const;
|
||||||
|
void BuildPresentation(const ProductEditorSession& session);
|
||||||
|
|
||||||
|
bool m_visible = false;
|
||||||
|
bool m_hasSelection = false;
|
||||||
|
::XCEngine::UI::UIRect m_bounds = {};
|
||||||
|
std::string m_title = {};
|
||||||
|
std::string m_subtitle = {};
|
||||||
|
std::vector<Section> m_sections = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace XCEngine::UI::Editor::App
|
||||||
@@ -7,11 +7,9 @@
|
|||||||
#include <XCEditor/Foundation/UIEditorTheme.h>
|
#include <XCEditor/Foundation/UIEditorTheme.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <system_error>
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
@@ -32,7 +30,6 @@ using Widgets::AppendUIEditorTreeViewBackground;
|
|||||||
using Widgets::AppendUIEditorTreeViewForeground;
|
using Widgets::AppendUIEditorTreeViewForeground;
|
||||||
|
|
||||||
constexpr std::string_view kProjectPanelId = "project";
|
constexpr std::string_view kProjectPanelId = "project";
|
||||||
constexpr std::string_view kAssetsRootId = "Assets";
|
|
||||||
constexpr std::size_t kInvalidLayoutIndex = static_cast<std::size_t>(-1);
|
constexpr std::size_t kInvalidLayoutIndex = static_cast<std::size_t>(-1);
|
||||||
|
|
||||||
constexpr float kBrowserHeaderHeight = 24.0f;
|
constexpr float kBrowserHeaderHeight = 24.0f;
|
||||||
@@ -102,122 +99,6 @@ float MeasureTextWidth(
|
|||||||
return static_cast<float>(text.size()) * fontSize * 0.56f;
|
return static_cast<float>(text.size()) * fontSize * 0.56f;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<UIInputEvent> FilterProjectPanelInputEvents(
|
std::vector<UIInputEvent> FilterProjectPanelInputEvents(
|
||||||
const UIRect& bounds,
|
const UIRect& bounds,
|
||||||
const std::vector<UIInputEvent>& inputEvents,
|
const std::vector<UIInputEvent>& inputEvents,
|
||||||
@@ -303,35 +184,6 @@ float ClampNavigationWidth(float value, float totalWidth) {
|
|||||||
return std::clamp(value, kNavigationMinWidth, maxWidth);
|
return std::clamp(value, kNavigationMinWidth, maxWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> BuildBreadcrumbSegments(std::string_view currentFolderId) {
|
|
||||||
std::vector<std::string> segments = {};
|
|
||||||
if (currentFolderId.empty()) {
|
|
||||||
segments.push_back(std::string(kAssetsRootId));
|
|
||||||
return segments;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t segmentStart = 0u;
|
|
||||||
while (segmentStart < currentFolderId.size()) {
|
|
||||||
const std::size_t separator = currentFolderId.find('/', segmentStart);
|
|
||||||
const std::size_t segmentLength =
|
|
||||||
separator == std::string_view::npos
|
|
||||||
? currentFolderId.size() - segmentStart
|
|
||||||
: separator - segmentStart;
|
|
||||||
if (segmentLength > 0u) {
|
|
||||||
segments.emplace_back(currentFolderId.substr(segmentStart, segmentLength));
|
|
||||||
}
|
|
||||||
if (separator == std::string_view::npos) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
segmentStart = separator + 1u;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (segments.empty()) {
|
|
||||||
segments.push_back(std::string(kAssetsRootId));
|
|
||||||
}
|
|
||||||
return segments;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AppendTilePreview(
|
void AppendTilePreview(
|
||||||
UIDrawList& drawList,
|
UIDrawList& drawList,
|
||||||
const UIRect& previewRect,
|
const UIRect& previewRect,
|
||||||
@@ -371,18 +223,14 @@ void AppendTilePreview(
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void ProductProjectPanel::Initialize(const std::filesystem::path& repoRoot) {
|
void ProductProjectPanel::Initialize(const std::filesystem::path& repoRoot) {
|
||||||
m_assetsRootPath = (repoRoot / "project/Assets").lexically_normal();
|
m_browserModel.Initialize(repoRoot);
|
||||||
RefreshFolderTree();
|
|
||||||
SyncCurrentFolderSelection();
|
SyncCurrentFolderSelection();
|
||||||
RefreshAssetList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProductProjectPanel::SetBuiltInIcons(const ProductBuiltInIcons* icons) {
|
void ProductProjectPanel::SetBuiltInIcons(const ProductBuiltInIcons* icons) {
|
||||||
m_icons = icons;
|
m_icons = icons;
|
||||||
if (!m_assetsRootPath.empty()) {
|
m_browserModel.SetFolderIcon(ResolveFolderIcon(m_icons));
|
||||||
RefreshFolderTree();
|
|
||||||
SyncCurrentFolderSelection();
|
SyncCurrentFolderSelection();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProductProjectPanel::SetTextMeasurer(const UIEditorTextMeasurer* textMeasurer) {
|
void ProductProjectPanel::SetTextMeasurer(const UIEditorTextMeasurer* textMeasurer) {
|
||||||
@@ -411,24 +259,12 @@ const std::vector<ProductProjectPanel::Event>& ProductProjectPanel::GetFrameEven
|
|||||||
|
|
||||||
const ProductProjectPanel::FolderEntry* ProductProjectPanel::FindFolderEntry(
|
const ProductProjectPanel::FolderEntry* ProductProjectPanel::FindFolderEntry(
|
||||||
std::string_view itemId) const {
|
std::string_view itemId) const {
|
||||||
for (const FolderEntry& entry : m_folderEntries) {
|
return m_browserModel.FindFolderEntry(itemId);
|
||||||
if (entry.itemId == itemId) {
|
|
||||||
return &entry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProductProjectPanel::AssetEntry* ProductProjectPanel::FindAssetEntry(
|
const ProductProjectPanel::AssetEntry* ProductProjectPanel::FindAssetEntry(
|
||||||
std::string_view itemId) const {
|
std::string_view itemId) const {
|
||||||
for (const AssetEntry& entry : m_assetEntries) {
|
return m_browserModel.FindAssetEntry(itemId);
|
||||||
if (entry.itemId == itemId) {
|
|
||||||
return &entry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const UIEditorPanelContentHostPanelState* ProductProjectPanel::FindMountedProjectPanel(
|
const UIEditorPanelContentHostPanelState* ProductProjectPanel::FindMountedProjectPanel(
|
||||||
@@ -444,6 +280,9 @@ const UIEditorPanelContentHostPanelState* ProductProjectPanel::FindMountedProjec
|
|||||||
|
|
||||||
ProductProjectPanel::Layout ProductProjectPanel::BuildLayout(const UIRect& bounds) const {
|
ProductProjectPanel::Layout ProductProjectPanel::BuildLayout(const UIRect& bounds) const {
|
||||||
Layout layout = {};
|
Layout layout = {};
|
||||||
|
const auto& assetEntries = m_browserModel.GetAssetEntries();
|
||||||
|
const std::vector<ProductProjectPanel::BrowserModel::BreadcrumbSegment> breadcrumbSegments =
|
||||||
|
m_browserModel.BuildBreadcrumbSegments();
|
||||||
const float dividerThickness = ResolveUIEditorDockHostMetrics().splitterMetrics.thickness;
|
const float dividerThickness = ResolveUIEditorDockHostMetrics().splitterMetrics.thickness;
|
||||||
layout.bounds = UIRect(
|
layout.bounds = UIRect(
|
||||||
bounds.x,
|
bounds.x,
|
||||||
@@ -496,9 +335,7 @@ ProductProjectPanel::Layout ProductProjectPanel::BuildLayout(const UIRect& bound
|
|||||||
const float headerRight =
|
const float headerRight =
|
||||||
layout.browserHeaderRect.x + layout.browserHeaderRect.width - kHeaderHorizontalPadding;
|
layout.browserHeaderRect.x + layout.browserHeaderRect.width - kHeaderHorizontalPadding;
|
||||||
float nextItemX = layout.browserHeaderRect.x + kHeaderHorizontalPadding;
|
float nextItemX = layout.browserHeaderRect.x + kHeaderHorizontalPadding;
|
||||||
std::string cumulativeFolderId = {};
|
for (std::size_t index = 0u; index < breadcrumbSegments.size(); ++index) {
|
||||||
const std::vector<std::string> segments = BuildBreadcrumbSegments(m_currentFolderId);
|
|
||||||
for (std::size_t index = 0u; index < segments.size(); ++index) {
|
|
||||||
if (index > 0u) {
|
if (index > 0u) {
|
||||||
const float separatorWidth =
|
const float separatorWidth =
|
||||||
MeasureTextWidth(m_textMeasurer, ">", kHeaderFontSize);
|
MeasureTextWidth(m_textMeasurer, ">", kHeaderFontSize);
|
||||||
@@ -519,15 +356,9 @@ ProductProjectPanel::Layout ProductProjectPanel::BuildLayout(const UIRect& bound
|
|||||||
nextItemX += separatorWidth + kBreadcrumbSpacing;
|
nextItemX += separatorWidth + kBreadcrumbSpacing;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index == 0u) {
|
const ProductProjectPanel::BrowserModel::BreadcrumbSegment& segment = breadcrumbSegments[index];
|
||||||
cumulativeFolderId = segments[index];
|
|
||||||
} else {
|
|
||||||
cumulativeFolderId += "/";
|
|
||||||
cumulativeFolderId += segments[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
const float labelWidth =
|
const float labelWidth =
|
||||||
MeasureTextWidth(m_textMeasurer, segments[index], kHeaderFontSize);
|
MeasureTextWidth(m_textMeasurer, segment.label, kHeaderFontSize);
|
||||||
const float itemWidth = labelWidth + kBreadcrumbItemPaddingX * 2.0f;
|
const float itemWidth = labelWidth + kBreadcrumbItemPaddingX * 2.0f;
|
||||||
const float availableWidth = headerRight - nextItemX;
|
const float availableWidth = headerRight - nextItemX;
|
||||||
if (availableWidth <= 0.0f) {
|
if (availableWidth <= 0.0f) {
|
||||||
@@ -535,16 +366,16 @@ ProductProjectPanel::Layout ProductProjectPanel::BuildLayout(const UIRect& bound
|
|||||||
}
|
}
|
||||||
|
|
||||||
layout.breadcrumbItems.push_back({
|
layout.breadcrumbItems.push_back({
|
||||||
segments[index],
|
segment.label,
|
||||||
cumulativeFolderId,
|
segment.targetFolderId,
|
||||||
UIRect(
|
UIRect(
|
||||||
nextItemX,
|
nextItemX,
|
||||||
breadcrumbY,
|
breadcrumbY,
|
||||||
ClampNonNegative((std::min)(itemWidth, availableWidth)),
|
ClampNonNegative((std::min)(itemWidth, availableWidth)),
|
||||||
breadcrumbRowHeight),
|
breadcrumbRowHeight),
|
||||||
false,
|
false,
|
||||||
index + 1u != segments.size(),
|
!segment.current,
|
||||||
index + 1u == segments.size()
|
segment.current
|
||||||
});
|
});
|
||||||
nextItemX += itemWidth + kBreadcrumbSpacing;
|
nextItemX += itemWidth + kBreadcrumbSpacing;
|
||||||
}
|
}
|
||||||
@@ -557,8 +388,8 @@ ProductProjectPanel::Layout ProductProjectPanel::BuildLayout(const UIRect& bound
|
|||||||
columnCount = 1;
|
columnCount = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
layout.assetTiles.reserve(m_assetEntries.size());
|
layout.assetTiles.reserve(assetEntries.size());
|
||||||
for (std::size_t index = 0; index < m_assetEntries.size(); ++index) {
|
for (std::size_t index = 0; index < assetEntries.size(); ++index) {
|
||||||
const int column = static_cast<int>(index % static_cast<std::size_t>(columnCount));
|
const int column = static_cast<int>(index % static_cast<std::size_t>(columnCount));
|
||||||
const int row = static_cast<int>(index / static_cast<std::size_t>(columnCount));
|
const int row = static_cast<int>(index / static_cast<std::size_t>(columnCount));
|
||||||
const float tileX = layout.gridRect.x + static_cast<float>(column) * (kGridTileWidth + kGridTileGapX);
|
const float tileX = layout.gridRect.x + static_cast<float>(column) * (kGridTileWidth + kGridTileGapX);
|
||||||
@@ -604,85 +435,31 @@ std::size_t ProductProjectPanel::HitTestAssetTile(const UIPoint& point) const {
|
|||||||
return kInvalidLayoutIndex;
|
return kInvalidLayoutIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProductProjectPanel::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;
|
|
||||||
m_folderEntries.push_back(std::move(folderEntry));
|
|
||||||
|
|
||||||
Widgets::UIEditorTreeViewItem item = {};
|
|
||||||
item.itemId = itemId;
|
|
||||||
item.label = PathToUtf8String(folderPath.filename());
|
|
||||||
item.depth = depth;
|
|
||||||
item.forceLeaf = !HasChildDirectories(folderPath);
|
|
||||||
item.leadingIcon = ResolveFolderIcon(m_icons);
|
|
||||||
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 ProductProjectPanel::EnsureValidCurrentFolder() {
|
|
||||||
if (m_currentFolderId.empty()) {
|
|
||||||
m_currentFolderId = std::string(kAssetsRootId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (FindFolderEntry(m_currentFolderId) == nullptr && !m_treeItems.empty()) {
|
|
||||||
m_currentFolderId = m_treeItems.front().itemId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ProductProjectPanel::ExpandFolderAncestors(std::string_view itemId) {
|
|
||||||
const FolderEntry* folderEntry = FindFolderEntry(itemId);
|
|
||||||
if (folderEntry == nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::filesystem::path path = folderEntry->absolutePath;
|
|
||||||
while (true) {
|
|
||||||
m_folderExpansion.Expand(BuildRelativeItemId(path, m_assetsRootPath));
|
|
||||||
if (path == m_assetsRootPath) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
path = path.parent_path();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ProductProjectPanel::SyncCurrentFolderSelection() {
|
void ProductProjectPanel::SyncCurrentFolderSelection() {
|
||||||
EnsureValidCurrentFolder();
|
const std::string& currentFolderId = m_browserModel.GetCurrentFolderId();
|
||||||
ExpandFolderAncestors(m_currentFolderId);
|
if (currentFolderId.empty()) {
|
||||||
m_folderSelection.SetSelection(m_currentFolderId);
|
m_folderSelection.ClearSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<std::string> ancestorFolderIds =
|
||||||
|
m_browserModel.CollectCurrentFolderAncestorIds();
|
||||||
|
for (const std::string& ancestorFolderId : ancestorFolderIds) {
|
||||||
|
m_folderExpansion.Expand(ancestorFolderId);
|
||||||
|
}
|
||||||
|
m_folderSelection.SetSelection(currentFolderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ProductProjectPanel::NavigateToFolder(std::string_view itemId, EventSource source) {
|
bool ProductProjectPanel::NavigateToFolder(std::string_view itemId, EventSource source) {
|
||||||
if (itemId.empty() || FindFolderEntry(itemId) == nullptr || itemId == m_currentFolderId) {
|
if (!m_browserModel.NavigateToFolder(itemId)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_currentFolderId = std::string(itemId);
|
|
||||||
SyncCurrentFolderSelection();
|
SyncCurrentFolderSelection();
|
||||||
m_assetSelection.ClearSelection();
|
m_assetSelection.ClearSelection();
|
||||||
m_hoveredAssetItemId.clear();
|
m_hoveredAssetItemId.clear();
|
||||||
m_lastPrimaryClickedAssetId.clear();
|
m_lastPrimaryClickedAssetId.clear();
|
||||||
RefreshAssetList();
|
EmitEvent(EventKind::FolderNavigated, source, FindFolderEntry(m_browserModel.GetCurrentFolderId()));
|
||||||
EmitEvent(EventKind::FolderNavigated, source, FindFolderEntry(m_currentFolderId));
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,7 +476,7 @@ void ProductProjectPanel::EmitEvent(
|
|||||||
event.source = source;
|
event.source = source;
|
||||||
event.itemId = folder->itemId;
|
event.itemId = folder->itemId;
|
||||||
event.absolutePath = folder->absolutePath;
|
event.absolutePath = folder->absolutePath;
|
||||||
event.displayName = PathToUtf8String(folder->absolutePath.filename());
|
event.displayName = folder->label;
|
||||||
event.directory = true;
|
event.directory = true;
|
||||||
m_frameEvents.push_back(std::move(event));
|
m_frameEvents.push_back(std::move(event));
|
||||||
}
|
}
|
||||||
@@ -731,55 +508,6 @@ void ProductProjectPanel::EmitSelectionClearedEvent(EventSource source) {
|
|||||||
m_frameEvents.push_back(std::move(event));
|
m_frameEvents.push_back(std::move(event));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProductProjectPanel::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 ProductProjectPanel::ResetTransientFrames() {
|
void ProductProjectPanel::ResetTransientFrames() {
|
||||||
m_treeFrame = {};
|
m_treeFrame = {};
|
||||||
m_frameEvents.clear();
|
m_frameEvents.clear();
|
||||||
@@ -811,10 +539,9 @@ void ProductProjectPanel::Update(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_treeItems.empty()) {
|
if (m_browserModel.GetTreeItems().empty()) {
|
||||||
RefreshFolderTree();
|
m_browserModel.Refresh();
|
||||||
SyncCurrentFolderSelection();
|
SyncCurrentFolderSelection();
|
||||||
RefreshAssetList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
m_visible = true;
|
m_visible = true;
|
||||||
@@ -836,13 +563,13 @@ void ProductProjectPanel::Update(
|
|||||||
m_folderSelection,
|
m_folderSelection,
|
||||||
m_folderExpansion,
|
m_folderExpansion,
|
||||||
m_layout.treeRect,
|
m_layout.treeRect,
|
||||||
m_treeItems,
|
m_browserModel.GetTreeItems(),
|
||||||
treeEvents,
|
treeEvents,
|
||||||
treeMetrics);
|
treeMetrics);
|
||||||
|
|
||||||
if (m_treeFrame.result.selectionChanged &&
|
if (m_treeFrame.result.selectionChanged &&
|
||||||
!m_treeFrame.result.selectedItemId.empty() &&
|
!m_treeFrame.result.selectedItemId.empty() &&
|
||||||
m_treeFrame.result.selectedItemId != m_currentFolderId) {
|
m_treeFrame.result.selectedItemId != m_browserModel.GetCurrentFolderId()) {
|
||||||
NavigateToFolder(m_treeFrame.result.selectedItemId, EventSource::Tree);
|
NavigateToFolder(m_treeFrame.result.selectedItemId, EventSource::Tree);
|
||||||
m_layout = BuildLayout(panelState->bounds);
|
m_layout = BuildLayout(panelState->bounds);
|
||||||
}
|
}
|
||||||
@@ -871,9 +598,10 @@ void ProductProjectPanel::Update(
|
|||||||
m_splitterDragging || ContainsPoint(m_layout.dividerRect, event.position);
|
m_splitterDragging || ContainsPoint(m_layout.dividerRect, event.position);
|
||||||
m_hoveredBreadcrumbIndex = HitTestBreadcrumbItem(event.position);
|
m_hoveredBreadcrumbIndex = HitTestBreadcrumbItem(event.position);
|
||||||
const std::size_t hoveredAssetIndex = HitTestAssetTile(event.position);
|
const std::size_t hoveredAssetIndex = HitTestAssetTile(event.position);
|
||||||
|
const auto& assetEntries = m_browserModel.GetAssetEntries();
|
||||||
m_hoveredAssetItemId =
|
m_hoveredAssetItemId =
|
||||||
hoveredAssetIndex < m_assetEntries.size()
|
hoveredAssetIndex < assetEntries.size()
|
||||||
? m_assetEntries[hoveredAssetIndex].itemId
|
? assetEntries[hoveredAssetIndex].itemId
|
||||||
: std::string();
|
: std::string();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -902,8 +630,9 @@ void ProductProjectPanel::Update(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto& assetEntries = m_browserModel.GetAssetEntries();
|
||||||
const std::size_t hitIndex = HitTestAssetTile(event.position);
|
const std::size_t hitIndex = HitTestAssetTile(event.position);
|
||||||
if (hitIndex >= m_assetEntries.size()) {
|
if (hitIndex >= assetEntries.size()) {
|
||||||
if (m_assetSelection.HasSelection()) {
|
if (m_assetSelection.HasSelection()) {
|
||||||
m_assetSelection.ClearSelection();
|
m_assetSelection.ClearSelection();
|
||||||
EmitSelectionClearedEvent(EventSource::Background);
|
EmitSelectionClearedEvent(EventSource::Background);
|
||||||
@@ -911,7 +640,7 @@ void ProductProjectPanel::Update(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AssetEntry& assetEntry = m_assetEntries[hitIndex];
|
const AssetEntry& assetEntry = assetEntries[hitIndex];
|
||||||
const bool alreadySelected = m_assetSelection.IsSelected(assetEntry.itemId);
|
const bool alreadySelected = m_assetSelection.IsSelected(assetEntry.itemId);
|
||||||
const bool selectionChanged = m_assetSelection.SetSelection(assetEntry.itemId);
|
const bool selectionChanged = m_assetSelection.SetSelection(assetEntry.itemId);
|
||||||
if (selectionChanged) {
|
if (selectionChanged) {
|
||||||
@@ -945,13 +674,17 @@ void ProductProjectPanel::Update(
|
|||||||
|
|
||||||
if (event.pointerButton == ::XCEngine::UI::UIPointerButton::Right &&
|
if (event.pointerButton == ::XCEngine::UI::UIPointerButton::Right &&
|
||||||
ContainsPoint(m_layout.gridRect, event.position)) {
|
ContainsPoint(m_layout.gridRect, event.position)) {
|
||||||
|
const auto& assetEntries = m_browserModel.GetAssetEntries();
|
||||||
const std::size_t hitIndex = HitTestAssetTile(event.position);
|
const std::size_t hitIndex = HitTestAssetTile(event.position);
|
||||||
if (hitIndex >= m_assetEntries.size()) {
|
if (hitIndex >= assetEntries.size()) {
|
||||||
EmitEvent(EventKind::ContextMenuRequested, EventSource::Background, static_cast<const AssetEntry*>(nullptr));
|
EmitEvent(
|
||||||
|
EventKind::ContextMenuRequested,
|
||||||
|
EventSource::Background,
|
||||||
|
static_cast<const AssetEntry*>(nullptr));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AssetEntry& assetEntry = m_assetEntries[hitIndex];
|
const AssetEntry& assetEntry = assetEntries[hitIndex];
|
||||||
if (!m_assetSelection.IsSelected(assetEntry.itemId)) {
|
if (!m_assetSelection.IsSelected(assetEntry.itemId)) {
|
||||||
m_assetSelection.SetSelection(assetEntry.itemId);
|
m_assetSelection.SetSelection(assetEntry.itemId);
|
||||||
EmitEvent(EventKind::AssetSelected, EventSource::GridSecondary, &assetEntry);
|
EmitEvent(EventKind::AssetSelected, EventSource::GridSecondary, &assetEntry);
|
||||||
@@ -1000,6 +733,8 @@ void ProductProjectPanel::Append(UIDrawList& drawList) const {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto& assetEntries = m_browserModel.GetAssetEntries();
|
||||||
|
|
||||||
drawList.AddFilledRect(m_layout.bounds, kSurfaceColor);
|
drawList.AddFilledRect(m_layout.bounds, kSurfaceColor);
|
||||||
drawList.AddFilledRect(m_layout.leftPaneRect, kPaneColor);
|
drawList.AddFilledRect(m_layout.leftPaneRect, kPaneColor);
|
||||||
drawList.AddFilledRect(m_layout.rightPaneRect, kPaneColor);
|
drawList.AddFilledRect(m_layout.rightPaneRect, kPaneColor);
|
||||||
@@ -1021,7 +756,7 @@ void ProductProjectPanel::Append(UIDrawList& drawList) const {
|
|||||||
AppendUIEditorTreeViewBackground(
|
AppendUIEditorTreeViewBackground(
|
||||||
drawList,
|
drawList,
|
||||||
m_treeFrame.layout,
|
m_treeFrame.layout,
|
||||||
m_treeItems,
|
m_browserModel.GetTreeItems(),
|
||||||
m_folderSelection,
|
m_folderSelection,
|
||||||
m_treeInteractionState.treeViewState,
|
m_treeInteractionState.treeViewState,
|
||||||
treePalette,
|
treePalette,
|
||||||
@@ -1029,7 +764,7 @@ void ProductProjectPanel::Append(UIDrawList& drawList) const {
|
|||||||
AppendUIEditorTreeViewForeground(
|
AppendUIEditorTreeViewForeground(
|
||||||
drawList,
|
drawList,
|
||||||
m_treeFrame.layout,
|
m_treeFrame.layout,
|
||||||
m_treeItems,
|
m_browserModel.GetTreeItems(),
|
||||||
treePalette,
|
treePalette,
|
||||||
treeMetrics);
|
treeMetrics);
|
||||||
|
|
||||||
@@ -1055,11 +790,11 @@ void ProductProjectPanel::Append(UIDrawList& drawList) const {
|
|||||||
drawList.PopClipRect();
|
drawList.PopClipRect();
|
||||||
|
|
||||||
for (const AssetTileLayout& tile : m_layout.assetTiles) {
|
for (const AssetTileLayout& tile : m_layout.assetTiles) {
|
||||||
if (tile.itemIndex >= m_assetEntries.size()) {
|
if (tile.itemIndex >= assetEntries.size()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AssetEntry& assetEntry = m_assetEntries[tile.itemIndex];
|
const AssetEntry& assetEntry = assetEntries[tile.itemIndex];
|
||||||
const bool selected = m_assetSelection.IsSelected(assetEntry.itemId);
|
const bool selected = m_assetSelection.IsSelected(assetEntry.itemId);
|
||||||
const bool hovered = m_hoveredAssetItemId == assetEntry.itemId;
|
const bool hovered = m_hoveredAssetItemId == assetEntry.itemId;
|
||||||
|
|
||||||
@@ -1084,7 +819,7 @@ void ProductProjectPanel::Append(UIDrawList& drawList) const {
|
|||||||
drawList.PopClipRect();
|
drawList.PopClipRect();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_assetEntries.empty()) {
|
if (assetEntries.empty()) {
|
||||||
const UIRect messageRect(
|
const UIRect messageRect(
|
||||||
m_layout.gridRect.x,
|
m_layout.gridRect.x,
|
||||||
m_layout.gridRect.y,
|
m_layout.gridRect.y,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "Project/ProductProjectBrowserModel.h"
|
||||||
|
|
||||||
#include <XCEditor/Collections/UIEditorTreeViewInteraction.h>
|
#include <XCEditor/Collections/UIEditorTreeViewInteraction.h>
|
||||||
#include <XCEditor/Foundation/UIEditorTextMeasurement.h>
|
#include <XCEditor/Foundation/UIEditorTextMeasurement.h>
|
||||||
#include <XCEditor/Shell/UIEditorPanelContentHost.h>
|
#include <XCEditor/Shell/UIEditorPanelContentHost.h>
|
||||||
@@ -70,17 +72,9 @@ public:
|
|||||||
const std::vector<Event>& GetFrameEvents() const;
|
const std::vector<Event>& GetFrameEvents() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct FolderEntry {
|
using BrowserModel = ::XCEngine::UI::Editor::App::Project::ProductProjectBrowserModel;
|
||||||
std::string itemId = {};
|
using FolderEntry = BrowserModel::FolderEntry;
|
||||||
std::filesystem::path absolutePath = {};
|
using AssetEntry = BrowserModel::AssetEntry;
|
||||||
};
|
|
||||||
|
|
||||||
struct AssetEntry {
|
|
||||||
std::string itemId = {};
|
|
||||||
std::filesystem::path absolutePath = {};
|
|
||||||
std::string displayName = {};
|
|
||||||
bool directory = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct BreadcrumbItemLayout {
|
struct BreadcrumbItemLayout {
|
||||||
std::string label = {};
|
std::string label = {};
|
||||||
@@ -118,10 +112,6 @@ private:
|
|||||||
Layout BuildLayout(const ::XCEngine::UI::UIRect& bounds) const;
|
Layout BuildLayout(const ::XCEngine::UI::UIRect& bounds) const;
|
||||||
std::size_t HitTestBreadcrumbItem(const ::XCEngine::UI::UIPoint& point) const;
|
std::size_t HitTestBreadcrumbItem(const ::XCEngine::UI::UIPoint& point) const;
|
||||||
std::size_t HitTestAssetTile(const ::XCEngine::UI::UIPoint& point) const;
|
std::size_t HitTestAssetTile(const ::XCEngine::UI::UIPoint& point) const;
|
||||||
void RefreshFolderTree();
|
|
||||||
void RefreshAssetList();
|
|
||||||
void EnsureValidCurrentFolder();
|
|
||||||
void ExpandFolderAncestors(std::string_view itemId);
|
|
||||||
void SyncCurrentFolderSelection();
|
void SyncCurrentFolderSelection();
|
||||||
bool NavigateToFolder(std::string_view itemId, EventSource source = EventSource::None);
|
bool NavigateToFolder(std::string_view itemId, EventSource source = EventSource::None);
|
||||||
void EmitEvent(EventKind kind, EventSource source, const FolderEntry* folder);
|
void EmitEvent(EventKind kind, EventSource source, const FolderEntry* folder);
|
||||||
@@ -129,10 +119,7 @@ private:
|
|||||||
void EmitSelectionClearedEvent(EventSource source);
|
void EmitSelectionClearedEvent(EventSource source);
|
||||||
void ResetTransientFrames();
|
void ResetTransientFrames();
|
||||||
|
|
||||||
std::filesystem::path m_assetsRootPath = {};
|
BrowserModel m_browserModel = {};
|
||||||
std::vector<FolderEntry> m_folderEntries = {};
|
|
||||||
std::vector<Widgets::UIEditorTreeViewItem> m_treeItems = {};
|
|
||||||
std::vector<AssetEntry> m_assetEntries = {};
|
|
||||||
const ProductBuiltInIcons* m_icons = nullptr;
|
const ProductBuiltInIcons* m_icons = nullptr;
|
||||||
const ::XCEngine::UI::Editor::UIEditorTextMeasurer* m_textMeasurer = nullptr;
|
const ::XCEngine::UI::Editor::UIEditorTextMeasurer* m_textMeasurer = nullptr;
|
||||||
::XCEngine::UI::Widgets::UISelectionModel m_folderSelection = {};
|
::XCEngine::UI::Widgets::UISelectionModel m_folderSelection = {};
|
||||||
@@ -142,7 +129,6 @@ private:
|
|||||||
UIEditorTreeViewInteractionFrame m_treeFrame = {};
|
UIEditorTreeViewInteractionFrame m_treeFrame = {};
|
||||||
std::vector<Event> m_frameEvents = {};
|
std::vector<Event> m_frameEvents = {};
|
||||||
Layout m_layout = {};
|
Layout m_layout = {};
|
||||||
std::string m_currentFolderId = {};
|
|
||||||
std::string m_hoveredAssetItemId = {};
|
std::string m_hoveredAssetItemId = {};
|
||||||
std::string m_lastPrimaryClickedAssetId = {};
|
std::string m_lastPrimaryClickedAssetId = {};
|
||||||
float m_navigationWidth = 248.0f;
|
float m_navigationWidth = 248.0f;
|
||||||
|
|||||||
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
|
||||||
67
new_editor/app/Project/ProductProjectBrowserModel.h
Normal file
67
new_editor/app/Project/ProductProjectBrowserModel.h
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <XCEditor/Collections/UIEditorTreeView.h>
|
||||||
|
|
||||||
|
#include <XCEngine/UI/Types.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace XCEngine::UI::Editor::App::Project {
|
||||||
|
|
||||||
|
class ProductProjectBrowserModel {
|
||||||
|
public:
|
||||||
|
struct BreadcrumbSegment {
|
||||||
|
std::string label = {};
|
||||||
|
std::string targetFolderId = {};
|
||||||
|
bool current = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FolderEntry {
|
||||||
|
std::string itemId = {};
|
||||||
|
std::filesystem::path absolutePath = {};
|
||||||
|
std::string label = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AssetEntry {
|
||||||
|
std::string itemId = {};
|
||||||
|
std::filesystem::path absolutePath = {};
|
||||||
|
std::string displayName = {};
|
||||||
|
bool directory = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
void Initialize(const std::filesystem::path& repoRoot);
|
||||||
|
void SetFolderIcon(const ::XCEngine::UI::UITextureHandle& icon);
|
||||||
|
void Refresh();
|
||||||
|
|
||||||
|
bool Empty() const;
|
||||||
|
const std::filesystem::path& GetAssetsRootPath() const;
|
||||||
|
const std::vector<FolderEntry>& GetFolderEntries() const;
|
||||||
|
const std::vector<Widgets::UIEditorTreeViewItem>& GetTreeItems() const;
|
||||||
|
const std::vector<AssetEntry>& GetAssetEntries() const;
|
||||||
|
const std::string& GetCurrentFolderId() const;
|
||||||
|
|
||||||
|
const FolderEntry* FindFolderEntry(std::string_view itemId) const;
|
||||||
|
const AssetEntry* FindAssetEntry(std::string_view itemId) const;
|
||||||
|
bool NavigateToFolder(std::string_view itemId);
|
||||||
|
std::vector<BreadcrumbSegment> BuildBreadcrumbSegments() const;
|
||||||
|
std::vector<std::string> CollectCurrentFolderAncestorIds() const;
|
||||||
|
std::vector<std::string> BuildAncestorFolderIds(std::string_view itemId) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void RefreshFolderTree();
|
||||||
|
void RefreshAssetList();
|
||||||
|
void EnsureValidCurrentFolder();
|
||||||
|
|
||||||
|
std::filesystem::path m_assetsRootPath = {};
|
||||||
|
std::vector<FolderEntry> m_folderEntries = {};
|
||||||
|
std::vector<Widgets::UIEditorTreeViewItem> m_treeItems = {};
|
||||||
|
std::vector<AssetEntry> m_assetEntries = {};
|
||||||
|
::XCEngine::UI::UITextureHandle m_folderIcon = {};
|
||||||
|
std::string m_currentFolderId = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace XCEngine::UI::Editor::App::Project
|
||||||
@@ -23,8 +23,8 @@ UIEditorPanelRegistry BuildPanelRegistry() {
|
|||||||
{ "hierarchy", "Hierarchy", UIEditorPanelPresentationKind::HostedContent, true, false, false },
|
{ "hierarchy", "Hierarchy", UIEditorPanelPresentationKind::HostedContent, true, false, false },
|
||||||
{ "scene", "Scene", UIEditorPanelPresentationKind::HostedContent, false, false, false },
|
{ "scene", "Scene", UIEditorPanelPresentationKind::HostedContent, false, false, false },
|
||||||
{ "game", "Game", UIEditorPanelPresentationKind::HostedContent, false, false, false },
|
{ "game", "Game", UIEditorPanelPresentationKind::HostedContent, false, false, false },
|
||||||
{ "inspector", "Inspector", UIEditorPanelPresentationKind::Placeholder, true, false, false },
|
{ "inspector", "Inspector", UIEditorPanelPresentationKind::HostedContent, true, false, false },
|
||||||
{ "console", "Console", UIEditorPanelPresentationKind::Placeholder, true, false, false },
|
{ "console", "Console", UIEditorPanelPresentationKind::HostedContent, true, false, false },
|
||||||
{ "project", "Project", UIEditorPanelPresentationKind::HostedContent, false, false, false }
|
{ "project", "Project", UIEditorPanelPresentationKind::HostedContent, false, false, false }
|
||||||
};
|
};
|
||||||
return registry;
|
return registry;
|
||||||
@@ -434,8 +434,8 @@ UIEditorShellInteractionDefinition BuildBaseShellDefinition() {
|
|||||||
BuildHostedContentPresentation("hierarchy"),
|
BuildHostedContentPresentation("hierarchy"),
|
||||||
BuildHostedContentPresentation("scene"),
|
BuildHostedContentPresentation("scene"),
|
||||||
BuildHostedContentPresentation("game"),
|
BuildHostedContentPresentation("game"),
|
||||||
BuildPlaceholderPresentation("inspector"),
|
BuildHostedContentPresentation("inspector"),
|
||||||
BuildPlaceholderPresentation("console"),
|
BuildHostedContentPresentation("console"),
|
||||||
BuildHostedContentPresentation("project")
|
BuildHostedContentPresentation("project")
|
||||||
};
|
};
|
||||||
return definition;
|
return definition;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "ProductEditorWorkspace.h"
|
#include "ProductEditorWorkspace.h"
|
||||||
|
|
||||||
|
#include "Workspace/ProductEditorWorkspaceEventRouter.h"
|
||||||
|
|
||||||
#include <XCEditor/Shell/UIEditorShellCompose.h>
|
#include <XCEditor/Shell/UIEditorShellCompose.h>
|
||||||
#include <XCEditor/Foundation/UIEditorTheme.h>
|
#include <XCEditor/Foundation/UIEditorTheme.h>
|
||||||
|
|
||||||
@@ -90,6 +92,7 @@ void ProductEditorWorkspace::Initialize(
|
|||||||
void ProductEditorWorkspace::Shutdown() {
|
void ProductEditorWorkspace::Shutdown() {
|
||||||
m_shellFrame = {};
|
m_shellFrame = {};
|
||||||
m_shellInteractionState = {};
|
m_shellInteractionState = {};
|
||||||
|
m_traceEntries.clear();
|
||||||
m_builtInIcons.Shutdown();
|
m_builtInIcons.Shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +134,13 @@ void ProductEditorWorkspace::Update(
|
|||||||
hostedContentEvents,
|
hostedContentEvents,
|
||||||
!m_shellFrame.result.workspaceInputSuppressed,
|
!m_shellFrame.result.workspaceInputSuppressed,
|
||||||
activePanelId == "project");
|
activePanelId == "project");
|
||||||
|
m_traceEntries = ConsumeProductEditorWorkspaceEvents(context, *this);
|
||||||
|
m_inspectorPanel.Update(
|
||||||
|
context.GetSession(),
|
||||||
|
m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame);
|
||||||
|
m_consolePanel.Update(
|
||||||
|
context.GetSession(),
|
||||||
|
m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProductEditorWorkspace::Append(UIDrawList& drawList) const {
|
void ProductEditorWorkspace::Append(UIDrawList& drawList) const {
|
||||||
@@ -145,7 +155,9 @@ void ProductEditorWorkspace::Append(UIDrawList& drawList) const {
|
|||||||
m_shellInteractionState.composeState,
|
m_shellInteractionState.composeState,
|
||||||
palette.shellPalette,
|
palette.shellPalette,
|
||||||
metrics.shellMetrics);
|
metrics.shellMetrics);
|
||||||
|
m_consolePanel.Append(drawList);
|
||||||
m_hierarchyPanel.Append(drawList);
|
m_hierarchyPanel.Append(drawList);
|
||||||
|
m_inspectorPanel.Append(drawList);
|
||||||
m_projectPanel.Append(drawList);
|
m_projectPanel.Append(drawList);
|
||||||
AppendShellPopups(drawList, m_shellFrame, palette, metrics);
|
AppendShellPopups(drawList, m_shellFrame, palette, metrics);
|
||||||
}
|
}
|
||||||
@@ -158,6 +170,14 @@ const UIEditorShellInteractionState& ProductEditorWorkspace::GetShellInteraction
|
|||||||
return m_shellInteractionState;
|
return m_shellInteractionState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::vector<ProductEditorWorkspaceTraceEntry>& ProductEditorWorkspace::GetTraceEntries() const {
|
||||||
|
return m_traceEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<ProductHierarchyPanel::Event>& ProductEditorWorkspace::GetHierarchyPanelEvents() const {
|
||||||
|
return m_hierarchyPanel.GetFrameEvents();
|
||||||
|
}
|
||||||
|
|
||||||
const std::vector<ProductProjectPanel::Event>& ProductEditorWorkspace::GetProjectPanelEvents() const {
|
const std::vector<ProductProjectPanel::Event>& ProductEditorWorkspace::GetProjectPanelEvents() const {
|
||||||
return m_projectPanel.GetFrameEvents();
|
return m_projectPanel.GetFrameEvents();
|
||||||
}
|
}
|
||||||
@@ -176,15 +196,19 @@ Widgets::UIEditorDockHostCursorKind ProductEditorWorkspace::GetDockCursorKind()
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool ProductEditorWorkspace::WantsHostPointerCapture() const {
|
bool ProductEditorWorkspace::WantsHostPointerCapture() const {
|
||||||
return m_projectPanel.WantsHostPointerCapture();
|
return m_hierarchyPanel.WantsHostPointerCapture() ||
|
||||||
|
m_projectPanel.WantsHostPointerCapture();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ProductEditorWorkspace::WantsHostPointerRelease() const {
|
bool ProductEditorWorkspace::WantsHostPointerRelease() const {
|
||||||
return m_projectPanel.WantsHostPointerRelease();
|
return (m_hierarchyPanel.WantsHostPointerRelease() ||
|
||||||
|
m_projectPanel.WantsHostPointerRelease()) &&
|
||||||
|
!HasHostedContentCapture();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ProductEditorWorkspace::HasHostedContentCapture() const {
|
bool ProductEditorWorkspace::HasHostedContentCapture() const {
|
||||||
return m_projectPanel.HasActivePointerCapture();
|
return m_hierarchyPanel.HasActivePointerCapture() ||
|
||||||
|
m_projectPanel.HasActivePointerCapture();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ProductEditorWorkspace::HasShellInteractiveCapture() const {
|
bool ProductEditorWorkspace::HasShellInteractiveCapture() const {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "Core/ProductEditorContext.h"
|
#include "Core/ProductEditorContext.h"
|
||||||
|
#include "Panels/ProductConsolePanel.h"
|
||||||
#include "Icons/ProductBuiltInIcons.h"
|
#include "Icons/ProductBuiltInIcons.h"
|
||||||
#include "Panels/ProductHierarchyPanel.h"
|
#include "Panels/ProductHierarchyPanel.h"
|
||||||
|
#include "Panels/ProductInspectorPanel.h"
|
||||||
#include "Panels/ProductProjectPanel.h"
|
#include "Panels/ProductProjectPanel.h"
|
||||||
|
#include "Workspace/ProductEditorWorkspaceEventRouter.h"
|
||||||
|
|
||||||
#include <Host/NativeRenderer.h>
|
#include <Host/NativeRenderer.h>
|
||||||
|
|
||||||
@@ -33,6 +36,8 @@ public:
|
|||||||
|
|
||||||
const UIEditorShellInteractionFrame& GetShellFrame() const;
|
const UIEditorShellInteractionFrame& GetShellFrame() const;
|
||||||
const UIEditorShellInteractionState& GetShellInteractionState() const;
|
const UIEditorShellInteractionState& GetShellInteractionState() const;
|
||||||
|
const std::vector<ProductEditorWorkspaceTraceEntry>& GetTraceEntries() const;
|
||||||
|
const std::vector<ProductHierarchyPanel::Event>& GetHierarchyPanelEvents() const;
|
||||||
const std::vector<ProductProjectPanel::Event>& GetProjectPanelEvents() const;
|
const std::vector<ProductProjectPanel::Event>& GetProjectPanelEvents() const;
|
||||||
const std::string& GetBuiltInIconError() const;
|
const std::string& GetBuiltInIconError() const;
|
||||||
|
|
||||||
@@ -46,10 +51,13 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
ProductBuiltInIcons m_builtInIcons = {};
|
ProductBuiltInIcons m_builtInIcons = {};
|
||||||
|
ProductConsolePanel m_consolePanel = {};
|
||||||
ProductHierarchyPanel m_hierarchyPanel = {};
|
ProductHierarchyPanel m_hierarchyPanel = {};
|
||||||
|
ProductInspectorPanel m_inspectorPanel = {};
|
||||||
ProductProjectPanel m_projectPanel = {};
|
ProductProjectPanel m_projectPanel = {};
|
||||||
UIEditorShellInteractionState m_shellInteractionState = {};
|
UIEditorShellInteractionState m_shellInteractionState = {};
|
||||||
UIEditorShellInteractionFrame m_shellFrame = {};
|
UIEditorShellInteractionFrame m_shellFrame = {};
|
||||||
|
std::vector<ProductEditorWorkspaceTraceEntry> m_traceEntries = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace XCEngine::UI::Editor::App
|
} // namespace XCEngine::UI::Editor::App
|
||||||
|
|||||||
178
new_editor/app/Workspace/ProductEditorWorkspaceEventRouter.cpp
Normal file
178
new_editor/app/Workspace/ProductEditorWorkspaceEventRouter.cpp
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
#include "Workspace/ProductEditorWorkspaceEventRouter.h"
|
||||||
|
|
||||||
|
#include "Core/ProductEditorContext.h"
|
||||||
|
#include "Panels/ProductHierarchyPanel.h"
|
||||||
|
#include "Panels/ProductProjectPanel.h"
|
||||||
|
#include "Workspace/ProductEditorWorkspace.h"
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace XCEngine::UI::Editor::App {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string DescribeProjectPanelEvent(const ProductProjectPanel::Event& event) {
|
||||||
|
std::ostringstream stream = {};
|
||||||
|
switch (event.kind) {
|
||||||
|
case ProductProjectPanel::EventKind::AssetSelected:
|
||||||
|
stream << "AssetSelected";
|
||||||
|
break;
|
||||||
|
case ProductProjectPanel::EventKind::AssetSelectionCleared:
|
||||||
|
stream << "AssetSelectionCleared";
|
||||||
|
break;
|
||||||
|
case ProductProjectPanel::EventKind::FolderNavigated:
|
||||||
|
stream << "FolderNavigated";
|
||||||
|
break;
|
||||||
|
case ProductProjectPanel::EventKind::AssetOpened:
|
||||||
|
stream << "AssetOpened";
|
||||||
|
break;
|
||||||
|
case ProductProjectPanel::EventKind::ContextMenuRequested:
|
||||||
|
stream << "ContextMenuRequested";
|
||||||
|
break;
|
||||||
|
case ProductProjectPanel::EventKind::None:
|
||||||
|
default:
|
||||||
|
stream << "None";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream << " source=";
|
||||||
|
switch (event.source) {
|
||||||
|
case ProductProjectPanel::EventSource::Tree:
|
||||||
|
stream << "Tree";
|
||||||
|
break;
|
||||||
|
case ProductProjectPanel::EventSource::Breadcrumb:
|
||||||
|
stream << "Breadcrumb";
|
||||||
|
break;
|
||||||
|
case ProductProjectPanel::EventSource::GridPrimary:
|
||||||
|
stream << "GridPrimary";
|
||||||
|
break;
|
||||||
|
case ProductProjectPanel::EventSource::GridDoubleClick:
|
||||||
|
stream << "GridDoubleClick";
|
||||||
|
break;
|
||||||
|
case ProductProjectPanel::EventSource::GridSecondary:
|
||||||
|
stream << "GridSecondary";
|
||||||
|
break;
|
||||||
|
case ProductProjectPanel::EventSource::Background:
|
||||||
|
stream << "Background";
|
||||||
|
break;
|
||||||
|
case ProductProjectPanel::EventSource::None:
|
||||||
|
default:
|
||||||
|
stream << "None";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!event.itemId.empty()) {
|
||||||
|
stream << " item=" << event.itemId;
|
||||||
|
}
|
||||||
|
if (!event.displayName.empty()) {
|
||||||
|
stream << " label=" << event.displayName;
|
||||||
|
}
|
||||||
|
return stream.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string DescribeHierarchyPanelEvent(const ProductHierarchyPanel::Event& event) {
|
||||||
|
std::ostringstream stream = {};
|
||||||
|
switch (event.kind) {
|
||||||
|
case ProductHierarchyPanel::EventKind::SelectionChanged:
|
||||||
|
stream << "SelectionChanged";
|
||||||
|
break;
|
||||||
|
case ProductHierarchyPanel::EventKind::Reparented:
|
||||||
|
stream << "Reparented";
|
||||||
|
break;
|
||||||
|
case ProductHierarchyPanel::EventKind::MovedToRoot:
|
||||||
|
stream << "MovedToRoot";
|
||||||
|
break;
|
||||||
|
case ProductHierarchyPanel::EventKind::RenameRequested:
|
||||||
|
stream << "RenameRequested";
|
||||||
|
break;
|
||||||
|
case ProductHierarchyPanel::EventKind::None:
|
||||||
|
default:
|
||||||
|
stream << "None";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!event.itemId.empty()) {
|
||||||
|
stream << " item=" << event.itemId;
|
||||||
|
}
|
||||||
|
if (!event.targetItemId.empty()) {
|
||||||
|
stream << " target=" << event.targetItemId;
|
||||||
|
}
|
||||||
|
if (!event.label.empty()) {
|
||||||
|
stream << " label=" << event.label;
|
||||||
|
}
|
||||||
|
return stream.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApplyHierarchySelection(
|
||||||
|
ProductEditorContext& context,
|
||||||
|
const ProductHierarchyPanel::Event& event) {
|
||||||
|
if (event.kind != ProductHierarchyPanel::EventKind::SelectionChanged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.itemId.empty()) {
|
||||||
|
context.ClearSelection();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProductEditorSelectionState selection = {};
|
||||||
|
selection.kind = ProductEditorSelectionKind::HierarchyNode;
|
||||||
|
selection.itemId = event.itemId;
|
||||||
|
selection.displayName = event.label.empty() ? event.itemId : event.label;
|
||||||
|
context.SetSelection(std::move(selection));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApplyProjectSelection(
|
||||||
|
ProductEditorContext& context,
|
||||||
|
const ProductProjectPanel::Event& event) {
|
||||||
|
switch (event.kind) {
|
||||||
|
case ProductProjectPanel::EventKind::AssetSelected: {
|
||||||
|
ProductEditorSelectionState selection = {};
|
||||||
|
selection.kind = ProductEditorSelectionKind::ProjectItem;
|
||||||
|
selection.itemId = event.itemId;
|
||||||
|
selection.displayName = event.displayName.empty() ? event.itemId : event.displayName;
|
||||||
|
selection.absolutePath = event.absolutePath;
|
||||||
|
selection.directory = event.directory;
|
||||||
|
context.SetSelection(std::move(selection));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case ProductProjectPanel::EventKind::AssetSelectionCleared:
|
||||||
|
case ProductProjectPanel::EventKind::FolderNavigated:
|
||||||
|
if (context.GetSession().selection.kind == ProductEditorSelectionKind::ProjectItem) {
|
||||||
|
context.ClearSelection();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case ProductProjectPanel::EventKind::AssetOpened:
|
||||||
|
case ProductProjectPanel::EventKind::ContextMenuRequested:
|
||||||
|
case ProductProjectPanel::EventKind::None:
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::vector<ProductEditorWorkspaceTraceEntry> ConsumeProductEditorWorkspaceEvents(
|
||||||
|
ProductEditorContext& context,
|
||||||
|
const ProductEditorWorkspace& workspace) {
|
||||||
|
std::vector<ProductEditorWorkspaceTraceEntry> entries = {};
|
||||||
|
|
||||||
|
for (const ProductHierarchyPanel::Event& event : workspace.GetHierarchyPanelEvents()) {
|
||||||
|
ApplyHierarchySelection(context, event);
|
||||||
|
const std::string message = DescribeHierarchyPanelEvent(event);
|
||||||
|
context.SetStatus("Hierarchy", message);
|
||||||
|
entries.push_back(ProductEditorWorkspaceTraceEntry{ "hierarchy", std::move(message) });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const ProductProjectPanel::Event& event : workspace.GetProjectPanelEvents()) {
|
||||||
|
ApplyProjectSelection(context, event);
|
||||||
|
const std::string message = DescribeProjectPanelEvent(event);
|
||||||
|
context.SetStatus("Project", message);
|
||||||
|
entries.push_back(ProductEditorWorkspaceTraceEntry{ "project", std::move(message) });
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace XCEngine::UI::Editor::App
|
||||||
20
new_editor/app/Workspace/ProductEditorWorkspaceEventRouter.h
Normal file
20
new_editor/app/Workspace/ProductEditorWorkspaceEventRouter.h
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace XCEngine::UI::Editor::App {
|
||||||
|
|
||||||
|
class ProductEditorContext;
|
||||||
|
class ProductEditorWorkspace;
|
||||||
|
|
||||||
|
struct ProductEditorWorkspaceTraceEntry {
|
||||||
|
std::string channel = {};
|
||||||
|
std::string message = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<ProductEditorWorkspaceTraceEntry> ConsumeProductEditorWorkspaceEvents(
|
||||||
|
ProductEditorContext& context,
|
||||||
|
const ProductEditorWorkspace& workspace);
|
||||||
|
|
||||||
|
} // namespace XCEngine::UI::Editor::App
|
||||||
Reference in New Issue
Block a user