mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-06 03:07:04 +02:00
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request ### This PR implements various fixes and improvements into RegistryPreview for saving files: 1. Adds an unsaved file indicator in the title bar like in Notepad. (As indicator we show the * character before the file title.) 2. The save button behaves like a "save as" button, if the file does not exist on disk like in Notepad. (Without fix when running as non-admin you get an access denied error message.) 3. If the app gets closed without saving and the file does not exist on disk, then the user is now asked for the path. (Fixes crash on clicking save button while closing the app.) 4. Failed save actions are handled now correctly on dirty closing, opening files and all other actions that require saving the current state. They will stop the process. 5. A fix for an incorrect enabled state of the save button after opening, reloading and saving a file. 6. Reuse file name on save as button, if known. Otherwise use "new file" template name like in Notepad. 7. Fix an app crash if you click the window's close button a second time while the "Should save?" dialog is opened. 8. Added an reload dialog in case of unsaved changes.   <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] **Closes:** #36876, #36875 - [x] **Communication:** I've discussed this with core contributors already. If work hasn't been agreed, this work might be rejected - [x] **Tests:** Added/updated and all pass - [x] **Localization:** All end user facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed Local test build. --------- Co-authored-by: Gordon Lam (SH) <yeelam@microsoft.com>
533 lines
22 KiB
C#
533 lines
22 KiB
C#
// Copyright (c) Microsoft Corporation
|
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
|
// See the LICENSE file in the project root for more information.
|
|
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using CommunityToolkit.WinUI.UI.Controls;
|
|
using Microsoft.UI.Dispatching;
|
|
using Microsoft.UI.Xaml;
|
|
using Microsoft.UI.Xaml.Controls;
|
|
using Microsoft.UI.Xaml.Data;
|
|
using Windows.Foundation.Metadata;
|
|
using Windows.Storage;
|
|
|
|
namespace RegistryPreviewUILib
|
|
{
|
|
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.
|
|
// (Solves the problem that enabling the event handler fires it one time.)
|
|
private static bool editorContentChangedScripted;
|
|
|
|
/// <summary>
|
|
/// Event that is will prevent the app from closing if the "save file" flag is active
|
|
/// </summary>
|
|
public void MainWindow_Closed(object sender, WindowEventArgs args)
|
|
{
|
|
// Only block closing if the REG file has been edited but not yet saved
|
|
if (saveButton.IsEnabled)
|
|
{
|
|
// if true, the app will not close
|
|
args.Handled = true;
|
|
|
|
// ask the user if they want to save, discard or cancel the close; strings must be loaded here and passed to avoid timing issues
|
|
HandleDirtyClosing(
|
|
resourceLoader.GetString("YesNoCancelDialogTitle"),
|
|
resourceLoader.GetString("YesNoCancelDialogContent"),
|
|
resourceLoader.GetString("YesNoCancelDialogPrimaryButtonText"),
|
|
resourceLoader.GetString("YesNoCancelDialogSecondaryButtonText"),
|
|
resourceLoader.GetString("YesNoCancelDialogCloseButtonText"));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Event that gets fired after the visual tree has been fully loaded; the app opens the reg file from here so it can show a message box successfully
|
|
/// </summary>
|
|
private async void GridPreview_Loaded(object sender, RoutedEventArgs e)
|
|
{
|
|
// static flag to track whether the Visual Tree is ready - if the main Grid has been loaded, the tree is ready.
|
|
visualTreeReady = true;
|
|
|
|
// Check to see if the REG file was opened and parsed successfully
|
|
if (await OpenRegistryFile(_appFileName) == false)
|
|
{
|
|
if (File.Exists(_appFileName))
|
|
{
|
|
// Allow Refresh and Edit to be enabled because a broken Reg file might be fixable
|
|
UpdateToolBarAndUI(false, true, true);
|
|
_updateWindowTitleFunction(resourceLoader.GetString("InvalidRegistryFileTitle"));
|
|
MonacoEditor.TextChanged += MonacoEditor_TextChanged;
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
UpdateToolBarAndUI(false, false, false);
|
|
_updateWindowTitleFunction(string.Empty);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MonacoEditor.TextChanged += MonacoEditor_TextChanged;
|
|
}
|
|
|
|
MonacoEditor.Focus(FocusState.Programmatic);
|
|
}
|
|
|
|
/// <summary>
|
|
/// New button action: Ask to save last changes and reset editor content to reg header only
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uses a picker to select a new file to open
|
|
/// </summary>
|
|
private async void OpenButton_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 file open
|
|
if (!AskFileName(string.Empty) ||
|
|
!SaveFile())
|
|
{
|
|
return;
|
|
}
|
|
|
|
break;
|
|
case ContentDialogResult.Secondary:
|
|
// Don't save and continue the file open!
|
|
break;
|
|
default:
|
|
// Don't open the new file!
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Pull in a new REG file - 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 = OpenFilePicker.ShowDialog(
|
|
windowHandle,
|
|
resourceLoader.GetString("FilterRegistryName") + '\0' + "*.reg" + '\0' + resourceLoader.GetString("FilterAllFiles") + '\0' + "*.*" + '\0' + '\0',
|
|
resourceLoader.GetString("OpenDialogTitle"));
|
|
|
|
if (filename == string.Empty || File.Exists(filename) == false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
StorageFile storageFile = await StorageFile.GetFileFromPathAsync(filename);
|
|
|
|
if (storageFile != null)
|
|
{
|
|
// 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
|
|
UpdateUnsavedFileState(false);
|
|
|
|
// Restore the event handler as we're loaded
|
|
ButtonAction_RestoreTextChangedEvent();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves the currently opened file in place
|
|
/// </summary>
|
|
private void SaveButton_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (!AskFileName(string.Empty))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// save and update window title
|
|
// error handling and ui update happens in SaveFile() method
|
|
_ = SaveFile();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uses a picker to save out a copy of the current reg file
|
|
/// </summary>
|
|
private async void SaveAsButton_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
// mute the TextChanged handler to make for clean UI
|
|
MonacoEditor.TextChanged -= MonacoEditor_TextChanged;
|
|
|
|
if (!AskFileName(_appFileName) || !SaveFile())
|
|
{
|
|
return;
|
|
}
|
|
|
|
UpdateToolBarAndUI(await OpenRegistryFile(_appFileName));
|
|
|
|
// restore the TextChanged handler
|
|
ButtonAction_RestoreTextChangedEvent();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reloads the current REG file from storage
|
|
/// </summary>
|
|
private async void RefreshButton_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("ReloadDialogContent"),
|
|
PrimaryButtonText = resourceLoader.GetString("ReloadDialogPrimaryButtonText"),
|
|
CloseButtonText = resourceLoader.GetString("ReloadDialogCloseButtonText"),
|
|
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:
|
|
// Don't save and continue the reload action!
|
|
break;
|
|
default:
|
|
// Don't continue the reload action!
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 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
|
|
UpdateUnsavedFileState(false);
|
|
|
|
// restore the TextChanged handler
|
|
ButtonAction_RestoreTextChangedEvent();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Opens the Registry Editor; UAC is handled by the request to open
|
|
/// </summary>
|
|
private void RegistryButton_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
// pass in an empty string as we have no file to open
|
|
OpenRegistryEditor(string.Empty);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Opens the Registry Editor and tries to set "last used"; UAC is handled by the request to open
|
|
/// </summary>
|
|
private void RegistryJumpToKeyButton_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
// Get the selected Key, if there is one
|
|
TreeViewNode currentNode = treeView.SelectedNode;
|
|
if (currentNode != null)
|
|
{
|
|
// since there is a valid node, get the FullPath of the key that was selected
|
|
string key = ((RegistryKey)currentNode.Content).FullPath;
|
|
|
|
// it's impossible to directly open a key via command-line option, so we must override the last remember key
|
|
Microsoft.Win32.Registry.SetValue(@"HKEY_Current_User\Software\Microsoft\Windows\CurrentVersion\Applets\Regedit", "LastKey", key);
|
|
}
|
|
|
|
// pass in an empty string as we have no file to open
|
|
OpenRegistryEditor(string.Empty);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Merges the currently saved file into the Registry Editor; UAC is handled by the request to open
|
|
/// </summary>
|
|
private async void WriteButton_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 merge action
|
|
if (!AskFileName(string.Empty) ||
|
|
!SaveFile())
|
|
{
|
|
return;
|
|
}
|
|
|
|
break;
|
|
case ContentDialogResult.Secondary:
|
|
// Don't save and continue the merge action!
|
|
UpdateUnsavedFileState(false);
|
|
break;
|
|
default:
|
|
// Don't merge the file!
|
|
return;
|
|
}
|
|
}
|
|
|
|
// pass in the filename so we can edit the current file
|
|
OpenRegistryEditor(_appFileName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Opens the currently saved file in the PC's default REG file editor (often Notepad)
|
|
/// </summary>
|
|
private void EditButton_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
// use the REG file's filename and verb so we can respect the selected editor
|
|
Process process = new Process();
|
|
process.StartInfo.FileName = string.Format(CultureInfo.InvariantCulture, "\"{0}\"", _appFileName);
|
|
process.StartInfo.Verb = "Edit";
|
|
process.StartInfo.UseShellExecute = true;
|
|
|
|
try
|
|
{
|
|
process.Start();
|
|
}
|
|
catch
|
|
{
|
|
ShowMessageBox(
|
|
resourceLoader.GetString("ErrorDialogTitle"),
|
|
resourceLoader.GetString("FileEditorError"),
|
|
resourceLoader.GetString("OkButtonText"));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Trigger that fires when a node in treeView is clicked and which populates dataGrid
|
|
/// Can also be fired from elsewhere in the code
|
|
/// </summary>
|
|
private void TreeView_ItemInvoked(TreeView sender, TreeViewItemInvokedEventArgs args)
|
|
{
|
|
TreeViewItemInvokedEventArgs localArgs = args as TreeViewItemInvokedEventArgs;
|
|
TreeViewNode treeViewNode = null;
|
|
|
|
// if there are no args, the mouse didn't get clicked but we want to believe it did
|
|
if (args != null)
|
|
{
|
|
treeViewNode = args.InvokedItem as TreeViewNode;
|
|
}
|
|
else
|
|
{
|
|
treeViewNode = treeView.SelectedNode;
|
|
}
|
|
|
|
// Update the toolbar button for the tree
|
|
registryJumpToKeyButton.IsEnabled = CheckTreeForValidKey();
|
|
|
|
// Grab the object that has Registry data in it from the currently selected treeView node
|
|
RegistryKey registryKey = (RegistryKey)treeViewNode.Content;
|
|
|
|
// no matter what happens, clear the ListView of items on each click
|
|
ClearTable();
|
|
|
|
// if there's no ListView items stored for the selected node, dataGrid is clear so get out now
|
|
if (registryKey.Tag == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// if there WAS something in the Tag property, cast it to a list and Populate the ListView
|
|
ArrayList arrayList = (ArrayList)registryKey.Tag;
|
|
listRegistryValues = new List<RegistryValue>();
|
|
|
|
for (int i = 0; i < arrayList.Count; i++)
|
|
{
|
|
RegistryValue listViewItem = (RegistryValue)arrayList[i];
|
|
listRegistryValues.Add(listViewItem);
|
|
}
|
|
|
|
// create a new binding for dataGrid and reattach it, updating the rows
|
|
Binding listRegistryValuesBinding = new Binding { Source = listRegistryValues };
|
|
dataGrid.SetBinding(DataGrid.ItemsSourceProperty, listRegistryValuesBinding);
|
|
}
|
|
|
|
/// <summary>
|
|
/// When the text in editor changes, reload treeView and possibly dataGrid and reset the save button
|
|
/// </summary>
|
|
private void MonacoEditor_TextChanged(object sender, EventArgs e)
|
|
{
|
|
_dispatcherQueue.TryEnqueue(() =>
|
|
{
|
|
RefreshRegistryFile();
|
|
if (!editorContentChangedScripted)
|
|
{
|
|
UpdateUnsavedFileState(true);
|
|
}
|
|
|
|
editorContentChangedScripted = false;
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets indicator for programatic text change and adds text changed handler
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Use this always, if button actions temporary disable the text changed event
|
|
/// </remarks>
|
|
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)
|
|
{
|
|
var data = ((Button)sender).DataContext as RegistryValue;
|
|
InvokeExtendedDataPreview(data);
|
|
}
|
|
|
|
public void MenuExtendedPreview_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var data = ((MenuFlyoutItem)sender).DataContext as RegistryValue;
|
|
InvokeExtendedDataPreview(data);
|
|
}
|
|
|
|
private async void InvokeExtendedDataPreview(RegistryValue valueData)
|
|
{
|
|
// Only one content dialog can be open at the same time and multiple instances of data preview can crash the app.
|
|
if (_dialogSemaphore.CurrentCount == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
// Lock ui and request dialog lock
|
|
_dialogSemaphore.Wait();
|
|
ChangeCursor(gridPreview, true);
|
|
|
|
await ShowExtendedDataPreview(valueData.Name, valueData.Type, valueData.Value);
|
|
}
|
|
catch
|
|
{
|
|
#if DEBUG
|
|
throw;
|
|
#endif
|
|
}
|
|
finally
|
|
{
|
|
// Unblock ui and release dialog lock
|
|
ChangeCursor(gridPreview, false);
|
|
_dialogSemaphore.Release();
|
|
}
|
|
}
|
|
}
|
|
}
|