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

Core - Reworked brush/effect property storage

Profile editor - Added brush selection
Profile editor - Added playback controls
This commit is contained in:
Robert 2022-01-16 00:46:19 +01:00
parent 022beb6a48
commit 1832a25426
64 changed files with 1168 additions and 481 deletions

View File

@ -94,7 +94,7 @@ namespace Artemis.Core
/// <summary> /// <summary>
/// Gets the general properties of the layer /// Gets the general properties of the layer
/// </summary> /// </summary>
[PropertyGroupDescription(Name = "General", Description = "A collection of general properties")] [PropertyGroupDescription(Identifier = "General", Name = "General", Description = "A collection of general properties")]
public LayerGeneralProperties General public LayerGeneralProperties General
{ {
get => _general; get => _general;
@ -104,7 +104,7 @@ namespace Artemis.Core
/// <summary> /// <summary>
/// Gets the transform properties of the layer /// Gets the transform properties of the layer
/// </summary> /// </summary>
[PropertyGroupDescription(Name = "Transform", Description = "A collection of transformation properties")] [PropertyGroupDescription(Identifier = "Transform", Name = "Transform", Description = "A collection of transformation properties")]
public LayerTransformProperties Transform public LayerTransformProperties Transform
{ {
get => _transform; get => _transform;
@ -208,19 +208,20 @@ namespace Artemis.Core
LayerBrushStore.LayerBrushRemoved += LayerBrushStoreOnLayerBrushRemoved; LayerBrushStore.LayerBrushRemoved += LayerBrushStoreOnLayerBrushRemoved;
// Layers have two hardcoded property groups, instantiate them // Layers have two hardcoded property groups, instantiate them
Attribute generalAttribute = Attribute.GetCustomAttribute( PropertyGroupDescriptionAttribute generalAttribute = (PropertyGroupDescriptionAttribute) Attribute.GetCustomAttribute(
GetType().GetProperty(nameof(General))!, GetType().GetProperty(nameof(General))!,
typeof(PropertyGroupDescriptionAttribute) typeof(PropertyGroupDescriptionAttribute)
)!; )!;
Attribute transformAttribute = Attribute.GetCustomAttribute( PropertyGroupDescriptionAttribute transformAttribute = (PropertyGroupDescriptionAttribute) Attribute.GetCustomAttribute(
GetType().GetProperty(nameof(Transform))!, GetType().GetProperty(nameof(Transform))!,
typeof(PropertyGroupDescriptionAttribute) typeof(PropertyGroupDescriptionAttribute)
)!; )!;
General.GroupDescription = (PropertyGroupDescriptionAttribute) generalAttribute; LayerEntity.GeneralPropertyGroup ??= new PropertyGroupEntity {Identifier = generalAttribute.Identifier};
General.Initialize(this, "General.", Constants.CorePluginFeature); LayerEntity.TransformPropertyGroup ??= new PropertyGroupEntity {Identifier = transformAttribute.Identifier};
Transform.GroupDescription = (PropertyGroupDescriptionAttribute) transformAttribute;
Transform.Initialize(this, "Transform.", Constants.CorePluginFeature); General.Initialize(this, null, generalAttribute, LayerEntity.GeneralPropertyGroup);
Transform.Initialize(this, null, transformAttribute, LayerEntity.TransformPropertyGroup);
General.ShapeType.CurrentValueSet += ShapeTypeOnCurrentValueSet; General.ShapeType.CurrentValueSet += ShapeTypeOnCurrentValueSet;
ApplyShapeType(); ApplyShapeType();
@ -241,8 +242,7 @@ namespace Artemis.Core
return; return;
LayerBrushReference? current = General.BrushReference.CurrentValue; LayerBrushReference? current = General.BrushReference.CurrentValue;
if (e.Registration.PluginFeature.Id == current?.LayerBrushProviderId && if (e.Registration.PluginFeature.Id == current?.LayerBrushProviderId && e.Registration.LayerBrushDescriptor.LayerBrushType.Name == current.BrushType)
e.Registration.LayerBrushDescriptor.LayerBrushType.Name == current.BrushType)
ActivateLayerBrush(); ActivateLayerBrush();
} }
@ -282,7 +282,13 @@ namespace Artemis.Core
General.ApplyToEntity(); General.ApplyToEntity();
Transform.ApplyToEntity(); Transform.ApplyToEntity();
LayerBrush?.BaseProperties?.ApplyToEntity();
// Don't override the old value of LayerBrush if the current value is null, this avoid losing settings of an unavailable brush
if (LayerBrush != null)
{
LayerBrush.Save();
LayerEntity.LayerBrush = LayerBrush.LayerBrushEntity;
}
// LEDs // LEDs
LayerEntity.Leds.Clear(); LayerEntity.Leds.Clear();
@ -712,53 +718,32 @@ namespace Artemis.Core
#region Brush management #region Brush management
/// <summary> /// <summary>
/// Changes the current layer brush to the brush described in the provided <paramref name="descriptor" /> /// Changes the current layer brush to the provided layer brush and activates it
/// </summary> /// </summary>
public void ChangeLayerBrush(LayerBrushDescriptor descriptor) public void ChangeLayerBrush(BaseLayerBrush? layerBrush)
{ {
if (descriptor == null) General.BrushReference.SetCurrentValue(layerBrush != null ? new LayerBrushReference(layerBrush.Descriptor) : null, null);
throw new ArgumentNullException(nameof(descriptor)); LayerBrush = layerBrush;
if (LayerBrush != null) if (LayerBrush != null)
{ ActivateLayerBrush();
BaseLayerBrush brush = LayerBrush; else
LayerBrush = null; OnLayerBrushUpdated();
brush.Dispose();
}
// Ensure the brush reference matches the brush
LayerBrushReference? current = General.BrushReference.BaseValue;
if (!descriptor.MatchesLayerBrushReference(current))
General.BrushReference.SetCurrentValue(new LayerBrushReference(descriptor), null);
ActivateLayerBrush();
}
/// <summary>
/// Removes the current layer brush from the layer
/// </summary>
public void RemoveLayerBrush()
{
if (LayerBrush == null)
return;
BaseLayerBrush brush = LayerBrush;
DeactivateLayerBrush();
LayerEntity.PropertyEntities.RemoveAll(p => p.FeatureId == brush.ProviderId && p.Path.StartsWith("LayerBrush."));
} }
internal void ActivateLayerBrush() internal void ActivateLayerBrush()
{ {
try try
{ {
LayerBrushReference? current = General.BrushReference.CurrentValue; if (LayerBrush == null)
if (current == null) {
// If the brush is null, try to instantiate it
LayerBrushReference? brushReference = General.BrushReference.CurrentValue;
if (brushReference?.LayerBrushProviderId != null && brushReference.BrushType != null)
ChangeLayerBrush(LayerBrushStore.Get(brushReference.LayerBrushProviderId, brushReference.BrushType)?.LayerBrushDescriptor.CreateInstance(this, LayerEntity.LayerBrush));
// If that's not possible there's nothing to do
return; return;
}
LayerBrushDescriptor? descriptor = current.LayerBrushProviderId != null && current.BrushType != null
? LayerBrushStore.Get(current.LayerBrushProviderId, current.BrushType)?.LayerBrushDescriptor
: null;
descriptor?.CreateInstance(this);
General.ShapeType.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation; General.ShapeType.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation;
General.BlendMode.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation; General.BlendMode.IsHidden = LayerBrush != null && !LayerBrush.SupportsTransformation;
@ -778,9 +763,9 @@ namespace Artemis.Core
if (LayerBrush == null) if (LayerBrush == null)
return; return;
BaseLayerBrush brush = LayerBrush; BaseLayerBrush? brush = LayerBrush;
LayerBrush = null; LayerBrush = null;
brush.Dispose(); brush?.Dispose();
OnLayerBrushUpdated(); OnLayerBrushUpdated();
} }

View File

@ -7,6 +7,11 @@ namespace Artemis.Core
/// </summary> /// </summary>
public class PropertyDescriptionAttribute : Attribute public class PropertyDescriptionAttribute : Attribute
{ {
/// <summary>
/// The identifier of this property used for storage, if not set one will be generated property name in code
/// </summary>
public string? Identifier { get; set; }
/// <summary> /// <summary>
/// The user-friendly name for this property, shown in the UI /// The user-friendly name for this property, shown in the UI
/// </summary> /// </summary>

View File

@ -8,12 +8,17 @@ namespace Artemis.Core
public class PropertyGroupDescriptionAttribute : Attribute public class PropertyGroupDescriptionAttribute : Attribute
{ {
/// <summary> /// <summary>
/// The user-friendly name for this property, shown in the UI. /// The identifier of this property group used for storage, if not set one will be generated based on the group name in code
/// </summary> /// </summary>
public string? Name { get; set; } public string? Identifier { get; set; }
/// <summary> /// <summary>
/// The user-friendly description for this property, shown in the UI. /// The user-friendly name for this property group, shown in the UI.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// The user-friendly description for this property group, shown in the UI.
/// </summary> /// </summary>
public string? Description { get; set; } public string? Description { get; set; }
} }

View File

@ -16,6 +16,11 @@ namespace Artemis.Core
/// Gets the description attribute applied to this property /// Gets the description attribute applied to this property
/// </summary> /// </summary>
PropertyDescriptionAttribute PropertyDescription { get; } PropertyDescriptionAttribute PropertyDescription { get; }
/// <summary>
/// Gets the profile element (such as layer or folder) this property is applied to
/// </summary>
RenderProfileElement ProfileElement { get; }
/// <summary> /// <summary>
/// The parent group of this layer property, set after construction /// The parent group of this layer property, set after construction
@ -43,7 +48,7 @@ namespace Artemis.Core
public bool DataBindingsSupported { get; } public bool DataBindingsSupported { get; }
/// <summary> /// <summary>
/// Gets the unique path of the property on the layer /// Gets the unique path of the property on the render element
/// </summary> /// </summary>
string Path { get; } string Path { get; }
@ -56,7 +61,7 @@ namespace Artemis.Core
/// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied /// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied
/// </summary> /// </summary>
bool IsLoadedFromStorage { get; } bool IsLoadedFromStorage { get; }
/// <summary> /// <summary>
/// Initializes the layer property /// Initializes the layer property
/// <para> /// <para>
@ -64,7 +69,7 @@ namespace Artemis.Core
/// <see cref="LayerProperty{T}" /> /// <see cref="LayerProperty{T}" />
/// </para> /// </para>
/// </summary> /// </summary>
void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description, string path); void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description);
/// <summary> /// <summary>
/// Attempts to load and add the provided keyframe entity to the layer property /// Attempts to load and add the provided keyframe entity to the layer property

View File

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Text;
using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -27,10 +28,10 @@ namespace Artemis.Core
// These are set right after construction to keep the constructor (and inherited constructs) clean // These are set right after construction to keep the constructor (and inherited constructs) clean
ProfileElement = null!; ProfileElement = null!;
LayerPropertyGroup = null!; LayerPropertyGroup = null!;
Path = null!;
Entity = null!; Entity = null!;
PropertyDescription = null!; PropertyDescription = null!;
DataBinding = null!; DataBinding = null!;
Path = "";
CurrentValue = default!; CurrentValue = default!;
DefaultValue = default!; DefaultValue = default!;
@ -117,13 +118,11 @@ namespace Artemis.Core
} }
} }
/// <summary> /// <inheritdoc />
/// Gets the profile element (such as layer or folder) this property is applied to public RenderProfileElement ProfileElement { get; private set; }
/// </summary>
public RenderProfileElement ProfileElement { get; internal set; }
/// <inheritdoc /> /// <inheritdoc />
public LayerPropertyGroup LayerPropertyGroup { get; internal set; } public LayerPropertyGroup LayerPropertyGroup { get; private set; }
#endregion #endregion
@ -457,16 +456,18 @@ namespace Artemis.Core
internal PropertyEntity Entity { get; set; } internal PropertyEntity Entity { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description, string path) public void Initialize(RenderProfileElement profileElement, LayerPropertyGroup group, PropertyEntity entity, bool fromStorage, PropertyDescriptionAttribute description)
{ {
if (_disposed) if (_disposed)
throw new ObjectDisposedException("LayerProperty"); throw new ObjectDisposedException("LayerProperty");
if (description.Identifier == null)
throw new ArtemisCoreException("Can't initialize a property group without an identifier");
_isInitialized = true; _isInitialized = true;
ProfileElement = profileElement ?? throw new ArgumentNullException(nameof(profileElement)); ProfileElement = profileElement ?? throw new ArgumentNullException(nameof(profileElement));
LayerPropertyGroup = group ?? throw new ArgumentNullException(nameof(group)); LayerPropertyGroup = group ?? throw new ArgumentNullException(nameof(group));
Path = path;
Entity = entity ?? throw new ArgumentNullException(nameof(entity)); Entity = entity ?? throw new ArgumentNullException(nameof(entity));
PropertyDescription = description ?? throw new ArgumentNullException(nameof(description)); PropertyDescription = description ?? throw new ArgumentNullException(nameof(description));
IsLoadedFromStorage = fromStorage; IsLoadedFromStorage = fromStorage;
@ -475,6 +476,9 @@ namespace Artemis.Core
if (PropertyDescription.DisableKeyframes) if (PropertyDescription.DisableKeyframes)
KeyframesSupported = false; KeyframesSupported = false;
// Create the path to this property by walking up the tree
Path = LayerPropertyGroup.Path + "." + description.Identifier;
OnInitialize(); OnInitialize();
} }

View File

