Add project files.

This commit is contained in:
Ankitkumar Satapara
2026-04-17 22:31:58 +05:30
commit 21aaef6776
473 changed files with 50152 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Nodify.Calculator
{
public class APIOperationViewModel : OperationViewModel
{
public APIOperationViewModel()
{
_operationType = "GET";
}
private string _operationType;
public string OperationType
{
get => _operationType;
set => SetProperty(ref _operationType, value);
}
}
}

View File

@@ -0,0 +1,52 @@
<Window x:Class="Nodify.Calculator.AddVariableDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Add New Variable"
Width="350"
Height="280"
WindowStartupLocation="CenterOwner"
ResizeMode="NoResize"
Background="#2D2D30"
Foreground="White">
<Grid Margin="15">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="Variable Name:" Grid.Row="0" Margin="0 0 0 4" />
<TextBox x:Name="VariableNameBox" Grid.Row="1" Margin="0 0 0 10"
Background="#3E3E42" Foreground="White" Padding="4" />
<TextBlock Text="Variable Type:" Grid.Row="2" Margin="0 0 0 4" />
<ComboBox x:Name="VariableTypeBox" Grid.Row="3" Margin="0 0 0 10"
Background="#3E3E42" Foreground="White" SelectedIndex="0">
<ComboBoxItem Content="string" />
<ComboBoxItem Content="int" />
<ComboBoxItem Content="double" />
<ComboBoxItem Content="bool" />
<ComboBoxItem Content="float" />
<ComboBoxItem Content="decimal" />
<ComboBoxItem Content="long" />
<ComboBoxItem Content="DateTime" />
<ComboBoxItem Content="object" />
</ComboBox>
<TextBlock Text="Default Value (optional):" Grid.Row="4" Margin="0 0 0 4" />
<TextBox x:Name="DefaultValueBox" Grid.Row="5" Margin="0 0 0 10"
Background="#3E3E42" Foreground="White" Padding="4" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Grid.Row="7">
<Button Content="Add" Width="80" Margin="0 0 10 0" Padding="4"
Click="OnAddClick" IsDefault="True" />
<Button Content="Cancel" Width="80" Padding="4"
Click="OnCancelClick" IsCancel="True" />
</StackPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,36 @@
using System.Windows;
using System.Windows.Controls;
namespace Nodify.Calculator
{
public partial class AddVariableDialog : Window
{
public string VariableName { get; private set; } = string.Empty;
public string VariableType { get; private set; } = "string";
public string DefaultValue { get; private set; } = string.Empty;
public AddVariableDialog()
{
InitializeComponent();
}
private void OnAddClick(object sender, RoutedEventArgs e)
{
if (string.IsNullOrWhiteSpace(VariableNameBox.Text))
{
MessageBox.Show("Please enter a variable name.", "Validation", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
VariableName = VariableNameBox.Text.Trim();
VariableType = ((ComboBoxItem)VariableTypeBox.SelectedItem).Content.ToString()!;
DefaultValue = DefaultValueBox.Text.Trim();
DialogResult = true;
}
private void OnCancelClick(object sender, RoutedEventArgs e)
{
DialogResult = false;
}
}
}

View File

@@ -0,0 +1,44 @@
<Application x:Class="Nodify.Calculator.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:nodify="https://miroiu.github.io/nodify"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Nodify;component/Themes/Dark.xaml" />
<ResourceDictionary Source="pack://application:,,,/Nodify;component/Themes/FocusVisual.xaml" />
<ResourceDictionary Source="pack://application:,,,/Nodify.Shared;component/Themes/Icons.xaml" />
<ResourceDictionary Source="pack://application:,,,/Nodify.Shared;component/Themes/Dark.xaml" />
<ResourceDictionary>
<Style x:Key="{x:Static SystemParameters.FocusVisualStyleKey}">
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Rectangle StrokeThickness="1"
StrokeDashArray="2"
Margin="-2"
RadiusX="3"
RadiusY="3"
Stroke="DodgerBlue" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!--WPF is wonderful-->
<Style x:Key="OriginalNodeInputStyle"
TargetType="{x:Type nodify:NodeInput}"
BasedOn="{StaticResource {x:Type nodify:NodeInput}}" />
<Style x:Key="OriginalNodeOutputStyle"
TargetType="{x:Type nodify:NodeOutput}"
BasedOn="{StaticResource {x:Type nodify:NodeOutput}}" />
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
namespace Nodify.Calculator
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}
}

View File

@@ -0,0 +1,142 @@
using Newtonsoft.Json;
using Nodify.Calculator.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Input;
using System.Windows.Shapes;
namespace Nodify.Calculator
{
public class ApplicationViewModel : ObservableObject
{
public NodifyObservableCollection<EditorViewModel> Editors { get; } = new NodifyObservableCollection<EditorViewModel>();
public ApplicationViewModel()
{
AddEditorCommand = new DelegateCommand(() => Editors.Add(new EditorViewModel
{
Name = $"Editor {Editors.Count + 1}"
}));
CloseEditorCommand = new DelegateCommand<Guid>(
id => Editors.RemoveOne(editor => editor.Id == id),
_ => Editors.Count > 0 && SelectedEditor != null);
RunFlowCommand = new DelegateCommand(() =>
{
Executor ex = new Executor(Editors.First());
var result = ex.PerformPreCheck();
if (!string.IsNullOrEmpty(result))
{
MessageBox.Show("Error occured while executing the request : " + result);
}
else
{
FlowRunner runner = new FlowRunner(ex);
runner.ShowDialog();
}
});
SaveFileCommand = new DelegateCommand(() =>
{
var firstEditor = Editors.First();
var allNodes = firstEditor.Calculator.Operations;
SaveGraphModel svm = new SaveGraphModel();
foreach (var item in allNodes)
{
SaveNodes svn = new SaveNodes()
{
Location = item.Location,
};
svm.Nodes.Add(svn);
}
var jsonEditors = JsonConvert.SerializeObject(svm, new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
});
//var jsonEditors = JsonConvert.SerializeObject(Editors);
File.WriteAllText("SaveFile.AEXN", jsonEditors);
});
OpenFileCommand = new DelegateCommand(() =>
{
if (File.Exists("SaveFile.AEXN"))
{
var allText = File.ReadAllText("SaveFile.AEXN");
var edts = JsonConvert.DeserializeObject<SaveGraphModel>(allText);
var firstEditor = Editors.First();
var nodesToAdd = edts.Nodes;
foreach (var item in nodesToAdd)
{
firstEditor.Calculator.Operations.Add(new()
{
Location = item.Location,
NodeId = "H",
Title = "HHA"
});
}
}
});
Editors.WhenAdded((editor) =>
{
if (AutoSelectNewEditor || Editors.Count == 1)
{
SelectedEditor = editor;
}
editor.OnOpenInnerCalculator += OnOpenInnerCalculator;
})
.WhenRemoved((editor) =>
{
editor.OnOpenInnerCalculator -= OnOpenInnerCalculator;
var childEditors = Editors.Where(ed => ed.Parent == editor).ToList();
childEditors.ForEach(ed => Editors.Remove(ed));
});
Editors.Add(new EditorViewModel
{
Name = $"Editor {Editors.Count + 1}"
});
}
private void OnOpenInnerCalculator(EditorViewModel parentEditor, CalculatorViewModel calculator)
{
var editor = Editors.FirstOrDefault(e => e.Calculator == calculator);
if (editor != null)
{
SelectedEditor = editor;
}
else
{
var childEditor = new EditorViewModel
{
Parent = parentEditor,
Calculator = calculator,
Name = $"[Inner] Editor {Editors.Count + 1}"
};
Editors.Add(childEditor);
}
}
public ICommand AddEditorCommand { get; }
public ICommand CloseEditorCommand { get; }
public ICommand RunFlowCommand { get; }
public ICommand SaveFileCommand { get; }
public ICommand OpenFileCommand { get; }
private EditorViewModel? _selectedEditor;
public EditorViewModel? SelectedEditor
{
get => _selectedEditor;
set => SetProperty(ref _selectedEditor, value);
}
private bool _autoSelectNewEditor = true;
public bool AutoSelectNewEditor
{
get => _autoSelectNewEditor;
set => SetProperty(ref _autoSelectNewEditor, value);
}
}
}

View File

@@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]

View File

@@ -0,0 +1,25 @@
namespace Nodify.Calculator
{
public class AuthOperationViewModel : SystemOperationViewModel
{
public AuthOperationViewModel()
{
Title = "Auth";
SystemOperationType = SystemOperations.AUTH;
}
private string _baseUrl = string.Empty;
public string BaseUrl
{
get => _baseUrl;
set => SetProperty(ref _baseUrl, value);
}
private string _authType = "Bearer Token";
public string AuthType
{
get => _authType;
set => SetProperty(ref _authType, value);
}
}
}

View File

@@ -0,0 +1,178 @@
using System;
using System.IO;
using System.Linq;
using System.Reflection;
public static class BinarySerializationHelper
{
public static void SerializeObject<T>(T obj, BinaryWriter writer)
{
if (obj == null)
{
writer.Write(false); // Null flag
return;
}
writer.Write(true); // Not null
Type type = typeof(T);
// Serialize all non-indexed properties
foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
if (property.CanRead && property.GetIndexParameters().Length == 0) // Skip indexers
{
var value = property.GetValue(obj);
SerializeValue(value, writer);
}
}
// Serialize all public fields
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
{
var value = field.GetValue(obj);
SerializeValue(value, writer);
}
}
public static T DeserializeObject<T>(BinaryReader reader) where T : new()
{
if (!reader.ReadBoolean()) // Null flag
{
return default;
}
T obj = new T();
Type type = typeof(T);
foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
if (property.CanWrite && property.GetIndexParameters().Length == 0) // Skip indexers
{
var value = DeserializeValue(property.PropertyType, reader);
property.SetValue(obj, value);
}
}
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
{
var value = DeserializeValue(field.FieldType, reader);
field.SetValue(obj, value);
}
return obj;
}
private static void SerializeValue(object value, BinaryWriter writer)
{
if (value == null)
{
writer.Write(false); // Null flag for the value
return;
}
writer.Write(true); // Not null!
Type type = value.GetType();
if (type == typeof(int))
{
writer.Write((int)value);
}
else if (type == typeof(string))
{
writer.Write((string)value);
}
else if (type == typeof(bool))
{
writer.Write((bool)value);
}
else if (type == typeof(double))
{
writer.Write((double)value);
}
else if (type == typeof(float))
{
writer.Write((float)value);
}
else if (type == typeof(long))
{
writer.Write((long)value);
}
else if (type.IsArray)
{
var array = (Array)value;
writer.Write(array.Length); // Write array length
foreach (var item in array)
{
SerializeValue(item, writer);
}
}
else if (typeof(System.Collections.IList).IsAssignableFrom(type))
{
var list = (System.Collections.IList)value;
writer.Write(list.Count); // Write list count
foreach (var item in list)
{
SerializeValue(item, writer); // Serialize each item
}
}
else
{
SerializeObject(value, writer); // Serialize nested objects
}
}
private static object DeserializeValue(Type type, BinaryReader reader)
{
if (!reader.ReadBoolean()) // Null flag
{
return null;
}
if (type == typeof(int))
{
return reader.ReadInt32();
}
else if (type == typeof(string))
{
return reader.ReadString();
}
else if (type == typeof(bool))
{
return reader.ReadBoolean();
}
else if (type == typeof(double))
{
return reader.ReadDouble();
}
else if (type.IsArray)
{
int length = reader.ReadInt32();
var array = Array.CreateInstance(type.GetElementType(), length);
for (int i = 0; i < length; i++)
{
array.SetValue(DeserializeValue(type.GetElementType(), reader), i);
}
return array;
}
else if (typeof(System.Collections.IList).IsAssignableFrom(type))
{
int count = reader.ReadInt32();
var list = (System.Collections.IList)Activator.CreateInstance(type);
for (int i = 0; i < count; i++)
{
list.Add(DeserializeValue(type.GenericTypeArguments[0], reader));
}
return list;
}
else
{
var method = typeof(BinarySerializationHelper)
.GetMethod("DeserializeObject")
.MakeGenericMethod(type);
return method.Invoke(null, new object[] { reader });
}
}
}

View File

@@ -0,0 +1,30 @@
namespace Nodify.Calculator
{
public class CalculatorInputOperationViewModel : OperationViewModel
{
public CalculatorInputOperationViewModel()
{
AddOutputCommand = new RequeryCommand(
() => Output.Add(new ConnectorViewModel
{
Title = $"In {Output.Count}"
}),
() => Output.Count < 10);
RemoveOutputCommand = new RequeryCommand(
() => Output.RemoveAt(Output.Count - 1),
() => Output.Count > 1);
Output.Add(new ConnectorViewModel
{
Title = $"In {Output.Count}"
});
}
public new NodifyObservableCollection<ConnectorViewModel> Output { get; set; } =
new NodifyObservableCollection<ConnectorViewModel>();
public INodifyCommand AddOutputCommand { get; }
public INodifyCommand RemoveOutputCommand { get; }
}
}

View File

@@ -0,0 +1,51 @@
using System.Windows;
namespace Nodify.Calculator
{
public class CalculatorOperationViewModel : OperationViewModel
{
public CalculatorViewModel InnerCalculator { get; } = new CalculatorViewModel();
private OperationViewModel InnerOutput { get; } = new OperationViewModel
{
Title = "Output Parameters",
Input = { new ConnectorViewModel() },
Location = new Point(500, 300),
IsReadOnly = true
};
private CalculatorInputOperationViewModel InnerInput { get; } = new CalculatorInputOperationViewModel
{
Title = "Input Parameters",
Location = new Point(300, 300),
IsReadOnly = true
};
public CalculatorOperationViewModel()
{
InnerCalculator.Operations.Add(InnerInput);
InnerCalculator.Operations.Add(InnerOutput);
InnerInput.Output.ForEach(x => Input.Add(new ConnectorViewModel
{
Title = x.Title
}));
InnerInput.Output
.WhenAdded(x => Input.Add(new ConnectorViewModel
{
Title = x.Title
}))
.WhenRemoved(x => Input.RemoveOne(i => i.Title == x.Title));
}
protected override void OnInputValueChanged()
{
for (var i = 0; i < Input.Count; i++)
{
InnerInput.Output[i].Value = Input[i].Value;
}
}
}
}

View File

