Skip to content

Commit 56a305a

Browse files
committed
[Editor] Implement copy/paste in asset composite editors
1 parent de6c392 commit 56a305a

File tree

13 files changed

+487
-28
lines changed

13 files changed

+487
-28
lines changed

sources/editor/Stride.Assets.Editor.Avalonia/Views/EntityHierarchyEditorView.axaml

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,46 @@
33
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
44
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
55
xmlns:sd="http://schemas.stride3d.net/xaml/presentation"
6-
xmlns:vm="using:Stride.Assets.Editor.ViewModels"
7-
xmlns:vm2="using:Stride.Assets.Presentation.ViewModels"
6+
xmlns:aevm="using:Stride.Assets.Editor.ViewModels"
7+
xmlns:apvm="using:Stride.Assets.Presentation.ViewModels"
8+
xmlns:cpc="using:Stride.Core.Presentation.Commands"
89
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
910
x:Class="Stride.Assets.Editor.Avalonia.Views.EntityHierarchyEditorView"
10-
x:DataType="vm:EntityHierarchyEditorViewModel">
11+
x:DataType="aevm:EntityHierarchyEditorViewModel">
12+
<UserControl.Resources>
13+
<ContextMenu x:Key="TreeViewItemContextMenu">
14+
<Separator/>
15+
<MenuItem Header="{sd:LocalizeString Cut, Context=Menu}"
16+
Command="{Binding $parent[UserControl].DataContext.CutCommand, FallbackValue={x:Static cpc:DisabledCommand.Instance}}"/>
17+
<MenuItem Header="{sd:LocalizeString Copy, Context=Menu}"
18+
Command="{Binding $parent[UserControl].DataContext.CopyCommand, FallbackValue={x:Static cpc:DisabledCommand.Instance}}"/>
19+
<MenuItem Header="{sd:LocalizeString Paste, Context=Menu}"
20+
Command="{Binding $parent[UserControl].DataContext.PasteCommand, FallbackValue={x:Static cpc:DisabledCommand.Instance}}"/>
21+
<MenuItem Header="{sd:LocalizeString Delete, Context=Menu}"
22+
Command="{Binding $parent[UserControl].DataContext.DeleteCommand, FallbackValue={x:Static cpc:DisabledCommand.Instance}}"/>
23+
</ContextMenu>
24+
</UserControl.Resources>
1125
<Grid ColumnDefinitions="*, 4, 3*">
1226
<DockPanel Grid.Column="0">
1327
<TreeView ItemsSource="{Binding HierarchyRoot, Mode=OneWay, Converter={sd:Yield}}"
1428
SelectedItems="{Binding SelectedContent}"
1529
SelectionMode="Multiple">
1630
<Control.DataTemplates>
1731
<!-- Default template -->
18-
<TreeDataTemplate DataType="{x:Type vm2:EntityHierarchyItemViewModel}"
32+
<TreeDataTemplate DataType="{x:Type apvm:EntityHierarchyItemViewModel}"
1933
ItemsSource="{Binding Children}">
2034
<TextBlock Text="{Binding Name}" />
2135
</TreeDataTemplate>
2236
<!-- Entity template -->
23-
<TreeDataTemplate DataType="{x:Type vm2:EntityViewModel}"
37+
<TreeDataTemplate DataType="{x:Type apvm:EntityViewModel}"
2438
ItemsSource="{Binding Children}">
2539
<StackPanel Orientation="Horizontal">
2640
<!-- TODO icon -->
2741
<TextBlock Text="{Binding Name}" />
2842
</StackPanel>
2943
</TreeDataTemplate>
3044
<!-- Scene template -->
31-
<TreeDataTemplate DataType="{x:Type vm2:SceneRootViewModel}"
45+
<TreeDataTemplate DataType="{x:Type apvm:SceneRootViewModel}"
3246
ItemsSource="{Binding Children}">
3347
<StackPanel Orientation="Horizontal">
3448
<!-- TODO icon -->
@@ -38,6 +52,7 @@
3852
</Control.DataTemplates>
3953
<StyledElement.Styles>
4054
<Style Selector="TreeViewItem">
55+
<Setter Property="ContextMenu" Value="{StaticResource TreeViewItemContextMenu}"/>
4156
<Setter Property="IsExpanded" Value="True"/>
4257
</Style>
4358
</StyledElement.Styles>