@ -31,9 +31,7 @@ namespace Artemis.Core
{ {
// These are set right after construction to keep the constructor (and inherited constructs) clean // These are set right after construction to keep the constructor (and inherited constructs) clean
GroupDescription = null!; GroupDescription = null!;
Feature = null!; Path = "";
ProfileElement = null!;
Path = null!;
_layerProperties = new List<ILayerProperty>(); _layerProperties = new List<ILayerProperty>();
_layerPropertyGroups = new List<LayerPropertyGroup>(); _layerPropertyGroups = new List<LayerPropertyGroup>();
@ -42,47 +40,32 @@ namespace Artemis.Core
LayerPropertyGroups = new ReadOnlyCollection<LayerPropertyGroup>(_layerPropertyGroups); LayerPropertyGroups = new ReadOnlyCollection<LayerPropertyGroup>(_layerPropertyGroups);
} }
/// <summary>
/// Gets the description of this group
/// </summary>
public PropertyGroupDescriptionAttribute GroupDescription { get; internal set; }
/// <summary>
/// Gets the plugin feature this group is associated with
/// </summary>
public PluginFeature Feature { get; set; }
/// <summary> /// <summary>
/// Gets the profile element (such as layer or folder) this group is associated with /// Gets the profile element (such as layer or folder) this group is associated with
/// </summary> /// </summary>
public RenderProfileElement ProfileElement { get; internal set; } public RenderProfileElement ProfileElement { get; private set; }
/// <summary>
/// Gets the description of this group
/// </summary>
public PropertyGroupDescriptionAttribute GroupDescription { get; private set; }
/// <summary> /// <summary>
/// The parent group of this group /// The parent group of this group
/// </summary> /// </summary>
[LayerPropertyIgnore] [LayerPropertyIgnore] // Ignore the parent when selecting child groups
public LayerPropertyGroup? Parent { get; internal set; } public LayerPropertyGroup? Parent { get; internal set; }
/// <summary> /// <summary>
/// The path of this property group /// Gets the unique path of the property on the render element
/// </summary> /// </summary>
public string Path { get; internal set; } public string Path { get; private set; }
/// <summary> /// <summary>
/// Gets whether this property groups properties are all initialized /// Gets whether this property groups properties are all initialized
/// </summary> /// </summary>
public bool PropertiesInitialized { get; private set; } public bool PropertiesInitialized { get; private set; }
/// <summary>
/// The layer brush this property group belongs to
/// </summary>
public BaseLayerBrush? LayerBrush { get; internal set; }
/// <summary>
/// The layer effect this property group belongs to
/// </summary>
public BaseLayerEffect? LayerEffect { get; internal set; }
/// <summary> /// <summary>
/// Gets or sets whether the property is hidden in the UI /// Gets or sets whether the property is hidden in the UI
/// </summary> /// </summary>
@ -96,6 +79,11 @@ namespace Artemis.Core
} }
} }
/// <summary>
/// Gets the entity this property group uses for persistent storage
/// </summary>
public PropertyGroupEntity? PropertyGroupEntity { get; internal set; }
/// <summary> /// <summary>
/// A list of all layer properties in this group /// A list of all layer properties in this group
/// </summary> /// </summary>
@ -194,17 +182,20 @@ namespace Artemis.Core
} }
} }
internal void Initialize(RenderProfileElement profileElement, string path, PluginFeature feature) internal void Initialize(RenderProfileElement profileElement, LayerPropertyGroup? parent, PropertyGroupDescriptionAttribute groupDescription, PropertyGroupEntity? propertyGroupEntity)
{ {
if (path == null) throw new ArgumentNullException(nameof(path)); if (groupDescription.Identifier == null)
throw new ArtemisCoreException("Can't initialize a property group without an identifier");
// 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");
Feature = feature ?? throw new ArgumentNullException(nameof(feature)); ProfileElement = profileElement;
ProfileElement = profileElement ?? throw new ArgumentNullException(nameof(profileElement)); Parent = parent;
Path = path.TrimEnd('.'); GroupDescription = groupDescription;
PropertyGroupEntity = propertyGroupEntity ?? new PropertyGroupEntity {Identifier = groupDescription.Identifier};
Path = parent != null ? parent.Path + "." + groupDescription.Identifier : groupDescription.Identifier;
// Get all properties implementing ILayerProperty or LayerPropertyGroup // Get all properties implementing ILayerProperty or LayerPropertyGroup
foreach (PropertyInfo propertyInfo in GetType().GetProperties()) foreach (PropertyInfo propertyInfo in GetType().GetProperties())
@ -271,61 +262,68 @@ namespace Artemis.Core
private void InitializeProperty(PropertyInfo propertyInfo, PropertyDescriptionAttribute propertyDescription) private void InitializeProperty(PropertyInfo propertyInfo, PropertyDescriptionAttribute propertyDescription)
{ {
string path = $"{Path}.{propertyInfo.Name}"; // Ensure the description has an identifier and name, if not this is a good point to set it based on the property info
if (string.IsNullOrWhiteSpace(propertyDescription.Identifier))
if (!typeof(ILayerProperty).IsAssignableFrom(propertyInfo.PropertyType)) propertyDescription.Identifier = propertyInfo.Name;
throw new ArtemisPluginException($"Layer property with PropertyDescription attribute must be of type LayerProperty at {path}");
if (!(Activator.CreateInstance(propertyInfo.PropertyType, true) is ILayerProperty instance))
throw new ArtemisPluginException($"Failed to create instance of layer property at {path}");
// Ensure the description has a name, if not this is a good point to set it based on the property info
if (string.IsNullOrWhiteSpace(propertyDescription.Name)) if (string.IsNullOrWhiteSpace(propertyDescription.Name))
propertyDescription.Name = propertyInfo.Name.Humanize(); propertyDescription.Name = propertyInfo.Name.Humanize();
PropertyEntity entity = GetPropertyEntity(ProfileElement, path, out bool fromStorage); if (!typeof(ILayerProperty).IsAssignableFrom(propertyInfo.PropertyType))
instance.Initialize(ProfileElement, this, entity, fromStorage, propertyDescription, path); throw new ArtemisPluginException($"Property with PropertyDescription attribute must be of type ILayerProperty: {propertyDescription.Identifier}");
if (Activator.CreateInstance(propertyInfo.PropertyType, true) is not ILayerProperty instance)
throw new ArtemisPluginException($"Failed to create instance of layer property: {propertyDescription.Identifier}");
PropertyEntity entity = GetPropertyEntity(propertyDescription.Identifier, out bool fromStorage);
instance.Initialize(ProfileElement, this, entity, fromStorage, propertyDescription);
propertyInfo.SetValue(this, instance); propertyInfo.SetValue(this, instance);
_layerProperties.Add(instance); _layerProperties.Add(instance);
} }
private void InitializeChildGroup(PropertyInfo propertyInfo, PropertyGroupDescriptionAttribute propertyGroupDescription) private void InitializeChildGroup(PropertyInfo propertyInfo, PropertyGroupDescriptionAttribute propertyGroupDescription)
{ {
string path = Path + "."; // Ensure the description has an identifier and name name, if not this is a good point to set it based on the property info
if (string.IsNullOrWhiteSpace(propertyGroupDescription.Identifier))
if (!typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType)) propertyGroupDescription.Identifier = propertyInfo.Name;
throw new ArtemisPluginException("Layer property with PropertyGroupDescription attribute must be of type LayerPropertyGroup");
if (!(Activator.CreateInstance(propertyInfo.PropertyType) is LayerPropertyGroup instance))
throw new ArtemisPluginException($"Failed to create instance of layer property group at {path + propertyInfo.Name}");
// Ensure the description has a name, if not this is a good point to set it based on the property info
if (string.IsNullOrWhiteSpace(propertyGroupDescription.Name)) if (string.IsNullOrWhiteSpace(propertyGroupDescription.Name))
propertyGroupDescription.Name = propertyInfo.Name.Humanize(); propertyGroupDescription.Name = propertyInfo.Name.Humanize();
instance.Parent = this; if (!typeof(LayerPropertyGroup).IsAssignableFrom(propertyInfo.PropertyType))
instance.GroupDescription = propertyGroupDescription; throw new ArtemisPluginException($"Property with PropertyGroupDescription attribute must be of type LayerPropertyGroup: {propertyGroupDescription.Identifier}");
instance.LayerBrush = LayerBrush; if (!(Activator.CreateInstance(propertyInfo.PropertyType) is LayerPropertyGroup instance))
instance.LayerEffect = LayerEffect; throw new ArtemisPluginException($"Failed to create instance of layer property group: {propertyGroupDescription.Identifier}");
instance.Initialize(ProfileElement, $"{path}{propertyInfo.Name}.", Feature);
PropertyGroupEntity entity = GetPropertyGroupEntity(propertyGroupDescription.Identifier);
instance.Initialize(ProfileElement, this, propertyGroupDescription, entity);
propertyInfo.SetValue(this, instance); propertyInfo.SetValue(this, instance);
_layerPropertyGroups.Add(instance); _layerPropertyGroups.Add(instance);
} }
private PropertyEntity GetPropertyEntity(RenderProfileElement profileElement, string path, out bool fromStorage) private PropertyEntity GetPropertyEntity(string identifier, out bool fromStorage)
{ {
PropertyEntity? entity = profileElement.RenderElementEntity.PropertyEntities.FirstOrDefault(p => p.FeatureId == Feature.Id && p.Path == path); if (PropertyGroupEntity == null)
throw new ArtemisCoreException($"Can't execute {nameof(GetPropertyEntity)} without {nameof(PropertyGroupEntity)} being setup");
PropertyEntity? entity = PropertyGroupEntity.Properties.FirstOrDefault(p => p.Identifier == identifier);
fromStorage = entity != null; fromStorage = entity != null;
if (entity == null) if (entity == null)
{ {
entity = new PropertyEntity {FeatureId = Feature.Id, Path = path}; entity = new PropertyEntity {Identifier = identifier};
profileElement.RenderElementEntity.PropertyEntities.Add(entity); PropertyGroupEntity.Properties.Add(entity);
} }
return entity; return entity;
} }
private PropertyGroupEntity GetPropertyGroupEntity(string identifier)
{
if (PropertyGroupEntity == null)
throw new ArtemisCoreException($"Can't execute {nameof(GetPropertyGroupEntity)} without {nameof(PropertyGroupEntity)} being setup");
return PropertyGroupEntity.PropertyGroups.FirstOrDefault(g => g.Identifier == identifier) ?? new PropertyGroupEntity() {Identifier = identifier};
}
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() public void Dispose()
{ {

View File

@ -97,20 +97,10 @@ namespace Artemis.Core
internal void SaveRenderElement() internal void SaveRenderElement()
{ {
RenderElementEntity.LayerEffects.Clear(); RenderElementEntity.LayerEffects.Clear();
foreach (BaseLayerEffect layerEffect in LayerEffects) foreach (BaseLayerEffect baseLayerEffect in LayerEffects)
{ {
LayerEffectEntity layerEffectEntity = new() baseLayerEffect.Save();
{ RenderElementEntity.LayerEffects.Add(baseLayerEffect.LayerEffectEntity);
Id = layerEffect.EntityId,
ProviderId = layerEffect.Descriptor?.PlaceholderFor ?? layerEffect.ProviderId,
EffectType = layerEffect.GetEffectTypeName(),
Name = layerEffect.Name,
Suspended = layerEffect.Suspended,
HasBeenRenamed = layerEffect.HasBeenRenamed,
Order = layerEffect.Order
};
RenderElementEntity.LayerEffects.Add(layerEffectEntity);
layerEffect.BaseProperties?.ApplyToEntity();
} }
// Condition // Condition
@ -310,7 +300,7 @@ namespace Artemis.Core
foreach (LayerEffectEntity layerEffectEntity in RenderElementEntity.LayerEffects) foreach (LayerEffectEntity layerEffectEntity in RenderElementEntity.LayerEffects)
{ {
// If there is a non-placeholder existing effect, skip this entity // If there is a non-placeholder existing effect, skip this entity
BaseLayerEffect? existing = LayerEffectsList.FirstOrDefault(e => e.EntityId == layerEffectEntity.Id); BaseLayerEffect? existing = LayerEffectsList.FirstOrDefault(e => e.LayerEffectEntity.Id == layerEffectEntity.Id);
if (existing != null && existing.Descriptor.PlaceholderFor == null) if (existing != null && existing.Descriptor.PlaceholderFor == null)
continue; continue;
@ -351,7 +341,7 @@ namespace Artemis.Core
List<BaseLayerEffect> pluginEffects = LayerEffectsList.Where(ef => ef.ProviderId == e.Registration.PluginFeature.Id).ToList(); List<BaseLayerEffect> pluginEffects = LayerEffectsList.Where(ef => ef.ProviderId == e.Registration.PluginFeature.Id).ToList();
foreach (BaseLayerEffect pluginEffect in pluginEffects) foreach (BaseLayerEffect pluginEffect in pluginEffects)
{ {
LayerEffectEntity entity = RenderElementEntity.LayerEffects.First(en => en.Id == pluginEffect.EntityId); LayerEffectEntity entity = RenderElementEntity.LayerEffects.First(en => en.Id == pluginEffect.LayerEffectEntity.Id);
LayerEffectsList.Remove(pluginEffect); LayerEffectsList.Remove(pluginEffect);
pluginEffect.Dispose(); pluginEffect.Dispose();

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Artemis.Storage.Entities.Profile;
using SkiaSharp; using SkiaSharp;
namespace Artemis.Core.LayerBrushes namespace Artemis.Core.LayerBrushes
@ -24,6 +25,7 @@ namespace Artemis.Core.LayerBrushes
// Both are set right after construction to keep the constructor of inherited classes clean // Both are set right after construction to keep the constructor of inherited classes clean
_layer = null!; _layer = null!;
_descriptor = null!; _descriptor = null!;
LayerBrushEntity = null!;
} }
/// <summary> /// <summary>
@ -35,6 +37,11 @@ namespace Artemis.Core.LayerBrushes
internal set => SetAndNotify(ref _layer, value); internal set => SetAndNotify(ref _layer, value);
} }
/// <summary>
/// Gets the brush entity this brush uses for persistent storage
/// </summary>
public LayerBrushEntity LayerBrushEntity { get; internal set; }
/// <summary> /// <summary>
/// Gets the descriptor of this brush /// Gets the descriptor of this brush
/// </summary> /// </summary>
@ -185,6 +192,13 @@ namespace Artemis.Core.LayerBrushes
public override string BrokenDisplayName => Descriptor.DisplayName; public override string BrokenDisplayName => Descriptor.DisplayName;
#endregion #endregion
internal void Save()
{
// No need to update the type or provider ID, they're set once by the LayerBrushDescriptors CreateInstance and can't change
BaseProperties?.ApplyToEntity();
LayerBrushEntity.PropertyGroup = BaseProperties?.PropertyGroupEntity;
}
} }
/// <summary> /// <summary>

View File

@ -1,4 +1,5 @@
using System; using System;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core.LayerBrushes namespace Artemis.Core.LayerBrushes
{ {
@ -32,12 +33,11 @@ namespace Artemis.Core.LayerBrushes
internal set => _properties = value; internal set => _properties = value;
} }
internal void InitializeProperties() internal void InitializeProperties(PropertyGroupEntity? propertyGroupEntity)
{ {
Properties = Activator.CreateInstance<T>(); Properties = Activator.CreateInstance<T>();
Properties.GroupDescription = new PropertyGroupDescriptionAttribute {Name = Descriptor.DisplayName, Description = Descriptor.Description}; PropertyGroupDescriptionAttribute groupDescription = new() {Identifier = "Brush", Name = Descriptor.DisplayName, Description = Descriptor.Description};
Properties.LayerBrush = this; Properties.Initialize(Layer, null, groupDescription, propertyGroupEntity);
Properties.Initialize(Layer, "LayerBrush.", Descriptor.Provider);
PropertiesInitialized = true; PropertiesInitialized = true;
EnableLayerBrush(); EnableLayerBrush();

View File

@ -33,7 +33,7 @@ namespace Artemis.Core.LayerBrushes
internal override void Initialize() internal override void Initialize()
{ {
TryOrBreak(InitializeProperties, "Failed to initialize"); TryOrBreak(() => InitializeProperties(Layer.LayerEntity.LayerBrush?.PropertyGroup), "Failed to initialize");
} }
} }
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using Artemis.Storage.Entities.Profile;
using Ninject; using Ninject;
namespace Artemis.Core.LayerBrushes namespace Artemis.Core.LayerBrushes
@ -57,23 +58,20 @@ namespace Artemis.Core.LayerBrushes
/// <summary> /// <summary>
/// Creates an instance of the described brush and applies it to the layer /// Creates an instance of the described brush and applies it to the layer
/// </summary> /// </summary>
internal void CreateInstance(Layer layer) public BaseLayerBrush CreateInstance(Layer layer, LayerBrushEntity? entity)
{ {
if (layer == null) throw new ArgumentNullException(nameof(layer)); if (layer == null)
if (layer.LayerBrush != null) throw new ArgumentNullException(nameof(layer));
throw new ArtemisCoreException("Layer already has an instantiated layer brush");
BaseLayerBrush brush = (BaseLayerBrush) Provider.Plugin.Kernel!.Get(LayerBrushType); BaseLayerBrush brush = (BaseLayerBrush) Provider.Plugin.Kernel!.Get(LayerBrushType);
brush.Layer = layer; brush.Layer = layer;
brush.Descriptor = this; brush.Descriptor = this;
brush.LayerBrushEntity = entity ?? new LayerBrushEntity { ProviderId = Provider.Id, BrushType = LayerBrushType.FullName };
brush.Initialize(); brush.Initialize();
brush.Update(0); brush.Update(0);
layer.LayerBrush = brush; return brush;
layer.OnLayerBrushUpdated();
if (layer.ShouldBeEnabled)
brush.InternalEnable();
} }
} }
} }

