diff --git a/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs index 387c2d9ef..e811a4e0f 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs @@ -1,4 +1,5 @@ -using System.ComponentModel; +using System.Collections.Specialized; +using System.ComponentModel; using SkiaSharp; namespace Artemis.Core @@ -23,17 +24,16 @@ namespace Artemis.Core if (CurrentValue == null) return; - for (int index = 0; index < CurrentValue.Stops.Count; index++) + for (int index = 0; index < CurrentValue.Count; index++) { int stopIndex = index; void Setter(SKColor value) { - CurrentValue.Stops[stopIndex].Color = value; - CurrentValue.OnColorValuesUpdated(); + CurrentValue[stopIndex].Color = value; } - RegisterDataBindingProperty(() => CurrentValue.Stops[stopIndex].Color, Setter, new ColorStopDataBindingConverter(), $"Color #{stopIndex + 1}"); + RegisterDataBindingProperty(() => CurrentValue[stopIndex].Color, Setter, new ColorStopDataBindingConverter(), $"Color #{stopIndex + 1}"); } } @@ -61,17 +61,17 @@ namespace Artemis.Core if (_subscribedGradient != BaseValue) { if (_subscribedGradient != null) - _subscribedGradient.PropertyChanged -= SubscribedGradientOnPropertyChanged; + _subscribedGradient.CollectionChanged -= SubscribedGradientOnPropertyChanged; _subscribedGradient = BaseValue; - _subscribedGradient.PropertyChanged += SubscribedGradientOnPropertyChanged; + _subscribedGradient.CollectionChanged += SubscribedGradientOnPropertyChanged; } CreateDataBindingRegistrations(); } - private void SubscribedGradientOnPropertyChanged(object? sender, PropertyChangedEventArgs e) + private void SubscribedGradientOnPropertyChanged(object? sender, NotifyCollectionChangedEventArgs args) { - if (CurrentValue.Stops.Count != GetAllDataBindingRegistrations().Count) + if (CurrentValue.Count != GetAllDataBindingRegistrations().Count) CreateDataBindingRegistrations(); } diff --git a/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs b/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs index 4f630fcda..c621eb5c7 100644 --- a/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs +++ b/src/Artemis.Core/Models/Profile/Colors/ColorGradient.cs @@ -1,5 +1,8 @@ using System; +using System.Collections; using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; using System.Linq; using SkiaSharp; @@ -8,7 +11,7 @@ namespace Artemis.Core /// /// A gradient containing a list of s /// - public class ColorGradient : CorePropertyChanged + public class ColorGradient : IList, INotifyCollectionChanged { private static readonly SKColor[] FastLedRainbow = { @@ -23,19 +26,16 @@ namespace Artemis.Core new(0xFFFF0000) // and back to Red }; + private readonly List _stops; + /// /// Creates a new instance of the class /// public ColorGradient() { - Stops = new List(); + _stops = new List(); } - /// - /// Gets a list of all the s in the gradient - /// - public List Stops { get; } - /// /// Gets all the colors in the color gradient /// @@ -49,13 +49,16 @@ namespace Artemis.Core { List result = new(); if (timesToRepeat == 0) - result = Stops.Select(c => c.Color).ToList(); + { + result = this.Select(c => c.Color).ToList(); + } else { - List colors = Stops.Select(c => c.Color).ToList(); + List colors = this.Select(c => c.Color).ToList(); for (int i = 0; i <= timesToRepeat; i++) result.AddRange(colors); } + if (seamless && !IsSeamless()) result.Add(result[0]); @@ -77,11 +80,13 @@ namespace Artemis.Core { List result = new(); if (timesToRepeat == 0) - result = Stops.Select(c => c.Position).ToList(); + { + result = this.Select(c => c.Position).ToList(); + } else { // Create stops and a list of divided stops - List stops = Stops.Select(c => c.Position / (timesToRepeat + 1)).ToList(); + List stops = this.Select(c => c.Position / (timesToRepeat + 1)).ToList(); // For each repeat cycle, add the base stops to the end result for (int i = 0; i <= timesToRepeat; i++) @@ -104,25 +109,16 @@ namespace Artemis.Core return result.ToArray(); } - /// - /// Triggers a property changed event of the collection - /// - public void OnColorValuesUpdated() - { - Stops.Sort((a, b) => a.Position.CompareTo(b.Position)); - OnPropertyChanged(nameof(Stops)); - } - /// /// Gets a color at any position between 0.0 and 1.0 using interpolation /// /// A position between 0.0 and 1.0 public SKColor GetColor(float position) { - if (!Stops.Any()) + if (!this.Any()) return SKColor.Empty; - ColorGradientStop[] stops = Stops.ToArray(); + ColorGradientStop[] stops = this.ToArray(); if (position <= 0) return stops[0].Color; if (position >= 1) return stops[^1].Color; ColorGradientStop left = stops[0]; @@ -160,7 +156,7 @@ namespace Artemis.Core { SKColor skColor = FastLedRainbow[index]; float position = 1f / (FastLedRainbow.Length - 1f) * index; - gradient.Stops.Add(new ColorGradientStop(skColor, position)); + gradient.Add(new ColorGradientStop(skColor, position)); } return gradient; @@ -172,7 +168,141 @@ namespace Artemis.Core /// if the gradient is seamless; otherwise public bool IsSeamless() { - return Stops.Count == 0 || Stops.First().Color.Equals(Stops.Last().Color); + return Count == 0 || this.First().Color.Equals(this.Last().Color); } + + internal void Sort() + { + _stops.Sort((a, b) => a.Position.CompareTo(b.Position)); + } + + private void ItemOnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + Sort(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + #region Implementation of IEnumerable + + /// + public IEnumerator GetEnumerator() + { + return _stops.GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + + #region Implementation of ICollection + + /// + public void Add(ColorGradientStop item) + { + _stops.Add(item); + item.PropertyChanged += ItemOnPropertyChanged; + Sort(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, _stops.IndexOf(item))); + } + + + /// + public void Clear() + { + foreach (ColorGradientStop item in _stops) + item.PropertyChanged -= ItemOnPropertyChanged; + _stops.Clear(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + /// + public bool Contains(ColorGradientStop item) + { + return _stops.Contains(item); + } + + /// + public void CopyTo(ColorGradientStop[] array, int arrayIndex) + { + _stops.CopyTo(array, arrayIndex); + } + + /// + public bool Remove(ColorGradientStop item) + { + item.PropertyChanged -= ItemOnPropertyChanged; + int index = _stops.IndexOf(item); + bool removed = _stops.Remove(item); + if (removed) + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index)); + + return removed; + } + + /// + public int Count => _stops.Count; + + /// + public bool IsReadOnly => false; + + #endregion + + #region Implementation of IList + + /// + public int IndexOf(ColorGradientStop item) + { + return _stops.IndexOf(item); + } + + /// + public void Insert(int index, ColorGradientStop item) + { + _stops.Insert(index, item); + item.PropertyChanged += ItemOnPropertyChanged; + Sort(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, _stops.IndexOf(item))); + } + + /// + public void RemoveAt(int index) + { + _stops[index].PropertyChanged -= ItemOnPropertyChanged; + _stops.RemoveAt(index); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, index)); + } + + /// + public ColorGradientStop this[int index] + { + get => _stops[index]; + set + { + ColorGradientStop? oldValue = _stops[index]; + oldValue.PropertyChanged -= ItemOnPropertyChanged; + _stops[index] = value; + _stops[index].PropertyChanged += ItemOnPropertyChanged; + Sort(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, value, oldValue)); + } + } + + #endregion + + #region Implementation of INotifyCollectionChanged + + /// + public event NotifyCollectionChangedEventHandler? CollectionChanged; + + private void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + CollectionChanged?.Invoke(this, e); + } + + #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 47a08ab8a..5e544e187 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -297,6 +297,9 @@ namespace Artemis.Core private void ApplyTimeline(Timeline timeline) { + if (timeline.Delta == TimeSpan.Zero) + return; + General.Update(timeline); Transform.Update(timeline); if (LayerBrush != null) diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs index c42b1f74c..c9bdf5872 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/ILayerProperty.cs @@ -33,6 +33,11 @@ namespace Artemis.Core /// Type PropertyType { get; } + /// + /// Indicates whether the BaseValue was loaded from storage, useful to check whether a default value must be applied + /// + bool IsLoadedFromStorage { get; } + /// /// Initializes the layer property /// @@ -54,6 +59,11 @@ namespace Artemis.Core /// If succeeded the resulting keyframe, otherwise ILayerPropertyKeyframe? AddKeyframeEntity(KeyframeEntity keyframeEntity); + /// + /// Overrides the property value with the default value + /// + void ApplyDefaultValue(); + /// /// Updates the layer properties internal state /// diff --git a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs index c35bc401d..afe35710d 100644 --- a/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs +++ b/src/Artemis.Core/Models/Profile/LayerProperties/LayerProperty.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Linq.Expressions; using Artemis.Storage.Entities.Profile; using Newtonsoft.Json; @@ -39,6 +38,107 @@ namespace Artemis.Core _keyframes = new List>(); } + /// + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _disposed = true; + + foreach (IDataBinding dataBinding in _dataBindings) + dataBinding.Dispose(); + + Disposed?.Invoke(this, EventArgs.Empty); + } + } + + /// + /// Invokes the event + /// + protected virtual void OnUpdated() + { + Updated?.Invoke(this, new LayerPropertyEventArgs(this)); + } + + /// + /// Invokes the event + /// + protected virtual void OnCurrentValueSet() + { + CurrentValueSet?.Invoke(this, new LayerPropertyEventArgs(this)); + LayerPropertyGroup.OnLayerPropertyOnCurrentValueSet(new LayerPropertyEventArgs(this)); + } + + /// + /// Invokes the event + /// + protected virtual void OnVisibilityChanged() + { + VisibilityChanged?.Invoke(this, new LayerPropertyEventArgs(this)); + } + + /// + /// Invokes the event + /// + protected virtual void OnKeyframesToggled() + { + KeyframesToggled?.Invoke(this, new LayerPropertyEventArgs(this)); + } + + /// + /// Invokes the event + /// + protected virtual void OnKeyframeAdded() + { + KeyframeAdded?.Invoke(this, new LayerPropertyEventArgs(this)); + } + + /// + /// Invokes the event + /// + protected virtual void OnKeyframeRemoved() + { + KeyframeRemoved?.Invoke(this, new LayerPropertyEventArgs(this)); + } + + /// + /// Invokes the event + /// + protected virtual void OnDataBindingPropertyRegistered() + { + DataBindingPropertyRegistered?.Invoke(this, new LayerPropertyEventArgs(this)); + } + + /// + /// Invokes the event + /// + protected virtual void OnDataBindingPropertiesCleared() + { + DataBindingPropertiesCleared?.Invoke(this, new LayerPropertyEventArgs(this)); + } + + /// + /// Invokes the event + /// + protected virtual void OnDataBindingEnabled(LayerPropertyEventArgs e) + { + DataBindingEnabled?.Invoke(this, e); + } + + /// + /// Invokes the event + /// + protected virtual void OnDataBindingDisabled(LayerPropertyEventArgs e) + { + DataBindingDisabled?.Invoke(this, e); + } + /// public PropertyDescriptionAttribute PropertyDescription { get; internal set; } @@ -62,28 +162,6 @@ namespace Artemis.Core OnUpdated(); } - #region IDisposable - - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - _disposed = true; - - foreach (IDataBinding dataBinding in _dataBindings) - dataBinding.Dispose(); - - Disposed?.Invoke(this, EventArgs.Empty); - } - } - /// public void Dispose() { @@ -91,7 +169,38 @@ namespace Artemis.Core GC.SuppressFinalize(this); } - #endregion + /// + public event EventHandler? Disposed; + + /// + public event EventHandler? Updated; + + /// + public event EventHandler? CurrentValueSet; + + /// + public event EventHandler? VisibilityChanged; + + /// + public event EventHandler? KeyframesToggled; + + /// + public event EventHandler? KeyframeAdded; + + /// + public event EventHandler? KeyframeRemoved; + + /// + public event EventHandler? DataBindingPropertyRegistered; + + /// + public event EventHandler? DataBindingPropertiesCleared; + + /// + public event EventHandler? DataBindingEnabled; + + /// + public event EventHandler? DataBindingDisabled; #region Hierarchy @@ -195,22 +304,25 @@ namespace Artemis.Core ReapplyUpdate(); } - /// - /// Overrides the property value with the default value - /// - public void ApplyDefaultValue(TimeSpan? time) + /// + public void ApplyDefaultValue() { if (_disposed) throw new ObjectDisposedException("LayerProperty"); string json = CoreJson.SerializeObject(DefaultValue, true); - SetCurrentValue(CoreJson.DeserializeObject(json)!, time); + KeyframesEnabled = false; + SetCurrentValue(CoreJson.DeserializeObject(json)!, null); } private void ReapplyUpdate() { - ProfileElement.Timeline.ClearDelta(); - Update(ProfileElement.Timeline); + // Create a timeline with the same position but a delta of zero + Timeline temporaryTimeline = new(); + temporaryTimeline.Override(ProfileElement.Timeline.Position, false); + temporaryTimeline.ClearDelta(); + + Update(temporaryTimeline); OnCurrentValueSet(); } @@ -440,10 +552,10 @@ namespace Artemis.Core if (_disposed) throw new ObjectDisposedException("LayerProperty"); - foreach (IDataBindingRegistration dataBindingRegistration in _dataBindingRegistrations) + foreach (IDataBindingRegistration dataBindingRegistration in _dataBindingRegistrations) dataBindingRegistration.ClearDataBinding(); _dataBindingRegistrations.Clear(); - + OnDataBindingPropertiesCleared(); } @@ -593,7 +705,7 @@ namespace Artemis.Core throw new ArtemisCoreException("Layer property is not yet initialized"); if (!IsLoadedFromStorage) - ApplyDefaultValue(null); + ApplyDefaultValue(); else try { @@ -657,123 +769,5 @@ namespace Artemis.Core } #endregion - - #region Events - - /// - public event EventHandler? Disposed; - - /// - public event EventHandler? Updated; - - /// - public event EventHandler? CurrentValueSet; - - /// - public event EventHandler? VisibilityChanged; - - /// - public event EventHandler? KeyframesToggled; - - /// - public event EventHandler? KeyframeAdded; - - /// - public event EventHandler? KeyframeRemoved; - - /// - public event EventHandler? DataBindingPropertyRegistered; - - /// - public event EventHandler? DataBindingPropertiesCleared; - - /// - public event EventHandler? DataBindingEnabled; - - /// - public event EventHandler? DataBindingDisabled; - - /// - /// Invokes the event - /// - protected virtual void OnUpdated() - { - Updated?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnCurrentValueSet() - { - CurrentValueSet?.Invoke(this, new LayerPropertyEventArgs(this)); - LayerPropertyGroup.OnLayerPropertyOnCurrentValueSet(new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnVisibilityChanged() - { - VisibilityChanged?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnKeyframesToggled() - { - KeyframesToggled?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnKeyframeAdded() - { - KeyframeAdded?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnKeyframeRemoved() - { - KeyframeRemoved?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnDataBindingPropertyRegistered() - { - DataBindingPropertyRegistered?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnDataBindingPropertiesCleared() - { - DataBindingPropertiesCleared?.Invoke(this, new LayerPropertyEventArgs(this)); - } - - /// - /// Invokes the event - /// - protected virtual void OnDataBindingEnabled(LayerPropertyEventArgs e) - { - DataBindingEnabled?.Invoke(this, e); - } - - /// - /// Invokes the event - /// - protected virtual void OnDataBindingDisabled(LayerPropertyEventArgs e) - { - DataBindingDisabled?.Invoke(this, e); - } - - #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs b/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs index dda5d03cb..7483733a9 100644 --- a/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs +++ b/src/Artemis.Core/Models/Profile/LayerPropertyGroup.cs @@ -121,6 +121,31 @@ namespace Artemis.Core return result.AsReadOnly(); } + /// + /// Applies the default value to all layer properties + /// + public void ResetAllLayerProperties() + { + foreach (ILayerProperty layerProperty in GetAllLayerProperties()) + layerProperty.ApplyDefaultValue(); + } + + /// + /// Occurs when the property group has initialized all its children + /// + public event EventHandler? PropertyGroupInitialized; + + /// + /// Occurs when one of the current value of one of the layer properties in this group changes by some form of input + /// Note: Will not trigger on properties in child groups + /// + public event EventHandler? LayerPropertyOnCurrentValueSet; + + /// + /// Occurs when the value of the layer property was updated + /// + public event EventHandler? VisibilityChanged; + /// /// Called before property group is activated to allow you to populate on /// the properties you want @@ -145,6 +170,27 @@ namespace Artemis.Core PropertyGroupInitialized?.Invoke(this, EventArgs.Empty); } + /// + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _disposed = true; + DisableProperties(); + + foreach (ILayerProperty layerProperty in _layerProperties) + layerProperty.Dispose(); + foreach (LayerPropertyGroup layerPropertyGroup in _layerPropertyGroups) + layerPropertyGroup.Dispose(); + } + } + internal void Initialize(RenderProfileElement profileElement, string path, PluginFeature feature) { if (path == null) throw new ArgumentNullException(nameof(path)); @@ -209,6 +255,17 @@ namespace Artemis.Core layerPropertyGroup.Update(timeline); } + internal virtual void OnVisibilityChanged() + { + VisibilityChanged?.Invoke(this, EventArgs.Empty); + } + + internal virtual void OnLayerPropertyOnCurrentValueSet(LayerPropertyEventArgs e) + { + Parent?.OnLayerPropertyOnCurrentValueSet(e); + LayerPropertyOnCurrentValueSet?.Invoke(this, e); + } + private void InitializeProperty(PropertyInfo propertyInfo, PropertyDescriptionAttribute propertyDescription) { string path = $"{Path}.{propertyInfo.Name}"; @@ -266,67 +323,11 @@ namespace Artemis.Core return entity; } - #region IDisposable - - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - _disposed = true; - DisableProperties(); - - foreach (ILayerProperty layerProperty in _layerProperties) - layerProperty.Dispose(); - foreach (LayerPropertyGroup layerPropertyGroup in _layerPropertyGroups) - layerPropertyGroup.Dispose(); - } - } - /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } - - #endregion - - #region Events - - /// - /// Occurs when the property group has initialized all its children - /// - public event EventHandler? PropertyGroupInitialized; - - /// - /// Occurs when one of the current value of one of the layer properties in this group changes by some form of input - /// Note: Will not trigger on properties in child groups - /// - public event EventHandler? LayerPropertyOnCurrentValueSet; - - /// - /// Occurs when the value of the layer property was updated - /// - public event EventHandler? VisibilityChanged; - - internal virtual void OnVisibilityChanged() - { - VisibilityChanged?.Invoke(this, EventArgs.Empty); - } - - internal virtual void OnLayerPropertyOnCurrentValueSet(LayerPropertyEventArgs e) - { - Parent?.OnLayerPropertyOnCurrentValueSet(e); - LayerPropertyOnCurrentValueSet?.Invoke(this, e); - } - - #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs index d28885e65..0baf7d834 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs @@ -329,6 +329,13 @@ namespace Artemis.Core /// A boolean indicating whether to remove excess LEDs present in the device but missing in the layout internal void ApplyLayout(ArtemisLayout layout, bool createMissingLeds, bool removeExcessiveLeds) { + if (createMissingLeds && !DeviceProvider.CreateMissingLedsSupported) + throw new ArtemisCoreException($"Cannot apply layout with {nameof(createMissingLeds)} " + + "set to true because the device provider does not support it"); + if (removeExcessiveLeds && !DeviceProvider.RemoveExcessiveLedsSupported) + throw new ArtemisCoreException($"Cannot apply layout with {nameof(removeExcessiveLeds)} " + + "set to true because the device provider does not support it"); + if (layout.IsValid) layout.RgbLayout!.ApplyTo(RgbDevice, createMissingLeds, removeExcessiveLeds); diff --git a/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs b/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs index 949065503..f41e9c145 100644 --- a/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs +++ b/src/Artemis.Core/Plugins/DeviceProviders/DeviceProvider.cs @@ -51,6 +51,18 @@ namespace Artemis.Core.DeviceProviders /// public bool CanDetectLogicalLayout { get; protected set; } + /// + /// Gets or sets a boolean indicating whether adding missing LEDs defined in a layout but missing on the device is supported + /// Note: Defaults to . + /// + public bool CreateMissingLedsSupported { get; protected set; } = true; + + /// + /// Gets or sets a boolean indicating whether removing excess LEDs present in the device but missing in the layout is supported + /// Note: Defaults to . + /// + public bool RemoveExcessiveLedsSupported { get; protected set; } = true; + /// /// Loads a layout for the specified device and wraps it in an /// diff --git a/src/Artemis.Core/Plugins/LayerBrushes/ILayerBrushPreset.cs b/src/Artemis.Core/Plugins/LayerBrushes/ILayerBrushPreset.cs new file mode 100644 index 000000000..72fa2aa40 --- /dev/null +++ b/src/Artemis.Core/Plugins/LayerBrushes/ILayerBrushPreset.cs @@ -0,0 +1,28 @@ +namespace Artemis.Core.LayerBrushes +{ + /// + /// Represents a brush preset for a brush. + /// + public interface ILayerBrushPreset + { + /// + /// Gets the name of the preset + /// + string Name { get; } + + /// + /// Gets the description of the preset + /// + string Description { get; } + + /// + /// Gets the icon of the preset + /// + string Icon { get; } + + /// + /// Applies the preset to the layer brush + /// + void Apply(); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs index c452384e3..2e1712250 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/Internal/BaseLayerBrush.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using SkiaSharp; namespace Artemis.Core.LayerBrushes @@ -70,6 +72,16 @@ namespace Artemis.Core.LayerBrushes /// public virtual LayerPropertyGroup? BaseProperties => null; + /// + /// Gets a list of presets available to this layer brush + /// + public virtual List? Presets => null; + + /// + /// Gets the default preset used for new instances of this layer brush + /// + public virtual ILayerBrushPreset? DefaultPreset => Presets?.FirstOrDefault(); + /// /// Gets or sets whether the brush supports transformations /// Note: RGB.NET brushes can never be transformed and setting this to true will throw an exception diff --git a/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs b/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs index 5b12849b8..9184e1c6b 100644 --- a/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs +++ b/src/Artemis.Core/Plugins/LayerBrushes/Internal/PropertiesLayerBrush.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; namespace Artemis.Core.LayerBrushes { diff --git a/src/Artemis.Core/Resources/intro-profile.json b/src/Artemis.Core/Resources/intro-profile.json index e95c17153..dad8392dc 100644 --- a/src/Artemis.Core/Resources/intro-profile.json +++ b/src/Artemis.Core/Resources/intro-profile.json @@ -292,7 +292,7 @@ "$type": "Artemis.Storage.Entities.Profile.PropertyEntity, Artemis.Storage", "FeatureId": null, "Path": "LayerBrush.GradientColor", - "Value": "{\"Stops\":[{\"Color\":\"#ff0b4a40\",\"Position\":0.0},{\"Color\":\"#ff00897c\",\"Position\":0.242},{\"Color\":\"#ffffffff\",\"Position\":1.0},{\"Color\":\"#ff00ffe6\",\"Position\":0.67391306}]}", + "Value": "[{\"Color\":\"#ff0b4a40\",\"Position\":0.0},{\"Color\":\"#ff00897c\",\"Position\":0.242},{\"Color\":\"#ffffffff\",\"Position\":1.0},{\"Color\":\"#ff00ffe6\",\"Position\":0.67391306}]", "KeyframesEnabled": false, "KeyframeEntities": { "$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib", @@ -592,7 +592,7 @@ "$type": "Artemis.Storage.Entities.Profile.PropertyEntity, Artemis.Storage", "FeatureId": "Artemis.Plugins.LayerBrushes.Noise.NoiseBrushProvider-61cbbf01", "Path": "LayerBrush.GradientColor", - "Value": "{\"Stops\":[{\"Color\":\"#ffff0000\",\"Position\":0.0},{\"Color\":\"#ffffbf00\",\"Position\":0.125},{\"Color\":\"#ff7fff00\",\"Position\":0.25},{\"Color\":\"#ff00ff3f\",\"Position\":0.375},{\"Color\":\"#ff00ffff\",\"Position\":0.5},{\"Color\":\"#ff003fff\",\"Position\":0.625},{\"Color\":\"#ff7f00ff\",\"Position\":0.75},{\"Color\":\"#ffff00bf\",\"Position\":0.875},{\"Color\":\"#ffff0000\",\"Position\":1.0}]}", + "Value": "[{\"Color\":\"#ffff0000\",\"Position\":0.0},{\"Color\":\"#ffffbf00\",\"Position\":0.125},{\"Color\":\"#ff7fff00\",\"Position\":0.25},{\"Color\":\"#ff00ff3f\",\"Position\":0.375},{\"Color\":\"#ff00ffff\",\"Position\":0.5},{\"Color\":\"#ff003fff\",\"Position\":0.625},{\"Color\":\"#ff7f00ff\",\"Position\":0.75},{\"Color\":\"#ffff00bf\",\"Position\":0.875},{\"Color\":\"#ffff0000\",\"Position\":1.0}]", "KeyframesEnabled": false, "KeyframeEntities": { "$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib", @@ -909,7 +909,7 @@ "$type": "Artemis.Storage.Entities.Profile.PropertyEntity, Artemis.Storage", "FeatureId": null, "Path": "LayerBrush.Colors", - "Value": "{\"Stops\":[{\"Color\":\"#00ff0000\",\"Position\":0.0},{\"Color\":\"#ffffdf00\",\"Position\":0.916},{\"Color\":\"#00ffcd00\",\"Position\":1.0},{\"Color\":\"#fff31900\",\"Position\":0.636},{\"Color\":\"#dbf41500\",\"Position\":0.5461956},{\"Color\":\"#00f60f00\",\"Position\":0.462},{\"Color\":\"#93f60d00\",\"Position\":0.3668478}]}", + "Value": "[{\"Color\":\"#00ff0000\",\"Position\":0.0},{\"Color\":\"#ffffdf00\",\"Position\":0.916},{\"Color\":\"#00ffcd00\",\"Position\":1.0},{\"Color\":\"#fff31900\",\"Position\":0.636},{\"Color\":\"#dbf41500\",\"Position\":0.5461956},{\"Color\":\"#00f60f00\",\"Position\":0.462},{\"Color\":\"#93f60d00\",\"Position\":0.3668478}]", "KeyframesEnabled": false, "KeyframeEntities": { "$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib", @@ -1164,7 +1164,7 @@ "$type": "Artemis.Storage.Entities.Profile.PropertyEntity, Artemis.Storage", "FeatureId": "Artemis.Plugins.LayerBrushes.Color.ColorBrushProvider-92a9d6ba", "Path": "LayerBrush.Colors", - "Value": "{\"Stops\":[{\"Color\":\"#ffcaff00\",\"Position\":0.709},{\"Color\":\"#ffff0000\",\"Position\":0.902},{\"Color\":\"#ffffffff\",\"Position\":0.5842391},{\"Color\":\"#ffff4100\",\"Position\":0.95108694},{\"Color\":\"#00ff4100\",\"Position\":0.98641306}]}", + "Value": "[{\"Color\":\"#ffcaff00\",\"Position\":0.709},{\"Color\":\"#ffff0000\",\"Position\":0.902},{\"Color\":\"#ffffffff\",\"Position\":0.5842391},{\"Color\":\"#ffff4100\",\"Position\":0.95108694},{\"Color\":\"#00ff4100\",\"Position\":0.98641306}]", "KeyframesEnabled": false, "KeyframeEntities": { "$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib", @@ -1496,7 +1496,7 @@ "$type": "Artemis.Storage.Entities.Profile.PropertyEntity, Artemis.Storage", "FeatureId": null, "Path": "LayerBrush.Colors", - "Value": "{\"Stops\":[{\"Color\":\"#ffff0000\",\"Position\":0.0},{\"Color\":\"#ffff8800\",\"Position\":0.125},{\"Color\":\"#ffedff00\",\"Position\":0.25},{\"Color\":\"#ff65ff00\",\"Position\":0.375},{\"Color\":\"#ff00ff22\",\"Position\":0.5},{\"Color\":\"#ff00ffaa\",\"Position\":0.625},{\"Color\":\"#ff00cbff\",\"Position\":0.75},{\"Color\":\"#ff0043ff\",\"Position\":0.875},{\"Color\":\"#ffff0000\",\"Position\":1.0}]}", + "Value": "[{\"Color\":\"#ffff0000\",\"Position\":0.0},{\"Color\":\"#ffff8800\",\"Position\":0.125},{\"Color\":\"#ffedff00\",\"Position\":0.25},{\"Color\":\"#ff65ff00\",\"Position\":0.375},{\"Color\":\"#ff00ff22\",\"Position\":0.5},{\"Color\":\"#ff00ffaa\",\"Position\":0.625},{\"Color\":\"#ff00cbff\",\"Position\":0.75},{\"Color\":\"#ff0043ff\",\"Position\":0.875},{\"Color\":\"#ffff0000\",\"Position\":1.0}]", "KeyframesEnabled": false, "KeyframeEntities": { "$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib", @@ -1751,7 +1751,7 @@ "$type": "Artemis.Storage.Entities.Profile.PropertyEntity, Artemis.Storage", "FeatureId": "Artemis.Plugins.LayerBrushes.Color.ColorBrushProvider-92a9d6ba", "Path": "LayerBrush.Colors", - "Value": "{\"Stops\":[{\"Color\":\"#ffff0000\",\"Position\":0.0},{\"Color\":\"#ffffbf00\",\"Position\":0.125},{\"Color\":\"#ff7fff00\",\"Position\":0.25},{\"Color\":\"#ff00ff3f\",\"Position\":0.375},{\"Color\":\"#ff00ffff\",\"Position\":0.5},{\"Color\":\"#ff003fff\",\"Position\":0.625},{\"Color\":\"#ff7f00ff\",\"Position\":0.75},{\"Color\":\"#ffff00bf\",\"Position\":0.875},{\"Color\":\"#ffff0000\",\"Position\":1.0}]}", + "Value": "[{\"Color\":\"#ffff0000\",\"Position\":0.0},{\"Color\":\"#ffffbf00\",\"Position\":0.125},{\"Color\":\"#ff7fff00\",\"Position\":0.25},{\"Color\":\"#ff00ff3f\",\"Position\":0.375},{\"Color\":\"#ff00ffff\",\"Position\":0.5},{\"Color\":\"#ff003fff\",\"Position\":0.625},{\"Color\":\"#ff7f00ff\",\"Position\":0.75},{\"Color\":\"#ffff00bf\",\"Position\":0.875},{\"Color\":\"#ffff0000\",\"Position\":1.0}]", "KeyframesEnabled": false, "KeyframeEntities": { "$type": "System.Collections.Generic.List`1[[Artemis.Storage.Entities.Profile.KeyframeEntity, Artemis.Storage]], System.Private.CoreLib", diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs index 079bb18e2..28f590376 100644 --- a/src/Artemis.Core/Services/CoreService.cs +++ b/src/Artemis.Core/Services/CoreService.cs @@ -251,20 +251,16 @@ namespace Artemis.Core.Services // 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; - - // Stop rendering after the profile finishes (take 1 second extra in case of slow updates) - TimeSpan introLength = intro.AnimationProfile.GetAllLayers().Max(l => l.Timeline.Length)!; - Task.Run(async () => - { - await Task.Delay(introLength.Add(TimeSpan.FromSeconds(1))); - FrameRendering -= DrawOverlay; - - intro.AnimationProfile.Dispose(); - }); } public event EventHandler? Initialized; diff --git a/src/Artemis.Core/Services/Interfaces/IRgbService.cs b/src/Artemis.Core/Services/Interfaces/IRgbService.cs index 00c5f85d0..cfcb5ffb0 100644 --- a/src/Artemis.Core/Services/Interfaces/IRgbService.cs +++ b/src/Artemis.Core/Services/Interfaces/IRgbService.cs @@ -90,9 +90,7 @@ namespace Artemis.Core.Services /// /// /// - /// - /// - void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout layout, bool createMissingLeds, bool removeExessiveLeds); + void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout layout); /// /// Attempts to retrieve the that corresponds the provided RGB.NET diff --git a/src/Artemis.Core/Services/RgbService.cs b/src/Artemis.Core/Services/RgbService.cs index 4f812c886..5e4a5066a 100644 --- a/src/Artemis.Core/Services/RgbService.cs +++ b/src/Artemis.Core/Services/RgbService.cs @@ -312,7 +312,7 @@ namespace Artemis.Core.Services layout = new ArtemisLayout(device.CustomLayoutPath, LayoutSource.Configured); if (layout.IsValid) { - ApplyDeviceLayout(device, layout, true, true); + ApplyDeviceLayout(device, layout); return layout; } } @@ -321,7 +321,7 @@ namespace Artemis.Core.Services layout = device.DeviceProvider.LoadUserLayout(device); if (layout.IsValid) { - ApplyDeviceLayout(device, layout, true, true); + ApplyDeviceLayout(device, layout); return layout; } @@ -329,13 +329,13 @@ namespace Artemis.Core.Services layout = device.DeviceProvider.LoadLayout(device); if (layout.IsValid) { - ApplyDeviceLayout(device, layout, true, true); + ApplyDeviceLayout(device, layout); return layout; } // Finally fall back to a default layout layout = LoadDefaultLayout(device); - ApplyDeviceLayout(device, layout, true, true); + ApplyDeviceLayout(device, layout); return layout; } @@ -344,9 +344,9 @@ namespace Artemis.Core.Services return new("NYI", LayoutSource.Default); } - public void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout layout, bool createMissingLeds, bool removeExessiveLeds) + public void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout layout) { - device.ApplyLayout(layout, createMissingLeds, removeExessiveLeds); + device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported); UpdateLedGroup(); } diff --git a/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs b/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs index d88e3da3b..8a20910db 100644 --- a/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs @@ -55,7 +55,8 @@ namespace Artemis.Core.Services DataModelJsonPluginEndPoint AddDataModelJsonEndPoint(Module module, string endPointName) where T : DataModel; /// - /// Adds a new endpoint that directly maps received JSON to the data model of the provided . + /// Adds a new endpoint that directly maps received JSON to the data model of the provided + /// . /// /// The data model type of the module /// The module whose datamodel to apply the received JSON to @@ -64,7 +65,8 @@ namespace Artemis.Core.Services DataModelJsonPluginEndPoint AddDataModelJsonEndPoint(ProfileModule profileModule, string endPointName) where T : DataModel; /// - /// Adds a new endpoint that directly maps received JSON to the data model of the provided . + /// Adds a new endpoint that directly maps received JSON to the data model of the provided + /// . /// /// The data model type of the module /// The data model expansion whose datamodel to apply the received JSON to @@ -114,7 +116,7 @@ namespace Artemis.Core.Services /// Adds a new Web API controller and restarts the web server /// /// The type of Web API controller to remove - void AddController() where T : WebApiController; + void AddController(PluginFeature feature) where T : WebApiController; /// /// Removes an existing Web API controller and restarts the web server @@ -122,6 +124,18 @@ namespace Artemis.Core.Services /// The type of Web API controller to remove void RemoveController() where T : WebApiController; + /// + /// Adds a new EmbedIO module and restarts the web server + /// + /// The type of module to add + void AddModule(PluginFeature feature) where T : IWebModule; + + /// + /// Removes a EmbedIO module and restarts the web server + /// + /// The type of module to remove + void RemoveModule() where T : IWebModule; + /// /// Occurs when the web server has been created and is about to start. This is the ideal place to add your own modules. /// diff --git a/src/Artemis.Core/Services/WebServer/PluginsModule.cs b/src/Artemis.Core/Services/WebServer/PluginsModule.cs index d7f9d49de..4ad697b66 100644 --- a/src/Artemis.Core/Services/WebServer/PluginsModule.cs +++ b/src/Artemis.Core/Services/WebServer/PluginsModule.cs @@ -48,13 +48,13 @@ namespace Artemis.Core.Services protected override async Task OnRequestAsync(IHttpContext context) { if (context.Route.SubPath == null) - throw HttpException.NotFound(); + return; // Split the sub path string[] pathParts = context.Route.SubPath.Substring(1).Split('/'); // Expect a plugin ID and an endpoint - if (pathParts == null || pathParts.Length != 2) - throw HttpException.BadRequest("Path must contain a plugin ID and endpoint and nothing else."); + if (pathParts.Length != 2) + return; // Find a matching plugin if (!_pluginEndPoints.TryGetValue(pathParts[0], out Dictionary? endPoints)) @@ -78,7 +78,7 @@ namespace Artemis.Core.Services } /// - public override bool IsFinalHandler => true; + public override bool IsFinalHandler => false; internal string? ServerUrl { get; set; } diff --git a/src/Artemis.Core/Services/WebServer/WebApiControllerRegistration.cs b/src/Artemis.Core/Services/WebServer/WebApiControllerRegistration.cs index ad5fb30dc..f12455c5c 100644 --- a/src/Artemis.Core/Services/WebServer/WebApiControllerRegistration.cs +++ b/src/Artemis.Core/Services/WebServer/WebApiControllerRegistration.cs @@ -6,9 +6,9 @@ namespace Artemis.Core.Services { internal class WebApiControllerRegistration : WebApiControllerRegistration where T : WebApiController { - public WebApiControllerRegistration(IKernel kernel) : base(typeof(T)) + public WebApiControllerRegistration(PluginFeature feature) : base(feature, typeof(T)) { - Factory = () => kernel.Get(); + Factory = () => feature.Plugin.Kernel!.Get(); } public Func Factory { get; set; } @@ -17,12 +17,14 @@ namespace Artemis.Core.Services internal abstract class WebApiControllerRegistration { - protected WebApiControllerRegistration(Type controllerType) + protected WebApiControllerRegistration(PluginFeature feature, Type controllerType) { + Feature = feature; ControllerType = controllerType; } public abstract object UntypedFactory { get; } public Type ControllerType { get; set; } + public PluginFeature Feature { get; } } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/WebModuleRegistration.cs b/src/Artemis.Core/Services/WebServer/WebModuleRegistration.cs new file mode 100644 index 000000000..fb7f03d13 --- /dev/null +++ b/src/Artemis.Core/Services/WebServer/WebModuleRegistration.cs @@ -0,0 +1,20 @@ +using System; +using EmbedIO; +using Ninject; + +namespace Artemis.Core.Services +{ + internal class WebModuleRegistration + { + public PluginFeature Feature { get; } + public Type WebModuleType { get; } + + public WebModuleRegistration(PluginFeature feature, Type webModuleType) + { + Feature = feature; + WebModuleType = webModuleType; + } + + public IWebModule CreateInstance() => (IWebModule) Feature.Plugin.Kernel!.Get(WebModuleType); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/WebServerService.cs b/src/Artemis.Core/Services/WebServer/WebServerService.cs index 589004e29..a310fca63 100644 --- a/src/Artemis.Core/Services/WebServer/WebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/WebServerService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using System.Threading.Tasks; using Artemis.Core.DataModelExpansions; @@ -8,7 +9,6 @@ using Artemis.Core.Modules; using EmbedIO; using EmbedIO.WebApi; using Newtonsoft.Json; -using Ninject; using Serilog; namespace Artemis.Core.Services @@ -16,18 +16,19 @@ namespace Artemis.Core.Services internal class WebServerService : IWebServerService, IDisposable { private readonly List _controllers; - private readonly IKernel _kernel; + private readonly List _modules; private readonly ILogger _logger; private readonly PluginSetting _webServerPortSetting; - public WebServerService(IKernel kernel, ILogger logger, ISettingsService settingsService) + public WebServerService(ILogger logger, ISettingsService settingsService, IPluginManagementService pluginManagementService) { - _kernel = kernel; _logger = logger; _controllers = new List(); + _modules = new List(); _webServerPortSetting = settingsService.GetSetting("WebServer.Port", 9696); _webServerPortSetting.SettingChanged += WebServerPortSettingOnSettingChanged; + pluginManagementService.PluginFeatureDisabled += PluginManagementServiceOnPluginFeatureDisabled; PluginsModule = new PluginsModule("/plugins"); StartWebServer(); @@ -43,12 +44,18 @@ namespace Artemis.Core.Services Server?.Dispose(); Server = null; - WebApiModule apiModule = new("/api/", JsonNetSerializer); + WebApiModule apiModule = new("/", JsonNetSerializer); PluginsModule.ServerUrl = $"http://localhost:{_webServerPortSetting.Value}/"; WebServer server = new WebServer(o => o.WithUrlPrefix($"http://*:{_webServerPortSetting.Value}/").WithMode(HttpListenerMode.EmbedIO)) .WithLocalSessionManager() + .WithModule(PluginsModule); + + // Add registered modules + foreach (var webModule in _modules) + server = server.WithModule(webModule.CreateInstance()); + + server = server .WithModule(apiModule) - .WithModule(PluginsModule) .HandleHttpException((context, exception) => HandleHttpExceptionJson(context, exception)) .HandleUnhandledException(JsonExceptionHandlerCallback); @@ -166,9 +173,9 @@ namespace Artemis.Core.Services #region Controller management - public void AddController() where T : WebApiController + public void AddController(PluginFeature feature) where T : WebApiController { - _controllers.Add(new WebApiControllerRegistration(_kernel)); + _controllers.Add(new WebApiControllerRegistration(feature)); StartWebServer(); } @@ -180,6 +187,26 @@ namespace Artemis.Core.Services #endregion + #region Module management + + public void AddModule(PluginFeature feature) where T : IWebModule + { + if (feature == null) throw new ArgumentNullException(nameof(feature)); + if (_modules.Any(r => r.WebModuleType == typeof(T))) + return; + + _modules.Add(new WebModuleRegistration(feature, typeof(T))); + StartWebServer(); + } + + public void RemoveModule() where T : IWebModule + { + _modules.RemoveAll(r => r.WebModuleType == typeof(T)); + StartWebServer(); + } + + #endregion + #region Handlers private async Task JsonExceptionHandlerCallback(IHttpContext context, Exception exception) @@ -235,6 +262,25 @@ namespace Artemis.Core.Services StartWebServer(); } + private void PluginManagementServiceOnPluginFeatureDisabled(object? sender, PluginFeatureEventArgs e) + { + bool mustRestart = false; + if (_controllers.Any(c => c.Feature == e.PluginFeature)) + { + mustRestart = true; + _controllers.RemoveAll(c => c.Feature == e.PluginFeature); + } + + if (_modules.Any(m => m.Feature == e.PluginFeature)) + { + mustRestart = true; + _modules.RemoveAll(m => m.Feature == e.PluginFeature); + } + + if (mustRestart) + StartWebServer(); + } + #endregion #region IDisposable diff --git a/src/Artemis.Core/Utilities/IntroAnimation.cs b/src/Artemis.Core/Utilities/IntroAnimation.cs index 9bb5456e4..9d295e499 100644 --- a/src/Artemis.Core/Utilities/IntroAnimation.cs +++ b/src/Artemis.Core/Utilities/IntroAnimation.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using Artemis.Core.Modules; diff --git a/src/Artemis.Storage/Migrations/M1AttributeBasedPropertiesMigration.cs b/src/Artemis.Storage/Migrations/M0001AttributeBasedPropertiesMigration.cs similarity index 86% rename from src/Artemis.Storage/Migrations/M1AttributeBasedPropertiesMigration.cs rename to src/Artemis.Storage/Migrations/M0001AttributeBasedPropertiesMigration.cs index b1697878a..4ac0e1979 100644 --- a/src/Artemis.Storage/Migrations/M1AttributeBasedPropertiesMigration.cs +++ b/src/Artemis.Storage/Migrations/M0001AttributeBasedPropertiesMigration.cs @@ -3,7 +3,7 @@ using LiteDB; namespace Artemis.Storage.Migrations { - public class M1AttributeBasedPropertiesMigration : IStorageMigration + public class M0001AttributeBasedPropertiesMigration : IStorageMigration { public int UserVersion => 1; diff --git a/src/Artemis.Storage/Migrations/M2ProfileEntitiesEnabledMigration.cs b/src/Artemis.Storage/Migrations/M0002ProfileEntitiesEnabledMigration.cs similarity index 94% rename from src/Artemis.Storage/Migrations/M2ProfileEntitiesEnabledMigration.cs rename to src/Artemis.Storage/Migrations/M0002ProfileEntitiesEnabledMigration.cs index e37a48776..409e2ad23 100644 --- a/src/Artemis.Storage/Migrations/M2ProfileEntitiesEnabledMigration.cs +++ b/src/Artemis.Storage/Migrations/M0002ProfileEntitiesEnabledMigration.cs @@ -5,7 +5,7 @@ using LiteDB; namespace Artemis.Storage.Migrations { - public class M2ProfileEntitiesEnabledMigration : IStorageMigration + public class M0002ProfileEntitiesEnabledMigration : IStorageMigration { public int UserVersion => 2; diff --git a/src/Artemis.Storage/Migrations/M3PluginEntitiesIndexChangesMigration.cs b/src/Artemis.Storage/Migrations/M0003PluginEntitiesIndexChangesMigration.cs similarity index 89% rename from src/Artemis.Storage/Migrations/M3PluginEntitiesIndexChangesMigration.cs rename to src/Artemis.Storage/Migrations/M0003PluginEntitiesIndexChangesMigration.cs index 0d38b175d..17560d181 100644 --- a/src/Artemis.Storage/Migrations/M3PluginEntitiesIndexChangesMigration.cs +++ b/src/Artemis.Storage/Migrations/M0003PluginEntitiesIndexChangesMigration.cs @@ -3,7 +3,7 @@ using LiteDB; namespace Artemis.Storage.Migrations { - public class M3PluginEntitiesIndexChangesMigration : IStorageMigration + public class M0003PluginEntitiesIndexChangesMigration : IStorageMigration { public int UserVersion => 3; diff --git a/src/Artemis.Storage/Migrations/M4ProfileSegments.cs b/src/Artemis.Storage/Migrations/M0004ProfileSegments.cs similarity index 96% rename from src/Artemis.Storage/Migrations/M4ProfileSegments.cs rename to src/Artemis.Storage/Migrations/M0004ProfileSegments.cs index c54e3dc0f..74e1c4f48 100644 --- a/src/Artemis.Storage/Migrations/M4ProfileSegments.cs +++ b/src/Artemis.Storage/Migrations/M0004ProfileSegments.cs @@ -7,7 +7,7 @@ using System.Linq; namespace Artemis.Storage.Migrations { - public class M4ProfileSegmentsMigration : IStorageMigration + public class M0004ProfileSegmentsMigration : IStorageMigration { public int UserVersion => 4; diff --git a/src/Artemis.Storage/Migrations/M5DataBindingTypes.cs b/src/Artemis.Storage/Migrations/M0005DataBindingTypes.cs similarity index 94% rename from src/Artemis.Storage/Migrations/M5DataBindingTypes.cs rename to src/Artemis.Storage/Migrations/M0005DataBindingTypes.cs index 7bc4bcff6..5371de084 100644 --- a/src/Artemis.Storage/Migrations/M5DataBindingTypes.cs +++ b/src/Artemis.Storage/Migrations/M0005DataBindingTypes.cs @@ -3,7 +3,7 @@ using LiteDB; namespace Artemis.Storage.Migrations { - public class M5DataBindingTypes : IStorageMigration + public class M0005DataBindingTypes : IStorageMigration { public int UserVersion => 5; diff --git a/src/Artemis.Storage/Migrations/M6PredicateAbstraction.cs b/src/Artemis.Storage/Migrations/M0006PredicateAbstraction.cs similarity index 96% rename from src/Artemis.Storage/Migrations/M6PredicateAbstraction.cs rename to src/Artemis.Storage/Migrations/M0006PredicateAbstraction.cs index 339b42358..4e7567710 100644 --- a/src/Artemis.Storage/Migrations/M6PredicateAbstraction.cs +++ b/src/Artemis.Storage/Migrations/M0006PredicateAbstraction.cs @@ -4,7 +4,7 @@ using LiteDB; namespace Artemis.Storage.Migrations { - public class M6PredicateAbstraction : IStorageMigration + public class M0006PredicateAbstraction : IStorageMigration { public int UserVersion => 6; diff --git a/src/Artemis.Storage/Migrations/M8PluginFeatures.cs b/src/Artemis.Storage/Migrations/M0008PluginFeatures.cs similarity index 99% rename from src/Artemis.Storage/Migrations/M8PluginFeatures.cs rename to src/Artemis.Storage/Migrations/M0008PluginFeatures.cs index d782ff2bd..5dc088531 100644 --- a/src/Artemis.Storage/Migrations/M8PluginFeatures.cs +++ b/src/Artemis.Storage/Migrations/M0008PluginFeatures.cs @@ -5,7 +5,7 @@ using LiteDB; namespace Artemis.Storage.Migrations { - public class M8PluginFeatures : IStorageMigration + public class M0008PluginFeatures : IStorageMigration { private void Migrate(BsonValue bsonValue, Dictionary pluginMap) { diff --git a/src/Artemis.Storage/Migrations/M9DeviceCalibration.cs b/src/Artemis.Storage/Migrations/M0009DeviceCalibration.cs similarity index 92% rename from src/Artemis.Storage/Migrations/M9DeviceCalibration.cs rename to src/Artemis.Storage/Migrations/M0009DeviceCalibration.cs index fdb45dd8b..7d6aef3b8 100644 --- a/src/Artemis.Storage/Migrations/M9DeviceCalibration.cs +++ b/src/Artemis.Storage/Migrations/M0009DeviceCalibration.cs @@ -3,7 +3,7 @@ using LiteDB; namespace Artemis.Storage.Migrations { - public class M9DeviceCalibration : IStorageMigration + public class M0009DeviceCalibration : IStorageMigration { public int UserVersion => 9; diff --git a/src/Artemis.Storage/Migrations/M10BetterDataBindings.cs b/src/Artemis.Storage/Migrations/M0010BetterDataBindings.cs similarity index 97% rename from src/Artemis.Storage/Migrations/M10BetterDataBindings.cs rename to src/Artemis.Storage/Migrations/M0010BetterDataBindings.cs index 4b7daf20f..ae33770c0 100644 --- a/src/Artemis.Storage/Migrations/M10BetterDataBindings.cs +++ b/src/Artemis.Storage/Migrations/M0010BetterDataBindings.cs @@ -3,7 +3,7 @@ using LiteDB; namespace Artemis.Storage.Migrations { - public class M10BetterDataBindings : IStorageMigration + public class M0010BetterDataBindings : IStorageMigration { private void Migrate(BsonValue bsonValue) { diff --git a/src/Artemis.Storage/Migrations/M0011ColorGradients.cs b/src/Artemis.Storage/Migrations/M0011ColorGradients.cs new file mode 100644 index 000000000..1b4f00bd2 --- /dev/null +++ b/src/Artemis.Storage/Migrations/M0011ColorGradients.cs @@ -0,0 +1,42 @@ +using Artemis.Storage.Migrations.Interfaces; +using LiteDB; + +namespace Artemis.Storage.Migrations +{ + public class M0011ColorGradients : IStorageMigration + { + private void Migrate(BsonValue bsonValue) + { + if (!bsonValue.IsDocument || !bsonValue.AsDocument.TryGetValue("PropertyEntities", out BsonValue propertyEntities)) + return; + + foreach (BsonValue propertyEntity in propertyEntities.AsArray) + { + string valueString = propertyEntity["Value"].AsString; + if (!valueString.StartsWith("{\"Stops\":[{") || !valueString.EndsWith("}]}")) + continue; + + valueString = valueString.Replace("{\"Stops\":[{", "[{"); + valueString = valueString.Replace("}]}", "}]"); + propertyEntity["Value"] = valueString; + } + } + + public int UserVersion => 11; + + public void Apply(LiteRepository repository) + { + ILiteCollection collection = repository.Database.GetCollection("ProfileEntity"); + foreach (BsonDocument bsonDocument in collection.FindAll()) + { + foreach (BsonValue bsonLayer in bsonDocument["Layers"].AsArray) + Migrate(bsonLayer); + + foreach (BsonValue bsonLayer in bsonDocument["Folders"].AsArray) + Migrate(bsonLayer); + + collection.Update(bsonDocument); + } + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs b/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs index bc4b3648f..6dce06e42 100644 --- a/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs +++ b/src/Artemis.UI.Shared/Controls/DeviceVisualizer.cs @@ -10,7 +10,6 @@ using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using Artemis.Core; -using SkiaSharp; using Stylet; namespace Artemis.UI.Shared @@ -88,6 +87,11 @@ namespace Artemis.UI.Shared set => SetValue(HighlightedLedsProperty, value); } + /// + /// Occurs when a LED of the device has been clicked + /// + public event EventHandler? LedClicked; + /// protected override void OnRender(DrawingContext drawingContext) { @@ -140,6 +144,27 @@ namespace Artemis.UI.Shared return ResizeKeepAspect(deviceSize, availableSize.Width, availableSize.Height); } + /// + /// Invokes the event + /// + /// + protected virtual void OnLedClicked(LedClickedEventArgs e) + { + LedClicked?.Invoke(this, e); + } + + /// + /// Releases the unmanaged resources used by the object and optionally releases the managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) _timer.Stop(); + } + private static Size ResizeKeepAspect(Size src, double maxWidth, double maxHeight) { @@ -189,8 +214,8 @@ namespace Artemis.UI.Shared return; Point position = e.GetPosition(this); - double x = (position.X / RenderSize.Width); - double y = (position.Y / RenderSize.Height); + double x = position.X / RenderSize.Width; + double y = position.Y / RenderSize.Height; Point scaledPosition = new(x * Device.Rectangle.Width, y * Device.Rectangle.Height); DeviceVisualizerLed? deviceVisualizerLed = _deviceVisualizerLeds.FirstOrDefault(l => l.DisplayGeometry != null && l.LedRect.Contains(scaledPosition)); @@ -317,35 +342,6 @@ namespace Artemis.UI.Shared drawingContext.Close(); } - #region Events - - public event EventHandler? LedClicked; - - /// - /// Invokes the event - /// - /// - protected virtual void OnLedClicked(LedClickedEventArgs e) - { - LedClicked?.Invoke(this, e); - } - - #endregion - - #region IDisposable - - /// - /// Releases the unmanaged resources used by the object and optionally releases the managed resources. - /// - /// - /// to release both managed and unmanaged resources; - /// to release only unmanaged resources. - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) _timer.Stop(); - } - /// public void Dispose() @@ -353,7 +349,5 @@ namespace Artemis.UI.Shared Dispose(true); GC.SuppressFinalize(this); } - - #endregion } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Controls/GradientPicker.xaml b/src/Artemis.UI.Shared/Controls/GradientPicker.xaml index e9f083456..c13770ec8 100644 --- a/src/Artemis.UI.Shared/Controls/GradientPicker.xaml +++ b/src/Artemis.UI.Shared/Controls/GradientPicker.xaml @@ -37,10 +37,7 @@ Background="{StaticResource Checkerboard}"> - + diff --git a/src/Artemis.UI.Shared/Controls/GradientPicker.xaml.cs b/src/Artemis.UI.Shared/Controls/GradientPicker.xaml.cs index eaa6883d8..ad7992140 100644 --- a/src/Artemis.UI.Shared/Controls/GradientPicker.xaml.cs +++ b/src/Artemis.UI.Shared/Controls/GradientPicker.xaml.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Specialized; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Windows; using System.Windows.Input; +using System.Windows.Media; using Artemis.Core; using Artemis.UI.Shared.Properties; using Artemis.UI.Shared.Services; @@ -17,12 +19,14 @@ namespace Artemis.UI.Shared { private static IColorPickerService? _colorPickerService; private bool _inCallback; + private ColorGradientToGradientStopsConverter _gradientConverter; /// /// Creates a new instance of the class /// public GradientPicker() { + _gradientConverter = new ColorGradientToGradientStopsConverter(); InitializeComponent(); } @@ -99,10 +103,27 @@ namespace Artemis.UI.Shared return; gradientPicker._inCallback = true; + + if (e.OldValue is ColorGradient oldGradient) + oldGradient.CollectionChanged -= gradientPicker.GradientChanged; + if (e.NewValue is ColorGradient newGradient) + newGradient.CollectionChanged += gradientPicker.GradientChanged; + gradientPicker.UpdateGradientStops(); gradientPicker.OnPropertyChanged(nameof(ColorGradient)); + gradientPicker._inCallback = false; } + private void GradientChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + Dispatcher.Invoke(UpdateGradientStops); + } + + private void UpdateGradientStops() + { + GradientPreview.GradientStops = (GradientStopCollection)_gradientConverter.Convert(ColorGradient, null!, null!, null!); + } + private void UIElement_OnMouseUp(object sender, MouseButtonEventArgs e) { if (_colorPickerService == null) diff --git a/src/Artemis.UI.Shared/Converters/ColorGradientToGradientStopsConverter.cs b/src/Artemis.UI.Shared/Converters/ColorGradientToGradientStopsConverter.cs index ad4138311..2a1c9de0a 100644 --- a/src/Artemis.UI.Shared/Converters/ColorGradientToGradientStopsConverter.cs +++ b/src/Artemis.UI.Shared/Converters/ColorGradientToGradientStopsConverter.cs @@ -14,18 +14,18 @@ namespace Artemis.UI.Shared /// Converts into a /// . /// - [ValueConversion(typeof(List), typeof(GradientStopCollection))] + [ValueConversion(typeof(ColorGradient), typeof(GradientStopCollection))] public class ColorGradientToGradientStopsConverter : IValueConverter { /// public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - List colorGradients = (List) value; + ColorGradient? colorGradient = value as ColorGradient; GradientStopCollection collection = new(); - if (colorGradients == null) + if (colorGradient == null) return collection; - foreach (ColorGradientStop c in colorGradients.OrderBy(s => s.Position)) + foreach (ColorGradientStop c in colorGradient.OrderBy(s => s.Position)) collection.Add(new GradientStop(Color.FromArgb(c.Color.Alpha, c.Color.Red, c.Color.Green, c.Color.Blue), c.Position)); return collection; } @@ -33,8 +33,8 @@ namespace Artemis.UI.Shared /// public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { - GradientStopCollection collection = (GradientStopCollection) value; - List colorGradients = new(); + GradientStopCollection? collection = value as GradientStopCollection; + ColorGradient colorGradients = new(); if (collection == null) return colorGradients; diff --git a/src/Artemis.UI.Shared/Screens/GradientEditor/ColorStopViewModel.cs b/src/Artemis.UI.Shared/Screens/GradientEditor/ColorStopViewModel.cs index 7d93d868a..7629a9899 100644 --- a/src/Artemis.UI.Shared/Screens/GradientEditor/ColorStopViewModel.cs +++ b/src/Artemis.UI.Shared/Screens/GradientEditor/ColorStopViewModel.cs @@ -20,7 +20,6 @@ namespace Artemis.UI.Shared.Screens.GradientEditor { _gradientEditorViewModel = gradientEditorViewModel; ColorStop = colorStop; - ColorStop.PropertyChanged += ColorStopOnPropertyChanged; } public ColorGradientStop ColorStop { get; } @@ -58,12 +57,7 @@ namespace Artemis.UI.Shared.Screens.GradientEditor get => _willRemoveColorStop; set => SetAndNotify(ref _willRemoveColorStop, value); } - - private void ColorStopOnPropertyChanged(object? sender, PropertyChangedEventArgs e) - { - _gradientEditorViewModel.ColorGradient.OnColorValuesUpdated(); - } - + #region Movement public void StopMouseDown(object sender, MouseButtonEventArgs e) @@ -102,7 +96,7 @@ namespace Artemis.UI.Shared.Screens.GradientEditor double minValue = 0.0; double maxValue = _gradientEditorViewModel.PreviewWidth; - List stops = _gradientEditorViewModel.ColorGradient.Stops.OrderBy(s => s.Position).ToList(); + List stops = _gradientEditorViewModel.ColorGradient.ToList(); ColorGradientStop? previous = stops.IndexOf(ColorStop) >= 1 ? stops[stops.IndexOf(ColorStop) - 1] : null; ColorGradientStop? next = stops.IndexOf(ColorStop) + 1 < stops.Count ? stops[stops.IndexOf(ColorStop) + 1] : null; if (previous != null) @@ -111,7 +105,6 @@ namespace Artemis.UI.Shared.Screens.GradientEditor maxValue = next.Position * _gradientEditorViewModel.PreviewWidth; Offset = Math.Max(minValue, Math.Min(maxValue, position.X)); - _gradientEditorViewModel.ColorGradient.OnColorValuesUpdated(); } #endregion diff --git a/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorView.xaml b/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorView.xaml index 49af71b72..64e084204 100644 --- a/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorView.xaml +++ b/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorView.xaml @@ -66,7 +66,7 @@ diff --git a/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorViewModel.cs b/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorViewModel.cs index f53a099af..497d81963 100644 --- a/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorViewModel.cs +++ b/src/Artemis.UI.Shared/Screens/GradientEditor/GradientEditorViewModel.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Collections.Specialized; using System.ComponentModel; using System.Linq; using System.Windows; @@ -6,6 +7,7 @@ using System.Windows.Controls; using System.Windows.Input; using Artemis.Core; using Artemis.UI.Shared.Services; +using MaterialDesignThemes.Wpf; using Stylet; namespace Artemis.UI.Shared.Screens.GradientEditor @@ -21,12 +23,24 @@ namespace Artemis.UI.Shared.Screens.GradientEditor ColorGradient = colorGradient; ColorStopViewModels = new BindableCollection(); - _originalStops = ColorGradient.Stops.Select(s => new ColorGradientStop(s.Color, s.Position)).ToList(); + _originalStops = ColorGradient.Select(s => new ColorGradientStop(s.Color, s.Position)).ToList(); PropertyChanged += UpdateColorStopViewModels; + ColorGradient.CollectionChanged += ColorGradientOnCollectionChanged; } - public BindableCollection ColorStopViewModels { get; set; } + #region Overrides of DialogViewModelBase + + /// + public override void OnDialogClosed(object sender, DialogClosingEventArgs e) + { + ColorGradient.CollectionChanged -= ColorGradientOnCollectionChanged; + base.OnDialogClosed(sender, e); + } + + #endregion + + public BindableCollection ColorStopViewModels { get; } public ColorStopViewModel? SelectedColorStopViewModel { @@ -48,15 +62,19 @@ namespace Artemis.UI.Shared.Screens.GradientEditor set => SetAndNotify(ref _previewWidth, value); } + public ColorGradient Stops + { + get => ColorGradient; + } + public void AddColorStop(object sender, MouseEventArgs e) { Canvas? child = VisualTreeUtilities.FindChild((DependencyObject) sender, null); float position = (float) (e.GetPosition(child).X / PreviewWidth); ColorGradientStop stop = new(ColorGradient.GetColor(position), position); - ColorGradient.Stops.Add(stop); - ColorGradient.OnColorValuesUpdated(); + ColorGradient.Add(stop); - int index = ColorGradient.Stops.OrderBy(s => s.Position).ToList().IndexOf(stop); + int index = ColorGradient.IndexOf(stop); ColorStopViewModel viewModel = new(this, stop); ColorStopViewModels.Insert(index, viewModel); @@ -69,8 +87,7 @@ namespace Artemis.UI.Shared.Screens.GradientEditor return; ColorStopViewModels.Remove(colorStopViewModel); - ColorGradient.Stops.Remove(colorStopViewModel.ColorStop); - ColorGradient.OnColorValuesUpdated(); + ColorGradient.Remove(colorStopViewModel.ColorStop); SelectColorStop(null); } @@ -97,9 +114,9 @@ namespace Artemis.UI.Shared.Screens.GradientEditor public override void Cancel() { // Restore the saved state - ColorGradient.Stops.Clear(); - ColorGradient.Stops.AddRange(_originalStops); - ColorGradient.OnColorValuesUpdated(); + ColorGradient.Clear(); + foreach (ColorGradientStop colorGradientStop in _originalStops) + ColorGradient.Add(colorGradientStop); base.Cancel(); } @@ -107,8 +124,13 @@ namespace Artemis.UI.Shared.Screens.GradientEditor private void UpdateColorStopViewModels(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName != nameof(PreviewWidth)) return; - foreach (ColorGradientStop colorStop in ColorGradient.Stops.OrderBy(s => s.Position)) + foreach (ColorGradientStop colorStop in ColorGradient) ColorStopViewModels.Add(new ColorStopViewModel(this, colorStop)); } + + private void ColorGradientOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + NotifyOfPropertyChange(nameof(ColorGradient)); + } } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs index 79d71359b..1cff68bfd 100644 --- a/src/Artemis.UI.Shared/Services/ProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/ProfileEditorService.cs @@ -19,7 +19,6 @@ namespace Artemis.UI.Shared.Services { internal class ProfileEditorService : IProfileEditorService { - private readonly ICoreService _coreService; private readonly IKernel _kernel; private readonly ILogger _logger; private readonly IProfileService _profileService; @@ -28,6 +27,7 @@ namespace Artemis.UI.Shared.Services private readonly object _selectedProfileElementLock = new(); private readonly object _selectedProfileLock = new(); private TimeSpan _currentTime; + private bool _doTick; private int _pixelsPerSecond; public ProfileEditorService(IKernel kernel, ILogger logger, IProfileService profileService, ICoreService coreService, IRgbService rgbService) @@ -35,16 +35,63 @@ namespace Artemis.UI.Shared.Services _kernel = kernel; _logger = logger; _profileService = profileService; - _coreService = coreService; _rgbService = rgbService; _registeredPropertyEditors = new List(); - + coreService.FrameRendered += CoreServiceOnFrameRendered; 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) { - _coreService.FrameRendered -= CoreServiceOnFrameRendered; + if (!_doTick) return; + _doTick = false; Execute.PostToUIThread(OnProfilePreviewUpdated); } @@ -72,6 +119,26 @@ namespace Artemis.UI.Shared.Services UpdateProfilePreview(); } + private void Tick() + { + if (SelectedProfile == null || _doTick) + return; + + // Stick to the main segment for any element that is not currently selected + foreach (Folder folder in SelectedProfile.GetAllFolders()) + folder.Timeline.Override(CurrentTime, folder.Timeline.PlayMode == TimelinePlayMode.Repeat); + foreach (Layer layer in SelectedProfile.GetAllLayers()) + layer.Timeline.Override(CurrentTime, (layer != SelectedProfileElement || layer.Timeline.Length < CurrentTime) && layer.Timeline.PlayMode == TimelinePlayMode.Repeat); + + _doTick = true; + } + + private void SelectedProfileOnDeactivated(object? sender, EventArgs e) + { + // Execute.PostToUIThread(() => ChangeSelectedProfile(null)); + ChangeSelectedProfile(null); + } + public ReadOnlyCollection RegisteredPropertyEditors => _registeredPropertyEditors.AsReadOnly(); public bool Playing { get; set; } @@ -86,7 +153,7 @@ namespace Artemis.UI.Shared.Services { if (_currentTime.Equals(value)) return; _currentTime = value; - UpdateProfilePreview(); + Tick(); OnCurrentTimeChanged(); } } @@ -180,16 +247,9 @@ namespace Artemis.UI.Shared.Services public void UpdateProfilePreview() { - if (SelectedProfile == null) + if (Playing) return; - - // Stick to the main segment for any element that is not currently selected - foreach (Folder folder in SelectedProfile.GetAllFolders()) - folder.Timeline.Override(CurrentTime, folder.Timeline.PlayMode == TimelinePlayMode.Repeat); - foreach (Layer layer in SelectedProfile.GetAllLayers()) - layer.Timeline.Override(CurrentTime, (layer != SelectedProfileElement || layer.Timeline.Length < CurrentTime) && layer.Timeline.PlayMode == TimelinePlayMode.Repeat); - - _coreService.FrameRendered += CoreServiceOnFrameRendered; + Tick(); } public bool UndoUpdateProfile() @@ -357,6 +417,15 @@ namespace Artemis.UI.Shared.Services .ToList(); } + public event EventHandler? ProfileSelected; + public event EventHandler? SelectedProfileUpdated; + public event EventHandler? ProfileElementSelected; + public event EventHandler? SelectedProfileElementUpdated; + public event EventHandler? SelectedDataBindingChanged; + public event EventHandler? CurrentTimeChanged; + public event EventHandler? PixelsPerSecondChanged; + public event EventHandler? ProfilePreviewUpdated; + #region Copy/paste public ProfileElement? DuplicateProfileElement(ProfileElement profileElement) @@ -436,70 +505,5 @@ namespace Artemis.UI.Shared.Services } #endregion - - #region Events - - public event EventHandler? ProfileSelected; - public event EventHandler? SelectedProfileUpdated; - public event EventHandler? ProfileElementSelected; - public event EventHandler? SelectedProfileElementUpdated; - public event EventHandler? SelectedDataBindingChanged; - public event EventHandler? CurrentTimeChanged; - public event EventHandler? PixelsPerSecondChanged; - public event EventHandler? ProfilePreviewUpdated; - 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 SelectedProfileOnDeactivated(object? sender, EventArgs e) - { - // Execute.PostToUIThread(() => ChangeSelectedProfile(null)); - ChangeSelectedProfile(null); - } - - #endregion } } \ No newline at end of file diff --git a/src/Artemis.UI/ApplicationStateManager.cs b/src/Artemis.UI/ApplicationStateManager.cs index e2f1100a8..c751abcb6 100644 --- a/src/Artemis.UI/ApplicationStateManager.cs +++ b/src/Artemis.UI/ApplicationStateManager.cs @@ -64,7 +64,7 @@ namespace Artemis.UI string url = await File.ReadAllTextAsync(Path.Combine(Constants.DataFolder, "webserver.txt")); using HttpClient client = new(); - await client.PostAsync(url + "api/remote/bring-to-foreground", null!); + await client.PostAsync(url + "remote/bring-to-foreground", null!); } private void UtilitiesOnRestartRequested(object sender, RestartEventArgs e) diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs index 20409eb1c..c356cfc60 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs @@ -1,8 +1,10 @@ using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.LayerBrushes; using Artemis.Core.Services; +using Artemis.UI.Screens.ProfileEditor.Dialogs; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using Stylet; @@ -12,12 +14,15 @@ namespace Artemis.UI.DefaultTypes.PropertyInput public class BrushPropertyInputViewModel : PropertyInputViewModel { private readonly IPluginManagementService _pluginManagementService; + private readonly IDialogService _dialogService; private BindableCollection _descriptors; - public BrushPropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IPluginManagementService pluginManagementService) + public BrushPropertyInputViewModel(LayerProperty layerProperty, IProfileEditorService profileEditorService, IPluginManagementService pluginManagementService, + IDialogService dialogService) : base(layerProperty, profileEditorService) { _pluginManagementService = pluginManagementService; + _dialogService = dialogService; UpdateEnumValues(); } @@ -43,7 +48,17 @@ namespace Artemis.UI.DefaultTypes.PropertyInput protected override void OnInputValueApplied() { if (LayerProperty.ProfileElement is Layer layer) + { layer.ChangeLayerBrush(SelectedDescriptor); + if (layer.LayerBrush?.Presets != null && layer.LayerBrush.Presets.Any()) + { + Execute.PostToUIThread(async () => + { + await Task.Delay(400); + _dialogService.ShowDialogAt("LayerProperties", new Dictionary {{"layerBrush", layer.LayerBrush}}); + }); + } + } } private void SetBrushByDescriptor(LayerBrushDescriptor value) diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.xaml b/src/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.xaml index ef17ba34e..ea3f4a60f 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.xaml +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/ColorGradientPropertyInputView.xaml @@ -5,6 +5,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:s="https://github.com/canton7/Stylet" xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" + xmlns:propertyInput="clr-namespace:Artemis.UI.DefaultTypes.PropertyInput" mc:Ignorable="d" d:DesignHeight="25" d:DesignWidth="800" d:DataContext="{d:DesignInstance propertyInput:ColorGradientPropertyInputViewModel}"> diff --git a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Abstract/DataModelConditionPredicateViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Abstract/DataModelConditionPredicateViewModel.cs index 6efb8a33a..a38c17ddb 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Conditions/Abstract/DataModelConditionPredicateViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Conditions/Abstract/DataModelConditionPredicateViewModel.cs @@ -132,16 +132,12 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract DisposeRightSideDynamicViewModel(); if (RightSideInputViewModel == null) CreateRightSideInputViewModel(); - + Type preferredType = DataModelConditionPredicate.GetPreferredRightSideType(); - // Ensure the right static value is never null when the preferred type is a value type - if (preferredType.IsValueType && DataModelConditionPredicate.RightStaticValue == null) - RightSideInputViewModel.Value = preferredType.GetDefault(); - else - RightSideInputViewModel.Value = DataModelConditionPredicate.RightStaticValue; - - if (RightSideInputViewModel.TargetType != preferredType) + if (preferredType != null && RightSideInputViewModel.TargetType != preferredType) RightSideInputViewModel.UpdateTargetType(preferredType); + + RightSideInputViewModel.Value = DataModelConditionPredicate.RightStaticValue; } } @@ -294,6 +290,12 @@ namespace Artemis.UI.Screens.ProfileEditor.Conditions.Abstract private void RightSideSelectionViewModelOnSwitchToStaticRequested(object sender, EventArgs e) { DataModelConditionPredicate.PredicateType = ProfileRightSideType.Static; + + // Ensure the right static value is never null when the preferred type is a value type + Type preferredType = DataModelConditionPredicate.GetPreferredRightSideType(); + if (DataModelConditionPredicate.RightStaticValue == null && preferredType != null && preferredType.IsValueType) + DataModelConditionPredicate.UpdateRightSideStatic(preferredType.GetDefault()); + Update(); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/Dialogs/LayerBrushPresetView.xaml b/src/Artemis.UI/Screens/ProfileEditor/Dialogs/LayerBrushPresetView.xaml new file mode 100644 index 000000000..6958ab076 --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/Dialogs/LayerBrushPresetView.xaml @@ -0,0 +1,42 @@ + + + + Select a brush preset + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Dialogs/LayerBrushPresetViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Dialogs/LayerBrushPresetViewModel.cs new file mode 100644 index 000000000..44d90cf25 --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/Dialogs/LayerBrushPresetViewModel.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using Artemis.Core.LayerBrushes; +using Artemis.UI.Shared.Services; +using Stylet; + +namespace Artemis.UI.Screens.ProfileEditor.Dialogs +{ + public class LayerBrushPresetViewModel : DialogViewModelBase + { + private readonly BaseLayerBrush _layerBrush; + private ILayerBrushPreset _selectedPreset; + + public LayerBrushPresetViewModel(BaseLayerBrush layerBrush) + { + _layerBrush = layerBrush; + Presets = new BindableCollection(); + Presets.AddRange(layerBrush.Presets); + } + + public BindableCollection Presets { get; } + + public ILayerBrushPreset SelectedPreset + { + get => _selectedPreset; + set + { + SetAndNotify(ref _selectedPreset, value); + SelectPreset(value); + } + } + + public void SelectPreset(ILayerBrushPreset preset) + { + _layerBrush.BaseProperties?.ResetAllLayerProperties(); + preset.Apply(); + Execute.OnUIThreadAsync(async () => + { + await Task.Delay(250); + Session?.Close(true); + }); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierViewModel.cs index ee01ad3f4..8b943a6b6 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/DataBindings/DirectDataBinding/DataBindingModifierViewModel.cs @@ -73,7 +73,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.DirectDa Modifier.DirectDataBinding.RemoveModifier(Modifier); _profileEditorService.UpdateSelectedProfileElement(); } - + private void ParameterSelectionViewModelOnPropertySelected(object sender, DataModelInputDynamicEventArgs e) { Modifier.UpdateParameterDynamic(e.DataModelPath); @@ -139,13 +139,8 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.DirectDa // Parameter if (DynamicSelectionViewModel != null) DynamicSelectionViewModel.ChangeDataModelPath(Modifier.ParameterPath); - else if (StaticInputViewModel != null) - { - // Ensure the right static value is never null when the preferred type is a value type + else if (StaticInputViewModel != null) StaticInputViewModel.Value = Modifier.ParameterStaticValue; - if (SelectedModifierType.ParameterType.IsValueType && StaticInputViewModel.Value == null) - StaticInputViewModel.Value = SelectedModifierType.ParameterType.GetDefault(); - } } private void ExecuteSelectModifierTypeCommand(object context) @@ -197,6 +192,12 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.DataBindings.DirectDa private void DynamicSelectionViewModelOnSwitchToStaticRequested(object sender, EventArgs e) { Modifier.ParameterType = ProfileRightSideType.Static; + + // Ensure the right static value is never null when the preferred type is a value type + if (SelectedModifierType.ParameterType != null && + SelectedModifierType.ParameterType.IsValueType && Modifier.ParameterStaticValue == null) + Modifier.UpdateParameterStatic(SelectedModifierType.ParameterType.GetDefault()); + Update(); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesView.xaml b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesView.xaml index 5f640610f..a6f1a5dd0 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesView.xaml +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/LayerPropertiesView.xaml @@ -66,210 +66,188 @@ - - - - - - - - - - - - - + + - + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + Don't repeat the timeline + + + This setting only applies to the editor and affect the repeat mode during normal profile playback + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - + HorizontalAlignment="Right" + Margin="10 5" + Minimum="31" + Maximum="350" + TickFrequency="1" + IsSnapToTickEnabled="True" + AutoToolTipPlacement="TopLeft" + Value="{Binding ProfileEditorService.PixelsPerSecond}" + Width="319" /> + + - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreeGroupViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreeGroupViewModel.cs index 76c54481b..30d8fd822 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreeGroupViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreeGroupViewModel.cs @@ -32,7 +32,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree private readonly IDialogService _dialogService; private readonly IProfileEditorService _profileEditorService; private readonly IWindowManager _windowManager; - private LayerBrushSettingsWindowViewModel? _layerBrushSettingsWindowVm; + private LayerBrushSettingsWindowViewModel _layerBrushSettingsWindowVm; private LayerEffectSettingsWindowViewModel _layerEffectSettingsWindowVm; public TreeGroupViewModel(LayerPropertyGroupViewModel layerPropertyGroupViewModel, IProfileEditorService profileEditorService, IDialogService dialogService, IWindowManager windowManager) diff --git a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreePropertyViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreePropertyViewModel.cs index c6dc54879..971746de6 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreePropertyViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/LayerProperties/Tree/TreePropertyViewModel.cs @@ -47,7 +47,7 @@ namespace Artemis.UI.Screens.ProfileEditor.LayerProperties.Tree public void ResetToDefault() { - LayerProperty.ApplyDefaultValue(_profileEditorService.CurrentTime); + LayerProperty.ApplyDefaultValue(); _profileEditorService.UpdateSelectedProfileElement(); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs index 2fc7d718f..1906c9cf8 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Visualization/ProfileViewModel.cs @@ -321,7 +321,8 @@ namespace Artemis.UI.Screens.ProfileEditor.Visualization dataBindingRegistration.GetDataBinding()?.UpdateWithDelta(delta); // TODO: Only update when there are data bindings - _profileEditorService.UpdateProfilePreview(); + if (!_profileEditorService.Playing) + _profileEditorService.UpdateProfilePreview(); } private void HighlightSelectedLayerOnSettingChanged(object sender, EventArgs e) diff --git a/src/Artemis.UI/Screens/Settings/Device/DeviceDialogView.xaml b/src/Artemis.UI/Screens/Settings/Device/DeviceDialogView.xaml index 8973e4109..8134150e7 100644 --- a/src/Artemis.UI/Screens/Settings/Device/DeviceDialogView.xaml +++ b/src/Artemis.UI/Screens/Settings/Device/DeviceDialogView.xaml @@ -105,8 +105,8 @@ + .Collection.OneActive { + private readonly ICoreService _coreService; private readonly IDeviceService _deviceService; private readonly IDialogService _dialogService; private readonly IRgbService _rgbService; - private ArtemisLed _selectedLed; private SnackbarMessageQueue _deviceMessageQueue; + private BindableCollection _selectedLeds; - public DeviceDialogViewModel(ArtemisDevice device, IDeviceService deviceService, IRgbService rgbService, IDialogService dialogService, IDeviceDebugVmFactory factory) + public DeviceDialogViewModel(ArtemisDevice device, + ICoreService coreService, + IDeviceService deviceService, + IRgbService rgbService, + IDialogService dialogService, + IDeviceDebugVmFactory factory) { + _coreService = coreService; _deviceService = deviceService; _rgbService = rgbService; _dialogService = dialogService; @@ -39,19 +47,8 @@ namespace Artemis.UI.Screens.Settings.Device Items.Add(factory.DeviceLedsTabViewModel(device)); ActiveItem = Items.First(); DisplayName = $"{device.RgbDevice.DeviceInfo.Model} | Artemis"; - } - protected override void OnInitialActivate() - { - DeviceMessageQueue = new SnackbarMessageQueue(TimeSpan.FromSeconds(5)); - Device.DeviceUpdated += DeviceOnDeviceUpdated; - base.OnInitialActivate(); - } - - protected override void OnClose() - { - Device.DeviceUpdated -= DeviceOnDeviceUpdated; - base.OnClose(); + SelectedLeds = new BindableCollection(); } public ArtemisDevice Device { get; } @@ -63,29 +60,61 @@ namespace Artemis.UI.Screens.Settings.Device set => SetAndNotify(ref _deviceMessageQueue, value); } - public ArtemisLed SelectedLed - { - get => _selectedLed; - set - { - if (!SetAndNotify(ref _selectedLed, value)) return; - NotifyOfPropertyChange(nameof(SelectedLeds)); - } - } - public bool CanExportLayout => Device.Layout?.IsValid ?? false; - public List SelectedLeds => SelectedLed != null ? new List {SelectedLed} : null; + public BindableCollection SelectedLeds + { + get => _selectedLeds; + set => SetAndNotify(ref _selectedLeds, value); + } public bool CanOpenImageDirectory => Device.Layout?.Image != null; + public void OnLedClicked(object sender, LedClickedEventArgs e) + { + if (!Keyboard.IsKeyDown(Key.LeftShift) && !Keyboard.IsKeyDown(Key.RightShift)) + SelectedLeds.Clear(); + SelectedLeds.Add(e.Led); + } + + protected override void OnInitialActivate() + { + _coreService.FrameRendering += CoreServiceOnFrameRendering; + DeviceMessageQueue = new SnackbarMessageQueue(TimeSpan.FromSeconds(5)); + Device.DeviceUpdated += DeviceOnDeviceUpdated; + base.OnInitialActivate(); + } + + protected override void OnClose() + { + _coreService.FrameRendering -= CoreServiceOnFrameRendering; + Device.DeviceUpdated -= DeviceOnDeviceUpdated; + base.OnClose(); + } + + private void CoreServiceOnFrameRendering(object sender, FrameRenderingEventArgs e) + { + if (SelectedLeds == null || !SelectedLeds.Any()) + return; + + using SKPaint highlightPaint = new() {Color = SKColors.White}; + using SKPaint dimPaint = new() {Color = new SKColor(0, 0, 0, 192)}; + foreach (ArtemisLed artemisLed in Device.Leds) + e.Canvas.DrawRect(artemisLed.AbsoluteRectangle, SelectedLeds.Contains(artemisLed) ? highlightPaint : dimPaint); + } + + private void DeviceOnDeviceUpdated(object sender, EventArgs e) + { + NotifyOfPropertyChange(nameof(CanExportLayout)); + } + // ReSharper disable UnusedMember.Global #region Command handlers public void ClearSelection() { - SelectedLed = null; + SelectedLeds.Clear(); } public void IdentifyDevice() @@ -190,19 +219,5 @@ namespace Artemis.UI.Screens.Settings.Device #endregion // ReSharper restore UnusedMember.Global - - #region Event handlers - - private void DeviceOnDeviceUpdated(object sender, EventArgs e) - { - NotifyOfPropertyChange(nameof(CanExportLayout)); - } - - public void OnLedClicked(object sender, LedClickedEventArgs e) - { - SelectedLed = e.Led; - } - - #endregion } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Device/Tabs/DeviceLedsTabView.xaml b/src/Artemis.UI/Screens/Settings/Device/Tabs/DeviceLedsTabView.xaml index 08b374383..6bfec663f 100644 --- a/src/Artemis.UI/Screens/Settings/Device/Tabs/DeviceLedsTabView.xaml +++ b/src/Artemis.UI/Screens/Settings/Device/Tabs/DeviceLedsTabView.xaml @@ -4,34 +4,39 @@ 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:core="clr-namespace:Artemis.Core;assembly=Artemis.Core" xmlns:tabs="clr-namespace:Artemis.UI.Screens.Settings.Device.Tabs" - xmlns:Converters="clr-namespace:Artemis.UI.Converters" x:Class="Artemis.UI.Screens.Settings.Device.Tabs.DeviceLedsTabView" + xmlns:converters="clr-namespace:Artemis.UI.Converters" + x:Class="Artemis.UI.Screens.Settings.Device.Tabs.DeviceLedsTabView" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" d:DataContext="{d:DesignInstance {x:Type tabs:DeviceLedsTabViewModel}}"> - + - + + + - - - - - - + + + + + + diff --git a/src/Artemis.UI/Screens/Settings/Device/Tabs/DeviceLedsTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Device/Tabs/DeviceLedsTabViewModel.cs index d57f8cb1a..5ec8986f7 100644 --- a/src/Artemis.UI/Screens/Settings/Device/Tabs/DeviceLedsTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Device/Tabs/DeviceLedsTabViewModel.cs @@ -1,17 +1,92 @@ -using Artemis.Core; +using System.Collections.Specialized; +using System.Linq; +using Artemis.Core; using Stylet; namespace Artemis.UI.Screens.Settings.Device.Tabs { public class DeviceLedsTabViewModel : Screen { - public DeviceLedsTabViewModel(ArtemisDevice device) { Device = device; DisplayName = "LEDS"; + LedViewModels = new BindableCollection(); } public ArtemisDevice Device { get; } + public BindableCollection LedViewModels { get; } + + private void SelectedLedsOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + UpdateSelectedLeds(); + } + + private void UpdateSelectedLeds() + { + foreach (DeviceLedsTabLedViewModel deviceLedsTabLedViewModel in LedViewModels) + deviceLedsTabLedViewModel.Update(); + } + + #region Overrides of Screen + + /// + protected override void OnInitialActivate() + { + BindableCollection selectedLeds = ((DeviceDialogViewModel) Parent).SelectedLeds; + LedViewModels.Clear(); + LedViewModels.AddRange(Device.Leds.Select(l => new DeviceLedsTabLedViewModel(l, selectedLeds))); + selectedLeds.CollectionChanged += SelectedLedsOnCollectionChanged; + + base.OnInitialActivate(); + } + + /// + protected override void OnClose() + { + ((DeviceDialogViewModel) Parent).SelectedLeds.CollectionChanged -= SelectedLedsOnCollectionChanged; + base.OnClose(); + } + + #endregion + } + + public class DeviceLedsTabLedViewModel : PropertyChangedBase + { + private readonly BindableCollection _selectedLeds; + private bool _isSelected; + + public DeviceLedsTabLedViewModel(ArtemisLed artemisLed, BindableCollection selectedLeds) + { + _selectedLeds = selectedLeds; + ArtemisLed = artemisLed; + + Update(); + } + + public ArtemisLed ArtemisLed { get; } + + public bool IsSelected + { + get => _isSelected; + set + { + if (!SetAndNotify(ref _isSelected, value)) return; + Apply(); + } + } + + public void Update() + { + IsSelected = _selectedLeds.Contains(ArtemisLed); + } + + public void Apply() + { + if (IsSelected && !_selectedLeds.Contains(ArtemisLed)) + _selectedLeds.Add(ArtemisLed); + else if (!IsSelected && _selectedLeds.Contains(ArtemisLed)) + _selectedLeds.Remove(ArtemisLed); + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureViewModel.cs index a6a80b341..a4e8587dc 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureViewModel.cs @@ -16,7 +16,6 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins private readonly IPluginManagementService _pluginManagementService; private bool _enabling; private readonly IMessageService _messageService; - private bool _canToggleEnabled; public PluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo, bool showShield, diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsWindowView.xaml b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsWindowView.xaml index f8bbfa6a6..a05dd93df 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsWindowView.xaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsWindowView.xaml @@ -7,6 +7,7 @@ xmlns:controls="clr-namespace:MaterialDesignExtensions.Controls;assembly=MaterialDesignExtensions" xmlns:s="https://github.com/canton7/Stylet" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" + xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" mc:Ignorable="d" Title="Plugin configuration" Background="{DynamicResource MaterialDesignPaper}" @@ -25,7 +26,7 @@ - + diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/LayoutStepViewModel.cs b/src/Artemis.UI/Screens/StartupWizard/Steps/LayoutStepViewModel.cs index 4977b25b2..701451d48 100644 --- a/src/Artemis.UI/Screens/StartupWizard/Steps/LayoutStepViewModel.cs +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/LayoutStepViewModel.cs @@ -25,5 +25,23 @@ namespace Artemis.UI.Screens.StartupWizard.Steps StartupWizardViewModel startupWizardViewModel = (StartupWizardViewModel) Parent; startupWizardViewModel.Continue(); } + + #region Overrides of Screen + + /// + protected override void OnActivate() + { + _rgbService.IsRenderPaused = true; + base.OnActivate(); + } + + /// + protected override void OnDeactivate() + { + _rgbService.IsRenderPaused = false; + base.OnDeactivate(); + } + + #endregion } } \ No newline at end of file diff --git a/src/Artemis.UI/Services/RegistrationService.cs b/src/Artemis.UI/Services/RegistrationService.cs index 9cbda99e6..f8c4f8733 100644 --- a/src/Artemis.UI/Services/RegistrationService.cs +++ b/src/Artemis.UI/Services/RegistrationService.cs @@ -106,7 +106,7 @@ namespace Artemis.UI.Services public void RegisterControllers() { - _webServerService.AddController(); + _webServerService.AddController(Constants.CorePlugin.Features.First().Instance!); } ///