mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-09 18:36:27 +00:00
Finish initial implementation with shitty UI
This commit is contained in:
1 parent
eb812cb5e1
commit
964c5b6599
45 files changed
+1231
-198
No files matched your search
@@ -191,7 +191,8 @@ public class DataModelPath : IStorageModel, IDisposable, IPluginFeatureDependent
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<PluginFeature> GetFeatureDependencies()
|
||||
{
|
||||
if (Target == null)
|
||||
// Data models provided by Artemis itself are always available
|
||||
if (Target == null || Target.Module.Plugin == Constants.CorePlugin)
|
||||
return [];
|
||||
return [Target.Module];
|
||||
}
|
||||
|
||||
@@ -1,8 +1,71 @@
|
||||
namespace Artemis.Core;
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
|
||||
namespace Artemis.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a configuration item that accepts ColorGradient input from the user.
|
||||
/// </summary>
|
||||
public class ConfigurationColorGradientItem : ConfigurationInputItem<ColorGradient>
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="ConfigurationColorGradientItem" /> class.
|
||||
/// </summary>
|
||||
public ConfigurationColorGradientItem()
|
||||
{
|
||||
// The gradient picker doesn't handle null values well
|
||||
DefaultValue = ColorGradient.GetUnicornBarf();
|
||||
Value = ColorGradient.GetUnicornBarf();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ColorGradient DataModelFallbackValue => new();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool IsSameValue(ColorGradient? a, ColorGradient? b)
|
||||
{
|
||||
// Gradients are mutable and compared by value, a different instance should always be treated as a change
|
||||
return ReferenceEquals(a, b);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ColorGradient? CopyValue(ColorGradient? value)
|
||||
{
|
||||
return value != null ? new ColorGradient(value) : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnValueReplaced(ColorGradient? previous, ColorGradient? current)
|
||||
{
|
||||
// Gradients are edited in-place, forward those edits as a change of the property
|
||||
if (previous != null)
|
||||
{
|
||||
previous.CollectionChanged -= GradientOnCollectionChanged;
|
||||
previous.StopChanged -= GradientOnStopChanged;
|
||||
}
|
||||
|
||||
if (current != null)
|
||||
{
|
||||
current.CollectionChanged += GradientOnCollectionChanged;
|
||||
current.StopChanged += GradientOnStopChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void GradientOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
OnGradientEdited(sender);
|
||||
}
|
||||
|
||||
private void GradientOnStopChanged(object? sender, EventArgs e)
|
||||
{
|
||||
OnGradientEdited(sender);
|
||||
}
|
||||
|
||||
private void OnGradientEdited(object? sender)
|
||||
{
|
||||
if (ReferenceEquals(sender, Value))
|
||||
OnPropertyChanged(nameof(Value));
|
||||
if (ReferenceEquals(sender, DefaultValue))
|
||||
OnPropertyChanged(nameof(DefaultValue));
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
namespace Artemis.Core;
|
||||
namespace Artemis.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single option in a configuration dropdown list.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the value that this dropdown option represents.</typeparam>
|
||||
public class ConfigurationDropdownValue<T> : CorePropertyChanged
|
||||
public abstract class ConfigurationDropdownValue : CorePropertyChanged
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the display name shown to the user for this dropdown option.
|
||||
@@ -14,7 +13,14 @@ public class ConfigurationDropdownValue<T> : CorePropertyChanged
|
||||
get;
|
||||
set => SetAndNotify(ref field, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single option in a configuration dropdown list.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the value that this dropdown option represents.</typeparam>
|
||||
public class ConfigurationDropdownValue<T> : ConfigurationDropdownValue
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the actual value associated with this dropdown option.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using Artemis.Core.Modules;
|
||||
using Artemis.Storage.Entities.Profile.Configuration;
|
||||
|
||||
namespace Artemis.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a generic base class for configuration items that accept user input.
|
||||
/// Represents a base class for configuration items that accept user input.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the value that this configuration item holds.</typeparam>
|
||||
public class ConfigurationInputItem<T> : CorePropertyChanged, IConfigurationItem
|
||||
public abstract class ConfigurationInputItem : CorePropertyChanged, IConfigurationItem
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Guid Id { get; init; } = Guid.NewGuid();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the display name of the configuration item.
|
||||
/// </summary>
|
||||
@@ -27,36 +36,208 @@ public class ConfigurationInputItem<T> : CorePropertyChanged, IConfigurationItem
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current value of the configuration item.
|
||||
/// Resets the value of the configuration item to a copy of its default value.
|
||||
/// </summary>
|
||||
public abstract void ResetToDefault();
|
||||
|
||||
internal abstract string? SerializeValue();
|
||||
internal abstract void DeserializeValue(string? json);
|
||||
internal abstract void Load(ConfigurationInputItemEntity entity);
|
||||
internal abstract void Save(ConfigurationInputItemEntity entity);
|
||||
internal abstract void AddDynamicChild(DataModel dataModel, string key, DataModelPropertyAttribute attribute);
|
||||
internal abstract void UpdateDynamicChild(DataModel dataModel, DynamicChild dynamicChild);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a generic base class for configuration items that accept user input.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the value that this configuration item holds.</typeparam>
|
||||
public abstract class ConfigurationInputItem<T> : ConfigurationInputItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of the <see cref="ConfigurationInputItem{T}" /> class.
|
||||
/// </summary>
|
||||
protected ConfigurationInputItem()
|
||||
{
|
||||
DropdownValues.CollectionChanged += DropdownValuesOnCollectionChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current value of the configuration item, as picked by the user.
|
||||
/// </summary>
|
||||
public T? Value
|
||||
{
|
||||
get;
|
||||
set => SetAndNotify(ref field, value);
|
||||
set
|
||||
{
|
||||
if (IsSameValue(field, value))
|
||||
return;
|
||||
|
||||
T? previous = field;
|
||||
field = value;
|
||||
OnValueReplaced(previous, value);
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default value of the configuration item.
|
||||
/// Gets or sets the default value of the configuration item, as set by the author of the profile.
|
||||
/// </summary>
|
||||
public T? DefaultValue
|
||||
{
|
||||
get;
|
||||
set => SetAndNotify(ref field, value);
|
||||
set
|
||||
{
|
||||
if (IsSameValue(field, value))
|
||||
return;
|
||||
|
||||
T? previous = field;
|
||||
field = value;
|
||||
OnValueReplaced(previous, value);
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection of dropdown values for this configuration item.
|
||||
/// Gets the collection of dropdown values for this configuration item.
|
||||
/// When populated, the configuration item will be rendered as a dropdown/combo box.
|
||||
/// </summary>
|
||||
public ObservableCollection<ConfigurationDropdownValue<T>>? DropdownValues
|
||||
public ObservableCollection<ConfigurationDropdownValue<T>> DropdownValues { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value that is exposed to the data model when <see cref="Value" /> is <see langword="null" />.
|
||||
/// </summary>
|
||||
protected virtual T DataModelFallbackValue => default!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void ResetToDefault()
|
||||
{
|
||||
get;
|
||||
set
|
||||
Value = CopyValue(DefaultValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether two values are considered the same, if so no change notification is raised.
|
||||
/// </summary>
|
||||
protected virtual bool IsSameValue(T? a, T? b)
|
||||
{
|
||||
return EqualityComparer<T?>.Default.Equals(a, b);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a copy of the provided value, override this for mutable reference types.
|
||||
/// </summary>
|
||||
protected virtual T? CopyValue(T? value)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when <see cref="Value" /> or <see cref="DefaultValue" /> is replaced by another value.
|
||||
/// </summary>
|
||||
protected virtual void OnValueReplaced(T? previous, T? current)
|
||||
{
|
||||
}
|
||||
|
||||
// Forward changes to the dropdown values as a change of the property so that they are picked up like any other change
|
||||
private void DropdownValuesOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (e.OldItems != null)
|
||||
{
|
||||
if (Equals(value, field))
|
||||
return;
|
||||
field = value;
|
||||
OnPropertyChanged();
|
||||
foreach (ConfigurationDropdownValue<T> dropdownValue in e.OldItems)
|
||||
dropdownValue.PropertyChanged -= DropdownValueOnPropertyChanged;
|
||||
}
|
||||
} = [];
|
||||
}
|
||||
|
||||
if (e.NewItems != null)
|
||||
{
|
||||
foreach (ConfigurationDropdownValue<T> dropdownValue in e.NewItems)
|
||||
dropdownValue.PropertyChanged += DropdownValueOnPropertyChanged;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(DropdownValues));
|
||||
}
|
||||
|
||||
private void DropdownValueOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
OnPropertyChanged(nameof(DropdownValues));
|
||||
}
|
||||
|
||||
#region Storage
|
||||
|
||||
internal override string? SerializeValue()
|
||||
{
|
||||
return SerializeValue(Value);
|
||||
}
|
||||
|
||||
internal override void DeserializeValue(string? json)
|
||||
{
|
||||
if (TryDeserializeValue(json, out T? value))
|
||||
Value = value;
|
||||
else
|
||||
ResetToDefault();
|
||||
}
|
||||
|
||||
internal override void Load(ConfigurationInputItemEntity entity)
|
||||
{
|
||||
Name = entity.Name;
|
||||
Description = entity.Description;
|
||||
if (TryDeserializeValue(entity.DefaultValue, out T? defaultValue))
|
||||
DefaultValue = defaultValue;
|
||||
|
||||
DropdownValues.Clear();
|
||||
foreach (ConfigurationDropdownValueEntity dropdownValueEntity in entity.DropdownValues)
|
||||
{
|
||||
if (TryDeserializeValue(dropdownValueEntity.Value, out T? dropdownValue) && dropdownValue != null)
|
||||
DropdownValues.Add(new ConfigurationDropdownValue<T> {DisplayName = dropdownValueEntity.DisplayName, Value = dropdownValue});
|
||||
}
|
||||
}
|
||||
|
||||
internal override void Save(ConfigurationInputItemEntity entity)
|
||||
{
|
||||
entity.Id = Id;
|
||||
entity.Name = Name;
|
||||
entity.Description = Description;
|
||||
entity.DefaultValue = SerializeValue(DefaultValue);
|
||||
entity.DropdownValues = DropdownValues
|
||||
.Select(d => new ConfigurationDropdownValueEntity {DisplayName = d.DisplayName, Value = SerializeValue(d.Value)})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string? SerializeValue(T? value)
|
||||
{
|
||||
return value == null ? null : CoreJson.Serialize(value);
|
||||
}
|
||||
|
||||
private static bool TryDeserializeValue(string? json, out T? value)
|
||||
{
|
||||
value = default;
|
||||
if (json == null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
value = CoreJson.Deserialize<T>(json);
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Data model
|
||||
|
||||
internal override void AddDynamicChild(DataModel dataModel, string key, DataModelPropertyAttribute attribute)
|
||||
{
|
||||
dataModel.AddDynamicChild(key, Value ?? DataModelFallbackValue, attribute);
|
||||
}
|
||||
|
||||
internal override void UpdateDynamicChild(DataModel dataModel, DynamicChild dynamicChild)
|
||||
{
|
||||
if (dynamicChild is DynamicChild<T> typedDynamicChild)
|
||||
typedDynamicChild.Value = Value ?? DataModelFallbackValue;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,25 +1,48 @@
|
||||
namespace Artemis.Core;
|
||||
using Artemis.Core.Modules;
|
||||
|
||||
namespace Artemis.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a configuration item that accepts numeric input from the user.
|
||||
/// </summary>
|
||||
public class ConfigurationNumericItem : ConfigurationInputItem<Numeric>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum value the user can enter, or <see langword="null" /> for no minimum.
|
||||
/// </summary>
|
||||
public Numeric? Minimum
|
||||
{
|
||||
get;
|
||||
set => SetAndNotify(ref field, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum value the user can enter, or <see langword="null" /> for no maximum.
|
||||
/// </summary>
|
||||
public Numeric? Maximum
|
||||
{
|
||||
get;
|
||||
set => SetAndNotify(ref field, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a boolean indicating whether the value is entered using a slider instead of a number box.
|
||||
/// </summary>
|
||||
public bool Slider
|
||||
{
|
||||
get;
|
||||
set => SetAndNotify(ref field, value);
|
||||
}
|
||||
|
||||
// Numerics are exposed to the data model as floats, like most other numeric data model values
|
||||
internal override void AddDynamicChild(DataModel dataModel, string key, DataModelPropertyAttribute attribute)
|
||||
{
|
||||
dataModel.AddDynamicChild(key, (float) Value, attribute);
|
||||
}
|
||||
|
||||
internal override void UpdateDynamicChild(DataModel dataModel, DynamicChild dynamicChild)
|
||||
{
|
||||
if (dynamicChild is DynamicChild<float> typedDynamicChild)
|
||||
typedDynamicChild.Value = Value;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Artemis.Storage.Entities.Profile.Configuration;
|
||||
|
||||
namespace Artemis.Core;
|
||||
|
||||
@@ -34,4 +36,68 @@ public class ConfigurationSection : CorePropertyChanged
|
||||
/// Gets the collection of configuration items in this section.
|
||||
/// </summary>
|
||||
public ObservableCollection<IConfigurationItem> Items { get; } = [];
|
||||
|
||||
#region Storage
|
||||
|
||||
internal static ConfigurationSection Load(ProfileConfiguration profileConfiguration, ConfigurationSectionEntity entity)
|
||||
{
|
||||
ConfigurationSection section = new(profileConfiguration) {Name = entity.Name};
|
||||
foreach (IConfigurationItemEntity itemEntity in entity.Items)
|
||||
{
|
||||
IConfigurationItem? item = LoadItem(itemEntity);
|
||||
if (item != null)
|
||||
section.Items.Add(item);
|
||||
}
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
internal ConfigurationSectionEntity Save()
|
||||
{
|
||||
return new ConfigurationSectionEntity {Name = Name, Items = Items.Select(SaveItem).ToList()};
|
||||
}
|
||||
|
||||
private static IConfigurationItem? LoadItem(IConfigurationItemEntity entity)
|
||||
{
|
||||
ConfigurationInputItem? inputItem = entity switch
|
||||
{
|
||||
ConfigurationStringItemEntity e => new ConfigurationStringItem {Id = e.Id, Name = e.Name},
|
||||
ConfigurationBooleanItemEntity e => new ConfigurationBooleanItem {Id = e.Id, Name = e.Name, TrueText = e.TrueText, FalseText = e.FalseText},
|
||||
ConfigurationNumericItemEntity e => new ConfigurationNumericItem {Id = e.Id, Name = e.Name, Minimum = e.Minimum, Maximum = e.Maximum, Slider = e.Slider},
|
||||
ConfigurationSKColorItemEntity e => new ConfigurationSKColorItem {Id = e.Id, Name = e.Name},
|
||||
ConfigurationColorGradientItemEntity e => new ConfigurationColorGradientItem {Id = e.Id, Name = e.Name},
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (inputItem != null)
|
||||
{
|
||||
inputItem.Load((ConfigurationInputItemEntity) entity);
|
||||
return inputItem;
|
||||
}
|
||||
|
||||
if (entity is ConfigurationTextItemEntity textItemEntity)
|
||||
return new ConfigurationTextItem {Id = textItemEntity.Id, Text = textItemEntity.Text};
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IConfigurationItemEntity SaveItem(IConfigurationItem item)
|
||||
{
|
||||
if (item is ConfigurationTextItem textItem)
|
||||
return new ConfigurationTextItemEntity {Id = textItem.Id, Text = textItem.Text};
|
||||
|
||||
ConfigurationInputItemEntity entity = item switch
|
||||
{
|
||||
ConfigurationStringItem => new ConfigurationStringItemEntity(),
|
||||
ConfigurationBooleanItem booleanItem => new ConfigurationBooleanItemEntity {TrueText = booleanItem.TrueText, FalseText = booleanItem.FalseText},
|
||||
ConfigurationNumericItem numericItem => new ConfigurationNumericItemEntity {Minimum = numericItem.Minimum, Maximum = numericItem.Maximum, Slider = numericItem.Slider},
|
||||
ConfigurationSKColorItem => new ConfigurationSKColorItemEntity(),
|
||||
ConfigurationColorGradientItem => new ConfigurationColorGradientItemEntity(),
|
||||
_ => throw new ArtemisCoreException($"Unknown configuration item type: {item.GetType()}")
|
||||
};
|
||||
|
||||
((ConfigurationInputItem) item).Save(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
namespace Artemis.Core;
|
||||
namespace Artemis.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a configuration item that accepts string input from the user.
|
||||
/// </summary>
|
||||
public class ConfigurationStringItem : ConfigurationInputItem<string>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override string DataModelFallbackValue => string.Empty;
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
namespace Artemis.Core;
|
||||
using System;
|
||||
|
||||
namespace Artemis.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a configuration item that displays static text.
|
||||
/// </summary>
|
||||
public class ConfigurationTextItem : CorePropertyChanged, IConfigurationItem
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Guid Id { get; init; } = Guid.NewGuid();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the text content to display.
|
||||
/// </summary>
|
||||
@@ -13,4 +18,4 @@ public class ConfigurationTextItem : CorePropertyChanged, IConfigurationItem
|
||||
get;
|
||||
set => SetAndNotify(ref field, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
namespace Artemis.Core;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Artemis.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a contract for configuration items that can be added to a configuration section.
|
||||
/// </summary>
|
||||
public interface IConfigurationItem
|
||||
public interface IConfigurationItem : INotifyPropertyChanged
|
||||
{
|
||||
}
|
||||
/// <summary>
|
||||
/// Gets the unique ID of this configuration item, this ID remains stable between versions of a profile.
|
||||
/// </summary>
|
||||
Guid Id { get; }
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using Artemis.Core.Modules;
|
||||
using Artemis.Storage.Entities.Profile;
|
||||
using Artemis.Storage.Entities.Profile.Configuration;
|
||||
|
||||
namespace Artemis.Core;
|
||||
|
||||
@@ -17,7 +20,11 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
|
||||
/// </summary>
|
||||
public static readonly ProfileConfiguration Empty = new(ProfileCategory.Empty, "Empty", "Empty");
|
||||
|
||||
private readonly ProfileConfigurationDataModel _configurationDataModel = new();
|
||||
private readonly HashSet<ConfigurationSection> _trackedSections = [];
|
||||
private readonly HashSet<IConfigurationItem> _trackedItems = [];
|
||||
private bool _disposed;
|
||||
private bool _configurationDataModelRegistered;
|
||||
|
||||
internal ProfileConfiguration(ProfileCategory category, string name, string icon)
|
||||
{
|
||||
@@ -28,6 +35,7 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
|
||||
Icon = new ProfileConfigurationIcon(Entity);
|
||||
Icon.SetIconByName(icon);
|
||||
ActivationCondition = new NodeScript<bool>("Activate profile", "Whether or not the profile should be active", this);
|
||||
ConfigurationSections.CollectionChanged += ConfigurationSectionsOnCollectionChanged;
|
||||
|
||||
Entity.Profile.Id = Guid.NewGuid();
|
||||
Entity.ProfileConfiguration.ProfileId = Entity.Profile.Id;
|
||||
@@ -42,6 +50,7 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
|
||||
Entity = entity;
|
||||
Icon = new ProfileConfigurationIcon(Entity);
|
||||
ActivationCondition = new NodeScript<bool>("Activate profile", "Whether or not the profile should be active", this);
|
||||
ConfigurationSections.CollectionChanged += ConfigurationSectionsOnCollectionChanged;
|
||||
|
||||
Load();
|
||||
}
|
||||
@@ -52,7 +61,11 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
|
||||
public string Name
|
||||
{
|
||||
get;
|
||||
set => SetAndNotify(ref field, value);
|
||||
set
|
||||
{
|
||||
if (SetAndNotify(ref field, value))
|
||||
UpdateConfigurationDataModel();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -182,6 +195,16 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
|
||||
/// </summary>
|
||||
public ObservableCollection<ConfigurationSection> ConfigurationSections { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets all configuration items in all <see cref="ConfigurationSections" /> that accept user input.
|
||||
/// </summary>
|
||||
public IEnumerable<ConfigurationInputItem> ConfigurationInputItems => ConfigurationSections.SelectMany(s => s.Items).OfType<ConfigurationInputItem>();
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when anything in the <see cref="ConfigurationSections" /> changed, including the values of items.
|
||||
/// </summary>
|
||||
public event EventHandler? ConfigurationChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the icon configuration
|
||||
/// </summary>
|
||||
@@ -245,12 +268,44 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
|
||||
return Module == null || Module.IsActivated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the value of every configuration item to its default value.
|
||||
/// </summary>
|
||||
public void ResetConfigurationToDefaults()
|
||||
{
|
||||
foreach (ConfigurationInputItem item in ConfigurationInputItems)
|
||||
item.ResetToDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the configured values of the provided profile configuration onto the matching configuration items of this
|
||||
/// profile configuration. Items are matched by their ID, items without a match keep their current value.
|
||||
/// </summary>
|
||||
/// <param name="source">The profile configuration to copy the values from.</param>
|
||||
public void CopyConfigurationValues(ProfileConfiguration source)
|
||||
{
|
||||
Dictionary<Guid, ConfigurationInputItem> sourceItems = source.ConfigurationInputItems.ToDictionary(i => i.Id);
|
||||
foreach (ConfigurationInputItem item in ConfigurationInputItems)
|
||||
{
|
||||
if (sourceItems.TryGetValue(item.Id, out ConfigurationInputItem? sourceItem) && sourceItem.GetType() == item.GetType())
|
||||
item.DeserializeValue(sourceItem.SerializeValue());
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[ProfileConfiguration] {nameof(Name)}: {Name}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the <see cref="ConfigurationChanged" /> event
|
||||
/// </summary>
|
||||
protected virtual void OnConfigurationChanged()
|
||||
{
|
||||
ConfigurationChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
internal void LoadModules(List<Module> enabledModules)
|
||||
{
|
||||
if (_disposed)
|
||||
@@ -263,10 +318,128 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
RemoveConfigurationDataModel();
|
||||
_disposed = true;
|
||||
ActivationCondition.Dispose();
|
||||
}
|
||||
|
||||
#region Configuration
|
||||
|
||||
/// <summary>
|
||||
/// Exposes the configuration values of this profile in the data model, done once the profile is part of the profile service.
|
||||
/// </summary>
|
||||
internal void AddConfigurationDataModel()
|
||||
{
|
||||
_configurationDataModelRegistered = true;
|
||||
UpdateConfigurationDataModel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the configuration values of this profile from the data model, done once the profile is removed from the profile service.
|
||||
/// </summary>
|
||||
internal void RemoveConfigurationDataModel()
|
||||
{
|
||||
_configurationDataModelRegistered = false;
|
||||
UpdateConfigurationDataModel();
|
||||
}
|
||||
|
||||
private void UpdateConfigurationDataModel()
|
||||
{
|
||||
if (_disposed || Entity == null)
|
||||
return;
|
||||
|
||||
List<ConfigurationInputItem> items = ConfigurationInputItems.ToList();
|
||||
|
||||
// Sync the values of this profile
|
||||
HashSet<string> keys = items.Select(i => i.Id.ToString("N")).ToHashSet();
|
||||
foreach (string key in _configurationDataModel.DynamicChildren.Keys.Where(k => !keys.Contains(k)).ToList())
|
||||
_configurationDataModel.RemoveDynamicChildByKey(key);
|
||||
foreach (ConfigurationInputItem item in items)
|
||||
{
|
||||
string key = item.Id.ToString("N");
|
||||
if (_configurationDataModel.TryGetDynamicChild(key, out DynamicChild? dynamicChild))
|
||||
{
|
||||
dynamicChild.Attribute.Name = item.Name;
|
||||
dynamicChild.Attribute.Description = item.Description;
|
||||
item.UpdateDynamicChild(_configurationDataModel, dynamicChild);
|
||||
}
|
||||
else
|
||||
{
|
||||
item.AddDynamicChild(_configurationDataModel, key, new DataModelPropertyAttribute {Name = item.Name, Description = item.Description});
|
||||
}
|
||||
}
|
||||
|
||||
// Only list this profile in the data model if there's actually something to show
|
||||
ProfileConfigurationsDataModel root = ProfileConfigurationsDataModel.Instance;
|
||||
string profileKey = ProfileId.ToString("N");
|
||||
bool shouldBeInRoot = _configurationDataModelRegistered && items.Count > 0;
|
||||
bool isInRoot = root.TryGetDynamicChild(profileKey, out DynamicChild? profileChild) && profileChild.BaseValue == _configurationDataModel;
|
||||
|
||||
if (shouldBeInRoot && !isInRoot)
|
||||
{
|
||||
root.RemoveDynamicChildByKey(profileKey);
|
||||
root.AddDynamicChild(profileKey, _configurationDataModel, Name, "The configuration values of this profile");
|
||||
}
|
||||
else if (!shouldBeInRoot && isInRoot)
|
||||
{
|
||||
root.RemoveDynamicChildByKey(profileKey);
|
||||
}
|
||||
else if (isInRoot)
|
||||
{
|
||||
profileChild!.Attribute.Name = Name;
|
||||
}
|
||||
}
|
||||
|
||||
private void TrackConfiguration()
|
||||
{
|
||||
foreach (ConfigurationSection section in _trackedSections.Except(ConfigurationSections).ToList())
|
||||
{
|
||||
section.PropertyChanged -= ConfigurationOnPropertyChanged;
|
||||
section.Items.CollectionChanged -= ConfigurationOnCollectionChanged;
|
||||
_trackedSections.Remove(section);
|
||||
}
|
||||
|
||||
foreach (ConfigurationSection section in ConfigurationSections.Except(_trackedSections).ToList())
|
||||
{
|
||||
section.PropertyChanged += ConfigurationOnPropertyChanged;
|
||||
section.Items.CollectionChanged += ConfigurationOnCollectionChanged;
|
||||
_trackedSections.Add(section);
|
||||
}
|
||||
|
||||
List<IConfigurationItem> items = ConfigurationSections.SelectMany(s => s.Items).ToList();
|
||||
foreach (IConfigurationItem item in _trackedItems.Except(items).ToList())
|
||||
{
|
||||
item.PropertyChanged -= ConfigurationOnPropertyChanged;
|
||||
_trackedItems.Remove(item);
|
||||
}
|
||||
|
||||
foreach (IConfigurationItem item in items.Except(_trackedItems).ToList())
|
||||
{
|
||||
item.PropertyChanged += ConfigurationOnPropertyChanged;
|
||||
_trackedItems.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void ConfigurationSectionsOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
ConfigurationOnCollectionChanged(sender, e);
|
||||
}
|
||||
|
||||
private void ConfigurationOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
TrackConfiguration();
|
||||
UpdateConfigurationDataModel();
|
||||
OnConfigurationChanged();
|
||||
}
|
||||
|
||||
private void ConfigurationOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
UpdateConfigurationDataModel();
|
||||
OnConfigurationChanged();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<PluginFeature> GetFeatureDependencies()
|
||||
{
|
||||
@@ -303,29 +476,19 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
|
||||
EnableHotkey = Entity.ProfileConfiguration.EnableHotkey != null ? new Hotkey(Entity.ProfileConfiguration.EnableHotkey) : null;
|
||||
DisableHotkey = Entity.ProfileConfiguration.DisableHotkey != null ? new Hotkey(Entity.ProfileConfiguration.DisableHotkey) : null;
|
||||
|
||||
// Placeholder configuration sections
|
||||
IsConfigurable = Entity.ProfileConfiguration.IsConfigurable;
|
||||
ConfigurationSections.Clear();
|
||||
ConfigurationSections.Add(new ConfigurationSection(this)
|
||||
foreach (ConfigurationSectionEntity sectionEntity in Entity.ProfileConfiguration.ConfigurationSections)
|
||||
ConfigurationSections.Add(ConfigurationSection.Load(this, sectionEntity));
|
||||
|
||||
// Apply the values picked by the user, falling back to the defaults set by the author
|
||||
foreach (ConfigurationInputItem item in ConfigurationInputItems)
|
||||
{
|
||||
Name = "General"
|
||||
});
|
||||
ConfigurationSections.Add(new ConfigurationSection(this)
|
||||
{
|
||||
Name = "Other"
|
||||
});
|
||||
ConfigurationSections.Add(new ConfigurationSection(this)
|
||||
{
|
||||
Name = "Something else"
|
||||
});
|
||||
ConfigurationSections[0].Items.Add(new ConfigurationTextItem {Text = "This is a placeholder text item in the General section."});
|
||||
ConfigurationSections[0].Items.Add(new ConfigurationNumericItem {Name = "Numeric item", Description = "This one also has a description"});
|
||||
ConfigurationSections[0].Items.Add(new ConfigurationBooleanItem {Name = "Do the thing?", TrueText = "Absolutely", FalseText = "Nope"});
|
||||
ConfigurationSections[1].Items.Add(new ConfigurationTextItem {Text = "This is a placeholder text item in the Other section."});
|
||||
ConfigurationSections[2].Items.Add(new ConfigurationTextItem {Text = "This is a placeholder text item in the Something else section."});
|
||||
ConfigurationSections[2].Items.Add(new ConfigurationTextItem {Text = "This is another placeholder text item in the Something else section."});
|
||||
ConfigurationSections[2].Items.Add(new ConfigurationNumericItem {Name = "Numeric item", Description = "This one uses a slider", Minimum = 0, Maximum = 10, Slider = true});
|
||||
ConfigurationSections[2].Items.Add(new ConfigurationColorGradientItem {Name = "Color gradient"});
|
||||
ConfigurationSections[2].Items.Add(new ConfigurationSKColorItem {Name = "A simple color", Description = "Again with a description"});
|
||||
if (Entity.ProfileConfiguration.ConfigurationValues.TryGetValue(item.Id, out string? value))
|
||||
item.DeserializeValue(value);
|
||||
else
|
||||
item.ResetToDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -354,6 +517,16 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
|
||||
|
||||
if (!IsMissingModule)
|
||||
Entity.ProfileConfiguration.ModuleId = Module?.Id;
|
||||
|
||||
Entity.ProfileConfiguration.IsConfigurable = IsConfigurable;
|
||||
Entity.ProfileConfiguration.ConfigurationSections = ConfigurationSections.Select(s => s.Save()).ToList();
|
||||
Entity.ProfileConfiguration.ConfigurationValues = [];
|
||||
foreach (ConfigurationInputItem item in ConfigurationInputItems)
|
||||
{
|
||||
string? value = item.SerializeValue();
|
||||
if (value != null)
|
||||
Entity.ProfileConfiguration.ConfigurationValues[item.Id] = value;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using Artemis.Core.Modules;
|
||||
|
||||
namespace Artemis.Core;
|
||||
|
||||
/// <summary>
|
||||
/// The data model containing the configured values of all configurable profiles, each profile is a dynamic child
|
||||
/// keyed by its profile ID.
|
||||
/// </summary>
|
||||
internal class ProfileConfigurationsDataModel : DataModel
|
||||
{
|
||||
private ProfileConfigurationsDataModel()
|
||||
{
|
||||
Module = Constants.CorePluginFeature;
|
||||
DataModelDescription = new DataModelPropertyAttribute
|
||||
{
|
||||
Name = "Profile configuration",
|
||||
Description = "The values users configured on configurable profiles"
|
||||
};
|
||||
IsExpansion = true;
|
||||
}
|
||||
|
||||
public static ProfileConfigurationsDataModel Instance { get; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The data model containing the configured values of a single profile, each value is a dynamic child keyed by its
|
||||
/// configuration item ID.
|
||||
/// </summary>
|
||||
internal class ProfileConfigurationDataModel : DataModel;
|
||||
@@ -54,6 +54,12 @@ internal class ProfileService : IProfileService
|
||||
if (!ProfileCategories.Any())
|
||||
CreateDefaultProfileCategories();
|
||||
UpdateModules();
|
||||
|
||||
// Expose the values of configurable profiles in the data model
|
||||
if (DataModelStore.GetAll().All(r => r.DataModel != ProfileConfigurationsDataModel.Instance))
|
||||
DataModelStore.Add(ProfileConfigurationsDataModel.Instance);
|
||||
foreach (ProfileConfiguration profileConfiguration in ProfileCategories.SelectMany(c => c.ProfileConfigurations))
|
||||
profileConfiguration.AddConfigurationDataModel();
|
||||
}
|
||||
|
||||
public ProfileConfiguration? FocusProfile { get; set; }
|
||||
@@ -258,6 +264,7 @@ internal class ProfileService : IProfileService
|
||||
ProfileConfiguration configuration = new(category, name, icon);
|
||||
|
||||
category.AddProfileConfiguration(configuration, category.ProfileConfigurations.FirstOrDefault());
|
||||
configuration.AddConfigurationDataModel();
|
||||
SaveProfileCategory(category);
|
||||
|
||||
OnProfileAdded(new ProfileConfigurationEventArgs(configuration));
|
||||
@@ -273,6 +280,7 @@ internal class ProfileService : IProfileService
|
||||
|
||||
category.RemoveProfileConfiguration(profileConfiguration);
|
||||
category.Save();
|
||||
profileConfiguration.RemoveConfigurationDataModel();
|
||||
|
||||
_profileRepository.Remove(profileConfiguration.Entity);
|
||||
_profileCategoryRepository.Save(category.Entity);
|
||||
@@ -390,7 +398,17 @@ internal class ProfileService : IProfileService
|
||||
|
||||
// Assign a new GUID to make sure it is unique in case of a previous import of the same content
|
||||
if (makeUnique)
|
||||
profileEntity.UpdateGuid(Guid.NewGuid());
|
||||
{
|
||||
Guid newGuid = Guid.NewGuid();
|
||||
|
||||
// Data model paths pointing to the configuration values of this profile contain the profile ID, point those to the new ID
|
||||
string oldKey = profileEntity.Id.ToString("N");
|
||||
string newKey = newGuid.ToString("N");
|
||||
configurationEntity = ReplaceInEntity(configurationEntity, oldKey, newKey);
|
||||
profileEntity = ReplaceInEntity(profileEntity, oldKey, newKey);
|
||||
|
||||
profileEntity.UpdateGuid(newGuid);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the profile already exists and this one is not to be made unique, return the existing profile
|
||||
@@ -424,6 +442,7 @@ internal class ProfileService : IProfileService
|
||||
|
||||
profileConfiguration.Entity.ProfileConfiguration.ProfileId = profileEntity.Id;
|
||||
category.AddProfileConfiguration(profileConfiguration, target);
|
||||
profileConfiguration.AddConfigurationDataModel();
|
||||
|
||||
List<Module> modules = _pluginManagementService.GetFeaturesOfType<Module>();
|
||||
profileConfiguration.LoadModules(modules);
|
||||
@@ -432,6 +451,15 @@ internal class ProfileService : IProfileService
|
||||
return profileConfiguration;
|
||||
}
|
||||
|
||||
private static T ReplaceInEntity<T>(T entity, string oldValue, string newValue)
|
||||
{
|
||||
string json = CoreJson.Serialize(entity);
|
||||
if (!json.Contains(oldValue))
|
||||
return entity;
|
||||
|
||||
return CoreJson.Deserialize<T>(json.Replace(oldValue, newValue)) ?? entity;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AdaptProfile(Profile profile)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Artemis.Storage.Entities.Profile.Configuration;
|
||||
|
||||
public abstract class ConfigurationInputItemEntity : IConfigurationItemEntity
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The JSON-serialized default value as set by the author of the profile
|
||||
/// </summary>
|
||||
public string? DefaultValue { get; set; }
|
||||
|
||||
public List<ConfigurationDropdownValueEntity> DropdownValues { get; set; } = [];
|
||||
}
|
||||
|
||||
public class ConfigurationDropdownValueEntity
|
||||
{
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The JSON-serialized value of the dropdown option
|
||||
/// </summary>
|
||||
public string? Value { get; set; }
|
||||
}
|
||||
|
||||
public class ConfigurationStringItemEntity : ConfigurationInputItemEntity;
|
||||
|
||||
public class ConfigurationSKColorItemEntity : ConfigurationInputItemEntity;
|
||||
|
||||
public class ConfigurationColorGradientItemEntity : ConfigurationInputItemEntity;
|
||||
|
||||
public class ConfigurationBooleanItemEntity : ConfigurationInputItemEntity
|
||||
{
|
||||
public string TrueText { get; set; } = string.Empty;
|
||||
public string FalseText { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class ConfigurationNumericItemEntity : ConfigurationInputItemEntity
|
||||
{
|
||||
public float? Minimum { get; set; }
|
||||
public float? Maximum { get; set; }
|
||||
public bool Slider { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Artemis.Storage.Entities.Profile.Configuration;
|
||||
|
||||
public class ConfigurationSectionEntity
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public List<IConfigurationItemEntity> Items { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace Artemis.Storage.Entities.Profile.Configuration;
|
||||
|
||||
public class ConfigurationTextItemEntity : IConfigurationItemEntity
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Text { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Artemis.Storage.Entities.Profile.Configuration;
|
||||
|
||||
[JsonDerivedType(typeof(ConfigurationTextItemEntity), "Text")]
|
||||
[JsonDerivedType(typeof(ConfigurationStringItemEntity), "String")]
|
||||
[JsonDerivedType(typeof(ConfigurationBooleanItemEntity), "Boolean")]
|
||||
[JsonDerivedType(typeof(ConfigurationNumericItemEntity), "Numeric")]
|
||||
[JsonDerivedType(typeof(ConfigurationSKColorItemEntity), "SKColor")]
|
||||
[JsonDerivedType(typeof(ConfigurationColorGradientItemEntity), "ColorGradient")]
|
||||
public interface IConfigurationItemEntity
|
||||
{
|
||||
Guid Id { get; set; }
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Artemis.Storage.Entities.Profile.Configuration;
|
||||
using Artemis.Storage.Entities.Profile.Nodes;
|
||||
|
||||
namespace Artemis.Storage.Entities.Profile;
|
||||
@@ -25,5 +27,14 @@ public class ProfileConfigurationEntity
|
||||
public Guid ProfileId { get; set; }
|
||||
|
||||
public bool FadeInAndOut { get; set; }
|
||||
|
||||
public bool IsConfigurable { get; set; }
|
||||
public List<ConfigurationSectionEntity> ConfigurationSections { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// The JSON-serialized values the user picked for the configuration items, keyed by item ID
|
||||
/// </summary>
|
||||
public Dictionary<Guid, string> ConfigurationValues { get; set; } = [];
|
||||
|
||||
public int Version { get; set; }
|
||||
}
|
||||
+1
-10
@@ -1,14 +1,5 @@
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ConfigurationPanels.Items;
|
||||
|
||||
public class ConfigurationBooleanItemViewModel : ActivatableViewModelBase
|
||||
{
|
||||
public ConfigurationBooleanItemViewModel(ConfigurationBooleanItem item)
|
||||
{
|
||||
Item = item;
|
||||
}
|
||||
|
||||
public ConfigurationBooleanItem Item { get; }
|
||||
}
|
||||
public class ConfigurationBooleanItemViewModel(ConfigurationBooleanItem item) : ConfigurationInputItemViewModel<ConfigurationBooleanItem, bool>(item);
|
||||
+1
-10
@@ -1,14 +1,5 @@
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ConfigurationPanels.Items;
|
||||
|
||||
public class ConfigurationColorGradientItemViewModel : ActivatableViewModelBase
|
||||
{
|
||||
public ConfigurationColorGradientItemViewModel(ConfigurationColorGradientItem item)
|
||||
{
|
||||
Item = item;
|
||||
}
|
||||
|
||||
public ConfigurationColorGradientItem Item { get; }
|
||||
}
|
||||
public class ConfigurationColorGradientItemViewModel(ConfigurationColorGradientItem item) : ConfigurationInputItemViewModel<ConfigurationColorGradientItem, ColorGradient>(item);
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables.Fluent;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ConfigurationPanels.Items;
|
||||
|
||||
public abstract class ConfigurationInputItemViewModel<TItem, T> : ActivatableViewModelBase where TItem : ConfigurationInputItem<T>
|
||||
{
|
||||
protected ConfigurationInputItemViewModel(TItem item)
|
||||
{
|
||||
Item = item;
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(h => Item.PropertyChanged += h, h => Item.PropertyChanged -= h)
|
||||
.Select(e => e.EventArgs.PropertyName)
|
||||
.Where(p => p is nameof(ConfigurationInputItem<T>.Value) or nameof(ConfigurationInputItem<T>.DropdownValues))
|
||||
.Subscribe(_ =>
|
||||
{
|
||||
this.RaisePropertyChanged(nameof(HasDropdown));
|
||||
this.RaisePropertyChanged(nameof(SelectedDropdownValue));
|
||||
})
|
||||
.DisposeWith(d);
|
||||
});
|
||||
}
|
||||
|
||||
public TItem Item { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a boolean indicating whether the user picks the value from a dropdown instead of entering it.
|
||||
/// </summary>
|
||||
public bool HasDropdown => Item.DropdownValues.Count > 0;
|
||||
|
||||
public ConfigurationDropdownValue<T>? SelectedDropdownValue
|
||||
{
|
||||
get => Item.DropdownValues.FirstOrDefault(d => Equals(d.Value, Item.Value));
|
||||
set
|
||||
{
|
||||
if (value != null)
|
||||
Item.Value = value.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
-13
@@ -5,6 +5,8 @@
|
||||
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:behaviors="clr-namespace:Artemis.UI.Shared.Behaviors;assembly=Artemis.UI.Shared"
|
||||
xmlns:items="clr-namespace:Artemis.UI.Screens.ProfileEditor.ConfigurationPanels.Items"
|
||||
xmlns:core="clr-namespace:Artemis.Core;assembly=Artemis.Core"
|
||||
xmlns:system="clr-namespace:System;assembly=System.Runtime"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.ProfileEditor.ConfigurationPanels.Items.ConfigurationNumericItemView"
|
||||
x:DataType="items:ConfigurationNumericItemViewModel">
|
||||
@@ -16,17 +18,32 @@
|
||||
Text="{CompiledBinding Item.Description}"
|
||||
IsVisible="{CompiledBinding Item.Description, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
|
||||
<controls:NumberBox Width="150" IsVisible="{CompiledBinding !Item.Slider}">
|
||||
<Interaction.Behaviors>
|
||||
<behaviors:LostFocusNumberBoxBindingBehavior Value="{CompiledBinding Item.Value}" />
|
||||
</Interaction.Behaviors>
|
||||
</controls:NumberBox>
|
||||
<Slider Minimum="{CompiledBinding Item.Minimum}"
|
||||
Maximum="{CompiledBinding Item.Maximum}"
|
||||
Width="150"
|
||||
IsVisible="{CompiledBinding Item.Slider}"
|
||||
Value="{CompiledBinding Item.Value, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
<Panel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
|
||||
<Panel IsVisible="{CompiledBinding !HasDropdown}">
|
||||
<controls:NumberBox Width="150"
|
||||
Minimum="{CompiledBinding Item.Minimum, TargetNullValue={x:Static system:Double.MinValue}}"
|
||||
Maximum="{CompiledBinding Item.Maximum, TargetNullValue={x:Static system:Double.MaxValue}}"
|
||||
IsVisible="{CompiledBinding !Item.Slider}">
|
||||
<Interaction.Behaviors>
|
||||
<behaviors:LostFocusNumberBoxBindingBehavior Value="{CompiledBinding Item.Value}" />
|
||||
</Interaction.Behaviors>
|
||||
</controls:NumberBox>
|
||||
<Slider Minimum="{CompiledBinding Item.Minimum, TargetNullValue=0}"
|
||||
Maximum="{CompiledBinding Item.Maximum, TargetNullValue=100}"
|
||||
Width="150"
|
||||
IsVisible="{CompiledBinding Item.Slider}"
|
||||
Value="{CompiledBinding Item.Value, Mode=TwoWay}" />
|
||||
</Panel>
|
||||
<ComboBox ItemsSource="{CompiledBinding Item.DropdownValues}"
|
||||
SelectedItem="{CompiledBinding SelectedDropdownValue}"
|
||||
IsVisible="{CompiledBinding HasDropdown}"
|
||||
Width="150">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="core:ConfigurationDropdownValue">
|
||||
<TextBlock Text="{CompiledBinding DisplayName}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</Panel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
</UserControl>
|
||||
+1
-10
@@ -1,14 +1,5 @@
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ConfigurationPanels.Items;
|
||||
|
||||
public class ConfigurationNumericItemViewModel : ActivatableViewModelBase
|
||||
{
|
||||
public ConfigurationNumericItemViewModel(ConfigurationNumericItem item)
|
||||
{
|
||||
Item = item;
|
||||
}
|
||||
|
||||
public ConfigurationNumericItem Item { get; }
|
||||
}
|
||||
public class ConfigurationNumericItemViewModel(ConfigurationNumericItem item) : ConfigurationInputItemViewModel<ConfigurationNumericItem, Numeric>(item);
|
||||
+2
-10
@@ -1,14 +1,6 @@
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ConfigurationPanels.Items;
|
||||
|
||||
public class ConfigurationSKColorItemViewModel : ActivatableViewModelBase
|
||||
{
|
||||
public ConfigurationSKColorItemViewModel(ConfigurationSKColorItem item)
|
||||
{
|
||||
Item = item;
|
||||
}
|
||||
|
||||
public ConfigurationSKColorItem Item { get; }
|
||||
}
|
||||
public class ConfigurationSKColorItemViewModel(ConfigurationSKColorItem item) : ConfigurationInputItemViewModel<ConfigurationSKColorItem, SKColor>(item);
|
||||
+13
-2
@@ -3,6 +3,7 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:items="clr-namespace:Artemis.UI.Screens.ProfileEditor.ConfigurationPanels.Items"
|
||||
xmlns:core="clr-namespace:Artemis.Core;assembly=Artemis.Core"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.ProfileEditor.ConfigurationPanels.Items.ConfigurationStringItemView"
|
||||
x:DataType="items:ConfigurationStringItemViewModel">
|
||||
@@ -15,7 +16,17 @@
|
||||
IsVisible="{CompiledBinding Item.Description, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBox Text="{CompiledBinding Item.Value, Mode=TwoWay}" MinWidth="150" />
|
||||
<TextBox Text="{CompiledBinding Item.Value, Mode=TwoWay}" MinWidth="150" IsVisible="{CompiledBinding !HasDropdown}" />
|
||||
<ComboBox ItemsSource="{CompiledBinding Item.DropdownValues}"
|
||||
SelectedItem="{CompiledBinding SelectedDropdownValue}"
|
||||
IsVisible="{CompiledBinding HasDropdown}"
|
||||
MinWidth="150">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="core:ConfigurationDropdownValue">
|
||||
<TextBlock Text="{CompiledBinding DisplayName}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
</UserControl>
|
||||
+1
-10
@@ -1,14 +1,5 @@
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ConfigurationPanels.Items;
|
||||
|
||||
public class ConfigurationStringItemViewModel : ActivatableViewModelBase
|
||||
{
|
||||
public ConfigurationStringItemViewModel(ConfigurationStringItem item)
|
||||
{
|
||||
Item = item;
|
||||
}
|
||||
|
||||
public ConfigurationStringItem Item { get; }
|
||||
}
|
||||
public class ConfigurationStringItemViewModel(ConfigurationStringItem item) : ConfigurationInputItemViewModel<ConfigurationStringItem, string>(item);
|
||||
+36
-19
@@ -1,33 +1,50 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Reactive.Disposables.Fluent;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Screens.ProfileEditor.ConfigurationPanels.Items;
|
||||
using Artemis.UI.Shared;
|
||||
using DynamicData;
|
||||
using DynamicData.Binding;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.ConfigurationPanels.Section;
|
||||
|
||||
public class ConfigurationSectionViewModel : ActivatableViewModelBase
|
||||
public partial class ConfigurationSectionViewModel : ActivatableViewModelBase
|
||||
{
|
||||
public ConfigurationSection ConfigurationSection { get; }
|
||||
public ObservableCollection<ActivatableViewModelBase> Items { get; }
|
||||
[Notify] private ReadOnlyObservableCollection<ActivatableViewModelBase>? _items;
|
||||
|
||||
public ConfigurationSectionViewModel(ConfigurationSection configurationSection)
|
||||
{
|
||||
ConfigurationSection = configurationSection;
|
||||
Items = [];
|
||||
foreach (IConfigurationItem item in ConfigurationSection.Items)
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
if (item is ConfigurationTextItem textItem)
|
||||
Items.Add(new ConfigurationTextItemViewModel(textItem));
|
||||
else if (item is ConfigurationStringItem stringItem)
|
||||
Items.Add(new ConfigurationStringItemViewModel(stringItem));
|
||||
else if (item is ConfigurationBooleanItem booleanItem)
|
||||
Items.Add(new ConfigurationBooleanItemViewModel(booleanItem));
|
||||
else if (item is ConfigurationNumericItem numberItem)
|
||||
Items.Add(new ConfigurationNumericItemViewModel(numberItem));
|
||||
else if (item is ConfigurationSKColorItem colorItem)
|
||||
Items.Add(new ConfigurationSKColorItemViewModel(colorItem));
|
||||
else if (item is ConfigurationColorGradientItem colorGradientItem)
|
||||
Items.Add(new ConfigurationColorGradientItemViewModel(colorGradientItem));
|
||||
}
|
||||
ConfigurationSection.Items
|
||||
.ToObservableChangeSet()
|
||||
.Transform(CreateViewModelForItem)
|
||||
.Bind(out ReadOnlyObservableCollection<ActivatableViewModelBase> items)
|
||||
.Subscribe()
|
||||
.DisposeWith(d);
|
||||
|
||||
Items = items;
|
||||
});
|
||||
}
|
||||
|
||||
public ConfigurationSection ConfigurationSection { get; }
|
||||
|
||||
private static ActivatableViewModelBase CreateViewModelForItem(IConfigurationItem item)
|
||||
{
|
||||
return item switch
|
||||
{
|
||||
ConfigurationTextItem textItem => new ConfigurationTextItemViewModel(textItem),
|
||||
ConfigurationStringItem stringItem => new ConfigurationStringItemViewModel(stringItem),
|
||||
ConfigurationBooleanItem booleanItem => new ConfigurationBooleanItemViewModel(booleanItem),
|
||||
ConfigurationNumericItem numberItem => new ConfigurationNumericItemViewModel(numberItem),
|
||||
ConfigurationSKColorItem colorItem => new ConfigurationSKColorItemViewModel(colorItem),
|
||||
ConfigurationColorGradientItem colorGradientItem => new ConfigurationColorGradientItemViewModel(colorGradientItem),
|
||||
_ => throw new ArgumentException($"Unknown configuration item type: {item.GetType()}")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables.Fluent;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
@@ -10,7 +12,9 @@ using Artemis.UI.Screens.ProfileEditor.ConfigurationPanels.Section;
|
||||
using Artemis.UI.Shared.Routing;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using DynamicData;
|
||||
using DynamicData.Binding;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor;
|
||||
|
||||
@@ -18,9 +22,9 @@ public partial class ConfigureProfileViewModel : RoutableScreen<ProfileViewModel
|
||||
{
|
||||
private readonly IProfileService _profileService;
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private readonly SourceList<ConfigurationSection> _configurationSections;
|
||||
|
||||
[Notify] private ProfileConfiguration? _profileConfiguration;
|
||||
[Notify] private ReadOnlyObservableCollection<ConfigurationSectionViewModel>? _sections;
|
||||
|
||||
public ConfigureProfileViewModel(IProfileService profileService, IProfileEditorService profileEditorService, PreviewViewModel previewViewModel,
|
||||
Func<ConfigurationSection, ConfigurationSectionViewModel> getConfigurationSectionViewModel)
|
||||
@@ -31,17 +35,31 @@ public partial class ConfigureProfileViewModel : RoutableScreen<ProfileViewModel
|
||||
|
||||
PreviewViewModel = previewViewModel;
|
||||
|
||||
_configurationSections = new SourceList<ConfigurationSection>();
|
||||
_configurationSections.Connect()
|
||||
.Transform(getConfigurationSectionViewModel)
|
||||
.Bind(out ReadOnlyObservableCollection<ConfigurationSectionViewModel> sections)
|
||||
.Subscribe();
|
||||
|
||||
Sections = sections;
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
this.WhenAnyValue(vm => vm.ProfileConfiguration)
|
||||
.Select(p => p?.ConfigurationSections.ToObservableChangeSet() ?? Observable.Empty<IChangeSet<ConfigurationSection>>())
|
||||
.Switch()
|
||||
.Transform(getConfigurationSectionViewModel)
|
||||
.Bind(out ReadOnlyObservableCollection<ConfigurationSectionViewModel> sections)
|
||||
.Subscribe()
|
||||
.DisposeWith(d);
|
||||
|
||||
Sections = sections;
|
||||
|
||||
this.WhenAnyValue(vm => vm.ProfileConfiguration)
|
||||
.Select(p => p != null
|
||||
? Observable.FromEventPattern(h => p.ConfigurationChanged += h, h => p.ConfigurationChanged -= h).Select(_ => p)
|
||||
: Observable.Empty<ProfileConfiguration>())
|
||||
.Switch()
|
||||
.Throttle(TimeSpan.FromMilliseconds(500))
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.Subscribe(p => _profileService.SaveProfileCategory(p.Category))
|
||||
.DisposeWith(d);
|
||||
});
|
||||
}
|
||||
|
||||
public PreviewViewModel PreviewViewModel { get; }
|
||||
public ReadOnlyObservableCollection<ConfigurationSectionViewModel> Sections { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task OnNavigating(ProfileViewModelParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
|
||||
@@ -57,16 +75,15 @@ public partial class ConfigureProfileViewModel : RoutableScreen<ProfileViewModel
|
||||
|
||||
await _profileEditorService.ChangeCurrentProfileConfiguration(profileConfiguration);
|
||||
ProfileConfiguration = profileConfiguration;
|
||||
_configurationSections.Edit(editableSections =>
|
||||
{
|
||||
editableSections.Clear();
|
||||
editableSections.AddRange(profileConfiguration.ConfigurationSections);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task OnClosing(NavigationArguments args)
|
||||
{
|
||||
// Don't lose changes that are still waiting on the throttled save
|
||||
if (ProfileConfiguration != null)
|
||||
_profileService.SaveProfileCategory(ProfileConfiguration.Category);
|
||||
|
||||
if (!args.Path.StartsWith("profile"))
|
||||
{
|
||||
ProfileConfiguration = null;
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="0" Grid.Column="1">
|
||||
<TextBlock Margin="0 14 0 4">Default:</TextBlock>
|
||||
<ToggleSwitch IsChecked="{CompiledBinding BooleanItem.Value}"
|
||||
<ToggleSwitch IsChecked="{CompiledBinding BooleanItem.DefaultValue}"
|
||||
OnContent="{CompiledBinding BooleanItem.TrueText}"
|
||||
OffContent="{CompiledBinding BooleanItem.FalseText}"
|
||||
Width="150"
|
||||
|
||||
+3
-2
@@ -2,7 +2,8 @@
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.DesignPanels.Items;
|
||||
|
||||
public class ConfigurationBooleanItemDesignViewModel(ConfigurationBooleanItem item, ConfigurationSection configurationSection) : ConfigurationItemDesignViewModel(item, configurationSection)
|
||||
public class ConfigurationBooleanItemDesignViewModel(ConfigurationBooleanItem item, ConfigurationSection configurationSection)
|
||||
: ConfigurationInputItemDesignViewModel<ConfigurationBooleanItem, bool>(item, configurationSection)
|
||||
{
|
||||
public ConfigurationBooleanItem BooleanItem { get; } = item;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock Margin="0 14 0 4">Default:</TextBlock>
|
||||
<gradientPicker:GradientPickerButton Width="150"
|
||||
ColorGradient="{CompiledBinding GradientItem.Value}"
|
||||
ColorGradient="{CompiledBinding GradientItem.DefaultValue}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
<Button Grid.Row="0" Grid.Column="1" ToolTip.Tip="Remove" HorizontalAlignment="Right" VerticalAlignment="Top" Classes="icon-button-outlined" Command="{CompiledBinding RemoveItem}">
|
||||
|
||||
+4
-12
@@ -2,16 +2,8 @@
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.DesignPanels.Items;
|
||||
|
||||
public class ConfigurationColorGradientItemDesignViewModel : ConfigurationItemDesignViewModel
|
||||
public class ConfigurationColorGradientItemDesignViewModel(ConfigurationColorGradientItem item, ConfigurationSection configurationSection)
|
||||
: ConfigurationInputItemDesignViewModel<ConfigurationColorGradientItem, ColorGradient>(item, configurationSection)
|
||||
{
|
||||
public ConfigurationColorGradientItemDesignViewModel(ConfigurationColorGradientItem item, ConfigurationSection configurationSection) : base(item, configurationSection)
|
||||
{
|
||||
// The gradient picker doesn't handle null values well
|
||||
item.DefaultValue ??= ColorGradient.GetUnicornBarf();
|
||||
item.Value ??= ColorGradient.GetUnicornBarf();
|
||||
|
||||
GradientItem = item;
|
||||
}
|
||||
|
||||
public ConfigurationColorGradientItem GradientItem { get; }
|
||||
}
|
||||
public ConfigurationColorGradientItem GradientItem { get; } = item;
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Disposables.Fluent;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.Core;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.DesignPanels.Items;
|
||||
|
||||
public abstract class ConfigurationInputItemDesignViewModel<TItem, T> : ConfigurationItemDesignViewModel where TItem : ConfigurationInputItem<T>
|
||||
{
|
||||
protected ConfigurationInputItemDesignViewModel(TItem item, ConfigurationSection configurationSection) : base(item, configurationSection)
|
||||
{
|
||||
InputItem = item;
|
||||
RemoveDropdownValue = ReactiveCommand.Create<ConfigurationDropdownValue<T>>(ExecuteRemoveDropdownValue);
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
IObservable<string?> propertyChanged = Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
|
||||
h => InputItem.PropertyChanged += h,
|
||||
h => InputItem.PropertyChanged -= h)
|
||||
.Select(e => e.EventArgs.PropertyName);
|
||||
|
||||
// While designing, the value follows the default so that the preview shows what users will initially get
|
||||
propertyChanged.Where(p => p == nameof(ConfigurationInputItem<T>.DefaultValue))
|
||||
.Subscribe(_ =>
|
||||
{
|
||||
InputItem.ResetToDefault();
|
||||
this.RaisePropertyChanged(nameof(SelectedDefaultDropdownValue));
|
||||
})
|
||||
.DisposeWith(d);
|
||||
|
||||
propertyChanged.Where(p => p == nameof(ConfigurationInputItem<T>.DropdownValues))
|
||||
.Subscribe(_ =>
|
||||
{
|
||||
this.RaisePropertyChanged(nameof(HasDropdown));
|
||||
this.RaisePropertyChanged(nameof(SelectedDefaultDropdownValue));
|
||||
})
|
||||
.DisposeWith(d);
|
||||
});
|
||||
}
|
||||
|
||||
public TItem InputItem { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a boolean indicating whether users pick the value from a dropdown instead of entering it.
|
||||
/// </summary>
|
||||
public bool HasDropdown => InputItem.DropdownValues.Count > 0;
|
||||
|
||||
public ConfigurationDropdownValue<T>? SelectedDefaultDropdownValue
|
||||
{
|
||||
get => InputItem.DropdownValues.FirstOrDefault(d => Equals(d.Value, InputItem.DefaultValue));
|
||||
set
|
||||
{
|
||||
if (value != null)
|
||||
InputItem.DefaultValue = value.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public ReactiveCommand<ConfigurationDropdownValue<T>, Unit> RemoveDropdownValue { get; }
|
||||
|
||||
public void AddDropdownValue()
|
||||
{
|
||||
int number = InputItem.DropdownValues.Count + 1;
|
||||
ConfigurationDropdownValue<T> dropdownValue = new() {DisplayName = $"Option {number}", Value = CreateDropdownValue(number)};
|
||||
InputItem.DropdownValues.Add(dropdownValue);
|
||||
|
||||
// The default should always be one of the options
|
||||
if (SelectedDefaultDropdownValue == null)
|
||||
InputItem.DefaultValue = dropdownValue.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the value of a newly added dropdown option.
|
||||
/// </summary>
|
||||
/// <param name="number">The number of the new option, starting at 1.</param>
|
||||
protected virtual T CreateDropdownValue(int number)
|
||||
{
|
||||
throw new NotSupportedException($"{GetType().Name} does not support dropdown values");
|
||||
}
|
||||
|
||||
private void ExecuteRemoveDropdownValue(ConfigurationDropdownValue<T> dropdownValue)
|
||||
{
|
||||
InputItem.DropdownValues.Remove(dropdownValue);
|
||||
if (HasDropdown && SelectedDefaultDropdownValue == null)
|
||||
InputItem.DefaultValue = InputItem.DropdownValues[0].Value;
|
||||
}
|
||||
}
|
||||
+71
-13
@@ -6,14 +6,21 @@
|
||||
xmlns:behaviors="clr-namespace:Artemis.UI.Shared.Behaviors;assembly=Artemis.UI.Shared"
|
||||
xmlns:items="clr-namespace:Artemis.UI.Screens.ProfileEditor.DesignPanels.Items"
|
||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:core="clr-namespace:Artemis.Core;assembly=Artemis.Core"
|
||||
xmlns:system="clr-namespace:System;assembly=System.Runtime"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.ProfileEditor.DesignPanels.Items.ConfigurationNumericItemDesignView"
|
||||
x:DataType="items:ConfigurationNumericItemDesignViewModel">
|
||||
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto" ColumnSpacing="5">
|
||||
<Grid RowDefinitions="*,Auto" ColumnDefinitions="*,Auto" ColumnSpacing="5">
|
||||
<StackPanel Grid.Column="0" VerticalAlignment="Center" Spacing="5">
|
||||
<TextBox Text="{CompiledBinding NumericItem.Name}" Watermark="Name" />
|
||||
<TextBox Text="{CompiledBinding NumericItem.Description}" Watermark="Description" />
|
||||
<Grid ColumnDefinitions="*,*" ColumnSpacing="5" IsVisible="{CompiledBinding !HasDropdown}">
|
||||
<controls:NumberBox Grid.Column="0" Value="{CompiledBinding Minimum}" PlaceholderText="No minimum" ToolTip.Tip="Minimum" />
|
||||
<controls:NumberBox Grid.Column="1" Value="{CompiledBinding Maximum}" PlaceholderText="No maximum" ToolTip.Tip="Maximum" />
|
||||
</Grid>
|
||||
<ToggleSwitch IsChecked="{CompiledBinding NumericItem.Slider}"
|
||||
IsVisible="{CompiledBinding !HasDropdown}"
|
||||
OnContent="Slider"
|
||||
OffContent="Number box"
|
||||
MinWidth="0"
|
||||
@@ -21,20 +28,71 @@
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Top">
|
||||
<TextBlock Margin="0 14 0 4">Default:</TextBlock>
|
||||
<controls:NumberBox Width="150" IsVisible="{CompiledBinding !NumericItem.Slider}">
|
||||
<Interaction.Behaviors>
|
||||
<behaviors:LostFocusNumberBoxBindingBehavior Value="{CompiledBinding NumericItem.Value}" />
|
||||
</Interaction.Behaviors>
|
||||
</controls:NumberBox>
|
||||
<Slider Minimum="{CompiledBinding NumericItem.Minimum}"
|
||||
Maximum="{CompiledBinding NumericItem.Maximum}"
|
||||
Width="150"
|
||||
Margin="0 -5 0 0"
|
||||
IsVisible="{CompiledBinding NumericItem.Slider}"
|
||||
Value="{CompiledBinding NumericItem.Value, Mode=TwoWay}" />
|
||||
<Panel IsVisible="{CompiledBinding !HasDropdown}">
|
||||
<controls:NumberBox Width="150"
|
||||
Minimum="{CompiledBinding NumericItem.Minimum, TargetNullValue={x:Static system:Double.MinValue}}"
|
||||
Maximum="{CompiledBinding NumericItem.Maximum, TargetNullValue={x:Static system:Double.MaxValue}}"
|
||||
IsVisible="{CompiledBinding !NumericItem.Slider}">
|
||||
<Interaction.Behaviors>
|
||||
<behaviors:LostFocusNumberBoxBindingBehavior Value="{CompiledBinding NumericItem.DefaultValue}" />
|
||||
</Interaction.Behaviors>
|
||||
</controls:NumberBox>
|
||||
<Slider Minimum="{CompiledBinding NumericItem.Minimum, TargetNullValue=0}"
|
||||
Maximum="{CompiledBinding NumericItem.Maximum, TargetNullValue=100}"
|
||||
Width="150"
|
||||
Margin="0 -5 0 0"
|
||||
IsVisible="{CompiledBinding NumericItem.Slider}"
|
||||
Value="{CompiledBinding NumericItem.DefaultValue, Mode=TwoWay}" />
|
||||
</Panel>
|
||||
<ComboBox ItemsSource="{CompiledBinding NumericItem.DropdownValues}"
|
||||
SelectedItem="{CompiledBinding SelectedDefaultDropdownValue}"
|
||||
IsVisible="{CompiledBinding HasDropdown}"
|
||||
Width="150">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="core:ConfigurationDropdownValue">
|
||||
<TextBlock Text="{CompiledBinding DisplayName}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<Button Grid.Row="0" Grid.Column="1" ToolTip.Tip="Remove" HorizontalAlignment="Right" VerticalAlignment="Top" Classes="icon-button-outlined" Command="{CompiledBinding RemoveItem}">
|
||||
<avalonia:MaterialIcon Kind="Trash" />
|
||||
</Button>
|
||||
|
||||
<StackPanel Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Margin="0 8 0 0" Spacing="5">
|
||||
<TextBlock Classes="subtitle" IsVisible="{CompiledBinding HasDropdown}">Dropdown options</TextBlock>
|
||||
<ItemsControl ItemsSource="{CompiledBinding NumericItem.DropdownValues}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Spacing="5" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="core:ConfigurationDropdownValue">
|
||||
<Grid ColumnDefinitions="*,*,Auto" ColumnSpacing="5">
|
||||
<TextBox Grid.Column="0" Text="{CompiledBinding DisplayName}" Watermark="Display name" />
|
||||
<controls:NumberBox Grid.Column="1" ToolTip.Tip="Value">
|
||||
<Interaction.Behaviors>
|
||||
<behaviors:LostFocusNumberBoxBindingBehavior Value="{Binding Value}" />
|
||||
</Interaction.Behaviors>
|
||||
</controls:NumberBox>
|
||||
<Button Grid.Column="2"
|
||||
ToolTip.Tip="Remove option"
|
||||
Classes="icon-button"
|
||||
Command="{CompiledBinding $parent[ItemsControl].((items:ConfigurationNumericItemDesignViewModel)DataContext).RemoveDropdownValue}"
|
||||
CommandParameter="{CompiledBinding}">
|
||||
<avalonia:MaterialIcon Kind="Close" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<Button Command="{CompiledBinding AddDropdownValue}" ToolTip.Tip="Let users pick the value from a dropdown instead of entering a number">
|
||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||
<avalonia:MaterialIcon Kind="FormatListBulleted" />
|
||||
<TextBlock>Add dropdown option</TextBlock>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
</UserControl>
|
||||
+46
-3
@@ -1,8 +1,51 @@
|
||||
using Artemis.Core;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Reactive.Disposables.Fluent;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.Core;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.DesignPanels.Items;
|
||||
|
||||
public class ConfigurationNumericItemDesignViewModel(ConfigurationNumericItem item, ConfigurationSection configurationSection) : ConfigurationItemDesignViewModel(item, configurationSection)
|
||||
public class ConfigurationNumericItemDesignViewModel : ConfigurationInputItemDesignViewModel<ConfigurationNumericItem, Numeric>
|
||||
{
|
||||
public ConfigurationNumericItem NumericItem { get; } = item;
|
||||
public ConfigurationNumericItemDesignViewModel(ConfigurationNumericItem item, ConfigurationSection configurationSection) : base(item, configurationSection)
|
||||
{
|
||||
NumericItem = item;
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
Observable.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(h => item.PropertyChanged += h, h => item.PropertyChanged -= h)
|
||||
.Select(e => e.EventArgs.PropertyName)
|
||||
.Subscribe(p =>
|
||||
{
|
||||
if (p == nameof(ConfigurationNumericItem.Minimum))
|
||||
this.RaisePropertyChanged(nameof(Minimum));
|
||||
else if (p == nameof(ConfigurationNumericItem.Maximum))
|
||||
this.RaisePropertyChanged(nameof(Maximum));
|
||||
})
|
||||
.DisposeWith(d);
|
||||
});
|
||||
}
|
||||
|
||||
public ConfigurationNumericItem NumericItem { get; }
|
||||
|
||||
// A number box shows NaN as empty, use that to represent no minimum/maximum
|
||||
public double Minimum
|
||||
{
|
||||
get => NumericItem.Minimum ?? double.NaN;
|
||||
set => NumericItem.Minimum = double.IsNaN(value) ? null : value;
|
||||
}
|
||||
|
||||
public double Maximum
|
||||
{
|
||||
get => NumericItem.Maximum ?? double.NaN;
|
||||
set => NumericItem.Maximum = double.IsNaN(value) ? null : value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Numeric CreateDropdownValue(int number)
|
||||
{
|
||||
return number;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock Margin="0 14 0 4">Default:</TextBlock>
|
||||
<controls:ColorPickerButton Color="{CompiledBinding ColorItem.Value, Converter={StaticResource SKColorToColorConverter}}"
|
||||
<controls:ColorPickerButton Color="{CompiledBinding ColorItem.DefaultValue, Converter={StaticResource SKColorToColorConverter}}"
|
||||
Width="150"
|
||||
ShowAcceptDismissButtons="False"
|
||||
FlyoutPlacement="Right" />
|
||||
|
||||
+4
-2
@@ -1,8 +1,10 @@
|
||||
using Artemis.Core;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.DesignPanels.Items;
|
||||
|
||||
public class ConfigurationSKColorItemDesignViewModel(ConfigurationSKColorItem item, ConfigurationSection configurationSection) : ConfigurationItemDesignViewModel(item, configurationSection)
|
||||
public class ConfigurationSKColorItemDesignViewModel(ConfigurationSKColorItem item, ConfigurationSection configurationSection)
|
||||
: ConfigurationInputItemDesignViewModel<ConfigurationSKColorItem, SKColor>(item, configurationSection)
|
||||
{
|
||||
public ConfigurationSKColorItem ColorItem { get; } = item;
|
||||
}
|
||||
}
|
||||
+46
-3
@@ -4,21 +4,64 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:items="clr-namespace:Artemis.UI.Screens.ProfileEditor.DesignPanels.Items"
|
||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:core="clr-namespace:Artemis.Core;assembly=Artemis.Core"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.ProfileEditor.DesignPanels.Items.ConfigurationStringItemDesignView"
|
||||
x:DataType="items:ConfigurationStringItemDesignViewModel">
|
||||
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto" ColumnSpacing="5">
|
||||
<Grid RowDefinitions="*,Auto" ColumnDefinitions="*,Auto" ColumnSpacing="5">
|
||||
<StackPanel Grid.Column="0" VerticalAlignment="Center" Spacing="5">
|
||||
<TextBox Text="{CompiledBinding StringItem.Name}" Watermark="Name" />
|
||||
<TextBox Text="{CompiledBinding StringItem.Description}" Watermark="Description" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock Margin="0 14 0 4">Default:</TextBlock>
|
||||
<TextBox Text="{CompiledBinding StringItem.Value, Mode=TwoWay}" MinWidth="150" />
|
||||
<TextBox Text="{CompiledBinding StringItem.DefaultValue, Mode=TwoWay}" MinWidth="150" IsVisible="{CompiledBinding !HasDropdown}" />
|
||||
<ComboBox ItemsSource="{CompiledBinding StringItem.DropdownValues}"
|
||||
SelectedItem="{CompiledBinding SelectedDefaultDropdownValue}"
|
||||
IsVisible="{CompiledBinding HasDropdown}"
|
||||
MinWidth="150">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="core:ConfigurationDropdownValue">
|
||||
<TextBlock Text="{CompiledBinding DisplayName}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Row="0" Grid.Column="1" ToolTip.Tip="Remove" HorizontalAlignment="Right" VerticalAlignment="Top" Classes="icon-button-outlined" Command="{CompiledBinding RemoveItem}">
|
||||
<avalonia:MaterialIcon Kind="Trash" />
|
||||
</Button>
|
||||
|
||||
<StackPanel Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Margin="0 8 0 0" Spacing="5">
|
||||
<TextBlock Classes="subtitle" IsVisible="{CompiledBinding HasDropdown}">Dropdown options</TextBlock>
|
||||
<ItemsControl ItemsSource="{CompiledBinding StringItem.DropdownValues}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Spacing="5" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="core:ConfigurationDropdownValue">
|
||||
<Grid ColumnDefinitions="*,*,Auto" ColumnSpacing="5">
|
||||
<TextBox Grid.Column="0" Text="{CompiledBinding DisplayName}" Watermark="Display name" />
|
||||
<TextBox Grid.Column="1" Text="{Binding Value}" Watermark="Value" />
|
||||
<Button Grid.Column="2"
|
||||
ToolTip.Tip="Remove option"
|
||||
Classes="icon-button"
|
||||
Command="{CompiledBinding $parent[ItemsControl].((items:ConfigurationStringItemDesignViewModel)DataContext).RemoveDropdownValue}"
|
||||
CommandParameter="{CompiledBinding}">
|
||||
<avalonia:MaterialIcon Kind="Close" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<Button Command="{CompiledBinding AddDropdownValue}" ToolTip.Tip="Let users pick the value from a dropdown instead of typing it">
|
||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||
<avalonia:MaterialIcon Kind="FormatListBulleted" />
|
||||
<TextBlock>Add dropdown option</TextBlock>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
</UserControl>
|
||||
+9
-2
@@ -2,7 +2,14 @@
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.DesignPanels.Items;
|
||||
|
||||
public class ConfigurationStringItemDesignViewModel(ConfigurationStringItem item, ConfigurationSection configurationSection) : ConfigurationItemDesignViewModel(item, configurationSection)
|
||||
public class ConfigurationStringItemDesignViewModel(ConfigurationStringItem item, ConfigurationSection configurationSection)
|
||||
: ConfigurationInputItemDesignViewModel<ConfigurationStringItem, string>(item, configurationSection)
|
||||
{
|
||||
public ConfigurationStringItem StringItem { get; } = item;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override string CreateDropdownValue(int number)
|
||||
{
|
||||
return $"Option {number}";
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
using Artemis.Core;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Shared;
|
||||
|
||||
namespace Artemis.UI.Screens.ProfileEditor.DesignPanels.Items;
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ public partial class AddConfigurationItemDesignView : ReactiveUserControl<AddCon
|
||||
|
||||
private void SelectingItemsControl_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (e.AddedItems[0] is not ListBoxItem item) return;
|
||||
if (e.AddedItems.Count == 0 || e.AddedItems[0] is not ListBoxItem item) return;
|
||||
|
||||
if (item == BooleanInputItem)
|
||||
ViewModel?.AddItem(new ConfigurationBooleanItem {Name = "New Boolean"});
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ public partial class ConfigurationSectionDesignViewModel : ActivatableViewModelB
|
||||
|
||||
public ConfigurationSection ConfigurationSection { get; }
|
||||
|
||||
public async Task RemoveSection()
|
||||
public void RemoveSection()
|
||||
{
|
||||
ConfigurationSection.ProfileConfiguration.ConfigurationSections.Remove(ConfigurationSection);
|
||||
}
|
||||
|
||||
@@ -46,6 +46,16 @@ public partial class DesignProfileViewModel : RoutableScreen<ProfileViewModelPar
|
||||
.DisposeWith(d);
|
||||
|
||||
Sections = sections;
|
||||
|
||||
this.WhenAnyValue(vm => vm.ProfileConfiguration)
|
||||
.Select(p => p != null
|
||||
? Observable.FromEventPattern(h => p.ConfigurationChanged += h, h => p.ConfigurationChanged -= h).Select(_ => p)
|
||||
: Observable.Empty<ProfileConfiguration>())
|
||||
.Switch()
|
||||
.Throttle(TimeSpan.FromMilliseconds(500))
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.Subscribe(p => _profileService.SaveProfileCategory(p.Category))
|
||||
.DisposeWith(d);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -70,6 +80,10 @@ public partial class DesignProfileViewModel : RoutableScreen<ProfileViewModelPar
|
||||
/// <inheritdoc />
|
||||
public override async Task OnClosing(NavigationArguments args)
|
||||
{
|
||||
// Don't lose changes that are still waiting on the throttled save
|
||||
if (ProfileConfiguration != null)
|
||||
_profileService.SaveProfileCategory(ProfileConfiguration.Category);
|
||||
|
||||
if (!args.Path.StartsWith("profile"))
|
||||
{
|
||||
ProfileConfiguration = null;
|
||||
@@ -77,7 +91,7 @@ public partial class DesignProfileViewModel : RoutableScreen<ProfileViewModelPar
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AddSection()
|
||||
public void AddSection()
|
||||
{
|
||||
ProfileConfiguration?.ConfigurationSections.Add(new ConfigurationSection(ProfileConfiguration)
|
||||
{
|
||||
|
||||
+7
-1
@@ -44,7 +44,10 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler
|
||||
{
|
||||
ProfileConfiguration overwritten = await _profileService.ImportProfile(stream, existing.Category, true, false, null, existing);
|
||||
overwritten.Name = existing.Name;
|
||||
|
||||
// Keep whatever the user configured in the previous version
|
||||
overwritten.CopyConfigurationValues(existing);
|
||||
_profileService.SaveProfileCategory(overwritten.Category);
|
||||
|
||||
// Update the release
|
||||
installedEntry.SetMetadata("ProfileId", overwritten.ProfileId);
|
||||
UpdateRelease(installedEntry, release);
|
||||
@@ -62,6 +65,9 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler
|
||||
// Add the profile as a fresh import
|
||||
ProfileCategory category = _profileService.ProfileCategories.FirstOrDefault(c => c.Name == "Workshop") ?? _profileService.CreateProfileCategory("Workshop", true);
|
||||
ProfileConfiguration imported = await _profileService.ImportProfile(stream, category, true, true, null);
|
||||
// The export contains the values the author had configured, start with the defaults the author picked instead
|
||||
imported.ResetConfigurationToDefaults();
|
||||
_profileService.SaveProfileCategory(category);
|
||||
installedEntry.SetMetadata("ProfileId", imported.ProfileId);
|
||||
|
||||
// Update the release and return the profile configuration
|
||||
|
||||
@@ -28,6 +28,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
Directory.Packages.props = Directory.Packages.props
|
||||
Directory.Build.props = Directory.Build.props
|
||||
Directory.Build.targets = Directory.Build.targets
|
||||
CLAUDE.md = CLAUDE.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Artemis.Storage.Legacy", "Artemis.Storage.Legacy\Artemis.Storage.Legacy.csproj", "{D7B0966D-774A-40E4-9455-00C1261ACB6A}"
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Artemis
|
||||
|
||||
Artemis is a desktop app that adds highly configurable RGB lighting support for games/peripherals (RGB.NET-based). Cross-platform via Avalonia, with OS-specific host projects for Windows/Linux/macOS.
|
||||
|
||||
## Solution layout
|
||||
|
||||
- `Artemis.Core` — domain/business logic: profiles, plugins, devices, services (DI-registered), color science, layouts. No UI dependency.
|
||||
- `Artemis.Storage` — EF Core + SQLite persistence (`ArtemisDbContext`), entities and repositories. `Artemis.Storage.Legacy` handles migration from the old storage format.
|
||||
- `Artemis.UI.Shared` — reusable MVVM building blocks shared by the main UI: routing framework (`Routing/`), `ActivatableViewModelBase`, shared controls/behaviors/converters/styles.
|
||||
- `Artemis.UI` — the actual application screens (`Screens/...`), organized by feature area (ProfileEditor, Workshop, SurfaceEditor, Settings, etc.). This is where almost all feature work happens.
|
||||
- `Artemis.UI.Windows` / `Artemis.UI.Linux` / `Artemis.UI.MacOS` — thin OS-specific host/entry-point projects (WinExe etc.) that reference `Artemis.UI`. On this machine, `Artemis.UI.Windows` is the one that actually runs.
|
||||
- `Artemis.WebClient.Workshop` / `Artemis.WebClient.Updating` — GraphQL/HTTP clients for the workshop (plugin/profile marketplace) and update checks.
|
||||
- Plugins themselves live in a separate sibling repo, `Artemis.Plugins` — not needed for typical work in this repo and not expected to be cloned alongside it.
|
||||
|
||||
Target framework: `net10.0` (Windows host uses `net10.0-windows10.0.17763.0`). Nullable reference types enabled everywhere.
|
||||
|
||||
## Build & verify
|
||||
|
||||
- Build the whole solution to sanity-check changes: `dotnet build Artemis.sln` (or `Artemis.UI.csproj` while iterating, but always do a full solution build before considering a task done).
|
||||
- There is no automated test suite in this solution (no `*.Tests` projects) — verification is: it builds, plus running the app manually for anything UI-visible (see `run` skill / launch `Artemis.UI.Windows`).
|
||||
- Treat compiler warnings you introduce as worth fixing, but the codebase already has a long tail of pre-existing warnings (obsolete API usage, missing XML docs, etc.) — don't try to clean those up incidentally.
|
||||
|
||||
## MVVM conventions (ReactiveUI + DynamicData + Avalonia)
|
||||
|
||||
- View models use `PropertyChanged.SourceGenerator`: private backing fields annotated `[Notify]` (e.g. `[Notify] private ProfileConfiguration? _profileConfiguration;`) generate the public property — don't hand-write `INotifyPropertyChanged` boilerplate.
|
||||
- Routable screens derive from `RoutableScreen` / `RoutableScreen<TParam>` (in `Artemis.UI.Shared.Routing`), which itself derives from `ActivatableViewModelBase`. Override `OnNavigating`/`OnClosing` for route lifecycle, not the constructor, for anything that depends on navigation parameters.
|
||||
- The router (`Artemis.UI.Shared/Routing/Router/Router.cs`) recycles screen instances by default (`RecycleScreens = true`): navigating between two routes that resolve to the same view model type reuses the existing instance rather than constructing a new one. Don't assume a fresh instance per navigation — state must be reset/updated in `OnNavigating`, not just the constructor.
|
||||
- Any `Transform`/`Bind`/subscription wiring on observable collections (DynamicData) or services goes inside `this.WhenActivated(d => { ... }).DisposeWith(d)`, even when the source is just one of the view model's own properties. This is the established convention throughout the codebase (see `ConfigurationSectionDesignViewModel`, `StatusBarViewModel`, `PropertiesViewModel`) — follow it for consistency and so activation/deactivation cleanup is automatic if the subscription later grows an external dependency.
|
||||
- To react to a *changing* collection-valued property (e.g. rebinding a child list when the parent object changes), use `this.WhenAnyValue(vm => vm.Parent).Select(p => p?.Children.ToObservableChangeSet() ?? Observable.Empty<IChangeSet<T>>()).Switch()`. DynamicData's `Switch()` on an `IObservable<IObservable<IChangeSet<T>>>` clears the previously-bound results when the source switches — don't manually clear a `SourceList` in navigation code as a substitute for this.
|
||||
- Prefer `SourceList<T>`/`SourceCache<T>` + `Connect()`/`ToObservableChangeSet()` + `.Transform(...).Bind(out ReadOnlyObservableCollection<TViewModel> x)` over manually maintaining `ObservableCollection<TViewModel>` in view models that wrap a model-level collection.
|
||||
- DI is DryIoc (`Splat.DryIoc`); constructor injection throughout, view models are resolved via the container/`Func<TModel, TViewModel>` factories for per-item view model creation in `Transform`.
|
||||
|
||||
## Git
|
||||
|
||||
- Current default branch: `master`. Commit message style seen in history is short and imperative, often prefixed with the feature area, e.g. `Workshop - Fix misalignment in EntryTypes causing plugins to not load`, `Surface editor - Show model instead of device name`. Follow that `Area - Change` style when the change is scoped to one area; a plain imperative summary is fine otherwise.
|
||||
Reference in new issue
Block a user