diff --git a/Amaltea/Amaltea.csproj b/Amaltea/Amaltea.csproj new file mode 100644 index 0000000..e3e33e3 --- /dev/null +++ b/Amaltea/Amaltea.csproj @@ -0,0 +1,11 @@ + + + + WinExe + net8.0-windows + enable + enable + true + + + diff --git a/Amaltea/Amaltea.sln b/Amaltea/Amaltea.sln new file mode 100644 index 0000000..ab457b9 --- /dev/null +++ b/Amaltea/Amaltea.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36429.23 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Amaltea", "Amaltea.csproj", "{11514D9A-D918-4E19-84CA-0D72FFA124B1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {11514D9A-D918-4E19-84CA-0D72FFA124B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11514D9A-D918-4E19-84CA-0D72FFA124B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11514D9A-D918-4E19-84CA-0D72FFA124B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11514D9A-D918-4E19-84CA-0D72FFA124B1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {02F963B3-CBB5-49DB-9A2B-3182AE54069A} + EndGlobalSection +EndGlobal diff --git a/Amaltea/Amaltea/Converters/BlockTypeToBrushConverter.cs b/Amaltea/Amaltea/Converters/BlockTypeToBrushConverter.cs new file mode 100644 index 0000000..35df635 --- /dev/null +++ b/Amaltea/Amaltea/Converters/BlockTypeToBrushConverter.cs @@ -0,0 +1,29 @@ +using System; +using System.Globalization; +using System.Windows.Data; +using System.Windows.Media; +using Amaltea.ViewModels; + +namespace Amaltea.Converters; + +public class BlockTypeToBrushConverter : IValueConverter +{ + public object? Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is StrategyBlockType type) + { + var resourceKey = type switch + { + StrategyBlockType.Trigger => "Brush.Block.Trigger", + StrategyBlockType.Condition => "Brush.Block.Condition", + StrategyBlockType.Action => "Brush.Block.Action", + StrategyBlockType.Risk => "Brush.Block.Risk", + _ => "Brush.Block.Trigger" + }; + return App.Current.TryFindResource(resourceKey) as Brush; + } + return null; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotSupportedException(); +} diff --git a/Amaltea/Amaltea/Converters/NullToVisibilityConverter.cs b/Amaltea/Amaltea/Converters/NullToVisibilityConverter.cs new file mode 100644 index 0000000..2ba4e81 --- /dev/null +++ b/Amaltea/Amaltea/Converters/NullToVisibilityConverter.cs @@ -0,0 +1,21 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace Amaltea.Converters; + +public class NullToVisibilityConverter : IValueConverter +{ + // If parameter == "Invert" then returns Visible when value is null. + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + bool invert = string.Equals(parameter as string, "Invert", StringComparison.OrdinalIgnoreCase); + bool isNull = value is null; + if (invert) + return isNull ? Visibility.Visible : Visibility.Collapsed; + return isNull ? Visibility.Collapsed : Visibility.Visible; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotSupportedException(); +} diff --git a/Amaltea/Amaltea/ViewModels/BaseViewModel.cs b/Amaltea/Amaltea/ViewModels/BaseViewModel.cs new file mode 100644 index 0000000..2f088e3 --- /dev/null +++ b/Amaltea/Amaltea/ViewModels/BaseViewModel.cs @@ -0,0 +1,20 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace Amaltea.ViewModels; + +public abstract class BaseViewModel : INotifyPropertyChanged +{ + public event PropertyChangedEventHandler? PropertyChanged; + + protected void RaisePropertyChanged([CallerMemberName] string? propertyName = null) + => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + + protected bool SetProperty(ref T storage, T value, [CallerMemberName] string? propertyName = null) + { + if (Equals(storage, value)) return false; + storage = value; + RaisePropertyChanged(propertyName); + return true; + } +} diff --git a/Amaltea/Amaltea/ViewModels/StrategyBlockViewModel.cs b/Amaltea/Amaltea/ViewModels/StrategyBlockViewModel.cs new file mode 100644 index 0000000..ec6dc23 --- /dev/null +++ b/Amaltea/Amaltea/ViewModels/StrategyBlockViewModel.cs @@ -0,0 +1,44 @@ +using System; +using System.Windows; +using System.Windows.Media; + +namespace Amaltea.ViewModels; + +public enum StrategyBlockType +{ + Trigger, + Condition, + Action, + Risk +} + +public class StrategyBlockViewModel : BaseViewModel +{ + private double _x; + private double _y; + private string _title = string.Empty; + private string? _subtitle; + private StrategyBlockType _blockType; + private bool _isSelected; + + public Guid Id { get; } = Guid.NewGuid(); + + public double X { get => _x; set => SetProperty(ref _x, value); } + public double Y { get => _y; set => SetProperty(ref _y, value); } + + public string Title { get => _title; set => SetProperty(ref _title, value); } + public string? Subtitle { get => _subtitle; set => SetProperty(ref _subtitle, value); } + + public StrategyBlockType BlockType { get => _blockType; set { if (SetProperty(ref _blockType, value)) RaisePropertyChanged(nameof(BlockBrushKey)); } } + + public bool IsSelected { get => _isSelected; set => SetProperty(ref _isSelected, value); } + + public string BlockBrushKey => BlockType switch + { + StrategyBlockType.Trigger => "Brush.Block.Trigger", + StrategyBlockType.Condition => "Brush.Block.Condition", + StrategyBlockType.Action => "Brush.Block.Action", + StrategyBlockType.Risk => "Brush.Block.Risk", + _ => "Brush.Block.Trigger" + }; +} diff --git a/Amaltea/Amaltea/ViewModels/StrategyEditorViewModel.cs b/Amaltea/Amaltea/ViewModels/StrategyEditorViewModel.cs new file mode 100644 index 0000000..8270ecc --- /dev/null +++ b/Amaltea/Amaltea/ViewModels/StrategyEditorViewModel.cs @@ -0,0 +1,151 @@ +using System.Collections.ObjectModel; +using System.Text.Json; +using System.IO; +using System.Linq; +using System.Collections.Generic; + +namespace Amaltea.ViewModels; + +public class StrategyEditorViewModel : BaseViewModel +{ + private StrategyBlockViewModel? _selectedBlock; + private readonly ObservableCollection _selectedBlocks = new(); + + public ObservableCollection Blocks { get; } = new(); + public ObservableCollection Connections { get; } = new(); + + public IEnumerable SelectedBlocks => _selectedBlocks; + + public StrategyBlockViewModel? SelectedBlock + { + get => _selectedBlock; + set + { + if (SetProperty(ref _selectedBlock, value)) + { + _selectedBlocks.Clear(); + if (value != null) + { + value.IsSelected = true; + _selectedBlocks.Add(value); + } + RaisePropertyChanged(nameof(SelectedBlocks)); + } + } + } + + public StrategyEditorViewModel() + { + // Seed with demo blocks + var b1 = new StrategyBlockViewModel { Title = "Event Trigger", Subtitle = "In-Play Start", X = 60, Y = 60, BlockType = StrategyBlockType.Trigger }; + var b2 = new StrategyBlockViewModel { Title = "Condition: Odds", Subtitle = "Back Odds < 3.0", X = 260, Y = 160, BlockType = StrategyBlockType.Condition }; + var b3 = new StrategyBlockViewModel { Title = "Action: Back", Subtitle = "Stake 10 EUR", X = 520, Y = 180, BlockType = StrategyBlockType.Action }; + var b4 = new StrategyBlockViewModel { Title = "Risk Mgmt", Subtitle = "Stop Loss 5%", X = 760, Y = 260, BlockType = StrategyBlockType.Risk }; + + Blocks.Add(b1); + Blocks.Add(b2); + Blocks.Add(b3); + Blocks.Add(b4); + + Connections.Add(new StrategyConnectionViewModel(b1.Id, b2.Id)); + Connections.Add(new StrategyConnectionViewModel(b2.Id, b3.Id)); + Connections.Add(new StrategyConnectionViewModel(b3.Id, b4.Id)); + } + + public StrategyBlockViewModel AddBlock(StrategyBlockType type, string title, string subtitle, double x, double y) + { + var block = new StrategyBlockViewModel { BlockType = type, Title = title, Subtitle = subtitle, X = x, Y = y }; + Blocks.Add(block); + return block; + } + + public void AddConnection(StrategyBlockViewModel from, StrategyBlockViewModel to) + { + if (from == to) return; + if (Connections.Any(c => c.From == from.Id && c.To == to.Id)) return; + Connections.Add(new StrategyConnectionViewModel(from.Id, to.Id)); + } + + public void ClearSelection() + { + foreach (var b in _selectedBlocks) b.IsSelected = false; + _selectedBlocks.Clear(); + _selectedBlock = null; + RaisePropertyChanged(nameof(SelectedBlocks)); + RaisePropertyChanged(nameof(SelectedBlock)); + } + + public void ToggleSelection(StrategyBlockViewModel block, bool add) + { + if (!add) + { + ClearSelection(); + block.IsSelected = true; + _selectedBlocks.Add(block); + _selectedBlock = block; + } + else + { + if (_selectedBlocks.Contains(block)) + { + block.IsSelected = false; + _selectedBlocks.Remove(block); + } + else + { + block.IsSelected = true; + _selectedBlocks.Add(block); + } + _selectedBlock = _selectedBlocks.LastOrDefault(); + } + RaisePropertyChanged(nameof(SelectedBlocks)); + RaisePropertyChanged(nameof(SelectedBlock)); + } + + public string Serialize() + { + var dto = new StrategyGraphDto + { + Blocks = Blocks.Select(b => new StrategyBlockDto + { + Id = b.Id, + X = b.X, + Y = b.Y, + Title = b.Title, + Subtitle = b.Subtitle, + BlockType = b.BlockType + }).ToList(), + Connections = Connections.Select(c => new StrategyConnectionDto { From = c.From, To = c.To }).ToList() + }; + return JsonSerializer.Serialize(dto, new JsonSerializerOptions { WriteIndented = true }); + } + + public void SaveToFile(string path) + { + File.WriteAllText(path, Serialize()); + } +} + +public record StrategyConnectionViewModel(System.Guid From, System.Guid To); + +public class StrategyGraphDto +{ + public List Blocks { get; set; } = new(); + public List Connections { get; set; } = new(); +} + +public class StrategyBlockDto +{ + public System.Guid Id { get; set; } + public double X { get; set; } + public double Y { get; set; } + public string? Title { get; set; } + public string? Subtitle { get; set; } + public StrategyBlockType BlockType { get; set; } +} + +public class StrategyConnectionDto +{ + public System.Guid From { get; set; } + public System.Guid To { get; set; } +} diff --git a/Amaltea/Amaltea/Views/StrategyEditorView.xaml b/Amaltea/Amaltea/Views/StrategyEditorView.xaml new file mode 100644 index 0000000..2354902 --- /dev/null +++ b/Amaltea/Amaltea/Views/StrategyEditorView.xaml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +