1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Core - Refactored display conditions

Profile editor - Added UIs for each condition type
This commit is contained in:
Robert 2022-04-09 00:11:22 +02:00
parent 66ea718316
commit 3dfc25b092
47 changed files with 1305 additions and 546 deletions

View File

@ -0,0 +1,78 @@
using System;
using Artemis.Storage.Entities.Profile.Abstract;
using Artemis.Storage.Entities.Profile.Conditions;
namespace Artemis.Core
{
public class AlwaysOnCondition : ICondition
{
public AlwaysOnCondition(RenderProfileElement profileElement)
{
ProfileElement = profileElement;
Entity = new AlwaysOnConditionEntity();
}
public AlwaysOnCondition(AlwaysOnConditionEntity alwaysOnConditionEntity, RenderProfileElement profileElement)
{
ProfileElement = profileElement;
Entity = alwaysOnConditionEntity;
}
#region Implementation of IDisposable
/// <inheritdoc />
public void Dispose()
{
}
#endregion
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
}
/// <inheritdoc />
public void Save()
{
}
#endregion
#region Implementation of ICondition
/// <inheritdoc />
public IConditionEntity Entity { get; }
/// <inheritdoc />
public RenderProfileElement ProfileElement { get; }
/// <inheritdoc />
public bool IsMet { get; private set; }
/// <inheritdoc />
public void Update()
{
if (ProfileElement.Parent is RenderProfileElement parent)
IsMet = parent.DisplayConditionMet;
else
IsMet = true;
}
/// <inheritdoc />
public void UpdateTimeline(double deltaTime)
{
ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), true);
}
/// <inheritdoc />
public void OverrideTimeline(TimeSpan position)
{
ProfileElement.Timeline.Override(position, position > ProfileElement.Timeline.Length);
}
#endregion
}
}

View File

@ -4,211 +4,262 @@ using Artemis.Core.Internal;
using Artemis.Storage.Entities.Profile.Abstract;
using Artemis.Storage.Entities.Profile.Conditions;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents a condition that is based on a <see cref="DataModelEvent" />
/// </summary>
public class EventCondition : CorePropertyChanged, INodeScriptCondition
{
private readonly string _displayName;
private readonly EventConditionEntity _entity;
private readonly EventDefaultNode _eventNode;
private DataModelPath? _eventPath;
private DateTime _lastProcessedTrigger;
private EventOverlapMode _overlapMode;
private NodeScript<bool> _script;
private EventTriggerMode _triggerMode;
private bool _wasMet;
/// <summary>
/// Creates a new instance of the <see cref="EventCondition" /> class
/// </summary>
public EventCondition(RenderProfileElement profileElement)
{
ProfileElement = profileElement;
_entity = new EventConditionEntity();
_displayName = profileElement.GetType().Name;
_eventNode = new EventDefaultNode {X = -300};
_script = new NodeScript<bool>($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile);
}
internal EventCondition(EventConditionEntity entity, RenderProfileElement profileElement)
{
ProfileElement = profileElement;
_entity = entity;
_displayName = profileElement.GetType().Name;
_eventNode = new EventDefaultNode();
_script = null!;
Load();
}
/// <summary>
/// Gets the script that drives the event condition
/// </summary>
public NodeScript<bool> Script
{
get => _script;
set => SetAndNotify(ref _script, value);
}
/// <summary>
/// Gets or sets the path to the event that drives this event condition
/// </summary>
public DataModelPath? EventPath
{
get => _eventPath;
set => SetAndNotify(ref _eventPath, value);
}
/// <summary>
/// Gets or sets how the condition behaves when the event fires.
/// </summary>
public EventTriggerMode TriggerMode
{
get => _triggerMode;
set => SetAndNotify(ref _triggerMode, value);
}
/// <summary>
/// Gets or sets how the condition behaves when events trigger before the timeline finishes in the
/// <see cref="EventTriggerMode.Play" /> event trigger mode.
/// </summary>
public EventOverlapMode OverlapMode
{
get => _overlapMode;
set => SetAndNotify(ref _overlapMode, value);
}
/// <summary>
/// Updates the event node, applying the selected event
/// </summary>
public void UpdateEventNode()
{
IDataModelEvent? dataModelEvent = EventPath?.GetValue() as IDataModelEvent;
_eventNode.CreatePins(dataModelEvent);
if (dataModelEvent != null && !Script.Nodes.Contains(_eventNode))
Script.AddNode(_eventNode);
else if (dataModelEvent == null && Script.Nodes.Contains(_eventNode))
Script.RemoveNode(_eventNode);
}
/// <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 _eventNode;
}
private bool Evaluate()
{
if (EventPath?.GetValue() is not IDataModelEvent dataModelEvent || dataModelEvent.LastTrigger <= _lastProcessedTrigger)
return false;
_lastProcessedTrigger = dataModelEvent.LastTrigger;
if (!Script.ExitNodeConnected)
return true;
Script.Run();
return Script.Result;
}
/// <inheritdoc />
public IConditionEntity Entity => _entity;
/// <inheritdoc />
public RenderProfileElement ProfileElement { get; }
/// <inheritdoc />
public bool IsMet { get; private set; }
/// <inheritdoc />
public void Update()
{
_wasMet = IsMet;
if (TriggerMode == EventTriggerMode.Toggle)
{
if (Evaluate())
IsMet = !IsMet;
}
else
{
IsMet = Evaluate();
}
}
/// <inheritdoc />
public void UpdateTimeline(double deltaTime)
{
if (TriggerMode == EventTriggerMode.Toggle)
{
if (!IsMet && _wasMet)
ProfileElement.Timeline.JumpToEnd();
else if (IsMet && !_wasMet)
ProfileElement.Timeline.JumpToStart();
}
else
{
if (IsMet && ProfileElement.Timeline.IsFinished)
{
ProfileElement.Timeline.JumpToStart();
}
else if (IsMet)
{
if (OverlapMode == EventOverlapMode.Restart)
ProfileElement.Timeline.JumpToStart();
else if (OverlapMode == EventOverlapMode.Copy && ProfileElement is Layer layer)
layer.CreateCopyAsChild();
}
}
ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), TriggerMode == EventTriggerMode.Toggle);
}
/// <inheritdoc />
public void OverrideTimeline(TimeSpan position)
{
ProfileElement.Timeline.Override(position, TriggerMode == EventTriggerMode.Toggle && position > ProfileElement.Timeline.Length);
}
/// <inheritdoc />
public void Dispose()
{
Script?.Dispose();
EventPath?.Dispose();
}
#region Storage
/// <inheritdoc />
public void Load()
{
TriggerMode = (EventTriggerMode) _entity.TriggerMode;
OverlapMode = (EventOverlapMode) _entity.OverlapMode;
if (_entity.EventPath != null)
EventPath = new DataModelPath(_entity.EventPath);
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);
}
/// <inheritdoc />
public void Save()
{
_entity.TriggerMode = (int) TriggerMode;
_entity.OverlapMode = (int) OverlapMode;
Script.Save();
_entity.Script = Script?.Entity;
EventPath?.Save();
_entity.EventPath = EventPath?.Entity;
}
/// <inheritdoc />
public INodeScript NodeScript => Script;
/// <inheritdoc />
public void LoadNodeScript()
{
Script.Load();
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>
/// Represents a condition that is based on a <see cref="DataModelEvent" />
/// Play the timeline once.
/// </summary>
public class EventCondition : CorePropertyChanged, INodeScriptCondition
{
private readonly string _displayName;
private readonly EventConditionEntity _entity;
private EventDefaultNode? _eventNode;
private TimeLineEventOverlapMode _eventOverlapMode;
private DataModelPath? _eventPath;
private DateTime _lastProcessedTrigger;
private NodeScript<bool>? _script;
Play,
/// <summary>
/// Creates a new instance of the <see cref="EventCondition" /> class
/// </summary>
public EventCondition(ProfileElement profileElement)
{
_entity = new EventConditionEntity();
_displayName = profileElement.GetType().Name;
/// <summary>
/// Toggle repeating the timeline.
/// </summary>
Toggle
}
ProfileElement = profileElement;
}
/// <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,
internal EventCondition(EventConditionEntity entity, ProfileElement profileElement)
{
_entity = entity;
_displayName = profileElement.GetType().Name;
/// <summary>
/// Ignore subsequent event fires until the timeline finishes
/// </summary>
Ignore,
ProfileElement = profileElement;
Load();
}
/// <summary>
/// Gets the script that drives the event condition
/// </summary>
public NodeScript<bool>? Script
{
get => _script;
set => SetAndNotify(ref _script, value);
}
/// <summary>
/// Gets or sets the path to the event that drives this event condition
/// </summary>
public DataModelPath? EventPath
{
get => _eventPath;
set => SetAndNotify(ref _eventPath, value);
}
/// <summary>
/// Gets or sets how the condition behaves when events trigger before the timeline finishes
/// </summary>
public TimeLineEventOverlapMode EventOverlapMode
{
get => _eventOverlapMode;
set => SetAndNotify(ref _eventOverlapMode, value);
}
/// <summary>
/// Updates the event node, applying the selected event
/// </summary>
public void UpdateEventNode()
{
if (Script == null || EventPath?.GetValue() is not IDataModelEvent dataModelEvent)
return;
if (Script.Nodes.FirstOrDefault(n => n is EventDefaultNode) is EventDefaultNode existing)
{
existing.UpdateDataModelEvent(dataModelEvent);
_eventNode = existing;
}
else
{
_eventNode = new EventDefaultNode {X = -300};
_eventNode.UpdateDataModelEvent(dataModelEvent);
}
if (_eventNode.Pins.Any() && !Script.Nodes.Contains(_eventNode))
Script.AddNode(_eventNode);
else
Script.RemoveNode(_eventNode);
}
/// <summary>
/// Updates the <see cref="Script" /> with a new empty node script
/// </summary>
public void CreateEmptyNodeScript()
{
Script?.Dispose();
Script = new NodeScript<bool>($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile);
UpdateEventNode();
}
private bool Evaluate()
{
if (EventPath?.GetValue() is not IDataModelEvent dataModelEvent || dataModelEvent.LastTrigger <= _lastProcessedTrigger)
return false;
_lastProcessedTrigger = dataModelEvent.LastTrigger;
if (Script == null)
return true;
Script.Run();
return Script.Result;
}
/// <inheritdoc />
public IConditionEntity Entity => _entity;
/// <inheritdoc />
public ProfileElement ProfileElement { get; }
/// <inheritdoc />
public bool IsMet { get; private set; }
/// <inheritdoc />
public void Update()
{
if (EventOverlapMode == TimeLineEventOverlapMode.Toggle)
{
if (Evaluate())
IsMet = !IsMet;
}
else
{
IsMet = Evaluate();
}
}
/// <inheritdoc />
public void ApplyToTimeline(bool isMet, bool wasMet, Timeline timeline)
{
if (!isMet)
{
if (EventOverlapMode == TimeLineEventOverlapMode.Toggle)
timeline.JumpToEnd();
return;
}
// Event overlap mode doesn't apply in this case
if (timeline.IsFinished)
{
timeline.JumpToStart();
return;
}
// If the timeline was already running, look at the event overlap mode
if (EventOverlapMode == TimeLineEventOverlapMode.Restart)
timeline.JumpToStart();
else if (EventOverlapMode == TimeLineEventOverlapMode.Copy && ProfileElement is Layer layer)
layer.CreateCopyAsChild();
else if (EventOverlapMode == TimeLineEventOverlapMode.Toggle && !wasMet)
timeline.JumpToStart();
// The remaining overlap mode is 'ignore' which requires no further action
}
/// <inheritdoc />
public void Dispose()
{
Script?.Dispose();
EventPath?.Dispose();
}
#region Storage
/// <inheritdoc />
public void Load()
{
EventOverlapMode = (TimeLineEventOverlapMode) _entity.EventOverlapMode;
if (_entity.Script != null)
Script = new NodeScript<bool>($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", _entity.Script, ProfileElement.Profile);
if (_entity.EventPath != null)
EventPath = new DataModelPath(_entity.EventPath);
}
/// <inheritdoc />
public void Save()
{
_entity.EventOverlapMode = (int) EventOverlapMode;
Script?.Save();
_entity.Script = Script?.Entity;
EventPath?.Save();
_entity.EventPath = EventPath?.Entity;
}
/// <inheritdoc />
public INodeScript NodeScript => Script;
/// <inheritdoc />
public void LoadNodeScript()
{
if (Script == null)
return;
Script.Load();
UpdateEventNode();
Script.LoadConnections();
}
#endregion
}
/// <summary>
/// Play another copy of the timeline on top of the current run
/// </summary>
Copy
}

