577 lines
18 KiB
C++
577 lines
18 KiB
C++
#include "HierarchyPanelInternal.h"
|
|
|
|
#include <utility>
|
|
|
|
namespace XCEngine::UI::Editor::App {
|
|
|
|
using namespace HierarchyPanelInternal;
|
|
|
|
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;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void HierarchyPanel::Initialize() {
|
|
m_model = HierarchyModel::BuildDefault();
|
|
RebuildItems();
|
|
}
|
|
|
|
void HierarchyPanel::SetBuiltInIcons(const BuiltInIcons* icons) {
|
|
m_icons = icons;
|
|
RebuildItems();
|
|
}
|
|
|
|
void HierarchyPanel::ResetInteractionState() {
|
|
m_treeInteractionState = {};
|
|
m_treeFrame = {};
|
|
m_dragState = {};
|
|
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();
|
|
m_dragState.requestPointerCapture = false;
|
|
m_dragState.requestPointerRelease = false;
|
|
}
|
|
|
|
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");
|
|
|
|
if (!previousSelection.empty() && m_model.ContainsNode(previousSelection)) {
|
|
m_selection.SetSelection(previousSelection);
|
|
return;
|
|
}
|
|
|
|
if (!m_treeItems.empty()) {
|
|
m_selection.SetSelection(m_treeItems.front().itemId);
|
|
return;
|
|
}
|
|
|
|
m_selection.ClearSelection();
|
|
}
|
|
|
|
const HierarchyNode* HierarchyPanel::GetSelectedNode() const {
|
|
if (!m_selection.HasSelection()) {
|
|
return nullptr;
|
|
}
|
|
|
|
return m_model.FindNode(m_selection.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));
|
|
}
|
|
|
|
bool HierarchyPanel::WantsHostPointerCapture() const {
|
|
return m_dragState.requestPointerCapture;
|
|
}
|
|
|
|
bool HierarchyPanel::WantsHostPointerRelease() const {
|
|
return m_dragState.requestPointerRelease;
|
|
}
|
|
|
|
bool HierarchyPanel::HasActivePointerCapture() const {
|
|
return m_dragState.dragging;
|
|
}
|
|
|
|
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 (commandId == "edit.rename") {
|
|
EmitRenameRequestedEvent(selectedNodeId);
|
|
return BuildDispatchResult(
|
|
true,
|
|
"Hierarchy rename requested for '" + selectedNodeLabel + "'.");
|
|
}
|
|
|
|
if (commandId == "edit.delete") {
|
|
if (!m_model.DeleteNode(selectedNodeId)) {
|
|
return BuildDispatchResult(false, "Failed to delete the selected hierarchy object.");
|
|
}
|
|
|
|
RebuildItems();
|
|
EmitSelectionEvent();
|
|
return BuildDispatchResult(
|
|
true,
|
|
"Deleted hierarchy object '" + selectedNodeLabel + "'.");
|
|
}
|
|
|
|
if (commandId == "edit.duplicate") {
|
|
const std::string duplicatedNodeId = m_model.DuplicateNode(selectedNodeId);
|
|
if (duplicatedNodeId.empty()) {
|
|
return BuildDispatchResult(false, "Failed to duplicate the selected hierarchy object.");
|
|
}
|
|
|
|
RebuildItems();
|
|
m_selection.SetSelection(duplicatedNodeId);
|
|
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.");
|
|
}
|
|
|
|
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,
|
|
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 HierarchyNode* 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 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 = {};
|
|
return;
|
|
}
|
|
|
|
if (m_treeItems.empty()) {
|
|
RebuildItems();
|
|
}
|
|
|
|
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,
|
|
interactionEvents,
|
|
ResolveUIEditorTreeViewMetrics());
|
|
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_selection,
|
|
m_treeInteractionState.treeViewState,
|
|
palette,
|
|
metrics);
|
|
AppendUIEditorTreeViewForeground(
|
|
drawList,
|
|
m_treeFrame.layout,
|
|
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);
|
|
}
|
|
|
|
} // namespace XCEngine::UI::Editor::App
|