diff --git a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Events.cs b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Events.cs
index c8f72837f1..b2bf8ea277 100644
--- a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Events.cs
+++ b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Events.cs
@@ -22,6 +22,10 @@ namespace RegistryPreviewUILib
{
private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
+ // Indicator if we loaded/reloaded/saved a file and need to skip TextChanged event one time.
+ // (Solves the problem that enabling the event handler fires it one time.)
+ private static bool editorContentChangedScripted;
+
///
/// Event that is will prevent the app from closing if the "save file" flag is active
///
@@ -76,6 +80,67 @@ namespace RegistryPreviewUILib
MonacoEditor.Focus(FocusState.Programmatic);
}
+ ///
+ /// New button action: Ask to save last changes and reset editor content to reg header only
+ ///
+ private async void NewButton_Click(object sender, RoutedEventArgs e)
+ {
+ // Check to see if the current file has been saved
+ if (saveButton.IsEnabled)
+ {
+ ContentDialog contentDialog = new ContentDialog()
+ {
+ Title = resourceLoader.GetString("YesNoCancelDialogTitle"),
+ Content = resourceLoader.GetString("YesNoCancelDialogContent"),
+ PrimaryButtonText = resourceLoader.GetString("YesNoCancelDialogPrimaryButtonText"),
+ SecondaryButtonText = resourceLoader.GetString("YesNoCancelDialogSecondaryButtonText"),
+ CloseButtonText = resourceLoader.GetString("YesNoCancelDialogCloseButtonText"),
+ DefaultButton = ContentDialogButton.Primary,
+ };
+
+ // Use this code to associate the dialog to the appropriate AppWindow by setting
+ // the dialog's XamlRoot to the same XamlRoot as an element that is already present in the AppWindow.
+ if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8))
+ {
+ contentDialog.XamlRoot = this.Content.XamlRoot;
+ }
+
+ ContentDialogResult contentDialogResult = await contentDialog.ShowAsync();
+ switch (contentDialogResult)
+ {
+ case ContentDialogResult.Primary:
+ // Save, then continue the new action
+ if (!AskFileName(string.Empty) ||
+ !SaveFile())
+ {
+ return;
+ }
+
+ break;
+ case ContentDialogResult.Secondary:
+ // Don't save and continue the new action!
+ break;
+ default:
+ // Don't open the new action!
+ return;
+ }
+ }
+
+ // mute the TextChanged handler to make for clean UI
+ MonacoEditor.TextChanged -= MonacoEditor_TextChanged;
+
+ // reset editor, file info and ui.
+ _appFileName = string.Empty;
+ ResetEditorAndFile();
+
+ // disable buttons that do not make sense
+ UpdateUnsavedFileState(false);
+ refreshButton.IsEnabled = false;
+
+ // restore the TextChanged handler
+ ButtonAction_RestoreTextChangedEvent();
+ }
+
///
/// Uses a picker to select a new file to open
///
@@ -106,11 +171,15 @@ namespace RegistryPreviewUILib
{
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;
break;
default:
// Don't open the new file!
@@ -137,14 +206,16 @@ namespace RegistryPreviewUILib
{
// mute the TextChanged handler to make for clean UI
MonacoEditor.TextChanged -= MonacoEditor_TextChanged;
+
+ // update file name
_appFileName = storageFile.Path;
UpdateToolBarAndUI(await OpenRegistryFile(_appFileName));
// disable the Save button as it's a new file
- saveButton.IsEnabled = false;
+ UpdateUnsavedFileState(false);
// Restore the event handler as we're loaded
- MonacoEditor.TextChanged += MonacoEditor_TextChanged;
+ ButtonAction_RestoreTextChangedEvent();
}
}
@@ -153,7 +224,14 @@ namespace RegistryPreviewUILib
///
private void SaveButton_Click(object sender, RoutedEventArgs e)
{
- SaveFile();
+ if (!AskFileName(string.Empty))
+ {
+ return;
+ }
+
+ // save and update window title
+ // error handling and ui update happens in SaveFile() method
+ _ = SaveFile();
}
///
@@ -161,47 +239,24 @@ namespace RegistryPreviewUILib
///
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
+ MonacoEditor.TextChanged -= MonacoEditor_TextChanged;
- if (filename == string.Empty)
+ if (!AskFileName(_appFileName) || !SaveFile())
{
return;
}
- _appFileName = filename;
- SaveFile();
UpdateToolBarAndUI(await OpenRegistryFile(_appFileName));
+
+ // restore the TextChanged handler
+ ButtonAction_RestoreTextChangedEvent();
}
///
/// Reloads the current REG file from storage
///
private async void RefreshButton_Click(object sender, RoutedEventArgs e)
- {
- // mute the TextChanged handler to make for clean UI
- MonacoEditor.TextChanged -= MonacoEditor_TextChanged;
-
- // reload the current Registry file and update the toolbar accordingly.
- UpdateToolBarAndUI(await OpenRegistryFile(_appFileName), true, true);
-
- // disable the Save button as it's a new file
- saveButton.IsEnabled = false;
-
- // restore the TextChanged handler
- MonacoEditor.TextChanged += MonacoEditor_TextChanged;
- }
-
- ///
- /// Resets the editor content
- ///
- private async void NewButton_Click(object sender, RoutedEventArgs e)
{
// Check to see if the current file has been saved
if (saveButton.IsEnabled)
@@ -209,10 +264,9 @@ namespace RegistryPreviewUILib
ContentDialog contentDialog = new ContentDialog()
{
Title = resourceLoader.GetString("YesNoCancelDialogTitle"),
- Content = resourceLoader.GetString("YesNoCancelDialogContent"),
- PrimaryButtonText = resourceLoader.GetString("YesNoCancelDialogPrimaryButtonText"),
- SecondaryButtonText = resourceLoader.GetString("YesNoCancelDialogSecondaryButtonText"),
- CloseButtonText = resourceLoader.GetString("YesNoCancelDialogCloseButtonText"),
+ Content = resourceLoader.GetString("ReloadDialogContent"),
+ PrimaryButtonText = resourceLoader.GetString("ReloadDialogPrimaryButtonText"),
+ CloseButtonText = resourceLoader.GetString("ReloadDialogCloseButtonText"),
DefaultButton = ContentDialogButton.Primary,
};
@@ -227,15 +281,10 @@ namespace RegistryPreviewUILib
switch (contentDialogResult)
{
case ContentDialogResult.Primary:
- // Save, then continue the file open
- SaveFile();
- break;
- case ContentDialogResult.Secondary:
- // Don't save and continue the file open!
- saveButton.IsEnabled = false;
+ // Don't save and continue the reload action!
break;
default:
- // Don't open the new file!
+ // Don't continue the reload action!
return;
}
}
@@ -243,16 +292,14 @@ namespace RegistryPreviewUILib
// mute the TextChanged handler to make for clean UI
MonacoEditor.TextChanged -= MonacoEditor_TextChanged;
- // reset editor, file info and ui.
- _appFileName = string.Empty;
- ResetEditorAndFile();
+ // reload the current Registry file and update the toolbar accordingly.
+ UpdateToolBarAndUI(await OpenRegistryFile(_appFileName), true, true);
+
+ // disable the Save button as it's a new file
+ UpdateUnsavedFileState(false);
// restore the TextChanged handler
- MonacoEditor.TextChanged += MonacoEditor_TextChanged;
-
- // disable buttons that do not make sense
- saveButton.IsEnabled = false;
- refreshButton.IsEnabled = false;
+ ButtonAction_RestoreTextChangedEvent();
}
///
@@ -313,15 +360,20 @@ namespace RegistryPreviewUILib
switch (contentDialogResult)
{
case ContentDialogResult.Primary:
- // Save, then continue the file open
- SaveFile();
+ // Save, then continue the merge action
+ if (!AskFileName(string.Empty) ||
+ !SaveFile())
+ {
+ return;
+ }
+
break;
case ContentDialogResult.Secondary:
- // Don't save and continue the file open!
- saveButton.IsEnabled = false;
+ // Don't save and continue the merge action!
+ UpdateUnsavedFileState(false);
break;
default:
- // Don't open the new file!
+ // Don't merge the file!
return;
}
}
@@ -411,10 +463,29 @@ namespace RegistryPreviewUILib
_dispatcherQueue.TryEnqueue(() =>
{
RefreshRegistryFile();
- saveButton.IsEnabled = true;
+ if (!editorContentChangedScripted)
+ {
+ UpdateUnsavedFileState(true);
+ }
+
+ editorContentChangedScripted = false;
});
}
+ ///
+ /// Sets indicator for programatic text change and adds text changed handler
+ ///
+ ///
+ /// Use this always, if button actions temporary disable the text changed event
+ ///
+ private void ButtonAction_RestoreTextChangedEvent()
+ {
+ // Solves the problem that enabling the event handler fires it one time.
+ // These one time fired event would causes wrong unsaved changes state.
+ editorContentChangedScripted = true;
+ MonacoEditor.TextChanged += MonacoEditor_TextChanged;
+ }
+
// Commands to show data preview
public void ButtonExtendedPreview_Click(object sender, RoutedEventArgs e)
{
diff --git a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Utilities.cs b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Utilities.cs
index 57d27fdc12..2211113f2c 100644
--- a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Utilities.cs
+++ b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Utilities.cs
@@ -11,6 +11,7 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
+using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.UI.Input;
@@ -24,7 +25,10 @@ namespace RegistryPreviewUILib
{
private const string NEWFILEHEADER = "Windows Registry Editor Version 5.00\r\n\r\n";
+ private static readonly string _unsavedFileIndicator = "* ";
+ private static readonly char[] _unsavedFileIndicatorChars = [' ', '*'];
private static SemaphoreSlim _dialogSemaphore = new(1);
+
private string lastKeyPath;
public delegate void UpdateWindowTitleFunction(string title);
@@ -832,42 +836,66 @@ namespace RegistryPreviewUILib
///
private async void HandleDirtyClosing(string title, string content, string primaryButtonText, string secondaryButtonText, string closeButtonText)
{
- ContentDialog contentDialog = new ContentDialog()
+ if (_dialogSemaphore.CurrentCount == 0)
{
- Title = title,
- Content = content,
- PrimaryButtonText = primaryButtonText,
- SecondaryButtonText = secondaryButtonText,
- CloseButtonText = closeButtonText,
- DefaultButton = ContentDialogButton.Primary,
- };
-
- // Use this code to associate the dialog to the appropriate AppWindow by setting
- // the dialog's XamlRoot to the same XamlRoot as an element that is already present in the AppWindow.
- if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8))
- {
- contentDialog.XamlRoot = this.Content.XamlRoot;
+ return;
}
- ContentDialogResult contentDialogResult = await contentDialog.ShowAsync();
-
- switch (contentDialogResult)
+ try
{
- case ContentDialogResult.Primary:
- // Save, then close
- SaveFile();
- break;
- case ContentDialogResult.Secondary:
- // Don't save, and then close!
- saveButton.IsEnabled = false;
- break;
- default:
- // Cancel closing!
- return;
- }
+ await _dialogSemaphore.WaitAsync();
- // if we got here, we should try to close again
- Application.Current.Exit();
+ ContentDialog contentDialog = new ContentDialog()
+ {
+ Title = title,
+ Content = content,
+ PrimaryButtonText = primaryButtonText,
+ SecondaryButtonText = secondaryButtonText,
+ CloseButtonText = closeButtonText,
+ DefaultButton = ContentDialogButton.Primary,
+ };
+
+ // Use this code to associate the dialog to the appropriate AppWindow by setting
+ // the dialog's XamlRoot to the same XamlRoot as an element that is already present in the AppWindow.
+ if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8))
+ {
+ contentDialog.XamlRoot = this.Content.XamlRoot;
+ }
+
+ ContentDialogResult contentDialogResult = await contentDialog.ShowAsync();
+
+ switch (contentDialogResult)
+ {
+ case ContentDialogResult.Primary:
+ // Save, then close
+ if (!AskFileName(string.Empty) ||
+ !SaveFile())
+ {
+ return;
+ }
+
+ break;
+ case ContentDialogResult.Secondary:
+ // Don't save, and then close!
+ UpdateUnsavedFileState(false);
+ break;
+ default:
+ // Cancel closing!
+ return;
+ }
+
+ // if we got here, we should try to close again
+ Application.Current.Exit();
+ }
+ catch
+ {
+ // Normally nothing to catch here.
+ // But for safety the try-catch ensures that we always release the content dialog lock and exit correctly.
+ }
+ finally
+ {
+ _dialogSemaphore.Release();
+ }
}
///
@@ -927,11 +955,71 @@ namespace RegistryPreviewUILib
type.InvokeMember("ProtectedCursor", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.SetProperty | BindingFlags.Instance, null, uiElement, new object[] { cursor }, CultureInfo.InvariantCulture);
}
+ public void UpdateUnsavedFileState(bool unsavedChanges)
+ {
+ // get, cut and analyze the current title
+ string currentTitle = Regex.Replace(_mainWindow.Title, APPNAME + @"$|\s-\s" + APPNAME + @"$", string.Empty);
+ bool titleContainsIndicator = currentTitle.StartsWith(_unsavedFileIndicator, StringComparison.CurrentCultureIgnoreCase);
+
+ // update window title and save button state
+ if (unsavedChanges)
+ {
+ saveButton.IsEnabled = true;
+
+ if (!titleContainsIndicator)
+ {
+ _updateWindowTitleFunction(_unsavedFileIndicator + currentTitle);
+ }
+ }
+ else
+ {
+ saveButton.IsEnabled = false;
+
+ if (titleContainsIndicator)
+ {
+ _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
@@ -955,10 +1043,13 @@ namespace RegistryPreviewUILib
streamWriter.Close();
// only change when the save is successful
- saveButton.IsEnabled = false;
+ UpdateUnsavedFileState(false);
+ _updateWindowTitleFunction(_appFileName);
}
catch (UnauthorizedAccessException ex)
{
+ saveSuccess = false;
+
// this exception is thrown if the file is there but marked as read only
ShowMessageBox(
resourceLoader.GetString("ErrorDialogTitle"),
@@ -967,6 +1058,8 @@ namespace RegistryPreviewUILib
}
catch
{
+ saveSuccess = false;
+
// this catch handles all other exceptions thrown when trying to write the file out
ShowMessageBox(
resourceLoader.GetString("ErrorDialogTitle"),
@@ -984,6 +1077,8 @@ namespace RegistryPreviewUILib
// restore the cursor
ChangeCursor(gridPreview, false);
+
+ return saveSuccess;
}
///
diff --git a/src/modules/registrypreview/RegistryPreviewUILib/Strings/en-US/Resources.resw b/src/modules/registrypreview/RegistryPreviewUILib/Strings/en-US/Resources.resw
index 54ef71284e..b52dd4b511 100644
--- a/src/modules/registrypreview/RegistryPreviewUILib/Strings/en-US/Resources.resw
+++ b/src/modules/registrypreview/RegistryPreviewUILib/Strings/en-US/Resources.resw
@@ -258,12 +258,18 @@
Cancel
+
+ No
+
Save changes?
Save
+
+ Yes
+
Don't save
@@ -363,4 +369,7 @@
New
+
+ You lose any unsaved changes. Reload anyway?
+
\ No newline at end of file