View File

@ -16,7 +16,7 @@ public interface ICondition : IDisposable, IStorageModel
/// <summary>
/// Gets the profile element this condition applies to
/// </summary>
public ProfileElement ProfileElement { get; }
public RenderProfileElement ProfileElement { get; }
/// <summary>
/// Gets a boolean indicating whether the condition is currently met
@ -30,12 +30,11 @@ public interface ICondition : IDisposable, IStorageModel
void Update();
/// <summary>
/// Applies the display condition to the provided timeline
/// Updates the timeline according to the provided <paramref name="deltaTime" /> as the display condition sees fit.
/// </summary>
/// <param name="isMet"></param>
/// <param name="wasMet"></param>
/// <param name="timeline">The timeline to apply the display condition to</param>
void ApplyToTimeline(bool isMet, bool wasMet, Timeline timeline);
void UpdateTimeline(double deltaTime);
void OverrideTimeline(TimeSpan position);
}
/// <summary>

View File

@ -0,0 +1,78 @@
using System;
using Artemis.Storage.Entities.Profile.Abstract;
using Artemis.Storage.Entities.Profile.Conditions;
namespace Artemis.Core
{
public class PlayOnceCondition : ICondition
{
public PlayOnceCondition(RenderProfileElement profileElement)
{
ProfileElement = profileElement;
Entity = new PlayOnceConditionEntity();
}
public PlayOnceCondition(PlayOnceConditionEntity entity, RenderProfileElement profileElement)
{
ProfileElement = profileElement;
Entity = entity;
}
#region Implementation of IDisposable
/// <inheritdoc />
public void Dispose()
{
}
#endregion
#region Implementation of IStorageModel
/// <inheritdoc />
public void Load()
{
}
/// <inheritdoc />
public void Save()
{
}
#endregion
#region Implementation of ICondition
/// <inheritdoc />
public IConditionEntity Entity { get; }
/// <inheritdoc />
public RenderProfileElement ProfileElement { get; }
/// <inheritdoc />
public bool IsMet { get; private set; }
/// <inheritdoc />
public void Update()
{
if (ProfileElement.Parent is RenderProfileElement parent)
IsMet = parent.DisplayConditionMet;
else
IsMet = true;
}
/// <inheritdoc />
public void UpdateTimeline(double deltaTime)
{
ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), false);
}
/// <inheritdoc />
public void OverrideTimeline(TimeSpan position)
{
ProfileElement.Timeline.Override(position, false);
}
#endregion
}
}

View File

@ -1,4 +1,5 @@
using Artemis.Storage.Entities.Profile.Abstract;
using System;
using Artemis.Storage.Entities.Profile.Abstract;
using Artemis.Storage.Entities.Profile.Conditions;
namespace Artemis.Core
@ -10,11 +11,14 @@ namespace Artemis.Core
{
private readonly string _displayName;
private readonly StaticConditionEntity _entity;
private StaticPlayMode _playMode;
private StaticStopMode _stopMode;
private bool _wasMet;
/// <summary>
/// Creates a new instance of the <see cref="StaticCondition" /> class
/// </summary>
public StaticCondition(ProfileElement profileElement)
public StaticCondition(RenderProfileElement profileElement)
{
_entity = new StaticConditionEntity();
_displayName = profileElement.GetType().Name;
@ -23,7 +27,7 @@ namespace Artemis.Core
Script = new NodeScript<bool>($"Activate {_displayName}", $"Whether or not this {_displayName} should be active", profileElement.Profile);
}
internal StaticCondition(StaticConditionEntity entity, ProfileElement profileElement)
internal StaticCondition(StaticConditionEntity entity, RenderProfileElement profileElement)
{
_entity = entity;
_displayName = profileElement.GetType().Name;
@ -43,31 +47,66 @@ namespace Artemis.Core
public IConditionEntity Entity => _entity;
/// <inheritdoc />
public ProfileElement ProfileElement { get; }
public RenderProfileElement ProfileElement { get; }
/// <inheritdoc />
public bool IsMet { get; private set; }
/// <inheritdoc />
public void Update()
/// <summary>
/// Gets or sets the mode in which the render element starts its timeline when display conditions are met
/// </summary>
public StaticPlayMode PlayMode
{
if (!Script.ExitNodeConnected)
{
IsMet = true;
return;
}
get => _playMode;
set => SetAndNotify(ref _playMode, value);
}
Script.Run();
IsMet = Script.Result;
/// <summary>
/// Gets or sets the mode in which the render element stops its timeline when display conditions are no longer met
/// </summary>
public StaticStopMode StopMode
{
get => _stopMode;
set => SetAndNotify(ref _stopMode, value);
}
/// <inheritdoc />
public void ApplyToTimeline(bool isMet, bool wasMet, Timeline timeline)
public void Update()
{
if (isMet && !wasMet && timeline.IsFinished)
timeline.JumpToStart();
else if (!isMet && wasMet && timeline.StopMode == TimelineStopMode.SkipToEnd)
timeline.JumpToEndSegment();
_wasMet = IsMet;
// No need to run the script if the parent isn't met anyway
bool parentConditionMet = ProfileElement.Parent is not RenderProfileElement renderProfileElement || renderProfileElement.DisplayConditionMet;
if (!parentConditionMet)
{
IsMet = false;
return;
}
if (!Script.ExitNodeConnected)
IsMet = true;
else
{
Script.Run();
IsMet = Script.Result;
}
}
/// <inheritdoc />
public void UpdateTimeline(double deltaTime)
{
if (IsMet && !_wasMet && ProfileElement.Timeline.IsFinished)
ProfileElement.Timeline.JumpToStart();
else if (!IsMet && _wasMet && StopMode == StaticStopMode.SkipToEnd)
ProfileElement.Timeline.JumpToEndSegment();
ProfileElement.Timeline.Update(TimeSpan.FromSeconds(deltaTime), PlayMode == StaticPlayMode.Repeat && IsMet);
}
/// <inheritdoc />
public void OverrideTimeline(TimeSpan position)
{
ProfileElement.Timeline.Override(position, PlayMode == StaticPlayMode.Repeat && position > ProfileElement.Timeline.Length);
}
/// <inheritdoc />
@ -81,12 +120,18 @@ namespace Artemis.Core
/// <inheritdoc />
public void Load()
{
PlayMode = (StaticPlayMode) _entity.PlayMode;
StopMode = (StaticStopMode) _entity.StopMode;
Script = new NodeScript<bool>($"Activate {_displayName}", $"Whether or not this {_displayName} should be active", _entity.Script, ProfileElement.Profile);
}
/// <inheritdoc />
public void Save()
{
_entity.PlayMode = (int) PlayMode;
_entity.StopMode = (int) StopMode;
Script.Save();
_entity.Script = Script.Entity;
}
@ -102,4 +147,36 @@ namespace Artemis.Core
#endregion
}
/// <summary>
/// Represents a mode for render elements to start their timeline when display conditions are met
/// </summary>
public enum StaticPlayMode
{
/// <summary>
/// Continue repeating the main segment of the timeline while the condition is met
/// </summary>
Repeat,
/// <summary>
/// Only play the timeline once when the condition is met
/// </summary>
Once
}
/// <summary>
/// Represents a mode for render elements to stop their timeline when display conditions are no longer met
/// </summary>
public enum StaticStopMode
{
/// <summary>
/// When conditions are no longer met, finish the the current run of the main timeline
/// </summary>
Finish,
/// <summary>
/// When conditions are no longer met, skip to the end segment of the timeline
/// </summary>
SkipToEnd
}
}

