Files
PowerToys/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Events.cs
Heiko abc5c3e249 [RegistryPreview] Extended preview for value data (#37689)
<!-- 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**

![image](https://github.com/user-attachments/assets/bb275831-be6d-490c-9193-5df719ce6c39)

**Context menu** (Usefull on long data, if button is scrolled out of
view.)

![image](https://github.com/user-attachments/assets/f5fb07ef-6f73-4eac-a289-9dce1c610ceb)

**Preview: REG_SZ**

![image](https://github.com/user-attachments/assets/e03fbbc7-adaa-40d0-967c-7783b1a97b74)

**Preview: REG_MULTI_SZ**

![image](https://github.com/user-attachments/assets/717590a6-7d91-4c9c-8e94-d875a5d2ba6b)

**Preview: REG_EXPAND_SZ**

![image](https://github.com/user-attachments/assets/20135b66-528f-40e7-beed-adfc2b50313d)

**Preview: REG_DWORD and REG_QWORD**

![image](https://github.com/user-attachments/assets/b60110ab-bfc7-40e7-ada3-d278a62b9d01)

**Preview: REG_BINARY**

![image](https://github.com/user-attachments/assets/95f81036-6833-439e-8c01-b3a45c2d8edd)

![image](https://github.com/user-attachments/assets/ce237664-da96-4dbd-835f-969982560b9f)


<!-- 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.
2025-06-13 18:08:01 +08:00

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();
}
}
}
}