mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-12 11:56:30 +00:00
Meta - Replace WPF projects with Avalonia projects
This commit is contained in:
1 parent
fcf0376a9a
commit
d7f3ee1190
1110 files changed
+10872
-52240
No files matched your search
@@ -0,0 +1,83 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
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:visualScripting="clr-namespace:Artemis.UI.Screens.VisualScripting"
|
||||
xmlns:converters="clr-namespace:Artemis.UI.Converters"
|
||||
xmlns:skiaSharp="clr-namespace:SkiaSharp;assembly=SkiaSharp"
|
||||
xmlns:shared="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
|
||||
xmlns:system="clr-namespace:System;assembly=System.Runtime"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.VisualScripting.CableView"
|
||||
x:DataType="visualScripting:CableViewModel"
|
||||
ClipToBounds="False"
|
||||
IsVisible="{CompiledBinding Connected}">
|
||||
<UserControl.Resources>
|
||||
<converters:ColorToSolidColorBrushConverter x:Key="ColorToSolidColorBrushConverter" />
|
||||
<shared:SKColorToStringConverter x:Key="SKColorToStringConverter" />
|
||||
<shared:SKColorToColorConverter x:Key="SKColorToColorConverter" />
|
||||
</UserControl.Resources>
|
||||
<Canvas>
|
||||
<Path Name="CablePath"
|
||||
Stroke="{CompiledBinding CableColor, Converter={StaticResource ColorToSolidColorBrushConverter}}"
|
||||
StrokeThickness="4"
|
||||
StrokeLineCap="Round">
|
||||
<Path.Transitions>
|
||||
<Transitions>
|
||||
<ThicknessTransition Property="Margin" Duration="200"></ThicknessTransition>
|
||||
</Transitions>
|
||||
</Path.Transitions>
|
||||
<Path.Data>
|
||||
<PathGeometry>
|
||||
<PathGeometry.Figures>
|
||||
<PathFigure IsClosed="False">
|
||||
<PathFigure.Segments>
|
||||
<BezierSegment />
|
||||
</PathFigure.Segments>
|
||||
</PathFigure>
|
||||
</PathGeometry.Figures>
|
||||
</PathGeometry>
|
||||
</Path.Data>
|
||||
</Path>
|
||||
<Border Name="ValueBorder"
|
||||
Background="{DynamicResource ContentDialogBackground}"
|
||||
BorderBrush="{CompiledBinding CableColor, Converter={StaticResource ColorToSolidColorBrushConverter}}"
|
||||
BorderThickness="2"
|
||||
CornerRadius="3"
|
||||
Padding="4"
|
||||
Canvas.Left="{CompiledBinding ValuePoint.X}"
|
||||
Canvas.Top="{CompiledBinding ValuePoint.Y}"
|
||||
IsVisible="{CompiledBinding FromViewModel.Pin.PinValue, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<ContentControl Content="{CompiledBinding FromViewModel.Pin.PinValue}">
|
||||
<ContentControl.DataTemplates>
|
||||
<DataTemplate DataType="skiaSharp:SKColor">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<TextBlock x:Name="HexDisplay"
|
||||
Text="{CompiledBinding Converter={StaticResource SKColorToStringConverter}}"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Stretch" />
|
||||
<Border Margin="5 0 0 0"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Right"
|
||||
BorderThickness="1"
|
||||
MinWidth="18"
|
||||
MinHeight="18"
|
||||
Background="{DynamicResource CheckerboardBrush}"
|
||||
BorderBrush="{DynamicResource ColorPickerButtonOutline}"
|
||||
CornerRadius="2">
|
||||
<Ellipse Stroke="{DynamicResource NormalBorderBrush}">
|
||||
<Ellipse.Fill>
|
||||
<SolidColorBrush Color="{Binding Converter={StaticResource SKColorToColorConverter}}" />
|
||||
</Ellipse.Fill>
|
||||
</Ellipse>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding}" />
|
||||
</DataTemplate>
|
||||
</ContentControl.DataTemplates>
|
||||
</ContentControl>
|
||||
</Border>
|
||||
</Canvas>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using Avalonia.Controls.Shapes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.ReactiveUI;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.VisualScripting;
|
||||
|
||||
public class CableView : ReactiveUserControl<CableViewModel>
|
||||
{
|
||||
private const double CABLE_OFFSET = 24 * 4;
|
||||
private readonly Path _cablePath;
|
||||
private readonly Border _valueBorder;
|
||||
|
||||
public CableView()
|
||||
{
|
||||
InitializeComponent();
|
||||
_cablePath = this.Get<Path>("CablePath");
|
||||
_valueBorder = this.Get<Border>("ValueBorder");
|
||||
|
||||
// Not using bindings here to avoid a warnings
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
_valueBorder.GetObservable(BoundsProperty).Subscribe(rect => _valueBorder.RenderTransform = new TranslateTransform(rect.Width / 2 * -1, rect.Height / 2 * -1)).DisposeWith(d);
|
||||
|
||||
ViewModel.WhenAnyValue(vm => vm.FromPoint).Subscribe(_ => Update()).DisposeWith(d);
|
||||
ViewModel.WhenAnyValue(vm => vm.ToPoint).Subscribe(_ => Update()).DisposeWith(d);
|
||||
Update();
|
||||
});
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Workaround for https://github.com/AvaloniaUI/Avalonia/issues/4748
|
||||
_cablePath.Margin = _cablePath.Margin != new Thickness(0, 0, 0, 0) ? new Thickness(0, 0, 0, 0) : new Thickness(1, 1, 0, 0);
|
||||
|
||||
PathFigure pathFigure = ((PathGeometry) _cablePath.Data).Figures.First();
|
||||
BezierSegment segment = (BezierSegment) pathFigure.Segments!.First();
|
||||
pathFigure.StartPoint = ViewModel!.FromPoint;
|
||||
segment.Point1 = new Point(ViewModel.FromPoint.X + CABLE_OFFSET, ViewModel.FromPoint.Y);
|
||||
segment.Point2 = new Point(ViewModel.ToPoint.X - CABLE_OFFSET, ViewModel.ToPoint.Y);
|
||||
segment.Point3 = new Point(ViewModel.ToPoint.X, ViewModel.ToPoint.Y);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Exceptions;
|
||||
using Artemis.UI.Screens.VisualScripting.Pins;
|
||||
using Artemis.UI.Shared;
|
||||
using Avalonia;
|
||||
using Avalonia.Media;
|
||||
using DynamicData;
|
||||
using DynamicData.Binding;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.VisualScripting;
|
||||
|
||||
public class CableViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private readonly ObservableAsPropertyHelper<bool> _connected;
|
||||
private readonly ObservableAsPropertyHelper<Point> _fromPoint;
|
||||
private readonly ObservableAsPropertyHelper<Point> _toPoint;
|
||||
private readonly ObservableAsPropertyHelper<Point> _valuePoint;
|
||||
private ObservableAsPropertyHelper<Color>? _cableColor;
|
||||
|
||||
private PinViewModel? _fromViewModel;
|
||||
private PinViewModel? _toViewModel;
|
||||
|
||||
public CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin from, IPin to)
|
||||
{
|
||||
if (from.Direction != PinDirection.Output)
|
||||
throw new ArtemisUIException("Can only create cables originating from an output pin");
|
||||
if (to.Direction != PinDirection.Input)
|
||||
throw new ArtemisUIException("Can only create cables targeted to an input pin");
|
||||
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
nodeScriptViewModel.PinViewModels.ToObservableChangeSet().Filter(p => ReferenceEquals(p.Pin, from)).Transform(model => FromViewModel = model).Subscribe().DisposeWith(d);
|
||||
nodeScriptViewModel.PinViewModels.ToObservableChangeSet().Filter(p => ReferenceEquals(p.Pin, to)).Transform(model => ToViewModel = model).Subscribe().DisposeWith(d);
|
||||
|
||||
_cableColor = this.WhenAnyValue(vm => vm.FromViewModel, vm => vm.ToViewModel)
|
||||
.Select(tuple => tuple.Item1?.WhenAnyValue(p => p.PinColor) ?? tuple.Item2?.WhenAnyValue(p => p.PinColor) ?? Observable.Never<Color>())
|
||||
.Switch()
|
||||
.ToProperty(this, vm => vm.CableColor)
|
||||
.DisposeWith(d);
|
||||
});
|
||||
|
||||
_fromPoint = this.WhenAnyValue(vm => vm.FromViewModel)
|
||||
.Select(p => p != null ? p.WhenAnyValue(pvm => pvm.Position) : Observable.Never<Point>())
|
||||
.Switch()
|
||||
.ToProperty(this, vm => vm.FromPoint);
|
||||
_toPoint = this.WhenAnyValue(vm => vm.ToViewModel)
|
||||
.Select(p => p != null ? p.WhenAnyValue(pvm => pvm.Position) : Observable.Never<Point>())
|
||||
.Switch()
|
||||
.ToProperty(this, vm => vm.ToPoint);
|
||||
_valuePoint = this.WhenAnyValue(vm => vm.FromPoint, vm => vm.ToPoint).Select(tuple => new Point(
|
||||
tuple.Item1.X + (tuple.Item2.X - tuple.Item1.X) / 2,
|
||||
tuple.Item1.Y + (tuple.Item2.Y - tuple.Item1.Y) / 2
|
||||
)).ToProperty(this, vm => vm.ValuePoint);
|
||||
|
||||
// Not a perfect solution but this makes sure the cable never renders at 0,0 (can happen when the cable spawns before the pin ever rendered)
|
||||
_connected = this.WhenAnyValue(vm => vm.FromPoint, vm => vm.ToPoint)
|
||||
.Select(tuple => tuple.Item1 != new Point(0, 0) && tuple.Item2 != new Point(0, 0))
|
||||
.ToProperty(this, vm => vm.Connected);
|
||||
}
|
||||
|
||||
public PinViewModel? FromViewModel
|
||||
{
|
||||
get => _fromViewModel;
|
||||
set => RaiseAndSetIfChanged(ref _fromViewModel, value);
|
||||
}
|
||||
|
||||
public PinViewModel? ToViewModel
|
||||
{
|
||||
get => _toViewModel;
|
||||
set => RaiseAndSetIfChanged(ref _toViewModel, value);
|
||||
}
|
||||
|
||||
public bool Connected => _connected.Value;
|
||||
|
||||
public Point FromPoint => _fromPoint.Value;
|
||||
public Point ToPoint => _toPoint.Value;
|
||||
public Point ValuePoint => _valuePoint.Value;
|
||||
public Color CableColor => _cableColor?.Value ?? new Color(255, 255, 255, 255);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
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:visualScripting="clr-namespace:Artemis.UI.Screens.VisualScripting"
|
||||
xmlns:converters="clr-namespace:Artemis.UI.Converters"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.VisualScripting.DragCableView"
|
||||
x:DataType="visualScripting:DragCableViewModel"
|
||||
ClipToBounds="False">
|
||||
<UserControl.Resources>
|
||||
<converters:ColorToSolidColorBrushConverter x:Key="ColorToSolidColorBrushConverter" />
|
||||
</UserControl.Resources>
|
||||
<Path Name="CablePath"
|
||||
Stroke="{CompiledBinding PinViewModel.PinColor, Converter={StaticResource ColorToSolidColorBrushConverter}}"
|
||||
StrokeThickness="4"
|
||||
StrokeLineCap="Round">
|
||||
<Path.Data>
|
||||
<PathGeometry>
|
||||
<PathGeometry.Figures>
|
||||
<PathFigure IsClosed="False">
|
||||
<PathFigure.Segments>
|
||||
<BezierSegment />
|
||||
</PathFigure.Segments>
|
||||
</PathFigure>
|
||||
</PathGeometry.Figures>
|
||||
</PathGeometry>
|
||||
</Path.Data>
|
||||
</Path>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using Avalonia.Controls.Shapes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.ReactiveUI;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.VisualScripting;
|
||||
|
||||
public class DragCableView : ReactiveUserControl<DragCableViewModel>
|
||||
{
|
||||
private const double CABLE_OFFSET = 24 * 4;
|
||||
private readonly Path _cablePath;
|
||||
|
||||
public DragCableView()
|
||||
{
|
||||
InitializeComponent();
|
||||
_cablePath = this.Get<Path>("CablePath");
|
||||
|
||||
// Not using bindings here to avoid warnings
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
ViewModel.WhenAnyValue(vm => vm.FromPoint).Subscribe(_ => Update()).DisposeWith(d);
|
||||
ViewModel.WhenAnyValue(vm => vm.ToPoint).Subscribe(_ => Update()).DisposeWith(d);
|
||||
Update();
|
||||
});
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
PathFigure pathFigure = ((PathGeometry) _cablePath.Data).Figures.First();
|
||||
BezierSegment segment = (BezierSegment) pathFigure.Segments!.First();
|
||||
pathFigure.StartPoint = ViewModel!.FromPoint;
|
||||
segment.Point1 = new Point(ViewModel.FromPoint.X + CABLE_OFFSET, ViewModel.FromPoint.Y);
|
||||
segment.Point2 = new Point(ViewModel.ToPoint.X - CABLE_OFFSET, ViewModel.ToPoint.Y);
|
||||
segment.Point3 = new Point(ViewModel.ToPoint.X, ViewModel.ToPoint.Y);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Screens.VisualScripting.Pins;
|
||||
using Artemis.UI.Shared;
|
||||
using Avalonia;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.VisualScripting;
|
||||
|
||||
public class DragCableViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private Point _dragPoint;
|
||||
|
||||
private ObservableAsPropertyHelper<Point>? _fromPoint;
|
||||
private ObservableAsPropertyHelper<Point>? _toPoint;
|
||||
|
||||
public DragCableViewModel(PinViewModel pinViewModel)
|
||||
{
|
||||
PinViewModel = pinViewModel;
|
||||
|
||||
// If the pin is output, the pin is the from-point and the drag position is the to-point
|
||||
if (PinViewModel.Pin.Direction == PinDirection.Output)
|
||||
{
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
_fromPoint = PinViewModel.WhenAnyValue(vm => vm.Position).ToProperty(this, vm => vm.FromPoint).DisposeWith(d);
|
||||
});
|
||||
_toPoint = this.WhenAnyValue(vm => vm.DragPoint).ToProperty(this, vm => vm.ToPoint);
|
||||
}
|
||||
// If the pin is input, the pin is the to-point and the drag position is the from-point;
|
||||
else
|
||||
{
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
_toPoint = PinViewModel.WhenAnyValue(vm => vm.Position).ToProperty(this, vm => vm.ToPoint).DisposeWith(d);
|
||||
});
|
||||
_fromPoint = this.WhenAnyValue(vm => vm.DragPoint).ToProperty(this, vm => vm.FromPoint);
|
||||
}
|
||||
}
|
||||
|
||||
public PinViewModel PinViewModel { get; }
|
||||
public Point FromPoint => _fromPoint?.Value ?? new Point(0, 0);
|
||||
public Point ToPoint => _toPoint?.Value ?? new Point(0, 0);
|
||||
|
||||
public Point DragPoint
|
||||
{
|
||||
get => _dragPoint;
|
||||
set => RaiseAndSetIfChanged(ref _dragPoint, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
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:visualScripting="clr-namespace:Artemis.UI.Screens.VisualScripting"
|
||||
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:core="clr-namespace:Artemis.Core;assembly=Artemis.Core"
|
||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
mc:Ignorable="d" d:DesignWidth="650" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.VisualScripting.NodePickerView"
|
||||
x:DataType="visualScripting:NodePickerViewModel"
|
||||
Width="600"
|
||||
Height="400">
|
||||
<UserControl.Styles>
|
||||
<Style Selector="TextBox#SearchBox">
|
||||
<Setter Property="VerticalAlignment" Value="Top"></Setter>
|
||||
<Setter Property="InnerRightContent">
|
||||
<Template>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<controls:Button Content=""
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
Classes="AppBarButton"
|
||||
Command="{Binding $parent[TextBox].Clear}"
|
||||
IsVisible="{Binding Text, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TextBox}}, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
<controls:Button Content=""
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
Classes="AppBarButton"
|
||||
IsHitTestVisible="False" />
|
||||
</StackPanel>
|
||||
</Template>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="TreeView#NodeTree">
|
||||
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
<Border Classes="picker-container">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<TextBox Name="SearchBox" Text="{CompiledBinding SearchText}" Margin="0 0 0 15" Watermark="Search"></TextBox>
|
||||
<TreeView Name="NodeTree" Grid.Row="1" Items="{CompiledBinding Categories}" IsVisible="{CompiledBinding Categories.Count}" SelectedItem="{CompiledBinding SelectedNode}">
|
||||
<TreeView.Styles>
|
||||
<Style Selector="TreeViewItem">
|
||||
<Setter Property="IsExpanded" Value="True" />
|
||||
</Style>
|
||||
</TreeView.Styles>
|
||||
<TreeView.DataTemplates>
|
||||
<TreeDataTemplate DataType="{x:Type core:NodeData}">
|
||||
<StackPanel Margin="-15 1 0 1">
|
||||
<TextBlock Classes="BodyStrongTextBlockStyle" Text="{Binding Name}"></TextBlock>
|
||||
<TextBlock Foreground="{DynamicResource TextFillColorSecondary}" Text="{Binding Description}"></TextBlock>
|
||||
</StackPanel>
|
||||
</TreeDataTemplate>
|
||||
<TreeDataTemplate ItemsSource="{Binding Items}">
|
||||
<TextBlock Text="{Binding Key}"></TextBlock>
|
||||
</TreeDataTemplate>
|
||||
</TreeView.DataTemplates>
|
||||
</TreeView>
|
||||
<StackPanel Grid.Row="1" VerticalAlignment="Center" Spacing="20" IsVisible="{CompiledBinding !Categories.Count}">
|
||||
<avalonia:MaterialIcon Kind="CloseCircle" Width="64" Height="64"></avalonia:MaterialIcon>
|
||||
<TextBlock Classes="h4" TextAlignment="Center">None of the nodes match your search</TextBlock>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Reactive.Linq;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using Avalonia.LogicalTree;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.VisualScripting;
|
||||
|
||||
public class NodePickerView : ReactiveUserControl<NodePickerViewModel>
|
||||
{
|
||||
public NodePickerView()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
ViewModel?.WhenAnyValue(vm => vm.IsVisible).Where(visible => !visible).Subscribe(_ => this.FindLogicalAncestorOfType<Grid>()?.ContextFlyout?.Hide()).DisposeWith(d);
|
||||
this.Get<TextBox>("SearchBox").SelectAll();
|
||||
});
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.NodeEditor;
|
||||
using Artemis.UI.Shared.Services.NodeEditor.Commands;
|
||||
using Avalonia;
|
||||
using DynamicData;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.VisualScripting;
|
||||
|
||||
public class NodePickerViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private readonly INodeEditorService _nodeEditorService;
|
||||
private readonly NodeScript _nodeScript;
|
||||
|
||||
private bool _isVisible;
|
||||
private Point _position;
|
||||
private DateTime _closed;
|
||||
private string? _searchText;
|
||||
private object? _selectedNode;
|
||||
private IPin? _targetPin;
|
||||
|
||||
public NodePickerViewModel(NodeScript nodeScript, INodeService nodeService, INodeEditorService nodeEditorService)
|
||||
{
|
||||
_nodeScript = nodeScript;
|
||||
_nodeEditorService = nodeEditorService;
|
||||
|
||||
SourceList<NodeData> nodeSourceList = new();
|
||||
IObservable<Func<NodeData, bool>> nodeFilter = this.WhenAnyValue(vm => vm.SearchText, vm => vm.TargetPin).Select(v => CreatePredicate(v.Item1, v.Item2));
|
||||
|
||||
nodeSourceList.Connect()
|
||||
.Filter(nodeFilter)
|
||||
.GroupWithImmutableState(n => n.Category)
|
||||
.Bind(out ReadOnlyObservableCollection<DynamicData.List.IGrouping<NodeData, string>> categories)
|
||||
.Subscribe();
|
||||
Categories = categories;
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
if (DateTime.Now - _closed > TimeSpan.FromSeconds(10))
|
||||
SearchText = null;
|
||||
TargetPin = null;
|
||||
|
||||
nodeSourceList.Edit(list =>
|
||||
{
|
||||
list.Clear();
|
||||
list.AddRange(nodeService.AvailableNodes);
|
||||
});
|
||||
|
||||
IsVisible = true;
|
||||
|
||||
Disposable.Create(() =>
|
||||
{
|
||||
_closed = DateTime.Now;
|
||||
IsVisible = false;
|
||||
}).DisposeWith(d);
|
||||
});
|
||||
|
||||
this.WhenAnyValue(vm => vm.SelectedNode)
|
||||
.WhereNotNull()
|
||||
.Where(o => o is NodeData)
|
||||
.Throttle(TimeSpan.FromMilliseconds(200), RxApp.MainThreadScheduler)
|
||||
.Subscribe(data =>
|
||||
{
|
||||
CreateNode((NodeData) data);
|
||||
IsVisible = false;
|
||||
SelectedNode = null;
|
||||
});
|
||||
}
|
||||
|
||||
public ReadOnlyObservableCollection<DynamicData.List.IGrouping<NodeData, string>> Categories { get; }
|
||||
|
||||
public bool IsVisible
|
||||
{
|
||||
get => _isVisible;
|
||||
set => RaiseAndSetIfChanged(ref _isVisible, value);
|
||||
}
|
||||
|
||||
public Point Position
|
||||
{
|
||||
get => _position;
|
||||
set => RaiseAndSetIfChanged(ref _position, value);
|
||||
}
|
||||
|
||||
public string? SearchText
|
||||
{
|
||||
get => _searchText;
|
||||
set => RaiseAndSetIfChanged(ref _searchText, value);
|
||||
}
|
||||
|
||||
public IPin? TargetPin
|
||||
{
|
||||
get => _targetPin;
|
||||
set => RaiseAndSetIfChanged(ref _targetPin, value);
|
||||
}
|
||||
|
||||
public object? SelectedNode
|
||||
{
|
||||
get => _selectedNode;
|
||||
set => RaiseAndSetIfChanged(ref _selectedNode, value);
|
||||
}
|
||||
|
||||
public void CreateNode(NodeData data)
|
||||
{
|
||||
INode node = data.CreateNode(_nodeScript, null);
|
||||
node.X = Math.Round(Position.X / 10d, 0, MidpointRounding.AwayFromZero) * 10d;
|
||||
node.Y = Math.Round(Position.Y / 10d, 0, MidpointRounding.AwayFromZero) * 10d;
|
||||
|
||||
if (TargetPin != null)
|
||||
{
|
||||
using (_nodeEditorService.CreateCommandScope(_nodeScript, "Create node for pin"))
|
||||
{
|
||||
_nodeEditorService.ExecuteCommand(_nodeScript, new AddNode(_nodeScript, node));
|
||||
|
||||
// Find the first compatible source pin for the target pin
|
||||
IPin? source = TargetPin.Direction == PinDirection.Output
|
||||
? node.Pins.Concat(node.PinCollections.SelectMany(c => c)).Where(p => p.Direction == PinDirection.Input).FirstOrDefault(p => TargetPin.IsTypeCompatible(p.Type))
|
||||
: node.Pins.Concat(node.PinCollections.SelectMany(c => c)).Where(p => p.Direction == PinDirection.Output).FirstOrDefault(p => TargetPin.IsTypeCompatible(p.Type));
|
||||
|
||||
if (source != null)
|
||||
_nodeEditorService.ExecuteCommand(_nodeScript, new ConnectPins(source, TargetPin));
|
||||
}
|
||||
}
|
||||
else
|
||||
_nodeEditorService.ExecuteCommand(_nodeScript, new AddNode(_nodeScript, node));
|
||||
}
|
||||
|
||||
private Func<NodeData, bool> CreatePredicate(string? text, IPin? targetPin)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return data => data.IsCompatibleWithPin(targetPin);
|
||||
return data => data.IsCompatibleWithPin(targetPin) && data.MatchesSearch(text);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
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:paz="clr-namespace:Avalonia.Controls.PanAndZoom;assembly=Avalonia.Controls.PanAndZoom"
|
||||
xmlns:visualScripting="clr-namespace:Artemis.UI.Screens.VisualScripting"
|
||||
xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.VisualScripting.NodeScriptView"
|
||||
x:DataType="visualScripting:NodeScriptViewModel">
|
||||
<UserControl.Styles>
|
||||
<Style Selector="FlyoutPresenter.node-picker-flyout">
|
||||
<Setter Property="MaxWidth" Value="1000"></Setter>
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
<UserControl.KeyBindings>
|
||||
<KeyBinding Command="{CompiledBinding History.Undo}" Gesture="Ctrl+Z"></KeyBinding>
|
||||
<KeyBinding Command="{CompiledBinding History.Redo}" Gesture="Ctrl+Y"></KeyBinding>
|
||||
<KeyBinding Command="{Binding DeleteSelectedNodes}" Gesture="Delete"></KeyBinding>
|
||||
</UserControl.KeyBindings>
|
||||
<paz:ZoomBorder Name="ZoomBorder"
|
||||
Stretch="None"
|
||||
Focusable="True"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalAlignment="Stretch"
|
||||
Background="{DynamicResource LargeCheckerboardBrush}"
|
||||
ZoomChanged="ZoomBorder_OnZoomChanged"
|
||||
MaxZoomX="1"
|
||||
MaxZoomY="1"
|
||||
EnableConstrains="True"
|
||||
PointerReleased="ZoomBorder_OnPointerReleased">
|
||||
<Grid Name="ContainerGrid" Background="Transparent" ClipToBounds="False">
|
||||
<Grid.ContextFlyout>
|
||||
<Flyout FlyoutPresenterClasses="node-picker-flyout">
|
||||
<ContentControl Content="{CompiledBinding NodePickerViewModel}" />
|
||||
</Flyout>
|
||||
</Grid.ContextFlyout>
|
||||
<Grid.Transitions>
|
||||
<Transitions>
|
||||
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.2" Easing="CubicEaseOut" />
|
||||
</Transitions>
|
||||
</Grid.Transitions>
|
||||
|
||||
<!-- Drag cable, if any -->
|
||||
<ContentControl Content="{CompiledBinding DragViewModel}" ClipToBounds="False"/>
|
||||
|
||||
<!-- Cables -->
|
||||
<ItemsControl Items="{CompiledBinding CableViewModels}" ClipToBounds="False">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Canvas />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.Styles>
|
||||
<Style Selector="ItemsControl > ContentPresenter">
|
||||
<Setter Property="Canvas.Left" Value="{Binding Node.X, TargetNullValue=0}" />
|
||||
<Setter Property="Canvas.Top" Value="{Binding Node.Y, TargetNullValue=0}" />
|
||||
</Style>
|
||||
</ItemsControl.Styles>
|
||||
</ItemsControl>
|
||||
|
||||
<!-- Nodes -->
|
||||
<ItemsControl Name="NodesContainer" Items="{CompiledBinding NodeViewModels}" ClipToBounds="False">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<Canvas />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.Styles>
|
||||
<Style Selector="ItemsControl > ContentPresenter">
|
||||
<Setter Property="Canvas.Left" Value="{Binding Node.X, TargetNullValue=0}" />
|
||||
<Setter Property="Canvas.Top" Value="{Binding Node.Y, TargetNullValue=0}" />
|
||||
</Style>
|
||||
</ItemsControl.Styles>
|
||||
</ItemsControl>
|
||||
|
||||
<controls:SelectionRectangle Name="SelectionRectangle"
|
||||
InputElement="{Binding #ZoomBorder}"
|
||||
SelectionUpdated="SelectionRectangle_OnSelectionUpdated"
|
||||
SelectionFinished="SelectionRectangle_OnSelectionFinished"
|
||||
BorderBrush="{DynamicResource SystemAccentColor}"
|
||||
BorderRadius="8">
|
||||
<controls:SelectionRectangle.Background>
|
||||
<SolidColorBrush Color="{DynamicResource SystemAccentColorLight1}" Opacity="0.2"></SolidColorBrush>
|
||||
</controls:SelectionRectangle.Background>
|
||||
</controls:SelectionRectangle>
|
||||
</Grid>
|
||||
</paz:ZoomBorder>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Artemis.UI.Shared.Controls;
|
||||
using Artemis.UI.Shared.Events;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Generators;
|
||||
using Avalonia.Controls.PanAndZoom;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.ReactiveUI;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.VisualScripting;
|
||||
|
||||
public class NodeScriptView : ReactiveUserControl<NodeScriptViewModel>
|
||||
{
|
||||
private readonly Grid _grid;
|
||||
private readonly ItemsControl _nodesContainer;
|
||||
private readonly SelectionRectangle _selectionRectangle;
|
||||
private readonly ZoomBorder _zoomBorder;
|
||||
|
||||
public NodeScriptView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_grid = this.Find<Grid>("ContainerGrid");
|
||||
_zoomBorder = this.Find<ZoomBorder>("ZoomBorder");
|
||||
_nodesContainer = this.Find<ItemsControl>("NodesContainer");
|
||||
_selectionRectangle = this.Find<SelectionRectangle>("SelectionRectangle");
|
||||
_zoomBorder.PropertyChanged += ZoomBorderOnPropertyChanged;
|
||||
UpdateZoomBorderBackground();
|
||||
|
||||
_grid.AddHandler(PointerReleasedEvent, CanvasOnPointerReleased, RoutingStrategies.Direct | RoutingStrategies.Tunnel | RoutingStrategies.Bubble, true);
|
||||
this.WhenActivated(_ => ViewModel?.PickerPositionSubject.Subscribe(p =>
|
||||
{
|
||||
ViewModel.NodePickerViewModel.Position = p;
|
||||
_grid?.ContextFlyout?.ShowAt(_grid, true);
|
||||
}));
|
||||
}
|
||||
|
||||
private void CanvasOnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
// If the flyout handled the click, update the position of the node picker
|
||||
if (e.Handled && ViewModel != null)
|
||||
ViewModel.NodePickerViewModel.Position = e.GetPosition(_grid);
|
||||
}
|
||||
|
||||
private void ZoomBorderOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.Property.Name == nameof(_zoomBorder.Background))
|
||||
UpdateZoomBorderBackground();
|
||||
}
|
||||
|
||||
private void UpdateZoomBorderBackground()
|
||||
{
|
||||
if (_zoomBorder.Background is VisualBrush visualBrush)
|
||||
visualBrush.DestinationRect = new RelativeRect(_zoomBorder.OffsetX * -1, _zoomBorder.OffsetY * -1, 20, 20, RelativeUnit.Absolute);
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
private void ZoomBorder_OnZoomChanged(object sender, ZoomChangedEventArgs e)
|
||||
{
|
||||
UpdateZoomBorderBackground();
|
||||
}
|
||||
|
||||
private void SelectionRectangle_OnSelectionUpdated(object? sender, SelectionRectangleEventArgs e)
|
||||
{
|
||||
List<ItemContainerInfo> itemContainerInfos = _nodesContainer.ItemContainerGenerator.Containers.Where(c => c.ContainerControl.Bounds.Intersects(e.Rectangle)).ToList();
|
||||
List<NodeViewModel> nodes = itemContainerInfos.Where(c => c.Item is NodeViewModel).Select(c => (NodeViewModel) c.Item).ToList();
|
||||
ViewModel?.UpdateNodeSelection(nodes, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control));
|
||||
}
|
||||
|
||||
private void SelectionRectangle_OnSelectionFinished(object? sender, SelectionRectangleEventArgs e)
|
||||
{
|
||||
ViewModel?.FinishNodeSelection();
|
||||
}
|
||||
|
||||
private void ZoomBorder_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
if (!_selectionRectangle.IsSelecting)
|
||||
ViewModel?.ClearNodeSelection();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Events;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Screens.VisualScripting.Pins;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using Artemis.UI.Shared.Services.NodeEditor;
|
||||
using Artemis.UI.Shared.Services.NodeEditor.Commands;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using DynamicData;
|
||||
using DynamicData.Binding;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.VisualScripting;
|
||||
|
||||
public class NodeScriptViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private readonly INodeEditorService _nodeEditorService;
|
||||
private readonly INotificationService _notificationService;
|
||||
private readonly INodeVmFactory _nodeVmFactory;
|
||||
private readonly INodeService _nodeService;
|
||||
private readonly SourceList<NodeViewModel> _nodeViewModels;
|
||||
private readonly Subject<Point> _requestedPickerPositionSubject;
|
||||
|
||||
private DragCableViewModel? _dragViewModel;
|
||||
private List<NodeViewModel>? _initialNodeSelection;
|
||||
|
||||
public NodeScriptViewModel(NodeScript nodeScript, INodeVmFactory nodeVmFactory, INodeService nodeService, INodeEditorService nodeEditorService, INotificationService notificationService)
|
||||
{
|
||||
_nodeVmFactory = nodeVmFactory;
|
||||
_nodeService = nodeService;
|
||||
_nodeEditorService = nodeEditorService;
|
||||
_notificationService = notificationService;
|
||||
_nodeViewModels = new SourceList<NodeViewModel>();
|
||||
_requestedPickerPositionSubject = new Subject<Point>();
|
||||
|
||||
NodeScript = nodeScript;
|
||||
NodePickerViewModel = _nodeVmFactory.NodePickerViewModel(nodeScript);
|
||||
History = nodeEditorService.GetHistory(NodeScript);
|
||||
PickerPositionSubject = _requestedPickerPositionSubject.AsObservable();
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
Observable.FromEventPattern<SingleValueEventArgs<INode>>(x => NodeScript.NodeAdded += x, x => NodeScript.NodeAdded -= x)
|
||||
.Subscribe(e => HandleNodeAdded(e.EventArgs))
|
||||
.DisposeWith(d);
|
||||
Observable.FromEventPattern<SingleValueEventArgs<INode>>(x => NodeScript.NodeRemoved += x, x => NodeScript.NodeRemoved -= x)
|
||||
.Subscribe(e => HandleNodeRemoved(e.EventArgs))
|
||||
.DisposeWith(d);
|
||||
|
||||
nodeEditorService.GetHistory(NodeScript).Undo
|
||||
.Subscribe(c => _notificationService.CreateNotification().WithMessage(c != null ? $"Undid {c.DisplayName}" : "Nothing to undo").Show())
|
||||
.DisposeWith(d);
|
||||
nodeEditorService.GetHistory(NodeScript).Redo
|
||||
.Subscribe(c => _notificationService.CreateNotification().WithMessage(c != null ? $"Redid {c.DisplayName}" : "Nothing to redo").Show())
|
||||
.DisposeWith(d);
|
||||
});
|
||||
|
||||
// Create VMs for all nodes
|
||||
_nodeViewModels.Connect().Bind(out ReadOnlyObservableCollection<NodeViewModel> nodeViewModels).Subscribe();
|
||||
_nodeViewModels.Edit(l =>
|
||||
{
|
||||
foreach (INode nodeScriptNode in NodeScript.Nodes)
|
||||
l.Add(_nodeVmFactory.NodeViewModel(this, nodeScriptNode));
|
||||
});
|
||||
NodeViewModels = nodeViewModels;
|
||||
|
||||
NodeViewModels.ToObservableChangeSet().TransformMany(vm => vm.PinViewModels)
|
||||
.Bind(out ReadOnlyObservableCollection<PinViewModel> pinViewModels)
|
||||
.Subscribe();
|
||||
PinViewModels = pinViewModels;
|
||||
|
||||
// Observe all outgoing pin connections and create cables for them
|
||||
PinViewModels.ToObservableChangeSet()
|
||||
.Filter(p => p.Pin.Direction == PinDirection.Output)
|
||||
.TransformMany(p => p.Connections)
|
||||
.Filter(p => p.ConnectedTo.Any())
|
||||
.Transform(pin => _nodeVmFactory.CableViewModel(this, pin.ConnectedTo.First(), pin))
|
||||
.Bind(out ReadOnlyObservableCollection<CableViewModel> cableViewModels)
|
||||
.Subscribe();
|
||||
CableViewModels = cableViewModels;
|
||||
}
|
||||
|
||||
public NodeScript NodeScript { get; }
|
||||
public ReadOnlyObservableCollection<NodeViewModel> NodeViewModels { get; }
|
||||
public ReadOnlyObservableCollection<PinViewModel> PinViewModels { get; }
|
||||
public ReadOnlyObservableCollection<CableViewModel> CableViewModels { get; }
|
||||
public NodePickerViewModel NodePickerViewModel { get; }
|
||||
public NodeEditorHistory History { get; }
|
||||
public IObservable<Point> PickerPositionSubject { get; }
|
||||
|
||||
public DragCableViewModel? DragViewModel
|
||||
{
|
||||
get => _dragViewModel;
|
||||
set => RaiseAndSetIfChanged(ref _dragViewModel, value);
|
||||
}
|
||||
|
||||
public void DeleteSelectedNodes()
|
||||
{
|
||||
List<NodeViewModel> toRemove = NodeViewModels.Where(vm => vm.IsSelected && !vm.Node.IsDefaultNode && !vm.Node.IsExitNode).ToList();
|
||||
if (!toRemove.Any())
|
||||
return;
|
||||
|
||||
using (_nodeEditorService.CreateCommandScope(NodeScript, "Delete nodes"))
|
||||
{
|
||||
foreach (NodeViewModel node in toRemove)
|
||||
_nodeEditorService.ExecuteCommand(NodeScript, new DeleteNode(NodeScript, node.Node));
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateNodeSelection(List<NodeViewModel> nodes, bool expand, bool invert)
|
||||
{
|
||||
_initialNodeSelection ??= NodeViewModels.Where(vm => vm.IsSelected).ToList();
|
||||
|
||||
if (expand)
|
||||
{
|
||||
foreach (NodeViewModel nodeViewModel in nodes)
|
||||
nodeViewModel.IsSelected = true;
|
||||
}
|
||||
else if (invert)
|
||||
{
|
||||
foreach (NodeViewModel nodeViewModel in nodes)
|
||||
nodeViewModel.IsSelected = !_initialNodeSelection.Contains(nodeViewModel);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (NodeViewModel nodeViewModel in nodes)
|
||||
nodeViewModel.IsSelected = true;
|
||||
foreach (NodeViewModel nodeViewModel in NodeViewModels.Except(nodes))
|
||||
nodeViewModel.IsSelected = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void FinishNodeSelection()
|
||||
{
|
||||
_initialNodeSelection = null;
|
||||
}
|
||||
|
||||
public void ClearNodeSelection()
|
||||
{
|
||||
foreach (NodeViewModel nodeViewModel in NodeViewModels)
|
||||
nodeViewModel.IsSelected = false;
|
||||
}
|
||||
|
||||
public void StartNodeDrag(Point position)
|
||||
{
|
||||
foreach (NodeViewModel nodeViewModel in NodeViewModels)
|
||||
nodeViewModel.StartDrag(position);
|
||||
}
|
||||
|
||||
public void UpdateNodeDrag(Point position)
|
||||
{
|
||||
foreach (NodeViewModel nodeViewModel in NodeViewModels)
|
||||
nodeViewModel.UpdateDrag(position);
|
||||
}
|
||||
|
||||
public void FinishNodeDrag()
|
||||
{
|
||||
List<MoveNode> commands = NodeViewModels.Select(n => n.FinishDrag()).Where(c => c != null).Cast<MoveNode>().ToList();
|
||||
|
||||
if (!commands.Any())
|
||||
return;
|
||||
|
||||
if (commands.Count == 1)
|
||||
_nodeEditorService.ExecuteCommand(NodeScript, commands.First());
|
||||
|
||||
using NodeEditorCommandScope scope = _nodeEditorService.CreateCommandScope(NodeScript, $"Move {commands.Count} nodes");
|
||||
foreach (MoveNode moveNode in commands)
|
||||
_nodeEditorService.ExecuteCommand(NodeScript, moveNode);
|
||||
}
|
||||
|
||||
public bool UpdatePinDrag(PinViewModel sourcePinViewModel, PinViewModel? targetPinVmModel, Point position)
|
||||
{
|
||||
if (DragViewModel?.PinViewModel != sourcePinViewModel)
|
||||
DragViewModel = new DragCableViewModel(sourcePinViewModel);
|
||||
|
||||
DragViewModel.DragPoint = position;
|
||||
|
||||
return targetPinVmModel == null || targetPinVmModel.IsCompatibleWith(sourcePinViewModel);
|
||||
}
|
||||
|
||||
public void FinishPinDrag(PinViewModel sourcePinViewModel, PinViewModel? targetPinVmModel, Point position)
|
||||
{
|
||||
if (DragViewModel == null)
|
||||
return;
|
||||
|
||||
DragViewModel = null;
|
||||
|
||||
// If dropped on top of a compatible pin, connect to it
|
||||
if (targetPinVmModel != null && targetPinVmModel.IsCompatibleWith(sourcePinViewModel))
|
||||
_nodeEditorService.ExecuteCommand(NodeScript, new ConnectPins(sourcePinViewModel.Pin, targetPinVmModel.Pin));
|
||||
// If not dropped on a pin allow the user to create a new node
|
||||
else if (targetPinVmModel == null)
|
||||
{
|
||||
// If there is only one, spawn that straight away
|
||||
List<NodeData> singleCompatibleNode = _nodeService.AvailableNodes.Where(n => n.IsCompatibleWithPin(sourcePinViewModel.Pin)).ToList();
|
||||
if (singleCompatibleNode.Count == 1)
|
||||
{
|
||||
// Borrow the node picker to spawn the node in, even if it's never shown
|
||||
NodePickerViewModel.TargetPin = sourcePinViewModel.Pin;
|
||||
NodePickerViewModel.CreateNode(singleCompatibleNode.First());
|
||||
}
|
||||
// Otherwise show the user the picker by requesting it at the drop position
|
||||
else
|
||||
{
|
||||
_requestedPickerPositionSubject.OnNext(position);
|
||||
NodePickerViewModel.TargetPin = sourcePinViewModel.Pin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleNodeAdded(SingleValueEventArgs<INode> eventArgs)
|
||||
{
|
||||
_nodeViewModels.Add(_nodeVmFactory.NodeViewModel(this, eventArgs.Value));
|
||||
}
|
||||
|
||||
private void HandleNodeRemoved(SingleValueEventArgs<INode> eventArgs)
|
||||
{
|
||||
NodeViewModel? toRemove = NodeViewModels.FirstOrDefault(vm => ReferenceEquals(vm.Node, eventArgs.Value));
|
||||
if (toRemove != null)
|
||||
_nodeViewModels.Remove(toRemove);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:visualScripting="clr-namespace:Artemis.UI.Screens.VisualScripting"
|
||||
mc:Ignorable="d" d:DesignWidth="250" d:DesignHeight="150"
|
||||
x:Class="Artemis.UI.Screens.VisualScripting.NodeView"
|
||||
x:DataType="visualScripting:NodeViewModel">
|
||||
<UserControl.Styles>
|
||||
<Style Selector="Border.node-container">
|
||||
<Setter Property="CornerRadius" Value="6" />
|
||||
<Setter Property="Background">
|
||||
<SolidColorBrush Color="{DynamicResource SolidBackgroundFillColorBase}" Opacity="0.75"></SolidColorBrush>
|
||||
</Setter>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource CardStrokeColorDefaultBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Transitions">
|
||||
<Setter.Value>
|
||||
<Transitions>
|
||||
<BrushTransition Property="BorderBrush" Duration="0:0:0.2" Easing="CubicEaseOut" />
|
||||
</Transitions>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style Selector="Border.node-container-selected">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}" />
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
<Border Classes="node-container" Classes.node-container-selected="{CompiledBinding IsSelected}" PointerMoved="NodeContainer_OnPointerMoved">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<Border Grid.Row="0"
|
||||
CornerRadius="6 6 0 0"
|
||||
Cursor="Hand"
|
||||
PointerReleased="InputElement_OnPointerReleased"
|
||||
PointerMoved="InputElement_OnPointerMoved"
|
||||
ClipToBounds="True"
|
||||
Background="{DynamicResource ContentDialogBackground}">
|
||||
<Border Background="{DynamicResource TaskDialogHeaderBackground}">
|
||||
<Grid Classes="node-header"
|
||||
VerticalAlignment="Top"
|
||||
ColumnDefinitions="*,Auto">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Margin="10 0 0 0"
|
||||
Text="{CompiledBinding Node.Name}"
|
||||
ToolTip.Tip="{CompiledBinding Node.Description}">
|
||||
</TextBlock>
|
||||
<Button VerticalAlignment="Center"
|
||||
Classes="icon-button icon-button-small"
|
||||
Grid.Column="1"
|
||||
Margin="5"
|
||||
Command="{CompiledBinding DeleteNode}">
|
||||
<avalonia:MaterialIcon Kind="Close"></avalonia:MaterialIcon>
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Border>
|
||||
|
||||
|
||||
<Grid Grid.Row="1" ColumnDefinitions="Auto,*,Auto" Margin="4">
|
||||
<StackPanel Grid.Column="0" IsVisible="{CompiledBinding HasInputPins}">
|
||||
<ItemsControl Items="{CompiledBinding InputPinViewModels}" Margin="4 0" />
|
||||
<ItemsControl Items="{CompiledBinding InputPinCollectionViewModels}" />
|
||||
</StackPanel>
|
||||
|
||||
<ContentControl Grid.Column="1" Name="CustomViewModelContainer" Content="{CompiledBinding CustomNodeViewModel}" IsVisible="{CompiledBinding CustomNodeViewModel}" />
|
||||
|
||||
<StackPanel Grid.Column="2" IsVisible="{CompiledBinding HasOutputPins}">
|
||||
<ItemsControl Items="{CompiledBinding OutputPinViewModels}" Margin="4 0" />
|
||||
<ItemsControl Items="{CompiledBinding OutputPinCollectionViewModels}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
using Avalonia.VisualTree;
|
||||
|
||||
namespace Artemis.UI.Screens.VisualScripting;
|
||||
|
||||
public class NodeView : ReactiveUserControl<NodeViewModel>
|
||||
{
|
||||
private bool _dragging;
|
||||
|
||||
public NodeView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
#region Overrides of Layoutable
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
// Take the base implementation's size
|
||||
(double width, double height) = base.MeasureOverride(availableSize);
|
||||
|
||||
// Ceil the resulting size
|
||||
width = Math.Ceiling(width / 10.0) * 10.0;
|
||||
height = Math.Ceiling(height / 10.0) * 10.0;
|
||||
|
||||
return new Size(width, height);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
if (ViewModel == null || e.InitialPressMouseButton != MouseButton.Left)
|
||||
return;
|
||||
|
||||
if (_dragging)
|
||||
{
|
||||
_dragging = false;
|
||||
ViewModel.NodeScriptViewModel.FinishNodeDrag();
|
||||
e.Pointer.Capture(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
ViewModel.NodeScriptViewModel.UpdateNodeSelection(new List<NodeViewModel> {ViewModel}, e.KeyModifiers.HasFlag(KeyModifiers.Shift), e.KeyModifiers.HasFlag(KeyModifiers.Control));
|
||||
ViewModel.NodeScriptViewModel.FinishNodeSelection();
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void InputElement_OnPointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
PointerPoint point = e.GetCurrentPoint(this.FindAncestorOfType<Canvas>());
|
||||
if (ViewModel == null || !point.Properties.IsLeftButtonPressed)
|
||||
return;
|
||||
|
||||
if (!_dragging)
|
||||
{
|
||||
_dragging = true;
|
||||
|
||||
if (!ViewModel.IsSelected)
|
||||
{
|
||||
ViewModel.NodeScriptViewModel.UpdateNodeSelection(new List<NodeViewModel> {ViewModel}, false, false);
|
||||
ViewModel.NodeScriptViewModel.FinishNodeSelection();
|
||||
}
|
||||
|
||||
ViewModel.NodeScriptViewModel.StartNodeDrag(point.Position);
|
||||
e.Pointer.Capture((IInputElement?) sender);
|
||||
}
|
||||
|
||||
ViewModel.NodeScriptViewModel.UpdateNodeDrag(point.Position);
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void NodeContainer_OnPointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Screens.VisualScripting.Pins;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.NodeEditor;
|
||||
using Artemis.UI.Shared.Services.NodeEditor.Commands;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using DynamicData;
|
||||
using DynamicData.Binding;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.VisualScripting;
|
||||
|
||||
public class NodeViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private readonly INodeEditorService _nodeEditorService;
|
||||
|
||||
private ICustomNodeViewModel? _customNodeViewModel;
|
||||
private ReactiveCommand<Unit, Unit>? _deleteNode;
|
||||
private double _dragOffsetX;
|
||||
private double _dragOffsetY;
|
||||
private bool _isSelected;
|
||||
private double _startX;
|
||||
private double _startY;
|
||||
|
||||
private ObservableAsPropertyHelper<bool>? _isStaticNode;
|
||||
private ObservableAsPropertyHelper<bool>? _hasInputPins;
|
||||
private ObservableAsPropertyHelper<bool>? _hasOutputPins;
|
||||
|
||||
public NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService)
|
||||
{
|
||||
NodeScriptViewModel = nodeScriptViewModel;
|
||||
_nodeEditorService = nodeEditorService;
|
||||
Node = node;
|
||||
|
||||
DeleteNode = ReactiveCommand.Create(ExecuteDeleteNode, this.WhenAnyValue(vm => vm.IsStaticNode).Select(v => !v));
|
||||
|
||||
SourceList<IPin> nodePins = new();
|
||||
SourceList<IPinCollection> nodePinCollections = new();
|
||||
|
||||
// Create observable collections split up by direction
|
||||
nodePins.Connect()
|
||||
.Filter(n => n.Direction == PinDirection.Input)
|
||||
.Transform(p => (PinViewModel) nodeVmFactory.InputPinViewModel(p))
|
||||
.Bind(out ReadOnlyObservableCollection<PinViewModel> inputPins)
|
||||
.Subscribe();
|
||||
nodePins.Connect()
|
||||
.Filter(n => n.Direction == PinDirection.Output)
|
||||
.Transform(p => (PinViewModel) nodeVmFactory.OutputPinViewModel(p))
|
||||
.Bind(out ReadOnlyObservableCollection<PinViewModel> outputPins)
|
||||
.Subscribe();
|
||||
InputPinViewModels = inputPins;
|
||||
OutputPinViewModels = outputPins;
|
||||
|
||||
// Same again but for pin collections
|
||||
nodePinCollections.Connect()
|
||||
.Filter(n => n.Direction == PinDirection.Input)
|
||||
.Transform(c => (PinCollectionViewModel) nodeVmFactory.InputPinCollectionViewModel(c, nodeScriptViewModel))
|
||||
.Bind(out ReadOnlyObservableCollection<PinCollectionViewModel> inputPinCollections)
|
||||
.Subscribe();
|
||||
nodePinCollections.Connect()
|
||||
.Filter(n => n.Direction == PinDirection.Output)
|
||||
.Transform(c => (PinCollectionViewModel) nodeVmFactory.OutputPinCollectionViewModel(c, nodeScriptViewModel))
|
||||
.Bind(out ReadOnlyObservableCollection<PinCollectionViewModel> outputPinCollections)
|
||||
.Subscribe();
|
||||
InputPinCollectionViewModels = inputPinCollections;
|
||||
OutputPinCollectionViewModels = outputPinCollections;
|
||||
|
||||
// Create a single observable collection containing all pin view models
|
||||
InputPinViewModels.ToObservableChangeSet()
|
||||
.Merge(OutputPinViewModels.ToObservableChangeSet())
|
||||
.Merge(InputPinCollectionViewModels.ToObservableChangeSet().TransformMany(c => c.PinViewModels))
|
||||
.Merge(OutputPinCollectionViewModels.ToObservableChangeSet().TransformMany(c => c.PinViewModels))
|
||||
.Bind(out ReadOnlyObservableCollection<PinViewModel> pins)
|
||||
.Subscribe();
|
||||
|
||||
PinViewModels = pins;
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
_isStaticNode = Node.WhenAnyValue(n => n.IsDefaultNode, n => n.IsExitNode)
|
||||
.Select(tuple => tuple.Item1 || tuple.Item2)
|
||||
.ToProperty(this, model => model.IsStaticNode)
|
||||
.DisposeWith(d);
|
||||
_hasInputPins = InputPinViewModels.ToObservableChangeSet()
|
||||
.Merge(InputPinCollectionViewModels.ToObservableChangeSet().TransformMany(c => c.PinViewModels))
|
||||
.Any()
|
||||
.ToProperty(this, vm => vm.HasInputPins)
|
||||
.DisposeWith(d);
|
||||
_hasOutputPins = OutputPinViewModels.ToObservableChangeSet()
|
||||
.Merge(OutputPinCollectionViewModels.ToObservableChangeSet().TransformMany(c => c.PinViewModels))
|
||||
.Any()
|
||||
.ToProperty(this, vm => vm.HasOutputPins)
|
||||
.DisposeWith(d);
|
||||
|
||||
// Subscribe to pin changes
|
||||
Node.WhenAnyValue(n => n.Pins).Subscribe(p => nodePins.Edit(source =>
|
||||
{
|
||||
source.Clear();
|
||||
source.AddRange(p);
|
||||
})).DisposeWith(d);
|
||||
// Subscribe to pin collection changes
|
||||
Node.WhenAnyValue(n => n.PinCollections).Subscribe(c => nodePinCollections.Edit(source =>
|
||||
{
|
||||
source.Clear();
|
||||
source.AddRange(c);
|
||||
})).DisposeWith(d);
|
||||
|
||||
if (Node is Node coreNode)
|
||||
CustomNodeViewModel = coreNode.GetCustomViewModel(nodeScriptViewModel.NodeScript);
|
||||
});
|
||||
}
|
||||
|
||||
public bool IsStaticNode => _isStaticNode?.Value ?? true;
|
||||
public bool HasInputPins => _hasInputPins?.Value ?? false;
|
||||
public bool HasOutputPins => _hasOutputPins?.Value ?? false;
|
||||
|
||||
public NodeScriptViewModel NodeScriptViewModel { get; }
|
||||
public INode Node { get; }
|
||||
public ReadOnlyObservableCollection<PinViewModel> InputPinViewModels { get; }
|
||||
public ReadOnlyObservableCollection<PinCollectionViewModel> InputPinCollectionViewModels { get; }
|
||||
public ReadOnlyObservableCollection<PinViewModel> OutputPinViewModels { get; }
|
||||
public ReadOnlyObservableCollection<PinCollectionViewModel> OutputPinCollectionViewModels { get; }
|
||||
public ReadOnlyObservableCollection<PinViewModel> PinViewModels { get; }
|
||||
|
||||
public ICustomNodeViewModel? CustomNodeViewModel
|
||||
{
|
||||
get => _customNodeViewModel;
|
||||
set => RaiseAndSetIfChanged(ref _customNodeViewModel, value);
|
||||
}
|
||||
|
||||
public ReactiveCommand<Unit, Unit>? DeleteNode
|
||||
{
|
||||
get => _deleteNode;
|
||||
set => RaiseAndSetIfChanged(ref _deleteNode, value);
|
||||
}
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
set => RaiseAndSetIfChanged(ref _isSelected, value);
|
||||
}
|
||||
|
||||
public void StartDrag(Point mouseStartPosition)
|
||||
{
|
||||
if (!IsSelected)
|
||||
return;
|
||||
|
||||
_dragOffsetX = Node.X - mouseStartPosition.X;
|
||||
_dragOffsetY = Node.Y - mouseStartPosition.Y;
|
||||
_startX = Node.X;
|
||||
_startY = Node.Y;
|
||||
}
|
||||
|
||||
public void UpdateDrag(Point mousePosition)
|
||||
{
|
||||
if (!IsSelected)
|
||||
return;
|
||||
|
||||
Node.X = Math.Round((mousePosition.X + _dragOffsetX) / 10d, 0, MidpointRounding.AwayFromZero) * 10d;
|
||||
Node.Y = Math.Round((mousePosition.Y + _dragOffsetY) / 10d, 0, MidpointRounding.AwayFromZero) * 10d;
|
||||
}
|
||||
|
||||
public MoveNode? FinishDrag()
|
||||
{
|
||||
if (IsSelected && (Math.Abs(_startX - Node.X) > 0.01 || Math.Abs(_startY - Node.Y) > 0.01))
|
||||
return new MoveNode(Node, Node.X, Node.Y, _startX, _startY);
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ExecuteDeleteNode()
|
||||
{
|
||||
_nodeEditorService.ExecuteCommand(NodeScriptViewModel.NodeScript, new DeleteNode(NodeScriptViewModel.NodeScript, Node));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:pins="clr-namespace:Artemis.UI.Screens.VisualScripting.Pins"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.VisualScripting.Pins.InputPinCollectionView"
|
||||
x:DataType="pins:PinCollectionViewModel">
|
||||
<StackPanel>
|
||||
<Button Classes="icon-button icon-button-small pin-collection-button"
|
||||
ToolTip.Tip="Add new pin"
|
||||
Command="{CompiledBinding AddPin}">
|
||||
<avalonia:MaterialIcon Kind="Add"></avalonia:MaterialIcon>
|
||||
</Button>
|
||||
<ItemsControl Items="{CompiledBinding PinViewModels}" Margin="4 0">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="pins:PinViewModel">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<ContentControl Content="{CompiledBinding}"></ContentControl>
|
||||
<Button Classes="icon-button icon-button-small"
|
||||
ToolTip.Tip="Remove pin"
|
||||
Command="{CompiledBinding RemovePin}"
|
||||
CommandParameter="{CompiledBinding Pin}">
|
||||
<avalonia:MaterialIcon Kind="Close"></avalonia:MaterialIcon>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,20 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.VisualScripting.Pins
|
||||
{
|
||||
public partial class InputPinCollectionView : ReactiveUserControl<PinCollectionViewModel>
|
||||
{
|
||||
public InputPinCollectionView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Shared.Services.NodeEditor;
|
||||
|
||||
namespace Artemis.UI.Screens.VisualScripting.Pins;
|
||||
|
||||
public class InputPinCollectionViewModel : PinCollectionViewModel
|
||||
{
|
||||
private readonly INodeVmFactory _nodeVmFactory;
|
||||
public IPinCollection InputPinCollection { get; }
|
||||
|
||||
public InputPinCollectionViewModel(IPinCollection inputPinCollection, NodeScriptViewModel nodeScriptViewModel, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService)
|
||||
: base(inputPinCollection, nodeScriptViewModel, nodeEditorService)
|
||||
{
|
||||
_nodeVmFactory = nodeVmFactory;
|
||||
InputPinCollection = inputPinCollection;
|
||||
}
|
||||
|
||||
protected override PinViewModel CreatePinViewModel(IPin pin)
|
||||
{
|
||||
PinViewModel vm = _nodeVmFactory.InputPinViewModel(pin);
|
||||
vm.RemovePin = RemovePin;
|
||||
return vm;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
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:pins="clr-namespace:Artemis.UI.Screens.VisualScripting.Pins"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="200"
|
||||
x:Class="Artemis.UI.Screens.VisualScripting.Pins.InputPinView"
|
||||
x:DataType="pins:PinViewModel">
|
||||
<UserControl.Styles>
|
||||
<StyleInclude Source="/Screens/VisualScripting/VisualScripting.axaml" />
|
||||
<Style Selector="StackPanel#PinContainer Border#VisualPinPoint">
|
||||
<Setter Property="Background">
|
||||
<Setter.Value>
|
||||
<SolidColorBrush Color="{CompiledBinding DarkenedPinColor}"></SolidColorBrush>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="BorderBrush">
|
||||
<Setter.Value>
|
||||
<SolidColorBrush Color="{CompiledBinding PinColor}"></SolidColorBrush>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
<StackPanel Name="PinContainer" Orientation="Horizontal" Spacing="6">
|
||||
<Border Name="PinPoint">
|
||||
<Border Name="VisualPinPoint" />
|
||||
</Border>
|
||||
<TextBlock Name="PinName" VerticalAlignment="Center" Text="{CompiledBinding Pin.Name}" />
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,18 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Artemis.UI.Screens.VisualScripting.Pins;
|
||||
|
||||
public class InputPinView : PinView
|
||||
{
|
||||
public InputPinView()
|
||||
{
|
||||
InitializeComponent();
|
||||
InitializePin(this.Get<Border>("PinPoint"));
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
|
||||
namespace Artemis.UI.Screens.VisualScripting.Pins;
|
||||
|
||||
public class InputPinViewModel : PinViewModel
|
||||
{
|
||||
public InputPinViewModel(IPin inputPin, INodeService nodeService) : base(inputPin, nodeService)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:pins="clr-namespace:Artemis.UI.Screens.VisualScripting.Pins"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.VisualScripting.Pins.OutputPinCollectionView"
|
||||
x:DataType="pins:PinCollectionViewModel">
|
||||
<StackPanel>
|
||||
<Button Classes="icon-button icon-button-small pin-collection-button"
|
||||
ToolTip.Tip="Add new pin"
|
||||
HorizontalAlignment="Right"
|
||||
Command="{CompiledBinding AddPin}">
|
||||
<avalonia:MaterialIcon Kind="Add"></avalonia:MaterialIcon>
|
||||
</Button>
|
||||
<ItemsControl Items="{CompiledBinding PinViewModels}" Margin="4 0" HorizontalAlignment="Right">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="pins:PinViewModel">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Button Classes="icon-button icon-button-small"
|
||||
ToolTip.Tip="Remove pin"
|
||||
Command="{CompiledBinding RemovePin}"
|
||||
CommandParameter="{CompiledBinding Pin}">
|
||||
<avalonia:MaterialIcon Kind="Close"></avalonia:MaterialIcon>
|
||||
</Button>
|
||||
<ContentControl Content="{CompiledBinding}"></ContentControl>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,20 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.VisualScripting.Pins
|
||||
{
|
||||
public partial class OutputPinCollectionView : ReactiveUserControl<PinCollectionViewModel>
|
||||
{
|
||||
public OutputPinCollectionView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Shared.Services.NodeEditor;
|
||||
|
||||
namespace Artemis.UI.Screens.VisualScripting.Pins;
|
||||
|
||||
public class OutputPinCollectionViewModel : PinCollectionViewModel
|
||||
{
|
||||
private readonly INodeVmFactory _nodeVmFactory;
|
||||
public IPinCollection OutputPinCollection { get; }
|
||||
|
||||
public OutputPinCollectionViewModel(IPinCollection outputPinCollection, NodeScriptViewModel nodeScriptViewModel, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService)
|
||||
: base(outputPinCollection, nodeScriptViewModel, nodeEditorService)
|
||||
{
|
||||
_nodeVmFactory = nodeVmFactory;
|
||||
OutputPinCollection = outputPinCollection;
|
||||
}
|
||||
|
||||
protected override PinViewModel CreatePinViewModel(IPin pin)
|
||||
{
|
||||
PinViewModel vm = _nodeVmFactory.OutputPinViewModel(pin);
|
||||
vm.RemovePin = RemovePin;
|
||||
return vm;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
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:pins="clr-namespace:Artemis.UI.Screens.VisualScripting.Pins"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="200"
|
||||
x:Class="Artemis.UI.Screens.VisualScripting.Pins.OutputPinView"
|
||||
x:DataType="pins:PinViewModel">
|
||||
<UserControl.Styles>
|
||||
<StyleInclude Source="/Screens/VisualScripting/VisualScripting.axaml" />
|
||||
<Style Selector="StackPanel#PinContainer Border#VisualPinPoint">
|
||||
<Setter Property="Background">
|
||||
<Setter.Value>
|
||||
<SolidColorBrush Color="{CompiledBinding DarkenedPinColor}" />
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="BorderBrush">
|
||||
<Setter.Value>
|
||||
<SolidColorBrush Color="{CompiledBinding PinColor}" />
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
<StackPanel Name="PinContainer" Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Name="PinName" VerticalAlignment="Center" Text="{CompiledBinding Pin.Name}" />
|
||||
<Border Name="PinPoint">
|
||||
<Border Name="VisualPinPoint" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,18 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Artemis.UI.Screens.VisualScripting.Pins;
|
||||
|
||||
public class OutputPinView : PinView
|
||||
{
|
||||
public OutputPinView()
|
||||
{
|
||||
InitializeComponent();
|
||||
InitializePin(this.Get<Border>("PinPoint"));
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
|
||||
namespace Artemis.UI.Screens.VisualScripting.Pins;
|
||||
|
||||
public class OutputPinViewModel : PinViewModel
|
||||
{
|
||||
public OutputPinViewModel(IPin outputPin, INodeService nodeService) : base(outputPin, nodeService)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Events;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.NodeEditor;
|
||||
using Artemis.UI.Shared.Services.NodeEditor.Commands;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using DynamicData;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.VisualScripting.Pins;
|
||||
|
||||
public abstract class PinCollectionViewModel : ActivatableViewModelBase
|
||||
{
|
||||
protected PinCollectionViewModel(IPinCollection pinCollection, NodeScriptViewModel nodeScriptViewModel, INodeEditorService nodeEditorService)
|
||||
{
|
||||
PinCollection = pinCollection;
|
||||
PinViewModels = new ObservableCollection<PinViewModel>();
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
PinViewModels.Clear();
|
||||
PinViewModels.AddRange(PinCollection.Select(CreatePinViewModel));
|
||||
|
||||
Observable.FromEventPattern<SingleValueEventArgs<IPin>>(x => PinCollection.PinAdded += x, x => PinCollection.PinAdded -= x)
|
||||
.Subscribe(e => PinViewModels.Add(CreatePinViewModel(e.EventArgs.Value)))
|
||||
.DisposeWith(d);
|
||||
Observable.FromEventPattern<SingleValueEventArgs<IPin>>(x => PinCollection.PinRemoved += x, x => PinCollection.PinRemoved -= x)
|
||||
.Subscribe(e => PinViewModels.RemoveMany(PinViewModels.Where(p => p.Pin == e.EventArgs.Value).ToList()))
|
||||
.DisposeWith(d);
|
||||
});
|
||||
|
||||
AddPin = ReactiveCommand.Create(() => nodeEditorService.ExecuteCommand(nodeScriptViewModel.NodeScript, new AddPin(pinCollection)));
|
||||
RemovePin = ReactiveCommand.Create((IPin pin) => nodeEditorService.ExecuteCommand(nodeScriptViewModel.NodeScript, new RemovePin(pinCollection, pin)));
|
||||
}
|
||||
|
||||
public IPinCollection PinCollection { get; }
|
||||
public ReactiveCommand<Unit, Unit> AddPin { get; }
|
||||
public ReactiveCommand<IPin, Unit> RemovePin { get; }
|
||||
|
||||
public ObservableCollection<PinViewModel> PinViewModels { get; }
|
||||
|
||||
protected abstract PinViewModel CreatePinViewModel(IPin pin);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.ReactiveUI;
|
||||
using Avalonia.VisualTree;
|
||||
|
||||
namespace Artemis.UI.Screens.VisualScripting.Pins;
|
||||
|
||||
public class PinView : ReactiveUserControl<PinViewModel>
|
||||
{
|
||||
private bool _dragging;
|
||||
private Canvas? _container;
|
||||
private Border? _pinPoint;
|
||||
|
||||
protected void InitializePin(Border pinPoint)
|
||||
{
|
||||
_pinPoint = pinPoint;
|
||||
_pinPoint.PointerMoved += PinPointOnPointerMoved;
|
||||
_pinPoint.PointerReleased += PinPointOnPointerReleased;
|
||||
}
|
||||
|
||||
private void PinPointOnPointerMoved(object? sender, PointerEventArgs e)
|
||||
{
|
||||
if (ViewModel == null || _container == null || _pinPoint == null)
|
||||
return;
|
||||
|
||||
NodeScriptViewModel? nodeScriptViewModel = this.FindAncestorOfType<NodeScriptView>()?.ViewModel;
|
||||
PointerPoint point = e.GetCurrentPoint(_container);
|
||||
if (nodeScriptViewModel == null || !point.Properties.IsLeftButtonPressed)
|
||||
return;
|
||||
|
||||
if (!_dragging)
|
||||
e.Pointer.Capture(_pinPoint);
|
||||
|
||||
PinViewModel? targetPin = (_container.InputHitTest(point.Position) as Border)?.DataContext as PinViewModel;
|
||||
if (targetPin == ViewModel)
|
||||
targetPin = null;
|
||||
|
||||
_pinPoint.Cursor = new Cursor(nodeScriptViewModel.UpdatePinDrag(ViewModel, targetPin, point.Position) ? StandardCursorType.Hand : StandardCursorType.No);
|
||||
_dragging = true;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void PinPointOnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
if (!_dragging || ViewModel == null || _container == null || _pinPoint == null)
|
||||
return;
|
||||
|
||||
_dragging = false;
|
||||
e.Pointer.Capture(null);
|
||||
|
||||
PointerPoint point = e.GetCurrentPoint(_container);
|
||||
PinViewModel? targetPin = (_container.InputHitTest(point.Position) as Border)?.DataContext as PinViewModel;
|
||||
if (targetPin == ViewModel)
|
||||
targetPin = null;
|
||||
|
||||
this.FindAncestorOfType<NodeScriptView>()?.ViewModel?.FinishPinDrag(ViewModel, targetPin, point.Position);
|
||||
_pinPoint.Cursor = new Cursor(StandardCursorType.Hand);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
#region Overrides of Visual
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnAttachedToVisualTree(e);
|
||||
_container = this.FindAncestorOfType<Canvas>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Render(DrawingContext context)
|
||||
{
|
||||
base.Render(context);
|
||||
UpdatePosition();
|
||||
}
|
||||
|
||||
private void UpdatePosition()
|
||||
{
|
||||
if (_container == null || _pinPoint == null || ViewModel == null)
|
||||
return;
|
||||
|
||||
Matrix? transform = _pinPoint.TransformToVisual(_container);
|
||||
if (transform != null)
|
||||
ViewModel.Position = new Point(_pinPoint.Bounds.Width / 2, _pinPoint.Bounds.Height / 2).Transform(transform.Value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Events;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Extensions;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.Mixins;
|
||||
using Avalonia.Media;
|
||||
using DynamicData;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.VisualScripting.Pins;
|
||||
|
||||
public abstract class PinViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private readonly INodeService _nodeService;
|
||||
private ReactiveCommand<IPin, Unit>? _removePin;
|
||||
private Point _position;
|
||||
private Color _pinColor;
|
||||
private Color _darkenedPinColor;
|
||||
|
||||
protected PinViewModel(IPin pin, INodeService nodeService)
|
||||
{
|
||||
_nodeService = nodeService;
|
||||
|
||||
Pin = pin;
|
||||
SourceList<IPin> connectedPins = new();
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
Observable.FromEventPattern<SingleValueEventArgs<IPin>>(x => Pin.PinConnected += x, x => Pin.PinConnected -= x)
|
||||
.Subscribe(e => connectedPins.Add(e.EventArgs.Value))
|
||||
.DisposeWith(d);
|
||||
Observable.FromEventPattern<SingleValueEventArgs<IPin>>(x => Pin.PinDisconnected += x, x => Pin.PinDisconnected -= x)
|
||||
.Subscribe(e => connectedPins.Remove(e.EventArgs.Value))
|
||||
.DisposeWith(d);
|
||||
Pin.WhenAnyValue(p => p.Type).Subscribe(_ => UpdatePinColor()).DisposeWith(d);
|
||||
});
|
||||
|
||||
Connections = connectedPins.Connect().AsObservableList();
|
||||
connectedPins.AddRange(Pin.ConnectedTo);
|
||||
}
|
||||
|
||||
private void UpdatePinColor()
|
||||
{
|
||||
TypeColorRegistration registration = _nodeService.GetTypeColorRegistration(Pin.Type);
|
||||
PinColor = registration.Color.ToColor();
|
||||
DarkenedPinColor = registration.DarkenedColor.ToColor();
|
||||
}
|
||||
|
||||
public IObservableList<IPin> Connections { get; }
|
||||
|
||||
public IPin Pin { get; }
|
||||
|
||||
public Color PinColor
|
||||
{
|
||||
get => _pinColor;
|
||||
set => RaiseAndSetIfChanged(ref _pinColor, value);
|
||||
}
|
||||
|
||||
public Color DarkenedPinColor
|
||||
{
|
||||
get => _darkenedPinColor;
|
||||
set => RaiseAndSetIfChanged(ref _darkenedPinColor, value);
|
||||
}
|
||||
|
||||
public Point Position
|
||||
{
|
||||
get => _position;
|
||||
set => RaiseAndSetIfChanged(ref _position, value);
|
||||
}
|
||||
|
||||
public ReactiveCommand<IPin, Unit>? RemovePin
|
||||
{
|
||||
get => _removePin;
|
||||
set => RaiseAndSetIfChanged(ref _removePin, value);
|
||||
}
|
||||
|
||||
public bool IsCompatibleWith(PinViewModel pinViewModel)
|
||||
{
|
||||
if (pinViewModel.Pin.Direction == Pin.Direction || pinViewModel.Pin.Node == Pin.Node)
|
||||
return false;
|
||||
|
||||
return Pin.IsTypeCompatible(pinViewModel.Pin.Type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Design.PreviewWith>
|
||||
<Border Padding="20">
|
||||
<!-- Add Controls for Previewer Here -->
|
||||
</Border>
|
||||
</Design.PreviewWith>
|
||||
|
||||
<Style Selector=":is(Button).pin-collection-button">
|
||||
<!-- <Setter Property="Margin" Value="0 2"/> -->
|
||||
</Style>
|
||||
|
||||
<Style Selector="StackPanel#PinContainer">
|
||||
<Setter Property="Height" Value="26" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
</Style>
|
||||
<Style Selector="StackPanel#PinContainer Border#PinPoint">
|
||||
<Setter Property="Width" Value="13" />
|
||||
<Setter Property="Height" Value="13" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
</Style>
|
||||
<Style Selector="StackPanel#PinContainer Border#VisualPinPoint">
|
||||
<Setter Property="Width" Value="11" />
|
||||
<Setter Property="Height" Value="11" />
|
||||
<Setter Property="Margin" Value="2" />
|
||||
<Setter Property="CornerRadius" Value="6" />
|
||||
<Setter Property="Background" Value="DarkRed" />
|
||||
<Setter Property="BorderBrush" Value="Red" />
|
||||
<Setter Property="BorderThickness" Value="2" />
|
||||
</Style>
|
||||
<Style Selector="StackPanel#PinContainer TextBlock#PinName">
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
</Style>
|
||||
</Styles>
|
||||
Reference in new issue
Block a user