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

Nodes - Added gradient nodes

Nodes - Added color gradient pin type
Data bindings - Changed color gradient data bindings to now take a color gradient
This commit is contained in:
Robert 2022-09-23 21:40:30 +02:00
parent 32a444fbeb
commit 9c117d2773
48 changed files with 830 additions and 344 deletions

View File

@ -93,4 +93,6 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=stores_005Cregistrations/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=utilities/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=visualscripting/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=visualscripting_005Cinterfaces/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=visualscripting_005Cinterfaces/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=visualscripting_005Cnodes/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=visualscripting_005Cpins/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -1,13 +1,8 @@
using System.Collections.Specialized;
using SkiaSharp;
namespace Artemis.Core;
namespace Artemis.Core;
/// <inheritdoc />
public class ColorGradientLayerProperty : LayerProperty<ColorGradient>
{
private ColorGradient? _subscribedGradient;
internal ColorGradientLayerProperty()
{
KeyframesSupported = false;
@ -22,29 +17,6 @@ public class ColorGradientLayerProperty : LayerProperty<ColorGradient>
return p.CurrentValue;
}
#region Overrides of LayerProperty<ColorGradient>
/// <inheritdoc />
protected override void OnCurrentValueSet()
{
// Don't allow color gradients to be null
if (BaseValue == null!)
BaseValue = new ColorGradient(DefaultValue);
if (!ReferenceEquals(_subscribedGradient, BaseValue))
{
if (_subscribedGradient != null)
_subscribedGradient.CollectionChanged -= SubscribedGradientOnPropertyChanged;
_subscribedGradient = BaseValue;
_subscribedGradient.CollectionChanged += SubscribedGradientOnPropertyChanged;
}
CreateDataBindingRegistrations();
base.OnCurrentValueSet();
}
#endregion
/// <inheritdoc />
protected override void UpdateCurrentValue(float keyframeProgress, float keyframeProgressEased)
{
@ -60,33 +32,12 @@ public class ColorGradientLayerProperty : LayerProperty<ColorGradient>
if (BaseValue == null!)
BaseValue = new ColorGradient(DefaultValue);
base.OnInitialize();
DataBinding.RegisterDataBindingProperty(() => CurrentValue, value =>
{
if (value != null)
CurrentValue = value;
}, "Value");
}
#endregion
private void CreateDataBindingRegistrations()
{
DataBinding.ClearDataBindingProperties();
if (CurrentValue == null!)
return;
for (int index = 0; index < CurrentValue.Count; index++)
{
int stopIndex = index;
void Setter(SKColor value)
{
CurrentValue[stopIndex].Color = value;
}
DataBinding.RegisterDataBindingProperty(() => CurrentValue[stopIndex].Color, Setter, $"Color #{stopIndex + 1}");
}
}
private void SubscribedGradientOnPropertyChanged(object? sender, NotifyCollectionChangedEventArgs args)
{
if (CurrentValue.Count != DataBinding.Properties.Count)
CreateDataBindingRegistrations();
}
}

View File

@ -35,7 +35,7 @@ public class DataBindingProperty<TProperty> : IDataBindingProperty
}
/// <inheritdoc />
public void SetValue(object? value)
public void SetValue(object value)
{
// Numeric has a bunch of conversion, this seems the cheapest way to use them :)
switch (value)

View File

@ -27,5 +27,5 @@ public interface IDataBindingProperty
/// Sets the value of the property this registration points to
/// </summary>
/// <param name="value">A value matching the type of <see cref="ValueType" /></param>
void SetValue(object? value);
void SetValue(object value);
}

View File

@ -22,6 +22,7 @@ public class PluginFeatureAttribute : Attribute
/// The plugins display icon that's shown in the settings see <see href="https://materialdesignicons.com" /> for
/// available icons
/// </summary>
[Obsolete("Feature icons are no longer shown in the UI.")]
public string? Icon { get; set; }
/// <summary>

View File

@ -31,20 +31,7 @@ public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject
Name = attribute?.Name ?? featureType.Name.Humanize(LetterCasing.Title);
Description = attribute?.Description;
Icon = attribute?.Icon;
AlwaysEnabled = attribute?.AlwaysEnabled ?? false;
if (Icon != null) return;
if (typeof(DeviceProvider).IsAssignableFrom(featureType))
Icon = "Devices";
else if (typeof(Module).IsAssignableFrom(featureType))
Icon = "VectorRectangle";
else if (typeof(LayerBrushProvider).IsAssignableFrom(featureType))
Icon = "Brush";
else if (typeof(LayerEffectProvider).IsAssignableFrom(featureType))
Icon = "AutoAwesome";
else
Icon = "Plugin";
}
internal PluginFeatureInfo(Plugin plugin, PluginFeatureAttribute? attribute, PluginFeature instance)
@ -56,19 +43,8 @@ public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject
Name = attribute?.Name ?? instance.GetType().Name.Humanize(LetterCasing.Title);
Description = attribute?.Description;
Icon = attribute?.Icon;
AlwaysEnabled = attribute?.AlwaysEnabled ?? false;
Instance = instance;
if (Icon != null) return;
Icon = Instance switch
{
DeviceProvider => "Devices",
Module => "VectorRectangle",
LayerBrushProvider => "Brush",
LayerEffectProvider => "AutoAwesome",
_ => "Plugin"
};
}
/// <summary>
@ -110,17 +86,6 @@ public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject
set => SetAndNotify(ref _description, value);
}
/// <summary>
/// The plugins display icon that's shown in the settings see <see href="https://materialdesignicons.com" /> for
/// available icons
/// </summary>
[JsonProperty]
public string? Icon
{
get => _icon;
set => SetAndNotify(ref _icon, value);
}
/// <summary>
/// Marks the feature to always be enabled as long as the plugin is enabled and cannot be disabled.
/// <para>Note: always <see langword="true" /> if this is the plugin's only feature</para>
@ -142,20 +107,7 @@ public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject
get => _instance;
internal set => SetAndNotify(ref _instance, value);
}
/// <summary>
/// Gets a string representing either a full path pointing to an svg or the markdown icon
/// </summary>
public string? ResolvedIcon
{
get
{
if (Icon == null)
return null;
return Icon.Contains('.') ? Plugin.ResolveRelativePath(Icon) : Icon;
}
}
internal PluginFeatureEntity Entity { get; }
/// <inheritdoc />

View File

@ -23,7 +23,10 @@ internal class DataBindingExitNode<TLayerProperty> : Node, IExitNode
public void ApplyToDataBinding()
{
foreach ((IDataBindingProperty? property, object? pendingValue) in _propertyValues)
property.SetValue(pendingValue);
{
if (pendingValue != null)
property.SetValue(pendingValue);
}
}
public override void Evaluate()

View File

@ -15,8 +15,7 @@ internal class EventConditionEventStartNode : DefaultNode
public void SetDataModelEvent(IDataModelEvent? dataModelEvent)
{
}
}
public void CreatePins(IDataModelEvent? dataModelEvent)
{

View File

@ -93,10 +93,8 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript
NodeTypeStore.NodeTypeRemoved += NodeTypeStoreOnNodeTypeRemoved;
if (defaultNodes != null)
{
foreach (DefaultNode defaultNode in defaultNodes)
AddNode(defaultNode);
}
}
internal NodeScript(string name, string description, NodeScriptEntity entity, object? context = null, List<DefaultNode>? defaultNodes = null)
@ -109,12 +107,10 @@ public abstract class NodeScript : CorePropertyChanged, INodeScript
NodeTypeStore.NodeTypeAdded += NodeTypeStoreOnNodeTypeAdded;
NodeTypeStore.NodeTypeRemoved += NodeTypeStoreOnNodeTypeRemoved;
if (defaultNodes != null)
{
foreach (DefaultNode defaultNode in defaultNodes)
AddNode(defaultNode);
}
}
#endregion

