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

Node editor - Added node creation by dropping cables in empty space

Node editor - Added undo/redo to static value nodes
UI - Throw exception when binding a non-reactive view to an activatable view model
This commit is contained in:
Robert 2022-03-26 19:13:51 +01:00
parent 06ab2c5bb6
commit 2ae1f5f56c
31 changed files with 630 additions and 253 deletions

View File

@ -192,6 +192,52 @@ namespace Artemis.Core
return enumerableType?.GenericTypeArguments[0];
}
/// <summary>
/// Determines if the <paramref name="typeToCheck"></paramref> is of a certain <paramref name="genericType"/>.
/// </summary>
/// <param name="typeToCheck">The type to check.</param>
/// <param name="genericType">The generic type it should be or implement</param>
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;
}
}
/// <summary>
/// Determines a display name for the given type
/// </summary>

View File

@ -1,83 +1,117 @@
using System;
using Artemis.Storage.Entities.Profile.Nodes;
namespace Artemis.Core
namespace Artemis.Core;
/// <summary>
/// Represents node data describing a certain <see cref="INode" />
/// </summary>
public class NodeData
{
/// <summary>
/// Represents node data describing a certain <see cref="INode" />
/// </summary>
public class NodeData
#region Constructors
internal NodeData(Plugin plugin, Type type, string name, string description, string category, Type? inputType, Type? outputType, Func<INodeScript, NodeEntity?, INode> create)
{
#region Constructors
internal NodeData(Plugin plugin, Type type, string name, string description, string category, Type? inputType, Type? outputType, Func<INodeScript, NodeEntity?, INode> create)
{
Plugin = plugin;
Type = type;
Name = name;
Description = description;
Category = category;
InputType = inputType;
OutputType = outputType;
_create = create;
}
#endregion
#region Methods
/// <summary>
/// Creates a new instance of the node this data represents
/// </summary>
/// <param name="script">The script to create the node for</param>
/// <param name="entity">An optional storage entity to apply to the node</param>
/// <returns>The returning node of type <see cref="Type" /></returns>
public INode CreateNode(INodeScript script, NodeEntity? entity)
{
return _create(script, entity);
}
#endregion
#region Properties & Fields
/// <summary>
/// Gets the plugin that provided this node data
/// </summary>
public Plugin Plugin { get; }
/// <summary>
/// Gets the type of <see cref="INode" /> this data represents
/// </summary>
public Type Type { get; }
/// <summary>
/// Gets the name of the node this data represents
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the description of the node this data represents
/// </summary>
public string Description { get; }
/// <summary>
/// Gets the category of the node this data represents
/// </summary>
public string Category { get; }
/// <summary>
/// Gets the primary input type of the node this data represents
/// </summary>
public Type? InputType { get; }
/// <summary>
/// Gets the primary output of the node this data represents
/// </summary>
public Type? OutputType { get; }
private readonly Func<INodeScript, NodeEntity?, INode> _create;
#endregion
Plugin = plugin;
Type = type;
Name = name;
Description = description;
Category = category;
InputType = inputType;
OutputType = outputType;
_create = create;
}
#endregion
#region Methods
/// <summary>
/// Creates a new instance of the node this data represents
/// </summary>
/// <param name="script">The script to create the node for</param>
/// <param name="entity">An optional storage entity to apply to the node</param>
/// <returns>The returning node of type <see cref="Type" /></returns>
public INode CreateNode(INodeScript script, NodeEntity? entity)
{
return _create(script, entity);
}
#endregion
/// <summary>
/// Determines whether the given pin is compatible with this node data's node.
/// </summary>
/// <param name="pin">
/// The pin to check compatibility with, if <see langword="null" /> then the node data is always
/// considered compatible.
/// </param>
/// <returns>
/// <see langword="true" /> if the pin is compatible with this node data's node; otherwise <see langword="false" />.
/// </returns>
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);
}
/// <summary>
/// Determines whether the given text matches this node data for a search query.
/// </summary>
/// <param name="text">The text to search for.</param>
/// <returns>
/// <see langword="true" /> if the node matches; otherwise <see langword="false" />.
/// </returns>
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
/// <summary>
/// Gets the plugin that provided this node data
/// </summary>
public Plugin Plugin { get; }
/// <summary>
/// Gets the type of <see cref="INode" /> this data represents
/// </summary>
public Type Type { get; }
/// <summary>
/// Gets the name of the node this data represents
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the description of the node this data represents
/// </summary>
public string Description { get; }
/// <summary>
/// Gets the category of the node this data represents
/// </summary>
public string Category { get; }
/// <summary>
/// Gets the primary input type of the node this data represents
/// </summary>
public Type? InputType { get; }
/// <summary>
/// Gets the primary output of the node this data represents
/// </summary>
public Type? OutputType { get; }
private readonly Func<INodeScript, NodeEntity?, INode> _create;
#endregion
}

