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

View File

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

View File

@ -4,6 +4,7 @@
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">
@ -23,7 +24,10 @@
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
Background="{DynamicResource LargeCheckerboardBrush}"
ZoomChanged="ZoomBorder_OnZoomChanged">
ZoomChanged="ZoomBorder_OnZoomChanged"
MaxZoomX="1"
MaxZoomY="1"
PointerReleased="ZoomBorder_OnPointerReleased">
<Grid Name="ContainerGrid" Background="Transparent">
<Grid.ContextFlyout>
<Flyout FlyoutPresenterClasses="node-picker-flyout">
@ -52,7 +56,7 @@
</ItemsControl>
<!-- Nodes -->
<ItemsControl Items="{CompiledBinding NodeViewModels}">
<ItemsControl Name="NodesContainer" Items="{CompiledBinding NodeViewModels}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
@ -65,6 +69,17 @@
</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>

View File

@ -1,6 +1,11 @@
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;
@ -15,13 +20,17 @@ namespace Artemis.UI.Screens.VisualScripting
{
private readonly ZoomBorder _zoomBorder;
private readonly Grid _grid;
private readonly ItemsControl _nodesContainer;
private readonly SelectionRectangle _selectionRectangle;
public NodeScriptView()
{
InitializeComponent();
_nodesContainer = this.Find<ItemsControl>("NodesContainer");
_zoomBorder = this.Find<ZoomBorder>("ZoomBorder");
_grid = this.Find<Grid>("ContainerGrid");
_selectionRectangle = this.Find<SelectionRectangle>("SelectionRectangle");
_zoomBorder.PropertyChanged += ZoomBorderOnPropertyChanged;
UpdateZoomBorderBackground();
@ -56,5 +65,23 @@ namespace Artemis.UI.Screens.VisualScripting
{
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.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.Core.Events;
using Artemis.Core.Services;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.NodeEditor;
@ -16,19 +16,16 @@ namespace Artemis.UI.Screens.VisualScripting;
public class NodeScriptViewModel : ActivatableViewModelBase
{
private readonly INodeService _nodeService;
private readonly INodeEditorService _nodeEditorService;
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;
_nodeService = nodeService;
_nodeEditorService = nodeEditorService;
NodeScript = nodeScript;
NodePickerViewModel = _nodeVmFactory.NodePickerViewModel(nodeScript);
History = _nodeEditorService.GetHistory(NodeScript);
History = nodeEditorService.GetHistory(NodeScript);
this.WhenActivated(d =>
{
@ -42,7 +39,9 @@ public class NodeScriptViewModel : ActivatableViewModelBase
NodeViewModels = new ObservableCollection<NodeViewModel>();
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; }
@ -51,9 +50,60 @@ public class NodeScriptViewModel : ActivatableViewModelBase
public NodePickerViewModel NodePickerViewModel { 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)
{
NodeViewModels.Add(_nodeVmFactory.NodeViewModel(NodeScript, eventArgs.Value));
NodeViewModels.Add(_nodeVmFactory.NodeViewModel(this, eventArgs.Value));
}
private void HandleNodeRemoved(SingleValueEventArgs<INode> eventArgs)

View File

@ -13,14 +13,29 @@
<Setter Property="Background" Value="{DynamicResource ContentDialogBackground}" />
<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>
<Style Selector="ContentControl#CustomViewModelContainer">
<Setter Property="Margin" Value="20 0"></Setter>
</Style>
</UserControl.Styles>
<Border Classes="node-container">
<Border Classes="node-container" Classes.node-container-selected="{CompiledBinding IsSelected}">
<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"
VerticalAlignment="Top"
ColumnDefinitions="*,Auto">

View File

@ -1,12 +1,18 @@
using System;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Controls.PanAndZoom;
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();
@ -33,4 +39,40 @@ public class NodeView : ReactiveUserControl<NodeViewModel>
{
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 Artemis.Core;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.SurfaceEditor;
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 ReactiveUI;
@ -16,16 +18,18 @@ namespace Artemis.UI.Screens.VisualScripting;
public class NodeViewModel : ActivatableViewModelBase
{
private readonly NodeScript _nodeScript;
private readonly INodeEditorService _nodeEditorService;
private ICustomNodeViewModel? _customNodeViewModel;
private ReactiveCommand<Unit, Unit>? _deleteNode;
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;
Node = node;
@ -54,6 +58,7 @@ public class NodeViewModel : ActivatableViewModelBase
public bool IsStaticNode => _isStaticNode?.Value ?? true;
public NodeScriptViewModel NodeScriptViewModel { get; set; }
public INode Node { get; }
public ReadOnlyObservableCollection<PinViewModel> InputPinViewModels { get; }
public ReadOnlyObservableCollection<PinCollectionViewModel> InputPinCollectionViewModels { get; }
@ -72,8 +77,32 @@ public class NodeViewModel : ActivatableViewModelBase
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()
{
_nodeEditorService.ExecuteCommand(_nodeScript, new DeleteNode(_nodeScript, Node));
_nodeEditorService.ExecuteCommand(NodeScriptViewModel.NodeScript, new DeleteNode(NodeScriptViewModel.NodeScript, Node));
}
}