mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Merge branch 'master' into development
This commit is contained in:
commit
2eeae54d58
@ -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>
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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()
|
||||
|
||||
@ -15,8 +15,7 @@ internal class EventConditionEventStartNode : DefaultNode
|
||||
|
||||
public void SetDataModelEvent(IDataModelEvent? dataModelEvent)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public void CreatePins(IDataModelEvent? dataModelEvent)
|
||||
{
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
52
src/Artemis.Core/VisualScripting/Nodes/NodeTStorage.cs
Normal file
52
src/Artemis.Core/VisualScripting/Nodes/NodeTStorage.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
87
src/Artemis.Storage/Migrations/M0021GradientNodes.cs
Normal file
87
src/Artemis.Storage/Migrations/M0021GradientNodes.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
|
||||
@ -45,7 +45,7 @@ public class UpdateProvider : IUpdateProvider, IDisposable
|
||||
Url request = API_URL.AppendPathSegments("build", "builds")
|
||||
.SetQueryParam("definitions", buildDefinition)
|
||||
.SetQueryParam("resultFilter", "succeeded")
|
||||
.SetQueryParam("branchName", "master")
|
||||
.SetQueryParam("branchName", "refs/heads/master")
|
||||
.SetQueryParam("$top", 1)
|
||||
.SetQueryParam("api-version", "6.1-preview.6");
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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; }
|
||||
|
||||
@ -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))
|
||||
{
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
108
src/Artemis.VisualScripting/Nodes/Color/ColorGradientNode.cs
Normal file
108
src/Artemis.VisualScripting/Nodes/Color/ColorGradientNode.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user