1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2026-01-01 02:03:32 +00:00

Profile editor - Ported keyframe display and selection

This commit is contained in:
Robert 2022-01-18 23:27:35 +01:00
parent 66a2e51979
commit 98180df5f2
62 changed files with 1199 additions and 641 deletions

View File

@ -73,7 +73,4 @@
<ItemGroup> <ItemGroup>
<Resource Include="Resources\Fonts\RobotoMono-Regular.ttf" /> <Resource Include="Resources\Fonts\RobotoMono-Regular.ttf" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Extensions\" />
</ItemGroup>
</Project> </Project>

View File

@ -44,9 +44,11 @@ namespace Artemis.UI.Shared.Controls
AvaloniaProperty.Register<SelectionRectangle, IControl?>(nameof(InputElement), notifying: OnInputElementChanged); AvaloniaProperty.Register<SelectionRectangle, IControl?>(nameof(InputElement), notifying: OnInputElementChanged);
private Rect? _displayRect; private Rect? _displayRect;
private Rect? _absoluteRect;
private IControl? _oldInputElement; private IControl? _oldInputElement;
private Point _startPosition; private Point _startPosition;
private Point _absoluteStartPosition;
/// <inheritdoc /> /// <inheritdoc />
public SelectionRectangle() public SelectionRectangle()
{ {
@ -140,6 +142,7 @@ namespace Artemis.UI.Shared.Controls
e.Pointer.Capture(this); e.Pointer.Capture(this);
_startPosition = e.GetPosition(Parent); _startPosition = e.GetPosition(Parent);
_absoluteStartPosition = e.GetPosition(VisualRoot);
_displayRect = null; _displayRect = null;
} }
@ -149,11 +152,18 @@ namespace Artemis.UI.Shared.Controls
return; return;
Point currentPosition = e.GetPosition(Parent); Point currentPosition = e.GetPosition(Parent);
Point absoluteCurrentPosition = e.GetPosition(VisualRoot);
_displayRect = new Rect( _displayRect = new Rect(
new Point(Math.Min(_startPosition.X, currentPosition.X), Math.Min(_startPosition.Y, currentPosition.Y)), new Point(Math.Min(_startPosition.X, currentPosition.X), Math.Min(_startPosition.Y, currentPosition.Y)),
new Point(Math.Max(_startPosition.X, currentPosition.X), Math.Max(_startPosition.Y, currentPosition.Y)) new Point(Math.Max(_startPosition.X, currentPosition.X), Math.Max(_startPosition.Y, currentPosition.Y))
); );
OnSelectionUpdated(new SelectionRectangleEventArgs(_displayRect.Value, e.KeyModifiers)); _absoluteRect = new Rect(
new Point(Math.Min(_absoluteStartPosition.X, absoluteCurrentPosition.X), Math.Min(_absoluteStartPosition.Y, absoluteCurrentPosition.Y)),
new Point(Math.Max(_absoluteStartPosition.X, absoluteCurrentPosition.X), Math.Max(_absoluteStartPosition.Y, absoluteCurrentPosition.Y))
);
OnSelectionUpdated(new SelectionRectangleEventArgs(_displayRect.Value, _absoluteRect.Value, e.KeyModifiers));
InvalidateVisual(); InvalidateVisual();
} }
@ -164,8 +174,11 @@ namespace Artemis.UI.Shared.Controls
e.Pointer.Capture(null); e.Pointer.Capture(null);
if (_displayRect != null) if (_displayRect != null && _absoluteRect != null)
OnSelectionFinished(new SelectionRectangleEventArgs(_displayRect.Value, e.KeyModifiers)); {
OnSelectionFinished(new SelectionRectangleEventArgs(_displayRect.Value, _absoluteRect.Value, e.KeyModifiers));
e.Handled = true;
}
_displayRect = null; _displayRect = null;
InvalidateVisual(); InvalidateVisual();

View File

@ -13,17 +13,23 @@ namespace Artemis.UI.Shared.Events
/// <summary> /// <summary>
/// Creates a new instance of the <see cref="SelectionRectangleEventArgs" /> class. /// Creates a new instance of the <see cref="SelectionRectangleEventArgs" /> class.
/// </summary> /// </summary>
public SelectionRectangleEventArgs(Rect rectangle, KeyModifiers keyModifiers) public SelectionRectangleEventArgs(Rect rectangle, Rect absoluteRectangle, KeyModifiers keyModifiers)
{ {
KeyModifiers = keyModifiers; KeyModifiers = keyModifiers;
Rectangle = rectangle; Rectangle = rectangle;
AbsoluteRectangle = absoluteRectangle;
} }
/// <summary> /// <summary>
/// Gets the rectangle that was selected when the event occurred. /// Gets the rectangle relative to the parent that was selected when the event occurred.
/// </summary> /// </summary>
public Rect Rectangle { get; } public Rect Rectangle { get; }
/// <summary>
/// Gets the rectangle relative to the window that was selected when the event occurred.
/// </summary>
public Rect AbsoluteRectangle { get; }
/// <summary> /// <summary>
/// Gets the key modifiers that where pressed when the event occurred. /// Gets the key modifiers that where pressed when the event occurred.
/// </summary> /// </summary>

View File

@ -0,0 +1,62 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.VisualTree;
namespace Artemis.UI.Shared.Extensions;
/// <summary>
/// Provides extension methods for Avalonia's <see cref="IVisual" /> type
/// </summary>
public static class VisualExtensions
{
/// <summary>
/// Returns a recursive list of all visual children of type <typeparamref name="T" />.
/// </summary>
/// <typeparam name="T">The type the children should have.</typeparam>
/// <param name="root">The root visual at which to start searching.</param>
/// <returns>A recursive list of all visual children of type <typeparamref name="T" />.</returns>
public static List<T> GetVisualChildrenOfType<T>(this IVisual root)
{
List<T> result = new();
List<IVisual>? visualChildren = root.GetVisualChildren()?.ToList();
if (visualChildren == null || !visualChildren.Any())
return result;
foreach (IVisual visualChild in visualChildren)
{
if (visualChild is T toFind)
result.Add(toFind);
result.AddRange(GetVisualChildrenOfType<T>(visualChild));
}
return result;
}
/// <summary>
/// Returns a recursive list of all visual children with a data context of type <typeparamref name="T" />.
/// </summary>
/// <typeparam name="T">The type of data context the children should have.</typeparam>
/// <param name="root">The root visual at which to start searching.</param>
/// <returns>A recursive list of all visual children with a data context of type <typeparamref name="T" />.</returns>
public static List<T> GetVisualChildrenOfDataContextType<T>(this IVisual root)
{
List<T> result = new();
List<IVisual>? visualChildren = root.GetVisualChildren()?.ToList();
if (visualChildren == null || !visualChildren.Any())
return result;
foreach (IVisual visualChild in visualChildren)
{
if (visualChild is IDataContextProvider dataContextProvider && dataContextProvider.DataContext is T toFind)
result.Add(toFind);
result.AddRange(GetVisualChildrenOfType<T>(visualChild));
}
return result;
}
}

View File

@ -0,0 +1,41 @@
using Artemis.Core;
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
/// <summary>
/// Represents a profile editor command that can be used to enable or disable keyframes on a layer property.
/// </summary>
/// <typeparam name="T"></typeparam>
public class ToggleLayerPropertyKeyframes<T> : IProfileEditorCommand
{
private readonly bool _enable;
private readonly LayerProperty<T> _layerProperty;
/// <summary>
/// Creates a new instance of the <see cref="ToggleLayerPropertyKeyframes{T}"/> class.
/// </summary>
public ToggleLayerPropertyKeyframes(LayerProperty<T> layerProperty, bool enable)
{
_layerProperty = layerProperty;
_enable = enable;
}
#region Implementation of IProfileEditorCommand
/// <inheritdoc />
public string DisplayName => _enable ? "Enable keyframes" : "Disable keyframes";
/// <inheritdoc />
public void Execute()
{
_layerProperty.KeyframesEnabled = _enable;
}
/// <inheritdoc />
public void Undo()
{
_layerProperty.KeyframesEnabled = !_enable;
}
#endregion
}

View File

