Refactor hierarchy drag-and-drop and cleanup inspector
Refactored HandleDrawer to use a static UpdateUI method for clarity. Removed unused PropertyDrawerContext. Added EntityCreated event to EditorWorldService. Updated Hierarchy.xaml to modern TreeView drag-and-drop events. Overhauled drag-and-drop logic in Hierarchy.xaml.cs for better validation, cycle prevention, and scene graph consistency. Introduced helper methods for parent retrieval and scene graph rebuilding. Improved error handling throughout drag-and-drop operations.
This commit is contained in:
@@ -9,6 +9,13 @@ internal class HandleDrawer<T> : PropertyDrawer<Handle<T>> where T : unmanaged
|
|||||||
{
|
{
|
||||||
public override FrameworkElement CreateControlT(PropertyNode<Handle<T>> model)
|
public override FrameworkElement CreateControlT(PropertyNode<Handle<T>> model)
|
||||||
{
|
{
|
||||||
|
static void UpdateUI(HandlePropertyNode<T> handleNode, ReferenceField field)
|
||||||
|
{
|
||||||
|
var guid = handleNode?.AssetGuid ?? Guid.Empty;
|
||||||
|
field.HasValue = guid != Guid.Empty;
|
||||||
|
field.DisplayText = guid != Guid.Empty ? $"{typeof(T).Name} ({guid.ToString().Substring(0, 8)})" : $"None ({typeof(T).Name})";
|
||||||
|
}
|
||||||
|
|
||||||
var field = new ReferenceField
|
var field = new ReferenceField
|
||||||
{
|
{
|
||||||
TypeLabel = typeof(T).Name,
|
TypeLabel = typeof(T).Name,
|
||||||
@@ -16,13 +23,7 @@ internal class HandleDrawer<T> : PropertyDrawer<Handle<T>> where T : unmanaged
|
|||||||
};
|
};
|
||||||
|
|
||||||
var handleNode = model as HandlePropertyNode<T>;
|
var handleNode = model as HandlePropertyNode<T>;
|
||||||
|
Logger.DebugAssert(handleNode != null);
|
||||||
Action updateUI = () =>
|
|
||||||
{
|
|
||||||
var guid = handleNode?.AssetGuid ?? Guid.Empty;
|
|
||||||
field.HasValue = guid != Guid.Empty;
|
|
||||||
field.DisplayText = guid != Guid.Empty ? $"{typeof(T).Name} ({guid.ToString().Substring(0, 8)})" : $"None ({typeof(T).Name})";
|
|
||||||
};
|
|
||||||
|
|
||||||
field.ValidateDrop = (args) =>
|
field.ValidateDrop = (args) =>
|
||||||
{
|
{
|
||||||
@@ -32,12 +33,16 @@ internal class HandleDrawer<T> : PropertyDrawer<Handle<T>> where T : unmanaged
|
|||||||
|
|
||||||
field.OnDropAccepted = async (args) =>
|
field.OnDropAccepted = async (args) =>
|
||||||
{
|
{
|
||||||
if (handleNode == null) return;
|
if (handleNode == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var text = await args.DataView.GetTextAsync();
|
var text = await args.DataView.GetTextAsync();
|
||||||
if (Guid.TryParse(text, out var guid))
|
if (Guid.TryParse(text, out var guid))
|
||||||
{
|
{
|
||||||
handleNode.SetHandleFromAsset(guid);
|
handleNode.SetHandleFromAsset(guid);
|
||||||
updateUI();
|
UpdateUI(handleNode, field);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -46,11 +51,11 @@ internal class HandleDrawer<T> : PropertyDrawer<Handle<T>> where T : unmanaged
|
|||||||
if (handleNode != null)
|
if (handleNode != null)
|
||||||
{
|
{
|
||||||
handleNode.ClearHandle();
|
handleNode.ClearHandle();
|
||||||
updateUI();
|
UpdateUI(handleNode, field);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
updateUI();
|
UpdateUI(handleNode, field);
|
||||||
|
|
||||||
// When ECS value changes outside of UI
|
// When ECS value changes outside of UI
|
||||||
model.OnValueChanged += (val) =>
|
model.OnValueChanged += (val) =>
|
||||||
@@ -58,7 +63,7 @@ internal class HandleDrawer<T> : PropertyDrawer<Handle<T>> where T : unmanaged
|
|||||||
// UI Thread check usually required here, but property model events should be on UI thread or marshaled
|
// UI Thread check usually required here, but property model events should be on UI thread or marshaled
|
||||||
field.DispatcherQueue.TryEnqueue(() =>
|
field.DispatcherQueue.TryEnqueue(() =>
|
||||||
{
|
{
|
||||||
updateUI();
|
UpdateUI(handleNode, field);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
using Ghost.Editor.Core.SceneGraph;
|
|
||||||
using Ghost.Entities;
|
|
||||||
|
|
||||||
namespace Ghost.Editor.Core.Inspector;
|
|
||||||
|
|
||||||
public sealed class PropertyDrawerContext
|
|
||||||
{
|
|
||||||
public required World World { get; init; }
|
|
||||||
public required Entity Entity { get; init; }
|
|
||||||
public required EntityNode EntityNode { get; init; }
|
|
||||||
public required ComponentNode ComponentNode { get; init; }
|
|
||||||
}
|
|
||||||
@@ -20,6 +20,7 @@ public class EditorWorldService : IDisposable
|
|||||||
{
|
{
|
||||||
get;
|
get;
|
||||||
} = new();
|
} = new();
|
||||||
|
|
||||||
public event Action<Entity, string, ushort>? EntityCreated;
|
public event Action<Entity, string, ushort>? EntityCreated;
|
||||||
public event Action<Entity>? EntityDestroyed;
|
public event Action<Entity>? EntityDestroyed;
|
||||||
public event Action<Entity, Entity, Entity>? EntityParentChanged; // (child, oldParent, newParent)
|
public event Action<Entity, Entity, Entity>? EntityParentChanged; // (child, oldParent, newParent)
|
||||||
|
|||||||
@@ -101,11 +101,11 @@
|
|||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Margin="4,2,0,2"
|
Margin="4,2,0,2"
|
||||||
AllowDrop="True"
|
AllowDrop="True"
|
||||||
|
CanDrag="True"
|
||||||
CanDragItems="True"
|
CanDragItems="True"
|
||||||
CanReorderItems="True"
|
CanReorderItems="True"
|
||||||
|
DragItemsCompleted="OnTreeViewDragItemsCompleted"
|
||||||
DragItemsStarting="OnTreeViewDragItemsStarting"
|
DragItemsStarting="OnTreeViewDragItemsStarting"
|
||||||
DragOver="OnTreeViewDragOver"
|
|
||||||
Drop="OnTreeViewDrop"
|
|
||||||
ItemTemplateSelector="{StaticResource SceneGraphTemplateSelector}"
|
ItemTemplateSelector="{StaticResource SceneGraphTemplateSelector}"
|
||||||
KeyDown="OnTreeViewKeyDown"
|
KeyDown="OnTreeViewKeyDown"
|
||||||
SelectionMode="Single" />
|
SelectionMode="Single" />
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
using Ghost.Editor.Core.Contracts;
|
using Ghost.Editor.Core.Contracts;
|
||||||
using Ghost.Editor.Core.SceneGraph;
|
using Ghost.Editor.Core.SceneGraph;
|
||||||
using Ghost.Editor.Core.Services;
|
using Ghost.Editor.Core.Services;
|
||||||
|
using Ghost.Core;
|
||||||
using Ghost.Engine;
|
using Ghost.Engine;
|
||||||
|
using Ghost.Entities;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Microsoft.UI.Xaml.Input;
|
using Microsoft.UI.Xaml.Input;
|
||||||
using Microsoft.UI.Xaml.Media;
|
|
||||||
|
|
||||||
namespace Ghost.Editor.Views.Controls;
|
namespace Ghost.Editor.Views.Controls;
|
||||||
|
|
||||||
@@ -72,80 +73,87 @@ public sealed partial class Hierarchy : UserControl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTreeViewDragOver(object sender, DragEventArgs e)
|
private void OnTreeViewDragItemsCompleted(TreeView sender, TreeViewDragItemsCompletedEventArgs args)
|
||||||
{
|
{
|
||||||
e.AcceptedOperation = global::Windows.ApplicationModel.DataTransfer.DataPackageOperation.None;
|
var entityNode = args.Items.Count > 0 ? args.Items[0] as EntityNode : _draggedNode;
|
||||||
|
_draggedNode = null;
|
||||||
|
|
||||||
if (_draggedNode == null)
|
if (entityNode == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var targetItem = GetAncestorTreeViewItem(e.OriginalSource as DependencyObject);
|
if (args.DropResult != global::Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move)
|
||||||
if (targetItem == null)
|
|
||||||
{
|
{
|
||||||
|
RebuildSceneGraphFromECS();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var targetNode = targetItem.DataContext as SceneGraphNode;
|
if (args.NewParentItem is not SceneGraphNode newParent)
|
||||||
if (targetNode == null)
|
|
||||||
{
|
{
|
||||||
|
RebuildSceneGraphFromECS();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Can't drag onto itself
|
if (newParent == entityNode)
|
||||||
if (_draggedNode == targetNode)
|
|
||||||
{
|
{
|
||||||
|
RebuildSceneGraphFromECS();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Can't drag onto a child of itself (cycle checking)
|
var result = Error.None;
|
||||||
if (targetNode is EntityNode targetEntityNode)
|
|
||||||
|
if (newParent is EntityNode parentEntityNode)
|
||||||
{
|
{
|
||||||
if (HierarchyUtility.IsAncestor(_worldService.EditorWorld, targetEntityNode.Entity, _draggedNode.Entity))
|
if (HierarchyUtility.IsAncestor(_worldService.EditorWorld, parentEntityNode.Entity, entityNode.Entity))
|
||||||
{
|
{
|
||||||
|
RebuildSceneGraphFromECS();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
e.AcceptedOperation = global::Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move;
|
var currentParent = GetCurrentParent(entityNode);
|
||||||
}
|
if (currentParent == parentEntityNode.Entity)
|
||||||
|
|
||||||
private void OnTreeViewDrop(object sender, DragEventArgs e)
|
|
||||||
{
|
|
||||||
if (_draggedNode == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetItem = GetAncestorTreeViewItem(e.OriginalSource as DependencyObject);
|
|
||||||
if (targetItem == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var targetNode = targetItem.DataContext as SceneGraphNode;
|
|
||||||
if (targetNode == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_draggedNode == targetNode)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetNode is EntityNode targetEntityNode)
|
|
||||||
{
|
|
||||||
if (!HierarchyUtility.IsAncestor(_worldService.EditorWorld, targetEntityNode.Entity, _draggedNode.Entity))
|
|
||||||
{
|
{
|
||||||
_worldService.SetParent(_draggedNode.Entity, targetEntityNode.Entity);
|
RebuildSceneGraphFromECS();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = _worldService.SetParent(entityNode.Entity, parentEntityNode.Entity);
|
||||||
|
}
|
||||||
|
else if (newParent is SceneNode sceneNode)
|
||||||
|
{
|
||||||
|
var currentParent = GetCurrentParent(entityNode);
|
||||||
|
var sceneChanged = _worldService.GetEntitySceneID(entityNode.Entity) != sceneNode.Scene.ID;
|
||||||
|
if (!currentParent.IsValid && !sceneChanged)
|
||||||
|
{
|
||||||
|
RebuildSceneGraphFromECS();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentParent.IsValid)
|
||||||
|
{
|
||||||
|
result = _worldService.RemoveParent(entityNode.Entity);
|
||||||
|
if (result != Error.None)
|
||||||
|
{
|
||||||
|
RebuildSceneGraphFromECS();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sceneChanged)
|
||||||
|
{
|
||||||
|
_worldService.ChangeEntityScene(entityNode.Entity, sceneNode.Scene.ID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (targetNode is SceneNode sceneNode)
|
else
|
||||||
{
|
{
|
||||||
_worldService.RemoveParent(_draggedNode.Entity);
|
RebuildSceneGraphFromECS();
|
||||||
_worldService.ChangeEntityScene(_draggedNode.Entity, sceneNode.Scene.ID);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result != Error.None)
|
||||||
|
{
|
||||||
|
RebuildSceneGraphFromECS();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,17 +185,38 @@ public sealed partial class Hierarchy : UserControl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private TreeViewItem? GetAncestorTreeViewItem(DependencyObject? current)
|
private Entity GetCurrentParent(EntityNode entityNode)
|
||||||
{
|
{
|
||||||
while (current != null)
|
if (!_worldService.EditorWorld.EntityManager.HasComponent<Ghost.Engine.Components.Hierarchy>(entityNode.Entity))
|
||||||
{
|
{
|
||||||
if (current is TreeViewItem item)
|
return Entity.Invalid;
|
||||||
{
|
}
|
||||||
return item;
|
|
||||||
}
|
return _worldService.EditorWorld.EntityManager.GetComponent<Ghost.Engine.Components.Hierarchy>(entityNode.Entity).parent;
|
||||||
current = VisualTreeHelper.GetParent(current);
|
}
|
||||||
|
|
||||||
|
private void RebuildSceneGraphFromECS()
|
||||||
|
{
|
||||||
|
var names = new Dictionary<Entity, string>();
|
||||||
|
foreach (var sceneNode in _worldService.RootNodes)
|
||||||
|
{
|
||||||
|
CaptureEntityNames(sceneNode, names);
|
||||||
|
}
|
||||||
|
|
||||||
|
_worldService.RebuildSceneGraph(names);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CaptureEntityNames(SceneGraphNode node, Dictionary<Entity, string> names)
|
||||||
|
{
|
||||||
|
if (node is EntityNode entityNode)
|
||||||
|
{
|
||||||
|
names[entityNode.Entity] = entityNode.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var child in node.Children)
|
||||||
|
{
|
||||||
|
CaptureEntityNames(child, names);
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnUnloaded(object sender, RoutedEventArgs e)
|
private void OnUnloaded(object sender, RoutedEventArgs e)
|
||||||
|
|||||||
Reference in New Issue
Block a user