Profile tree - Implemented proper copy/pasting

This commit is contained in:
Robert committed 2020-12-01 19:08:10 +01:00
1 parent f694f39219
commit aae4e71d8e
21 files changed
+524 -294

No files matched your search

@@ -92,46 +92,6 @@ namespace Artemis.UI.Shared.Services
/// <returns>The current module the profile editor is initialized for</returns>
ProfileModule? GetCurrentModule();
/// <summary>
/// Occurs when a new profile is selected
/// </summary>
event EventHandler<ProfileEventArgs> ProfileSelected;
/// <summary>
/// Occurs then the currently selected profile is updated
/// </summary>
event EventHandler<ProfileEventArgs> SelectedProfileUpdated;
/// <summary>
/// Occurs when a new profile element is selected
/// </summary>
event EventHandler<RenderProfileElementEventArgs> ProfileElementSelected;
/// <summary>
/// Occurs when the currently selected profile element is updated
/// </summary>
event EventHandler<RenderProfileElementEventArgs> SelectedProfileElementUpdated;
/// <summary>
/// Occurs when the currently selected data binding layer property is changed
/// </summary>
event EventHandler SelectedDataBindingChanged;
/// <summary>
/// Occurs when the current editor time is changed
/// </summary>
event EventHandler CurrentTimeChanged;
/// <summary>
/// Occurs when the pixels per second (zoom level) is changed
/// </summary>
event EventHandler PixelsPerSecondChanged;
/// <summary>
/// Occurs when the profile preview has been updated
/// </summary>
event EventHandler ProfilePreviewUpdated;
/// <summary>
/// Registers a new property input view model used in the profile editor for the generic type defined in
/// <see cref="PropertyInputViewModel{T}" />
@@ -174,5 +134,66 @@ namespace Artemis.UI.Shared.Services
/// <typeparamref name="T" />
/// </summary>
PropertyInputViewModel<T>? CreatePropertyInputViewModel<T>(LayerProperty<T> layerProperty);
/// <summary>
/// Duplicates the provided profile element placing it in the same folder and one position higher
/// </summary>
/// <param name="profileElement">The profile element to duplicate</param>
/// <returns>The duplicated profile element</returns>
ProfileElement? DuplicateProfileElement(ProfileElement profileElement);
/// <summary>
/// Copies the provided profile element onto the clipboard
/// </summary>
/// <param name="profileElement">The profile element to copy</param>
void CopyProfileElement(ProfileElement profileElement);
/// <summary>
/// Pastes a render profile element from the clipboard into the target folder
/// </summary>
/// <param name="target">The folder to paste the render element in to</param>
/// <param name="position">The position at which to paste the element</param>
/// <returns>The pasted render element</returns>
ProfileElement? PasteProfileElement(Folder target, int position);
/// <summary>
/// Occurs when a new profile is selected
/// </summary>
event EventHandler<ProfileEventArgs> ProfileSelected;
/// <summary>
/// Occurs then the currently selected profile is updated
/// </summary>
event EventHandler<ProfileEventArgs> SelectedProfileUpdated;
/// <summary>
/// Occurs when a new profile element is selected
/// </summary>
event EventHandler<RenderProfileElementEventArgs> ProfileElementSelected;
/// <summary>
/// Occurs when the currently selected profile element is updated
/// </summary>
event EventHandler<RenderProfileElementEventArgs> SelectedProfileElementUpdated;
/// <summary>
/// Occurs when the currently selected data binding layer property is changed
/// </summary>
event EventHandler SelectedDataBindingChanged;
/// <summary>
/// Occurs when the current editor time is changed
/// </summary>
event EventHandler CurrentTimeChanged;
/// <summary>
/// Occurs when the pixels per second (zoom level) is changed
/// </summary>
event EventHandler PixelsPerSecondChanged;
/// <summary>
/// Occurs when the profile preview has been updated
/// </summary>
event EventHandler ProfilePreviewUpdated;
}
}
@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using Artemis.Core;
using Artemis.Storage.Entities.Profile;
using Artemis.Storage.Entities.Profile.Abstract;
namespace Artemis.UI.Shared.Services.Models
{
internal class FolderClipboardModel
{
public FolderClipboardModel(Folder folder)
{
FolderEntity = folder.FolderEntity;
Folders = new List<FolderEntity>();
Layers = new List<LayerEntity>();
foreach (Folder allFolder in folder.GetAllFolders())
Folders.Add(allFolder.FolderEntity);
foreach (Layer allLayer in folder.GetAllLayers())
Layers.Add(allLayer.LayerEntity);
}
// ReSharper disable once UnusedMember.Global - For JSON.NET
public FolderClipboardModel()
{
FolderEntity = null;
Folders = new List<FolderEntity>();
Layers = new List<LayerEntity>();
}
public FolderEntity? FolderEntity { get; set; }
public List<FolderEntity> Folders { get; set; }
public List<LayerEntity> Layers { get; set; }
public bool HasBeenPasted { get; set; }
public Folder Paste(Profile profile, ProfileElement parent)
{
if (FolderEntity == null)
throw new ArtemisSharedUIException("Couldn't paste folder because FolderEntity deserialized as null");
if (HasBeenPasted)
throw new ArtemisSharedUIException("Clipboard model can only be pasted once");
HasBeenPasted = true;
// Generate new GUIDs
ReplaceGuid(FolderEntity);
foreach (FolderEntity folderEntity in Folders)
ReplaceGuid(folderEntity);
foreach (LayerEntity layerEntity in Layers)
ReplaceGuid(layerEntity);
// Inject the pasted elements into the profile
profile.ProfileEntity.Folders.AddRange(Folders);
profile.ProfileEntity.Layers.AddRange(Layers);
// Let the folder initialize and load as usual
FolderEntity.Name += " - copy";
Folder folder = new Folder(profile, parent, FolderEntity);
return folder;
}
private void ReplaceGuid(RenderElementEntity parent)
{
Guid old = parent.Id;
parent.Id = Guid.NewGuid();
foreach (FolderEntity child in Folders)
{
if (child.ParentId == old)
child.ParentId = parent.Id;
}
foreach (LayerEntity child in Layers)
{
if (child.ParentId == old)
child.ParentId = parent.Id;
}
}
}
}
@@ -5,6 +5,9 @@ using System.Linq;
using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.Core.Services;
using Artemis.Storage.Entities.Profile;
using Artemis.UI.Shared.Services.Models;
using Newtonsoft.Json;
using Ninject;
using Ninject.Parameters;
using Serilog;
@@ -14,23 +17,25 @@ namespace Artemis.UI.Shared.Services
{
internal class ProfileEditorService : IProfileEditorService
{
private readonly ILogger _logger;
private readonly ICoreService _coreService;
private readonly ISurfaceService _surfaceService;
private readonly IKernel _kernel;
private readonly ILogger _logger;
private readonly IProfileService _profileService;
private readonly List<PropertyInputRegistration> _registeredPropertyEditors;
private readonly object _selectedProfileElementLock = new object();
private readonly object _selectedProfileLock = new object();
private TimeSpan _currentTime;
private int _pixelsPerSecond;
private readonly IKernel _kernel;
public ProfileEditorService(IProfileService profileService, IKernel kernel, ILogger logger, ICoreService coreService)
public ProfileEditorService(IKernel kernel, ILogger logger, IProfileService profileService, ICoreService coreService, ISurfaceService surfaceService)
{
_profileService = profileService;
_logger = logger;
_coreService = coreService;
_registeredPropertyEditors = new List<PropertyInputRegistration>();
_kernel = kernel;
_logger = logger;
_profileService = profileService;
_coreService = coreService;
_surfaceService = surfaceService;
_registeredPropertyEditors = new List<PropertyInputRegistration>();
PixelsPerSecond = 100;
}
@@ -41,6 +46,30 @@ namespace Artemis.UI.Shared.Services
Execute.PostToUIThread(OnProfilePreviewUpdated);
}
private void ReloadProfile()
{
if (SelectedProfile == null)
return;
// Trigger a profile change
OnSelectedProfileChanged(new ProfileEventArgs(SelectedProfile, SelectedProfile));
// Trigger a selected element change
RenderProfileElement? previousSelectedProfileElement = SelectedProfileElement;
if (SelectedProfileElement is Folder folder)
SelectedProfileElement = SelectedProfile.GetAllFolders().FirstOrDefault(f => f.EntityId == folder.EntityId);
else if (SelectedProfileElement is Layer layer)
SelectedProfileElement = SelectedProfile.GetAllLayers().FirstOrDefault(l => l.EntityId == layer.EntityId);
OnSelectedProfileElementChanged(new RenderProfileElementEventArgs(SelectedProfileElement, previousSelectedProfileElement));
// Trigger selected data binding change
if (SelectedDataBinding != null)
{
SelectedDataBinding = SelectedProfileElement?.GetAllLayerProperties().FirstOrDefault(p => p.Path == SelectedDataBinding.Path);
OnSelectedDataBindingChanged();
}
UpdateProfilePreview();
}
public ReadOnlyCollection<PropertyInputRegistration> RegisteredPropertyEditors => _registeredPropertyEditors.AsReadOnly();
public Profile? SelectedProfile { get; private set; }
public RenderProfileElement? SelectedProfileElement { get; private set; }
@@ -107,7 +136,7 @@ namespace Artemis.UI.Shared.Services
return;
_profileService.UpdateProfile(SelectedProfile, true);
OnSelectedProfileChanged(new ProfileEventArgs(SelectedProfile));
OnSelectedProfileUpdated(new ProfileEventArgs(SelectedProfile));
UpdateProfilePreview();
}
}
@@ -206,7 +235,7 @@ namespace Artemis.UI.Shared.Services
if (supportedType.IsGenericParameter)
{
if (supportedType.BaseType == null)
throw new ArtemisSharedUIException($"Generic property input VM type must have a type constraint");
throw new ArtemisSharedUIException("Generic property input VM type must have a type constraint");
supportedType = supportedType.BaseType;
}
@@ -258,11 +287,9 @@ namespace Artemis.UI.Shared.Services
}
if (snapToCurrentTime)
{
// Snap to the current time
if (Math.Abs(time.TotalMilliseconds - CurrentTime.TotalMilliseconds) < tolerance.TotalMilliseconds)
return CurrentTime;
}
if (snapTimes != null)
{
@@ -289,9 +316,13 @@ namespace Artemis.UI.Shared.Services
viewModelType = registration.ViewModelType.MakeGenericType(layerProperty.GetType().GenericTypeArguments);
}
else if (registration != null)
{
viewModelType = registration.ViewModelType;
}
else
{
return null;
}
if (viewModelType == null)
return null;
@@ -308,30 +339,80 @@ namespace Artemis.UI.Shared.Services
return SelectedProfile?.Module;
}
private void ReloadProfile()
{
if (SelectedProfile == null)
return;
#region Copy/paste
// Trigger a profile change
OnSelectedProfileChanged(new ProfileEventArgs(SelectedProfile, SelectedProfile));
// Trigger a selected element change
RenderProfileElement? previousSelectedProfileElement = SelectedProfileElement;
if (SelectedProfileElement is Folder folder)
SelectedProfileElement = SelectedProfile.GetAllFolders().FirstOrDefault(f => f.EntityId == folder.EntityId);
else if (SelectedProfileElement is Layer layer)
SelectedProfileElement = SelectedProfile.GetAllLayers().FirstOrDefault(l => l.EntityId == layer.EntityId);
OnSelectedProfileElementChanged(new RenderProfileElementEventArgs(SelectedProfileElement, previousSelectedProfileElement));
// Trigger selected data binding change
if (SelectedDataBinding != null)
public ProfileElement? DuplicateProfileElement(ProfileElement profileElement)
{
if (!(profileElement.Parent is Folder parent))
return null;
object? clipboardModel = null;
switch (profileElement)
{
SelectedDataBinding = SelectedProfileElement?.GetAllLayerProperties().FirstOrDefault(p => p.Path == SelectedDataBinding.Path);
OnSelectedDataBindingChanged();
case Folder folder:
{
clipboardModel = CoreJson.DeserializeObject(CoreJson.SerializeObject(new FolderClipboardModel(folder), true), true);
break;
}
case Layer layer:
clipboardModel = CoreJson.DeserializeObject(CoreJson.SerializeObject(layer.LayerEntity, true), true);
break;
}
UpdateProfilePreview();
return clipboardModel == null ? null : PasteClipboardData(clipboardModel, parent, profileElement.Order);
}
public void CopyProfileElement(ProfileElement profileElement)
{
switch (profileElement)
{
case Folder folder:
{
FolderClipboardModel clipboardModel = new FolderClipboardModel(folder);
JsonClipboard.SetObject(clipboardModel);
break;
}
case Layer layer:
JsonClipboard.SetObject(layer.LayerEntity);
break;
}
}
public ProfileElement? PasteProfileElement(Folder target, int position)
{
object? clipboardObject = JsonClipboard.GetData();
return clipboardObject != null ? PasteClipboardData(clipboardObject, target, position) : null;
}
private RenderProfileElement? PasteClipboardData(object clipboardObject, Folder target, int position)
{
RenderProfileElement? pasted = null;
switch (clipboardObject)
{
case FolderClipboardModel folderClipboardModel:
pasted = folderClipboardModel.Paste(target.Profile, target);
target.AddChild(pasted, position);
break;
case LayerEntity layerEntity:
layerEntity.Id = Guid.NewGuid();
layerEntity.Name += " - copy";
pasted = new Layer(target.Profile, target, layerEntity);
target.AddChild(pasted, position);
break;
}
if (pasted != null)
{
target.Profile.PopulateLeds(_surfaceService.ActiveSurface);
UpdateSelectedProfile();
ChangeSelectedProfileElement(pasted);
}
return pasted;
}
#endregion
#region Events
public event EventHandler<ProfileEventArgs>? ProfileSelected;
@@ -396,6 +477,5 @@ namespace Artemis.UI.Shared.Services
}
#endregion
}
}
@@ -0,0 +1,58 @@
using System.Diagnostics.CodeAnalysis;
using System.Windows;
using Newtonsoft.Json;
namespace Artemis.UI.Shared
{
/// <summary>
/// Provides access to the clipboard via JSON-serialized objects
/// </summary>
public static class JsonClipboard
{
private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.All};
/// <summary>
/// Sets the provided object on the clipboard
/// </summary>
/// <param name="clipboardObject">The object to set on the clipboard</param>
public static void SetObject(object clipboardObject)
{
string json = JsonConvert.SerializeObject(clipboardObject, JsonSettings);
Clipboard.SetData("Artemis", json);
}
/// <summary>
/// If set, gets the current object off of the clipboard
/// </summary>
/// <returns>The object that is on the clipboard, if none is set <see langword="null" />.</returns>
public static object? GetData()
{
string? json = Clipboard.GetData("Artemis")?.ToString();
return json != null ? JsonConvert.DeserializeObject(json, JsonSettings) : null;
}
/// <summary>
/// If set, gets the current object of type <typeparamref name="T" /> off of the clipboard
/// </summary>
/// <typeparam name="T">The type of object to get</typeparam>
/// <returns>
/// The object that is on the clipboard. If none is set or not of type <typeparamref name="T" />,
/// <see langword="null" />.
/// </returns>
[return: MaybeNull]
public static T GetData<T>()
{
object? data = GetData();
return data is T castData ? castData : default;
}
/// <summary>
/// Determines whether the clipboard currently contains Artemis data
/// </summary>
/// <returns><see langword="true" /> if the clipboard contains Artemis data, otherwise <see langword="false" /></returns>
public static bool ContainsArtemisData()
{
return Clipboard.ContainsData("Artemis");
}
}
}