View File

@ -77,7 +77,7 @@ namespace Artemis.Core.LayerBrushes
internal override void Initialize() internal override void Initialize()
{ {
TryOrBreak(InitializeProperties, "Failed to initialize"); TryOrBreak(() => InitializeProperties(Layer.LayerEntity.LayerBrush?.PropertyGroup), "Failed to initialize");
} }
} }
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using Artemis.Storage.Entities.Profile;
using SkiaSharp; using SkiaSharp;
namespace Artemis.Core.LayerEffects namespace Artemis.Core.LayerEffects
@ -24,16 +25,13 @@ namespace Artemis.Core.LayerEffects
_profileElement = null!; _profileElement = null!;
_descriptor = null!; _descriptor = null!;
_name = null!; _name = null!;
LayerEffectEntity = null!;
} }
/// <summary> /// <summary>
/// Gets the unique ID of this effect /// Gets the
/// </summary> /// </summary>
public Guid EntityId public LayerEffectEntity LayerEffectEntity { get; internal set; }
{
get => _entityId;
internal set => SetAndNotify(ref _entityId, value);
}
/// <summary> /// <summary>
/// Gets the profile element (such as layer or folder) this effect is applied to /// Gets the profile element (such as layer or folder) this effect is applied to
@ -109,8 +107,6 @@ namespace Artemis.Core.LayerEffects
/// </summary> /// </summary>
public virtual LayerPropertyGroup? BaseProperties => null; public virtual LayerPropertyGroup? BaseProperties => null;
internal string PropertyRootPath => $"LayerEffect.{EntityId}.{GetType().Name}.";
/// <summary> /// <summary>
/// Gets a boolean indicating whether the layer effect is enabled or not /// Gets a boolean indicating whether the layer effect is enabled or not
/// </summary> /// </summary>
@ -227,5 +223,17 @@ namespace Artemis.Core.LayerEffects
public override string BrokenDisplayName => Name; public override string BrokenDisplayName => Name;
#endregion #endregion
public void Save()
{
// No need to update the ID, type and provider ID. They're set once by the LayerBrushDescriptors CreateInstance and can't change
LayerEffectEntity.Name = Name;
LayerEffectEntity.Suspended = Suspended;
LayerEffectEntity.HasBeenRenamed = HasBeenRenamed;
LayerEffectEntity.Order = Order;
BaseProperties?.ApplyToEntity();
LayerEffectEntity.PropertyGroup = BaseProperties?.PropertyGroupEntity;
}
} }
} }

View File

@ -36,8 +36,7 @@ namespace Artemis.Core.LayerEffects
internal void InitializeProperties() internal void InitializeProperties()
{ {
Properties = Activator.CreateInstance<T>(); Properties = Activator.CreateInstance<T>();
Properties.LayerEffect = this; Properties.Initialize(ProfileElement, null, new PropertyGroupDescriptionAttribute(){Identifier = "LayerEffect"}, LayerEffectEntity.PropertyGroup);
Properties.Initialize(ProfileElement, PropertyRootPath, Descriptor.Provider);
PropertiesInitialized = true; PropertiesInitialized = true;
EnableLayerEffect(); EnableLayerEffect();

View File

@ -54,11 +54,28 @@ namespace Artemis.Core.LayerEffects
/// <summary> /// <summary>
/// Creates an instance of the described effect and applies it to the render element /// Creates an instance of the described effect and applies it to the render element
/// </summary> /// </summary>
internal void CreateInstance(RenderProfileElement renderElement, LayerEffectEntity entity) internal void CreateInstance(RenderProfileElement renderElement, LayerEffectEntity? entity)
{ {
// Skip effects already on the element if (LayerEffectType == null)
if (renderElement.LayerEffects.Any(e => e.EntityId == entity.Id)) throw new ArtemisCoreException("Cannot create an instance of a layer effect because this descriptor is not a placeholder but is still missing its LayerEffectType");
return;
if (entity == null)
{
entity = new LayerEffectEntity
{
Id = Guid.NewGuid(),
Suspended = false,
Order = renderElement.LayerEffects.Count + 1,
ProviderId = Provider.Id,
EffectType = LayerEffectType.FullName
};
}
else
{
// Skip effects already on the element
if (renderElement.LayerEffects.Any(e => e.LayerEffectEntity.Id == entity.Id))
return;
}
if (PlaceholderFor != null) if (PlaceholderFor != null)
{ {
@ -66,12 +83,9 @@ namespace Artemis.Core.LayerEffects
return; return;
} }
if (LayerEffectType == null)
throw new ArtemisCoreException("Cannot create an instance of a layer effect because this descriptor is not a placeholder but is still missing its LayerEffectType");
BaseLayerEffect effect = (BaseLayerEffect) Provider.Plugin.Kernel!.Get(LayerEffectType); BaseLayerEffect effect = (BaseLayerEffect) Provider.Plugin.Kernel!.Get(LayerEffectType);
effect.ProfileElement = renderElement; effect.ProfileElement = renderElement;
effect.EntityId = entity.Id; effect.LayerEffectEntity = entity;
effect.Order = entity.Order; effect.Order = entity.Order;
effect.Name = entity.Name; effect.Name = entity.Name;
effect.Suspended = entity.Suspended; effect.Suspended = entity.Suspended;

View File

@ -13,7 +13,7 @@ namespace Artemis.Core.LayerEffects.Placeholder
OriginalEntity = originalEntity; OriginalEntity = originalEntity;
PlaceholderFor = placeholderFor; PlaceholderFor = placeholderFor;
EntityId = OriginalEntity.Id; LayerEffectEntity = originalEntity;
Order = OriginalEntity.Order; Order = OriginalEntity.Order;
Name = OriginalEntity.Name; Name = OriginalEntity.Name;
Suspended = OriginalEntity.Suspended; Suspended = OriginalEntity.Suspended;

View File

@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace Artemis.Storage.Entities.Profile;
public class LayerBrushEntity
{
public string ProviderId { get; set; }
public string BrushType { get; set; }
public PropertyGroupEntity PropertyGroup { get; set; }
}

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
namespace Artemis.Storage.Entities.Profile namespace Artemis.Storage.Entities.Profile
{ {
@ -11,5 +12,7 @@ namespace Artemis.Storage.Entities.Profile
public bool Suspended { get; set; } public bool Suspended { get; set; }
public bool HasBeenRenamed { get; set; } public bool HasBeenRenamed { get; set; }
public int Order { get; set; } public int Order { get; set; }
public PropertyGroupEntity PropertyGroup { get; set; }
} }
} }

View File

@ -24,9 +24,12 @@ namespace Artemis.Storage.Entities.Profile
public List<LedEntity> Leds { get; set; } public List<LedEntity> Leds { get; set; }
public List<IAdaptionHintEntity> AdaptionHints { get; set; } public List<IAdaptionHintEntity> AdaptionHints { get; set; }
public PropertyGroupEntity GeneralPropertyGroup { get; set; }
public PropertyGroupEntity TransformPropertyGroup { get; set; }
public LayerBrushEntity LayerBrush { get; set; }
[BsonRef("ProfileEntity")] [BsonRef("ProfileEntity")]
public ProfileEntity Profile { get; set; } public ProfileEntity Profile { get; set; }
public Guid ProfileId { get; set; } public Guid ProfileId { get; set; }
} }
} }

View File

@ -1,22 +1,14 @@
using System.Collections.Generic; using System.Collections.Generic;
using Artemis.Storage.Entities.Profile.DataBindings; using Artemis.Storage.Entities.Profile.DataBindings;
namespace Artemis.Storage.Entities.Profile namespace Artemis.Storage.Entities.Profile;
public class PropertyEntity
{ {
public class PropertyEntity public string Identifier { get; set; }
{ public string Value { get; set; }
public PropertyEntity() public bool KeyframesEnabled { get; set; }
{
KeyframeEntities = new List<KeyframeEntity>();
}
public string FeatureId { get; set; } public DataBindingEntity DataBinding { get; set; }
public string Path { get; set; } public List<KeyframeEntity> KeyframeEntities { get; set; } = new();
public DataBindingEntity DataBinding { get; set; }
public string Value { get; set; }
public bool KeyframesEnabled { get; set; }
public List<KeyframeEntity> KeyframeEntities { get; set; }
}
} }

View File

@ -0,0 +1,10 @@
using System.Collections.Generic;
namespace Artemis.Storage.Entities.Profile;
public class PropertyGroupEntity
{
public string Identifier { get; set; }
public List<PropertyEntity> Properties { get; set; } = new();
public List<PropertyGroupEntity> PropertyGroups { get; set; } = new();
}

View File

@ -49,13 +49,13 @@ namespace Artemis.UI.DefaultTypes.PropertyInput
{ {
if (LayerProperty.ProfileElement is Layer layer) if (LayerProperty.ProfileElement is Layer layer)
{ {
layer.ChangeLayerBrush(SelectedDescriptor); // layer.ChangeLayerBrush(SelectedDescriptor);
if (layer.LayerBrush?.Presets != null && layer.LayerBrush.Presets.Any()) // if (layer.LayerBrush?.Presets != null && layer.LayerBrush.Presets.Any())
Execute.PostToUIThread(async () => // Execute.PostToUIThread(async () =>
{ // {
await Task.Delay(400); // await Task.Delay(400);
await _dialogService.ShowDialogAt<LayerBrushPresetViewModel>("LayerProperties", new Dictionary<string, object> {{"layerBrush", layer.LayerBrush}}); // await _dialogService.ShowDialogAt<LayerBrushPresetViewModel>("LayerProperties", new Dictionary<string, object> {{"layerBrush", layer.LayerBrush}});
}); // });
} }
} }

View File

@ -332,19 +332,19 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties
if (SelectedProfileElement == null) if (SelectedProfileElement == null)
return; return;
// Remove VMs of effects no longer applied on the layer // // Remove VMs of effects no longer applied on the layer
List<LayerPropertyGroupViewModel> toRemove = Items // List<LayerPropertyGroupViewModel> toRemove = Items
.Where(l => l.LayerPropertyGroup.LayerEffect != null && !SelectedProfileElement.LayerEffects.Contains(l.LayerPropertyGroup.LayerEffect)) // .Where(l => l.LayerPropertyGroup.LayerEffect != null && !SelectedProfileElement.LayerEffects.Contains(l.LayerPropertyGroup.LayerEffect))
.ToList(); // .ToList();
Items.RemoveRange(toRemove); // Items.RemoveRange(toRemove);
//
foreach (BaseLayerEffect layerEffect in SelectedProfileElement.LayerEffects) // foreach (BaseLayerEffect layerEffect in SelectedProfileElement.LayerEffects)
{ // {
if (Items.Any(l => l.LayerPropertyGroup.LayerEffect == layerEffect) || layerEffect.BaseProperties == null) // if (Items.Any(l => l.LayerPropertyGroup.LayerEffect == layerEffect) || layerEffect.BaseProperties == null)
continue; // continue;
//
Items.Add(_layerPropertyVmFactory.LayerPropertyGroupViewModel(layerEffect.BaseProperties)); // Items.Add(_layerPropertyVmFactory.LayerPropertyGroupViewModel(layerEffect.BaseProperties));
} // }
SortProperties(); SortProperties();
} }
@ -355,11 +355,11 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties
List<LayerPropertyGroupViewModel> nonEffectProperties = Items List<LayerPropertyGroupViewModel> nonEffectProperties = Items
.Where(l => l.TreeGroupViewModel.GroupType != LayerEffectRoot) .Where(l => l.TreeGroupViewModel.GroupType != LayerEffectRoot)
.ToList(); .ToList();
// Order the effects // // Order the effects
List<LayerPropertyGroupViewModel> effectProperties = Items // List<LayerPropertyGroupViewModel> effectProperties = Items
.Where(l => l.TreeGroupViewModel.GroupType == LayerEffectRoot && l.LayerPropertyGroup.LayerEffect != null) // .Where(l => l.TreeGroupViewModel.GroupType == LayerEffectRoot && l.LayerPropertyGroup.LayerEffect != null)
.OrderBy(l => l.LayerPropertyGroup.LayerEffect.Order) // .OrderBy(l => l.LayerPropertyGroup.LayerEffect.Order)
.ToList(); // .ToList();
// Put the non-effect properties in front // Put the non-effect properties in front
for (int index = 0; index < nonEffectProperties.Count; index++) for (int index = 0; index < nonEffectProperties.Count; index++)
@ -369,13 +369,13 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties
((BindableCollection<LayerPropertyGroupViewModel>) Items).Move(Items.IndexOf(layerPropertyGroupViewModel), index); ((BindableCollection<LayerPropertyGroupViewModel>) Items).Move(Items.IndexOf(layerPropertyGroupViewModel), index);
} }
// Put the effect properties after, sorted by their order // // Put the effect properties after, sorted by their order
for (int index = 0; index < effectProperties.Count; index++) // for (int index = 0; index < effectProperties.Count; index++)
{ // {
LayerPropertyGroupViewModel layerPropertyGroupViewModel = effectProperties[index]; // LayerPropertyGroupViewModel layerPropertyGroupViewModel = effectProperties[index];
if (Items.IndexOf(layerPropertyGroupViewModel) != index + nonEffectProperties.Count) // if (Items.IndexOf(layerPropertyGroupViewModel) != index + nonEffectProperties.Count)
((BindableCollection<LayerPropertyGroupViewModel>) Items).Move(Items.IndexOf(layerPropertyGroupViewModel), index + nonEffectProperties.Count); // ((BindableCollection<LayerPropertyGroupViewModel>) Items).Move(Items.IndexOf(layerPropertyGroupViewModel), index + nonEffectProperties.Count);
} // }
} }
public async void ToggleEffectsViewModel() public async void ToggleEffectsViewModel()

