Event condition - Added toggle off mode

Profile editor - Added element copy/pasting
Shared UI - Fix namespaces
This commit is contained in:
Robert committed 2022-04-17 20:23:18 +02:00
1 parent 8c7bbc3f0f
commit 52f2338154
84 files changed
+1059 -608

No files matched your search

@@ -75,6 +75,9 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cinput_005Cevents/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cinput_005Cinterfaces/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cinterfaces/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cprocessmonitor/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cprocessmonitor_005Cevents/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cprocessmonitor_005Cinterfaces/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cregistration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cregistration_005Cinterfaces/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Cstorage/@EntryIndexedValue">True</s:Boolean>
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using Artemis.Core.Internal;
using Artemis.Core.VisualScripting.Internal;
using Artemis.Storage.Entities.Profile.Abstract;
using Artemis.Storage.Entities.Profile.Conditions;
@@ -13,13 +14,15 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
{
private readonly string _displayName;
private readonly EventConditionEntity _entity;
private EventDefaultNode _eventNode;
private IEventConditionNode _startNode;
private DataModelPath? _eventPath;
private DateTime _lastProcessedTrigger;
private EventOverlapMode _overlapMode;
private NodeScript<bool> _script;
private EventTriggerMode _triggerMode;
private bool _wasMet;
private DateTime _lastProcessedTrigger;
private object? _lastProcessedValue;
private EventOverlapMode _overlapMode;
private EventTriggerMode _triggerMode;
private EventToggleOffMode _toggleOffMode;
/// <summary>
/// Creates a new instance of the <see cref="EventCondition" /> class
@@ -30,7 +33,7 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
_entity = new EventConditionEntity();
_displayName = profileElement.GetType().Name;
_eventNode = new EventDefaultNode {X = -300};
_startNode = new EventConditionEventStartNode {X = -300};
_script = new NodeScript<bool>($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile);
}
@@ -40,7 +43,7 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
_entity = entity;
_displayName = profileElement.GetType().Name;
_eventNode = new EventDefaultNode();
_startNode = new EventConditionEventStartNode();
_script = null!;
Load();
@@ -83,18 +86,70 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
set => SetAndNotify(ref _overlapMode, value);
}
/// <summary>
/// Gets or sets the mode for render elements when toggling off the event when using <see cref="EventTriggerMode.Toggle"/>.
/// </summary>
public EventToggleOffMode ToggleOffMode
{
get => _toggleOffMode;
set => SetAndNotify(ref _toggleOffMode, value);
}
/// <summary>
/// Updates the event node, applying the selected event
/// </summary>
public void UpdateEventNode()
{
IDataModelEvent? dataModelEvent = EventPath?.GetValue() as IDataModelEvent;
_eventNode.CreatePins(dataModelEvent);
if (EventPath == null)
return;
if (dataModelEvent != null && !Script.Nodes.Contains(_eventNode))
Script.AddNode(_eventNode);
else if (dataModelEvent == null && Script.Nodes.Contains(_eventNode))
Script.RemoveNode(_eventNode);
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);
}
}
private void ReplaceStartNode(IEventConditionNode newStartNode)
{
if (Script.Nodes.Contains(_startNode))
Script.RemoveNode(_startNode);
_startNode = newStartNode;
if (!Script.Nodes.Contains(_startNode))
Script.AddNode(_startNode);
}
/// <summary>
@@ -103,15 +158,30 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
/// <returns>The start node of the event script, if any.</returns>
public INode GetStartNode()
{
return _eventNode;
return _startNode;
}
private bool Evaluate()
{
if (EventPath?.GetValue() is not IDataModelEvent dataModelEvent || dataModelEvent.LastTrigger <= _lastProcessedTrigger)
if (EventPath == null)
return false;
_lastProcessedTrigger = dataModelEvent.LastTrigger;
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;
@@ -151,6 +221,8 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
{
if (IsMet && !_wasMet)
ProfileElement.Timeline.JumpToStart();
if (!IsMet && _wasMet && ToggleOffMode == EventToggleOffMode.SkipToEnd)
ProfileElement.Timeline.JumpToEndSegment();
}
else
{
@@ -191,6 +263,7 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
{
TriggerMode = (EventTriggerMode) _entity.TriggerMode;
OverlapMode = (EventOverlapMode) _entity.OverlapMode;
ToggleOffMode = (EventToggleOffMode) _entity.ToggleOffMode;
if (_entity.EventPath != null)
EventPath = new DataModelPath(_entity.EventPath);
@@ -206,6 +279,8 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
{
_entity.TriggerMode = (int) TriggerMode;
_entity.OverlapMode = (int) OverlapMode;
_entity.ToggleOffMode = (int) ToggleOffMode;
Script.Save();
_entity.Script = Script?.Entity;
EventPath?.Save();
@@ -221,9 +296,9 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
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 == EventDefaultNode.NodeId);
INode? existingEventNode = Script.Nodes.FirstOrDefault(n => n.Id == EventConditionEventStartNode.NodeId || n.Id == EventConditionValueChangedStartNode.NodeId);
if (existingEventNode != null)
_eventNode = (EventDefaultNode) existingEventNode;
_startNode = (IEventConditionNode) existingEventNode;
UpdateEventNode();
Script.LoadConnections();
@@ -268,4 +343,20 @@ public enum EventOverlapMode
/// 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
}
@@ -1,67 +0,0 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core
{
internal class DataModelValueChangedEvent<T> : IDataModelEvent
{
public DataModelValueChangedEvent(DataModelPath path)
{
Path = path;
}
public DataModelPath Path { get; }
public T? LastValue { get; private set; }
public T? CurrentValue { get; private set; }
public DateTime LastTrigger { get; private set; }
public TimeSpan TimeSinceLastTrigger => DateTime.Now - LastTrigger;
public int TriggerCount { get; private set; }
public Type ArgumentsType { get; } = typeof(DataModelValueChangedEventArgs<T>);
public string TriggerPastParticiple => "changed";
public bool TrackHistory { get; set; } = false;
public DataModelEventArgs? LastEventArgumentsUntyped { get; private set; }
public List<DataModelEventArgs> EventArgumentsHistoryUntyped { get; } = new();
public void Update()
{
if (!Path.IsValid)
return;
object? value = Path.GetValue();
if (value != null)
CurrentValue = (T?) value;
else
CurrentValue = default;
if (!Equals(LastValue, CurrentValue))
Trigger();
LastValue = CurrentValue;
}
public void Reset()
{
TriggerCount = 0;
}
private void Trigger()
{
LastEventArgumentsUntyped = new DataModelValueChangedEventArgs<T>(CurrentValue, LastValue);
LastTrigger = DateTime.Now;
TriggerCount++;
OnEventTriggered();
}
#region Events
public event EventHandler? EventTriggered;
internal virtual void OnEventTriggered()
{
EventTriggered?.Invoke(this, EventArgs.Empty);
}
#endregion
}
}
@@ -1,18 +0,0 @@
using Artemis.Core.Modules;
namespace Artemis.Core
{
internal class DataModelValueChangedEventArgs<T> : DataModelEventArgs
{
public DataModelValueChangedEventArgs(T? currentValue, T? previousValue)
{
CurrentValue = currentValue;
PreviousValue = previousValue;
}
[DataModelProperty(Description = "The current value of the property")]
public T? CurrentValue { get; }
[DataModelProperty(Description = "The previous value of the property")]
public T? PreviousValue { get; }
}
}
@@ -334,20 +334,18 @@ namespace Artemis.Core
/// Explorer
/// </summary>
/// <returns>The resulting name i.e. <c>New layer</c> or <c>New layer (2)</c></returns>
public string GetNewLayerName()
public string GetNewLayerName(string baseName = "New layer")
{
if (!Children.Any(c => c is Layer))
return "New layer";
if (!Children.Any(c => c is Layer && c.Name == baseName))
return baseName;
// Grab existing unnamed layers and get the first available number
// Looks slow but it's not https://stackoverflow.com/a/8865806/5015269
Regex regex = new(@"New layer \((\d+)\)");
int firstAvailable = Enumerable.Range(1, int.MaxValue)
.Except(Children.Where(c => c is Layer && c.Name != null && regex.IsMatch(c.Name))
.Select(c => int.Parse(regex.Match(c.Name!).Groups[1].Value))
.OrderBy(i => i))
.First();
return $"New layer ({firstAvailable})";
int current = 2;
while (true)
{
if (Children.All(c => c is Layer && c.Name != $"{baseName} ({current})"))
return $"{baseName} ({current})";
current++;
}
}
/// <summary>
@@ -355,20 +353,18 @@ namespace Artemis.Core
/// in Explorer
/// </summary>
/// <returns>The resulting name i.e. <c>New folder</c> or <c>New folder (2)</c></returns>
public string GetNewFolderName()
public string GetNewFolderName(string baseName = "New folder")
{
if (!Children.Any(c => c is Folder))
return "New folder";
if (!Children.Any(c => c is Folder && c.Name == baseName))
return baseName;
// Grab existing unnamed layers and get the first available number
// Looks slow but it's not https://stackoverflow.com/a/8865806/5015269
Regex regex = new(@"New folder \((\d+)\)");
int firstAvailable = Enumerable.Range(1, int.MaxValue)
.Except(Children.Where(c => c is Folder && c.Name != null && regex.IsMatch(c.Name))
.Select(c => int.Parse(regex.Match(c.Name!).Groups[1].Value))
.OrderBy(i => i))
.First();
return $"New folder ({firstAvailable})";
int current = 2;
while (true)
{
if (Children.All(c => c is Folder && c.Name != $"{baseName} ({current})"))
return $"{baseName} ({current})";
current++;
}
}
#endregion
+3 -2
View File
@@ -38,13 +38,14 @@ namespace Artemis.Core.Services
// ReSharper disable UnusedParameter.Local
public CoreService(IKernel kernel,
ILogger logger,
StorageMigrationService _, // injected to ensure migration runs early
StorageMigrationService _1, // injected to ensure migration runs early
ISettingsService settingsService,
IPluginManagementService pluginManagementService,
IRgbService rgbService,
IProfileService profileService,
IModuleService moduleService,
IScriptingService scriptingService)
IScriptingService scriptingService,
IProcessMonitorService _2)
{
Kernel = kernel;
Constants.CorePlugin.Kernel = kernel;
@@ -11,7 +11,6 @@ namespace Artemis.Core.Services
internal class ProcessMonitorService : IProcessMonitorService
{
private readonly ILogger _logger;
private readonly Timer _processScanTimer;
private readonly ProcessComparer _comparer;
private Process[] _lastScannedProcesses;
@@ -19,16 +18,15 @@ namespace Artemis.Core.Services
{
_logger = logger;
_lastScannedProcesses = Process.GetProcesses();
_processScanTimer = new Timer(1000);
_processScanTimer.Elapsed += OnTimerElapsed;
_processScanTimer.Start();
Timer processScanTimer = new(1000);
processScanTimer.Elapsed += OnTimerElapsed;
processScanTimer.Start();
_comparer = new ProcessComparer();
ProcessActivationRequirement.ProcessMonitorService = this;
}
public event EventHandler<ProcessEventArgs>? ProcessStarted;
public event EventHandler<ProcessEventArgs>? ProcessStopped;
public IEnumerable<Process> GetRunningProcesses()
@@ -36,7 +34,7 @@ namespace Artemis.Core.Services
return _lastScannedProcesses;
}
private void OnTimerElapsed(object sender, ElapsedEventArgs e)
private void OnTimerElapsed(object? sender, ElapsedEventArgs e)
{
Process[] newProcesses = Process.GetProcesses();
foreach (Process startedProcess in newProcesses.Except(_lastScannedProcesses, _comparer))
@@ -64,7 +62,7 @@ namespace Artemis.Core.Services
return x.Id == y.Id && x.ProcessName == y.ProcessName && x.SessionId == y.SessionId;
}
public int GetHashCode(Process obj)
public int GetHashCode(Process? obj)
{
if (obj == null) return 0;
return obj.Id;
@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Artemis.Core.Modules;
using Artemis.Core.VisualScripting.Internal;
using Humanizer;
namespace Artemis.Core.Internal;
internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode
{
internal static readonly Guid NodeId = new("278735FE-69E9-4A73-A6B8-59E83EE19305");
private readonly List<OutputPin> _pinBucket = new();
private readonly Dictionary<PropertyInfo, OutputPin> _propertyPins;
private IDataModelEvent? _dataModelEvent;
public EventConditionEventStartNode() : base(NodeId, "Event Arguments", "Contains the event arguments that triggered the evaluation")
{
_propertyPins = new Dictionary<PropertyInfo, OutputPin>();
}
public void CreatePins(IDataModelEvent? dataModelEvent)
{
if (_dataModelEvent == dataModelEvent)
return;
while (Pins.Any())
RemovePin((Pin) Pins.First());
_propertyPins.Clear();
_dataModelEvent = dataModelEvent;
if (dataModelEvent == null)
return;
foreach (PropertyInfo propertyInfo in dataModelEvent.ArgumentsType
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(p => p.CustomAttributes.All(a => a.AttributeType != typeof(DataModelIgnoreAttribute))))
_propertyPins.Add(propertyInfo, CreateOrAddOutputPin(propertyInfo.PropertyType, propertyInfo.Name.Humanize()));
}
public override void Evaluate()
{
if (_dataModelEvent?.LastEventArgumentsUntyped == null)
return;
foreach ((PropertyInfo propertyInfo, OutputPin outputPin) in _propertyPins)
{
if (outputPin.ConnectedTo.Any())
outputPin.Value = propertyInfo.GetValue(_dataModelEvent.LastEventArgumentsUntyped) ?? outputPin.Type.GetDefault()!;
}
}
/// <summary>
/// Creates or adds an input pin to the node using a bucket.
/// The bucket might grow a bit over time as the user edits the node but pins won't get lost, enabling undo/redo in the
/// editor.
/// </summary>
private OutputPin CreateOrAddOutputPin(Type valueType, string displayName)
{
// Grab the first pin from the bucket that isn't on the node yet
OutputPin? pin = _pinBucket.FirstOrDefault(p => !Pins.Contains(p));
// If there is none, create a new one and add it to the bucket
if (pin == null)
{
pin = CreateOutputPin(valueType, displayName);
_pinBucket.Add(pin);
}
// If there was a pin in the bucket, update it's type and display name and reuse it
else
{
pin.ChangeType(valueType);
pin.Name = displayName;
AddPin(pin);
}
return pin;
}
}
@@ -0,0 +1,46 @@
using System;
using Artemis.Core.VisualScripting.Internal;
namespace Artemis.Core.Internal;
internal class EventConditionValueChangedStartNode : DefaultNode, IEventConditionNode
{
internal static readonly Guid NodeId = new("F9A270DB-A231-4800-BAB3-DC1F96856756");
private object? _newValue;
private object? _oldValue;
public EventConditionValueChangedStartNode() : base(NodeId, "Changed values", "Contains the old and new values of the property chat was changed.")
{
NewValue = CreateOutputPin(typeof(object), "New value");
OldValue = CreateOutputPin(typeof(object), "Old value");
}
public OutputPin NewValue { get; }
public OutputPin OldValue { get; }
public void UpdateOutputPins(DataModelPath dataModelPath)
{
Type? type = dataModelPath?.GetPropertyType();
if (Numeric.IsTypeCompatible(type))
type = typeof(Numeric);
type ??= typeof(object);
if (NewValue.Type != type)
NewValue.ChangeType(type);
if (OldValue.Type != type)
OldValue.ChangeType(type);
}
public void UpdateValues(object? newValue, object? oldValue)
{
_newValue = newValue;
_oldValue = oldValue;
}
/// <inheritdoc />
public override void Evaluate()
{
NewValue.Value = _newValue;
OldValue.Value = _oldValue;
}
}
@@ -1,82 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Artemis.Core.Modules;
using Humanizer;
namespace Artemis.Core.Internal
{
internal class EventDefaultNode : DefaultNode
{
internal static readonly Guid NodeId = new("278735FE-69E9-4A73-A6B8-59E83EE19305");
private readonly Dictionary<PropertyInfo, OutputPin> _propertyPins;
private readonly List<OutputPin> _pinBucket = new();
private IDataModelEvent? _dataModelEvent;
public EventDefaultNode() : base(NodeId, "Event Arguments", "Contains the event arguments that triggered the evaluation")
{
_propertyPins = new Dictionary<PropertyInfo, OutputPin>();
}
public void CreatePins(IDataModelEvent? dataModelEvent)
{
if (_dataModelEvent == dataModelEvent)
return;
while (Pins.Any())
RemovePin((Pin) Pins.First());
_propertyPins.Clear();
_dataModelEvent = dataModelEvent;
if (dataModelEvent == null)
return;
foreach (PropertyInfo propertyInfo in dataModelEvent.ArgumentsType
.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(p => p.CustomAttributes.All(a => a.AttributeType != typeof(DataModelIgnoreAttribute))))
{
_propertyPins.Add(propertyInfo, CreateOrAddOutputPin(propertyInfo.PropertyType, propertyInfo.Name.Humanize()));
}
}
public override void Evaluate()
{
if (_dataModelEvent?.LastEventArgumentsUntyped == null)
return;
foreach ((PropertyInfo propertyInfo, OutputPin outputPin) in _propertyPins)
{
if (outputPin.ConnectedTo.Any())
outputPin.Value = propertyInfo.GetValue(_dataModelEvent.LastEventArgumentsUntyped) ?? outputPin.Type.GetDefault()!;
}
}
/// <summary>
/// Creates or adds an input pin to the node using a bucket.
/// The bucket might grow a bit over time as the user edits the node but pins won't get lost, enabling undo/redo in the
/// editor.
/// </summary>
private OutputPin CreateOrAddOutputPin(Type valueType, string displayName)
{
// Grab the first pin from the bucket that isn't on the node yet
OutputPin? pin = _pinBucket.FirstOrDefault(p => !Pins.Contains(p));
// If there is none, create a new one and add it to the bucket
if (pin == null)
{
pin = CreateOutputPin(valueType, displayName);
_pinBucket.Add(pin);
}
// If there was a pin in the bucket, update it's type and display name and reuse it
else
{
pin.ChangeType(valueType);
pin.Name = displayName;
AddPin(pin);
}
return pin;
}
}
}
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Artemis.Core.VisualScripting.Internal
{
internal interface IEventConditionNode : INode
{
}
}