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,15 @@
<Application x:Class="Nodify.StateMachine.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/Nodify;component/Themes/Light.xaml" />
<ResourceDictionary Source="pack://application:,,,/Nodify.Shared;component/Themes/Icons.xaml" />
<ResourceDictionary Source="pack://application:,,,/Nodify.Shared;component/Themes/Light.xaml" />
<ResourceDictionary Source="pack://application:,,,/Nodify.StateMachine;component/Themes/Light.xaml" />
</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.StateMachine
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}
}

View File

@@ -0,0 +1,11 @@
using System;
namespace Nodify.StateMachine
{
// Condition or Action reference
public class BlackboardItemReferenceViewModel
{
public string? Name { get; set; }
public Type? Type { get; set; }
}
}

View File

@@ -0,0 +1,51 @@
using System;
namespace Nodify.StateMachine
{
public class BlackboardItemViewModel : ObservableObject
{
private string? _name;
public string? Name
{
get => _name;
set => SetProperty(ref _name, value);
}
private Type? _type;
public Type? Type
{
get => _type;
set => SetProperty(ref _type, value);
}
private NodifyObservableCollection<BlackboardKeyViewModel> _input = new NodifyObservableCollection<BlackboardKeyViewModel>();
public NodifyObservableCollection<BlackboardKeyViewModel> Input
{
get => _input;
set
{
if (value == null)
{
value = new NodifyObservableCollection<BlackboardKeyViewModel>();
}
SetProperty(ref _input!, value);
}
}
private NodifyObservableCollection<BlackboardKeyViewModel> _output = new NodifyObservableCollection<BlackboardKeyViewModel>();
public NodifyObservableCollection<BlackboardKeyViewModel> Output
{
get => _output;
set
{
if (value == null)
{
value = new NodifyObservableCollection<BlackboardKeyViewModel>();
}
SetProperty(ref _output!, value);
}
}
}
}

View File

@@ -0,0 +1,152 @@
<UserControl x:Class="Nodify.StateMachine.BlackboardKeyEditorView"
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.StateMachine"
xmlns:shared="clr-namespace:Nodify;assembly=Nodify.Shared"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type={x:Type local:BlackboardKeyEditorViewModel}, IsDesignTimeCreatable=True}"
d:Background="{DynamicResource PanelBackgroundBrush}"
d:DesignWidth="400">
<UserControl.Resources>
<DataTemplate x:Key="BooleanTemplate"
DataType="{x:Type local:BlackboardKeyEditorViewModel}">
<CheckBox IsChecked="{Binding Target.Value}"
HorizontalAlignment="Left"
VerticalAlignment="Center" />
</DataTemplate>
<DataTemplate x:Key="IntegerTemplate"
DataType="{x:Type local:BlackboardKeyEditorViewModel}">
<TextBox Text="{Binding Target.Value, UpdateSourceTrigger=LostFocus}" />
</DataTemplate>
<DataTemplate x:Key="DoubleTemplate"
DataType="{x:Type local:BlackboardKeyEditorViewModel}">
<TextBox Text="{Binding Target.Value, UpdateSourceTrigger=LostFocus}" />
</DataTemplate>
<DataTemplate x:Key="StringTemplate"
DataType="{x:Type local:BlackboardKeyEditorViewModel}">
<TextBox Text="{Binding Target.Value, UpdateSourceTrigger=LostFocus}" />
</DataTemplate>
<DataTemplate x:Key="ObjectTemplate"
DataType="{x:Type local:BlackboardKeyEditorViewModel}">
<TextBox Text="{Binding Target.Value, UpdateSourceTrigger=LostFocus}"
IsEnabled="False" />
</DataTemplate>
<DataTemplate x:Key="KeyTemplate"
DataType="{x:Type local:BlackboardKeyEditorViewModel}">
<ComboBox SelectedItem="{Binding Target.Value}"
DisplayMemberPath="Name">
<ComboBox.ItemsSource>
<MultiBinding Converter="{local:FilterBlackboardKeysConverter}">
<Binding Path="AvailableKeys" />
<Binding Path="Target.Type" />
<!--USED TO NOTIFY OF COLLECTION CHANGED-->
<Binding Path="AvailableKeys.Count" />
</MultiBinding>
</ComboBox.ItemsSource>
</ComboBox>
</DataTemplate>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"
SharedSizeGroup="KeyName" />
<ColumnDefinition Width="Auto"
SharedSizeGroup="KeyType" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<shared:EditableTextBlock Text="{Binding Target.Name}"
d:Text="My blackboard key"
IsEditing="{Binding IsEditing}"
Foreground="{DynamicResource ForegroundBrush}"
VerticalAlignment="Stretch"
VerticalContentAlignment="Center"
Margin="1 1 5 1" />
<ComboBox ItemsSource="{Binding Target.Type, Converter={shared:EnumValuesConverter}}"
IsEnabled="{Binding CanChangeKeyType}"
SelectedValue="{Binding Target.Type}"
SelectedValuePath="Value"
DisplayMemberPath="Name"
Grid.Column="1"
Margin="0 0 5 0" />
<Grid Grid.Column="2">
<Grid.ColumnDefinitions>
<ColumnDefinition MaxWidth="150" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentControl Content="{Binding}">
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}">
<Style.Triggers>
<DataTrigger Binding="{Binding Target.Type}"
Value="Boolean">
<Setter Property="ContentTemplate"
Value="{StaticResource BooleanTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding Target.Type}"
Value="Integer">
<Setter Property="ContentTemplate"
Value="{StaticResource IntegerTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding Target.Type}"
Value="Double">
<Setter Property="ContentTemplate"
Value="{StaticResource DoubleTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding Target.Type}"
Value="String">
<Setter Property="ContentTemplate"
Value="{StaticResource StringTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding Target.Type}"
Value="Object">
<Setter Property="ContentTemplate"
Value="{StaticResource ObjectTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding Target.Type}"
Value="Key">
<Setter Property="ContentTemplate"
Value="{StaticResource KeyTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding Target.ValueIsKey}"
Value="True">
<Setter Property="ContentTemplate"
Value="{StaticResource KeyTemplate}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
<CheckBox Visibility="{Binding CanChangeInputType, Converter={shared:BooleanToVisibilityConverter}}"
IsChecked="{Binding Target.ValueIsKey}"
ToolTip="Toggle input type"
Grid.Column="1">
<CheckBox.Style>
<Style TargetType="{x:Type CheckBox}"
BasedOn="{StaticResource IconCheckBox}">
<Setter Property="Content"
Value="{StaticResource DiamondIcon}" />
<Style.Triggers>
<Trigger Property="IsChecked"
Value="True">
<Setter Property="Content"
Value="{StaticResource DiamondFillIcon}" />
</Trigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
</CheckBox>
</Grid>
</Grid>
</UserControl>

View File