@@ -0,0 +1,184 @@
using System.Diagnostics;
using System.Linq;
using System.Windows;
namespace Nodify.Calculator
{
public class CalculatorViewModel : ObservableObject
{
public CalculatorViewModel()
{
CreateConnectionCommand = new DelegateCommand<ConnectorViewModel>(
_ => CreateConnection(PendingConnection.Source, PendingConnection.Target),
_ => CanCreateConnection(PendingConnection.Source, PendingConnection.Target));
StartConnectionCommand = new DelegateCommand<ConnectorViewModel>(_ => PendingConnection.IsVisible = true, (c) => c != null && !(c.IsConnected && c.IsInput));
DisconnectConnectorCommand = new DelegateCommand<ConnectorViewModel>(DisconnectConnector);
DeleteSelectionCommand = new DelegateCommand(DeleteSelection);
GroupSelectionCommand = new DelegateCommand(GroupSelectedOperations, () => SelectedOperations.Count > 0);
Connections.WhenAdded(c =>
{
c.Input.IsConnected = true;
c.Output.IsConnected = true;
c.Input.Value = c.Output.Value;
c.Output.ValueObservers.Add(c.Input);
})
.WhenRemoved(c =>
{
var ic = Connections.Count(con => con.Input == c.Input || con.Output == c.Input);
var oc = Connections.Count(con => con.Input == c.Output || con.Output == c.Output);
if (ic == 0)
{
c.Input.IsConnected = false;
}
if (oc == 0)
{
c.Output.IsConnected = false;
}
c.Output.ValueObservers.Remove(c.Input);
});
Operations.WhenAdded(x =>
{
x.Input.WhenRemoved(RemoveConnection);
x.NodeId = (Operations.Count + 1).ToString();
Debug.WriteLine($"Currently adding the node with node id : {x.NodeId} , Title : {x.Title}");
if (x is CalculatorInputOperationViewModel ci)
{
ci.Output.WhenRemoved(RemoveConnection);
}
void RemoveConnection(ConnectorViewModel i)
{
var c = Connections.Where(con => con.Input == i || con.Output == i).ToArray();
c.ForEach(con => Connections.Remove(con));
}
})
.WhenRemoved(x =>
{
foreach (var input in x.Input)
{
DisconnectConnector(input);
}
foreach (var item in x.Output)
{
DisconnectConnector(item);
}
});
OperationsMenu = new OperationsMenuViewModel(this);
}
private NodifyObservableCollection<OperationViewModel> _operations = new NodifyObservableCollection<OperationViewModel>();
public NodifyObservableCollection<OperationViewModel> Operations
{
get => _operations;
set => SetProperty(ref _operations, value);
}
private NodifyObservableCollection<OperationViewModel> _selectedOperations = new NodifyObservableCollection<OperationViewModel>();
public NodifyObservableCollection<OperationViewModel> SelectedOperations
{
get => _selectedOperations;
set => SetProperty(ref _selectedOperations, value);
}
public NodifyObservableCollection<ConnectionViewModel> Connections { get; } = new NodifyObservableCollection<ConnectionViewModel>();
public PendingConnectionViewModel PendingConnection { get; set; } = new PendingConnectionViewModel();
public OperationsMenuViewModel OperationsMenu { get; set; }
public INodifyCommand StartConnectionCommand { get; }
public INodifyCommand CreateConnectionCommand { get; }
public INodifyCommand DisconnectConnectorCommand { get; }
public INodifyCommand DeleteSelectionCommand { get; }
public INodifyCommand GroupSelectionCommand { get; }
private void DisconnectConnector(ConnectorViewModel connector)
{
var connections = Connections.Where(c => c.Input == connector || c.Output == connector).ToList();
connections.ForEach(c => Connections.Remove(c));
}
internal bool CanCreateConnection(ConnectorViewModel source, ConnectorViewModel? target)
=> target == null || (source != target &&
source.Shape == target.Shape &&
!source.IsConnected &&
!target.IsConnected &&
source.IsInput != target.IsInput);
internal void OpenGetSetVariable(Point TargetLocation, string className)
{
OperationsMenu.OpenOnlyGetSetVariable(TargetLocation, className);
OperationsMenu.Closed += OnOperationsMenuClosed;
}
internal void OpenGetSetForVariable(Point targetLocation, OperationInfoViewModel variableInfo)
{
OperationsMenu.OpenGetSetForVariable(targetLocation, variableInfo);
OperationsMenu.Closed += OnOperationsMenuClosed;
}
internal void CreateConnection(ConnectorViewModel source, ConnectorViewModel? target)
{
if (target == null)
{
PendingConnection.IsVisible = true;
OperationsMenu.OpenAt(PendingConnection.TargetLocation);
OperationsMenu.Closed += OnOperationsMenuClosed;
return;
}
var input = source.IsInput ? source : target;
var output = target.IsInput ? source : target;
PendingConnection.IsVisible = false;
DisconnectConnector(input);
Connections.Add(new ConnectionViewModel
{
Input = input,
Output = output,
InputNodeId = source.Operation.NodeId,
OutputNodeId = target.Operation.NodeId
});
Debug.WriteLine($"Creating a connection between input node id: {source.Operation.NodeId} and :{target.Operation.NodeId}");
}
private void OnOperationsMenuClosed()
{
PendingConnection.IsVisible = false;
OperationsMenu.Closed -= OnOperationsMenuClosed;
}
private void DeleteSelection()
{
var selected = SelectedOperations.ToList();
selected.ForEach(o => Operations.Remove(o));
}
private void GroupSelectedOperations()
{
var selected = SelectedOperations.ToList();
var bounding = selected.GetBoundingBox(50);
Operations.Add(new OperationGroupViewModel
{
Title = "Operations",
Location = bounding.Location,
GroupSize = new Size(bounding.Width, bounding.Height)
});
}
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
namespace Nodify.Calculator
{
internal static class ColorToSolidBrushConverter
{
public static Brush Convert(Color value)
{
var brush = new SolidColorBrush(value);
return brush;
}
public static Color ConvertBack(Brush value)
{
if (value is SolidColorBrush brush)
return brush.Color;
return default;
}
}
}

View File

@@ -0,0 +1,36 @@
namespace Nodify.Calculator
{
public class ConnectionViewModel : ObservableObject
{
private ConnectorViewModel _input = default!;
public ConnectorViewModel Input
{
get => _input;
set => SetProperty(ref _input, value);
}
private ConnectorViewModel _output = default!;
public ConnectorViewModel Output
{
get => _output;
set => SetProperty(ref _output, value);
}
private string _inputNodeId;
public string InputNodeId
{
get { return _inputNodeId; }
set { _inputNodeId = value; }
}
private string _outputNodeId;
public string OutputNodeId
{
get { return _outputNodeId; }
set { _outputNodeId = value; }
}
}
}

View File

@@ -0,0 +1,100 @@
using LiteDB;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using System.Windows;
using System.Windows.Media;
namespace Nodify.Calculator
{
public enum ConnectorShape
{
Circle,
Triangle,
Square,
}
public class ConnectorViewModel : ObservableObject
{
private string? _title;
public string? Title
{
get => _title;
set => SetProperty(ref _title, value);
}
private double _value;
public double Value
{
get => _value;
set => SetProperty(ref _value, value)
.Then(() => ValueObservers.ForEach(o => o.Value = value));
}
private ConnectorShape _shape;
public ConnectorShape Shape
{
get => _shape;
set => SetProperty(ref _shape, value);
}
private bool _isConnected;
public bool IsConnected
{
get => _isConnected;
set => SetProperty(ref _isConnected, value);
}
private bool _isInput;
public bool IsInput
{
get => _isInput;
set => SetProperty(ref _isInput, value);
}
private Point _anchor;
public Point Anchor
{
get => _anchor;
set => SetProperty(ref _anchor, value);
}
private System.Drawing.Color _color = System.Drawing.Color.DodgerBlue;
public System.Drawing.Color ConnectorColor
{
set
{
_color = value;
}
}
public Brush Color
{
get
{
var mediacolor = System.Windows.Media.Color.FromArgb
(
_color.A,
_color.R,
_color.G,
_color.B
);
return ColorToSolidBrushConverter.Convert(mediacolor);
}
}
[Newtonsoft.Json.JsonIgnore]
[BsonIgnore]
private OperationViewModel _operation = default!;
[Newtonsoft.Json.JsonIgnore]
[BsonIgnore]
public OperationViewModel Operation
{
get => _operation;
set => SetProperty(ref _operation, value);
}
[Newtonsoft.Json.JsonIgnore]
[BsonIgnore]
public List<ConnectorViewModel> ValueObservers { get; } = new List<ConnectorViewModel>();
}
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Windows.Data;
namespace Nodify.Calculator
{
public class ItemToListConverter : IValueConverter
{
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value != null)
{
var argType = value.GetType();
var listType = typeof(List<>).MakeGenericType(argType);
var list = Activator.CreateInstance(listType) as IList;
list?.Add(value);
return list;
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
}

View File

@@ -0,0 +1,16 @@
using System.Windows;
namespace Nodify.Calculator
{
public class CreateOperationInfoViewModel
{
public CreateOperationInfoViewModel(OperationInfoViewModel info, Point location)
{
Info = info;
Location = location;
}
public OperationInfoViewModel Info { get; }
public Point Location { get; }
}
}

View File

@@ -0,0 +1,699 @@
<UserControl x:Class="Nodify.Calculator.EditorView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Nodify.Calculator"
xmlns:nodify="https://miroiu.github.io/nodify"
xmlns:shared="clr-namespace:Nodify;assembly=Nodify.Shared"
xmlns:sys="clr-namespace:System;assembly=System.Runtime"
d:DataContext="{d:DesignInstance Type=local:EditorViewModel}"
mc:Ignorable="d"
d:DesignHeight="450"
d:DesignWidth="800">
<UserControl.Resources>
<GeometryDrawing x:Key="SmallGridGeometry"
Geometry="M0,0 L0,1 0.03,1 0.03,0.03 1,0.03 1,0 Z"
Brush="{DynamicResource GridLinesBrush}" />
<GeometryDrawing x:Key="LargeGridGeometry"
Geometry="M0,0 L0,1 0.015,1 0.015,0.015 1,0.015 1,0 Z"
Brush="{DynamicResource GridLinesBrush}" />
<DrawingBrush x:Key="SmallGridLinesDrawingBrush"
TileMode="Tile"
ViewportUnits="Absolute"
Viewport="0 0 15 15"
Transform="{Binding ViewportTransform, ElementName=Editor}"
Drawing="{StaticResource SmallGridGeometry}" />
<DrawingBrush x:Key="LargeGridLinesDrawingBrush"
TileMode="Tile"
ViewportUnits="Absolute"
Opacity="0.5"
Viewport="0 0 150 150"
Transform="{Binding ViewportTransform, ElementName=Editor}"
Drawing="{StaticResource LargeGridGeometry}" />
<LinearGradientBrush x:Key="AnimatedBrush"
StartPoint="0 0"
EndPoint="1 0">
<GradientStop Color="#6366f1"
Offset="0" />
<GradientStop Color="#a855f7"
Offset="0.5" />
<GradientStop Color="#ec4899"
Offset="1" />
</LinearGradientBrush>
<Border x:Key="AnimatedBorderPlaceholder"
BorderBrush="{StaticResource AnimatedBrush}" />
<Storyboard x:Key="AnimateBorder"
RepeatBehavior="Forever">
<PointAnimation Storyboard.TargetProperty="BorderBrush.(LinearGradientBrush.StartPoint)"
Storyboard.Target="{StaticResource AnimatedBorderPlaceholder}"
Duration="0:0:2"
To="1 0" />
<PointAnimation Storyboard.TargetProperty="BorderBrush.(LinearGradientBrush.StartPoint)"
Storyboard.Target="{StaticResource AnimatedBorderPlaceholder}"
Duration="0:0:2"
To="1 1"
BeginTime="0:0:2" />
<PointAnimation Storyboard.TargetProperty="BorderBrush.(LinearGradientBrush.StartPoint)"
Storyboard.Target="{StaticResource AnimatedBorderPlaceholder}"
Duration="0:0:2"
To="0 1"
BeginTime="0:0:4" />
<PointAnimation Storyboard.TargetProperty="BorderBrush.(LinearGradientBrush.StartPoint)"
Storyboard.Target="{StaticResource AnimatedBorderPlaceholder}"
Duration="0:0:2"
To="0 0"
BeginTime="0:0:6" />
<PointAnimation Storyboard.TargetProperty="BorderBrush.(LinearGradientBrush.EndPoint)"
Storyboard.Target="{StaticResource AnimatedBorderPlaceholder}"
Duration="0:0:2"
To="1 1" />
<PointAnimation Storyboard.TargetProperty="BorderBrush.(LinearGradientBrush.EndPoint)"
Storyboard.Target="{StaticResource AnimatedBorderPlaceholder}"
Duration="0:0:2"
To="0 1"
BeginTime="0:0:2" />
<PointAnimation Storyboard.TargetProperty="BorderBrush.(LinearGradientBrush.EndPoint)"
Storyboard.Target="{StaticResource AnimatedBorderPlaceholder}"
Duration="0:0:2"
To="0 0"
BeginTime="0:0:4" />
<PointAnimation Storyboard.TargetProperty="BorderBrush.(LinearGradientBrush.EndPoint)"
Storyboard.Target="{StaticResource AnimatedBorderPlaceholder}"
Duration="0:0:2"
To="1 0"
BeginTime="0:0:6" />
</Storyboard>
<local:ItemToListConverter x:Key="ItemToListConverter" />
<DataTemplate x:Key="ConnectionTemplate"
DataType="{x:Type local:ConnectionViewModel}">
<nodify:CircuitConnection Source="{Binding Output.Anchor}"
Target="{Binding Input.Anchor}"
Foreground="{Binding Input.Color}"
Stroke="{Binding Input.Color}"
StrokeThickness="2"
/>
</DataTemplate>
<DataTemplate x:Key="PendingConnectionTemplate"
DataType="{x:Type local:PendingConnectionViewModel}">
<nodify:PendingConnection IsVisible="{Binding IsVisible}"
Source="{Binding Source, Mode=OneWayToSource}"
Target="{Binding Target, Mode=OneWayToSource}"
TargetAnchor="{Binding TargetLocation, Mode=OneWayToSource}"
StartedCommand="{Binding DataContext.StartConnectionCommand, RelativeSource={RelativeSource AncestorType={x:Type nodify:NodifyEditor}}}"
CompletedCommand="{Binding DataContext.CreateConnectionCommand, RelativeSource={RelativeSource AncestorType={x:Type nodify:NodifyEditor}}}" />
</DataTemplate>
<Style x:Key="ItemContainerStyle"
TargetType="{x:Type nodify:ItemContainer}"
BasedOn="{StaticResource {x:Type nodify:ItemContainer}}">
<Setter Property="Location"
Value="{Binding Location}" />
<Setter Property="IsSelected"
Value="{Binding IsSelected}" />
<Setter Property="ActualSize"
Value="{Binding Size, Mode=OneWayToSource}" />
<Setter Property="BorderBrush"
Value="{Binding BorderBrush, Source={StaticResource AnimatedBorderPlaceholder}}" />
<Setter Property="BorderThickness"
Value="2" />
</Style>
<SolidColorBrush x:Key="SquareConnectorColor" Color="MediumSlateBlue"></SolidColorBrush>
<SolidColorBrush x:Key="TriangleConnectorColor" Color="White"></SolidColorBrush>
<Style x:Key="ConnectionStyle" TargetType="{x:Type nodify:BaseConnection}">
<Style.Triggers>
<DataTrigger Binding="{Binding Input.Shape}"
Value="{x:Static local:ConnectorShape.Square}">
<Setter Property="Stroke" Value="{StaticResource SquareConnectorColor}"></Setter>
</DataTrigger>
<DataTrigger Binding="{Binding Input.Shape}"
Value="{x:Static local:ConnectorShape.Triangle}">
<Setter Property="Stroke" Value="{StaticResource TriangleConnectorColor}"></Setter>
</DataTrigger>
</Style.Triggers>
<Setter Property="Stroke" Value="{DynamicResource Connection.StrokeBrush}"></Setter>
<Setter Property="Cursor" Value="Hand"></Setter>
<Setter Property="ToolTip" Value="Double click to split"></Setter>
</Style>
<ControlTemplate x:Key="SquareConnector" TargetType="Control">
<Rectangle Width="14"
Height="14"
StrokeDashCap="Round"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Stroke="{TemplateBinding BorderBrush}"
Fill="{TemplateBinding Background}"
StrokeThickness="2" />
</ControlTemplate>
<ControlTemplate x:Key="TriangleConnector" TargetType="Control">
<Polygon Width="14"
Height="14"
Points="2,2 4,2 12,7 4,12 2,12"
StrokeDashCap="Round"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Stroke="{TemplateBinding BorderBrush}"
Fill="{TemplateBinding Background}"
/>
</ControlTemplate>
</UserControl.Resources>
<Grid>
<nodify:NodifyEditor DataContext="{Binding Calculator}"
ItemsSource="{Binding Operations}"
Connections="{Binding Connections}"
SelectedItems="{Binding SelectedOperations}"
DisconnectConnectorCommand="{Binding DisconnectConnectorCommand}"
PendingConnection="{Binding PendingConnection}"
PendingConnectionTemplate="{StaticResource PendingConnectionTemplate}"
ConnectionTemplate="{StaticResource ConnectionTemplate}"
Background="{StaticResource SmallGridLinesDrawingBrush}"
ItemContainerStyle="{StaticResource ItemContainerStyle}"
HasCustomContextMenu="True"
GridCellSize="15"
AllowDrop="True"
Drop="OnDropNode"
x:Name="Editor">
<nodify:NodifyEditor.Resources>
<Style TargetType="{x:Type nodify:NodeInput}"
BasedOn="{StaticResource OriginalNodeInputStyle}">
<Setter Property="Header"
Value="{Binding}" />
<Setter Property="IsConnected"
Value="{Binding IsConnected}" />
<Setter Property="Anchor"
Value="{Binding Anchor, Mode=OneWayToSource}" />
<Setter Property="ToolTip"
Value="{Binding Value}" />
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate DataType="{x:Type local:ConnectorViewModel}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Title}"
Margin="0 0 5 0" />
<TextBox Text="{Binding Value}"
Visibility="{Binding IsConnected, Converter={shared:BooleanToVisibilityConverter Negate=True}}" />
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding Title}" Value="">
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate/>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding Title}" Value="{x:Null}">
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate/>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding Shape}"
Value="{x:Static local:ConnectorShape.Square}">
<Setter Property="ConnectorTemplate" Value="{StaticResource SquareConnector}" />
<Setter Property="BorderBrush" Value="{StaticResource SquareConnectorColor}"></Setter>
</DataTrigger>
<DataTrigger Binding="{Binding Shape}"
Value="{x:Static local:ConnectorShape.Triangle}">
<Setter Property="ConnectorTemplate" Value="{StaticResource TriangleConnector}" />
<Setter Property="BorderBrush" Value="{StaticResource TriangleConnectorColor}"></Setter>
</DataTrigger>
<DataTrigger Binding="{Binding Shape}"
Value="{x:Static local:ConnectorShape.Circle}">
<Setter Property="BorderBrush" Value="{Binding Color}"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
<Style TargetType="{x:Type nodify:NodeOutput}"
BasedOn="{StaticResource OriginalNodeOutputStyle}">
<Setter Property="Header"
Value="{Binding}" />
<Setter Property="IsConnected"
Value="{Binding IsConnected}" />
<Setter Property="Anchor"
Value="{Binding Anchor, Mode=OneWayToSource}" />
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate DataType="{x:Type local:ConnectorViewModel}">
<TextBox Text="{Binding Title}"
IsEnabled="False" />
</DataTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding Title}" Value="">
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate/>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding Title}" Value="{x:Null}">
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate/>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding Shape}"
Value="{x:Static local:ConnectorShape.Square}">
<Setter Property="ConnectorTemplate" Value="{StaticResource SquareConnector}" />
<Setter Property="BorderBrush" Value="{StaticResource SquareConnectorColor}"></Setter>
</DataTrigger>
<DataTrigger Binding="{Binding Shape}"
Value="{x:Static local:ConnectorShape.Triangle}">
<Setter Property="ConnectorTemplate" Value="{StaticResource TriangleConnector}" />
<Setter Property="BorderBrush" Value="{StaticResource TriangleConnectorColor}"></Setter>
</DataTrigger>
<DataTrigger Binding="{Binding Shape}"
Value="{x:Static local:ConnectorShape.Circle}">
<Setter Property="BorderBrush" Value="{Binding Color}"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
<DataTemplate DataType="{x:Type local:OperationGraphViewModel}">
<nodify:GroupingNode Header="{Binding}"
CanResize="{Binding IsExpanded}"
ActualSize="{Binding DesiredSize, Mode=TwoWay}"
MovementMode="Self">
<nodify:GroupingNode.HeaderTemplate>
<DataTemplate DataType="{x:Type local:OperationGraphViewModel}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Title}" />
<StackPanel Orientation="Horizontal"
Margin="5 0 0 0"
Grid.Column="1">
<TextBlock Text="Expand?"
Visibility="{Binding IsExpanded, Converter={shared:BooleanToVisibilityConverter}}"
Margin="0 0 5 0" />
<CheckBox IsChecked="{Binding IsExpanded}" />
</StackPanel>
</Grid>
</DataTemplate>
</nodify:GroupingNode.HeaderTemplate>
<Grid>
<ScrollViewer CanContentScroll="True"
Visibility="{Binding IsExpanded, Converter={shared:BooleanToVisibilityConverter}}">
<nodify:NodifyEditor Tag="{Binding DataContext, RelativeSource={RelativeSource Self}}"
DataContext="{Binding InnerCalculator}"
ItemsSource="{Binding Operations}"
Connections="{Binding Connections}"
SelectedItems="{Binding SelectedOperations}"
DisconnectConnectorCommand="{Binding DisconnectConnectorCommand}"
PendingConnection="{Binding PendingConnection}"
PendingConnectionTemplate="{StaticResource PendingConnectionTemplate}"
ConnectionTemplate="{StaticResource ConnectionTemplate}"
ItemContainerStyle="{StaticResource ItemContainerStyle}"
HasCustomContextMenu="True"
Background="Transparent"
GridCellSize="15"
AllowDrop="True"
Drop="OnDropNode"
Visibility="{Binding DataContext.IsExpanded, RelativeSource={RelativeSource AncestorType=nodify:GroupingNode}, Converter={shared:BooleanToVisibilityConverter}}">
<nodify:NodifyEditor.InputBindings>
<KeyBinding Key="Delete"
Command="{Binding DeleteSelectionCommand}" />
<KeyBinding Key="G"
Modifiers="Ctrl"
Command="{Binding GroupSelectionCommand}" />
</nodify:NodifyEditor.InputBindings>
<nodify:NodifyEditor.CommandBindings>
<CommandBinding Command="{x:Static ApplicationCommands.ContextMenu}"
Executed="OpenContextMenu_Executed" />
</nodify:NodifyEditor.CommandBindings>
<CompositeCollection>
<nodify:DecoratorContainer DataContext="{Binding OperationsMenu}"
Location="{Binding Location}">
<local:OperationsMenuView />
</nodify:DecoratorContainer>
</CompositeCollection>
</nodify:NodifyEditor>
</ScrollViewer>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ItemsControl ItemsSource="{Binding Input}"
Focusable="False">
<ItemsControl.ItemTemplate>
<DataTemplate>
<nodify:NodeInput />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<nodify:NodeOutput DataContext="{Binding Output}"
Grid.Column="1"
VerticalAlignment="Top"
HorizontalAlignment="Right" />
</Grid>
</Grid>
</nodify:GroupingNode>
</DataTemplate>
<DataTemplate DataType="{x:Type local:ExpandoOperationViewModel}">
<nodify:Node Header="{Binding Title}"
Content="{Binding}"
Input="{Binding Input}"
Output="{Binding Output}">
<nodify:Node.ContentTemplate>
<DataTemplate DataType="{x:Type local:ExpandoOperationViewModel}">
<StackPanel>
<Button Style="{StaticResource IconButton}"
Content="{StaticResource PlusIcon}"
FocusVisualStyle="{StaticResource {x:Static SystemParameters.FocusVisualStyleKey}}"
Command="{Binding AddInputCommand}" />
<Button Style="{StaticResource IconButton}"
Content="{StaticResource RemoveKeyIcon}"
FocusVisualStyle="{StaticResource {x:Static SystemParameters.FocusVisualStyleKey}}"
Command="{Binding RemoveInputCommand}" />
</StackPanel>
</DataTemplate>
</nodify:Node.ContentTemplate>
</nodify:Node>
</DataTemplate>
<DataTemplate DataType="{x:Type local:APIOperationViewModel}">
<nodify:Node Header="{Binding Title}"
Content="{Binding}"
Input="{Binding Input}"
Output="{Binding Output}">
<nodify:Node.ContentTemplate>
<DataTemplate DataType="{x:Type local:APIOperationViewModel}">
<StackPanel>
<TextBlock Text="{Binding OperationType}"></TextBlock>
</StackPanel>
</DataTemplate>
</nodify:Node.ContentTemplate>
</nodify:Node>
</DataTemplate>
<DataTemplate DataType="{x:Type local:ExpressionOperationViewModel}">
<nodify:Node Content="{Binding}"
Input="{Binding Input}"
Output="{Binding Output}">
<nodify:Node.ContentTemplate>
<DataTemplate DataType="{x:Type local:ExpressionOperationViewModel}">
<TextBox Text="{Binding Expression}"
MinWidth="100"
Margin="5 0 0 0" />
</DataTemplate>
</nodify:Node.ContentTemplate>
</nodify:Node>
</DataTemplate>
<DataTemplate DataType="{x:Type local:CalculatorOperationViewModel}">
<nodify:Node Header="{Binding Title}"
Input="{Binding Input}"
Output="{Binding Output}"
ToolTip="Double click to expand">
<nodify:Node.InputBindings>
<MouseBinding Gesture="LeftDoubleClick"
Command="{Binding DataContext.OpenCalculatorCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding InnerCalculator}" />
</nodify:Node.InputBindings>
</nodify:Node>
</DataTemplate>
<DataTemplate DataType="{x:Type local:CalculatorInputOperationViewModel}">
<DataTemplate.Resources>
<Style TargetType="{x:Type nodify:NodeOutput}"
BasedOn="{StaticResource {x:Type nodify:NodeOutput}}">
<Setter Property="Header"
Value="{Binding}" />
<Setter Property="IsConnected"
Value="{Binding IsConnected}" />
<Setter Property="Anchor"
Value="{Binding Anchor, Mode=OneWayToSource}" />
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate DataType="{x:Type local:ConnectorViewModel}">
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding Value}"
IsEnabled="False" />
<TextBlock Text="{Binding Title}"
Margin="5 0 0 0" />
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</DataTemplate.Resources>
<nodify:Node Header="{Binding Title}"
Output="{Binding Output}">
<StackPanel>
<Button Style="{StaticResource IconButton}"
Content="{StaticResource PlusIcon}"
Command="{Binding AddOutputCommand}" />
<Button Style="{StaticResource IconButton}"
Content="{StaticResource RemoveKeyIcon}"
Command="{Binding RemoveOutputCommand}" />
</StackPanel>
</nodify:Node>
</DataTemplate>
<DataTemplate DataType="{x:Type local:OperationGroupViewModel}">
<nodify:GroupingNode ActualSize="{Binding GroupSize, Mode=TwoWay}">
<nodify:GroupingNode.Header>
<shared:EditableTextBlock Text="{Binding Title}"
FontWeight="SemiBold"
IsEditing="True" />
</nodify:GroupingNode.Header>
</nodify:GroupingNode>
</DataTemplate>
<DataTemplate DataType="{x:Type local:AuthOperationViewModel}">
<nodify:Node Header="🔐 Auth Configuration"
Content="{Binding}"
Input="{Binding Input}"
Output="{Binding Output}">
<nodify:Node.ContentTemplate>
<DataTemplate DataType="{x:Type local:AuthOperationViewModel}">
<StackPanel Margin="4">
<TextBlock Text="Base URL" FontWeight="SemiBold" Margin="0 0 0 2" />
<TextBox Text="{Binding BaseUrl, UpdateSourceTrigger=PropertyChanged}"
MinWidth="180" Margin="0 0 0 6" />
<TextBlock Text="Auth Type" FontWeight="SemiBold" Margin="0 0 0 2" />
<ComboBox SelectedItem="{Binding AuthType}"
MinWidth="180" Margin="0 0 0 6">
<sys:String>Bearer Token</sys:String>
<sys:String>Basic Auth</sys:String>
<sys:String>API Key</sys:String>
<sys:String>None</sys:String>
</ComboBox>
</StackPanel>
</DataTemplate>
</nodify:Node.ContentTemplate>
</nodify:Node>
</DataTemplate>
<DataTemplate DataType="{x:Type local:OperationViewModel}">
<nodify:Node Content="{Binding Title}"
Input="{Binding Input}"
Output="{Binding Output}" />
</DataTemplate>
</nodify:NodifyEditor.Resources>
<nodify:NodifyEditor.InputBindings>
<KeyBinding Key="Delete"
Command="{Binding DeleteSelectionCommand}" />
<KeyBinding Key="G"
Modifiers="Ctrl"
Command="{Binding GroupSelectionCommand}" />
</nodify:NodifyEditor.InputBindings>
<nodify:NodifyEditor.CommandBindings>
<CommandBinding Command="{x:Static ApplicationCommands.ContextMenu}"
Executed="OpenContextMenu_Executed" />
</nodify:NodifyEditor.CommandBindings>
<nodify:NodifyEditor.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard Name="AnimateBorder"
Storyboard="{StaticResource AnimateBorder}" />
</EventTrigger>
</nodify:NodifyEditor.Triggers>
<CompositeCollection>
<nodify:DecoratorContainer DataContext="{Binding OperationsMenu}"
Location="{Binding Location}">
<local:OperationsMenuView />
</nodify:DecoratorContainer>
</CompositeCollection>
</nodify:NodifyEditor>
<Grid Background="{StaticResource LargeGridLinesDrawingBrush}"
Panel.ZIndex="-2" />
<Border HorizontalAlignment="Right"
MinWidth="200"
MaxWidth="300"
MaxHeight="500"
Padding="7"
Margin="10"
CornerRadius="3"
BorderThickness="2">
<Border.Background>
<SolidColorBrush Color="{DynamicResource BackgroundColor}"
Opacity="0.7" />
</Border.Background>
<DockPanel>
<Button Content="📥 Import Swagger"
Command="{Binding Calculator.OperationsMenu.ImportSwaggerCommand}"
Margin="5"
Padding="8 4"
Cursor="Hand"
HorizontalAlignment="Stretch"
Background="{DynamicResource NodeInput.BorderBrush}"
Foreground="{DynamicResource ForegroundBrush}"
FontWeight="Bold"
DockPanel.Dock="Top" />
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding Calculator.OperationsMenu.AvailableOperations}"
Focusable="False">
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="FrameworkElement.Margin"
Value="5" />
<Setter Property="FrameworkElement.HorizontalAlignment"
Value="Left" />
<Setter Property="FrameworkElement.Cursor"
Value="Hand" />
<Setter Property="FrameworkElement.ToolTip"
Value="Drag and drop into the editor" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:OperationViewModel}">
<nodify:Node Content="{Binding Title}"
Input="{Binding Input}"
Output="{Binding Output}"
BorderBrush="{StaticResource AnimatedBrush}"
BorderThickness="2"
MouseMove="OnNodeDrag"
Focusable="True"
KeyboardNavigation.TabNavigation="None">
</nodify:Node>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
</Border>
<Border HorizontalAlignment="Left"
MinWidth="200"
MaxWidth="250"
MaxHeight="500"
MinHeight="200"
Padding="7"
Margin="10"
CornerRadius="3"
BorderThickness="2">
<Border.Background>
<SolidColorBrush Color="{DynamicResource BackgroundColor}"
Opacity="0.7" />
</Border.Background>
<DockPanel>
<TextBlock Text="Variables" FontWeight="Bold" Margin="0 0 0 4" DockPanel.Dock="Top" />
<Button Content=" Add New Variable"
Command="{Binding Calculator.OperationsMenu.AddVariableCommand}"
Margin="0 0 0 6"
Padding="6 3"
Cursor="Hand"
DockPanel.Dock="Top"
Background="{DynamicResource NodeInput.BorderBrush}"
Foreground="{DynamicResource ForegroundBrush}" />
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel>
<!-- Simple Variables -->
<ItemsControl ItemsSource="{Binding Calculator.OperationsMenu.AvailableVariables}"
Focusable="False">
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="FrameworkElement.Margin" Value="3" />
<Setter Property="FrameworkElement.HorizontalAlignment" Value="Left" />
<Setter Property="FrameworkElement.Cursor" Value="Hand" />
<Setter Property="FrameworkElement.ToolTip" Value="Drag and drop to GET or SET this variable" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:OperationInfoViewModel}">
<Border Background="#3E3E42" CornerRadius="3" Padding="6 4"
MouseMove="OnNodeDrag" Cursor="Hand">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Title}" Foreground="LightGreen" FontWeight="SemiBold" />
<TextBlock Text=" : " Foreground="Gray" />
<TextBlock Text="{Binding VariableType}" Foreground="CornflowerBlue" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Separator -->
<TextBlock Text="Models" FontWeight="Bold" Margin="0 8 0 4"
Visibility="{Binding Calculator.OperationsMenu.AvailableModels.Count, Converter={shared:BooleanToVisibilityConverter}}" />
<!-- Class Model Variables -->
<ItemsControl ItemsSource="{Binding Calculator.OperationsMenu.AvailableModels}"
Focusable="False">
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="FrameworkElement.Margin" Value="5" />
<Setter Property="FrameworkElement.HorizontalAlignment" Value="Left" />
<Setter Property="FrameworkElement.Cursor" Value="Hand" />
<Setter Property="FrameworkElement.ToolTip" Value="Drag and drop into the editor" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:OperationViewModel}">
<nodify:Node Content="{Binding Title}"
Input="{Binding Input}"
Output="{Binding Output}"
BorderBrush="{StaticResource AnimatedBrush}"
BorderThickness="2"
MouseMove="OnNodeDrag"
Focusable="True"
KeyboardNavigation.TabNavigation="None">
</nodify:Node>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</DockPanel>
</Border>
</Grid>
</UserControl>

