1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Profile editor - Added unro/redo

Profile editor - Added adding layers and folders
This commit is contained in:
Robert 2022-01-02 00:04:46 +01:00
parent 34757716aa
commit 736324e45e
40 changed files with 1878 additions and 317 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,20 @@
using System;
namespace Artemis.Core
{
/// <summary>
/// Provides data for profile element events.
/// </summary>
public class ProfileElementEventArgs : EventArgs
{
internal ProfileElementEventArgs(ProfileElement profileElement)
{
ProfileElement = profileElement;
}
/// <summary>
/// Gets the profile element this event is related to
/// </summary>
public ProfileElement ProfileElement { get; }
}
}

View File

@ -22,17 +22,13 @@ namespace Artemis.Core
/// </summary>
/// <param name="parent">The parent of the folder</param>
/// <param name="name">The name of the folder</param>
/// <param name="order">The order where to place the child (0-based), defaults to the end of the collection</param>
public Folder(ProfileElement parent, string name, int order) : base(parent.Profile)
public Folder(ProfileElement parent, string name) : base(parent, parent.Profile)
{
FolderEntity = new FolderEntity();
EntityId = Guid.NewGuid();
Parent = parent ?? throw new ArgumentNullException(nameof(parent));
Profile = Parent.Profile;
Name = name;
Parent.AddChild(this, order);
}
/// <summary>
@ -41,13 +37,12 @@ namespace Artemis.Core
/// <param name="profile">The profile the folder belongs to</param>
/// <param name="parent">The parent of the folder</param>
/// <param name="folderEntity">The entity of the folder</param>
public Folder(Profile profile, ProfileElement parent, FolderEntity folderEntity) : base(parent.Profile)
public Folder(Profile profile, ProfileElement parent, FolderEntity folderEntity) : base(parent, parent.Profile)
{
FolderEntity = folderEntity;
EntityId = folderEntity.Id;
Profile = profile;
Parent = parent;
Name = folderEntity.Name;
IsExpanded = folderEntity.IsExpanded;
Suspended = folderEntity.Suspended;

View File

@ -4,7 +4,6 @@ using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects;
using Artemis.Core.ScriptingProviders;
using Artemis.Storage.Entities.Profile;
using Artemis.Storage.Entities.Profile.Abstract;
using RGB.NET.Core;
@ -29,13 +28,11 @@ namespace Artemis.Core
/// </summary>
/// <param name="parent">The parent of the layer</param>
/// <param name="name">The name of the layer</param>
/// <param name="order">The order where to place the child (0-based), defaults to the end of the collection</param>
public Layer(ProfileElement parent, string name, int order) : base(parent.Profile)
public Layer(ProfileElement parent, string name) : base(parent, parent.Profile)
{
LayerEntity = new LayerEntity();
EntityId = Guid.NewGuid();
Parent = parent ?? throw new ArgumentNullException(nameof(parent));
Profile = Parent.Profile;
Name = name;
Suspended = false;
@ -48,7 +45,6 @@ namespace Artemis.Core
Adapter = new LayerAdapter(this);
Initialize();
Parent.AddChild(this, order);
}
/// <summary>
@ -57,7 +53,7 @@ namespace Artemis.Core
/// <param name="profile">The profile the layer belongs to</param>
/// <param name="parent">The parent of the layer</param>
/// <param name="layerEntity">The entity of the layer</param>
public Layer(Profile profile, ProfileElement parent, LayerEntity layerEntity) : base(parent.Profile)
public Layer(Profile profile, ProfileElement parent, LayerEntity layerEntity) : base(parent, parent.Profile)
{
LayerEntity = layerEntity;
EntityId = layerEntity.Id;
@ -148,10 +144,8 @@ namespace Artemis.Core
if (LayerBrush?.BaseProperties != null)
result.AddRange(LayerBrush.BaseProperties.GetAllLayerProperties());
foreach (BaseLayerEffect layerEffect in LayerEffects)
{
if (layerEffect.BaseProperties != null)
result.AddRange(layerEffect.BaseProperties.GetAllLayerProperties());
}
return result;
}
@ -172,6 +166,19 @@ namespace Artemis.Core
/// </summary>
public event EventHandler? LayerBrushUpdated;
#region Overrides of BreakableModel
/// <inheritdoc />
public override IEnumerable<IBreakableModel> GetBrokenHierarchy()
{
if (LayerBrush?.BrokenState != null)
yield return LayerBrush;
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.BrokenState != null))
yield return baseLayerEffect;
}
#endregion
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
@ -385,6 +392,18 @@ namespace Artemis.Core
Enabled = true;
}
/// <inheritdoc />
public override void Activate()
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override void Deactivate()
{
throw new NotImplementedException();
}
/// <inheritdoc />
public override void Disable()
{
@ -512,7 +531,9 @@ namespace Artemis.Core
throw new ObjectDisposedException("Layer");
if (!Leds.Any())
{
Path = new SKPath();
}
else
{
SKPath path = new() {FillType = SKPathFillType.Winding};
@ -761,19 +782,6 @@ namespace Artemis.Core
}
#endregion
#region Overrides of BreakableModel
/// <inheritdoc />
public override IEnumerable<IBreakableModel> GetBrokenHierarchy()
{
if (LayerBrush?.BrokenState != null)
yield return LayerBrush;
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => e.BrokenState != null))
yield return baseLayerEffect;
}
#endregion
}
/// <summary>

View File

@ -197,13 +197,9 @@ namespace Artemis.Core
// Populate the profile starting at the root, the rest is populated recursively
FolderEntity? rootFolder = ProfileEntity.Folders.FirstOrDefault(f => f.ParentId == EntityId);
if (rootFolder == null)
{
Folder _ = new(this, "Root folder", 0);
}
AddChild(new Folder(this, "Root folder"));
else
{
AddChild(new Folder(this, this, rootFolder));
}
}
List<RenderProfileElement> renderElements = GetAllRenderElements();

View File

@ -156,7 +156,7 @@ namespace Artemis.Core
StreamlineOrder();
}
OnChildAdded();
OnChildAdded(child);
}
/// <summary>
@ -176,7 +176,7 @@ namespace Artemis.Core
child.Parent = null;
}
OnChildRemoved();
OnChildRemoved(child);
}
private void StreamlineOrder()
@ -262,27 +262,27 @@ namespace Artemis.Core
/// <summary>
/// Occurs when a child was added to the <see cref="Children" /> list
/// </summary>
public event EventHandler? ChildAdded;
public event EventHandler<ProfileElementEventArgs>? ChildAdded;
/// <summary>
/// Occurs when a child was removed from the <see cref="Children" /> list
/// </summary>
public event EventHandler? ChildRemoved;
public event EventHandler<ProfileElementEventArgs>? ChildRemoved;
/// <summary>
/// Invokes the <see cref="ChildAdded" /> event
/// </summary>
protected virtual void OnChildAdded()
protected virtual void OnChildAdded(ProfileElement child)
{
ChildAdded?.Invoke(this, EventArgs.Empty);
ChildAdded?.Invoke(this, new ProfileElementEventArgs(child));
}
/// <summary>
/// Invokes the <see cref="ChildRemoved" /> event
/// </summary>
protected virtual void OnChildRemoved()
protected virtual void OnChildRemoved(ProfileElement child)
{
ChildRemoved?.Invoke(this, EventArgs.Empty);
ChildRemoved?.Invoke(this, new ProfileElementEventArgs(child));
}
#endregion

