From 9911566675fa7db19402ddff3afd3cc376be06dd Mon Sep 17 00:00:00 2001 From: "zhengxuan.zhang" Date: Fri, 24 Apr 2026 01:58:11 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=8A=82=E7=82=B9=E5=8F=B3?= =?UTF-8?q?=E9=94=AE=E8=8F=9C=E5=8D=95=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E4=B8=8A=E6=96=B9=E6=8F=92=E5=85=A5=E5=92=8C=E4=B8=8B?= =?UTF-8?q?=E6=96=B9=E6=8F=92=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ViewModels/Cnc/CncEditorViewModel.cs | 92 ++++++++++- XplorePlane/Views/Cnc/CncPageView.xaml.cs | 156 ++++++++++++++++++ 2 files changed, 245 insertions(+), 3 deletions(-) diff --git a/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs b/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs index a38dbaa..738b945 100644 --- a/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs +++ b/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs @@ -36,6 +36,8 @@ namespace XplorePlane.ViewModels.Cnc private string _programName; private string _programDisplayName = "新建检测程序.xp"; private Guid? _preferredSelectedNodeId; + private Guid? _pendingInsertAnchorNodeId; + private bool _pendingInsertAfterAnchor; public CncEditorViewModel( ICncProgramService cncProgramService, @@ -69,6 +71,8 @@ namespace XplorePlane.ViewModels.Cnc .ObservesProperty(() => SelectedNode); MoveNodeUpCommand = new DelegateCommand(ExecuteMoveNodeUp); MoveNodeDownCommand = new DelegateCommand(ExecuteMoveNodeDown); + PrepareInsertAboveCommand = new DelegateCommand(nodeVm => SetPendingInsertAnchor(nodeVm, insertAfter: false)); + PrepareInsertBelowCommand = new DelegateCommand(nodeVm => SetPendingInsertAnchor(nodeVm, insertAfter: true)); SaveProgramCommand = new DelegateCommand(async () => await ExecuteSaveProgramAsync()); LoadProgramCommand = new DelegateCommand(async () => await ExecuteLoadProgramAsync()); @@ -144,6 +148,8 @@ namespace XplorePlane.ViewModels.Cnc public DelegateCommand DeleteNodeCommand { get; } public DelegateCommand MoveNodeUpCommand { get; } public DelegateCommand MoveNodeDownCommand { get; } + public DelegateCommand PrepareInsertAboveCommand { get; } + public DelegateCommand PrepareInsertBelowCommand { get; } public DelegateCommand SaveProgramCommand { get; } public DelegateCommand LoadProgramCommand { get; } public DelegateCommand NewProgramCommand { get; } @@ -162,6 +168,7 @@ namespace XplorePlane.ViewModels.Cnc int afterIndex = ResolveInsertAfterIndex(nodeType); _currentProgram = _cncProgramService.InsertNode(_currentProgram, afterIndex, node); _preferredSelectedNodeId = node.Id; + ClearPendingInsertAnchor(); OnProgramEdited(); _logger.Info("Inserted node: Type={NodeType}", nodeType); @@ -179,21 +186,24 @@ namespace XplorePlane.ViewModels.Cnc try { + int deletedIndex = SelectedNode.Index; + if (SelectedNode.IsSavePosition) { var nodes = _currentProgram.Nodes.ToList(); - int startIndex = SelectedNode.Index; + int startIndex = deletedIndex; int endIndex = GetSavePositionBlockEndIndex(startIndex); nodes.RemoveRange(startIndex, endIndex - startIndex + 1); _currentProgram = ReplaceProgramNodes(nodes); } else { - _currentProgram = _cncProgramService.RemoveNode(_currentProgram, SelectedNode.Index); + _currentProgram = _cncProgramService.RemoveNode(_currentProgram, deletedIndex); } OnProgramEdited(); - _logger.Info("Deleted node at index: {Index}", SelectedNode.Index); + ClearPendingInsertAnchorIfMissing(); + _logger.Info("Deleted node at index: {Index}", deletedIndex); } catch (ArgumentOutOfRangeException ex) { @@ -305,6 +315,7 @@ namespace XplorePlane.ViewModels.Cnc ProgramName = _currentProgram.Name; ProgramDisplayName = Path.GetFileName(dlg.FileName); IsModified = false; + ClearPendingInsertAnchor(); RefreshNodes(); } catch (Exception ex) @@ -320,6 +331,7 @@ namespace XplorePlane.ViewModels.Cnc ProgramName = _currentProgram.Name; ProgramDisplayName = FormatProgramDisplayName(_currentProgram.Name); IsModified = false; + ClearPendingInsertAnchor(); RefreshNodes(); } @@ -461,6 +473,11 @@ namespace XplorePlane.ViewModels.Cnc return -1; } + if (TryResolvePendingInsertAfterIndex(nodeType, out int pendingAfterIndex)) + { + return pendingAfterIndex; + } + if (!IsSavePositionChild(nodeType)) { return SelectedNode?.Index ?? _currentProgram.Nodes.Count - 1; @@ -475,6 +492,75 @@ namespace XplorePlane.ViewModels.Cnc 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) { int? parentIndex = FindOwningSavePositionIndex(nodeVm.Index); diff --git a/XplorePlane/Views/Cnc/CncPageView.xaml.cs b/XplorePlane/Views/Cnc/CncPageView.xaml.cs index 2d1eb05..f602d68 100644 --- a/XplorePlane/Views/Cnc/CncPageView.xaml.cs +++ b/XplorePlane/Views/Cnc/CncPageView.xaml.cs @@ -1,6 +1,7 @@ using Prism.Ioc; using System; using System.Globalization; +using System.Windows.Media; using System.Windows; using System.Windows.Controls; using System.Windows.Data; @@ -16,6 +17,11 @@ namespace XplorePlane.Views.Cnc /// 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; public CncPageView() @@ -62,6 +68,12 @@ namespace XplorePlane.Views.Cnc { // 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 e) @@ -70,6 +82,8 @@ namespace XplorePlane.Views.Cnc { viewModel.SelectedNode = e.NewValue as CncNodeViewModel; } + + UpdateNodeVisualState(); } private void CncTreeView_PreviewKeyDown(object sender, KeyEventArgs e) @@ -83,6 +97,148 @@ namespace XplorePlane.Views.Cnc viewModel.DeleteNodeCommand.Execute(); 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(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