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" />