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

Profiles - Reworked profile system

Sidebar - Redesigned sidebar with customizable categories
Profiles - Added the ability to configure custom profile icons
Profiles - Added the ability to activate multiple profiles for modules at once
Profiles - Added the ability to create profiles for no modules
Profiles - Added the ability to suspend a profile or an entire category
Profiles - Added profile activation conditions
Profiles - Added file-based importing/exporting
Profile editor - Condensed UI, removed tabs 
Profile editor - Disable condition operators until a left-side is picked
This commit is contained in:
Robert Beekman 2021-06-03 22:34:43 +02:00 committed by GitHub
parent 4152c290d2
commit ceeaa4bf6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
164 changed files with 5019 additions and 4220 deletions

View File

@ -1,5 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=defaulttypes/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=defaulttypes/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofileconfiguration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Cadaption/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Cadaption/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Cadaptionhints/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Cadaptionhints/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Cadaption_005Chints/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=models_005Cprofile_005Cadaption_005Chints/@EntryIndexedValue">True</s:Boolean>

View File

@ -0,0 +1,21 @@
using System;
using Artemis.Core.Modules;
namespace Artemis.Core
{
/// <summary>
/// Provides data about module events
/// </summary>
public class ModuleEventArgs : EventArgs
{
internal ModuleEventArgs(Module module)
{
Module = module;
}
/// <summary>
/// Gets the module this event is related to
/// </summary>
public Module Module { get; }
}
}

View File

@ -0,0 +1,20 @@
using System;
namespace Artemis.Core
{
/// <summary>
/// Provides data for profile configuration events.
/// </summary>
public class ProfileConfigurationEventArgs : EventArgs
{
internal ProfileConfigurationEventArgs(ProfileConfiguration profileConfiguration)
{
ProfileConfiguration = profileConfiguration;
}
/// <summary>
/// Gets the profile configuration this event is related to
/// </summary>
public ProfileConfiguration ProfileConfiguration { get; }
}
}

View File

@ -0,0 +1,45 @@
using System;
using System.IO;
using Newtonsoft.Json;
namespace Artemis.Core.JsonConverters
{
/// <inheritdoc />
public class StreamConverter : JsonConverter<Stream>
{
#region Overrides of JsonConverter<Stream>
/// <inheritdoc />
public override void WriteJson(JsonWriter writer, Stream? value, JsonSerializer serializer)
{
if (value == null)
{
writer.WriteNull();
return;
}
using MemoryStream memoryStream = new();
value.Position = 0;
value.CopyTo(memoryStream);
writer.WriteValue(memoryStream.ToArray());
}
/// <inheritdoc />
public override Stream? ReadJson(JsonReader reader, Type objectType, Stream? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.Value is not string base64)
return null;
if (existingValue == null || !hasExistingValue || !existingValue.CanRead)
return new MemoryStream(Convert.FromBase64String(base64));
using MemoryStream memoryStream = new(Convert.FromBase64String(base64));
existingValue.Position = 0;
memoryStream.CopyTo(existingValue);
existingValue.Position = 0;
return existingValue;
}
#endregion
}
}

View File

@ -105,10 +105,19 @@ namespace Artemis.Core
InitializeRightPath(); InitializeRightPath();
// Right side static // Right side static
else if (PredicateType == ProfileRightSideType.Static && Entity.RightStaticValue != null) else if (PredicateType == ProfileRightSideType.Static && Entity.RightStaticValue != null)
{
try try
{ {
if (LeftPath != null && LeftPath.IsValid) // If the left path is not valid we cannot reliably set up the right side because the type is unknown
// Because of that wait for it to validate first
if (LeftPath != null && !LeftPath.IsValid)
{ {
LeftPath.PathValidated += InitializeRightSideStatic;
return;
}
if (LeftPath == null)
return;
// Use the left side type so JSON.NET has a better idea what to do // Use the left side type so JSON.NET has a better idea what to do
Type leftSideType = LeftPath.GetPropertyType()!; Type leftSideType = LeftPath.GetPropertyType()!;
object? rightSideValue; object? rightSideValue;
@ -126,17 +135,37 @@ namespace Artemis.Core
UpdateRightSideStatic(rightSideValue); UpdateRightSideStatic(rightSideValue);
} }
else
{
// Hope for the best...
UpdateRightSideStatic(CoreJson.DeserializeObject(Entity.RightStaticValue));
}
}
catch (JsonReaderException e) catch (JsonReaderException e)
{ {
DeserializationLogger.LogPredicateDeserializationFailure(this, e); DeserializationLogger.LogPredicateDeserializationFailure(this, e);
} }
} }
}
private void InitializeRightSideStatic(object? sender, EventArgs args)
{
if (LeftPath == null)
return;
LeftPath.PathValidated -= InitializeRightSideStatic;
// Use the left side type so JSON.NET has a better idea what to do
Type leftSideType = LeftPath.GetPropertyType()!;
object? rightSideValue;
try
{
rightSideValue = CoreJson.DeserializeObject(Entity.RightStaticValue, leftSideType);
}
// If deserialization fails, use the type's default
catch (JsonSerializationException e)
{
DeserializationLogger.LogPredicateDeserializationFailure(this, e);
rightSideValue = Activator.CreateInstance(leftSideType);
}
UpdateRightSideStatic(rightSideValue);
}
/// <summary> /// <summary>
/// Initializes the left path of this condition predicate /// Initializes the left path of this condition predicate

View File

