From 06ab2c5bb67447c0a823585886244607db1186bc Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 25 Mar 2022 20:21:45 +0100 Subject: [PATCH] Node editor - Added undo/redo to event and datamodel nodes --- src/Artemis.Core/VisualScripting/Node.cs | 623 +++++++++--------- .../NodeEditor/Commands/UpdateStorage.cs | 72 ++ .../VisualScripting/CustomNodeViewModel.cs | 84 +-- .../VisualScripting/NodeScriptViewModel.cs | 12 +- .../Screens/VisualScripting/NodeViewModel.cs | 2 +- .../StaticSKColorValueNodeCustomViewModel.cs | 5 +- .../EnumEqualsNodeCustomViewModel.cs | 2 +- .../LayerPropertyNodeCustomViewModel.cs | 2 +- .../StaticValueNodeViewModels.cs | 4 +- .../DataModelEventNodeCustomViewModel.cs | 80 ++- .../DataModelNodeCustomViewModel.cs | 75 ++- .../DataModelEventNodeCustomView.axaml.cs | 4 +- .../DataModelNodeCustomView.axaml.cs | 4 +- .../Nodes/DataModel/DataModelEventNode.cs | 25 +- .../Nodes/DataModel/DataModelNode.cs | 32 +- .../EasingTypeNodeCustomViewModel.cs | 2 +- .../MathExpressionNodeCustomViewModel.cs | 2 +- 17 files changed, 609 insertions(+), 421 deletions(-) create mode 100644 src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/UpdateStorage.cs diff --git a/src/Artemis.Core/VisualScripting/Node.cs b/src/Artemis.Core/VisualScripting/Node.cs index ec2cb019b..5e8f003f1 100644 --- a/src/Artemis.Core/VisualScripting/Node.cs +++ b/src/Artemis.Core/VisualScripting/Node.cs @@ -4,353 +4,364 @@ using System.Collections.ObjectModel; using Ninject; using Ninject.Parameters; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents a kind of node inside a +/// +public abstract class Node : CorePropertyChanged, INode { - /// - /// Represents a kind of node inside a - /// - public abstract class Node : CorePropertyChanged, INode + /// + public event EventHandler? Resetting; + + #region Properties & Fields + + private string _name; + + /// + public string Name { - /// - public event EventHandler? Resetting; + get => _name; + protected set => SetAndNotify(ref _name, value); + } - #region Properties & Fields + private string _description; - private string _name; + /// + public string Description + { + get => _description; + protected set => SetAndNotify(ref _description, value); + } - /// - public string Name + private double _x; + + /// + public double X + { + get => _x; + set => SetAndNotify(ref _x, value); + } + + private double _y; + + /// + public double Y + { + get => _y; + set => SetAndNotify(ref _y, value); + } + + /// + public virtual bool IsExitNode => false; + + /// + public virtual bool IsDefaultNode => false; + + private readonly List _pins = new(); + + /// + public IReadOnlyCollection Pins => new ReadOnlyCollection(_pins); + + private readonly List _pinCollections = new(); + + /// + public IReadOnlyCollection PinCollections => new ReadOnlyCollection(_pinCollections); + + #endregion + + #region Construtors + + /// + /// Creates a new instance of the class with an empty name and description + /// + protected Node() + { + _name = string.Empty; + _description = string.Empty; + } + + /// + /// Creates a new instance of the class with the provided name and description + /// + protected Node(string name, string description) + { + _name = name; + _description = description; + } + + #endregion + + #region Methods + + /// + /// Creates a new input pin and adds it to the collection + /// + /// The name of the pin + /// The type of value the pin will hold + /// The newly created pin + protected InputPin CreateInputPin(string name = "") + { + InputPin pin = new(this, name); + _pins.Add(pin); + OnPropertyChanged(nameof(Pins)); + return pin; + } + + /// + /// Creates a new input pin and adds it to the collection + /// + /// The type of value the pin will hold + /// The name of the pin + /// The newly created pin + protected InputPin CreateInputPin(Type type, string name = "") + { + InputPin pin = new(this, type, name); + _pins.Add(pin); + OnPropertyChanged(nameof(Pins)); + return pin; + } + + /// + /// Creates a new output pin and adds it to the collection + /// + /// The name of the pin + /// The type of value the pin will hold + /// The newly created pin + protected OutputPin CreateOutputPin(string name = "") + { + OutputPin pin = new(this, name); + _pins.Add(pin); + OnPropertyChanged(nameof(Pins)); + return pin; + } + + /// + /// Creates a new output pin and adds it to the collection + /// + /// The type of value the pin will hold + /// The name of the pin + /// The newly created pin + protected OutputPin CreateOutputPin(Type type, string name = "") + { + OutputPin pin = new(this, type, name); + _pins.Add(pin); + OnPropertyChanged(nameof(Pins)); + return pin; + } + + /// + /// Removes the provided from the node and it's collection + /// + /// The pin to remove + /// if the pin was removed; otherwise . + protected bool RemovePin(Pin pin) + { + bool isRemoved = _pins.Remove(pin); + if (isRemoved) { - get => _name; - protected set => SetAndNotify(ref _name, value); - } - - private string _description; - - /// - public string Description - { - get => _description; - protected set => SetAndNotify(ref _description, value); - } - - private double _x; - - /// - public double X - { - get => _x; - set => SetAndNotify(ref _x, value); - } - - private double _y; - - /// - public double Y - { - get => _y; - set => SetAndNotify(ref _y, value); - } - - /// - public virtual bool IsExitNode => false; - - /// - public virtual bool IsDefaultNode => false; - - private readonly List _pins = new(); - - /// - public IReadOnlyCollection Pins => new ReadOnlyCollection(_pins); - - private readonly List _pinCollections = new(); - - /// - public IReadOnlyCollection PinCollections => new ReadOnlyCollection(_pinCollections); - - #endregion - - #region Construtors - - /// - /// Creates a new instance of the class with an empty name and description - /// - protected Node() - { - _name = string.Empty; - _description = string.Empty; - } - - /// - /// Creates a new instance of the class with the provided name and description - /// - protected Node(string name, string description) - { - _name = name; - _description = description; - } - - #endregion - - #region Methods - - /// - /// Creates a new input pin and adds it to the collection - /// - /// The name of the pin - /// The type of value the pin will hold - /// The newly created pin - protected InputPin CreateInputPin(string name = "") - { - InputPin pin = new(this, name); - _pins.Add(pin); + pin.DisconnectAll(); OnPropertyChanged(nameof(Pins)); - return pin; } - /// - /// Creates a new input pin and adds it to the collection - /// - /// The type of value the pin will hold - /// The name of the pin - /// The newly created pin - protected InputPin CreateInputPin(Type type, string name = "") - { - InputPin pin = new(this, type, name); - _pins.Add(pin); - OnPropertyChanged(nameof(Pins)); - return pin; - } + return isRemoved; + } - /// - /// Creates a new output pin and adds it to the collection - /// - /// The name of the pin - /// The type of value the pin will hold - /// The newly created pin - protected OutputPin CreateOutputPin(string name = "") - { - OutputPin pin = new(this, name); - _pins.Add(pin); - OnPropertyChanged(nameof(Pins)); - return pin; - } + /// + /// Creates a new input pin collection and adds it to the collection + /// + /// The type of value the pins of this collection will hold + /// The name of the pin collection + /// The amount of pins to initially add to the collection + /// The resulting input pin collection + protected InputPinCollection CreateInputPinCollection(string name = "", int initialCount = 1) + { + InputPinCollection pin = new(this, name, initialCount); + _pinCollections.Add(pin); + OnPropertyChanged(nameof(PinCollections)); + return pin; + } - /// - /// Creates a new output pin and adds it to the collection - /// - /// The type of value the pin will hold - /// The name of the pin - /// The newly created pin - protected OutputPin CreateOutputPin(Type type, string name = "") - { - OutputPin pin = new(this, type, name); - _pins.Add(pin); - OnPropertyChanged(nameof(Pins)); - return pin; - } + /// + /// Creates a new input pin collection and adds it to the collection + /// + /// The type of value the pins of this collection will hold + /// The name of the pin collection + /// The amount of pins to initially add to the collection + /// The resulting input pin collection + protected InputPinCollection CreateInputPinCollection(Type type, string name = "", int initialCount = 1) + { + InputPinCollection pin = new(this, type, name, initialCount); + _pinCollections.Add(pin); + OnPropertyChanged(nameof(PinCollections)); + return pin; + } - /// - /// Removes the provided from the node and it's collection - /// - /// The pin to remove - /// if the pin was removed; otherwise . - protected bool RemovePin(Pin pin) + /// + /// Creates a new output pin collection and adds it to the collection + /// + /// The type of value the pins of this collection will hold + /// The name of the pin collection + /// The amount of pins to initially add to the collection + /// The resulting output pin collection + protected OutputPinCollection CreateOutputPinCollection(string name = "", int initialCount = 1) + { + OutputPinCollection pin = new(this, name, initialCount); + _pinCollections.Add(pin); + OnPropertyChanged(nameof(PinCollections)); + return pin; + } + + /// + /// Removes the provided from the node and it's + /// collection + /// + /// The pin collection to remove + /// if the pin collection was removed; otherwise . + protected bool RemovePinCollection(PinCollection pinCollection) + { + bool isRemoved = _pinCollections.Remove(pinCollection); + if (isRemoved) { - bool isRemoved = _pins.Remove(pin); - if (isRemoved) - { + foreach (IPin pin in pinCollection) pin.DisconnectAll(); - OnPropertyChanged(nameof(Pins)); - } - - return isRemoved; - } - - /// - /// Creates a new input pin collection and adds it to the collection - /// - /// The type of value the pins of this collection will hold - /// The name of the pin collection - /// The amount of pins to initially add to the collection - /// The resulting input pin collection - protected InputPinCollection CreateInputPinCollection(string name = "", int initialCount = 1) - { - InputPinCollection pin = new(this, name, initialCount); - _pinCollections.Add(pin); OnPropertyChanged(nameof(PinCollections)); - return pin; } - /// - /// Creates a new input pin collection and adds it to the collection - /// - /// The type of value the pins of this collection will hold - /// The name of the pin collection - /// The amount of pins to initially add to the collection - /// The resulting input pin collection - protected InputPinCollection CreateInputPinCollection(Type type, string name = "", int initialCount = 1) - { - InputPinCollection pin = new(this, type, name, initialCount); - _pinCollections.Add(pin); - OnPropertyChanged(nameof(PinCollections)); - return pin; - } + return isRemoved; + } - /// - /// Creates a new output pin collection and adds it to the collection - /// - /// The type of value the pins of this collection will hold - /// The name of the pin collection - /// The amount of pins to initially add to the collection - /// The resulting output pin collection - protected OutputPinCollection CreateOutputPinCollection(string name = "", int initialCount = 1) - { - OutputPinCollection pin = new(this, name, initialCount); - _pinCollections.Add(pin); - OnPropertyChanged(nameof(PinCollections)); - return pin; - } + /// + public virtual void Initialize(INodeScript script) + { + } - /// - /// Removes the provided from the node and it's - /// collection - /// - /// The pin collection to remove - /// if the pin collection was removed; otherwise . - protected bool RemovePinCollection(PinCollection pinCollection) - { - bool isRemoved = _pinCollections.Remove(pinCollection); - if (isRemoved) - { - foreach (IPin pin in pinCollection) - pin.DisconnectAll(); - OnPropertyChanged(nameof(PinCollections)); - } + /// + public abstract void Evaluate(); - return isRemoved; - } + /// + public virtual void Reset() + { + foreach (IPin pin in _pins) + pin.Reset(); + foreach (IPinCollection pinCollection in _pinCollections) + pinCollection.Reset(); - /// - public virtual void Initialize(INodeScript script) - { - } - - /// - public abstract void Evaluate(); - - /// - public virtual void Reset() - { - foreach (IPin pin in _pins) - pin.Reset(); - foreach (IPinCollection pinCollection in _pinCollections) - pinCollection.Reset(); - - Resetting?.Invoke(this, EventArgs.Empty); - } - - /// - /// Called whenever the node must show it's custom view model, if , no custom view model is used - /// - /// The custom view model, if , no custom view model is used - public virtual ICustomNodeViewModel? GetCustomViewModel() - { - return null; - } - - /// - /// Serializes the object into a string - /// - /// The serialized object - internal virtual string SerializeStorage() - { - return string.Empty; - } - - /// - /// Deserializes the object and sets it - /// - /// The serialized object - internal virtual void DeserializeStorage(string serialized) - { - } - - #endregion + Resetting?.Invoke(this, EventArgs.Empty); } /// - /// Represents a kind of node inside a containing storage value of type - /// . + /// Called whenever the node must show it's custom view model, if , no custom view model is used /// - /// The type of value the node stores - public abstract class Node : Node + /// + /// The custom view model, if , no custom view model is used + public virtual ICustomNodeViewModel? GetCustomViewModel(NodeScript nodeScript) { - private TStorage? _storage; + return null; + } - /// - protected Node() - { - } + /// + /// Serializes the object into a string + /// + /// The serialized object + internal virtual string SerializeStorage() + { + return string.Empty; + } - /// - protected Node(string name, string description) : base(name, description) - { - } + /// + /// Deserializes the object and sets it + /// + /// The serialized object + internal virtual void DeserializeStorage(string serialized) + { + } - /// - /// Gets or sets the storage object of this node, this is saved across sessions - /// - public TStorage? Storage - { - get => _storage; - set => SetAndNotify(ref _storage, value); - } + #endregion +} - internal override string SerializeStorage() - { - return CoreJson.SerializeObject(Storage, true); - } +/// +/// Represents a kind of node inside a containing storage value of type +/// . +/// +/// The type of value the node stores +public abstract class Node : Node +{ + private TStorage? _storage; - internal override void DeserializeStorage(string serialized) + /// + protected Node() + { + } + + /// + protected Node(string name, string description) : base(name, description) + { + } + + /// + /// Gets or sets the storage object of this node, this is saved across sessions + /// + public TStorage? Storage + { + get => _storage; + set { - Storage = CoreJson.DeserializeObject(serialized) ?? default(TStorage); + if (SetAndNotify(ref _storage, value)) + StorageModified?.Invoke(this, EventArgs.Empty); } } /// - /// Represents a kind of node inside a containing storage value of type - /// and a view model of type . + /// Occurs whenever the storage of this node was modified. /// - /// The type of value the node stores - /// The type of view model the node uses - public abstract class Node : Node where TViewModel : ICustomNodeViewModel + public event EventHandler? StorageModified; + + internal override string SerializeStorage() { - /// - protected Node() - { - } + return CoreJson.SerializeObject(Storage, true); + } - /// - protected Node(string name, string description) : base(name, description) - { - } + internal override void DeserializeStorage(string serialized) + { + Storage = CoreJson.DeserializeObject(serialized) ?? default(TStorage); + } +} - [Inject] - internal IKernel Kernel { get; set; } = null!; +/// +/// Represents a kind of node inside a containing storage value of type +/// and a view model of type . +/// +/// The type of value the node stores +/// The type of view model the node uses +public abstract class Node : Node where TViewModel : ICustomNodeViewModel +{ + /// + protected Node() + { + } - /// - /// Called when a view model is required - /// - public virtual TViewModel GetViewModel() - { - return Kernel.Get(new ConstructorArgument("node", this)); - } + /// + protected Node(string name, string description) : base(name, description) + { + } - /// - public override ICustomNodeViewModel GetCustomViewModel() - { - return GetViewModel(); - } + [Inject] + internal IKernel Kernel { get; set; } = null!; + + /// + /// Called when a view model is required + /// + /// + public virtual TViewModel GetViewModel(NodeScript nodeScript) + { + return Kernel.Get(new ConstructorArgument("node", this), new ConstructorArgument("script", nodeScript)); + } + + /// + /// + public override ICustomNodeViewModel? GetCustomViewModel(NodeScript nodeScript) + { + return GetViewModel(nodeScript); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/UpdateStorage.cs b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/UpdateStorage.cs new file mode 100644 index 000000000..8444d5c02 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Services/NodeEditor/Commands/UpdateStorage.cs @@ -0,0 +1,72 @@ +using System; +using Artemis.Core; + +namespace Artemis.UI.Shared.Services.NodeEditor.Commands; + +/// +/// Represents a node editor command that can be used to update the storage value of a node. +/// +/// The type of value the node stores +public class UpdateStorage : INodeEditorCommand, IDisposable +{ + private readonly Node _node; + private readonly TStorage? _originalValue; + private readonly TStorage? _value; + private readonly string _valueDescription; + private bool _updatedValue; + + /// + /// Creates a new instance of the class. + /// + /// The node to update. + /// The new value of the node. + /// The description of the value that was updated. + public UpdateStorage(Node node, TStorage? value, string valueDescription = "value") + { + _node = node; + _value = value; + _valueDescription = valueDescription; + + _originalValue = _node.Storage; + } + + #region Implementation of INodeEditorCommand + + /// + public string DisplayName => $"Update node {_valueDescription}"; + + /// + public void Execute() + { + _updatedValue = true; + _node.Storage = _value; + } + + /// + public void Undo() + { + _updatedValue = false; + _node.Storage = _originalValue; + } + + #endregion + + #region IDisposable + + /// + public void Dispose() + { + if (_updatedValue) + { + if (_originalValue is IDisposable disposable) + disposable.Dispose(); + } + else + { + if (_value is IDisposable disposable) + disposable.Dispose(); + } + } + + #endregion +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/VisualScripting/CustomNodeViewModel.cs b/src/Avalonia/Artemis.UI.Shared/VisualScripting/CustomNodeViewModel.cs index dcff99245..211ab8517 100644 --- a/src/Avalonia/Artemis.UI.Shared/VisualScripting/CustomNodeViewModel.cs +++ b/src/Avalonia/Artemis.UI.Shared/VisualScripting/CustomNodeViewModel.cs @@ -4,46 +4,54 @@ using System.Reactive.Disposables; using Artemis.Core; using ReactiveUI; -namespace Artemis.UI.Shared.VisualScripting +namespace Artemis.UI.Shared.VisualScripting; + +/// +/// Represents a custom view model for a node +/// +public abstract class CustomNodeViewModel : ActivatableViewModelBase, ICustomNodeViewModel { - public abstract class CustomNodeViewModel : ActivatableViewModelBase, ICustomNodeViewModel + /// + /// Creates a new instance of the class. + /// + /// The node the view model is for. + /// The script the node is contained in. + protected CustomNodeViewModel(INode node, INodeScript script) { - protected CustomNodeViewModel(INode node) + Node = node; + Script = script; + + this.WhenActivated(d => { - Node = node; - - this.WhenActivated(d => - { - Node.PropertyChanged += NodeOnPropertyChanged; - Disposable.Create(() => Node.PropertyChanged -= NodeOnPropertyChanged).DisposeWith(d); - }); - } - - public INode Node { get; } - - #region Events - - /// - public event EventHandler NodeModified; - - /// - /// Invokes the event - /// - protected virtual void OnNodeModified() - { - NodeModified?.Invoke(this, EventArgs.Empty); - } - - #endregion - - #region Event handlers - - private void NodeOnPropertyChanged(object? sender, PropertyChangedEventArgs e) - { - if (e.PropertyName == "Storage") - OnNodeModified(); - } - - #endregion + Node.PropertyChanged += NodeOnPropertyChanged; + Disposable.Create(() => Node.PropertyChanged -= NodeOnPropertyChanged).DisposeWith(d); + }); } + + /// + /// Gets the node the view model is for. + /// + public INode Node { get; } + + /// + /// Gets script the node is contained in. + /// + public INodeScript Script { get; } + + /// + /// Invokes the event + /// + protected virtual void OnNodeModified() + { + NodeModified?.Invoke(this, EventArgs.Empty); + } + + private void NodeOnPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == "Storage") + OnNodeModified(); + } + + /// + public event EventHandler? NodeModified; } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs index 99b3892ea..9dbe70cd0 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs @@ -8,6 +8,7 @@ using Artemis.Core.Events; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.VisualScripting.Pins; using Artemis.UI.Shared; +using Artemis.UI.Shared.Services.Interfaces; using Artemis.UI.Shared.Services.NodeEditor; using Artemis.UI.Shared.Services.NodeEditor.Commands; using Avalonia; @@ -21,15 +22,17 @@ namespace Artemis.UI.Screens.VisualScripting; public class NodeScriptViewModel : ActivatableViewModelBase { private readonly INodeEditorService _nodeEditorService; + private readonly INotificationService _notificationService; private readonly SourceList _nodeViewModels; private readonly INodeVmFactory _nodeVmFactory; private DragCableViewModel? _dragViewModel; private List? _initialNodeSelection; - public NodeScriptViewModel(NodeScript nodeScript, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService) + public NodeScriptViewModel(NodeScript nodeScript, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService, INotificationService notificationService) { _nodeVmFactory = nodeVmFactory; _nodeEditorService = nodeEditorService; + _notificationService = notificationService; _nodeViewModels = new SourceList(); NodeScript = nodeScript; @@ -44,6 +47,13 @@ public class NodeScriptViewModel : ActivatableViewModelBase Observable.FromEventPattern>(x => NodeScript.NodeRemoved += x, x => NodeScript.NodeRemoved -= x) .Subscribe(e => HandleNodeRemoved(e.EventArgs)) .DisposeWith(d); + + nodeEditorService.GetHistory(NodeScript).Undo + .Subscribe(c => _notificationService.CreateNotification().WithMessage(c != null ? $"Undid {c.DisplayName}" : "Nothing to undo").Show()) + .DisposeWith(d); + nodeEditorService.GetHistory(NodeScript).Redo + .Subscribe(c => _notificationService.CreateNotification().WithMessage(c != null ? $"Redid {c.DisplayName}" : "Nothing to redo").Show()) + .DisposeWith(d); }); // Create VMs for all nodes diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs index 8da647b8c..e26d17a2b 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs @@ -113,7 +113,7 @@ public class NodeViewModel : ActivatableViewModelBase })).DisposeWith(d); if (Node is Node coreNode) - CustomNodeViewModel = coreNode.GetCustomViewModel(); + CustomNodeViewModel = coreNode.GetCustomViewModel(nodeScriptViewModel.NodeScript); }); } diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Color/CustomViewModels/StaticSKColorValueNodeCustomViewModel.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/Color/CustomViewModels/StaticSKColorValueNodeCustomViewModel.cs index a71435a36..8e0fc74e3 100644 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/Color/CustomViewModels/StaticSKColorValueNodeCustomViewModel.cs +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Color/CustomViewModels/StaticSKColorValueNodeCustomViewModel.cs @@ -1,10 +1,11 @@ -using Artemis.UI.Shared.VisualScripting; +using Artemis.Core; +using Artemis.UI.Shared.VisualScripting; namespace Artemis.VisualScripting.Nodes.Color.CustomViewModels; public class StaticSKColorValueNodeCustomViewModel : CustomNodeViewModel { - public StaticSKColorValueNodeCustomViewModel(StaticSKColorValueNode node) : base(node) + public StaticSKColorValueNodeCustomViewModel(StaticSKColorValueNode node, INodeScript script) : base(node, script) { } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/EnumEqualsNodeCustomViewModel.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/EnumEqualsNodeCustomViewModel.cs index 54d8bbc7e..f5874a6c3 100644 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/EnumEqualsNodeCustomViewModel.cs +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/EnumEqualsNodeCustomViewModel.cs @@ -10,7 +10,7 @@ public class EnumEqualsNodeCustomViewModel : CustomNodeViewModel { private readonly EnumEqualsNode _node; - public EnumEqualsNodeCustomViewModel(EnumEqualsNode node) : base(node) + public EnumEqualsNodeCustomViewModel(EnumEqualsNode node, INodeScript script) : base(node, script) { _node = node; } diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/LayerPropertyNodeCustomViewModel.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/LayerPropertyNodeCustomViewModel.cs index f11a6ab08..d5ebe03ef 100644 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/LayerPropertyNodeCustomViewModel.cs +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/LayerPropertyNodeCustomViewModel.cs @@ -11,7 +11,7 @@ public class LayerPropertyNodeCustomViewModel : CustomNodeViewModel private RenderProfileElement _selectedProfileElement; - public LayerPropertyNodeCustomViewModel(LayerPropertyNode node) : base(node) + public LayerPropertyNodeCustomViewModel(LayerPropertyNode node, INodeScript script) : base(node, script) { _node = node; } diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/StaticValueNodeViewModels.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/StaticValueNodeViewModels.cs index 271e3d4dc..256681acb 100644 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/StaticValueNodeViewModels.cs +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/StaticValueNodeViewModels.cs @@ -5,14 +5,14 @@ namespace Artemis.VisualScripting.Nodes.CustomViewModels; public class StaticNumericValueNodeCustomViewModel : CustomNodeViewModel { - public StaticNumericValueNodeCustomViewModel(INode node) : base(node) + public StaticNumericValueNodeCustomViewModel(INode node, INodeScript script) : base(node, script) { } } public class StaticStringValueNodeCustomViewModel : CustomNodeViewModel { - public StaticStringValueNodeCustomViewModel(INode node) : base(node) + public StaticStringValueNodeCustomViewModel(INode node, INodeScript script) : base(node, script) { } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelEventNodeCustomViewModel.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelEventNodeCustomViewModel.cs index 8ebb713a4..e0a1b8a34 100644 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelEventNodeCustomViewModel.cs +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelEventNodeCustomViewModel.cs @@ -1,9 +1,11 @@ using System.Collections.ObjectModel; -using System.ComponentModel; using System.Reactive.Disposables; using Artemis.Core; using Artemis.Core.Modules; using Artemis.Core.Services; +using Artemis.Storage.Entities.Profile; +using Artemis.UI.Shared.Services.NodeEditor; +using Artemis.UI.Shared.Services.NodeEditor.Commands; using Artemis.UI.Shared.VisualScripting; using ReactiveUI; @@ -12,29 +14,40 @@ namespace Artemis.VisualScripting.Nodes.DataModel.CustomViewModels; public class DataModelEventNodeCustomViewModel : CustomNodeViewModel { private readonly DataModelEventNode _node; + private readonly INodeEditorService _nodeEditorService; + private bool _updating; + private DataModelPath? _dataModelPath; private ObservableCollection? _modules; - public DataModelEventNodeCustomViewModel(DataModelEventNode node, ISettingsService settingsService) : base(node) + public DataModelEventNodeCustomViewModel(DataModelEventNode node, INodeScript script, ISettingsService settingsService, INodeEditorService nodeEditorService) : base(node, script) { _node = node; + _nodeEditorService = nodeEditorService; ShowFullPaths = settingsService.GetSetting("ProfileEditor.ShowFullPaths", true); ShowDataModelValues = settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false); + Modules = new ObservableCollection(); this.WhenActivated(d => { - if (Modules != null) - return; + // Set up extra modules + if (_node.Script?.Context is Profile scriptProfile && scriptProfile.Configuration.Module != null) + Modules = new ObservableCollection {scriptProfile.Configuration.Module}; + else if (_node.Script?.Context is ProfileConfiguration profileConfiguration && profileConfiguration.Module != null) + Modules = new ObservableCollection {profileConfiguration.Module}; - Modules = new ObservableCollection(); - if (_node.Script.Context is Profile scriptProfile && scriptProfile.Configuration.Module != null) - Modules.Add(scriptProfile.Configuration.Module); - else if (_node.Script.Context is ProfileConfiguration profileConfiguration && profileConfiguration.Module != null) - Modules.Add(profileConfiguration.Module); + // Subscribe to node changes + _node.WhenAnyValue(n => n.Storage).Subscribe(UpdateDataModelPath).DisposeWith(d); + UpdateDataModelPath(_node.Storage); - _node.PropertyChanged += NodeOnPropertyChanged; - Disposable.Create(() => _node.PropertyChanged -= NodeOnPropertyChanged).DisposeWith(d); + Disposable.Create(() => + { + _dataModelPath?.Dispose(); + _dataModelPath = null; + }).DisposeWith(d); }); + + this.WhenAnyValue(vm => vm.DataModelPath).Subscribe(ApplyDataModelPath); } public PluginSetting ShowFullPaths { get; } @@ -47,25 +60,48 @@ public class DataModelEventNodeCustomViewModel : CustomNodeViewModel set => RaiseAndSetIfChanged(ref _modules, value); } - public DataModelPath DataModelPath + public DataModelPath? DataModelPath { - get => _node.DataModelPath; - set + get => _dataModelPath; + set => RaiseAndSetIfChanged(ref _dataModelPath, value); + } + + private void UpdateDataModelPath(DataModelPathEntity? entity) + { + try { - if (ReferenceEquals(_node.DataModelPath, value)) + if (_updating) return; - _node.DataModelPath?.Dispose(); - _node.DataModelPath = value; - _node.DataModelPath.Save(); + _updating = true; - _node.Storage = _node.DataModelPath.Entity; + DataModelPath? old = DataModelPath; + DataModelPath = entity != null ? new DataModelPath(entity) : null; + old?.Dispose(); + } + finally + { + _updating = false; } } - private void NodeOnPropertyChanged(object? sender, PropertyChangedEventArgs e) + private void ApplyDataModelPath(DataModelPath? path) { - if (e.PropertyName == nameof(DataModelNode.DataModelPath)) - this.RaisePropertyChanged(nameof(DataModelPath)); + try + { + if (_updating) + return; + if (path?.Path == _node.Storage?.Path) + return; + + _updating = true; + + path?.Save(); + _nodeEditorService.ExecuteCommand(Script, new UpdateStorage(_node, path?.Entity, "event")); + } + finally + { + _updating = false; + } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelNodeCustomViewModel.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelNodeCustomViewModel.cs index ead1825d2..089121775 100644 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelNodeCustomViewModel.cs +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViewModels/DataModelNodeCustomViewModel.cs @@ -4,6 +4,9 @@ using System.Reactive.Disposables; using Artemis.Core; using Artemis.Core.Modules; using Artemis.Core.Services; +using Artemis.Storage.Entities.Profile; +using Artemis.UI.Shared.Services.NodeEditor; +using Artemis.UI.Shared.Services.NodeEditor.Commands; using Artemis.UI.Shared.VisualScripting; using ReactiveUI; @@ -12,29 +15,39 @@ namespace Artemis.VisualScripting.Nodes.DataModel.CustomViewModels; public class DataModelNodeCustomViewModel : CustomNodeViewModel { private readonly DataModelNode _node; + private readonly INodeEditorService _nodeEditorService; private ObservableCollection? _modules; + private DataModelPath? _dataModelPath; + private bool _updating; - public DataModelNodeCustomViewModel(DataModelNode node, ISettingsService settingsService) : base(node) + public DataModelNodeCustomViewModel(DataModelNode node, INodeScript script, ISettingsService settingsService, INodeEditorService nodeEditorService) : base(node, script) { _node = node; + _nodeEditorService = nodeEditorService; ShowFullPaths = settingsService.GetSetting("ProfileEditor.ShowFullPaths", true); ShowDataModelValues = settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false); this.WhenActivated(d => { - if (Modules != null) - return; - - Modules = new ObservableCollection(); + // Set up extra modules if (_node.Script?.Context is Profile scriptProfile && scriptProfile.Configuration.Module != null) - Modules.Add(scriptProfile.Configuration.Module); + Modules = new ObservableCollection {scriptProfile.Configuration.Module}; else if (_node.Script?.Context is ProfileConfiguration profileConfiguration && profileConfiguration.Module != null) - Modules.Add(profileConfiguration.Module); + Modules = new ObservableCollection {profileConfiguration.Module}; - _node.PropertyChanged += NodeOnPropertyChanged; - Disposable.Create(() => _node.PropertyChanged -= NodeOnPropertyChanged).DisposeWith(d); + // Subscribe to node changes + _node.WhenAnyValue(n => n.Storage).Subscribe(UpdateDataModelPath).DisposeWith(d); + UpdateDataModelPath(_node.Storage); + + Disposable.Create(() => + { + _dataModelPath?.Dispose(); + _dataModelPath = null; + }).DisposeWith(d); }); + + this.WhenAnyValue(vm => vm.DataModelPath).Subscribe(ApplyDataModelPath); } public PluginSetting ShowFullPaths { get; } @@ -48,24 +61,46 @@ public class DataModelNodeCustomViewModel : CustomNodeViewModel public DataModelPath? DataModelPath { - get => _node.DataModelPath; - set + get => _dataModelPath; + set => RaiseAndSetIfChanged(ref _dataModelPath, value); + } + + private void UpdateDataModelPath(DataModelPathEntity? entity) + { + try { - if (ReferenceEquals(_node.DataModelPath, value)) + if (_updating) return; - _node.DataModelPath?.Dispose(); - _node.DataModelPath = value; - _node.DataModelPath?.Save(); + _updating = true; - _node.Storage = _node.DataModelPath?.Entity; - _node.UpdateOutputPin(); + DataModelPath? old = DataModelPath; + DataModelPath = entity != null ? new DataModelPath(entity) : null; + old?.Dispose(); + } + finally + { + _updating = false; } } - private void NodeOnPropertyChanged(object? sender, PropertyChangedEventArgs e) + private void ApplyDataModelPath(DataModelPath? path) { - if (e.PropertyName == nameof(DataModelNode.DataModelPath)) - this.RaisePropertyChanged(nameof(DataModelPath)); + try + { + if (_updating) + return; + if (path?.Path == _node.Storage?.Path) + return; + + _updating = true; + + path?.Save(); + _nodeEditorService.ExecuteCommand(Script, new UpdateStorage(_node, path?.Entity, "path")); + } + finally + { + _updating = false; + } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelEventNodeCustomView.axaml.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelEventNodeCustomView.axaml.cs index 18a8d72de..c6cdd09f3 100644 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelEventNodeCustomView.axaml.cs +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelEventNodeCustomView.axaml.cs @@ -1,10 +1,12 @@ +using Artemis.VisualScripting.Nodes.DataModel.CustomViewModels; using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; namespace Artemis.VisualScripting.Nodes.DataModel.CustomViews { - public partial class DataModelEventNodeCustomView : UserControl + public partial class DataModelEventNodeCustomView : ReactiveUserControl { public DataModelEventNodeCustomView() { diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelNodeCustomView.axaml.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelNodeCustomView.axaml.cs index 6fe945575..4138d3c5e 100644 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelNodeCustomView.axaml.cs +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/CustomViews/DataModelNodeCustomView.axaml.cs @@ -1,10 +1,12 @@ +using Artemis.VisualScripting.Nodes.DataModel.CustomViewModels; using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; namespace Artemis.VisualScripting.Nodes.DataModel.CustomViews { - public partial class DataModelNodeCustomView : UserControl + public partial class DataModelNodeCustomView : ReactiveUserControl { public DataModelNodeCustomView() { diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/DataModelEventNode.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/DataModelEventNode.cs index 3638a10cd..3279e01ab 100644 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/DataModelEventNode.cs +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/DataModelEventNode.cs @@ -1,4 +1,5 @@ -using Artemis.Core; +using System.ComponentModel; +using Artemis.Core; using Artemis.Core.Events; using Artemis.Storage.Entities.Profile; using Artemis.VisualScripting.Nodes.DataModel.CustomViewModels; @@ -24,6 +25,9 @@ public class DataModelEventNode : Node UpdateDataModelPath(); } public INodeScript? Script { get; set; } @@ -31,12 +35,6 @@ public class DataModelEventNode : Node _dataModelPath; - set => SetAndNotify(ref _dataModelPath, value); - } - public override void Initialize(INodeScript script) { Script = script; @@ -44,13 +42,13 @@ public class DataModelEventNode : Node _lastTrigger) { @@ -111,6 +109,14 @@ public class DataModelEventNode : Node public void Dispose() { + _dataModelPath?.Dispose(); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/DataModelNode.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/DataModelNode.cs index 5aa8d9b0e..c2204ed9d 100644 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/DataModelNode.cs +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/DataModel/DataModelNode.cs @@ -1,4 +1,5 @@ -using Artemis.Core; +using System.ComponentModel; +using Artemis.Core; using Artemis.Storage.Entities.Profile; using Artemis.VisualScripting.Nodes.DataModel.CustomViewModels; using Avalonia.Threading; @@ -13,17 +14,12 @@ public class DataModelNode : Node UpdateDataModelPath(); } public INodeScript? Script { get; private set; } public OutputPin Output { get; } - - public DataModelPath? DataModelPath - { - get => _dataModelPath; - set => SetAndNotify(ref _dataModelPath, value); - } - + public override void Initialize(INodeScript script) { Script = script; @@ -31,18 +27,26 @@ public class DataModelNode : Node public void Dispose() { - DataModelPath?.Dispose(); + _dataModelPath?.Dispose(); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViewModels/EasingTypeNodeCustomViewModel.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViewModels/EasingTypeNodeCustomViewModel.cs index ae4a1de1f..996063499 100644 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViewModels/EasingTypeNodeCustomViewModel.cs +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViewModels/EasingTypeNodeCustomViewModel.cs @@ -9,7 +9,7 @@ public class EasingTypeNodeCustomViewModel : CustomNodeViewModel private readonly EasingTypeNode _node; private NodeEasingViewModel _selectedEasingViewModel; - public EasingTypeNodeCustomViewModel(EasingTypeNode node) : base(node) + public EasingTypeNodeCustomViewModel(EasingTypeNode node, INodeScript script) : base(node, script) { _node = node; EasingViewModels = new ObservableCollection(Enum.GetValues(typeof(Easings.Functions)).Cast().Select(e => new NodeEasingViewModel(e))); diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Maths/CustomViewModels/MathExpressionNodeCustomViewModel.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/Maths/CustomViewModels/MathExpressionNodeCustomViewModel.cs index 4bbe43396..ab443be24 100644 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/Maths/CustomViewModels/MathExpressionNodeCustomViewModel.cs +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Maths/CustomViewModels/MathExpressionNodeCustomViewModel.cs @@ -5,7 +5,7 @@ namespace Artemis.VisualScripting.Nodes.Maths.CustomViewModels; public class MathExpressionNodeCustomViewModel : CustomNodeViewModel { - public MathExpressionNodeCustomViewModel(INode node) : base(node) + public MathExpressionNodeCustomViewModel(INode node, INodeScript script) : base(node, script) { } } \ No newline at end of file