Layer properties - Added back most of the reworked VMs and views

This commit is contained in:
SpoinkyNL committed 2020-05-18 23:56:43 +02:00
1 parent 7b238e241e
commit ea66dcd39e
21 files changed
+862 -146

No files matched your search

@@ -20,38 +20,27 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
{
public class LayerPropertiesViewModel : ProfileEditorPanelViewModel
{
private readonly ICoreService _coreService;
private readonly IProfileEditorService _profileEditorService;
private readonly ISettingsService _settingsService;
public LayerPropertiesViewModel(IProfileEditorService profileEditorService, ICoreService coreService, ISettingsService settingsService)
{
_profileEditorService = profileEditorService;
_coreService = coreService;
_settingsService = settingsService;
ProfileEditorService = profileEditorService;
CoreService = coreService;
SettingsService = settingsService;
PixelsPerSecond = 31;
LayerPropertyGroups = new BindableCollection<LayerPropertyGroupViewModel>();
}
public IProfileEditorService ProfileEditorService { get; }
public ICoreService CoreService { get; }
public ISettingsService SettingsService { get; }
public bool Playing { get; set; }
public bool RepeatAfterLastKeyframe { get; set; }
public string FormattedCurrentTime => $"{Math.Floor(_profileEditorService.CurrentTime.TotalSeconds):00}.{_profileEditorService.CurrentTime.Milliseconds:000}";
public int PixelsPerSecond
{
get => _pixelsPerSecond;
set
{
_pixelsPerSecond = value;
OnPixelsPerSecondChanged();
}
}
public string FormattedCurrentTime => $"{Math.Floor(ProfileEditorService.CurrentTime.TotalSeconds):00}.{ProfileEditorService.CurrentTime.Milliseconds:000}";
public Thickness TimeCaretPosition
{
get => new Thickness(_profileEditorService.CurrentTime.TotalSeconds * PixelsPerSecond, 0, 0, 0);
set => _profileEditorService.CurrentTime = TimeSpan.FromSeconds(value.Left / PixelsPerSecond);
get => new Thickness(ProfileEditorService.CurrentTime.TotalSeconds * ProfileEditorService.PixelsPerSecond, 0, 0, 0);
set => ProfileEditorService.CurrentTime = TimeSpan.FromSeconds(value.Left / ProfileEditorService.PixelsPerSecond);
}
public BindableCollection<LayerPropertyGroupViewModel> LayerPropertyGroups { get; set; }
@@ -60,18 +49,18 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
protected override void OnInitialActivate()
{
PopulateProperties(_profileEditorService.SelectedProfileElement);
PopulateProperties(ProfileEditorService.SelectedProfileElement);
_profileEditorService.ProfileElementSelected += ProfileEditorServiceOnProfileElementSelected;
_profileEditorService.CurrentTimeChanged += ProfileEditorServiceOnCurrentTimeChanged;
ProfileEditorService.ProfileElementSelected += ProfileEditorServiceOnProfileElementSelected;
ProfileEditorService.CurrentTimeChanged += ProfileEditorServiceOnCurrentTimeChanged;
base.OnInitialActivate();
}
protected override void OnClose()
{
_profileEditorService.ProfileElementSelected -= ProfileEditorServiceOnProfileElementSelected;
_profileEditorService.CurrentTimeChanged -= ProfileEditorServiceOnCurrentTimeChanged;
ProfileEditorService.ProfileElementSelected -= ProfileEditorServiceOnProfileElementSelected;
ProfileEditorService.CurrentTimeChanged -= ProfileEditorServiceOnCurrentTimeChanged;
base.OnClose();
}
@@ -109,23 +98,24 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
layer.GetType().GetProperty(nameof(layer.Transform)),
typeof(PropertyGroupDescriptionAttribute)
);
LayerPropertyGroups.Add(new LayerPropertyGroupViewModel(_profileEditorService, layer.General, (PropertyGroupDescriptionAttribute) generalAttribute));
LayerPropertyGroups.Add(new LayerPropertyGroupViewModel(_profileEditorService, layer.Transform, (PropertyGroupDescriptionAttribute) transformAttribute));
LayerPropertyGroups.Add(new LayerPropertyGroupViewModel(ProfileEditorService, layer.General, (PropertyGroupDescriptionAttribute) generalAttribute));
LayerPropertyGroups.Add(new LayerPropertyGroupViewModel(ProfileEditorService, layer.Transform, (PropertyGroupDescriptionAttribute) transformAttribute));
if (layer.LayerBrush == null)
return;
// Add the rout group of the brush
// The root group of the brush has no attribute so let's pull one out of our sleeve
var brushDescription = new PropertyGroupDescriptionAttribute
if (layer.LayerBrush != null)
{
Name = layer.LayerBrush.Descriptor.DisplayName,
Description = layer.LayerBrush.Descriptor.Description
};
LayerPropertyGroups.Add(new LayerPropertyGroupViewModel(_profileEditorService, layer.LayerBrush.BaseProperties, brushDescription));
// Add the rout group of the brush
// The root group of the brush has no attribute so let's pull one out of our sleeve
var brushDescription = new PropertyGroupDescriptionAttribute
{
Name = layer.LayerBrush.Descriptor.DisplayName,
Description = layer.LayerBrush.Descriptor.Description
};
LayerPropertyGroups.Add(new LayerPropertyGroupViewModel(ProfileEditorService, layer.LayerBrush.BaseProperties, brushDescription));
}
}
TreeViewModel = new TreeViewModel(LayerPropertyGroups);
TimelineViewModel = new TimelineViewModel(LayerPropertyGroups);
TreeViewModel = new TreeViewModel(this, LayerPropertyGroups);
TimelineViewModel = new TimelineViewModel(this, LayerPropertyGroups);
}
#endregion
@@ -135,7 +125,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
public void PlayFromStart()
{
if (!Playing)
_profileEditorService.CurrentTime = TimeSpan.Zero;
ProfileEditorService.CurrentTime = TimeSpan.Zero;
Play();
}
@@ -150,7 +140,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
return;
}
_coreService.FrameRendering += CoreServiceOnFrameRendering;
CoreService.FrameRendering += CoreServiceOnFrameRendering;
Playing = true;
}
@@ -159,39 +149,39 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
if (!Playing)
return;
_coreService.FrameRendering -= CoreServiceOnFrameRendering;
CoreService.FrameRendering -= CoreServiceOnFrameRendering;
Playing = false;
}
public void GoToStart()
{
_profileEditorService.CurrentTime = TimeSpan.Zero;
ProfileEditorService.CurrentTime = TimeSpan.Zero;
}
public void GoToEnd()
{
_profileEditorService.CurrentTime = CalculateEndTime();
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);
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;
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);
ProfileEditorService.CurrentTime = TimeSpan.FromMilliseconds(newTime);
}
private TimeSpan CalculateEndTime()
{
if (!(_profileEditorService.SelectedProfileElement is Layer layer))
if (!(ProfileEditorService.SelectedProfileElement is Layer layer))
return TimeSpan.MaxValue;
var keyframes = GetKeyframes(false);
@@ -207,7 +197,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
{
Execute.PostToUIThread(() =>
{
var newTime = _profileEditorService.CurrentTime.Add(TimeSpan.FromSeconds(e.DeltaTime));
var newTime = ProfileEditorService.CurrentTime.Add(TimeSpan.FromSeconds(e.DeltaTime));
if (RepeatAfterLastKeyframe)
{
if (newTime > CalculateEndTime().Subtract(TimeSpan.FromSeconds(10)))
@@ -219,7 +209,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
Pause();
}
_profileEditorService.CurrentTime = newTime;
ProfileEditorService.CurrentTime = newTime;
});
}
@@ -227,8 +217,6 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
#region Caret movement
private int _pixelsPerSecond;
public void TimelineMouseDown(object sender, MouseButtonEventArgs e)
{
((IInputElement) sender).CaptureMouse();
@@ -246,28 +234,28 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
// 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);
var newTime = TimeSpan.FromSeconds(x / ProfileEditorService.PixelsPerSecond);
// Round the time to something that fits the current zoom level
if (PixelsPerSecond < 200)
if (ProfileEditorService.PixelsPerSecond < 200)
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 5.0) * 5.0);
else if (PixelsPerSecond < 500)
else if (ProfileEditorService.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;
ProfileEditorService.CurrentTime = newTime;
return;
}
var visibleKeyframes = GetKeyframes(true);
// Take a tolerance of 5 pixels (half a keyframe width)
var tolerance = 1000f / PixelsPerSecond * 5;
var tolerance = 1000f / ProfileEditorService.PixelsPerSecond * 5;
var closeKeyframe = visibleKeyframes.FirstOrDefault(k => Math.Abs(k.Position.TotalMilliseconds - newTime.TotalMilliseconds) < tolerance);
_profileEditorService.CurrentTime = closeKeyframe?.Position ?? newTime;
ProfileEditorService.CurrentTime = closeKeyframe?.Position ?? newTime;
}
}
@@ -281,16 +269,5 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
}
#endregion
#region Events
public event EventHandler PixelsPerSecondChanged;
protected virtual void OnPixelsPerSecondChanged()
{
PixelsPerSecondChanged?.Invoke(this, EventArgs.Empty);
}
#endregion
}
}
@@ -77,5 +77,18 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
foreach (var layerPropertyBaseViewModel in Children)
layerPropertyBaseViewModel.Dispose();
}
public List<LayerPropertyBaseViewModel> GetAllChildren()
{
var result = new List<LayerPropertyBaseViewModel>();
foreach (var layerPropertyBaseViewModel in Children)
{
result.Add(layerPropertyBaseViewModel);
if (layerPropertyBaseViewModel is LayerPropertyGroupViewModel layerPropertyGroupViewModel)
result.AddRange(layerPropertyGroupViewModel.GetAllChildren());
}
return result;
}
}
}
@@ -1,14 +0,0 @@
using Artemis.Core.Models.Profile.LayerProperties;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
{
public class LayerKeyframeViewModel<T>
{
public LayerKeyframeViewModel(LayerPropertyKeyframe<T> keyframe)
{
Keyframe = keyframe;
}
public LayerPropertyKeyframe<T> Keyframe { get; }
}
}
@@ -19,7 +19,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
PropertyDescription = propertyDescription;
TreePropertyViewModel = ProfileEditorService.CreateTreePropertyViewModel(this);
TimelinePropertyViewModel = new TimelinePropertyViewModel<T>(this);
TimelinePropertyViewModel = new TimelinePropertyViewModel<T>(this, profileEditorService);
TreePropertyBaseViewModel = TreePropertyViewModel;
TimelinePropertyBaseViewModel = TimelinePropertyViewModel;
@@ -50,7 +50,6 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
public override void Dispose()
{
TreePropertyViewModel.Dispose();
TimelinePropertyViewModel.Dispose();
}
public void SetCurrentValue(T value, bool saveChanges)
@@ -0,0 +1,50 @@
using System.Windows;
using System.Windows.Media;
using Artemis.Core.Utilities;
using Humanizer;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
{
public class TimelineEasingViewModel
{
private readonly TimelineKeyframeViewModel _keyframeViewModel;
private bool _isEasingModeSelected;
public TimelineEasingViewModel(TimelineKeyframeViewModel keyframeViewModel, Easings.Functions easingFunction)
{
_keyframeViewModel = keyframeViewModel;
_isEasingModeSelected = keyframeViewModel.BaseLayerPropertyKeyframe.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));
}
}
}
}
@@ -0,0 +1,189 @@
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 TimelineKeyframeViewModel<T> : TimelineKeyframeViewModel
{
private readonly IProfileEditorService _profileEditorService;
public TimelineKeyframeViewModel(IProfileEditorService profileEditorService, TimelineViewModel timelineViewModel, LayerPropertyKeyframe<T> layerPropertyKeyframe)
: base(profileEditorService, timelineViewModel, layerPropertyKeyframe)
{
_profileEditorService = profileEditorService;
LayerPropertyKeyframe = layerPropertyKeyframe;
}
public LayerPropertyKeyframe<T> LayerPropertyKeyframe { get; }
#region Context menu actions
public void Copy()
{
var newKeyframe = new LayerPropertyKeyframe<T>(
LayerPropertyKeyframe.Value,
LayerPropertyKeyframe.Position,
LayerPropertyKeyframe.EasingFunction,
LayerPropertyKeyframe.LayerProperty
);
LayerPropertyKeyframe.LayerProperty.AddKeyframe(newKeyframe);
_profileEditorService.UpdateSelectedProfileElement();
}
public void Delete()
{
LayerPropertyKeyframe.LayerProperty.RemoveKeyframe(LayerPropertyKeyframe);
_profileEditorService.UpdateSelectedProfileElement();
}
#endregion
}
public abstract class TimelineKeyframeViewModel
{
private readonly IProfileEditorService _profileEditorService;
private readonly TimelineViewModel _timelineViewModel;
private int _pixelsPerSecond;
protected TimelineKeyframeViewModel(IProfileEditorService profileEditorService, TimelineViewModel timelineViewModel, BaseLayerPropertyKeyframe baseLayerPropertyKeyframe)
{
_profileEditorService = profileEditorService;
_timelineViewModel = timelineViewModel;
BaseLayerPropertyKeyframe = baseLayerPropertyKeyframe;
}
public BaseLayerPropertyKeyframe BaseLayerPropertyKeyframe { get; }
public BindableCollection<TimelineEasingViewModel> EasingViewModels { get; set; }
public bool IsSelected { 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 * BaseLayerPropertyKeyframe.Position.TotalSeconds;
Timestamp = $"{Math.Floor(BaseLayerPropertyKeyframe.Position.TotalSeconds):00}.{BaseLayerPropertyKeyframe.Position.Milliseconds:000}";
}
#region Keyframe movement
public void KeyframeMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.LeftButton == MouseButtonState.Released)
return;
((IInputElement) sender).CaptureMouse();
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift) && !IsSelected)
_timelineViewModel.SelectKeyframe(this, true, false);
else if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
_timelineViewModel.SelectKeyframe(this, false, true);
else if (!IsSelected)
_timelineViewModel.SelectKeyframe(this, false, false);
e.Handled = true;
}
public void KeyframeMouseUp(object sender, MouseButtonEventArgs e)
{
_profileEditorService.UpdateSelectedProfileElement();
_timelineViewModel.ReleaseSelectedKeyframes();
((IInputElement) sender).ReleaseMouseCapture();
}
public void KeyframeMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
_timelineViewModel.MoveSelectedKeyframes(GetCursorTime(e.GetPosition(ParentView)));
e.Handled = true;
}
private TimeSpan GetCursorTime(Point position)
{
// Get the parent grid, need that for our position
var x = Math.Max(0, position.X);
var time = TimeSpan.FromSeconds(x / _pixelsPerSecond);
// Round the time to something that fits the current zoom level
if (_pixelsPerSecond < 200)
time = TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 5.0) * 5.0);
else if (_pixelsPerSecond < 500)
time = TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 2.0) * 2.0);
else
time = TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds));
// If shift is held, snap to the current time
// Take a tolerance of 5 pixels (half a keyframe width)
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
{
var tolerance = 1000f / _pixelsPerSecond * 5;
if (Math.Abs(_profileEditorService.CurrentTime.TotalMilliseconds - time.TotalMilliseconds) < tolerance)
time = _profileEditorService.CurrentTime;
}
return time;
}
#endregion
#region Easing
private void CreateEasingViewModels()
{
EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions)).Cast<Easings.Functions>().Select(v => new TimelineEasingViewModel(this, v)));
}
public void SelectEasingMode(TimelineEasingViewModel easingViewModel)
{
BaseLayerPropertyKeyframe.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
#region Movement
private bool _movementReleased = true;
private TimeSpan _startOffset;
public void ApplyMovement(TimeSpan cursorTime)
{
if (_movementReleased)
{
_movementReleased = false;
_startOffset = cursorTime - BaseLayerPropertyKeyframe.Position;
}
else
{
BaseLayerPropertyKeyframe.Position = cursorTime - _startOffset;
if (BaseLayerPropertyKeyframe.Position < TimeSpan.Zero)
BaseLayerPropertyKeyframe.Position = TimeSpan.Zero;
Update(_pixelsPerSecond);
}
}
public void ReleaseMovement()
{
_movementReleased = true;
}
#endregion
}
}
@@ -0,0 +1,86 @@
<UserControl x:Class="Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline.TimelinePropertyGroupView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:layerProperties="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.LayerProperties"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:TimelinePropertyGroupViewModel}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="25" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border Height="25" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource MaterialDesignDivider}" Grid.Row="0">
<ItemsControl ItemsSource="{Binding TimelineKeyframeViewModels}"
Background="{DynamicResource MaterialDesignToolBarBackground}"
HorizontalAlignment="Left">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="{x:Type ContentPresenter}">
<Setter Property="Canvas.Left" Value="{Binding X}" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Fill="{StaticResource PrimaryHueMidBrush}"
Stroke="White"
StrokeThickness="0"
Width="10"
Height="10"
Margin="-5,6,0,0"
s:View.ActionTarget="{Binding}">
<Ellipse.Style>
<Style TargetType="{x:Type Ellipse}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="StrokeThickness" To="1" Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="StrokeThickness" To="0" Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
<ItemsControl ItemsSource="{Binding LayerPropertyGroupViewModel.Children}" Grid.Row="1">
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type layerProperties:LayerPropertyGroupViewModel}">
<ContentControl s:View.Model="{Binding TimelinePropertyGroupViewModel}"
VerticalContentAlignment="Stretch"
HorizontalContentAlignment="Stretch"
IsTabStop="False" />
</DataTemplate>
<DataTemplate DataType="{x:Type layerProperties:LayerPropertyViewModel}">
<ContentControl s:View.Model="{Binding TimelinePropertyBaseViewModel}"
VerticalContentAlignment="Stretch"
HorizontalContentAlignment="Stretch"
IsTabStop="False" />
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
</Grid>
</UserControl>
@@ -0,0 +1,108 @@
<UserControl x:Class="Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline.TimelinePropertyView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:TimelinePropertyViewModel}">
<Border Height="25" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource MaterialDesignDivider}">
<ItemsControl ItemsSource="{Binding TimelineKeyframeViewModels}"
Background="{DynamicResource MaterialDesignToolBarBackground}"
HorizontalAlignment="Left">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="{x:Type ContentPresenter}">
<Setter Property="Canvas.Left" Value="{Binding X}" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Fill="{StaticResource PrimaryHueMidBrush}"
Stroke="White"
StrokeThickness="0"
Width="10"
Height="10"
Margin="-5,6,0,0"
ToolTip="{Binding Timestamp}"
s:View.ActionTarget="{Binding}"
MouseDown="{s:Action KeyframeMouseDown}"
MouseUp="{s:Action KeyframeMouseUp}"
MouseMove="{s:Action KeyframeMouseMove}">
<Ellipse.Style>
<Style TargetType="{x:Type Ellipse}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="StrokeThickness" To="1" Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="StrokeThickness" To="0" Duration="0:0:0.25" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
<Ellipse.ContextMenu>
<ContextMenu>
<MenuItem Header="Copy" Command="{s:Action Copy}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="ContentCopy" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Delete" Command="{s:Action Delete}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="Delete" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Easing" ItemsSource="{Binding EasingViewModels}">
<MenuItem.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}" BasedOn="{StaticResource MaterialDesignMenuItem}">
<Setter Property="IsCheckable" Value="True" />
<Setter Property="IsChecked" Value="{Binding Path=IsEasingModeSelected, Mode=TwoWay}" />
</Style>
</MenuItem.ItemContainerStyle>
<MenuItem.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Polyline Stroke="{DynamicResource MaterialDesignBody}"
StrokeThickness="1"
Points="{Binding EasingPoints}"
Stretch="Uniform"
Width="20"
Height="20"
Margin="0 0 10 0" />
<TextBlock Text="{Binding Description}" />
</StackPanel>
</DataTemplate>
</MenuItem.ItemTemplate>
</MenuItem>
<!-- <MenuItem Header="Easing mode" IsEnabled="{Binding CanSelectEasingMode}"> -->
<!-- <MenuItem Header="Ease in" Command="{s:Action SetEasingMode}" CommandParameter="EaseIn" /> -->
<!-- <MenuItem Header="Ease out" Command="{s:Action SetEasingMode}" CommandParameter="EaseOut" /> -->
<!-- <MenuItem Header="Ease in and out" Command="{s:Action SetEasingMode}" CommandParameter="EaseInOut" /> -->
<!-- </MenuItem> -->
</ContextMenu>
</Ellipse.ContextMenu>
</Ellipse>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</UserControl>
@@ -1,30 +1,49 @@
using System;
using System.Linq;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Abstract;
using Artemis.UI.Services.Interfaces;
using Stylet;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
{
public class TimelinePropertyViewModel<T> : TimelinePropertyViewModel
{
public TimelinePropertyViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel) : base(layerPropertyBaseViewModel)
private readonly IProfileEditorService _profileEditorService;
public TimelinePropertyViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel, IProfileEditorService profileEditorService) : base(layerPropertyBaseViewModel)
{
_profileEditorService = profileEditorService;
LayerPropertyViewModel = (LayerPropertyViewModel<T>) layerPropertyBaseViewModel;
}
public LayerPropertyViewModel<T> LayerPropertyViewModel { get; }
public override void Dispose()
public override void UpdateKeyframes(TimelineViewModel timelineViewModel)
{
var keyframes = LayerPropertyViewModel.LayerProperty.Keyframes.ToList();
TimelineKeyframeViewModels.RemoveRange(
TimelineKeyframeViewModels.Where(t => !keyframes.Contains(t.BaseLayerPropertyKeyframe))
);
TimelineKeyframeViewModels.AddRange(
keyframes.Where(k => TimelineKeyframeViewModels.All(t => t.BaseLayerPropertyKeyframe != k))
.Select(k => new TimelineKeyframeViewModel<T>(_profileEditorService, timelineViewModel, k))
);
foreach (var timelineKeyframeViewModel in TimelineKeyframeViewModels)
timelineKeyframeViewModel.Update(_profileEditorService.PixelsPerSecond);
}
}
public abstract class TimelinePropertyViewModel : IDisposable
public abstract class TimelinePropertyViewModel
{
protected TimelinePropertyViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel)
{
LayerPropertyBaseViewModel = layerPropertyBaseViewModel;
TimelineKeyframeViewModels = new BindableCollection<TimelineKeyframeViewModel>();
}
public LayerPropertyBaseViewModel LayerPropertyBaseViewModel { get; }
public abstract void Dispose();
public BindableCollection<TimelineKeyframeViewModel> TimelineKeyframeViewModels { get; set; }
public abstract void UpdateKeyframes(TimelineViewModel timelineViewModel);
}
}
@@ -4,9 +4,52 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
xmlns:s="https://github.com/canton7/Stylet"
mc:Ignorable="d"
d:DesignHeight="25"
d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:TimelineViewModel}">
<Grid Background="{DynamicResource MaterialDesignToolBarBackground}"
MouseDown="{s:Action TimelineCanvasMouseDown}"
MouseUp="{s:Action TimelineCanvasMouseUp}"
MouseMove="{s:Action TimelineCanvasMouseMove}">
<Grid.Triggers>
<EventTrigger RoutedEvent="UIElement.MouseLeftButtonDown">
<BeginStoryboard>
<Storyboard Storyboard.TargetName="MultiSelectionPath" Storyboard.TargetProperty="Opacity">
<DoubleAnimation From="0" To="1" Duration="0:0:0.1" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="UIElement.MouseLeftButtonUp">
<BeginStoryboard>
<Storyboard Storyboard.TargetName="MultiSelectionPath" Storyboard.TargetProperty="Opacity">
<DoubleAnimation From="1" To="0" Duration="0:0:0.2" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>
<ItemsControl ItemsSource="{Binding LayerPropertyGroups}"
Width="{Binding Width}"
MinWidth="{Binding ActualWidth, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ScrollViewer}}"
HorizontalAlignment="Left">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl s:View.Model="{Binding TimelinePropertyGroupViewModel}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Multi-selection rectangle -->
<Path Data="{Binding SelectionRectangle}" Opacity="0"
Stroke="{DynamicResource PrimaryHueLightBrush}"
StrokeThickness="1"
x:Name="MultiSelectionPath"
IsHitTestVisible="False">
<Path.Fill>
<SolidColorBrush Color="{DynamicResource Primary400}" Opacity="0.25" />
</Path.Fill>
</Path>
</Grid>
</UserControl>
</UserControl>
@@ -1,14 +1,174 @@
using Stylet;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Abstract;
using Artemis.UI.Shared.Utilities;
using Stylet;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
{
public class TimelineViewModel
{
public TimelineViewModel(BindableCollection<LayerPropertyGroupViewModel> layerPropertyGroups)
private readonly LayerPropertiesViewModel _layerPropertiesViewModel;
public TimelineViewModel(LayerPropertiesViewModel layerPropertiesViewModel, BindableCollection<LayerPropertyGroupViewModel> layerPropertyGroups)
{
_layerPropertiesViewModel = layerPropertiesViewModel;
LayerPropertyGroups = layerPropertyGroups;
SelectionRectangle = new RectangleGeometry();
UpdateKeyframes();
}
public BindableCollection<LayerPropertyGroupViewModel> LayerPropertyGroups { get; }
public double Width { get; set; }
public RectangleGeometry SelectionRectangle { get; set; }
public void UpdateKeyframes()
{
foreach (var layerPropertyGroupViewModel in LayerPropertyGroups)
{
foreach (var layerPropertyBaseViewModel in layerPropertyGroupViewModel.GetAllChildren())
{
if (layerPropertyBaseViewModel is LayerPropertyViewModel layerPropertyViewModel)
layerPropertyViewModel.TimelinePropertyBaseViewModel.UpdateKeyframes(this);
}
}
}
#region Keyframe movement
public void MoveSelectedKeyframes(TimeSpan cursorTime)
{
// Ensure the selection rectangle doesn't show, the view isn't aware of different types of dragging
SelectionRectangle.Rect = new Rect();
var keyframeViewModels = GetAllKeyframeViewModels();
foreach (var keyframeViewModel in keyframeViewModels.Where(k => k.IsSelected))
keyframeViewModel.ApplyMovement(cursorTime);
_layerPropertiesViewModel.ProfileEditorService.UpdateProfilePreview();
}
public void ReleaseSelectedKeyframes()
{
var keyframeViewModels = GetAllKeyframeViewModels();
foreach (var keyframeViewModel in keyframeViewModels.Where(k => k.IsSelected))
keyframeViewModel.ReleaseMovement();
}
#endregion
#region Keyframe selection
private Point _mouseDragStartPoint;
private bool _mouseDragging;
// ReSharper disable once UnusedMember.Global - Called from view
public void TimelineCanvasMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.LeftButton == MouseButtonState.Released)
return;
((IInputElement) sender).CaptureMouse();
SelectionRectangle.Rect = new Rect();
_mouseDragStartPoint = e.GetPosition((IInputElement) sender);
_mouseDragging = true;
e.Handled = true;
}
// ReSharper disable once UnusedMember.Global - Called from view
public void TimelineCanvasMouseUp(object sender, MouseEventArgs e)
{
if (!_mouseDragging)
return;
var position = e.GetPosition((IInputElement) sender);
var selectedRect = new Rect(_mouseDragStartPoint, position);
SelectionRectangle.Rect = selectedRect;
var keyframeViewModels = GetAllKeyframeViewModels();
var selectedKeyframes = HitTestUtilities.GetHitViewModels<TimelineKeyframeViewModel>((Visual) sender, SelectionRectangle);
foreach (var keyframeViewModel in keyframeViewModels)
keyframeViewModel.IsSelected = selectedKeyframes.Contains(keyframeViewModel);
_mouseDragging = false;
e.Handled = true;
((IInputElement) sender).ReleaseMouseCapture();
}
public void TimelineCanvasMouseMove(object sender, MouseEventArgs e)
{
if (_mouseDragging && e.LeftButton == MouseButtonState.Pressed)
{
var position = e.GetPosition((IInputElement) sender);
var selectedRect = new Rect(_mouseDragStartPoint, position);
SelectionRectangle.Rect = selectedRect;
e.Handled = true;
}
}
public void SelectKeyframe(TimelineKeyframeViewModel clicked, bool selectBetween, bool toggle)
{
var keyframeViewModels = GetAllKeyframeViewModels();
if (selectBetween)
{
var selectedIndex = keyframeViewModels.FindIndex(k => k.IsSelected);
// If nothing is selected, select only the clicked
if (selectedIndex == -1)
{
clicked.IsSelected = true;
return;
}
foreach (var keyframeViewModel in keyframeViewModels)
keyframeViewModel.IsSelected = false;
var clickedIndex = keyframeViewModels.IndexOf(clicked);
if (clickedIndex < selectedIndex)
{
foreach (var keyframeViewModel in keyframeViewModels.Skip(clickedIndex).Take(selectedIndex - clickedIndex + 1))
keyframeViewModel.IsSelected = true;
}
else
{
foreach (var keyframeViewModel in keyframeViewModels.Skip(selectedIndex).Take(clickedIndex - selectedIndex + 1))
keyframeViewModel.IsSelected = true;
}
}
else if (toggle)
{
// Toggle only the clicked keyframe, leave others alone
clicked.IsSelected = !clicked.IsSelected;
}
else
{
// Only select the clicked keyframe
foreach (var keyframeViewModel in keyframeViewModels)
keyframeViewModel.IsSelected = false;
clicked.IsSelected = true;
}
}
private List<TimelineKeyframeViewModel> GetAllKeyframeViewModels()
{
var viewModels = new List<LayerPropertyBaseViewModel>();
foreach (var layerPropertyGroupViewModel in LayerPropertyGroups)
viewModels.AddRange(layerPropertyGroupViewModel.GetAllChildren());
var keyframes = viewModels.Where(vm => vm is LayerPropertyViewModel)
.SelectMany(vm => ((LayerPropertyViewModel) vm).TimelinePropertyBaseViewModel.TimelineKeyframeViewModels)
.ToList();
return keyframes;
}
#endregion
}
}
@@ -1,13 +1,18 @@
using System;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Abstract;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree.PropertyInput.Abstract;
using Artemis.UI.Services.Interfaces;
namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree
{
public class TreePropertyViewModel<T> : TreePropertyViewModel
{
public TreePropertyViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel, PropertyInputViewModel<T> propertyInputViewModel) : base(layerPropertyBaseViewModel)
private readonly IProfileEditorService _profileEditorService;
public TreePropertyViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel, PropertyInputViewModel<T> propertyInputViewModel,
IProfileEditorService profileEditorService) : base(layerPropertyBaseViewModel)
{
_profileEditorService = profileEditorService;
LayerPropertyViewModel = (LayerPropertyViewModel<T>) layerPropertyBaseViewModel;
PropertyInputViewModel = propertyInputViewModel;
}
@@ -90,7 +90,7 @@
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem" BasedOn="{StaticResource PropertyTreeStyle}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="Visibility" Value="{Binding IsVisible}" />
<Setter Property="Visibility" Value="{Binding IsVisible, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
@@ -7,8 +7,11 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree
{
public class TreeViewModel
{
public TreeViewModel(BindableCollection<LayerPropertyGroupViewModel> layerPropertyGroups)
private readonly LayerPropertiesViewModel _layerPropertiesViewModel;
public TreeViewModel(LayerPropertiesViewModel layerPropertiesViewModel, BindableCollection<LayerPropertyGroupViewModel> layerPropertyGroups)
{
_layerPropertiesViewModel = layerPropertiesViewModel;
LayerPropertyGroups = layerPropertyGroups;
}