@ -16,7 +16,7 @@ namespace Artemis.Core
{ {
internal EventPredicateWrapperDataModel() internal EventPredicateWrapperDataModel()
{ {
Feature = Constants.CorePluginFeature; Module = Constants.CorePluginFeature;
} }
/// <summary> /// <summary>

View File

@ -16,7 +16,7 @@ namespace Artemis.Core
{ {
internal ListPredicateWrapperDataModel() internal ListPredicateWrapperDataModel()
{ {
Feature = Constants.CorePluginFeature; Module = Constants.CorePluginFeature;
} }
/// <summary> /// <summary>

View File

@ -93,7 +93,7 @@ namespace Artemis.Core
/// <summary> /// <summary>
/// Gets the data model ID of the <see cref="Target" /> if it is a <see cref="DataModel" /> /// Gets the data model ID of the <see cref="Target" /> if it is a <see cref="DataModel" />
/// </summary> /// </summary>
public string? DataModelId => Target?.Feature.Id; public string? DataModelId => Target?.Module.Id;
/// <summary> /// <summary>
/// Gets the point-separated path associated with this <see cref="DataModelPath" /> /// Gets the point-separated path associated with this <see cref="DataModelPath" />
@ -327,7 +327,7 @@ namespace Artemis.Core
private void DataModelStoreOnDataModelAdded(object? sender, DataModelStoreEvent e) private void DataModelStoreOnDataModelAdded(object? sender, DataModelStoreEvent e)
{ {
if (e.Registration.DataModel.Feature.Id != Entity.DataModelId) if (e.Registration.DataModel.Module.Id != Entity.DataModelId)
return; return;
Target = e.Registration.DataModel; Target = e.Registration.DataModel;
@ -336,7 +336,7 @@ namespace Artemis.Core
private void DataModelStoreOnDataModelRemoved(object? sender, DataModelStoreEvent e) private void DataModelStoreOnDataModelRemoved(object? sender, DataModelStoreEvent e)
{ {
if (e.Registration.DataModel.Feature.Id != Entity.DataModelId) if (e.Registration.DataModel.Module.Id != Entity.DataModelId)
return; return;
Target = null; Target = null;

View File

@ -205,13 +205,6 @@ namespace Artemis.Core
canvas.SaveLayer(layerPaint); canvas.SaveLayer(layerPaint);
canvas.Translate(Bounds.Left - basePosition.X, Bounds.Top - basePosition.Y); canvas.Translate(Bounds.Left - basePosition.X, Bounds.Top - basePosition.Y);
// If required, apply the opacity override of the module to the root folder
if (IsRootFolder && Profile.Module.OpacityOverride < 1)
{
double multiplier = Easings.SineEaseInOut(Profile.Module.OpacityOverride);
layerPaint.Color = layerPaint.Color.WithAlpha((byte) (layerPaint.Color.Alpha * multiplier));
}
// No point rendering if the alpha was set to zero by one of the effects // No point rendering if the alpha was set to zero by one of the effects
if (layerPaint.Color.Alpha == 0) if (layerPaint.Color.Alpha == 0)
return; return;
@ -240,6 +233,7 @@ namespace Artemis.Core
{ {
Disposed = true; Disposed = true;
Disable();
foreach (ProfileElement profileElement in Children) foreach (ProfileElement profileElement in Children)
profileElement.Dispose(); profileElement.Dispose();

View File

@ -168,6 +168,8 @@ namespace Artemis.Core
/// <inheritdoc /> /// <inheritdoc />
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
Disable();
Disposed = true; Disposed = true;
// Brush first in case it depends on any of the other disposables during it's own disposal // Brush first in case it depends on any of the other disposables during it's own disposal

View File

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Artemis.Core.Modules;
using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile;
using SkiaSharp; using SkiaSharp;
@ -13,31 +12,15 @@ namespace Artemis.Core
public sealed class Profile : ProfileElement public sealed class Profile : ProfileElement
{ {
private readonly object _lock = new(); private readonly object _lock = new();
private bool _isActivated;
private bool _isFreshImport; private bool _isFreshImport;
internal Profile(ProfileModule module, string name) : base(null!) internal Profile(ProfileConfiguration configuration, ProfileEntity profileEntity) : base(null!)
{
ProfileEntity = new ProfileEntity();
EntityId = Guid.NewGuid();
Profile = this;
Module = module;
Name = name;
UndoStack = new Stack<string>();
RedoStack = new Stack<string>();
Folder _ = new(this, "Root folder");
Save();
}
internal Profile(ProfileModule module, ProfileEntity profileEntity) : base(null!)
{ {
Configuration = configuration;
Profile = this; Profile = this;
ProfileEntity = profileEntity; ProfileEntity = profileEntity;
EntityId = profileEntity.Id; EntityId = profileEntity.Id;
Module = module;
UndoStack = new Stack<string>(); UndoStack = new Stack<string>();
RedoStack = new Stack<string>(); RedoStack = new Stack<string>();
@ -45,18 +28,9 @@ namespace Artemis.Core
} }
/// <summary> /// <summary>
/// Gets the module backing this profile /// Gets the profile configuration of this profile
/// </summary> /// </summary>
public ProfileModule Module { get; } public ProfileConfiguration Configuration { get; }
/// <summary>
/// Gets a boolean indicating whether this profile is activated
/// </summary>
public bool IsActivated
{
get => _isActivated;
private set => SetAndNotify(ref _isActivated, value);
}
/// <summary> /// <summary>
/// Gets or sets a boolean indicating whether this profile is freshly imported i.e. no changes have been made to it /// Gets or sets a boolean indicating whether this profile is freshly imported i.e. no changes have been made to it
@ -87,8 +61,6 @@ namespace Artemis.Core
{ {
if (Disposed) if (Disposed)
throw new ObjectDisposedException("Profile"); throw new ObjectDisposedException("Profile");
if (!IsActivated)
throw new ArtemisCoreException($"Cannot update inactive profile: {this}");
foreach (ProfileElement profileElement in Children) foreach (ProfileElement profileElement in Children)
profileElement.Update(deltaTime); profileElement.Update(deltaTime);
@ -102,8 +74,6 @@ namespace Artemis.Core
{ {
if (Disposed) if (Disposed)
throw new ObjectDisposedException("Profile"); throw new ObjectDisposedException("Profile");
if (!IsActivated)
throw new ArtemisCoreException($"Cannot render inactive profile: {this}");
foreach (ProfileElement profileElement in Children) foreach (ProfileElement profileElement in Children)
profileElement.Render(canvas, basePosition); profileElement.Render(canvas, basePosition);
@ -133,7 +103,7 @@ namespace Artemis.Core
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() public override string ToString()
{ {
return $"[Profile] {nameof(Name)}: {Name}, {nameof(IsActivated)}: {IsActivated}, {nameof(Module)}: {Module}"; return $"[Profile] {nameof(Name)}: {Name}";
} }
/// <summary> /// <summary>
@ -149,29 +119,15 @@ namespace Artemis.Core
layer.PopulateLeds(devices); layer.PopulateLeds(devices);
} }
/// <summary>
/// Occurs when the profile has been activated.
/// </summary>
public event EventHandler? Activated;
/// <summary>
/// Occurs when the profile is being deactivated.
/// </summary>
public event EventHandler? Deactivated;
/// <inheritdoc /> /// <inheritdoc />
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
if (!disposing) if (!disposing)
return; return;
OnDeactivating();
foreach (ProfileElement profileElement in Children) foreach (ProfileElement profileElement in Children)
profileElement.Dispose(); profileElement.Dispose();
ChildrenList.Clear(); ChildrenList.Clear();
IsActivated = false;
Disposed = true; Disposed = true;
} }
@ -180,7 +136,7 @@ namespace Artemis.Core
if (Disposed) if (Disposed)
throw new ObjectDisposedException("Profile"); throw new ObjectDisposedException("Profile");
Name = ProfileEntity.Name; Name = Configuration.Name;
IsFreshImport = ProfileEntity.IsFreshImport; IsFreshImport = ProfileEntity.IsFreshImport;
lock (ChildrenList) lock (ChildrenList)
@ -197,9 +153,11 @@ namespace Artemis.Core
Folder _ = new(this, "Root folder"); Folder _ = new(this, "Root folder");
} }
else else
{
AddChild(new Folder(this, this, rootFolder)); AddChild(new Folder(this, this, rootFolder));
} }
} }
}
internal override void Save() internal override void Save()
{ {
@ -207,9 +165,7 @@ namespace Artemis.Core
throw new ObjectDisposedException("Profile"); throw new ObjectDisposedException("Profile");
ProfileEntity.Id = EntityId; ProfileEntity.Id = EntityId;
ProfileEntity.ModuleId = Module.Id; ProfileEntity.Name = Configuration.Name;
ProfileEntity.Name = Name;
ProfileEntity.IsActive = IsActivated;
ProfileEntity.IsFreshImport = IsFreshImport; ProfileEntity.IsFreshImport = IsFreshImport;
foreach (ProfileElement profileElement in Children) foreach (ProfileElement profileElement in Children)
@ -221,30 +177,5 @@ namespace Artemis.Core
ProfileEntity.Layers.Clear(); ProfileEntity.Layers.Clear();
ProfileEntity.Layers.AddRange(GetAllLayers().Select(f => f.LayerEntity)); ProfileEntity.Layers.AddRange(GetAllLayers().Select(f => f.LayerEntity));
} }
internal void Activate(IEnumerable<ArtemisDevice> devices)
{
lock (_lock)
{
if (Disposed)
throw new ObjectDisposedException("Profile");
if (IsActivated)
return;
PopulateLeds(devices);
OnActivated();
IsActivated = true;
}
}
private void OnActivated()
{
Activated?.Invoke(this, EventArgs.Empty);
}
private void OnDeactivating()
{
Deactivated?.Invoke(this, EventArgs.Empty);
}
} }
} }

View File

@ -0,0 +1,244 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core
{
public class ProfileCategory : CorePropertyChanged, IStorageModel
{
private readonly List<ProfileConfiguration> _profileConfigurations = new();
private bool _isCollapsed;
private bool _isSuspended;
private string _name;
private int _order;
/// <summary>
/// Creates a new instance of the <see cref="ProfileCategory" /> class
/// </summary>
/// <param name="name">The name of the category</param>
internal ProfileCategory(string name)
{
_name = name;
Entity = new ProfileCategoryEntity();
}
internal ProfileCategory(ProfileCategoryEntity entity)
{
Entity = entity;
Load();
}
/// <summary>
/// Gets or sets the name of the profile category
/// </summary>
public string Name
{
get => _name;
set => SetAndNotify(ref _name, value);
}
/// <summary>
/// The order in which this category appears in the update loop and sidebar
/// </summary>
public int Order
{
get => _order;
set => SetAndNotify(ref _order, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the category is collapsed or not
/// <para>Note: Has no implications other than inside the UI</para>
/// </summary>
public bool IsCollapsed
{
get => _isCollapsed;
set => SetAndNotify(ref _isCollapsed, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether this category is suspended, disabling all its profiles
/// </summary>
public bool IsSuspended
{
get => _isSuspended;
set => SetAndNotify(ref _isSuspended, value);
}
/// <summary>
/// Gets a read only collection of the profiles inside this category
/// </summary>
public ReadOnlyCollection<ProfileConfiguration> ProfileConfigurations => _profileConfigurations.AsReadOnly();
/// <summary>
/// Gets the unique ID of this category
/// </summary>
public Guid EntityId => Entity.Id;
internal ProfileCategoryEntity Entity { get; }
/// <summary>
/// Adds a profile configuration to this category
/// </summary>
public void AddProfileConfiguration(ProfileConfiguration configuration, int? targetIndex)
{
// Removing the original will shift every item in the list forwards, keep that in mind with the target index
if (configuration.Category == this && targetIndex != null && targetIndex.Value > _profileConfigurations.IndexOf(configuration))
targetIndex -= 1;
configuration.Category.RemoveProfileConfiguration(configuration);
if (targetIndex != null)
_profileConfigurations.Insert(Math.Clamp(targetIndex.Value, 0, _profileConfigurations.Count), configuration);
else
_profileConfigurations.Add(configuration);
configuration.Category = this;
for (int index = 0; index < _profileConfigurations.Count; index++)
_profileConfigurations[index].Order = index;
OnProfileConfigurationAdded(new ProfileConfigurationEventArgs(configuration));
}
/// <inheritdoc />
public override string ToString()
{
return $"[ProfileCategory] {Order} {nameof(Name)}: {Name}, {nameof(IsSuspended)}: {IsSuspended}";
}
internal void RemoveProfileConfiguration(ProfileConfiguration configuration)
{
if (!_profileConfigurations.Remove(configuration)) return;
for (int index = 0; index < _profileConfigurations.Count; index++)
_profileConfigurations[index].Order = index;
OnProfileConfigurationRemoved(new ProfileConfigurationEventArgs(configuration));
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
Name = Entity.Name;
IsCollapsed = Entity.IsCollapsed;
IsSuspended = Entity.IsSuspended;
Order = Entity.Order;
_profileConfigurations.Clear();
foreach (ProfileConfigurationEntity entityProfileConfiguration in Entity.ProfileConfigurations)
_profileConfigurations.Add(new ProfileConfiguration(this, entityProfileConfiguration));
}
/// <inheritdoc />
public void Save()
{
Entity.Name = Name;
Entity.IsCollapsed = IsCollapsed;
Entity.IsSuspended = IsSuspended;
Entity.Order = Order;
Entity.ProfileConfigurations.Clear();
foreach (ProfileConfiguration profileConfiguration in ProfileConfigurations)
{
profileConfiguration.Save();
Entity.ProfileConfigurations.Add(profileConfiguration.Entity);
}
}
#endregion
#region Events
/// <summary>
/// Occurs when a profile configuration is added to this <see cref="ProfileCategory" />
/// </summary>
public event EventHandler<ProfileConfigurationEventArgs>? ProfileConfigurationAdded;
/// <summary>
/// Occurs when a profile configuration is removed from this <see cref="ProfileCategory" />
/// </summary>
public event EventHandler<ProfileConfigurationEventArgs>? ProfileConfigurationRemoved;
/// <summary>
/// Invokes the <see cref="ProfileConfigurationAdded" /> event
/// </summary>
protected virtual void OnProfileConfigurationAdded(ProfileConfigurationEventArgs e)
{
ProfileConfigurationAdded?.Invoke(this, e);
}
/// <summary>
/// Invokes the <see cref="ProfileConfigurationRemoved" /> event
/// </summary>
protected virtual void OnProfileConfigurationRemoved(ProfileConfigurationEventArgs e)
{
ProfileConfigurationRemoved?.Invoke(this, e);
}
#endregion
}
/// <summary>
/// Represents a name of one of the default categories
/// </summary>
public enum DefaultCategoryName
{
/// <summary>
/// The category used by profiles tied to games
/// </summary>
Games,
/// <summary>
/// The category used by profiles tied to applications
/// </summary>
Applications,
/// <summary>
/// The category used by general profiles
/// </summary>
General
}
/// <summary>
/// Represents a type of behaviour when this profile is activated
/// </summary>
public enum ActivationBehaviour
{
/// <summary>
/// Do nothing to other profiles
/// </summary>
None,
/// <summary>
/// Disable all other profiles
/// </summary>
DisableOthers,
/// <summary>
/// Disable all other profiles below this one
/// </summary>
DisableOthersBelow,
/// <summary>
/// Disable all other profiles above this one
/// </summary>
DisableOthersAbove,
/// <summary>
/// Disable all other profiles in the same category
/// </summary>
DisableOthersInCategory,
/// <summary>
/// Disable all other profiles below this one in the same category
/// </summary>
DisableOthersBelowInCategory,
/// <summary>
/// Disable all other profiles above this one in the same category
/// </summary>
DisableOthersAboveInCategory
}
}

View File

@ -1,41 +0,0 @@
using System;
using Artemis.Core.Modules;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core
{
/// <summary>
/// Represents a descriptor that describes a profile
/// </summary>
public class ProfileDescriptor : CorePropertyChanged
{
internal ProfileDescriptor(ProfileModule profileModule, ProfileEntity profileEntity)
{
ProfileModule = profileModule;
Id = profileEntity.Id;
Name = profileEntity.Name;
IsLastActiveProfile = profileEntity.IsActive;
}
/// <summary>
/// Gets the module backing the profile
/// </summary>
public ProfileModule ProfileModule { get; }
/// <summary>
/// Gets the unique ID of the profile by which it can be loaded from storage
/// </summary>
public Guid Id { get; }
/// <summary>
/// Gets the name of the profile
/// </summary>
public string Name { get; }
/// <summary>
/// Gets a boolean indicating whether this was the last active profile
/// </summary>
public bool IsLastActiveProfile { get; }
}
}

View File

@ -11,15 +11,14 @@ namespace Artemis.Core
/// </summary> /// </summary>
public abstract class ProfileElement : CorePropertyChanged, IDisposable public abstract class ProfileElement : CorePropertyChanged, IDisposable
{ {
private bool _suspended;
private Guid _entityId; private Guid _entityId;
private string? _name; private string? _name;
private int _order; private int _order;
private ProfileElement? _parent; private ProfileElement? _parent;
private Profile _profile; private Profile _profile;
private bool _suspended;
internal List<ProfileElement> ChildrenList; internal List<ProfileElement> ChildrenList;
internal bool Disposed;
internal ProfileElement(Profile profile) internal ProfileElement(Profile profile)
{ {
@ -95,6 +94,11 @@ namespace Artemis.Core
set => SetAndNotify(ref _suspended, value); set => SetAndNotify(ref _suspended, value);
} }
/// <summary>
/// Gets a boolean indicating whether the profile element is disposed
/// </summary>
public bool Disposed { get; protected set; }
/// <summary> /// <summary>
/// Updates the element /// Updates the element
/// </summary> /// </summary>

View File

@ -0,0 +1,210 @@
using System.Collections.Generic;
using System.Linq;
using Artemis.Core.Modules;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core
{
public class ProfileConfiguration : CorePropertyChanged, IStorageModel
{
private ProfileCategory _category;
private bool _isMissingModule;
private bool _isSuspended;
private Module? _module;
private string _name;
private int _order;
private Profile? _profile;
internal ProfileConfiguration(ProfileCategory category, string name, string icon)
{
_name = name;
_category = category;
Entity = new ProfileConfigurationEntity();
Icon = new ProfileConfigurationIcon(Entity) {MaterialIcon = icon};
}
internal ProfileConfiguration(ProfileCategory category, ProfileConfigurationEntity entity)
{
// Will be loaded from the entity
_name = null!;
_category = category;
Entity = entity;
Icon = new ProfileConfigurationIcon(Entity);
Load();
}
/// <summary>
/// Gets or sets the name of this profile configuration
/// </summary>
public string Name
{
get => _name;
set => SetAndNotify(ref _name, value);
}
/// <summary>
/// The order in which this profile appears in the update loop and sidebar
/// </summary>
public int Order
{
get => _order;
set => SetAndNotify(ref _order, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether this profile is suspended, disabling it regardless of the
/// <see cref="ActivationCondition" />
/// </summary>
public bool IsSuspended
{
get => _isSuspended;
set => SetAndNotify(ref _isSuspended, value);
}
/// <summary>
/// Gets a boolean indicating whether this profile configuration is missing any modules
/// </summary>
public bool IsMissingModule
{
get => _isMissingModule;
private set => SetAndNotify(ref _isMissingModule, value);
}
/// <summary>
/// Gets or sets the category of this profile configuration
/// </summary>
public ProfileCategory Category
{
get => _category;
internal set => SetAndNotify(ref _category, value);
}
/// <summary>
/// Gets the icon configuration
/// </summary>
public ProfileConfigurationIcon Icon { get; }
/// <summary>
/// Gets the profile of this profile configuration
/// </summary>
public Profile? Profile
{
get => _profile;
internal set => SetAndNotify(ref _profile, value);
}
/// <summary>
/// Gets or sets the behaviour of when this profile is activated
/// </summary>
public ActivationBehaviour ActivationBehaviour { get; set; }
/// <summary>
/// Gets the data model condition that must evaluate to <see langword="true" /> for this profile to be activated
/// alongside any activation requirements of the <see cref="Module" />, if set
/// </summary>
public DataModelConditionGroup? ActivationCondition { get; set; }
/// <summary>
/// Gets or sets the module this profile uses
/// </summary>
public Module? Module
{
get => _module;
set
{
_module = value;
IsMissingModule = false;
}
}
/// <summary>
/// Gets a boolean indicating whether the activation conditions where met during the last <see cref="Update" /> call
/// </summary>
public bool ActivationConditionMet { get; private set; }
/// <summary>
/// Gets or sets a boolean indicating whether this profile configuration is being edited
/// </summary>
public bool IsBeingEdited { get; set; }
/// <summary>
/// Gets the entity used by this profile config
/// </summary>
public ProfileConfigurationEntity Entity { get; }
/// <summary>
/// Updates this configurations activation condition status
/// </summary>
public void Update()
{
ActivationConditionMet = ActivationCondition == null || ActivationCondition.Evaluate();
}
public bool ShouldBeActive(bool includeActivationCondition)
{
if (Category.IsSuspended || IsSuspended || IsMissingModule)
return false;
if (includeActivationCondition)
return ActivationConditionMet && (Module == null || Module.IsActivated);
return Module == null || Module.IsActivated;
}
/// <inheritdoc />
public override string ToString()
{
return $"[ProfileConfiguration] {nameof(Name)}: {Name}";
}
internal void LoadModules(List<Module> enabledModules)
{
Module = enabledModules.FirstOrDefault(m => m.Id == Entity.ModuleId);
IsMissingModule = Module == null && Entity.ModuleId != null;
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
Name = Entity.Name;
IsSuspended = Entity.IsSuspended;
ActivationBehaviour = (ActivationBehaviour) Entity.ActivationBehaviour;
Icon.Load();
ActivationCondition = Entity.ActivationCondition != null
? new DataModelConditionGroup(null, Entity.ActivationCondition)
: null;
}
/// <inheritdoc />
public void Save()
{
Entity.Name = Name;
Entity.IsSuspended = IsSuspended;
Entity.ActivationBehaviour = (int) ActivationBehaviour;
Entity.ProfileCategoryId = Category.Entity.Id;
Icon.Save();
if (ActivationCondition != null)
{
ActivationCondition.Save();
Entity.ActivationCondition = ActivationCondition.Entity;
}
else
{
Entity.ActivationCondition = null;
}
if (!IsMissingModule)
Entity.ModuleId = Module?.Id;
}
#endregion
}
}

View File

@ -0,0 +1,30 @@
using System.IO;
using Artemis.Core.JsonConverters;
using Artemis.Storage.Entities.Profile;
using Newtonsoft.Json;
namespace Artemis.Core
{
/// <summary>
/// A model that can be used to serialize a profile configuration, it's profile and it's icon
/// </summary>
public class ProfileConfigurationExportModel
{
/// <summary>
/// Gets or sets the storage entity of the profile configuration
/// </summary>
public ProfileConfigurationEntity? ProfileConfigurationEntity { get; set; }
/// <summary>
/// Gets or sets the storage entity of the profile
/// </summary>
[JsonProperty(Required = Required.Always)]
public ProfileEntity ProfileEntity { get; set; } = null!;
/// <summary>
/// Gets or sets a stream containing the profile image
/// </summary>
[JsonConverter(typeof(StreamConverter))]
public Stream? ProfileImage { get; set; }
}
}

View File

@ -0,0 +1,89 @@
using System.ComponentModel;
using System.IO;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core
{
/// <summary>
/// Represents the icon of a <see cref="ProfileConfiguration" />
/// </summary>
public class ProfileConfigurationIcon : CorePropertyChanged, IStorageModel
{
private readonly ProfileConfigurationEntity _entity;
private Stream? _fileIcon;
private ProfileConfigurationIconType _iconType;
private string? _materialIcon;
internal ProfileConfigurationIcon(ProfileConfigurationEntity entity)
{
_entity = entity;
}
/// <summary>
/// Gets or sets the type of icon this profile configuration uses
/// </summary>
public ProfileConfigurationIconType IconType
{
get => _iconType;
set => SetAndNotify(ref _iconType, value);
}
/// <summary>
/// Gets or sets the icon if it is a Material icon
/// </summary>
public string? MaterialIcon
{
get => _materialIcon;
set => SetAndNotify(ref _materialIcon, value);
}
/// <summary>
/// Gets or sets a stream containing the icon if it is bitmap or SVG
/// </summary>
/// <returns></returns>
public Stream? FileIcon
{
get => _fileIcon;
set => SetAndNotify(ref _fileIcon, value);
}
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
IconType = (ProfileConfigurationIconType) _entity.IconType;
MaterialIcon = _entity.MaterialIcon;
}
/// <inheritdoc />
public void Save()
{
_entity.IconType = (int) IconType;
_entity.MaterialIcon = MaterialIcon;
}
#endregion
}
/// <summary>
/// Represents a type of profile icon
/// </summary>
public enum ProfileConfigurationIconType
{
/// <summary>
/// An icon picked from the Material Design Icons collection
/// </summary>
[Description("Material Design Icon")] MaterialIcon,
/// <summary>
/// A bitmap image icon
/// </summary>
[Description("Bitmap Image")] BitmapImage,
/// <summary>
/// An SVG image icon
/// </summary>
[Description("SVG Image")] SvgImage
}
}

View File

@ -1,53 +0,0 @@
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
namespace Artemis.Core.DataModelExpansions
{
/// <summary>
/// Allows you to expand the application-wide datamodel
/// </summary>
public abstract class DataModelExpansion<T> : BaseDataModelExpansion where T : DataModel
{
/// <summary>
/// The main data model of this data model expansion
/// <para>Note: This default data model is automatically registered upon plugin enable</para>
/// </summary>
public T DataModel
{
get => InternalDataModel as T ?? throw new InvalidOperationException("Internal datamodel does not match the type of the data model");
internal set => InternalDataModel = value;
}
/// <summary>
/// Hide the provided property using a lambda expression, e.g. HideProperty(dm => dm.TimeDataModel.CurrentTimeUTC)
/// </summary>
/// <param name="propertyLambda">A lambda expression pointing to the property to ignore</param>
public void HideProperty<TProperty>(Expression<Func<T, TProperty>> propertyLambda)
{
PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda);
if (!HiddenPropertiesList.Any(p => p.Equals(propertyInfo)))
HiddenPropertiesList.Add(propertyInfo);
}
/// <summary>
/// Stop hiding the provided property using a lambda expression, e.g. ShowProperty(dm =>
/// dm.TimeDataModel.CurrentTimeUTC)
/// </summary>
/// <param name="propertyLambda">A lambda expression pointing to the property to stop ignoring</param>
public void ShowProperty<TProperty>(Expression<Func<T, TProperty>> propertyLambda)
{
PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda);
HiddenPropertiesList.RemoveAll(p => p.Equals(propertyInfo));
}
internal override void InternalEnable()
{
DataModel = Activator.CreateInstance<T>();
DataModel.Feature = this;
DataModel.DataModelDescription = GetDataModelDescription();
base.InternalEnable();
}
}
}

View File

@ -1,47 +0,0 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reflection;
namespace Artemis.Core.DataModelExpansions
{
/// <summary>
/// For internal use only, to implement your own layer property type, extend <see cref="DataModelExpansion{T}" />
/// instead.
/// </summary>
public abstract class BaseDataModelExpansion : DataModelPluginFeature
{
/// <summary>
/// Gets a list of all properties ignored at runtime using <c>IgnoreProperty(x => x.y)</c>
/// </summary>
protected internal readonly List<PropertyInfo> HiddenPropertiesList = new();
/// <summary>
/// Gets a list of all properties ignored at runtime using <c>IgnoreProperty(x => x.y)</c>
/// </summary>
public ReadOnlyCollection<PropertyInfo> HiddenProperties => HiddenPropertiesList.AsReadOnly();
internal DataModel? InternalDataModel { get; set; }
/// <summary>
/// Called each frame when the data model should update
/// </summary>
/// <param name="deltaTime">Time in seconds since the last update</param>
public abstract void Update(double deltaTime);
internal void InternalUpdate(double deltaTime)
{
if (InternalDataModel != null)
Update(deltaTime);
}
/// <summary>
/// Override to provide your own data model description. By default this returns a description matching your plugin
/// name and description
/// </summary>
/// <returns></returns>
public virtual DataModelPropertyAttribute GetDataModelDescription()
{
return new() {Name = Plugin.Info.Name, Description = Plugin.Info.Description};
}
}
}

View File

@ -1,13 +0,0 @@
using System;
using System.Threading.Tasks;
namespace Artemis.Core
{
/// <summary>
/// Represents an feature of a certain type provided by a plugin with support for data models
/// </summary>
public abstract class DataModelPluginFeature : PluginFeature
{
}
}

View File

@ -6,7 +6,7 @@ using SkiaSharp;
namespace Artemis.Core.LayerBrushes namespace Artemis.Core.LayerBrushes
{ {
/// <summary> /// <summary>
/// For internal use only, please use <see cref="LayerBrush{T}" /> or <see cref="RgbNetLayerBrush{T}" /> or instead /// For internal use only, please use <see cref="LayerBrush{T}" /> or <see cref="PerLedLayerBrush{T}" /> or instead
/// </summary> /// </summary>
public abstract class BaseLayerBrush : CorePropertyChanged, IDisposable public abstract class BaseLayerBrush : CorePropertyChanged, IDisposable
{ {

View File

@ -1,11 +1,9 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
namespace Artemis.Core.LayerBrushes namespace Artemis.Core.LayerBrushes
{ {
/// <summary> /// <summary>
/// For internal use only, please use <see cref="LayerBrush{T}" /> or <see cref="RgbNetLayerBrush{T}" /> or instead /// For internal use only, please use <see cref="LayerBrush{T}" /> or <see cref="PerLedLayerBrush{T}" /> or instead
/// </summary> /// </summary>
public abstract class PropertiesLayerBrush<T> : BaseLayerBrush where T : LayerPropertyGroup public abstract class PropertiesLayerBrush<T> : BaseLayerBrush where T : LayerPropertyGroup
{ {

View File

@ -1,104 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Artemis.Core.Services;
using Ninject;
using RGB.NET.Core;
using SkiaSharp;
namespace Artemis.Core.LayerBrushes
{
/// <summary>
/// An RGB.NET brush that uses RGB.NET's per-LED rendering engine.
/// <para>Note: This brush type always renders on top of regular brushes</para>
/// </summary>
/// <typeparam name="T"></typeparam>
public abstract class RgbNetLayerBrush<T> : PropertiesLayerBrush<T> where T : LayerPropertyGroup
{
/// <summary>
/// Creates a new instance of the <see cref="RgbNetLayerBrush{T}" /> class
/// </summary>
protected RgbNetLayerBrush()
{
BrushType = LayerBrushType.RgbNet;
SupportsTransformation = false;
}
/// <summary>
/// The LED group this layer effect is applied to
/// </summary>
public ListLedGroup? LedGroup { get; internal set; }
/// <summary>
/// For internal use only, is public for dependency injection but ignore pl0x
/// </summary>
[Inject]
public IRgbService? RgbService { get; set; }
/// <summary>
/// Called when Artemis needs an instance of the RGB.NET effect you are implementing
/// </summary>
/// <returns>Your RGB.NET effect</returns>
public abstract IBrush GetBrush();
#region IDisposable
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (RgbService == null)
throw new ArtemisCoreException("Cannot dispose RGB.NET layer brush because RgbService is not set");
Layer.RenderPropertiesUpdated -= LayerOnRenderPropertiesUpdated;
LedGroup?.Detach();
LedGroup = null;
}
base.Dispose(disposing);
}
#endregion
internal void UpdateLedGroup()
{
if (LedGroup == null)
return;
if (Layer.Parent != null)
LedGroup.ZIndex = Layer.Parent.Children.Count - Layer.Parent.Children.IndexOf(Layer);
else
LedGroup.ZIndex = 1;
List<Led> missingLeds = Layer.Leds.Where(l => !LedGroup.ContainsLed(l.RgbLed)).Select(l => l.RgbLed).ToList();
List<Led> extraLeds = LedGroup.Where(l => Layer.Leds.All(layerLed => layerLed.RgbLed != l)).ToList();
LedGroup.AddLeds(missingLeds);
LedGroup.RemoveLeds(extraLeds);
LedGroup.Brush = GetBrush();
}
internal override void Initialize()
{
if (RgbService == null)
throw new ArtemisCoreException("Cannot initialize RGB.NET layer brush because RgbService is not set");
LedGroup = new ListLedGroup(RgbService.Surface);
Layer.RenderPropertiesUpdated += LayerOnRenderPropertiesUpdated;
InitializeProperties();
UpdateLedGroup();
}
// Not used in this effect type
internal override void InternalRender(SKCanvas canvas, SKRect bounds, SKPaint paint)
{
throw new NotImplementedException("RGB.NET layer effects do not implement InternalRender");
}
private void LayerOnRenderPropertiesUpdated(object? sender, EventArgs e)
{
UpdateLedGroup();
}
}
}

View File

@ -7,6 +7,7 @@ using System.Reflection;
using Artemis.Core.Modules; using Artemis.Core.Modules;
using Humanizer; using Humanizer;
using Newtonsoft.Json; using Newtonsoft.Json;
using Module = Artemis.Core.Modules.Module;
namespace Artemis.Core.DataModelExpansions namespace Artemis.Core.DataModelExpansions
{ {
@ -23,16 +24,16 @@ namespace Artemis.Core.DataModelExpansions
protected DataModel() protected DataModel()
{ {
// These are both set right after construction to keep the constructor of inherited classes clean // These are both set right after construction to keep the constructor of inherited classes clean
Feature = null!; Module = null!;
DataModelDescription = null!; DataModelDescription = null!;
} }
/// <summary> /// <summary>
/// Gets the plugin feature this data model belongs to /// Gets the module this data model belongs to
/// </summary> /// </summary>
[JsonIgnore] [JsonIgnore]
[DataModelIgnore] [DataModelIgnore]
public DataModelPluginFeature Feature { get; internal set; } public Module Module { get; internal set; }
/// <summary> /// <summary>
/// Gets the <see cref="DataModelPropertyAttribute" /> describing this data model /// Gets the <see cref="DataModelPropertyAttribute" /> describing this data model
@ -59,10 +60,8 @@ namespace Artemis.Core.DataModelExpansions
/// <returns></returns> /// <returns></returns>
public ReadOnlyCollection<PropertyInfo> GetHiddenProperties() public ReadOnlyCollection<PropertyInfo> GetHiddenProperties()
{ {
if (Feature is ProfileModule profileModule) if (Module is Module module)
return profileModule.HiddenProperties; return module.HiddenProperties;
if (Feature is BaseDataModelExpansion dataModelExpansion)
return dataModelExpansion.HiddenProperties;
return new List<PropertyInfo>().AsReadOnly(); return new List<PropertyInfo>().AsReadOnly();
} }
@ -149,7 +148,7 @@ namespace Artemis.Core.DataModelExpansions
attribute.Name ??= key.Humanize(); attribute.Name ??= key.Humanize();
if (initialValue is DataModel dynamicDataModel) if (initialValue is DataModel dynamicDataModel)
{ {
dynamicDataModel.Feature = Feature; dynamicDataModel.Module = Module;
dynamicDataModel.DataModelDescription = attribute; dynamicDataModel.DataModelDescription = attribute;
} }

View File

@ -1,21 +1,22 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Collections.ObjectModel;
using System.IO;
using System.Linq; using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Artemis.Core.DataModelExpansions; using Artemis.Core.DataModelExpansions;
using Artemis.Storage.Entities.Module;
using SkiaSharp;
namespace Artemis.Core.Modules namespace Artemis.Core.Modules
{ {
/// <summary> /// <summary>
/// Allows you to add support for new games/applications while utilizing your own data model /// Allows you to add new data to the Artemis data model
/// </summary> /// </summary>
public abstract class Module<T> : Module where T : DataModel public abstract class Module<T> : Module where T : DataModel
{ {
/// <summary> /// <summary>
/// The data model driving this module /// The data model driving this module
/// <para>Note: This default data model is automatically registered upon plugin enable</para> /// <para>Note: This default data model is automatically registered and instantiated upon plugin enable</para>
/// </summary> /// </summary>
public T DataModel public T DataModel
{ {
@ -24,43 +25,61 @@ namespace Artemis.Core.Modules
} }
/// <summary> /// <summary>
/// Gets or sets whether this module must also expand the main data model /// Hide the provided property using a lambda expression, e.g. HideProperty(dm => dm.TimeDataModel.CurrentTimeUTC)
/// <para>
/// Note: If expanding the main data model is all you want your plugin to do, create a
/// <see cref="DataModelExpansion{T}" /> plugin instead.
/// </para>
/// </summary> /// </summary>
public bool ExpandsDataModel /// <param name="propertyLambda">A lambda expression pointing to the property to ignore</param>
public void HideProperty<TProperty>(Expression<Func<T, TProperty>> propertyLambda)
{ {
get => InternalExpandsMainDataModel; PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda);
set => InternalExpandsMainDataModel = value; if (!HiddenPropertiesList.Any(p => p.Equals(propertyInfo)))
HiddenPropertiesList.Add(propertyInfo);
} }
/// <summary> /// <summary>
/// Override to provide your own data model description. By default this returns a description matching your plugin /// Stop hiding the provided property using a lambda expression, e.g. ShowProperty(dm =>
/// name and description /// dm.TimeDataModel.CurrentTimeUTC)
/// </summary> /// </summary>
/// <returns></returns> /// <param name="propertyLambda">A lambda expression pointing to the property to stop ignoring</param>
public virtual DataModelPropertyAttribute GetDataModelDescription() public void ShowProperty<TProperty>(Expression<Func<T, TProperty>> propertyLambda)
{ {
return new() {Name = Plugin.Info.Name, Description = Plugin.Info.Description}; PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda);
HiddenPropertiesList.RemoveAll(p => p.Equals(propertyInfo));
} }
internal override void InternalEnable() internal override void InternalEnable()
{ {
DataModel = Activator.CreateInstance<T>(); DataModel = Activator.CreateInstance<T>();
DataModel.Feature = this; DataModel.Module = this;
DataModel.DataModelDescription = GetDataModelDescription(); DataModel.DataModelDescription = GetDataModelDescription();
base.InternalEnable(); base.InternalEnable();
} }
internal override void InternalDisable()
{
Deactivate(true);
base.InternalDisable();
}
} }
/// <summary> /// <summary>
/// Allows you to add support for new games/applications /// For internal use only, please use <see cref="Module{T}" />.
/// </summary> /// </summary>
public abstract class Module : DataModelPluginFeature public abstract class Module : PluginFeature
{ {
private readonly List<(DefaultCategoryName, string)> _pendingDefaultProfilePaths = new();
private readonly List<(DefaultCategoryName, string)> _defaultProfilePaths = new();
/// <summary>
/// Gets a list of all properties ignored at runtime using <c>IgnoreProperty(x => x.y)</c>
/// </summary>
protected internal readonly List<PropertyInfo> HiddenPropertiesList = new();
/// <summary>
/// Gets a read only collection of default profile paths
/// </summary>
public IReadOnlyCollection<(DefaultCategoryName, string)> DefaultProfilePaths => _defaultProfilePaths.AsReadOnly();
/// <summary> /// <summary>
/// The modules display name that's shown in the menu /// The modules display name that's shown in the menu
/// </summary> /// </summary>
@ -107,35 +126,23 @@ namespace Artemis.Core.Modules
public ActivationRequirementType ActivationRequirementMode { get; set; } = ActivationRequirementType.Any; public ActivationRequirementType ActivationRequirementMode { get; set; } = ActivationRequirementType.Any;
/// <summary> /// <summary>
/// Gets or sets the default priority category for this module, defaults to /// Gets or sets a boolean indicating whether this module is always available to profiles or only when profiles
/// <see cref="ModulePriorityCategory.Normal" /> /// specifically target this module.
/// <para>Note: If set to <see langword="true" />, <see cref="ActivationRequirements" /> are not evaluated.</para>
/// </summary> /// </summary>
public ModulePriorityCategory DefaultPriorityCategory { get; set; } = ModulePriorityCategory.Normal; public bool IsAlwaysAvailable { get; set; }
/// <summary>
/// Gets the current priority category of this module
/// </summary>
public ModulePriorityCategory PriorityCategory { get; internal set; }
/// <summary>
/// Gets the current priority of this module within its priority category
/// </summary>
public int Priority { get; internal set; }
/// <summary>
/// A list of custom module tabs that show in the UI
/// </summary>
public IEnumerable<ModuleTab>? ModuleTabs { get; protected set; }
/// <summary> /// <summary>
/// Gets whether updating this module is currently allowed /// Gets whether updating this module is currently allowed
/// </summary> /// </summary>
public bool IsUpdateAllowed => IsActivated && (UpdateDuringActivationOverride || !IsActivatedOverride); public bool IsUpdateAllowed => IsActivated && (UpdateDuringActivationOverride || !IsActivatedOverride);
internal DataModel? InternalDataModel { get; set; } /// <summary>
/// Gets a list of all properties ignored at runtime using <c>IgnoreProperty(x => x.y)</c>
/// </summary>
public ReadOnlyCollection<PropertyInfo> HiddenProperties => HiddenPropertiesList.AsReadOnly();
internal bool InternalExpandsMainDataModel { get; set; } internal DataModel? InternalDataModel { get; set; }
internal ModuleSettingsEntity? SettingsEntity { get; set; }
/// <summary> /// <summary>
/// Called each frame when the module should update /// Called each frame when the module should update
@ -143,14 +150,6 @@ namespace Artemis.Core.Modules
/// <param name="deltaTime">Time in seconds since the last update</param> /// <param name="deltaTime">Time in seconds since the last update</param>
public abstract void Update(double deltaTime); public abstract void Update(double deltaTime);
/// <summary>
/// Called each frame when the module should render
/// </summary>
/// <param name="deltaTime">Time since the last render</param>
/// <param name="canvas"></param>
/// <param name="canvasInfo"></param>
public abstract void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo);
/// <summary> /// <summary>
/// Called when the <see cref="ActivationRequirements" /> are met or during an override /// Called when the <see cref="ActivationRequirements" /> are met or during an override
/// </summary> /// </summary>
@ -158,7 +157,9 @@ namespace Artemis.Core.Modules
/// If true, the activation was due to an override. This usually means the module was activated /// If true, the activation was due to an override. This usually means the module was activated
/// by the profile editor /// by the profile editor
/// </param> /// </param>
public abstract void ModuleActivated(bool isOverride); public virtual void ModuleActivated(bool isOverride)
{
}
/// <summary> /// <summary>
/// Called when the <see cref="ActivationRequirements" /> are no longer met or during an override /// Called when the <see cref="ActivationRequirements" /> are no longer met or during an override
@ -167,7 +168,9 @@ namespace Artemis.Core.Modules
/// If true, the deactivation was due to an override. This usually means the module was deactivated /// If true, the deactivation was due to an override. This usually means the module was deactivated
/// by the profile editor /// by the profile editor
/// </param> /// </param>
public abstract void ModuleDeactivated(bool isOverride); public virtual void ModuleDeactivated(bool isOverride)
{
}
/// <summary> /// <summary>
/// Evaluates the activation requirements following the <see cref="ActivationRequirementMode" /> and returns the result /// Evaluates the activation requirements following the <see cref="ActivationRequirementMode" /> and returns the result
@ -185,6 +188,50 @@ namespace Artemis.Core.Modules
return false; return false;
} }
/// <summary>
/// Override to provide your own data model description. By default this returns a description matching your plugin
/// name and description
/// </summary>
/// <returns></returns>
public virtual DataModelPropertyAttribute GetDataModelDescription()
{
return new() {Name = Plugin.Info.Name, Description = Plugin.Info.Description};
}
/// <summary>
/// Adds a default profile by reading it from the file found at the provided path
/// </summary>
/// <param name="category">The category in which to place the default profile</param>
/// <param name="file">A path pointing towards a profile file. May be relative to the plugin directory.</param>
/// <returns>
/// <see langword="true" /> if the default profile was added; <see langword="false" /> if it was not because it is
/// already in the list.
/// </returns>
protected bool AddDefaultProfile(DefaultCategoryName category, string file)
{
// It can be null if the plugin has not loaded yet in which case Plugin.ResolveRelativePath fails
if (Plugin == null!)
{
if (_pendingDefaultProfilePaths.Contains((category, file)))
return false;
_pendingDefaultProfilePaths.Add((category, file));
return true;
}
if (!Path.IsPathRooted(file))
file = Plugin.ResolveRelativePath(file);
// Ensure the file exists
if (!File.Exists(file))
throw new ArtemisPluginFeatureException(this, $"Could not find default profile at {file}.");
if (_defaultProfilePaths.Contains((category, file)))
return false;
_defaultProfilePaths.Add((category, file));
return true;
}
internal virtual void InternalUpdate(double deltaTime) internal virtual void InternalUpdate(double deltaTime)
{ {
StartUpdateMeasure(); StartUpdateMeasure();
@ -193,13 +240,6 @@ namespace Artemis.Core.Modules
StopUpdateMeasure(); StopUpdateMeasure();
} }
internal virtual void InternalRender(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo)
{
StartRenderMeasure();
Render(deltaTime, canvas, canvasInfo);
StopRenderMeasure();
}
internal virtual void Activate(bool isOverride) internal virtual void Activate(bool isOverride)
{ {
if (IsActivated) if (IsActivated)
@ -220,6 +260,20 @@ namespace Artemis.Core.Modules
ModuleDeactivated(isOverride); ModuleDeactivated(isOverride);
} }
#region Overrides of PluginFeature
/// <inheritdoc />
internal override void InternalEnable()
{
foreach ((DefaultCategoryName categoryName, var path) in _pendingDefaultProfilePaths)
AddDefaultProfile(categoryName, path);
_pendingDefaultProfilePaths.Clear();
base.InternalEnable();
}
#endregion
internal virtual void Reactivate(bool isDeactivateOverride, bool isActivateOverride) internal virtual void Reactivate(bool isDeactivateOverride, bool isActivateOverride)
{ {
if (!IsActivated) if (!IsActivated)
@ -228,16 +282,6 @@ namespace Artemis.Core.Modules
Deactivate(isDeactivateOverride); Deactivate(isDeactivateOverride);
Activate(isActivateOverride); Activate(isActivateOverride);
} }
internal void ApplyToEntity()
{
if (SettingsEntity == null)
SettingsEntity = new ModuleSettingsEntity();
SettingsEntity.ModuleId = Id;
SettingsEntity.PriorityCategory = (int) PriorityCategory;
SettingsEntity.Priority = Priority;
}
} }
/// <summary> /// <summary>
@ -255,25 +299,4 @@ namespace Artemis.Core.Modules
/// </summary> /// </summary>
All All
} }
/// <summary>
/// Describes the priority category of a module
/// </summary>
public enum ModulePriorityCategory
{
/// <summary>
/// Indicates a normal render priority
/// </summary>
Normal,
/// <summary>
/// Indicates that the module renders for a specific application/game, rendering on top of normal modules
/// </summary>
Application,
/// <summary>
/// Indicates that the module renders an overlay, always rendering on top
/// </summary>
Overlay
}
} }

View File

@ -1,44 +0,0 @@
using System;
namespace Artemis.Core.Modules
{
/// <inheritdoc />
public class ModuleTab<T> : ModuleTab where T : IModuleViewModel
{
/// <summary>
/// Creates a new instance of the <see cref="ModuleTab{T}" /> class
/// </summary>
/// <param name="title">The title of the tab</param>
public ModuleTab(string title) : base(title)
{
}
/// <inheritdoc />
public override Type Type => typeof(T);
}
/// <summary>
/// Describes a UI tab for a specific module
/// </summary>
public abstract class ModuleTab
{
/// <summary>
/// Creates a new instance of the <see cref="ModuleTab" /> class
/// </summary>
/// <param name="title">The title of the tab</param>
protected ModuleTab(string title)
{
Title = title;
}
/// <summary>
/// The title of the tab
/// </summary>
public string Title { get; protected set; }
/// <summary>
/// The type of view model the tab contains
/// </summary>
public abstract Type Type { get; }
}
}

View File

@ -1,329 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
using Artemis.Core.DataModelExpansions;
using Artemis.Core.Services;
using Artemis.Storage.Entities.Profile;
using Newtonsoft.Json;
using SkiaSharp;
namespace Artemis.Core.Modules
{
/// <summary>
/// Allows you to add support for new games/applications while utilizing Artemis' profile engine and your own data
/// model
/// </summary>
public abstract class ProfileModule<T> : ProfileModule where T : DataModel
{
/// <summary>
/// The data model driving this module
/// <para>Note: This default data model is automatically registered upon plugin enable</para>
/// </summary>
public T DataModel
{
get => InternalDataModel as T ?? throw new InvalidOperationException("Internal datamodel does not match the type of the data model");
internal set => InternalDataModel = value;
}
/// <summary>
/// Gets or sets whether this module must also expand the main data model
/// <para>
/// Note: If expanding the main data model is all you want your plugin to do, create a
/// <see cref="BaseDataModelExpansion" /> plugin instead.
/// </para>
/// </summary>
public bool ExpandsDataModel
{
get => InternalExpandsMainDataModel;
set => InternalExpandsMainDataModel = value;
}
/// <summary>
/// Override to provide your own data model description. By default this returns a description matching your plugin
/// name and description
/// </summary>
/// <returns></returns>
public virtual DataModelPropertyAttribute GetDataModelDescription()
{
return new() {Name = Plugin.Info.Name, Description = Plugin.Info.Description};
}
/// <summary>
/// Hide the provided property using a lambda expression, e.g. HideProperty(dm => dm.TimeDataModel.CurrentTimeUTC)
/// </summary>
/// <param name="propertyLambda">A lambda expression pointing to the property to ignore</param>
public void HideProperty<TProperty>(Expression<Func<T, TProperty>> propertyLambda)
{
PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda);
if (!HiddenPropertiesList.Any(p => p.Equals(propertyInfo)))
HiddenPropertiesList.Add(propertyInfo);
}
/// <summary>
/// Stop hiding the provided property using a lambda expression, e.g. ShowProperty(dm =>
/// dm.TimeDataModel.CurrentTimeUTC)
/// </summary>
/// <param name="propertyLambda">A lambda expression pointing to the property to stop ignoring</param>
public void ShowProperty<TProperty>(Expression<Func<T, TProperty>> propertyLambda)
{
PropertyInfo propertyInfo = ReflectionUtilities.GetPropertyInfo(DataModel, propertyLambda);
HiddenPropertiesList.RemoveAll(p => p.Equals(propertyInfo));
}
internal override void InternalEnable()
{
DataModel = Activator.CreateInstance<T>();
DataModel.Feature = this;
DataModel.DataModelDescription = GetDataModelDescription();
base.InternalEnable();
}
internal override void InternalDisable()
{
Deactivate(true);
base.InternalDisable();
}
}
/// <summary>
/// Allows you to add support for new games/applications while utilizing Artemis' profile engine
/// </summary>
public abstract class ProfileModule : Module
{
private readonly List<string> _defaultProfilePaths = new();
private readonly List<string> _pendingDefaultProfilePaths = new();
private readonly List<ProfileEntity> _defaultProfiles = new();
private readonly object _lock = new();
/// <summary>
/// Gets a list of all properties ignored at runtime using <c>IgnoreProperty(x => x.y)</c>
/// </summary>
protected internal readonly List<PropertyInfo> HiddenPropertiesList = new();
/// <summary>
/// Creates a new instance of the <see cref="ProfileModule" /> class
/// </summary>
protected ProfileModule()
{
OpacityOverride = 1;
}
/// <summary>
/// Gets a list of all properties ignored at runtime using <c>IgnoreProperty(x => x.y)</c>
/// </summary>
public ReadOnlyCollection<PropertyInfo> HiddenProperties => HiddenPropertiesList.AsReadOnly();
/// <summary>
/// Gets the currently active profile
/// </summary>
public Profile? ActiveProfile { get; private set; }
/// <summary>
/// Disables updating the profile, rendering does continue
/// </summary>
public bool IsProfileUpdatingDisabled { get; set; }
/// <summary>
/// Overrides the opacity of the root folder
/// </summary>
public double OpacityOverride { get; set; }
/// <summary>
/// Indicates whether or not a profile change is being animated
/// </summary>
public bool AnimatingProfileChange { get; private set; }
/// <summary>
/// Gets a list of default profiles, to add a new default profile use <see cref="AddDefaultProfile" />
/// </summary>
internal ReadOnlyCollection<ProfileEntity> DefaultProfiles => _defaultProfiles.AsReadOnly();
/// <summary>
/// Called after the profile has updated
/// </summary>
/// <param name="deltaTime">Time in seconds since the last update</param>
public virtual void ProfileUpdated(double deltaTime)
{
}
/// <summary>
/// Called after the profile has rendered
/// </summary>
/// <param name="deltaTime">Time since the last render</param>
/// <param name="canvas"></param>
/// <param name="canvasInfo"></param>
public virtual void ProfileRendered(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo)
{
}
/// <summary>
/// Occurs when the <see cref="ActiveProfile" /> has changed
/// </summary>
public event EventHandler? ActiveProfileChanged;
/// <summary>
/// Adds a default profile by reading it from the file found at the provided path
/// </summary>
/// <param name="file">A path pointing towards a profile file. May be relative to the plugin directory.</param>
/// <returns>
/// <see langword="true" /> if the default profile was added; <see langword="false" /> if it was not because it is
/// already in the list.
/// </returns>
protected bool AddDefaultProfile(string file)
{
// It can be null if the plugin has not loaded yet...
if (Plugin == null!)
{
if (_pendingDefaultProfilePaths.Contains(file))
return false;
_pendingDefaultProfilePaths.Add(file);
return true;
}
if (!Path.IsPathRooted(file))
file = Plugin.ResolveRelativePath(file);
if (_defaultProfilePaths.Contains(file))
return false;
_defaultProfilePaths.Add(file);
// Ensure the file exists
if (!File.Exists(file))
throw new ArtemisPluginFeatureException(this, $"Could not find default profile at {file}.");
// Deserialize and make sure that succeeded
ProfileEntity? profileEntity = JsonConvert.DeserializeObject<ProfileEntity>(File.ReadAllText(file), ProfileService.ExportSettings);
if (profileEntity == null)
throw new ArtemisPluginFeatureException(this, $"Failed to deserialize default profile at {file}.");
// Ensure the profile ID is unique
if (_defaultProfiles.Any(d => d.Id == profileEntity.Id))
throw new ArtemisPluginFeatureException(this, $"Cannot add default profile from {file}, profile ID {profileEntity.Id} already in use.");
profileEntity.IsFreshImport = true;
profileEntity.IsActive = false;
_defaultProfiles.Add(profileEntity);
return true;
}
/// <summary>
/// Invokes the <see cref="ActiveProfileChanged" /> event
/// </summary>
protected virtual void OnActiveProfileChanged()
{
ActiveProfileChanged?.Invoke(this, EventArgs.Empty);
}
internal override void InternalEnable()
{
foreach (string pendingDefaultProfile in _pendingDefaultProfilePaths)
AddDefaultProfile(pendingDefaultProfile);
_pendingDefaultProfilePaths.Clear();
base.InternalEnable();
}
internal override void InternalUpdate(double deltaTime)
{
StartUpdateMeasure();
if (IsUpdateAllowed)
Update(deltaTime);
lock (_lock)
{
OpacityOverride = AnimatingProfileChange
? Math.Max(0, OpacityOverride - 0.1)
: Math.Min(1, OpacityOverride + 0.1);
// Update the profile
if (!IsProfileUpdatingDisabled)
ActiveProfile?.Update(deltaTime);
}
ProfileUpdated(deltaTime);
StopUpdateMeasure();
}
internal override void InternalRender(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo)
{
StartRenderMeasure();
Render(deltaTime, canvas, canvasInfo);
lock (_lock)
{
// Render the profile
ActiveProfile?.Render(canvas, SKPointI.Empty);
}
ProfileRendered(deltaTime, canvas, canvasInfo);
StopRenderMeasure();
}
internal async Task ChangeActiveProfileAnimated(Profile? profile, IEnumerable<ArtemisDevice> devices)
{
if (profile != null && profile.Module != this)
throw new ArtemisCoreException($"Cannot activate a profile of module {profile.Module} on a module of plugin {this}.");
if (!IsActivated)
throw new ArtemisCoreException("Cannot activate a profile on a deactivated module");
if (profile == ActiveProfile || AnimatingProfileChange)
return;
AnimatingProfileChange = true;
while (OpacityOverride > 0)
await Task.Delay(50);
ChangeActiveProfile(profile, devices);
AnimatingProfileChange = false;
while (OpacityOverride < 1)
await Task.Delay(50);
}
internal void ChangeActiveProfile(Profile? profile, IEnumerable<ArtemisDevice> devices)
{
if (profile != null && profile.Module != this)
throw new ArtemisCoreException($"Cannot activate a profile of module {profile.Module} on a module of plugin {this}.");
if (!IsActivated)
throw new ArtemisCoreException("Cannot activate a profile on a deactivated module");
lock (_lock)
{
if (profile == ActiveProfile)
return;
ActiveProfile?.Dispose();
ActiveProfile = profile;
ActiveProfile?.Activate(devices);
}
OnActiveProfileChanged();
}
internal override void Deactivate(bool isOverride)
{
base.Deactivate(isOverride);
Profile? profile = ActiveProfile;
ActiveProfile = null;
profile?.Dispose();
}
internal override void Reactivate(bool isDeactivateOverride, bool isActivateOverride)
{
if (!IsActivated)
return;
// Avoid disposing the profile
base.Deactivate(isDeactivateOverride);
Activate(isActivateOverride);
}
}
}

View File

@ -35,14 +35,10 @@ namespace Artemis.Core
AlwaysEnabled = attribute?.AlwaysEnabled ?? false; AlwaysEnabled = attribute?.AlwaysEnabled ?? false;
if (Icon != null) return; if (Icon != null) return;
if (typeof(BaseDataModelExpansion).IsAssignableFrom(featureType)) if (typeof(DeviceProvider).IsAssignableFrom(featureType))
Icon = "TableAdd";
else if (typeof(DeviceProvider).IsAssignableFrom(featureType))
Icon = "Devices"; Icon = "Devices";
else if (typeof(ProfileModule).IsAssignableFrom(featureType))
Icon = "VectorRectangle";
else if (typeof(Module).IsAssignableFrom(featureType)) else if (typeof(Module).IsAssignableFrom(featureType))
Icon = "GearBox"; Icon = "VectorRectangle";
else if (typeof(LayerBrushProvider).IsAssignableFrom(featureType)) else if (typeof(LayerBrushProvider).IsAssignableFrom(featureType))
Icon = "Brush"; Icon = "Brush";
else if (typeof(LayerEffectProvider).IsAssignableFrom(featureType)) else if (typeof(LayerEffectProvider).IsAssignableFrom(featureType))
@ -66,10 +62,8 @@ namespace Artemis.Core
if (Icon != null) return; if (Icon != null) return;
Icon = Instance switch Icon = Instance switch
{ {
BaseDataModelExpansion => "TableAdd",
DeviceProvider => "Devices", DeviceProvider => "Devices",
ProfileModule => "VectorRectangle", Module => "VectorRectangle",
Module => "GearBox",
LayerBrushProvider => "Brush", LayerBrushProvider => "Brush",
LayerEffectProvider => "AutoAwesome", LayerEffectProvider => "AutoAwesome",
_ => "Plugin" _ => "Plugin"

View File

@ -29,11 +29,10 @@ namespace Artemis.Core.Services
private readonly PluginSetting<LogEventLevel> _loggingLevel; private readonly PluginSetting<LogEventLevel> _loggingLevel;
private readonly IPluginManagementService _pluginManagementService; private readonly IPluginManagementService _pluginManagementService;
private readonly IProfileService _profileService; private readonly IProfileService _profileService;
private readonly IModuleService _moduleService;
private readonly IRgbService _rgbService; private readonly IRgbService _rgbService;
private readonly List<Exception> _updateExceptions = new(); private readonly List<Exception> _updateExceptions = new();
private List<BaseDataModelExpansion> _dataModelExpansions = new();
private DateTime _lastExceptionLog; private DateTime _lastExceptionLog;
private List<Module> _modules = new();
// ReSharper disable UnusedParameter.Local // ReSharper disable UnusedParameter.Local
public CoreService(IKernel kernel, public CoreService(IKernel kernel,
@ -43,8 +42,7 @@ namespace Artemis.Core.Services
IPluginManagementService pluginManagementService, IPluginManagementService pluginManagementService,
IRgbService rgbService, IRgbService rgbService,
IProfileService profileService, IProfileService profileService,
IModuleService moduleService // injected to ensure module priorities get applied IModuleService moduleService)
)
{ {
Kernel = kernel; Kernel = kernel;
Constants.CorePlugin.Kernel = kernel; Constants.CorePlugin.Kernel = kernel;
@ -53,18 +51,14 @@ namespace Artemis.Core.Services
_pluginManagementService = pluginManagementService; _pluginManagementService = pluginManagementService;
_rgbService = rgbService; _rgbService = rgbService;
_profileService = profileService; _profileService = profileService;
_moduleService = moduleService;
_loggingLevel = settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Debug); _loggingLevel = settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Debug);
_frameStopWatch = new Stopwatch(); _frameStopWatch = new Stopwatch();
StartupArguments = new List<string>(); StartupArguments = new List<string>();
UpdatePluginCache();
_rgbService.IsRenderPaused = true; _rgbService.IsRenderPaused = true;
_rgbService.Surface.Updating += SurfaceOnUpdating; _rgbService.Surface.Updating += SurfaceOnUpdating;
_loggingLevel.SettingChanged += (sender, args) => ApplyLoggingLevel(); _loggingLevel.SettingChanged += (sender, args) => ApplyLoggingLevel();
_pluginManagementService.PluginFeatureEnabled += (sender, args) => UpdatePluginCache();
_pluginManagementService.PluginFeatureDisabled += (sender, args) => UpdatePluginCache();
} }
// ReSharper restore UnusedParameter.Local // ReSharper restore UnusedParameter.Local
@ -79,12 +73,6 @@ namespace Artemis.Core.Services
FrameRendered?.Invoke(this, e); FrameRendered?.Invoke(this, e);
} }
private void UpdatePluginCache()
{
_modules = _pluginManagementService.GetFeaturesOfType<Module>().Where(p => p.IsEnabled).ToList();
_dataModelExpansions = _pluginManagementService.GetFeaturesOfType<BaseDataModelExpansion>().Where(p => p.IsEnabled).ToList();
}
private void ApplyLoggingLevel() private void ApplyLoggingLevel()
{ {
string? argument = StartupArguments.FirstOrDefault(a => a.StartsWith("--logging")); string? argument = StartupArguments.FirstOrDefault(a => a.StartsWith("--logging"));
@ -120,67 +108,18 @@ namespace Artemis.Core.Services
{ {
_frameStopWatch.Restart(); _frameStopWatch.Restart();
// Render all active modules _moduleService.UpdateActiveModules(args.DeltaTime);
SKTexture texture = _rgbService.OpenRender(); SKTexture texture = _rgbService.OpenRender();
lock (_dataModelExpansions)
{
// Update all active modules, check Enabled status because it may go false before before the _dataModelExpansions list is updated
foreach (BaseDataModelExpansion dataModelExpansion in _dataModelExpansions.Where(e => e.IsEnabled))
{
try
{
dataModelExpansion.InternalUpdate(args.DeltaTime);
}
catch (Exception e)
{
_updateExceptions.Add(e);
}
}
}
List<Module> modules;
lock (_modules)
{
modules = _modules.Where(m => m.IsActivated || m.InternalExpandsMainDataModel)
.OrderBy(m => m.PriorityCategory)
.ThenByDescending(m => m.Priority)
.ToList();
}
// Update all active modules
foreach (Module module in modules)
{
try
{
module.InternalUpdate(args.DeltaTime);
}
catch (Exception e)
{
_updateExceptions.Add(e);
}
}
SKCanvas canvas = texture.Surface.Canvas; SKCanvas canvas = texture.Surface.Canvas;
canvas.Save(); canvas.Save();
if (Math.Abs(texture.RenderScale - 1) > 0.001) if (Math.Abs(texture.RenderScale - 1) > 0.001)
canvas.Scale(texture.RenderScale); canvas.Scale(texture.RenderScale);
canvas.Clear(new SKColor(0, 0, 0)); canvas.Clear(new SKColor(0, 0, 0));
// While non-activated modules may be updated above if they expand the main data model, they may never render if (!ProfileRenderingDisabled)
if (!ModuleRenderingDisabled)
{ {
foreach (Module module in modules.Where(m => m.IsActivated)) _profileService.UpdateProfiles(args.DeltaTime);
{ _profileService.RenderProfiles(canvas);
try
{
module.InternalRender(args.DeltaTime, canvas, texture.ImageInfo);
}
catch (Exception e)
{
_updateExceptions.Add(e);
}
}
} }
OnFrameRendering(new FrameRenderingEventArgs(canvas, args.DeltaTime, _rgbService.Surface)); OnFrameRendering(new FrameRenderingEventArgs(canvas, args.DeltaTime, _rgbService.Surface));
@ -228,7 +167,7 @@ namespace Artemis.Core.Services
} }
public TimeSpan FrameTime { get; private set; } public TimeSpan FrameTime { get; private set; }
public bool ModuleRenderingDisabled { get; set; } public bool ProfileRenderingDisabled { get; set; }
public List<string> StartupArguments { get; set; } public List<string> StartupArguments { get; set; }
public bool IsElevated { get; set; } public bool IsElevated { get; set; }
@ -272,25 +211,6 @@ namespace Artemis.Core.Services
OnInitialized(); OnInitialized();
} }
public void PlayIntroAnimation()
{
IntroAnimation intro = new(_logger, _profileService, _rgbService.EnabledDevices);
// Draw a white overlay over the device
void DrawOverlay(object? sender, FrameRenderingEventArgs args)
{
if (intro.AnimationProfile.GetAllLayers().All(l => l.Timeline.IsFinished))
{
FrameRendering -= DrawOverlay;
intro.AnimationProfile.Dispose();
}
intro.Render(args.DeltaTime, args.Canvas);
}
FrameRendering += DrawOverlay;
}
public event EventHandler? Initialized; public event EventHandler? Initialized;
public event EventHandler<FrameRenderingEventArgs>? FrameRendering; public event EventHandler<FrameRenderingEventArgs>? FrameRendering;
public event EventHandler<FrameRenderedEventArgs>? FrameRendered; public event EventHandler<FrameRenderedEventArgs>? FrameRendered;

View File

@ -19,9 +19,9 @@ namespace Artemis.Core.Services
TimeSpan FrameTime { get; } TimeSpan FrameTime { get; }
/// <summary> /// <summary>
/// Gets or sets whether modules are rendered each frame by calling their Render method /// Gets or sets whether profiles are rendered each frame by calling their Render method
/// </summary> /// </summary>
bool ModuleRenderingDisabled { get; set; } bool ProfileRenderingDisabled { get; set; }
/// <summary> /// <summary>
/// Gets or sets a list of startup arguments /// Gets or sets a list of startup arguments
@ -38,11 +38,6 @@ namespace Artemis.Core.Services
/// </summary> /// </summary>
void Initialize(); void Initialize();
/// <summary>
/// Plays the into animation profile defined in <c>Resources/intro-profile.json</c>
/// </summary>
void PlayIntroAnimation();
/// <summary> /// <summary>
/// Occurs the core has finished initializing /// Occurs the core has finished initializing
/// </summary> /// </summary>

View File

@ -1,5 +1,5 @@
using System; using System;
using System.Threading.Tasks; using System.Collections.Generic;
using Artemis.Core.Modules; using Artemis.Core.Modules;
namespace Artemis.Core.Services namespace Artemis.Core.Services
@ -10,33 +10,29 @@ namespace Artemis.Core.Services
public interface IModuleService : IArtemisService public interface IModuleService : IArtemisService
{ {
/// <summary> /// <summary>
/// Gets the current active module override. If set, all other modules are deactivated and only the /// Updates all currently active modules
/// <see cref="ActiveModuleOverride" /> is active.
/// </summary> /// </summary>
Module? ActiveModuleOverride { get; } /// <param name="deltaTime"></param>
void UpdateActiveModules(double deltaTime);
/// <summary>
/// Changes the current <see cref="ActiveModuleOverride" /> and deactivates all other modules
/// </summary>
/// <param name="overrideModule"></param>
Task SetActiveModuleOverride(Module? overrideModule);
/// <summary> /// <summary>
/// Evaluates every enabled module's activation requirements and activates/deactivates modules accordingly /// Evaluates every enabled module's activation requirements and activates/deactivates modules accordingly
/// </summary> /// </summary>
Task UpdateModuleActivation(); void UpdateModuleActivation();
/// <summary> /// <summary>
/// Updates the priority and priority category of the given module /// Overrides activation on the provided module and restores regular activation to any remaining modules
/// </summary> /// </summary>
/// <param name="module">The module to update</param> void SetActivationOverride(Module? module);
/// <param name="category">The new priority category of the module</param>
/// <param name="priority">The new priority of the module</param>
void UpdateModulePriority(Module module, ModulePriorityCategory category, int priority);
/// <summary> /// <summary>
/// Occurs when the priority of a module is updated. /// Occurs whenever a module is activated
/// </summary> /// </summary>
event EventHandler? ModulePriorityUpdated; event EventHandler<ModuleEventArgs> ModuleActivated;
/// <summary>
/// Occurs whenever a module is deactivated
/// </summary>
event EventHandler<ModuleEventArgs> ModuleDeactivated;
} }
} }

View File

@ -1,103 +1,48 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.IO;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Timers; using System.Timers;
using Artemis.Core.Modules; using Artemis.Core.Modules;
using Artemis.Storage.Entities.Profile; using Newtonsoft.Json;
using Artemis.Storage.Repositories.Interfaces;
using Serilog; using Serilog;
using Timer = System.Timers.Timer;
namespace Artemis.Core.Services namespace Artemis.Core.Services
{ {
internal class ModuleService : IModuleService internal class ModuleService : IModuleService
{ {
private static readonly SemaphoreSlim ActiveModuleSemaphore = new(1, 1); private readonly Timer _activationUpdateTimer;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IModuleRepository _moduleRepository;
private readonly IProfileRepository _profileRepository;
private readonly IPluginManagementService _pluginManagementService;
private readonly IProfileService _profileService; private readonly IProfileService _profileService;
private readonly List<Module> _modules;
private readonly object _updateLock = new();
public ModuleService(ILogger logger, IModuleRepository moduleRepository, IProfileRepository profileRepository, IPluginManagementService pluginManagementService, IProfileService profileService) private Module? _activationOverride;
public ModuleService(ILogger logger, IPluginManagementService pluginManagementService, IProfileService profileService)
{ {
_logger = logger; _logger = logger;
_moduleRepository = moduleRepository;
_profileRepository = profileRepository;
_pluginManagementService = pluginManagementService;
_profileService = profileService; _profileService = profileService;
_pluginManagementService.PluginFeatureEnabled += OnPluginFeatureEnabled;
Timer activationUpdateTimer = new(2000); _activationUpdateTimer = new Timer(2000);
activationUpdateTimer.Start(); _activationUpdateTimer.Start();
activationUpdateTimer.Elapsed += ActivationUpdateTimerOnElapsed; _activationUpdateTimer.Elapsed += ActivationUpdateTimerOnElapsed;
foreach (Module module in _pluginManagementService.GetFeaturesOfType<Module>()) pluginManagementService.PluginFeatureEnabled += PluginManagementServiceOnPluginFeatureEnabled;
InitialiseOrApplyPriority(module); pluginManagementService.PluginFeatureDisabled += PluginManagementServiceOnPluginFeatureDisabled;
_modules = pluginManagementService.GetFeaturesOfType<Module>().ToList();
foreach (Module module in _modules)
ImportDefaultProfiles(module);
} }
private async void ActivationUpdateTimerOnElapsed(object sender, ElapsedEventArgs e) protected virtual void OnModuleActivated(ModuleEventArgs e)
{ {
await UpdateModuleActivation(); ModuleActivated?.Invoke(this, e);
} }
private async Task ActivateModule(Module module) protected virtual void OnModuleDeactivated(ModuleEventArgs e)
{ {
try ModuleDeactivated?.Invoke(this, e);
{
ProfileModule? profileModule = module as ProfileModule;
if (profileModule != null && profileModule.DefaultProfiles.Any())
{
List<ProfileDescriptor> descriptors = _profileService.GetProfileDescriptors(profileModule);
foreach (ProfileEntity defaultProfile in profileModule.DefaultProfiles)
{
if (descriptors.All(d => d.Id != defaultProfile.Id))
_profileRepository.Add(defaultProfile);
}
}
module.Activate(false);
try
{
// If this is a profile module, activate the last active profile after module activation
if (profileModule != null)
await _profileService.ActivateLastProfileAnimated(profileModule);
}
catch (Exception e)
{
_logger.Warning(e, $"Failed to activate last profile on module {module}");
}
}
catch (Exception e)
{
_logger.Error(new ArtemisPluginFeatureException(module, "Failed to activate module.", e), "Failed to activate module");
throw;
}
}
private async Task DeactivateModule(Module module)
{
try
{
// If this is a profile module, animate profile disable
// module.Deactivate would do the same but without animation
if (module.IsActivated && module is ProfileModule profileModule)
await profileModule.ChangeActiveProfileAnimated(null, Enumerable.Empty<ArtemisDevice>());
module.Deactivate(false);
}
catch (Exception e)
{
_logger.Error(new ArtemisPluginFeatureException(
module, "Failed to deactivate module and last profile.", e), "Failed to deactivate module and last profile"
);
throw;
}
} }
private void OverrideActivate(Module module) private void OverrideActivate(Module module)
@ -110,21 +55,15 @@ namespace Artemis.Core.Services
// If activating while it should be deactivated, its an override // If activating while it should be deactivated, its an override
bool shouldBeActivated = module.EvaluateActivationRequirements(); bool shouldBeActivated = module.EvaluateActivationRequirements();
module.Activate(!shouldBeActivated); module.Activate(!shouldBeActivated);
// If this is a profile module, activate the last active profile after module activation
if (module is ProfileModule profileModule)
_profileService.ActivateLastProfile(profileModule);
} }
catch (Exception e) catch (Exception e)
{ {
_logger.Error(new ArtemisPluginFeatureException( _logger.Error(new ArtemisPluginFeatureException(module, "Failed to activate module.", e), "Failed to activate module");
module, "Failed to activate module and last profile.", e), "Failed to activate module and last profile"
);
throw; throw;
} }
} }
private void OverrideDeactivate(Module module, bool clearingOverride) private void OverrideDeactivate(Module module)
{ {
try try
{ {
@ -134,163 +73,132 @@ namespace Artemis.Core.Services
// If deactivating while it should be activated, its an override // If deactivating while it should be activated, its an override
bool shouldBeActivated = module.EvaluateActivationRequirements(); bool shouldBeActivated = module.EvaluateActivationRequirements();
// No need to deactivate if it is not in an overridden state // No need to deactivate if it is not in an overridden state
if (shouldBeActivated && !module.IsActivatedOverride && !clearingOverride) if (shouldBeActivated && !module.IsActivatedOverride)
return; return;
module.Deactivate(true); module.Deactivate(true);
} }
catch (Exception e) catch (Exception e)
{ {
_logger.Error(new ArtemisPluginFeatureException( _logger.Error(new ArtemisPluginFeatureException(module, "Failed to deactivate module.", e), "Failed to deactivate module");
module, "Failed to deactivate module and last profile.", e), "Failed to deactivate module and last profile"
);
throw; throw;
} }
} }
private void OnPluginFeatureEnabled(object? sender, PluginFeatureEventArgs e) private void ActivationUpdateTimerOnElapsed(object sender, ElapsedEventArgs e)
{
UpdateModuleActivation();
}
private void PluginManagementServiceOnPluginFeatureEnabled(object? sender, PluginFeatureEventArgs e)
{
lock (_updateLock)
{
if (e.PluginFeature is Module module && !_modules.Contains(module))
{
ImportDefaultProfiles(module);
_modules.Add(module);
}
}
}
private void PluginManagementServiceOnPluginFeatureDisabled(object? sender, PluginFeatureEventArgs e)
{
lock (_updateLock)
{ {
if (e.PluginFeature is Module module) if (e.PluginFeature is Module module)
InitialiseOrApplyPriority(module); _modules.Remove(module);
}
} }
private void InitialiseOrApplyPriority(Module module) private void ImportDefaultProfiles(Module module)
{
ModulePriorityCategory category = module.DefaultPriorityCategory;
int priority = 1;
module.SettingsEntity = _moduleRepository.GetByModuleId(module.Id);
if (module.SettingsEntity != null)
{
category = (ModulePriorityCategory) module.SettingsEntity.PriorityCategory;
priority = module.SettingsEntity.Priority;
}
UpdateModulePriority(module, category, priority);
}
public Module? ActiveModuleOverride { get; private set; }
public async Task SetActiveModuleOverride(Module? overrideModule)
{ {
try try
{ {
await ActiveModuleSemaphore.WaitAsync(); List<ProfileConfiguration> profileConfigurations = _profileService.ProfileCategories.SelectMany(c => c.ProfileConfigurations).ToList();
foreach ((DefaultCategoryName categoryName, string profilePath) in module.DefaultProfilePaths)
if (ActiveModuleOverride == overrideModule)
return;
if (overrideModule != null)
{ {
OverrideActivate(overrideModule); ProfileConfigurationExportModel? profileConfigurationExportModel = JsonConvert.DeserializeObject<ProfileConfigurationExportModel>(File.ReadAllText(profilePath), IProfileService.ExportSettings);
_logger.Information($"Setting active module override to {overrideModule.DisplayName}"); if (profileConfigurationExportModel?.ProfileEntity == null)
throw new ArtemisCoreException($"Default profile at path {profilePath} contains no valid profile data");
if (profileConfigurations.Any(p => p.Entity.ProfileId == profileConfigurationExportModel.ProfileEntity.Id))
continue;
ProfileCategory category = _profileService.ProfileCategories.FirstOrDefault(c => c.Name == categoryName.ToString()) ??
_profileService.CreateProfileCategory(categoryName.ToString());
_profileService.ImportProfile(category, profileConfigurationExportModel, false, true, null);
} }
else }
catch (Exception e)
{ {
_logger.Information("Clearing active module override"); _logger.Warning(e, "Failed to import default profiles for module {module}", module);
}
} }
// Always deactivate all other modules whenever override is called public void UpdateModuleActivation()
List<Module> modules = _pluginManagementService.GetFeaturesOfType<Module>().ToList();
foreach (Module module in modules.Where(m => m != overrideModule))
OverrideDeactivate(module, overrideModule != null);
ActiveModuleOverride = overrideModule;
}
finally
{ {
ActiveModuleSemaphore.Release(); lock (_updateLock)
}
}
public async Task UpdateModuleActivation()
{ {
if (ActiveModuleSemaphore.CurrentCount == 0)
return;
try try
{ {
await ActiveModuleSemaphore.WaitAsync(); _activationUpdateTimer.Elapsed -= ActivationUpdateTimerOnElapsed;
foreach (Module module in _modules)
if (ActiveModuleOverride != null)
{ {
// The conditions of the active module override may be matched, in that case reactivate as a non-override if (module.IsActivatedOverride)
// the principle is different for this service but not for the module continue;
bool shouldBeActivated = ActiveModuleOverride.EvaluateActivationRequirements();
if (shouldBeActivated && ActiveModuleOverride.IsActivatedOverride)
ActiveModuleOverride.Reactivate(true, false);
else if (!shouldBeActivated && !ActiveModuleOverride.IsActivatedOverride) ActiveModuleOverride.Reactivate(false, true);
return; if (module.IsAlwaysAvailable)
{
module.Activate(false);
continue;
} }
Stopwatch stopwatch = new(); module.Profiler.StartMeasurement("EvaluateActivationRequirements");
stopwatch.Start(); bool shouldBeActivated = module.IsEnabled && module.EvaluateActivationRequirements();
module.Profiler.StopMeasurement("EvaluateActivationRequirements");
List<Module> modules = _pluginManagementService.GetFeaturesOfType<Module>().ToList();
List<Task> tasks = new();
foreach (Module module in modules)
{
lock (module)
{
bool shouldBeActivated = module.EvaluateActivationRequirements() && module.IsEnabled;
if (shouldBeActivated && !module.IsActivated) if (shouldBeActivated && !module.IsActivated)
tasks.Add(ActivateModule(module)); {
module.Activate(false);
OnModuleActivated(new ModuleEventArgs(module));
}
else if (!shouldBeActivated && module.IsActivated) else if (!shouldBeActivated && module.IsActivated)
tasks.Add(DeactivateModule(module)); {
module.Deactivate(false);
OnModuleDeactivated(new ModuleEventArgs(module));
} }
} }
await Task.WhenAll(tasks);
stopwatch.Stop();
if (stopwatch.ElapsedMilliseconds > 100 && !tasks.Any())
_logger.Warning("Activation requirements evaluation took too long: {moduleCount} module(s) in {elapsed}", modules.Count, stopwatch.Elapsed);
} }
finally finally
{ {
ActiveModuleSemaphore.Release(); _activationUpdateTimer.Elapsed += ActivationUpdateTimerOnElapsed;
}
} }
} }
public void UpdateModulePriority(Module module, ModulePriorityCategory category, int priority) public void SetActivationOverride(Module? module)
{ {
if (module.PriorityCategory == category && module.Priority == priority) lock (_updateLock)
return;
List<Module> modules = _pluginManagementService
.GetFeaturesOfType<Module>()
.Where(m => m.PriorityCategory == category)
.OrderBy(m => m.Priority)
.ToList();
if (modules.Contains(module))
modules.Remove(module);
priority = Math.Min(modules.Count, Math.Max(0, priority));
modules.Insert(priority, module);
module.PriorityCategory = category;
for (int index = 0; index < modules.Count; index++)
{ {
Module categoryModule = modules[index]; if (_activationOverride != null)
categoryModule.Priority = index; OverrideDeactivate(_activationOverride);
_activationOverride = module;
if (_activationOverride != null)
OverrideActivate(_activationOverride);
}
}
// Don't save modules whose priority hasn't been initialized yet public void UpdateActiveModules(double deltaTime)
if (categoryModule == module || categoryModule.SettingsEntity != null)
{ {
categoryModule.ApplyToEntity(); lock (_updateLock)
_moduleRepository.Save(categoryModule.SettingsEntity); {
foreach (Module module in _modules)
module.InternalUpdate(deltaTime);
} }
} }
ModulePriorityUpdated?.Invoke(this, EventArgs.Empty); public event EventHandler<ModuleEventArgs>? ModuleActivated;
} public event EventHandler<ModuleEventArgs>? ModuleDeactivated;
#region Events
public event EventHandler? ModulePriorityUpdated;
#endregion
} }
} }