View File

@ -20,12 +20,13 @@ namespace Artemis.Core
private SKRectI _bounds;
private SKPath? _path;
internal RenderProfileElement(Profile profile) : base(profile)
internal RenderProfileElement(ProfileElement parent, Profile profile) : base(profile)
{
Timeline = new Timeline();
ExpandedPropertyGroups = new List<string>();
LayerEffectsList = new List<BaseLayerEffect>();
LayerEffects = new ReadOnlyCollection<BaseLayerEffect>(LayerEffectsList);
Parent = parent ?? throw new ArgumentNullException(nameof(parent));
LayerEffectStore.LayerEffectAdded += LayerEffectStoreOnLayerEffectAdded;
LayerEffectStore.LayerEffectRemoved += LayerEffectStoreOnLayerEffectRemoved;
@ -52,6 +53,16 @@ namespace Artemis.Core
/// </summary>
public event EventHandler? LayerEffectsUpdated;
/// <summary>
/// Activates the render profile element, loading required brushes, effects or anything else needed for rendering
/// </summary>
public abstract void Activate();
/// <summary>
/// Deactivates the render profile element, disposing required brushes, effects or anything else needed for rendering
/// </summary>
public abstract void Deactivate();
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
@ -155,9 +166,9 @@ namespace Artemis.Core
/// <summary>
/// Gets the parent of this element
/// </summary>
public new ProfileElement? Parent
public new ProfileElement Parent
{
get => base.Parent;
get => base.Parent!;
internal set
{
base.Parent = value;

View File

@ -137,7 +137,8 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem
if (!SupportsChildren)
throw new ArtemisUIException("Cannot add a folder to a profile element of type " + ProfileElement.GetType().Name);
Folder _ = new(ProfileElement, "New folder", 0);
Folder folder = new(ProfileElement, "New folder");
ProfileElement.AddChild(folder, 0);
_profileEditorService.SaveSelectedProfileConfiguration();
}
@ -146,8 +147,8 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem
if (!SupportsChildren)
throw new ArtemisUIException("Cannot add a layer to a profile element of type " + ProfileElement.GetType().Name);
Layer layer = new(ProfileElement, "New layer", 0);
Layer layer = new(ProfileElement, "New layer");
ProfileElement.AddChild(layer, 0);
// Could be null if the default brush got disabled
LayerBrushDescriptor brush = _layerBrushService.GetDefaultLayerBrush();
if (brush != null)

View File

@ -90,6 +90,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization.Tools
private void CreateLayer(Folder folder, List<ArtemisLed> selectedLeds)
{
Layer newLayer = new(folder, "New layer");
folder.AddChild(newLayer);
LayerBrushDescriptor brush = _layerBrushService.GetDefaultLayerBrush();
if (brush != null)

View File

@ -14,7 +14,7 @@
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="0.10.11" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.11" />
<PackageReference Include="ReactiveUI" Version="17.1.9" />
<PackageReference Include="ReactiveUI" Version="16.3.10" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Artemis.Core\Artemis.Core.csproj" />

View File

@ -55,16 +55,12 @@
},
"ReactiveUI": {
"type": "Direct",
"requested": "[17.1.9, )",
"resolved": "17.1.9",
"contentHash": "bxH6uzEi1b6cfGoBBvCXWrdT18+Rleggi0R/vOaCYc+zvlSleJW6wlUtcWjrKzgQHD8EN3c02A4JBTt9oGSWWQ==",
"requested": "[16.3.10, )",
"resolved": "16.3.10",
"contentHash": "NH9bg8BROqRrTp6YLpPDsJrfNDzRWNmP63fQ68CBAM+i7YHi6wcPeOkxyKpoemUxKEY4QECuicaTblJnxgbWmA==",
"dependencies": {
"DynamicData": "7.4.3",
"Splat": "14.1.1",
"System.ComponentModel": "4.3.0",
"System.Diagnostics.Contracts": "4.3.0",
"System.Dynamic.Runtime": "4.3.0",
"System.Runtime.Serialization.Primitives": "4.3.0"
"Splat": "13.1.42"
}
},
"Avalonia.Angle.Windows.Natives": {
@ -923,14 +919,6 @@
"System.Text.Encoding": "4.3.0"
}
},
"System.Diagnostics.Contracts": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "eelRRbnm+OloiQvp9CXS0ixjNQldjjkHO4iIkR5XH2VIP8sUB/SIpa1TdUW6/+HDcQ+MlhP3pNa1u5SbzYuWGA==",
"dependencies": {
"System.Runtime": "4.3.0"
}
},
"System.Diagnostics.Debug": {
"type": "Transitive",
"resolved": "4.3.0",
@ -1411,15 +1399,6 @@
"System.Runtime.Extensions": "4.3.0"
}
},
"System.Runtime.Serialization.Primitives": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "Wz+0KOukJGAlXjtKr+5Xpuxf8+c8739RI1C+A2BoQZT+wMCCoMDDdO8/4IRHfaVINqL78GO8dW8G2lW/e45Mcw==",
"dependencies": {
"System.Resources.ResourceManager": "4.3.0",
"System.Runtime": "4.3.0"
}
},
"System.Security.AccessControl": {
"type": "Transitive",
"resolved": "5.0.0",
@ -1779,7 +1758,7 @@
"Material.Icons.Avalonia": "1.0.2",
"RGB.NET.Core": "1.0.0-prerelease1",
"RGB.NET.Layout": "1.0.0-prerelease1",
"ReactiveUI": "17.1.9",
"ReactiveUI": "16.3.10",
"ReactiveUI.Validation": "2.2.1",
"Splat.Ninject": "14.1.17"
}
@ -1797,7 +1776,7 @@
"FluentAvaloniaUI": "1.1.8",
"Material.Icons.Avalonia": "1.0.2",
"RGB.NET.Core": "1.0.0-prerelease1",
"ReactiveUI": "17.1.9",
"ReactiveUI": "16.3.10",
"ReactiveUI.Validation": "2.2.1"
}
}

View File

@ -14,7 +14,7 @@
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="0.10.11" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.11" />
<PackageReference Include="ReactiveUI" Version="17.1.9" />
<PackageReference Include="ReactiveUI" Version="16.3.10" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Artemis.Core\Artemis.Core.csproj" />

View File

