diff --git a/src/DynamoCoreWpf/Controls/ShortcutToolbar.xaml.cs b/src/DynamoCoreWpf/Controls/ShortcutToolbar.xaml.cs index 5c154ffe08a..59278bd64bd 100644 --- a/src/DynamoCoreWpf/Controls/ShortcutToolbar.xaml.cs +++ b/src/DynamoCoreWpf/Controls/ShortcutToolbar.xaml.cs @@ -77,7 +77,6 @@ public ShortcutToolbar(DynamoViewModel dynamoViewModel) private void ShortcutToolbar_Loaded(object sender, RoutedEventArgs e) { - IsSaveButtonEnabled = false; IsExportMenuEnabled = false; IsLoginMenuEnabled = !DynamoViewModel.Model.NoNetworkMode; DynamoViewModel.OnRequestShorcutToolbarLoaded(RightMenu.ActualWidth); @@ -245,19 +244,6 @@ internal bool IsOpenButtonEnabled } } - internal bool IsSaveButtonEnabled - { - set - { - Button saveButton = GetButton("SAVE"); - if (saveButton != null) - { - saveButton.IsEnabled = value; - saveButton.Opacity = value ? 1 : 0.5; - } - } - } - internal bool IsLoginMenuEnabled { set diff --git a/src/DynamoCoreWpf/UI/GuidedTour/GuidesManager.cs b/src/DynamoCoreWpf/UI/GuidedTour/GuidesManager.cs index ca728b460a7..046d44ecb02 100644 --- a/src/DynamoCoreWpf/UI/GuidedTour/GuidesManager.cs +++ b/src/DynamoCoreWpf/UI/GuidedTour/GuidesManager.cs @@ -164,6 +164,7 @@ internal void LaunchTour(string tourName) GuideFlowEvents.OnGuidedTourStart(tourName); dynamoViewModel.ToastManager?.CloseRealTimeInfoWindow(); dynamoViewModel.OnEnableShortcutBarItems(false); + dynamoViewModel.SetGuidedTourActive(true); Logging.Analytics.TrackScreenView("InteractiveGuidedTours"); Logging.Analytics.TrackEvent(Logging.Actions.Start, Logging.Categories.GuidedTourOperations, Resources.ResourceManager.GetString(currentGuide.GuideNameResource, System.Globalization.CultureInfo.InvariantCulture).Replace("_", ""), currentGuide.SequenceOrder); } @@ -257,6 +258,7 @@ internal void ExitTour() } dynamoViewModel.OnEnableShortcutBarItems(true); + dynamoViewModel.SetGuidedTourActive(false); //Hide guide background overlay guideBackgroundElement.Visibility = Visibility.Hidden; diff --git a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs index 46459a29fb1..ca5078ec86a 100644 --- a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs @@ -72,6 +72,8 @@ public partial class DynamoViewModel : ViewModelBase, IDynamoViewModel private readonly DynamoModel model; private Point transformOrigin; private bool showStartPage = false; + private bool isGuidedTourActive = false; + private WorkspaceModel saveCommandsTrackedWorkspace; private PreferencesViewModel preferencesViewModel; private string dynamoMLDataPath = string.Empty; private const string dynamoMLDataFileName = "DynamoMLDataPipeline.json"; @@ -428,6 +430,9 @@ public bool ShowStartPage if(ShowInsertDialogAndInsertResultCommand != null) ShowInsertDialogAndInsertResultCommand.RaiseCanExecuteChanged(); + + ShowSaveDialogIfNeededAndSaveResultCommand?.RaiseCanExecuteChanged(); + ShowSaveDialogAndSaveResultCommand?.RaiseCanExecuteChanged(); } } @@ -898,6 +903,7 @@ protected DynamoViewModel(StartConfiguration startConfiguration) SubscribeModelUiEvents(); SubscribeModelChangedHandlers(); SubscribeModelBackupFileSaveEvent(); + TrackWorkspaceForSaveCommands(model.CurrentWorkspace); InitializeAutomationSettings(startConfiguration.CommandFilePath); @@ -1304,6 +1310,32 @@ private void UnsubscribeModelChangedEvents() model.PropertyChanged -= _model_PropertyChanged; model.WorkspaceCleared -= ModelWorkspaceCleared; model.RequestCancelActiveStateForNode -= this.CancelActiveState; + TrackWorkspaceForSaveCommands(null); + } + + /// + /// Keeps the Save command's CanExecute in sync with the current workspace's dirty + /// flag: unsubscribes from the previously tracked workspace and subscribes to the + /// new one, then re-evaluates CanExecute immediately (the new workspace may already + /// differ in HasUnsavedChanges from the old one). + /// + private void TrackWorkspaceForSaveCommands(WorkspaceModel workspace) + { + if (saveCommandsTrackedWorkspace != null) + saveCommandsTrackedWorkspace.PropertyChanged -= SaveCommandsTrackedWorkspace_PropertyChanged; + + saveCommandsTrackedWorkspace = workspace; + + if (saveCommandsTrackedWorkspace != null) + saveCommandsTrackedWorkspace.PropertyChanged += SaveCommandsTrackedWorkspace_PropertyChanged; + + ShowSaveDialogIfNeededAndSaveResultCommand?.RaiseCanExecuteChanged(); + } + + private void SaveCommandsTrackedWorkspace_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(WorkspaceModel.HasUnsavedChanges)) + ShowSaveDialogIfNeededAndSaveResultCommand.RaiseCanExecuteChanged(); } private void SubscribeDispatcherHandlers() @@ -1482,6 +1514,7 @@ void _model_PropertyChanged(object sender, PropertyChangedEventArgs e) RaisePropertyChanged("ViewingHomespace"); if (this.PublishCurrentWorkspaceCommand != null) this.PublishCurrentWorkspaceCommand.RaiseCanExecuteChanged(); + TrackWorkspaceForSaveCommands(model.CurrentWorkspace); RaisePropertyChanged("IsPanning"); RaisePropertyChanged("IsOrbiting"); //RaisePropertyChanged("RunEnabled"); @@ -3245,9 +3278,28 @@ public void ShowSaveDialogIfNeededAndSaveResult(object parameter) } } + /// + /// "Save" is only meaningful when there is something new to persist, so it is also + /// gated on the current workspace's dirty flag (unlike "Save As", which can always + /// save a copy regardless of whether anything changed). + /// internal bool CanShowSaveDialogIfNeededAndSaveResultCommand(object parameter) { - return true; + return !isGuidedTourActive && !ShowStartPage && (Model.CurrentWorkspace?.HasUnsavedChanges ?? false); + } + + /// + /// Blocks or unblocks the Save/Save As commands (menu items, shortcut bar, and + /// Ctrl+S/Ctrl+Shift+S) while a guided tour is active. Unlike ShowStartPage, this is + /// not tied to workspace-creation flows, so it can safely gate CanExecute without + /// resurrecting DYN-10717 (Save/Save As stuck disabled on a fresh workspace). + /// + /// Whether a guided tour is currently active. + internal void SetGuidedTourActive(bool isActive) + { + isGuidedTourActive = isActive; + ShowSaveDialogIfNeededAndSaveResultCommand.RaiseCanExecuteChanged(); + ShowSaveDialogAndSaveResultCommand.RaiseCanExecuteChanged(); } public void ShowSaveDialogAndSaveResult(object parameter) @@ -3371,7 +3423,7 @@ private bool ShowWarningDialogOnSaveWithUnresolvedIssues() internal bool CanShowSaveDialogAndSaveResult(object parameter) { - return true; + return !isGuidedTourActive && !ShowStartPage; } public void ToggleFullscreenWatchShowing(object parameter) diff --git a/src/DynamoCoreWpf/Views/Core/DynamoView.xaml b/src/DynamoCoreWpf/Views/Core/DynamoView.xaml index 28bd90a97ed..fa532c6668b 100644 --- a/src/DynamoCoreWpf/Views/Core/DynamoView.xaml +++ b/src/DynamoCoreWpf/Views/Core/DynamoView.xaml @@ -354,13 +354,11 @@ + InputGestureText="Ctrl + S" /> + InputGestureText="Ctrl + Shift + S" />