From b13300ea7d2cfa58d86b1610a02ec6a0fb8b23b9 Mon Sep 17 00:00:00 2001 From: Will B Date: Tue, 10 Feb 2026 17:25:19 -0700 Subject: [PATCH 1/3] Initial restoration of reorderable collection view model --- .../Updaters/CollectionPropertyNodeUpdater.cs | 34 +++- .../View/CommonResources.xaml | 11 ++ .../ReorderCollectionItemViewModel.cs | 148 ++++++++++-------- 3 files changed, 124 insertions(+), 69 deletions(-) diff --git a/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Updaters/CollectionPropertyNodeUpdater.cs b/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Updaters/CollectionPropertyNodeUpdater.cs index 3ecdf0ffc1..d9d9150b7b 100644 --- a/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Updaters/CollectionPropertyNodeUpdater.cs +++ b/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Updaters/CollectionPropertyNodeUpdater.cs @@ -1,11 +1,10 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. -using System.Linq; +using Stride.Core.Annotations; using Stride.Core.Assets.Editor.Quantum.NodePresenters.Keys; using Stride.Core.Assets.Editor.ViewModel; -using Stride.Core.Annotations; -using Stride.Core.Reflection; using Stride.Core.Presentation.Quantum.Presenters; +using Stride.Core.Reflection; namespace Stride.Core.Assets.Editor.Quantum.NodePresenters.Updaters { @@ -13,9 +12,8 @@ internal sealed class CollectionPropertyNodeUpdater : AssetNodePresenterUpdaterB { protected override void UpdateNode(IAssetNodePresenter node) { - var memberNode = node as MemberNodePresenter; MemberCollectionAttribute memberCollection; - if (memberNode != null && memberNode.IsEnumerable) + if (node is MemberNodePresenter memberNode && memberNode.IsEnumerable) { memberCollection = memberNode.MemberAttributes.OfType().FirstOrDefault(); } @@ -24,12 +22,34 @@ protected override void UpdateNode(IAssetNodePresenter node) memberCollection = node.Descriptor.Attributes.OfType().FirstOrDefault() ?? TypeDescriptorFactory.Default.AttributeRegistry.GetAttribute(node.Type); } + if (memberCollection != null) { - if (memberCollection.CanReorderItems) - node.AttachedProperties.Add(CollectionData.ReorderCollectionItemKey, new ReorderCollectionItemViewModel()); if (memberCollection.ReadOnly) + { node.AttachedProperties.Add(CollectionData.ReadOnlyCollectionKey, true); + } + } + + // Check if this is an item within a reorderable collection + var parentNode = node.Parent; + if (parentNode != null && node is ItemNodePresenter) + { + MemberCollectionAttribute parentCollection; + if (parentNode is MemberNodePresenter parentMemberNode && parentMemberNode.IsEnumerable) + { + parentCollection = parentMemberNode.MemberAttributes.OfType().FirstOrDefault(); + } + else + { + parentCollection = parentNode.Descriptor.Attributes.OfType().FirstOrDefault() + ?? TypeDescriptorFactory.Default.AttributeRegistry.GetAttribute(parentNode.Type); + } + + if (parentCollection?.CanReorderItems == true) + { + node.AttachedProperties.Add(CollectionData.ReorderCollectionItemKey, new ReorderCollectionItemViewModel()); + } } } } diff --git a/sources/editor/Stride.Core.Assets.Editor/View/CommonResources.xaml b/sources/editor/Stride.Core.Assets.Editor/View/CommonResources.xaml index 145e3edeb3..429d5dbef7 100644 --- a/sources/editor/Stride.Core.Assets.Editor/View/CommonResources.xaml +++ b/sources/editor/Stride.Core.Assets.Editor/View/CommonResources.xaml @@ -3,6 +3,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:sd="http://schemas.stride3d.net/xaml/presentation" + xmlns:interactivity="clr-namespace:Stride.Core.Presentation.Interactivity;assembly=Stride.Core.Presentation.Wpf" xmlns:view="clr-namespace:Stride.Core.Assets.Editor.View" xmlns:viewModel="clr-namespace:Stride.Core.Assets.Editor.ViewModel" xmlns:behaviors="clr-namespace:Stride.Core.Assets.Editor.View.Behaviors" @@ -14,6 +15,16 @@ + + diff --git a/sources/editor/Stride.Core.Assets.Editor/ViewModel/ReorderCollectionItemViewModel.cs b/sources/editor/Stride.Core.Assets.Editor/ViewModel/ReorderCollectionItemViewModel.cs index bdd3debb60..ad52820e82 100644 --- a/sources/editor/Stride.Core.Assets.Editor/ViewModel/ReorderCollectionItemViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Editor/ViewModel/ReorderCollectionItemViewModel.cs @@ -1,84 +1,108 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. -using System.Collections.Generic; +using Stride.Core.Assets.Editor.Quantum.NodePresenters.Commands; +using Stride.Core.Assets.Editor.Quantum.NodePresenters.Keys; using Stride.Core.Assets.Editor.View.Behaviors; +using Stride.Core.Presentation.Quantum.Presenters; using Stride.Core.Presentation.Quantum.ViewModels; +using Stride.Core.Presentation.Services; +using Stride.Core.Reflection; namespace Stride.Core.Assets.Editor.ViewModel { public class ReorderCollectionItemViewModel : IReorderItemViewModel { - + private NodeViewModel targetNode; + public bool CanInsertChildren(IReadOnlyCollection children, InsertPosition position, AddChildModifiers modifiers, out string message) { - // FIXME: This feature is disabled for now. - message = ""; - return false; - - //if (children.Count != 1) - //{ - // message = "Only a single item can be moved at a time"; - // return false; - //} - - //var node = children.First() as NodeViewModel; - //if (node?.Parent == null || !(TypeDescriptorFactory.Default.Find(node.Parent.Type) is CollectionDescriptor)) - //{ - // message = "This item cannot be moved because it is not in a collection"; - // return false; - //} - - //if (node.Parent.Type != targetNode.Parent.Type) - //{ - // message = "Invalid target location"; - // return false; - //} - - //object data; - //if (!node.AssociatedData.TryGetValue("ReorderCollectionItem", out data)) - // return false; - - //var sourceNode = (NodeViewModel)children.First(); - //var sourceIndex = sourceNode.Index.Int; - //var targetIndex = targetNode.Index.Int; - //if (sourceIndex == targetIndex) - //{ - // message = "The target position is the same that the current position"; - // return false; - //} - - //message = string.Format(position == InsertPosition.Before ? "Insert before {0}" : "Insert after {0}", targetNode.DisplayName); - //return node.Index.IsInt && data is IReorderItemViewModel; + if (children.Count != 1) + { + message = "Only a single item can be moved at a time"; + return false; + } + + var node = children.First() as NodeViewModel; + if (node?.Parent == null || !(TypeDescriptorFactory.Default.Find(node.Parent.Type) is CollectionDescriptor)) + { + message = "This item cannot be moved because it is not in a collection"; + return false; + } + + if (node.Parent != targetNode.Parent) + { + message = "Invalid target location"; + return false; + } + + object data; + if (!node.AssociatedData.TryGetValue(CollectionData.ReorderCollectionItem, out data)) + { + message = "This item cannot be reordered"; + return false; + } + + var sourcePresenter = node.NodePresenters.FirstOrDefault() as ItemNodePresenter; + var targetPresenter = targetNode.NodePresenters.FirstOrDefault() as ItemNodePresenter; + + if (sourcePresenter == null || targetPresenter == null || + !sourcePresenter.Index.IsInt || !targetPresenter.Index.IsInt) + { + message = "Items with non-integer indices cannot be reordered"; + return false; + } + + var sourceIndex = sourcePresenter.Index.Int; + var targetIndex = targetPresenter.Index.Int; + if (sourceIndex == targetIndex) + { + message = "The target position is the same as the current position"; + return false; + } + + message = string.Format(position == InsertPosition.Before ? "Insert before {0}" : "Insert after {0}", targetNode.DisplayName); + return data is IReorderItemViewModel; } public void InsertChildren(IReadOnlyCollection children, InsertPosition position, AddChildModifiers modifiers) { - // FIXME: This feature is disabled for now. - //var sourceNode = (NodeViewModel)children.First(); - //var sourceIndex = sourceNode.Index.Int; - //var targetIndex = targetNode.Index.Int; - //if (position == InsertPosition.After) - // ++targetIndex; - - //if (sourceNode.Parent.NodeValue == targetNode.Parent.NodeValue && sourceIndex < targetIndex) - // --targetIndex; - - //var moveCommand = (NodeCommandWrapperBase)sourceNode.Parent.GetCommand(MoveItemCommand.CommandName); - //if (moveCommand == null) - // return; - - //var actionService = sourceNode.ServiceProvider.Get(); - //using (var transaction = actionService.CreateTransaction()) - //{ - // moveCommand.Invoke(Tuple.Create(sourceIndex, targetIndex)); - // actionService.SetName(transaction, $"Move item {sourceIndex}"); - //} + var sourceNode = (NodeViewModel)children.First(); + var sourcePresenter = sourceNode.NodePresenters.FirstOrDefault() as ItemNodePresenter; + var targetPresenter = targetNode.NodePresenters.FirstOrDefault() as ItemNodePresenter; + + if (sourcePresenter == null || targetPresenter == null) + { + return; + } + + var sourceIndex = sourcePresenter.Index.Int; + var targetIndex = targetPresenter.Index.Int; + + if (position == InsertPosition.After) + { + ++targetIndex; + } + + if (sourceNode.Parent.NodeValue == targetNode.Parent.NodeValue && sourceIndex < targetIndex) + { + --targetIndex; + } + + var moveCommand = (NodePresenterCommandWrapper)sourceNode.GetCommand(MoveItemCommand.CommandName); + if (moveCommand == null) + { + return; + } + + var actionService = sourceNode.ServiceProvider.Get(); + using var transaction = actionService.CreateTransaction(); + moveCommand.Invoke(Tuple.Create(sourceIndex, targetIndex)); + actionService.SetName(transaction, $"Move item {sourceIndex}"); } public void SetTargetNode(NodeViewModel node) { - // FIXME: This feature is disabled for now. - //targetNode = node; + targetNode = node; } } } From ceb9e7c27a822554e993ce9a4ee81ce0f88c6e6c Mon Sep 17 00:00:00 2001 From: "Will B (Laptop)" Date: Thu, 12 Feb 2026 09:33:00 -0700 Subject: [PATCH 2/3] Add support for reordering arrays with editor drag and drop --- .../PropertyViewItemDragDropBehavior.cs | 6 +- .../ReorderCollectionItemViewModel.cs | 92 +++++++++++++++++-- 2 files changed, 90 insertions(+), 8 deletions(-) diff --git a/sources/editor/Stride.Core.Assets.Editor/View/Behaviors/DragDrop/PropertyViewItemDragDropBehavior.cs b/sources/editor/Stride.Core.Assets.Editor/View/Behaviors/DragDrop/PropertyViewItemDragDropBehavior.cs index 2990e2cc13..6d9946963a 100644 --- a/sources/editor/Stride.Core.Assets.Editor/View/Behaviors/DragDrop/PropertyViewItemDragDropBehavior.cs +++ b/sources/editor/Stride.Core.Assets.Editor/View/Behaviors/DragDrop/PropertyViewItemDragDropBehavior.cs @@ -21,7 +21,8 @@ protected override bool CanInitializeDrag(object originalSource) if (node?.Parent == null) return false; - if (!(TypeDescriptorFactory.Default.Find(node.Parent.Type) is CollectionDescriptor)) + var parentDescriptor = TypeDescriptorFactory.Default.Find(node.Parent.Type); + if (!(parentDescriptor is CollectionDescriptor or ArrayDescriptor)) return false; object data; @@ -37,7 +38,8 @@ protected override IEnumerable GetItemsToDrag(PropertyViewItem container if (node?.Parent == null) return Enumerable.Empty(); - if (!(TypeDescriptorFactory.Default.Find(node.Parent.Type) is CollectionDescriptor)) + var parentDescriptor = TypeDescriptorFactory.Default.Find(node.Parent.Type); + if (!(parentDescriptor is CollectionDescriptor or ArrayDescriptor)) return Enumerable.Empty(); object data; diff --git a/sources/editor/Stride.Core.Assets.Editor/ViewModel/ReorderCollectionItemViewModel.cs b/sources/editor/Stride.Core.Assets.Editor/ViewModel/ReorderCollectionItemViewModel.cs index ad52820e82..9b5f581b34 100644 --- a/sources/editor/Stride.Core.Assets.Editor/ViewModel/ReorderCollectionItemViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Editor/ViewModel/ReorderCollectionItemViewModel.cs @@ -23,12 +23,23 @@ public bool CanInsertChildren(IReadOnlyCollection children, InsertPositi } var node = children.First() as NodeViewModel; - if (node?.Parent == null || !(TypeDescriptorFactory.Default.Find(node.Parent.Type) is CollectionDescriptor)) + if (node?.Parent == null) { message = "This item cannot be moved because it is not in a collection"; return false; } + var parentDescriptor = TypeDescriptorFactory.Default.Find(node.Parent.Type); + var isArray = parentDescriptor is ArrayDescriptor; + var isCollection = parentDescriptor is CollectionDescriptor collectionDescriptor && + collectionDescriptor.HasRemoveAt && collectionDescriptor.HasInsert; + + if (!isArray && !isCollection) + { + message = "This collection does not support reordering"; + return false; + } + if (node.Parent != targetNode.Parent) { message = "Invalid target location"; @@ -88,15 +99,84 @@ public void InsertChildren(IReadOnlyCollection children, InsertPosition --targetIndex; } - var moveCommand = (NodePresenterCommandWrapper)sourceNode.GetCommand(MoveItemCommand.CommandName); - if (moveCommand == null) + var actionService = sourceNode.ServiceProvider.Get(); + var parentDescriptor = TypeDescriptorFactory.Default.Find(sourceNode.Parent.Type); + + if (parentDescriptor is ArrayDescriptor) { - return; + ReorderArray(sourceNode, targetNode, sourceIndex, targetIndex, actionService); + } + else + { + var moveCommand = (NodePresenterCommandWrapper)sourceNode.GetCommand(MoveItemCommand.CommandName); + if (moveCommand == null) + { + return; + } + + using var transaction = actionService.CreateTransaction(); + moveCommand.Invoke(Tuple.Create(sourceIndex, targetIndex)); + actionService.SetName(transaction, $"Move item {sourceIndex}"); } + } - var actionService = sourceNode.ServiceProvider.Get(); + private void ReorderArray(NodeViewModel sourceNode, NodeViewModel targetNode, int sourceIndex, int targetIndex, IUndoRedoService actionService) + { using var transaction = actionService.CreateTransaction(); - moveCommand.Invoke(Tuple.Create(sourceIndex, targetIndex)); + + var parentNode = sourceNode.Parent; + var array = parentNode.NodeValue as Array; + if (array == null) + return; + + var sourceValue = array.GetValue(sourceIndex); + + if (sourceIndex < targetIndex) + { + for (int i = sourceIndex; i < targetIndex; i++) + { + var currentChild = parentNode.Children.FirstOrDefault(c => + { + var presenter = c.NodePresenters.FirstOrDefault() as ItemNodePresenter; + return presenter?.Index.Int == i; + }); + if (currentChild != null) + { + var presenter = currentChild.NodePresenters.FirstOrDefault() as ItemNodePresenter; + var nextValue = array.GetValue(i + 1); + presenter?.UpdateValue(nextValue); + } + } + } + else + { + for (int i = sourceIndex; i > targetIndex; i--) + { + var currentChild = parentNode.Children.FirstOrDefault(c => + { + var presenter = c.NodePresenters.FirstOrDefault() as ItemNodePresenter; + return presenter?.Index.Int == i; + }); + if (currentChild != null) + { + var presenter = currentChild.NodePresenters.FirstOrDefault() as ItemNodePresenter; + var prevValue = array.GetValue(i - 1); + presenter?.UpdateValue(prevValue); + } + } + } + + var finalChild = parentNode.Children.FirstOrDefault(c => + { + var presenter = c.NodePresenters.FirstOrDefault() as ItemNodePresenter; + return presenter?.Index.Int == targetIndex; + }); + if (finalChild != null) + { + var presenter = finalChild.NodePresenters.FirstOrDefault() as ItemNodePresenter; + presenter?.UpdateValue(sourceValue); + } + actionService.SetName(transaction, $"Move item {sourceIndex}"); } From 9627291820822cce9b675eeb049397d23072d01d Mon Sep 17 00:00:00 2001 From: "Will B (Laptop)" Date: Thu, 12 Feb 2026 12:58:40 -0700 Subject: [PATCH 3/3] Disable dictionaries from showing reorderable display as that is currently not supported due to the way Quantum tracks their changes --- .../NodePresenters/Updaters/CollectionPropertyNodeUpdater.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Updaters/CollectionPropertyNodeUpdater.cs b/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Updaters/CollectionPropertyNodeUpdater.cs index d9d9150b7b..b82371b8ad 100644 --- a/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Updaters/CollectionPropertyNodeUpdater.cs +++ b/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Updaters/CollectionPropertyNodeUpdater.cs @@ -35,6 +35,10 @@ protected override void UpdateNode(IAssetNodePresenter node) var parentNode = node.Parent; if (parentNode != null && node is ItemNodePresenter) { + // Dictionaries are not supported for reordering + if (DictionaryDescriptor.IsDictionary(parentNode.Type)) + return; + MemberCollectionAttribute parentCollection; if (parentNode is MemberNodePresenter parentMemberNode && parentMemberNode.IsEnumerable) {