View File

@ -87,8 +87,8 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties
public void UpdateOrder(int order) public void UpdateOrder(int order)
{ {
if (LayerPropertyGroup.LayerEffect != null) // if (LayerPropertyGroup.LayerEffect != null)
LayerPropertyGroup.LayerEffect.Order = order; // LayerPropertyGroup.LayerEffect.Order = order;
NotifyOfPropertyChange(nameof(IsExpanded)); NotifyOfPropertyChange(nameof(IsExpanded));
} }

View File

@ -59,7 +59,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.Models
public KeyframeClipboardModel(ILayerPropertyKeyframe layerPropertyKeyframe) public KeyframeClipboardModel(ILayerPropertyKeyframe layerPropertyKeyframe)
{ {
FeatureId = layerPropertyKeyframe.UntypedLayerProperty.LayerPropertyGroup.Feature.Id; // FeatureId = layerPropertyKeyframe.UntypedLayerProperty.LayerPropertyGroup.Feature.Id;
Path = layerPropertyKeyframe.UntypedLayerProperty.Path; Path = layerPropertyKeyframe.UntypedLayerProperty.Path;
KeyframeEntity = layerPropertyKeyframe.GetKeyframeEntity(); KeyframeEntity = layerPropertyKeyframe.GetKeyframeEntity();
} }
@ -70,15 +70,15 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Timeline.Models
public ILayerPropertyKeyframe Paste(List<ILayerProperty> properties, TimeSpan offset) public ILayerPropertyKeyframe Paste(List<ILayerProperty> properties, TimeSpan offset)
{ {
ILayerProperty property = properties.FirstOrDefault(p => p.LayerPropertyGroup.Feature.Id == FeatureId && p.Path == Path); // ILayerProperty property = properties.FirstOrDefault(p => p.LayerPropertyGroup.Feature.Id == FeatureId && p.Path == Path);
if (property != null) // if (property != null)
{ // {
KeyframeEntity.Position += offset; // KeyframeEntity.Position += offset;
ILayerPropertyKeyframe keyframe = property.AddKeyframeEntity(KeyframeEntity); // ILayerPropertyKeyframe keyframe = property.AddKeyframeEntity(KeyframeEntity);
KeyframeEntity.Position -= offset; // KeyframeEntity.Position -= offset;
//
return keyframe; // return keyframe;
} // }
return null; return null;
} }

View File

@ -56,94 +56,94 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree
? LayerPropertyGroupViewModel.Items ? LayerPropertyGroupViewModel.Items
: null; : null;
public void OpenBrushSettings() // public void OpenBrushSettings()
{ // {
BaseLayerBrush layerBrush = LayerPropertyGroup.LayerBrush; // BaseLayerBrush layerBrush = LayerPropertyGroup.LayerBrush;
LayerBrushConfigurationDialog configurationViewModel = (LayerBrushConfigurationDialog) layerBrush.ConfigurationDialog; // LayerBrushConfigurationDialog configurationViewModel = (LayerBrushConfigurationDialog) layerBrush.ConfigurationDialog;
if (configurationViewModel == null) // if (configurationViewModel == null)
return; // return;
//
// try
// {
// // Limit to one constructor, there's no need to have more and it complicates things anyway
// ConstructorInfo[] constructors = configurationViewModel.Type.GetConstructors();
// if (constructors.Length != 1)
// throw new ArtemisUIException("Brush configuration dialogs must have exactly one constructor");
//
// // Find the BaseLayerBrush parameter, it is required by the base constructor so its there for sure
// ParameterInfo brushParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerBrush).IsAssignableFrom(p.ParameterType));
// ConstructorArgument argument = new(brushParameter.Name, layerBrush);
// BrushConfigurationViewModel viewModel = (BrushConfigurationViewModel) layerBrush.Descriptor.Provider.Plugin.Kernel.Get(configurationViewModel.Type, argument);
//
// _layerBrushSettingsWindowVm = new LayerBrushSettingsWindowViewModel(viewModel, configurationViewModel);
// _windowManager.ShowDialog(_layerBrushSettingsWindowVm);
//
// // Save changes after the dialog closes
// _profileEditorService.SaveSelectedProfileConfiguration();
// }
// catch (Exception e)
// {
// _dialogService.ShowExceptionDialog("An exception occured while trying to show the brush's settings window", e);
// }
// }
try // public void OpenEffectSettings()
{ // {
// Limit to one constructor, there's no need to have more and it complicates things anyway // BaseLayerEffect layerEffect = LayerPropertyGroup.LayerEffect;
ConstructorInfo[] constructors = configurationViewModel.Type.GetConstructors(); // LayerEffectConfigurationDialog configurationViewModel = (LayerEffectConfigurationDialog) layerEffect.ConfigurationDialog;
if (constructors.Length != 1) // if (configurationViewModel == null)
throw new ArtemisUIException("Brush configuration dialogs must have exactly one constructor"); // return;
//
// try
// {
// // Limit to one constructor, there's no need to have more and it complicates things anyway
// ConstructorInfo[] constructors = configurationViewModel.Type.GetConstructors();
// if (constructors.Length != 1)
// throw new ArtemisUIException("Effect configuration dialogs must have exactly one constructor");
//
// ParameterInfo effectParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerEffect).IsAssignableFrom(p.ParameterType));
// ConstructorArgument argument = new(effectParameter.Name, layerEffect);
// EffectConfigurationViewModel viewModel = (EffectConfigurationViewModel) layerEffect.Descriptor.Provider.Plugin.Kernel.Get(configurationViewModel.Type, argument);
//
// _layerEffectSettingsWindowVm = new LayerEffectSettingsWindowViewModel(viewModel, configurationViewModel);
// _windowManager.ShowDialog(_layerEffectSettingsWindowVm);
//
// // Save changes after the dialog closes
// _profileEditorService.SaveSelectedProfileConfiguration();
// }
// catch (Exception e)
// {
// _dialogService.ShowExceptionDialog("An exception occured while trying to show the effect's settings window", e);
// throw;
// }
// }
// Find the BaseLayerBrush parameter, it is required by the base constructor so its there for sure // public async void RenameEffect()
ParameterInfo brushParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerBrush).IsAssignableFrom(p.ParameterType)); // {
ConstructorArgument argument = new(brushParameter.Name, layerBrush); // object result = await _dialogService.ShowDialogAt<RenameViewModel>(
BrushConfigurationViewModel viewModel = (BrushConfigurationViewModel) layerBrush.Descriptor.Provider.Plugin.Kernel.Get(configurationViewModel.Type, argument); // "PropertyTreeDialogHost",
// new Dictionary<string, object>
_layerBrushSettingsWindowVm = new LayerBrushSettingsWindowViewModel(viewModel, configurationViewModel); // {
_windowManager.ShowDialog(_layerBrushSettingsWindowVm); // {"subject", "effect"},
// {"currentName", LayerPropertyGroup.LayerEffect.Name}
// Save changes after the dialog closes // }
_profileEditorService.SaveSelectedProfileConfiguration(); // );
} // if (result is string newName)
catch (Exception e) // {
{ // LayerPropertyGroup.LayerEffect.Name = newName;
_dialogService.ShowExceptionDialog("An exception occured while trying to show the brush's settings window", e); // LayerPropertyGroup.LayerEffect.HasBeenRenamed = true;
} // _profileEditorService.SaveSelectedProfileConfiguration();
} // }
// }
public void OpenEffectSettings() //
{ // public void DeleteEffect()
BaseLayerEffect layerEffect = LayerPropertyGroup.LayerEffect; // {
LayerEffectConfigurationDialog configurationViewModel = (LayerEffectConfigurationDialog) layerEffect.ConfigurationDialog; // if (LayerPropertyGroup.LayerEffect == null)
if (configurationViewModel == null) // return;
return; //
// LayerPropertyGroup.ProfileElement.RemoveLayerEffect(LayerPropertyGroup.LayerEffect);
try // _profileEditorService.SaveSelectedProfileConfiguration();
{ // }
// Limit to one constructor, there's no need to have more and it complicates things anyway
ConstructorInfo[] constructors = configurationViewModel.Type.GetConstructors();
if (constructors.Length != 1)
throw new ArtemisUIException("Effect configuration dialogs must have exactly one constructor");
ParameterInfo effectParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerEffect).IsAssignableFrom(p.ParameterType));
ConstructorArgument argument = new(effectParameter.Name, layerEffect);
EffectConfigurationViewModel viewModel = (EffectConfigurationViewModel) layerEffect.Descriptor.Provider.Plugin.Kernel.Get(configurationViewModel.Type, argument);
_layerEffectSettingsWindowVm = new LayerEffectSettingsWindowViewModel(viewModel, configurationViewModel);
_windowManager.ShowDialog(_layerEffectSettingsWindowVm);
// Save changes after the dialog closes
_profileEditorService.SaveSelectedProfileConfiguration();
}
catch (Exception e)
{
_dialogService.ShowExceptionDialog("An exception occured while trying to show the effect's settings window", e);
throw;
}
}
public async void RenameEffect()
{
object result = await _dialogService.ShowDialogAt<RenameViewModel>(
"PropertyTreeDialogHost",
new Dictionary<string, object>
{
{"subject", "effect"},
{"currentName", LayerPropertyGroup.LayerEffect.Name}
}
);
if (result is string newName)
{
LayerPropertyGroup.LayerEffect.Name = newName;
LayerPropertyGroup.LayerEffect.HasBeenRenamed = true;
_profileEditorService.SaveSelectedProfileConfiguration();
}
}
public void DeleteEffect()
{
if (LayerPropertyGroup.LayerEffect == null)
return;
LayerPropertyGroup.ProfileElement.RemoveLayerEffect(LayerPropertyGroup.LayerEffect);
_profileEditorService.SaveSelectedProfileConfiguration();
}
public void SuspendedToggled() public void SuspendedToggled()
{ {
@ -175,10 +175,10 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree
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 && LayerPropertyGroup.LayerBrush != null) // else if (LayerPropertyGroup.Parent == null && LayerPropertyGroup.LayerBrush != null)
GroupType = LayerPropertyGroupType.LayerBrushRoot; // GroupType = LayerPropertyGroupType.LayerBrushRoot;
else if (LayerPropertyGroup.Parent == null && LayerPropertyGroup.LayerEffect != null) // else if (LayerPropertyGroup.Parent == null && LayerPropertyGroup.LayerEffect != null)
GroupType = LayerPropertyGroupType.LayerEffectRoot; // GroupType = LayerPropertyGroupType.LayerEffectRoot;
else else
GroupType = LayerPropertyGroupType.None; GroupType = LayerPropertyGroupType.None;
} }

View File

@ -151,8 +151,8 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem
ProfileElement.AddChild(layer, 0); ProfileElement.AddChild(layer, 0);
// Could be null if the default brush got disabled // Could be null if the default brush got disabled
LayerBrushDescriptor brush = _layerBrushService.GetDefaultLayerBrush(); LayerBrushDescriptor brush = _layerBrushService.GetDefaultLayerBrush();
if (brush != null) // if (brush != null)
layer.ChangeLayerBrush(brush); // layer.ChangeLayerBrush(brush);
layer.AddLeds(_rgbService.EnabledDevices.SelectMany(d => d.Leds)); layer.AddLeds(_rgbService.EnabledDevices.SelectMany(d => d.Leds));
_profileEditorService.SaveSelectedProfileConfiguration(); _profileEditorService.SaveSelectedProfileConfiguration();

View File

@ -93,8 +93,8 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization.Tools
folder.AddChild(newLayer); folder.AddChild(newLayer);
LayerBrushDescriptor brush = _layerBrushService.GetDefaultLayerBrush(); LayerBrushDescriptor brush = _layerBrushService.GetDefaultLayerBrush();
if (brush != null) // if (brush != null)
newLayer.ChangeLayerBrush(brush); // newLayer.ChangeLayerBrush(brush);
newLayer.AddLeds(selectedLeds); newLayer.AddLeds(selectedLeds);
ProfileEditorService.ChangeSelectedProfileElement(newLayer); ProfileEditorService.ChangeSelectedProfileElement(newLayer);

View File

@ -1,4 +1,5 @@
using Artemis.Core; using System;
using Artemis.Core;
using Artemis.Core.LayerBrushes; using Artemis.Core.LayerBrushes;
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands; namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
@ -6,11 +7,14 @@ namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
/// <summary> /// <summary>
/// Represents a profile editor command that can be used to change the brush of a layer. /// Represents a profile editor command that can be used to change the brush of a layer.
/// </summary> /// </summary>
public class ChangeLayerBrush : IProfileEditorCommand public class ChangeLayerBrush : IProfileEditorCommand, IDisposable
{ {
private readonly Layer _layer; private readonly Layer _layer;
private readonly LayerBrushDescriptor _layerBrushDescriptor; private readonly LayerBrushDescriptor _layerBrushDescriptor;
private readonly LayerBrushDescriptor? _previousDescriptor; private readonly BaseLayerBrush? _previousBrush;
private BaseLayerBrush? _newBrush;
private bool _executed;
/// <summary> /// <summary>
/// Creates a new instance of the <see cref="ChangeLayerBrush" /> class. /// Creates a new instance of the <see cref="ChangeLayerBrush" /> class.
@ -19,7 +23,7 @@ public class ChangeLayerBrush : IProfileEditorCommand
{ {
_layer = layer; _layer = layer;
_layerBrushDescriptor = layerBrushDescriptor; _layerBrushDescriptor = layerBrushDescriptor;
_previousDescriptor = layer.LayerBrush?.Descriptor; _previousBrush = _layer.LayerBrush;
} }
#region Implementation of IProfileEditorCommand #region Implementation of IProfileEditorCommand
@ -30,14 +34,32 @@ public class ChangeLayerBrush : IProfileEditorCommand
/// <inheritdoc /> /// <inheritdoc />
public void Execute() public void Execute()
{ {
_layer.ChangeLayerBrush(_layerBrushDescriptor); // Create the new brush
_newBrush ??= _layerBrushDescriptor.CreateInstance(_layer, null);
// Change the brush to the new brush
_layer.ChangeLayerBrush(_newBrush);
_executed = true;
} }
/// <inheritdoc /> /// <inheritdoc />
public void Undo() public void Undo()
{ {
if (_previousDescriptor != null) _layer.ChangeLayerBrush(_previousBrush);
_layer.ChangeLayerBrush(_previousDescriptor); _executed = false;
}
#endregion
#region IDisposable
/// <inheritdoc />
public void Dispose()
{
if (_executed)
_previousBrush?.Dispose();
else
_newBrush?.Dispose();
} }
#endregion #endregion

View File

@ -3,22 +3,86 @@ using System.Threading.Tasks;
using Artemis.Core; using Artemis.Core;
using Artemis.UI.Shared.Services.Interfaces; using Artemis.UI.Shared.Services.Interfaces;
namespace Artemis.UI.Shared.Services.ProfileEditor namespace Artemis.UI.Shared.Services.ProfileEditor;
/// <summary>
/// Provides access the the profile editor back-end logic.
/// </summary>
public interface IProfileEditorService : IArtemisSharedUIService
{ {
public interface IProfileEditorService : IArtemisSharedUIService /// <summary>
{ /// Gets an observable of the currently selected profile configuration.
IObservable<ProfileConfiguration?> ProfileConfiguration { get; } /// </summary>
IObservable<RenderProfileElement?> ProfileElement { get; } IObservable<ProfileConfiguration?> ProfileConfiguration { get; }
IObservable<ProfileEditorHistory?> History { get; }
IObservable<TimeSpan> Time { get; }
IObservable<double> PixelsPerSecond { get; }
void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration); /// <summary>
void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement); /// Gets an observable of the currently selected profile element.
void ChangeTime(TimeSpan time); /// </summary>
IObservable<RenderProfileElement?> ProfileElement { get; }
void ExecuteCommand(IProfileEditorCommand command); /// <summary>
void SaveProfile(); /// Gets an observable of the current editor history.
Task SaveProfileAsync(); /// </summary>
} IObservable<ProfileEditorHistory?> History { get; }
/// <summary>
/// Gets an observable of the profile preview playback time.
/// </summary>
IObservable<TimeSpan> Time { get; }
/// <summary>
/// Gets an observable of the profile preview playing state.
/// </summary>
IObservable<bool> Playing { get; }
/// <summary>
/// Gets an observable of the zoom level.
/// </summary>
IObservable<double> PixelsPerSecond { get; }
/// <summary>
/// Changes the selected profile by its <see cref="Core.ProfileConfiguration" />.
/// </summary>
/// <param name="profileConfiguration">The profile configuration of the profile to select.</param>
void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration);
/// <summary>
/// Changes the selected profile element.
/// </summary>
/// <param name="renderProfileElement">The profile element to select.</param>
void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement);
/// <summary>
/// Changes the current profile preview playback time.
/// </summary>
/// <param name="time">The new time.</param>
void ChangeTime(TimeSpan time);
/// <summary>
/// Executes the provided command and adds it to the history.
/// </summary>
/// <param name="command">The command to execute.</param>
void ExecuteCommand(IProfileEditorCommand command);
/// <summary>
/// Saves the current profile.
/// </summary>
void SaveProfile();
/// <summary>
/// Asynchronously saves the current profile.
/// </summary>
/// <returns>A task representing the save action.</returns>
Task SaveProfileAsync();
/// <summary>
/// Resumes profile preview playback.
/// </summary>
void Play();
/// <summary>
/// Pauses profile preview playback.
/// </summary>
void Pause();
} }

