Compare commits
10 Commits
9c3241cea4
...
62e1b5b0f4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62e1b5b0f4 | ||
|
|
7288bdd6f2 | ||
|
|
dcedbfdc18 | ||
|
|
813c78eb40 | ||
|
|
1b0ad452ca | ||
|
|
a42e71af5e | ||
|
|
e2c43af907 | ||
|
|
ec620bf30d | ||
|
|
2d8da30eac | ||
|
|
1eb917b450 |
BIN
APIVisualExecutor-master_2.7z
Normal file
BIN
APIVisualExecutor-master_2.7z
Normal file
Binary file not shown.
@@ -4,6 +4,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Shapes;
|
using System.Windows.Shapes;
|
||||||
@@ -25,6 +26,8 @@ namespace Nodify.Calculator
|
|||||||
_ => Editors.Count > 0 && SelectedEditor != null);
|
_ => Editors.Count > 0 && SelectedEditor != null);
|
||||||
RunFlowCommand = new DelegateCommand(() =>
|
RunFlowCommand = new DelegateCommand(() =>
|
||||||
{
|
{
|
||||||
|
if (LogPanel.IsRunning) return;
|
||||||
|
|
||||||
Executor ex = new Executor(Editors.First());
|
Executor ex = new Executor(Editors.First());
|
||||||
var result = ex.PerformPreCheck();
|
var result = ex.PerformPreCheck();
|
||||||
if (!string.IsNullOrEmpty(result))
|
if (!string.IsNullOrEmpty(result))
|
||||||
@@ -33,51 +36,45 @@ namespace Nodify.Calculator
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
FlowRunner runner = new FlowRunner(ex);
|
LogPanel.LogEntries.Clear();
|
||||||
runner.ShowDialog();
|
LogPanel.IsOpen = true;
|
||||||
|
LogPanel.IsRunning = true;
|
||||||
|
ex.OnLogMe += LogPanel.WriteLog;
|
||||||
|
Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ex.Execute();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Application.Current?.Dispatcher.Invoke(() => LogPanel.IsRunning = false);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
SaveFileCommand = new DelegateCommand(() =>
|
SaveFileCommand = new DelegateCommand(() =>
|
||||||
{
|
{
|
||||||
var firstEditor = Editors.First();
|
try
|
||||||
var allNodes = firstEditor.Calculator.Operations;
|
|
||||||
|
|
||||||
SaveGraphModel svm = new SaveGraphModel();
|
|
||||||
foreach (var item in allNodes)
|
|
||||||
{
|
{
|
||||||
SaveNodes svn = new SaveNodes()
|
var firstEditor = Editors.First();
|
||||||
{
|
GraphSerializer.Save(firstEditor.Calculator);
|
||||||
Location = item.Location,
|
|
||||||
};
|
|
||||||
svm.Nodes.Add(svn);
|
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
|
||||||
var jsonEditors = JsonConvert.SerializeObject(svm, new JsonSerializerSettings
|
|
||||||
{
|
{
|
||||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
|
MessageBox.Show($"Failed to save: {ex.Message}", "Save Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
});
|
}
|
||||||
//var jsonEditors = JsonConvert.SerializeObject(Editors);
|
|
||||||
File.WriteAllText("SaveFile.AEXN", jsonEditors);
|
|
||||||
});
|
});
|
||||||
OpenFileCommand = new DelegateCommand(() =>
|
OpenFileCommand = new DelegateCommand(() =>
|
||||||
{
|
{
|
||||||
if (File.Exists("SaveFile.AEXN"))
|
try
|
||||||
{
|
{
|
||||||
var allText = File.ReadAllText("SaveFile.AEXN");
|
|
||||||
var edts = JsonConvert.DeserializeObject<SaveGraphModel>(allText);
|
|
||||||
var firstEditor = Editors.First();
|
var firstEditor = Editors.First();
|
||||||
var nodesToAdd = edts.Nodes;
|
GraphSerializer.Load(firstEditor.Calculator, firstEditor.Calculator.OperationsMenu);
|
||||||
foreach (var item in nodesToAdd)
|
}
|
||||||
{
|
catch (Exception ex)
|
||||||
firstEditor.Calculator.Operations.Add(new()
|
{
|
||||||
{
|
MessageBox.Show($"Failed to load: {ex.Message}", "Load Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
Location = item.Location,
|
|
||||||
NodeId = "H",
|
|
||||||
Title = "HHA"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Editors.WhenAdded((editor) =>
|
Editors.WhenAdded((editor) =>
|
||||||
@@ -98,6 +95,14 @@ namespace Nodify.Calculator
|
|||||||
{
|
{
|
||||||
Name = $"Editor {Editors.Count + 1}"
|
Name = $"Editor {Editors.Count + 1}"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Auto-load saved graph from project DB
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var firstEditor = Editors.First();
|
||||||
|
GraphSerializer.Load(firstEditor.Calculator, firstEditor.Calculator.OperationsMenu);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnOpenInnerCalculator(EditorViewModel parentEditor, CalculatorViewModel calculator)
|
private void OnOpenInnerCalculator(EditorViewModel parentEditor, CalculatorViewModel calculator)
|
||||||
@@ -138,5 +143,7 @@ namespace Nodify.Calculator
|
|||||||
get => _autoSelectNewEditor;
|
get => _autoSelectNewEditor;
|
||||||
set => SetProperty(ref _autoSelectNewEditor, value);
|
set => SetProperty(ref _autoSelectNewEditor, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LogPanelViewModel LogPanel { get; } = new LogPanelViewModel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ namespace Nodify.Calculator
|
|||||||
{
|
{
|
||||||
public class CalculatorViewModel : ObservableObject
|
public class CalculatorViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
|
public bool IsLoading { get; set; }
|
||||||
|
|
||||||
public CalculatorViewModel()
|
public CalculatorViewModel()
|
||||||
{
|
{
|
||||||
CreateConnectionCommand = new DelegateCommand<ConnectorViewModel>(
|
CreateConnectionCommand = new DelegateCommand<ConnectorViewModel>(
|
||||||
@@ -27,13 +29,16 @@ namespace Nodify.Calculator
|
|||||||
c.Output.ValueObservers.Add(c.Input);
|
c.Output.ValueObservers.Add(c.Input);
|
||||||
|
|
||||||
// Dynamic Split node: populate outputs when a model connects to the Square input
|
// Dynamic Split node: populate outputs when a model connects to the Square input
|
||||||
HandleSplitNodeConnected(c);
|
if (!IsLoading) HandleSplitNodeConnected(c);
|
||||||
|
|
||||||
|
// Dynamic New Object node: populate inputs when a model connects
|
||||||
|
if (!IsLoading) HandleNewObjectNodeConnected(c);
|
||||||
|
|
||||||
// Dynamic Copy node: adapt connectors when something connects
|
// Dynamic Copy node: adapt connectors when something connects
|
||||||
HandleCopyNodeConnected(c);
|
if (!IsLoading) HandleCopyNodeConnected(c);
|
||||||
|
|
||||||
// Dynamic Take node: adapt connectors when a list connects
|
// Dynamic Take node: adapt connectors when a list connects
|
||||||
HandleTakeNodeConnected(c);
|
if (!IsLoading) HandleTakeNodeConnected(c);
|
||||||
})
|
})
|
||||||
.WhenRemoved(c =>
|
.WhenRemoved(c =>
|
||||||
{
|
{
|
||||||
@@ -55,6 +60,9 @@ namespace Nodify.Calculator
|
|||||||
// Dynamic Split node: clear outputs when model disconnects
|
// Dynamic Split node: clear outputs when model disconnects
|
||||||
HandleSplitNodeDisconnected(c);
|
HandleSplitNodeDisconnected(c);
|
||||||
|
|
||||||
|
// Dynamic New Object node: clear inputs when model disconnects
|
||||||
|
HandleNewObjectNodeDisconnected(c);
|
||||||
|
|
||||||
// Dynamic Copy node: reset on disconnect
|
// Dynamic Copy node: reset on disconnect
|
||||||
HandleCopyNodeDisconnected(c);
|
HandleCopyNodeDisconnected(c);
|
||||||
|
|
||||||
@@ -65,7 +73,7 @@ namespace Nodify.Calculator
|
|||||||
Operations.WhenAdded(x =>
|
Operations.WhenAdded(x =>
|
||||||
{
|
{
|
||||||
x.Input.WhenRemoved(RemoveConnection);
|
x.Input.WhenRemoved(RemoveConnection);
|
||||||
x.NodeId = (Operations.Count + 1).ToString();
|
if (!IsLoading) x.NodeId = (Operations.Count + 1).ToString();
|
||||||
Debug.WriteLine($"Currently adding the node with node id : {x.NodeId} , Title : {x.Title}");
|
Debug.WriteLine($"Currently adding the node with node id : {x.NodeId} , Title : {x.Title}");
|
||||||
if (x is CalculatorInputOperationViewModel ci)
|
if (x is CalculatorInputOperationViewModel ci)
|
||||||
{
|
{
|
||||||
@@ -89,12 +97,12 @@ namespace Nodify.Calculator
|
|||||||
})
|
})
|
||||||
.WhenRemoved(x =>
|
.WhenRemoved(x =>
|
||||||
{
|
{
|
||||||
foreach (var input in x.Input)
|
foreach (var input in x.Input.ToList())
|
||||||
{
|
{
|
||||||
DisconnectConnector(input);
|
DisconnectConnector(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var item in x.Output)
|
foreach (var item in x.Output.ToList())
|
||||||
{
|
{
|
||||||
DisconnectConnector(item);
|
DisconnectConnector(item);
|
||||||
}
|
}
|
||||||
@@ -276,7 +284,7 @@ namespace Nodify.Calculator
|
|||||||
if (string.IsNullOrEmpty(className)) return;
|
if (string.IsNullOrEmpty(className)) return;
|
||||||
|
|
||||||
// Read model .cs file and parse properties
|
// Read model .cs file and parse properties
|
||||||
var customModelDir = "CustomModels";
|
var customModelDir = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels");
|
||||||
var filePath = Path.Combine(customModelDir, $"{className}.cs");
|
var filePath = Path.Combine(customModelDir, $"{className}.cs");
|
||||||
if (!File.Exists(filePath)) return;
|
if (!File.Exists(filePath)) return;
|
||||||
|
|
||||||
@@ -334,6 +342,118 @@ namespace Nodify.Calculator
|
|||||||
sysVm.Title = "Split";
|
sysVm.Title = "Split";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandleNewObjectNodeConnected(ConnectionViewModel c)
|
||||||
|
{
|
||||||
|
// Find the New Object node's Square input connector
|
||||||
|
var squareInput = c.Input.Shape == ConnectorShape.Square ? c.Input : null;
|
||||||
|
if (squareInput == null) squareInput = c.Output.Shape == ConnectorShape.Square ? c.Output : null;
|
||||||
|
if (squareInput == null) return;
|
||||||
|
|
||||||
|
var newObjOp = squareInput.Operation;
|
||||||
|
if (newObjOp is not SystemOperationViewModel sysVm || sysVm.SystemOperationType != SystemOperations.NEW_OBJECT)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// The other end is the source — find which model class it comes from
|
||||||
|
var sourceConnector = (c.Input == squareInput) ? c.Output : c.Input;
|
||||||
|
var sourceOp = sourceConnector.Operation;
|
||||||
|
|
||||||
|
string className = null;
|
||||||
|
if (sourceOp is SystemOperationViewModel srcSys)
|
||||||
|
{
|
||||||
|
var title = srcSys.Title ?? "";
|
||||||
|
if (title.StartsWith("GET ")) className = title.Substring(4).Trim();
|
||||||
|
else if (title.StartsWith("SET ")) className = title.Split(' ').ElementAtOrDefault(1);
|
||||||
|
}
|
||||||
|
else if (sourceOp is APIOperationViewModel apiVm)
|
||||||
|
{
|
||||||
|
className = apiVm.ResponseModelClassName;
|
||||||
|
if (!string.IsNullOrEmpty(className) && className.StartsWith("List<") && className.EndsWith(">"))
|
||||||
|
className = className.Substring(5, className.Length - 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(className) && !string.IsNullOrEmpty(sourceConnector.DataType))
|
||||||
|
{
|
||||||
|
var dt = sourceConnector.DataType;
|
||||||
|
if (dt.StartsWith("List<") && dt.EndsWith(">"))
|
||||||
|
dt = dt.Substring(5, dt.Length - 6);
|
||||||
|
if (!new[] { "string", "int", "double", "bool", "object" }.Contains(dt.ToLower()))
|
||||||
|
className = dt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(className)) return;
|
||||||
|
|
||||||
|
var customModelDir = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels");
|
||||||
|
var filePath = Path.Combine(customModelDir, $"{className}.cs");
|
||||||
|
if (!File.Exists(filePath)) return;
|
||||||
|
|
||||||
|
var fileContent = File.ReadAllText(filePath);
|
||||||
|
var properties = OperationFactory.GetPropertiesFromClassPublic(fileContent);
|
||||||
|
if (properties == null || properties.Count == 0) return;
|
||||||
|
|
||||||
|
// Remove existing dynamic inputs (non-triangle, non-square)
|
||||||
|
var toRemove = newObjOp.Input.Where(i => i.Shape != ConnectorShape.Triangle && i.Shape != ConnectorShape.Square).ToList();
|
||||||
|
toRemove.ForEach(i =>
|
||||||
|
{
|
||||||
|
DisconnectConnector(i);
|
||||||
|
newObjOp.Input.Remove(i);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add property inputs
|
||||||
|
foreach (var prop in properties)
|
||||||
|
{
|
||||||
|
var propColor = ConnectorViewModel.GetColorForType(prop.Type);
|
||||||
|
newObjOp.Input.Add(new ConnectorViewModel
|
||||||
|
{
|
||||||
|
Title = $"{prop.Name} ({prop.Type})",
|
||||||
|
Shape = ConnectorShape.Circle,
|
||||||
|
ConnectorColor = propColor,
|
||||||
|
DataType = prop.Type.ToLower()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the output connector to reflect the model type
|
||||||
|
var modelOutput = newObjOp.Output.FirstOrDefault(o => o.Shape == ConnectorShape.Square);
|
||||||
|
if (modelOutput != null)
|
||||||
|
{
|
||||||
|
modelOutput.Title = className;
|
||||||
|
modelOutput.DataType = className;
|
||||||
|
}
|
||||||
|
|
||||||
|
sysVm.Title = $"New {className}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleNewObjectNodeDisconnected(ConnectionViewModel c)
|
||||||
|
{
|
||||||
|
var squareInput = c.Input.Shape == ConnectorShape.Square ? c.Input : null;
|
||||||
|
if (squareInput == null) squareInput = c.Output.Shape == ConnectorShape.Square ? c.Output : null;
|
||||||
|
if (squareInput == null) return;
|
||||||
|
|
||||||
|
var newObjOp = squareInput.Operation;
|
||||||
|
if (newObjOp is not SystemOperationViewModel sysVm || sysVm.SystemOperationType != SystemOperations.NEW_OBJECT)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var stillConnected = Connections.Any(con => con.Input == squareInput || con.Output == squareInput);
|
||||||
|
if (stillConnected) return;
|
||||||
|
|
||||||
|
// Remove dynamic property inputs (keep triangle and square connectors)
|
||||||
|
var toRemove = newObjOp.Input.Where(i => i.Shape != ConnectorShape.Triangle && i.Shape != ConnectorShape.Square).ToList();
|
||||||
|
toRemove.ForEach(i =>
|
||||||
|
{
|
||||||
|
DisconnectConnector(i);
|
||||||
|
newObjOp.Input.Remove(i);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset output
|
||||||
|
var modelOutput = newObjOp.Output.FirstOrDefault(o => o.Shape == ConnectorShape.Square);
|
||||||
|
if (modelOutput != null)
|
||||||
|
{
|
||||||
|
modelOutput.Title = "Object";
|
||||||
|
modelOutput.DataType = "object";
|
||||||
|
}
|
||||||
|
|
||||||
|
sysVm.Title = "New Object";
|
||||||
|
}
|
||||||
|
|
||||||
private void HandleCopyNodeConnected(ConnectionViewModel c)
|
private void HandleCopyNodeConnected(ConnectionViewModel c)
|
||||||
{
|
{
|
||||||
// Find the COPY node's input connector
|
// Find the COPY node's input connector
|
||||||
@@ -463,7 +583,7 @@ namespace Nodify.Calculator
|
|||||||
|
|
||||||
// Check if the element type is a model class (has a .cs file in CustomModels)
|
// Check if the element type is a model class (has a .cs file in CustomModels)
|
||||||
bool isModelType = false;
|
bool isModelType = false;
|
||||||
var modelFilePath = Path.Combine("CustomModels", $"{elementType}.cs");
|
var modelFilePath = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels", $"{elementType}.cs");
|
||||||
if (File.Exists(modelFilePath))
|
if (File.Exists(modelFilePath))
|
||||||
isModelType = true;
|
isModelType = true;
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,7 @@
|
|||||||
</Storyboard>
|
</Storyboard>
|
||||||
|
|
||||||
<local:ItemToListConverter x:Key="ItemToListConverter" />
|
<local:ItemToListConverter x:Key="ItemToListConverter" />
|
||||||
|
<local:UpperCaseConverter x:Key="UpperCaseConverter" />
|
||||||
|
|
||||||
<DataTemplate x:Key="ConnectionTemplate"
|
<DataTemplate x:Key="ConnectionTemplate"
|
||||||
DataType="{x:Type local:ConnectionViewModel}">
|
DataType="{x:Type local:ConnectionViewModel}">
|
||||||
@@ -434,12 +435,29 @@
|
|||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
|
|
||||||
<DataTemplate DataType="{x:Type local:FunctionOperationViewModel}">
|
<DataTemplate DataType="{x:Type local:FunctionOperationViewModel}">
|
||||||
<nodify:Node Header="{Binding Title}"
|
<nodify:Node Input="{Binding Input}"
|
||||||
Input="{Binding Input}"
|
|
||||||
Output="{Binding Output}"
|
Output="{Binding Output}"
|
||||||
ToolTip="Double click to edit function"
|
ToolTip="Double click to edit function"
|
||||||
BorderBrush="#FF9800"
|
BorderBrush="#FF9800"
|
||||||
BorderThickness="2">
|
BorderThickness="2">
|
||||||
|
<nodify:Node.Header>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="ƒ"
|
||||||
|
FontSize="16"
|
||||||
|
FontWeight="Bold"
|
||||||
|
Foreground="#FF9800"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="0 0 6 0" />
|
||||||
|
<TextBlock Text="{Binding Title}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontWeight="SemiBold" />
|
||||||
|
</StackPanel>
|
||||||
|
</nodify:Node.Header>
|
||||||
|
<TextBlock Text="⚡ Double-click to edit"
|
||||||
|
FontSize="10"
|
||||||
|
Foreground="#FF9800"
|
||||||
|
Opacity="0.8"
|
||||||
|
Margin="4 2" />
|
||||||
<nodify:Node.InputBindings>
|
<nodify:Node.InputBindings>
|
||||||
<MouseBinding Gesture="LeftDoubleClick"
|
<MouseBinding Gesture="LeftDoubleClick"
|
||||||
Command="{Binding DataContext.OpenCalculatorCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
Command="{Binding DataContext.OpenCalculatorCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||||
@@ -590,8 +608,8 @@
|
|||||||
|
|
||||||
<Border HorizontalAlignment="Right"
|
<Border HorizontalAlignment="Right"
|
||||||
MinWidth="200"
|
MinWidth="200"
|
||||||
MaxWidth="300"
|
MaxWidth="350"
|
||||||
MaxHeight="500"
|
MaxHeight="600"
|
||||||
Padding="7"
|
Padding="7"
|
||||||
Margin="10"
|
Margin="10"
|
||||||
CornerRadius="3"
|
CornerRadius="3"
|
||||||
@@ -612,34 +630,102 @@
|
|||||||
FontWeight="Bold"
|
FontWeight="Bold"
|
||||||
DockPanel.Dock="Top" />
|
DockPanel.Dock="Top" />
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
<ItemsControl ItemsSource="{Binding Calculator.OperationsMenu.AvailableOperations}"
|
<StackPanel>
|
||||||
Focusable="False">
|
<!-- System nodes (non-API) -->
|
||||||
<ItemsControl.ItemContainerStyle>
|
<ItemsControl ItemsSource="{Binding Calculator.OperationsMenu.SystemNodes}"
|
||||||
<Style>
|
Focusable="False">
|
||||||
<Setter Property="FrameworkElement.Margin"
|
<ItemsControl.ItemContainerStyle>
|
||||||
Value="5" />
|
<Style>
|
||||||
<Setter Property="FrameworkElement.HorizontalAlignment"
|
<Setter Property="FrameworkElement.Margin" Value="5" />
|
||||||
Value="Left" />
|
<Setter Property="FrameworkElement.HorizontalAlignment" Value="Left" />
|
||||||
<Setter Property="FrameworkElement.Cursor"
|
<Setter Property="FrameworkElement.Cursor" Value="Hand" />
|
||||||
Value="Hand" />
|
<Setter Property="FrameworkElement.ToolTip" Value="Drag and drop into the editor" />
|
||||||
<Setter Property="FrameworkElement.ToolTip"
|
</Style>
|
||||||
Value="Drag and drop into the editor" />
|
</ItemsControl.ItemContainerStyle>
|
||||||
</Style>
|
<ItemsControl.ItemTemplate>
|
||||||
</ItemsControl.ItemContainerStyle>
|
<DataTemplate DataType="{x:Type local:OperationViewModel}">
|
||||||
<ItemsControl.ItemTemplate>
|
<nodify:Node Content="{Binding Title}"
|
||||||
<DataTemplate DataType="{x:Type local:OperationViewModel}">
|
Input="{Binding Input}"
|
||||||
<nodify:Node Content="{Binding Title}"
|
Output="{Binding Output}"
|
||||||
Input="{Binding Input}"
|
BorderBrush="{StaticResource AnimatedBrush}"
|
||||||
Output="{Binding Output}"
|
BorderThickness="2"
|
||||||
BorderBrush="{StaticResource AnimatedBrush}"
|
MouseMove="OnNodeDrag"
|
||||||
BorderThickness="2"
|
Focusable="True"
|
||||||
MouseMove="OnNodeDrag"
|
KeyboardNavigation.TabNavigation="None">
|
||||||
Focusable="True"
|
</nodify:Node>
|
||||||
KeyboardNavigation.TabNavigation="None">
|
</DataTemplate>
|
||||||
</nodify:Node>
|
</ItemsControl.ItemTemplate>
|
||||||
</DataTemplate>
|
</ItemsControl>
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
</ItemsControl>
|
<!-- Grouped Swagger API operations -->
|
||||||
|
<ItemsControl ItemsSource="{Binding Calculator.OperationsMenu.GroupedSwaggerOperations}"
|
||||||
|
Focusable="False">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Expander IsExpanded="True" Margin="0 6 0 0">
|
||||||
|
<Expander.Header>
|
||||||
|
<TextBlock Text="{Binding GroupName}"
|
||||||
|
FontWeight="Bold"
|
||||||
|
FontSize="15"
|
||||||
|
Foreground="{DynamicResource ForegroundBrush}" />
|
||||||
|
</Expander.Header>
|
||||||
|
<ItemsControl ItemsSource="{Binding Operations}" Focusable="False" Margin="4 2 0 0">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Border Margin="2 3" Padding="5 4" CornerRadius="4"
|
||||||
|
Cursor="Hand"
|
||||||
|
MouseMove="OnSwaggerItemDrag"
|
||||||
|
ToolTip="Drag and drop into the editor"
|
||||||
|
Tag="{Binding}">
|
||||||
|
<Border.Background>
|
||||||
|
<SolidColorBrush Color="{DynamicResource BackgroundColor}" Opacity="0.5" />
|
||||||
|
</Border.Background>
|
||||||
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<Border CornerRadius="3" Padding="6 2" Margin="0 0 6 0" MinWidth="50"
|
||||||
|
HorizontalAlignment="Left">
|
||||||
|
<Border.Style>
|
||||||
|
<Style TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="#61AFFE" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding OPType}" Value="post">
|
||||||
|
<Setter Property="Background" Value="#49CC90" />
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding OPType}" Value="put">
|
||||||
|
<Setter Property="Background" Value="#FCA130" />
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding OPType}" Value="delete">
|
||||||
|
<Setter Property="Background" Value="#F93E3E" />
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding OPType}" Value="patch">
|
||||||
|
<Setter Property="Background" Value="#50E3C2" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Border.Style>
|
||||||
|
<TextBlock FontWeight="Bold" Foreground="White"
|
||||||
|
HorizontalAlignment="Center">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Text" Value="{Binding OPType, Converter={StaticResource UpperCaseConverter}}" />
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</Border>
|
||||||
|
<TextBlock Text="{Binding Title}"
|
||||||
|
Foreground="{DynamicResource ForegroundBrush}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
TextTrimming="CharacterEllipsis"
|
||||||
|
MaxWidth="220" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</Expander>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -83,6 +83,15 @@ namespace Nodify.Calculator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnSwaggerItemDrag(object sender, MouseEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.LeftButton == MouseButtonState.Pressed && sender is FrameworkElement fe && fe.Tag is OperationInfoViewModel operation)
|
||||||
|
{
|
||||||
|
var data = new DataObject(typeof(OperationInfoViewModel), operation);
|
||||||
|
DragDrop.DoDragDrop(this, data, DragDropEffects.Copy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OpenContextMenu_Executed(object sender, ExecutedRoutedEventArgs e)
|
private void OpenContextMenu_Executed(object sender, ExecutedRoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Source is NodifyEditor editor && editor.DataContext is CalculatorViewModel calculator)
|
if (e.Source is NodifyEditor editor && editor.DataContext is CalculatorViewModel calculator)
|
||||||
|
|||||||
@@ -323,7 +323,7 @@ namespace Nodify.Calculator
|
|||||||
{
|
{
|
||||||
//Convert model here
|
//Convert model here
|
||||||
//CreateModelsFromString(outputValue);
|
//CreateModelsFromString(outputValue);
|
||||||
var customModelDir = "CustomModels";
|
var customModelDir = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels");
|
||||||
Directory.CreateDirectory(customModelDir);
|
Directory.CreateDirectory(customModelDir);
|
||||||
string className = "Class 1";
|
string className = "Class 1";
|
||||||
className = className.Replace(" ", "");
|
className = className.Replace(" ", "");
|
||||||
@@ -428,6 +428,79 @@ namespace Nodify.Calculator
|
|||||||
OnLogMe?.Invoke($"Auth node resolved. Base URL: {_authBaseUrl}, Auth Type: {_authType}");
|
OnLogMe?.Invoke($"Auth node resolved. Base URL: {_authBaseUrl}, Auth Type: {_authType}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (op is SystemOperationViewModel debugSysOp && debugSysOp.SystemOperationType == SystemOperations.DEBUG)
|
||||||
|
{
|
||||||
|
// Find the data input connection (non-triangle)
|
||||||
|
var debugInputCons = connections.Where(c => c.Input.Operation == op && c.Input.Shape != ConnectorShape.Triangle).ToList();
|
||||||
|
if (debugInputCons.Any())
|
||||||
|
{
|
||||||
|
var sourceConn = debugInputCons.First();
|
||||||
|
var sourceNodeId = sourceConn.Output.Operation.NodeId;
|
||||||
|
if (outputs.TryGetValue(sourceNodeId, out var debugVal))
|
||||||
|
{
|
||||||
|
OnLogMe?.Invoke($"[DEBUG] {debugVal}");
|
||||||
|
}
|
||||||
|
else if (sourceConn.Output.Value != null)
|
||||||
|
{
|
||||||
|
OnLogMe?.Invoke($"[DEBUG] {sourceConn.Output.Value}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OnLogMe?.Invoke($"[DEBUG] (no value)", logType.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OnLogMe?.Invoke($"[DEBUG] No input connected", logType.Warning);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle New Object nodes — construct JSON from property inputs
|
||||||
|
if (op is SystemOperationViewModel newObjSysOp && newObjSysOp.SystemOperationType == SystemOperations.NEW_OBJECT)
|
||||||
|
{
|
||||||
|
OnLogMe?.Invoke($"Constructing new object: {newObjSysOp.Title}");
|
||||||
|
var jObj = new JObject();
|
||||||
|
// Iterate property inputs (skip triangle flow + square model connectors)
|
||||||
|
foreach (var inp in op.Input)
|
||||||
|
{
|
||||||
|
if (inp.Shape == ConnectorShape.Triangle || inp.Shape == ConnectorShape.Square) continue;
|
||||||
|
|
||||||
|
// Extract property name from title like "Name (string)"
|
||||||
|
var propName = inp.Title ?? "";
|
||||||
|
var parenIdx = propName.IndexOf(" (");
|
||||||
|
if (parenIdx > 0) propName = propName.Substring(0, parenIdx);
|
||||||
|
|
||||||
|
// Find connection feeding this input
|
||||||
|
var inputCon = connections.FirstOrDefault(cn => cn.Input == inp);
|
||||||
|
string val = null;
|
||||||
|
if (inputCon != null)
|
||||||
|
{
|
||||||
|
var srcNodeId = inputCon.Output.Operation.NodeId;
|
||||||
|
if (outputs.TryGetValue(srcNodeId, out var srcVal))
|
||||||
|
val = srcVal;
|
||||||
|
else if (inputCon.Output.Value != null)
|
||||||
|
val = inputCon.Output.Value.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (val != null)
|
||||||
|
{
|
||||||
|
// Try to parse as JSON token, fallback to string
|
||||||
|
try { jObj[propName] = JToken.Parse(val); }
|
||||||
|
catch { jObj[propName] = val; }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
jObj[propName] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = jObj.ToString(Formatting.None);
|
||||||
|
outputs[op.NodeId] = json;
|
||||||
|
variables[op.NodeId] = json;
|
||||||
|
OnLogMe?.Invoke($"Object constructed: {json}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle Function nodes — execute the inner flow
|
// Handle Function nodes — execute the inner flow
|
||||||
if (op is FunctionOperationViewModel funcOp)
|
if (op is FunctionOperationViewModel funcOp)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
namespace Nodify.Calculator
|
namespace Nodify.Calculator
|
||||||
@@ -26,6 +27,19 @@ namespace Nodify.Calculator
|
|||||||
|
|
||||||
public FunctionOperationViewModel()
|
public FunctionOperationViewModel()
|
||||||
{
|
{
|
||||||
|
// Remove the default InnerInput/InnerOutput nodes added by the base class
|
||||||
|
// FunctionOperationViewModel uses its own InnerBegin/InnerEnd instead
|
||||||
|
var toRemove = InnerCalculator.Operations
|
||||||
|
.Where(op => op.Title == "Input Parameters" || op.Title == "Output Parameters")
|
||||||
|
.ToList();
|
||||||
|
foreach (var op in toRemove)
|
||||||
|
{
|
||||||
|
InnerCalculator.Operations.Remove(op);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear any input connectors inherited from the base class InnerInput sync
|
||||||
|
Input.Clear();
|
||||||
|
|
||||||
// Add Begin and End nodes inside the inner calculator for flow-based editing
|
// Add Begin and End nodes inside the inner calculator for flow-based editing
|
||||||
InnerBegin = new SystemOperationViewModel
|
InnerBegin = new SystemOperationViewModel
|
||||||
{
|
{
|
||||||
|
|||||||
452
Examples/Nodify.Calculator/GraphSerializer.cs
Normal file
452
Examples/Nodify.Calculator/GraphSerializer.cs
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
using Nodify.Calculator.Models;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace Nodify.Calculator
|
||||||
|
{
|
||||||
|
public static class GraphSerializer
|
||||||
|
{
|
||||||
|
public static void Save(CalculatorViewModel calculator)
|
||||||
|
{
|
||||||
|
var graph = new SaveGraphModel();
|
||||||
|
var operations = calculator.Operations;
|
||||||
|
var connections = calculator.Connections;
|
||||||
|
|
||||||
|
// Ensure all nodes have IDs
|
||||||
|
int idCounter = 1;
|
||||||
|
foreach (var op in operations)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(op.NodeId))
|
||||||
|
op.NodeId = $"node_{idCounter++}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize nodes
|
||||||
|
foreach (var op in operations)
|
||||||
|
{
|
||||||
|
var nodeData = new NodeData
|
||||||
|
{
|
||||||
|
NodeId = op.NodeId,
|
||||||
|
Title = op.Title ?? string.Empty,
|
||||||
|
LocationX = op.Location.X,
|
||||||
|
LocationY = op.Location.Y
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (op)
|
||||||
|
{
|
||||||
|
case TakeOperationViewModel take:
|
||||||
|
nodeData.NodeType = "System";
|
||||||
|
nodeData.SystemOp = take.SystemOperationType.ToString();
|
||||||
|
nodeData.NthIndex = take.NthIndex;
|
||||||
|
nodeData.IsRandom = take.IsRandom;
|
||||||
|
break;
|
||||||
|
case FunctionOperationViewModel func:
|
||||||
|
nodeData.NodeType = "Function";
|
||||||
|
nodeData.FunctionName = func.FunctionName;
|
||||||
|
foreach (var p in func.InputParameters)
|
||||||
|
nodeData.FunctionInputs.Add(new FunctionParamData { Name = p.Name, Type = p.Type });
|
||||||
|
foreach (var p in func.OutputParameters)
|
||||||
|
nodeData.FunctionOutputs.Add(new FunctionParamData { Name = p.Name, Type = p.Type });
|
||||||
|
break;
|
||||||
|
case SystemOperationViewModel sys:
|
||||||
|
nodeData.NodeType = "System";
|
||||||
|
nodeData.SystemOp = sys.SystemOperationType.ToString();
|
||||||
|
// For GET_SET nodes, derive ClassName from title
|
||||||
|
if (sys.SystemOperationType == SystemOperations.GET_SET)
|
||||||
|
{
|
||||||
|
var title = sys.Title ?? "";
|
||||||
|
if (title.StartsWith("GET ")) nodeData.ClassName = title.Substring(4).Trim();
|
||||||
|
else if (title.StartsWith("SET ")) nodeData.ClassName = title.Substring(4).Trim();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case APIOperationViewModel api:
|
||||||
|
nodeData.NodeType = "API";
|
||||||
|
nodeData.OPType = api.OperationType;
|
||||||
|
nodeData.ResponseModelClassName = api.ResponseModelClassName;
|
||||||
|
break;
|
||||||
|
case ExpandoOperationViewModel:
|
||||||
|
nodeData.NodeType = "Expando";
|
||||||
|
break;
|
||||||
|
case CalculatorOperationViewModel:
|
||||||
|
nodeData.NodeType = "Calculator";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
nodeData.NodeType = "Normal";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save connector metadata for all nodes
|
||||||
|
foreach (var c in op.Input)
|
||||||
|
nodeData.InputConnectors.Add(SerializeConnector(c));
|
||||||
|
foreach (var c in op.Output)
|
||||||
|
nodeData.OutputConnectors.Add(SerializeConnector(c));
|
||||||
|
|
||||||
|
graph.Nodes.Add(nodeData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize connections
|
||||||
|
foreach (var conn in connections)
|
||||||
|
{
|
||||||
|
var inputOp = conn.Input?.Operation;
|
||||||
|
var outputOp = conn.Output?.Operation;
|
||||||
|
if (inputOp == null || outputOp == null) continue;
|
||||||
|
|
||||||
|
var inputIndex = inputOp.Input.IndexOf(conn.Input);
|
||||||
|
var outputIndex = outputOp.Output.IndexOf(conn.Output);
|
||||||
|
|
||||||
|
if (inputIndex < 0 || outputIndex < 0) continue;
|
||||||
|
|
||||||
|
graph.Connections.Add(new ConnectionData
|
||||||
|
{
|
||||||
|
TargetNodeId = inputOp.NodeId,
|
||||||
|
TargetConnectorIndex = inputIndex,
|
||||||
|
SourceNodeId = outputOp.NodeId,
|
||||||
|
SourceConnectorIndex = outputIndex
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to LiteDB
|
||||||
|
using var db = new LiteDbHelper<SaveGraphModel>("Graph");
|
||||||
|
db.DeleteMany(_ => true);
|
||||||
|
db.Insert(graph);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Load(CalculatorViewModel calculator, OperationsMenuViewModel opsMenu)
|
||||||
|
{
|
||||||
|
SaveGraphModel graph;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var db = new LiteDbHelper<SaveGraphModel>("Graph");
|
||||||
|
graph = db.FindAll().FirstOrDefault();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (graph == null || graph.Nodes.Count == 0) return;
|
||||||
|
|
||||||
|
// Clear existing
|
||||||
|
calculator.Connections.Clear();
|
||||||
|
calculator.Operations.Clear();
|
||||||
|
|
||||||
|
calculator.IsLoading = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
|
||||||
|
var nodeMap = new Dictionary<string, OperationViewModel>();
|
||||||
|
|
||||||
|
// Recreate nodes
|
||||||
|
foreach (var nd in graph.Nodes)
|
||||||
|
{
|
||||||
|
OperationViewModel op = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
op = RecreateNode(nd, opsMenu);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (op == null) continue;
|
||||||
|
|
||||||
|
op.NodeId = nd.NodeId;
|
||||||
|
op.Location = new System.Windows.Point(nd.LocationX, nd.LocationY);
|
||||||
|
nodeMap[nd.NodeId] = op;
|
||||||
|
calculator.Operations.Add(op);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recreate connections
|
||||||
|
foreach (var cd in graph.Connections)
|
||||||
|
{
|
||||||
|
if (!nodeMap.TryGetValue(cd.SourceNodeId, out var sourceOp)) continue;
|
||||||
|
if (!nodeMap.TryGetValue(cd.TargetNodeId, out var targetOp)) continue;
|
||||||
|
if (cd.SourceConnectorIndex >= sourceOp.Output.Count) continue;
|
||||||
|
if (cd.TargetConnectorIndex >= targetOp.Input.Count) continue;
|
||||||
|
|
||||||
|
var sourceConn = sourceOp.Output[cd.SourceConnectorIndex];
|
||||||
|
var targetConn = targetOp.Input[cd.TargetConnectorIndex];
|
||||||
|
|
||||||
|
calculator.Connections.Add(new ConnectionViewModel
|
||||||
|
{
|
||||||
|
Input = targetConn,
|
||||||
|
Output = sourceConn
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
calculator.IsLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OperationViewModel RecreateNode(NodeData nd, OperationsMenuViewModel opsMenu)
|
||||||
|
{
|
||||||
|
switch (nd.NodeType)
|
||||||
|
{
|
||||||
|
case "API":
|
||||||
|
{
|
||||||
|
// Find matching swagger operation or create from saved data
|
||||||
|
var info = new OperationInfoViewModel
|
||||||
|
{
|
||||||
|
Title = nd.Title,
|
||||||
|
OPType = nd.OPType?.ToLower() ?? "get",
|
||||||
|
Type = OperationType.API,
|
||||||
|
ResponseModelClassName = nd.ResponseModelClassName ?? string.Empty
|
||||||
|
};
|
||||||
|
// Restore inputs from saved connector data (skip flow triangle at index 0)
|
||||||
|
foreach (var ic in nd.InputConnectors)
|
||||||
|
{
|
||||||
|
if (ic.Shape != "Triangle")
|
||||||
|
info.Input.Add(ic.Title);
|
||||||
|
}
|
||||||
|
info.Output.Add("");
|
||||||
|
return OperationFactory.GetOperation(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "System":
|
||||||
|
{
|
||||||
|
if (Enum.TryParse<SystemOperations>(nd.SystemOp, out var sysOp))
|
||||||
|
{
|
||||||
|
var info = new OperationInfoViewModel
|
||||||
|
{
|
||||||
|
Title = nd.Title,
|
||||||
|
Type = OperationType.System,
|
||||||
|
sysOp = sysOp
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sysOp == SystemOperations.GET_SET && !string.IsNullOrEmpty(nd.ClassName))
|
||||||
|
{
|
||||||
|
info.IsModelNode = true;
|
||||||
|
info.ClassName = nd.ClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up inputs/outputs based on system operation type
|
||||||
|
switch (sysOp)
|
||||||
|
{
|
||||||
|
case SystemOperations.BEGIN:
|
||||||
|
info.Output.Add(""); // flow output only
|
||||||
|
break;
|
||||||
|
case SystemOperations.END:
|
||||||
|
info.Input.Add(""); // flow input only
|
||||||
|
break;
|
||||||
|
case SystemOperations.DEBUG:
|
||||||
|
info.Input.Add("Value");
|
||||||
|
info.IsFlowNode = true;
|
||||||
|
break;
|
||||||
|
case SystemOperations.TAKE:
|
||||||
|
info.Input.Add("List");
|
||||||
|
break;
|
||||||
|
case SystemOperations.COPY:
|
||||||
|
info.Input.Add("");
|
||||||
|
break;
|
||||||
|
case SystemOperations.SPLIT:
|
||||||
|
info.Input.Add("");
|
||||||
|
break;
|
||||||
|
case SystemOperations.NEW_OBJECT:
|
||||||
|
info.Input.Add("");
|
||||||
|
info.IsFlowNode = true;
|
||||||
|
break;
|
||||||
|
case SystemOperations.IF:
|
||||||
|
info.Output.Add("");
|
||||||
|
info.IsFlowNode = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
info.Output.Add("");
|
||||||
|
info.IsFlowNode = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var op = OperationFactory.GetOperation(info);
|
||||||
|
|
||||||
|
// Restore Take properties
|
||||||
|
if (op is TakeOperationViewModel takeOp)
|
||||||
|
{
|
||||||
|
takeOp.NthIndex = nd.NthIndex;
|
||||||
|
takeOp.IsRandom = nd.IsRandom;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore dynamic connectors for SPLIT/COPY/TAKE from saved data
|
||||||
|
RestoreDynamicConnectors(op, nd);
|
||||||
|
|
||||||
|
return op;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "Function":
|
||||||
|
{
|
||||||
|
var info = new OperationInfoViewModel
|
||||||
|
{
|
||||||
|
Title = nd.FunctionName,
|
||||||
|
Type = OperationType.System,
|
||||||
|
sysOp = SystemOperations.FUNCTION,
|
||||||
|
IsFunction = true
|
||||||
|
};
|
||||||
|
foreach (var inp in nd.FunctionInputs)
|
||||||
|
info.FunctionInputs.Add(new FunctionParameterInfo { Name = inp.Name, Type = inp.Type });
|
||||||
|
foreach (var outp in nd.FunctionOutputs)
|
||||||
|
info.FunctionOutputs.Add(new FunctionParameterInfo { Name = outp.Name, Type = outp.Type });
|
||||||
|
info.Output.Add("");
|
||||||
|
return OperationFactory.GetOperation(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "Expando":
|
||||||
|
{
|
||||||
|
var info = new OperationInfoViewModel
|
||||||
|
{
|
||||||
|
Title = nd.Title,
|
||||||
|
Type = OperationType.Expando
|
||||||
|
};
|
||||||
|
// Restore input count from saved connectors
|
||||||
|
foreach (var ic in nd.InputConnectors)
|
||||||
|
info.Input.Add(ic.Title);
|
||||||
|
info.Output.Add("");
|
||||||
|
return OperationFactory.GetOperation(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
var info = new OperationInfoViewModel
|
||||||
|
{
|
||||||
|
Title = nd.Title,
|
||||||
|
Type = OperationType.Normal
|
||||||
|
};
|
||||||
|
foreach (var ic in nd.InputConnectors)
|
||||||
|
info.Input.Add(ic.Title);
|
||||||
|
info.Output.Add("");
|
||||||
|
return OperationFactory.GetOperation(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RestoreDynamicConnectors(OperationViewModel op, NodeData nd)
|
||||||
|
{
|
||||||
|
// For SPLIT nodes: if saved outputs have more than just the flow triangle,
|
||||||
|
// restore the dynamic property outputs
|
||||||
|
if (op is SystemOperationViewModel sysVm)
|
||||||
|
{
|
||||||
|
if (sysVm.SystemOperationType == SystemOperations.SPLIT)
|
||||||
|
{
|
||||||
|
// Remove default non-triangle outputs, then add saved ones
|
||||||
|
var defaultNonTriangle = op.Output.Where(o => o.Shape != ConnectorShape.Triangle).ToList();
|
||||||
|
foreach (var d in defaultNonTriangle) op.Output.Remove(d);
|
||||||
|
|
||||||
|
foreach (var sc in nd.OutputConnectors)
|
||||||
|
{
|
||||||
|
if (sc.Shape == "Triangle") continue;
|
||||||
|
op.Output.Add(DeserializeConnector(sc, false));
|
||||||
|
}
|
||||||
|
// Update title from saved
|
||||||
|
sysVm.Title = nd.Title;
|
||||||
|
}
|
||||||
|
else if (sysVm.SystemOperationType == SystemOperations.NEW_OBJECT)
|
||||||
|
{
|
||||||
|
// Restore dynamic property inputs (keep triangle + square, replace the rest)
|
||||||
|
var dynamicInputs = op.Input.Where(i => i.Shape != ConnectorShape.Triangle && i.Shape != ConnectorShape.Square).ToList();
|
||||||
|
foreach (var d in dynamicInputs) op.Input.Remove(d);
|
||||||
|
|
||||||
|
foreach (var ic in nd.InputConnectors)
|
||||||
|
{
|
||||||
|
if (ic.Shape == "Triangle" || ic.Shape == "Square") continue;
|
||||||
|
op.Input.Add(DeserializeConnector(ic, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore output connector metadata (Square model output)
|
||||||
|
var savedModelOutput = nd.OutputConnectors.FirstOrDefault(o => o.Shape == "Square");
|
||||||
|
var modelOutput = op.Output.FirstOrDefault(o => o.Shape == ConnectorShape.Square);
|
||||||
|
if (savedModelOutput != null && modelOutput != null)
|
||||||
|
{
|
||||||
|
modelOutput.Title = savedModelOutput.Title;
|
||||||
|
modelOutput.DataType = savedModelOutput.DataType;
|
||||||
|
modelOutput.ConnectorColor = Color.FromArgb(savedModelOutput.ColorArgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
sysVm.Title = nd.Title;
|
||||||
|
}
|
||||||
|
else if (sysVm.SystemOperationType == SystemOperations.COPY)
|
||||||
|
{
|
||||||
|
// Restore adapted connectors
|
||||||
|
if (nd.InputConnectors.Count > 0 && nd.InputConnectors[0].Shape != "Circle")
|
||||||
|
{
|
||||||
|
// Input was adapted — restore
|
||||||
|
var savedInput = nd.InputConnectors[0];
|
||||||
|
if (op.Input.Count > 0)
|
||||||
|
{
|
||||||
|
var inp = op.Input[0];
|
||||||
|
inp.Shape = ParseShape(savedInput.Shape);
|
||||||
|
inp.ConnectorColor = Color.FromArgb(savedInput.ColorArgb);
|
||||||
|
inp.DataType = savedInput.DataType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore output connectors
|
||||||
|
var existingOutputs = op.Output.ToList();
|
||||||
|
foreach (var eo in existingOutputs) op.Output.Remove(eo);
|
||||||
|
foreach (var sc in nd.OutputConnectors)
|
||||||
|
{
|
||||||
|
op.Output.Add(DeserializeConnector(sc, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (sysVm.SystemOperationType == SystemOperations.TAKE)
|
||||||
|
{
|
||||||
|
// Restore adapted list input
|
||||||
|
foreach (var inp in op.Input)
|
||||||
|
{
|
||||||
|
if (!inp.IsTakeListConnector) continue;
|
||||||
|
var savedInp = nd.InputConnectors.FirstOrDefault(c => c.IsTakeListConnector);
|
||||||
|
if (savedInp != null)
|
||||||
|
{
|
||||||
|
inp.Shape = ParseShape(savedInp.Shape);
|
||||||
|
inp.ConnectorColor = Color.FromArgb(savedInp.ColorArgb);
|
||||||
|
inp.DataType = savedInp.DataType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Restore output
|
||||||
|
var existingOutputs = op.Output.ToList();
|
||||||
|
foreach (var eo in existingOutputs) op.Output.Remove(eo);
|
||||||
|
foreach (var sc in nd.OutputConnectors)
|
||||||
|
{
|
||||||
|
op.Output.Add(DeserializeConnector(sc, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ConnectorData SerializeConnector(ConnectorViewModel c)
|
||||||
|
{
|
||||||
|
return new ConnectorData
|
||||||
|
{
|
||||||
|
Title = c.Title ?? string.Empty,
|
||||||
|
Shape = c.Shape.ToString(),
|
||||||
|
ColorArgb = c.RawColor.ToArgb(),
|
||||||
|
DataType = c.DataType ?? string.Empty,
|
||||||
|
IsCopyConnector = c.IsCopyConnector,
|
||||||
|
IsTakeListConnector = c.IsTakeListConnector
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ConnectorViewModel DeserializeConnector(ConnectorData cd, bool isInput)
|
||||||
|
{
|
||||||
|
return new ConnectorViewModel
|
||||||
|
{
|
||||||
|
Title = cd.Title,
|
||||||
|
Shape = ParseShape(cd.Shape),
|
||||||
|
ConnectorColor = Color.FromArgb(cd.ColorArgb),
|
||||||
|
DataType = cd.DataType,
|
||||||
|
IsCopyConnector = cd.IsCopyConnector,
|
||||||
|
IsTakeListConnector = cd.IsTakeListConnector,
|
||||||
|
IsInput = isInput
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ConnectorShape ParseShape(string shape)
|
||||||
|
{
|
||||||
|
return Enum.TryParse<ConnectorShape>(shape, out var s) ? s : ConnectorShape.Circle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
76
Examples/Nodify.Calculator/LogPanelViewModel.cs
Normal file
76
Examples/Nodify.Calculator/LogPanelViewModel.cs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace Nodify.Calculator
|
||||||
|
{
|
||||||
|
public class LogEntry
|
||||||
|
{
|
||||||
|
public string Timestamp { get; set; }
|
||||||
|
public string Level { get; set; }
|
||||||
|
public string Message { get; set; }
|
||||||
|
public Brush Color { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LogPanelViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private bool _isOpen;
|
||||||
|
public bool IsOpen
|
||||||
|
{
|
||||||
|
get => _isOpen;
|
||||||
|
set => SetProperty(ref _isOpen, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _isRunning;
|
||||||
|
public bool IsRunning
|
||||||
|
{
|
||||||
|
get => _isRunning;
|
||||||
|
set => SetProperty(ref _isRunning, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<LogEntry> LogEntries { get; } = new ObservableCollection<LogEntry>();
|
||||||
|
|
||||||
|
public INodifyCommand ClearLogsCommand { get; }
|
||||||
|
public INodifyCommand ClosePanelCommand { get; }
|
||||||
|
|
||||||
|
public LogPanelViewModel()
|
||||||
|
{
|
||||||
|
ClearLogsCommand = new DelegateCommand(() => LogEntries.Clear());
|
||||||
|
ClosePanelCommand = new DelegateCommand(() => IsOpen = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteLog(string message, logType logtype = logType.Information)
|
||||||
|
{
|
||||||
|
Brush color;
|
||||||
|
switch (logtype)
|
||||||
|
{
|
||||||
|
case logType.Warning:
|
||||||
|
color = Brushes.Orange;
|
||||||
|
break;
|
||||||
|
case logType.Error:
|
||||||
|
color = Brushes.OrangeRed;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
color = Brushes.White;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entry = new LogEntry
|
||||||
|
{
|
||||||
|
Timestamp = DateTime.Now.ToString("HH:mm:ss"),
|
||||||
|
Level = logtype.ToString(),
|
||||||
|
Message = message,
|
||||||
|
Color = color
|
||||||
|
};
|
||||||
|
|
||||||
|
if (System.Windows.Application.Current?.Dispatcher.CheckAccess() == true)
|
||||||
|
{
|
||||||
|
LogEntries.Add(entry);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
System.Windows.Application.Current?.Dispatcher.Invoke(() => LogEntries.Add(entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,7 +51,14 @@
|
|||||||
</Window.Resources>
|
</Window.Resources>
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<shared:TabControlEx ItemsSource="{Binding Editors}"
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" MinHeight="0" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<shared:TabControlEx Grid.Row="0"
|
||||||
|
ItemsSource="{Binding Editors}"
|
||||||
SelectedItem="{Binding SelectedEditor}"
|
SelectedItem="{Binding SelectedEditor}"
|
||||||
AddTabCommand="{Binding AddEditorCommand}"
|
AddTabCommand="{Binding AddEditorCommand}"
|
||||||
AutoScrollToEnd="{Binding AutoSelectNewEditor}">
|
AutoScrollToEnd="{Binding AutoSelectNewEditor}">
|
||||||
@@ -70,7 +77,110 @@
|
|||||||
</shared:TabControlEx.ItemContainerStyle>
|
</shared:TabControlEx.ItemContainerStyle>
|
||||||
</shared:TabControlEx>
|
</shared:TabControlEx>
|
||||||
|
|
||||||
<Expander Header="Click to hide/show"
|
<!-- Log Panel -->
|
||||||
|
<Border Grid.Row="1" Height="3" Cursor="SizeNS"
|
||||||
|
Background="Transparent">
|
||||||
|
<Border.Style>
|
||||||
|
<Style TargetType="Border">
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding LogPanel.IsOpen}" Value="False">
|
||||||
|
<Setter Property="Visibility" Value="Collapsed" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Border.Style>
|
||||||
|
<GridSplitter HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Background="#FF3F3F46"
|
||||||
|
ResizeBehavior="PreviousAndNext"
|
||||||
|
ResizeDirection="Rows" />
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Grid.Row="2" Background="#FF1E1E1E" BorderBrush="#FF3F3F46" BorderThickness="0,1,0,0">
|
||||||
|
<Border.Style>
|
||||||
|
<Style TargetType="Border">
|
||||||
|
<Setter Property="Height" Value="180" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding LogPanel.IsOpen}" Value="False">
|
||||||
|
<Setter Property="Visibility" Value="Collapsed" />
|
||||||
|
<Setter Property="Height" Value="0" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Border.Style>
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="28" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Header bar -->
|
||||||
|
<Border Grid.Row="0" Background="#FF2D2D30" BorderBrush="#FF3F3F46" BorderThickness="0,0,0,1">
|
||||||
|
<DockPanel Margin="8,0">
|
||||||
|
<TextBlock Text="⚡ Output" VerticalAlignment="Center" Foreground="#FFDCDCDC" FontWeight="SemiBold" FontSize="12" />
|
||||||
|
<TextBlock VerticalAlignment="Center" Margin="10,0,0,0" Foreground="#FF888888" FontSize="11">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Text" Value="" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding LogPanel.IsRunning}" Value="True">
|
||||||
|
<Setter Property="Text" Value="● Running..." />
|
||||||
|
<Setter Property="Foreground" Value="#FF4EC9B0" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<Button Content="🗑 Clear" Command="{Binding LogPanel.ClearLogsCommand}"
|
||||||
|
Background="Transparent" Foreground="#FFAAAAAA" BorderThickness="0"
|
||||||
|
Padding="6,2" FontSize="11" Cursor="Hand" Margin="0,0,4,0" />
|
||||||
|
<Button Content="✕" Command="{Binding LogPanel.ClosePanelCommand}"
|
||||||
|
Background="Transparent" Foreground="#FFAAAAAA" BorderThickness="0"
|
||||||
|
Padding="6,2" FontSize="11" Cursor="Hand" />
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Log entries -->
|
||||||
|
<ListBox Grid.Row="1" x:Name="LogListBox"
|
||||||
|
ItemsSource="{Binding LogPanel.LogEntries}"
|
||||||
|
Background="#FF1E1E1E" BorderThickness="0"
|
||||||
|
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||||
|
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||||
|
VirtualizingStackPanel.IsVirtualizing="True"
|
||||||
|
FontFamily="Consolas" FontSize="12">
|
||||||
|
<ListBox.ItemContainerStyle>
|
||||||
|
<Style TargetType="ListBoxItem">
|
||||||
|
<Setter Property="Padding" Value="8,1" />
|
||||||
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
|
<Setter Property="Focusable" Value="False" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ListBoxItem">
|
||||||
|
<Border Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}">
|
||||||
|
<ContentPresenter />
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</ListBox.ItemContainerStyle>
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock TextWrapping="NoWrap">
|
||||||
|
<Run Text="{Binding Timestamp, Mode=OneTime}" Foreground="#FF608B4E" />
|
||||||
|
<Run Text=" " />
|
||||||
|
<Run Text="{Binding Message, Mode=OneTime}" Foreground="{Binding Color, Mode=OneTime}" />
|
||||||
|
</TextBlock>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Expander Grid.Row="0" Header="Click to hide/show"
|
||||||
IsExpanded="False"
|
IsExpanded="False"
|
||||||
Margin="10"
|
Margin="10"
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
using Nodify.Interactivity;
|
using Nodify.Interactivity;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
|
|
||||||
namespace Nodify.Calculator
|
namespace Nodify.Calculator
|
||||||
@@ -16,6 +20,56 @@ namespace Nodify.Calculator
|
|||||||
typeof(UIElement),
|
typeof(UIElement),
|
||||||
Keyboard.PreviewGotKeyboardFocusEvent,
|
Keyboard.PreviewGotKeyboardFocusEvent,
|
||||||
(KeyboardFocusChangedEventHandler)OnPreviewGotKeyboardFocus);
|
(KeyboardFocusChangedEventHandler)OnPreviewGotKeyboardFocus);
|
||||||
|
|
||||||
|
Closing += MainWindow_Closing;
|
||||||
|
|
||||||
|
// Auto-scroll log list to bottom when new entries are added
|
||||||
|
Loaded += (_, __) =>
|
||||||
|
{
|
||||||
|
if (DataContext is ApplicationViewModel appVm)
|
||||||
|
{
|
||||||
|
((INotifyCollectionChanged)appVm.LogPanel.LogEntries).CollectionChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
if (e.Action == NotifyCollectionChangedAction.Add)
|
||||||
|
{
|
||||||
|
var listBox = FindName("LogListBox") as ListBox;
|
||||||
|
if (listBox != null && appVm.LogPanel.LogEntries.Count > 0)
|
||||||
|
listBox.ScrollIntoView(appVm.LogPanel.LogEntries[appVm.LogPanel.LogEntries.Count - 1]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MainWindow_Closing(object sender, CancelEventArgs e)
|
||||||
|
{
|
||||||
|
var result = MessageBox.Show(
|
||||||
|
"Do you want to save the project before closing?",
|
||||||
|
"Save Project",
|
||||||
|
MessageBoxButton.YesNoCancel,
|
||||||
|
MessageBoxImage.Question);
|
||||||
|
|
||||||
|
if (result == MessageBoxResult.Cancel)
|
||||||
|
{
|
||||||
|
e.Cancel = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == MessageBoxResult.Yes)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (DataContext is ApplicationViewModel appVm && appVm.Editors.Count > 0)
|
||||||
|
{
|
||||||
|
var firstEditor = appVm.Editors.First();
|
||||||
|
GraphSerializer.Save(firstEditor.Calculator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show($"Failed to save: {ex.Message}", "Save Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
|
private void OnPreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
|
||||||
|
|||||||
@@ -1,20 +1,75 @@
|
|||||||
using Microsoft.CodeAnalysis;
|
using System.Collections.Generic;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
namespace Nodify.Calculator.Models
|
namespace Nodify.Calculator.Models
|
||||||
{
|
{
|
||||||
public class SaveGraphModel
|
public class SaveGraphModel
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
public int Id { get; set; } = 1;
|
||||||
|
public List<NodeData> Nodes { get; set; } = new List<NodeData>();
|
||||||
public List<SaveNodes> Nodes { get; set; } = new List<SaveNodes>();
|
public List<ConnectionData> Connections { get; set; } = new List<ConnectionData>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class NodeData
|
||||||
|
{
|
||||||
|
/// <summary>Unique ID matching OperationViewModel.NodeId</summary>
|
||||||
|
public string NodeId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Discriminator: "API", "System", "Expando", "Calculator", "Function"</summary>
|
||||||
|
public string NodeType { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public double LocationX { get; set; }
|
||||||
|
public double LocationY { get; set; }
|
||||||
|
|
||||||
|
// API node properties
|
||||||
|
public string OPType { get; set; } = string.Empty;
|
||||||
|
public string ResponseModelClassName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// System node properties
|
||||||
|
public string SystemOp { get; set; } = string.Empty;
|
||||||
|
public string ClassName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// Take node properties
|
||||||
|
public int NthIndex { get; set; }
|
||||||
|
public bool IsRandom { get; set; }
|
||||||
|
|
||||||
|
// Function node properties
|
||||||
|
public string FunctionName { get; set; } = string.Empty;
|
||||||
|
public List<FunctionParamData> FunctionInputs { get; set; } = new List<FunctionParamData>();
|
||||||
|
public List<FunctionParamData> FunctionOutputs { get; set; } = new List<FunctionParamData>();
|
||||||
|
|
||||||
|
/// <summary>Saved input connector metadata (for dynamic nodes like SPLIT/COPY/TAKE)</summary>
|
||||||
|
public List<ConnectorData> InputConnectors { get; set; } = new List<ConnectorData>();
|
||||||
|
/// <summary>Saved output connector metadata</summary>
|
||||||
|
public List<ConnectorData> OutputConnectors { get; set; } = new List<ConnectorData>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConnectorData
|
||||||
|
{
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
public string Shape { get; set; } = "Circle";
|
||||||
|
public int ColorArgb { get; set; }
|
||||||
|
public string DataType { get; set; } = string.Empty;
|
||||||
|
public bool IsCopyConnector { get; set; }
|
||||||
|
public bool IsTakeListConnector { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConnectionData
|
||||||
|
{
|
||||||
|
public string SourceNodeId { get; set; } = string.Empty;
|
||||||
|
public int SourceConnectorIndex { get; set; }
|
||||||
|
public string TargetNodeId { get; set; } = string.Empty;
|
||||||
|
public int TargetConnectorIndex { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FunctionParamData
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Type { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep legacy for compatibility
|
||||||
public class SaveNodes
|
public class SaveNodes
|
||||||
{
|
{
|
||||||
public Point Location { get; set; }
|
public Point Location { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Nodify.Calculator
|
||||||
|
{
|
||||||
|
public class SwaggerGroupViewModel
|
||||||
|
{
|
||||||
|
public string GroupName { get; set; } = string.Empty;
|
||||||
|
public NodifyObservableCollection<OperationInfoViewModel> Operations { get; set; } = new NodifyObservableCollection<OperationInfoViewModel>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,5 +10,6 @@ namespace Nodify.Calculator.Models
|
|||||||
public List<string> InputNames { get; set; } = new List<string>();
|
public List<string> InputNames { get; set; } = new List<string>();
|
||||||
public string SwaggerFileName { get; set; } = string.Empty;
|
public string SwaggerFileName { get; set; } = string.Empty;
|
||||||
public string ResponseModelClassName { get; set; } = string.Empty;
|
public string ResponseModelClassName { get; set; } = string.Empty;
|
||||||
|
public string SwaggerGroup { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,5 +36,6 @@ namespace Nodify.Calculator
|
|||||||
public List<FunctionParameterInfo> FunctionInputs { get; set; } = new List<FunctionParameterInfo>();
|
public List<FunctionParameterInfo> FunctionInputs { get; set; } = new List<FunctionParameterInfo>();
|
||||||
public List<FunctionParameterInfo> FunctionOutputs { get; set; } = new List<FunctionParameterInfo>();
|
public List<FunctionParameterInfo> FunctionOutputs { get; set; } = new List<FunctionParameterInfo>();
|
||||||
public string ResponseModelClassName { get; set; } = string.Empty;
|
public string ResponseModelClassName { get; set; } = string.Empty;
|
||||||
|
public string SwaggerGroup { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,8 +99,28 @@ namespace Nodify.Calculator
|
|||||||
authNode.Input.Add("Username");
|
authNode.Input.Add("Username");
|
||||||
authNode.Input.Add("Password");
|
authNode.Input.Add("Password");
|
||||||
|
|
||||||
|
var debugNode = new OperationInfoViewModel()
|
||||||
|
{
|
||||||
|
Title = "Debug",
|
||||||
|
Type = OperationType.System,
|
||||||
|
sysOp = SystemOperations.DEBUG,
|
||||||
|
IsFlowNode = true
|
||||||
|
};
|
||||||
|
debugNode.Input.Add("Value");
|
||||||
|
|
||||||
|
var newObjectNode = new OperationInfoViewModel()
|
||||||
|
{
|
||||||
|
Title = "New Object",
|
||||||
|
Type = OperationType.System,
|
||||||
|
sysOp = SystemOperations.NEW_OBJECT,
|
||||||
|
IsFlowNode = true
|
||||||
|
};
|
||||||
|
newObjectNode.Input.Add("");
|
||||||
|
|
||||||
systemNodes.Add(authNode);
|
systemNodes.Add(authNode);
|
||||||
systemNodes.Add(copynode);
|
systemNodes.Add(copynode);
|
||||||
|
systemNodes.Add(debugNode);
|
||||||
|
systemNodes.Add(newObjectNode);
|
||||||
systemNodes.Add(begin);
|
systemNodes.Add(begin);
|
||||||
systemNodes.Add(ending);
|
systemNodes.Add(ending);
|
||||||
systemNodes.Add(debugAndCreateModels);
|
systemNodes.Add(debugAndCreateModels);
|
||||||
@@ -321,6 +341,54 @@ namespace Nodify.Calculator
|
|||||||
return funcOp;
|
return funcOp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (info.sysOp == SystemOperations.NEW_OBJECT)
|
||||||
|
{
|
||||||
|
var newObjOp = new SystemOperationViewModel
|
||||||
|
{
|
||||||
|
Title = info.Title,
|
||||||
|
SystemOperationType = SystemOperations.NEW_OBJECT
|
||||||
|
};
|
||||||
|
// Flow connectors
|
||||||
|
newObjOp.Input.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle });
|
||||||
|
newObjOp.Output.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle, IsInput = false });
|
||||||
|
// Model class input (Square — user connects a model source)
|
||||||
|
foreach (var inp in input)
|
||||||
|
{
|
||||||
|
inp.Shape = ConnectorShape.Square;
|
||||||
|
inp.ConnectorColor = Color.MediumPurple;
|
||||||
|
newObjOp.Input.Add(inp);
|
||||||
|
}
|
||||||
|
// Model output (Square — returns the constructed object)
|
||||||
|
newObjOp.Output.Add(new ConnectorViewModel
|
||||||
|
{
|
||||||
|
Title = "Object",
|
||||||
|
IsInput = false,
|
||||||
|
Shape = ConnectorShape.Square,
|
||||||
|
ConnectorColor = Color.MediumPurple,
|
||||||
|
DataType = "object"
|
||||||
|
});
|
||||||
|
return newObjOp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.sysOp == SystemOperations.DEBUG)
|
||||||
|
{
|
||||||
|
var debugOp = new SystemOperationViewModel
|
||||||
|
{
|
||||||
|
Title = info.Title,
|
||||||
|
SystemOperationType = SystemOperations.DEBUG
|
||||||
|
};
|
||||||
|
// Flow connectors
|
||||||
|
debugOp.Input.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle });
|
||||||
|
debugOp.Output.Add(new ConnectorViewModel { Title = "", Shape = ConnectorShape.Triangle, IsInput = false });
|
||||||
|
// Data input
|
||||||
|
foreach (var inp in input)
|
||||||
|
{
|
||||||
|
inp.ConnectorColor = Color.LimeGreen;
|
||||||
|
debugOp.Input.Add(inp);
|
||||||
|
}
|
||||||
|
return debugOp;
|
||||||
|
}
|
||||||
|
|
||||||
if (info.sysOp == SystemOperations.AUTH)
|
if (info.sysOp == SystemOperations.AUTH)
|
||||||
{
|
{
|
||||||
var authOp = new AuthOperationViewModel
|
var authOp = new AuthOperationViewModel
|
||||||
@@ -427,7 +495,7 @@ namespace Nodify.Calculator
|
|||||||
else if (info.Title == "SET")
|
else if (info.Title == "SET")
|
||||||
{
|
{
|
||||||
info.Input.Add("");
|
info.Input.Add("");
|
||||||
var customModelDir = "CustomModels";
|
var customModelDir = System.IO.Path.Combine(ProjectManager.ProjectDirectory, "CustomModels");
|
||||||
Directory.CreateDirectory(customModelDir);
|
Directory.CreateDirectory(customModelDir);
|
||||||
var flpath = System.IO.Path.Combine(customModelDir, info.ClassName + ".cs");
|
var flpath = System.IO.Path.Combine(customModelDir, info.ClassName + ".cs");
|
||||||
if (File.Exists(flpath))
|
if (File.Exists(flpath))
|
||||||
|
|||||||
@@ -114,6 +114,12 @@ namespace Nodify.Calculator
|
|||||||
public NodifyObservableCollection<OperationInfoViewModel> AvailableModels { get; }
|
public NodifyObservableCollection<OperationInfoViewModel> AvailableModels { get; }
|
||||||
public NodifyObservableCollection<OperationInfoViewModel> AvailableVariables { get; }
|
public NodifyObservableCollection<OperationInfoViewModel> AvailableVariables { get; }
|
||||||
public NodifyObservableCollection<OperationInfoViewModel> AvailableFunctions { get; }
|
public NodifyObservableCollection<OperationInfoViewModel> AvailableFunctions { get; }
|
||||||
|
|
||||||
|
/// <summary>Swagger operations grouped by SwaggerGroup (tag), for the right-side panel.</summary>
|
||||||
|
public NodifyObservableCollection<SwaggerGroupViewModel> GroupedSwaggerOperations { get; } = new NodifyObservableCollection<SwaggerGroupViewModel>();
|
||||||
|
|
||||||
|
/// <summary>Non-API system nodes for the right-side panel.</summary>
|
||||||
|
public NodifyObservableCollection<OperationInfoViewModel> SystemNodes { get; } = new NodifyObservableCollection<OperationInfoViewModel>();
|
||||||
public INodifyCommand CreateOperationCommand { get; }
|
public INodifyCommand CreateOperationCommand { get; }
|
||||||
|
|
||||||
[Newtonsoft.Json.JsonIgnore]
|
[Newtonsoft.Json.JsonIgnore]
|
||||||
@@ -129,7 +135,7 @@ namespace Nodify.Calculator
|
|||||||
AvailableFunctions = new NodifyObservableCollection<OperationInfoViewModel>();
|
AvailableFunctions = new NodifyObservableCollection<OperationInfoViewModel>();
|
||||||
LoadVariablesFromDb();
|
LoadVariablesFromDb();
|
||||||
|
|
||||||
var customModelDir = "CustomModels";
|
var customModelDir = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels");
|
||||||
Directory.CreateDirectory(customModelDir);
|
Directory.CreateDirectory(customModelDir);
|
||||||
var dirInfo = new DirectoryInfo(customModelDir);
|
var dirInfo = new DirectoryInfo(customModelDir);
|
||||||
var allFiles = dirInfo.GetFiles("*.cs");
|
var allFiles = dirInfo.GetFiles("*.cs");
|
||||||
@@ -150,8 +156,13 @@ namespace Nodify.Calculator
|
|||||||
operations.AddRange(OperationFactory.GetSystemNodes());
|
operations.AddRange(OperationFactory.GetSystemNodes());
|
||||||
//operations.AddRange(OperationFactory.GetOperationsInfo(typeof(OperationsContainer)));
|
//operations.AddRange(OperationFactory.GetOperationsInfo(typeof(OperationsContainer)));
|
||||||
|
|
||||||
|
// Populate system-only nodes for the right panel
|
||||||
|
foreach (var sysNode in operations)
|
||||||
|
SystemNodes.Add(sysNode);
|
||||||
|
|
||||||
SwaggerOperations = new NodifyObservableCollection<OperationInfoViewModel>();
|
SwaggerOperations = new NodifyObservableCollection<OperationInfoViewModel>();
|
||||||
LoadSwaggerNodesFromDb();
|
LoadSwaggerNodesFromDb();
|
||||||
|
RebuildGroupedSwaggerOperations();
|
||||||
|
|
||||||
operations.AddRange(SwaggerOperations);
|
operations.AddRange(SwaggerOperations);
|
||||||
|
|
||||||
@@ -203,7 +214,7 @@ namespace Nodify.Calculator
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate .cs file
|
// Generate .cs file
|
||||||
var customModelDir = "CustomModels";
|
var customModelDir = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels");
|
||||||
Directory.CreateDirectory(customModelDir);
|
Directory.CreateDirectory(customModelDir);
|
||||||
var sb = new System.Text.StringBuilder();
|
var sb = new System.Text.StringBuilder();
|
||||||
sb.AppendLine($"public class {className}");
|
sb.AppendLine($"public class {className}");
|
||||||
@@ -344,6 +355,8 @@ namespace Nodify.Calculator
|
|||||||
MenuAvailableOperations.Add(node);
|
MenuAvailableOperations.Add(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RebuildGroupedSwaggerOperations();
|
||||||
|
|
||||||
MessageBox.Show($"Successfully imported {nodes.Count} API endpoints from Swagger.", "Import Swagger", MessageBoxButton.OK, MessageBoxImage.Information);
|
MessageBox.Show($"Successfully imported {nodes.Count} API endpoints from Swagger.", "Import Swagger", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -362,11 +375,20 @@ namespace Nodify.Calculator
|
|||||||
{
|
{
|
||||||
foreach (var method in path.Value)
|
foreach (var method in path.Value)
|
||||||
{
|
{
|
||||||
|
// Determine swagger group from operation tags or path segments
|
||||||
|
var tag = (method.Value.Tags != null && method.Value.Tags.Count > 0)
|
||||||
|
? method.Value.Tags[0]
|
||||||
|
: null;
|
||||||
|
var group = !string.IsNullOrEmpty(tag)
|
||||||
|
? tag
|
||||||
|
: ExtractGroupFromPath(path.Key);
|
||||||
|
|
||||||
var ovmodel = new OperationInfoViewModel
|
var ovmodel = new OperationInfoViewModel
|
||||||
{
|
{
|
||||||
Title = path.Key,
|
Title = path.Key,
|
||||||
OPType = method.Key,
|
OPType = method.Key,
|
||||||
Type = OperationType.API
|
Type = OperationType.API,
|
||||||
|
SwaggerGroup = group
|
||||||
};
|
};
|
||||||
|
|
||||||
var addedParams = new HashSet<string>();
|
var addedParams = new HashSet<string>();
|
||||||
@@ -473,7 +495,7 @@ namespace Nodify.Calculator
|
|||||||
|
|
||||||
private void GenerateModelFromSchema(string className, NJsonSchema.JsonSchema schema)
|
private void GenerateModelFromSchema(string className, NJsonSchema.JsonSchema schema)
|
||||||
{
|
{
|
||||||
var customModelDir = "CustomModels";
|
var customModelDir = Path.Combine(ProjectManager.ProjectDirectory, "CustomModels");
|
||||||
Directory.CreateDirectory(customModelDir);
|
Directory.CreateDirectory(customModelDir);
|
||||||
var sb = new System.Text.StringBuilder();
|
var sb = new System.Text.StringBuilder();
|
||||||
sb.AppendLine($"public class {className}");
|
sb.AppendLine($"public class {className}");
|
||||||
@@ -522,6 +544,38 @@ namespace Nodify.Calculator
|
|||||||
return new string(name.Where(c => char.IsLetterOrDigit(c) || c == '_').ToArray());
|
return new string(name.Where(c => char.IsLetterOrDigit(c) || c == '_').ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string ExtractGroupFromPath(string path)
|
||||||
|
{
|
||||||
|
// Extract group from path like "/api/Pizza/{id}" -> "Pizza"
|
||||||
|
var segments = path.Split('/', System.StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
// Skip "api" prefix if present, take the first meaningful segment
|
||||||
|
foreach (var seg in segments)
|
||||||
|
{
|
||||||
|
if (seg.StartsWith("{") || string.Equals(seg, "api", System.StringComparison.OrdinalIgnoreCase))
|
||||||
|
continue;
|
||||||
|
// Capitalize first letter
|
||||||
|
return char.ToUpper(seg[0]) + seg.Substring(1);
|
||||||
|
}
|
||||||
|
return "Other";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RebuildGroupedSwaggerOperations()
|
||||||
|
{
|
||||||
|
GroupedSwaggerOperations.Clear();
|
||||||
|
var groups = SwaggerOperations
|
||||||
|
.GroupBy(op => string.IsNullOrEmpty(op.SwaggerGroup) ? "Other" : op.SwaggerGroup)
|
||||||
|
.OrderBy(g => g.Key);
|
||||||
|
|
||||||
|
foreach (var group in groups)
|
||||||
|
{
|
||||||
|
GroupedSwaggerOperations.Add(new SwaggerGroupViewModel
|
||||||
|
{
|
||||||
|
GroupName = group.Key,
|
||||||
|
Operations = new NodifyObservableCollection<OperationInfoViewModel>(group.ToList())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void SaveSwaggerNodesToDb(List<OperationInfoViewModel> nodes, string swaggerFileName)
|
private void SaveSwaggerNodesToDb(List<OperationInfoViewModel> nodes, string swaggerFileName)
|
||||||
{
|
{
|
||||||
using var db = new LiteDbHelper<SwaggerNodeModel>("SwaggerNodes");
|
using var db = new LiteDbHelper<SwaggerNodeModel>("SwaggerNodes");
|
||||||
@@ -535,7 +589,8 @@ namespace Nodify.Calculator
|
|||||||
OPType = node.OPType ?? string.Empty,
|
OPType = node.OPType ?? string.Empty,
|
||||||
InputNames = new List<string>(node.Input),
|
InputNames = new List<string>(node.Input),
|
||||||
SwaggerFileName = swaggerFileName,
|
SwaggerFileName = swaggerFileName,
|
||||||
ResponseModelClassName = node.ResponseModelClassName ?? string.Empty
|
ResponseModelClassName = node.ResponseModelClassName ?? string.Empty,
|
||||||
|
SwaggerGroup = node.SwaggerGroup ?? string.Empty
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -549,11 +604,16 @@ namespace Nodify.Calculator
|
|||||||
|
|
||||||
foreach (var saved in savedNodes)
|
foreach (var saved in savedNodes)
|
||||||
{
|
{
|
||||||
|
var savedGroup = saved.SwaggerGroup;
|
||||||
|
if (string.IsNullOrEmpty(savedGroup))
|
||||||
|
savedGroup = ExtractGroupFromPath(saved.Title ?? "");
|
||||||
|
|
||||||
var ovmodel = new OperationInfoViewModel
|
var ovmodel = new OperationInfoViewModel
|
||||||
{
|
{
|
||||||
Title = saved.Title,
|
Title = saved.Title,
|
||||||
OPType = saved.OPType,
|
OPType = saved.OPType,
|
||||||
Type = OperationType.API
|
Type = OperationType.API,
|
||||||
|
SwaggerGroup = savedGroup
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var inputName in saved.InputNames)
|
foreach (var inputName in saved.InputNames)
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ namespace Nodify.Calculator
|
|||||||
PARSEJSON,
|
PARSEJSON,
|
||||||
SPLIT,
|
SPLIT,
|
||||||
AUTH,
|
AUTH,
|
||||||
FUNCTION
|
FUNCTION,
|
||||||
|
DEBUG,
|
||||||
|
NEW_OBJECT
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SystemOperationViewModel : OperationViewModel
|
public class SystemOperationViewModel : OperationViewModel
|
||||||
|
|||||||
19
Examples/Nodify.Calculator/UpperCaseConverter.cs
Normal file
19
Examples/Nodify.Calculator/UpperCaseConverter.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Windows.Data;
|
||||||
|
|
||||||
|
namespace Nodify.Calculator
|
||||||
|
{
|
||||||
|
public class UpperCaseConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
return value is string s ? s.ToUpper() : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user