mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Merge branch 'master' into feature/NodeSorting
This commit is contained in:
commit
3ecf0bed5a
@ -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/=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/=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/@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>
|
||||||
<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_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;
|
namespace Artemis.Core;
|
||||||
using SkiaSharp;
|
|
||||||
|
|
||||||
namespace Artemis.Core;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public class ColorGradientLayerProperty : LayerProperty<ColorGradient>
|
public class ColorGradientLayerProperty : LayerProperty<ColorGradient>
|
||||||
{
|
{
|
||||||
private ColorGradient? _subscribedGradient;
|
|
||||||
|
|
||||||
internal ColorGradientLayerProperty()
|
internal ColorGradientLayerProperty()
|
||||||
{
|
{
|
||||||
KeyframesSupported = false;
|
KeyframesSupported = false;
|
||||||
@ -22,29 +17,6 @@ public class ColorGradientLayerProperty : LayerProperty<ColorGradient>
|
|||||||
return p.CurrentValue;
|
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 />
|
/// <inheritdoc />
|
||||||
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
|
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
|
||||||
{
|
{
|
||||||
@ -60,33 +32,12 @@ public class ColorGradientLayerProperty : LayerProperty<ColorGradient>
|
|||||||
if (BaseValue == null!)
|
if (BaseValue == null!)
|
||||||
BaseValue = new ColorGradient(DefaultValue);
|
BaseValue = new ColorGradient(DefaultValue);
|
||||||
|
|
||||||
base.OnInitialize();
|
DataBinding.RegisterDataBindingProperty(() => CurrentValue, value =>
|
||||||
|
{
|
||||||
|
if (value != null)
|
||||||
|
CurrentValue = value;
|
||||||
|
}, "Value");
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#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;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Artemis.Core.Internal;
|
using Artemis.Core.Internal;
|
||||||
using Artemis.Core.VisualScripting.Internal;
|
|
||||||
using Artemis.Storage.Entities.Profile.Abstract;
|
using Artemis.Storage.Entities.Profile.Abstract;
|
||||||
using Artemis.Storage.Entities.Profile.Conditions;
|
using Artemis.Storage.Entities.Profile.Conditions;
|
||||||
|
|
||||||
@ -14,12 +14,12 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
|
|||||||
{
|
{
|
||||||
private readonly string _displayName;
|
private readonly string _displayName;
|
||||||
private readonly EventConditionEntity _entity;
|
private readonly EventConditionEntity _entity;
|
||||||
|
private NodeScript<bool> _script;
|
||||||
|
private DefaultNode _startNode;
|
||||||
private DataModelPath? _eventPath;
|
private DataModelPath? _eventPath;
|
||||||
private DateTime _lastProcessedTrigger;
|
private DateTime _lastProcessedTrigger;
|
||||||
private object? _lastProcessedValue;
|
private object? _lastProcessedValue;
|
||||||
private EventOverlapMode _overlapMode;
|
private EventOverlapMode _overlapMode;
|
||||||
private NodeScript<bool> _script;
|
|
||||||
private IEventConditionNode _startNode;
|
|
||||||
private EventToggleOffMode _toggleOffMode;
|
private EventToggleOffMode _toggleOffMode;
|
||||||
private EventTriggerMode _triggerMode;
|
private EventTriggerMode _triggerMode;
|
||||||
private bool _wasMet;
|
private bool _wasMet;
|
||||||
@ -34,7 +34,7 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
|
|||||||
_entity = new EventConditionEntity();
|
_entity = new EventConditionEntity();
|
||||||
_displayName = profileElement.GetType().Name;
|
_displayName = profileElement.GetType().Name;
|
||||||
_startNode = new EventConditionEventStartNode {X = -300};
|
_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)
|
internal EventCondition(EventConditionEntity entity, RenderProfileElement profileElement)
|
||||||
@ -96,109 +96,6 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
|
|||||||
set => SetAndNotify(ref _toggleOffMode, value);
|
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 />
|
/// <inheritdoc />
|
||||||
public IConditionEntity Entity => _entity;
|
public IConditionEntity Entity => _entity;
|
||||||
|
|
||||||
@ -265,6 +162,116 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
|
|||||||
EventPath?.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
|
#region Storage
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -276,11 +283,13 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
|
|||||||
|
|
||||||
if (_entity.EventPath != null)
|
if (_entity.EventPath != null)
|
||||||
EventPath = new DataModelPath(_entity.EventPath);
|
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
|
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>(name, description, _entity.Script, ProfileElement.Profile, new List<DefaultNode> {_startNode})
|
||||||
: new NodeScript<bool>($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile);
|
: new NodeScript<bool>(name, description, ProfileElement.Profile, new List<DefaultNode> {_startNode});
|
||||||
UpdateEventNode();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -311,71 +320,9 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void LoadNodeScript()
|
public void LoadNodeScript()
|
||||||
{
|
{
|
||||||
|
UpdateEventNode(true);
|
||||||
Script.Load();
|
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
|
#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 />
|
/// <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 :)
|
// Numeric has a bunch of conversion, this seems the cheapest way to use them :)
|
||||||
switch (value)
|
switch (value)
|
||||||
|
|||||||
@ -27,5 +27,5 @@ public interface IDataBindingProperty
|
|||||||
/// Sets the value of the property this registration points to
|
/// Sets the value of the property this registration points to
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="value">A value matching the type of <see cref="ValueType" /></param>
|
/// <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
|
/// The plugins display icon that's shown in the settings see <see href="https://materialdesignicons.com" /> for
|
||||||
/// available icons
|
/// available icons
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Obsolete("Feature icons are no longer shown in the UI.")]
|
||||||
public string? Icon { get; set; }
|
public string? Icon { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -31,20 +31,7 @@ public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject
|
|||||||
|
|
||||||
Name = attribute?.Name ?? featureType.Name.Humanize(LetterCasing.Title);
|
Name = attribute?.Name ?? featureType.Name.Humanize(LetterCasing.Title);
|
||||||
Description = attribute?.Description;
|
Description = attribute?.Description;
|
||||||
Icon = attribute?.Icon;
|
|
||||||
AlwaysEnabled = attribute?.AlwaysEnabled ?? false;
|
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)
|
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);
|
Name = attribute?.Name ?? instance.GetType().Name.Humanize(LetterCasing.Title);
|
||||||
Description = attribute?.Description;
|
Description = attribute?.Description;
|
||||||
Icon = attribute?.Icon;
|
|
||||||
AlwaysEnabled = attribute?.AlwaysEnabled ?? false;
|
AlwaysEnabled = attribute?.AlwaysEnabled ?? false;
|
||||||
Instance = instance;
|
Instance = instance;
|
||||||
|
|
||||||
if (Icon != null) return;
|
|
||||||
Icon = Instance switch
|
|
||||||
{
|
|
||||||
DeviceProvider => "Devices",
|
|
||||||
Module => "VectorRectangle",
|
|
||||||
LayerBrushProvider => "Brush",
|
|
||||||
LayerEffectProvider => "AutoAwesome",
|
|
||||||
_ => "Plugin"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -110,17 +86,6 @@ public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject
|
|||||||
set => SetAndNotify(ref _description, value);
|
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>
|
/// <summary>
|
||||||
/// Marks the feature to always be enabled as long as the plugin is enabled and cannot be disabled.
|
/// 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>
|
/// <para>Note: always <see langword="true" /> if this is the plugin's only feature</para>
|
||||||
@ -143,19 +108,6 @@ public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject
|
|||||||
internal set => SetAndNotify(ref _instance, value);
|
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; }
|
internal PluginFeatureEntity Entity { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@ -8,6 +8,13 @@ namespace Artemis.Core.Services;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class InputProvider : IDisposable
|
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>
|
/// <summary>
|
||||||
/// Called when the input service requests a <see cref="KeyboardToggleStatusReceived" /> event
|
/// Called when the input service requests a <see cref="KeyboardToggleStatusReceived" /> event
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -10,12 +10,18 @@ internal class InputService : IInputService
|
|||||||
{
|
{
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly IRgbService _rgbService;
|
private readonly IRgbService _rgbService;
|
||||||
|
private ArtemisDevice? _firstKeyboard;
|
||||||
|
private ArtemisDevice? _firstMouse;
|
||||||
|
private int _keyboardCount;
|
||||||
|
private int _mouseCount;
|
||||||
|
|
||||||
public InputService(ILogger logger, IRgbService rgbService)
|
public InputService(ILogger logger, IRgbService rgbService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_rgbService = rgbService;
|
_rgbService = rgbService;
|
||||||
|
|
||||||
|
_rgbService.DeviceAdded += RgbServiceOnDevicesModified;
|
||||||
|
_rgbService.DeviceRemoved += RgbServiceOnDevicesModified;
|
||||||
BustIdentifierCache();
|
BustIdentifierCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,8 +138,6 @@ internal class InputService : IInputService
|
|||||||
|
|
||||||
private readonly Dictionary<Tuple<InputProvider, object>, ArtemisDevice> _deviceCache = new();
|
private readonly Dictionary<Tuple<InputProvider, object>, ArtemisDevice> _deviceCache = new();
|
||||||
private List<ArtemisDevice> _devices = new();
|
private List<ArtemisDevice> _devices = new();
|
||||||
private ArtemisDevice? _cachedFallbackKeyboard;
|
|
||||||
private ArtemisDevice? _cachedFallbackMouse;
|
|
||||||
private ArtemisDevice? _identifyingDevice;
|
private ArtemisDevice? _identifyingDevice;
|
||||||
|
|
||||||
public void IdentifyDevice(ArtemisDevice device)
|
public void IdentifyDevice(ArtemisDevice device)
|
||||||
@ -164,13 +168,29 @@ internal class InputService : IInputService
|
|||||||
if (provider == null) throw new ArgumentNullException(nameof(provider));
|
if (provider == null) throw new ArgumentNullException(nameof(provider));
|
||||||
if (identifier == null) throw new ArgumentNullException(nameof(identifier));
|
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
|
// Try cache first
|
||||||
ArtemisDevice? cacheMatch = GetDeviceFromCache(provider, identifier);
|
ArtemisDevice? cacheMatch = GetDeviceFromCache(provider, identifier);
|
||||||
if (cacheMatch != null)
|
if (cacheMatch != null)
|
||||||
return cacheMatch;
|
return cacheMatch;
|
||||||
|
|
||||||
string providerName = provider.GetType().FullName!;
|
ArtemisDevice? match = _devices.FirstOrDefault(m => m.InputIdentifiers.Any(i => Equals(i.InputProvider, provider.ProviderName) && Equals(i.Identifier, identifier)));
|
||||||
ArtemisDevice? match = _devices.FirstOrDefault(m => m.InputIdentifiers.Any(i => Equals(i.InputProvider, providerName) && Equals(i.Identifier, identifier)));
|
|
||||||
|
|
||||||
// If a match was found cache it to speed up the next event and return the match
|
// If a match was found cache it to speed up the next event and return the match
|
||||||
if (match != null)
|
if (match != null)
|
||||||
@ -179,34 +199,16 @@ internal class InputService : IInputService
|
|||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is no match, apply our fallback type
|
|
||||||
if (type == InputDeviceType.None)
|
|
||||||
return null;
|
|
||||||
if (type == InputDeviceType.Keyboard)
|
if (type == InputDeviceType.Keyboard)
|
||||||
{
|
return _firstKeyboard;
|
||||||
if (_cachedFallbackKeyboard != null)
|
|
||||||
return _cachedFallbackKeyboard;
|
|
||||||
_cachedFallbackKeyboard = _rgbService.EnabledDevices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Keyboard);
|
|
||||||
return _cachedFallbackKeyboard;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type == InputDeviceType.Mouse)
|
if (type == InputDeviceType.Mouse)
|
||||||
{
|
return _firstMouse;
|
||||||
if (_cachedFallbackMouse != null)
|
|
||||||
return _cachedFallbackMouse;
|
|
||||||
_cachedFallbackMouse = _rgbService.EnabledDevices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Mouse);
|
|
||||||
return _cachedFallbackMouse;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BustIdentifierCache()
|
public void BustIdentifierCache()
|
||||||
{
|
{
|
||||||
_deviceCache.Clear();
|
_deviceCache.Clear();
|
||||||
_cachedFallbackKeyboard = null;
|
|
||||||
_cachedFallbackMouse = null;
|
|
||||||
|
|
||||||
_devices = _rgbService.EnabledDevices.Where(d => d.InputIdentifiers.Any()).ToList();
|
_devices = _rgbService.EnabledDevices.Where(d => d.InputIdentifiers.Any()).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,11 +223,6 @@ internal class InputService : IInputService
|
|||||||
return device;
|
return device;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SurfaceConfigurationChanged(object? sender, SurfaceConfigurationEventArgs e)
|
|
||||||
{
|
|
||||||
BustIdentifierCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InputProviderOnIdentifierReceived(object? sender, InputProviderIdentifierEventArgs e)
|
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
|
// 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))
|
if (!(sender is InputProvider inputProvider))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
string providerName = inputProvider.GetType().FullName!;
|
|
||||||
|
|
||||||
// Remove existing identification
|
// Remove existing identification
|
||||||
_identifyingDevice.InputIdentifiers.RemoveAll(i => i.InputProvider == providerName);
|
_identifyingDevice.InputIdentifiers.RemoveAll(i => i.InputProvider == inputProvider.ProviderName);
|
||||||
_identifyingDevice.InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(providerName, e.Identifier));
|
_identifyingDevice.InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(inputProvider.ProviderName, e.Identifier));
|
||||||
|
|
||||||
StopIdentify();
|
StopIdentify();
|
||||||
OnDeviceIdentified();
|
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
|
#endregion
|
||||||
|
|
||||||
#region Keyboard
|
#region Keyboard
|
||||||
@ -379,6 +384,7 @@ internal class InputService : IInputService
|
|||||||
|
|
||||||
private readonly HashSet<MouseButton> _pressedButtons = new();
|
private readonly HashSet<MouseButton> _pressedButtons = new();
|
||||||
|
|
||||||
|
|
||||||
private void InputProviderOnMouseButtonDataReceived(object? sender, InputProviderMouseButtonEventArgs e)
|
private void InputProviderOnMouseButtonDataReceived(object? sender, InputProviderMouseButtonEventArgs e)
|
||||||
{
|
{
|
||||||
bool foundLedId = InputKeyUtilities.MouseButtonLedIdMap.TryGetValue(e.Button, out LedId ledId);
|
bool foundLedId = InputKeyUtilities.MouseButtonLedIdMap.TryGetValue(e.Button, out LedId ledId);
|
||||||
|
|||||||
@ -23,7 +23,10 @@ internal class DataBindingExitNode<TLayerProperty> : Node, IExitNode
|
|||||||
public void ApplyToDataBinding()
|
public void ApplyToDataBinding()
|
||||||
{
|
{
|
||||||
foreach ((IDataBindingProperty? property, object? pendingValue) in _propertyValues)
|
foreach ((IDataBindingProperty? property, object? pendingValue) in _propertyValues)
|
||||||
property.SetValue(pendingValue);
|
{
|
||||||
|
if (pendingValue != null)
|
||||||
|
property.SetValue(pendingValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Evaluate()
|
public override void Evaluate()
|
||||||
|
|||||||
@ -1,23 +1,20 @@
|
|||||||
using System;
|
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;
|
namespace Artemis.Core.Internal;
|
||||||
|
|
||||||
internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode
|
internal class EventConditionEventStartNode : DefaultNode
|
||||||
{
|
{
|
||||||
internal static readonly Guid NodeId = new("278735FE-69E9-4A73-A6B8-59E83EE19305");
|
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;
|
private IDataModelEvent? _dataModelEvent;
|
||||||
|
|
||||||
public EventConditionEventStartNode() : base(NodeId, "Event Arguments", "Contains the event arguments that triggered the evaluation")
|
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)
|
public void CreatePins(IDataModelEvent? dataModelEvent)
|
||||||
@ -25,30 +22,8 @@ internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode
|
|||||||
if (_dataModelEvent == dataModelEvent)
|
if (_dataModelEvent == dataModelEvent)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
while (Pins.Any())
|
|
||||||
RemovePin((Pin) Pins.First());
|
|
||||||
_propertyPins.Clear();
|
|
||||||
|
|
||||||
_dataModelEvent = dataModelEvent;
|
_dataModelEvent = dataModelEvent;
|
||||||
if (dataModelEvent == null)
|
_objectOutputPins.ChangeType(dataModelEvent?.ArgumentsType);
|
||||||
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()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Evaluate()
|
public override void Evaluate()
|
||||||
@ -56,12 +31,6 @@ internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode
|
|||||||
if (_dataModelEvent?.LastEventArgumentsUntyped == null)
|
if (_dataModelEvent?.LastEventArgumentsUntyped == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
foreach ((Func<DataModelEventArgs, object> propertyAccessor, OutputPin outputPin) in _propertyPins)
|
_objectOutputPins.SetCurrentValue(_dataModelEvent.LastEventArgumentsUntyped);
|
||||||
{
|
|
||||||
if (!outputPin.ConnectedTo.Any())
|
|
||||||
continue;
|
|
||||||
object value = _dataModelEvent.LastEventArgumentsUntyped != null ? propertyAccessor(_dataModelEvent.LastEventArgumentsUntyped) : outputPin.Type.GetDefault()!;
|
|
||||||
outputPin.Value = outputPin.IsNumeric ? new Numeric(value) : value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,9 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using Artemis.Core.VisualScripting.Internal;
|
|
||||||
|
|
||||||
namespace Artemis.Core.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");
|
internal static readonly Guid NodeId = new("F9A270DB-A231-4800-BAB3-DC1F96856756");
|
||||||
private object? _newValue;
|
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
|
#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 />
|
/// <inheritdoc />
|
||||||
public string Name { get; }
|
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
|
/// The context of the node script, usually a <see cref="Profile" /> or
|
||||||
/// <see cref="ProfileConfiguration" />
|
/// <see cref="ProfileConfiguration" />
|
||||||
/// </param>
|
/// </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;
|
Name = name;
|
||||||
Description = description;
|
Description = description;
|
||||||
@ -87,9 +91,13 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript
|
|||||||
|
|
||||||
NodeTypeStore.NodeTypeAdded += NodeTypeStoreOnNodeTypeAdded;
|
NodeTypeStore.NodeTypeAdded += NodeTypeStoreOnNodeTypeAdded;
|
||||||
NodeTypeStore.NodeTypeRemoved += NodeTypeStoreOnNodeTypeRemoved;
|
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;
|
Name = name;
|
||||||
Description = description;
|
Description = description;
|
||||||
@ -99,6 +107,10 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript
|
|||||||
|
|
||||||
NodeTypeStore.NodeTypeAdded += NodeTypeStoreOnNodeTypeAdded;
|
NodeTypeStore.NodeTypeAdded += NodeTypeStoreOnNodeTypeAdded;
|
||||||
NodeTypeStore.NodeTypeRemoved += NodeTypeStoreOnNodeTypeRemoved;
|
NodeTypeStore.NodeTypeRemoved += NodeTypeStoreOnNodeTypeRemoved;
|
||||||
|
|
||||||
|
if (defaultNodes != null)
|
||||||
|
foreach (DefaultNode defaultNode in defaultNodes)
|
||||||
|
AddNode(defaultNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -410,8 +422,9 @@ public class NodeScript<T> : NodeScript, INodeScript<T>
|
|||||||
|
|
||||||
#region Constructors
|
#region Constructors
|
||||||
|
|
||||||
internal NodeScript(string name, string description, NodeScriptEntity entity, object? context = null)
|
/// <inheritdoc />
|
||||||
: base(name, description, entity, context)
|
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);
|
ExitNode = new ExitNode<T>(name, description);
|
||||||
AddNode(ExitNode);
|
AddNode(ExitNode);
|
||||||
@ -420,8 +433,8 @@ public class NodeScript<T> : NodeScript, INodeScript<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public NodeScript(string name, string description, object? context = null)
|
public NodeScript(string name, string description, object? context = null, List<DefaultNode>? defaultNodes = null)
|
||||||
: base(name, description, context)
|
: base(name, description, context, defaultNodes)
|
||||||
{
|
{
|
||||||
ExitNode = new ExitNode<T>(name, description);
|
ExitNode = new ExitNode<T>(name, description);
|
||||||
AddNode(ExitNode);
|
AddNode(ExitNode);
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
@ -1,18 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace Artemis.Core.Internal;
|
namespace Artemis.Core;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a kind of node that cannot be deleted inside a <see cref="INode" />.
|
|
||||||
/// </summary>
|
|
||||||
public interface IDefaultNode : INode
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a kind of node that cannot be deleted inside a <see cref="NodeScript" />.
|
/// Represents a kind of node that cannot be deleted inside a <see cref="NodeScript" />.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class DefaultNode : Node, IDefaultNode
|
public abstract class DefaultNode : Node
|
||||||
{
|
{
|
||||||
#region Constructors
|
#region Constructors
|
||||||
|
|
||||||
@ -20,8 +13,6 @@ public abstract class DefaultNode : Node, IDefaultNode
|
|||||||
protected DefaultNode(Guid id, string name, string description = "") : base(name, description)
|
protected DefaultNode(Guid id, string name, string description = "") : base(name, description)
|
||||||
{
|
{
|
||||||
Id = id;
|
Id = id;
|
||||||
Name = name;
|
|
||||||
Description = description;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#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);
|
||||||
|
}
|
||||||
@ -2,10 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
|
||||||
using Artemis.Core.Events;
|
using Artemis.Core.Events;
|
||||||
using Ninject;
|
|
||||||
using Ninject.Parameters;
|
|
||||||
|
|
||||||
namespace Artemis.Core;
|
namespace Artemis.Core;
|
||||||
|
|
||||||
@ -132,7 +129,7 @@ public abstract class Node : BreakableModel, INode
|
|||||||
/// <param name="name">The name of the pin</param>
|
/// <param name="name">The name of the pin</param>
|
||||||
/// <typeparam name="T">The type of value the pin will hold</typeparam>
|
/// <typeparam name="T">The type of value the pin will hold</typeparam>
|
||||||
/// <returns>The newly created pin</returns>
|
/// <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);
|
InputPin<T> pin = new(this, name);
|
||||||
_pins.Add(pin);
|
_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="type">The type of value the pin will hold</param>
|
||||||
/// <param name="name">The name of the pin</param>
|
/// <param name="name">The name of the pin</param>
|
||||||
/// <returns>The newly created pin</returns>
|
/// <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);
|
InputPin pin = new(this, type, name);
|
||||||
_pins.Add(pin);
|
_pins.Add(pin);
|
||||||
@ -160,7 +157,7 @@ public abstract class Node : BreakableModel, INode
|
|||||||
/// <param name="name">The name of the pin</param>
|
/// <param name="name">The name of the pin</param>
|
||||||
/// <typeparam name="T">The type of value the pin will hold</typeparam>
|
/// <typeparam name="T">The type of value the pin will hold</typeparam>
|
||||||
/// <returns>The newly created pin</returns>
|
/// <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);
|
OutputPin<T> pin = new(this, name);
|
||||||
_pins.Add(pin);
|
_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="type">The type of value the pin will hold</param>
|
||||||
/// <param name="name">The name of the pin</param>
|
/// <param name="name">The name of the pin</param>
|
||||||
/// <returns>The newly created pin</returns>
|
/// <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);
|
OutputPin pin = new(this, type, name);
|
||||||
_pins.Add(pin);
|
_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
|
/// 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.
|
/// editor.
|
||||||
/// </summary>
|
/// </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
|
// Grab the first pin from the bucket that isn't on the node yet
|
||||||
OutputPin? pin = _outputPinBucket.FirstOrDefault(p => !Pins.Contains(p));
|
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
|
/// 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.
|
/// editor.
|
||||||
/// </summary>
|
/// </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
|
// Grab the first pin from the bucket that isn't on the node yet
|
||||||
InputPin? pin = _inputPinBucket.FirstOrDefault(p => !Pins.Contains(p));
|
InputPin? pin = _inputPinBucket.FirstOrDefault(p => !Pins.Contains(p));
|
||||||
@ -247,7 +244,7 @@ public abstract class Node : BreakableModel, INode
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pin">The pin to remove</param>
|
/// <param name="pin">The pin to remove</param>
|
||||||
/// <returns><see langword="true" /> if the pin was removed; otherwise <see langword="false" />.</returns>
|
/// <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);
|
bool isRemoved = _pins.Remove(pin);
|
||||||
if (isRemoved)
|
if (isRemoved)
|
||||||
@ -263,7 +260,7 @@ public abstract class Node : BreakableModel, INode
|
|||||||
/// Adds an existing <paramref name="pin" /> to the <see cref="Pins" /> collection.
|
/// Adds an existing <paramref name="pin" /> to the <see cref="Pins" /> collection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pin">The pin to add</param>
|
/// <param name="pin">The pin to add</param>
|
||||||
protected void AddPin(Pin pin)
|
public void AddPin(Pin pin)
|
||||||
{
|
{
|
||||||
if (pin.Node != this)
|
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.");
|
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="name">The name of the pin collection</param>
|
||||||
/// <param name="initialCount">The amount of pins to initially add to the collection</param>
|
/// <param name="initialCount">The amount of pins to initially add to the collection</param>
|
||||||
/// <returns>The resulting input pin collection</returns>
|
/// <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);
|
InputPinCollection<T> pin = new(this, name, initialCount);
|
||||||
_pinCollections.Add(pin);
|
_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="name">The name of the pin collection</param>
|
||||||
/// <param name="initialCount">The amount of pins to initially add to the collection</param>
|
/// <param name="initialCount">The amount of pins to initially add to the collection</param>
|
||||||
/// <returns>The resulting input pin collection</returns>
|
/// <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);
|
InputPinCollection pin = new(this, type, name, initialCount);
|
||||||
_pinCollections.Add(pin);
|
_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="name">The name of the pin collection</param>
|
||||||
/// <param name="initialCount">The amount of pins to initially add to the collection</param>
|
/// <param name="initialCount">The amount of pins to initially add to the collection</param>
|
||||||
/// <returns>The resulting output pin collection</returns>
|
/// <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);
|
OutputPinCollection<T> pin = new(this, name, initialCount);
|
||||||
_pinCollections.Add(pin);
|
_pinCollections.Add(pin);
|
||||||
@ -325,7 +322,7 @@ public abstract class Node : BreakableModel, INode
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="pinCollection">The pin collection to remove</param>
|
/// <param name="pinCollection">The pin collection to remove</param>
|
||||||
/// <returns><see langword="true" /> if the pin collection was removed; otherwise <see langword="false" />.</returns>
|
/// <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);
|
bool isRemoved = _pinCollections.Remove(pinCollection);
|
||||||
if (isRemoved)
|
if (isRemoved)
|
||||||
@ -339,7 +336,8 @@ public abstract class Node : BreakableModel, INode
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <param name="script">The script the node is contained in</param>
|
/// <param name="script">The script the node is contained in</param>
|
||||||
public virtual void Initialize(INodeScript script)
|
public virtual void Initialize(INodeScript script)
|
||||||
@ -374,16 +372,6 @@ public abstract class Node : BreakableModel, INode
|
|||||||
TryOrBreak(Evaluate, "Failed to evaluate");
|
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>
|
/// <summary>
|
||||||
/// Serializes the <see cref="Storage" /> object into a string
|
/// Serializes the <see cref="Storage" /> object into a string
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -403,101 +391,3 @@ public abstract class Node : BreakableModel, INode
|
|||||||
|
|
||||||
#endregion
|
#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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
52
src/Artemis.Core/VisualScripting/Nodes/NodeTStorage.cs
Normal file
52
src/Artemis.Core/VisualScripting/Nodes/NodeTStorage.cs
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
142
src/Artemis.Core/VisualScripting/Pins/ObjectOutputPins.cs
Normal file
142
src/Artemis.Core/VisualScripting/Pins/ObjectOutputPins.cs
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/Artemis.Storage/Migrations/M0021GradientNodes.cs
Normal file
87
src/Artemis.Storage/Migrations/M0021GradientNodes.cs
Normal file
@ -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<ProfileEntity> collection = repository.Database.GetCollection<ProfileEntity>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -129,7 +129,7 @@ public class ArtemisIcon : UserControl
|
|||||||
/// theme
|
/// theme
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly StyledProperty<bool> FillProperty =
|
public static readonly StyledProperty<bool> FillProperty =
|
||||||
AvaloniaProperty.Register<ArtemisIcon, bool>(nameof(Icon), true, notifying: IconChanging);
|
AvaloniaProperty.Register<ArtemisIcon, bool>(nameof(Icon), false, notifying: IconChanging);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a boolean indicating whether or not the icon should be filled in with the primary text color of the
|
/// Gets or sets a boolean indicating whether or not the icon should be filled in with the primary text color of the
|
||||||
|
|||||||
@ -59,22 +59,29 @@ public class GradientPicker : TemplatedControl
|
|||||||
public static readonly DirectProperty<GradientPicker, ICommand> DeleteStopProperty =
|
public static readonly DirectProperty<GradientPicker, ICommand> DeleteStopProperty =
|
||||||
AvaloniaProperty.RegisterDirect<GradientPicker, ICommand>(nameof(DeleteStop), g => g.DeleteStop);
|
AvaloniaProperty.RegisterDirect<GradientPicker, ICommand>(nameof(DeleteStop), g => g.DeleteStop);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the color gradient currently being edited, for internal use only
|
||||||
|
/// </summary>
|
||||||
|
public static readonly DirectProperty<GradientPicker, ColorGradient> EditingColorGradientProperty =
|
||||||
|
AvaloniaProperty.RegisterDirect<GradientPicker, ColorGradient>(nameof(EditingColorGradient), g => g.EditingColorGradient);
|
||||||
|
|
||||||
private readonly ICommand _deleteStop;
|
private readonly ICommand _deleteStop;
|
||||||
private ColorPicker? _colorPicker;
|
private ColorPicker? _colorPicker;
|
||||||
private Button? _flipStops;
|
private Button? _flipStops;
|
||||||
private Border? _gradient;
|
private Border? _gradient;
|
||||||
private ColorGradient? _lastColorGradient;
|
|
||||||
private Button? _randomize;
|
private Button? _randomize;
|
||||||
private Button? _rotateStops;
|
private Button? _rotateStops;
|
||||||
private bool _shiftDown;
|
private bool _shiftDown;
|
||||||
private Button? _spreadStops;
|
private Button? _spreadStops;
|
||||||
private Button? _toggleSeamless;
|
private Button? _toggleSeamless;
|
||||||
|
private ColorGradient _colorGradient = null!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new instance of the <see cref="GradientPicker" /> class.
|
/// Creates a new instance of the <see cref="GradientPicker" /> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public GradientPicker()
|
public GradientPicker()
|
||||||
{
|
{
|
||||||
|
EditingColorGradient = ColorGradient.GetUnicornBarf();
|
||||||
_deleteStop = ReactiveCommand.Create<ColorGradientStop>(s =>
|
_deleteStop = ReactiveCommand.Create<ColorGradientStop>(s =>
|
||||||
{
|
{
|
||||||
if (ColorGradient.Count <= 2)
|
if (ColorGradient.Count <= 2)
|
||||||
@ -144,6 +151,15 @@ public class GradientPicker : TemplatedControl
|
|||||||
private init => SetAndRaise(DeleteStopProperty, ref _deleteStop, value);
|
private init => SetAndRaise(DeleteStopProperty, ref _deleteStop, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the color gradient backing the editor, this is a copy of <see cref="ColorGradient"/>.
|
||||||
|
/// </summary>
|
||||||
|
public ColorGradient EditingColorGradient
|
||||||
|
{
|
||||||
|
get => _colorGradient;
|
||||||
|
private set => SetAndRaise(EditingColorGradientProperty, ref _colorGradient, value);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||||
{
|
{
|
||||||
@ -188,7 +204,7 @@ public class GradientPicker : TemplatedControl
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||||
{
|
{
|
||||||
Subscribe();
|
ApplyToField();
|
||||||
|
|
||||||
KeyUp += OnKeyUp;
|
KeyUp += OnKeyUp;
|
||||||
KeyDown += OnKeyDown;
|
KeyDown += OnKeyDown;
|
||||||
@ -197,60 +213,72 @@ public class GradientPicker : TemplatedControl
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||||
{
|
{
|
||||||
Unsubscribe();
|
ApplyToProperty();
|
||||||
KeyUp -= OnKeyUp;
|
KeyUp -= OnKeyUp;
|
||||||
KeyDown -= OnKeyDown;
|
KeyDown -= OnKeyDown;
|
||||||
|
|
||||||
_shiftDown = false;
|
_shiftDown = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static void ColorGradientChanged(IAvaloniaObject sender, bool before)
|
private static void ColorGradientChanged(IAvaloniaObject sender, bool before)
|
||||||
{
|
{
|
||||||
(sender as GradientPicker)?.Subscribe();
|
(sender as GradientPicker)?.ApplyToField();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void StorageProviderChanged(IAvaloniaObject sender, bool before)
|
private static void StorageProviderChanged(IAvaloniaObject sender, bool before)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Subscribe()
|
private void ApplyToField()
|
||||||
{
|
{
|
||||||
Unsubscribe();
|
EditingColorGradient = new ColorGradient(ColorGradient);
|
||||||
|
EditingColorGradient.CollectionChanged += ColorGradientOnCollectionChanged;
|
||||||
ColorGradient.CollectionChanged += ColorGradientOnCollectionChanged;
|
EditingColorGradient.StopChanged += ColorGradientOnStopChanged;
|
||||||
ColorGradient.StopChanged += ColorGradientOnStopChanged;
|
SelectedColorStop = EditingColorGradient.FirstOrDefault();
|
||||||
SelectedColorStop = ColorGradient.FirstOrDefault();
|
|
||||||
|
|
||||||
UpdateGradient();
|
UpdateGradient();
|
||||||
|
|
||||||
_lastColorGradient = ColorGradient;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Unsubscribe()
|
private void ApplyToProperty()
|
||||||
{
|
{
|
||||||
if (_lastColorGradient == null)
|
// Remove extra color gradients
|
||||||
return;
|
while (ColorGradient.Count > EditingColorGradient.Count)
|
||||||
|
ColorGradient.RemoveAt(ColorGradient.Count - 1);
|
||||||
|
|
||||||
_lastColorGradient.CollectionChanged -= ColorGradientOnCollectionChanged;
|
for (int index = 0; index < EditingColorGradient.Count; index++)
|
||||||
_lastColorGradient.StopChanged -= ColorGradientOnStopChanged;
|
{
|
||||||
_lastColorGradient = null;
|
ColorGradientStop colorGradientStop = EditingColorGradient[index];
|
||||||
|
// Add missing color gradients
|
||||||
|
if (index >= ColorGradient.Count)
|
||||||
|
{
|
||||||
|
ColorGradient.Add(new ColorGradientStop(colorGradientStop.Color, colorGradientStop.Position));
|
||||||
|
}
|
||||||
|
// Update existing color gradients
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ColorGradient[index].Color = colorGradientStop.Color;
|
||||||
|
ColorGradient[index].Position = colorGradientStop.Position;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ColorGradientOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
private void ColorGradientOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
UpdateGradient();
|
UpdateGradient();
|
||||||
|
ApplyToProperty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ColorGradientOnStopChanged(object? sender, EventArgs e)
|
private void ColorGradientOnStopChanged(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
UpdateGradient();
|
UpdateGradient();
|
||||||
|
ApplyToProperty();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateGradient()
|
private void UpdateGradient()
|
||||||
{
|
{
|
||||||
// Update the display gradient
|
// Update the display gradient
|
||||||
GradientStops collection = new();
|
GradientStops collection = new();
|
||||||
foreach (ColorGradientStop c in ColorGradient.OrderBy(s => s.Position))
|
foreach (ColorGradientStop c in EditingColorGradient.OrderBy(s => s.Position))
|
||||||
collection.Add(new GradientStop(Color.FromArgb(c.Color.Alpha, c.Color.Red, c.Color.Green, c.Color.Blue), c.Position));
|
collection.Add(new GradientStop(Color.FromArgb(c.Color.Alpha, c.Color.Red, c.Color.Green, c.Color.Blue), c.Position));
|
||||||
|
|
||||||
LinearGradientBrush.GradientStops = collection;
|
LinearGradientBrush.GradientStops = collection;
|
||||||
@ -263,8 +291,8 @@ public class GradientPicker : TemplatedControl
|
|||||||
|
|
||||||
float position = (float) (e.GetPosition(_gradient).X / _gradient.Bounds.Width);
|
float position = (float) (e.GetPosition(_gradient).X / _gradient.Bounds.Width);
|
||||||
|
|
||||||
ColorGradientStop newStop = new(ColorGradient.GetColor(position), position);
|
ColorGradientStop newStop = new(EditingColorGradient.GetColor(position), position);
|
||||||
ColorGradient.Add(newStop);
|
EditingColorGradient.Add(newStop);
|
||||||
SelectedColorStop = newStop;
|
SelectedColorStop = newStop;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,50 +307,50 @@ public class GradientPicker : TemplatedControl
|
|||||||
if (e.Key is Key.LeftShift or Key.RightShift)
|
if (e.Key is Key.LeftShift or Key.RightShift)
|
||||||
_shiftDown = false;
|
_shiftDown = false;
|
||||||
|
|
||||||
if (e.Key != Key.Delete || SelectedColorStop == null || ColorGradient.Count <= 2)
|
if (e.Key != Key.Delete || SelectedColorStop == null || EditingColorGradient.Count <= 2)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
int index = ColorGradient.IndexOf(SelectedColorStop);
|
int index = EditingColorGradient.IndexOf(SelectedColorStop);
|
||||||
ColorGradient.Remove(SelectedColorStop);
|
EditingColorGradient.Remove(SelectedColorStop);
|
||||||
if (index > ColorGradient.Count - 1)
|
if (index > EditingColorGradient.Count - 1)
|
||||||
index--;
|
index--;
|
||||||
|
|
||||||
SelectedColorStop = ColorGradient.ElementAtOrDefault(index);
|
SelectedColorStop = EditingColorGradient.ElementAtOrDefault(index);
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SpreadStopsOnClick(object? sender, RoutedEventArgs e)
|
private void SpreadStopsOnClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
ColorGradient.SpreadStops();
|
EditingColorGradient.SpreadStops();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ToggleSeamlessOnClick(object? sender, RoutedEventArgs e)
|
private void ToggleSeamlessOnClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (SelectedColorStop == null || ColorGradient.Count < 2)
|
if (SelectedColorStop == null || EditingColorGradient.Count < 2)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ColorGradient.ToggleSeamless();
|
EditingColorGradient.ToggleSeamless();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FlipStopsOnClick(object? sender, RoutedEventArgs e)
|
private void FlipStopsOnClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (SelectedColorStop == null || ColorGradient.Count < 2)
|
if (SelectedColorStop == null || EditingColorGradient.Count < 2)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ColorGradient.FlipStops();
|
EditingColorGradient.FlipStops();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RotateStopsOnClick(object? sender, RoutedEventArgs e)
|
private void RotateStopsOnClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (SelectedColorStop == null || ColorGradient.Count < 2)
|
if (SelectedColorStop == null || EditingColorGradient.Count < 2)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ColorGradient.RotateStops(_shiftDown);
|
EditingColorGradient.RotateStops(_shiftDown);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RandomizeOnClick(object? sender, RoutedEventArgs e)
|
private void RandomizeOnClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
ColorGradient.Randomize(6);
|
EditingColorGradient.Randomize(6);
|
||||||
SelectedColorStop = ColorGradient.First();
|
SelectedColorStop = EditingColorGradient.First();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -46,9 +46,6 @@ public class GradientPickerButton : TemplatedControl
|
|||||||
AvaloniaProperty.RegisterDirect<GradientPickerButton, LinearGradientBrush>(nameof(LinearGradientBrush), g => g.LinearGradientBrush);
|
AvaloniaProperty.RegisterDirect<GradientPickerButton, LinearGradientBrush>(nameof(LinearGradientBrush), g => g.LinearGradientBrush);
|
||||||
|
|
||||||
private Button? _button;
|
private Button? _button;
|
||||||
private GradientPickerFlyout? _flyout;
|
|
||||||
private bool _flyoutActive;
|
|
||||||
|
|
||||||
private ColorGradient? _lastColorGradient;
|
private ColorGradient? _lastColorGradient;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -150,27 +147,23 @@ public class GradientPickerButton : TemplatedControl
|
|||||||
|
|
||||||
private void OnButtonClick(object? sender, RoutedEventArgs e)
|
private void OnButtonClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (_flyout == null || ColorGradient == null)
|
if (ColorGradient == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Logic here is taken from Fluent Avalonia's ColorPicker which also reuses the same control since it's large
|
GradientPickerFlyout flyout = new();
|
||||||
_flyout.GradientPicker.ColorGradient = ColorGradient;
|
flyout.FlyoutPresenterClasses.Add("gradient-picker-presenter");
|
||||||
_flyout.GradientPicker.IsCompact = IsCompact;
|
flyout.GradientPicker.ColorGradient = ColorGradient;
|
||||||
_flyout.GradientPicker.StorageProvider = StorageProvider;
|
flyout.GradientPicker.IsCompact = IsCompact;
|
||||||
|
flyout.GradientPicker.StorageProvider = StorageProvider;
|
||||||
_flyout.ShowAt(this);
|
|
||||||
_flyoutActive = true;
|
|
||||||
|
|
||||||
|
flyout.Closed += FlyoutOnClosed;
|
||||||
|
flyout.ShowAt(this);
|
||||||
FlyoutOpened?.Invoke(this, EventArgs.Empty);
|
FlyoutOpened?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
|
||||||
|
|
||||||
|
void FlyoutOnClosed(object? closedSender, EventArgs closedEventArgs)
|
||||||
private void OnFlyoutClosed(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
if (_flyoutActive)
|
|
||||||
{
|
{
|
||||||
|
flyout.Closed -= FlyoutOnClosed;
|
||||||
FlyoutClosed?.Invoke(this, EventArgs.Empty);
|
FlyoutClosed?.Invoke(this, EventArgs.Empty);
|
||||||
_flyoutActive = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,22 +184,12 @@ public class GradientPickerButton : TemplatedControl
|
|||||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||||
{
|
{
|
||||||
Subscribe();
|
Subscribe();
|
||||||
|
|
||||||
if (_flyout == null)
|
|
||||||
{
|
|
||||||
_flyout = new GradientPickerFlyout();
|
|
||||||
_flyout.FlyoutPresenterClasses.Add("gradient-picker-presenter");
|
|
||||||
}
|
|
||||||
|
|
||||||
_flyout.Closed += OnFlyoutClosed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||||
{
|
{
|
||||||
Unsubscribe();
|
Unsubscribe();
|
||||||
if (_flyout != null)
|
|
||||||
_flyout.Closed -= OnFlyoutClosed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
using Artemis.Core;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Shared.Services.NodeEditor;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the base of the node script editor window view model.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class NodeScriptWindowViewModelBase : DialogViewModelBase<bool>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of the <see cref="NodeScriptWindowViewModelBase" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="nodeScript">The node script being edited.</param>
|
||||||
|
protected NodeScriptWindowViewModelBase(NodeScript nodeScript)
|
||||||
|
{
|
||||||
|
NodeScript = nodeScript;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the node script being edited.
|
||||||
|
/// </summary>
|
||||||
|
public NodeScript NodeScript { get; init; }
|
||||||
|
}
|
||||||
@ -19,7 +19,7 @@ public class UpdateColorGradient : IProfileEditorCommand
|
|||||||
public UpdateColorGradient(ColorGradient colorGradient, List<ColorGradientStop> stops, List<ColorGradientStop>? originalStops)
|
public UpdateColorGradient(ColorGradient colorGradient, List<ColorGradientStop> stops, List<ColorGradientStop>? originalStops)
|
||||||
{
|
{
|
||||||
_colorGradient = colorGradient;
|
_colorGradient = colorGradient;
|
||||||
_stops = stops;
|
_stops = stops.Select(s => new ColorGradientStop(s.Color, s.Position)).ToList();
|
||||||
_originalStops = originalStops ?? _colorGradient.Select(s => new ColorGradientStop(s.Color, s.Position)).ToList();
|
_originalStops = originalStops ?? _colorGradient.Select(s => new ColorGradientStop(s.Color, s.Position)).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,18 +31,36 @@ public class UpdateColorGradient : IProfileEditorCommand
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Execute()
|
public void Execute()
|
||||||
{
|
{
|
||||||
_colorGradient.Clear();
|
ApplyStops(_stops);
|
||||||
foreach (ColorGradientStop colorGradientStop in _stops)
|
|
||||||
_colorGradient.Add(colorGradientStop);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Undo()
|
public void Undo()
|
||||||
{
|
{
|
||||||
_colorGradient.Clear();
|
ApplyStops(_originalStops);
|
||||||
foreach (ColorGradientStop colorGradientStop in _originalStops)
|
|
||||||
_colorGradient.Add(colorGradientStop);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
private void ApplyStops(List<ColorGradientStop> stops)
|
||||||
|
{
|
||||||
|
while (_colorGradient.Count > stops.Count)
|
||||||
|
_colorGradient.RemoveAt(_colorGradient.Count - 1);
|
||||||
|
|
||||||
|
for (int index = 0; index < stops.Count; index++)
|
||||||
|
{
|
||||||
|
ColorGradientStop colorGradientStop = stops[index];
|
||||||
|
// Add missing color gradients
|
||||||
|
if (index >= _colorGradient.Count)
|
||||||
|
{
|
||||||
|
_colorGradient.Add(new ColorGradientStop(colorGradientStop.Color, colorGradientStop.Position));
|
||||||
|
}
|
||||||
|
// Update existing color gradients
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_colorGradient[index].Color = colorGradientStop.Color;
|
||||||
|
_colorGradient[index].Position = colorGradientStop.Position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -49,7 +49,7 @@ public class UpdateEventConditionPath : IProfileEditorCommand, IDisposable
|
|||||||
|
|
||||||
// Change the end node
|
// Change the end node
|
||||||
_eventCondition.EventPath = _value;
|
_eventCondition.EventPath = _value;
|
||||||
_eventCondition.UpdateEventNode();
|
_eventCondition.UpdateEventNode(true);
|
||||||
|
|
||||||
_executed = true;
|
_executed = true;
|
||||||
}
|
}
|
||||||
@ -59,7 +59,7 @@ public class UpdateEventConditionPath : IProfileEditorCommand, IDisposable
|
|||||||
{
|
{
|
||||||
// Change the end node
|
// Change the end node
|
||||||
_eventCondition.EventPath = _oldValue;
|
_eventCondition.EventPath = _oldValue;
|
||||||
_eventCondition.UpdateEventNode();
|
_eventCondition.UpdateEventNode(true);
|
||||||
|
|
||||||
// Restore old connections
|
// Restore old connections
|
||||||
_store?.Restore();
|
_store?.Restore();
|
||||||
|
|||||||
@ -103,4 +103,11 @@
|
|||||||
<Setter Property="Padding" Value="6 3 11 3" />
|
<Setter Property="Padding" Value="6 3 11 3" />
|
||||||
<Setter Property="Height" Value="24" />
|
<Setter Property="Height" Value="24" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Button.condensed">
|
||||||
|
<Setter Property="Padding" Value="1" />
|
||||||
|
<Setter Property="FontSize" Value="13" />
|
||||||
|
<Setter Property="MinHeight" Value="24" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
</Styles>
|
</Styles>
|
||||||
@ -73,7 +73,6 @@
|
|||||||
|
|
||||||
<Style Selector="gradientPicker|GradientPicker">
|
<Style Selector="gradientPicker|GradientPicker">
|
||||||
<Style.Resources>
|
<Style.Resources>
|
||||||
<converters:ColorGradientToGradientStopsConverter x:Key="ColorGradientToGradientStopsConverter" />
|
|
||||||
<converters:SKColorToColorConverter x:Key="SKColorToColorConverter" />
|
<converters:SKColorToColorConverter x:Key="SKColorToColorConverter" />
|
||||||
<converters:SKColorToBrushConverter x:Key="SKColorToBrushConverter" />
|
<converters:SKColorToBrushConverter x:Key="SKColorToBrushConverter" />
|
||||||
<converters:SKColorToStringConverter x:Key="SKColorToStringConverter" />
|
<converters:SKColorToStringConverter x:Key="SKColorToStringConverter" />
|
||||||
@ -89,7 +88,7 @@
|
|||||||
Background="{DynamicResource LightCheckerboardBrush}"
|
Background="{DynamicResource LightCheckerboardBrush}"
|
||||||
Margin="5 0">
|
Margin="5 0">
|
||||||
<Border Background="{TemplateBinding LinearGradientBrush}">
|
<Border Background="{TemplateBinding LinearGradientBrush}">
|
||||||
<ItemsControl Name="GradientStops" Items="{TemplateBinding ColorGradient}" ClipToBounds="False">
|
<ItemsControl Name="GradientStops" Items="{TemplateBinding EditingColorGradient}" ClipToBounds="False">
|
||||||
<ItemsControl.Styles>
|
<ItemsControl.Styles>
|
||||||
<Style Selector="ItemsControl#GradientStops > ContentPresenter">
|
<Style Selector="ItemsControl#GradientStops > ContentPresenter">
|
||||||
<Setter Property="Canvas.Left">
|
<Setter Property="Canvas.Left">
|
||||||
@ -105,9 +104,9 @@
|
|||||||
<ItemsControl.ItemTemplate>
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate DataType="core:ColorGradientStop">
|
<DataTemplate DataType="core:ColorGradientStop">
|
||||||
<gradientPicker:GradientPickerColorStop ColorStop="{Binding}"
|
<gradientPicker:GradientPickerColorStop ColorStop="{Binding}"
|
||||||
PositionReference="{Binding $parent[Border]}"
|
PositionReference="{Binding $parent[Border]}"
|
||||||
Classes="gradient-handle"
|
Classes="gradient-handle"
|
||||||
GradientPicker="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type gradientPicker:GradientPicker}}}">
|
GradientPicker="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type gradientPicker:GradientPicker}}}">
|
||||||
</gradientPicker:GradientPickerColorStop>
|
</gradientPicker:GradientPickerColorStop>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
@ -135,7 +134,7 @@
|
|||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Grid.ColumnSpan="2"
|
Grid.ColumnSpan="2"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Items="{TemplateBinding ColorGradient}"
|
Items="{TemplateBinding EditingColorGradient}"
|
||||||
ClipToBounds="False"
|
ClipToBounds="False"
|
||||||
Margin="5 0">
|
Margin="5 0">
|
||||||
<ItemsControl.Styles>
|
<ItemsControl.Styles>
|
||||||
@ -180,9 +179,10 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Grid Grid.Row="2" Grid.Column="1" RowDefinitions="*,Auto">
|
<Grid Grid.Row="2" Grid.Column="1" RowDefinitions="*,Auto">
|
||||||
<ListBox Grid.Row="0"
|
<ListBox Name="GradientColors"
|
||||||
|
Grid.Row="0"
|
||||||
MaxHeight="280"
|
MaxHeight="280"
|
||||||
Items="{TemplateBinding ColorGradient}"
|
Items="{TemplateBinding EditingColorGradient}"
|
||||||
SelectedItem="{TemplateBinding SelectedColorStop, Mode=TwoWay}"
|
SelectedItem="{TemplateBinding SelectedColorStop, Mode=TwoWay}"
|
||||||
Padding="10 0 15 0">
|
Padding="10 0 15 0">
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
@ -201,7 +201,7 @@
|
|||||||
<TextBox Grid.Column="1" Text="{Binding Color, Converter={StaticResource SKColorToStringConverter}}" />
|
<TextBox Grid.Column="1" Text="{Binding Color, Converter={StaticResource SKColorToStringConverter}}" />
|
||||||
<NumericUpDown Grid.Column="2" FormatString="F3" ShowButtonSpinner="False" Margin="5 0" Minimum="0" Maximum="1" Increment="0.01">
|
<NumericUpDown Grid.Column="2" FormatString="F3" ShowButtonSpinner="False" Margin="5 0" Minimum="0" Maximum="1" Increment="0.01">
|
||||||
<Interaction.Behaviors>
|
<Interaction.Behaviors>
|
||||||
<behaviors:LostFocusNumericUpDownBindingBehavior Value="{Binding Position}"/>
|
<behaviors:LostFocusNumericUpDownBindingBehavior Value="{Binding Position}" />
|
||||||
</Interaction.Behaviors>
|
</Interaction.Behaviors>
|
||||||
</NumericUpDown>
|
</NumericUpDown>
|
||||||
<Button Name="DeleteButton"
|
<Button Name="DeleteButton"
|
||||||
|
|||||||
@ -19,7 +19,6 @@ public class WindowsInputProvider : InputProvider
|
|||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly SpongeWindow _sponge;
|
private readonly SpongeWindow _sponge;
|
||||||
private readonly Timer _taskManagerTimer;
|
private readonly Timer _taskManagerTimer;
|
||||||
private DateTime _lastMouseUpdate;
|
|
||||||
private int _lastProcessId;
|
private int _lastProcessId;
|
||||||
|
|
||||||
public WindowsInputProvider(ILogger logger, IInputService inputService)
|
public WindowsInputProvider(ILogger logger, IInputService inputService)
|
||||||
@ -160,8 +159,8 @@ public class WindowsInputProvider : InputProvider
|
|||||||
|
|
||||||
#region Mouse
|
#region Mouse
|
||||||
|
|
||||||
private int _mouseDeltaX;
|
private int _previousMouseX;
|
||||||
private int _mouseDeltaY;
|
private int _previousMouseY;
|
||||||
|
|
||||||
private void HandleMouseData(RawInputData data, RawInputMouseData mouseData)
|
private void HandleMouseData(RawInputData data, RawInputMouseData mouseData)
|
||||||
{
|
{
|
||||||
@ -169,10 +168,8 @@ public class WindowsInputProvider : InputProvider
|
|||||||
// This can create a small inaccuracy of course, but Artemis is not a shooter :')
|
// This can create a small inaccuracy of course, but Artemis is not a shooter :')
|
||||||
if (mouseData.Mouse.Buttons == RawMouseButtonFlags.None)
|
if (mouseData.Mouse.Buttons == RawMouseButtonFlags.None)
|
||||||
{
|
{
|
||||||
_mouseDeltaX += mouseData.Mouse.LastX;
|
_previousMouseX += mouseData.Mouse.LastX;
|
||||||
_mouseDeltaY += mouseData.Mouse.LastY;
|
_previousMouseY += mouseData.Mouse.LastY;
|
||||||
if (DateTime.Now - _lastMouseUpdate < TimeSpan.FromMilliseconds(40))
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ArtemisDevice? device = null;
|
ArtemisDevice? device = null;
|
||||||
@ -193,10 +190,7 @@ public class WindowsInputProvider : InputProvider
|
|||||||
if (mouseData.Mouse.Buttons == RawMouseButtonFlags.None)
|
if (mouseData.Mouse.Buttons == RawMouseButtonFlags.None)
|
||||||
{
|
{
|
||||||
Win32Point cursorPosition = GetCursorPosition();
|
Win32Point cursorPosition = GetCursorPosition();
|
||||||
OnMouseMoveDataReceived(device, cursorPosition.X, cursorPosition.Y, _mouseDeltaX, _mouseDeltaY);
|
OnMouseMoveDataReceived(device, cursorPosition.X, cursorPosition.Y, cursorPosition.X - _previousMouseX, cursorPosition.Y - _previousMouseY);
|
||||||
_mouseDeltaX = 0;
|
|
||||||
_mouseDeltaY = 0;
|
|
||||||
_lastMouseUpdate = DateTime.Now;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -45,7 +45,7 @@ public class UpdateProvider : IUpdateProvider, IDisposable
|
|||||||
Url request = API_URL.AppendPathSegments("build", "builds")
|
Url request = API_URL.AppendPathSegments("build", "builds")
|
||||||
.SetQueryParam("definitions", buildDefinition)
|
.SetQueryParam("definitions", buildDefinition)
|
||||||
.SetQueryParam("resultFilter", "succeeded")
|
.SetQueryParam("resultFilter", "succeeded")
|
||||||
.SetQueryParam("branchName", "master")
|
.SetQueryParam("branchName", "refs/heads/master")
|
||||||
.SetQueryParam("$top", 1)
|
.SetQueryParam("$top", 1)
|
||||||
.SetQueryParam("api-version", "6.1-preview.6");
|
.SetQueryParam("api-version", "6.1-preview.6");
|
||||||
|
|
||||||
|
|||||||
@ -49,15 +49,16 @@ public class WindowSize
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// The -8 seems to be a FluentAvalonia thing?
|
||||||
_applying = true;
|
_applying = true;
|
||||||
if (IsMaximized)
|
if (IsMaximized)
|
||||||
{
|
{
|
||||||
window.Position = new PixelPoint(MaximizedLeft, MaximizedTop);
|
window.Position = new PixelPoint(Math.Max(-8, MaximizedLeft), Math.Max(-8, MaximizedTop));
|
||||||
window.WindowState = WindowState.Maximized;
|
window.WindowState = WindowState.Maximized;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
window.Position = new PixelPoint(Left, Top);
|
window.Position = new PixelPoint(Math.Max(-8, Left), Math.Max(-8, Top));
|
||||||
window.Height = Height;
|
window.Height = Height;
|
||||||
window.Width = Width;
|
window.Width = Width;
|
||||||
window.WindowState = WindowState.Normal;
|
window.WindowState = WindowState.Normal;
|
||||||
|
|||||||
@ -2,8 +2,10 @@
|
|||||||
using Artemis.UI.Ninject.Factories;
|
using Artemis.UI.Ninject.Factories;
|
||||||
using Artemis.UI.Ninject.InstanceProviders;
|
using Artemis.UI.Ninject.InstanceProviders;
|
||||||
using Artemis.UI.Screens;
|
using Artemis.UI.Screens;
|
||||||
|
using Artemis.UI.Screens.VisualScripting;
|
||||||
using Artemis.UI.Services.Interfaces;
|
using Artemis.UI.Services.Interfaces;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.UI.Shared.Services.NodeEditor;
|
||||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||||
using Avalonia.Platform;
|
using Avalonia.Platform;
|
||||||
using Avalonia.Shared.PlatformSupport;
|
using Avalonia.Shared.PlatformSupport;
|
||||||
@ -57,6 +59,7 @@ public class UIModule : NinjectModule
|
|||||||
.BindToFactory();
|
.BindToFactory();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Kernel.Bind<NodeScriptWindowViewModelBase>().To<NodeScriptWindowViewModel>();
|
||||||
Kernel.Bind<IPropertyVmFactory>().ToFactory(() => new LayerPropertyViewModelInstanceProvider());
|
Kernel.Bind<IPropertyVmFactory>().ToFactory(() => new LayerPropertyViewModelInstanceProvider());
|
||||||
|
|
||||||
// Bind all UI services as singletons
|
// Bind all UI services as singletons
|
||||||
|
|||||||
@ -70,7 +70,7 @@
|
|||||||
<Grid Margin="8" RowDefinitions="*,*">
|
<Grid Margin="8" RowDefinitions="*,*">
|
||||||
<controls:HyperlinkButton Grid.Row="0" NavigateUri="https://github.com/Artemis-RGB/Artemis">
|
<controls:HyperlinkButton Grid.Row="0" NavigateUri="https://github.com/Artemis-RGB/Artemis">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<avalonia:MaterialIcon Kind="Gift" />
|
<avalonia:MaterialIcon Kind="Github" />
|
||||||
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">GitHub</TextBlock>
|
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">GitHub</TextBlock>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</controls:HyperlinkButton>
|
</controls:HyperlinkButton>
|
||||||
|
|||||||
@ -25,12 +25,12 @@
|
|||||||
</Grid.ContextFlyout>
|
</Grid.ContextFlyout>
|
||||||
|
|
||||||
<!-- Icon column -->
|
<!-- Icon column -->
|
||||||
<shared:ArtemisIcon Grid.Column="0"
|
<avalonia:MaterialIcon Grid.Column="0"
|
||||||
Icon="{CompiledBinding FeatureInfo.ResolvedIcon}"
|
ToolTip.Tip="{CompiledBinding FeatureType}"
|
||||||
Fill="False"
|
Kind="{CompiledBinding FeatureIcon}"
|
||||||
Width="20"
|
Width="20"
|
||||||
Height="20"
|
Height="20"
|
||||||
IsVisible="{CompiledBinding LoadException, Converter={x:Static ObjectConverters.IsNull}}" />
|
IsVisible="{CompiledBinding LoadException, Converter={x:Static ObjectConverters.IsNull}}" />
|
||||||
|
|
||||||
<Button Grid.Column="0"
|
<Button Grid.Column="0"
|
||||||
Classes="AppBarButton icon-button"
|
Classes="AppBarButton icon-button"
|
||||||
|
|||||||
@ -5,11 +5,17 @@ using System.Reactive;
|
|||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
|
using Artemis.Core.DeviceProviders;
|
||||||
|
using Artemis.Core.LayerBrushes;
|
||||||
|
using Artemis.Core.LayerEffects;
|
||||||
|
using Artemis.Core.Modules;
|
||||||
|
using Artemis.Core.ScriptingProviders;
|
||||||
using Artemis.Core.Services;
|
using Artemis.Core.Services;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
using Artemis.UI.Shared.Services.Builders;
|
using Artemis.UI.Shared.Services.Builders;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
using Material.Icons;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.Plugins;
|
namespace Artemis.UI.Screens.Plugins;
|
||||||
@ -90,6 +96,42 @@ public class PluginFeatureViewModel : ActivatableViewModelBase
|
|||||||
public bool CanRemovePrerequisites => FeatureInfo.PlatformPrerequisites.Any(p => p.UninstallActions.Any());
|
public bool CanRemovePrerequisites => FeatureInfo.PlatformPrerequisites.Any(p => p.UninstallActions.Any());
|
||||||
public bool IsPopupEnabled => CanInstallPrerequisites || CanRemovePrerequisites;
|
public bool IsPopupEnabled => CanInstallPrerequisites || CanRemovePrerequisites;
|
||||||
|
|
||||||
|
public MaterialIconKind FeatureIcon
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (FeatureInfo.FeatureType.IsAssignableTo(typeof(DeviceProvider)))
|
||||||
|
return MaterialIconKind.Devices;
|
||||||
|
if (FeatureInfo.FeatureType.IsAssignableTo(typeof(Module)))
|
||||||
|
return MaterialIconKind.VectorRectangle;
|
||||||
|
if (FeatureInfo.FeatureType.IsAssignableTo(typeof(LayerBrushProvider)))
|
||||||
|
return MaterialIconKind.Brush;
|
||||||
|
if (FeatureInfo.FeatureType.IsAssignableTo(typeof(LayerEffectProvider)))
|
||||||
|
return MaterialIconKind.AutoAwesome;
|
||||||
|
if (FeatureInfo.FeatureType.IsAssignableTo(typeof(ScriptingProvider)))
|
||||||
|
return MaterialIconKind.Code;
|
||||||
|
return MaterialIconKind.Extension;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string FeatureType
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (FeatureInfo.FeatureType.IsAssignableTo(typeof(DeviceProvider)))
|
||||||
|
return "Device Provider";
|
||||||
|
if (FeatureInfo.FeatureType.IsAssignableTo(typeof(Module)))
|
||||||
|
return "Module";
|
||||||
|
if (FeatureInfo.FeatureType.IsAssignableTo(typeof(LayerBrushProvider)))
|
||||||
|
return "Layer Brush";
|
||||||
|
if (FeatureInfo.FeatureType.IsAssignableTo(typeof(LayerEffectProvider)))
|
||||||
|
return "Layer Effect";
|
||||||
|
if (FeatureInfo.FeatureType.IsAssignableTo(typeof(ScriptingProvider)))
|
||||||
|
return "Scripting Provider";
|
||||||
|
return "Miscellaneous feature";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ExecuteShowLogsFolder()
|
private void ExecuteShowLogsFolder()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@ -16,7 +16,6 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes;
|
|||||||
public class EventConditionViewModel : ActivatableViewModelBase
|
public class EventConditionViewModel : ActivatableViewModelBase
|
||||||
{
|
{
|
||||||
private readonly EventCondition _eventCondition;
|
private readonly EventCondition _eventCondition;
|
||||||
private readonly INodeService _nodeService;
|
|
||||||
private readonly IProfileEditorService _profileEditorService;
|
private readonly IProfileEditorService _profileEditorService;
|
||||||
private readonly ISettingsService _settingsService;
|
private readonly ISettingsService _settingsService;
|
||||||
private readonly ObservableAsPropertyHelper<bool> _showOverlapOptions;
|
private readonly ObservableAsPropertyHelper<bool> _showOverlapOptions;
|
||||||
@ -27,11 +26,10 @@ public class EventConditionViewModel : ActivatableViewModelBase
|
|||||||
private ObservableAsPropertyHelper<int>? _selectedToggleOffMode;
|
private ObservableAsPropertyHelper<int>? _selectedToggleOffMode;
|
||||||
private ObservableAsPropertyHelper<int>? _selectedTriggerMode;
|
private ObservableAsPropertyHelper<int>? _selectedTriggerMode;
|
||||||
|
|
||||||
public EventConditionViewModel(EventCondition eventCondition, IProfileEditorService profileEditorService, INodeService nodeService, IWindowService windowService, ISettingsService settingsService)
|
public EventConditionViewModel(EventCondition eventCondition, IProfileEditorService profileEditorService, IWindowService windowService, ISettingsService settingsService)
|
||||||
{
|
{
|
||||||
_eventCondition = eventCondition;
|
_eventCondition = eventCondition;
|
||||||
_profileEditorService = profileEditorService;
|
_profileEditorService = profileEditorService;
|
||||||
_nodeService = nodeService;
|
|
||||||
_windowService = windowService;
|
_windowService = windowService;
|
||||||
_settingsService = settingsService;
|
_settingsService = settingsService;
|
||||||
_showOverlapOptions = this.WhenAnyValue(vm => vm.SelectedTriggerMode).Select(m => m == 0).ToProperty(this, vm => vm.ShowOverlapOptions);
|
_showOverlapOptions = this.WhenAnyValue(vm => vm.SelectedTriggerMode).Select(m => m == 0).ToProperty(this, vm => vm.ShowOverlapOptions);
|
||||||
@ -45,7 +43,7 @@ public class EventConditionViewModel : ActivatableViewModelBase
|
|||||||
_selectedToggleOffMode = eventCondition.WhenAnyValue(c => c.OverlapMode).Select(m => (int) m).ToProperty(this, vm => vm.SelectedOverlapMode).DisposeWith(d);
|
_selectedToggleOffMode = eventCondition.WhenAnyValue(c => c.OverlapMode).Select(m => (int) m).ToProperty(this, vm => vm.SelectedOverlapMode).DisposeWith(d);
|
||||||
});
|
});
|
||||||
|
|
||||||
OpenEditor = ReactiveCommand.CreateFromTask(ExecuteOpenEditor);
|
OpenEditor = ReactiveCommand.CreateFromTask(ExecuteOpenEditor, this.WhenAnyValue(vm => vm.EventPath).Select(p => p != null));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ReactiveCommand<Unit, Unit> OpenEditor { get; }
|
public ReactiveCommand<Unit, Unit> OpenEditor { get; }
|
||||||
|
|||||||
@ -29,7 +29,22 @@ public class PlaybackViewModel : ActivatableViewModelBase
|
|||||||
{
|
{
|
||||||
_profileEditorService = profileEditorService;
|
_profileEditorService = profileEditorService;
|
||||||
_settingsService = settingsService;
|
_settingsService = settingsService;
|
||||||
_repeatTimeline = true;
|
|
||||||
|
if (_settingsService.GetSetting("ProfileEditor.RepeatTimeline", true).Value)
|
||||||
|
{
|
||||||
|
_repeating = true;
|
||||||
|
_repeatTimeline = true;
|
||||||
|
}
|
||||||
|
else if (_settingsService.GetSetting("ProfileEditor.RepeatSegment", false).Value)
|
||||||
|
{
|
||||||
|
_repeating = true;
|
||||||
|
_repeatSegment = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_repeating = false;
|
||||||
|
_repeatTimeline = true;
|
||||||
|
}
|
||||||
|
|
||||||
this.WhenActivated(d =>
|
this.WhenActivated(d =>
|
||||||
{
|
{
|
||||||
@ -42,7 +57,12 @@ public class PlaybackViewModel : ActivatableViewModelBase
|
|||||||
_lastUpdate = DateTime.MinValue;
|
_lastUpdate = DateTime.MinValue;
|
||||||
DispatcherTimer updateTimer = new(TimeSpan.FromMilliseconds(60.0 / 1000), DispatcherPriority.Render, Update);
|
DispatcherTimer updateTimer = new(TimeSpan.FromMilliseconds(60.0 / 1000), DispatcherPriority.Render, Update);
|
||||||
updateTimer.Start();
|
updateTimer.Start();
|
||||||
Disposable.Create(() => updateTimer.Stop()).DisposeWith(d);
|
Disposable.Create(() =>
|
||||||
|
{
|
||||||
|
updateTimer.Stop();
|
||||||
|
_settingsService.GetSetting("ProfileEditor.RepeatTimeline", true).Value = _repeating && _repeatTimeline;
|
||||||
|
_settingsService.GetSetting("ProfileEditor.RepeatSegment", false).Value = _repeating && _repeatSegment;
|
||||||
|
}).DisposeWith(d);
|
||||||
});
|
});
|
||||||
|
|
||||||
PlayFromStart = ReactiveCommand.Create(ExecutePlayFromStart, this.WhenAnyValue(vm => vm.KeyBindingsEnabled));
|
PlayFromStart = ReactiveCommand.Create(ExecutePlayFromStart, this.WhenAnyValue(vm => vm.KeyBindingsEnabled));
|
||||||
|
|||||||
@ -76,18 +76,23 @@ public class PropertiesViewModel : ActivatableViewModelBase
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Subscribe to events of the latest selected profile element - borrowed from https://stackoverflow.com/a/63950940
|
// Subscribe to events of the latest selected profile element - borrowed from https://stackoverflow.com/a/63950940
|
||||||
this.WhenAnyValue(vm => vm.ProfileElement)
|
this.WhenActivated(d =>
|
||||||
.Select(p => p is Layer l
|
{
|
||||||
? Observable.FromEventPattern(x => l.LayerBrushUpdated += x, x => l.LayerBrushUpdated -= x)
|
this.WhenAnyValue(vm => vm.ProfileElement)
|
||||||
: Observable.Never<EventPattern<object>>())
|
.Select(p => p is Layer l
|
||||||
.Switch()
|
? Observable.FromEventPattern(x => l.LayerBrushUpdated += x, x => l.LayerBrushUpdated -= x)
|
||||||
.Subscribe(_ => UpdatePropertyGroups());
|
: Observable.Never<EventPattern<object>>())
|
||||||
this.WhenAnyValue(vm => vm.ProfileElement)
|
.Switch()
|
||||||
.Select(p => p != null
|
.Subscribe(_ => UpdatePropertyGroups())
|
||||||
? Observable.FromEventPattern(x => p.LayerEffectsUpdated += x, x => p.LayerEffectsUpdated -= x)
|
.DisposeWith(d);
|
||||||
: Observable.Never<EventPattern<object>>())
|
this.WhenAnyValue(vm => vm.ProfileElement)
|
||||||
.Switch()
|
.Select(p => p != null
|
||||||
.Subscribe(_ => UpdatePropertyGroups());
|
? Observable.FromEventPattern(x => p.LayerEffectsUpdated += x, x => p.LayerEffectsUpdated -= x)
|
||||||
|
: Observable.Never<EventPattern<object>>())
|
||||||
|
.Switch()
|
||||||
|
.Subscribe(_ => UpdatePropertyGroups())
|
||||||
|
.DisposeWith(d);
|
||||||
|
});
|
||||||
this.WhenAnyValue(vm => vm.ProfileElement).Subscribe(_ => UpdatePropertyGroups());
|
this.WhenAnyValue(vm => vm.ProfileElement).Subscribe(_ => UpdatePropertyGroups());
|
||||||
this.WhenAnyValue(vm => vm.LayerProperty).Subscribe(_ => UpdateTimelineViewModel());
|
this.WhenAnyValue(vm => vm.LayerProperty).Subscribe(_ => UpdateTimelineViewModel());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
<ComboBox.ItemTemplate>
|
<ComboBox.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
<shared:ArtemisIcon Icon="{CompiledBinding Info.ResolvedIcon}" Width="16" Height="16" Margin="0 0 5 0" />
|
<shared:ArtemisIcon Icon="{CompiledBinding Plugin.Info.ResolvedIcon}" Width="16" Height="16" Margin="0 0 5 0" />
|
||||||
<TextBlock Text="{CompiledBinding LanguageName}" />
|
<TextBlock Text="{CompiledBinding LanguageName}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
<shared:ArtemisIcon Grid.Row="0"
|
<shared:ArtemisIcon Grid.Row="0"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Grid.RowSpan="2"
|
Grid.RowSpan="2"
|
||||||
Icon="{CompiledBinding ScriptConfiguration.Script.ScriptingProvider.Info.ResolvedIcon, FallbackValue=QuestionMark}"
|
Icon="{CompiledBinding ScriptConfiguration.Script.ScriptingProvider.Plugin.Info.ResolvedIcon, FallbackValue=QuestionMark}"
|
||||||
Width="32 "
|
Width="32 "
|
||||||
Height="32"
|
Height="32"
|
||||||
Margin="0 0 10 0"
|
Margin="0 0 10 0"
|
||||||
|
|||||||
@ -10,7 +10,7 @@ public class ProfileModuleViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
Module = module;
|
Module = module;
|
||||||
Name = module.Info.Name;
|
Name = module.Info.Name;
|
||||||
Icon = module.Info.ResolvedIcon ?? MaterialIconKind.QuestionMark.ToString();
|
Icon = module.Plugin.Info.ResolvedIcon ?? MaterialIconKind.QuestionMark.ToString();
|
||||||
Description = module.Info.Description;
|
Description = module.Info.Description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,23 +15,6 @@
|
|||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<converters:ColorOpacityConverter x:Key="ColorOpacityConverter" />
|
<converters:ColorOpacityConverter x:Key="ColorOpacityConverter" />
|
||||||
<MenuFlyout x:Key="CategoryMenuFlyout" Placement="Bottom">
|
<MenuFlyout x:Key="CategoryMenuFlyout" Placement="Bottom">
|
||||||
<MenuItem Header="Rename" Command="{CompiledBinding RenameCategory}">
|
|
||||||
<MenuItem.Icon>
|
|
||||||
<avalonia:MaterialIcon Kind="RenameBox" />
|
|
||||||
</MenuItem.Icon>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem Header="-" />
|
|
||||||
<MenuItem Header="Create profile" Command="{CompiledBinding AddProfile}">
|
|
||||||
<MenuItem.Icon>
|
|
||||||
<avalonia:MaterialIcon Kind="Plus" />
|
|
||||||
</MenuItem.Icon>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem Header="Import profile" Command="{CompiledBinding ImportProfile}">
|
|
||||||
<MenuItem.Icon>
|
|
||||||
<avalonia:MaterialIcon Kind="Import" />
|
|
||||||
</MenuItem.Icon>
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem Header="-" />
|
|
||||||
<MenuItem Header="Suspend" Command="{CompiledBinding ToggleSuspended}">
|
<MenuItem Header="Suspend" Command="{CompiledBinding ToggleSuspended}">
|
||||||
<MenuItem.Icon>
|
<MenuItem.Icon>
|
||||||
<avalonia:MaterialIcon Kind="Check" IsVisible="{CompiledBinding IsSuspended}" />
|
<avalonia:MaterialIcon Kind="Check" IsVisible="{CompiledBinding IsSuspended}" />
|
||||||
@ -48,12 +31,29 @@
|
|||||||
</MenuItem.Icon>
|
</MenuItem.Icon>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem Header="-" />
|
<MenuItem Header="-" />
|
||||||
|
<MenuItem Header="Rename" Command="{CompiledBinding RenameCategory}">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<avalonia:MaterialIcon Kind="RenameBox" />
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
<MenuItem Header="Delete" Command="{CompiledBinding DeleteCategory}">
|
<MenuItem Header="Delete" Command="{CompiledBinding DeleteCategory}">
|
||||||
<MenuItem.Icon>
|
<MenuItem.Icon>
|
||||||
<avalonia:MaterialIcon Kind="TrashCan" />
|
<avalonia:MaterialIcon Kind="TrashCan" />
|
||||||
</MenuItem.Icon>
|
</MenuItem.Icon>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuFlyout>
|
</MenuFlyout>
|
||||||
|
<MenuFlyout x:Key="PlusMenuFlyout" Placement="Bottom">
|
||||||
|
<MenuItem Header="Create profile" Command="{CompiledBinding AddProfile}">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<avalonia:MaterialIcon Kind="Plus" />
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Header="Import profile" Command="{CompiledBinding ImportProfile}">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<avalonia:MaterialIcon Kind="Import" />
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuFlyout>
|
||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
<UserControl.Styles>
|
<UserControl.Styles>
|
||||||
<Style Selector=":is(Button).properties-button">
|
<Style Selector=":is(Button).properties-button">
|
||||||
@ -65,6 +65,9 @@
|
|||||||
<Style Selector="Grid#ContainerGrid.flyout-open :is(Button).properties-button">
|
<Style Selector="Grid#ContainerGrid.flyout-open :is(Button).properties-button">
|
||||||
<Setter Property="IsVisible" Value="True" />
|
<Setter Property="IsVisible" Value="True" />
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style Selector="Grid#ContainerGrid.plus-flyout-open :is(Button).properties-button">
|
||||||
|
<Setter Property="IsVisible" Value="True" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<Style Selector="avalonia|MaterialIcon.chevron-collapsed">
|
<Style Selector="avalonia|MaterialIcon.chevron-collapsed">
|
||||||
<Setter Property="RenderTransform" Value="rotate(180deg)" />
|
<Setter Property="RenderTransform" Value="rotate(180deg)" />
|
||||||
@ -137,7 +140,8 @@
|
|||||||
Margin="0 8 0 0"
|
Margin="0 8 0 0"
|
||||||
RowDefinitions="Auto,*"
|
RowDefinitions="Auto,*"
|
||||||
ContextFlyout="{StaticResource CategoryMenuFlyout}"
|
ContextFlyout="{StaticResource CategoryMenuFlyout}"
|
||||||
Classes.flyout-open="{Binding IsOpen, Source={StaticResource CategoryMenuFlyout}}">
|
Classes.flyout-open="{Binding IsOpen, Source={StaticResource CategoryMenuFlyout}}"
|
||||||
|
Classes.plus-flyout-open="{Binding IsOpen, Source={StaticResource PlusMenuFlyout}}">
|
||||||
<Grid Grid.Row="0" Background="Transparent" Margin="0 0 6 0" ColumnDefinitions="Auto,*,Auto,Auto,Auto">
|
<Grid Grid.Row="0" Background="Transparent" Margin="0 0 6 0" ColumnDefinitions="Auto,*,Auto,Auto,Auto">
|
||||||
<avalonia:MaterialIcon Classes.chevron-collapsed="{CompiledBinding !IsCollapsed}"
|
<avalonia:MaterialIcon Classes.chevron-collapsed="{CompiledBinding !IsCollapsed}"
|
||||||
Kind="ChevronUp"
|
Kind="ChevronUp"
|
||||||
@ -188,13 +192,21 @@
|
|||||||
<Button Classes="properties-button icon-button icon-button-small"
|
<Button Classes="properties-button icon-button icon-button-small"
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
|
Flyout="{StaticResource PlusMenuFlyout}"
|
||||||
|
Margin="0 0 2 0">
|
||||||
|
<avalonia:MaterialIcon Kind="Plus" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button Classes="properties-button icon-button icon-button-small"
|
||||||
|
Grid.Column="3"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
Flyout="{StaticResource CategoryMenuFlyout}"
|
Flyout="{StaticResource CategoryMenuFlyout}"
|
||||||
Margin="0 0 2 0">
|
Margin="0 0 2 0">
|
||||||
<avalonia:MaterialIcon Kind="Cog" />
|
<avalonia:MaterialIcon Kind="Cog" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button Classes="icon-button icon-button-small"
|
<Button Classes="icon-button icon-button-small"
|
||||||
Command="{CompiledBinding ToggleSuspended}"
|
Command="{CompiledBinding ToggleSuspended}"
|
||||||
Grid.Column="3"
|
Grid.Column="4"
|
||||||
ToolTip.Tip="Suspend/resume category"
|
ToolTip.Tip="Suspend/resume category"
|
||||||
Margin="0 0 2 0">
|
Margin="0 0 2 0">
|
||||||
<Panel>
|
<Panel>
|
||||||
|
|||||||
@ -71,14 +71,33 @@ public class SidebarCategoryViewModel : ActivatableViewModelBase
|
|||||||
.Subscribe(e => profileConfigurations.RemoveMany(profileConfigurations.Items.Where(c => c == e.EventArgs.ProfileConfiguration)))
|
.Subscribe(e => profileConfigurations.RemoveMany(profileConfigurations.Items.Where(c => c == e.EventArgs.ProfileConfiguration)))
|
||||||
.DisposeWith(d);
|
.DisposeWith(d);
|
||||||
|
|
||||||
profileEditorService.ProfileConfiguration.Subscribe(p => SelectedProfileConfiguration = ProfileConfigurations.FirstOrDefault(c => ReferenceEquals(c.ProfileConfiguration, p)))
|
profileEditorService.ProfileConfiguration
|
||||||
|
.Subscribe(p => SelectedProfileConfiguration = ProfileConfigurations.FirstOrDefault(c => ReferenceEquals(c.ProfileConfiguration, p)))
|
||||||
.DisposeWith(d);
|
.DisposeWith(d);
|
||||||
|
|
||||||
_isCollapsed = ProfileCategory.WhenAnyValue(vm => vm.IsCollapsed).ToProperty(this, vm => vm.IsCollapsed).DisposeWith(d);
|
_isCollapsed = ProfileCategory.WhenAnyValue(vm => vm.IsCollapsed).ToProperty(this, vm => vm.IsCollapsed).DisposeWith(d);
|
||||||
_isSuspended = ProfileCategory.WhenAnyValue(vm => vm.IsSuspended).ToProperty(this, vm => vm.IsSuspended).DisposeWith(d);
|
_isSuspended = ProfileCategory.WhenAnyValue(vm => vm.IsSuspended).ToProperty(this, vm => vm.IsSuspended).DisposeWith(d);
|
||||||
|
|
||||||
// Change the current profile configuration when a new one is selected
|
// Change the current profile configuration when a new one is selected
|
||||||
this.WhenAnyValue(vm => vm.SelectedProfileConfiguration).WhereNotNull().Subscribe(s => profileEditorService.ChangeCurrentProfileConfiguration(s.ProfileConfiguration));
|
this.WhenAnyValue(vm => vm.SelectedProfileConfiguration)
|
||||||
|
.WhereNotNull()
|
||||||
|
.Subscribe(s =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
profileEditorService.ChangeCurrentProfileConfiguration(s.ProfileConfiguration);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
if (s.ProfileConfiguration.BrokenState != null && s.ProfileConfiguration.BrokenStateException != null)
|
||||||
|
_windowService.ShowExceptionDialog(s.ProfileConfiguration.BrokenState, s.ProfileConfiguration.BrokenStateException);
|
||||||
|
else
|
||||||
|
_windowService.ShowExceptionDialog(e.Message, e);
|
||||||
|
|
||||||
|
profileEditorService.ChangeCurrentProfileConfiguration(null);
|
||||||
|
SelectedProfileConfiguration = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
profileConfigurations.Edit(updater =>
|
profileConfigurations.Edit(updater =>
|
||||||
@ -88,10 +107,7 @@ public class SidebarCategoryViewModel : ActivatableViewModelBase
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public ReactiveCommand<Unit, Unit> ImportProfile { get; }
|
public ReactiveCommand<Unit, Unit> ImportProfile { get; }
|
||||||
|
|
||||||
public ReactiveCommand<Unit, Unit> ToggleCollapsed { get; }
|
public ReactiveCommand<Unit, Unit> ToggleCollapsed { get; }
|
||||||
public ReactiveCommand<Unit, Unit> ToggleSuspended { get; }
|
public ReactiveCommand<Unit, Unit> ToggleSuspended { get; }
|
||||||
public ReactiveCommand<Unit, Unit> AddProfile { get; }
|
public ReactiveCommand<Unit, Unit> AddProfile { get; }
|
||||||
|
|||||||
@ -55,10 +55,10 @@
|
|||||||
<DataTemplate DataType="skiaSharp:SKColor">
|
<DataTemplate DataType="skiaSharp:SKColor">
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
<TextBlock x:Name="HexDisplay"
|
<TextBlock x:Name="HexDisplay"
|
||||||
Text="{CompiledBinding Converter={StaticResource SKColorToStringConverter}}"
|
Text="{CompiledBinding Converter={StaticResource SKColorToStringConverter}, Mode=OneWay}"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
FontFamily="Consolas"/>
|
FontFamily="Consolas" />
|
||||||
<Border Margin="5 0 0 0"
|
<Border Margin="5 0 0 0"
|
||||||
VerticalAlignment="Bottom"
|
VerticalAlignment="Bottom"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
@ -69,22 +69,25 @@
|
|||||||
BorderBrush="{DynamicResource ColorPickerButtonOutline}"
|
BorderBrush="{DynamicResource ColorPickerButtonOutline}"
|
||||||
CornerRadius="4"
|
CornerRadius="4"
|
||||||
ClipToBounds="True">
|
ClipToBounds="True">
|
||||||
<Border CornerRadius="4">
|
<Border CornerRadius="4">
|
||||||
<Border.Background>
|
<Border.Background>
|
||||||
<SolidColorBrush Color="{Binding Converter={StaticResource SKColorToColorConverter}}" />
|
<SolidColorBrush Color="{Binding Converter={StaticResource SKColorToColorConverter}, Mode=OneWay}" />
|
||||||
</Border.Background>
|
</Border.Background>
|
||||||
</Border>
|
</Border>
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
<DataTemplate DataType="core:ColorGradient">
|
||||||
|
<TextBlock Text="Color gradient" FontFamily="Consolas" />
|
||||||
|
</DataTemplate>
|
||||||
<DataTemplate DataType="core:Numeric">
|
<DataTemplate DataType="core:Numeric">
|
||||||
<TextBlock Text="{Binding}" FontFamily="Consolas"/>
|
<TextBlock Text="{Binding Mode=OneWay}" FontFamily="Consolas" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
<DataTemplate DataType="collections:IList">
|
<DataTemplate DataType="collections:IList">
|
||||||
<TextBlock Text="{Binding Count, StringFormat='List - {0} item(s)'}" FontFamily="Consolas"/>
|
<TextBlock Text="{Binding Count, StringFormat='List - {0} item(s)', Mode=OneWay}" FontFamily="Consolas" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<TextBlock Text="{Binding}" FontFamily="Consolas"/>
|
<TextBlock Text="{Binding Mode=OneWay}" FontFamily="Consolas" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ContentControl.DataTemplates>
|
</ContentControl.DataTemplates>
|
||||||
</ContentControl>
|
</ContentControl>
|
||||||
|
|||||||
@ -8,7 +8,6 @@ using System.Threading.Tasks;
|
|||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
using Artemis.Core.Services;
|
using Artemis.Core.Services;
|
||||||
using Artemis.UI.Ninject.Factories;
|
using Artemis.UI.Ninject.Factories;
|
||||||
using Artemis.UI.Shared;
|
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
using Artemis.UI.Shared.Services.NodeEditor;
|
using Artemis.UI.Shared.Services.NodeEditor;
|
||||||
using Artemis.UI.Shared.Services.NodeEditor.Commands;
|
using Artemis.UI.Shared.Services.NodeEditor.Commands;
|
||||||
@ -20,7 +19,7 @@ using ReactiveUI;
|
|||||||
|
|
||||||
namespace Artemis.UI.Screens.VisualScripting;
|
namespace Artemis.UI.Screens.VisualScripting;
|
||||||
|
|
||||||
public class NodeScriptWindowViewModel : DialogViewModelBase<bool>
|
public class NodeScriptWindowViewModel : NodeScriptWindowViewModelBase
|
||||||
{
|
{
|
||||||
private readonly INodeEditorService _nodeEditorService;
|
private readonly INodeEditorService _nodeEditorService;
|
||||||
private readonly INodeService _nodeService;
|
private readonly INodeService _nodeService;
|
||||||
@ -34,7 +33,7 @@ public class NodeScriptWindowViewModel : DialogViewModelBase<bool>
|
|||||||
INodeVmFactory vmFactory,
|
INodeVmFactory vmFactory,
|
||||||
ISettingsService settingsService,
|
ISettingsService settingsService,
|
||||||
IProfileService profileService,
|
IProfileService profileService,
|
||||||
IWindowService windowService)
|
IWindowService windowService) : base(nodeScript)
|
||||||
{
|
{
|
||||||
NodeScript = nodeScript;
|
NodeScript = nodeScript;
|
||||||
NodeScriptViewModel = vmFactory.NodeScriptViewModel(NodeScript, false);
|
NodeScriptViewModel = vmFactory.NodeScriptViewModel(NodeScript, false);
|
||||||
@ -77,7 +76,6 @@ public class NodeScriptWindowViewModel : DialogViewModelBase<bool>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public NodeScript NodeScript { get; }
|
|
||||||
public NodeScriptViewModel NodeScriptViewModel { get; set; }
|
public NodeScriptViewModel NodeScriptViewModel { get; set; }
|
||||||
|
|
||||||
public NodeEditorHistory History { get; }
|
public NodeEditorHistory History { get; }
|
||||||
|
|||||||
@ -51,7 +51,7 @@
|
|||||||
<avalonia:MaterialIcon Kind="AlertCircle"></avalonia:MaterialIcon>
|
<avalonia:MaterialIcon Kind="AlertCircle"></avalonia:MaterialIcon>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="10 0 0 0" Text="{CompiledBinding Node.Name}" ToolTip.Tip="{CompiledBinding Node.Description}"/>
|
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="10 0 0 0" Text="{CompiledBinding Node.Name}" ToolTip.Tip="{CompiledBinding Node.Description}" />
|
||||||
|
|
||||||
<Button Grid.Column="2" VerticalAlignment="Center" Classes="icon-button icon-button-small" Margin="5" Command="{CompiledBinding DeleteNode}">
|
<Button Grid.Column="2" VerticalAlignment="Center" Classes="icon-button icon-button-small" Margin="5" Command="{CompiledBinding DeleteNode}">
|
||||||
<avalonia:MaterialIcon Kind="Close"></avalonia:MaterialIcon>
|
<avalonia:MaterialIcon Kind="Close"></avalonia:MaterialIcon>
|
||||||
@ -60,21 +60,36 @@
|
|||||||
</Border>
|
</Border>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Grid Grid.Row="1" ColumnDefinitions="Auto,*,Auto" Margin="4">
|
<Grid Grid.Row="1" RowDefinitions="Auto,Auto,Auto" ColumnDefinitions="Auto,*,Auto" Margin="4">
|
||||||
<StackPanel Grid.Column="0" IsVisible="{CompiledBinding HasInputPins}">
|
<ContentControl Grid.Row="0"
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.ColumnSpan="3"
|
||||||
|
Margin="5"
|
||||||
|
Content="{CompiledBinding CustomNodeViewModel}"
|
||||||
|
IsVisible="{CompiledBinding DisplayCustomViewModelAbove}" />
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="1" Grid.Column="0" IsVisible="{CompiledBinding HasInputPins}">
|
||||||
<ItemsControl Items="{CompiledBinding InputPinViewModels}" Margin="4 0" />
|
<ItemsControl Items="{CompiledBinding InputPinViewModels}" Margin="4 0" />
|
||||||
<ItemsControl Items="{CompiledBinding InputPinCollectionViewModels}" />
|
<ItemsControl Items="{CompiledBinding InputPinCollectionViewModels}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<ContentControl Grid.Column="1"
|
<ContentControl Grid.Row="1"
|
||||||
Name="CustomViewModelContainer"
|
Grid.Column="1"
|
||||||
Content="{CompiledBinding CustomNodeViewModel}"
|
Content="{CompiledBinding CustomNodeViewModel}"
|
||||||
IsVisible="{CompiledBinding CustomNodeViewModel, Converter={x:Static ObjectConverters.IsNotNull}}" />
|
VerticalAlignment="{CompiledBinding CustomViewModelVerticalAlignment}"
|
||||||
|
IsVisible="{CompiledBinding DisplayCustomViewModelBetween}" />
|
||||||
|
|
||||||
<StackPanel Grid.Column="2" IsVisible="{CompiledBinding HasOutputPins}">
|
<StackPanel Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding HasOutputPins}">
|
||||||
<ItemsControl Items="{CompiledBinding OutputPinViewModels}" Margin="4 0" />
|
<ItemsControl Items="{CompiledBinding OutputPinViewModels}" Margin="4 0" />
|
||||||
<ItemsControl Items="{CompiledBinding OutputPinCollectionViewModels}" />
|
<ItemsControl Items="{CompiledBinding OutputPinCollectionViewModels}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<ContentControl Grid.Row="2"
|
||||||
|
Grid.Column="0"
|
||||||
|
Grid.ColumnSpan="3"
|
||||||
|
Margin="5"
|
||||||
|
Content="{CompiledBinding CustomNodeViewModel}"
|
||||||
|
IsVisible="{CompiledBinding DisplayCustomViewModelBelow}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@ -13,6 +13,7 @@ using Artemis.UI.Shared.Services.NodeEditor;
|
|||||||
using Artemis.UI.Shared.Services.NodeEditor.Commands;
|
using Artemis.UI.Shared.Services.NodeEditor.Commands;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls.Mixins;
|
using Avalonia.Controls.Mixins;
|
||||||
|
using Avalonia.Layout;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using DynamicData.Binding;
|
using DynamicData.Binding;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
@ -34,6 +35,10 @@ public class NodeViewModel : ActivatableViewModelBase
|
|||||||
private ObservableAsPropertyHelper<bool>? _isStaticNode;
|
private ObservableAsPropertyHelper<bool>? _isStaticNode;
|
||||||
private double _startX;
|
private double _startX;
|
||||||
private double _startY;
|
private double _startY;
|
||||||
|
private bool _displayCustomViewModelAbove;
|
||||||
|
private bool _displayCustomViewModelBetween;
|
||||||
|
private bool _displayCustomViewModelBelow;
|
||||||
|
private VerticalAlignment _customViewModelVerticalAlignment;
|
||||||
|
|
||||||
public NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService, IWindowService windowService)
|
public NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService, IWindowService windowService)
|
||||||
{
|
{
|
||||||
@ -132,15 +137,32 @@ public class NodeViewModel : ActivatableViewModelBase
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Node is Node coreNode)
|
// Set up the custom node VM if needed
|
||||||
CustomNodeViewModel = coreNode.GetCustomViewModel(nodeScriptViewModel.NodeScript);
|
if (Node is ICustomViewModelNode customViewModelNode)
|
||||||
|
{
|
||||||
|
CustomNodeViewModel = customViewModelNode.GetCustomViewModel(nodeScriptViewModel.NodeScript);
|
||||||
|
if (customViewModelNode.ViewModelPosition == CustomNodeViewModelPosition.AbovePins)
|
||||||
|
DisplayCustomViewModelAbove = true;
|
||||||
|
else if (customViewModelNode.ViewModelPosition == CustomNodeViewModelPosition.BelowPins)
|
||||||
|
DisplayCustomViewModelBelow = true;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DisplayCustomViewModelBetween = true;
|
||||||
|
|
||||||
|
if (customViewModelNode.ViewModelPosition == CustomNodeViewModelPosition.BetweenPinsTop)
|
||||||
|
CustomViewModelVerticalAlignment = VerticalAlignment.Top;
|
||||||
|
else if (customViewModelNode.ViewModelPosition == CustomNodeViewModelPosition.BetweenPinsTop)
|
||||||
|
CustomViewModelVerticalAlignment = VerticalAlignment.Center;
|
||||||
|
else
|
||||||
|
CustomViewModelVerticalAlignment = VerticalAlignment.Bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsStaticNode => _isStaticNode?.Value ?? true;
|
public bool IsStaticNode => _isStaticNode?.Value ?? true;
|
||||||
public bool HasInputPins => _hasInputPins?.Value ?? false;
|
public bool HasInputPins => _hasInputPins?.Value ?? false;
|
||||||
public bool HasOutputPins => _hasOutputPins?.Value ?? false;
|
public bool HasOutputPins => _hasOutputPins?.Value ?? false;
|
||||||
|
|
||||||
public NodeScriptViewModel NodeScriptViewModel { get; }
|
public NodeScriptViewModel NodeScriptViewModel { get; }
|
||||||
public INode Node { get; }
|
public INode Node { get; }
|
||||||
public ReadOnlyObservableCollection<PinViewModel> InputPinViewModels { get; }
|
public ReadOnlyObservableCollection<PinViewModel> InputPinViewModels { get; }
|
||||||
@ -149,16 +171,40 @@ public class NodeViewModel : ActivatableViewModelBase
|
|||||||
public ReadOnlyObservableCollection<PinCollectionViewModel> OutputPinCollectionViewModels { get; }
|
public ReadOnlyObservableCollection<PinCollectionViewModel> OutputPinCollectionViewModels { get; }
|
||||||
public ReadOnlyObservableCollection<PinViewModel> PinViewModels { get; }
|
public ReadOnlyObservableCollection<PinViewModel> PinViewModels { get; }
|
||||||
|
|
||||||
|
public bool IsSelected
|
||||||
|
{
|
||||||
|
get => _isSelected;
|
||||||
|
set => RaiseAndSetIfChanged(ref _isSelected, value);
|
||||||
|
}
|
||||||
|
|
||||||
public ICustomNodeViewModel? CustomNodeViewModel
|
public ICustomNodeViewModel? CustomNodeViewModel
|
||||||
{
|
{
|
||||||
get => _customNodeViewModel;
|
get => _customNodeViewModel;
|
||||||
set => RaiseAndSetIfChanged(ref _customNodeViewModel, value);
|
set => RaiseAndSetIfChanged(ref _customNodeViewModel, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsSelected
|
public bool DisplayCustomViewModelAbove
|
||||||
{
|
{
|
||||||
get => _isSelected;
|
get => _displayCustomViewModelAbove;
|
||||||
set => RaiseAndSetIfChanged(ref _isSelected, value);
|
set => RaiseAndSetIfChanged(ref _displayCustomViewModelAbove, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DisplayCustomViewModelBetween
|
||||||
|
{
|
||||||
|
get => _displayCustomViewModelBetween;
|
||||||
|
set => RaiseAndSetIfChanged(ref _displayCustomViewModelBetween, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DisplayCustomViewModelBelow
|
||||||
|
{
|
||||||
|
get => _displayCustomViewModelBelow;
|
||||||
|
set => RaiseAndSetIfChanged(ref _displayCustomViewModelBelow, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public VerticalAlignment CustomViewModelVerticalAlignment
|
||||||
|
{
|
||||||
|
get => _customViewModelVerticalAlignment;
|
||||||
|
set => RaiseAndSetIfChanged(ref _customViewModelVerticalAlignment, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ReactiveCommand<Unit, Unit> ShowBrokenState { get; }
|
public ReactiveCommand<Unit, Unit> ShowBrokenState { get; }
|
||||||
|
|||||||
@ -105,6 +105,7 @@ public class RegistrationService : IRegistrationService
|
|||||||
_nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(SKColor), new SKColor(0xFFAD3EED));
|
_nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(SKColor), new SKColor(0xFFAD3EED));
|
||||||
_nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(IList), new SKColor(0xFFED3E61));
|
_nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(IList), new SKColor(0xFFED3E61));
|
||||||
_nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(Enum), new SKColor(0xFF1E90FF));
|
_nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(Enum), new SKColor(0xFF1E90FF));
|
||||||
|
_nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(ColorGradient), new SKColor(0xFF00B2A9));
|
||||||
|
|
||||||
foreach (Type nodeType in typeof(SumNumericsNode).Assembly.GetTypes().Where(t => typeof(INode).IsAssignableFrom(t) && t.IsPublic && !t.IsAbstract && !t.IsInterface))
|
foreach (Type nodeType in typeof(SumNumericsNode).Assembly.GetTypes().Where(t => typeof(INode).IsAssignableFrom(t) && t.IsPublic && !t.IsAbstract && !t.IsInterface))
|
||||||
{
|
{
|
||||||
|
|||||||
@ -37,6 +37,9 @@
|
|||||||
<DependentUpon>DataModelEventNodeCustomView.axaml</DependentUpon>
|
<DependentUpon>DataModelEventNodeCustomView.axaml</DependentUpon>
|
||||||
<SubType>Code</SubType>
|
<SubType>Code</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Update="Nodes\Color\Screens\ColorGradientNodeCustomView.axaml.cs">
|
||||||
|
<DependentUpon>ColorGradientNodeCustomView.axaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -0,0 +1,102 @@
|
|||||||
|
using Artemis.Core;
|
||||||
|
using Artemis.Core.Events;
|
||||||
|
using SkiaSharp;
|
||||||
|
|
||||||
|
namespace Artemis.VisualScripting.Nodes.Color;
|
||||||
|
|
||||||
|
[Node("Color Gradient (Advanced)", "Outputs a Color Gradient from colors and positions", "Color", OutputType = typeof(ColorGradient))]
|
||||||
|
public class ColorGradientFromPinsNode : Node
|
||||||
|
{
|
||||||
|
public OutputPin<ColorGradient> Gradient { get; set; }
|
||||||
|
public InputPinCollection<SKColor> Colors { get; set; }
|
||||||
|
public InputPinCollection<Numeric> Positions { get; set; }
|
||||||
|
|
||||||
|
public ColorGradientFromPinsNode() : base("Color Gradient", "Outputs a Color Gradient from colors and positions")
|
||||||
|
{
|
||||||
|
Colors = CreateInputPinCollection<SKColor>("Colors", 0);
|
||||||
|
Positions = CreateInputPinCollection<Numeric>("Positions", 0);
|
||||||
|
Gradient = CreateOutputPin<ColorGradient>("Gradient");
|
||||||
|
|
||||||
|
Colors.PinAdded += OnPinAdded;
|
||||||
|
Colors.PinRemoved += OnPinRemoved;
|
||||||
|
Positions.PinAdded += OnPinAdded;
|
||||||
|
Positions.PinRemoved += OnPinRemoved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPinRemoved(object? sender, SingleValueEventArgs<IPin> e)
|
||||||
|
{
|
||||||
|
int colorsCount = Colors.Count();
|
||||||
|
int positionsCount = Positions.Count();
|
||||||
|
if (colorsCount == positionsCount)
|
||||||
|
return;
|
||||||
|
|
||||||
|
while (colorsCount > positionsCount)
|
||||||
|
{
|
||||||
|
IPin pinToRemove = Colors.Last();
|
||||||
|
Colors.Remove(pinToRemove);
|
||||||
|
|
||||||
|
--colorsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (positionsCount > colorsCount)
|
||||||
|
{
|
||||||
|
IPin pinToRemove = Positions.Last();
|
||||||
|
Positions.Remove(pinToRemove);
|
||||||
|
--positionsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenamePins();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPinAdded(object? sender, SingleValueEventArgs<IPin> e)
|
||||||
|
{
|
||||||
|
int colorsCount = Colors.Count();
|
||||||
|
int positionsCount = Positions.Count();
|
||||||
|
if (colorsCount == positionsCount)
|
||||||
|
return;
|
||||||
|
|
||||||
|
while (colorsCount < positionsCount)
|
||||||
|
{
|
||||||
|
Colors.Add(Colors.CreatePin());
|
||||||
|
|
||||||
|
++colorsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (positionsCount < colorsCount)
|
||||||
|
{
|
||||||
|
Positions.Add(Positions.CreatePin());
|
||||||
|
|
||||||
|
++positionsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenamePins();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RenamePins()
|
||||||
|
{
|
||||||
|
int colors = 0;
|
||||||
|
foreach (IPin item in Colors)
|
||||||
|
{
|
||||||
|
item.Name = $"Color #{++colors}";
|
||||||
|
}
|
||||||
|
|
||||||
|
int positions = 0;
|
||||||
|
foreach (IPin item in Positions)
|
||||||
|
{
|
||||||
|
item.Name = $"Position #{++positions}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Evaluate()
|
||||||
|
{
|
||||||
|
List<ColorGradientStop> stops = new List<ColorGradientStop>();
|
||||||
|
InputPin<SKColor>[] colors = Colors.Pins.ToArray();
|
||||||
|
InputPin<Numeric>[] positions = Positions.Pins.ToArray();
|
||||||
|
for (int i = 0; i < colors.Length; i++)
|
||||||
|
{
|
||||||
|
stops.Add(new ColorGradientStop(colors[i].Value, positions[i].Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
Gradient.Value = new ColorGradient(stops);
|
||||||
|
}
|
||||||
|
}
|
||||||
108
src/Artemis.VisualScripting/Nodes/Color/ColorGradientNode.cs
Normal file
108
src/Artemis.VisualScripting/Nodes/Color/ColorGradientNode.cs
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
using System.Collections.Specialized;
|
||||||
|
using Artemis.Core;
|
||||||
|
using Artemis.VisualScripting.Nodes.Color.Screens;
|
||||||
|
using SkiaSharp;
|
||||||
|
|
||||||
|
namespace Artemis.VisualScripting.Nodes.Color;
|
||||||
|
|
||||||
|
[Node("Color Gradient (Simple)", "Outputs a color gradient with the given colors", "Color", OutputType = typeof(ColorGradient))]
|
||||||
|
public class ColorGradientNode : Node<ColorGradient, ColorGradientNodeCustomViewModel>
|
||||||
|
{
|
||||||
|
private readonly List<InputPin> _inputPins;
|
||||||
|
|
||||||
|
public ColorGradientNode() : base("Color Gradient", "Outputs a color gradient with the given colors")
|
||||||
|
{
|
||||||
|
_inputPins = new List<InputPin>();
|
||||||
|
|
||||||
|
Gradient = ColorGradient.GetUnicornBarf();
|
||||||
|
Output = CreateOutputPin<ColorGradient>();
|
||||||
|
ViewModelPosition = CustomNodeViewModelPosition.AbovePins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ColorGradient Gradient { get; private set; }
|
||||||
|
public OutputPin<ColorGradient> Output { get; }
|
||||||
|
|
||||||
|
public override void Initialize(INodeScript script)
|
||||||
|
{
|
||||||
|
UpdateGradient();
|
||||||
|
ComputeInputPins();
|
||||||
|
|
||||||
|
// Not expecting storage to get modified, but lets just make sure
|
||||||
|
StorageModified += OnStorageModified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Evaluate()
|
||||||
|
{
|
||||||
|
ColorGradientStop[] stops = Gradient.ToArray();
|
||||||
|
|
||||||
|
if (_inputPins.Count != stops.Length)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (int i = 0; i < _inputPins.Count; i++)
|
||||||
|
{
|
||||||
|
// if nothing is connected, leave the stop alone.
|
||||||
|
if (_inputPins[i].ConnectedTo.Count == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// if the pin has a connection, update the stop.
|
||||||
|
if (_inputPins[i].PinValue is SKColor color)
|
||||||
|
stops[i].Color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
Output.Value = Gradient;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DisconnectAllInputPins()
|
||||||
|
{
|
||||||
|
foreach (InputPin item in _inputPins)
|
||||||
|
item.DisconnectAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateGradient()
|
||||||
|
{
|
||||||
|
Gradient.CollectionChanged -= OnGradientCollectionChanged;
|
||||||
|
if (Storage != null)
|
||||||
|
Gradient = Storage;
|
||||||
|
else
|
||||||
|
Storage = Gradient;
|
||||||
|
Gradient.CollectionChanged += OnGradientCollectionChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ComputeInputPins()
|
||||||
|
{
|
||||||
|
int newAmount = Gradient.Count;
|
||||||
|
if (newAmount == _inputPins.Count)
|
||||||
|
return;
|
||||||
|
|
||||||
|
while (newAmount > _inputPins.Count)
|
||||||
|
_inputPins.Add(CreateOrAddInputPin(typeof(SKColor), string.Empty));
|
||||||
|
|
||||||
|
while (newAmount < _inputPins.Count)
|
||||||
|
{
|
||||||
|
InputPin pin = _inputPins.Last();
|
||||||
|
RemovePin(pin);
|
||||||
|
_inputPins.Remove(pin);
|
||||||
|
}
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
foreach (InputPin item in _inputPins)
|
||||||
|
item.Name = $"Color #{++index}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnStorageModified(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
UpdateGradient();
|
||||||
|
ComputeInputPins();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGradientCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
// if the user reorders the gradient, let it slide and do nothing.
|
||||||
|
// of course, the user might want to change the input pins since they will no longer line up.
|
||||||
|
if (e.Action == NotifyCollectionChangedAction.Move)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// DisconnectAllInputPins();
|
||||||
|
ComputeInputPins();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,7 +23,7 @@ public class RampSKColorNode : Node<ColorGradient, RampSKColorNodeCustomViewMode
|
|||||||
|
|
||||||
public override void Evaluate()
|
public override void Evaluate()
|
||||||
{
|
{
|
||||||
Output.Value = Storage?.GetColor(Input.Value) ?? SKColor.Empty;
|
Output.Value = Storage?.GetColor(Input.Value % 1.0) ?? SKColor.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@ -0,0 +1,15 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:screens="clr-namespace:Artemis.VisualScripting.Nodes.Color.Screens"
|
||||||
|
xmlns:gradientPicker="clr-namespace:Artemis.UI.Shared.Controls.GradientPicker;assembly=Artemis.UI.Shared"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Artemis.VisualScripting.Nodes.Color.Screens.ColorGradientNodeCustomView"
|
||||||
|
x:DataType="screens:ColorGradientNodeCustomViewModel">
|
||||||
|
<gradientPicker:GradientPickerButton Classes="condensed"
|
||||||
|
ColorGradient="{CompiledBinding Gradient}"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
FlyoutOpened="GradientPickerButton_OnFlyoutOpened"
|
||||||
|
FlyoutClosed="GradientPickerButton_OnFlyoutClosed" />
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
using Artemis.UI.Shared.Controls.GradientPicker;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.VisualScripting.Nodes.Color.Screens;
|
||||||
|
|
||||||
|
public class ColorGradientNodeCustomView : ReactiveUserControl<ColorGradientNodeCustomViewModel>
|
||||||
|
{
|
||||||
|
public ColorGradientNodeCustomView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GradientPickerButton_OnFlyoutOpened(GradientPickerButton sender, EventArgs args)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GradientPickerButton_OnFlyoutClosed(GradientPickerButton sender, EventArgs args)
|
||||||
|
{
|
||||||
|
ViewModel?.StoreGradient();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
using Artemis.Core;
|
||||||
|
using Artemis.UI.Shared.Services.NodeEditor;
|
||||||
|
using Artemis.UI.Shared.Services.NodeEditor.Commands;
|
||||||
|
using Artemis.UI.Shared.VisualScripting;
|
||||||
|
|
||||||
|
namespace Artemis.VisualScripting.Nodes.Color.Screens;
|
||||||
|
|
||||||
|
public class ColorGradientNodeCustomViewModel : CustomNodeViewModel
|
||||||
|
{
|
||||||
|
private readonly ColorGradientNode _node;
|
||||||
|
private readonly INodeEditorService _nodeEditorService;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ColorGradientNodeCustomViewModel(ColorGradientNode node, INodeScript script, INodeEditorService nodeEditorService) : base(node, script)
|
||||||
|
{
|
||||||
|
_node = node;
|
||||||
|
_nodeEditorService = nodeEditorService;
|
||||||
|
|
||||||
|
Gradient = _node.Gradient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ColorGradient Gradient { get; }
|
||||||
|
|
||||||
|
public void StoreGradient()
|
||||||
|
{
|
||||||
|
_nodeEditorService.ExecuteCommand(Script, new UpdateStorage<ColorGradient>(_node, Gradient));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,28 +1,24 @@
|
|||||||
using System.Linq.Expressions;
|
using Artemis.Core;
|
||||||
using System.Reflection;
|
|
||||||
using Artemis.Core;
|
|
||||||
using Artemis.Core.Modules;
|
|
||||||
using Artemis.Storage.Entities.Profile;
|
using Artemis.Storage.Entities.Profile;
|
||||||
using Artemis.VisualScripting.Nodes.DataModel.Screens;
|
using Artemis.VisualScripting.Nodes.DataModel.Screens;
|
||||||
using Humanizer;
|
|
||||||
|
|
||||||
namespace Artemis.VisualScripting.Nodes.DataModel;
|
namespace Artemis.VisualScripting.Nodes.DataModel;
|
||||||
|
|
||||||
[Node("Data Model-Event", "Outputs the latest values of a data model event.", "Data Model", OutputType = typeof(object))]
|
[Node("Data Model-Event", "Outputs the latest values of a data model event.", "Data Model", OutputType = typeof(object))]
|
||||||
public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCustomViewModel>, IDisposable
|
public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCustomViewModel>, IDisposable
|
||||||
{
|
{
|
||||||
private readonly Dictionary<Func<DataModelEventArgs, object>, OutputPin> _propertyPins;
|
private readonly ObjectOutputPins _objectOutputPins;
|
||||||
private DataModelPath? _dataModelPath;
|
|
||||||
private IDataModelEvent? _dataModelEvent;
|
private IDataModelEvent? _dataModelEvent;
|
||||||
private OutputPin? _oldValuePin;
|
private DataModelPath? _dataModelPath;
|
||||||
private OutputPin? _newValuePin;
|
|
||||||
private DateTime _lastTrigger;
|
private DateTime _lastTrigger;
|
||||||
private object? _lastValue;
|
private object? _lastValue;
|
||||||
|
private OutputPin? _newValuePin;
|
||||||
|
private OutputPin? _oldValuePin;
|
||||||
private int _valueChangeCount;
|
private int _valueChangeCount;
|
||||||
|
|
||||||
public DataModelEventNode() : base("Data Model-Event", "Outputs the latest values of a data model event.")
|
public DataModelEventNode() : base("Data Model-Event", "Outputs the latest values of a data model event.")
|
||||||
{
|
{
|
||||||
_propertyPins = new Dictionary<Func<DataModelEventArgs, object>, OutputPin>();
|
_objectOutputPins = new ObjectOutputPins(this);
|
||||||
|
|
||||||
TimeSinceLastTrigger = CreateOutputPin<Numeric>("Time since trigger");
|
TimeSinceLastTrigger = CreateOutputPin<Numeric>("Time since trigger");
|
||||||
TriggerCount = CreateOutputPin<Numeric>("Trigger count");
|
TriggerCount = CreateOutputPin<Numeric>("Trigger count");
|
||||||
@ -55,17 +51,14 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
|
|||||||
TimeSinceLastTrigger.Value = dataModelEvent.TimeSinceLastTrigger.TotalMilliseconds;
|
TimeSinceLastTrigger.Value = dataModelEvent.TimeSinceLastTrigger.TotalMilliseconds;
|
||||||
TriggerCount.Value = dataModelEvent.TriggerCount;
|
TriggerCount.Value = dataModelEvent.TriggerCount;
|
||||||
|
|
||||||
foreach ((Func<DataModelEventArgs, object> propertyAccessor, OutputPin outputPin) in _propertyPins)
|
_objectOutputPins.SetCurrentValue(dataModelEvent.LastEventArgumentsUntyped);
|
||||||
{
|
|
||||||
if (!outputPin.ConnectedTo.Any())
|
|
||||||
continue;
|
|
||||||
object value = dataModelEvent.LastEventArgumentsUntyped != null ? propertyAccessor(dataModelEvent.LastEventArgumentsUntyped) : outputPin.Type.GetDefault()!;
|
|
||||||
outputPin.Value = outputPin.IsNumeric ? new Numeric(value) : value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// If the path is a regular value, evaluate the current value
|
// If the path is a regular value, evaluate the current value
|
||||||
else if (_oldValuePin != null && _newValuePin != null)
|
else if (_oldValuePin != null && _newValuePin != null)
|
||||||
{
|
{
|
||||||
|
if (_newValuePin.IsNumeric)
|
||||||
|
pathValue = new Numeric(pathValue);
|
||||||
|
|
||||||
if (Equals(_lastValue, pathValue))
|
if (Equals(_lastValue, pathValue))
|
||||||
{
|
{
|
||||||
TimeSinceLastTrigger.Value = (DateTime.Now - _lastTrigger).TotalMilliseconds;
|
TimeSinceLastTrigger.Value = (DateTime.Now - _lastTrigger).TotalMilliseconds;
|
||||||
@ -104,6 +97,7 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
|
|||||||
private void UpdateOutputPins()
|
private void UpdateOutputPins()
|
||||||
{
|
{
|
||||||
object? pathValue = _dataModelPath?.GetValue();
|
object? pathValue = _dataModelPath?.GetValue();
|
||||||
|
|
||||||
if (pathValue is IDataModelEvent dataModelEvent)
|
if (pathValue is IDataModelEvent dataModelEvent)
|
||||||
CreateEventPins(dataModelEvent);
|
CreateEventPins(dataModelEvent);
|
||||||
else
|
else
|
||||||
@ -117,22 +111,7 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
|
|||||||
|
|
||||||
ClearPins();
|
ClearPins();
|
||||||
_dataModelEvent = dataModelEvent;
|
_dataModelEvent = dataModelEvent;
|
||||||
foreach (PropertyInfo propertyInfo in dataModelEvent.ArgumentsType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
_objectOutputPins.ChangeType(dataModelEvent.ArgumentsType);
|
||||||
.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()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CreateValuePins()
|
private void CreateValuePins()
|
||||||
@ -143,6 +122,9 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
|
|||||||
if (propertyType == null)
|
if (propertyType == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (Numeric.IsTypeCompatible(propertyType))
|
||||||
|
propertyType = typeof(Numeric);
|
||||||
|
|
||||||
_oldValuePin = CreateOrAddOutputPin(propertyType, "Old value");
|
_oldValuePin = CreateOrAddOutputPin(propertyType, "Old value");
|
||||||
_newValuePin = CreateOrAddOutputPin(propertyType, "New value");
|
_newValuePin = CreateOrAddOutputPin(propertyType, "New value");
|
||||||
_lastValue = null;
|
_lastValue = null;
|
||||||
@ -151,13 +133,20 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
|
|||||||
|
|
||||||
private void ClearPins()
|
private void ClearPins()
|
||||||
{
|
{
|
||||||
List<IPin> pins = Pins.Skip(2).ToList();
|
// Clear the output pins by changing the type to null
|
||||||
foreach (IPin pin in pins)
|
_objectOutputPins.ChangeType(null);
|
||||||
RemovePin((Pin) pin);
|
|
||||||
|
|
||||||
_propertyPins.Clear();
|
if (_oldValuePin != null)
|
||||||
_oldValuePin = null;
|
{
|
||||||
_newValuePin = null;
|
RemovePin(_oldValuePin);
|
||||||
|
_oldValuePin = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_newValuePin != null)
|
||||||
|
{
|
||||||
|
RemovePin(_newValuePin);
|
||||||
|
_newValuePin = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DataModelPathOnPathValidated(object? sender, EventArgs e)
|
private void DataModelPathOnPathValidated(object? sender, EventArgs e)
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
using Artemis.Storage.Entities.Profile.Nodes;
|
||||||
|
|
||||||
|
namespace Artemis.VisualScripting.Nodes.List;
|
||||||
|
|
||||||
|
public class ListOperatorEntity
|
||||||
|
{
|
||||||
|
public NodeScriptEntity? Script { get; set; }
|
||||||
|
public ListOperator Operator { get; set; }
|
||||||
|
}
|
||||||
@ -4,37 +4,37 @@ using Artemis.VisualScripting.Nodes.List.Screens;
|
|||||||
|
|
||||||
namespace Artemis.VisualScripting.Nodes.List;
|
namespace Artemis.VisualScripting.Nodes.List;
|
||||||
|
|
||||||
[Node("List Operator (Simple)", "Checks if any/all/no value in the input list matches the input value", "List", InputType = typeof(IEnumerable), OutputType = typeof(bool))]
|
[Node("List Operator (Simple)", "Checks if any/all/no values in the input list match the input value", "List", InputType = typeof(IEnumerable), OutputType = typeof(bool))]
|
||||||
public class ListOperatorNode : Node<ListOperator, ListOperatorNodeCustomViewModel>
|
public class ListOperatorNode : Node<ListOperator, ListOperatorNodeCustomViewModel>
|
||||||
{
|
{
|
||||||
public ListOperatorNode() : base("List Operator", "Checks if any/all/no value in the input list matches the input value")
|
public ListOperatorNode() : base("List Operator (Simple)", "Checks if any/all/no values in the input list match the input value")
|
||||||
{
|
{
|
||||||
InputList = CreateInputPin<IList>();
|
InputList = CreateInputPin<IList>();
|
||||||
InputValue = CreateInputPin<object>();
|
InputValue = CreateInputPin<object>();
|
||||||
|
|
||||||
Ouput = CreateOutputPin<bool>();
|
Output = CreateOutputPin<bool>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public InputPin<IList> InputList { get; }
|
public InputPin<IList> InputList { get; }
|
||||||
public InputPin<object> InputValue { get; }
|
public InputPin<object> InputValue { get; }
|
||||||
public OutputPin<bool> Ouput { get; }
|
public OutputPin<bool> Output { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Evaluate()
|
public override void Evaluate()
|
||||||
{
|
{
|
||||||
if (InputList.Value == null)
|
if (InputList.Value == null)
|
||||||
{
|
{
|
||||||
Ouput.Value = Storage == ListOperator.None;
|
Output.Value = Storage == ListOperator.None;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
object? input = InputValue.Value;
|
object? input = InputValue.Value;
|
||||||
if (Storage == ListOperator.Any)
|
if (Storage == ListOperator.Any)
|
||||||
Ouput.Value = InputList.Value.Cast<object>().Any(v => v.Equals(input));
|
Output.Value = InputList.Value.Cast<object>().Any(v => v.Equals(input));
|
||||||
else if (Storage == ListOperator.All)
|
else if (Storage == ListOperator.All)
|
||||||
Ouput.Value = InputList.Value.Cast<object>().All(v => v.Equals(input));
|
Output.Value = InputList.Value.Cast<object>().All(v => v.Equals(input));
|
||||||
else if (Storage == ListOperator.All)
|
else if (Storage == ListOperator.None)
|
||||||
Ouput.Value = InputList.Value.Cast<object>().All(v => !v.Equals(input));
|
Output.Value = InputList.Value.Cast<object>().All(v => !v.Equals(input));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,107 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using Artemis.Core;
|
||||||
|
using Artemis.Core.Events;
|
||||||
|
using Artemis.VisualScripting.Nodes.List.Screens;
|
||||||
|
|
||||||
|
namespace Artemis.VisualScripting.Nodes.List;
|
||||||
|
|
||||||
|
[Node("List Operator (Advanced)", "Checks if any/all/no values in the input list match a condition", "List", InputType = typeof(IEnumerable), OutputType = typeof(bool))]
|
||||||
|
public class ListOperatorPredicateNode : Node<ListOperatorEntity, ListOperatorPredicateNodeCustomViewModel>, IDisposable
|
||||||
|
{
|
||||||
|
private readonly object _scriptLock = new();
|
||||||
|
private ListOperatorPredicateStartNode _startNode;
|
||||||
|
|
||||||
|
public ListOperatorPredicateNode() : base("List Operator (Advanced)", "Checks if any/all/no values in the input list match a condition")
|
||||||
|
{
|
||||||
|
_startNode = new ListOperatorPredicateStartNode {X = -200};
|
||||||
|
|
||||||
|
InputList = CreateInputPin<IList>();
|
||||||
|
Output = CreateOutputPin<bool>();
|
||||||
|
|
||||||
|
InputList.PinConnected += InputListOnPinConnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputPin<IList> InputList { get; }
|
||||||
|
public OutputPin<bool> Output { get; }
|
||||||
|
public NodeScript<bool>? Script { get; private set; }
|
||||||
|
|
||||||
|
public override void Initialize(INodeScript script)
|
||||||
|
{
|
||||||
|
Storage ??= new ListOperatorEntity();
|
||||||
|
|
||||||
|
lock (_scriptLock)
|
||||||
|
{
|
||||||
|
Script = Storage?.Script != null
|
||||||
|
? new NodeScript<bool>("Is match", "Determines whether the current list item is a match", Storage.Script, script.Context, new List<DefaultNode> {_startNode})
|
||||||
|
: new NodeScript<bool>("Is match", "Determines whether the current list item is a match", script.Context, new List<DefaultNode> {_startNode});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Evaluate()
|
||||||
|
{
|
||||||
|
if (Storage == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (InputList.Value == null)
|
||||||
|
{
|
||||||
|
Output.Value = Storage.Operator == ListOperator.None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_scriptLock)
|
||||||
|
{
|
||||||
|
if (Script == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (Storage.Operator == ListOperator.Any)
|
||||||
|
Output.Value = InputList.Value.Cast<object>().Any(EvaluateItem);
|
||||||
|
else if (Storage.Operator == ListOperator.All)
|
||||||
|
Output.Value = InputList.Value.Cast<object>().All(EvaluateItem);
|
||||||
|
else if (Storage.Operator == ListOperator.None)
|
||||||
|
Output.Value = InputList.Value.Cast<object>().All(v => !EvaluateItem(v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool EvaluateItem(object item)
|
||||||
|
{
|
||||||
|
if (Script == null || _startNode == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_startNode.Item = item;
|
||||||
|
Script.Run();
|
||||||
|
return Script.Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateStartNode()
|
||||||
|
{
|
||||||
|
Type? type = InputList.ConnectedTo.FirstOrDefault()?.Type;
|
||||||
|
// List must be generic or there's no way to tell what objects it contains in advance, that's not supported for now
|
||||||
|
if (type is not {IsGenericType: true})
|
||||||
|
return;
|
||||||
|
|
||||||
|
Type listType = type.GetGenericArguments().Single();
|
||||||
|
_startNode?.ChangeType(listType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InputListOnPinConnected(object? sender, SingleValueEventArgs<IPin> e)
|
||||||
|
{
|
||||||
|
lock (_scriptLock)
|
||||||
|
{
|
||||||
|
UpdateStartNode();
|
||||||
|
Script?.LoadConnections();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region IDisposable
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Script?.Dispose();
|
||||||
|
Script = null;
|
||||||
|
_startNode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
using Artemis.Core;
|
||||||
|
|
||||||
|
namespace Artemis.VisualScripting.Nodes.List;
|
||||||
|
|
||||||
|
public class ListOperatorPredicateStartNode : DefaultNode
|
||||||
|
{
|
||||||
|
internal static readonly Guid NodeId = new("9A714CF3-8D02-4CC3-A1AC-73833F82D7C6");
|
||||||
|
private readonly ObjectOutputPins _objectOutputPins;
|
||||||
|
|
||||||
|
public ListOperatorPredicateStartNode() : base(NodeId, "List item", "Contains the current list item")
|
||||||
|
{
|
||||||
|
_objectOutputPins = new ObjectOutputPins(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? Item { get; set; }
|
||||||
|
|
||||||
|
public override void Evaluate()
|
||||||
|
{
|
||||||
|
if (Item != null)
|
||||||
|
_objectOutputPins.SetCurrentValue(Item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeType(Type? type)
|
||||||
|
{
|
||||||
|
_objectOutputPins.ChangeType(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:screens="clr-namespace:Artemis.VisualScripting.Nodes.List.Screens"
|
||||||
|
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Artemis.VisualScripting.Nodes.List.Screens.ListOperatorPredicateNodeCustomView"
|
||||||
|
x:DataType="screens:ListOperatorPredicateNodeCustomViewModel">
|
||||||
|
<StackPanel Spacing="5">
|
||||||
|
<shared:EnumComboBox Value="{CompiledBinding Operator}" Classes="condensed" HorizontalAlignment="Stretch"/>
|
||||||
|
<Button HorizontalAlignment="Stretch" Classes="condensed" Command="{CompiledBinding OpenEditor}">Edit script</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.VisualScripting.Nodes.List.Screens;
|
||||||
|
|
||||||
|
public partial class ListOperatorPredicateNodeCustomView : ReactiveUserControl<ListOperatorPredicateNodeCustomViewModel>
|
||||||
|
{
|
||||||
|
public ListOperatorPredicateNodeCustomView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
using System.Reactive;
|
||||||
|
using System.Reactive.Disposables;
|
||||||
|
using Artemis.Core;
|
||||||
|
using Artemis.Core.Events;
|
||||||
|
using Artemis.UI.Shared.Services;
|
||||||
|
using Artemis.UI.Shared.Services.NodeEditor;
|
||||||
|
using Artemis.UI.Shared.VisualScripting;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.VisualScripting.Nodes.List.Screens;
|
||||||
|
|
||||||
|
public class ListOperatorPredicateNodeCustomViewModel : CustomNodeViewModel
|
||||||
|
{
|
||||||
|
private readonly ListOperatorPredicateNode _node;
|
||||||
|
private readonly IWindowService _windowService;
|
||||||
|
private ListOperator _operator;
|
||||||
|
private bool _canOpenEditor;
|
||||||
|
|
||||||
|
public ListOperatorPredicateNodeCustomViewModel(ListOperatorPredicateNode node, INodeScript script, IWindowService windowService) : base(node, script)
|
||||||
|
{
|
||||||
|
_node = node;
|
||||||
|
_windowService = windowService;
|
||||||
|
|
||||||
|
OpenEditor = ReactiveCommand.CreateFromTask(ExecuteOpenEditor, this.WhenAnyValue(vm => vm.CanOpenEditor));
|
||||||
|
CanOpenEditor = node.InputList.ConnectedTo.Any();
|
||||||
|
|
||||||
|
this.WhenActivated(d =>
|
||||||
|
{
|
||||||
|
node.InputList.PinConnected += InputListOnPinConnected;
|
||||||
|
node.InputList.PinDisconnected += InputListOnPinDisconnected;
|
||||||
|
|
||||||
|
Disposable.Create(() =>
|
||||||
|
{
|
||||||
|
node.InputList.PinConnected -= InputListOnPinConnected;
|
||||||
|
node.InputList.PinDisconnected -= InputListOnPinDisconnected;
|
||||||
|
}).DisposeWith(d);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReactiveCommand<Unit, Unit> OpenEditor { get; }
|
||||||
|
|
||||||
|
private bool CanOpenEditor
|
||||||
|
{
|
||||||
|
get => _canOpenEditor;
|
||||||
|
set => this.RaiseAndSetIfChanged(ref _canOpenEditor, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListOperator Operator
|
||||||
|
{
|
||||||
|
get => _operator;
|
||||||
|
set => this.RaiseAndSetIfChanged(ref _operator, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteOpenEditor()
|
||||||
|
{
|
||||||
|
if (_node.Script == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await _windowService.ShowDialogAsync<NodeScriptWindowViewModelBase, bool>(("nodeScript", _node.Script));
|
||||||
|
_node.Script.Save();
|
||||||
|
|
||||||
|
_node.Storage ??= new ListOperatorEntity();
|
||||||
|
_node.Storage.Script = _node.Script.Entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InputListOnPinDisconnected(object? sender, SingleValueEventArgs<IPin> e)
|
||||||
|
{
|
||||||
|
CanOpenEditor = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InputListOnPinConnected(object? sender, SingleValueEventArgs<IPin> e)
|
||||||
|
{
|
||||||
|
CanOpenEditor = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user