View File

@@ -0,0 +1,101 @@
using Nodify.Interactivity;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace Nodify.Calculator
{
public class OperationsMenuHandler : InputElementState<NodifyEditor>
{
private static InputGesture OpenGesture { get; } = new Interactivity.MouseGesture(MouseAction.RightClick);
private static InputGesture CloseGesture { get; } = new Interactivity.MouseGesture(MouseAction.LeftClick);
private OperationsMenuViewModel ViewModel => ((CalculatorViewModel)Element.DataContext).OperationsMenu;
public OperationsMenuHandler(NodifyEditor element) : base(element)
{
ProcessHandledEvents = true;
}
protected override void OnMouseUp(MouseButtonEventArgs e)
{
if (!e.Handled && OpenGesture.Matches(e.Source, e))
{
ViewModel.OpenAt(Element.MouseLocation);
}
}
protected override void OnMouseDown(MouseButtonEventArgs e)
{
if (CloseGesture.Matches(e.Source, e))
{
ViewModel.Close();
}
}
}
public partial class EditorView : UserControl
{
public EditorView()
{
InitializeComponent();
}
static EditorView()
{
InputProcessor.Shared<NodifyEditor>.RegisterHandlerFactory(editor => new OperationsMenuHandler(editor));
}
private void OnDropNode(object sender, DragEventArgs e)
{
if (e.Source is NodifyEditor editor && editor.DataContext is CalculatorViewModel calculator
&& e.Data.GetData(typeof(OperationInfoViewModel)) is OperationInfoViewModel operation)
{
if (operation.IsModelNode)
{
var dc = editor.DataContext as CalculatorViewModel;
var lc = editor.GetLocationInsideEditor(e);
var orTitle = operation.Title;
dc.OpenGetSetVariable(lc, orTitle);
}
else if (operation.IsSimpleVariable)
{
var lc = editor.GetLocationInsideEditor(e);
calculator.OpenGetSetForVariable(lc, operation);
}
else
{
OperationViewModel op = OperationFactory.GetOperation(operation);
op.Location = editor.GetLocationInsideEditor(e);
calculator.Operations.Add(op);
}
e.Handled = true;
}
}
private void OnNodeDrag(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed && ((FrameworkElement)sender).DataContext is OperationInfoViewModel operation)
{
var data = new DataObject(typeof(OperationInfoViewModel), operation);
DragDrop.DoDragDrop(this, data, DragDropEffects.Copy);
}
}
private void OpenContextMenu_Executed(object sender, ExecutedRoutedEventArgs e)
{
if (e.Source is NodifyEditor editor && editor.DataContext is CalculatorViewModel calculator)
{
if (calculator.OperationsMenu.IsVisible)
{
calculator.OperationsMenu.Close();
}
else
{
calculator.OperationsMenu.OpenAt(editor.ViewportLocation + new Vector(editor.ViewportSize.Width / 3, editor.ViewportSize.Height / 3));
}
}
}
}
}

