1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Layer properties - Restored much functionality on the reworked VMs

This commit is contained in:
SpoinkyNL 2020-05-24 22:05:04 +02:00
parent ea66dcd39e
commit 221c8bc7e7
29 changed files with 440 additions and 209 deletions

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Windows.Media.Animation;
using Artemis.Core.Extensions; using Artemis.Core.Extensions;
using Artemis.Core.Models.Profile.LayerProperties; using Artemis.Core.Models.Profile.LayerProperties;
using Artemis.Core.Models.Profile.LayerProperties.Attributes; using Artemis.Core.Models.Profile.LayerProperties.Attributes;
@ -37,6 +38,7 @@ namespace Artemis.Core.Models.Profile
Transform = new LayerTransformProperties {IsCorePropertyGroup = true}; Transform = new LayerTransformProperties {IsCorePropertyGroup = true};
_leds = new List<ArtemisLed>(); _leds = new List<ArtemisLed>();
General.PropertyGroupInitialized += GeneralOnPropertyGroupInitialized;
} }
internal Layer(Profile profile, ProfileElement parent, LayerEntity layerEntity) internal Layer(Profile profile, ProfileElement parent, LayerEntity layerEntity)
@ -52,6 +54,7 @@ namespace Artemis.Core.Models.Profile
Transform = new LayerTransformProperties {IsCorePropertyGroup = true}; Transform = new LayerTransformProperties {IsCorePropertyGroup = true};
_leds = new List<ArtemisLed>(); _leds = new List<ArtemisLed>();
General.PropertyGroupInitialized += GeneralOnPropertyGroupInitialized;
} }
internal LayerEntity LayerEntity { get; set; } internal LayerEntity LayerEntity { get; set; }
@ -147,6 +150,18 @@ namespace Artemis.Core.Models.Profile
#region Shape management #region Shape management
private void GeneralOnPropertyGroupInitialized(object sender, EventArgs e)
{
ApplyShapeType();
General.ShapeType.BaseValueChanged -= ShapeTypeOnBaseValueChanged;
General.ShapeType.BaseValueChanged += ShapeTypeOnBaseValueChanged;
}
private void ShapeTypeOnBaseValueChanged(object sender, EventArgs e)
{
ApplyShapeType();
}
private void ApplyShapeType() private void ApplyShapeType()
{ {
switch (General.ShapeType.CurrentValue) switch (General.ShapeType.CurrentValue)
@ -164,34 +179,21 @@ namespace Artemis.Core.Models.Profile
#endregion #endregion
#region Properties
internal void InitializeProperties(ILayerService layerService)
{
PropertiesInitialized = true;
ApplyShapeType();
}
public bool PropertiesInitialized { get; private set; }
#endregion
#region Rendering #region Rendering
/// <inheritdoc /> /// <inheritdoc />
public override void Update(double deltaTime) public override void Update(double deltaTime)
{ {
if (LayerBrush == null) if (LayerBrush == null || !LayerBrush.BaseProperties.PropertiesInitialized)
return; return;
var properties = new List<BaseLayerProperty>(General.GetAllLayerProperties()); var properties = new List<BaseLayerProperty>(General.GetAllLayerProperties().Where(p => p.BaseKeyframes.Any()));
properties.AddRange(Transform.GetAllLayerProperties()); properties.AddRange(Transform.GetAllLayerProperties().Where(p => p.BaseKeyframes.Any()));
properties.AddRange(LayerBrush.BaseProperties.GetAllLayerProperties()); properties.AddRange(LayerBrush.BaseProperties.GetAllLayerProperties().Where(p => p.BaseKeyframes.Any()));
// For now, reset all keyframe engines after the last keyframe was hit // For now, reset all keyframe engines after the last keyframe was hit
// This is a placeholder method of repeating the animation until repeat modes are implemented // This is a placeholder method of repeating the animation until repeat modes are implemented
var timeLineEnd = properties.Max(p => p.BaseKeyframes.Max(k => k.Position)); var timeLineEnd = properties.Any() ? properties.Max(p => p.BaseKeyframes.Max(k => k.Position)) : TimeSpan.MaxValue;
if (properties.Any(p => p.TimelineProgress >= timeLineEnd)) if (properties.Any(p => p.TimelineProgress >= timeLineEnd))
{ {
General.Override(TimeSpan.Zero); General.Override(TimeSpan.Zero);
@ -218,7 +220,7 @@ namespace Artemis.Core.Models.Profile
/// <inheritdoc /> /// <inheritdoc />
public override void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo) public override void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo)
{ {
if (Path == null || LayerShape?.Path == null) if (Path == null || LayerShape?.Path == null || !General.PropertiesInitialized || !Transform.PropertiesInitialized)
return; return;
canvas.Save(); canvas.Save();
@ -263,7 +265,8 @@ namespace Artemis.Core.Models.Profile
canvas.Scale(sizeProperty.Width / 100f, sizeProperty.Height / 100f, anchorPosition.X, anchorPosition.Y); canvas.Scale(sizeProperty.Width / 100f, sizeProperty.Height / 100f, anchorPosition.X, anchorPosition.Y);
canvas.Translate(x, y); canvas.Translate(x, y);
LayerBrush?.Render(canvas, canvasInfo, new SKPath(LayerShape.Path), paint); if (LayerBrush != null && LayerBrush.BaseProperties.PropertiesInitialized)
LayerBrush.Render(canvas, canvasInfo, new SKPath(LayerShape.Path), paint);
} }
private void ClipRender(SKCanvas canvas, SKImageInfo canvasInfo, SKPaint paint) private void ClipRender(SKCanvas canvas, SKImageInfo canvasInfo, SKPaint paint)
@ -405,6 +408,7 @@ namespace Artemis.Core.Models.Profile
public event EventHandler RenderPropertiesUpdated; public event EventHandler RenderPropertiesUpdated;
public event EventHandler ShapePropertiesUpdated; public event EventHandler ShapePropertiesUpdated;
public event EventHandler LayerBrushUpdated;
private void OnRenderPropertiesUpdated() private void OnRenderPropertiesUpdated()
{ {
@ -416,6 +420,11 @@ namespace Artemis.Core.Models.Profile
ShapePropertiesUpdated?.Invoke(this, EventArgs.Empty); ShapePropertiesUpdated?.Invoke(this, EventArgs.Empty);
} }
internal void OnLayerBrushUpdated()
{
LayerBrushUpdated?.Invoke(this, EventArgs.Empty);
}
#endregion #endregion
} }

View File

@ -9,10 +9,17 @@ namespace Artemis.Core.Models.Profile.LayerProperties
/// </summary> /// </summary>
public abstract class BaseLayerProperty public abstract class BaseLayerProperty
{ {
private bool _keyframesEnabled;
internal BaseLayerProperty() internal BaseLayerProperty()
{ {
} }
/// <summary>
/// The layer this property applies to
/// </summary>
public Layer Layer { get; internal set; }
/// <summary> /// <summary>
/// The parent group of this layer property, set after construction /// The parent group of this layer property, set after construction
/// </summary> /// </summary>
@ -21,13 +28,22 @@ namespace Artemis.Core.Models.Profile.LayerProperties
/// <summary> /// <summary>
/// Gets whether keyframes are supported on this property /// Gets whether keyframes are supported on this property
/// </summary> /// </summary>
public bool KeyframesSupported { get; protected set; } public bool KeyframesSupported { get; protected set; } = true;
/// <summary> /// <summary>
/// Gets or sets whether keyframes are enabled on this property, has no effect if <see cref="KeyframesSupported" /> is /// Gets or sets whether keyframes are enabled on this property, has no effect if <see cref="KeyframesSupported" /> is
/// False /// False
/// </summary> /// </summary>
public bool KeyframesEnabled { get; set; } public bool KeyframesEnabled
{
get => _keyframesEnabled;
set
{
if (_keyframesEnabled == value) return;
_keyframesEnabled = value;
OnKeyframesToggled();
}
}
/// <summary> /// <summary>
/// Gets or sets whether the property is hidden in the UI /// Gets or sets whether the property is hidden in the UI
@ -57,17 +73,73 @@ namespace Artemis.Core.Models.Profile.LayerProperties
internal PropertyEntity PropertyEntity { get; set; } internal PropertyEntity PropertyEntity { get; set; }
internal LayerPropertyGroup LayerPropertyGroup { get; set; } internal LayerPropertyGroup LayerPropertyGroup { get; set; }
/// <summary> /// <summary>
/// Applies the provided property entity to the layer property by deserializing the JSON base value and keyframe values /// Applies the provided property entity to the layer property by deserializing the JSON base value and keyframe values
/// </summary> /// </summary>
/// <param name="entity"></param> /// <param name="entity"></param>
/// <param name="layerPropertyGroup"></param> /// <param name="layerPropertyGroup"></param>
internal abstract void ApplyToLayerProperty(PropertyEntity entity, LayerPropertyGroup layerPropertyGroup); /// <param name="fromStorage"></param>
internal abstract void ApplyToLayerProperty(PropertyEntity entity, LayerPropertyGroup layerPropertyGroup, bool fromStorage);
/// <summary> /// <summary>
/// Saves the property to the underlying property entity that was configured when calling /// Saves the property to the underlying property entity that was configured when calling
/// <see cref="ApplyToLayerProperty" /> /// <see cref="ApplyToLayerProperty" />
/// </summary> /// </summary>
internal abstract void ApplyToEntity(); internal abstract void ApplyToEntity();
#region Events
/// <summary>
/// Occurs once every frame when the layer property is updated
/// </summary>
public event EventHandler Updated;
/// <summary>
/// Occurs when the base value of the layer property was updated
/// </summary>
public event EventHandler BaseValueChanged;
/// <summary>
/// Occurs when keyframes are enabled/disabled
/// </summary>
public event EventHandler KeyframesToggled;
/// <summary>
/// Occurs when a new keyframe was added to the layer property
/// </summary>
public event EventHandler KeyframeAdded;
/// <summary>
/// Occurs when a keyframe was removed from the layer property
/// </summary>
public event EventHandler KeyframeRemoved;
protected virtual void OnUpdated()
{
Updated?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnBaseValueChanged()
{
BaseValueChanged?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnKeyframesToggled()
{
KeyframesToggled?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnKeyframeAdded()
{
KeyframeAdded?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnKeyframeRemoved()
{
KeyframeRemoved?.Invoke(this, EventArgs.Empty);
}
#endregion
} }
} }

View File

@ -180,12 +180,13 @@ namespace Artemis.Core.Models.Profile.LayerProperties
// The current keyframe is the last keyframe before the current time // The current keyframe is the last keyframe before the current time
CurrentKeyframe = _keyframes.LastOrDefault(k => k.Position <= TimelineProgress); CurrentKeyframe = _keyframes.LastOrDefault(k => k.Position <= TimelineProgress);
// The next keyframe is the first keyframe that's after the current time // Keyframes are sorted by position so we can safely assume the next keyframe's position is after the current
NextKeyframe = _keyframes.FirstOrDefault(k => k.Position > TimelineProgress); var nextIndex = _keyframes.IndexOf(CurrentKeyframe) + 1;
NextKeyframe = _keyframes.Count > nextIndex ? _keyframes[nextIndex] : null;
// No need to update the current value if either of the keyframes are null // No need to update the current value if either of the keyframes are null
if (CurrentKeyframe == null) if (CurrentKeyframe == null)
CurrentValue = BaseValue; CurrentValue = _keyframes.Any() ? _keyframes[0].Value : BaseValue;
else if (NextKeyframe == null) else if (NextKeyframe == null)
CurrentValue = CurrentKeyframe.Value; CurrentValue = CurrentKeyframe.Value;
// Only determine progress and current value if both keyframes are present // Only determine progress and current value if both keyframes are present
@ -218,7 +219,7 @@ namespace Artemis.Core.Models.Profile.LayerProperties
_keyframes = _keyframes.OrderBy(k => k.Position).ToList(); _keyframes = _keyframes.OrderBy(k => k.Position).ToList();
} }
internal override void ApplyToLayerProperty(PropertyEntity entity, LayerPropertyGroup layerPropertyGroup) internal override void ApplyToLayerProperty(PropertyEntity entity, LayerPropertyGroup layerPropertyGroup, bool fromStorage)
{ {
// Doubt this will happen but let's make sure // Doubt this will happen but let's make sure
if (_isInitialized) if (_isInitialized)
@ -231,8 +232,10 @@ namespace Artemis.Core.Models.Profile.LayerProperties
try try
{ {
IsLoadedFromStorage = true; if (entity.Value != null)
BaseValue = JsonConvert.DeserializeObject<T>(entity.Value); BaseValue = JsonConvert.DeserializeObject<T>(entity.Value);
IsLoadedFromStorage = fromStorage;
CurrentValue = BaseValue; CurrentValue = BaseValue;
KeyframesEnabled = entity.KeyframesEnabled; KeyframesEnabled = entity.KeyframesEnabled;
@ -259,7 +262,7 @@ namespace Artemis.Core.Models.Profile.LayerProperties
internal override void ApplyToEntity() internal override void ApplyToEntity()
{ {
if (_isInitialized) if (!_isInitialized)
throw new ArtemisCoreException("Layer property is not yet initialized"); throw new ArtemisCoreException("Layer property is not yet initialized");
PropertyEntity.Value = JsonConvert.SerializeObject(BaseValue); PropertyEntity.Value = JsonConvert.SerializeObject(BaseValue);
@ -272,49 +275,5 @@ namespace Artemis.Core.Models.Profile.LayerProperties
EasingFunction = (int) k.EasingFunction EasingFunction = (int) k.EasingFunction
})); }));
} }
#region Events
/// <summary>
/// Occurs once every frame when the layer property is updated
/// </summary>
public event EventHandler Updated;
/// <summary>
/// Occurs when the base value of the layer property was updated
/// </summary>
public event EventHandler BaseValueChanged;
/// <summary>
/// Occurs when a new keyframe was added to the layer property
/// </summary>
public event EventHandler KeyframeAdded;
/// <summary>
/// Occurs when a keyframe was removed from the layer property
/// </summary>
public event EventHandler KeyframeRemoved;
protected virtual void OnUpdated()
{
Updated?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnBaseValueChanged()
{
BaseValueChanged?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnKeyframeAdded()
{
KeyframeAdded?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnKeyframeRemoved()
{
KeyframeRemoved?.Invoke(this, EventArgs.Empty);
}
#endregion
} }
} }