View File

@ -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

View File

@ -242,6 +242,7 @@
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAlwaysTreatStructAsNotReorderableMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=activatable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Hotkey/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=luma/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=pixmap/@EntryIndexedValue">True</s:Boolean>

View File

@ -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",

View File

@ -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",

View File

@ -20,6 +20,7 @@
<PackageReference Include="Avalonia" Version="0.10.13" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.13" />
<PackageReference Include="Avalonia.Svg.Skia" Version="0.10.12" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="0.10.13.2" />
<PackageReference Include="DynamicData" Version="7.5.4" />
<PackageReference Include="FluentAvaloniaUI" Version="1.3.0" />
<PackageReference Include="Material.Icons.Avalonia" Version="1.0.2" />

View File

@ -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;
/// <summary>
/// Represents a behavior that can be used to make a text box only update it's binding on focus loss.
/// </summary>
public class LostFocusNumberBoxBindingBehavior : Behavior<NumberBox>
{
public static readonly StyledProperty<double> ValueProperty = AvaloniaProperty.Register<LostFocusTextBoxBindingBehavior, double>(
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);
}
/// <inheritdoc />
protected override void OnAttached()
{
if (AssociatedObject != null)
AssociatedObject.LostFocus += OnLostFocus;
base.OnAttached();
}
/// <inheritdoc />
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;
}
}

View File

@ -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;
/// <summary>
/// Represents a behavior that can be used to make a text box only update it's binding on focus loss.
/// </summary>
public class LostFocusTextBoxBindingBehavior : Behavior<TextBox>
{
public static readonly StyledProperty<string> TextProperty = AvaloniaProperty.Register<LostFocusTextBoxBindingBehavior, string>(
"Text", defaultBindingMode: BindingMode.TwoWay);
static LostFocusTextBoxBindingBehavior()
{
TextProperty.Changed.Subscribe(e => ((LostFocusTextBoxBindingBehavior) e.Sender).OnBindingValueChanged());
}
public string Text
{
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
/// <inheritdoc />
protected override void OnAttached()
{
if (AssociatedObject != null)
AssociatedObject.LostFocus += OnLostFocus;
base.OnAttached();
}
/// <inheritdoc />
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;
}
}

View File

@ -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;

View File

@ -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",

View File

@ -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",

View File

@ -21,9 +21,7 @@
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.13" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.13" />
<PackageReference Include="Avalonia.Svg.Skia" Version="0.10.12" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="0.10.12.2" />
<PackageReference Include="Avalonia.Xaml.Interactions" Version="0.10.12.2" />
<PackageReference Include="Avalonia.Xaml.Interactivity" Version="0.10.12.2" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="0.10.13.2" />
<PackageReference Include="DynamicData" Version="7.5.4" />
<PackageReference Include="FluentAvaloniaUI" Version="1.3.0" />
<PackageReference Include="Flurl.Http" Version="3.2.0" />

View File

