1
0
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:
SpoinkyNL 2020-05-18 23:56:43 +02:00
parent 7b238e241e
commit ea66dcd39e
21 changed files with 862 additions and 146 deletions

View File

@ -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; }
}
}

View File

@ -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)
{

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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}"));
}
}
}

View File

@ -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
}
}

View File

@ -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;
}
}
}

View File

@ -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; }
}
}

View File

@ -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)

View File

@ -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));
}
}
}
}

View File

@ -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
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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
}
}

View File

@ -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;
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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>

View File

@ -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);