Refactor new editor scene runtime ownership
This commit is contained in:
@@ -285,7 +285,9 @@ if(XCENGINE_BUILD_XCUI_EDITOR_APP)
|
||||
)
|
||||
|
||||
set(XCUI_EDITOR_APP_SUPPORT_SOURCES
|
||||
app/Scene/EditorSceneRuntime.cpp
|
||||
app/Internal/EmbeddedPngLoader.cpp
|
||||
app/Scene/EditorSceneBridge.cpp
|
||||
)
|
||||
|
||||
set(XCUI_EDITOR_APP_PLATFORM_SOURCES
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "State/EditorContext.h"
|
||||
|
||||
#include <XCEditor/App/EditorPanelIds.h>
|
||||
#include <XCEditor/Foundation/UIEditorTheme.h>
|
||||
|
||||
namespace XCEngine::UI::Editor::App::Internal {
|
||||
@@ -121,6 +122,7 @@ void EditorShellRuntime::Update(
|
||||
const Widgets::UIEditorDockHostLayout preUpdateDockLayout =
|
||||
m_shellFrame.workspaceInteractionFrame.dockHostFrame.layout;
|
||||
|
||||
m_hierarchyPanel.SetSceneRuntime(&context.GetSceneRuntime());
|
||||
context.BindEditCommandRoutes(&m_hierarchyPanel, &m_projectPanel);
|
||||
context.SyncSessionFromWorkspace(workspaceController);
|
||||
UIEditorShellInteractionDefinition definition =
|
||||
@@ -175,15 +177,16 @@ void EditorShellRuntime::Update(
|
||||
m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame,
|
||||
hostedContentEvents,
|
||||
!m_shellFrame.result.workspaceInputSuppressed,
|
||||
activePanelId == "hierarchy");
|
||||
activePanelId == kHierarchyPanelId);
|
||||
m_projectPanel.Update(
|
||||
m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame,
|
||||
hostedContentEvents,
|
||||
!m_shellFrame.result.workspaceInputSuppressed,
|
||||
activePanelId == "project");
|
||||
activePanelId == kProjectPanelId);
|
||||
m_traceEntries = SyncWorkspaceEvents(context, *this);
|
||||
m_inspectorPanel.Update(
|
||||
context.GetSession(),
|
||||
context.GetSceneRuntime(),
|
||||
m_shellFrame.workspaceInteractionFrame.composeFrame.contentHostFrame);
|
||||
m_consolePanel.Update(
|
||||
context.GetSession(),
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include "Features/Project/ProjectPanel.h"
|
||||
#include "Composition/EditorShellRuntime.h"
|
||||
|
||||
#include <XCEditor/App/EditorPanelIds.h>
|
||||
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
@@ -114,15 +116,20 @@ void ApplyHierarchySelection(
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.itemId.empty()) {
|
||||
context.ClearSelection();
|
||||
const EditorSceneRuntime& sceneRuntime = context.GetSceneRuntime();
|
||||
if (!sceneRuntime.HasSceneSelection()) {
|
||||
if (context.GetSession().selection.kind == EditorSelectionKind::HierarchyNode) {
|
||||
context.ClearSelection();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
EditorSelectionState selection = {};
|
||||
selection.kind = EditorSelectionKind::HierarchyNode;
|
||||
selection.itemId = event.itemId;
|
||||
selection.displayName = event.label.empty() ? event.itemId : event.label;
|
||||
selection.itemId = sceneRuntime.GetSelectedItemId();
|
||||
selection.displayName = sceneRuntime.GetSelectedDisplayName().empty()
|
||||
? selection.itemId
|
||||
: sceneRuntime.GetSelectedDisplayName();
|
||||
context.SetSelection(std::move(selection));
|
||||
}
|
||||
|
||||
@@ -166,14 +173,14 @@ std::vector<WorkspaceTraceEntry> SyncWorkspaceEvents(
|
||||
ApplyHierarchySelection(context, event);
|
||||
const std::string message = DescribeHierarchyPanelEvent(event);
|
||||
context.SetStatus("Hierarchy", message);
|
||||
entries.push_back(WorkspaceTraceEntry{ "hierarchy", std::move(message) });
|
||||
entries.push_back(WorkspaceTraceEntry{ std::string(kHierarchyPanelId), std::move(message) });
|
||||
}
|
||||
|
||||
for (const ProjectPanel::Event& event : runtime.GetProjectPanelEvents()) {
|
||||
ApplyProjectSelection(context, event);
|
||||
const std::string message = DescribeProjectPanelEvent(event);
|
||||
context.SetStatus("Project", message);
|
||||
entries.push_back(WorkspaceTraceEntry{ "project", std::move(message) });
|
||||
entries.push_back(WorkspaceTraceEntry{ std::string(kProjectPanelId), std::move(message) });
|
||||
}
|
||||
|
||||
return entries;
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
#include "HierarchyPanelInternal.h"
|
||||
|
||||
#include "Scene/EditorSceneRuntime.h"
|
||||
|
||||
#include <XCEditor/Fields/UIEditorFieldStyle.h>
|
||||
#include <XCEditor/Fields/UIEditorTextField.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace XCEngine::UI::Editor::App {
|
||||
|
||||
using namespace HierarchyPanelInternal;
|
||||
namespace TreeDrag = TreeItemDragDrop;
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -26,11 +32,23 @@ UIEditorHostCommandDispatchResult BuildDispatchResult(
|
||||
return result;
|
||||
}
|
||||
|
||||
bool HasValidBounds(const UIRect& bounds) {
|
||||
return bounds.width > 0.0f && bounds.height > 0.0f;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void HierarchyPanel::Initialize() {
|
||||
m_model = HierarchyModel::BuildDefault();
|
||||
RebuildItems();
|
||||
SyncModelFromScene();
|
||||
}
|
||||
|
||||
void HierarchyPanel::SetSceneRuntime(EditorSceneRuntime* sceneRuntime) {
|
||||
if (m_sceneRuntime == sceneRuntime) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_sceneRuntime = sceneRuntime;
|
||||
SyncModelFromScene();
|
||||
}
|
||||
|
||||
void HierarchyPanel::SetBuiltInIcons(const BuiltInIcons* icons) {
|
||||
@@ -42,6 +60,7 @@ void HierarchyPanel::ResetInteractionState() {
|
||||
m_treeInteractionState = {};
|
||||
m_treeFrame = {};
|
||||
m_dragState = {};
|
||||
ClearRenameState();
|
||||
ResetTransientState();
|
||||
}
|
||||
|
||||
@@ -58,39 +77,89 @@ const UIEditorPanelContentHostPanelState* HierarchyPanel::FindMountedHierarchyPa
|
||||
|
||||
void HierarchyPanel::ResetTransientState() {
|
||||
m_frameEvents.clear();
|
||||
m_dragState.requestPointerCapture = false;
|
||||
m_dragState.requestPointerRelease = false;
|
||||
TreeDrag::ResetTransientRequests(m_dragState);
|
||||
}
|
||||
|
||||
void HierarchyPanel::SyncModelFromScene() {
|
||||
if (m_sceneRuntime != nullptr) {
|
||||
m_sceneRuntime->RefreshScene();
|
||||
}
|
||||
|
||||
const HierarchyModel sceneModel =
|
||||
HierarchyModel::BuildFromScene(
|
||||
m_sceneRuntime != nullptr
|
||||
? m_sceneRuntime->GetActiveScene()
|
||||
: nullptr);
|
||||
if (!m_model.HasSameTree(sceneModel) || m_treeItems.empty()) {
|
||||
m_model = sceneModel;
|
||||
if (m_sceneRuntime != nullptr && !m_model.Empty()) {
|
||||
m_sceneRuntime->EnsureSceneSelection();
|
||||
}
|
||||
RebuildItems();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_sceneRuntime != nullptr && !m_model.Empty()) {
|
||||
m_sceneRuntime->EnsureSceneSelection();
|
||||
}
|
||||
|
||||
SyncTreeSelectionFromSceneRuntime();
|
||||
}
|
||||
|
||||
void HierarchyPanel::RebuildItems() {
|
||||
const auto icon = ResolveGameObjectIcon(m_icons);
|
||||
const std::string previousSelection =
|
||||
m_selection.HasSelection() ? m_selection.GetSelectedId() : std::string();
|
||||
|
||||
m_treeItems = m_model.BuildTreeItems(icon);
|
||||
m_expansion.Expand("player");
|
||||
m_expansion.Expand("environment");
|
||||
m_expansion.Expand("props");
|
||||
SyncTreeSelectionFromSceneRuntime();
|
||||
}
|
||||
|
||||
if (!previousSelection.empty() && m_model.ContainsNode(previousSelection)) {
|
||||
m_selection.SetSelection(previousSelection);
|
||||
void HierarchyPanel::SyncTreeSelectionFromSceneRuntime() {
|
||||
if (m_sceneRuntime == nullptr) {
|
||||
m_treeSelection.ClearSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_treeItems.empty()) {
|
||||
m_selection.SetSelection(m_treeItems.front().itemId);
|
||||
const std::string selectedItemId = m_sceneRuntime->GetSelectedItemId();
|
||||
if (!selectedItemId.empty() && m_model.ContainsNode(selectedItemId)) {
|
||||
m_treeSelection.SetSelection(selectedItemId);
|
||||
return;
|
||||
}
|
||||
|
||||
m_selection.ClearSelection();
|
||||
m_treeSelection.ClearSelection();
|
||||
}
|
||||
|
||||
void HierarchyPanel::SyncSceneRuntimeSelectionFromTree() {
|
||||
if (m_sceneRuntime == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_treeSelection.HasSelection()) {
|
||||
if (!m_sceneRuntime->SetSelection(m_treeSelection.GetSelectedId())) {
|
||||
m_sceneRuntime->RefreshScene();
|
||||
}
|
||||
} else {
|
||||
m_sceneRuntime->ClearSelection();
|
||||
}
|
||||
|
||||
if (!m_model.Empty()) {
|
||||
m_sceneRuntime->EnsureSceneSelection();
|
||||
}
|
||||
|
||||
SyncTreeSelectionFromSceneRuntime();
|
||||
}
|
||||
|
||||
const HierarchyNode* HierarchyPanel::GetSelectedNode() const {
|
||||
if (!m_selection.HasSelection()) {
|
||||
if (m_sceneRuntime != nullptr) {
|
||||
const std::string selectedItemId = m_sceneRuntime->GetSelectedItemId();
|
||||
if (!selectedItemId.empty()) {
|
||||
return m_model.FindNode(selectedItemId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_treeSelection.HasSelection()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return m_model.FindNode(m_selection.GetSelectedId());
|
||||
return m_model.FindNode(m_treeSelection.GetSelectedId());
|
||||
}
|
||||
|
||||
void HierarchyPanel::EmitSelectionEvent() {
|
||||
@@ -127,6 +196,155 @@ void HierarchyPanel::EmitRenameRequestedEvent(std::string_view itemId) {
|
||||
m_frameEvents.push_back(std::move(event));
|
||||
}
|
||||
|
||||
void HierarchyPanel::ClearRenameState() {
|
||||
m_renameState = {};
|
||||
m_renameFrame = {};
|
||||
m_pendingRenameItemId.clear();
|
||||
}
|
||||
|
||||
void HierarchyPanel::QueueRenameSession(std::string_view itemId) {
|
||||
if (itemId.empty() || !m_model.ContainsNode(itemId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_renameState.active && m_renameState.itemId == itemId) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_pendingRenameItemId = std::string(itemId);
|
||||
}
|
||||
|
||||
bool HierarchyPanel::TryStartQueuedRenameSession(
|
||||
const Widgets::UIEditorTreeViewLayout& layout) {
|
||||
if (m_pendingRenameItemId.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const HierarchyNode* node = m_model.FindNode(m_pendingRenameItemId);
|
||||
if (node == nullptr) {
|
||||
m_pendingRenameItemId.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
const UIRect bounds = BuildRenameBounds(m_pendingRenameItemId, layout);
|
||||
if (!HasValidBounds(bounds)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Widgets::UIEditorTextFieldMetrics textFieldMetrics =
|
||||
BuildUIEditorInlineRenameTextFieldMetrics(
|
||||
bounds,
|
||||
BuildUIEditorPropertyGridTextFieldMetrics(
|
||||
ResolveUIEditorPropertyGridMetrics(),
|
||||
ResolveUIEditorTextFieldMetrics()));
|
||||
|
||||
UIEditorInlineRenameSessionRequest request = {};
|
||||
request.beginSession = true;
|
||||
request.itemId = m_pendingRenameItemId;
|
||||
request.initialText = node->label;
|
||||
request.bounds = bounds;
|
||||
m_renameFrame = UpdateUIEditorInlineRenameSession(
|
||||
m_renameState,
|
||||
request,
|
||||
{},
|
||||
textFieldMetrics);
|
||||
|
||||
if (m_renameFrame.result.sessionStarted) {
|
||||
m_pendingRenameItemId.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void HierarchyPanel::UpdateRenameSession(
|
||||
const std::vector<UIInputEvent>& inputEvents,
|
||||
const Widgets::UIEditorTreeViewLayout& layout) {
|
||||
if (!m_renameState.active) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_model.ContainsNode(m_renameState.itemId)) {
|
||||
ClearRenameState();
|
||||
return;
|
||||
}
|
||||
|
||||
const UIRect bounds = BuildRenameBounds(m_renameState.itemId, layout);
|
||||
if (!HasValidBounds(bounds)) {
|
||||
ClearRenameState();
|
||||
return;
|
||||
}
|
||||
|
||||
const Widgets::UIEditorTextFieldMetrics textFieldMetrics =
|
||||
BuildUIEditorInlineRenameTextFieldMetrics(
|
||||
bounds,
|
||||
BuildUIEditorPropertyGridTextFieldMetrics(
|
||||
ResolveUIEditorPropertyGridMetrics(),
|
||||
ResolveUIEditorTextFieldMetrics()));
|
||||
|
||||
UIEditorInlineRenameSessionRequest request = {};
|
||||
request.itemId = m_renameState.itemId;
|
||||
request.initialText = m_renameState.textFieldSpec.value;
|
||||
request.bounds = bounds;
|
||||
m_renameFrame = UpdateUIEditorInlineRenameSession(
|
||||
m_renameState,
|
||||
request,
|
||||
inputEvents,
|
||||
textFieldMetrics);
|
||||
|
||||
if (!m_renameFrame.result.sessionCommitted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_renameFrame.result.valueChanged &&
|
||||
m_sceneRuntime != nullptr) {
|
||||
m_sceneRuntime->RenameGameObject(
|
||||
m_renameFrame.result.itemId,
|
||||
m_renameFrame.result.valueAfter);
|
||||
}
|
||||
|
||||
SyncModelFromScene();
|
||||
EmitSelectionEvent();
|
||||
}
|
||||
|
||||
void HierarchyPanel::SyncTreeFocusState(
|
||||
const std::vector<UIInputEvent>& inputEvents) {
|
||||
for (const UIInputEvent& event : inputEvents) {
|
||||
if (event.type == ::XCEngine::UI::UIInputEventType::FocusGained) {
|
||||
m_treeInteractionState.treeViewState.focused = true;
|
||||
} else if (event.type == ::XCEngine::UI::UIInputEventType::FocusLost) {
|
||||
m_treeInteractionState.treeViewState.focused = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UIRect HierarchyPanel::BuildRenameBounds(
|
||||
std::string_view itemId,
|
||||
const Widgets::UIEditorTreeViewLayout& layout) const {
|
||||
if (itemId.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::size_t visibleIndex =
|
||||
FindVisibleIndexForItemId(layout, m_treeItems, itemId);
|
||||
if (visibleIndex == UIEditorTreeViewInvalidIndex ||
|
||||
visibleIndex >= layout.rowRects.size() ||
|
||||
visibleIndex >= layout.labelRects.size()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const Widgets::UIEditorTextFieldMetrics hostedMetrics =
|
||||
BuildUIEditorPropertyGridTextFieldMetrics(
|
||||
ResolveUIEditorPropertyGridMetrics(),
|
||||
ResolveUIEditorTextFieldMetrics());
|
||||
const UIRect& rowRect = layout.rowRects[visibleIndex];
|
||||
const UIRect& labelRect = layout.labelRects[visibleIndex];
|
||||
const float x = (std::max)(rowRect.x, labelRect.x - hostedMetrics.valueTextInsetX);
|
||||
const float right = rowRect.x + rowRect.width - 8.0f;
|
||||
const float width = (std::max)(120.0f, right - x);
|
||||
return UIRect(x, rowRect.y, width, rowRect.height);
|
||||
}
|
||||
|
||||
bool HierarchyPanel::WantsHostPointerCapture() const {
|
||||
return m_dragState.requestPointerCapture;
|
||||
}
|
||||
@@ -136,7 +354,7 @@ bool HierarchyPanel::WantsHostPointerRelease() const {
|
||||
}
|
||||
|
||||
bool HierarchyPanel::HasActivePointerCapture() const {
|
||||
return m_dragState.dragging;
|
||||
return TreeDrag::HasActivePointerCapture(m_dragState);
|
||||
}
|
||||
|
||||
const std::vector<HierarchyPanel::Event>& HierarchyPanel::GetFrameEvents() const {
|
||||
@@ -193,20 +411,27 @@ UIEditorHostCommandDispatchResult HierarchyPanel::DispatchEditCommand(
|
||||
|
||||
const std::string selectedNodeId = selectedNode->nodeId;
|
||||
const std::string selectedNodeLabel = selectedNode->label;
|
||||
if (m_sceneRuntime == nullptr) {
|
||||
return BuildDispatchResult(false, "Hierarchy scene runtime is unavailable.");
|
||||
}
|
||||
|
||||
if (commandId == "edit.rename") {
|
||||
QueueRenameSession(selectedNodeId);
|
||||
EmitRenameRequestedEvent(selectedNodeId);
|
||||
if (m_visible) {
|
||||
TryStartQueuedRenameSession(m_treeFrame.layout);
|
||||
}
|
||||
return BuildDispatchResult(
|
||||
true,
|
||||
"Hierarchy rename requested for '" + selectedNodeLabel + "'.");
|
||||
}
|
||||
|
||||
if (commandId == "edit.delete") {
|
||||
if (!m_model.DeleteNode(selectedNodeId)) {
|
||||
if (!m_sceneRuntime->DeleteGameObject(selectedNodeId)) {
|
||||
return BuildDispatchResult(false, "Failed to delete the selected hierarchy object.");
|
||||
}
|
||||
|
||||
RebuildItems();
|
||||
SyncModelFromScene();
|
||||
EmitSelectionEvent();
|
||||
return BuildDispatchResult(
|
||||
true,
|
||||
@@ -214,13 +439,13 @@ UIEditorHostCommandDispatchResult HierarchyPanel::DispatchEditCommand(
|
||||
}
|
||||
|
||||
if (commandId == "edit.duplicate") {
|
||||
const std::string duplicatedNodeId = m_model.DuplicateNode(selectedNodeId);
|
||||
const std::string duplicatedNodeId =
|
||||
m_sceneRuntime->DuplicateGameObject(selectedNodeId);
|
||||
if (duplicatedNodeId.empty()) {
|
||||
return BuildDispatchResult(false, "Failed to duplicate the selected hierarchy object.");
|
||||
}
|
||||
|
||||
RebuildItems();
|
||||
m_selection.SetSelection(duplicatedNodeId);
|
||||
SyncModelFromScene();
|
||||
EmitSelectionEvent();
|
||||
|
||||
const HierarchyNode* duplicatedNode = m_model.FindNode(duplicatedNodeId);
|
||||
@@ -234,115 +459,6 @@ UIEditorHostCommandDispatchResult HierarchyPanel::DispatchEditCommand(
|
||||
return BuildDispatchResult(false, "Hierarchy does not expose this edit command.");
|
||||
}
|
||||
|
||||
std::vector<UIInputEvent> HierarchyPanel::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,
|
||||
ResolveUIEditorTreeViewMetrics());
|
||||
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 HierarchyPanel::ProcessDragAndFrameEvents(
|
||||
const std::vector<UIInputEvent>& inputEvents,
|
||||
const UIRect& bounds,
|
||||
@@ -356,125 +472,70 @@ void HierarchyPanel::ProcessDragAndFrameEvents(
|
||||
HasActivePointerCapture());
|
||||
|
||||
if (m_treeFrame.result.selectionChanged) {
|
||||
SyncSceneRuntimeSelectionFromTree();
|
||||
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 HierarchyNode* node = m_model.FindNode(event.itemId); node != nullptr) {
|
||||
event.label = node->label;
|
||||
|
||||
struct HierarchyTreeDragCallbacks {
|
||||
::XCEngine::UI::Widgets::UISelectionModel& selection;
|
||||
HierarchyModel& model;
|
||||
EditorSceneRuntime* sceneRuntime = nullptr;
|
||||
|
||||
bool IsItemSelected(std::string_view itemId) const {
|
||||
return selection.IsSelected(itemId);
|
||||
}
|
||||
m_frameEvents.push_back(std::move(event));
|
||||
|
||||
bool SelectDraggedItem(std::string_view itemId) {
|
||||
return selection.SetSelection(std::string(itemId));
|
||||
}
|
||||
|
||||
bool CanDropOnItem(
|
||||
std::string_view draggedItemId,
|
||||
std::string_view targetItemId) const {
|
||||
return model.CanReparent(draggedItemId, targetItemId);
|
||||
}
|
||||
|
||||
bool CanDropToRoot(std::string_view draggedItemId) const {
|
||||
return model.GetParentId(draggedItemId).has_value();
|
||||
}
|
||||
|
||||
bool CommitDropOnItem(
|
||||
std::string_view draggedItemId,
|
||||
std::string_view targetItemId) {
|
||||
return sceneRuntime != nullptr &&
|
||||
sceneRuntime->ReparentGameObject(draggedItemId, targetItemId);
|
||||
}
|
||||
|
||||
bool CommitDropToRoot(std::string_view draggedItemId) {
|
||||
return sceneRuntime != nullptr &&
|
||||
sceneRuntime->MoveGameObjectToRoot(draggedItemId);
|
||||
}
|
||||
} callbacks{ m_treeSelection, m_model, m_sceneRuntime };
|
||||
|
||||
const TreeDrag::ProcessResult dragResult =
|
||||
TreeDrag::ProcessInputEvents(
|
||||
m_dragState,
|
||||
m_treeFrame.layout,
|
||||
m_treeItems,
|
||||
filteredEvents,
|
||||
bounds,
|
||||
callbacks,
|
||||
kDragThreshold);
|
||||
if (dragResult.selectionForced) {
|
||||
SyncSceneRuntimeSelectionFromTree();
|
||||
EmitSelectionEvent();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
if (dragResult.dropCommitted) {
|
||||
SyncModelFromScene();
|
||||
m_treeFrame.layout = Widgets::BuildUIEditorTreeViewLayout(
|
||||
bounds,
|
||||
m_treeItems,
|
||||
m_expansion,
|
||||
ResolveUIEditorTreeViewMetrics());
|
||||
EmitReparentEvent(
|
||||
dragResult.droppedToRoot ? EventKind::MovedToRoot : EventKind::Reparented,
|
||||
dragResult.draggedItemId,
|
||||
dragResult.dropTargetItemId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,28 +552,67 @@ void HierarchyPanel::Update(
|
||||
m_visible = false;
|
||||
m_treeFrame = {};
|
||||
m_dragState = {};
|
||||
ClearRenameState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_treeItems.empty()) {
|
||||
RebuildItems();
|
||||
SyncModelFromScene();
|
||||
if (m_renameState.active && !m_model.ContainsNode(m_renameState.itemId)) {
|
||||
ClearRenameState();
|
||||
} else if (!m_pendingRenameItemId.empty() &&
|
||||
!m_model.ContainsNode(m_pendingRenameItemId)) {
|
||||
m_pendingRenameItemId.clear();
|
||||
}
|
||||
|
||||
m_visible = true;
|
||||
const std::vector<UIInputEvent> interactionEvents =
|
||||
BuildInteractionInputEvents(
|
||||
inputEvents,
|
||||
const std::vector<UIInputEvent> filteredEvents = FilterHierarchyInputEvents(
|
||||
panelState->bounds,
|
||||
inputEvents,
|
||||
allowInteraction,
|
||||
panelActive,
|
||||
HasActivePointerCapture());
|
||||
SyncTreeFocusState(filteredEvents);
|
||||
|
||||
const Widgets::UIEditorTreeViewMetrics treeMetrics =
|
||||
ResolveUIEditorTreeViewMetrics();
|
||||
const Widgets::UIEditorTreeViewLayout layout =
|
||||
Widgets::BuildUIEditorTreeViewLayout(
|
||||
panelState->bounds,
|
||||
allowInteraction,
|
||||
panelActive);
|
||||
m_treeItems,
|
||||
m_expansion,
|
||||
treeMetrics);
|
||||
|
||||
if (m_renameState.active || !m_pendingRenameItemId.empty()) {
|
||||
m_treeFrame.layout = layout;
|
||||
m_treeFrame.result = {};
|
||||
TryStartQueuedRenameSession(layout);
|
||||
UpdateRenameSession(filteredEvents, layout);
|
||||
return;
|
||||
}
|
||||
|
||||
const std::vector<UIInputEvent> interactionEvents =
|
||||
TreeDrag::BuildInteractionInputEvents(
|
||||
m_dragState,
|
||||
layout,
|
||||
m_treeItems,
|
||||
filteredEvents,
|
||||
kDragThreshold);
|
||||
m_treeFrame = UpdateUIEditorTreeViewInteraction(
|
||||
m_treeInteractionState,
|
||||
m_selection,
|
||||
m_treeSelection,
|
||||
m_expansion,
|
||||
panelState->bounds,
|
||||
m_treeItems,
|
||||
interactionEvents,
|
||||
ResolveUIEditorTreeViewMetrics());
|
||||
treeMetrics);
|
||||
if (m_treeFrame.result.renameRequested &&
|
||||
!m_treeFrame.result.renameItemId.empty()) {
|
||||
QueueRenameSession(m_treeFrame.result.renameItemId);
|
||||
EmitRenameRequestedEvent(m_treeFrame.result.renameItemId);
|
||||
TryStartQueuedRenameSession(m_treeFrame.layout);
|
||||
return;
|
||||
}
|
||||
|
||||
ProcessDragAndFrameEvents(
|
||||
inputEvents,
|
||||
panelState->bounds,
|
||||
@@ -533,7 +633,7 @@ void HierarchyPanel::Append(UIDrawList& drawList) const {
|
||||
drawList,
|
||||
m_treeFrame.layout,
|
||||
m_treeItems,
|
||||
m_selection,
|
||||
m_treeSelection,
|
||||
m_treeInteractionState.treeViewState,
|
||||
palette,
|
||||
metrics);
|
||||
@@ -544,6 +644,33 @@ void HierarchyPanel::Append(UIDrawList& drawList) const {
|
||||
palette,
|
||||
metrics);
|
||||
|
||||
if (m_renameState.active) {
|
||||
const Widgets::UIEditorTextFieldPalette textFieldPalette =
|
||||
BuildUIEditorPropertyGridTextFieldPalette(
|
||||
ResolveUIEditorPropertyGridPalette(),
|
||||
ResolveUIEditorTextFieldPalette());
|
||||
const Widgets::UIEditorTextFieldMetrics textFieldMetrics =
|
||||
BuildUIEditorInlineRenameTextFieldMetrics(
|
||||
BuildRenameBounds(m_renameState.itemId, m_treeFrame.layout),
|
||||
BuildUIEditorPropertyGridTextFieldMetrics(
|
||||
ResolveUIEditorPropertyGridMetrics(),
|
||||
ResolveUIEditorTextFieldMetrics()));
|
||||
Widgets::AppendUIEditorTextFieldBackground(
|
||||
drawList,
|
||||
m_renameFrame.layout,
|
||||
m_renameState.textFieldSpec,
|
||||
m_renameState.textFieldInteraction.textFieldState,
|
||||
textFieldPalette,
|
||||
textFieldMetrics);
|
||||
Widgets::AppendUIEditorTextFieldForeground(
|
||||
drawList,
|
||||
m_renameFrame.layout,
|
||||
m_renameState.textFieldSpec,
|
||||
m_renameState.textFieldInteraction.textFieldState,
|
||||
textFieldPalette,
|
||||
textFieldMetrics);
|
||||
}
|
||||
|
||||
if (!m_dragState.dragging || !m_dragState.validDropTarget) {
|
||||
return;
|
||||
}
|
||||
@@ -557,7 +684,7 @@ void HierarchyPanel::Append(UIDrawList& drawList) const {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::size_t visibleIndex = FindVisibleIndexForItemId(
|
||||
const std::size_t visibleIndex = TreeDrag::FindVisibleIndexForItemId(
|
||||
m_treeFrame.layout,
|
||||
m_treeItems,
|
||||
m_dragState.dropTargetItemId);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "Composition/EditorEditCommandRoute.h"
|
||||
#include "Features/Shared/TreeItemDragDrop.h"
|
||||
#include "HierarchyModel.h"
|
||||
|
||||
#include <XCEditor/App/EditorEditCommandRoute.h>
|
||||
#include <XCEditor/Collections/UIEditorInlineRenameSession.h>
|
||||
#include <XCEditor/Collections/UIEditorTreeViewInteraction.h>
|
||||
#include <XCEditor/Panels/UIEditorPanelContentHost.h>
|
||||
|
||||
@@ -17,6 +19,7 @@
|
||||
namespace XCEngine::UI::Editor::App {
|
||||
|
||||
class BuiltInIcons;
|
||||
class EditorSceneRuntime;
|
||||
|
||||
class HierarchyPanel final : public EditorEditCommandRoute {
|
||||
public:
|
||||
@@ -36,6 +39,7 @@ public:
|
||||
};
|
||||
|
||||
void Initialize();
|
||||
void SetSceneRuntime(EditorSceneRuntime* sceneRuntime);
|
||||
void SetBuiltInIcons(const BuiltInIcons* icons);
|
||||
void ResetInteractionState();
|
||||
void Update(
|
||||
@@ -54,22 +58,10 @@ public:
|
||||
std::string_view commandId) override;
|
||||
|
||||
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 SyncModelFromScene();
|
||||
void RebuildItems();
|
||||
void ProcessDragAndFrameEvents(
|
||||
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
|
||||
@@ -77,27 +69,40 @@ private:
|
||||
bool allowInteraction,
|
||||
bool panelActive);
|
||||
const HierarchyNode* GetSelectedNode() const;
|
||||
void SyncTreeSelectionFromSceneRuntime();
|
||||
void SyncSceneRuntimeSelectionFromTree();
|
||||
void EmitSelectionEvent();
|
||||
void EmitReparentEvent(
|
||||
EventKind kind,
|
||||
std::string itemId,
|
||||
std::string targetItemId);
|
||||
void EmitRenameRequestedEvent(std::string_view itemId);
|
||||
std::vector<::XCEngine::UI::UIInputEvent> BuildInteractionInputEvents(
|
||||
void ClearRenameState();
|
||||
void QueueRenameSession(std::string_view itemId);
|
||||
bool TryStartQueuedRenameSession(
|
||||
const Widgets::UIEditorTreeViewLayout& layout);
|
||||
void UpdateRenameSession(
|
||||
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents,
|
||||
const ::XCEngine::UI::UIRect& bounds,
|
||||
bool allowInteraction,
|
||||
bool panelActive) const;
|
||||
const Widgets::UIEditorTreeViewLayout& layout);
|
||||
void SyncTreeFocusState(
|
||||
const std::vector<::XCEngine::UI::UIInputEvent>& inputEvents);
|
||||
::XCEngine::UI::UIRect BuildRenameBounds(
|
||||
std::string_view itemId,
|
||||
const Widgets::UIEditorTreeViewLayout& layout) const;
|
||||
|
||||
const BuiltInIcons* m_icons = nullptr;
|
||||
EditorSceneRuntime* m_sceneRuntime = nullptr;
|
||||
HierarchyModel m_model = {};
|
||||
std::vector<Widgets::UIEditorTreeViewItem> m_treeItems = {};
|
||||
::XCEngine::UI::Widgets::UISelectionModel m_selection = {};
|
||||
::XCEngine::UI::Widgets::UISelectionModel m_treeSelection = {};
|
||||
::XCEngine::UI::Widgets::UIExpansionModel m_expansion = {};
|
||||
UIEditorTreeViewInteractionState m_treeInteractionState = {};
|
||||
UIEditorTreeViewInteractionFrame m_treeFrame = {};
|
||||
UIEditorInlineRenameSessionState m_renameState = {};
|
||||
UIEditorInlineRenameSessionFrame m_renameFrame = {};
|
||||
std::string m_pendingRenameItemId = {};
|
||||
std::vector<Event> m_frameEvents = {};
|
||||
DragState m_dragState = {};
|
||||
TreeItemDragDrop::State m_dragState = {};
|
||||
bool m_visible = false;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "InspectorPanel.h"
|
||||
|
||||
#include "Scene/EditorSceneRuntime.h"
|
||||
|
||||
#include <XCEditor/App/EditorPanelIds.h>
|
||||
#include <XCEditor/Foundation/UIEditorTheme.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -13,7 +16,6 @@ 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;
|
||||
@@ -55,32 +57,15 @@ const UIEditorPanelContentHostPanelState* InspectorPanel::FindMountedInspectorPa
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void InspectorPanel::BuildPresentation(const EditorSession& session) {
|
||||
void InspectorPanel::BuildPresentation(
|
||||
const EditorSession& session,
|
||||
const EditorSceneRuntime& sceneRuntime) {
|
||||
m_sections.clear();
|
||||
m_title.clear();
|
||||
m_subtitle.clear();
|
||||
m_hasSelection = false;
|
||||
|
||||
switch (session.selection.kind) {
|
||||
case EditorSelectionKind::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 EditorSelectionKind::ProjectItem: {
|
||||
if (session.selection.kind == EditorSelectionKind::ProjectItem) {
|
||||
m_hasSelection = true;
|
||||
m_title = session.selection.displayName.empty()
|
||||
? (session.selection.directory ? std::string("Folder") : std::string("Asset"))
|
||||
@@ -102,19 +87,50 @@ void InspectorPanel::BuildPresentation(const EditorSession& session) {
|
||||
{ "Path", PathToUtf8String(session.selection.absolutePath) }
|
||||
};
|
||||
m_sections.push_back(std::move(location));
|
||||
break;
|
||||
return;
|
||||
}
|
||||
|
||||
case EditorSelectionKind::None:
|
||||
default:
|
||||
m_title = "Nothing selected";
|
||||
m_subtitle = "Select a hierarchy item or project asset.";
|
||||
break;
|
||||
if (session.selection.kind == EditorSelectionKind::HierarchyNode &&
|
||||
sceneRuntime.HasSceneSelection()) {
|
||||
const auto* selectedGameObject = sceneRuntime.GetSelectedGameObject();
|
||||
m_hasSelection = true;
|
||||
m_title = sceneRuntime.GetSelectedDisplayName().empty()
|
||||
? std::string("GameObject")
|
||||
: sceneRuntime.GetSelectedDisplayName();
|
||||
m_subtitle = "GameObject";
|
||||
|
||||
Section identity = {};
|
||||
identity.title = "Identity";
|
||||
identity.rows = {
|
||||
{ "Type", "GameObject" },
|
||||
{ "Name", m_title },
|
||||
{ "Id", sceneRuntime.GetSelectedItemId() }
|
||||
};
|
||||
m_sections.push_back(std::move(identity));
|
||||
|
||||
if (selectedGameObject != nullptr) {
|
||||
Section hierarchy = {};
|
||||
hierarchy.title = "Hierarchy";
|
||||
hierarchy.rows = {
|
||||
{ "Children", std::to_string(selectedGameObject->GetChildCount()) },
|
||||
{ "Parent", selectedGameObject->GetParent() != nullptr
|
||||
? (selectedGameObject->GetParent()->GetName().empty()
|
||||
? std::string("GameObject")
|
||||
: selectedGameObject->GetParent()->GetName())
|
||||
: std::string("Scene Root") }
|
||||
};
|
||||
m_sections.push_back(std::move(hierarchy));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
m_title = "Nothing selected";
|
||||
m_subtitle = "Select a hierarchy item or project asset.";
|
||||
}
|
||||
|
||||
void InspectorPanel::Update(
|
||||
const EditorSession& session,
|
||||
const EditorSceneRuntime& sceneRuntime,
|
||||
const UIEditorPanelContentHostFrame& contentHostFrame) {
|
||||
const UIEditorPanelContentHostPanelState* panelState =
|
||||
FindMountedInspectorPanel(contentHostFrame);
|
||||
@@ -130,7 +146,7 @@ void InspectorPanel::Update(
|
||||
|
||||
m_visible = true;
|
||||
m_bounds = panelState->bounds;
|
||||
BuildPresentation(session);
|
||||
BuildPresentation(session, sceneRuntime);
|
||||
}
|
||||
|
||||
void InspectorPanel::Append(UIDrawList& drawList) const {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "State/EditorSession.h"
|
||||
#include <XCEditor/App/EditorSession.h>
|
||||
|
||||
#include <XCEditor/Panels/UIEditorPanelContentHost.h>
|
||||
|
||||
@@ -11,10 +11,13 @@
|
||||
|
||||
namespace XCEngine::UI::Editor::App {
|
||||
|
||||
class EditorSceneRuntime;
|
||||
|
||||
class InspectorPanel {
|
||||
public:
|
||||
void Update(
|
||||
const EditorSession& session,
|
||||
const EditorSceneRuntime& sceneRuntime,
|
||||
const UIEditorPanelContentHostFrame& contentHostFrame);
|
||||
void Append(::XCEngine::UI::UIDrawList& drawList) const;
|
||||
|
||||
@@ -31,7 +34,9 @@ private:
|
||||
|
||||
const UIEditorPanelContentHostPanelState* FindMountedInspectorPanel(
|
||||
const UIEditorPanelContentHostFrame& contentHostFrame) const;
|
||||
void BuildPresentation(const EditorSession& session);
|
||||
void BuildPresentation(
|
||||
const EditorSession& session,
|
||||
const EditorSceneRuntime& sceneRuntime);
|
||||
|
||||
bool m_visible = false;
|
||||
bool m_hasSelection = false;
|
||||
|
||||
183
new_editor/app/Scene/EditorSceneRuntime.cpp
Normal file
183
new_editor/app/Scene/EditorSceneRuntime.cpp
Normal file
@@ -0,0 +1,183 @@
|
||||
#include "Scene/EditorSceneRuntime.h"
|
||||
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
#include <XCEngine/Scene/Scene.h>
|
||||
|
||||
namespace XCEngine::UI::Editor::App {
|
||||
|
||||
namespace {
|
||||
|
||||
using ::XCEngine::Components::GameObject;
|
||||
using ::XCEngine::Components::Scene;
|
||||
|
||||
std::string ResolveGameObjectDisplayName(const GameObject& gameObject) {
|
||||
return gameObject.GetName().empty()
|
||||
? std::string("GameObject")
|
||||
: gameObject.GetName();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool EditorSceneRuntime::Initialize(const std::filesystem::path& projectRoot) {
|
||||
m_projectRoot = projectRoot;
|
||||
m_startupSceneResult = EnsureEditorStartupScene(projectRoot);
|
||||
RefreshScene();
|
||||
return m_startupSceneResult.ready;
|
||||
}
|
||||
|
||||
void EditorSceneRuntime::RefreshScene() {
|
||||
if (!HasValidSelection()) {
|
||||
m_selectedGameObjectId.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void EditorSceneRuntime::EnsureSceneSelection() {
|
||||
if (HasValidSelection()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SelectFirstAvailableGameObject();
|
||||
}
|
||||
|
||||
const EditorStartupSceneResult& EditorSceneRuntime::GetStartupResult() const {
|
||||
return m_startupSceneResult;
|
||||
}
|
||||
|
||||
Scene* EditorSceneRuntime::GetActiveScene() const {
|
||||
return GetActiveEditorScene();
|
||||
}
|
||||
|
||||
bool EditorSceneRuntime::HasSceneSelection() const {
|
||||
return HasValidSelection();
|
||||
}
|
||||
|
||||
std::optional<GameObject::ID> EditorSceneRuntime::GetSelectedGameObjectId() const {
|
||||
return HasValidSelection() ? m_selectedGameObjectId : std::nullopt;
|
||||
}
|
||||
|
||||
std::string EditorSceneRuntime::GetSelectedItemId() const {
|
||||
const std::optional<GameObject::ID> selectedId = GetSelectedGameObjectId();
|
||||
return selectedId.has_value()
|
||||
? MakeEditorGameObjectItemId(selectedId.value())
|
||||
: std::string();
|
||||
}
|
||||
|
||||
std::string EditorSceneRuntime::GetSelectedDisplayName() const {
|
||||
const GameObject* gameObject = GetSelectedGameObject();
|
||||
return gameObject != nullptr
|
||||
? ResolveGameObjectDisplayName(*gameObject)
|
||||
: std::string();
|
||||
}
|
||||
|
||||
const GameObject* EditorSceneRuntime::GetSelectedGameObject() const {
|
||||
if (!m_selectedGameObjectId.has_value()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Scene* scene = GetActiveScene();
|
||||
return scene != nullptr
|
||||
? scene->FindByID(m_selectedGameObjectId.value())
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
bool EditorSceneRuntime::SetSelection(std::string_view itemId) {
|
||||
const std::optional<GameObject::ID> gameObjectId =
|
||||
ParseEditorGameObjectItemId(itemId);
|
||||
if (!gameObjectId.has_value()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SetSelection(gameObjectId.value());
|
||||
}
|
||||
|
||||
bool EditorSceneRuntime::SetSelection(GameObject::ID id) {
|
||||
if (id == GameObject::INVALID_ID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Scene* scene = GetActiveScene();
|
||||
if (scene == nullptr || scene->FindByID(id) == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool changed =
|
||||
!m_selectedGameObjectId.has_value() ||
|
||||
m_selectedGameObjectId.value() != id;
|
||||
m_selectedGameObjectId = id;
|
||||
return changed;
|
||||
}
|
||||
|
||||
void EditorSceneRuntime::ClearSelection() {
|
||||
m_selectedGameObjectId.reset();
|
||||
}
|
||||
|
||||
GameObject* EditorSceneRuntime::FindGameObject(std::string_view itemId) const {
|
||||
return FindEditorGameObject(itemId);
|
||||
}
|
||||
|
||||
bool EditorSceneRuntime::RenameGameObject(
|
||||
std::string_view itemId,
|
||||
std::string_view newName) {
|
||||
const bool renamed = RenameEditorGameObject(itemId, newName);
|
||||
RefreshScene();
|
||||
return renamed;
|
||||
}
|
||||
|
||||
bool EditorSceneRuntime::DeleteGameObject(std::string_view itemId) {
|
||||
const bool deleted = DeleteEditorGameObject(itemId);
|
||||
RefreshScene();
|
||||
EnsureSceneSelection();
|
||||
return deleted;
|
||||
}
|
||||
|
||||
std::string EditorSceneRuntime::DuplicateGameObject(std::string_view itemId) {
|
||||
const std::string duplicatedItemId = DuplicateEditorGameObject(itemId);
|
||||
if (!duplicatedItemId.empty()) {
|
||||
SetSelection(duplicatedItemId);
|
||||
} else {
|
||||
RefreshScene();
|
||||
}
|
||||
return duplicatedItemId;
|
||||
}
|
||||
|
||||
bool EditorSceneRuntime::ReparentGameObject(
|
||||
std::string_view itemId,
|
||||
std::string_view parentItemId) {
|
||||
const bool reparented =
|
||||
ReparentEditorGameObject(itemId, parentItemId);
|
||||
RefreshScene();
|
||||
return reparented;
|
||||
}
|
||||
|
||||
bool EditorSceneRuntime::MoveGameObjectToRoot(std::string_view itemId) {
|
||||
const bool moved = MoveEditorGameObjectToRoot(itemId);
|
||||
RefreshScene();
|
||||
return moved;
|
||||
}
|
||||
|
||||
bool EditorSceneRuntime::HasValidSelection() const {
|
||||
return GetSelectedGameObject() != nullptr;
|
||||
}
|
||||
|
||||
bool EditorSceneRuntime::SelectFirstAvailableGameObject() {
|
||||
Scene* scene = GetActiveScene();
|
||||
if (scene == nullptr) {
|
||||
m_selectedGameObjectId.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
for (GameObject* root : scene->GetRootGameObjects()) {
|
||||
if (root == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
m_selectedGameObjectId = root->GetID();
|
||||
return true;
|
||||
}
|
||||
|
||||
m_selectedGameObjectId.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace XCEngine::UI::Editor::App
|
||||
|
||||
59
new_editor/app/Scene/EditorSceneRuntime.h
Normal file
59
new_editor/app/Scene/EditorSceneRuntime.h
Normal file
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include "Scene/EditorSceneBridge.h"
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace XCEngine::Components {
|
||||
|
||||
class GameObject;
|
||||
class Scene;
|
||||
|
||||
} // namespace XCEngine::Components
|
||||
|
||||
namespace XCEngine::UI::Editor::App {
|
||||
|
||||
class EditorSceneRuntime {
|
||||
public:
|
||||
bool Initialize(const std::filesystem::path& projectRoot);
|
||||
|
||||
void RefreshScene();
|
||||
void EnsureSceneSelection();
|
||||
|
||||
const EditorStartupSceneResult& GetStartupResult() const;
|
||||
::XCEngine::Components::Scene* GetActiveScene() const;
|
||||
|
||||
bool HasSceneSelection() const;
|
||||
std::optional<::XCEngine::Components::GameObject::ID> GetSelectedGameObjectId() const;
|
||||
std::string GetSelectedItemId() const;
|
||||
std::string GetSelectedDisplayName() const;
|
||||
const ::XCEngine::Components::GameObject* GetSelectedGameObject() const;
|
||||
|
||||
bool SetSelection(std::string_view itemId);
|
||||
bool SetSelection(::XCEngine::Components::GameObject::ID id);
|
||||
void ClearSelection();
|
||||
|
||||
::XCEngine::Components::GameObject* FindGameObject(std::string_view itemId) const;
|
||||
bool RenameGameObject(
|
||||
std::string_view itemId,
|
||||
std::string_view newName);
|
||||
bool DeleteGameObject(std::string_view itemId);
|
||||
std::string DuplicateGameObject(std::string_view itemId);
|
||||
bool ReparentGameObject(
|
||||
std::string_view itemId,
|
||||
std::string_view parentItemId);
|
||||
bool MoveGameObjectToRoot(std::string_view itemId);
|
||||
|
||||
private:
|
||||
bool HasValidSelection() const;
|
||||
bool SelectFirstAvailableGameObject();
|
||||
|
||||
std::filesystem::path m_projectRoot = {};
|
||||
EditorStartupSceneResult m_startupSceneResult = {};
|
||||
std::optional<::XCEngine::Components::GameObject::ID> m_selectedGameObjectId = std::nullopt;
|
||||
};
|
||||
|
||||
} // namespace XCEngine::UI::Editor::App
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "EditorContext.h"
|
||||
|
||||
#include "Composition/EditorShellAssetBuilder.h"
|
||||
#include "Scene/EditorSceneRuntime.h"
|
||||
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
@@ -28,7 +29,7 @@ std::string ComposeStatusText(
|
||||
} // namespace
|
||||
|
||||
bool EditorContext::Initialize(const std::filesystem::path& repoRoot) {
|
||||
m_shellAsset = BuildEditorShellAsset(repoRoot);
|
||||
m_shellAsset = BuildEditorApplicationShellAsset(repoRoot);
|
||||
m_shellValidation = ValidateEditorShellAsset(m_shellAsset);
|
||||
if (!m_shellValidation.IsValid()) {
|
||||
return false;
|
||||
@@ -37,6 +38,8 @@ bool EditorContext::Initialize(const std::filesystem::path& repoRoot) {
|
||||
m_session = {};
|
||||
m_session.repoRoot = repoRoot;
|
||||
m_session.projectRoot = (repoRoot / "project").lexically_normal();
|
||||
m_sceneRuntime = {};
|
||||
m_sceneRuntime.Initialize(m_session.projectRoot);
|
||||
m_hostCommandBridge.BindSession(m_session);
|
||||
m_shortcutManager = BuildEditorShellShortcutManager(m_shellAsset);
|
||||
m_shortcutManager.SetHostCommandHandler(&m_hostCommandBridge);
|
||||
@@ -83,6 +86,14 @@ const EditorSession& EditorContext::GetSession() const {
|
||||
return m_session;
|
||||
}
|
||||
|
||||
EditorSceneRuntime& EditorContext::GetSceneRuntime() {
|
||||
return m_sceneRuntime;
|
||||
}
|
||||
|
||||
const EditorSceneRuntime& EditorContext::GetSceneRuntime() const {
|
||||
return m_sceneRuntime;
|
||||
}
|
||||
|
||||
void EditorContext::SetSelection(EditorSelectionState selection) {
|
||||
m_session.selection = std::move(selection);
|
||||
}
|
||||
@@ -106,7 +117,7 @@ UIEditorShellInteractionDefinition EditorContext::BuildShellDefinition(
|
||||
const UIEditorWorkspaceController& workspaceController,
|
||||
std::string_view captureText,
|
||||
EditorShellVariant variant) const {
|
||||
return BuildEditorShellInteractionDefinition(
|
||||
return BuildEditorApplicationShellInteractionDefinition(
|
||||
m_shellAsset,
|
||||
workspaceController,
|
||||
ComposeStatusText(m_lastStatus, m_lastMessage),
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "Composition/EditorHostCommandBridge.h"
|
||||
#include "State/EditorSession.h"
|
||||
#include "Composition/EditorShellVariant.h"
|
||||
#include "Scene/EditorSceneRuntime.h"
|
||||
|
||||
#include <XCEditor/App/EditorHostCommandBridge.h>
|
||||
#include <XCEditor/App/EditorSession.h>
|
||||
#include <XCEditor/Foundation/UIEditorShortcutManager.h>
|
||||
#include <XCEditor/Shell/UIEditorShellAsset.h>
|
||||
#include <XCEditor/Shell/UIEditorShellInteraction.h>
|
||||
@@ -32,6 +33,8 @@ public:
|
||||
const std::string& GetValidationMessage() const;
|
||||
const EditorShellAsset& GetShellAsset() const;
|
||||
const EditorSession& GetSession() const;
|
||||
EditorSceneRuntime& GetSceneRuntime();
|
||||
const EditorSceneRuntime& GetSceneRuntime() const;
|
||||
void SetSelection(EditorSelectionState selection);
|
||||
void ClearSelection();
|
||||
|
||||
@@ -60,6 +63,7 @@ private:
|
||||
UIEditorShortcutManager m_shortcutManager = {};
|
||||
UIEditorShellInteractionServices m_shellServices = {};
|
||||
EditorSession m_session = {};
|
||||
EditorSceneRuntime m_sceneRuntime = {};
|
||||
EditorHostCommandBridge m_hostCommandBridge = {};
|
||||
std::string m_lastStatus = {};
|
||||
std::string m_lastMessage = {};
|
||||
|
||||
@@ -68,11 +68,6 @@ set(EDITOR_UI_UNIT_TEST_SOURCES
|
||||
|
||||
add_executable(editor_ui_tests ${EDITOR_UI_UNIT_TEST_SOURCES})
|
||||
|
||||
target_sources(editor_ui_tests
|
||||
PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/new_editor/app/Composition/EditorHostCommandBridge.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(editor_ui_tests
|
||||
PRIVATE
|
||||
XCUIEditorLib
|
||||
@@ -105,3 +100,52 @@ include(GoogleTest)
|
||||
gtest_discover_tests(editor_ui_tests
|
||||
DISCOVERY_MODE PRE_TEST
|
||||
)
|
||||
|
||||
if(TARGET XCUIEditorAppLib)
|
||||
add_executable(editor_app_feature_tests
|
||||
test_project_browser_model.cpp
|
||||
test_hierarchy_scene_binding.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(editor_app_feature_tests
|
||||
PRIVATE
|
||||
XCUIEditorAppLib
|
||||
XCUIEditorLib
|
||||
XCUIEditorHost
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
target_include_directories(editor_app_feature_tests
|
||||
PRIVATE
|
||||
${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}/include
|
||||
${XCENGINE_EDITOR_UI_TESTS_EDITOR_ROOT}
|
||||
${CMAKE_SOURCE_DIR}/engine/include
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(editor_app_feature_tests PRIVATE /utf-8 /FS)
|
||||
set_target_properties(editor_app_feature_tests PROPERTIES
|
||||
MSVC_DEBUG_INFORMATION_FORMAT "$<$<CONFIG:Debug,RelWithDebInfo>:Embedded>"
|
||||
COMPILE_PDB_NAME "editor_app_feature_tests-compile"
|
||||
COMPILE_PDB_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb"
|
||||
COMPILE_PDB_OUTPUT_DIRECTORY_DEBUG "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/Debug"
|
||||
COMPILE_PDB_OUTPUT_DIRECTORY_RELEASE "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/Release"
|
||||
COMPILE_PDB_OUTPUT_DIRECTORY_MINSIZEREL "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/MinSizeRel"
|
||||
COMPILE_PDB_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_CURRENT_BINARY_DIR}/compile-pdb/RelWithDebInfo"
|
||||
)
|
||||
set_property(TARGET editor_app_feature_tests PROPERTY
|
||||
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
|
||||
endif()
|
||||
|
||||
if(WIN32 AND EXISTS "${CMAKE_SOURCE_DIR}/engine/third_party/assimp/bin/assimp-vc143-mt.dll")
|
||||
add_custom_command(TARGET editor_app_feature_tests POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${CMAKE_SOURCE_DIR}/engine/third_party/assimp/bin/assimp-vc143-mt.dll
|
||||
$<TARGET_FILE_DIR:editor_app_feature_tests>/assimp-vc143-mt.dll
|
||||
)
|
||||
endif()
|
||||
|
||||
gtest_discover_tests(editor_app_feature_tests
|
||||
DISCOVERY_MODE PRE_TEST
|
||||
)
|
||||
endif()
|
||||
|
||||
Reference in New Issue
Block a user