From ffbc1812e6b638e91cc14ebecd4aa633f029d817 Mon Sep 17 00:00:00 2001 From: "Aaron (Qilong)" <173288704@qq.com> Date: Sat, 27 Jan 2024 08:58:11 -0500 Subject: [PATCH] Open Dynamo Template as new workspace (#14871) * Open Template UI change * Add more functions for template opening * add comments * reuse open function * Open Template with FileName as empty * Give user notification * Add message confirmation * Unit test * Unit test * comments * comments * reuse ShowOpenDialogAndOpenResult code * clean up --- src/DynamoCore/Configuration/IPathResolver.cs | 5 ++ src/DynamoCore/Configuration/PathManager.cs | 57 ++++++++++++++++++- src/DynamoCore/Models/DynamoModel.cs | 35 ++++++++++-- src/DynamoCore/Models/DynamoModelCommands.cs | 8 +++ src/DynamoCore/Models/RecordableCommands.cs | 16 +++++- .../Properties/Resources.Designer.cs | 36 ++++++++++++ .../Properties/Resources.en-US.resx | 14 ++++- src/DynamoCoreWpf/Properties/Resources.resx | 12 ++++ .../ViewModels/Core/DynamoViewModel.cs | 57 +++++++++++++++---- .../Core/DynamoViewModelDelegateCommands.cs | 2 + src/DynamoCoreWpf/Views/Core/DynamoView.xaml | 18 +++++- .../Views/Core/DynamoView.xaml.cs | 2 +- .../PreferencesViewModelTests.cs | 8 ++- test/DynamoCoreWpfTests/WorkspaceSaving.cs | 21 +++++++ 14 files changed, 264 insertions(+), 27 deletions(-) diff --git a/src/DynamoCore/Configuration/IPathResolver.cs b/src/DynamoCore/Configuration/IPathResolver.cs index 5273787238a..a2d7b854bbd 100644 --- a/src/DynamoCore/Configuration/IPathResolver.cs +++ b/src/DynamoCore/Configuration/IPathResolver.cs @@ -156,6 +156,11 @@ public interface IPathManager /// string SamplesDirectory { get; } + /// + /// The root directory where all template files are stored + /// + string TemplatesDirectory { get; } + /// /// The directory where the automatically saved files will be stored. /// diff --git a/src/DynamoCore/Configuration/PathManager.cs b/src/DynamoCore/Configuration/PathManager.cs index 6a794e8ab04..1e3649483b1 100644 --- a/src/DynamoCore/Configuration/PathManager.cs +++ b/src/DynamoCore/Configuration/PathManager.cs @@ -66,6 +66,7 @@ internal static Lazy public const string ViewExtensionsDirectoryName = "viewExtensions"; public const string DefinitionsDirectoryName = "definitions"; public const string SamplesDirectoryName = "samples"; + public const string TemplateDirectoryName = "templates"; public const string BackupDirectoryName = "backup"; public const string PreferenceSettingsFileName = "DynamoSettings.xml"; public const string PythonTemplateFileName = "PythonTemplate.py"; @@ -82,6 +83,7 @@ internal static Lazy private string commonPackages; private string logDirectory; private string samplesDirectory; + private string templatesDirectory; private string backupDirectory; private string defaultBackupDirectory; private string preferenceFilePath; @@ -240,6 +242,14 @@ public string SamplesDirectory get { return samplesDirectory; } } + /// + /// Dynamo Templates folder + /// + public string TemplatesDirectory + { + get { return templatesDirectory; } + } + public string BackupDirectory { get { return backupDirectory; } @@ -572,6 +582,7 @@ private void BuildCommonDirectories() commonDefinitions = Path.Combine(commonDataDir, DefinitionsDirectoryName); commonPackages = Path.Combine(commonDataDir, PackagesDirectoryName); samplesDirectory = GetSamplesFolder(commonDataDir); + templatesDirectory = GetTemplateFolder(commonDataDir); rootDirectories = new List { userDataDir }; @@ -715,7 +726,51 @@ private static string GetSamplesFolder(string dataRootDirectory) return sampleDirectory; } - + + /// + /// Get template folder path from common data directory + /// + /// + /// + private string GetTemplateFolder(string dataRootDirectory) + { + var versionedDirectory = dataRootDirectory; + if (!Directory.Exists(versionedDirectory)) + { + // Try to see if folder "%ProgramData%\{...}\{major}.{minor}" exists, if it + // does not, then root directory would be "%ProgramData%\{...}". + // + dataRootDirectory = Directory.GetParent(versionedDirectory).FullName; + } + else if (!Directory.Exists(Path.Combine(versionedDirectory, TemplateDirectoryName))) + { + // If the folder "%ProgramData%\{...}\{major}.{minor}" exists, then try to see + // if the folder "%ProgramData%\{...}\{major}.{minor}\templates" exists. If it + // doesn't exist, then root directory would be "%ProgramData%\{...}". + // + dataRootDirectory = Directory.GetParent(versionedDirectory).FullName; + } + + var uiCulture = CultureInfo.CurrentUICulture.Name; + var templateDirectory = Path.Combine(dataRootDirectory, TemplateDirectoryName, uiCulture); + + // If the localized template directory does not exist then fall back + // to using the en-US template folder. Do an additional check to see + // if the localized folder is available but is empty. + // + var di = new DirectoryInfo(templateDirectory); + if (!Directory.Exists(templateDirectory) || + !di.GetDirectories().Any() || + !di.GetFiles("*.dyn", SearchOption.AllDirectories).Any()) + { + var neturalCommonTemplates = Path.Combine(dataRootDirectory, TemplateDirectoryName, "en-US"); + if (Directory.Exists(neturalCommonTemplates)) + templateDirectory = neturalCommonTemplates; + } + + return templateDirectory; + } + private IEnumerable LibrarySearchPaths(string library) { // Strip out possible directory from library path. diff --git a/src/DynamoCore/Models/DynamoModel.cs b/src/DynamoCore/Models/DynamoModel.cs index ced6d3402f5..46293ee6158 100644 --- a/src/DynamoCore/Models/DynamoModel.cs +++ b/src/DynamoCore/Models/DynamoModel.cs @@ -1957,6 +1957,29 @@ public void OpenFileFromPath(string filePath, bool forceManualExecutionMode = fa } } + /// + /// Opens a Dynamo workspace from a path to a template on disk. + /// + /// Path to file + /// Set this to true to discard + /// execution mode specified in the file and set manual mode + public void OpenTemplateFromPath(string filePath, bool forceManualExecutionMode = false) + { + + if (DynamoUtilities.PathHelper.isValidJson(filePath, out string fileContents, out Exception ex)) + { + OpenJsonFileFromPath(fileContents, filePath, forceManualExecutionMode, true); + } + else + { + // These kind of exceptions indicate that file is not accessible + if (ex is IOException || ex is UnauthorizedAccessException || ex is JsonReaderException) + { + throw ex; + } + } + } + /// /// Inserts a Dynamo graph or Custom Node inside the current workspace from a file path /// @@ -2028,8 +2051,9 @@ static private DynamoPreferencesData DynamoPreferencesDataFromJson(string json) /// Path to file /// Set this to true to discard /// execution mode specified in the file and set manual mode + /// Set this to true to indicate that the file is a template /// True if workspace was opened successfully - private bool OpenJsonFileFromPath(string fileContents, string filePath, bool forceManualExecutionMode) + private bool OpenJsonFileFromPath(string fileContents, string filePath, bool forceManualExecutionMode, bool isTemplate = false) { try { @@ -2040,7 +2064,7 @@ private bool OpenJsonFileFromPath(string fileContents, string filePath, bool for if (true) //MigrationManager.ProcessWorkspace(dynamoPreferences.Version, xmlDoc, IsTestMode, NodeFactory)) { WorkspaceModel ws; - if (OpenJsonFile(filePath, fileContents, dynamoPreferences, forceManualExecutionMode, out ws)) + if (OpenJsonFile(filePath, fileContents, dynamoPreferences, forceManualExecutionMode, isTemplate, out ws)) { OpenWorkspace(ws); //Raise an event to deserialize the view parameters before @@ -2076,7 +2100,7 @@ private bool InsertJsonFileFromPath(string fileContents, string filePath, bool f { if (true) //MigrationManager.ProcessWorkspace(dynamoPreferences.Version, xmlDoc, IsTestMode, NodeFactory)) { - if (OpenJsonFile(filePath, fileContents, dynamoPreferences, forceManualExecutionMode, out WorkspaceModel ws)) + if (OpenJsonFile(filePath, fileContents, dynamoPreferences, forceManualExecutionMode, false, out WorkspaceModel ws)) { ExtraWorkspaceViewInfo viewInfo = ExtraWorkspaceViewInfo.ExtraWorkspaceViewInfoFromJson(fileContents); @@ -2268,6 +2292,7 @@ private bool OpenJsonFile( string fileContents, DynamoPreferencesData dynamoPreferences, bool forceManualExecutionMode, + bool isTemplate, out WorkspaceModel workspace) { if (!string.IsNullOrEmpty(filePath)) @@ -2295,8 +2320,8 @@ private bool OpenJsonFile( CustomNodeManager, this.LinterManager); - workspace.FileName = string.IsNullOrEmpty(filePath) ? "" : filePath; - workspace.FromJsonGraphId = string.IsNullOrEmpty(filePath) ? WorkspaceModel.ComputeGraphIdFromJson(fileContents) : ""; + workspace.FileName = string.IsNullOrEmpty(filePath) || isTemplate? string.Empty : filePath; + workspace.FromJsonGraphId = string.IsNullOrEmpty(filePath) ? WorkspaceModel.ComputeGraphIdFromJson(fileContents) : string.Empty; workspace.ScaleFactor = dynamoPreferences.ScaleFactor; if (!IsTestMode && !IsHeadless) diff --git a/src/DynamoCore/Models/DynamoModelCommands.cs b/src/DynamoCore/Models/DynamoModelCommands.cs index b2259ab6ca7..eae0f04c19a 100644 --- a/src/DynamoCore/Models/DynamoModelCommands.cs +++ b/src/DynamoCore/Models/DynamoModelCommands.cs @@ -46,12 +46,20 @@ protected virtual void OpenFileImpl(OpenFileCommand command) { string filePath = command.FilePath; bool forceManualMode = command.ForceManualExecutionMode; + bool isTemplate = command.IsTemplate; OpenFileFromPath(filePath, forceManualMode); //clear the clipboard to avoid copying between dyns //ClipBoard.Clear(); } + protected virtual void OpenTemplateImpl(OpenFileCommand command) + { + string filePath = command.FilePath; + bool forceManualMode = command.ForceManualExecutionMode; + OpenTemplateFromPath(filePath, forceManualMode); + } + protected virtual void OpenFileFromJsonImpl(OpenFileFromJsonCommand command) { string fileContents = command.FileContents; diff --git a/src/DynamoCore/Models/RecordableCommands.cs b/src/DynamoCore/Models/RecordableCommands.cs index 44355e6cf15..af0232dd2e4 100644 --- a/src/DynamoCore/Models/RecordableCommands.cs +++ b/src/DynamoCore/Models/RecordableCommands.cs @@ -458,14 +458,16 @@ public class OpenFileCommand : RecordableCommand #region Public Class Methods /// - /// + /// Constructor /// /// The path to the file. /// Should the file be opened in manual execution mode? - public OpenFileCommand(string filePath, bool forceManualExecutionMode = false) + /// Is Dynamo opening a template file? + public OpenFileCommand(string filePath, bool forceManualExecutionMode = false, bool isTemplate = false) { FilePath = filePath; ForceManualExecutionMode = forceManualExecutionMode; + IsTemplate = isTemplate; } private static string TryFindFile(string xmlFilePath, string uriString = null) @@ -507,6 +509,7 @@ internal static OpenFileCommand DeserializeCore(XmlElement element) [DataMember] internal string FilePath { get; private set; } internal bool ForceManualExecutionMode { get; private set; } + internal bool IsTemplate { get; private set; } private DynamoModel dynamoModel; #endregion @@ -516,7 +519,14 @@ internal static OpenFileCommand DeserializeCore(XmlElement element) protected override void ExecuteCore(DynamoModel dynamoModel) { this.dynamoModel = dynamoModel; - dynamoModel.OpenFileImpl(this); + if (IsTemplate) + { + dynamoModel.OpenTemplateImpl(this); + } + else + { + dynamoModel.OpenFileImpl(this); + } } protected override void SerializeCore(XmlElement element) diff --git a/src/DynamoCoreWpf/Properties/Resources.Designer.cs b/src/DynamoCoreWpf/Properties/Resources.Designer.cs index b0c93817cad..4a2d17e80b2 100644 --- a/src/DynamoCoreWpf/Properties/Resources.Designer.cs +++ b/src/DynamoCoreWpf/Properties/Resources.Designer.cs @@ -1801,6 +1801,24 @@ public static string DynamoViewFileMenuOpen { } } + /// + /// Looks up a localized string similar to _File. + /// + public static string DynamoViewFileMenuOpenFile { + get { + return ResourceManager.GetString("DynamoViewFileMenuOpenFile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to _Template. + /// + public static string DynamoViewFileMenuOpenTemplate { + get { + return ResourceManager.GetString("DynamoViewFileMenuOpenTemplate", resourceCulture); + } + } + /// /// Looks up a localized string similar to Open _Recent Files. /// @@ -10143,6 +10161,24 @@ public static string WebView2RequiredTitle { } } + /// + /// Looks up a localized string similar to Workspaces cannot be saved to the Templates folder. Please choose a different folder to save your file.. + /// + public static string WorkspaceSaveTemplateDirectoryBlockMsg { + get { + return ResourceManager.GetString("WorkspaceSaveTemplateDirectoryBlockMsg", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid Save Path. + /// + public static string WorkspaceSaveTemplateDirectoryBlockTitle { + get { + return ResourceManager.GetString("WorkspaceSaveTemplateDirectoryBlockTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to You haven't saved this file yet.. /// diff --git a/src/DynamoCoreWpf/Properties/Resources.en-US.resx b/src/DynamoCoreWpf/Properties/Resources.en-US.resx index f1c98329dca..02c8c5c4e8a 100644 --- a/src/DynamoCoreWpf/Properties/Resources.en-US.resx +++ b/src/DynamoCoreWpf/Properties/Resources.en-US.resx @@ -3900,4 +3900,16 @@ In certain complex graphs or host program scenarios, Automatic mode may cause in Your changes will be lost if you proceed. - + + _File + + + _Template + + + Workspaces cannot be saved to the Templates folder. Please choose a different folder to save your file. + + + Invalid Save Path + + \ No newline at end of file diff --git a/src/DynamoCoreWpf/Properties/Resources.resx b/src/DynamoCoreWpf/Properties/Resources.resx index 2192e865b97..7179ad5159b 100644 --- a/src/DynamoCoreWpf/Properties/Resources.resx +++ b/src/DynamoCoreWpf/Properties/Resources.resx @@ -3887,4 +3887,16 @@ In certain complex graphs or host program scenarios, Automatic mode may cause in Your changes will be lost if you proceed. + + _File + + + _Template + + + Workspaces cannot be saved to the Templates folder. Please choose a different folder to save your file. + + + Invalid Save Path + \ No newline at end of file diff --git a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs index 0f0b706213a..a66dde38597 100644 --- a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs @@ -1686,8 +1686,9 @@ private void OpenFromJson(object parameters) /// Open a definition or workspace. /// For most cases, parameters variable refers to the file path to open /// However, when this command is used in OpenFileDialog, the variable is - /// a Tuple{string, bool} instead. The boolean flag is used to override the - /// RunSetting of the workspace. + /// a Tuple{string, bool} instead. When this command is used in OpenTemplateDialog, + /// the variable is a Tuple{string, bool} instead. The second boolean flag is + /// used to override the RunSetting of the workspace. /// /// private void Open(object parameters) @@ -1696,7 +1697,8 @@ private void Open(object parameters) // that can't be handled reliably filePath = string.Empty; fileContents = string.Empty; - bool forceManualMode = false; + bool forceManualMode = false; + bool isTemplate = false; try { if (parameters is Tuple packedParams) @@ -1704,6 +1706,12 @@ private void Open(object parameters) filePath = packedParams.Item1; forceManualMode = packedParams.Item2; } + else if (parameters is Tuple tupleParams) + { + filePath = tupleParams.Item1; + forceManualMode = tupleParams.Item2; + isTemplate = tupleParams.Item3; + } else { filePath = parameters as string; @@ -1719,7 +1727,7 @@ private void Open(object parameters) && FileTrustViewModel != null; RunSettings.ForceBlockRun = displayTrustWarning; // Execute graph open command - ExecuteCommand(new DynamoModel.OpenFileCommand(filePath, forceManualMode)); + ExecuteCommand(new DynamoModel.OpenFileCommand(filePath, forceManualMode, isTemplate)); // Only show trust warning popop when current opened workspace is homeworkspace and not custom node workspace if (displayTrustWarning && (currentWorkspaceViewModel?.IsHomeSpace ?? false)) { @@ -1920,6 +1928,8 @@ private void ShowOpenDialogAndOpenResult(object parameter) return; } + bool isTemplate = (parameter as string).Equals("Template"); + DynamoOpenFileDialog _fileDialog = new DynamoOpenFileDialog(this) { Filter = string.Format(Resources.FileDialogDynamoDefinitions, @@ -1928,8 +1938,18 @@ private void ShowOpenDialogAndOpenResult(object parameter) Title = string.Format(Resources.OpenDynamoDefinitionDialogTitle,BrandingResourceProvider.ProductName) }; - // if you've got the current space path, use it as the inital dir - if (!string.IsNullOrEmpty(Model.CurrentWorkspace.FileName)) + // If opening a template, use templates dir as the initial dir + if (isTemplate && !string.IsNullOrEmpty(Model.PathManager.TemplatesDirectory)) + { + string path = Model.PathManager.TemplatesDirectory; + if (Directory.Exists(path)) + { + var di = new DirectoryInfo(Model.PathManager.TemplatesDirectory); + _fileDialog.InitialDirectory = di.FullName; + } + } + // otherwise, if you've got the current space path, use it as the initial dir + else if (!string.IsNullOrEmpty(Model.CurrentWorkspace.FileName)) { string path = Model.CurrentWorkspace.FileName; if (File.Exists(path)) @@ -1952,19 +1972,26 @@ private void ShowOpenDialogAndOpenResult(object parameter) if (Directory.Exists(path)) _fileDialog.InitialDirectory = path; } - + if (_fileDialog.ShowDialog() == DialogResult.OK) { if (CanOpen(_fileDialog.FileName)) { - Open(new Tuple(_fileDialog.FileName, _fileDialog.RunManualMode)); + if (isTemplate) + { + // File opening API which does not modify the original template file + Open(new Tuple(_fileDialog.FileName, _fileDialog.RunManualMode, true)); + } + else + { + Open(new Tuple(_fileDialog.FileName, _fileDialog.RunManualMode)); + } } } } private bool CanShowOpenDialogAndOpenResultCommand(object parameter) => CanRunGraph; - /// /// Present the open dialog and open the workspace that is selected. /// @@ -2118,7 +2145,17 @@ private void InternalSaveAs(string path, SaveContext saveContext, bool isBackup try { Model.Logger.Log(string.Format(Properties.Resources.SavingInProgress, path)); - var hasSaved = CurrentSpaceViewModel.Save(path, isBackup, Model.EngineController, saveContext); + var hasSaved = false; + if (path.Contains(Model.PathManager.TemplatesDirectory)) + { + // Give user notifications + DynamoMessageBox.Show(WpfResources.WorkspaceSaveTemplateDirectoryBlockMsg, WpfResources.WorkspaceSaveTemplateDirectoryBlockTitle, + MessageBoxButton.OK, MessageBoxImage.Warning); + } + else + { + hasSaved = CurrentSpaceViewModel.Save(path, isBackup, Model.EngineController, saveContext); + } if (!isBackup && hasSaved) { diff --git a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModelDelegateCommands.cs b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModelDelegateCommands.cs index b8b4a4d1e6b..56faceb04df 100644 --- a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModelDelegateCommands.cs +++ b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModelDelegateCommands.cs @@ -18,6 +18,7 @@ private void InitializeDelegateCommands() SaveCommand = new DelegateCommand(Save, CanSave); SaveAsCommand = new DelegateCommand(SaveAs, CanSaveAs); ShowOpenDialogAndOpenResultCommand = new DelegateCommand(ShowOpenDialogAndOpenResult, CanShowOpenDialogAndOpenResultCommand); + ShowOpenTemplateDialogCommand = new DelegateCommand(ShowOpenDialogAndOpenResult, CanShowOpenDialogAndOpenResultCommand); ShowInsertDialogAndInsertResultCommand = new DelegateCommand(ShowInsertDialogAndInsertResult, CanShowInsertDialogAndInsertResultCommand); ShowSaveDialogAndSaveResultCommand = new DelegateCommand(ShowSaveDialogAndSaveResult, CanShowSaveDialogAndSaveResult); ShowSaveDialogIfNeededAndSaveResultCommand = new DelegateCommand(ShowSaveDialogIfNeededAndSaveResult, CanShowSaveDialogIfNeededAndSaveResultCommand); @@ -98,6 +99,7 @@ private void InitializeDelegateCommands() public DelegateCommand OpenIfSavedCommand { get; set; } public DelegateCommand OpenCommand { get; set; } public DelegateCommand ShowOpenDialogAndOpenResultCommand { get; set; } + public DelegateCommand ShowOpenTemplateDialogCommand { get; set; } public DelegateCommand ShowInsertDialogAndInsertResultCommand { get; set; } public DelegateCommand WriteToLogCmd { get; set; } public DelegateCommand PostUiActivationCommand { get; set; } diff --git a/src/DynamoCoreWpf/Views/Core/DynamoView.xaml b/src/DynamoCoreWpf/Views/Core/DynamoView.xaml index c7611fce55b..889c07a2ca8 100644 --- a/src/DynamoCoreWpf/Views/Core/DynamoView.xaml +++ b/src/DynamoCoreWpf/Views/Core/DynamoView.xaml @@ -112,6 +112,9 @@ + @@ -334,9 +337,18 @@ + Name="openButton"> + + + +