View File

@ -13,8 +13,6 @@ namespace Artemis.Core.Services
// Add data models of already loaded plugins // Add data models of already loaded plugins
foreach (Module module in pluginManagementService.GetFeaturesOfType<Module>().Where(p => p.IsEnabled)) foreach (Module module in pluginManagementService.GetFeaturesOfType<Module>().Where(p => p.IsEnabled))
AddModuleDataModel(module); AddModuleDataModel(module);
foreach (BaseDataModelExpansion dataModelExpansion in pluginManagementService.GetFeaturesOfType<BaseDataModelExpansion>().Where(p => p.IsEnabled))
AddDataModelExpansionDataModel(dataModelExpansion);
// Add data models of new plugins when they get enabled // Add data models of new plugins when they get enabled
pluginManagementService.PluginFeatureEnabled += OnPluginFeatureEnabled; pluginManagementService.PluginFeatureEnabled += OnPluginFeatureEnabled;
@ -54,8 +52,6 @@ namespace Artemis.Core.Services
{ {
if (e.PluginFeature is Module module) if (e.PluginFeature is Module module)
AddModuleDataModel(module); AddModuleDataModel(module);
else if (e.PluginFeature is BaseDataModelExpansion dataModelExpansion)
AddDataModelExpansionDataModel(dataModelExpansion);
} }
private void AddModuleDataModel(Module module) private void AddModuleDataModel(Module module)
@ -66,19 +62,8 @@ namespace Artemis.Core.Services
if (module.InternalDataModel.DataModelDescription == null) if (module.InternalDataModel.DataModelDescription == null)
throw new ArtemisPluginFeatureException(module, "Module overrides GetDataModelDescription but returned null"); throw new ArtemisPluginFeatureException(module, "Module overrides GetDataModelDescription but returned null");
module.InternalDataModel.IsExpansion = module.InternalExpandsMainDataModel; module.InternalDataModel.IsExpansion = module.IsAlwaysAvailable;
RegisterDataModel(module.InternalDataModel); RegisterDataModel(module.InternalDataModel);
} }
private void AddDataModelExpansionDataModel(BaseDataModelExpansion dataModelExpansion)
{
if (dataModelExpansion.InternalDataModel == null)
throw new ArtemisCoreException("Cannot add data model expansion that is not enabled");
if (dataModelExpansion.InternalDataModel.DataModelDescription == null)
throw new ArtemisPluginFeatureException(dataModelExpansion, "Data model expansion overrides GetDataModelDescription but returned null");
dataModelExpansion.InternalDataModel.IsExpansion = true;
RegisterDataModel(dataModelExpansion.InternalDataModel);
}
} }
} }