View File

@ -0,0 +1,32 @@
namespace Artemis.Core;
/// <summary>
/// Represents the position of a node's custom view model.
/// </summary>
public enum CustomNodeViewModelPosition
{
/// <summary>
/// Puts the view model above the pins.
/// </summary>
AbovePins,
/// <summary>
/// Puts the view model between the pins, vertically aligned to the top.
/// </summary>
BetweenPinsTop,
/// <summary>
/// Puts the view model between the pins, vertically aligned to the center.
/// </summary>
BetweenPinsCenter,
/// <summary>
/// Puts the view model between the pins, vertically aligned to the bottom.
/// </summary>
BetweenPinsBottom,
/// <summary>
/// Puts the view model below the pins.
/// </summary>
BelowPins
}

View File

@ -0,0 +1,18 @@
namespace Artemis.Core;
/// <summary>
/// Represents a node that has a custom view model.
/// </summary>
public interface ICustomViewModelNode
{
/// <summary>
/// Gets or sets the position of the node's custom view model.
/// </summary>
CustomNodeViewModelPosition ViewModelPosition { get; }
/// <summary>
/// Called whenever the node must show it's custom view model, if <see langword="null" />, no custom view model is used
/// </summary>
/// <returns>The custom view model, if <see langword="null" />, no custom view model is used</returns>
ICustomNodeViewModel? GetCustomViewModel(NodeScript nodeScript);
}

View File

@ -2,10 +2,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using Artemis.Core.Events;
using Ninject;
using Ninject.Parameters;
namespace Artemis.Core;
@ -339,7 +336,8 @@ public abstract class Node : BreakableModel, INode
}
/// <summary>
/// Called when the node was loaded from storage or newly created, at this point pin connections aren't reestablished yet.
/// Called when the node was loaded from storage or newly created, at this point pin connections aren't reestablished
/// yet.
/// </summary>
/// <param name="script">The script the node is contained in</param>
public virtual void Initialize(INodeScript script)
@ -374,16 +372,6 @@ public abstract class Node : BreakableModel, INode
TryOrBreak(Evaluate, "Failed to evaluate");
}
/// <summary>
/// Called whenever the node must show it's custom view model, if <see langword="null" />, no custom view model is used
/// </summary>
/// <param name="nodeScript"></param>
/// <returns>The custom view model, if <see langword="null" />, no custom view model is used</returns>
public virtual ICustomNodeViewModel? GetCustomViewModel(NodeScript nodeScript)
{
return null;
}
/// <summary>
/// Serializes the <see cref="Storage" /> object into a string
/// </summary>
@ -402,102 +390,4 @@ public abstract class Node : BreakableModel, INode
}
#endregion
}
/// <summary>
/// Represents a kind of node inside a <see cref="NodeScript" /> containing storage value of type
/// <typeparamref name="TStorage" />.
/// </summary>
/// <typeparam name="TStorage">The type of value the node stores</typeparam>
public abstract class Node<TStorage> : Node
{
private TStorage? _storage;
/// <inheritdoc />
protected Node()
{
}
/// <inheritdoc />
protected Node(string name, string description) : base(name, description)
{
}
/// <summary>
/// Gets or sets the storage object of this node, this is saved across sessions
/// </summary>
public TStorage? Storage
{
get => _storage;
set
{
if (SetAndNotify(ref _storage, value))
StorageModified?.Invoke(this, EventArgs.Empty);
}
}
/// <summary>
/// Occurs whenever the storage of this node was modified.
/// </summary>
public event EventHandler? StorageModified;
/// <inheritdoc />
public override string SerializeStorage()
{
return CoreJson.SerializeObject(Storage, true);
}
/// <inheritdoc />
public override void DeserializeStorage(string serialized)
{
Storage = CoreJson.DeserializeObject<TStorage>(serialized) ?? default(TStorage);
}
}
/// <summary>
/// Represents a kind of node inside a <see cref="NodeScript" /> containing storage value of type
/// <typeparamref name="TStorage" /> and a view model of type <typeparamref name="TViewModel" />.
/// </summary>
/// <typeparam name="TStorage">The type of value the node stores</typeparam>
/// <typeparam name="TViewModel">The type of view model the node uses</typeparam>
public abstract class Node<TStorage, TViewModel> : Node<TStorage> where TViewModel : ICustomNodeViewModel
{
/// <inheritdoc />
protected Node()
{
}
/// <inheritdoc />
protected Node(string name, string description) : base(name, description)
{
}
[Inject]
internal IKernel Kernel { get; set; } = null!;
/// <summary>
/// Called when a view model is required
/// </summary>
/// <param name="nodeScript"></param>
public virtual TViewModel GetViewModel(NodeScript nodeScript)
{
// Limit to one constructor, there's no need to have more and it complicates things anyway
ConstructorInfo[] constructors = typeof(TViewModel).GetConstructors();
if (constructors.Length != 1)
throw new ArtemisCoreException("Node VMs must have exactly one constructor");
// Find the ScriptConfiguration parameter, it is required by the base constructor so its there for sure
ParameterInfo? configurationParameter = constructors.First().GetParameters().FirstOrDefault(p => GetType().IsAssignableFrom(p.ParameterType));
if (configurationParameter?.Name == null)
throw new ArtemisCoreException($"Couldn't find a valid constructor argument on {typeof(TViewModel).Name} with type {GetType().Name}");
return Kernel.Get<TViewModel>(new ConstructorArgument(configurationParameter.Name, this), new ConstructorArgument("script", nodeScript));
}
/// <param name="nodeScript"></param>
/// <inheritdoc />
public override ICustomNodeViewModel? GetCustomViewModel(NodeScript nodeScript)
{
return GetViewModel(nodeScript);
}
}

View File

@ -0,0 +1,52 @@
using System;
using Artemis.Core;
/// <summary>
/// Represents a kind of node inside a <see cref="NodeScript" /> containing storage value of type
/// <typeparamref name="TStorage" />.
/// </summary>
/// <typeparam name="TStorage">The type of value the node stores</typeparam>
public abstract class Node<TStorage> : Node
{
private TStorage? _storage;
/// <inheritdoc />
protected Node()
{
}
/// <inheritdoc />
protected Node(string name, string description) : base(name, description)
{
}
/// <summary>
/// Gets or sets the storage object of this node, this is saved across sessions
/// </summary>
public TStorage? Storage
{
get => _storage;
set
{
if (SetAndNotify(ref _storage, value))
StorageModified?.Invoke(this, EventArgs.Empty);
}
}
/// <summary>
/// Occurs whenever the storage of this node was modified.
/// </summary>
public event EventHandler? StorageModified;
/// <inheritdoc />
public override string SerializeStorage()
{
return CoreJson.SerializeObject(Storage, true);
}
/// <inheritdoc />
public override void DeserializeStorage(string serialized)
{
Storage = CoreJson.DeserializeObject<TStorage>(serialized) ?? default(TStorage);
}
}

View File