@@ -0,0 +1,12 @@
using System.Windows.Controls;
namespace Nodify.StateMachine
{
public partial class BlackboardKeyEditorView : UserControl
{
public BlackboardKeyEditorView()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,42 @@
using System.Collections.Generic;
namespace Nodify.StateMachine
{
public class BlackboardKeyEditorViewModel : ObservableObject
{
private ICollection<BlackboardKeyViewModel>? _availableKeys;
public ICollection<BlackboardKeyViewModel>? AvailableKeys
{
get => _availableKeys;
set => SetProperty(ref _availableKeys, value);
}
private BlackboardKeyViewModel? _target;
public BlackboardKeyViewModel? Target
{
get => _target;
set => SetProperty(ref _target, value);
}
private bool _canChangeInputType;
public bool CanChangeInputType
{
get => _canChangeInputType;
set => SetProperty(ref _canChangeInputType, value);
}
private bool _canChangeKeyType = true;
public bool CanChangeKeyType
{
get => _canChangeKeyType;
set => SetProperty(ref _canChangeKeyType, value);
}
private bool _isEditing;
public bool IsEditing
{
get => _isEditing;
set => SetProperty(ref _isEditing, value);
}
}
}

View File

@@ -0,0 +1,107 @@
using System.Collections.Generic;
namespace Nodify.StateMachine
{
public class BlackboardKeyViewModel : ObservableObject
{
// Cache the key and the input value so we can restore them when swapping input types
private readonly Dictionary<bool, object?> _values = new Dictionary<bool, object?>();
public string? PropertyName { get; set; }
private string _name = "New key";
public string Name
{
get => _name;
set
{
if (!string.IsNullOrWhiteSpace(value))
{
SetProperty(ref _name, value);
}
}
}
private BlackboardKeyType _type;
public BlackboardKeyType Type
{
get => _type;
set
{
if (SetProperty(ref _type, value))
{
Value = GetDefaultValue(_type);
}
}
}
private object? _value = BoxValue.False;
public object? Value
{
get => _value;
set => SetProperty(ref _value, GetRealValue(value)).Then(() => _values[ValueIsKey] = Value);
}
private bool _valueIsKey;
public bool ValueIsKey
{
get => _valueIsKey;
set
{
if (SetProperty(ref _valueIsKey, value) && _values.TryGetValue(_valueIsKey, out var existingValue))
{
Value = existingValue;
}
}
}
private bool _canChangeType = true;
public bool CanChangeType
{
get => _canChangeType;
set => SetProperty(ref _canChangeType, value);
}
private object? GetRealValue(object? value)
{
if (value is string str)
{
switch (Type)
{
case BlackboardKeyType.Boolean:
bool.TryParse(str, out var b);
value = b;
break;
case BlackboardKeyType.Integer:
int.TryParse(str, out var i);
value = i;
break;
case BlackboardKeyType.Double:
double.TryParse(str, out var d);
value = d;
break;
case BlackboardKeyType.String:
case BlackboardKeyType.Object:
value = str;
break;
}
}
return value;
}
public static object? GetDefaultValue(BlackboardKeyType type)
=> type switch
{
BlackboardKeyType.Boolean => BoxValue.False,
BlackboardKeyType.Integer => BoxValue.Int0,
BlackboardKeyType.Double => BoxValue.Double0,
BlackboardKeyType.String => null,
BlackboardKeyType.Object => null,
_ => null
};
}
}

View File

@@ -0,0 +1,47 @@
using System.Linq;
namespace Nodify.StateMachine
{
public class BlackboardViewModel : ObservableObject
{
private NodifyObservableCollection<BlackboardKeyViewModel> _keys = new NodifyObservableCollection<BlackboardKeyViewModel>();
public NodifyObservableCollection<BlackboardKeyViewModel> Keys
{
get => _keys;
set => SetProperty(ref _keys, value);
}
private NodifyObservableCollection<BlackboardItemReferenceViewModel> _actions = new NodifyObservableCollection<BlackboardItemReferenceViewModel>();
public NodifyObservableCollection<BlackboardItemReferenceViewModel> Actions
{
get => _actions;
set => SetProperty(ref _actions, value);
}
private NodifyObservableCollection<BlackboardItemReferenceViewModel> _conditions = new NodifyObservableCollection<BlackboardItemReferenceViewModel>();
public NodifyObservableCollection<BlackboardItemReferenceViewModel> Conditions
{
get => _conditions;
set => SetProperty(ref _conditions, value);
}
public INodifyCommand AddKeyCommand { get; }
public INodifyCommand RemoveKeyCommand { get; }
public BlackboardViewModel()
{
AddKeyCommand = new DelegateCommand(() => Keys.Add(new BlackboardKeyViewModel
{
Name = "New Key "
}));
RemoveKeyCommand = new DelegateCommand<BlackboardKeyViewModel>(key => Keys.Remove(key));
Keys.WhenAdded(key =>
{
var existingKeyNames = Keys.Where(k => k != key).Select(k => k.Name).ToList();
key.Name = existingKeyNames.GetUnique(key.Name);
});
}
}
}

View File

@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Markup;
namespace Nodify.StateMachine
{
public class BlackboardKeyEditorConverter : MarkupExtension, IMultiValueConverter
{
public bool CanChangeInputType { get; set; }
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length >= 2 && values[0] is ICollection<BlackboardKeyViewModel> availableKeys && values[1] is BlackboardKeyViewModel target)
{
return new BlackboardKeyEditorViewModel
{
AvailableKeys = availableKeys,
Target = target,
IsEditing = values.Length >= 3 && values[2] is bool b && b,
CanChangeInputType = CanChangeInputType && (target.Type != BlackboardKeyType.Object || target.CanChangeType),
CanChangeKeyType = target.CanChangeType
};
}
return values;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
public override object ProvideValue(IServiceProvider serviceProvider) => this;
}
}

View File

@@ -0,0 +1,32 @@
using System;
using System.Windows;
using System.Globalization;
using System.Windows.Data;
namespace Nodify.StateMachine
{
public class ConnectorOffsetConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
double offset = System.Convert.ToDouble(parameter);
if (value is Size s)
{
return new Size((s.Width + offset) / 2, (s.Height + offset) / 2);
}
return new Size(offset / 2, offset / 2);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
double offset = System.Convert.ToDouble(parameter);
if (value is Size s)
{
return new Size((s.Width + offset) / 2, (s.Height + offset) / 2);
}
return new Size(offset / 2, offset / 2);
}
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows.Data;
using System.Windows.Markup;
namespace Nodify.StateMachine
{
public class FilterBlackboardKeysConverter : MarkupExtension, IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length >= 2 && values[0] is IEnumerable<BlackboardKeyViewModel> keys && values[1] is BlackboardKeyType filter)
{
return keys.Where(k => k.Type == filter || filter == BlackboardKeyType.Object);
}
return values;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
public override object ProvideValue(IServiceProvider serviceProvider) => this;
}
}

View File

@@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace Nodify.StateMachine
{
public static class BlackboardDescriptor
{
private class KeyDescription
{
public KeyDescription(string displayName, string propertyName, BlackboardKeyType type, bool canChangeType)
{
DisplayName = displayName;
PropertyName = propertyName;
Type = type;
CanChangeType = canChangeType;
}
public string DisplayName { get; }
public string PropertyName { get; }
public BlackboardKeyType Type { get; }
public bool CanChangeType { get; }
}
private class ItemDescription
{
public string? Name { get; set; }
public List<KeyDescription> Input { get; } = new List<KeyDescription>();
public List<KeyDescription> Output { get; } = new List<KeyDescription>();
}
public static BlackboardItemViewModel? GetItem(BlackboardItemReferenceViewModel? actionRef)
{
if (actionRef?.Type != null)
{
var description = GetDescription(actionRef.Type);
var input = description.Input.Select(d => new BlackboardKeyViewModel
{
Name = d.DisplayName,
Type = d.Type,
PropertyName = d.PropertyName,
CanChangeType = d.CanChangeType,
ValueIsKey = true
});
var output = description.Output.Select(d => new BlackboardKeyViewModel
{
Name = d.DisplayName,
Type = d.Type,
PropertyName = d.PropertyName,
CanChangeType = d.CanChangeType,
ValueIsKey = true
});
return new BlackboardItemViewModel
{
Name = actionRef.Name,
Type = actionRef.Type,
Input = new NodifyObservableCollection<BlackboardKeyViewModel>(input),
Output = new NodifyObservableCollection<BlackboardKeyViewModel>(output),
};
}
return default;
}
public static BlackboardItemReferenceViewModel GetReference(Type type)
{
var desc = GetDescription(type);
return new BlackboardItemReferenceViewModel
{
Name = desc.Name,
Type = type
};
}
private static readonly Dictionary<Type, ItemDescription> _descriptions = new Dictionary<Type, ItemDescription>();
private static ItemDescription GetDescription(Type type)
{
if (!_descriptions.TryGetValue(type, out var description))
{
var actionAttr = type.GetCustomAttribute<BlackboardItemAttribute>();
var desc = new ItemDescription
{
Name = actionAttr?.DisplayName ?? type.Name
};
var props = type.GetProperties();
for (int i = 0; i < props.Length; i++)
{
var prop = props[i];
var keyAttr = prop.GetCustomAttribute<BlackboardPropertyAttribute>();
if (keyAttr != null)
{
var key = new KeyDescription(keyAttr.Name ?? prop.Name, prop.Name, keyAttr.Type, keyAttr.CanChangeType);
if (keyAttr.Usage == BlackboardKeyUsage.Input)
{
desc.Input.Add(key);
}
else
{
desc.Output.Add(key);
}
}
}
_descriptions.Add(type, desc);
return desc;
}
return description;
}
public static List<BlackboardItemReferenceViewModel> GetAvailableItems<T>()
{
var result = new List<BlackboardItemReferenceViewModel>();
var ourType = typeof(T);
var types = ourType.Assembly.GetTypes();
for (int i = 0; i < types.Length; i++)
{
var type = types[i];
if (type.IsClass && !type.IsAbstract && ourType.IsAssignableFrom(type) && type.GetCustomAttribute<BlackboardItemAttribute>() != null)
{
result.Add(GetReference(type));
}
}
return result;
}
}
}

View File

