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:
parent
034879a2c9
commit
885cd852fc
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user