@ -0,0 +1,58 @@
using System.Linq;
using System.Reflection;
using Artemis.Core;
using Ninject;
using Ninject.Parameters;
/// <summary>
/// Represents a kind of node inside a <see cref="NodeScript" /> containing storage value of type
/// <typeparamref name="TStorage" /> and a view model of type <typeparamref name="TViewModel" />.
/// </summary>
/// <typeparam name="TStorage">The type of value the node stores</typeparam>
/// <typeparam name="TViewModel">The type of view model the node uses</typeparam>
public abstract class Node<TStorage, TViewModel> : Node<TStorage>, ICustomViewModelNode where TViewModel : ICustomNodeViewModel
{
/// <inheritdoc />
protected Node()
{
}
/// <inheritdoc />
protected Node(string name, string description) : base(name, description)
{
}
[Inject]
internal IKernel Kernel { get; set; } = null!;
/// <summary>
/// Called when a view model is required
/// </summary>
/// <param name="nodeScript"></param>
public virtual TViewModel GetViewModel(NodeScript nodeScript)
{
// Limit to one constructor, there's no need to have more and it complicates things anyway
ConstructorInfo[] constructors = typeof(TViewModel).GetConstructors();
if (constructors.Length != 1)
throw new ArtemisCoreException("Node VMs must have exactly one constructor");
// Find the ScriptConfiguration parameter, it is required by the base constructor so its there for sure
ParameterInfo? configurationParameter = constructors.First().GetParameters().FirstOrDefault(p => GetType().IsAssignableFrom(p.ParameterType));
if (configurationParameter?.Name == null)
throw new ArtemisCoreException($"Couldn't find a valid constructor argument on {typeof(TViewModel).Name} with type {GetType().Name}");
return Kernel.Get<TViewModel>(new ConstructorArgument(configurationParameter.Name, this), new ConstructorArgument("script", nodeScript));
}
/// <summary>
/// Gets or sets the position of the node's custom view model.
/// </summary>
public CustomNodeViewModelPosition ViewModelPosition { get; protected set; } = CustomNodeViewModelPosition.BetweenPinsTop;
/// <param name="nodeScript"></param>
/// <inheritdoc />
public ICustomNodeViewModel GetCustomViewModel(NodeScript nodeScript)
{
return GetViewModel(nodeScript);
}
}

View File

@ -0,0 +1,87 @@
using System;
using System.Linq;
using Artemis.Storage.Entities.Profile;
using Artemis.Storage.Entities.Profile.Nodes;
using Artemis.Storage.Migrations.Interfaces;
using LiteDB;
namespace Artemis.Storage.Migrations;
public class M0021GradientNodes : IStorageMigration
{
private void MigrateDataBinding(PropertyEntity property)
{
NodeScriptEntity script = property.DataBinding.NodeScript;
NodeEntity exitNode = script.Nodes.FirstOrDefault(s => s.IsExitNode);
if (exitNode == null)
return;
// Create a new node at the same position of the exit node
NodeEntity gradientNode = new()
{
Id = Guid.NewGuid(),
Type = "ColorGradientNode",
PluginId = Guid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff"),
Name = "Color Gradient",
Description = "Outputs a color gradient with the given colors",
X = exitNode.X,
Y = exitNode.Y,
Storage = property.Value // Copy the value of the property into the node storage
};
script.Nodes.Add(gradientNode);
// Move all connections of the exit node to the new node
foreach (NodeConnectionEntity connection in script.Connections)
{
if (connection.SourceNode == exitNode.Id)
{
connection.SourceNode = gradientNode.Id;
connection.SourcePinId++;
}
}
// Connect the data binding node to the source node
script.Connections.Add(new NodeConnectionEntity
{
SourceType = "ColorGradient",
SourceNode = exitNode.Id,
SourcePinCollectionId = -1,
SourcePinId = 0,
TargetType = "ColorGradient",
TargetNode = gradientNode.Id,
TargetPinCollectionId = -1,
TargetPinId = 0,
});
// Move the exit node to the right
exitNode.X += 300;
exitNode.Y += 30;
}
public int UserVersion => 21;
public void Apply(LiteRepository repository)
{
// Find all color gradient data bindings, there's no really good way to do this so infer it from the value
ILiteCollection<ProfileEntity> collection = repository.Database.GetCollection<ProfileEntity>();
foreach (ProfileEntity profileEntity in collection.FindAll())
{
foreach (LayerEntity layer in profileEntity.Layers)
MigrateDataBinding(layer.LayerBrush.PropertyGroup);
collection.Update(profileEntity);
}
}
private void MigrateDataBinding(PropertyGroupEntity propertyGroup)
{
foreach (PropertyGroupEntity propertyGroupPropertyGroup in propertyGroup.PropertyGroups)
MigrateDataBinding(propertyGroupPropertyGroup);
foreach (PropertyEntity property in propertyGroup.Properties)
{
if (property.Value.StartsWith("[{\"Color\":\"") && property.DataBinding?.NodeScript != null && property.DataBinding.IsEnabled)
MigrateDataBinding(property);
}
}
}

View File

@ -129,7 +129,7 @@ public class ArtemisIcon : UserControl
/// theme
/// </summary>
public static readonly StyledProperty<bool> FillProperty =
AvaloniaProperty.Register<ArtemisIcon, bool>(nameof(Icon), true, notifying: IconChanging);
AvaloniaProperty.Register<ArtemisIcon, bool>(nameof(Icon), false, notifying: IconChanging);
/// <summary>
/// Gets or sets a boolean indicating whether or not the icon should be filled in with the primary text color of the

View File