View File

@ -2,12 +2,14 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using Artemis.Core.Annotations;
using Artemis.Core.Events; using Artemis.Core.Events;
using Artemis.Core.Exceptions; using Artemis.Core.Exceptions;
using Artemis.Core.Models.Profile.LayerProperties; using Artemis.Core.Models.Profile.LayerProperties;
using Artemis.Core.Models.Profile.LayerProperties.Attributes; using Artemis.Core.Models.Profile.LayerProperties.Attributes;
using Artemis.Core.Plugins.Exceptions; using Artemis.Core.Plugins.Exceptions;
using Artemis.Core.Services.Interfaces; using Artemis.Core.Services.Interfaces;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core.Models.Profile namespace Artemis.Core.Models.Profile
{ {
@ -23,6 +25,11 @@ namespace Artemis.Core.Models.Profile
_layerPropertyGroups = new List<LayerPropertyGroup>(); _layerPropertyGroups = new List<LayerPropertyGroup>();
} }
/// <summary>
/// The layer this property group applies to
/// </summary>
public Layer Layer { get; internal set; }
/// <summary> /// <summary>
/// The parent group of this layer property group, set after construction /// The parent group of this layer property group, set after construction
/// </summary> /// </summary>
@ -60,12 +67,16 @@ namespace Artemis.Core.Models.Profile
{ {
} }
internal void InitializeProperties(ILayerService layerService, Layer layer, string path) internal void InitializeProperties(ILayerService layerService, Layer layer, [NotNull] string path)
{ {
if (path == null)
throw new ArgumentNullException(nameof(path));
// Doubt this will happen but let's make sure // Doubt this will happen but let's make sure
if (PropertiesInitialized) if (PropertiesInitialized)
throw new ArtemisCoreException("Layer property group already initialized, wut"); throw new ArtemisCoreException("Layer property group already initialized, wut");
Layer = layer;
// Get all properties with a PropertyDescriptionAttribute // Get all properties with a PropertyDescriptionAttribute
foreach (var propertyInfo in GetType().GetProperties()) foreach (var propertyInfo in GetType().GetProperties())
{ {
@ -77,7 +88,8 @@ namespace Artemis.Core.Models.Profile
var instance = (BaseLayerProperty) Activator.CreateInstance(propertyInfo.PropertyType, true); var instance = (BaseLayerProperty) Activator.CreateInstance(propertyInfo.PropertyType, true);
instance.Parent = this; instance.Parent = this;
InitializeProperty(layer, path, instance); instance.Layer = layer;
InitializeProperty(layer, path + propertyInfo.Name, instance);
propertyInfo.SetValue(this, instance); propertyInfo.SetValue(this, instance);
_layerProperties.Add(instance); _layerProperties.Add(instance);
} }
@ -100,6 +112,8 @@ namespace Artemis.Core.Models.Profile
OnPropertiesInitialized(); OnPropertiesInitialized();
PropertiesInitialized = true; PropertiesInitialized = true;
OnPropertyGroupInitialized();
} }
internal void ApplyToEntity() internal void ApplyToEntity()
@ -110,12 +124,16 @@ namespace Artemis.Core.Models.Profile
var propertyDescription = Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyDescriptionAttribute)); var propertyDescription = Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyDescriptionAttribute));
if (propertyDescription != null) if (propertyDescription != null)
{ {
var layerProperty = (BaseLayerProperty) propertyInfo.GetValue(this);
layerProperty.ApplyToEntity();
} }
else else
{ {
var propertyGroupDescription = Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyGroupDescriptionAttribute)); var propertyGroupDescription = Attribute.GetCustomAttribute(propertyInfo, typeof(PropertyGroupDescriptionAttribute));
if (propertyGroupDescription != null) if (propertyGroupDescription != null)
{ {
var layerPropertyGroup = (LayerPropertyGroup) propertyInfo.GetValue(this);
layerPropertyGroup.ApplyToEntity();
} }
} }
} }
@ -157,14 +175,22 @@ namespace Artemis.Core.Models.Profile
{ {
var pluginGuid = IsCorePropertyGroup || instance.IsCoreProperty ? Constants.CorePluginInfo.Guid : layer.LayerBrush.PluginInfo.Guid; var pluginGuid = IsCorePropertyGroup || instance.IsCoreProperty ? Constants.CorePluginInfo.Guid : layer.LayerBrush.PluginInfo.Guid;
var entity = layer.LayerEntity.PropertyEntities.FirstOrDefault(p => p.PluginGuid == pluginGuid && p.Path == path); var entity = layer.LayerEntity.PropertyEntities.FirstOrDefault(p => p.PluginGuid == pluginGuid && p.Path == path);
if (entity != null) var fromStorage = true;
instance.ApplyToLayerProperty(entity, this); if (entity == null)
{
fromStorage = false;
entity = new PropertyEntity {PluginGuid = pluginGuid, Path = path};
layer.LayerEntity.PropertyEntities.Add(entity);
}
instance.ApplyToLayerProperty(entity, this, fromStorage);
} }
#region Events #region Events
internal event EventHandler<PropertyGroupUpdatingEventArgs> PropertyGroupUpdating; internal event EventHandler<PropertyGroupUpdatingEventArgs> PropertyGroupUpdating;
internal event EventHandler<PropertyGroupUpdatingEventArgs> PropertyGroupOverriding; internal event EventHandler<PropertyGroupUpdatingEventArgs> PropertyGroupOverriding;
public event EventHandler PropertyGroupInitialized;
internal virtual void OnPropertyGroupUpdating(PropertyGroupUpdatingEventArgs e) internal virtual void OnPropertyGroupUpdating(PropertyGroupUpdatingEventArgs e)
{ {
@ -177,5 +203,10 @@ namespace Artemis.Core.Models.Profile
} }
#endregion #endregion
protected virtual void OnPropertyGroupInitialized()
{
PropertyGroupInitialized?.Invoke(this, EventArgs.Empty);
}
} }
} }