sources/editor/Stride.Assets.Editor/ViewModels/EntityHierarchyEditorViewModel.cs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using System.Collections.Specialized;
55
using Stride.Assets.Entities;
66
using Stride.Assets.Presentation.ViewModels;
7+
using Stride.Core.Assets;
8+
using Stride.Core.Assets.Editor.Quantum;
79
using Stride.Core.Assets.Editor.ViewModels;
810
using Stride.Engine;
911

@@ -18,10 +20,73 @@ protected EntityHierarchyEditorViewModel(EntityHierarchyViewModel asset)
1820

1921
public EntityHierarchyRootViewModel HierarchyRoot => (EntityHierarchyRootViewModel)RootPart;
2022

23+
/// <inheritdoc />
24+
protected override bool CanDelete()
25+
{
26+
return SelectedContent.Count > 0 && !SelectedContent.Contains(HierarchyRoot);
27+
}
28+
29+
/// <inheritdoc />
30+
protected override bool CanPaste(bool asRoot)
31+
{
32+
if (!base.CanPaste(asRoot))
33+
return false;
34+
35+
return CopyPasteService!.CanPaste(
36+
ClipboardService!.GetTextAsync().Result, Asset.AssetType,
37+
asRoot ? typeof(AssetCompositeHierarchyData<EntityDesign, Entity>) : typeof(Entity),
38+
typeof(AssetCompositeHierarchyData<EntityDesign, Entity>), typeof(EntityComponent));
39+
}
40+
41+
/// <inheritdoc />
42+
protected override async Task Delete()
43+
{
44+
var entitiesToDelete = GetCommonRoots(SelectedItems);
45+
// FIXME xplat-editor
46+
//var ask = SceneEditorSettings.AskBeforeDeletingEntities.GetValue();
47+
//if (ask)
48+
//{
49+
// var confirmMessage = Tr._p("Message", "Are you sure you want to delete this entity?");
50+
// // TODO: we should compute the actual total number of entities to be deleted here (children recursively, etc.)
51+
// if (entitiesToDelete.Count > 1)
52+
// confirmMessage = string.Format(Tr._p("Message", "Are you sure you want to delete these {0} entities?"), entitiesToDelete.Count);
53+
// var checkedMessage = string.Format(Stride.Core.Assets.Editor.Settings.EditorSettings.AlwaysDeleteWithoutAsking, "entities");
54+
// var buttons = DialogHelper.CreateButtons(new[] { Tr._p("Button", "Delete"), Tr._p("Button", "Cancel") }, 1, 2);
55+
// var result = await ServiceProvider.Get<IDialogService>().CheckedMessageBoxAsync(confirmMessage, false, checkedMessage, buttons, MessageBoxImage.Question);
56+
// if (result.Result != 1)
57+
// return;
58+
// if (result.IsChecked == true)
59+
// {
60+
// SceneEditorSettings.AskBeforeDeletingEntities.SetValue(false);
61+
// SceneEditorSettings.Save();
62+
// }
63+
//}
64+
65+
using var transaction = Session.ActionService?.CreateTransaction();
66+
//var foldersToDelete = SelectedContent.OfType<EntityFolderViewModel>().ToList();
67+
ClearSelection();
68+
69+
// Delete entities first
70+
var entitiesPerScene = entitiesToDelete.GroupBy(x => x.Asset);
71+
foreach (var entities in entitiesPerScene)
72+
{
73+
entities.Key.AssetHierarchyPropertyGraph.DeleteParts(entities.Select(x => x.PartDesign), out var mapping);
74+
Session.ActionService?.PushOperation(new DeletedPartsTrackingOperation<EntityDesign, Entity>(entities.Key, mapping));
75+
}
76+
77+
//// Then folders
78+
//foreach (var folder in foldersToDelete)
79+
//{
80+
// folder.Delete();
81+
//}
82+
83+
Session.ActionService?.SetName(transaction!, "Delete selected entities");
84+
}
85+
2186
/// <inheritdoc />
2287
protected override async Task RefreshEditorProperties()
2388
{
24-
EditorProperties.UpdateTypeAndName(SelectedItems, x => "Entity", x => x.Name, "entities");
89+
EditorProperties.UpdateTypeAndName(SelectedItems, _ => "Entity", x => x.Name ?? string.Empty, "entities");
2590
await EditorProperties.GenerateSelectionPropertiesAsync(SelectedItems);
2691
}
2792