@ -22,6 +22,7 @@ public abstract class PropertyInputViewModel<T> : PropertyInputViewModel
[AllowNull] private T _inputValue; [AllowNull] private T _inputValue;
private TimeSpan _time; private TimeSpan _time;
private bool _updating;
/// <summary> /// <summary>
/// Creates a new instance of the <see cref="PropertyInputViewModel{T}" /> class /// Creates a new instance of the <see cref="PropertyInputViewModel{T}" /> class
@ -156,6 +157,9 @@ public abstract class PropertyInputViewModel<T> : PropertyInputViewModel
/// </summary> /// </summary>
protected virtual void ApplyInputValue() protected virtual void ApplyInputValue()
{ {
if (_updating)
return;
if (InputDragging) if (InputDragging)
ProfileEditorService.ChangeTime(_time); ProfileEditorService.ChangeTime(_time);
else if (ValidationContext.IsValid) else if (ValidationContext.IsValid)
@ -164,16 +168,25 @@ public abstract class PropertyInputViewModel<T> : PropertyInputViewModel
private void UpdateInputValue() private void UpdateInputValue()
{ {
// Avoid unnecessary UI updates and validator cycles try
if (_inputValue != null && _inputValue.Equals(LayerProperty.CurrentValue) || _inputValue == null && LayerProperty.CurrentValue == null) {
return; _updating = true;
// Avoid unnecessary UI updates and validator cycles
if (_inputValue != null && _inputValue.Equals(LayerProperty.CurrentValue) || _inputValue == null && LayerProperty.CurrentValue == null)
return;
// Override the input value // Override the input value
_inputValue = LayerProperty.CurrentValue; _inputValue = LayerProperty.CurrentValue;
// Notify a change in the input value // Notify a change in the input value
OnInputValueChanged(); OnInputValueChanged();
this.RaisePropertyChanged(nameof(InputValue)); this.RaisePropertyChanged(nameof(InputValue));
}
finally
{
_updating = false;
}
} }
private void UpdateDataBinding() private void UpdateDataBinding()

View File

@ -48,4 +48,9 @@
<Resource Include="Assets\Images\Logo\bow.ico" /> <Resource Include="Assets\Images\Logo\bow.ico" />
<Resource Include="Assets\Images\Logo\bow.svg" /> <Resource Include="Assets\Images\Logo\bow.svg" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Update="Screens\ProfileEditor\Panels\Properties\PropertiesView.axaml.cs">
<DependentUpon>PropertiesView.axaml</DependentUpon>
</Compile>
</ItemGroup>
</Project> </Project>

View File

@ -1,6 +1,6 @@
using System; using System;
using System.Globalization; using System.Globalization;
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
using Avalonia; using Avalonia;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;

View File

@ -5,7 +5,7 @@ using System.Reactive.Linq;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.LayerBrushes; using Artemis.Core.LayerBrushes;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree.Dialogs; using Artemis.UI.Screens.ProfileEditor.Properties.Tree.Dialogs;
using Artemis.UI.Shared.Services.Interfaces; using Artemis.UI.Shared.Services.Interfaces;
using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands; using Artemis.UI.Shared.Services.ProfileEditor.Commands;

View File

@ -12,6 +12,7 @@
Value="{Binding InputValue}" Value="{Binding InputValue}"
SmallChange="{Binding LayerProperty.PropertyDescription.InputStepSize}" SmallChange="{Binding LayerProperty.PropertyDescription.InputStepSize}"
AcceptsExpression="True" AcceptsExpression="True"
SimpleNumberFormat="F3"
VerticalAlignment="Center"/> VerticalAlignment="Center"/>
<TextBlock Width="25" Text="{Binding LayerProperty.PropertyDescription.InputAffix}" VerticalAlignment="Center"/> <TextBlock Width="25" Text="{Binding LayerProperty.PropertyDescription.InputAffix}" VerticalAlignment="Center"/>
</StackPanel> </StackPanel>

View File

@ -13,11 +13,13 @@
Value="{Binding X}" Value="{Binding X}"
SmallChange="{Binding LayerProperty.PropertyDescription.InputStepSize}" SmallChange="{Binding LayerProperty.PropertyDescription.InputStepSize}"
ToolTip.Tip="X-coordinate (horizontal)" ToolTip.Tip="X-coordinate (horizontal)"
SimpleNumberFormat="F3"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center">,</TextBlock> <TextBlock VerticalAlignment="Center">,</TextBlock>
<controls:NumberBox Classes="condensed" <controls:NumberBox Classes="condensed"
Value="{Binding Y}" Value="{Binding Y}"
SmallChange="{Binding LayerProperty.PropertyDescription.InputStepSize}" SmallChange="{Binding LayerProperty.PropertyDescription.InputStepSize}"
SimpleNumberFormat="F3"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
<TextBlock Width="25" <TextBlock Width="25"
Text="{Binding LayerProperty.PropertyDescription.InputAffix}" Text="{Binding LayerProperty.PropertyDescription.InputAffix}"

View File

@ -13,11 +13,13 @@
Value="{Binding Height}" Value="{Binding Height}"
SmallChange="{Binding LayerProperty.PropertyDescription.InputStepSize}" SmallChange="{Binding LayerProperty.PropertyDescription.InputStepSize}"
ToolTip.Tip="Height" ToolTip.Tip="Height"
SimpleNumberFormat="F3"
VerticalAlignment="Center"/> VerticalAlignment="Center"/>
<TextBlock VerticalAlignment="Center">,</TextBlock> <TextBlock VerticalAlignment="Center">,</TextBlock>
<controls:NumberBox Classes="condensed" <controls:NumberBox Classes="condensed"
Value="{Binding Width}" Value="{Binding Width}"
SmallChange="{Binding LayerProperty.PropertyDescription.InputStepSize}" SmallChange="{Binding LayerProperty.PropertyDescription.InputStepSize}"
SimpleNumberFormat="F3"
VerticalAlignment="Center"/> VerticalAlignment="Center"/>
<TextBlock Width="25" <TextBlock Width="25"
Text="{Binding LayerProperty.PropertyDescription.InputAffix}" Text="{Binding LayerProperty.PropertyDescription.InputAffix}"

View File

@ -5,10 +5,10 @@ using Artemis.Core.LayerEffects;
using Artemis.UI.Screens.Device; using Artemis.UI.Screens.Device;
using Artemis.UI.Screens.Plugins; using Artemis.UI.Screens.Plugins;
using Artemis.UI.Screens.ProfileEditor; using Artemis.UI.Screens.ProfileEditor;
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties;
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline;
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
using Artemis.UI.Screens.ProfileEditor.ProfileTree; using Artemis.UI.Screens.ProfileEditor.ProfileTree;
using Artemis.UI.Screens.ProfileEditor.Properties;
using Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
using Artemis.UI.Screens.Settings; using Artemis.UI.Screens.Settings;
using Artemis.UI.Screens.Sidebar; using Artemis.UI.Screens.Sidebar;
using Artemis.UI.Screens.SurfaceEditor; using Artemis.UI.Screens.SurfaceEditor;
@ -67,13 +67,15 @@ namespace Artemis.UI.Ninject.Factories
public interface ILayerPropertyVmFactory : IVmFactory public interface ILayerPropertyVmFactory : IVmFactory
{ {
ProfileElementPropertyViewModel ProfileElementPropertyViewModel(ILayerProperty layerProperty); PropertyViewModel PropertyViewModel(ILayerProperty layerProperty);
ProfileElementPropertyGroupViewModel ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup); PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup);
ProfileElementPropertyGroupViewModel ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerBrush layerBrush); PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerBrush layerBrush);
ProfileElementPropertyGroupViewModel ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerEffect layerEffect); PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerEffect layerEffect);
TreeGroupViewModel TreeGroupViewModel(ProfileElementPropertyGroupViewModel profileElementPropertyGroupViewModel); TreeGroupViewModel TreeGroupViewModel(PropertyGroupViewModel propertyGroupViewModel);
// TimelineGroupViewModel TimelineGroupViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel);
TimelineViewModel TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels);
TimelineGroupViewModel TimelineGroupViewModel(PropertyGroupViewModel propertyGroupViewModel);
// TreeViewModel TreeViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel, IObservableCollection<ProfileElementPropertyGroupViewModel> profileElementPropertyGroups); // TreeViewModel TreeViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel, IObservableCollection<ProfileElementPropertyGroupViewModel> profileElementPropertyGroups);
// EffectsViewModel EffectsViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel); // EffectsViewModel EffectsViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel);
@ -83,7 +85,7 @@ namespace Artemis.UI.Ninject.Factories
public interface IPropertyVmFactory public interface IPropertyVmFactory
{ {
ITreePropertyViewModel TreePropertyViewModel(ILayerProperty layerProperty, ProfileElementPropertyViewModel profileElementPropertyViewModel); ITreePropertyViewModel TreePropertyViewModel(ILayerProperty layerProperty, PropertyViewModel propertyViewModel);
ITimelinePropertyViewModel TimelinePropertyViewModel(ILayerProperty layerProperty, ProfileElementPropertyViewModel profileElementPropertyViewModel); ITimelinePropertyViewModel TimelinePropertyViewModel(ILayerProperty layerProperty, PropertyViewModel propertyViewModel);
} }
} }

View File

@ -1,8 +1,8 @@
using System; using System;
using System.Reflection; using System.Reflection;
using Artemis.Core; using Artemis.Core;
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline; using Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
using Ninject.Extensions.Factory; using Ninject.Extensions.Factory;
namespace Artemis.UI.Ninject.InstanceProviders namespace Artemis.UI.Ninject.InstanceProviders

View File

@ -1,169 +0,0 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Core;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Avalonia.Controls.Mixins;
using DynamicData;
using ReactiveUI;
using Disposable = System.Reactive.Disposables.Disposable;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline
{
public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineKeyframeViewModel
{
private bool _isSelected;
private string _timestamp;
private double _x;
private readonly IProfileEditorService _profileEditorService;
public TimelineKeyframeViewModel(LayerPropertyKeyframe<T> layerPropertyKeyframe, IProfileEditorService profileEditorService)
{
_profileEditorService = profileEditorService;
LayerPropertyKeyframe = layerPropertyKeyframe;
EasingViewModels = new ObservableCollection<TimelineEasingViewModel>();
this.WhenActivated(d =>
{
_profileEditorService.PixelsPerSecond.Subscribe(p =>
{
_pixelsPerSecond = p;
_profileEditorService.PixelsPerSecond.Subscribe(_ => Update()).DisposeWith(d);
Disposable.Create(() =>
{
foreach (TimelineEasingViewModel timelineEasingViewModel in EasingViewModels)
timelineEasingViewModel.EasingModeSelected -= TimelineEasingViewModelOnEasingModeSelected;
}).DisposeWith(d);
}).DisposeWith(d);
});
}
public LayerPropertyKeyframe<T> LayerPropertyKeyframe { get; }
public ObservableCollection<TimelineEasingViewModel> EasingViewModels { get; }
public double X
{
get => _x;
set => this.RaiseAndSetIfChanged(ref _x, value);
}
public string Timestamp
{
get => _timestamp;
set => this.RaiseAndSetIfChanged(ref _timestamp, value);
}
public bool IsSelected
{
get => _isSelected;
set => this.RaiseAndSetIfChanged(ref _isSelected, value);
}
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}";
}
#region Movement
private TimeSpan? _offset;
private double _pixelsPerSecond;
public void ReleaseMovement()
{
_offset = null;
}
public void SaveOffsetToKeyframe(ITimelineKeyframeViewModel source)
{
if (source == this)
{
_offset = null;
return;
}
if (_offset != null)
return;
_offset = LayerPropertyKeyframe.Position - source.Position;
}
public void ApplyOffsetToKeyframe(ITimelineKeyframeViewModel source)
{
if (source == this || _offset == null)
return;
UpdatePosition(source.Position + _offset.Value);
}
public void UpdatePosition(TimeSpan position)
{
throw new NotImplementedException();
// if (position < TimeSpan.Zero)
// LayerPropertyKeyframe.Position = TimeSpan.Zero;
// else if (position > _profileEditorService.SelectedProfileElement.Timeline.Length)
// LayerPropertyKeyframe.Position = _profileEditorService.SelectedProfileElement.Timeline.Length;
// else
// LayerPropertyKeyframe.Position = position;
Update();
}
#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, e == LayerPropertyKeyframe.EasingFunction)));
foreach (TimelineEasingViewModel timelineEasingViewModel in EasingViewModels)
timelineEasingViewModel.EasingModeSelected += TimelineEasingViewModelOnEasingModeSelected;
}
public void ClearEasingViewModels()
{
EasingViewModels.Clear();
}
private void TimelineEasingViewModelOnEasingModeSelected(object? sender, EventArgs e)
{
if (sender is TimelineEasingViewModel timelineEasingViewModel)
SelectEasingMode(timelineEasingViewModel);
}
public void SelectEasingMode(TimelineEasingViewModel easingViewModel)
{
throw new NotImplementedException();
LayerPropertyKeyframe.EasingFunction = easingViewModel.EasingFunction;
// Set every selection to false except on the VM that made the change
foreach (TimelineEasingViewModel propertyTrackEasingViewModel in EasingViewModels.Where(vm => vm != easingViewModel))
propertyTrackEasingViewModel.IsEasingModeSelected = false;
}
#endregion
#region Context menu actions
public void Delete(bool save = true)
{
throw new NotImplementedException();
}
#endregion
}
}