@ -55,16 +55,12 @@
},
"ReactiveUI": {
"type": "Direct",
"requested": "[17.1.9, )",
"resolved": "17.1.9",
"contentHash": "bxH6uzEi1b6cfGoBBvCXWrdT18+Rleggi0R/vOaCYc+zvlSleJW6wlUtcWjrKzgQHD8EN3c02A4JBTt9oGSWWQ==",
"requested": "[16.3.10, )",
"resolved": "16.3.10",
"contentHash": "NH9bg8BROqRrTp6YLpPDsJrfNDzRWNmP63fQ68CBAM+i7YHi6wcPeOkxyKpoemUxKEY4QECuicaTblJnxgbWmA==",
"dependencies": {
"DynamicData": "7.4.3",
"Splat": "14.1.1",
"System.ComponentModel": "4.3.0",
"System.Diagnostics.Contracts": "4.3.0",
"System.Dynamic.Runtime": "4.3.0",
"System.Runtime.Serialization.Primitives": "4.3.0"
"Splat": "13.1.42"
}
},
"Avalonia.Angle.Windows.Natives": {
@ -923,14 +919,6 @@
"System.Text.Encoding": "4.3.0"
}
},
"System.Diagnostics.Contracts": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "eelRRbnm+OloiQvp9CXS0ixjNQldjjkHO4iIkR5XH2VIP8sUB/SIpa1TdUW6/+HDcQ+MlhP3pNa1u5SbzYuWGA==",
"dependencies": {
"System.Runtime": "4.3.0"
}
},
"System.Diagnostics.Debug": {
"type": "Transitive",
"resolved": "4.3.0",
@ -1411,15 +1399,6 @@
"System.Runtime.Extensions": "4.3.0"
}
},
"System.Runtime.Serialization.Primitives": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "Wz+0KOukJGAlXjtKr+5Xpuxf8+c8739RI1C+A2BoQZT+wMCCoMDDdO8/4IRHfaVINqL78GO8dW8G2lW/e45Mcw==",
"dependencies": {
"System.Resources.ResourceManager": "4.3.0",
"System.Runtime": "4.3.0"
}
},
"System.Security.AccessControl": {
"type": "Transitive",
"resolved": "5.0.0",
@ -1779,7 +1758,7 @@
"Material.Icons.Avalonia": "1.0.2",
"RGB.NET.Core": "1.0.0-prerelease1",
"RGB.NET.Layout": "1.0.0-prerelease1",
"ReactiveUI": "17.1.9",
"ReactiveUI": "16.3.10",
"ReactiveUI.Validation": "2.2.1",
"Splat.Ninject": "14.1.17"
}
@ -1797,7 +1776,7 @@
"FluentAvaloniaUI": "1.1.8",
"Material.Icons.Avalonia": "1.0.2",
"RGB.NET.Core": "1.0.0-prerelease1",
"ReactiveUI": "17.1.9",
"ReactiveUI": "16.3.10",
"ReactiveUI.Validation": "2.2.1"
}
}

View File

@ -24,7 +24,7 @@
<PackageReference Include="Avalonia.Xaml.Interactivity" Version="0.10.11.5" />
<PackageReference Include="FluentAvaloniaUI" Version="1.1.8" />
<PackageReference Include="Material.Icons.Avalonia" Version="1.0.2" />
<PackageReference Include="ReactiveUI" Version="17.1.9" />
<PackageReference Include="ReactiveUI" Version="16.3.10" />
<PackageReference Include="ReactiveUI.Validation" Version="2.2.1" />
<PackageReference Include="RGB.NET.Core" Version="1.0.0-prerelease1" />
</ItemGroup>

View File

@ -93,16 +93,12 @@
},
"ReactiveUI": {
"type": "Direct",
"requested": "[17.1.9, )",
"resolved": "17.1.9",
"contentHash": "bxH6uzEi1b6cfGoBBvCXWrdT18+Rleggi0R/vOaCYc+zvlSleJW6wlUtcWjrKzgQHD8EN3c02A4JBTt9oGSWWQ==",
"requested": "[16.3.10, )",
"resolved": "16.3.10",
"contentHash": "NH9bg8BROqRrTp6YLpPDsJrfNDzRWNmP63fQ68CBAM+i7YHi6wcPeOkxyKpoemUxKEY4QECuicaTblJnxgbWmA==",
"dependencies": {
"DynamicData": "7.4.3",
"Splat": "14.1.1",
"System.ComponentModel": "4.3.0",
"System.Diagnostics.Contracts": "4.3.0",
"System.Dynamic.Runtime": "4.3.0",
"System.Runtime.Serialization.Primitives": "4.3.0"
"Splat": "13.1.42"
}
},
"ReactiveUI.Validation": {
@ -725,8 +721,8 @@
},
"Splat": {
"type": "Transitive",
"resolved": "14.1.1",
"contentHash": "bKQtKu57w+iJ1T+WDyDdNq+LBNVdmNu2i0vGNyUdtmg4TEIEFiX2GfPusNEAW2QOfxyDE7i4+xTxbOKZr/4jhg=="
"resolved": "13.1.42",
"contentHash": "a/NkGyoSsmvH2YZGgjFxt0dsXkRTgQRMgoUDN8WpBhTUr3wnPTdeQTOLLr2Jc/BCAdOA7cK2+E4Io8I1/q3f3Q=="
},
"Svg.Custom": {
"type": "Transitive",
@ -889,14 +885,6 @@
"System.Text.Encoding": "4.3.0"
}
},
"System.Diagnostics.Contracts": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "eelRRbnm+OloiQvp9CXS0ixjNQldjjkHO4iIkR5XH2VIP8sUB/SIpa1TdUW6/+HDcQ+MlhP3pNa1u5SbzYuWGA==",
"dependencies": {
"System.Runtime": "4.3.0"
}
},
"System.Diagnostics.Debug": {
"type": "Transitive",
"resolved": "4.3.0",
@ -1377,15 +1365,6 @@
"System.Runtime.Extensions": "4.3.0"
}
},
"System.Runtime.Serialization.Primitives": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "Wz+0KOukJGAlXjtKr+5Xpuxf8+c8739RI1C+A2BoQZT+wMCCoMDDdO8/4IRHfaVINqL78GO8dW8G2lW/e45Mcw==",
"dependencies": {
"System.Resources.ResourceManager": "4.3.0",
"System.Runtime": "4.3.0"
}
},
"System.Security.AccessControl": {
"type": "Transitive",
"resolved": "5.0.0",

View File

@ -17,7 +17,7 @@
<PackageReference Include="Avalonia.Win32" Version="0.10.11" />
<PackageReference Include="Microsoft.Win32" Version="2.0.1" />
<PackageReference Include="RawInput.Sharp" Version="0.0.4" />
<PackageReference Include="ReactiveUI" Version="17.1.9" />
<PackageReference Include="ReactiveUI" Version="16.3.10" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Artemis.Core\Artemis.Core.csproj" />

View File

@ -82,16 +82,12 @@
},
"ReactiveUI": {
"type": "Direct",
"requested": "[17.1.9, )",
"resolved": "17.1.9",
"contentHash": "bxH6uzEi1b6cfGoBBvCXWrdT18+Rleggi0R/vOaCYc+zvlSleJW6wlUtcWjrKzgQHD8EN3c02A4JBTt9oGSWWQ==",
"requested": "[16.3.10, )",
"resolved": "16.3.10",
"contentHash": "NH9bg8BROqRrTp6YLpPDsJrfNDzRWNmP63fQ68CBAM+i7YHi6wcPeOkxyKpoemUxKEY4QECuicaTblJnxgbWmA==",
"dependencies": {
"DynamicData": "7.4.3",
"Splat": "14.1.1",
"System.ComponentModel": "4.3.0",
"System.Diagnostics.Contracts": "4.3.0",
"System.Dynamic.Runtime": "4.3.0",
"System.Runtime.Serialization.Primitives": "4.3.0"
"Splat": "13.1.42"
}
},
"Avalonia.Angle.Windows.Natives": {
@ -939,14 +935,6 @@
"System.Text.Encoding": "4.3.0"
}
},
"System.Diagnostics.Contracts": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "eelRRbnm+OloiQvp9CXS0ixjNQldjjkHO4iIkR5XH2VIP8sUB/SIpa1TdUW6/+HDcQ+MlhP3pNa1u5SbzYuWGA==",
"dependencies": {
"System.Runtime": "4.3.0"
}
},
"System.Diagnostics.Debug": {
"type": "Transitive",
"resolved": "4.3.0",
@ -1427,15 +1415,6 @@
"System.Runtime.Extensions": "4.3.0"
}
},
"System.Runtime.Serialization.Primitives": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "Wz+0KOukJGAlXjtKr+5Xpuxf8+c8739RI1C+A2BoQZT+wMCCoMDDdO8/4IRHfaVINqL78GO8dW8G2lW/e45Mcw==",
"dependencies": {
"System.Resources.ResourceManager": "4.3.0",
"System.Runtime": "4.3.0"
}
},
"System.Security.AccessControl": {
"type": "Transitive",
"resolved": "5.0.0",
@ -1795,7 +1774,7 @@
"Material.Icons.Avalonia": "1.0.2",
"RGB.NET.Core": "1.0.0-prerelease1",
"RGB.NET.Layout": "1.0.0-prerelease1",
"ReactiveUI": "17.1.9",
"ReactiveUI": "16.3.10",
"ReactiveUI.Validation": "2.2.1",
"Splat.Ninject": "14.1.17"
}
@ -1813,7 +1792,7 @@
"FluentAvaloniaUI": "1.1.8",
"Material.Icons.Avalonia": "1.0.2",
"RGB.NET.Core": "1.0.0-prerelease1",
"ReactiveUI": "17.1.9",
"ReactiveUI": "16.3.10",
"ReactiveUI.Validation": "2.2.1"
}
}

