Skip to content
Open
14 changes: 0 additions & 14 deletions src/DynamoCoreWpf/Controls/ShortcutToolbar.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/DynamoCoreWpf/UI/GuidedTour/GuidesManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -257,6 +258,7 @@ internal void ExitTour()
}

dynamoViewModel.OnEnableShortcutBarItems(true);
dynamoViewModel.SetGuidedTourActive(false);

//Hide guide background overlay
guideBackgroundElement.Visibility = Visibility.Hidden;
Expand Down
56 changes: 54 additions & 2 deletions src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -428,6 +430,9 @@ public bool ShowStartPage

if(ShowInsertDialogAndInsertResultCommand != null)
ShowInsertDialogAndInsertResultCommand.RaiseCanExecuteChanged();

ShowSaveDialogIfNeededAndSaveResultCommand?.RaiseCanExecuteChanged();
ShowSaveDialogAndSaveResultCommand?.RaiseCanExecuteChanged();
}
}

Expand Down Expand Up @@ -898,6 +903,7 @@ protected DynamoViewModel(StartConfiguration startConfiguration)
SubscribeModelUiEvents();
SubscribeModelChangedHandlers();
SubscribeModelBackupFileSaveEvent();
TrackWorkspaceForSaveCommands(model.CurrentWorkspace);

InitializeAutomationSettings(startConfiguration.CommandFilePath);

Expand Down Expand Up @@ -1304,6 +1310,32 @@ private void UnsubscribeModelChangedEvents()
model.PropertyChanged -= _model_PropertyChanged;
model.WorkspaceCleared -= ModelWorkspaceCleared;
model.RequestCancelActiveStateForNode -= this.CancelActiveState;
TrackWorkspaceForSaveCommands(null);
}

/// <summary>
/// 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).
/// </summary>
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()
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -3245,9 +3278,28 @@ public void ShowSaveDialogIfNeededAndSaveResult(object parameter)
}
}

/// <summary>
/// "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).
/// </summary>
internal bool CanShowSaveDialogIfNeededAndSaveResultCommand(object parameter)
{
return true;
return !isGuidedTourActive && !ShowStartPage && (Model.CurrentWorkspace?.HasUnsavedChanges ?? false);
}

/// <summary>
/// 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).
/// </summary>
/// <param name="isActive">Whether a guided tour is currently active.</param>
internal void SetGuidedTourActive(bool isActive)
{
isGuidedTourActive = isActive;
ShowSaveDialogIfNeededAndSaveResultCommand.RaiseCanExecuteChanged();
ShowSaveDialogAndSaveResultCommand.RaiseCanExecuteChanged();
}

public void ShowSaveDialogAndSaveResult(object parameter)
Expand Down Expand Up @@ -3371,7 +3423,7 @@ private bool ShowWarningDialogOnSaveWithUnresolvedIssues()

internal bool CanShowSaveDialogAndSaveResult(object parameter)
{
return true;
return !isGuidedTourActive && !ShowStartPage;
}

public void ToggleFullscreenWatchShowing(object parameter)
Expand Down
6 changes: 2 additions & 4 deletions src/DynamoCoreWpf/Views/Core/DynamoView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -354,13 +354,11 @@
<MenuItem Name="saveThisButton"
Command="{Binding ShowSaveDialogIfNeededAndSaveResultCommand}"
Header="{x:Static p:Resources.DynamoViewFileMenuSave}"
InputGestureText="Ctrl + S"
IsEnabled="False" />
Comment thread
edwin-vasquez-ucaldas marked this conversation as resolved.
InputGestureText="Ctrl + S" />
<MenuItem Name="saveButton"
Command="{Binding ShowSaveDialogAndSaveResultCommand}"
Header="{x:Static p:Resources.DynamoViewFileMenuSaveAs}"
InputGestureText="Ctrl + Shift + S"
IsEnabled="False" />
InputGestureText="Ctrl + Shift + S" />
<Separator />
<MenuItem Name="importLibrary"
Command="{Binding Path=DataContext.ImportLibraryCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type controls:DynamoView}}}"
Expand Down
13 changes: 0 additions & 13 deletions src/DynamoCoreWpf/Views/Core/DynamoView.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -435,11 +435,6 @@ void DynamoView_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)