View File

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Shared.Services.Interfaces; using Artemis.UI.Shared.Services.Interfaces;
using Serilog;
namespace Artemis.UI.Shared.Services.ProfileEditor; namespace Artemis.UI.Shared.Services.ProfileEditor;
@ -15,19 +16,27 @@ internal class ProfileEditorService : IProfileEditorService
private readonly Dictionary<ProfileConfiguration, ProfileEditorHistory> _profileEditorHistories = new(); private readonly Dictionary<ProfileConfiguration, ProfileEditorHistory> _profileEditorHistories = new();
private readonly BehaviorSubject<RenderProfileElement?> _profileElementSubject = new(null); private readonly BehaviorSubject<RenderProfileElement?> _profileElementSubject = new(null);
private readonly BehaviorSubject<TimeSpan> _timeSubject = new(TimeSpan.Zero); private readonly BehaviorSubject<TimeSpan> _timeSubject = new(TimeSpan.Zero);
private readonly BehaviorSubject<bool> _playingSubject = new(false);
private readonly BehaviorSubject<bool> _suspendedEditingSubject = new(false);
private readonly BehaviorSubject<double> _pixelsPerSecondSubject = new(300); private readonly BehaviorSubject<double> _pixelsPerSecondSubject = new(300);
private readonly ILogger _logger;
private readonly IProfileService _profileService; private readonly IProfileService _profileService;
private readonly IModuleService _moduleService;
private readonly IWindowService _windowService; private readonly IWindowService _windowService;
public ProfileEditorService(IProfileService profileService, IWindowService windowService) public ProfileEditorService(ILogger logger, IProfileService profileService, IModuleService moduleService, IWindowService windowService)
{ {
_logger = logger;
_profileService = profileService; _profileService = profileService;
_moduleService = moduleService;
_windowService = windowService; _windowService = windowService;
ProfileConfiguration = _profileConfigurationSubject.AsObservable(); ProfileConfiguration = _profileConfigurationSubject.AsObservable();
ProfileElement = _profileElementSubject.AsObservable(); ProfileElement = _profileElementSubject.AsObservable();
History = Observable.Defer(() => Observable.Return(GetHistory(_profileConfigurationSubject.Value))).Concat(ProfileConfiguration.Select(GetHistory)); History = Observable.Defer(() => Observable.Return(GetHistory(_profileConfigurationSubject.Value))).Concat(ProfileConfiguration.Select(GetHistory));
Time = _timeSubject.AsObservable(); Time = _timeSubject.AsObservable();
Playing = _playingSubject.AsObservable();
SuspendedEditing = _suspendedEditingSubject.AsObservable();
PixelsPerSecond = _pixelsPerSecondSubject.AsObservable(); PixelsPerSecond = _pixelsPerSecondSubject.AsObservable();
} }
@ -47,10 +56,46 @@ internal class ProfileEditorService : IProfileEditorService
public IObservable<RenderProfileElement?> ProfileElement { get; } public IObservable<RenderProfileElement?> ProfileElement { get; }
public IObservable<ProfileEditorHistory?> History { get; } public IObservable<ProfileEditorHistory?> History { get; }
public IObservable<TimeSpan> Time { get; } public IObservable<TimeSpan> Time { get; }
public IObservable<bool> Playing { get; }
public IObservable<bool> SuspendedEditing { get; }
public IObservable<double> PixelsPerSecond { get; } public IObservable<double> PixelsPerSecond { get; }
public void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration) public void ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration)
{ {
if (ReferenceEquals(_profileConfigurationSubject.Value, profileConfiguration))
return;
_logger.Verbose("ChangeCurrentProfileConfiguration {profile}", profileConfiguration);
// Stop playing and save the current profile
Pause();
if (_profileConfigurationSubject.Value?.Profile != null)
_profileConfigurationSubject.Value.Profile.LastSelectedProfileElement = _profileElementSubject.Value;
SaveProfile();
// No need to deactivate the profile, if needed it will be deactivated next update
if (_profileConfigurationSubject.Value != null)
_profileConfigurationSubject.Value.IsBeingEdited = false;
// Deselect whatever profile element was active
ChangeCurrentProfileElement(null);
// The new profile may need activation
if (profileConfiguration != null)
{
profileConfiguration.IsBeingEdited = true;
_moduleService.SetActivationOverride(profileConfiguration.Module);
_profileService.ActivateProfile(profileConfiguration);
_profileService.RenderForEditor = true;
if (profileConfiguration.Profile?.LastSelectedProfileElement is RenderProfileElement renderProfileElement)
ChangeCurrentProfileElement(renderProfileElement);
}
else
{
_moduleService.SetActivationOverride(null);
_profileService.RenderForEditor = false;
}
_profileConfigurationSubject.OnNext(profileConfiguration); _profileConfigurationSubject.OnNext(profileConfiguration);
} }
@ -61,6 +106,7 @@ internal class ProfileEditorService : IProfileEditorService
public void ChangeTime(TimeSpan time) public void ChangeTime(TimeSpan time)
{ {
Tick(time);
_timeSubject.OnNext(time); _timeSubject.OnNext(time);
} }
@ -101,4 +147,48 @@ internal class ProfileEditorService : IProfileEditorService
{ {
await Task.Run(SaveProfile); await Task.Run(SaveProfile);
} }
/// <inheritdoc />
public void Play()
{
if (!_playingSubject.Value)
_playingSubject.OnNext(true);
}
/// <inheritdoc />
public void Pause()
{
if (_playingSubject.Value)
_playingSubject.OnNext(false);
}
private void Tick(TimeSpan time)
{
if (_profileConfigurationSubject.Value?.Profile == null || _suspendedEditingSubject.Value)
return;
TickProfileElement(_profileConfigurationSubject.Value.Profile.GetRootFolder(), time);
}
private void TickProfileElement(ProfileElement profileElement, TimeSpan time)
{
if (profileElement is not RenderProfileElement renderElement)
return;
if (renderElement.Suspended)
{
renderElement.Disable();
}
else
{
renderElement.Enable();
renderElement.Timeline.Override(
time,
(renderElement != _profileElementSubject.Value || renderElement.Timeline.Length < time) && renderElement.Timeline.PlayMode == TimelinePlayMode.Repeat
);
foreach (ProfileElement child in renderElement.Children)
TickProfileElement(child, time);
}
}
} }

View File

@ -24,8 +24,8 @@
</Styles.Resources> </Styles.Resources>
<StyleInclude Source="/Styles/Border.axaml" /> <StyleInclude Source="/Styles/Border.axaml" />
<StyleInclude Source="/Styles/Button.axaml" /> <StyleInclude Source="/Styles/Button.axaml" />
<StyleInclude Source="/Styles/TextBlock.axaml" /> <StyleInclude Source="/Styles/Condensed.axaml" />
<StyleInclude Source="/Styles/TextBox.axaml" /> <StyleInclude Source="/Styles/TextBlock.axaml" />
<StyleInclude Source="/Styles/Sidebar.axaml" /> <StyleInclude Source="/Styles/Sidebar.axaml" />
<StyleInclude Source="/Styles/InfoBar.axaml" /> <StyleInclude Source="/Styles/InfoBar.axaml" />
<StyleInclude Source="/Styles/TreeView.axaml" /> <StyleInclude Source="/Styles/TreeView.axaml" />

View File

@ -1,6 +1,7 @@
<Styles xmlns="https://github.com/avaloniaui" <Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"> xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:controls1="clr-namespace:Artemis.UI.Shared.Controls">
<Design.PreviewWith> <Design.PreviewWith>
<Border Padding="50"> <Border Padding="50">
<StackPanel Spacing="5"> <StackPanel Spacing="5">
@ -24,6 +25,9 @@
<ComboBoxItem>Bluheheheheh</ComboBoxItem> <ComboBoxItem>Bluheheheheh</ComboBoxItem>
<ComboBoxItem>Bluhgfdgdsheheh</ComboBoxItem> <ComboBoxItem>Bluhgfdgdsheheh</ComboBoxItem>
</ComboBox> </ComboBox>
<controls:ColorPickerButton Color="Firebrick"></controls:ColorPickerButton>
<controls:ColorPickerButton Color="Firebrick" Classes="condensed"></controls:ColorPickerButton>
</StackPanel> </StackPanel>
</Border> </Border>
</Design.PreviewWith> </Design.PreviewWith>
@ -31,19 +35,34 @@
<!-- Add Styles Here --> <!-- Add Styles Here -->
<Style Selector="TextBox.condensed"> <Style Selector="TextBox.condensed">
<Setter Property="Padding" Value="4 2" /> <Setter Property="Padding" Value="4 2" />
<Setter Property="FontSize" Value="14" /> <Setter Property="FontSize" Value="13" />
<Setter Property="MinHeight" Value="25" /> <Setter Property="MinHeight" Value="24" />
</Style> </Style>
<Style Selector="controls|NumberBox.condensed /template/ TextBox#InputBox"> <Style Selector="controls|NumberBox.condensed /template/ TextBox#InputBox">
<Setter Property="Padding" Value="4 2" /> <Setter Property="Padding" Value="4 2" />
<Setter Property="FontSize" Value="14" /> <Setter Property="FontSize" Value="13" />
<Setter Property="MinHeight" Value="25" /> <Setter Property="MinHeight" Value="24" />
</Style> </Style>
<Style Selector="ComboBox.condensed"> <Style Selector="ComboBox.condensed">
<Setter Property="Padding" Value="4 2" /> <Setter Property="Padding" Value="4 2" />
<Setter Property="FontSize" Value="14" /> <Setter Property="FontSize" Value="13" />
<Setter Property="Height" Value="25" /> <Setter Property="Height" Value="24" />
</Style> </Style>
<Style Selector="controls1|EnumComboBox.condensed > ComboBox">
<Setter Property="Padding" Value="4 2" />
<Setter Property="FontSize" Value="13" />
<Setter Property="Height" Value="24" />
</Style>
<Style Selector="controls|ColorPickerButton.condensed">
<Setter Property="RenderTransform">
<Setter.Value>
<ScaleTransform ScaleX="0.72" ScaleY="0.72"></ScaleTransform>
</Setter.Value>
</Setter>
</Style>
</Styles> </Styles>

View File

@ -12,6 +12,7 @@
<ItemGroup> <ItemGroup>
<None Remove="Artemis.UI.Avalonia.csproj.DotSettings" /> <None Remove="Artemis.UI.Avalonia.csproj.DotSettings" />
<None Remove="Artemis.UI.csproj.DotSettings" /> <None Remove="Artemis.UI.csproj.DotSettings" />
<None Remove="Assets\Images\Logo\application.ico" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.11" /> <PackageReference Include="Avalonia" Version="0.10.11" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

View File

@ -0,0 +1,29 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using FluentAvalonia.UI.Media;
using SkiaSharp;
namespace Artemis.UI.Converters;
/// <summary>
/// Converts <see cref="SKColor" /> into <see cref="Color2" />.
/// </summary>
public class SKColorToColor2Converter : IValueConverter
{
/// <inheritdoc />
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is SKColor skColor)
return new Color2(skColor.Red, skColor.Green, skColor.Blue, skColor.Alpha);
return new Color2();
}
/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is Color2 color2)
return new SKColor(color2.R, color2.G, color2.B, color2.A);
return SKColor.Empty;
}
}

View File

@ -0,0 +1,28 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using SkiaSharp;
namespace Artemis.UI.Converters;
/// <inheritdoc />
/// <summary>
/// Converts <see cref="SKColor" />into <see cref="T:System.String" />.
/// </summary>
public class SKColorToStringConverter : IValueConverter
{
/// <inheritdoc />
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
return value?.ToString();
}
/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (string.IsNullOrWhiteSpace(value as string))
return SKColor.Empty;
return SKColor.TryParse((string) value!, out SKColor color) ? color : SKColor.Empty;
}
}

View File

@ -2,7 +2,8 @@
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:controls="clr-namespace:Artemis.UI.Shared.Controls;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.DefaultTypes.PropertyInput.BoolPropertyInputView"> x:Class="Artemis.UI.DefaultTypes.PropertyInput.BoolPropertyInputView">
TODO <controls:EnumComboBox Classes="condensed" Width="200" Value="{Binding SelectedBooleanOption}" VerticalAlignment="Center" />
</UserControl> </UserControl>

View File