@@ -0,0 +1,744 @@
<Window x:Class="Nodify.StateMachine.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.StateMachine"
xmlns:shared="clr-namespace:Nodify;assembly=Nodify.Shared"
xmlns:nodify="https://miroiu.github.io/nodify"
ResizeMode="CanResizeWithGrip"
mc:Ignorable="d"
Background="{DynamicResource NodifyEditor.BackgroundBrush}"
Foreground="{DynamicResource ForegroundBrush}"
Title="State Machine Editor"
Height="500"
Width="930">
<Window.DataContext>
<local:StateMachineViewModel />
</Window.DataContext>
<Window.Resources>
<shared:BindingProxy x:Key="EditorProxy"
DataContext="{Binding}" />
<shared:BindingProxy x:Key="BlackboardProxy"
DataContext="{Binding Blackboard}" />
<local:ConnectorOffsetConverter x:Key="ConnectorOffsetConverter" />
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ScrollViewer CanContentScroll="True"
PreviewKeyDown="ScrollViewer_PreviewKeyDown"
Grid.Column="1">
<nodify:NodifyEditor x:Name="Editor"
ItemsSource="{Binding States}"
SelectedItem="{Binding SelectedState}"
SelectedItems="{Binding SelectedStates}"
Connections="{Binding Transitions}"
PendingConnection="{Binding PendingTransition}"
DisconnectConnectorCommand="{Binding DisconnectStateCommand}"
ConnectionCompletedCommand="{Binding CreateTransitionCommand}"
RemoveConnectionCommand="{Binding DeleteTransitionCommand}">
<nodify:NodifyEditor.PendingConnectionTemplate>
<DataTemplate DataType="{x:Type local:TransitionViewModel}">
<nodify:PendingConnection Source="{Binding Source, Mode=OneWayToSource}"
Target="{Binding Target, Mode=OneWayToSource}"
StrokeDashArray=""
EnablePreview="True">
<nodify:PendingConnection.Template>
<ControlTemplate TargetType="{x:Type nodify:PendingConnection}">
<nodify:LineConnection Source="{TemplateBinding SourceAnchor}"
Target="{TemplateBinding TargetAnchor}"
StrokeThickness="{TemplateBinding StrokeThickness}"
StrokeDashArray="{TemplateBinding StrokeDashArray}"
SourceOffset="{Binding Source.Size, Converter={StaticResource ConnectorOffsetConverter}, ConverterParameter=5}"
Spacing="0"
SourceOffsetMode="Edge"
TargetOffsetMode="None" />
</ControlTemplate>
</nodify:PendingConnection.Template>
</nodify:PendingConnection>
</DataTemplate>
</nodify:NodifyEditor.PendingConnectionTemplate>
<nodify:NodifyEditor.ConnectionTemplate>
<DataTemplate DataType="{x:Type local:TransitionViewModel}">
<nodify:LineConnection Source="{Binding Source.Anchor}"
Target="{Binding Target.Anchor}"
SourceOffset="{Binding Source.Size, Converter={StaticResource ConnectorOffsetConverter}, ConverterParameter=5}"
TargetOffset="{Binding Target.Size, Converter={StaticResource ConnectorOffsetConverter}, ConverterParameter=5}"
Spacing="0"
SourceOffsetMode="Edge"
TargetOffsetMode="Edge"
OutlineThickness="5"
Tag="{Binding}">
<nodify:LineConnection.Style>
<Style TargetType="{x:Type nodify:LineConnection}"
BasedOn="{StaticResource {x:Type nodify:LineConnection}}">
<Setter Property="OutlineBrush"
Value="Transparent" />
<Setter Property="StrokeThickness"
Value="3" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsActive}"
Value="True">
<Setter Property="Stroke"
Value="{DynamicResource ActiveStateBrush}" />
<Setter Property="StrokeThickness"
Value="6" />
</DataTrigger>
<Trigger Property="IsMouseOver"
Value="True">
<Setter Property="OutlineBrush">
<Setter.Value>
<SolidColorBrush Color="{StaticResource LineConnection.StrokeColor}"
Opacity="0.15" />
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
</nodify:LineConnection.Style>
<nodify:LineConnection.ContextMenu>
<ContextMenu DataContext="{Binding DataContext, Source={StaticResource EditorProxy}}">
<MenuItem Header="_Delete"
Icon="{StaticResource DeleteIcon}"
Command="{Binding DeleteTransitionCommand}"
CommandParameter="{Binding PlacementTarget.Tag, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}" />
</ContextMenu>
</nodify:LineConnection.ContextMenu>
</nodify:LineConnection>
</DataTemplate>
</nodify:NodifyEditor.ConnectionTemplate>
<nodify:NodifyEditor.ItemTemplate>
<DataTemplate DataType="{x:Type local:StateViewModel}">
<!--If IsConnected is false, Anchor won't be updated-->
<nodify:StateNode Content="{Binding}"
IsConnected="True"
Anchor="{Binding Anchor, Mode=OneWayToSource}">
<nodify:StateNode.ContentTemplate>
<DataTemplate DataType="{x:Type local:StateViewModel}">
<shared:EditableTextBlock Text="{Binding Name}"
IsEditing="{Binding IsRenaming}"
IsEditable="{Binding IsEditable}"
MaxLength="30" />
</DataTemplate>
</nodify:StateNode.ContentTemplate>
<nodify:StateNode.Style>
<Style TargetType="{x:Type nodify:StateNode}"
BasedOn="{StaticResource {x:Type nodify:StateNode}}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsEditable}"
Value="False">
<Setter Property="BorderBrush"
Value="{DynamicResource ReadOnlyStateBrush}" />
</DataTrigger>
<DataTrigger Binding="{Binding IsActive}"
Value="True">
<Setter Property="BorderBrush"
Value="{DynamicResource ActiveStateBrush}" />
</DataTrigger>
</Style.Triggers>
</Style>
</nodify:StateNode.Style>
</nodify:StateNode>
</DataTemplate>
</nodify:NodifyEditor.ItemTemplate>
<nodify:NodifyEditor.ItemContainerStyle>
<Style TargetType="{x:Type nodify:ItemContainer}"
BasedOn="{StaticResource {x:Type nodify:ItemContainer}}">
<Setter Property="BorderBrush"
Value="Transparent" />
<Setter Property="Location"
Value="{Binding Location}" />
<Setter Property="ActualSize"
Value="{Binding Size, Mode=OneWayToSource}" />
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu DataContext="{Binding DataContext, Source={StaticResource EditorProxy}}">
<MenuItem Header="_Delete"
Icon="{StaticResource DeleteIcon}"
Command="{Binding DeleteSelectionCommand}" />
<MenuItem Header="Di_sconnect"
Icon="{StaticResource DisconnectIcon}"
Command="{Binding DisconnectSelectionCommand}" />
<MenuItem Header="_Rename"
Icon="{StaticResource RenameIcon}"
Command="{Binding RenameStateCommand}" />
<MenuItem Header="_Lock"
Icon="{StaticResource LockIcon}"
Command="{x:Static nodify:EditorCommands.LockSelection}" />
<MenuItem Header="_Unlock"
Icon="{StaticResource UnlockIcon}"
Command="{x:Static nodify:EditorCommands.UnlockSelection}" />
<MenuItem Header="_Alignment"
Icon="{StaticResource AlignTopIcon}">
<MenuItem Header="_Top"
Icon="{StaticResource AlignTopIcon}"
Command="{x:Static nodify:EditorCommands.Align}"
CommandParameter="Top" />
<MenuItem Header="_Left"
Icon="{StaticResource AlignLeftIcon}"
Command="{x:Static nodify:EditorCommands.Align}"
CommandParameter="Left" />
<MenuItem Header="_Bottom"
Icon="{StaticResource AlignBottomIcon}"
Command="{x:Static nodify:EditorCommands.Align}"
CommandParameter="Bottom" />
<MenuItem Header="_Right"
Icon="{StaticResource AlignRightIcon}"
Command="{x:Static nodify:EditorCommands.Align}"
CommandParameter="Right" />
<MenuItem Header="_Middle"
Icon="{StaticResource AlignMiddleIcon}"
Command="{x:Static nodify:EditorCommands.Align}"
CommandParameter="Middle" />
<MenuItem Header="_Center"
Icon="{StaticResource AlignCenterIcon}"
Command="{x:Static nodify:EditorCommands.Align}"
CommandParameter="Center" />
</MenuItem>
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</nodify:NodifyEditor.ItemContainerStyle>
<nodify:NodifyEditor.ContextMenu>
<ContextMenu DataContext="{Binding DataContext, Source={StaticResource EditorProxy}}">
<MenuItem Header="_Add State"
Icon="{StaticResource AddStateIcon}"
InputGestureText="Shift+A"
Command="{Binding AddStateCommand}"
CommandParameter="{Binding PlacementTarget.MouseLocation, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}" />
<MenuItem Header="_Delete"
Icon="{StaticResource DeleteIcon}"
InputGestureText="Delete"
Command="{Binding DeleteSelectionCommand}" />
<Separator Background="{DynamicResource BorderBrush}" />
<MenuItem Header="_Select All"
Icon="{StaticResource SelectAllIcon}"
InputGestureText="Ctrl+A"
Command="{x:Static nodify:EditorCommands.SelectAll}" />
</ContextMenu>
</nodify:NodifyEditor.ContextMenu>
<nodify:NodifyEditor.InputBindings>
<KeyBinding Key="Delete"
Command="{Binding DeleteSelectionCommand}" />
<KeyBinding Key="A"
Modifiers="Shift"
Command="{Binding AddStateCommand}"
CommandParameter="{Binding MouseLocation, RelativeSource={RelativeSource AncestorType={x:Type nodify:NodifyEditor}}}" />
</nodify:NodifyEditor.InputBindings>
</nodify:NodifyEditor>
</ScrollViewer>
<!--TOOLBAR-->
<Border CornerRadius="2"
Background="{DynamicResource PanelBackgroundBrush}"
BorderThickness="0 0 0 1"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Margin="10 0"
Grid.Column="1">
<StackPanel Orientation="Horizontal">
<Button Command="{Binding PauseCommand}">
<Button.Style>
<Style TargetType="{x:Type Button}"
BasedOn="{StaticResource IconButton}">
<Setter Property="Content"
Value="{StaticResource PauseIcon}" />
<Setter Property="ToolTip"
Value="Pause" />
<Style.Triggers>
<DataTrigger Binding="{Binding Runner.State}"
Value="Stopped">
<Setter Property="Visibility"
Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding Runner.State}"
Value="Paused">
<Setter Property="Content"
Value="{StaticResource UnpauseIcon}" />
<Setter Property="ToolTip"
Value="Continue" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
<Button Command="{Binding RunCommand}"
Style="{StaticResource IconButton}">
<StackPanel Orientation="Horizontal">
<ContentPresenter DataContext="{Binding}"
Margin="0 0 4 0">
<ContentPresenter.Style>
<Style TargetType="{x:Type ContentPresenter}">
<Setter Property="Content"
Value="{StaticResource StopIcon}" />
<Setter Property="ToolTip"
Value="Stop" />
<Style.Triggers>
<DataTrigger Binding="{Binding Runner.State}"
Value="Stopped">
<Setter Property="Content"
Value="{StaticResource RunIcon}" />
<Setter Property="ToolTip"
Value="Run" />
</DataTrigger>
</Style.Triggers>
</Style>
</ContentPresenter.Style>
</ContentPresenter>
<TextBlock Text="{Binding Name}" />
</StackPanel>
</Button>
<Separator Height="Auto"
BorderThickness="0 0 1 0" />
<Button Content="{StaticResource ZoomInIcon}"
Command="{x:Static nodify:EditorCommands.ZoomIn}"
CommandTarget="{Binding ElementName=Editor}"
ToolTip="Zoom In"
Style="{StaticResource IconButton}" />
<Button Content="{StaticResource ZoomOutIcon}"
Command="{x:Static nodify:EditorCommands.ZoomOut}"
CommandTarget="{Binding ElementName=Editor}"
ToolTip="Zoom Out"
Style="{StaticResource IconButton}" />
<Button Style="{StaticResource IconButton}"
Content="{StaticResource ThemeIcon}"
Command="{Binding Source={x:Static shared:ThemeManager.SetNextThemeCommand}}"
ToolTip="Change theme" />
</StackPanel>
</Border>
<!--Settings-->
<Expander HorizontalContentAlignment="Left"
VerticalContentAlignment="Center"
HorizontalAlignment="Left"
Background="{DynamicResource PanelBackgroundBrush}"
Padding="0 1 4 3"
IsExpanded="True"
ExpandDirection="Left">
<Expander.Style>
<Style TargetType="{x:Type Expander}"
BasedOn="{StaticResource {x:Type Expander}}">
<Setter Property="Tag"
Value="{StaticResource ExpandRightIcon}" />
<Style.Triggers>
<Trigger Property="IsExpanded"
Value="True">
<Setter Property="Tag"
Value="{StaticResource ExpandLeftIcon}" />
</Trigger>
</Style.Triggers>
</Style>
</Expander.Style>
<Border BorderBrush="{DynamicResource BackgroundBrush}"
BorderThickness="1"
Width="300"
Padding="10"
HorizontalAlignment="Stretch">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="2*" />
</Grid.RowDefinitions>
<!--TRANSITIONS-->
<ScrollViewer Visibility="{Binding SelectedState, Converter={shared:BooleanToVisibilityConverter Negate=True}}"
VerticalScrollBarVisibility="Auto"
Grid.Row="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="Transitions"
Foreground="{DynamicResource ForegroundBrush}"
FontWeight="Bold"
FontSize="16" />
<Separator Height="2"
Width="Auto"
Margin="0 2 0 5"
Grid.Row="1" />
<ItemsControl ItemsSource="{Binding Transitions}"
Grid.Row="2"
Focusable="False">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:TransitionViewModel}">
<Expander BorderThickness="0 0 0 1"
Padding="0 5 0 0"
IsExpanded="True"
BorderBrush="{DynamicResource BackgroundBrush}">
<Expander.Style>
<Style TargetType="{x:Type Expander}"
BasedOn="{StaticResource {x:Type Expander}}">
<Setter Property="Tag"
Value="{StaticResource ExpandRightIcon}" />
<Style.Triggers>
<Trigger Property="IsExpanded"
Value="True">
<Setter Property="Tag"
Value="{StaticResource ExpandDownIcon}" />
</Trigger>
</Style.Triggers>
</Style>
</Expander.Style>
<Expander.Header>
<TextBlock>
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Foreground"
Value="{DynamicResource ForegroundBrush}" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsActive}"
Value="True">
<Setter Property="Foreground"
Value="{DynamicResource ActiveStateBrush}" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
<Run Text="{Binding Source.Name, Mode=OneWay}" />
<Run Text="🠚" />
<Run Text="{Binding Target.Name, Mode=OneWay}" />
</TextBlock>
</Expander.Header>
<Border HorizontalAlignment="Stretch">
<Grid IsSharedSizeScope="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"
SharedSizeGroup="ConditionName" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!--CONDITION-->
<TextBlock Text="Condition"
Margin="0 0 10 0"
VerticalAlignment="Center" />
<ComboBox ItemsSource="{Binding DataContext.Blackboard.Conditions, Source={StaticResource EditorProxy}}"
SelectedItem="{Binding ConditionReference}"
DisplayMemberPath="Name"
Grid.Column="1" />
<!--INPUT-->
<ItemsControl ItemsSource="{Binding Condition.Input}"
Padding="0 5 0 0"
Grid.Row="1"
Grid.ColumnSpan="2">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:BlackboardKeyViewModel}">
<local:BlackboardKeyEditorView Margin="0 0 0 2">
<local:BlackboardKeyEditorView.DataContext>
<MultiBinding Converter="{local:BlackboardKeyEditorConverter CanChangeInputType=True}">
<Binding Source="{StaticResource BlackboardProxy}"
Path="DataContext.Keys" />
<Binding BindsDirectlyToSource="True" />
</MultiBinding>
</local:BlackboardKeyEditorView.DataContext>
</local:BlackboardKeyEditorView>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Grid Grid.Row="2"
Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<TextBlock Text="From: "
VerticalAlignment="Center" />
<Button Style="{StaticResource IconButton}"
Command="{x:Static nodify:EditorCommands.BringIntoView}"
CommandParameter="{Binding Source.Location}"
CommandTarget="{Binding ElementName=Editor}"
Foreground="DodgerBlue">
<TextBlock Text="{Binding Source.Name}"
TextDecorations="Underline" />
</Button>
</StackPanel>
<StackPanel Orientation="Horizontal"
Grid.Column="1">
<TextBlock Text="To: "
VerticalAlignment="Center" />
<Button Style="{StaticResource IconButton}"
Command="{x:Static nodify:EditorCommands.BringIntoView}"
CommandParameter="{Binding Target.Location}"
CommandTarget="{Binding ElementName=Editor}"
Foreground="DodgerBlue">
<TextBlock Text="{Binding Target.Name}"
TextDecorations="Underline" />
</Button>
</StackPanel>
</Grid>
</Grid>
</Border>
</Expander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</ScrollViewer>
<!--STATES-->
<Grid Visibility="{Binding SelectedState, Converter={shared:BooleanToVisibilityConverter}}"
Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<!--STATE NAME-->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<shared:EditableTextBlock Text="{Binding SelectedState.Name}"
IsEditing="{Binding IsChecked, ElementName=EditStateName}"
IsEditable="{Binding SelectedState.IsEditable}"
Foreground="{DynamicResource ForegroundBrush}"
FontWeight="Bold"
FontSize="16"
MaxLength="20" />
<CheckBox x:Name="EditStateName"
Visibility="{Binding SelectedState.IsEditable, Converter={shared:BooleanToVisibilityConverter}}"
Content="{StaticResource EditIcon}"
Style="{StaticResource IconCheckBox}"
Grid.Column="1" />
</Grid>
<Separator Height="2"
Width="Auto"
Margin="0 2 0 10"
Grid.Row="1" />
<ScrollViewer Visibility="{Binding SelectedState.Action, Converter={shared:BooleanToVisibilityConverter}}"
VerticalScrollBarVisibility="Auto"
Grid.Row="2">
<Grid IsSharedSizeScope="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<!--ACTION-->
<TextBlock Text="Action"
Margin="0 0 10 0"
VerticalAlignment="Center" />
<ComboBox ItemsSource="{Binding Blackboard.Actions}"
SelectedItem="{Binding SelectedState.ActionReference}"
IsEnabled="{Binding SelectedState.IsEditable}"
DisplayMemberPath="Name"
Grid.Column="1" />
<!--INPUT-->
<Expander Margin="0 5 0 0"
Padding="0 5 0 0"
Grid.Row="1"
Grid.ColumnSpan="2"
BorderThickness="0 0 0 1"
Header="Input"
FontWeight="Bold"
IsExpanded="True"
BorderBrush="{DynamicResource BackgroundBrush}"
Visibility="{Binding SelectedState.Action.Input.Count, Converter={shared:BooleanToVisibilityConverter}}">
<Expander.Style>
<Style TargetType="{x:Type Expander}"
BasedOn="{StaticResource {x:Type Expander}}">
<Setter Property="Tag"
Value="{StaticResource ExpandRightIcon}" />
<Style.Triggers>
<Trigger Property="IsExpanded"
Value="True">
<Setter Property="Tag"
Value="{StaticResource ExpandDownIcon}" />
</Trigger>
</Style.Triggers>
</Style>
</Expander.Style>
<ItemsControl ItemsSource="{Binding SelectedState.Action.Input}"
FontWeight="Normal">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:BlackboardKeyViewModel}">
<local:BlackboardKeyEditorView Margin="0 0 0 2">
<local:BlackboardKeyEditorView.DataContext>
<MultiBinding Converter="{local:BlackboardKeyEditorConverter CanChangeInputType=True}">
<Binding Source="{StaticResource BlackboardProxy}"
Path="DataContext.Keys" />
<Binding BindsDirectlyToSource="True" />
</MultiBinding>
</local:BlackboardKeyEditorView.DataContext>
</local:BlackboardKeyEditorView>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Expander>
<!--OUTPUT-->
<Expander Margin="0 5 0 0"
Padding="0 5 0 0"
Grid.Row="2"
Grid.ColumnSpan="2"
Header="Output"
FontWeight="Bold"
BorderThickness="0 0 0 1"
IsExpanded="True"
BorderBrush="{DynamicResource BackgroundBrush}"
Visibility="{Binding SelectedState.Action.Output.Count, Converter={shared:BooleanToVisibilityConverter}}">
<Expander.Style>
<Style TargetType="{x:Type Expander}"
BasedOn="{StaticResource {x:Type Expander}}">
<Setter Property="Tag"
Value="{StaticResource ExpandRightIcon}" />
<Style.Triggers>
<Trigger Property="IsExpanded"
Value="True">
<Setter Property="Tag"
Value="{StaticResource ExpandDownIcon}" />
</Trigger>
</Style.Triggers>
</Style>
</Expander.Style>
<ItemsControl ItemsSource="{Binding SelectedState.Action.Output}"
FontWeight="Normal">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:BlackboardKeyViewModel}">
<local:BlackboardKeyEditorView Margin="0 0 0 2">
<local:BlackboardKeyEditorView.DataContext>
<MultiBinding Converter="{local:BlackboardKeyEditorConverter CanChangeInputType=False}">
<Binding Source="{StaticResource BlackboardProxy}"
Path="DataContext.Keys" />
<Binding BindsDirectlyToSource="True" />
</MultiBinding>
</local:BlackboardKeyEditorView.DataContext>
</local:BlackboardKeyEditorView>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Expander>
</Grid>
</ScrollViewer>
</Grid>
<!--BLACKBOARD-->
<Grid IsSharedSizeScope="True">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<shared:EditableTextBlock Text="{Binding Name}"
IsEditing="{Binding IsChecked, ElementName=EditName}"
Foreground="{DynamicResource ForegroundBrush}"
FontWeight="Bold"
FontSize="16"
MaxLength="20" />
<StackPanel Orientation="Horizontal"
Grid.Column="1">
<CheckBox x:Name="EditName"
Content="{StaticResource EditIcon}"
ToolTip="Edit Name"
Style="{StaticResource IconCheckBox}" />
<Button Content="{StaticResource AddKeyIcon}"
Command="{Binding Blackboard.AddKeyCommand}"
ToolTip="Add New Key"
Style="{StaticResource IconButton}" />
</StackPanel>
</Grid>
<Separator Height="2"
Width="Auto"
Margin="0 2 0 10"
Grid.Row="1" />
<ScrollViewer VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"
Grid.Row="2">
<ItemsControl ItemsSource="{Binding Blackboard.Keys}"
Focusable="False">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:BlackboardKeyViewModel}">
<Grid Margin="0 0 0 2">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition SharedSizeGroup="Actions" />
</Grid.ColumnDefinitions>
<local:BlackboardKeyEditorView>
<local:BlackboardKeyEditorView.DataContext>
<MultiBinding Converter="{local:BlackboardKeyEditorConverter CanChangeInputType=False}">
<Binding Source="{StaticResource BlackboardProxy}"
Path="DataContext.Keys" />
<Binding BindsDirectlyToSource="True" />
<Binding ElementName="EditKeyName"
Path="IsChecked" />
</MultiBinding>
</local:BlackboardKeyEditorView.DataContext>
</local:BlackboardKeyEditorView>
<StackPanel Orientation="Horizontal"
Grid.Column="3">
<CheckBox x:Name="EditKeyName"
Content="{StaticResource EditIcon}"
ToolTip="Edit Name"
Style="{StaticResource IconCheckBox}" />
<Button Content="{StaticResource RemoveKeyIcon}"
Command="{Binding DataContext.Blackboard.RemoveKeyCommand, Source={StaticResource EditorProxy}}"
CommandParameter="{Binding}"
Style="{StaticResource IconButton}" />
</StackPanel>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</Grid>
</Border>
</Expander>
</Grid>
</Window>