sources/editor/Stride.Assets.Editor/ViewModels/SceneEditorViewModel.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,32 @@ public SceneEditorViewModel(SceneViewModel asset)
1818

1919
/// <inheritdoc />
2020
public override SceneViewModel Asset => (SceneViewModel)base.Asset;
21+
22+
/// <inheritdoc />
23+
protected override Task Delete()
24+
{
25+
var sceneRoots = SelectedContent.OfType<SceneRootViewModel>().ToList();
26+
// Mix of scene roots and entities selected
27+
if (sceneRoots.Count != SelectedContent.Count)
28+
return base.Delete();
29+
30+
using var transaction = Session.ActionService?.CreateTransaction();
31+
ClearSelection();
32+
foreach (var sceneRoot in GetCommonRoots(sceneRoots))
33+
{
34+
DeleteSceneRoot(sceneRoot);
35+
}
36+
Session.ActionService?.SetName(transaction!, "Remove selected child scenes");
37+
return Task.CompletedTask;
38+
}
39+
40+
private void DeleteSceneRoot(SceneRootViewModel sceneRoot)
41+
{
42+
if (sceneRoot.Parent is SceneRootViewModel parent)
43+
{
44+
// Reset parenting link
45+
parent.Asset.Children.Remove(Asset);
46+
}
47+
}
48+
2149
}

sources/editor/Stride.Assets.Editor/ViewModels/UILibraryEditorViewModel.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,10 @@ public UILibraryEditorViewModel(UILibraryViewModel asset)
1818

1919
/// <inheritdoc />
2020
public override UILibraryViewModel Asset => (UILibraryViewModel)base.Asset;
21+
22+
/// <inheritdoc />
23+
protected override Task Delete()
24+
{
25+
throw new NotImplementedException();
26+
}
2127
}

sources/editor/Stride.Assets.Editor/ViewModels/UIPageEditorViewModel.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,10 @@ public UIPageEditorViewModel(UIPageViewModel asset)
1818

1919
/// <inheritdoc />
2020
public override UIPageViewModel Asset => (UIPageViewModel)base.Asset;
21+
22+
/// <inheritdoc />
23+
protected override Task Delete()
24+
{
25+
throw new NotImplementedException();
26+
}
2127
}

sources/editor/Stride.Assets.Presentation/ViewModels/SceneViewModel.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Stride.Assets.Entities;
55
using Stride.Core.Assets.Presentation.Annotations;
66
using Stride.Core.Assets.Presentation.ViewModels;
7+
using Stride.Core.Presentation.Collections;
78

89
namespace Stride.Assets.Presentation.ViewModels;
910

@@ -20,4 +21,6 @@ public SceneViewModel(ConstructorParameters parameters)
2021