View File

@ -51,6 +51,7 @@ namespace Artemis.Core.Plugins.LayerBrush
internal override void InitializeProperties(ILayerService layerService, string path) internal override void InitializeProperties(ILayerService layerService, string path)
{ {
Properties = Activator.CreateInstance<T>();
Properties.InitializeProperties(layerService, Layer, path); Properties.InitializeProperties(layerService, Layer, path);
OnPropertiesInitialized(); OnPropertiesInitialized();
PropertiesInitialized = true; PropertiesInitialized = true;

View File

@ -27,8 +27,8 @@ namespace Artemis.Core.Services
var layer = new Layer(profile, parent, name); var layer = new Layer(profile, parent, name);
// Layers have two hardcoded property groups, instantiate them // Layers have two hardcoded property groups, instantiate them
layer.General.InitializeProperties(this, layer, null); layer.General.InitializeProperties(this, layer, "General.");
layer.Transform.InitializeProperties(this, layer, null); layer.Transform.InitializeProperties(this, layer, "Transform.");
// With the properties loaded, the layer brush can be instantiated // With the properties loaded, the layer brush can be instantiated
InstantiateLayerBrush(layer); InstantiateLayerBrush(layer);
@ -58,11 +58,10 @@ namespace Artemis.Core.Services
new ConstructorArgument("layer", layer), new ConstructorArgument("layer", layer),
new ConstructorArgument("descriptor", descriptor) new ConstructorArgument("descriptor", descriptor)
}; };
var layerBrush = (BaseLayerBrush) _kernel.Get(descriptor.LayerBrushType, arguments); layer.LayerBrush = (BaseLayerBrush)_kernel.Get(descriptor.LayerBrushType, arguments); ;
layerBrush.InitializeProperties(this, null); layer.LayerBrush.InitializeProperties(this, "LayerBrush.");
layer.LayerBrush = layerBrush; layer.OnLayerBrushUpdated();
return layer.LayerBrush;
return layerBrush;
} }
public void RemoveLayerBrush(Layer layer) public void RemoveLayerBrush(Layer layer)

View File

