mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Layer properties - Added back most of the reworked VMs and views
This commit is contained in:
parent
7b238e241e
commit
ea66dcd39e
@ -8,10 +8,16 @@ namespace Artemis.Core.Models.Profile.LayerProperties
|
||||
/// </summary>
|
||||
public abstract class BaseLayerPropertyKeyframe
|
||||
{
|
||||
internal BaseLayerPropertyKeyframe()
|
||||
internal BaseLayerPropertyKeyframe(BaseLayerProperty baseLayerProperty)
|
||||
{
|
||||
BaseLayerProperty = baseLayerProperty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The base class of the layer property this keyframe is applied to
|
||||
/// </summary>
|
||||
public BaseLayerProperty BaseLayerProperty { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The position of this keyframe in the timeline
|
||||
/// </summary>
|
||||
@ -20,8 +26,6 @@ namespace Artemis.Core.Models.Profile.LayerProperties
|
||||
/// <summary>
|
||||
/// The easing function applied on the value of the keyframe
|
||||
/// </summary>
|
||||
public abstract Easings.Functions EasingFunction { get; set; }
|
||||
|
||||
internal abstract BaseLayerProperty BaseLayerProperty { get; }
|
||||
public Easings.Functions EasingFunction { get; set; }
|
||||
}
|
||||
}
|
||||
@ -90,7 +90,7 @@ namespace Artemis.Core.Models.Profile.LayerProperties
|
||||
var currentKeyframe = Keyframes.FirstOrDefault(k => k.Position == time.Value);
|
||||
// Create a new keyframe if none found
|
||||
if (currentKeyframe == null)
|
||||
AddKeyframe(new LayerPropertyKeyframe<T>(value, time.Value, Easings.Functions.Linear));
|
||||
AddKeyframe(new LayerPropertyKeyframe<T>(value, time.Value, Easings.Functions.Linear, this));
|
||||
else
|
||||
currentKeyframe.Value = value;
|
||||
|
||||
@ -105,20 +105,47 @@ namespace Artemis.Core.Models.Profile.LayerProperties
|
||||
/// <param name="keyframe">The keyframe to add</param>
|
||||
public void AddKeyframe(LayerPropertyKeyframe<T> keyframe)
|
||||
{
|
||||
if (_keyframes.Contains(keyframe))
|
||||
return;
|
||||
|
||||
keyframe.LayerProperty?.RemoveKeyframe(keyframe);
|
||||
|
||||
keyframe.LayerProperty = this;
|
||||
keyframe.BaseLayerProperty = this;
|
||||
_keyframes.Add(keyframe);
|
||||
SortKeyframes();
|
||||
OnKeyframeAdded();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a keyframe from the layer property
|
||||
/// </summary>
|
||||
/// <param name="keyframe">The keyframe to remove</param>
|
||||
public LayerPropertyKeyframe<T> CopyKeyframe(LayerPropertyKeyframe<T> keyframe)
|
||||
{
|
||||
var newKeyframe = new LayerPropertyKeyframe<T>(
|
||||
keyframe.Value,
|
||||
keyframe.Position,
|
||||
keyframe.EasingFunction,
|
||||
keyframe.LayerProperty
|
||||
);
|
||||
AddKeyframe(newKeyframe);
|
||||
|
||||
return newKeyframe;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a keyframe from the layer property
|
||||
/// </summary>
|
||||
/// <param name="keyframe">The keyframe to remove</param>
|
||||
public void RemoveKeyframe(LayerPropertyKeyframe<T> keyframe)
|
||||
{
|
||||
if (!_keyframes.Contains(keyframe))
|
||||
return;
|
||||
|
||||
_keyframes.Remove(keyframe);
|
||||
keyframe.LayerProperty = null;
|
||||
keyframe.BaseLayerProperty = null;
|
||||
SortKeyframes();
|
||||
OnKeyframeRemoved();
|
||||
}
|
||||
@ -213,8 +240,9 @@ namespace Artemis.Core.Models.Profile.LayerProperties
|
||||
_keyframes.AddRange(entity.KeyframeEntities.Select(k => new LayerPropertyKeyframe<T>(
|
||||
JsonConvert.DeserializeObject<T>(k.Value),
|
||||
k.Position,
|
||||
(Easings.Functions) k.EasingFunction)
|
||||
));
|
||||
(Easings.Functions) k.EasingFunction,
|
||||
this
|
||||
)));
|
||||
}
|
||||
catch (JsonException e)
|
||||
{
|
||||
|
||||
@ -7,10 +7,11 @@ namespace Artemis.Core.Models.Profile.LayerProperties
|
||||
{
|
||||
private TimeSpan _position;
|
||||
|
||||
public LayerPropertyKeyframe(T value, TimeSpan position, Easings.Functions easingFunction)
|
||||
public LayerPropertyKeyframe(T value, TimeSpan position, Easings.Functions easingFunction, LayerProperty<T> layerProperty) : base(layerProperty)
|
||||
{
|
||||
_position = position;
|
||||
Value = value;
|
||||
LayerProperty = layerProperty;
|
||||
EasingFunction = easingFunction;
|
||||
}
|
||||
|
||||
@ -34,10 +35,5 @@ namespace Artemis.Core.Models.Profile.LayerProperties
|
||||
LayerProperty.SortKeyframes();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed override Easings.Functions EasingFunction { get; set; }
|
||||
|
||||
internal override BaseLayerProperty BaseLayerProperty => LayerProperty;
|
||||
}
|
||||
}
|
||||
@ -10,29 +10,38 @@
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="163.274" d:DesignWidth="254.425"
|
||||
d:DataContext="{d:DesignInstance dialogs:ExceptionDialogViewModel}">
|
||||
<StackPanel Margin="16">
|
||||
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" Text="{Binding Header}" TextWrapping="Wrap" />
|
||||
|
||||
<StackPanel Orientation="Vertical" HorizontalAlignment="Right" Margin="16">
|
||||
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" Text="{Binding Header}"
|
||||
TextWrapping="Wrap" />
|
||||
<Separator Margin="0 15" />
|
||||
<TextBlock Style="{StaticResource MaterialDesignBody1TextBlock}" FontWeight="Bold" Margin="22 0">Exception message</TextBlock>
|
||||
<TextBlock Style="{StaticResource MaterialDesignBody1TextBlock}" HorizontalAlignment="Left" Text="{Binding Exception.Message}" TextWrapping="Wrap" Margin="22 5" MaxWidth="1000" />
|
||||
<Separator Margin="0 15" />
|
||||
<TextBlock Style="{StaticResource MaterialDesignBody1TextBlock}" Text="Stack trace" TextWrapping="Wrap" FontWeight="Bold" Margin="22 0" />
|
||||
<ScrollViewer MaxHeight="500">
|
||||
<StackPanel>
|
||||
<ItemsControl ItemsSource="{Binding Exceptions}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource MaterialDesignBody1TextBlock}" Text="Stack trace"
|
||||
TextWrapping="Wrap" FontWeight="Bold"/>
|
||||
|
||||
<avalonedit:TextEditor SyntaxHighlighting="C#"
|
||||
FontFamily="pack://application:,,,/Resources/Fonts/#Roboto Mono"
|
||||
FontSize="10pt"
|
||||
IsReadOnly="True"
|
||||
Document="{Binding Document}"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
MaxWidth="1000"
|
||||
Margin="0 10" />
|
||||
<avalonedit:TextEditor SyntaxHighlighting="C#"
|
||||
FontFamily="pack://application:,,,/Resources/Fonts/#Roboto Mono"
|
||||
FontSize="10pt"
|
||||
IsReadOnly="True"
|
||||
Document="{Binding Document}"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
MaxWidth="1000"
|
||||
Margin="0 10 10 0"
|
||||
Padding="10"/>
|
||||
|
||||
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Style="{StaticResource MaterialDesignFlatButton}" IsDefault="True" Margin="0 8 0 0"
|
||||
Command="{s:Action Close}" Content="Close" />
|
||||
</StackPanel>
|
||||
<Separator Margin="0 15" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
<Button Style="{StaticResource MaterialDesignFlatButton}" IsDefault="True" Margin="0 8 0 0"
|
||||
Command="{s:Action Close}" Content="Close" HorizontalAlignment="Right" />
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Artemis.UI.Shared.Services.Dialog;
|
||||
using ICSharpCode.AvalonEdit;
|
||||
using ICSharpCode.AvalonEdit.Document;
|
||||
|
||||
namespace Artemis.UI.Shared.Screens.Dialogs
|
||||
@ -10,18 +10,35 @@ namespace Artemis.UI.Shared.Screens.Dialogs
|
||||
public ExceptionDialogViewModel(string message, Exception exception)
|
||||
{
|
||||
Header = message;
|
||||
Exception = exception;
|
||||
Document = new TextDocument(new StringTextSource(exception.StackTrace));
|
||||
Exceptions = new List<DialogException>();
|
||||
|
||||
var currentException = exception;
|
||||
while (currentException != null)
|
||||
{
|
||||
Exceptions.Add(new DialogException(currentException));
|
||||
currentException = currentException.InnerException;
|
||||
}
|
||||
}
|
||||
|
||||
public string Header { get; }
|
||||
public Exception Exception { get; }
|
||||
public List<DialogException> Exceptions { get; set; }
|
||||
|
||||
public IDocument Document { get; set; }
|
||||
|
||||
public void Close()
|
||||
{
|
||||
Session.Close();
|
||||
}
|
||||
}
|
||||
|
||||
public class DialogException
|
||||
{
|
||||
public Exception Exception { get; }
|
||||
public IDocument Document { get; set; }
|
||||
|
||||
public DialogException(Exception exception)
|
||||
{
|
||||
Exception = exception;
|
||||
Document = new TextDocument(new StringTextSource($"{exception.Message}\r\n\r\n{exception.StackTrace}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,6 @@ using Artemis.UI.Events;
|
||||
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties;
|
||||
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Abstract;
|
||||
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree;
|
||||
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree.PropertyInput.Abstract;
|
||||
|
||||
namespace Artemis.UI.Services.Interfaces
|
||||
{
|
||||
@ -16,6 +15,7 @@ namespace Artemis.UI.Services.Interfaces
|
||||
Profile SelectedProfile { get; }
|
||||
ProfileElement SelectedProfileElement { get; }
|
||||
TimeSpan CurrentTime { get; set; }
|
||||
int PixelsPerSecond { get; set; }
|
||||
|
||||
LayerPropertyBaseViewModel CreateLayerPropertyViewModel(BaseLayerProperty baseLayerProperty, PropertyDescriptionAttribute propertyDescription);
|
||||
void ChangeSelectedProfile(Profile profile);
|
||||
@ -53,6 +53,11 @@ namespace Artemis.UI.Services.Interfaces
|
||||
/// </summary>
|
||||
event EventHandler CurrentTimeChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the pixels per second (zoom level) is changed
|
||||
/// </summary>
|
||||
event EventHandler PixelsPerSecondChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the profile preview has been updated
|
||||
/// </summary>
|
||||
|
||||
@ -29,6 +29,7 @@ namespace Artemis.UI.Services
|
||||
private readonly IKernel _kernel;
|
||||
private TimeSpan _currentTime;
|
||||
private TimeSpan _lastUpdateTime;
|
||||
private int _pixelsPerSecond;
|
||||
|
||||
public ProfileEditorService(ICoreService coreService, IProfileService profileService, IKernel kernel)
|
||||
{
|
||||
@ -46,6 +47,7 @@ namespace Artemis.UI.Services
|
||||
{typeof(SKPoint), typeof(SKPointPropertyInputViewModel)},
|
||||
{typeof(SKSize), typeof(SKSizePropertyInputViewModel)}
|
||||
};
|
||||
PixelsPerSecond = 31;
|
||||
}
|
||||
|
||||
public Dictionary<Type, Type> RegisteredPropertyEditors { get; set; }
|
||||
@ -66,6 +68,17 @@ namespace Artemis.UI.Services
|
||||
}
|
||||
}
|
||||
|
||||
public int PixelsPerSecond
|
||||
{
|
||||
get => _pixelsPerSecond;
|
||||
set
|
||||
{
|
||||
_pixelsPerSecond = value;
|
||||
OnPixelsPerSecondChanged();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public LayerPropertyBaseViewModel CreateLayerPropertyViewModel(BaseLayerProperty baseLayerProperty, PropertyDescriptionAttribute propertyDescription)
|
||||
{
|
||||
// Go through the pain of instantiating a generic type VM now via reflection to make things a lot simpler down the line
|
||||
@ -93,7 +106,7 @@ namespace Artemis.UI.Services
|
||||
{
|
||||
new ConstructorArgument("layerPropertyViewModel", layerPropertyViewModel)
|
||||
};
|
||||
return new TreePropertyViewModel<T>(layerPropertyViewModel, (PropertyInputViewModel<T>) _kernel.Get(type, parameters));
|
||||
return new TreePropertyViewModel<T>(layerPropertyViewModel, (PropertyInputViewModel<T>) _kernel.Get(type, parameters), this);
|
||||
}
|
||||
|
||||
public void ChangeSelectedProfile(Profile profile)
|
||||
@ -181,8 +194,9 @@ namespace Artemis.UI.Services
|
||||
public event EventHandler<ProfileElementEventArgs> ProfileElementSelected;
|
||||
public event EventHandler<ProfileElementEventArgs> SelectedProfileElementUpdated;
|
||||
public event EventHandler CurrentTimeChanged;
|
||||
public event EventHandler PixelsPerSecondChanged;
|
||||
public event EventHandler ProfilePreviewUpdated;
|
||||
|
||||
|
||||
public void StopRegularRender()
|
||||
{
|
||||
_coreService.ModuleUpdatingDisabled = true;
|
||||
@ -218,6 +232,11 @@ namespace Artemis.UI.Services
|
||||
CurrentTimeChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
protected virtual void OnPixelsPerSecondChanged()
|
||||
{
|
||||
PixelsPerSecondChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
protected virtual void OnProfilePreviewUpdated()
|
||||
{
|
||||
ProfilePreviewUpdated?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user