View File

@ -23,7 +23,7 @@
<PackageReference Include="Flurl.Http" Version="3.2.0" />
<PackageReference Include="Live.Avalonia" Version="1.3.1" />
<PackageReference Include="Material.Icons.Avalonia" Version="1.0.2" />
<PackageReference Include="ReactiveUI" Version="17.1.9" />
<PackageReference Include="ReactiveUI" Version="16.3.10" />
<PackageReference Include="ReactiveUI.Validation" Version="2.2.1" />
<PackageReference Include="RGB.NET.Core" Version="1.0.0-prerelease1" />
<PackageReference Include="RGB.NET.Layout" Version="1.0.0-prerelease1" />
@ -59,9 +59,4 @@
<ItemGroup>
<Folder Include="Screens\ProfileEditor\Tools\" />
</ItemGroup>
<ItemGroup>
<Reference Include="HistoricalReactiveCommand">
<HintPath>..\..\..\..\HistoricalReactiveCommand\HistoricalReactiveCommand\bin\Debug\netstandard2.1\HistoricalReactiveCommand.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,56 @@
using System;
using Artemis.Core;
using Artemis.UI.Services.ProfileEditor;
namespace Artemis.UI.Screens.ProfileEditor.Commands
{
public class AddProfileElement : IProfileEditorCommand, IDisposable
{
private readonly int _index;
private readonly RenderProfileElement _subject;
private readonly ProfileElement _target;
private bool _isAdded;
public AddProfileElement(RenderProfileElement subject, ProfileElement target, int index)
{
_subject = subject;
_target = target;
_index = index;
DisplayName = subject switch
{
Layer => "Add layer",
Folder => "Add folder",
_ => throw new ArgumentException("Type of subject is not supported")
};
}
/// <inheritdoc />
public void Dispose()
{
if (!_isAdded)
_subject.Dispose();
}
#region Implementation of IProfileEditorCommand
/// <inheritdoc />
public string DisplayName { get; }
/// <inheritdoc />
public void Execute()
{
_isAdded = true;
_target.AddChild(_subject, _index);
}
/// <inheritdoc />
public void Undo()
{
_isAdded = false;
_target.RemoveChild(_subject);
}
#endregion
}
}

View File

@ -0,0 +1,63 @@
using System;
using Artemis.Core;
using Artemis.UI.Exceptions;
using Artemis.UI.Services.ProfileEditor;
namespace Artemis.UI.Screens.ProfileEditor.Commands
{
public class RemoveProfileElement : IProfileEditorCommand, IDisposable
{
private readonly int _index;
private readonly RenderProfileElement _subject;
private readonly ProfileElement _target;
private bool _isRemoved;
public RemoveProfileElement(RenderProfileElement subject)
{
if (subject.Parent == null)
throw new ArtemisUIException("Can't remove a subject that has no parent");
_subject = subject;
_target = _subject.Parent;
_index = _subject.Children.IndexOf(_subject);
DisplayName = subject switch
{
Layer => "Remove layer",
Folder => "Remove folder",
_ => throw new ArgumentException("Type of subject is not supported")
};
}
/// <inheritdoc />
public void Dispose()
{
if (_isRemoved)
_subject.Dispose();
}
#region Implementation of IProfileEditorCommand
/// <inheritdoc />
public string DisplayName { get; }
/// <inheritdoc />
public void Execute()
{
_isRemoved = true;
_target.RemoveChild(_subject);
_target.Deactivate();
}
/// <inheritdoc />
public void Undo()
{
_isRemoved = false;
_target.Activate();
_target.AddChild(_subject, _index);
}
#endregion
}
}

View File

@ -64,10 +64,27 @@
</MenuItem>
</MenuItem>
<MenuItem Header="_Edit" SubmenuOpened="MenuItem_OnSubmenuOpened">
<MenuItem Header="_Undo"
Command="{Binding History.Undo}"
HotKey="Ctrl+Z"
InputGesture="Ctrl+Z">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Undo" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="_Redo"
Command="{Binding History.Redo}"
HotKey="Ctrl+Y"
InputGesture="Ctrl+Y">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Redo" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="_Duplicate"
Command="{Binding Duplicate}"
HotKey="Ctrl+D"
IsEnabled="{Binding HasSelectedElement}">
Command="{Binding Duplicate}"
HotKey="Ctrl+D"
IsEnabled="{Binding HasSelectedElement}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentDuplicate" />
</MenuItem.Icon>