2122
/// <inheritdoc />
2223
public override SceneAsset Asset => (SceneAsset)base.Asset;
24+
25+
public IObservableList<SceneViewModel> Children { get; } = new ObservableSet<SceneViewModel>();
2326
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp)
2+
// Distributed under the MIT license. See the LICENSE.md file in the project root for more information.
3+
using Stride.Core.Assets.Presentation.ViewModels;
4+
using Stride.Core.Assets.Quantum;
5+
using Stride.Core.Extensions;
6+
using Stride.Core.Presentation.Dirtiables;
7+
8+
namespace Stride.Core.Assets.Editor.Quantum;
9+
10+
/// <summary>
11+
/// Represents the operation of updating the mapping of deleted part instances in an <see cref="AssetCompositeHierarchyPropertyGraph{TAssetPartDesign,TAssetPart}"/>.
12+
/// </summary>
13+
/// <typeparam name="TAssetPartDesign"></typeparam>
14+
/// <typeparam name="TAssetPart"></typeparam>
15+
public sealed class DeletedPartsTrackingOperation<TAssetPartDesign, TAssetPart> : DirtyingOperation
16+
where TAssetPartDesign : class, IAssetPartDesign<TAssetPart>
17+
where TAssetPart : class, IIdentifiable
18+
{
19+
private readonly HashSet<Tuple<Guid, Guid>> deletedPartsMapping;
20+
private AssetCompositeHierarchyPropertyGraph<TAssetPartDesign, TAssetPart> propertyGraph;
21+
22+
/// <summary>
23+
/// Initializes a new instance of the <see cref="DeletedPartsTrackingOperation{TAssetPartDesign,TAssetPart}"/> class.
24+
/// </summary>
25+
/// <param name="viewmodel"></param>
26+
/// <param name="deletedPartsMapping">A mapping of the base information (base part id, instance id) of the deleted parts that have a base.</param>
27+
public DeletedPartsTrackingOperation(AssetCompositeHierarchyViewModel<TAssetPartDesign, TAssetPart> viewmodel, HashSet<Tuple<Guid, Guid>> deletedPartsMapping)
28+
: base(viewmodel.SafeArgument(nameof(viewmodel)).Dirtiables)
29+
{
30+
this.deletedPartsMapping = deletedPartsMapping ?? throw new ArgumentNullException(nameof(deletedPartsMapping));
31+
propertyGraph = viewmodel.AssetHierarchyPropertyGraph;
32+
33+
}
34+
35+
/// <inheritdoc />
36+
protected override void FreezeContent()
37+
{
38+
propertyGraph = null!;
39+
}
40+
41+
/// <inheritdoc />
42+
protected override void Undo()
43+
{
44+
propertyGraph.UntrackDeletedInstanceParts(deletedPartsMapping);
45+
}
46+
47+
/// <inheritdoc />
48+
protected override void Redo()
49+
{
50+
propertyGraph.TrackDeletedInstanceParts(deletedPartsMapping);
51+
}
52+
}

sources/editor/Stride.Core.Assets.Editor/Quantum/NodePresenters/Commands/PastePropertyCommandBase.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,19 @@ public override bool CanAttach(INodePresenter nodePresenter)
2727