View File

@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Windows.Input;
namespace Nodify.Calculator
{
public class EditorViewModel : ObservableObject
{
public event Action<EditorViewModel, CalculatorViewModel>? OnOpenInnerCalculator;
public EditorViewModel? Parent { get; set; }
public EditorViewModel()
{
Calculator = new CalculatorViewModel();
OpenCalculatorCommand = new DelegateCommand<CalculatorViewModel>(calculator =>
{
OnOpenInnerCalculator?.Invoke(this, calculator);
});
}
public INodifyCommand OpenCalculatorCommand { get; }
public Guid Id { get; } = Guid.NewGuid();
private CalculatorViewModel _calculator = default!;
public CalculatorViewModel Calculator
{
get => _calculator;
set => SetProperty(ref _calculator, value);
}
private string? _name;
public string? Name
{
get => _name;
set => SetProperty(ref _name, value);
}
}
}

View File

@@ -0,0 +1,677 @@
using System;
using System.Collections.Generic;
using System.Linq;
#if NET8_0_OR_GREATER
using System.Net.Http;
using System.Reflection.Emit;
using System.Reflection;
#endif
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using System.IO;
using Newtonsoft.Json;
namespace Nodify.Calculator
{
public enum logType
{
Information,
Warning,
Error
}
public delegate void LogMe(string message, logType logtype = logType.Information);
public class Executor
{
private readonly EditorViewModel editorViewModel;
public event LogMe OnLogMe;
static bool isAlreadyLogged = false;
static Dictionary<string, string> outputs = new Dictionary<string, string>();
static Dictionary<string, dynamic> variables = new Dictionary<string, dynamic>();
// Auth configuration populated from the Auth node
private string _authBaseUrl = string.Empty;
private string _authType = string.Empty;
private string _authToken = string.Empty;
private string _authApiKey = string.Empty;
private string _authUsername = string.Empty;
private string _authPassword = string.Empty;
public Executor(EditorViewModel editorViewModel)
{
this.editorViewModel = editorViewModel;
}
public string PerformPreCheck()
{
string errorString = string.Empty;
var calcModel = editorViewModel.Calculator;
var allnodes = calcModel.Operations;
errorString = ValidateRequiredNodes("end", allnodes);
if (!string.IsNullOrEmpty(errorString))
{
OnLogMe?.Invoke(errorString, logType.Error);
return errorString;
}
errorString = ValidateRequiredNodes("begin", allnodes);
OnLogMe?.Invoke(errorString, logType.Error);
return errorString;
}
public string Execute()
{
string errorString = string.Empty;
try
{
OnLogMe?.Invoke("Starting executing the flow.");
OnLogMe?.Invoke("Wish all the best to us :)");
OnLogMe?.Invoke("Perorming chain check, it may take some time depending upon the number of nodes.");
outputs.Clear();
var calcModel = editorViewModel.Calculator;
var allnodes = calcModel.Operations;
var allConnections = calcModel.Connections;
// Resolve Auth node configuration before execution
ResolveAuthNode(allnodes);
// Resolve GET variable nodes (they are not in the flow chain)
ResolveGetVariableNodes(allnodes, allConnections);
PerformChainCheck(allnodes, allConnections, false);
PerformChainCheck(allnodes, allConnections, true); //Execute operations
}
catch (Exception e)
{
OnLogMe?.Invoke("either your or our bad luck :(", logType.Error);
OnLogMe?.Invoke("Error Details : ", logType.Error);
OnLogMe?.Invoke(e.Message, logType.Error);
OnLogMe?.Invoke(e.StackTrace, logType.Error);
errorString = "Error occured dueing execution of flow";
}
return errorString;
}
public static string GenerateClassFromJson(string json, string className)
{
// Parse the JSON to understand its structure
JToken token = JToken.Parse(json);
if (token.Type == JTokenType.Array)
{
// If the JSON is an array, get the first object in the array for structure
JArray array = (JArray)token;
if (array.Count > 0 && array[0].Type == JTokenType.Object)
{
JObject firstObject = (JObject)array[0];
return $"{GenerateClassFromJObject(firstObject, className)}";
}
return $"The JSON array does not contain valid objects.";
}
else if (token.Type == JTokenType.Object)
{
// If the JSON is an object
JObject obj = (JObject)token;
return GenerateClassFromJObject(obj, className);
}
else
{
return "Unsupported JSON structure.";
}
}
/// <summary>
/// Generates a class definition string from a JObject.
/// </summary>
/// <param name="jObject">The JObject representing the JSON object.</param>
/// <param name="className">The name of the class to generate.</param>
/// <returns>The class definition as a string.</returns>
public static string GenerateClassFromJObject(JObject jObject, string className)
{
var sb = new StringBuilder();
// Start class definition
sb.AppendLine($"public class {className}");
sb.AppendLine("{");
// Loop through properties in the JObject and generate class fields
foreach (var property in jObject.Properties())
{
string propName = property.Name;
string propType = GetCSharpType(property.Value.Type);
// Generate the property definition
sb.AppendLine($"\tpublic {propType} {ToPascalCase(propName)} {{ get; set; }}");
}
// End class definition
sb.AppendLine("}");
return sb.ToString();
}
/// <summary>
/// Maps a JTokenType to a C# type.
/// </summary>
/// <param name="jsonType">The JTokenType.</param>
/// <returns>The corresponding C# type as a string.</returns>
public static string GetCSharpType(JTokenType jsonType)
{
return jsonType switch
{
JTokenType.Integer => "int",
JTokenType.Float => "double",
JTokenType.String => "string",
JTokenType.Boolean => "bool",
JTokenType.Object => "object",
JTokenType.Array => "List<object>",
_ => "string", // Default to string for other types
};
}
/// <summary>
/// Converts a string to PascalCase for naming conventions.
/// </summary>
/// <param name="input">The string to convert.</param>
/// <returns>A PascalCase version of the input string.</returns>
public static string ToPascalCase(string input)
{
if (string.IsNullOrEmpty(input))
return input;
return char.ToUpper(input[0]) + input.Substring(1);
}
static Type CreateTypeFromJson(JsonElement root, string typeName)
{
#if NET8_0_OR_GREATER
var assemblyName = new AssemblyName("DynamicAssembly");
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
var typeBuilder = moduleBuilder.DefineType(typeName, TypeAttributes.Public);
foreach (var prop in root.EnumerateObject())
{
Type propType = prop.Value.ValueKind switch
{
JsonValueKind.String => typeof(string),
JsonValueKind.Number => typeof(int),
JsonValueKind.True or JsonValueKind.False => typeof(bool),
_ => typeof(object)
};
var fieldBuilder = typeBuilder.DefineField("_" + prop.Name, propType, FieldAttributes.Private);
var propertyBuilder = typeBuilder.DefineProperty(prop.Name, PropertyAttributes.HasDefault, propType, null);
// Getter
var getter = typeBuilder.DefineMethod(
"get_" + prop.Name,
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
propType,
Type.EmptyTypes
);
var getterIL = getter.GetILGenerator();
getterIL.Emit(OpCodes.Ldarg_0);
getterIL.Emit(OpCodes.Ldfld, fieldBuilder);
getterIL.Emit(OpCodes.Ret);
propertyBuilder.SetGetMethod(getter);
// Setter
var setter = typeBuilder.DefineMethod(
"set_" + prop.Name,
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,
null,
new Type[] { propType }
);
var setterIL = setter.GetILGenerator();
setterIL.Emit(OpCodes.Ldarg_0);
setterIL.Emit(OpCodes.Ldarg_1);
setterIL.Emit(OpCodes.Stfld, fieldBuilder);
setterIL.Emit(OpCodes.Ret);
propertyBuilder.SetSetMethod(setter);
}
return typeBuilder.CreateType()!;
#endif
return default;
}
private void CreateModelsFromString(string jsonString)
{
#if NET8_0_OR_GREATER
var jsonDoc = JsonDocument.Parse(jsonString);
var root = jsonDoc.RootElement;
if (root.ValueKind == JsonValueKind.Object)
{
ParseFromJsonelement(root);
}
else if(root.ValueKind == JsonValueKind.Array)
{
foreach (var item in root.EnumerateArray())
{
if (item.ValueKind == JsonValueKind.Object)
{
ParseFromJsonelement(item);
break;
}
}
}
#endif
}
private void ParseFromJsonelement(JsonElement jsonElem)
{
#if NET8_0_OR_GREATER
Type dynamicType = CreateTypeFromJson(jsonElem, "DynamicPerson");
object instance = Activator.CreateInstance(dynamicType);
foreach (var prop in jsonElem.EnumerateObject())
{
PropertyInfo pi = dynamicType.GetProperty(prop.Name);
object value = prop.Value.ValueKind switch
{
JsonValueKind.String => prop.Value.GetString(),
JsonValueKind.Number => prop.Value.GetDouble(),
JsonValueKind.True => true,
JsonValueKind.False => false,
_ => null
};
pi.SetValue(instance, value);
}
Console.WriteLine("Properties of runtime class:");
foreach (var prop in dynamicType.GetProperties())
{
Console.WriteLine($"{prop.Name} = {prop.GetValue(instance)}");
}
#endif
}
private void StartExecution(OperationViewModel op, ICollection<ConnectionViewModel> connections)
{
var url = op.Title ?? "";
if (url.ToLower().Contains("create model"))
{
var conByTitle = connections.Where(c => c.Input.Operation.Title.ToLower().Contains("create model")).ToList();
if (conByTitle.Count <= 0)
{
OnLogMe?.Invoke("No input found", logType.Error);
throw new Exception("Input connection missig for : " + url);
}
var flowConnection = conByTitle.Where(c=>c.Input.Shape != ConnectorShape.Triangle).FirstOrDefault();
if (flowConnection == null)
{
OnLogMe?.Invoke("No input found", logType.Error);
throw new Exception("Input connection missig for : " + url);
}
var outputNodeId = flowConnection.InputNodeId;
string outputValue = string.Empty;
if (outputs.TryGetValue(outputNodeId, out outputValue))
{
//Convert model here
//CreateModelsFromString(outputValue);
var customModelDir = "CustomModels";
Directory.CreateDirectory(customModelDir);
string className = "Class 1";
className = className.Replace(" ", "");
var classStruct = GenerateClassFromJson(outputValue, className);
string flPath = Path.Join(customModelDir, $"{className}.cs");
File.WriteAllText(flPath, classStruct);
OperationInfoViewModel opv = new OperationInfoViewModel()
{
Title = className,
IsModelNode = true,
Type = OperationType.System,
sysOp = SystemOperations.GET_SET,
ClassName = className
};
this.editorViewModel.Calculator.OperationsMenu.AddNewModel(opv);
}
}
if (url.ToLower().Contains("set"))
{
// Handle simple variable SET (e.g., "SET myVar (string)")
if (op is SystemOperationViewModel sysVarOp && url.Contains("(") && url.Contains(")"))
{
var parts = url.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 2)
{
var varName = parts[1];
// Find the data input connection (non-triangle)
var inputCons = connections.Where(c => c.Input.Operation == op && c.Input.Shape != ConnectorShape.Triangle).ToList();
if (inputCons.Any())
{
var sourceConn = inputCons.First();
var sourceNodeId = sourceConn.Output.Operation.NodeId;
if (outputs.TryGetValue(sourceNodeId, out var sourceVal))
{
variables[varName] = sourceVal;
OnLogMe?.Invoke($"Variable '{varName}' SET to: {sourceVal}");
}
else
{
// Use the connector value directly
variables[varName] = sourceConn.Output.Value.ToString();
OnLogMe?.Invoke($"Variable '{varName}' SET to connector value: {sourceConn.Output.Value}");
}
}
else
{
// No connection — use default value from the Value input connector
var valueInput = op.Input.FirstOrDefault(i => i.Title == "Value");
if (valueInput != null)
{
variables[varName] = valueInput.Value.ToString();
OnLogMe?.Invoke($"Variable '{varName}' SET to default: {valueInput.Value}");
}
}
outputs[op.NodeId] = variables.ContainsKey(varName) ? variables[varName]?.ToString() ?? "" : "";
}
return;
}
var fullTitle = url;
var classNameToSet = fullTitle.Split(" ", StringSplitOptions.RemoveEmptyEntries)[1];
var inputConnectionList = connections.Where(c => c.Input.Operation.Title == url).ToList();
var inputConnection = inputConnectionList.Where(c => c.Input.Shape != ConnectorShape.Triangle).FirstOrDefault();
if (inputConnection == null)
{
OnLogMe?.Invoke("No input found", logType.Error);
throw new Exception("Input connection missig for : " + url);
}
var outPutNode = inputConnection.Output;
if (outPutNode == null)
{
OnLogMe?.Invoke("No output found", logType.Error);
throw new Exception("Output connection missig for : " + url);
}
if (outPutNode.Operation.Title.ToLower().Contains("parse json"))
{
var inputNodeForParseJson = connections.Where(c => c.Input.Operation.Title == outPutNode.Title).FirstOrDefault();
if (inputNodeForParseJson == null)
{
OnLogMe?.Invoke("No input found", logType.Error);
throw new Exception("Input connection missig for : " + url);
}
var inputNodeOutput = outputs[inputNodeForParseJson.InputNodeId];
if (inputNodeOutput != null)
{
var obj = JsonConvert.DeserializeObject(inputNodeOutput);
variables[op.NodeId] = obj;
}
}
}
OnLogMe?.Invoke($"Execution started : {url}");
if (url.ToLower() == "begin" || url.ToLower() == "end")
{
OnLogMe?.Invoke($"Being or End node found. skipping it");
return;
}
if (url.ToLower() == "auth")
{
ResolveAuthNode(editorViewModel.Calculator.Operations);
OnLogMe?.Invoke($"Auth node resolved. Base URL: {_authBaseUrl}, Auth Type: {_authType}");
return;
}
OnLogMe?.Invoke($"Starting Execution : {url}");
var res = GetResponse(url, "get");
if (!string.IsNullOrEmpty(res))
{
outputs.Add(op.NodeId, res);
}
OnLogMe?.Invoke($"Response Result : {res}");
}
private void ResolveAuthNode(ICollection<OperationViewModel> allNodes)
{
var authNode = allNodes.OfType<AuthOperationViewModel>().FirstOrDefault();
if (authNode == null)
{
OnLogMe?.Invoke("No Auth node found. Using defaults.", logType.Warning);
return;
}
foreach (var inp in authNode.Input)
{
var title = inp.Title?.Trim() ?? string.Empty;
// For unconnected inputs the user types a value into the textbox, which is stored as Value (double).
// But the actual text is stored in the Title's textbox or the Value textbox.
// We read the connector's Value textbox string representation via the bound text.
var val = GetConnectorTextValue(inp);
switch (title)
{
case "Base URL": _authBaseUrl = val; break;
case "Auth Type": _authType = val; break;
case "Token": _authToken = val; break;
case "API Key": _authApiKey = val; break;
case "Username": _authUsername = val; break;
case "Password": _authPassword = val; break;
}
}
// Also read from the view-model properties which are bound to the text boxes in the node
if (!string.IsNullOrWhiteSpace(authNode.BaseUrl))
_authBaseUrl = authNode.BaseUrl;
if (!string.IsNullOrWhiteSpace(authNode.AuthType))
_authType = authNode.AuthType;
OnLogMe?.Invoke($"Auth configured — Base URL: {_authBaseUrl}, Auth Type: {_authType}");
}
private void ResolveGetVariableNodes(ICollection<OperationViewModel> allNodes, ICollection<ConnectionViewModel> connections)
{
// Find all GET variable nodes (not in flow chain) and populate their output
foreach (var node in allNodes)
{
var title = node.Title ?? "";
if (!title.StartsWith("GET ") || !title.Contains("(") || !title.Contains(")"))
continue;
var parts = title.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2) continue;
var varName = parts[1];
// Check if the variable already has a value
if (variables.TryGetValue(varName, out var existingVal))
{
outputs[node.NodeId] = existingVal?.ToString() ?? "";
// Set the output connector value for downstream nodes
var outConn = node.Output.FirstOrDefault(c => c.Shape != ConnectorShape.Triangle);
if (outConn != null)
{
if (double.TryParse(existingVal?.ToString(), out double dVal))
outConn.Value = dVal;
}
OnLogMe?.Invoke($"GET variable '{varName}' resolved to: {existingVal}");
}
else
{
OnLogMe?.Invoke($"GET variable '{varName}' has no value yet (will resolve during execution).", logType.Warning);
}
}
}
private static string GetConnectorTextValue(ConnectorViewModel connector)
{
// When the connector is not connected, the user enters text in the Value text box.
// Value is a double, so we try to use it as a string representation.
// However, for string inputs (URL, token, etc.) the value won't be meaningful as a double.
// The user-typed string is actually stored in the Title field for unconnected inputs in some cases,
// but here we rely on the ConnectorViewModel.Value being 0 (default) and the actual string
// is not captured as double. So we return empty — the AuthOperationViewModel properties are the
// primary source set via the node's text boxes.
return string.Empty;
}
private string GetResponse(string url, string type)
{
string baseURL = !string.IsNullOrWhiteSpace(_authBaseUrl) ? _authBaseUrl : "https://localhost:7107";
string responseString = string.Empty;
#if NET8_0_OR_GREATER
using (HttpClient client = new HttpClient())
{
client.BaseAddress = new Uri(baseURL);
// Apply authentication headers
if (!string.IsNullOrWhiteSpace(_authType))
{
var authTypeLower = _authType.Trim().ToLower();
if (authTypeLower.Contains("bearer") && !string.IsNullOrWhiteSpace(_authToken))
{
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _authToken);
}
else if (authTypeLower.Contains("basic") &&
!string.IsNullOrWhiteSpace(_authUsername))
{
var credentials = Convert.ToBase64String(
Encoding.UTF8.GetBytes($"{_authUsername}:{_authPassword}"));
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", credentials);
}
else if (authTypeLower.Contains("api") && !string.IsNullOrWhiteSpace(_authApiKey))
{
client.DefaultRequestHeaders.Add("X-Api-Key", _authApiKey);
}
}
if (type == "get")
{
HttpResponseMessage response = client.GetAsync(url).Result;
response.EnsureSuccessStatusCode();
responseString = response.Content.ReadAsStringAsync().Result;
}
}
#endif
return responseString;
}
private void PerformChainCheck(ICollection<OperationViewModel> allNodes, ICollection<ConnectionViewModel> connections, bool isExecute)
{
string startNodeTitle = "begin";
string endNodeTitle = "end";
// Find the starting node
var startNode = allNodes.FirstOrDefault(node => node.Title?.ToLower() == startNodeTitle);
if (startNode == null)
{
OnLogMe?.Invoke("No Begin node found", logType.Error);
throw new Exception("Begin node not found");
}
// Track visited nodes to prevent infinite loops or revisits
HashSet<string> visitedNodes = new HashSet<string>();
// Perform the DFS Traversal
bool isValidChain = TraverseChain(startNode, endNodeTitle, connections, visitedNodes, isExecute);
if (isValidChain)
{
OnLogMe?.Invoke("Found complete chain...");
OnLogMe?.Invoke("You did it champ...");
}
else
{
OnLogMe?.Invoke("Broken Chain found!!! Please fix the chain from begin to end.", logType.Warning);
}
}
private bool TraverseChain(OperationViewModel currentNode, string endNodeTitle,
ICollection<ConnectionViewModel> connections,
HashSet<string> visitedNodes, bool isExecute)
{
OnLogMe?.Invoke($"Checking node: {currentNode.Title} , NodeId : {currentNode.NodeId}");
// If we've reached the "end" node, the chain is valid
if (currentNode.Title?.Equals(endNodeTitle, StringComparison.OrdinalIgnoreCase) == true)
{
return true;
}
// Detect cycles and avoid infinite recursion
if (visitedNodes.Contains(currentNode.NodeId))
{
OnLogMe?.Invoke($"Cycle detected at node: {currentNode.Title}. Broken chain found!", logType.Warning);
return false;
}
visitedNodes.Add(currentNode.NodeId);
// Find all outgoing connections from the current node
var outgoingConnections = connections.Where(conn => conn.Output?.Operation == currentNode && conn.Output.Shape == ConnectorShape.Triangle);
if (!outgoingConnections.Any())
{
// Only log the first "broken chain" warning, and stop further logging on backtrack
OnLogMe?.Invoke($"Broken chain detected at node: {currentNode.Title}", logType.Warning);
isAlreadyLogged = true;
return false;
}
// Check all outgoing connections
foreach (var connection in outgoingConnections)
{
var nextNode = connection.Input?.Operation;
if (nextNode == null)
{
// Handle null input in the connection
OnLogMe?.Invoke($"Broken chain detected due to null input connection from node: {currentNode.Title}", logType.Warning);
return false;
}
OnLogMe?.Invoke($"Following connection from {currentNode.Title} to {nextNode.Title}");
if (isExecute)
{
StartExecution(currentNode, connections);
}
// Recursively check the next node; stop on the first failure
if (TraverseChain(nextNode, endNodeTitle, connections, visitedNodes, isExecute))
{
return true; // If a valid path to the "end" is found, exit
}
}
if (!isAlreadyLogged)
{
// If no connections lead to the end, this is the broken point => log here ONLY
OnLogMe?.Invoke($"Broken chain detected at node: {currentNode.Title}", logType.Warning);
isAlreadyLogged = true;
}
return false;
}
private string ValidateRequiredNodes(string nodeName, ICollection<OperationViewModel> allnodes)
{
var requireNode = allnodes.Where(c => c.Title.ToLower() == nodeName).ToList();
if (requireNode.Count > 1)
{
return $"One or more {nodeName} node found. Only one allowed at a time.";
}
else if (requireNode.Count == 0)
{
return $"No {nodeName} node found. At least one {nodeName} node required system to work";
}
return string.Empty;
}
}
}