@ -93,8 +93,8 @@ namespace Artemis.Core.Services.Storage
module.ChangeActiveProfile(profile, _surfaceService.ActiveSurface); module.ChangeActiveProfile(profile, _surfaceService.ActiveSurface);
if (profile != null) if (profile != null)
{ {
InitializeCoreProperties(profile); InitializeLayerProperties(profile);
InstantiateProfileLayerBrushes(profile); InstantiateLayerBrushes(profile);
} }
} }
@ -159,18 +159,18 @@ namespace Artemis.Core.Services.Storage
_logger.Debug("Redo profile update - Success"); _logger.Debug("Redo profile update - Success");
} }
private void InitializeCoreProperties(Profile profile) private void InitializeLayerProperties(Profile profile)
{ {
foreach (var layer in profile.GetAllLayers().Where(l => l.LayerBrush == null)) foreach (var layer in profile.GetAllLayers().Where(l => l.LayerBrush == null))
{ {
if (!layer.General.PropertiesInitialized) if (!layer.General.PropertiesInitialized)
layer.General.InitializeProperties(_layerService, layer, null); layer.General.InitializeProperties(_layerService, layer, "General.");
if (!layer.Transform.PropertiesInitialized) if (!layer.Transform.PropertiesInitialized)
layer.Transform.InitializeProperties(_layerService, layer, null); layer.Transform.InitializeProperties(_layerService, layer, "Transform.");
}; };
} }
private void InstantiateProfileLayerBrushes(Profile profile) private void InstantiateLayerBrushes(Profile profile)
{ {
// Only instantiate brushes for layers without an existing brush instance // Only instantiate brushes for layers without an existing brush instance
foreach (var layer in profile.GetAllLayers().Where(l => l.LayerBrush == null)) foreach (var layer in profile.GetAllLayers().Where(l => l.LayerBrush == null))
@ -188,7 +188,7 @@ namespace Artemis.Core.Services.Storage
{ {
var profileModules = _pluginService.GetPluginsOfType<ProfileModule>(); var profileModules = _pluginService.GetPluginsOfType<ProfileModule>();
foreach (var profileModule in profileModules.Where(p => p.ActiveProfile != null).ToList()) foreach (var profileModule in profileModules.Where(p => p.ActiveProfile != null).ToList())
InstantiateProfileLayerBrushes(profileModule.ActiveProfile); InstantiateLayerBrushes(profileModule.ActiveProfile);
} }
#region Event handlers #region Event handlers

View File

@ -4,17 +4,25 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"> d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Style>
<Style>
<Setter Property="Validation.ErrorTemplate" Value="{StaticResource MaterialDesignValidationErrorTemplate}" />
</Style>
</UserControl.Style>
<StackPanel> <StackPanel>
<!-- Drag handle --> <!-- Drag handle -->
<Border x:Name="DragHandle" BorderThickness="0,0,0,1" Height="19"> <Border x:Name="DragHandle" BorderThickness="0,0,0,1" Height="19">
<Border.BorderBrush> <Border.BorderBrush>
<VisualBrush> <VisualBrush>
<VisualBrush.Visual> <VisualBrush.Visual>
<Rectangle StrokeDashArray="2 2" Stroke="{DynamicResource SecondaryAccentBrush}" StrokeThickness="1" <Rectangle x:Name="BorderVisual"
StrokeDashArray="2 2" Stroke="{DynamicResource SecondaryAccentBrush}" StrokeThickness="1"
Width="{Binding RelativeSource={RelativeSource AncestorType={x:Type Border}}, Path=ActualWidth}" Width="{Binding RelativeSource={RelativeSource AncestorType={x:Type Border}}, Path=ActualWidth}"
Height="{Binding RelativeSource={RelativeSource AncestorType={x:Type Border}}, Path=ActualHeight}" /> Height="{Binding RelativeSource={RelativeSource AncestorType={x:Type Border}}, Path=ActualHeight}">
</Rectangle>
</VisualBrush.Visual> </VisualBrush.Visual>
</VisualBrush> </VisualBrush>
</Border.BorderBrush> </Border.BorderBrush>
@ -27,15 +35,14 @@
Foreground="{DynamicResource SecondaryAccentBrush}" Foreground="{DynamicResource SecondaryAccentBrush}"
MouseDown="InputMouseDown" MouseDown="InputMouseDown"
MouseUp="InputMouseUp" MouseUp="InputMouseUp"
MouseMove="InputMouseMove" MouseMove="InputMouseMove"
RequestBringIntoView="Input_OnRequestBringIntoView"/> RequestBringIntoView="Input_OnRequestBringIntoView" />
</Border> </Border>
<!-- Input --> <!-- Input -->
<TextBox x:Name="Input" <TextBox x:Name="Input"
Width="60" Width="60"
Height="20" Height="20"
materialDesign:ValidationAssist.UsePopup="True"
HorizontalAlignment="Left" HorizontalAlignment="Left"
Text="{Binding Value, StringFormat=N3, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}" Text="{Binding Value, StringFormat=N3, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
LostFocus="InputLostFocus" LostFocus="InputLostFocus"

View File

@ -104,6 +104,8 @@ namespace Artemis.UI.Shared.Controls
var startX = new decimal(_mouseDragStartPoint.X); var startX = new decimal(_mouseDragStartPoint.X);
var x = new decimal(e.GetPosition((IInputElement) sender).X); var x = new decimal(e.GetPosition((IInputElement) sender).X);
var stepSize = new decimal(StepSize); var stepSize = new decimal(StepSize);
if (stepSize == 0)
stepSize = 0.1m;
Value = (float) UltimateRoundingFunction(startValue + stepSize * (x - startX), stepSize, 0.5m); Value = (float) UltimateRoundingFunction(startValue + stepSize * (x - startX), stepSize, 0.5m);
} }

View File

@ -20,8 +20,8 @@ namespace Artemis.UI.Shared.Utilities
var resultCallback = new HitTestResultCallback(r => HitTestResultBehavior.Continue); var resultCallback = new HitTestResultCallback(r => HitTestResultBehavior.Continue);
var filterCallback = new HitTestFilterCallback(e => var filterCallback = new HitTestFilterCallback(e =>
{ {
if (e is FrameworkElement fe && fe.DataContext.GetType() == typeof(T) && !result.Contains((T) fe.DataContext)) if (e is FrameworkElement fe && fe.DataContext is T context && !result.Contains(context))
result.Add((T) fe.DataContext); result.Add(context);
return HitTestFilterBehavior.Continue; return HitTestFilterBehavior.Continue;
}); });
VisualTreeHelper.HitTest(container, filterCallback, resultCallback, hitTestParams); VisualTreeHelper.HitTest(container, filterCallback, resultCallback, hitTestParams);

View File

@ -43,6 +43,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
set => ProfileEditorService.CurrentTime = TimeSpan.FromSeconds(value.Left / ProfileEditorService.PixelsPerSecond); set => ProfileEditorService.CurrentTime = TimeSpan.FromSeconds(value.Left / ProfileEditorService.PixelsPerSecond);
} }
public Layer SelectedLayer { get; set; }
public BindableCollection<LayerPropertyGroupViewModel> LayerPropertyGroups { get; set; } public BindableCollection<LayerPropertyGroupViewModel> LayerPropertyGroups { get; set; }
public TreeViewModel TreeViewModel { get; set; } public TreeViewModel TreeViewModel { get; set; }
public TimelineViewModel TimelineViewModel { get; set; } public TimelineViewModel TimelineViewModel { get; set; }
@ -86,9 +87,21 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
private void PopulateProperties(ProfileElement profileElement) private void PopulateProperties(ProfileElement profileElement)
{ {
if (SelectedLayer != null)
{
SelectedLayer.LayerBrushUpdated -= SelectedLayerOnLayerBrushUpdated;
SelectedLayer = null;
}
foreach (var layerPropertyGroupViewModel in LayerPropertyGroups)
layerPropertyGroupViewModel.Dispose();
LayerPropertyGroups.Clear(); LayerPropertyGroups.Clear();
if (profileElement is Layer layer) if (profileElement is Layer layer)
{ {
SelectedLayer = layer;
SelectedLayer.LayerBrushUpdated += SelectedLayerOnLayerBrushUpdated;
// Add the built-in root groups of the layer // Add the built-in root groups of the layer
var generalAttribute = Attribute.GetCustomAttribute( var generalAttribute = Attribute.GetCustomAttribute(
layer.GetType().GetProperty(nameof(layer.General)), layer.GetType().GetProperty(nameof(layer.General)),
@ -113,11 +126,35 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
LayerPropertyGroups.Add(new LayerPropertyGroupViewModel(ProfileEditorService, layer.LayerBrush.BaseProperties, brushDescription)); LayerPropertyGroups.Add(new LayerPropertyGroupViewModel(ProfileEditorService, layer.LayerBrush.BaseProperties, brushDescription));
} }
} }
else
SelectedLayer = null;
TreeViewModel = new TreeViewModel(this, LayerPropertyGroups); TreeViewModel = new TreeViewModel(this, LayerPropertyGroups);
TimelineViewModel = new TimelineViewModel(this, LayerPropertyGroups); TimelineViewModel = new TimelineViewModel(this, LayerPropertyGroups);
} }
private void SelectedLayerOnLayerBrushUpdated(object sender, EventArgs e)
{
// Get rid of the old layer properties group
if (LayerPropertyGroups.Count == 3)
{
LayerPropertyGroups[2].Dispose();
LayerPropertyGroups.RemoveAt(2);
}
if (SelectedLayer.LayerBrush != null)
{
// Add the rout group of the brush
// The root group of the brush has no attribute so let's pull one out of our sleeve
var brushDescription = new PropertyGroupDescriptionAttribute
{
Name = SelectedLayer.LayerBrush.Descriptor.DisplayName,
Description = SelectedLayer.LayerBrush.Descriptor.Description
};
LayerPropertyGroups.Add(new LayerPropertyGroupViewModel(ProfileEditorService, SelectedLayer.LayerBrush.BaseProperties, brushDescription));
}
}
#endregion #endregion
#region Controls #region Controls

