From f110383ed41eb1661b6d9ff1aa0ef22e03421cbe Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 2 Dec 2020 19:11:39 +0100 Subject: [PATCH 1/2] Keyframes - Copy/paste WIP Timeline - Improved sizing, avoid unnecessary scrolling Timeline - Fix selection rectangle appearing on mousedown --- .../LayerProperties/ILayerPropertyKeyframe.cs | 36 ++++++ .../Profile/LayerProperties/LayerProperty.cs | 7 +- .../LayerProperties/LayerPropertyKeyFrame.cs | 30 +++-- .../Models/Profile/ProfileElement.cs | 35 +++--- .../Interfaces/IProfileEditorService.cs | 5 + .../Services/ProfileEditorService.cs | 6 + .../LayerProperties/LayerPropertiesView.xaml | 4 +- .../LayerPropertiesViewModel.cs | 7 ++ .../Timeline/Models/KeyframeClipboardModel.cs | 32 ++++++ .../Timeline/TimelineKeyframeViewModel.cs | 36 ++---- .../Timeline/TimelinePropertyView.xaml | 37 +++--- .../Timeline/TimelineView.xaml | 52 +++++++-- .../Timeline/TimelineViewModel.cs | 108 ++++++++++++++++-- .../ProfileTree/ProfileTreeView.xaml | 44 ++++++- .../ProfileTree/ProfileTreeViewModel.cs | 53 ++++++--- .../ProfileTree/TreeItem/FolderView.xaml | 2 +- .../ProfileTree/TreeItem/LayerView.xaml | 2 +- .../ProfileTree/TreeItem/TreeItemViewModel.cs | 7 ++ 18 files changed, 381 insertions(+), 122 deletions(-) create mode 100644 src/Artemis.Core/Models/Profile/LayerProperties/ILayerPropertyKeyframe.cs create mode 100644 src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/Models/KeyframeClipboardModel.cs diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerPropertyKeyframe.cs b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerPropertyKeyframe.cs new file mode 100644 index 000000000..bcf182c27 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerPropertyKeyframe.cs @@ -0,0 +1,36 @@ +using System; +using Artemis.Storage.Entities.Profile; + +namespace Artemis.Core +{ + /// + /// Represents a keyframe on a containing a value and a timestamp + /// + public interface ILayerPropertyKeyframe + { + /// + /// Gets an untyped reference to the layer property of this keyframe + /// + ILayerProperty UntypedLayerProperty { get; } + + /// + /// The position of this keyframe in the timeline + /// + TimeSpan Position { get; set; } + + /// + /// The easing function applied on the value of the keyframe + /// + Easings.Functions EasingFunction { get; set; } + + /// + /// Gets the entity this keyframe uses for persistent storage + /// + KeyframeEntity GetKeyframeEntity(); + + /// + /// Removes the keyframe from the layer property + /// + void Remove(); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index 64a7e1f34..9d8330785 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -559,12 +559,7 @@ namespace Artemis.Core Entity.Value = CoreJson.SerializeObject(BaseValue); Entity.KeyframesEnabled = KeyframesEnabled; Entity.KeyframeEntities.Clear(); - Entity.KeyframeEntities.AddRange(Keyframes.Select(k => new KeyframeEntity - { - Value = CoreJson.SerializeObject(k.Value), - Position = k.Position, - EasingFunction = (int) k.EasingFunction - })); + Entity.KeyframeEntities.AddRange(Keyframes.Select(k => k.GetKeyframeEntity())); Entity.DataBindingEntities.Clear(); foreach (IDataBinding dataBinding in _dataBindings) diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs index e58dd5635..299c0638e 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerPropertyKeyFrame.cs @@ -1,11 +1,12 @@ using System; +using Artemis.Storage.Entities.Profile; namespace Artemis.Core { /// /// Represents a keyframe on a containing a value and a timestamp /// - public class LayerPropertyKeyframe : CorePropertyChanged + public class LayerPropertyKeyframe : CorePropertyChanged, ILayerPropertyKeyframe { private LayerProperty _layerProperty; private TimeSpan _position; @@ -45,10 +46,10 @@ namespace Artemis.Core set => SetAndNotify(ref _value, value); } - - /// - /// The position of this keyframe in the timeline - /// + /// + public ILayerProperty UntypedLayerProperty => LayerProperty; + + /// public TimeSpan Position { get => _position; @@ -59,14 +60,21 @@ namespace Artemis.Core } } - /// - /// The easing function applied on the value of the keyframe - /// + /// public Easings.Functions EasingFunction { get; set; } - /// - /// Removes the keyframe from the layer property - /// + /// + public KeyframeEntity GetKeyframeEntity() + { + return new KeyframeEntity + { + Value = CoreJson.SerializeObject(Value), + Position = Position, + EasingFunction = (int) EasingFunction + }; + } + + /// public void Remove() { LayerProperty.RemoveKeyframe(this); diff --git a/src/Artemis.Core/Models/Profile/ProfileElement.cs b/src/Artemis.Core/Models/Profile/ProfileElement.cs index 5b25c82c5..52c5b3f8e 100644 --- a/src/Artemis.Core/Models/Profile/ProfileElement.cs +++ b/src/Artemis.Core/Models/Profile/ProfileElement.cs @@ -123,7 +123,7 @@ namespace Artemis.Core /// Adds a profile element to the collection, optionally at the given position (1-based) /// /// The profile element to add - /// The order where to place the child (1-based), defaults to the end of the collection + /// The order where to place the child (0-based), defaults to the end of the collection public virtual void AddChild(ProfileElement child, int? order = null) { if (Disposed) @@ -136,31 +136,19 @@ namespace Artemis.Core // Add to the end of the list if (order == null) - { ChildrenList.Add(child); - child.Order = ChildrenList.Count; - } - // Shift everything after the given order + // Insert at the given index else { if (order < 0) order = 0; - foreach (ProfileElement profileElement in ChildrenList.Where(c => c.Order >= order).ToList()) - profileElement.Order++; - - int targetIndex; - if (order == 0) - targetIndex = 0; - else if (order > ChildrenList.Count) - targetIndex = ChildrenList.Count; - else - targetIndex = ChildrenList.FindIndex(c => c.Order == order + 1); - - ChildrenList.Insert(targetIndex, child); - child.Order = order.Value; + if (order > ChildrenList.Count) + order = ChildrenList.Count; + ChildrenList.Insert(order.Value, child); } child.Parent = this; + StreamlineOrder(); } OnChildAdded(); @@ -178,10 +166,7 @@ namespace Artemis.Core lock (ChildrenList) { ChildrenList.Remove(child); - - // Shift everything after the given order - foreach (ProfileElement profileElement in ChildrenList.Where(c => c.Order > child.Order).ToList()) - profileElement.Order--; + StreamlineOrder(); child.Parent = null; } @@ -189,6 +174,12 @@ namespace Artemis.Core OnChildRemoved(); } + private void StreamlineOrder() + { + for (int index = 0; index < ChildrenList.Count; index++) + ChildrenList[index].Order = index; + } + /// /// Returns a flattened list of all child folders /// diff --git a/src/Artemis.UI.Shared/Services/Interfaces/IProfileEditorService.cs b/src/Artemis.UI.Shared/Services/Interfaces/IProfileEditorService.cs index e279f7df1..32b6a92e1 100644 --- a/src/Artemis.UI.Shared/Services/Interfaces/IProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/Interfaces/IProfileEditorService.cs @@ -156,6 +156,11 @@ namespace Artemis.UI.Shared.Services /// The pasted render element ProfileElement? PasteProfileElement(Folder target, int position); + /// + /// Gets a boolean indicating whether a profile element is on the clipboard + /// + bool GetCanPasteProfileElement(); + /// /// Occurs when a new profile is selected /// diff --git a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs index 493a9e287..a3a32d01f 100644 --- a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs @@ -384,6 +384,12 @@ namespace Artemis.UI.Shared.Services return clipboardObject != null ? PasteClipboardData(clipboardObject, target, position) : null; } + public bool GetCanPasteProfileElement() + { + object? clipboardObject = JsonClipboard.GetData(); + return clipboardObject is LayerEntity || clipboardObject is FolderClipboardModel; + } + private RenderProfileElement? PasteClipboardData(object clipboardObject, Folder target, int position) { RenderProfileElement? pasted = null; diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesView.xaml index e43278cf1..053ab91d9 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesView.xaml @@ -160,7 +160,9 @@ VerticalScrollBarVisibility="Hidden" ScrollChanged="TimelineScrollChanged"> - + diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs index a4df1b62b..5da0d6163 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs @@ -35,6 +35,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties private int _rightSideIndex; private RenderProfileElement _selectedProfileElement; private DateTime _lastEffectsViewModelToggle; + private double _treeViewModelHeight; public LayerPropertiesViewModel(IProfileEditorService profileEditorService, ICoreService coreService, @@ -157,6 +158,12 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties public Layer SelectedLayer => SelectedProfileElement as Layer; public Folder SelectedFolder => SelectedProfileElement as Folder; + public double TreeViewModelHeight + { + get => _treeViewModelHeight; + set => SetAndNotify(ref _treeViewModelHeight, value); + } + #region Segments diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/Models/KeyframeClipboardModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/Models/KeyframeClipboardModel.cs new file mode 100644 index 000000000..03a078f0e --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/Models/KeyframeClipboardModel.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using Artemis.Core; +using Artemis.Storage.Entities.Profile; +using Artemis.UI.Exceptions; + +namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.Models +{ + public class KeyframeClipboardModel + { + public Dictionary KeyframeEntities { get; set; } + public KeyframeClipboardModel(List keyframes) + { + KeyframeEntities = new Dictionary(); + foreach (ILayerPropertyKeyframe keyframe in keyframes) + { + KeyframeEntities.Add(keyframe.UntypedLayerProperty.Path, ); + } + } + + public void 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"); + + HasBeenPasted = true; + } + + public bool HasBeenPasted { get; set; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineKeyframeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineKeyframeViewModel.cs index 6e06a734a..3bd23bd47 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineKeyframeViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelineKeyframeViewModel.cs @@ -48,6 +48,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline } public TimeSpan Position => LayerPropertyKeyframe.Position; + public ILayerPropertyKeyframe Keyframe => LayerPropertyKeyframe; public void Dispose() { @@ -158,35 +159,12 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline #endregion #region Context menu actions - - public void Copy() - { - LayerPropertyKeyframe newKeyframe = new LayerPropertyKeyframe( - LayerPropertyKeyframe.Value, - LayerPropertyKeyframe.Position, - LayerPropertyKeyframe.EasingFunction, - LayerPropertyKeyframe.LayerProperty - ); - // If possible, shift the keyframe to the right by 11 pixels - TimeSpan desiredPosition = newKeyframe.Position + TimeSpan.FromMilliseconds(1000f / _profileEditorService.PixelsPerSecond * 11); - if (desiredPosition <= newKeyframe.LayerProperty.ProfileElement.Timeline.Length) - newKeyframe.Position = desiredPosition; - // Otherwise if possible shift it to the left by 11 pixels - else - { - desiredPosition = newKeyframe.Position - TimeSpan.FromMilliseconds(1000f / _profileEditorService.PixelsPerSecond * 11); - if (desiredPosition > TimeSpan.Zero) - newKeyframe.Position = desiredPosition; - } - - LayerPropertyKeyframe.LayerProperty.AddKeyframe(newKeyframe); - _profileEditorService.UpdateSelectedProfileElement(); - } - - public void Delete() + + public void Delete(bool save = true) { LayerPropertyKeyframe.LayerProperty.RemoveKeyframe(LayerPropertyKeyframe); - _profileEditorService.UpdateSelectedProfileElement(); + if (save) + _profileEditorService.UpdateSelectedProfileElement(); } #endregion @@ -196,6 +174,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline { bool IsSelected { get; set; } TimeSpan Position { get; } + ILayerPropertyKeyframe Keyframe { get; } #region Movement @@ -210,8 +189,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline void PopulateEasingViewModels(); void ClearEasingViewModels(); - void Copy(); - void Delete(); + void Delete(bool save = true); #endregion } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelinePropertyView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelinePropertyView.xaml index 5fa7f7181..844e39add 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelinePropertyView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Timeline/TimelinePropertyView.xaml @@ -36,8 +36,8 @@ MouseDown="{s:Action KeyframeMouseDown}" MouseUp="{s:Action KeyframeMouseUp}" MouseMove="{s:Action KeyframeMouseMove}" - ContextMenuOpening="{s:Action ContextMenuOpening}" - ContextMenuClosing="{s:Action ContextMenuClosing}"> + ContextMenuOpening="{s:Action KeyframeContextMenuOpening}" + ContextMenuClosing="{s:Action KeyframeContextMenuClosing}">