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]; 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> /// <summary>
/// Determines a display name for the given type /// Determines a display name for the given type
/// </summary> /// </summary>

View File

@ -1,83 +1,117 @@
using System; using System;
using Artemis.Storage.Entities.Profile.Nodes; 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> #region Constructors
/// Represents node data describing a certain <see cref="INode" />
/// </summary> internal NodeData(Plugin plugin, Type type, string name, string description, string category, Type? inputType, Type? outputType, Func<INodeScript, NodeEntity?, INode> create)
public class NodeData
{ {
#region Constructors Plugin = plugin;
Type = type;
internal NodeData(Plugin plugin, Type type, string name, string description, string category, Type? inputType, Type? outputType, Func<INodeScript, NodeEntity?, INode> create) Name = name;
{ Description = description;
Plugin = plugin; Category = category;
Type = type; InputType = inputType;
Name = name; OutputType = outputType;
Description = description; _create = create;
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
} }
#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) if (Node?.Node is Node customViewModelNode)
{ {
CustomViewModel = customViewModelNode.GetCustomViewModel(); CustomViewModel = customViewModelNode.GetCustomViewModel((NodeScript) Node.Script.Script);
// CustomViewModel?.OnActivate(); // CustomViewModel?.OnActivate();
} }
else 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_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_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/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/=Hotkey/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=luma/@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> <s:Boolean x:Key="/Default/UserDictionary/Words/=pixmap/@EntryIndexedValue">True</s:Boolean>

View File

@ -157,29 +157,29 @@
}, },
"Avalonia.Xaml.Behaviors": { "Avalonia.Xaml.Behaviors": {
"type": "Transitive", "type": "Transitive",
"resolved": "0.10.12.2", "resolved": "0.10.13.2",
"contentHash": "EqfzwstvqQcWnTJnaBvezxKwBSddozXpkFi5WrzVe976zedE+A1NruFgnC19aG7Vvy0mTQdlWFTtbAInv6IQyg==", "contentHash": "sZlq6FFzNNzYmHK+vARWFpxtDY4XUdnU6q6zVIm4l1iQ3/ZXor4SeUnYDdd3lFZtoJ9yc8K2g4X7d/lVEgV9tA==",
"dependencies": { "dependencies": {
"Avalonia": "0.10.12", "Avalonia": "0.10.13",
"Avalonia.Xaml.Interactions": "0.10.12.2", "Avalonia.Xaml.Interactions": "0.10.13.2",
"Avalonia.Xaml.Interactivity": "0.10.12.2" "Avalonia.Xaml.Interactivity": "0.10.13.2"
} }
}, },
"Avalonia.Xaml.Interactions": { "Avalonia.Xaml.Interactions": {
"type": "Transitive", "type": "Transitive",
"resolved": "0.10.12.2", "resolved": "0.10.13.2",
"contentHash": "01NGXHMbvpg1JcZ4tFAZXD6i55vHIQnJl3+HFi7RSP1jevkjkSaVM8qjwLsTSfREsJ2OoiWxx2LcyUQJvO5Kjw==", "contentHash": "bMgr5NtEjJ/qvf+1JD4T4rRt9AbZNnJdYCx5cBfGyXHETbeliTJAt07mqTahcoPY1G2FskF1OSIW5ytljbviLw==",
"dependencies": { "dependencies": {
"Avalonia": "0.10.12", "Avalonia": "0.10.13",
"Avalonia.Xaml.Interactivity": "0.10.12.2" "Avalonia.Xaml.Interactivity": "0.10.13.2"
} }
}, },
"Avalonia.Xaml.Interactivity": { "Avalonia.Xaml.Interactivity": {
"type": "Transitive", "type": "Transitive",
"resolved": "0.10.12.2", "resolved": "0.10.13.2",
"contentHash": "AGAbT1I6XW1+9tweLHDMGX8+SijE111vNNIQy2gI3bpbLfPYTirLPyK0do2s9V6l7hHfQnNmiX2NA6JHC4WG4Q==", "contentHash": "OIjK5XCsUrBCqog8lxI/DEbubaNQRwy8e8Px4i3dvllomU28EYsJm4XtrPVakY7MC+we825uXY47tsO/benLug==",
"dependencies": { "dependencies": {
"Avalonia": "0.10.12" "Avalonia": "0.10.13"
} }
}, },
"Castle.Core": { "Castle.Core": {
@ -1778,9 +1778,7 @@
"Avalonia.Diagnostics": "0.10.13", "Avalonia.Diagnostics": "0.10.13",
"Avalonia.ReactiveUI": "0.10.13", "Avalonia.ReactiveUI": "0.10.13",
"Avalonia.Svg.Skia": "0.10.12", "Avalonia.Svg.Skia": "0.10.12",
"Avalonia.Xaml.Behaviors": "0.10.12.2", "Avalonia.Xaml.Behaviors": "0.10.13.2",
"Avalonia.Xaml.Interactions": "0.10.12.2",
"Avalonia.Xaml.Interactivity": "0.10.12.2",
"DynamicData": "7.5.4", "DynamicData": "7.5.4",
"FluentAvaloniaUI": "1.3.0", "FluentAvaloniaUI": "1.3.0",
"Flurl.Http": "3.2.0", "Flurl.Http": "3.2.0",
@ -1801,6 +1799,7 @@
"Avalonia": "0.10.13", "Avalonia": "0.10.13",
"Avalonia.ReactiveUI": "0.10.13", "Avalonia.ReactiveUI": "0.10.13",
"Avalonia.Svg.Skia": "0.10.12", "Avalonia.Svg.Skia": "0.10.12",
"Avalonia.Xaml.Behaviors": "0.10.13.2",
"DynamicData": "7.5.4", "DynamicData": "7.5.4",
"FluentAvaloniaUI": "1.3.0", "FluentAvaloniaUI": "1.3.0",
"Material.Icons.Avalonia": "1.0.2", "Material.Icons.Avalonia": "1.0.2",
@ -1817,6 +1816,7 @@
"Artemis.UI.Shared": "1.0.0", "Artemis.UI.Shared": "1.0.0",
"Avalonia": "0.10.13", "Avalonia": "0.10.13",
"Avalonia.ReactiveUI": "0.10.13", "Avalonia.ReactiveUI": "0.10.13",
"Avalonia.Xaml.Behaviors": "0.10.13.2",
"Ninject": "3.3.4", "Ninject": "3.3.4",
"NoStringEvaluating": "2.2.2", "NoStringEvaluating": "2.2.2",
"ReactiveUI": "17.1.50", "ReactiveUI": "17.1.50",

View File

@ -157,29 +157,29 @@
}, },
"Avalonia.Xaml.Behaviors": { "Avalonia.Xaml.Behaviors": {
"type": "Transitive", "type": "Transitive",
"resolved": "0.10.12.2", "resolved": "0.10.13.2",
"contentHash": "EqfzwstvqQcWnTJnaBvezxKwBSddozXpkFi5WrzVe976zedE+A1NruFgnC19aG7Vvy0mTQdlWFTtbAInv6IQyg==", "contentHash": "sZlq6FFzNNzYmHK+vARWFpxtDY4XUdnU6q6zVIm4l1iQ3/ZXor4SeUnYDdd3lFZtoJ9yc8K2g4X7d/lVEgV9tA==",
"dependencies": { "dependencies": {
"Avalonia": "0.10.12", "Avalonia": "0.10.13",
"Avalonia.Xaml.Interactions": "0.10.12.2", "Avalonia.Xaml.Interactions": "0.10.13.2",
"Avalonia.Xaml.Interactivity": "0.10.12.2" "Avalonia.Xaml.Interactivity": "0.10.13.2"
} }
}, },
"Avalonia.Xaml.Interactions": { "Avalonia.Xaml.Interactions": {
"type": "Transitive", "type": "Transitive",
"resolved": "0.10.12.2", "resolved": "0.10.13.2",
"contentHash": "01NGXHMbvpg1JcZ4tFAZXD6i55vHIQnJl3+HFi7RSP1jevkjkSaVM8qjwLsTSfREsJ2OoiWxx2LcyUQJvO5Kjw==", "contentHash": "bMgr5NtEjJ/qvf+1JD4T4rRt9AbZNnJdYCx5cBfGyXHETbeliTJAt07mqTahcoPY1G2FskF1OSIW5ytljbviLw==",
"dependencies": { "dependencies": {
"Avalonia": "0.10.12", "Avalonia": "0.10.13",
"Avalonia.Xaml.Interactivity": "0.10.12.2" "Avalonia.Xaml.Interactivity": "0.10.13.2"
} }
}, },
"Avalonia.Xaml.Interactivity": { "Avalonia.Xaml.Interactivity": {
"type": "Transitive", "type": "Transitive",
"resolved": "0.10.12.2", "resolved": "0.10.13.2",
"contentHash": "AGAbT1I6XW1+9tweLHDMGX8+SijE111vNNIQy2gI3bpbLfPYTirLPyK0do2s9V6l7hHfQnNmiX2NA6JHC4WG4Q==", "contentHash": "OIjK5XCsUrBCqog8lxI/DEbubaNQRwy8e8Px4i3dvllomU28EYsJm4XtrPVakY7MC+we825uXY47tsO/benLug==",
"dependencies": { "dependencies": {
"Avalonia": "0.10.12" "Avalonia": "0.10.13"
} }
}, },
"Castle.Core": { "Castle.Core": {
@ -1778,9 +1778,7 @@
"Avalonia.Diagnostics": "0.10.13", "Avalonia.Diagnostics": "0.10.13",
"Avalonia.ReactiveUI": "0.10.13", "Avalonia.ReactiveUI": "0.10.13",
"Avalonia.Svg.Skia": "0.10.12", "Avalonia.Svg.Skia": "0.10.12",
"Avalonia.Xaml.Behaviors": "0.10.12.2", "Avalonia.Xaml.Behaviors": "0.10.13.2",
"Avalonia.Xaml.Interactions": "0.10.12.2",
"Avalonia.Xaml.Interactivity": "0.10.12.2",
"DynamicData": "7.5.4", "DynamicData": "7.5.4",
"FluentAvaloniaUI": "1.3.0", "FluentAvaloniaUI": "1.3.0",
"Flurl.Http": "3.2.0", "Flurl.Http": "3.2.0",
@ -1801,6 +1799,7 @@
"Avalonia": "0.10.13", "Avalonia": "0.10.13",
"Avalonia.ReactiveUI": "0.10.13", "Avalonia.ReactiveUI": "0.10.13",
"Avalonia.Svg.Skia": "0.10.12", "Avalonia.Svg.Skia": "0.10.12",
"Avalonia.Xaml.Behaviors": "0.10.13.2",
"DynamicData": "7.5.4", "DynamicData": "7.5.4",
"FluentAvaloniaUI": "1.3.0", "FluentAvaloniaUI": "1.3.0",
"Material.Icons.Avalonia": "1.0.2", "Material.Icons.Avalonia": "1.0.2",
@ -1817,6 +1816,7 @@
"Artemis.UI.Shared": "1.0.0", "Artemis.UI.Shared": "1.0.0",
"Avalonia": "0.10.13", "Avalonia": "0.10.13",
"Avalonia.ReactiveUI": "0.10.13", "Avalonia.ReactiveUI": "0.10.13",
"Avalonia.Xaml.Behaviors": "0.10.13.2",
"Ninject": "3.3.4", "Ninject": "3.3.4",
"NoStringEvaluating": "2.2.2", "NoStringEvaluating": "2.2.2",
"ReactiveUI": "17.1.50", "ReactiveUI": "17.1.50",

View File

@ -20,6 +20,7 @@
<PackageReference Include="Avalonia" Version="0.10.13" /> <PackageReference Include="Avalonia" Version="0.10.13" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.13" /> <PackageReference Include="Avalonia.ReactiveUI" Version="0.10.13" />
<PackageReference Include="Avalonia.Svg.Skia" Version="0.10.12" /> <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="DynamicData" Version="7.5.4" />
<PackageReference Include="FluentAvaloniaUI" Version="1.3.0" /> <PackageReference Include="FluentAvaloniaUI" Version="1.3.0" />
<PackageReference Include="Material.Icons.Avalonia" Version="1.0.2" /> <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;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Data; using Avalonia.Data;
using Material.Icons;
using Material.Icons.Avalonia; using Material.Icons.Avalonia;
using ReactiveUI; using ReactiveUI;
using SkiaSharp;
namespace Artemis.UI.Shared.Controls.DataModelPicker; namespace Artemis.UI.Shared.Controls.DataModelPicker;

View File

@ -40,6 +40,17 @@
"Svg.Skia": "0.5.12" "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": { "DynamicData": {
"type": "Direct", "type": "Direct",
"requested": "[7.5.4, )", "requested": "[7.5.4, )",
@ -204,6 +215,23 @@
"Avalonia.Skia": "0.10.13" "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": { "Castle.Core": {
"type": "Transitive", "type": "Transitive",
"resolved": "4.2.0", "resolved": "4.2.0",

View File

@ -173,29 +173,29 @@
}, },
"Avalonia.Xaml.Behaviors": { "Avalonia.Xaml.Behaviors": {
"type": "Transitive", "type": "Transitive",
"resolved": "0.10.12.2", "resolved": "0.10.13.2",
"contentHash": "EqfzwstvqQcWnTJnaBvezxKwBSddozXpkFi5WrzVe976zedE+A1NruFgnC19aG7Vvy0mTQdlWFTtbAInv6IQyg==", "contentHash": "sZlq6FFzNNzYmHK+vARWFpxtDY4XUdnU6q6zVIm4l1iQ3/ZXor4SeUnYDdd3lFZtoJ9yc8K2g4X7d/lVEgV9tA==",
"dependencies": { "dependencies": {
"Avalonia": "0.10.12", "Avalonia": "0.10.13",
"Avalonia.Xaml.Interactions": "0.10.12.2", "Avalonia.Xaml.Interactions": "0.10.13.2",
"Avalonia.Xaml.Interactivity": "0.10.12.2" "Avalonia.Xaml.Interactivity": "0.10.13.2"
} }
}, },
"Avalonia.Xaml.Interactions": { "Avalonia.Xaml.Interactions": {
"type": "Transitive", "type": "Transitive",
"resolved": "0.10.12.2", "resolved": "0.10.13.2",
"contentHash": "01NGXHMbvpg1JcZ4tFAZXD6i55vHIQnJl3+HFi7RSP1jevkjkSaVM8qjwLsTSfREsJ2OoiWxx2LcyUQJvO5Kjw==", "contentHash": "bMgr5NtEjJ/qvf+1JD4T4rRt9AbZNnJdYCx5cBfGyXHETbeliTJAt07mqTahcoPY1G2FskF1OSIW5ytljbviLw==",
"dependencies": { "dependencies": {
"Avalonia": "0.10.12", "Avalonia": "0.10.13",
"Avalonia.Xaml.Interactivity": "0.10.12.2" "Avalonia.Xaml.Interactivity": "0.10.13.2"
} }
}, },
"Avalonia.Xaml.Interactivity": { "Avalonia.Xaml.Interactivity": {
"type": "Transitive", "type": "Transitive",
"resolved": "0.10.12.2", "resolved": "0.10.13.2",
"contentHash": "AGAbT1I6XW1+9tweLHDMGX8+SijE111vNNIQy2gI3bpbLfPYTirLPyK0do2s9V6l7hHfQnNmiX2NA6JHC4WG4Q==", "contentHash": "OIjK5XCsUrBCqog8lxI/DEbubaNQRwy8e8Px4i3dvllomU28EYsJm4XtrPVakY7MC+we825uXY47tsO/benLug==",
"dependencies": { "dependencies": {
"Avalonia": "0.10.12" "Avalonia": "0.10.13"
} }
}, },
"Castle.Core": { "Castle.Core": {
@ -1794,9 +1794,7 @@
"Avalonia.Diagnostics": "0.10.13", "Avalonia.Diagnostics": "0.10.13",
"Avalonia.ReactiveUI": "0.10.13", "Avalonia.ReactiveUI": "0.10.13",
"Avalonia.Svg.Skia": "0.10.12", "Avalonia.Svg.Skia": "0.10.12",
"Avalonia.Xaml.Behaviors": "0.10.12.2", "Avalonia.Xaml.Behaviors": "0.10.13.2",
"Avalonia.Xaml.Interactions": "0.10.12.2",
"Avalonia.Xaml.Interactivity": "0.10.12.2",
"DynamicData": "7.5.4", "DynamicData": "7.5.4",
"FluentAvaloniaUI": "1.3.0", "FluentAvaloniaUI": "1.3.0",
"Flurl.Http": "3.2.0", "Flurl.Http": "3.2.0",
@ -1817,6 +1815,7 @@
"Avalonia": "0.10.13", "Avalonia": "0.10.13",
"Avalonia.ReactiveUI": "0.10.13", "Avalonia.ReactiveUI": "0.10.13",
"Avalonia.Svg.Skia": "0.10.12", "Avalonia.Svg.Skia": "0.10.12",
"Avalonia.Xaml.Behaviors": "0.10.13.2",
"DynamicData": "7.5.4", "DynamicData": "7.5.4",
"FluentAvaloniaUI": "1.3.0", "FluentAvaloniaUI": "1.3.0",
"Material.Icons.Avalonia": "1.0.2", "Material.Icons.Avalonia": "1.0.2",
@ -1833,6 +1832,7 @@
"Artemis.UI.Shared": "1.0.0", "Artemis.UI.Shared": "1.0.0",
"Avalonia": "0.10.13", "Avalonia": "0.10.13",
"Avalonia.ReactiveUI": "0.10.13", "Avalonia.ReactiveUI": "0.10.13",
"Avalonia.Xaml.Behaviors": "0.10.13.2",
"Ninject": "3.3.4", "Ninject": "3.3.4",
"NoStringEvaluating": "2.2.2", "NoStringEvaluating": "2.2.2",
"ReactiveUI": "17.1.50", "ReactiveUI": "17.1.50",

View File

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

View File

@ -1,9 +1,10 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Sidebar namespace Artemis.UI.Screens.Sidebar
{ {
public class SidebarProfileConfigurationView : UserControl public class SidebarProfileConfigurationView : ReactiveUserControl<SidebarProfileConfigurationViewModel>
{ {
public SidebarProfileConfigurationView() public SidebarProfileConfigurationView()
{ {

View File

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

View File

@ -1,33 +1,28 @@
using System; using System;
using System.Reactive.Linq; using System.Reactive.Linq;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Mixins; using Avalonia.Controls.Mixins;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Avalonia.VisualTree;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.VisualScripting 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)
);
}
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;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Reactive.Linq; using System.Reactive.Linq;
using Artemis.Core; using Artemis.Core;
@ -8,6 +9,7 @@ using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.NodeEditor; using Artemis.UI.Shared.Services.NodeEditor;
using Artemis.UI.Shared.Services.NodeEditor.Commands; using Artemis.UI.Shared.Services.NodeEditor.Commands;
using Avalonia; using Avalonia;
using DynamicData;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.VisualScripting; namespace Artemis.UI.Screens.VisualScripting;
@ -16,36 +18,63 @@ public class NodePickerViewModel : ActivatableViewModelBase
{ {
private readonly INodeEditorService _nodeEditorService; private readonly INodeEditorService _nodeEditorService;
private readonly NodeScript _nodeScript; private readonly NodeScript _nodeScript;
private readonly INodeService _nodeService;
private bool _isVisible; private bool _isVisible;
private Point _position; private Point _position;
private DateTime _closed;
private string? _searchText; private string? _searchText;
private NodeData? _selectedNode; private object? _selectedNode;
private IPin? _targetPin;
public NodePickerViewModel(NodeScript nodeScript, INodeService nodeService, INodeEditorService nodeEditorService) public NodePickerViewModel(NodeScript nodeScript, INodeService nodeService, INodeEditorService nodeEditorService)
{ {
_nodeScript = nodeScript; _nodeScript = nodeScript;
_nodeService = nodeService;
_nodeEditorService = nodeEditorService; _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 => 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; 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 => this.WhenAnyValue(vm => vm.SelectedNode)
{ .WhereNotNull()
CreateNode(data); .Where(o => o is NodeData)
Hide(); .Throttle(TimeSpan.FromMilliseconds(200), RxApp.MainThreadScheduler)
SelectedNode = null; .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 public bool IsVisible
{ {
@ -65,29 +94,47 @@ public class NodePickerViewModel : ActivatableViewModelBase
set => RaiseAndSetIfChanged(ref _searchText, value); set => RaiseAndSetIfChanged(ref _searchText, value);
} }
public NodeData? SelectedNode public IPin? TargetPin
{
get => _targetPin;
set => RaiseAndSetIfChanged(ref _targetPin, value);
}
public object? SelectedNode
{ {
get => _selectedNode; get => _selectedNode;
set => RaiseAndSetIfChanged(ref _selectedNode, value); set => RaiseAndSetIfChanged(ref _selectedNode, value);
} }
public void Show(Point position) public void CreateNode(NodeData data)
{
IsVisible = true;
Position = position;
}
public void Hide()
{
IsVisible = false;
}
private void CreateNode(NodeData data)
{ {
INode node = data.CreateNode(_nodeScript, null); INode node = data.CreateNode(_nodeScript, null);
node.X = Position.X; node.X = Math.Round(Position.X / 10d, 0, MidpointRounding.AwayFromZero) * 10d;
node.Y = Position.Y; 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.Collections.Generic;
using System.Linq; using System.Linq;
using Artemis.UI.Shared.Controls; using Artemis.UI.Shared.Controls;
@ -11,6 +12,7 @@ using Avalonia.Interactivity;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using ReactiveUI;
namespace Artemis.UI.Screens.VisualScripting; namespace Artemis.UI.Screens.VisualScripting;
@ -33,6 +35,11 @@ public class NodeScriptView : ReactiveUserControl<NodeScriptViewModel>
UpdateZoomBorderBackground(); UpdateZoomBorderBackground();
_grid.AddHandler(PointerReleasedEvent, CanvasOnPointerReleased, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true); _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) private void CanvasOnPointerReleased(object? sender, PointerReleasedEventArgs e)

View File

@ -3,8 +3,10 @@ using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Events; using Artemis.Core.Events;
using Artemis.Core.Services;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.VisualScripting.Pins; using Artemis.UI.Screens.VisualScripting.Pins;
using Artemis.UI.Shared; using Artemis.UI.Shared;
@ -23,21 +25,27 @@ public class NodeScriptViewModel : ActivatableViewModelBase
{ {
private readonly INodeEditorService _nodeEditorService; private readonly INodeEditorService _nodeEditorService;
private readonly INotificationService _notificationService; private readonly INotificationService _notificationService;
private readonly SourceList<NodeViewModel> _nodeViewModels;
private readonly INodeVmFactory _nodeVmFactory; private readonly INodeVmFactory _nodeVmFactory;
private readonly INodeService _nodeService;
private readonly SourceList<NodeViewModel> _nodeViewModels;
private readonly Subject<Point> _requestedPickerPositionSubject;
private DragCableViewModel? _dragViewModel; private DragCableViewModel? _dragViewModel;
private List<NodeViewModel>? _initialNodeSelection; 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; _nodeVmFactory = nodeVmFactory;
_nodeService = nodeService;
_nodeEditorService = nodeEditorService; _nodeEditorService = nodeEditorService;
_notificationService = notificationService; _notificationService = notificationService;
_nodeViewModels = new SourceList<NodeViewModel>(); _nodeViewModels = new SourceList<NodeViewModel>();
_requestedPickerPositionSubject = new Subject<Point>();
NodeScript = nodeScript; NodeScript = nodeScript;
NodePickerViewModel = _nodeVmFactory.NodePickerViewModel(nodeScript); NodePickerViewModel = _nodeVmFactory.NodePickerViewModel(nodeScript);
History = nodeEditorService.GetHistory(NodeScript); History = nodeEditorService.GetHistory(NodeScript);
PickerPositionSubject = _requestedPickerPositionSubject.AsObservable();
this.WhenActivated(d => this.WhenActivated(d =>
{ {
@ -87,6 +95,7 @@ public class NodeScriptViewModel : ActivatableViewModelBase
public ReadOnlyObservableCollection<CableViewModel> CableViewModels { get; } public ReadOnlyObservableCollection<CableViewModel> CableViewModels { get; }
public NodePickerViewModel NodePickerViewModel { get; } public NodePickerViewModel NodePickerViewModel { get; }
public NodeEditorHistory History { get; } public NodeEditorHistory History { get; }
public IObservable<Point> PickerPositionSubject { get; }
public DragCableViewModel? DragViewModel public DragCableViewModel? DragViewModel
{ {
@ -165,7 +174,7 @@ public class NodeScriptViewModel : ActivatableViewModelBase
return targetPinVmModel == null || targetPinVmModel.IsCompatibleWith(sourcePinViewModel); 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) if (DragViewModel == null)
return; return;
@ -175,6 +184,24 @@ public class NodeScriptViewModel : ActivatableViewModelBase
// If dropped on top of a compatible pin, connect to it // If dropped on top of a compatible pin, connect to it
if (targetPinVmModel != null && targetPinVmModel.IsCompatibleWith(sourcePinViewModel)) if (targetPinVmModel != null && targetPinVmModel.IsCompatibleWith(sourcePinViewModel))
_nodeEditorService.ExecuteCommand(NodeScript, new ConnectPins(sourcePinViewModel.Pin, targetPinVmModel.Pin)); _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) private void HandleNodeAdded(SingleValueEventArgs<INode> eventArgs)

View File

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

View File

@ -1,28 +1,33 @@
using System; using System;
using Artemis.Core;
using Artemis.UI.Exceptions;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.ReactiveUI;
using 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) // 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.
Type dataType = data.GetType(); if (data is IActivatableViewModel && type != null && !type.IsOfGenericType(typeof(ReactiveUserControl<>)))
string name = dataType.FullName!.Split('`')[0].Replace("ViewModel", "View"); throw new ArtemisUIException($"The views of activatable view models should inherit ReactiveUserControl<T>, in this case ReactiveUserControl<{data.GetType().Name}>.");
Type? type = dataType.Assembly.GetType(name);
if (type != null) if (type != null)
return (Control) Activator.CreateInstance(type)!; return (Control) Activator.CreateInstance(type)!;
return new TextBlock {Text = "Not Found: " + name}; return new TextBlock {Text = "Not Found: " + name};
} }
public bool Match(object data) public bool Match(object data)
{ {
return data is ReactiveObject; return data is ReactiveObject;
}
} }
} }

View File

@ -76,32 +76,13 @@
}, },
"Avalonia.Xaml.Behaviors": { "Avalonia.Xaml.Behaviors": {
"type": "Direct", "type": "Direct",
"requested": "[0.10.12.2, )", "requested": "[0.10.13.2, )",
"resolved": "0.10.12.2", "resolved": "0.10.13.2",
"contentHash": "EqfzwstvqQcWnTJnaBvezxKwBSddozXpkFi5WrzVe976zedE+A1NruFgnC19aG7Vvy0mTQdlWFTtbAInv6IQyg==", "contentHash": "sZlq6FFzNNzYmHK+vARWFpxtDY4XUdnU6q6zVIm4l1iQ3/ZXor4SeUnYDdd3lFZtoJ9yc8K2g4X7d/lVEgV9tA==",
"dependencies": { "dependencies": {
"Avalonia": "0.10.12", "Avalonia": "0.10.13",
"Avalonia.Xaml.Interactions": "0.10.12.2", "Avalonia.Xaml.Interactions": "0.10.13.2",
"Avalonia.Xaml.Interactivity": "0.10.12.2" "Avalonia.Xaml.Interactivity": "0.10.13.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"
} }
}, },
"DynamicData": { "DynamicData": {
@ -284,6 +265,23 @@
"Avalonia.Skia": "0.10.13" "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": { "Castle.Core": {
"type": "Transitive", "type": "Transitive",
"resolved": "4.2.0", "resolved": "4.2.0",
@ -1788,6 +1786,7 @@
"Avalonia": "0.10.13", "Avalonia": "0.10.13",
"Avalonia.ReactiveUI": "0.10.13", "Avalonia.ReactiveUI": "0.10.13",
"Avalonia.Svg.Skia": "0.10.12", "Avalonia.Svg.Skia": "0.10.12",
"Avalonia.Xaml.Behaviors": "0.10.13.2",
"DynamicData": "7.5.4", "DynamicData": "7.5.4",
"FluentAvaloniaUI": "1.3.0", "FluentAvaloniaUI": "1.3.0",
"Material.Icons.Avalonia": "1.0.2", "Material.Icons.Avalonia": "1.0.2",
@ -1804,6 +1803,7 @@
"Artemis.UI.Shared": "1.0.0", "Artemis.UI.Shared": "1.0.0",
"Avalonia": "0.10.13", "Avalonia": "0.10.13",
"Avalonia.ReactiveUI": "0.10.13", "Avalonia.ReactiveUI": "0.10.13",
"Avalonia.Xaml.Behaviors": "0.10.13.2",
"Ninject": "3.3.4", "Ninject": "3.3.4",
"NoStringEvaluating": "2.2.2", "NoStringEvaluating": "2.2.2",
"ReactiveUI": "17.1.50", "ReactiveUI": "17.1.50",

View File

@ -21,6 +21,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.13" /> <PackageReference Include="Avalonia" Version="0.10.13" />
<PackageReference Include="Avalonia.ReactiveUI" 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="Ninject" Version="3.3.4" />
<PackageReference Include="NoStringEvaluating" Version="2.2.2" /> <PackageReference Include="NoStringEvaluating" Version="2.2.2" />
<PackageReference Include="ReactiveUI" Version="17.1.50" /> <PackageReference Include="ReactiveUI" Version="17.1.50" />

View File

@ -12,18 +12,15 @@ public class NumericConverter : IValueConverter
/// <inheritdoc /> /// <inheritdoc />
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{ {
if (targetType == typeof(Numeric)) if (value is not Numeric numeric)
return new Numeric(value); return value;
return value; return Numeric.IsTypeCompatible(targetType) ? numeric.ToType(targetType, NumberFormatInfo.InvariantInfo) : value;
} }
/// <inheritdoc /> /// <inheritdoc />
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{ {
if (targetType == typeof(Numeric)) return new Numeric(value);
return new Numeric(value);
return value;
} }
} }

View File

@ -1,18 +1,47 @@
using Artemis.Core; using Artemis.Core;
using Artemis.UI.Shared.Services.NodeEditor;
using Artemis.UI.Shared.Services.NodeEditor.Commands;
using Artemis.UI.Shared.VisualScripting; using Artemis.UI.Shared.VisualScripting;
using ReactiveUI;
namespace Artemis.VisualScripting.Nodes.CustomViewModels; namespace Artemis.VisualScripting.Nodes.CustomViewModels;
public class StaticNumericValueNodeCustomViewModel : CustomNodeViewModel 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 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:customViewModels="clr-namespace:Artemis.VisualScripting.Nodes.CustomViewModels"
xmlns:converters="clr-namespace:Artemis.VisualScripting.Converters" xmlns:converters="clr-namespace:Artemis.VisualScripting.Converters"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" 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" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.VisualScripting.Nodes.CustomViews.StaticNumericValueNodeCustomView" x:Class="Artemis.VisualScripting.Nodes.CustomViews.StaticNumericValueNodeCustomView"
x:DataType="customViewModels:StaticNumericValueNodeCustomViewModel"> x:DataType="customViewModels:StaticNumericValueNodeCustomViewModel">
<UserControl.Resources> <UserControl.Resources>
<converters:NumericConverter x:Key="NumericConverter" /> <converters:NumericConverter x:Key="NumericConverter" />
</UserControl.Resources> </UserControl.Resources>
<controls:NumberBox VerticalAlignment="Center" <controls:NumberBox VerticalAlignment="Center" MinWidth="75" SimpleNumberFormat="F3" Classes="condensed">
MinWidth="75" <Interaction.Behaviors>
Value="{Binding Node.Storage, Converter={StaticResource NumericConverter}}" <behaviors:LostFocusNumberBoxBindingBehavior Value="{CompiledBinding CurrentValue, Converter={StaticResource NumericConverter}}"/>
SimpleNumberFormat="F3" </Interaction.Behaviors>
Classes="condensed"/> </controls:NumberBox>
</UserControl> </UserControl>

View File

@ -1,9 +1,11 @@
using Artemis.VisualScripting.Nodes.CustomViewModels;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.VisualScripting.Nodes.CustomViews namespace Artemis.VisualScripting.Nodes.CustomViews
{ {
public partial class StaticNumericValueNodeCustomView : UserControl public partial class StaticNumericValueNodeCustomView : ReactiveUserControl<StaticNumericValueNodeCustomViewModel>
{ {
public StaticNumericValueNodeCustomView() public StaticNumericValueNodeCustomView()
{ {

View File

@ -3,8 +3,13 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:customViewModels="clr-namespace:Artemis.VisualScripting.Nodes.CustomViewModels" 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" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.VisualScripting.Nodes.CustomViews.StaticStringValueNodeCustomView" x:Class="Artemis.VisualScripting.Nodes.CustomViews.StaticStringValueNodeCustomView"
x:DataType="customViewModels:StaticStringValueNodeCustomViewModel"> 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> </UserControl>

View File

@ -1,9 +1,11 @@
using Artemis.VisualScripting.Nodes.CustomViewModels;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.VisualScripting.Nodes.CustomViews namespace Artemis.VisualScripting.Nodes.CustomViews
{ {
public partial class StaticStringValueNodeCustomView : UserControl public partial class StaticStringValueNodeCustomView : ReactiveUserControl<StaticStringValueNodeCustomViewModel>
{ {
public StaticStringValueNodeCustomView() public StaticStringValueNodeCustomView()
{ {

View File

@ -1,10 +1,12 @@
using Artemis.VisualScripting.Nodes.Easing.CustomViewModels;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.VisualScripting.Nodes.Easing.CustomViews namespace Artemis.VisualScripting.Nodes.Easing.CustomViews
{ {
public partial class EasingTypeNodeCustomView : UserControl public partial class EasingTypeNodeCustomView : ReactiveUserControl<EasingTypeNodeCustomViewModel>
{ {
public EasingTypeNodeCustomView() public EasingTypeNodeCustomView()
{ {

View File

@ -28,6 +28,17 @@
"System.Reactive": "5.0.0" "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": { "Ninject": {
"type": "Direct", "type": "Direct",
"requested": "[3.3.4, )", "requested": "[3.3.4, )",
@ -175,6 +186,23 @@
"Avalonia.Skia": "0.10.13" "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": { "Castle.Core": {
"type": "Transitive", "type": "Transitive",
"resolved": "4.2.0", "resolved": "4.2.0",
@ -1707,6 +1735,7 @@
"Avalonia": "0.10.13", "Avalonia": "0.10.13",
"Avalonia.ReactiveUI": "0.10.13", "Avalonia.ReactiveUI": "0.10.13",
"Avalonia.Svg.Skia": "0.10.12", "Avalonia.Svg.Skia": "0.10.12",
"Avalonia.Xaml.Behaviors": "0.10.13.2",
"DynamicData": "7.5.4", "DynamicData": "7.5.4",
"FluentAvaloniaUI": "1.3.0", "FluentAvaloniaUI": "1.3.0",
"Material.Icons.Avalonia": "1.0.2", "Material.Icons.Avalonia": "1.0.2",