@ -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<SidebarProfileConfigurationViewModel>
{
public SidebarProfileConfigurationView()
{

View File

@ -30,24 +30,33 @@
</Template>
</Setter>
</Style>
<Style Selector="TreeView#NodeTree">
</Style>
</UserControl.Styles>
<Border Classes="picker-container">
<Grid RowDefinitions="Auto,*">
<TextBox Name="SearchBox" Text="{CompiledBinding SearchText}" Margin="0 0 0 15"></TextBox>
<ListBox Grid.Row="1"
Items="{CompiledBinding Nodes}"
SelectedItem="{CompiledBinding SelectedNode}"
IsVisible="{CompiledBinding Nodes.Count}">
<ListBox.ItemTemplate>
<DataTemplate DataType="core:NodeData">
<StackPanel>
<TextBlock Text="{CompiledBinding Name}" FontWeight="Bold"></TextBlock>
<TextBlock Text="{CompiledBinding Description}"></TextBlock>
<TextBox Name="SearchBox" Text="{CompiledBinding SearchText}" Margin="0 0 0 15" Watermark="Search"></TextBox>
<TreeView Name="NodeTree" Grid.Row="1" Items="{CompiledBinding Categories}" IsVisible="{CompiledBinding Categories.Count}" SelectedItem="{CompiledBinding SelectedNode}">
<TreeView.Styles>
<Style Selector="TreeViewItem">
<Setter Property="IsExpanded" Value="True" />
</Style>
</TreeView.Styles>
<TreeView.DataTemplates>
<TreeDataTemplate DataType="{x:Type core:NodeData}">
<StackPanel Margin="-15 1 0 1">
<TextBlock Classes="BodyStrongTextBlockStyle" Text="{Binding Name}"></TextBlock>
<TextBlock Foreground="{DynamicResource TextFillColorSecondary}" Text="{Binding Description}"></TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Grid.Row="1" VerticalAlignment="Center" Spacing="20" IsVisible="{CompiledBinding !Nodes.Count}">
</TreeDataTemplate>
<TreeDataTemplate ItemsSource="{Binding Items}">
<TextBlock Text="{Binding Key}"></TextBlock>
</TreeDataTemplate>
</TreeView.DataTemplates>
</TreeView>
<StackPanel Grid.Row="1" VerticalAlignment="Center" Spacing="20" IsVisible="{CompiledBinding !Categories.Count}">
<avalonia:MaterialIcon Kind="CloseCircle" Width="64" Height="64"></avalonia:MaterialIcon>
<TextBlock Classes="h4" TextAlignment="Center">None of the nodes match your search</TextBlock>
</StackPanel>

View File

@ -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<NodePickerViewModel>
{
public NodePickerView()
{
InitializeComponent();
this.WhenActivated(
d => ViewModel
.WhenAnyValue(vm => vm.IsVisible)
.Where(visible => !visible)
.Subscribe(_ => this.FindLogicalAncestorOfType<Grid>()?.ContextFlyout?.Hide())
.DisposeWith(d)
);
}
namespace Artemis.UI.Screens.VisualScripting;
private void InitializeComponent()
public class NodePickerView : ReactiveUserControl<NodePickerViewModel>
{
public NodePickerView()
{
InitializeComponent();
this.WhenActivated(d =>
{
AvaloniaXamlLoader.Load(this);
}
ViewModel?.WhenAnyValue(vm => vm.IsVisible).Where(visible => !visible).Subscribe(_ => this.FindLogicalAncestorOfType<Grid>()?.ContextFlyout?.Hide()).DisposeWith(d);
this.Get<TextBox>("SearchBox").SelectAll();
});
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

View File

@ -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<NodeData>(_nodeService.AvailableNodes);
SourceList<NodeData> nodeSourceList = new();
IObservable<Func<NodeData, bool>> 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<DynamicData.List.IGrouping<NodeData, string>> 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<NodeData> Nodes { get; }
public ReadOnlyObservableCollection<DynamicData.List.IGrouping<NodeData, string>> 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<NodeData, bool> CreatePredicate(string? text, IPin? targetPin)
{
if (string.IsNullOrWhiteSpace(text))
return data => data.IsCompatibleWithPin(targetPin);
return data => data.IsCompatibleWithPin(targetPin) && data.MatchesSearch(text);
}
}

View File

@ -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<NodeScriptViewModel>
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)

View File

@ -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<NodeViewModel> _nodeViewModels;
private readonly INodeVmFactory _nodeVmFactory;
private readonly INodeService _nodeService;
private readonly SourceList<NodeViewModel> _nodeViewModels;
private readonly Subject<Point> _requestedPickerPositionSubject;
private DragCableViewModel? _dragViewModel;
private List<NodeViewModel>? _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<NodeViewModel>();
_requestedPickerPositionSubject = new Subject<Point>();
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<CableViewModel> CableViewModels { get; }
public NodePickerViewModel NodePickerViewModel { get; }
public NodeEditorHistory History { get; }
public IObservable<Point> 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<NodeData> 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<INode> eventArgs)

View File

@ -55,7 +55,7 @@ public class PinView : ReactiveUserControl<PinViewModel>
if (targetPin == ViewModel)
targetPin = null;
this.FindAncestorOfType<NodeScriptView>()?.ViewModel?.FinishPinDrag(ViewModel, targetPin);
this.FindAncestorOfType<NodeScriptView>()?.ViewModel?.FinishPinDrag(ViewModel, targetPin, point.Position);
_pinPoint.Cursor = new Cursor(StandardCursorType.Hand);
e.Handled = true;
}

