diff --git a/src/Artemis.Core/Models/Profile/KeyframeEngines/FloatKeyframeEngine.cs b/src/Artemis.Core/Models/Profile/KeyframeEngines/FloatKeyframeEngine.cs index 4fd1313b6..88a922d39 100644 --- a/src/Artemis.Core/Models/Profile/KeyframeEngines/FloatKeyframeEngine.cs +++ b/src/Artemis.Core/Models/Profile/KeyframeEngines/FloatKeyframeEngine.cs @@ -15,7 +15,7 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines var nextKeyframe = (Keyframe) NextKeyframe; var diff = nextKeyframe.Value - currentKeyframe.Value; - return currentKeyframe.Value + diff * KeyframeProgress; + return currentKeyframe.Value + diff * KeyframeProgressEased; } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/KeyframeEngines/IntKeyframeEngine.cs b/src/Artemis.Core/Models/Profile/KeyframeEngines/IntKeyframeEngine.cs index 31b202643..b31e87b8d 100644 --- a/src/Artemis.Core/Models/Profile/KeyframeEngines/IntKeyframeEngine.cs +++ b/src/Artemis.Core/Models/Profile/KeyframeEngines/IntKeyframeEngine.cs @@ -15,7 +15,7 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines var nextKeyframe = (Keyframe) NextKeyframe; var diff = nextKeyframe.Value - currentKeyframe.Value; - return currentKeyframe.Value + diff * KeyframeProgress; + return currentKeyframe.Value + diff * KeyframeProgressEased; } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/KeyframeEngines/KeyframeEngine.cs b/src/Artemis.Core/Models/Profile/KeyframeEngines/KeyframeEngine.cs index c9793812f..ab6b93532 100644 --- a/src/Artemis.Core/Models/Profile/KeyframeEngines/KeyframeEngine.cs +++ b/src/Artemis.Core/Models/Profile/KeyframeEngines/KeyframeEngine.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Artemis.Core.Exceptions; using Artemis.Core.Models.Profile.LayerProperties; +using Artemis.Core.Utilities; namespace Artemis.Core.Models.Profile.KeyframeEngines { @@ -24,10 +25,17 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines public TimeSpan Progress { get; private set; } /// - /// The progress from the current keyframe to the next 0 to 1 + /// The progress from the current keyframe to the next. + /// Range 0.0 to 1.0. /// public float KeyframeProgress { get; private set; } + /// + /// The progress from the current keyframe to the next with the current keyframes easing function applied. + /// Range 0.0 to 1.0 but can be higher than 1.0 depending on easing function. + /// + public float KeyframeProgressEased { get; set; } + /// /// The current keyframe /// @@ -68,28 +76,35 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines if (!Initialized) return; + var keyframes = LayerProperty.UntypedKeyframes.ToList(); Progress = Progress.Add(TimeSpan.FromMilliseconds(deltaTime)); - // TODO Keep them sorted somewhere else, iterating all keyframes multiple times sucks - var sortedKeyframes = LayerProperty.UntypedKeyframes.ToList().OrderBy(k => k.Position).ToList(); + // The current keyframe is the last keyframe before the current time + CurrentKeyframe = keyframes.LastOrDefault(k => k.Position <= Progress); + // The next keyframe is the first keyframe that's after the current time + NextKeyframe = keyframes.FirstOrDefault(k => k.Position > Progress); - CurrentKeyframe = sortedKeyframes.LastOrDefault(k => k.Position <= Progress); - NextKeyframe = sortedKeyframes.FirstOrDefault(k => k.Position > Progress); if (CurrentKeyframe == null) + { KeyframeProgress = 0; + KeyframeProgressEased = 0; + } else if (NextKeyframe == null) + { KeyframeProgress = 1; + KeyframeProgressEased = 1; + } else { var timeDiff = NextKeyframe.Position - CurrentKeyframe.Position; KeyframeProgress = (float) ((Progress - CurrentKeyframe.Position).TotalMilliseconds / timeDiff.TotalMilliseconds); + KeyframeProgressEased = (float) Easings.Interpolate(KeyframeProgress, CurrentKeyframe.EasingFunction); } - // TODO Apply easing and store it separately - // LayerProperty determines what's next: reset, stop, continue } + /// /// Overrides the engine's progress to the provided value /// @@ -106,6 +121,8 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines /// public object GetCurrentValue() { + if (CurrentKeyframe == null && LayerProperty.UntypedKeyframes.Any()) + return LayerProperty.UntypedKeyframes.First().BaseValue; if (CurrentKeyframe == null) return LayerProperty.BaseValue; if (NextKeyframe == null) diff --git a/src/Artemis.Core/Models/Profile/KeyframeEngines/SKPointKeyframeEngine.cs b/src/Artemis.Core/Models/Profile/KeyframeEngines/SKPointKeyframeEngine.cs index b46e34bea..b38250ca6 100644 --- a/src/Artemis.Core/Models/Profile/KeyframeEngines/SKPointKeyframeEngine.cs +++ b/src/Artemis.Core/Models/Profile/KeyframeEngines/SKPointKeyframeEngine.cs @@ -17,7 +17,7 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines var xDiff = nextKeyframe.Value.X - currentKeyframe.Value.X; var yDiff = nextKeyframe.Value.Y - currentKeyframe.Value.Y; - return new SKPoint(currentKeyframe.Value.X + xDiff * KeyframeProgress, currentKeyframe.Value.Y + yDiff * KeyframeProgress); + return new SKPoint(currentKeyframe.Value.X + xDiff * KeyframeProgressEased, currentKeyframe.Value.Y + yDiff * KeyframeProgressEased); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/KeyframeEngines/SKSizeKeyframeEngine.cs b/src/Artemis.Core/Models/Profile/KeyframeEngines/SKSizeKeyframeEngine.cs index 59c4c5b97..b69590872 100644 --- a/src/Artemis.Core/Models/Profile/KeyframeEngines/SKSizeKeyframeEngine.cs +++ b/src/Artemis.Core/Models/Profile/KeyframeEngines/SKSizeKeyframeEngine.cs @@ -17,7 +17,7 @@ namespace Artemis.Core.Models.Profile.KeyframeEngines var widthDiff = nextKeyframe.Value.Width - currentKeyframe.Value.Width; var heightDiff = nextKeyframe.Value.Height - currentKeyframe.Value.Height; - return new SKSize(currentKeyframe.Value.Width + widthDiff * KeyframeProgress, currentKeyframe.Value.Height + heightDiff * KeyframeProgress); + return new SKSize(currentKeyframe.Value.Width + widthDiff * KeyframeProgressEased, currentKeyframe.Value.Height + heightDiff * KeyframeProgressEased); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 8139b97a9..3ad462f2f 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -48,7 +48,6 @@ namespace Artemis.Core.Models.Profile _leds = new List(); _properties = new Dictionary(); - // TODO: Load properties from entity instead of creating the defaults CreateDefaultProperties(); switch (layerEntity.ShapeEntity?.Type) @@ -355,7 +354,7 @@ namespace Artemis.Core.Models.Profile private void CreateDefaultProperties() { - var transformProperty = new LayerProperty(this, null, "Core.Transform", "Transform", "The default properties collection every layer has, allows you to transform the shape."); + var transformProperty = new LayerProperty(this, null, "Core.Transform", "Transform", "The default properties collection every layer has, allows you to transform the shape.") {ExpandByDefault = true}; AnchorPointProperty = new LayerProperty(this, transformProperty, "Core.AnchorPoint", "Anchor Point", "The point at which the shape is attached to its position."); PositionProperty = new LayerProperty(this, transformProperty, "Core.Position", "Position", "The position of the shape."); SizeProperty = new LayerProperty(this, transformProperty, "Core.Size", "Size", "The size of the shape.") {InputAffix = "%"}; diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/BaseKeyframe.cs b/src/Artemis.Core/Models/Profile/LayerProperties/BaseKeyframe.cs index a7a36f52b..f4b6d4d19 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/BaseKeyframe.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/BaseKeyframe.cs @@ -1,9 +1,12 @@ using System; +using Artemis.Core.Utilities; namespace Artemis.Core.Models.Profile.LayerProperties { public class BaseKeyframe { + private TimeSpan _position; + protected BaseKeyframe(Layer layer, BaseLayerProperty property) { Layer = layer; @@ -11,9 +14,20 @@ namespace Artemis.Core.Models.Profile.LayerProperties } public Layer Layer { get; set; } - public TimeSpan Position { get; set; } + + public TimeSpan Position + { + get => _position; + set + { + if (value == _position) return; + _position = value; + BaseProperty.SortKeyframes(); + } + } protected BaseLayerProperty BaseProperty { get; } protected internal object BaseValue { get; set; } + public Easings.Functions EasingFunction { get; set; } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs index fef95bb8d..8e7e2d039 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/BaseLayerProperty.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Artemis.Core.Exceptions; using Artemis.Core.Models.Profile.KeyframeEngines; +using Artemis.Core.Utilities; using Artemis.Storage.Entities.Profile; using Newtonsoft.Json; @@ -56,6 +57,11 @@ namespace Artemis.Core.Models.Profile.LayerProperties /// public string Description { get; set; } + /// + /// Whether to expand this property by default, this is useful for important parent properties. + /// + public bool ExpandByDefault { get; set; } + /// /// An optional input prefix to show before input elements in the UI. /// @@ -114,7 +120,8 @@ namespace Artemis.Core.Models.Profile.LayerProperties propertyEntity.KeyframeEntities.Add(new KeyframeEntity { Position = baseKeyframe.Position, - Value = JsonConvert.SerializeObject(baseKeyframe.BaseValue) + Value = JsonConvert.SerializeObject(baseKeyframe.BaseValue), + EasingFunction = (int) baseKeyframe.EasingFunction }); } } @@ -124,12 +131,15 @@ namespace Artemis.Core.Models.Profile.LayerProperties BaseValue = DeserializePropertyValue(propertyEntity.Value); BaseKeyframes.Clear(); - foreach (var keyframeEntity in propertyEntity.KeyframeEntities) + foreach (var keyframeEntity in propertyEntity.KeyframeEntities.OrderBy(e => e.Position)) { // Create a strongly typed keyframe or else it cannot be cast later on var keyframeType = typeof(Keyframe<>); var keyframe = (BaseKeyframe) Activator.CreateInstance(keyframeType.MakeGenericType(Type), Layer, this); + keyframe.Position = keyframeEntity.Position; keyframe.BaseValue = DeserializePropertyValue(keyframeEntity.Value); + keyframe.EasingFunction = (Easings.Functions) keyframeEntity.EasingFunction; + BaseKeyframes.Add(keyframe); } } @@ -146,18 +156,25 @@ namespace Artemis.Core.Models.Profile.LayerProperties keyframe.Position = position; keyframe.BaseValue = BaseValue; BaseKeyframes.Add(keyframe); + SortKeyframes(); return keyframe; } /// - /// Removes all keyframes from the property. + /// Removes all keyframes from the property and sets the base value to the current value. /// public void ClearKeyframes() { + BaseValue = KeyframeEngine.GetCurrentValue(); BaseKeyframes.Clear(); } + internal void SortKeyframes() + { + BaseKeyframes = BaseKeyframes.OrderBy(k => k.Position).ToList(); + } + public override string ToString() { return $"{nameof(Id)}: {Id}, {nameof(Name)}: {Name}, {nameof(Description)}: {Description}"; diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index 62858b53e..5614cbb38 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -36,6 +36,7 @@ namespace Artemis.Core.Models.Profile.LayerProperties public void AddKeyframe(Keyframe keyframe) { BaseKeyframes.Add(keyframe); + SortKeyframes(); } /// @@ -45,6 +46,7 @@ namespace Artemis.Core.Models.Profile.LayerProperties public void RemoveKeyframe(Keyframe keyframe) { BaseKeyframes.Remove(keyframe); + SortKeyframes(); } /// diff --git a/src/Artemis.Storage/Entities/Profile/PropertyEntity.cs b/src/Artemis.Storage/Entities/Profile/PropertyEntity.cs index 90e56a3c2..b19bfcd47 100644 --- a/src/Artemis.Storage/Entities/Profile/PropertyEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/PropertyEntity.cs @@ -21,5 +21,6 @@ namespace Artemis.Storage.Entities.Profile { public TimeSpan Position { get; set; } public string Value { get; set; } + public int EasingFunction { get; set; } } } \ No newline at end of file diff --git a/src/Artemis.UI/App.xaml b/src/Artemis.UI/App.xaml index c46f01392..ce1f31722 100644 --- a/src/Artemis.UI/App.xaml +++ b/src/Artemis.UI/App.xaml @@ -60,6 +60,12 @@ + + + \ No newline at end of file diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index 878cf7b41..7ad2b2c08 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -144,6 +144,7 @@ MSBuild:Compile Designer + @@ -171,6 +172,7 @@ + diff --git a/src/Artemis.UI/Behaviors/InputBindingBehavior.cs b/src/Artemis.UI/Behaviors/InputBindingBehavior.cs new file mode 100644 index 000000000..d5601670e --- /dev/null +++ b/src/Artemis.UI/Behaviors/InputBindingBehavior.cs @@ -0,0 +1,43 @@ +using System.Windows; + +namespace Artemis.UI.Behaviors +{ + public class InputBindingBehavior + { + public static readonly DependencyProperty PropagateInputBindingsToWindowProperty = + DependencyProperty.RegisterAttached("PropagateInputBindingsToWindow", typeof(bool), typeof(InputBindingBehavior), + new PropertyMetadata(false, OnPropagateInputBindingsToWindowChanged)); + + public static bool GetPropagateInputBindingsToWindow(FrameworkElement obj) + { + return (bool) obj.GetValue(PropagateInputBindingsToWindowProperty); + } + + public static void SetPropagateInputBindingsToWindow(FrameworkElement obj, bool value) + { + obj.SetValue(PropagateInputBindingsToWindowProperty, value); + } + + private static void OnPropagateInputBindingsToWindowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + ((FrameworkElement) d).Loaded += OnLoaded; + } + + private static void OnLoaded(object sender, RoutedEventArgs e) + { + var frameworkElement = (FrameworkElement)sender; + frameworkElement.Loaded -= OnLoaded; + + var window = Window.GetWindow(frameworkElement); + if (window == null) return; + + // Move input bindings from the FrameworkElement to the window. + for (var i = frameworkElement.InputBindings.Count - 1; i >= 0; i--) + { + var inputBinding = frameworkElement.InputBindings[i]; + window.InputBindings.Add(inputBinding); + frameworkElement.InputBindings.Remove(inputBinding); + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Ninject/Factories/IViewModelFactory.cs b/src/Artemis.UI/Ninject/Factories/IViewModelFactory.cs index 74de4bf92..aa65d2797 100644 --- a/src/Artemis.UI/Ninject/Factories/IViewModelFactory.cs +++ b/src/Artemis.UI/Ninject/Factories/IViewModelFactory.cs @@ -62,4 +62,14 @@ namespace Artemis.UI.Ninject.Factories { PropertyTimelineViewModel Create(LayerPropertiesViewModel layerPropertiesViewModel); } + + public interface IPropertyTrackViewModelFactory : IViewModelFactory + { + PropertyTrackViewModel Create(PropertyTimelineViewModel propertyTimelineViewModel, LayerPropertyViewModel layerPropertyViewModel); + } + + public interface IPropertyTrackKeyframeViewModelFactory : IViewModelFactory + { + PropertyTrackKeyframeViewModel Create(BaseKeyframe keyframe); + } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesView.xaml b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesView.xaml index 0b27a31cf..cc2ac125c 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesView.xaml +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesView.xaml @@ -6,10 +6,20 @@ xmlns:local="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.LayerProperties" xmlns:s="https://github.com/canton7/Stylet" xmlns:timeline="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline" + xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" + xmlns:behaviors="clr-namespace:Artemis.UI.Behaviors" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" - d:DataContext="{d:DesignInstance local:LayerPropertiesViewModel}"> + d:DataContext="{d:DesignInstance local:LayerPropertiesViewModel}" + behaviors:InputBindingBehavior.PropagateInputBindingsToWindow="True"> + + + + + + + @@ -28,9 +38,34 @@ - - - + + + + + + + + + + + + + @@ -50,15 +85,14 @@ - + + MouseDown="{s:Action TimelineMouseDown}" + MouseUp="{s:Action TimelineMouseUp}" + MouseMove="{s:Action TimelineMouseMove}"> @@ -74,13 +108,13 @@ - + + MouseDown="{s:Action TimelineMouseDown}" + MouseUp="{s:Action TimelineMouseUp}" + MouseMove="{s:Action TimelineMouseMove}"> @@ -102,11 +136,10 @@ + Width="319" /> diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesView.xaml.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesView.xaml.cs index 859b751f3..44ac59450 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesView.xaml.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesView.xaml.cs @@ -1,4 +1,5 @@ using System.Windows.Controls; +using System.Windows.Input; namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties { diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs index 7284c619c..85c77e957 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertiesViewModel.cs @@ -2,28 +2,39 @@ using System.Linq; using System.Windows; using System.Windows.Input; +using System.Windows.Media; +using Artemis.Core.Events; using Artemis.Core.Models.Profile; +using Artemis.Core.Services; +using Artemis.Core.Services.Interfaces; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.PropertyTree; using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline; using Artemis.UI.Services.Interfaces; +using Stylet; namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties { public class LayerPropertiesViewModel : ProfileEditorPanelViewModel { + private readonly ICoreService _coreService; private readonly ILayerPropertyViewModelFactory _layerPropertyViewModelFactory; private readonly IProfileEditorService _profileEditorService; + private readonly ISettingsService _settingsService; public LayerPropertiesViewModel(IProfileEditorService profileEditorService, + ICoreService coreService, + ISettingsService settingsService, ILayerPropertyViewModelFactory layerPropertyViewModelFactory, IPropertyTreeViewModelFactory propertyTreeViewModelFactory, IPropertyTimelineViewModelFactory propertyTimelineViewModelFactory) { _profileEditorService = profileEditorService; + _coreService = coreService; + _settingsService = settingsService; _layerPropertyViewModelFactory = layerPropertyViewModelFactory; - PixelsPerSecond = 1; + PixelsPerSecond = 31; PropertyTree = propertyTreeViewModelFactory.Create(this); PropertyTimeline = propertyTimelineViewModelFactory.Create(this); @@ -33,17 +44,9 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties _profileEditorService.CurrentTimeChanged += ProfileEditorServiceOnCurrentTimeChanged; } - public string FormattedCurrentTime - { - get - { - if (PixelsPerSecond > 200) - return $"{Math.Floor(_profileEditorService.CurrentTime.TotalSeconds):00}.{_profileEditorService.CurrentTime.Milliseconds:000}"; - if (PixelsPerSecond > 60) - return $"{Math.Floor(_profileEditorService.CurrentTime.TotalSeconds):00}.{_profileEditorService.CurrentTime.Milliseconds:000}"; - return $"{Math.Floor(_profileEditorService.CurrentTime.TotalMinutes):0}:{_profileEditorService.CurrentTime.Seconds:00}"; - } - } + public bool Playing { get; set; } + + public string FormattedCurrentTime => $"{Math.Floor(_profileEditorService.CurrentTime.TotalSeconds):00}.{_profileEditorService.CurrentTime.Milliseconds:000}"; public int PixelsPerSecond { @@ -90,48 +93,145 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties NotifyOfPropertyChange(() => TimeCaretPosition); } + protected override void OnDeactivate() + { + Pause(); + base.OnDeactivate(); + } + + #region Controls + + public void PlayFromStart() + { + if (!IsActive) + return; + if (Playing) + Pause(); + + _profileEditorService.CurrentTime = TimeSpan.Zero; + Play(); + } + + public void Play() + { + if (!IsActive) + return; + if (Playing) + { + Pause(); + return; + } + + _coreService.FrameRendering += CoreServiceOnFrameRendering; + Playing = true; + } + + public void Pause() + { + if (!Playing) + return; + + _coreService.FrameRendering -= CoreServiceOnFrameRendering; + Playing = false; + } + + + public void GoToStart() + { + _profileEditorService.CurrentTime = TimeSpan.Zero; + } + + public void GoToEnd() + { + _profileEditorService.CurrentTime = CalculateEndTime(); + } + + public void GoToPreviousFrame() + { + var frameTime = 1000.0 / _settingsService.GetSetting("Core.TargetFrameRate", 25).Value; + var newTime = Math.Max(0, Math.Round((_profileEditorService.CurrentTime.TotalMilliseconds - frameTime) / frameTime) * frameTime); + _profileEditorService.CurrentTime = TimeSpan.FromMilliseconds(newTime); + } + + public void GoToNextFrame() + { + var frameTime = 1000.0 / _settingsService.GetSetting("Core.TargetFrameRate", 25).Value; + var newTime = Math.Round((_profileEditorService.CurrentTime.TotalMilliseconds + frameTime) / frameTime) * frameTime; + newTime = Math.Min(newTime, CalculateEndTime().TotalMilliseconds); + _profileEditorService.CurrentTime = TimeSpan.FromMilliseconds(newTime); + } + + private TimeSpan CalculateEndTime() + { + // End time is the last keyframe + 10 sec + var lastKeyFrame = PropertyTimeline.PropertyTrackViewModels.SelectMany(r => r.KeyframeViewModels).OrderByDescending(t => t.Keyframe.Position).FirstOrDefault(); + return lastKeyFrame?.Keyframe.Position.Add(new TimeSpan(0, 0, 0, 10)) ?? TimeSpan.FromSeconds(10); + } + + private void CoreServiceOnFrameRendering(object sender, FrameRenderingEventArgs e) + { + Execute.PostToUIThread(() => + { + var newTime = _profileEditorService.CurrentTime.Add(TimeSpan.FromSeconds(e.DeltaTime)); + if (newTime > CalculateEndTime()) + { + newTime = CalculateEndTime(); + Pause(); + } + + _profileEditorService.CurrentTime = newTime; + }); + } + + #endregion + #region Caret movement - private double _caretStartMouseStartOffset; - private bool _mouseOverCaret; private int _pixelsPerSecond; public void TimelineMouseDown(object sender, MouseButtonEventArgs e) { - // TODO Preserve mouse offset - _caretStartMouseStartOffset = e.GetPosition((IInputElement) sender).X - TimeCaretPosition.Left; + ((IInputElement) sender).CaptureMouse(); } - public void CaretMouseEnter(object sender, MouseEventArgs e) + public void TimelineMouseUp(object sender, MouseButtonEventArgs e) { - _mouseOverCaret = true; - } - - public void CaretMouseLeave(object sender, MouseEventArgs e) - { - if (e.LeftButton != MouseButtonState.Pressed) - _mouseOverCaret = false; + ((IInputElement) sender).ReleaseMouseCapture(); } public void TimelineMouseMove(object sender, MouseEventArgs e) { - if (_mouseOverCaret && e.LeftButton == MouseButtonState.Pressed) + if (e.LeftButton == MouseButtonState.Pressed) { - // Snap to visible keyframes - var visibleKeyframes = PropertyTimeline.PropertyTrackViewModels.Where(t => t.LayerPropertyViewModel.Parent != null && - t.LayerPropertyViewModel.Parent.IsExpanded) + // Get the parent grid, need that for our position + var parent = (IInputElement) VisualTreeHelper.GetParent((DependencyObject) sender); + var x = Math.Max(0, e.GetPosition(parent).X); + var newTime = TimeSpan.FromSeconds(x / PixelsPerSecond); + + // Round the time to something that fits the current zoom level + if (PixelsPerSecond < 200) + newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 5.0) * 5.0); + else if (PixelsPerSecond < 500) + newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 2.0) * 2.0); + else + newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds)); + + if (!Keyboard.IsKeyDown(Key.LeftShift) && !Keyboard.IsKeyDown(Key.RightShift)) + { + _profileEditorService.CurrentTime = newTime; + return; + } + + // If shift is held, snap to closest keyframe + var visibleKeyframes = PropertyTimeline.PropertyTrackViewModels + .Where(t => t.LayerPropertyViewModel.Parent != null && t.LayerPropertyViewModel.Parent.IsExpanded) .SelectMany(t => t.KeyframeViewModels); - - TimeCaretPosition = new Thickness(Math.Max(0, e.GetPosition((IInputElement) sender).X + _caretStartMouseStartOffset), 0, 0, 0); - - // Take a tolerance of 5 pixels (half a keyframe width) var tolerance = 1000f / PixelsPerSecond * 5; var closeKeyframe = visibleKeyframes.FirstOrDefault( - kf => Math.Abs(kf.Keyframe.Position.TotalMilliseconds - _profileEditorService.CurrentTime.TotalMilliseconds) < tolerance + kf => Math.Abs(kf.Keyframe.Position.TotalMilliseconds - newTime.TotalMilliseconds) < tolerance ); - if (closeKeyframe != null) - _profileEditorService.CurrentTime = closeKeyframe.Keyframe.Position; + _profileEditorService.CurrentTime = closeKeyframe?.Keyframe.Position ?? newTime; } } diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertyViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertyViewModel.cs index af5eab74f..be0ef884a 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertyViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/LayerPropertyViewModel.cs @@ -24,6 +24,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties LayerProperty = layerProperty; Parent = parent; Children = new List(); + IsExpanded = layerProperty.ExpandByDefault; foreach (var child in layerProperty.Children) Children.Add(layerPropertyViewModelFactory.Create(child, this)); diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTimelineViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTimelineViewModel.cs index 574d7586f..170448f03 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTimelineViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTimelineViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Artemis.UI.Ninject.Factories; using Artemis.UI.Services.Interfaces; using Stylet; @@ -9,10 +10,14 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline public class PropertyTimelineViewModel : PropertyChangedBase { private readonly IProfileEditorService _profileEditorService; + private readonly IPropertyTrackViewModelFactory _propertyTrackViewModelFactory; - public PropertyTimelineViewModel(LayerPropertiesViewModel layerPropertiesViewModel, IProfileEditorService profileEditorService) + public PropertyTimelineViewModel(LayerPropertiesViewModel layerPropertiesViewModel, + IProfileEditorService profileEditorService, + IPropertyTrackViewModelFactory propertyTrackViewModelFactory) { _profileEditorService = profileEditorService; + _propertyTrackViewModelFactory = propertyTrackViewModelFactory; LayerPropertiesViewModel = layerPropertiesViewModel; PropertyTrackViewModels = new BindableCollection(); @@ -50,7 +55,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline private void CreateViewModels(LayerPropertyViewModel property) { - PropertyTrackViewModels.Add(new PropertyTrackViewModel(this, property)); + PropertyTrackViewModels.Add(_propertyTrackViewModelFactory.Create(this, property)); foreach (var child in property.Children) CreateViewModels(child); } diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTrackEasingViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTrackEasingViewModel.cs new file mode 100644 index 000000000..6fcccd060 --- /dev/null +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTrackEasingViewModel.cs @@ -0,0 +1,51 @@ +using System.Windows; +using System.Windows.Media; +using Artemis.Core.Utilities; +using Humanizer; +using Stylet; + +namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline +{ + public class PropertyTrackEasingViewModel : PropertyChangedBase + { + private readonly PropertyTrackKeyframeViewModel _keyframeViewModel; + private bool _isEasingModeSelected; + + public PropertyTrackEasingViewModel(PropertyTrackKeyframeViewModel keyframeViewModel, Easings.Functions easingFunction) + { + _keyframeViewModel = keyframeViewModel; + _isEasingModeSelected = keyframeViewModel.Keyframe.EasingFunction == easingFunction; + + EasingFunction = easingFunction; + Description = easingFunction.Humanize(); + + CreateGeometry(); + } + + public Easings.Functions EasingFunction { get; } + public PointCollection EasingPoints { get; set; } + public string Description { get; set; } + + public bool IsEasingModeSelected + { + get => _isEasingModeSelected; + set + { + _isEasingModeSelected = value; + if (_isEasingModeSelected) + _keyframeViewModel.SelectEasingMode(this); + } + } + + private void CreateGeometry() + { + EasingPoints = new PointCollection(); + for (var i = 1; i <= 10; i++) + { + var x = i; + var y = Easings.Interpolate(i / 10.0, EasingFunction) * 10; + EasingPoints.Add(new Point(x, y)); + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTrackKeyframeViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTrackKeyframeViewModel.cs index 85f2fd5fd..3690e89e4 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTrackKeyframeViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTrackKeyframeViewModel.cs @@ -1,25 +1,114 @@ using System; +using System.Linq; +using System.Windows; +using System.Windows.Input; using Artemis.Core.Models.Profile.LayerProperties; +using Artemis.Core.Utilities; +using Artemis.UI.Services.Interfaces; using Stylet; namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline { public class PropertyTrackKeyframeViewModel : PropertyChangedBase { - public PropertyTrackKeyframeViewModel(BaseKeyframe keyframe) + private readonly IProfileEditorService _profileEditorService; + private int _pixelsPerSecond; + + public PropertyTrackKeyframeViewModel(BaseKeyframe keyframe, IProfileEditorService profileEditorService) { + _profileEditorService = profileEditorService; + Keyframe = keyframe; + EasingViewModels = new BindableCollection(); + CreateEasingViewModels(); } public BaseKeyframe Keyframe { get; } - + public BindableCollection EasingViewModels { get; set; } public double X { get; set; } public string Timestamp { get; set; } + public UIElement ParentView { get; set; } + + public void Update(int pixelsPerSecond) { + _pixelsPerSecond = pixelsPerSecond; + X = pixelsPerSecond * Keyframe.Position.TotalSeconds; Timestamp = $"{Math.Floor(Keyframe.Position.TotalSeconds):00}.{Keyframe.Position.Milliseconds:000}"; } + + #region Keyframe movement + + public void KeyframeMouseDown(object sender, MouseButtonEventArgs e) + { + ((IInputElement) sender).CaptureMouse(); + } + + public void KeyframeMouseUp(object sender, MouseButtonEventArgs e) + { + ((IInputElement) sender).ReleaseMouseCapture(); + } + + public void KeyframeMouseMove(object sender, MouseEventArgs e) + { + if (e.LeftButton == MouseButtonState.Pressed) + { + // Get the parent grid, need that for our position + var x = Math.Max(0, e.GetPosition(ParentView).X); + var newTime = TimeSpan.FromSeconds(x / _pixelsPerSecond); + + // Round the time to something that fits the current zoom level + if (_pixelsPerSecond < 200) + newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 5.0) * 5.0); + else if (_pixelsPerSecond < 500) + newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 2.0) * 2.0); + else + newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds)); + + if (!Keyboard.IsKeyDown(Key.LeftShift) && !Keyboard.IsKeyDown(Key.RightShift)) + { + Keyframe.Position = newTime; + + Update(_pixelsPerSecond); + _profileEditorService.UpdateSelectedProfileElement(); + return; + } + + // If shift is held, snap to the current time + // Take a tolerance of 5 pixels (half a keyframe width) + var tolerance = 1000f / _pixelsPerSecond * 5; + if (Math.Abs(_profileEditorService.CurrentTime.TotalMilliseconds - newTime.TotalMilliseconds) < tolerance) + Keyframe.Position = _profileEditorService.CurrentTime; + else + Keyframe.Position = newTime; + + Update(_pixelsPerSecond); + _profileEditorService.UpdateSelectedProfileElement(); + } + } + + #endregion + + #region Easing + + private void CreateEasingViewModels() + { + foreach (Easings.Functions value in Enum.GetValues(typeof(Easings.Functions))) + EasingViewModels.Add(new PropertyTrackEasingViewModel(this, value)); + } + + public void SelectEasingMode(PropertyTrackEasingViewModel easingViewModel) + { + Keyframe.EasingFunction = easingViewModel.EasingFunction; + // Set every selection to false except on the VM that made the change + foreach (var propertyTrackEasingViewModel in EasingViewModels.Where(vm => vm != easingViewModel)) + propertyTrackEasingViewModel.IsEasingModeSelected = false; + + _profileEditorService.UpdateSelectedProfileElement(); + } + + #endregion } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTrackView.xaml b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTrackView.xaml index fd12f649e..a1358120a 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTrackView.xaml +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTrackView.xaml @@ -28,7 +28,48 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTrackViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTrackViewModel.cs index 2d1662077..ce056bec4 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTrackViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTrackViewModel.cs @@ -1,12 +1,18 @@ using System.Linq; +using Artemis.UI.Ninject.Factories; using Stylet; namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline { - public class PropertyTrackViewModel : PropertyChangedBase + public class PropertyTrackViewModel : Screen { - public PropertyTrackViewModel(PropertyTimelineViewModel propertyTimelineViewModel, LayerPropertyViewModel layerPropertyViewModel) + private readonly IPropertyTrackKeyframeViewModelFactory _propertyTrackKeyframeViewModelFactory; + + public PropertyTrackViewModel(PropertyTimelineViewModel propertyTimelineViewModel, + LayerPropertyViewModel layerPropertyViewModel, + IPropertyTrackKeyframeViewModelFactory propertyTrackKeyframeViewModelFactory) { + _propertyTrackKeyframeViewModelFactory = propertyTrackKeyframeViewModelFactory; PropertyTimelineViewModel = propertyTimelineViewModel; LayerPropertyViewModel = layerPropertyViewModel; KeyframeViewModels = new BindableCollection(); @@ -33,7 +39,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline { if (KeyframeViewModels.Any(k => k.Keyframe == keyframe)) continue; - KeyframeViewModels.Add(new PropertyTrackKeyframeViewModel(keyframe)); + KeyframeViewModels.Add(_propertyTrackKeyframeViewModelFactory.Create(keyframe)); } UpdateKeyframes(PropertyTimelineViewModel.LayerPropertiesViewModel.PixelsPerSecond); @@ -42,7 +48,17 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline public void UpdateKeyframes(int pixelsPerSecond) { foreach (var keyframeViewModel in KeyframeViewModels) + { + keyframeViewModel.ParentView = View; keyframeViewModel.Update(pixelsPerSecond); + } + } + + protected override void OnViewLoaded() + { + foreach (var keyframeViewModel in KeyframeViewModels) + keyframeViewModel.ParentView = View; + base.OnViewLoaded(); } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/ProfileDeviceView.xaml b/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/ProfileDeviceView.xaml index 1d30533fc..9b6fde7de 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/ProfileDeviceView.xaml +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/ProfileDeviceView.xaml @@ -8,8 +8,9 @@ xmlns:visualization="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.Visualization" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" mc:Ignorable="d" - d:DataContext="{d:DesignInstance {x:Type visualization:ProfileDeviceViewModel}}" - d:DesignHeight="450" d:DesignWidth="800"> + d:DesignHeight="450" + d:DesignWidth="800" + d:DataContext="{d:DesignInstance {x:Type visualization:ProfileDeviceViewModel}}"> diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/ProfileLayerView.xaml b/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/ProfileLayerView.xaml index eda1c8394..1dc437c9b 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/ProfileLayerView.xaml +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/ProfileLayerView.xaml @@ -5,7 +5,9 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.Visualization" mc:Ignorable="d" - d:DesignHeight="450" d:DesignWidth="800"> + d:DesignHeight="450" + d:DesignWidth="800" + d:DataContext="{d:DesignInstance {x:Type local:ProfileLayerViewModel}}"> + + + + + + + + + @@ -141,6 +168,8 @@ + + diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/ProfileViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/ProfileViewModel.cs index 9693b215c..785b69742 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/ProfileViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/ProfileViewModel.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; +using System.Windows; using System.Windows.Input; using Artemis.Core.Events; using Artemis.Core.Models.Profile; @@ -45,6 +46,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization Execute.OnUIThreadSync(() => { CanvasViewModels = new ObservableCollection(); + DeviceViewModels = new ObservableCollection(); PanZoomViewModel = new PanZoomViewModel(); }); @@ -62,16 +64,11 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization public bool IsInitializing { get; private set; } public ObservableCollection CanvasViewModels { get; set; } + public ObservableCollection DeviceViewModels { get; set; } public PanZoomViewModel PanZoomViewModel { get; set; } public PluginSetting HighlightSelectedLayer { get; set; } public PluginSetting PauseRenderingOnFocusLoss { get; set; } - public ReadOnlyCollection Devices => CanvasViewModels - .Where(vm => vm is ProfileDeviceViewModel) - .Cast() - .ToList() - .AsReadOnly(); - public VisualizationToolViewModel ActiveToolViewModel { get => _activeToolViewModel; @@ -141,7 +138,6 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization var layerViewModels = CanvasViewModels.Where(vm => vm is ProfileLayerViewModel).Cast().ToList(); var layers = _profileEditorService.SelectedProfile?.GetAllLayers() ?? new List(); - // Add new layers missing a VM foreach (var layer in layers) { @@ -156,74 +152,51 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization profileLayerViewModel.Dispose(); CanvasViewModels.Remove(profileLayerViewModel); } - - // Sort the devices by ZIndex - Execute.PostToUIThread(() => - { - foreach (var device in Devices.ToList()) - CanvasViewModels.Move(CanvasViewModels.IndexOf(device), device.Device.ZIndex - 1); - }); } }); } private void ApplySurfaceConfiguration(ArtemisSurface surface) { - var devices = new List(); - devices.AddRange(surface.Devices); - // Make sure all devices have an up-to-date VM - foreach (var surfaceDeviceConfiguration in devices) - { - // Create VMs for missing devices - ProfileDeviceViewModel viewModel; - lock (CanvasViewModels) - { - viewModel = Devices.FirstOrDefault(vm => vm.Device.RgbDevice == surfaceDeviceConfiguration.RgbDevice); - } - - if (viewModel == null) - { - // Create outside the UI thread to avoid slowdowns as much as possible - var profileDeviceViewModel = new ProfileDeviceViewModel(surfaceDeviceConfiguration); - Execute.PostToUIThread(() => - { - // Gotta call IsInitializing on the UI thread or its never gets picked up - IsInitializing = true; - lock (CanvasViewModels) - { - CanvasViewModels.Add(profileDeviceViewModel); - } - }); - } - // Update existing devices - else - viewModel.Device = surfaceDeviceConfiguration; - } - - - // Sort the devices by ZIndex Execute.PostToUIThread(() => { - lock (CanvasViewModels) + lock (DeviceViewModels) { - foreach (var device in Devices.OrderBy(d => d.ZIndex).ToList()) + var existing = DeviceViewModels.ToList(); + var deviceViewModels = new List(); + + // Add missing/update existing + foreach (var surfaceDeviceConfiguration in surface.Devices.OrderBy(d => d.ZIndex).ToList()) { - var newIndex = Math.Max(device.ZIndex - 1, CanvasViewModels.Count - 1); - CanvasViewModels.Move(CanvasViewModels.IndexOf(device), newIndex); + // Create VMs for missing devices + var viewModel = existing.FirstOrDefault(vm => vm.Device.RgbDevice == surfaceDeviceConfiguration.RgbDevice); + if (viewModel == null) + { + IsInitializing = true; + viewModel = new ProfileDeviceViewModel(surfaceDeviceConfiguration); + } + // Update existing devices + else + viewModel.Device = surfaceDeviceConfiguration; + + // Add the viewModel to the list of VMs we want to keep + deviceViewModels.Add(viewModel); } + + DeviceViewModels = new ObservableCollection(deviceViewModels); } }); } private void UpdateLeds(object sender, CustomUpdateData customUpdateData) { - lock (CanvasViewModels) + lock (DeviceViewModels) { if (IsInitializing) - IsInitializing = Devices.Any(d => !d.AddedLeds); + IsInitializing = DeviceViewModels.Any(d => !d.AddedLeds); - foreach (var profileDeviceViewModel in Devices) + foreach (var profileDeviceViewModel in DeviceViewModels) profileDeviceViewModel.Update(); } } @@ -232,12 +205,12 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization { if (HighlightSelectedLayer.Value && _profileEditorService.SelectedProfileElement is Layer layer) { - foreach (var led in Devices.SelectMany(d => d.Leds)) + foreach (var led in DeviceViewModels.SelectMany(d => d.Leds)) led.IsDimmed = !layer.Leds.Contains(led.Led); } else { - foreach (var led in Devices.SelectMany(d => d.Leds)) + foreach (var led in DeviceViewModels.SelectMany(d => d.Leds)) led.IsDimmed = false; } } @@ -316,11 +289,13 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization public void CanvasMouseDown(object sender, MouseButtonEventArgs e) { + ((IInputElement) sender).CaptureMouse(); ActiveToolViewModel?.MouseDown(sender, e); } public void CanvasMouseUp(object sender, MouseButtonEventArgs e) { + ((IInputElement) sender).ReleaseMouseCapture(); ActiveToolViewModel?.MouseUp(sender, e); } @@ -351,26 +326,26 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization return; layer.ClearLeds(); - layer.AddLeds(Devices.SelectMany(d => d.Leds).Where(vm => vm.IsSelected).Select(vm => vm.Led)); + layer.AddLeds(DeviceViewModels.SelectMany(d => d.Leds).Where(vm => vm.IsSelected).Select(vm => vm.Led)); _profileEditorService.UpdateSelectedProfileElement(); } public void SelectAll() { - foreach (var ledVm in Devices.SelectMany(d => d.Leds)) + foreach (var ledVm in DeviceViewModels.SelectMany(d => d.Leds)) ledVm.IsSelected = true; } public void InverseSelection() { - foreach (var ledVm in Devices.SelectMany(d => d.Leds)) + foreach (var ledVm in DeviceViewModels.SelectMany(d => d.Leds)) ledVm.IsSelected = !ledVm.IsSelected; } public void ClearSelection() { - foreach (var ledVm in Devices.SelectMany(d => d.Leds)) + foreach (var ledVm in DeviceViewModels.SelectMany(d => d.Leds)) ledVm.IsSelected = false; } @@ -421,7 +396,6 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization public void Handle(MainWindowKeyEvent message) { - Debug.WriteLine(message.KeyDown); if (message.KeyDown) { if (ActiveToolIndex != 0) diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/Tools/SelectionAddToolViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/Tools/SelectionAddToolViewModel.cs index decdc6e5c..f808e7a6e 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/Tools/SelectionAddToolViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/Tools/SelectionAddToolViewModel.cs @@ -23,7 +23,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization.Tools var position = e.GetPosition((IInputElement) sender); var selectedRect = new Rect(MouseDownStartPosition, position); - foreach (var device in ProfileViewModel.Devices) + foreach (var device in ProfileViewModel.DeviceViewModels) { foreach (var ledViewModel in device.Leds) { diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/Tools/SelectionRemoveToolViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/Tools/SelectionRemoveToolViewModel.cs index 692271412..793ab0c5b 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/Tools/SelectionRemoveToolViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/Tools/SelectionRemoveToolViewModel.cs @@ -23,7 +23,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization.Tools var position = e.GetPosition((IInputElement) sender); var selectedRect = new Rect(MouseDownStartPosition, position); - foreach (var device in ProfileViewModel.Devices) + foreach (var device in ProfileViewModel.DeviceViewModels) { foreach (var ledViewModel in device.Leds) { diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/Tools/SelectionToolViewModel.cs b/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/Tools/SelectionToolViewModel.cs index c01d09a22..33f2554a7 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/Tools/SelectionToolViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/Tools/SelectionToolViewModel.cs @@ -33,7 +33,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization.Tools // Get selected LEDs var selectedLeds = new List(); - foreach (var device in ProfileViewModel.Devices) + foreach (var device in ProfileViewModel.DeviceViewModels) { foreach (var ledViewModel in device.Leds) { @@ -90,7 +90,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization.Tools var position = ProfileViewModel.PanZoomViewModel.GetRelativeMousePosition(sender, e); var selectedRect = new Rect(MouseDownStartPosition, position); - foreach (var device in ProfileViewModel.Devices) + foreach (var device in ProfileViewModel.DeviceViewModels) { foreach (var ledViewModel in device.Leds) { diff --git a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs index 7049afb0a..fc014154c 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs +++ b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs @@ -64,7 +64,7 @@ namespace Artemis.UI.Screens.SurfaceEditor ApplySelectedSurfaceConfiguration(); } } - + public ArtemisSurface CreateSurfaceConfiguration(string name) { var config = _surfaceService.CreateSurfaceConfiguration(name); @@ -111,22 +111,32 @@ namespace Artemis.UI.Screens.SurfaceEditor private void ApplySelectedSurfaceConfiguration() { // Make sure all devices have an up-to-date VM - foreach (var surfaceDeviceConfiguration in SelectedSurface.Devices) - { - // Create VMs for missing devices - var viewModel = Devices.FirstOrDefault(vm => vm.Device.RgbDevice == surfaceDeviceConfiguration.RgbDevice); - if (viewModel == null) - Execute.PostToUIThread(() => Devices.Add(new SurfaceDeviceViewModel(surfaceDeviceConfiguration))); - // Update existing devices - else - viewModel.Device = surfaceDeviceConfiguration; - } - - // Sort the devices by ZIndex Execute.PostToUIThread(() => { - foreach (var device in Devices.OrderBy(d => d.Device.ZIndex).ToList()) - Devices.Move(Devices.IndexOf(device), device.Device.ZIndex - 1); + lock (Devices) + { + var existing = Devices.ToList(); + var deviceViewModels = new List(); + + // Add missing/update existing + foreach (var surfaceDeviceConfiguration in SelectedSurface.Devices.OrderBy(d => d.ZIndex).ToList()) + { + // Create VMs for missing devices + var viewModel = existing.FirstOrDefault(vm => vm.Device.RgbDevice == surfaceDeviceConfiguration.RgbDevice); + if (viewModel == null) + { + viewModel = new SurfaceDeviceViewModel(surfaceDeviceConfiguration); + } + // Update existing devices + else + viewModel.Device = surfaceDeviceConfiguration; + + // Add the viewModel to the list of VMs we want to keep + deviceViewModels.Add(viewModel); + } + + Devices = new ObservableCollection(deviceViewModels); + } }); _surfaceService.SetActiveSurfaceConfiguration(SelectedSurface); @@ -189,6 +199,7 @@ namespace Artemis.UI.Screens.SurfaceEditor var deviceViewModel = Devices[i]; deviceViewModel.Device.ZIndex = i + 1; } + _surfaceService.UpdateSurfaceConfiguration(SelectedSurface, true); } public void BringForward(SurfaceDeviceViewModel surfaceDeviceViewModel) @@ -202,6 +213,7 @@ namespace Artemis.UI.Screens.SurfaceEditor var deviceViewModel = Devices[i]; deviceViewModel.Device.ZIndex = i + 1; } + _surfaceService.UpdateSurfaceConfiguration(SelectedSurface, true); } public void SendToBack(SurfaceDeviceViewModel surfaceDeviceViewModel) @@ -212,6 +224,7 @@ namespace Artemis.UI.Screens.SurfaceEditor var deviceViewModel = Devices[i]; deviceViewModel.Device.ZIndex = i + 1; } + _surfaceService.UpdateSurfaceConfiguration(SelectedSurface, true); } public void SendBackward(SurfaceDeviceViewModel surfaceDeviceViewModel) @@ -224,6 +237,7 @@ namespace Artemis.UI.Screens.SurfaceEditor var deviceViewModel = Devices[i]; deviceViewModel.Device.ZIndex = i + 1; } + _surfaceService.UpdateSurfaceConfiguration(SelectedSurface, true); } public async Task ViewProperties(SurfaceDeviceViewModel surfaceDeviceViewModel) @@ -248,6 +262,11 @@ namespace Artemis.UI.Screens.SurfaceEditor // ReSharper disable once UnusedMember.Global - Called from view public void EditorGridMouseClick(object sender, MouseButtonEventArgs e) { + if (e.LeftButton == MouseButtonState.Pressed) + ((IInputElement) sender).CaptureMouse(); + else + ((IInputElement) sender).ReleaseMouseCapture(); + if (IsPanKeyDown() || e.ChangedButton == MouseButton.Right) return; diff --git a/src/Artemis.UI/Services/ProfileEditorService.cs b/src/Artemis.UI/Services/ProfileEditorService.cs index f9c60dc85..c20af3a57 100644 --- a/src/Artemis.UI/Services/ProfileEditorService.cs +++ b/src/Artemis.UI/Services/ProfileEditorService.cs @@ -35,12 +35,14 @@ namespace Artemis.UI.Services public void ChangeSelectedProfile(Profile profile) { SelectedProfile = profile; + UpdateProfilePreview(); OnSelectedProfileChanged(); } public void UpdateSelectedProfile() { _profileService.UpdateProfile(SelectedProfile, false); + UpdateProfilePreview(); OnSelectedProfileElementUpdated(); } @@ -53,12 +55,15 @@ namespace Artemis.UI.Services public void UpdateSelectedProfileElement() { _profileService.UpdateProfile(SelectedProfile, true); + UpdateProfilePreview(); OnSelectedProfileElementUpdated(); } private void UpdateProfilePreview() { + if (SelectedProfile == null) + return; var delta = CurrentTime - _lastUpdateTime; foreach (var layer in SelectedProfile.GetAllLayers()) {