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 SelectedContent { get; } = []; + /// + /// Clears the selection. + /// + public void ClearSelection() + { + SelectedContent.Clear(); + } + public override void Destroy() { // Unregister collection diff --git a/sources/editor/Stride.Core.Assets.Editor/ViewModels/AssetCompositeHierarchyEditorViewModel.CopyPaste.cs b/sources/editor/Stride.Core.Assets.Editor/ViewModels/AssetCompositeHierarchyEditorViewModel.CopyPaste.cs new file mode 100644 index 0000000000..5e3e0ac3cd --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/ViewModels/AssetCompositeHierarchyEditorViewModel.CopyPaste.cs @@ -0,0 +1,223 @@ +// 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 System.Diagnostics.CodeAnalysis; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Core.Assets.Quantum; +using Stride.Core.Extensions; +using Stride.Core.Presentation.Commands; +using Stride.Core.Presentation.Services; +using Stride.Core.Quantum; + +namespace Stride.Core.Assets.Editor.ViewModels; + +partial class AssetCompositeHierarchyEditorViewModel +{ + protected IClipboardService? ClipboardService => ServiceProvider.TryGet(); + protected ICopyPasteService? CopyPasteService => ServiceProvider.TryGet(); + private IDialogService DialogService => ServiceProvider.Get(); + + public ICommandBase CopyCommand { get; } + + public ICommandBase CutCommand { get; } + + public ICommandBase DeleteCommand { get; } + + public ICommandBase PasteCommand { get; } + + /// + /// Attaches additional properties into the given , to be consumed by the paste processor. + /// + /// The container into which to attach the properties + /// The view model of the item into which the paste will occur. + protected virtual void AttachPropertiesForPaste(ref PropertyContainer propertyContainer, AssetCompositeItemViewModel pasteTarget) + { + // Do nothing by default. + } + + protected virtual bool CanCopy() + { + return CopyPasteService is not null && ClipboardService is not null; + } + + protected virtual bool CanCut() => CanCopy() && CanDelete(); + + protected virtual bool CanDelete() + { + return SelectedItems.Count > 0; + } + + protected virtual bool CanPaste(bool asRoot) => CopyPasteService is not null && ClipboardService is not null; + + /// + /// Checks whether the given paste data can be pasted into the given item. + /// + /// + /// + /// + /// + protected virtual bool CanPasteIntoItem(IPasteResult pasteResult, AssetCompositeItemViewModel item, [NotNullWhen(false)] out string? error) + { + if (pasteResult == null) throw new ArgumentNullException(nameof(pasteResult)); + + if (pasteResult.Items + .Select(r => r.Data as AssetCompositeHierarchyData).NotNull() + .Any(h => GatherAllBasePartAssets(h).Contains(item.Asset.Id))) + { + error = "The copied elements depend on this asset and cannot be pasted."; + return false; + } + + error = null; + return true; + } + + protected abstract Task Delete(); + + protected virtual async Task Paste(bool asRoot) + { + using var transaction = Session.ActionService?.CreateTransaction(); + string actionName; + if (asRoot) + { + // Attempt to paste at the root level + await PasteIntoItems(RootPart.Yield()!); + actionName = $"Paste into {Asset.Name}"; + } + else + { + var selectedItems = SelectedContent.OfType().ToList(); + if (selectedItems.Count == 0) + return; + + // Attempt to paste into the selected items + await PasteIntoItems(selectedItems); + actionName = "Paste into selection"; + } + + Session.ActionService?.SetName(transaction!, actionName); + } + + /// + /// Attempts to paste the current clipboard's content into the specified . + /// + /// + /// A that can be awaited until the operation completes. + protected async Task PasteIntoItems(IEnumerable items) + { + ArgumentNullException.ThrowIfNull(items); + + // Retrieve data from the clipboard + var text = await ClipboardService!.GetTextAsync(); + if (string.IsNullOrEmpty(text)) + return; + + var pasteResults = new Dictionary(); + foreach (var item in items) + { + var pasteResult = CopyPasteService!.DeserializeCopiedData(text, item.Asset.Asset, typeof(TAssetPart)); + if (pasteResult.Items.Count == 0) + return; + + if (!CanPasteIntoItem(pasteResult, item, out var error)) + { + await DialogService.MessageBoxAsync(error, MessageBoxButton.OK, MessageBoxImage.Information); + return; + } + + pasteResults.Add(item, pasteResult); + } + foreach (var (item, pasteResult) in pasteResults) + { + var targetContent = item.GetNodePath().GetNode(); + var propertyContainer = new PropertyContainer(); + AttachPropertiesForPaste(ref propertyContainer, item); + var nodeAccessor = new NodeAccessor(targetContent, NodeIndex.Empty); + foreach (var pasteItem in pasteResult.Items) + { + await (pasteItem.Processor?.Paste(pasteItem, item.Asset.PropertyGraph!, ref nodeAccessor, ref propertyContainer) ?? Task.CompletedTask); + } + } + } + + /// + /// Prepares the given hierarchy to be copied into the clipboard. + /// + /// The hierarchy to prepare, that has been cloned out of the actual parts. + /// The view models of the actual items that are being copied (including parts and virtual items). + /// The view models of the actual parts that are being copied. + protected virtual void PrepareToCopy(AssetCompositeHierarchyData clonedHierarchy, IReadOnlyCollection commonRoots, IReadOnlyCollection commonParts) + { + // Do nothing by default + } + + protected virtual void UpdateCommands() + { + // We need to do it on the cut/copy/paste/delete commands too, otherwise it is not correct in the game view + CopyCommand.IsEnabled = CanCopy(); + DeleteCommand.IsEnabled = CanDelete(); + } + + private async Task Copy() + { + // Group by asset + var items = SelectedContent.Cast().GroupBy(x => x.Asset).Select(grp => + { + IReadOnlyCollection commonRoots = GetCommonRoots(grp.ToList()); + IReadOnlyCollection commonParts = GetCommonRoots(SelectedItems.Where(x => x.Asset == grp.Key).ToList()); + return (commonRoots, commonParts, asset: (TAssetViewModel)grp.Key); + }); + await WriteToClipboardAsync(items); + } + + private async Task Cut() + { + if (SelectedItems.Count == 0) + return; + + // Group by asset + var items = SelectedContent.Cast().GroupBy(x => x.Asset).Select(grp => + { + IReadOnlyCollection commonRoots = GetCommonRoots(grp.ToList()); + IReadOnlyCollection commonParts = GetCommonRoots(SelectedItems.Where(x => x.Asset == grp.Key).ToList()); + return (commonRoots, commonParts, asset: (TAssetViewModel)grp.Key); + }).ToList(); + + // Clear the selection + ClearSelection(); + + using var transaction = Session.ActionService?.CreateTransaction(); + await WriteToClipboardAsync(items); + + // We don't use DeletePart but rather RemovePartFromAsset so references to the cut element won't be cleared. + // Then, if we paste into the same asset, they will be automagically restored. + foreach (var item in items.SelectMany(x => x.commonParts).DepthFirst(x => x.EnumerateChildren().OfType()).Reverse()) + { + ((AssetCompositeHierarchyPropertyGraph?)item.Asset.PropertyGraph)?.RemovePartFromAsset(item.PartDesign); + } + Session.ActionService?.SetName(transaction!, "Cut selection"); + } + + private async Task WriteToClipboardAsync( + IEnumerable<(IReadOnlyCollection commonRoots, IReadOnlyCollection commonParts, TAssetViewModel asset)> items) + { + try + { + var text = CopyPasteService!.CopyFromAssets(items.Select(x => + { + var hierarchy = AssetCompositeHierarchyPropertyGraph.CloneSubHierarchies(Session.AssetNodeContainer, x.asset.Asset, x.commonParts.Select(r => r.Id.ObjectId), SubHierarchyCloneFlags.None, out _); + PrepareToCopy(hierarchy, x.commonRoots, x.commonParts); + return ((AssetPropertyGraph)x.asset.AssetHierarchyPropertyGraph, (AssetId?)x.asset.Id, (object)hierarchy, false); + }).ToList(), typeof(AssetCompositeHierarchyData)); + if (string.IsNullOrEmpty(text)) + return; + + await ClipboardService!.SetTextAsync(text); + } + catch (SystemException) + { + // We don't provide feedback when copying fails. + } + } +} diff --git a/sources/editor/Stride.Core.Assets.Editor/ViewModels/AssetCompositeHierarchyEditorViewModel.cs b/sources/editor/Stride.Core.Assets.Editor/ViewModels/AssetCompositeHierarchyEditorViewModel.cs index ac17e3b6f5..6534330fc3 100644 --- a/sources/editor/Stride.Core.Assets.Editor/ViewModels/AssetCompositeHierarchyEditorViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Editor/ViewModels/AssetCompositeHierarchyEditorViewModel.cs @@ -5,6 +5,7 @@ using Stride.Core.Assets.Presentation.ViewModels; using Stride.Core.Extensions; using Stride.Core.Presentation.Collections; +using Stride.Core.Presentation.Commands; using Stride.Core.Presentation.Quantum; namespace Stride.Core.Assets.Editor.ViewModels; @@ -16,29 +17,31 @@ namespace Stride.Core.Assets.Editor.ViewModels; /// The type of a part. /// /// The type of a real that can be copied/cut/pasted. -public abstract class AssetCompositeHierarchyEditorViewModel - : AssetCompositeEditorViewModel, AssetCompositeHierarchyViewModel> +public abstract partial class AssetCompositeHierarchyEditorViewModel + : AssetCompositeEditorViewModel, TAssetViewModel> where TAssetPartDesign : class, IAssetPartDesign where TAssetPart : class, IIdentifiable where TAssetViewModel : AssetCompositeHierarchyViewModel - where TItemViewModel : AssetCompositeItemViewModel + where TItemViewModel : AssetCompositeItemViewModel, IPartDesignViewModel, IAssetPartViewModel { - private TItemViewModel rootPart; private bool updateSelectionGuard; protected AssetCompositeHierarchyEditorViewModel(TAssetViewModel asset) : base(asset) { - rootPart = CreateRootPartViewModel(); + CopyCommand = new AnonymousTaskCommand(ServiceProvider, Copy, CanCopy); + CutCommand = new AnonymousTaskCommand(ServiceProvider, Cut, CanCut); + DeleteCommand = new AnonymousTaskCommand(ServiceProvider, Delete, CanDelete); + PasteCommand = new AnonymousTaskCommand(ServiceProvider, Paste, CanPaste); SelectedContent.CollectionChanged += SelectedContentCollectionChanged; SelectedItems.CollectionChanged += SelectedItemsCollectionChanged; } - public TItemViewModel RootPart { get => rootPart; private set => SetValue(ref rootPart, value); } + public required AssetCompositeItemViewModel RootPart { get; init; } public ObservableSet SelectedItems { get; } = []; - + /// public override void Destroy() { @@ -64,21 +67,52 @@ public override void Destroy() base.Destroy(); } + public static IReadOnlySet GetCommonRoots(IReadOnlyCollection items) + where TViewModel : AssetCompositeItemViewModel + { + var hashSet = new HashSet(items); + foreach (var item in items) + { + var parent = item.Parent; + while (parent != null) + { + if (hashSet.Contains(parent)) + { + hashSet.Remove(item); + break; + } + parent = parent.Parent; + } + } + return hashSet; + } + /// public override IAssetPartViewModel? FindPartViewModel(AbsoluteId id) { if (RootPart is IAssetPartViewModel item && id == item.Id) return item; - return RootPart?.EnumerateChildren().BreadthFirst(x => x.EnumerateChildren()).FirstOrDefault(part => part is IAssetPartViewModel viewModel && viewModel.Id == id) as IAssetPartViewModel; + return RootPart.EnumerateChildren().BreadthFirst(x => x.EnumerateChildren()).FirstOrDefault(part => part is IAssetPartViewModel viewModel && viewModel.Id == id) as IAssetPartViewModel; } - protected abstract TItemViewModel CreateRootPartViewModel(); + /// + /// Gathers all base assets used in the composition of the given hierarchy, recursively. + /// + /// + /// + public IReadOnlySet GatherAllBasePartAssets(AssetCompositeHierarchyData hierarchy) + { + ArgumentNullException.ThrowIfNull(hierarchy); + var baseAssets = new HashSet(); + GatherAllBasePartAssetsRecursively(hierarchy.Parts.Values, Session.PackageSession, baseAssets); + return baseAssets; + } protected abstract Task RefreshEditorProperties(); /// - /// Called when the content of changed. + /// Called when the content of changed. /// /// The action that caused the event. /// @@ -95,7 +129,7 @@ protected virtual void SelectedContentCollectionChanged(NotifyCollectionChangedA /// /// 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. ///