View File

@ -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<T>, 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;
}
}

View File

@ -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",

View File

@ -21,6 +21,7 @@
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.13" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.13" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="0.10.13.2" />
<PackageReference Include="Ninject" Version="3.3.4" />
<PackageReference Include="NoStringEvaluating" Version="2.2.2" />
<PackageReference Include="ReactiveUI" Version="17.1.50" />

View File

@ -12,18 +12,15 @@ public class NumericConverter : IValueConverter
/// <inheritdoc />
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;
}
/// <inheritdoc />
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);
}
}

View File

@ -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<Numeric>(_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<string>(_node, value));
}
}

View File

@ -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">
<UserControl.Resources>
<converters:NumericConverter x:Key="NumericConverter" />
</UserControl.Resources>
<controls:NumberBox VerticalAlignment="Center"
MinWidth="75"
Value="{Binding Node.Storage, Converter={StaticResource NumericConverter}}"
SimpleNumberFormat="F3"
Classes="condensed"/>
<controls:NumberBox VerticalAlignment="Center" MinWidth="75" SimpleNumberFormat="F3" Classes="condensed">
<Interaction.Behaviors>
<behaviors:LostFocusNumberBoxBindingBehavior Value="{CompiledBinding CurrentValue, Converter={StaticResource NumericConverter}}"/>
</Interaction.Behaviors>
</controls:NumberBox>
</UserControl>

View File

@ -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<StaticNumericValueNodeCustomViewModel>
{
public StaticNumericValueNodeCustomView()
{

View File

@ -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">
<TextBox VerticalAlignment="Center" MinWidth="75" Text="{Binding Node.Storage}" Classes="condensed" />
<TextBox VerticalAlignment="Center" MinWidth="75" Classes="condensed">
<Interaction.Behaviors>
<behaviors:LostFocusTextBoxBindingBehavior Text="{CompiledBinding CurrentValue}"/>
</Interaction.Behaviors>
</TextBox>
</UserControl>

View File

@ -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<StaticStringValueNodeCustomViewModel>
{
public StaticStringValueNodeCustomView()
{

View File

@ -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<EasingTypeNodeCustomViewModel>
{
public EasingTypeNodeCustomView()
{

View File

@ -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",