private void DynamoViewModel_RequestEnableShortcutBarItems(bool enable)
{
if (!(saveThisButton is null))
{
saveThisButton.IsEnabled = enable;
saveButton.IsEnabled = enable;
}
if (!(exportMenu is null))
{
exportMenu.IsEnabled = enable;
Comment thread
edwin-vasquez-ucaldas marked this conversation as resolved.
Expand All @@ -449,7 +444,6 @@ private void DynamoViewModel_RequestEnableShortcutBarItems(bool enable)
{
shortcutBar.IsNewButtonEnabled = enable;
shortcutBar.IsOpenButtonEnabled = enable;
shortcutBar.IsSaveButtonEnabled = enable;
shortcutBar.IsLoginMenuEnabled = enable;
shortcutBar.IsExportMenuEnabled = enable;
shortcutBar.IsNotificationCenterEnabled = enable;
Expand All @@ -471,19 +465,12 @@ private void DynamoViewModel_RequestEnableShortcutBarItems(bool enable)

private void OnWorkspaceOpened(WorkspaceModel workspace)
{
if (!(saveThisButton is null))
{
saveThisButton.IsEnabled = true;
saveButton.IsEnabled = true;
}

if (!(exportMenu is null))
{
exportMenu.IsEnabled = true;
}
if (!(shortcutBar is null))
{
ShortcutBar.IsSaveButtonEnabled = true;
shortcutBar.IsExportMenuEnabled = true;
}

Expand Down
51 changes: 51 additions & 0 deletions test/DynamoCoreWpfTests/DynamoViewTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,57 @@ public void TestHomeWorkspaceClosedBeforeCustomNode()
Assert.IsTrue(View.saveButton.IsEnabled);
}

[Test]
public void WhenDynamoLaunchesThenSaveAsIsEnabledButSaveIsDisabledUntilDirty()
{
// Regression test for DYN-10717: Save/Save As enablement is driven by a shared CanExecute (menu, hotkey, toolbar all in sync), with "Save" additionally gated on the workspace's dirty flag.
Assert.IsFalse(ViewModel.ShowSaveDialogIfNeededAndSaveResultCommand.CanExecute(null));
Assert.IsTrue(ViewModel.ShowSaveDialogAndSaveResultCommand.CanExecute(null));

ViewModel.HomeSpace.HasUnsavedChanges = true;

Assert.IsTrue(ViewModel.ShowSaveDialogIfNeededAndSaveResultCommand.CanExecute(null));
}

[Test]
public void WhenLastWorkspaceIsClosedThenSaveMenuItemsAreDisabled()
{
// Regression test for DYN-10717: closing the only open workspace shows the Start Page, where Save/Save As are intentionally disabled (this is not the bug; the bug was menu/hotkey/toolbar disagreeing with each other).
var wasTestMode = DynamoModel.IsTestMode;
try
{
DynamoModel.IsTestMode = false;
ViewModel.CloseHomeWorkspaceCommand.Execute(null);
}
finally
{
DynamoModel.IsTestMode = wasTestMode;
}

Assert.IsTrue(ViewModel.ShowStartPage);
Assert.IsFalse(ViewModel.ShowSaveDialogIfNeededAndSaveResultCommand.CanExecute(null));
Assert.IsFalse(ViewModel.ShowSaveDialogAndSaveResultCommand.CanExecute(null));
}

[Test]
public void WhenGuidedTourIsActiveThenSaveMenuItemsAreDisabledUntilExit()
{
// Regression test for DYN-10717: Save/Save As must be disabled while a guided tour is active, and re-enabled once it ends.
ViewModel.HomeSpace.HasUnsavedChanges = true;
Assert.IsTrue(ViewModel.ShowSaveDialogIfNeededAndSaveResultCommand.CanExecute(null));
Assert.IsTrue(ViewModel.ShowSaveDialogAndSaveResultCommand.CanExecute(null));

ViewModel.SetGuidedTourActive(true);

Assert.IsFalse(ViewModel.ShowSaveDialogIfNeededAndSaveResultCommand.CanExecute(null));
Assert.IsFalse(ViewModel.ShowSaveDialogAndSaveResultCommand.CanExecute(null));

ViewModel.SetGuidedTourActive(false);

Assert.IsTrue(ViewModel.ShowSaveDialogIfNeededAndSaveResultCommand.CanExecute(null));
Assert.IsTrue(ViewModel.ShowSaveDialogAndSaveResultCommand.CanExecute(null));
}

[Test]
public void ElementBinding_SaveAs()
{
Expand Down
Loading