View File

@ -1,6 +1,6 @@
using System.Collections.Generic; using System.Collections.ObjectModel;
using System.Threading.Tasks; using Newtonsoft.Json;
using Artemis.Core.Modules; using SkiaSharp;
namespace Artemis.Core.Services namespace Artemis.Core.Services
{ {
@ -10,122 +10,144 @@ namespace Artemis.Core.Services
public interface IProfileService : IArtemisService public interface IProfileService : IArtemisService
{ {
/// <summary> /// <summary>
/// Creates a new profile for the given module and returns a descriptor pointing to it /// Gets the JSON serializer settings used to create profile mementos
/// </summary> /// </summary>
/// <param name="module">The profile module to create the profile for</param> public static JsonSerializerSettings MementoSettings { get; } = new() {TypeNameHandling = TypeNameHandling.All};
/// <param name="name">The name of the new profile</param>
/// <returns></returns>
ProfileDescriptor CreateProfileDescriptor(ProfileModule module, string name);
/// <summary> /// <summary>
/// Gets a descriptor for each profile stored for the given <see cref="ProfileModule" /> /// Gets the JSON serializer settings used to import/export profiles
/// </summary> /// </summary>
/// <param name="module">The module to return profile descriptors for</param> public static JsonSerializerSettings ExportSettings { get; } = new() {TypeNameHandling = TypeNameHandling.All, Formatting = Formatting.Indented};
/// <returns></returns>
List<ProfileDescriptor> GetProfileDescriptors(ProfileModule module); /// <summary>
/// Gets a read only collection containing all the profile categories
/// </summary>
ReadOnlyCollection<ProfileCategory> ProfileCategories { get; }
/// <summary>
/// Gets a read only collection containing all the profile configurations
/// </summary>
ReadOnlyCollection<ProfileConfiguration> ProfileConfigurations { get; }
/// <summary>
/// Gets or sets a boolean indicating whether rendering should only be done for profiles being edited
/// </summary>
bool RenderForEditor { get; set; }
/// <summary>
/// Activates the profile of the given <see cref="ProfileConfiguration" /> with the currently active surface
/// </summary>
/// <param name="profileConfiguration">The profile configuration of the profile to activate</param>
Profile ActivateProfile(ProfileConfiguration profileConfiguration);
/// <summary>
/// Deactivates the profile of the given <see cref="ProfileConfiguration" /> with the currently active surface
/// </summary>
/// <param name="profileConfiguration">The profile configuration of the profile to activate</param>
void DeactivateProfile(ProfileConfiguration profileConfiguration);
/// <summary>
/// Permanently deletes the profile of the given <see cref="ProfileConfiguration" />
/// </summary>
/// <param name="profileConfiguration">The profile configuration of the profile to delete</param>
void DeleteProfile(ProfileConfiguration profileConfiguration);
/// <summary>
/// Saves the provided <see cref="ProfileCategory" /> and it's <see cref="ProfileConfiguration" />s but not the
/// <see cref="Profile" />s themselves
/// </summary>
/// <param name="profileCategory">The profile category to update</param>
void SaveProfileCategory(ProfileCategory profileCategory);
/// <summary>
/// Creates a new profile category and saves it to persistent storage
/// </summary>
/// <param name="name">The name of the new profile category, must be unique</param>
/// <returns>The newly created profile category</returns>
ProfileCategory CreateProfileCategory(string name);
/// <summary>
/// Permanently deletes the provided profile category
/// </summary>
void DeleteProfileCategory(ProfileCategory profileCategory);
/// <summary>
/// Creates a new profile configuration and adds it to the provided <see cref="ProfileCategory" />
/// </summary>
/// <param name="category">The profile category to add the profile to</param>
/// <param name="name">The name of the new profile configuration</param>
/// <param name="icon">The icon of the new profile configuration</param>
/// <returns>The newly created profile configuration</returns>
ProfileConfiguration CreateProfileConfiguration(ProfileCategory category, string name, string icon);
/// <summary>
/// Removes the provided profile configuration from the <see cref="ProfileCategory" />
/// </summary>
/// <param name="profileConfiguration"></param>
void RemoveProfileConfiguration(ProfileConfiguration profileConfiguration);
/// <summary>
/// Loads the icon of this profile configuration if needed and puts it into <c>ProfileConfiguration.Icon.FileIcon</c>
/// </summary>
void LoadProfileConfigurationIcon(ProfileConfiguration profileConfiguration);
/// <summary>
/// Saves the current icon of this profile
/// </summary>
void SaveProfileConfigurationIcon(ProfileConfiguration profileConfiguration);
/// <summary> /// <summary>
/// Writes the profile to persistent storage /// Writes the profile to persistent storage
/// </summary> /// </summary>
/// <param name="profile"></param> /// <param name="profile"></param>
/// <param name="includeChildren"></param> /// <param name="includeChildren"></param>
void UpdateProfile(Profile profile, bool includeChildren); void SaveProfile(Profile profile, bool includeChildren);
/// <summary> /// <summary>
/// Disposes and permanently deletes the provided profile /// Attempts to restore the profile to the state it had before the last <see cref="SaveProfile" /> call.
/// </summary>
/// <param name="profile">The profile to delete</param>
void DeleteProfile(Profile profile);
/// <summary>
/// Permanently deletes the profile described by the provided profile descriptor
/// </summary>
/// <param name="profileDescriptor">The descriptor pointing to the profile to delete</param>
void DeleteProfile(ProfileDescriptor profileDescriptor);
/// <summary>
/// Activates the last profile of the given profile module
/// </summary>
/// <param name="profileModule"></param>
void ActivateLastProfile(ProfileModule profileModule);
/// <summary>
/// Reloads the currently active profile on the provided profile module
/// </summary>
void ReloadProfile(ProfileModule module);
/// <summary>
/// Asynchronously activates the last profile of the given profile module using a fade animation
/// </summary>
/// <param name="profileModule"></param>
/// <returns></returns>
Task ActivateLastProfileAnimated(ProfileModule profileModule);
/// <summary>
/// Activates the profile described in the given <see cref="ProfileDescriptor" /> with the currently active surface
/// </summary>
/// <param name="profileDescriptor">The descriptor describing the profile to activate</param>
Profile ActivateProfile(ProfileDescriptor profileDescriptor);
/// <summary>
/// Asynchronously activates the profile described in the given <see cref="ProfileDescriptor" /> with the currently
/// active surface using a fade animation
/// </summary>
/// <param name="profileDescriptor">The descriptor describing the profile to activate</param>
Task<Profile> ActivateProfileAnimated(ProfileDescriptor profileDescriptor);
/// <summary>
/// Clears the active profile on the given <see cref="ProfileModule" />
/// </summary>
/// <param name="module">The profile module to deactivate the active profile on</param>
void ClearActiveProfile(ProfileModule module);
/// <summary>
/// Asynchronously clears the active profile on the given <see cref="ProfileModule" /> using a fade animation
/// </summary>
/// <param name="module">The profile module to deactivate the active profile on</param>
Task ClearActiveProfileAnimated(ProfileModule module);
/// <summary>
/// Attempts to restore the profile to the state it had before the last <see cref="UpdateProfile" /> call.
/// </summary> /// </summary>
/// <param name="profile"></param> /// <param name="profile"></param>
bool UndoUpdateProfile(Profile profile); bool UndoSaveProfile(Profile profile);
/// <summary> /// <summary>
/// Attempts to restore the profile to the state it had before the last <see cref="UndoUpdateProfile" /> call. /// Attempts to restore the profile to the state it had before the last <see cref="UndoSaveProfile" /> call.
/// </summary> /// </summary>
/// <param name="profile"></param> /// <param name="profile"></param>
bool RedoUpdateProfile(Profile profile); bool RedoSaveProfile(Profile profile);
/// <summary> /// <summary>
/// Prepares the profile for rendering. You should not need to call this, it is exposed for some niche usage in the /// Exports the profile described in the given <see cref="ProfileConfiguration" /> into an export model
/// core
/// </summary> /// </summary>
/// <param name="profile"></param> /// <param name="profileConfiguration">The profile configuration of the profile to export</param>
void InstantiateProfile(Profile profile); /// <returns>The resulting export model</returns>
ProfileConfigurationExportModel ExportProfile(ProfileConfiguration profileConfiguration);
/// <summary> /// <summary>
/// [Placeholder] Exports the profile described in the given <see cref="ProfileDescriptor" /> in a JSON format /// Imports the provided base64 encoded GZIPed JSON as a profile configuration
/// </summary> /// </summary>
/// <param name="profileDescriptor">The descriptor of the profile to export</param> /// <param name="category">The <see cref="ProfileCategory" /> in which to import the profile</param>
/// <returns>The resulting JSON</returns> /// <param name="exportModel">The model containing the profile to import</param>
string ExportProfile(ProfileDescriptor profileDescriptor); /// <param name="makeUnique">Whether or not to give the profile a new GUID, making it unique</param>
/// <param name="markAsFreshImport">Whether or not to mark the profile as a fresh import, causing it to be adapted until any changes are made to it</param>
/// <summary>
/// [Placeholder] Imports the provided base64 encoded GZIPed JSON as a profile for the given
/// <see cref="ProfileModule" />
/// </summary>
/// <param name="json">The content of the profile as JSON</param>
/// <param name="profileModule">The module to import the profile in to</param>
/// <param name="nameAffix">Text to add after the name of the profile (separated by a dash)</param> /// <param name="nameAffix">Text to add after the name of the profile (separated by a dash)</param>
/// <returns></returns> /// <returns>The resulting profile configuration</returns>
ProfileDescriptor ImportProfile(string json, ProfileModule profileModule, string nameAffix = "imported"); ProfileConfiguration ImportProfile(ProfileCategory category, ProfileConfigurationExportModel exportModel, bool makeUnique = true, bool markAsFreshImport = true, string? nameAffix = "imported");
/// <summary> /// <summary>
/// Adapts a given profile to the currently active devices /// Adapts a given profile to the currently active devices
/// </summary> /// </summary>
/// <param name="profile">The profile to adapt</param> /// <param name="profile">The profile to adapt</param>
void AdaptProfile(Profile profile); void AdaptProfile(Profile profile);
/// <summary>
/// Updates all currently active profiles
/// </summary>
void UpdateProfiles(double deltaTime);
/// <summary>
/// Renders all currently active profiles
/// </summary>
/// <param name="canvas"></param>
void RenderProfiles(SKCanvas canvas);
} }
} }

View File

@ -1,12 +1,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Artemis.Core.Modules; using Artemis.Core.Modules;
using Artemis.Storage.Entities.Profile; using Artemis.Storage.Entities.Profile;
using Artemis.Storage.Repositories.Interfaces; using Artemis.Storage.Repositories.Interfaces;
using Newtonsoft.Json; using Newtonsoft.Json;
using Serilog; using Serilog;
using SkiaSharp;
namespace Artemis.Core.Services namespace Artemis.Core.Services
{ {
@ -14,48 +15,38 @@ namespace Artemis.Core.Services
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IPluginManagementService _pluginManagementService; private readonly IPluginManagementService _pluginManagementService;
private readonly IRgbService _rgbService; private readonly List<ProfileCategory> _profileCategories;
private readonly IProfileCategoryRepository _profileCategoryRepository;
private readonly IProfileRepository _profileRepository; private readonly IProfileRepository _profileRepository;
private readonly IRgbService _rgbService;
private readonly List<Exception> _updateExceptions = new();
private DateTime _lastUpdateExceptionLog;
private readonly List<Exception> _renderExceptions = new();
private DateTime _lastRenderExceptionLog;
public ProfileService(ILogger logger, public ProfileService(ILogger logger,
IPluginManagementService pluginManagementService,
IRgbService rgbService, IRgbService rgbService,
// TODO: Move these two
IConditionOperatorService conditionOperatorService, IConditionOperatorService conditionOperatorService,
IDataBindingService dataBindingService, IDataBindingService dataBindingService,
IProfileCategoryRepository profileCategoryRepository,
IPluginManagementService pluginManagementService,
IProfileRepository profileRepository) IProfileRepository profileRepository)
{ {
_logger = logger; _logger = logger;
_pluginManagementService = pluginManagementService;
_rgbService = rgbService; _rgbService = rgbService;
_profileCategoryRepository = profileCategoryRepository;
_pluginManagementService = pluginManagementService;
_profileRepository = profileRepository; _profileRepository = profileRepository;
_profileCategories = new List<ProfileCategory>(_profileCategoryRepository.GetAll().Select(c => new ProfileCategory(c)).OrderBy(c => c.Order));
_rgbService.LedsChanged += RgbServiceOnLedsChanged; _rgbService.LedsChanged += RgbServiceOnLedsChanged;
} _pluginManagementService.PluginFeatureEnabled += PluginManagementServiceOnPluginFeatureToggled;
_pluginManagementService.PluginFeatureDisabled += PluginManagementServiceOnPluginFeatureToggled;
public static JsonSerializerSettings MementoSettings { get; set; } = new() {TypeNameHandling = TypeNameHandling.All}; if (!_profileCategories.Any())
public static JsonSerializerSettings ExportSettings { get; set; } = new() {TypeNameHandling = TypeNameHandling.All, Formatting = Formatting.Indented}; CreateDefaultProfileCategories();
public ProfileDescriptor? GetLastActiveProfile(ProfileModule module)
{
List<ProfileEntity> moduleProfiles = _profileRepository.GetByModuleId(module.Id);
if (!moduleProfiles.Any())
return CreateProfileDescriptor(module, "Default");
ProfileEntity? profileEntity = moduleProfiles.FirstOrDefault(p => p.IsActive) ?? moduleProfiles.FirstOrDefault();
return profileEntity == null ? null : new ProfileDescriptor(module, profileEntity);
}
private void SaveActiveProfile(ProfileModule module)
{
if (module.ActiveProfile == null)
return;
List<ProfileEntity> profileEntities = _profileRepository.GetByModuleId(module.Id);
foreach (ProfileEntity profileEntity in profileEntities)
{
profileEntity.IsActive = module.ActiveProfile.EntityId == profileEntity.Id;
_profileRepository.Save(profileEntity);
}
} }
/// <summary> /// <summary>
@ -63,168 +54,324 @@ namespace Artemis.Core.Services
/// </summary> /// </summary>
private void ActiveProfilesPopulateLeds() private void ActiveProfilesPopulateLeds()
{ {
List<ProfileModule> profileModules = _pluginManagementService.GetFeaturesOfType<ProfileModule>(); foreach (ProfileConfiguration profileConfiguration in ProfileConfigurations)
foreach (ProfileModule profileModule in profileModules)
{ {
// Avoid race condition, make the check here if (profileConfiguration.Profile == null) continue;
if (profileModule.ActiveProfile == null) profileConfiguration.Profile.PopulateLeds(_rgbService.EnabledDevices);
if (!profileConfiguration.Profile.IsFreshImport) continue;
_logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profileConfiguration.Profile);
AdaptProfile(profileConfiguration.Profile);
}
}
private void UpdateModules()
{
lock (_profileRepository)
{
List<Module> modules = _pluginManagementService.GetFeaturesOfType<Module>();
foreach (ProfileCategory profileCategory in _profileCategories)
foreach (ProfileConfiguration profileConfiguration in profileCategory.ProfileConfigurations)
profileConfiguration.LoadModules(modules);
}
}
private void RgbServiceOnLedsChanged(object? sender, EventArgs e)
{
ActiveProfilesPopulateLeds();
}
private void PluginManagementServiceOnPluginFeatureToggled(object? sender, PluginFeatureEventArgs e)
{
if (e.PluginFeature is Module)
UpdateModules();
}
public bool RenderForEditor { get; set; }
public void UpdateProfiles(double deltaTime)
{
lock (_profileCategories)
{
// Iterate the children in reverse because the first category must be rendered last to end up on top
for (int i = _profileCategories.Count - 1; i > -1; i--)
{
ProfileCategory profileCategory = _profileCategories[i];
for (int j = profileCategory.ProfileConfigurations.Count - 1; j > -1; j--)
{
ProfileConfiguration profileConfiguration = profileCategory.ProfileConfigurations[j];
// Profiles being edited are updated at their own leisure
if (profileConfiguration.IsBeingEdited)
continue; continue;
profileModule.ActiveProfile.PopulateLeds(_rgbService.EnabledDevices); bool shouldBeActive = profileConfiguration.ShouldBeActive(false);
if (profileModule.ActiveProfile.IsFreshImport) if (shouldBeActive)
{ {
_logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profileModule.ActiveProfile); profileConfiguration.Update();
AdaptProfile(profileModule.ActiveProfile); shouldBeActive = profileConfiguration.ActivationConditionMet;
}
}
} }
public List<ProfileDescriptor> GetProfileDescriptors(ProfileModule module) try
{ {
List<ProfileEntity> profileEntities = _profileRepository.GetByModuleId(module.Id); // Make sure the profile is active or inactive according to the parameters above
return profileEntities.Select(e => new ProfileDescriptor(module, e)).ToList(); if (shouldBeActive && profileConfiguration.Profile == null)
ActivateProfile(profileConfiguration);
else if (!shouldBeActive && profileConfiguration.Profile != null)
DeactivateProfile(profileConfiguration);
profileConfiguration.Profile?.Update(deltaTime);
}
catch (Exception e)
{
_updateExceptions.Add(e);
}
}
} }
public ProfileDescriptor CreateProfileDescriptor(ProfileModule module, string name) LogProfileUpdateExceptions();
{ }
ProfileEntity profileEntity = new() {Id = Guid.NewGuid(), Name = name, ModuleId = module.Id};
_profileRepository.Add(profileEntity);
return new ProfileDescriptor(module, profileEntity);
} }
public void ActivateLastProfile(ProfileModule profileModule) public void RenderProfiles(SKCanvas canvas)
{ {
ProfileDescriptor? activeProfile = GetLastActiveProfile(profileModule); lock (_profileCategories)
if (activeProfile != null) {
ActivateProfile(activeProfile); // Iterate the children in reverse because the first category must be rendered last to end up on top
for (int i = _profileCategories.Count - 1; i > -1; i--)
{
ProfileCategory profileCategory = _profileCategories[i];
for (int j = profileCategory.ProfileConfigurations.Count - 1; j > -1; j--)
{
try
{
ProfileConfiguration profileConfiguration = profileCategory.ProfileConfigurations[j];
if (RenderForEditor)
{
if (profileConfiguration.IsBeingEdited)
profileConfiguration.Profile?.Render(canvas, SKPointI.Empty);
} }
public async Task ActivateLastProfileAnimated(ProfileModule profileModule)
{
ProfileDescriptor? activeProfile = GetLastActiveProfile(profileModule);
if (activeProfile != null)
await ActivateProfileAnimated(activeProfile);
}
public Profile ActivateProfile(ProfileDescriptor profileDescriptor)
{
if (profileDescriptor.ProfileModule.ActiveProfile?.EntityId == profileDescriptor.Id)
return profileDescriptor.ProfileModule.ActiveProfile;
ProfileEntity profileEntity = _profileRepository.Get(profileDescriptor.Id);
if (profileEntity == null)
throw new ArtemisCoreException($"Cannot find profile named: {profileDescriptor.Name} ID: {profileDescriptor.Id}");
Profile profile = new(profileDescriptor.ProfileModule, profileEntity);
InstantiateProfile(profile);
profileDescriptor.ProfileModule.ChangeActiveProfile(profile, _rgbService.EnabledDevices);
if (profile.IsFreshImport)
{
_logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profile);
AdaptProfile(profile);
}
SaveActiveProfile(profileDescriptor.ProfileModule);
return profile;
}
public void ReloadProfile(ProfileModule module)
{
if (module.ActiveProfile == null)
return;
ProfileEntity entity = _profileRepository.Get(module.ActiveProfile.EntityId);
Profile profile = new(module, entity);
InstantiateProfile(profile);
module.ChangeActiveProfile(null, _rgbService.EnabledDevices);
module.ChangeActiveProfile(profile, _rgbService.EnabledDevices);
}
public async Task<Profile> ActivateProfileAnimated(ProfileDescriptor profileDescriptor)
{
if (profileDescriptor.ProfileModule.ActiveProfile?.EntityId == profileDescriptor.Id)
return profileDescriptor.ProfileModule.ActiveProfile;
ProfileEntity profileEntity = _profileRepository.Get(profileDescriptor.Id);
if (profileEntity == null)
throw new ArtemisCoreException($"Cannot find profile named: {profileDescriptor.Name} ID: {profileDescriptor.Id}");
Profile profile = new(profileDescriptor.ProfileModule, profileEntity);
InstantiateProfile(profile);
void ActivatingRgbServiceOnLedsChanged(object? sender, EventArgs e)
{
profile.PopulateLeds(_rgbService.EnabledDevices);
}
void ActivatingProfilePluginToggle(object? sender, PluginEventArgs e)
{
if (!profile.Disposed)
InstantiateProfile(profile);
}
// This could happen during activation so subscribe to it
_pluginManagementService.PluginEnabled += ActivatingProfilePluginToggle;
_pluginManagementService.PluginDisabled += ActivatingProfilePluginToggle;
_rgbService.LedsChanged += ActivatingRgbServiceOnLedsChanged;
await profileDescriptor.ProfileModule.ChangeActiveProfileAnimated(profile, _rgbService.EnabledDevices);
if (profile.IsFreshImport)
{
_logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profile);
AdaptProfile(profile);
}
SaveActiveProfile(profileDescriptor.ProfileModule);
_pluginManagementService.PluginEnabled -= ActivatingProfilePluginToggle;
_pluginManagementService.PluginDisabled -= ActivatingProfilePluginToggle;
_rgbService.LedsChanged -= ActivatingRgbServiceOnLedsChanged;
return profile;
}
public void ClearActiveProfile(ProfileModule module)
{
module.ChangeActiveProfile(null, _rgbService.EnabledDevices);
SaveActiveProfile(module);
}
public async Task ClearActiveProfileAnimated(ProfileModule module)
{
await module.ChangeActiveProfileAnimated(null, _rgbService.EnabledDevices);
}
public void DeleteProfile(Profile profile)
{
_logger.Debug("Removing profile " + profile);
// If the given profile is currently active, disable it first (this also disposes it)
if (profile.Module.ActiveProfile == profile)
ClearActiveProfile(profile.Module);
else else
profile.Dispose(); {
// Ensure all criteria are met before rendering
_profileRepository.Remove(profile.ProfileEntity); if (!profileConfiguration.IsSuspended && !profileConfiguration.IsMissingModule && profileConfiguration.ActivationConditionMet)
profileConfiguration.Profile?.Render(canvas, SKPointI.Empty);
}
}
catch (Exception e)
{
_renderExceptions.Add(e);
}
}
} }
public void DeleteProfile(ProfileDescriptor profileDescriptor) LogProfileRenderExceptions();
}
}
private void CreateDefaultProfileCategories()
{ {
ProfileEntity profileEntity = _profileRepository.Get(profileDescriptor.Id); foreach (DefaultCategoryName defaultCategoryName in Enum.GetValues<DefaultCategoryName>())
CreateProfileCategory(defaultCategoryName.ToString());
}
private void LogProfileUpdateExceptions()
{
// Only log update exceptions every 10 seconds to avoid spamming the logs
if (DateTime.Now - _lastUpdateExceptionLog < TimeSpan.FromSeconds(10))
return;
_lastUpdateExceptionLog = DateTime.Now;
if (!_updateExceptions.Any())
return;
// Group by stack trace, that should gather up duplicate exceptions
foreach (IGrouping<string?, Exception> exceptions in _updateExceptions.GroupBy(e => e.StackTrace))
_logger.Warning(exceptions.First(), "Exception was thrown {count} times during profile update in the last 10 seconds", exceptions.Count());
// When logging is finished start with a fresh slate
_updateExceptions.Clear();
}
private void LogProfileRenderExceptions()
{
// Only log update exceptions every 10 seconds to avoid spamming the logs
if (DateTime.Now - _lastRenderExceptionLog < TimeSpan.FromSeconds(10))
return;
_lastRenderExceptionLog = DateTime.Now;
if (!_renderExceptions.Any())
return;
// Group by stack trace, that should gather up duplicate exceptions
foreach (IGrouping<string?, Exception> exceptions in _renderExceptions.GroupBy(e => e.StackTrace))
_logger.Warning(exceptions.First(), "Exception was thrown {count} times during profile render in the last 10 seconds", exceptions.Count());
// When logging is finished start with a fresh slate
_renderExceptions.Clear();
}
public ReadOnlyCollection<ProfileCategory> ProfileCategories
{
get
{
lock (_profileRepository)
{
return _profileCategories.AsReadOnly();
}
}
}
public ReadOnlyCollection<ProfileConfiguration> ProfileConfigurations
{
get
{
lock (_profileRepository)
{
return _profileCategories.SelectMany(c => c.ProfileConfigurations).ToList().AsReadOnly();
}
}
}
public void LoadProfileConfigurationIcon(ProfileConfiguration profileConfiguration)
{
if (profileConfiguration.Icon.IconType == ProfileConfigurationIconType.MaterialIcon)
return;
if (profileConfiguration.Icon.FileIcon != null)
return;
profileConfiguration.Icon.FileIcon = _profileCategoryRepository.GetProfileIconStream(profileConfiguration.Entity.FileIconId);
}
public void SaveProfileConfigurationIcon(ProfileConfiguration profileConfiguration)
{
if (profileConfiguration.Icon.IconType == ProfileConfigurationIconType.MaterialIcon)
return;
if (profileConfiguration.Icon.FileIcon != null)
{
profileConfiguration.Icon.FileIcon.Position = 0;
_profileCategoryRepository.SaveProfileIconStream(profileConfiguration.Entity, profileConfiguration.Icon.FileIcon);
}
}
public Profile ActivateProfile(ProfileConfiguration profileConfiguration)
{
if (profileConfiguration.Profile != null)
return profileConfiguration.Profile;
ProfileEntity profileEntity = _profileRepository.Get(profileConfiguration.Entity.ProfileId);
if (profileEntity == null)
throw new ArtemisCoreException($"Cannot find profile named: {profileConfiguration.Name} ID: {profileConfiguration.Entity.ProfileId}");
Profile profile = new(profileConfiguration, profileEntity);
profile.PopulateLeds(_rgbService.EnabledDevices);
if (profile.IsFreshImport)
{
_logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profile);
AdaptProfile(profile);
}
profileConfiguration.Profile = profile;
return profile;
}
public void DeactivateProfile(ProfileConfiguration profileConfiguration)
{
if (profileConfiguration.IsBeingEdited)
throw new ArtemisCoreException("Cannot disable a profile that is being edited, that's rude");
if (profileConfiguration.Profile == null)
return;
Profile profile = profileConfiguration.Profile;
profileConfiguration.Profile = null;
profile.Dispose();
}
public void DeleteProfile(ProfileConfiguration profileConfiguration)
{
DeactivateProfile(profileConfiguration);
ProfileEntity profileEntity = _profileRepository.Get(profileConfiguration.Entity.ProfileId);
if (profileEntity == null) if (profileEntity == null)
return; return;
profileConfiguration.Category.RemoveProfileConfiguration(profileConfiguration);
_profileRepository.Remove(profileEntity);
SaveProfileCategory(profileConfiguration.Category);
}
public ProfileCategory CreateProfileCategory(string name)
{
lock (_profileRepository)
{
ProfileCategory profileCategory = new(name);
_profileCategories.Add(profileCategory);
SaveProfileCategory(profileCategory);
return profileCategory;
}
}
public void DeleteProfileCategory(ProfileCategory profileCategory)
{
List<ProfileConfiguration> profileConfigurations = profileCategory.ProfileConfigurations.ToList();
foreach (ProfileConfiguration profileConfiguration in profileConfigurations)
RemoveProfileConfiguration(profileConfiguration);
lock (_profileRepository)
{
_profileCategories.Remove(profileCategory);
_profileCategoryRepository.Remove(profileCategory.Entity);
}
}
/// <summary>
/// Creates a new profile configuration and adds it to the provided <see cref="ProfileCategory" />
/// </summary>
/// <param name="category">The profile category to add the profile to</param>
/// <param name="name">The name of the new profile configuration</param>
/// <param name="icon">The icon of the new profile configuration</param>
/// <returns>The newly created profile configuration</returns>
public ProfileConfiguration CreateProfileConfiguration(ProfileCategory category, string name, string icon)
{
ProfileConfiguration configuration = new(category, name, icon);
ProfileEntity entity = new();
_profileRepository.Add(entity);
configuration.Entity.ProfileId = entity.Id;
category.AddProfileConfiguration(configuration, 0);
return configuration;
}
/// <summary>
/// Removes the provided profile configuration from the <see cref="ProfileCategory" />
/// </summary>
/// <param name="profileConfiguration"></param>
public void RemoveProfileConfiguration(ProfileConfiguration profileConfiguration)
{
profileConfiguration.Category.RemoveProfileConfiguration(profileConfiguration);
DeactivateProfile(profileConfiguration);
SaveProfileCategory(profileConfiguration.Category);
ProfileEntity profileEntity = _profileRepository.Get(profileConfiguration.Entity.ProfileId);
if (profileEntity != null)
_profileRepository.Remove(profileEntity); _profileRepository.Remove(profileEntity);
} }
public void UpdateProfile(Profile profile, bool includeChildren) public void SaveProfileCategory(ProfileCategory profileCategory)
{ {
string memento = JsonConvert.SerializeObject(profile.ProfileEntity, MementoSettings); profileCategory.Save();
_profileCategoryRepository.Save(profileCategory.Entity);
lock (_profileCategories)
{
_profileCategories.Sort((a, b) => a.Order - b.Order);
}
}
public void SaveProfile(Profile profile, bool includeChildren)
{
string memento = JsonConvert.SerializeObject(profile.ProfileEntity, IProfileService.MementoSettings);
profile.Save(); profile.Save();
if (includeChildren) if (includeChildren)
{ {
@ -235,7 +382,7 @@ namespace Artemis.Core.Services
} }
// If there are no changes, don't bother saving // If there are no changes, don't bother saving
string updatedMemento = JsonConvert.SerializeObject(profile.ProfileEntity, MementoSettings); string updatedMemento = JsonConvert.SerializeObject(profile.ProfileEntity, IProfileService.MementoSettings);
if (memento.Equals(updatedMemento)) if (memento.Equals(updatedMemento))
{ {
_logger.Debug("Updating profile - Skipping save, no changes"); _logger.Debug("Updating profile - Skipping save, no changes");
@ -253,7 +400,7 @@ namespace Artemis.Core.Services
_profileRepository.Save(profile.ProfileEntity); _profileRepository.Save(profile.ProfileEntity);
} }
public bool UndoUpdateProfile(Profile profile) public bool UndoSaveProfile(Profile profile)
{ {
// Keep the profile from being rendered by locking it // Keep the profile from being rendered by locking it
lock (profile) lock (profile)
@ -265,20 +412,20 @@ namespace Artemis.Core.Services
} }
string top = profile.UndoStack.Pop(); string top = profile.UndoStack.Pop();
string memento = JsonConvert.SerializeObject(profile.ProfileEntity, MementoSettings); string memento = JsonConvert.SerializeObject(profile.ProfileEntity, IProfileService.MementoSettings);
profile.RedoStack.Push(memento); profile.RedoStack.Push(memento);
profile.ProfileEntity = JsonConvert.DeserializeObject<ProfileEntity>(top, MementoSettings) profile.ProfileEntity = JsonConvert.DeserializeObject<ProfileEntity>(top, IProfileService.MementoSettings)
?? throw new InvalidOperationException("Failed to deserialize memento"); ?? throw new InvalidOperationException("Failed to deserialize memento");
profile.Load(); profile.Load();
InstantiateProfile(profile); profile.PopulateLeds(_rgbService.EnabledDevices);
} }
_logger.Debug("Undo profile update - Success"); _logger.Debug("Undo profile update - Success");
return true; return true;
} }
public bool RedoUpdateProfile(Profile profile) public bool RedoSaveProfile(Profile profile)
{ {
// Keep the profile from being rendered by locking it // Keep the profile from being rendered by locking it
lock (profile) lock (profile)
@ -290,53 +437,81 @@ namespace Artemis.Core.Services
} }
string top = profile.RedoStack.Pop(); string top = profile.RedoStack.Pop();
string memento = JsonConvert.SerializeObject(profile.ProfileEntity, MementoSettings); string memento = JsonConvert.SerializeObject(profile.ProfileEntity, IProfileService.MementoSettings);
profile.UndoStack.Push(memento); profile.UndoStack.Push(memento);
profile.ProfileEntity = JsonConvert.DeserializeObject<ProfileEntity>(top, MementoSettings) profile.ProfileEntity = JsonConvert.DeserializeObject<ProfileEntity>(top, IProfileService.MementoSettings)
?? throw new InvalidOperationException("Failed to deserialize memento"); ?? throw new InvalidOperationException("Failed to deserialize memento");
profile.Load(); profile.Load();
InstantiateProfile(profile); profile.PopulateLeds(_rgbService.EnabledDevices);
_logger.Debug("Redo profile update - Success"); _logger.Debug("Redo profile update - Success");
return true; return true;
} }
} }
public void InstantiateProfile(Profile profile) public ProfileConfigurationExportModel ExportProfile(ProfileConfiguration profileConfiguration)
{ {
profile.PopulateLeds(_rgbService.EnabledDevices); // The profile may not be active and in that case lets activate it real quick
Profile profile = profileConfiguration.Profile ?? ActivateProfile(profileConfiguration);
return new ProfileConfigurationExportModel
{
ProfileConfigurationEntity = profileConfiguration.Entity,
ProfileEntity = profile.ProfileEntity,
ProfileImage = profileConfiguration.Icon.FileIcon
};
} }
public string ExportProfile(ProfileDescriptor profileDescriptor) public ProfileConfiguration ImportProfile(ProfileCategory category, ProfileConfigurationExportModel exportModel, bool makeUnique, bool markAsFreshImport, string? nameAffix)
{ {
ProfileEntity profileEntity = _profileRepository.Get(profileDescriptor.Id); if (exportModel.ProfileEntity == null)
if (profileEntity == null) throw new ArtemisCoreException("Cannot import a profile without any data");
throw new ArtemisCoreException($"Cannot find profile named: {profileDescriptor.Name} ID: {profileDescriptor.Id}");
return JsonConvert.SerializeObject(profileEntity, ExportSettings); // Create a copy of the entity because we'll be using it from now on
} ProfileEntity profileEntity = JsonConvert.DeserializeObject<ProfileEntity>(
JsonConvert.SerializeObject(exportModel.ProfileEntity, IProfileService.ExportSettings), IProfileService.ExportSettings
public ProfileDescriptor ImportProfile(string json, ProfileModule profileModule, string nameAffix) )!;
{
ProfileEntity? profileEntity = JsonConvert.DeserializeObject<ProfileEntity>(json, ExportSettings);
if (profileEntity == null)
throw new ArtemisCoreException("Failed to import profile but JSON.NET threw no error :(");
// Assign a new GUID to make sure it is unique in case of a previous import of the same content // 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()); profileEntity.UpdateGuid(Guid.NewGuid());
if (nameAffix != null)
profileEntity.Name = $"{profileEntity.Name} - {nameAffix}"; profileEntity.Name = $"{profileEntity.Name} - {nameAffix}";
if (markAsFreshImport)
profileEntity.IsFreshImport = true; profileEntity.IsFreshImport = true;
profileEntity.IsActive = false;
_profileRepository.Add(profileEntity); _profileRepository.Add(profileEntity);
return new ProfileDescriptor(profileModule, profileEntity);
ProfileConfiguration profileConfiguration;
if (exportModel.ProfileConfigurationEntity != null)
{
// A new GUID will be given on save
exportModel.ProfileConfigurationEntity.FileIconId = Guid.Empty;
profileConfiguration = new ProfileConfiguration(category, exportModel.ProfileConfigurationEntity);
if (nameAffix != null)
profileConfiguration.Name = $"{profileConfiguration.Name} - {nameAffix}";
}
else
{
profileConfiguration = new ProfileConfiguration(category, exportModel.ProfileEntity!.Name, "Import");
}
if (exportModel.ProfileImage != null)
profileConfiguration.Icon.FileIcon = exportModel.ProfileImage;
profileConfiguration.Entity.ProfileId = profileEntity.Id;
category.AddProfileConfiguration(profileConfiguration, 0);
SaveProfileCategory(category);
return profileConfiguration;
} }
/// <inheritdoc /> /// <inheritdoc />
public void AdaptProfile(Profile profile) public void AdaptProfile(Profile profile)
{ {
string memento = JsonConvert.SerializeObject(profile.ProfileEntity, MementoSettings); string memento = JsonConvert.SerializeObject(profile.ProfileEntity, IProfileService.MementoSettings);
List<ArtemisDevice> devices = _rgbService.EnabledDevices.ToList(); List<ArtemisDevice> devices = _rgbService.EnabledDevices.ToList();
foreach (Layer layer in profile.GetAllLayers()) foreach (Layer layer in profile.GetAllLayers())
@ -355,14 +530,5 @@ namespace Artemis.Core.Services
_profileRepository.Save(profile.ProfileEntity); _profileRepository.Save(profile.ProfileEntity);
} }
#region Event handlers
private void RgbServiceOnLedsChanged(object? sender, EventArgs e)
{
ActiveProfilesPopulateLeds();
}
#endregion
} }
} }