2828
protected virtual bool CanPaste(IReadOnlyCollection<INodePresenter> nodePresenters)
2929
{
30-
foreach (var nodePresenter in nodePresenters)
30+
foreach (var nodePresenter in nodePresenters.OfType<IAssetNodePresenter>())
3131
{
32-
var assetNodePresenter = nodePresenter as IAssetNodePresenter;
33-
var copyPasteService = assetNodePresenter?.Asset?.ServiceProvider.TryGet<ICopyPasteService>();
32+
var copyPasteService = nodePresenter.Asset?.ServiceProvider.TryGet<ICopyPasteService>();
3433
if (copyPasteService is null)
3534
return false;
3635

37-
var asset = assetNodePresenter!.Asset!.Asset;
3836

39-
var clipboard = assetNodePresenter?.Asset?.ServiceProvider.TryGet<IClipboardService>();
37+
var clipboard = nodePresenter.Asset?.ServiceProvider.TryGet<IClipboardService>();
4038
if (clipboard is null)
4139
return false;
4240

41+
var asset = nodePresenter.Asset!.Asset;
42+
4343
if (!copyPasteService.CanPaste(clipboard.GetTextAsync().Result, asset.GetType(), (nodePresenter as ItemNodePresenter)?.OwnerCollection.Type ?? nodePresenter.Type))
4444
return false;
4545

@@ -54,7 +54,7 @@ protected virtual bool CanPaste(IReadOnlyCollection<INodePresenter> nodePresente
5454
return true;
5555
}
5656

57-
protected async Task DoPasteAsync(INodePresenter nodePresenter, bool replace)
57+
protected static async Task DoPasteAsync(INodePresenter nodePresenter, bool replace)
5858
{
5959
var asset = ((IAssetNodePresenter)nodePresenter).Asset;
6060
if (asset is null)
@@ -77,7 +77,7 @@ protected async Task DoPasteAsync(INodePresenter nodePresenter, bool replace)
7777
return;
7878

7979
var actionService = asset.UndoRedoService;
80-
using var transaction = actionService.CreateTransaction();
80+
using var transaction = actionService?.CreateTransaction();
8181

8282
// FIXME: for now we only handle one result item
8383
var item = result.Items[0];
@@ -86,12 +86,12 @@ protected async Task DoPasteAsync(INodePresenter nodePresenter, bool replace)
8686

8787
var propertyContainer = new PropertyContainer { { AssetPropertyPasteProcessor.IsReplaceKey, replace } };
8888
await (item.Processor?.Paste(item, asset.PropertyGraph, ref nodeAccessor, ref propertyContainer) ?? Task.CompletedTask);
89-
actionService.SetName(transaction, replace ? "Replace property" : "Paste property");
89+
actionService?.SetName(transaction!, replace ? "Replace property" : "Paste property");
9090
}
9191

9292
private static bool IsInReadOnlyCollection(INodePresenter? nodePresenter)
9393
{
94-
if (nodePresenter is null || !nodePresenter.IsEnumerable)
94+
if (nodePresenter is not { IsEnumerable: true })
9595
return false;
9696

9797
var memberCollection = (nodePresenter as MemberNodePresenter)?.MemberAttributes.OfType<MemberCollectionAttribute>().FirstOrDefault()

sources/editor/Stride.Core.Assets.Editor/ViewModels/AssetCollectionViewModel.CopyPaste.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,12 @@ private bool CanCopy()
4141

4242
private bool CanPaste()
4343
{
44-
if (CopyPasteService is { } copyPaste && ClipboardService is { } clipboard)
45-
{
46-
var text = clipboard.GetTextAsync().Result;
47-
return copyPaste.CanPaste(text, typeof(List<AssetItem>), typeof(List<AssetItem>), typeof(List<AssetItem>));
48-
}
44+
if (CopyPasteService is not { } copyPaste || ClipboardService is not { } clipboard)
45+
return false;
46+
47+
var text = clipboard.GetTextAsync().Result;
48+
return copyPaste.CanPaste(text, typeof(List<AssetItem>), typeof(List<AssetItem>), typeof(List<AssetItem>));
4949

50-
return false;
5150
}
5251

5352
private async Task CopyAssetUrl()

sources/editor/Stride.Core.Assets.Editor/ViewModels/AssetCompositeEditorViewModel.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ protected AssetCompositeEditorViewModel(TAssetViewModel asset)
2424

2525
public ObservableSet<object> SelectedContent { get; } = [];
2626

27+
/// <summary>
28+
/// Clears the selection.
29+
/// </summary>
30+
public void ClearSelection()
31+
{
32+
SelectedContent.Clear();
33+
}
34+
2735
public override void Destroy()
2836
{
2937
// Unregister collection

0 commit comments

Comments
 (0)