diff --git a/src/Artemis.Core/Models/Profile/Folder.cs b/src/Artemis.Core/Models/Profile/Folder.cs index 6f4fd1d1c..6b30eb6c5 100644 --- a/src/Artemis.Core/Models/Profile/Folder.cs +++ b/src/Artemis.Core/Models/Profile/Folder.cs @@ -33,7 +33,13 @@ namespace Artemis.Core Parent.AddChild(this); } - internal Folder(Profile profile, ProfileElement parent, FolderEntity folderEntity) : base(parent.Profile) + /// + /// Creates a new instance of the class based on the provided folder entity + /// + /// The profile the folder belongs to + /// The parent of the folder + /// The entity of the folder + public Folder(Profile profile, ProfileElement parent, FolderEntity folderEntity) : base(parent.Profile) { FolderEntity = folderEntity; EntityId = folderEntity.Id; diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 51da2b65f..6c82080fc 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -122,29 +122,6 @@ namespace Artemis.Core internal override RenderElementEntity RenderElementEntity => LayerEntity; - /// - /// Creates a deep copy of the layer - /// - /// The newly created copy - public Layer CreateCopy() - { - if (Parent == null) - throw new ArtemisCoreException("Cannot create a copy of a layer without a parent"); - - LayerEntity entityCopy = CoreJson.DeserializeObject(CoreJson.SerializeObject(LayerEntity, true), true)!; - entityCopy.Id = Guid.NewGuid(); - entityCopy.Name += " - Copy"; - - Layer copy = new Layer(Profile, Parent, entityCopy); - if (LayerBrush?.Descriptor != null) - copy.ChangeLayerBrush(LayerBrush.Descriptor); - copy.AddLeds(Leds); - - Parent.AddChild(copy, Order + 1); - - return copy; - } - /// public override List GetAllLayerProperties() { @@ -399,7 +376,14 @@ namespace Artemis.Core } finally { - canvas.Restore(); + try + { + canvas.Restore(); + } + catch + { + // ignored + } Renderer.Close(); } } diff --git a/src/Artemis.Core/Models/Profile/Profile.cs b/src/Artemis.Core/Models/Profile/Profile.cs index a8e21ce76..51c47682a 100644 --- a/src/Artemis.Core/Models/Profile/Profile.cs +++ b/src/Artemis.Core/Models/Profile/Profile.cs @@ -12,8 +12,8 @@ namespace Artemis.Core /// public sealed class Profile : ProfileElement { - private bool _isActivated; private readonly object _lock = new object(); + private bool _isActivated; internal Profile(ProfileModule module, string name) : base(null!) { @@ -57,7 +57,10 @@ namespace Artemis.Core private set => SetAndNotify(ref _isActivated, value); } - internal ProfileEntity ProfileEntity { get; set; } + /// + /// Gets the profile entity this profile uses for persistent storage + /// + public ProfileEntity ProfileEntity { get; internal set; } internal Stack UndoStack { get; set; } internal Stack RedoStack { get; set; } @@ -118,6 +121,19 @@ namespace Artemis.Core return $"[Profile] {nameof(Name)}: {Name}, {nameof(IsActivated)}: {IsActivated}, {nameof(Module)}: {Module}"; } + /// + /// Populates all the LEDs on the elements in this profile + /// + /// The currently active surface that contains the LEDs + public void PopulateLeds(ArtemisSurface surface) + { + if (Disposed) + throw new ObjectDisposedException("Profile"); + + foreach (Layer layer in GetAllLayers()) + layer.PopulateLeds(surface); + } + /// protected override void Dispose(bool disposing) { @@ -196,15 +212,6 @@ namespace Artemis.Core } } - internal void PopulateLeds(ArtemisSurface surface) - { - if (Disposed) - throw new ObjectDisposedException("Profile"); - - foreach (Layer layer in GetAllLayers()) - layer.PopulateLeds(surface); - } - #region Events /// diff --git a/src/Artemis.Core/Utilities/CoreJson.cs b/src/Artemis.Core/Utilities/CoreJson.cs index b4e0ff0db..c4a79588d 100644 --- a/src/Artemis.Core/Utilities/CoreJson.cs +++ b/src/Artemis.Core/Utilities/CoreJson.cs @@ -5,7 +5,10 @@ using Newtonsoft.Json; namespace Artemis.Core { - internal static class CoreJson + /// + /// A static helper class that serializes and deserializes JSON with the Artemis Core JSON settings + /// + public static class CoreJson { #region Serialize diff --git a/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs b/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs index d8f7ded16..8e9cd1db2 100644 --- a/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/Abstract/RenderElementEntity.cs @@ -6,6 +6,9 @@ namespace Artemis.Storage.Entities.Profile.Abstract { public abstract class RenderElementEntity { + public Guid Id { get; set; } + public Guid ParentId { get; set; } + public List LayerEffects { get; set; } public List PropertyEntities { get; set; } public List ExpandedPropertyGroups { get; set; } diff --git a/src/Artemis.Storage/Entities/Profile/FolderEntity.cs b/src/Artemis.Storage/Entities/Profile/FolderEntity.cs index 57614e6e9..7060ee6e0 100644 --- a/src/Artemis.Storage/Entities/Profile/FolderEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/FolderEntity.cs @@ -14,9 +14,6 @@ namespace Artemis.Storage.Entities.Profile ExpandedPropertyGroups = new List(); } - public Guid Id { get; set; } - public Guid ParentId { get; set; } - public int Order { get; set; } public string Name { get; set; } public bool Enabled { get; set; } diff --git a/src/Artemis.Storage/Entities/Profile/LayerEntity.cs b/src/Artemis.Storage/Entities/Profile/LayerEntity.cs index 9b8f447e4..60dd2071c 100644 --- a/src/Artemis.Storage/Entities/Profile/LayerEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/LayerEntity.cs @@ -15,9 +15,6 @@ namespace Artemis.Storage.Entities.Profile ExpandedPropertyGroups = new List(); } - public Guid Id { get; set; } - public Guid ParentId { get; set; } - public int Order { get; set; } public string Name { get; set; } public bool Enabled { get; set; } diff --git a/src/Artemis.UI.Shared/Services/Interfaces/IProfileEditorService.cs b/src/Artemis.UI.Shared/Services/Interfaces/IProfileEditorService.cs index fda32c11f..e279f7df1 100644 --- a/src/Artemis.UI.Shared/Services/Interfaces/IProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/Interfaces/IProfileEditorService.cs @@ -92,46 +92,6 @@ namespace Artemis.UI.Shared.Services /// The current module the profile editor is initialized for ProfileModule? GetCurrentModule(); - /// - /// Occurs when a new profile is selected - /// - event EventHandler ProfileSelected; - - /// - /// Occurs then the currently selected profile is updated - /// - event EventHandler SelectedProfileUpdated; - - /// - /// Occurs when a new profile element is selected - /// - event EventHandler ProfileElementSelected; - - /// - /// Occurs when the currently selected profile element is updated - /// - event EventHandler SelectedProfileElementUpdated; - - /// - /// Occurs when the currently selected data binding layer property is changed - /// - event EventHandler SelectedDataBindingChanged; - - /// - /// Occurs when the current editor time is changed - /// - event EventHandler CurrentTimeChanged; - - /// - /// Occurs when the pixels per second (zoom level) is changed - /// - event EventHandler PixelsPerSecondChanged; - - /// - /// Occurs when the profile preview has been updated - /// - event EventHandler ProfilePreviewUpdated; - /// /// Registers a new property input view model used in the profile editor for the generic type defined in /// @@ -174,5 +134,66 @@ namespace Artemis.UI.Shared.Services /// /// PropertyInputViewModel? CreatePropertyInputViewModel(LayerProperty layerProperty); + + /// + /// Duplicates the provided profile element placing it in the same folder and one position higher + /// + /// The profile element to duplicate + /// The duplicated profile element + ProfileElement? DuplicateProfileElement(ProfileElement profileElement); + + /// + /// Copies the provided profile element onto the clipboard + /// + /// The profile element to copy + void CopyProfileElement(ProfileElement profileElement); + + /// + /// Pastes a render profile element from the clipboard into the target folder + /// + /// The folder to paste the render element in to + /// The position at which to paste the element + /// The pasted render element + ProfileElement? PasteProfileElement(Folder target, int position); + + /// + /// Occurs when a new profile is selected + /// + event EventHandler ProfileSelected; + + /// + /// Occurs then the currently selected profile is updated + /// + event EventHandler SelectedProfileUpdated; + + /// + /// Occurs when a new profile element is selected + /// + event EventHandler ProfileElementSelected; + + /// + /// Occurs when the currently selected profile element is updated + /// + event EventHandler SelectedProfileElementUpdated; + + /// + /// Occurs when the currently selected data binding layer property is changed + /// + event EventHandler SelectedDataBindingChanged; + + /// + /// Occurs when the current editor time is changed + /// + event EventHandler CurrentTimeChanged; + + /// + /// Occurs when the pixels per second (zoom level) is changed + /// + event EventHandler PixelsPerSecondChanged; + + /// + /// Occurs when the profile preview has been updated + /// + event EventHandler ProfilePreviewUpdated; } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/Models/FolderClipboardModel.cs b/src/Artemis.UI.Shared/Services/Models/FolderClipboardModel.cs new file mode 100644 index 000000000..9511e5299 --- /dev/null +++ b/src/Artemis.UI.Shared/Services/Models/FolderClipboardModel.cs @@ -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(); + Layers = new List(); + 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(); + Layers = new List(); + } + + public FolderEntity? FolderEntity { get; set; } + public List Folders { get; set; } + public List 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; + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs index d7fb126d0..493a9e287 100644 --- a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs @@ -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 _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(); _kernel = kernel; + _logger = logger; + _profileService = profileService; + _coreService = coreService; + _surfaceService = surfaceService; + _registeredPropertyEditors = new List(); 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 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? ProfileSelected; @@ -396,6 +477,5 @@ namespace Artemis.UI.Shared.Services } #endregion - } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Utilities/JsonClipboard.cs b/src/Artemis.UI.Shared/Utilities/JsonClipboard.cs new file mode 100644 index 000000000..20276bcc2 --- /dev/null +++ b/src/Artemis.UI.Shared/Utilities/JsonClipboard.cs @@ -0,0 +1,58 @@ +using System.Diagnostics.CodeAnalysis; +using System.Windows; +using Newtonsoft.Json; + +namespace Artemis.UI.Shared +{ + /// + /// Provides access to the clipboard via JSON-serialized objects + /// + public static class JsonClipboard + { + private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.All}; + + /// + /// Sets the provided object on the clipboard + /// + /// The object to set on the clipboard + public static void SetObject(object clipboardObject) + { + string json = JsonConvert.SerializeObject(clipboardObject, JsonSettings); + Clipboard.SetData("Artemis", json); + } + + /// + /// If set, gets the current object off of the clipboard + /// + /// The object that is on the clipboard, if none is set . + public static object? GetData() + { + string? json = Clipboard.GetData("Artemis")?.ToString(); + return json != null ? JsonConvert.DeserializeObject(json, JsonSettings) : null; + } + + /// + /// If set, gets the current object of type off of the clipboard + /// + /// The type of object to get + /// + /// The object that is on the clipboard. If none is set or not of type , + /// . + /// + [return: MaybeNull] + public static T GetData() + { + object? data = GetData(); + return data is T castData ? castData : default; + } + + /// + /// Determines whether the clipboard currently contains Artemis data + /// + /// if the clipboard contains Artemis data, otherwise + public static bool ContainsArtemisData() + { + return Clipboard.ContainsData("Artemis"); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Behaviors/TreeViewSelectionBehavior.cs b/src/Artemis.UI/Behaviors/TreeViewSelectionBehavior.cs index 11de9ea70..b01c00e4b 100644 --- a/src/Artemis.UI/Behaviors/TreeViewSelectionBehavior.cs +++ b/src/Artemis.UI/Behaviors/TreeViewSelectionBehavior.cs @@ -104,9 +104,16 @@ namespace Artemis.UI.Behaviors if (SelectedItem == model && !item.IsSelected) { item.IsSelected = true; + // Focus the newly selected item as if was clicked + item.Focus(); if (ExpandSelected) item.IsExpanded = true; } + else if (SelectedItem == model && !item.IsFocused) + { + // Focus the newly selected item as if was clicked + item.Focus(); + } // If the selected item is a parent of this model - expand else { @@ -117,14 +124,12 @@ namespace Artemis.UI.Behaviors // Recurse into children if (recurse) - { foreach (object subitem in item.Items) { TreeViewItem tvi = item.ItemContainerGenerator.ContainerFromItem(subitem) as TreeViewItem; if (tvi != null) UpdateTreeViewItem(tvi, true); } - } } // Update state of all items @@ -143,11 +148,9 @@ namespace Artemis.UI.Behaviors private void UpdateTreeViewItemStyle() { if (AssociatedObject.ItemContainerStyle == null) - { AssociatedObject.ItemContainerStyle = new Style( typeof(TreeViewItem), Application.Current.TryFindResource(typeof(TreeViewItem)) as Style); - } if (!AssociatedObject.ItemContainerStyle.Setters.Contains(_treeViewItemEventSetter)) AssociatedObject.ItemContainerStyle.Setters.Add(_treeViewItemEventSetter); diff --git a/src/Artemis.UI/Extensions/ScreenExtensions.cs b/src/Artemis.UI/Extensions/ScreenExtensions.cs new file mode 100644 index 000000000..dd9277b30 --- /dev/null +++ b/src/Artemis.UI/Extensions/ScreenExtensions.cs @@ -0,0 +1,20 @@ +using Stylet; + +namespace Artemis.UI.Extensions +{ + public static class ScreenExtensions + { + public static T FindScreenOfType(this Screen screen) where T : Screen + { + Screen parent = screen.Parent as Screen; + while (parent != null) + { + if (parent is T match) + return match; + parent = parent.Parent as Screen; + } + + return default; + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/ProfileTreeView.xaml b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/ProfileTreeView.xaml index 33414f284..f3f8ea165 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/ProfileTreeView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/ProfileTreeView.xaml @@ -34,10 +34,11 @@ dd:DragDrop.IsDropTarget="True" dd:DragDrop.DropHandler="{Binding}"> - - - - + + + + + diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/ProfileTreeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/ProfileTreeViewModel.cs index 72db66f48..4e65def4a 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/ProfileTreeViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/ProfileTreeViewModel.cs @@ -13,8 +13,8 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree { public class ProfileTreeViewModel : Conductor, IProfileEditorPanelViewModel, IDropTarget { - private readonly IProfileTreeVmFactory _profileTreeVmFactory; private readonly IProfileEditorService _profileEditorService; + private readonly IProfileTreeVmFactory _profileTreeVmFactory; private TreeItemViewModel _selectedTreeItem; private bool _updatingTree; @@ -41,6 +41,73 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree } } + // ReSharper disable once UnusedMember.Global - Called from view + public void AddFolder() + { + ActiveItem?.AddFolder(); + } + + // ReSharper disable once UnusedMember.Global - Called from view + public void AddLayer() + { + ActiveItem?.AddLayer(); + } + + protected override void OnInitialActivate() + { + Subscribe(); + base.OnInitialActivate(); + } + + protected override void OnClose() + { + Unsubscribe(); + base.OnClose(); + } + + private void CreateRootFolderViewModel() + { + _updatingTree = true; + ProfileElement firstChild = _profileEditorService.SelectedProfile?.Children?.FirstOrDefault(); + if (!(firstChild is Folder folder)) + { + ActivateItem(null); + return; + } + + ActivateItem(_profileTreeVmFactory.FolderViewModel(folder)); + _updatingTree = false; + } + + private static DragDropType GetDragDropType(IDropInfo dropInfo) + { + TreeItemViewModel source = (TreeItemViewModel) dropInfo.Data; + TreeItemViewModel target = (TreeItemViewModel) dropInfo.TargetItem; + if (source == target) + return DragDropType.None; + + TreeItemViewModel parent = target; + while (parent != null) + { + if (parent == source) + return DragDropType.None; + parent = parent.Parent as TreeItemViewModel; + } + + switch (dropInfo.InsertPosition) + { + case RelativeInsertPosition.AfterTargetItem | RelativeInsertPosition.TargetItemCenter: + case RelativeInsertPosition.BeforeTargetItem | RelativeInsertPosition.TargetItemCenter: + return target.SupportsChildren ? DragDropType.Add : DragDropType.None; + case RelativeInsertPosition.BeforeTargetItem: + return DragDropType.InsertBefore; + case RelativeInsertPosition.AfterTargetItem: + return DragDropType.InsertAfter; + default: + return DragDropType.None; + } + } + public void DragOver(IDropInfo dropInfo) { DragDropType dragDropType = GetDragDropType(dropInfo); @@ -84,80 +151,6 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree Subscribe(); } - // ReSharper disable once UnusedMember.Global - Called from view - public void AddFolder() - { - ActiveItem?.AddFolder(); - } - - // ReSharper disable once UnusedMember.Global - Called from view - public void AddLayer() - { - ActiveItem?.AddLayer(); - } - - protected override void OnInitialActivate() - { - Subscribe(); - base.OnInitialActivate(); - } - - protected override void OnClose() - { - Unsubscribe(); - base.OnClose(); - } - - private void CreateRootFolderViewModel() - { - _updatingTree = true; - ProfileElement firstChild = _profileEditorService.SelectedProfile?.Children?.FirstOrDefault(); - if (!(firstChild is Folder folder)) - { - ActivateItem(null); - return; - } - - ActivateItem(_profileTreeVmFactory.FolderViewModel(folder)); - _updatingTree = false; - - // Auto-select the first layer - // if (_profileEditorService.SelectedProfile != null && SelectedTreeItem == null) - // { - // if (_profileEditorService.SelectedProfile.GetRootFolder().Children.FirstOrDefault() is RenderProfileElement firstElement) - // Execute.PostToUIThread(() => _profileEditorService.ChangeSelectedProfileElement(firstElement)); - // } - } - - private static DragDropType GetDragDropType(IDropInfo dropInfo) - { - TreeItemViewModel source = (TreeItemViewModel) dropInfo.Data; - TreeItemViewModel target = (TreeItemViewModel) dropInfo.TargetItem; - if (source == target) - return DragDropType.None; - - TreeItemViewModel parent = target; - while (parent != null) - { - if (parent == source) - return DragDropType.None; - parent = parent.Parent as TreeItemViewModel; - } - - switch (dropInfo.InsertPosition) - { - case RelativeInsertPosition.AfterTargetItem | RelativeInsertPosition.TargetItemCenter: - case RelativeInsertPosition.BeforeTargetItem | RelativeInsertPosition.TargetItemCenter: - return target.SupportsChildren ? DragDropType.Add : DragDropType.None; - case RelativeInsertPosition.BeforeTargetItem: - return DragDropType.InsertBefore; - case RelativeInsertPosition.AfterTargetItem: - return DragDropType.InsertAfter; - default: - return DragDropType.None; - } - } - #region Event handlers private void Subscribe() @@ -187,7 +180,9 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree ActiveItem.UpdateProfileElements(); _updatingTree = false; if (e.RenderProfileElement == null) + { SelectedTreeItem = null; + } else { TreeItemViewModel match = ActiveItem.GetAllChildren().FirstOrDefault(vm => vm.ProfileElement == e.RenderProfileElement); diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/FolderView.xaml b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/FolderView.xaml index 7e10a1d49..bf68ab782 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/FolderView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/FolderView.xaml @@ -13,17 +13,6 @@ - - - - - - - - - - - @@ -34,6 +23,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/LayerView.xaml b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/LayerView.xaml index 65f09b121..14c1d87a0 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/LayerView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/LayerView.xaml @@ -13,17 +13,28 @@ - + - + - - + + - + + + + + + + + + + + + diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/LayerViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/LayerViewModel.cs index 534db413e..b338ddd13 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/LayerViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/LayerViewModel.cs @@ -1,5 +1,4 @@ -using System.Threading.Tasks; -using Artemis.Core; +using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; using Artemis.UI.Shared.Services; @@ -8,8 +7,6 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem { public class LayerViewModel : TreeItemViewModel { - private readonly IProfileEditorService _profileEditorService; - public LayerViewModel(ProfileElement layer, IProfileEditorService profileEditorService, IDialogService dialogService, @@ -18,15 +15,6 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem ISurfaceService surfaceService) : base(layer, profileEditorService, dialogService, profileTreeVmFactory, layerBrushService, surfaceService) { - _profileEditorService = profileEditorService; - } - - public void DuplicateElement() - { - Layer layer = Layer.CreateCopy(); - - _profileEditorService.UpdateSelectedProfile(); - _profileEditorService.ChangeSelectedProfileElement(layer); } public Layer Layer => ProfileElement as Layer; diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs index b1c85351e..77d9c402e 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs @@ -5,12 +5,11 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.LayerBrushes; using Artemis.Core.Services; -using Artemis.Storage.Entities.Profile; using Artemis.UI.Exceptions; +using Artemis.UI.Extensions; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.ProfileEditor.Dialogs; using Artemis.UI.Shared.Services; -using Artemis.UI.Utilities; using Stylet; namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem @@ -178,36 +177,44 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem if (!result) return; + ProfileElement newSelection = null; + if (ProfileElement.Parent != null) + { + int index = ProfileElement.Parent.Children.IndexOf(ProfileElement); + // If there is a next element, select that + if (index < ProfileElement.Parent.Children.Count - 1) + newSelection = ProfileElement.Parent.Children[index + 1]; + // Otherwise select the previous element + else if (index > 0) + newSelection = ProfileElement.Parent.Children[index - 1]; + // And if that's not there, fall back to the parent + else + newSelection = ProfileElement.Parent; + } + // Farewell, cruel world TreeItemViewModel parent = (TreeItemViewModel) Parent; ProfileElement.Parent?.RemoveChild(ProfileElement); parent.RemoveExistingElement(this); _profileEditorService.UpdateSelectedProfile(); - _profileEditorService.ChangeSelectedProfileElement(null); + _profileEditorService.ChangeSelectedProfileElement(newSelection as RenderProfileElement); + } + + public void DuplicateElement() + { + _profileEditorService.DuplicateProfileElement(ProfileElement); } public void CopyElement() { - if (ProfileElement is Layer layer) - JsonClipboard.SetObject(layer.LayerEntity); - else if (ProfileElement is Folder folder) - JsonClipboard.SetObject(folder.FolderEntity); + _profileEditorService.CopyProfileElement(ProfileElement); } public void PasteElement() { - object? clipboardObject = JsonClipboard.GetData(); - if (clipboardObject is LayerEntity layerEntity) - { - layerEntity.Id = Guid.NewGuid(); - layerEntity.Name += " - copy"; - Layer pasted = new Layer(ProfileElement.Profile, ProfileElement.Parent, layerEntity); - ProfileElement.Parent.AddChild(pasted, ProfileElement.Order - 1); - } - else if (clipboardObject is FolderEntity folderEntity) - { - } + if (ProfileElement.Parent is Folder parent) + _profileEditorService.PasteProfileElement(parent, ProfileElement.Order); } public void UpdateProfileElements() @@ -217,19 +224,7 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem foreach (TreeItemViewModel treeItemViewModel in toRemove) Items.Remove(treeItemViewModel); - // Order the children - List vmsList = Items.OrderBy(v => v.ProfileElement.Order).ToList(); - for (int index = 0; index < vmsList.Count; index++) - { - TreeItemViewModel profileElementViewModel = vmsList[index]; - if (Items.IndexOf(profileElementViewModel) != index) - ((BindableCollection) Items).Move(Items.IndexOf(profileElementViewModel), index); - } - - // Ensure every child element has an up-to-date VM - if (ProfileElement.Children == null) - return; - + // Add missing children List newChildren = new List(); foreach (ProfileElement profileElement in ProfileElement.Children.OrderBy(c => c.Order)) if (profileElement is Folder folder) @@ -243,15 +238,15 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem newChildren.Add(_profileTreeVmFactory.LayerViewModel(layer)); } - if (!newChildren.Any()) - return; - // Add the new children in one call, prevent extra UI events foreach (TreeItemViewModel treeItemViewModel in newChildren) { treeItemViewModel.UpdateProfileElements(); Items.Add(treeItemViewModel); } + + // Order the children + ((BindableCollection) Items).Sort(i => i.ProfileElement.Order); } public void EnableToggled() diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs index 780aac675..ccd778935 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs @@ -382,6 +382,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization else _previousSelectedLayer = null; + ApplyActiveProfile(); UpdateLedsDimStatus(); UpdateCanSelectEditTool(); } diff --git a/src/Artemis.UI/Utilities/ClipboardHelper.cs b/src/Artemis.UI/Utilities/ClipboardHelper.cs deleted file mode 100644 index 20cb8975f..000000000 --- a/src/Artemis.UI/Utilities/ClipboardHelper.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Windows; -using Newtonsoft.Json; - -namespace Artemis.UI.Utilities -{ - public static class JsonClipboard - { - private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings {TypeNameHandling = TypeNameHandling.All}; - - public static void SetObject(object clipboardObject) - { - string json = JsonConvert.SerializeObject(clipboardObject, JsonSettings); - Clipboard.SetData("Artemis", json); - } - - public static object GetData() - { - string json = Clipboard.GetData("Artemis")?.ToString(); - if (json != null) - return JsonConvert.DeserializeObject(json, JsonSettings); - return null; - } - - public static T GetData() - { - object data = GetData(); - return data is T castData ? castData : default; - } - - public static bool ContainsArtemisData() - { - return Clipboard.ContainsData("Artemis"); - } - } -} \ No newline at end of file