@ -1,13 +1,31 @@
using Artemis.Core; using System;
using Artemis.Core;
using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.PropertyInput; using Artemis.UI.Shared.Services.PropertyInput;
using ReactiveUI;
namespace Artemis.UI.DefaultTypes.PropertyInput; namespace Artemis.UI.DefaultTypes.PropertyInput;
public class BoolPropertyInputViewModel : PropertyInputViewModel<bool> public class BoolPropertyInputViewModel : PropertyInputViewModel<bool>
{ {
private BooleanOptions _selectedBooleanOption;
public BoolPropertyInputViewModel(LayerProperty<bool> layerProperty, IProfileEditorService profileEditorService, IPropertyInputService propertyInputService) public BoolPropertyInputViewModel(LayerProperty<bool> layerProperty, IProfileEditorService profileEditorService, IPropertyInputService propertyInputService)
: base(layerProperty, profileEditorService, propertyInputService) : base(layerProperty, profileEditorService, propertyInputService)
{ {
this.WhenAnyValue(vm => vm.InputValue).Subscribe(v => SelectedBooleanOption = v ? BooleanOptions.True : BooleanOptions.False);
this.WhenAnyValue(vm => vm.SelectedBooleanOption).Subscribe(v => InputValue = v == BooleanOptions.True);
} }
public BooleanOptions SelectedBooleanOption
{
get => _selectedBooleanOption;
set => this.RaiseAndSetIfChanged(ref _selectedBooleanOption, value);
}
}
public enum BooleanOptions
{
True,
False
} }

View File

@ -23,7 +23,7 @@
</UserControl.Styles> </UserControl.Styles>
<ComboBox Classes="brush condensed" <ComboBox Classes="brush condensed"
Width="200" Width="200"
HorizontalAlignment="Left" VerticalAlignment="Center"
Items="{Binding Descriptors}" Items="{Binding Descriptors}"
SelectedItem="{Binding SelectedDescriptor}"> SelectedItem="{Binding SelectedDescriptor}">
<ComboBox.ItemTemplate> <ComboBox.ItemTemplate>

View File

@ -66,6 +66,16 @@ public class BrushPropertyInputViewModel : PropertyInputViewModel<LayerBrushRefe
Dispatcher.UIThread.InvokeAsync(() => _windowService.CreateContentDialog().WithViewModel(out LayerBrushPresetViewModel _, ("layerBrush", layer.LayerBrush)).ShowAsync()); Dispatcher.UIThread.InvokeAsync(() => _windowService.CreateContentDialog().WithViewModel(out LayerBrushPresetViewModel _, ("layerBrush", layer.LayerBrush)).ShowAsync());
} }
#region Overrides of PropertyInputViewModel<LayerBrushReference>
/// <inheritdoc />
protected override void OnInputValueChanged()
{
this.RaisePropertyChanged(nameof(SelectedDescriptor));
}
#endregion
private void UpdateDescriptorsIfChanged(PluginFeatureEventArgs e) private void UpdateDescriptorsIfChanged(PluginFeatureEventArgs e)
{ {
if (e.PluginFeature is not LayerBrushProvider) if (e.PluginFeature is not LayerBrushProvider)

View File

@ -2,7 +2,8 @@
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:controls="clr-namespace:Artemis.UI.Shared.Controls;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.DefaultTypes.PropertyInput.EnumPropertyInputView"> x:Class="Artemis.UI.DefaultTypes.PropertyInput.EnumPropertyInputView">
TODO <controls:EnumComboBox Classes="condensed" Width="200" Value="{Binding InputValue}" VerticalAlignment="Center" />
</UserControl> </UserControl>

View File

@ -2,7 +2,31 @@
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:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:shared="clr-namespace: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.DefaultTypes.PropertyInput.SKColorPropertyInputView"> x:Class="Artemis.UI.DefaultTypes.PropertyInput.SKColorPropertyInputView">
TODO <UserControl.Resources>
</UserControl> <converters:SKColorToStringConverter x:Key="SKColorToStringConverter" />
<converters:SKColorToColor2Converter x:Key="SKColorToColor2Converter" />
</UserControl.Resources>
<Grid Height="24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox Classes="condensed"
Grid.Row="0"
Grid.Column="0"
Text="{Binding InputValue, Converter={StaticResource SKColorToStringConverter}}"/>
<controls:ColorPickerButton Grid.Row="0"
Grid.Column="1"
Color="{Binding InputValue, Converter={StaticResource SKColorToColor2Converter}}"
Classes="condensed"
VerticalAlignment="Center"
HorizontalAlignment="Right"
Margin="-6 0 -8 0"/>
</Grid>
</UserControl>

View File

@ -5,7 +5,7 @@
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.MainWindow" x:Class="Artemis.UI.MainWindow"
Icon="/Assets/Images/Logo/bow.ico" Icon="/Assets/Images/Logo/application.ico"
Title="Artemis 2.0"> Title="Artemis 2.0">
<!-- Use a panel here so the main window can host ContentDialogs --> <!-- Use a panel here so the main window can host ContentDialogs -->
<Panel> <Panel>

View File

@ -1,5 +1,7 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.LayerBrushes;
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;
@ -67,6 +69,8 @@ namespace Artemis.UI.Ninject.Factories
{ {
ProfileElementPropertyViewModel ProfileElementPropertyViewModel(ILayerProperty layerProperty); ProfileElementPropertyViewModel ProfileElementPropertyViewModel(ILayerProperty layerProperty);
ProfileElementPropertyGroupViewModel ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup); ProfileElementPropertyGroupViewModel ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup);
ProfileElementPropertyGroupViewModel ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerBrush layerBrush);
ProfileElementPropertyGroupViewModel ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerEffect layerEffect);
TreeGroupViewModel TreeGroupViewModel(ProfileElementPropertyGroupViewModel profileElementPropertyGroupViewModel); TreeGroupViewModel TreeGroupViewModel(ProfileElementPropertyGroupViewModel profileElementPropertyGroupViewModel);
// TimelineGroupViewModel TimelineGroupViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel); // TimelineGroupViewModel TimelineGroupViewModel(ProfileElementPropertiesViewModel profileElementPropertiesViewModel);

View File

@ -7,7 +7,7 @@
xmlns:reactiveUi="http://reactiveui.net" xmlns:reactiveUi="http://reactiveui.net"
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800" mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800"
x:Class="Artemis.UI.Screens.Debugger.DebugView" x:Class="Artemis.UI.Screens.Debugger.DebugView"
Icon="/Assets/Images/Logo/bow.ico" Icon="/Assets/Images/Logo/application.ico"
Title="Artemis | Debugger" Title="Artemis | Debugger"
Width="1200" Width="1200"
Height="800"> Height="800">

View File

@ -6,7 +6,7 @@
xmlns:controls1="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:controls1="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800" mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="800"
x:Class="Artemis.UI.Screens.Device.DevicePropertiesView" x:Class="Artemis.UI.Screens.Device.DevicePropertiesView"
Icon="/Assets/Images/Logo/bow.ico" Icon="/Assets/Images/Logo/application.ico"
Title="Artemis | Device Properties" Title="Artemis | Device Properties"
Width="1250" Width="1250"
Height="900"> Height="900">

View File

@ -5,7 +5,7 @@
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Plugins.PluginSettingsWindowView" x:Class="Artemis.UI.Screens.Plugins.PluginSettingsWindowView"
Icon="/Assets/Images/Logo/bow.ico" Icon="/Assets/Images/Logo/application.ico"
Title="{Binding DisplayName}" Title="{Binding DisplayName}"
Width="800" Width="800"
Height="800" Height="800"

View File

@ -0,0 +1,63 @@
<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="48"
x:Class="Artemis.UI.Screens.ProfileEditor.Playback.PlaybackView">
<DockPanel Margin="8 0">
<StackPanel Orientation="Horizontal" Spacing="5">
<Button Classes="icon-button icon-button-large"
ToolTip.Tip="Play from start (Shift+Space)" Command="{Binding PlayFromStart}"
Focusable="False">
<avalonia:MaterialIcon Kind="StepForward" />
</Button>
<Button Classes="icon-button icon-button-large"
ToolTip.Tip="Toggle play/pause (Space)" Command="{Binding TogglePlay}" Focusable="False">
<StackPanel>
<avalonia:MaterialIcon Kind="Play" IsVisible="{Binding !Playing}" />
<avalonia:MaterialIcon Kind="Pause" IsVisible="{Binding Playing}" />
</StackPanel>
</Button>
<Button Classes="icon-button icon-button-large" ToolTip.Tip="Go to start" Command="{Binding GoToStart}" Focusable="False">
<avalonia:MaterialIcon Kind="SkipBackward" />
</Button>
<Button Classes="icon-button icon-button-large" ToolTip.Tip="Go to end" Command="{Binding GoToEnd}" Focusable="False">
<avalonia:MaterialIcon Kind="SkipForward" />
</Button>
<Button Classes="icon-button icon-button-large" ToolTip.Tip="Previous frame" Command="{Binding GoToPreviousFrame}" Focusable="False">
<avalonia:MaterialIcon Kind="SkipPrevious" />
</Button>
<Button Classes="icon-button icon-button-large" ToolTip.Tip="Next frame" Command="{Binding GoToNextFrame}" Focusable="False">
<avalonia:MaterialIcon Kind="SkipNext" />
</Button>
<ToggleButton Classes="icon-button icon-button-large"
IsChecked="{Binding Repeating}"
Focusable="False"
Command="{Binding CycleRepeating}">
<ToolTip.Tip>
<StackPanel>
<StackPanel IsVisible="{Binding Repeating}">
<TextBlock Text="Repeat entire timeline"
IsVisible="{Binding RepeatTimeline}" />
<TextBlock Text="Repeat segment"
IsVisible="{Binding RepeatSegment}" />
</StackPanel>
<TextBlock IsVisible="{Binding !Repeating}">
Don't repeat the timeline
</TextBlock>
<TextBlock TextWrapping="Wrap">
This setting only applies to the editor and does not affect the repeat mode during normal profile playback
</TextBlock>
</StackPanel>
</ToolTip.Tip>
<Grid>
<avalonia:MaterialIcon Kind="Repeat" IsVisible="{Binding RepeatTimeline}" />
<avalonia:MaterialIcon Kind="RepeatOne" IsVisible="{Binding RepeatSegment}" />
</Grid>
</ToggleButton>
</StackPanel>
<TextBlock Classes="h4" Text="{Binding FormattedCurrentTime, FallbackValue=00.000}" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0"/>
</DockPanel>
</UserControl>

View File

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

View File

@ -0,0 +1,193 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Playback;
public class PlaybackViewModel : ActivatableViewModelBase
{
private readonly ICoreService _coreService;
private readonly IProfileEditorService _profileEditorService;
private readonly ISettingsService _settingsService;
private RenderProfileElement? _profileElement;
private ObservableAsPropertyHelper<TimeSpan>? _currentTime;
private ObservableAsPropertyHelper<string?>? _formattedCurrentTime;
private ObservableAsPropertyHelper<bool>? _playing;
private bool _repeating;
private bool _repeatTimeline;
private bool _repeatSegment;
public PlaybackViewModel(ICoreService coreService, IProfileEditorService profileEditorService, ISettingsService settingsService)
{
_coreService = coreService;
_profileEditorService = profileEditorService;
_settingsService = settingsService;
this.WhenActivated(d =>
{
_profileEditorService.ProfileElement.Subscribe(e => _profileElement = e).DisposeWith(d);
_currentTime = _profileEditorService.Time.ToProperty(this, vm => vm.CurrentTime).DisposeWith(d);
_formattedCurrentTime = _profileEditorService.Time.Select(t => $"{Math.Floor(t.TotalSeconds):00}.{t.Milliseconds:000}").ToProperty(this, vm => vm.FormattedCurrentTime).DisposeWith(d);
_playing = _profileEditorService.Playing.ToProperty(this, vm => vm.Playing).DisposeWith(d);
Observable.FromEventPattern<FrameRenderingEventArgs>(x => coreService.FrameRendering += x, x => coreService.FrameRendering -= x)
.Subscribe(e => CoreServiceOnFrameRendering(e.EventArgs))
.DisposeWith(d);
});
}
public TimeSpan CurrentTime => _currentTime?.Value ?? TimeSpan.Zero;
public string? FormattedCurrentTime => _formattedCurrentTime?.Value;
public bool Playing => _playing?.Value ?? false;
public bool Repeating
{
get => _repeating;
set => this.RaiseAndSetIfChanged(ref _repeating, value);
}
public bool RepeatTimeline
{
get => _repeatTimeline;
set => this.RaiseAndSetIfChanged(ref _repeatTimeline, value);
}
public bool RepeatSegment
{
get => _repeatSegment;
set => this.RaiseAndSetIfChanged(ref _repeatSegment, value);
}
public void PlayFromStart()
{
GoToStart();
if (!Playing)
_profileEditorService.Play();
}
public void TogglePlay()
{
if (!Playing)
_profileEditorService.Play();
else
_profileEditorService.Pause();
}
public void GoToStart()
{
_profileEditorService.ChangeTime(TimeSpan.Zero);
}
public void GoToEnd()
{
if (_profileElement == null)
return;
_profileEditorService.ChangeTime(_profileElement.Timeline.EndSegmentEndPosition);
}
public void GoToPreviousFrame()
{
if (_profileElement == null)
return;
double frameTime = 1000.0 / _settingsService.GetSetting("Core.TargetFrameRate", 30).Value;
double newTime = Math.Max(0, Math.Round((CurrentTime.TotalMilliseconds - frameTime) / frameTime) * frameTime);
_profileEditorService.ChangeTime(TimeSpan.FromMilliseconds(newTime));
}
public void GoToNextFrame()
{
if (_profileElement == null)
return;
double frameTime = 1000.0 / _settingsService.GetSetting("Core.TargetFrameRate", 30).Value;
double newTime = Math.Round((CurrentTime.TotalMilliseconds + frameTime) / frameTime) * frameTime;
newTime = Math.Min(newTime, _profileElement.Timeline.EndSegmentEndPosition.TotalMilliseconds);
_profileEditorService.ChangeTime(TimeSpan.FromMilliseconds(newTime));
}
public void CycleRepeating()
{
if (!Repeating)
{
RepeatTimeline = true;
RepeatSegment = false;
Repeating = true;
}
else if (RepeatTimeline)
{
RepeatTimeline = false;
RepeatSegment = true;
}
else if (RepeatSegment)
{
RepeatTimeline = true;
RepeatSegment = false;
Repeating = false;
}
}
private TimeSpan GetCurrentSegmentStart()
{
if (_profileElement == null)
return TimeSpan.Zero;
if (CurrentTime < _profileElement.Timeline.StartSegmentEndPosition)
return TimeSpan.Zero;
if (CurrentTime < _profileElement.Timeline.MainSegmentEndPosition)
return _profileElement.Timeline.MainSegmentStartPosition;
if (CurrentTime < _profileElement.Timeline.EndSegmentEndPosition)
return _profileElement.Timeline.EndSegmentStartPosition;
return TimeSpan.Zero;
}
private TimeSpan GetCurrentSegmentEnd()
{
if (_profileElement == null)
return TimeSpan.Zero;
if (CurrentTime < _profileElement.Timeline.StartSegmentEndPosition)
return _profileElement.Timeline.StartSegmentEndPosition;
if (CurrentTime < _profileElement.Timeline.MainSegmentEndPosition)
return _profileElement.Timeline.MainSegmentEndPosition;
if (CurrentTime < _profileElement.Timeline.EndSegmentEndPosition)
return _profileElement.Timeline.EndSegmentEndPosition;
return TimeSpan.Zero;
}
private void CoreServiceOnFrameRendering(FrameRenderingEventArgs e)
{
if (!Playing)
return;
TimeSpan newTime = CurrentTime.Add(TimeSpan.FromSeconds(e.DeltaTime));
if (_profileElement != null)
{
if (Repeating && RepeatTimeline)
{
if (newTime > _profileElement.Timeline.Length)
newTime = TimeSpan.Zero;
}
else if (Repeating && RepeatSegment)
{
if (newTime > GetCurrentSegmentEnd())
newTime = GetCurrentSegmentStart();
}
else if (newTime > _profileElement.Timeline.Length)
{
newTime = _profileElement.Timeline.Length;
_profileEditorService.Pause();
}
}
_profileEditorService.ChangeTime(newTime);
}
}