View File

@@ -0,0 +1,51 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Nodify.Interactivity;
namespace Nodify.StateMachine
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
ConnectorState.EnableToggledConnectingMode = true;
NodifyEditor.EnableCuttingLinePreview = true;
EditorGestures.Mappings.Connection.Disconnect.Unbind();
EditorGestures.Mappings.Editor.ZoomModifierKey = ModifierKeys.Control;
EditorGestures.Mappings.Editor.PanWithMouseWheel = true;
EventManager.RegisterClassHandler(
typeof(UIElement),
Keyboard.PreviewGotKeyboardFocusEvent,
(KeyboardFocusChangedEventHandler)OnPreviewGotKeyboardFocus);
}
private void OnPreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
Title = e.NewFocus.ToString();
}
private void ScrollViewer_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (Keyboard.Modifiers != ModifierKeys.Shift)
return;
var scrollViewer = (ScrollViewer)sender;
if (e.Key == Key.PageUp)
{
scrollViewer.PageLeft();
e.Handled = true;
}
else if (e.Key == Key.PageDown)
{
scrollViewer.PageRight();
e.Handled = true;
}
}
}
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFrameworks>net9-windows;net8-windows;net6-windows;net5-windows;netcoreapp3.1;net48;net472</TargetFrameworks>
<UseWPF>true</UseWPF>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)'=='net472' OR '$(TargetFramework)'=='net48'">
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Nodify\Nodify.csproj" />
<ProjectReference Include="..\Nodify.Shared\Nodify.Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,25 @@
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
[BlackboardItem("Copy Key")]
public class CopyKeyAction : IBlackboardAction
{
[BlackboardProperty("Source", BlackboardKeyType.Object)]
public BlackboardProperty Source { get; set; }
[BlackboardProperty("Target", BlackboardKeyType.Object)]
public BlackboardProperty Target { get; set; }
public Task Execute(Blackboard blackboard)
{
if (Source != Target && Source.IsKey && Target.IsKey)
{
var value = blackboard[Source];
blackboard[Target] = value;
}
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,22 @@
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
[BlackboardItem("Set Value")]
public class SetKeyValueAction : IBlackboardAction
{
[BlackboardProperty(BlackboardKeyType.Object)]
public BlackboardProperty Key { get; set; }
[BlackboardProperty(BlackboardKeyType.Object, CanChangeType = true)]
public BlackboardProperty Value { get; set; }
public Task Execute(Blackboard blackboard)
{
var value = blackboard.GetValue<int>(Value);
blackboard[Key] = value;
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,28 @@
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
[BlackboardItem("Set State Delay")]
public class SetStateDelayAction : IBlackboardAction
{
[BlackboardProperty("Delay", BlackboardKeyType.Integer)]
public BlackboardProperty Delay { get; set; }
[BlackboardProperty("Success", BlackboardKeyType.Boolean, Usage = BlackboardKeyUsage.Output)]
public BlackboardProperty Success { get; set; }
public Task Execute(Blackboard blackboard)
{
var delay = blackboard.GetValue<int>(Delay);
if (delay.HasValue)
{
blackboard[DebugBlackboardDecorator.StateDelayKey] = delay;
}
blackboard[Success] = delay.HasValue;
return Task.CompletedTask;
}
}
}

View File

@@ -0,0 +1,88 @@
using System.Collections.Generic;
namespace Nodify.StateMachine
{
public class Blackboard
{
private readonly Dictionary<BlackboardKey, object?> _objects = new Dictionary<BlackboardKey, object?>();
public virtual IReadOnlyCollection<BlackboardKey> Keys
=> _objects.Keys;
public virtual T? GetValue<T>(BlackboardKey key)
where T : struct
{
if (_objects.TryGetValue(key, out var value) && value is T result)
{
return result;
}
return default;
}
public virtual T? GetObject<T>(BlackboardKey key)
where T : class
{
if (_objects.TryGetValue(key, out var value))
{
return value as T;
}
return default;
}
public virtual object? GetObject(BlackboardKey key)
{
if (_objects.TryGetValue(key, out var value))
{
return value;
}
return default;
}
public virtual void Set(BlackboardKey key, object? value)
=> _objects[key] = value;
public virtual bool HasKey(BlackboardKey key)
=> _objects.ContainsKey(key);
public virtual void Remove(BlackboardKey key)
=> _objects.Remove(key);
public virtual void Clear()
=> _objects.Clear();
public void CopyTo(Blackboard newBlackboard)
{
foreach (var kvp in _objects)
{
newBlackboard.Set(kvp.Key, kvp.Value);
}
}
public object? this[BlackboardKey key]
{
get => GetObject(key);
set => Set(key, value);
}
public T? GetValue<T>(BlackboardProperty value) where T : struct
=> value.IsValue ? value.GetValue<T>() : GetValue<T>(value.Key);
public T? GetObject<T>(BlackboardProperty value) where T : class
=> value.IsValue ? value.GetObject<T>() : GetObject<T>(value.Key);
public object? GetObject(BlackboardProperty value)
=> value.IsValue ? value.Value : GetObject(value.Key);
}
public static class BlackboardExtensions
{
public static bool IsValid(this BlackboardKey key)
=> key != BlackboardKey.Invalid;
public static bool IsValid(this BlackboardProperty action)
=> action != BlackboardProperty.Invalid;
}
}

View File

@@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
public enum BooleanOperator
{
And,
Or
}
public class BlackboardConditionSet : IBlackboardCondition
{
public BlackboardConditionSet(IEnumerable<IBlackboardCondition> conditions, BooleanOperator op)
{
Conditions = new List<IBlackboardCondition>(conditions);
Operator = op;
}
public IReadOnlyList<IBlackboardCondition> Conditions { get; }
public BooleanOperator Operator { get; set; }
public async Task<bool> Evaluate(Blackboard blackboard)
{
bool result = true;
if (Operator == BooleanOperator.And)
{
for (int i = 0; i < Conditions.Count; i++)
{
result &= await Conditions[i].Evaluate(blackboard);
}
}
else if (Operator == BooleanOperator.Or)
{
for (int i = 0; i < Conditions.Count; i++)
{
result |= await Conditions[i].Evaluate(blackboard);
}
}
return result;
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
namespace Nodify.StateMachine
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class BlackboardItemAttribute : Attribute
{
public BlackboardItemAttribute(string displayName)
{
DisplayName = displayName;
}
public string DisplayName { get; }
}
}

View File

@@ -0,0 +1,54 @@
using System;
using System.Diagnostics;
namespace Nodify.StateMachine
{
public enum BlackboardKeyType
{
Boolean,
Integer,
Double,
String,
Object
}
[DebuggerDisplay("{Name}: {Type}")]
public readonly struct BlackboardKey : IEquatable<BlackboardKey>
{
public static BlackboardKey Invalid { get; } = new BlackboardKey();
public BlackboardKey(string name, BlackboardKeyType type)
{
Name = name ?? throw new ArgumentException(nameof(name));
Type = type;
}
public BlackboardKey(string name) : this(name, BlackboardKeyType.Object)
{
}
public readonly string Name;
public readonly BlackboardKeyType Type;
public static implicit operator BlackboardKey(string name)
=> new BlackboardKey(name);
public static implicit operator string(BlackboardKey key)
=> key.Name;
public override bool Equals(object? obj)
=> obj is BlackboardKey bk && bk.Equals(this);
public override int GetHashCode()
=> Name?.GetHashCode() ?? -1;
public bool Equals(BlackboardKey other)
=> other.Name == Name;
public static bool operator ==(BlackboardKey left, BlackboardKey right)
=> left.Equals(right);
public static bool operator !=(BlackboardKey left, BlackboardKey right)
=> !(left == right);
}
}

View File

@@ -0,0 +1,53 @@
using System;
using System.Diagnostics;
namespace Nodify.StateMachine
{
[DebuggerDisplay("{IsKey ? Key : Value}")]
public struct BlackboardProperty : IEquatable<BlackboardProperty>
{
public static BlackboardProperty Invalid { get; } = new BlackboardProperty();
public BlackboardProperty(BlackboardKey key)
{
Key = key;
Value = default;
}
public BlackboardProperty(object? value)
{
Key = BlackboardKey.Invalid;
Value = value;
}
public BlackboardKey Key { get; }
public object? Value { get; }
public bool IsKey => Key.IsValid();
public bool IsValue => !IsKey;
public static implicit operator BlackboardKey(BlackboardProperty action)
=> action.Key;
public override bool Equals(object? obj)
=> obj is BlackboardProperty action && action.Equals(this);
public override int GetHashCode()
=> IsKey ? Key.GetHashCode() : Value?.GetHashCode() ?? -1;
public bool Equals(BlackboardProperty other)
=> IsKey == other.IsKey && IsValue == other.IsValue && Key == other.Key && Value == other.Value;
public static bool operator ==(BlackboardProperty left, BlackboardProperty right)
=> left.Equals(right);
public static bool operator !=(BlackboardProperty left, BlackboardProperty right)
=> !(left == right);
public T? GetValue<T>() where T : struct
=> Value is T result ? result : default;
public T? GetObject<T>() where T : class
=> Value as T;
}
}

View File

@@ -0,0 +1,42 @@
using System;
namespace Nodify.StateMachine
{
public enum BlackboardKeyUsage
{
Input,
Output
}
/// <summary>
/// Properties decorated with this attribute must always be of type <see cref="BlackboardProperty"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class BlackboardPropertyAttribute : Attribute
{
/// <summary>
/// Properties decorated with this attribute must always be of type <see cref="BlackboardProperty"/>.
/// </summary>
/// <param name="name">The display name of the key.</param>
/// <param name="type">The data type of the value that the key refers to.</param>
public BlackboardPropertyAttribute(string? name, BlackboardKeyType type = BlackboardKeyType.Object)
{
Name = name;
Type = type;
}
/// <summary>
/// Properties decorated with this attribute must always be of type <see cref="BlackboardProperty"/>.
/// </summary>
/// <param name="type">The data type of the value that the key refers to.</param>
public BlackboardPropertyAttribute(BlackboardKeyType type = BlackboardKeyType.Object) : this(null, type)
{
}
public string? Name { get; }
public BlackboardKeyType Type { get; }
public BlackboardKeyUsage Usage { get; set; }
public bool CanChangeType { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
public interface IBlackboardAction
{
Task Execute(Blackboard blackboard);
}
}

View File

@@ -0,0 +1,9 @@
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
public interface IBlackboardCondition
{
Task<bool> Evaluate(Blackboard blackboard);
}
}

View File

@@ -0,0 +1,23 @@
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
[BlackboardItem("Are Equal")]
public class AreEqualCondition : IBlackboardCondition
{
[BlackboardProperty(BlackboardKeyType.Object, CanChangeType = true)]
public BlackboardProperty Left { get; set; }
[BlackboardProperty(BlackboardKeyType.Object, CanChangeType = true)]
public BlackboardProperty Right { get; set; }
public Task<bool> Evaluate(Blackboard blackboard)
{
var left = blackboard.GetObject(Left);
var right = blackboard.GetObject(Right);
// TODO: Equality
return Task.FromResult(Equals(left, right));
}
}
}

View File

@@ -0,0 +1,23 @@
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
[BlackboardItem("Has Key")]
public class HasKeyCondition : IBlackboardCondition
{
[BlackboardProperty("Key Name", BlackboardKeyType.String)]
public BlackboardProperty Key { get; set; }
public Task<bool> Evaluate(Blackboard blackboard)
{
var keyName = blackboard.GetObject<string>(Key);
if (keyName != null)
{
return Task.FromResult(blackboard.HasKey(keyName));
}
return Task.FromResult(false);
}
}
}

View File

@@ -0,0 +1,14 @@
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
[BlackboardItem("Has Value")]
public class HasValueCondition : IBlackboardCondition
{
[BlackboardProperty(BlackboardKeyType.Object)]
public BlackboardKey Key { get; set; }
public Task<bool> Evaluate(Blackboard blackboard)
=> Task.FromResult(blackboard.GetObject(Key) != null);
}
}

View File

@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
namespace Nodify.StateMachine
{
public class DebugBlackboardDecorator : Blackboard
{
public static BlackboardKey StateDelayKey { get; } = "__state.delay";
public static BlackboardKey TransitionDelayKey { get; } = "__transition.delay";
private Blackboard? _blackboard;
public event Action<BlackboardKey, object?>? ValueChanged;
public DebugBlackboardDecorator(Blackboard? blackboard = default)
=> Attach(blackboard);
public override IReadOnlyCollection<BlackboardKey> Keys => _blackboard?.Keys ?? Array.Empty<BlackboardKey>();
public override void Remove(BlackboardKey key)
=> _blackboard?.Remove(key);
public override void Clear()
=> _blackboard?.Clear();
public override T? GetObject<T>(BlackboardKey key) where T : class
=> _blackboard?.GetObject<T>(key);
public override T? GetValue<T>(BlackboardKey key)
=> _blackboard?.GetValue<T>(key);
public override void Set(BlackboardKey key, object? value)
{
_blackboard?.Set(key, value);
ValueChanged?.Invoke(key, value);
}
public override bool HasKey(BlackboardKey key)
=> _blackboard?.HasKey(key) ?? false;
public override object? GetObject(BlackboardKey key)
=> _blackboard?.GetObject(key);
public virtual void Attach(Blackboard? blackboard)
{
_blackboard = blackboard;
Set(StateDelayKey, 100);
}
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
public class DebugStateDecorator : State
{
private readonly State _state;
public DebugStateDecorator(State state) : base(state.Id, state.Transitions)
{
_state = state;
}
public override async Task Activate(Blackboard blackboard)
{
int? delay = blackboard.GetValue<int>(DebugBlackboardDecorator.StateDelayKey);
await Task.Delay(Math.Max(10, delay ?? 10));
await _state.Activate(blackboard);
}
}
}

View File

@@ -0,0 +1,26 @@
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
public class DebugTransitionDecorator : Transition
{
private readonly Transition _transition;
public DebugTransitionDecorator(Transition transition) : base(transition.From, transition.To)
{
_transition = transition;
}
public override async Task<bool> CanActivate(Blackboard blackboard)
{
int? delay = blackboard.GetValue<int>(DebugBlackboardDecorator.TransitionDelayKey);
if (delay > 0)
{
await Task.Delay(delay.Value);
}
return await _transition.CanActivate(blackboard);
}
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
public class State
{
public Guid Id { get; }
public IBlackboardAction? Action { get; }
public State(Guid id, IEnumerable<Transition> transitions, IBlackboardAction? action = default)
{
Id = id;
Action = action;
Transitions = new List<Transition>(transitions);
}
public IReadOnlyList<Transition> Transitions { get; }
public virtual Task Activate(Blackboard blackboard)
=> Action?.Execute(blackboard) ?? Task.CompletedTask;
}
}

View File

@@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
public enum MachineState
{
Stopped,
Running,
Paused,
}
public delegate void StateTransitionEventHandler(Guid from, Guid to);
public delegate void StateChangedEventHandler(MachineState newStatus);
public class StateMachine
{
private readonly Dictionary<Guid, State> _states;
public State Root { get; }
public MachineState? State { get; private set; }
public Blackboard Blackboard { get; } = new Blackboard();
// param = aborted
public event StateChangedEventHandler? StateChanged;
public event StateTransitionEventHandler? StateTransition;
public StateMachine(Guid root, IEnumerable<State> states, Blackboard? blackboard = default)
{
_states = states.ToDictionary(x => x.Id, x => x);
if (!_states.ContainsKey(root))
{
throw new ArgumentException(nameof(root));
}
Root = _states[root];
if (blackboard != null)
{
Blackboard = blackboard;
}
}
public async Task Start()
{
if (ChangeState(MachineState.Running))
{
// Skip root state
State? previous = Root;
State? current = await GetNext(Root);
while (State != MachineState.Stopped && current != null)
{
if (State == MachineState.Paused)
{
await Task.Delay(10);
}
else
{
StateTransition?.Invoke(previous.Id, current.Id);
previous = current;
await current.Activate(Blackboard);
current = await GetNext(current);
}
}
ChangeState(MachineState.Stopped);
}
}
private async Task<State?> GetNext(State current)
{
var transitions = current.Transitions;
for (int i = 0; i < transitions.Count; i++)
{
var transition = transitions[i];
if (_states.TryGetValue(transition.To, out var result) && await transition.CanActivate(Blackboard))
{
return result;
}
}
return default;
}
public void Stop()
=> ChangeState(MachineState.Stopped);
public void Pause()
=> ChangeState(MachineState.Paused);
public void Unpause()
=> ChangeState(MachineState.Running);
private bool ChangeState(MachineState newState)
{
if (newState == MachineState.Running || (State != null && State != newState))
{
State = newState;
StateChanged?.Invoke(newState);
return true;
}
return false;
}
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Threading.Tasks;
namespace Nodify.StateMachine
{
public class Transition
{
public Transition(Guid from, Guid to, IBlackboardCondition? condition = default)
{
From = from;
To = to;
Condition = condition;
}
public Guid From { get; }
public Guid To { get; }
public IBlackboardCondition? Condition { get; }
public virtual Task<bool> CanActivate(Blackboard blackboard)
=> Condition?.Evaluate(blackboard) ?? Task.FromResult(true);
}
}

View File

@@ -0,0 +1,230 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Nodify.StateMachine
{
public class StateMachineRunnerViewModel : ObservableObject
{
private StateMachine? _stateMachine;
private StateViewModel? _activeState;
private TransitionViewModel? _activeTransition;
private readonly DebugBlackboardDecorator _debugger = new DebugBlackboardDecorator();
private readonly Blackboard _original = new Blackboard();
protected StateMachineViewModel StateMachineViewModel { get; }
private MachineState _state;
public MachineState State
{
get => _state;
protected set => SetProperty(ref _state, value);
}
private int _nodesVisited;
public int NodesVisited
{
get => _nodesVisited;
protected set => SetProperty(ref _nodesVisited, value);
}
public StateMachineRunnerViewModel(StateMachineViewModel stateMachineViewModel)
{
StateMachineViewModel = stateMachineViewModel;
_debugger.ValueChanged += OnBlackboardKeyValueChanged;
}
private void OnBlackboardKeyValueChanged(BlackboardKey key, object? newValue)
{
if (_stateMachine != null && _stateMachine.State != MachineState.Stopped)
{
var existing = StateMachineViewModel.Blackboard.Keys.FirstOrDefault(k => k.Name == key.Name && k.Type == key.Type);
if (existing != null)
{
existing.Value = newValue;
}
}
}
#region State Machine Actions
public async void Start()
{
NodesVisited = 0;
_stateMachine = new StateMachine(StateMachineViewModel.States[0].Id, CreateStates(StateMachineViewModel.States), CreateBlackboard(StateMachineViewModel.Blackboard));
_stateMachine.StateTransition += HandleStateTransition;
_stateMachine.StateChanged += HandleStateChange;
await _stateMachine.Start();
}
public void Stop()
{
_stateMachine?.Stop();
_stateMachine = null;
}
private void HandleStateTransition(Guid from, Guid to)
{
NodesVisited++;
SetActiveStateAndTransition(false);
_activeTransition = StateMachineViewModel.Transitions.FirstOrDefault(t => t.Source.Id == from);
_activeState = StateMachineViewModel.States.FirstOrDefault(st => st.Id == to);
SetActiveStateAndTransition(true);
}
private void SetActiveStateAndTransition(bool value)
{
if (_activeState != null)
{
_activeState.IsActive = value;
}
if (_activeTransition != null)
{
_activeTransition.IsActive = value;
}
}
private void HandleStateChange(MachineState newState)
{
if (newState == MachineState.Stopped)
{
SetActiveStateAndTransition(false);
ResetBlackboardToOriginal();
}
State = newState;
}
private void ResetBlackboardToOriginal()
{
var keys = StateMachineViewModel.Blackboard.Keys;
for (int i = 0; i < keys.Count; i++)
{
var key = keys[i];
key.Value = _original.GetObject(key.Name);
}
}
public void TogglePause()
{
if (State == MachineState.Paused)
{
_stateMachine?.Unpause();
}
else if (State != MachineState.Stopped)
{
_stateMachine?.Pause();
}
}
#endregion
#region Initialize State Machine
private IEnumerable<State> CreateStates(IEnumerable<StateViewModel> states)
=> states.Select(s => new DebugStateDecorator(new State(s.Id, CreateTransitions(s), CreateAction(s.Action))));
private IEnumerable<Transition> CreateTransitions(StateViewModel state)
{
var transitions = StateMachineViewModel.Transitions.Where(t => t.Source == state).ToList();
var result = new List<Transition>(transitions.Count);
for (int i = 0; i < transitions.Count; i++)
{
var transition = transitions[i];
var tr = new Transition(transition.Source.Id, transition.Target.Id, CreateCondition(transition.Condition));
result.Add(new DebugTransitionDecorator(tr));
}
return result;
}
private IBlackboardCondition? CreateCondition(BlackboardItemViewModel? condition)
{
if (condition?.Type != null && typeof(IBlackboardCondition).IsAssignableFrom(condition.Type))
{
// TODO: DI Container
var result = (IBlackboardCondition?)Activator.CreateInstance(condition.Type);
InitializeKeys(condition.Input, result, condition.Type);
return result;
}
return default;
}
private IBlackboardAction? CreateAction(BlackboardItemViewModel? action)
{
if (action?.Type != null && typeof(IBlackboardAction).IsAssignableFrom(action.Type))
{
// TODO: DI Container
var result = (IBlackboardAction?)Activator.CreateInstance(action.Type);
InitializeKeys(action.Input, result, action.Type);
InitializeKeys(action.Output, result, action.Type);
return result;
}
return default;
}
private void InitializeKeys(NodifyObservableCollection<BlackboardKeyViewModel> keys, object? instance, Type type)
{
for (int i = 0; i < keys.Count; i++)
{
var vm = keys[i];
var key = CreateActionValue(vm);
// TODO: Property cache
if (vm.PropertyName != null)
{
var prop = type.GetProperty(vm.PropertyName);
if (prop?.CanWrite ?? false)
{
prop.SetValue(instance, key);
}
}
}
}
private Blackboard CreateBlackboard(BlackboardViewModel blackboard)
{
Blackboard result = new Blackboard();
for (int i = 0; i < blackboard.Keys.Count; i++)
{
var key = blackboard.Keys[i];
if (!string.IsNullOrWhiteSpace(key.Name))
{
result.Set(new BlackboardKey(key.Name, key.Type), key.Value);
}
}
result.CopyTo(_original);
_debugger.Attach(result);
return _debugger;
}
private BlackboardProperty CreateActionValue(BlackboardKeyViewModel key)
{
if (key.Value is BlackboardKeyViewModel bkv)
{
return new BlackboardProperty(new BlackboardKey(bkv.Name, bkv.Type));
}
return new BlackboardProperty(key.Value);
}
#endregion
}
}

View File

@@ -0,0 +1,247 @@
using System;
using System.Linq;
using System.Windows;
namespace Nodify.StateMachine
{
public class StateMachineViewModel : ObservableObject
{
public StateMachineViewModel()
{
PendingTransition = new TransitionViewModel();
Runner = new StateMachineRunnerViewModel(this);
Blackboard = new BlackboardViewModel()
{
Actions = new NodifyObservableCollection<BlackboardItemReferenceViewModel>(BlackboardDescriptor.GetAvailableItems<IBlackboardAction>()),
Conditions = new NodifyObservableCollection<BlackboardItemReferenceViewModel>(BlackboardDescriptor.GetAvailableItems<IBlackboardCondition>())
};
Transitions.WhenAdded(c =>
{
c.Source.Transitions.Add(c.Target);
c.Target.Transitions.Add(c.Source);
})
.WhenRemoved(c =>
{
c.Source.Transitions.Remove(c.Target);
c.Target.Transitions.Remove(c.Source);
})
.WhenCleared(c => c.ForEach(i =>
{
i.Source.Transitions.Clear();
i.Target.Transitions.Clear();
}));
States.WhenAdded(x => x.Graph = this)
.WhenRemoved(x => DisconnectState(x))
.WhenCleared(x =>
{
Transitions.Clear();
OnCreateDefaultNodes();
});
OnCreateDefaultKeys();
OnCreateDefaultNodes();
RenameStateCommand = new RequeryCommand(() => SelectedStates[0].IsRenaming = true, () => SelectedStates.Count == 1 && SelectedStates[0].IsEditable);
DisconnectStateCommand = new RequeryCommand<StateViewModel>(x => DisconnectState(x), x => !IsRunning && x.Transitions.Count > 0);
DisconnectSelectionCommand = new RequeryCommand(() => SelectedStates.ForEach(x => DisconnectState(x)), () => !IsRunning && SelectedStates.Count > 0 && Transitions.Count > 0);
DeleteSelectionCommand = new RequeryCommand(() => SelectedStates.ToList().ForEach(x => x.IsEditable.Then(() => States.Remove(x))), () => !IsRunning && (SelectedStates.Count > 1 || (SelectedStates.Count == 1 && SelectedStates[0].IsEditable)));
AddStateCommand = new RequeryCommand<Point>(p => States.Add(new StateViewModel
{
Name = "New State",
IsRenaming = true,
Location = p,
ActionReference = Blackboard.Actions.Count > 0 ? Blackboard.Actions[0] : null
}), p => !IsRunning);
CreateTransitionCommand = new DelegateCommand<(object Source, object? Target)>(s => Transitions.Add(new TransitionViewModel
{
Source = (StateViewModel)s.Source,
Target = (StateViewModel)s.Target!
}), s => !IsRunning && s.Source is StateViewModel source && s.Target is StateViewModel target && target != s.Source && target != States[0] && !source.Transitions.Contains(s.Target));
DeleteTransitionCommand = new RequeryCommand<TransitionViewModel>(t => Transitions.Remove(t), t => !IsRunning);
RunCommand = new RequeryCommand(() => IsRunning.Then(Runner.Stop).Else(Runner.Start), () => Transitions.Count > 0);
PauseCommand = new RequeryCommand(Runner.TogglePause, () => IsRunning);
}
private NodifyObservableCollection<StateViewModel> _states = new NodifyObservableCollection<StateViewModel>();
public NodifyObservableCollection<StateViewModel> States
{
get => _states;
set => SetProperty(ref _states, value);
}
private NodifyObservableCollection<StateViewModel> _selectedStates = new NodifyObservableCollection<StateViewModel>();
public NodifyObservableCollection<StateViewModel> SelectedStates
{
get => _selectedStates;
set => SetProperty(ref _selectedStates, value);
}
private NodifyObservableCollection<TransitionViewModel> _connections = new NodifyObservableCollection<TransitionViewModel>();
public NodifyObservableCollection<TransitionViewModel> Transitions
{
get => _connections;
set => SetProperty(ref _connections, value);
}
private StateViewModel? _selectedState;
public StateViewModel? SelectedState
{
get => _selectedState;
set => SetProperty(ref _selectedState, value);
}
private string? _name = "State Machine";
public string? Name
{
get => _name;
set => SetProperty(ref _name, value);
}
public bool IsRunning => Runner.State != MachineState.Stopped;
public bool IsPaused => Runner.State == MachineState.Paused;
public TransitionViewModel PendingTransition { get; }
public StateMachineRunnerViewModel Runner { get; }
public BlackboardViewModel Blackboard { get; }
public INodifyCommand DeleteTransitionCommand { get; }
public INodifyCommand DeleteSelectionCommand { get; }
public INodifyCommand DisconnectStateCommand { get; }
public INodifyCommand DisconnectSelectionCommand { get; }
public INodifyCommand RenameStateCommand { get; }
public INodifyCommand AddStateCommand { get; }
public INodifyCommand CreateTransitionCommand { get; }
public INodifyCommand RunCommand { get; }
public INodifyCommand PauseCommand { get; }
public void DisconnectState(StateViewModel state)
{
var transitions = Transitions.Where(t => t.Source == state || t.Target == state).ToList();
transitions.ForEach(t => Transitions.Remove(t));
}
protected virtual void OnCreateDefaultNodes()
{
States.Insert(0, new StateViewModel
{
Name = "Enter",
Location = new Point(100, 100),
IsEditable = false
});
var currentDelayKey = Blackboard.Keys.First(k => k.Name == "Current Delay");
var originalDelayKey = Blackboard.Keys.First(k => k.Name == "Original Delay");
var welcomeKey = Blackboard.Keys.First(k => k.Name == "Welcome");
States.Add(new StateViewModel
{
Name = "Set delay value",
Location = new Point(300, 100),
ActionReference = Blackboard.Actions.FirstOrDefault(a => a.Type == typeof(SetKeyValueAction))
});
States[1].Action!.Input[0].Value = currentDelayKey;
States[1].Action!.Input[1].ValueIsKey = false;
States[1].Action!.Input[1].Type = BlackboardKeyType.Integer;
States[1].Action!.Input[1].Value = 100;
States.Add(new StateViewModel
{
Name = "Set new delay",
Location = new Point(380, 250),
ActionReference = Blackboard.Actions.FirstOrDefault(a => a.Type == typeof(SetStateDelayAction))
});
States[2].Action!.Input[0].Value = currentDelayKey;
States.Add(new StateViewModel
{
Name = "Reset delay",
Location = new Point(300, 350),
ActionReference = Blackboard.Actions.FirstOrDefault(a => a.Type == typeof(CopyKeyAction))
});
States[3].Action!.Input[0].Value = originalDelayKey;
States[3].Action!.Input[1].Value = currentDelayKey;
States.Add(new StateViewModel
{
Name = "Set original delay",
Location = new Point(200, 250),
ActionReference = Blackboard.Actions.FirstOrDefault(a => a.Type == typeof(SetStateDelayAction))
});
States[4].Action!.Input[0].Value = originalDelayKey;
Transitions.Add(new TransitionViewModel
{
Source = States[0],
Target = States[1],
ConditionReference = Blackboard.Conditions.FirstOrDefault(c => c.Type == typeof(HasKeyCondition))
});
Transitions[0].Condition!.Input[0].Value = welcomeKey;
Transitions.Add(new TransitionViewModel
{
Source = States[1],
Target = States[2],
ConditionReference = Blackboard.Conditions.FirstOrDefault(c => c.Type == typeof(AreEqualCondition))
});
Transitions[1].Condition!.Input[0].Value = welcomeKey;
Transitions[1].Condition!.Input[1].ValueIsKey = false;
Transitions[1].Condition!.Input[1].Type = BlackboardKeyType.String;
Transitions[1].Condition!.Input[1].Value = currentDelayKey.Name;
Transitions.Add(new TransitionViewModel
{
Source = States[2],
Target = States[3]
});
Transitions.Add(new TransitionViewModel
{
Source = States[3],
Target = States[4]
});
Transitions.Add(new TransitionViewModel
{
Source = States[4],
Target = States[1]
});
}
protected virtual void OnCreateDefaultKeys()
{
Blackboard.Keys.Add(new BlackboardKeyViewModel
{
Name = "Current Delay",
Type = BlackboardKeyType.Integer,
Value = 1000
});
Blackboard.Keys.Add(new BlackboardKeyViewModel
{
Name = "Original Delay",
Type = BlackboardKeyType.Integer,
Value = 1000
});
Blackboard.Keys.Add(new BlackboardKeyViewModel
{
Name = "Welcome",
Type = BlackboardKeyType.String,
Value = "Current Delay"
});
}
}
}

View File

@@ -0,0 +1,84 @@
using System;
using System.Windows;
namespace Nodify.StateMachine
{
public class StateViewModel : ObservableObject
{
public Guid Id { get; }
public StateViewModel(Guid id)
=> Id = id;
public StateViewModel() : this(Guid.NewGuid()) { }
// TODO: Can remove when auto layout is added
private Point _location;
public Point Location
{
get => _location;
set => SetProperty(ref _location, value);
}
private Point _anchor;
public Point Anchor
{
get => _anchor;
set => SetProperty(ref _anchor, value);
}
private Size _size;
public Size Size
{
get => _size;
set => SetProperty(ref _size, value);
}
private string? _name;
public string? Name
{
get => _name;
set => SetProperty(ref _name, value);
}
private bool _isRenaming;
public bool IsRenaming
{
get => _isRenaming;
set => SetProperty(ref _isRenaming, value);
}
private bool _isActive;
public bool IsActive
{
get => _isActive;
set => SetProperty(ref _isActive, value);
}
private BlackboardItemReferenceViewModel? _actionReference;
public BlackboardItemReferenceViewModel? ActionReference
{
get => _actionReference;
set
{
if (SetProperty(ref _actionReference, value))
{
SetAction(_actionReference);
}
}
}
public BlackboardItemViewModel? Action { get; private set; }
public bool IsEditable { get; set; } = true;
public StateMachineViewModel Graph { get; internal set; } = default!;
public NodifyObservableCollection<StateViewModel> Transitions { get; } = new NodifyObservableCollection<StateViewModel>();
private void SetAction(BlackboardItemReferenceViewModel? actionRef)
{
Action = BlackboardDescriptor.GetItem(actionRef);
OnPropertyChanged(nameof(Action));
}
}
}

View File

@@ -0,0 +1,17 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:o="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options">
<SolidColorBrush x:Key="PanelBackgroundBrush"
o:Freeze="True"
Color="{DynamicResource PanelBackgroundColor}" />
<SolidColorBrush x:Key="ActiveStateBrush"
o:Freeze="True"
Color="{DynamicResource ActiveStateColor}" />
<SolidColorBrush x:Key="ReadOnlyStateBrush"
o:Freeze="True"
Color="{DynamicResource ReadOnlyStateColor}" />
</ResourceDictionary>

View File

@@ -0,0 +1,13 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Brushes.xaml" />
</ResourceDictionary.MergedDictionaries>
<Color x:Key="PanelBackgroundColor">#2D2D30</Color>
<Color x:Key="ActiveStateColor">#8DD28A</Color>
<Color x:Key="ReadOnlyStateColor">#E6AF86</Color>
</ResourceDictionary>

View File

@@ -0,0 +1,13 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Brushes.xaml" />
</ResourceDictionary.MergedDictionaries>
<Color x:Key="PanelBackgroundColor">#E9EEFE</Color>
<Color x:Key="ActiveStateColor">#8DD28A</Color>
<Color x:Key="ReadOnlyStateColor">#E6AF86</Color>
</ResourceDictionary>

View File

@@ -0,0 +1,13 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Brushes.xaml" />
</ResourceDictionary.MergedDictionaries>
<Color x:Key="PanelBackgroundColor">#2A1B47</Color>
<Color x:Key="ActiveStateColor">#A6D99C</Color>
<Color x:Key="ReadOnlyStateColor">#FD5618</Color>
</ResourceDictionary>

View File

@@ -0,0 +1,48 @@
namespace Nodify.StateMachine
{
public class TransitionViewModel : ObservableObject
{
private StateViewModel _source = default!;
public StateViewModel Source
{
get => _source;
set => SetProperty(ref _source, value);
}
private StateViewModel _target = default!;
public StateViewModel Target
{
get => _target;
set => SetProperty(ref _target, value);
}
private BlackboardItemReferenceViewModel? _conditionReference;
public BlackboardItemReferenceViewModel? ConditionReference
{
get => _conditionReference;
set
{
if (SetProperty(ref _conditionReference, value))
{
SetCondition(_conditionReference);
}
}
}
public BlackboardItemViewModel? Condition { get; private set; }
private bool _isActive;
public bool IsActive
{
get => _isActive;
set => SetProperty(ref _isActive, value);
}
private void SetCondition(BlackboardItemReferenceViewModel? conditionRef)
{
Condition = BlackboardDescriptor.GetItem(conditionRef);
OnPropertyChanged(nameof(Condition));
}
}
}