1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Node editor - Added node selection

This commit is contained in:
Robert 2022-03-14 23:45:21 +01:00
parent 034879a2c9
commit 885cd852fc
8 changed files with 205 additions and 33 deletions

View File

@ -166,23 +166,20 @@ public class SelectionRectangle : Control
((SelectionRectangle) sender).SubscribeToInputElement(); ((SelectionRectangle) sender).SubscribeToInputElement();
} }
private void ParentOnPointerMoved(object? sender, PointerEventArgs e)
private void ParentOnPointerPressed(object? sender, PointerPressedEventArgs e)
{ {
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return; return;
e.Pointer.Capture(this); // Capture the pointer and initialize dragging the first time it moves
_startPosition = e.GetPosition(Parent);
_absoluteStartPosition = e.GetPosition(VisualRoot);
_displayRect = null;
}
private void ParentOnPointerMoved(object? sender, PointerEventArgs e)
{
if (!ReferenceEquals(e.Pointer.Captured, this)) if (!ReferenceEquals(e.Pointer.Captured, this))
return; {
e.Pointer.Capture(this);
_startPosition = e.GetPosition(Parent);
_absoluteStartPosition = e.GetPosition(VisualRoot);
_displayRect = null;
}
Point currentPosition = e.GetPosition(Parent); Point currentPosition = e.GetPosition(Parent);
Point absoluteCurrentPosition = e.GetPosition(VisualRoot); Point absoluteCurrentPosition = e.GetPosition(VisualRoot);
@ -223,7 +220,6 @@ public class SelectionRectangle : Control
{ {
if (_oldInputElement != null) if (_oldInputElement != null)
{ {
_oldInputElement.PointerPressed -= ParentOnPointerPressed;
_oldInputElement.PointerMoved -= ParentOnPointerMoved; _oldInputElement.PointerMoved -= ParentOnPointerMoved;
_oldInputElement.PointerReleased -= ParentOnPointerReleased; _oldInputElement.PointerReleased -= ParentOnPointerReleased;
} }
@ -232,7 +228,6 @@ public class SelectionRectangle : Control
if (InputElement != null) if (InputElement != null)
{ {
InputElement.PointerPressed += ParentOnPointerPressed;
InputElement.PointerMoved += ParentOnPointerMoved; InputElement.PointerMoved += ParentOnPointerMoved;
InputElement.PointerReleased += ParentOnPointerReleased; InputElement.PointerReleased += ParentOnPointerReleased;
} }
@ -259,7 +254,6 @@ public class SelectionRectangle : Control
{ {
if (_oldInputElement != null) if (_oldInputElement != null)
{ {
_oldInputElement.PointerPressed -= ParentOnPointerPressed;
_oldInputElement.PointerMoved -= ParentOnPointerMoved; _oldInputElement.PointerMoved -= ParentOnPointerMoved;
_oldInputElement.PointerReleased -= ParentOnPointerReleased; _oldInputElement.PointerReleased -= ParentOnPointerReleased;
_oldInputElement = null; _oldInputElement = null;

View File

@ -94,7 +94,7 @@ namespace Artemis.UI.Ninject.Factories
{ {
NodeScriptViewModel NodeScriptViewModel(NodeScript nodeScript); NodeScriptViewModel NodeScriptViewModel(NodeScript nodeScript);
NodePickerViewModel NodePickerViewModel(NodeScript nodeScript); NodePickerViewModel NodePickerViewModel(NodeScript nodeScript);
NodeViewModel NodeViewModel(NodeScript nodeScript, INode node); NodeViewModel NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node);
} }
public interface INodePinVmFactory public interface INodePinVmFactory

View File

@ -4,6 +4,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:paz="clr-namespace:Avalonia.Controls.PanAndZoom;assembly=Avalonia.Controls.PanAndZoom" xmlns:paz="clr-namespace:Avalonia.Controls.PanAndZoom;assembly=Avalonia.Controls.PanAndZoom"
xmlns:visualScripting="clr-namespace:Artemis.UI.Screens.VisualScripting" 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" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.VisualScripting.NodeScriptView" x:Class="Artemis.UI.Screens.VisualScripting.NodeScriptView"
x:DataType="visualScripting:NodeScriptViewModel"> x:DataType="visualScripting:NodeScriptViewModel">
@ -23,7 +24,10 @@
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Background="{DynamicResource LargeCheckerboardBrush}" Background="{DynamicResource LargeCheckerboardBrush}"
ZoomChanged="ZoomBorder_OnZoomChanged"> ZoomChanged="ZoomBorder_OnZoomChanged"
MaxZoomX="1"
MaxZoomY="1"
PointerReleased="ZoomBorder_OnPointerReleased">
<Grid Name="ContainerGrid" Background="Transparent"> <Grid Name="ContainerGrid" Background="Transparent">
<Grid.ContextFlyout> <Grid.ContextFlyout>
<Flyout FlyoutPresenterClasses="node-picker-flyout"> <Flyout FlyoutPresenterClasses="node-picker-flyout">
@ -52,7 +56,7 @@
</ItemsControl> </ItemsControl>
<!-- Nodes --> <!-- Nodes -->
<ItemsControl Items="{CompiledBinding NodeViewModels}"> <ItemsControl Name="NodesContainer" Items="{CompiledBinding NodeViewModels}">
<ItemsControl.ItemsPanel> <ItemsControl.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
<Canvas /> <Canvas />
@ -65,6 +69,17 @@
</Style> </Style>
</ItemsControl.Styles> </ItemsControl.Styles>
</ItemsControl> </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> </Grid>
</paz:ZoomBorder> </paz:ZoomBorder>

View File

@ -1,6 +1,11 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using Artemis.UI.Shared.Controls;
using Artemis.UI.Shared.Events;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Generators;
using Avalonia.Controls.PanAndZoom; using Avalonia.Controls.PanAndZoom;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
@ -15,13 +20,17 @@ namespace Artemis.UI.Screens.VisualScripting
{ {
private readonly ZoomBorder _zoomBorder; private readonly ZoomBorder _zoomBorder;
private readonly Grid _grid; private readonly Grid _grid;
private readonly ItemsControl _nodesContainer;
private readonly SelectionRectangle _selectionRectangle;
public NodeScriptView() public NodeScriptView()
{ {
InitializeComponent(); InitializeComponent();
_nodesContainer = this.Find<ItemsControl>("NodesContainer");
_zoomBorder = this.Find<ZoomBorder>("ZoomBorder"); _zoomBorder = this.Find<ZoomBorder>("ZoomBorder");
_grid = this.Find<Grid>("ContainerGrid"); _grid = this.Find<Grid>("ContainerGrid");
_selectionRectangle = this.Find<SelectionRectangle>("SelectionRectangle");
_zoomBorder.PropertyChanged += ZoomBorderOnPropertyChanged; _zoomBorder.PropertyChanged += ZoomBorderOnPropertyChanged;
UpdateZoomBorderBackground(); UpdateZoomBorderBackground();
@ -56,5 +65,23 @@ namespace Artemis.UI.Screens.VisualScripting
{ {
UpdateZoomBorderBackground(); 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();
}
} }
} }

View File

@ -1,10 +1,10 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Events; using Artemis.Core.Events;
using Artemis.Core.Services;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.NodeEditor; using Artemis.UI.Shared.Services.NodeEditor;
@ -16,19 +16,16 @@ namespace Artemis.UI.Screens.VisualScripting;
public class NodeScriptViewModel : ActivatableViewModelBase public class NodeScriptViewModel : ActivatableViewModelBase
{ {
private readonly INodeService _nodeService;
private readonly INodeEditorService _nodeEditorService;
private readonly INodeVmFactory _nodeVmFactory; private readonly INodeVmFactory _nodeVmFactory;
private List<NodeViewModel>? _initialNodeSelection;
public NodeScriptViewModel(NodeScript nodeScript, INodeVmFactory nodeVmFactory, INodeService nodeService, INodeEditorService nodeEditorService) public NodeScriptViewModel(NodeScript nodeScript, INodeVmFactory nodeVmFactory, INodeEditorService nodeEditorService)
{ {
_nodeVmFactory = nodeVmFactory; _nodeVmFactory = nodeVmFactory;
_nodeService = nodeService;
_nodeEditorService = nodeEditorService;
NodeScript = nodeScript; NodeScript = nodeScript;
NodePickerViewModel = _nodeVmFactory.NodePickerViewModel(nodeScript); NodePickerViewModel = _nodeVmFactory.NodePickerViewModel(nodeScript);
History = _nodeEditorService.GetHistory(NodeScript); History = nodeEditorService.GetHistory(NodeScript);
this.WhenActivated(d => this.WhenActivated(d =>
{ {
@ -42,7 +39,9 @@ public class NodeScriptViewModel : ActivatableViewModelBase
NodeViewModels = new ObservableCollection<NodeViewModel>(); NodeViewModels = new ObservableCollection<NodeViewModel>();
foreach (INode nodeScriptNode in NodeScript.Nodes) foreach (INode nodeScriptNode in NodeScript.Nodes)
NodeViewModels.Add(_nodeVmFactory.NodeViewModel(NodeScript, nodeScriptNode)); NodeViewModels.Add(_nodeVmFactory.NodeViewModel(this, nodeScriptNode));
CableViewModels = new ObservableCollection<CableViewModel>();
} }
public NodeScript NodeScript { get; } public NodeScript NodeScript { get; }
@ -51,9 +50,60 @@ public class NodeScriptViewModel : ActivatableViewModelBase
public NodePickerViewModel NodePickerViewModel { get; } public NodePickerViewModel NodePickerViewModel { get; }
public NodeEditorHistory History { get; } public NodeEditorHistory History { get; }
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.SaveDragOffset(position);
}
public void UpdateNodeDrag(Point position)
{
foreach (NodeViewModel nodeViewModel in NodeViewModels)
nodeViewModel.UpdatePosition(position);
}
public void FinishNodeDrag()
{
// TODO: Command
}
private void HandleNodeAdded(SingleValueEventArgs<INode> eventArgs) private void HandleNodeAdded(SingleValueEventArgs<INode> eventArgs)
{ {
NodeViewModels.Add(_nodeVmFactory.NodeViewModel(NodeScript, eventArgs.Value)); NodeViewModels.Add(_nodeVmFactory.NodeViewModel(this, eventArgs.Value));
} }
private void HandleNodeRemoved(SingleValueEventArgs<INode> eventArgs) private void HandleNodeRemoved(SingleValueEventArgs<INode> eventArgs)

View File

@ -13,14 +13,29 @@
<Setter Property="Background" Value="{DynamicResource ContentDialogBackground}" /> <Setter Property="Background" Value="{DynamicResource ContentDialogBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource CardStrokeColorDefaultBrush}" /> <Setter Property="BorderBrush" Value="{DynamicResource CardStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="1" /> <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> </Style>
<Style Selector="ContentControl#CustomViewModelContainer"> <Style Selector="ContentControl#CustomViewModelContainer">
<Setter Property="Margin" Value="20 0"></Setter> <Setter Property="Margin" Value="20 0"></Setter>
</Style> </Style>
</UserControl.Styles> </UserControl.Styles>
<Border Classes="node-container"> <Border Classes="node-container" Classes.node-container-selected="{CompiledBinding IsSelected}">
<Grid RowDefinitions="Auto,*"> <Grid RowDefinitions="Auto,*">
<Border Grid.Row="0" Background="{DynamicResource TaskDialogHeaderBackground}" CornerRadius="6 6 0 0"> <Border Grid.Row="0"
Background="{DynamicResource TaskDialogHeaderBackground}"
CornerRadius="6 6 0 0"
Cursor="Hand"
PointerReleased="InputElement_OnPointerReleased"
PointerMoved="InputElement_OnPointerMoved">
<Grid Classes="node-header" <Grid Classes="node-header"
VerticalAlignment="Top" VerticalAlignment="Top"
ColumnDefinitions="*,Auto"> ColumnDefinitions="*,Auto">

View File

@ -1,12 +1,18 @@
using System; using System;
using System.Collections.Generic;
using Avalonia; using Avalonia;
using Avalonia.Controls.PanAndZoom;
using Avalonia.Input;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Avalonia.VisualTree;
namespace Artemis.UI.Screens.VisualScripting; namespace Artemis.UI.Screens.VisualScripting;
public class NodeView : ReactiveUserControl<NodeViewModel> public class NodeView : ReactiveUserControl<NodeViewModel>
{ {
private bool _dragging;
public NodeView() public NodeView()
{ {
InitializeComponent(); InitializeComponent();
@ -33,4 +39,40 @@ public class NodeView : ReactiveUserControl<NodeViewModel>
{ {
AvaloniaXamlLoader.Load(this); 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);
return;
}
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<ZoomBorder>());
if (ViewModel == null || !point.Properties.IsLeftButtonPressed)
return;
if (!_dragging)
{
_dragging = true;
ViewModel.NodeScriptViewModel.StartNodeDrag(point.Position);
e.Pointer.Capture((IInputElement?) sender);
}
ViewModel.NodeScriptViewModel.UpdateNodeDrag(point.Position);
e.Handled = true;
}
} }

View File

@ -4,10 +4,12 @@ using System.Reactive;
using System.Reactive.Linq; using System.Reactive.Linq;
using Artemis.Core; using Artemis.Core;
using Artemis.UI.Ninject.Factories; using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.SurfaceEditor;
using Artemis.UI.Screens.VisualScripting.Pins; using Artemis.UI.Screens.VisualScripting.Pins;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.NodeEditor; using Artemis.UI.Shared.Services.NodeEditor;
using Artemis.UI.Shared.Services.NodeEditor.Commands; using Artemis.UI.Shared.Services.NodeEditor.Commands;
using Avalonia;
using Avalonia.Controls.Mixins; using Avalonia.Controls.Mixins;
using DynamicData; using DynamicData;
using ReactiveUI; using ReactiveUI;
@ -16,16 +18,18 @@ namespace Artemis.UI.Screens.VisualScripting;
public class NodeViewModel : ActivatableViewModelBase public class NodeViewModel : ActivatableViewModelBase
{ {
private readonly NodeScript _nodeScript;
private readonly INodeEditorService _nodeEditorService; private readonly INodeEditorService _nodeEditorService;
private ICustomNodeViewModel? _customNodeViewModel; private ICustomNodeViewModel? _customNodeViewModel;
private ReactiveCommand<Unit, Unit>? _deleteNode; private ReactiveCommand<Unit, Unit>? _deleteNode;
private ObservableAsPropertyHelper<bool>? _isStaticNode; private ObservableAsPropertyHelper<bool>? _isStaticNode;
private double _dragOffsetX;
private double _dragOffsetY;
private bool _isSelected;
public NodeViewModel(NodeScript nodeScript, INode node, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService) public NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node, INodePinVmFactory nodePinVmFactory, INodeEditorService nodeEditorService)
{ {
_nodeScript = nodeScript; NodeScriptViewModel = nodeScriptViewModel;
_nodeEditorService = nodeEditorService; _nodeEditorService = nodeEditorService;
Node = node; Node = node;
@ -54,6 +58,7 @@ public class NodeViewModel : ActivatableViewModelBase
public bool IsStaticNode => _isStaticNode?.Value ?? true; public bool IsStaticNode => _isStaticNode?.Value ?? true;
public NodeScriptViewModel NodeScriptViewModel { get; set; }
public INode Node { get; } public INode Node { get; }
public ReadOnlyObservableCollection<PinViewModel> InputPinViewModels { get; } public ReadOnlyObservableCollection<PinViewModel> InputPinViewModels { get; }
public ReadOnlyObservableCollection<PinCollectionViewModel> InputPinCollectionViewModels { get; } public ReadOnlyObservableCollection<PinCollectionViewModel> InputPinCollectionViewModels { get; }
@ -72,8 +77,32 @@ public class NodeViewModel : ActivatableViewModelBase
set => RaiseAndSetIfChanged(ref _deleteNode, value); set => RaiseAndSetIfChanged(ref _deleteNode, value);
} }
public bool IsSelected
{
get => _isSelected;
set => RaiseAndSetIfChanged(ref _isSelected, value);
}
public void SaveDragOffset(Point mouseStartPosition)
{
if (!IsSelected)
return;
_dragOffsetX = Node.X - mouseStartPosition.X;
_dragOffsetY = Node.Y - mouseStartPosition.Y;
}
public void UpdatePosition(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;
}
private void ExecuteDeleteNode() private void ExecuteDeleteNode()
{ {
_nodeEditorService.ExecuteCommand(_nodeScript, new DeleteNode(_nodeScript, Node)); _nodeEditorService.ExecuteCommand(NodeScriptViewModel.NodeScript, new DeleteNode(NodeScriptViewModel.NodeScript, Node));
} }
} }