View File

@ -15,17 +15,7 @@ namespace Artemis.Core.Services
/// </summary> /// </summary>
public class DataModelJsonPluginEndPoint<T> : PluginEndPoint where T : DataModel public class DataModelJsonPluginEndPoint<T> : PluginEndPoint where T : DataModel
{ {
private readonly ProfileModule<T>? _profileModule; private readonly Module<T> _module;
private readonly Module<T>? _module;
private readonly DataModelExpansion<T>? _dataModelExpansion;
internal DataModelJsonPluginEndPoint(ProfileModule<T> profileModule, string name, PluginsModule pluginsModule) : base(profileModule, name, pluginsModule)
{
_profileModule = profileModule ?? throw new ArgumentNullException(nameof(profileModule));
ThrowOnFail = true;
Accepts = MimeType.Json;
}
internal DataModelJsonPluginEndPoint(Module<T> module, string name, PluginsModule pluginsModule) : base(module, name, pluginsModule) internal DataModelJsonPluginEndPoint(Module<T> module, string name, PluginsModule pluginsModule) : base(module, name, pluginsModule)
{ {
@ -35,14 +25,6 @@ namespace Artemis.Core.Services
Accepts = MimeType.Json; Accepts = MimeType.Json;
} }
internal DataModelJsonPluginEndPoint(DataModelExpansion<T> dataModelExpansion, string name, PluginsModule pluginsModule) : base(dataModelExpansion, name, pluginsModule)
{
_dataModelExpansion = dataModelExpansion ?? throw new ArgumentNullException(nameof(dataModelExpansion));
ThrowOnFail = true;
Accepts = MimeType.Json;
}
/// <summary> /// <summary>
/// Whether or not the end point should throw an exception if deserializing the received JSON fails. /// Whether or not the end point should throw an exception if deserializing the received JSON fails.
/// If set to <see langword="false" /> malformed JSON is silently ignored; if set to <see langword="true" /> malformed /// If set to <see langword="false" /> malformed JSON is silently ignored; if set to <see langword="true" /> malformed
@ -63,12 +45,7 @@ namespace Artemis.Core.Services
using TextReader reader = context.OpenRequestText(); using TextReader reader = context.OpenRequestText();
try try
{ {
if (_profileModule != null)
JsonConvert.PopulateObject(await reader.ReadToEndAsync(), _profileModule.DataModel);
else if (_module != null)
JsonConvert.PopulateObject(await reader.ReadToEndAsync(), _module.DataModel); JsonConvert.PopulateObject(await reader.ReadToEndAsync(), _module.DataModel);
else
JsonConvert.PopulateObject(await reader.ReadToEndAsync(), _dataModelExpansion!.DataModel);
} }
catch (JsonException) catch (JsonException)
{ {

View File

@ -54,26 +54,6 @@ namespace Artemis.Core.Services
/// <returns>The resulting end point</returns> /// <returns>The resulting end point</returns>
DataModelJsonPluginEndPoint<T> AddDataModelJsonEndPoint<T>(Module<T> module, string endPointName) where T : DataModel; DataModelJsonPluginEndPoint<T> AddDataModelJsonEndPoint<T>(Module<T> module, string endPointName) where T : DataModel;
/// <summary>
/// Adds a new endpoint that directly maps received JSON to the data model of the provided
/// <paramref name="profileModule" />.
/// </summary>
/// <typeparam name="T">The data model type of the module</typeparam>
/// <param name="profileModule">The module whose datamodel to apply the received JSON to</param>
/// <param name="endPointName">The name of the end point, must be unique</param>
/// <returns>The resulting end point</returns>
DataModelJsonPluginEndPoint<T> AddDataModelJsonEndPoint<T>(ProfileModule<T> profileModule, string endPointName) where T : DataModel;
/// <summary>
/// Adds a new endpoint that directly maps received JSON to the data model of the provided
/// <paramref name="dataModelExpansion" />.
/// </summary>
/// <typeparam name="T">The data model type of the module</typeparam>
/// <param name="dataModelExpansion">The data model expansion whose datamodel to apply the received JSON to</param>
/// <param name="endPointName">The name of the end point, must be unique</param>
/// <returns>The resulting end point</returns>
DataModelJsonPluginEndPoint<T> AddDataModelJsonEndPoint<T>(DataModelExpansion<T> dataModelExpansion, string endPointName) where T : DataModel;
/// <summary> /// <summary>
/// Adds a new endpoint for the given plugin feature receiving an a <see cref="string" />. /// Adds a new endpoint for the given plugin feature receiving an a <see cref="string" />.
/// </summary> /// </summary>

View File

@ -142,24 +142,6 @@ namespace Artemis.Core.Services
return endPoint; return endPoint;
} }
public DataModelJsonPluginEndPoint<T> AddDataModelJsonEndPoint<T>(ProfileModule<T> profileModule, string endPointName) where T : DataModel
{
if (profileModule == null) throw new ArgumentNullException(nameof(profileModule));
if (endPointName == null) throw new ArgumentNullException(nameof(endPointName));
DataModelJsonPluginEndPoint<T> endPoint = new(profileModule, endPointName, PluginsModule);
PluginsModule.AddPluginEndPoint(endPoint);
return endPoint;
}
public DataModelJsonPluginEndPoint<T> AddDataModelJsonEndPoint<T>(DataModelExpansion<T> dataModelExpansion, string endPointName) where T : DataModel
{
if (dataModelExpansion == null) throw new ArgumentNullException(nameof(dataModelExpansion));
if (endPointName == null) throw new ArgumentNullException(nameof(endPointName));
DataModelJsonPluginEndPoint<T> endPoint = new(dataModelExpansion, endPointName, PluginsModule);
PluginsModule.AddPluginEndPoint(endPoint);
return endPoint;
}
private void HandleDataModelRequest<T>(Module<T> module, T value) where T : DataModel private void HandleDataModelRequest<T>(Module<T> module, T value) where T : DataModel
{ {
} }

View File

@ -17,7 +17,7 @@ namespace Artemis.Core
if (Registrations.Any(r => r.DataModel == dataModel)) if (Registrations.Any(r => r.DataModel == dataModel))
throw new ArtemisCoreException($"Data model store already contains data model '{dataModel.DataModelDescription}'"); throw new ArtemisCoreException($"Data model store already contains data model '{dataModel.DataModelDescription}'");
registration = new DataModelRegistration(dataModel, dataModel.Feature) {IsInStore = true}; registration = new DataModelRegistration(dataModel, dataModel.Module) {IsInStore = true};
Registrations.Add(registration); Registrations.Add(registration);
} }

View File

@ -1,11 +1,12 @@
using Artemis.Core.LayerEffects; using Artemis.Core.LayerEffects;
using Artemis.Core.Modules;
namespace Artemis.Core namespace Artemis.Core
{ {
/// <summary> /// <summary>
/// An empty data model plugin feature used by <see cref="Constants.CorePlugin" /> /// An empty data model plugin feature used by <see cref="Constants.CorePlugin" />
/// </summary> /// </summary>
internal class CorePluginFeature : DataModelPluginFeature internal class CorePluginFeature : Module
{ {
public CorePluginFeature() public CorePluginFeature()
{ {
@ -20,6 +21,10 @@ namespace Artemis.Core
public override void Disable() public override void Disable()
{ {
} }
public override void Update(double deltaTime)
{
}
} }
internal class EffectPlaceholderPlugin : LayerEffectProvider internal class EffectPlaceholderPlugin : LayerEffectProvider

View File

@ -1,99 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Artemis.Core.Modules;
using Artemis.Core.Services;
using Artemis.Storage.Entities.Profile;
using Serilog;
using SkiaSharp;
namespace Artemis.Core
{
internal class IntroAnimation
{
private readonly ILogger _logger;
private readonly IProfileService _profileService;
private readonly IEnumerable<ArtemisDevice> _devices;
public IntroAnimation(ILogger logger, IProfileService profileService, IEnumerable<ArtemisDevice> devices)
{
_logger = logger;
_profileService = profileService;
_devices = devices;
AnimationProfile = CreateIntroProfile();
}
public Profile AnimationProfile { get; set; }
public void Render(double deltaTime, SKCanvas canvas)
{
AnimationProfile.Update(deltaTime);
AnimationProfile.Render(canvas, SKPointI.Empty);
}
private Profile CreateIntroProfile()
{
try
{
// Load the intro profile from JSON into a ProfileEntity
string json = File.ReadAllText(Path.Combine(Constants.ApplicationFolder, "Resources", "intro-profile.json"));
ProfileEntity profileEntity = CoreJson.DeserializeObject<ProfileEntity>(json)!;
// Inject every LED on the surface into each layer
foreach (LayerEntity profileEntityLayer in profileEntity.Layers)
profileEntityLayer.Leds.AddRange(_devices.SelectMany(d => d.Leds).Select(l => new LedEntity
{
DeviceIdentifier = l.Device.Identifier,
LedName = l.RgbLed.Id.ToString()
}));
Profile profile = new(new DummyModule(), profileEntity);
profile.Activate(_devices);
_profileService.InstantiateProfile(profile);
return profile;
}
catch (Exception e)
{
_logger.Warning(e, "Failed to load intro profile");
}
return new Profile(new DummyModule(), "Intro");
}
}
internal class DummyModule : ProfileModule
{
public override void Enable()
{
throw new NotImplementedException();
}
public override void Disable()
{
throw new NotImplementedException();
}
public override void Update(double deltaTime)
{
throw new NotImplementedException();
}
public override void Render(double deltaTime, SKCanvas canvas, SKImageInfo canvasInfo)
{
throw new NotImplementedException();
}
public override void ModuleActivated(bool isOverride)
{
throw new NotImplementedException();
}
public override void ModuleDeactivated(bool isOverride)
{
throw new NotImplementedException();
}
}
}

View File

@ -1,17 +0,0 @@
using System;
namespace Artemis.Storage.Entities.Module
{
public class ModuleSettingsEntity
{
public ModuleSettingsEntity()
{
Id = Guid.NewGuid();
}
public Guid Id { get; set; }
public string ModuleId { get; set; }
public int PriorityCategory { get; set; }
public int Priority { get; set; }
}
}

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
namespace Artemis.Storage.Entities.Profile
{
public class ProfileCategoryEntity
{
public Guid Id { get; set; }
public string Name { get; set; }
public bool IsCollapsed { get; set; }
public bool IsSuspended { get; set; }
public int Order { get; set; }
public List<ProfileConfigurationEntity> ProfileConfigurations { get; set; } = new();
}
}

View File

@ -0,0 +1,22 @@
using System;
using Artemis.Storage.Entities.Profile.Conditions;
namespace Artemis.Storage.Entities.Profile
{
public class ProfileConfigurationEntity
{
public string Name { get; set; }
public string MaterialIcon { get; set; }
public Guid FileIconId { get; set; }
public int IconType { get; set; }
public bool IsSuspended { get; set; }
public int ActivationBehaviour { get; set; }
public DataModelConditionGroupEntity ActivationCondition { get; set; }
public string ModuleId { get; set; }
public Guid ProfileCategoryId { get; set; }
public Guid ProfileId { get; set; }
}
}

View File

@ -13,10 +13,8 @@ namespace Artemis.Storage.Entities.Profile
} }
public Guid Id { get; set; } public Guid Id { get; set; }
public string ModuleId { get; set; }
public string Name { get; set; } public string Name { get; set; }
public bool IsActive { get; set; }
public bool IsFreshImport { get; set; } public bool IsFreshImport { get; set; }
public List<FolderEntity> Folders { get; set; } public List<FolderEntity> Folders { get; set; }

View File

@ -1,5 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using Artemis.Storage.Entities.Module;
using Artemis.Storage.Migrations.Interfaces; using Artemis.Storage.Migrations.Interfaces;
using LiteDB; using LiteDB;
@ -87,18 +86,6 @@ namespace Artemis.Storage.Migrations
// Remove the default brush the user selected, this will make the UI pick a new one // Remove the default brush the user selected, this will make the UI pick a new one
repository.Database.Execute("DELETE PluginSettingEntity WHERE $.Name = \"ProfileEditor.DefaultLayerBrushDescriptor\""); repository.Database.Execute("DELETE PluginSettingEntity WHERE $.Name = \"ProfileEditor.DefaultLayerBrushDescriptor\"");
// Module settings
repository.Database.GetCollection<ModuleSettingsEntity>().DropIndex("PluginGuid");
ILiteCollection<BsonDocument> modules = repository.Database.GetCollection("ModuleSettingsEntity");
foreach (BsonDocument bsonDocument in modules.FindAll())
{
if (ReplaceIfFound(bsonDocument, "PluginGuid", "ModuleId", pluginMap))
modules.Update(bsonDocument);
else if (bsonDocument.ContainsKey("PluginGuid"))
modules.Delete(bsonDocument["_id"]);
}
repository.Database.GetCollection<ModuleSettingsEntity>().EnsureIndex(s => s.ModuleId, true);
// Profiles // Profiles
ILiteCollection<BsonDocument> collection = repository.Database.GetCollection("ProfileEntity"); ILiteCollection<BsonDocument> collection = repository.Database.GetCollection("ProfileEntity");
foreach (BsonDocument bsonDocument in collection.FindAll()) foreach (BsonDocument bsonDocument in collection.FindAll())

View File

@ -0,0 +1,48 @@
using System.Linq;
using Artemis.Storage.Entities.Profile;
using Artemis.Storage.Migrations.Interfaces;
using LiteDB;
namespace Artemis.Storage.Migrations
{
public class M0012ProfileCategories : IStorageMigration
{
public int UserVersion => 12;
public void Apply(LiteRepository repository)
{
ILiteCollection<ProfileCategoryEntity> profileCategories = repository.Database.GetCollection<ProfileCategoryEntity>();
profileCategories.EnsureIndex(s => s.Name, true);
ProfileCategoryEntity? profileCategoryEntity = profileCategories.Find(c => c.Name == "Converted").FirstOrDefault();
if (profileCategoryEntity == null)
{
profileCategoryEntity = new ProfileCategoryEntity {Name = "Imported"};
profileCategories.Insert(profileCategoryEntity);
}
ILiteCollection<BsonDocument> collection = repository.Database.GetCollection("ProfileEntity");
foreach (BsonDocument bsonDocument in collection.FindAll())
{
// Profiles with a ModuleId have not been converted
if (bsonDocument.ContainsKey("ModuleId"))
{
string moduleId = bsonDocument["ModuleId"].AsString;
bsonDocument.Remove("ModuleId");
ProfileConfigurationEntity profileConfiguration = new()
{
Name = bsonDocument["Name"].AsString,
MaterialIcon = "ApplicationImport",
ModuleId = moduleId,
ProfileId = bsonDocument["_id"].AsGuid
};
profileCategoryEntity.ProfileConfigurations.Add(profileConfiguration);
collection.Update(bsonDocument);
}
}
profileCategories.Update(profileCategoryEntity);
}
}
}

View File

@ -1,14 +0,0 @@
using System.Collections.Generic;
using Artemis.Storage.Entities.Module;
namespace Artemis.Storage.Repositories.Interfaces
{
public interface IModuleRepository : IRepository
{
void Add(ModuleSettingsEntity moduleSettingsEntity);
ModuleSettingsEntity GetByModuleId(string moduleId);
List<ModuleSettingsEntity> GetAll();
List<ModuleSettingsEntity> GetByCategory(int category);
void Save(ModuleSettingsEntity moduleSettingsEntity);
}
}

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.IO;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Storage.Repositories.Interfaces
{
public interface IProfileCategoryRepository : IRepository
{
void Add(ProfileCategoryEntity profileCategoryEntity);
void Remove(ProfileCategoryEntity profileCategoryEntity);
List<ProfileCategoryEntity> GetAll();
ProfileCategoryEntity Get(Guid id);
Stream GetProfileIconStream(Guid id);
void SaveProfileIconStream(ProfileConfigurationEntity profileConfigurationEntity, Stream stream);
ProfileCategoryEntity IsUnique(string name, Guid? id);
void Save(ProfileCategoryEntity profileCategoryEntity);
}
}

View File

@ -10,7 +10,6 @@ namespace Artemis.Storage.Repositories.Interfaces
void Remove(ProfileEntity profileEntity); void Remove(ProfileEntity profileEntity);
List<ProfileEntity> GetAll(); List<ProfileEntity> GetAll();
ProfileEntity Get(Guid id); ProfileEntity Get(Guid id);
List<ProfileEntity> GetByModuleId(string moduleId);
void Save(ProfileEntity profileEntity); void Save(ProfileEntity profileEntity);
} }
} }

View File

@ -1,43 +0,0 @@
using System.Collections.Generic;
using Artemis.Storage.Entities.Module;
using Artemis.Storage.Repositories.Interfaces;
using LiteDB;
namespace Artemis.Storage.Repositories
{
internal class ModuleRepository : IModuleRepository
{
private readonly LiteRepository _repository;
public ModuleRepository(LiteRepository repository)
{
_repository = repository;
_repository.Database.GetCollection<ModuleSettingsEntity>().EnsureIndex(s => s.ModuleId, true);
}
public void Add(ModuleSettingsEntity moduleSettingsEntity)
{
_repository.Insert(moduleSettingsEntity);
}
public ModuleSettingsEntity GetByModuleId(string moduleId)
{
return _repository.FirstOrDefault<ModuleSettingsEntity>(s => s.ModuleId == moduleId);
}
public List<ModuleSettingsEntity> GetAll()
{
return _repository.Query<ModuleSettingsEntity>().ToList();
}
public List<ModuleSettingsEntity> GetByCategory(int category)
{
return _repository.Query<ModuleSettingsEntity>().Where(s => s.PriorityCategory == category).ToList();
}
public void Save(ModuleSettingsEntity moduleSettingsEntity)
{
_repository.Upsert(moduleSettingsEntity);
}
}
}

View File

@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.IO;
using Artemis.Storage.Entities.Profile;
using Artemis.Storage.Repositories.Interfaces;
using LiteDB;
namespace Artemis.Storage.Repositories
{
internal class ProfileCategoryRepository : IProfileCategoryRepository
{
private readonly LiteRepository _repository;
private readonly ILiteStorage<Guid> _profileIcons;
public ProfileCategoryRepository(LiteRepository repository)
{
_repository = repository;
_repository.Database.GetCollection<ProfileCategoryEntity>().EnsureIndex(s => s.Name, true);
_profileIcons = _repository.Database.GetStorage<Guid>("profileIcons");
}
public void Add(ProfileCategoryEntity profileCategoryEntity)
{
_repository.Insert(profileCategoryEntity);
}
public void Remove(ProfileCategoryEntity profileCategoryEntity)
{
_repository.Delete<ProfileCategoryEntity>(profileCategoryEntity.Id);
}
public List<ProfileCategoryEntity> GetAll()
{
return _repository.Query<ProfileCategoryEntity>().ToList();
}
public ProfileCategoryEntity Get(Guid id)
{
return _repository.FirstOrDefault<ProfileCategoryEntity>(p => p.Id == id);
}
public ProfileCategoryEntity IsUnique(string name, Guid? id)
{
if (id == null)
return _repository.FirstOrDefault<ProfileCategoryEntity>(p => p.Name == name);
return _repository.FirstOrDefault<ProfileCategoryEntity>(p => p.Name == name && p.Id != id.Value);
}
public void Save(ProfileCategoryEntity profileCategoryEntity)
{
_repository.Upsert(profileCategoryEntity);
}
public Stream GetProfileIconStream(Guid id)
{
if (!_profileIcons.Exists(id))
return null;
MemoryStream stream = new();
_profileIcons.Download(id, stream);
return stream;
}
public void SaveProfileIconStream(ProfileConfigurationEntity profileConfigurationEntity, Stream stream)
{
if (profileConfigurationEntity.FileIconId == Guid.Empty)
profileConfigurationEntity.FileIconId = Guid.NewGuid();
if (stream == null && _profileIcons.Exists(profileConfigurationEntity.FileIconId))
_profileIcons.Delete(profileConfigurationEntity.FileIconId);
_profileIcons.Upload(profileConfigurationEntity.FileIconId, "image", stream);
}
}
}

View File

