From 885cd852fc167a6c78071ac13bb9613e15f62063 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 14 Mar 2022 23:45:21 +0100 Subject: [PATCH] Node editor - Added node selection --- .../Controls/SelectionRectangle.cs | 24 +++---- .../Ninject/Factories/IVMFactory.cs | 2 +- .../VisualScripting/NodeScriptView.axaml | 19 +++++- .../VisualScripting/NodeScriptView.axaml.cs | 27 ++++++++ .../VisualScripting/NodeScriptViewModel.cs | 68 ++++++++++++++++--- .../Screens/VisualScripting/NodeView.axaml | 19 +++++- .../Screens/VisualScripting/NodeView.axaml.cs | 42 ++++++++++++ .../Screens/VisualScripting/NodeViewModel.cs | 37 ++++++++-- 8 files changed, 205 insertions(+), 33 deletions(-) diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs b/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs index c51659c1b..09e102ae0 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/SelectionRectangle.cs @@ -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; diff --git a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs index 652feee62..140914580 100644 --- a/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Avalonia/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -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 diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml index fd189eb1d..b08cf9b2c 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml @@ -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"> @@ -52,7 +56,7 @@ - + @@ -65,6 +69,17 @@ + + + + + + diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs index c3094011b..9ace2bbf8 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs @@ -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("NodesContainer"); _zoomBorder = this.Find("ZoomBorder"); _grid = this.Find("ContainerGrid"); + _selectionRectangle = this.Find("SelectionRectangle"); _zoomBorder.PropertyChanged += ZoomBorderOnPropertyChanged; UpdateZoomBorderBackground(); @@ -56,5 +65,23 @@ namespace Artemis.UI.Screens.VisualScripting { UpdateZoomBorderBackground(); } + + private void SelectionRectangle_OnSelectionUpdated(object? sender, SelectionRectangleEventArgs e) + { + List itemContainerInfos = _nodesContainer.ItemContainerGenerator.Containers.Where(c => c.ContainerControl.Bounds.Intersects(e.Rectangle)).ToList(); + List 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(); + } } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs index 88caf49a6..26f42fd97 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeScriptViewModel.cs @@ -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? _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(); foreach (INode nodeScriptNode in NodeScript.Nodes) - NodeViewModels.Add(_nodeVmFactory.NodeViewModel(NodeScript, nodeScriptNode)); + NodeViewModels.Add(_nodeVmFactory.NodeViewModel(this, nodeScriptNode)); + + CableViewModels = new ObservableCollection(); } public NodeScript NodeScript { get; } @@ -51,9 +50,60 @@ public class NodeScriptViewModel : ActivatableViewModelBase public NodePickerViewModel NodePickerViewModel { get; } public NodeEditorHistory History { get; } + public void UpdateNodeSelection(List 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 eventArgs) { - NodeViewModels.Add(_nodeVmFactory.NodeViewModel(NodeScript, eventArgs.Value)); + NodeViewModels.Add(_nodeVmFactory.NodeViewModel(this, eventArgs.Value)); } private void HandleNodeRemoved(SingleValueEventArgs eventArgs) diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml index a3ce0fbda..33b2bb1b2 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml @@ -13,14 +13,29 @@ + + + + + + + + + - + - + diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs index 9244d4791..6a3cf384a 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeView.axaml.cs @@ -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 { + private bool _dragging; + public NodeView() { InitializeComponent(); @@ -33,4 +39,40 @@ public class NodeView : ReactiveUserControl { 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 {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()); + 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; + } } \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs index 07be3a158..13eb8c500 100644 --- a/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/VisualScripting/NodeViewModel.cs @@ -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? _deleteNode; private ObservableAsPropertyHelper? _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 InputPinViewModels { get; } public ReadOnlyObservableCollection 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)); } } \ No newline at end of file