View File

@ -237,11 +237,11 @@ namespace Artemis.Core
}
/// <inheritdoc />
public override void OverrideTimelineAndApply(TimeSpan position, bool stickToMainSegment)
public override void OverrideTimelineAndApply(TimeSpan position)
{
Timeline.Override(position, stickToMainSegment);
DisplayCondition.OverrideTimeline(position);
foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
baseLayerEffect.InternalUpdate(Timeline);
baseLayerEffect.InternalUpdate(Timeline); ;
}
/// <inheritdoc />

View File

@ -476,9 +476,9 @@ namespace Artemis.Core
}
/// <inheritdoc />
public override void OverrideTimelineAndApply(TimeSpan position, bool stickToMainSegment)
public override void OverrideTimelineAndApply(TimeSpan position)
{
Timeline.Override(position, stickToMainSegment);
DisplayCondition.OverrideTimeline(position);
General.Update(Timeline);
Transform.Update(Timeline);

View File

@ -27,6 +27,7 @@ namespace Artemis.Core
LayerEffectsList = new List<BaseLayerEffect>();
LayerEffects = new ReadOnlyCollection<BaseLayerEffect>(LayerEffectsList);
Parent = parent ?? throw new ArgumentNullException(nameof(parent));
_displayCondition = new AlwaysOnCondition(this);
LayerEffectStore.LayerEffectAdded += LayerEffectStoreOnLayerEffectAdded;
LayerEffectStore.LayerEffectRemoved += LayerEffectStoreOnLayerEffectRemoved;
@ -52,7 +53,7 @@ namespace Artemis.Core
/// Occurs when a layer effect has been added or removed to this render element
/// </summary>
public event EventHandler? LayerEffectsUpdated;
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
@ -76,8 +77,10 @@ namespace Artemis.Core
DisplayCondition = RenderElementEntity.DisplayCondition switch
{
StaticConditionEntity staticConditionEntity => new StaticCondition(staticConditionEntity, this),
EventConditionEntity eventConditionEntity => new EventCondition(eventConditionEntity, this),
AlwaysOnConditionEntity entity => new AlwaysOnCondition(entity, this),
PlayOnceConditionEntity entity => new PlayOnceCondition(entity, this),
StaticConditionEntity entity => new StaticCondition(entity, this),
EventConditionEntity entity => new EventCondition(entity, this),
_ => DisplayCondition
};
@ -124,17 +127,11 @@ namespace Artemis.Core
public Timeline Timeline { get; private set; }
/// <summary>
/// Updates the <see cref="Timeline" /> according to the provided <paramref name="deltaTime" /> and current display
/// condition status
/// Updates the <see cref="Timeline" /> according to the provided <paramref name="deltaTime" /> and current display condition
/// </summary>
protected void UpdateTimeline(double deltaTime)
{
// TODO: Move to conditions
// The play mode dictates whether to stick to the main segment unless the display conditions contains events
bool stickToMainSegment = Timeline.PlayMode == TimelinePlayMode.Repeat && DisplayConditionMet;
Timeline.Update(TimeSpan.FromSeconds(deltaTime), stickToMainSegment);
DisplayCondition.UpdateTimeline(deltaTime);
}
#endregion
@ -317,7 +314,7 @@ namespace Artemis.Core
OrderEffects();
}
internal void ActivateLayerEffect(BaseLayerEffect layerEffect)
{
@ -365,13 +362,13 @@ namespace Artemis.Core
/// <summary>
/// Gets or sets the display condition used to determine whether this element is active or not
/// </summary>
public ICondition? DisplayCondition
public ICondition DisplayCondition
{
get => _displayCondition;
set => SetAndNotify(ref _displayCondition, value);
}
private ICondition? _displayCondition;
private ICondition _displayCondition;
/// <summary>
/// Evaluates the display conditions on this element and applies any required changes to the <see cref="Timeline" />
@ -384,14 +381,7 @@ namespace Artemis.Core
return;
}
if (DisplayCondition == null)
{
DisplayConditionMet = true;
return;
}
DisplayCondition.Update();
DisplayCondition.ApplyToTimeline(DisplayCondition.IsMet, DisplayConditionMet, Timeline);
DisplayConditionMet = DisplayCondition.IsMet;
}
@ -401,7 +391,6 @@ namespace Artemis.Core
/// Overrides the main timeline to the specified time and clears any extra time lines
/// </summary>
/// <param name="position">The position to set the timeline to</param>
/// <param name="stickToMainSegment">Whether to stick to the main segment, wrapping around if needed</param>
public abstract void OverrideTimelineAndApply(TimeSpan position, bool stickToMainSegment);
public abstract void OverrideTimelineAndApply(TimeSpan position);
}
}

View File