View File

@ -3,18 +3,45 @@
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:local="clr-namespace:Artemis.UI.Screens.ProfileEditor.ProfileElementProperties"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
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.ProfileElementProperties.ProfileElementPropertiesView">
<Grid ColumnDefinitions="*,Auto,*">
<ItemsControl Grid.Column="0" Items="{Binding PropertyGroupViewModels}">
<ItemsControl.ItemTemplate>
<TreeDataTemplate DataType="{x:Type local:ProfileElementPropertyGroupViewModel}" ItemsSource="{Binding Children}">
<ContentControl Content="{Binding TreeGroupViewModel}" />
</TreeDataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<GridSplitter Grid.Column="1"></GridSplitter> <Grid ColumnDefinitions="*,Auto,*" RowDefinitions="48,*">
</Grid> <ContentControl Grid.Row="0" Content="{Binding PlaybackViewModel}"></ContentControl>
</UserControl> <GridSplitter Grid.Row="0" Grid.Column="1" Cursor="SizeWestEast" Foreground="Transparent" Background="Transparent" />
<ScrollViewer Grid.Row="1"
Grid.Column="0"
Name="TreeScrollViewer"
Offset="{Binding #TimelineScrollViewer.Offset, Mode=OneWay}"
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}">
<ItemsControl Items="{Binding PropertyGroupViewModels}" Padding="0 0 8 0">
<ItemsControl.ItemTemplate>
<TreeDataTemplate DataType="{x:Type local:ProfileElementPropertyGroupViewModel}" ItemsSource="{Binding Children}">
<ContentControl Content="{Binding TreeGroupViewModel}" />
</TreeDataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<GridSplitter Grid.Row="1" Grid.Column="1" Cursor="SizeWestEast" Foreground="Transparent" Background="{DynamicResource CardStrokeColorDefaultSolidBrush}" />
<ScrollViewer Grid.Row="1"
Grid.Column="2"
Name="TimelineScrollViewer"
Offset="{Binding #TreeScrollViewer.Offset, Mode=OneWay}"
VerticalScrollBarVisibility="Hidden"
Background="{DynamicResource CardStrokeColorDefaultSolidBrush}">
<ItemsControl Items="{Binding PropertyGroupViewModels}">
<ItemsControl.ItemTemplate>
<TreeDataTemplate DataType="{x:Type local:ProfileElementPropertyGroupViewModel}" ItemsSource="{Binding Children}">
<ContentControl Content="{Binding TreeGroupViewModel}" />
</TreeDataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</UserControl>

View File

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

View File

@ -6,9 +6,10 @@ using System.Reactive;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Reactive.Linq; using System.Reactive.Linq;
using Artemis.Core; using Artemis.Core;
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.Tree; using Artemis.UI.Screens.ProfileEditor.Playback;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor;
using ReactiveUI; using ReactiveUI;
@ -17,20 +18,18 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties;
public class ProfileElementPropertiesViewModel : ActivatableViewModelBase public class ProfileElementPropertiesViewModel : ActivatableViewModelBase
{ {
private readonly Dictionary<LayerPropertyGroup, ProfileElementPropertyGroupViewModel> _cachedViewModels;
private readonly ILayerPropertyVmFactory _layerPropertyVmFactory; private readonly ILayerPropertyVmFactory _layerPropertyVmFactory;
private readonly IProfileEditorService _profileEditorService;
private ProfileElementPropertyGroupViewModel? _brushPropertyGroup;
private ObservableAsPropertyHelper<RenderProfileElement?>? _profileElement; private ObservableAsPropertyHelper<RenderProfileElement?>? _profileElement;
private readonly Dictionary<RenderProfileElement, List<ProfileElementPropertyGroupViewModel>> _profileElementGroups;
private ObservableCollection<ProfileElementPropertyGroupViewModel> _propertyGroupViewModels; private ObservableCollection<ProfileElementPropertyGroupViewModel> _propertyGroupViewModels;
/// <inheritdoc /> /// <inheritdoc />
public ProfileElementPropertiesViewModel(IProfileEditorService profileEditorService, ILayerPropertyVmFactory layerPropertyVmFactory) public ProfileElementPropertiesViewModel(IProfileEditorService profileEditorService, ILayerPropertyVmFactory layerPropertyVmFactory, PlaybackViewModel playbackViewModel)
{ {
_profileEditorService = profileEditorService;
_layerPropertyVmFactory = layerPropertyVmFactory; _layerPropertyVmFactory = layerPropertyVmFactory;
PropertyGroupViewModels = new ObservableCollection<ProfileElementPropertyGroupViewModel>(); _propertyGroupViewModels = new ObservableCollection<ProfileElementPropertyGroupViewModel>();
_profileElementGroups = new Dictionary<RenderProfileElement, List<ProfileElementPropertyGroupViewModel>>(); _cachedViewModels = new Dictionary<LayerPropertyGroup, ProfileElementPropertyGroupViewModel>();
PlaybackViewModel = playbackViewModel;
// 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)
@ -47,10 +46,11 @@ public class ProfileElementPropertiesViewModel : ActivatableViewModelBase
.Subscribe(_ => UpdateGroups()); .Subscribe(_ => UpdateGroups());
// React to service profile element changes as long as the VM is active // React to service profile element changes as long as the VM is active
this.WhenActivated(d => _profileElement = _profileEditorService.ProfileElement.ToProperty(this, vm => vm.ProfileElement).DisposeWith(d)); this.WhenActivated(d => _profileElement = profileEditorService.ProfileElement.ToProperty(this, vm => vm.ProfileElement).DisposeWith(d));
this.WhenAnyValue(vm => vm.ProfileElement).Subscribe(_ => UpdateGroups()); this.WhenAnyValue(vm => vm.ProfileElement).Subscribe(_ => UpdateGroups());
} }
public PlaybackViewModel PlaybackViewModel { get; }
public RenderProfileElement? ProfileElement => _profileElement?.Value; public RenderProfileElement? ProfileElement => _profileElement?.Value;
public Layer? Layer => _profileElement?.Value as Layer; public Layer? Layer => _profileElement?.Value as Layer;
@ -68,57 +68,51 @@ public class ProfileElementPropertiesViewModel : ActivatableViewModelBase
return; return;
} }
if (!_profileElementGroups.TryGetValue(ProfileElement, out List<ProfileElementPropertyGroupViewModel>? viewModels)) ObservableCollection<ProfileElementPropertyGroupViewModel> viewModels = new();
{
viewModels = new List<ProfileElementPropertyGroupViewModel>();
_profileElementGroups[ProfileElement] = viewModels;
}
List<LayerPropertyGroup> groups = new();
if (Layer != null) if (Layer != null)
{ {
// Add default layer groups // Add base VMs
groups.Add(Layer.General); viewModels.Add(GetOrCreateViewModel(Layer.General, null, null));
groups.Add(Layer.Transform); viewModels.Add(GetOrCreateViewModel(Layer.Transform, null, null));
// Add brush group
// Add brush VM if the brush has properties
if (Layer.LayerBrush?.BaseProperties != null) if (Layer.LayerBrush?.BaseProperties != null)
groups.Add(Layer.LayerBrush.BaseProperties); viewModels.Add(GetOrCreateViewModel(Layer.LayerBrush.BaseProperties, Layer.LayerBrush, null));
} }
// Add effect groups // Add effect VMs
foreach (BaseLayerEffect layerEffect in ProfileElement.LayerEffects) foreach (BaseLayerEffect layerEffect in ProfileElement.LayerEffects.OrderBy(e => e.Order))
{
if (layerEffect.BaseProperties != null) if (layerEffect.BaseProperties != null)
groups.Add(layerEffect.BaseProperties); viewModels.Add(GetOrCreateViewModel(layerEffect.BaseProperties, null, layerEffect));
}
// Remove redundant VMs // Map the most recent collection of VMs to the current list of VMs, making as little changes to the collection as possible
viewModels.RemoveAll(vm => !groups.Contains(vm.LayerPropertyGroup)); for (int index = 0; index < viewModels.Count; index++)
// Create VMs for missing groups
foreach (LayerPropertyGroup group in groups)
{ {
if (viewModels.All(vm => vm.LayerPropertyGroup != group)) ProfileElementPropertyGroupViewModel profileElementPropertyGroupViewModel = viewModels[index];
viewModels.Add(_layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(group)); if (index > PropertyGroupViewModels.Count - 1)
PropertyGroupViewModels.Add(profileElementPropertyGroupViewModel);
else if (!ReferenceEquals(PropertyGroupViewModels[index], profileElementPropertyGroupViewModel))
PropertyGroupViewModels[index] = profileElementPropertyGroupViewModel;
} }
// Get all non-effect properties while (PropertyGroupViewModels.Count > viewModels.Count)
List<ProfileElementPropertyGroupViewModel> nonEffectProperties = viewModels PropertyGroupViewModels.RemoveAt(PropertyGroupViewModels.Count - 1);
.Where(l => l.TreeGroupViewModel.GroupType != LayerPropertyGroupType.LayerEffectRoot) }
.ToList();
// Order the effects
List<ProfileElementPropertyGroupViewModel> effectProperties = viewModels
.Where(l => l.TreeGroupViewModel.GroupType == LayerPropertyGroupType.LayerEffectRoot && l.LayerPropertyGroup.LayerEffect != null)
.OrderBy(l => l.LayerPropertyGroup.LayerEffect?.Order)
.ToList();
ObservableCollection<ProfileElementPropertyGroupViewModel> propertyGroupViewModels = new(); private ProfileElementPropertyGroupViewModel GetOrCreateViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerBrush? layerBrush, BaseLayerEffect? layerEffect)
foreach (ProfileElementPropertyGroupViewModel viewModel in nonEffectProperties) {
propertyGroupViewModels.Add(viewModel); if (_cachedViewModels.TryGetValue(layerPropertyGroup, out ProfileElementPropertyGroupViewModel? cachedVm))
foreach (ProfileElementPropertyGroupViewModel viewModel in effectProperties) return cachedVm;
propertyGroupViewModels.Add(viewModel);
PropertyGroupViewModels = propertyGroupViewModels; ProfileElementPropertyGroupViewModel createdVm;
if (layerBrush != null)
createdVm = _layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(layerPropertyGroup, layerBrush);
else if (layerEffect != null)
createdVm = _layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(layerPropertyGroup, layerEffect);
else
createdVm = _layerPropertyVmFactory.ProfileElementPropertyGroupViewModel(layerPropertyGroup);
_cachedViewModels[layerPropertyGroup] = createdVm;
return createdVm;
} }
} }

View File

@ -3,6 +3,8 @@ using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; using Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
using Artemis.UI.Shared; using Artemis.UI.Shared;
@ -33,8 +35,23 @@ public class ProfileElementPropertyGroupViewModel : ViewModelBase
PopulateChildren(); PopulateChildren();
} }
public ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService, BaseLayerBrush layerBrush)
: this(layerPropertyGroup, layerPropertyVmFactory, propertyInputService)
{
LayerBrush = layerBrush;
}
public ProfileElementPropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, ILayerPropertyVmFactory layerPropertyVmFactory, IPropertyInputService propertyInputService, BaseLayerEffect layerEffect)
: this(layerPropertyGroup, layerPropertyVmFactory, propertyInputService)
{
LayerEffect = layerEffect;
}
public ObservableCollection<ViewModelBase> Children { get; } public ObservableCollection<ViewModelBase> Children { get; }
public LayerPropertyGroup LayerPropertyGroup { get; } public LayerPropertyGroup LayerPropertyGroup { get; }
public BaseLayerBrush? LayerBrush { get; }
public BaseLayerEffect? LayerEffect { get; }
public TreeGroupViewModel TreeGroupViewModel { get; } public TreeGroupViewModel TreeGroupViewModel { get; }
public bool IsVisible public bool IsVisible

View File

@ -1,9 +1,11 @@
using ReactiveUI; using Artemis.Core;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree; namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree;
public interface ITreePropertyViewModel : IReactiveObject public interface ITreePropertyViewModel : IReactiveObject
{ {
ILayerProperty BaseLayerProperty { get; }
bool HasDataBinding { get; } bool HasDataBinding { get; }
double GetDepth(); double GetDepth();
} }

View File

