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,8 +4,8 @@ using Artemis.Core.Internal;
using Artemis.Storage.Entities.Profile.Abstract; using Artemis.Storage.Entities.Profile.Abstract;
using Artemis.Storage.Entities.Profile.Conditions; using Artemis.Storage.Entities.Profile.Conditions;
namespace Artemis.Core namespace Artemis.Core;
{
/// <summary> /// <summary>
/// Represents a condition that is based on a <see cref="DataModelEvent" /> /// Represents a condition that is based on a <see cref="DataModelEvent" />
/// </summary> /// </summary>
@ -13,36 +13,43 @@ namespace Artemis.Core
{ {
private readonly string _displayName; private readonly string _displayName;
private readonly EventConditionEntity _entity; private readonly EventConditionEntity _entity;
private EventDefaultNode? _eventNode; private readonly EventDefaultNode _eventNode;
private TimeLineEventOverlapMode _eventOverlapMode;
private DataModelPath? _eventPath; private DataModelPath? _eventPath;
private DateTime _lastProcessedTrigger; private DateTime _lastProcessedTrigger;
private NodeScript<bool>? _script; private EventOverlapMode _overlapMode;
private NodeScript<bool> _script;
private EventTriggerMode _triggerMode;
private bool _wasMet;
/// <summary> /// <summary>
/// Creates a new instance of the <see cref="EventCondition" /> class /// Creates a new instance of the <see cref="EventCondition" /> class
/// </summary> /// </summary>
public EventCondition(ProfileElement profileElement) public EventCondition(RenderProfileElement profileElement)
{ {
ProfileElement = profileElement;
_entity = new EventConditionEntity(); _entity = new EventConditionEntity();
_displayName = profileElement.GetType().Name; _displayName = profileElement.GetType().Name;
_eventNode = new EventDefaultNode {X = -300};
ProfileElement = profileElement; _script = new NodeScript<bool>($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile);
} }
internal EventCondition(EventConditionEntity entity, ProfileElement profileElement) internal EventCondition(EventConditionEntity entity, RenderProfileElement profileElement)
{ {
ProfileElement = profileElement;
_entity = entity; _entity = entity;
_displayName = profileElement.GetType().Name; _displayName = profileElement.GetType().Name;
_eventNode = new EventDefaultNode();
_script = null!;
ProfileElement = profileElement;
Load(); Load();
} }
/// <summary> /// <summary>
/// Gets the script that drives the event condition /// Gets the script that drives the event condition
/// </summary> /// </summary>
public NodeScript<bool>? Script public NodeScript<bool> Script
{ {
get => _script; get => _script;
set => SetAndNotify(ref _script, value); set => SetAndNotify(ref _script, value);
@ -58,12 +65,22 @@ namespace Artemis.Core
} }
/// <summary> /// <summary>
/// Gets or sets how the condition behaves when events trigger before the timeline finishes /// Gets or sets how the condition behaves when the event fires.
/// </summary> /// </summary>
public TimeLineEventOverlapMode EventOverlapMode public EventTriggerMode TriggerMode
{ {
get => _eventOverlapMode; get => _triggerMode;
set => SetAndNotify(ref _eventOverlapMode, value); 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> /// <summary>
@ -71,34 +88,22 @@ namespace Artemis.Core
/// </summary> /// </summary>
public void UpdateEventNode() public void UpdateEventNode()
{ {
if (Script == null || EventPath?.GetValue() is not IDataModelEvent dataModelEvent) IDataModelEvent? dataModelEvent = EventPath?.GetValue() as IDataModelEvent;
return; _eventNode.CreatePins(dataModelEvent);
if (Script.Nodes.FirstOrDefault(n => n is EventDefaultNode) is EventDefaultNode existing) if (dataModelEvent != null && !Script.Nodes.Contains(_eventNode))
{
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); Script.AddNode(_eventNode);
else else if (dataModelEvent == null && Script.Nodes.Contains(_eventNode))
Script.RemoveNode(_eventNode); Script.RemoveNode(_eventNode);
} }
/// <summary> /// <summary>
/// Updates the <see cref="Script" /> with a new empty node script /// Gets the start node of the event script, if any
/// </summary> /// </summary>
public void CreateEmptyNodeScript() /// <returns>The start node of the event script, if any.</returns>
public INode GetStartNode()
{ {
Script?.Dispose(); return _eventNode;
Script = new NodeScript<bool>($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", ProfileElement.Profile);
UpdateEventNode();
} }
private bool Evaluate() private bool Evaluate()
@ -108,7 +113,7 @@ namespace Artemis.Core
_lastProcessedTrigger = dataModelEvent.LastTrigger; _lastProcessedTrigger = dataModelEvent.LastTrigger;
if (Script == null) if (!Script.ExitNodeConnected)
return true; return true;
Script.Run(); Script.Run();
@ -119,7 +124,7 @@ namespace Artemis.Core
public IConditionEntity Entity => _entity; public IConditionEntity Entity => _entity;
/// <inheritdoc /> /// <inheritdoc />
public ProfileElement ProfileElement { get; } public RenderProfileElement ProfileElement { get; }
/// <inheritdoc /> /// <inheritdoc />
public bool IsMet { get; private set; } public bool IsMet { get; private set; }
@ -127,7 +132,8 @@ namespace Artemis.Core
/// <inheritdoc /> /// <inheritdoc />
public void Update() public void Update()
{ {
if (EventOverlapMode == TimeLineEventOverlapMode.Toggle) _wasMet = IsMet;
if (TriggerMode == EventTriggerMode.Toggle)
{ {
if (Evaluate()) if (Evaluate())
IsMet = !IsMet; IsMet = !IsMet;
@ -139,31 +145,37 @@ namespace Artemis.Core
} }
/// <inheritdoc /> /// <inheritdoc />
public void ApplyToTimeline(bool isMet, bool wasMet, Timeline timeline) public void UpdateTimeline(double deltaTime)
{ {
if (!isMet) if (TriggerMode == EventTriggerMode.Toggle)
{ {
if (EventOverlapMode == TimeLineEventOverlapMode.Toggle) if (!IsMet && _wasMet)
timeline.JumpToEnd(); ProfileElement.Timeline.JumpToEnd();
return; else if (IsMet && !_wasMet)
ProfileElement.Timeline.JumpToStart();
} }
else
// Event overlap mode doesn't apply in this case
if (timeline.IsFinished)
{ {
timeline.JumpToStart(); if (IsMet && ProfileElement.Timeline.IsFinished)
return; {
ProfileElement.Timeline.JumpToStart();
} }
else if (IsMet)
// If the timeline was already running, look at the event overlap mode {
if (EventOverlapMode == TimeLineEventOverlapMode.Restart) if (OverlapMode == EventOverlapMode.Restart)
timeline.JumpToStart(); ProfileElement.Timeline.JumpToStart();
else if (EventOverlapMode == TimeLineEventOverlapMode.Copy && ProfileElement is Layer layer) else if (OverlapMode == EventOverlapMode.Copy && ProfileElement is Layer layer)
layer.CreateCopyAsChild(); layer.CreateCopyAsChild();
else if (EventOverlapMode == TimeLineEventOverlapMode.Toggle && !wasMet) }
timeline.JumpToStart(); }
// The remaining overlap mode is 'ignore' which requires no further action 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 /> /// <inheritdoc />
@ -178,18 +190,23 @@ namespace Artemis.Core
/// <inheritdoc /> /// <inheritdoc />
public void Load() public void Load()
{ {
EventOverlapMode = (TimeLineEventOverlapMode) _entity.EventOverlapMode; TriggerMode = (EventTriggerMode) _entity.TriggerMode;
if (_entity.Script != null) OverlapMode = (EventOverlapMode) _entity.OverlapMode;
Script = new NodeScript<bool>($"Activate {_displayName}", $"Whether or not the event should activate the {_displayName}", _entity.Script, ProfileElement.Profile);
if (_entity.EventPath != null) if (_entity.EventPath != null)
EventPath = new DataModelPath(_entity.EventPath); 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 /> /// <inheritdoc />
public void Save() public void Save()
{ {
_entity.EventOverlapMode = (int) EventOverlapMode; _entity.TriggerMode = (int) TriggerMode;
Script?.Save(); _entity.OverlapMode = (int) OverlapMode;
Script.Save();
_entity.Script = Script?.Entity; _entity.Script = Script?.Entity;
EventPath?.Save(); EventPath?.Save();
_entity.EventPath = EventPath?.Entity; _entity.EventPath = EventPath?.Entity;
@ -201,9 +218,6 @@ namespace Artemis.Core
/// <inheritdoc /> /// <inheritdoc />
public void LoadNodeScript() public void LoadNodeScript()
{ {
if (Script == null)
return;
Script.Load(); Script.Load();
UpdateEventNode(); UpdateEventNode();
Script.LoadConnections(); Script.LoadConnections();
@ -211,4 +225,41 @@ namespace Artemis.Core
#endregion #endregion
} }
/// <summary>
/// Represents a mode for render elements to start their timeline when display conditions events are fired.
/// </summary>
public enum EventTriggerMode
{
/// <summary>
/// Play the timeline once.
/// </summary>
Play,
/// <summary>
/// Toggle repeating the timeline.
/// </summary>
Toggle
}
/// <summary>
/// Represents a mode for render elements to configure the behaviour of events that overlap i.e. trigger again before
/// the timeline finishes.
/// </summary>
public enum EventOverlapMode
{
/// <summary>
/// Stop the current run and restart the timeline
/// </summary>
Restart,
/// <summary>
/// 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
} }

View File

@ -16,7 +16,7 @@ public interface ICondition : IDisposable, IStorageModel
/// <summary> /// <summary>
/// Gets the profile element this condition applies to /// Gets the profile element this condition applies to
/// </summary> /// </summary>
public ProfileElement ProfileElement { get; } public RenderProfileElement ProfileElement { get; }
/// <summary> /// <summary>
/// Gets a boolean indicating whether the condition is currently met /// Gets a boolean indicating whether the condition is currently met
@ -30,12 +30,11 @@ public interface ICondition : IDisposable, IStorageModel
void Update(); void Update();
/// <summary> /// <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> /// </summary>
/// <param name="isMet"></param> void UpdateTimeline(double deltaTime);
/// <param name="wasMet"></param>
/// <param name="timeline">The timeline to apply the display condition to</param> void OverrideTimeline(TimeSpan position);
void ApplyToTimeline(bool isMet, bool wasMet, Timeline timeline);
} }
/// <summary> /// <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; using Artemis.Storage.Entities.Profile.Conditions;
namespace Artemis.Core namespace Artemis.Core
@ -10,11 +11,14 @@ namespace Artemis.Core
{ {
private readonly string _displayName; private readonly string _displayName;
private readonly StaticConditionEntity _entity; private readonly StaticConditionEntity _entity;
private StaticPlayMode _playMode;
private StaticStopMode _stopMode;
private bool _wasMet;
/// <summary> /// <summary>
/// Creates a new instance of the <see cref="StaticCondition" /> class /// Creates a new instance of the <see cref="StaticCondition" /> class
/// </summary> /// </summary>
public StaticCondition(ProfileElement profileElement) public StaticCondition(RenderProfileElement profileElement)
{ {
_entity = new StaticConditionEntity(); _entity = new StaticConditionEntity();
_displayName = profileElement.GetType().Name; _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); 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; _entity = entity;
_displayName = profileElement.GetType().Name; _displayName = profileElement.GetType().Name;
@ -43,31 +47,66 @@ namespace Artemis.Core
public IConditionEntity Entity => _entity; public IConditionEntity Entity => _entity;
/// <inheritdoc /> /// <inheritdoc />
public ProfileElement ProfileElement { get; } public RenderProfileElement ProfileElement { get; }
/// <inheritdoc /> /// <inheritdoc />
public bool IsMet { get; private set; } public bool IsMet { get; private set; }
/// <summary>
/// Gets or sets the mode in which the render element starts its timeline when display conditions are met
/// </summary>
public StaticPlayMode 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 StaticStopMode StopMode
{
get => _stopMode;
set => SetAndNotify(ref _stopMode, value);
}
/// <inheritdoc /> /// <inheritdoc />
public void Update() public void Update()
{ {
if (!Script.ExitNodeConnected) _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 = true; IsMet = false;
return; return;
} }
if (!Script.ExitNodeConnected)
IsMet = true;
else
{
Script.Run(); Script.Run();
IsMet = Script.Result; IsMet = Script.Result;
} }
}
/// <inheritdoc /> /// <inheritdoc />
public void ApplyToTimeline(bool isMet, bool wasMet, Timeline timeline) public void UpdateTimeline(double deltaTime)
{ {
if (isMet && !wasMet && timeline.IsFinished) if (IsMet && !_wasMet && ProfileElement.Timeline.IsFinished)
timeline.JumpToStart(); ProfileElement.Timeline.JumpToStart();
else if (!isMet && wasMet && timeline.StopMode == TimelineStopMode.SkipToEnd) else if (!IsMet && _wasMet && StopMode == StaticStopMode.SkipToEnd)
timeline.JumpToEndSegment(); 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 /> /// <inheritdoc />
@ -81,12 +120,18 @@ namespace Artemis.Core
/// <inheritdoc /> /// <inheritdoc />
public void Load() 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); Script = new NodeScript<bool>($"Activate {_displayName}", $"Whether or not this {_displayName} should be active", _entity.Script, ProfileElement.Profile);
} }
/// <inheritdoc /> /// <inheritdoc />
public void Save() public void Save()
{ {
_entity.PlayMode = (int) PlayMode;
_entity.StopMode = (int) StopMode;
Script.Save(); Script.Save();
_entity.Script = Script.Entity; _entity.Script = Script.Entity;
} }
@ -102,4 +147,36 @@ namespace Artemis.Core
#endregion #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 /> /// <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)) foreach (BaseLayerEffect baseLayerEffect in LayerEffects.Where(e => !e.Suspended))
baseLayerEffect.InternalUpdate(Timeline); baseLayerEffect.InternalUpdate(Timeline); ;
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -476,9 +476,9 @@ namespace Artemis.Core
} }
/// <inheritdoc /> /// <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); General.Update(Timeline);
Transform.Update(Timeline); Transform.Update(Timeline);

View File

@ -27,6 +27,7 @@ namespace Artemis.Core
LayerEffectsList = new List<BaseLayerEffect>(); LayerEffectsList = new List<BaseLayerEffect>();
LayerEffects = new ReadOnlyCollection<BaseLayerEffect>(LayerEffectsList); LayerEffects = new ReadOnlyCollection<BaseLayerEffect>(LayerEffectsList);
Parent = parent ?? throw new ArgumentNullException(nameof(parent)); Parent = parent ?? throw new ArgumentNullException(nameof(parent));
_displayCondition = new AlwaysOnCondition(this);
LayerEffectStore.LayerEffectAdded += LayerEffectStoreOnLayerEffectAdded; LayerEffectStore.LayerEffectAdded += LayerEffectStoreOnLayerEffectAdded;
LayerEffectStore.LayerEffectRemoved += LayerEffectStoreOnLayerEffectRemoved; LayerEffectStore.LayerEffectRemoved += LayerEffectStoreOnLayerEffectRemoved;
@ -76,8 +77,10 @@ namespace Artemis.Core
DisplayCondition = RenderElementEntity.DisplayCondition switch DisplayCondition = RenderElementEntity.DisplayCondition switch
{ {
StaticConditionEntity staticConditionEntity => new StaticCondition(staticConditionEntity, this), AlwaysOnConditionEntity entity => new AlwaysOnCondition(entity, this),
EventConditionEntity eventConditionEntity => new EventCondition(eventConditionEntity, this), PlayOnceConditionEntity entity => new PlayOnceCondition(entity, this),
StaticConditionEntity entity => new StaticCondition(entity, this),
EventConditionEntity entity => new EventCondition(entity, this),
_ => DisplayCondition _ => DisplayCondition
}; };
@ -124,17 +127,11 @@ namespace Artemis.Core
public Timeline Timeline { get; private set; } public Timeline Timeline { get; private set; }
/// <summary> /// <summary>
/// Updates the <see cref="Timeline" /> according to the provided <paramref name="deltaTime" /> and current display /// Updates the <see cref="Timeline" /> according to the provided <paramref name="deltaTime" /> and current display condition
/// condition status
/// </summary> /// </summary>
protected void UpdateTimeline(double deltaTime) protected void UpdateTimeline(double deltaTime)
{ {
// TODO: Move to conditions DisplayCondition.UpdateTimeline(deltaTime);
// 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);
} }
#endregion #endregion
@ -365,13 +362,13 @@ namespace Artemis.Core
/// <summary> /// <summary>
/// Gets or sets the display condition used to determine whether this element is active or not /// Gets or sets the display condition used to determine whether this element is active or not
/// </summary> /// </summary>
public ICondition? DisplayCondition public ICondition DisplayCondition
{ {
get => _displayCondition; get => _displayCondition;
set => SetAndNotify(ref _displayCondition, value); set => SetAndNotify(ref _displayCondition, value);
} }
private ICondition? _displayCondition; private ICondition _displayCondition;
/// <summary> /// <summary>
/// Evaluates the display conditions on this element and applies any required changes to the <see cref="Timeline" /> /// 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; return;
} }
if (DisplayCondition == null)
{
DisplayConditionMet = true;
return;
}
DisplayCondition.Update(); DisplayCondition.Update();
DisplayCondition.ApplyToTimeline(DisplayCondition.IsMet, DisplayConditionMet, Timeline);
DisplayConditionMet = DisplayCondition.IsMet; DisplayConditionMet = DisplayCondition.IsMet;
} }
@ -401,7 +391,6 @@ namespace Artemis.Core
/// Overrides the main timeline to the specified time and clears any extra time lines /// Overrides the main timeline to the specified time and clears any extra time lines
/// </summary> /// </summary>
/// <param name="position">The position to set the timeline to</param> /// <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);
public abstract void OverrideTimelineAndApply(TimeSpan position, bool stickToMainSegment);
} }
} }