@ -41,8 +41,7 @@ public class Timeline : CorePropertyChanged, IStorageModel
private TimeSpan _position;
private TimeSpan _lastDelta;
private TimelinePlayMode _playMode;
private TimelineStopMode _stopMode;
private TimeSpan _startSegmentLength;
private TimeSpan _mainSegmentLength;
private TimeSpan _endSegmentLength;
@ -67,23 +66,7 @@ public class Timeline : CorePropertyChanged, IStorageModel
private set => SetAndNotify(ref _lastDelta, value);
}
/// <summary>
/// Gets or sets the mode in which the render element starts its timeline when display conditions are met
/// </summary>
public TimelinePlayMode PlayMode
{
get => _playMode;
set => SetAndNotify(ref _playMode, value);
}
/// <summary>
/// Gets or sets the mode in which the render element stops its timeline when display conditions are no longer met
/// </summary>
public TimelineStopMode StopMode
{
get => _stopMode;
set => SetAndNotify(ref _stopMode, value);
}
/// <summary>
/// Gets a boolean indicating whether the timeline has finished its run
@ -388,8 +371,6 @@ public class Timeline : CorePropertyChanged, IStorageModel
StartSegmentLength = Entity.StartSegmentLength;
MainSegmentLength = Entity.MainSegmentLength;
EndSegmentLength = Entity.EndSegmentLength;
PlayMode = (TimelinePlayMode) Entity.PlayMode;
StopMode = (TimelineStopMode) Entity.StopMode;
JumpToEnd();
}
@ -400,8 +381,6 @@ public class Timeline : CorePropertyChanged, IStorageModel
Entity.StartSegmentLength = StartSegmentLength;
Entity.MainSegmentLength = MainSegmentLength;
Entity.EndSegmentLength = EndSegmentLength;
Entity.PlayMode = (int) PlayMode;
Entity.StopMode = (int) StopMode;
}
#endregion
@ -413,61 +392,3 @@ internal enum TimelineSegment
Main,
End
}
/// <summary>
/// Represents a mode for render elements to start their timeline when display conditions are met
/// </summary>
public enum TimelinePlayMode
{
/// <summary>
/// Continue repeating the main segment of the timeline while the condition is met
/// </summary>
Repeat,
/// <summary>
/// Only play the timeline once when the condition is met
/// </summary>
Once
}
/// <summary>
/// Represents a mode for render elements to stop their timeline when display conditions are no longer met
/// </summary>
public enum TimelineStopMode
{
/// <summary>
/// When conditions are no longer met, finish the the current run of the main timeline
/// </summary>
Finish,
/// <summary>
/// When conditions are no longer met, skip to the end segment of the timeline
/// </summary>
SkipToEnd
}
/// <summary>
/// Represents a mode for render elements to start their timeline when display conditions events are fired
/// </summary>
public enum TimeLineEventOverlapMode
{
/// <summary>
/// Stop the current run and restart the timeline
/// </summary>
Restart,
/// <summary>
/// Ignore subsequent event fires until the timeline finishes
/// </summary>
Ignore,
/// <summary>
/// Play another copy of the timeline on top of the current run
/// </summary>
Copy,
/// <summary>
/// Repeat the timeline until the event fires again
/// </summary>
Toggle
}

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Artemis.Core.Modules;
@ -9,6 +10,7 @@ namespace Artemis.Core.Internal
internal class EventDefaultNode : Node
{
private readonly Dictionary<PropertyInfo, OutputPin> _propertyPins;
private readonly List<OutputPin> _pinBucket = new();
private IDataModelEvent? _dataModelEvent;
public EventDefaultNode() : base("Event Arguments", "Contains the event arguments that triggered the evaluation")
@ -18,19 +20,22 @@ namespace Artemis.Core.Internal
public override bool IsDefaultNode => true;
public void UpdateDataModelEvent(IDataModelEvent dataModelEvent)
public void CreatePins(IDataModelEvent? dataModelEvent)
{
if (_dataModelEvent == dataModelEvent)
return;
foreach (var (_, outputPin) in _propertyPins)
RemovePin(outputPin);
while (Pins.Any())
RemovePin((Pin) Pins.First());
_propertyPins.Clear();
_dataModelEvent = dataModelEvent;
if (dataModelEvent == null)
return;
foreach (PropertyInfo propertyInfo in dataModelEvent.ArgumentsType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(p => p.CustomAttributes.All(a => a.AttributeType != typeof(DataModelIgnoreAttribute))))
_propertyPins.Add(propertyInfo, CreateOutputPin(propertyInfo.PropertyType, propertyInfo.Name.Humanize()));
.Where(p => p.CustomAttributes.All(a => a.AttributeType != typeof(DataModelIgnoreAttribute))))
_propertyPins.Add(propertyInfo, CreateOrAddOutputPin(propertyInfo.PropertyType, propertyInfo.Name.Humanize()));
}
public override void Evaluate()
@ -38,11 +43,38 @@ namespace Artemis.Core.Internal
if (_dataModelEvent?.LastEventArgumentsUntyped == null)
return;
foreach (var (propertyInfo, outputPin) in _propertyPins)
foreach ((PropertyInfo propertyInfo, OutputPin outputPin) in _propertyPins)
{
if (outputPin.ConnectedTo.Any())
outputPin.Value = propertyInfo.GetValue(_dataModelEvent.LastEventArgumentsUntyped) ?? outputPin.Type.GetDefault()!;
}
}
/// <summary>
/// Creates or adds an input pin to the node using a bucket.
/// The bucket might grow a bit over time as the user edits the node but pins won't get lost, enabling undo/redo in the
/// editor.
/// </summary>
private OutputPin CreateOrAddOutputPin(Type valueType, string displayName)
{
// Grab the first pin from the bucket that isn't on the node yet
OutputPin? pin = _pinBucket.FirstOrDefault(p => !Pins.Contains(p));
// If there is none, create a new one and add it to the bucket
if (pin == null)
{
pin = CreateOutputPin(valueType, displayName);
_pinBucket.Add(pin);
}
// If there was a pin in the bucket, update it's type and display name and reuse it
else
{
pin.ChangeType(valueType);
pin.Name = displayName;
AddPin(pin);
}
return pin;
}
}
}

View File

@ -280,7 +280,7 @@ public abstract class Node : CorePropertyChanged, INode
/// Serializes the <see cref="Storage" /> object into a string
/// </summary>
/// <returns>The serialized object</returns>
internal virtual string SerializeStorage()
public virtual string SerializeStorage()
{
return string.Empty;
}
@ -289,7 +289,7 @@ public abstract class Node : CorePropertyChanged, INode
/// Deserializes the <see cref="Storage" /> object and sets it
/// </summary>
/// <param name="serialized">The serialized object</param>
internal virtual void DeserializeStorage(string serialized)
public virtual void DeserializeStorage(string serialized)
{
}
@ -333,12 +333,14 @@ public abstract class Node<TStorage> : Node
/// </summary>
public event EventHandler? StorageModified;
internal override string SerializeStorage()
/// <inheritdoc />
public override string SerializeStorage()
{
return CoreJson.SerializeObject(Storage, true);
}
internal override void DeserializeStorage(string serialized)
/// <inheritdoc />
public override void DeserializeStorage(string serialized)
{
Storage = CoreJson.DeserializeObject<TStorage>(serialized) ?? default(TStorage);
}

View File

@ -0,0 +1,10 @@
using Artemis.Storage.Entities.Profile.Abstract;
using Artemis.Storage.Entities.Profile.Nodes;
namespace Artemis.Storage.Entities.Profile.Conditions
{
public class AlwaysOnConditionEntity : IConditionEntity
{
}
}

View File

@ -5,7 +5,8 @@ namespace Artemis.Storage.Entities.Profile.Conditions
{
public class EventConditionEntity : IConditionEntity
{
public int EventOverlapMode { get; set; }
public int TriggerMode { get; set; }
public int OverlapMode { get; set; }
public DataModelPathEntity EventPath { get; set; }
public NodeScriptEntity Script { get; set; }
}

View File

@ -0,0 +1,9 @@
using Artemis.Storage.Entities.Profile.Abstract;
using Artemis.Storage.Entities.Profile.Nodes;
namespace Artemis.Storage.Entities.Profile.Conditions
{
public class PlayOnceConditionEntity : IConditionEntity
{
}
}

View File

@ -5,6 +5,8 @@ namespace Artemis.Storage.Entities.Profile.Conditions
{
public class StaticConditionEntity : IConditionEntity
{
public int PlayMode { get; set; }
public int StopMode { get; set; }
public NodeScriptEntity Script { get; set; }
}
}

View File

@ -7,8 +7,5 @@ namespace Artemis.Storage.Entities.Profile
public TimeSpan StartSegmentLength { get; set; }
public TimeSpan MainSegmentLength { get; set; }
public TimeSpan EndSegmentLength { get; set; }
public int PlayMode { get; set; }
public int StopMode { get; set; }
}
}

View File