View File

@ -1,81 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Core;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline
{
public class TimelinePropertyViewModel<T> : ActivatableViewModelBase, ITimelinePropertyViewModel
{
private readonly IProfileEditorService _profileEditorService;
public LayerProperty<T> LayerProperty { get; }
public ProfileElementPropertyViewModel ProfileElementPropertyViewModel { get; }
public ObservableCollection<TimelineKeyframeViewModel<T>> KeyframeViewModels { get; }
public TimelinePropertyViewModel(LayerProperty<T> layerProperty, ProfileElementPropertyViewModel profileElementPropertyViewModel, IProfileEditorService profileEditorService)
{
_profileEditorService = profileEditorService;
LayerProperty = layerProperty;
ProfileElementPropertyViewModel = profileElementPropertyViewModel;
KeyframeViewModels = new ObservableCollection<TimelineKeyframeViewModel<T>>();
}
#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
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();
}
}
}

View File

@ -1,15 +0,0 @@
<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.ProfileElementProperties"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline.TimelineView">
<ItemsControl Items="{Binding PropertyGroupViewModels}" Padding="0 0 8 0">
<ItemsControl.ItemTemplate>
<TreeDataTemplate DataType="{x:Type local:ProfileElementPropertyGroupViewModel}" ItemsSource="{Binding Children}">
<ContentControl Content="{Binding TimelineGroupViewModel}" />
</TreeDataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</UserControl>

View File

@ -1,18 +0,0 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline
{
public partial class TimelineView : ReactiveUserControl<TimelineViewModel>
{
public TimelineView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@ -1,39 +0,0 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Avalonia.Controls.Mixins;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline;
public class TimelineViewModel : ActivatableViewModelBase
{
private readonly IProfileEditorService _profileEditorService;
private ObservableAsPropertyHelper<double>? _caretPosition;
public TimelineViewModel(IProfileEditorService profileEditorService)
{
_profileEditorService = profileEditorService;
this.WhenActivated(d =>
{
_caretPosition = _profileEditorService.Time
.CombineLatest(_profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
.ToProperty(this, vm => vm.CaretPosition)
.DisposeWith(d);
});
}
public double CaretPosition => _caretPosition?.Value ?? 0.0;
public void ChangeTime(TimeSpan newTime)
{
_profileEditorService.ChangeTime(newTime);
}
public TimeSpan SnapToTimeline(TimeSpan time, TimeSpan tolerance, bool snapToSegments, bool snapToCurrentTime, List<TimeSpan>? snapTimes = null)
{
return _profileEditorService.SnapToTimeline(time, tolerance, snapToSegments, snapToCurrentTime, snapTimes);
}
}

View File

@ -1,15 +0,0 @@
using Artemis.Core.LayerBrushes;
using Artemis.UI.Shared;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree.Dialogs
{
public class LayerBrushPresetViewModel : ContentDialogViewModelBase
{
public BaseLayerBrush LayerBrush { get; }
public LayerBrushPresetViewModel(BaseLayerBrush layerBrush)
{
LayerBrush = layerBrush;
}
}
}

View File

@ -1,25 +0,0 @@
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree
{
public partial 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.ProfileElementPropertyGroupViewModel.IsExpanded = !ViewModel.ProfileElementPropertyGroupViewModel.IsExpanded;
}
}
}

View File

@ -1,33 +0,0 @@
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.ProfileElementProperties.Tree
{
public partial 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);
}
}
}

View File

@ -1,35 +0,0 @@
using Artemis.Core;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.PropertyInput;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
internal class TreePropertyViewModel<T> : ActivatableViewModelBase, ITreePropertyViewModel
{
public TreePropertyViewModel(LayerProperty<T> layerProperty, ProfileElementPropertyViewModel profileElementPropertyViewModel, IPropertyInputService propertyInputService)
{
LayerProperty = layerProperty;
ProfileElementPropertyViewModel = profileElementPropertyViewModel;
PropertyInputViewModel = propertyInputService.CreatePropertyInputViewModel(LayerProperty);
}
public LayerProperty<T> LayerProperty { get; }
public ProfileElementPropertyViewModel ProfileElementPropertyViewModel { get; }
public PropertyInputViewModel<T>? PropertyInputViewModel { get; }
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;
}
}

View File

@ -1,16 +0,0 @@
<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.ProfileElementProperties.Windows.BrushConfigurationWindowView"
Title="Artemis | Brush configuration"
Width="{Binding Configuration.DialogWidth}"
Height="{Binding Configuration.DialogHeight}">
<Panel>
<ContentControl Content="{Binding ConfigurationViewModel}"></ContentControl>
</Panel>
</controls:CoreWindow>

View File

@ -1,16 +0,0 @@
<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.ProfileElementProperties.Windows.EffectConfigurationWindowView"
Title="Artemis | Effect configuration"
Width="{Binding Configuration.DialogWidth}"
Height="{Binding Configuration.DialogHeight}">
<Panel>
<ContentControl Content="{Binding ConfigurationViewModel}"></ContentControl>
</Panel>
</controls:CoreWindow>

View File

@ -2,14 +2,13 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileElementProperties"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:controls="clr-namespace:Artemis.UI.Controls" xmlns:controls="clr-namespace:Artemis.UI.Controls"
xmlns:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.ProfileElementPropertiesView"> x:Class="Artemis.UI.Screens.ProfileEditor.Properties.PropertiesView">
<Grid ColumnDefinitions="*,Auto,*" Name="ContainerGrid"> <Grid ColumnDefinitions="*,Auto,*" Name="ContainerGrid">
<Grid RowDefinitions="48,*"> <Grid RowDefinitions="48,*">
<ContentControl Grid.Row="0" Content="{Binding PlaybackViewModel}"></ContentControl> <ContentControl Grid.Row="0" Content="{Binding PlaybackViewModel}" />
<ScrollViewer Grid.Row="1" <ScrollViewer Grid.Row="1"
Grid.Column="0" Grid.Column="0"
@ -18,7 +17,7 @@
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}"> Background="{DynamicResource CardStrokeColorDefaultSolidBrush}">
<ItemsControl Items="{Binding PropertyGroupViewModels}" Padding="0 0 8 0"> <ItemsControl Items="{Binding PropertyGroupViewModels}" Padding="0 0 8 0">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<TreeDataTemplate DataType="{x:Type local:ProfileElementPropertyGroupViewModel}" ItemsSource="{Binding Children}"> <TreeDataTemplate DataType="{x:Type local:PropertyGroupViewModel}" ItemsSource="{Binding Children}">
<ContentControl Content="{Binding TreeGroupViewModel}" /> <ContentControl Content="{Binding TreeGroupViewModel}" />
</TreeDataTemplate> </TreeDataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
@ -61,11 +60,11 @@
PointerMoved="TimelineCaret_OnPointerMoved" PointerMoved="TimelineCaret_OnPointerMoved"
Points="-8,0 -8,8 0,20, 8,8 8,0" Points="-8,0 -8,8 0,20, 8,8 8,0"
Fill="{DynamicResource SystemAccentColorLight1}"> Fill="{DynamicResource SystemAccentColorLight1}">
<Polygon.Transitions> <!-- <Polygon.Transitions> -->
<Transitions> <!-- <Transitions> -->
<DoubleTransition Property="Canvas.Left" Duration="0.05"></DoubleTransition> <!-- <DoubleTransition Property="Canvas.Left" Duration="0.05"></DoubleTransition> -->
</Transitions> <!-- </Transitions> -->
</Polygon.Transitions> <!-- </Polygon.Transitions> -->
</Polygon> </Polygon>
<Line Name="TimelineLine" <Line Name="TimelineLine"
Canvas.Left="{Binding TimelineViewModel.CaretPosition}" Canvas.Left="{Binding TimelineViewModel.CaretPosition}"
@ -78,25 +77,24 @@
StrokeThickness="2" StrokeThickness="2"
Stroke="{DynamicResource SystemAccentColorLight1}" Stroke="{DynamicResource SystemAccentColorLight1}"
RenderTransformOrigin="0,0"> RenderTransformOrigin="0,0">
<Line.Transitions> <!-- <Line.Transitions> -->
<Transitions> <!-- <Transitions> -->
<DoubleTransition Property="Canvas.Left" Duration="0.05"></DoubleTransition> <!-- <DoubleTransition Property="Canvas.Left" Duration="0.05"></DoubleTransition> -->
</Transitions> <!-- </Transitions> -->
</Line.Transitions> <!-- </Line.Transitions> -->
<Line.RenderTransform> <Line.RenderTransform>
<ScaleTransform ScaleX="1" ScaleY="{Binding #ContainerGrid.Bounds.Height}"></ScaleTransform> <ScaleTransform ScaleX="1" ScaleY="{Binding #ContainerGrid.Bounds.Height}" />
</Line.RenderTransform> </Line.RenderTransform>
</Line> </Line>
</Canvas> </Canvas>
<!-- Horizontal scrolling --> <!-- Horizontal scrolling -->
<ScrollViewer Grid.Row="1"> <ScrollViewer Grid.Row="1"
<ScrollViewer Name="TimelineScrollViewer" Name="TimelineScrollViewer"
Offset="{Binding #TreeScrollViewer.Offset, Mode=OneWay}" Offset="{Binding #TreeScrollViewer.Offset, Mode=OneWay}"
VerticalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}"> Background="{DynamicResource CardStrokeColorDefaultSolidBrush}">
<ContentControl Content="{Binding TimelineViewModel}"></ContentControl> <ContentControl Content="{Binding TimelineViewModel}" />
</ScrollViewer>
</ScrollViewer> </ScrollViewer>
<!-- TODO: Databindings here --> <!-- TODO: Databindings here -->

