From 83cf9a45fb2a4a19f92f5a921150b2f82f8f734d Mon Sep 17 00:00:00 2001 From: SpoinkyNL Date: Sun, 15 Mar 2020 10:42:23 +0100 Subject: [PATCH] Gradient editor - Implemented stop create/remove and edit --- .../AnimationTimeline/BrushAnimation.cs | 76 +++++++++++ .../Screens/GradientEditor/ColorStopView.xaml | 53 +++++++- .../GradientEditor/ColorStopViewModel.cs | 118 ++++++++++++------ .../GradientEditor/GradientEditorView.xaml | 23 +++- .../GradientEditor/GradientEditorViewModel.cs | 77 ++++++++---- .../Utilities/HitTestUtilities.cs | 2 +- .../Utilities/ShortcutUtilities.cs | 2 +- .../Utilities/SizeObserver.cs | 2 +- .../Utilities/TriggerTracing.cs | 2 +- ...{UIUtilities.cs => VisualTreeUtilities.cs} | 42 ++++++- .../Timeline/PropertyTimelineViewModel.cs | 1 + .../Visualization/ProfileView.xaml | 2 +- 12 files changed, 318 insertions(+), 82 deletions(-) create mode 100644 src/Artemis.UI.Shared/AnimationTimeline/BrushAnimation.cs rename src/{Artemis.UI => Artemis.UI.Shared}/Utilities/HitTestUtilities.cs (97%) rename src/{Artemis.UI => Artemis.UI.Shared}/Utilities/ShortcutUtilities.cs (98%) rename src/{Artemis.UI => Artemis.UI.Shared}/Utilities/SizeObserver.cs (98%) rename src/{Artemis.UI => Artemis.UI.Shared}/Utilities/TriggerTracing.cs (99%) rename src/Artemis.UI.Shared/Utilities/{UIUtilities.cs => VisualTreeUtilities.cs} (55%) diff --git a/src/Artemis.UI.Shared/AnimationTimeline/BrushAnimation.cs b/src/Artemis.UI.Shared/AnimationTimeline/BrushAnimation.cs new file mode 100644 index 000000000..094243472 --- /dev/null +++ b/src/Artemis.UI.Shared/AnimationTimeline/BrushAnimation.cs @@ -0,0 +1,76 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.Animation; + +namespace Artemis.UI.Shared.AnimationTimeline +{ + public class BrushAnimation : System.Windows.Media.Animation.AnimationTimeline + { + public static readonly DependencyProperty FromProperty = + DependencyProperty.Register("From", typeof(Brush), typeof(BrushAnimation)); + + public static readonly DependencyProperty ToProperty = + DependencyProperty.Register("To", typeof(Brush), typeof(BrushAnimation)); + + public override Type TargetPropertyType => typeof(Brush); + + //we must define From and To, AnimationTimeline does not have this properties + public Brush From + { + get => (Brush) GetValue(FromProperty); + set => SetValue(FromProperty, value); + } + + public Brush To + { + get => (Brush) GetValue(ToProperty); + set => SetValue(ToProperty, value); + } + + public override object GetCurrentValue(object defaultOriginValue, + object defaultDestinationValue, + AnimationClock animationClock) + { + return GetCurrentValue(defaultOriginValue as Brush, + defaultDestinationValue as Brush, + animationClock); + } + + public object GetCurrentValue(Brush defaultOriginValue, + Brush defaultDestinationValue, + AnimationClock animationClock) + { + if (!animationClock.CurrentProgress.HasValue) + return Brushes.Transparent; + + //use the standard values if From and To are not set + //(it is the value of the given property) + defaultOriginValue = From ?? defaultOriginValue; + defaultDestinationValue = To ?? defaultDestinationValue; + + if (animationClock.CurrentProgress.Value == 0) + return defaultOriginValue; + if (animationClock.CurrentProgress.Value == 1) + return defaultDestinationValue; + + return new VisualBrush(new Border + { + Width = 1, + Height = 1, + Background = defaultOriginValue, + Child = new Border + { + Background = defaultDestinationValue, + Opacity = animationClock.CurrentProgress.Value + } + }); + } + + protected override Freezable CreateInstanceCore() + { + return new BrushAnimation(); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Screens/GradientEditor/ColorStopView.xaml b/src/Artemis.UI.Shared/Screens/GradientEditor/ColorStopView.xaml index cb2854546..be290ccf5 100644 --- a/src/Artemis.UI.Shared/Screens/GradientEditor/ColorStopView.xaml +++ b/src/Artemis.UI.Shared/Screens/GradientEditor/ColorStopView.xaml @@ -6,20 +6,61 @@ xmlns:local="clr-namespace:Artemis.UI.Shared.Screens.GradientEditor" xmlns:s="https://github.com/canton7/Stylet" xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters" + xmlns:utilities="clr-namespace:Artemis.UI.Shared.Utilities" + xmlns:animationTimeline="clr-namespace:Artemis.UI.Shared.AnimationTimeline" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" - d:DataContext="{d:DesignInstance local:ColorStopViewModel}"> + d:DataContext="{d:DesignInstance local:ColorStopViewModel}" Opacity="1"> + - + MouseMove="{s:Action StopMouseMove}"> + + + diff --git a/src/Artemis.UI.Shared/Screens/GradientEditor/ColorStopViewModel.cs b/src/Artemis.UI.Shared/Screens/GradientEditor/ColorStopViewModel.cs index cafdfca8d..67227cfd1 100644 --- a/src/Artemis.UI.Shared/Screens/GradientEditor/ColorStopViewModel.cs +++ b/src/Artemis.UI.Shared/Screens/GradientEditor/ColorStopViewModel.cs @@ -1,10 +1,12 @@ using System; +using System.ComponentModel; +using System.Drawing; +using System.Linq; using System.Windows; +using System.Windows.Controls; using System.Windows.Input; -using System.Windows.Media; using Artemis.Core.Models.Profile; -using PropertyChanged; -using SkiaSharp; +using Artemis.UI.Shared.Utilities; using Stylet; namespace Artemis.UI.Shared.Screens.GradientEditor @@ -12,69 +14,107 @@ namespace Artemis.UI.Shared.Screens.GradientEditor public class ColorStopViewModel : PropertyChangedBase { private readonly GradientEditorViewModel _gradientEditorViewModel; + private bool _isSelected; + private bool _willRemoveColorStop; - public ColorStopViewModel(GradientEditorViewModel gradientEditorViewModel) + public ColorStopViewModel(GradientEditorViewModel gradientEditorViewModel, ColorGradientStop colorStop) { _gradientEditorViewModel = gradientEditorViewModel; + ColorStop = colorStop; + ColorStop.PropertyChanged += ColorStopOnPropertyChanged; } - public ColorGradientStop ColorStop { get; set; } - public double Offset => ColorStop.Position * _gradientEditorViewModel.PreviewWidth; - public SKColor Color => ColorStop.Color; + private void ColorStopOnPropertyChanged(object sender, PropertyChangedEventArgs e) + { + _gradientEditorViewModel.ColorGradient.OnColorValuesUpdated(); + } + + public ColorGradientStop ColorStop { get; } + + public double Offset + { + get => ColorStop.Position * _gradientEditorViewModel.PreviewWidth; + set + { + ColorStop.Position = (float) Math.Round(value / _gradientEditorViewModel.PreviewWidth, 3, MidpointRounding.AwayFromZero); + NotifyOfPropertyChange(nameof(Offset)); + NotifyOfPropertyChange(nameof(OffsetPercent)); + } + } + + public float OffsetPercent + { + get => (float) Math.Round(ColorStop.Position * 100.0, MidpointRounding.AwayFromZero); + set + { + ColorStop.Position = Math.Min(100, Math.Max(0, value)) / 100f; + NotifyOfPropertyChange(nameof(Offset)); + NotifyOfPropertyChange(nameof(OffsetPercent)); + } + } + + public bool IsSelected + { + get => _isSelected; + set => SetAndNotify(ref _isSelected, value); + } + + public bool WillRemoveColorStop + { + get => _willRemoveColorStop; + set => SetAndNotify(ref _willRemoveColorStop, value); + } #region Movement - - private double _mouseDownOffset; - private DateTime _mouseDownTime; - + public void StopMouseDown(object sender, MouseButtonEventArgs e) { - var position = GetPositionInPreview(sender, e); + e.Handled = true; + ((IInputElement) sender).CaptureMouse(); - _mouseDownOffset = Offset - position.X; - _mouseDownTime = DateTime.Now; + _gradientEditorViewModel.SelectColorStop(this); } public void StopMouseUp(object sender, MouseButtonEventArgs e) { - // On regular click, select this color stop - if (DateTime.Now - _mouseDownTime <= TimeSpan.FromMilliseconds(250)) - { - } + e.Handled = true; ((IInputElement) sender).ReleaseMouseCapture(); - e.Handled = true; + if (WillRemoveColorStop) + _gradientEditorViewModel.RemoveColorStop(this); } public void StopMouseMove(object sender, MouseEventArgs e) { + e.Handled = true; + if (!((IInputElement) sender).IsMouseCaptured) return; - var position = GetPositionInPreview(sender, e); - // Scale down with a precision of 3 decimals - var newPosition = Math.Round((position.X + _mouseDownOffset) / _gradientEditorViewModel.PreviewWidth, 3, MidpointRounding.AwayFromZero); - // Limit from 0.0 to 1.0 - newPosition = Math.Min(1, Math.Max(0, newPosition)); + var parent = VisualTreeUtilities.FindParent((DependencyObject) sender, null); + var position = e.GetPosition(parent); + if (position.Y > 50) + { + WillRemoveColorStop = true; + return; + } - ColorStop.Position = (float) newPosition; - NotifyOfPropertyChange(() => Offset); - NotifyOfPropertyChange(() => Color); - } + WillRemoveColorStop = false; - private Point GetPositionInPreview(object sender, MouseEventArgs e) - { - var parent = VisualTreeHelper.GetParent((DependencyObject) sender); - return e.GetPosition((IInputElement) parent); + var minValue = 0.0; + var maxValue = _gradientEditorViewModel.PreviewWidth; + var stops = _gradientEditorViewModel.ColorGradient.Stops.OrderBy(s => s.Position).ToList(); + var previous = stops.IndexOf(ColorStop) >= 1 ? stops[stops.IndexOf(ColorStop) - 1] : null; + var next = stops.IndexOf(ColorStop) + 1 < stops.Count ? stops[stops.IndexOf(ColorStop) + 1] : null; + if (previous != null) + minValue = previous.Position * _gradientEditorViewModel.PreviewWidth; + if (next != null) + maxValue = next.Position * _gradientEditorViewModel.PreviewWidth; + + Offset = Math.Max(minValue, Math.Min(maxValue, position.X)); + _gradientEditorViewModel.ColorGradient.OnColorValuesUpdated(); } #endregion - - public void Update(ColorGradientStop colorStop) - { - ColorStop = colorStop; - NotifyOfPropertyChange(() => Offset); - NotifyOfPropertyChange(() => Color); - } } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorView.xaml b/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorView.xaml index 847602f85..8bf368fc2 100644 --- a/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorView.xaml +++ b/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorView.xaml @@ -23,6 +23,7 @@ d:DataContext="{d:DesignInstance local:GradientEditorViewModel}"> + @@ -48,10 +49,10 @@ - + - + @@ -79,15 +80,27 @@ - + - + - diff --git a/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorViewModel.cs b/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorViewModel.cs index ac9a0bf60..c930dc833 100644 --- a/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorViewModel.cs +++ b/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorViewModel.cs @@ -1,46 +1,79 @@ using System.ComponentModel; using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; using Artemis.Core.Models.Profile; +using Artemis.UI.Shared.Utilities; using Stylet; namespace Artemis.UI.Shared.Screens.GradientEditor { public class GradientEditorViewModel : Screen { + private ColorStopViewModel _selectedColorStopViewModel; + public GradientEditorViewModel(ColorGradient colorGradient) { ColorGradient = colorGradient; ColorStopViewModels = new BindableCollection(); - ColorGradient.PropertyChanged += ColorGradientOnPropertyChanged; - UpdateColorStopViewModels(); + foreach (var colorStop in ColorGradient.Stops.OrderBy(s => s.Position)) + ColorStopViewModels.Add(new ColorStopViewModel(this, colorStop)); } - + public BindableCollection ColorStopViewModels { get; set; } - public ColorGradient ColorGradient { get; } - public double PreviewWidth => 440; - - private void ColorGradientOnPropertyChanged(object sender, PropertyChangedEventArgs e) + public ColorStopViewModel SelectedColorStopViewModel { - if (e.PropertyName == nameof(ColorGradient.Stops)) - UpdateColorStopViewModels(); + get => _selectedColorStopViewModel; + set + { + SetAndNotify(ref _selectedColorStopViewModel, value); + NotifyOfPropertyChange(nameof(HasSelectedColorStopViewModel)); + } } - private void UpdateColorStopViewModels() - { - while (ColorGradient.Stops.Count > ColorStopViewModels.Count) - ColorStopViewModels.Add(new ColorStopViewModel(this)); - while (ColorGradient.Stops.Count < ColorStopViewModels.Count) - ColorStopViewModels.RemoveAt(0); + public bool HasSelectedColorStopViewModel => SelectedColorStopViewModel != null; - var index = 0; - foreach (var colorStop in ColorGradient.Stops.OrderBy(s => s.Position)) - { - var viewModel = ColorStopViewModels[index]; - viewModel.Update(colorStop); - index++; - } + public ColorGradient ColorGradient { get; } + public double PreviewWidth => 437.5; + + public void AddColorStop(object sender, MouseEventArgs e) + { + var child = VisualTreeUtilities.FindChild((DependencyObject) sender, null); + var position = (float) (e.GetPosition(child).X / PreviewWidth); + var stop = new ColorGradientStop(ColorGradient.GetColor(position), position); + ColorGradient.Stops.Add(stop); + ColorGradient.OnColorValuesUpdated(); + + var index = ColorGradient.Stops.OrderBy(s => s.Position).ToList().IndexOf(stop); + var viewModel = new ColorStopViewModel(this, stop); + ColorStopViewModels.Insert(index, viewModel); + + SelectColorStop(viewModel); + } + + public void RemoveColorStop(ColorStopViewModel colorStopViewModel) + { + ColorStopViewModels.Remove(colorStopViewModel); + ColorGradient.Stops.Remove(colorStopViewModel.ColorStop); + ColorGradient.OnColorValuesUpdated(); + + SelectColorStop(null); + } + + public Point GetPositionInPreview(object sender, MouseEventArgs e) + { + var parent = VisualTreeUtilities.FindParent((DependencyObject) sender, null); + return e.GetPosition(parent); + } + + public void SelectColorStop(ColorStopViewModel colorStopViewModel) + { + SelectedColorStopViewModel = colorStopViewModel; + foreach (var stopViewModel in ColorStopViewModels) + stopViewModel.IsSelected = stopViewModel == SelectedColorStopViewModel; } } } \ No newline at end of file diff --git a/src/Artemis.UI/Utilities/HitTestUtilities.cs b/src/Artemis.UI.Shared/Utilities/HitTestUtilities.cs similarity index 97% rename from src/Artemis.UI/Utilities/HitTestUtilities.cs rename to src/Artemis.UI.Shared/Utilities/HitTestUtilities.cs index 92a2ce499..5a5c90081 100644 --- a/src/Artemis.UI/Utilities/HitTestUtilities.cs +++ b/src/Artemis.UI.Shared/Utilities/HitTestUtilities.cs @@ -2,7 +2,7 @@ using System.Windows; using System.Windows.Media; -namespace Artemis.UI.Utilities +namespace Artemis.UI.Shared.Utilities { public static class HitTestUtilities { diff --git a/src/Artemis.UI/Utilities/ShortcutUtilities.cs b/src/Artemis.UI.Shared/Utilities/ShortcutUtilities.cs similarity index 98% rename from src/Artemis.UI/Utilities/ShortcutUtilities.cs rename to src/Artemis.UI.Shared/Utilities/ShortcutUtilities.cs index 300a3009d..237e51ffa 100644 --- a/src/Artemis.UI/Utilities/ShortcutUtilities.cs +++ b/src/Artemis.UI.Shared/Utilities/ShortcutUtilities.cs @@ -1,7 +1,7 @@ using System; using System.Runtime.InteropServices; -namespace Artemis.UI.Utilities +namespace Artemis.UI.Shared.Utilities { public class ShortcutUtilities { diff --git a/src/Artemis.UI/Utilities/SizeObserver.cs b/src/Artemis.UI.Shared/Utilities/SizeObserver.cs similarity index 98% rename from src/Artemis.UI/Utilities/SizeObserver.cs rename to src/Artemis.UI.Shared/Utilities/SizeObserver.cs index 0dcb66bf5..7f099c9bf 100644 --- a/src/Artemis.UI/Utilities/SizeObserver.cs +++ b/src/Artemis.UI.Shared/Utilities/SizeObserver.cs @@ -1,6 +1,6 @@ using System.Windows; -namespace Artemis.UI.Utilities +namespace Artemis.UI.Shared.Utilities { public static class SizeObserver { diff --git a/src/Artemis.UI/Utilities/TriggerTracing.cs b/src/Artemis.UI.Shared/Utilities/TriggerTracing.cs similarity index 99% rename from src/Artemis.UI/Utilities/TriggerTracing.cs rename to src/Artemis.UI.Shared/Utilities/TriggerTracing.cs index e8f6120ec..4f01fd345 100644 --- a/src/Artemis.UI/Utilities/TriggerTracing.cs +++ b/src/Artemis.UI.Shared/Utilities/TriggerTracing.cs @@ -20,7 +20,7 @@ using System.Windows.Media.Animation; // // As this works on anything that inherits from TriggerBase, it will also work on . -namespace Artemis.UI.Utilities +namespace Artemis.UI.Shared.Utilities { #if DEBUG diff --git a/src/Artemis.UI.Shared/Utilities/UIUtilities.cs b/src/Artemis.UI.Shared/Utilities/VisualTreeUtilities.cs similarity index 55% rename from src/Artemis.UI.Shared/Utilities/UIUtilities.cs rename to src/Artemis.UI.Shared/Utilities/VisualTreeUtilities.cs index 5babfa48f..61788c0a4 100644 --- a/src/Artemis.UI.Shared/Utilities/UIUtilities.cs +++ b/src/Artemis.UI.Shared/Utilities/VisualTreeUtilities.cs @@ -4,7 +4,7 @@ using System.Windows.Media; namespace Artemis.UI.Shared.Utilities { // ReSharper disable once InconsistentNaming - public static class UIUtilities + public static class VisualTreeUtilities { /// /// Finds a Child of a given item in the visual tree. @@ -29,8 +29,7 @@ namespace Artemis.UI.Shared.Utilities { var child = VisualTreeHelper.GetChild(parent, i); // If the child is not of the request child type child - var childType = child as T; - if (childType == null) + if (!(child is T)) { // recursively drill down the tree foundChild = FindChild(child, childName); @@ -40,9 +39,8 @@ namespace Artemis.UI.Shared.Utilities } else if (!string.IsNullOrEmpty(childName)) { - var frameworkElement = child as FrameworkElement; // If the child's name is set for search - if (frameworkElement != null && frameworkElement.Name == childName) + if (child is FrameworkElement frameworkElement && frameworkElement.Name == childName) { // if the child's name is of the request name foundChild = (T) child; @@ -59,5 +57,39 @@ namespace Artemis.UI.Shared.Utilities return foundChild; } + + /// + /// Finds a parent of a given item in the visual tree. + /// + /// A child of the queried item. + /// The type of the queried item. + /// x:Name or Name of parent. + /// + /// The first parent item that matches the submitted type parameter. + /// If not matching item can be found, + /// a null parent is being returned. + /// + public static T FindParent(DependencyObject child, string parentName) where T : DependencyObject + { + // Get parent item + var parentObject = VisualTreeHelper.GetParent(child); + + // We've reached the end of the tree + if (parentObject == null) + return null; + + // Check if the parent matches the type we're looking for + if (!(parentObject is T parent)) + return FindParent(parentObject, parentName); + + // If no name is set the first matching type is a match + if (string.IsNullOrEmpty(parentName)) + return parent; + + // If the parent's name is set for search that must match as well + if (parent is FrameworkElement frameworkElement && frameworkElement.Name == parentName) + return parent; + return FindParent(parentObject, parentName); + } } } \ No newline at end of file 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 356aed5be..655c0e007 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTimelineViewModel.cs +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/LayerProperties/Timeline/PropertyTimelineViewModel.cs @@ -7,6 +7,7 @@ using System.Windows.Media; using Artemis.UI.Events; using Artemis.UI.Ninject.Factories; using Artemis.UI.Services.Interfaces; +using Artemis.UI.Shared.Utilities; using Artemis.UI.Utilities; using Stylet; diff --git a/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/ProfileView.xaml b/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/ProfileView.xaml index 14b3e840b..a336795c2 100644 --- a/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/ProfileView.xaml +++ b/src/Artemis.UI/Screens/Module/ProfileEditor/Visualization/ProfileView.xaml @@ -6,7 +6,7 @@ xmlns:s="https://github.com/canton7/Stylet" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:profileEditor="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.Visualization" - xmlns:utilities="clr-namespace:Artemis.UI.Utilities" + xmlns:utilities="clr-namespace:Artemis.UI.Shared.Utilities;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignHeight="510.9" d:DesignWidth="800" d:DataContext="{d:DesignInstance {x:Type profileEditor:ProfileViewModel}}">