配方编辑部分的交互逻辑

This commit is contained in:
zhengxuan.zhang
2026-04-23 16:12:55 +08:00
parent e0326c2d80
commit 3f3820073f
6 changed files with 482 additions and 131 deletions
+2 -2
View File
@@ -3,7 +3,7 @@ using System.Collections.Generic;
namespace XplorePlane.Models namespace XplorePlane.Models
{ {
public class PipelineModel public class PipelineModel //流程图
{ {
public Guid Id { get; set; } = Guid.NewGuid(); public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
@@ -13,7 +13,7 @@ namespace XplorePlane.Models
public List<PipelineNodeModel> Nodes { get; set; } = new(); public List<PipelineNodeModel> Nodes { get; set; } = new();
} }
public class PipelineNodeModel public class PipelineNodeModel //节点
{ {
public Guid Id { get; set; } = Guid.NewGuid(); public Guid Id { get; set; } = Guid.NewGuid();
public string OperatorKey { get; set; } = string.Empty; public string OperatorKey { get; set; } = string.Empty;
@@ -1,4 +1,4 @@
using Microsoft.Win32; using Microsoft.Win32;
using Prism.Commands; using Prism.Commands;
using Prism.Mvvm; using Prism.Mvvm;
using System; using System;
@@ -44,6 +44,8 @@ namespace XplorePlane.ViewModels.Cnc
AddOperatorCommand = new DelegateCommand<string>(AddOperator, _ => HasActiveModule); AddOperatorCommand = new DelegateCommand<string>(AddOperator, _ => HasActiveModule);
RemoveOperatorCommand = new DelegateCommand<PipelineNodeViewModel>(RemoveOperator); RemoveOperatorCommand = new DelegateCommand<PipelineNodeViewModel>(RemoveOperator);
ReorderOperatorCommand = new DelegateCommand<PipelineReorderArgs>(ReorderOperator);
ToggleOperatorEnabledCommand = new DelegateCommand<PipelineNodeViewModel>(ToggleOperatorEnabled);
MoveNodeUpCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeUp); MoveNodeUpCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeUp);
MoveNodeDownCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeDown); MoveNodeDownCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeDown);
NewPipelineCommand = new DelegateCommand(NewPipeline); NewPipelineCommand = new DelegateCommand(NewPipeline);
@@ -79,6 +81,10 @@ namespace XplorePlane.ViewModels.Cnc
public ICommand RemoveOperatorCommand { get; } public ICommand RemoveOperatorCommand { get; }
public ICommand ReorderOperatorCommand { get; }
public ICommand ToggleOperatorEnabledCommand { get; }
public ICommand MoveNodeUpCommand { get; } public ICommand MoveNodeUpCommand { get; }
public ICommand MoveNodeDownCommand { get; } public ICommand MoveNodeDownCommand { get; }
@@ -152,13 +158,10 @@ namespace XplorePlane.ViewModels.Cnc
if (!HasActiveModule || node == null || !PipelineNodes.Contains(node)) if (!HasActiveModule || node == null || !PipelineNodes.Contains(node))
return; return;
var removedIndex = PipelineNodes.IndexOf(node);
PipelineNodes.Remove(node); PipelineNodes.Remove(node);
RenumberNodes(); RenumberNodes();
SelectNeighborAfterRemoval(removedIndex);
if (SelectedNode == node)
{
SelectedNode = PipelineNodes.LastOrDefault();
}
PersistActiveModule($"已移除算子:{node.DisplayName}"); PersistActiveModule($"已移除算子:{node.DisplayName}");
} }
@@ -177,6 +180,26 @@ namespace XplorePlane.ViewModels.Cnc
PersistActiveModule($"已上移算子:{node.DisplayName}"); PersistActiveModule($"已上移算子:{node.DisplayName}");
} }
private void ReorderOperator(PipelineReorderArgs args)
{
if (!HasActiveModule || args == null)
return;
var oldIndex = args.OldIndex;
var newIndex = args.NewIndex;
if (oldIndex < 0 || oldIndex >= PipelineNodes.Count)
return;
if (newIndex < 0 || newIndex >= PipelineNodes.Count || oldIndex == newIndex)
return;
var node = PipelineNodes[oldIndex];
PipelineNodes.Move(oldIndex, newIndex);
RenumberNodes();
SelectedNode = node;
PersistActiveModule($"已调整算子顺序:{node.DisplayName}");
}
private void MoveNodeDown(PipelineNodeViewModel node) private void MoveNodeDown(PipelineNodeViewModel node)
{ {
if (!HasActiveModule || node == null) if (!HasActiveModule || node == null)
@@ -191,6 +214,18 @@ namespace XplorePlane.ViewModels.Cnc
PersistActiveModule($"已下移算子:{node.DisplayName}"); PersistActiveModule($"已下移算子:{node.DisplayName}");
} }
private void ToggleOperatorEnabled(PipelineNodeViewModel node)
{
if (!HasActiveModule || node == null || !PipelineNodes.Contains(node))
return;
node.IsEnabled = !node.IsEnabled;
SelectedNode = node;
PersistActiveModule(node.IsEnabled
? $"已启用算子:{node.DisplayName}"
: $"已停用算子:{node.DisplayName}");
}
private void NewPipeline() private void NewPipeline()
{ {
if (!HasActiveModule) if (!HasActiveModule)
@@ -359,6 +394,20 @@ namespace XplorePlane.ViewModels.Cnc
} }
} }
private void SelectNeighborAfterRemoval(int removedIndex)
{
if (PipelineNodes.Count == 0)
{
SelectedNode = null;
return;
}
var nextIndex = removedIndex < PipelineNodes.Count
? removedIndex
: PipelineNodes.Count - 1;
SelectedNode = PipelineNodes[nextIndex];
}
private void RaiseModuleVisibilityChanged() private void RaiseModuleVisibilityChanged()
{ {
RaisePropertyChanged(nameof(HasActiveModule)); RaisePropertyChanged(nameof(HasActiveModule));
@@ -15,6 +15,10 @@ namespace XplorePlane.ViewModels
ICommand RemoveOperatorCommand { get; } ICommand RemoveOperatorCommand { get; }
ICommand ReorderOperatorCommand { get; }
ICommand ToggleOperatorEnabledCommand { get; }
ICommand MoveNodeUpCommand { get; } ICommand MoveNodeUpCommand { get; }
ICommand MoveNodeDownCommand { get; } ICommand MoveNodeDownCommand { get; }
@@ -1,4 +1,4 @@
using Microsoft.Win32; using Microsoft.Win32;
using Prism.Events; using Prism.Events;
using Prism.Commands; using Prism.Commands;
using Prism.Mvvm; using Prism.Mvvm;
@@ -59,6 +59,7 @@ namespace XplorePlane.ViewModels
AddOperatorCommand = new DelegateCommand<string>(AddOperator, CanAddOperator); AddOperatorCommand = new DelegateCommand<string>(AddOperator, CanAddOperator);
RemoveOperatorCommand = new DelegateCommand<PipelineNodeViewModel>(RemoveOperator); RemoveOperatorCommand = new DelegateCommand<PipelineNodeViewModel>(RemoveOperator);
ReorderOperatorCommand = new DelegateCommand<PipelineReorderArgs>(ReorderOperator); ReorderOperatorCommand = new DelegateCommand<PipelineReorderArgs>(ReorderOperator);
ToggleOperatorEnabledCommand = new DelegateCommand<PipelineNodeViewModel>(ToggleOperatorEnabled);
ExecutePipelineCommand = new DelegateCommand(async () => await ExecutePipelineAsync(), () => !IsExecuting && SourceImage != null); ExecutePipelineCommand = new DelegateCommand(async () => await ExecutePipelineAsync(), () => !IsExecuting && SourceImage != null);
CancelExecutionCommand = new DelegateCommand(CancelExecution, () => IsExecuting); CancelExecutionCommand = new DelegateCommand(CancelExecution, () => IsExecuting);
NewPipelineCommand = new DelegateCommand(NewPipeline); NewPipelineCommand = new DelegateCommand(NewPipeline);
@@ -152,6 +153,7 @@ namespace XplorePlane.ViewModels
public DelegateCommand<string> AddOperatorCommand { get; } public DelegateCommand<string> AddOperatorCommand { get; }
public DelegateCommand<PipelineNodeViewModel> RemoveOperatorCommand { get; } public DelegateCommand<PipelineNodeViewModel> RemoveOperatorCommand { get; }
public DelegateCommand<PipelineReorderArgs> ReorderOperatorCommand { get; } public DelegateCommand<PipelineReorderArgs> ReorderOperatorCommand { get; }
public DelegateCommand<PipelineNodeViewModel> ToggleOperatorEnabledCommand { get; }
public DelegateCommand ExecutePipelineCommand { get; } public DelegateCommand ExecutePipelineCommand { get; }
public DelegateCommand CancelExecutionCommand { get; } public DelegateCommand CancelExecutionCommand { get; }
public DelegateCommand NewPipelineCommand { get; } public DelegateCommand NewPipelineCommand { get; }
@@ -168,6 +170,8 @@ namespace XplorePlane.ViewModels
ICommand IPipelineEditorHostViewModel.AddOperatorCommand => AddOperatorCommand; ICommand IPipelineEditorHostViewModel.AddOperatorCommand => AddOperatorCommand;
ICommand IPipelineEditorHostViewModel.RemoveOperatorCommand => RemoveOperatorCommand; ICommand IPipelineEditorHostViewModel.RemoveOperatorCommand => RemoveOperatorCommand;
ICommand IPipelineEditorHostViewModel.ReorderOperatorCommand => ReorderOperatorCommand;
ICommand IPipelineEditorHostViewModel.ToggleOperatorEnabledCommand => ToggleOperatorEnabledCommand;
ICommand IPipelineEditorHostViewModel.MoveNodeUpCommand => MoveNodeUpCommand; ICommand IPipelineEditorHostViewModel.MoveNodeUpCommand => MoveNodeUpCommand;
ICommand IPipelineEditorHostViewModel.MoveNodeDownCommand => MoveNodeDownCommand; ICommand IPipelineEditorHostViewModel.MoveNodeDownCommand => MoveNodeDownCommand;
ICommand IPipelineEditorHostViewModel.NewPipelineCommand => NewPipelineCommand; ICommand IPipelineEditorHostViewModel.NewPipelineCommand => NewPipelineCommand;
@@ -217,6 +221,7 @@ namespace XplorePlane.ViewModels
}; };
LoadNodeParameters(node); LoadNodeParameters(node);
PipelineNodes.Add(node); PipelineNodes.Add(node);
SelectedNode = node;
_logger.Info("节点已添加到 PipelineNodes{Key} ({DisplayName}),当前节点数={Count}", _logger.Info("节点已添加到 PipelineNodes{Key} ({DisplayName}),当前节点数={Count}",
operatorKey, displayName, PipelineNodes.Count); operatorKey, displayName, PipelineNodes.Count);
StatusMessage = $"已添加算子:{displayName}"; StatusMessage = $"已添加算子:{displayName}";
@@ -227,11 +232,10 @@ namespace XplorePlane.ViewModels
{ {
if (node == null || !PipelineNodes.Contains(node)) return; if (node == null || !PipelineNodes.Contains(node)) return;
var removedIndex = PipelineNodes.IndexOf(node);
PipelineNodes.Remove(node); PipelineNodes.Remove(node);
RenumberNodes(); RenumberNodes();
SelectNeighborAfterRemoval(removedIndex);
if (SelectedNode == node)
SelectedNode = null;
StatusMessage = $"已移除算子:{node.DisplayName}"; StatusMessage = $"已移除算子:{node.DisplayName}";
TriggerDebouncedExecution(); TriggerDebouncedExecution();
@@ -271,6 +275,20 @@ namespace XplorePlane.ViewModels
PipelineNodes.RemoveAt(oldIndex); PipelineNodes.RemoveAt(oldIndex);
PipelineNodes.Insert(newIndex, node); PipelineNodes.Insert(newIndex, node);
RenumberNodes(); RenumberNodes();
SelectedNode = node;
StatusMessage = $"已调整算子顺序:{node.DisplayName}";
TriggerDebouncedExecution();
}
private void ToggleOperatorEnabled(PipelineNodeViewModel node)
{
if (node == null || !PipelineNodes.Contains(node)) return;
node.IsEnabled = !node.IsEnabled;
SelectedNode = node;
StatusMessage = node.IsEnabled
? $"已启用算子:{node.DisplayName}"
: $"已停用算子:{node.DisplayName}";
TriggerDebouncedExecution(); TriggerDebouncedExecution();
} }
@@ -280,6 +298,20 @@ namespace XplorePlane.ViewModels
PipelineNodes[i].Order = i; PipelineNodes[i].Order = i;
} }
private void SelectNeighborAfterRemoval(int removedIndex)
{
if (PipelineNodes.Count == 0)
{
SelectedNode = null;
return;
}
var nextIndex = removedIndex < PipelineNodes.Count
? removedIndex
: PipelineNodes.Count - 1;
SelectedNode = PipelineNodes[nextIndex];
}
private void LoadNodeParameters(PipelineNodeViewModel node) private void LoadNodeParameters(PipelineNodeViewModel node)
{ {
var paramDefs = _imageProcessingService.GetProcessorParameters(node.OperatorKey); var paramDefs = _imageProcessingService.GetProcessorParameters(node.OperatorKey);
@@ -4,7 +4,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
d:DesignHeight="700" d:DesignHeight="700"
d:DesignWidth="350" d:DesignWidth="350"
mc:Ignorable="d"> mc:Ignorable="d">
@@ -13,13 +12,27 @@
<SolidColorBrush x:Key="PanelBg" Color="White" /> <SolidColorBrush x:Key="PanelBg" Color="White" />
<SolidColorBrush x:Key="PanelBorder" Color="#CDCBCB" /> <SolidColorBrush x:Key="PanelBorder" Color="#CDCBCB" />
<SolidColorBrush x:Key="SeparatorBrush" Color="#E0E0E0" /> <SolidColorBrush x:Key="SeparatorBrush" Color="#E0E0E0" />
<SolidColorBrush x:Key="AccentBlue" Color="#E3F0FF" /> <SolidColorBrush x:Key="AccentBlue" Color="#D9ECFF" />
<SolidColorBrush x:Key="DisabledNodeBg" Color="#F3F3F3" />
<SolidColorBrush x:Key="DisabledNodeLine" Color="#B9B9B9" />
<SolidColorBrush x:Key="DisabledNodeText" Color="#8A8A8A" />
<FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily> <FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily>
<Style x:Key="PipelineNodeItemStyle" TargetType="ListBoxItem"> <Style x:Key="PipelineNodeItemStyle" TargetType="ListBoxItem">
<Setter Property="Padding" Value="0" /> <Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0" /> <Setter Property="Margin" Value="0" />
<Setter Property="Focusable" Value="False" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" /> <Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="{StaticResource AccentBlue}" />
<Setter Property="BorderBrush" Value="#5B9BD5" />
<Setter Property="BorderThickness" Value="1" />
</Trigger>
</Style.Triggers>
</Style> </Style>
<Style x:Key="ToolbarBtn" TargetType="Button"> <Style x:Key="ToolbarBtn" TargetType="Button">
@@ -43,7 +56,7 @@
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="2*" MinHeight="180" /> <RowDefinition Height="4*" MinHeight="180" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="2*" MinHeight="80" /> <RowDefinition Height="2*" MinHeight="80" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
@@ -66,19 +79,19 @@
Command="{Binding SavePipelineCommand}" Command="{Binding SavePipelineCommand}"
Content="保存" Content="保存"
Style="{StaticResource ToolbarBtn}" Style="{StaticResource ToolbarBtn}"
ToolTip="保存当前检测模块流水线" /> ToolTip="保存当前流水线" />
<Button <Button
Width="60" Width="60"
Command="{Binding SaveAsPipelineCommand}" Command="{Binding SaveAsPipelineCommand}"
Content="另存为" Content="另存为"
Style="{StaticResource ToolbarBtn}" Style="{StaticResource ToolbarBtn}"
ToolTip="导出当前检测模块流水线" /> ToolTip="另存当前流水线" />
<Button <Button
Width="52" Width="52"
Command="{Binding LoadPipelineCommand}" Command="{Binding LoadPipelineCommand}"
Content="加载" Content="加载"
Style="{StaticResource ToolbarBtn}" Style="{StaticResource ToolbarBtn}"
ToolTip="流水线模板加载到当前检测模块" /> ToolTip="加载流水线" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</Border> </Border>
@@ -90,10 +103,19 @@
BorderThickness="0" BorderThickness="0"
ItemContainerStyle="{StaticResource PipelineNodeItemStyle}" ItemContainerStyle="{StaticResource PipelineNodeItemStyle}"
ItemsSource="{Binding PipelineNodes}" ItemsSource="{Binding PipelineNodes}"
KeyboardNavigation.TabNavigation="Continue"
ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.HorizontalScrollBarVisibility="Disabled"
SelectedItem="{Binding SelectedNode, Mode=TwoWay}"> SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<Border
x:Name="NodeContainer"
Margin="2"
Padding="2"
Background="Transparent"
BorderBrush="Transparent"
BorderThickness="1"
CornerRadius="3">
<Grid x:Name="NodeRoot" MinHeight="48"> <Grid x:Name="NodeRoot" MinHeight="48">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="44" /> <ColumnDefinition Width="44" />
@@ -112,6 +134,7 @@
Y1="0" Y1="0"
Y2="10" /> Y2="10" />
<Line <Line
x:Name="BottomLine"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Bottom" VerticalAlignment="Bottom"
Stroke="#5B9BD5" Stroke="#5B9BD5"
@@ -122,6 +145,7 @@
Y2="14" /> Y2="14" />
<Border <Border
x:Name="IconBorder"
Grid.Column="0" Grid.Column="0"
Width="28" Width="28"
Height="28" Height="28"
@@ -138,13 +162,20 @@
Text="{Binding IconPath}" /> Text="{Binding IconPath}" />
</Border> </Border>
<StackPanel Grid.Column="1" Margin="6,0,0,0" VerticalAlignment="Center">
<TextBlock <TextBlock
Grid.Column="1" x:Name="NodeTitle"
Margin="6,0,0,0"
VerticalAlignment="Center"
FontFamily="Microsoft YaHei UI" FontFamily="Microsoft YaHei UI"
FontSize="12" FontSize="12"
Text="{Binding DisplayName}" /> Text="{Binding DisplayName}" />
<TextBlock
x:Name="NodeState"
Margin="0,2,0,0"
FontFamily="Microsoft YaHei UI"
FontSize="10"
Foreground="#6E6E6E"
Text="已启用" />
</StackPanel>
<StackPanel <StackPanel
x:Name="NodeActions" x:Name="NodeActions"
@@ -194,10 +225,35 @@
ToolTip="删除" /> ToolTip="删除" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</Border>
<DataTemplate.Triggers> <DataTemplate.Triggers>
<DataTrigger Binding="{Binding Order}" Value="0"> <DataTrigger Binding="{Binding Order}" Value="0">
<Setter TargetName="TopLine" Property="Visibility" Value="Collapsed" /> <Setter TargetName="TopLine" Property="Visibility" Value="Collapsed" />
</DataTrigger> </DataTrigger>
<DataTrigger Binding="{Binding IsEnabled}" Value="False">
<Setter TargetName="NodeContainer" Property="Background" Value="{StaticResource DisabledNodeBg}" />
<Setter TargetName="NodeContainer" Property="Opacity" Value="0.78" />
<Setter TargetName="TopLine" Property="Stroke" Value="{StaticResource DisabledNodeLine}" />
<Setter TargetName="BottomLine" Property="Stroke" Value="{StaticResource DisabledNodeLine}" />
<Setter TargetName="IconBorder" Property="BorderBrush" Value="{StaticResource DisabledNodeLine}" />
<Setter TargetName="IconBorder" Property="Background" Value="#ECECEC" />
<Setter TargetName="NodeTitle" Property="Foreground" Value="{StaticResource DisabledNodeText}" />
<Setter TargetName="NodeState" Property="Text" Value="已停用" />
<Setter TargetName="NodeState" Property="Foreground" Value="#9A6767" />
</DataTrigger>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ListBoxItem}, Path=IsSelected}" Value="True">
<Setter TargetName="NodeContainer" Property="Background" Value="#D9ECFF" />
<Setter TargetName="NodeContainer" Property="BorderBrush" Value="#5B9BD5" />
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsEnabled}" Value="False" />
<Condition Binding="{Binding RelativeSource={RelativeSource AncestorType=ListBoxItem}, Path=IsSelected}" Value="True" />
</MultiDataTrigger.Conditions>
<Setter TargetName="NodeContainer" Property="Background" Value="#E6EEF7" />
<Setter TargetName="NodeContainer" Property="BorderBrush" Value="#7E9AB6" />
<Setter TargetName="NodeContainer" Property="Opacity" Value="1" />
</MultiDataTrigger>
<Trigger SourceName="NodeRoot" Property="IsMouseOver" Value="True"> <Trigger SourceName="NodeRoot" Property="IsMouseOver" Value="True">
<Setter TargetName="NodeActions" Property="Visibility" Value="Visible" /> <Setter TargetName="NodeActions" Property="Visibility" Value="Visible" />
</Trigger> </Trigger>
@@ -2,15 +2,26 @@ using Prism.Ioc;
using System; using System;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XplorePlane.Models;
using XplorePlane.ViewModels; using XplorePlane.ViewModels;
namespace XplorePlane.Views namespace XplorePlane.Views
{ {
public partial class PipelineEditorView : UserControl public partial class PipelineEditorView : UserControl
{ {
private const string PipelineNodeDragFormat = "PipelineNodeDrag";
private readonly ILoggerService _logger; private readonly ILoggerService _logger;
private Point _dragStartPoint;
private bool _isInternalDragging;
private bool _suppressClickToggle;
private PipelineNodeViewModel _draggedNode;
public PipelineEditorView() public PipelineEditorView()
{ {
InitializeComponent(); InitializeComponent();
@@ -35,50 +46,249 @@ namespace XplorePlane.Views
} }
} }
_logger?.Info("PipelineEditorView DataContext 类型={Type}",
DataContext?.GetType().Name);
PipelineListBox.AllowDrop = true; PipelineListBox.AllowDrop = true;
PipelineListBox.Drop += OnOperatorDropped; PipelineListBox.Focusable = true;
PipelineListBox.Drop -= OnListBoxDrop;
PipelineListBox.Drop += OnListBoxDrop;
PipelineListBox.DragOver -= OnDragOver;
PipelineListBox.DragOver += OnDragOver; PipelineListBox.DragOver += OnDragOver;
_logger?.Debug("PipelineEditorView 原生 Drop 目标已注册"); PipelineListBox.PreviewMouseLeftButtonDown -= OnPreviewMouseLeftButtonDown;
PipelineListBox.PreviewMouseLeftButtonDown += OnPreviewMouseLeftButtonDown;
PipelineListBox.PreviewMouseMove -= OnPreviewMouseMove;
PipelineListBox.PreviewMouseMove += OnPreviewMouseMove;
PipelineListBox.PreviewMouseLeftButtonUp -= OnPreviewMouseLeftButtonUp;
PipelineListBox.PreviewMouseLeftButtonUp += OnPreviewMouseLeftButtonUp;
PipelineListBox.PreviewKeyDown -= OnPreviewKeyDown;
PipelineListBox.PreviewKeyDown += OnPreviewKeyDown;
}
private void OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_dragStartPoint = e.GetPosition(PipelineListBox);
_isInternalDragging = false;
_draggedNode = FindNodeFromOriginalSource(e.OriginalSource);
if (_draggedNode != null)
{
PipelineListBox.SelectedItem = _draggedNode;
PipelineListBox.Focus();
}
}
private void OnPreviewMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed || _draggedNode == null || IsInteractiveChild(e.OriginalSource))
return;
var position = e.GetPosition(PipelineListBox);
var delta = position - _dragStartPoint;
if (_isInternalDragging
|| (Math.Abs(delta.X) < SystemParameters.MinimumHorizontalDragDistance
&& Math.Abs(delta.Y) < SystemParameters.MinimumVerticalDragDistance))
{
return;
}
_isInternalDragging = true;
var data = new DataObject(PipelineNodeDragFormat, _draggedNode);
DragDrop.DoDragDrop(PipelineListBox, data, DragDropEffects.Move);
_suppressClickToggle = true;
}
private void OnPreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
var vm = DataContext as IPipelineEditorHostViewModel;
var clickedNode = FindNodeFromOriginalSource(e.OriginalSource);
if (_isInternalDragging)
{
ResetDragState();
return;
}
if (_suppressClickToggle)
{
_suppressClickToggle = false;
ResetDragState();
return;
}
if (vm == null || clickedNode == null || IsInteractiveChild(e.OriginalSource))
{
ResetDragState();
return;
}
PipelineListBox.SelectedItem = clickedNode;
PipelineListBox.Focus();
vm.ToggleOperatorEnabledCommand.Execute(clickedNode);
e.Handled = true;
ResetDragState();
}
private void OnPreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Delete || DataContext is not IPipelineEditorHostViewModel vm || vm.SelectedNode == null)
return;
vm.RemoveOperatorCommand.Execute(vm.SelectedNode);
e.Handled = true;
} }
private void OnDragOver(object sender, DragEventArgs e) private void OnDragOver(object sender, DragEventArgs e)
{ {
e.Effects = e.Data.GetDataPresent(OperatorToolboxView.DragFormat) if (e.Data.GetDataPresent(OperatorToolboxView.DragFormat))
? DragDropEffects.Copy {
: DragDropEffects.None; e.Effects = DragDropEffects.Copy;
}
else if (e.Data.GetDataPresent(PipelineNodeDragFormat))
{
e.Effects = DragDropEffects.Move;
}
else
{
e.Effects = DragDropEffects.None;
}
e.Handled = true; e.Handled = true;
} }
private void OnOperatorDropped(object sender, DragEventArgs e) private void OnListBoxDrop(object sender, DragEventArgs e)
{ {
if (DataContext is not IPipelineEditorHostViewModel vm) if (DataContext is not IPipelineEditorHostViewModel vm)
{ {
_logger?.Warn("Drop 事件触发,但 DataContext 不是流水线宿主 ViewModel"); ResetDragState();
return; return;
} }
if (!e.Data.GetDataPresent(OperatorToolboxView.DragFormat)) if (e.Data.GetDataPresent(OperatorToolboxView.DragFormat))
{ {
_logger?.Warn("Drop 事件触发,但数据中没有 {Format}", OperatorToolboxView.DragFormat); OnOperatorDropped(vm, e);
return; }
else if (e.Data.GetDataPresent(PipelineNodeDragFormat))
{
OnInternalNodeDropped(vm, e);
} }
ResetDragState();
e.Handled = true;
}
private void OnOperatorDropped(IPipelineEditorHostViewModel vm, DragEventArgs e)
{
var operatorKey = e.Data.GetData(OperatorToolboxView.DragFormat) as string; var operatorKey = e.Data.GetData(OperatorToolboxView.DragFormat) as string;
if (string.IsNullOrWhiteSpace(operatorKey)) if (string.IsNullOrWhiteSpace(operatorKey))
{ {
_logger?.Warn("Drop 事件触发,但 OperatorKey 为空"); _logger?.Warn("Drop 触发,但 OperatorKey 为空");
return; return;
} }
_logger?.Info("算子已放入流水线:{OperatorKey},当前节点数(执行前)={Count}",
operatorKey, vm.PipelineNodes.Count);
vm.AddOperatorCommand.Execute(operatorKey); vm.AddOperatorCommand.Execute(operatorKey);
_logger?.Info("AddOperator 执行后节点数={Count}PipelineListBox.Items.Count={ItemsCount}", }
vm.PipelineNodes.Count, PipelineListBox.Items.Count);
e.Handled = true; private void OnInternalNodeDropped(IPipelineEditorHostViewModel vm, DragEventArgs e)
{
if (e.Data.GetData(PipelineNodeDragFormat) is not PipelineNodeViewModel draggedNode
|| !vm.PipelineNodes.Contains(draggedNode))
{
return;
}
var oldIndex = vm.PipelineNodes.IndexOf(draggedNode);
var targetIndex = GetDropTargetIndex(e.GetPosition(PipelineListBox));
if (targetIndex < 0)
{
targetIndex = vm.PipelineNodes.Count - 1;
}
if (targetIndex >= oldIndex && targetIndex < vm.PipelineNodes.Count - 1)
{
var targetItem = GetItemAtPosition(e.GetPosition(PipelineListBox));
if (targetItem != null)
{
var itemBounds = VisualTreeHelper.GetDescendantBounds(targetItem);
var itemTopLeft = targetItem.TranslatePoint(new Point(0, 0), PipelineListBox);
var itemMidY = itemTopLeft.Y + (itemBounds.Height / 2);
if (e.GetPosition(PipelineListBox).Y > itemMidY)
{
targetIndex = Math.Min(targetIndex + 1, vm.PipelineNodes.Count - 1);
}
}
}
targetIndex = Math.Max(0, Math.Min(targetIndex, vm.PipelineNodes.Count - 1));
if (oldIndex == targetIndex)
{
return;
}
vm.ReorderOperatorCommand.Execute(new PipelineReorderArgs
{
OldIndex = oldIndex,
NewIndex = targetIndex
});
}
private int GetDropTargetIndex(Point position)
{
var item = GetItemAtPosition(position);
return item == null
? -1
: PipelineListBox.ItemContainerGenerator.IndexFromContainer(item);
}
private ListBoxItem GetItemAtPosition(Point position)
{
var element = PipelineListBox.InputHitTest(position) as DependencyObject;
while (element != null)
{
if (element is ListBoxItem item)
{
return item;
}
element = VisualTreeHelper.GetParent(element);
}
return null;
}
private PipelineNodeViewModel FindNodeFromOriginalSource(object originalSource)
{
var current = originalSource as DependencyObject;
while (current != null)
{
if (current is FrameworkElement element && element.DataContext is PipelineNodeViewModel node)
{
return node;
}
current = VisualTreeHelper.GetParent(current);
}
return null;
}
private static bool IsInteractiveChild(object originalSource)
{
var current = originalSource as DependencyObject;
while (current != null)
{
if (current is ButtonBase || current is TextBoxBase || current is Selector || current is ScrollBar)
{
return true;
}
current = VisualTreeHelper.GetParent(current);
}
return false;
}
private void ResetDragState()
{
_isInternalDragging = false;
_draggedNode = null;
} }
} }
} }