Meta - Replace WPF projects with Avalonia projects

This commit is contained in:
Robert committed 2022-03-27 11:55:26 +02:00
1 parent fcf0376a9a
commit d7f3ee1190
1110 files changed
+10872 -52240

No files matched your search

@@ -0,0 +1,147 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:Artemis.UI.Controls"
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="350"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.PropertiesView"
x:DataType="local:PropertiesViewModel">
<UserControl.Styles>
<StyleInclude Source="/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml" />
</UserControl.Styles>
<UserControl.Resources>
<converters:DoubleToGridLengthConverter x:Key="DoubleToGridLengthConverter"></converters:DoubleToGridLengthConverter>
</UserControl.Resources>
<Grid Name="ContainerGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{CompiledBinding PropertiesTreeWidth.Value, Mode=TwoWay, Converter={StaticResource DoubleToGridLengthConverter}}" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid RowDefinitions="48,*">
<ContentControl Grid.Row="0" Content="{CompiledBinding PlaybackViewModel}" />
<ScrollViewer Grid.Row="1"
Grid.Column="0"
Name="TreeScrollViewer"
Offset="{CompiledBinding #TimelineScrollViewer.Offset, Mode=OneWay}"
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}">
<ItemsControl Items="{CompiledBinding PropertyGroupViewModels}" Padding="0 0 8 0">
<ItemsControl.ItemTemplate>
<TreeDataTemplate DataType="{x:Type local:PropertyGroupViewModel}" ItemsSource="{CompiledBinding Children}">
<ContentControl Content="{CompiledBinding TreeGroupViewModel}" />
</TreeDataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
<GridSplitter Grid.Column="1"
Cursor="SizeWestEast"
Foreground="Transparent"
Background="Transparent"
Margin="0 0 -5 0" />
<!-- Horizontal scrolling -->
<ScrollViewer Grid.Column="2" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
<Grid RowDefinitions="48,*">
<!-- Timeline header body -->
<controls:TimelineHeader Grid.Row="0"
Name="TimelineHeader"
Margin="0 18 0 0"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
HorizontalAlignment="Left"
PixelsPerSecond="{CompiledBinding PixelsPerSecond}"
HorizontalOffset="{CompiledBinding #TimelineScrollViewer.Offset.X, Mode=OneWay}"
VisibleWidth="{CompiledBinding #TimelineScrollViewer.Bounds.Width}"
OffsetFirstValue="True"
PointerReleased="TimelineHeader_OnPointerReleased"
Width="{CompiledBinding #TimelineScrollViewer.Viewport.Width}"
Cursor="Hand" />
<Canvas Grid.Row="0" ZIndex="2">
<!-- Segment dividers -->
<Line Name="TimelineLine"
Canvas.Left="{CompiledBinding TimelineViewModel.CaretPosition}"
Cursor="SizeWestEast"
PointerPressed="TimelineCaret_OnPointerPressed"
PointerReleased="TimelineCaret_OnPointerReleased"
PointerMoved="TimelineCaret_OnPointerMoved"
StartPoint="0,0"
EndPoint="{CompiledBinding #ContainerGrid.Bounds.BottomLeft}"
StrokeThickness="2"
Stroke="{DynamicResource SystemAccentColorLight1}">
</Line>
<Line Name="StartSegmentLine"
Canvas.Left="{CompiledBinding TimelineViewModel.StartSegmentViewModel.EndX}"
IsVisible="{CompiledBinding !TimelineViewModel.MainSegmentViewModel.ShowAddStart}"
StartPoint="0,0"
EndPoint="{CompiledBinding #ContainerGrid.Bounds.BottomLeft}"
StrokeThickness="2"
Stroke="{DynamicResource SystemAccentColorLight1}"
StrokeDashArray="6,2"
Opacity="0.5">
</Line>
<Line Name="MainSegmentLine"
Canvas.Left="{CompiledBinding TimelineViewModel.MainSegmentViewModel.EndX}"
IsVisible="{CompiledBinding !TimelineViewModel.MainSegmentViewModel.ShowAddMain}"
StartPoint="0,0"
EndPoint="{CompiledBinding #ContainerGrid.Bounds.BottomLeft}"
StrokeThickness="2"
Stroke="{DynamicResource SystemAccentColorLight1}"
StrokeDashArray="6,2"
Opacity="0.5">
</Line>
<Line Name="EndSegmentLine"
Canvas.Left="{CompiledBinding TimelineViewModel.EndSegmentViewModel.EndX}"
IsVisible="{CompiledBinding !TimelineViewModel.MainSegmentViewModel.ShowAddEnd}"
StartPoint="0,0"
EndPoint="{CompiledBinding #ContainerGrid.Bounds.BottomLeft}"
StrokeThickness="2"
Stroke="{DynamicResource SystemAccentColorLight1}"
StrokeDashArray="6,2"
Opacity="0.5">
</Line>
<!-- Timeline segments -->
<ContentControl Canvas.Left="{CompiledBinding TimelineViewModel.EndSegmentViewModel.StartX}"
Classes="segment-content-control"
Content="{CompiledBinding TimelineViewModel.EndSegmentViewModel}" />
<ContentControl Canvas.Left="{CompiledBinding TimelineViewModel.MainSegmentViewModel.StartX}"
Classes="segment-content-control"
Content="{CompiledBinding TimelineViewModel.MainSegmentViewModel}" />
<ContentControl Canvas.Left="{CompiledBinding TimelineViewModel.StartSegmentViewModel.StartX}"
Classes="segment-content-control"
Content="{CompiledBinding TimelineViewModel.StartSegmentViewModel}" />
<!-- Timeline caret -->
<Polygon Name="TimelineCaret"
Canvas.Left="{CompiledBinding TimelineViewModel.CaretPosition}"
Cursor="SizeWestEast"
PointerPressed="TimelineCaret_OnPointerPressed"
PointerReleased="TimelineCaret_OnPointerReleased"
PointerMoved="TimelineCaret_OnPointerMoved"
Points="-8,0 -8,8 0,20, 8,8 8,0"
Fill="{DynamicResource SystemAccentColorLight1}">
</Polygon>
</Canvas>
<ScrollViewer Grid.Row="1"
Name="TimelineScrollViewer"
Offset="{CompiledBinding #TreeScrollViewer.Offset, Mode=OneWay}"
VerticalScrollBarVisibility="Hidden">
<ContentControl Content="{CompiledBinding TimelineViewModel}"
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}" />
</ScrollViewer>
<!-- TODO: Databindings here -->
</Grid>
</ScrollViewer>
</Grid>
</UserControl>
@@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using Avalonia.VisualTree;
namespace Artemis.UI.Screens.ProfileEditor.Properties;
public class PropertiesView : ReactiveUserControl<PropertiesViewModel>
{
private readonly Polygon _timelineCaret;
private readonly Line _timelineLine;
public PropertiesView()
{
InitializeComponent();
_timelineCaret = this.Get<Polygon>("TimelineCaret");
_timelineLine = this.Get<Line>("TimelineLine");
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void ApplyTransition(bool enable)
{
// if (enable)
// {
// ((DoubleTransition) _timelineCaret.Transitions![0]).Duration = TimeSpan.FromMilliseconds(50);
// ((DoubleTransition) _timelineLine.Transitions![0]).Duration = TimeSpan.FromMilliseconds(50);
// }
// else
// {
// ((DoubleTransition) _timelineCaret.Transitions![0]).Duration = TimeSpan.Zero;
// ((DoubleTransition) _timelineLine.Transitions![0]).Duration = TimeSpan.Zero;
// }
}
private void TimelineCaret_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
e.Pointer.Capture((IInputElement?) sender);
ApplyTransition(false);
}
private void TimelineCaret_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
e.Pointer.Capture(null);
ApplyTransition(true);
}
private void TimelineCaret_OnPointerMoved(object? sender, PointerEventArgs e)
{
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed || ViewModel == null)
return;
IInputElement? senderElement = (IInputElement?) sender;
if (senderElement == null)
return;
// Get the parent grid, need that for our position
IVisual? parent = senderElement.VisualParent;
double x = Math.Max(0, e.GetPosition(parent).X);
TimeSpan newTime = TimeSpan.FromSeconds(x / ViewModel.PixelsPerSecond);
newTime = RoundTime(newTime);
// If holding down shift, snap to the closest segment or keyframe
if (e.KeyModifiers.HasFlag(KeyModifiers.Shift))
{
List<TimeSpan> snapTimes = ViewModel.PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Select(k => k.Position).ToList();
newTime = ViewModel.TimelineViewModel.SnapToTimeline(newTime, true, false, snapTimes);
}
// If holding down control, round to the closest 50ms
if (e.KeyModifiers.HasFlag(KeyModifiers.Control))
newTime = TimeSpan.FromMilliseconds(Math.Round(newTime.TotalMilliseconds / 50.0) * 50.0);
ViewModel.TimelineViewModel.ChangeTime(newTime);
}
private void TimelineHeader_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (ViewModel == null || sender is not IInputElement senderElement)
return;
// Get the parent grid, need that for our position
double x = Math.Max(0, e.GetPosition(senderElement.VisualParent).X);
TimeSpan newTime = TimeSpan.FromSeconds(x / ViewModel.PixelsPerSecond);
ViewModel.TimelineViewModel.ChangeTime(RoundTime(newTime));
}
private TimeSpan RoundTime(TimeSpan time)
{
// Round the time to something that fits the current zoom level
if (ViewModel!.PixelsPerSecond < 200)
return TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 5.0) * 5.0);
if (ViewModel.PixelsPerSecond < 500)
return TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds / 2.0) * 2.0);
return TimeSpan.FromMilliseconds(Math.Round(time.TotalMilliseconds));
}
}
@@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects;
using Artemis.Core.Services;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.Playback;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties;
public class PropertiesViewModel : ActivatableViewModelBase
{
private readonly Dictionary<LayerPropertyGroup, PropertyGroupViewModel> _cachedViewModels;
private readonly ILayerPropertyVmFactory _layerPropertyVmFactory;
private readonly IProfileEditorService _profileEditorService;
private readonly ISettingsService _settingsService;
private ObservableAsPropertyHelper<int>? _pixelsPerSecond;
private ObservableAsPropertyHelper<RenderProfileElement?>? _profileElement;
/// <inheritdoc />
public PropertiesViewModel(IProfileEditorService profileEditorService, ISettingsService settingsService, ILayerPropertyVmFactory layerPropertyVmFactory, PlaybackViewModel playbackViewModel)
{
_profileEditorService = profileEditorService;
_settingsService = settingsService;
_layerPropertyVmFactory = layerPropertyVmFactory;
_cachedViewModels = new Dictionary<LayerPropertyGroup, PropertyGroupViewModel>();
PropertyGroupViewModels = new ObservableCollection<PropertyGroupViewModel>();
PlaybackViewModel = playbackViewModel;
TimelineViewModel = layerPropertyVmFactory.TimelineViewModel(PropertyGroupViewModels);
// Subscribe to events of the latest selected profile element - borrowed from https://stackoverflow.com/a/63950940
this.WhenAnyValue(vm => vm.ProfileElement)
.Select(p => p is Layer l
? Observable.FromEventPattern(x => l.LayerBrushUpdated += x, x => l.LayerBrushUpdated -= x)
: Observable.Never<EventPattern<object>>())
.Switch()
.Subscribe(_ => UpdateGroups());
this.WhenAnyValue(vm => vm.ProfileElement)
.Select(p => p != null
? Observable.FromEventPattern(x => p.LayerEffectsUpdated += x, x => p.LayerEffectsUpdated -= x)
: Observable.Never<EventPattern<object>>())
.Switch()
.Subscribe(_ => UpdateGroups());
// React to service profile element changes as long as the VM is active
this.WhenActivated(d =>
{
_profileElement = profileEditorService.ProfileElement.ToProperty(this, vm => vm.ProfileElement).DisposeWith(d);
_pixelsPerSecond = profileEditorService.PixelsPerSecond.ToProperty(this, vm => vm.PixelsPerSecond).DisposeWith(d);
Disposable.Create(() => _settingsService.SaveAllSettings()).DisposeWith(d);
});
this.WhenAnyValue(vm => vm.ProfileElement).Subscribe(_ => UpdateGroups());
}
public ObservableCollection<PropertyGroupViewModel> PropertyGroupViewModels { get; }
public PlaybackViewModel PlaybackViewModel { get; }
public TimelineViewModel TimelineViewModel { get; }
public RenderProfileElement? ProfileElement => _profileElement?.Value;
public Layer? Layer => _profileElement?.Value as Layer;
public int PixelsPerSecond => _pixelsPerSecond?.Value ?? 0;
public IObservable<bool> Playing => _profileEditorService.Playing;
public PluginSetting<double> PropertiesTreeWidth => _settingsService.GetSetting("ProfileEditor.PropertiesTreeWidth", 500.0);
private void UpdateGroups()
{
if (ProfileElement == null)
{
PropertyGroupViewModels.Clear();
return;
}
ObservableCollection<PropertyGroupViewModel> viewModels = new();
if (Layer != null)
{
// Add base VMs
viewModels.Add(GetOrCreateViewModel(Layer.General, null, null));
viewModels.Add(GetOrCreateViewModel(Layer.Transform, null, null));
// Add brush VM if the brush has properties
if (Layer.LayerBrush?.BaseProperties != null)
viewModels.Add(GetOrCreateViewModel(Layer.LayerBrush.BaseProperties, Layer.LayerBrush, null));
}
// Add effect VMs
foreach (BaseLayerEffect layerEffect in ProfileElement.LayerEffects.OrderBy(e => e.Order))
if (layerEffect.BaseProperties != null)
viewModels.Add(GetOrCreateViewModel(layerEffect.BaseProperties, null, layerEffect));
// Map the most recent collection of VMs to the current list of VMs, making as little changes to the collection as possible
for (int index = 0; index < viewModels.Count; index++)
{
PropertyGroupViewModel propertyGroupViewModel = viewModels[index];
if (index > PropertyGroupViewModels.Count - 1)
PropertyGroupViewModels.Add(propertyGroupViewModel);
else if (!ReferenceEquals(PropertyGroupViewModels[index], propertyGroupViewModel))
PropertyGroupViewModels[index] = propertyGroupViewModel;
}
while (PropertyGroupViewModels.Count > viewModels.Count)
PropertyGroupViewModels.RemoveAt(PropertyGroupViewModels.Count - 1);
}
private PropertyGroupViewModel GetOrCreateViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerBrush? layerBrush, BaseLayerEffect? layerEffect)
{
if (_cachedViewModels.TryGetValue(layerPropertyGroup, out PropertyGroupViewModel? cachedVm))
return cachedVm;
PropertyGroupViewModel createdVm;
if (layerBrush != null)
createdVm = _layerPropertyVmFactory.PropertyGroupViewModel(layerPropertyGroup, layerBrush);
else if (layerEffect != null)
createdVm = _layerPropertyVmFactory.PropertyGroupViewModel(layerPropertyGroup, layerEffect);
else
createdVm = _layerPropertyVmFactory.PropertyGroupViewModel(layerPropertyGroup);
_cachedViewModels[layerPropertyGroup] = createdVm;
return createdVm;
}
}
@@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using Artemis.Core;
using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.PropertyInput;
namespace Artemis.UI.Screens.ProfileEditor.Properties;
public class PropertyGroupViewModel : ViewModelBase, IDisposable
{
private readonly ILayerPropertyVmFactory _layerPropertyVmFactory;
private readonly IPropertyInputService _propertyInputService;
private bool _hasChildren;
private bool _isExpanded;
private bool _isVisible;
public PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService)
{
_layerPropertyVmFactory = layerPropertyVmFactory;
_propertyInputService = propertyInputService;
Children = new ObservableCollection<ViewModelBase>();
LayerPropertyGroup = layerPropertyGroup;
TreeGroupViewModel = layerPropertyVmFactory.TreeGroupViewModel(this);
TimelineGroupViewModel = layerPropertyVmFactory.TimelineGroupViewModel(this);
LayerPropertyGroup.VisibilityChanged += LayerPropertyGroupOnVisibilityChanged;
_isVisible = !LayerPropertyGroup.IsHidden;
PopulateChildren();
}
public PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService,
BaseLayerBrush layerBrush)
: this(layerPropertyGroup, layerPropertyVmFactory, propertyInputService)
{
LayerBrush = layerBrush;
}
public PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService,
BaseLayerEffect layerEffect)
: this(layerPropertyGroup, layerPropertyVmFactory, propertyInputService)
{
LayerEffect = layerEffect;
}
public ObservableCollection<ViewModelBase> Children { get; }
public LayerPropertyGroup LayerPropertyGroup { get; }
public BaseLayerBrush? LayerBrush { get; }
public BaseLayerEffect? LayerEffect { get; }
public TreeGroupViewModel TreeGroupViewModel { get; }
public TimelineGroupViewModel TimelineGroupViewModel { get; }
public bool IsVisible
{
get => _isVisible;
set => RaiseAndSetIfChanged(ref _isVisible, value);
}
public bool IsExpanded
{
get => _isExpanded;
set => RaiseAndSetIfChanged(ref _isExpanded, value);
}
public bool HasChildren
{
get => _hasChildren;
set => RaiseAndSetIfChanged(ref _hasChildren, value);
}
public List<ITimelineKeyframeViewModel> GetAllKeyframeViewModels(bool expandedOnly)
{
List<ITimelineKeyframeViewModel> result = new();
if (expandedOnly && !IsExpanded)
return result;
foreach (ViewModelBase child in Children)
{
if (child is PropertyViewModel profileElementPropertyViewModel)
result.AddRange(profileElementPropertyViewModel.TimelinePropertyViewModel.GetAllKeyframeViewModels());
else if (child is PropertyGroupViewModel profileElementPropertyGroupViewModel)
result.AddRange(profileElementPropertyGroupViewModel.GetAllKeyframeViewModels(expandedOnly));
}
return result;
}
private void PopulateChildren()
{
// Get all properties and property groups and create VMs for them
// The group has methods for getting this without reflection but then we lose the order of the properties as they are defined on the group
foreach (PropertyInfo propertyInfo in LayerPropertyGroup.GetType().GetProperties())
{
if (Attribute.IsDefined(propertyInfo, typeof(LayerPropertyIgnoreAttribute)))
continue;
if (typeof(ILayerProperty).IsAssignableFrom(propertyInfo.PropertyType))
{
ILayerProperty? value = (ILayerProperty?) propertyInfo.GetValue(LayerPropertyGroup);
// Ensure a supported input VM was found, otherwise don't add it
if (value != null && _propertyInputService.CanCreatePropertyInputViewModel(value))
Children.Add(_layerPropertyVmFactory.PropertyViewModel(value));
}
else if (typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType))
{
LayerPropertyGroup? value = (LayerPropertyGroup?) propertyInfo.GetValue(LayerPropertyGroup);
if (value != null)
Children.Add(_layerPropertyVmFactory.PropertyGroupViewModel(value));
}
}
HasChildren = Children.Any(i => i is PropertyViewModel {IsVisible: true} || i is PropertyGroupViewModel {IsVisible: true});
}
private void LayerPropertyGroupOnVisibilityChanged(object? sender, EventArgs e)
{
IsVisible = !LayerPropertyGroup.IsHidden;
}
/// <inheritdoc />
public void Dispose()
{
LayerPropertyGroup.VisibilityChanged -= LayerPropertyGroupOnVisibilityChanged;
foreach (ViewModelBase viewModelBase in Children)
{
if (viewModelBase is IDisposable disposable)
disposable.Dispose();
}
}
}
@@ -0,0 +1,58 @@
using System;
using Artemis.Core;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
using Artemis.UI.Shared;
namespace Artemis.UI.Screens.ProfileEditor.Properties;
public class PropertyViewModel : ViewModelBase, IDisposable
{
private bool _isExpanded;
private bool _isHighlighted;
private bool _isVisible;
public PropertyViewModel(ILayerProperty layerProperty, IPropertyVmFactory propertyVmFactory)
{
LayerProperty = layerProperty;
TreePropertyViewModel = propertyVmFactory.TreePropertyViewModel(LayerProperty, this);
TimelinePropertyViewModel = propertyVmFactory.TimelinePropertyViewModel(LayerProperty, this);
LayerProperty.VisibilityChanged += LayerPropertyOnVisibilityChanged;
_isVisible = !LayerProperty.IsHidden;
}
public ILayerProperty LayerProperty { get; }
public ITreePropertyViewModel TreePropertyViewModel { get; }
public ITimelinePropertyViewModel TimelinePropertyViewModel { get; }
public bool IsVisible
{
get => _isVisible;
set => RaiseAndSetIfChanged(ref _isVisible, value);
}
public bool IsHighlighted
{
get => _isHighlighted;
set => RaiseAndSetIfChanged(ref _isHighlighted, value);
}
public bool IsExpanded
{
get => _isExpanded;
set => RaiseAndSetIfChanged(ref _isExpanded, value);
}
private void LayerPropertyOnVisibilityChanged(object? sender, LayerPropertyEventArgs e)
{
IsVisible = !LayerProperty.IsHidden;
}
/// <inheritdoc />
public void Dispose()
{
LayerProperty.VisibilityChanged -= LayerPropertyOnVisibilityChanged;
}
}
@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public interface ITimelinePropertyViewModel : IReactiveObject
{
List<ITimelineKeyframeViewModel> GetAllKeyframeViewModels();
void WipeKeyframes(TimeSpan? start, TimeSpan? end);
void ShiftKeyframes(TimeSpan? start, TimeSpan? end, TimeSpan amount);
}
@@ -0,0 +1,31 @@
using System;
using Artemis.Core;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
public interface ITimelineKeyframeViewModel
{
bool IsSelected { get; }
TimeSpan Position { get; }
ILayerPropertyKeyframe Keyframe { get; }
#region Movement
void Select(bool expand, bool toggle);
void StartMovement(ITimelineKeyframeViewModel source);
void UpdateMovement(TimeSpan position);
void FinishMovement();
TimeSpan GetTimeSpanAtPosition(double x);
#endregion
#region Context menu actions
void PopulateEasingViewModels();
void Duplicate();
void Copy();
void Paste();
void Delete();
#endregion
}
@@ -0,0 +1,19 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:keyframes="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes.TimelineEasingView"
x:DataType="keyframes:TimelineEasingViewModel">
<StackPanel Orientation="Horizontal">
<Polyline Stroke="{DynamicResource TextFillColorPrimaryBrush}"
StrokeThickness="1"
Points="{CompiledBinding EasingPoints}"
Stretch="Uniform"
Width="20"
Height="20"
Margin="0 0 10 0" />
<TextBlock Text="{CompiledBinding Description}" />
</StackPanel>
</UserControl>
@@ -0,0 +1,18 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes
{
public partial class TimelineEasingView : UserControl
{
public TimelineEasingView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}
@@ -0,0 +1,33 @@
using System.Collections.Generic;
using Artemis.Core;
using Artemis.UI.Shared;
using Avalonia;
using Humanizer;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
public class TimelineEasingViewModel : ViewModelBase
{
private readonly ILayerPropertyKeyframe _keyframe;
public TimelineEasingViewModel(Easings.Functions easingFunction, ILayerPropertyKeyframe keyframe)
{
_keyframe = keyframe;
EasingFunction = easingFunction;
Description = easingFunction.Humanize();
EasingPoints = new List<Point>();
for (int i = 1; i <= 10; i++)
{
int x = i;
double y = Easings.Interpolate(i / 10.0, EasingFunction) * 10;
EasingPoints.Add(new Point(x, y));
}
}
public Easings.Functions EasingFunction { get; }
public List<Point> EasingPoints { get; }
public string Description { get; }
public bool IsEasingModeSelected => _keyframe.EasingFunction == EasingFunction;
}
@@ -0,0 +1,80 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes.TimelineKeyframeView"
ClipToBounds="False"
Height="{DynamicResource RailsHeight}">
<Ellipse Fill="{DynamicResource SystemAccentColorLight2}"
Stroke="White"
Width="10"
Height="10"
Margin="-5,0,0,0"
ToolTip.Tip="{Binding Timestamp}"
Classes.selected="{Binding IsSelected}"
PointerPressed="InputElement_OnPointerPressed"
PointerReleased="InputElement_OnPointerReleased"
PointerMoved="InputElement_OnPointerMoved"
Cursor="Hand">
<Ellipse.Styles>
<Style Selector="Ellipse">
<Setter Property="StrokeThickness" Value="0" />
</Style>
<Style Selector="Ellipse.selected">
<Setter Property="StrokeThickness" Value="1" />
</Style>
</Ellipse.Styles>
<Ellipse.Transitions>
<Transitions>
<DoubleTransition Property="StrokeThickness" Duration="0:0:0.25" />
</Transitions>
</Ellipse.Transitions>
<Ellipse.ContextFlyout>
<MenuFlyout Opening="FlyoutBase_OnOpening">
<MenuItem Header="Easing" Items="{Binding EasingViewModels}">
<MenuItem.Styles>
<Style Selector="MenuItem > MenuItem">
<Setter Property="Icon">
<Setter.Value>
<Template>
<CheckBox IsHitTestVisible="False" IsChecked="{Binding IsEasingModeSelected}" />
</Template>
</Setter.Value>
</Setter>
<Setter Property="Command" Value="{Binding $parent[UserControl].DataContext.SelectEasingFunction}"></Setter>
<Setter Property="CommandParameter" Value="{Binding EasingFunction}"></Setter>
</Style>
</MenuItem.Styles>
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Creation" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Duplicate" Command="{Binding $parent[timeline:TimelineView].DataContext.DuplicateKeyframes}" CommandParameter="{Binding}" InputGesture="Ctrl+D">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentDuplicate" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Copy" Command="{Binding $parent[timeline:TimelineView].DataContext.CopyKeyframes}" CommandParameter="{Binding}" InputGesture="Ctrl+C">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentCopy" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Paste" Command="{Binding $parent[timeline:TimelineView].DataContext.PasteKeyframes}" CommandParameter="{Binding}" InputGesture="Ctrl+V">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentPaste" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Delete" Command="{Binding $parent[timeline:TimelineView].DataContext.DeleteKeyframes}" CommandParameter="{Binding}" InputGesture="Delete">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Delete" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Ellipse.ContextFlyout>
</Ellipse>
</UserControl>
@@ -0,0 +1,85 @@
using System;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
public class TimelineKeyframeView : ReactiveUserControl<ITimelineKeyframeViewModel>
{
private TimelinePropertyView? _timelinePropertyView;
private TimelineView? _timelineView;
private bool _moved;
public TimelineKeyframeView()
{
InitializeComponent();
}
#region Overrides of TemplatedControl
/// <inheritdoc />
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_timelineView = this.FindLogicalAncestorOfType<TimelineView>();
_timelinePropertyView = this.FindLogicalAncestorOfType<TimelinePropertyView>();
base.OnAttachedToLogicalTree(e);
}
#endregion
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void InputElement_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (ViewModel == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
e.Pointer.Capture((IInputElement?) sender);
e.Handled = true;
_moved = false;
}
private void InputElement_OnPointerMoved(object? sender, PointerEventArgs e)
{
if (ViewModel == null || _timelineView?.ViewModel == null || e.Pointer.Captured != sender || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
if (!_moved)
_timelineView.ViewModel.StartKeyframeMovement(ViewModel);
_moved = true;
TimeSpan time = ViewModel.GetTimeSpanAtPosition(e.GetPosition(_timelinePropertyView).X);
_timelineView.ViewModel.UpdateKeyframeMovement(ViewModel, time, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control));
e.Handled = true;
}
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (ViewModel == null || _timelineView?.ViewModel == null || e.Pointer.Captured != sender || e.InitialPressMouseButton != MouseButton.Left)
return;
e.Pointer.Capture(null);
e.Handled = true;
// Select the keyframe if the user didn't move
if (!_moved)
ViewModel.Select(e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control));
else
{
TimeSpan time = ViewModel.GetTimeSpanAtPosition(e.GetPosition(_timelinePropertyView).X);
_timelineView.ViewModel.FinishKeyframeMovement(ViewModel, time, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control));
}
}
private void FlyoutBase_OnOpening(object? sender, EventArgs e)
{
ViewModel?.PopulateEasingViewModels();
}
}
@@ -0,0 +1,168 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins;
using DynamicData;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineKeyframeViewModel
{
private readonly IProfileEditorService _profileEditorService;
private double _x;
private string _timestamp;
private ObservableAsPropertyHelper<bool>? _isSelected;
public TimelineKeyframeViewModel(LayerPropertyKeyframe<T> layerPropertyKeyframe, IProfileEditorService profileEditorService)
{
_profileEditorService = profileEditorService;
_timestamp = "0.000";
LayerPropertyKeyframe = layerPropertyKeyframe;
EasingViewModels = new ObservableCollection<TimelineEasingViewModel>();
this.WhenActivated(d =>
{
_isSelected = profileEditorService.ConnectToKeyframes().ToCollection().Select(keyframes => keyframes.Contains(LayerPropertyKeyframe)).ToProperty(this, vm => vm.IsSelected).DisposeWith(d);
profileEditorService.ConnectToKeyframes();
profileEditorService.PixelsPerSecond.Subscribe(p =>
{
_pixelsPerSecond = p;
profileEditorService.PixelsPerSecond.Subscribe(_ => Update()).DisposeWith(d);
}).DisposeWith(d);
this.WhenAnyValue(vm => vm.LayerPropertyKeyframe.Position).Subscribe(_ => Update()).DisposeWith(d);
});
}
public LayerPropertyKeyframe<T> LayerPropertyKeyframe { get; }
public ObservableCollection<TimelineEasingViewModel> EasingViewModels { get; }
public double X
{
get => _x;
set => RaiseAndSetIfChanged(ref _x, value);
}
public string Timestamp
{
get => _timestamp;
set => RaiseAndSetIfChanged(ref _timestamp, value);
}
public bool IsSelected => _isSelected?.Value ?? false;
public TimeSpan Position => LayerPropertyKeyframe.Position;
public ILayerPropertyKeyframe Keyframe => LayerPropertyKeyframe;
public void Update()
{
X = _pixelsPerSecond * LayerPropertyKeyframe.Position.TotalSeconds;
Timestamp = $"{Math.Floor(LayerPropertyKeyframe.Position.TotalSeconds):00}.{LayerPropertyKeyframe.Position.Milliseconds:000}";
}
/// <inheritdoc />
public void Select(bool expand, bool toggle)
{
_profileEditorService.SelectKeyframe(Keyframe, expand, toggle);
}
public TimeSpan GetTimeSpanAtPosition(double x)
{
return TimeSpan.FromSeconds(x / _pixelsPerSecond);
}
#region Context menu actions
public void Duplicate()
{
throw new NotImplementedException();
}
public void Copy()
{
throw new NotImplementedException();
}
public void Paste()
{
throw new NotImplementedException();
}
public void Delete()
{
_profileEditorService.ExecuteCommand(new DeleteKeyframe(LayerPropertyKeyframe));
}
#endregion
#region Movement
private TimeSpan? _offset;
private TimeSpan? _startPosition;
private double _pixelsPerSecond;
public void StartMovement(ITimelineKeyframeViewModel source)
{
_startPosition = LayerPropertyKeyframe.Position;
if (source != this)
_offset = LayerPropertyKeyframe.Position - source.Position;
}
public void UpdateMovement(TimeSpan position)
{
if (_offset == null)
UpdatePosition(position);
else
UpdatePosition(position + _offset.Value);
}
public void FinishMovement()
{
MoveKeyframe command = _startPosition != null
? new MoveKeyframe(Keyframe, Keyframe.Position, _startPosition.Value)
: new MoveKeyframe(Keyframe, Keyframe.Position);
_startPosition = null;
_offset = null;
_profileEditorService.ExecuteCommand(command);
}
private void UpdatePosition(TimeSpan position)
{
if (position < TimeSpan.Zero)
LayerPropertyKeyframe.Position = TimeSpan.Zero;
else if (position > LayerPropertyKeyframe.LayerProperty.ProfileElement.Timeline.Length)
LayerPropertyKeyframe.Position = LayerPropertyKeyframe.LayerProperty.ProfileElement.Timeline.Length;
else
LayerPropertyKeyframe.Position = position;
}
#endregion
#region Easing
public void PopulateEasingViewModels()
{
if (EasingViewModels.Any())
return;
EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions))
.Cast<Easings.Functions>()
.Select(e => new TimelineEasingViewModel(e, Keyframe)));
}
public void SelectEasingFunction(Easings.Functions easingFunction)
{
_profileEditorService.ExecuteCommand(new ChangeKeyframeEasing(Keyframe, easingFunction));
}
#endregion
}
@@ -0,0 +1,58 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:segments="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="18"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments.EndSegmentView"
x:DataType="segments:EndSegmentViewModel">
<UserControl.Styles>
<StyleInclude Source="/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml" />
</UserControl.Styles>
<Border Classes="segment-container">
<Grid Name="SegmentGrid"
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}"
Width="{CompiledBinding Width}"
ColumnDefinitions="Auto, Auto,*,Auto">
<Rectangle Name="KeyframeDragVisualLeft"
Grid.Column="0"
Classes="resize-visual" />
<Button Grid.Column="1"
Name="AddMainSegment"
Classes="AppBarButton icon-button icon-button-small"
ToolTip.Tip="Add main segment"
Command="{Binding AddMainSegment}"
IsVisible="{CompiledBinding ShowAddMain}">
<avalonia:MaterialIcon Kind="PlusCircle" />
</Button>
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center" ClipToBounds="True">
<TextBlock Name="SegmentTitle"
FontSize="13"
ToolTip.Tip="This segment is played once a condition is no longer met">
End
</TextBlock>
<Button Name="SegmentClose"
Classes="AppBarButton icon-button icon-button-small"
ToolTip.Tip="Remove this segment"
Command="{Binding RemoveSegment}">
<avalonia:MaterialIcon Kind="CloseCircle" />
</Button>
</StackPanel>
<Rectangle Name="KeyframeDragVisualRight"
Grid.Column="3"
Classes="resize-visual" />
<Rectangle Name="KeyframeDragAnchor"
Grid.Column="3"
Classes="resize-anchor"
PointerPressed="KeyframeDragAnchor_OnPointerPressed"
PointerMoved="KeyframeDragAnchor_OnPointerMoved"
PointerReleased="KeyframeDragAnchor_OnPointerReleased"
ToolTip.Tip="{CompiledBinding EndTimestamp}"/>
</Grid>
</Border>
</UserControl>
@@ -0,0 +1,50 @@
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments
{
public partial class EndSegmentView : ReactiveUserControl<EndSegmentViewModel>
{
private readonly Rectangle _keyframeDragAnchor;
private double _dragOffset;
public EndSegmentView()
{
InitializeComponent();
_keyframeDragAnchor = this.Get<Rectangle>("KeyframeDragAnchor");
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void KeyframeDragAnchor_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (ViewModel == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
e.Pointer.Capture(_keyframeDragAnchor);
_dragOffset = ViewModel.Width - e.GetCurrentPoint(this).Position.X;
ViewModel.StartResize();
}
private void KeyframeDragAnchor_OnPointerMoved(object? sender, PointerEventArgs e)
{
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor))
return;
ViewModel.UpdateResize(e.GetCurrentPoint(this).Position.X + _dragOffset);
}
private void KeyframeDragAnchor_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor))
return;
e.Pointer.Capture(null);
ViewModel.FinishResize(e.GetCurrentPoint(this).Position.X + _dragOffset);
}
}
}
@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
public class EndSegmentViewModel : TimelineSegmentViewModel
{
private readonly IProfileEditorService _profileEditorService;
private RenderProfileElement? _profileElement;
private int _pixelsPerSecond;
private TimeSpan _time;
private ObservableAsPropertyHelper<double>? _start;
private ObservableAsPropertyHelper<double>? _end;
private ObservableAsPropertyHelper<string?>? _endTimestamp;
private readonly ObservableAsPropertyHelper<double> _width;
private TimeSpan _initialLength;
public EndSegmentViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
{
_profileEditorService = profileEditorService;
this.WhenActivated(d =>
{
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
profileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d);
profileEditorService.PixelsPerSecond.Subscribe(p => _pixelsPerSecond = p).DisposeWith(d);
_start = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.EndSegmentStartPosition) ?? Observable.Never<TimeSpan>())
.Switch()
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
.ToProperty(this, vm => vm.StartX)
.DisposeWith(d);
_end = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.EndSegmentEndPosition) ?? Observable.Never<TimeSpan>())
.Switch()
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
.ToProperty(this, vm => vm.EndX)
.DisposeWith(d);
_endTimestamp = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.EndSegmentEndPosition) ?? Observable.Never<TimeSpan>())
.Switch()
.Select(p => $"{Math.Floor(p.TotalSeconds):00}.{p.Milliseconds:000}")
.ToProperty(this, vm => vm.EndTimestamp)
.DisposeWith(d);
});
_width = this.WhenAnyValue(vm => vm.StartX, vm => vm.EndX).Select(t => t.Item2 - t.Item1).ToProperty(this, vm => vm.Width);
}
public override TimeSpan Start => _profileElement?.Timeline.EndSegmentStartPosition ?? TimeSpan.Zero;
public override double StartX => _start?.Value ?? 0;
public override TimeSpan End => _profileElement?.Timeline.EndSegmentEndPosition ?? TimeSpan.Zero;
public override double EndX => _end?.Value ?? 0;
public override TimeSpan Length
{
get => _profileElement?.Timeline.EndSegmentLength ?? TimeSpan.Zero;
set
{
if (_profileElement != null)
_profileElement.Timeline.EndSegmentLength = value;
}
}
public override double Width => _width.Value;
public override string? EndTimestamp => _endTimestamp?.Value;
public override ResizeTimelineSegment.SegmentType Type => ResizeTimelineSegment.SegmentType.End;
}
@@ -0,0 +1,80 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:segments="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="18"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments.MainSegmentView"
x:DataType="segments:MainSegmentViewModel">
<UserControl.Styles>
<StyleInclude Source="/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml" />
</UserControl.Styles>
<Border Classes="segment-container">
<Grid Name="SegmentGrid"
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}"
Width="{CompiledBinding Width}"
ColumnDefinitions="Auto,Auto,*,Auto,Auto">
<Rectangle Name="KeyframeDragVisualLeft"
Grid.Column="0"
IsVisible="{CompiledBinding !ShowAddStart}"
Classes="resize-visual" />
<Button Name="AddStartSegment"
Grid.Column="1"
Classes="AppBarButton icon-button icon-button-small"
VerticalAlignment="Center"
ToolTip.Tip="Add a start segment"
Command="{Binding AddStartSegment}"
IsVisible="{CompiledBinding ShowAddStart}">
<avalonia:MaterialIcon Kind="PlusCircle" />
</Button>
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Name="SegmentTitle"
VerticalAlignment="Center"
HorizontalAlignment="Center"
FontSize="13"
ToolTip.Tip="This segment is played while a condition is met, either once or on a repeating loop">
Main
</TextBlock>
<Button Name="SegmentClose"
Classes="AppBarButton icon-button icon-button-small"
ToolTip.Tip="Remove this segment"
Command="{Binding DisableSegment}">
<avalonia:MaterialIcon Kind="CloseCircle" />
</Button>
<ToggleButton Name="SegmentRepeat"
Classes="icon-button icon-button-small"
ToolTip.Tip="Repeat this segment"
IsChecked="{CompiledBinding RepeatSegment}"
Padding="0">
<avalonia:MaterialIcon Kind="Repeat" />
</ToggleButton>
</StackPanel>
<Button Name="AddEndSegment"
Grid.Column="3"
Classes="AppBarButton icon-button icon-button-small"
VerticalAlignment="Center"
ToolTip.Tip="Add an end segment"
Command="{Binding AddEndSegment}"
IsVisible="{CompiledBinding ShowAddEnd}">
<avalonia:MaterialIcon Kind="PlusCircle" />
</Button>
<Rectangle Name="KeyframeDragVisual"
Grid.Column="4"
Classes="resize-visual" />
<Rectangle Name="KeyframeDragAnchor"
Grid.Column="4"
Classes="resize-anchor"
PointerPressed="KeyframeDragAnchor_OnPointerPressed"
PointerMoved="KeyframeDragAnchor_OnPointerMoved"
PointerReleased="KeyframeDragAnchor_OnPointerReleased"
ToolTip.Tip="{CompiledBinding EndTimestamp}"/>
</Grid>
</Border>
</UserControl>
@@ -0,0 +1,62 @@
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments
{
public partial class MainSegmentView : ReactiveUserControl<MainSegmentViewModel>
{
private readonly Rectangle _keyframeDragAnchor;
private double _dragOffset;
public MainSegmentView()
{
InitializeComponent();
_keyframeDragAnchor = this.Get<Rectangle>("KeyframeDragAnchor");
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void KeyframeDragAnchor_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (ViewModel == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
e.Pointer.Capture(_keyframeDragAnchor);
_dragOffset = ViewModel.Width - e.GetCurrentPoint(this).Position.X;
ViewModel.StartResize();
}
private void KeyframeDragAnchor_OnPointerMoved(object? sender, PointerEventArgs e)
{
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor))
return;
ViewModel.UpdateResize(e.GetCurrentPoint(this).Position.X + _dragOffset);
}
private void KeyframeDragAnchor_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor))
return;
e.Pointer.Capture(null);
ViewModel.FinishResize(e.GetCurrentPoint(this).Position.X + _dragOffset);
}
private void KeyframeDragStartAnchor_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
}
private void KeyframeDragStartAnchor_OnPointerMoved(object? sender, PointerEventArgs e)
{
}
private void KeyframeDragStartAnchor_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
}
}
}
@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
public class MainSegmentViewModel : TimelineSegmentViewModel
{
private RenderProfileElement? _profileElement;
private ObservableAsPropertyHelper<double>? _start;
private ObservableAsPropertyHelper<double>? _end;
private ObservableAsPropertyHelper<string?>? _endTimestamp;
private ObservableAsPropertyHelper<bool>? _repeatSegment;
private readonly ObservableAsPropertyHelper<double> _width;
public MainSegmentViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
{
this.WhenActivated(d =>
{
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
_start = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.MainSegmentStartPosition) ?? Observable.Never<TimeSpan>())
.Switch()
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
.ToProperty(this, vm => vm.StartX)
.DisposeWith(d);
_end = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.MainSegmentEndPosition) ?? Observable.Never<TimeSpan>())
.Switch()
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
.ToProperty(this, vm => vm.EndX)
.DisposeWith(d);
_endTimestamp = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.MainSegmentEndPosition) ?? Observable.Never<TimeSpan>())
.Switch()
.Select(p => $"{Math.Floor(p.TotalSeconds):00}.{p.Milliseconds:000}")
.ToProperty(this, vm => vm.EndTimestamp)
.DisposeWith(d);
_repeatSegment = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.PlayMode) ?? Observable.Never<TimelinePlayMode>())
.Switch()
.Select(p => p == TimelinePlayMode.Repeat)
.ToProperty(this, vm => vm.RepeatSegment)
.DisposeWith(d);
});
_width = this.WhenAnyValue(vm => vm.StartX, vm => vm.EndX).Select(t => t.Item2 - t.Item1).ToProperty(this, vm => vm.Width);
}
public override TimeSpan Start => _profileElement?.Timeline.MainSegmentStartPosition ?? TimeSpan.Zero;
public override double StartX => _start?.Value ?? 0;
public override TimeSpan End => _profileElement?.Timeline.MainSegmentEndPosition ?? TimeSpan.Zero;
public override double EndX => _end?.Value ?? 0;
public override TimeSpan Length
{
get => _profileElement?.Timeline.MainSegmentLength ?? TimeSpan.Zero;
set
{
if (_profileElement != null)
_profileElement.Timeline.MainSegmentLength = value;
}
}
public override double Width => _width.Value;
public override string? EndTimestamp => _endTimestamp?.Value;
public override ResizeTimelineSegment.SegmentType Type => ResizeTimelineSegment.SegmentType.Main;
public bool RepeatSegment
{
get => _repeatSegment?.Value ?? false;
set => throw new NotImplementedException();
}
}
@@ -0,0 +1,28 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<Border Padding="20">
<!-- Add Controls for Previewer Here -->
</Border>
</Design.PreviewWith>
<Style Selector="ContentControl.segment-content-control">
</Style>
<Style Selector="Border.segment-container">
<Setter Property="Height" Value="18"></Setter>
</Style>
<Style Selector="Rectangle.resize-visual">
<Setter Property="Fill" Value="{DynamicResource SystemAccentColorLight2}"/>
<Setter Property="Width" Value="4"/>
<Setter Property="HorizontalAlignment" Value="Right"/>
</Style>
<Style Selector="Rectangle.resize-anchor">
<Setter Property="Fill" Value="Transparent"/>
<Setter Property="Width" Value="10"/>
<Setter Property="HorizontalAlignment" Value="Right"/>
<Setter Property="Cursor" Value="SizeWestEast"/>
</Style>
</Styles>
@@ -0,0 +1,56 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:segments="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="18"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments.StartSegmentView"
x:DataType="segments:StartSegmentViewModel">
<UserControl.Styles>
<StyleInclude Source="/Screens/ProfileEditor/Panels/Properties/Timeline/Segments/Segment.axaml" />
</UserControl.Styles>
<Border Classes="segment-container">
<Grid Name="SegmentGrid"
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}"
Width="{CompiledBinding Width}"
ColumnDefinitions="*,Auto,Auto">
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Name="SegmentTitle"
FontSize="13"
ToolTip.Tip="This segment is played when a layer starts displaying because it's conditions are met">
Start
</TextBlock>
<Button Name="SegmentClose"
Classes="AppBarButton icon-button icon-button-small"
ToolTip.Tip="Remove this segment"
Command="{Binding RemoveSegment}">
<avalonia:MaterialIcon Kind="CloseCircle" />
</Button>
</StackPanel>
<Button Grid.Column="1"
Name="AddMainSegment"
Classes="AppBarButton icon-button icon-button-small"
ToolTip.Tip="Add main segment"
Command="{Binding AddMainSegment}"
IsVisible="{CompiledBinding ShowAddMain}">
<avalonia:MaterialIcon Kind="PlusCircle" />
</Button>
<Rectangle Name="KeyframeDragVisual"
Grid.Column="2"
Classes="resize-visual" />
<Rectangle Name="KeyframeDragAnchor"
Grid.Column="2"
Classes="resize-anchor"
PointerPressed="KeyframeDragAnchor_OnPointerPressed"
PointerMoved="KeyframeDragAnchor_OnPointerMoved"
PointerReleased="KeyframeDragAnchor_OnPointerReleased"
ToolTip.Tip="{CompiledBinding EndTimestamp}"/>
</Grid>
</Border>
</UserControl>
@@ -0,0 +1,50 @@
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments
{
public partial class StartSegmentView : ReactiveUserControl<StartSegmentViewModel>
{
private readonly Rectangle _keyframeDragAnchor;
private double _dragOffset;
public StartSegmentView()
{
InitializeComponent();
_keyframeDragAnchor = this.Get<Rectangle>("KeyframeDragAnchor");
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void KeyframeDragAnchor_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (ViewModel == null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
e.Pointer.Capture(_keyframeDragAnchor);
_dragOffset = ViewModel.Width - e.GetCurrentPoint(this).Position.X;
ViewModel.StartResize();
}
private void KeyframeDragAnchor_OnPointerMoved(object? sender, PointerEventArgs e)
{
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor))
return;
ViewModel.UpdateResize(e.GetCurrentPoint(this).Position.X + _dragOffset);
}
private void KeyframeDragAnchor_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (ViewModel == null || !ReferenceEquals(e.Pointer.Captured, _keyframeDragAnchor))
return;
e.Pointer.Capture(null);
ViewModel.FinishResize(e.GetCurrentPoint(this).Position.X + _dragOffset);
}
}
}
@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins;
using Castle.DynamicProxy.Generators;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
public class StartSegmentViewModel : TimelineSegmentViewModel
{
private readonly IProfileEditorService _profileEditorService;
private RenderProfileElement? _profileElement;
private int _pixelsPerSecond;
private TimeSpan _time;
private ObservableAsPropertyHelper<double>? _end;
private ObservableAsPropertyHelper<string?>? _endTimestamp;
private readonly ObservableAsPropertyHelper<double> _width;
private TimeSpan _initialLength;
public StartSegmentViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
{
_profileEditorService = profileEditorService;
this.WhenActivated(d =>
{
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
profileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d);
profileEditorService.PixelsPerSecond.Subscribe(p => _pixelsPerSecond = p).DisposeWith(d);
_end = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.StartSegmentEndPosition) ?? Observable.Never<TimeSpan>())
.Switch()
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
.ToProperty(this, vm => vm.EndX)
.DisposeWith(d);
_endTimestamp = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.StartSegmentEndPosition) ?? Observable.Never<TimeSpan>())
.Switch()
.Select(p => $"{Math.Floor(p.TotalSeconds):00}.{p.Milliseconds:000}")
.ToProperty(this, vm => vm.EndTimestamp)
.DisposeWith(d);
});
_width = this.WhenAnyValue(vm => vm.StartX, vm => vm.EndX).Select(t => t.Item2 - t.Item1).ToProperty(this, vm => vm.Width);
}
public override TimeSpan Start => TimeSpan.Zero;
public override double StartX => 0;
public override TimeSpan End => _profileElement?.Timeline.StartSegmentEndPosition ?? TimeSpan.Zero;
public override double EndX => _end?.Value ?? 0;
public override TimeSpan Length
{
get => _profileElement?.Timeline.StartSegmentLength ?? TimeSpan.Zero;
set
{
if (_profileElement != null)
_profileElement.Timeline.StartSegmentLength = value;
}
}
public override double Width => _width.Value;
public override string? EndTimestamp => _endTimestamp?.Value;
public override ResizeTimelineSegment.SegmentType Type => ResizeTimelineSegment.SegmentType.Start;
}
@@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
public abstract class TimelineSegmentViewModel : ActivatableViewModelBase
{
private static readonly TimeSpan NewSegmentLength = TimeSpan.FromSeconds(2);
private readonly IProfileEditorService _profileEditorService;
private RenderProfileElement? _profileElement;
private int _pixelsPerSecond;
private Dictionary<ILayerPropertyKeyframe, TimeSpan> _originalKeyframePositions = new();
private ObservableAsPropertyHelper<bool>? _showAddStart;
private ObservableAsPropertyHelper<bool>? _showAddMain;
private ObservableAsPropertyHelper<bool>? _showAddEnd;
private TimeSpan _initialLength;
protected TimelineSegmentViewModel(IProfileEditorService profileEditorService)
{
_profileEditorService = profileEditorService;
this.WhenActivated(d =>
{
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
profileEditorService.PixelsPerSecond.Subscribe(p => _pixelsPerSecond = p).DisposeWith(d);
_showAddStart = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.StartSegmentLength) ?? Observable.Never<TimeSpan>())
.Switch()
.Select(t => t == TimeSpan.Zero)
.ToProperty(this, vm => vm.ShowAddStart)
.DisposeWith(d);
_showAddMain = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.MainSegmentLength) ?? Observable.Never<TimeSpan>())
.Switch()
.Select(t => t == TimeSpan.Zero)
.ToProperty(this, vm => vm.ShowAddMain)
.DisposeWith(d);
_showAddEnd = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.EndSegmentLength) ?? Observable.Never<TimeSpan>())
.Switch()
.Select(t => t == TimeSpan.Zero)
.ToProperty(this, vm => vm.ShowAddEnd)
.DisposeWith(d);
});
}
public bool ShowAddStart => _showAddStart?.Value ?? false;
public bool ShowAddMain => _showAddMain?.Value ?? false;
public bool ShowAddEnd => _showAddEnd?.Value ?? false;
public abstract TimeSpan Start { get; }
public abstract double StartX { get; }
public abstract TimeSpan End { get; }
public abstract double EndX { get; }
public abstract TimeSpan Length { get; set; }
public abstract double Width { get; }
public abstract string? EndTimestamp { get; }
public abstract ResizeTimelineSegment.SegmentType Type { get; }
public void AddStartSegment()
{
if (_profileElement == null)
return;
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Add start segment");
ShiftKeyframes(_profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes), NewSegmentLength);
ApplyPendingKeyframeMovement();
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(ResizeTimelineSegment.SegmentType.Start, _profileElement, NewSegmentLength));
}
public void AddMainSegment()
{
if (_profileElement == null)
return;
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Add main segment");
ShiftKeyframes(_profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes).Where(s => s.Position > _profileElement.Timeline.StartSegmentEndPosition), NewSegmentLength);
ApplyPendingKeyframeMovement();
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(ResizeTimelineSegment.SegmentType.Main, _profileElement, NewSegmentLength));
}
public void AddEndSegment()
{
if (_profileElement == null)
return;
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Add end segment");
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(ResizeTimelineSegment.SegmentType.End, _profileElement, NewSegmentLength));
}
public void StartResize()
{
if (_profileElement == null)
return;
_initialLength = Length;
}
public void UpdateResize(double x)
{
if (_profileElement == null)
return;
TimeSpan difference = GetTimeFromX(x) - Length;
List<ILayerPropertyKeyframe> keyframes = _profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes).ToList();
ShiftKeyframes(keyframes.Where(k => k.Position > End.Add(difference)), difference);
Length = GetTimeFromX(x);
}
public void FinishResize(double x)
{
if (_profileElement == null)
return;
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Resize segment");
ApplyPendingKeyframeMovement();
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(Type, _profileElement, GetTimeFromX(x), _initialLength));
}
public void RemoveSegment()
{
if (_profileElement == null)
return;
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope("Remove segment");
IEnumerable<ILayerPropertyKeyframe> keyframes = _profileElement.GetAllLayerProperties().SelectMany(p => p.UntypedKeyframes);
// Delete keyframes in the segment
foreach (ILayerPropertyKeyframe layerPropertyKeyframe in keyframes)
if (layerPropertyKeyframe.Position > Start && layerPropertyKeyframe.Position <= End)
_profileEditorService.ExecuteCommand(new DeleteKeyframe(layerPropertyKeyframe));
// Move keyframes after the segment forwards
ShiftKeyframes(keyframes.Where(s => s.Position > End), new TimeSpan(Length.Ticks * -1));
ApplyPendingKeyframeMovement();
_profileEditorService.ExecuteCommand(new ResizeTimelineSegment(Type, _profileElement, TimeSpan.Zero));
}
protected TimeSpan GetTimeFromX(double x)
{
TimeSpan length = TimeSpan.FromSeconds(x / _pixelsPerSecond);
if (length < TimeSpan.Zero)
length = TimeSpan.Zero;
return length;
}
protected void ShiftKeyframes(IEnumerable<ILayerPropertyKeyframe> keyframes, TimeSpan amount)
{
foreach (ILayerPropertyKeyframe layerPropertyKeyframe in keyframes)
{
if (!_originalKeyframePositions.ContainsKey(layerPropertyKeyframe))
_originalKeyframePositions[layerPropertyKeyframe] = layerPropertyKeyframe.Position;
layerPropertyKeyframe.Position = layerPropertyKeyframe.Position.Add(amount);
}
}
protected void ApplyPendingKeyframeMovement()
{
foreach ((ILayerPropertyKeyframe keyframe, TimeSpan originalPosition) in _originalKeyframePositions)
_profileEditorService.ExecuteCommand(new MoveKeyframe(keyframe, keyframe.Position, originalPosition));
_originalKeyframePositions.Clear();
}
}
@@ -0,0 +1,55 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:properties="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties"
xmlns:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.TimelineGroupView"
x:DataType="timeline:TimelineGroupViewModel">
<Grid RowDefinitions="28,1,Auto">
<ItemsControl Grid.Row="0"
Height="{DynamicResource RailsHeight}"
IsVisible="{CompiledBinding !PropertyGroupViewModel.IsExpanded}"
Items="{CompiledBinding KeyframePositions}"
HorizontalAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Styles>
<Style Selector="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding}" />
</Style>
</ItemsControl.Styles>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Ellipse Fill="{DynamicResource ControlFillColorDisabledBrush}"
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 ButtonBorderBrush}" Height="1" />
<ItemsControl Grid.Row="2"
Items="{CompiledBinding Children}"
IsVisible="{CompiledBinding PropertyGroupViewModel.IsExpanded}"
HorizontalAlignment="Stretch">
<ItemsControl.DataTemplates>
<DataTemplate DataType="properties:PropertyGroupViewModel">
<ContentControl Content="{CompiledBinding TimelineGroupViewModel}" IsVisible="{CompiledBinding IsVisible}" />
</DataTemplate>
<DataTemplate DataType="properties:PropertyViewModel">
<ContentControl Content="{CompiledBinding TimelinePropertyViewModel}" IsVisible="{CompiledBinding IsVisible}" />
</DataTemplate>
</ItemsControl.DataTemplates>
</ItemsControl>
</Grid>
</UserControl>
@@ -0,0 +1,17 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public class TimelineGroupView : ReactiveUserControl<TimelineGroupViewModel>
{
public TimelineGroupView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
@@ -0,0 +1,48 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Avalonia.Controls.Mixins;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public class TimelineGroupViewModel : ActivatableViewModelBase
{
private ObservableCollection<double>? _keyframePositions;
private int _pixelsPerSecond;
public TimelineGroupViewModel(PropertyGroupViewModel propertyGroupViewModel, IProfileEditorService profileEditorService)
{
PropertyGroupViewModel = propertyGroupViewModel;
this.WhenActivated(d =>
{
profileEditorService.PixelsPerSecond.Subscribe(p =>
{
_pixelsPerSecond = p;
UpdateKeyframePositions();
});
this.WhenAnyValue(vm => vm.PropertyGroupViewModel.IsExpanded).Subscribe(_ => UpdateKeyframePositions()).DisposeWith(d);
PropertyGroupViewModel.WhenAnyValue(vm => vm.IsExpanded).Subscribe(_ => this.RaisePropertyChanged(nameof(Children))).DisposeWith(d);
});
}
public PropertyGroupViewModel PropertyGroupViewModel { get; }
public ObservableCollection<ViewModelBase>? Children => PropertyGroupViewModel.IsExpanded ? PropertyGroupViewModel.Children : null;
public ObservableCollection<double>? KeyframePositions
{
get => _keyframePositions;
set => RaiseAndSetIfChanged(ref _keyframePositions, value);
}
private void UpdateKeyframePositions()
{
KeyframePositions = new ObservableCollection<double>(PropertyGroupViewModel
.GetAllKeyframeViewModels(false)
.Select(p => p.Position.TotalSeconds * _pixelsPerSecond));
}
}
@@ -0,0 +1,21 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.TimelinePropertyView">
<Border Height="{DynamicResource RailsBorderHeight}" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource ButtonBorderBrush}">
<ItemsControl Items="{Binding KeyframeViewModels}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Styles>
<Style Selector="ItemsControl > ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X, TargetNullValue=0}" />
</Style>
</ItemsControl.Styles>
</ItemsControl>
</Border>
</UserControl>
@@ -0,0 +1,17 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public class TimelinePropertyView : ReactiveUserControl<ITimelinePropertyViewModel>
{
public TimelinePropertyView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
@@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Avalonia.Controls.Mixins;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public class TimelinePropertyViewModel<T> : ActivatableViewModelBase, ITimelinePropertyViewModel
{
private readonly IProfileEditorService _profileEditorService;
public TimelinePropertyViewModel(LayerProperty<T> layerProperty, PropertyViewModel propertyViewModel, IProfileEditorService profileEditorService)
{
_profileEditorService = profileEditorService;
LayerProperty = layerProperty;
PropertyViewModel = propertyViewModel;
KeyframeViewModels = new ObservableCollection<TimelineKeyframeViewModel<T>>();
this.WhenActivated(d =>
{
Observable.FromEventPattern<LayerPropertyEventArgs>(x => LayerProperty.KeyframesToggled += x, x => LayerProperty.KeyframesToggled -= x)
.Subscribe(_ => UpdateKeyframes())
.DisposeWith(d);
Observable.FromEventPattern<LayerPropertyEventArgs>(x => LayerProperty.KeyframeAdded += x, x => LayerProperty.KeyframeAdded -= x)
.Subscribe(_ => UpdateKeyframes())
.DisposeWith(d);
Observable.FromEventPattern<LayerPropertyEventArgs>(x => LayerProperty.KeyframeRemoved += x, x => LayerProperty.KeyframeRemoved -= x)
.Subscribe(_ => UpdateKeyframes())
.DisposeWith(d);
UpdateKeyframes();
});
}
public LayerProperty<T> LayerProperty { get; }
public PropertyViewModel PropertyViewModel { get; }
public ObservableCollection<TimelineKeyframeViewModel<T>> KeyframeViewModels { get; }
private void UpdateKeyframes()
{
// Only show keyframes if they are enabled
if (LayerProperty.KeyframesEnabled)
{
List<LayerPropertyKeyframe<T>> keyframes = LayerProperty.Keyframes.ToList();
List<TimelineKeyframeViewModel<T>> toRemove = KeyframeViewModels.Where(t => !keyframes.Contains(t.LayerPropertyKeyframe)).ToList();
foreach (TimelineKeyframeViewModel<T> timelineKeyframeViewModel in toRemove)
KeyframeViewModels.Remove(timelineKeyframeViewModel);
List<TimelineKeyframeViewModel<T>> toAdd = keyframes.Where(k => KeyframeViewModels.All(t => t.LayerPropertyKeyframe != k))
.Select(k => new TimelineKeyframeViewModel<T>(k, _profileEditorService)).ToList();
foreach (TimelineKeyframeViewModel<T> timelineKeyframeViewModel in toAdd)
KeyframeViewModels.Add(timelineKeyframeViewModel);
}
else
{
KeyframeViewModels.Clear();
}
foreach (TimelineKeyframeViewModel<T> timelineKeyframeViewModel in KeyframeViewModels)
timelineKeyframeViewModel.Update();
}
#region Implementation of ITimelinePropertyViewModel
public List<ITimelineKeyframeViewModel> GetAllKeyframeViewModels()
{
return KeyframeViewModels.Cast<ITimelineKeyframeViewModel>().ToList();
}
public void WipeKeyframes(TimeSpan? start, TimeSpan? end)
{
start ??= TimeSpan.Zero;
end ??= TimeSpan.MaxValue;
List<LayerPropertyKeyframe<T>> toShift = LayerProperty.Keyframes.Where(k => k.Position >= start && k.Position < end).ToList();
foreach (LayerPropertyKeyframe<T> keyframe in toShift)
LayerProperty.RemoveKeyframe(keyframe);
UpdateKeyframes();
}
public void ShiftKeyframes(TimeSpan? start, TimeSpan? end, TimeSpan amount)
{
start ??= TimeSpan.Zero;
end ??= TimeSpan.MaxValue;
List<LayerPropertyKeyframe<T>> toShift = LayerProperty.Keyframes.Where(k => k.Position > start && k.Position < end).ToList();
foreach (LayerPropertyKeyframe<T> keyframe in toShift)
keyframe.Position += amount;
UpdateKeyframes();
}
#endregion
}
@@ -0,0 +1,30 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties"
xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared"
xmlns:timeline="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Timeline"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.TimelineView"
x:DataType="timeline:TimelineViewModel">
<UserControl.Resources>
<x:Double x:Key="RailsHeight">28</x:Double>
<x:Double x:Key="RailsBorderHeight">29</x:Double>
</UserControl.Resources>
<Grid Background="Transparent" PointerReleased="InputElement_OnPointerReleased" Focusable="True" MinWidth="{Binding MinWidth}">
<Grid.KeyBindings>
<KeyBinding Command="{Binding DeleteKeyframes}" Gesture="Delete"/>
</Grid.KeyBindings>
<ItemsControl Items="{CompiledBinding PropertyGroupViewModels}">
<ItemsControl.ItemTemplate>
<TreeDataTemplate DataType="{x:Type local:PropertyGroupViewModel}" ItemsSource="{CompiledBinding Children}">
<ContentControl Content="{CompiledBinding TimelineGroupViewModel}" />
</TreeDataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<controls:SelectionRectangle Name="SelectionRectangle" InputElement="{CompiledBinding $parent}" SelectionFinished="SelectionRectangle_OnSelectionFinished"></controls:SelectionRectangle>
</Grid>
</UserControl>
@@ -0,0 +1,51 @@
using System.Collections.Generic;
using System.Linq;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
using Artemis.UI.Shared.Controls;
using Artemis.UI.Shared.Events;
using Artemis.UI.Shared.Extensions;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public class TimelineView : ReactiveUserControl<TimelineViewModel>
{
private readonly SelectionRectangle _selectionRectangle;
public TimelineView()
{
InitializeComponent();
_selectionRectangle = this.Get<SelectionRectangle>("SelectionRectangle");
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void SelectionRectangle_OnSelectionFinished(object? sender, SelectionRectangleEventArgs e)
{
if (ViewModel == null)
return;
List<TimelineKeyframeView> keyframeViews = this.GetVisualChildrenOfType<TimelineKeyframeView>().Where(k =>
{
Rect hitTestRect = k.TransformedBounds != null ? k.TransformedBounds.Value.Bounds.TransformToAABB(k.TransformedBounds.Value.Transform) : Rect.Empty;
return e.AbsoluteRectangle.Intersects(hitTestRect);
}).ToList();
ViewModel.SelectKeyframes(keyframeViews.Where(kv => kv.ViewModel != null).Select(kv => kv.ViewModel!), e.KeyModifiers.HasFlag(KeyModifiers.Shift));
}
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (_selectionRectangle.IsSelecting)
return;
ViewModel?.SelectKeyframes(new List<ITimelineKeyframeViewModel>(), false);
}
}
@@ -0,0 +1,195 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Keyframes;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public class TimelineViewModel : ActivatableViewModelBase
{
private readonly IProfileEditorService _profileEditorService;
private ObservableAsPropertyHelper<double>? _caretPosition;
private ObservableAsPropertyHelper<int>? _pixelsPerSecond;
private List<ITimelineKeyframeViewModel>? _moveKeyframes;
private ObservableAsPropertyHelper<double> _minWidth;
public TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels,
StartSegmentViewModel startSegmentViewModel,
MainSegmentViewModel mainSegmentViewModel,
EndSegmentViewModel endSegmentViewModel,
IProfileEditorService profileEditorService)
{
PropertyGroupViewModels = propertyGroupViewModels;
StartSegmentViewModel = startSegmentViewModel;
MainSegmentViewModel = mainSegmentViewModel;
EndSegmentViewModel = endSegmentViewModel;
_profileEditorService = profileEditorService;
this.WhenActivated(d =>
{
_caretPosition = _profileEditorService.Time
.CombineLatest(_profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
.ToProperty(this, vm => vm.CaretPosition)
.DisposeWith(d);
_pixelsPerSecond = _profileEditorService.PixelsPerSecond.ToProperty(this, vm => vm.PixelsPerSecond).DisposeWith(d);
_minWidth = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.Length) ?? Observable.Never<TimeSpan>())
.Switch()
.CombineLatest(profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p + 100)
.ToProperty(this, vm => vm.MinWidth)
.DisposeWith(d);
});
}
public ObservableCollection<PropertyGroupViewModel> PropertyGroupViewModels { get; }
public StartSegmentViewModel StartSegmentViewModel { get; }
public MainSegmentViewModel MainSegmentViewModel { get; }
public EndSegmentViewModel EndSegmentViewModel { get; }
public double CaretPosition => _caretPosition?.Value ?? 0.0;
public int PixelsPerSecond => _pixelsPerSecond?.Value ?? 0;
public double MinWidth => _minWidth?.Value ?? 0;
public void ChangeTime(TimeSpan newTime)
{
_profileEditorService.ChangeTime(newTime);
}
public TimeSpan SnapToTimeline(TimeSpan time, bool snapToSegments, bool snapToCurrentTime, List<TimeSpan>? snapTimes = null)
{
TimeSpan tolerance = TimeSpan.FromMilliseconds(1000f / PixelsPerSecond * 5);
return _profileEditorService.SnapToTimeline(time, tolerance, snapToSegments, snapToCurrentTime, snapTimes);
}
public void SelectKeyframes(IEnumerable<ITimelineKeyframeViewModel> keyframes, bool expand)
{
_profileEditorService.SelectKeyframes(keyframes.Select(k => k.Keyframe), expand);
}
#region Keyframe movement
public void StartKeyframeMovement(ITimelineKeyframeViewModel source)
{
if (!source.IsSelected)
SelectKeyframes(new List<ITimelineKeyframeViewModel> {source}, false);
_moveKeyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList();
source.StartMovement(source);
foreach (ITimelineKeyframeViewModel keyframeViewModel in _moveKeyframes)
keyframeViewModel.StartMovement(source);
}
public void UpdateKeyframeMovement(ITimelineKeyframeViewModel source, TimeSpan time, bool snap, bool round)
{
if (_moveKeyframes == null)
return;
if (round)
time = _profileEditorService.RoundTime(time);
if (snap)
time = SnapToTimeline(time, true, true);
// Always update source first
source.UpdateMovement(time);
foreach (ITimelineKeyframeViewModel keyframeViewModel in _moveKeyframes)
keyframeViewModel.UpdateMovement(time);
}
public void FinishKeyframeMovement(ITimelineKeyframeViewModel source, TimeSpan time, bool snap, bool round)
{
if (_moveKeyframes == null)
return;
if (round)
time = _profileEditorService.RoundTime(time);
if (snap)
time = SnapToTimeline(time, true, true);
// If only one selected it's always the source, update it as a single command
if (_moveKeyframes.Count == 1)
{
source.UpdateMovement(time);
source.FinishMovement();
}
// Otherwise update in a scope
else
{
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope($"Move {_moveKeyframes.Count} keyframes.");
// Always update source first
source.UpdateMovement(time);
foreach (ITimelineKeyframeViewModel keyframeViewModel in _moveKeyframes)
{
keyframeViewModel.UpdateMovement(time);
keyframeViewModel.FinishMovement();
}
}
}
#endregion
#region Keyframe actions
public void DuplicateKeyframes(ITimelineKeyframeViewModel? source = null)
{
if (source is { IsSelected: false })
source.Delete();
else
{
List<ITimelineKeyframeViewModel> keyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList();
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope($"Duplicate {keyframes.Count} keyframes.");
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in keyframes)
timelineKeyframeViewModel.Duplicate();
}
}
public void CopyKeyframes(ITimelineKeyframeViewModel? source = null)
{
if (source is { IsSelected: false })
source.Copy();
else
{
List<ITimelineKeyframeViewModel> keyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList();
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope($"Copy {keyframes.Count} keyframes.");
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in keyframes)
timelineKeyframeViewModel.Copy();
}
}
public void PasteKeyframes(ITimelineKeyframeViewModel? source = null)
{
if (source is { IsSelected: false })
source.Paste();
else
{
List<ITimelineKeyframeViewModel> keyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList();
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope($"Paste {keyframes.Count} keyframes.");
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in keyframes)
timelineKeyframeViewModel.Paste();
}
}
public void DeleteKeyframes(ITimelineKeyframeViewModel? source = null)
{
if (source is {IsSelected: false})
source.Delete();
else
{
List<ITimelineKeyframeViewModel> keyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).Where(k => k.IsSelected).ToList();
using ProfileEditorCommandScope scope = _profileEditorService.CreateCommandScope($"Delete {keyframes.Count} keyframes.");
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in keyframes)
timelineKeyframeViewModel.Delete();
}
}
#endregion
}
@@ -0,0 +1,14 @@
using Artemis.Core.LayerBrushes;
using Artemis.UI.Shared;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Tree.Dialogs;
public class LayerBrushPresetViewModel : ContentDialogViewModelBase
{
public LayerBrushPresetViewModel(BaseLayerBrush layerBrush)
{
LayerBrush = layerBrush;
}
public BaseLayerBrush LayerBrush { get; }
}
@@ -0,0 +1,11 @@
using Artemis.Core;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Tree;
public interface ITreePropertyViewModel : IReactiveObject
{
ILayerProperty BaseLayerProperty { get; }
bool HasDataBinding { get; }
double GetDepth();
}
@@ -0,0 +1,196 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared"
xmlns:sharedConverters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:viewModel="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties.Tree"
xmlns:properties="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Tree.TreeGroupView">
<UserControl.Resources>
<converters:PropertyTreeMarginConverter x:Key="PropertyTreeMarginConverter" Length="20" />
<sharedConverters:EnumToBooleanConverter x:Key="EnumBoolConverter" />
</UserControl.Resources>
<UserControl.Styles>
<Style Selector="avalonia|MaterialIcon.chevron-collapsed">
<Setter Property="RenderTransform" Value="rotate(-90deg)" />
</Style>
</UserControl.Styles>
<StackPanel>
<Border Name="Bd"
BorderBrush="{DynamicResource ButtonBorderBrush}"
BorderThickness="0,0,0,1"
Height="29">
<Grid Margin="{Binding Converter={StaticResource PropertyTreeMarginConverter}}" ColumnDefinitions="19,*">
<avalonia:MaterialIcon Classes.chevron-collapsed="{Binding !PropertyGroupViewModel.IsExpanded}"
IsVisible="{Binding PropertyGroupViewModel.HasChildren}"
Kind="ChevronDown"
Grid.Column="0"
Margin="5 0"
PointerPressed="InputElement_OnPointerPressed"
Background="Transparent">
<avalonia:MaterialIcon.Transitions>
<Transitions>
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.2" />
</Transitions>
</avalonia:MaterialIcon.Transitions>
</avalonia:MaterialIcon>
<StackPanel Grid.Column="1">
<!-- Type: None -->
<TextBlock Text="{Binding LayerPropertyGroup.GroupDescription.Name}"
ToolTip.Tip="{Binding LayerPropertyGroup.GroupDescription.Description}"
VerticalAlignment="Center"
HorizontalAlignment="Left"
Margin="3 5 0 5"
IsVisible="{Binding GroupType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static viewModel:LayerPropertyGroupType.None}}" />
<!-- Type: General -->
<StackPanel Orientation="Horizontal"
Margin="0 5"
IsVisible="{Binding GroupType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static viewModel:LayerPropertyGroupType.General}}">
<avalonia:MaterialIcon Kind="HammerWrench" Margin="0 -1 5 0" />
<TextBlock ToolTip.Tip="{Binding LayerPropertyGroup.GroupDescription.Description}">General</TextBlock>
</StackPanel>
<!-- Type: Transform -->
<StackPanel Orientation="Horizontal"
Margin="0 5"
IsVisible="{Binding GroupType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static viewModel:LayerPropertyGroupType.Transform}}">
<avalonia:MaterialIcon Kind="TransitConnectionVariant" Margin="0 -1 5 0" />
<TextBlock ToolTip.Tip="{Binding LayerPropertyGroup.GroupDescription.Description}">Transform</TextBlock>
</StackPanel>
<!-- Type: LayerBrushRoot -->
<Grid IsVisible="{Binding GroupType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static viewModel:LayerPropertyGroupType.LayerBrushRoot}}"
ColumnDefinitions="Auto,Auto,Auto,*">
<controls:ArtemisIcon Grid.Column="0"
Icon="{Binding LayerBrush.Descriptor.Icon}"
Width="16"
Height="16"
Margin="0 5 5 0" />
<TextBlock Grid.Column="1"
ToolTip.Tip="{Binding LayerBrush.Descriptor.Description}"
Margin="0 5 0 0">
Brush -&#160;
</TextBlock>
<TextBlock Grid.Column="2"
Text="{Binding LayerBrush.Descriptor.DisplayName}"
ToolTip.Tip="{Binding LayerBrush.Descriptor.Description}"
Margin="0 5 0 0" />
<StackPanel Grid.Column="3"
Orientation="Horizontal"
HorizontalAlignment="Right"
IsVisible="{Binding LayerBrush.ConfigurationDialog, Converter={x:Static ObjectConverters.IsNotNull}}">
<TextBlock VerticalAlignment="Center">Extra options available!</TextBlock>
<avalonia:MaterialIcon Kind="ChevronRight" VerticalAlignment="Center">
<avalonia:MaterialIcon.RenderTransform>
<TranslateTransform X="0" />
</avalonia:MaterialIcon.RenderTransform>
</avalonia:MaterialIcon>
<Button Classes="icon-button" ToolTip.Tip="Open brush settings" Width="24" Height="24" HorizontalAlignment="Right" Command="{Binding OpenBrushSettings}">
<avalonia:MaterialIcon Kind="Settings" Height="16" Width="16" />
</Button>
</StackPanel>
</Grid>
<!-- Type: LayerEffectRoot -->
<Grid Height="24"
IsVisible="{Binding GroupType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static viewModel:LayerPropertyGroupType.LayerEffectRoot}}"
ColumnDefinitions="Auto,Auto,Auto,Auto,*,Auto">
<controls:ArtemisIcon
Grid.Column="0"
Cursor="SizeNorthSouth"
Icon="{Binding LayerEffect.Descriptor.Icon}"
Width="16"
Height="16"
Margin="0 5 5 0"
Background="Transparent" />
<TextBlock Grid.Column="1" ToolTip.Tip="{Binding LayerEffect.Descriptor.Description}" Margin="0 5 0 0">
Effect
</TextBlock>
<TextBlock Grid.Column="2"
ToolTip.Tip="{Binding LayerEffect.Descriptor.Description}"
Margin="3 5">
-
</TextBlock>
<!-- Show either the descriptors display name or, if set, the effect name -->
<TextBlock Grid.Column="3"
Text="{Binding LayerEffect.Descriptor.DisplayName}"
ToolTip.Tip="{Binding LayerEffect.Descriptor.Description}"
Margin="0 5"
IsVisible="{Binding !LayerEffect.Name, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
<TextBlock Grid.Column="4"
Text="{Binding LayerEffect.Name}"
ToolTip.Tip="{Binding LayerEffect.Descriptor.Description}"
Margin="0 5"
IsVisible="{Binding LayerEffect.Name, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
<StackPanel Grid.Column="5" Orientation="Horizontal">
<ToggleButton
Classes="icon-button"
ToolTip.Tip="Toggle suspended state"
Width="18"
Height="18"
IsChecked="{Binding !LayerEffect.Suspended}"
VerticalAlignment="Center" Padding="-25"
Margin="5 0"
Command="{Binding SuspendedToggled}">
<avalonia:MaterialIcon Kind="Eye" Height="13" Width="13" />
</ToggleButton>
<Button Classes="icon-button"
ToolTip.Tip="Rename"
Width="24"
Height="24"
VerticalAlignment="Center"
Command="{Binding RenameEffect}">
<avalonia:MaterialIcon Kind="RenameBox" Height="16" Width="16" />
</Button>
<Button Classes="icon-button"
ToolTip.Tip="Open effect settings"
Width="24"
Height="24"
VerticalAlignment="Center"
Command="{Binding OpenEffectSettings}"
IsVisible="{Binding LayerEffect.ConfigurationDialog, Converter={x:Static ObjectConverters.IsNotNull}}">
<avalonia:MaterialIcon Kind="Settings" Height="16" Width="16" />
</Button>
<Button Classes="icon-button"
ToolTip.Tip="Remove"
Width="24"
Height="24"
VerticalAlignment="Center"
Command="{Binding DeleteEffect}">
<avalonia:MaterialIcon Kind="TrashCan" Height="16" Width="16" />
</Button>
</StackPanel>
</Grid>
</StackPanel>
</Grid>
</Border>
<!--
Do not bind directly to the PropertyGroupViewModel.Children collection
Instead use a reference provided by the VM that is null when collapsed, virtualization for noobs
-->
<ItemsControl Items="{Binding Children}"
IsVisible="{Binding PropertyGroupViewModel.IsExpanded}"
HorizontalAlignment="Stretch">
<ItemsControl.DataTemplates>
<DataTemplate DataType="properties:PropertyGroupViewModel">
<ContentControl Content="{Binding TreeGroupViewModel}" IsVisible="{Binding IsVisible}" />
</DataTemplate>
<DataTemplate DataType="properties:PropertyViewModel">
<ContentControl Content="{Binding TreePropertyViewModel}" IsVisible="{Binding IsVisible}" />
</DataTemplate>
</ItemsControl.DataTemplates>
</ItemsControl>
</StackPanel>
</UserControl>
@@ -0,0 +1,24 @@
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Tree;
public class TreeGroupView : ReactiveUserControl<TreeGroupViewModel>
{
public TreeGroupView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void InputElement_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (ViewModel != null)
ViewModel.PropertyGroupViewModel.IsExpanded = !ViewModel.PropertyGroupViewModel.IsExpanded;
}
}
@@ -0,0 +1,167 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Reflection;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects;
using Artemis.UI.Exceptions;
using Artemis.UI.Screens.ProfileEditor.Properties.Windows;
using Artemis.UI.Shared;
using Artemis.UI.Shared.LayerBrushes;
using Artemis.UI.Shared.LayerEffects;
using Artemis.UI.Shared.Services.Interfaces;
using Artemis.UI.Shared.Services.ProfileEditor;
using Ninject;
using Ninject.Parameters;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Tree;
public class TreeGroupViewModel : ActivatableViewModelBase
{
private readonly IProfileEditorService _profileEditorService;
private readonly IWindowService _windowService;
private BrushConfigurationWindowViewModel? _brushConfigurationWindowViewModel;
private EffectConfigurationWindowViewModel? _effectConfigurationWindowViewModel;
public TreeGroupViewModel(PropertyGroupViewModel propertyGroupViewModel, IWindowService windowService, IProfileEditorService profileEditorService)
{
_windowService = windowService;
_profileEditorService = profileEditorService;
PropertyGroupViewModel = propertyGroupViewModel;
DetermineGroupType();
this.WhenActivated(d =>
{
PropertyGroupViewModel.WhenAnyValue(vm => vm.IsExpanded).Subscribe(_ => this.RaisePropertyChanged(nameof(Children))).DisposeWith(d);
Disposable.Create(CloseViewModels).DisposeWith(d);
});
// TODO: Update ProfileElementPropertyGroupViewModel visibility on change (can remove the sub on line 41 as well then)
}
public PropertyGroupViewModel PropertyGroupViewModel { get; }
public LayerPropertyGroup LayerPropertyGroup => PropertyGroupViewModel.LayerPropertyGroup;
public BaseLayerBrush? LayerBrush => PropertyGroupViewModel.LayerBrush;
public BaseLayerEffect? LayerEffect => PropertyGroupViewModel.LayerEffect;
public ObservableCollection<ViewModelBase>? Children => PropertyGroupViewModel.IsExpanded ? PropertyGroupViewModel.Children : null;
public LayerPropertyGroupType GroupType { get; private set; }
public async Task OpenBrushSettings()
{
if (LayerBrush?.ConfigurationDialog is not LayerBrushConfigurationDialog configurationViewModel)
return;
try
{
// Limit to one constructor, there's no need to have more and it complicates things anyway
ConstructorInfo[] constructors = configurationViewModel.Type.GetConstructors();
if (constructors.Length != 1)
throw new ArtemisUIException("Brush configuration dialogs must have exactly one constructor");
// Find the BaseLayerBrush parameter, it is required by the base constructor so its there for sure
ParameterInfo brushParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerBrush).IsAssignableFrom(p.ParameterType));
ConstructorArgument argument = new(brushParameter.Name!, LayerBrush);
BrushConfigurationViewModel viewModel =
(BrushConfigurationViewModel) LayerBrush.Descriptor.Provider.Plugin.Kernel!.Get(configurationViewModel.Type, argument);
_brushConfigurationWindowViewModel = new BrushConfigurationWindowViewModel(viewModel, configurationViewModel);
await _windowService.ShowDialogAsync(_brushConfigurationWindowViewModel);
// Save changes after the dialog closes
await _profileEditorService.SaveProfileAsync();
}
catch (Exception e)
{
_windowService.ShowExceptionDialog("An exception occurred while trying to show the brush's settings window", e);
}
}
public async Task OpenEffectSettings()
{
if (LayerEffect?.ConfigurationDialog is not LayerEffectConfigurationDialog configurationViewModel)
return;
try
{
// Limit to one constructor, there's no need to have more and it complicates things anyway
ConstructorInfo[] constructors = configurationViewModel.Type.GetConstructors();
if (constructors.Length != 1)
throw new ArtemisUIException("Effect configuration dialogs must have exactly one constructor");
// Find the BaseLayerEffect parameter, it is required by the base constructor so its there for sure
ParameterInfo effectParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerEffect).IsAssignableFrom(p.ParameterType));
ConstructorArgument argument = new(effectParameter.Name!, LayerEffect);
EffectConfigurationViewModel viewModel =
(EffectConfigurationViewModel) LayerEffect.Descriptor.Provider.Plugin.Kernel!.Get(configurationViewModel.Type, argument);
_effectConfigurationWindowViewModel = new EffectConfigurationWindowViewModel(viewModel, configurationViewModel);
await _windowService.ShowDialogAsync(_effectConfigurationWindowViewModel);
// Save changes after the dialog closes
await _profileEditorService.SaveProfileAsync();
}
catch (Exception e)
{
_windowService.ShowExceptionDialog("An exception occurred while trying to show the effect's settings window", e);
}
}
public async Task RenameEffect()
{
await _windowService.ShowConfirmContentDialog("Not yet implemented", "Try again later :p");
}
public async Task DeleteEffect()
{
await _windowService.ShowConfirmContentDialog("Not yet implemented", "Try again later :p");
}
public double GetDepth()
{
int depth = 0;
LayerPropertyGroup? current = LayerPropertyGroup.Parent;
while (current != null)
{
depth++;
current = current.Parent;
}
return depth;
}
private void CloseViewModels()
{
_effectConfigurationWindowViewModel?.Close(null);
_brushConfigurationWindowViewModel?.Close(null);
}
private void DetermineGroupType()
{
if (LayerPropertyGroup is LayerGeneralProperties)
GroupType = LayerPropertyGroupType.General;
else if (LayerPropertyGroup is LayerTransformProperties)
GroupType = LayerPropertyGroupType.Transform;
else if (LayerPropertyGroup.Parent == null && PropertyGroupViewModel.LayerBrush != null)
GroupType = LayerPropertyGroupType.LayerBrushRoot;
else if (LayerPropertyGroup.Parent == null && PropertyGroupViewModel.LayerEffect != null)
GroupType = LayerPropertyGroupType.LayerEffectRoot;
else
GroupType = LayerPropertyGroupType.None;
}
}
public enum LayerPropertyGroupType
{
General,
Transform,
LayerBrushRoot,
LayerEffectRoot,
None
}
@@ -0,0 +1,65 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:sharedConverters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Tree.TreePropertyView">
<UserControl.Resources>
<converters:PropertyTreeMarginConverter x:Key="PropertyTreeMarginConverter" Length="20" />
</UserControl.Resources>
<Border Name="Bd"
BorderBrush="{DynamicResource ButtonBorderBrush}"
BorderThickness="0,0,0,1"
Height="29">
<Grid Margin="{Binding Converter={StaticResource PropertyTreeMarginConverter}}" ColumnDefinitions="Auto,*,Auto,Auto,Auto">
<ToggleButton Grid.Column="0"
Classes="icon-button"
ToolTip.Tip="Toggle key-framing"
Width="24"
Height="24"
IsChecked="{Binding KeyframesEnabled}"
IsEnabled="{Binding LayerProperty.KeyframesSupported}"
VerticalAlignment="Center" Padding="-25">
<avalonia:MaterialIcon Kind="Stopwatch" />
</ToggleButton>
<TextBlock Grid.Column="1"
Margin="5,0,0,0"
Padding="0,0,5,0"
VerticalAlignment="Center"
TextTrimming="CharacterEllipsis"
Text="{Binding LayerProperty.PropertyDescription.Name}"
ToolTip.Tip="{Binding LayerProperty.PropertyDescription.Description}"
HorizontalAlignment="Left" />
<ContentControl Grid.Column="2"
Margin="5 0"
Content="{Binding PropertyInputViewModel}"/>
<Button Grid.Column="3"
Margin="0 0 2 0"
Command="{Binding ResetToDefault}"
Classes="icon-button"
ToolTip.Tip="Reset the property to its default value."
Width="24"
Height="24">
<avalonia:MaterialIcon Kind="BackupRestore" />
</Button>
<ToggleButton Grid.Column="4" Classes="icon-button"
Margin="2 0 0 0"
ToolTip.Tip="Change the property's data binding"
Width="24"
Height="24"
VerticalAlignment="Center"
IsEnabled="{Binding LayerProperty.DataBindingsSupported}"
IsChecked="{Binding HasDataBinding, Mode=OneWay}">
<avalonia:MaterialIcon Kind="VectorLink" />
</ToggleButton>
</Grid>
</Border>
</UserControl>
@@ -0,0 +1,30 @@
using System;
using System.Reactive.Linq;
using Artemis.Core;
using Avalonia.Controls;
using Avalonia.Controls.Mixins;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Tree;
public class TreePropertyView : ReactiveUserControl<ITreePropertyViewModel>
{
public TreePropertyView()
{
this.WhenActivated(d =>
{
if (ViewModel != null)
Observable.FromEventPattern<LayerPropertyEventArgs>(e => ViewModel.BaseLayerProperty.CurrentValueSet += e, e => ViewModel.BaseLayerProperty.CurrentValueSet -= e)
.Subscribe(_ => this.BringIntoView())
.DisposeWith(d);
});
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
@@ -0,0 +1,76 @@
using System;
using System.Reactive;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Artemis.UI.Shared.Services.PropertyInput;
using Avalonia.Controls.Mixins;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Tree;
internal class TreePropertyViewModel<T> : ActivatableViewModelBase, ITreePropertyViewModel
{
private readonly IProfileEditorService _profileEditorService;
private TimeSpan _time;
public TreePropertyViewModel(LayerProperty<T> layerProperty, PropertyViewModel propertyViewModel, IProfileEditorService profileEditorService, IPropertyInputService propertyInputService)
{
_profileEditorService = profileEditorService;
LayerProperty = layerProperty;
PropertyViewModel = propertyViewModel;
PropertyInputViewModel = propertyInputService.CreatePropertyInputViewModel(LayerProperty);
this.WhenActivated(d =>
{
_profileEditorService.Time.Subscribe(t => _time = t).DisposeWith(d);
this.WhenAnyValue(vm => vm.LayerProperty.KeyframesEnabled).Subscribe(_ => this.RaisePropertyChanged(nameof(KeyframesEnabled))).DisposeWith(d);
});
ResetToDefault = ReactiveCommand.Create(ExecuteResetToDefault, Observable.Return(LayerProperty.DefaultValue != null));
}
private void ExecuteResetToDefault()
{
_profileEditorService.ExecuteCommand(new ResetLayerProperty<T>(LayerProperty));
}
public LayerProperty<T> LayerProperty { get; }
public PropertyViewModel PropertyViewModel { get; }
public PropertyInputViewModel<T>? PropertyInputViewModel { get; }
public ReactiveCommand<Unit, Unit> ResetToDefault { get; }
public bool KeyframesEnabled
{
get => LayerProperty.KeyframesEnabled;
set => UpdateKeyframesEnabled(value);
}
private void UpdateKeyframesEnabled(bool value)
{
if (value == LayerProperty.KeyframesEnabled)
return;
_profileEditorService.ExecuteCommand(new ToggleLayerPropertyKeyframes<T>(LayerProperty, value, _time));
}
public ILayerProperty BaseLayerProperty => LayerProperty;
public bool HasDataBinding => LayerProperty.HasDataBinding;
public double GetDepth()
{
int depth = 0;
LayerPropertyGroup? current = LayerProperty.LayerPropertyGroup;
while (current != null)
{
depth++;
current = current.Parent;
}
return depth;
}
}
@@ -0,0 +1,16 @@
<controls:CoreWindow xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d"
d:DesignWidth="800"
d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Windows.BrushConfigurationWindowView"
Title="Artemis | Brush configuration"
Width="{Binding Configuration.DialogWidth}"
Height="{Binding Configuration.DialogHeight}">
<Panel>
<ContentControl Content="{Binding ConfigurationViewModel}" />
</Panel>
</controls:CoreWindow>
@@ -0,0 +1,27 @@
using System.ComponentModel;
using Avalonia;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Windows;
public class BrushConfigurationWindowView : ReactiveCoreWindow<EffectConfigurationWindowViewModel>
{
public BrushConfigurationWindowView()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
Closing += OnClosing;
}
private void OnClosing(object? sender, CancelEventArgs e)
{
e.Cancel = ViewModel?.CanClose() ?? true;
}
}
@@ -0,0 +1,31 @@
using System;
using Artemis.UI.Shared;
using Artemis.UI.Shared.LayerBrushes;
using Avalonia.Threading;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Windows;
public class BrushConfigurationWindowViewModel : DialogViewModelBase<object?>
{
public BrushConfigurationWindowViewModel(BrushConfigurationViewModel configurationViewModel, LayerBrushConfigurationDialog configuration)
{
ConfigurationViewModel = configurationViewModel;
Configuration = configuration;
ConfigurationViewModel.CloseRequested += ConfigurationViewModelOnCloseRequested;
}
public BrushConfigurationViewModel ConfigurationViewModel { get; }
public LayerBrushConfigurationDialog Configuration { get; }
public bool CanClose()
{
return ConfigurationViewModel.CanClose() && Dispatcher.UIThread.InvokeAsync(async () => await ConfigurationViewModel.CanCloseAsync()).GetAwaiter().GetResult();
}
private void ConfigurationViewModelOnCloseRequested(object? sender, EventArgs e)
{
if (CanClose())
Close(null);
}
}
@@ -0,0 +1,16 @@
<controls:CoreWindow xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d"
d:DesignWidth="800"
d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Windows.EffectConfigurationWindowView"
Title="Artemis | Effect configuration"
Width="{Binding Configuration.DialogWidth}"
Height="{Binding Configuration.DialogHeight}">
<Panel>
<ContentControl Content="{Binding ConfigurationViewModel}" />
</Panel>
</controls:CoreWindow>
@@ -0,0 +1,27 @@
using System.ComponentModel;
using Avalonia;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Windows;
public class EffectConfigurationWindowView : ReactiveCoreWindow<EffectConfigurationWindowViewModel>
{
public EffectConfigurationWindowView()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
Closing += OnClosing;
}
private void OnClosing(object? sender, CancelEventArgs e)
{
e.Cancel = ViewModel?.CanClose() ?? true;
}
}
@@ -0,0 +1,31 @@
using System;
using Artemis.UI.Shared;
using Artemis.UI.Shared.LayerEffects;
using Avalonia.Threading;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Windows;
public class EffectConfigurationWindowViewModel : DialogViewModelBase<object?>
{
public EffectConfigurationWindowViewModel(EffectConfigurationViewModel configurationViewModel, LayerEffectConfigurationDialog configuration)
{
ConfigurationViewModel = configurationViewModel;
Configuration = configuration;
ConfigurationViewModel.CloseRequested += ConfigurationViewModelOnCloseRequested;
}
public EffectConfigurationViewModel ConfigurationViewModel { get; }
public LayerEffectConfigurationDialog Configuration { get; }
public bool CanClose()
{
return ConfigurationViewModel.CanClose() && Dispatcher.UIThread.InvokeAsync(async () => await ConfigurationViewModel.CanCloseAsync()).GetAwaiter().GetResult();
}
private void ConfigurationViewModelOnCloseRequested(object? sender, EventArgs e)
{
if (CanClose())
Close(null);
}
}