View File

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Animation;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Shapes; using Avalonia.Controls.Shapes;
using Avalonia.Input; using Avalonia.Input;
@ -9,14 +8,14 @@ using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Avalonia.VisualTree; using Avalonia.VisualTree;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties; namespace Artemis.UI.Screens.ProfileEditor.Properties;
public class ProfileElementPropertiesView : ReactiveUserControl<ProfileElementPropertiesViewModel> public class PropertiesView : ReactiveUserControl<PropertiesViewModel>
{ {
private readonly Polygon _timelineCaret; private readonly Polygon _timelineCaret;
private readonly Line _timelineLine; private readonly Line _timelineLine;
public ProfileElementPropertiesView() public PropertiesView()
{ {
InitializeComponent(); InitializeComponent();
_timelineCaret = this.Get<Polygon>("TimelineCaret"); _timelineCaret = this.Get<Polygon>("TimelineCaret");
@ -37,8 +36,8 @@ public class ProfileElementPropertiesView : ReactiveUserControl<ProfileElementPr
// } // }
// else // else
// { // {
((DoubleTransition) _timelineCaret.Transitions![0]).Duration = TimeSpan.Zero; // ((DoubleTransition) _timelineCaret.Transitions![0]).Duration = TimeSpan.Zero;
((DoubleTransition) _timelineLine.Transitions![0]).Duration = TimeSpan.Zero; // ((DoubleTransition) _timelineLine.Transitions![0]).Duration = TimeSpan.Zero;
// } // }
} }

View File

@ -10,31 +10,30 @@ using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects; using Artemis.Core.LayerEffects;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.Playback; using Artemis.UI.Screens.ProfileEditor.Playback;
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline; using Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties; namespace Artemis.UI.Screens.ProfileEditor.Properties;
public class ProfileElementPropertiesViewModel : ActivatableViewModelBase public class PropertiesViewModel : ActivatableViewModelBase
{ {
private readonly Dictionary<LayerPropertyGroup, ProfileElementPropertyGroupViewModel> _cachedViewModels; private readonly Dictionary<LayerPropertyGroup, PropertyGroupViewModel> _cachedViewModels;
private readonly IProfileEditorService _profileEditorService;
private readonly ILayerPropertyVmFactory _layerPropertyVmFactory; private readonly ILayerPropertyVmFactory _layerPropertyVmFactory;
private ObservableAsPropertyHelper<RenderProfileElement?>? _profileElement; private readonly IProfileEditorService _profileEditorService;
private ObservableAsPropertyHelper<int>? _pixelsPerSecond; private ObservableAsPropertyHelper<int>? _pixelsPerSecond;
private ObservableCollection<ProfileElementPropertyGroupViewModel> _propertyGroupViewModels; private ObservableAsPropertyHelper<RenderProfileElement?>? _profileElement;
/// <inheritdoc /> /// <inheritdoc />
public ProfileElementPropertiesViewModel(IProfileEditorService profileEditorService, ILayerPropertyVmFactory layerPropertyVmFactory, PlaybackViewModel playbackViewModel, TimelineViewModel timelineViewModel) public PropertiesViewModel(IProfileEditorService profileEditorService, ILayerPropertyVmFactory layerPropertyVmFactory, PlaybackViewModel playbackViewModel)
{ {
_profileEditorService = profileEditorService; _profileEditorService = profileEditorService;
_layerPropertyVmFactory = layerPropertyVmFactory; _layerPropertyVmFactory = layerPropertyVmFactory;
_propertyGroupViewModels = new ObservableCollection<ProfileElementPropertyGroupViewModel>(); PropertyGroupViewModels = new ObservableCollection<PropertyGroupViewModel>();
_cachedViewModels = new Dictionary<LayerPropertyGroup, ProfileElementPropertyGroupViewModel>(); _cachedViewModels = new Dictionary<LayerPropertyGroup, PropertyGroupViewModel>();
PlaybackViewModel = playbackViewModel; PlaybackViewModel = playbackViewModel;
TimelineViewModel = timelineViewModel; TimelineViewModel = layerPropertyVmFactory.TimelineViewModel(PropertyGroupViewModels);
// Subscribe to events of the latest selected profile element - borrowed from https://stackoverflow.com/a/63950940 // Subscribe to events of the latest selected profile element - borrowed from https://stackoverflow.com/a/63950940
this.WhenAnyValue(vm => vm.ProfileElement) this.WhenAnyValue(vm => vm.ProfileElement)
@ -66,12 +65,8 @@ public class ProfileElementPropertiesViewModel : ActivatableViewModelBase
public int PixelsPerSecond => _pixelsPerSecond?.Value ?? 0; public int PixelsPerSecond => _pixelsPerSecond?.Value ?? 0;
public IObservable<bool> Playing => _profileEditorService.Playing; public IObservable<bool> Playing => _profileEditorService.Playing;
public ObservableCollection<ProfileElementPropertyGroupViewModel> PropertyGroupViewModels public ObservableCollection<PropertyGroupViewModel> PropertyGroupViewModels { get; }
{
get => _propertyGroupViewModels;
set => this.RaiseAndSetIfChanged(ref _propertyGroupViewModels, value);
}
private void UpdateGroups() private void UpdateGroups()
{ {
if (ProfileElement == null) if (ProfileElement == null)
@ -80,7 +75,7 @@ public class ProfileElementPropertiesViewModel : ActivatableViewModelBase
return; return;
} }
ObservableCollection<ProfileElementPropertyGroupViewModel> viewModels = new(); ObservableCollection<PropertyGroupViewModel> viewModels = new();
if (Layer != null) if (Layer != null)
{ {
// Add base VMs // Add base VMs
@ -100,29 +95,29 @@ public class ProfileElementPropertiesViewModel : ActivatableViewModelBase
// Map the most recent collection of VMs to the current list of VMs, making as little changes to the collection as possible // 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++) for (int index = 0; index < viewModels.Count; index++)
{ {
ProfileElementPropertyGroupViewModel profileElementPropertyGroupViewModel = viewModels[index]; PropertyGroupViewModel propertyGroupViewModel = viewModels[index];
if (index > PropertyGroupViewModels.Count - 1) if (index > PropertyGroupViewModels.Count - 1)
PropertyGroupViewModels.Add(profileElementPropertyGroupViewModel); PropertyGroupViewModels.Add(propertyGroupViewModel);
else if (!ReferenceEquals(PropertyGroupViewModels[index], profileElementPropertyGroupViewModel)) else if (!ReferenceEquals(PropertyGroupViewModels[index], propertyGroupViewModel))
PropertyGroupViewModels[index] = profileElementPropertyGroupViewModel; PropertyGroupViewModels[index] = propertyGroupViewModel;
} }
while (PropertyGroupViewModels.Count > viewModels.Count) while (PropertyGroupViewModels.Count > viewModels.Count)
PropertyGroupViewModels.RemoveAt(PropertyGroupViewModels.Count - 1); PropertyGroupViewModels.RemoveAt(PropertyGroupViewModels.Count - 1);
} }
private ProfileElementPropertyGroupViewModel GetOrCreateViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerBrush? layerBrush, BaseLayerEffect? layerEffect) private PropertyGroupViewModel GetOrCreateViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerBrush? layerBrush, BaseLayerEffect? layerEffect)
{ {
if (_cachedViewModels.TryGetValue(layerPropertyGroup, out ProfileElementPropertyGroupViewModel? cachedVm)) if (_cachedViewModels.TryGetValue(layerPropertyGroup, out PropertyGroupViewModel? cachedVm))
return cachedVm; return cachedVm;
ProfileElementPropertyGroupViewModel createdVm; PropertyGroupViewModel createdVm;
if (layerBrush != null) if (layerBrush != null)
createdVm = _layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(layerPropertyGroup, layerBrush); createdVm = _layerPropertyVmFactory.PropertyGroupViewModel(layerPropertyGroup, layerBrush);
else if (layerEffect != null) else if (layerEffect != null)
createdVm = _layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(layerPropertyGroup, layerEffect); createdVm = _layerPropertyVmFactory.PropertyGroupViewModel(layerPropertyGroup, layerEffect);
else else
createdVm = _layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(layerPropertyGroup); createdVm = _layerPropertyVmFactory.PropertyGroupViewModel(layerPropertyGroup);
_cachedViewModels[layerPropertyGroup] = createdVm; _cachedViewModels[layerPropertyGroup] = createdVm;
return createdVm; return createdVm;

View File

@ -7,15 +7,15 @@ using Artemis.Core;
using Artemis.Core.LayerBrushes; using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects; using Artemis.Core.LayerEffects;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline; using Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.PropertyInput; using Artemis.UI.Shared.Services.PropertyInput;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties; namespace Artemis.UI.Screens.ProfileEditor.Properties;
public class ProfileElementPropertyGroupViewModel : ViewModelBase public class PropertyGroupViewModel : ViewModelBase
{ {
private readonly ILayerPropertyVmFactory _layerPropertyVmFactory; private readonly ILayerPropertyVmFactory _layerPropertyVmFactory;
private readonly IPropertyInputService _propertyInputService; private readonly IPropertyInputService _propertyInputService;
@ -23,13 +23,14 @@ public class ProfileElementPropertyGroupViewModel : ViewModelBase
private bool _isExpanded; private bool _isExpanded;
private bool _isVisible; private bool _isVisible;
public ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService) public PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService)
{ {
_layerPropertyVmFactory = layerPropertyVmFactory; _layerPropertyVmFactory = layerPropertyVmFactory;
_propertyInputService = propertyInputService; _propertyInputService = propertyInputService;
Children = new ObservableCollection<ViewModelBase>(); Children = new ObservableCollection<ViewModelBase>();
LayerPropertyGroup = layerPropertyGroup; LayerPropertyGroup = layerPropertyGroup;
TreeGroupViewModel = layerPropertyVmFactory.TreeGroupViewModel(this); TreeGroupViewModel = layerPropertyVmFactory.TreeGroupViewModel(this);
TimelineGroupViewModel = layerPropertyVmFactory.TimelineGroupViewModel(this);
// TODO: Centralize visibility updating or do it here and dispose // TODO: Centralize visibility updating or do it here and dispose
_isVisible = !LayerPropertyGroup.IsHidden; _isVisible = !LayerPropertyGroup.IsHidden;
@ -37,14 +38,14 @@ public class ProfileElementPropertyGroupViewModel : ViewModelBase
PopulateChildren(); PopulateChildren();
} }
public ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService, public PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService,
BaseLayerBrush layerBrush) BaseLayerBrush layerBrush)
: this(layerPropertyGroup, layerPropertyVmFactory, propertyInputService) : this(layerPropertyGroup, layerPropertyVmFactory, propertyInputService)
{ {
LayerBrush = layerBrush; LayerBrush = layerBrush;
} }
public ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService, public PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService,
BaseLayerEffect layerEffect) BaseLayerEffect layerEffect)
: this(layerPropertyGroup, layerPropertyVmFactory, propertyInputService) : this(layerPropertyGroup, layerPropertyVmFactory, propertyInputService)
{ {
@ -57,6 +58,7 @@ public class ProfileElementPropertyGroupViewModel : ViewModelBase
public BaseLayerEffect? LayerEffect { get; } public BaseLayerEffect? LayerEffect { get; }
public TreeGroupViewModel TreeGroupViewModel { get; } public TreeGroupViewModel TreeGroupViewModel { get; }
public TimelineGroupViewModel TimelineGroupViewModel { get; }
public bool IsVisible public bool IsVisible
{ {
@ -83,9 +85,9 @@ public class ProfileElementPropertyGroupViewModel : ViewModelBase
return result; return result;
foreach (ViewModelBase child in Children) foreach (ViewModelBase child in Children)
if (child is ProfileElementPropertyViewModel profileElementPropertyViewModel) if (child is PropertyViewModel profileElementPropertyViewModel)
result.AddRange(profileElementPropertyViewModel.TimelinePropertyViewModel.GetAllKeyframeViewModels()); result.AddRange(profileElementPropertyViewModel.TimelinePropertyViewModel.GetAllKeyframeViewModels());
else if (child is ProfileElementPropertyGroupViewModel profileElementPropertyGroupViewModel) else if (child is PropertyGroupViewModel profileElementPropertyGroupViewModel)
result.AddRange(profileElementPropertyGroupViewModel.GetAllKeyframeViewModels(expandedOnly)); result.AddRange(profileElementPropertyGroupViewModel.GetAllKeyframeViewModels(expandedOnly));
return result; return result;
@ -105,16 +107,16 @@ public class ProfileElementPropertyGroupViewModel : ViewModelBase
ILayerProperty? value = (ILayerProperty?) propertyInfo.GetValue(LayerPropertyGroup); ILayerProperty? value = (ILayerProperty?) propertyInfo.GetValue(LayerPropertyGroup);
// Ensure a supported input VM was found, otherwise don't add it // Ensure a supported input VM was found, otherwise don't add it
if (value != null && _propertyInputService.CanCreatePropertyInputViewModel(value)) if (value != null && _propertyInputService.CanCreatePropertyInputViewModel(value))
Children.Add(_layerPropertyVmFactory.ProfileElementPropertyViewModel(value)); Children.Add(_layerPropertyVmFactory.PropertyViewModel(value));
} }
else if (typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType)) else if (typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType))
{ {
LayerPropertyGroup? value = (LayerPropertyGroup?) propertyInfo.GetValue(LayerPropertyGroup); LayerPropertyGroup? value = (LayerPropertyGroup?) propertyInfo.GetValue(LayerPropertyGroup);
if (value != null) if (value != null)
Children.Add(_layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(value)); Children.Add(_layerPropertyVmFactory.PropertyGroupViewModel(value));
} }
} }
HasChildren = Children.Any(i => i is ProfileElementPropertyViewModel {IsVisible: true} || i is ProfileElementPropertyGroupViewModel {IsVisible: true}); HasChildren = Children.Any(i => i is PropertyViewModel {IsVisible: true} || i is PropertyGroupViewModel {IsVisible: true});
} }
} }