View File

@ -50,6 +50,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties
public override void Dispose() public override void Dispose()
{ {
TreePropertyViewModel.Dispose(); TreePropertyViewModel.Dispose();
TimelinePropertyViewModel.Dispose();
} }
public void SetCurrentValue(T value, bool saveChanges) public void SetCurrentValue(T value, bool saveChanges)

View File

@ -18,7 +18,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
EasingFunction = easingFunction; EasingFunction = easingFunction;
Description = easingFunction.Humanize(); Description = easingFunction.Humanize();
CreateGeometry(); CreateEasingPoints();
} }
public Easings.Functions EasingFunction { get; } public Easings.Functions EasingFunction { get; }
@ -36,7 +36,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
} }
} }
private void CreateGeometry() private void CreateEasingPoints()
{ {
EasingPoints = new PointCollection(); EasingPoints = new PointCollection();
for (var i = 1; i <= 10; i++) for (var i = 1; i <= 10; i++)

View File

@ -45,7 +45,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
#endregion #endregion
} }
public abstract class TimelineKeyframeViewModel public abstract class TimelineKeyframeViewModel : PropertyChangedBase
{ {
private readonly IProfileEditorService _profileEditorService; private readonly IProfileEditorService _profileEditorService;
private readonly TimelineViewModel _timelineViewModel; private readonly TimelineViewModel _timelineViewModel;
@ -56,6 +56,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
_profileEditorService = profileEditorService; _profileEditorService = profileEditorService;
_timelineViewModel = timelineViewModel; _timelineViewModel = timelineViewModel;
BaseLayerPropertyKeyframe = baseLayerPropertyKeyframe; BaseLayerPropertyKeyframe = baseLayerPropertyKeyframe;
EasingViewModels = new BindableCollection<TimelineEasingViewModel>();
} }
public BaseLayerPropertyKeyframe BaseLayerPropertyKeyframe { get; } public BaseLayerPropertyKeyframe BaseLayerPropertyKeyframe { get; }
@ -139,6 +140,16 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
#region Easing #region Easing
public void ContextMenuOpening()
{
CreateEasingViewModels();
}
public void ContextMenuClosing()
{
EasingViewModels.Clear();
}
private void CreateEasingViewModels() private void CreateEasingViewModels()
{ {
EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions)).Cast<Easings.Functions>().Select(v => new TimelineEasingViewModel(this, v))); EasingViewModels.AddRange(Enum.GetValues(typeof(Easings.Functions)).Cast<Easings.Functions>().Select(v => new TimelineEasingViewModel(this, v)));

View File

@ -12,72 +12,82 @@
d:DataContext="{d:DesignInstance local:TimelinePropertyGroupViewModel}"> d:DataContext="{d:DesignInstance local:TimelinePropertyGroupViewModel}">
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="25" /> <RowDefinition Height="24" />
<RowDefinition Height="1" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Border Height="25" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource MaterialDesignDivider}" Grid.Row="0">
<ItemsControl ItemsSource="{Binding TimelineKeyframeViewModels}" <ItemsControl Grid.Row="0"
Height="24"
Visibility="{Binding LayerPropertyGroupViewModel.IsExpanded, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}"
ItemsSource="{Binding TimelineKeyframeViewModels}"
Background="{DynamicResource MaterialDesignToolBarBackground}" Background="{DynamicResource MaterialDesignToolBarBackground}"
HorizontalAlignment="Left"> HorizontalAlignment="Left">
<ItemsControl.ItemsPanel> <ItemsControl.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
<Canvas /> <Canvas />
</ItemsPanelTemplate> </ItemsPanelTemplate>
</ItemsControl.ItemsPanel> </ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle> <ItemsControl.ItemContainerStyle>
<Style TargetType="{x:Type ContentPresenter}"> <Style TargetType="{x:Type ContentPresenter}">
<Setter Property="Canvas.Left" Value="{Binding X}" /> <Setter Property="Canvas.Left" Value="{Binding X}" />
</Style> </Style>
</ItemsControl.ItemContainerStyle> </ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<Ellipse Fill="{StaticResource PrimaryHueMidBrush}" <Ellipse Fill="{StaticResource PrimaryHueMidBrush}"
Stroke="White" Stroke="White"
StrokeThickness="0" StrokeThickness="0"
Width="10" Width="10"
Height="10" Height="10"
Margin="-5,6,0,0" Margin="-5,6,0,0"
s:View.ActionTarget="{Binding}"> s:View.ActionTarget="{Binding}">
<Ellipse.Style> <Ellipse.Style>
<Style TargetType="{x:Type Ellipse}"> <Style TargetType="{x:Type Ellipse}">
<Style.Triggers> <Style.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True"> <DataTrigger Binding="{Binding IsSelected}" Value="True">
<DataTrigger.EnterActions> <DataTrigger.EnterActions>
<BeginStoryboard> <BeginStoryboard>
<Storyboard> <Storyboard>
<DoubleAnimation Storyboard.TargetProperty="StrokeThickness" To="1" Duration="0:0:0.25" /> <DoubleAnimation Storyboard.TargetProperty="StrokeThickness"
</Storyboard> To="1" Duration="0:0:0.25" />
</BeginStoryboard> </Storyboard>
</DataTrigger.EnterActions> </BeginStoryboard>
<DataTrigger.ExitActions> </DataTrigger.EnterActions>
<BeginStoryboard> <DataTrigger.ExitActions>
<Storyboard> <BeginStoryboard>
<DoubleAnimation Storyboard.TargetProperty="StrokeThickness" To="0" Duration="0:0:0.25" /> <Storyboard>
</Storyboard> <DoubleAnimation Storyboard.TargetProperty="StrokeThickness"
</BeginStoryboard> To="0" Duration="0:0:0.25" />
</DataTrigger.ExitActions> </Storyboard>
</DataTrigger> </BeginStoryboard>
</Style.Triggers> </DataTrigger.ExitActions>
</Style> </DataTrigger>
</Ellipse.Style> </Style.Triggers>
</Ellipse> </Style>
</DataTemplate> </Ellipse.Style>
</ItemsControl.ItemTemplate> </Ellipse>
</ItemsControl> </DataTemplate>
</Border> </ItemsControl.ItemTemplate>
<ItemsControl ItemsSource="{Binding LayerPropertyGroupViewModel.Children}" Grid.Row="1"> </ItemsControl>
<Rectangle Grid.Row="1" HorizontalAlignment="Stretch" Fill="{DynamicResource MaterialDesignDivider}" Height="1" />
<ItemsControl Grid.Row="2"
Visibility="{Binding LayerPropertyGroupViewModel.IsExpanded, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}"
ItemsSource="{Binding LayerPropertyGroupViewModel.Children}">
<ItemsControl.Resources> <ItemsControl.Resources>
<DataTemplate DataType="{x:Type layerProperties:LayerPropertyGroupViewModel}"> <DataTemplate DataType="{x:Type layerProperties:LayerPropertyGroupViewModel}">
<ContentControl s:View.Model="{Binding TimelinePropertyGroupViewModel}" <ContentControl s:View.Model="{Binding TimelinePropertyGroupViewModel}"
VerticalContentAlignment="Stretch" VerticalContentAlignment="Stretch"
HorizontalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"
IsTabStop="False" /> IsTabStop="False" />
</DataTemplate> </DataTemplate>
<DataTemplate DataType="{x:Type layerProperties:LayerPropertyViewModel}"> <DataTemplate DataType="{x:Type layerProperties:LayerPropertyViewModel}">
<ContentControl s:View.Model="{Binding TimelinePropertyBaseViewModel}" <ContentControl s:View.Model="{Binding TimelinePropertyBaseViewModel}"
VerticalContentAlignment="Stretch" VerticalContentAlignment="Stretch"
HorizontalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"
IsTabStop="False" /> IsTabStop="False" />
</DataTemplate> </DataTemplate>
</ItemsControl.Resources> </ItemsControl.Resources>
</ItemsControl> </ItemsControl>

