diff --git a/XplorePlane/Assets/Icons/eye.png b/XplorePlane/Assets/Icons/eye.png new file mode 100644 index 0000000..800e01e Binary files /dev/null and b/XplorePlane/Assets/Icons/eye.png differ diff --git a/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs b/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs index c6e3ae7..1802177 100644 --- a/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs +++ b/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs @@ -324,6 +324,9 @@ namespace XplorePlane.ViewModels.Cnc } } + /// 供外部直接 await 的保存方法 + public Task SaveAsync() => ExecuteSaveProgramAsync(); + private async Task ExecuteSaveProgramAsync() { if (_currentProgram == null) diff --git a/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs b/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs index be00d31..bf7c2d7 100644 --- a/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs +++ b/XplorePlane/ViewModels/Cnc/CncInspectionModulePipelineViewModel.cs @@ -77,6 +77,9 @@ namespace XplorePlane.ViewModels.Cnc _editorViewModel.PropertyChanged += OnEditorPropertyChanged; RefreshFromSelection(); + + _eventAggregator?.GetEvent() + .Subscribe(key => { if (CanAddOperator(key)) AddOperator(key); }); } public ObservableCollection PipelineNodes { get; } @@ -174,6 +177,9 @@ namespace XplorePlane.ViewModels.Cnc RaiseCommandCanExecuteChanged(); } + private bool CanAddOperator(string operatorKey) => + HasActiveModule && !string.IsNullOrWhiteSpace(operatorKey); + private void AddOperator(string operatorKey) { if (!HasActiveModule || string.IsNullOrWhiteSpace(operatorKey)) diff --git a/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs b/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs index f0233c0..e21d72f 100644 --- a/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs +++ b/XplorePlane/ViewModels/ImageProcessing/PipelineEditorViewModel.cs @@ -42,6 +42,7 @@ namespace XplorePlane.ViewModels private string _pipelineFileDisplayName = DefaultPipelineFileDisplayName; private string _currentFilePath; private PipelineNodeViewModel _executionEndNode; + private bool _isModified; private CancellationTokenSource _executionCts; private CancellationTokenSource _debounceCts; @@ -172,6 +173,13 @@ namespace XplorePlane.ViewModels private set => SetProperty(ref _pipelineFileDisplayName, value); } + /// 流水线是否有未保存的修改 + public bool IsModified + { + get => _isModified; + private set => SetProperty(ref _isModified, value); + } + public PipelineNodeViewModel ExecutionEndNode { get => _executionEndNode; @@ -266,6 +274,7 @@ namespace XplorePlane.ViewModels _logger.Info("节点已添加到 PipelineNodes:{Key} ({DisplayName}),当前节点数={Count}", operatorKey, displayName, PipelineNodes.Count); SetInfoStatus($"已添加算子:{displayName}"); + IsModified = true; TriggerDebouncedExecution(); } @@ -283,6 +292,7 @@ namespace XplorePlane.ViewModels SelectNeighborAfterRemoval(removedIndex); SetInfoStatus($"已移除算子:{node.DisplayName}"); + IsModified = true; TriggerDebouncedExecution(); } @@ -294,6 +304,7 @@ namespace XplorePlane.ViewModels PipelineNodes.Move(index, index - 1); RenumberNodes(); UpdateExecutionRangeState(); + IsModified = true; TriggerDebouncedExecution(); } @@ -305,6 +316,7 @@ namespace XplorePlane.ViewModels PipelineNodes.Move(index, index + 1); RenumberNodes(); UpdateExecutionRangeState(); + IsModified = true; TriggerDebouncedExecution(); } @@ -325,6 +337,7 @@ namespace XplorePlane.ViewModels UpdateExecutionRangeState(); SelectedNode = node; SetInfoStatus($"已调整算子顺序:{node.DisplayName}"); + IsModified = true; TriggerDebouncedExecution(); } @@ -337,6 +350,7 @@ namespace XplorePlane.ViewModels SetInfoStatus(node.IsEnabled ? $"已启用算子:{node.DisplayName}" : $"已停用算子:{node.DisplayName}"); + IsModified = true; TriggerDebouncedExecution(); } @@ -406,6 +420,7 @@ namespace XplorePlane.ViewModels { if (e.PropertyName == nameof(ProcessorParameterVM.Value)) { + IsModified = true; if (TryReportInvalidParameters()) return; @@ -623,9 +638,13 @@ namespace XplorePlane.ViewModels PreviewImage = null; _currentFilePath = null; PipelineFileDisplayName = DefaultPipelineFileDisplayName; + IsModified = false; SetInfoStatus("已新建流水线"); } + /// 供外部直接 await 的保存方法 + public Task SaveAsync() => SavePipelineAsync(); + private async Task SavePipelineAsync() { if (string.IsNullOrWhiteSpace(PipelineName) || PipelineName.Length > 100) @@ -674,6 +693,7 @@ namespace XplorePlane.ViewModels var model = BuildPipelineModel(); await _persistenceService.SaveAsync(model, filePath); PipelineFileDisplayName = FormatPipelinePath(filePath); + IsModified = false; SetInfoStatus($"流水线已保存:{Path.GetFileName(filePath)}"); } catch (IOException ex) @@ -750,6 +770,7 @@ namespace XplorePlane.ViewModels UpdateExecutionRangeState(); _logger.Info("流水线已加载:{Name},节点数={Count}", model.Name, PipelineNodes.Count); + IsModified = false; SetInfoStatus($"已加载流水线:{model.Name}({PipelineNodes.Count} 个节点)"); } catch (Exception ex) diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs index 26e6403..9e9deb8 100644 --- a/XplorePlane/ViewModels/Main/MainViewModel.cs +++ b/XplorePlane/ViewModels/Main/MainViewModel.cs @@ -36,6 +36,8 @@ namespace XplorePlane.ViewModels private readonly IXpDataPathService _xpDataPathService; private readonly CncEditorViewModel _cncEditorViewModel; private readonly CncPageView _cncPageView; + private readonly PipelineEditorViewModel _pipelineEditorViewModel; + private readonly PipelineEditorView _pipelineEditorView; public string LicenseInfo { @@ -212,6 +214,8 @@ namespace XplorePlane.ViewModels _xpDataPathService = xpDataPathService ?? throw new ArgumentNullException(nameof(xpDataPathService)); _cncEditorViewModel = _containerProvider.Resolve(); _cncPageView = new CncPageView { DataContext = _cncEditorViewModel }; + _pipelineEditorViewModel = _containerProvider.Resolve(); + _pipelineEditorView = new PipelineEditorView { DataContext = _pipelineEditorViewModel }; _mainViewportService.StateChanged += OnMainViewportStateChanged; _cncEditorViewModel.PropertyChanged += (s, e) => @@ -310,7 +314,7 @@ namespace XplorePlane.ViewModels OpenRealTimeLogViewerCommand = new DelegateCommand(ExecuteOpenRealTimeLogViewer); UseLiveDetectorSourceCommand = new DelegateCommand(ExecuteUseLiveDetectorSource); - ImagePanelContent = new PipelineEditorView(); + ImagePanelContent = _pipelineEditorView; ViewportPanelWidth = new GridLength(1, GridUnitType.Star); ImagePanelWidth = new GridLength(320); DataRootPath = _xpDataPathService.RootPath; @@ -352,10 +356,31 @@ namespace XplorePlane.ViewModels } private void ExecuteOpenCncEditor() + { + _ = ExecuteOpenCncEditorAsync(); + } + + private async Task ExecuteOpenCncEditorAsync() { if (_isCncEditorMode) { - ImagePanelContent = new PipelineEditorView(); + // CNC → 普通模式:检查 CNC 程序是否有未保存修改 + if (_cncEditorViewModel.IsModified) + { + var result = MessageBox.Show( + "CNC 程序有未保存的修改,是否保存?", + "未保存的修改", + MessageBoxButton.YesNoCancel, + MessageBoxImage.Warning); + + if (result == MessageBoxResult.Cancel) + return; + + if (result == MessageBoxResult.Yes) + await _cncEditorViewModel.SaveAsync(); + } + + ImagePanelContent = _pipelineEditorView; ViewportPanelWidth = new GridLength(1, GridUnitType.Star); ImagePanelWidth = new GridLength(320); _isCncEditorMode = false; @@ -363,6 +388,22 @@ namespace XplorePlane.ViewModels return; } + // 普通 → CNC 模式:检查流水线是否有未保存修改 + if (_pipelineEditorViewModel.IsModified) + { + var result = MessageBox.Show( + "图像处理流水线有未保存的修改,是否保存?", + "未保存的修改", + MessageBoxButton.YesNoCancel, + MessageBoxImage.Warning); + + if (result == MessageBoxResult.Cancel) + return; + + if (result == MessageBoxResult.Yes) + await _pipelineEditorViewModel.SaveAsync(); + } + ShowCncEditor(); }