@ -36,15 +36,6 @@ namespace Artemis.Storage.Repositories
return _repository.FirstOrDefault<ProfileEntity>(p => p.Id == id); return _repository.FirstOrDefault<ProfileEntity>(p => p.Id == id);
} }
public List<ProfileEntity> GetByModuleId(string moduleId)
{
return _repository.Query<ProfileEntity>()
.Include(p => p.Folders)
.Include(p => p.Layers)
.Where(s => s.ModuleId == moduleId)
.ToList();
}
public void Save(ProfileEntity profileEntity) public void Save(ProfileEntity profileEntity)
{ {
_repository.Upsert(profileEntity); _repository.Upsert(profileEntity);

View File

@ -0,0 +1,63 @@
<UserControl x:Class="Artemis.UI.Shared.ProfileConfigurationIcon"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:core="clr-namespace:Artemis.Core;assembly=Artemis.Core"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
xmlns:shared="clr-namespace:Artemis.UI.Shared"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<shared:StreamToBitmapImageConverter x:Key="StreamToBitmapImageConverter" />
</UserControl.Resources>
<ContentControl>
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding ConfigurationIcon.IconType, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Value="{x:Static core:ProfileConfigurationIconType.MaterialIcon}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<materialDesign:PackIcon Kind="{Binding ConfigurationIcon.MaterialIcon, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Width="Auto"
Height="Auto" />
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding ConfigurationIcon.IconType, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Value="{x:Static core:ProfileConfigurationIconType.BitmapImage}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Image
Source="{Binding ConfigurationIcon.FileIcon, Converter={StaticResource StreamToBitmapImageConverter}, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
RenderOptions.BitmapScalingMode="HighQuality"
Width="Auto"
Height="Auto" />
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding ConfigurationIcon.IconType, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Value="{x:Static core:ProfileConfigurationIconType.SvgImage}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<svgc:SvgViewbox StreamSource="{Binding ConfigurationIcon.FileIcon, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
RenderOptions.BitmapScalingMode="HighQuality"
Width="Auto"
Height="Auto" />
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</UserControl>

View File

@ -0,0 +1,30 @@
using System.Windows;
using System.Windows.Controls;
using MaterialDesignThemes.Wpf;
namespace Artemis.UI.Shared
{
/// <summary>
/// Interaction logic for ProfileConfigurationIcon.xaml
/// </summary>
public partial class ProfileConfigurationIcon : UserControl
{
/// <summary>
/// Gets or sets the <see cref="PackIconKind" />
/// </summary>
public static readonly DependencyProperty ConfigurationIconProperty =
DependencyProperty.Register(nameof(ConfigurationIcon), typeof(Core.ProfileConfigurationIcon), typeof(ProfileConfigurationIcon));
public ProfileConfigurationIcon()
{
InitializeComponent();
}
public Core.ProfileConfigurationIcon ConfigurationIcon
{
get => (Core.ProfileConfigurationIcon) GetValue(ConfigurationIconProperty);
set => SetValue(ConfigurationIconProperty, value);
}
}
}

View File

@ -0,0 +1,42 @@
using System;
using System.Globalization;
using System.IO;
using System.Windows.Data;
using System.Windows.Media.Imaging;
namespace Artemis.UI.Shared
{
/// <inheritdoc />
/// <summary>
/// Converts <see cref="T:Stream" /> into <see cref="T:BitmapImage" />.
/// </summary>
[ValueConversion(typeof(Stream), typeof(BitmapImage))]
public class StreamToBitmapImageConverter : IValueConverter
{
/// <inheritdoc />
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is not Stream stream)
return null;
stream.Position = 0;
BitmapImage selectedBitmap = new();
selectedBitmap.BeginInit();
selectedBitmap.StreamSource = stream;
selectedBitmap.CacheOption = BitmapCacheOption.OnLoad;
selectedBitmap.EndInit();
selectedBitmap.Freeze();
stream.Position = 0;
return selectedBitmap;
}
/// <inheritdoc />
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return Binding.DoNothing;
}
}
}

View File

@ -77,6 +77,11 @@
</ContextMenu.Resources> </ContextMenu.Resources>
<ContextMenu.ItemsSource> <ContextMenu.ItemsSource>
<CompositeCollection> <CompositeCollection>
<MenuItem Header="No modules available" IsEnabled="False" Visibility="{Binding Data.HasNoModules, Source={StaticResource DataContextProxy}, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
<MenuItem.Icon>
<materialDesign:PackIcon Kind="EmojiSad" />
</MenuItem.Icon>
</MenuItem>
<CollectionContainer Collection="{Binding Data.ExtraDataModelViewModels, Source={StaticResource DataContextProxy}}" /> <CollectionContainer Collection="{Binding Data.ExtraDataModelViewModels, Source={StaticResource DataContextProxy}}" />
<Separator Visibility="{Binding Data.HasExtraDataModels, Source={StaticResource DataContextProxy}, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}"/> <Separator Visibility="{Binding Data.HasExtraDataModels, Source={StaticResource DataContextProxy}, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}"/>
<CollectionContainer Collection="{Binding Data.DataModelViewModel.Children, Source={StaticResource DataContextProxy}}" /> <CollectionContainer Collection="{Binding Data.DataModelViewModel.Children, Source={StaticResource DataContextProxy}}" />

View File

@ -19,8 +19,8 @@ namespace Artemis.UI.Shared.Input
/// </summary> /// </summary>
public class DataModelDynamicViewModel : PropertyChangedBase, IDisposable public class DataModelDynamicViewModel : PropertyChangedBase, IDisposable
{ {
private readonly List<Module> _modules;
private readonly IDataModelUIService _dataModelUIService; private readonly IDataModelUIService _dataModelUIService;
private readonly Module _module;
private readonly Timer _updateTimer; private readonly Timer _updateTimer;
private SolidColorBrush _buttonBrush = new(Color.FromRgb(171, 71, 188)); private SolidColorBrush _buttonBrush = new(Color.FromRgb(171, 71, 188));
private DataModelPath? _dataModelPath; private DataModelPath? _dataModelPath;
@ -31,9 +31,9 @@ namespace Artemis.UI.Shared.Input
private bool _isEnabled = true; private bool _isEnabled = true;
private string _placeholder = "Select a property"; private string _placeholder = "Select a property";
internal DataModelDynamicViewModel(Module module, ISettingsService settingsService, IDataModelUIService dataModelUIService) internal DataModelDynamicViewModel(List<Module> modules, ISettingsService settingsService, IDataModelUIService dataModelUIService)
{ {
_module = module; _modules = modules;
_dataModelUIService = dataModelUIService; _dataModelUIService = dataModelUIService;
_updateTimer = new Timer(500); _updateTimer = new Timer(500);
@ -107,6 +107,11 @@ namespace Artemis.UI.Shared.Input
/// </summary> /// </summary>
public BindableCollection<DataModelPropertiesViewModel> ExtraDataModelViewModels { get; } public BindableCollection<DataModelPropertiesViewModel> ExtraDataModelViewModels { get; }
/// <summary>
/// Gets a boolean indicating whether there are any modules providing data models
/// </summary>
public bool HasNoModules => (DataModelViewModel == null || !DataModelViewModel.Children.Any()) && !HasExtraDataModels;
/// <summary> /// <summary>
/// Gets a boolean indicating whether there are any extra data models /// Gets a boolean indicating whether there are any extra data models
/// </summary> /// </summary>
@ -233,7 +238,7 @@ namespace Artemis.UI.Shared.Input
private void Initialize() private void Initialize()
{ {
// Get the data models // Get the data models
DataModelViewModel = _dataModelUIService.GetPluginDataModelVisualization(_module, true); DataModelViewModel = _dataModelUIService.GetPluginDataModelVisualization(_modules, true);
if (DataModelViewModel != null) if (DataModelViewModel != null)
DataModelViewModel.UpdateRequested += DataModelOnUpdateRequested; DataModelViewModel.UpdateRequested += DataModelOnUpdateRequested;
ExtraDataModelViewModels.CollectionChanged += ExtraDataModelViewModelsOnCollectionChanged; ExtraDataModelViewModels.CollectionChanged += ExtraDataModelViewModelsOnCollectionChanged;

View File

@ -0,0 +1,32 @@
using System;
using Artemis.Core;
namespace Artemis.UI.Shared
{
/// <summary>
/// Provides data on profile related events raised by the profile editor
/// </summary>
public class ProfileConfigurationEventArgs : EventArgs
{
internal ProfileConfigurationEventArgs(ProfileConfiguration? profileConfiguration)
{
ProfileConfiguration = profileConfiguration;
}
internal ProfileConfigurationEventArgs(ProfileConfiguration? profileConfiguration, ProfileConfiguration? previousProfileConfiguration)
{
ProfileConfiguration = profileConfiguration;
PreviousProfileConfiguration = previousProfileConfiguration;
}
/// <summary>
/// Gets the profile the event was raised for
/// </summary>
public ProfileConfiguration? ProfileConfiguration { get; }
/// <summary>
/// If applicable, the previous active profile before the event was raised
/// </summary>
public ProfileConfiguration? PreviousProfileConfiguration { get; }
}
}

View File

@ -1,32 +0,0 @@
using System;
using Artemis.Core;
namespace Artemis.UI.Shared
{
/// <summary>
/// Provides data on profile related events raised by the profile editor
/// </summary>
public class ProfileEventArgs : EventArgs
{
internal ProfileEventArgs(Profile? profile)
{
Profile = profile;
}
internal ProfileEventArgs(Profile? profile, Profile? previousProfile)
{
Profile = profile;
PreviousProfile = previousProfile;
}
/// <summary>
/// Gets the profile the event was raised for
/// </summary>
public Profile? Profile { get; }
/// <summary>
/// If applicable, the previous active profile before the event was raised
/// </summary>
public Profile? PreviousProfile { get; }
}
}

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using Artemis.Core.DataModelExpansions; using Artemis.Core.DataModelExpansions;
using Artemis.Core.Modules; using Artemis.Core.Modules;
using Artemis.UI.Shared.Input; using Artemis.UI.Shared.Input;
@ -20,9 +21,9 @@ namespace Artemis.UI.Shared
/// <summary> /// <summary>
/// Creates a new instance of the <see cref="DataModelDynamicViewModel" /> class /// Creates a new instance of the <see cref="DataModelDynamicViewModel" /> class
/// </summary> /// </summary>
/// <param name="module">The module to associate the dynamic view model with</param> /// <param name="modules">The modules to associate the dynamic view model with</param>
/// <returns>A new instance of the <see cref="DataModelDynamicViewModel" /> class</returns> /// <returns>A new instance of the <see cref="DataModelDynamicViewModel" /> class</returns>
DataModelDynamicViewModel DataModelDynamicViewModel(Module module); DataModelDynamicViewModel DataModelDynamicViewModel(List<Module> modules);
/// <summary> /// <summary>
/// Creates a new instance of the <see cref="DataModelStaticViewModel" /> class /// Creates a new instance of the <see cref="DataModelStaticViewModel" /> class

View File

@ -142,7 +142,7 @@ namespace Artemis.UI.Shared
if (InputDragging) if (InputDragging)
ProfileEditorService.UpdateProfilePreview(); ProfileEditorService.UpdateProfilePreview();
else else
ProfileEditorService.UpdateSelectedProfileElement(); ProfileEditorService.SaveSelectedProfileElement();
} }
} }
@ -186,7 +186,7 @@ namespace Artemis.UI.Shared
public void InputDragEnded(object sender, EventArgs e) public void InputDragEnded(object sender, EventArgs e)
{ {
InputDragging = false; InputDragging = false;
ProfileEditorService.UpdateSelectedProfileElement(); ProfileEditorService.SaveSelectedProfileElement();
} }
private void LayerPropertyOnUpdated(object? sender, EventArgs e) private void LayerPropertyOnUpdated(object? sender, EventArgs e)

View File

@ -35,7 +35,7 @@ namespace Artemis.UI.Shared.Services
public DataModelPropertiesViewModel GetMainDataModelVisualization() public DataModelPropertiesViewModel GetMainDataModelVisualization()
{ {
DataModelPropertiesViewModel viewModel = new(null, null, null); DataModelPropertiesViewModel viewModel = new(null, null, null);
foreach (DataModel dataModelExpansion in _dataModelService.GetDataModels().OrderBy(d => d.DataModelDescription.Name)) foreach (DataModel dataModelExpansion in _dataModelService.GetDataModels().Where(d => d.IsExpansion).OrderBy(d => d.DataModelDescription.Name))
viewModel.Children.Add(new DataModelPropertiesViewModel(dataModelExpansion, viewModel, new DataModelPath(dataModelExpansion))); viewModel.Children.Add(new DataModelPropertiesViewModel(dataModelExpansion, viewModel, new DataModelPath(dataModelExpansion)));
// Update to populate children // Update to populate children
@ -47,7 +47,7 @@ namespace Artemis.UI.Shared.Services
public void UpdateModules(DataModelPropertiesViewModel mainDataModelVisualization) public void UpdateModules(DataModelPropertiesViewModel mainDataModelVisualization)
{ {
List<DataModelVisualizationViewModel> disabledChildren = mainDataModelVisualization.Children List<DataModelVisualizationViewModel> disabledChildren = mainDataModelVisualization.Children
.Where(d => d.DataModel != null && !d.DataModel.Feature.IsEnabled) .Where(d => d.DataModel != null && !d.DataModel.Module.IsEnabled)
.ToList(); .ToList();
foreach (DataModelVisualizationViewModel child in disabledChildren) foreach (DataModelVisualizationViewModel child in disabledChildren)
mainDataModelVisualization.Children.Remove(child); mainDataModelVisualization.Children.Remove(child);
@ -65,34 +65,33 @@ namespace Artemis.UI.Shared.Services
mainDataModelVisualization.Update(this, null); mainDataModelVisualization.Update(this, null);
} }
public DataModelPropertiesViewModel? GetPluginDataModelVisualization(PluginFeature pluginFeature, bool includeMainDataModel) public DataModelPropertiesViewModel? GetPluginDataModelVisualization(List<Module> modules, bool includeMainDataModel)
{ {
DataModelPropertiesViewModel root;
// This will contain any modules that are always available
if (includeMainDataModel) if (includeMainDataModel)
root = GetMainDataModelVisualization();
else
{ {
DataModelPropertiesViewModel mainDataModel = GetMainDataModelVisualization(); root = new DataModelPropertiesViewModel(null, null, null);
root.UpdateRequested += (sender, args) => root.Update(this, null);
// If the main data model already includes the plugin data model we're done
if (mainDataModel.Children.Any(c => c.DataModel?.Feature == pluginFeature))
return mainDataModel;
// Otherwise get just the plugin data model and add it
DataModelPropertiesViewModel? pluginDataModel = GetPluginDataModelVisualization(pluginFeature, false);
if (pluginDataModel != null)
mainDataModel.Children.Add(pluginDataModel);
return mainDataModel;
} }
DataModel? dataModel = _dataModelService.GetPluginDataModel(pluginFeature); foreach (Module module in modules)
{
DataModel? dataModel = _dataModelService.GetPluginDataModel(module);
if (dataModel == null) if (dataModel == null)
continue;
root.Children.Add(new DataModelPropertiesViewModel(dataModel, root, new DataModelPath(dataModel)));
}
if (!root.Children.Any())
return null; return null;
DataModelPropertiesViewModel viewModel = new(null, null, null);
viewModel.Children.Add(new DataModelPropertiesViewModel(dataModel, viewModel, new DataModelPath(dataModel)));
// Update to populate children // Update to populate children
viewModel.Update(this, null); root.Update(this, null);
viewModel.UpdateRequested += (sender, args) => viewModel.Update(this, null); return root;
return viewModel;
} }
public DataModelVisualizationRegistration RegisterDataModelInput<T>(Plugin plugin, IReadOnlyCollection<Type>? compatibleConversionTypes = null) where T : DataModelInputViewModel public DataModelVisualizationRegistration RegisterDataModelInput<T>(Plugin plugin, IReadOnlyCollection<Type>? compatibleConversionTypes = null) where T : DataModelInputViewModel
@ -226,9 +225,14 @@ namespace Artemis.UI.Shared.Services
} }
} }
public DataModelDynamicViewModel GetDynamicSelectionViewModel(Module module) public DataModelDynamicViewModel GetDynamicSelectionViewModel(Module? module)
{ {
return _dataModelVmFactory.DataModelDynamicViewModel(module); return _dataModelVmFactory.DataModelDynamicViewModel(module == null ? new List<Module>() : new List<Module> {module});
}
public DataModelDynamicViewModel GetDynamicSelectionViewModel(List<Module> modules)
{
return _dataModelVmFactory.DataModelDynamicViewModel(modules);
} }
public DataModelStaticViewModel GetStaticInputViewModel(Type targetType, DataModelPropertyAttribute targetDescription) public DataModelStaticViewModel GetStaticInputViewModel(Type targetType, DataModelPropertyAttribute targetDescription)

View File

@ -34,10 +34,13 @@ namespace Artemis.UI.Shared.Services
/// <summary> /// <summary>
/// Creates a data model visualization view model for the data model of the provided plugin feature /// Creates a data model visualization view model for the data model of the provided plugin feature
/// </summary> /// </summary>
/// <param name="pluginFeature">The plugin feature to create hte data model visualization view model for</param> /// <param name="modules">The modules to create the data model visualization view model for</param>
/// <param name="includeMainDataModel">Whether or not also to include the main data model</param> /// <param name="includeMainDataModel">
/// Whether or not also to include the main data model (and therefore any modules marked
/// as <see cref="Module.IsAlwaysAvailable" />)
/// </param>
/// <returns>A data model visualization view model containing the data model of the provided feature</returns> /// <returns>A data model visualization view model containing the data model of the provided feature</returns>
DataModelPropertiesViewModel? GetPluginDataModelVisualization(PluginFeature pluginFeature, bool includeMainDataModel); DataModelPropertiesViewModel? GetPluginDataModelVisualization(List<Module> modules, bool includeMainDataModel);
/// <summary> /// <summary>
/// Updates the children of the provided main data model visualization, removing disabled children and adding newly /// Updates the children of the provided main data model visualization, removing disabled children and adding newly
@ -105,9 +108,16 @@ namespace Artemis.UI.Shared.Services
/// <summary> /// <summary>
/// Creates a view model that allows selecting a value from the data model /// Creates a view model that allows selecting a value from the data model
/// </summary> /// </summary>
/// <param name="module"></param> /// <param name="module">An extra non-always active module to include</param>
/// <returns></returns>
DataModelDynamicViewModel GetDynamicSelectionViewModel(Module? module);
/// <summary>
/// Creates a view model that allows selecting a value from the data model
/// </summary>
/// <param name="modules">A list of extra extra non-always active modules to include</param>
/// <returns>A view model that allows selecting a value from the data model</returns> /// <returns>A view model that allows selecting a value from the data model</returns>
DataModelDynamicViewModel GetDynamicSelectionViewModel(Module module); DataModelDynamicViewModel GetDynamicSelectionViewModel(List<Module> modules);
/// <summary> /// <summary>
/// Creates a view model that allows entering a value matching the target data model type /// Creates a view model that allows entering a value matching the target data model type

View File

@ -19,7 +19,7 @@ namespace Artemis.UI.Shared.Services
/// <param name="confirmText">The text of the confirm button, defaults to "Confirm"</param> /// <param name="confirmText">The text of the confirm button, defaults to "Confirm"</param>
/// <param name="cancelText">The text of the cancel button, defaults to "Cancel"</param> /// <param name="cancelText">The text of the cancel button, defaults to "Cancel"</param>
/// <returns>A task that resolves to true if confirmed and false if cancelled</returns> /// <returns>A task that resolves to true if confirmed and false if cancelled</returns>
Task<bool> ShowConfirmDialog(string header, string text, string confirmText = "Confirm", string cancelText = "Cancel"); Task<bool> ShowConfirmDialog(string header, string text, string confirmText = "Confirm", string? cancelText = "Cancel");
/// <summary> /// <summary>
/// Shows a confirm dialog on the dialog host provided in identifier. /// Shows a confirm dialog on the dialog host provided in identifier.
@ -33,7 +33,7 @@ namespace Artemis.UI.Shared.Services
/// <param name="confirmText">The text of the confirm button, defaults to "Confirm"</param> /// <param name="confirmText">The text of the confirm button, defaults to "Confirm"</param>
/// <param name="cancelText">The text of the cancel button, defaults to "Cancel"</param> /// <param name="cancelText">The text of the cancel button, defaults to "Cancel"</param>
/// <returns>A task that resolves to true if confirmed and false if cancelled</returns> /// <returns>A task that resolves to true if confirmed and false if cancelled</returns>
Task<bool> ShowConfirmDialogAt(string identifier, string header, string text, string confirmText = "Confirm", string cancelText = "Cancel"); Task<bool> ShowConfirmDialogAt(string identifier, string header, string text, string confirmText = "Confirm", string? cancelText = "Cancel");
/// <summary> /// <summary>
/// Shows a dialog by initializing a view model implementing <see cref="DialogViewModelBase" /> /// Shows a dialog by initializing a view model implementing <see cref="DialogViewModelBase" />

View File

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Windows; using System.Windows;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Modules;
namespace Artemis.UI.Shared.Services namespace Artemis.UI.Shared.Services
{ {
@ -12,8 +11,15 @@ namespace Artemis.UI.Shared.Services
/// </summary> /// </summary>
public interface IProfileEditorService : IArtemisSharedUIService public interface IProfileEditorService : IArtemisSharedUIService
{ {
/// <summary>
/// Gets the currently selected profile configuration
/// <para><see langword="null" /> if the editor is closed</para>
/// </summary>
ProfileConfiguration? SelectedProfileConfiguration { get; }
/// <summary> /// <summary>
/// Gets the currently selected profile /// Gets the currently selected profile
/// <para><see langword="null" /> if the editor is closed, always equal to <see cref="SelectedProfileConfiguration" />.<see cref="Profile" /></para>
/// </summary> /// </summary>
Profile? SelectedProfile { get; } Profile? SelectedProfile { get; }
@ -48,15 +54,15 @@ namespace Artemis.UI.Shared.Services
bool Playing { get; set; } bool Playing { get; set; }
/// <summary> /// <summary>
/// Changes the selected profile /// Changes the selected profile by its <see cref="ProfileConfiguration" />
/// </summary> /// </summary>
/// <param name="profile">The profile to select</param> /// <param name="profileConfiguration">The profile configuration of the profile to select</param>
void ChangeSelectedProfile(Profile? profile); void ChangeSelectedProfileConfiguration(ProfileConfiguration? profileConfiguration);
/// <summary> /// <summary>
/// Updates the selected profile and saves it to persistent storage /// Saves the <see cref="Profile" /> of the selected <see cref="ProfileConfiguration" /> to persistent storage
/// </summary> /// </summary>
void UpdateSelectedProfile(); void SaveSelectedProfileConfiguration();
/// <summary> /// <summary>
/// Changes the selected profile element /// Changes the selected profile element
@ -65,9 +71,9 @@ namespace Artemis.UI.Shared.Services
void ChangeSelectedProfileElement(RenderProfileElement? profileElement); void ChangeSelectedProfileElement(RenderProfileElement? profileElement);
/// <summary> /// <summary>
/// Updates the selected profile element and saves the profile it is contained in to persistent storage /// Saves the currently selected <see cref="ProfileElement" /> to persistent storage
/// </summary> /// </summary>
void UpdateSelectedProfileElement(); void SaveSelectedProfileElement();
/// <summary> /// <summary>
/// Changes the selected data binding property /// Changes the selected data binding property
@ -81,22 +87,16 @@ namespace Artemis.UI.Shared.Services
void UpdateProfilePreview(); void UpdateProfilePreview();
/// <summary> /// <summary>
/// Restores the profile to the last <see cref="UpdateSelectedProfile" /> call /// Restores the profile to the last <see cref="SaveSelectedProfileConfiguration" /> call
/// </summary> /// </summary>
/// <returns><see langword="true" /> if undo was successful, otherwise <see langword="false" /></returns> /// <returns><see langword="true" /> if undo was successful, otherwise <see langword="false" /></returns>
bool UndoUpdateProfile(); bool UndoSaveProfile();
/// <summary> /// <summary>
/// Restores the profile to the last <see cref="UndoUpdateProfile" /> call /// Restores the profile to the last <see cref="UndoSaveProfile" /> call
/// </summary> /// </summary>
/// <returns><see langword="true" /> if redo was successful, otherwise <see langword="false" /></returns> /// <returns><see langword="true" /> if redo was successful, otherwise <see langword="false" /></returns>
bool RedoUpdateProfile(); bool RedoSaveProfile();
/// <summary>
/// Gets the current module the profile editor is initialized for
/// </summary>
/// <returns>The current module the profile editor is initialized for</returns>
ProfileModule? GetCurrentModule();
/// <summary> /// <summary>
/// Registers a new property input view model used in the profile editor for the generic type defined in /// Registers a new property input view model used in the profile editor for the generic type defined in
@ -182,22 +182,22 @@ namespace Artemis.UI.Shared.Services
/// <summary> /// <summary>
/// Occurs when a new profile is selected /// Occurs when a new profile is selected
/// </summary> /// </summary>
event EventHandler<ProfileEventArgs> ProfileSelected; event EventHandler<ProfileConfigurationEventArgs> SelectedProfileChanged;
/// <summary> /// <summary>
/// Occurs then the currently selected profile is updated /// Occurs then the currently selected profile is updated
/// </summary> /// </summary>
event EventHandler<ProfileEventArgs> SelectedProfileUpdated; event EventHandler<ProfileConfigurationEventArgs> SelectedProfileSaved;
/// <summary> /// <summary>
/// Occurs when a new profile element is selected /// Occurs when a new profile element is selected
/// </summary> /// </summary>
event EventHandler<RenderProfileElementEventArgs> ProfileElementSelected; event EventHandler<RenderProfileElementEventArgs> SelectedProfileElementChanged;
/// <summary> /// <summary>
/// Occurs when the currently selected profile element is updated /// Occurs when the currently selected profile element is updated
/// </summary> /// </summary>
event EventHandler<RenderProfileElementEventArgs> SelectedProfileElementUpdated; event EventHandler<RenderProfileElementEventArgs> SelectedProfileElementSaved;
/// <summary> /// <summary>
/// Occurs when the currently selected data binding layer property is changed /// Occurs when the currently selected data binding layer property is changed

View File

@ -24,70 +24,25 @@ namespace Artemis.UI.Shared.Services
private readonly IProfileService _profileService; private readonly IProfileService _profileService;
private readonly List<PropertyInputRegistration> _registeredPropertyEditors; private readonly List<PropertyInputRegistration> _registeredPropertyEditors;
private readonly IRgbService _rgbService; private readonly IRgbService _rgbService;
private readonly IModuleService _moduleService;
private readonly object _selectedProfileElementLock = new(); private readonly object _selectedProfileElementLock = new();
private readonly object _selectedProfileLock = new(); private readonly object _selectedProfileLock = new();
private TimeSpan _currentTime; private TimeSpan _currentTime;
private bool _doTick; private bool _doTick;
private int _pixelsPerSecond; private int _pixelsPerSecond;
public ProfileEditorService(IKernel kernel, ILogger logger, IProfileService profileService, ICoreService coreService, IRgbService rgbService) public ProfileEditorService(IKernel kernel, ILogger logger, IProfileService profileService, ICoreService coreService, IRgbService rgbService, IModuleService moduleService)
{ {
_kernel = kernel; _kernel = kernel;
_logger = logger; _logger = logger;
_profileService = profileService; _profileService = profileService;
_rgbService = rgbService; _rgbService = rgbService;
_moduleService = moduleService;
_registeredPropertyEditors = new List<PropertyInputRegistration>(); _registeredPropertyEditors = new List<PropertyInputRegistration>();
coreService.FrameRendered += CoreServiceOnFrameRendered; coreService.FrameRendered += CoreServiceOnFrameRendered;
PixelsPerSecond = 100; PixelsPerSecond = 100;
} }
public event EventHandler? CurrentTimelineChanged;
protected virtual void OnSelectedProfileChanged(ProfileEventArgs e)
{
ProfileSelected?.Invoke(this, e);
}
protected virtual void OnSelectedProfileUpdated(ProfileEventArgs e)
{
SelectedProfileUpdated?.Invoke(this, e);
}
protected virtual void OnSelectedProfileElementChanged(RenderProfileElementEventArgs e)
{
ProfileElementSelected?.Invoke(this, e);
}
protected virtual void OnSelectedProfileElementUpdated(RenderProfileElementEventArgs e)
{
SelectedProfileElementUpdated?.Invoke(this, e);
}
protected virtual void OnCurrentTimeChanged()
{
CurrentTimeChanged?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnCurrentTimelineChanged()
{
CurrentTimelineChanged?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnPixelsPerSecondChanged()
{
PixelsPerSecondChanged?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnProfilePreviewUpdated()
{
ProfilePreviewUpdated?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnSelectedDataBindingChanged()
{
SelectedDataBindingChanged?.Invoke(this, EventArgs.Empty);
}
private void CoreServiceOnFrameRendered(object? sender, FrameRenderedEventArgs e) private void CoreServiceOnFrameRendered(object? sender, FrameRenderedEventArgs e)
{ {
if (!_doTick) return; if (!_doTick) return;
@ -101,7 +56,7 @@ namespace Artemis.UI.Shared.Services
return; return;
// Trigger a profile change // Trigger a profile change
OnSelectedProfileChanged(new ProfileEventArgs(SelectedProfile, SelectedProfile)); OnSelectedProfileChanged(new ProfileConfigurationEventArgs(SelectedProfileConfiguration, SelectedProfileConfiguration));
// Trigger a selected element change // Trigger a selected element change
RenderProfileElement? previousSelectedProfileElement = SelectedProfileElement; RenderProfileElement? previousSelectedProfileElement = SelectedProfileElement;
if (SelectedProfileElement is Folder folder) if (SelectedProfileElement is Folder folder)
@ -148,16 +103,11 @@ namespace Artemis.UI.Shared.Services
} }
} }
private void SelectedProfileOnDeactivated(object? sender, EventArgs e)
{
// Execute.PostToUIThread(() => ChangeSelectedProfile(null));
ChangeSelectedProfile(null);
}
public ReadOnlyCollection<PropertyInputRegistration> RegisteredPropertyEditors => _registeredPropertyEditors.AsReadOnly(); public ReadOnlyCollection<PropertyInputRegistration> RegisteredPropertyEditors => _registeredPropertyEditors.AsReadOnly();
public bool Playing { get; set; } public bool Playing { get; set; }
public Profile? SelectedProfile { get; private set; } public ProfileConfiguration? SelectedProfileConfiguration { get; private set; }
public Profile? SelectedProfile => SelectedProfileConfiguration?.Profile;
public RenderProfileElement? SelectedProfileElement { get; private set; } public RenderProfileElement? SelectedProfileElement { get; private set; }
public ILayerProperty? SelectedDataBinding { get; private set; } public ILayerProperty? SelectedDataBinding { get; private set; }
@ -183,43 +133,54 @@ namespace Artemis.UI.Shared.Services
} }
} }
public void ChangeSelectedProfile(Profile? profile) public void ChangeSelectedProfileConfiguration(ProfileConfiguration? profileConfiguration)
{ {
lock (_selectedProfileLock) lock (_selectedProfileLock)
{ {
if (SelectedProfile == profile) if (SelectedProfileConfiguration == profileConfiguration)
return; return;
if (profile != null && !profile.IsActivated) if (profileConfiguration?.Profile != null && profileConfiguration.Profile.Disposed)
throw new ArtemisSharedUIException("Cannot change the selected profile to an inactive profile"); throw new ArtemisSharedUIException("Cannot select a disposed profile");
_logger.Verbose("ChangeSelectedProfile {profile}", profile); _logger.Verbose("ChangeSelectedProfileConfiguration {profile}", profileConfiguration);
ChangeSelectedProfileElement(null); ChangeSelectedProfileElement(null);
ProfileConfigurationEventArgs profileConfigurationElementEvent = new(profileConfiguration, SelectedProfileConfiguration);
ProfileEventArgs profileElementEvent = new(profile, SelectedProfile); // No need to deactivate the profile, if needed it will be deactivated next update
if (SelectedProfileConfiguration != null)
SelectedProfileConfiguration.IsBeingEdited = false;
// Ensure there is never a deactivated profile as the selected profile // The new profile may need activation
if (SelectedProfile != null) SelectedProfileConfiguration = profileConfiguration;
SelectedProfile.Deactivated -= SelectedProfileOnDeactivated; if (SelectedProfileConfiguration != null)
SelectedProfile = profile; {
if (SelectedProfile != null) SelectedProfileConfiguration.IsBeingEdited = true;
SelectedProfile.Deactivated += SelectedProfileOnDeactivated; _moduleService.SetActivationOverride(SelectedProfileConfiguration.Module);
_profileService.ActivateProfile(SelectedProfileConfiguration);
_profileService.RenderForEditor = true;
}
else
{
_moduleService.SetActivationOverride(null);
_profileService.RenderForEditor = false;
}
OnSelectedProfileChanged(profileElementEvent); OnSelectedProfileChanged(profileConfigurationElementEvent);
UpdateProfilePreview(); UpdateProfilePreview();
} }
} }
public void UpdateSelectedProfile() public void SaveSelectedProfileConfiguration()
{ {
lock (_selectedProfileLock) lock (_selectedProfileLock)
{ {
_logger.Verbose("UpdateSelectedProfile {profile}", SelectedProfile); _logger.Verbose("SaveSelectedProfileConfiguration {profile}", SelectedProfile);
if (SelectedProfile == null) if (SelectedProfile == null)
return; return;
_profileService.UpdateProfile(SelectedProfile, true); _profileService.SaveProfile(SelectedProfile, true);
OnSelectedProfileUpdated(new ProfileEventArgs(SelectedProfile)); OnSelectedProfileUpdated(new ProfileConfigurationEventArgs(SelectedProfileConfiguration));
UpdateProfilePreview(); UpdateProfilePreview();
} }
} }
@ -240,15 +201,15 @@ namespace Artemis.UI.Shared.Services
} }
} }
public void UpdateSelectedProfileElement() public void SaveSelectedProfileElement()
{ {
lock (_selectedProfileElementLock) lock (_selectedProfileElementLock)
{ {
_logger.Verbose("UpdateSelectedProfileElement {profile}", SelectedProfileElement); _logger.Verbose("SaveSelectedProfileElement {profile}", SelectedProfileElement);
if (SelectedProfile == null) if (SelectedProfile == null)
return; return;
_profileService.UpdateProfile(SelectedProfile, true); _profileService.SaveProfile(SelectedProfile, true);
OnSelectedProfileElementUpdated(new RenderProfileElementEventArgs(SelectedProfileElement)); OnSelectedProfileElementUpdated(new RenderProfileElementEventArgs(SelectedProfileElement));
UpdateProfilePreview(); UpdateProfilePreview();
} }
@ -267,12 +228,12 @@ namespace Artemis.UI.Shared.Services
Tick(); Tick();
} }
public bool UndoUpdateProfile() public bool UndoSaveProfile()
{ {
if (SelectedProfile == null) if (SelectedProfile == null)
return false; return false;
bool undid = _profileService.UndoUpdateProfile(SelectedProfile); bool undid = _profileService.UndoSaveProfile(SelectedProfile);
if (!undid) if (!undid)
return false; return false;
@ -280,12 +241,12 @@ namespace Artemis.UI.Shared.Services
return true; return true;
} }
public bool RedoUpdateProfile() public bool RedoSaveProfile()
{ {
if (SelectedProfile == null) if (SelectedProfile == null)
return false; return false;
bool redid = _profileService.RedoUpdateProfile(SelectedProfile); bool redid = _profileService.RedoSaveProfile(SelectedProfile);
if (!redid) if (!redid)
return false; return false;
@ -319,8 +280,11 @@ namespace Artemis.UI.Shared.Services
if (existing != null) if (existing != null)
{ {
if (existing.Plugin != plugin) if (existing.Plugin != plugin)
{
throw new ArtemisSharedUIException($"Cannot register property editor for type {supportedType.Name} because an editor was already " + throw new ArtemisSharedUIException($"Cannot register property editor for type {supportedType.Name} because an editor was already " +
$"registered by {existing.Plugin}"); $"registered by {existing.Plugin}");
}
return existing; return existing;
} }
@ -364,8 +328,10 @@ namespace Artemis.UI.Shared.Services
if (snapToCurrentTime) if (snapToCurrentTime)
// Snap to the current time // Snap to the current time
{
if (Math.Abs(time.TotalMilliseconds - CurrentTime.TotalMilliseconds) < tolerance.TotalMilliseconds) if (Math.Abs(time.TotalMilliseconds - CurrentTime.TotalMilliseconds) < tolerance.TotalMilliseconds)
return CurrentTime; return CurrentTime;
}
if (snapTimes != null) if (snapTimes != null)
{ {
@ -401,13 +367,9 @@ namespace Artemis.UI.Shared.Services
viewModelType = registration.ViewModelType.MakeGenericType(layerProperty.GetType().GenericTypeArguments); viewModelType = registration.ViewModelType.MakeGenericType(layerProperty.GetType().GenericTypeArguments);
} }
else if (registration != null) else if (registration != null)
{
viewModelType = registration.ViewModelType; viewModelType = registration.ViewModelType;
}
else else
{
return null; return null;
}
if (viewModelType == null) if (viewModelType == null)
return null; return null;
@ -419,11 +381,6 @@ namespace Artemis.UI.Shared.Services
return (PropertyInputViewModel<T>) kernel.Get(viewModelType, parameter); return (PropertyInputViewModel<T>) kernel.Get(viewModelType, parameter);
} }
public ProfileModule? GetCurrentModule()
{
return SelectedProfile?.Module;
}
public List<ArtemisLed> GetLedsInRectangle(Rect rect) public List<ArtemisLed> GetLedsInRectangle(Rect rect)
{ {
return _rgbService.EnabledDevices return _rgbService.EnabledDevices
@ -432,15 +389,6 @@ namespace Artemis.UI.Shared.Services
.ToList(); .ToList();
} }
public event EventHandler<ProfileEventArgs>? ProfileSelected;
public event EventHandler<ProfileEventArgs>? SelectedProfileUpdated;
public event EventHandler<RenderProfileElementEventArgs>? ProfileElementSelected;
public event EventHandler<RenderProfileElementEventArgs>? SelectedProfileElementUpdated;
public event EventHandler? SelectedDataBindingChanged;
public event EventHandler? CurrentTimeChanged;
public event EventHandler? PixelsPerSecondChanged;
public event EventHandler? ProfilePreviewUpdated;
#region Copy/paste #region Copy/paste
public ProfileElement? DuplicateProfileElement(ProfileElement profileElement) public ProfileElement? DuplicateProfileElement(ProfileElement profileElement)
@ -512,7 +460,7 @@ namespace Artemis.UI.Shared.Services
if (pasted != null) if (pasted != null)
{ {
target.Profile.PopulateLeds(_rgbService.EnabledDevices); target.Profile.PopulateLeds(_rgbService.EnabledDevices);
UpdateSelectedProfile(); SaveSelectedProfileConfiguration();
ChangeSelectedProfileElement(pasted); ChangeSelectedProfileElement(pasted);
} }
@ -520,5 +468,58 @@ namespace Artemis.UI.Shared.Services
} }
#endregion #endregion
#region Events
public event EventHandler<ProfileConfigurationEventArgs>? SelectedProfileChanged;
public event EventHandler<ProfileConfigurationEventArgs>? SelectedProfileSaved;
public event EventHandler<RenderProfileElementEventArgs>? SelectedProfileElementChanged;
public event EventHandler<RenderProfileElementEventArgs>? SelectedProfileElementSaved;
public event EventHandler? SelectedDataBindingChanged;
public event EventHandler? CurrentTimeChanged;
public event EventHandler? PixelsPerSecondChanged;
public event EventHandler? ProfilePreviewUpdated;
protected virtual void OnSelectedProfileChanged(ProfileConfigurationEventArgs e)
{
SelectedProfileChanged?.Invoke(this, e);
}
protected virtual void OnSelectedProfileUpdated(ProfileConfigurationEventArgs e)
{
SelectedProfileSaved?.Invoke(this, e);
}
protected virtual void OnSelectedProfileElementChanged(RenderProfileElementEventArgs e)
{
SelectedProfileElementChanged?.Invoke(this, e);
}
protected virtual void OnSelectedProfileElementUpdated(RenderProfileElementEventArgs e)
{
SelectedProfileElementSaved?.Invoke(this, e);
}
protected virtual void OnCurrentTimeChanged()
{
CurrentTimeChanged?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnPixelsPerSecondChanged()
{
PixelsPerSecondChanged?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnProfilePreviewUpdated()
{
ProfilePreviewUpdated?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnSelectedDataBindingChanged()
{
SelectedDataBindingChanged?.Invoke(this, EventArgs.Empty);
}
#endregion
} }
} }