View File

@ -1,19 +1,19 @@
using Artemis.Core; using Artemis.Core;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline; using Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties; namespace Artemis.UI.Screens.ProfileEditor.Properties;
public class ProfileElementPropertyViewModel : ViewModelBase public class PropertyViewModel : ViewModelBase
{ {
private bool _isExpanded; private bool _isExpanded;
private bool _isHighlighted; private bool _isHighlighted;
private bool _isVisible; private bool _isVisible;
public ProfileElementPropertyViewModel(ILayerProperty layerProperty, IPropertyVmFactory propertyVmFactory) public PropertyViewModel(ILayerProperty layerProperty, IPropertyVmFactory propertyVmFactory)
{ {
LayerProperty = layerProperty; LayerProperty = layerProperty;
TreePropertyViewModel = propertyVmFactory.TreePropertyViewModel(LayerProperty, this); TreePropertyViewModel = propertyVmFactory.TreePropertyViewModel(LayerProperty, this);

View File

@ -1,7 +1,7 @@
using System; using System;
using Artemis.Core; using Artemis.Core;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline; namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public interface ITimelineKeyframeViewModel public interface ITimelineKeyframeViewModel
{ {

View File

@ -2,7 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline; namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public interface ITimelinePropertyViewModel : IReactiveObject public interface ITimelinePropertyViewModel : IReactiveObject
{ {

View File

@ -5,7 +5,7 @@ using Artemis.UI.Shared;
using Avalonia; using Avalonia;
using Humanizer; using Humanizer;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Timeline; namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public class TimelineEasingViewModel : ViewModelBase public class TimelineEasingViewModel : ViewModelBase
{ {

View File

@ -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:properties="clr-namespace:Artemis.UI.Screens.ProfileEditor.Properties"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.TimelineGroupView">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="28" />
<RowDefinition Height="1" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ItemsControl Grid.Row="0"
Height="{DynamicResource RailsHeight}"
IsVisible="{Binding !PropertyGroupViewModel.IsExpanded}"
Items="{Binding 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="{Binding Children}"
IsVisible="{Binding PropertyGroupViewModel.IsExpanded}"
HorizontalAlignment="Stretch">
<ItemsControl.DataTemplates>
<DataTemplate DataType="properties:PropertyGroupViewModel">
<ContentControl Content="{Binding TimelineGroupViewModel}" IsVisible="{Binding IsVisible}" />
</DataTemplate>
<DataTemplate DataType="properties:PropertyViewModel">
<ContentControl Content="{Binding TimelinePropertyViewModel}" IsVisible="{Binding IsVisible}" />
</DataTemplate>
</ItemsControl.DataTemplates>
</ItemsControl>
</Grid>
</UserControl>

View File

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

View File

@ -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>? _keyframePosition;
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>? KeyframePosition
{
get => _keyframePosition;
set => this.RaiseAndSetIfChanged(ref _keyframePosition, value);
}
private void UpdateKeyframePositions()
{
KeyframePosition = new ObservableCollection<double>(PropertyGroupViewModel
.GetAllKeyframeViewModels(false)
.Select(p => p.Position.TotalSeconds * _pixelsPerSecond));
}
}

View File

@ -0,0 +1,76 @@
<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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.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}">
<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>
<MenuItem Header="Easing" Items="{Binding EasingViewModels}">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Creation" />
</MenuItem.Icon>
<MenuItem.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Polyline Stroke="{DynamicResource MaterialDesignBody}"
StrokeThickness="1"
Points="{Binding EasingPoints}"
Stretch="Uniform"
Width="20"
Height="20"
Margin="0 0 10 0" />
<TextBlock Text="{Binding Description}" />
</StackPanel>
</DataTemplate>
</MenuItem.ItemTemplate>
</MenuItem>
<Separator />
<MenuItem Header="Duplicate" Command="{Binding DuplicateKeyframes}" CommandParameter="{Binding}" InputGesture="Ctrl+D">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentDuplicate" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Copy" Command="{Binding CopyKeyframes}" CommandParameter="{Binding}" InputGesture="Ctrl+C">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentCopy" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Paste" Command="{Binding PasteKeyframes}" CommandParameter="{Binding}" InputGesture="Ctrl+V">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="ContentPaste" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Delete" Command="{Binding DeleteKeyframes}" InputGesture="Delete">
<MenuItem.Icon>
<avalonia:MaterialIcon Kind="Delete" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Ellipse.ContextFlyout>
</Ellipse>
</UserControl>

View File

@ -0,0 +1,17 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public class TimelineKeyframeView : ReactiveUserControl<ITimelineKeyframeViewModel>
{
public TimelineKeyframeView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

View File

@ -0,0 +1,167 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Core;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Avalonia.Controls.Mixins;
using DynamicData;
using ReactiveUI;
using Disposable = System.Reactive.Disposables.Disposable;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public class TimelineKeyframeViewModel<T> : ActivatableViewModelBase, ITimelineKeyframeViewModel
{
private bool _isSelected;
private string _timestamp;
private double _x;
public TimelineKeyframeViewModel(LayerPropertyKeyframe<T> layerPropertyKeyframe, IProfileEditorService profileEditorService)
{
IProfileEditorService profileEditorService1 = profileEditorService;
_timestamp = "0.000";
LayerPropertyKeyframe = layerPropertyKeyframe;
EasingViewModels = new ObservableCollection<TimelineEasingViewModel>();
this.WhenActivated(d =>
{
profileEditorService1.PixelsPerSecond.Subscribe(p =>
{
_pixelsPerSecond = p;
profileEditorService1.PixelsPerSecond.Subscribe(_ => Update()).DisposeWith(d);
Disposable.Create(() =>
{
foreach (TimelineEasingViewModel timelineEasingViewModel in EasingViewModels)
timelineEasingViewModel.EasingModeSelected -= TimelineEasingViewModelOnEasingModeSelected;
}).DisposeWith(d);
}).DisposeWith(d);
});
}
public LayerPropertyKeyframe<T> LayerPropertyKeyframe { get; }
public ObservableCollection<TimelineEasingViewModel> EasingViewModels { get; }
public double X
{
get => _x;
set => this.RaiseAndSetIfChanged(ref _x, value);
}
public string Timestamp
{
get => _timestamp;
set => this.RaiseAndSetIfChanged(ref _timestamp, value);
}
public void Update()
{
X = _pixelsPerSecond * LayerPropertyKeyframe.Position.TotalSeconds;
Timestamp = $"{Math.Floor(LayerPropertyKeyframe.Position.TotalSeconds):00}.{LayerPropertyKeyframe.Position.Milliseconds:000}";
}
public bool IsSelected
{
get => _isSelected;
set => this.RaiseAndSetIfChanged(ref _isSelected, value);
}
public TimeSpan Position => LayerPropertyKeyframe.Position;
public ILayerPropertyKeyframe Keyframe => LayerPropertyKeyframe;
#region Context menu actions
public void Delete(bool save = true)
{
throw new NotImplementedException();
}
#endregion
#region Movement
private TimeSpan? _offset;
private double _pixelsPerSecond;
public void ReleaseMovement()
{
_offset = null;
}
public void SaveOffsetToKeyframe(ITimelineKeyframeViewModel source)
{
if (source == this)
{
_offset = null;
return;
}
if (_offset != null)
return;
_offset = LayerPropertyKeyframe.Position - source.Position;
}
public void ApplyOffsetToKeyframe(ITimelineKeyframeViewModel source)
{
if (source == this || _offset == null)
return;
UpdatePosition(source.Position + _offset.Value);
}
public void UpdatePosition(TimeSpan position)
{
throw new NotImplementedException();
// if (position < TimeSpan.Zero)
// LayerPropertyKeyframe.Position = TimeSpan.Zero;
// else if (position > _profileEditorService.SelectedProfileElement.Timeline.Length)
// LayerPropertyKeyframe.Position = _profileEditorService.SelectedProfileElement.Timeline.Length;
// else
// LayerPropertyKeyframe.Position = position;
Update();
}
#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, e == LayerPropertyKeyframe.EasingFunction)));
foreach (TimelineEasingViewModel timelineEasingViewModel in EasingViewModels)
timelineEasingViewModel.EasingModeSelected += TimelineEasingViewModelOnEasingModeSelected;
}
public void ClearEasingViewModels()
{
EasingViewModels.Clear();
}
private void TimelineEasingViewModelOnEasingModeSelected(object? sender, EventArgs e)
{
if (sender is TimelineEasingViewModel timelineEasingViewModel)
SelectEasingMode(timelineEasingViewModel);
}
public void SelectEasingMode(TimelineEasingViewModel easingViewModel)
{
throw new NotImplementedException();
LayerPropertyKeyframe.EasingFunction = easingViewModel.EasingFunction;
// Set every selection to false except on the VM that made the change
foreach (TimelineEasingViewModel propertyTrackEasingViewModel in EasingViewModels.Where(vm => vm != easingViewModel))
propertyTrackEasingViewModel.IsEasingModeSelected = false;
}
#endregion
}

View File

@ -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="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}" />
</Style>
</ItemsControl.Styles>
</ItemsControl>
</Border>
</UserControl>

View File

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

View File

@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
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 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.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
}

View File

@ -0,0 +1,25 @@
<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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Timeline.TimelineView">
<UserControl.Resources>
<x:Double x:Key="RailsHeight">28</x:Double>
<x:Double x:Key="RailsBorderHeight">29</x:Double>
</UserControl.Resources>
<Grid Background="Transparent" PointerMoved="InputElement_OnPointerMoved" PointerReleased="InputElement_OnPointerReleased">
<ItemsControl Items="{Binding PropertyGroupViewModels}" Padding="0 0 8 0">
<ItemsControl.ItemTemplate>
<TreeDataTemplate DataType="{x:Type local:PropertyGroupViewModel}" ItemsSource="{Binding Children}">
<ContentControl Content="{Binding TimelineGroupViewModel}" />
</TreeDataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<controls:SelectionRectangle InputElement="{Binding $parent}" SelectionFinished="SelectionRectangle_OnSelectionFinished"></controls:SelectionRectangle>
</Grid>
</UserControl>

View File

@ -0,0 +1,68 @@
using System.Collections.Generic;
using System.Linq;
using Artemis.UI.Shared.Events;
using Artemis.UI.Shared.Extensions;
using Avalonia;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline;
public class TimelineView : ReactiveUserControl<TimelineViewModel>
{
private bool _draggedCursor;
public TimelineView()
{
InitializeComponent();
}
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!).ToList(), e.KeyModifiers.HasFlag(KeyModifiers.Shift));
}
private void InputElement_OnPointerMoved(object? sender, PointerEventArgs e)
{
if (_draggedCursor)
return;
_draggedCursor = e.GetCurrentPoint(this).Properties.IsLeftButtonPressed;
}
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (ViewModel == null)
return;
if (_draggedCursor)
{
_draggedCursor = false;
return;
}
Point position = e.GetPosition(VisualRoot);
TimelineKeyframeView? keyframeView = this.GetVisualChildrenOfType<TimelineKeyframeView>().Where(k =>
{
Rect hitTestRect = k.TransformedBounds != null ? k.TransformedBounds.Value.Bounds.TransformToAABB(k.TransformedBounds.Value.Transform) : Rect.Empty;
return hitTestRect.Contains(position);
}).FirstOrDefault(kv => kv.ViewModel != null);
ViewModel.SelectKeyframe(keyframeView?.ViewModel, e.KeyModifiers.HasFlag(KeyModifiers.Shift), false);
}
}

View File

@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.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 TimelineViewModel : ActivatableViewModelBase
{
private readonly IProfileEditorService _profileEditorService;
private ObservableAsPropertyHelper<double>? _caretPosition;
public TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels, IProfileEditorService profileEditorService)
{
PropertyGroupViewModels = propertyGroupViewModels;
_profileEditorService = profileEditorService;
this.WhenActivated(d =>
{
_caretPosition = _profileEditorService.Time
.CombineLatest(_profileEditorService.PixelsPerSecond, (t, p) => t.TotalSeconds * p)
.ToProperty(this, vm => vm.CaretPosition)
.DisposeWith(d);
});
}
public ObservableCollection<PropertyGroupViewModel> PropertyGroupViewModels { get; }
public double CaretPosition => _caretPosition?.Value ?? 0.0;
public void ChangeTime(TimeSpan newTime)
{
_profileEditorService.ChangeTime(newTime);
}
public TimeSpan SnapToTimeline(TimeSpan time, TimeSpan tolerance, bool snapToSegments, bool snapToCurrentTime, List<TimeSpan>? snapTimes = null)
{
return _profileEditorService.SnapToTimeline(time, tolerance, snapToSegments, snapToCurrentTime, snapTimes);
}
public void SelectKeyframes(List<ITimelineKeyframeViewModel> keyframes, bool expand)
{
List<ITimelineKeyframeViewModel> expandedKeyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).ToList();
List<ITimelineKeyframeViewModel> collapsedKeyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(false)).Except(expandedKeyframes).ToList();
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in collapsedKeyframes)
timelineKeyframeViewModel.IsSelected = false;
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in expandedKeyframes)
{
if (timelineKeyframeViewModel.IsSelected && expand)
continue;
timelineKeyframeViewModel.IsSelected = keyframes.Contains(timelineKeyframeViewModel);
}
}
public void SelectKeyframe(ITimelineKeyframeViewModel? clicked, bool selectBetween, bool toggle)
{
List<ITimelineKeyframeViewModel> expandedKeyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(true)).ToList();
List<ITimelineKeyframeViewModel> collapsedKeyframes = PropertyGroupViewModels.SelectMany(g => g.GetAllKeyframeViewModels(false)).Except(expandedKeyframes).ToList();
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in collapsedKeyframes)
timelineKeyframeViewModel.IsSelected = false;
if (clicked == null)
{
foreach (ITimelineKeyframeViewModel timelineKeyframeViewModel in expandedKeyframes)
timelineKeyframeViewModel.IsSelected = false;
return;
}
if (selectBetween)
{
int selectedIndex = expandedKeyframes.FindIndex(k => k.IsSelected);
// If nothing is selected, select only the clicked
if (selectedIndex == -1)
{
clicked.IsSelected = true;
return;
}
foreach (ITimelineKeyframeViewModel keyframeViewModel in expandedKeyframes)
keyframeViewModel.IsSelected = false;
int clickedIndex = expandedKeyframes.IndexOf(clicked);
if (clickedIndex < selectedIndex)
foreach (ITimelineKeyframeViewModel keyframeViewModel in expandedKeyframes.Skip(clickedIndex).Take(selectedIndex - clickedIndex + 1))
keyframeViewModel.IsSelected = true;
else
foreach (ITimelineKeyframeViewModel keyframeViewModel in expandedKeyframes.Skip(selectedIndex).Take(clickedIndex - selectedIndex + 1))
keyframeViewModel.IsSelected = true;
}
else if (toggle)
{
// Toggle only the clicked keyframe, leave others alone
clicked.IsSelected = !clicked.IsSelected;
}
else
{
// Only select the clicked keyframe
foreach (ITimelineKeyframeViewModel keyframeViewModel in expandedKeyframes)
keyframeViewModel.IsSelected = false;
clicked.IsSelected = true;
}
}
}

