diff --git a/src/Artemis.Core/Extensions/TypeExtensions.cs b/src/Artemis.Core/Extensions/TypeExtensions.cs index 9a148caef..e8c25e05a 100644 --- a/src/Artemis.Core/Extensions/TypeExtensions.cs +++ b/src/Artemis.Core/Extensions/TypeExtensions.cs @@ -192,6 +192,52 @@ namespace Artemis.Core return enumerableType?.GenericTypeArguments[0]; } + /// + /// Determines if the is of a certain . + /// + /// The type to check. + /// The generic type it should be or implement + public static bool IsOfGenericType(this Type typeToCheck, Type genericType) + { + return typeToCheck.IsOfGenericType(genericType, out Type _); + } + + private static bool IsOfGenericType(this Type typeToCheck, Type genericType, out Type concreteGenericType) + { + while (true) + { + concreteGenericType = null; + + if (genericType == null) + throw new ArgumentNullException(nameof(genericType)); + + if (!genericType.IsGenericTypeDefinition) + throw new ArgumentException("The definition needs to be a GenericTypeDefinition", nameof(genericType)); + + if (typeToCheck == null || typeToCheck == typeof(object)) + return false; + + if (typeToCheck == genericType) + { + concreteGenericType = typeToCheck; + return true; + } + + if ((typeToCheck.IsGenericType ? typeToCheck.GetGenericTypeDefinition() : typeToCheck) == genericType) + { + concreteGenericType = typeToCheck; + return true; + } + + if (genericType.IsInterface) + foreach (var i in typeToCheck.GetInterfaces()) + if (i.IsOfGenericType(genericType, out concreteGenericType)) + return true; + + typeToCheck = typeToCheck.BaseType; + } + } + /// /// Determines a display name for the given type /// diff --git a/src/Artemis.Core/VisualScripting/NodeData.cs b/src/Artemis.Core/VisualScripting/NodeData.cs index f6e2a1ca3..6259f6ca6 100644 --- a/src/Artemis.Core/VisualScripting/NodeData.cs +++ b/src/Artemis.Core/VisualScripting/NodeData.cs @@ -1,83 +1,117 @@ using System; using Artemis.Storage.Entities.Profile.Nodes; -namespace Artemis.Core +namespace Artemis.Core; + +/// +/// Represents node data describing a certain +/// +public class NodeData { - /// - /// Represents node data describing a certain - /// - public class NodeData + #region Constructors + + internal NodeData(Plugin plugin, Type type, string name, string description, string category, Type? inputType, Type? outputType, Func create) { - #region Constructors - - internal NodeData(Plugin plugin, Type type, string name, string description, string category, Type? inputType, Type? outputType, Func create) - { - Plugin = plugin; - Type = type; - Name = name; - Description = description; - Category = category; - InputType = inputType; - OutputType = outputType; - _create = create; - } - - #endregion - - #region Methods - - /// - /// Creates a new instance of the node this data represents - /// - /// The script to create the node for - /// An optional storage entity to apply to the node - /// The returning node of type - public INode CreateNode(INodeScript script, NodeEntity? entity) - { - return _create(script, entity); - } - - #endregion - - #region Properties & Fields - - /// - /// Gets the plugin that provided this node data - /// - public Plugin Plugin { get; } - - /// - /// Gets the type of this data represents - /// - public Type Type { get; } - - /// - /// Gets the name of the node this data represents - /// - public string Name { get; } - - /// - /// Gets the description of the node this data represents - /// - public string Description { get; } - - /// - /// Gets the category of the node this data represents - /// - public string Category { get; } - - /// - /// Gets the primary input type of the node this data represents - /// - public Type? InputType { get; } - - /// - /// Gets the primary output of the node this data represents - /// - public Type? OutputType { get; } - - private readonly Func _create; - - #endregion + Plugin = plugin; + Type = type; + Name = name; + Description = description; + Category = category; + InputType = inputType; + OutputType = outputType; + _create = create; } + + #endregion + + #region Methods + + /// + /// Creates a new instance of the node this data represents + /// + /// The script to create the node for + /// An optional storage entity to apply to the node + /// The returning node of type + public INode CreateNode(INodeScript script, NodeEntity? entity) + { + return _create(script, entity); + } + + #endregion + + /// + /// Determines whether the given pin is compatible with this node data's node. + /// + /// + /// The pin to check compatibility with, if then the node data is always + /// considered compatible. + /// + /// + /// if the pin is compatible with this node data's node; otherwise . + /// + public bool IsCompatibleWithPin(IPin? pin) + { + if (pin == null) + return true; + + if (pin.Direction == PinDirection.Input) + return OutputType != null && pin.IsTypeCompatible(OutputType); + return InputType != null && pin.IsTypeCompatible(InputType); + } + + /// + /// Determines whether the given text matches this node data for a search query. + /// + /// The text to search for. + /// + /// if the node matches; otherwise . + /// + public bool MatchesSearch(string text) + { + text = text.Trim(); + return Name.Contains(text, StringComparison.InvariantCultureIgnoreCase) || + Description.Contains(text, StringComparison.InvariantCultureIgnoreCase) || + Category.Contains(text, StringComparison.InvariantCultureIgnoreCase); + } + + #region Properties & Fields + + /// + /// Gets the plugin that provided this node data + /// + public Plugin Plugin { get; } + + /// + /// Gets the type of this data represents + /// + public Type Type { get; } + + /// + /// Gets the name of the node this data represents + /// + public string Name { get; } + + /// + /// Gets the description of the node this data represents + /// + public string Description { get; } + + /// + /// Gets the category of the node this data represents + /// + public string Category { get; } + + /// + /// Gets the primary input type of the node this data represents + /// + public Type? InputType { get; } + + /// + /// Gets the primary output of the node this data represents + /// + public Type? OutputType { get; } + + private readonly Func _create; + + #endregion } \ No newline at end of file diff --git a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodePresenter.cs b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodePresenter.cs index 56ae49395..9c611d37f 100644 --- a/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodePresenter.cs +++ b/src/Artemis.VisualScripting/Editor/Controls/VisualScriptNodePresenter.cs @@ -158,7 +158,7 @@ namespace Artemis.VisualScripting.Editor.Controls if (Node?.Node is Node customViewModelNode) { - CustomViewModel = customViewModelNode.GetCustomViewModel(); + CustomViewModel = customViewModelNode.GetCustomViewModel((NodeScript) Node.Script.Script); // CustomViewModel?.OnActivate(); } else diff --git a/src/Artemis.sln.DotSettings b/src/Artemis.sln.DotSettings index 7af04acad..6943df083 100644 --- a/src/Artemis.sln.DotSettings +++ b/src/Artemis.sln.DotSettings @@ -242,6 +242,7 @@ True True True + True True True True diff --git a/src/Avalonia/Artemis.UI.Linux/packages.lock.json b/src/Avalonia/Artemis.UI.Linux/packages.lock.json index 748e8244f..66c4330a9 100644 --- a/src/Avalonia/Artemis.UI.Linux/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Linux/packages.lock.json @@ -157,29 +157,29 @@ }, "Avalonia.Xaml.Behaviors": { "type": "Transitive", - "resolved": "0.10.12.2", - "contentHash": "EqfzwstvqQcWnTJnaBvezxKwBSddozXpkFi5WrzVe976zedE+A1NruFgnC19aG7Vvy0mTQdlWFTtbAInv6IQyg==", + "resolved": "0.10.13.2", + "contentHash": "sZlq6FFzNNzYmHK+vARWFpxtDY4XUdnU6q6zVIm4l1iQ3/ZXor4SeUnYDdd3lFZtoJ9yc8K2g4X7d/lVEgV9tA==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Xaml.Interactions": "0.10.12.2", - "Avalonia.Xaml.Interactivity": "0.10.12.2" + "Avalonia": "0.10.13", + "Avalonia.Xaml.Interactions": "0.10.13.2", + "Avalonia.Xaml.Interactivity": "0.10.13.2" } }, "Avalonia.Xaml.Interactions": { "type": "Transitive", - "resolved": "0.10.12.2", - "contentHash": "01NGXHMbvpg1JcZ4tFAZXD6i55vHIQnJl3+HFi7RSP1jevkjkSaVM8qjwLsTSfREsJ2OoiWxx2LcyUQJvO5Kjw==", + "resolved": "0.10.13.2", + "contentHash": "bMgr5NtEjJ/qvf+1JD4T4rRt9AbZNnJdYCx5cBfGyXHETbeliTJAt07mqTahcoPY1G2FskF1OSIW5ytljbviLw==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Xaml.Interactivity": "0.10.12.2" + "Avalonia": "0.10.13", + "Avalonia.Xaml.Interactivity": "0.10.13.2" } }, "Avalonia.Xaml.Interactivity": { "type": "Transitive", - "resolved": "0.10.12.2", - "contentHash": "AGAbT1I6XW1+9tweLHDMGX8+SijE111vNNIQy2gI3bpbLfPYTirLPyK0do2s9V6l7hHfQnNmiX2NA6JHC4WG4Q==", + "resolved": "0.10.13.2", + "contentHash": "OIjK5XCsUrBCqog8lxI/DEbubaNQRwy8e8Px4i3dvllomU28EYsJm4XtrPVakY7MC+we825uXY47tsO/benLug==", "dependencies": { - "Avalonia": "0.10.12" + "Avalonia": "0.10.13" } }, "Castle.Core": { @@ -1778,9 +1778,7 @@ "Avalonia.Diagnostics": "0.10.13", "Avalonia.ReactiveUI": "0.10.13", "Avalonia.Svg.Skia": "0.10.12", - "Avalonia.Xaml.Behaviors": "0.10.12.2", - "Avalonia.Xaml.Interactions": "0.10.12.2", - "Avalonia.Xaml.Interactivity": "0.10.12.2", + "Avalonia.Xaml.Behaviors": "0.10.13.2", "DynamicData": "7.5.4", "FluentAvaloniaUI": "1.3.0", "Flurl.Http": "3.2.0", @@ -1801,6 +1799,7 @@ "Avalonia": "0.10.13", "Avalonia.ReactiveUI": "0.10.13", "Avalonia.Svg.Skia": "0.10.12", + "Avalonia.Xaml.Behaviors": "0.10.13.2", "DynamicData": "7.5.4", "FluentAvaloniaUI": "1.3.0", "Material.Icons.Avalonia": "1.0.2", @@ -1817,6 +1816,7 @@ "Artemis.UI.Shared": "1.0.0", "Avalonia": "0.10.13", "Avalonia.ReactiveUI": "0.10.13", + "Avalonia.Xaml.Behaviors": "0.10.13.2", "Ninject": "3.3.4", "NoStringEvaluating": "2.2.2", "ReactiveUI": "17.1.50", diff --git a/src/Avalonia/Artemis.UI.MacOS/packages.lock.json b/src/Avalonia/Artemis.UI.MacOS/packages.lock.json index 748e8244f..66c4330a9 100644 --- a/src/Avalonia/Artemis.UI.MacOS/packages.lock.json +++ b/src/Avalonia/Artemis.UI.MacOS/packages.lock.json @@ -157,29 +157,29 @@ }, "Avalonia.Xaml.Behaviors": { "type": "Transitive", - "resolved": "0.10.12.2", - "contentHash": "EqfzwstvqQcWnTJnaBvezxKwBSddozXpkFi5WrzVe976zedE+A1NruFgnC19aG7Vvy0mTQdlWFTtbAInv6IQyg==", + "resolved": "0.10.13.2", + "contentHash": "sZlq6FFzNNzYmHK+vARWFpxtDY4XUdnU6q6zVIm4l1iQ3/ZXor4SeUnYDdd3lFZtoJ9yc8K2g4X7d/lVEgV9tA==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Xaml.Interactions": "0.10.12.2", - "Avalonia.Xaml.Interactivity": "0.10.12.2" + "Avalonia": "0.10.13", + "Avalonia.Xaml.Interactions": "0.10.13.2", + "Avalonia.Xaml.Interactivity": "0.10.13.2" } }, "Avalonia.Xaml.Interactions": { "type": "Transitive", - "resolved": "0.10.12.2", - "contentHash": "01NGXHMbvpg1JcZ4tFAZXD6i55vHIQnJl3+HFi7RSP1jevkjkSaVM8qjwLsTSfREsJ2OoiWxx2LcyUQJvO5Kjw==", + "resolved": "0.10.13.2", + "contentHash": "bMgr5NtEjJ/qvf+1JD4T4rRt9AbZNnJdYCx5cBfGyXHETbeliTJAt07mqTahcoPY1G2FskF1OSIW5ytljbviLw==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Xaml.Interactivity": "0.10.12.2" + "Avalonia": "0.10.13", + "Avalonia.Xaml.Interactivity": "0.10.13.2" } }, "Avalonia.Xaml.Interactivity": { "type": "Transitive", - "resolved": "0.10.12.2", - "contentHash": "AGAbT1I6XW1+9tweLHDMGX8+SijE111vNNIQy2gI3bpbLfPYTirLPyK0do2s9V6l7hHfQnNmiX2NA6JHC4WG4Q==", + "resolved": "0.10.13.2", + "contentHash": "OIjK5XCsUrBCqog8lxI/DEbubaNQRwy8e8Px4i3dvllomU28EYsJm4XtrPVakY7MC+we825uXY47tsO/benLug==", "dependencies": { - "Avalonia": "0.10.12" + "Avalonia": "0.10.13" } }, "Castle.Core": { @@ -1778,9 +1778,7 @@ "Avalonia.Diagnostics": "0.10.13", "Avalonia.ReactiveUI": "0.10.13", "Avalonia.Svg.Skia": "0.10.12", - "Avalonia.Xaml.Behaviors": "0.10.12.2", - "Avalonia.Xaml.Interactions": "0.10.12.2", - "Avalonia.Xaml.Interactivity": "0.10.12.2", + "Avalonia.Xaml.Behaviors": "0.10.13.2", "DynamicData": "7.5.4", "FluentAvaloniaUI": "1.3.0", "Flurl.Http": "3.2.0", @@ -1801,6 +1799,7 @@ "Avalonia": "0.10.13", "Avalonia.ReactiveUI": "0.10.13", "Avalonia.Svg.Skia": "0.10.12", + "Avalonia.Xaml.Behaviors": "0.10.13.2", "DynamicData": "7.5.4", "FluentAvaloniaUI": "1.3.0", "Material.Icons.Avalonia": "1.0.2", @@ -1817,6 +1816,7 @@ "Artemis.UI.Shared": "1.0.0", "Avalonia": "0.10.13", "Avalonia.ReactiveUI": "0.10.13", + "Avalonia.Xaml.Behaviors": "0.10.13.2", "Ninject": "3.3.4", "NoStringEvaluating": "2.2.2", "ReactiveUI": "17.1.50", diff --git a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj index e9e9bd769..18db8283c 100644 --- a/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj +++ b/src/Avalonia/Artemis.UI.Shared/Artemis.UI.Shared.csproj @@ -20,6 +20,7 @@ + diff --git a/src/Avalonia/Artemis.UI.Shared/Behaviors/LostFocusNumberBoxBindingBehavior.cs b/src/Avalonia/Artemis.UI.Shared/Behaviors/LostFocusNumberBoxBindingBehavior.cs new file mode 100644 index 000000000..cf004cf16 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Behaviors/LostFocusNumberBoxBindingBehavior.cs @@ -0,0 +1,56 @@ +using System; +using Avalonia; +using Avalonia.Data; +using Avalonia.Interactivity; +using Avalonia.Xaml.Interactivity; +using FluentAvalonia.UI.Controls; + +namespace Artemis.UI.Shared.Behaviors; + +/// +/// Represents a behavior that can be used to make a text box only update it's binding on focus loss. +/// +public class LostFocusNumberBoxBindingBehavior : Behavior +{ + public static readonly StyledProperty ValueProperty = AvaloniaProperty.Register( + nameof(Value), defaultBindingMode: BindingMode.TwoWay); + + static LostFocusNumberBoxBindingBehavior() + { + ValueProperty.Changed.Subscribe(e => ((LostFocusNumberBoxBindingBehavior) e.Sender).OnBindingValueChanged()); + } + + public double Value + { + get => GetValue(ValueProperty); + set => SetValue(ValueProperty, value); + } + + /// + protected override void OnAttached() + { + if (AssociatedObject != null) + AssociatedObject.LostFocus += OnLostFocus; + base.OnAttached(); + } + + /// + protected override void OnDetaching() + { + if (AssociatedObject != null) + AssociatedObject.LostFocus -= OnLostFocus; + base.OnDetaching(); + } + + private void OnLostFocus(object? sender, RoutedEventArgs e) + { + if (AssociatedObject != null) + Value = AssociatedObject.Value; + } + + private void OnBindingValueChanged() + { + if (AssociatedObject != null) + AssociatedObject.Value = Value; + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Behaviors/LostFocusTextBoxBindingBehavior.cs b/src/Avalonia/Artemis.UI.Shared/Behaviors/LostFocusTextBoxBindingBehavior.cs new file mode 100644 index 000000000..961ea308f --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Behaviors/LostFocusTextBoxBindingBehavior.cs @@ -0,0 +1,56 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Interactivity; +using Avalonia.Xaml.Interactivity; + +namespace Artemis.UI.Shared.Behaviors; + +/// +/// Represents a behavior that can be used to make a text box only update it's binding on focus loss. +/// +public class LostFocusTextBoxBindingBehavior : Behavior +{ + public static readonly StyledProperty TextProperty = AvaloniaProperty.Register( + "Text", defaultBindingMode: BindingMode.TwoWay); + + static LostFocusTextBoxBindingBehavior() + { + TextProperty.Changed.Subscribe(e => ((LostFocusTextBoxBindingBehavior) e.Sender).OnBindingValueChanged()); + } + + public string Text + { + get => GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + /// + protected override void OnAttached() + { + if (AssociatedObject != null) + AssociatedObject.LostFocus += OnLostFocus; + base.OnAttached(); + } + + /// + protected override void OnDetaching() + { + if (AssociatedObject != null) + AssociatedObject.LostFocus -= OnLostFocus; + base.OnDetaching(); + } + + private void OnLostFocus(object? sender, RoutedEventArgs e) + { + if (AssociatedObject != null) + Text = AssociatedObject.Text; + } + + private void OnBindingValueChanged() + { + if (AssociatedObject != null) + AssociatedObject.Text = Text; + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPicker.cs b/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPicker.cs index 0a9f50f3e..644388738 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPicker.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/DataModelPicker/DataModelPicker.cs @@ -13,10 +13,8 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Data; -using Material.Icons; using Material.Icons.Avalonia; using ReactiveUI; -using SkiaSharp; namespace Artemis.UI.Shared.Controls.DataModelPicker; diff --git a/src/Avalonia/Artemis.UI.Shared/packages.lock.json b/src/Avalonia/Artemis.UI.Shared/packages.lock.json index cb93e25fb..aaa59d395 100644 --- a/src/Avalonia/Artemis.UI.Shared/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Shared/packages.lock.json @@ -40,6 +40,17 @@ "Svg.Skia": "0.5.12" } }, + "Avalonia.Xaml.Behaviors": { + "type": "Direct", + "requested": "[0.10.13.2, )", + "resolved": "0.10.13.2", + "contentHash": "sZlq6FFzNNzYmHK+vARWFpxtDY4XUdnU6q6zVIm4l1iQ3/ZXor4SeUnYDdd3lFZtoJ9yc8K2g4X7d/lVEgV9tA==", + "dependencies": { + "Avalonia": "0.10.13", + "Avalonia.Xaml.Interactions": "0.10.13.2", + "Avalonia.Xaml.Interactivity": "0.10.13.2" + } + }, "DynamicData": { "type": "Direct", "requested": "[7.5.4, )", @@ -204,6 +215,23 @@ "Avalonia.Skia": "0.10.13" } }, + "Avalonia.Xaml.Interactions": { + "type": "Transitive", + "resolved": "0.10.13.2", + "contentHash": "bMgr5NtEjJ/qvf+1JD4T4rRt9AbZNnJdYCx5cBfGyXHETbeliTJAt07mqTahcoPY1G2FskF1OSIW5ytljbviLw==", + "dependencies": { + "Avalonia": "0.10.13", + "Avalonia.Xaml.Interactivity": "0.10.13.2" + } + }, + "Avalonia.Xaml.Interactivity": { + "type": "Transitive", + "resolved": "0.10.13.2", + "contentHash": "OIjK5XCsUrBCqog8lxI/DEbubaNQRwy8e8Px4i3dvllomU28EYsJm4XtrPVakY7MC+we825uXY47tsO/benLug==", + "dependencies": { + "Avalonia": "0.10.13" + } + }, "Castle.Core": { "type": "Transitive", "resolved": "4.2.0", diff --git a/src/Avalonia/Artemis.UI.Windows/packages.lock.json b/src/Avalonia/Artemis.UI.Windows/packages.lock.json index c8195bec5..f78508371 100644 --- a/src/Avalonia/Artemis.UI.Windows/packages.lock.json +++ b/src/Avalonia/Artemis.UI.Windows/packages.lock.json @@ -173,29 +173,29 @@ }, "Avalonia.Xaml.Behaviors": { "type": "Transitive", - "resolved": "0.10.12.2", - "contentHash": "EqfzwstvqQcWnTJnaBvezxKwBSddozXpkFi5WrzVe976zedE+A1NruFgnC19aG7Vvy0mTQdlWFTtbAInv6IQyg==", + "resolved": "0.10.13.2", + "contentHash": "sZlq6FFzNNzYmHK+vARWFpxtDY4XUdnU6q6zVIm4l1iQ3/ZXor4SeUnYDdd3lFZtoJ9yc8K2g4X7d/lVEgV9tA==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Xaml.Interactions": "0.10.12.2", - "Avalonia.Xaml.Interactivity": "0.10.12.2" + "Avalonia": "0.10.13", + "Avalonia.Xaml.Interactions": "0.10.13.2", + "Avalonia.Xaml.Interactivity": "0.10.13.2" } }, "Avalonia.Xaml.Interactions": { "type": "Transitive", - "resolved": "0.10.12.2", - "contentHash": "01NGXHMbvpg1JcZ4tFAZXD6i55vHIQnJl3+HFi7RSP1jevkjkSaVM8qjwLsTSfREsJ2OoiWxx2LcyUQJvO5Kjw==", + "resolved": "0.10.13.2", + "contentHash": "bMgr5NtEjJ/qvf+1JD4T4rRt9AbZNnJdYCx5cBfGyXHETbeliTJAt07mqTahcoPY1G2FskF1OSIW5ytljbviLw==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Xaml.Interactivity": "0.10.12.2" + "Avalonia": "0.10.13", + "Avalonia.Xaml.Interactivity": "0.10.13.2" } }, "Avalonia.Xaml.Interactivity": { "type": "Transitive", - "resolved": "0.10.12.2", - "contentHash": "AGAbT1I6XW1+9tweLHDMGX8+SijE111vNNIQy2gI3bpbLfPYTirLPyK0do2s9V6l7hHfQnNmiX2NA6JHC4WG4Q==", + "resolved": "0.10.13.2", + "contentHash": "OIjK5XCsUrBCqog8lxI/DEbubaNQRwy8e8Px4i3dvllomU28EYsJm4XtrPVakY7MC+we825uXY47tsO/benLug==", "dependencies": { - "Avalonia": "0.10.12" + "Avalonia": "0.10.13" } }, "Castle.Core": { @@ -1794,9 +1794,7 @@ "Avalonia.Diagnostics": "0.10.13", "Avalonia.ReactiveUI": "0.10.13", "Avalonia.Svg.Skia": "0.10.12", - "Avalonia.Xaml.Behaviors": "0.10.12.2", - "Avalonia.Xaml.Interactions": "0.10.12.2", - "Avalonia.Xaml.Interactivity": "0.10.12.2", + "Avalonia.Xaml.Behaviors": "0.10.13.2", "DynamicData": "7.5.4", "FluentAvaloniaUI": "1.3.0", "Flurl.Http": "3.2.0", @@ -1817,6 +1815,7 @@ "Avalonia": "0.10.13", "Avalonia.ReactiveUI": "0.10.13", "Avalonia.Svg.Skia": "0.10.12", + "Avalonia.Xaml.Behaviors": "0.10.13.2", "DynamicData": "7.5.4", "FluentAvaloniaUI": "1.3.0", "Material.Icons.Avalonia": "1.0.2", @@ -1833,6 +1832,7 @@ "Artemis.UI.Shared": "1.0.0", "Avalonia": "0.10.13", "Avalonia.ReactiveUI": "0.10.13", + "Avalonia.Xaml.Behaviors": "0.10.13.2", "Ninject": "3.3.4", "NoStringEvaluating": "2.2.2", "ReactiveUI": "17.1.50", diff --git a/src/Avalonia/Artemis.UI/Artemis.UI.csproj b/src/Avalonia/Artemis.UI/Artemis.UI.csproj index 16018ec21..1c0c569b1 100644 --- a/src/Avalonia/Artemis.UI/Artemis.UI.csproj +++ b/src/Avalonia/Artemis.UI/Artemis.UI.csproj @@ -21,9 +21,7 @@ - - - + diff --git a/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.axaml.cs index 0024f1448..b759491c9 100644 --- a/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.axaml.cs @@ -1,9 +1,10 @@ using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; namespace Artemis.UI.Screens.Sidebar { - public class SidebarProfileConfigurationView : UserControl + public class SidebarProfileConfigurationView : ReactiveUserControl { public SidebarProfileConfigurationView() { diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodePickerView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodePickerView.axaml index bb7c03328..9025e35c8 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodePickerView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodePickerView.axaml @@ -30,24 +30,33 @@ + + - - - - - - - + + + + + + + + + + - - - - + + + + + + + None of the nodes match your search diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodePickerView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodePickerView.axaml.cs index e6ddc86fc..27e0f04f7 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodePickerView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodePickerView.axaml.cs @@ -1,33 +1,28 @@ using System; using System.Reactive.Linq; -using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Mixins; using Avalonia.LogicalTree; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -using Avalonia.VisualTree; using ReactiveUI; -namespace Artemis.UI.Screens.VisualScripting -{ - public partial class NodePickerView : ReactiveUserControl - { - public NodePickerView() - { - InitializeComponent(); - this.WhenActivated( - d => ViewModel - .WhenAnyValue(vm => vm.IsVisible) - .Where(visible => !visible) - .Subscribe(_ => this.FindLogicalAncestorOfType()?.ContextFlyout?.Hide()) - .DisposeWith(d) - ); - } +namespace Artemis.UI.Screens.VisualScripting; - private void InitializeComponent() +public class NodePickerView : ReactiveUserControl +{ + public NodePickerView() + { + InitializeComponent(); + this.WhenActivated(d => { - AvaloniaXamlLoader.Load(this); - } + ViewModel?.WhenAnyValue(vm => vm.IsVisible).Where(visible => !visible).Subscribe(_ => this.FindLogicalAncestorOfType()?.ContextFlyout?.Hide()).DisposeWith(d); + this.Get("SearchBox").SelectAll(); + }); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodePickerViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodePickerViewModel.cs index 9e66457c8..b19f4e559 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodePickerViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodePickerViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.ObjectModel; +using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; using Artemis.Core; @@ -8,6 +9,7 @@ using Artemis.UI.Shared; using Artemis.UI.Shared.Services.NodeEditor; using Artemis.UI.Shared.Services.NodeEditor.Commands; using Avalonia; +using DynamicData; using ReactiveUI; namespace Artemis.UI.Screens.VisualScripting; @@ -16,36 +18,63 @@ public class NodePickerViewModel : ActivatableViewModelBase { private readonly INodeEditorService _nodeEditorService; private readonly NodeScript _nodeScript; - private readonly INodeService _nodeService; private bool _isVisible; private Point _position; + private DateTime _closed; private string? _searchText; - private NodeData? _selectedNode; + private object? _selectedNode; + private IPin? _targetPin; public NodePickerViewModel(NodeScript nodeScript, INodeService nodeService, INodeEditorService nodeEditorService) { _nodeScript = nodeScript; - _nodeService = nodeService; _nodeEditorService = nodeEditorService; - Nodes = new ObservableCollection(_nodeService.AvailableNodes); + SourceList nodeSourceList = new(); + IObservable> nodeFilter = this.WhenAnyValue(vm => vm.SearchText, vm => vm.TargetPin).Select(v => CreatePredicate(v.Item1, v.Item2)); + + nodeSourceList.Connect() + .Filter(nodeFilter) + .GroupWithImmutableState(n => n.Category) + .Bind(out ReadOnlyObservableCollection> categories) + .Subscribe(); + Categories = categories; this.WhenActivated(d => { + if (DateTime.Now - _closed > TimeSpan.FromSeconds(10)) + SearchText = null; + TargetPin = null; + + nodeSourceList.Edit(list => + { + list.Clear(); + list.AddRange(nodeService.AvailableNodes); + }); + IsVisible = true; - Disposable.Create(() => IsVisible = false).DisposeWith(d); + + Disposable.Create(() => + { + _closed = DateTime.Now; + IsVisible = false; + }).DisposeWith(d); }); - this.WhenAnyValue(vm => vm.SelectedNode).WhereNotNull().Throttle(TimeSpan.FromMilliseconds(200), RxApp.MainThreadScheduler).Subscribe(data => - { - CreateNode(data); - Hide(); - SelectedNode = null; - }); + this.WhenAnyValue(vm => vm.SelectedNode) + .WhereNotNull() + .Where(o => o is NodeData) + .Throttle(TimeSpan.FromMilliseconds(200), RxApp.MainThreadScheduler) + .Subscribe(data => + { + CreateNode((NodeData) data); + IsVisible = false; + SelectedNode = null; + }); } - public ObservableCollection Nodes { get; } + public ReadOnlyObservableCollection> Categories { get; } public bool IsVisible { @@ -65,29 +94,47 @@ public class NodePickerViewModel : ActivatableViewModelBase set => RaiseAndSetIfChanged(ref _searchText, value); } - public NodeData? SelectedNode + public IPin? TargetPin + { + get => _targetPin; + set => RaiseAndSetIfChanged(ref _targetPin, value); + } + + public object? SelectedNode { get => _selectedNode; set => RaiseAndSetIfChanged(ref _selectedNode, value); } - public void Show(Point position) - { - IsVisible = true; - Position = position; - } - - public void Hide() - { - IsVisible = false; - } - - private void CreateNode(NodeData data) + public void CreateNode(NodeData data) { INode node = data.CreateNode(_nodeScript, null); - node.X = Position.X; - node.Y = Position.Y; + node.X = Math.Round(Position.X / 10d, 0, MidpointRounding.AwayFromZero) * 10d; + node.Y = Math.Round(Position.Y / 10d, 0, MidpointRounding.AwayFromZero) * 10d; - _nodeEditorService.ExecuteCommand(_nodeScript, new AddNode(_nodeScript, node)); + if (TargetPin != null) + { + using (_nodeEditorService.CreateCommandScope(_nodeScript, "Create node for pin")) + { + _nodeEditorService.ExecuteCommand(_nodeScript, new AddNode(_nodeScript, node)); + + // Find the first compatible source pin for the target pin + IPin? source = TargetPin.Direction == PinDirection.Output + ? node.Pins.Concat(node.PinCollections.SelectMany(c => c)).Where(p => p.Direction == PinDirection.Input).FirstOrDefault(p => TargetPin.IsTypeCompatible(p.Type)) + : node.Pins.Concat(node.PinCollections.SelectMany(c => c)).Where(p => p.Direction == PinDirection.Output).FirstOrDefault(p => TargetPin.IsTypeCompatible(p.Type)); + + if (source != null) + _nodeEditorService.ExecuteCommand(_nodeScript, new ConnectPins(source, TargetPin)); + } + } + else + _nodeEditorService.ExecuteCommand(_nodeScript, new AddNode(_nodeScript, node)); + } + + private Func CreatePredicate(string? text, IPin? targetPin) + { + if (string.IsNullOrWhiteSpace(text)) + return data => data.IsCompatibleWithPin(targetPin); + return data => data.IsCompatibleWithPin(targetPin) && data.MatchesSearch(text); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs index cf4a8e15b..3a3d84fa4 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Artemis.UI.Shared.Controls; @@ -11,6 +12,7 @@ using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.ReactiveUI; +using ReactiveUI; namespace Artemis.UI.Screens.VisualScripting; @@ -33,6 +35,11 @@ public class NodeScriptView : ReactiveUserControl UpdateZoomBorderBackground(); _grid.AddHandler(PointerReleasedEvent, CanvasOnPointerReleased, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true); + this.WhenActivated(_ => ViewModel?.PickerPositionSubject.Subscribe(p => + { + ViewModel.NodePickerViewModel.Position = p; + _grid?.ContextFlyout?.ShowAt(_grid, true); + })); } private void CanvasOnPointerReleased(object? sender, PointerReleasedEventArgs e) diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs index 9dbe70cd0..9bdf0eb50 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reactive.Linq; +using System.Reactive.Subjects; using Artemis.Core; using Artemis.Core.Events; +using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; using Artemis.UI.Screens.VisualScripting.Pins; using Artemis.UI.Shared; @@ -23,21 +25,27 @@ public class NodeScriptViewModel : ActivatableViewModelBase { private readonly INodeEditorService _nodeEditorService; private readonly INotificationService _notificationService; - private readonly SourceList _nodeViewModels; private readonly INodeVmFactory _nodeVmFactory; + private readonly INodeService _nodeService; + private readonly SourceList _nodeViewModels; + private readonly Subject _requestedPickerPositionSubject; + private DragCableViewModel? _dragViewModel; private List? _initialNodeSelection; - public NodeScriptViewModel(NodeScript nodeScript, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService, INotificationService notificationService) + public NodeScriptViewModel(NodeScript nodeScript, INodeVmFactory nodeVmFactory, INodeService nodeService, INodeEditorService nodeEditorService, INotificationService notificationService) { _nodeVmFactory = nodeVmFactory; + _nodeService = nodeService; _nodeEditorService = nodeEditorService; _notificationService = notificationService; _nodeViewModels = new SourceList(); + _requestedPickerPositionSubject = new Subject(); NodeScript = nodeScript; NodePickerViewModel = _nodeVmFactory.NodePickerViewModel(nodeScript); History = nodeEditorService.GetHistory(NodeScript); + PickerPositionSubject = _requestedPickerPositionSubject.AsObservable(); this.WhenActivated(d => { @@ -87,6 +95,7 @@ public class NodeScriptViewModel : ActivatableViewModelBase public ReadOnlyObservableCollection CableViewModels { get; } public NodePickerViewModel NodePickerViewModel { get; } public NodeEditorHistory History { get; } + public IObservable PickerPositionSubject { get; } public DragCableViewModel? DragViewModel { @@ -165,7 +174,7 @@ public class NodeScriptViewModel : ActivatableViewModelBase return targetPinVmModel == null || targetPinVmModel.IsCompatibleWith(sourcePinViewModel); } - public void FinishPinDrag(PinViewModel sourcePinViewModel, PinViewModel? targetPinVmModel) + public void FinishPinDrag(PinViewModel sourcePinViewModel, PinViewModel? targetPinVmModel, Point position) { if (DragViewModel == null) return; @@ -175,6 +184,24 @@ public class NodeScriptViewModel : ActivatableViewModelBase // If dropped on top of a compatible pin, connect to it if (targetPinVmModel != null && targetPinVmModel.IsCompatibleWith(sourcePinViewModel)) _nodeEditorService.ExecuteCommand(NodeScript, new ConnectPins(sourcePinViewModel.Pin, targetPinVmModel.Pin)); + // If not dropped on a pin allow the user to create a new node + else if (targetPinVmModel == null) + { + // If there is only one, spawn that straight away + List singleCompatibleNode = _nodeService.AvailableNodes.Where(n => n.IsCompatibleWithPin(sourcePinViewModel.Pin)).ToList(); + if (singleCompatibleNode.Count == 1) + { + // Borrow the node picker to spawn the node in, even if it's never shown + NodePickerViewModel.TargetPin = sourcePinViewModel.Pin; + NodePickerViewModel.CreateNode(singleCompatibleNode.First()); + } + // Otherwise show the user the picker by requesting it at the drop position + else + { + _requestedPickerPositionSubject.OnNext(position); + NodePickerViewModel.TargetPin = sourcePinViewModel.Pin; + } + } } private void HandleNodeAdded(SingleValueEventArgs eventArgs) diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinView.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinView.cs index 5d7cb3bb5..34d252d65 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinView.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/Pins/PinView.cs @@ -55,7 +55,7 @@ public class PinView : ReactiveUserControl if (targetPin == ViewModel) targetPin = null; - this.FindAncestorOfType()?.ViewModel?.FinishPinDrag(ViewModel, targetPin); + this.FindAncestorOfType()?.ViewModel?.FinishPinDrag(ViewModel, targetPin, point.Position); _pinPoint.Cursor = new Cursor(StandardCursorType.Hand); e.Handled = true; } diff --git a/src/Avalonia/Artemis.UI/ViewLocator.cs b/src/Avalonia/Artemis.UI/ViewLocator.cs index c0c797ed7..09a87cddc 100644 --- a/src/Avalonia/Artemis.UI/ViewLocator.cs +++ b/src/Avalonia/Artemis.UI/ViewLocator.cs @@ -1,28 +1,33 @@ using System; +using Artemis.Core; +using Artemis.UI.Exceptions; using Avalonia.Controls; using Avalonia.Controls.Templates; +using Avalonia.ReactiveUI; using ReactiveUI; -namespace Artemis.UI +namespace Artemis.UI; + +public class ViewLocator : IDataTemplate { - public class ViewLocator : IDataTemplate + public IControl Build(object data) { - public bool SupportsRecycling => false; + Type dataType = data.GetType(); + string name = dataType.FullName!.Split('`')[0].Replace("ViewModel", "View"); + Type? type = dataType.Assembly.GetType(name); - public IControl Build(object data) - { - Type dataType = data.GetType(); - string name = dataType.FullName!.Split('`')[0].Replace("ViewModel", "View"); - Type? type = dataType.Assembly.GetType(name); + // This isn't strictly required but it's super confusing (and happens to me all the time) if you implement IActivatableViewModel but forget to make your user control reactive. + // When this happens your OnActivated never gets called and it's easy to miss. + if (data is IActivatableViewModel && type != null && !type.IsOfGenericType(typeof(ReactiveUserControl<>))) + throw new ArtemisUIException($"The views of activatable view models should inherit ReactiveUserControl, in this case ReactiveUserControl<{data.GetType().Name}>."); - if (type != null) - return (Control) Activator.CreateInstance(type)!; - return new TextBlock {Text = "Not Found: " + name}; - } + if (type != null) + return (Control) Activator.CreateInstance(type)!; + return new TextBlock {Text = "Not Found: " + name}; + } - public bool Match(object data) - { - return data is ReactiveObject; - } + public bool Match(object data) + { + return data is ReactiveObject; } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/packages.lock.json b/src/Avalonia/Artemis.UI/packages.lock.json index 1e975be58..a3900f74e 100644 --- a/src/Avalonia/Artemis.UI/packages.lock.json +++ b/src/Avalonia/Artemis.UI/packages.lock.json @@ -76,32 +76,13 @@ }, "Avalonia.Xaml.Behaviors": { "type": "Direct", - "requested": "[0.10.12.2, )", - "resolved": "0.10.12.2", - "contentHash": "EqfzwstvqQcWnTJnaBvezxKwBSddozXpkFi5WrzVe976zedE+A1NruFgnC19aG7Vvy0mTQdlWFTtbAInv6IQyg==", + "requested": "[0.10.13.2, )", + "resolved": "0.10.13.2", + "contentHash": "sZlq6FFzNNzYmHK+vARWFpxtDY4XUdnU6q6zVIm4l1iQ3/ZXor4SeUnYDdd3lFZtoJ9yc8K2g4X7d/lVEgV9tA==", "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Xaml.Interactions": "0.10.12.2", - "Avalonia.Xaml.Interactivity": "0.10.12.2" - } - }, - "Avalonia.Xaml.Interactions": { - "type": "Direct", - "requested": "[0.10.12.2, )", - "resolved": "0.10.12.2", - "contentHash": "01NGXHMbvpg1JcZ4tFAZXD6i55vHIQnJl3+HFi7RSP1jevkjkSaVM8qjwLsTSfREsJ2OoiWxx2LcyUQJvO5Kjw==", - "dependencies": { - "Avalonia": "0.10.12", - "Avalonia.Xaml.Interactivity": "0.10.12.2" - } - }, - "Avalonia.Xaml.Interactivity": { - "type": "Direct", - "requested": "[0.10.12.2, )", - "resolved": "0.10.12.2", - "contentHash": "AGAbT1I6XW1+9tweLHDMGX8+SijE111vNNIQy2gI3bpbLfPYTirLPyK0do2s9V6l7hHfQnNmiX2NA6JHC4WG4Q==", - "dependencies": { - "Avalonia": "0.10.12" + "Avalonia": "0.10.13", + "Avalonia.Xaml.Interactions": "0.10.13.2", + "Avalonia.Xaml.Interactivity": "0.10.13.2" } }, "DynamicData": { @@ -284,6 +265,23 @@ "Avalonia.Skia": "0.10.13" } }, + "Avalonia.Xaml.Interactions": { + "type": "Transitive", + "resolved": "0.10.13.2", + "contentHash": "bMgr5NtEjJ/qvf+1JD4T4rRt9AbZNnJdYCx5cBfGyXHETbeliTJAt07mqTahcoPY1G2FskF1OSIW5ytljbviLw==", + "dependencies": { + "Avalonia": "0.10.13", + "Avalonia.Xaml.Interactivity": "0.10.13.2" + } + }, + "Avalonia.Xaml.Interactivity": { + "type": "Transitive", + "resolved": "0.10.13.2", + "contentHash": "OIjK5XCsUrBCqog8lxI/DEbubaNQRwy8e8Px4i3dvllomU28EYsJm4XtrPVakY7MC+we825uXY47tsO/benLug==", + "dependencies": { + "Avalonia": "0.10.13" + } + }, "Castle.Core": { "type": "Transitive", "resolved": "4.2.0", @@ -1788,6 +1786,7 @@ "Avalonia": "0.10.13", "Avalonia.ReactiveUI": "0.10.13", "Avalonia.Svg.Skia": "0.10.12", + "Avalonia.Xaml.Behaviors": "0.10.13.2", "DynamicData": "7.5.4", "FluentAvaloniaUI": "1.3.0", "Material.Icons.Avalonia": "1.0.2", @@ -1804,6 +1803,7 @@ "Artemis.UI.Shared": "1.0.0", "Avalonia": "0.10.13", "Avalonia.ReactiveUI": "0.10.13", + "Avalonia.Xaml.Behaviors": "0.10.13.2", "Ninject": "3.3.4", "NoStringEvaluating": "2.2.2", "ReactiveUI": "17.1.50", diff --git a/src/Avalonia/Artemis.VisualScripting/Artemis.VisualScripting.csproj b/src/Avalonia/Artemis.VisualScripting/Artemis.VisualScripting.csproj index a4c935bf5..31b30959d 100644 --- a/src/Avalonia/Artemis.VisualScripting/Artemis.VisualScripting.csproj +++ b/src/Avalonia/Artemis.VisualScripting/Artemis.VisualScripting.csproj @@ -21,6 +21,7 @@ + diff --git a/src/Avalonia/Artemis.VisualScripting/Converters/NumericConverter.cs b/src/Avalonia/Artemis.VisualScripting/Converters/NumericConverter.cs index 4673f2912..320250b7e 100644 --- a/src/Avalonia/Artemis.VisualScripting/Converters/NumericConverter.cs +++ b/src/Avalonia/Artemis.VisualScripting/Converters/NumericConverter.cs @@ -12,18 +12,15 @@ public class NumericConverter : IValueConverter /// public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - if (targetType == typeof(Numeric)) - return new Numeric(value); + if (value is not Numeric numeric) + return value; - return value; + return Numeric.IsTypeCompatible(targetType) ? numeric.ToType(targetType, NumberFormatInfo.InvariantInfo) : value; } /// public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { - if (targetType == typeof(Numeric)) - return new Numeric(value); - - return value; + return new Numeric(value); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/StaticValueNodeViewModels.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/StaticValueNodeViewModels.cs index 256681acb..312f9bf2a 100644 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/StaticValueNodeViewModels.cs +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViewModels/StaticValueNodeViewModels.cs @@ -1,18 +1,47 @@ using Artemis.Core; +using Artemis.UI.Shared.Services.NodeEditor; +using Artemis.UI.Shared.Services.NodeEditor.Commands; using Artemis.UI.Shared.VisualScripting; +using ReactiveUI; namespace Artemis.VisualScripting.Nodes.CustomViewModels; public class StaticNumericValueNodeCustomViewModel : CustomNodeViewModel { - public StaticNumericValueNodeCustomViewModel(INode node, INodeScript script) : base(node, script) + private readonly StaticNumericValueNode _node; + private readonly INodeEditorService _nodeEditorService; + + public StaticNumericValueNodeCustomViewModel(StaticNumericValueNode node, INodeScript script, INodeEditorService nodeEditorService) : base(node, script) { + _node = node; + _nodeEditorService = nodeEditorService; + + NodeModified += (_, _) => this.RaisePropertyChanged(nameof(CurrentValue)); + } + + public Numeric? CurrentValue + { + get => _node.Storage; + set => _nodeEditorService.ExecuteCommand(Script, new UpdateStorage(_node, value ?? new Numeric())); } } public class StaticStringValueNodeCustomViewModel : CustomNodeViewModel { - public StaticStringValueNodeCustomViewModel(INode node, INodeScript script) : base(node, script) + private readonly StaticStringValueNode _node; + private readonly INodeEditorService _nodeEditorService; + + public StaticStringValueNodeCustomViewModel(StaticStringValueNode node, INodeScript script, INodeEditorService nodeEditorService) : base(node, script) { + _node = node; + _nodeEditorService = nodeEditorService; + + NodeModified += (_, _) => this.RaisePropertyChanged(nameof(CurrentValue)); + } + + public string? CurrentValue + { + get => _node.Storage; + set => _nodeEditorService.ExecuteCommand(Script, new UpdateStorage(_node, value)); } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml index f4ef79c1a..5ea33c945 100644 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml @@ -5,15 +5,16 @@ xmlns:customViewModels="clr-namespace:Artemis.VisualScripting.Nodes.CustomViewModels" xmlns:converters="clr-namespace:Artemis.VisualScripting.Converters" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:behaviors="clr-namespace:Artemis.UI.Shared.Behaviors;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.VisualScripting.Nodes.CustomViews.StaticNumericValueNodeCustomView" x:DataType="customViewModels:StaticNumericValueNodeCustomViewModel"> - + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml.cs index ea3efe812..118464c0f 100644 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml.cs +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticNumericValueNodeCustomView.axaml.cs @@ -1,9 +1,11 @@ +using Artemis.VisualScripting.Nodes.CustomViewModels; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; namespace Artemis.VisualScripting.Nodes.CustomViews { - public partial class StaticNumericValueNodeCustomView : UserControl + public partial class StaticNumericValueNodeCustomView : ReactiveUserControl { public StaticNumericValueNodeCustomView() { diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.axaml b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.axaml index 966921331..8748f8b6a 100644 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.axaml +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.axaml @@ -3,8 +3,13 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:customViewModels="clr-namespace:Artemis.VisualScripting.Nodes.CustomViewModels" + xmlns:behaviors="clr-namespace:Artemis.UI.Shared.Behaviors;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.VisualScripting.Nodes.CustomViews.StaticStringValueNodeCustomView" x:DataType="customViewModels:StaticStringValueNodeCustomViewModel"> - + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.axaml.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.axaml.cs index c619b4b29..c53bc453c 100644 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.axaml.cs +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/CustomViews/StaticStringValueNodeCustomView.axaml.cs @@ -1,9 +1,11 @@ +using Artemis.VisualScripting.Nodes.CustomViewModels; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; namespace Artemis.VisualScripting.Nodes.CustomViews { - public partial class StaticStringValueNodeCustomView : UserControl + public partial class StaticStringValueNodeCustomView : ReactiveUserControl { public StaticStringValueNodeCustomView() { diff --git a/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViews/EasingTypeNodeCustomView.axaml.cs b/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViews/EasingTypeNodeCustomView.axaml.cs index 7a699a2a5..7a038e3ce 100644 --- a/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViews/EasingTypeNodeCustomView.axaml.cs +++ b/src/Avalonia/Artemis.VisualScripting/Nodes/Easing/CustomViews/EasingTypeNodeCustomView.axaml.cs @@ -1,10 +1,12 @@ +using Artemis.VisualScripting.Nodes.Easing.CustomViewModels; using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; namespace Artemis.VisualScripting.Nodes.Easing.CustomViews { - public partial class EasingTypeNodeCustomView : UserControl + public partial class EasingTypeNodeCustomView : ReactiveUserControl { public EasingTypeNodeCustomView() { diff --git a/src/Avalonia/Artemis.VisualScripting/packages.lock.json b/src/Avalonia/Artemis.VisualScripting/packages.lock.json index 39d2fbeb4..beb46fa29 100644 --- a/src/Avalonia/Artemis.VisualScripting/packages.lock.json +++ b/src/Avalonia/Artemis.VisualScripting/packages.lock.json @@ -28,6 +28,17 @@ "System.Reactive": "5.0.0" } }, + "Avalonia.Xaml.Behaviors": { + "type": "Direct", + "requested": "[0.10.13.2, )", + "resolved": "0.10.13.2", + "contentHash": "sZlq6FFzNNzYmHK+vARWFpxtDY4XUdnU6q6zVIm4l1iQ3/ZXor4SeUnYDdd3lFZtoJ9yc8K2g4X7d/lVEgV9tA==", + "dependencies": { + "Avalonia": "0.10.13", + "Avalonia.Xaml.Interactions": "0.10.13.2", + "Avalonia.Xaml.Interactivity": "0.10.13.2" + } + }, "Ninject": { "type": "Direct", "requested": "[3.3.4, )", @@ -175,6 +186,23 @@ "Avalonia.Skia": "0.10.13" } }, + "Avalonia.Xaml.Interactions": { + "type": "Transitive", + "resolved": "0.10.13.2", + "contentHash": "bMgr5NtEjJ/qvf+1JD4T4rRt9AbZNnJdYCx5cBfGyXHETbeliTJAt07mqTahcoPY1G2FskF1OSIW5ytljbviLw==", + "dependencies": { + "Avalonia": "0.10.13", + "Avalonia.Xaml.Interactivity": "0.10.13.2" + } + }, + "Avalonia.Xaml.Interactivity": { + "type": "Transitive", + "resolved": "0.10.13.2", + "contentHash": "OIjK5XCsUrBCqog8lxI/DEbubaNQRwy8e8Px4i3dvllomU28EYsJm4XtrPVakY7MC+we825uXY47tsO/benLug==", + "dependencies": { + "Avalonia": "0.10.13" + } + }, "Castle.Core": { "type": "Transitive", "resolved": "4.2.0", @@ -1707,6 +1735,7 @@ "Avalonia": "0.10.13", "Avalonia.ReactiveUI": "0.10.13", "Avalonia.Svg.Skia": "0.10.12", + "Avalonia.Xaml.Behaviors": "0.10.13.2", "DynamicData": "7.5.4", "FluentAvaloniaUI": "1.3.0", "Material.Icons.Avalonia": "1.0.2",