@ -10,7 +10,7 @@ public class DeleteKeyframe : IProfileEditorCommand
private readonly ILayerPropertyKeyframe _keyframe;
/// <summary>
/// Creates a new instance of the <see cref="DeleteKeyframe{T}" /> class.
/// Creates a new instance of the <see cref="DeleteKeyframe" /> class.
/// </summary>
public DeleteKeyframe(ILayerPropertyKeyframe keyframe)
{

View File

@ -0,0 +1,73 @@
using System;
using Artemis.Core;
using Artemis.UI.Shared.Services.NodeEditor;
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
/// <summary>
/// Represents a profile editor command that can be used to update an event condition's trigger mode.
/// </summary>
public class UpdateEventConditionPath : IProfileEditorCommand, IDisposable
{
private readonly EventCondition _eventCondition;
private readonly DataModelPath? _value;
private readonly DataModelPath? _oldValue;
private readonly NodeConnectionStore? _store;
private bool _executed;
/// <summary>
/// Creates a new instance of the <see cref="UpdateEventTriggerMode" /> class.
/// </summary>
public UpdateEventConditionPath(EventCondition eventCondition, DataModelPath? value)
{
_eventCondition = eventCondition;
_value = value;
_oldValue = eventCondition.EventPath;
INode? startNode = eventCondition.GetStartNode();
if (startNode != null)
_store = new NodeConnectionStore(startNode);
}
/// <inheritdoc />
public string DisplayName => "Update event path";
/// <inheritdoc />
public void Execute()
{
// Store old connections
_store?.Store();
// Change the end node
_eventCondition.EventPath = _value;
_eventCondition.UpdateEventNode();
_executed = true;
}
/// <inheritdoc />
public void Undo()
{
// Change the end node
_eventCondition.EventPath = _oldValue;
_eventCondition.UpdateEventNode();
// Restore old connections
_store?.Restore();
_executed = false;
}
#region IDisposable
/// <inheritdoc />
public void Dispose()
{
if (_executed)
_oldValue?.Dispose();
else
_value?.Dispose();
}
#endregion
}

View File

@ -0,0 +1,38 @@
using Artemis.Core;
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
/// <summary>
/// Represents a profile editor command that can be used to update an event condition's overlap mode.
/// </summary>
public class UpdateEventOverlapMode : IProfileEditorCommand
{
private readonly EventCondition _eventCondition;
private readonly EventOverlapMode _value;
private readonly EventOverlapMode _oldValue;
/// <summary>
/// Creates a new instance of the <see cref="UpdateEventOverlapMode" /> class.
/// </summary>
public UpdateEventOverlapMode(EventCondition eventCondition, EventOverlapMode value)
{
_eventCondition = eventCondition;
_value = value;
_oldValue = eventCondition.OverlapMode;
}
/// <inheritdoc />
public string DisplayName => "Update event overlap mode";
/// <inheritdoc />
public void Execute()
{
_eventCondition.OverlapMode = _value;
}
/// <inheritdoc />
public void Undo()
{
_eventCondition.OverlapMode = _oldValue;
}
}

View File

@ -0,0 +1,38 @@
using Artemis.Core;
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
/// <summary>
/// Represents a profile editor command that can be used to update an event condition's trigger mode.
/// </summary>
public class UpdateEventTriggerMode : IProfileEditorCommand
{
private readonly EventCondition _eventCondition;
private readonly EventTriggerMode _oldValue;
private readonly EventTriggerMode _value;
/// <summary>
/// Creates a new instance of the <see cref="UpdateEventTriggerMode" /> class.
/// </summary>
public UpdateEventTriggerMode(EventCondition eventCondition, EventTriggerMode value)
{
_eventCondition = eventCondition;
_value = value;
_oldValue = eventCondition.TriggerMode;
}
/// <inheritdoc />
public string DisplayName => "Update event trigger mode";
/// <inheritdoc />
public void Execute()
{
_eventCondition.TriggerMode = _value;
}
/// <inheritdoc />
public void Undo()
{
_eventCondition.TriggerMode = _oldValue;
}
}

View File

@ -0,0 +1,38 @@
using Artemis.Core;
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
/// <summary>
/// Represents a profile editor command that can be used to update an static condition's play mode.
/// </summary>
public class UpdateStaticPlayMode : IProfileEditorCommand
{
private readonly StaticCondition _staticCondition;
private readonly StaticPlayMode _value;
private readonly StaticPlayMode _oldValue;
/// <summary>
/// Creates a new instance of the <see cref="UpdateEventTriggerMode" /> class.
/// </summary>
public UpdateStaticPlayMode(StaticCondition staticCondition, StaticPlayMode value)
{
_staticCondition = staticCondition;
_value = value;
_oldValue = staticCondition.PlayMode;
}
/// <inheritdoc />
public string DisplayName => "Update condition play mode";
/// <inheritdoc />
public void Execute()
{
_staticCondition.PlayMode = _value;
}
/// <inheritdoc />
public void Undo()
{
_staticCondition.PlayMode = _oldValue;
}
}

View File

@ -0,0 +1,38 @@
using Artemis.Core;
namespace Artemis.UI.Shared.Services.ProfileEditor.Commands;
/// <summary>
/// Represents a profile editor command that can be used to update an static condition's play mode.
/// </summary>
public class UpdateStaticStopMode : IProfileEditorCommand
{
private readonly StaticCondition _staticCondition;
private readonly StaticStopMode _value;
private readonly StaticStopMode _oldValue;
/// <summary>
/// Creates a new instance of the <see cref="UpdateEventTriggerMode" /> class.
/// </summary>
public UpdateStaticStopMode(StaticCondition staticCondition, StaticStopMode value)
{
_staticCondition = staticCondition;
_value = value;
_oldValue = staticCondition.StopMode;
}
/// <inheritdoc />
public string DisplayName => "Update condition stop mode";
/// <inheritdoc />
public void Execute()
{
_staticCondition.StopMode = _value;
}
/// <inheritdoc />
public void Undo()
{
_staticCondition.StopMode = _oldValue;
}
}

View File

@ -96,8 +96,7 @@ internal class ProfileEditorService : IProfileEditorService
else
{
renderElement.Enable();
bool stickToMainSegment = (renderElement != _profileElementSubject.Value || renderElement.Timeline.Length < time) && renderElement.Timeline.PlayMode == TimelinePlayMode.Repeat;
renderElement.OverrideTimelineAndApply(time, stickToMainSegment);
renderElement.OverrideTimelineAndApply(time);
foreach (ProfileElement child in renderElement.Children)
TickProfileElement(child, time);

View File

@ -5,6 +5,7 @@ using Artemis.Core.LayerEffects;
using Artemis.UI.Screens.Device;
using Artemis.UI.Screens.Plugins;
using Artemis.UI.Screens.ProfileEditor;
using Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes;
using Artemis.UI.Screens.ProfileEditor.ProfileTree;
using Artemis.UI.Screens.ProfileEditor.Properties;
using Artemis.UI.Screens.ProfileEditor.Properties.DataBinding;
@ -52,7 +53,7 @@ namespace Artemis.UI.Ninject.Factories
SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(SidebarViewModel sidebarViewModel, ProfileConfiguration profileConfiguration);
}
public interface SurfaceVmFactory : IVmFactory
public interface ISurfaceVmFactory : IVmFactory
{
SurfaceDeviceViewModel SurfaceDeviceViewModel(ArtemisDevice device);
ListDeviceViewModel ListDeviceViewModel(ArtemisDevice device);
@ -108,4 +109,12 @@ namespace Artemis.UI.Ninject.Factories
InputPinCollectionViewModel InputPinCollectionViewModel(IPinCollection inputPinCollection, NodeScriptViewModel nodeScriptViewModel);
OutputPinCollectionViewModel OutputPinCollectionViewModel(IPinCollection outputPinCollection, NodeScriptViewModel nodeScriptViewModel);
}
public interface IConditionVmFactory : IVmFactory
{
AlwaysOnConditionViewModel AlwaysOnConditionViewModel(AlwaysOnCondition alwaysOnCondition);
PlayOnceConditionViewModel PlayOnceConditionViewModel(PlayOnceCondition playOnceCondition);
StaticConditionViewModel StaticConditionViewModel(StaticCondition staticCondition);
EventConditionViewModel EventConditionViewModel(EventCondition eventCondition);
}
}

View File

