mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-12 11:56:30 +00:00
Profile editor - Moved files
This commit is contained in:
1 parent
0516ccc658
commit
698d1b8bbd
86 files changed
+199
-215
No files matched your search
+179
@@ -0,0 +1,179 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.Controls
|
||||
{
|
||||
public class PropertyTimelineHeader : FrameworkElement
|
||||
{
|
||||
public static readonly DependencyProperty FillProperty = DependencyProperty.Register(nameof(Fill), typeof(Brush), typeof(PropertyTimelineHeader),
|
||||
new FrameworkPropertyMetadata(default(Brush), FrameworkPropertyMetadataOptions.AffectsRender));
|
||||
|
||||
public static readonly DependencyProperty FontFamilyProperty = DependencyProperty.Register(nameof(FontFamily), typeof(FontFamily), typeof(PropertyTimelineHeader),
|
||||
new FrameworkPropertyMetadata(TextBlock.FontFamilyProperty.DefaultMetadata.DefaultValue, FrameworkPropertyMetadataOptions.AffectsRender));
|
||||
|
||||
public static readonly DependencyProperty PixelsPerSecondProperty = DependencyProperty.Register(nameof(PixelsPerSecond), typeof(int), typeof(PropertyTimelineHeader),
|
||||
new FrameworkPropertyMetadata(default(int), FrameworkPropertyMetadataOptions.AffectsRender));
|
||||
|
||||
public static readonly DependencyProperty HorizontalOffsetProperty = DependencyProperty.Register(nameof(HorizontalOffset), typeof(double), typeof(PropertyTimelineHeader),
|
||||
new FrameworkPropertyMetadata(default(double), FrameworkPropertyMetadataOptions.AffectsRender));
|
||||
|
||||
public static readonly DependencyProperty VisibleWidthProperty = DependencyProperty.Register(nameof(VisibleWidth), typeof(double), typeof(PropertyTimelineHeader),
|
||||
new FrameworkPropertyMetadata(default(double), FrameworkPropertyMetadataOptions.AffectsRender));
|
||||
|
||||
public static readonly DependencyProperty OffsetFirstValueProperty = DependencyProperty.Register(nameof(OffsetFirstValue), typeof(bool), typeof(PropertyTimelineHeader),
|
||||
new FrameworkPropertyMetadata(default(bool), FrameworkPropertyMetadataOptions.AffectsRender));
|
||||
|
||||
private double _subd1;
|
||||
private double _subd2;
|
||||
private double _subd3;
|
||||
|
||||
public Brush Fill
|
||||
{
|
||||
get => (Brush) GetValue(FillProperty);
|
||||
set => SetValue(FillProperty, value);
|
||||
}
|
||||
|
||||
public FontFamily FontFamily
|
||||
{
|
||||
get => (FontFamily) GetValue(FontFamilyProperty);
|
||||
set => SetValue(FontFamilyProperty, value);
|
||||
}
|
||||
|
||||
public int PixelsPerSecond
|
||||
{
|
||||
get => (int) GetValue(PixelsPerSecondProperty);
|
||||
set => SetValue(PixelsPerSecondProperty, value);
|
||||
}
|
||||
|
||||
public double HorizontalOffset
|
||||
{
|
||||
get => (double) GetValue(HorizontalOffsetProperty);
|
||||
set => SetValue(HorizontalOffsetProperty, value);
|
||||
}
|
||||
|
||||
public double VisibleWidth
|
||||
{
|
||||
get => (double) GetValue(VisibleWidthProperty);
|
||||
set => SetValue(VisibleWidthProperty, value);
|
||||
}
|
||||
|
||||
public bool OffsetFirstValue
|
||||
{
|
||||
get => (bool) GetValue(OffsetFirstValueProperty);
|
||||
set => SetValue(OffsetFirstValueProperty, value);
|
||||
}
|
||||
|
||||
protected override void OnRender(DrawingContext drawingContext)
|
||||
{
|
||||
base.OnRender(drawingContext);
|
||||
UpdateTimeScale();
|
||||
|
||||
var linePen = new Pen(Fill, 1);
|
||||
var width = HorizontalOffset + VisibleWidth;
|
||||
var frameStart = 0;
|
||||
|
||||
var units = PixelsPerSecond / _subd1;
|
||||
var offsetUnits = frameStart * PixelsPerSecond % units;
|
||||
|
||||
// Labels
|
||||
var count = (width + offsetUnits) / units;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var x = i * units - offsetUnits;
|
||||
// Add a 100px margin to allow the text to partially render when needed
|
||||
if (x < HorizontalOffset - 100 || x > HorizontalOffset + width)
|
||||
continue;
|
||||
|
||||
var t = TimeSpan.FromSeconds((i * units - offsetUnits) / PixelsPerSecond + frameStart);
|
||||
// 0.00 is always formatted as 0.00
|
||||
if (t == TimeSpan.Zero)
|
||||
RenderLabel(drawingContext, "0.00", x);
|
||||
else if (PixelsPerSecond > 200)
|
||||
RenderLabel(drawingContext, $"{Math.Floor(t.TotalSeconds):00}.{t.Milliseconds:000}", x);
|
||||
else if (PixelsPerSecond > 60)
|
||||
RenderLabel(drawingContext, $"{Math.Floor(t.TotalSeconds):00}.{t.Milliseconds:000}", x);
|
||||
else
|
||||
RenderLabel(drawingContext, $"{Math.Floor(t.TotalMinutes):0}:{t.Seconds:00}", x);
|
||||
}
|
||||
|
||||
// Large ticks
|
||||
units = PixelsPerSecond / _subd2;
|
||||
count = (width + offsetUnits) / units;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var x = i * units - offsetUnits;
|
||||
if (x == 0 && OffsetFirstValue)
|
||||
drawingContext.DrawLine(linePen, new Point(1, 20), new Point(1, 30));
|
||||
else if (x > HorizontalOffset && x < HorizontalOffset + width)
|
||||
drawingContext.DrawLine(linePen, new Point(x, 20), new Point(x, 30));
|
||||
}
|
||||
|
||||
// Small ticks
|
||||
var mul = _subd3 / _subd2;
|
||||
units = PixelsPerSecond / _subd3;
|
||||
count = (width + offsetUnits) / units;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
if (Math.Abs(i % mul) < 0.001) continue;
|
||||
var x = i * units - offsetUnits;
|
||||
if (x > HorizontalOffset && x < HorizontalOffset + width)
|
||||
drawingContext.DrawLine(linePen, new Point(x, 25), new Point(x, 30));
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderLabel(DrawingContext drawingContext, string text, double x)
|
||||
{
|
||||
var typeFace = new Typeface(FontFamily, new FontStyle(), new FontWeight(), new FontStretch());
|
||||
var formattedText = new FormattedText(text, CultureInfo.CurrentUICulture, FlowDirection.LeftToRight, typeFace, 9, Fill, null, VisualTreeHelper.GetDpi(this).PixelsPerDip);
|
||||
if (x == 0 && OffsetFirstValue)
|
||||
drawingContext.DrawText(formattedText, new Point(2, 2));
|
||||
else
|
||||
drawingContext.DrawText(formattedText, new Point(x - formattedText.Width / 2, 2));
|
||||
}
|
||||
|
||||
private void UpdateTimeScale()
|
||||
{
|
||||
object[] subds;
|
||||
if (PixelsPerSecond > 350)
|
||||
subds = new object[] {12d, 12d, 60d};
|
||||
else if (PixelsPerSecond > 250)
|
||||
subds = new object[] {6d, 12d, 60d};
|
||||
else if (PixelsPerSecond > 200)
|
||||
subds = new object[] {6d, 6d, 30d};
|
||||
else if (PixelsPerSecond > 150)
|
||||
subds = new object[] {4d, 4d, 20d};
|
||||
else if (PixelsPerSecond > 140)
|
||||
subds = new object[] {4d, 4d, 20d};
|
||||
else if (PixelsPerSecond > 90)
|
||||
subds = new object[] {2d, 4d, 20d};
|
||||
else if (PixelsPerSecond > 60)
|
||||
subds = new object[] {2d, 4d, 8d};
|
||||
else if (PixelsPerSecond > 40)
|
||||
subds = new object[] {1d, 2d, 10d};
|
||||
else if (PixelsPerSecond > 30)
|
||||
subds = new object[] {1d, 2d, 10d};
|
||||
else if (PixelsPerSecond > 10)
|
||||
subds = new object[] {1d / 2d, 1d / 2d, 1d / 2d};
|
||||
else if (PixelsPerSecond > 4)
|
||||
subds = new object[] {1d / 5d, 1d / 5d, 1d / 5d};
|
||||
else if (PixelsPerSecond > 3)
|
||||
subds = new object[] {1d / 10d, 1d / 10d, 1d / 5d};
|
||||
else if (PixelsPerSecond > 1)
|
||||
subds = new object[] {1d / 20d, 1d / 20d, 1d / 10d};
|
||||
else if (PixelsPerSecond >= 1)
|
||||
subds = new object[] {1d / 30d, 1d / 30d, 1d / 15d};
|
||||
else
|
||||
{
|
||||
// 1s per pixel
|
||||
subds = new object[] {1d / 60d, 1d / 60d, 1d / 15d};
|
||||
}
|
||||
|
||||
_subd1 = (double) subds[0]; // big ticks / labels
|
||||
_subd2 = (double) subds[1]; // medium ticks
|
||||
_subd3 = (double) subds[2]; // small ticks
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using Artemis.Core.Utilities;
|
||||
using Humanizer;
|
||||
|
||||
namespace Artemis.UI.Screens.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();
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
public Easings.Functions EasingFunction { get; }
|
||||
public PointCollection EasingPoints { get; }
|
||||
public string Description { get; }
|
||||
|
||||
public bool IsEasingModeSelected
|
||||
{
|
||||
get => _isEasingModeSelected;
|
||||
set
|
||||
{
|
||||
_isEasingModeSelected = value;
|
||||
if (_isEasingModeSelected)
|
||||
_keyframeViewModel.SelectEasingMode(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using Artemis.Core.Models.Profile.LayerProperties;
|
||||
using Artemis.Core.Utilities;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
|
||||
{
|
||||
public class TimelineKeyframeViewModel<T> : TimelineKeyframeViewModel
|
||||
{
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
|
||||
public TimelineKeyframeViewModel(IProfileEditorService profileEditorService, LayerPropertyKeyframe<T> layerPropertyKeyframe)
|
||||
: base(profileEditorService, layerPropertyKeyframe)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
LayerPropertyKeyframe = layerPropertyKeyframe;
|
||||
}
|
||||
|
||||
public LayerPropertyKeyframe<T> LayerPropertyKeyframe { get; }
|
||||
|
||||
#region Context menu actions
|
||||
|
||||
public override void Copy()
|
||||
{
|
||||
var newKeyframe = new LayerPropertyKeyframe<T>(
|
||||
LayerPropertyKeyframe.Value,
|
||||
LayerPropertyKeyframe.Position,
|
||||
LayerPropertyKeyframe.EasingFunction,
|
||||
LayerPropertyKeyframe.LayerProperty
|
||||
);
|
||||
// If possible, shift the keyframe to the right by 11 pixels
|
||||
var desiredPosition = newKeyframe.Position + TimeSpan.FromMilliseconds(1000f / _profileEditorService.PixelsPerSecond * 11);
|
||||
if (desiredPosition <= newKeyframe.LayerProperty.ProfileElement.TimelineLength)
|
||||
newKeyframe.Position = desiredPosition;
|
||||
// Otherwise if possible shift it to the left by 11 pixels
|
||||
else
|
||||
{
|
||||
desiredPosition = newKeyframe.Position - TimeSpan.FromMilliseconds(1000f / _profileEditorService.PixelsPerSecond * 11);
|
||||
if (desiredPosition > TimeSpan.Zero)
|
||||
newKeyframe.Position = desiredPosition;
|
||||
}
|
||||
|
||||
LayerPropertyKeyframe.LayerProperty.AddKeyframe(newKeyframe);
|
||||
_profileEditorService.UpdateSelectedProfileElement();
|
||||
}
|
||||
|
||||
public override void Delete()
|
||||
{
|
||||
LayerPropertyKeyframe.LayerProperty.RemoveKeyframe(LayerPropertyKeyframe);
|
||||
_profileEditorService.UpdateSelectedProfileElement();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public abstract class TimelineKeyframeViewModel : PropertyChangedBase, IDisposable
|
||||
{
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private BindableCollection<TimelineEasingViewModel> _easingViewModels;
|
||||
private bool _isSelected;
|
||||
private int _pixelsPerSecond;
|
||||
private string _timestamp;
|
||||
private double _x;
|
||||
|
||||
protected TimelineKeyframeViewModel(IProfileEditorService profileEditorService, BaseLayerPropertyKeyframe baseLayerPropertyKeyframe)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
BaseLayerPropertyKeyframe = baseLayerPropertyKeyframe;
|
||||
EasingViewModels = new BindableCollection<TimelineEasingViewModel>();
|
||||
|
||||
BaseLayerPropertyKeyframe.PropertyChanged += BaseLayerPropertyKeyframeOnPropertyChanged;
|
||||
}
|
||||
|
||||
public BaseLayerPropertyKeyframe BaseLayerPropertyKeyframe { get; }
|
||||
|
||||
public BindableCollection<TimelineEasingViewModel> EasingViewModels
|
||||
{
|
||||
get => _easingViewModels;
|
||||
set => SetAndNotify(ref _easingViewModels, value);
|
||||
}
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
set => SetAndNotify(ref _isSelected, value);
|
||||
}
|
||||
|
||||
public double X
|
||||
{
|
||||
get => _x;
|
||||
set => SetAndNotify(ref _x, value);
|
||||
}
|
||||
|
||||
public string Timestamp
|
||||
{
|
||||
get => _timestamp;
|
||||
set => SetAndNotify(ref _timestamp, value);
|
||||
}
|
||||
|
||||
public void Update(int pixelsPerSecond)
|
||||
{
|
||||
_pixelsPerSecond = pixelsPerSecond;
|
||||
|
||||
X = pixelsPerSecond * BaseLayerPropertyKeyframe.Position.TotalSeconds;
|
||||
Timestamp = $"{Math.Floor(BaseLayerPropertyKeyframe.Position.TotalSeconds):00}.{BaseLayerPropertyKeyframe.Position.Milliseconds:000}";
|
||||
}
|
||||
|
||||
public abstract void Copy();
|
||||
|
||||
public abstract void Delete();
|
||||
|
||||
private void BaseLayerPropertyKeyframeOnPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(BaseLayerPropertyKeyframe.Position))
|
||||
Update(_pixelsPerSecond);
|
||||
}
|
||||
|
||||
#region Easing
|
||||
|
||||
public 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 TimeSpan? _offset;
|
||||
|
||||
public void ReleaseMovement()
|
||||
{
|
||||
_offset = null;
|
||||
}
|
||||
|
||||
public void SaveOffsetToKeyframe(TimelineKeyframeViewModel keyframeViewModel)
|
||||
{
|
||||
if (keyframeViewModel == this)
|
||||
{
|
||||
_offset = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_offset != null)
|
||||
return;
|
||||
|
||||
_offset = BaseLayerPropertyKeyframe.Position - keyframeViewModel.BaseLayerPropertyKeyframe.Position;
|
||||
}
|
||||
|
||||
public void ApplyOffsetToKeyframe(TimelineKeyframeViewModel keyframeViewModel)
|
||||
{
|
||||
if (keyframeViewModel == this || _offset == null)
|
||||
return;
|
||||
|
||||
UpdatePosition(keyframeViewModel.BaseLayerPropertyKeyframe.Position + _offset.Value);
|
||||
}
|
||||
|
||||
public void UpdatePosition(TimeSpan position)
|
||||
{
|
||||
if (position < TimeSpan.Zero)
|
||||
BaseLayerPropertyKeyframe.Position = TimeSpan.Zero;
|
||||
else if (position > _profileEditorService.SelectedProfileElement.TimelineLength)
|
||||
BaseLayerPropertyKeyframe.Position = _profileEditorService.SelectedProfileElement.TimelineLength;
|
||||
else
|
||||
BaseLayerPropertyKeyframe.Position = position;
|
||||
|
||||
Update(_pixelsPerSecond);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
BaseLayerPropertyKeyframe.PropertyChanged -= BaseLayerPropertyKeyframeOnPropertyChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.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.ProfileEditor.LayerProperties.Timeline"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:layerProperties="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:layerProperties1="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance local:TimelinePropertyGroupViewModel}"
|
||||
Visibility="{Binding LayerPropertyGroupViewModel.IsVisible, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="24" />
|
||||
<RowDefinition Height="1" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ItemsControl Grid.Row="0"
|
||||
Height="24"
|
||||
Visibility="{Binding LayerPropertyGroupViewModel.IsExpanded, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}"
|
||||
ItemsSource="{Binding TimelineKeyframeViewModels}"
|
||||
Background="{DynamicResource MaterialDesignToolBarBackground}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Canvas />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemContainerStyle>
|
||||
<Style TargetType="{x:Type ContentPresenter}">
|
||||
<Setter Property="Canvas.Left" Value="{Binding}" />
|
||||
</Style>
|
||||
</ItemsControl.ItemContainerStyle>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Ellipse Fill="{StaticResource MaterialDesignCheckBoxDisabled}"
|
||||
Stroke="White"
|
||||
StrokeThickness="0"
|
||||
Width="10"
|
||||
Height="10"
|
||||
Margin="-5,6,0,0" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Rectangle Grid.Row="1" HorizontalAlignment="Stretch" Fill="{DynamicResource MaterialDesignDivider}" Height="1" />
|
||||
|
||||
<ItemsControl Grid.Row="2"
|
||||
ItemsSource="{Binding LayerPropertyGroupViewModel.Children}"
|
||||
Visibility="{Binding LayerPropertyGroupViewModel.IsExpanded, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch">
|
||||
<ItemsControl.ItemContainerStyle>
|
||||
<Style TargetType="ContentPresenter">
|
||||
<Setter Property="VerticalAlignment" Value="Stretch" />
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
</Style >
|
||||
</ItemsControl.ItemContainerStyle>
|
||||
<ItemsControl.Resources>
|
||||
<DataTemplate DataType="{x:Type layerProperties1:LayerPropertyGroupViewModel}">
|
||||
<ContentControl s:View.Model="{Binding TimelinePropertyGroupViewModel}" IsTabStop="False" HorizontalAlignment="Stretch"/>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="{x:Type layerProperties1:LayerPropertyViewModel}">
|
||||
<ContentControl s:View.Model="{Binding TimelinePropertyBaseViewModel}" IsTabStop="False" HorizontalAlignment="Stretch" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.Resources>
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Abstract;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
|
||||
{
|
||||
public class TimelinePropertyGroupViewModel : PropertyChangedBase
|
||||
{
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private BindableCollection<double> _timelineKeyframeViewModels;
|
||||
|
||||
public TimelinePropertyGroupViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel, IProfileEditorService profileEditorService)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
LayerPropertyGroupViewModel = (LayerPropertyGroupViewModel) layerPropertyBaseViewModel;
|
||||
TimelineKeyframeViewModels = new BindableCollection<double>();
|
||||
|
||||
LayerPropertyGroupViewModel.ProfileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
|
||||
LayerPropertyGroupViewModel.PropertyChanged += LayerPropertyGroupViewModelOnPropertyChanged;
|
||||
UpdateKeyframes();
|
||||
}
|
||||
|
||||
public LayerPropertyGroupViewModel LayerPropertyGroupViewModel { get; }
|
||||
|
||||
public BindableCollection<double> TimelineKeyframeViewModels
|
||||
{
|
||||
get => _timelineKeyframeViewModels;
|
||||
set => SetAndNotify(ref _timelineKeyframeViewModels, value);
|
||||
}
|
||||
|
||||
public void UpdateKeyframes()
|
||||
{
|
||||
TimelineKeyframeViewModels.Clear();
|
||||
TimelineKeyframeViewModels.AddRange(LayerPropertyGroupViewModel.GetKeyframes(false)
|
||||
.Select(k => LayerPropertyGroupViewModel.ProfileEditorService.PixelsPerSecond * k.Position.TotalSeconds));
|
||||
}
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
LayerPropertyGroupViewModel.ProfileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged;
|
||||
LayerPropertyGroupViewModel.PropertyChanged -= LayerPropertyGroupViewModelOnPropertyChanged;
|
||||
}
|
||||
|
||||
private void LayerPropertyGroupViewModelOnPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(LayerPropertyGroupViewModel.IsExpanded))
|
||||
UpdateKeyframes();
|
||||
}
|
||||
|
||||
private void ProfileEditorServiceOnPixelsPerSecondChanged(object sender, EventArgs e)
|
||||
{
|
||||
UpdateKeyframes();
|
||||
}
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.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.ProfileEditor.LayerProperties.Timeline"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance local:TimelinePropertyViewModel}"
|
||||
Visibility="{Binding LayerPropertyBaseViewModel.IsVisible, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}"
|
||||
MinWidth="{Binding Width}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<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 RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type timeline:TimelineView}}, Path=DataContext}"
|
||||
MouseDown="{s:Action KeyframeMouseDown}"
|
||||
MouseUp="{s:Action KeyframeMouseUp}"
|
||||
MouseMove="{s:Action KeyframeMouseMove}"
|
||||
ContextMenuOpening="{s:Action ContextMenuOpening}"
|
||||
ContextMenuClosing="{s:Action ContextMenuClosing}">
|
||||
<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}" CommandParameter="{Binding}">
|
||||
<MenuItem.Icon>
|
||||
<materialDesign:PackIcon Kind="ContentCopy" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Delete" Command="{s:Action Delete}" CommandParameter="{Binding}">
|
||||
<MenuItem.Icon>
|
||||
<materialDesign:PackIcon Kind="Delete" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem Header="Easing" ItemsSource="{Binding EasingViewModels}">
|
||||
<MenuItem.Icon>
|
||||
<materialDesign:PackIcon Kind="Creation" />
|
||||
</MenuItem.Icon>
|
||||
<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>
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Abstract;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
|
||||
{
|
||||
public class TimelinePropertyViewModel<T> : TimelinePropertyViewModel
|
||||
{
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
|
||||
public TimelinePropertyViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel, IProfileEditorService profileEditorService) : base(layerPropertyBaseViewModel)
|
||||
{
|
||||
_profileEditorService = profileEditorService;
|
||||
LayerPropertyViewModel = (LayerPropertyViewModel<T>) layerPropertyBaseViewModel;
|
||||
|
||||
LayerPropertyViewModel.LayerProperty.KeyframeAdded += LayerPropertyOnKeyframeModified;
|
||||
LayerPropertyViewModel.LayerProperty.KeyframeRemoved += LayerPropertyOnKeyframeModified;
|
||||
LayerPropertyViewModel.LayerProperty.KeyframesToggled += LayerPropertyOnKeyframeModified;
|
||||
_profileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
|
||||
}
|
||||
|
||||
public LayerPropertyViewModel<T> LayerPropertyViewModel { get; }
|
||||
|
||||
public override void UpdateKeyframes()
|
||||
{
|
||||
// Only show keyframes if they are enabled
|
||||
if (LayerPropertyViewModel.LayerProperty.KeyframesEnabled)
|
||||
{
|
||||
var keyframes = LayerPropertyViewModel.LayerProperty.Keyframes.ToList();
|
||||
var toRemove = TimelineKeyframeViewModels.Where(t => !keyframes.Contains(t.BaseLayerPropertyKeyframe)).ToList();
|
||||
foreach (var timelineKeyframeViewModel in toRemove)
|
||||
timelineKeyframeViewModel.Dispose();
|
||||
|
||||
TimelineKeyframeViewModels.RemoveRange(toRemove);
|
||||
TimelineKeyframeViewModels.AddRange(keyframes
|
||||
.Where(k => TimelineKeyframeViewModels.All(t => t.BaseLayerPropertyKeyframe != k))
|
||||
.Select(k => new TimelineKeyframeViewModel<T>(_profileEditorService, k))
|
||||
);
|
||||
}
|
||||
else
|
||||
DisposeKeyframeViewModels();
|
||||
|
||||
foreach (var timelineKeyframeViewModel in TimelineKeyframeViewModels)
|
||||
timelineKeyframeViewModel.Update(_profileEditorService.PixelsPerSecond);
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
_profileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged;
|
||||
LayerPropertyViewModel.LayerProperty.KeyframeAdded -= LayerPropertyOnKeyframeModified;
|
||||
LayerPropertyViewModel.LayerProperty.KeyframeRemoved -= LayerPropertyOnKeyframeModified;
|
||||
LayerPropertyViewModel.LayerProperty.KeyframesToggled -= LayerPropertyOnKeyframeModified;
|
||||
DisposeKeyframeViewModels();
|
||||
}
|
||||
|
||||
private void DisposeKeyframeViewModels()
|
||||
{
|
||||
foreach (var timelineKeyframeViewModel in TimelineKeyframeViewModels)
|
||||
timelineKeyframeViewModel.Dispose();
|
||||
TimelineKeyframeViewModels.Clear();
|
||||
}
|
||||
|
||||
private void LayerPropertyOnKeyframeModified(object sender, EventArgs e)
|
||||
{
|
||||
UpdateKeyframes();
|
||||
}
|
||||
|
||||
private void ProfileEditorServiceOnPixelsPerSecondChanged(object sender, EventArgs e)
|
||||
{
|
||||
foreach (var timelineKeyframeViewModel in TimelineKeyframeViewModels)
|
||||
timelineKeyframeViewModel.Update(_profileEditorService.PixelsPerSecond);
|
||||
|
||||
Width = TimelineKeyframeViewModels.Any() ? TimelineKeyframeViewModels.Max(t => t.X) + 25 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class TimelinePropertyViewModel : PropertyChangedBase, IDisposable
|
||||
{
|
||||
private BindableCollection<TimelineKeyframeViewModel> _timelineKeyframeViewModels;
|
||||
private double _width;
|
||||
|
||||
protected TimelinePropertyViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel)
|
||||
{
|
||||
LayerPropertyBaseViewModel = layerPropertyBaseViewModel;
|
||||
TimelineKeyframeViewModels = new BindableCollection<TimelineKeyframeViewModel>();
|
||||
}
|
||||
|
||||
public LayerPropertyBaseViewModel LayerPropertyBaseViewModel { get; }
|
||||
|
||||
public BindableCollection<TimelineKeyframeViewModel> TimelineKeyframeViewModels
|
||||
{
|
||||
get => _timelineKeyframeViewModels;
|
||||
set => SetAndNotify(ref _timelineKeyframeViewModels, value);
|
||||
}
|
||||
|
||||
public double Width
|
||||
{
|
||||
get => _width;
|
||||
set => SetAndNotify(ref _width, value);
|
||||
}
|
||||
|
||||
public abstract void Dispose();
|
||||
|
||||
public abstract void UpdateKeyframes();
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.TimelineSegmentView"
|
||||
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.ProfileEditor.LayerProperties.Timeline"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:utilities="clr-namespace:Artemis.UI.Utilities"
|
||||
xmlns:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance {x:Type timeline:TimelineSegmentViewModel}}">
|
||||
<UserControl.Resources>
|
||||
<utilities:BindingProxy x:Key="DataContextProxy" Data="{Binding}" />
|
||||
</UserControl.Resources>
|
||||
<StackPanel Orientation="Horizontal" Visibility="{Binding SegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
||||
<StackPanel.ContextMenu>
|
||||
<ContextMenu>
|
||||
<MenuItem Header="Repeat main segment"
|
||||
IsCheckable="True"
|
||||
IsChecked="{Binding Data.RepeatSegment, Source={StaticResource DataContextProxy}}"
|
||||
Visibility="{Binding Data.IsMainSegment, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Source={StaticResource DataContextProxy}}"/>
|
||||
<MenuItem Header="Disable segment" Command="{s:Action DisableSegment}"/>
|
||||
</ContextMenu>
|
||||
</StackPanel.ContextMenu>
|
||||
|
||||
<!-- Segment content -->
|
||||
<Grid VerticalAlignment="Top" Background="{StaticResource MaterialDesignPaper}" Width="{Binding SegmentWidth}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock TextAlignment="Center"
|
||||
Padding="0 3"
|
||||
Margin="5 0 0 0"
|
||||
FontSize="12"
|
||||
ToolTip="{Binding ToolTip}"
|
||||
Text="{Binding Segment}"
|
||||
ClipToBounds="False">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ShowSegmentName}" Value="False">
|
||||
<DataTrigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="1.0" To="0.0" Duration="0:0:0.25">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<QuadraticEase EasingMode="EaseInOut" />
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.EnterActions>
|
||||
<DataTrigger.ExitActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="0.0" To="1.0" Duration="0:0:0.25" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.ExitActions>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
|
||||
<ToggleButton Grid.Column="1"
|
||||
ToolTip="Toggle main segment repeat"
|
||||
Width="16"
|
||||
Height="16"
|
||||
IsChecked="{Binding RepeatSegment}"
|
||||
VerticalAlignment="Center"
|
||||
Visibility="{Binding IsMainSegment, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
||||
<ToggleButton.Style>
|
||||
<Style TargetType="ToggleButton" BasedOn="{StaticResource MaterialDesignFlatToggleButton}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ShowRepeatButton}" Value="False">
|
||||
<DataTrigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="1.0" To="0.0" Duration="0:0:0.25">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<QuadraticEase EasingMode="EaseInOut" />
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" >
|
||||
<DiscreteObjectKeyFrame KeyTime="00:00:0.25" Value="{x:Static Visibility.Collapsed}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.EnterActions>
|
||||
<DataTrigger.ExitActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)">
|
||||
<DiscreteObjectKeyFrame KeyTime="00:00:00" Value="{x:Static Visibility.Visible}"/>
|
||||
</ObjectAnimationUsingKeyFrames>
|
||||
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="0.0" To="1.0" Duration="0:0:0.25" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.ExitActions>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</ToggleButton.Style>
|
||||
<materialDesign:PackIcon Kind="Repeat" Height="12" Width="12" />
|
||||
</ToggleButton>
|
||||
|
||||
<Button Grid.Column="2" ToolTip="Disable segment" Width="20" Height="20" Margin="0 0 5 0" Command="{s:Action DisableSegment}">
|
||||
<Button.Style>
|
||||
<Style TargetType="Button" BasedOn="{StaticResource MaterialDesignIconButton}">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding ShowDisableButton}" Value="False">
|
||||
<DataTrigger.EnterActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="1.0" To="0.0" Duration="0:0:0.25">
|
||||
<DoubleAnimation.EasingFunction>
|
||||
<QuadraticEase EasingMode="EaseInOut" />
|
||||
</DoubleAnimation.EasingFunction>
|
||||
</DoubleAnimation>
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.EnterActions>
|
||||
<DataTrigger.ExitActions>
|
||||
<BeginStoryboard>
|
||||
<Storyboard>
|
||||
<DoubleAnimation Storyboard.TargetProperty="Opacity" From="0.0" To="1.0" Duration="0:0:0.25" />
|
||||
</Storyboard>
|
||||
</BeginStoryboard>
|
||||
</DataTrigger.ExitActions>
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Button.Style>
|
||||
<materialDesign:PackIcon Kind="Close" Height="18" Width="18" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- Segment movement display -->
|
||||
<Rectangle RadiusX="2" RadiusY="2" Fill="{DynamicResource PrimaryHueDarkBrush}" Margin="-2 -2 0 0" Width="5" Height="24" VerticalAlignment="Top" HorizontalAlignment="Right" />
|
||||
|
||||
<!-- Segment movement handles -->
|
||||
<Rectangle RadiusX="2"
|
||||
RadiusY="2"
|
||||
Fill="Transparent"
|
||||
Margin="-9 -2 0 0"
|
||||
Width="12"
|
||||
Height="25"
|
||||
VerticalAlignment="Top"
|
||||
HorizontalAlignment="Right"
|
||||
Cursor="SizeWE"
|
||||
MouseDown="{s:Action SegmentMouseDown}"
|
||||
MouseUp="{s:Action SegmentMouseUp}"
|
||||
MouseMove="{s:Action SegmentMouseMove}" />
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using Artemis.Core.Models.Profile;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Artemis.UI.Shared.Utilities;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
|
||||
{
|
||||
public class TimelineSegmentViewModel : PropertyChangedBase, IDisposable
|
||||
{
|
||||
private bool _draggingSegment;
|
||||
private bool _showSegmentName;
|
||||
private bool _showRepeatButton;
|
||||
private bool _showDisableButton;
|
||||
|
||||
public TimelineSegmentViewModel(IProfileEditorService profileEditorService, SegmentViewModelType segment)
|
||||
{
|
||||
ProfileEditorService = profileEditorService;
|
||||
Segment = segment;
|
||||
SelectedProfileElement = ProfileEditorService.SelectedProfileElement;
|
||||
|
||||
switch (Segment)
|
||||
{
|
||||
case SegmentViewModelType.Start:
|
||||
ToolTip = "This segment is played when a layer starts displaying because it's conditions are met";
|
||||
break;
|
||||
case SegmentViewModelType.Main:
|
||||
ToolTip = "This segment is played while a condition is met, either once or on a repeating loop";
|
||||
break;
|
||||
case SegmentViewModelType.End:
|
||||
ToolTip = "This segment is played once a condition is no longer met";
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(segment));
|
||||
}
|
||||
|
||||
UpdateDisplay();
|
||||
ProfileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
|
||||
SelectedProfileElement.PropertyChanged += SelectedProfileElementOnPropertyChanged;
|
||||
}
|
||||
|
||||
public RenderProfileElement SelectedProfileElement { get; }
|
||||
|
||||
public SegmentViewModelType Segment { get; }
|
||||
public IProfileEditorService ProfileEditorService { get; }
|
||||
public string ToolTip { get; }
|
||||
|
||||
public TimeSpan SegmentLength
|
||||
{
|
||||
get
|
||||
{
|
||||
return Segment switch
|
||||
{
|
||||
SegmentViewModelType.Start => SelectedProfileElement?.StartSegmentLength ?? TimeSpan.Zero,
|
||||
SegmentViewModelType.Main => SelectedProfileElement?.MainSegmentLength ?? TimeSpan.Zero,
|
||||
SegmentViewModelType.End => SelectedProfileElement?.EndSegmentLength ?? TimeSpan.Zero,
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public double SegmentWidth => ProfileEditorService.PixelsPerSecond * SegmentLength.TotalSeconds;
|
||||
|
||||
public bool SegmentEnabled => SegmentLength != TimeSpan.Zero;
|
||||
public bool IsMainSegment => Segment == SegmentViewModelType.Main;
|
||||
|
||||
// Only the main segment supports this, for any other segment the getter always returns false and the setter does nothing
|
||||
public bool RepeatSegment
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Segment != SegmentViewModelType.Main)
|
||||
return false;
|
||||
|
||||
return SelectedProfileElement?.DisplayContinuously ?? false;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (Segment != SegmentViewModelType.Main)
|
||||
return;
|
||||
|
||||
SelectedProfileElement.DisplayContinuously = value;
|
||||
ProfileEditorService.UpdateSelectedProfileElement();
|
||||
NotifyOfPropertyChange(nameof(RepeatSegment));
|
||||
}
|
||||
}
|
||||
|
||||
public double SegmentStartPosition
|
||||
{
|
||||
get
|
||||
{
|
||||
return Segment switch
|
||||
{
|
||||
SegmentViewModelType.Start => 0,
|
||||
SegmentViewModelType.Main => ProfileEditorService.PixelsPerSecond * SelectedProfileElement.StartSegmentLength.TotalSeconds,
|
||||
SegmentViewModelType.End => ProfileEditorService.PixelsPerSecond * (SelectedProfileElement.StartSegmentLength.TotalSeconds + SelectedProfileElement.MainSegmentLength.TotalSeconds),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShowSegmentName
|
||||
{
|
||||
get => _showSegmentName;
|
||||
set => SetAndNotify(ref _showSegmentName, value);
|
||||
}
|
||||
|
||||
public bool ShowRepeatButton
|
||||
{
|
||||
get => _showRepeatButton;
|
||||
set => SetAndNotify(ref _showRepeatButton, value);
|
||||
}
|
||||
|
||||
public bool ShowDisableButton
|
||||
{
|
||||
get => _showDisableButton;
|
||||
set => SetAndNotify(ref _showDisableButton, value);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ProfileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged;
|
||||
SelectedProfileElement.PropertyChanged -= SelectedProfileElementOnPropertyChanged;
|
||||
}
|
||||
|
||||
public void DisableSegment()
|
||||
{
|
||||
var keyframes = SelectedProfileElement.GetAllKeyframes();
|
||||
var startSegmentEnd = SelectedProfileElement.StartSegmentLength;
|
||||
var mainSegmentEnd = SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength;
|
||||
|
||||
var oldSegmentLength = SegmentLength;
|
||||
|
||||
if (Segment == SegmentViewModelType.Start)
|
||||
{
|
||||
// Remove keyframes that fall in this segment
|
||||
foreach (var baseLayerPropertyKeyframe in keyframes.Where(k => k.Position <= startSegmentEnd))
|
||||
baseLayerPropertyKeyframe.Remove();
|
||||
SelectedProfileElement.StartSegmentLength = TimeSpan.Zero;
|
||||
}
|
||||
else if (Segment == SegmentViewModelType.Main)
|
||||
{
|
||||
// Remove keyframes that fall in this segment
|
||||
foreach (var baseLayerPropertyKeyframe in keyframes.Where(k => k.Position > startSegmentEnd && k.Position <= mainSegmentEnd))
|
||||
baseLayerPropertyKeyframe.Remove();
|
||||
SelectedProfileElement.MainSegmentLength = TimeSpan.Zero;
|
||||
}
|
||||
else if (Segment == SegmentViewModelType.End)
|
||||
{
|
||||
// Remove keyframes that fall in this segment
|
||||
foreach (var baseLayerPropertyKeyframe in keyframes.Where(k => k.Position > mainSegmentEnd))
|
||||
baseLayerPropertyKeyframe.Remove();
|
||||
SelectedProfileElement.EndSegmentLength = TimeSpan.Zero;
|
||||
}
|
||||
|
||||
NotifyOfPropertyChange(nameof(SegmentEnabled));
|
||||
ShiftNextSegment(SegmentLength - oldSegmentLength);
|
||||
ProfileEditorService.UpdateSelectedProfileElement();
|
||||
}
|
||||
|
||||
public void EnableSegment()
|
||||
{
|
||||
ShiftNextSegment(TimeSpan.FromSeconds(1));
|
||||
if (Segment == SegmentViewModelType.Start)
|
||||
SelectedProfileElement.StartSegmentLength = TimeSpan.FromSeconds(1);
|
||||
else if (Segment == SegmentViewModelType.Main)
|
||||
SelectedProfileElement.MainSegmentLength = TimeSpan.FromSeconds(1);
|
||||
else if (Segment == SegmentViewModelType.End)
|
||||
SelectedProfileElement.EndSegmentLength = TimeSpan.FromSeconds(1);
|
||||
|
||||
NotifyOfPropertyChange(nameof(SegmentEnabled));
|
||||
ProfileEditorService.UpdateSelectedProfileElement();
|
||||
UpdateDisplay();
|
||||
}
|
||||
|
||||
public void ShiftNextSegment(TimeSpan amount)
|
||||
{
|
||||
var segmentEnd = TimeSpan.Zero;
|
||||
if (Segment == SegmentViewModelType.Start)
|
||||
segmentEnd = SelectedProfileElement.StartSegmentLength;
|
||||
else if (Segment == SegmentViewModelType.Main)
|
||||
segmentEnd = SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength;
|
||||
else if (Segment == SegmentViewModelType.End)
|
||||
segmentEnd = SelectedProfileElement.TimelineLength;
|
||||
|
||||
foreach (var baseLayerPropertyKeyframe in SelectedProfileElement.GetAllKeyframes().Where(k => k.Position > segmentEnd))
|
||||
baseLayerPropertyKeyframe.Position += amount;
|
||||
}
|
||||
|
||||
public void SegmentMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
((IInputElement) sender).CaptureMouse();
|
||||
_draggingSegment = true;
|
||||
}
|
||||
|
||||
public void SegmentMouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
((IInputElement) sender).ReleaseMouseCapture();
|
||||
_draggingSegment = false;
|
||||
|
||||
ProfileEditorService.UpdateSelectedProfileElement();
|
||||
}
|
||||
|
||||
public void SegmentMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (e.LeftButton != MouseButtonState.Pressed || !_draggingSegment)
|
||||
return;
|
||||
|
||||
// Get the parent scroll viewer, need that for our position
|
||||
var parent = VisualTreeUtilities.FindParent<ScrollViewer>((DependencyObject) sender, "TimelineHeaderScrollViewer");
|
||||
|
||||
var x = Math.Max(0, e.GetPosition(parent).X);
|
||||
var newTime = TimeSpan.FromSeconds(x / ProfileEditorService.PixelsPerSecond);
|
||||
|
||||
// Round the time to something that fits the current zoom level
|
||||
if (ProfileEditorService.PixelsPerSecond < 200)
|
||||
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 5.0) * 5.0);
|
||||
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 holding down shift, snap to the closest element on the timeline
|
||||
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
|
||||
newTime = ProfileEditorService.SnapToTimeline(newTime, TimeSpan.FromMilliseconds(1000f / ProfileEditorService.PixelsPerSecond * 5), false, true, true);
|
||||
// If holding down control, round to the closest 50ms
|
||||
else if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
|
||||
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 50.0) * 50.0);
|
||||
|
||||
var oldSegmentLength = SegmentLength;
|
||||
if (Segment == SegmentViewModelType.Start)
|
||||
{
|
||||
if (newTime < TimeSpan.FromMilliseconds(100))
|
||||
newTime = TimeSpan.FromMilliseconds(100);
|
||||
SelectedProfileElement.StartSegmentLength = newTime;
|
||||
}
|
||||
else if (Segment == SegmentViewModelType.Main)
|
||||
{
|
||||
if (newTime < SelectedProfileElement.StartSegmentLength + TimeSpan.FromMilliseconds(100))
|
||||
newTime = SelectedProfileElement.StartSegmentLength + TimeSpan.FromMilliseconds(100);
|
||||
SelectedProfileElement.MainSegmentLength = newTime - SelectedProfileElement.StartSegmentLength;
|
||||
}
|
||||
else if (Segment == SegmentViewModelType.End)
|
||||
{
|
||||
if (newTime < SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength + TimeSpan.FromMilliseconds(100))
|
||||
newTime = SelectedProfileElement.StartSegmentLength + SelectedProfileElement.MainSegmentLength + TimeSpan.FromMilliseconds(100);
|
||||
SelectedProfileElement.EndSegmentLength = newTime - SelectedProfileElement.StartSegmentLength - SelectedProfileElement.MainSegmentLength;
|
||||
}
|
||||
|
||||
NotifyOfPropertyChange(nameof(SegmentLength));
|
||||
NotifyOfPropertyChange(nameof(SegmentWidth));
|
||||
|
||||
ShiftNextSegment(SegmentLength - oldSegmentLength);
|
||||
UpdateDisplay();
|
||||
}
|
||||
|
||||
private void SelectedProfileElementOnPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(RenderProfileElement.StartSegmentLength) ||
|
||||
e.PropertyName == nameof(RenderProfileElement.MainSegmentLength) ||
|
||||
e.PropertyName == nameof(RenderProfileElement.EndSegmentLength))
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(SegmentStartPosition));
|
||||
NotifyOfPropertyChange(nameof(SegmentWidth));
|
||||
}
|
||||
else if (e.PropertyName == nameof(RenderProfileElement.DisplayContinuously))
|
||||
NotifyOfPropertyChange(nameof(RepeatSegment));
|
||||
}
|
||||
|
||||
|
||||
private void ProfileEditorServiceOnPixelsPerSecondChanged(object? sender, EventArgs e)
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(SegmentWidth));
|
||||
NotifyOfPropertyChange(nameof(SegmentStartPosition));
|
||||
|
||||
UpdateDisplay();
|
||||
}
|
||||
|
||||
private void UpdateDisplay()
|
||||
{
|
||||
if (!IsMainSegment)
|
||||
ShowSegmentName = SegmentWidth > 60;
|
||||
else
|
||||
ShowSegmentName = SegmentWidth > 80;
|
||||
|
||||
ShowRepeatButton = SegmentWidth > 45 && IsMainSegment;
|
||||
ShowDisableButton = SegmentWidth > 25;
|
||||
}
|
||||
}
|
||||
|
||||
public enum SegmentViewModelType
|
||||
{
|
||||
Start,
|
||||
Main,
|
||||
End
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.TimelineView"
|
||||
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.ProfileEditor.LayerProperties.Timeline"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="25"
|
||||
d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance local:TimelineViewModel}">
|
||||
<Grid x:Name="TimelineContainerGrid"
|
||||
Background="{DynamicResource MaterialDesignToolBarBackground}"
|
||||
MouseDown="{s:Action TimelineCanvasMouseDown}"
|
||||
MouseUp="{s:Action TimelineCanvasMouseUp}"
|
||||
MouseMove="{s:Action TimelineCanvasMouseMove}"
|
||||
Margin="0 0 -1 0">
|
||||
<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}"
|
||||
MinWidth="{Binding TotalTimelineWidth}"
|
||||
HorizontalAlignment="Left">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<ContentControl s:View.Model="{Binding TimelinePropertyGroupViewModel}" HorizontalContentAlignment="Left" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Line Stroke="{StaticResource PrimaryHueDarkBrush}"
|
||||
Opacity="0.5"
|
||||
StrokeDashArray="4 2"
|
||||
X1="{Binding StartSegmentEndPosition}"
|
||||
X2="{Binding StartSegmentEndPosition}"
|
||||
Y1="0"
|
||||
Y2="{Binding ActualHeight, ElementName=TimelineContainerGrid}"
|
||||
HorizontalAlignment="Left"
|
||||
Visibility="{Binding StartSegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"/>
|
||||
<Line Stroke="{StaticResource PrimaryHueDarkBrush}"
|
||||
Opacity="0.5"
|
||||
StrokeDashArray="4 2"
|
||||
X1="{Binding MainSegmentEndPosition}"
|
||||
X2="{Binding MainSegmentEndPosition}"
|
||||
Y1="0"
|
||||
Y2="{Binding ActualHeight, ElementName=TimelineContainerGrid}" />
|
||||
<Line Stroke="{StaticResource PrimaryHueDarkBrush}"
|
||||
Opacity="0.5"
|
||||
StrokeDashArray="4 2"
|
||||
X1="{Binding EndSegmentEndPosition}"
|
||||
X2="{Binding EndSegmentEndPosition}"
|
||||
Y1="0"
|
||||
Y2="{Binding ActualHeight, ElementName=TimelineContainerGrid}"
|
||||
Visibility="{Binding EndSegmentEnabled, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"/>
|
||||
|
||||
<!-- Multi-selection rectangle -->
|
||||
<Path x:Name="MultiSelectionPath"
|
||||
Data="{Binding SelectionRectangle}"
|
||||
Opacity="0"
|
||||
Stroke="{DynamicResource PrimaryHueLightBrush}"
|
||||
StrokeThickness="1"
|
||||
IsHitTestVisible="False">
|
||||
<Path.Fill>
|
||||
<SolidColorBrush Color="{DynamicResource Primary400}" Opacity="0.25" />
|
||||
</Path.Fill>
|
||||
</Path>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,378 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
using Artemis.Core.Models.Profile;
|
||||
using Artemis.UI.Screens.ProfileEditor.LayerProperties.Abstract;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Artemis.UI.Shared.Utilities;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline
|
||||
{
|
||||
public class TimelineViewModel : PropertyChangedBase, IViewAware, IDisposable
|
||||
{
|
||||
private readonly LayerPropertiesViewModel _layerPropertiesViewModel;
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private RectangleGeometry _selectionRectangle;
|
||||
|
||||
public TimelineViewModel(LayerPropertiesViewModel layerPropertiesViewModel, BindableCollection<LayerPropertyGroupViewModel> layerPropertyGroups, IProfileEditorService profileEditorService)
|
||||
{
|
||||
_layerPropertiesViewModel = layerPropertiesViewModel;
|
||||
_profileEditorService = profileEditorService;
|
||||
|
||||
LayerPropertyGroups = layerPropertyGroups;
|
||||
SelectionRectangle = new RectangleGeometry();
|
||||
SelectedProfileElement = layerPropertiesViewModel.SelectedProfileElement;
|
||||
|
||||
SelectedProfileElement.PropertyChanged += SelectedProfileElementOnPropertyChanged;
|
||||
_profileEditorService.PixelsPerSecondChanged += ProfileEditorServiceOnPixelsPerSecondChanged;
|
||||
|
||||
Update();
|
||||
}
|
||||
|
||||
public RenderProfileElement SelectedProfileElement { get; set; }
|
||||
|
||||
public BindableCollection<LayerPropertyGroupViewModel> LayerPropertyGroups { get; }
|
||||
|
||||
public RectangleGeometry SelectionRectangle
|
||||
{
|
||||
get => _selectionRectangle;
|
||||
set => SetAndNotify(ref _selectionRectangle, value);
|
||||
}
|
||||
|
||||
public double StartSegmentWidth => _profileEditorService.PixelsPerSecond * SelectedProfileElement.StartSegmentLength.TotalSeconds;
|
||||
public double StartSegmentEndPosition => StartSegmentWidth;
|
||||
public double MainSegmentWidth => _profileEditorService.PixelsPerSecond * SelectedProfileElement.MainSegmentLength.TotalSeconds;
|
||||
public double MainSegmentEndPosition => StartSegmentWidth + MainSegmentWidth;
|
||||
public double EndSegmentWidth => _profileEditorService.PixelsPerSecond * SelectedProfileElement.EndSegmentLength.TotalSeconds;
|
||||
public double EndSegmentEndPosition => StartSegmentWidth + MainSegmentWidth + EndSegmentWidth;
|
||||
public double TotalTimelineWidth => _profileEditorService.PixelsPerSecond * SelectedProfileElement.TimelineLength.TotalSeconds;
|
||||
|
||||
public bool StartSegmentEnabled => SelectedProfileElement.StartSegmentLength != TimeSpan.Zero;
|
||||
public bool EndSegmentEnabled => SelectedProfileElement.EndSegmentLength != TimeSpan.Zero;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_profileEditorService.PixelsPerSecondChanged -= ProfileEditorServiceOnPixelsPerSecondChanged;
|
||||
SelectedProfileElement.PropertyChanged -= SelectedProfileElementOnPropertyChanged;
|
||||
}
|
||||
|
||||
private void SelectedProfileElementOnPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(_profileEditorService.SelectedProfileElement.StartSegmentLength))
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(StartSegmentWidth));
|
||||
NotifyOfPropertyChange(nameof(StartSegmentEndPosition));
|
||||
NotifyOfPropertyChange(nameof(MainSegmentEndPosition));
|
||||
NotifyOfPropertyChange(nameof(EndSegmentEndPosition));
|
||||
NotifyOfPropertyChange(nameof(StartSegmentEnabled));
|
||||
NotifyOfPropertyChange(nameof(TotalTimelineWidth));
|
||||
}
|
||||
else if (e.PropertyName == nameof(_profileEditorService.SelectedProfileElement.MainSegmentLength))
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(MainSegmentWidth));
|
||||
NotifyOfPropertyChange(nameof(MainSegmentEndPosition));
|
||||
NotifyOfPropertyChange(nameof(EndSegmentEndPosition));
|
||||
NotifyOfPropertyChange(nameof(TotalTimelineWidth));
|
||||
}
|
||||
else if (e.PropertyName == nameof(_profileEditorService.SelectedProfileElement.EndSegmentLength))
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(EndSegmentWidth));
|
||||
NotifyOfPropertyChange(nameof(EndSegmentEndPosition));
|
||||
NotifyOfPropertyChange(nameof(EndSegmentEnabled));
|
||||
NotifyOfPropertyChange(nameof(TotalTimelineWidth));
|
||||
}
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
foreach (var layerPropertyGroupViewModel in LayerPropertyGroups)
|
||||
{
|
||||
layerPropertyGroupViewModel.TimelinePropertyGroupViewModel.UpdateKeyframes();
|
||||
|
||||
foreach (var layerPropertyBaseViewModel in layerPropertyGroupViewModel.GetAllChildren())
|
||||
{
|
||||
if (layerPropertyBaseViewModel is LayerPropertyViewModel layerPropertyViewModel)
|
||||
layerPropertyViewModel.TimelinePropertyBaseViewModel.UpdateKeyframes();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ProfileEditorServiceOnPixelsPerSecondChanged(object sender, EventArgs e)
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(StartSegmentWidth));
|
||||
NotifyOfPropertyChange(nameof(StartSegmentEndPosition));
|
||||
NotifyOfPropertyChange(nameof(MainSegmentWidth));
|
||||
NotifyOfPropertyChange(nameof(MainSegmentEndPosition));
|
||||
NotifyOfPropertyChange(nameof(EndSegmentWidth));
|
||||
NotifyOfPropertyChange(nameof(EndSegmentEndPosition));
|
||||
NotifyOfPropertyChange(nameof(TotalTimelineWidth));
|
||||
}
|
||||
|
||||
#region Command handlers
|
||||
|
||||
public void KeyframeMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// if (e.LeftButton == MouseButtonState.Released)
|
||||
// return;
|
||||
|
||||
var viewModel = (sender as Ellipse)?.DataContext as TimelineKeyframeViewModel;
|
||||
if (viewModel == null)
|
||||
return;
|
||||
|
||||
((IInputElement) sender).CaptureMouse();
|
||||
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift) && !viewModel.IsSelected)
|
||||
SelectKeyframe(viewModel, true, false);
|
||||
else if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
|
||||
SelectKeyframe(viewModel, false, true);
|
||||
else if (!viewModel.IsSelected)
|
||||
SelectKeyframe(viewModel, false, false);
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
public void KeyframeMouseUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_profileEditorService.UpdateSelectedProfileElement();
|
||||
ReleaseSelectedKeyframes();
|
||||
|
||||
((IInputElement) sender).ReleaseMouseCapture();
|
||||
}
|
||||
|
||||
public void KeyframeMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
var viewModel = (sender as Ellipse)?.DataContext as TimelineKeyframeViewModel;
|
||||
if (viewModel == null)
|
||||
return;
|
||||
|
||||
if (e.LeftButton == MouseButtonState.Pressed)
|
||||
MoveSelectedKeyframes(GetCursorTime(e.GetPosition(View)), viewModel);
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
#region Context menu actions
|
||||
|
||||
public void ContextMenuOpening(object sender, ContextMenuEventArgs e)
|
||||
{
|
||||
var viewModel = (sender as Ellipse)?.DataContext as TimelineKeyframeViewModel;
|
||||
viewModel?.CreateEasingViewModels();
|
||||
}
|
||||
|
||||
public void ContextMenuClosing(object sender, ContextMenuEventArgs e)
|
||||
{
|
||||
var viewModel = (sender as Ellipse)?.DataContext as TimelineKeyframeViewModel;
|
||||
viewModel?.EasingViewModels.Clear();
|
||||
}
|
||||
|
||||
public void Copy(TimelineKeyframeViewModel viewModel)
|
||||
{
|
||||
viewModel.Copy();
|
||||
}
|
||||
|
||||
public void Delete(TimelineKeyframeViewModel viewModel)
|
||||
{
|
||||
viewModel.Delete();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
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 / _profileEditorService.PixelsPerSecond);
|
||||
|
||||
// Round the time to something that fits the current zoom level
|
||||
if (_profileEditorService.PixelsPerSecond < 200)
|
||||
time = TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 5.0) * 5.0);
|
||||
else if (_profileEditorService.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 / _profileEditorService.PixelsPerSecond * 5;
|
||||
if (Math.Abs(_profileEditorService.CurrentTime.TotalMilliseconds - time.TotalMilliseconds) < tolerance)
|
||||
time = _profileEditorService.CurrentTime;
|
||||
else if (Math.Abs(_profileEditorService.SelectedProfileElement.StartSegmentLength.TotalMilliseconds - time.TotalMilliseconds) < tolerance)
|
||||
time = _profileEditorService.SelectedProfileElement.StartSegmentLength;
|
||||
}
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Keyframe movement
|
||||
|
||||
public void MoveSelectedKeyframes(TimeSpan cursorTime, TimelineKeyframeViewModel sourceKeyframeViewModel)
|
||||
{
|
||||
// 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.SaveOffsetToKeyframe(sourceKeyframeViewModel);
|
||||
|
||||
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
|
||||
{
|
||||
cursorTime = _profileEditorService.SnapToTimeline(
|
||||
cursorTime,
|
||||
TimeSpan.FromMilliseconds(1000f / _profileEditorService.PixelsPerSecond * 5),
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
sourceKeyframeViewModel.BaseLayerPropertyKeyframe
|
||||
);
|
||||
}
|
||||
|
||||
sourceKeyframeViewModel.UpdatePosition(cursorTime);
|
||||
|
||||
foreach (var keyframeViewModel in keyframeViewModels.Where(k => k.IsSelected))
|
||||
keyframeViewModel.ApplyOffsetToKeyframe(sourceKeyframeViewModel);
|
||||
|
||||
_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
|
||||
|
||||
#region IViewAware
|
||||
|
||||
public void AttachView(UIElement view)
|
||||
{
|
||||
if (View != null)
|
||||
throw new InvalidOperationException(string.Format("Tried to attach View {0} to ViewModel {1}, but it already has a view attached", view.GetType().Name, GetType().Name));
|
||||
|
||||
View = view;
|
||||
}
|
||||
|
||||
public UIElement View { get; set; }
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user