View File

@@ -0,0 +1,33 @@
namespace Nodify.Calculator
{
public class ExpandoOperationViewModel : OperationViewModel
{
public ExpandoOperationViewModel()
{
AddInputCommand = new RequeryCommand(
() => Input.Add(new ConnectorViewModel()),
() => Input.Count < MaxInput);
RemoveInputCommand = new RequeryCommand(
() => Input.RemoveAt(Input.Count - 1),
() => Input.Count > MinInput);
}
public INodifyCommand AddInputCommand { get; }
public INodifyCommand RemoveInputCommand { get; }
private uint _minInput = 0;
public uint MinInput
{
get => _minInput;
set => SetProperty(ref _minInput, value);
}
private uint _maxInput = uint.MaxValue;
public uint MaxInput
{
get => _maxInput;
set => SetProperty(ref _maxInput, value);
}
}
}

View File

@@ -0,0 +1,59 @@
using StringMath;
using System.Collections.Generic;
using System.Linq;
namespace Nodify.Calculator
{
public class ExpressionOperationViewModel : OperationViewModel
{
private MathExpr? _expr;
private string? _expression;
public string? Expression
{
get => _expression;
set => SetProperty(ref _expression, value)
.Then(GenerateInput);
}
private void GenerateInput()
{
try
{
_expr = Expression!.ToMathExpr();
ConnectorViewModel[]? toRemove = Input.Where(i => !_expr.LocalVariables.Contains(i.Title)).ToArray();
toRemove.ForEach(i => Input.Remove(i));
HashSet<string> existingVars = Input.Select(s => s.Title).Where(s => s != null).ToHashSet()!;
foreach (string variable in _expr.LocalVariables.Except(existingVars))
{
Input.Add(new ConnectorViewModel
{
Title = variable
});
}
OnInputValueChanged();
}
catch
{
}
}
protected override void OnInputValueChanged()
{
if (Output != null && _expr != null)
{
try
{
Input.ForEach(i => _expr.Substitute(i.Title!, i.Value));
//Output.Value = _expr.Result;
}
catch
{
}
}
}
}
}

View File

@@ -0,0 +1,23 @@
<Window x:Class="Nodify.Calculator.FlowRunner"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Nodify.Calculator"
mc:Ignorable="d"
Loaded="Window_Loaded"
Title="FlowRunner" Height="450" Width="800" ResizeMode="NoResize" WindowStartupLocation="CenterOwner">
<Grid>
<RichTextBox x:Name="LogRichTextBox"
IsReadOnly="True"
VerticalScrollBarVisibility="Auto"
Background="#FF2D2D30"
Foreground="White"
FontFamily="Consolas"
FontSize="14"
BorderThickness="0">
<!-- Initialize with an empty document -->
<FlowDocument />
</RichTextBox>
</Grid>
</Window>

View File

@@ -0,0 +1,73 @@
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace Nodify.Calculator
{
/// <summary>
/// Interaction logic for FlowRunner.xaml
/// </summary>
public partial class FlowRunner : Window
{
private readonly Executor _executor;
public FlowRunner(Executor executor)
{
InitializeComponent();
_executor = executor;
}
private void WriteLog(string message, logType logtype)
{
// IMPORTANT: UI updates must happen on the main UI thread.
// Dispatcher.Invoke ensures that, even if the event is fired from a background thread.
Dispatcher.Invoke(() =>
{
// Choose a color based on the log type
Brush color = Brushes.White; // Default for Information
switch (logtype)
{
case logType.Warning:
color = Brushes.Yellow;
break;
case logType.Error:
color = Brushes.Red;
break;
}
// Create a text run with the specified color
var run = new Run($"{DateTime.Now:HH:mm:ss} [{logtype}]: {message}\n")
{
Foreground = color
};
// Add the text to a new paragraph and add the paragraph to the RichTextBox
var paragraph = new Paragraph();
paragraph.Inlines.Add(run);
LogRichTextBox.Document.Blocks.Add(paragraph);
// Auto-scroll to the bottom
LogRichTextBox.ScrollToEnd();
});
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
Task.Run(() => {
_executor.OnLogMe += WriteLog;
_executor.Execute();
});
}
}
}

View File