@ -59,22 +59,29 @@ public class GradientPicker : TemplatedControl
public static readonly DirectProperty<GradientPicker, ICommand> DeleteStopProperty =
AvaloniaProperty.RegisterDirect<GradientPicker, ICommand>(nameof(DeleteStop), g => g.DeleteStop);
/// <summary>
/// Gets the color gradient currently being edited, for internal use only
/// </summary>
public static readonly DirectProperty<GradientPicker, ColorGradient> EditingColorGradientProperty =
AvaloniaProperty.RegisterDirect<GradientPicker, ColorGradient>(nameof(EditingColorGradient), g => g.EditingColorGradient);
private readonly ICommand _deleteStop;
private ColorPicker? _colorPicker;
private Button? _flipStops;
private Border? _gradient;
private ColorGradient? _lastColorGradient;
private Button? _randomize;
private Button? _rotateStops;
private bool _shiftDown;
private Button? _spreadStops;
private Button? _toggleSeamless;
private ColorGradient _colorGradient = null!;
/// <summary>
/// Creates a new instance of the <see cref="GradientPicker" /> class.
/// </summary>
public GradientPicker()
{
EditingColorGradient = ColorGradient.GetUnicornBarf();
_deleteStop = ReactiveCommand.Create<ColorGradientStop>(s =>
{
if (ColorGradient.Count <= 2)
@ -143,6 +150,15 @@ public class GradientPicker : TemplatedControl
get => _deleteStop;
private init => SetAndRaise(DeleteStopProperty, ref _deleteStop, value);
}
/// <summary>
/// Gets the color gradient backing the editor, this is a copy of <see cref="ColorGradient"/>.
/// </summary>
public ColorGradient EditingColorGradient
{
get => _colorGradient;
private set => SetAndRaise(EditingColorGradientProperty, ref _colorGradient, value);
}
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
@ -167,7 +183,7 @@ public class GradientPicker : TemplatedControl
_flipStops = e.NameScope.Find<Button>("FlipStops");
_rotateStops = e.NameScope.Find<Button>("RotateStops");
_randomize = e.NameScope.Find<Button>("Randomize");
if (_gradient != null)
_gradient.PointerPressed += GradientOnPointerPressed;
if (_spreadStops != null)
@ -188,7 +204,7 @@ public class GradientPicker : TemplatedControl
/// <inheritdoc />
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
Subscribe();
ApplyToField();
KeyUp += OnKeyUp;
KeyDown += OnKeyDown;
@ -197,60 +213,72 @@ public class GradientPicker : TemplatedControl
/// <inheritdoc />
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
Unsubscribe();
ApplyToProperty();
KeyUp -= OnKeyUp;
KeyDown -= OnKeyDown;
_shiftDown = false;
}
private static void ColorGradientChanged(IAvaloniaObject sender, bool before)
{
(sender as GradientPicker)?.Subscribe();
(sender as GradientPicker)?.ApplyToField();
}
private static void StorageProviderChanged(IAvaloniaObject sender, bool before)
{
}
private void Subscribe()
private void ApplyToField()
{
Unsubscribe();
ColorGradient.CollectionChanged += ColorGradientOnCollectionChanged;
ColorGradient.StopChanged += ColorGradientOnStopChanged;
SelectedColorStop = ColorGradient.FirstOrDefault();
EditingColorGradient = new ColorGradient(ColorGradient);
EditingColorGradient.CollectionChanged += ColorGradientOnCollectionChanged;
EditingColorGradient.StopChanged += ColorGradientOnStopChanged;
SelectedColorStop = EditingColorGradient.FirstOrDefault();
UpdateGradient();
_lastColorGradient = ColorGradient;
}
private void Unsubscribe()
private void ApplyToProperty()
{
if (_lastColorGradient == null)
return;
// Remove extra color gradients
while (ColorGradient.Count > EditingColorGradient.Count)
ColorGradient.RemoveAt(ColorGradient.Count - 1);
_lastColorGradient.CollectionChanged -= ColorGradientOnCollectionChanged;
_lastColorGradient.StopChanged -= ColorGradientOnStopChanged;
_lastColorGradient = null;
for (int index = 0; index < EditingColorGradient.Count; index++)
{
ColorGradientStop colorGradientStop = EditingColorGradient[index];
// Add missing color gradients
if (index >= ColorGradient.Count)
{
ColorGradient.Add(new ColorGradientStop(colorGradientStop.Color, colorGradientStop.Position));
}
// Update existing color gradients
else
{
ColorGradient[index].Color = colorGradientStop.Color;
ColorGradient[index].Position = colorGradientStop.Position;
}
}
}
private void ColorGradientOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
UpdateGradient();
ApplyToProperty();
}
private void ColorGradientOnStopChanged(object? sender, EventArgs e)
{
UpdateGradient();
ApplyToProperty();
}
private void UpdateGradient()
{
// Update the display gradient
GradientStops collection = new();
foreach (ColorGradientStop c in ColorGradient.OrderBy(s => s.Position))
foreach (ColorGradientStop c in EditingColorGradient.OrderBy(s => s.Position))
collection.Add(new GradientStop(Color.FromArgb(c.Color.Alpha, c.Color.Red, c.Color.Green, c.Color.Blue), c.Position));
LinearGradientBrush.GradientStops = collection;
@ -263,8 +291,8 @@ public class GradientPicker : TemplatedControl
float position = (float) (e.GetPosition(_gradient).X / _gradient.Bounds.Width);
ColorGradientStop newStop = new(ColorGradient.GetColor(position), position);
ColorGradient.Add(newStop);
ColorGradientStop newStop = new(EditingColorGradient.GetColor(position), position);
EditingColorGradient.Add(newStop);
SelectedColorStop = newStop;
}
@ -279,50 +307,50 @@ public class GradientPicker : TemplatedControl
if (e.Key is Key.LeftShift or Key.RightShift)
_shiftDown = false;
if (e.Key != Key.Delete || SelectedColorStop == null || ColorGradient.Count <= 2)
if (e.Key != Key.Delete || SelectedColorStop == null || EditingColorGradient.Count <= 2)
return;
int index = ColorGradient.IndexOf(SelectedColorStop);
ColorGradient.Remove(SelectedColorStop);
if (index > ColorGradient.Count - 1)
int index = EditingColorGradient.IndexOf(SelectedColorStop);
EditingColorGradient.Remove(SelectedColorStop);
if (index > EditingColorGradient.Count - 1)
index--;
SelectedColorStop = ColorGradient.ElementAtOrDefault(index);
SelectedColorStop = EditingColorGradient.ElementAtOrDefault(index);
e.Handled = true;
}
private void SpreadStopsOnClick(object? sender, RoutedEventArgs e)
{
ColorGradient.SpreadStops();
EditingColorGradient.SpreadStops();
}
private void ToggleSeamlessOnClick(object? sender, RoutedEventArgs e)
{
if (SelectedColorStop == null || ColorGradient.Count < 2)
if (SelectedColorStop == null || EditingColorGradient.Count < 2)
return;
ColorGradient.ToggleSeamless();
EditingColorGradient.ToggleSeamless();
}
private void FlipStopsOnClick(object? sender, RoutedEventArgs e)
{
if (SelectedColorStop == null || ColorGradient.Count < 2)
if (SelectedColorStop == null || EditingColorGradient.Count < 2)
return;
ColorGradient.FlipStops();
EditingColorGradient.FlipStops();
}
private void RotateStopsOnClick(object? sender, RoutedEventArgs e)
{
if (SelectedColorStop == null || ColorGradient.Count < 2)
if (SelectedColorStop == null || EditingColorGradient.Count < 2)
return;
ColorGradient.RotateStops(_shiftDown);
EditingColorGradient.RotateStops(_shiftDown);
}
private void RandomizeOnClick(object? sender, RoutedEventArgs e)
{
ColorGradient.Randomize(6);
SelectedColorStop = ColorGradient.First();
EditingColorGradient.Randomize(6);
SelectedColorStop = EditingColorGradient.First();
}
}

View File

@ -46,9 +46,6 @@ public class GradientPickerButton : TemplatedControl
AvaloniaProperty.RegisterDirect<GradientPickerButton, LinearGradientBrush>(nameof(LinearGradientBrush), g => g.LinearGradientBrush);
private Button? _button;
private GradientPickerFlyout? _flyout;
private bool _flyoutActive;
private ColorGradient? _lastColorGradient;
/// <summary>
@ -150,27 +147,23 @@ public class GradientPickerButton : TemplatedControl
private void OnButtonClick(object? sender, RoutedEventArgs e)
{
if (_flyout == null || ColorGradient == null)
if (ColorGradient == null)
return;
// Logic here is taken from Fluent Avalonia's ColorPicker which also reuses the same control since it's large
_flyout.GradientPicker.ColorGradient = ColorGradient;
_flyout.GradientPicker.IsCompact = IsCompact;
_flyout.GradientPicker.StorageProvider = StorageProvider;
_flyout.ShowAt(this);
_flyoutActive = true;
GradientPickerFlyout flyout = new();
flyout.FlyoutPresenterClasses.Add("gradient-picker-presenter");
flyout.GradientPicker.ColorGradient = ColorGradient;
flyout.GradientPicker.IsCompact = IsCompact;
flyout.GradientPicker.StorageProvider = StorageProvider;
flyout.Closed += FlyoutOnClosed;
flyout.ShowAt(this);
FlyoutOpened?.Invoke(this, EventArgs.Empty);
}
private void OnFlyoutClosed(object? sender, EventArgs e)
{
if (_flyoutActive)
void FlyoutOnClosed(object? closedSender, EventArgs closedEventArgs)
{
flyout.Closed -= FlyoutOnClosed;
FlyoutClosed?.Invoke(this, EventArgs.Empty);
_flyoutActive = false;
}
}
@ -191,22 +184,12 @@ public class GradientPickerButton : TemplatedControl
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
Subscribe();
if (_flyout == null)
{
_flyout = new GradientPickerFlyout();
_flyout.FlyoutPresenterClasses.Add("gradient-picker-presenter");
}
_flyout.Closed += OnFlyoutClosed;
}
/// <inheritdoc />
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
Unsubscribe();
if (_flyout != null)
_flyout.Closed -= OnFlyoutClosed;
}
#endregion

