Files
XCEngine/new_editor/app/Features/Hierarchy/HierarchyPanel.cpp

728 lines
22 KiB
C++

#include "HierarchyPanelInternal.h"
#include "Scene/EditorSceneRuntime.h"
#include "State/EditorCommandFocusService.h"
#include <XCEditor/Collections/UIEditorTreePanelBehavior.h>
#include <XCEditor/Fields/UIEditorFieldStyle.h>
#include <XCEditor/Fields/UIEditorTextField.h>
#include <utility>
namespace XCEngine::UI::Editor::App {
using namespace HierarchyPanelInternal;
namespace TreeDrag = XCEngine::UI::Editor::Collections::TreeDragDrop;
namespace {
UIEditorHostCommandEvaluationResult BuildEvaluationResult(
bool executable,
std::string message) {
UIEditorHostCommandEvaluationResult result = {};
result.executable = executable;
result.message = std::move(message);
return result;
}
UIEditorHostCommandDispatchResult BuildDispatchResult(
bool commandExecuted,
std::string message) {
UIEditorHostCommandDispatchResult result = {};
result.commandExecuted = commandExecuted;
result.message = std::move(message);
return result;
}
bool HasValidBounds(const UIRect& bounds) {
return bounds.width > 0.0f && bounds.height > 0.0f;
}
bool ContainsPoint(const UIRect& rect, const UIPoint& point) {
return point.x >= rect.x &&
point.x <= rect.x + rect.width &&
point.y >= rect.y &&
point.y <= rect.y + rect.height;
}
} // namespace
void HierarchyPanel::Initialize() {
SyncModelFromScene();
}
void HierarchyPanel::SetSceneRuntime(EditorSceneRuntime* sceneRuntime) {
if (m_sceneRuntime == sceneRuntime) {
return;
}
m_sceneRuntime = sceneRuntime;
SyncModelFromScene();
}
void HierarchyPanel::SetCommandFocusService(
EditorCommandFocusService* commandFocusService) {
m_commandFocusService = commandFocusService;
}
void HierarchyPanel::SetBuiltInIcons(const BuiltInIcons* icons) {
m_icons = icons;
RebuildItems();
}
void HierarchyPanel::ResetInteractionState() {
m_treeInteractionState = {};
m_treeFrame = {};
m_dragState = {};
ClearRenameState();
ResetTransientState();
}
const UIEditorPanelContentHostPanelState* HierarchyPanel::FindMountedHierarchyPanel(
const UIEditorPanelContentHostFrame& contentHostFrame) const {
for (const UIEditorPanelContentHostPanelState& panelState : contentHostFrame.panelStates) {
if (panelState.panelId == kHierarchyPanelId && panelState.mounted) {
return &panelState;
}
}
return nullptr;
}
void HierarchyPanel::ResetTransientState() {
m_frameEvents.clear();
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);
m_treeItems = m_model.BuildTreeItems(icon);
SyncTreeSelectionFromSceneRuntime();
}
void HierarchyPanel::SyncTreeSelectionFromSceneRuntime() {
if (m_sceneRuntime == nullptr) {
m_treeSelection.ClearSelection();
return;
}
const std::string selectedItemId = m_sceneRuntime->GetSelectedItemId();
if (!selectedItemId.empty() && m_model.ContainsNode(selectedItemId)) {
m_treeSelection.SetSelection(selectedItemId);
return;
}
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_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_treeSelection.GetSelectedId());
}
void HierarchyPanel::EmitSelectionEvent() {
Event event = {};
event.kind = EventKind::SelectionChanged;
if (const HierarchyNode* node = GetSelectedNode(); node != nullptr) {
event.itemId = node->nodeId;
event.label = node->label;
}
m_frameEvents.push_back(std::move(event));
}
void HierarchyPanel::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 HierarchyNode* node = m_model.FindNode(event.itemId); node != nullptr) {
event.label = node->label;
}
m_frameEvents.push_back(std::move(event));
}
void HierarchyPanel::EmitRenameRequestedEvent(std::string_view itemId) {
Event event = {};
event.kind = EventKind::RenameRequested;
event.itemId = std::string(itemId);
if (const HierarchyNode* node = m_model.FindNode(itemId); node != nullptr) {
event.label = node->label;
}
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;
}
}
}
void HierarchyPanel::ClaimCommandFocus(
const std::vector<UIInputEvent>& inputEvents,
const UIRect& bounds,
bool allowInteraction) {
if (m_commandFocusService == nullptr) {
return;
}
for (const UIInputEvent& event : inputEvents) {
if (event.type == ::XCEngine::UI::UIInputEventType::FocusGained) {
m_commandFocusService->ClaimFocus(EditorActionRoute::Hierarchy);
return;
}
if (!allowInteraction ||
event.type != ::XCEngine::UI::UIInputEventType::PointerButtonDown ||
!ContainsPoint(bounds, event.position)) {
continue;
}
m_commandFocusService->ClaimFocus(EditorActionRoute::Hierarchy);
return;
}
}
UIRect HierarchyPanel::BuildRenameBounds(
std::string_view itemId,
const Widgets::UIEditorTreeViewLayout& layout) const {
const Widgets::UIEditorTextFieldMetrics hostedMetrics =
BuildUIEditorPropertyGridTextFieldMetrics(
ResolveUIEditorPropertyGridMetrics(),
ResolveUIEditorTextFieldMetrics());
return BuildUIEditorTreePanelInlineRenameBounds(
layout,
m_treeItems,
itemId,
hostedMetrics);
}
bool HierarchyPanel::WantsHostPointerCapture() const {
return m_dragState.requestPointerCapture;
}
bool HierarchyPanel::WantsHostPointerRelease() const {
return m_dragState.requestPointerRelease;
}
bool HierarchyPanel::HasActivePointerCapture() const {
return TreeDrag::HasActivePointerCapture(m_dragState);
}
const std::vector<HierarchyPanel::Event>& HierarchyPanel::GetFrameEvents() const {
return m_frameEvents;
}
UIEditorHostCommandEvaluationResult HierarchyPanel::EvaluateEditCommand(
std::string_view commandId) const {
const HierarchyNode* selectedNode = GetSelectedNode();
if (selectedNode == nullptr) {
return BuildEvaluationResult(false, "Select a hierarchy object first.");
}
if (commandId == "edit.rename") {
return BuildEvaluationResult(
true,
"Rename hierarchy object '" + selectedNode->label + "'.");
}
if (commandId == "edit.delete") {
return BuildEvaluationResult(
true,
"Delete hierarchy object '" + selectedNode->label + "'.");
}
if (commandId == "edit.duplicate") {
return BuildEvaluationResult(
true,
"Duplicate hierarchy object '" + selectedNode->label + "'.");
}
if (commandId == "edit.cut" ||
commandId == "edit.copy" ||
commandId == "edit.paste") {
return BuildEvaluationResult(
false,
"Hierarchy clipboard has no bound transfer owner in the current shell.");
}
return BuildEvaluationResult(false, "Hierarchy does not expose this edit command.");
}
UIEditorHostCommandDispatchResult HierarchyPanel::DispatchEditCommand(
std::string_view commandId) {
const UIEditorHostCommandEvaluationResult evaluation = EvaluateEditCommand(commandId);
if (!evaluation.executable) {
return BuildDispatchResult(false, evaluation.message);
}
const HierarchyNode* selectedNode = GetSelectedNode();
if (selectedNode == nullptr) {
return BuildDispatchResult(false, "Select a hierarchy object first.");
}
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_sceneRuntime->DeleteGameObject(selectedNodeId)) {
return BuildDispatchResult(false, "Failed to delete the selected hierarchy object.");
}
SyncModelFromScene();
EmitSelectionEvent();
return BuildDispatchResult(
true,
"Deleted hierarchy object '" + selectedNodeLabel + "'.");
}
if (commandId == "edit.duplicate") {
const std::string duplicatedNodeId =
m_sceneRuntime->DuplicateGameObject(selectedNodeId);
if (duplicatedNodeId.empty()) {
return BuildDispatchResult(false, "Failed to duplicate the selected hierarchy object.");
}
SyncModelFromScene();
EmitSelectionEvent();
const HierarchyNode* duplicatedNode = m_model.FindNode(duplicatedNodeId);
const std::string duplicatedLabel =
duplicatedNode != nullptr ? duplicatedNode->label : selectedNodeLabel;
return BuildDispatchResult(
true,
"Duplicated hierarchy object '" + duplicatedLabel + "'.");
}
return BuildDispatchResult(false, "Hierarchy does not expose this edit command.");
}
void HierarchyPanel::ProcessDragAndFrameEvents(
const std::vector<UIInputEvent>& inputEvents,
const UIRect& bounds,
bool allowInteraction,
bool panelActive) {
const std::vector<UIInputEvent> filteredEvents = FilterUIEditorTreePanelInputEvents(
bounds,
inputEvents,
UIEditorTreePanelInputFilterOptions{
.allowInteraction = allowInteraction,
.panelActive = panelActive,
.captureActive = HasActivePointerCapture()
});
if (m_treeFrame.result.selectionChanged) {
SyncSceneRuntimeSelectionFromTree();
EmitSelectionEvent();
}
struct HierarchyTreeDragCallbacks {
::XCEngine::UI::Widgets::UISelectionModel& selection;
HierarchyModel& model;
EditorSceneRuntime* sceneRuntime = nullptr;
bool IsItemSelected(std::string_view itemId) const {
return selection.IsSelected(itemId);
}
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();
}
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);
}
}
void HierarchyPanel::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 = {};
ClearRenameState();
return;
}
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> filteredEvents = FilterUIEditorTreePanelInputEvents(
panelState->bounds,
inputEvents,
UIEditorTreePanelInputFilterOptions{
.allowInteraction = allowInteraction,
.panelActive = panelActive,
.captureActive = HasActivePointerCapture()
});
SyncTreeFocusState(filteredEvents);
ClaimCommandFocus(filteredEvents, panelState->bounds, allowInteraction);
const Widgets::UIEditorTreeViewMetrics treeMetrics =
ResolveUIEditorTreeViewMetrics();
const Widgets::UIEditorTreeViewLayout layout =
Widgets::BuildUIEditorTreeViewLayout(
panelState->bounds,
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 =
BuildUIEditorTreePanelInteractionInputEvents(
m_dragState,
layout,
m_treeItems,
filteredEvents,
false,
kDragThreshold);
m_treeFrame = UpdateUIEditorTreeViewInteraction(
m_treeInteractionState,
m_treeSelection,
m_expansion,
panelState->bounds,
m_treeItems,
interactionEvents,
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,
allowInteraction,
panelActive);
}
void HierarchyPanel::Append(UIDrawList& drawList) const {
if (!m_visible ||
m_treeFrame.layout.bounds.width <= 0.0f ||
m_treeFrame.layout.bounds.height <= 0.0f) {
return;
}
const Widgets::UIEditorTreeViewPalette palette = ResolveUIEditorTreeViewPalette();
const Widgets::UIEditorTreeViewMetrics metrics = ResolveUIEditorTreeViewMetrics();
AppendUIEditorTreeViewBackground(
drawList,
m_treeFrame.layout,
m_treeItems,
m_treeSelection,
m_treeInteractionState.treeViewState,
palette,
metrics);
AppendUIEditorTreeViewForeground(
drawList,
m_treeFrame.layout,
m_treeItems,
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()));
AppendUIEditorInlineRenameSession(
drawList,
m_renameFrame,
m_renameState,
textFieldPalette,
textFieldMetrics);
}
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 = FindUIEditorTreePanelVisibleItemIndex(
m_treeFrame.layout,
m_treeItems,
m_dragState.dropTargetItemId);
if (visibleIndex == Widgets::UIEditorTreeViewInvalidIndex ||
visibleIndex >= m_treeFrame.layout.rowRects.size()) {
return;
}
drawList.AddRectOutline(
m_treeFrame.layout.rowRects[visibleIndex],
kDragPreviewColor,
1.0f,
0.0f);
}
} // namespace XCEngine::UI::Editor::App