View File

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

View File

@ -1,7 +1,7 @@
using Artemis.Core; using Artemis.Core;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; namespace Artemis.UI.Screens.ProfileEditor.Properties.Tree;
public interface ITreePropertyViewModel : IReactiveObject public interface ITreePropertyViewModel : IReactiveObject
{ {

View File

@ -4,30 +4,30 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared" xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared"
xmlns:viewModel="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree"
xmlns:sharedConverters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared" xmlns:sharedConverters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
xmlns:profileElementProperties="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileElementProperties"
xmlns:converters="clr-namespace:Artemis.UI.Converters" 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" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree.TreeGroupView"> x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Tree.TreeGroupView">
<UserControl.Resources> <UserControl.Resources>
<converters:PropertyTreeMarginConverter x:Key="PropertyTreeMarginConverter" Length="20"/> <converters:PropertyTreeMarginConverter x:Key="PropertyTreeMarginConverter" Length="20" />
<sharedConverters:EnumToBooleanConverter x:Key="EnumBoolConverter" /> <sharedConverters:EnumToBooleanConverter x:Key="EnumBoolConverter" />
</UserControl.Resources> </UserControl.Resources>
<UserControl.Styles> <UserControl.Styles>
<Style Selector="avalonia|MaterialIcon.chevron-collapsed"> <Style Selector="avalonia|MaterialIcon.chevron-collapsed">
<Setter Property="RenderTransform" Value="rotate(-90deg)" /> <Setter Property="RenderTransform" Value="rotate(-90deg)" />
</Style> </Style>
</UserControl.Styles> </UserControl.Styles>
<StackPanel> <StackPanel>
<Border Name="Bd" <Border Name="Bd"
BorderBrush="{DynamicResource ButtonBorderBrush}" BorderBrush="{DynamicResource ButtonBorderBrush}"
BorderThickness="0,0,0,1" BorderThickness="0,0,0,1"
Height="29"> Height="29">
<Grid Margin="{Binding Converter={StaticResource PropertyTreeMarginConverter}}" ColumnDefinitions="19,*"> <Grid Margin="{Binding Converter={StaticResource PropertyTreeMarginConverter}}" ColumnDefinitions="19,*">
<avalonia:MaterialIcon Classes.chevron-collapsed="{Binding !ProfileElementPropertyGroupViewModel.IsExpanded}" <avalonia:MaterialIcon Classes.chevron-collapsed="{Binding !PropertyGroupViewModel.IsExpanded}"
IsVisible="{Binding ProfileElementPropertyGroupViewModel.HasChildren}" IsVisible="{Binding PropertyGroupViewModel.HasChildren}"
Kind="ChevronDown" Kind="ChevronDown"
Grid.Column="0" Grid.Column="0"
Margin="5 0" Margin="5 0"
@ -40,15 +40,14 @@
</avalonia:MaterialIcon.Transitions> </avalonia:MaterialIcon.Transitions>
</avalonia:MaterialIcon> </avalonia:MaterialIcon>
<StackPanel Grid.Column="1"> <StackPanel Grid.Column="1">
<!-- Type: None --> <!-- Type: None -->
<TextBlock Text="{Binding LayerPropertyGroup.GroupDescription.Name}" <TextBlock Text="{Binding LayerPropertyGroup.GroupDescription.Name}"
ToolTip.Tip="{Binding LayerPropertyGroup.GroupDescription.Description}" ToolTip.Tip="{Binding LayerPropertyGroup.GroupDescription.Description}"
VerticalAlignment="Center" VerticalAlignment="Center"
HorizontalAlignment="Left" HorizontalAlignment="Left"
Margin="3 5 0 5" Margin="3 5 0 5"
IsVisible="{Binding GroupType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static viewModel:LayerPropertyGroupType.None}}"> IsVisible="{Binding GroupType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static viewModel:LayerPropertyGroupType.None}}" />
</TextBlock>
<!-- Type: General --> <!-- Type: General -->
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
@ -178,18 +177,18 @@
</Border> </Border>
<!-- <!--
Do not bind directly to the ProfileElementPropertyGroupViewModel.Children collection 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 Instead use a reference provided by the VM that is null when collapsed, virtualization for noobs
--> -->
<ItemsControl Items="{Binding Children}" <ItemsControl Items="{Binding Children}"
IsVisible="{Binding ProfileElementPropertyGroupViewModel.IsExpanded}" IsVisible="{Binding PropertyGroupViewModel.IsExpanded}"
HorizontalAlignment="Stretch"> HorizontalAlignment="Stretch">
<ItemsControl.DataTemplates> <ItemsControl.DataTemplates>
<DataTemplate DataType="profileElementProperties:ProfileElementPropertyGroupViewModel"> <DataTemplate DataType="properties:PropertyGroupViewModel">
<ContentControl Content="{Binding TreeGroupViewModel}" IsVisible="{Binding IsVisible}"></ContentControl> <ContentControl Content="{Binding TreeGroupViewModel}" IsVisible="{Binding IsVisible}" />
</DataTemplate> </DataTemplate>
<DataTemplate DataType="profileElementProperties:ProfileElementPropertyViewModel"> <DataTemplate DataType="properties:PropertyViewModel">
<ContentControl Content="{Binding TreePropertyViewModel}" IsVisible="{Binding IsVisible}"></ContentControl> <ContentControl Content="{Binding TreePropertyViewModel}" IsVisible="{Binding IsVisible}" />
</DataTemplate> </DataTemplate>
</ItemsControl.DataTemplates> </ItemsControl.DataTemplates>
</ItemsControl> </ItemsControl>

View File

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

View File

@ -8,7 +8,7 @@ using Artemis.Core;
using Artemis.Core.LayerBrushes; using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects; using Artemis.Core.LayerEffects;
using Artemis.UI.Exceptions; using Artemis.UI.Exceptions;
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Windows; using Artemis.UI.Screens.ProfileEditor.Properties.Windows;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.LayerBrushes; using Artemis.UI.Shared.LayerBrushes;
using Artemis.UI.Shared.LayerEffects; using Artemis.UI.Shared.LayerEffects;
@ -18,7 +18,7 @@ using Ninject;
using Ninject.Parameters; using Ninject.Parameters;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; namespace Artemis.UI.Screens.ProfileEditor.Properties.Tree;
public class TreeGroupViewModel : ActivatableViewModelBase public class TreeGroupViewModel : ActivatableViewModelBase
{ {
@ -27,16 +27,16 @@ public class TreeGroupViewModel : ActivatableViewModelBase
private BrushConfigurationWindowViewModel? _brushConfigurationWindowViewModel; private BrushConfigurationWindowViewModel? _brushConfigurationWindowViewModel;
private EffectConfigurationWindowViewModel? _effectConfigurationWindowViewModel; private EffectConfigurationWindowViewModel? _effectConfigurationWindowViewModel;
public TreeGroupViewModel(ProfileElementPropertyGroupViewModel profileElementPropertyGroupViewModel, IWindowService windowService, IProfileEditorService profileEditorService) public TreeGroupViewModel(PropertyGroupViewModel propertyGroupViewModel, IWindowService windowService, IProfileEditorService profileEditorService)
{ {
_windowService = windowService; _windowService = windowService;
_profileEditorService = profileEditorService; _profileEditorService = profileEditorService;
ProfileElementPropertyGroupViewModel = profileElementPropertyGroupViewModel; PropertyGroupViewModel = propertyGroupViewModel;
DetermineGroupType(); DetermineGroupType();
this.WhenActivated(d => this.WhenActivated(d =>
{ {
ProfileElementPropertyGroupViewModel.WhenAnyValue(vm => vm.IsExpanded).Subscribe(_ => this.RaisePropertyChanged(nameof(Children))).DisposeWith(d); PropertyGroupViewModel.WhenAnyValue(vm => vm.IsExpanded).Subscribe(_ => this.RaisePropertyChanged(nameof(Children))).DisposeWith(d);
Disposable.Create(CloseViewModels).DisposeWith(d); Disposable.Create(CloseViewModels).DisposeWith(d);
}); });
@ -44,12 +44,12 @@ public class TreeGroupViewModel : ActivatableViewModelBase
} }
public ProfileElementPropertyGroupViewModel ProfileElementPropertyGroupViewModel { get; } public PropertyGroupViewModel PropertyGroupViewModel { get; }
public LayerPropertyGroup LayerPropertyGroup => ProfileElementPropertyGroupViewModel.LayerPropertyGroup; public LayerPropertyGroup LayerPropertyGroup => PropertyGroupViewModel.LayerPropertyGroup;
public BaseLayerBrush? LayerBrush => ProfileElementPropertyGroupViewModel.LayerBrush; public BaseLayerBrush? LayerBrush => PropertyGroupViewModel.LayerBrush;
public BaseLayerEffect? LayerEffect => ProfileElementPropertyGroupViewModel.LayerEffect; public BaseLayerEffect? LayerEffect => PropertyGroupViewModel.LayerEffect;
public ObservableCollection<ViewModelBase>? Children => ProfileElementPropertyGroupViewModel.IsExpanded ? ProfileElementPropertyGroupViewModel.Children : null; public ObservableCollection<ViewModelBase>? Children => PropertyGroupViewModel.IsExpanded ? PropertyGroupViewModel.Children : null;
public LayerPropertyGroupType GroupType { get; private set; } public LayerPropertyGroupType GroupType { get; private set; }
@ -148,9 +148,9 @@ public class TreeGroupViewModel : ActivatableViewModelBase
GroupType = LayerPropertyGroupType.General; GroupType = LayerPropertyGroupType.General;
else if (LayerPropertyGroup is LayerTransformProperties) else if (LayerPropertyGroup is LayerTransformProperties)
GroupType = LayerPropertyGroupType.Transform; GroupType = LayerPropertyGroupType.Transform;
else if (LayerPropertyGroup.Parent == null && ProfileElementPropertyGroupViewModel.LayerBrush != null) else if (LayerPropertyGroup.Parent == null && PropertyGroupViewModel.LayerBrush != null)
GroupType = LayerPropertyGroupType.LayerBrushRoot; GroupType = LayerPropertyGroupType.LayerBrushRoot;
else if (LayerPropertyGroup.Parent == null && ProfileElementPropertyGroupViewModel.LayerEffect != null) else if (LayerPropertyGroup.Parent == null && PropertyGroupViewModel.LayerEffect != null)
GroupType = LayerPropertyGroupType.LayerEffectRoot; GroupType = LayerPropertyGroupType.LayerEffectRoot;
else else
GroupType = LayerPropertyGroupType.None; GroupType = LayerPropertyGroupType.None;

View File

@ -6,12 +6,11 @@
xmlns:converters="clr-namespace:Artemis.UI.Converters" xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:sharedConverters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared" xmlns:sharedConverters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree.TreePropertyView"> x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Tree.TreePropertyView">
<UserControl.Resources> <UserControl.Resources>
<converters:PropertyTreeMarginConverter x:Key="PropertyTreeMarginConverter" Length="20" /> <converters:PropertyTreeMarginConverter x:Key="PropertyTreeMarginConverter" Length="20" />
<sharedConverters:EnumToBooleanConverter x:Key="EnumBoolConverter" /> </UserControl.Resources>
</UserControl.Resources> <Border Name="Bd"
<Border Name="Bd"
BorderBrush="{DynamicResource ButtonBorderBrush}" BorderBrush="{DynamicResource ButtonBorderBrush}"
BorderThickness="0,0,0,1" BorderThickness="0,0,0,1"
Height="29"> Height="29">
@ -24,10 +23,10 @@
IsChecked="{Binding KeyframesEnabled}" IsChecked="{Binding KeyframesEnabled}"
IsEnabled="{Binding LayerProperty.KeyframesSupported}" IsEnabled="{Binding LayerProperty.KeyframesSupported}"
VerticalAlignment="Center" Padding="-25"> VerticalAlignment="Center" Padding="-25">
<avalonia:MaterialIcon Kind="Stopwatch" /> <avalonia:MaterialIcon Kind="Stopwatch" />
</ToggleButton> </ToggleButton>
<TextBlock Grid.Column="1" <TextBlock Grid.Column="1"
Margin="5,0,0,0" Margin="5,0,0,0"
Padding="0,0,5,0" Padding="0,0,5,0"
VerticalAlignment="Center" VerticalAlignment="Center"
@ -36,30 +35,30 @@
ToolTip.Tip="{Binding LayerProperty.PropertyDescription.Description}" ToolTip.Tip="{Binding LayerProperty.PropertyDescription.Description}"
HorizontalAlignment="Left" /> HorizontalAlignment="Left" />
<ContentControl Grid.Column="2" <ContentControl Grid.Column="2"
Margin="5 0" Margin="5 0"
Content="{Binding PropertyInputViewModel}" Content="{Binding PropertyInputViewModel}"
ToolTip.Tip="{Binding LayerProperty.PropertyDescription.Description}"/> ToolTip.Tip="{Binding LayerProperty.PropertyDescription.Description}" />
<Button Grid.Column="3" <Button Grid.Column="3"
Command="{Binding ResetToDefault}" Command="{Binding ResetToDefault}"
Classes="icon-button" Classes="icon-button"
ToolTip.Tip="Reset the property to its default value." ToolTip.Tip="Reset the property to its default value."
Width="24" Width="24"
Height="24"> Height="24">
<avalonia:MaterialIcon Kind="BackupRestore" /> <avalonia:MaterialIcon Kind="BackupRestore" />
</Button> </Button>
<ToggleButton Grid.Column="4" Classes="icon-button" <ToggleButton Grid.Column="4" Classes="icon-button"
ToolTip.Tip="Change the property's data binding" ToolTip.Tip="Change the property's data binding"
Width="24" Width="24"
Height="24" Height="24"
VerticalAlignment="Center" VerticalAlignment="Center"
IsEnabled="{Binding LayerProperty.DataBindingsSupported}" IsEnabled="{Binding LayerProperty.DataBindingsSupported}"
IsChecked="{Binding HasDataBinding, Mode=OneWay}"> IsChecked="{Binding HasDataBinding, Mode=OneWay}">
<avalonia:MaterialIcon Kind="VectorLink" /> <avalonia:MaterialIcon Kind="VectorLink" />
</ToggleButton> </ToggleButton>
</Grid> </Grid>
</Border> </Border>
</UserControl> </UserControl>

View File

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

View File

@ -0,0 +1,61 @@
using System;
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;
public TreePropertyViewModel(LayerProperty<T> layerProperty, PropertyViewModel propertyViewModel, IProfileEditorService profileEditorService,
IPropertyInputService propertyInputService)
{
_profileEditorService = profileEditorService;
LayerProperty = layerProperty;
PropertyViewModel = propertyViewModel;
PropertyInputViewModel = propertyInputService.CreatePropertyInputViewModel(LayerProperty);
this.WhenActivated(d => this.WhenAnyValue(vm => vm.LayerProperty.KeyframesEnabled).Subscribe(_ => this.RaisePropertyChanged(nameof(KeyframesEnabled))).DisposeWith(d));
}
public LayerProperty<T> LayerProperty { get; }
public PropertyViewModel PropertyViewModel { get; }
public PropertyInputViewModel<T>? PropertyInputViewModel { 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));
}
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;
}
}

