增加节点右键菜单功能,支持上方插入和下方插入

This commit is contained in:
zhengxuan.zhang
2026-04-24 01:58:11 +08:00
parent ed4b1d8031
commit 9911566675
2 changed files with 245 additions and 3 deletions
@@ -36,6 +36,8 @@ namespace XplorePlane.ViewModels.Cnc
private string _programName; private string _programName;
private string _programDisplayName = "新建检测程序.xp"; private string _programDisplayName = "新建检测程序.xp";
private Guid? _preferredSelectedNodeId; private Guid? _preferredSelectedNodeId;
private Guid? _pendingInsertAnchorNodeId;
private bool _pendingInsertAfterAnchor;
public CncEditorViewModel( public CncEditorViewModel(
ICncProgramService cncProgramService, ICncProgramService cncProgramService,
@@ -69,6 +71,8 @@ namespace XplorePlane.ViewModels.Cnc
.ObservesProperty(() => SelectedNode); .ObservesProperty(() => SelectedNode);
MoveNodeUpCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeUp); MoveNodeUpCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeUp);
MoveNodeDownCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeDown); MoveNodeDownCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeDown);
PrepareInsertAboveCommand = new DelegateCommand<CncNodeViewModel>(nodeVm => SetPendingInsertAnchor(nodeVm, insertAfter: false));
PrepareInsertBelowCommand = new DelegateCommand<CncNodeViewModel>(nodeVm => SetPendingInsertAnchor(nodeVm, insertAfter: true));
SaveProgramCommand = new DelegateCommand(async () => await ExecuteSaveProgramAsync()); SaveProgramCommand = new DelegateCommand(async () => await ExecuteSaveProgramAsync());
LoadProgramCommand = new DelegateCommand(async () => await ExecuteLoadProgramAsync()); LoadProgramCommand = new DelegateCommand(async () => await ExecuteLoadProgramAsync());
@@ -144,6 +148,8 @@ namespace XplorePlane.ViewModels.Cnc
public DelegateCommand DeleteNodeCommand { get; } public DelegateCommand DeleteNodeCommand { get; }
public DelegateCommand<CncNodeViewModel> MoveNodeUpCommand { get; } public DelegateCommand<CncNodeViewModel> MoveNodeUpCommand { get; }
public DelegateCommand<CncNodeViewModel> MoveNodeDownCommand { get; } public DelegateCommand<CncNodeViewModel> MoveNodeDownCommand { get; }
public DelegateCommand<CncNodeViewModel> PrepareInsertAboveCommand { get; }
public DelegateCommand<CncNodeViewModel> PrepareInsertBelowCommand { get; }
public DelegateCommand SaveProgramCommand { get; } public DelegateCommand SaveProgramCommand { get; }
public DelegateCommand LoadProgramCommand { get; } public DelegateCommand LoadProgramCommand { get; }
public DelegateCommand NewProgramCommand { get; } public DelegateCommand NewProgramCommand { get; }
@@ -162,6 +168,7 @@ namespace XplorePlane.ViewModels.Cnc
int afterIndex = ResolveInsertAfterIndex(nodeType); int afterIndex = ResolveInsertAfterIndex(nodeType);
_currentProgram = _cncProgramService.InsertNode(_currentProgram, afterIndex, node); _currentProgram = _cncProgramService.InsertNode(_currentProgram, afterIndex, node);
_preferredSelectedNodeId = node.Id; _preferredSelectedNodeId = node.Id;
ClearPendingInsertAnchor();
OnProgramEdited(); OnProgramEdited();
_logger.Info("Inserted node: Type={NodeType}", nodeType); _logger.Info("Inserted node: Type={NodeType}", nodeType);
@@ -179,21 +186,24 @@ namespace XplorePlane.ViewModels.Cnc
try try
{ {
int deletedIndex = SelectedNode.Index;
if (SelectedNode.IsSavePosition) if (SelectedNode.IsSavePosition)
{ {
var nodes = _currentProgram.Nodes.ToList(); var nodes = _currentProgram.Nodes.ToList();
int startIndex = SelectedNode.Index; int startIndex = deletedIndex;
int endIndex = GetSavePositionBlockEndIndex(startIndex); int endIndex = GetSavePositionBlockEndIndex(startIndex);
nodes.RemoveRange(startIndex, endIndex - startIndex + 1); nodes.RemoveRange(startIndex, endIndex - startIndex + 1);
_currentProgram = ReplaceProgramNodes(nodes); _currentProgram = ReplaceProgramNodes(nodes);
} }
else else
{ {
_currentProgram = _cncProgramService.RemoveNode(_currentProgram, SelectedNode.Index); _currentProgram = _cncProgramService.RemoveNode(_currentProgram, deletedIndex);
} }
OnProgramEdited(); OnProgramEdited();
_logger.Info("Deleted node at index: {Index}", SelectedNode.Index); ClearPendingInsertAnchorIfMissing();
_logger.Info("Deleted node at index: {Index}", deletedIndex);
} }
catch (ArgumentOutOfRangeException ex) catch (ArgumentOutOfRangeException ex)
{ {
@@ -305,6 +315,7 @@ namespace XplorePlane.ViewModels.Cnc
ProgramName = _currentProgram.Name; ProgramName = _currentProgram.Name;
ProgramDisplayName = Path.GetFileName(dlg.FileName); ProgramDisplayName = Path.GetFileName(dlg.FileName);
IsModified = false; IsModified = false;
ClearPendingInsertAnchor();
RefreshNodes(); RefreshNodes();
} }
catch (Exception ex) catch (Exception ex)
@@ -320,6 +331,7 @@ namespace XplorePlane.ViewModels.Cnc
ProgramName = _currentProgram.Name; ProgramName = _currentProgram.Name;
ProgramDisplayName = FormatProgramDisplayName(_currentProgram.Name); ProgramDisplayName = FormatProgramDisplayName(_currentProgram.Name);
IsModified = false; IsModified = false;
ClearPendingInsertAnchor();
RefreshNodes(); RefreshNodes();
} }
@@ -461,6 +473,11 @@ namespace XplorePlane.ViewModels.Cnc
return -1; return -1;
} }
if (TryResolvePendingInsertAfterIndex(nodeType, out int pendingAfterIndex))
{
return pendingAfterIndex;
}
if (!IsSavePositionChild(nodeType)) if (!IsSavePositionChild(nodeType))
{ {
return SelectedNode?.Index ?? _currentProgram.Nodes.Count - 1; return SelectedNode?.Index ?? _currentProgram.Nodes.Count - 1;
@@ -475,6 +492,75 @@ namespace XplorePlane.ViewModels.Cnc
return GetSavePositionBlockEndIndex(savePositionIndex.Value); return GetSavePositionBlockEndIndex(savePositionIndex.Value);
} }
private void SetPendingInsertAnchor(CncNodeViewModel nodeVm, bool insertAfter)
{
if (_currentProgram == null || nodeVm == null)
{
return;
}
_pendingInsertAnchorNodeId = nodeVm.Id;
_pendingInsertAfterAnchor = insertAfter;
SelectedNode = nodeVm;
}
private bool TryResolvePendingInsertAfterIndex(CncNodeType nodeType, out int afterIndex)
{
afterIndex = -1;
if (!_pendingInsertAnchorNodeId.HasValue || _currentProgram == null || IsSavePositionChild(nodeType))
{
return false;
}
int anchorIndex = FindNodeIndexById(_pendingInsertAnchorNodeId.Value);
if (anchorIndex < 0)
{
ClearPendingInsertAnchor();
return false;
}
afterIndex = _pendingInsertAfterAnchor ? anchorIndex : anchorIndex - 1;
return true;
}
private int FindNodeIndexById(Guid nodeId)
{
if (_currentProgram?.Nodes == null)
{
return -1;
}
for (int i = 0; i < _currentProgram.Nodes.Count; i++)
{
if (_currentProgram.Nodes[i].Id == nodeId)
{
return i;
}
}
return -1;
}
private void ClearPendingInsertAnchor()
{
_pendingInsertAnchorNodeId = null;
_pendingInsertAfterAnchor = false;
}
private void ClearPendingInsertAnchorIfMissing()
{
if (!_pendingInsertAnchorNodeId.HasValue)
{
return;
}
if (FindNodeIndexById(_pendingInsertAnchorNodeId.Value) < 0)
{
ClearPendingInsertAnchor();
}
}
private void MoveSavePositionChild(CncNodeViewModel nodeVm, bool moveDown) private void MoveSavePositionChild(CncNodeViewModel nodeVm, bool moveDown)
{ {
int? parentIndex = FindOwningSavePositionIndex(nodeVm.Index); int? parentIndex = FindOwningSavePositionIndex(nodeVm.Index);
+156
View File
@@ -1,6 +1,7 @@
using Prism.Ioc; using Prism.Ioc;
using System; using System;
using System.Globalization; using System.Globalization;
using System.Windows.Media;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Data; using System.Windows.Data;
@@ -16,6 +17,11 @@ namespace XplorePlane.Views.Cnc
/// </summary> /// </summary>
public partial class CncPageView : UserControl public partial class CncPageView : UserControl
{ {
private static readonly Brush SelectedNodeBackground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#E7F0F7"));
private static readonly Brush SelectedNodeBorder = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9CB9D1"));
private static readonly Brush HoverNodeBackground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F6FAFC"));
private static readonly Brush HoverNodeBorder = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#D7E4EE"));
private static readonly Brush TransparentBrush = Brushes.Transparent;
private CncInspectionModulePipelineViewModel _inspectionModulePipelineViewModel; private CncInspectionModulePipelineViewModel _inspectionModulePipelineViewModel;
public CncPageView() public CncPageView()
@@ -62,6 +68,12 @@ namespace XplorePlane.Views.Cnc
{ {
// keep page usable even if pipeline editor host setup fails // keep page usable even if pipeline editor host setup fails
} }
CncTreeView.ContextMenuOpening -= CncTreeView_ContextMenuOpening;
CncTreeView.ContextMenuOpening += CncTreeView_ContextMenuOpening;
CncTreeView.LayoutUpdated -= CncTreeView_LayoutUpdated;
CncTreeView.LayoutUpdated += CncTreeView_LayoutUpdated;
UpdateNodeVisualState();
} }
private void CncTreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) private void CncTreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
@@ -70,6 +82,8 @@ namespace XplorePlane.Views.Cnc
{ {
viewModel.SelectedNode = e.NewValue as CncNodeViewModel; viewModel.SelectedNode = e.NewValue as CncNodeViewModel;
} }
UpdateNodeVisualState();
} }
private void CncTreeView_PreviewKeyDown(object sender, KeyEventArgs e) private void CncTreeView_PreviewKeyDown(object sender, KeyEventArgs e)
@@ -83,6 +97,148 @@ namespace XplorePlane.Views.Cnc
viewModel.DeleteNodeCommand.Execute(); viewModel.DeleteNodeCommand.Execute();
e.Handled = true; e.Handled = true;
} }
private void CncTreeView_ContextMenuOpening(object sender, ContextMenuEventArgs e)
{
if (DataContext is not CncEditorViewModel viewModel)
{
return;
}
var position = Mouse.GetPosition(CncTreeView);
var hit = VisualTreeHelper.HitTest(CncTreeView, position);
var treeViewItem = FindAncestor<TreeViewItem>(hit?.VisualHit);
if (treeViewItem?.DataContext is not CncNodeViewModel nodeVm)
{
CncTreeView.ContextMenu = null;
return;
}
viewModel.SelectedNode = nodeVm;
UpdateNodeVisualState();
CncTreeView.ContextMenu = new ContextMenu
{
Items =
{
new MenuItem
{
Header = "在上方插入位置",
Command = viewModel.PrepareInsertAboveCommand,
CommandParameter = nodeVm
},
new MenuItem
{
Header = "在下方插入位置",
Command = viewModel.PrepareInsertBelowCommand,
CommandParameter = nodeVm
}
}
};
}
private void CncTreeView_LayoutUpdated(object sender, EventArgs e)
{
HideInlineDeleteButtons();
UpdateNodeVisualState();
}
private void HideInlineDeleteButtons()
{
foreach (var button in FindVisualDescendants<Button>(CncTreeView))
{
if (button.ToolTip is string)
{
button.Visibility = Visibility.Collapsed;
button.IsHitTestVisible = false;
}
}
}
private void UpdateNodeVisualState()
{
foreach (var item in FindVisualDescendants<TreeViewItem>(CncTreeView))
{
if (item.DataContext is not CncNodeViewModel)
{
continue;
}
var card = FindNodeCard(item);
if (card == null)
{
continue;
}
if (item.IsSelected)
{
card.Background = SelectedNodeBackground;
card.BorderBrush = SelectedNodeBorder;
}
else if (card.IsMouseOver)
{
card.Background = HoverNodeBackground;
card.BorderBrush = HoverNodeBorder;
}
else
{
card.Background = TransparentBrush;
card.BorderBrush = TransparentBrush;
}
}
}
private static Border FindNodeCard(DependencyObject root)
{
foreach (var border in FindVisualDescendants<Border>(root))
{
if (border.DataContext is CncNodeViewModel && border.CornerRadius.TopLeft == 4 && border.BorderThickness.Left == 1)
{
return border;
}
}
return null;
}
private static T FindAncestor<T>(DependencyObject dependencyObject) where T : DependencyObject
{
var current = dependencyObject;
while (current != null)
{
if (current is T match)
{
return match;
}
current = VisualTreeHelper.GetParent(current);
}
return null;
}
private static System.Collections.Generic.IEnumerable<T> FindVisualDescendants<T>(DependencyObject root) where T : DependencyObject
{
if (root == null)
{
yield break;
}
int childCount = VisualTreeHelper.GetChildrenCount(root);
for (int i = 0; i < childCount; i++)
{
var child = VisualTreeHelper.GetChild(root, i);
if (child is T typed)
{
yield return typed;
}
foreach (var descendant in FindVisualDescendants<T>(child))
{
yield return descendant;
}
}
}
} }
public class NullToVisibilityConverter : IValueConverter public class NullToVisibilityConverter : IValueConverter