View File

@ -19,7 +19,7 @@ public class UpdateColorGradient : IProfileEditorCommand
public UpdateColorGradient(ColorGradient colorGradient, List<ColorGradientStop> stops, List<ColorGradientStop>? originalStops)
{
_colorGradient = colorGradient;
_stops = stops;
_stops = stops.Select(s => new ColorGradientStop(s.Color, s.Position)).ToList();
_originalStops = originalStops ?? _colorGradient.Select(s => new ColorGradientStop(s.Color, s.Position)).ToList();
}
@ -31,18 +31,36 @@ public class UpdateColorGradient : IProfileEditorCommand
/// <inheritdoc />
public void Execute()
{
_colorGradient.Clear();
foreach (ColorGradientStop colorGradientStop in _stops)
_colorGradient.Add(colorGradientStop);
ApplyStops(_stops);
}
/// <inheritdoc />
public void Undo()
{
_colorGradient.Clear();
foreach (ColorGradientStop colorGradientStop in _originalStops)
_colorGradient.Add(colorGradientStop);
ApplyStops(_originalStops);
}
#endregion
private void ApplyStops(List<ColorGradientStop> stops)
{
while (_colorGradient.Count > stops.Count)
_colorGradient.RemoveAt(_colorGradient.Count - 1);
for (int index = 0; index < stops.Count; index++)
{
ColorGradientStop colorGradientStop = stops[index];
// Add missing color gradients
if (index >= _colorGradient.Count)
{
_colorGradient.Add(new ColorGradientStop(colorGradientStop.Color, colorGradientStop.Position));
}
// Update existing color gradients
else
{
_colorGradient[index].Color = colorGradientStop.Color;
_colorGradient[index].Position = colorGradientStop.Position;
}
}
}
}

View File

@ -73,7 +73,6 @@
<Style Selector="gradientPicker|GradientPicker">
<Style.Resources>
<converters:ColorGradientToGradientStopsConverter x:Key="ColorGradientToGradientStopsConverter" />
<converters:SKColorToColorConverter x:Key="SKColorToColorConverter" />
<converters:SKColorToBrushConverter x:Key="SKColorToBrushConverter" />
<converters:SKColorToStringConverter x:Key="SKColorToStringConverter" />
@ -89,7 +88,7 @@
Background="{DynamicResource LightCheckerboardBrush}"
Margin="5 0">
<Border Background="{TemplateBinding LinearGradientBrush}">
<ItemsControl Name="GradientStops" Items="{TemplateBinding ColorGradient}" ClipToBounds="False">
<ItemsControl Name="GradientStops" Items="{TemplateBinding EditingColorGradient}" ClipToBounds="False">
<ItemsControl.Styles>
<Style Selector="ItemsControl#GradientStops > ContentPresenter">
<Setter Property="Canvas.Left">
@ -105,9 +104,9 @@
<ItemsControl.ItemTemplate>
<DataTemplate DataType="core:ColorGradientStop">
<gradientPicker:GradientPickerColorStop ColorStop="{Binding}"
PositionReference="{Binding $parent[Border]}"
Classes="gradient-handle"
GradientPicker="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type gradientPicker:GradientPicker}}}">
PositionReference="{Binding $parent[Border]}"
Classes="gradient-handle"
GradientPicker="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type gradientPicker:GradientPicker}}}">
</gradientPicker:GradientPickerColorStop>
</DataTemplate>
</ItemsControl.ItemTemplate>
@ -135,7 +134,7 @@
Grid.Column="0"
Grid.ColumnSpan="2"
VerticalAlignment="Center"
Items="{TemplateBinding ColorGradient}"
Items="{TemplateBinding EditingColorGradient}"
ClipToBounds="False"
Margin="5 0">
<ItemsControl.Styles>
@ -180,9 +179,10 @@
</Border>
<Grid Grid.Row="2" Grid.Column="1" RowDefinitions="*,Auto">
<ListBox Grid.Row="0"
<ListBox Name="GradientColors"
Grid.Row="0"
MaxHeight="280"
Items="{TemplateBinding ColorGradient}"
Items="{TemplateBinding EditingColorGradient}"
SelectedItem="{TemplateBinding SelectedColorStop, Mode=TwoWay}"
Padding="10 0 15 0">
<ListBox.ItemTemplate>
@ -201,7 +201,7 @@
<TextBox Grid.Column="1" Text="{Binding Color, Converter={StaticResource SKColorToStringConverter}}" />
<NumericUpDown Grid.Column="2" FormatString="F3" ShowButtonSpinner="False" Margin="5 0" Minimum="0" Maximum="1" Increment="0.01">
<Interaction.Behaviors>
<behaviors:LostFocusNumericUpDownBindingBehavior Value="{Binding Position}"/>
<behaviors:LostFocusNumericUpDownBindingBehavior Value="{Binding Position}" />
</Interaction.Behaviors>
</NumericUpDown>
<Button Name="DeleteButton"

View File

@ -25,12 +25,12 @@
</Grid.ContextFlyout>
<!-- Icon column -->
<shared:ArtemisIcon Grid.Column="0"
Icon="{CompiledBinding FeatureInfo.ResolvedIcon}"
Fill="False"
Width="20"
Height="20"
IsVisible="{CompiledBinding LoadException, Converter={x:Static ObjectConverters.IsNull}}" />
<avalonia:MaterialIcon Grid.Column="0"
ToolTip.Tip="{CompiledBinding FeatureType}"
Kind="{CompiledBinding FeatureIcon}"
Width="20"
Height="20"
IsVisible="{CompiledBinding LoadException, Converter={x:Static ObjectConverters.IsNull}}" />
<Button Grid.Column="0"
Classes="AppBarButton icon-button"

View File