View File

@ -1,12 +1,12 @@
<UserControl x:Class="Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline.TimelinePropertyView" <UserControl x:Class="Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline.TimelinePropertyView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline" xmlns:local="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet" xmlns:s="https://github.com/canton7/Stylet"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800" d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:TimelinePropertyViewModel}"> d:DataContext="{d:DesignInstance local:TimelinePropertyViewModel}">
<Border Height="25" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource MaterialDesignDivider}"> <Border Height="25" BorderThickness="0,0,0,1" BorderBrush="{DynamicResource MaterialDesignDivider}">
@ -35,7 +35,9 @@
s:View.ActionTarget="{Binding}" s:View.ActionTarget="{Binding}"
MouseDown="{s:Action KeyframeMouseDown}" MouseDown="{s:Action KeyframeMouseDown}"
MouseUp="{s:Action KeyframeMouseUp}" MouseUp="{s:Action KeyframeMouseUp}"
MouseMove="{s:Action KeyframeMouseMove}"> MouseMove="{s:Action KeyframeMouseMove}"
ContextMenuOpening="{s:Action ContextMenuOpening}"
ContextMenuClosing="{s:Action ContextMenuClosing}">
<Ellipse.Style> <Ellipse.Style>
<Style TargetType="{x:Type Ellipse}"> <Style TargetType="{x:Type Ellipse}">
<Style.Triggers> <Style.Triggers>
@ -72,6 +74,9 @@
</MenuItem> </MenuItem>
<Separator /> <Separator />
<MenuItem Header="Easing" ItemsSource="{Binding EasingViewModels}"> <MenuItem Header="Easing" ItemsSource="{Binding EasingViewModels}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="Creation" />
</MenuItem.Icon>
<MenuItem.ItemContainerStyle> <MenuItem.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}" BasedOn="{StaticResource MaterialDesignMenuItem}"> <Style TargetType="{x:Type MenuItem}" BasedOn="{StaticResource MaterialDesignMenuItem}">
<Setter Property="IsCheckable" Value="True" /> <Setter Property="IsCheckable" Value="True" />
@ -105,4 +110,4 @@
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
</Border> </Border>
</UserControl> </UserControl>

View File

@ -1,4 +1,6 @@
using System.Linq; using System;
using System.Linq;
using Artemis.UI.Exceptions;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Abstract; using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Abstract;
using Artemis.UI.Services.Interfaces; using Artemis.UI.Services.Interfaces;
using Stylet; using Stylet;
@ -13,27 +15,53 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
{ {
_profileEditorService = profileEditorService; _profileEditorService = profileEditorService;
LayerPropertyViewModel = (LayerPropertyViewModel<T>) layerPropertyBaseViewModel; LayerPropertyViewModel = (LayerPropertyViewModel<T>) layerPropertyBaseViewModel;
LayerPropertyViewModel.LayerProperty.KeyframeAdded += LayerPropertyOnKeyframeModified;
LayerPropertyViewModel.LayerProperty.KeyframeRemoved += LayerPropertyOnKeyframeModified;
LayerPropertyViewModel.LayerProperty.KeyframesToggled += LayerPropertyOnKeyframeModified;
}
private void LayerPropertyOnKeyframeModified(object sender, EventArgs e)
{
UpdateKeyframes();
} }
public LayerPropertyViewModel<T> LayerPropertyViewModel { get; } public LayerPropertyViewModel<T> LayerPropertyViewModel { get; }
public override void UpdateKeyframes(TimelineViewModel timelineViewModel) public override void UpdateKeyframes()
{ {
var keyframes = LayerPropertyViewModel.LayerProperty.Keyframes.ToList(); if (TimelineViewModel == null)
TimelineKeyframeViewModels.RemoveRange( throw new ArtemisUIException("Timeline view model must be set before keyframes can be updated");
TimelineKeyframeViewModels.Where(t => !keyframes.Contains(t.BaseLayerPropertyKeyframe))
); // Only show keyframes if they are enabled
TimelineKeyframeViewModels.AddRange( if (LayerPropertyViewModel.LayerProperty.KeyframesEnabled)
keyframes.Where(k => TimelineKeyframeViewModels.All(t => t.BaseLayerPropertyKeyframe != k)) {
.Select(k => new TimelineKeyframeViewModel<T>(_profileEditorService, timelineViewModel, k)) var keyframes = LayerPropertyViewModel.LayerProperty.Keyframes.ToList();
); var toRemove = TimelineKeyframeViewModels.Where(t => !keyframes.Contains(t.BaseLayerPropertyKeyframe)).ToList();
TimelineKeyframeViewModels.RemoveRange(toRemove);
TimelineKeyframeViewModels.AddRange(
keyframes.Where(k => TimelineKeyframeViewModels.All(t => t.BaseLayerPropertyKeyframe != k))
.Select(k => new TimelineKeyframeViewModel<T>(_profileEditorService, TimelineViewModel, k))
);
}
else
{
TimelineKeyframeViewModels.Clear();
}
foreach (var timelineKeyframeViewModel in TimelineKeyframeViewModels) foreach (var timelineKeyframeViewModel in TimelineKeyframeViewModels)
timelineKeyframeViewModel.Update(_profileEditorService.PixelsPerSecond); timelineKeyframeViewModel.Update(_profileEditorService.PixelsPerSecond);
} }
public override void Dispose()
{
LayerPropertyViewModel.LayerProperty.KeyframeAdded -= LayerPropertyOnKeyframeModified;
LayerPropertyViewModel.LayerProperty.KeyframeRemoved -= LayerPropertyOnKeyframeModified;
LayerPropertyViewModel.LayerProperty.KeyframesToggled -= LayerPropertyOnKeyframeModified;
}
} }
public abstract class TimelinePropertyViewModel public abstract class TimelinePropertyViewModel : IDisposable
{ {
protected TimelinePropertyViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel) protected TimelinePropertyViewModel(LayerPropertyBaseViewModel layerPropertyBaseViewModel)
{ {
@ -42,8 +70,11 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
} }
public LayerPropertyBaseViewModel LayerPropertyBaseViewModel { get; } public LayerPropertyBaseViewModel LayerPropertyBaseViewModel { get; }
public TimelineViewModel TimelineViewModel { get; set; }
public BindableCollection<TimelineKeyframeViewModel> TimelineKeyframeViewModels { get; set; } public BindableCollection<TimelineKeyframeViewModel> TimelineKeyframeViewModels { get; set; }
public abstract void UpdateKeyframes(TimelineViewModel timelineViewModel); public abstract void UpdateKeyframes();
public abstract void Dispose();
} }
} }

View File

@ -1,18 +1,18 @@
<UserControl x:Class="Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline.TimelineView" <UserControl x:Class="Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline.TimelineView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline" xmlns:local="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline"
xmlns:s="https://github.com/canton7/Stylet" xmlns:s="https://github.com/canton7/Stylet"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="25" d:DesignHeight="25"
d:DesignWidth="800" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:TimelineViewModel}"> d:DataContext="{d:DesignInstance local:TimelineViewModel}">
<Grid Background="{DynamicResource MaterialDesignToolBarBackground}" <Grid Background="{DynamicResource MaterialDesignToolBarBackground}"
MouseDown="{s:Action TimelineCanvasMouseDown}" MouseDown="{s:Action TimelineCanvasMouseDown}"
MouseUp="{s:Action TimelineCanvasMouseUp}" MouseUp="{s:Action TimelineCanvasMouseUp}"
MouseMove="{s:Action TimelineCanvasMouseMove}"> MouseMove="{s:Action TimelineCanvasMouseMove}">
<Grid.Triggers> <Grid.Triggers>
<EventTrigger RoutedEvent="UIElement.MouseLeftButtonDown"> <EventTrigger RoutedEvent="UIElement.MouseLeftButtonDown">
<BeginStoryboard> <BeginStoryboard>
@ -42,10 +42,11 @@
</ItemsControl> </ItemsControl>
<!-- Multi-selection rectangle --> <!-- Multi-selection rectangle -->
<Path Data="{Binding SelectionRectangle}" Opacity="0" <Path x:Name="MultiSelectionPath"
Data="{Binding SelectionRectangle}"
Opacity="0"
Stroke="{DynamicResource PrimaryHueLightBrush}" Stroke="{DynamicResource PrimaryHueLightBrush}"
StrokeThickness="1" StrokeThickness="1"
x:Name="MultiSelectionPath"
IsHitTestVisible="False"> IsHitTestVisible="False">
<Path.Fill> <Path.Fill>
<SolidColorBrush Color="{DynamicResource Primary400}" Opacity="0.25" /> <SolidColorBrush Color="{DynamicResource Primary400}" Opacity="0.25" />

