You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
547 lines
19 KiB
547 lines
19 KiB
using ReactiveUI.Avalonia;
|
|
using ReactiveUI;
|
|
using Avalonia.Markup.Xaml;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Controls.Shapes;
|
|
using Avalonia.Input;
|
|
using Avalonia;
|
|
using Avalonia.Media;
|
|
using Avalonia.Collections;
|
|
using Avalonia.VisualTree;
|
|
using AuroraDesk.Presentation.ViewModels.Pages;
|
|
using AuroraDesk.Core.Entities;
|
|
using System.Linq;
|
|
using System.Reactive.Disposables;
|
|
using System;
|
|
using Avalonia.Layout;
|
|
|
|
namespace AuroraDesk.Presentation.Views.Pages;
|
|
|
|
public partial class NodeCanvasPageView : ReactiveUserControl<NodeCanvasPageViewModel>
|
|
{
|
|
private Canvas? _canvasContainer;
|
|
private Canvas? _gridBackgroundLayer;
|
|
private ScrollViewer? _canvasScrollViewer;
|
|
private Node? _draggedNode;
|
|
private Point? _dragStartPoint;
|
|
private ConnectionPoint? _hoveredConnectionPoint;
|
|
private Path? _tempConnectionLine;
|
|
private const double GridSize = 20;
|
|
|
|
public NodeCanvasPageView()
|
|
{
|
|
InitializeComponent();
|
|
}
|
|
|
|
private void InitializeComponent()
|
|
{
|
|
AvaloniaXamlLoader.Load(this);
|
|
|
|
// 延迟初始化,等待加载完成
|
|
this.Loaded += (s, e) =>
|
|
{
|
|
_canvasContainer = this.FindControl<Canvas>("CanvasContainer");
|
|
_gridBackgroundLayer = this.FindControl<Canvas>("GridBackgroundLayer");
|
|
_canvasScrollViewer = this.FindControl<ScrollViewer>("CanvasScrollViewer");
|
|
|
|
if (_canvasContainer != null)
|
|
{
|
|
_canvasContainer.PointerPressed += OnCanvasPointerPressed;
|
|
_canvasContainer.PointerMoved += OnCanvasPointerMoved;
|
|
_canvasContainer.PointerReleased += OnCanvasPointerReleased;
|
|
|
|
// 在 Canvas 上添加拖拽事件
|
|
_canvasContainer.AddHandler(DragDrop.DragOverEvent, OnCanvasDragOver);
|
|
_canvasContainer.AddHandler(DragDrop.DropEvent, OnCanvasDrop);
|
|
|
|
// 监听 Canvas 尺寸变化,更新网格
|
|
_canvasContainer.SizeChanged += (sender, args) => DrawGridBackground();
|
|
}
|
|
|
|
// 在 ScrollViewer 上也添加拖拽事件,确保可以接收从左侧拖拽的组件
|
|
if (_canvasScrollViewer != null)
|
|
{
|
|
_canvasScrollViewer.AddHandler(DragDrop.DragOverEvent, OnCanvasDragOver);
|
|
_canvasScrollViewer.AddHandler(DragDrop.DropEvent, OnCanvasDrop);
|
|
}
|
|
|
|
// 设置组件库的拖拽支持
|
|
SetupTemplateDragAndDrop();
|
|
|
|
// 绘制网格背景
|
|
DrawGridBackground();
|
|
|
|
// 监听连接变化,更新连接线
|
|
if (ViewModel != null)
|
|
{
|
|
ViewModel.WhenAnyValue(x => x.Connections)
|
|
.Subscribe(_ => UpdateConnectionLines());
|
|
|
|
ViewModel.WhenAnyValue(x => x.Nodes)
|
|
.Subscribe(nodes =>
|
|
{
|
|
UpdateConnectionLines();
|
|
UpdateNodesOnCanvas(nodes);
|
|
// 调试:检查节点数量
|
|
System.Diagnostics.Debug.WriteLine($"节点数量: {nodes?.Count ?? 0}");
|
|
if (nodes != null)
|
|
{
|
|
foreach (var node in nodes)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine($"节点: {node.Title}, 位置: ({node.X}, {node.Y})");
|
|
}
|
|
}
|
|
});
|
|
|
|
ViewModel.WhenAnyValue(x => x.IsConnecting, x => x.ConnectingSourcePoint)
|
|
.Subscribe(_ => UpdateConnectionLines());
|
|
}
|
|
};
|
|
}
|
|
|
|
private void SetupTemplateDragAndDrop()
|
|
{
|
|
// 延迟设置,等待模板加载完成
|
|
if (ViewModel != null)
|
|
{
|
|
ViewModel.WhenAnyValue(x => x.NodeTemplates)
|
|
.Subscribe(_ =>
|
|
{
|
|
// 使用延迟来确保UI已渲染
|
|
System.Threading.Tasks.Task.Delay(100).ContinueWith(_ =>
|
|
{
|
|
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
|
{
|
|
SetupTemplateItems();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
private void SetupTemplateItems()
|
|
{
|
|
// 查找所有模板项
|
|
var templateItems = this.GetVisualDescendants()
|
|
.OfType<Border>()
|
|
.Where(b => b.Name == "TemplateItem")
|
|
.ToList();
|
|
|
|
foreach (var item in templateItems)
|
|
{
|
|
SetupTemplateItemDrag(item);
|
|
}
|
|
}
|
|
|
|
private void SetupTemplateItemDrag(Border item)
|
|
{
|
|
if (item.DataContext is NodeTemplate template)
|
|
{
|
|
item.PointerPressed += async (sender, args) =>
|
|
{
|
|
if (args.GetCurrentPoint(item).Properties.IsLeftButtonPressed)
|
|
{
|
|
// 设置允许拖拽的光标
|
|
item.Cursor = new Cursor(StandardCursorType.DragMove);
|
|
|
|
var dataTransfer = new DataObject();
|
|
dataTransfer.Set("NodeTemplate", template);
|
|
|
|
// 执行拖拽操作
|
|
var result = await DragDrop.DoDragDrop(args, dataTransfer, DragDropEffects.Copy);
|
|
|
|
// 恢复光标
|
|
item.Cursor = Cursor.Default;
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
private void OnCanvasDragOver(object? sender, DragEventArgs e)
|
|
{
|
|
if (e.Data.GetDataFormats().Contains("NodeTemplate"))
|
|
{
|
|
e.DragEffects = DragDropEffects.Copy;
|
|
}
|
|
}
|
|
|
|
private void OnCanvasDrop(object? sender, DragEventArgs e)
|
|
{
|
|
if (_canvasContainer == null || ViewModel == null) return;
|
|
|
|
if (e.Data.GetDataFormats().Contains("NodeTemplate") &&
|
|
e.Data.Get("NodeTemplate") is NodeTemplate template)
|
|
{
|
|
// 获取相对于 Canvas 的位置
|
|
var point = e.GetPosition(_canvasContainer);
|
|
ViewModel.AddNodeFromTemplateCommand.Execute((template, point.X, point.Y)).Subscribe();
|
|
e.Handled = true;
|
|
}
|
|
}
|
|
|
|
private void UpdateConnectionLines()
|
|
{
|
|
if (_canvasContainer == null || ViewModel == null) return;
|
|
|
|
// 移除临时连接线
|
|
if (_tempConnectionLine != null && _canvasContainer.Children.Contains(_tempConnectionLine))
|
|
{
|
|
_canvasContainer.Children.Remove(_tempConnectionLine);
|
|
_tempConnectionLine = null;
|
|
}
|
|
|
|
// 更新所有连接线
|
|
var connections = ViewModel.Connections.ToList();
|
|
foreach (var connection in connections)
|
|
{
|
|
UpdateConnectionLine(connection);
|
|
}
|
|
|
|
// 如果正在连接,显示临时连接线
|
|
if (ViewModel.IsConnecting && ViewModel.ConnectingSourcePoint != null)
|
|
{
|
|
ShowTempConnectionLine(ViewModel.ConnectingSourcePoint);
|
|
}
|
|
}
|
|
|
|
private void UpdateConnectionLine(Connection connection)
|
|
{
|
|
if (_canvasContainer == null || connection.SourcePoint == null || connection.TargetPoint == null)
|
|
return;
|
|
|
|
var sourcePoint = GetConnectionPointPosition(connection.SourcePoint);
|
|
var targetPoint = GetConnectionPointPosition(connection.TargetPoint);
|
|
|
|
// 查找或创建连接线Path
|
|
var path = _canvasContainer.Children.OfType<Path>()
|
|
.FirstOrDefault(p => p.Tag == connection);
|
|
|
|
if (path == null)
|
|
{
|
|
path = new Path
|
|
{
|
|
Stroke = new SolidColorBrush(Color.FromRgb(52, 152, 219)),
|
|
StrokeThickness = 2,
|
|
Tag = connection
|
|
};
|
|
_canvasContainer.Children.Add(path);
|
|
}
|
|
|
|
// 创建贝塞尔曲线
|
|
var geometry = new PathGeometry();
|
|
var figure = new PathFigure
|
|
{
|
|
StartPoint = sourcePoint
|
|
};
|
|
|
|
var controlPoint1 = new Point(sourcePoint.X + 50, sourcePoint.Y);
|
|
var controlPoint2 = new Point(targetPoint.X - 50, targetPoint.Y);
|
|
var bezierSegment = new BezierSegment
|
|
{
|
|
Point1 = controlPoint1,
|
|
Point2 = controlPoint2,
|
|
Point3 = targetPoint
|
|
};
|
|
|
|
figure.Segments.Add(bezierSegment);
|
|
geometry.Figures.Add(figure);
|
|
path.Data = geometry;
|
|
}
|
|
|
|
private void ShowTempConnectionLine(ConnectionPoint sourcePoint)
|
|
{
|
|
if (_canvasContainer == null) return;
|
|
|
|
var startPoint = GetConnectionPointPosition(sourcePoint);
|
|
var endPoint = new Point(startPoint.X + 100, startPoint.Y);
|
|
|
|
if (_tempConnectionLine == null)
|
|
{
|
|
_tempConnectionLine = new Path
|
|
{
|
|
Stroke = new SolidColorBrush(Color.FromRgb(52, 152, 219)),
|
|
StrokeThickness = 2,
|
|
StrokeDashArray = new AvaloniaList<double> { 5, 5 }
|
|
};
|
|
_canvasContainer.Children.Add(_tempConnectionLine);
|
|
}
|
|
|
|
var geometry = new PathGeometry();
|
|
var figure = new PathFigure { StartPoint = startPoint };
|
|
var lineSegment = new LineSegment { Point = endPoint };
|
|
figure.Segments.Add(lineSegment);
|
|
geometry.Figures.Add(figure);
|
|
_tempConnectionLine.Data = geometry;
|
|
}
|
|
|
|
private Point GetConnectionPointPosition(ConnectionPoint point)
|
|
{
|
|
if (point.Node == null || _canvasContainer == null)
|
|
return new Point(0, 0);
|
|
|
|
var nodeX = point.Node.X;
|
|
var nodeY = point.Node.Y;
|
|
var nodeHeight = point.Node.Height;
|
|
|
|
// 计算连接点在Canvas上的绝对位置
|
|
if (point.Type == ConnectionPointType.Input)
|
|
{
|
|
// 输入点在左侧,垂直居中
|
|
var inputIndex = point.Node.ConnectionPoints
|
|
.Where(p => p.Type == ConnectionPointType.Input)
|
|
.OrderBy(p => p.Index)
|
|
.ToList()
|
|
.IndexOf(point);
|
|
|
|
var spacing = nodeHeight / (point.Node.ConnectionPoints.Count(p => p.Type == ConnectionPointType.Input) + 1);
|
|
return new Point(nodeX, nodeY + spacing * (inputIndex + 1));
|
|
}
|
|
else
|
|
{
|
|
// 输出点在右侧,垂直排列
|
|
var outputIndex = point.Node.ConnectionPoints
|
|
.Where(p => p.Type == ConnectionPointType.Output)
|
|
.OrderBy(p => p.Index)
|
|
.ToList()
|
|
.IndexOf(point);
|
|
|
|
var spacing = nodeHeight / (point.Node.ConnectionPoints.Count(p => p.Type == ConnectionPointType.Output) + 1);
|
|
return new Point(nodeX + point.Node.Width, nodeY + spacing * (outputIndex + 1));
|
|
}
|
|
}
|
|
|
|
private void OnCanvasPointerPressed(object? sender, PointerPressedEventArgs e)
|
|
{
|
|
if (_canvasContainer == null || ViewModel == null) return;
|
|
|
|
var point = e.GetPosition(_canvasContainer);
|
|
var source = e.Source as Control;
|
|
|
|
// 检查是否点击了连接点
|
|
var connectionPoint = FindConnectionPoint(source);
|
|
if (connectionPoint != null)
|
|
{
|
|
HandleConnectionPointClick(connectionPoint);
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
// 检查是否点击了节点(但不是连接点)
|
|
var node = FindNode(source);
|
|
if (node != null && connectionPoint == null)
|
|
{
|
|
ViewModel.SelectedNode = node;
|
|
_draggedNode = node;
|
|
_dragStartPoint = point;
|
|
e.Handled = true;
|
|
return;
|
|
}
|
|
|
|
// 点击空白区域,取消选中和连接
|
|
ViewModel.SelectedNode = null;
|
|
ViewModel.CancelConnection();
|
|
|
|
// 在点击位置添加新节点(仅在左键点击时)
|
|
if (e.GetCurrentPoint(_canvasContainer).Properties.IsLeftButtonPressed)
|
|
{
|
|
// 调试:输出点击位置
|
|
System.Diagnostics.Debug.WriteLine($"点击位置: ({point.X}, {point.Y})");
|
|
System.Diagnostics.Debug.WriteLine($"Canvas大小: {_canvasContainer.Bounds.Width} x {_canvasContainer.Bounds.Height}");
|
|
|
|
ViewModel.AddNodeCommand.Execute((point.X, point.Y)).Subscribe();
|
|
}
|
|
}
|
|
|
|
private void OnCanvasPointerMoved(object? sender, PointerEventArgs e)
|
|
{
|
|
if (_canvasContainer == null || ViewModel == null) return;
|
|
|
|
var point = e.GetPosition(_canvasContainer);
|
|
|
|
// 拖拽节点
|
|
if (_draggedNode != null && _dragStartPoint.HasValue)
|
|
{
|
|
var deltaX = point.X - _dragStartPoint.Value.X;
|
|
var deltaY = point.Y - _dragStartPoint.Value.Y;
|
|
ViewModel.UpdateNodePosition(_draggedNode,
|
|
_draggedNode.X + deltaX,
|
|
_draggedNode.Y + deltaY);
|
|
_dragStartPoint = point;
|
|
}
|
|
|
|
// 如果正在连接,更新临时连接线
|
|
if (ViewModel.IsConnecting && ViewModel.ConnectingSourcePoint != null)
|
|
{
|
|
var sourcePos = GetConnectionPointPosition(ViewModel.ConnectingSourcePoint);
|
|
if (_tempConnectionLine != null)
|
|
{
|
|
var geometry = new PathGeometry();
|
|
var figure = new PathFigure { StartPoint = sourcePos };
|
|
var lineSegment = new LineSegment { Point = point };
|
|
figure.Segments.Add(lineSegment);
|
|
geometry.Figures.Add(figure);
|
|
_tempConnectionLine.Data = geometry;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnCanvasPointerReleased(object? sender, PointerReleasedEventArgs e)
|
|
{
|
|
_draggedNode = null;
|
|
_dragStartPoint = null;
|
|
}
|
|
|
|
private void HandleConnectionPointClick(ConnectionPoint connectionPoint)
|
|
{
|
|
if (ViewModel == null) return;
|
|
|
|
if (ViewModel.IsConnecting)
|
|
{
|
|
// 完成连接
|
|
if (connectionPoint.Type == ConnectionPointType.Input)
|
|
{
|
|
ViewModel.CompleteConnectionCommand.Execute(connectionPoint).Subscribe();
|
|
}
|
|
else
|
|
{
|
|
// 取消当前连接,开始新连接
|
|
ViewModel.CancelConnection();
|
|
ViewModel.StartConnectionCommand.Execute(connectionPoint).Subscribe();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 开始连接
|
|
if (connectionPoint.Type == ConnectionPointType.Output)
|
|
{
|
|
ViewModel.StartConnectionCommand.Execute(connectionPoint).Subscribe();
|
|
}
|
|
}
|
|
}
|
|
|
|
private Node? FindNode(Control? control)
|
|
{
|
|
if (control == null || ViewModel == null) return null;
|
|
|
|
// 向上查找直到找到包含Node的容器
|
|
var current = control;
|
|
while (current != null)
|
|
{
|
|
if (current.DataContext is Node node && ViewModel.Nodes.Contains(node))
|
|
{
|
|
return node;
|
|
}
|
|
current = current.Parent as Control;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private ConnectionPoint? FindConnectionPoint(Control? control)
|
|
{
|
|
if (control == null) return null;
|
|
|
|
var current = control;
|
|
while (current != null)
|
|
{
|
|
if (current.DataContext is ConnectionPoint point)
|
|
{
|
|
return point;
|
|
}
|
|
current = current.Parent as Control;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private void DrawGridBackground()
|
|
{
|
|
if (_gridBackgroundLayer == null || _canvasContainer == null) return;
|
|
|
|
// 清空之前的网格线
|
|
_gridBackgroundLayer.Children.Clear();
|
|
|
|
var width = _canvasContainer.Bounds.Width > 0 ? _canvasContainer.Bounds.Width : 2000;
|
|
var height = _canvasContainer.Bounds.Height > 0 ? _canvasContainer.Bounds.Height : 2000;
|
|
|
|
// 创建网格线画笔
|
|
var gridBrush = new SolidColorBrush(Color.FromRgb(224, 224, 224)); // #E0E0E0
|
|
|
|
// 绘制垂直线
|
|
for (double x = 0; x <= width; x += GridSize)
|
|
{
|
|
var line = new Line
|
|
{
|
|
StartPoint = new Point(x, 0),
|
|
EndPoint = new Point(x, height),
|
|
Stroke = gridBrush,
|
|
StrokeThickness = 0.5
|
|
};
|
|
_gridBackgroundLayer.Children.Add(line);
|
|
}
|
|
|
|
// 绘制水平线
|
|
for (double y = 0; y <= height; y += GridSize)
|
|
{
|
|
var line = new Line
|
|
{
|
|
StartPoint = new Point(0, y),
|
|
EndPoint = new Point(width, y),
|
|
Stroke = gridBrush,
|
|
StrokeThickness = 0.5
|
|
};
|
|
_gridBackgroundLayer.Children.Add(line);
|
|
}
|
|
}
|
|
|
|
private void UpdateNodesOnCanvas(System.Collections.ObjectModel.ObservableCollection<Node>? nodes)
|
|
{
|
|
if (_canvasContainer == null || nodes == null) return;
|
|
|
|
// 查找 ItemsControl
|
|
var itemsControl = this.FindControl<ItemsControl>("NodesItemsControl");
|
|
if (itemsControl == null)
|
|
{
|
|
itemsControl = _canvasContainer.GetVisualDescendants()
|
|
.OfType<ItemsControl>()
|
|
.FirstOrDefault();
|
|
}
|
|
|
|
if (itemsControl != null)
|
|
{
|
|
// 强制更新 ItemsControl 的布局
|
|
itemsControl.InvalidateMeasure();
|
|
itemsControl.InvalidateArrange();
|
|
|
|
// 延迟更新,确保容器已生成
|
|
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
|
{
|
|
try
|
|
{
|
|
foreach (var item in itemsControl.Items)
|
|
{
|
|
if (item is Node node)
|
|
{
|
|
// 在 Avalonia 中,通过遍历视觉树来查找对应的容器
|
|
var container = itemsControl.GetVisualDescendants()
|
|
.OfType<ContentControl>()
|
|
.FirstOrDefault(cc => cc.Content == node);
|
|
|
|
if (container is ContentControl contentControl)
|
|
{
|
|
Canvas.SetLeft(contentControl, node.X);
|
|
Canvas.SetTop(contentControl, node.Y);
|
|
System.Diagnostics.Debug.WriteLine($"设置节点 {node.Title} 位置: ({node.X}, {node.Y})");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine($"更新节点位置时出错: {ex.Message}");
|
|
}
|
|
}, Avalonia.Threading.DispatcherPriority.Normal);
|
|
}
|
|
}
|
|
}
|
|
|
|
|