View File

@ -2,7 +2,7 @@
namespace Artemis.UI.Shared namespace Artemis.UI.Shared
{ {
internal class BindingProxy : Freezable public class BindingProxy : Freezable
{ {
// Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc... // Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DataProperty = public static readonly DependencyProperty DataProperty =

View File

@ -369,11 +369,15 @@
<Page Update="Screens\Plugins\PluginPrerequisitesUninstallDialogView.xaml"> <Page Update="Screens\Plugins\PluginPrerequisitesUninstallDialogView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime> <XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
</Page> </Page>
<Page Update="Screens\ProfileEditor\Dialogs\ProfileEditView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
</Page>
<Page Update="Screens\Settings\Debug\Tabs\Performance\PerformanceDebugView.xaml"> <Page Update="Screens\Settings\Debug\Tabs\Performance\PerformanceDebugView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime> <XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
</Page> </Page>
<Page Update="Screens\Sidebar\Dialogs\SidebarCategoryUpdateView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
<SubType>Designer</SubType>
</Page>
</ItemGroup>
<ItemGroup>
<Folder Include="Screens\Sidebar\Models\" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,41 +0,0 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Artemis.UI.Converters
{
public class NullToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Parameters direction;
if (parameter == null)
direction = Parameters.Normal;
else
direction = (Parameters) Enum.Parse(typeof(Parameters), (string) parameter);
if (direction == Parameters.Normal)
{
if (value == null)
return Visibility.Collapsed;
return Visibility.Visible;
}
if (value == null)
return Visibility.Visible;
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
private enum Parameters
{
Normal,
Inverted
}
}
}

View File

@ -0,0 +1,29 @@
using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
namespace Artemis.UI.Converters
{
/// <inheritdoc />
/// <summary>
/// Converts <see cref="T:SolidColorBrush" /> into <see cref="T:System.Windows.Media.SolidColorBrush" />.
/// </summary>
[ValueConversion(typeof(SolidColorBrush), typeof(Color))]
public class SolidColorBrushToColorConverter : IValueConverter
{
/// <inheritdoc />
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is SolidColorBrush brush)
return brush.Color;
return Colors.Transparent;
}
/// <inheritdoc />
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return Binding.DoNothing;
}
}
}

View File

@ -2,11 +2,11 @@
{ {
public class RequestSelectSidebarItemEvent public class RequestSelectSidebarItemEvent
{ {
public RequestSelectSidebarItemEvent(string label) public RequestSelectSidebarItemEvent(string displayName)
{ {
Label = label; DisplayName = displayName;
} }
public string Label { get; } public string DisplayName { get; }
} }
} }

View File

@ -1,9 +1,7 @@
using Artemis.Core; using System.Collections.Generic;
using Artemis.Core;
using Artemis.Core.Modules; using Artemis.Core.Modules;
using Artemis.UI.Screens.Modules;
using Artemis.UI.Screens.Modules.Tabs;
using Artemis.UI.Screens.Plugins; using Artemis.UI.Screens.Plugins;
using Artemis.UI.Screens.ProfileEditor;
using Artemis.UI.Screens.ProfileEditor.Conditions; using Artemis.UI.Screens.ProfileEditor.Conditions;
using Artemis.UI.Screens.ProfileEditor.LayerProperties; using Artemis.UI.Screens.ProfileEditor.LayerProperties;
using Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings; using Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings;
@ -17,12 +15,13 @@ using Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.AdaptionHints;
using Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem; using Artemis.UI.Screens.ProfileEditor.ProfileTree.TreeItem;
using Artemis.UI.Screens.ProfileEditor.Visualization; using Artemis.UI.Screens.ProfileEditor.Visualization;
using Artemis.UI.Screens.ProfileEditor.Visualization.Tools; using Artemis.UI.Screens.ProfileEditor.Visualization.Tools;
using Artemis.UI.Screens.Settings.Debug;
using Artemis.UI.Screens.Settings.Device; using Artemis.UI.Screens.Settings.Device;
using Artemis.UI.Screens.Settings.Device.Tabs; using Artemis.UI.Screens.Settings.Device.Tabs;
using Artemis.UI.Screens.Settings.Tabs.Devices; using Artemis.UI.Screens.Settings.Tabs.Devices;
using Artemis.UI.Screens.Settings.Tabs.Plugins; using Artemis.UI.Screens.Settings.Tabs.Plugins;
using Artemis.UI.Screens.Shared; using Artemis.UI.Screens.Shared;
using Artemis.UI.Screens.Sidebar;
using Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit;
using Stylet; using Stylet;
namespace Artemis.UI.Ninject.Factories namespace Artemis.UI.Ninject.Factories
@ -31,14 +30,6 @@ namespace Artemis.UI.Ninject.Factories
{ {
} }
public interface IModuleVmFactory : IVmFactory
{
ModuleRootViewModel CreateModuleRootViewModel(Module module);
ProfileEditorViewModel CreateProfileEditorViewModel(ProfileModule module);
ActivationRequirementsViewModel CreateActivationRequirementsViewModel(Module module);
ActivationRequirementViewModel CreateActivationRequirementViewModel(IModuleActivationRequirement activationRequirement);
}
public interface ISettingsVmFactory : IVmFactory public interface ISettingsVmFactory : IVmFactory
{ {
PluginSettingsViewModel CreatePluginSettingsViewModel(Plugin plugin); PluginSettingsViewModel CreatePluginSettingsViewModel(Plugin plugin);
@ -84,12 +75,12 @@ namespace Artemis.UI.Ninject.Factories
public interface IDataModelConditionsVmFactory : IVmFactory public interface IDataModelConditionsVmFactory : IVmFactory
{ {
DataModelConditionGroupViewModel DataModelConditionGroupViewModel(DataModelConditionGroup dataModelConditionGroup, ConditionGroupType groupType); DataModelConditionGroupViewModel DataModelConditionGroupViewModel(DataModelConditionGroup dataModelConditionGroup, ConditionGroupType groupType, List<Module> modules);
DataModelConditionListViewModel DataModelConditionListViewModel(DataModelConditionList dataModelConditionList); DataModelConditionListViewModel DataModelConditionListViewModel(DataModelConditionList dataModelConditionList, List<Module> modules);
DataModelConditionEventViewModel DataModelConditionEventViewModel(DataModelConditionEvent dataModelConditionEvent); DataModelConditionEventViewModel DataModelConditionEventViewModel(DataModelConditionEvent dataModelConditionEvent, List<Module> modules);
DataModelConditionGeneralPredicateViewModel DataModelConditionGeneralPredicateViewModel(DataModelConditionGeneralPredicate dataModelConditionGeneralPredicate); DataModelConditionGeneralPredicateViewModel DataModelConditionGeneralPredicateViewModel(DataModelConditionGeneralPredicate dataModelConditionGeneralPredicate, List<Module> modules);
DataModelConditionListPredicateViewModel DataModelConditionListPredicateViewModel(DataModelConditionListPredicate dataModelConditionListPredicate); DataModelConditionListPredicateViewModel DataModelConditionListPredicateViewModel(DataModelConditionListPredicate dataModelConditionListPredicate, List<Module> modules);
DataModelConditionEventPredicateViewModel DataModelConditionEventPredicateViewModel(DataModelConditionEventPredicate dataModelConditionEventPredicate); DataModelConditionEventPredicateViewModel DataModelConditionEventPredicateViewModel(DataModelConditionEventPredicate dataModelConditionEventPredicate, List<Module> modules);
} }
public interface ILayerPropertyVmFactory : IVmFactory public interface ILayerPropertyVmFactory : IVmFactory
@ -111,6 +102,13 @@ namespace Artemis.UI.Ninject.Factories
PluginPrerequisiteViewModel PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall); PluginPrerequisiteViewModel PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall);
} }
public interface ISidebarVmFactory : IVmFactory
{
SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory);
SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration);
ModuleActivationRequirementViewModel ModuleActivationRequirementViewModel(IModuleActivationRequirement activationRequirement);
}
// TODO: Move these two // TODO: Move these two
public interface IDataBindingsVmFactory public interface IDataBindingsVmFactory
{ {

View File

@ -1,26 +0,0 @@
<UserControl x:Class="Artemis.UI.Screens.Modules.ModuleRootView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:modules="clr-namespace:Artemis.UI.Screens.Modules"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance modules:ModuleRootViewModel}">
<TabControl Margin="0 -1 0 0"
ItemsSource="{Binding Items}"
SelectedItem="{Binding ActiveItem}"
DisplayMemberPath="DisplayName"
Style="{StaticResource MaterialDesignTabControl}">
<TabControl.ContentTemplate>
<DataTemplate>
<materialDesign:TransitioningContent OpeningEffect="{materialDesign:TransitionEffect FadeIn}">
<ContentControl s:View.Model="{Binding IsAsync=True}" TextElement.Foreground="{DynamicResource MaterialDesignBody}" />
</materialDesign:TransitioningContent>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</UserControl>

View File

@ -1,57 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using Artemis.Core.Modules;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Shared.Modules;
using Ninject;
using Ninject.Parameters;
using Stylet;
namespace Artemis.UI.Screens.Modules
{
public class ModuleRootViewModel : Conductor<Screen>.Collection.OneActive
{
private readonly IModuleVmFactory _moduleVmFactory;
public ModuleRootViewModel(Module module, IModuleVmFactory moduleVmFactory)
{
DisplayName = module?.DisplayName;
Module = module;
_moduleVmFactory = moduleVmFactory;
}
public Module Module { get; }
protected override void OnInitialActivate()
{
AddTabs();
base.OnInitialActivate();
}
private void AddTabs()
{
// Create the profile editor and module VMs
if (Module is ProfileModule profileModule)
Items.Add(_moduleVmFactory.CreateProfileEditorViewModel(profileModule));
if (Module.ActivationRequirements.Any())
Items.Add(_moduleVmFactory.CreateActivationRequirementsViewModel(Module));
if (Module.ModuleTabs != null)
{
List<ModuleTab> moduleTabs = new(Module.ModuleTabs);
foreach (ModuleTab moduleTab in moduleTabs.Where(m => m != null))
{
ConstructorArgument module = new("module", Module);
ConstructorArgument displayName = new("displayName", DisplayName);
ModuleViewModel viewModel = (ModuleViewModel) Module.Plugin.Kernel.Get(moduleTab.Type, module, displayName);
Items.Add(viewModel);
}
}
ActiveItem = Items.FirstOrDefault();
}
}
}

View File

@ -1,44 +0,0 @@
<UserControl x:Class="Artemis.UI.Screens.Modules.Tabs.ActivationRequirementView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:tabs="clr-namespace:Artemis.UI.Screens.Modules.Tabs"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance tabs:ActivationRequirementViewModel}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Style="{StaticResource MaterialDesignTextBlock}"
Text="{Binding RequirementName}" />
<TextBlock Style="{StaticResource MaterialDesignTextBlock}"
Foreground="{DynamicResource MaterialDesignNavigationItemSubheader}"
TextWrapping="Wrap"
Text="{Binding RequirementDescription}" />
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
<ToggleButton Style="{StaticResource MaterialDesignActionToggleButton}"
Focusable="False"
IsHitTestVisible="False"
IsChecked="{Binding RequirementMet}">
<ToggleButton.Content>
<Border Background="#E74C4C" Width="32" Height="32">
<materialDesign:PackIcon Kind="Close" VerticalAlignment="Center" HorizontalAlignment="Center" />
</Border>
</ToggleButton.Content>
<materialDesign:ToggleButtonAssist.OnContent>
<materialDesign:PackIcon Kind="Check" />
</materialDesign:ToggleButtonAssist.OnContent>
</ToggleButton>
</StackPanel>
</Grid>
</UserControl>

View File

@ -1,54 +0,0 @@
<UserControl x:Class="Artemis.UI.Screens.Modules.Tabs.ActivationRequirementsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:local="clr-namespace:Artemis.UI.Screens.Modules.Tabs"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:ActivationRequirementsViewModel}">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="15" MaxWidth="800">
<!-- General settings -->
<TextBlock Style="{StaticResource MaterialDesignHeadline5TextBlock}" Margin="0 15">Activation requirements</TextBlock>
<TextBlock Margin="0 0 0 15" TextWrapping="Wrap" Style="{StaticResource MaterialDesignTextBlock}">
This module has built-in activation requirements and won't activate until
<Run Text="{Binding ActivationType}" FontWeight="Medium" Foreground="{StaticResource SecondaryHueMidBrush}" />. <LineBreak />
These requirements allow the module creator to decide when the module is activated and you cannot override them.
</TextBlock>
<TextBlock Style="{StaticResource MaterialDesignTextBlock}"
Foreground="{DynamicResource MaterialDesignNavigationItemSubheader}"
TextWrapping="Wrap">
Note: While you have the profile editor open the module is always activated and any other modules are deactivated.
</TextBlock>
<materialDesign:Card materialDesign:ShadowAssist.ShadowDepth="Depth1" VerticalAlignment="Stretch" Margin="0,0,5,0">
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<Separator Margin="0 -10">
<Separator.Style>
<Style TargetType="Separator" BasedOn="{StaticResource MaterialDesignSeparator}">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource PreviousData}}" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</Separator.Style>
</Separator>
<ContentControl s:View.Model="{Binding}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" IsTabStop="False" Margin="20" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</materialDesign:Card>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@ -1,36 +0,0 @@
using System.Linq;
using Artemis.Core.Modules;
using Artemis.UI.Ninject.Factories;
using Stylet;
namespace Artemis.UI.Screens.Modules.Tabs
{
public class ActivationRequirementsViewModel : Conductor<ActivationRequirementViewModel>.Collection.AllActive
{
private readonly IModuleVmFactory _moduleVmFactory;
public ActivationRequirementsViewModel(Module module, IModuleVmFactory moduleVmFactory)
{
_moduleVmFactory = moduleVmFactory;
DisplayName = "ACTIVATION REQUIREMENTS";
Module = module;
ActivationType = Module.ActivationRequirementMode == ActivationRequirementType.All
? "all requirements are met"
: "any requirement is met";
}
public Module Module { get; }
public string ActivationType { get; set; }
protected override void OnActivate()
{
Items.Clear();
Items.AddRange(Module.ActivationRequirements.Select(_moduleVmFactory.CreateActivationRequirementViewModel));
base.OnActivate();
}
}
}

View File

@ -1,20 +0,0 @@
<UserControl x:Class="Artemis.UI.Screens.News.NewsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Top" Margin="16">
<materialDesign:PackIcon Kind="Crane" Width="250" Height="250" HorizontalAlignment="Center" />
<TextBlock Style="{StaticResource MaterialDesignHeadline4TextBlock}" TextWrapping="Wrap" HorizontalAlignment="Center" Margin="0 25">
News is not yet implemented
</TextBlock>
<TextBlock Style="{StaticResource MaterialDesignBody1TextBlock}" TextWrapping="Wrap" HorizontalAlignment="Center" Margin="0 25">
The news page will keep you up-to-date with the latest developments in the Artemis community. <LineBreak />
You'll find the latest patch notes here and see featured workshop contributions.<LineBreak /><LineBreak />
</TextBlock>
</StackPanel>
</UserControl>

View File

@ -1,12 +0,0 @@
using Stylet;
namespace Artemis.UI.Screens.News
{
public class NewsViewModel : Screen, IMainScreenViewModel
{
public NewsViewModel()
{
DisplayName = "News";
}
}
}

View File

