diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 41cbe3a5b4..69df4fe631 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -65,6 +65,7 @@ applicationframehost Applist applog appmanifest +APPNAME appref apps appwindow @@ -217,6 +218,7 @@ CLIPCHILDREN CLIPSIBLINGS Cloneable clrcall +clrcompression Cls CLSCTX clsid @@ -350,6 +352,7 @@ DCOM dcommon dcomp dcompi +DCompiler DComposition DCR DCs @@ -373,6 +376,7 @@ DEFERERASE DEFPUSHBUTTON deinitialization DELA +DELETEDKEYIMAGE DELETESCANS deletethis Delimarsky @@ -469,6 +473,7 @@ editkeyboardwindow EDITSHORTCUTS editshortcutswindow EFile +egfile ekus emmintrin Emoji @@ -508,6 +513,7 @@ EWXREBOOT EWXSHUTDOWN examplehandler examplepowertoy +EXAND EXCLUDEFROMCAPTURE exdisp executionpolicy @@ -748,6 +754,7 @@ IMAGERESIZEREXT imageresizerinput imageresizersettings imagingdevices +Imc ime imeutil inetcpl @@ -836,6 +843,7 @@ keydown keydropdowncontrol keyevent KEYEVENTF +KEYIMAGE keynum keyremaps Keytool @@ -1293,6 +1301,7 @@ pinfo pinvoke pipename PKBDLLHOOKSTRUCT +Pkcs PKEY plib PLK @@ -1435,7 +1444,10 @@ regfile REGFILTER REGFILTERPINS REGISTERCLASSFAILED +REGISTRYHEADER registrypath +registrypreview +REGISTRYPREVIEWEXT regkey REGPINTYPES regsvr @@ -2043,6 +2055,7 @@ wox wparam wpf wpfdepsjsonpath +wpfgfx wpftmp wpr wprp @@ -2091,6 +2104,7 @@ XStr XUP XVIRTUALSCREEN YAxis +Yeet YIncrement yinle yinwang diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json index 24f0d42852..8ebf519c13 100644 --- a/.pipelines/ESRPSigning_core.json +++ b/.pipelines/ESRPSigning_core.json @@ -147,6 +147,10 @@ "modules\\PowerRename\\PowerToys.PowerRenameContextMenu.dll", "modules\\PowerRename\\PowerRenameContextMenuPackage.msix", + "modules\\RegistryPreview\\PowerToys.RegistryPreviewExt.dll", + "modules\\RegistryPreview\\PowerToys.RegistryPreview.dll", + "modules\\RegistryPreview\\PowerToys.RegistryPreview.exe", + "modules\\ShortcutGuide\\ShortcutGuide\\PowerToys.ShortcutGuide.exe", "modules\\ShortcutGuide\\ShortcutGuideModuleInterface\\PowerToys.ShortcutGuideModuleInterface.dll", @@ -240,6 +244,8 @@ "modules\\PowerAccent\\Vanara.PInvoke.Shell32.dll", "modules\\PowerAccent\\Vanara.PInvoke.ShlwApi.dll", "modules\\PowerAccent\\Vanara.PInvoke.User32.dll", + "modules\\RegistryPreview\\clrcompression.dll", + "modules\\RegistryPreview\\Microsoft.Graphics.Canvas.Interop.dll", "modules\\FileExplorerPreview\\Microsoft.Web.WebView2.Core.dll", "modules\\FileExplorerPreview\\Microsoft.Web.WebView2.WinForms.dll", "modules\\FileExplorerPreview\\Microsoft.Web.WebView2.Wpf.dll", diff --git a/.pipelines/versionAndSignCheck.ps1 b/.pipelines/versionAndSignCheck.ps1 index 753ab98897..b0089f17fa 100644 --- a/.pipelines/versionAndSignCheck.ps1 +++ b/.pipelines/versionAndSignCheck.ps1 @@ -18,7 +18,8 @@ $versionExceptions = @( "Microsoft.Xaml.Interactions.dll", "Microsoft.Xaml.Interactivity.dll", "hyjiacan.py4n.dll", - "Microsoft.WindowsAppRuntime.Release.Net.dll") -join '|'; + "Microsoft.WindowsAppRuntime.Release.Net.dll", + "Microsoft.Windows.Widgets.Projection.dll") -join '|'; $nullVersionExceptions = @( "codicon.ttf", "e_sqlite3.dll", @@ -32,7 +33,8 @@ $nullVersionExceptions = @( "MRM.dll", "PushNotificationsLongRunningTask.ProxyStub.dll", "WindowsAppSdk.AppxDeploymentExtensions.Desktop.dll", - "System.Diagnostics.EventLog.Messages.dll") -join '|'; + "System.Diagnostics.EventLog.Messages.dll", + "Microsoft.Windows.Widgets.dll") -join '|'; $totalFailure = 0; Write-Host $DirPath; diff --git a/PowerToys.sln b/PowerToys.sln index 8f3f5d3ed5..ec3bdfde46 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -494,6 +494,12 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PastePlainModuleInterface", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AllExperiments", "src\common\AllExperiments\AllExperiments.csproj", "{9CE59ED5-7087-4353-88EB-788038A73CEC}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RegistryPreviewUI", "src\modules\registrypreview\RegistryPreviewUI\RegistryPreviewUI.csproj", "{FD86C06A-FB54-4D5E-9831-1CDADF60D45F}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "RegistryPreviewExt", "src\modules\registrypreview\RegistryPreviewExt\RegistryPreviewExt.vcxproj", "{697C6AF9-0A48-49A9-866C-67DA12384015}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RegistryPreview", "RegistryPreview", "{929C1324-22E8-4412-A9A8-80E85F3985A5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -2066,6 +2072,30 @@ Global {9CE59ED5-7087-4353-88EB-788038A73CEC}.Release|x64.Build.0 = Release|x64 {9CE59ED5-7087-4353-88EB-788038A73CEC}.Release|x86.ActiveCfg = Release|x64 {9CE59ED5-7087-4353-88EB-788038A73CEC}.Release|x86.Build.0 = Release|x64 + {FD86C06A-FB54-4D5E-9831-1CDADF60D45F}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {FD86C06A-FB54-4D5E-9831-1CDADF60D45F}.Debug|ARM64.Build.0 = Debug|ARM64 + {FD86C06A-FB54-4D5E-9831-1CDADF60D45F}.Debug|x64.ActiveCfg = Debug|x64 + {FD86C06A-FB54-4D5E-9831-1CDADF60D45F}.Debug|x64.Build.0 = Debug|x64 + {FD86C06A-FB54-4D5E-9831-1CDADF60D45F}.Debug|x86.ActiveCfg = Debug|x64 + {FD86C06A-FB54-4D5E-9831-1CDADF60D45F}.Debug|x86.Build.0 = Debug|x64 + {FD86C06A-FB54-4D5E-9831-1CDADF60D45F}.Release|ARM64.ActiveCfg = Release|ARM64 + {FD86C06A-FB54-4D5E-9831-1CDADF60D45F}.Release|ARM64.Build.0 = Release|ARM64 + {FD86C06A-FB54-4D5E-9831-1CDADF60D45F}.Release|x64.ActiveCfg = Release|x64 + {FD86C06A-FB54-4D5E-9831-1CDADF60D45F}.Release|x64.Build.0 = Release|x64 + {FD86C06A-FB54-4D5E-9831-1CDADF60D45F}.Release|x86.ActiveCfg = Release|x64 + {FD86C06A-FB54-4D5E-9831-1CDADF60D45F}.Release|x86.Build.0 = Release|x64 + {697C6AF9-0A48-49A9-866C-67DA12384015}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {697C6AF9-0A48-49A9-866C-67DA12384015}.Debug|ARM64.Build.0 = Debug|ARM64 + {697C6AF9-0A48-49A9-866C-67DA12384015}.Debug|x64.ActiveCfg = Debug|x64 + {697C6AF9-0A48-49A9-866C-67DA12384015}.Debug|x64.Build.0 = Debug|x64 + {697C6AF9-0A48-49A9-866C-67DA12384015}.Debug|x86.ActiveCfg = Debug|x64 + {697C6AF9-0A48-49A9-866C-67DA12384015}.Debug|x86.Build.0 = Debug|x64 + {697C6AF9-0A48-49A9-866C-67DA12384015}.Release|ARM64.ActiveCfg = Release|ARM64 + {697C6AF9-0A48-49A9-866C-67DA12384015}.Release|ARM64.Build.0 = Release|ARM64 + {697C6AF9-0A48-49A9-866C-67DA12384015}.Release|x64.ActiveCfg = Release|x64 + {697C6AF9-0A48-49A9-866C-67DA12384015}.Release|x64.Build.0 = Release|x64 + {697C6AF9-0A48-49A9-866C-67DA12384015}.Release|x86.ActiveCfg = Release|x64 + {697C6AF9-0A48-49A9-866C-67DA12384015}.Release|x86.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2240,6 +2270,9 @@ Global {9873BA05-4C41-4819-9283-CF45D795431B} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} {FC373B24-3293-453C-AAF5-CF2909DCEE6A} = {9873BA05-4C41-4819-9283-CF45D795431B} {9CE59ED5-7087-4353-88EB-788038A73CEC} = {1AFB6476-670D-4E80-A464-657E01DFF482} + {FD86C06A-FB54-4D5E-9831-1CDADF60D45F} = {929C1324-22E8-4412-A9A8-80E85F3985A5} + {697C6AF9-0A48-49A9-866C-67DA12384015} = {929C1324-22E8-4412-A9A8-80E85F3985A5} + {929C1324-22E8-4412-A9A8-80E85F3985A5} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/installer/PowerToysSetup/Common.wxi b/installer/PowerToysSetup/Common.wxi index 9e5b9ea5bc..c0956226fc 100644 --- a/installer/PowerToysSetup/Common.wxi +++ b/installer/PowerToysSetup/Common.wxi @@ -15,6 +15,7 @@ + diff --git a/installer/PowerToysSetup/PowerToysInstaller.wixproj b/installer/PowerToysSetup/PowerToysInstaller.wixproj index 256ba27132..131d2dceda 100644 --- a/installer/PowerToysSetup/PowerToysInstaller.wixproj +++ b/installer/PowerToysSetup/PowerToysInstaller.wixproj @@ -77,6 +77,7 @@ call "..\..\publish.cmd" arm64 + diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs index daed766bcc..88c47e351a 100644 --- a/installer/PowerToysSetup/Product.wxs +++ b/installer/PowerToysSetup/Product.wxs @@ -68,6 +68,7 @@ + @@ -485,6 +486,11 @@ + + + + + diff --git a/installer/PowerToysSetup/RegistryPreview.wxs b/installer/PowerToysSetup/RegistryPreview.wxs new file mode 100644 index 0000000000..393ca839d2 --- /dev/null +++ b/installer/PowerToysSetup/RegistryPreview.wxs @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/installer/PowerToysSetup/Settings.wxs b/installer/PowerToysSetup/Settings.wxs index 0d2f8a1e8d..0686ccfab6 100644 --- a/installer/PowerToysSetup/Settings.wxs +++ b/installer/PowerToysSetup/Settings.wxs @@ -5,9 +5,9 @@ - - - + + + diff --git a/installer/PowerToysSetup/WinAppSDK.wxs b/installer/PowerToysSetup/WinAppSDK.wxs index b1b7203bc9..998a28dfbf 100644 --- a/installer/PowerToysSetup/WinAppSDK.wxs +++ b/installer/PowerToysSetup/WinAppSDK.wxs @@ -18,7 +18,7 @@ - + @@ -351,6 +351,13 @@ + + + + diff --git a/installer/PowerToysSetup/publish.cmd b/installer/PowerToysSetup/publish.cmd index 58c6ef48aa..ec3658f294 100644 --- a/installer/PowerToysSetup/publish.cmd +++ b/installer/PowerToysSetup/publish.cmd @@ -22,3 +22,5 @@ msbuild !PTRoot!\src\modules\previewpane\SvgThumbnailProvider\SvgThumbnailProvid msbuild !PTRoot!\src\modules\MeasureTool\MeasureToolUI\MeasureToolUI.csproj -t:Publish -p:Configuration="Release" -p:Platform="!PlatformArg!" -p:AppxBundle=Never -p:PowerToysRoot=!PTRoot! -p:VCRTForwarders-IncludeDebugCRT=false -p:PublishProfile=InstallationPublishProfile.pubxml msbuild !PTRoot!\src\modules\FileLocksmith\FileLocksmithUI\FileLocksmithUI.csproj -t:Publish -p:Configuration="Release" -p:Platform="!PlatformArg!" -p:AppxBundle=Never -p:PowerToysRoot=!PTRoot! -p:VCRTForwarders-IncludeDebugCRT=false -p:PublishProfile=InstallationPublishProfile.pubxml + +msbuild !PTRoot!\src\modules\registrypreview\RegistryPreviewUI\RegistryPreviewUI.csproj -t:Publish -p:Configuration="Release" -p:Platform="!PlatformArg!" -p:AppxBundle=Never -p:PowerToysRoot=!PTRoot! -p:VCRTForwarders-IncludeDebugCRT=false -p:PublishProfile=InstallationPublishProfile.pubxml diff --git a/installer/PowerToysSetupCustomActions/CustomAction.cpp b/installer/PowerToysSetupCustomActions/CustomAction.cpp index ed17d9a34c..f68c969c28 100644 --- a/installer/PowerToysSetupCustomActions/CustomAction.cpp +++ b/installer/PowerToysSetupCustomActions/CustomAction.cpp @@ -984,6 +984,7 @@ const std::wstring WinAppSDKConsumers[] = L"modules\\MeasureTool", L"modules\\FileLocksmith", L"modules\\Hosts", + L"modules\\RegistryPreview", }; UINT __stdcall CreateWinAppSDKHardlinksCA(MSIHANDLE hInstall) @@ -1037,6 +1038,7 @@ const std::wstring PTInteropConsumers[] = L"modules\\Hosts", L"modules\\FileExplorerPreview", L"modules\\MouseUtils\\MouseJumpUI", + L"modules\\RegistryPreview", }; UINT __stdcall CreatePTInteropHardlinksCA(MSIHANDLE hInstall) @@ -1081,7 +1083,7 @@ UINT __stdcall CreateDotnetRuntimeHardlinksCA(MSIHANDLE hInstall) UINT er = ERROR_SUCCESS; std::wstring installationFolder, dotnetRuntimeFilesSrcDir, colorPickerDir, powerOCRDir, launcherDir, fancyZonesDir, imageResizerDir, settingsDir, awakeDir, measureToolDir, powerAccentDir, fileExplorerAddOnsDir, hostsDir, fileLocksmithDir, - mouseJumpDir; + mouseJumpDir, registryPreviewDir; hr = WcaInitialize(hInstall, "CreateDotnetRuntimeHardlinksCA"); ExitOnFailure(hr, "Failed to initialize"); @@ -1103,6 +1105,7 @@ UINT __stdcall CreateDotnetRuntimeHardlinksCA(MSIHANDLE hInstall) hostsDir = installationFolder + L"modules\\Hosts\\"; fileLocksmithDir = installationFolder + L"modules\\FileLocksmith\\"; mouseJumpDir = installationFolder + L"modules\\MouseUtils\\MouseJumpUI\\"; + registryPreviewDir = installationFolder + L"modules\\RegistryPreview\\"; for (auto file : dotnetRuntimeFiles) { @@ -1120,6 +1123,7 @@ UINT __stdcall CreateDotnetRuntimeHardlinksCA(MSIHANDLE hInstall) std::filesystem::create_hard_link((dotnetRuntimeFilesSrcDir + file).c_str(), (hostsDir + file).c_str(), ec); std::filesystem::create_hard_link((dotnetRuntimeFilesSrcDir + file).c_str(), (fileLocksmithDir + file).c_str(), ec); std::filesystem::create_hard_link((dotnetRuntimeFilesSrcDir + file).c_str(), (mouseJumpDir + file).c_str(), ec); + std::filesystem::create_hard_link((dotnetRuntimeFilesSrcDir + file).c_str(), (registryPreviewDir + file).c_str(), ec); if (ec.value() != S_OK) { @@ -1144,6 +1148,7 @@ UINT __stdcall CreateDotnetRuntimeHardlinksCA(MSIHANDLE hInstall) std::filesystem::create_hard_link((dotnetRuntimeFilesSrcDir + file).c_str(), (fileExplorerAddOnsDir + file).c_str(), ec); std::filesystem::create_hard_link((dotnetRuntimeFilesSrcDir + file).c_str(), (hostsDir + file).c_str(), ec); std::filesystem::create_hard_link((dotnetRuntimeFilesSrcDir + file).c_str(), (mouseJumpDir + file).c_str(), ec); + std::filesystem::create_hard_link((dotnetRuntimeFilesSrcDir + file).c_str(), (registryPreviewDir + file).c_str(), ec); if (ec.value() != S_OK) { @@ -1238,7 +1243,7 @@ UINT __stdcall DeleteDotnetRuntimeHardlinksCA(MSIHANDLE hInstall) UINT er = ERROR_SUCCESS; std::wstring installationFolder, colorPickerDir, powerOCRDir, launcherDir, fancyZonesDir, imageResizerDir, settingsDir, awakeDir, measureToolDir, powerAccentDir, fileExplorerAddOnsDir, - hostsDir, fileLocksmithDir, mouseJumpDir; + hostsDir, fileLocksmithDir, mouseJumpDir, registryPreviewDir; hr = WcaInitialize(hInstall, "DeleteDotnetRuntimeHardlinksCA"); ExitOnFailure(hr, "Failed to initialize"); @@ -1259,6 +1264,7 @@ UINT __stdcall DeleteDotnetRuntimeHardlinksCA(MSIHANDLE hInstall) hostsDir = installationFolder + L"modules\\Hosts\\"; fileLocksmithDir = installationFolder + L"modules\\FileLocksmith\\"; mouseJumpDir = installationFolder + L"modules\\MouseUtils\\MouseJumpUI\\"; + registryPreviewDir = installationFolder + L"modules\\RegistryPreview\\"; try { @@ -1277,6 +1283,7 @@ UINT __stdcall DeleteDotnetRuntimeHardlinksCA(MSIHANDLE hInstall) DeleteFile((hostsDir + file).c_str()); DeleteFile((fileLocksmithDir + file).c_str()); DeleteFile((mouseJumpDir + file).c_str()); + DeleteFile((registryPreviewDir + file).c_str()); } for (auto file : dotnetRuntimeWPFFiles) @@ -1291,6 +1298,7 @@ UINT __stdcall DeleteDotnetRuntimeHardlinksCA(MSIHANDLE hInstall) DeleteFile((fileExplorerAddOnsDir + file).c_str()); DeleteFile((hostsDir + file).c_str()); DeleteFile((mouseJumpDir + file).c_str()); + DeleteFile((registryPreviewDir + file).c_str()); } } catch (std::exception e) @@ -1324,7 +1332,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall) } processes.resize(bytes / sizeof(processes[0])); - std::array processesToTerminate = { + std::array processesToTerminate = { L"PowerToys.PowerLauncher.exe", L"PowerToys.Settings.exe", L"PowerToys.Awake.exe", @@ -1334,7 +1342,8 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall) L"PowerToys.MouseJumpUI.exe", L"PowerToys.ColorPickerUI.exe", L"PowerToys.AlwaysOnTop.exe", - L"PowerToys.exe" + L"PowerToys.exe", + L"PowerToys.RegistryPreview.exe", }; for (const auto procID : processes) diff --git a/src/common/Common.UI/SettingsDeepLink.cs b/src/common/Common.UI/SettingsDeepLink.cs index 42da58b8a0..c63f3fc1ad 100644 --- a/src/common/Common.UI/SettingsDeepLink.cs +++ b/src/common/Common.UI/SettingsDeepLink.cs @@ -24,6 +24,7 @@ namespace Common.UI ShortcutGuide, VideoConference, Hosts, + RegistryPreview, } private static string SettingsWindowNameToString(SettingsWindow value) @@ -56,6 +57,8 @@ namespace Common.UI return "VideoConference"; case SettingsWindow.Hosts: return "Hosts"; + case SettingsWindow.RegistryPreview: + return "RegistryPreview"; default: { return string.Empty; diff --git a/src/common/GPOWrapper/GPOWrapper.cpp b/src/common/GPOWrapper/GPOWrapper.cpp index 95e228ec3e..9e4a6ef347 100644 --- a/src/common/GPOWrapper/GPOWrapper.cpp +++ b/src/common/GPOWrapper/GPOWrapper.cpp @@ -100,6 +100,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation { return static_cast(powertoys_gpo::getConfiguredQuickAccentEnabledValue()); } + GpoRuleConfigured GPOWrapper::GetConfiguredRegistryPreviewEnabledValue() + { + return static_cast(powertoys_gpo::getConfiguredRegistryPreviewEnabledValue()); + } GpoRuleConfigured GPOWrapper::GetConfiguredScreenRulerEnabledValue() { return static_cast(powertoys_gpo::getConfiguredScreenRulerEnabledValue()); diff --git a/src/common/GPOWrapper/GPOWrapper.h b/src/common/GPOWrapper/GPOWrapper.h index 3a0ca75ace..77d6ba72f9 100644 --- a/src/common/GPOWrapper/GPOWrapper.h +++ b/src/common/GPOWrapper/GPOWrapper.h @@ -31,6 +31,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation static GpoRuleConfigured GetConfiguredPowerRenameEnabledValue(); static GpoRuleConfigured GetConfiguredPowerLauncherEnabledValue(); static GpoRuleConfigured GetConfiguredQuickAccentEnabledValue(); + static GpoRuleConfigured GetConfiguredRegistryPreviewEnabledValue(); static GpoRuleConfigured GetConfiguredScreenRulerEnabledValue(); static GpoRuleConfigured GetConfiguredShortcutGuideEnabledValue(); static GpoRuleConfigured GetConfiguredTextExtractorEnabledValue(); diff --git a/src/common/GPOWrapper/GPOWrapper.idl b/src/common/GPOWrapper/GPOWrapper.idl index 5ab4744702..b0f1633dfc 100644 --- a/src/common/GPOWrapper/GPOWrapper.idl +++ b/src/common/GPOWrapper/GPOWrapper.idl @@ -35,6 +35,7 @@ namespace PowerToys static GpoRuleConfigured GetConfiguredPowerRenameEnabledValue(); static GpoRuleConfigured GetConfiguredPowerLauncherEnabledValue(); static GpoRuleConfigured GetConfiguredQuickAccentEnabledValue(); + static GpoRuleConfigured GetConfiguredRegistryPreviewEnabledValue(); static GpoRuleConfigured GetConfiguredScreenRulerEnabledValue(); static GpoRuleConfigured GetConfiguredShortcutGuideEnabledValue(); static GpoRuleConfigured GetConfiguredTextExtractorEnabledValue(); diff --git a/src/common/interop/interop.cpp b/src/common/interop/interop.cpp index a7ec3b47dd..ae63efa64b 100644 --- a/src/common/interop/interop.cpp +++ b/src/common/interop/interop.cpp @@ -215,10 +215,14 @@ public return gcnew String(CommonSharedConstants::SHORTCUT_GUIDE_TRIGGER_EVENT); } - static String - ^ MeasureToolTriggerEvent() { + static String ^ RegistryPreviewTriggerEvent() { + return gcnew String(CommonSharedConstants::REGISTRY_PREVIEW_TRIGGER_EVENT); + } + + static String ^ MeasureToolTriggerEvent() { return gcnew String(CommonSharedConstants::MEASURE_TOOL_TRIGGER_EVENT); - } + } + static String ^ GcodePreviewResizeEvent() { return gcnew String(CommonSharedConstants::GCODE_PREVIEW_RESIZE_EVENT); } diff --git a/src/common/interop/shared_constants.h b/src/common/interop/shared_constants.h index 43fc2bad20..7f8c0bbd7e 100644 --- a/src/common/interop/shared_constants.h +++ b/src/common/interop/shared_constants.h @@ -46,6 +46,9 @@ namespace CommonSharedConstants // Path to the event used by PowerOCR const wchar_t SHOW_POWEROCR_SHARED_EVENT[] = L"Local\\PowerOCREvent-dc864e06-e1af-4ecc-9078-f98bee745e3a"; + // Path to the event used by RegistryPreview + const wchar_t REGISTRY_PREVIEW_TRIGGER_EVENT[] = L"Local\\RegistryPreviewEvent-4C559468-F75A-4E7F-BC4F-9C9688316687"; + // Path to the event used by MeasureTool const wchar_t MEASURE_TOOL_TRIGGER_EVENT[] = L"Local\\MeasureToolEvent-3d46745f-09b3-4671-a577-236be7abd199"; diff --git a/src/common/logger/logger_settings.h b/src/common/logger/logger_settings.h index bfe35315d8..fb64a11eae 100644 --- a/src/common/logger/logger_settings.h +++ b/src/common/logger/logger_settings.h @@ -57,6 +57,8 @@ struct LogSettings inline const static std::wstring alwaysOnTopLogPath = L"always-on-top-log.txt"; inline const static std::string hostsLoggerName = "hosts"; inline const static std::wstring hostsLogPath = L"Logs\\hosts-log.txt"; + inline const static std::string registryPreviewLoggerName = "registrypreview"; + inline const static std::wstring registryPreviewLogPath = L"Logs\\registryPreview-log.txt"; inline const static int retention = 30; std::wstring logLevel; LogSettings(); diff --git a/src/common/utils/gpo.h b/src/common/utils/gpo.h index 4e54aeda3d..31563a2075 100644 --- a/src/common/utils/gpo.h +++ b/src/common/utils/gpo.h @@ -48,6 +48,7 @@ namespace powertoys_gpo { const std::wstring POLICY_CONFIGURE_ENABLED_TEXT_EXTRACTOR = L"ConfigureEnabledUtilityTextExtractor"; const std::wstring POLICY_CONFIGURE_ENABLED_PASTE_PLAIN = L"ConfigureEnabledUtilityPastePlain"; const std::wstring POLICY_CONFIGURE_ENABLED_VIDEO_CONFERENCE_MUTE = L"ConfigureEnabledUtilityVideoConferenceMute"; + const std::wstring POLICY_CONFIGURE_ENABLED_REGISTRY_PREVIEW = L"ConfigureEnabledUtilityRegistryPreview"; // The registry value names for PowerToys installer and update policies. const std::wstring POLICY_DISABLE_AUTOMATIC_UPDATE_DOWNLOAD = L"AutomaticUpdateDownloadDisabled"; @@ -255,6 +256,10 @@ namespace powertoys_gpo { return getConfiguredValue(POLICY_CONFIGURE_ENABLED_VIDEO_CONFERENCE_MUTE); } + inline gpo_rule_configured_t getConfiguredRegistryPreviewEnabledValue() + { + return getConfiguredValue(POLICY_CONFIGURE_ENABLED_REGISTRY_PREVIEW); + } inline gpo_rule_configured_t getDisableAutomaticUpdateDownloadValue() { return getConfiguredValue(POLICY_DISABLE_AUTOMATIC_UPDATE_DOWNLOAD); diff --git a/src/common/utils/modulesRegistry.h b/src/common/utils/modulesRegistry.h index 62d0d35cf2..1f58ab55ca 100644 --- a/src/common/utils/modulesRegistry.h +++ b/src/common/utils/modulesRegistry.h @@ -192,6 +192,24 @@ inline registry::ChangeSet getStlThumbnailHandlerChangeSet(const std::wstring in NonLocalizable::ExtSTL); } +inline registry::ChangeSet getRegistryPreviewChangeSet(const std::wstring installationDir,const bool perUser) +{ + const HKEY scope = perUser ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE; + + using vec_t = std::vector; + vec_t changes; + + std::wstring command = installationDir; + command.append(L"\\modules\\RegistryPreview\\PowerToys.RegistryPreview.exe \"%1\""); + changes.push_back({ scope, L"Software\\Classes\\regfile\\shell\\preview\\command", std::nullopt, command }); + + std::wstring icon_path = installationDir; + icon_path.append(L"\\modules\\RegistryPreview\\app.ico"); + changes.push_back({ scope, L"Software\\Classes\\regfile\\shell\\preview", L"icon", icon_path }); + + return { changes }; +} + inline std::vector getAllOnByDefaultModulesChangeSets(const std::wstring installationDir) { constexpr bool PER_USER = true; @@ -201,7 +219,8 @@ inline std::vector getAllOnByDefaultModulesChangeSets(const getGcodePreviewHandlerChangeSet(installationDir, PER_USER), getSvgThumbnailHandlerChangeSet(installationDir, PER_USER), getGcodeThumbnailHandlerChangeSet(installationDir, PER_USER), - getStlThumbnailHandlerChangeSet(installationDir, PER_USER) }; + getStlThumbnailHandlerChangeSet(installationDir, PER_USER), + getRegistryPreviewChangeSet(installationDir, PER_USER) }; } inline std::vector getAllModulesChangeSets(const std::wstring installationDir) @@ -215,5 +234,6 @@ inline std::vector getAllModulesChangeSets(const std::wstri getSvgThumbnailHandlerChangeSet(installationDir, PER_USER), getPdfThumbnailHandlerChangeSet(installationDir, PER_USER), getGcodeThumbnailHandlerChangeSet(installationDir, PER_USER), - getStlThumbnailHandlerChangeSet(installationDir, PER_USER) }; + getStlThumbnailHandlerChangeSet(installationDir, PER_USER), + getRegistryPreviewChangeSet(installationDir, PER_USER) }; } diff --git a/src/common/utils/registry.h b/src/common/utils/registry.h index fbb44a23f5..cd4a3fb715 100644 --- a/src/common/utils/registry.h +++ b/src/common/utils/registry.h @@ -16,6 +16,9 @@ namespace registry { + template + inline constexpr bool always_false_v = false; + namespace detail { struct on_exit @@ -27,9 +30,6 @@ namespace registry ~on_exit() { f(); } }; - template - inline constexpr bool always_false_v = false; - template struct overloaded : Ts... { diff --git a/src/gpo/assets/PowerToys.admx b/src/gpo/assets/PowerToys.admx index ef5b2c015d..e3f44a5118 100644 --- a/src/gpo/assets/PowerToys.admx +++ b/src/gpo/assets/PowerToys.admx @@ -269,7 +269,17 @@ - + + + + + + + + + + + diff --git a/src/gpo/assets/en-US/PowerToys.adml b/src/gpo/assets/en-US/PowerToys.adml index d86fbf9a95..915d61f6c9 100644 --- a/src/gpo/assets/en-US/PowerToys.adml +++ b/src/gpo/assets/en-US/PowerToys.adml @@ -11,6 +11,7 @@ PowerToys version 0.64.0 or later PowerToys version 0.68.0 or later + PowerToys version 0.69.0 or later This policy configures the enabled state for a PowerToys utility. @@ -81,6 +82,7 @@ If this setting is disabled, experimentation is not allowed. Power Rename: Configure enabled state PowerToys Run: Configure enabled state Quick Accent: Configure enabled state + Registry Preview: Configure enabled state Screen Ruler: Configure enabled state Shortcut Guide: Configure enabled state Text Extractor: Configure enabled state diff --git a/src/modules/registrypreview/RegistryPreviewExt/Constants.h b/src/modules/registrypreview/RegistryPreviewExt/Constants.h new file mode 100644 index 0000000000..5c81392657 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewExt/Constants.h @@ -0,0 +1,7 @@ +#include + +namespace RegistryPreviewConstants +{ + // Name of the powertoy module. + inline const std::wstring ModuleKey = L"RegistryPreview"; +} \ No newline at end of file diff --git a/src/modules/registrypreview/RegistryPreviewExt/RegistryPreviewExt.rc b/src/modules/registrypreview/RegistryPreviewExt/RegistryPreviewExt.rc new file mode 100644 index 0000000000..e13a322c4f --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewExt/RegistryPreviewExt.rc @@ -0,0 +1,108 @@ +// Microsoft Visual C++ generated resource script. +// +#include +#include "resource.h" +#include "../../../common/version/version.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +1 VERSIONINFO +FILEVERSION FILE_VERSION +PRODUCTVERSION PRODUCT_VERSION +FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG +FILEFLAGS VS_FF_DEBUG +#else +FILEFLAGS 0x0L +#endif +FILEOS VOS_NT_WINDOWS32 +FILETYPE VFT_DLL +FILESUBTYPE VFT2_UNKNOWN +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset + BEGIN + VALUE "CompanyName", COMPANY_NAME + VALUE "FileDescription", FILE_DESCRIPTION + VALUE "FileVersion", FILE_VERSION_STRING + VALUE "InternalName", INTERNAL_NAME + VALUE "LegalCopyright", COPYRIGHT_NOTE + VALUE "OriginalFilename", ORIGINAL_FILENAME + VALUE "ProductName", PRODUCT_NAME + VALUE "ProductVersion", PRODUCT_VERSION_STRING + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset + END +END + + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + +STRINGTABLE +BEGIN + IDS_REGISTRYPREVIEW_NAME "Registry Preview" +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/src/modules/registrypreview/RegistryPreviewExt/RegistryPreviewExt.vcxproj b/src/modules/registrypreview/RegistryPreviewExt/RegistryPreviewExt.vcxproj new file mode 100644 index 0000000000..5f9b2fac10 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewExt/RegistryPreviewExt.vcxproj @@ -0,0 +1,134 @@ + + + + + + Debug + ARM64 + + + Release + ARM64 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + {697C6AF9-0A48-49A9-866C-67DA12384015} + Win32Proj + RegistryPreviewExt + 10.0.19041.0 + + + + DynamicLibrary + true + v143 + Unicode + + + DynamicLibrary + false + v143 + true + Unicode + + + + + + + + + + + + $(SolutionDir)$(Platform)\$(Configuration)\modules\RegistryPreview\ + PowerToys.RegistryPreviewExt + + + + Level3 + true + _DEBUG;REGISTRYPREVIEWEXT_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + Use + ..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories) + + + Windows + true + false + Shlwapi.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + + + + + Level3 + true + true + true + NDEBUG;REGISTRYPREVIEWEXT_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + Use + ..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories) + + + Windows + true + true + true + false + Shlwapi.lib;$(CoreLibraryDependencies);%(AdditionalDependencies) + + + + + + + + + + + + Create + pch.h + + + + + + {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd} + + + {6955446d-23f7-4023-9bb3-8657f904af99} + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/src/modules/registrypreview/RegistryPreviewExt/RegistryPreviewExt.vcxproj.filters b/src/modules/registrypreview/RegistryPreviewExt/RegistryPreviewExt.vcxproj.filters new file mode 100644 index 0000000000..bca37b946b --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewExt/RegistryPreviewExt.vcxproj.filters @@ -0,0 +1,53 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + Source Files + + + + + Resource Files + + + + + + + + + \ No newline at end of file diff --git a/src/modules/registrypreview/RegistryPreviewExt/Trace.cpp b/src/modules/registrypreview/RegistryPreviewExt/Trace.cpp new file mode 100644 index 0000000000..7dda85e43e --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewExt/Trace.cpp @@ -0,0 +1,40 @@ +#include "pch.h" +#include "trace.h" + +TRACELOGGING_DEFINE_PROVIDER( + g_hProvider, + "Microsoft.PowerToys", + // {38e8889b-9731-53f5-e901-e8a7c1753074} + (0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74), + TraceLoggingOptionProjectTelemetry()); + +void Trace::RegisterProvider() +{ + TraceLoggingRegister(g_hProvider); +} + +void Trace::UnregisterProvider() +{ + TraceLoggingUnregister(g_hProvider); +} + +// Log if the user has enabled or disabled the app +void Trace::EnableRegistryPreview(_In_ bool enabled) noexcept +{ + TraceLoggingWrite( + g_hProvider, + "RegistryPreview_EnableRegistryPreview", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE), + TraceLoggingBoolean(enabled, "Enabled")); +} + +// Log that the user tried to activate the app +void Trace::ActivateEditor() noexcept +{ + TraceLoggingWrite( + g_hProvider, + "RegistryPreview_Activate", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); +} diff --git a/src/modules/registrypreview/RegistryPreviewExt/Trace.h b/src/modules/registrypreview/RegistryPreviewExt/Trace.h new file mode 100644 index 0000000000..d2cda345d8 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewExt/Trace.h @@ -0,0 +1,14 @@ +#pragma once + +class Trace +{ +public: + static void RegisterProvider(); + static void UnregisterProvider(); + + // Log if the user has enabled or disabled the app + static void EnableRegistryPreview(const bool enabled) noexcept; + + // Log that the user tried to activate the app + static void ActivateEditor() noexcept; +}; diff --git a/src/modules/registrypreview/RegistryPreviewExt/dllmain.cpp b/src/modules/registrypreview/RegistryPreviewExt/dllmain.cpp new file mode 100644 index 0000000000..4ddac84299 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewExt/dllmain.cpp @@ -0,0 +1,265 @@ +// dllmain.cpp : Defines the entry point for the DLL application. +#include "pch.h" +#include +#include +#include "trace.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#include "resource.h" +#include "Constants.h" + +extern "C" IMAGE_DOS_HEADER __ImageBase; + +BOOL APIENTRY DllMain(HMODULE /*hModule*/, DWORD ul_reason_for_call, LPVOID /*lpReserved*/) +{ + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + Trace::RegisterProvider(); + break; + case DLL_THREAD_ATTACH: + case DLL_THREAD_DETACH: + break; + case DLL_PROCESS_DETACH: + Trace::UnregisterProvider(); + break; + } + return TRUE; +} + +const static wchar_t* MODULE_NAME = L"RegistryPreview"; +const static wchar_t* MODULE_DESC = L"A quick little utility to visualize and edit complex Windows Registry files."; + +class RegistryPreviewModule : public PowertoyModuleIface +{ + +private: + bool m_enabled = false; + + //Hotkey m_hotkey; + HANDLE m_hProcess; + + HANDLE triggerEvent; + EventWaiter triggerEventWaiter; + + bool is_process_running() + { + return WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT; + } + + void launch_process() + { + if (m_enabled) + { + Logger::trace(L"Starting Registry Preview process"); + unsigned long powertoys_pid = GetCurrentProcessId(); + + std::wstring executable_args = L""; + executable_args.append(std::to_wstring(powertoys_pid)); + + SHELLEXECUTEINFOW sei{ sizeof(sei) }; + sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI }; + sei.lpFile = L"modules\\RegistryPreview\\PowerToys.RegistryPreview.exe"; + sei.nShow = SW_SHOWNORMAL; + sei.lpParameters = executable_args.data(); + if (ShellExecuteExW(&sei)) + { + Logger::trace("Successfully started the Registry Preview process"); + } + else + { + Logger::error(L"Registry Preview failed to start. {}", get_last_error_or_default(GetLastError())); + } + + m_hProcess = sei.hProcess; + } + } + + void terminate_process() + { + TerminateProcess(m_hProcess, 1); + } + +public: + RegistryPreviewModule() + { + LoggerHelpers::init_logger(GET_RESOURCE_STRING(IDS_REGISTRYPREVIEW_NAME), L"ModuleInterface", "RegistryPreview"); + Logger::info("Registry Preview object is constructing"); + + if (!m_enabled) + { + const std::wstring installationDir = get_module_folderpath(); + + auto regChanges = getRegistryPreviewChangeSet(installationDir, true); + + if (!regChanges.unApply()) + { + Logger::error(L"Unapplying registry changes failed"); + } + } + + triggerEvent = CreateEvent(nullptr, false, false, CommonSharedConstants::REGISTRY_PREVIEW_TRIGGER_EVENT); + triggerEventWaiter = EventWaiter(CommonSharedConstants::REGISTRY_PREVIEW_TRIGGER_EVENT, [this](int) { + on_hotkey(0); + }); + } + + ~RegistryPreviewModule() + { + if (m_enabled) + { + terminate_process(); + } + m_enabled = false; + } + + // Destroy the powertoy and free memory + virtual void destroy() override + { + delete this; + } + + // Return the localized display name of the powertoy + virtual const wchar_t* get_name() override + { + return MODULE_NAME; + } + + // Return the non localized key of the powertoy, this will be cached by the runner + virtual const wchar_t* get_key() override + { + return MODULE_NAME; + } + + // Return the configured status for the gpo policy for the module + virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override + { + return powertoys_gpo::getConfiguredRegistryPreviewEnabledValue(); + } + + // Return JSON with the configuration options. + virtual bool get_config(wchar_t* buffer, int* buffer_size) override + { + HINSTANCE hinstance = reinterpret_cast(&__ImageBase); + + // Create a Settings object. + PowerToysSettings::Settings settings(hinstance, get_name()); + settings.set_description(MODULE_DESC); + + return settings.serialize_to_buffer(buffer, buffer_size); + } + + // Pop open the app, if the OOBE page asks it to + virtual void call_custom_action(const wchar_t* action) override + { + try + { + PowerToysSettings::CustomActionObject action_object = + PowerToysSettings::CustomActionObject::from_json_string(action); + + if (action_object.get_name() == L"Launch") + { + launch_process(); + Trace::ActivateEditor(); + } + } + catch (std::exception&) + { + Logger::error(L"Failed to parse action. {}", action); + } + } + + // Called by the runner to pass the updated settings values as a serialized JSON. + virtual void set_config(const wchar_t* config) override + { + try + { + // Parse the input JSON string. + PowerToysSettings::PowerToyValues values = PowerToysSettings::PowerToyValues::from_json_string(config, get_key()); + + // If you don't need to do any custom processing of the settings, proceed + // to persists the values. + values.save_to_settings_file(); + } + catch (std::exception&) + { + // Improper JSON. + } + } + + // Enable the powertoy + virtual void enable() + { + const std::wstring installationDir = get_module_folderpath(); + + if (!getRegistryPreviewChangeSet(installationDir, true).apply()) + { + Logger::error(L"Applying registry changes failed"); + } + + // let the DLL enable the app + m_enabled = true; + Trace::EnableRegistryPreview(true); + }; + + virtual void disable() + { + if (m_enabled) + { + // let the DLL disable the app + terminate_process(); + + Trace::EnableRegistryPreview(false); + Logger::trace(L"Disabling Registry Preview..."); + + // Yeet the Registry setting so preview doesn't work anymore + const std::wstring installationDir = get_module_folderpath(); + + if (!getRegistryPreviewChangeSet(installationDir, true).unApply()) + { + Logger::error(L"Unapplying registry changes failed"); + } + } + + m_enabled = false; + } + + // Returns if the powertoys is enabled + virtual bool is_enabled() override + { + return m_enabled; + } + + // Respond to a "click" from the launcher + virtual bool on_hotkey(size_t /*hotkeyId*/) override + { + if (m_enabled) + { + Logger::trace(L"Registry Preview hotkey pressed"); + if (is_process_running()) + { + terminate_process(); + } + else + { + launch_process(); + } + + return true; + } + + return false; + } +}; + +extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() +{ + return new RegistryPreviewModule(); +} diff --git a/src/modules/registrypreview/RegistryPreviewExt/packages.config b/src/modules/registrypreview/RegistryPreviewExt/packages.config new file mode 100644 index 0000000000..c92dd4bf0c --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewExt/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/modules/registrypreview/RegistryPreviewExt/pch.cpp b/src/modules/registrypreview/RegistryPreviewExt/pch.cpp new file mode 100644 index 0000000000..1d9f38c57d --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewExt/pch.cpp @@ -0,0 +1 @@ +#include "pch.h" diff --git a/src/modules/registrypreview/RegistryPreviewExt/pch.h b/src/modules/registrypreview/RegistryPreviewExt/pch.h new file mode 100644 index 0000000000..be72eb015e --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewExt/pch.h @@ -0,0 +1,16 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include +//#include +#include +#include +#include + +#include + +#include +#include +//#include +#include +#include diff --git a/src/modules/registrypreview/RegistryPreviewExt/resource.h b/src/modules/registrypreview/RegistryPreviewExt/resource.h new file mode 100644 index 0000000000..a62c618221 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewExt/resource.h @@ -0,0 +1,21 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Awake.rc +// +#define IDS_REGISTRYPREVIEW_NAME 101 + + +#define FILE_DESCRIPTION "PowerToys Registry Preview Module" +#define INTERNAL_NAME "PowerToys.RegistryPreview" +#define ORIGINAL_FILENAME "PowerToys.RegistryPreview.dll" + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/src/modules/registrypreview/RegistryPreviewUI/App.xaml b/src/modules/registrypreview/RegistryPreviewUI/App.xaml new file mode 100644 index 0000000000..ffd4f5f5ae --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUI/App.xaml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/src/modules/registrypreview/RegistryPreviewUI/App.xaml.cs b/src/modules/registrypreview/RegistryPreviewUI/App.xaml.cs new file mode 100644 index 0000000000..749a5bfcbe --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUI/App.xaml.cs @@ -0,0 +1,66 @@ +// 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.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Microsoft.UI.Xaml; +using Windows.ApplicationModel.Activation; +using LaunchActivatedEventArgs = Windows.ApplicationModel.Activation.LaunchActivatedEventArgs; + +namespace RegistryPreview +{ + /// + /// Provides application-specific behavior to supplement the default Application class. + /// + public partial class App : Application + { + /// + /// Initializes a new instance of the class. + /// + public App() + { + this.InitializeComponent(); + } + + /// + /// Invoked when the application is launched normally by the end user. Other entry points + /// will be used such as when the application is launched to open a specific file. + /// + /// Details about the launch request and process. + protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) + { + // Grab the command line parameters directly from the Environment since this is expected to be run + // via Context Menu of a REG file. + string[] cmdArgs = Environment.GetCommandLineArgs(); + if (cmdArgs == null) + { + // Covers the double click exe scenario and treated as no file loaded + AppFilename = string.Empty; + } + else if (cmdArgs.Length == 2) + { + // GetCommandLineArgs() send in the called EXE as 0 and the selected filename as 1 + AppFilename = cmdArgs[1]; + } + else + { + // Anything else should be treated as no file loaded + AppFilename = string.Empty; + } + + // Start the application + appWindow = new MainWindow(); + appWindow.Activate(); + } + + private Window appWindow; + +#pragma warning disable SA1401 // Fields should be private +#pragma warning disable CA2211 // Non-constant fields should not be visible. TODO: consider making it a property + public static string AppFilename; +#pragma warning restore CA2211 // Non-constant fields should not be visible +#pragma warning restore SA1401 // Fields should be private + } +} diff --git a/src/modules/registrypreview/RegistryPreviewUI/Assets/data32.png b/src/modules/registrypreview/RegistryPreviewUI/Assets/data32.png new file mode 100644 index 0000000000..de5afb66e9 Binary files /dev/null and b/src/modules/registrypreview/RegistryPreviewUI/Assets/data32.png differ diff --git a/src/modules/registrypreview/RegistryPreviewUI/Assets/deleted-folder32.png b/src/modules/registrypreview/RegistryPreviewUI/Assets/deleted-folder32.png new file mode 100644 index 0000000000..ccd53343a6 Binary files /dev/null and b/src/modules/registrypreview/RegistryPreviewUI/Assets/deleted-folder32.png differ diff --git a/src/modules/registrypreview/RegistryPreviewUI/Assets/deleted-value32.png b/src/modules/registrypreview/RegistryPreviewUI/Assets/deleted-value32.png new file mode 100644 index 0000000000..6bf283e6c9 Binary files /dev/null and b/src/modules/registrypreview/RegistryPreviewUI/Assets/deleted-value32.png differ diff --git a/src/modules/registrypreview/RegistryPreviewUI/Assets/error32.png b/src/modules/registrypreview/RegistryPreviewUI/Assets/error32.png new file mode 100644 index 0000000000..100bfed840 Binary files /dev/null and b/src/modules/registrypreview/RegistryPreviewUI/Assets/error32.png differ diff --git a/src/modules/registrypreview/RegistryPreviewUI/Assets/folder32.png b/src/modules/registrypreview/RegistryPreviewUI/Assets/folder32.png new file mode 100644 index 0000000000..ca0bd90c99 Binary files /dev/null and b/src/modules/registrypreview/RegistryPreviewUI/Assets/folder32.png differ diff --git a/src/modules/registrypreview/RegistryPreviewUI/Assets/string32.png b/src/modules/registrypreview/RegistryPreviewUI/Assets/string32.png new file mode 100644 index 0000000000..499f28daea Binary files /dev/null and b/src/modules/registrypreview/RegistryPreviewUI/Assets/string32.png differ diff --git a/src/modules/registrypreview/RegistryPreviewUI/MainWindow.Events.cs b/src/modules/registrypreview/RegistryPreviewUI/MainWindow.Events.cs new file mode 100644 index 0000000000..996f745c42 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUI/MainWindow.Events.cs @@ -0,0 +1,385 @@ +// 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.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Media; +using Windows.Data.Json; +using Windows.Foundation.Metadata; +using Windows.Storage; +using Windows.Storage.Pickers; +using WinRT.Interop; + +namespace RegistryPreview +{ + public sealed partial class MainWindow : Window + { + /// + /// Event handler to grab the main window's size and position before it closes + /// + private void AppWindow_Closing(Microsoft.UI.Windowing.AppWindow sender, Microsoft.UI.Windowing.AppWindowClosingEventArgs args) + { + jsonSettings.SetNamedValue("appWindow.Position.X", JsonValue.CreateNumberValue(appWindow.Position.X)); + jsonSettings.SetNamedValue("appWindow.Position.Y", JsonValue.CreateNumberValue(appWindow.Position.Y)); + jsonSettings.SetNamedValue("appWindow.Size.Width", JsonValue.CreateNumberValue(appWindow.Size.Width)); + jsonSettings.SetNamedValue("appWindow.Size.Height", JsonValue.CreateNumberValue(appWindow.Size.Height)); + } + + /// + /// Event that is will prevent the app from closing if the "save file" flag is active + /// + public void Window_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")); + } + + // Save app settings + jsonSettings.SetNamedValue("checkBoxTextBox.Checked", JsonValue.CreateBooleanValue(checkBoxTextBox.IsChecked.Value)); + SaveSettingsFile(settingsFolder, settingsFile); + } + + /// + /// 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 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; + + // Load and restore app settings + if (jsonSettings.ContainsKey("checkBoxTextBox.Checked")) + { + checkBoxTextBox.IsChecked = jsonSettings.GetNamedBoolean("checkBoxTextBox.Checked"); + } + + // Check to see if the REG file was opened and parsed successfully + if (OpenRegistryFile(App.AppFilename) == false) + { + if (File.Exists(App.AppFilename)) + { + // Allow Refresh and Edit to be enabled because a broken Reg file might be fixable + UpdateToolBarAndUI(false, true, true); + UpdateWindowTitle(resourceLoader.GetString("InvalidRegistryFileTitle")); + textBox.TextChanged += TextBox_TextChanged; + return; + } + else + { + UpdateToolBarAndUI(false, false, false); + UpdateWindowTitle(); + } + } + else + { + textBox.TextChanged += TextBox_TextChanged; + } + + textBox.Focus(FocusState.Programmatic); + } + + /// + /// 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 + 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 + FileOpenPicker fileOpenPicker = new FileOpenPicker(); + fileOpenPicker.ViewMode = PickerViewMode.List; + fileOpenPicker.CommitButtonText = resourceLoader.GetString("OpenButtonText"); + fileOpenPicker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary; + fileOpenPicker.FileTypeFilter.Add(".reg"); + + // Get the HWND so we an open the modal + IntPtr hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this); + InitializeWithWindow.Initialize(fileOpenPicker, hWnd); + + StorageFile storageFile = await fileOpenPicker.PickSingleFileAsync(); + + if (storageFile != null) + { + // mute the TextChanged handler to make for clean UI + textBox.TextChanged -= TextBox_TextChanged; + + App.AppFilename = storageFile.Path; + UpdateToolBarAndUI(OpenRegistryFile(App.AppFilename)); + + // disable the Save button as it's a new file + saveButton.IsEnabled = false; + + // Restore the event handler as we're loaded + textBox.TextChanged += TextBox_TextChanged; + } + } + + /// + /// Saves the currently opened file in place + /// + private void SaveButton_Click(object sender, RoutedEventArgs e) + { + SaveFile(); + } + + /// + /// Uses a picker to save out a copy of the current reg file + /// + private async void SaveAsButton_Click(object sender, RoutedEventArgs e) + { + // Save out a new REG file and then open it + FileSavePicker fileSavePicker = new FileSavePicker(); + fileSavePicker.CommitButtonText = resourceLoader.GetString("SaveButtonText"); + fileSavePicker.SuggestedStartLocation = PickerLocationId.DocumentsLibrary; + fileSavePicker.FileTypeChoices.Add("Registry file", new List() { ".reg" }); + fileSavePicker.SuggestedFileName = resourceLoader.GetString("SuggestFileName"); + + // Get the HWND so we an save the modal + IntPtr hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this); + InitializeWithWindow.Initialize(fileSavePicker, hWnd); + + StorageFile storageFile = await fileSavePicker.PickSaveFileAsync(); + + if (storageFile != null) + { + App.AppFilename = storageFile.Path; + SaveFile(); + UpdateToolBarAndUI(OpenRegistryFile(App.AppFilename)); + } + } + + /// + /// Reloads the current REG file from storage + /// + private void RefreshButton_Click(object sender, RoutedEventArgs e) + { + // mute the TextChanged handler to make for clean UI + textBox.TextChanged -= TextBox_TextChanged; + + // reload the current Registry file and update the toolbar accordingly. + UpdateToolBarAndUI(OpenRegistryFile(App.AppFilename), true, true); + + saveButton.IsEnabled = false; + + // restore the TextChanged handler + textBox.TextChanged += TextBox_TextChanged; + } + + /// + /// 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); + } + + /// + /// 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 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(App.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}\"", App.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; + } + + // 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 textBox changes, reload treeView and possibly dataGrid and reset the save button + /// + private void TextBox_TextChanged(object sender, TextChangedEventArgs e) + { + RefreshRegistryFile(); + saveButton.IsEnabled = true; + } + + /// + /// Readonly checkbox is checked, set textBox to read only; also update the font color so it has a hint of being "disabled" (also the hover state!) + /// + private void CheckBoxTextBox_Checked(object sender, RoutedEventArgs e) + { + textBox.IsReadOnly = true; + SolidColorBrush brush = new SolidColorBrush(Windows.UI.Color.FromArgb(255, 120, 120, 120)); // (SolidColorBrush)Application.Current.Resources["TextBoxDisabledForegroundThemeBrush"]; + if (brush != null) + { + textBox.Foreground = brush; + textBox.Resources["TextControlForegroundPointerOver"] = brush; + } + } + + /// + /// Readonly checkbox is unchecked, set textBox to be editable; also update the font color back to a theme friendly foreground (also the hover state!) + /// + private void CheckBoxTextBox_Unchecked(object sender, RoutedEventArgs e) + { + textBox.IsReadOnly = false; + SolidColorBrush brush = (SolidColorBrush)Application.Current.Resources["TextControlForeground"]; + if (brush != null) + { + textBox.Foreground = brush; + textBox.Resources["TextControlForegroundPointerOver"] = brush; + } + } + } +} diff --git a/src/modules/registrypreview/RegistryPreviewUI/MainWindow.Utilities.cs b/src/modules/registrypreview/RegistryPreviewUI/MainWindow.Utilities.cs new file mode 100644 index 0000000000..62de8f22d2 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUI/MainWindow.Utilities.cs @@ -0,0 +1,896 @@ +// 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 System.Reflection; +using Microsoft.UI.Input; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Windows.Foundation.Metadata; +using Windows.Storage; + +namespace RegistryPreview +{ + public sealed partial class MainWindow : Window + { + /// + /// Method that opens and processes the passed in file name; expected to be an absolute path and a first time open + /// + private bool OpenRegistryFile(string filename) + { + // clamp to prevent attempts to open a file larger than 10MB + try + { + long fileLength = new System.IO.FileInfo(filename).Length; + if (fileLength > 1048576) + { + ShowMessageBox(resourceLoader.GetString("LargeRegistryFileTitle"), App.AppFilename + resourceLoader.GetString("LargeRegistryFile"), resourceLoader.GetString("OkButtonText")); + ChangeCursor(gridPreview, false); + return false; + } + } + catch + { + // Do nothing here - a missing or invalid file will be caught below + } + + // Disable parts of the UI that can cause trouble when loading + ChangeCursor(gridPreview, true); + textBox.Text = string.Empty; + + // clear the treeView and dataGrid no matter what + treeView.RootNodes.Clear(); + ClearTable(); + + // update the current window's title with the current filename + UpdateWindowTitle(filename); + + // Load in the whole file in one call and plop it all into textBox + FileStream fileStream = null; + try + { + FileStreamOptions fileStreamOptions = new FileStreamOptions(); + fileStreamOptions.Access = FileAccess.Read; + fileStreamOptions.Share = FileShare.ReadWrite; + fileStreamOptions.Mode = FileMode.Open; + + fileStream = new FileStream(filename, fileStreamOptions); + StreamReader streamReader = new StreamReader(fileStream); + + string filenameText = streamReader.ReadToEnd(); + textBox.Text = filenameText; + streamReader.Close(); + } + catch + { + // restore TextChanged handler to make for clean UI + textBox.TextChanged += TextBox_TextChanged; + + // Reset the cursor but leave textBox disabled as no content got loaded + ChangeCursor(gridPreview, false); + return false; + } + finally + { + // clean up no matter what + if (fileStream != null) + { + fileStream.Dispose(); + } + } + + // now that the file is loaded and in textBox, parse the data + ParseRegistryFile(textBox.Text); + + // Getting here means that the entire REG file was parsed without incident + // so select the root of the tree and celebrate + if (treeView.RootNodes.Count > 0) + { + treeView.SelectedNode = treeView.RootNodes[0]; + treeView.Focus(FocusState.Programmatic); + } + + // reset the cursor + ChangeCursor(gridPreview, false); + return true; + } + + /// + /// Method that re-opens and processes the filename the app already knows about; expected to not be a first time open + /// + private void RefreshRegistryFile() + { + // Disable parts of the UI that can cause trouble when loading + ChangeCursor(gridPreview, true); + + // Get the current selected node so we can return focus to an existing node + TreeViewNode currentNode = treeView.SelectedNode; + + // clear the treeView and dataGrid no matter what + treeView.RootNodes.Clear(); + ClearTable(); + + // the existing text is still in textBox so parse the data again + ParseRegistryFile(textBox.Text); + + // check to see if there was a key in treeView before the refresh happened + if (currentNode != null) + { + // since there is a valid node, get the FullPath of the key that was selected + string selectedFullPath = ((RegistryKey)currentNode.Content).FullPath; + + // check to see if we still have the key in the new Dictionary of keys + if (mapRegistryKeys.ContainsKey(selectedFullPath)) + { + // we found it! select it in the tree and pretend it was selected + TreeViewNode treeViewNode; + mapRegistryKeys.TryGetValue(selectedFullPath, out treeViewNode); + treeView.SelectedNode = treeViewNode; + TreeView_ItemInvoked(treeView, null); + } + else + { + // we failed to find an existing node; it could have been deleted in the edit + if (treeView.RootNodes.Count > 0) + { + treeView.SelectedNode = treeView.RootNodes[0]; + } + } + } + else + { + // no node was previously selected so check for a RootNode and select it + if (treeView.RootNodes.Count > 0) + { + treeView.SelectedNode = treeView.RootNodes[0]; + } + } + + // enable the UI + ChangeCursor(gridPreview, false); + } + + /// + /// Parses the text that is passed in, which should be the same text that's in textBox + /// + private bool ParseRegistryFile(string filenameText) + { + // if this is a not-first open, clear out the Dictionary of nodes + if (mapRegistryKeys != null) + { + mapRegistryKeys.Clear(); + mapRegistryKeys = null; + } + + // set up a new dictionary + mapRegistryKeys = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + // As we'll be processing the text one line at a time, this string will be the current line + string registryLine; + + // Brute force editing: for textBox to show Cr-Lf corrected, we need to strip out the \n's + filenameText = filenameText.Replace("\r\n", "\r"); + + // split apart all of the text in textBox, where one element in the array represents one line + string[] registryLines = filenameText.Split("\r"); + if (registryLines.Length <= 1) + { + // after the split, we have no lines so get out + ChangeCursor(gridPreview, false); + return false; + } + + // REG files have to start with one of two headers and it's case insensitive + registryLine = registryLines[0]; + registryLine = registryLine.ToLowerInvariant(); + + // make sure that this is a valid REG file, based on the first line of the file + switch (registryLine) + { + case REGISTRYHEADER4: + case REGISTRYHEADER5: + break; + default: + ShowMessageBox(APPNAME, App.AppFilename + resourceLoader.GetString("InvalidRegistryFile"), resourceLoader.GetString("OkButtonText")); + ChangeCursor(gridPreview, false); + return false; + } + + // these are used for populating the tree as we read in one line at a time + TreeViewNode treeViewNode = null; + RegistryValue registryValue = null; + + // start with the first element of the array + int index = 1; + registryLine = registryLines[index]; + + while (index < registryLines.Length) + { + // special case for when the registryLine begins with a @ - make some tweaks and + // let the regular processing handle the rest. + if (registryLine.StartsWith("@=-", StringComparison.InvariantCulture)) + { + // REG file has a callout to delete the @ Value which won't work *but* the Registry Editor will + // clear the value of the @ Value instead, so it's still a valid line. + registryLine = registryLine.Replace("@=-", "\"(Default)\"=\"\""); + } + else if (registryLine.StartsWith("@=", StringComparison.InvariantCulture)) + { + // This is the a Value called "(Default)" so we tweak the line for the UX + registryLine = registryLine.Replace("@=", "\"(Default)\"="); + } + + // continue until we have nothing left to read + // switch logic, based off what the current line we're reading is + if (registryLine.StartsWith("[-", StringComparison.InvariantCulture)) + { + // remove the - as we won't need it but it will get special treatment in the UI + registryLine = registryLine.Remove(1, 1); + + // this is a key, so remove the first [ and last ] + registryLine = StripFirstAndLast(registryLine); + + // do not track the result of this node, since it should have no children + AddTextToTree(registryLine, DELETEDKEYIMAGE); + } + else if (registryLine.StartsWith("[", StringComparison.InvariantCulture)) + { + // this is a key, so remove the first [ and last ] + registryLine = StripFirstAndLast(registryLine); + + treeViewNode = AddTextToTree(registryLine, KEYIMAGE); + } + else if (registryLine.StartsWith("\"", StringComparison.InvariantCulture) && registryLine.EndsWith("=-", StringComparison.InvariantCulture)) + { + // this line deletes this value so it gets special treatment for the UI + registryLine = registryLine.Replace("=-", string.Empty); + + // remove the "'s without removing all of them + registryLine = StripFirstAndLast(registryLine); + + // Create a new listview item that will be used to display the delete value and store it + registryValue = new RegistryValue(registryLine, string.Empty, string.Empty); + SetValueToolTip(registryValue); + + // store the ListViewItem, if we have a valid Key to attach to + if (treeViewNode != null) + { + StoreTheListValue((RegistryKey)treeViewNode.Content, registryValue); + } + } + else if (registryLine.StartsWith("\"", StringComparison.InvariantCulture)) + { + // this is a named value + + // split up the name from the value by looking for the first found = + int equal = registryLine.IndexOf('='); + if ((equal < 0) || (equal > registryLine.Length - 1)) + { + // something is very wrong + Debug.WriteLine(string.Format(CultureInfo.InvariantCulture, "SOMETHING WENT WRONG: {0}", registryLine)); + break; + } + + // set the name and the value + string name = registryLine.Substring(0, equal); + name = StripFirstAndLast(name); + + // Clean out any escaped characters in the value, only for the preview + name = StripEscapedCharacters(name); + + string value = registryLine.Substring(equal + 1); + + // Create a new listview item that will be used to display the value + registryValue = new RegistryValue(name, "REG_SZ", string.Empty); + + // if the first and last character is a " then this is a string value; get rid of the first and last " + if (value.StartsWith("\"", StringComparison.InvariantCulture) && value.EndsWith("\"", StringComparison.InvariantCulture)) + { + value = StripFirstAndLast(value); + } + else + { + // this is an invalid value as there are no "s in the right side of the = + registryValue.Type = "ERROR"; + } + + if (value.StartsWith("dword:", StringComparison.InvariantCultureIgnoreCase)) + { + registryValue.Type = "REG_DWORD"; + value = value.Replace("dword:", string.Empty); + } + else if (value.StartsWith("hex(b):", StringComparison.InvariantCultureIgnoreCase)) + { + registryValue.Type = "REG_QWORD"; + value = value.Replace("hex(b):", string.Empty); + } + else if (value.StartsWith("hex:", StringComparison.InvariantCultureIgnoreCase)) + { + registryValue.Type = "REG_BINARY"; + value = value.Replace("hex:", string.Empty); + } + else if (value.StartsWith("hex(2):", StringComparison.InvariantCultureIgnoreCase)) + { + registryValue.Type = "REG_EXAND_SZ"; + value = value.Replace("hex(2):", string.Empty); + } + else if (value.StartsWith("hex(7):", StringComparison.InvariantCultureIgnoreCase)) + { + registryValue.Type = "REG_MULTI_SZ"; + value = value.Replace("hex(7):", string.Empty); + } + + // If the end of a decimal line ends in a \ then you have to keep + // reading the block as a single value! + while (value.EndsWith(@",\", StringComparison.InvariantCulture)) + { + value = value.TrimEnd('\\'); + index++; + if (index >= registryLines.Length) + { + ChangeCursor(gridPreview, false); + return false; + } + + registryLine = registryLines[index]; + registryLine = registryLine.TrimStart(); + value += registryLine; + } + + // Clean out any escaped characters in the value, only for the preview + value = StripEscapedCharacters(value); + + // update the ListViewItem with this information + if (registryValue.Type != "ERROR") + { + registryValue.Value = value; + } + + // update the ToolTip + SetValueToolTip(registryValue); + + // store the ListViewItem, if we have a valid Key to attach to + if (treeViewNode != null) + { + StoreTheListValue((RegistryKey)treeViewNode.Content, registryValue); + } + } + + // if we get here, it's not a Key (starts with [) or Value (starts with ") so it's likely waste (comments that start with ; fall out here) + + // read the next line from the REG file + index++; + + // if we've gone too far, escape the proc! + if (index >= registryLines.Length) + { + // check to see if anything got parsed! + if (treeView.RootNodes.Count <= 0) + { + ShowMessageBox(APPNAME, App.AppFilename + resourceLoader.GetString("InvalidRegistryFile"), resourceLoader.GetString("OkButtonText")); + } + + ChangeCursor(gridPreview, false); + return false; + } + + // carry on with the next line + registryLine = registryLines[index]; + } + + // last check, to see if anything got parsed! + if (treeView.RootNodes.Count <= 0) + { + ShowMessageBox(APPNAME, App.AppFilename + resourceLoader.GetString("InvalidRegistryFile"), resourceLoader.GetString("OkButtonText")); + ChangeCursor(gridPreview, false); + return false; + } + + return true; + } + + /// + /// We're going to store this ListViewItem in an ArrayList which will then + /// be attached to the most recently returned TreeNode that came back from + /// AddTextToTree. If there's already a list there, we will use that list and + /// add our new node to it. + /// + private void StoreTheListValue(RegistryKey registryKey, RegistryValue registryValue) + { + ArrayList arrayList = null; + if (registryKey.Tag == null) + { + arrayList = new ArrayList(); + } + else + { + arrayList = (ArrayList)registryKey.Tag; + } + + arrayList.Add(registryValue); + + // shove the updated array into the Tag property + registryKey.Tag = arrayList; + } + + /// + /// Adds the REG file that's being currently being viewed to the app's title bar + /// + private void UpdateWindowTitle(string filename) + { + string[] file = filename.Split('\\'); + if (file.Length > 0) + { + appWindow.Title = file[file.Length - 1] + " - " + APPNAME; + } + else + { + appWindow.Title = filename + " - " + APPNAME; + } + } + + /// + /// No REG file was opened, so leave the app's title bar alone + /// + private void UpdateWindowTitle() + { + appWindow.Title = APPNAME; + } + + /// + /// Helper method that assumes everything is enabled/disabled together + /// + private void UpdateToolBarAndUI(bool enable) + { + UpdateToolBarAndUI(enable, enable, enable); + } + + /// + /// Enable command bar buttons and textBox. + /// Note that writeButton and textBox all update with the same value on purpose + /// + private void UpdateToolBarAndUI(bool enableWrite, bool enableRefresh, bool enableEdit) + { + refreshButton.IsEnabled = enableRefresh; + editButton.IsEnabled = enableEdit; + writeButton.IsEnabled = enableWrite; + } + + /// + /// Helper method that creates a new TreeView node, attaches it to a parent if any, and then passes the new node back to the caller + /// mapRegistryKeys is a collection of all of the [] lines in the file + /// keys comes from the REG file and represents a bunch of nodes + /// + private TreeViewNode AddTextToTree(string keys, string image) + { + string[] individualKeys = keys.Split('\\'); + string fullPath = keys; + TreeViewNode returnNewNode = null, newNode = null, previousNode = null; + + // Walk the list of keys backwards + for (int i = individualKeys.Length - 1; i >= 0; i--) + { + // when a Key is marked for deletion, make sure it only sets the icon for the bottom most leaf + if (image == DELETEDKEYIMAGE) + { + if (i < individualKeys.Length - 1) + { + image = KEYIMAGE; + } + else + { + // special casing for Registry roots + switch (individualKeys[i]) + { + case "HKEY_CLASSES_ROOT": + case "HKEY_CURRENT_USER": + case "HKEY_LOCAL_MACHINE": + case "HKEY_USERS": + case "HKEY_CURRENT_CONFIG": + image = KEYIMAGE; + break; + } + } + } + + // First check the dictionary, and return the current node if it already exists + if (mapRegistryKeys.ContainsKey(fullPath)) + { + // was a new node created? + if (returnNewNode == null) + { + // if no new nodes have been created, send out the node we should have already + mapRegistryKeys.TryGetValue(fullPath, out returnNewNode); + } + else + { + // as a new node was created, hook it up to this found parent + mapRegistryKeys.TryGetValue(fullPath, out newNode); + newNode.Children.Add(previousNode); + } + + // return the new node no matter what + return returnNewNode; + } + + // Since the path is not in the tree, create a new node and add it to the dictionary + RegistryKey registryKey = new RegistryKey(individualKeys[i], fullPath, image, GetFolderToolTip(image)); + + newNode = new TreeViewNode() { Content = registryKey, IsExpanded = true }; + mapRegistryKeys.Add(fullPath, newNode); + + // if this is the first new node we're creating, we need to return it to the caller + if (previousNode == null) + { + // capture the first node so it can be returned + returnNewNode = newNode; + } + else + { + // The newly created node is a parent to the previously created node, as add it here. + newNode.Children.Add(previousNode); + } + + // before moving onto the next node, tag the previous node and update the path + previousNode = newNode; + fullPath = fullPath.Replace(string.Format(CultureInfo.InvariantCulture, @"\{0}", individualKeys[i]), string.Empty); + + // One last check: if we get here, the parent of this node is not yet in the tree, so we need to add it as a RootNode + if (i == 0) + { + treeView.RootNodes.Add(newNode); + treeView.UpdateLayout(); + } + } + + return returnNewNode; + } + + /// + /// Wrapper method that shows a simple one-button message box, parented by the main application window + /// + private async void ShowMessageBox(string title, string content, string closeButtonText) + { + ContentDialog contentDialog = new ContentDialog() + { + Title = title, + Content = content, + CloseButtonText = closeButtonText, + }; + + // 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; + } + + await contentDialog.ShowAsync(); + } + + /// + /// Wrapper method that shows a Save/Don't Save/Cancel message box, parented by the main application window and shown when closing the app + /// + private async void HandleDirtyClosing(string title, string content, string primaryButtonText, string secondaryButtonText, string closeButtonText) + { + 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 + SaveFile(); + break; + case ContentDialogResult.Secondary: + // Don't save, and then close! + saveButton.IsEnabled = false; + break; + default: + // Cancel closing! + return; + } + + // if we got here, we should try to close again + App.Current.Exit(); + } + + /// + /// Method will open the Registry Editor or merge the current REG file into the Registry via the Editor + /// Process will prompt for elevation if it needs it. + /// + private void OpenRegistryEditor(string fileMerge) + { + Process process = new Process(); + process.StartInfo.FileName = "regedit.exe"; + process.StartInfo.UseShellExecute = true; + if (File.Exists(fileMerge)) + { + // If Merge was called, pass in the filename as a param to the Editor + process.StartInfo.Arguments = string.Format(CultureInfo.InvariantCulture, "\"{0}\"", fileMerge); + } + + try + { + process.Start(); + } + catch + { + ShowMessageBox( + resourceLoader.GetString("UACDialogTitle"), + resourceLoader.GetString("UACDialogError"), + resourceLoader.GetString("OkButtonText")); + } + } + + /// + /// Utility method that clears out the GridView as there's no other way to do it. + /// + private void ClearTable() + { + if (listRegistryValues != null) + { + listRegistryValues.Clear(); + } + + dataGrid.ItemsSource = null; + } + + /// + /// Change the current app cursor at the grid level to be a wait cursor. Sort of works, sort of doesn't, but it's a nice attempt. + /// + public void ChangeCursor(UIElement uiElement, bool wait) + { + // You can only change the Cursor if the visual tree is loaded + if (!visualTreeReady) + { + return; + } + + InputCursor cursor = InputSystemCursor.Create(wait ? InputSystemCursorShape.Wait : InputSystemCursorShape.Arrow); + System.Type type = typeof(UIElement); + type.InvokeMember("ProtectedCursor", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.SetProperty | BindingFlags.Instance, null, uiElement, new object[] { cursor }, CultureInfo.InvariantCulture); + } + + /// + /// Wrapper method that saves the current file in place, using the current text in textBox. + /// + private void SaveFile() + { + ChangeCursor(gridPreview, true); + + // set up the FileStream for all writing + FileStream fileStream = null; + + try + { + // attempt to open the existing file for writing + FileStreamOptions fileStreamOptions = new FileStreamOptions(); + fileStreamOptions.Access = FileAccess.Write; + fileStreamOptions.Share = FileShare.Write; + fileStreamOptions.Mode = FileMode.OpenOrCreate; + + fileStream = new FileStream(App.AppFilename, fileStreamOptions); + StreamWriter streamWriter = new StreamWriter(fileStream, System.Text.Encoding.Unicode); + + // if we get here, the file is open and writable so dump the whole contents of textBox + string filenameText = textBox.Text; + streamWriter.Write(filenameText); + streamWriter.Flush(); + streamWriter.Close(); + + // only change when the save is successful + saveButton.IsEnabled = false; + } + catch (UnauthorizedAccessException ex) + { + // this exception is thrown if the file is there but marked as read only + ShowMessageBox( + resourceLoader.GetString("ErrorDialogTitle"), + ex.Message, + resourceLoader.GetString("OkButtonText")); + } + catch + { + // this catch handles all other exceptions thrown when trying to write the file out + ShowMessageBox( + resourceLoader.GetString("ErrorDialogTitle"), + resourceLoader.GetString("FileSaveError"), + resourceLoader.GetString("OkButtonText")); + } + finally + { + // clean up no matter what + if (fileStream != null) + { + fileStream.Dispose(); + } + } + + // restore the cursor + ChangeCursor(gridPreview, false); + } + + private async void OpenSettingsFile(string path, string filename) + { + StorageFolder storageFolder = null; + StorageFile storageFile = null; + string fileContents = string.Empty; + + try + { + storageFolder = await StorageFolder.GetFolderFromPathAsync(path); + } + catch + { + } + + try + { + if (storageFolder != null) + { + storageFile = await storageFolder.GetFileAsync(filename); + } + } + catch + { + } + + try + { + if (storageFile != null) + { + fileContents = await Windows.Storage.FileIO.ReadTextAsync(storageFile); + } + } + catch + { + } + + try + { + jsonSettings = Windows.Data.Json.JsonObject.Parse(fileContents); + } + catch + { + // set up default JSON blob + fileContents = "{ }"; + jsonSettings = Windows.Data.Json.JsonObject.Parse(fileContents); + } + } + + /// + /// Save the settings JSON blob out to a local file + /// + private async void SaveSettingsFile(string path, string filename) + { + StorageFolder storageFolder = null; + StorageFile storageFile = null; + string fileContents = string.Empty; + + try + { + storageFolder = await StorageFolder.GetFolderFromPathAsync(path); + } + catch (FileNotFoundException ex) + { + Debug.WriteLine(ex.Message); + Directory.CreateDirectory(path); + storageFolder = await StorageFolder.GetFolderFromPathAsync(path); + } + + try + { + storageFile = await storageFolder.CreateFileAsync(filename, CreationCollisionOption.OpenIfExists); + } + catch (FileNotFoundException ex) + { + Debug.WriteLine(ex.Message); + storageFile = await storageFolder.CreateFileAsync(filename); + } + + try + { + fileContents = jsonSettings.Stringify(); + await Windows.Storage.FileIO.WriteTextAsync(storageFile, fileContents); + } + catch (Exception ex) + { + Debug.WriteLine(ex.Message); + } + } + + /// + /// Rip the first and last character off a string, + /// checking that the string is at least 2 characters long to avoid errors + /// + private string StripFirstAndLast(string line) + { + if (line.Length > 1) + { + line = line.Remove(line.Length - 1, 1); + line = line.Remove(0, 1); + } + + return line; + } + + /// + /// Replace any escaped characters in the REG file with their counterparts, for the UX + /// + private string StripEscapedCharacters(string value) + { + value = value.Replace("\\\\", "\\"); // Replace \\ with \ in the UI + value = value.Replace("\\\"", "\""); // Replace \" with " in the UI + return value; + } + + /// + /// Loads and returns a string for a given Key's image in the tree, based off the current set image + /// + private string GetFolderToolTip(string key) + { + string value = string.Empty; + switch (key) + { + case DELETEDKEYIMAGE: + value = resourceLoader.GetString("ToolTipDeletedKey"); + break; + case KEYIMAGE: + value = resourceLoader.GetString("ToolTipAddedKey"); + break; + } + + return value; + } + + /// + /// Loads a string for a given Value's image in the grid, based off the current type and updates the RegistryValue that's passed in + /// + private void SetValueToolTip(RegistryValue registryValue) + { + string value = string.Empty; + switch (registryValue.Type) + { + case "REG_SZ": + case "REG_EXAND_SZ": + case "REG_MULTI_SZ": + value = resourceLoader.GetString("ToolTipStringValue"); + break; + case "ERROR": + value = resourceLoader.GetString("ToolTipErrorValue"); + break; + case "": + value = resourceLoader.GetString("ToolTipDeletedValue"); + break; + default: + value = resourceLoader.GetString("ToolTipBinaryValue"); + break; + } + + registryValue.ToolTipText = value; + } + } +} diff --git a/src/modules/registrypreview/RegistryPreviewUI/MainWindow.xaml b/src/modules/registrypreview/RegistryPreviewUI/MainWindow.xaml new file mode 100644 index 0000000000..a0157581f5 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUI/MainWindow.xaml @@ -0,0 +1,212 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/registrypreview/RegistryPreviewUI/MainWindow.xaml.cs b/src/modules/registrypreview/RegistryPreviewUI/MainWindow.xaml.cs new file mode 100644 index 0000000000..8866ca2112 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUI/MainWindow.xaml.cs @@ -0,0 +1,93 @@ +// 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.Generic; +using System.IO; +using Microsoft.UI; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Windows.ApplicationModel.Resources; +using Windows.Data.Json; +using Windows.Graphics; + +namespace RegistryPreview +{ + public sealed partial class MainWindow : Window + { + // Const values + private const string REGISTRYHEADER4 = "regedit4"; + private const string REGISTRYHEADER5 = "windows registry editor version 5.00"; + private const string APPNAME = "Registry Preview"; + private const string KEYIMAGE = "ms-appx:///Assets/folder32.png"; + private const string DELETEDKEYIMAGE = "ms-appx:///Assets/deleted-folder32.png"; + + // private members + private Microsoft.UI.Windowing.AppWindow appWindow; + private ResourceLoader resourceLoader; + private bool visualTreeReady; + private Dictionary mapRegistryKeys; + private List listRegistryValues; + private JsonObject jsonSettings; + private string settingsFolder = string.Empty; + private string settingsFile = string.Empty; + + internal MainWindow() + { + this.InitializeComponent(); + + // Initialize the string table + resourceLoader = ResourceLoader.GetForViewIndependentUse(); + + // Removed this on 2/15/23 as it doesn't seem to be doing anything any more + // Attempts to force the visual tree to load faster + // this.Activate(); + + // Update the Win32 looking window with the correct icon (and grab the appWindow handle for later) + IntPtr windowHandle = WinRT.Interop.WindowNative.GetWindowHandle(this); + WindowId windowId = Win32Interop.GetWindowIdFromWindow(windowHandle); + appWindow = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(windowId); + appWindow.SetIcon("app.ico"); + appWindow.Closing += AppWindow_Closing; + + // Open settings file; this moved to after the window tweak because it gives the window time to start up + settingsFolder = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\Microsoft\PowerToys\" + APPNAME; + settingsFile = APPNAME + "_settings.json"; + OpenSettingsFile(settingsFolder, settingsFile); + + // TODO: figure out a way to only call this once after MainWindow is initialized but before it shows itself + // Calling it from here only successfully resizes/moves the window and it seems to be based off timing, which is horrible. + // Calling it from GridPreview_Loaded() works 100% of the time, but the initial state of the window flashes before sizing/moving it + // + // // if have settings, update the location of the window + // if (jsonSettings != null) + // { + // // resize the window + // if (jsonSettings.ContainsKey("appWindow.Size.Width") && jsonSettings.ContainsKey("appWindow.Size.Height")) + // { + // SizeInt32 size; + // size.Width = (int)jsonSettings.GetNamedNumber("appWindow.Size.Width"); + // size.Height = (int)jsonSettings.GetNamedNumber("appWindow.Size.Height"); + // appWindow.Resize(size); + // } + // + // // reposition the window + // if (jsonSettings.ContainsKey("appWindow.Position.X") && jsonSettings.ContainsKey("appWindow.Position.Y")) + // { + // PointInt32 point; + // point.X = (int)jsonSettings.GetNamedNumber("appWindow.Position.X"); + // point.Y = (int)jsonSettings.GetNamedNumber("appWindow.Position.Y"); + // appWindow.Move(point); + // } + // } + + // Update Toolbar + if ((App.AppFilename == null) || (File.Exists(App.AppFilename) != true)) + { + UpdateToolBarAndUI(false); + UpdateWindowTitle(resourceLoader.GetString("FileNotFound")); + } + } + } +} diff --git a/src/modules/registrypreview/RegistryPreviewUI/RegistryKey.xaml.cs b/src/modules/registrypreview/RegistryPreviewUI/RegistryKey.xaml.cs new file mode 100644 index 0000000000..343966fa3d --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUI/RegistryKey.xaml.cs @@ -0,0 +1,32 @@ +// 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. + +namespace RegistryPreview +{ + /// + /// Class representing an each item in the tree view, each one a Registry Key; + /// FullPath is so we can re-select the node after a live update + /// Tag is an Array of ListViewItems that stores all the children for the current object + /// + public class RegistryKey + { + public string Name { get; set; } + + public string FullPath { get; set; } + + public string Image { get; set; } + + public string ToolTipText { get; set; } + + public object Tag { get; set; } + + public RegistryKey(string name, string fullPath, string image, string toolTipText) + { + this.Name = name; + this.FullPath = fullPath; + this.Image = image; + this.ToolTipText = toolTipText; + } + } +} diff --git a/src/modules/registrypreview/RegistryPreviewUI/RegistryPreviewUI.csproj b/src/modules/registrypreview/RegistryPreviewUI/RegistryPreviewUI.csproj new file mode 100644 index 0000000000..35c36b74c6 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUI/RegistryPreviewUI.csproj @@ -0,0 +1,67 @@ + + + + WinExe + net7.0-windows10.0.19041.0 + 10.0.19041.0 + x86;x64;arm64 + $(SolutionDir)$(Platform)\$(Configuration)\modules\RegistryPreview + true + False + app.ico + app.manifest + true + false + false + true + None + true + 10.0.19041.0 + true + $(AssemblyName) + PowerToys.RegistryPreview + PowerToys.RegistryPreview + PowerToys RegistryPreview + RegistryPreview + true + + + + + win10-x64 + + + win10-arm64 + + + + + + + + + https://pkgs.dev.azure.com/dotnet/CommunityToolkit/_packaging/CommunityToolkit-Labs/nuget/v3/index.json + + + + + + + + + + + + + + + + + Never + + + Never + + + + diff --git a/src/modules/registrypreview/RegistryPreviewUI/RegistryValue.xaml.cs b/src/modules/registrypreview/RegistryPreviewUI/RegistryValue.xaml.cs new file mode 100644 index 0000000000..ce372281b3 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUI/RegistryValue.xaml.cs @@ -0,0 +1,57 @@ +// 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; + +namespace RegistryPreview +{ + /// + /// Class representing an each item in the list view, each one a Registry Value. + /// + public class RegistryValue + { + // Static members + private static Uri uriStringValue = new Uri("ms-appx:///Assets/string32.png"); + private static Uri uriBinaryValue = new Uri("ms-appx:///Assets/data32.png"); + private static Uri uriDeleteValue = new Uri("ms-appx:///Assets/deleted-value32.png"); + private static Uri uriErrorValue = new Uri("ms-appx:///Assets/error32.png"); + + public string Name { get; set; } + + public string Type { get; set; } + + public string Value { get; set; } + + public string ToolTipText { get; set; } + + public Uri ImageUri + { + // Based off the Type of the item, pass back the correct image Uri used by the Binding of the DataGrid + get + { + switch (Type) + { + case "REG_SZ": + case "REG_EXAND_SZ": + case "REG_MULTI_SZ": + return uriStringValue; + case "ERROR": + return uriErrorValue; + case "": + return uriDeleteValue; + } + + return uriBinaryValue; + } + } + + public RegistryValue(string name, string type, string value) + { + this.Name = name; + this.Type = type; + this.Value = value; + this.ToolTipText = string.Empty; + } + } +} diff --git a/src/modules/registrypreview/RegistryPreviewUI/Strings/en-US/Resources.resw b/src/modules/registrypreview/RegistryPreviewUI/Strings/en-US/Resources.resw new file mode 100644 index 0000000000..dc89b61703 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUI/Strings/en-US/Resources.resw @@ -0,0 +1,231 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Read only + + + Edit file... + + + Error + + + The REG file editor could not be opened. + + + File was not found! + + + The REG file cannot be written to. + + + doesn't appear to be a valid registry file! + + + File was not a Registry file! + + + is larger than 10MB which is too large for this application. + + + File is too large! + + + Name + + + OK + + + Open file... + + + Open + + + Reload from file + + + Open Registry Editor... + + + Save file as... + + + Save file + + + Save + + + New Registry File + + + Registry file text will appear here... + + + Key will be added, if needed + + + Binary value will be updated + + + Key will be deleted + + + Value will be deleted + + + Value has a syntax error + + + String value will be updated + + + Type + + + You must click Yes on the previous popup if you want to run the Registry application. + + + User Account Control + + + Value + + + Write to Registry... + + + Cancel + + + Changes were made to the text file. Do you want to save your changes? + + + Save + + + Don't save + + + Registry Preview + + \ No newline at end of file diff --git a/src/modules/registrypreview/RegistryPreviewUI/app.ico b/src/modules/registrypreview/RegistryPreviewUI/app.ico new file mode 100644 index 0000000000..77b76593e3 Binary files /dev/null and b/src/modules/registrypreview/RegistryPreviewUI/app.ico differ diff --git a/src/modules/registrypreview/RegistryPreviewUI/app.manifest b/src/modules/registrypreview/RegistryPreviewUI/app.manifest new file mode 100644 index 0000000000..0871bb63b2 --- /dev/null +++ b/src/modules/registrypreview/RegistryPreviewUI/app.manifest @@ -0,0 +1,21 @@ + + + + + + + + true/PM + PerMonitorV2, PerMonitor + + + + + + + + + diff --git a/src/runner/main.cpp b/src/runner/main.cpp index 3e0ed200cc..cc07f71126 100644 --- a/src/runner/main.cpp +++ b/src/runner/main.cpp @@ -165,6 +165,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow L"modules/PowerOCR/PowerToys.PowerOCRModuleInterface.dll", L"modules/PastePlain/PowerToys.PastePlainModuleInterface.dll", L"modules/FileLocksmith/PowerToys.FileLocksmithExt.dll", + L"modules/RegistryPreview/PowerToys.RegistryPreviewExt.dll", L"modules/MeasureTool/PowerToys.MeasureToolModuleInterface.dll", L"modules/Hosts/PowerToys.HostsModuleInterface.dll", }; diff --git a/src/runner/settings_window.cpp b/src/runner/settings_window.cpp index 06f853713d..99dc0a4ae6 100644 --- a/src/runner/settings_window.cpp +++ b/src/runner/settings_window.cpp @@ -663,6 +663,8 @@ std::string ESettingsWindowNames_to_string(ESettingsWindowNames value) return "VideoConference"; case ESettingsWindowNames::Hosts: return "Hosts"; + case ESettingsWindowNames::RegistryPreview: + return "RegistryPreview"; default: { Logger::error(L"Can't convert ESettingsWindowNames value={} to string", static_cast(value)); @@ -726,6 +728,10 @@ ESettingsWindowNames ESettingsWindowNames_from_string(std::string value) { return ESettingsWindowNames::Hosts; } + else if (value == "RegistryPreview") + { + return ESettingsWindowNames::RegistryPreview; + } else { Logger::error(L"Can't convert string value={} to ESettingsWindowNames", winrt::to_hstring(value)); diff --git a/src/runner/settings_window.h b/src/runner/settings_window.h index e03a7c0ef0..1497c2382e 100644 --- a/src/runner/settings_window.h +++ b/src/runner/settings_window.h @@ -16,7 +16,8 @@ enum class ESettingsWindowNames FileExplorer, ShortcutGuide, VideoConference, - Hosts + Hosts, + RegistryPreview, }; std::string ESettingsWindowNames_to_string(ESettingsWindowNames value); diff --git a/src/settings-ui/Settings.UI.Library/EnabledModules.cs b/src/settings-ui/Settings.UI.Library/EnabledModules.cs index c501ada3f3..7db56bc0ec 100644 --- a/src/settings-ui/Settings.UI.Library/EnabledModules.cs +++ b/src/settings-ui/Settings.UI.Library/EnabledModules.cs @@ -362,6 +362,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library } } + private bool registryPreview = true; + + [JsonPropertyName("RegistryPreview")] + public bool RegistryPreview + { + get => registryPreview; + set + { + if (registryPreview != value) + { + LogTelemetryEvent(value); + registryPreview = value; + } + } + } + private void NotifyChange() { notifyEnabledChangedAction?.Invoke(); diff --git a/src/settings-ui/Settings.UI.Library/RegistryPreviewSettings.cs b/src/settings-ui/Settings.UI.Library/RegistryPreviewSettings.cs new file mode 100644 index 0000000000..92718d2e91 --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/RegistryPreviewSettings.cs @@ -0,0 +1,27 @@ +// 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.Text.Json.Serialization; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class RegistryPreviewSettings : BasePTModuleSettings, ISettingsConfig + { + public const string ModuleName = "RegistryPreview"; + + public RegistryPreviewSettings() + { + Version = "1"; + Name = ModuleName; + } + + public string GetModuleName() + => Name; + + // This can be utilized in the future if the settings.json file is to be modified/deleted. + public bool UpgradeSettingsConfiguration() + => false; + } +} diff --git a/src/settings-ui/Settings.UI/App.xaml.cs b/src/settings-ui/Settings.UI/App.xaml.cs index 75cc05883e..007482ee92 100644 --- a/src/settings-ui/Settings.UI/App.xaml.cs +++ b/src/settings-ui/Settings.UI/App.xaml.cs @@ -153,6 +153,7 @@ namespace Microsoft.PowerToys.Settings.UI case "VideoConference": StartupPage = typeof(Views.VideoConferencePage); break; case "MeasureTool": StartupPage = typeof(Views.MeasureToolPage); break; case "Hosts": StartupPage = typeof(Views.HostsPage); break; + case "RegistryPreview": StartupPage = typeof(Views.RegistryPreviewPage); break; case "PastePlain": StartupPage = typeof(Views.PastePlainPage); break; default: Debug.Assert(false, "Unexpected SettingsWindow argument value"); break; } diff --git a/src/settings-ui/Settings.UI/Assets/FluentIcons/FluentIconsRegistryPreview.png b/src/settings-ui/Settings.UI/Assets/FluentIcons/FluentIconsRegistryPreview.png new file mode 100644 index 0000000000..57e7549189 Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/FluentIcons/FluentIconsRegistryPreview.png differ diff --git a/src/settings-ui/Settings.UI/Assets/Modules/OOBE/RegistryPreview.png b/src/settings-ui/Settings.UI/Assets/Modules/OOBE/RegistryPreview.png new file mode 100644 index 0000000000..369e117a7e Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Modules/OOBE/RegistryPreview.png differ diff --git a/src/settings-ui/Settings.UI/Assets/Modules/RegistryPreview.png b/src/settings-ui/Settings.UI/Assets/Modules/RegistryPreview.png new file mode 100644 index 0000000000..ec1a043536 Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Modules/RegistryPreview.png differ diff --git a/src/settings-ui/Settings.UI/Flyout/LaunchPage.xaml.cs b/src/settings-ui/Settings.UI/Flyout/LaunchPage.xaml.cs index c921aeb4e0..e16a48461b 100644 --- a/src/settings-ui/Settings.UI/Flyout/LaunchPage.xaml.cs +++ b/src/settings-ui/Settings.UI/Flyout/LaunchPage.xaml.cs @@ -65,6 +65,13 @@ namespace Microsoft.PowerToys.Settings.UI.Flyout break; + case "RegistryPreview": // Launch Registry Preview + using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.RegistryPreviewTriggerEvent())) + { + eventHandle.Set(); + } + + break; case "MeasureTool": // Launch Screen Ruler using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.MeasureToolTriggerEvent())) { diff --git a/src/settings-ui/Settings.UI/MainWindow.xaml.cs b/src/settings-ui/Settings.UI/MainWindow.xaml.cs index 06ce02f20d..b214590e65 100644 --- a/src/settings-ui/Settings.UI/MainWindow.xaml.cs +++ b/src/settings-ui/Settings.UI/MainWindow.xaml.cs @@ -138,6 +138,9 @@ namespace Microsoft.PowerToys.Settings.UI case "PowerAccent": needToUpdate = generalSettingsConfig.Enabled.PowerAccent != isEnabled; generalSettingsConfig.Enabled.PowerAccent = isEnabled; break; + case "RegistryPreview": + needToUpdate = generalSettingsConfig.Enabled.RegistryPreview != isEnabled; + generalSettingsConfig.Enabled.RegistryPreview = isEnabled; break; case "MeasureTool": needToUpdate = generalSettingsConfig.Enabled.MeasureTool != isEnabled; generalSettingsConfig.Enabled.MeasureTool = isEnabled; break; diff --git a/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs b/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs index b7ceb31266..bdfb3e54f6 100644 --- a/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs +++ b/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs @@ -26,5 +26,6 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Enums Hosts, PastePlain, WhatsNew, + RegistryPreview, } } diff --git a/src/settings-ui/Settings.UI/OOBE/Views/OobeRegistryPreview.xaml b/src/settings-ui/Settings.UI/OOBE/Views/OobeRegistryPreview.xaml new file mode 100644 index 0000000000..0709030bea --- /dev/null +++ b/src/settings-ui/Settings.UI/OOBE/Views/OobeRegistryPreview.xaml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + +