Skip to content

Commit

Permalink
[Editor] Collection supports in abstract properties
Browse files Browse the repository at this point in the history
  • Loading branch information
Eideren committed Jan 28, 2024
1 parent 022296f commit 3f710c6
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ protected PropertiesViewModel([NotNull] IViewModelServiceProvider serviceProvide
RegisterNodePresenterCommand(new FlagEnumSelectNoneCommand());
RegisterNodePresenterCommand(new FlagEnumSelectInvertCommand());

RegisterNodePresenterUpdater(new AbstractNodeCollectionEntryNodeUpdater());
RegisterNodePresenterUpdater(new AbstractNodeEntryNodeUpdater());
RegisterNodePresenterUpdater(new CategoryNodeUpdater());
RegisterNodePresenterUpdater(new CollectionPropertyNodeUpdater());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using Stride.Core.Reflection;
using Stride.Core.Presentation.Quantum;
using Stride.Core.Presentation.Quantum.Presenters;
using Stride.Core.Quantum;

namespace Stride.Core.Assets.Editor.Quantum.NodePresenters.Commands
{
Expand All @@ -27,30 +26,26 @@ public class AddNewItemCommand : SyncNodePresenterCommandBase
public override bool CanAttach(INodePresenter nodePresenter)
{
// We are in a collection...
var collectionDescriptor = nodePresenter.Descriptor as CollectionDescriptor;
if (collectionDescriptor == null)
if (nodePresenter.ValueIsAnyCollection(out bool hasAdd, out var itemType, out var descriptor) == false)
return false;

// ... that is not read-only...
var memberCollection = (nodePresenter as MemberNodePresenter)?.MemberAttributes.OfType<MemberCollectionAttribute>().FirstOrDefault()
?? nodePresenter.Descriptor.Attributes.OfType<MemberCollectionAttribute>().FirstOrDefault();
?? descriptor.Attributes.OfType<MemberCollectionAttribute>().FirstOrDefault();
if (memberCollection?.ReadOnly == true)
return false;

// ... supports add...
if (!collectionDescriptor.HasAdd)
if (!hasAdd)
return false;

// ... and can construct element
var elementType = collectionDescriptor.ElementType;
return CanConstruct(elementType) || elementType.IsAbstract || elementType.IsNullable() || IsReferenceType(elementType);
return CanConstruct(itemType) || itemType.IsAbstract || itemType.IsNullable() || IsReferenceType(itemType);
}

/// <inheritdoc/>
protected override void ExecuteSync(INodePresenter nodePresenter, object parameter, object preExecuteResult)
{
var assetNodePresenter = nodePresenter as IAssetNodePresenter;
var collectionDescriptor = (CollectionDescriptor)nodePresenter.Descriptor;

object itemToAdd;

Expand All @@ -63,12 +58,12 @@ protected override void ExecuteSync(INodePresenter nodePresenter, object paramet
// Otherwise, assume it's an object
else
{
var elementType = collectionDescriptor.ElementType;
nodePresenter.ValueIsAnyCollection(out _, out var itemType, out _);
itemToAdd = parameter;
if (itemToAdd == null)
{
var instance = ObjectFactoryRegistry.NewInstance(elementType);
if (!IsReferenceType(elementType) && (assetNodePresenter == null || !assetNodePresenter.IsObjectReference(instance)))
var instance = ObjectFactoryRegistry.NewInstance(itemType);
if (!IsReferenceType(itemType) && (assetNodePresenter == null || !assetNodePresenter.IsObjectReference(instance)))
itemToAdd = instance;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// 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 System.Linq;
using Stride.Core.Assets.Editor.Quantum.NodePresenters.Commands;
using Stride.Core.Presentation.Quantum.Presenters;

namespace Stride.Core.Assets.Editor.Quantum.NodePresenters.Keys
{
public static class AbstractNodeCollectionEntriesData
{
public const string AbstractNodeCollectionMatchingEntries = nameof(AbstractNodeCollectionMatchingEntries);
public static readonly PropertyKey<IEnumerable<AbstractNodeEntry>> Key = new PropertyKey<IEnumerable<AbstractNodeEntry>>(AbstractNodeCollectionMatchingEntries, typeof(AbstractNodeEntryData), new PropertyCombinerMetadata(CombineProperty));

public static object CombineProperty(IEnumerable<object> properties)
{
var result = new HashSet<AbstractNodeEntry>();
var hashSets = new List<HashSet<AbstractNodeEntry>>();
hashSets.AddRange(properties.Cast<IEnumerable<AbstractNodeEntry>>().Select(x => new HashSet<AbstractNodeEntry>(x)));
result = hashSets[0];
// We display only component types that are available for all entities
for (var i = 1; i < hashSets.Count; ++i)
{
result.IntersectWith(hashSets[i]);
}
return result.OrderBy(x => x.Order).ThenBy(x => x.DisplayValue);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// 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;
using System.Collections.Generic;
using System.Linq;
using Stride.Core.Assets.Editor.Quantum.NodePresenters.Commands;
using Stride.Core.Assets.Editor.Quantum.NodePresenters.Keys;
using Stride.Core.Annotations;
using Stride.Core.Extensions;
using Stride.Core.Reflection;
using Stride.Core.Presentation.Quantum.Presenters;

namespace Stride.Core.Assets.Editor.Quantum.NodePresenters.Updaters
{
public sealed class AbstractNodeCollectionEntryNodeUpdater : AssetNodePresenterUpdaterBase
{
public static IEnumerable<AbstractNodeEntry> FillDefaultAbstractNodeCollectionEntries(IAssetNodePresenter node, Type type)
{
IEnumerable<AbstractNodeEntry> abstractNodeMatchingEntries = AbstractNodeType.GetInheritedInstantiableTypes(type);

if (abstractNodeMatchingEntries != null)
{
// Prepend the value that will allow to set the value to null, if this command is allowed.
if (IsAllowingNull(node))
abstractNodeMatchingEntries = AbstractNodeValue.Null.Yield().Concat(abstractNodeMatchingEntries);
}
return abstractNodeMatchingEntries;
}

/// <summary>
/// Checks if <see cref="MemberCollectionAttribute.NotNullItems"/> is present and set.
/// </summary>
/// <param name="node">The node to check.</param>
/// <returns>True if null is a possible choice for this node, otherwise false.</returns>
public static bool IsAllowingNull(IAssetNodePresenter node)
{
var abstractNodeAllowNull = true;
var memberNode = node as MemberNodePresenter ?? (node as ItemNodePresenter)?.Parent as MemberNodePresenter;
if (memberNode != null)
{
var memberCollection = memberNode.MemberAttributes.OfType<MemberCollectionAttribute>().FirstOrDefault()
?? memberNode.Descriptor.Attributes.OfType<MemberCollectionAttribute>().FirstOrDefault();

if (memberNode.IsEnumerable && memberCollection != null && memberCollection.NotNullItems)
{
// Collections
abstractNodeAllowNull = false;
}
else
{
// Members
abstractNodeAllowNull = !memberNode.MemberAttributes.OfType<NotNullAttribute>().Any();
}
}
return abstractNodeAllowNull;
}

protected override void UpdateNode(IAssetNodePresenter node)
{
if (node.ValueIsAnyCollection(out _, out var type, out _) && type.IsAbstract && !IsReferenceType(type) && IsInstantiable(type))
{
var abstractNodeEntries = FillDefaultAbstractNodeCollectionEntries(node, type);
node.AttachedProperties.Add(AbstractNodeCollectionEntriesData.Key, abstractNodeEntries);
}
}

private static bool IsInstantiable(Type type) => TypeDescriptorFactory.Default.AttributeRegistry.GetAttribute<NonInstantiableAttribute>(type) == null;

private static bool IsReferenceType(Type type) => AssetRegistry.IsContentType(type) || typeof(AssetReference).IsAssignableFrom(type);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public sealed class AbstractNodeEntryNodeUpdater : AssetNodePresenterUpdaterBase
{
public static IEnumerable<AbstractNodeEntry> FillDefaultAbstractNodeEntry(IAssetNodePresenter node)
{
var type = node.Descriptor.GetInnerCollectionType();
var type = node.Descriptor.Type;

IEnumerable<AbstractNodeEntry> abstractNodeMatchingEntries = AbstractNodeType.GetInheritedInstantiableTypes(type);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
<Style x:Key="AddNewItemButtonStyle" BasedOn="{StaticResource ImageButtonStyle}" TargetType="Button">
<Setter Property="Visibility" Value="{sd:MultiBinding {Binding HasCommand_AddNewItem}, {Binding HasCollection},
{Binding HasAssociatedData_ReadOnlyCollection, Converter={sd:InvertBool}},
{Binding HasAssociatedData_AbstractNodeMatchingEntries, Converter={sd:InvertBool}},
{Binding HasAssociatedData_AbstractNodeCollectionMatchingEntries, Converter={sd:InvertBool}},
Converter={sd:MultiChained {sd:AndMultiConverter}, {sd:VisibleOrCollapsed}}, FallbackValue={sd:Collapsed}}"/>
<Setter Property="Command" Value="{Binding AddNewItem}"/>
<Setter Property="ToolTip" Value="{sd:Localize Add..., Context=ToolTip}"/>
Expand Down Expand Up @@ -126,7 +126,7 @@
<Setter Property="Height" Value="16"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Visibility" Value="{sd:MultiBinding {Binding HasCommand_AddNewItem}, {Binding HasCollection}, {Binding HasAssociatedData_AbstractNodeMatchingEntries},
<Setter Property="Visibility" Value="{sd:MultiBinding {Binding HasCommand_AddNewItem}, {Binding HasCollection}, {Binding HasAssociatedData_AbstractNodeCollectionMatchingEntries},
Converter={sd:MultiChained {sd:AndMultiConverter}, {sd:VisibleOrCollapsed}}, FallbackValue={sd:Collapsed}}"/>
<Setter Property="ToolTip" Value="{sd:Localize Add..., Context=ToolTip}" />
<Setter Property="Tag" Value="{StaticResource ImageAdd}"/>
Expand Down Expand Up @@ -406,9 +406,9 @@
<DockPanel>
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" VerticalAlignment="Center">
<Button Style="{StaticResource AddNewItemButtonStyle}"/>
<ComboBox x:Name="InstanceTypeSelectionComboBox" Style="{StaticResource AddItemComboBox}" ItemsSource="{Binding AbstractNodeMatchingEntries}">
<ComboBox x:Name="AddItemSelectionComboBox" Style="{StaticResource AddItemComboBox}" ItemsSource="{Binding AbstractNodeCollectionMatchingEntries}">
<i:Interaction.Behaviors>
<behaviors:OnComboBoxClosedWithSelectionBehavior Command="{Binding AddNewItem}" CommandParameter="{Binding SelectedItem, ElementName=InstanceTypeSelectionComboBox}"/>
<behaviors:OnComboBoxClosedWithSelectionBehavior Command="{Binding AddNewItem}" CommandParameter="{Binding SelectedItem, ElementName=AddItemSelectionComboBox}"/>
</i:Interaction.Behaviors>
</ComboBox>
</StackPanel>
Expand Down Expand Up @@ -450,10 +450,10 @@
<!-- Concrete type -->
<Button DockPanel.Dock="Right" Style="{StaticResource AddNewItemButtonStyle}"/>
<!-- Abstract type -->
<ComboBox DockPanel.Dock="Right" x:Name="InstanceTypeSelectionComboBox" Style="{StaticResource AddItemComboBox}"
ItemsSource="{Binding AbstractNodeMatchingEntries}" Margin="2,0">
<ComboBox DockPanel.Dock="Right" x:Name="AddItemSelectionComboBox" Style="{StaticResource AddItemComboBox}"
ItemsSource="{Binding AbstractNodeCollectionMatchingEntries}" Margin="2,0">
<i:Interaction.Behaviors>
<behaviors:OnComboBoxClosedWithSelectionBehavior Command="{Binding AddNewItem}" CommandParameter="{Binding SelectedItem, ElementName=InstanceTypeSelectionComboBox}"/>
<behaviors:OnComboBoxClosedWithSelectionBehavior Command="{Binding AddNewItem}" CommandParameter="{Binding SelectedItem, ElementName=AddItemSelectionComboBox}"/>
</i:Interaction.Behaviors>
</ComboBox>
<TextBlock Text="{Binding DisplayName, StringFormat={sd:Localize Add to {0}}}" TextTrimming="CharacterEllipsis" FontStyle="Italic"/>
Expand Down Expand Up @@ -512,9 +512,9 @@
<DockPanel>
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" VerticalAlignment="Center">
<Button Style="{StaticResource AddNewItemButtonStyle}"/>
<ComboBox x:Name="InstanceTypeSelectionComboBox" Style="{StaticResource AddItemComboBox}" ItemsSource="{Binding AbstractNodeMatchingEntries}">
<ComboBox x:Name="AddItemSelectionComboBox" Style="{StaticResource AddItemComboBox}" ItemsSource="{Binding AbstractNodeCollectionMatchingEntries}">
<i:Interaction.Behaviors>
<behaviors:OnComboBoxClosedWithSelectionBehavior Command="{Binding AddNewItem}" CommandParameter="{Binding SelectedItem, ElementName=InstanceTypeSelectionComboBox}"/>
<behaviors:OnComboBoxClosedWithSelectionBehavior Command="{Binding AddNewItem}" CommandParameter="{Binding SelectedItem, ElementName=AddItemSelectionComboBox}"/>
</i:Interaction.Behaviors>
</ComboBox>
</StackPanel>
Expand Down Expand Up @@ -556,10 +556,10 @@
<!-- Concrete type -->
<Button DockPanel.Dock="Right" Style="{StaticResource AddNewItemButtonStyle}"/>
<!-- Abstract type -->
<ComboBox DockPanel.Dock="Right" x:Name="InstanceTypeSelectionComboBox" Style="{StaticResource AddItemComboBox}"
ItemsSource="{Binding AbstractNodeMatchingEntries}" Margin="2,0">
<ComboBox DockPanel.Dock="Right" x:Name="AddItemSelectionComboBox" Style="{StaticResource AddItemComboBox}"
ItemsSource="{Binding AbstractNodeCollectionMatchingEntries}" Margin="2,0">
<i:Interaction.Behaviors>
<behaviors:OnComboBoxClosedWithSelectionBehavior Command="{Binding AddNewItem}" CommandParameter="{Binding SelectedItem, ElementName=InstanceTypeSelectionComboBox}"/>
<behaviors:OnComboBoxClosedWithSelectionBehavior Command="{Binding AddNewItem}" CommandParameter="{Binding SelectedItem, ElementName=AddItemSelectionComboBox}"/>
</i:Interaction.Behaviors>
</ComboBox>
<TextBlock Text="{Binding DisplayName, StringFormat={sd:Localize Add to {0}}}" TextTrimming="CharacterEllipsis" FontStyle="Italic"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
using Stride.Core.Annotations;
using Stride.Core.Reflection;
using Stride.Core.Quantum;
using System.Diagnostics.CodeAnalysis;
using NotNullAttribute = Stride.Core.Annotations.NotNullAttribute;

namespace Stride.Core.Presentation.Quantum.Presenters
{
Expand Down Expand Up @@ -83,5 +85,54 @@ public interface INodePresenter : IDisposable

[CanBeNull]
INodePresenter TryGetChild(string childName);

/// <summary>
/// Returns true if this <see cref="Descriptor"/> or <see cref="Value"/> is an array, dictionary or collection
/// </summary>
/// <param name="hasAdd">Whether this collection supports adding items</param>
/// <param name="itemType">What the collection is composed of, for dictionaries this would be the value type</param>
/// <param name="descriptor"><see cref="Descriptor"/> or <see cref="Value"/> if <see cref="Descriptor"/> is not a collection </param>
public bool ValueIsAnyCollection(out bool hasAdd, [MaybeNullWhen(false)] out Type itemType, [MaybeNullWhen(false)] out ITypeDescriptor descriptor)
{
if (DescriptorIsAnyCollection(Descriptor, out hasAdd, out itemType))
{
descriptor = Descriptor;
return true;
}
if (Value is { } val
&& TypeDescriptorFactory.Default.Find(val.GetType()) is { } valueDescriptor
&& DescriptorIsAnyCollection(valueDescriptor, out hasAdd, out itemType))
{
descriptor = valueDescriptor;
return true;
}
descriptor = null;
return false;
}

private static bool DescriptorIsAnyCollection(ITypeDescriptor descriptor, out bool hasAdd, [MaybeNullWhen(false)] out Type itemType)
{
if (descriptor is DictionaryDescriptor dd)
{
itemType = dd.ValueType;
hasAdd = true;
return true;
}
else if (descriptor is CollectionDescriptor cd)
{
itemType = cd.ElementType;
hasAdd = cd.HasAdd;
return true;
}
else if (descriptor is ArrayDescriptor arr)
{
itemType = arr.ElementType;
hasAdd = false;
return true;
}
itemType = null;
hasAdd = false;
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -183,28 +183,33 @@ public NodeViewModel Root {
/// </summary>
/// <remarks>Used mostly for sorting purpose.</remarks>
/// <seealso cref="HasList"/>
public bool HasList => ListDescriptor.IsList(Type);
public bool HasList => ListDescriptor.IsList(Type) || this.NodeValue != null && ListDescriptor.IsList(this.NodeValue.GetType());

/// <summary>
/// Gets whether this node contains a collection.
/// </summary>
/// <remarks>Used mostly for sorting purpose.</remarks>
/// <seealso cref="HasDictionary"/>
public bool HasCollection => OldCollectionDescriptor.IsCollection(Type);
public bool HasCollection => OldCollectionDescriptor.IsCollection(Type) || this.NodeValue != null && OldCollectionDescriptor.IsCollection(this.NodeValue.GetType());

/// <summary>
/// Gets whether this node contains a dictionary.
/// </summary>
/// <remarks>Usually a dictionary is also a collection.</remarks>
/// <seealso cref="HasCollection"/>
public bool HasDictionary => DictionaryDescriptor.IsDictionary(Type);
public bool HasDictionary => DictionaryDescriptor.IsDictionary(Type) || this.NodeValue != null && DictionaryDescriptor.IsDictionary(this.NodeValue.GetType());

/// <summary>
/// Gets whether this node contains a set.
/// </summary>
/// <remarks>Usually a set is also a collection.</remarks>
/// <seealso cref="HasCollection"/>
public bool HasSet => SetDescriptor.IsSet(Type);
public bool HasSet => SetDescriptor.IsSet(Type) || this.NodeValue != null && SetDescriptor.IsSet(this.NodeValue.GetType());

/// <summary>
/// Gets whether this node contains any kind of collection.
/// </summary>
public bool HasAnyCollection => HasList || HasCollection || HasDictionary || HasSet;

/// <summary>
/// Gets the number of visible children.
Expand Down

0 comments on commit 3f710c6

Please sign in to comment.