// 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; /// /// Event that is will prevent the app from closing if the "save file" flag is active /// 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")); } } /// /// 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 /// 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); } /// /// 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 /// 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(); } } /// /// Saves the currently opened file in place /// 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(); } /// /// Uses a picker to save out a copy of the current reg file /// 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(); } /// /// Reloads the current REG file from storage /// 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(); } /// /// Opens the Registry Editor; UAC is handled by the request to open /// private void RegistryButton_Click(object sender, RoutedEventArgs e) { // pass in an empty string as we have no file to open OpenRegistryEditor(string.Empty); } /// /// Opens the Registry Editor and tries to set "last used"; UAC is handled by the request to open /// 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); } /// /// Merges the currently saved file into the Registry Editor; UAC is handled by the request to open /// 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); } /// /// Opens the currently saved file in the PC's default REG file editor (often Notepad) /// 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")); } } /// /// Trigger that fires when a node in treeView is clicked and which populates dataGrid /// Can also be fired from elsewhere in the code /// 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(); 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); } /// /// When the text in editor changes, reload treeView and possibly dataGrid and reset the save button /// private void MonacoEditor_TextChanged(object sender, EventArgs e) { _dispatcherQueue.TryEnqueue(() => { RefreshRegistryFile(); 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) { 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(); } } } }