View File

@ -41,8 +41,7 @@ public class Timeline : CorePropertyChanged, IStorageModel
private TimeSpan _position; private TimeSpan _position;
private TimeSpan _lastDelta; private TimeSpan _lastDelta;
private TimelinePlayMode _playMode;
private TimelineStopMode _stopMode;
private TimeSpan _startSegmentLength; private TimeSpan _startSegmentLength;
private TimeSpan _mainSegmentLength; private TimeSpan _mainSegmentLength;
private TimeSpan _endSegmentLength; private TimeSpan _endSegmentLength;
@ -67,23 +66,7 @@ public class Timeline : CorePropertyChanged, IStorageModel
private set => SetAndNotify(ref _lastDelta, value); 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> /// <summary>
/// Gets a boolean indicating whether the timeline has finished its run /// Gets a boolean indicating whether the timeline has finished its run
@ -388,8 +371,6 @@ public class Timeline : CorePropertyChanged, IStorageModel
StartSegmentLength = Entity.StartSegmentLength; StartSegmentLength = Entity.StartSegmentLength;
MainSegmentLength = Entity.MainSegmentLength; MainSegmentLength = Entity.MainSegmentLength;
EndSegmentLength = Entity.EndSegmentLength; EndSegmentLength = Entity.EndSegmentLength;
PlayMode = (TimelinePlayMode) Entity.PlayMode;
StopMode = (TimelineStopMode) Entity.StopMode;
JumpToEnd(); JumpToEnd();
} }
@ -400,8 +381,6 @@ public class Timeline : CorePropertyChanged, IStorageModel
Entity.StartSegmentLength = StartSegmentLength; Entity.StartSegmentLength = StartSegmentLength;
Entity.MainSegmentLength = MainSegmentLength; Entity.MainSegmentLength = MainSegmentLength;
Entity.EndSegmentLength = EndSegmentLength; Entity.EndSegmentLength = EndSegmentLength;
Entity.PlayMode = (int) PlayMode;
Entity.StopMode = (int) StopMode;
} }
#endregion #endregion
@ -413,61 +392,3 @@ internal enum TimelineSegment
Main, Main,
End 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.Linq;
using System.Reflection; using System.Reflection;
using Artemis.Core.Modules; using Artemis.Core.Modules;
@ -9,6 +10,7 @@ namespace Artemis.Core.Internal
internal class EventDefaultNode : Node internal class EventDefaultNode : Node
{ {
private readonly Dictionary<PropertyInfo, OutputPin> _propertyPins; private readonly Dictionary<PropertyInfo, OutputPin> _propertyPins;
private readonly List<OutputPin> _pinBucket = new();
private IDataModelEvent? _dataModelEvent; private IDataModelEvent? _dataModelEvent;
public EventDefaultNode() : base("Event Arguments", "Contains the event arguments that triggered the evaluation") 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 override bool IsDefaultNode => true;
public void UpdateDataModelEvent(IDataModelEvent dataModelEvent) public void CreatePins(IDataModelEvent? dataModelEvent)
{ {
if (_dataModelEvent == dataModelEvent) if (_dataModelEvent == dataModelEvent)
return; return;
foreach (var (_, outputPin) in _propertyPins) while (Pins.Any())
RemovePin(outputPin); RemovePin((Pin) Pins.First());
_propertyPins.Clear(); _propertyPins.Clear();
_dataModelEvent = dataModelEvent; _dataModelEvent = dataModelEvent;
if (dataModelEvent == null)
return;
foreach (PropertyInfo propertyInfo in dataModelEvent.ArgumentsType.GetProperties(BindingFlags.Instance | BindingFlags.Public) foreach (PropertyInfo propertyInfo in dataModelEvent.ArgumentsType.GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(p => p.CustomAttributes.All(a => a.AttributeType != typeof(DataModelIgnoreAttribute)))) .Where(p => p.CustomAttributes.All(a => a.AttributeType != typeof(DataModelIgnoreAttribute))))
_propertyPins.Add(propertyInfo, CreateOutputPin(propertyInfo.PropertyType, propertyInfo.Name.Humanize())); _propertyPins.Add(propertyInfo, CreateOrAddOutputPin(propertyInfo.PropertyType, propertyInfo.Name.Humanize()));
} }
public override void Evaluate() public override void Evaluate()
@ -38,11 +43,38 @@ namespace Artemis.Core.Internal
if (_dataModelEvent?.LastEventArgumentsUntyped == null) if (_dataModelEvent?.LastEventArgumentsUntyped == null)
return; return;
foreach (var (propertyInfo, outputPin) in _propertyPins) foreach ((PropertyInfo propertyInfo, OutputPin outputPin) in _propertyPins)
{ {
if (outputPin.ConnectedTo.Any()) if (outputPin.ConnectedTo.Any())
outputPin.Value = propertyInfo.GetValue(_dataModelEvent.LastEventArgumentsUntyped) ?? outputPin.Type.GetDefault()!; 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 /// Serializes the <see cref="Storage" /> object into a string
/// </summary> /// </summary>
/// <returns>The serialized object</returns> /// <returns>The serialized object</returns>
internal virtual string SerializeStorage() public virtual string SerializeStorage()
{ {
return string.Empty; return string.Empty;
} }
@ -289,7 +289,7 @@ public abstract class Node : CorePropertyChanged, INode
/// Deserializes the <see cref="Storage" /> object and sets it /// Deserializes the <see cref="Storage" /> object and sets it
/// </summary> /// </summary>
/// <param name="serialized">The serialized object</param> /// <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> /// </summary>
public event EventHandler? StorageModified; public event EventHandler? StorageModified;
internal override string SerializeStorage() /// <inheritdoc />
public override string SerializeStorage()
{ {
return CoreJson.SerializeObject(Storage, true); 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); 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 class EventConditionEntity : IConditionEntity
{ {
public int EventOverlapMode { get; set; } public int TriggerMode { get; set; }
public int OverlapMode { get; set; }
public DataModelPathEntity EventPath { get; set; } public DataModelPathEntity EventPath { get; set; }
public NodeScriptEntity Script { 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 class StaticConditionEntity : IConditionEntity
{ {
public int PlayMode { get; set; }
public int StopMode { get; set; }
public NodeScriptEntity Script { 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 StartSegmentLength { get; set; }
public TimeSpan MainSegmentLength { get; set; } public TimeSpan MainSegmentLength { get; set; }
public TimeSpan EndSegmentLength { 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; private readonly ILayerPropertyKeyframe _keyframe;
/// <summary> /// <summary>
/// Creates a new instance of the <see cref="DeleteKeyframe{T}" /> class. /// Creates a new instance of the <see cref="DeleteKeyframe" /> class.
/// </summary> /// </summary>
public DeleteKeyframe(ILayerPropertyKeyframe keyframe) 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 else
{ {
renderElement.Enable(); renderElement.Enable();
bool stickToMainSegment = (renderElement != _profileElementSubject.Value || renderElement.Timeline.Length < time) && renderElement.Timeline.PlayMode == TimelinePlayMode.Repeat; renderElement.OverrideTimelineAndApply(time);
renderElement.OverrideTimelineAndApply(time, stickToMainSegment);
foreach (ProfileElement child in renderElement.Children) foreach (ProfileElement child in renderElement.Children)
TickProfileElement(child, time); TickProfileElement(child, time);

View File

@ -5,6 +5,7 @@ using Artemis.Core.LayerEffects;
using Artemis.UI.Screens.Device; using Artemis.UI.Screens.Device;
using Artemis.UI.Screens.Plugins; using Artemis.UI.Screens.Plugins;
using Artemis.UI.Screens.ProfileEditor; using Artemis.UI.Screens.ProfileEditor;
using Artemis.UI.Screens.ProfileEditor.DisplayCondition.ConditionTypes;
using Artemis.UI.Screens.ProfileEditor.ProfileTree; using Artemis.UI.Screens.ProfileEditor.ProfileTree;
using Artemis.UI.Screens.ProfileEditor.Properties; using Artemis.UI.Screens.ProfileEditor.Properties;
using Artemis.UI.Screens.ProfileEditor.Properties.DataBinding; using Artemis.UI.Screens.ProfileEditor.Properties.DataBinding;
@ -52,7 +53,7 @@ namespace Artemis.UI.Ninject.Factories
SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(SidebarViewModel sidebarViewModel, ProfileConfiguration profileConfiguration); SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(SidebarViewModel sidebarViewModel, ProfileConfiguration profileConfiguration);
} }
public interface SurfaceVmFactory : IVmFactory public interface ISurfaceVmFactory : IVmFactory
{ {
SurfaceDeviceViewModel SurfaceDeviceViewModel(ArtemisDevice device); SurfaceDeviceViewModel SurfaceDeviceViewModel(ArtemisDevice device);
ListDeviceViewModel ListDeviceViewModel(ArtemisDevice device); ListDeviceViewModel ListDeviceViewModel(ArtemisDevice device);
@ -108,4 +109,12 @@ namespace Artemis.UI.Ninject.Factories
InputPinCollectionViewModel InputPinCollectionViewModel(IPinCollection inputPinCollection, NodeScriptViewModel nodeScriptViewModel); InputPinCollectionViewModel InputPinCollectionViewModel(IPinCollection inputPinCollection, NodeScriptViewModel nodeScriptViewModel);
OutputPinCollectionViewModel OutputPinCollectionViewModel(IPinCollection outputPinCollection, 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,11 +1,11 @@
using System; using System;
using Artemis.UI.Shared; using Artemis.UI.Shared;
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition;
{
public class ConditionTypeViewModel : ViewModelBase public class ConditionTypeViewModel : ViewModelBase
{ {
public ConditionTypeViewModel(string name, string description, Type? conditionType) public ConditionTypeViewModel(string name, string description, Type conditionType)
{ {
Name = name; Name = name;
Description = description; Description = description;
@ -14,6 +14,5 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition
public string Name { get; } public string Name { get; }
public string Description { get; } public string Description { get; }
public Type? ConditionType { 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:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:displayCondition="clr-namespace:Artemis.UI.Screens.ProfileEditor.DisplayCondition" 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" mc:Ignorable="d" d:DesignWidth="200" d:DesignHeight="650"
x:Class="Artemis.UI.Screens.ProfileEditor.DisplayCondition.DisplayConditionScriptView" x:Class="Artemis.UI.Screens.ProfileEditor.DisplayCondition.DisplayConditionScriptView"
x:DataType="displayCondition:DisplayConditionScriptViewModel"> x:DataType="displayCondition:DisplayConditionScriptViewModel">
@ -20,19 +18,14 @@
</Style> </Style>
</UserControl.Styles> </UserControl.Styles>
<ScrollViewer> <ScrollViewer>
<DockPanel LastChildFill="False"> <Grid RowDefinitions="Auto,Auto,*">
<DockPanel.Styles> <TextBlock Grid.Row="0" Margin="0 5">Activation type</TextBlock>
<Style Selector="DockPanel > TextBlock">
<Setter Property="Margin" Value="0 5"></Setter>
</Style>
</DockPanel.Styles>
<TextBlock DockPanel.Dock="Top">Condition type</TextBlock>
<ComboBox Name="ConditionType" <ComboBox Name="ConditionType"
DockPanel.Dock="Top" Grid.Row="1"
Classes="condition-type" Classes="condition-type"
PlaceholderText="Select a condition type" PlaceholderText="Select an activation type"
Items="{CompiledBinding ConditionTypeViewModels}" Items="{CompiledBinding ConditionTypeViewModels}"
IsEnabled="{CompiledBinding ProfileElement, Converter={x:Static ObjectConverters.IsNotNull}}"
SelectedItem="{CompiledBinding SelectedConditionTypeViewModel}" SelectedItem="{CompiledBinding SelectedConditionTypeViewModel}"
HorizontalAlignment="Stretch"> HorizontalAlignment="Stretch">
<ComboBox.ItemTemplate> <ComboBox.ItemTemplate>
@ -44,64 +37,7 @@
</DataTemplate> </DataTemplate>
</ComboBox.ItemTemplate> </ComboBox.ItemTemplate>
</ComboBox> </ComboBox>
<ContentControl Grid.Row="2" Content="{CompiledBinding ConditionViewModel}" />
<!-- Event options --> </Grid>
<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>
</ScrollViewer> </ScrollViewer>
</UserControl> </UserControl>

View File

@ -1,9 +1,9 @@
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition;
{
public partial class DisplayConditionScriptView : ReactiveUserControl<DisplayConditionScriptViewModel> public class DisplayConditionScriptView : ReactiveUserControl<DisplayConditionScriptViewModel>
{ {
public DisplayConditionScriptView() public DisplayConditionScriptView()
{ {
@ -15,4 +15,3 @@ namespace Artemis.UI.Screens.ProfileEditor.DisplayCondition
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
} }
} }
}

View File

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

View File

@ -46,13 +46,6 @@
Command="{Binding DisableSegment}"> Command="{Binding DisableSegment}">
<avalonia:MaterialIcon Kind="CloseCircle" /> <avalonia:MaterialIcon Kind="CloseCircle" />
</Button> </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> </StackPanel>
<Button Name="AddEndSegment" <Button Name="AddEndSegment"

View File

@ -1,7 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq; using System.Reactive.Linq;
using Artemis.Core; using Artemis.Core;
using Artemis.UI.Shared.Services.ProfileEditor; using Artemis.UI.Shared.Services.ProfileEditor;
@ -13,12 +10,11 @@ namespace Artemis.UI.Screens.ProfileEditor.Properties.Timeline.Segments;
public class MainSegmentViewModel : TimelineSegmentViewModel public class MainSegmentViewModel : TimelineSegmentViewModel
{ {
private RenderProfileElement? _profileElement; private readonly ObservableAsPropertyHelper<double> _width;
private ObservableAsPropertyHelper<double>? _start;
private ObservableAsPropertyHelper<double>? _end; private ObservableAsPropertyHelper<double>? _end;
private ObservableAsPropertyHelper<string?>? _endTimestamp; private ObservableAsPropertyHelper<string?>? _endTimestamp;
private ObservableAsPropertyHelper<bool>? _repeatSegment; private RenderProfileElement? _profileElement;
private readonly ObservableAsPropertyHelper<double> _width; private ObservableAsPropertyHelper<double>? _start;
public MainSegmentViewModel(IProfileEditorService profileEditorService) : base(profileEditorService) public MainSegmentViewModel(IProfileEditorService profileEditorService) : base(profileEditorService)
{ {
@ -44,12 +40,6 @@ public class MainSegmentViewModel : TimelineSegmentViewModel
.Select(p => $"{Math.Floor(p.TotalSeconds):00}.{p.Milliseconds:000}") .Select(p => $"{Math.Floor(p.TotalSeconds):00}.{p.Milliseconds:000}")
.ToProperty(this, vm => vm.EndTimestamp) .ToProperty(this, vm => vm.EndTimestamp)
.DisposeWith(d); .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); _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 double Width => _width.Value;
public override string? EndTimestamp => _endTimestamp?.Value; public override string? EndTimestamp => _endTimestamp?.Value;
public override ResizeTimelineSegment.SegmentType Type => ResizeTimelineSegment.SegmentType.Main; 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, public SurfaceEditorViewModel(IScreen hostScreen,
IRgbService rgbService, IRgbService rgbService,
SurfaceVmFactory surfaceVmFactory, ISurfaceVmFactory surfaceVmFactory,
ISettingsService settingsService) : base(hostScreen, "surface-editor") ISettingsService settingsService) : base(hostScreen, "surface-editor")
{ {
_rgbService = rgbService; _rgbService = rgbService;

View File

@ -3,9 +3,30 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:visualScripting="clr-namespace:Artemis.UI.Screens.VisualScripting" 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" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.VisualScripting.NodeScriptWindowView" x:Class="Artemis.UI.Screens.VisualScripting.NodeScriptWindowView"
x:DataType="visualScripting:NodeScriptWindowViewModel" x:DataType="visualScripting:NodeScriptWindowViewModel"
Title="VisualEditorWindowView"> 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}" /> <ContentControl Content="{CompiledBinding NodeScriptViewModel}" />
</Border>
</Grid>
</Window> </Window>

View File

@ -5,7 +5,7 @@ using Artemis.VisualScripting.Nodes.Operators.Screens;
namespace Artemis.VisualScripting.Nodes.Operators; 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))] [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") 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) private void InputPinOnPinConnected(object? sender, SingleValueEventArgs<IPin> e)
{ {
if (Storage?.GetType() != InputPin.ConnectedTo.First().Type) Storage = 0;
Storage = Enum.GetValues(InputPin.ConnectedTo.First().Type).Cast<Enum>().FirstOrDefault();
} }
public InputPin<Enum> InputPin { get; } public InputPin<Enum> InputPin { get; }
public OutputPin<bool> OutputPin { get; } public OutputPin<bool> OutputPin { get; }
#region Overrides of Node
/// <inheritdoc /> /// <inheritdoc />
public override void Evaluate() 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"> x:DataType="screens:EnumEqualsNodeCustomViewModel">
<ComboBox IsEnabled="{CompiledBinding EnumValues.Count}" <ComboBox IsEnabled="{CompiledBinding EnumValues.Count}"
Items="{CompiledBinding EnumValues}" Items="{CompiledBinding EnumValues}"
SelectedItem="{CompiledBinding CurrentValue}" SelectedIndex="{CompiledBinding CurrentValue}"
PlaceholderText="Connect a pin..." PlaceholderText="Connect a pin..."
Classes="condensed" Classes="condensed"
VerticalAlignment="Center" /> 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) Observable.FromEventPattern<SingleValueEventArgs<IPin>>(x => _node.InputPin.PinConnected += x, x => _node.InputPin.PinConnected -= x)
.Subscribe(p => .Subscribe(p => EnumValues.AddRange(Enum.GetValues(p.EventArgs.Value.Type).Cast<Enum>()))
{
EnumValues.AddRange(Enum.GetValues(p.EventArgs.Value.Type).Cast<Enum>());
this.RaisePropertyChanged(nameof(CurrentValue));
})
.DisposeWith(d); .DisposeWith(d);
Observable.FromEventPattern<SingleValueEventArgs<IPin>>(x => _node.InputPin.PinDisconnected += x, x => _node.InputPin.PinDisconnected -= x) Observable.FromEventPattern<SingleValueEventArgs<IPin>>(x => _node.InputPin.PinDisconnected += x, x => _node.InputPin.PinDisconnected -= x)
.Subscribe(_ => EnumValues.Clear()) .Subscribe(_ => EnumValues.Clear())
@ -47,13 +43,13 @@ public class EnumEqualsNodeCustomViewModel : CustomNodeViewModel
public ObservableCollection<Enum> EnumValues { get; } = new(); public ObservableCollection<Enum> EnumValues { get; } = new();
public Enum? CurrentValue public int CurrentValue
{ {
get => _node.Storage; get => _node.Storage;
set set
{ {
if (value != null && !Equals(_node.Storage, value)) if (!Equals(_node.Storage, value))
_nodeEditorService.ExecuteCommand(Script, new UpdateStorage<Enum>(_node, value)); _nodeEditorService.ExecuteCommand(Script, new UpdateStorage<int>(_node, value));
} }
} }
} }