mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-15 19:27:56 +01: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 Add a button to the data grid that shows a windows for complex value preview. ### Screenshots **Button**  **Context menu** (Usefull on long data, if button is scrolled out of view.)  **Preview: REG_SZ**  **Preview: REG_MULTI_SZ**  **Preview: REG_EXPAND_SZ**  **Preview: REG_DWORD and REG_QWORD**  **Preview: REG_BINARY**   <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] **Closes:** #36877 - [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 => missing yet - [ ] **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 Additionally this PR updates the context menu for values. <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed Tested code with a local build.
462 lines
19 KiB
C#
462 lines
19 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();
|
|
|
|
/// <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>
|
|
/// 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
|
|
SaveFile();
|
|
break;
|
|
case ContentDialogResult.Secondary:
|
|
// Don't save and continue the file open!
|
|
saveButton.IsEnabled = false;
|
|
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;
|
|
_appFileName = storageFile.Path;
|
|
UpdateToolBarAndUI(await OpenRegistryFile(_appFileName));
|
|
|
|
// disable the Save button as it's a new file
|
|
saveButton.IsEnabled = false;
|
|
|
|
// Restore the event handler as we're loaded
|
|
MonacoEditor.TextChanged += MonacoEditor_TextChanged;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves the currently opened file in place
|
|
/// </summary>
|
|
private void SaveButton_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
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)
|
|
{
|
|
// 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"));
|
|
|
|
if (filename == string.Empty)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_appFileName = filename;
|
|
SaveFile();
|
|
UpdateToolBarAndUI(await OpenRegistryFile(_appFileName));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reloads the current REG file from storage
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets the editor content
|
|
/// </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 file open
|
|
SaveFile();
|
|
break;
|
|
case ContentDialogResult.Secondary:
|
|
// Don't save and continue the file open!
|
|
saveButton.IsEnabled = false;
|
|
break;
|
|
default:
|
|
// Don't open the new file!
|
|
return;
|
|
}
|
|
}
|
|
|
|
// mute the TextChanged handler to make for clean UI
|
|
MonacoEditor.TextChanged -= MonacoEditor_TextChanged;
|
|
|
|
// reset editor, file info and ui.
|
|
_appFileName = string.Empty;
|
|
ResetEditorAndFile();
|
|
|
|
// restore the TextChanged handler
|
|
MonacoEditor.TextChanged += MonacoEditor_TextChanged;
|
|
|
|
// disable buttons that do not make sense
|
|
saveButton.IsEnabled = false;
|
|
refreshButton.IsEnabled = false;
|
|
}
|
|
|
|
/// <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 file open
|
|
SaveFile();
|
|
break;
|
|
case ContentDialogResult.Secondary:
|
|
// Don't save and continue the file open!
|
|
saveButton.IsEnabled = false;
|
|
break;
|
|
default:
|
|
// Don't open the new 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();
|
|
saveButton.IsEnabled = true;
|
|
});
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
}
|
|
}
|
|
}
|