diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs index 512b63213..c586fd548 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs @@ -16,10 +16,15 @@ namespace Artemis.Core /// /// Gets the description attribute applied to this property /// - public PropertyDescriptionAttribute PropertyDescription { get; } + PropertyDescriptionAttribute PropertyDescription { get; } /// - /// Gets the unique path of the property on the layer + /// The parent group of this layer property, set after construction + /// + LayerPropertyGroup LayerPropertyGroup { get; } + + /// + /// Gets the unique path of the property on the layer /// public string Path { get; } @@ -30,7 +35,7 @@ namespace Artemis.Core /// /// /// - void Initialize(RenderProfileElement profileElement, LayerPropertyGroup @group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description, string path); + void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description, string path); /// /// Returns a list off all data binding registrations @@ -38,7 +43,14 @@ namespace Artemis.Core List GetAllDataBindingRegistrations(); /// - /// Updates the layer properties internal state + /// Attempts to load and add the provided keyframe entity to the layer property + /// + /// The entity representing the keyframe to add + /// If succeeded the resulting keyframe, otherwise + ILayerPropertyKeyframe? AddKeyframeEntity(KeyframeEntity keyframeEntity); + + /// + /// Updates the layer properties internal state /// /// The timeline to apply to the property void Update(Timeline timeline); diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index 9d8330785..c72c89f82 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -118,9 +118,7 @@ namespace Artemis.Core /// public RenderProfileElement ProfileElement { get; internal set; } - /// - /// The parent group of this layer property, set after construction - /// + /// public LayerPropertyGroup LayerPropertyGroup { get; internal set; } #endregion @@ -282,6 +280,22 @@ namespace Artemis.Core OnKeyframeAdded(); } + /// + public ILayerPropertyKeyframe? AddKeyframeEntity(KeyframeEntity keyframeEntity) + { + if (keyframeEntity.Position > ProfileElement.Timeline.Length) + return null; + T value = CoreJson.DeserializeObject(keyframeEntity.Value); + if (value == null) + return null; + + LayerPropertyKeyframe keyframe = new LayerPropertyKeyframe( + CoreJson.DeserializeObject(keyframeEntity.Value)!, keyframeEntity.Position, (Easings.Functions) keyframeEntity.EasingFunction, this + ); + AddKeyframe(keyframe); + return keyframe; + } + /// /// Removes a keyframe from the layer property /// @@ -508,6 +522,7 @@ namespace Artemis.Core if (!IsLoadedFromStorage) ApplyDefaultValue(null); else + { try { if (Entity.Value != null) @@ -517,6 +532,7 @@ namespace Artemis.Core { // ignored for now } + } CurrentValue = BaseValue; KeyframesEnabled = Entity.KeyframesEnabled; @@ -524,12 +540,8 @@ namespace Artemis.Core _keyframes.Clear(); try { - _keyframes.AddRange(Entity.KeyframeEntities - .Where(k => k.Position <= ProfileElement.Timeline.Length) - .Select(k => new LayerPropertyKeyframe( - CoreJson.DeserializeObject(k.Value)!, k.Position, (Easings.Functions) k.EasingFunction, this - )) - ); + foreach (KeyframeEntity keyframeEntity in Entity.KeyframeEntities.Where(k => k.Position <= ProfileElement.Timeline.Length)) + AddKeyframeEntity(keyframeEntity); } catch (JsonException) { diff --git a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj index 03d21fda6..f92e6bb8f 100644 --- a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj +++ b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj @@ -43,7 +43,7 @@ - + diff --git a/src/Artemis.UI.Shared/packages.lock.json b/src/Artemis.UI.Shared/packages.lock.json index eaa3f5270..a25ff9ed5 100644 --- a/src/Artemis.UI.Shared/packages.lock.json +++ b/src/Artemis.UI.Shared/packages.lock.json @@ -85,9 +85,9 @@ }, "Stylet": { "type": "Direct", - "requested": "[1.3.4, )", - "resolved": "1.3.4", - "contentHash": "bCEdA+AIi+TM9SQQGLYMsFRIfzZcDUDg2Mznyr72kOkcC/cdBj01/jel4/v2aoKwbFcxVjiqmpgnbsFgMEZ4zQ==", + "requested": "[1.3.5, )", + "resolved": "1.3.5", + "contentHash": "9vjjaTgf5sZAGHnxQWIslD32MG5gXj7ANgS+w965L5Eh//UC3qwZDrEf226Pf+v1P/ldAJDpUySnOyGlb3TSSw==", "dependencies": { "System.Drawing.Common": "4.6.0" } diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index 00b623a35..032a4ac72 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -145,7 +145,7 @@ - + diff --git a/src/Artemis.UI/Behaviors/TreeViewSelectionBehavior.cs b/src/Artemis.UI/Behaviors/TreeViewSelectionBehavior.cs index b01c00e4b..fd3c1f42d 100644 --- a/src/Artemis.UI/Behaviors/TreeViewSelectionBehavior.cs +++ b/src/Artemis.UI/Behaviors/TreeViewSelectionBehavior.cs @@ -1,6 +1,7 @@ using System.Collections.Specialized; using System.Windows; using System.Windows.Controls; +using System.Windows.Input; using Microsoft.Xaml.Behaviors; namespace Artemis.UI.Behaviors @@ -105,7 +106,8 @@ namespace Artemis.UI.Behaviors { item.IsSelected = true; // Focus the newly selected item as if was clicked - item.Focus(); + if (FocusManager.GetIsFocusScope(item)) + item.Focus(); if (ExpandSelected) item.IsExpanded = true; } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesView.xaml index 053ab91d9..aeabbaf8b 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesView.xaml @@ -199,6 +199,12 @@ + + + + + + @@ -206,7 +212,11 @@ - + diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/Models/KeyframeClipboardModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/Models/KeyframeClipboardModel.cs index 03a078f0e..821729f2b 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/Models/KeyframeClipboardModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/Models/KeyframeClipboardModel.cs @@ -1,32 +1,86 @@ using System; using System.Collections.Generic; +using System.Linq; using Artemis.Core; using Artemis.Storage.Entities.Profile; using Artemis.UI.Exceptions; namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.Models { - public class KeyframeClipboardModel + public class KeyframesClipboardModel { - public Dictionary KeyframeEntities { get; set; } - public KeyframeClipboardModel(List keyframes) + // ReSharper disable once UnusedMember.Global - For JSON.NET + public KeyframesClipboardModel() { - KeyframeEntities = new Dictionary(); - foreach (ILayerPropertyKeyframe keyframe in keyframes) - { - KeyframeEntities.Add(keyframe.UntypedLayerProperty.Path, ); - } + ClipboardModels = new List(); } - public void Paste(RenderProfileElement target, TimeSpan pastePosition) + public KeyframesClipboardModel(IEnumerable keyframes) + { + ClipboardModels = new List(); + foreach (ILayerPropertyKeyframe keyframe in keyframes.OrderBy(k => k.Position)) + ClipboardModels.Add(new KeyframeClipboardModel(keyframe)); + } + + public List ClipboardModels { get; set; } + public bool HasBeenPasted { get; set; } + + public List Paste(RenderProfileElement target, TimeSpan pastePosition) { if (target == null) throw new ArgumentNullException(nameof(target)); if (HasBeenPasted) throw new ArtemisUIException("Clipboard model can only be pasted once"); + List results = new List(); + if (!ClipboardModels.Any()) + return results; + + // Determine the offset by looking at the position of the first keyframe, start pasting from there + TimeSpan offset = pastePosition - ClipboardModels.First().KeyframeEntity.Position; + List properties = target.GetAllLayerProperties(); + foreach (KeyframeClipboardModel clipboardModel in ClipboardModels) + { + ILayerPropertyKeyframe layerPropertyKeyframe = clipboardModel.Paste(properties, offset); + if (layerPropertyKeyframe != null) + results.Add(layerPropertyKeyframe); + } + HasBeenPasted = true; + return results; + } + } + + public class KeyframeClipboardModel + { + // ReSharper disable once UnusedMember.Global - For JSON.NET + public KeyframeClipboardModel() + { } - public bool HasBeenPasted { get; set; } + public KeyframeClipboardModel(ILayerPropertyKeyframe layerPropertyKeyframe) + { + FeatureId = layerPropertyKeyframe.UntypedLayerProperty.LayerPropertyGroup.Feature.Id; + Path = layerPropertyKeyframe.UntypedLayerProperty.Path; + KeyframeEntity = layerPropertyKeyframe.GetKeyframeEntity(); + } + + public string FeatureId { get; set; } + public string Path { get; set; } + public KeyframeEntity KeyframeEntity { get; set; } + + public ILayerPropertyKeyframe Paste(List properties, TimeSpan offset) + { + ILayerProperty property = properties.FirstOrDefault(p => p.LayerPropertyGroup.Feature.Id == FeatureId && p.Path == Path); + if (property != null) + { + KeyframeEntity.Position += offset; + ILayerPropertyKeyframe keyframe = property.AddKeyframeEntity(KeyframeEntity); + KeyframeEntity.Position -= offset; + + return keyframe; + } + + return null; + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineView.xaml index 49f2d7076..eaec826d2 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineView.xaml @@ -19,12 +19,6 @@ Height="{Binding LayerPropertiesViewModel.TreeViewModelHeight}" VerticalAlignment="Top" Focusable="True"> - - - - - - @@ -43,12 +37,12 @@ - + - + @@ -59,7 +53,7 @@ - + diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineViewModel.cs index 87f683dac..6294d0d7a 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineViewModel.cs @@ -158,13 +158,17 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline public bool CanDuplicateKeyframes => GetAllKeyframeViewModels().Any(k => k.IsSelected); public bool CanCopyKeyframes => GetAllKeyframeViewModels().Any(k => k.IsSelected); public bool CanDeleteKeyframes => GetAllKeyframeViewModels().Any(k => k.IsSelected); - public bool CanPasteKeyframes => JsonClipboard.GetData() is KeyframeClipboardModel; + public bool CanPasteKeyframes => JsonClipboard.GetData() is KeyframesClipboardModel; private TimeSpan? _contextMenuOpenPosition; public void ContextMenuOpening(object sender, ContextMenuEventArgs e) { _contextMenuOpenPosition = GetCursorTime(new Point(e.CursorLeft, e.CursorTop)); + NotifyOfPropertyChange(nameof(CanDuplicateKeyframes)); + NotifyOfPropertyChange(nameof(CanCopyKeyframes)); + NotifyOfPropertyChange(nameof(CanDeleteKeyframes)); + NotifyOfPropertyChange(nameof(CanPasteKeyframes)); } public void ContextMenuClosing(object sender, ContextMenuEventArgs e) @@ -193,12 +197,17 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline _profileEditorService.UpdateSelectedProfileElement(); } - public void DuplicateKeyframes(ITimelineKeyframeViewModel viewModel = null) + public void DuplicateKeyframes(object sender) { - TimeSpan pastePosition = GetPastePosition(viewModel); + TimeSpan pastePosition = GetPastePosition(sender as ITimelineKeyframeViewModel); List keyframes = GetAllKeyframeViewModels().Where(k => k.IsSelected).Select(k => k.Keyframe).ToList(); - DuplicateKeyframes(keyframes, pastePosition); + List newKeyframes = DuplicateKeyframes(keyframes, pastePosition); + // Select only the newly duplicated keyframes + foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in GetAllKeyframeViewModels()) + timelineKeyframeViewModel.IsSelected = newKeyframes.Contains(timelineKeyframeViewModel.Keyframe); + + _profileEditorService.UpdateSelectedProfileElement(); } public void CopyKeyframes() @@ -207,10 +216,15 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline CopyKeyframes(keyframes); } - public void PasteKeyframes(ITimelineKeyframeViewModel viewModel = null) + public void PasteKeyframes(object sender) { - TimeSpan pastePosition = GetPastePosition(viewModel); - PasteKeyframes(pastePosition); + TimeSpan pastePosition = GetPastePosition(sender as ITimelineKeyframeViewModel); + List newKeyframes = PasteKeyframes(pastePosition); + // Select only the newly pasted keyframes + foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in GetAllKeyframeViewModels()) + timelineKeyframeViewModel.IsSelected = newKeyframes.Contains(timelineKeyframeViewModel.Keyframe); + + _profileEditorService.UpdateSelectedProfileElement(); } private TimeSpan GetPastePosition(ITimelineKeyframeViewModel viewModel) @@ -226,26 +240,26 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline return pastePosition; } - - public List DuplicateKeyframes(List keyframes, TimeSpan pastePosition) + + private List DuplicateKeyframes(List keyframes, TimeSpan pastePosition) { - KeyframeClipboardModel clipboardModel = CoreJson.DeserializeObject(CoreJson.SerializeObject(new KeyframeClipboardModel(keyframes), true), true); + KeyframesClipboardModel clipboardModel = CoreJson.DeserializeObject(CoreJson.SerializeObject(new KeyframesClipboardModel(keyframes), true), true); return PasteClipboardData(clipboardModel, pastePosition); } - public void CopyKeyframes(List keyframes) + private void CopyKeyframes(List keyframes) { - KeyframeClipboardModel clipboardModel = new KeyframeClipboardModel(keyframes); + KeyframesClipboardModel clipboardModel = new KeyframesClipboardModel(keyframes); JsonClipboard.SetObject(clipboardModel); } - public List PasteKeyframes(TimeSpan pastePosition) + private List PasteKeyframes(TimeSpan pastePosition) { - KeyframeClipboardModel clipboardObject = JsonClipboard.GetData(); + KeyframesClipboardModel clipboardObject = JsonClipboard.GetData(); return PasteClipboardData(clipboardObject, pastePosition); } - private List PasteClipboardData(KeyframeClipboardModel clipboardModel, TimeSpan pastePosition) + private List PasteClipboardData(KeyframesClipboardModel clipboardModel, TimeSpan pastePosition) { List pasted = new List(); if (clipboardModel == null) @@ -254,9 +268,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline if (target == null) return pasted; - clipboardModel.Paste(target, pastePosition); - - return pasted; + return clipboardModel.Paste(target, pastePosition); } #endregion diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs index 36fb2e3ad..24532605a 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Windows; +using System.Windows.Input; using Artemis.Core; using Artemis.Core.Modules; using Artemis.Core.Services; @@ -209,6 +210,8 @@ namespace Artemis.UI.Screens.ProfileEditor // Expanded status is also undone because undoing works a bit crude, that's annoying List beforeGroups = LayerPropertiesViewModel.GetAllLayerPropertyGroupViewModels(); List expandedPaths = beforeGroups.Where(g => g.IsExpanded).Select(g => g.LayerPropertyGroup.Path).ToList(); + // Store the focused element so we can restore it later + IInputElement focusedElement = FocusManager.GetFocusedElement(Window.GetWindow(View)); if (!_profileEditorService.UndoUpdateProfile()) { @@ -219,7 +222,13 @@ namespace Artemis.UI.Screens.ProfileEditor // Restore the expanded status foreach (LayerPropertyGroupViewModel allLayerPropertyGroupViewModel in LayerPropertiesViewModel.GetAllLayerPropertyGroupViewModels()) allLayerPropertyGroupViewModel.IsExpanded = expandedPaths.Contains(allLayerPropertyGroupViewModel.LayerPropertyGroup.Path); - + // Restore the focused element + Execute.PostToUIThread(async () => + { + await Task.Delay(50); + focusedElement?.Focus(); + }); + _snackbarMessageQueue.Enqueue("Undid profile update", "REDO", Redo); } @@ -228,6 +237,8 @@ namespace Artemis.UI.Screens.ProfileEditor // Expanded status is also undone because undoing works a bit crude, that's annoying List beforeGroups = LayerPropertiesViewModel.GetAllLayerPropertyGroupViewModels(); List expandedPaths = beforeGroups.Where(g => g.IsExpanded).Select(g => g.LayerPropertyGroup.Path).ToList(); + // Store the focused element so we can restore it later + IInputElement focusedElement = FocusManager.GetFocusedElement(Window.GetWindow(View)); if (!_profileEditorService.RedoUpdateProfile()) { @@ -238,6 +249,12 @@ namespace Artemis.UI.Screens.ProfileEditor // Restore the expanded status foreach (LayerPropertyGroupViewModel allLayerPropertyGroupViewModel in LayerPropertiesViewModel.GetAllLayerPropertyGroupViewModels()) allLayerPropertyGroupViewModel.IsExpanded = expandedPaths.Contains(allLayerPropertyGroupViewModel.LayerPropertyGroup.Path); + // Restore the focused element + Execute.PostToUIThread(async () => + { + await Task.Delay(50); + focusedElement?.Focus(); + }); _snackbarMessageQueue.Enqueue("Redid profile update", "UNDO", Undo); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/ProfileTreeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/ProfileTreeViewModel.cs index 576d77912..15b3f486f 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/ProfileTreeViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/ProfileTreeViewModel.cs @@ -42,7 +42,7 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree } } - public bool CanPasteElement => _profileEditorService.GetCanPaste(); + public bool CanPasteElement => _profileEditorService.GetCanPasteProfileElement(); protected override void OnInitialActivate() { diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs index 4f62336ce..f9f9a629f 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileTree/TreeItem/TreeItemViewModel.cs @@ -48,7 +48,7 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem set => SetAndNotify(ref _profileElement, value); } - public bool CanPasteElement => _profileEditorService.GetCanPaste(); + public bool CanPasteElement => _profileEditorService.GetCanPasteProfileElement(); public abstract bool SupportsChildren { get; } diff --git a/src/Artemis.UI/packages.lock.json b/src/Artemis.UI/packages.lock.json index 0be5696bf..4b3b83c1f 100644 --- a/src/Artemis.UI/packages.lock.json +++ b/src/Artemis.UI/packages.lock.json @@ -107,9 +107,9 @@ }, "Stylet": { "type": "Direct", - "requested": "[1.3.4, )", - "resolved": "1.3.4", - "contentHash": "bCEdA+AIi+TM9SQQGLYMsFRIfzZcDUDg2Mznyr72kOkcC/cdBj01/jel4/v2aoKwbFcxVjiqmpgnbsFgMEZ4zQ==", + "requested": "[1.3.5, )", + "resolved": "1.3.5", + "contentHash": "9vjjaTgf5sZAGHnxQWIslD32MG5gXj7ANgS+w965L5Eh//UC3qwZDrEf226Pf+v1P/ldAJDpUySnOyGlb3TSSw==", "dependencies": { "System.Drawing.Common": "4.6.0" } @@ -1410,7 +1410,7 @@ "SharpVectors.Reloaded": "1.6.0", "SkiaSharp": "2.80.2", "SkiaSharp.Views.WPF": "2.80.2", - "Stylet": "1.3.4", + "Stylet": "1.3.5", "System.Buffers": "4.5.0", "System.Numerics.Vectors": "4.5.0", "WriteableBitmapEx": "1.6.5"