@ -5,11 +5,17 @@ using System.Reactive;
using System.Reactive.Disposables;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.DeviceProviders;
using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects;
using Artemis.Core.Modules;
using Artemis.Core.ScriptingProviders;
using Artemis.Core.Services;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders;
using Avalonia.Threading;
using Material.Icons;
using ReactiveUI;
namespace Artemis.UI.Screens.Plugins;
@ -90,6 +96,42 @@ public class PluginFeatureViewModel : ActivatableViewModelBase
public bool CanRemovePrerequisites => FeatureInfo.PlatformPrerequisites.Any(p => p.UninstallActions.Any());
public bool IsPopupEnabled => CanInstallPrerequisites || CanRemovePrerequisites;
public MaterialIconKind FeatureIcon
{
get
{
if (FeatureInfo.FeatureType.IsAssignableTo(typeof(DeviceProvider)))
return MaterialIconKind.Devices;
if (FeatureInfo.FeatureType.IsAssignableTo(typeof(Module)))
return MaterialIconKind.VectorRectangle;
if (FeatureInfo.FeatureType.IsAssignableTo(typeof(LayerBrushProvider)))
return MaterialIconKind.Brush;
if (FeatureInfo.FeatureType.IsAssignableTo(typeof(LayerEffectProvider)))
return MaterialIconKind.AutoAwesome;
if (FeatureInfo.FeatureType.IsAssignableTo(typeof(ScriptingProvider)))
return MaterialIconKind.Code;
return MaterialIconKind.Extension;
}
}
public string FeatureType
{
get
{
if (FeatureInfo.FeatureType.IsAssignableTo(typeof(DeviceProvider)))
return "Device Provider";
if (FeatureInfo.FeatureType.IsAssignableTo(typeof(Module)))
return "Module";
if (FeatureInfo.FeatureType.IsAssignableTo(typeof(LayerBrushProvider)))
return "Layer Brush";
if (FeatureInfo.FeatureType.IsAssignableTo(typeof(LayerEffectProvider)))
return "Layer Effect";
if (FeatureInfo.FeatureType.IsAssignableTo(typeof(ScriptingProvider)))
return "Scripting Provider";
return "Miscellaneous feature";
}
}
private void ExecuteShowLogsFolder()
{
try

View File

@ -29,7 +29,22 @@ public class PlaybackViewModel : ActivatableViewModelBase
{
_profileEditorService = profileEditorService;
_settingsService = settingsService;
_repeatTimeline = true;
if (_settingsService.GetSetting("ProfileEditor.RepeatTimeline", true).Value)
{
_repeating = true;
_repeatTimeline = true;
}
else if (_settingsService.GetSetting("ProfileEditor.RepeatSegment", false).Value)
{
_repeating = true;
_repeatSegment = true;
}
else
{
_repeating = false;
_repeatTimeline = true;
}
this.WhenActivated(d =>
{
@ -42,7 +57,12 @@ public class PlaybackViewModel : ActivatableViewModelBase
_lastUpdate = DateTime.MinValue;
DispatcherTimer updateTimer = new(TimeSpan.FromMilliseconds(60.0 / 1000), DispatcherPriority.Render, Update);
updateTimer.Start();
Disposable.Create(() => updateTimer.Stop()).DisposeWith(d);
Disposable.Create(() =>
{
updateTimer.Stop();
_settingsService.GetSetting("ProfileEditor.RepeatTimeline", true).Value = _repeating && _repeatTimeline;
_settingsService.GetSetting("ProfileEditor.RepeatSegment", false).Value = _repeating && _repeatSegment;
}).DisposeWith(d);
});
PlayFromStart = ReactiveCommand.Create(ExecutePlayFromStart, this.WhenAnyValue(vm => vm.KeyBindingsEnabled));

View File

@ -21,7 +21,7 @@
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<shared:ArtemisIcon Icon="{CompiledBinding Info.ResolvedIcon}" Width="16" Height="16" Margin="0 0 5 0" />
<shared:ArtemisIcon Icon="{CompiledBinding Plugin.Info.ResolvedIcon}" Width="16" Height="16" Margin="0 0 5 0" />
<TextBlock Text="{CompiledBinding LanguageName}" />
</StackPanel>
</DataTemplate>

View File

@ -23,7 +23,7 @@
<shared:ArtemisIcon Grid.Row="0"
Grid.Column="0"
Grid.RowSpan="2"
Icon="{CompiledBinding ScriptConfiguration.Script.ScriptingProvider.Info.ResolvedIcon, FallbackValue=QuestionMark}"
Icon="{CompiledBinding ScriptConfiguration.Script.ScriptingProvider.Plugin.Info.ResolvedIcon, FallbackValue=QuestionMark}"
Width="32 "
Height="32"
Margin="0 0 10 0"

View File

@ -10,7 +10,7 @@ public class ProfileModuleViewModel : ViewModelBase
{
Module = module;
Name = module.Info.Name;
Icon = module.Info.ResolvedIcon ?? MaterialIconKind.QuestionMark.ToString();
Icon = module.Plugin.Info.ResolvedIcon ?? MaterialIconKind.QuestionMark.ToString();
Description = module.Info.Description;
}

View File

@ -55,10 +55,10 @@
<DataTemplate DataType="skiaSharp:SKColor">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<TextBlock x:Name="HexDisplay"
Text="{CompiledBinding Converter={StaticResource SKColorToStringConverter}}"
Text="{CompiledBinding Converter={StaticResource SKColorToStringConverter}, Mode=OneWay}"
VerticalAlignment="Center"
HorizontalAlignment="Stretch"
FontFamily="Consolas"/>
HorizontalAlignment="Stretch"
FontFamily="Consolas" />
<Border Margin="5 0 0 0"
VerticalAlignment="Bottom"
HorizontalAlignment="Right"
@ -69,22 +69,25 @@
BorderBrush="{DynamicResource ColorPickerButtonOutline}"
CornerRadius="4"
ClipToBounds="True">
<Border CornerRadius="4">
<Border.Background>
<SolidColorBrush Color="{Binding Converter={StaticResource SKColorToColorConverter}}" />
</Border.Background>
</Border>
<Border CornerRadius="4">
<Border.Background>
<SolidColorBrush Color="{Binding Converter={StaticResource SKColorToColorConverter}, Mode=OneWay}" />
</Border.Background>
</Border>
</Border>
</StackPanel>
</DataTemplate>
<DataTemplate DataType="core:ColorGradient">
<TextBlock Text="Color gradient" FontFamily="Consolas" />
</DataTemplate>
<DataTemplate DataType="core:Numeric">
<TextBlock Text="{Binding}" FontFamily="Consolas"/>
<TextBlock Text="{Binding Mode=OneWay}" FontFamily="Consolas" />
</DataTemplate>
<DataTemplate DataType="collections:IList">
<TextBlock Text="{Binding Count, StringFormat='List - {0} item(s)'}" FontFamily="Consolas"/>
<TextBlock Text="{Binding Count, StringFormat='List - {0} item(s)', Mode=OneWay}" FontFamily="Consolas" />
</DataTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" FontFamily="Consolas"/>
<TextBlock Text="{Binding Mode=OneWay}" FontFamily="Consolas" />
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>

View File

@ -51,7 +51,7 @@
<avalonia:MaterialIcon Kind="AlertCircle"></avalonia:MaterialIcon>
</Button>
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="10 0 0 0" Text="{CompiledBinding Node.Name}" ToolTip.Tip="{CompiledBinding Node.Description}"/>
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="10 0 0 0" Text="{CompiledBinding Node.Name}" ToolTip.Tip="{CompiledBinding Node.Description}" />
<Button Grid.Column="2" VerticalAlignment="Center" Classes="icon-button icon-button-small" Margin="5" Command="{CompiledBinding DeleteNode}">
<avalonia:MaterialIcon Kind="Close"></avalonia:MaterialIcon>
@ -60,21 +60,36 @@
</Border>
</Border>
<Grid Grid.Row="1" ColumnDefinitions="Auto,*,Auto" Margin="4">
<StackPanel Grid.Column="0" IsVisible="{CompiledBinding HasInputPins}">
<Grid Grid.Row="1" RowDefinitions="Auto,Auto,Auto" ColumnDefinitions="Auto,*,Auto" Margin="4">
<ContentControl Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="3"
Margin="5"
Content="{CompiledBinding CustomNodeViewModel}"
IsVisible="{CompiledBinding DisplayCustomViewModelAbove}" />
<StackPanel Grid.Row="1" Grid.Column="0" IsVisible="{CompiledBinding HasInputPins}">
<ItemsControl Items="{CompiledBinding InputPinViewModels}" Margin="4 0" />
<ItemsControl Items="{CompiledBinding InputPinCollectionViewModels}" />
</StackPanel>
<ContentControl Grid.Column="1"
Name="CustomViewModelContainer"
<ContentControl Grid.Row="1"
Grid.Column="1"
Content="{CompiledBinding CustomNodeViewModel}"
IsVisible="{CompiledBinding CustomNodeViewModel, Converter={x:Static ObjectConverters.IsNotNull}}" />
VerticalAlignment="{CompiledBinding CustomViewModelVerticalAlignment}"
IsVisible="{CompiledBinding DisplayCustomViewModelBetween}" />
<StackPanel Grid.Column="2" IsVisible="{CompiledBinding HasOutputPins}">
<StackPanel Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding HasOutputPins}">
<ItemsControl Items="{CompiledBinding OutputPinViewModels}" Margin="4 0" />
<ItemsControl Items="{CompiledBinding OutputPinCollectionViewModels}" />
</StackPanel>
<ContentControl Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="3"
Margin="5"
Content="{CompiledBinding CustomNodeViewModel}"
IsVisible="{CompiledBinding DisplayCustomViewModelBelow}" />
</Grid>
</Grid>
</Border>

View File

@ -13,6 +13,7 @@ using Artemis.UI.Shared.Services.NodeEditor;
using Artemis.UI.Shared.Services.NodeEditor.Commands;
using Avalonia;
using Avalonia.Controls.Mixins;
using Avalonia.Layout;
using DynamicData;
using DynamicData.Binding;
using ReactiveUI;
@ -34,6 +35,10 @@ public class NodeViewModel : ActivatableViewModelBase
private ObservableAsPropertyHelper<bool>? _isStaticNode;
private double _startX;
private double _startY;
private bool _displayCustomViewModelAbove;
private bool _displayCustomViewModelBetween;
private bool _displayCustomViewModelBelow;
private VerticalAlignment _customViewModelVerticalAlignment;
public NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService, IWindowService windowService)
{
@ -132,15 +137,32 @@ public class NodeViewModel : ActivatableViewModelBase
}
});
if (Node is Node coreNode)
CustomNodeViewModel = coreNode.GetCustomViewModel(nodeScriptViewModel.NodeScript);
// Set up the custom node VM if needed
if (Node is ICustomViewModelNode customViewModelNode)
{
CustomNodeViewModel = customViewModelNode.GetCustomViewModel(nodeScriptViewModel.NodeScript);
if (customViewModelNode.ViewModelPosition == CustomNodeViewModelPosition.AbovePins)
DisplayCustomViewModelAbove = true;
else if (customViewModelNode.ViewModelPosition == CustomNodeViewModelPosition.BelowPins)
DisplayCustomViewModelBelow = true;
else
{
DisplayCustomViewModelBetween = true;
if (customViewModelNode.ViewModelPosition == CustomNodeViewModelPosition.BetweenPinsTop)
CustomViewModelVerticalAlignment = VerticalAlignment.Top;
else if (customViewModelNode.ViewModelPosition == CustomNodeViewModelPosition.BetweenPinsTop)
CustomViewModelVerticalAlignment = VerticalAlignment.Center;
else
CustomViewModelVerticalAlignment = VerticalAlignment.Bottom;
}
}
});
}
public bool IsStaticNode => _isStaticNode?.Value ?? true;
public bool HasInputPins => _hasInputPins?.Value ?? false;
public bool HasOutputPins => _hasOutputPins?.Value ?? false;
public NodeScriptViewModel NodeScriptViewModel { get; }
public INode Node { get; }
public ReadOnlyObservableCollection<PinViewModel> InputPinViewModels { get; }
@ -149,16 +171,40 @@ public class NodeViewModel : ActivatableViewModelBase
public ReadOnlyObservableCollection<PinCollectionViewModel> OutputPinCollectionViewModels { get; }
public ReadOnlyObservableCollection<PinViewModel> PinViewModels { get; }
public bool IsSelected
{
get => _isSelected;
set => RaiseAndSetIfChanged(ref _isSelected, value);
}
public ICustomNodeViewModel? CustomNodeViewModel
{
get => _customNodeViewModel;
set => RaiseAndSetIfChanged(ref _customNodeViewModel, value);
}
public bool IsSelected
public bool DisplayCustomViewModelAbove
{
get => _isSelected;
set => RaiseAndSetIfChanged(ref _isSelected, value);
get => _displayCustomViewModelAbove;
set => RaiseAndSetIfChanged(ref _displayCustomViewModelAbove, value);
}
public bool DisplayCustomViewModelBetween
{
get => _displayCustomViewModelBetween;
set => RaiseAndSetIfChanged(ref _displayCustomViewModelBetween, value);
}
public bool DisplayCustomViewModelBelow
{
get => _displayCustomViewModelBelow;
set => RaiseAndSetIfChanged(ref _displayCustomViewModelBelow, value);
}
public VerticalAlignment CustomViewModelVerticalAlignment
{
get => _customViewModelVerticalAlignment;
set => RaiseAndSetIfChanged(ref _customViewModelVerticalAlignment, value);
}
public ReactiveCommand<Unit, Unit> ShowBrokenState { get; }

