mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-11 19:36:28 +00:00
Merge branch 'master' into feature/NodeSorting
This commit is contained in:
commit
3ecf0bed5a
76 files changed
+1653
-745
No files matched your search
@@ -93,5 +93,6 @@
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=stores_005Cregistrations/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=utilities/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=visualscripting/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=visualscripting_005Cextensions/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=visualscripting_005Cinterfaces/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=visualscripting_005Cinterfaces/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=visualscripting_005Cnodes/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=visualscripting_005Cpins/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
@@ -1,13 +1,8 @@
|
||||
using System.Collections.Specialized;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Artemis.Core;
|
||||
namespace Artemis.Core;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class ColorGradientLayerProperty : LayerProperty<ColorGradient>
|
||||
{
|
||||
private ColorGradient? _subscribedGradient;
|
||||
|
||||
internal ColorGradientLayerProperty()
|
||||
{
|
||||
KeyframesSupported = false;
|
||||
@@ -22,29 +17,6 @@ public class ColorGradientLayerProperty : LayerProperty<ColorGradient>
|
||||
return p.CurrentValue;
|
||||
}
|
||||
|
||||
#region Overrides of LayerProperty<ColorGradient>
|
||||
|
||||
/// <inheritdoc />
|
||||
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
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
|
||||
{
|
||||
@@ -60,33 +32,12 @@ public class ColorGradientLayerProperty : LayerProperty<ColorGradient>
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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<bool> _script;
|
||||
private DefaultNode _startNode;
|
||||
private DataModelPath? _eventPath;
|
||||
private DateTime _lastProcessedTrigger;
|
||||
private object? _lastProcessedValue;
|
||||
private EventOverlapMode _overlapMode;
|
||||
private NodeScript<bool> _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<bool>($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile);
|
||||
_script = new NodeScript<bool>($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile, new List<DefaultNode> {_startNode});
|
||||
}
|
||||
|
||||
internal EventCondition(EventConditionEntity entity, RenderProfileElement profileElement)
|
||||
@@ -96,109 +96,6 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
|
||||
set => SetAndNotify(ref _toggleOffMode, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the event node, applying the selected event
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the start node of the event script, if any
|
||||
/// </summary>
|
||||
/// <returns>The start node of the event script, if any.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IConditionEntity Entity => _entity;
|
||||
|
||||
@@ -264,6 +161,116 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
|
||||
Script?.Dispose();
|
||||
EventPath?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the event node, applying the selected event
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the start node of the event script, if any
|
||||
/// </summary>
|
||||
/// <returns>The start node of the event script, if any.</returns>
|
||||
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<bool>($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", _entity.Script, ProfileElement.Profile)
|
||||
: new NodeScript<bool>($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile);
|
||||
UpdateEventNode();
|
||||
? new NodeScript<bool>(name, description, _entity.Script, ProfileElement.Profile, new List<DefaultNode> {_startNode})
|
||||
: new NodeScript<bool>(name, description, ProfileElement.Profile, new List<DefaultNode> {_startNode});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -311,71 +320,9 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
|
||||
/// <inheritdoc />
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a mode for render elements to start their timeline when display conditions events are fired.
|
||||
/// </summary>
|
||||
public enum EventTriggerMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Play the timeline once.
|
||||
/// </summary>
|
||||
Play,
|
||||
|
||||
/// <summary>
|
||||
/// Toggle repeating the timeline.
|
||||
/// </summary>
|
||||
Toggle
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a mode for render elements to configure the behaviour of events that overlap i.e. trigger again before
|
||||
/// the timeline finishes.
|
||||
/// </summary>
|
||||
public enum EventOverlapMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Stop the current run and restart the timeline
|
||||
/// </summary>
|
||||
Restart,
|
||||
|
||||
/// <summary>
|
||||
/// Play another copy of the timeline on top of the current run
|
||||
/// </summary>
|
||||
Copy,
|
||||
|
||||
/// <summary>
|
||||
/// Ignore subsequent event fires until the timeline finishes
|
||||
/// </summary>
|
||||
Ignore
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a mode for render elements when toggling off the event when using <see cref="EventTriggerMode.Toggle" />
|
||||
/// .
|
||||
/// </summary>
|
||||
public enum EventToggleOffMode
|
||||
{
|
||||
/// <summary>
|
||||
/// When the event toggles the condition off, finish the the current run of the main timeline
|
||||
/// </summary>
|
||||
Finish,
|
||||
|
||||
/// <summary>
|
||||
/// When the event toggles the condition off, skip to the end segment of the timeline
|
||||
/// </summary>
|
||||
SkipToEnd
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace Artemis.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a mode for render elements to configure the behaviour of events that overlap i.e. trigger again before
|
||||
/// the timeline finishes.
|
||||
/// </summary>
|
||||
public enum EventOverlapMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Stop the current run and restart the timeline
|
||||
/// </summary>
|
||||
Restart,
|
||||
|
||||
/// <summary>
|
||||
/// Play another copy of the timeline on top of the current run
|
||||
/// </summary>
|
||||
Copy,
|
||||
|
||||
/// <summary>
|
||||
/// Ignore subsequent event fires until the timeline finishes
|
||||
/// </summary>
|
||||
Ignore
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Artemis.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a mode for render elements when toggling off the event when using <see cref="EventTriggerMode.Toggle" />
|
||||
/// .
|
||||
/// </summary>
|
||||
public enum EventToggleOffMode
|
||||
{
|
||||
/// <summary>
|
||||
/// When the event toggles the condition off, finish the the current run of the main timeline
|
||||
/// </summary>
|
||||
Finish,
|
||||
|
||||
/// <summary>
|
||||
/// When the event toggles the condition off, skip to the end segment of the timeline
|
||||
/// </summary>
|
||||
SkipToEnd
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Artemis.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a mode for render elements to start their timeline when display conditions events are fired.
|
||||
/// </summary>
|
||||
public enum EventTriggerMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Play the timeline once.
|
||||
/// </summary>
|
||||
Play,
|
||||
|
||||
/// <summary>
|
||||
/// Toggle repeating the timeline.
|
||||
/// </summary>
|
||||
Toggle
|
||||
}
|
||||
@@ -35,7 +35,7 @@ public class DataBindingProperty<TProperty> : IDataBindingProperty
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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)
|
||||
|
||||
@@ -27,5 +27,5 @@ public interface IDataBindingProperty
|
||||
/// Sets the value of the property this registration points to
|
||||
/// </summary>
|
||||
/// <param name="value">A value matching the type of <see cref="ValueType" /></param>
|
||||
void SetValue(object? value);
|
||||
void SetValue(object value);
|
||||
}
|
||||
@@ -22,6 +22,7 @@ public class PluginFeatureAttribute : Attribute
|
||||
/// The plugins display icon that's shown in the settings see <see href="https://materialdesignicons.com" /> for
|
||||
/// available icons
|
||||
/// </summary>
|
||||
[Obsolete("Feature icons are no longer shown in the UI.")]
|
||||
public string? Icon { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -110,17 +86,6 @@ public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject
|
||||
set => SetAndNotify(ref _description, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The plugins display icon that's shown in the settings see <see href="https://materialdesignicons.com" /> for
|
||||
/// available icons
|
||||
/// </summary>
|
||||
[JsonProperty]
|
||||
public string? Icon
|
||||
{
|
||||
get => _icon;
|
||||
set => SetAndNotify(ref _icon, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks the feature to always be enabled as long as the plugin is enabled and cannot be disabled.
|
||||
/// <para>Note: always <see langword="true" /> if this is the plugin's only feature</para>
|
||||
@@ -142,20 +107,7 @@ public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject
|
||||
get => _instance;
|
||||
internal set => SetAndNotify(ref _instance, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a string representing either a full path pointing to an svg or the markdown icon
|
||||
/// </summary>
|
||||
public string? ResolvedIcon
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Icon == null)
|
||||
return null;
|
||||
return Icon.Contains('.') ? Plugin.ResolveRelativePath(Icon) : Icon;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal PluginFeatureEntity Entity { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -8,6 +8,13 @@ namespace Artemis.Core.Services;
|
||||
/// </summary>
|
||||
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; }
|
||||
|
||||
/// <summary>
|
||||
/// Called when the input service requests a <see cref="KeyboardToggleStatusReceived" /> event
|
||||
/// </summary>
|
||||
|
||||
@@ -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<Tuple<InputProvider, object>, ArtemisDevice> _deviceCache = new();
|
||||
private List<ArtemisDevice> _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<InputProvider, object>(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<MouseButton> _pressedButtons = new();
|
||||
|
||||
|
||||
private void InputProviderOnMouseButtonDataReceived(object? sender, InputProviderMouseButtonEventArgs e)
|
||||
{
|
||||
bool foundLedId = InputKeyUtilities.MouseButtonLedIdMap.TryGetValue(e.Button, out LedId ledId);
|
||||
|
||||
@@ -23,7 +23,10 @@ internal class DataBindingExitNode<TLayerProperty> : 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()
|
||||
|
||||
@@ -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<Func<DataModelEventArgs, object>, 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<Func<DataModelEventArgs, object>, 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<DataModelEventArgs, object> expression = Expression.Lambda<Func<DataModelEventArgs, object>>(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<DataModelEventArgs, object> 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace Artemis.Core.VisualScripting.Internal;
|
||||
|
||||
internal interface IEventConditionNode : INode
|
||||
{
|
||||
}
|
||||
@@ -35,7 +35,10 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript
|
||||
|
||||
#region Properties & Fields
|
||||
|
||||
internal NodeScriptEntity Entity { get; private set; }
|
||||
/// <summary>
|
||||
/// Gets the entity used to store this script.
|
||||
/// </summary>
|
||||
public NodeScriptEntity Entity { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name { get; }
|
||||
@@ -77,7 +80,8 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript
|
||||
/// The context of the node script, usually a <see cref="Profile" /> or
|
||||
/// <see cref="ProfileConfiguration" />
|
||||
/// </param>
|
||||
protected NodeScript(string name, string description, object? context = null)
|
||||
/// <param name="defaultNodes">A list of default nodes to add to the node script.</param>
|
||||
protected NodeScript(string name, string description, object? context = null, List<DefaultNode>? 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<DefaultNode>? 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<T> : NodeScript, INodeScript<T>
|
||||
|
||||
#region Constructors
|
||||
|
||||
internal NodeScript(string name, string description, NodeScriptEntity entity, object? context = null)
|
||||
: base(name, description, entity, context)
|
||||
/// <inheritdoc />
|
||||
public NodeScript(string name, string description, NodeScriptEntity entity, object? context = null, List<DefaultNode>? defaultNodes = null)
|
||||
: base(name, description, entity, context, defaultNodes)
|
||||
{
|
||||
ExitNode = new ExitNode<T>(name, description);
|
||||
AddNode(ExitNode);
|
||||
@@ -420,8 +433,8 @@ public class NodeScript<T> : NodeScript, INodeScript<T>
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NodeScript(string name, string description, object? context = null)
|
||||
: base(name, description, context)
|
||||
public NodeScript(string name, string description, object? context = null, List<DefaultNode>? defaultNodes = null)
|
||||
: base(name, description, context, defaultNodes)
|
||||
{
|
||||
ExitNode = new ExitNode<T>(name, description);
|
||||
AddNode(ExitNode);
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1,32 @@
|
||||
namespace Artemis.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the position of a node's custom view model.
|
||||
/// </summary>
|
||||
public enum CustomNodeViewModelPosition
|
||||
{
|
||||
/// <summary>
|
||||
/// Puts the view model above the pins.
|
||||
/// </summary>
|
||||
AbovePins,
|
||||
|
||||
/// <summary>
|
||||
/// Puts the view model between the pins, vertically aligned to the top.
|
||||
/// </summary>
|
||||
BetweenPinsTop,
|
||||
|
||||
/// <summary>
|
||||
/// Puts the view model between the pins, vertically aligned to the center.
|
||||
/// </summary>
|
||||
BetweenPinsCenter,
|
||||
|
||||
/// <summary>
|
||||
/// Puts the view model between the pins, vertically aligned to the bottom.
|
||||
/// </summary>
|
||||
BetweenPinsBottom,
|
||||
|
||||
/// <summary>
|
||||
/// Puts the view model below the pins.
|
||||
/// </summary>
|
||||
BelowPins
|
||||
}
|
||||
+2
-11
@@ -1,18 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace Artemis.Core.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a kind of node that cannot be deleted inside a <see cref="INode" />.
|
||||
/// </summary>
|
||||
public interface IDefaultNode : INode
|
||||
{
|
||||
}
|
||||
namespace Artemis.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a kind of node that cannot be deleted inside a <see cref="NodeScript" />.
|
||||
/// </summary>
|
||||
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
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Artemis.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a node that has a custom view model.
|
||||
/// </summary>
|
||||
public interface ICustomViewModelNode
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the position of the node's custom view model.
|
||||
/// </summary>
|
||||
CustomNodeViewModelPosition ViewModelPosition { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Called whenever the node must show it's custom view model, if <see langword="null" />, no custom view model is used
|
||||
/// </summary>
|
||||
/// <returns>The custom view model, if <see langword="null" />, no custom view model is used</returns>
|
||||
ICustomNodeViewModel? GetCustomViewModel(NodeScript nodeScript);
|
||||
}
|
||||
+14
-124
@@ -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
|
||||
/// <param name="name">The name of the pin</param>
|
||||
/// <typeparam name="T">The type of value the pin will hold</typeparam>
|
||||
/// <returns>The newly created pin</returns>
|
||||
protected InputPin<T> CreateInputPin<T>(string name = "")
|
||||
public InputPin<T> CreateInputPin<T>(string name = "")
|
||||
{
|
||||
InputPin<T> pin = new(this, name);
|
||||
_pins.Add(pin);
|
||||
@@ -146,7 +143,7 @@ public abstract class Node : BreakableModel, INode
|
||||
/// <param name="type">The type of value the pin will hold</param>
|
||||
/// <param name="name">The name of the pin</param>
|
||||
/// <returns>The newly created pin</returns>
|
||||
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
|
||||
/// <param name="name">The name of the pin</param>
|
||||
/// <typeparam name="T">The type of value the pin will hold</typeparam>
|
||||
/// <returns>The newly created pin</returns>
|
||||
protected OutputPin<T> CreateOutputPin<T>(string name = "")
|
||||
public OutputPin<T> CreateOutputPin<T>(string name = "")
|
||||
{
|
||||
OutputPin<T> pin = new(this, name);
|
||||
_pins.Add(pin);
|
||||
@@ -174,7 +171,7 @@ public abstract class Node : BreakableModel, INode
|
||||
/// <param name="type">The type of value the pin will hold</param>
|
||||
/// <param name="name">The name of the pin</param>
|
||||
/// <returns>The newly created pin</returns>
|
||||
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.
|
||||
/// </summary>
|
||||
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.
|
||||
/// </summary>
|
||||
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
|
||||
/// </summary>
|
||||
/// <param name="pin">The pin to remove</param>
|
||||
/// <returns><see langword="true" /> if the pin was removed; otherwise <see langword="false" />.</returns>
|
||||
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 <paramref name="pin" /> to the <see cref="Pins" /> collection.
|
||||
/// </summary>
|
||||
/// <param name="pin">The pin to add</param>
|
||||
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
|
||||
/// <param name="name">The name of the pin collection</param>
|
||||
/// <param name="initialCount">The amount of pins to initially add to the collection</param>
|
||||
/// <returns>The resulting input pin collection</returns>
|
||||
protected InputPinCollection<T> CreateInputPinCollection<T>(string name = "", int initialCount = 1)
|
||||
public InputPinCollection<T> CreateInputPinCollection<T>(string name = "", int initialCount = 1)
|
||||
{
|
||||
InputPinCollection<T> pin = new(this, name, initialCount);
|
||||
_pinCollections.Add(pin);
|
||||
@@ -296,7 +293,7 @@ public abstract class Node : BreakableModel, INode
|
||||
/// <param name="name">The name of the pin collection</param>
|
||||
/// <param name="initialCount">The amount of pins to initially add to the collection</param>
|
||||
/// <returns>The resulting input pin collection</returns>
|
||||
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
|
||||
/// <param name="name">The name of the pin collection</param>
|
||||
/// <param name="initialCount">The amount of pins to initially add to the collection</param>
|
||||
/// <returns>The resulting output pin collection</returns>
|
||||
protected OutputPinCollection<T> CreateOutputPinCollection<T>(string name = "", int initialCount = 1)
|
||||
public OutputPinCollection<T> CreateOutputPinCollection<T>(string name = "", int initialCount = 1)
|
||||
{
|
||||
OutputPinCollection<T> pin = new(this, name, initialCount);
|
||||
_pinCollections.Add(pin);
|
||||
@@ -325,7 +322,7 @@ public abstract class Node : BreakableModel, INode
|
||||
/// </summary>
|
||||
/// <param name="pinCollection">The pin collection to remove</param>
|
||||
/// <returns><see langword="true" /> if the pin collection was removed; otherwise <see langword="false" />.</returns>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="script">The script the node is contained in</param>
|
||||
public virtual void Initialize(INodeScript script)
|
||||
@@ -374,16 +372,6 @@ public abstract class Node : BreakableModel, INode
|
||||
TryOrBreak(Evaluate, "Failed to evaluate");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called whenever the node must show it's custom view model, if <see langword="null" />, no custom view model is used
|
||||
/// </summary>
|
||||
/// <param name="nodeScript"></param>
|
||||
/// <returns>The custom view model, if <see langword="null" />, no custom view model is used</returns>
|
||||
public virtual ICustomNodeViewModel? GetCustomViewModel(NodeScript nodeScript)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the <see cref="Storage" /> object into a string
|
||||
/// </summary>
|
||||
@@ -402,102 +390,4 @@ public abstract class Node : BreakableModel, INode
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a kind of node inside a <see cref="NodeScript" /> containing storage value of type
|
||||
/// <typeparamref name="TStorage" />.
|
||||
/// </summary>
|
||||
/// <typeparam name="TStorage">The type of value the node stores</typeparam>
|
||||
public abstract class Node<TStorage> : Node
|
||||
{
|
||||
private TStorage? _storage;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected Node()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected Node(string name, string description) : base(name, description)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the storage object of this node, this is saved across sessions
|
||||
/// </summary>
|
||||
public TStorage? Storage
|
||||
{
|
||||
get => _storage;
|
||||
set
|
||||
{
|
||||
if (SetAndNotify(ref _storage, value))
|
||||
StorageModified?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs whenever the storage of this node was modified.
|
||||
/// </summary>
|
||||
public event EventHandler? StorageModified;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string SerializeStorage()
|
||||
{
|
||||
return CoreJson.SerializeObject(Storage, true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void DeserializeStorage(string serialized)
|
||||
{
|
||||
Storage = CoreJson.DeserializeObject<TStorage>(serialized) ?? default(TStorage);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a kind of node inside a <see cref="NodeScript" /> containing storage value of type
|
||||
/// <typeparamref name="TStorage" /> and a view model of type <typeparamref name="TViewModel" />.
|
||||
/// </summary>
|
||||
/// <typeparam name="TStorage">The type of value the node stores</typeparam>
|
||||
/// <typeparam name="TViewModel">The type of view model the node uses</typeparam>
|
||||
public abstract class Node<TStorage, TViewModel> : Node<TStorage> where TViewModel : ICustomNodeViewModel
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected Node()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected Node(string name, string description) : base(name, description)
|
||||
{
|
||||
}
|
||||
|
||||
[Inject]
|
||||
internal IKernel Kernel { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Called when a view model is required
|
||||
/// </summary>
|
||||
/// <param name="nodeScript"></param>
|
||||
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<TViewModel>(new ConstructorArgument(configurationParameter.Name, this), new ConstructorArgument("script", nodeScript));
|
||||
}
|
||||
|
||||
/// <param name="nodeScript"></param>
|
||||
/// <inheritdoc />
|
||||
public override ICustomNodeViewModel? GetCustomViewModel(NodeScript nodeScript)
|
||||
{
|
||||
return GetViewModel(nodeScript);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using Artemis.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a kind of node inside a <see cref="NodeScript" /> containing storage value of type
|
||||
/// <typeparamref name="TStorage" />.
|
||||
/// </summary>
|
||||
/// <typeparam name="TStorage">The type of value the node stores</typeparam>
|
||||
public abstract class Node<TStorage> : Node
|
||||
{
|
||||
private TStorage? _storage;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected Node()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected Node(string name, string description) : base(name, description)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the storage object of this node, this is saved across sessions
|
||||
/// </summary>
|
||||
public TStorage? Storage
|
||||
{
|
||||
get => _storage;
|
||||
set
|
||||
{
|
||||
if (SetAndNotify(ref _storage, value))
|
||||
StorageModified?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs whenever the storage of this node was modified.
|
||||
/// </summary>
|
||||
public event EventHandler? StorageModified;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string SerializeStorage()
|
||||
{
|
||||
return CoreJson.SerializeObject(Storage, true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void DeserializeStorage(string serialized)
|
||||
{
|
||||
Storage = CoreJson.DeserializeObject<TStorage>(serialized) ?? default(TStorage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Artemis.Core;
|
||||
using Ninject;
|
||||
using Ninject.Parameters;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a kind of node inside a <see cref="NodeScript" /> containing storage value of type
|
||||
/// <typeparamref name="TStorage" /> and a view model of type <typeparamref name="TViewModel" />.
|
||||
/// </summary>
|
||||
/// <typeparam name="TStorage">The type of value the node stores</typeparam>
|
||||
/// <typeparam name="TViewModel">The type of view model the node uses</typeparam>
|
||||
public abstract class Node<TStorage, TViewModel> : Node<TStorage>, ICustomViewModelNode where TViewModel : ICustomNodeViewModel
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected Node()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected Node(string name, string description) : base(name, description)
|
||||
{
|
||||
}
|
||||
|
||||
[Inject]
|
||||
internal IKernel Kernel { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Called when a view model is required
|
||||
/// </summary>
|
||||
/// <param name="nodeScript"></param>
|
||||
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<TViewModel>(new ConstructorArgument(configurationParameter.Name, this), new ConstructorArgument("script", nodeScript));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the position of the node's custom view model.
|
||||
/// </summary>
|
||||
public CustomNodeViewModelPosition ViewModelPosition { get; protected set; } = CustomNodeViewModelPosition.BetweenPinsTop;
|
||||
|
||||
/// <param name="nodeScript"></param>
|
||||
/// <inheritdoc />
|
||||
public ICustomNodeViewModel GetCustomViewModel(NodeScript nodeScript)
|
||||
{
|
||||
return GetViewModel(nodeScript);
|
||||
}
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a collection of output pins for a node capable of outputting the properties of an object or value type.
|
||||
/// </summary>
|
||||
public class ObjectOutputPins
|
||||
{
|
||||
private readonly Dictionary<Func<object, object>, OutputPin> _propertyPins;
|
||||
private OutputPin? _valueTypePin;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of the <see cref="ObjectOutputPins" /> class.
|
||||
/// </summary>
|
||||
/// <param name="node">The node the object output was created for.</param>
|
||||
public ObjectOutputPins(Node node)
|
||||
{
|
||||
Node = node;
|
||||
_propertyPins = new Dictionary<Func<object, object>, OutputPin>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the node the object output was created for.
|
||||
/// </summary>
|
||||
public Node Node { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current type the node's pins are set up for.
|
||||
/// </summary>
|
||||
public Type? CurrentType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a read only collection of the pins outputting the object of this object node.
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<OutputPin> Pins => _valueTypePin != null ? new ReadOnlyCollection<OutputPin>(new List<OutputPin> {_valueTypePin}) : _propertyPins.Values.ToList().AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// Change the current type and create pins on the node to reflect this.
|
||||
/// </summary>
|
||||
/// <param name="type">The type to change the collection to.</param>
|
||||
public void ChangeType(Type? type)
|
||||
{
|
||||
if (type == CurrentType)
|
||||
return;
|
||||
CurrentType = type;
|
||||
|
||||
// Remove current pins
|
||||
foreach ((Func<object, object>? _, 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<TypeColorRegistration> 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<object, object> expression = Expression.Lambda<Func<object, object>>(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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the current value to be output onto connected pins.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to output onto the connected pins.</param>
|
||||
/// <exception cref="ArtemisCoreException"></exception>
|
||||
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<object, object>? propertyAccessor, OutputPin? outputPin) in _propertyPins)
|
||||
{
|
||||
if (outputPin.ConnectedTo.Any())
|
||||
outputPin.Value = value != null ? propertyAccessor(value) : outputPin.Type.GetDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Reference in new issue
Block a user