View File

@ -35,7 +35,10 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Timeline
foreach (var layerPropertyBaseViewModel in layerPropertyGroupViewModel.GetAllChildren()) foreach (var layerPropertyBaseViewModel in layerPropertyGroupViewModel.GetAllChildren())
{ {
if (layerPropertyBaseViewModel is LayerPropertyViewModel layerPropertyViewModel) if (layerPropertyBaseViewModel is LayerPropertyViewModel layerPropertyViewModel)
layerPropertyViewModel.TimelinePropertyBaseViewModel.UpdateKeyframes(this); {
layerPropertyViewModel.TimelinePropertyBaseViewModel.TimelineViewModel = this;
layerPropertyViewModel.TimelinePropertyBaseViewModel.UpdateKeyframes();
}
} }
} }
} }

View File

@ -11,12 +11,14 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree.PropertyI
{ {
LayerPropertyViewModel = layerPropertyViewModel; LayerPropertyViewModel = layerPropertyViewModel;
LayerPropertyViewModel.LayerProperty.Updated += LayerPropertyOnUpdated; LayerPropertyViewModel.LayerProperty.Updated += LayerPropertyOnUpdated;
UpdateInputValue();
} }
protected PropertyInputViewModel(LayerPropertyViewModel<T> layerPropertyViewModel, IModelValidator validator) : base(validator) protected PropertyInputViewModel(LayerPropertyViewModel<T> layerPropertyViewModel, IModelValidator validator) : base(validator)
{ {
LayerPropertyViewModel = layerPropertyViewModel; LayerPropertyViewModel = layerPropertyViewModel;
LayerPropertyViewModel.LayerProperty.Updated += LayerPropertyOnUpdated; LayerPropertyViewModel.LayerProperty.Updated += LayerPropertyOnUpdated;
UpdateInputValue();
} }
public LayerPropertyViewModel<T> LayerPropertyViewModel { get; } public LayerPropertyViewModel<T> LayerPropertyViewModel { get; }
@ -38,6 +40,10 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree.PropertyI
LayerPropertyViewModel.LayerProperty.Updated -= LayerPropertyOnUpdated; LayerPropertyViewModel.LayerProperty.Updated -= LayerPropertyOnUpdated;
} }
protected virtual void OnInputValueApplied()
{
}
protected virtual void OnInputValueChanged() protected virtual void OnInputValueChanged()
{ {
} }
@ -50,7 +56,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree.PropertyI
private void UpdateInputValue() private void UpdateInputValue()
{ {
// Avoid unnecessary UI updates and validator cycles // Avoid unnecessary UI updates and validator cycles
if (_inputValue.Equals(LayerPropertyViewModel.LayerProperty.CurrentValue)) if (_inputValue != null && _inputValue.Equals(LayerPropertyViewModel.LayerProperty.CurrentValue) || _inputValue == null && LayerPropertyViewModel.LayerProperty.CurrentValue == null)
return; return;
// Override the input value // Override the input value
@ -61,20 +67,24 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree.PropertyI
NotifyOfPropertyChange(nameof(InputValue)); NotifyOfPropertyChange(nameof(InputValue));
// Force the validator to run with the overridden value // Force the validator to run with the overridden value
Validate(); if (Validator != null)
Validate();
} }
private void ApplyInputValue() private void ApplyInputValue()
{ {
// Force the validator to run // Force the validator to run
Validate(); if (Validator != null)
Validate();
// Only apply the input value to the layer property if the validator found no errors // Only apply the input value to the layer property if the validator found no errors
if (!HasErrors) if (!HasErrors)
LayerPropertyViewModel.SetCurrentValue(_inputValue, !InputDragging); LayerPropertyViewModel.SetCurrentValue(_inputValue, !InputDragging);
OnInputValueChanged(); OnInputValueChanged();
OnInputValueApplied();
} }
#region Event handlers #region Event handlers
public void InputDragStarted(object sender, EventArgs e) public void InputDragStarted(object sender, EventArgs e)

View File

@ -27,7 +27,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree.PropertyI
} }
public BindableCollection<ValueDescription> ComboboxValues { get; } public BindableCollection<ValueDescription> ComboboxValues { get; }
public void UpdateEnumValues() public void UpdateEnumValues()
{ {
var layerBrushProviders = _pluginService.GetPluginsOfType<LayerBrushProvider>(); var layerBrushProviders = _pluginService.GetPluginsOfType<LayerBrushProvider>();
@ -47,7 +47,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree.PropertyI
ComboboxValues.Clear(); ComboboxValues.Clear();
ComboboxValues.AddRange(enumValues); ComboboxValues.AddRange(enumValues);
} }
public override void Dispose() public override void Dispose()
{ {
_pluginService.PluginLoaded -= PluginServiceOnPluginLoaded; _pluginService.PluginLoaded -= PluginServiceOnPluginLoaded;
@ -58,5 +58,10 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree.PropertyI
{ {
UpdateEnumValues(); UpdateEnumValues();
} }
protected override void OnInputValueApplied()
{
_layerService.InstantiateLayerBrush(LayerPropertyViewModel.LayerProperty.Layer);
}
} }
} }

View File

@ -8,15 +8,17 @@
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
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:propertyInput="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree.PropertyInput" xmlns:propertyInput="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree.PropertyInput"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800" d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance propertyInput:FloatPropertyInputViewModel}"> d:DataContext="{d:DesignInstance propertyInput:FloatPropertyInputViewModel}">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock Margin="0 0 5 4" Width="10" VerticalAlignment="Bottom" Text="{Binding LayerPropertyViewModel.PropertyDescription.InputPrefix}" /> <TextBlock Margin="0 0 5 4" Width="10" VerticalAlignment="Bottom" Text="{Binding LayerPropertyViewModel.PropertyDescription.InputPrefix}" />
<controls:DraggableFloat Value="{Binding InputValue}" <controls:DraggableFloat Value="{Binding InputValue}"
StepSize="{Binding LayerPropertyViewModel.PropertyDescription.InputStepSize}" materialDesign:ValidationAssist.UsePopup="True"
DragStarted="{s:Action InputDragStarted}" StepSize="{Binding LayerPropertyViewModel.PropertyDescription.InputStepSize}"
DragEnded="{s:Action InputDragEnded}" /> DragStarted="{s:Action InputDragStarted}"
DragEnded="{s:Action InputDragEnded}" />
<TextBlock Margin="5 0 0 4" Width="10" VerticalAlignment="Bottom" Text="{Binding LayerPropertyViewModel.PropertyDescription.InputAffix}" /> <TextBlock Margin="5 0 0 4" Width="10" VerticalAlignment="Bottom" Text="{Binding LayerPropertyViewModel.PropertyDescription.InputAffix}" />
</StackPanel> </StackPanel>
</UserControl> </UserControl>

View File