View File

@ -2,10 +2,11 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Panels.MenuBar
{
public partial class MenuBarView : UserControl
public partial class MenuBarView : ReactiveUserControl<MenuBarViewModel>
{
public MenuBarView()
{

View File

@ -1,13 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reactive.Disposables;
using Artemis.UI.Services.ProfileEditor;
using Artemis.UI.Shared;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Panels.MenuBar
{
public class MenuBarViewModel : ViewModelBase
public class MenuBarViewModel : ActivatableViewModelBase
{
private ProfileEditorHistory? _history;
public MenuBarViewModel(IProfileEditorService profileEditorService)
{
this.WhenActivated(d => profileEditorService.History.Subscribe(history => History = history).DisposeWith(d));
}
public ProfileEditorHistory? History
{
get => _history;
set => this.RaiseAndSetIfChanged(ref _history, value);
}
}
}
}

View File

@ -7,8 +7,8 @@
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileTree.FolderTreeItemView">
<Grid ColumnDefinitions="Auto,Auto,*,Auto">
<Button Grid.Column="0"
ToolTip.Tip="{Binding BrokenState}"
IsVisible="{Binding BrokenState, Converter={x:Static ObjectConverters.IsNotNull}}"
ToolTip.Tip="{Binding ProfileElement.BrokenState}"
IsVisible="{Binding ProfileElement.BrokenState, Converter={x:Static ObjectConverters.IsNotNull}}"
Classes="icon-button icon-button-small"
Foreground="White"
Background="#E74C4C"
@ -28,7 +28,7 @@
<ToggleButton Grid.Column="3"
Classes="icon-button icon-button-small"
ToolTip.Tip="Toggle suspended state"
IsChecked="{Binding !Folder.Suspended}"
IsChecked="{Binding Folder.Suspended}"
VerticalAlignment="Center"
Margin="4 0">
<avalonia:MaterialIcon Kind="Pause" />

View File

@ -1,11 +1,12 @@
using Artemis.Core;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Shared.Services.Interfaces;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree
{
public class FolderTreeItemViewModel : TreeItemViewModel
{
public FolderTreeItemViewModel(TreeItemViewModel? parent, Folder folder, IProfileEditorVmFactory profileEditorVmFactory) : base(parent, folder)
public FolderTreeItemViewModel(TreeItemViewModel? parent, Folder folder, IProfileEditorVmFactory profileEditorVmFactory, IWindowService windowService) : base(parent, folder, windowService)
{
Folder = folder;

View File

@ -7,8 +7,8 @@
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileTree.LayerTreeItemView">
<Grid ColumnDefinitions="Auto,Auto,*,Auto">
<Button Grid.Column="0"
ToolTip.Tip="{Binding BrokenState}"
IsVisible="{Binding BrokenState, Converter={x:Static ObjectConverters.IsNotNull}}"
ToolTip.Tip="{Binding ProfileElement.BrokenState}"
IsVisible="{Binding ProfileElement.BrokenState, Converter={x:Static ObjectConverters.IsNotNull}}"
Classes="icon-button icon-button-small"
Foreground="#E74C4C"
Command="{Binding ShowBrokenStateExceptions}">

View File

@ -1,10 +1,11 @@
using Artemis.Core;
using Artemis.UI.Shared.Services.Interfaces;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree
{
public class LayerTreeItemViewModel : TreeItemViewModel
{
public LayerTreeItemViewModel(TreeItemViewModel? parent, Layer layer) : base(parent, layer)
public LayerTreeItemViewModel(TreeItemViewModel? parent, Layer layer, IWindowService windowService) : base(parent, layer, windowService)
{
Layer = layer;
}

View File

@ -4,51 +4,58 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Services;
using Artemis.UI.Screens.ProfileEditor.Commands;
using Artemis.UI.Services.ProfileEditor;
using Artemis.UI.Shared;
using HistoricalReactiveCommand;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree
{
public class ProfileTreeViewModel : ActivatableViewModelBase
{
private readonly IProfileEditorService _profileEditorService;
private readonly IProfileEditorVmFactory _profileEditorVmFactory;
private ReactiveCommand<Unit, Unit>? _addFolder;
private ReactiveCommand<Unit, Unit>? _addLayer;
private TreeItemViewModel? _selectedTreeItem;
public ProfileTreeViewModel(IProfileEditorService profileEditorService, IProfileEditorVmFactory profileEditorVmFactory)
{
_profileEditorService = profileEditorService;
_profileEditorVmFactory = profileEditorVmFactory;
this.WhenActivated(d =>
{
ProfileConfiguration profileConfiguration = null!;
profileEditorService.CurrentProfileConfiguration.WhereNotNull().Subscribe(p => profileConfiguration = p).DisposeWith(d);
profileEditorService.CurrentProfileConfiguration.WhereNotNull().Subscribe(Repopulate).DisposeWith(d);
profileEditorService.CurrentProfileElement.WhereNotNull().Subscribe(SelectCurrentProfileElement).DisposeWith(d);
profileEditorService.ProfileConfiguration.WhereNotNull().Subscribe(configuration =>
{
Folder rootFolder = configuration.Profile!.GetRootFolder();
CreateTreeItems(rootFolder);
Observable.FromEventPattern<ProfileElementEventArgs>(x => rootFolder.ChildAdded += x, x => rootFolder.ChildAdded -= x).Subscribe(c => AddTreeItemIfMissing(c.EventArgs.ProfileElement));
Observable.FromEventPattern<ProfileElementEventArgs>(x => rootFolder.ChildRemoved += x, x => rootFolder.ChildRemoved -= x).Subscribe(c => RemoveTreeItemsIfFound(c.EventArgs.ProfileElement));
Folder rootFolder = profileConfiguration.Profile!.GetRootFolder();
AddLayer = ReactiveCommandEx.CreateWithHistory("AddLayerAtRoot",
// ReSharper disable once ObjectCreationAsStatement
() => new Layer(rootFolder, "New layer", 0),
() => rootFolder.RemoveChild(rootFolder.Children[0]),
profileConfiguration.Profile.EntityId.ToString()
);
AddLayer = ReactiveCommand.Create(() => AddLayerToRoot(configuration.Profile), profileEditorService.ProfileConfiguration.Select(p => p != null));
AddFolder = ReactiveCommand.Create(() => AddFolderToRoot(configuration.Profile), profileEditorService.ProfileConfiguration.Select(p => p != null));
}).DisposeWith(d);
AddFolder = ReactiveCommandEx.CreateWithHistory("AddFolderAtRoot",
// ReSharper disable once ObjectCreationAsStatement
() => new Folder(rootFolder, "New folder", 0),
() => rootFolder.RemoveChild(rootFolder.Children[0]),
profileConfiguration.Profile.EntityId.ToString()
);
profileEditorService.ProfileElement.WhereNotNull().Subscribe(SelectCurrentProfileElement).DisposeWith(d);
});
this.WhenAnyValue(vm => vm.SelectedTreeItem).Subscribe(model => profileEditorService.ChangeCurrentProfileElement(model?.ProfileElement));
}
public ReactiveCommandWithHistory<Unit, Unit>? AddLayer { get; set; }
public ReactiveCommandWithHistory<Unit, Unit>? AddFolder { get; set; }
public ReactiveCommand<Unit, Unit>? AddLayer
{
get => _addLayer;
set => this.RaiseAndSetIfChanged(ref _addLayer, value);
}
public ReactiveCommand<Unit, Unit>? AddFolder
{
get => _addFolder;
set => this.RaiseAndSetIfChanged(ref _addFolder, value);
}
public ObservableCollection<TreeItemViewModel> TreeItems { get; } = new();
@ -58,15 +65,44 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree
set => this.RaiseAndSetIfChanged(ref _selectedTreeItem, value);
}
private void Repopulate(ProfileConfiguration profileConfiguration)
private void RemoveTreeItemsIfFound(ProfileElement profileElement)
{
List<TreeItemViewModel> toRemove = TreeItems.Where(t => t.ProfileElement == profileElement).ToList();
foreach (TreeItemViewModel treeItemViewModel in toRemove)
TreeItems.Remove(treeItemViewModel);
}
private void AddTreeItemIfMissing(ProfileElement profileElement)
{
if (TreeItems.Any(t => t.ProfileElement == profileElement))
return;
if (profileElement is Folder folder)
TreeItems.Insert(folder.Parent.Children.IndexOf(folder), _profileEditorVmFactory.FolderTreeItemViewModel(null, folder));
else if (profileElement is Layer layer)
TreeItems.Insert(layer.Parent.Children.IndexOf(layer), _profileEditorVmFactory.LayerTreeItemViewModel(null, layer));
}
private void AddFolderToRoot(Profile profile)
{
Folder rootFolder = profile.GetRootFolder();
Folder folder = new(rootFolder, "New folder");
_profileEditorService.ExecuteCommand(new AddProfileElement(folder, rootFolder, 0));
}
private void AddLayerToRoot(Profile profile)
{
Folder rootFolder = profile.GetRootFolder();
Layer layer = new(rootFolder, "New layer");
_profileEditorService.ExecuteCommand(new AddProfileElement(layer, rootFolder, 0));
}
private void CreateTreeItems(Folder rootFolder)
{
if (TreeItems.Any())
TreeItems.Clear();
if (profileConfiguration.Profile == null)
return;
foreach (ProfileElement profileElement in profileConfiguration.Profile.GetRootFolder().Children)
foreach (ProfileElement profileElement in rootFolder.Children)
{
if (profileElement is Folder folder)
TreeItems.Add(_profileEditorVmFactory.FolderTreeItemViewModel(null, folder));

View File

@ -1,46 +1,51 @@
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Shared;
using HistoricalReactiveCommand;
using Artemis.UI.Shared.Services.Interfaces;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileTree
{
public abstract class TreeItemViewModel : ActivatableViewModelBase
{
private readonly IWindowService _windowService;
private bool _isExpanded;
protected TreeItemViewModel(TreeItemViewModel? parent, RenderProfileElement profileElement)
protected TreeItemViewModel(TreeItemViewModel? parent, RenderProfileElement profileElement, IWindowService windowService)
{
_windowService = windowService;
Parent = parent;
ProfileElement = profileElement;
AddLayerAtIndex = ReactiveCommandEx.CreateWithHistory<int, Layer>("AddLayerAtIndex",
(targetIndex, _) => new Layer(ProfileElement, "New folder", targetIndex),
(targetIndex, _) =>
{
Layer toRemove = (Layer) ProfileElement.Children.ElementAt(targetIndex);
ProfileElement.RemoveChild(toRemove);
return toRemove;
},
historyId: ProfileElement.Profile.EntityId.ToString()
);
// AddLayerAtIndex = ReactiveCommandWithHistory.CreateWithHistory<int, Layer>(
// (targetIndex, _) => new Layer(ProfileElement, "New folder"),
// (targetIndex, _) =>
// {
// Layer toRemove = (Layer) ProfileElement.Children.ElementAt(targetIndex);
// ProfileElement.RemoveChild(toRemove);
// return toRemove;
// },
// historyId: ProfileElement.Profile.EntityId.ToString()
// );
AddFolderAtIndex = ReactiveCommandEx.CreateWithHistory<int, Folder>("AddFolderAtIndex",
(targetIndex, _) => new Folder(ProfileElement, "New folder", targetIndex),
(targetIndex, _) =>
{
Folder toRemove = (Folder) ProfileElement.Children.ElementAt(targetIndex);
ProfileElement.RemoveChild(toRemove);
return toRemove;
},
historyId: ProfileElement.Profile.EntityId.ToString()
);
// AddFolderAtIndex = ReactiveCommandWithHistory.CreateWithHistory<int, Folder>(
// (targetIndex, _) => new Folder(ProfileElement, "New folder"),
// (targetIndex, _) =>
// {
// Folder toRemove = (Folder) ProfileElement.Children.ElementAt(targetIndex);
// ProfileElement.RemoveChild(toRemove);
// return toRemove;
// },
// historyId: ProfileElement.Profile.EntityId.ToString()
// );
}
public ReactiveCommandWithHistory<int, Layer> AddLayerAtIndex { get; set; }
public ReactiveCommandWithHistory<int, Folder> AddFolderAtIndex { get; set; }
public ReactiveCommand<int, Unit> AddLayerAtIndex { get; set; }
public ReactiveCommand<int, Unit> AddFolderAtIndex { get; set; }
public RenderProfileElement ProfileElement { get; }
public TreeItemViewModel? Parent { get; set; }
@ -51,5 +56,20 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree
get => _isExpanded;
set => this.RaiseAndSetIfChanged(ref _isExpanded, value);
}
public async Task ShowBrokenStateExceptions()
{
List<IBreakableModel> broken = ProfileElement.GetBrokenHierarchy().Where(b => b.BrokenStateException != null).ToList();
foreach (IBreakableModel current in broken)
{
_windowService.ShowExceptionDialog($"{current.BrokenDisplayName} - {current.BrokenState}", current.BrokenStateException!);
if (broken.Last() != current)
{
if (!await _windowService.ShowConfirmContentDialog("Broken state", "Do you want to view the next exception?"))
return;
}
}
}
}
}

View File

@ -2,6 +2,7 @@
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Services;
using Artemis.UI.Services.ProfileEditor;
using Artemis.UI.Shared;
namespace Artemis.UI.Screens.ProfileEditor.VisualEditor

View File

@ -6,6 +6,10 @@
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileEditorView">
<UserControl.KeyBindings>
<KeyBinding Command="{Binding History.Undo}" Gesture="Ctrl+Z"></KeyBinding>
<KeyBinding Command="{Binding History.Redo}" Gesture="Ctrl+Y"></KeyBinding>
</UserControl.KeyBindings>
<UserControl.Styles>
<Style Selector="GridSplitter.editor-grid-splitter-vertical">
<Setter Property="MinWidth" Value="4" />

View File

@ -4,17 +4,20 @@ using Artemis.Core;
using Artemis.UI.Screens.ProfileEditor.Panels.MenuBar;
using Artemis.UI.Screens.ProfileEditor.ProfileTree;
using Artemis.UI.Screens.ProfileEditor.VisualEditor;
using Artemis.UI.Services;
using Artemis.UI.Services.ProfileEditor;
using Ninject;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor
{
public class ProfileEditorViewModel : MainScreenViewModel
{
private ProfileConfiguration? _profile;
private ProfileConfiguration? _profileConfiguration;
private ProfileEditorHistory? _history;
/// <inheritdoc />
public ProfileEditorViewModel(IScreen hostScreen,
IKernel kernel,
IProfileEditorService profileEditorService,
VisualEditorViewModel visualEditorViewModel,
ProfileTreeViewModel profileTreeViewModel,
@ -30,17 +33,25 @@ namespace Artemis.UI.Screens.ProfileEditor
else
MenuBarViewModel = menuBarViewModel;
this.WhenActivated(disposables => profileEditorService.CurrentProfileConfiguration.WhereNotNull().Subscribe(p => Profile = p).DisposeWith(disposables));
this.WhenActivated(d => profileEditorService.ProfileConfiguration.WhereNotNull().Subscribe(p => ProfileConfiguration = p).DisposeWith(d));
this.WhenActivated(d => profileEditorService.History.Subscribe(history => History = history).DisposeWith(d));
}
public VisualEditorViewModel VisualEditorViewModel { get; }
public ProfileTreeViewModel ProfileTreeViewModel { get; }
public MenuBarViewModel? MenuBarViewModel { get; }
public ProfileConfiguration? Profile
public ProfileConfiguration? ProfileConfiguration
{
get => _profile;
set => this.RaiseAndSetIfChanged(ref _profile, value);
get => _profileConfiguration;
set => this.RaiseAndSetIfChanged(ref _profileConfiguration, value);
}
public ProfileEditorHistory? History
{
get => _history;
set => this.RaiseAndSetIfChanged(ref _history, value);
}
public void OpenUrl(string url)

View File

@ -7,6 +7,7 @@ using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Services;
using Artemis.UI.Services.ProfileEditor;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.Interfaces;
using FluentAvalonia.UI.Controls;
@ -35,7 +36,7 @@ namespace Artemis.UI.Screens.Sidebar
this.WhenActivated(disposables =>
{
profileEditorService.CurrentProfileConfiguration
profileEditorService.ProfileConfiguration
.Subscribe(p => SelectedProfileConfiguration = ProfileConfigurations.FirstOrDefault(c => ReferenceEquals(c.ProfileConfiguration, p)))
.DisposeWith(disposables);
this.WhenAnyValue(vm => vm.SelectedProfileConfiguration)

View File

@ -13,6 +13,7 @@ using Artemis.UI.Screens.Settings;
using Artemis.UI.Screens.SurfaceEditor;
using Artemis.UI.Screens.Workshop;
using Artemis.UI.Services;
using Artemis.UI.Services.ProfileEditor;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.Interfaces;
using FluentAvalonia.UI.Controls;
@ -73,7 +74,7 @@ namespace Artemis.UI.Screens.Sidebar
profileEditorService.ChangeCurrentProfileConfiguration(null);
});
this.WhenAnyObservable(vm => vm._profileEditorService.CurrentProfileConfiguration)
this.WhenAnyObservable(vm => vm._profileEditorService.ProfileConfiguration)
.WhereNotNull()
.Subscribe(_ =>
{

View File

@ -0,0 +1,23 @@
namespace Artemis.UI.Services.ProfileEditor
{
/// <summary>
/// Represents a command that can be executed and if needed, undone
/// </summary>
public interface IProfileEditorCommand
{
/// <summary>
/// Gets the name of the command
/// </summary>
string DisplayName { get; }
/// <summary>
/// Executes the command
/// </summary>
void Execute();
/// <summary>
/// Undoes the command
/// </summary>
void Undo();
}
}

View File

@ -0,0 +1,17 @@
using System;
using Artemis.Core;
using Artemis.UI.Services.Interfaces;
namespace Artemis.UI.Services.ProfileEditor
{
public interface IProfileEditorService : IArtemisUIService
{
IObservable<ProfileConfiguration?> ProfileConfiguration { get; }
IObservable<RenderProfileElement?> ProfileElement { get; }
IObservable<ProfileEditorHistory?> History { get; }
void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration);
void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement);
void ExecuteCommand(IProfileEditorCommand command);
}
}

View File

@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Artemis.Core;
using ReactiveUI;
namespace Artemis.UI.Services.ProfileEditor
{
public class ProfileEditorHistory
{
private readonly Subject<bool> _canRedo = new();
private readonly Subject<bool> _canUndo = new();
private readonly Stack<IProfileEditorCommand> _redoCommands = new();
private readonly Stack<IProfileEditorCommand> _undoCommands = new();
public ProfileEditorHistory(ProfileConfiguration profileConfiguration)
{
ProfileConfiguration = profileConfiguration;
Execute = ReactiveCommand.Create<IProfileEditorCommand>(ExecuteEditorCommand);
Undo = ReactiveCommand.Create(ExecuteUndo, CanUndo);
Redo = ReactiveCommand.Create(ExecuteRedo, CanRedo);
}
public ProfileConfiguration ProfileConfiguration { get; }
public IObservable<bool> CanUndo => _canUndo.AsObservable().DistinctUntilChanged();
public IObservable<bool> CanRedo => _canRedo.AsObservable().DistinctUntilChanged();
public ReactiveCommand<IProfileEditorCommand, Unit> Execute { get; }
public ReactiveCommand<Unit, Unit> Undo { get; }
public ReactiveCommand<Unit, Unit> Redo { get; }
public void Clear()
{
ClearRedo();
ClearUndo();
UpdateSubjects();
}
public void ExecuteEditorCommand(IProfileEditorCommand command)
{
command.Execute();
_undoCommands.Push(command);
ClearRedo();
UpdateSubjects();
}
private void ClearRedo()
{
foreach (IProfileEditorCommand profileEditorCommand in _redoCommands)
if (profileEditorCommand is IDisposable disposable)
disposable.Dispose();
_redoCommands.Clear();
}
private void ClearUndo()
{
foreach (IProfileEditorCommand profileEditorCommand in _undoCommands)
if (profileEditorCommand is IDisposable disposable)
disposable.Dispose();
_undoCommands.Clear();
}
private void ExecuteUndo()
{
if (!_undoCommands.TryPop(out IProfileEditorCommand? command))
return;
command.Undo();
_redoCommands.Push(command);
UpdateSubjects();
}
private void ExecuteRedo()
{
if (!_redoCommands.TryPop(out IProfileEditorCommand? command))
return;
command.Execute();
_undoCommands.Push(command);
UpdateSubjects();
}
private void UpdateSubjects()
{
_canUndo.OnNext(_undoCommands.Any());
_canRedo.OnNext(_redoCommands.Any());
}
}
}

View File

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using Artemis.Core;
using Artemis.UI.Exceptions;
namespace Artemis.UI.Services.ProfileEditor
{
public class ProfileEditorService : IProfileEditorService
{
private readonly Dictionary<ProfileConfiguration, ProfileEditorHistory> _profileEditorHistories = new();
private readonly BehaviorSubject<ProfileConfiguration?> _profileConfigurationSubject = new(null);
private readonly BehaviorSubject<RenderProfileElement?> _profileElementSubject = new(null);
public ProfileEditorService()
{
ProfileConfiguration = _profileConfigurationSubject.AsObservable().DistinctUntilChanged();
ProfileElement = _profileElementSubject.AsObservable().DistinctUntilChanged();
History = Observable.Defer(() => Observable.Return(GetHistory(_profileConfigurationSubject.Value))).Concat(ProfileConfiguration.Select(GetHistory));
}
private ProfileEditorHistory? GetHistory(ProfileConfiguration? profileConfiguration)
{
if (profileConfiguration == null)
return null;
if (_profileEditorHistories.TryGetValue(profileConfiguration, out ProfileEditorHistory? history))
return history;
ProfileEditorHistory newHistory = new(profileConfiguration);
_profileEditorHistories.Add(profileConfiguration, newHistory);
return newHistory;
}
public IObservable<ProfileConfiguration?> ProfileConfiguration { get; }
public IObservable<RenderProfileElement?> ProfileElement { get; }
public IObservable<ProfileEditorHistory?> History { get; }
public void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration)
{
_profileConfigurationSubject.OnNext(profileConfiguration);
}
public void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement)
{
_profileElementSubject.OnNext(renderProfileElement);
}
public void ExecuteCommand(IProfileEditorCommand command)
{
ProfileEditorHistory? history = GetHistory(_profileConfigurationSubject.Value);
if (history == null)
throw new ArtemisUIException("Can't execute a command when there's no active profile configuration");
history.Execute.Execute(command).Subscribe();
}
}
}

View File

@ -1,50 +0,0 @@
using System;
using System.Diagnostics;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Services.Interfaces;
using ReactiveUI;
namespace Artemis.UI.Services
{
public class ProfileEditorService : IProfileEditorService
{
private readonly ReactiveCommand<ProfileConfiguration?, ProfileConfiguration?> _changeCurrentProfileConfiguration;
private readonly ReactiveCommand<RenderProfileElement?, RenderProfileElement?> _changeCurrentProfileElement;
private ProfileConfiguration? _currentProfileConfiguration;
private RenderProfileElement? _currentProfileElement;
public ProfileEditorService()
{
_changeCurrentProfileConfiguration = ReactiveCommand.CreateFromObservable<ProfileConfiguration?, ProfileConfiguration?>(Observable.Return);
_changeCurrentProfileElement = ReactiveCommand.CreateFromObservable<RenderProfileElement?, RenderProfileElement?>(Observable.Return);
CurrentProfileConfiguration = Observable.Defer(() => Observable.Return(_currentProfileConfiguration)).Concat(_changeCurrentProfileConfiguration);
CurrentProfileElement = Observable.Defer(() => Observable.Return(_currentProfileElement)).Concat(_changeCurrentProfileElement);
}
public IObservable<ProfileConfiguration?> CurrentProfileConfiguration { get; }
public IObservable<RenderProfileElement?> CurrentProfileElement { get; }
public void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration)
{
_currentProfileConfiguration = profileConfiguration;
_changeCurrentProfileConfiguration.Execute(profileConfiguration).Subscribe();
}
public void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement)
{
_currentProfileElement = renderProfileElement;
_changeCurrentProfileElement.Execute(renderProfileElement).Subscribe();
}
}
public interface IProfileEditorService : IArtemisUIService
{
IObservable<ProfileConfiguration?> CurrentProfileConfiguration { get; }
IObservable<RenderProfileElement?> CurrentProfileElement { get; }
void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration);
void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement);
}
}

View File

@ -117,16 +117,12 @@
},
"ReactiveUI": {
"type": "Direct",
"requested": "[17.1.9, )",
"resolved": "17.1.9",
"contentHash": "bxH6uzEi1b6cfGoBBvCXWrdT18+Rleggi0R/vOaCYc+zvlSleJW6wlUtcWjrKzgQHD8EN3c02A4JBTt9oGSWWQ==",
"requested": "[16.3.10, )",
"resolved": "16.3.10",
"contentHash": "NH9bg8BROqRrTp6YLpPDsJrfNDzRWNmP63fQ68CBAM+i7YHi6wcPeOkxyKpoemUxKEY4QECuicaTblJnxgbWmA==",
"dependencies": {
"DynamicData": "7.4.3",
"Splat": "14.1.1",
"System.ComponentModel": "4.3.0",
"System.Diagnostics.Contracts": "4.3.0",
"System.Dynamic.Runtime": "4.3.0",
"System.Runtime.Serialization.Primitives": "4.3.0"
"Splat": "13.1.42"
}
},
"ReactiveUI.Validation": {
@ -933,14 +929,6 @@
"System.Text.Encoding": "4.3.0"
}
},
"System.Diagnostics.Contracts": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "eelRRbnm+OloiQvp9CXS0ixjNQldjjkHO4iIkR5XH2VIP8sUB/SIpa1TdUW6/+HDcQ+MlhP3pNa1u5SbzYuWGA==",
"dependencies": {
"System.Runtime": "4.3.0"
}
},
"System.Diagnostics.Debug": {
"type": "Transitive",
"resolved": "4.3.0",
@ -1421,15 +1409,6 @@
"System.Runtime.Extensions": "4.3.0"
}
},
"System.Runtime.Serialization.Primitives": {
"type": "Transitive",
"resolved": "4.3.0",
"contentHash": "Wz+0KOukJGAlXjtKr+5Xpuxf8+c8739RI1C+A2BoQZT+wMCCoMDDdO8/4IRHfaVINqL78GO8dW8G2lW/e45Mcw==",
"dependencies": {
"System.Resources.ResourceManager": "4.3.0",
"System.Runtime": "4.3.0"
}
},
"System.Security.AccessControl": {
"type": "Transitive",
"resolved": "5.0.0",
@ -1785,7 +1764,7 @@
"FluentAvaloniaUI": "1.1.8",
"Material.Icons.Avalonia": "1.0.2",
"RGB.NET.Core": "1.0.0-prerelease1",
"ReactiveUI": "17.1.9",
"ReactiveUI": "16.3.10",
"ReactiveUI.Validation": "2.2.1"
}
}