View File

@ -105,6 +105,7 @@ public class RegistrationService : IRegistrationService
_nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(SKColor), new SKColor(0xFFAD3EED));
_nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(IList), new SKColor(0xFFED3E61));
_nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(Enum), new SKColor(0xFF1E90FF));
_nodeService.RegisterTypeColor(Constants.CorePlugin, typeof(ColorGradient), new SKColor(0xFF00B2A9));
foreach (Type nodeType in typeof(SumNumericsNode).Assembly.GetTypes().Where(t => typeof(INode).IsAssignableFrom(t) && t.IsPublic && !t.IsAbstract && !t.IsInterface))
{

View File

@ -37,6 +37,9 @@
<DependentUpon>DataModelEventNodeCustomView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Nodes\Color\Screens\ColorGradientNodeCustomView.axaml.cs">
<DependentUpon>ColorGradientNodeCustomView.axaml</DependentUpon>
</Compile>
</ItemGroup>
</Project>

View File

@ -0,0 +1,102 @@
using Artemis.Core;
using Artemis.Core.Events;
using SkiaSharp;
namespace Artemis.VisualScripting.Nodes.Color;
[Node("Color Gradient (Advanced)", "Outputs a Color Gradient from colors and positions", "Color", OutputType = typeof(ColorGradient))]
public class ColorGradientFromPinsNode : Node
{
public OutputPin<ColorGradient> Gradient { get; set; }
public InputPinCollection<SKColor> Colors { get; set; }
public InputPinCollection<Numeric> Positions { get; set; }
public ColorGradientFromPinsNode() : base("Color Gradient", "Outputs a Color Gradient from colors and positions")
{
Colors = CreateInputPinCollection<SKColor>("Colors", 0);
Positions = CreateInputPinCollection<Numeric>("Positions", 0);
Gradient = CreateOutputPin<ColorGradient>("Gradient");
Colors.PinAdded += OnPinAdded;
Colors.PinRemoved += OnPinRemoved;
Positions.PinAdded += OnPinAdded;
Positions.PinRemoved += OnPinRemoved;
}
private void OnPinRemoved(object? sender, SingleValueEventArgs<IPin> e)
{
int colorsCount = Colors.Count();
int positionsCount = Positions.Count();
if (colorsCount == positionsCount)
return;
while (colorsCount > positionsCount)
{
IPin pinToRemove = Colors.Last();
Colors.Remove(pinToRemove);
--colorsCount;
}
while (positionsCount > colorsCount)
{
IPin pinToRemove = Positions.Last();
Positions.Remove(pinToRemove);
--positionsCount;
}
RenamePins();
}
private void OnPinAdded(object? sender, SingleValueEventArgs<IPin> e)
{
int colorsCount = Colors.Count();
int positionsCount = Positions.Count();
if (colorsCount == positionsCount)
return;
while (colorsCount < positionsCount)
{
Colors.Add(Colors.CreatePin());
++colorsCount;
}
while (positionsCount < colorsCount)
{
Positions.Add(Positions.CreatePin());
++positionsCount;
}
RenamePins();
}
private void RenamePins()
{
int colors = 0;
foreach (IPin item in Colors)
{
item.Name = $"Color #{++colors}";
}
int positions = 0;
foreach (IPin item in Positions)
{
item.Name = $"Position #{++positions}";
}
}
public override void Evaluate()
{
List<ColorGradientStop> stops = new List<ColorGradientStop>();
InputPin<SKColor>[] colors = Colors.Pins.ToArray();
InputPin<Numeric>[] positions = Positions.Pins.ToArray();
for (int i = 0; i < colors.Length; i++)
{
stops.Add(new ColorGradientStop(colors[i].Value, positions[i].Value));
}
Gradient.Value = new ColorGradient(stops);
}
}

View File

@ -0,0 +1,108 @@
using System.Collections.Specialized;
using Artemis.Core;
using Artemis.VisualScripting.Nodes.Color.Screens;
using SkiaSharp;
namespace Artemis.VisualScripting.Nodes.Color;
[Node("Color Gradient (Simple)", "Outputs a color gradient with the given colors", "Color", OutputType = typeof(ColorGradient))]
public class ColorGradientNode : Node<ColorGradient, ColorGradientNodeCustomViewModel>
{
private readonly List<InputPin> _inputPins;
public ColorGradientNode() : base("Color Gradient", "Outputs a color gradient with the given colors")
{
_inputPins = new List<InputPin>();
Gradient = ColorGradient.GetUnicornBarf();
Output = CreateOutputPin<ColorGradient>();
ViewModelPosition = CustomNodeViewModelPosition.AbovePins;
}
public ColorGradient Gradient { get; private set; }
public OutputPin<ColorGradient> Output { get; }
public override void Initialize(INodeScript script)
{
UpdateGradient();
ComputeInputPins();
// Not expecting storage to get modified, but lets just make sure
StorageModified += OnStorageModified;
}
public override void Evaluate()
{
ColorGradientStop[] stops = Gradient.ToArray();
if (_inputPins.Count != stops.Length)
return;
for (int i = 0; i < _inputPins.Count; i++)
{
// if nothing is connected, leave the stop alone.
if (_inputPins[i].ConnectedTo.Count == 0)
continue;
// if the pin has a connection, update the stop.
if (_inputPins[i].PinValue is SKColor color)
stops[i].Color = color;
}
Output.Value = Gradient;
}
private void DisconnectAllInputPins()
{
foreach (InputPin item in _inputPins)
item.DisconnectAll();
}
private void UpdateGradient()
{
Gradient.CollectionChanged -= OnGradientCollectionChanged;
if (Storage != null)
Gradient = Storage;
else
Storage = Gradient;
Gradient.CollectionChanged += OnGradientCollectionChanged;
}
private void ComputeInputPins()
{
int newAmount = Gradient.Count;
if (newAmount == _inputPins.Count)
return;
while (newAmount > _inputPins.Count)
_inputPins.Add(CreateOrAddInputPin(typeof(SKColor), string.Empty));
while (newAmount < _inputPins.Count)
{
InputPin pin = _inputPins.Last();
RemovePin(pin);
_inputPins.Remove(pin);
}
int index = 0;
foreach (InputPin item in _inputPins)
item.Name = $"Color #{++index}";
}
private void OnStorageModified(object? sender, EventArgs e)
{
UpdateGradient();
ComputeInputPins();
}
private void OnGradientCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
// if the user reorders the gradient, let it slide and do nothing.
// of course, the user might want to change the input pins since they will no longer line up.
if (e.Action == NotifyCollectionChangedAction.Move)
return;
// DisconnectAllInputPins();
ComputeInputPins();
}
}

