mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Merge branch 'development'
This commit is contained in:
commit
ab7c3933c5
@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Artemis.Core.Internal;
|
||||
using Artemis.Core.VisualScripting.Internal;
|
||||
using Artemis.Storage.Entities.Profile.Abstract;
|
||||
using Artemis.Storage.Entities.Profile.Conditions;
|
||||
|
||||
@ -14,12 +14,12 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
|
||||
{
|
||||
private readonly string _displayName;
|
||||
private readonly EventConditionEntity _entity;
|
||||
private NodeScript<bool> _script;
|
||||
private DefaultNode _startNode;
|
||||
private DataModelPath? _eventPath;
|
||||
private DateTime _lastProcessedTrigger;
|
||||
private object? _lastProcessedValue;
|
||||
private EventOverlapMode _overlapMode;
|
||||
private NodeScript<bool> _script;
|
||||
private IEventConditionNode _startNode;
|
||||
private EventToggleOffMode _toggleOffMode;
|
||||
private EventTriggerMode _triggerMode;
|
||||
private bool _wasMet;
|
||||
@ -34,7 +34,7 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
|
||||
_entity = new EventConditionEntity();
|
||||
_displayName = profileElement.GetType().Name;
|
||||
_startNode = new EventConditionEventStartNode {X = -300};
|
||||
_script = new NodeScript<bool>($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile);
|
||||
_script = new NodeScript<bool>($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile, new List<DefaultNode> {_startNode});
|
||||
}
|
||||
|
||||
internal EventCondition(EventConditionEntity entity, RenderProfileElement profileElement)
|
||||
@ -96,109 +96,6 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
|
||||
set => SetAndNotify(ref _toggleOffMode, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the event node, applying the selected event
|
||||
/// </summary>
|
||||
public void UpdateEventNode()
|
||||
{
|
||||
if (EventPath == null)
|
||||
return;
|
||||
|
||||
Type? pathType = EventPath.GetPropertyType();
|
||||
if (pathType == null)
|
||||
return;
|
||||
|
||||
// Create an event node if the path type is a data model event
|
||||
if (pathType.IsAssignableTo(typeof(IDataModelEvent)))
|
||||
{
|
||||
EventConditionEventStartNode eventNode;
|
||||
// Ensure the start node is an event node
|
||||
if (_startNode is not EventConditionEventStartNode node)
|
||||
{
|
||||
eventNode = new EventConditionEventStartNode();
|
||||
ReplaceStartNode(eventNode);
|
||||
_startNode = eventNode;
|
||||
}
|
||||
else
|
||||
{
|
||||
eventNode = node;
|
||||
}
|
||||
|
||||
IDataModelEvent? dataModelEvent = EventPath?.GetValue() as IDataModelEvent;
|
||||
eventNode.CreatePins(dataModelEvent);
|
||||
}
|
||||
// Create a value changed node if the path type is a regular value
|
||||
else
|
||||
{
|
||||
// Ensure the start nod is a value changed node
|
||||
EventConditionValueChangedStartNode valueChangedNode;
|
||||
// Ensure the start node is an event node
|
||||
if (_startNode is not EventConditionValueChangedStartNode node)
|
||||
{
|
||||
valueChangedNode = new EventConditionValueChangedStartNode();
|
||||
ReplaceStartNode(valueChangedNode);
|
||||
}
|
||||
else
|
||||
{
|
||||
valueChangedNode = node;
|
||||
}
|
||||
|
||||
valueChangedNode.UpdateOutputPins(EventPath);
|
||||
}
|
||||
|
||||
if (!Script.Nodes.Contains(_startNode))
|
||||
Script.AddNode(_startNode);
|
||||
Script.Save();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the start node of the event script, if any
|
||||
/// </summary>
|
||||
/// <returns>The start node of the event script, if any.</returns>
|
||||
public INode GetStartNode()
|
||||
{
|
||||
return _startNode;
|
||||
}
|
||||
|
||||
private void ReplaceStartNode(IEventConditionNode newStartNode)
|
||||
{
|
||||
if (Script.Nodes.Contains(_startNode))
|
||||
Script.RemoveNode(_startNode);
|
||||
|
||||
_startNode = newStartNode;
|
||||
if (!Script.Nodes.Contains(_startNode))
|
||||
Script.AddNode(_startNode);
|
||||
}
|
||||
|
||||
private bool Evaluate()
|
||||
{
|
||||
if (EventPath == null)
|
||||
return false;
|
||||
|
||||
object? value = EventPath.GetValue();
|
||||
if (_startNode is EventConditionEventStartNode)
|
||||
{
|
||||
if (value is not IDataModelEvent dataModelEvent || dataModelEvent.LastTrigger <= _lastProcessedTrigger)
|
||||
return false;
|
||||
|
||||
_lastProcessedTrigger = dataModelEvent.LastTrigger;
|
||||
}
|
||||
else if (_startNode is EventConditionValueChangedStartNode valueChangedNode)
|
||||
{
|
||||
if (Equals(value, _lastProcessedValue))
|
||||
return false;
|
||||
|
||||
valueChangedNode.UpdateValues(value, _lastProcessedValue);
|
||||
_lastProcessedValue = value;
|
||||
}
|
||||
|
||||
if (!Script.ExitNodeConnected)
|
||||
return true;
|
||||
|
||||
Script.Run();
|
||||
return Script.Result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IConditionEntity Entity => _entity;
|
||||
|
||||
@ -264,6 +161,116 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
|
||||
Script?.Dispose();
|
||||
EventPath?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the event node, applying the selected event
|
||||
/// </summary>
|
||||
public void UpdateEventNode(bool updateScript)
|
||||
{
|
||||
if (EventPath == null)
|
||||
return;
|
||||
|
||||
Type? pathType = EventPath.GetPropertyType();
|
||||
if (pathType == null)
|
||||
return;
|
||||
|
||||
// Create an event node if the path type is a data model event
|
||||
if (pathType.IsAssignableTo(typeof(IDataModelEvent)))
|
||||
{
|
||||
EventConditionEventStartNode eventNode;
|
||||
// Ensure the start node is an event node
|
||||
if (_startNode is not EventConditionEventStartNode node)
|
||||
{
|
||||
eventNode = new EventConditionEventStartNode();
|
||||
if (updateScript)
|
||||
ReplaceStartNode(eventNode);
|
||||
_startNode = eventNode;
|
||||
}
|
||||
else
|
||||
{
|
||||
eventNode = node;
|
||||
}
|
||||
|
||||
IDataModelEvent? dataModelEvent = EventPath?.GetValue() as IDataModelEvent;
|
||||
eventNode.CreatePins(dataModelEvent);
|
||||
}
|
||||
// Create a value changed node if the path type is a regular value
|
||||
else
|
||||
{
|
||||
// Ensure the start nod is a value changed node
|
||||
EventConditionValueChangedStartNode valueChangedNode;
|
||||
// Ensure the start node is an event node
|
||||
if (_startNode is not EventConditionValueChangedStartNode node)
|
||||
{
|
||||
valueChangedNode = new EventConditionValueChangedStartNode();
|
||||
if (updateScript)
|
||||
ReplaceStartNode(valueChangedNode);
|
||||
_startNode = valueChangedNode;
|
||||
}
|
||||
else
|
||||
{
|
||||
valueChangedNode = node;
|
||||
}
|
||||
|
||||
valueChangedNode.UpdateOutputPins(EventPath);
|
||||
}
|
||||
|
||||
// Script can be null if called before load
|
||||
if (!updateScript)
|
||||
return;
|
||||
if (!Script.Nodes.Contains(_startNode))
|
||||
{
|
||||
Script.AddNode(_startNode);
|
||||
Script.LoadConnections();
|
||||
}
|
||||
Script.Save();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the start node of the event script, if any
|
||||
/// </summary>
|
||||
/// <returns>The start node of the event script, if any.</returns>
|
||||
public INode GetStartNode()
|
||||
{
|
||||
return _startNode;
|
||||
}
|
||||
|
||||
private void ReplaceStartNode(DefaultNode newStartNode)
|
||||
{
|
||||
if (Script.Nodes.Contains(_startNode))
|
||||
Script.RemoveNode(_startNode);
|
||||
if (!Script.Nodes.Contains(newStartNode))
|
||||
Script.AddNode(newStartNode);
|
||||
}
|
||||
|
||||
private bool Evaluate()
|
||||
{
|
||||
if (EventPath == null)
|
||||
return false;
|
||||
|
||||
object? value = EventPath.GetValue();
|
||||
if (_startNode is EventConditionEventStartNode)
|
||||
{
|
||||
if (value is not IDataModelEvent dataModelEvent || dataModelEvent.LastTrigger <= _lastProcessedTrigger)
|
||||
return false;
|
||||
|
||||
_lastProcessedTrigger = dataModelEvent.LastTrigger;
|
||||
}
|
||||
else if (_startNode is EventConditionValueChangedStartNode valueChangedNode)
|
||||
{
|
||||
if (Equals(value, _lastProcessedValue))
|
||||
return false;
|
||||
|
||||
valueChangedNode.UpdateValues(value, _lastProcessedValue);
|
||||
_lastProcessedValue = value;
|
||||
}
|
||||
|
||||
if (!Script.ExitNodeConnected)
|
||||
return true;
|
||||
|
||||
Script.Run();
|
||||
return Script.Result;
|
||||
}
|
||||
|
||||
#region Storage
|
||||
|
||||
@ -276,11 +283,13 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
|
||||
|
||||
if (_entity.EventPath != null)
|
||||
EventPath = new DataModelPath(_entity.EventPath);
|
||||
|
||||
UpdateEventNode(false);
|
||||
|
||||
string name = $"Activate {_displayName}";
|
||||
string description = $"Whether or not the event should activate the {_displayName}";
|
||||
Script = _entity.Script != null
|
||||
? new NodeScript<bool>($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", _entity.Script, ProfileElement.Profile)
|
||||
: new NodeScript<bool>($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile);
|
||||
UpdateEventNode();
|
||||
? new NodeScript<bool>(name, description, _entity.Script, ProfileElement.Profile, new List<DefaultNode> {_startNode})
|
||||
: new NodeScript<bool>(name, description, ProfileElement.Profile, new List<DefaultNode> {_startNode});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -311,71 +320,9 @@ public class EventCondition : CorePropertyChanged, INodeScriptCondition
|
||||
/// <inheritdoc />
|
||||
public void LoadNodeScript()
|
||||
{
|
||||
UpdateEventNode(true);
|
||||
Script.Load();
|
||||
|
||||
// The load action may have created an event node, use that one over the one we have here
|
||||
INode? existingEventNode = Script.Nodes.FirstOrDefault(n => n.Id == EventConditionEventStartNode.NodeId || n.Id == EventConditionValueChangedStartNode.NodeId);
|
||||
if (existingEventNode != null)
|
||||
_startNode = (IEventConditionNode) existingEventNode;
|
||||
|
||||
UpdateEventNode();
|
||||
Script.LoadConnections();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a mode for render elements to start their timeline when display conditions events are fired.
|
||||
/// </summary>
|
||||
public enum EventTriggerMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Play the timeline once.
|
||||
/// </summary>
|
||||
Play,
|
||||
|
||||
/// <summary>
|
||||
/// Toggle repeating the timeline.
|
||||
/// </summary>
|
||||
Toggle
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a mode for render elements to configure the behaviour of events that overlap i.e. trigger again before
|
||||
/// the timeline finishes.
|
||||
/// </summary>
|
||||
public enum EventOverlapMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Stop the current run and restart the timeline
|
||||
/// </summary>
|
||||
Restart,
|
||||
|
||||
/// <summary>
|
||||
/// Play another copy of the timeline on top of the current run
|
||||
/// </summary>
|
||||
Copy,
|
||||
|
||||
/// <summary>
|
||||
/// Ignore subsequent event fires until the timeline finishes
|
||||
/// </summary>
|
||||
Ignore
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a mode for render elements when toggling off the event when using <see cref="EventTriggerMode.Toggle" />
|
||||
/// .
|
||||
/// </summary>
|
||||
public enum EventToggleOffMode
|
||||
{
|
||||
/// <summary>
|
||||
/// When the event toggles the condition off, finish the the current run of the main timeline
|
||||
/// </summary>
|
||||
Finish,
|
||||
|
||||
/// <summary>
|
||||
/// When the event toggles the condition off, skip to the end segment of the timeline
|
||||
/// </summary>
|
||||
SkipToEnd
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
namespace Artemis.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a mode for render elements to configure the behaviour of events that overlap i.e. trigger again before
|
||||
/// the timeline finishes.
|
||||
/// </summary>
|
||||
public enum EventOverlapMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Stop the current run and restart the timeline
|
||||
/// </summary>
|
||||
Restart,
|
||||
|
||||
/// <summary>
|
||||
/// Play another copy of the timeline on top of the current run
|
||||
/// </summary>
|
||||
Copy,
|
||||
|
||||
/// <summary>
|
||||
/// Ignore subsequent event fires until the timeline finishes
|
||||
/// </summary>
|
||||
Ignore
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
namespace Artemis.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a mode for render elements when toggling off the event when using <see cref="EventTriggerMode.Toggle" />
|
||||
/// .
|
||||
/// </summary>
|
||||
public enum EventToggleOffMode
|
||||
{
|
||||
/// <summary>
|
||||
/// When the event toggles the condition off, finish the the current run of the main timeline
|
||||
/// </summary>
|
||||
Finish,
|
||||
|
||||
/// <summary>
|
||||
/// When the event toggles the condition off, skip to the end segment of the timeline
|
||||
/// </summary>
|
||||
SkipToEnd
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
namespace Artemis.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a mode for render elements to start their timeline when display conditions events are fired.
|
||||
/// </summary>
|
||||
public enum EventTriggerMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Play the timeline once.
|
||||
/// </summary>
|
||||
Play,
|
||||
|
||||
/// <summary>
|
||||
/// Toggle repeating the timeline.
|
||||
/// </summary>
|
||||
Toggle
|
||||
}
|
||||
@ -8,6 +8,13 @@ namespace Artemis.Core.Services;
|
||||
/// </summary>
|
||||
public abstract class InputProvider : IDisposable
|
||||
{
|
||||
public InputProvider()
|
||||
{
|
||||
ProviderName = GetType().FullName ?? throw new InvalidOperationException("Input provider must have a type with a name");
|
||||
}
|
||||
|
||||
internal string ProviderName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Called when the input service requests a <see cref="KeyboardToggleStatusReceived" /> event
|
||||
/// </summary>
|
||||
|
||||
@ -10,12 +10,18 @@ internal class InputService : IInputService
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IRgbService _rgbService;
|
||||
private ArtemisDevice? _firstKeyboard;
|
||||
private ArtemisDevice? _firstMouse;
|
||||
private int _keyboardCount;
|
||||
private int _mouseCount;
|
||||
|
||||
public InputService(ILogger logger, IRgbService rgbService)
|
||||
{
|
||||
_logger = logger;
|
||||
_rgbService = rgbService;
|
||||
|
||||
_rgbService.DeviceAdded += RgbServiceOnDevicesModified;
|
||||
_rgbService.DeviceRemoved += RgbServiceOnDevicesModified;
|
||||
BustIdentifierCache();
|
||||
}
|
||||
|
||||
@ -132,8 +138,6 @@ internal class InputService : IInputService
|
||||
|
||||
private readonly Dictionary<Tuple<InputProvider, object>, ArtemisDevice> _deviceCache = new();
|
||||
private List<ArtemisDevice> _devices = new();
|
||||
private ArtemisDevice? _cachedFallbackKeyboard;
|
||||
private ArtemisDevice? _cachedFallbackMouse;
|
||||
private ArtemisDevice? _identifyingDevice;
|
||||
|
||||
public void IdentifyDevice(ArtemisDevice device)
|
||||
@ -164,13 +168,29 @@ internal class InputService : IInputService
|
||||
if (provider == null) throw new ArgumentNullException(nameof(provider));
|
||||
if (identifier == null) throw new ArgumentNullException(nameof(identifier));
|
||||
|
||||
// We will almost always only have zero or one of each
|
||||
if (type == InputDeviceType.Keyboard)
|
||||
{
|
||||
if (_keyboardCount == 0)
|
||||
return null;
|
||||
if (_keyboardCount == 1)
|
||||
return _firstKeyboard;
|
||||
}
|
||||
|
||||
if (type == InputDeviceType.Mouse)
|
||||
{
|
||||
if (_mouseCount == 0)
|
||||
return null;
|
||||
if (_mouseCount == 1)
|
||||
return _firstMouse;
|
||||
}
|
||||
|
||||
// Try cache first
|
||||
ArtemisDevice? cacheMatch = GetDeviceFromCache(provider, identifier);
|
||||
if (cacheMatch != null)
|
||||
return cacheMatch;
|
||||
|
||||
string providerName = provider.GetType().FullName!;
|
||||
ArtemisDevice? match = _devices.FirstOrDefault(m => m.InputIdentifiers.Any(i => Equals(i.InputProvider, providerName) && Equals(i.Identifier, identifier)));
|
||||
ArtemisDevice? match = _devices.FirstOrDefault(m => m.InputIdentifiers.Any(i => Equals(i.InputProvider, provider.ProviderName) && Equals(i.Identifier, identifier)));
|
||||
|
||||
// If a match was found cache it to speed up the next event and return the match
|
||||
if (match != null)
|
||||
@ -179,34 +199,16 @@ internal class InputService : IInputService
|
||||
return match;
|
||||
}
|
||||
|
||||
// If there is no match, apply our fallback type
|
||||
if (type == InputDeviceType.None)
|
||||
return null;
|
||||
if (type == InputDeviceType.Keyboard)
|
||||
{
|
||||
if (_cachedFallbackKeyboard != null)
|
||||
return _cachedFallbackKeyboard;
|
||||
_cachedFallbackKeyboard = _rgbService.EnabledDevices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Keyboard);
|
||||
return _cachedFallbackKeyboard;
|
||||
}
|
||||
|
||||
return _firstKeyboard;
|
||||
if (type == InputDeviceType.Mouse)
|
||||
{
|
||||
if (_cachedFallbackMouse != null)
|
||||
return _cachedFallbackMouse;
|
||||
_cachedFallbackMouse = _rgbService.EnabledDevices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Mouse);
|
||||
return _cachedFallbackMouse;
|
||||
}
|
||||
|
||||
return _firstMouse;
|
||||
return null;
|
||||
}
|
||||
|
||||
public void BustIdentifierCache()
|
||||
{
|
||||
_deviceCache.Clear();
|
||||
_cachedFallbackKeyboard = null;
|
||||
_cachedFallbackMouse = null;
|
||||
|
||||
_devices = _rgbService.EnabledDevices.Where(d => d.InputIdentifiers.Any()).ToList();
|
||||
}
|
||||
|
||||
@ -220,12 +222,7 @@ internal class InputService : IInputService
|
||||
_deviceCache.TryGetValue(new Tuple<InputProvider, object>(provider, identifier), out ArtemisDevice? device);
|
||||
return device;
|
||||
}
|
||||
|
||||
private void SurfaceConfigurationChanged(object? sender, SurfaceConfigurationEventArgs e)
|
||||
{
|
||||
BustIdentifierCache();
|
||||
}
|
||||
|
||||
|
||||
private void InputProviderOnIdentifierReceived(object? sender, InputProviderIdentifierEventArgs e)
|
||||
{
|
||||
// Don't match if there is no device or if the device type differs from the event device type
|
||||
@ -236,16 +233,24 @@ internal class InputService : IInputService
|
||||
if (!(sender is InputProvider inputProvider))
|
||||
return;
|
||||
|
||||
string providerName = inputProvider.GetType().FullName!;
|
||||
|
||||
// Remove existing identification
|
||||
_identifyingDevice.InputIdentifiers.RemoveAll(i => i.InputProvider == providerName);
|
||||
_identifyingDevice.InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(providerName, e.Identifier));
|
||||
_identifyingDevice.InputIdentifiers.RemoveAll(i => i.InputProvider == inputProvider.ProviderName);
|
||||
_identifyingDevice.InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(inputProvider.ProviderName, e.Identifier));
|
||||
|
||||
StopIdentify();
|
||||
OnDeviceIdentified();
|
||||
}
|
||||
|
||||
private void RgbServiceOnDevicesModified(object? sender, DeviceEventArgs args)
|
||||
{
|
||||
_firstKeyboard = _rgbService.Devices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Keyboard);
|
||||
_firstMouse = _rgbService.Devices.FirstOrDefault(d => d.DeviceType == RGBDeviceType.Mouse);
|
||||
_keyboardCount = _rgbService.Devices.Count(d => d.DeviceType == RGBDeviceType.Keyboard);
|
||||
_mouseCount = _rgbService.Devices.Count(d => d.DeviceType == RGBDeviceType.Mouse);
|
||||
|
||||
BustIdentifierCache();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Keyboard
|
||||
@ -379,6 +384,7 @@ internal class InputService : IInputService
|
||||
|
||||
private readonly HashSet<MouseButton> _pressedButtons = new();
|
||||
|
||||
|
||||
private void InputProviderOnMouseButtonDataReceived(object? sender, InputProviderMouseButtonEventArgs e)
|
||||
{
|
||||
bool foundLedId = InputKeyUtilities.MouseButtonLedIdMap.TryGetValue(e.Button, out LedId ledId);
|
||||
|
||||
@ -1,18 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace Artemis.Core.Internal;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a kind of node that cannot be deleted inside a <see cref="INode" />.
|
||||
/// </summary>
|
||||
public interface IDefaultNode : INode
|
||||
{
|
||||
}
|
||||
namespace Artemis.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a kind of node that cannot be deleted inside a <see cref="NodeScript" />.
|
||||
/// </summary>
|
||||
public abstract class DefaultNode : Node, IDefaultNode
|
||||
public abstract class DefaultNode : Node
|
||||
{
|
||||
#region Constructors
|
||||
|
||||
@ -20,8 +13,6 @@ public abstract class DefaultNode : Node, IDefaultNode
|
||||
protected DefaultNode(Guid id, string name, string description = "") : base(name, description)
|
||||
{
|
||||
Id = id;
|
||||
Name = name;
|
||||
Description = description;
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -1,54 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using Artemis.Core.Modules;
|
||||
using Artemis.Core.VisualScripting.Internal;
|
||||
using Humanizer;
|
||||
|
||||
namespace Artemis.Core.Internal;
|
||||
|
||||
internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode
|
||||
internal class EventConditionEventStartNode : DefaultNode
|
||||
{
|
||||
internal static readonly Guid NodeId = new("278735FE-69E9-4A73-A6B8-59E83EE19305");
|
||||
private readonly Dictionary<Func<DataModelEventArgs, object>, OutputPin> _propertyPins;
|
||||
private readonly ObjectOutputPins _objectOutputPins;
|
||||
private IDataModelEvent? _dataModelEvent;
|
||||
|
||||
public EventConditionEventStartNode() : base(NodeId, "Event Arguments", "Contains the event arguments that triggered the evaluation")
|
||||
{
|
||||
_propertyPins = new Dictionary<Func<DataModelEventArgs, object>, OutputPin>();
|
||||
_objectOutputPins = new ObjectOutputPins(this);
|
||||
}
|
||||
|
||||
public void SetDataModelEvent(IDataModelEvent? dataModelEvent)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void CreatePins(IDataModelEvent? dataModelEvent)
|
||||
{
|
||||
if (_dataModelEvent == dataModelEvent)
|
||||
return;
|
||||
|
||||
while (Pins.Any())
|
||||
RemovePin((Pin) Pins.First());
|
||||
_propertyPins.Clear();
|
||||
|
||||
_dataModelEvent = dataModelEvent;
|
||||
if (dataModelEvent == null)
|
||||
return;
|
||||
|
||||
foreach (PropertyInfo propertyInfo in dataModelEvent.ArgumentsType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||
.Where(p => p.CustomAttributes.All(a => a.AttributeType != typeof(DataModelIgnoreAttribute))))
|
||||
{
|
||||
// Expect an IDataModelEvent
|
||||
ParameterExpression eventParameter = Expression.Parameter(typeof(DataModelEventArgs), "event");
|
||||
// Cast it to the actual event type
|
||||
UnaryExpression eventCast = Expression.Convert(eventParameter, propertyInfo.DeclaringType!);
|
||||
// Access the property
|
||||
MemberExpression accessor = Expression.Property(eventCast, propertyInfo);
|
||||
// Cast the property to an object (sadly boxing)
|
||||
UnaryExpression objectCast = Expression.Convert(accessor, typeof(object));
|
||||
// Compile the resulting expression
|
||||
Func<DataModelEventArgs, object> expression = Expression.Lambda<Func<DataModelEventArgs, object>>(objectCast, eventParameter).Compile();
|
||||
|
||||
_propertyPins.Add(expression, CreateOrAddOutputPin(propertyInfo.PropertyType, propertyInfo.Name.Humanize()));
|
||||
}
|
||||
_objectOutputPins.ChangeType(dataModelEvent?.ArgumentsType);
|
||||
}
|
||||
|
||||
public override void Evaluate()
|
||||
@ -56,12 +32,6 @@ internal class EventConditionEventStartNode : DefaultNode, IEventConditionNode
|
||||
if (_dataModelEvent?.LastEventArgumentsUntyped == null)
|
||||
return;
|
||||
|
||||
foreach ((Func<DataModelEventArgs, object> propertyAccessor, OutputPin outputPin) in _propertyPins)
|
||||
{
|
||||
if (!outputPin.ConnectedTo.Any())
|
||||
continue;
|
||||
object value = _dataModelEvent.LastEventArgumentsUntyped != null ? propertyAccessor(_dataModelEvent.LastEventArgumentsUntyped) : outputPin.Type.GetDefault()!;
|
||||
outputPin.Value = outputPin.IsNumeric ? new Numeric(value) : value;
|
||||
}
|
||||
_objectOutputPins.SetCurrentValue(_dataModelEvent.LastEventArgumentsUntyped);
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,8 @@
|
||||
using System;
|
||||
using Artemis.Core.VisualScripting.Internal;
|
||||
|
||||
namespace Artemis.Core.Internal;
|
||||
|
||||
internal class EventConditionValueChangedStartNode : DefaultNode, IEventConditionNode
|
||||
internal class EventConditionValueChangedStartNode : DefaultNode
|
||||
{
|
||||
internal static readonly Guid NodeId = new("F9A270DB-A231-4800-BAB3-DC1F96856756");
|
||||
private object? _newValue;
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
namespace Artemis.Core.VisualScripting.Internal;
|
||||
|
||||
internal interface IEventConditionNode : INode
|
||||
{
|
||||
}
|
||||
@ -132,7 +132,7 @@ public abstract class Node : BreakableModel, INode
|
||||
/// <param name="name">The name of the pin</param>
|
||||
/// <typeparam name="T">The type of value the pin will hold</typeparam>
|
||||
/// <returns>The newly created pin</returns>
|
||||
protected InputPin<T> CreateInputPin<T>(string name = "")
|
||||
public InputPin<T> CreateInputPin<T>(string name = "")
|
||||
{
|
||||
InputPin<T> pin = new(this, name);
|
||||
_pins.Add(pin);
|
||||
@ -146,7 +146,7 @@ public abstract class Node : BreakableModel, INode
|
||||
/// <param name="type">The type of value the pin will hold</param>
|
||||
/// <param name="name">The name of the pin</param>
|
||||
/// <returns>The newly created pin</returns>
|
||||
protected InputPin CreateInputPin(Type type, string name = "")
|
||||
public InputPin CreateInputPin(Type type, string name = "")
|
||||
{
|
||||
InputPin pin = new(this, type, name);
|
||||
_pins.Add(pin);
|
||||
@ -160,7 +160,7 @@ public abstract class Node : BreakableModel, INode
|
||||
/// <param name="name">The name of the pin</param>
|
||||
/// <typeparam name="T">The type of value the pin will hold</typeparam>
|
||||
/// <returns>The newly created pin</returns>
|
||||
protected OutputPin<T> CreateOutputPin<T>(string name = "")
|
||||
public OutputPin<T> CreateOutputPin<T>(string name = "")
|
||||
{
|
||||
OutputPin<T> pin = new(this, name);
|
||||
_pins.Add(pin);
|
||||
@ -174,7 +174,7 @@ public abstract class Node : BreakableModel, INode
|
||||
/// <param name="type">The type of value the pin will hold</param>
|
||||
/// <param name="name">The name of the pin</param>
|
||||
/// <returns>The newly created pin</returns>
|
||||
protected OutputPin CreateOutputPin(Type type, string name = "")
|
||||
public OutputPin CreateOutputPin(Type type, string name = "")
|
||||
{
|
||||
OutputPin pin = new(this, type, name);
|
||||
_pins.Add(pin);
|
||||
@ -187,7 +187,7 @@ public abstract class Node : BreakableModel, INode
|
||||
/// The bucket might grow a bit over time as the user edits the node but pins won't get lost, enabling undo/redo in the
|
||||
/// editor.
|
||||
/// </summary>
|
||||
protected OutputPin CreateOrAddOutputPin(Type valueType, string displayName)
|
||||
public OutputPin CreateOrAddOutputPin(Type valueType, string displayName)
|
||||
{
|
||||
// Grab the first pin from the bucket that isn't on the node yet
|
||||
OutputPin? pin = _outputPinBucket.FirstOrDefault(p => !Pins.Contains(p));
|
||||
@ -217,7 +217,7 @@ public abstract class Node : BreakableModel, INode
|
||||
/// The bucket might grow a bit over time as the user edits the node but pins won't get lost, enabling undo/redo in the
|
||||
/// editor.
|
||||
/// </summary>
|
||||
protected InputPin CreateOrAddInputPin(Type valueType, string displayName)
|
||||
public InputPin CreateOrAddInputPin(Type valueType, string displayName)
|
||||
{
|
||||
// Grab the first pin from the bucket that isn't on the node yet
|
||||
InputPin? pin = _inputPinBucket.FirstOrDefault(p => !Pins.Contains(p));
|
||||
@ -247,7 +247,7 @@ public abstract class Node : BreakableModel, INode
|
||||
/// </summary>
|
||||
/// <param name="pin">The pin to remove</param>
|
||||
/// <returns><see langword="true" /> if the pin was removed; otherwise <see langword="false" />.</returns>
|
||||
protected bool RemovePin(Pin pin)
|
||||
public bool RemovePin(Pin pin)
|
||||
{
|
||||
bool isRemoved = _pins.Remove(pin);
|
||||
if (isRemoved)
|
||||
@ -263,7 +263,7 @@ public abstract class Node : BreakableModel, INode
|
||||
/// Adds an existing <paramref name="pin" /> to the <see cref="Pins" /> collection.
|
||||
/// </summary>
|
||||
/// <param name="pin">The pin to add</param>
|
||||
protected void AddPin(Pin pin)
|
||||
public void AddPin(Pin pin)
|
||||
{
|
||||
if (pin.Node != this)
|
||||
throw new ArtemisCoreException("Can't add a pin to a node that belongs to a different node than the one it's being added to.");
|
||||
@ -281,7 +281,7 @@ public abstract class Node : BreakableModel, INode
|
||||
/// <param name="name">The name of the pin collection</param>
|
||||
/// <param name="initialCount">The amount of pins to initially add to the collection</param>
|
||||
/// <returns>The resulting input pin collection</returns>
|
||||
protected InputPinCollection<T> CreateInputPinCollection<T>(string name = "", int initialCount = 1)
|
||||
public InputPinCollection<T> CreateInputPinCollection<T>(string name = "", int initialCount = 1)
|
||||
{
|
||||
InputPinCollection<T> pin = new(this, name, initialCount);
|
||||
_pinCollections.Add(pin);
|
||||
@ -296,7 +296,7 @@ public abstract class Node : BreakableModel, INode
|
||||
/// <param name="name">The name of the pin collection</param>
|
||||
/// <param name="initialCount">The amount of pins to initially add to the collection</param>
|
||||
/// <returns>The resulting input pin collection</returns>
|
||||
protected InputPinCollection CreateInputPinCollection(Type type, string name = "", int initialCount = 1)
|
||||
public InputPinCollection CreateInputPinCollection(Type type, string name = "", int initialCount = 1)
|
||||
{
|
||||
InputPinCollection pin = new(this, type, name, initialCount);
|
||||
_pinCollections.Add(pin);
|
||||
@ -311,7 +311,7 @@ public abstract class Node : BreakableModel, INode
|
||||
/// <param name="name">The name of the pin collection</param>
|
||||
/// <param name="initialCount">The amount of pins to initially add to the collection</param>
|
||||
/// <returns>The resulting output pin collection</returns>
|
||||
protected OutputPinCollection<T> CreateOutputPinCollection<T>(string name = "", int initialCount = 1)
|
||||
public OutputPinCollection<T> CreateOutputPinCollection<T>(string name = "", int initialCount = 1)
|
||||
{
|
||||
OutputPinCollection<T> pin = new(this, name, initialCount);
|
||||
_pinCollections.Add(pin);
|
||||
@ -325,7 +325,7 @@ public abstract class Node : BreakableModel, INode
|
||||
/// </summary>
|
||||
/// <param name="pinCollection">The pin collection to remove</param>
|
||||
/// <returns><see langword="true" /> if the pin collection was removed; otherwise <see langword="false" />.</returns>
|
||||
protected bool RemovePinCollection(PinCollection pinCollection)
|
||||
public bool RemovePinCollection(PinCollection pinCollection)
|
||||
{
|
||||
bool isRemoved = _pinCollections.Remove(pinCollection);
|
||||
if (isRemoved)
|
||||
@ -339,7 +339,7 @@ public abstract class Node : BreakableModel, INode
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the node was loaded from storage or newly created
|
||||
/// Called when the node was loaded from storage or newly created, at this point pin connections aren't reestablished yet.
|
||||
/// </summary>
|
||||
/// <param name="script">The script the node is contained in</param>
|
||||
public virtual void Initialize(INodeScript script)
|
||||
|
||||
@ -35,7 +35,10 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript
|
||||
|
||||
#region Properties & Fields
|
||||
|
||||
internal NodeScriptEntity Entity { get; private set; }
|
||||
/// <summary>
|
||||
/// Gets the entity used to store this script.
|
||||
/// </summary>
|
||||
public NodeScriptEntity Entity { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name { get; }
|
||||
@ -77,7 +80,8 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript
|
||||
/// The context of the node script, usually a <see cref="Profile" /> or
|
||||
/// <see cref="ProfileConfiguration" />
|
||||
/// </param>
|
||||
protected NodeScript(string name, string description, object? context = null)
|
||||
/// <param name="defaultNodes">A list of default nodes to add to the node script.</param>
|
||||
protected NodeScript(string name, string description, object? context = null, List<DefaultNode>? defaultNodes = null)
|
||||
{
|
||||
Name = name;
|
||||
Description = description;
|
||||
@ -87,9 +91,15 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript
|
||||
|
||||
NodeTypeStore.NodeTypeAdded += NodeTypeStoreOnNodeTypeAdded;
|
||||
NodeTypeStore.NodeTypeRemoved += NodeTypeStoreOnNodeTypeRemoved;
|
||||
|
||||
if (defaultNodes != null)
|
||||
{
|
||||
foreach (DefaultNode defaultNode in defaultNodes)
|
||||
AddNode(defaultNode);
|
||||
}
|
||||
}
|
||||
|
||||
internal NodeScript(string name, string description, NodeScriptEntity entity, object? context = null)
|
||||
internal NodeScript(string name, string description, NodeScriptEntity entity, object? context = null, List<DefaultNode>? defaultNodes = null)
|
||||
{
|
||||
Name = name;
|
||||
Description = description;
|
||||
@ -99,6 +109,12 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript
|
||||
|
||||
NodeTypeStore.NodeTypeAdded += NodeTypeStoreOnNodeTypeAdded;
|
||||
NodeTypeStore.NodeTypeRemoved += NodeTypeStoreOnNodeTypeRemoved;
|
||||
|
||||
if (defaultNodes != null)
|
||||
{
|
||||
foreach (DefaultNode defaultNode in defaultNodes)
|
||||
AddNode(defaultNode);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -410,8 +426,9 @@ public class NodeScript<T> : NodeScript, INodeScript<T>
|
||||
|
||||
#region Constructors
|
||||
|
||||
internal NodeScript(string name, string description, NodeScriptEntity entity, object? context = null)
|
||||
: base(name, description, entity, context)
|
||||
/// <inheritdoc />
|
||||
public NodeScript(string name, string description, NodeScriptEntity entity, object? context = null, List<DefaultNode>? defaultNodes = null)
|
||||
: base(name, description, entity, context, defaultNodes)
|
||||
{
|
||||
ExitNode = new ExitNode<T>(name, description);
|
||||
AddNode(ExitNode);
|
||||
@ -420,8 +437,8 @@ public class NodeScript<T> : NodeScript, INodeScript<T>
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NodeScript(string name, string description, object? context = null)
|
||||
: base(name, description, context)
|
||||
public NodeScript(string name, string description, object? context = null, List<DefaultNode>? defaultNodes = null)
|
||||
: base(name, description, context, defaultNodes)
|
||||
{
|
||||
ExitNode = new ExitNode<T>(name, description);
|
||||
AddNode(ExitNode);
|
||||
|
||||
142
src/Artemis.Core/VisualScripting/ObjectOutputPins.cs
Normal file
142
src/Artemis.Core/VisualScripting/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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
@ -49,7 +49,7 @@ public class UpdateEventConditionPath : IProfileEditorCommand, IDisposable
|
||||
|
||||
// Change the end node
|
||||
_eventCondition.EventPath = _value;
|
||||
_eventCondition.UpdateEventNode();
|
||||
_eventCondition.UpdateEventNode(true);
|
||||
|
||||
_executed = true;
|
||||
}
|
||||
@ -59,7 +59,7 @@ public class UpdateEventConditionPath : IProfileEditorCommand, IDisposable
|
||||
{
|
||||
// Change the end node
|
||||
_eventCondition.EventPath = _oldValue;
|
||||
_eventCondition.UpdateEventNode();
|
||||
_eventCondition.UpdateEventNode(true);
|
||||
|
||||
// Restore old connections
|
||||
_store?.Restore();
|
||||
|
||||
@ -103,4 +103,11 @@
|
||||
<Setter Property="Padding" Value="6 3 11 3" />
|
||||
<Setter Property="Height" Value="24" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.condensed">
|
||||
<Setter Property="Padding" Value="1" />
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="MinHeight" Value="24" />
|
||||
</Style>
|
||||
|
||||
</Styles>
|
||||
@ -19,7 +19,6 @@ public class WindowsInputProvider : InputProvider
|
||||
private readonly ILogger _logger;
|
||||
private readonly SpongeWindow _sponge;
|
||||
private readonly Timer _taskManagerTimer;
|
||||
private DateTime _lastMouseUpdate;
|
||||
private int _lastProcessId;
|
||||
|
||||
public WindowsInputProvider(ILogger logger, IInputService inputService)
|
||||
@ -160,8 +159,8 @@ public class WindowsInputProvider : InputProvider
|
||||
|
||||
#region Mouse
|
||||
|
||||
private int _mouseDeltaX;
|
||||
private int _mouseDeltaY;
|
||||
private int _previousMouseX;
|
||||
private int _previousMouseY;
|
||||
|
||||
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 :')
|
||||
if (mouseData.Mouse.Buttons == RawMouseButtonFlags.None)
|
||||
{
|
||||
_mouseDeltaX += mouseData.Mouse.LastX;
|
||||
_mouseDeltaY += mouseData.Mouse.LastY;
|
||||
if (DateTime.Now - _lastMouseUpdate < TimeSpan.FromMilliseconds(40))
|
||||
return;
|
||||
_previousMouseX += mouseData.Mouse.LastX;
|
||||
_previousMouseY += mouseData.Mouse.LastY;
|
||||
}
|
||||
|
||||
ArtemisDevice? device = null;
|
||||
@ -193,10 +190,7 @@ public class WindowsInputProvider : InputProvider
|
||||
if (mouseData.Mouse.Buttons == RawMouseButtonFlags.None)
|
||||
{
|
||||
Win32Point cursorPosition = GetCursorPosition();
|
||||
OnMouseMoveDataReceived(device, cursorPosition.X, cursorPosition.Y, _mouseDeltaX, _mouseDeltaY);
|
||||
_mouseDeltaX = 0;
|
||||
_mouseDeltaY = 0;
|
||||
_lastMouseUpdate = DateTime.Now;
|
||||
OnMouseMoveDataReceived(device, cursorPosition.X, cursorPosition.Y, cursorPosition.X - _previousMouseX, cursorPosition.Y - _previousMouseY);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.Storage.Entities.Profile;
|
||||
using Artemis.UI.Models;
|
||||
using Avalonia;
|
||||
using Avalonia.Input;
|
||||
|
||||
@ -21,7 +22,7 @@ public static class ProfileElementExtensions
|
||||
return;
|
||||
|
||||
DataObject dataObject = new();
|
||||
string copy = CoreJson.SerializeObject(folder.FolderEntity, true);
|
||||
string copy = CoreJson.SerializeObject(new FolderClipboardModel(folder), true);
|
||||
dataObject.Set(ClipboardDataFormat, copy);
|
||||
await Application.Current.Clipboard.SetDataObjectAsync(dataObject);
|
||||
}
|
||||
@ -50,9 +51,8 @@ public static class ProfileElementExtensions
|
||||
object? entity = CoreJson.DeserializeObject(Encoding.Unicode.GetString(bytes), true);
|
||||
switch (entity)
|
||||
{
|
||||
case FolderEntity folderEntity:
|
||||
folderEntity.Id = Guid.NewGuid();
|
||||
return new Folder(parent.Profile, parent, folderEntity);
|
||||
case FolderClipboardModel folderClipboardModel:
|
||||
return folderClipboardModel.Paste(parent.Profile, parent);
|
||||
case LayerEntity layerEntity:
|
||||
layerEntity.Id = Guid.NewGuid();
|
||||
return new Layer(parent.Profile, parent, layerEntity);
|
||||
|
||||
@ -7,7 +7,8 @@
|
||||
x:Class="Artemis.UI.MainWindow"
|
||||
Icon="/Assets/Images/Logo/application.ico"
|
||||
Title="Artemis 2.0"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
MinWidth="600"
|
||||
MinHeight="400">
|
||||
<Panel Name="RootPanel">
|
||||
<Border Name="DragHandle" Background="Transparent" Height="40" HorizontalAlignment="Stretch" VerticalAlignment="Top"/>
|
||||
<DockPanel>
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
using System;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.UI.Models;
|
||||
using Artemis.UI.Screens.Root;
|
||||
using Artemis.UI.Shared;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Threading;
|
||||
using FluentAvalonia.Core.ApplicationModel;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI;
|
||||
|
||||
@ -12,13 +17,17 @@ public class MainWindow : ReactiveCoreWindow<RootViewModel>
|
||||
{
|
||||
private readonly Panel _rootPanel;
|
||||
private readonly ContentControl _sidebarContentControl;
|
||||
private bool _activated;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
Opened += OnOpened;
|
||||
Activated += OnActivated;
|
||||
Deactivated += OnDeactivated;
|
||||
|
||||
ApplyWindowSize();
|
||||
InitializeComponent();
|
||||
|
||||
_rootPanel = this.Get<Panel>("RootPanel");
|
||||
_sidebarContentControl = this.Get<ContentControl>("SidebarContentControl");
|
||||
_rootPanel.LayoutUpdated += OnLayoutUpdated;
|
||||
@ -26,6 +35,27 @@ public class MainWindow : ReactiveCoreWindow<RootViewModel>
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
|
||||
Observable.FromEventPattern<PixelPointEventArgs>(x => PositionChanged += x, x => PositionChanged -= x)
|
||||
.Select(_ => Unit.Default)
|
||||
.Merge(this.WhenAnyValue(vm => vm.WindowState, vm => vm.Width, vm => vm.Width, vm => vm.Height).Select(_ => Unit.Default))
|
||||
.Throttle(TimeSpan.FromMilliseconds(200), AvaloniaScheduler.Instance)
|
||||
.Subscribe(_ => SaveWindowSize());
|
||||
}
|
||||
|
||||
private void ApplyWindowSize()
|
||||
{
|
||||
_activated = true;
|
||||
RootViewModel.WindowSizeSetting?.Value?.ApplyToWindow(this);
|
||||
}
|
||||
|
||||
private void SaveWindowSize()
|
||||
{
|
||||
if (RootViewModel.WindowSizeSetting == null || !_activated)
|
||||
return;
|
||||
|
||||
RootViewModel.WindowSizeSetting.Value ??= new WindowSize();
|
||||
RootViewModel.WindowSizeSetting.Value.ApplyFromWindow(this);
|
||||
}
|
||||
|
||||
// TODO: Replace with a media query once https://github.com/AvaloniaUI/Avalonia/pull/7938 is implemented
|
||||
|
||||
79
src/Artemis.UI/Models/FolderClipboardModel.cs
Normal file
79
src/Artemis.UI/Models/FolderClipboardModel.cs
Normal file
@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Artemis.Core;
|
||||
using Artemis.Storage.Entities.Profile;
|
||||
using Artemis.Storage.Entities.Profile.Abstract;
|
||||
using Artemis.UI.Exceptions;
|
||||
|
||||
namespace Artemis.UI.Models;
|
||||
|
||||
public class FolderClipboardModel
|
||||
{
|
||||
public FolderClipboardModel(Folder folder)
|
||||
{
|
||||
FolderEntity = folder.FolderEntity;
|
||||
Folders = new List<FolderEntity>();
|
||||
Layers = new List<LayerEntity>();
|
||||
foreach (Folder allFolder in folder.GetAllFolders())
|
||||
Folders.Add(allFolder.FolderEntity);
|
||||
foreach (Layer allLayer in folder.GetAllLayers())
|
||||
Layers.Add(allLayer.LayerEntity);
|
||||
}
|
||||
|
||||
// ReSharper disable once UnusedMember.Global - For JSON.NET
|
||||
public FolderClipboardModel()
|
||||
{
|
||||
FolderEntity = null;
|
||||
Folders = new List<FolderEntity>();
|
||||
Layers = new List<LayerEntity>();
|
||||
}
|
||||
|
||||
public FolderEntity? FolderEntity { get; set; }
|
||||
public List<FolderEntity> Folders { get; set; }
|
||||
public List<LayerEntity> Layers { get; set; }
|
||||
public bool HasBeenPasted { get; set; }
|
||||
|
||||
public Folder Paste(Profile profile, ProfileElement parent)
|
||||
{
|
||||
if (FolderEntity == null)
|
||||
throw new ArtemisUIException("Couldn't paste folder because FolderEntity deserialized as null");
|
||||
if (HasBeenPasted)
|
||||
throw new ArtemisUIException("Clipboard model can only be pasted once");
|
||||
|
||||
HasBeenPasted = true;
|
||||
|
||||
// Generate new GUIDs
|
||||
ReplaceGuid(FolderEntity);
|
||||
foreach (FolderEntity folderEntity in Folders)
|
||||
ReplaceGuid(folderEntity);
|
||||
foreach (LayerEntity layerEntity in Layers)
|
||||
ReplaceGuid(layerEntity);
|
||||
|
||||
// Inject the pasted elements into the profile
|
||||
profile.ProfileEntity.Folders.AddRange(Folders);
|
||||
profile.ProfileEntity.Layers.AddRange(Layers);
|
||||
|
||||
// Let the folder initialize and load as usual
|
||||
FolderEntity.Name += " - copy";
|
||||
Folder folder = new(profile, parent, FolderEntity);
|
||||
return folder;
|
||||
}
|
||||
|
||||
private void ReplaceGuid(RenderElementEntity parent)
|
||||
{
|
||||
Guid old = parent.Id;
|
||||
parent.Id = Guid.NewGuid();
|
||||
|
||||
foreach (FolderEntity child in Folders)
|
||||
{
|
||||
if (child.ParentId == old)
|
||||
child.ParentId = parent.Id;
|
||||
}
|
||||
|
||||
foreach (LayerEntity child in Layers)
|
||||
{
|
||||
if (child.ParentId == old)
|
||||
child.ParentId = parent.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
72
src/Artemis.UI/Models/WindowSize.cs
Normal file
72
src/Artemis.UI/Models/WindowSize.cs
Normal file
@ -0,0 +1,72 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace Artemis.UI.Models;
|
||||
|
||||
public class WindowSize
|
||||
{
|
||||
private bool _applying;
|
||||
public int Top { get; set; }
|
||||
public int Left { get; set; }
|
||||
public double Width { get; set; }
|
||||
public double Height { get; set; }
|
||||
public int MaximizedTop { get; set; }
|
||||
public int MaximizedLeft { get; set; }
|
||||
public double MaximizedWidth { get; set; }
|
||||
public double MaximizedHeight { get; set; }
|
||||
public bool IsMaximized { get; set; }
|
||||
|
||||
public void ApplyFromWindow(Window window)
|
||||
{
|
||||
if (_applying)
|
||||
return;
|
||||
|
||||
if (double.IsNaN(window.Width) || double.IsNaN(window.Height))
|
||||
return;
|
||||
|
||||
IsMaximized = window.WindowState == WindowState.Maximized;
|
||||
if (IsMaximized)
|
||||
{
|
||||
MaximizedTop = window.Position.Y;
|
||||
MaximizedLeft = window.Position.X;
|
||||
MaximizedHeight = window.Height;
|
||||
MaximizedWidth = window.Width;
|
||||
}
|
||||
else
|
||||
{
|
||||
Top = window.Position.Y;
|
||||
Left = window.Position.X;
|
||||
Height = window.Height;
|
||||
Width = window.Width;
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyToWindow(Window window)
|
||||
{
|
||||
if (_applying)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
// The -8 seems to be a FluentAvalonia thing?
|
||||
_applying = true;
|
||||
if (IsMaximized)
|
||||
{
|
||||
window.Position = new PixelPoint(Math.Max(-8, MaximizedLeft), Math.Max(-8, MaximizedTop));
|
||||
window.WindowState = WindowState.Maximized;
|
||||
}
|
||||
else
|
||||
{
|
||||
window.Position = new PixelPoint(Math.Max(-8, Left), Math.Max(-8, Top));
|
||||
window.Height = Height;
|
||||
window.Width = Width;
|
||||
window.WindowState = WindowState.Normal;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_applying = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,8 +2,10 @@
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Ninject.InstanceProviders;
|
||||
using Artemis.UI.Screens;
|
||||
using Artemis.UI.Screens.VisualScripting;
|
||||
using Artemis.UI.Services.Interfaces;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.NodeEditor;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Shared.PlatformSupport;
|
||||
@ -57,6 +59,7 @@ public class UIModule : NinjectModule
|
||||
.BindToFactory();
|
||||
});
|
||||
|
||||
Kernel.Bind<NodeScriptWindowViewModelBase>().To<NodeScriptWindowViewModel>();
|
||||
Kernel.Bind<IPropertyVmFactory>().ToFactory(() => new LayerPropertyViewModelInstanceProvider());
|
||||
|
||||
// Bind all UI services as singletons
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
<Grid Margin="8" RowDefinitions="*,*">
|
||||
<controls:HyperlinkButton Grid.Row="0" NavigateUri="https://github.com/Artemis-RGB/Artemis">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<avalonia:MaterialIcon Kind="Gift" />
|
||||
<avalonia:MaterialIcon Kind="Github" />
|
||||
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">GitHub</TextBlock>
|
||||
</StackPanel>
|
||||
</controls:HyperlinkButton>
|
||||
|
||||
@ -16,7 +16,6 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes;
|
||||
public class EventConditionViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private readonly EventCondition _eventCondition;
|
||||
private readonly INodeService _nodeService;
|
||||
private readonly IProfileEditorService _profileEditorService;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ObservableAsPropertyHelper<bool> _showOverlapOptions;
|
||||
@ -27,11 +26,10 @@ public class EventConditionViewModel : ActivatableViewModelBase
|
||||
private ObservableAsPropertyHelper<int>? _selectedToggleOffMode;
|
||||
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;
|
||||
_profileEditorService = profileEditorService;
|
||||
_nodeService = nodeService;
|
||||
_windowService = windowService;
|
||||
_settingsService = settingsService;
|
||||
_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);
|
||||
});
|
||||
|
||||
OpenEditor = ReactiveCommand.CreateFromTask(ExecuteOpenEditor);
|
||||
OpenEditor = ReactiveCommand.CreateFromTask(ExecuteOpenEditor, this.WhenAnyValue(vm => vm.EventPath).Select(p => p != null));
|
||||
}
|
||||
|
||||
public ReactiveCommand<Unit, Unit> OpenEditor { get; }
|
||||
|
||||
@ -76,18 +76,23 @@ public class PropertiesViewModel : ActivatableViewModelBase
|
||||
});
|
||||
|
||||
// Subscribe to events of the latest selected profile element - borrowed from https://stackoverflow.com/a/63950940
|
||||
this.WhenAnyValue(vm => vm.ProfileElement)
|
||||
.Select(p => p is Layer l
|
||||
? Observable.FromEventPattern(x => l.LayerBrushUpdated += x, x => l.LayerBrushUpdated -= x)
|
||||
: Observable.Never<EventPattern<object>>())
|
||||
.Switch()
|
||||
.Subscribe(_ => UpdatePropertyGroups());
|
||||
this.WhenAnyValue(vm => vm.ProfileElement)
|
||||
.Select(p => p != null
|
||||
? Observable.FromEventPattern(x => p.LayerEffectsUpdated += x, x => p.LayerEffectsUpdated -= x)
|
||||
: Observable.Never<EventPattern<object>>())
|
||||
.Switch()
|
||||
.Subscribe(_ => UpdatePropertyGroups());
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
this.WhenAnyValue(vm => vm.ProfileElement)
|
||||
.Select(p => p is Layer l
|
||||
? Observable.FromEventPattern(x => l.LayerBrushUpdated += x, x => l.LayerBrushUpdated -= x)
|
||||
: Observable.Never<EventPattern<object>>())
|
||||
.Switch()
|
||||
.Subscribe(_ => UpdatePropertyGroups())
|
||||
.DisposeWith(d);
|
||||
this.WhenAnyValue(vm => vm.ProfileElement)
|
||||
.Select(p => p != null
|
||||
? 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.LayerProperty).Subscribe(_ => UpdateTimelineViewModel());
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Models;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Screens.Sidebar;
|
||||
using Artemis.UI.Services.Interfaces;
|
||||
@ -44,7 +45,8 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
|
||||
ISidebarVmFactory sidebarVmFactory)
|
||||
{
|
||||
Router = new RoutingState();
|
||||
|
||||
WindowSizeSetting = settingsService.GetSetting<WindowSize?>("WindowSize");
|
||||
|
||||
_coreService = coreService;
|
||||
_settingsService = settingsService;
|
||||
_windowService = windowService;
|
||||
@ -54,7 +56,7 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
|
||||
_defaultTitleBarViewModel = defaultTitleBarViewModel;
|
||||
_sidebarVmFactory = sidebarVmFactory;
|
||||
_lifeTime = (IClassicDesktopStyleApplicationLifetime) Application.Current!.ApplicationLifetime!;
|
||||
|
||||
|
||||
mainWindowService.ConfigureMainWindowProvider(this);
|
||||
|
||||
DisplayAccordingToSettings();
|
||||
@ -90,6 +92,7 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
|
||||
|
||||
private void CurrentMainWindowOnClosing(object? sender, EventArgs e)
|
||||
{
|
||||
WindowSizeSetting.Save();
|
||||
_lifeTime.MainWindow = null;
|
||||
SidebarViewModel = null;
|
||||
Router.NavigateAndReset.Execute(new EmptyViewModel(this, "blank")).Subscribe();
|
||||
@ -121,6 +124,8 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
|
||||
/// <inheritdoc />
|
||||
public RoutingState Router { get; }
|
||||
|
||||
public static PluginSetting<WindowSize?>? WindowSizeSetting { get; private set; }
|
||||
|
||||
#region Tray commands
|
||||
|
||||
public void OpenScreen(string displayName)
|
||||
@ -170,7 +175,6 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
|
||||
_lifeTime.MainWindow.Closing += CurrentMainWindowOnClosing;
|
||||
}
|
||||
|
||||
_lifeTime.MainWindow.WindowState = WindowState.Normal;
|
||||
_lifeTime.MainWindow.Activate();
|
||||
OnMainWindowOpened();
|
||||
}
|
||||
@ -226,6 +230,11 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public void SaveWindowBounds(int x, int y, int width, int height)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
internal class EmptyViewModel : MainScreenViewModel
|
||||
|
||||
@ -2,12 +2,14 @@
|
||||
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:sidebar="clr-namespace:Artemis.UI.Screens.Sidebar"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Sidebar.SidebarCategoryEditView">
|
||||
x:Class="Artemis.UI.Screens.Sidebar.SidebarCategoryEditView"
|
||||
x:DataType="sidebar:SidebarCategoryEditViewModel">
|
||||
<StackPanel>
|
||||
<StackPanel.KeyBindings>
|
||||
<KeyBinding Gesture="Enter" Command="{Binding Confirm}" />
|
||||
<KeyBinding Gesture="Enter" Command="{CompiledBinding Confirm}" />
|
||||
</StackPanel.KeyBindings>
|
||||
<TextBox Text="{Binding CategoryName}" Watermark="Category name"/>
|
||||
<TextBox Text="{CompiledBinding CategoryName}" Watermark="Category name"/>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@ -23,13 +23,9 @@ public class SidebarCategoryEditViewModel : ContentDialogViewModelBase
|
||||
_categoryName = _category.Name;
|
||||
|
||||
Confirm = ReactiveCommand.Create(ExecuteConfirm, ValidationContext.Valid);
|
||||
Delete = ReactiveCommand.Create(ExecuteDelete);
|
||||
|
||||
this.ValidationRule(vm => vm.CategoryName, categoryName => !string.IsNullOrWhiteSpace(categoryName), "You must specify a valid name");
|
||||
}
|
||||
|
||||
public ReactiveCommand<Unit, Unit> Delete { get; set; }
|
||||
|
||||
public string? CategoryName
|
||||
{
|
||||
get => _categoryName;
|
||||
@ -52,10 +48,4 @@ public class SidebarCategoryEditViewModel : ContentDialogViewModelBase
|
||||
|
||||
ContentDialog?.Hide(ContentDialogResult.Primary);
|
||||
}
|
||||
|
||||
private void ExecuteDelete()
|
||||
{
|
||||
if (_category != null)
|
||||
_profileService.DeleteProfileCategory(_category);
|
||||
}
|
||||
}
|
||||
@ -76,18 +76,20 @@
|
||||
|
||||
<StackPanel Orientation="Horizontal"
|
||||
IsVisible="{CompiledBinding IconType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static core:ProfileConfigurationIconType.BitmapImage}}">
|
||||
|
||||
<Border IsVisible="{CompiledBinding ProfileConfiguration.Icon.Fill}" Background="{DynamicResource CheckerboardBrush}" CornerRadius="{DynamicResource CardCornerRadius}" Width="78" Height="78">
|
||||
|
||||
<Border IsVisible="{CompiledBinding ProfileConfiguration.Icon.Fill}" Background="{DynamicResource CheckerboardBrush}" CornerRadius="{DynamicResource CardCornerRadius}"
|
||||
Width="78" Height="78">
|
||||
<Border Name="FillPreview" Background="{DynamicResource TextFillColorPrimary}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Margin="10">
|
||||
<Border.OpacityMask>
|
||||
<ImageBrush Source="{CompiledBinding SelectedBitmapSource}" BitmapInterpolationMode="HighQuality" />
|
||||
</Border.OpacityMask>
|
||||
</Border>
|
||||
</Border>
|
||||
<Border IsVisible="{CompiledBinding !ProfileConfiguration.Icon.Fill}" Background="{DynamicResource CheckerboardBrush}" CornerRadius="{DynamicResource CardCornerRadius}" Width="78" Height="78">
|
||||
<Border IsVisible="{CompiledBinding !ProfileConfiguration.Icon.Fill}" Background="{DynamicResource CheckerboardBrush}" CornerRadius="{DynamicResource CardCornerRadius}"
|
||||
Width="78" Height="78">
|
||||
<Image Source="{CompiledBinding SelectedBitmapSource}" Margin="10" />
|
||||
</Border>
|
||||
|
||||
|
||||
<Button Command="{CompiledBinding BrowseBitmapFile}"
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="10 0"
|
||||
@ -96,7 +98,7 @@
|
||||
</Button>
|
||||
<CheckBox VerticalAlignment="Bottom" IsChecked="{CompiledBinding ProfileConfiguration.Icon.Fill}">Fill</CheckBox>
|
||||
</StackPanel>
|
||||
|
||||
|
||||
<StackPanel Orientation="Horizontal"
|
||||
IsVisible="{CompiledBinding IconType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static core:ProfileConfigurationIconType.MaterialIcon}}">
|
||||
<Border Background="{DynamicResource CheckerboardBrush}" CornerRadius="{DynamicResource CardCornerRadius}" Width="78" Height="78">
|
||||
@ -169,8 +171,8 @@
|
||||
<Border CornerRadius="5" ClipToBounds="True">
|
||||
<ContentControl Content="{CompiledBinding VisualEditorViewModel}" Height="150" />
|
||||
</Border>
|
||||
<Border Background="Black" Opacity="0.5" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" CornerRadius="5"/>
|
||||
|
||||
<Border Background="Black" Opacity="0.5" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" CornerRadius="5" />
|
||||
|
||||
<Button DockPanel.Dock="Bottom"
|
||||
ToolTip.Tip="Open editor"
|
||||
HorizontalAlignment="Center"
|
||||
@ -185,12 +187,11 @@
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<Grid Grid.Row="1" ColumnDefinitions="Auto,Auto,Auto" HorizontalAlignment="Right">
|
||||
<Button Grid.Column="0" Classes="accent" Command="{CompiledBinding Confirm}">Confirm</Button>
|
||||
<Button Grid.Column="1" Margin="5" Command="{CompiledBinding Import}" IsVisible="{CompiledBinding IsNew}">Import</Button>
|
||||
<Button Grid.Column="1" Margin="5" Command="{CompiledBinding Delete}" IsVisible="{CompiledBinding !IsNew}">Delete</Button>
|
||||
<Button Grid.Column="2" Command="{CompiledBinding Cancel}">Cancel</Button>
|
||||
</Grid>
|
||||
<StackPanel Grid.Row="1" Spacing="5" HorizontalAlignment="Right" Orientation="Horizontal">
|
||||
<Button Command="{CompiledBinding Confirm}" Classes="accent">Confirm</Button>
|
||||
<Button Command="{CompiledBinding Delete}" IsVisible="{CompiledBinding !IsNew}">Delete</Button>
|
||||
<Button Command="{CompiledBinding Cancel}">Cancel</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
</controls1:CoreWindow>
|
||||
@ -63,7 +63,7 @@ public class ProfileConfigurationEditViewModel : DialogViewModelBase<ProfileConf
|
||||
_disableHotkey = new Hotkey {Key = _profileConfiguration.DisableHotkey.Key, Modifiers = _profileConfiguration.DisableHotkey.Modifiers};
|
||||
|
||||
IsNew = profileConfiguration == null;
|
||||
DisplayName = IsNew ? "Artemis | Add profile" : "Artemis | Edit profile";
|
||||
DisplayName = IsNew ? "Artemis | Add profile" : "Artemis | Edit profile properties";
|
||||
Modules = new ObservableCollection<ProfileModuleViewModel?>(
|
||||
pluginManagementService.GetFeaturesOfType<Module>().Where(m => !m.IsAlwaysAvailable).Select(m => new ProfileModuleViewModel(m))
|
||||
);
|
||||
@ -75,7 +75,6 @@ public class ProfileConfigurationEditViewModel : DialogViewModelBase<ProfileConf
|
||||
BrowseBitmapFile = ReactiveCommand.CreateFromTask(ExecuteBrowseBitmapFile);
|
||||
OpenConditionEditor = ReactiveCommand.CreateFromTask(ExecuteOpenConditionEditor);
|
||||
Confirm = ReactiveCommand.CreateFromTask(ExecuteConfirm);
|
||||
Import = ReactiveCommand.CreateFromTask(ExecuteImport);
|
||||
Delete = ReactiveCommand.CreateFromTask(ExecuteDelete);
|
||||
Cancel = ReactiveCommand.Create(ExecuteCancel);
|
||||
|
||||
@ -135,51 +134,7 @@ public class ProfileConfigurationEditViewModel : DialogViewModelBase<ProfileConf
|
||||
public ReactiveCommand<Unit, Unit> Import { get; }
|
||||
public ReactiveCommand<Unit, Unit> Delete { get; }
|
||||
public ReactiveCommand<Unit, Unit> Cancel { get; }
|
||||
|
||||
private async Task ExecuteImport()
|
||||
{
|
||||
if (!IsNew)
|
||||
return;
|
||||
|
||||
string[]? result = await _windowService.CreateOpenFileDialog()
|
||||
.HavingFilter(f => f.WithExtension("json").WithName("Artemis profile"))
|
||||
.ShowAsync();
|
||||
|
||||
if (result == null)
|
||||
return;
|
||||
|
||||
string json = await File.ReadAllTextAsync(result[0]);
|
||||
ProfileConfigurationExportModel? profileConfigurationExportModel = null;
|
||||
try
|
||||
{
|
||||
profileConfigurationExportModel = JsonConvert.DeserializeObject<ProfileConfigurationExportModel>(json, IProfileService.ExportSettings);
|
||||
}
|
||||
catch (JsonException e)
|
||||
{
|
||||
_windowService.ShowExceptionDialog("Import profile failed", e);
|
||||
}
|
||||
|
||||
if (profileConfigurationExportModel == null)
|
||||
{
|
||||
await _windowService.ShowConfirmContentDialog("Import profile", "Failed to import this profile, make sure it is a valid Artemis profile.", "Confirm", null);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ProfileConfiguration profileConfiguration = _profileService.ImportProfile(_profileCategory, profileConfigurationExportModel);
|
||||
|
||||
// Remove the temporary profile configuration
|
||||
_profileService.RemoveProfileConfiguration(_profileConfiguration);
|
||||
|
||||
Close(profileConfiguration);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_windowService.ShowExceptionDialog("Import profile failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async Task ExecuteDelete()
|
||||
{
|
||||
if (IsNew)
|
||||
|
||||
@ -14,15 +14,61 @@
|
||||
x:DataType="local:SidebarCategoryViewModel">
|
||||
<UserControl.Resources>
|
||||
<converters:ColorOpacityConverter x:Key="ColorOpacityConverter" />
|
||||
<MenuFlyout x:Key="CategoryMenuFlyout" Placement="Bottom">
|
||||
<MenuItem Header="Suspend" Command="{CompiledBinding ToggleSuspended}">
|
||||
<MenuItem.Icon>
|
||||
<avalonia:MaterialIcon Kind="Check" IsVisible="{CompiledBinding IsSuspended}" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Move up" Command="{CompiledBinding MoveUp}">
|
||||
<MenuItem.Icon>
|
||||
<avalonia:MaterialIcon Kind="ArrowUp" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Move down" Command="{CompiledBinding MoveDown}">
|
||||
<MenuItem.Icon>
|
||||
<avalonia:MaterialIcon Kind="ArrowDown" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="-" />
|
||||
<MenuItem Header="Rename" Command="{CompiledBinding RenameCategory}">
|
||||
<MenuItem.Icon>
|
||||
<avalonia:MaterialIcon Kind="RenameBox" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Delete" Command="{CompiledBinding DeleteCategory}">
|
||||
<MenuItem.Icon>
|
||||
<avalonia:MaterialIcon Kind="TrashCan" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
</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.Styles>
|
||||
<Style Selector=":is(Button).category-button">
|
||||
<Style Selector=":is(Button).properties-button">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="Grid#ContainerGrid:pointerover :is(Button).category-button">
|
||||
<Style Selector="Grid#ContainerGrid:pointerover :is(Button).properties-button">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Grid#ContainerGrid.flyout-open :is(Button).properties-button">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
<Style Selector="Grid#ContainerGrid.plus-flyout-open :is(Button).properties-button">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="avalonia|MaterialIcon.chevron-collapsed">
|
||||
<Setter Property="RenderTransform" Value="rotate(180deg)" />
|
||||
</Style>
|
||||
@ -89,33 +135,14 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
<Grid x:Name="ContainerGrid" Margin="0 8 0 0" RowDefinitions="Auto,*" >
|
||||
<Grid.ContextFlyout>
|
||||
<MenuFlyout>
|
||||
<MenuItem Header="View properties" Command="{CompiledBinding EditCategory}">
|
||||
<MenuItem.Icon>
|
||||
<avalonia:MaterialIcon Kind="Cog" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Suspend" Command="{CompiledBinding ToggleSuspended}">
|
||||
<MenuItem.Icon>
|
||||
<avalonia:MaterialIcon Kind="Check" IsVisible="{CompiledBinding IsSuspended}" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="-" />
|
||||
<MenuItem Header="Move up" Command="{CompiledBinding MoveUp}">
|
||||
<MenuItem.Icon>
|
||||
<avalonia:MaterialIcon Kind="ArrowUp" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Move down" Command="{CompiledBinding MoveDown}">
|
||||
<MenuItem.Icon>
|
||||
<avalonia:MaterialIcon Kind="ArrowDown" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
</MenuFlyout>
|
||||
</Grid.ContextFlyout>
|
||||
<Grid Grid.Row="0" Background="Transparent" Margin="0 0 6 0" ColumnDefinitions="Auto,*,Auto,Auto,Auto,Auto">
|
||||
|
||||
<Grid Name="ContainerGrid"
|
||||
Margin="0 8 0 0"
|
||||
RowDefinitions="Auto,*"
|
||||
ContextFlyout="{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">
|
||||
<avalonia:MaterialIcon Classes.chevron-collapsed="{CompiledBinding !IsCollapsed}"
|
||||
Kind="ChevronUp"
|
||||
Grid.Column="0"
|
||||
@ -162,43 +189,49 @@
|
||||
</Border>
|
||||
</Panel>
|
||||
|
||||
<Button Classes="category-button icon-button icon-button-small"
|
||||
<Button Classes="properties-button icon-button icon-button-small"
|
||||
Grid.Column="2"
|
||||
ToolTip.Tip="Edit category"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{CompiledBinding EditCategory}"
|
||||
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}"
|
||||
Margin="0 0 2 0">
|
||||
<avalonia:MaterialIcon Kind="Cog" />
|
||||
</Button>
|
||||
<Button Classes="icon-button icon-button-small"
|
||||
Command="{CompiledBinding ToggleSuspended}"
|
||||
Grid.Column="3"
|
||||
ToolTip.Tip="Suspend/resume profile"
|
||||
Grid.Column="4"
|
||||
ToolTip.Tip="Suspend/resume category"
|
||||
Margin="0 0 2 0">
|
||||
<Panel>
|
||||
<avalonia:MaterialIcon Kind="EyeOff" IsVisible="{CompiledBinding IsSuspended}" />
|
||||
<avalonia:MaterialIcon Kind="Eye" IsVisible="{CompiledBinding !IsSuspended}" />
|
||||
</Panel>
|
||||
</Button>
|
||||
<Button Classes="category-button icon-button icon-button-small"
|
||||
Grid.Column="4"
|
||||
ToolTip.Tip="Add profile"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{CompiledBinding AddProfile}"
|
||||
Margin="0 0 2 0">
|
||||
<avalonia:MaterialIcon Kind="Plus" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Border Grid.Row="1">
|
||||
<Panel Grid.Row="1">
|
||||
<ListBox Name="SidebarListBox"
|
||||
Classes="sidebar-listbox"
|
||||
Items="{CompiledBinding ProfileConfigurations}"
|
||||
SelectedItem="{CompiledBinding SelectedProfileConfiguration}"
|
||||
MinHeight="10"
|
||||
MinHeight="35"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Disabled">
|
||||
</ListBox>
|
||||
</Border>
|
||||
<TextBlock IsVisible="{CompiledBinding !ProfileConfigurations.Count}"
|
||||
Margin="12 0 0 0"
|
||||
FontSize="13"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Left"
|
||||
IsHitTestVisible="False"
|
||||
Classes="subtitle">Empty category</TextBlock>
|
||||
</Panel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Disposables;
|
||||
@ -15,6 +16,7 @@ using Artemis.UI.Shared.Services.Builders;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using DynamicData;
|
||||
using DynamicData.Binding;
|
||||
using Newtonsoft.Json;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Sidebar;
|
||||
@ -53,9 +55,11 @@ public class SidebarCategoryViewModel : ActivatableViewModelBase
|
||||
ToggleCollapsed = ReactiveCommand.Create(ExecuteToggleCollapsed);
|
||||
ToggleSuspended = ReactiveCommand.Create(ExecuteToggleSuspended);
|
||||
AddProfile = ReactiveCommand.CreateFromTask(ExecuteAddProfile);
|
||||
EditCategory = ReactiveCommand.CreateFromTask(ExecuteEditCategory);
|
||||
ImportProfile = ReactiveCommand.CreateFromTask(ExecuteImportProfile);
|
||||
MoveUp = ReactiveCommand.Create(ExecuteMoveUp);
|
||||
MoveDown = ReactiveCommand.Create(ExecuteMoveDown);
|
||||
RenameCategory = ReactiveCommand.CreateFromTask(ExecuteRenameCategory);
|
||||
DeleteCategory = ReactiveCommand.CreateFromTask(ExecuteDeleteCategory);
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
@ -67,14 +71,33 @@ public class SidebarCategoryViewModel : ActivatableViewModelBase
|
||||
.Subscribe(e => profileConfigurations.RemoveMany(profileConfigurations.Items.Where(c => c == e.EventArgs.ProfileConfiguration)))
|
||||
.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);
|
||||
|
||||
_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);
|
||||
|
||||
// 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 =>
|
||||
@ -84,12 +107,14 @@ public class SidebarCategoryViewModel : ActivatableViewModelBase
|
||||
});
|
||||
}
|
||||
|
||||
public ReactiveCommand<Unit, Unit> ImportProfile { get; }
|
||||
public ReactiveCommand<Unit, Unit> ToggleCollapsed { get; }
|
||||
public ReactiveCommand<Unit, Unit> ToggleSuspended { get; }
|
||||
public ReactiveCommand<Unit, Unit> AddProfile { get; }
|
||||
public ReactiveCommand<Unit, Unit> EditCategory { get; }
|
||||
public ReactiveCommand<Unit, Unit> MoveUp { get; }
|
||||
public ReactiveCommand<Unit, Unit> MoveDown { get; }
|
||||
public ReactiveCommand<Unit, Unit> RenameCategory { get; }
|
||||
public ReactiveCommand<Unit, Unit> DeleteCategory { get; }
|
||||
|
||||
public ProfileCategory ProfileCategory { get; }
|
||||
public ReadOnlyObservableCollection<SidebarProfileConfigurationViewModel> ProfileConfigurations { get; }
|
||||
@ -114,13 +139,12 @@ public class SidebarCategoryViewModel : ActivatableViewModelBase
|
||||
_profileService.SaveProfileCategory(oldCategory);
|
||||
}
|
||||
|
||||
private async Task ExecuteEditCategory()
|
||||
private async Task ExecuteRenameCategory()
|
||||
{
|
||||
await _windowService.CreateContentDialog()
|
||||
.WithTitle("Edit category")
|
||||
.WithViewModel(out SidebarCategoryEditViewModel vm, ("category", ProfileCategory))
|
||||
.HavingPrimaryButton(b => b.WithText("Confirm").WithCommand(vm.Confirm))
|
||||
.HavingSecondaryButton(b => b.WithText("Delete").WithCommand(vm.Delete))
|
||||
.WithCloseButtonText("Cancel")
|
||||
.WithDefaultButton(ContentDialogButton.Primary)
|
||||
.ShowAsync();
|
||||
@ -128,6 +152,12 @@ public class SidebarCategoryViewModel : ActivatableViewModelBase
|
||||
_sidebarViewModel.UpdateProfileCategories();
|
||||
}
|
||||
|
||||
private async Task ExecuteDeleteCategory()
|
||||
{
|
||||
if (await _windowService.ShowConfirmContentDialog($"Delete {ProfileCategory.Name}", "Do you want to delete this category and all its profiles?"))
|
||||
_profileService.DeleteProfileCategory(ProfileCategory);
|
||||
}
|
||||
|
||||
private async Task ExecuteAddProfile()
|
||||
{
|
||||
ProfileConfiguration? result = await _windowService.ShowDialogAsync<ProfileConfigurationEditViewModel, ProfileConfiguration?>(
|
||||
@ -141,6 +171,42 @@ public class SidebarCategoryViewModel : ActivatableViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteImportProfile()
|
||||
{
|
||||
string[]? result = await _windowService.CreateOpenFileDialog()
|
||||
.HavingFilter(f => f.WithExtension("json").WithName("Artemis profile"))
|
||||
.ShowAsync();
|
||||
|
||||
if (result == null)
|
||||
return;
|
||||
|
||||
string json = await File.ReadAllTextAsync(result[0]);
|
||||
ProfileConfigurationExportModel? profileConfigurationExportModel = null;
|
||||
try
|
||||
{
|
||||
profileConfigurationExportModel = JsonConvert.DeserializeObject<ProfileConfigurationExportModel>(json, IProfileService.ExportSettings);
|
||||
}
|
||||
catch (JsonException e)
|
||||
{
|
||||
_windowService.ShowExceptionDialog("Import profile failed", e);
|
||||
}
|
||||
|
||||
if (profileConfigurationExportModel == null)
|
||||
{
|
||||
await _windowService.ShowConfirmContentDialog("Import profile", "Failed to import this profile, make sure it is a valid Artemis profile.", "Confirm", null);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_profileService.ImportProfile(ProfileCategory, profileConfigurationExportModel);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_windowService.ShowExceptionDialog("Import profile failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteToggleCollapsed()
|
||||
{
|
||||
ProfileCategory.IsCollapsed = !ProfileCategory.IsCollapsed;
|
||||
|
||||
@ -9,13 +9,14 @@
|
||||
x:Class="Artemis.UI.Screens.Sidebar.SidebarProfileConfigurationView"
|
||||
x:DataType="sidebar:SidebarProfileConfigurationViewModel"
|
||||
Background="Transparent">
|
||||
<UserControl.ContextFlyout>
|
||||
<MenuFlyout>
|
||||
<MenuItem Header="View properties" Command="{CompiledBinding EditProfile}">
|
||||
<UserControl.Resources>
|
||||
<MenuFlyout x:Key="ProfileMenuFlyout" Placement="Bottom">
|
||||
<MenuItem Header="Edit properties" Command="{CompiledBinding EditProfile}">
|
||||
<MenuItem.Icon>
|
||||
<avalonia:MaterialIcon Kind="Cog" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="-" />
|
||||
<MenuItem Header="Suspend" Command="{CompiledBinding ToggleSuspended}">
|
||||
<MenuItem.Icon>
|
||||
<avalonia:MaterialIcon Kind="Check" IsVisible="{CompiledBinding ProfileConfiguration.IsSuspended}" />
|
||||
@ -53,8 +54,24 @@
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
</MenuFlyout>
|
||||
</UserControl.ContextFlyout>
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
|
||||
</UserControl.Resources>
|
||||
<UserControl.Styles>
|
||||
<Style Selector=":is(Button).properties-button">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="Grid#ProfileContainerGrid:pointerover :is(Button).properties-button">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
<Style Selector="Grid#ProfileContainerGrid.flyout-open :is(Button).properties-button">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<Grid Name="ProfileContainerGrid"
|
||||
ColumnDefinitions="Auto,*,Auto,Auto"
|
||||
Background="Transparent"
|
||||
ContextFlyout="{StaticResource ProfileMenuFlyout}"
|
||||
Classes.flyout-open="{Binding IsOpen, Source={StaticResource ProfileMenuFlyout}}">
|
||||
<shared:ProfileConfigurationIcon Grid.Column="0"
|
||||
x:Name="ProfileIcon"
|
||||
VerticalAlignment="Center"
|
||||
@ -101,11 +118,10 @@
|
||||
</Border>
|
||||
</Panel>
|
||||
|
||||
<Button Command="{CompiledBinding EditProfile}"
|
||||
Classes="icon-button icon-button-small"
|
||||
<Button Classes="properties-button icon-button icon-button-small"
|
||||
Grid.Column="2"
|
||||
ToolTip.Tip="View properties"
|
||||
HorizontalAlignment="Right"
|
||||
Flyout="{StaticResource ProfileMenuFlyout}"
|
||||
Margin="0 0 2 0">
|
||||
<avalonia:MaterialIcon Kind="Cog" />
|
||||
</Button>
|
||||
|
||||
@ -8,7 +8,6 @@ using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.UI.Shared.Services.NodeEditor;
|
||||
using Artemis.UI.Shared.Services.NodeEditor.Commands;
|
||||
@ -20,7 +19,7 @@ using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.VisualScripting;
|
||||
|
||||
public class NodeScriptWindowViewModel : DialogViewModelBase<bool>
|
||||
public class NodeScriptWindowViewModel : NodeScriptWindowViewModelBase
|
||||
{
|
||||
private readonly INodeEditorService _nodeEditorService;
|
||||
private readonly INodeService _nodeService;
|
||||
@ -34,7 +33,7 @@ public class NodeScriptWindowViewModel : DialogViewModelBase<bool>
|
||||
INodeVmFactory vmFactory,
|
||||
ISettingsService settingsService,
|
||||
IProfileService profileService,
|
||||
IWindowService windowService)
|
||||
IWindowService windowService) : base(nodeScript)
|
||||
{
|
||||
NodeScript = nodeScript;
|
||||
NodeScriptViewModel = vmFactory.NodeScriptViewModel(NodeScript, false);
|
||||
@ -77,7 +76,6 @@ public class NodeScriptWindowViewModel : DialogViewModelBase<bool>
|
||||
});
|
||||
}
|
||||
|
||||
public NodeScript NodeScript { get; }
|
||||
public NodeScriptViewModel NodeScriptViewModel { get; set; }
|
||||
|
||||
public NodeEditorHistory History { get; }
|
||||
|
||||
@ -15,7 +15,8 @@ public class NumericConverter : IValueConverter
|
||||
if (value is not Numeric numeric)
|
||||
return value;
|
||||
|
||||
return Numeric.IsTypeCompatible(targetType) ? numeric.ToType(targetType, NumberFormatInfo.InvariantInfo) : value;
|
||||
object result = Numeric.IsTypeCompatible(targetType) ? numeric.ToType(targetType, NumberFormatInfo.InvariantInfo) : value;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@ -1,29 +1,25 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Modules;
|
||||
using Artemis.Core;
|
||||
using Artemis.Storage.Entities.Profile;
|
||||
using Artemis.VisualScripting.Nodes.DataModel.Screens;
|
||||
using Humanizer;
|
||||
|
||||
namespace Artemis.VisualScripting.Nodes.DataModel;
|
||||
|
||||
[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
|
||||
{
|
||||
private readonly Dictionary<Func<DataModelEventArgs, object>, OutputPin> _propertyPins;
|
||||
private DataModelPath? _dataModelPath;
|
||||
private readonly ObjectOutputPins _objectOutputPins;
|
||||
private IDataModelEvent? _dataModelEvent;
|
||||
private OutputPin? _oldValuePin;
|
||||
private OutputPin? _newValuePin;
|
||||
private DataModelPath? _dataModelPath;
|
||||
private DateTime _lastTrigger;
|
||||
private object? _lastValue;
|
||||
private OutputPin? _newValuePin;
|
||||
private OutputPin? _oldValuePin;
|
||||
private int _valueChangeCount;
|
||||
|
||||
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");
|
||||
TriggerCount = CreateOutputPin<Numeric>("Trigger count");
|
||||
|
||||
@ -48,20 +44,14 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
|
||||
public override void Evaluate()
|
||||
{
|
||||
object? pathValue = _dataModelPath?.GetValue();
|
||||
|
||||
|
||||
// If the path is a data model event, evaluate the event
|
||||
if (pathValue is IDataModelEvent dataModelEvent)
|
||||
{
|
||||
TimeSinceLastTrigger.Value = dataModelEvent.TimeSinceLastTrigger.TotalMilliseconds;
|
||||
TriggerCount.Value = dataModelEvent.TriggerCount;
|
||||
|
||||
foreach ((Func<DataModelEventArgs, object> propertyAccessor, OutputPin outputPin) in _propertyPins)
|
||||
{
|
||||
if (!outputPin.ConnectedTo.Any())
|
||||
continue;
|
||||
object value = dataModelEvent.LastEventArgumentsUntyped != null ? propertyAccessor(dataModelEvent.LastEventArgumentsUntyped) : outputPin.Type.GetDefault()!;
|
||||
outputPin.Value = outputPin.IsNumeric ? new Numeric(value) : value;
|
||||
}
|
||||
_objectOutputPins.SetCurrentValue(dataModelEvent.LastEventArgumentsUntyped);
|
||||
}
|
||||
// If the path is a regular value, evaluate the current value
|
||||
else if (_oldValuePin != null && _newValuePin != null)
|
||||
@ -71,13 +61,13 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
|
||||
TimeSinceLastTrigger.Value = (DateTime.Now - _lastTrigger).TotalMilliseconds;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
_valueChangeCount++;
|
||||
_lastTrigger = DateTime.Now;
|
||||
|
||||
|
||||
_oldValuePin.Value = _lastValue;
|
||||
_newValuePin.Value = pathValue;
|
||||
|
||||
|
||||
_lastValue = pathValue;
|
||||
|
||||
TimeSinceLastTrigger.Value = 0;
|
||||
@ -91,19 +81,20 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
|
||||
_dataModelPath = Storage != null ? new DataModelPath(Storage) : null;
|
||||
if (_dataModelPath != null)
|
||||
_dataModelPath.PathValidated += DataModelPathOnPathValidated;
|
||||
|
||||
|
||||
if (old != null)
|
||||
{
|
||||
old.PathValidated -= DataModelPathOnPathValidated;
|
||||
old.Dispose();
|
||||
}
|
||||
|
||||
|
||||
UpdateOutputPins();
|
||||
}
|
||||
|
||||
private void UpdateOutputPins()
|
||||
{
|
||||
object? pathValue = _dataModelPath?.GetValue();
|
||||
|
||||
if (pathValue is IDataModelEvent dataModelEvent)
|
||||
CreateEventPins(dataModelEvent);
|
||||
else
|
||||
@ -114,25 +105,10 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
|
||||
{
|
||||
if (_dataModelEvent == dataModelEvent)
|
||||
return;
|
||||
|
||||
|
||||
ClearPins();
|
||||
_dataModelEvent = dataModelEvent;
|
||||
foreach (PropertyInfo propertyInfo in dataModelEvent.ArgumentsType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||
.Where(p => p.CustomAttributes.All(a => a.AttributeType != typeof(DataModelIgnoreAttribute))))
|
||||
{
|
||||
// Expect an IDataModelEvent
|
||||
ParameterExpression eventParameter = Expression.Parameter(typeof(DataModelEventArgs), "event");
|
||||
// Cast it to the actual event type
|
||||
UnaryExpression eventCast = Expression.Convert(eventParameter, propertyInfo.DeclaringType!);
|
||||
// Access the property
|
||||
MemberExpression accessor = Expression.Property(eventCast, propertyInfo);
|
||||
// Cast the property to an object (sadly boxing)
|
||||
UnaryExpression objectCast = Expression.Convert(accessor, typeof(object));
|
||||
// Compile the resulting expression
|
||||
Func<DataModelEventArgs, object> expression = Expression.Lambda<Func<DataModelEventArgs, object>>(objectCast, eventParameter).Compile();
|
||||
|
||||
_propertyPins.Add(expression, CreateOrAddOutputPin(propertyInfo.PropertyType, propertyInfo.Name.Humanize()));
|
||||
}
|
||||
_objectOutputPins.ChangeType(dataModelEvent.ArgumentsType);
|
||||
}
|
||||
|
||||
private void CreateValuePins()
|
||||
@ -142,24 +118,31 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
|
||||
Type? propertyType = _dataModelPath?.GetPropertyType();
|
||||
if (propertyType == null)
|
||||
return;
|
||||
|
||||
|
||||
_oldValuePin = CreateOrAddOutputPin(propertyType, "Old value");
|
||||
_newValuePin = CreateOrAddOutputPin(propertyType, "New value");
|
||||
_lastValue = null;
|
||||
_valueChangeCount = 0;
|
||||
}
|
||||
|
||||
|
||||
private void ClearPins()
|
||||
{
|
||||
List<IPin> pins = Pins.Skip(2).ToList();
|
||||
foreach (IPin pin in pins)
|
||||
RemovePin((Pin) pin);
|
||||
|
||||
_propertyPins.Clear();
|
||||
_oldValuePin = null;
|
||||
_newValuePin = null;
|
||||
// Clear the output pins by changing the type to null
|
||||
_objectOutputPins.ChangeType(null);
|
||||
|
||||
if (_oldValuePin != null)
|
||||
{
|
||||
RemovePin(_oldValuePin);
|
||||
_oldValuePin = null;
|
||||
}
|
||||
|
||||
if (_newValuePin != null)
|
||||
{
|
||||
RemovePin(_newValuePin);
|
||||
_newValuePin = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void DataModelPathOnPathValidated(object? sender, EventArgs e)
|
||||
{
|
||||
// Update the output pin now that the type is known and attempt to restore the connection that was likely missing
|
||||
|
||||
@ -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;
|
||||
|
||||
[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 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>();
|
||||
InputValue = CreateInputPin<object>();
|
||||
|
||||
Ouput = CreateOutputPin<bool>();
|
||||
Output = CreateOutputPin<bool>();
|
||||
}
|
||||
|
||||
public InputPin<IList> InputList { get; }
|
||||
public InputPin<object> InputValue { get; }
|
||||
public OutputPin<bool> Ouput { get; }
|
||||
public OutputPin<bool> Output { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Evaluate()
|
||||
{
|
||||
if (InputList.Value == null)
|
||||
{
|
||||
Ouput.Value = Storage == ListOperator.None;
|
||||
Output.Value = Storage == ListOperator.None;
|
||||
return;
|
||||
}
|
||||
|
||||
object? input = InputValue.Value;
|
||||
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)
|
||||
Ouput.Value = InputList.Value.Cast<object>().All(v => v.Equals(input));
|
||||
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.None)
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -12,9 +12,11 @@
|
||||
<UserControl.Resources>
|
||||
<converters:NumericConverter x:Key="NumericConverter" />
|
||||
</UserControl.Resources>
|
||||
<controls:NumberBox VerticalAlignment="Center" MinWidth="75" SimpleNumberFormat="F3" Classes="condensed">
|
||||
<Interaction.Behaviors>
|
||||
<behaviors:LostFocusNumberBoxBindingBehavior Value="{CompiledBinding CurrentValue, Converter={StaticResource NumericConverter}}"/>
|
||||
</Interaction.Behaviors>
|
||||
<controls:NumberBox VerticalAlignment="Center"
|
||||
MinWidth="75"
|
||||
SimpleNumberFormat="F3"
|
||||
Classes="condensed"
|
||||
AcceptsExpression="True"
|
||||
Value="{CompiledBinding CurrentValue, Converter={StaticResource NumericConverter}}">
|
||||
</controls:NumberBox>
|
||||
</UserControl>
|
||||
Loading…
x
Reference in New Issue
Block a user