diff --git a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Events.cs b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Events.cs index 0df3a5c0f5ed..ab9fd900b21b 100644 --- a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Events.cs +++ b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Events.cs @@ -23,6 +23,9 @@ public sealed partial class RegistryPreviewMainPage : Page { private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + // Indicator if we loaded/reloaded/saved a file and need to skip TextChanged event one time. + private static bool editorContentChangedScripted; + /// /// Event that is will prevent the app from closing if the "save file" flag is active /// @@ -107,11 +110,17 @@ private async void OpenButton_Click(object sender, RoutedEventArgs e) { case ContentDialogResult.Primary: // Save, then continue the file open - SaveFile(); + if (!AskFileName(string.Empty) || + !SaveFile()) + { + return; + } + break; case ContentDialogResult.Secondary: // Don't save and continue the file open! saveButton.IsEnabled = false; + UpdateUnsavedFileIndicator(false); break; default: // Don't open the new file! @@ -138,11 +147,15 @@ private async void OpenButton_Click(object sender, RoutedEventArgs e) { // mute the TextChanged handler to make for clean UI MonacoEditor.TextChanged -= MonacoEditor_TextChanged; + editorContentChangedScripted = true; + + // update file name _appFileName = storageFile.Path; UpdateToolBarAndUI(await OpenRegistryFile(_appFileName)); // disable the Save button as it's a new file saveButton.IsEnabled = false; + UpdateUnsavedFileIndicator(false); // Restore the event handler as we're loaded MonacoEditor.TextChanged += MonacoEditor_TextChanged; @@ -154,7 +167,13 @@ private async void OpenButton_Click(object sender, RoutedEventArgs e) /// private void SaveButton_Click(object sender, RoutedEventArgs e) { - SaveFile(); + if (!AskFileName(string.Empty)) + { + return; + } + + // save and update window title + _ = SaveFile(); } /// @@ -162,23 +181,19 @@ private void SaveButton_Click(object sender, RoutedEventArgs e) /// private async void SaveAsButton_Click(object sender, RoutedEventArgs e) { - // Save out a new REG file and then open it - we have to use the direct Win32 method because FileOpenPicker crashes when it's - // called while running as admin - IntPtr windowHandle = WinRT.Interop.WindowNative.GetWindowHandle(_mainWindow); - string filename = SaveFilePicker.ShowDialog( - windowHandle, - resourceLoader.GetString("SuggestFileName"), - resourceLoader.GetString("FilterRegistryName") + '\0' + "*.reg" + '\0' + resourceLoader.GetString("FilterAllFiles") + '\0' + "*.*" + '\0' + '\0', - resourceLoader.GetString("SaveDialogTitle")); + // mute the TextChanged handler to make for clean UI + editorContentChangedScripted = true; + MonacoEditor.TextChanged -= MonacoEditor_TextChanged; - if (filename == string.Empty) + if (!AskFileName(_appFileName) || !SaveFile()) { return; } - _appFileName = filename; - SaveFile(); UpdateToolBarAndUI(await OpenRegistryFile(_appFileName)); + + // restore the TextChanged handler + MonacoEditor.TextChanged += MonacoEditor_TextChanged; } /// @@ -187,12 +202,14 @@ private async void SaveAsButton_Click(object sender, RoutedEventArgs e) private async void RefreshButton_Click(object sender, RoutedEventArgs e) { // mute the TextChanged handler to make for clean UI + editorContentChangedScripted = true; MonacoEditor.TextChanged -= MonacoEditor_TextChanged; // reload the current Registry file and update the toolbar accordingly. UpdateToolBarAndUI(await OpenRegistryFile(_appFileName), true, true); saveButton.IsEnabled = false; + UpdateUnsavedFileIndicator(false); // restore the TextChanged handler MonacoEditor.TextChanged += MonacoEditor_TextChanged; @@ -257,7 +274,12 @@ private async void WriteButton_Click(object sender, RoutedEventArgs e) { case ContentDialogResult.Primary: // Save, then continue the file open - SaveFile(); + if (!AskFileName(string.Empty) || + !SaveFile()) + { + return; + } + break; case ContentDialogResult.Secondary: // Don't save and continue the file open! @@ -354,7 +376,13 @@ private void MonacoEditor_TextChanged(object sender, EventArgs e) _dispatcherQueue.TryEnqueue(() => { RefreshRegistryFile(); - saveButton.IsEnabled = true; + if (!editorContentChangedScripted) + { + saveButton.IsEnabled = true; + UpdateUnsavedFileIndicator(true); + } + + editorContentChangedScripted = false; }); } } diff --git a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Utilities.cs b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Utilities.cs index b8fc6e4e1d9a..dfce7f88398c 100644 --- a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Utilities.cs +++ b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Utilities.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Reflection; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.UI.Input; @@ -22,6 +23,9 @@ namespace RegistryPreviewUILib { public sealed partial class RegistryPreviewMainPage : Page { + private static readonly string _unsavedFileIndicator = "* "; + private static readonly char[] _unsavedFileIndicatorChars = [' ', '*']; + private static SemaphoreSlim _dialogSemaphore = new(1); private string lastKeyPath; @@ -830,10 +834,16 @@ private async void HandleDirtyClosing(string title, string content, string prima { case ContentDialogResult.Primary: // Save, then close - SaveFile(); + if (!AskFileName(string.Empty) || + !SaveFile()) + { + return; + } + break; case ContentDialogResult.Secondary: // Don't save, and then close! + UpdateUnsavedFileIndicator(false); saveButton.IsEnabled = false; break; default: @@ -902,11 +912,63 @@ public void ChangeCursor(UIElement uiElement, bool wait) type.InvokeMember("ProtectedCursor", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.SetProperty | BindingFlags.Instance, null, uiElement, new object[] { cursor }, CultureInfo.InvariantCulture); } + public void UpdateUnsavedFileIndicator(bool show) + { + // get and cut current title + string currentTitle = Regex.Replace(_mainWindow.Title, APPNAME + @"$|\s-\s" + APPNAME + @"$", string.Empty); + + // verify + bool titleContainsIndicator = currentTitle.StartsWith(_unsavedFileIndicator, StringComparison.CurrentCultureIgnoreCase); + + // update + if (!titleContainsIndicator && show) + { + _updateWindowTitleFunction(_unsavedFileIndicator + currentTitle); + } + else if (titleContainsIndicator && !show) + { + _updateWindowTitleFunction(currentTitle.TrimStart(_unsavedFileIndicatorChars)); + } + } + + /// + /// Ask the user for the file path if it is unknown because of an unsaved file + /// + /// If not empty always ask for a file path and use the value as name. + /// Returns true if user selected a path, otherwise false + public bool AskFileName(string fileName) + { + if (string.IsNullOrEmpty(_appFileName) || !string.IsNullOrEmpty(fileName) ) + { + string fName = string.IsNullOrEmpty(fileName) ? resourceLoader.GetString("SuggestFileName") : fileName; + + // Save out a new REG file and then open it - we have to use the direct Win32 method because FileOpenPicker crashes when it's + // called while running as admin + IntPtr windowHandle = WinRT.Interop.WindowNative.GetWindowHandle(_mainWindow); + string filename = SaveFilePicker.ShowDialog( + windowHandle, + fName, + resourceLoader.GetString("FilterRegistryName") + '\0' + "*.reg" + '\0' + resourceLoader.GetString("FilterAllFiles") + '\0' + "*.*" + '\0' + '\0', + resourceLoader.GetString("SaveDialogTitle")); + + if (filename == string.Empty) + { + return false; + } + + _appFileName = filename; + } + + return true; + } + /// /// Wrapper method that saves the current file in place, using the current text in editor. /// - private void SaveFile() + private bool SaveFile() { + bool saveSuccess = true; + ChangeCursor(gridPreview, true); // set up the FileStream for all writing @@ -930,10 +992,13 @@ private void SaveFile() streamWriter.Close(); // only change when the save is successful + _updateWindowTitleFunction(_appFileName); saveButton.IsEnabled = false; } catch (UnauthorizedAccessException ex) { + saveSuccess = false; + // this exception is thrown if the file is there but marked as read only ShowMessageBox( resourceLoader.GetString("ErrorDialogTitle"), @@ -942,6 +1007,8 @@ private void SaveFile() } catch { + saveSuccess = false; + // this catch handles all other exceptions thrown when trying to write the file out ShowMessageBox( resourceLoader.GetString("ErrorDialogTitle"), @@ -959,6 +1026,8 @@ private void SaveFile() // restore the cursor ChangeCursor(gridPreview, false); + + return saveSuccess; } ///