diff --git a/sources/assets/Stride.Core.Assets/Analysis/AssetCollision.cs b/sources/assets/Stride.Core.Assets/Analysis/AssetCollision.cs
index 41010a56f2..b12888e60d 100644
--- a/sources/assets/Stride.Core.Assets/Analysis/AssetCollision.cs
+++ b/sources/assets/Stride.Core.Assets/Analysis/AssetCollision.cs
@@ -27,7 +27,7 @@ public static class AssetCollision
/// assetResolver
///
/// List cannot contain null items;inputItems
- public static void Clean(Package package, ICollection inputItems, ICollection outputItems, AssetResolver assetResolver, bool cloneInput, bool removeUnloadableObjects)
+ public static void Clean(Package? package, ICollection inputItems, ICollection outputItems, AssetResolver assetResolver, bool cloneInput, bool removeUnloadableObjects)
{
ArgumentNullException.ThrowIfNull(inputItems);
ArgumentNullException.ThrowIfNull(outputItems);
diff --git a/sources/editor/Stride.Assets.Editor.Avalonia/Module.cs b/sources/editor/Stride.Assets.Editor.Avalonia/Module.cs
index c3ad130fc5..7093ac3317 100644
--- a/sources/editor/Stride.Assets.Editor.Avalonia/Module.cs
+++ b/sources/editor/Stride.Assets.Editor.Avalonia/Module.cs
@@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
-using Stride.Core.Assets.Presentation;
+using Stride.Core.Assets.Presentation.Services;
using Stride.Editor.Avalonia.Preview.Views;
using Stride.Editor.Preview;
diff --git a/sources/editor/Stride.Assets.Editor.Avalonia/StrideEditorViewPlugin.cs b/sources/editor/Stride.Assets.Editor.Avalonia/StrideEditorViewPlugin.cs
index 6e64d3733c..2eb18e763b 100644
--- a/sources/editor/Stride.Assets.Editor.Avalonia/StrideEditorViewPlugin.cs
+++ b/sources/editor/Stride.Assets.Editor.Avalonia/StrideEditorViewPlugin.cs
@@ -3,7 +3,6 @@
using System.Reflection;
using Stride.Assets.Editor.Avalonia.Views;
-using Stride.Core.Assets.Editor;
using Stride.Core.Assets.Editor.Services;
using Stride.Core.Assets.Presentation.ViewModels;
using Stride.Core.Diagnostics;
diff --git a/sources/editor/Stride.Assets.Editor.Avalonia/Views/EntityHierarchyEditorView.axaml b/sources/editor/Stride.Assets.Editor.Avalonia/Views/EntityHierarchyEditorView.axaml
index 6296b3709e..881bd1c6d9 100644
--- a/sources/editor/Stride.Assets.Editor.Avalonia/Views/EntityHierarchyEditorView.axaml
+++ b/sources/editor/Stride.Assets.Editor.Avalonia/Views/EntityHierarchyEditorView.axaml
@@ -3,11 +3,25 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:sd="http://schemas.stride3d.net/xaml/presentation"
- xmlns:vm="using:Stride.Assets.Editor.ViewModels"
- xmlns:vm2="using:Stride.Assets.Presentation.ViewModels"
+ xmlns:aevm="using:Stride.Assets.Editor.ViewModels"
+ xmlns:apvm="using:Stride.Assets.Presentation.ViewModels"
+ xmlns:cpc="using:Stride.Core.Presentation.Commands"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Stride.Assets.Editor.Avalonia.Views.EntityHierarchyEditorView"
- x:DataType="vm:EntityHierarchyEditorViewModel">
+ x:DataType="aevm:EntityHierarchyEditorViewModel">
+
+
+
+
+
+
+
+
+
-
-
@@ -28,7 +42,7 @@
-
@@ -38,6 +52,7 @@
diff --git a/sources/editor/Stride.Assets.Editor/Components/CopyPasteProcessors/EntityComponentCopyProcessor.cs b/sources/editor/Stride.Assets.Editor/Components/CopyPasteProcessors/EntityComponentCopyProcessor.cs
new file mode 100644
index 0000000000..008b7753d8
--- /dev/null
+++ b/sources/editor/Stride.Assets.Editor/Components/CopyPasteProcessors/EntityComponentCopyProcessor.cs
@@ -0,0 +1,48 @@
+// 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 Stride.Core.Assets.Editor.Services;
+using Stride.Core.Assets.Yaml;
+using Stride.Engine;
+
+namespace Stride.Assets.Editor.Components.CopyPasteProcessors;
+
+internal sealed class EntityComponentCopyProcessor : ICopyProcessor
+{
+ ///
+ public bool Accept(Type dataType)
+ {
+ return dataType == typeof(TransformComponent) || dataType == typeof(EntityComponentCollection);
+ }
+
+ ///
+ public bool Process(ref object data, AttachedYamlAssetMetadata metadata)
+ {
+ switch (data)
+ {
+ case TransformComponent transform:
+ PatchTransformComponent(transform);
+ return true;
+
+ case EntityComponentCollection collection:
+ {
+ var processed = false;
+ foreach (var t in collection.OfType())
+ {
+ PatchTransformComponent(t);
+ processed = true;
+ }
+ return processed;
+ }
+
+ default:
+ return false;
+ }
+
+ static void PatchTransformComponent(TransformComponent transform)
+ {
+ // We don't want to copy the children of a transform component
+ transform.Children.Clear();
+ }
+ }
+}
diff --git a/sources/editor/Stride.Assets.Editor/Components/CopyPasteProcessors/EntityComponentPasteProcessor.cs b/sources/editor/Stride.Assets.Editor/Components/CopyPasteProcessors/EntityComponentPasteProcessor.cs
new file mode 100644
index 0000000000..7db4679911
--- /dev/null
+++ b/sources/editor/Stride.Assets.Editor/Components/CopyPasteProcessors/EntityComponentPasteProcessor.cs
@@ -0,0 +1,92 @@
+// 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 Stride.Core.Assets.Editor.Components.CopyPasteProcessors;
+using Stride.Core.Extensions;
+using Stride.Core.Quantum;
+using Stride.Core.Reflection;
+using Stride.Engine;
+
+namespace Stride.Assets.Editor.Components.CopyPasteProcessors;
+
+internal sealed class EntityComponentPasteProcessor : AssetPropertyPasteProcessor
+{
+ ///
+ public override bool Accept(Type targetRootType, Type targetMemberType, Type pastedDataType)
+ {
+ return (targetMemberType == typeof(EntityComponent) || targetMemberType == typeof(EntityComponentCollection)) &&
+ typeof(EntityComponent).IsAssignableFrom(TypeDescriptorFactory.Default.Find(pastedDataType).GetInnerCollectionType());
+ }
+
+ ///
+ protected override bool CanRemoveItem(IObjectNode collection, NodeIndex index)
+ {
+ if (collection.Type != typeof(EntityComponentCollection))
+ return base.CanRemoveItem(collection, index);
+
+ // Cannot remove the transform component
+ if (index.Int == 0)
+ return false;
+
+ return base.CanRemoveItem(collection, index);
+ }
+
+ ///
+ protected override bool CanInsertItem(IObjectNode collection, NodeIndex index, object? newItem)
+ {
+ if (collection.Type != typeof(EntityComponentCollection))
+ return base.CanInsertItem(collection, index, newItem);
+
+ if (newItem == null)
+ return false;
+
+ var componentType = newItem.GetType();
+ if (!EntityComponentAttributes.Get(componentType).AllowMultipleComponents)
+ {
+ // Cannot insert components that disallow multiple components
+ var components = (EntityComponentCollection)collection.Retrieve();
+ if (components.Any(x => x.GetType() == componentType))
+ return false;
+ }
+ return base.CanInsertItem(collection, index, newItem);
+ }
+
+ ///
+ protected override bool CanReplaceItem(IObjectNode collection, NodeIndex index, object? newItem)
+ {
+ if (collection.Type != typeof(EntityComponentCollection))
+ return base.CanReplaceItem(collection, index, newItem);
+
+ if (newItem == null)
+ return false;
+
+ var componentType = newItem.GetType();
+ // Cannot replace the transform component by another type of component
+ if (collection.IndexedTarget(index).Type == typeof(TransformComponent) && componentType != typeof(TransformComponent))
+ return false;
+
+ if (!EntityComponentAttributes.Get(componentType).AllowMultipleComponents)
+ {
+ // Cannot replace components that disallow multiple components, unless it is that specific component we're replacing
+ var components = (EntityComponentCollection)collection.Retrieve();
+ if (components.Where((x, i) => x.GetType() == componentType && i != index.Int).Any())
+ return false;
+ }
+ return base.CanReplaceItem(collection, index, newItem);
+ }
+
+ ///
+ protected override void ReplaceItem(IObjectNode collection, NodeIndex index, object? newItem)
+ {
+ // If we're replacing the transform component, only manually copy allowed properties to the existing one.
+ if (collection.Type == typeof(EntityComponentCollection) && newItem is TransformComponent newTransform)
+ {
+ var node = collection.IndexedTarget(index);
+ node[nameof(TransformComponent.Position)].Update(newTransform.Position);
+ node[nameof(TransformComponent.Rotation)].Update(newTransform.Rotation);
+ node[nameof(TransformComponent.Scale)].Update(newTransform.Scale);
+ return;
+ }
+ base.ReplaceItem(collection, index, newItem);
+ }
+}
diff --git a/sources/editor/Stride.Assets.Editor/Components/CopyPasteProcessors/EntityHierarchyPasteProcessor.cs b/sources/editor/Stride.Assets.Editor/Components/CopyPasteProcessors/EntityHierarchyPasteProcessor.cs
new file mode 100644
index 0000000000..56bf1893a5
--- /dev/null
+++ b/sources/editor/Stride.Assets.Editor/Components/CopyPasteProcessors/EntityHierarchyPasteProcessor.cs
@@ -0,0 +1,49 @@
+// 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 Stride.Assets.Entities;
+using Stride.Assets.Presentation.Quantum;
+using Stride.Core.Assets.Quantum;
+using Stride.Core.Assets;
+using Stride.Core.Quantum;
+using Stride.Core;
+using Stride.Core.Assets.Editor.Components.CopyPasteProcessors;
+using Stride.Core.Assets.Editor.Services;
+using Stride.Engine;
+
+namespace Stride.Assets.Editor.Components.CopyPasteProcessors;
+
+internal sealed class EntityHierarchyPasteProcessor : AssetCompositeHierarchyPasteProcessor
+{
+ public static readonly PropertyKey TargetFolderKey = new("TargetFolder", typeof(EntityHierarchyPasteProcessor));
+
+ public override Task Paste(IPasteItem pasteResultItem, AssetPropertyGraph assetPropertyGraph, ref NodeAccessor nodeAccessor, ref PropertyContainer propertyContainer)
+ {
+ if (pasteResultItem == null) throw new ArgumentNullException(nameof(pasteResultItem));
+
+ var propertyGraph = (EntityHierarchyPropertyGraph)assetPropertyGraph;
+ var parentEntity = nodeAccessor.RetrieveValue() as Entity;
+ propertyContainer.TryGetValue(TargetFolderKey, out var targetFolder);
+
+ if (pasteResultItem.Data is AssetCompositeHierarchyData hierarchy)
+ {
+ foreach (var rootEntity in hierarchy.RootParts)
+ {
+ var insertIndex = parentEntity?.Transform.Children.Count ?? propertyGraph.Asset.Hierarchy.RootParts.Count;
+ var entityDesign = hierarchy.Parts[rootEntity.Id];
+ var folder = targetFolder;
+ if (!string.IsNullOrEmpty(entityDesign.Folder))
+ {
+ if (!string.IsNullOrEmpty(targetFolder))
+ folder = folder + "/" + entityDesign.Folder;
+ else
+ folder = entityDesign.Folder;
+ }
+ entityDesign.Folder = folder ?? string.Empty;
+ propertyGraph.AddPartToAsset(hierarchy.Parts, entityDesign, parentEntity, insertIndex);
+ }
+ }
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/sources/editor/Stride.Assets.Editor/Components/CopyPasteProcessors/ScenePostPasteProcessor.cs b/sources/editor/Stride.Assets.Editor/Components/CopyPasteProcessors/ScenePostPasteProcessor.cs
new file mode 100644
index 0000000000..6f83802a79
--- /dev/null
+++ b/sources/editor/Stride.Assets.Editor/Components/CopyPasteProcessors/ScenePostPasteProcessor.cs
@@ -0,0 +1,18 @@
+// 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 Stride.Assets.Entities;
+using Stride.Core.Assets.Editor.Components.CopyPasteProcessors;
+
+namespace Stride.Assets.Editor.Components.CopyPasteProcessors;
+
+internal sealed class ScenePostPasteProcessor : AssetPostPasteProcessorBase
+{
+ ///
+ protected override void PostPasteDeserialization(SceneAsset asset)
+ {
+ // Clear all references (for now)
+ asset.Parent = null;
+ asset.ChildrenIds.Clear();
+ }
+}
diff --git a/sources/editor/Stride.Assets.Editor/Components/CopyPasteProcessors/UIHierarchyPasteProcessor.cs b/sources/editor/Stride.Assets.Editor/Components/CopyPasteProcessors/UIHierarchyPasteProcessor.cs
new file mode 100644
index 0000000000..b598531558
--- /dev/null
+++ b/sources/editor/Stride.Assets.Editor/Components/CopyPasteProcessors/UIHierarchyPasteProcessor.cs
@@ -0,0 +1,39 @@
+// 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 Stride.Assets.Presentation.Quantum;
+using Stride.Assets.UI;
+using Stride.Core.Assets.Quantum;
+using Stride.Core.Assets;
+using Stride.Core.Quantum;
+using Stride.Core;
+using Stride.Core.Assets.Editor.Components.CopyPasteProcessors;
+using Stride.Core.Assets.Editor.Services;
+using Stride.UI.Panels;
+using Stride.UI;
+
+namespace Stride.Assets.Editor.Components.CopyPasteProcessors;
+
+internal sealed class UIHierarchyPasteProcessor : AssetCompositeHierarchyPasteProcessor
+{
+ public override Task Paste(IPasteItem pasteResultItem, AssetPropertyGraph assetPropertyGraph, ref NodeAccessor nodeAccessor, ref PropertyContainer container)
+ {
+ if (pasteResultItem == null) throw new ArgumentNullException(nameof(pasteResultItem));
+
+ var propertyGraph = (UIAssetPropertyGraph)assetPropertyGraph;
+ var parentElement = nodeAccessor.RetrieveValue() as UIElement;
+
+ // 1. try to paste as hierarchy
+ if (pasteResultItem.Data is AssetCompositeHierarchyData hierarchy)
+ {
+ // Note: check that adding or inserting is supported is done in CanPaste()
+ foreach (var rootUIElement in hierarchy.RootParts)
+ {
+ var asset = (UIAssetBase)propertyGraph.Asset;
+ var insertIndex = parentElement == null ? asset.Hierarchy.RootParts.Count : ((parentElement as Panel)?.Children.Count ?? 0);
+ propertyGraph.AddPartToAsset(hierarchy.Parts, hierarchy.Parts[rootUIElement.Id], parentElement, insertIndex);
+ }
+ }
+ return Task.CompletedTask;
+ }
+}
diff --git a/sources/editor/Stride.Assets.Editor/Module.cs b/sources/editor/Stride.Assets.Editor/Module.cs
index e224d2ff5c..05b6d8df2f 100644
--- a/sources/editor/Stride.Assets.Editor/Module.cs
+++ b/sources/editor/Stride.Assets.Editor/Module.cs
@@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
-using Stride.Core.Assets.Presentation;
+using Stride.Core.Assets.Presentation.Services;
namespace Stride.Assets.Editor;
diff --git a/sources/editor/Stride.Assets.Editor/StrideEditorPlugin.cs b/sources/editor/Stride.Assets.Editor/StrideEditorPlugin.cs
index 09c73aefa2..766a8617bb 100644
--- a/sources/editor/Stride.Assets.Editor/StrideEditorPlugin.cs
+++ b/sources/editor/Stride.Assets.Editor/StrideEditorPlugin.cs
@@ -2,10 +2,11 @@
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
using System.Reflection;
+using Stride.Assets.Editor.Components.CopyPasteProcessors;
using Stride.Assets.Editor.Quantum.NodePresenters.Commands;
using Stride.Assets.Editor.Quantum.NodePresenters.Updaters;
using Stride.Core.Assets;
-using Stride.Core.Assets.Editor;
+using Stride.Core.Assets.Editor.Services;
using Stride.Core.Assets.Editor.ViewModels;
using Stride.Core.Assets.Presentation.ViewModels;
using Stride.Core.Diagnostics;
@@ -60,6 +61,15 @@ public override void InitializeSession(ISessionViewModel session)
//var thumbnailService = new GameStudioThumbnailService((SessionViewModel)session, settingsProvider, builderService);
//session.ServiceProvider.RegisterService(thumbnailService);
+ if (session.ServiceProvider.TryGet() is { } copyPasteService)
+ {
+ copyPasteService.RegisterProcessor(new EntityComponentCopyProcessor());
+ copyPasteService.RegisterProcessor(new EntityComponentPasteProcessor());
+ copyPasteService.RegisterProcessor(new EntityHierarchyPasteProcessor());
+ copyPasteService.RegisterProcessor(new UIHierarchyPasteProcessor());
+ copyPasteService.RegisterProcessor(new ScenePostPasteProcessor());
+ }
+
if (session is SessionViewModel sessionVm)
{
// commands
diff --git a/sources/editor/Stride.Assets.Editor/ViewModels/EntityHierarchyEditorViewModel.cs b/sources/editor/Stride.Assets.Editor/ViewModels/EntityHierarchyEditorViewModel.cs
index 5dc72eb233..178cd9a087 100644
--- a/sources/editor/Stride.Assets.Editor/ViewModels/EntityHierarchyEditorViewModel.cs
+++ b/sources/editor/Stride.Assets.Editor/ViewModels/EntityHierarchyEditorViewModel.cs
@@ -4,12 +4,14 @@
using System.Collections.Specialized;
using Stride.Assets.Entities;
using Stride.Assets.Presentation.ViewModels;
+using Stride.Core.Assets;
+using Stride.Core.Assets.Editor.Quantum;
using Stride.Core.Assets.Editor.ViewModels;
using Stride.Engine;
namespace Stride.Assets.Editor.ViewModels;
-public abstract class EntityHierarchyEditorViewModel : AssetCompositeHierarchyEditorViewModel
+public abstract class EntityHierarchyEditorViewModel : AssetCompositeHierarchyEditorViewModel
{
protected EntityHierarchyEditorViewModel(EntityHierarchyViewModel asset)
: base(asset)
@@ -18,11 +20,74 @@ protected EntityHierarchyEditorViewModel(EntityHierarchyViewModel asset)
public EntityHierarchyRootViewModel HierarchyRoot => (EntityHierarchyRootViewModel)RootPart;
+ ///
+ protected override bool CanDelete()
+ {
+ return SelectedContent.Count > 0 && !SelectedContent.Contains(HierarchyRoot);
+ }
+
+ ///
+ protected override bool CanPaste(bool asRoot)
+ {
+ if (!base.CanPaste(asRoot))
+ return false;
+
+ return CopyPasteService!.CanPaste(
+ ClipboardService!.GetTextAsync().Result, Asset.AssetType,
+ asRoot ? typeof(AssetCompositeHierarchyData) : typeof(Entity),
+ typeof(AssetCompositeHierarchyData), typeof(EntityComponent));
+ }
+
+ ///
+ protected override async Task Delete()
+ {
+ var entitiesToDelete = GetCommonRoots(SelectedItems);
+ // FIXME xplat-editor
+ //var ask = SceneEditorSettings.AskBeforeDeletingEntities.GetValue();
+ //if (ask)
+ //{
+ // var confirmMessage = Tr._p("Message", "Are you sure you want to delete this entity?");
+ // // TODO: we should compute the actual total number of entities to be deleted here (children recursively, etc.)
+ // if (entitiesToDelete.Count > 1)
+ // confirmMessage = string.Format(Tr._p("Message", "Are you sure you want to delete these {0} entities?"), entitiesToDelete.Count);
+ // var checkedMessage = string.Format(Stride.Core.Assets.Editor.Settings.EditorSettings.AlwaysDeleteWithoutAsking, "entities");
+ // var buttons = DialogHelper.CreateButtons(new[] { Tr._p("Button", "Delete"), Tr._p("Button", "Cancel") }, 1, 2);
+ // var result = await ServiceProvider.Get().CheckedMessageBoxAsync(confirmMessage, false, checkedMessage, buttons, MessageBoxImage.Question);
+ // if (result.Result != 1)
+ // return;
+ // if (result.IsChecked == true)
+ // {
+ // SceneEditorSettings.AskBeforeDeletingEntities.SetValue(false);
+ // SceneEditorSettings.Save();
+ // }
+ //}
+
+ using var transaction = Session.ActionService?.CreateTransaction();
+ //var foldersToDelete = SelectedContent.OfType().ToList();
+ ClearSelection();
+
+ // Delete entities first
+ var entitiesPerScene = entitiesToDelete.GroupBy(x => x.Asset);
+ foreach (var entities in entitiesPerScene)
+ {
+ entities.Key.AssetHierarchyPropertyGraph.DeleteParts(entities.Select(x => x.PartDesign), out var mapping);
+ Session.ActionService?.PushOperation(new DeletedPartsTrackingOperation(entities.Key, mapping));
+ }
+
+ //// Then folders
+ //foreach (var folder in foldersToDelete)
+ //{
+ // folder.Delete();
+ //}
+
+ Session.ActionService?.SetName(transaction!, "Delete selected entities");
+ }
+
///
protected override async Task RefreshEditorProperties()
{
- EditorProperties.UpdateTypeAndName(SelectedItems, x => "Entity", x => x.Name, "entities");
- await EditorProperties.GenerateSelectionPropertiesAsync(SelectedItems.OfType());
+ EditorProperties.UpdateTypeAndName(SelectedItems, _ => "Entity", x => x.Name ?? string.Empty, "entities");
+ await EditorProperties.GenerateSelectionPropertiesAsync(SelectedItems);
}
///
diff --git a/sources/editor/Stride.Assets.Editor/ViewModels/PrefabEditorViewModel.cs b/sources/editor/Stride.Assets.Editor/ViewModels/PrefabEditorViewModel.cs
index 45e769dc5a..fab9ad5aa8 100644
--- a/sources/editor/Stride.Assets.Editor/ViewModels/PrefabEditorViewModel.cs
+++ b/sources/editor/Stride.Assets.Editor/ViewModels/PrefabEditorViewModel.cs
@@ -13,14 +13,9 @@ public sealed class PrefabEditorViewModel : EntityHierarchyEditorViewModel, IAss
public PrefabEditorViewModel(PrefabViewModel asset)
: base(asset)
{
+ RootPart = new PrefabRootViewModel(Asset);
}
///
public override PrefabViewModel Asset => (PrefabViewModel)base.Asset;
-
- ///
- protected override PrefabRootViewModel CreateRootPartViewModel()
- {
- return new PrefabRootViewModel(Asset);
- }
}
diff --git a/sources/editor/Stride.Assets.Editor/ViewModels/SceneEditorViewModel.cs b/sources/editor/Stride.Assets.Editor/ViewModels/SceneEditorViewModel.cs
index c1e06a6ecf..4d12efeca9 100644
--- a/sources/editor/Stride.Assets.Editor/ViewModels/SceneEditorViewModel.cs
+++ b/sources/editor/Stride.Assets.Editor/ViewModels/SceneEditorViewModel.cs
@@ -13,14 +13,37 @@ public sealed class SceneEditorViewModel : EntityHierarchyEditorViewModel, IAsse
public SceneEditorViewModel(SceneViewModel asset)
: base(asset)
{
+ RootPart = new SceneRootViewModel(Asset);
}
///
public override SceneViewModel Asset => (SceneViewModel)base.Asset;
///
- protected override SceneRootViewModel CreateRootPartViewModel()
+ protected override Task Delete()
{
- return new SceneRootViewModel(Asset);
+ var sceneRoots = SelectedContent.OfType().ToList();
+ // Mix of scene roots and entities selected
+ if (sceneRoots.Count != SelectedContent.Count)
+ return base.Delete();
+
+ using var transaction = Session.ActionService?.CreateTransaction();
+ ClearSelection();
+ foreach (var sceneRoot in GetCommonRoots(sceneRoots))
+ {
+ DeleteSceneRoot(sceneRoot);
+ }
+ Session.ActionService?.SetName(transaction!, "Remove selected child scenes");
+ return Task.CompletedTask;
}
+
+ private void DeleteSceneRoot(SceneRootViewModel sceneRoot)
+ {
+ if (sceneRoot.Parent is SceneRootViewModel parent)
+ {
+ // Reset parenting link
+ parent.Asset.Children.Remove(Asset);
+ }
+ }
+
}
diff --git a/sources/editor/Stride.Assets.Editor/ViewModels/UIEditorBaseViewModel.cs b/sources/editor/Stride.Assets.Editor/ViewModels/UIEditorBaseViewModel.cs
index cfb2b37619..f8ec1d841e 100644
--- a/sources/editor/Stride.Assets.Editor/ViewModels/UIEditorBaseViewModel.cs
+++ b/sources/editor/Stride.Assets.Editor/ViewModels/UIEditorBaseViewModel.cs
@@ -8,7 +8,7 @@
namespace Stride.Assets.Editor.ViewModels;
-public abstract class UIEditorBaseViewModel : AssetCompositeHierarchyEditorViewModel
+public abstract class UIEditorBaseViewModel : AssetCompositeHierarchyEditorViewModel
{
protected UIEditorBaseViewModel(UIBaseViewModel asset)
: base(asset)
@@ -22,7 +22,7 @@ protected override Task RefreshEditorProperties()
{
// note: here we are assuming that all items are UIElementViewModel.
// if that were to change, revisit this code.
- EditorProperties.UpdateTypeAndName(SelectedItems.OfType(), SelectedItems.Count, e => e.ElementType.Name, e => e.AssetSideUIElement.Name, "elements");
- return EditorProperties.GenerateSelectionPropertiesAsync(SelectedItems.OfType());
+ EditorProperties.UpdateTypeAndName(SelectedItems, SelectedItems.Count, e => e.ElementType.Name, e => e.AssetSideUIElement.Name, "elements");
+ return EditorProperties.GenerateSelectionPropertiesAsync(SelectedItems);
}
}
diff --git a/sources/editor/Stride.Assets.Editor/ViewModels/UILibraryEditorViewModel.cs b/sources/editor/Stride.Assets.Editor/ViewModels/UILibraryEditorViewModel.cs
index 83b099ddad..7b9aa90b7c 100644
--- a/sources/editor/Stride.Assets.Editor/ViewModels/UILibraryEditorViewModel.cs
+++ b/sources/editor/Stride.Assets.Editor/ViewModels/UILibraryEditorViewModel.cs
@@ -13,14 +13,15 @@ public sealed class UILibraryEditorViewModel : UIEditorBaseViewModel, IAssetEdit
public UILibraryEditorViewModel(UILibraryViewModel asset)
: base(asset)
{
+ RootPart = new UILibraryRootViewModel(Asset);
}
///
public override UILibraryViewModel Asset => (UILibraryViewModel)base.Asset;
///
- protected override UILibraryRootViewModel CreateRootPartViewModel()
+ protected override Task Delete()
{
- return new UILibraryRootViewModel(Asset);
+ throw new NotImplementedException();
}
}
diff --git a/sources/editor/Stride.Assets.Editor/ViewModels/UIPageEditorViewModel.cs b/sources/editor/Stride.Assets.Editor/ViewModels/UIPageEditorViewModel.cs
index d5eb2a4ffd..eb82a50037 100644
--- a/sources/editor/Stride.Assets.Editor/ViewModels/UIPageEditorViewModel.cs
+++ b/sources/editor/Stride.Assets.Editor/ViewModels/UIPageEditorViewModel.cs
@@ -13,14 +13,15 @@ public sealed class UIPageEditorViewModel : UIEditorBaseViewModel, IAssetEditorV
public UIPageEditorViewModel(UIPageViewModel asset)
: base(asset)
{
+ RootPart = new UIPageRootViewModel(Asset);
}
///
public override UIPageViewModel Asset => (UIPageViewModel)base.Asset;
///
- protected override UIPageRootViewModel CreateRootPartViewModel()
+ protected override Task Delete()
{
- return new UIPageRootViewModel(Asset);
+ throw new NotImplementedException();
}
}
diff --git a/sources/editor/Stride.Assets.Presentation/Module.cs b/sources/editor/Stride.Assets.Presentation/Module.cs
index 0dca3339e0..4fbd4163ad 100644
--- a/sources/editor/Stride.Assets.Presentation/Module.cs
+++ b/sources/editor/Stride.Assets.Presentation/Module.cs
@@ -3,7 +3,7 @@
using System.Runtime.CompilerServices;
using Stride.Assets.Materials;
-using Stride.Core.Assets.Presentation;
+using Stride.Core.Assets.Presentation.Services;
using Stride.Core.Assets.Quantum;
using Stride.Core.Reflection;
diff --git a/sources/editor/Stride.Assets.Presentation/StrideDefaultAssetsPlugin.cs b/sources/editor/Stride.Assets.Presentation/StrideDefaultAssetsPlugin.cs
index 468fad1544..826cec906c 100644
--- a/sources/editor/Stride.Assets.Presentation/StrideDefaultAssetsPlugin.cs
+++ b/sources/editor/Stride.Assets.Presentation/StrideDefaultAssetsPlugin.cs
@@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
-using Stride.Core.Assets.Presentation;
+using Stride.Core.Assets.Presentation.Services;
using Stride.Core.Assets.Presentation.ViewModels;
using Stride.Core.Diagnostics;
diff --git a/sources/editor/Stride.Assets.Presentation/ViewModels/EntityViewModel.cs b/sources/editor/Stride.Assets.Presentation/ViewModels/EntityViewModel.cs
index 0be7bcadb7..a229c1c602 100644
--- a/sources/editor/Stride.Assets.Presentation/ViewModels/EntityViewModel.cs
+++ b/sources/editor/Stride.Assets.Presentation/ViewModels/EntityViewModel.cs
@@ -12,7 +12,7 @@
namespace Stride.Assets.Presentation.ViewModels;
-public sealed class EntityViewModel : EntityHierarchyItemViewModel, IAssetPropertyProviderViewModel
+public sealed class EntityViewModel : EntityHierarchyItemViewModel, IPartDesignViewModel, IAssetPropertyProviderViewModel
{
private readonly MemberGraphNodeBinding nameNodeBinding;
private readonly ObjectGraphNodeBinding componentsNodeBinding;
@@ -20,16 +20,17 @@ public sealed class EntityViewModel : EntityHierarchyItemViewModel, IAssetProper
public EntityViewModel(EntityHierarchyViewModel asset, EntityDesign entityDesign)
: base(asset, GetOrCreateChildPartDesigns((EntityHierarchyAssetBase)asset.Asset, entityDesign))
{
- EntityDesign = entityDesign;
+ PartDesign = entityDesign;
var assetNode = asset.Session.AssetNodeContainer.GetOrCreateNode(entityDesign.Entity);
nameNodeBinding = new MemberGraphNodeBinding(assetNode[nameof(Entity.Name)], nameof(Name), OnPropertyChanging, OnPropertyChanged, ServiceProvider.TryGet());
componentsNodeBinding = new ObjectGraphNodeBinding(assetNode[nameof(Entity.Components)].Target!, nameof(Components), OnPropertyChanging, OnPropertyChanged, ServiceProvider.TryGet(), false);
}
- public IEnumerable Components => componentsNodeBinding.GetNodeValue();
+ public Entity AssetSideEntity => PartDesign.Entity;
+
- public Entity AssetSideEntity => EntityDesign.Entity;
+ public IEnumerable Components => componentsNodeBinding.GetNodeValue();
///
public override AbsoluteId Id => new(Asset.Id, AssetSideEntity.Id);
@@ -44,7 +45,8 @@ public override string? Name
set => nameNodeBinding.Value = value;
}
- internal EntityDesign EntityDesign { get; }
+ ///
+ public EntityDesign PartDesign { get; }
bool IPropertyProviderViewModel.CanProvidePropertiesViewModel => true;
@@ -59,7 +61,7 @@ public override GraphNodePath GetNodePath()
path.PushMember(nameof(EntityHierarchy.Hierarchy.Parts));
path.PushTarget();
path.PushIndex(new NodeIndex(Id.ObjectId));
- path.PushMember(nameof(EntityDesign.Entity));
+ path.PushMember(nameof(PartDesign.Entity));
path.PushTarget();
return path;
}
diff --git a/sources/editor/Stride.Assets.Presentation/ViewModels/PrefabViewModel.cs b/sources/editor/Stride.Assets.Presentation/ViewModels/PrefabViewModel.cs
index 3d17134d1e..331deb7772 100644
--- a/sources/editor/Stride.Assets.Presentation/ViewModels/PrefabViewModel.cs
+++ b/sources/editor/Stride.Assets.Presentation/ViewModels/PrefabViewModel.cs
@@ -19,5 +19,5 @@ public PrefabViewModel(ConstructorParameters parameters)
}
///
- public new PrefabAsset Asset => (PrefabAsset)base.Asset;
+ public override PrefabAsset Asset => (PrefabAsset)base.Asset;
}
diff --git a/sources/editor/Stride.Assets.Presentation/ViewModels/SceneRootViewModel.cs b/sources/editor/Stride.Assets.Presentation/ViewModels/SceneRootViewModel.cs
index b3f7946389..6d3451838a 100644
--- a/sources/editor/Stride.Assets.Presentation/ViewModels/SceneRootViewModel.cs
+++ b/sources/editor/Stride.Assets.Presentation/ViewModels/SceneRootViewModel.cs
@@ -21,5 +21,9 @@ public SceneRootViewModel(SceneViewModel asset)
///
public override AbsoluteId Id => new(Asset.Id, sceneId);
+ ///
public override string? Name { get => "SceneRoot"; set => throw new NotSupportedException($"Cannot change the name of a {nameof(SceneRootViewModel)} object."); }
+
+ ///
+ public override SceneViewModel Asset => (SceneViewModel)base.Asset;
}
diff --git a/sources/editor/Stride.Assets.Presentation/ViewModels/SceneViewModel.cs b/sources/editor/Stride.Assets.Presentation/ViewModels/SceneViewModel.cs
index cff37f9b4a..1597e446a0 100644
--- a/sources/editor/Stride.Assets.Presentation/ViewModels/SceneViewModel.cs
+++ b/sources/editor/Stride.Assets.Presentation/ViewModels/SceneViewModel.cs
@@ -4,6 +4,7 @@
using Stride.Assets.Entities;
using Stride.Core.Assets.Presentation.Annotations;
using Stride.Core.Assets.Presentation.ViewModels;
+using Stride.Core.Presentation.Collections;
namespace Stride.Assets.Presentation.ViewModels;
@@ -19,5 +20,7 @@ public SceneViewModel(ConstructorParameters parameters)
}
///
- public new SceneAsset Asset => (SceneAsset)base.Asset;
+ public override SceneAsset Asset => (SceneAsset)base.Asset;
+
+ public IObservableList Children { get; } = new ObservableSet();
}
diff --git a/sources/editor/Stride.Assets.Presentation/ViewModels/UIElementViewModel.cs b/sources/editor/Stride.Assets.Presentation/ViewModels/UIElementViewModel.cs
index 1c258eb92e..7b3fdac263 100644
--- a/sources/editor/Stride.Assets.Presentation/ViewModels/UIElementViewModel.cs
+++ b/sources/editor/Stride.Assets.Presentation/ViewModels/UIElementViewModel.cs
@@ -13,7 +13,7 @@
namespace Stride.Assets.Presentation.ViewModels;
-public class UIElementViewModel : UIHierarchyItemViewModel, IAssetPropertyProviderViewModel
+public sealed class UIElementViewModel : UIHierarchyItemViewModel, IPartDesignViewModel, IAssetPropertyProviderViewModel
{
private string? name;
@@ -28,8 +28,10 @@ public UIElementViewModel(UIBaseViewModel asset, UIElementDesign elementDesign)
public Type ElementType { get; }
- public AbsoluteId Id => new(Asset.Id, AssetSideUIElement.Id);
+ ///
+ public override AbsoluteId Id => new(Asset.Id, AssetSideUIElement.Id);
+ ///
public override string? Name
{
get => name;
@@ -54,13 +56,15 @@ public override GraphNodePath GetNodePath()
AssetViewModel IAssetPropertyProviderViewModel.RelatedAsset => Asset;
+ UIElementDesign IPartDesignViewModel.PartDesign => UIElementDesign;
+
bool IPropertyProviderViewModel.CanProvidePropertiesViewModel => true;
- private static IEnumerable GetOrCreateChildPartDesigns( UIAssetBase asset, UIElementDesign elementDesign)
+ private static IEnumerable GetOrCreateChildPartDesigns(UIAssetBase asset, UIElementDesign elementDesign)
{
switch (elementDesign.UIElement)
{
- case ContentControl control:
+ case ContentControl control:
if (control.Content != null)
{
if (!asset.Hierarchy.Parts.TryGetValue(control.Content.Id, out var partDesign))
diff --git a/sources/editor/Stride.Assets.Presentation/ViewModels/UIHierarchyItemViewModel.cs b/sources/editor/Stride.Assets.Presentation/ViewModels/UIHierarchyItemViewModel.cs
index fbb2567168..07f0b53be8 100644
--- a/sources/editor/Stride.Assets.Presentation/ViewModels/UIHierarchyItemViewModel.cs
+++ b/sources/editor/Stride.Assets.Presentation/ViewModels/UIHierarchyItemViewModel.cs
@@ -2,11 +2,12 @@
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
using Stride.Assets.UI;
+using Stride.Core;
using Stride.Core.Assets.Presentation.ViewModels;
namespace Stride.Assets.Presentation.ViewModels;
-public abstract class UIHierarchyItemViewModel : AssetCompositeItemViewModel
+public abstract class UIHierarchyItemViewModel : AssetCompositeItemViewModel, IAssetPartViewModel
{
protected UIHierarchyItemViewModel(UIBaseViewModel asset, IEnumerable childElements)
: base(asset)
@@ -14,5 +15,8 @@ protected UIHierarchyItemViewModel(UIBaseViewModel asset, IEnumerable
+ public abstract AbsoluteId Id { get; }
+
protected UIAssetBase UIAsset => (UIAssetBase)Asset.Asset;
}
diff --git a/sources/editor/Stride.Assets.Presentation/ViewModels/UILibraryRootViewModel.cs b/sources/editor/Stride.Assets.Presentation/ViewModels/UILibraryRootViewModel.cs
index 030bb60f2f..c194ad12db 100644
--- a/sources/editor/Stride.Assets.Presentation/ViewModels/UILibraryRootViewModel.cs
+++ b/sources/editor/Stride.Assets.Presentation/ViewModels/UILibraryRootViewModel.cs
@@ -1,6 +1,7 @@
// 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 Stride.Core;
using Stride.Core.Assets;
namespace Stride.Assets.Presentation.ViewModels;
@@ -12,5 +13,8 @@ public UILibraryRootViewModel(UILibraryViewModel asset)
{
}
+ ///
+ public override AbsoluteId Id => new(Asset.Id, Guid.Empty);
+
public override string? Name { get => "UILibraryRoot"; set => throw new NotSupportedException($"Cannot change the name of a {nameof(UILibraryRootViewModel)} object."); }
}
diff --git a/sources/editor/Stride.Assets.Presentation/ViewModels/UILibraryViewModel.cs b/sources/editor/Stride.Assets.Presentation/ViewModels/UILibraryViewModel.cs
index 181aa05c61..8a9ad08aba 100644
--- a/sources/editor/Stride.Assets.Presentation/ViewModels/UILibraryViewModel.cs
+++ b/sources/editor/Stride.Assets.Presentation/ViewModels/UILibraryViewModel.cs
@@ -19,5 +19,5 @@ public UILibraryViewModel(ConstructorParameters parameters)
}
///
- public new UILibraryAsset Asset => (UILibraryAsset)base.Asset;
+ public override UILibraryAsset Asset => (UILibraryAsset)base.Asset;
}
diff --git a/sources/editor/Stride.Assets.Presentation/ViewModels/UIPageRootViewModel.cs b/sources/editor/Stride.Assets.Presentation/ViewModels/UIPageRootViewModel.cs
index 3ec66f6d2f..a756b2b189 100644
--- a/sources/editor/Stride.Assets.Presentation/ViewModels/UIPageRootViewModel.cs
+++ b/sources/editor/Stride.Assets.Presentation/ViewModels/UIPageRootViewModel.cs
@@ -1,6 +1,7 @@
// 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 Stride.Core;
using Stride.Core.Assets;
using Stride.Core.Extensions;
@@ -13,5 +14,8 @@ public UIPageRootViewModel(UIPageViewModel asset)
{
}
+ ///
+ public override AbsoluteId Id => new(Asset.Id, Guid.Empty);
+
public override string? Name { get => "UIPageRoot"; set => throw new NotSupportedException($"Cannot change the name of a {nameof(UIPageRootViewModel)} object."); }
}
diff --git a/sources/editor/Stride.Assets.Presentation/ViewModels/UIPageViewModel.cs b/sources/editor/Stride.Assets.Presentation/ViewModels/UIPageViewModel.cs
index a601d03b8b..f813ea9d3c 100644
--- a/sources/editor/Stride.Assets.Presentation/ViewModels/UIPageViewModel.cs
+++ b/sources/editor/Stride.Assets.Presentation/ViewModels/UIPageViewModel.cs
@@ -19,5 +19,5 @@ public UIPageViewModel(ConstructorParameters parameters)
}
///
- public new UIPageAsset Asset => (UIPageAsset)base.Asset;
+ public override UIPageAsset Asset => (UIPageAsset)base.Asset;
}
diff --git a/sources/editor/Stride.Core.Assets.Editor.Avalonia/Module.cs b/sources/editor/Stride.Core.Assets.Editor.Avalonia/Module.cs
index c5adce8ed6..0134af3fea 100644
--- a/sources/editor/Stride.Core.Assets.Editor.Avalonia/Module.cs
+++ b/sources/editor/Stride.Core.Assets.Editor.Avalonia/Module.cs
@@ -1,7 +1,7 @@
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
-using Stride.Core.Assets.Presentation;
+using Stride.Core.Assets.Presentation.Services;
namespace Stride.Core.Assets.Editor.Avalonia;
diff --git a/sources/editor/Stride.Core.Assets.Editor.Avalonia/StrideCoreEditorViewPlugin.cs b/sources/editor/Stride.Core.Assets.Editor.Avalonia/StrideCoreEditorViewPlugin.cs
index f0acc7b010..0049142702 100644
--- a/sources/editor/Stride.Core.Assets.Editor.Avalonia/StrideCoreEditorViewPlugin.cs
+++ b/sources/editor/Stride.Core.Assets.Editor.Avalonia/StrideCoreEditorViewPlugin.cs
@@ -2,6 +2,7 @@
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
using Stride.Core.Assets.Editor.Avalonia.Views;
+using Stride.Core.Assets.Editor.Services;
using Stride.Core.Assets.Presentation.ViewModels;
using Stride.Core.Diagnostics;
using Stride.Core.Presentation.Avalonia.Views;
diff --git a/sources/editor/Stride.Core.Assets.Editor/Components/CopyPasteProcessors/AssetItemPasteProcessor.cs b/sources/editor/Stride.Core.Assets.Editor/Components/CopyPasteProcessors/AssetItemPasteProcessor.cs
index a9a308080f..c1535bd082 100644
--- a/sources/editor/Stride.Core.Assets.Editor/Components/CopyPasteProcessors/AssetItemPasteProcessor.cs
+++ b/sources/editor/Stride.Core.Assets.Editor/Components/CopyPasteProcessors/AssetItemPasteProcessor.cs
@@ -4,7 +4,7 @@
using Microsoft.CSharp.RuntimeBinder;
using Stride.Core.Assets.Analysis;
using Stride.Core.Assets.Editor.Services;
-using Stride.Core.Assets.Editor.ViewModels;
+using Stride.Core.Assets.Presentation.ViewModels;
using Stride.Core.Assets.Quantum;
using Stride.Core.Assets.Yaml;
using Stride.Core.Extensions;
@@ -15,11 +15,11 @@ namespace Stride.Core.Assets.Editor.Components.CopyPasteProcessors;
///
/// Paste processor for collection of .
///
-public sealed class AssetItemPasteProcessor : PasteProcessorBase
+internal sealed class AssetItemPasteProcessor : PasteProcessorBase
{
- private readonly SessionViewModel session;
+ private readonly ISessionViewModel session;
- public AssetItemPasteProcessor(SessionViewModel session)
+ public AssetItemPasteProcessor(ISessionViewModel session)
{
this.session = session;
}
@@ -36,8 +36,7 @@ public override bool ProcessDeserializedData(AssetPropertyGraphContainer graphCo
{
var collectionDescriptor = (CollectionDescriptor)TypeDescriptorFactory.Default.Find(targetRootObject.GetType());
- var collection = data as IList;
- if (collection == null)
+ if (data is not IList collection)
{
collection = (IList)Activator.CreateInstance(collectionDescriptor.Type, true);
collectionDescriptor.Add(collection, data);
diff --git a/sources/editor/Stride.Core.Assets.Editor/Components/CopyPasteProcessors/AssetPostPasteProcessorBase.cs b/sources/editor/Stride.Core.Assets.Editor/Components/CopyPasteProcessors/AssetPostPasteProcessorBase.cs
index cee782cee0..76e5823372 100644
--- a/sources/editor/Stride.Core.Assets.Editor/Components/CopyPasteProcessors/AssetPostPasteProcessorBase.cs
+++ b/sources/editor/Stride.Core.Assets.Editor/Components/CopyPasteProcessors/AssetPostPasteProcessorBase.cs
@@ -20,7 +20,7 @@ bool IAssetPostPasteProcessor.Accept(Type assetType)
///
void IAssetPostPasteProcessor.PostPasteDeserialization(Asset asset)
{
- if (asset is not TAsset) throw new ArgumentException("Incompatible type of asset", nameof(asset));
- PostPasteDeserialization((TAsset)asset);
+ if (asset is not TAsset tasset) throw new ArgumentException("Incompatible type of asset", nameof(asset));
+ PostPasteDeserialization(tasset);
}
}
diff --git a/sources/editor/Stride.Core.Assets.Editor/Components/CopyPasteProcessors/AssetPropertyPasteProcessor.cs b/sources/editor/Stride.Core.Assets.Editor/Components/CopyPasteProcessors/AssetPropertyPasteProcessor.cs
index cc3765efa0..edafd19194 100644
--- a/sources/editor/Stride.Core.Assets.Editor/Components/CopyPasteProcessors/AssetPropertyPasteProcessor.cs
+++ b/sources/editor/Stride.Core.Assets.Editor/Components/CopyPasteProcessors/AssetPropertyPasteProcessor.cs
@@ -2,6 +2,7 @@
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
using System.Collections;
+using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Stride.Core.Assets.Editor.Services;
using Stride.Core.Assets.Quantum;
@@ -160,7 +161,7 @@ private void Paste(IPasteItem pasteResultItem, IGraphNode targetNode, NodeIndex
}
// Check if target collection/dictionary is null.
- if (memberNode != null && memberNode.Target == null)
+ if (memberNode is { Target: null })
{
// Check if the type has a public constructor with no arguments
if (targetNode.Type.GetConstructor(Type.EmptyTypes) != null)
@@ -511,9 +512,9 @@ private void Paste(IPasteItem pasteResultItem, IGraphNode targetNode, NodeIndex
}
}
- protected virtual bool CanUpdateMember(IMemberNode member, object newValue)
+ protected virtual bool CanUpdateMember([NotNullWhen(true)] IMemberNode? member, object? newValue)
{
- return member != null && member.MemberDescriptor.HasSet;
+ return member is { MemberDescriptor.HasSet: true };
}
protected virtual bool CanRemoveItem(IObjectNode collection, NodeIndex index)
@@ -521,27 +522,27 @@ protected virtual bool CanRemoveItem(IObjectNode collection, NodeIndex index)
return true;
}
- protected virtual bool CanReplaceItem(IObjectNode collection, NodeIndex index, object newItem)
+ protected virtual bool CanReplaceItem(IObjectNode collection, NodeIndex index, object? newItem)
{
return true;
}
- protected virtual bool CanInsertItem(IObjectNode collection, NodeIndex index, object newItem)
+ protected virtual bool CanInsertItem(IObjectNode collection, NodeIndex index, object? newItem)
{
return true;
}
- protected virtual void UpdateMember(IMemberNode member, object newValue)
+ protected virtual void UpdateMember(IMemberNode member, object? newValue)
{
member.Update(newValue);
}
- protected virtual void ReplaceItem(IObjectNode collection, NodeIndex index, object newItem)
+ protected virtual void ReplaceItem(IObjectNode collection, NodeIndex index, object? newItem)
{
collection.Update(newItem, index);
}
- protected virtual void InsertItem(IObjectNode collection, NodeIndex index, object newItem)
+ protected virtual void InsertItem(IObjectNode collection, NodeIndex index, object? newItem)
{
collection.Add(newItem, index);
}
diff --git a/sources/editor/Stride.Core.Assets.Editor/CoreAssetsEditorPlugin.cs b/sources/editor/Stride.Core.Assets.Editor/CoreAssetsEditorPlugin.cs
new file mode 100644
index 0000000000..fdff9360fb
--- /dev/null
+++ b/sources/editor/Stride.Core.Assets.Editor/CoreAssetsEditorPlugin.cs
@@ -0,0 +1,49 @@
+// 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 Stride.Core.Assets.Editor.Components.CopyPasteProcessors;
+using Stride.Core.Assets.Editor.Services;
+using Stride.Core.Assets.Presentation.ViewModels;
+using Stride.Core.Diagnostics;
+using Stride.Core.Presentation.Views;
+
+namespace Stride.Core.Assets.Editor;
+
+internal sealed class CoreAssetsEditorPlugin : AssetsEditorPlugin
+{
+ public override void InitializePlugin(ILogger logger)
+ {
+ // nothing for now
+ }
+
+ public override void InitializeSession(ISessionViewModel session)
+ {
+ if (session.ServiceProvider.TryGet() is { } copyPasteService)
+ {
+ // FIXME xplat-editor order seems to matter we could make it buggy with plugins
+ // instead, we should have a sorting/priority mechanism similar to the template providers
+ copyPasteService.RegisterProcessor(new AssetPropertyPasteProcessor());
+ copyPasteService.RegisterProcessor(new AssetItemPasteProcessor(session));
+ }
+ }
+
+ public override void RegisterAssetPreviewViewModelTypes(IDictionary assetPreviewViewModelTypes)
+ {
+ // nothing for now
+ }
+
+ public override void RegisterAssetPreviewViewTypes(IDictionary assetPreviewViewTypes)
+ {
+ // nothing for now
+ }
+
+ public override void RegisterPrimitiveTypes(ICollection primitiveTypes)
+ {
+ // nothing for now
+ }
+
+ public override void RegisterTemplateProviders(ICollection templateProviders)
+ {
+ // nothing for now
+ }
+}
diff --git a/sources/editor/Stride.Core.Assets.Editor/Module.cs b/sources/editor/Stride.Core.Assets.Editor/Module.cs
index 32cd3a4930..bd92adaca5 100644
--- a/sources/editor/Stride.Core.Assets.Editor/Module.cs
+++ b/sources/editor/Stride.Core.Assets.Editor/Module.cs
@@ -2,10 +2,8 @@
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
using System.Reflection;
-using Stride.Core.Assets.Presentation;
+using Stride.Core.Assets.Presentation.Services;
using Stride.Core.Reflection;
-using Stride.Core.Translation;
-using Stride.Core.Translation.Providers;
namespace Stride.Core.Assets.Editor;
@@ -15,5 +13,6 @@ internal class Module
public static void Initialize()
{
AssemblyRegistry.Register(typeof(Module).GetTypeInfo().Assembly, AssemblyCommonCategories.Assets);
+ AssetsPlugin.RegisterPlugin(typeof(CoreAssetsEditorPlugin));
}
}
diff --git a/sources/editor/Stride.Core.Assets.Editor/Quantum/DeletedPartsTrackingOperation.cs b/sources/editor/Stride.Core.Assets.Editor/Quantum/DeletedPartsTrackingOperation.cs
new file mode 100644
index 0000000000..e7626ec8a7
--- /dev/null
+++ b/sources/editor/Stride.Core.Assets.Editor/Quantum/DeletedPartsTrackingOperation.cs
@@ -0,0 +1,52 @@
+// 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 Stride.Core.Assets.Presentation.ViewModels;
+using Stride.Core.Assets.Quantum;
+using Stride.Core.Extensions;
+using Stride.Core.Presentation.Dirtiables;
+
+namespace Stride.Core.Assets.Editor.Quantum;
+
+///
+/// Represents the operation of updating the mapping of deleted part instances in an .
+///
+///
+///
+public sealed class DeletedPartsTrackingOperation : DirtyingOperation
+ where TAssetPartDesign : class, IAssetPartDesign
+ where TAssetPart : class, IIdentifiable
+{
+ private readonly HashSet> deletedPartsMapping;
+ private AssetCompositeHierarchyPropertyGraph propertyGraph;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ /// A mapping of the base information (base part id, instance id) of the deleted parts that have a base.
+ public DeletedPartsTrackingOperation(AssetCompositeHierarchyViewModel viewmodel, HashSet> deletedPartsMapping)
+ : base(viewmodel.SafeArgument(nameof(viewmodel)).Dirtiables)
+ {
+ this.deletedPartsMapping = deletedPartsMapping ?? throw new ArgumentNullException(nameof(deletedPartsMapping));
+ propertyGraph = viewmodel.AssetHierarchyPropertyGraph;
+
+ }
+
+ ///
+ protected override void FreezeContent()
+ {
+ propertyGraph = null!;
+ }
+
+ ///
+ protected override void Undo()
+ {
+ propertyGraph.UntrackDeletedInstanceParts(deletedPartsMapping);
+ }
+
+ ///
+ protected override void Redo()
+ {
+ propertyGraph.TrackDeletedInstanceParts(deletedPartsMapping);
+ }
+}
diff --git a/sources/editor/Stride.Core.Assets.Editor/Quantum/FixAssetReferenceOperation.cs b/sources/editor/Stride.Core.Assets.Editor/Quantum/FixAssetReferenceOperation.cs
new file mode 100644
index 0000000000..d285d95776
--- /dev/null
+++ b/sources/editor/Stride.Core.Assets.Editor/Quantum/FixAssetReferenceOperation.cs
@@ -0,0 +1,58 @@
+// 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 Stride.Core.Assets.Analysis;
+using Stride.Core.Assets.Presentation.ViewModels;
+using Stride.Core.Presentation.Dirtiables;
+
+namespace Stride.Core.Assets.Editor.Quantum;
+
+internal sealed class FixAssetReferenceOperation : DirtyingOperation
+{
+ private readonly bool fixOnUndo;
+ private readonly bool fixOnRedo;
+ private IReadOnlyCollection assets;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The list of assets to fix.
+ /// Indicates whether this action item should fix the reference during an Undo operation.
+ /// Indicates whether this action item should fix the reference during a Redo operation.
+ public FixAssetReferenceOperation(IReadOnlyCollection assets, bool fixOnUndo, bool fixOnRedo)
+ : base(assets)
+ {
+ this.assets = assets;
+ this.fixOnUndo = fixOnUndo;
+ this.fixOnRedo = fixOnRedo;
+ }
+
+ public void FixAssetReferences()
+ {
+ AssetAnalysis.FixAssetReferences(assets.Select(x => x.AssetItem));
+ }
+
+ ///
+ protected override void FreezeContent()
+ {
+ assets = null;
+ }
+
+ ///
+ protected override void Undo()
+ {
+ if (!fixOnUndo)
+ return;
+
+ FixAssetReferences();
+ }
+
+ ///
+ protected override void Redo()
+ {
+ if (!fixOnRedo)
+ return;
+
+ FixAssetReferences();
+ }
+}
diff --git a/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/AssetInitialDirectoryProvider.cs b/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/AssetInitialDirectoryProvider.cs
index 34790ace95..d7b54ea208 100644
--- a/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/AssetInitialDirectoryProvider.cs
+++ b/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/AssetInitialDirectoryProvider.cs
@@ -17,7 +17,7 @@ public AssetInitialDirectoryProvider(SessionViewModel session)
public UDirectory? GetInitialDirectory(UDirectory? currentPath)
{
- if (session != null && session.AssetCollection.SelectedAssets.Count == 1 && currentPath != null)
+ if (session is { AssetCollection.SelectedAssets.Count: 1 } && currentPath != null)
{
var asset = session.AssetCollection.SelectedAssets[0];
var projectPath = asset.Directory.Package.PackagePath;
diff --git a/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/CopyPropertyCommand.cs b/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/CopyPropertyCommand.cs
index c4df0de2f6..8a101122dc 100644
--- a/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/CopyPropertyCommand.cs
+++ b/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/CopyPropertyCommand.cs
@@ -40,7 +40,7 @@ public override async Task Execute(INodePresenter nodePresenter, object? paramet
var service = asset?.ServiceProvider.Get();
var text = service?.CopyFromAsset(asset?.PropertyGraph, asset?.Id, nodePresenter.Value, assetNodePresenter.IsObjectReference(nodePresenter.Value));
if (string.IsNullOrEmpty(text)) return;
- if (asset?.ServiceProvider.Get().SetTextAsync(text) is Task t) await t;
+ if (asset?.ServiceProvider.Get().SetTextAsync(text) is { } t) await t;
}
catch (AggregateException e) when (e.InnerException is SystemException)
{
diff --git a/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/MoveItemCommand.cs b/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/MoveItemCommand.cs
index c42c8eb0f0..754f2a1fcb 100644
--- a/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/MoveItemCommand.cs
+++ b/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/MoveItemCommand.cs
@@ -38,7 +38,7 @@ public override bool CanAttach(INodePresenter nodePresenter)
// ... and supports remove and insert
var collectionDescriptor = collectionNode.Descriptor as CollectionDescriptor;
- return collectionDescriptor?.HasRemoveAt == true && collectionDescriptor.HasInsert;
+ return collectionDescriptor is { HasRemoveAt: true, HasInsert: true };
}
///
diff --git a/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/PastePropertyCommand.cs b/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/PastePropertyCommand.cs
index 0137681247..f4e1a78876 100644
--- a/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/PastePropertyCommand.cs
+++ b/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/PastePropertyCommand.cs
@@ -6,7 +6,7 @@
namespace Stride.Core.Assets.Editor.Quantum.NodePresenters.Commands;
-public class PastePropertyCommand : PastePropertyCommandBase
+public sealed class PastePropertyCommand : PastePropertyCommandBase
{
///
/// The name of this command.
diff --git a/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/PastePropertyCommandBase.cs b/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/PastePropertyCommandBase.cs
index 173f15a933..03fce7e571 100644
--- a/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/PastePropertyCommandBase.cs
+++ b/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/PastePropertyCommandBase.cs
@@ -27,19 +27,19 @@ public override bool CanAttach(INodePresenter nodePresenter)
protected virtual bool CanPaste(IReadOnlyCollection nodePresenters)
{
- foreach (var nodePresenter in nodePresenters)
+ foreach (var nodePresenter in nodePresenters.OfType())
{
- var assetNodePresenter = nodePresenter as IAssetNodePresenter;
- var copyPasteService = assetNodePresenter?.Asset?.ServiceProvider.TryGet();
- if (copyPasteService == null)
+ var copyPasteService = nodePresenter.Asset?.ServiceProvider.TryGet();
+ if (copyPasteService is null)
return false;
- var asset = assetNodePresenter!.Asset!.Asset;
- var clipboard = assetNodePresenter?.Asset?.ServiceProvider.TryGet();
- if (clipboard == null)
+ var clipboard = nodePresenter.Asset?.ServiceProvider.TryGet();
+ if (clipboard is null)
return false;
+ var asset = nodePresenter.Asset!.Asset;
+
if (!copyPasteService.CanPaste(clipboard.GetTextAsync().Result, asset.GetType(), (nodePresenter as ItemNodePresenter)?.OwnerCollection.Type ?? nodePresenter.Type))
return false;
@@ -48,13 +48,13 @@ protected virtual bool CanPaste(IReadOnlyCollection nodePresente
return false;
// Cannot paste into read-only property (non-collection)
- if (!nodePresenter.IsEnumerable && nodePresenter.IsReadOnly)
+ if (nodePresenter is { IsEnumerable: false, IsReadOnly: true })
return false;
}
return true;
}
- protected async Task DoPasteAsync(INodePresenter nodePresenter, bool replace)
+ protected static async Task DoPasteAsync(INodePresenter nodePresenter, bool replace)
{
var asset = ((IAssetNodePresenter)nodePresenter).Asset;
if (asset is null)
@@ -73,11 +73,11 @@ protected async Task DoPasteAsync(INodePresenter nodePresenter, bool replace)
var nodeAccessor = nodePresenter.GetNodeAccessor();
var targetNode = nodeAccessor.Node;
// If the node presenter is a virtual node without node, we cannot paste.
- if (targetNode == null)
+ if (targetNode is null)
return;
var actionService = asset.UndoRedoService;
- using var transaction = actionService.CreateTransaction();
+ using var transaction = actionService?.CreateTransaction();
// FIXME: for now we only handle one result item
var item = result.Items[0];
@@ -86,16 +86,16 @@ protected async Task DoPasteAsync(INodePresenter nodePresenter, bool replace)
var propertyContainer = new PropertyContainer { { AssetPropertyPasteProcessor.IsReplaceKey, replace } };
await (item.Processor?.Paste(item, asset.PropertyGraph, ref nodeAccessor, ref propertyContainer) ?? Task.CompletedTask);
- actionService.SetName(transaction, replace ? "Replace property" : "Paste property");
+ actionService?.SetName(transaction!, replace ? "Replace property" : "Paste property");
}
private static bool IsInReadOnlyCollection(INodePresenter? nodePresenter)
{
- if (nodePresenter == null || !nodePresenter.IsEnumerable)
+ if (nodePresenter is not { IsEnumerable: true })
return false;
var memberCollection = (nodePresenter as MemberNodePresenter)?.MemberAttributes.OfType().FirstOrDefault()
?? nodePresenter.Descriptor.Attributes.OfType().FirstOrDefault();
- return memberCollection != null && memberCollection.ReadOnly;
+ return memberCollection is { ReadOnly: true };
}
}
diff --git a/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/ReplacePropertyCommand.cs b/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/ReplacePropertyCommand.cs
index 4d380b4b70..a420c59493 100644
--- a/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/ReplacePropertyCommand.cs
+++ b/sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/ReplacePropertyCommand.cs
@@ -6,7 +6,7 @@
namespace Stride.Core.Assets.Editor.Quantum.NodePresenters.Commands;
-public class ReplacePropertyCommand : PastePropertyCommandBase
+public sealed class ReplacePropertyCommand : PastePropertyCommandBase
{
///
/// The name of this command.
diff --git a/sources/editor/Stride.Core.Assets.Editor/AssetsEditorPlugin.cs b/sources/editor/Stride.Core.Assets.Editor/Services/AssetsEditorPlugin.cs
similarity index 95%
rename from sources/editor/Stride.Core.Assets.Editor/AssetsEditorPlugin.cs
rename to sources/editor/Stride.Core.Assets.Editor/Services/AssetsEditorPlugin.cs
index ef88bdbf8c..12ffa3d79f 100644
--- a/sources/editor/Stride.Core.Assets.Editor/AssetsEditorPlugin.cs
+++ b/sources/editor/Stride.Core.Assets.Editor/Services/AssetsEditorPlugin.cs
@@ -5,10 +5,10 @@
using Stride.Core.Assets.Editor.Annotations;
using Stride.Core.Assets.Editor.Editors;
using Stride.Core.Assets.Editor.ViewModels;
-using Stride.Core.Assets.Presentation;
+using Stride.Core.Assets.Presentation.Services;
using Stride.Core.Presentation.Views;
-namespace Stride.Core.Assets.Editor;
+namespace Stride.Core.Assets.Editor.Services;
public abstract class AssetsEditorPlugin : AssetsPlugin
{
diff --git a/sources/editor/Stride.Core.Assets.Editor/Services/CopyPasteService.cs b/sources/editor/Stride.Core.Assets.Editor/Services/CopyPasteService.cs
index 9ede78d67e..3f958de14b 100644
--- a/sources/editor/Stride.Core.Assets.Editor/Services/CopyPasteService.cs
+++ b/sources/editor/Stride.Core.Assets.Editor/Services/CopyPasteService.cs
@@ -10,7 +10,7 @@
namespace Stride.Core.Assets.Editor.Services;
-internal class CopyPasteService : ICopyPasteService
+internal sealed class CopyPasteService : ICopyPasteService
{
private readonly List copyProcessors = [];
private readonly List pasteProcessors = [];
@@ -228,6 +228,7 @@ public void RegisterProcessor(IPasteProcessor processor)
{
pasteProcessors.Add(processor);
}
+
///
public void RegisterProcessor(IAssetPostPasteProcessor processor)
{
diff --git a/sources/editor/Stride.Core.Assets.Editor/Services/IAssetsPluginService.cs b/sources/editor/Stride.Core.Assets.Editor/Services/IAssetsPluginService.cs
index 17f21ef0f9..052dd0d21c 100644
--- a/sources/editor/Stride.Core.Assets.Editor/Services/IAssetsPluginService.cs
+++ b/sources/editor/Stride.Core.Assets.Editor/Services/IAssetsPluginService.cs
@@ -1,7 +1,7 @@
// 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 Stride.Core.Assets.Presentation;
+using Stride.Core.Assets.Presentation.Services;
using Stride.Core.Diagnostics;
namespace Stride.Core.Assets.Editor.Services;
diff --git a/sources/editor/Stride.Core.Assets.Editor/ViewModels/AssetCollectionViewModel.CopyPaste.cs b/sources/editor/Stride.Core.Assets.Editor/ViewModels/AssetCollectionViewModel.CopyPaste.cs
new file mode 100644
index 0000000000..378fa04f02
--- /dev/null
+++ b/sources/editor/Stride.Core.Assets.Editor/ViewModels/AssetCollectionViewModel.CopyPaste.cs
@@ -0,0 +1,424 @@
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
+// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
+
+using Stride.Core.Assets.Analysis;
+using Stride.Core.Assets.Editor.Quantum;
+using Stride.Core.Assets.Editor.Services;
+using Stride.Core.Assets.Presentation.ViewModels;
+using Stride.Core.Extensions;
+using Stride.Core.IO;
+using Stride.Core.Presentation.Collections;
+using Stride.Core.Presentation.Commands;
+using Stride.Core.Presentation.Services;
+using Stride.Core.Translation;
+
+namespace Stride.Core.Assets.Editor.ViewModels;
+
+partial class AssetCollectionViewModel
+{
+ private IClipboardService? ClipboardService => ServiceProvider.TryGet();
+ private ICopyPasteService? CopyPasteService => ServiceProvider.TryGet();
+ private IDialogService DialogService => ServiceProvider.Get();
+
+ public ICommandBase CopyAssetsRecursivelyCommand { get; }
+
+ public ICommandBase CopyAssetUrlCommand { get; }
+
+ public ICommandBase CopyContentCommand { get; }
+
+ public ICommandBase CopyLocationsCommand { get; }
+
+ public ICommandBase CutContentCommand { get; }
+
+ public ICommandBase CutLocationsCommand { get; }
+
+ public ICommandBase PasteCommand { get; }
+
+ private bool CanCopy()
+ {
+ return CopyPasteService is not null && ClipboardService is not null;
+ }
+
+ private bool CanPaste()
+ {
+ if (CopyPasteService is not { } copyPaste || ClipboardService is not { } clipboard)
+ return false;
+
+ var text = clipboard.GetTextAsync().Result;
+ return copyPaste.CanPaste(text, typeof(List), typeof(List), typeof(List));
+
+ }
+
+ private async Task CopyAssetUrl()
+ {
+ if (SingleSelectedAsset is null)
+ return;
+
+ try
+ {
+ await ClipboardService!.SetTextAsync(SingleSelectedAsset.Url);
+ }
+ catch (SystemException e)
+ {
+ // We don't provide feedback when copying fails.
+ e.Ignore();
+ }
+ }
+
+ private async Task CopySelectedAssetsRecursively()
+ {
+ var assetsToCopy = new ObservableSet();
+ foreach (var asset in SelectedAssets)
+ {
+ assetsToCopy.Add(asset);
+ assetsToCopy.AddRange(asset.Dependencies.RecursiveReferencedAssets.Where(a => a.IsEditable));
+ }
+
+ await CopySelection(null, assetsToCopy);
+ UpdateCommands();
+ }
+
+ private async Task CopySelectedContent()
+ {
+ var directories = SelectedContent.OfType().ToList();
+ await CopySelection(directories, SelectedAssets);
+ UpdateCommands();
+ }
+
+ private async Task CopySelectedLocations()
+ {
+ var directories = GetSelectedDirectories(false);
+ await CopySelection(directories, null);
+ UpdateCommands();
+ }
+
+ private async Task CopySelection(IReadOnlyCollection? directories, IEnumerable? assetsToCopy)
+ {
+ var assetsToWrite = await GetCopyCollection(directories, assetsToCopy);
+ if (assetsToWrite?.Count > 0)
+ {
+ await WriteToClipboardAsync(assetsToWrite);
+ }
+ }
+
+ private async Task CutSelectedContent()
+ {
+ var directories = SelectedContent.OfType().ToList();
+ await CutSelection(directories, SelectedAssets);
+ UpdateCommands();
+ }
+
+ private async Task CutSelectedLocations()
+ {
+ var directories = GetSelectedDirectories(false);
+ await CutSelection(directories, null);
+ UpdateCommands();
+ }
+
+ private async Task CutSelection(IReadOnlyCollection? directories, IEnumerable? assetsToCut)
+ {
+ // Ensure all directories can be cut
+ if (directories?.Any(d => !d.IsEditable) == true)
+ {
+ await DialogService.MessageBoxAsync(Tr._p("Message", "Read-only folders can't be cut."), MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+ var assetsToWrite = await GetCopyCollection(directories, assetsToCut);
+ if (assetsToWrite is null || assetsToWrite.Count == 0)
+ return;
+
+ //// Flatten to a list
+ //var assetList = assetsToWrite.SelectMany(x => x).ToList();
+ //foreach (var asset in assetList)
+ //{
+ // if (!asset.CanDelete(out string error))
+ // {
+ // error = string.Format(Tr._p("Message", "The asset {0} can't be deleted. {1}{2}"), asset.Url, Environment.NewLine, error);
+ // await DialogService.MessageBoxAsync(error, MessageBoxButton.OK, MessageBoxImage.Error);
+ // return;
+ // }
+ //}
+
+ // Copy
+ if (!await WriteToClipboardAsync(assetsToWrite))
+ {
+ return;
+ }
+
+ using var transaction = Session.ActionService?.CreateTransaction();
+
+ // Clear the selection at first to reduce view updates in the following actions
+ ClearSelection();
+ //// Add an action item that will fix back the references in the referencers of the assets being cut, in case the
+ //var assetsToFix = PackageViewModel.GetReferencers(dependencyManager, Session, assetList.Select(x => x.AssetItem));
+ //var fixReferencesOperation = new FixAssetReferenceOperation(assetsToFix, true, false);
+ //Session.ActionService.PushOperation(fixReferencesOperation);
+ //// Delete the assets
+ //DeleteAssets(assetList);
+ //if (directories is not null)
+ //{
+ // // Delete the directories
+ // foreach (var directory in directories)
+ // {
+ // // Last-chance check (note that we already checked that the directories are not read-only)
+ // if (!directory.CanDelete(out string error))
+ // {
+ // error = string.Format(Tr._p("Message", "{0} can't be deleted. {1}{2}"), directory.Name, Environment.NewLine, error);
+ // await DialogService.MessageBoxAsync(error, MessageBoxButton.OK, MessageBoxImage.Error);
+ // return;
+ // }
+ // directory.Delete();
+ // }
+ //}
+
+ Session.ActionService?.SetName(transaction!, "Cut selection");
+ }
+
+ ///
+ /// Gets the whole collection of assets to be copied.
+ ///
+ /// The collection of separate directories of assets.
+ /// The collection of assets in the current directory.
+ /// Directories cannot be in the same hierarchy of one another.
+ /// The collection of assets to be copied, or null if the selection cannot be copied.
+ private async Task>?> GetCopyCollection(IReadOnlyCollection? directories, IEnumerable? assetsToCopy)
+ {
+ var collection = new List>();
+ // First level assets will be copied as is
+ if (assetsToCopy is not null)
+ {
+ collection.AddRange(assetsToCopy.GroupBy(_ => string.Empty));
+ }
+
+ if (directories is not null)
+ {
+ // Check directory structure
+ foreach (var directory in directories)
+ {
+ var parent = directory.Parent;
+ while (parent is not MountPointViewModel)
+ {
+ if (directories.Contains(parent))
+ {
+ await DialogService.MessageBoxAsync(Tr._p("Message", "Unable to cut or copy a selection that contains a folder and one of its subfolders."), MessageBoxButton.OK, MessageBoxImage.Information);
+ return null;
+ }
+ parent = parent.Parent;
+ }
+ }
+ // Get all assets from directories
+ foreach (var directory in directories)
+ {
+ var hierarchy = directory.GetDirectoryHierarchy();
+ foreach (var folder in hierarchy)
+ {
+ EnsureDirectoryHierarchy(folder.Assets, folder);
+ // Add assets grouped by relative path
+ collection.AddRange(folder.Assets.GroupBy(_ => folder.Path.Remove(0, directory.Parent.Path.Length)));
+ }
+ }
+ }
+
+ return collection;
+
+ static void EnsureDirectoryHierarchy(IEnumerable assets, DirectoryBaseViewModel directory)
+ {
+ if (assets.Any(asset => asset.AssetItem.Location.HasDirectory && !asset.Url.StartsWith(directory.Parent.Path, StringComparison.Ordinal)))
+ {
+ throw new InvalidOperationException("One of the asset does not match the directory hierarchy.");
+ }
+ }
+ }
+
+ private async Task Paste()
+ {
+ var directories = GetSelectedDirectories(false);
+ if (directories.Count != 1)
+ {
+ await DialogService.MessageBoxAsync(Tr._p("Message", "Select a valid asset folder to paste the selection to."), MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+
+ // If the selection is already a directory, paste into it
+ var directory = SingleSelectedContent as DirectoryBaseViewModel ?? directories.First();
+ var package = directory.Package;
+ if (!package.IsEditable)
+ {
+ await DialogService.MessageBoxAsync(Tr._p("Message", "This package or directory can't be modified."), MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+
+ var text = await ClipboardService!.GetTextAsync();
+ if (string.IsNullOrWhiteSpace(text))
+ return;
+
+ var pastedAssets = new List();
+ pastedAssets = CopyPasteService!.DeserializeCopiedData(text, pastedAssets, typeof(List)).Items.FirstOrDefault()?.Data as List;
+ if (pastedAssets is null)
+ return;
+
+ var updatedAssets = new List();
+ var root = directory.Root;
+ var project = (root as ProjectCodeViewModel)?.Project;
+ foreach (var assetItem in pastedAssets)
+ {
+ // Perform allowed asset types validation
+ if (!root.AcceptAssetType(assetItem.Asset.GetType()))
+ {
+ // Skip invalid assets
+ continue;
+ }
+
+ var location = UPath.Combine(directory.Path, assetItem.Location);
+
+ // Check if we are pasting to package or a project (with a source code)
+ if (project is not null)
+ {
+ // Link source project
+ assetItem.SourceFolder = project.Package.RootDirectory;
+ }
+
+ // Resolve folders to paste collisions with those existing in a directory
+ var assetLocationDir = assetItem.Location.FullPath;
+ {
+ // Split path into two parts
+ int firstSeparator = assetLocationDir.IndexOf(DirectoryBaseViewModel.Separator, StringComparison.Ordinal);
+ if (firstSeparator > 0)
+ {
+ // Left: (folder)
+ // /
+ // Right: (..folders..) / (file.ext)
+ UDirectory leftPart = assetLocationDir.Remove(firstSeparator);
+ UFile rightPart = assetLocationDir[(firstSeparator + 1)..];
+
+ // Find valid left part location (if already in use)
+ leftPart = NamingHelper.ComputeNewName(leftPart, e => directory.GetDirectory(e) is not null, "{0} ({1})");
+
+ // Fix location: (paste directory) / left/ right
+ location = UPath.Combine(Path.Combine(directory.Path, leftPart), rightPart);
+ }
+ }
+
+ var updatedAsset = assetItem.Clone(true, location, assetItem.Asset);
+ updatedAssets.Add(updatedAsset);
+ }
+
+ if (updatedAssets.Count == 0)
+ return;
+
+ var viewModels = PasteAssetsIntoPackage(package, updatedAssets, project);
+
+ var referencerViewModels = AssetViewModel.ComputeRecursiveReferencerAssets(viewModels);
+ viewModels.AddRange(referencerViewModels);
+ await Session.NotifyAssetPropertiesChangedAsync(viewModels);
+ UpdateCommands();
+ }
+
+ public static List PasteAssetsIntoPackage(PackageViewModel package, List assets, ProjectViewModel? project)
+ {
+ var viewModels = new List();
+
+ // Don't touch the action stack in this case.
+ if (assets.Count == 0)
+ return viewModels;
+
+ var fixedAssets = new List();
+
+ using var transaction = package.UndoRedoService.CreateTransaction();
+ // Clean collision by renaming pasted asset if an asset with the same name already exists in that location.
+ AssetCollision.Clean(null, assets, fixedAssets, AssetResolver.FromPackage(package.Package), false, false);
+
+ // Temporarily add the new asset to the package
+ fixedAssets.ForEach(x => package.Package.Assets.Add(x));
+
+ // Find which assets are referencing the pasted assets in order to fix the reference link.
+ var assetsToFix = GetReferencers(package.Session.DependencyManager, package.Session, fixedAssets);
+
+ // Remove temporarily added assets - they will be properly re-added with the correct action stack entry when creating the view model
+ fixedAssets.ForEach(x => package.Package.Assets.Remove(x));
+
+ // Create directories and view models, actually add assets to package.
+ foreach (var asset in fixedAssets)
+ {
+ var location = asset.Location.GetFullDirectory();
+ var assetDirectory = project == null ?
+ package.GetOrCreateAssetDirectory(location) :
+ project.GetOrCreateProjectDirectory(location);
+ var assetViewModel = package.CreateAsset(asset, assetDirectory, true);
+ viewModels.Add(assetViewModel);
+ }
+
+ // Fix references in the assets that references what we pasted.
+ // We wrap this operation in an action item so the action stack can properly re-execute it.
+ var fixReferencesAction = new FixAssetReferenceOperation(assetsToFix, false, true);
+ fixReferencesAction.FixAssetReferences();
+ package.UndoRedoService.PushOperation(fixReferencesAction);
+
+ package.UndoRedoService.SetName(transaction, "Paste assets");
+ return viewModels;
+ }
+
+ private static List GetReferencers(IAssetDependencyManager dependencyManager, ISessionViewModel session, IEnumerable assets)
+ {
+ var result = new List();
+
+ // Find which assets are referencing the pasted assets in order to fix the reference link.
+ foreach (var asset in assets)
+ {
+ if (dependencyManager.ComputeDependencies(asset.Id, AssetDependencySearchOptions.In) is not { } referencers)
+ continue;
+
+ foreach (var referencerLink in referencers.LinksIn)
+ {
+ if (session.GetAssetById(referencerLink.Item.Id) is not { } assetViewModel)
+ continue;
+
+ if (!result.Contains(assetViewModel))
+ result.Add(assetViewModel);
+ }
+ }
+ return result;
+ }
+
+ private void UpdateCommands()
+ {
+ var atLeastOneAsset = SelectedAssets.Count > 0;
+ var atLeastOneContent = SelectedContent.Count > 0;
+
+ CopyAssetsRecursivelyCommand.IsEnabled = atLeastOneAsset;
+ CopyAssetUrlCommand.IsEnabled = SingleSelectedAsset is not null;
+ CopyContentCommand.IsEnabled = atLeastOneContent;
+ // Can copy from asset mount point
+ CopyLocationsCommand.IsEnabled = SelectedLocations.All(x => x is DirectoryBaseViewModel or PackageViewModel);
+ CutContentCommand.IsEnabled = atLeastOneContent;
+ // TODO: Allow to cut asset mount point - but do not remove the mount point
+ CutLocationsCommand.IsEnabled = SelectedLocations.All(x => x is DirectoryViewModel or PackageViewModel);
+ PasteCommand.IsEnabled = SelectedLocations.Count == 1 && SelectedLocations.All(x => x is DirectoryBaseViewModel or PackageViewModel);
+ }
+
+ ///
+ /// Actually writes the assets to the clipboard.
+ ///
+ ///
+ ///
+ private async Task WriteToClipboardAsync(IEnumerable> assetsToWrite)
+ {
+ var assetCollection = new List();
+ assetCollection.AddRange(assetsToWrite.SelectMany(
+ grp => grp.Select(a => new AssetItem(UPath.Combine(grp.Key, a.AssetItem.Location.GetFileNameWithoutExtension()!), a.AssetItem.Asset))));
+ try
+ {
+ var text = CopyPasteService!.CopyMultipleAssets(assetCollection);
+ if (string.IsNullOrEmpty(text))
+ return false;
+
+ await ClipboardService!.SetTextAsync(text);
+ return true;
+ }
+ catch (SystemException)
+ {
+ // We don't provide feedback when copying fails.
+ return false;
+ }
+ }
+}
diff --git a/sources/editor/Stride.Core.Assets.Editor/ViewModels/AssetCollectionViewModel.cs b/sources/editor/Stride.Core.Assets.Editor/ViewModels/AssetCollectionViewModel.cs
index 1fafb48c32..af27b21f55 100644
--- a/sources/editor/Stride.Core.Assets.Editor/ViewModels/AssetCollectionViewModel.cs
+++ b/sources/editor/Stride.Core.Assets.Editor/ViewModels/AssetCollectionViewModel.cs
@@ -12,7 +12,7 @@
namespace Stride.Core.Assets.Editor.ViewModels;
-public sealed class AssetCollectionViewModel : DispatcherViewModel
+public sealed partial class AssetCollectionViewModel : DispatcherViewModel
{
private readonly ObservableSet assets = [];
private readonly HashSet monitoredDirectories = [];
@@ -28,6 +28,14 @@ public AssetCollectionViewModel(SessionViewModel session)
// Initialize the view model that will manage the properties of the assets selected on the main asset view
AssetViewProperties = new SessionObjectPropertiesViewModel(session);
+ CopyAssetsRecursivelyCommand = new AnonymousTaskCommand(ServiceProvider, CopySelectedAssetsRecursively, CanCopy);
+ CopyAssetUrlCommand = new AnonymousTaskCommand(ServiceProvider, CopyAssetUrl, CanCopy);
+ CopyContentCommand = new AnonymousTaskCommand(ServiceProvider, CopySelectedContent, CanCopy);
+ CopyLocationsCommand = new AnonymousTaskCommand(ServiceProvider, CopySelectedLocations, CanCopy);
+ CutContentCommand = new AnonymousTaskCommand(ServiceProvider, CutSelectedContent, CanCopy);
+ CutLocationsCommand = new AnonymousTaskCommand(ServiceProvider, CutSelectedLocations, CanCopy);
+ PasteCommand = new AnonymousTaskCommand(ServiceProvider, Paste, CanPaste);
+
SelectAssetCommand = new AnonymousCommand(ServiceProvider, x => SelectAssets(x.Yield()!));
selectedContent.CollectionChanged += SelectedContentCollectionChanged;
@@ -190,6 +198,8 @@ private async void SelectedContentCollectionChanged(object? sender, NotifyCollec
}
}
+ UpdateCommands();
+
AssetViewProperties.UpdateTypeAndName(SelectedAssets, x => x.TypeDisplayName, x => x.Url, "assets");
await AssetViewProperties.GenerateSelectionPropertiesAsync(SelectedAssets);
}
@@ -197,6 +207,7 @@ private async void SelectedContentCollectionChanged(object? sender, NotifyCollec
private void SelectedLocationCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
UpdateLocations();
+ UpdateCommands();
}
private void SubDirectoriesCollectionInDirectoryChanged(object? sender, NotifyCollectionChangedEventArgs e)
@@ -204,6 +215,11 @@ private void SubDirectoriesCollectionInDirectoryChanged(object? sender, NotifyCo
UpdateLocations();
}
+ public void ClearSelection()
+ {
+ selectedContent.Clear();
+ }
+
private void UpdateAssetsCollection(ICollection newAssets, bool clearMonitoredDirectory)
{
if (clearMonitoredDirectory)
diff --git a/sources/editor/Stride.Core.Assets.Editor/ViewModels/AssetCompositeEditorViewModel.cs b/sources/editor/Stride.Core.Assets.Editor/ViewModels/AssetCompositeEditorViewModel.cs
index 0be4a66c00..d7040bb2aa 100644
--- a/sources/editor/Stride.Core.Assets.Editor/ViewModels/AssetCompositeEditorViewModel.cs
+++ b/sources/editor/Stride.Core.Assets.Editor/ViewModels/AssetCompositeEditorViewModel.cs
@@ -24,6 +24,14 @@ protected AssetCompositeEditorViewModel(TAssetViewModel asset)
public ObservableSet
/// The action that caused the event.
///
- /// Default implementation populates with the same elements.
+ /// Default implementation populates with the same elements.
///
protected virtual void SelectedItemsCollectionChanged(NotifyCollectionChangedAction action)
{
@@ -103,6 +137,24 @@ protected virtual void SelectedItemsCollectionChanged(NotifyCollectionChangedAct
SelectedContent.AddRange(SelectedItems);
}
+ ///
+ /// Gathers all base assets used in the composition of the given asset parts, recursively.
+ ///
+ ///
+ private static void GatherAllBasePartAssetsRecursively(IEnumerable assetParts, IAssetFinder assetFinder, ISet baseAssets)
+ {
+ foreach (var part in assetParts)
+ {
+ if (part.Base == null || !baseAssets.Add(part.Base.BasePartAsset.Id))
+ continue;
+
+ if (assetFinder.FindAsset(part.Base.BasePartAsset.Id)?.Asset is AssetCompositeHierarchy baseAsset)
+ {
+ GatherAllBasePartAssetsRecursively(baseAsset.Hierarchy.Parts.Values, assetFinder, baseAssets);
+ }
+ }
+ }
+
private void SelectedContentCollectionChanged(object? sender, NotifyCollectionChangedEventArgs args)
{
if (updateSelectionGuard)
diff --git a/sources/editor/Stride.Core.Assets.Editor/ViewModels/SessionViewModel.static.cs b/sources/editor/Stride.Core.Assets.Editor/ViewModels/SessionViewModel.static.cs
index 04c6ae4131..cc19f04658 100644
--- a/sources/editor/Stride.Core.Assets.Editor/ViewModels/SessionViewModel.static.cs
+++ b/sources/editor/Stride.Core.Assets.Editor/ViewModels/SessionViewModel.static.cs
@@ -15,7 +15,7 @@ partial class SessionViewModel
{
private static SessionViewModel? instance;
private static readonly SemaphoreSlim semaphore = new(1, 1);
-
+
///
/// The current instance of .
///
@@ -92,7 +92,17 @@ public static SessionViewModel Instance
}, token);
- sessionViewModel?.AutoSelectCurrentProject();
+ if (sessionViewModel == null || cancellationSource.IsCancellationRequested)
+ {
+ sessionViewModel?.Destroy();
+ sessionResult.OperationCancelled = cancellationSource.IsCancellationRequested;
+ return null;
+ }
+
+ // Register the node container to the copy/paste service.
+ copyPasteService.PropertyGraphContainer = sessionViewModel.GraphContainer;
+
+ sessionViewModel.AutoSelectCurrentProject();
// Now resize the undo stack to the correct size.
actionService.Resize(200);
@@ -101,24 +111,21 @@ public static SessionViewModel Instance
sessionViewModel.ActionHistory?.Initialize();
// Copy the result of the asset loading to the log panel.
- sessionViewModel?.AssetLog.AddLogger(LogKey.Get("Session"), sessionResult);
+ sessionViewModel.AssetLog.AddLogger(LogKey.Get("Session"), sessionResult);
// Notify that the task is finished
sessionResult.OperationCancelled = token.IsCancellationRequested;
await workProgress.NotifyWorkFinished(token.IsCancellationRequested, sessionResult.HasErrors);
// Update the singleton instance
- if (sessionViewModel is not null)
+ await semaphore.WaitAsync(token);
+ try
{
- await semaphore.WaitAsync(token);
- try
- {
- instance = sessionViewModel;
- }
- finally
- {
- semaphore.Release();
- }
+ instance = sessionViewModel;
+ }
+ finally
+ {
+ semaphore.Release();
}
return sessionViewModel;
diff --git a/sources/editor/Stride.Core.Assets.Presentation/AssetsPlugin.cs b/sources/editor/Stride.Core.Assets.Presentation/Services/AssetsPlugin.cs
similarity index 97%
rename from sources/editor/Stride.Core.Assets.Presentation/AssetsPlugin.cs
rename to sources/editor/Stride.Core.Assets.Presentation/Services/AssetsPlugin.cs
index 9b8ec6f573..77069c83a4 100644
--- a/sources/editor/Stride.Core.Assets.Presentation/AssetsPlugin.cs
+++ b/sources/editor/Stride.Core.Assets.Presentation/Services/AssetsPlugin.cs
@@ -6,7 +6,7 @@
using Stride.Core.Assets.Presentation.ViewModels;
using Stride.Core.Diagnostics;
-namespace Stride.Core.Assets.Presentation;
+namespace Stride.Core.Assets.Presentation.Services;
public abstract class AssetsPlugin
{
diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetCompositeItemViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetCompositeItemViewModel.cs
index d99452c60b..d128bfd33c 100644
--- a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetCompositeItemViewModel.cs
+++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetCompositeItemViewModel.cs
@@ -1,6 +1,7 @@
// 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.Diagnostics;
using Stride.Core.Assets.Presentation.Components.Properties;
using Stride.Core.Presentation.Collections;
using Stride.Core.Presentation.ViewModels;
@@ -8,8 +9,13 @@
namespace Stride.Core.Assets.Presentation.ViewModels;
+///
+/// A view model representing an item, real or virtual, of a hierarchical composite asset.
+///
public abstract class AssetCompositeItemViewModel : DispatcherViewModel
{
+ private AssetCompositeItemViewModel? parent;
+
protected AssetCompositeItemViewModel(AssetViewModel asset)
: base(asset.ServiceProvider)
{
@@ -19,13 +25,29 @@ protected AssetCompositeItemViewModel(AssetViewModel asset)
///
/// The related asset.
///
- public AssetViewModel Asset { get; }
+ public virtual AssetViewModel Asset { get; }
///
/// Gets or sets the name of this item.
///
public abstract string? Name { get; set; }
+ ///
+ /// The parent of this item.
+ ///
+ public AssetCompositeItemViewModel? Parent
+ {
+ get { return parent; }
+ protected set
+ {
+ if (value == parent)
+ {
+ Debug.WriteLine("Ineffective change to the Parent.");
+ }
+ SetValue(ref parent, value);
+ }
+ }
+
///
/// Enumerates all child items of this .
///
@@ -42,42 +64,42 @@ protected AssetCompositeItemViewModel(AssetViewModel asset)
protected IObjectNode GetNode() => Asset.Session.AssetNodeContainer.GetNode(Asset.Asset);
}
-public abstract class AssetCompositeItemViewModel : AssetCompositeItemViewModel
+///
+/// A view model representing an item, real or virtual, of a hierarchical composite asset.
+///
+/// The type of the related asset.
+/// The type of the parent item.
+/// The type of the child items.
+public abstract class AssetCompositeItemViewModel : AssetCompositeItemViewModel
where TAssetViewModel : AssetViewModel
- where TItemViewModel : AssetCompositeItemViewModel
+ where TParentItemViewModel : AssetCompositeItemViewModel
+ where TChildItemViewModel : AssetCompositeItemViewModel
{
- private readonly ObservableList children = new();
- private TItemViewModel? parent;
+ private readonly ObservableList children = [];
- protected AssetCompositeItemViewModel(AssetViewModel asset)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The related asset.
+ protected AssetCompositeItemViewModel(TAssetViewModel asset)
: base(asset)
{
}
- public IReadOnlyObservableList Children => children;
-
- public TItemViewModel? Parent
- {
- get => parent;
- protected set => SetValue(ref parent, value);
- }
-
///
- public override void Destroy()
- {
- base.Destroy();
- children.Clear();
- }
+ public override TAssetViewModel Asset => (TAssetViewModel)base.Asset;
+
+ public IReadOnlyObservableList Children => children;
///
/// Adds an to the collection.
///
/// The item to add to the collection.
/// is null.
- protected void AddItem(TItemViewModel item)
+ protected void AddItem(TChildItemViewModel item)
{
children.Add(item);
- item.Parent = (TItemViewModel)this;
+ item.Parent = (TParentItemViewModel)this;
}
///
@@ -85,15 +107,22 @@ protected void AddItem(TItemViewModel item)
///
/// An enumeration of items to add to the collection.
/// is null.
- protected void AddItems(IEnumerable items)
+ protected void AddItems(IEnumerable items)
{
foreach (var item in items)
{
children.Add(item);
- item.Parent = (TItemViewModel)this;
+ item.Parent = (TParentItemViewModel)this;
}
}
+ ///
+ public override void Destroy()
+ {
+ base.Destroy();
+ children.Clear();
+ }
+
///
/// Enumerates all child items of this .
///
@@ -107,10 +136,10 @@ protected void AddItems(IEnumerable items)
/// The item to insert into the collection.
/// is null.
/// is not a valid index in the collection.
- protected void InsertItem(int index, TItemViewModel item)
+ protected void InsertItem(int index, TChildItemViewModel item)
{
children.Insert(index, item);
- item.Parent = (TItemViewModel)this;
+ item.Parent = (TParentItemViewModel)this;
}
///
@@ -118,7 +147,7 @@ protected void InsertItem(int index, TItemViewModel item)
///
/// The item to remove from the collection.
/// true if item was successfully removed from the collection; otherwise, false.
- protected bool RemoveItem(TItemViewModel item)
+ protected bool RemoveItem(TChildItemViewModel item)
{
if (!children.Remove(item))
return false;
@@ -138,3 +167,18 @@ protected void RemoveItemAt(int index)
item.Parent = null;
}
}
+
+///
+/// A view model representing an item, real or virtual, of a hierarchical composite asset.
+///
+/// The type of the related asset.
+/// The type of the parent and child items.
+public abstract class AssetCompositeItemViewModel : AssetCompositeItemViewModel
+ where TAssetViewModel : AssetViewModel
+ where TItemViewModel : AssetCompositeItemViewModel
+{
+ protected AssetCompositeItemViewModel(TAssetViewModel asset)
+ : base(asset)
+ {
+ }
+}
diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetMountPointViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetMountPointViewModel.cs
index 1aa9e8d79c..5b8e326f53 100644
--- a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetMountPointViewModel.cs
+++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetMountPointViewModel.cs
@@ -19,4 +19,10 @@ public override string Name
get => "Assets";
set => throw new InvalidOperationException($"Cannot change the name of a {nameof(AssetMountPointViewModel)}");
}
+
+ ///
+ public override bool AcceptAssetType(Type assetType)
+ {
+ return !typeof(IProjectAsset).IsAssignableFrom(assetType);
+ }
}
diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetViewModel.cs
index bfee60aec3..8eca032b77 100644
--- a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetViewModel.cs
+++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetViewModel.cs
@@ -25,8 +25,8 @@ public AssetViewModel(ConstructorParameters parameters)
{
}
- ///
- public new TAsset Asset => (TAsset)base.Asset;
+ ///
+ public override TAsset Asset => (TAsset)base.Asset;
}
public abstract class AssetViewModel : SessionObjectViewModel, IAssetPropertyProviderViewModel
@@ -57,10 +57,12 @@ protected AssetViewModel(ConstructorParameters parameters)
PropertyGraph.Changed += AssetPropertyChanged;
PropertyGraph.ItemChanged += AssetPropertyChanged;
}
+ // Add to directory after asset node has been created, so that listener to directory changes can retrieve it
+ directory.AddAsset(this, parameters.CanUndoRedoCreation);
Initializing = false;
}
- public Asset Asset => AssetItem.Asset;
+ public virtual Asset Asset => AssetItem.Asset;
public AssetItem AssetItem
{
@@ -192,7 +194,7 @@ public static HashSet ComputeRecursiveReferencedAssets(IEnumerab
var result = new HashSet(assets.SelectMany(x => x.Dependencies.RecursiveReferencedAssets));
return result;
}
-
+
private void BaseContentChanged(INodeChangeEventArgs e, IGraphNode node)
{
// FIXME xplat-editor
diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/DirectoryBaseViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/DirectoryBaseViewModel.cs
index 6786d7a078..59bec7036b 100644
--- a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/DirectoryBaseViewModel.cs
+++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/DirectoryBaseViewModel.cs
@@ -25,7 +25,7 @@ protected DirectoryBaseViewModel(ISessionViewModel session)
/// Gets the package containing this directory.
///
public abstract PackageViewModel Package { get; }
-
+
///
/// Gets or sets the parent directory of this directory.
///
@@ -46,6 +46,27 @@ protected DirectoryBaseViewModel(ISessionViewModel session)
///
public ReadOnlyObservableCollection SubDirectories { get; }
+ ///
+ /// Retrieves the directory corresponding to the given path.
+ ///
+ /// The path to the directory.
+ /// The directory corresponding to the given path if found, otherwise null.
+ /// The path should correspond to a directory, not an asset.
+ public DirectoryBaseViewModel? GetDirectory(string path)
+ {
+ ArgumentNullException.ThrowIfNull(path);
+
+ var directoryNames = path.Split(Separator.ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
+ DirectoryBaseViewModel? currentDirectory = this;
+ foreach (var directoryName in directoryNames)
+ {
+ currentDirectory = currentDirectory.SubDirectories.FirstOrDefault(x => string.Equals(directoryName, x.Name, StringComparison.InvariantCultureIgnoreCase));
+ if (currentDirectory is null)
+ return null;
+ }
+ return currentDirectory;
+ }
+
public IReadOnlyCollection GetDirectoryHierarchy()
{
var hierarchy = new List { this };
@@ -55,7 +76,8 @@ public IReadOnlyCollection GetDirectoryHierarchy()
public DirectoryBaseViewModel GetOrCreateDirectory(string path)
{
- if (path == null) throw new ArgumentNullException(nameof(path));
+ ArgumentNullException.ThrowIfNull(path);
+
DirectoryBaseViewModel result = this;
if (!string.IsNullOrEmpty(path))
{
@@ -65,9 +87,19 @@ public DirectoryBaseViewModel GetOrCreateDirectory(string path)
return result;
}
- internal void AddAsset(AssetViewModel asset)
+ internal void AddAsset(AssetViewModel asset, bool canUndoRedo)
{
- assets.Add(asset);
+ if (canUndoRedo)
+ {
+ assets.Add(asset);
+ }
+ else
+ {
+ using (SuspendNotificationForCollectionChange(nameof(Assets)))
+ {
+ assets.Add(asset);
+ }
+ }
}
internal void RemoveAsset(AssetViewModel asset)
diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/IPartDesignViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/IPartDesignViewModel.cs
new file mode 100644
index 0000000000..93c1779b72
--- /dev/null
+++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/IPartDesignViewModel.cs
@@ -0,0 +1,17 @@
+// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net)
+// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
+
+namespace Stride.Core.Assets.Presentation.ViewModels;
+
+///
+/// An interface for view models that contain a design part of .
+///
+public interface IPartDesignViewModel
+ where TAssetPartDesign : IAssetPartDesign
+ where TAssetPart : IIdentifiable
+{
+ ///
+ /// Gets the part design object associated to the asset-side part.
+ ///
+ TAssetPartDesign PartDesign { get; }
+}
diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/MountPointViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/MountPointViewModel.cs
index ae25819d8e..ee77ffe4ff 100644
--- a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/MountPointViewModel.cs
+++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/MountPointViewModel.cs
@@ -42,9 +42,11 @@ public override string Name
///
public override string TypeDisplayName => "Mount Point";
+ public abstract bool AcceptAssetType(Type assetType);
+
///
protected override void UpdateIsDeletedStatus()
{
- throw new NotImplementedException();
+ throw new InvalidOperationException();
}
}
diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/PackageViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/PackageViewModel.cs
index 6f769c1f86..c3c3434627 100644
--- a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/PackageViewModel.cs
+++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/PackageViewModel.cs
@@ -56,7 +56,7 @@ public PackageViewModel(ISessionViewModel session, PackageContainer packageConta
public IEnumerable MountPoints => Content.OfType();
- internal ObservableList DeletedAssetsInternal { get; } = new ObservableList();
+ internal ObservableList DeletedAssetsInternal { get; } = [];
///
/// Gets or sets the name of this package.
@@ -90,7 +90,7 @@ public UFile PackagePath
///
/// Gets the collection of root assets for this package.
///
- public ObservableSet RootAssets { get; } = new ObservableSet();
+ public ObservableSet RootAssets { get; } = [];
public UDirectory RootDirectory => Package.RootDirectory;
@@ -112,6 +112,7 @@ public DirectoryBaseViewModel GetOrCreateAssetDirectory(string assetDirectory)
/// Creates the view models for each asset, directory, profile, project and reference of this package.
///
/// A cancellation token to cancel the load process. Can be null.
+ // FIXME xplat-editor: most method here should be moved to an utility in the editor project (asset project should have minimum capability)
public void LoadPackageInformation(IProgressViewModel? progressVM, ref double progress, CancellationToken token = default)
{
if (token.IsCancellationRequested)
@@ -135,9 +136,7 @@ public void LoadPackageInformation(IProgressViewModel? progressVM, ref double pr
{
directory = GetOrCreateAssetDirectory(url.GetFullDirectory());
}
- var assetViewModel = CreateAsset(asset, directory);
- directory.AddAsset(assetViewModel);
-
+ CreateAsset(asset, directory, false);
progress++;
}
@@ -195,7 +194,8 @@ private static int ComparePackageContent(ViewModelBase x, ViewModelBase y)
throw new InvalidOperationException("Unable to sort the given items for the Content collection of PackageViewModel");
}
- private AssetViewModel CreateAsset(AssetItem assetItem, DirectoryBaseViewModel directory, ILogger? logger = null)
+ // FIXME xplat-editor: most method here should be moved to an utility in the editor project (asset project should have minimum capability)
+ public AssetViewModel CreateAsset(AssetItem assetItem, DirectoryBaseViewModel directory, bool canUndoRedoCreation, ILogger? logger = null)
{
AssetCollectionItemIdHelper.GenerateMissingItemIds(assetItem.Asset);
Session.GraphContainer.InitializeAsset(assetItem, logger);
@@ -204,7 +204,7 @@ private AssetViewModel CreateAsset(AssetItem assetItem, DirectoryBaseViewModel d
{
assetViewModelType = assetViewModelType.MakeGenericType(assetItem.Asset.GetType());
}
- return (AssetViewModel)Activator.CreateInstance(assetViewModelType, new ConstructorParameters(assetItem, directory, false))!;
+ return (AssetViewModel)Activator.CreateInstance(assetViewModelType, new ConstructorParameters(assetItem, directory, canUndoRedoCreation))!;
}
private void FillRootAssetCollection()
diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/ProjectCodeViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/ProjectCodeViewModel.cs
index 4d308e0ee6..7957e76745 100644
--- a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/ProjectCodeViewModel.cs
+++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/ProjectCodeViewModel.cs
@@ -10,8 +10,10 @@ public ProjectCodeViewModel(ProjectViewModel package)
{
}
+ ///
public override bool IsEditable => false;
+ ///
public override string Name
{
get => "Code";
@@ -19,4 +21,10 @@ public override string Name
}
public ProjectViewModel Project => (ProjectViewModel)Package;
+
+ ///
+ public override bool AcceptAssetType(Type assetType)
+ {
+ return typeof(IProjectAsset).IsAssignableFrom(assetType);
+ }
}
diff --git a/sources/editor/Stride.GameStudio.Avalonia/App.axaml.cs b/sources/editor/Stride.GameStudio.Avalonia/App.axaml.cs
index ef0e4f011e..1ad1077206 100644
--- a/sources/editor/Stride.GameStudio.Avalonia/App.axaml.cs
+++ b/sources/editor/Stride.GameStudio.Avalonia/App.axaml.cs
@@ -43,7 +43,7 @@ public override void OnFrameworkInitializationCompleted()
}
public void Restart(UFile? initialPath = null)
- {
+ {
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow!.DataContext = InitializeMainViewModel(initialPath);
@@ -81,11 +81,15 @@ private static IViewModelServiceProvider InitializeServiceProvider()
var services = new object[]
{
dispatcherService,
- new PluginService(dispatcherService)
+ new PluginService(dispatcherService),
};
var serviceProvider = new ViewModelServiceProvider(services);
serviceProvider.RegisterService(new EditorDebugService(serviceProvider));
serviceProvider.RegisterService(new EditorDialogService(serviceProvider));
+ if (DialogService.MainWindow?.Clipboard is { } clipboard)
+ {
+ serviceProvider.RegisterService(new ClipboardService(clipboard));
+ }
return serviceProvider;
}
}
diff --git a/sources/editor/Stride.GameStudio.Avalonia/Services/PluginService.cs b/sources/editor/Stride.GameStudio.Avalonia/Services/PluginService.cs
index 4c16a0905b..5c632f79a8 100644
--- a/sources/editor/Stride.GameStudio.Avalonia/Services/PluginService.cs
+++ b/sources/editor/Stride.GameStudio.Avalonia/Services/PluginService.cs
@@ -2,12 +2,11 @@
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
using Stride.Core.Assets;
-using Stride.Core.Assets.Editor;
using Stride.Core.Assets.Editor.Avalonia.Views;
using Stride.Core.Assets.Editor.Editors;
using Stride.Core.Assets.Editor.Services;
using Stride.Core.Assets.Editor.ViewModels;
-using Stride.Core.Assets.Presentation;
+using Stride.Core.Assets.Presentation.Services;
using Stride.Core.Assets.Presentation.ViewModels;
using Stride.Core.Diagnostics;
using Stride.Core.Extensions;
diff --git a/sources/editor/Stride.GameStudio.Avalonia/Views/AssetExplorerView.axaml b/sources/editor/Stride.GameStudio.Avalonia/Views/AssetExplorerView.axaml
index 7db603a00d..949f22a063 100644
--- a/sources/editor/Stride.GameStudio.Avalonia/Views/AssetExplorerView.axaml
+++ b/sources/editor/Stride.GameStudio.Avalonia/Views/AssetExplorerView.axaml
@@ -2,12 +2,34 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
- xmlns:vm="using:Stride.Core.Assets.Presentation.ViewModels"
- xmlns:vm2="using:Stride.Core.Assets.Editor.ViewModels"
- xmlns:cv="using:Stride.GameStudio.Avalonia.Converters"
+ xmlns:sd="http://schemas.stride3d.net/xaml/presentation"
+ xmlns:caev="using:Stride.Core.Assets.Editor.ViewModels"
+ xmlns:capvm="using:Stride.Core.Assets.Presentation.ViewModels"
+ xmlns:cpc="using:Stride.Core.Presentation.Commands"
+ xmlns:gc="using:Stride.GameStudio.Avalonia.Converters"
+ xmlns:gvw="using:Stride.GameStudio.Avalonia.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Stride.GameStudio.Avalonia.Views.AssetExplorerView"
- x:DataType="vm2:AssetCollectionViewModel">
+ x:DataType="caev:AssetCollectionViewModel">
+
+
+
+
+
+
+
+
+
+
+
@@ -15,14 +37,15 @@
+ Margin="4"
+ ContextMenu="{StaticResource AssetContextMenu}">
-
+
@@ -31,7 +54,7 @@
-
diff --git a/sources/editor/Stride.GameStudio.Avalonia/Views/MainView.axaml.cs b/sources/editor/Stride.GameStudio.Avalonia/Views/MainView.axaml.cs
index 75f1a03def..d5ea64728f 100644
--- a/sources/editor/Stride.GameStudio.Avalonia/Views/MainView.axaml.cs
+++ b/sources/editor/Stride.GameStudio.Avalonia/Views/MainView.axaml.cs
@@ -14,6 +14,21 @@ public MainView()
InitializeComponent();
}
+ ///
+ /// Gets a platform-specific for the Copy action
+ ///
+ public static KeyGesture? CopyGesture => Application.Current?.PlatformSettings?.HotkeyConfiguration.Copy.FirstOrDefault();
+
+ ///
+ /// Gets a platform-specific for the Cut action
+ ///
+ public static KeyGesture? CutGesture => Application.Current?.PlatformSettings?.HotkeyConfiguration.Cut.FirstOrDefault();
+
+ ///
+ /// Gets a platform-specific for the Paste action
+ ///
+ public static KeyGesture? PasteGesture => Application.Current?.PlatformSettings?.HotkeyConfiguration.Paste.FirstOrDefault();
+
///
/// Gets a platform-specific for the Redo action
///
diff --git a/sources/presentation/Stride.Core.Presentation/Commands/AnonymousCommand.cs b/sources/presentation/Stride.Core.Presentation/Commands/AnonymousCommand.cs
index 5723b4b49e..0ac47dd84d 100644
--- a/sources/presentation/Stride.Core.Presentation/Commands/AnonymousCommand.cs
+++ b/sources/presentation/Stride.Core.Presentation/Commands/AnonymousCommand.cs
@@ -142,6 +142,7 @@ public override bool CanExecute(object? parameter)
return result && canExecute != null ? canExecute((T)parameter!) : result;
}
}
+
///
/// An implementation of that routes calls to a given anonymous method with a typed parameter.
///