@@ -0,0 +1,189 @@
using LiteDB;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq.Expressions;
public class LiteDbHelper<T> : IDisposable where T : class
{
private string dbPath = @"MyData.db";
private readonly LiteDatabase _database;
private readonly ILiteCollection<T> _collection;
/// <summary>
/// Initializes a new instance of the LiteDbHelper class.
/// </summary>
/// <param name="databasePath">The path to the LiteDB database file.</param>
/// <param name="collectionName">The name of the collection to work with.</param>
public LiteDbHelper(string collectionName)
{
_database = new LiteDatabase(dbPath);
_collection = _database.GetCollection<T>(collectionName);
}
// --- CRUD Operations ---
/// <summary>
/// Inserts a new document into the collection.
/// </summary>
/// <param name="document">The document to insert.</param>
/// <returns>The BsonValue of the inserted document's Id.</returns>
public BsonValue Insert(T document)
{
return _collection.Insert(document);
}
/// <summary>
/// Inserts a collection of documents.
/// </summary>
/// <param name="documents">The documents to insert.</param>
/// <returns>The number of documents inserted.</returns>
public int BulkInsert(IEnumerable<T> documents)
{
return _collection.Insert(documents);
}
/// <summary>
/// Updates a document in the collection.
/// </summary>
/// <param name="document">The document to update.</param>
/// <returns>True if the document was updated, otherwise false.</returns>
public bool Update(T document)
{
return _collection.Update(document);
}
/// <summary>
/// Deletes a document from the collection by its Id.
/// </summary>
/// <param name="id">The Id of the document to delete.</param>
/// <returns>True if the document was deleted, otherwise false.</returns>
public bool Delete(BsonValue id)
{
return _collection.Delete(id);
}
/// <summary>
/// Deletes documents from the collection based on a predicate.
/// </summary>
/// <param name="predicate">The expression to filter documents to delete.</param>
/// <returns>The number of documents deleted.</returns>
public int DeleteMany(Expression<Func<T, bool>> predicate)
{
return _collection.DeleteMany(predicate);
}
// --- Query Operations ---
/// <summary>
/// Finds a single document by its Id.
/// </summary>
/// <param name="id">The Id of the document.</param>
/// <returns>The document if found, otherwise null.</returns>
public T FindById(BsonValue id)
{
return _collection.FindById(id);
}
/// <summary>
/// Finds the first document that matches the predicate.
/// </summary>
/// <param name="predicate">The expression to filter documents.</param>
/// <returns>The first matching document, or null if none are found.</returns>
public T FindOne(Expression<Func<T, bool>> predicate)
{
return _collection.FindOne(predicate);
}
/// <summary>
/// Finds all documents in the collection.
/// </summary>
/// <returns>An enumerable of all documents.</returns>
public IEnumerable<T> FindAll()
{
return _collection.FindAll();
}
/// <summary>
/// Finds documents based on a predicate.
/// </summary>
/// <param name="predicate">The expression to filter documents.</param>
/// <returns>An enumerable of matching documents.</returns>
public IEnumerable<T> Find(Expression<Func<T, bool>> predicate)
{
return _collection.Find(predicate);
}
/// <summary>
/// Checks if any document exists that matches the predicate.
/// </summary>
/// <param name="predicate">The expression to filter documents.</param>
/// <returns>True if a matching document exists, otherwise false.</returns>
public bool Exists(Expression<Func<T, bool>> predicate)
{
return _collection.Exists(predicate);
}
// --- Indexing ---
/// <summary>
/// Ensures that an index is created for the specified field.
/// </summary>
/// <param name="field">The expression representing the field to be indexed.</param>
/// <param name="unique">Whether the index should enforce unique values.</param>
/// <returns>True if the index was created, otherwise false.</returns>
public bool EnsureIndex<K>(Expression<Func<T, K>> field, bool unique = false)
{
return _collection.EnsureIndex(field, unique);
}
// --- File Storage Operations ---
/// <summary>
/// Uploads a file to the LiteDB file storage.
/// </summary>
/// <param name="id">A unique identifier for the file.</param>
/// <param name="filePath">The path to the file to upload.</param>
public void UploadFile(string id, string filePath)
{
_database.FileStorage.Upload(id, filePath);
}
/// <summary>
/// Downloads a file from the LiteDB file storage.
/// </summary>
/// <param name="id">The unique identifier of the file.</param>
/// <param name="destinationPath">The path to save the downloaded file.</param>
public void DownloadFile(string id, Stream destinationPath)
{
_database.FileStorage.Download(id, destinationPath);
}
/// <summary>
/// Deletes a file from the LiteDB file storage.
/// </summary>
/// <param name="id">The unique identifier of the file to delete.</param>
/// <returns>True if the file was deleted, otherwise false.</returns>
public bool DeleteFile(string id)
{
return _database.FileStorage.Delete(id);
}
/// <summary>
/// Finds a file's metadata in the LiteDB file storage.
/// </summary>
/// <param name="id">The unique identifier of the file.</param>
/// <returns>The LiteFileInfo object if found, otherwise null.</returns>
public LiteFileInfo<string> FindFileById(string id)
{
return _database.FileStorage.FindById(id);
}
/// <summary>
/// Disposes the LiteDatabase connection.
/// </summary>
public void Dispose()
{
_database?.Dispose();
}
}

View File

@@ -0,0 +1,164 @@
<Window x:Class="Nodify.Calculator.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Nodify.Calculator"
xmlns:shared="clr-namespace:Nodify;assembly=Nodify.Shared"
Background="{DynamicResource NodifyEditor.BackgroundBrush}"
Foreground="{DynamicResource ForegroundBrush}"
mc:Ignorable="d"
Title="MainWindow"
Height="650"
Width="1200">
<Window.DataContext>
<local:ApplicationViewModel />
</Window.DataContext>
<Window.InputBindings>
<KeyBinding Key="T"
Modifiers="Ctrl"
Command="{Binding Source={x:Static shared:ThemeManager.SetNextThemeCommand}}" />
<KeyBinding Key="N"
Modifiers="Ctrl"
Command="{Binding AddEditorCommand}" />
<KeyBinding Key="R"
Modifiers="Ctrl"
Command="{Binding RunFlowCommand}" />
<KeyBinding Key="S"
Modifiers="Ctrl"
Command="{Binding SaveFileCommand}" />
<KeyBinding Key="O"
Modifiers="Ctrl"
Command="{Binding OpenFileCommand}" />
<KeyBinding Key="W"
Modifiers="Ctrl"
Command="{Binding CloseEditorCommand}"
CommandParameter="{Binding SelectedEditor.Id}"/>
</Window.InputBindings>
<Window.Resources>
<shared:BindingProxy x:Key="Proxy"
DataContext="{Binding}"/>
<DataTemplate DataType="{x:Type local:EditorViewModel}">
<local:EditorView/>
</DataTemplate>
</Window.Resources>
<Grid>
<shared:TabControlEx ItemsSource="{Binding Editors}"
SelectedItem="{Binding SelectedEditor}"
AddTabCommand="{Binding AddEditorCommand}"
AutoScrollToEnd="{Binding AutoSelectNewEditor}">
<shared:TabControlEx.ItemContainerStyle>
<Style TargetType="{x:Type shared:TabItemEx}"
BasedOn="{StaticResource {x:Type shared:TabItemEx}}">
<Setter Property="Header"
Value="{Binding Name}"/>
<Setter Property="CloseTabCommand"
Value="{Binding DataContext.CloseEditorCommand ,Source={StaticResource Proxy}}"/>
<Setter Property="CloseTabCommandParameter"
Value="{Binding Id}"/>
<Setter Property="ToolTip"
Value="Double click to edit" />
</Style>
</shared:TabControlEx.ItemContainerStyle>
</shared:TabControlEx>
<Expander Header="Click to hide/show"
IsExpanded="True"
Margin="10"
HorizontalAlignment="Left"
VerticalAlignment="Bottom">
<Border MaxWidth="325"
MaxHeight="300"
CornerRadius="3">
<Border.Background>
<SolidColorBrush Color="{DynamicResource BackgroundColor}"
Opacity="0.7" />
</Border.Background>
<ScrollViewer HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="10"
IsHitTestVisible="False">
<StackPanel.Resources>
<Style TargetType="{x:Type TextBlock}"
BasedOn="{StaticResource {x:Type TextBlock}}">
<Setter Property="Margin"
Value="0 0 0 5" />
</Style>
</StackPanel.Resources>
<StackPanel Margin="0 0 0 20">
<TextBlock Text="(New) Drag and drop nodes from the toolbox"
TextWrapping="Wrap"
Foreground="{DynamicResource NodeInput.BorderBrush}"
FontWeight="Bold"/>
</StackPanel>
<TextBlock TextWrapping="Wrap">
<Run Foreground="Red"
FontWeight="Bold">CTRL + N/W</Run>
<Run>: open/close editor</Run>
</TextBlock>
<TextBlock TextWrapping="Wrap">
<Run Foreground="Red"
FontWeight="Bold">CTRL + R</Run>
<Run>: Run Flow</Run>
</TextBlock>
<TextBlock TextWrapping="Wrap">
<Run Foreground="Red"
FontWeight="Bold">CTRL + S</Run>
<Run>: Save Flow</Run>
</TextBlock>
<TextBlock TextWrapping="Wrap">
<Run Foreground="Red"
FontWeight="Bold">CTRL + O</Run>
<Run>: Open Saved Flow</Run>
</TextBlock>
<TextBlock TextWrapping="Wrap">
<Run Foreground="Red"
FontWeight="Bold">ALT + Click</Run>
<Run>: disconnect connector</Run>
</TextBlock>
<TextBlock TextWrapping="Wrap">
<Run Foreground="Red"
FontWeight="Bold">Right Click</Run>
<Run>: show operations menu (create nodes)</Run>
</TextBlock>
<TextBlock TextWrapping="Wrap">
<Run Foreground="Red"
FontWeight="Bold">Delete</Run>
<Run>: delete selection</Run>
</TextBlock>
<TextBlock TextWrapping="Wrap">
<Run Foreground="Red"
FontWeight="Bold">CTRL + T</Run>
<Run>: change theme</Run>
</TextBlock>
<TextBlock TextWrapping="Wrap">
<Run Foreground="Red"
FontWeight="Bold">CTRL + G</Run>
<Run>: group selection (hold SHIFT and mouse drag the header to move the group node alone)</Run>
</TextBlock>
<TextBlock Text="Drag a connection and drop it on the editor"
TextWrapping="Wrap"
FontWeight="Bold" />
<TextBlock Text="Hover over a connector to see its value"
TextWrapping="Wrap"
FontWeight="Bold" />
<TextBlock Text="Create a Calculator node and double click it to open"
TextWrapping="Wrap"
FontWeight="Bold" />
<TextBlock Text="Create an Operation Graph and add operations to it"
TextWrapping="Wrap"
FontWeight="Bold" />
</StackPanel>
</ScrollViewer>
</Border>
</Expander>
</Grid>
</Window>

View File

@@ -0,0 +1,26 @@
using Nodify.Interactivity;
using System.Windows;
using System.Windows.Input;
namespace Nodify.Calculator
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
EditorGestures.Mappings.Editor.Cutting.Unbind();
EventManager.RegisterClassHandler(
typeof(UIElement),
Keyboard.PreviewGotKeyboardFocusEvent,
(KeyboardFocusChangedEventHandler)OnPreviewGotKeyboardFocus);
}
private void OnPreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
Title = e.NewFocus.ToString();
}
}
}

View File