@ -8,12 +8,14 @@
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
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:propertyInput="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree.PropertyInput" xmlns:propertyInput="clr-namespace:Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree.PropertyInput"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800" d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance propertyInput:IntPropertyInputViewModel}"> d:DataContext="{d:DesignInstance propertyInput:IntPropertyInputViewModel}">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock Margin="0 0 5 4" Width="10" VerticalAlignment="Bottom" Text="{Binding LayerPropertyViewModel.PropertyDescription.InputPrefix}" /> <TextBlock Margin="0 0 5 4" Width="10" VerticalAlignment="Bottom" Text="{Binding LayerPropertyViewModel.PropertyDescription.InputPrefix}" />
<controls:DraggableFloat Value="{Binding InputValue}" <controls:DraggableFloat Value="{Binding InputValue}"
materialDesign:ValidationAssist.UsePopup="True"
StepSize="{Binding LayerPropertyViewModel.PropertyDescription.InputStepSize}" StepSize="{Binding LayerPropertyViewModel.PropertyDescription.InputStepSize}"
DragStarted="{s:Action InputDragStarted}" DragStarted="{s:Action InputDragStarted}"
DragEnded="{s:Action InputDragEnded}" /> DragEnded="{s:Action InputDragEnded}" />

View File

@ -7,7 +7,8 @@
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet" xmlns:s="https://github.com/canton7/Stylet"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"> d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance {x:Type local:TreePropertyViewModel}}">
<Grid Height="22"> <Grid Height="22">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
@ -20,7 +21,7 @@
ToolTip="Toggle key-framing" ToolTip="Toggle key-framing"
Width="18" Width="18"
Height="18" Height="18"
IsChecked="{Binding LayerPropertyViewModel.LayerProperty.KeyframesEnabled}" IsChecked="{Binding KeyframesEnabled}"
IsEnabled="{Binding LayerPropertyViewModel.LayerProperty.KeyframesSupported}" IsEnabled="{Binding LayerPropertyViewModel.LayerProperty.KeyframesSupported}"
VerticalAlignment="Center" Padding="-25"> VerticalAlignment="Center" Padding="-25">
<materialDesign:PackIcon Kind="Stopwatch" Height="13" Width="13" /> <materialDesign:PackIcon Kind="Stopwatch" Height="13" Width="13" />

View File

@ -1,4 +1,7 @@
using System; using System;
using System.Linq;
using Artemis.Core.Models.Profile.LayerProperties;
using Artemis.Core.Utilities;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Abstract; using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Abstract;
using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree.PropertyInput.Abstract; using Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree.PropertyInput.Abstract;
using Artemis.UI.Services.Interfaces; using Artemis.UI.Services.Interfaces;
@ -20,10 +23,37 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree
public LayerPropertyViewModel<T> LayerPropertyViewModel { get; } public LayerPropertyViewModel<T> LayerPropertyViewModel { get; }
public PropertyInputViewModel<T> PropertyInputViewModel { get; set; } public PropertyInputViewModel<T> PropertyInputViewModel { get; set; }
public bool KeyframesEnabled
{
get => LayerPropertyViewModel.LayerProperty.KeyframesEnabled;
set => ApplyKeyframesEnabled(value);
}
public override void Dispose() public override void Dispose()
{ {
PropertyInputViewModel.Dispose(); PropertyInputViewModel.Dispose();
} }
private void ApplyKeyframesEnabled(bool enable)
{
// If enabling keyframes for the first time, add a keyframe with the current value at the current position
if (enable && !LayerPropertyViewModel.LayerProperty.Keyframes.Any())
{
LayerPropertyViewModel.LayerProperty.AddKeyframe(new LayerPropertyKeyframe<T>(
LayerPropertyViewModel.LayerProperty.CurrentValue,
_profileEditorService.CurrentTime,
Easings.Functions.Linear,
LayerPropertyViewModel.LayerProperty
));
}
// If disabling keyframes, set the base value to the current value
else if (!enable && LayerPropertyViewModel.LayerProperty.Keyframes.Any())
LayerPropertyViewModel.LayerProperty.BaseValue = LayerPropertyViewModel.LayerProperty.CurrentValue;
LayerPropertyViewModel.LayerProperty.KeyframesEnabled = enable;
_profileEditorService.UpdateSelectedProfileElement();
}
} }
public abstract class TreePropertyViewModel : IDisposable public abstract class TreePropertyViewModel : IDisposable

View File

@ -35,8 +35,7 @@
BorderThickness="0,0,0,1" BorderThickness="0,0,0,1"
Height="25" Height="25"
Padding="{TemplateBinding Padding}"> Padding="{TemplateBinding Padding}">
<Grid <Grid Margin="{Binding Converter={StaticResource lengthConverter}, RelativeSource={RelativeSource TemplatedParent}}">
Margin="{Binding Converter={StaticResource lengthConverter}, RelativeSource={RelativeSource TemplatedParent}}">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="19" /> <ColumnDefinition Width="19" />
<ColumnDefinition /> <ColumnDefinition />
@ -94,8 +93,7 @@
</Style> </Style>
</TreeView.ItemContainerStyle> </TreeView.ItemContainerStyle>
<TreeView.Resources> <TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type layerProperties:LayerPropertyGroupViewModel}" <HierarchicalDataTemplate DataType="{x:Type layerProperties:LayerPropertyGroupViewModel}" ItemsSource="{Binding Children}">
ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding PropertyGroupDescription.Name}" <TextBlock Text="{Binding PropertyGroupDescription.Name}"
ToolTip="{Binding PropertyGroupDescription.Description}" Margin="5 0" ToolTip="{Binding PropertyGroupDescription.Description}" Margin="5 0"

View File

@ -1,4 +1,5 @@
using System.Windows; using System.Linq;
using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input; using System.Windows.Input;
using Stylet; using Stylet;
@ -19,7 +20,7 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.LayerProperties.Tree
public void PropertyTreePreviewMouseWheel(object sender, MouseWheelEventArgs e) public void PropertyTreePreviewMouseWheel(object sender, MouseWheelEventArgs e)
{ {
if (e.Handled || !(sender is TreeView)) if (e.Handled || !(sender is System.Windows.Controls.TreeView))
return; return;
e.Handled = true; e.Handled = true;

View File

@ -103,19 +103,23 @@ namespace Artemis.UI.Screens.Module.ProfileEditor.Visualization
OpacityGeometry = opacityGeometry; OpacityGeometry = opacityGeometry;
// Render the store as a bitmap // Render the store as a bitmap
var drawingImage = new DrawingImage(new GeometryDrawing(new SolidColorBrush(Colors.Black), null, LayerGeometry)); Execute.OnUIThread(() =>
var image = new Image {Source = drawingImage}; {
var bitmap = new RenderTargetBitmap( var drawingImage = new DrawingImage(new GeometryDrawing(new SolidColorBrush(Colors.Black), null, LayerGeometry));
(int) (LayerGeometry.Bounds.Width * 2.5), var image = new Image { Source = drawingImage };
(int) (LayerGeometry.Bounds.Height * 2.5), var bitmap = new RenderTargetBitmap(
96, (int)(LayerGeometry.Bounds.Width * 2.5),
96, (int)(LayerGeometry.Bounds.Height * 2.5),
PixelFormats.Pbgra32 96,
); 96,
image.Arrange(new Rect(0, 0, bitmap.Width, bitmap.Height)); PixelFormats.Pbgra32
bitmap.Render(image); );
bitmap.Freeze(); image.Arrange(new Rect(0, 0, bitmap.Width, bitmap.Height));
LayerGeometryBitmap = bitmap; bitmap.Render(image);
bitmap.Freeze();
LayerGeometryBitmap = bitmap;
});
} }
private void CreateShapeGeometry() private void CreateShapeGeometry()

View File

@ -139,8 +139,7 @@ namespace Artemis.UI.Services
UpdateProfilePreview(); UpdateProfilePreview();
OnSelectedProfileElementUpdated(new ProfileElementEventArgs(SelectedProfileElement)); OnSelectedProfileElementUpdated(new ProfileElementEventArgs(SelectedProfileElement));
} }
public void UpdateProfilePreview() public void UpdateProfilePreview()
{ {
if (SelectedProfile == null) if (SelectedProfile == null)