@ -70,24 +70,24 @@
<Grid IsVisible="{Binding GroupType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static viewModel:LayerPropertyGroupType.LayerBrushRoot}}" <Grid IsVisible="{Binding GroupType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static viewModel:LayerPropertyGroupType.LayerBrushRoot}}"
ColumnDefinitions="Auto,Auto,Auto,*"> ColumnDefinitions="Auto,Auto,Auto,*">
<controls:ArtemisIcon Grid.Column="0" <controls:ArtemisIcon Grid.Column="0"
Icon="{Binding LayerPropertyGroup.LayerBrush.Descriptor.Icon}" Icon="{Binding LayerBrush.Descriptor.Icon}"
Width="16" Width="16"
Height="16" Height="16"
Margin="0 5 5 0" /> Margin="0 5 5 0" />
<TextBlock Grid.Column="1" <TextBlock Grid.Column="1"
ToolTip.Tip="{Binding LayerPropertyGroup.LayerBrush.Descriptor.Description}" ToolTip.Tip="{Binding LayerBrush.Descriptor.Description}"
Margin="0 5 0 0"> Margin="0 5 0 0">
Brush -&#160; Brush -&#160;
</TextBlock> </TextBlock>
<TextBlock Grid.Column="2" <TextBlock Grid.Column="2"
Text="{Binding LayerPropertyGroup.LayerBrush.Descriptor.DisplayName}" Text="{Binding LayerBrush.Descriptor.DisplayName}"
ToolTip.Tip="{Binding LayerPropertyGroup.LayerBrush.Descriptor.Description}" ToolTip.Tip="{Binding LayerBrush.Descriptor.Description}"
Margin="0 5 0 0" /> Margin="0 5 0 0" />
<StackPanel Grid.Column="3" <StackPanel Grid.Column="3"
Orientation="Horizontal" Orientation="Horizontal"
HorizontalAlignment="Right" HorizontalAlignment="Right"
IsVisible="{Binding LayerPropertyGroup.LayerBrush.ConfigurationDialog, Converter={x:Static ObjectConverters.IsNotNull}}"> IsVisible="{Binding LayerBrush.ConfigurationDialog, Converter={x:Static ObjectConverters.IsNotNull}}">
<TextBlock VerticalAlignment="Center">Extra options available!</TextBlock> <TextBlock VerticalAlignment="Center">Extra options available!</TextBlock>
<avalonia:MaterialIcon Kind="ChevronRight" VerticalAlignment="Center"> <avalonia:MaterialIcon Kind="ChevronRight" VerticalAlignment="Center">
<avalonia:MaterialIcon.RenderTransform> <avalonia:MaterialIcon.RenderTransform>
@ -108,31 +108,31 @@
<controls:ArtemisIcon <controls:ArtemisIcon
Grid.Column="0" Grid.Column="0"
Cursor="SizeNorthSouth" Cursor="SizeNorthSouth"
Icon="{Binding LayerPropertyGroup.LayerEffect.Descriptor.Icon}" Icon="{Binding LayerEffect.Descriptor.Icon}"
Width="16" Width="16"
Height="16" Height="16"
Margin="0 5 5 0" Margin="0 5 5 0"
Background="Transparent" /> Background="Transparent" />
<TextBlock Grid.Column="1" ToolTip.Tip="{Binding LayerPropertyGroup.LayerEffect.Descriptor.Description}" Margin="0 5 0 0"> <TextBlock Grid.Column="1" ToolTip.Tip="{Binding LayerEffect.Descriptor.Description}" Margin="0 5 0 0">
Effect Effect
</TextBlock> </TextBlock>
<TextBlock Grid.Column="2" <TextBlock Grid.Column="2"
ToolTip.Tip="{Binding LayerPropertyGroup.LayerEffect.Descriptor.Description}" ToolTip.Tip="{Binding LayerEffect.Descriptor.Description}"
Margin="3 5"> Margin="3 5">
- -
</TextBlock> </TextBlock>
<!-- Show either the descriptors display name or, if set, the effect name --> <!-- Show either the descriptors display name or, if set, the effect name -->
<TextBlock Grid.Column="3" <TextBlock Grid.Column="3"
Text="{Binding LayerPropertyGroup.LayerEffect.Descriptor.DisplayName}" Text="{Binding LayerEffect.Descriptor.DisplayName}"
ToolTip.Tip="{Binding LayerPropertyGroup.LayerEffect.Descriptor.Description}" ToolTip.Tip="{Binding LayerEffect.Descriptor.Description}"
Margin="0 5" Margin="0 5"
IsVisible="{Binding !LayerPropertyGroup.LayerEffect.Name, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" /> IsVisible="{Binding !LayerEffect.Name, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
<TextBlock Grid.Column="4" <TextBlock Grid.Column="4"
Text="{Binding LayerPropertyGroup.LayerEffect.Name}" Text="{Binding LayerEffect.Name}"
ToolTip.Tip="{Binding LayerPropertyGroup.LayerEffect.Descriptor.Description}" ToolTip.Tip="{Binding LayerEffect.Descriptor.Description}"
Margin="0 5" Margin="0 5"
IsVisible="{Binding LayerPropertyGroup.LayerEffect.Name, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" /> IsVisible="{Binding LayerEffect.Name, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
<StackPanel Grid.Column="5" Orientation="Horizontal"> <StackPanel Grid.Column="5" Orientation="Horizontal">
<ToggleButton <ToggleButton
@ -140,7 +140,7 @@
ToolTip.Tip="Toggle suspended state" ToolTip.Tip="Toggle suspended state"
Width="18" Width="18"
Height="18" Height="18"
IsChecked="{Binding !LayerPropertyGroup.LayerEffect.Suspended}" IsChecked="{Binding !LayerEffect.Suspended}"
VerticalAlignment="Center" Padding="-25" VerticalAlignment="Center" Padding="-25"
Margin="5 0" Margin="5 0"
Command="{Binding SuspendedToggled}"> Command="{Binding SuspendedToggled}">
@ -160,7 +160,7 @@
Height="24" Height="24"
VerticalAlignment="Center" VerticalAlignment="Center"
Command="{Binding OpenEffectSettings}" Command="{Binding OpenEffectSettings}"
IsVisible="{Binding LayerPropertyGroup.LayerEffect.ConfigurationDialog, Converter={x:Static ObjectConverters.IsNotNull}}"> IsVisible="{Binding LayerEffect.ConfigurationDialog, Converter={x:Static ObjectConverters.IsNotNull}}">
<avalonia:MaterialIcon Kind="Settings" Height="16" Width="16" /> <avalonia:MaterialIcon Kind="Settings" Height="16" Width="16" />
</Button> </Button>
<Button Classes="icon-button" <Button Classes="icon-button"

View File

@ -46,14 +46,16 @@ public class TreeGroupViewModel : ActivatableViewModelBase
public ProfileElementPropertyGroupViewModel ProfileElementPropertyGroupViewModel { get; } public ProfileElementPropertyGroupViewModel ProfileElementPropertyGroupViewModel { get; }
public LayerPropertyGroup LayerPropertyGroup => ProfileElementPropertyGroupViewModel.LayerPropertyGroup; public LayerPropertyGroup LayerPropertyGroup => ProfileElementPropertyGroupViewModel.LayerPropertyGroup;
public BaseLayerBrush? LayerBrush => ProfileElementPropertyGroupViewModel.LayerBrush;
public BaseLayerEffect? LayerEffect => ProfileElementPropertyGroupViewModel.LayerEffect;
public ObservableCollection<ViewModelBase>? Children => ProfileElementPropertyGroupViewModel.IsExpanded ? ProfileElementPropertyGroupViewModel.Children : null; public ObservableCollection<ViewModelBase>? Children => ProfileElementPropertyGroupViewModel.IsExpanded ? ProfileElementPropertyGroupViewModel.Children : null;
public LayerPropertyGroupType GroupType { get; private set; } public LayerPropertyGroupType GroupType { get; private set; }
public async Task OpenBrushSettings() public async Task OpenBrushSettings()
{ {
BaseLayerBrush? layerBrush = LayerPropertyGroup.LayerBrush; if (LayerBrush?.ConfigurationDialog is not LayerBrushConfigurationDialog configurationViewModel)
if (layerBrush?.ConfigurationDialog is not LayerBrushConfigurationDialog configurationViewModel)
return; return;
try try
@ -65,8 +67,9 @@ public class TreeGroupViewModel : ActivatableViewModelBase
// Find the BaseLayerBrush parameter, it is required by the base constructor so its there for sure // Find the BaseLayerBrush parameter, it is required by the base constructor so its there for sure
ParameterInfo brushParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerBrush).IsAssignableFrom(p.ParameterType)); ParameterInfo brushParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerBrush).IsAssignableFrom(p.ParameterType));
ConstructorArgument argument = new(brushParameter.Name!, layerBrush); ConstructorArgument argument = new(brushParameter.Name!, LayerBrush);
BrushConfigurationViewModel viewModel = (BrushConfigurationViewModel) layerBrush.Descriptor.Provider.Plugin.Kernel!.Get(configurationViewModel.Type, argument); BrushConfigurationViewModel viewModel =
(BrushConfigurationViewModel) LayerBrush.Descriptor.Provider.Plugin.Kernel!.Get(configurationViewModel.Type, argument);
_brushConfigurationWindowViewModel = new BrushConfigurationWindowViewModel(viewModel, configurationViewModel); _brushConfigurationWindowViewModel = new BrushConfigurationWindowViewModel(viewModel, configurationViewModel);
await _windowService.ShowDialogAsync(_brushConfigurationWindowViewModel); await _windowService.ShowDialogAsync(_brushConfigurationWindowViewModel);
@ -82,8 +85,7 @@ public class TreeGroupViewModel : ActivatableViewModelBase
public async Task OpenEffectSettings() public async Task OpenEffectSettings()
{ {
BaseLayerEffect? layerEffect = LayerPropertyGroup.LayerEffect; if (LayerEffect?.ConfigurationDialog is not LayerEffectConfigurationDialog configurationViewModel)
if (layerEffect?.ConfigurationDialog is not LayerEffectConfigurationDialog configurationViewModel)
return; return;
try try
@ -95,8 +97,9 @@ public class TreeGroupViewModel : ActivatableViewModelBase
// Find the BaseLayerEffect parameter, it is required by the base constructor so its there for sure // Find the BaseLayerEffect parameter, it is required by the base constructor so its there for sure
ParameterInfo effectParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerEffect).IsAssignableFrom(p.ParameterType)); ParameterInfo effectParameter = constructors.First().GetParameters().First(p => typeof(BaseLayerEffect).IsAssignableFrom(p.ParameterType));
ConstructorArgument argument = new(effectParameter.Name!, layerEffect); ConstructorArgument argument = new(effectParameter.Name!, LayerEffect);
EffectConfigurationViewModel viewModel = (EffectConfigurationViewModel) layerEffect.Descriptor.Provider.Plugin.Kernel!.Get(configurationViewModel.Type, argument); EffectConfigurationViewModel viewModel =
(EffectConfigurationViewModel) LayerEffect.Descriptor.Provider.Plugin.Kernel!.Get(configurationViewModel.Type, argument);
_effectConfigurationWindowViewModel = new EffectConfigurationWindowViewModel(viewModel, configurationViewModel); _effectConfigurationWindowViewModel = new EffectConfigurationWindowViewModel(viewModel, configurationViewModel);
await _windowService.ShowDialogAsync(_effectConfigurationWindowViewModel); await _windowService.ShowDialogAsync(_effectConfigurationWindowViewModel);
@ -145,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 && LayerPropertyGroup.LayerBrush != null) else if (LayerPropertyGroup.Parent == null && ProfileElementPropertyGroupViewModel.LayerBrush != null)
GroupType = LayerPropertyGroupType.LayerBrushRoot; GroupType = LayerPropertyGroupType.LayerBrushRoot;
else if (LayerPropertyGroup.Parent == null && LayerPropertyGroup.LayerEffect != null) else if (LayerPropertyGroup.Parent == null && ProfileElementPropertyGroupViewModel.LayerEffect != null)
GroupType = LayerPropertyGroupType.LayerEffectRoot; GroupType = LayerPropertyGroupType.LayerEffectRoot;
else else
GroupType = LayerPropertyGroupType.None; GroupType = LayerPropertyGroupType.None;

View File

@ -1,13 +1,27 @@
using System;
using System.Reactive.Linq;
using Artemis.Core;
using Avalonia.Controls;
using Avalonia.Controls.Mixins;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree
{ {
public partial class TreePropertyView : ReactiveUserControl<IActivatableViewModel> public partial class TreePropertyView : ReactiveUserControl<ITreePropertyViewModel>
{ {
public TreePropertyView() 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(); InitializeComponent();
} }
@ -16,4 +30,4 @@ namespace Artemis.UI.Screens.ProfileEditor.ProfileElementProperties.Tree
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
} }
} }

View File

@ -16,6 +16,8 @@ internal class TreePropertyViewModel<T> : ActivatableViewModelBase, ITreePropert
public LayerProperty<T> LayerProperty { get; } public LayerProperty<T> LayerProperty { get; }
public ProfileElementPropertyViewModel ProfileElementPropertyViewModel { get; } public ProfileElementPropertyViewModel ProfileElementPropertyViewModel { get; }
public PropertyInputViewModel<T>? PropertyInputViewModel { get; } public PropertyInputViewModel<T>? PropertyInputViewModel { get; }
public ILayerProperty BaseLayerProperty => LayerProperty;
public bool HasDataBinding => LayerProperty.HasDataBinding; public bool HasDataBinding => LayerProperty.HasDataBinding;
public double GetDepth() public double GetDepth()

View File

@ -37,8 +37,8 @@
</UserControl.Styles> </UserControl.Styles>
<Grid ColumnDefinitions="4*,Auto,*" Classes="editor-grid"> <Grid ColumnDefinitions="4*,Auto,*" Classes="editor-grid">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="*"/> <RowDefinition Height="*" />
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<ContentControl Content="{Binding MenuBarViewModel}"></ContentControl> <ContentControl Content="{Binding MenuBarViewModel}"></ContentControl>
<Grid Grid.Column="0" RowDefinitions="3*,Auto,*"> <Grid Grid.Column="0" RowDefinitions="3*,Auto,*">
@ -66,8 +66,8 @@
<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"> <Border Grid.Row="2" Classes="card card-condensed" Margin="4" Padding="0" ClipToBounds="True">
<ContentControl Content="{Binding ProfileElementPropertiesViewModel}"></ContentControl> <ContentControl Content="{Binding ProfileElementPropertiesViewModel}"/>
</Border> </Border>
</Grid> </Grid>
@ -85,6 +85,6 @@
</Border> </Border>
</Grid> </Grid>
<ContentControl Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Content="{Binding StatusBarViewModel}"></ContentControl> <ContentControl Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Content="{Binding StatusBarViewModel}"/>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -126,7 +126,7 @@ namespace Artemis.UI.Screens.Root
{ {
_trayIcon = new TrayIcon _trayIcon = new TrayIcon
{ {
Icon = new WindowIcon(_assetLoader.Open(new Uri("avares://Artemis.UI/Assets/Images/Logo/bow.ico"))), Icon = new WindowIcon(_assetLoader.Open(new Uri("avares://Artemis.UI/Assets/Images/Logo/application.ico"))),
Command = ReactiveCommand.Create(OpenMainWindow) Command = ReactiveCommand.Create(OpenMainWindow)
}; };
_trayIcon.Menu = (NativeMenu?) Application.Current!.FindResource("TrayIconMenu"); _trayIcon.Menu = (NativeMenu?) Application.Current!.FindResource("TrayIconMenu");

View File

@ -5,7 +5,7 @@
xmlns:svg="clr-namespace:Avalonia.Svg.Skia;assembly=Avalonia.Svg.Skia" xmlns:svg="clr-namespace:Avalonia.Svg.Skia;assembly=Avalonia.Svg.Skia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Root.SplashView" x:Class="Artemis.UI.Screens.Root.SplashView"
Icon="/Assets/Images/Logo/bow.ico" Icon="/Assets/Images/Logo/application.ico"
Title="Artemis 2.0" Title="Artemis 2.0"
Height="450" Height="450"
Width="400" Width="400"

View File

@ -12,7 +12,7 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="850" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="850"
x:Class="Artemis.UI.Screens.Sidebar.ProfileConfigurationEditView" x:Class="Artemis.UI.Screens.Sidebar.ProfileConfigurationEditView"
Title="{Binding DisplayName}" Title="{Binding DisplayName}"
Icon="/Assets/Images/Logo/bow.ico" Icon="/Assets/Images/Logo/application.ico"
Width="800" Width="800"
MinWidth="420" MinWidth="420"
Height="850"> Height="850">