diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings index d42d584d5..c23015925 100644 --- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings +++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings @@ -93,5 +93,6 @@ True True True - True - True \ No newline at end of file + True + True + True \ No newline at end of file diff --git a/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs b/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs index c88cbd625..b108d81f1 100644 --- a/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs +++ b/src/Artemis.Core/DefaultTypes/Properties/ColorGradientLayerProperty.cs @@ -1,13 +1,8 @@ -using System.Collections.Specialized; -using SkiaSharp; - -namespace Artemis.Core; +namespace Artemis.Core; /// public class ColorGradientLayerProperty : LayerProperty { - private ColorGradient? _subscribedGradient; - internal ColorGradientLayerProperty() { KeyframesSupported = false; @@ -22,29 +17,6 @@ public class ColorGradientLayerProperty : LayerProperty return p.CurrentValue; } - #region Overrides of LayerProperty - - /// - protected override void OnCurrentValueSet() - { - // Don't allow color gradients to be null - if (BaseValue == null!) - BaseValue = new ColorGradient(DefaultValue); - - if (!ReferenceEquals(_subscribedGradient, BaseValue)) - { - if (_subscribedGradient != null) - _subscribedGradient.CollectionChanged -= SubscribedGradientOnPropertyChanged; - _subscribedGradient = BaseValue; - _subscribedGradient.CollectionChanged += SubscribedGradientOnPropertyChanged; - } - - CreateDataBindingRegistrations(); - base.OnCurrentValueSet(); - } - - #endregion - /// protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased) { @@ -60,33 +32,12 @@ public class ColorGradientLayerProperty : LayerProperty if (BaseValue == null!) BaseValue = new ColorGradient(DefaultValue); - base.OnInitialize(); + DataBinding.RegisterDataBindingProperty(() => CurrentValue, value => + { + if (value != null) + CurrentValue = value; + }, "Value"); } #endregion - - private void CreateDataBindingRegistrations() - { - DataBinding.ClearDataBindingProperties(); - if (CurrentValue == null!) - return; - - for (int index = 0; index < CurrentValue.Count; index++) - { - int stopIndex = index; - - void Setter(SKColor value) - { - CurrentValue[stopIndex].Color = value; - } - - DataBinding.RegisterDataBindingProperty(() => CurrentValue[stopIndex].Color, Setter, $"Color #{stopIndex + 1}"); - } - } - - private void SubscribedGradientOnPropertyChanged(object? sender, NotifyCollectionChangedEventArgs args) - { - if (CurrentValue.Count != DataBinding.Properties.Count) - CreateDataBindingRegistrations(); - } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs b/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs index d39a0f7f5..f198b5397 100644 --- a/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs +++ b/src/Artemis.Core/Models/Profile/Conditions/EventCondition.cs @@ -1,7 +1,7 @@ using System; +using System.Collections.Generic; using System.Linq; using Artemis.Core.Internal; -using Artemis.Core.VisualScripting.Internal; using Artemis.Storage.Entities.Profile.Abstract; using Artemis.Storage.Entities.Profile.Conditions; @@ -14,12 +14,12 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition { private readonly string _displayName; private readonly EventConditionEntity _entity; + private NodeScript _script; + private DefaultNode _startNode; private DataModelPath? _eventPath; private DateTime _lastProcessedTrigger; private object? _lastProcessedValue; private EventOverlapMode _overlapMode; - private NodeScript _script; - private IEventConditionNode _startNode; private EventToggleOffMode _toggleOffMode; private EventTriggerMode _triggerMode; private bool _wasMet; @@ -34,7 +34,7 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition _entity = new EventConditionEntity(); _displayName = profileElement.GetType().Name; _startNode = new EventConditionEventStartNode {X = -300}; - _script = new NodeScript($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile); + _script = new NodeScript($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile, new List {_startNode}); } internal EventCondition(EventConditionEntity entity, RenderProfileElement profileElement) @@ -96,109 +96,6 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition set => SetAndNotify(ref _toggleOffMode, value); } - /// - /// Updates the event node, applying the selected event - /// - public void UpdateEventNode() - { - if (EventPath == null) - return; - - Type? pathType = EventPath.GetPropertyType(); - if (pathType == null) - return; - - // Create an event node if the path type is a data model event - if (pathType.IsAssignableTo(typeof(IDataModelEvent))) - { - EventConditionEventStartNode eventNode; - // Ensure the start node is an event node - if (_startNode is not EventConditionEventStartNode node) - { - eventNode = new EventConditionEventStartNode(); - ReplaceStartNode(eventNode); - _startNode = eventNode; - } - else - { - eventNode = node; - } - - IDataModelEvent? dataModelEvent = EventPath?.GetValue() as IDataModelEvent; - eventNode.CreatePins(dataModelEvent); - } - // Create a value changed node if the path type is a regular value - else - { - // Ensure the start nod is a value changed node - EventConditionValueChangedStartNode valueChangedNode; - // Ensure the start node is an event node - if (_startNode is not EventConditionValueChangedStartNode node) - { - valueChangedNode = new EventConditionValueChangedStartNode(); - ReplaceStartNode(valueChangedNode); - } - else - { - valueChangedNode = node; - } - - valueChangedNode.UpdateOutputPins(EventPath); - } - - if (!Script.Nodes.Contains(_startNode)) - Script.AddNode(_startNode); - Script.Save(); - } - - /// - /// Gets the start node of the event script, if any - /// - /// The start node of the event script, if any. - public INode GetStartNode() - { - return _startNode; - } - - private void ReplaceStartNode(IEventConditionNode newStartNode) - { - if (Script.Nodes.Contains(_startNode)) - Script.RemoveNode(_startNode); - - _startNode = newStartNode; - if (!Script.Nodes.Contains(_startNode)) - Script.AddNode(_startNode); - } - - private bool Evaluate() - { - if (EventPath == null) - return false; - - object? value = EventPath.GetValue(); - if (_startNode is EventConditionEventStartNode) - { - if (value is not IDataModelEvent dataModelEvent || dataModelEvent.LastTrigger <= _lastProcessedTrigger) - return false; - - _lastProcessedTrigger = dataModelEvent.LastTrigger; - } - else if (_startNode is EventConditionValueChangedStartNode valueChangedNode) - { - if (Equals(value, _lastProcessedValue)) - return false; - - valueChangedNode.UpdateValues(value, _lastProcessedValue); - _lastProcessedValue = value; - } - - if (!Script.ExitNodeConnected) - return true; - - Script.Run(); - return Script.Result; - } - /// public IConditionEntity Entity => _entity; @@ -264,6 +161,116 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition Script?.Dispose(); EventPath?.Dispose(); } + + /// + /// Updates the event node, applying the selected event + /// + public void UpdateEventNode(bool updateScript) + { + if (EventPath == null) + return; + + Type? pathType = EventPath.GetPropertyType(); + if (pathType == null) + return; + + // Create an event node if the path type is a data model event + if (pathType.IsAssignableTo(typeof(IDataModelEvent))) + { + EventConditionEventStartNode eventNode; + // Ensure the start node is an event node + if (_startNode is not EventConditionEventStartNode node) + { + eventNode = new EventConditionEventStartNode(); + if (updateScript) + ReplaceStartNode(eventNode); + _startNode = eventNode; + } + else + { + eventNode = node; + } + + IDataModelEvent? dataModelEvent = EventPath?.GetValue() as IDataModelEvent; + eventNode.CreatePins(dataModelEvent); + } + // Create a value changed node if the path type is a regular value + else + { + // Ensure the start nod is a value changed node + EventConditionValueChangedStartNode valueChangedNode; + // Ensure the start node is an event node + if (_startNode is not EventConditionValueChangedStartNode node) + { + valueChangedNode = new EventConditionValueChangedStartNode(); + if (updateScript) + ReplaceStartNode(valueChangedNode); + _startNode = valueChangedNode; + } + else + { + valueChangedNode = node; + } + + valueChangedNode.UpdateOutputPins(EventPath); + } + + // Script can be null if called before load + if (!updateScript) + return; + if (!Script.Nodes.Contains(_startNode)) + { + Script.AddNode(_startNode); + Script.LoadConnections(); + } + Script.Save(); + } + + /// + /// Gets the start node of the event script, if any + /// + /// The start node of the event script, if any. + public INode GetStartNode() + { + return _startNode; + } + + private void ReplaceStartNode(DefaultNode newStartNode) + { + if (Script.Nodes.Contains(_startNode)) + Script.RemoveNode(_startNode); + if (!Script.Nodes.Contains(newStartNode)) + Script.AddNode(newStartNode); + } + + private bool Evaluate() + { + if (EventPath == null) + return false; + + object? value = EventPath.GetValue(); + if (_startNode is EventConditionEventStartNode) + { + if (value is not IDataModelEvent dataModelEvent || dataModelEvent.LastTrigger <= _lastProcessedTrigger) + return false; + + _lastProcessedTrigger = dataModelEvent.LastTrigger; + } + else if (_startNode is EventConditionValueChangedStartNode valueChangedNode) + { + if (Equals(value, _lastProcessedValue)) + return false; + + valueChangedNode.UpdateValues(value, _lastProcessedValue); + _lastProcessedValue = value; + } + + if (!Script.ExitNodeConnected) + return true; + + Script.Run(); + return Script.Result; + } #region Storage @@ -276,11 +283,13 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition if (_entity.EventPath != null) EventPath = new DataModelPath(_entity.EventPath); - + UpdateEventNode(false); + + string name = $"Activate {_displayName}"; + string description = $"Whether or not the event should activate the {_displayName}"; Script = _entity.Script != null - ? new NodeScript($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", _entity.Script, ProfileElement.Profile) - : new NodeScript($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile); - UpdateEventNode(); + ? new NodeScript(name, description, _entity.Script, ProfileElement.Profile, new List {_startNode}) + : new NodeScript(name, description, ProfileElement.Profile, new List {_startNode}); } /// @@ -311,71 +320,9 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition /// public void LoadNodeScript() { + UpdateEventNode(true); Script.Load(); - - // The load action may have created an event node, use that one over the one we have here - INode? existingEventNode = Script.Nodes.FirstOrDefault(n => n.Id == EventConditionEventStartNode.NodeId || n.Id == EventConditionValueChangedStartNode.NodeId); - if (existingEventNode != null) - _startNode = (IEventConditionNode) existingEventNode; - - UpdateEventNode(); - Script.LoadConnections(); } #endregion -} - -/// -/// Represents a mode for render elements to start their timeline when display conditions events are fired. -/// -public enum EventTriggerMode -{ - /// - /// Play the timeline once. - /// - Play, - - /// - /// Toggle repeating the timeline. - /// - Toggle -} - -/// -/// Represents a mode for render elements to configure the behaviour of events that overlap i.e. trigger again before -/// the timeline finishes. -/// -public enum EventOverlapMode -{ - /// - /// Stop the current run and restart the timeline - /// - Restart, - - /// - /// Play another copy of the timeline on top of the current run - /// - Copy, - - /// - /// Ignore subsequent event fires until the timeline finishes - /// - Ignore -} - -/// -/// Represents a mode for render elements when toggling off the event when using -/// . -/// -public enum EventToggleOffMode -{ - /// - /// When the event toggles the condition off, finish the the current run of the main timeline - /// - Finish, - - /// - /// When the event toggles the condition off, skip to the end segment of the timeline - /// - SkipToEnd } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/EventOverlapMode.cs b/src/Artemis.Core/Models/Profile/Conditions/EventOverlapMode.cs new file mode 100644 index 000000000..56068a797 --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/EventOverlapMode.cs @@ -0,0 +1,23 @@ +namespace Artemis.Core; + +/// +/// Represents a mode for render elements to configure the behaviour of events that overlap i.e. trigger again before +/// the timeline finishes. +/// +public enum EventOverlapMode +{ + /// + /// Stop the current run and restart the timeline + /// + Restart, + + /// + /// Play another copy of the timeline on top of the current run + /// + Copy, + + /// + /// Ignore subsequent event fires until the timeline finishes + /// + Ignore +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/EventToggleOffMode.cs b/src/Artemis.Core/Models/Profile/Conditions/EventToggleOffMode.cs new file mode 100644 index 000000000..c8db3aa6a --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/EventToggleOffMode.cs @@ -0,0 +1,18 @@ +namespace Artemis.Core; + +/// +/// Represents a mode for render elements when toggling off the event when using +/// . +/// +public enum EventToggleOffMode +{ + /// + /// When the event toggles the condition off, finish the the current run of the main timeline + /// + Finish, + + /// + /// When the event toggles the condition off, skip to the end segment of the timeline + /// + SkipToEnd +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/Conditions/EventTriggerMode.cs b/src/Artemis.Core/Models/Profile/Conditions/EventTriggerMode.cs new file mode 100644 index 000000000..cfe31816d --- /dev/null +++ b/src/Artemis.Core/Models/Profile/Conditions/EventTriggerMode.cs @@ -0,0 +1,17 @@ +namespace Artemis.Core; + +/// +/// Represents a mode for render elements to start their timeline when display conditions events are fired. +/// +public enum EventTriggerMode +{ + /// + /// Play the timeline once. + /// + Play, + + /// + /// Toggle repeating the timeline. + /// + Toggle +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingProperty.cs b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingProperty.cs index 13a5c9e27..81c4b719a 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/DataBindingProperty.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/DataBindingProperty.cs @@ -35,7 +35,7 @@ public class DataBindingProperty : IDataBindingProperty } /// - public void SetValue(object? value) + public void SetValue(object value) { // Numeric has a bunch of conversion, this seems the cheapest way to use them :) switch (value) diff --git a/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingProperty.cs b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingProperty.cs index 7d5dc49bd..7e4ce8d06 100644 --- a/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingProperty.cs +++ b/src/Artemis.Core/Models/Profile/DataBindings/IDataBindingProperty.cs @@ -27,5 +27,5 @@ public interface IDataBindingProperty /// Sets the value of the property this registration points to /// /// A value matching the type of - void SetValue(object? value); + void SetValue(object value); } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/PluginFeatureAttribute.cs b/src/Artemis.Core/Plugins/PluginFeatureAttribute.cs index 19acfa645..cf36770e1 100644 --- a/src/Artemis.Core/Plugins/PluginFeatureAttribute.cs +++ b/src/Artemis.Core/Plugins/PluginFeatureAttribute.cs @@ -22,6 +22,7 @@ public class PluginFeatureAttribute : Attribute /// The plugins display icon that's shown in the settings see for /// available icons /// + [Obsolete("Feature icons are no longer shown in the UI.")] public string? Icon { get; set; } /// diff --git a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs index 35798a064..32d5775ec 100644 --- a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs +++ b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs @@ -31,20 +31,7 @@ public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject Name = attribute?.Name ?? featureType.Name.Humanize(LetterCasing.Title); Description = attribute?.Description; - Icon = attribute?.Icon; AlwaysEnabled = attribute?.AlwaysEnabled ?? false; - - if (Icon != null) return; - if (typeof(DeviceProvider).IsAssignableFrom(featureType)) - Icon = "Devices"; - else if (typeof(Module).IsAssignableFrom(featureType)) - Icon = "VectorRectangle"; - else if (typeof(LayerBrushProvider).IsAssignableFrom(featureType)) - Icon = "Brush"; - else if (typeof(LayerEffectProvider).IsAssignableFrom(featureType)) - Icon = "AutoAwesome"; - else - Icon = "Plugin"; } internal PluginFeatureInfo(Plugin plugin, PluginFeatureAttribute? attribute, PluginFeature instance) @@ -56,19 +43,8 @@ public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject Name = attribute?.Name ?? instance.GetType().Name.Humanize(LetterCasing.Title); Description = attribute?.Description; - Icon = attribute?.Icon; AlwaysEnabled = attribute?.AlwaysEnabled ?? false; Instance = instance; - - if (Icon != null) return; - Icon = Instance switch - { - DeviceProvider => "Devices", - Module => "VectorRectangle", - LayerBrushProvider => "Brush", - LayerEffectProvider => "AutoAwesome", - _ => "Plugin" - }; } /// @@ -110,17 +86,6 @@ public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject set => SetAndNotify(ref _description, value); } - /// - /// The plugins display icon that's shown in the settings see for - /// available icons - /// - [JsonProperty] - public string? Icon - { - get => _icon; - set => SetAndNotify(ref _icon, value); - } - /// /// Marks the feature to always be enabled as long as the plugin is enabled and cannot be disabled. /// Note: always if this is the plugin's only feature @@ -142,20 +107,7 @@ public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject get => _instance; internal set => SetAndNotify(ref _instance, value); } - - /// - /// Gets a string representing either a full path pointing to an svg or the markdown icon - /// - public string? ResolvedIcon - { - get - { - if (Icon == null) - return null; - return Icon.Contains('.') ? Plugin.ResolveRelativePath(Icon) : Icon; - } - } - + internal PluginFeatureEntity Entity { get; } /// diff --git a/src/Artemis.Core/Services/Input/InputProvider.cs b/src/Artemis.Core/Services/Input/InputProvider.cs index f04b1a1f9..c51a25d63 100644 --- a/src/Artemis.Core/Services/Input/InputProvider.cs +++ b/src/Artemis.Core/Services/Input/InputProvider.cs @@ -8,6 +8,13 @@ namespace Artemis.Core.Services; /// public abstract class InputProvider : IDisposable { + public InputProvider() + { + ProviderName = GetType().FullName ?? throw new InvalidOperationException("Input provider must have a type with a name"); + } + + internal string ProviderName { get; set; } + /// /// Called when the input service requests a event /// diff --git a/src/Artemis.Core/Services/Input/InputService.cs b/src/Artemis.Core/Services/Input/InputService.cs index 187f0db4b..48ae607f6 100644 --- a/src/Artemis.Core/Services/Input/InputService.cs +++ b/src/Artemis.Core/Services/Input/InputService.cs @@ -10,12 +10,18 @@ internal class InputService : IInputService { private readonly ILogger _logger; private readonly IRgbService _rgbService; + private ArtemisDevice? _firstKeyboard; + private ArtemisDevice? _firstMouse; + private int _keyboardCount; + private int _mouseCount; public InputService(ILogger logger, IRgbService rgbService) { _logger = logger; _rgbService = rgbService; + _rgbService.DeviceAdded += RgbServiceOnDevicesModified; + _rgbService.DeviceRemoved += RgbServiceOnDevicesModified; BustIdentifierCache(); } @@ -132,8 +138,6 @@ internal class InputService : IInputService private readonly Dictionary, ArtemisDevice> _deviceCache = new(); private List _devices = new(); - private ArtemisDevice? _cachedFallbackKeyboard; - private ArtemisDevice? _cachedFallbackMouse; private ArtemisDevice? _identifyingDevice; public void IdentifyDevice(ArtemisDevice device) @@ -164,13 +168,29 @@ internal class InputService : IInputService if (provider == null) throw new ArgumentNullException(nameof(provider)); if (identifier == null) throw new ArgumentNullException(nameof(identifier)); + // We will almost always only have zero or one of each + if (type == InputDeviceType.Keyboard) + { + if (_keyboardCount == 0) + return null; + if (_keyboardCount == 1) + return _firstKeyboard; + } + + if (type == InputDeviceType.Mouse) + { + if (_mouseCount == 0) + return null; + if (_mouseCount == 1) + return _firstMouse; + } + // Try cache first ArtemisDevice? cacheMatch = GetDeviceFromCache(provider, identifier); if (cacheMatch != null) return cacheMatch; - string providerName = provider.GetType().FullName!; - ArtemisDevice? match = _devices.FirstOrDefault(m => m.InputIdentifiers.Any(i => Equals(i.InputProvider, providerName) && Equals(i.Identifier, identifier))); + ArtemisDevice? match = _devices.FirstOrDefault(m => m.InputIdentifiers.Any(i => Equals(i.InputProvider, provider.ProviderName) && Equals(i.Identifier, identifier))); // If a match was found cache it to speed up the next event and return the match if (match != null) @@ -179,34 +199,16 @@ internal class InputService : IInputService return match; } - // If there is no match, apply our fallback type - if (type == InputDeviceType.None) - return null; if (type == InputDeviceType.Keyboard) - { - if (_cachedFallbackKeyboard != null) - return _cachedFallbackKeyboard; - _cachedFallbackKeyboard = _rgbService.EnabledDevices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Keyboard); - return _cachedFallbackKeyboard; - } - + return _firstKeyboard; if (type == InputDeviceType.Mouse) - { - if (_cachedFallbackMouse != null) - return _cachedFallbackMouse; - _cachedFallbackMouse = _rgbService.EnabledDevices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Mouse); - return _cachedFallbackMouse; - } - + return _firstMouse; return null; } public void BustIdentifierCache() { _deviceCache.Clear(); - _cachedFallbackKeyboard = null; - _cachedFallbackMouse = null; - _devices = _rgbService.EnabledDevices.Where(d => d.InputIdentifiers.Any()).ToList(); } @@ -220,12 +222,7 @@ internal class InputService : IInputService _deviceCache.TryGetValue(new Tuple(provider, identifier), out ArtemisDevice? device); return device; } - - private void SurfaceConfigurationChanged(object? sender, SurfaceConfigurationEventArgs e) - { - BustIdentifierCache(); - } - + private void InputProviderOnIdentifierReceived(object? sender, InputProviderIdentifierEventArgs e) { // Don't match if there is no device or if the device type differs from the event device type @@ -236,16 +233,24 @@ internal class InputService : IInputService if (!(sender is InputProvider inputProvider)) return; - string providerName = inputProvider.GetType().FullName!; - // Remove existing identification - _identifyingDevice.InputIdentifiers.RemoveAll(i => i.InputProvider == providerName); - _identifyingDevice.InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(providerName, e.Identifier)); + _identifyingDevice.InputIdentifiers.RemoveAll(i => i.InputProvider == inputProvider.ProviderName); + _identifyingDevice.InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(inputProvider.ProviderName, e.Identifier)); StopIdentify(); OnDeviceIdentified(); } + private void RgbServiceOnDevicesModified(object? sender, DeviceEventArgs args) + { + _firstKeyboard = _rgbService.Devices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Keyboard); + _firstMouse = _rgbService.Devices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Mouse); + _keyboardCount = _rgbService.Devices.Count(d => d.DeviceType == RGBDeviceType.Keyboard); + _mouseCount = _rgbService.Devices.Count(d => d.DeviceType == RGBDeviceType.Mouse); + + BustIdentifierCache(); + } + #endregion #region Keyboard @@ -379,6 +384,7 @@ internal class InputService : IInputService private readonly HashSet _pressedButtons = new(); + private void InputProviderOnMouseButtonDataReceived(object? sender, InputProviderMouseButtonEventArgs e) { bool foundLedId = InputKeyUtilities.MouseButtonLedIdMap.TryGetValue(e.Button, out LedId ledId); diff --git a/src/Artemis.Core/VisualScripting/Internal/DataBindingExitNode.cs b/src/Artemis.Core/VisualScripting/Internal/DataBindingExitNode.cs index 6b4d367f5..7857f533d 100644 --- a/src/Artemis.Core/VisualScripting/Internal/DataBindingExitNode.cs +++ b/src/Artemis.Core/VisualScripting/Internal/DataBindingExitNode.cs @@ -23,7 +23,10 @@ internal class DataBindingExitNode : Node, IExitNode public void ApplyToDataBinding() { foreach ((IDataBindingProperty? property, object? pendingValue) in _propertyValues) - property.SetValue(pendingValue); + { + if (pendingValue != null) + property.SetValue(pendingValue); + } } public override void Evaluate() diff --git a/src/Artemis.Core/VisualScripting/Internal/EventConditionEventStartNode.cs b/src/Artemis.Core/VisualScripting/Internal/EventConditionEventStartNode.cs index 21a264caa..14f1e0562 100644 --- a/src/Artemis.Core/VisualScripting/Internal/EventConditionEventStartNode.cs +++ b/src/Artemis.Core/VisualScripting/Internal/EventConditionEventStartNode.cs @@ -1,23 +1,20 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using Artemis.Core.Modules; -using Artemis.Core.VisualScripting.Internal; -using Humanizer; namespace Artemis.Core.Internal; -internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode +internal class EventConditionEventStartNode : DefaultNode { internal static readonly Guid NodeId = new("278735FE-69E9-4A73-A6B8-59E83EE19305"); - private readonly Dictionary, OutputPin> _propertyPins; + private readonly ObjectOutputPins _objectOutputPins; private IDataModelEvent? _dataModelEvent; public EventConditionEventStartNode() : base(NodeId, "Event Arguments", "Contains the event arguments that triggered the evaluation") { - _propertyPins = new Dictionary, OutputPin>(); + _objectOutputPins = new ObjectOutputPins(this); + } + + public void SetDataModelEvent(IDataModelEvent? dataModelEvent) + { } public void CreatePins(IDataModelEvent? dataModelEvent) @@ -25,30 +22,8 @@ internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode if (_dataModelEvent == dataModelEvent) return; - while (Pins.Any()) - RemovePin((Pin) Pins.First()); - _propertyPins.Clear(); - _dataModelEvent = dataModelEvent; - if (dataModelEvent == null) - return; - - foreach (PropertyInfo propertyInfo in dataModelEvent.ArgumentsType.GetProperties(BindingFlags.Instance | BindingFlags.Public) - .Where(p => p.CustomAttributes.All(a => a.AttributeType != typeof(DataModelIgnoreAttribute)))) - { - // Expect an IDataModelEvent - ParameterExpression eventParameter = Expression.Parameter(typeof(DataModelEventArgs), "event"); - // Cast it to the actual event type - UnaryExpression eventCast = Expression.Convert(eventParameter, propertyInfo.DeclaringType!); - // Access the property - MemberExpression accessor = Expression.Property(eventCast, propertyInfo); - // Cast the property to an object (sadly boxing) - UnaryExpression objectCast = Expression.Convert(accessor, typeof(object)); - // Compile the resulting expression - Func expression = Expression.Lambda>(objectCast, eventParameter).Compile(); - - _propertyPins.Add(expression, CreateOrAddOutputPin(propertyInfo.PropertyType, propertyInfo.Name.Humanize())); - } + _objectOutputPins.ChangeType(dataModelEvent?.ArgumentsType); } public override void Evaluate() @@ -56,12 +31,6 @@ internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode if (_dataModelEvent?.LastEventArgumentsUntyped == null) return; - foreach ((Func propertyAccessor, OutputPin outputPin) in _propertyPins) - { - if (!outputPin.ConnectedTo.Any()) - continue; - object value = _dataModelEvent.LastEventArgumentsUntyped != null ? propertyAccessor(_dataModelEvent.LastEventArgumentsUntyped) : outputPin.Type.GetDefault()!; - outputPin.Value = outputPin.IsNumeric ? new Numeric(value) : value; - } + _objectOutputPins.SetCurrentValue(_dataModelEvent.LastEventArgumentsUntyped); } } \ No newline at end of file diff --git a/src/Artemis.Core/VisualScripting/Internal/EventConditionValueChangedStartNode.cs b/src/Artemis.Core/VisualScripting/Internal/EventConditionValueChangedStartNode.cs index abf571979..c334681bc 100644 --- a/src/Artemis.Core/VisualScripting/Internal/EventConditionValueChangedStartNode.cs +++ b/src/Artemis.Core/VisualScripting/Internal/EventConditionValueChangedStartNode.cs @@ -1,9 +1,8 @@ using System; -using Artemis.Core.VisualScripting.Internal; namespace Artemis.Core.Internal; -internal class EventConditionValueChangedStartNode : DefaultNode, IEventConditionNode +internal class EventConditionValueChangedStartNode : DefaultNode { internal static readonly Guid NodeId = new("F9A270DB-A231-4800-BAB3-DC1F96856756"); private object? _newValue; diff --git a/src/Artemis.Core/VisualScripting/Internal/IEventConditionNode.cs b/src/Artemis.Core/VisualScripting/Internal/IEventConditionNode.cs deleted file mode 100644 index e47ad33a7..000000000 --- a/src/Artemis.Core/VisualScripting/Internal/IEventConditionNode.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Artemis.Core.VisualScripting.Internal; - -internal interface IEventConditionNode : INode -{ -} \ No newline at end of file diff --git a/src/Artemis.Core/VisualScripting/NodeScript.cs b/src/Artemis.Core/VisualScripting/NodeScript.cs index 1c77626f8..7a02555ac 100644 --- a/src/Artemis.Core/VisualScripting/NodeScript.cs +++ b/src/Artemis.Core/VisualScripting/NodeScript.cs @@ -35,7 +35,10 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript #region Properties & Fields - internal NodeScriptEntity Entity { get; private set; } + /// + /// Gets the entity used to store this script. + /// + public NodeScriptEntity Entity { get; private set; } /// public string Name { get; } @@ -77,7 +80,8 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript /// The context of the node script, usually a or /// /// - protected NodeScript(string name, string description, object? context = null) + /// A list of default nodes to add to the node script. + protected NodeScript(string name, string description, object? context = null, List? defaultNodes = null) { Name = name; Description = description; @@ -87,9 +91,13 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript NodeTypeStore.NodeTypeAdded += NodeTypeStoreOnNodeTypeAdded; NodeTypeStore.NodeTypeRemoved += NodeTypeStoreOnNodeTypeRemoved; + + if (defaultNodes != null) + foreach (DefaultNode defaultNode in defaultNodes) + AddNode(defaultNode); } - internal NodeScript(string name, string description, NodeScriptEntity entity, object? context = null) + internal NodeScript(string name, string description, NodeScriptEntity entity, object? context = null, List? defaultNodes = null) { Name = name; Description = description; @@ -99,6 +107,10 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript NodeTypeStore.NodeTypeAdded += NodeTypeStoreOnNodeTypeAdded; NodeTypeStore.NodeTypeRemoved += NodeTypeStoreOnNodeTypeRemoved; + + if (defaultNodes != null) + foreach (DefaultNode defaultNode in defaultNodes) + AddNode(defaultNode); } #endregion @@ -410,8 +422,9 @@ public class NodeScript : NodeScript, INodeScript #region Constructors - internal NodeScript(string name, string description, NodeScriptEntity entity, object? context = null) - : base(name, description, entity, context) + /// + public NodeScript(string name, string description, NodeScriptEntity entity, object? context = null, List? defaultNodes = null) + : base(name, description, entity, context, defaultNodes) { ExitNode = new ExitNode(name, description); AddNode(ExitNode); @@ -420,8 +433,8 @@ public class NodeScript : NodeScript, INodeScript } /// - public NodeScript(string name, string description, object? context = null) - : base(name, description, context) + public NodeScript(string name, string description, object? context = null, List? defaultNodes = null) + : base(name, description, context, defaultNodes) { ExitNode = new ExitNode(name, description); AddNode(ExitNode); diff --git a/src/Artemis.Core/VisualScripting/CustomNodeViewModel.cs b/src/Artemis.Core/VisualScripting/Nodes/CustomNodeViewModel.cs similarity index 100% rename from src/Artemis.Core/VisualScripting/CustomNodeViewModel.cs rename to src/Artemis.Core/VisualScripting/Nodes/CustomNodeViewModel.cs diff --git a/src/Artemis.Core/VisualScripting/Nodes/CustomNodeViewModelPosition.cs b/src/Artemis.Core/VisualScripting/Nodes/CustomNodeViewModelPosition.cs new file mode 100644 index 000000000..74616dd8e --- /dev/null +++ b/src/Artemis.Core/VisualScripting/Nodes/CustomNodeViewModelPosition.cs @@ -0,0 +1,32 @@ +namespace Artemis.Core; + +/// +/// Represents the position of a node's custom view model. +/// +public enum CustomNodeViewModelPosition +{ + /// + /// Puts the view model above the pins. + /// + AbovePins, + + /// + /// Puts the view model between the pins, vertically aligned to the top. + /// + BetweenPinsTop, + + /// + /// Puts the view model between the pins, vertically aligned to the center. + /// + BetweenPinsCenter, + + /// + /// Puts the view model between the pins, vertically aligned to the bottom. + /// + BetweenPinsBottom, + + /// + /// Puts the view model below the pins. + /// + BelowPins +} \ No newline at end of file diff --git a/src/Artemis.Core/VisualScripting/Internal/DefaultNode.cs b/src/Artemis.Core/VisualScripting/Nodes/DefaultNode.cs similarity index 60% rename from src/Artemis.Core/VisualScripting/Internal/DefaultNode.cs rename to src/Artemis.Core/VisualScripting/Nodes/DefaultNode.cs index 103aa4e12..733b4e97b 100644 --- a/src/Artemis.Core/VisualScripting/Internal/DefaultNode.cs +++ b/src/Artemis.Core/VisualScripting/Nodes/DefaultNode.cs @@ -1,18 +1,11 @@ using System; -namespace Artemis.Core.Internal; - -/// -/// Represents a kind of node that cannot be deleted inside a . -/// -public interface IDefaultNode : INode -{ -} +namespace Artemis.Core; /// /// Represents a kind of node that cannot be deleted inside a . /// -public abstract class DefaultNode : Node, IDefaultNode +public abstract class DefaultNode : Node { #region Constructors @@ -20,8 +13,6 @@ public abstract class DefaultNode : Node, IDefaultNode protected DefaultNode(Guid id, string name, string description = "") : base(name, description) { Id = id; - Name = name; - Description = description; } #endregion diff --git a/src/Artemis.Core/VisualScripting/Nodes/ICustomViewModelNode.cs b/src/Artemis.Core/VisualScripting/Nodes/ICustomViewModelNode.cs new file mode 100644 index 000000000..b16623935 --- /dev/null +++ b/src/Artemis.Core/VisualScripting/Nodes/ICustomViewModelNode.cs @@ -0,0 +1,18 @@ +namespace Artemis.Core; + +/// +/// Represents a node that has a custom view model. +/// +public interface ICustomViewModelNode +{ + /// + /// Gets or sets the position of the node's custom view model. + /// + CustomNodeViewModelPosition ViewModelPosition { get; } + + /// + /// Called whenever the node must show it's custom view model, if , no custom view model is used + /// + /// The custom view model, if , no custom view model is used + ICustomNodeViewModel? GetCustomViewModel(NodeScript nodeScript); +} \ No newline at end of file diff --git a/src/Artemis.Core/VisualScripting/Node.cs b/src/Artemis.Core/VisualScripting/Nodes/Node.cs similarity index 71% rename from src/Artemis.Core/VisualScripting/Node.cs rename to src/Artemis.Core/VisualScripting/Nodes/Node.cs index 7e2161580..e451f05aa 100644 --- a/src/Artemis.Core/VisualScripting/Node.cs +++ b/src/Artemis.Core/VisualScripting/Nodes/Node.cs @@ -2,10 +2,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Reflection; using Artemis.Core.Events; -using Ninject; -using Ninject.Parameters; namespace Artemis.Core; @@ -132,7 +129,7 @@ public abstract class Node : BreakableModel, INode /// The name of the pin /// The type of value the pin will hold /// The newly created pin - protected InputPin CreateInputPin(string name = "") + public InputPin CreateInputPin(string name = "") { InputPin pin = new(this, name); _pins.Add(pin); @@ -146,7 +143,7 @@ public abstract class Node : BreakableModel, INode /// The type of value the pin will hold /// The name of the pin /// The newly created pin - protected InputPin CreateInputPin(Type type, string name = "") + public InputPin CreateInputPin(Type type, string name = "") { InputPin pin = new(this, type, name); _pins.Add(pin); @@ -160,7 +157,7 @@ public abstract class Node : BreakableModel, INode /// The name of the pin /// The type of value the pin will hold /// The newly created pin - protected OutputPin CreateOutputPin(string name = "") + public OutputPin CreateOutputPin(string name = "") { OutputPin pin = new(this, name); _pins.Add(pin); @@ -174,7 +171,7 @@ public abstract class Node : BreakableModel, INode /// The type of value the pin will hold /// The name of the pin /// The newly created pin - protected OutputPin CreateOutputPin(Type type, string name = "") + public OutputPin CreateOutputPin(Type type, string name = "") { OutputPin pin = new(this, type, name); _pins.Add(pin); @@ -187,7 +184,7 @@ public abstract class Node : BreakableModel, INode /// The bucket might grow a bit over time as the user edits the node but pins won't get lost, enabling undo/redo in the /// editor. /// - protected OutputPin CreateOrAddOutputPin(Type valueType, string displayName) + public OutputPin CreateOrAddOutputPin(Type valueType, string displayName) { // Grab the first pin from the bucket that isn't on the node yet OutputPin? pin = _outputPinBucket.FirstOrDefault(p => !Pins.Contains(p)); @@ -217,7 +214,7 @@ public abstract class Node : BreakableModel, INode /// The bucket might grow a bit over time as the user edits the node but pins won't get lost, enabling undo/redo in the /// editor. /// - protected InputPin CreateOrAddInputPin(Type valueType, string displayName) + public InputPin CreateOrAddInputPin(Type valueType, string displayName) { // Grab the first pin from the bucket that isn't on the node yet InputPin? pin = _inputPinBucket.FirstOrDefault(p => !Pins.Contains(p)); @@ -247,7 +244,7 @@ public abstract class Node : BreakableModel, INode /// /// The pin to remove /// if the pin was removed; otherwise . - protected bool RemovePin(Pin pin) + public bool RemovePin(Pin pin) { bool isRemoved = _pins.Remove(pin); if (isRemoved) @@ -263,7 +260,7 @@ public abstract class Node : BreakableModel, INode /// Adds an existing to the collection. /// /// The pin to add - protected void AddPin(Pin pin) + public void AddPin(Pin pin) { if (pin.Node != this) throw new ArtemisCoreException("Can't add a pin to a node that belongs to a different node than the one it's being added to."); @@ -281,7 +278,7 @@ public abstract class Node : BreakableModel, INode /// The name of the pin collection /// The amount of pins to initially add to the collection /// The resulting input pin collection - protected InputPinCollection CreateInputPinCollection(string name = "", int initialCount = 1) + public InputPinCollection CreateInputPinCollection(string name = "", int initialCount = 1) { InputPinCollection pin = new(this, name, initialCount); _pinCollections.Add(pin); @@ -296,7 +293,7 @@ public abstract class Node : BreakableModel, INode /// The name of the pin collection /// The amount of pins to initially add to the collection /// The resulting input pin collection - protected InputPinCollection CreateInputPinCollection(Type type, string name = "", int initialCount = 1) + public InputPinCollection CreateInputPinCollection(Type type, string name = "", int initialCount = 1) { InputPinCollection pin = new(this, type, name, initialCount); _pinCollections.Add(pin); @@ -311,7 +308,7 @@ public abstract class Node : BreakableModel, INode /// The name of the pin collection /// The amount of pins to initially add to the collection /// The resulting output pin collection - protected OutputPinCollection CreateOutputPinCollection(string name = "", int initialCount = 1) + public OutputPinCollection CreateOutputPinCollection(string name = "", int initialCount = 1) { OutputPinCollection pin = new(this, name, initialCount); _pinCollections.Add(pin); @@ -325,7 +322,7 @@ public abstract class Node : BreakableModel, INode /// /// The pin collection to remove /// if the pin collection was removed; otherwise . - protected bool RemovePinCollection(PinCollection pinCollection) + public bool RemovePinCollection(PinCollection pinCollection) { bool isRemoved = _pinCollections.Remove(pinCollection); if (isRemoved) @@ -339,7 +336,8 @@ public abstract class Node : BreakableModel, INode } /// - /// Called when the node was loaded from storage or newly created + /// Called when the node was loaded from storage or newly created, at this point pin connections aren't reestablished + /// yet. /// /// The script the node is contained in public virtual void Initialize(INodeScript script) @@ -374,16 +372,6 @@ public abstract class Node : BreakableModel, INode TryOrBreak(Evaluate, "Failed to evaluate"); } - /// - /// Called whenever the node must show it's custom view model, if , no custom view model is used - /// - /// - /// The custom view model, if , no custom view model is used - public virtual ICustomNodeViewModel? GetCustomViewModel(NodeScript nodeScript) - { - return null; - } - /// /// Serializes the object into a string /// @@ -402,102 +390,4 @@ public abstract class Node : BreakableModel, INode } #endregion -} - -/// -/// Represents a kind of node inside a containing storage value of type -/// . -/// -/// The type of value the node stores -public abstract class Node : Node -{ - private TStorage? _storage; - - /// - protected Node() - { - } - - /// - protected Node(string name, string description) : base(name, description) - { - } - - /// - /// Gets or sets the storage object of this node, this is saved across sessions - /// - public TStorage? Storage - { - get => _storage; - set - { - if (SetAndNotify(ref _storage, value)) - StorageModified?.Invoke(this, EventArgs.Empty); - } - } - - /// - /// Occurs whenever the storage of this node was modified. - /// - public event EventHandler? StorageModified; - - /// - public override string SerializeStorage() - { - return CoreJson.SerializeObject(Storage, true); - } - - /// - public override void DeserializeStorage(string serialized) - { - Storage = CoreJson.DeserializeObject(serialized) ?? default(TStorage); - } -} - -/// -/// Represents a kind of node inside a containing storage value of type -/// and a view model of type . -/// -/// The type of value the node stores -/// The type of view model the node uses -public abstract class Node : Node where TViewModel : ICustomNodeViewModel -{ - /// - protected Node() - { - } - - /// - protected Node(string name, string description) : base(name, description) - { - } - - [Inject] - internal IKernel Kernel { get; set; } = null!; - - /// - /// Called when a view model is required - /// - /// - public virtual TViewModel GetViewModel(NodeScript nodeScript) - { - // Limit to one constructor, there's no need to have more and it complicates things anyway - ConstructorInfo[] constructors = typeof(TViewModel).GetConstructors(); - if (constructors.Length != 1) - throw new ArtemisCoreException("Node VMs must have exactly one constructor"); - - // Find the ScriptConfiguration parameter, it is required by the base constructor so its there for sure - ParameterInfo? configurationParameter = constructors.First().GetParameters().FirstOrDefault(p => GetType().IsAssignableFrom(p.ParameterType)); - - if (configurationParameter?.Name == null) - throw new ArtemisCoreException($"Couldn't find a valid constructor argument on {typeof(TViewModel).Name} with type {GetType().Name}"); - return Kernel.Get(new ConstructorArgument(configurationParameter.Name, this), new ConstructorArgument("script", nodeScript)); - } - - /// - /// - public override ICustomNodeViewModel? GetCustomViewModel(NodeScript nodeScript) - { - return GetViewModel(nodeScript); - } } \ No newline at end of file diff --git a/src/Artemis.Core/VisualScripting/Nodes/NodeTStorage.cs b/src/Artemis.Core/VisualScripting/Nodes/NodeTStorage.cs new file mode 100644 index 000000000..81d9e7eac --- /dev/null +++ b/src/Artemis.Core/VisualScripting/Nodes/NodeTStorage.cs @@ -0,0 +1,52 @@ +using System; +using Artemis.Core; + +/// +/// Represents a kind of node inside a containing storage value of type +/// . +/// +/// The type of value the node stores +public abstract class Node : Node +{ + private TStorage? _storage; + + /// + protected Node() + { + } + + /// + protected Node(string name, string description) : base(name, description) + { + } + + /// + /// Gets or sets the storage object of this node, this is saved across sessions + /// + public TStorage? Storage + { + get => _storage; + set + { + if (SetAndNotify(ref _storage, value)) + StorageModified?.Invoke(this, EventArgs.Empty); + } + } + + /// + /// Occurs whenever the storage of this node was modified. + /// + public event EventHandler? StorageModified; + + /// + public override string SerializeStorage() + { + return CoreJson.SerializeObject(Storage, true); + } + + /// + public override void DeserializeStorage(string serialized) + { + Storage = CoreJson.DeserializeObject(serialized) ?? default(TStorage); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/VisualScripting/Nodes/NodeTStorageTViewModel.cs b/src/Artemis.Core/VisualScripting/Nodes/NodeTStorageTViewModel.cs new file mode 100644 index 000000000..1b4109714 --- /dev/null +++ b/src/Artemis.Core/VisualScripting/Nodes/NodeTStorageTViewModel.cs @@ -0,0 +1,58 @@ +using System.Linq; +using System.Reflection; +using Artemis.Core; +using Ninject; +using Ninject.Parameters; + +/// +/// Represents a kind of node inside a containing storage value of type +/// and a view model of type . +/// +/// The type of value the node stores +/// The type of view model the node uses +public abstract class Node : Node, ICustomViewModelNode where TViewModel : ICustomNodeViewModel +{ + /// + protected Node() + { + } + + /// + protected Node(string name, string description) : base(name, description) + { + } + + [Inject] + internal IKernel Kernel { get; set; } = null!; + + /// + /// Called when a view model is required + /// + /// + public virtual TViewModel GetViewModel(NodeScript nodeScript) + { + // Limit to one constructor, there's no need to have more and it complicates things anyway + ConstructorInfo[] constructors = typeof(TViewModel).GetConstructors(); + if (constructors.Length != 1) + throw new ArtemisCoreException("Node VMs must have exactly one constructor"); + + // Find the ScriptConfiguration parameter, it is required by the base constructor so its there for sure + ParameterInfo? configurationParameter = constructors.First().GetParameters().FirstOrDefault(p => GetType().IsAssignableFrom(p.ParameterType)); + + if (configurationParameter?.Name == null) + throw new ArtemisCoreException($"Couldn't find a valid constructor argument on {typeof(TViewModel).Name} with type {GetType().Name}"); + return Kernel.Get(new ConstructorArgument(configurationParameter.Name, this), new ConstructorArgument("script", nodeScript)); + } + + /// + /// Gets or sets the position of the node's custom view model. + /// + public CustomNodeViewModelPosition ViewModelPosition { get; protected set; } = CustomNodeViewModelPosition.BetweenPinsTop; + + /// + /// + public ICustomNodeViewModel GetCustomViewModel(NodeScript nodeScript) + { + return GetViewModel(nodeScript); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/VisualScripting/InputPin.cs b/src/Artemis.Core/VisualScripting/Pins/InputPin.cs similarity index 100% rename from src/Artemis.Core/VisualScripting/InputPin.cs rename to src/Artemis.Core/VisualScripting/Pins/InputPin.cs diff --git a/src/Artemis.Core/VisualScripting/InputPinCollection.cs b/src/Artemis.Core/VisualScripting/Pins/InputPinCollection.cs similarity index 100% rename from src/Artemis.Core/VisualScripting/InputPinCollection.cs rename to src/Artemis.Core/VisualScripting/Pins/InputPinCollection.cs diff --git a/src/Artemis.Core/VisualScripting/Pins/ObjectOutputPins.cs b/src/Artemis.Core/VisualScripting/Pins/ObjectOutputPins.cs new file mode 100644 index 000000000..fb9c874f7 --- /dev/null +++ b/src/Artemis.Core/VisualScripting/Pins/ObjectOutputPins.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Artemis.Core.Modules; +using Humanizer; + +namespace Artemis.Core; + +/// +/// Represents a collection of output pins for a node capable of outputting the properties of an object or value type. +/// +public class ObjectOutputPins +{ + private readonly Dictionary, OutputPin> _propertyPins; + private OutputPin? _valueTypePin; + + /// + /// Creates an instance of the class. + /// + /// The node the object output was created for. + public ObjectOutputPins(Node node) + { + Node = node; + _propertyPins = new Dictionary, OutputPin>(); + } + + /// + /// Gets the node the object output was created for. + /// + public Node Node { get; } + + /// + /// Gets the current type the node's pins are set up for. + /// + public Type? CurrentType { get; private set; } + + /// + /// Gets a read only collection of the pins outputting the object of this object node. + /// + public ReadOnlyCollection Pins => _valueTypePin != null ? new ReadOnlyCollection(new List {_valueTypePin}) : _propertyPins.Values.ToList().AsReadOnly(); + + /// + /// Change the current type and create pins on the node to reflect this. + /// + /// The type to change the collection to. + public void ChangeType(Type? type) + { + if (type == CurrentType) + return; + CurrentType = type; + + // Remove current pins + foreach ((Func? _, OutputPin? pin) in _propertyPins) + Node.RemovePin(pin); + _propertyPins.Clear(); + if (_valueTypePin != null) + { + Node.RemovePin(_valueTypePin); + _valueTypePin = null; + } + + if (type == null) + return; + + // Create new pins + List nodeTypeColors = NodeTypeStore.GetColors(); + if (type.IsClass && type != typeof(string)) + foreach (PropertyInfo propertyInfo in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)) + { + Type propertyType = propertyInfo.PropertyType; + bool toNumeric = Numeric.IsTypeCompatible(propertyType); + + // Skip ignored properties + if (propertyInfo.CustomAttributes.Any(a => a.AttributeType == typeof(DataModelIgnoreAttribute))) + continue; + // Skip incompatible properties + if (!toNumeric && !nodeTypeColors.Any(c => c.Type.IsAssignableFrom(propertyType))) + continue; + + // Expect an object + ParameterExpression itemParameter = Expression.Parameter(typeof(object), "item"); + // Cast it to the actual item type + UnaryExpression itemCast = Expression.Convert(itemParameter, propertyInfo.DeclaringType!); + // Access the property + MemberExpression accessor = Expression.Property(itemCast, propertyInfo); + + // Turn into a numeric if needed or access directly + UnaryExpression objectExpression; + if (toNumeric) + { + propertyType = typeof(Numeric); + ConstructorInfo constructor = typeof(Numeric).GetConstructors().First(c => c.GetParameters().First().ParameterType == propertyInfo.PropertyType); + // Cast the property to an object (sadly boxing) + objectExpression = Expression.Convert(Expression.New(constructor, accessor), typeof(object)); + } + else + { + // Cast the property to an object (sadly boxing) + objectExpression = Expression.Convert(accessor, typeof(object)); + } + + // Compile the resulting expression + Func expression = Expression.Lambda>(objectExpression, itemParameter).Compile(); + _propertyPins.Add(expression, Node.CreateOrAddOutputPin(propertyType, propertyInfo.Name.Humanize())); + } + else + // Value types are applied directly to a single pin, however if the type is compatible with Numeric, we use a Numeric pin instead + // the value will then be turned into a numeric in SetCurrentValue + _valueTypePin = Node.CreateOrAddOutputPin(Numeric.IsTypeCompatible(type) ? typeof(Numeric) : type, "Item"); + } + + /// + /// Set the current value to be output onto connected pins. + /// + /// The value to output onto the connected pins. + /// + public void SetCurrentValue(object? value) + { + if (CurrentType == null) + throw new ArtemisCoreException("Cannot apply a value to an object output pins not yet configured for a type."); + if (value != null && CurrentType != value.GetType()) + throw new ArtemisCoreException($"Cannot apply a value of type {value.GetType().FullName} to an object output pins configured for type {CurrentType.FullName}"); + + // Apply the object to the pin, it must be connected if SetCurrentValue got called + if (_valueTypePin != null) + { + value ??= _valueTypePin.Type.GetDefault(); + _valueTypePin.Value = _valueTypePin.Type == typeof(Numeric) ? new Numeric(value) : value; + return; + } + + // Apply the properties of the object to each connected pin + foreach ((Func? propertyAccessor, OutputPin? outputPin) in _propertyPins) + { + if (outputPin.ConnectedTo.Any()) + outputPin.Value = value != null ? propertyAccessor(value) : outputPin.Type.GetDefault(); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/VisualScripting/OutputPin.cs b/src/Artemis.Core/VisualScripting/Pins/OutputPin.cs similarity index 100% rename from src/Artemis.Core/VisualScripting/OutputPin.cs rename to src/Artemis.Core/VisualScripting/Pins/OutputPin.cs diff --git a/src/Artemis.Core/VisualScripting/OutputPinCollection.cs b/src/Artemis.Core/VisualScripting/Pins/OutputPinCollection.cs similarity index 100% rename from src/Artemis.Core/VisualScripting/OutputPinCollection.cs rename to src/Artemis.Core/VisualScripting/Pins/OutputPinCollection.cs diff --git a/src/Artemis.Core/VisualScripting/Pin.cs b/src/Artemis.Core/VisualScripting/Pins/Pin.cs similarity index 100% rename from src/Artemis.Core/VisualScripting/Pin.cs rename to src/Artemis.Core/VisualScripting/Pins/Pin.cs diff --git a/src/Artemis.Core/VisualScripting/PinCollection.cs b/src/Artemis.Core/VisualScripting/Pins/PinCollection.cs similarity index 100% rename from src/Artemis.Core/VisualScripting/PinCollection.cs rename to src/Artemis.Core/VisualScripting/Pins/PinCollection.cs diff --git a/src/Artemis.Core/VisualScripting/PinDirection.cs b/src/Artemis.Core/VisualScripting/Pins/PinDirection.cs similarity index 100% rename from src/Artemis.Core/VisualScripting/PinDirection.cs rename to src/Artemis.Core/VisualScripting/Pins/PinDirection.cs diff --git a/src/Artemis.Storage/Migrations/M0021GradientNodes.cs b/src/Artemis.Storage/Migrations/M0021GradientNodes.cs new file mode 100644 index 000000000..11f062bd7 --- /dev/null +++ b/src/Artemis.Storage/Migrations/M0021GradientNodes.cs @@ -0,0 +1,87 @@ +using System; +using System.Linq; +using Artemis.Storage.Entities.Profile; +using Artemis.Storage.Entities.Profile.Nodes; +using Artemis.Storage.Migrations.Interfaces; +using LiteDB; + +namespace Artemis.Storage.Migrations; + +public class M0021GradientNodes : IStorageMigration +{ + private void MigrateDataBinding(PropertyEntity property) + { + NodeScriptEntity script = property.DataBinding.NodeScript; + NodeEntity exitNode = script.Nodes.FirstOrDefault(s => s.IsExitNode); + if (exitNode == null) + return; + + // Create a new node at the same position of the exit node + NodeEntity gradientNode = new() + { + Id = Guid.NewGuid(), + Type = "ColorGradientNode", + PluginId = Guid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff"), + Name = "Color Gradient", + Description = "Outputs a color gradient with the given colors", + X = exitNode.X, + Y = exitNode.Y, + Storage = property.Value // Copy the value of the property into the node storage + }; + script.Nodes.Add(gradientNode); + + // Move all connections of the exit node to the new node + foreach (NodeConnectionEntity connection in script.Connections) + { + if (connection.SourceNode == exitNode.Id) + { + connection.SourceNode = gradientNode.Id; + connection.SourcePinId++; + } + } + + // Connect the data binding node to the source node + script.Connections.Add(new NodeConnectionEntity + { + SourceType = "ColorGradient", + SourceNode = exitNode.Id, + SourcePinCollectionId = -1, + SourcePinId = 0, + TargetType = "ColorGradient", + TargetNode = gradientNode.Id, + TargetPinCollectionId = -1, + TargetPinId = 0, + }); + + // Move the exit node to the right + exitNode.X += 300; + exitNode.Y += 30; + } + + public int UserVersion => 21; + + public void Apply(LiteRepository repository) + { + // Find all color gradient data bindings, there's no really good way to do this so infer it from the value + ILiteCollection collection = repository.Database.GetCollection(); + foreach (ProfileEntity profileEntity in collection.FindAll()) + { + foreach (LayerEntity layer in profileEntity.Layers) + MigrateDataBinding(layer.LayerBrush.PropertyGroup); + + collection.Update(profileEntity); + } + } + + private void MigrateDataBinding(PropertyGroupEntity propertyGroup) + { + foreach (PropertyGroupEntity propertyGroupPropertyGroup in propertyGroup.PropertyGroups) + MigrateDataBinding(propertyGroupPropertyGroup); + + foreach (PropertyEntity property in propertyGroup.Properties) + { + if (property.Value.StartsWith("[{\"Color\":\"") && property.DataBinding?.NodeScript != null && property.DataBinding.IsEnabled) + MigrateDataBinding(property); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Controls/ArtemisIcon.axaml.cs b/src/Artemis.UI.Shared/Controls/ArtemisIcon.axaml.cs index e60e2e73b..1e3e47a1d 100644 --- a/src/Artemis.UI.Shared/Controls/ArtemisIcon.axaml.cs +++ b/src/Artemis.UI.Shared/Controls/ArtemisIcon.axaml.cs @@ -129,7 +129,7 @@ public class ArtemisIcon : UserControl /// theme /// public static readonly StyledProperty FillProperty = - AvaloniaProperty.Register(nameof(Icon), true, notifying: IconChanging); + AvaloniaProperty.Register(nameof(Icon), false, notifying: IconChanging); /// /// Gets or sets a boolean indicating whether or not the icon should be filled in with the primary text color of the diff --git a/src/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs b/src/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs index e98be34a9..0066ad760 100644 --- a/src/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs +++ b/src/Artemis.UI.Shared/Controls/GradientPicker/GradientPicker.cs @@ -59,22 +59,29 @@ public class GradientPicker : TemplatedControl public static readonly DirectProperty DeleteStopProperty = AvaloniaProperty.RegisterDirect(nameof(DeleteStop), g => g.DeleteStop); + /// + /// Gets the color gradient currently being edited, for internal use only + /// + public static readonly DirectProperty EditingColorGradientProperty = + AvaloniaProperty.RegisterDirect(nameof(EditingColorGradient), g => g.EditingColorGradient); + private readonly ICommand _deleteStop; private ColorPicker? _colorPicker; private Button? _flipStops; private Border? _gradient; - private ColorGradient? _lastColorGradient; private Button? _randomize; private Button? _rotateStops; private bool _shiftDown; private Button? _spreadStops; private Button? _toggleSeamless; + private ColorGradient _colorGradient = null!; /// /// Creates a new instance of the class. /// public GradientPicker() { + EditingColorGradient = ColorGradient.GetUnicornBarf(); _deleteStop = ReactiveCommand.Create(s => { if (ColorGradient.Count <= 2) @@ -143,6 +150,15 @@ public class GradientPicker : TemplatedControl get => _deleteStop; private init => SetAndRaise(DeleteStopProperty, ref _deleteStop, value); } + + /// + /// Gets the color gradient backing the editor, this is a copy of . + /// + public ColorGradient EditingColorGradient + { + get => _colorGradient; + private set => SetAndRaise(EditingColorGradientProperty, ref _colorGradient, value); + } /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) @@ -167,7 +183,7 @@ public class GradientPicker : TemplatedControl _flipStops = e.NameScope.Find