View File

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

View File

@ -2,7 +2,7 @@ using System.ComponentModel;
using Avalonia; using Avalonia;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Windows; namespace Artemis.UI.Screens.ProfileEditor.Properties.Windows;
public class BrushConfigurationWindowView : ReactiveCoreWindow<EffectConfigurationWindowViewModel> public class BrushConfigurationWindowView : ReactiveCoreWindow<EffectConfigurationWindowViewModel>
{ {

View File

@ -1,10 +1,9 @@
using System; using System;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.LayerBrushes; using Artemis.UI.Shared.LayerBrushes;
using Artemis.UI.Shared.LayerEffects;
using Avalonia.Threading; using Avalonia.Threading;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Windows; namespace Artemis.UI.Screens.ProfileEditor.Properties.Windows;
public class BrushConfigurationWindowViewModel : DialogViewModelBase<object?> public class BrushConfigurationWindowViewModel : DialogViewModelBase<object?>
{ {
@ -12,7 +11,7 @@ public class BrushConfigurationWindowViewModel : DialogViewModelBase<object?>
{ {
ConfigurationViewModel = configurationViewModel; ConfigurationViewModel = configurationViewModel;
Configuration = configuration; Configuration = configuration;
ConfigurationViewModel.CloseRequested += ConfigurationViewModelOnCloseRequested; ConfigurationViewModel.CloseRequested += ConfigurationViewModelOnCloseRequested;
} }

View File

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

View File

@ -2,7 +2,7 @@ using System.ComponentModel;
using Avalonia; using Avalonia;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Windows; namespace Artemis.UI.Screens.ProfileEditor.Properties.Windows;
public class EffectConfigurationWindowView : ReactiveCoreWindow<EffectConfigurationWindowViewModel> public class EffectConfigurationWindowView : ReactiveCoreWindow<EffectConfigurationWindowViewModel>
{ {

View File

@ -3,7 +3,7 @@ using Artemis.UI.Shared;
using Artemis.UI.Shared.LayerEffects; using Artemis.UI.Shared.LayerEffects;
using Avalonia.Threading; using Avalonia.Threading;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Windows; namespace Artemis.UI.Screens.ProfileEditor.Properties.Windows;
public class EffectConfigurationWindowViewModel : DialogViewModelBase<object?> public class EffectConfigurationWindowViewModel : DialogViewModelBase<object?>
{ {
@ -22,7 +22,7 @@ public class EffectConfigurationWindowViewModel : DialogViewModelBase<object?>
{ {
return ConfigurationViewModel.CanClose() && Dispatcher.UIThread.InvokeAsync(async () => await ConfigurationViewModel.CanCloseAsync()).GetAwaiter().GetResult(); return ConfigurationViewModel.CanClose() && Dispatcher.UIThread.InvokeAsync(async () => await ConfigurationViewModel.CanCloseAsync()).GetAwaiter().GetResult();
} }
private void ConfigurationViewModelOnCloseRequested(object? sender, EventArgs e) private void ConfigurationViewModelOnCloseRequested(object? sender, EventArgs e)
{ {
if (CanClose()) if (CanClose())

View File

@ -67,7 +67,7 @@
<GridSplitter Grid.Row="1" Classes="editor-grid-splitter-horizontal" /> <GridSplitter Grid.Row="1" Classes="editor-grid-splitter-horizontal" />
<Border Grid.Row="2" Classes="card card-condensed" Margin="4" Padding="0" ClipToBounds="True"> <Border Grid.Row="2" Classes="card card-condensed" Margin="4" Padding="0" ClipToBounds="True">
<ContentControl Content="{Binding ProfileElementPropertiesViewModel}"/> <ContentControl Content="{Binding PropertiesViewModel}"/>
</Border> </Border>
</Grid> </Grid>

View File

@ -2,8 +2,8 @@
using System.Reactive.Disposables; using System.Reactive.Disposables;
using Artemis.Core; using Artemis.Core;
using Artemis.UI.Screens.ProfileEditor.MenuBar; using Artemis.UI.Screens.ProfileEditor.MenuBar;
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties;
using Artemis.UI.Screens.ProfileEditor.ProfileTree; using Artemis.UI.Screens.ProfileEditor.ProfileTree;
using Artemis.UI.Screens.ProfileEditor.Properties;
using Artemis.UI.Screens.ProfileEditor.StatusBar; using Artemis.UI.Screens.ProfileEditor.StatusBar;
using Artemis.UI.Screens.ProfileEditor.VisualEditor; using Artemis.UI.Screens.ProfileEditor.VisualEditor;
using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor;
@ -25,13 +25,13 @@ namespace Artemis.UI.Screens.ProfileEditor
ProfileTreeViewModel profileTreeViewModel, ProfileTreeViewModel profileTreeViewModel,
ProfileEditorTitleBarViewModel profileEditorTitleBarViewModel, ProfileEditorTitleBarViewModel profileEditorTitleBarViewModel,
MenuBarViewModel menuBarViewModel, MenuBarViewModel menuBarViewModel,
ProfileElementPropertiesViewModel profileElementPropertiesViewModel, PropertiesViewModel propertiesViewModel,
StatusBarViewModel statusBarViewModel) StatusBarViewModel statusBarViewModel)
: base(hostScreen, "profile-editor") : base(hostScreen, "profile-editor")
{ {
VisualEditorViewModel = visualEditorViewModel; VisualEditorViewModel = visualEditorViewModel;
ProfileTreeViewModel = profileTreeViewModel; ProfileTreeViewModel = profileTreeViewModel;
ProfileElementPropertiesViewModel = profileElementPropertiesViewModel; PropertiesViewModel = propertiesViewModel;
StatusBarViewModel = statusBarViewModel; StatusBarViewModel = statusBarViewModel;
if (OperatingSystem.IsWindows()) if (OperatingSystem.IsWindows())
@ -46,7 +46,7 @@ namespace Artemis.UI.Screens.ProfileEditor
public VisualEditorViewModel VisualEditorViewModel { get; } public VisualEditorViewModel VisualEditorViewModel { get; }
public ProfileTreeViewModel ProfileTreeViewModel { get; } public ProfileTreeViewModel ProfileTreeViewModel { get; }
public MenuBarViewModel? MenuBarViewModel { get; } public MenuBarViewModel? MenuBarViewModel { get; }
public ProfileElementPropertiesViewModel ProfileElementPropertiesViewModel { get; } public PropertiesViewModel PropertiesViewModel { get; }
public StatusBarViewModel StatusBarViewModel { get; } public StatusBarViewModel StatusBarViewModel { get; }
public ProfileConfiguration? ProfileConfiguration => _profileConfiguration?.Value; public ProfileConfiguration? ProfileConfiguration => _profileConfiguration?.Value;