配方编辑部分的交互逻辑
This commit is contained in:
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
|
||||
namespace XplorePlane.Models
|
||||
{
|
||||
public class PipelineModel
|
||||
public class PipelineModel //流程图
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public string Name { get; set; } = string.Empty;
|
||||
@@ -13,7 +13,7 @@ namespace XplorePlane.Models
|
||||
public List<PipelineNodeModel> Nodes { get; set; } = new();
|
||||
}
|
||||
|
||||
public class PipelineNodeModel
|
||||
public class PipelineNodeModel //节点
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public string OperatorKey { get; set; } = string.Empty;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Win32;
|
||||
using Microsoft.Win32;
|
||||
using Prism.Commands;
|
||||
using Prism.Mvvm;
|
||||
using System;
|
||||
@@ -44,6 +44,8 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
|
||||
AddOperatorCommand = new DelegateCommand<string>(AddOperator, _ => HasActiveModule);
|
||||
RemoveOperatorCommand = new DelegateCommand<PipelineNodeViewModel>(RemoveOperator);
|
||||
ReorderOperatorCommand = new DelegateCommand<PipelineReorderArgs>(ReorderOperator);
|
||||
ToggleOperatorEnabledCommand = new DelegateCommand<PipelineNodeViewModel>(ToggleOperatorEnabled);
|
||||
MoveNodeUpCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeUp);
|
||||
MoveNodeDownCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeDown);
|
||||
NewPipelineCommand = new DelegateCommand(NewPipeline);
|
||||
@@ -79,6 +81,10 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
|
||||
public ICommand RemoveOperatorCommand { get; }
|
||||
|
||||
public ICommand ReorderOperatorCommand { get; }
|
||||
|
||||
public ICommand ToggleOperatorEnabledCommand { get; }
|
||||
|
||||
public ICommand MoveNodeUpCommand { get; }
|
||||
|
||||
public ICommand MoveNodeDownCommand { get; }
|
||||
@@ -152,13 +158,10 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
if (!HasActiveModule || node == null || !PipelineNodes.Contains(node))
|
||||
return;
|
||||
|
||||
var removedIndex = PipelineNodes.IndexOf(node);
|
||||
PipelineNodes.Remove(node);
|
||||
RenumberNodes();
|
||||
|
||||
if (SelectedNode == node)
|
||||
{
|
||||
SelectedNode = PipelineNodes.LastOrDefault();
|
||||
}
|
||||
SelectNeighborAfterRemoval(removedIndex);
|
||||
|
||||
PersistActiveModule($"已移除算子:{node.DisplayName}");
|
||||
}
|
||||
@@ -177,6 +180,26 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
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)
|
||||
{
|
||||
if (!HasActiveModule || node == null)
|
||||
@@ -191,6 +214,18 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
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()
|
||||
{
|
||||
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()
|
||||
{
|
||||
RaisePropertyChanged(nameof(HasActiveModule));
|
||||
|
||||
@@ -15,6 +15,10 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
ICommand RemoveOperatorCommand { get; }
|
||||
|
||||
ICommand ReorderOperatorCommand { get; }
|
||||
|
||||
ICommand ToggleOperatorEnabledCommand { get; }
|
||||
|
||||
ICommand MoveNodeUpCommand { get; }
|
||||
|
||||
ICommand MoveNodeDownCommand { get; }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Win32;
|
||||
using Microsoft.Win32;
|
||||
using Prism.Events;
|
||||
using Prism.Commands;
|
||||
using Prism.Mvvm;
|
||||
@@ -59,6 +59,7 @@ namespace XplorePlane.ViewModels
|
||||
AddOperatorCommand = new DelegateCommand<string>(AddOperator, CanAddOperator);
|
||||
RemoveOperatorCommand = new DelegateCommand<PipelineNodeViewModel>(RemoveOperator);
|
||||
ReorderOperatorCommand = new DelegateCommand<PipelineReorderArgs>(ReorderOperator);
|
||||
ToggleOperatorEnabledCommand = new DelegateCommand<PipelineNodeViewModel>(ToggleOperatorEnabled);
|
||||
ExecutePipelineCommand = new DelegateCommand(async () => await ExecutePipelineAsync(), () => !IsExecuting && SourceImage != null);
|
||||
CancelExecutionCommand = new DelegateCommand(CancelExecution, () => IsExecuting);
|
||||
NewPipelineCommand = new DelegateCommand(NewPipeline);
|
||||
@@ -152,6 +153,7 @@ namespace XplorePlane.ViewModels
|
||||
public DelegateCommand<string> AddOperatorCommand { get; }
|
||||
public DelegateCommand<PipelineNodeViewModel> RemoveOperatorCommand { get; }
|
||||
public DelegateCommand<PipelineReorderArgs> ReorderOperatorCommand { get; }
|
||||
public DelegateCommand<PipelineNodeViewModel> ToggleOperatorEnabledCommand { get; }
|
||||
public DelegateCommand ExecutePipelineCommand { get; }
|
||||
public DelegateCommand CancelExecutionCommand { get; }
|
||||
public DelegateCommand NewPipelineCommand { get; }
|
||||
@@ -168,6 +170,8 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
ICommand IPipelineEditorHostViewModel.AddOperatorCommand => AddOperatorCommand;
|
||||
ICommand IPipelineEditorHostViewModel.RemoveOperatorCommand => RemoveOperatorCommand;
|
||||
ICommand IPipelineEditorHostViewModel.ReorderOperatorCommand => ReorderOperatorCommand;
|
||||
ICommand IPipelineEditorHostViewModel.ToggleOperatorEnabledCommand => ToggleOperatorEnabledCommand;
|
||||
ICommand IPipelineEditorHostViewModel.MoveNodeUpCommand => MoveNodeUpCommand;
|
||||
ICommand IPipelineEditorHostViewModel.MoveNodeDownCommand => MoveNodeDownCommand;
|
||||
ICommand IPipelineEditorHostViewModel.NewPipelineCommand => NewPipelineCommand;
|
||||
@@ -217,6 +221,7 @@ namespace XplorePlane.ViewModels
|
||||
};
|
||||
LoadNodeParameters(node);
|
||||
PipelineNodes.Add(node);
|
||||
SelectedNode = node;
|
||||
_logger.Info("节点已添加到 PipelineNodes:{Key} ({DisplayName}),当前节点数={Count}",
|
||||
operatorKey, displayName, PipelineNodes.Count);
|
||||
StatusMessage = $"已添加算子:{displayName}";
|
||||
@@ -227,11 +232,10 @@ namespace XplorePlane.ViewModels
|
||||
{
|
||||
if (node == null || !PipelineNodes.Contains(node)) return;
|
||||
|
||||
var removedIndex = PipelineNodes.IndexOf(node);
|
||||
PipelineNodes.Remove(node);
|
||||
RenumberNodes();
|
||||
|
||||
if (SelectedNode == node)
|
||||
SelectedNode = null;
|
||||
SelectNeighborAfterRemoval(removedIndex);
|
||||
|
||||
StatusMessage = $"已移除算子:{node.DisplayName}";
|
||||
TriggerDebouncedExecution();
|
||||
@@ -271,6 +275,20 @@ namespace XplorePlane.ViewModels
|
||||
PipelineNodes.RemoveAt(oldIndex);
|
||||
PipelineNodes.Insert(newIndex, node);
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -280,6 +298,20 @@ namespace XplorePlane.ViewModels
|
||||
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)
|
||||
{
|
||||
var paramDefs = _imageProcessingService.GetProcessorParameters(node.OperatorKey);
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
|
||||
d:DesignHeight="700"
|
||||
d:DesignWidth="350"
|
||||
mc:Ignorable="d">
|
||||
@@ -13,13 +12,27 @@
|
||||
<SolidColorBrush x:Key="PanelBg" Color="White" />
|
||||
<SolidColorBrush x:Key="PanelBorder" Color="#CDCBCB" />
|
||||
<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>
|
||||
|
||||
<Style x:Key="PipelineNodeItemStyle" TargetType="ListBoxItem">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<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 x:Key="ToolbarBtn" TargetType="Button">
|
||||
@@ -43,7 +56,7 @@
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="2*" MinHeight="180" />
|
||||
<RowDefinition Height="4*" MinHeight="180" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="2*" MinHeight="80" />
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -66,19 +79,19 @@
|
||||
Command="{Binding SavePipelineCommand}"
|
||||
Content="保存"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="保存当前检测模块流水线" />
|
||||
ToolTip="保存当前流水线" />
|
||||
<Button
|
||||
Width="60"
|
||||
Command="{Binding SaveAsPipelineCommand}"
|
||||
Content="另存为"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="导出当前检测模块流水线" />
|
||||
ToolTip="另存当前流水线" />
|
||||
<Button
|
||||
Width="52"
|
||||
Command="{Binding LoadPipelineCommand}"
|
||||
Content="加载"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="将流水线模板加载到当前检测模块" />
|
||||
ToolTip="加载流水线" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
@@ -90,114 +103,157 @@
|
||||
BorderThickness="0"
|
||||
ItemContainerStyle="{StaticResource PipelineNodeItemStyle}"
|
||||
ItemsSource="{Binding PipelineNodes}"
|
||||
KeyboardNavigation.TabNavigation="Continue"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid x:Name="NodeRoot" MinHeight="48">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="44" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border
|
||||
x:Name="NodeContainer"
|
||||
Margin="2"
|
||||
Padding="2"
|
||||
Background="Transparent"
|
||||
BorderBrush="Transparent"
|
||||
BorderThickness="1"
|
||||
CornerRadius="3">
|
||||
<Grid x:Name="NodeRoot" MinHeight="48">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="44" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Line
|
||||
x:Name="TopLine"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Top"
|
||||
Stroke="#5B9BD5"
|
||||
StrokeThickness="2"
|
||||
X1="0"
|
||||
X2="0"
|
||||
Y1="0"
|
||||
Y2="10" />
|
||||
<Line
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
Stroke="#5B9BD5"
|
||||
StrokeThickness="2"
|
||||
X1="0"
|
||||
X2="0"
|
||||
Y1="0"
|
||||
Y2="14" />
|
||||
<Line
|
||||
x:Name="TopLine"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Top"
|
||||
Stroke="#5B9BD5"
|
||||
StrokeThickness="2"
|
||||
X1="0"
|
||||
X2="0"
|
||||
Y1="0"
|
||||
Y2="10" />
|
||||
<Line
|
||||
x:Name="BottomLine"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
Stroke="#5B9BD5"
|
||||
StrokeThickness="2"
|
||||
X1="0"
|
||||
X2="0"
|
||||
Y1="0"
|
||||
Y2="14" />
|
||||
|
||||
<Border
|
||||
Grid.Column="0"
|
||||
Width="28"
|
||||
Height="28"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="#E8F0FE"
|
||||
BorderBrush="#5B9BD5"
|
||||
BorderThickness="1.5"
|
||||
CornerRadius="4">
|
||||
<TextBlock
|
||||
<Border
|
||||
x:Name="IconBorder"
|
||||
Grid.Column="0"
|
||||
Width="28"
|
||||
Height="28"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="13"
|
||||
Text="{Binding IconPath}" />
|
||||
</Border>
|
||||
Background="#E8F0FE"
|
||||
BorderBrush="#5B9BD5"
|
||||
BorderThickness="1.5"
|
||||
CornerRadius="4">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="13"
|
||||
Text="{Binding IconPath}" />
|
||||
</Border>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="6,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="12"
|
||||
Text="{Binding DisplayName}" />
|
||||
<StackPanel Grid.Column="1" Margin="6,0,0,0" VerticalAlignment="Center">
|
||||
<TextBlock
|
||||
x:Name="NodeTitle"
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="12"
|
||||
Text="{Binding DisplayName}" />
|
||||
<TextBlock
|
||||
x:Name="NodeState"
|
||||
Margin="0,2,0,0"
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="10"
|
||||
Foreground="#6E6E6E"
|
||||
Text="已启用" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel
|
||||
x:Name="NodeActions"
|
||||
Grid.Column="2"
|
||||
Margin="0,0,4,0"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal"
|
||||
Visibility="Collapsed">
|
||||
<Button
|
||||
Width="22"
|
||||
Height="22"
|
||||
Margin="1,0"
|
||||
Background="Transparent"
|
||||
BorderBrush="#CDCBCB"
|
||||
BorderThickness="1"
|
||||
Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="上"
|
||||
Cursor="Hand"
|
||||
FontSize="10"
|
||||
ToolTip="上移" />
|
||||
<Button
|
||||
Width="22"
|
||||
Height="22"
|
||||
Margin="1,0"
|
||||
Background="Transparent"
|
||||
BorderBrush="#CDCBCB"
|
||||
BorderThickness="1"
|
||||
Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="下"
|
||||
Cursor="Hand"
|
||||
FontSize="10"
|
||||
ToolTip="下移" />
|
||||
<Button
|
||||
Width="22"
|
||||
Height="22"
|
||||
Margin="1,0"
|
||||
Background="Transparent"
|
||||
BorderBrush="#E05050"
|
||||
BorderThickness="1"
|
||||
Command="{Binding DataContext.RemoveOperatorCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="删"
|
||||
Cursor="Hand"
|
||||
FontSize="10"
|
||||
ToolTip="删除" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<StackPanel
|
||||
x:Name="NodeActions"
|
||||
Grid.Column="2"
|
||||
Margin="0,0,4,0"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal"
|
||||
Visibility="Collapsed">
|
||||
<Button
|
||||
Width="22"
|
||||
Height="22"
|
||||
Margin="1,0"
|
||||
Background="Transparent"
|
||||
BorderBrush="#CDCBCB"
|
||||
BorderThickness="1"
|
||||
Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="上"
|
||||
Cursor="Hand"
|
||||
FontSize="10"
|
||||
ToolTip="上移" />
|
||||
<Button
|
||||
Width="22"
|
||||
Height="22"
|
||||
Margin="1,0"
|
||||
Background="Transparent"
|
||||
BorderBrush="#CDCBCB"
|
||||
BorderThickness="1"
|
||||
Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="下"
|
||||
Cursor="Hand"
|
||||
FontSize="10"
|
||||
ToolTip="下移" />
|
||||
<Button
|
||||
Width="22"
|
||||
Height="22"
|
||||
Margin="1,0"
|
||||
Background="Transparent"
|
||||
BorderBrush="#E05050"
|
||||
BorderThickness="1"
|
||||
Command="{Binding DataContext.RemoveOperatorCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="删"
|
||||
Cursor="Hand"
|
||||
FontSize="10"
|
||||
ToolTip="删除" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
<DataTemplate.Triggers>
|
||||
<DataTrigger Binding="{Binding Order}" Value="0">
|
||||
<Setter TargetName="TopLine" Property="Visibility" Value="Collapsed" />
|
||||
</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">
|
||||
<Setter TargetName="NodeActions" Property="Visibility" Value="Visible" />
|
||||
</Trigger>
|
||||
|
||||
@@ -2,15 +2,26 @@ using Prism.Ioc;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.ViewModels;
|
||||
|
||||
namespace XplorePlane.Views
|
||||
{
|
||||
public partial class PipelineEditorView : UserControl
|
||||
{
|
||||
private const string PipelineNodeDragFormat = "PipelineNodeDrag";
|
||||
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
private Point _dragStartPoint;
|
||||
private bool _isInternalDragging;
|
||||
private bool _suppressClickToggle;
|
||||
private PipelineNodeViewModel _draggedNode;
|
||||
|
||||
public PipelineEditorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -35,50 +46,249 @@ namespace XplorePlane.Views
|
||||
}
|
||||
}
|
||||
|
||||
_logger?.Info("PipelineEditorView DataContext 类型={Type}",
|
||||
DataContext?.GetType().Name);
|
||||
|
||||
PipelineListBox.AllowDrop = true;
|
||||
PipelineListBox.Drop += OnOperatorDropped;
|
||||
PipelineListBox.Focusable = true;
|
||||
PipelineListBox.Drop -= OnListBoxDrop;
|
||||
PipelineListBox.Drop += OnListBoxDrop;
|
||||
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)
|
||||
{
|
||||
e.Effects = e.Data.GetDataPresent(OperatorToolboxView.DragFormat)
|
||||
? DragDropEffects.Copy
|
||||
: DragDropEffects.None;
|
||||
if (e.Data.GetDataPresent(OperatorToolboxView.DragFormat))
|
||||
{
|
||||
e.Effects = DragDropEffects.Copy;
|
||||
}
|
||||
else if (e.Data.GetDataPresent(PipelineNodeDragFormat))
|
||||
{
|
||||
e.Effects = DragDropEffects.Move;
|
||||
}
|
||||
else
|
||||
{
|
||||
e.Effects = DragDropEffects.None;
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnOperatorDropped(object sender, DragEventArgs e)
|
||||
private void OnListBoxDrop(object sender, DragEventArgs e)
|
||||
{
|
||||
if (DataContext is not IPipelineEditorHostViewModel vm)
|
||||
{
|
||||
_logger?.Warn("Drop 事件触发,但 DataContext 不是流水线宿主 ViewModel");
|
||||
ResetDragState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!e.Data.GetDataPresent(OperatorToolboxView.DragFormat))
|
||||
if (e.Data.GetDataPresent(OperatorToolboxView.DragFormat))
|
||||
{
|
||||
_logger?.Warn("Drop 事件触发,但数据中没有 {Format}", OperatorToolboxView.DragFormat);
|
||||
return;
|
||||
OnOperatorDropped(vm, e);
|
||||
}
|
||||
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;
|
||||
if (string.IsNullOrWhiteSpace(operatorKey))
|
||||
{
|
||||
_logger?.Warn("Drop 事件触发,但 OperatorKey 为空");
|
||||
_logger?.Warn("Drop 触发,但 OperatorKey 为空");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger?.Info("算子已放入流水线:{OperatorKey},当前节点数(执行前)={Count}",
|
||||
operatorKey, vm.PipelineNodes.Count);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user