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:
parent
06ab2c5bb6
commit
2ae1f5f56c
@ -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>
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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()
|
||||
{
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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",
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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()
|
||||
{
|
||||
|
||||
@ -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>
|
||||
@ -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()
|
||||
{
|
||||
|
||||
@ -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()
|
||||
{
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user