@ -4,6 +4,7 @@ using System.Linq;
using System.Windows; using System.Windows;
using System.Windows.Media; using System.Windows.Media;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Input; using Artemis.UI.Shared.Input;
@ -16,6 +17,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract
{ {
private readonly IConditionOperatorService _conditionOperatorService; private readonly IConditionOperatorService _conditionOperatorService;
private readonly IDataModelUIService _dataModelUIService; private readonly IDataModelUIService _dataModelUIService;
private readonly List<Module> _modules;
private readonly IProfileEditorService _profileEditorService; private readonly IProfileEditorService _profileEditorService;
private DataModelStaticViewModel _rightSideInputViewModel; private DataModelStaticViewModel _rightSideInputViewModel;
private DataModelDynamicViewModel _rightSideSelectionViewModel; private DataModelDynamicViewModel _rightSideSelectionViewModel;
@ -25,11 +27,13 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract
protected DataModelConditionPredicateViewModel( protected DataModelConditionPredicateViewModel(
DataModelConditionPredicate dataModelConditionPredicate, DataModelConditionPredicate dataModelConditionPredicate,
List<Module> modules,
IProfileEditorService profileEditorService, IProfileEditorService profileEditorService,
IDataModelUIService dataModelUIService, IDataModelUIService dataModelUIService,
IConditionOperatorService conditionOperatorService, IConditionOperatorService conditionOperatorService,
ISettingsService settingsService) : base(dataModelConditionPredicate) ISettingsService settingsService) : base(dataModelConditionPredicate)
{ {
_modules = modules;
_profileEditorService = profileEditorService; _profileEditorService = profileEditorService;
_dataModelUIService = dataModelUIService; _dataModelUIService = dataModelUIService;
_conditionOperatorService = conditionOperatorService; _conditionOperatorService = conditionOperatorService;
@ -44,6 +48,8 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract
public DataModelConditionPredicate DataModelConditionPredicate => (DataModelConditionPredicate) Model; public DataModelConditionPredicate DataModelConditionPredicate => (DataModelConditionPredicate) Model;
public PluginSetting<bool> ShowDataModelValues { get; } public PluginSetting<bool> ShowDataModelValues { get; }
public bool CanSelectOperator => DataModelConditionPredicate.LeftPath is {IsValid: true};
public BaseConditionOperator SelectedOperator public BaseConditionOperator SelectedOperator
{ {
get => _selectedOperator; get => _selectedOperator;
@ -75,12 +81,12 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract
public override void Delete() public override void Delete()
{ {
base.Delete(); base.Delete();
_profileEditorService.UpdateSelectedProfileElement(); _profileEditorService.SaveSelectedProfileElement();
} }
public virtual void Initialize() public virtual void Initialize()
{ {
LeftSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.GetCurrentModule()); LeftSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_modules);
LeftSideSelectionViewModel.PropertySelected += LeftSideOnPropertySelected; LeftSideSelectionViewModel.PropertySelected += LeftSideOnPropertySelected;
if (LeftSideColor != null) if (LeftSideColor != null)
LeftSideSelectionViewModel.ButtonBrush = LeftSideColor; LeftSideSelectionViewModel.ButtonBrush = LeftSideColor;
@ -107,10 +113,11 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract
else if (!Operators.Contains(DataModelConditionPredicate.Operator)) else if (!Operators.Contains(DataModelConditionPredicate.Operator))
DataModelConditionPredicate.UpdateOperator(Operators.FirstOrDefault(o => o.Description == DataModelConditionPredicate.Operator.Description) ?? Operators.FirstOrDefault()); DataModelConditionPredicate.UpdateOperator(Operators.FirstOrDefault(o => o.Description == DataModelConditionPredicate.Operator.Description) ?? Operators.FirstOrDefault());
NotifyOfPropertyChange(nameof(CanSelectOperator));
SelectedOperator = DataModelConditionPredicate.Operator; SelectedOperator = DataModelConditionPredicate.Operator;
// Without a selected operator or one that supports a right side, leave the right side input empty // Without a selected operator or one that supports a right side, leave the right side input empty
if (SelectedOperator == null || SelectedOperator.RightSideType == null) if (SelectedOperator?.RightSideType == null)
{ {
DisposeRightSideStaticViewModel(); DisposeRightSideStaticViewModel();
DisposeRightSideDynamicViewModel(); DisposeRightSideDynamicViewModel();
@ -149,7 +156,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract
return; return;
DataModelConditionPredicate.UpdateLeftSide(LeftSideSelectionViewModel.DataModelPath); DataModelConditionPredicate.UpdateLeftSide(LeftSideSelectionViewModel.DataModelPath);
_profileEditorService.UpdateSelectedProfileElement(); _profileEditorService.SaveSelectedProfileElement();
SelectedOperator = DataModelConditionPredicate.Operator; SelectedOperator = DataModelConditionPredicate.Operator;
Update(); Update();
@ -158,7 +165,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract
public void ApplyRightSideDynamic() public void ApplyRightSideDynamic()
{ {
DataModelConditionPredicate.UpdateRightSideDynamic(RightSideSelectionViewModel.DataModelPath); DataModelConditionPredicate.UpdateRightSideDynamic(RightSideSelectionViewModel.DataModelPath);
_profileEditorService.UpdateSelectedProfileElement(); _profileEditorService.SaveSelectedProfileElement();
Update(); Update();
} }
@ -166,7 +173,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract
public void ApplyRightSideStatic(object value) public void ApplyRightSideStatic(object value)
{ {
DataModelConditionPredicate.UpdateRightSideStatic(value); DataModelConditionPredicate.UpdateRightSideStatic(value);
_profileEditorService.UpdateSelectedProfileElement(); _profileEditorService.SaveSelectedProfileElement();
Update(); Update();
} }
@ -174,7 +181,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract
public void ApplyOperator() public void ApplyOperator()
{ {
DataModelConditionPredicate.UpdateOperator(SelectedOperator); DataModelConditionPredicate.UpdateOperator(SelectedOperator);
_profileEditorService.UpdateSelectedProfileElement(); _profileEditorService.SaveSelectedProfileElement();
Update(); Update();
} }
@ -196,6 +203,26 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract
ApplyOperator(); ApplyOperator();
} }
public override void UpdateModules()
{
if (LeftSideSelectionViewModel != null)
{
LeftSideSelectionViewModel.PropertySelected -= LeftSideOnPropertySelected;
LeftSideSelectionViewModel.Dispose();
LeftSideSelectionViewModel = null;
}
DisposeRightSideStaticViewModel();
DisposeRightSideDynamicViewModel();
// If the modules changed the paths may no longer be valid if they targeted a module no longer available, in that case clear the path
if (DataModelConditionPredicate.LeftPath?.Target != null && !DataModelConditionPredicate.LeftPath.Target.IsExpansion && !_modules.Contains(DataModelConditionPredicate.LeftPath.Target.Module))
DataModelConditionPredicate.UpdateLeftSide(null);
if (DataModelConditionPredicate.RightPath?.Target != null && !DataModelConditionPredicate.RightPath.Target.IsExpansion && !_modules.Contains(DataModelConditionPredicate.RightPath.Target.Module))
DataModelConditionPredicate.UpdateRightSideDynamic(null);
Initialize();
}
#region IDisposable #region IDisposable
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)
@ -227,7 +254,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract
private void CreateRightSideSelectionViewModel() private void CreateRightSideSelectionViewModel()
{ {
RightSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.GetCurrentModule()); RightSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_modules);
RightSideSelectionViewModel.ButtonBrush = (SolidColorBrush) Application.Current.FindResource("PrimaryHueMidBrush"); RightSideSelectionViewModel.ButtonBrush = (SolidColorBrush) Application.Current.FindResource("PrimaryHueMidBrush");
RightSideSelectionViewModel.DisplaySwitchButton = true; RightSideSelectionViewModel.DisplaySwitchButton = true;
RightSideSelectionViewModel.PropertySelected += RightSideOnPropertySelected; RightSideSelectionViewModel.PropertySelected += RightSideOnPropertySelected;

View File

@ -63,5 +63,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract
groupViewModel.ConvertToPredicate(this); groupViewModel.ConvertToPredicate(this);
return true; return true;
} }
public abstract void UpdateModules();
} }
} }

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Windows.Media; using System.Windows.Media;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract; using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract;
using Artemis.UI.Shared; using Artemis.UI.Shared;
@ -14,15 +15,18 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
{ {
private readonly IDataModelConditionsVmFactory _dataModelConditionsVmFactory; private readonly IDataModelConditionsVmFactory _dataModelConditionsVmFactory;
private readonly IDataModelUIService _dataModelUIService; private readonly IDataModelUIService _dataModelUIService;
private readonly List<Module> _modules;
private readonly IProfileEditorService _profileEditorService; private readonly IProfileEditorService _profileEditorService;
private DateTime _lastTrigger; private DateTime _lastTrigger;
private string _triggerPastParticiple; private string _triggerPastParticiple;
public DataModelConditionEventViewModel(DataModelConditionEvent dataModelConditionEvent, public DataModelConditionEventViewModel(DataModelConditionEvent dataModelConditionEvent,
List<Module> modules,
IProfileEditorService profileEditorService, IProfileEditorService profileEditorService,
IDataModelUIService dataModelUIService, IDataModelUIService dataModelUIService,
IDataModelConditionsVmFactory dataModelConditionsVmFactory) : base(dataModelConditionEvent) IDataModelConditionsVmFactory dataModelConditionsVmFactory) : base(dataModelConditionEvent)
{ {
_modules = modules;
_profileEditorService = profileEditorService; _profileEditorService = profileEditorService;
_dataModelUIService = dataModelUIService; _dataModelUIService = dataModelUIService;
_dataModelConditionsVmFactory = dataModelConditionsVmFactory; _dataModelConditionsVmFactory = dataModelConditionsVmFactory;
@ -46,7 +50,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
public void Initialize() public void Initialize()
{ {
LeftSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.GetCurrentModule()); LeftSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_modules);
LeftSideSelectionViewModel.PropertySelected += LeftSideSelectionViewModelOnPropertySelected; LeftSideSelectionViewModel.PropertySelected += LeftSideSelectionViewModelOnPropertySelected;
LeftSideSelectionViewModel.LoadEventChildren = false; LeftSideSelectionViewModel.LoadEventChildren = false;
@ -82,7 +86,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
if (!(childModel is DataModelConditionGroup dataModelConditionGroup)) if (!(childModel is DataModelConditionGroup dataModelConditionGroup))
continue; continue;
DataModelConditionGroupViewModel viewModel = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(dataModelConditionGroup, ConditionGroupType.Event); DataModelConditionGroupViewModel viewModel = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(dataModelConditionGroup, ConditionGroupType.Event, _modules);
viewModel.IsRootGroup = true; viewModel.IsRootGroup = true;
viewModels.Add(viewModel); viewModels.Add(viewModel);
} }
@ -103,11 +107,18 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
public void ApplyEvent() public void ApplyEvent()
{ {
DataModelConditionEvent.UpdateEvent(LeftSideSelectionViewModel.DataModelPath); DataModelConditionEvent.UpdateEvent(LeftSideSelectionViewModel.DataModelPath);
_profileEditorService.UpdateSelectedProfileElement(); _profileEditorService.SaveSelectedProfileElement();
Update(); Update();
} }
public override void UpdateModules()
{
LeftSideSelectionViewModel.Dispose();
LeftSideSelectionViewModel.PropertySelected -= LeftSideSelectionViewModelOnPropertySelected;
Initialize();
}
protected override void OnInitialActivate() protected override void OnInitialActivate()
{ {
Initialize(); Initialize();

View File

@ -2,6 +2,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.Core.Services;
using Artemis.UI.Extensions; using Artemis.UI.Extensions;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract; using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract;
@ -14,17 +16,23 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
public class DataModelConditionGroupViewModel : DataModelConditionViewModel public class DataModelConditionGroupViewModel : DataModelConditionViewModel
{ {
private readonly IDataModelConditionsVmFactory _dataModelConditionsVmFactory; private readonly IDataModelConditionsVmFactory _dataModelConditionsVmFactory;
private readonly List<Module> _modules;
private readonly ICoreService _coreService;
private readonly IProfileEditorService _profileEditorService; private readonly IProfileEditorService _profileEditorService;
private bool _isEventGroup; private bool _isEventGroup;
private bool _isRootGroup; private bool _isRootGroup;
public DataModelConditionGroupViewModel(DataModelConditionGroup dataModelConditionGroup, public DataModelConditionGroupViewModel(DataModelConditionGroup dataModelConditionGroup,
ConditionGroupType groupType, ConditionGroupType groupType,
List<Module> modules,
ICoreService coreService,
IProfileEditorService profileEditorService, IProfileEditorService profileEditorService,
IDataModelConditionsVmFactory dataModelConditionsVmFactory) IDataModelConditionsVmFactory dataModelConditionsVmFactory)
: base(dataModelConditionGroup) : base(dataModelConditionGroup)
{ {
GroupType = groupType; GroupType = groupType;
_modules = modules;
_coreService = coreService;
_profileEditorService = profileEditorService; _profileEditorService = profileEditorService;
_dataModelConditionsVmFactory = dataModelConditionsVmFactory; _dataModelConditionsVmFactory = dataModelConditionsVmFactory;
@ -66,7 +74,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
DataModelConditionGroup.BooleanOperator = enumValue; DataModelConditionGroup.BooleanOperator = enumValue;
NotifyOfPropertyChange(nameof(SelectedBooleanOperator)); NotifyOfPropertyChange(nameof(SelectedBooleanOperator));
_profileEditorService.UpdateSelectedProfileElement(); _profileEditorService.SaveSelectedProfileElement();
} }
public void AddCondition() public void AddCondition()
@ -87,7 +95,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
} }
Update(); Update();
_profileEditorService.UpdateSelectedProfileElement(); _profileEditorService.SaveSelectedProfileElement();
} }
public void AddEventCondition() public void AddEventCondition()
@ -104,7 +112,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
DataModelConditionGroup.AddChild(new DataModelConditionEvent(DataModelConditionGroup), index); DataModelConditionGroup.AddChild(new DataModelConditionEvent(DataModelConditionGroup), index);
Update(); Update();
_profileEditorService.UpdateSelectedProfileElement(); _profileEditorService.SaveSelectedProfileElement();
} }
public void AddGroup() public void AddGroup()
@ -112,7 +120,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
DataModelConditionGroup.AddChild(new DataModelConditionGroup(DataModelConditionGroup)); DataModelConditionGroup.AddChild(new DataModelConditionGroup(DataModelConditionGroup));
Update(); Update();
_profileEditorService.UpdateSelectedProfileElement(); _profileEditorService.SaveSelectedProfileElement();
} }
public override void Update() public override void Update()
@ -131,22 +139,22 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
switch (childModel) switch (childModel)
{ {
case DataModelConditionGroup dataModelConditionGroup: case DataModelConditionGroup dataModelConditionGroup:
Items.Add(_dataModelConditionsVmFactory.DataModelConditionGroupViewModel(dataModelConditionGroup, GroupType)); Items.Add(_dataModelConditionsVmFactory.DataModelConditionGroupViewModel(dataModelConditionGroup, GroupType, _modules));
break; break;
case DataModelConditionList dataModelConditionList: case DataModelConditionList dataModelConditionList:
Items.Add(_dataModelConditionsVmFactory.DataModelConditionListViewModel(dataModelConditionList)); Items.Add(_dataModelConditionsVmFactory.DataModelConditionListViewModel(dataModelConditionList, _modules));
break; break;
case DataModelConditionEvent dataModelConditionEvent: case DataModelConditionEvent dataModelConditionEvent:
Items.Add(_dataModelConditionsVmFactory.DataModelConditionEventViewModel(dataModelConditionEvent)); Items.Add(_dataModelConditionsVmFactory.DataModelConditionEventViewModel(dataModelConditionEvent, _modules));
break; break;
case DataModelConditionGeneralPredicate dataModelConditionGeneralPredicate: case DataModelConditionGeneralPredicate dataModelConditionGeneralPredicate:
Items.Add(_dataModelConditionsVmFactory.DataModelConditionGeneralPredicateViewModel(dataModelConditionGeneralPredicate)); Items.Add(_dataModelConditionsVmFactory.DataModelConditionGeneralPredicateViewModel(dataModelConditionGeneralPredicate, _modules));
break; break;
case DataModelConditionListPredicate dataModelConditionListPredicate: case DataModelConditionListPredicate dataModelConditionListPredicate:
Items.Add(_dataModelConditionsVmFactory.DataModelConditionListPredicateViewModel(dataModelConditionListPredicate)); Items.Add(_dataModelConditionsVmFactory.DataModelConditionListPredicateViewModel(dataModelConditionListPredicate, _modules));
break; break;
case DataModelConditionEventPredicate dataModelConditionEventPredicate: case DataModelConditionEventPredicate dataModelConditionEventPredicate:
Items.Add(_dataModelConditionsVmFactory.DataModelConditionEventPredicateViewModel(dataModelConditionEventPredicate)); Items.Add(_dataModelConditionsVmFactory.DataModelConditionEventPredicateViewModel(dataModelConditionEventPredicate, _modules));
break; break;
} }
} }
@ -173,6 +181,12 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
dataModelConditionViewModel.Evaluate(); dataModelConditionViewModel.Evaluate();
} }
public override void UpdateModules()
{
foreach (DataModelConditionViewModel dataModelConditionViewModel in Items)
dataModelConditionViewModel.UpdateModules();
}
public void ConvertToConditionList(DataModelConditionViewModel predicateViewModel) public void ConvertToConditionList(DataModelConditionViewModel predicateViewModel)
{ {
// Store the old index and remove the old predicate // Store the old index and remove the old predicate
@ -203,8 +217,33 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
Update(); Update();
} }
private void CoreServiceOnFrameRendered(object? sender, FrameRenderedEventArgs e)
{
if (IsRootGroup)
Evaluate();
}
public event EventHandler Updated; public event EventHandler Updated;
#region Overrides of Screen
/// <inheritdoc />
protected override void OnInitialActivate()
{
base.OnInitialActivate();
Update();
_coreService.FrameRendered += CoreServiceOnFrameRendered;
}
/// <inheritdoc />
protected override void OnClose()
{
_coreService.FrameRendered -= CoreServiceOnFrameRendered;
base.OnClose();
}
#endregion
protected virtual void OnUpdated() protected virtual void OnUpdated()
{ {
Updated?.Invoke(this, EventArgs.Empty); Updated?.Invoke(this, EventArgs.Empty);

View File

@ -1,9 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Windows.Forms.VisualStyles;
using System.Windows.Media; using System.Windows.Media;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract; using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract;
using Artemis.UI.Shared; using Artemis.UI.Shared;
@ -16,14 +16,16 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
{ {
private readonly IDataModelConditionsVmFactory _dataModelConditionsVmFactory; private readonly IDataModelConditionsVmFactory _dataModelConditionsVmFactory;
private readonly IDataModelUIService _dataModelUIService; private readonly IDataModelUIService _dataModelUIService;
private readonly List<Module> _modules;
private readonly IProfileEditorService _profileEditorService; private readonly IProfileEditorService _profileEditorService;
public DataModelConditionListViewModel( public DataModelConditionListViewModel(DataModelConditionList dataModelConditionList,
DataModelConditionList dataModelConditionList, List<Module> modules,
IProfileEditorService profileEditorService, IProfileEditorService profileEditorService,
IDataModelUIService dataModelUIService, IDataModelUIService dataModelUIService,
IDataModelConditionsVmFactory dataModelConditionsVmFactory) : base(dataModelConditionList) IDataModelConditionsVmFactory dataModelConditionsVmFactory) : base(dataModelConditionList)
{ {
_modules = modules;
_profileEditorService = profileEditorService; _profileEditorService = profileEditorService;
_dataModelUIService = dataModelUIService; _dataModelUIService = dataModelUIService;
_dataModelConditionsVmFactory = dataModelConditionsVmFactory; _dataModelConditionsVmFactory = dataModelConditionsVmFactory;
@ -39,7 +41,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
DataModelConditionList.ListOperator = enumValue; DataModelConditionList.ListOperator = enumValue;
NotifyOfPropertyChange(nameof(SelectedListOperator)); NotifyOfPropertyChange(nameof(SelectedListOperator));
_profileEditorService.UpdateSelectedProfileElement(); _profileEditorService.SaveSelectedProfileElement();
} }
public void AddCondition() public void AddCondition()
@ -47,7 +49,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
DataModelConditionList.AddChild(new DataModelConditionGeneralPredicate(DataModelConditionList, ProfileRightSideType.Dynamic)); DataModelConditionList.AddChild(new DataModelConditionGeneralPredicate(DataModelConditionList, ProfileRightSideType.Dynamic));
Update(); Update();
_profileEditorService.UpdateSelectedProfileElement(); _profileEditorService.SaveSelectedProfileElement();
} }
public void AddGroup() public void AddGroup()
@ -55,7 +57,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
DataModelConditionList.AddChild(new DataModelConditionGroup(DataModelConditionList)); DataModelConditionList.AddChild(new DataModelConditionGroup(DataModelConditionList));
Update(); Update();
_profileEditorService.UpdateSelectedProfileElement(); _profileEditorService.SaveSelectedProfileElement();
} }
public override void Evaluate() public override void Evaluate()
@ -68,12 +70,19 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
public override void Delete() public override void Delete()
{ {
base.Delete(); base.Delete();
_profileEditorService.UpdateSelectedProfileElement(); _profileEditorService.SaveSelectedProfileElement();
}
/// <inheritdoc />
public override void UpdateModules()
{
foreach (DataModelConditionViewModel dataModelConditionViewModel in Items)
dataModelConditionViewModel.UpdateModules();
} }
public void Initialize() public void Initialize()
{ {
LeftSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_profileEditorService.GetCurrentModule()); LeftSideSelectionViewModel = _dataModelUIService.GetDynamicSelectionViewModel(_modules);
LeftSideSelectionViewModel.PropertySelected += LeftSideSelectionViewModelOnPropertySelected; LeftSideSelectionViewModel.PropertySelected += LeftSideSelectionViewModelOnPropertySelected;
IReadOnlyCollection<DataModelVisualizationRegistration> editors = _dataModelUIService.RegisteredDataModelEditors; IReadOnlyCollection<DataModelVisualizationRegistration> editors = _dataModelUIService.RegisteredDataModelEditors;
@ -96,7 +105,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
return; return;
DataModelConditionList.UpdateList(LeftSideSelectionViewModel.DataModelPath); DataModelConditionList.UpdateList(LeftSideSelectionViewModel.DataModelPath);
_profileEditorService.UpdateSelectedProfileElement(); _profileEditorService.SaveSelectedProfileElement();
Update(); Update();
} }
@ -120,7 +129,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
if (!(childModel is DataModelConditionGroup dataModelConditionGroup)) if (!(childModel is DataModelConditionGroup dataModelConditionGroup))
continue; continue;
DataModelConditionGroupViewModel viewModel = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(dataModelConditionGroup, ConditionGroupType.List); DataModelConditionGroupViewModel viewModel = _dataModelConditionsVmFactory.DataModelConditionGroupViewModel(dataModelConditionGroup, ConditionGroupType.List, _modules);
viewModel.IsRootGroup = true; viewModel.IsRootGroup = true;
viewModels.Add(viewModel); viewModels.Add(viewModel);
} }

View File

@ -55,6 +55,7 @@
Background="#7B7B7B" Background="#7B7B7B"
BorderBrush="#7B7B7B" BorderBrush="#7B7B7B"
Content="{Binding SelectedOperator.Description}" Content="{Binding SelectedOperator.Description}"
IsEnabled="{Binding CanSelectOperator}"
Click="PropertyButton_OnClick"> Click="PropertyButton_OnClick">
<Button.ContextMenu> <Button.ContextMenu>
<ContextMenu ItemsSource="{Binding Operators}"> <ContextMenu ItemsSource="{Binding Operators}">

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Windows.Media; using System.Windows.Media;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Extensions; using Artemis.UI.Extensions;
using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract; using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract;
@ -16,11 +17,12 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
private readonly IDataModelUIService _dataModelUIService; private readonly IDataModelUIService _dataModelUIService;
public DataModelConditionEventPredicateViewModel(DataModelConditionEventPredicate dataModelConditionEventPredicate, public DataModelConditionEventPredicateViewModel(DataModelConditionEventPredicate dataModelConditionEventPredicate,
List<Module> modules,
IProfileEditorService profileEditorService, IProfileEditorService profileEditorService,
IDataModelUIService dataModelUIService, IDataModelUIService dataModelUIService,
IConditionOperatorService conditionOperatorService, IConditionOperatorService conditionOperatorService,
ISettingsService settingsService) ISettingsService settingsService)
: base(dataModelConditionEventPredicate, profileEditorService, dataModelUIService, conditionOperatorService, settingsService) : base(dataModelConditionEventPredicate, modules, profileEditorService, dataModelUIService, conditionOperatorService, settingsService)
{ {
_dataModelUIService = dataModelUIService; _dataModelUIService = dataModelUIService;

View File

@ -56,6 +56,7 @@
Background="#7B7B7B" Background="#7B7B7B"
BorderBrush="#7B7B7B" BorderBrush="#7B7B7B"
Content="{Binding SelectedOperator.Description}" Content="{Binding SelectedOperator.Description}"
IsEnabled="{Binding CanSelectOperator}"
Click="PropertyButton_OnClick"> Click="PropertyButton_OnClick">
<Button.ContextMenu> <Button.ContextMenu>
<ContextMenu ItemsSource="{Binding Operators}"> <ContextMenu ItemsSource="{Binding Operators}">

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract; using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract;
using Artemis.UI.Shared; using Artemis.UI.Shared;
@ -14,11 +15,12 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
private readonly IDataModelUIService _dataModelUIService; private readonly IDataModelUIService _dataModelUIService;
public DataModelConditionGeneralPredicateViewModel(DataModelConditionGeneralPredicate dataModelConditionGeneralPredicate, public DataModelConditionGeneralPredicateViewModel(DataModelConditionGeneralPredicate dataModelConditionGeneralPredicate,
List<Module> modules,
IProfileEditorService profileEditorService, IProfileEditorService profileEditorService,
IDataModelUIService dataModelUIService, IDataModelUIService dataModelUIService,
IConditionOperatorService conditionOperatorService, IConditionOperatorService conditionOperatorService,
ISettingsService settingsService) ISettingsService settingsService)
: base(dataModelConditionGeneralPredicate, profileEditorService, dataModelUIService, conditionOperatorService, settingsService) : base(dataModelConditionGeneralPredicate, modules, profileEditorService, dataModelUIService, conditionOperatorService, settingsService)
{ {
_dataModelUIService = dataModelUIService; _dataModelUIService = dataModelUIService;
} }

View File

@ -55,6 +55,7 @@
Background="#7B7B7B" Background="#7B7B7B"
BorderBrush="#7B7B7B" BorderBrush="#7B7B7B"
Content="{Binding SelectedOperator.Description}" Content="{Binding SelectedOperator.Description}"
IsEnabled="{Binding CanSelectOperator}"
Click="PropertyButton_OnClick"> Click="PropertyButton_OnClick">
<Button.ContextMenu> <Button.ContextMenu>
<ContextMenu ItemsSource="{Binding Operators}"> <ContextMenu ItemsSource="{Binding Operators}">

View File

@ -3,8 +3,8 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Windows.Media; using System.Windows.Media;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Extensions;
using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract; using Artemis.UI.Screens.ProfileEditor.Conditions.Abstract;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
@ -16,11 +16,12 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
private readonly IDataModelUIService _dataModelUIService; private readonly IDataModelUIService _dataModelUIService;
public DataModelConditionListPredicateViewModel(DataModelConditionListPredicate dataModelConditionListPredicate, public DataModelConditionListPredicateViewModel(DataModelConditionListPredicate dataModelConditionListPredicate,
List<Module> modules,
IProfileEditorService profileEditorService, IProfileEditorService profileEditorService,
IDataModelUIService dataModelUIService, IDataModelUIService dataModelUIService,
IConditionOperatorService conditionOperatorService, IConditionOperatorService conditionOperatorService,
ISettingsService settingsService) ISettingsService settingsService)
: base(dataModelConditionListPredicate, profileEditorService, dataModelUIService, conditionOperatorService, settingsService) : base(dataModelConditionListPredicate, modules, profileEditorService, dataModelUIService, conditionOperatorService, settingsService)
{ {
_dataModelUIService = dataModelUIService; _dataModelUIService = dataModelUIService;
@ -44,6 +45,17 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
} }
} }
public override void Evaluate()
{
throw new NotImplementedException();
}
public override void UpdateModules()
{
foreach (DataModelConditionViewModel dataModelConditionViewModel in Items)
dataModelConditionViewModel.UpdateModules();
}
protected override void OnInitialActivate() protected override void OnInitialActivate()
{ {
base.OnInitialActivate(); base.OnInitialActivate();
@ -81,10 +93,5 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions
return wrapper.CreateViewModel(_dataModelUIService, new DataModelUpdateConfiguration(true)); return wrapper.CreateViewModel(_dataModelUIService, new DataModelUpdateConfiguration(true));
} }
public override void Evaluate()
{
throw new NotImplementedException();
}
} }
} }

View File

@ -1,30 +0,0 @@
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.Dialogs.ProfileCreateView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="213.053" d:DesignWidth="254.425">
<StackPanel Margin="16">
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}">
Add a new profile
</TextBlock>
<TextBox materialDesign:HintAssist.Hint="Profile name"
Margin="0 8 0 16"
Width="300"
Style="{StaticResource MaterialDesignFilledTextBox}"
Text="{Binding ProfileName, UpdateSourceTrigger=PropertyChanged}" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0 8 0 0">
<Button Style="{StaticResource MaterialDesignFlatButton}" IsCancel="True" Margin="0 0 8 0" Command="{s:Action Cancel}">
CANCEL
</Button>
<Button Style="{StaticResource MaterialDesignFlatButton}" IsDefault="True" Margin="0 0 0 0" Command="{s:Action Accept}">
ACCEPT
</Button>
</StackPanel>
</StackPanel>
</UserControl>

View File

@ -1,40 +0,0 @@
using System.Threading.Tasks;
using Artemis.UI.Shared.Services;
using FluentValidation;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.Dialogs
{
public class ProfileCreateViewModel : DialogViewModelBase
{
private string _profileName;
public ProfileCreateViewModel(IModelValidator<ProfileCreateViewModel> validator) : base(validator)
{
}
public string ProfileName
{
get => _profileName;
set => SetAndNotify(ref _profileName, value);
}
public async Task Accept()
{
await ValidateAsync();
if (HasErrors)
return;
Session.Close(ProfileName);
}
}
public class ProfileCreateViewModelValidator : AbstractValidator<ProfileCreateViewModel>
{
public ProfileCreateViewModelValidator()
{
RuleFor(m => m.ProfileName).NotEmpty().WithMessage("Profile name may not be empty");
}
}
}

View File

@ -1,42 +0,0 @@
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Shared.Services;
using FluentValidation;
using Stylet;
namespace Artemis.UI.Screens.ProfileEditor.Dialogs
{
public class ProfileEditViewModel : DialogViewModelBase
{
private string _profileName;
public ProfileEditViewModel(IModelValidator<ProfileEditViewModel> validator, Profile profile) : base(validator)
{
ProfileName = profile.Name;
}
public string ProfileName
{
get => _profileName;
set => SetAndNotify(ref _profileName, value);
}
public async Task Accept()
{
await ValidateAsync();
if (HasErrors)
return;
Session.Close(ProfileName);
}
}
public class ProfileEditViewModelValidator : AbstractValidator<ProfileEditViewModel>
{
public ProfileEditViewModelValidator()
{
RuleFor(m => m.ProfileName).NotEmpty().WithMessage("Profile name may not be empty");
}
}
}

View File

@ -1,44 +0,0 @@
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.Dialogs.ProfileExportView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid Margin="16" Width="500">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" Grid.Row="0">
Export current profile
</TextBlock>
<TextBlock Grid.Row="1"
Margin="0 10"
TextWrapping="Wrap"
Style="{StaticResource MaterialDesignSubtitle1TextBlock}"
Foreground="{DynamicResource MaterialDesignBodyLight}">
It looks like you have not set up any profile adaption hints. This means Artemis can't do much to make your profile look good on a different surface other than try finding the same LEDs as you have.
<LineBreak/><LineBreak/>
To configure adaption hints, right-click on a layer and choose <materialDesign:PackIcon Kind="AutoFix" /> <Run FontWeight="Bold">View Adaption Hints</Run>.
<LineBreak/><LineBreak/>
To learn more about profile adaption, check out
<Hyperlink Style="{StaticResource ArtemisHyperlink}" RequestNavigate="{s:Action OpenHyperlink}" NavigateUri="https://wiki.artemis-rgb.com/guides/user/profiles/layers/adaption-hints">
this wiki article
</Hyperlink>.
</TextBlock>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Bottom" Grid.Row="2">
<Button Style="{StaticResource MaterialDesignFlatButton}" IsCancel="True" Margin="0 8 8 0" Command="{s:Action Cancel}">
CANCEL
</Button>
<Button Style="{StaticResource MaterialDesignFlatButton}" IsDefault="True" Margin="0 8 0 0" Command="{s:Action Accept}">
EXPORT ANYWAY
</Button>
</StackPanel>
</Grid>
</UserControl>

View File

@ -1,49 +0,0 @@
using System.Windows;
using System.Windows.Navigation;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Shared.Services;
namespace Artemis.UI.Screens.ProfileEditor.Dialogs
{
public class ProfileExportViewModel : DialogViewModelBase
{
private readonly IProfileService _profileService;
private readonly IMessageService _messageService;
public ProfileExportViewModel(ProfileDescriptor profileDescriptor, IProfileService profileService, IMessageService messageService)
{
ProfileDescriptor = profileDescriptor;
_profileService = profileService;
_messageService = messageService;
}
public ProfileDescriptor ProfileDescriptor { get; }
#region Overrides of Screen
/// <inheritdoc />
protected override void OnActivate()
{
// TODO: If the profile has hints on all layers, call Accept
base.OnActivate();
}
#endregion
public void OpenHyperlink(object sender, RequestNavigateEventArgs e)
{
Core.Utilities.OpenUrl(e.Uri.AbsoluteUri);
}
public void Accept()
{
string encoded = _profileService.ExportProfile(ProfileDescriptor);
Clipboard.SetText(encoded);
_messageService.ShowMessage("Profile contents exported to clipboard.");
Session.Close();
}
}
}

View File

@ -1,61 +0,0 @@
<UserControl x:Class="Artemis.UI.Screens.ProfileEditor.Dialogs.ProfileImportView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:avalonedit="http://icsharpcode.net/sharpdevelop/avalonedit"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid Margin="16" Width="800">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" Grid.Row="0">
Import profile to current module
</TextBlock>
<TextBlock Grid.Row="1"
Margin="0 10"
TextWrapping="Wrap"
Style="{StaticResource MaterialDesignSubtitle1TextBlock}"
Foreground="{DynamicResource MaterialDesignBodyLight}">
Please note that importing profiles like this is placeholder functionality. The idea is that this will eventually happen via the workshop.
</TextBlock>
<TextBlock Grid.Row="2"
Margin="0 10"
TextWrapping="Wrap"
Style="{StaticResource MaterialDesignSubtitle1TextBlock}"
Foreground="{DynamicResource MaterialDesignBodyLight}">
The workshop will include tools to make profiles convert easily and look good on different layouts.
That means right now when you import this profile unless you have the exact same setup as
the person who exported it, you'll have to select LEDs for each layer in the profile.
</TextBlock>
<TextBox Grid.Row="3"
Text="{Binding ProfileJson}"
Style="{StaticResource MaterialDesignOutlinedTextBox}"
FontFamily="Consolas"
VerticalAlignment="Top"
Height="400"
AcceptsReturn="True"
VerticalScrollBarVisibility="Auto"
materialDesign:HintAssist.Hint="Paste profile JSON here"
Margin="16" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row="4">
<Button Style="{StaticResource MaterialDesignFlatButton}" IsCancel="True" Margin="0 8 8 0" Command="{s:Action Cancel}">
CANCEL
</Button>
<Button Style="{StaticResource MaterialDesignFlatButton}" IsDefault="True" Margin="0 8 0 0" Command="{s:Action Accept}">
I UNDERSTAND, IMPORT
</Button>
</StackPanel>
</Grid>
</UserControl>

Some files were not shown because too many files have changed in this diff Show More