@ -1,19 +1,18 @@
using System;
using Artemis.UI.Shared;
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition
{
public class ConditionTypeViewModel : ViewModelBase
{
public ConditionTypeViewModel(string name, string description, Type? conditionType)
{
Name = name;
Description = description;
ConditionType = conditionType;
}
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition;
public string Name { get; }
public string Description { get; }
public Type? ConditionType { get; }
public class ConditionTypeViewModel : ViewModelBase
{
public ConditionTypeViewModel(string name, string description, Type conditionType)
{
Name = name;
Description = description;
ConditionType = conditionType;
}
public string Name { get; }
public string Description { get; }
public Type ConditionType { get; }
}

View File

@ -0,0 +1,16 @@
<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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="300" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes.AlwaysOnConditionView">
<DockPanel VerticalAlignment="Top" Margin="0 5">
<avalonia:MaterialIcon Kind="InfoCircle" Margin="5 0"/>
<TextBlock Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap">
After playing the start segment, the main segment is endlessly repeated.
</TextBlock>
</DockPanel>
</UserControl>

View File

@ -0,0 +1,17 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes;
public class AlwaysOnConditionView : UserControl
{
public AlwaysOnConditionView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

View File

@ -0,0 +1,14 @@
using Artemis.Core;
using Artemis.UI.Shared;
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes;
public class AlwaysOnConditionViewModel : ViewModelBase
{
private readonly AlwaysOnCondition _alwaysOnCondition;
public AlwaysOnConditionViewModel(AlwaysOnCondition alwaysOnCondition)
{
_alwaysOnCondition = alwaysOnCondition;
}
}

View File

@ -0,0 +1,59 @@
<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:dataModelPicker="clr-namespace:Artemis.UI.Shared.Controls.DataModelPicker;assembly=Artemis.UI.Shared"
xmlns:conditionTypes="clr-namespace:Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes.EventConditionView"
x:DataType="conditionTypes:EventConditionViewModel">
<DockPanel HorizontalAlignment="Stretch">
<DockPanel.Styles>
<Style Selector="DockPanel > TextBlock">
<Setter Property="Margin" Value="0 5" />
</Style>
</DockPanel.Styles>
<TextBlock DockPanel.Dock="Top">Triggered by event</TextBlock>
<dataModelPicker:DataModelPickerButton Placeholder="Select an event"
DockPanel.Dock="Top"
HorizontalAlignment="Stretch"
FilterTypes="{CompiledBinding FilterTypes}"
DataModelPath="{CompiledBinding EventPath}"/>
<TextBlock DockPanel.Dock="Top">When the event fires..</TextBlock>
<ComboBox PlaceholderText="Select a play mode" HorizontalAlignment="Stretch" DockPanel.Dock="Top" SelectedIndex="{CompiledBinding SelectedTriggerMode}">
<ComboBoxItem>
Play the timeline once
</ComboBoxItem>
<ComboBoxItem>
Toggle the element on or off
</ComboBoxItem>
</ComboBox>
<TextBlock DockPanel.Dock="Top" IsVisible="{CompiledBinding ShowOverlapOptions}">And if already playing..</TextBlock>
<ComboBox PlaceholderText="Select a play mode"
HorizontalAlignment="Stretch"
DockPanel.Dock="Top"
SelectedIndex="{CompiledBinding SelectedOverlapMode}"
IsVisible="{CompiledBinding ShowOverlapOptions}">
<ComboBoxItem>
Restart the timeline
</ComboBoxItem>
<ComboBoxItem IsEnabled="{CompiledBinding IsConditionForLayer}">
Play a second copy
</ComboBoxItem>
<ComboBoxItem>
Do nothing
</ComboBoxItem>
</ComboBox>
<Button DockPanel.Dock="Bottom"
ToolTip.Tip="Open editor"
Margin="0 15 0 5"
Command="{CompiledBinding OpenEditor}"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom">
Edit condition script
</Button>
</DockPanel>
</UserControl>

View File

@ -0,0 +1,17 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes;
public class EventConditionView : ReactiveUserControl<EventConditionViewModel>
{
public EventConditionView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

View File

@ -0,0 +1,73 @@
using System;
using System.Collections.ObjectModel;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Screens.VisualScripting;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes;
public class EventConditionViewModel : ActivatableViewModelBase
{
private readonly EventCondition _eventCondition;
private readonly IProfileEditorService _profileEditorService;
private readonly ObservableAsPropertyHelper<bool> _showOverlapOptions;
private readonly IWindowService _windowService;
private ObservableAsPropertyHelper<DataModelPath?>? _eventPath;
private ObservableAsPropertyHelper<int>? _selectedOverlapMode;
private ObservableAsPropertyHelper<int>? _selectedTriggerMode;
public EventConditionViewModel(EventCondition eventCondition, IProfileEditorService profileEditorService, IWindowService windowService)
{
_eventCondition = eventCondition;
_profileEditorService = profileEditorService;
_windowService = windowService;
_showOverlapOptions = this.WhenAnyValue(vm => vm.SelectedTriggerMode)
.Select(m => m == 0)
.ToProperty(this, vm => vm.ShowOverlapOptions);
this.WhenActivated(d =>
{
_eventPath = eventCondition.WhenAnyValue(c => c.EventPath).ToProperty(this, vm => vm.EventPath).DisposeWith(d);
_selectedTriggerMode = eventCondition.WhenAnyValue(c => c.TriggerMode).Select(m => (int) m).ToProperty(this, vm => vm.SelectedTriggerMode).DisposeWith(d);
_selectedOverlapMode = eventCondition.WhenAnyValue(c => c.OverlapMode).Select(m => (int) m).ToProperty(this, vm => vm.SelectedOverlapMode).DisposeWith(d);
});
OpenEditor = ReactiveCommand.CreateFromTask(ExecuteOpenEditor);
}
public ObservableCollection<Type> FilterTypes { get; } = new() {typeof(IDataModelEvent)};
public ReactiveCommand<Unit, Unit> OpenEditor { get; }
public bool ShowOverlapOptions => _showOverlapOptions.Value;
public bool IsConditionForLayer => _eventCondition.ProfileElement is Layer;
public DataModelPath? EventPath
{
get => _eventPath?.Value != null ? new DataModelPath(_eventPath.Value) : null;
set => _profileEditorService.ExecuteCommand(new UpdateEventConditionPath(_eventCondition, value != null ? new DataModelPath(value) : null));
}
public int SelectedTriggerMode
{
get => _selectedTriggerMode?.Value ?? 0;
set => _profileEditorService.ExecuteCommand(new UpdateEventTriggerMode(_eventCondition, (EventTriggerMode) value));
}
public int SelectedOverlapMode
{
get => _selectedOverlapMode?.Value ?? 0;
set => _profileEditorService.ExecuteCommand(new UpdateEventOverlapMode(_eventCondition, (EventOverlapMode) value));
}
private async Task ExecuteOpenEditor()
{
await _windowService.ShowDialogAsync<NodeScriptWindowViewModel, bool>(("nodeScript", _eventCondition.NodeScript));
}
}

View File

@ -0,0 +1,15 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes.PlayOnceConditionView">
<DockPanel VerticalAlignment="Top" Margin="0 5">
<avalonia:MaterialIcon Kind="InfoCircle" Margin="5 0"/>
<TextBlock Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap">
Every segment of the timeline is played once.
</TextBlock>
</DockPanel>
</UserControl>

View File

@ -0,0 +1,17 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes;
public class PlayOnceConditionView : UserControl
{
public PlayOnceConditionView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

View File

@ -0,0 +1,14 @@
using Artemis.Core;
using Artemis.UI.Shared;
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes;
public class PlayOnceConditionViewModel : ViewModelBase
{
private readonly PlayOnceCondition _playOnceCondition;
public PlayOnceConditionViewModel(PlayOnceCondition playOnceCondition)
{
_playOnceCondition = playOnceCondition;
}
}

View File

@ -0,0 +1,45 @@
<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:conditionTypes="clr-namespace:Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes.StaticConditionView"
x:DataType="conditionTypes:StaticConditionViewModel">
<DockPanel HorizontalAlignment="Stretch">
<DockPanel.Styles>
<Style Selector="DockPanel > TextBlock">
<Setter Property="Margin" Value="0 5" />
</Style>
</DockPanel.Styles>
<TextBlock DockPanel.Dock="Top">While condition is met..</TextBlock>
<ComboBox DockPanel.Dock="Top" PlaceholderText="Select a play mode" HorizontalAlignment="Stretch" SelectedIndex="{CompiledBinding SelectedPlayMode}">
<ComboBoxItem>Repeat the main segment</ComboBoxItem>
<ComboBoxItem>Play the main segment once</ComboBoxItem>
</ComboBox>
<TextBlock DockPanel.Dock="Top">And when no longer met..</TextBlock>
<ComboBox DockPanel.Dock="Top" PlaceholderText="Select a stop mode" HorizontalAlignment="Stretch" SelectedIndex="{CompiledBinding SelectedStopMode}">
<ComboBoxItem>Finish the main segment</ComboBoxItem>
<ComboBoxItem>Skip forward to the end segment</ComboBoxItem>
</ComboBox>
<DockPanel DockPanel.Dock="Top" Margin="0 5">
<avalonia:MaterialIcon Kind="InfoCircle" Margin="5 0" />
<TextBlock Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap">
The start- and end-segments are played once each time the condition is met.
</TextBlock>
</DockPanel>
<Button DockPanel.Dock="Bottom"
ToolTip.Tip="Open editor"
Margin="0 15 0 5"
Command="{CompiledBinding OpenEditor}"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom">
Edit condition script
</Button>
</DockPanel>
</UserControl>

View File

@ -0,0 +1,17 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes;
public class StaticConditionView : ReactiveUserControl<StaticConditionViewModel>
{
public StaticConditionView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

View File

@ -0,0 +1,56 @@
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Screens.VisualScripting;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes;
public class StaticConditionViewModel : ActivatableViewModelBase
{
private readonly IProfileEditorService _profileEditorService;
private readonly StaticCondition _staticCondition;
private readonly IWindowService _windowService;
private ObservableAsPropertyHelper<int>? _selectedPlayMode;
private ObservableAsPropertyHelper<int>? _selectedStopMode;
public StaticConditionViewModel(StaticCondition staticCondition, IProfileEditorService profileEditorService, IWindowService windowService)
{
_staticCondition = staticCondition;
_profileEditorService = profileEditorService;
_windowService = windowService;
this.WhenActivated(d =>
{
_selectedPlayMode = staticCondition.WhenAnyValue(c => c.PlayMode).Select(m => (int) m).ToProperty(this, vm => vm.SelectedPlayMode).DisposeWith(d);
_selectedStopMode = staticCondition.WhenAnyValue(c => c.StopMode).Select(m => (int) m).ToProperty(this, vm => vm.SelectedStopMode).DisposeWith(d);
});
OpenEditor = ReactiveCommand.CreateFromTask(ExecuteOpenEditor);
}
public ReactiveCommand<Unit, Unit> OpenEditor { get; }
public int SelectedPlayMode
{
get => _selectedPlayMode?.Value ?? 0;
set => _profileEditorService.ExecuteCommand(new UpdateStaticPlayMode(_staticCondition, (StaticPlayMode) value));
}
public int SelectedStopMode
{
get => _selectedStopMode?.Value ?? 0;
set => _profileEditorService.ExecuteCommand(new UpdateStaticStopMode(_staticCondition, (StaticStopMode) value));
}
private async Task ExecuteOpenEditor()
{
await _windowService.ShowDialogAsync<NodeScriptWindowViewModel, bool>(("nodeScript", _staticCondition.NodeScript));
}
}

View File

@ -3,8 +3,6 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:displayCondition="clr-namespace:Artemis.UI.Screens.ProfileEditor.DisplayCondition"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:dataModelPicker="clr-namespace:Artemis.UI.Shared.Controls.DataModelPicker;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="200" d:DesignHeight="650"
x:Class="Artemis.UI.Screens.ProfileEditor.DisplayCondition.DisplayConditionScriptView"
x:DataType="displayCondition:DisplayConditionScriptViewModel">
@ -20,19 +18,14 @@
</Style>
</UserControl.Styles>
<ScrollViewer>
<DockPanel LastChildFill="False">
<DockPanel.Styles>
<Style Selector="DockPanel > TextBlock">
<Setter Property="Margin" Value="0 5"></Setter>
</Style>
</DockPanel.Styles>
<TextBlock DockPanel.Dock="Top">Condition type</TextBlock>
<Grid RowDefinitions="Auto,Auto,*">
<TextBlock Grid.Row="0" Margin="0 5">Activation type</TextBlock>
<ComboBox Name="ConditionType"
DockPanel.Dock="Top"
Grid.Row="1"
Classes="condition-type"
PlaceholderText="Select a condition type"
PlaceholderText="Select an activation type"
Items="{CompiledBinding ConditionTypeViewModels}"
IsEnabled="{CompiledBinding ProfileElement, Converter={x:Static ObjectConverters.IsNotNull}}"
SelectedItem="{CompiledBinding SelectedConditionTypeViewModel}"
HorizontalAlignment="Stretch">
<ComboBox.ItemTemplate>
@ -44,64 +37,7 @@
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<!-- Event options -->
<DockPanel IsVisible="{CompiledBinding ShowEventOptions}" DockPanel.Dock="Top" HorizontalAlignment="Stretch">
<TextBlock DockPanel.Dock="Top">Triggered by event</TextBlock>
<dataModelPicker:DataModelPickerButton Placeholder="Select an event"
DockPanel.Dock="Top"
HorizontalAlignment="Stretch" />
<TextBlock DockPanel.Dock="Top">When the event fires..</TextBlock>
<ComboBox PlaceholderText="Select a play mode" HorizontalAlignment="Stretch" DockPanel.Dock="Top">
<ComboBoxItem>
Play the timeline once
</ComboBoxItem>
<ComboBoxItem>
Toggle the element on or off
</ComboBoxItem>
</ComboBox>
<TextBlock DockPanel.Dock="Top">And if already playing..</TextBlock>
<ComboBox PlaceholderText="Select a play mode" HorizontalAlignment="Stretch" DockPanel.Dock="Top">
<ComboBoxItem>
Restart the timeline
</ComboBoxItem>
<ComboBoxItem>
Play a second copy
</ComboBoxItem>
<ComboBoxItem>
Do nothing
</ComboBoxItem>
</ComboBox>
</DockPanel>
<!-- Static options -->
<DockPanel IsVisible="{CompiledBinding ShowStaticOptions}" HorizontalAlignment="Stretch" DockPanel.Dock="Top">
<TextBlock DockPanel.Dock="Top">While condition is met..</TextBlock>
<ComboBox DockPanel.Dock="Top" PlaceholderText="Select a play mode" HorizontalAlignment="Stretch">
<ComboBoxItem>Play the main segment once</ComboBoxItem>
<ComboBoxItem>Repeat the main segment</ComboBoxItem>
</ComboBox>
<TextBlock DockPanel.Dock="Top">And when no longer met..</TextBlock>
<ComboBox DockPanel.Dock="Top" PlaceholderText="Select a stop mode" HorizontalAlignment="Stretch">
<ComboBoxItem>Finish the main segment</ComboBoxItem>
<ComboBoxItem>Skip forward to the end segment</ComboBoxItem>
</ComboBox>
<DockPanel DockPanel.Dock="Top" Margin="0 5">
<avalonia:MaterialIcon Kind="InfoCircle" Margin="5 0"></avalonia:MaterialIcon>
<TextBlock Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap">
The start- and end-segments are always played once.
</TextBlock>
</DockPanel>
</DockPanel>
<Button DockPanel.Dock="Bottom" ToolTip.Tip="Open editor" Margin="0 15 0 5" Command="{Binding OpenEditor}" HorizontalAlignment="Stretch">
Edit condition script
</Button>
</DockPanel>
<ContentControl Grid.Row="2" Content="{CompiledBinding ConditionViewModel}" />
</Grid>
</ScrollViewer>
</UserControl>

View File

@ -1,18 +1,17 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition
{
public partial class DisplayConditionScriptView : ReactiveUserControl<DisplayConditionScriptViewModel>
{
public DisplayConditionScriptView()
{
InitializeComponent();
}
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition;
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public class DisplayConditionScriptView : ReactiveUserControl<DisplayConditionScriptViewModel>
{
public DisplayConditionScriptView()
{
InitializeComponent();
}
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

View File

@ -1,13 +1,9 @@
using System;
using System.Collections.ObjectModel;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.VisualScripting;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor;
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
using Avalonia.Controls.Mixins;
@ -17,81 +13,79 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition;
public class DisplayConditionScriptViewModel : ActivatableViewModelBase
{
private readonly ObservableAsPropertyHelper<ViewModelBase?> _conditionViewModel;
private readonly IConditionVmFactory _conditionVmFactory;
private readonly IProfileEditorService _profileEditorService;
private readonly ObservableAsPropertyHelper<bool> _showEventOptions;
private readonly ObservableAsPropertyHelper<bool> _showStaticOptions;
private readonly IWindowService _windowService;
private ObservableAsPropertyHelper<NodeScriptViewModel?>? _nodeScriptViewModel;
private RenderProfileElement? _profileElement;
private ObservableAsPropertyHelper<RenderProfileElement?>? _profileElement;
private ObservableAsPropertyHelper<ConditionTypeViewModel?>? _selectedConditionTypeViewModel;
public DisplayConditionScriptViewModel(IProfileEditorService profileEditorService, INodeVmFactory nodeVmFactory, IWindowService windowService)
public DisplayConditionScriptViewModel(IProfileEditorService profileEditorService, IConditionVmFactory conditionVmFactory)
{
_profileEditorService = profileEditorService;
_windowService = windowService;
_conditionVmFactory = conditionVmFactory;
ConditionTypeViewModels = new ObservableCollection<ConditionTypeViewModel>
{
new("None", "The element is always active.", null),
new("Regular", "The element is activated when the provided visual script ends in true.", typeof(StaticCondition)),
new("Event", "The element is activated when the selected event fires.\r\n" +
"Events that contain data can conditionally trigger the layer using a visual script.", typeof(EventCondition))
new("Always", "The element is always active.", typeof(AlwaysOnCondition)),
new("Once", "The element is shown once until its timeline is finished.", typeof(PlayOnceCondition)),
new("Conditional", "The element is activated when the provided visual script ends in true.", typeof(StaticCondition)),
new("On event", "The element is activated when the selected event fires.\r\n" +
"Events that contain data can conditionally trigger the layer using a visual script.", typeof(EventCondition))
};
this.WhenActivated(d =>
{
profileEditorService.ProfileElement.Subscribe(p => _profileElement = p).DisposeWith(d);
_nodeScriptViewModel = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.DisplayCondition) ?? Observable.Never<ICondition?>())
.Switch()
.Select(c => c is INodeScriptCondition {NodeScript: NodeScript nodeScript} ? nodeVmFactory.NodeScriptViewModel(nodeScript, true) : null)
.ToProperty(this, vm => vm.NodeScriptViewModel)
.DisposeWith(d);
_profileElement = profileEditorService.ProfileElement.ToProperty(this, vm => vm.ProfileElement).DisposeWith(d);
_selectedConditionTypeViewModel = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.DisplayCondition) ?? Observable.Never<ICondition?>())
.Select(p => p?.WhenAnyValue(element => element.DisplayCondition) ?? Observable.Return<ICondition?>(null))
.Switch()
.Select(c => c != null ? ConditionTypeViewModels.FirstOrDefault(vm => vm.ConditionType == c.GetType()) : null)
.ToProperty(this, vm => vm.SelectedConditionTypeViewModel)
.DisposeWith(d);
});
_showStaticOptions = this.WhenAnyValue(vm => vm.SelectedConditionTypeViewModel)
.Select(c => c != null && c.ConditionType == typeof(StaticCondition))
.ToProperty(this, vm => vm.ShowStaticOptions);
_showEventOptions = this.WhenAnyValue(vm => vm.SelectedConditionTypeViewModel)
.Select(c => c != null && c.ConditionType == typeof(EventCondition))
.ToProperty(this, vm => vm.ShowEventOptions);
_conditionViewModel = this.WhenAnyValue(vm => vm.SelectedConditionTypeViewModel).Select(_ => CreateConditionViewModel()).ToProperty(this, vm => vm.ConditionViewModel);
}
public NodeScriptViewModel? NodeScriptViewModel => _nodeScriptViewModel?.Value;
public RenderProfileElement? ProfileElement => _profileElement?.Value;
public ViewModelBase? ConditionViewModel => _conditionViewModel.Value;
public ObservableCollection<ConditionTypeViewModel> ConditionTypeViewModels { get; }
public ConditionTypeViewModel? SelectedConditionTypeViewModel
{
get => _selectedConditionTypeViewModel?.Value;
set
{
if (_profileElement == null)
return;
ICondition? condition = null;
if (value?.ConditionType == typeof(StaticCondition))
condition = new StaticCondition(_profileElement);
else if (value?.ConditionType == typeof(EventCondition))
condition = new EventCondition(_profileElement);
_profileEditorService.ExecuteCommand(new ChangeConditionType(_profileElement, condition));
}
set => ApplyConditionType(value);
}
public bool ShowStaticOptions => _showStaticOptions.Value;
public bool ShowEventOptions => _showEventOptions.Value;
public async Task OpenEditor()
private ViewModelBase? CreateConditionViewModel()
{
if (_profileElement?.DisplayCondition is StaticCondition staticCondition)
await _windowService.ShowDialogAsync<NodeScriptWindowViewModel, bool>(("nodeScript", staticCondition.Script));
if (ProfileElement == null)
return null;
if (ProfileElement.DisplayCondition is AlwaysOnCondition alwaysOnCondition)
return _conditionVmFactory.AlwaysOnConditionViewModel(alwaysOnCondition);
if (ProfileElement.DisplayCondition is PlayOnceCondition playOnceCondition)
return _conditionVmFactory.PlayOnceConditionViewModel(playOnceCondition);
if (ProfileElement.DisplayCondition is StaticCondition staticCondition)
return _conditionVmFactory.StaticConditionViewModel(staticCondition);
if (ProfileElement.DisplayCondition is EventCondition eventCondition)
return _conditionVmFactory.EventConditionViewModel(eventCondition);
return null;
}
private void ApplyConditionType(ConditionTypeViewModel? value)
{
if (ProfileElement == null || value == null || ProfileElement.DisplayCondition.GetType() == value.ConditionType)
return;
if (value.ConditionType == typeof(AlwaysOnCondition))
_profileEditorService.ExecuteCommand(new ChangeConditionType(ProfileElement, new AlwaysOnCondition(ProfileElement)));
if (value.ConditionType == typeof(PlayOnceCondition))
_profileEditorService.ExecuteCommand(new ChangeConditionType(ProfileElement, new PlayOnceCondition(ProfileElement)));
if (value.ConditionType == typeof(StaticCondition))
_profileEditorService.ExecuteCommand(new ChangeConditionType(ProfileElement, new StaticCondition(ProfileElement)));
if (value.ConditionType == typeof(EventCondition))
_profileEditorService.ExecuteCommand(new ChangeConditionType(ProfileElement, new EventCondition(ProfileElement)));
}
}

View File

@ -46,13 +46,6 @@
Command="{Binding DisableSegment}">
<avalonia:MaterialIcon Kind="CloseCircle" />
</Button>
<ToggleButton Name="SegmentRepeat"
Classes="icon-button icon-button-small"
ToolTip.Tip="Repeat this segment"
IsChecked="{CompiledBinding RepeatSegment}"
Padding="0">
<avalonia:MaterialIcon Kind="Repeat" />
</ToggleButton>
</StackPanel>
<Button Name="AddEndSegment"

View File

@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.Shared.Services.ProfileEditor;
@ -13,12 +10,11 @@ namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
public class MainSegmentViewModel : TimelineSegmentViewModel
{
private RenderProfileElement? _profileElement;
private ObservableAsPropertyHelper<double>? _start;
private readonly ObservableAsPropertyHelper<double> _width;
private ObservableAsPropertyHelper<double>? _end;
private ObservableAsPropertyHelper<string?>? _endTimestamp;
private ObservableAsPropertyHelper<bool>? _repeatSegment;
private readonly ObservableAsPropertyHelper<double> _width;
private RenderProfileElement? _profileElement;
private ObservableAsPropertyHelper<double>? _start;
public MainSegmentViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
{
@ -44,12 +40,6 @@ public class MainSegmentViewModel : TimelineSegmentViewModel
.Select(p => $"{Math.Floor(p.TotalSeconds):00}.{p.Milliseconds:000}")
.ToProperty(this, vm => vm.EndTimestamp)
.DisposeWith(d);
_repeatSegment = profileEditorService.ProfileElement
.Select(p => p?.WhenAnyValue(element => element.Timeline.PlayMode) ?? Observable.Never<TimelinePlayMode>())
.Switch()
.Select(p => p == TimelinePlayMode.Repeat)
.ToProperty(this, vm => vm.RepeatSegment)
.DisposeWith(d);
});
_width = this.WhenAnyValue(vm => vm.StartX, vm => vm.EndX).Select(t => t.Item2 - t.Item1).ToProperty(this, vm => vm.Width);
@ -73,10 +63,4 @@ public class MainSegmentViewModel : TimelineSegmentViewModel
public override double Width => _width.Value;
public override string? EndTimestamp => _endTimestamp?.Value;
public override ResizeTimelineSegment.SegmentType Type => ResizeTimelineSegment.SegmentType.Main;
public bool RepeatSegment
{
get => _repeatSegment?.Value ?? false;
set => throw new NotImplementedException();
}
}

View File

@ -22,7 +22,7 @@ namespace Artemis.UI.Screens.SurfaceEditor
public SurfaceEditorViewModel(IScreen hostScreen,
IRgbService rgbService,
SurfaceVmFactory surfaceVmFactory,
ISurfaceVmFactory surfaceVmFactory,
ISettingsService settingsService) : base(hostScreen, "surface-editor")
{
_rgbService = rgbService;

View File

@ -3,9 +3,30 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:visualScripting="clr-namespace:Artemis.UI.Screens.VisualScripting"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.VisualScripting.NodeScriptWindowView"
x:DataType="visualScripting:NodeScriptWindowViewModel"
Title="VisualEditorWindowView">
<ContentControl Content="{CompiledBinding NodeScriptViewModel}" />
Title="Artemis | Visual Script Editor"
Width="1200"
Height="800">
<Grid Margin="15" ColumnDefinitions="*,*" RowDefinitions="Auto,*">
<StackPanel Grid.Row="0">
<TextBlock Classes="h4" Text="{CompiledBinding NodeScript.Name}"/>
<TextBlock Classes="subtitle" Margin="0 0 0 10" Text="{CompiledBinding NodeScript.Description}"/>
</StackPanel>
<controls:HyperlinkButton Grid.Row="0"
Grid.Column="1"
VerticalAlignment="Top"
HorizontalAlignment="Right"
NavigateUri="https://wiki.artemis-rgb.com/en/guides/user/profiles/conditions">
Learn more about visual scripts
</controls:HyperlinkButton>
<Border Classes="card" Grid.Row="1" Grid.ColumnSpan="2">
<ContentControl Content="{CompiledBinding NodeScriptViewModel}" />
</Border>
</Grid>
</Window>

View File

@ -5,7 +5,7 @@ using Artemis.VisualScripting.Nodes.Operators.Screens;
namespace Artemis.VisualScripting.Nodes.Operators;
[Node("Enum Equals", "Determines the equality between an input and a selected enum value", "Operators", InputType = typeof(Enum), OutputType = typeof(bool))]
public class EnumEqualsNode : Node<Enum, EnumEqualsNodeCustomViewModel>
public class EnumEqualsNode : Node<int, EnumEqualsNodeCustomViewModel>
{
public EnumEqualsNode() : base("Enum Equals", "Determines the equality between an input and a selected enum value")
{
@ -17,20 +17,18 @@ public class EnumEqualsNode : Node<Enum, EnumEqualsNodeCustomViewModel>
private void InputPinOnPinConnected(object? sender, SingleValueEventArgs<IPin> e)
{
if (Storage?.GetType() != InputPin.ConnectedTo.First().Type)
Storage = Enum.GetValues(InputPin.ConnectedTo.First().Type).Cast<Enum>().FirstOrDefault();
Storage = 0;
}
public InputPin<Enum> InputPin { get; }
public OutputPin<bool> OutputPin { get; }
#region Overrides of Node
/// <inheritdoc />
public override void Evaluate()
{
OutputPin.Value = InputPin.Value != null && InputPin.Value.Equals(Storage);
if (InputPin.Value == null)
OutputPin.Value = false;
else
OutputPin.Value = Convert.ToInt32(InputPin.Value) == Storage;
}
#endregion
}

View File

@ -8,7 +8,7 @@
x:DataType="screens:EnumEqualsNodeCustomViewModel">
<ComboBox IsEnabled="{CompiledBinding EnumValues.Count}"
Items="{CompiledBinding EnumValues}"
SelectedItem="{CompiledBinding CurrentValue}"
SelectedIndex="{CompiledBinding CurrentValue}"
PlaceholderText="Connect a pin..."
Classes="condensed"
VerticalAlignment="Center" />

View File

@ -33,11 +33,7 @@ public class EnumEqualsNodeCustomViewModel : CustomNodeViewModel
}
Observable.FromEventPattern<SingleValueEventArgs<IPin>>(x => _node.InputPin.PinConnected += x, x => _node.InputPin.PinConnected -= x)
.Subscribe(p =>
{
EnumValues.AddRange(Enum.GetValues(p.EventArgs.Value.Type).Cast<Enum>());
this.RaisePropertyChanged(nameof(CurrentValue));
})
.Subscribe(p => EnumValues.AddRange(Enum.GetValues(p.EventArgs.Value.Type).Cast<Enum>()))
.DisposeWith(d);
Observable.FromEventPattern<SingleValueEventArgs<IPin>>(x => _node.InputPin.PinDisconnected += x, x => _node.InputPin.PinDisconnected -= x)
.Subscribe(_ => EnumValues.Clear())
@ -47,13 +43,13 @@ public class EnumEqualsNodeCustomViewModel : CustomNodeViewModel
public ObservableCollection<Enum> EnumValues { get; } = new();
public Enum? CurrentValue
public int CurrentValue
{
get => _node.Storage;
set
{
if (value != null && !Equals(_node.Storage, value))
_nodeEditorService.ExecuteCommand(Script, new UpdateStorage<Enum>(_node, value));
if (!Equals(_node.Storage, value))
_nodeEditorService.ExecuteCommand(Script, new UpdateStorage<int>(_node, value));
}
}
}