@@ -0,0 +1,22 @@
using Microsoft.CodeAnalysis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
namespace Nodify.Calculator.Models
{
public class SaveGraphModel
{
public string Name { get; set; }
public List<SaveNodes> Nodes { get; set; } = new List<SaveNodes>();
}
public class SaveNodes
{
public Point Location { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
using System.Collections.Generic;
namespace Nodify.Calculator.Models
{
public class SwaggerNodeModel
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string OPType { get; set; } = string.Empty;
public List<string> InputNames { get; set; } = new List<string>();
public string SwaggerFileName { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace Nodify.Calculator.Models
{
public class VariableModel
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string VariableType { get; set; } = "string";
public string DefaultValue { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFrameworks>net9-windows;</TargetFrameworks>
<UseWPF>true</UseWPF>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Remove="leet swagger.txt" />
</ItemGroup>
<ItemGroup>
<Content Include="leet swagger.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="LiteDB" Version="5.0.21" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageReference Include="NSwag.CodeGeneration.CSharp" Version="14.5.0" />
<PackageReference Include="NSwag.Core" Version="14.5.0" />
<PackageReference Include="StringMath" Version="4.1.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Nodify\Nodify.csproj" />
<ProjectReference Include="..\Nodify.Shared\Nodify.Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,44 @@
using System.Windows;
namespace Nodify.Calculator
{
public class OperationGraphViewModel : CalculatorOperationViewModel
{
private Size _size;
public Size DesiredSize
{
get => _size;
set => SetProperty(ref _size, value);
}
private Size _prevSize;
private bool _isExpanded = true;
public bool IsExpanded
{
get => _isExpanded;
set
{
if (SetProperty(ref _isExpanded, value))
{
if (_isExpanded)
{
DesiredSize = _prevSize;
}
else
{
_prevSize = Size;
// Fit content
DesiredSize = new Size(double.NaN, double.NaN);
}
}
}
}
public OperationGraphViewModel()
{
InnerCalculator.Operations[0].Location = new Point(50, 50);
InnerCalculator.Operations[1].Location = new Point(200, 50);
}
}
}

View File

@@ -0,0 +1,14 @@
using System.Windows;
namespace Nodify.Calculator
{
public class OperationGroupViewModel : OperationViewModel
{
private Size _size;
public Size GroupSize
{
get => _size;
set => SetProperty(ref _size, value);
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
namespace Nodify.Calculator
{
public enum OperationType
{
Normal,
Expando,
Expression,
Calculator,
Group,
Graph,
API,
System
}
public class OperationInfoViewModel
{
public string? Title { get; set; }
public OperationType Type { get; set; }
public IOperation? Operation { get; set; }
public SystemOperations sysOp { get; set; }
public List<string?> Input { get; } = new List<string?>();
public List<string?> Output { get; } = new List<string?>();
public uint MinInput { get; set; }
public uint MaxInput { get; set; }
public string InputType { get; set; } = string.Empty;
public string OPType { get; set; }
public bool IsFlowNode { get; set; }
public bool IsModelNode { get; set; }
public string ClassName { get; set; }
public string VariableType { get; set; } = string.Empty;
public string DefaultValue { get; set; } = string.Empty;
public bool IsSimpleVariable { get; set; }
}
}

View File

@@ -0,0 +1,128 @@
using LiteDB;
using System;
using System.ComponentModel;
using System.Linq;
using System.Windows;
namespace Nodify.Calculator
{
public class OperationViewModel : ObservableObject
{
public OperationViewModel()
{
Input.WhenAdded(x =>
{
x.Operation = this;
x.IsInput = true;
x.PropertyChanged += OnInputValueChanged;
})
.WhenRemoved(x =>
{
x.PropertyChanged -= OnInputValueChanged;
});
Output.WhenAdded(c =>
{
c.Operation = this;
c.IsInput = false;
c.Value = 0;
c.PropertyChanged += OnInputValueChanged;
})
.WhenRemoved(x =>
{
x.PropertyChanged -= OnInputValueChanged;
});
}
private void OnInputValueChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ConnectorViewModel.Value))
{
OnInputValueChanged();
}
}
private Point _location;
public Point Location
{
get => _location;
set => SetProperty(ref _location, value);
}
private Size _size;
public Size Size
{
get => _size;
set => SetProperty(ref _size, value);
}
private string? _title;
public string? Title
{
get => _title;
set => SetProperty(ref _title, value);
}
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set => SetProperty(ref _isSelected, value);
}
public bool IsReadOnly { get; set; }
[BsonIgnore]
private IOperation? _operation;
[BsonIgnore]
public IOperation? Operation
{
get => _operation;
set => SetProperty(ref _operation, value)
.Then(OnInputValueChanged);
}
private string nodeId;
public string NodeId
{
get { return nodeId; }
set => SetProperty(ref nodeId, value);
}
[BsonIgnore]
public NodifyObservableCollection<ConnectorViewModel> Input { get; } = new NodifyObservableCollection<ConnectorViewModel>();
[BsonIgnore]
public NodifyObservableCollection<ConnectorViewModel> Output { get; } = new NodifyObservableCollection<ConnectorViewModel>();
//private ConnectorViewModel? _output;
//public ConnectorViewModel? Output
//{
// get => _output;
// set
// {
// if (SetProperty(ref _output, value) && _output != null)
// {
// _output.Operation = this;
// }
// }
//}
protected virtual void OnInputValueChanged()
{
//if (Output != null && Operation != null)
//{
// try
// {
// var input = Input.Select(i => i.Value).ToArray();
// Output.Value = Operation?.Execute(input) ?? 0;
// }
// catch
// {
// }
//}
}
}
}

View File

@@ -0,0 +1,14 @@
using System;
namespace Nodify.Calculator
{
public class BinaryOperation : IOperation
{
private readonly Func<double, double, double> _func;
public BinaryOperation(Func<double, double, double> func) => _func = func;
public double Execute(params double[] operands)
=> _func.Invoke(operands[0], operands[1]);
}
}

View File

@@ -0,0 +1,7 @@
namespace Nodify.Calculator
{
public interface IOperation
{
double Execute(params double[] operands);
}
}

View File

@@ -0,0 +1,469 @@
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Windows;
using System.Windows.Shapes;
using Size = System.Windows.Size;
namespace Nodify.Calculator
{
public static class OperationFactory
{
public static List<OperationInfoViewModel> GetSystemNodes()
{
List<OperationInfoViewModel> systemNodes = new List<OperationInfoViewModel>();
var copynode = new OperationInfoViewModel()
{
Title = "COPY",
Type = OperationType.System,
OPType = "copy",
IsFlowNode = false,
};
copynode.Input.Add("");
copynode.Output.Add("");
copynode.Output.Add("");
var begin = new OperationInfoViewModel()
{
Title = "Begin",
Type = OperationType.System,
sysOp = SystemOperations.BEGIN,
IsFlowNode = true
};
begin.Output.Add("");
var ending = new OperationInfoViewModel()
{
Title = "End",
Type = OperationType.System,
sysOp = SystemOperations.END,
IsFlowNode = true
};
ending.Input.Add("");
var debugAndCreateModels = new OperationInfoViewModel()
{
Title = "Debug & Create Model",
Type = OperationType.System,
sysOp = SystemOperations.DEBUG_AND_CREATE_MODEL,
IsFlowNode = true
};
debugAndCreateModels.Input.Add("");
debugAndCreateModels.Output.Add("");
var jsonParseNode = new OperationInfoViewModel()
{
Title = "Parse Json",
Type = OperationType.System,
sysOp = SystemOperations.PARSEJSON,
IsFlowNode = false
};
jsonParseNode.Input.Add("");
jsonParseNode.Output.Add("");
var splitNode = new OperationInfoViewModel()
{
Title = "Split",
Type = OperationType.System,
sysOp = SystemOperations.SPLIT,
IsFlowNode = false
};
splitNode.Input.Add("");
splitNode.Output.Add("");
var takeNode = new OperationInfoViewModel()
{
Title = "TAKE",
Type = OperationType.System,
sysOp = SystemOperations.TAKE,
IsFlowNode = false
};
takeNode.Input.Add("");
takeNode.Output.Add("");
var authNode = new OperationInfoViewModel()
{
Title = "Auth",
Type = OperationType.System,
sysOp = SystemOperations.AUTH,
IsFlowNode = true
};
authNode.Input.Add("Base URL");
authNode.Input.Add("Auth Type");
authNode.Input.Add("Token");
authNode.Input.Add("API Key");
authNode.Input.Add("Username");
authNode.Input.Add("Password");
systemNodes.Add(authNode);
systemNodes.Add(copynode);
systemNodes.Add(begin);
systemNodes.Add(ending);
systemNodes.Add(debugAndCreateModels);
systemNodes.Add(jsonParseNode);
systemNodes.Add(splitNode);
systemNodes.Add(takeNode);
return systemNodes;
}
public static List<OperationInfoViewModel> GetOperationsInfo(Type container)
{
List<OperationInfoViewModel> result = new List<OperationInfoViewModel>();
foreach (var method in container.GetMethods())
{
if (method.IsStatic)
{
OperationInfoViewModel op = new OperationInfoViewModel
{
Title = method.Name
};
var attr = method.GetCustomAttribute<OperationAttribute>();
var para = method.GetParameters();
bool generateInputNames = true;
op.Type = OperationType.Normal;
if (para.Length == 2)
{
var delType = typeof(Func<double, double, double>);
var del = (Func<double, double, double>)Delegate.CreateDelegate(delType, method);
op.Operation = new BinaryOperation(del);
}
else if (para.Length == 1)
{
if (para[0].ParameterType.IsArray)
{
op.Type = OperationType.Expando;
var delType = typeof(Func<double[], double>);
var del = (Func<double[], double>)Delegate.CreateDelegate(delType, method);
op.Operation = new ParamsOperation(del);
op.MaxInput = int.MaxValue;
}
else
{
var delType = typeof(Func<double, double>);
var del = (Func<double, double>)Delegate.CreateDelegate(delType, method);
op.Operation = new UnaryOperation(del);
}
}
else if (para.Length == 0)
{
var delType = typeof(Func<double>);
var del = (Func<double>)Delegate.CreateDelegate(delType, method);
op.Operation = new ValueOperation(del);
}
if (attr != null)
{
op.MinInput = attr.MinInput;
op.MaxInput = attr.MaxInput;
generateInputNames = attr.GenerateInputNames;
}
else
{
op.MinInput = (uint)para.Length;
op.MaxInput = (uint)para.Length;
}
foreach (var param in para)
{
op.Input.Add(generateInputNames ? param.Name : null);
}
for (int i = op.Input.Count; i < op.MinInput; i++)
{
op.Input.Add(null);
}
op.Output.Add("");
result.Add(op);
}
}
return result;
}
public static OperationViewModel GetOperation(OperationInfoViewModel info)
{
var input = info.Input.Select(i => new ConnectorViewModel
{
Title = i
});
switch (info.Type)
{
case OperationType.Expression:
var eo = new ExpressionOperationViewModel
{
Title = info.Title,
Operation = info.Operation,
Expression = "1 + sin {a} + cos {b}"
};
eo.Output.Add(new ConnectorViewModel());
return eo;
case OperationType.Calculator:
return new CalculatorOperationViewModel
{
Title = info.Title,
Operation = info.Operation,
};
case OperationType.Expando:
var o = new ExpandoOperationViewModel
{
MaxInput = info.MaxInput,
MinInput = info.MinInput,
Title = info.Title,
Operation = info.Operation
};
o.Output.Add(new ConnectorViewModel());
o.Input.AddRange(input);
return o;
case OperationType.Group:
return new OperationGroupViewModel
{
Title = info.Title,
};
case OperationType.Graph:
return new OperationGraphViewModel
{
Title = info.Title,
DesiredSize = new Size(420, 250)
};
case OperationType.API:
var _o = new APIOperationViewModel
{
Title = info.Title,
OperationType = info.OPType.ToUpper()
};
var connectorViewModel = new ConnectorViewModel()
{
Title = "",
Shape = ConnectorShape.Triangle
};
var connectorViewModel2 = new ConnectorViewModel()
{
Title = "",
Shape = ConnectorShape.Triangle,
IsInput = false
};
_o.Output.Add(connectorViewModel2);
_o.Output.Add(new ConnectorViewModel());
_o.Input.Add(connectorViewModel);
foreach (var item in input)
{
item.ConnectorColor = Color.GreenYellow;
_o.Input.Add(item);
}
//_o.Input.AddRange(input);
return _o;
case OperationType.System:
if (info.sysOp == SystemOperations.AUTH)
{
var authOp = new AuthOperationViewModel
{
Title = info.Title,
SystemOperationType = SystemOperations.AUTH
};
// Add flow connectors (triangle)
var flowIn = new ConnectorViewModel()
{
Title = "",
Shape = ConnectorShape.Triangle
};
var flowOut = new ConnectorViewModel()
{
Title = "",
Shape = ConnectorShape.Triangle,
IsInput = false
};
authOp.Input.Add(flowIn);
authOp.Output.Add(flowOut);
// Add data input connectors
foreach (var inp in input)
{
inp.ConnectorColor = System.Drawing.Color.Orange;
authOp.Input.Add(inp);
}
return authOp;
}
var sysOp = new SystemOperationViewModel
{
Title = info.Title,
SystemOperationType = info.sysOp
};
if (info.sysOp == SystemOperations.GET_SET && info.IsModelNode)
{
if (info.Title == "GET")
{
info.Output.Add("");
info.IsFlowNode = false;
}
else if (info.Title == "SET")
{
info.Input.Add("");
var customModelDir = "CustomModels";
Directory.CreateDirectory(customModelDir);
var flpath = System.IO.Path.Combine(customModelDir, info.ClassName + ".cs");
if (File.Exists(flpath))
{
//Read all the properties from the file
var fileContent = File.ReadAllText(flpath);
// Parse and analyze properties
var properties = GetPropertiesFromClass(fileContent);
// Print out the extracted properties and their types
Console.WriteLine("Properties found:");
foreach (var property in properties)
{
Console.WriteLine($"Property Name: {property.Name}, Type: {property.Type}");
info.Output.Add(property.Name);
}
}
info.IsFlowNode = true;
}
var flTitle = $"{info.Title} {info.ClassName}";
sysOp.Title = flTitle;
}
if (info.sysOp == SystemOperations.GET_SET && info.IsSimpleVariable)
{
var varLabel = $"{info.Title} {info.ClassName} ({info.VariableType})";
sysOp.Title = varLabel;
if (info.Title == "GET")
{
// GET variable: output the value, no flow needed
info.Output.Add("Value");
info.IsFlowNode = false;
}
else if (info.Title == "SET")
{
// SET variable: input connector for the value, flow node
info.Input.Add("Value");
info.IsFlowNode = true;
}
}
if (info.sysOp != SystemOperations.BEGIN &&
info.sysOp != SystemOperations.END &&
info.IsFlowNode)
{
var flowinputnode = new ConnectorViewModel()
{
Title = "",
Shape = ConnectorShape.Triangle
};
var flowoutputnode = new ConnectorViewModel()
{
Title = "",
Shape = ConnectorShape.Triangle,
IsInput = false
};
sysOp.Input.Add(flowinputnode);
sysOp.Output.Add(flowoutputnode);
}
foreach (var item in info.Output)
{
var out1 = new ConnectorViewModel()
{
Title = string.IsNullOrEmpty(item) ? "" : item,
IsInput = false,
ConnectorColor = Color.DarkRed,
Shape = (info.sysOp == SystemOperations.BEGIN || info.sysOp == SystemOperations.END) ? ConnectorShape.Triangle : ConnectorShape.Circle,
};
if (out1.Shape != ConnectorShape.Triangle)
{
out1.ConnectorColor = Color.DeepPink;
}
sysOp.Output.Add(out1);
}
foreach (var item in input)
{
item.Shape = (info.sysOp == SystemOperations.BEGIN || info.sysOp == SystemOperations.END) ? ConnectorShape.Triangle : ConnectorShape.Circle;
sysOp.Input.Add(item);
}
return sysOp;
default:
{
var op = new OperationViewModel
{
Title = info.Title,
Operation = info.Operation,
};
var ccv = new ConnectorViewModel()
{
IsInput = false,
Shape = ConnectorShape.Circle,
Title = ""
};
op.Output.Add(ccv);
op.Input.AddRange(input);
return op;
}
}
}
static List<CustomProperty> GetPropertiesFromClass(string classContent)
{
// Parse the C# class content using Roslyn
var syntaxTree = CSharpSyntaxTree.ParseText(classContent);
var root = syntaxTree.GetRoot();
// Find the first class declaration (you can refine this if there are multiple classes)
var classDeclaration = root.DescendantNodes().OfType<ClassDeclarationSyntax>().FirstOrDefault();
// List to store extracted properties
var properties = new List<CustomProperty>();
if (classDeclaration != null)
{
// Look for property declarations inside the class
var propertyDeclarations = classDeclaration.Members.OfType<PropertyDeclarationSyntax>();
foreach (var property in propertyDeclarations)
{
var propertyName = property.Identifier.Text; // Property name
var propertyType = property.Type.ToString(); // Property type
// Add the property and its type to the list
properties.Add(new CustomProperty
{
Name = propertyName,
Type = propertyType
});
}
}
return properties;
}
}
}
class CustomProperty // <- Renamed to avoid conflicts with System.Reflection.PropertyInfo
{
public string Name { get; set; }
public string Type { get; set; }
}

View File

@@ -0,0 +1,35 @@
using System;
using System.Linq;
namespace Nodify.Calculator
{
public static class OperationsContainer
{
[Operation(MinInput = 2, MaxInput = 10, GenerateInputNames = false)]
public static double Add(params double[] operands)
=> operands.Sum();
[Operation(MinInput = 2, MaxInput = 10, GenerateInputNames = false)]
public static double Multiply(params double[] operands)
=> operands.Aggregate((x, y) => x * y);
public static double Divide(double a, double b)
=> a / b;
public static double Subtract(double a, double b)
=> a - b;
public static double Pow(double value, double exp)
=> (double)Math.Pow((double)value, (double)exp);
public static double PI()
=> (double)Math.PI;
}
public sealed class OperationAttribute : Attribute
{
public uint MaxInput { get; set; }
public uint MinInput { get; set; }
public bool GenerateInputNames { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
using System;
namespace Nodify.Calculator
{
public class ParamsOperation : IOperation
{
private readonly Func<double[], double> _func;
public ParamsOperation(Func<double[], double> func) => _func = func;
public double Execute(params double[] operands)
=> _func.Invoke(operands);
}
}

View File

@@ -0,0 +1,14 @@
using System;
namespace Nodify.Calculator
{
public class UnaryOperation : IOperation
{
private readonly Func<double, double> _func;
public UnaryOperation(Func<double, double> func) => _func = func;
public double Execute(params double[] operands)
=> _func.Invoke(operands[0]);
}
}

View File

@@ -0,0 +1,14 @@
using System;
namespace Nodify.Calculator
{
public class ValueOperation : IOperation
{
private readonly Func<double> _func;
public ValueOperation(Func<double> func) => _func = func;
public double Execute(params double[] operands)
=> _func();
}
}

View File

@@ -0,0 +1,50 @@
using System.Collections.Generic;
using System.Windows;
namespace Nodify.Calculator
{
public static class OperationsExtensions
{
public static Rect GetBoundingBox(this IEnumerable<OperationViewModel> nodes, double padding = 0, int gridCellSize = 15)
{
var minX = double.MaxValue;
var minY = double.MaxValue;
var maxX = double.MinValue;
var maxY = double.MinValue;
const int width = 200; //node.Width
const int height = 100; //node.Height
foreach (var node in nodes)
{
if (node.Location.X < minX)
{
minX = node.Location.X;
}
if (node.Location.Y < minY)
{
minY = node.Location.Y;
}
var sizeX = node.Location.X + width;
if (sizeX > maxX)
{
maxX = sizeX;
}
var sizeY = node.Location.Y + height;
if (sizeY > maxY)
{
maxY = sizeY;
}
}
var result = new Rect(minX - padding, minY - padding, maxX - minX + padding * 2, maxY - minY + padding * 2);
result.X = (int)result.X / gridCellSize * gridCellSize;
result.Y = (int)result.Y / gridCellSize * gridCellSize;
return result;
}
}
}

View File

@@ -0,0 +1,81 @@
<UserControl x:Class="Nodify.Calculator.OperationsMenuView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Nodify.Calculator"
xmlns:shared="clr-namespace:Nodify;assembly=Nodify.Shared"
mc:Ignorable="d"
MinWidth="250"
d:DesignHeight="400"
d:DesignWidth="250"
d:DataContext="{d:DesignInstance local:OperationsMenuViewModel}"
Visibility="{Binding IsVisible, Converter={shared:BooleanToVisibilityConverter}}">
<UserControl.Resources>
<Style TargetType="{x:Type TextBlock}"
BasedOn="{StaticResource {x:Type TextBlock}}">
<Setter Property="Foreground"
Value="{DynamicResource ForegroundBrush}" />
</Style>
</UserControl.Resources>
<Border Padding="7"
CornerRadius="3"
Background="{DynamicResource Node.BackgroundBrush}"
BorderBrush="{StaticResource NodifyEditor.SelectionRectangleStrokeBrush}"
BorderThickness="2">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ItemsControl Grid.Row="1"
x:Name="OperationsList"
Focusable="True"
KeyboardNavigation.TabNavigation="Cycle"
ItemsSource="{Binding MenuAvailableOperations}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:OperationInfoViewModel}">
<Button Content="{Binding Title}"
Command="{Binding DataContext.CreateOperationCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
ClickMode="Press"
Background="Transparent"
BorderBrush="Transparent"
Foreground="{DynamicResource ForegroundBrush}"
Padding="3"
Cursor="Hand"
HorizontalContentAlignment="Left">
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="FocusVisualStyle"
Value="{StaticResource {x:Static SystemParameters.FocusVisualStyleKey}}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Name="Border"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}">
<ContentPresenter />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver"
Value="True">
<Setter Property="Background"
TargetName="Border"
Value="{DynamicResource NodeInput.BorderBrush}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Button.Style>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Border>
</UserControl>

View File

@@ -0,0 +1,51 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace Nodify.Calculator
{
public partial class OperationsMenuView : UserControl
{
private readonly WeakReference<UIElement?> _focusToRestore = new WeakReference<UIElement?>(null!);
public OperationsMenuView()
{
InitializeComponent();
IsVisibleChanged += OperationsMenuView_IsVisibleChanged;
}
private void OperationsMenuView_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (!IsLoaded)
{
return;
}
if (e.NewValue is true)
{
_focusToRestore.SetTarget(Keyboard.FocusedElement as UIElement);
Dispatcher.BeginInvoke(new Action(() => OperationsList.Focus()), System.Windows.Threading.DispatcherPriority.Input);
}
else if (e.NewValue is false)
{
if (_focusToRestore.TryGetTarget(out var elementToFocus))
{
Dispatcher.BeginInvoke(new Action(() =>
{
elementToFocus!.Focus();
}), System.Windows.Threading.DispatcherPriority.Input);
}
}
}
protected override void OnKeyDown(KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
SetCurrentValue(VisibilityProperty, Visibility.Collapsed);
}
}
}
}

View File

@@ -0,0 +1,380 @@
using LiteDB;
using Microsoft.Win32;
using NSwag;
using NSwag.CodeGeneration.CSharp;
using Nodify.Calculator.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Windows;
namespace Nodify.Calculator
{
public class OperationsMenuViewModel : ObservableObject
{
private bool _isVisible;
public bool IsVisible
{
get => _isVisible;
set
{
SetProperty(ref _isVisible, value);
if (!value)
{
Closed?.Invoke();
}
}
}
private Point _location;
public Point Location
{
get => _location;
set => SetProperty(ref _location, value);
}
public event Action? Closed;
public void OpenAt(Point targetLocation)
{
MenuAvailableOperations.Clear();
MenuAvailableOperations.AddRange(AvailableOperations);
Close();
Location = targetLocation;
IsVisible = true;
}
public void OpenOnlyGetSetVariable(Point targetLocation, string className)
{
var getVM = new OperationInfoViewModel()
{
Title = "GET",
Type = OperationType.System,
sysOp = SystemOperations.GET_SET,
IsModelNode = true,
ClassName = className,
};
var setVM = new OperationInfoViewModel()
{
Title = "SET",
Type = OperationType.System,
sysOp = SystemOperations.GET_SET,
IsModelNode = true,
ClassName = className
};
MenuAvailableOperations.Clear();
MenuAvailableOperations.Add(getVM);
MenuAvailableOperations.Add(setVM);
Close();
Location = targetLocation;
IsVisible = true;
}
public void OpenGetSetForVariable(Point targetLocation, OperationInfoViewModel variableInfo)
{
var getVM = new OperationInfoViewModel()
{
Title = "GET",
Type = OperationType.System,
sysOp = SystemOperations.GET_SET,
IsSimpleVariable = true,
VariableType = variableInfo.VariableType,
DefaultValue = variableInfo.DefaultValue,
ClassName = variableInfo.Title ?? string.Empty
};
var setVM = new OperationInfoViewModel()
{
Title = "SET",
Type = OperationType.System,
sysOp = SystemOperations.GET_SET,
IsSimpleVariable = true,
VariableType = variableInfo.VariableType,
DefaultValue = variableInfo.DefaultValue,
ClassName = variableInfo.Title ?? string.Empty
};
MenuAvailableOperations.Clear();
MenuAvailableOperations.Add(getVM);
MenuAvailableOperations.Add(setVM);
Close();
Location = targetLocation;
IsVisible = true;
}
public void Close()
{
IsVisible = false;
}
public NodifyObservableCollection<OperationInfoViewModel> MenuAvailableOperations { get; }
public NodifyObservableCollection<OperationInfoViewModel> AvailableOperations { get; }
public NodifyObservableCollection<OperationInfoViewModel> SwaggerOperations { get; }
public NodifyObservableCollection<OperationInfoViewModel> AvailableModels { get; }
public NodifyObservableCollection<OperationInfoViewModel> AvailableVariables { get; }
public INodifyCommand CreateOperationCommand { get; }
[Newtonsoft.Json.JsonIgnore]
[BsonIgnore]
private readonly CalculatorViewModel _calculator;
public OperationsMenuViewModel(CalculatorViewModel calculator)
{
_calculator = calculator;
List<OperationInfoViewModel> operations = new List<OperationInfoViewModel>();
AvailableModels = new NodifyObservableCollection<OperationInfoViewModel>();
AvailableVariables = new NodifyObservableCollection<OperationInfoViewModel>();
LoadVariablesFromDb();
var customModelDir = "CustomModels";
Directory.CreateDirectory(customModelDir);
var dirInfo = new DirectoryInfo(customModelDir);
var allFiles = dirInfo.GetFiles("*.cs");
foreach (var item in allFiles)
{
var flName = Path.GetFileNameWithoutExtension(item.Name);
var opVInfo = new OperationInfoViewModel()
{
Title = flName,
IsModelNode = true,
Type = OperationType.System,
sysOp = SystemOperations.GET_SET,
ClassName = flName
};
AvailableModels.Add(opVInfo);
}
operations.AddRange(OperationFactory.GetSystemNodes());
operations.AddRange(OperationFactory.GetOperationsInfo(typeof(OperationsContainer)));
SwaggerOperations = new NodifyObservableCollection<OperationInfoViewModel>();
LoadSwaggerNodesFromDb();
operations.AddRange(SwaggerOperations);
AvailableOperations = new NodifyObservableCollection<OperationInfoViewModel>(operations);
MenuAvailableOperations = new NodifyObservableCollection<OperationInfoViewModel>(operations);
CreateOperationCommand = new DelegateCommand<OperationInfoViewModel>(CreateOperation);
ImportSwaggerCommand = new DelegateCommand(ImportSwagger);
AddVariableCommand = new DelegateCommand(AddVariable);
}
public void AddNewModel(OperationInfoViewModel opModel)
{
if (System.Windows.Application.Current.Dispatcher.CheckAccess())
{
// Add directly if on UI thread
AvailableModels.Add(opModel);
}
else
{
// Otherwise, marshal the call to the UI thread
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
AvailableModels.Add(opModel);
});
}
}
public INodifyCommand ImportSwaggerCommand { get; }
public INodifyCommand AddVariableCommand { get; }
private void AddVariable()
{
var dialog = new AddVariableDialog();
dialog.Owner = System.Windows.Application.Current.MainWindow;
if (dialog.ShowDialog() != true)
return;
var varName = dialog.VariableName;
var varType = dialog.VariableType;
var defaultVal = dialog.DefaultValue;
// Check for duplicate name
if (AvailableVariables.Any(v => v.Title == varName))
{
MessageBox.Show($"A variable named '{varName}' already exists.", "Duplicate Variable", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var varInfo = CreateVariableInfo(varName, varType, defaultVal);
AvailableVariables.Add(varInfo);
SaveVariableToDb(varName, varType, defaultVal);
}
private OperationInfoViewModel CreateVariableInfo(string name, string varType, string defaultValue)
{
return new OperationInfoViewModel
{
Title = name,
Type = OperationType.System,
sysOp = SystemOperations.GET_SET,
IsSimpleVariable = true,
VariableType = varType,
DefaultValue = defaultValue,
IsModelNode = false
};
}
private void SaveVariableToDb(string name, string varType, string defaultValue)
{
using var db = new LiteDbHelper<VariableModel>("Variables");
db.Insert(new VariableModel
{
Name = name,
VariableType = varType,
DefaultValue = defaultValue
});
}
private void LoadVariablesFromDb()
{
try
{
using var db = new LiteDbHelper<VariableModel>("Variables");
foreach (var v in db.FindAll())
{
AvailableVariables.Add(CreateVariableInfo(v.Name, v.VariableType, v.DefaultValue));
}
}
catch
{
// DB may not exist yet
}
}
private void ImportSwagger()
{
var openFileDialog = new OpenFileDialog
{
Filter = "JSON files (*.json)|*.json|Text files (*.txt)|*.txt|All files (*.*)|*.*",
Title = "Import Swagger JSON File"
};
if (openFileDialog.ShowDialog() != true)
return;
try
{
var nodes = ParseSwaggerFile(openFileDialog.FileName);
var fileName = Path.GetFileName(openFileDialog.FileName);
SaveSwaggerNodesToDb(nodes, fileName);
foreach (var node in nodes)
{
SwaggerOperations.Add(node);
AvailableOperations.Add(node);
MenuAvailableOperations.Add(node);
}
MessageBox.Show($"Successfully imported {nodes.Count} API endpoints from Swagger.", "Import Swagger", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
MessageBox.Show($"Failed to parse Swagger file: {ex.Message}", "Import Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private List<OperationInfoViewModel> ParseSwaggerFile(string jsonFilePath)
{
var operations = new List<OperationInfoViewModel>();
var openApiDocument = OpenApiDocument.FromFileAsync(jsonFilePath).Result;
foreach (var path in openApiDocument.Paths)
{
foreach (var method in path.Value)
{
var ovmodel = new OperationInfoViewModel
{
Title = path.Key,
OPType = method.Key,
Type = OperationType.API
};
var addedParams = new HashSet<string>();
foreach (var parameter in method.Value.Parameters)
{
if (addedParams.Add(parameter.Name))
{
ovmodel.Input.Add(parameter.Name);
}
}
ovmodel.Output.Add("");
operations.Add(ovmodel);
}
}
return operations;
}
private void SaveSwaggerNodesToDb(List<OperationInfoViewModel> nodes, string swaggerFileName)
{
using var db = new LiteDbHelper<SwaggerNodeModel>("SwaggerNodes");
db.DeleteMany(n => n.SwaggerFileName == swaggerFileName);
foreach (var node in nodes)
{
db.Insert(new SwaggerNodeModel
{
Title = node.Title ?? string.Empty,
OPType = node.OPType ?? string.Empty,
InputNames = new List<string>(node.Input),
SwaggerFileName = swaggerFileName
});
}
}
private void LoadSwaggerNodesFromDb()
{
try
{
using var db = new LiteDbHelper<SwaggerNodeModel>("SwaggerNodes");
var savedNodes = db.FindAll();
foreach (var saved in savedNodes)
{
var ovmodel = new OperationInfoViewModel
{
Title = saved.Title,
OPType = saved.OPType,
Type = OperationType.API
};
foreach (var inputName in saved.InputNames)
{
ovmodel.Input.Add(inputName);
}
ovmodel.Output.Add("");
SwaggerOperations.Add(ovmodel);
}
}
catch
{
// DB may not exist yet on first run
}
}
private void CreateOperation(OperationInfoViewModel operationInfo)
{
OperationViewModel op = OperationFactory.GetOperation(operationInfo);
op.Location = Location;
_calculator.Operations.Add(op);
var pending = _calculator.PendingConnection;
if (pending.IsVisible)
{
var connector = pending.Source.IsInput ? op.Output.FirstOrDefault() : op.Input.FirstOrDefault();
if (connector != null && _calculator.CanCreateConnection(pending.Source, connector))
{
_calculator.CreateConnection(pending.Source, connector);
}
}
Close();
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Windows;
namespace Nodify.Calculator
{
public class PendingConnectionViewModel : ObservableObject
{
private ConnectorViewModel _source = default!;
public ConnectorViewModel Source
{
get => _source;
set => SetProperty(ref _source, value);
}
private ConnectorViewModel? _target;
public ConnectorViewModel? Target
{
get => _target;
set => SetProperty(ref _target, value);
}
private bool _isVisible;
public bool IsVisible
{
get => _isVisible;
set => SetProperty(ref _isVisible, value);
}
private Point _targetLocation;
public Point TargetLocation
{
get => _targetLocation;
set => SetProperty(ref _targetLocation, value);
}
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Nodify.Calculator
{
public enum SystemOperations
{
COPY,
IF,
TAKE,
BEGIN,
END,
DEBUG_AND_CREATE_MODEL,
GET_SET,
PARSEJSON,
SPLIT,
AUTH
}
public class SystemOperationViewModel : OperationViewModel
{
private SystemOperations _systemOperation;
public SystemOperations SystemOperationType
{
get => _systemOperation;
set => SetProperty(ref _systemOperation, value);
}
}
}

View File

@@ -0,0 +1,189 @@
{
"openapi": "3.0.1",
"info": {
"title": "LeetU",
"version": "1.0"
},
"paths": {
"/course": {
"get": {
"tags": [
"Course"
],
"responses": {
"200": {
"description": "Success"
}
}
},
"post": {
"tags": [
"Course"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Course"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/Course"
}
},
"application/*+json": {
"schema": {
"$ref": "#/components/schemas/Course"
}
}
}
},
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/course/{courseId}": {
"get": {
"tags": [
"Course"
],
"parameters": [
{
"name": "courseId",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
}
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/student/{studentId}": {
"get": {
"tags": [
"Student"
],
"parameters": [
{
"name": "studentId",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
}
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/student": {
"get": {
"tags": [
"Student"
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/student/{studentId}/course": {
"get": {
"tags": [
"Student"
],
"parameters": [
{
"name": "studentId",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
}
],
"responses": {
"200": {
"description": "Success"
}
}
}
},
"/student/{studentId}/course/{courseId}": {
"post": {
"tags": [
"Student"
],
"parameters": [
{
"name": "studentId",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
},
{
"name": "courseId",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int64"
}
}
],
"responses": {
"200": {
"description": "Success"
}
}
}
}
},
"components": {
"schemas": {
"Course": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string",
"nullable": true
},
"description": {
"type": "string",
"nullable": true
},
"startDate": {
"type": "string",
"format": "date-time"
}
},
"additionalProperties": false
}
}
}
}