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/Core/ProductEditorContext.cpp
|
||||
app/Core/ProductEditorSession.cpp
|
||||
app/Panels/ProductConsolePanel.cpp
|
||||
app/Hierarchy/ProductHierarchyModel.cpp
|
||||
app/Icons/ProductBuiltInIcons.cpp
|
||||
app/Panels/ProductHierarchyPanel.cpp
|
||||
app/Panels/ProductInspectorPanel.cpp
|
||||
app/Panels/ProductProjectPanel.cpp
|
||||
app/Project/ProductProjectBrowserModel.cpp
|
||||
app/Shell/ProductShellAsset.cpp
|
||||
app/Workspace/ProductEditorWorkspace.cpp
|
||||
app/Workspace/ProductEditorWorkspaceEventRouter.cpp
|
||||
)
|
||||
|
||||
target_include_directories(XCUIEditorApp PRIVATE
|
||||
|
||||
@@ -344,6 +344,39 @@ std::string DescribeProjectPanelEvent(const App::ProductProjectPanel::Event& eve
|
||||
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
|
||||
|
||||
int Application::Run(HINSTANCE hInstance, int nCmdShow) {
|
||||
@@ -534,9 +567,8 @@ void Application::RenderFrame() {
|
||||
frameTrace.str());
|
||||
}
|
||||
ApplyHostCaptureRequests(shellFrame.result);
|
||||
for (const App::ProductProjectPanel::Event& event : m_editorWorkspace.GetProjectPanelEvents()) {
|
||||
LogRuntimeTrace("project", DescribeProjectPanelEvent(event));
|
||||
m_editorContext.SetStatus("Project", DescribeProjectPanelEvent(event));
|
||||
for (const App::ProductEditorWorkspaceTraceEntry& entry : m_editorWorkspace.GetTraceEntries()) {
|
||||
LogRuntimeTrace(entry.channel, entry.message);
|
||||
}
|
||||
ApplyHostedContentCaptureRequests();
|
||||
ApplyCurrentCursor();
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace XCEngine::UI::Editor::App {
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::UI::Editor::BuildEditorShellShortcutManager;
|
||||
constexpr std::size_t kMaxConsoleEntries = 256u;
|
||||
|
||||
std::string ComposeStatusText(
|
||||
std::string_view status,
|
||||
@@ -82,6 +83,14 @@ const ProductEditorSession& ProductEditorContext::GetSession() const {
|
||||
return m_session;
|
||||
}
|
||||
|
||||
void ProductEditorContext::SetSelection(ProductEditorSelectionState selection) {
|
||||
m_session.selection = std::move(selection);
|
||||
}
|
||||
|
||||
void ProductEditorContext::ClearSelection() {
|
||||
m_session.selection = {};
|
||||
}
|
||||
|
||||
UIEditorWorkspaceController& ProductEditorContext::GetWorkspaceController() {
|
||||
return m_workspaceController;
|
||||
}
|
||||
@@ -110,6 +119,9 @@ void ProductEditorContext::SetReadyStatus() {
|
||||
void ProductEditorContext::SetStatus(
|
||||
std::string status,
|
||||
std::string message) {
|
||||
if (m_lastStatus != status || m_lastMessage != message) {
|
||||
AppendConsoleEntry(status, message);
|
||||
}
|
||||
m_lastStatus = std::move(status);
|
||||
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(
|
||||
const UIEditorShellInteractionState& interactionState) const {
|
||||
std::ostringstream stream = {};
|
||||
|
||||
@@ -26,6 +26,8 @@ public:
|
||||
const std::string& GetValidationMessage() const;
|
||||
const EditorShellAsset& GetShellAsset() const;
|
||||
const ProductEditorSession& GetSession() const;
|
||||
void SetSelection(ProductEditorSelectionState selection);
|
||||
void ClearSelection();
|
||||
|
||||
UIEditorWorkspaceController& GetWorkspaceController();
|
||||
const UIEditorWorkspaceController& GetWorkspaceController() const;
|
||||
@@ -41,6 +43,8 @@ public:
|
||||
const UIEditorShellInteractionState& interactionState) const;
|
||||
|
||||
private:
|
||||
void AppendConsoleEntry(std::string channel, std::string message);
|
||||
|
||||
EditorShellAsset m_shellAsset = {};
|
||||
EditorShellAssetValidationResult m_shellValidation = {};
|
||||
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) {
|
||||
if (panelId == "hierarchy") {
|
||||
return ProductEditorActionRoute::Hierarchy;
|
||||
|
||||
@@ -25,16 +25,38 @@ enum class ProductEditorActionRoute : std::uint8_t {
|
||||
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 {
|
||||
std::filesystem::path repoRoot = {};
|
||||
std::filesystem::path projectRoot = {};
|
||||
std::string activePanelId = {};
|
||||
ProductEditorRuntimeMode runtimeMode = ProductEditorRuntimeMode::Edit;
|
||||
ProductEditorActionRoute activeRoute = ProductEditorActionRoute::None;
|
||||
ProductEditorSelectionState selection = {};
|
||||
std::vector<ProductEditorConsoleEntry> consoleEntries = {};
|
||||
};
|
||||
|
||||
std::string_view GetProductEditorRuntimeModeName(ProductEditorRuntimeMode mode);
|
||||
std::string_view GetProductEditorActionRouteName(ProductEditorActionRoute route);
|
||||
std::string_view GetProductEditorSelectionKindName(ProductEditorSelectionKind kind);
|
||||
|
||||
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 <algorithm>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
@@ -12,15 +14,25 @@ namespace XCEngine::UI::Editor::App {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::UI::UIColor;
|
||||
using ::XCEngine::UI::UIDrawList;
|
||||
using ::XCEngine::UI::UIInputEvent;
|
||||
using ::XCEngine::UI::UIInputEventType;
|
||||
using ::XCEngine::UI::UIPoint;
|
||||
using ::XCEngine::UI::UIPointerButton;
|
||||
using ::XCEngine::UI::UIRect;
|
||||
using Widgets::AppendUIEditorTreeViewBackground;
|
||||
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 float kDragThreshold = 4.0f;
|
||||
constexpr UIColor kDragPreviewColor(0.82f, 0.82f, 0.82f, 0.55f);
|
||||
|
||||
bool ContainsPoint(const UIRect& rect, const UIPoint& point) {
|
||||
return point.x >= rect.x &&
|
||||
@@ -29,12 +41,26 @@ bool ContainsPoint(const UIRect& rect, const UIPoint& point) {
|
||||
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(
|
||||
const UIRect& bounds,
|
||||
const std::vector<UIInputEvent>& inputEvents,
|
||||
bool allowInteraction,
|
||||
bool panelActive) {
|
||||
if (!allowInteraction) {
|
||||
bool panelActive,
|
||||
bool captureActive) {
|
||||
if (!allowInteraction && !captureActive) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -46,7 +72,7 @@ std::vector<UIInputEvent> FilterHierarchyInputEvents(
|
||||
case UIInputEventType::PointerButtonDown:
|
||||
case UIInputEventType::PointerButtonUp:
|
||||
case UIInputEventType::PointerWheel:
|
||||
if (ContainsPoint(bounds, event.position)) {
|
||||
if (captureActive || ContainsPoint(bounds, event.position)) {
|
||||
filteredEvents.push_back(event);
|
||||
}
|
||||
break;
|
||||
@@ -55,6 +81,10 @@ std::vector<UIInputEvent> FilterHierarchyInputEvents(
|
||||
break;
|
||||
case UIInputEventType::FocusGained:
|
||||
case UIInputEventType::FocusLost:
|
||||
if (panelActive || captureActive) {
|
||||
filteredEvents.push_back(event);
|
||||
}
|
||||
break;
|
||||
case UIInputEventType::KeyDown:
|
||||
case UIInputEventType::KeyUp:
|
||||
case UIInputEventType::Character:
|
||||
@@ -70,16 +100,41 @@ std::vector<UIInputEvent> FilterHierarchyInputEvents(
|
||||
return filteredEvents;
|
||||
}
|
||||
|
||||
::XCEngine::UI::UITextureHandle ResolveGameObjectIcon(
|
||||
const ProductBuiltInIcons* icons) {
|
||||
return icons != nullptr
|
||||
? icons->Resolve(ProductBuiltInIconKind::GameObject)
|
||||
: ::XCEngine::UI::UITextureHandle {};
|
||||
const Widgets::UIEditorTreeViewItem* ResolveHitItem(
|
||||
const Widgets::UIEditorTreeViewLayout& layout,
|
||||
const std::vector<Widgets::UIEditorTreeViewItem>& items,
|
||||
const UIPoint& point,
|
||||
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
|
||||
|
||||
void ProductHierarchyPanel::Initialize() {
|
||||
m_model = ProductHierarchyModel::BuildDefault();
|
||||
RebuildItems();
|
||||
}
|
||||
|
||||
@@ -99,35 +154,25 @@ const UIEditorPanelContentHostPanelState* ProductHierarchyPanel::FindMountedHier
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ProductHierarchyPanel::ResetTransientState() {
|
||||
m_frameEvents.clear();
|
||||
m_dragState.requestPointerCapture = false;
|
||||
m_dragState.requestPointerRelease = false;
|
||||
}
|
||||
|
||||
void ProductHierarchyPanel::RebuildItems() {
|
||||
const auto icon = ResolveGameObjectIcon(m_icons);
|
||||
const std::string previousSelection =
|
||||
m_selection.HasSelection() ? m_selection.GetSelectedId() : std::string();
|
||||
|
||||
m_treeItems = {
|
||||
{ "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_treeItems = m_model.BuildTreeItems(icon);
|
||||
m_expansion.Expand("player");
|
||||
m_expansion.Expand("environment");
|
||||
m_expansion.Expand("environment/props");
|
||||
m_expansion.Expand("props");
|
||||
|
||||
if (!previousSelection.empty()) {
|
||||
for (const Widgets::UIEditorTreeViewItem& item : m_treeItems) {
|
||||
if (item.itemId == previousSelection) {
|
||||
m_selection.SetSelection(previousSelection);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!previousSelection.empty() && m_model.ContainsNode(previousSelection)) {
|
||||
m_selection.SetSelection(previousSelection);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_treeItems.empty()) {
|
||||
@@ -135,16 +180,295 @@ void ProductHierarchyPanel::RebuildItems() {
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
const UIEditorPanelContentHostFrame& contentHostFrame,
|
||||
const std::vector<UIInputEvent>& inputEvents,
|
||||
bool allowInteraction,
|
||||
bool panelActive) {
|
||||
ResetTransientState();
|
||||
|
||||
const UIEditorPanelContentHostPanelState* panelState =
|
||||
FindMountedHierarchyPanel(contentHostFrame);
|
||||
if (panelState == nullptr) {
|
||||
m_visible = false;
|
||||
m_treeFrame = {};
|
||||
m_dragState = {};
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -153,14 +477,25 @@ void ProductHierarchyPanel::Update(
|
||||
}
|
||||
|
||||
m_visible = true;
|
||||
const std::vector<UIInputEvent> interactionEvents =
|
||||
BuildInteractionInputEvents(
|
||||
inputEvents,
|
||||
panelState->bounds,
|
||||
allowInteraction,
|
||||
panelActive);
|
||||
m_treeFrame = UpdateUIEditorTreeViewInteraction(
|
||||
m_treeInteractionState,
|
||||
m_selection,
|
||||
m_expansion,
|
||||
panelState->bounds,
|
||||
m_treeItems,
|
||||
FilterHierarchyInputEvents(panelState->bounds, inputEvents, allowInteraction, panelActive),
|
||||
interactionEvents,
|
||||
BuildProductTreeViewMetrics());
|
||||
ProcessDragAndFrameEvents(
|
||||
inputEvents,
|
||||
panelState->bounds,
|
||||
allowInteraction,
|
||||
panelActive);
|
||||
}
|
||||
|
||||
void ProductHierarchyPanel::Append(UIDrawList& drawList) const {
|
||||
@@ -184,6 +519,50 @@ void ProductHierarchyPanel::Append(UIDrawList& drawList) const {
|
||||
m_treeItems,
|
||||
palette,
|
||||
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
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Hierarchy/ProductHierarchyModel.h"
|
||||
|
||||
#include <XCEditor/Collections/UIEditorTreeViewInteraction.h>
|
||||
#include <XCEditor/Shell/UIEditorPanelContentHost.h>
|
||||
|
||||
@@ -7,6 +9,8 @@
|
||||
#include <XCEngine/UI/Widgets/UIExpansionModel.h>
|
||||
#include <XCEngine/UI/Widgets/UISelectionModel.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace XCEngine::UI::Editor::App {
|
||||
@@ -15,6 +19,21 @@ class ProductBuiltInIcons;
|
||||
|
||||
class ProductHierarchyPanel {
|
||||
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 SetBuiltInIcons(const ProductBuiltInIcons* icons);
|
||||
void Update(
|
||||
@@ -23,18 +42,54 @@ public:
|
||||
bool allowInteraction,
|
||||
bool panelActive);
|
||||
void Append(::XCEngine::UI::UIDrawList& drawList) const;
|
||||
bool WantsHostPointerCapture() const;
|
||||
bool WantsHostPointerRelease() const;
|
||||
bool HasActivePointerCapture() const;
|
||||
const std::vector<Event>& GetFrameEvents() const;
|
||||
|
||||
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 UIEditorPanelContentHostFrame& contentHostFrame) const;
|
||||
void ResetTransientState();
|
||||
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;
|
||||
ProductHierarchyModel m_model = {};
|
||||
std::vector<Widgets::UIEditorTreeViewItem> m_treeItems = {};
|
||||
::XCEngine::UI::Widgets::UISelectionModel m_selection = {};
|
||||
::XCEngine::UI::Widgets::UIExpansionModel m_expansion = {};
|
||||
UIEditorTreeViewInteractionState m_treeInteractionState = {};
|
||||
UIEditorTreeViewInteractionFrame m_treeFrame = {};
|
||||
std::vector<Event> m_frameEvents = {};
|
||||
DragState m_dragState = {};
|
||||
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 <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
|
||||
#include <windows.h>
|
||||
@@ -32,7 +30,6 @@ using Widgets::AppendUIEditorTreeViewBackground;
|
||||
using Widgets::AppendUIEditorTreeViewForeground;
|
||||
|
||||
constexpr std::string_view kProjectPanelId = "project";
|
||||
constexpr std::string_view kAssetsRootId = "Assets";
|
||||
constexpr std::size_t kInvalidLayoutIndex = static_cast<std::size_t>(-1);
|
||||
|
||||
constexpr float kBrowserHeaderHeight = 24.0f;
|
||||
@@ -102,122 +99,6 @@ float MeasureTextWidth(
|
||||
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(
|
||||
const UIRect& bounds,
|
||||
const std::vector<UIInputEvent>& inputEvents,
|
||||
@@ -303,35 +184,6 @@ float ClampNavigationWidth(float value, float totalWidth) {
|
||||
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(
|
||||
UIDrawList& drawList,
|
||||
const UIRect& previewRect,
|
||||
@@ -371,18 +223,14 @@ void AppendTilePreview(
|
||||
} // namespace
|
||||
|
||||
void ProductProjectPanel::Initialize(const std::filesystem::path& repoRoot) {
|
||||
m_assetsRootPath = (repoRoot / "project/Assets").lexically_normal();
|
||||
RefreshFolderTree();
|
||||
m_browserModel.Initialize(repoRoot);
|
||||
SyncCurrentFolderSelection();
|
||||
RefreshAssetList();
|
||||
}
|
||||
|
||||
void ProductProjectPanel::SetBuiltInIcons(const ProductBuiltInIcons* icons) {
|
||||
m_icons = icons;
|
||||
if (!m_assetsRootPath.empty()) {
|
||||
RefreshFolderTree();
|
||||
SyncCurrentFolderSelection();
|
||||
}
|
||||
m_browserModel.SetFolderIcon(ResolveFolderIcon(m_icons));
|
||||
SyncCurrentFolderSelection();
|
||||
}
|
||||
|
||||
void ProductProjectPanel::SetTextMeasurer(const UIEditorTextMeasurer* textMeasurer) {
|
||||
@@ -411,24 +259,12 @@ const std::vector<ProductProjectPanel::Event>& ProductProjectPanel::GetFrameEven
|
||||
|
||||
const ProductProjectPanel::FolderEntry* ProductProjectPanel::FindFolderEntry(
|
||||
std::string_view itemId) const {
|
||||
for (const FolderEntry& entry : m_folderEntries) {
|
||||
if (entry.itemId == itemId) {
|
||||
return &entry;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
return m_browserModel.FindFolderEntry(itemId);
|
||||
}
|
||||
|
||||
const ProductProjectPanel::AssetEntry* ProductProjectPanel::FindAssetEntry(
|
||||
std::string_view itemId) const {
|
||||
for (const AssetEntry& entry : m_assetEntries) {
|
||||
if (entry.itemId == itemId) {
|
||||
return &entry;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
return m_browserModel.FindAssetEntry(itemId);
|
||||
}
|
||||
|
||||
const UIEditorPanelContentHostPanelState* ProductProjectPanel::FindMountedProjectPanel(
|
||||
@@ -444,6 +280,9 @@ const UIEditorPanelContentHostPanelState* ProductProjectPanel::FindMountedProjec
|
||||
|
||||
ProductProjectPanel::Layout ProductProjectPanel::BuildLayout(const UIRect& bounds) const {
|
||||
Layout layout = {};
|
||||
const auto& assetEntries = m_browserModel.GetAssetEntries();
|
||||
const std::vector<ProductProjectPanel::BrowserModel::BreadcrumbSegment> breadcrumbSegments =
|
||||
m_browserModel.BuildBreadcrumbSegments();
|
||||
const float dividerThickness = ResolveUIEditorDockHostMetrics().splitterMetrics.thickness;
|
||||
layout.bounds = UIRect(
|
||||
bounds.x,
|
||||
@@ -496,9 +335,7 @@ ProductProjectPanel::Layout ProductProjectPanel::BuildLayout(const UIRect& bound
|
||||
const float headerRight =
|
||||
layout.browserHeaderRect.x + layout.browserHeaderRect.width - kHeaderHorizontalPadding;
|
||||
float nextItemX = layout.browserHeaderRect.x + kHeaderHorizontalPadding;
|
||||
std::string cumulativeFolderId = {};
|
||||
const std::vector<std::string> segments = BuildBreadcrumbSegments(m_currentFolderId);
|
||||
for (std::size_t index = 0u; index < segments.size(); ++index) {
|
||||
for (std::size_t index = 0u; index < breadcrumbSegments.size(); ++index) {
|
||||
if (index > 0u) {
|
||||
const float separatorWidth =
|
||||
MeasureTextWidth(m_textMeasurer, ">", kHeaderFontSize);
|
||||
@@ -519,15 +356,9 @@ ProductProjectPanel::Layout ProductProjectPanel::BuildLayout(const UIRect& bound
|
||||
nextItemX += separatorWidth + kBreadcrumbSpacing;
|
||||
}
|
||||
|
||||
if (index == 0u) {
|
||||
cumulativeFolderId = segments[index];
|
||||
} else {
|
||||
cumulativeFolderId += "/";
|
||||
cumulativeFolderId += segments[index];
|
||||
}
|
||||
|
||||
const ProductProjectPanel::BrowserModel::BreadcrumbSegment& segment = breadcrumbSegments[index];
|
||||
const float labelWidth =
|
||||
MeasureTextWidth(m_textMeasurer, segments[index], kHeaderFontSize);
|
||||
MeasureTextWidth(m_textMeasurer, segment.label, kHeaderFontSize);
|
||||
const float itemWidth = labelWidth + kBreadcrumbItemPaddingX * 2.0f;
|
||||
const float availableWidth = headerRight - nextItemX;
|
||||
if (availableWidth <= 0.0f) {
|
||||
@@ -535,16 +366,16 @@ ProductProjectPanel::Layout ProductProjectPanel::BuildLayout(const UIRect& bound
|
||||
}
|
||||
|
||||
layout.breadcrumbItems.push_back({
|
||||
segments[index],
|
||||
cumulativeFolderId,
|
||||
segment.label,
|
||||
segment.targetFolderId,
|
||||
UIRect(
|
||||
nextItemX,
|
||||
breadcrumbY,
|
||||
ClampNonNegative((std::min)(itemWidth, availableWidth)),
|
||||
breadcrumbRowHeight),
|
||||
false,
|
||||
index + 1u != segments.size(),
|
||||
index + 1u == segments.size()
|
||||
!segment.current,
|
||||
segment.current
|
||||
});
|
||||
nextItemX += itemWidth + kBreadcrumbSpacing;
|
||||
}
|
||||
@@ -557,8 +388,8 @@ ProductProjectPanel::Layout ProductProjectPanel::BuildLayout(const UIRect& bound
|
||||
columnCount = 1;
|
||||
}
|
||||
|
||||
layout.assetTiles.reserve(m_assetEntries.size());
|
||||
for (std::size_t index = 0; index < m_assetEntries.size(); ++index) {
|
||||
layout.assetTiles.reserve(assetEntries.size());
|
||||
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 row = static_cast<int>(index / static_cast<std::size_t>(columnCount));
|
||||
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;
|
||||
}
|
||||
|
||||
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() {
|
||||
EnsureValidCurrentFolder();
|
||||
ExpandFolderAncestors(m_currentFolderId);
|
||||
m_folderSelection.SetSelection(m_currentFolderId);
|
||||
const std::string& currentFolderId = m_browserModel.GetCurrentFolderId();
|
||||
if (currentFolderId.empty()) {
|
||||
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) {
|
||||
if (itemId.empty() || FindFolderEntry(itemId) == nullptr || itemId == m_currentFolderId) {
|
||||
if (!m_browserModel.NavigateToFolder(itemId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_currentFolderId = std::string(itemId);
|
||||
SyncCurrentFolderSelection();
|
||||
m_assetSelection.ClearSelection();
|
||||
m_hoveredAssetItemId.clear();
|
||||
m_lastPrimaryClickedAssetId.clear();
|
||||
RefreshAssetList();
|
||||
EmitEvent(EventKind::FolderNavigated, source, FindFolderEntry(m_currentFolderId));
|
||||
EmitEvent(EventKind::FolderNavigated, source, FindFolderEntry(m_browserModel.GetCurrentFolderId()));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -699,7 +476,7 @@ void ProductProjectPanel::EmitEvent(
|
||||
event.source = source;
|
||||
event.itemId = folder->itemId;
|
||||
event.absolutePath = folder->absolutePath;
|
||||
event.displayName = PathToUtf8String(folder->absolutePath.filename());
|
||||
event.displayName = folder->label;
|
||||
event.directory = true;
|
||||
m_frameEvents.push_back(std::move(event));
|
||||
}
|
||||
@@ -731,55 +508,6 @@ void ProductProjectPanel::EmitSelectionClearedEvent(EventSource source) {
|
||||
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() {
|
||||
m_treeFrame = {};
|
||||
m_frameEvents.clear();
|
||||
@@ -811,10 +539,9 @@ void ProductProjectPanel::Update(
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_treeItems.empty()) {
|
||||
RefreshFolderTree();
|
||||
if (m_browserModel.GetTreeItems().empty()) {
|
||||
m_browserModel.Refresh();
|
||||
SyncCurrentFolderSelection();
|
||||
RefreshAssetList();
|
||||
}
|
||||
|
||||
m_visible = true;
|
||||
@@ -836,13 +563,13 @@ void ProductProjectPanel::Update(
|
||||
m_folderSelection,
|
||||
m_folderExpansion,
|
||||
m_layout.treeRect,
|
||||
m_treeItems,
|
||||
m_browserModel.GetTreeItems(),
|
||||
treeEvents,
|
||||
treeMetrics);
|
||||
|
||||
if (m_treeFrame.result.selectionChanged &&
|
||||
!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);
|
||||
m_layout = BuildLayout(panelState->bounds);
|
||||
}
|
||||
@@ -871,9 +598,10 @@ void ProductProjectPanel::Update(
|
||||
m_splitterDragging || ContainsPoint(m_layout.dividerRect, event.position);
|
||||
m_hoveredBreadcrumbIndex = HitTestBreadcrumbItem(event.position);
|
||||
const std::size_t hoveredAssetIndex = HitTestAssetTile(event.position);
|
||||
const auto& assetEntries = m_browserModel.GetAssetEntries();
|
||||
m_hoveredAssetItemId =
|
||||
hoveredAssetIndex < m_assetEntries.size()
|
||||
? m_assetEntries[hoveredAssetIndex].itemId
|
||||
hoveredAssetIndex < assetEntries.size()
|
||||
? assetEntries[hoveredAssetIndex].itemId
|
||||
: std::string();
|
||||
break;
|
||||
}
|
||||
@@ -902,8 +630,9 @@ void ProductProjectPanel::Update(
|
||||
break;
|
||||
}
|
||||
|
||||
const auto& assetEntries = m_browserModel.GetAssetEntries();
|
||||
const std::size_t hitIndex = HitTestAssetTile(event.position);
|
||||
if (hitIndex >= m_assetEntries.size()) {
|
||||
if (hitIndex >= assetEntries.size()) {
|
||||
if (m_assetSelection.HasSelection()) {
|
||||
m_assetSelection.ClearSelection();
|
||||
EmitSelectionClearedEvent(EventSource::Background);
|
||||
@@ -911,7 +640,7 @@ void ProductProjectPanel::Update(
|
||||
break;
|
||||
}
|
||||
|
||||
const AssetEntry& assetEntry = m_assetEntries[hitIndex];
|
||||
const AssetEntry& assetEntry = assetEntries[hitIndex];
|
||||
const bool alreadySelected = m_assetSelection.IsSelected(assetEntry.itemId);
|
||||
const bool selectionChanged = m_assetSelection.SetSelection(assetEntry.itemId);
|
||||
if (selectionChanged) {
|
||||
@@ -945,13 +674,17 @@ void ProductProjectPanel::Update(
|
||||
|
||||
if (event.pointerButton == ::XCEngine::UI::UIPointerButton::Right &&
|
||||
ContainsPoint(m_layout.gridRect, event.position)) {
|
||||
const auto& assetEntries = m_browserModel.GetAssetEntries();
|
||||
const std::size_t hitIndex = HitTestAssetTile(event.position);
|
||||
if (hitIndex >= m_assetEntries.size()) {
|
||||
EmitEvent(EventKind::ContextMenuRequested, EventSource::Background, static_cast<const AssetEntry*>(nullptr));
|
||||
if (hitIndex >= assetEntries.size()) {
|
||||
EmitEvent(
|
||||
EventKind::ContextMenuRequested,
|
||||
EventSource::Background,
|
||||
static_cast<const AssetEntry*>(nullptr));
|
||||
break;
|
||||
}
|
||||
|
||||
const AssetEntry& assetEntry = m_assetEntries[hitIndex];
|
||||
const AssetEntry& assetEntry = assetEntries[hitIndex];
|
||||
if (!m_assetSelection.IsSelected(assetEntry.itemId)) {
|
||||
m_assetSelection.SetSelection(assetEntry.itemId);
|
||||
EmitEvent(EventKind::AssetSelected, EventSource::GridSecondary, &assetEntry);
|
||||
@@ -1000,6 +733,8 @@ void ProductProjectPanel::Append(UIDrawList& drawList) const {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& assetEntries = m_browserModel.GetAssetEntries();
|
||||
|
||||
drawList.AddFilledRect(m_layout.bounds, kSurfaceColor);
|
||||
drawList.AddFilledRect(m_layout.leftPaneRect, kPaneColor);
|
||||
drawList.AddFilledRect(m_layout.rightPaneRect, kPaneColor);
|
||||
@@ -1021,7 +756,7 @@ void ProductProjectPanel::Append(UIDrawList& drawList) const {
|
||||
AppendUIEditorTreeViewBackground(
|
||||
drawList,
|
||||
m_treeFrame.layout,
|
||||
m_treeItems,
|
||||
m_browserModel.GetTreeItems(),
|
||||
m_folderSelection,
|
||||
m_treeInteractionState.treeViewState,
|
||||
treePalette,
|
||||
@@ -1029,7 +764,7 @@ void ProductProjectPanel::Append(UIDrawList& drawList) const {
|
||||
AppendUIEditorTreeViewForeground(
|
||||
drawList,
|
||||
m_treeFrame.layout,
|
||||
m_treeItems,
|
||||
m_browserModel.GetTreeItems(),
|
||||
treePalette,
|
||||
treeMetrics);
|
||||
|
||||
@@ -1055,11 +790,11 @@ void ProductProjectPanel::Append(UIDrawList& drawList) const {
|
||||
drawList.PopClipRect();
|
||||
|
||||
for (const AssetTileLayout& tile : m_layout.assetTiles) {
|
||||
if (tile.itemIndex >= m_assetEntries.size()) {
|
||||
if (tile.itemIndex >= assetEntries.size()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const AssetEntry& assetEntry = m_assetEntries[tile.itemIndex];
|
||||
const AssetEntry& assetEntry = assetEntries[tile.itemIndex];
|
||||
const bool selected = m_assetSelection.IsSelected(assetEntry.itemId);
|
||||
const bool hovered = m_hoveredAssetItemId == assetEntry.itemId;
|
||||
|
||||
@@ -1084,7 +819,7 @@ void ProductProjectPanel::Append(UIDrawList& drawList) const {
|
||||
drawList.PopClipRect();
|
||||
}
|
||||
|
||||
if (m_assetEntries.empty()) {
|
||||
if (assetEntries.empty()) {
|
||||
const UIRect messageRect(
|
||||
m_layout.gridRect.x,
|
||||
m_layout.gridRect.y,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Project/ProductProjectBrowserModel.h"
|
||||
|
||||
#include <XCEditor/Collections/UIEditorTreeViewInteraction.h>
|
||||
#include <XCEditor/Foundation/UIEditorTextMeasurement.h>
|
||||
#include <XCEditor/Shell/UIEditorPanelContentHost.h>
|
||||
@@ -70,17 +72,9 @@ public:
|
||||
const std::vector<Event>& GetFrameEvents() const;
|
||||
|
||||
private:
|
||||
struct FolderEntry {
|
||||
std::string itemId = {};
|
||||
std::filesystem::path absolutePath = {};
|
||||
};
|
||||
|
||||
struct AssetEntry {
|
||||
std::string itemId = {};
|
||||
std::filesystem::path absolutePath = {};
|
||||
std::string displayName = {};
|
||||
bool directory = false;
|
||||
};
|
||||
using BrowserModel = ::XCEngine::UI::Editor::App::Project::ProductProjectBrowserModel;
|
||||
using FolderEntry = BrowserModel::FolderEntry;
|
||||
using AssetEntry = BrowserModel::AssetEntry;
|
||||
|
||||
struct BreadcrumbItemLayout {
|
||||
std::string label = {};
|
||||
@@ -118,10 +112,6 @@ private:
|
||||
Layout BuildLayout(const ::XCEngine::UI::UIRect& bounds) const;
|
||||
std::size_t HitTestBreadcrumbItem(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();
|
||||
bool NavigateToFolder(std::string_view itemId, EventSource source = EventSource::None);
|
||||
void EmitEvent(EventKind kind, EventSource source, const FolderEntry* folder);
|
||||
@@ -129,10 +119,7 @@ private:
|
||||
void EmitSelectionClearedEvent(EventSource source);
|
||||
void ResetTransientFrames();
|
||||
|
||||
std::filesystem::path m_assetsRootPath = {};
|
||||
std::vector<FolderEntry> m_folderEntries = {};
|
||||
std::vector<Widgets::UIEditorTreeViewItem> m_treeItems = {};
|
||||
std::vector<AssetEntry> m_assetEntries = {};
|
||||
BrowserModel m_browserModel = {};
|
||||
const ProductBuiltInIcons* m_icons = nullptr;
|
||||
const ::XCEngine::UI::Editor::UIEditorTextMeasurer* m_textMeasurer = nullptr;
|
||||
::XCEngine::UI::Widgets::UISelectionModel m_folderSelection = {};
|
||||
@@ -142,7 +129,6 @@ private:
|
||||
UIEditorTreeViewInteractionFrame m_treeFrame = {};
|
||||
std::vector<Event> m_frameEvents = {};
|
||||
Layout m_layout = {};
|
||||
std::string m_currentFolderId = {};
|
||||
std::string m_hoveredAssetItemId = {};
|
||||
std::string m_lastPrimaryClickedAssetId = {};
|
||||
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 },
|
||||
{ "scene", "Scene", UIEditorPanelPresentationKind::HostedContent, false, false, false },
|
||||
{ "game", "Game", UIEditorPanelPresentationKind::HostedContent, false, false, false },
|
||||
{ "inspector", "Inspector", UIEditorPanelPresentationKind::Placeholder, true, false, false },
|
||||
{ "console", "Console", UIEditorPanelPresentationKind::Placeholder, true, false, false },
|
||||
{ "inspector", "Inspector", UIEditorPanelPresentationKind::HostedContent, true, false, false },
|
||||
{ "console", "Console", UIEditorPanelPresentationKind::HostedContent, true, false, false },
|
||||
{ "project", "Project", UIEditorPanelPresentationKind::HostedContent, false, false, false }
|
||||
};
|
||||
return registry;
|
||||
@@ -434,8 +434,8 @@ UIEditorShellInteractionDefinition BuildBaseShellDefinition() {
|
||||
BuildHostedContentPresentation("hierarchy"),
|
||||
BuildHostedContentPresentation("scene"),
|
||||
BuildHostedContentPresentation("game"),
|
||||
BuildPlaceholderPresentation("inspector"),
|
||||
BuildPlaceholderPresentation("console"),
|
||||
BuildHostedContentPresentation("inspector"),
|
||||
BuildHostedContentPresentation("console"),
|
||||
BuildHostedContentPresentation("project")
|
||||
};
|
||||
return definition;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "ProductEditorWorkspace.h"
|
||||
|
||||
#include "Workspace/ProductEditorWorkspaceEventRouter.h"
|
||||
|
||||
#include <XCEditor/Shell/UIEditorShellCompose.h>
|
||||
#include <XCEditor/Foundation/UIEditorTheme.h>
|
||||
|
||||
@@ -90,6 +92,7 @@ void ProductEditorWorkspace::Initialize(
|
||||
void ProductEditorWorkspace::Shutdown() {
|
||||
m_shellFrame = {};
|
||||
m_shellInteractionState = {};
|
||||
m_traceEntries.clear();
|
||||
m_builtInIcons.Shutdown();
|
||||
}
|
||||
|
||||
@@ -131,6 +134,13 @@ void ProductEditorWorkspace::Update(
|
||||
hostedContentEvents,
|
||||
!m_shellFrame.result.workspaceInputSuppressed,
|
||||
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 {
|
||||
@@ -145,7 +155,9 @@ void ProductEditorWorkspace::Append(UIDrawList& drawList) const {
|
||||
m_shellInteractionState.composeState,
|
||||
palette.shellPalette,
|
||||
metrics.shellMetrics);
|
||||
m_consolePanel.Append(drawList);
|
||||
m_hierarchyPanel.Append(drawList);
|
||||
m_inspectorPanel.Append(drawList);
|
||||
m_projectPanel.Append(drawList);
|
||||
AppendShellPopups(drawList, m_shellFrame, palette, metrics);
|
||||
}
|
||||
@@ -158,6 +170,14 @@ const UIEditorShellInteractionState& ProductEditorWorkspace::GetShellInteraction
|
||||
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 {
|
||||
return m_projectPanel.GetFrameEvents();
|
||||
}
|
||||
@@ -176,15 +196,19 @@ Widgets::UIEditorDockHostCursorKind ProductEditorWorkspace::GetDockCursorKind()
|
||||
}
|
||||
|
||||
bool ProductEditorWorkspace::WantsHostPointerCapture() const {
|
||||
return m_projectPanel.WantsHostPointerCapture();
|
||||
return m_hierarchyPanel.WantsHostPointerCapture() ||
|
||||
m_projectPanel.WantsHostPointerCapture();
|
||||
}
|
||||
|
||||
bool ProductEditorWorkspace::WantsHostPointerRelease() const {
|
||||
return m_projectPanel.WantsHostPointerRelease();
|
||||
return (m_hierarchyPanel.WantsHostPointerRelease() ||
|
||||
m_projectPanel.WantsHostPointerRelease()) &&
|
||||
!HasHostedContentCapture();
|
||||
}
|
||||
|
||||
bool ProductEditorWorkspace::HasHostedContentCapture() const {
|
||||
return m_projectPanel.HasActivePointerCapture();
|
||||
return m_hierarchyPanel.HasActivePointerCapture() ||
|
||||
m_projectPanel.HasActivePointerCapture();
|
||||
}
|
||||
|
||||
bool ProductEditorWorkspace::HasShellInteractiveCapture() const {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "Core/ProductEditorContext.h"
|
||||
#include "Panels/ProductConsolePanel.h"
|
||||
#include "Icons/ProductBuiltInIcons.h"
|
||||
#include "Panels/ProductHierarchyPanel.h"
|
||||
#include "Panels/ProductInspectorPanel.h"
|
||||
#include "Panels/ProductProjectPanel.h"
|
||||
#include "Workspace/ProductEditorWorkspaceEventRouter.h"
|
||||
|
||||
#include <Host/NativeRenderer.h>
|
||||
|
||||
@@ -33,6 +36,8 @@ public:
|
||||
|
||||
const UIEditorShellInteractionFrame& GetShellFrame() 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::string& GetBuiltInIconError() const;
|
||||
|
||||
@@ -46,10 +51,13 @@ public:
|
||||
|
||||
private:
|
||||
ProductBuiltInIcons m_builtInIcons = {};
|
||||
ProductConsolePanel m_consolePanel = {};
|
||||
ProductHierarchyPanel m_hierarchyPanel = {};
|
||||
ProductInspectorPanel m_inspectorPanel = {};
|
||||
ProductProjectPanel m_projectPanel = {};
|
||||
UIEditorShellInteractionState m_shellInteractionState = {};
|
||||
UIEditorShellInteractionFrame m_shellFrame = {};
|
||||
std::vector<ProductEditorWorkspaceTraceEntry> m_traceEntries = {};
|
||||
};
|
||||
|
||||
} // 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