View File

@ -23,7 +23,7 @@ public class RampSKColorNode : Node<ColorGradient, RampSKColorNodeCustomViewMode
public override void Evaluate()
{
Output.Value = Storage?.GetColor(Input.Value) ?? SKColor.Empty;
Output.Value = Storage?.GetColor(Input.Value % 1.0) ?? SKColor.Empty;
}
#endregion

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:screens="clr-namespace:Artemis.VisualScripting.Nodes.Color.Screens"
xmlns:gradientPicker="clr-namespace:Artemis.UI.Shared.Controls.GradientPicker;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.VisualScripting.Nodes.Color.Screens.ColorGradientNodeCustomView"
x:DataType="screens:ColorGradientNodeCustomViewModel">
<gradientPicker:GradientPickerButton Classes="condensed"
ColorGradient="{CompiledBinding Gradient}"
VerticalAlignment="Top"
FlyoutOpened="GradientPickerButton_OnFlyoutOpened"
FlyoutClosed="GradientPickerButton_OnFlyoutClosed" />
</UserControl>

View File

@ -0,0 +1,27 @@
using Artemis.UI.Shared.Controls.GradientPicker;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.VisualScripting.Nodes.Color.Screens;
public class ColorGradientNodeCustomView : ReactiveUserControl<ColorGradientNodeCustomViewModel>
{
public ColorGradientNodeCustomView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void GradientPickerButton_OnFlyoutOpened(GradientPickerButton sender, EventArgs args)
{
}
private void GradientPickerButton_OnFlyoutClosed(GradientPickerButton sender, EventArgs args)
{
ViewModel?.StoreGradient();
}
}

View File

@ -0,0 +1,28 @@
using Artemis.Core;
using Artemis.UI.Shared.Services.NodeEditor;
using Artemis.UI.Shared.Services.NodeEditor.Commands;
using Artemis.UI.Shared.VisualScripting;
namespace Artemis.VisualScripting.Nodes.Color.Screens;
public class ColorGradientNodeCustomViewModel : CustomNodeViewModel
{
private readonly ColorGradientNode _node;
private readonly INodeEditorService _nodeEditorService;
/// <inheritdoc />
public ColorGradientNodeCustomViewModel(ColorGradientNode node, INodeScript script, INodeEditorService nodeEditorService) : base(node, script)
{
_node = node;
_nodeEditorService = nodeEditorService;
Gradient = _node.Gradient;
}
public ColorGradient Gradient { get; }
public void StoreGradient()
{
_nodeEditorService.ExecuteCommand(Script, new UpdateStorage<ColorGradient>(_node, Gradient));
}
}

View File

@ -56,6 +56,9 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
// If the path is a regular value, evaluate the current value
else if (_oldValuePin != null && _newValuePin != null)
{
if (_newValuePin.IsNumeric)
pathValue = new Numeric(pathValue);
if (Equals(_lastValue, pathValue))
{
TimeSinceLastTrigger.Value = (DateTime.Now - _lastTrigger).TotalMilliseconds;
@ -119,6 +122,9 @@ public class DataModelEventNode : Node<DataModelPathEntity, DataModelEventNodeCu
if (propertyType == null)
return;
if (Numeric.IsTypeCompatible(propertyType))
propertyType = typeof(Numeric);
_oldValuePin = CreateOrAddOutputPin(propertyType, "Old value");
_newValuePin = CreateOrAddOutputPin(propertyType, "New value");
_lastValue = null;