diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 425ec9bbd0..14d58e86b9 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -789,6 +789,7 @@ iid Iindex IIO iiq +IJson Ijwhost IKs ILogon @@ -914,6 +915,7 @@ Lastdevice Laute laviusmotileng LAYOUTRTL +Lbl LBUTTON LBUTTONDBLCLK LBUTTONDOWN @@ -1342,6 +1344,7 @@ PARTIALCONFIRMATIONDIALOGTITLE pasteplain PATCOPY pathcch +PATHEXT Pathto PATINVERT PATPAINT @@ -1469,6 +1472,7 @@ pscid PSECURITY psfgao psfi +PSMODULEPATH Psr psrm psrree @@ -1691,6 +1695,7 @@ setlocal SETREDRAW SETTEXT SETTINGCHANGE +SETTINGSCHANGED settingsheader settingshotkeycontrol SETWORKAREA diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json index c3c91deb6a..d217b2dd1e 100644 --- a/.pipelines/ESRPSigning_core.json +++ b/.pipelines/ESRPSigning_core.json @@ -98,6 +98,10 @@ "WinUI3Apps\\Powertoys.Peek.UI.exe", "WinUI3Apps\\Powertoys.Peek.dll", + "WinUI3Apps\\PowerToys.EnvironmentVariablesModuleInterface.dll", + "WinUI3Apps\\PowerToys.EnvironmentVariables.dll", + "WinUI3Apps\\PowerToys.EnvironmentVariables.exe", + "PowerToys.ImageResizer.exe", "PowerToys.ImageResizer.dll", "PowerToys.ImageResizerExt.dll", diff --git a/Directory.Packages.props b/Directory.Packages.props index daa51a7736..ab19b0f29e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,6 +8,7 @@ + diff --git a/NOTICE.md b/NOTICE.md index efe03a0e85..0733fc05dc 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -312,6 +312,7 @@ SOFTWARE. - CommunityToolkit.Mvvm 8.2.0 - CommunityToolkit.WinUI.Animations 8.0.230907 - CommunityToolkit.WinUI.Controls.Primitives 8.0.230907 +- CommunityToolkit.WinUI.Controls.Segmented 8.0.230907 - CommunityToolkit.WinUI.Controls.SettingsControls 8.0.230907 - CommunityToolkit.WinUI.Controls.Sizers 8.0.230907 - CommunityToolkit.WinUI.Converters 8.0.230907 diff --git a/PowerToys.sln b/PowerToys.sln index d629d970d4..21c1848742 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -539,6 +539,12 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CropAndLockModuleInterface" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests-FancyZonesEditor", "src\modules\fancyzones\UnitTests-FancyZonesEditor\UnitTests-FancyZonesEditor.csproj", "{FC8EB78F-F061-4BD9-A3F6-507BEA965E2B}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "EnvironmentVariables", "EnvironmentVariables", "{538ED0BB-B863-4B20-98CC-BCDF7FA0B68A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EnvironmentVariables", "src\modules\EnvironmentVariables\EnvironmentVariables\EnvironmentVariables.csproj", "{51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "EnvironmentVariablesModuleInterface", "src\modules\EnvironmentVariables\EnvironmentVariablesModuleInterface\EnvironmentVariablesModuleInterface.vcxproj", "{B9420661-B0E4-4241-ABD4-4A27A1F64250}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|ARM64 = Debug|ARM64 @@ -2321,6 +2327,30 @@ Global {FC8EB78F-F061-4BD9-A3F6-507BEA965E2B}.Release|x64.Build.0 = Release|x64 {FC8EB78F-F061-4BD9-A3F6-507BEA965E2B}.Release|x86.ActiveCfg = Release|x64 {FC8EB78F-F061-4BD9-A3F6-507BEA965E2B}.Release|x86.Build.0 = Release|x64 + {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Debug|ARM64.Build.0 = Debug|ARM64 + {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Debug|x64.ActiveCfg = Debug|x64 + {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Debug|x64.Build.0 = Debug|x64 + {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Debug|x86.ActiveCfg = Debug|x64 + {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Debug|x86.Build.0 = Debug|x64 + {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Release|ARM64.ActiveCfg = Release|ARM64 + {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Release|ARM64.Build.0 = Release|ARM64 + {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Release|x64.ActiveCfg = Release|x64 + {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Release|x64.Build.0 = Release|x64 + {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Release|x86.ActiveCfg = Release|x64 + {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA}.Release|x86.Build.0 = Release|x64 + {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Debug|ARM64.Build.0 = Debug|ARM64 + {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Debug|x64.ActiveCfg = Debug|x64 + {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Debug|x64.Build.0 = Debug|x64 + {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Debug|x86.ActiveCfg = Debug|x64 + {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Debug|x86.Build.0 = Debug|x64 + {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Release|ARM64.ActiveCfg = Release|ARM64 + {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Release|ARM64.Build.0 = Release|ARM64 + {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Release|x64.ActiveCfg = Release|x64 + {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Release|x64.Build.0 = Release|x64 + {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Release|x86.ActiveCfg = Release|x64 + {B9420661-B0E4-4241-ABD4-4A27A1F64250}.Release|x86.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2516,6 +2546,9 @@ Global {F5E1146E-B7B3-4E11-85FD-270A500BD78C} = {3B227528-4BA6-4CAF-B44A-A10C78A64849} {3157FA75-86CF-4EE2-8F62-C43F776493C6} = {3B227528-4BA6-4CAF-B44A-A10C78A64849} {FC8EB78F-F061-4BD9-A3F6-507BEA965E2B} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} + {538ED0BB-B863-4B20-98CC-BCDF7FA0B68A} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} + {51465DA1-C18B-4B99-93E1-ECF8E0FA0CBA} = {538ED0BB-B863-4B20-98CC-BCDF7FA0B68A} + {B9420661-B0E4-4241-ABD4-4A27A1F64250} = {538ED0BB-B863-4B20-98CC-BCDF7FA0B68A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} diff --git a/doc/images/icons/Environment Manager.png b/doc/images/icons/Environment Manager.png new file mode 100644 index 0000000000..1d14dbeaf8 Binary files /dev/null and b/doc/images/icons/Environment Manager.png differ diff --git a/installer/PowerToysSetup/EnvironmentVariables.wxs b/installer/PowerToysSetup/EnvironmentVariables.wxs new file mode 100644 index 0000000000..44567055af --- /dev/null +++ b/installer/PowerToysSetup/EnvironmentVariables.wxs @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/installer/PowerToysSetup/PowerToysInstaller.wixproj b/installer/PowerToysSetup/PowerToysInstaller.wixproj index e1cbe42d5f..9992d919ae 100644 --- a/installer/PowerToysSetup/PowerToysInstaller.wixproj +++ b/installer/PowerToysSetup/PowerToysInstaller.wixproj @@ -35,6 +35,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil call move /Y ..\..\..\BaseApplications.wxs.bk ..\..\..\BaseApplications.wxs call move /Y ..\..\..\ColorPicker.wxs.bk ..\..\..\ColorPicker.wxs call move /Y ..\..\..\Core.wxs.bk ..\..\..\Core.wxs + call move /Y ..\..\..\EnvironmentVariables.wxs.bk ..\..\..\EnvironmentVariables.wxs call move /Y ..\..\..\FileExplorerPreview.wxs.bk ..\..\..\FileExplorerPreview.wxs call move /Y ..\..\..\FileLocksmith.wxs.bk ..\..\..\FileLocksmith.wxs call move /Y ..\..\..\Hosts.wxs.bk ..\..\..\Hosts.wxs @@ -104,6 +105,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil + diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs index 853a1ac795..2dea1fab55 100644 --- a/installer/PowerToysSetup/Product.wxs +++ b/installer/PowerToysSetup/Product.wxs @@ -71,6 +71,7 @@ + diff --git a/installer/PowerToysSetup/generateAllFileComponents.ps1 b/installer/PowerToysSetup/generateAllFileComponents.ps1 index 6542bc45c3..f233ac5da6 100644 --- a/installer/PowerToysSetup/generateAllFileComponents.ps1 +++ b/installer/PowerToysSetup/generateAllFileComponents.ps1 @@ -32,6 +32,10 @@ Invoke-Expression -Command "$PSScriptRoot\generateFileComponents.ps1 -fileListNa Invoke-Expression -Command "$PSScriptRoot\generateFileList.ps1 -fileDepsJson """" -fileListName ColorPickerAssetsFiles -wxsFilePath $PSScriptRoot\ColorPicker.wxs -depsPath ""$PSScriptRoot..\..\..\$platform\Release\Assets\ColorPicker""" Invoke-Expression -Command "$PSScriptRoot\generateFileComponents.ps1 -fileListName ""ColorPickerAssetsFiles"" -wxsFilePath $PSScriptRoot\ColorPicker.wxs -regroot $registryroot" +#Environment Variables +Invoke-Expression -Command "$PSScriptRoot\generateFileList.ps1 -fileDepsJson """" -fileListName EnvironmentVariablesAssetsFiles -wxsFilePath $PSScriptRoot\EnvironmentVariables.wxs -depsPath ""$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\EnvironmentVariables""" +Invoke-Expression -Command "$PSScriptRoot\generateFileComponents.ps1 -fileListName ""EnvironmentVariablesAssetsFiles"" -wxsFilePath $PSScriptRoot\EnvironmentVariables.wxs -regroot $registryroot" + #FileExplorerAdd-ons Invoke-Expression -Command "$PSScriptRoot\generateFileList.ps1 -fileDepsJson """" -fileListName MonacoPreviewHandlerMonacoAssetsFiles -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -depsPath ""$PSScriptRoot..\..\..\$platform\Release\Assets\Monaco""" Invoke-Expression -Command "$PSScriptRoot\generateFileList.ps1 -fileDepsJson """" -fileListName MonacoPreviewHandlerCustomLanguagesFiles -wxsFilePath $PSScriptRoot\FileExplorerPreview.wxs -depsPath ""$PSScriptRoot..\..\..\$platform\Release\Assets\Monaco\customLanguages""" diff --git a/installer/PowerToysSetupCustomActions/CustomAction.cpp b/installer/PowerToysSetupCustomActions/CustomAction.cpp index 579fe3ba6b..4374d2403d 100644 --- a/installer/PowerToysSetupCustomActions/CustomAction.cpp +++ b/installer/PowerToysSetupCustomActions/CustomAction.cpp @@ -1005,7 +1005,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", @@ -1033,6 +1033,7 @@ UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall) L"PowerToys.MouseWithoutBordersHelper.exe", L"PowerToys.MouseWithoutBordersService.exe", L"PowerToys.CropAndLock.exe", + L"PowerToys.EnvironmentVariables.exe", L"PowerToys.exe", }; diff --git a/installer/PowerToysSetupCustomActions/PowerToysSetupCustomActions.vcxproj b/installer/PowerToysSetupCustomActions/PowerToysSetupCustomActions.vcxproj index 2768846e22..81235042b4 100644 --- a/installer/PowerToysSetupCustomActions/PowerToysSetupCustomActions.vcxproj +++ b/installer/PowerToysSetupCustomActions/PowerToysSetupCustomActions.vcxproj @@ -52,6 +52,7 @@ call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\BaseApplications.wxs"" ""$(ProjectDir)..\PowerToysSetup\BaseApplications.wxs.bk"""" call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\ColorPicker.wxs"" ""$(ProjectDir)..\PowerToysSetup\ColorPicker.wxs.bk"""" call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\Core.wxs"" ""$(ProjectDir)..\PowerToysSetup\Core.wxs.bk"""" + call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\EnvironmentVariables.wxs"" ""$(ProjectDir)..\PowerToysSetup\EnvironmentVariables.wxs.bk"""" call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\FileExplorerPreview.wxs"" ""$(ProjectDir)..\PowerToysSetup\FileExplorerPreview.wxs.bk"""" call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\FileLocksmith.wxs"" ""$(ProjectDir)..\PowerToysSetup\FileLocksmith.wxs.bk"""" call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\Hosts.wxs"" ""$(ProjectDir)..\PowerToysSetup\Hosts.wxs.bk"""" diff --git a/src/codeAnalysis/GlobalSuppressions.cs b/src/codeAnalysis/GlobalSuppressions.cs index f130177ee3..64e1ab9b16 100644 --- a/src/codeAnalysis/GlobalSuppressions.cs +++ b/src/codeAnalysis/GlobalSuppressions.cs @@ -33,7 +33,7 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Static methods may improve performance but decrease maintainability")] [assembly: SuppressMessage("Naming", "CA1711:Identifiers should not have incorrect suffix", Justification = "Renaming everything would be a lot of work. It does not do any harm if an EventHandler delegate ends with the suffix EventHandler. Besides this, the Rule causes some false positives.")] [assembly: SuppressMessage("Performance", "CA1838:Avoid 'StringBuilder' parameters for P/Invokes", Justification = "We are not concerned about the performance impact of marshaling a StringBuilder")] -[assembly: SuppressMessage("Performance", "CA1852:Seal internal types", Justification = "The assembly is getting a ComVisible set to false already.", Scope="namespaceanddescendants", Target="MouseWithoutBorders")] +[assembly: SuppressMessage("Performance", "CA1852:Seal internal types", Justification = "The assembly is getting a ComVisible set to false already.", Scope = "namespaceanddescendants", Target = "MouseWithoutBorders")] // Threading suppressions [assembly: SuppressMessage("Microsoft.VisualStudio.Threading.Analyzers", "VSTHRD100:Avoid async void methods", Justification = "Event handlers needs async void", Scope = "member", Target = "~M:Microsoft.Templates.UI.Controls.Notification.OnClose")] @@ -57,8 +57,8 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("CodeQuality", "IDE0076:Invalid global 'SuppressMessageAttribute'", Justification = "Affect predefined suppressions.")] // Dotnet port -[assembly: SuppressMessage("Design", "CA1069:Enums values should not be duplicated", Justification = "", Scope="namespaceanddescendants", Target="MouseWithoutBorders")] -[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "", Scope="namespaceanddescendants", Target="MouseWithoutBorders")] -[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "", Scope="namespaceanddescendants", Target="MouseWithoutBorders")] -[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "", Scope="namespaceanddescendants", Target="MouseWithoutBorders")] -[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "", Scope="namespaceanddescendants", Target="MouseWithoutBorders")] +[assembly: SuppressMessage("Design", "CA1069:Enums values should not be duplicated", Justification = "", Scope = "namespaceanddescendants", Target = "MouseWithoutBorders")] +[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "", Scope = "namespaceanddescendants", Target = "MouseWithoutBorders")] +[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "", Scope = "namespaceanddescendants", Target = "MouseWithoutBorders")] +[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1310:Field names should not contain underscore", Justification = "", Scope = "namespaceanddescendants", Target = "MouseWithoutBorders")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:File name should match first type name", Justification = "", Scope = "namespaceanddescendants", Target = "MouseWithoutBorders")] diff --git a/src/common/Common.UI/SettingsDeepLink.cs b/src/common/Common.UI/SettingsDeepLink.cs index 1a154f0a82..19cee0b7d8 100644 --- a/src/common/Common.UI/SettingsDeepLink.cs +++ b/src/common/Common.UI/SettingsDeepLink.cs @@ -28,6 +28,8 @@ namespace Common.UI PowerOCR, RegistryPreview, CropAndLock, + EnvironmentVariables, + Dashboard, } private static string SettingsWindowNameToString(SettingsWindow value) @@ -68,6 +70,10 @@ namespace Common.UI return "RegistryPreview"; case SettingsWindow.CropAndLock: return "CropAndLock"; + case SettingsWindow.EnvironmentVariables: + return "EnvironmentVariables"; + case SettingsWindow.Dashboard: + return "Dashboard"; default: { return string.Empty; diff --git a/src/common/GPOWrapper/GPOWrapper.cpp b/src/common/GPOWrapper/GPOWrapper.cpp index 9fd9d7d2af..378e3dcb52 100644 --- a/src/common/GPOWrapper/GPOWrapper.cpp +++ b/src/common/GPOWrapper/GPOWrapper.cpp @@ -148,4 +148,8 @@ namespace winrt::PowerToys::GPOWrapper::implementation { return static_cast(powertoys_gpo::getRunPluginEnabledValue(winrt::to_string(pluginID))); } + GpoRuleConfigured GPOWrapper::GetConfiguredEnvironmentVariablesEnabledValue() + { + return static_cast(powertoys_gpo::getConfiguredEnvironmentVariablesEnabledValue()); + } } diff --git a/src/common/GPOWrapper/GPOWrapper.h b/src/common/GPOWrapper/GPOWrapper.h index 34d3f0f5c2..c60f5e78ef 100644 --- a/src/common/GPOWrapper/GPOWrapper.h +++ b/src/common/GPOWrapper/GPOWrapper.h @@ -43,6 +43,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation static GpoRuleConfigured GetDisableAutomaticUpdateDownloadValue(); static GpoRuleConfigured GetAllowExperimentationValue(); static GpoRuleConfigured GetRunPluginEnabledValue(winrt::hstring const& pluginID); + static GpoRuleConfigured GetConfiguredEnvironmentVariablesEnabledValue(); }; } diff --git a/src/common/GPOWrapper/GPOWrapper.idl b/src/common/GPOWrapper/GPOWrapper.idl index 5472ad1e87..748f63db91 100644 --- a/src/common/GPOWrapper/GPOWrapper.idl +++ b/src/common/GPOWrapper/GPOWrapper.idl @@ -47,6 +47,7 @@ namespace PowerToys static GpoRuleConfigured GetDisableAutomaticUpdateDownloadValue(); static GpoRuleConfigured GetAllowExperimentationValue(); static GpoRuleConfigured GetRunPluginEnabledValue(String pluginID); + static GpoRuleConfigured GetConfiguredEnvironmentVariablesEnabledValue(); } } } diff --git a/src/common/interop/interop.cpp b/src/common/interop/interop.cpp index 46ce729219..ebf6c8be22 100644 --- a/src/common/interop/interop.cpp +++ b/src/common/interop/interop.cpp @@ -266,5 +266,13 @@ public static String ^ CropAndLockReparentEvent() { return gcnew String(CommonSharedConstants::CROP_AND_LOCK_REPARENT_EVENT); } + + static String ^ ShowEnvironmentVariablesSharedEvent() { + return gcnew String(CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_EVENT); + } + + static String ^ ShowEnvironmentVariablesAdminSharedEvent() { + return gcnew String(CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_ADMIN_EVENT); + } }; } diff --git a/src/common/interop/shared_constants.h b/src/common/interop/shared_constants.h index 4761d3c22c..3d71b5fdba 100644 --- a/src/common/interop/shared_constants.h +++ b/src/common/interop/shared_constants.h @@ -82,6 +82,10 @@ namespace CommonSharedConstants const wchar_t CROP_AND_LOCK_THUMBNAIL_EVENT[] = L"Local\\PowerToysCropAndLockThumbnailEvent-1637be50-da72-46b2-9220-b32b206b2434"; const wchar_t CROP_AND_LOCK_EXIT_EVENT[] = L"Local\\PowerToysCropAndLockExitEvent-d995d409-7b70-482b-bad6-e7c8666f375a"; + // Path to the events used by EnvironmentVariables + const wchar_t SHOW_ENVIRONMENT_VARIABLES_EVENT[] = L"Local\\PowerToysEnvironmentVariables-ShowEnvironmentVariablesEvent-1021f616-e951-4d64-b231-a8f972159978"; + const wchar_t SHOW_ENVIRONMENT_VARIABLES_ADMIN_EVENT[] = L"Local\\PowerToysEnvironmentVariables-EnvironmentVariablesAdminEvent-8c95d2ad-047c-49a2-9e8b-b4656326cfb2"; + // Max DWORD for key code to disable keys. const DWORD VK_DISABLED = 0x100; } diff --git a/src/common/logger/logger_settings.h b/src/common/logger/logger_settings.h index 8498f9bf4b..038a9a8e20 100644 --- a/src/common/logger/logger_settings.h +++ b/src/common/logger/logger_settings.h @@ -62,6 +62,7 @@ struct LogSettings inline const static std::string registryPreviewLoggerName = "registrypreview"; inline const static std::string cropAndLockLoggerName = "crop-and-lock"; inline const static std::wstring registryPreviewLogPath = L"Logs\\registryPreview-log.txt"; + inline const static std::string environmentVariablesLoggerName = "environment-variables"; 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 bbb6545f85..840fa266f0 100644 --- a/src/common/utils/gpo.h +++ b/src/common/utils/gpo.h @@ -55,6 +55,7 @@ namespace powertoys_gpo { const std::wstring POLICY_CONFIGURE_ENABLED_REGISTRY_PREVIEW = L"ConfigureEnabledUtilityRegistryPreview"; const std::wstring POLICY_CONFIGURE_ENABLED_MOUSE_WITHOUT_BORDERS = L"ConfigureEnabledUtilityMouseWithoutBorders"; const std::wstring POLICY_CONFIGURE_ENABLED_PEEK = L"ConfigureEnabledUtilityPeek"; + const std::wstring POLICY_CONFIGURE_ENABLED_ENVIRONMENT_VARIABLES = L"ConfigureEnabledUtilityEnvironmentVariables"; // The registry value names for PowerToys installer and update policies. const std::wstring POLICY_DISABLE_PER_USER_INSTALLATION = L"PerUserInstallationDisabled"; @@ -376,6 +377,11 @@ namespace powertoys_gpo { return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_REGISTRY_PREVIEW); } + inline gpo_rule_configured_t getConfiguredEnvironmentVariablesEnabledValue() + { + return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_ENVIRONMENT_VARIABLES); + } + inline gpo_rule_configured_t getDisablePerUserInstallationValue() { return getConfiguredValue(POLICY_DISABLE_PER_USER_INSTALLATION); @@ -440,5 +446,4 @@ namespace powertoys_gpo { return getConfiguredValue(POLICY_CONFIGURE_ENABLED_POWER_LAUNCHER_ALL_PLUGINS); } } - } diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/EnvironmentVariables.ico b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/EnvironmentVariables.ico new file mode 100644 index 0000000000..dc07264100 Binary files /dev/null and b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/EnvironmentVariables.ico differ diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/LockScreenLogo.scale-200.png b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/LockScreenLogo.scale-200.png new file mode 100644 index 0000000000..7440f0d4bf Binary files /dev/null and b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/LockScreenLogo.scale-200.png differ diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/ProfileIcon.png b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/ProfileIcon.png new file mode 100644 index 0000000000..7440f0d4bf Binary files /dev/null and b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/ProfileIcon.png differ diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/SplashScreen.scale-200.png b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/SplashScreen.scale-200.png new file mode 100644 index 0000000000..32f486a867 Binary files /dev/null and b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/SplashScreen.scale-200.png differ diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/Square150x150Logo.scale-200.png b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/Square150x150Logo.scale-200.png new file mode 100644 index 0000000000..53ee3777ea Binary files /dev/null and b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/Square150x150Logo.scale-200.png differ diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/Square44x44Logo.scale-200.png b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/Square44x44Logo.scale-200.png new file mode 100644 index 0000000000..f713bba67f Binary files /dev/null and b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/Square44x44Logo.scale-200.png differ diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/Square44x44Logo.targetsize-24_altform-unplated.png b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 0000000000..dc9f5bea0c Binary files /dev/null and b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/StoreLogo.png b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/StoreLogo.png new file mode 100644 index 0000000000..a4586f26bd Binary files /dev/null and b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/StoreLogo.png differ diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/SystemIcon.png b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/SystemIcon.png new file mode 100644 index 0000000000..af9436bd14 Binary files /dev/null and b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/SystemIcon.png differ diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/UserIcon.png b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/UserIcon.png new file mode 100644 index 0000000000..7440f0d4bf Binary files /dev/null and b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/UserIcon.png differ diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/Wide310x150Logo.scale-200.png b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/Wide310x150Logo.scale-200.png new file mode 100644 index 0000000000..8b4a5d0dd5 Binary files /dev/null and b/src/modules/EnvironmentVariables/EnvironmentVariables/Assets/EnvironmentVariables/Wide310x150Logo.scale-200.png differ diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariables.csproj b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariables.csproj new file mode 100644 index 0000000000..8d10c65e2c --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariables.csproj @@ -0,0 +1,114 @@ + + + + + WinExe + net7.0-windows10.0.20348.0 + 10.0.19041.0 + 10.0.19041.0 + EnvironmentVariables + app.manifest + win10-x64;win10-arm64 + true + true + true + None + false + false + true + ..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps + PowerToys.EnvironmentVariables + DISABLE_XAML_GENERATED_MAIN,TRACE + Assets/EnvironmentVariables/EnvironmentVariables.ico + true + + PowerToys.EnvironmentVariables.pri + + + + + + + + + + + + win10-x64 + + + win10-arm64 + + + + + PowerToys.GPOWrapper + $(OutDir) + false + + + + + + + + + + + + + + + + https://pkgs.dev.azure.com/dotnet/CommunityToolkit/_packaging/CommunityToolkit-Labs/nuget/v3/index.json + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MSBuild:Compile + + + + + + diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/App.xaml b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/App.xaml new file mode 100644 index 0000000000..ae8ef32742 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/App.xaml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/App.xaml.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/App.xaml.cs new file mode 100644 index 0000000000..ca2fdb72b8 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/App.xaml.cs @@ -0,0 +1,96 @@ +// 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.IO.Abstractions; +using EnvironmentVariables.Helpers; +using EnvironmentVariables.ViewModels; +using ManagedCommon; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.PowerToys.Telemetry; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml; + +namespace EnvironmentVariables +{ + /// + /// Provides application-specific behavior to supplement the default Application class. + /// + public partial class App : Application + { + public IHost Host { get; } + + public static T GetService() + where T : class + { + if ((App.Current as App)!.Host.Services.GetService(typeof(T)) is not T service) + { + throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices within App.xaml.cs."); + } + + return service; + } + + /// + /// Initializes a new instance of the class. + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + this.InitializeComponent(); + + Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder().UseContentRoot(AppContext.BaseDirectory).ConfigureServices((context, services) => + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + }).Build(); + + UnhandledException += App_UnhandledException; + } + + private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e) + { + Logger.LogError("Unhandled exception", e.Exception); + } + + /// + /// Invoked when the application is launched. + /// + /// Details about the launch request and process. + protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) + { + var cmdArgs = Environment.GetCommandLineArgs(); + if (cmdArgs?.Length > 1) + { + if (int.TryParse(cmdArgs[cmdArgs.Length - 1], out int powerToysRunnerPid)) + { + Logger.LogInfo($"EnvironmentVariables started from the PowerToys Runner. Runner pid={powerToysRunnerPid}."); + + var dispatcher = DispatcherQueue.GetForCurrentThread(); + RunnerHelper.WaitForPowerToysRunner(powerToysRunnerPid, () => + { + Logger.LogInfo("PowerToys Runner exited. Exiting EnvironmentVariables"); + dispatcher.TryEnqueue(App.Current.Exit); + }); + } + } + else + { + Logger.LogInfo($"EnvironmentVariables started detached from PowerToys Runner."); + } + + PowerToysTelemetry.Log.WriteEvent(new EnvironmentVariables.Telemetry.EnvironmentVariablesOpenedEvent()); + + window = new MainWindow(); + window.Activate(); + } + + private Window window; + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/EnvironmentStateToBoolConverter.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/EnvironmentStateToBoolConverter.cs new file mode 100644 index 0000000000..81e1ef1efd --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/EnvironmentStateToBoolConverter.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; +using EnvironmentVariables.Models; +using Microsoft.UI.Xaml.Data; + +namespace EnvironmentVariables.Converters; + +public class EnvironmentStateToBoolConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, string language) + { + var type = (EnvironmentState)value; + return type switch + { + EnvironmentState.Unchanged => false, + _ => true, + }; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/EnvironmentStateToMessageConverter.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/EnvironmentStateToMessageConverter.cs new file mode 100644 index 0000000000..ffe473ef28 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/EnvironmentStateToMessageConverter.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. + +using System; +using EnvironmentVariables.Helpers; +using EnvironmentVariables.Models; +using Microsoft.UI.Xaml.Data; + +namespace EnvironmentVariables.Converters; + +public class EnvironmentStateToMessageConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, string language) + { + var resourceLoader = ResourceLoaderInstance.ResourceLoader; + var type = (EnvironmentState)value; + return type switch + { + EnvironmentState.Unchanged => string.Empty, + EnvironmentState.ChangedOnStartup => resourceLoader.GetString("StateNotUpToDateOnStartupMsg"), + EnvironmentState.EnvironmentMessageReceived => resourceLoader.GetString("StateNotUpToDateEnvironmentMessageReceivedMsg"), + EnvironmentState.ProfileNotApplicable => resourceLoader.GetString("StateProfileNotApplicableMsg"), + _ => throw new NotImplementedException(), + }; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/EnvironmentStateToTitleConverter.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/EnvironmentStateToTitleConverter.cs new file mode 100644 index 0000000000..344c9b1072 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/EnvironmentStateToTitleConverter.cs @@ -0,0 +1,29 @@ +// 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 EnvironmentVariables.Helpers; +using EnvironmentVariables.Models; +using Microsoft.UI.Xaml.Data; + +namespace EnvironmentVariables.Converters; + +public class EnvironmentStateToTitleConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, string language) + { + var resourceLoader = ResourceLoaderInstance.ResourceLoader; + var type = (EnvironmentState)value; + return type switch + { + EnvironmentState.ProfileNotApplicable => resourceLoader.GetString("ProfileNotApplicableTitle"), + _ => resourceLoader.GetString("StateNotUpToDateTitle"), + }; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/EnvironmentStateToVisibilityConverter.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/EnvironmentStateToVisibilityConverter.cs new file mode 100644 index 0000000000..682a93cc13 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/EnvironmentStateToVisibilityConverter.cs @@ -0,0 +1,28 @@ +// 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 EnvironmentVariables.Models; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Data; + +namespace EnvironmentVariables.Converters; + +public class EnvironmentStateToVisibilityConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, string language) + { + var type = (EnvironmentState)value; + return type switch + { + EnvironmentState.Unchanged => Visibility.Collapsed, + _ => Visibility.Visible, + }; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/VariableTypeToGlyphConverter.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/VariableTypeToGlyphConverter.cs new file mode 100644 index 0000000000..fc6ec01f30 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Converters/VariableTypeToGlyphConverter.cs @@ -0,0 +1,31 @@ +// 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 EnvironmentVariables.Models; +using Microsoft.UI.Xaml.Data; + +namespace EnvironmentVariables.Converters; + +public class VariableTypeToGlyphConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, string language) + { + var type = (VariablesSetType)value; + return type switch + { + VariablesSetType.User => "\uE77B", + VariablesSetType.System => "\uE977", + VariablesSetType.Profile => "\uEDE3", + VariablesSetType.Path => "\uE8AC", + VariablesSetType.Duplicate => "\uE8C8", + _ => throw new NotImplementedException(), + }; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + throw new NotImplementedException(); + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/MainWindow.xaml b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/MainWindow.xaml new file mode 100644 index 0000000000..b5d7dafdb1 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/MainWindow.xaml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/MainWindow.xaml.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/MainWindow.xaml.cs new file mode 100644 index 0000000000..ec24920054 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/MainWindow.xaml.cs @@ -0,0 +1,76 @@ +// 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.Runtime.InteropServices; +using EnvironmentVariables.Helpers; +using EnvironmentVariables.Helpers.Win32; +using EnvironmentVariables.ViewModels; +using Microsoft.UI.Dispatching; +using WinUIEx; + +namespace EnvironmentVariables +{ + /// + /// An empty window that can be used on its own or navigated to within a Frame. + /// + public sealed partial class MainWindow : WindowEx + { + public MainWindow() + { + this.InitializeComponent(); + + ExtendsContentIntoTitleBar = true; + SetTitleBar(titleBar); + + AppWindow.SetIcon("Assets/EnvironmentVariables/EnvironmentVariables.ico"); + var loader = ResourceLoaderInstance.ResourceLoader; + var title = App.GetService().IsElevated ? loader.GetString("WindowAdminTitle") : loader.GetString("WindowTitle"); + Title = title; + AppTitleTextBlock.Text = title; + + RegisterWindow(); + } + + private static readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + private static NativeMethods.WinProc newWndProc; + private static IntPtr oldWndProc = IntPtr.Zero; + + private void RegisterWindow() + { + newWndProc = new NativeMethods.WinProc(WndProc); + + var handle = this.GetWindowHandle(); + + oldWndProc = NativeMethods.SetWindowLongPtr(handle, NativeMethods.WindowLongIndexFlags.GWL_WNDPROC, newWndProc); + } + + private static IntPtr WndProc(IntPtr hWnd, NativeMethods.WindowMessage msg, IntPtr wParam, IntPtr lParam) + { + switch (msg) + { + case NativeMethods.WindowMessage.WM_SETTINGSCHANGED: + { + var lParamStr = Marshal.PtrToStringUTF8(lParam); + if (lParamStr == "Environment") + { + // Do not react on self - not nice, re-check this + if (wParam != (IntPtr)0x12345) + { + var viewModel = App.GetService(); + viewModel.EnvironmentState = Models.EnvironmentState.EnvironmentMessageReceived; + } + } + + break; + } + + default: + break; + } + + return NativeMethods.CallWindowProc(oldWndProc, hWnd, msg, wParam, lParam); + } + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Styles/TextBlock.xaml b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Styles/TextBlock.xaml new file mode 100644 index 0000000000..bf6d01383b --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Styles/TextBlock.xaml @@ -0,0 +1,8 @@ + + + 12 + + \ No newline at end of file diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Views/MainPage.xaml b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Views/MainPage.xaml new file mode 100644 index 0000000000..08400e5478 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Views/MainPage.xaml @@ -0,0 +1,744 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Views/MainPage.xaml.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Views/MainPage.xaml.cs new file mode 100644 index 0000000000..7ca3c00456 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/EnvironmentVariablesXAML/Views/MainPage.xaml.cs @@ -0,0 +1,551 @@ +// 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.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Input; +using CommunityToolkit.Mvvm.Input; +using EnvironmentVariables.Models; +using EnvironmentVariables.ViewModels; +using Microsoft.UI.Xaml.Controls; +using Windows.Foundation.Collections; + +namespace EnvironmentVariables.Views +{ + public sealed partial class MainPage : Page + { + private sealed class RelayCommandParameter + { + public RelayCommandParameter(Variable variable, VariablesSet set) + { + Variable = variable; + this.Set = set; + } + + public Variable Variable { get; set; } + + public VariablesSet Set { get; set; } + } + + public MainViewModel ViewModel { get; private set; } + + public ICommand EditCommand => new RelayCommand(EditVariable); + + public ICommand NewProfileCommand => new AsyncRelayCommand(AddProfileAsync); + + public ICommand AddProfileCommand => new RelayCommand(AddProfile); + + public ICommand UpdateProfileCommand => new RelayCommand(UpdateProfile); + + public ICommand AddVariableCommand => new RelayCommand(AddVariable); + + public ICommand CancelAddVariableCommand => new RelayCommand(CancelAddVariable); + + public ICommand AddDefaultVariableCommand => new RelayCommand(AddDefaultVariable); + + public MainPage() + { + this.InitializeComponent(); + ViewModel = App.GetService(); + DataContext = ViewModel; + } + + private async Task ShowEditDialogAsync(Variable variable, VariablesSet parentSet) + { + var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader; + + EditVariableDialog.Title = resourceLoader.GetString("EditVariableDialog_Title"); + EditVariableDialog.PrimaryButtonText = resourceLoader.GetString("SaveBtn"); + EditVariableDialog.SecondaryButtonText = resourceLoader.GetString("CancelBtn"); + EditVariableDialog.PrimaryButtonCommand = EditCommand; + EditVariableDialog.PrimaryButtonCommandParameter = new RelayCommandParameter(variable, parentSet); + + var clone = variable.Clone(); + EditVariableDialog.DataContext = clone; + + await EditVariableDialog.ShowAsync(); + } + + private async void EditVariable_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + var btn = sender as MenuFlyoutItem; + var variablesSet = btn.DataContext as VariablesSet; + var variable = btn.CommandParameter as Variable; + + if (variable != null) + { + await ShowEditDialogAsync(variable, variablesSet); + } + } + + private void EditVariable(RelayCommandParameter param) + { + var variableSet = param.Set as ProfileVariablesSet; + var original = param.Variable; + var edited = EditVariableDialog.DataContext as Variable; + ViewModel.EditVariable(original, edited, variableSet); + } + + private async Task AddProfileAsync() + { + SwitchViewsSegmentedView.SelectedIndex = 0; + + var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader; + AddProfileDialog.Title = resourceLoader.GetString("AddNewProfileDialog_Title"); + AddProfileDialog.PrimaryButtonText = resourceLoader.GetString("AddBtn"); + AddProfileDialog.SecondaryButtonText = resourceLoader.GetString("CancelBtn"); + AddProfileDialog.PrimaryButtonCommand = AddProfileCommand; + AddProfileDialog.DataContext = new ProfileVariablesSet(Guid.NewGuid(), string.Empty); + + await AddProfileDialog.ShowAsync(); + } + + private void AddProfile() + { + var profile = AddProfileDialog.DataContext as ProfileVariablesSet; + ViewModel.AddProfile(profile); + } + + private void UpdateProfile() + { + var updatedProfile = AddProfileDialog.DataContext as ProfileVariablesSet; + ViewModel.UpdateProfile(updatedProfile); + } + + private async void RemoveProfileBtn_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + var button = sender as MenuFlyoutItem; + var profile = button.CommandParameter as ProfileVariablesSet; + + if (profile != null) + { + var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader; + ContentDialog dialog = new ContentDialog(); + dialog.XamlRoot = RootPage.XamlRoot; + dialog.Title = profile.Name; + dialog.PrimaryButtonText = resourceLoader.GetString("Yes"); + dialog.CloseButtonText = resourceLoader.GetString("No"); + dialog.DefaultButton = ContentDialogButton.Primary; + dialog.Content = new TextBlock() { Text = resourceLoader.GetString("Delete_Dialog_Description"), TextWrapping = Microsoft.UI.Xaml.TextWrapping.WrapWholeWords }; + dialog.PrimaryButtonClick += (s, args) => + { + ViewModel.RemoveProfile(profile); + }; + + var result = await dialog.ShowAsync(); + } + } + + private void AddVariable() + { + var profile = AddProfileDialog.DataContext as ProfileVariablesSet; + if (profile != null) + { + if (AddVariableSwitchPresenter.Value as string == "NewVariable") + { + profile.Variables.Add(new Variable(AddNewVariableName.Text, AddNewVariableValue.Text, VariablesSetType.Profile)); + } + else + { + foreach (Variable variable in ExistingVariablesListView.SelectedItems) + { + if (!profile.Variables.Where(x => x.Name == variable.Name).Any()) + { + var clone = variable.Clone(true); + profile.Variables.Add(clone); + } + } + } + } + + AddNewVariableName.Text = string.Empty; + AddNewVariableValue.Text = string.Empty; + ExistingVariablesListView.SelectionChanged -= ExistingVariablesListView_SelectionChanged; + ExistingVariablesListView.SelectedItems.Clear(); + ExistingVariablesListView.SelectionChanged += ExistingVariablesListView_SelectionChanged; + AddVariableFlyout.Hide(); + } + + private void CancelAddVariable() + { + AddNewVariableName.Text = string.Empty; + AddNewVariableValue.Text = string.Empty; + + ExistingVariablesListView.SelectionChanged -= ExistingVariablesListView_SelectionChanged; + ExistingVariablesListView.SelectedItems.Clear(); + ExistingVariablesListView.SelectionChanged += ExistingVariablesListView_SelectionChanged; + + AddVariableFlyout.Hide(); + } + + private void AddDefaultVariable(DefaultVariablesSet set) + { + var variable = AddDefaultVariableDialog.DataContext as Variable; + var type = set.Type; + + ViewModel.AddDefaultVariable(variable, type); + } + + private async void Delete_Variable_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + MenuFlyoutItem selectedItem = sender as MenuFlyoutItem; + var variableSet = selectedItem.DataContext as ProfileVariablesSet; + var variable = selectedItem.CommandParameter as Variable; + + if (variable != null) + { + var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader; + ContentDialog dialog = new ContentDialog(); + dialog.XamlRoot = RootPage.XamlRoot; + dialog.Title = variable.Name; + dialog.PrimaryButtonText = resourceLoader.GetString("Yes"); + dialog.CloseButtonText = resourceLoader.GetString("No"); + dialog.DefaultButton = ContentDialogButton.Primary; + dialog.Content = new TextBlock() { Text = resourceLoader.GetString("Delete_Variable_Description"), TextWrapping = Microsoft.UI.Xaml.TextWrapping.WrapWholeWords }; + dialog.PrimaryButtonClick += (s, args) => + { + ViewModel.DeleteVariable(variable, variableSet); + }; + var result = await dialog.ShowAsync(); + } + } + + private void AddNewVariableName_TextChanged(object sender, TextChangedEventArgs e) + { + TextBox nameTxtBox = sender as TextBox; + var profile = AddProfileDialog.DataContext as ProfileVariablesSet; + + if (nameTxtBox != null) + { + if (nameTxtBox.Text.Length == 0 || nameTxtBox.Text.Length >= 255 || profile.Variables.Where(x => x.Name.Equals(nameTxtBox.Text, StringComparison.OrdinalIgnoreCase)).Any()) + { + ConfirmAddVariableBtn.IsEnabled = false; + } + else + { + ConfirmAddVariableBtn.IsEnabled = true; + } + } + } + + private void ReloadButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + ViewModel.LoadEnvironmentVariables(); + ViewModel.EnvironmentState = EnvironmentState.Unchanged; + } + + private void ExistingVariablesListView_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + var profile = AddProfileDialog.DataContext as ProfileVariablesSet; + + int toRemove = -1; + + if (e.AddedItems.Count > 0) + { + var list = sender as ListView; + var duplicates = list.SelectedItems.GroupBy(x => ((Variable)x).Name.ToLowerInvariant()).Where(g => g.Count() > 1).ToList(); + + foreach (var dup in duplicates) + { + ExistingVariablesListView.SelectionChanged -= ExistingVariablesListView_SelectionChanged; + list.SelectedItems.Remove(dup.ElementAt(1)); + ExistingVariablesListView.SelectionChanged += ExistingVariablesListView_SelectionChanged; + } + } + + if (e.RemovedItems.Count > 0) + { + Variable removedVariable = e.RemovedItems[0] as Variable; + for (int i = 0; i < profile.Variables.Count; i++) + { + if (profile.Variables[i].Name == removedVariable.Name && profile.Variables[i].Values == removedVariable.Values) + { + toRemove = i; + break; + } + } + + if (toRemove != -1) + { + profile.Variables.RemoveAt(toRemove); + } + } + + ConfirmAddVariableBtn.IsEnabled = false; + foreach (Variable variable in ExistingVariablesListView.SelectedItems) + { + if (variable != null) + { + if (!profile.Variables.Where(x => x.Name.Equals(variable.Name, StringComparison.Ordinal) && x.Values.Equals(variable.Values, StringComparison.Ordinal)).Any()) + { + ConfirmAddVariableBtn.IsEnabled = true; + break; + } + } + } + } + + private async void EditProfileBtn_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + SwitchViewsSegmentedView.SelectedIndex = 0; + + var button = sender as MenuFlyoutItem; + var profile = button.CommandParameter as ProfileVariablesSet; + + if (profile != null) + { + var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader; + AddProfileDialog.Title = resourceLoader.GetString("EditProfileDialog_Title"); + AddProfileDialog.PrimaryButtonText = resourceLoader.GetString("SaveBtn"); + AddProfileDialog.SecondaryButtonText = resourceLoader.GetString("CancelBtn"); + AddProfileDialog.PrimaryButtonCommand = UpdateProfileCommand; + AddProfileDialog.DataContext = profile.Clone(); + await AddProfileDialog.ShowAsync(); + } + } + + private void ExistingVariablesListView_Loaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + var profile = AddProfileDialog.DataContext as ProfileVariablesSet; + + foreach (Variable item in ExistingVariablesListView.Items) + { + if (item != null) + { + foreach (var profileItem in profile.Variables) + { + if (profileItem.Name == item.Name && profileItem.Values == item.Values) + { + if (ExistingVariablesListView.SelectedItems.Where(x => ((Variable)x).Name.Equals(profileItem.Name, StringComparison.OrdinalIgnoreCase)).Any()) + { + continue; + } + + ExistingVariablesListView.SelectionChanged -= ExistingVariablesListView_SelectionChanged; + ExistingVariablesListView.SelectedItems.Add(item); + ExistingVariablesListView.SelectionChanged += ExistingVariablesListView_SelectionChanged; + } + } + } + } + } + + private async Task ShowAddDefaultVariableDialogAsync(DefaultVariablesSet set) + { + var resourceLoader = Helpers.ResourceLoaderInstance.ResourceLoader; + + AddDefaultVariableDialog.Title = resourceLoader.GetString("AddVariable_Title"); + AddDefaultVariableDialog.PrimaryButtonText = resourceLoader.GetString("SaveBtn"); + AddDefaultVariableDialog.SecondaryButtonText = resourceLoader.GetString("CancelBtn"); + AddDefaultVariableDialog.PrimaryButtonCommand = AddDefaultVariableCommand; + AddDefaultVariableDialog.PrimaryButtonCommandParameter = set; + + var variableType = set.Id == VariablesSet.SystemGuid ? VariablesSetType.System : VariablesSetType.User; + AddDefaultVariableDialog.DataContext = new Variable(string.Empty, string.Empty, variableType); + + await AddDefaultVariableDialog.ShowAsync(); + } + + private async void AddDefaultVariableBtn_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + var button = sender as Button; + var defaultVariableSet = button.CommandParameter as DefaultVariablesSet; + + if (defaultVariableSet != null) + { + await ShowAddDefaultVariableDialogAsync(defaultVariableSet); + } + } + + private void EditVariableDialogNameTxtBox_TextChanged(object sender, TextChangedEventArgs e) + { + var variable = EditVariableDialog.DataContext as Variable; + var param = EditVariableDialog.PrimaryButtonCommandParameter as RelayCommandParameter; + var variableSet = param.Set; + + if (variableSet == null) + { + // default set + variableSet = variable.ParentType == VariablesSetType.User ? ViewModel.UserDefaultSet : ViewModel.SystemDefaultSet; + } + + if (variableSet != null) + { + if (variableSet.Variables.Where(x => x.Name.Equals(EditVariableDialogNameTxtBox.Text, StringComparison.OrdinalIgnoreCase)).Any() || !variable.Valid) + { + EditVariableDialog.IsPrimaryButtonEnabled = false; + } + else + { + EditVariableDialog.IsPrimaryButtonEnabled = true; + } + } + + if (!variable.Validate()) + { + EditVariableDialog.IsPrimaryButtonEnabled = false; + } + } + + private void AddDefaultVariableNameTxtBox_TextChanged(object sender, TextChangedEventArgs e) + { + TextBox nameTxtBox = sender as TextBox; + var variable = AddDefaultVariableDialog.DataContext as Variable; + var defaultSet = variable.ParentType == VariablesSetType.User ? ViewModel.UserDefaultSet : ViewModel.SystemDefaultSet; + + if (nameTxtBox != null) + { + if (nameTxtBox.Text.Length == 0 || defaultSet.Variables.Where(x => x.Name.Equals(nameTxtBox.Text, StringComparison.OrdinalIgnoreCase)).Any()) + { + AddDefaultVariableDialog.IsPrimaryButtonEnabled = false; + } + else + { + AddDefaultVariableDialog.IsPrimaryButtonEnabled = true; + } + } + + if (!variable.Validate()) + { + AddDefaultVariableDialog.IsPrimaryButtonEnabled = false; + } + } + + private void EditVariableDialogValueTxtBox_TextChanged(object sender, TextChangedEventArgs e) + { + var txtBox = sender as TextBox; + var variable = EditVariableDialog.DataContext as Variable; + EditVariableDialog.IsPrimaryButtonEnabled = true; + + variable.ValuesList = Variable.ValuesStringToValuesListItemCollection(txtBox.Text); + } + + private void ReorderButtonUp_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + var listItem = ((MenuFlyoutItem)sender).DataContext as Variable.ValuesListItem; + if (listItem == null) + { + return; + } + + var variable = EditVariableDialog.DataContext as Variable; + + var index = variable.ValuesList.IndexOf(listItem); + if (index > 0) + { + variable.ValuesList.Move(index, index - 1); + } + + var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray()); + EditVariableDialogValueTxtBox.Text = newValues; + } + + private void ReorderButtonDown_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + var listItem = ((MenuFlyoutItem)sender).DataContext as Variable.ValuesListItem; + if (listItem == null) + { + return; + } + + var variable = EditVariableDialog.DataContext as Variable; + var btn = EditVariableDialog.PrimaryButtonCommandParameter as Button; + + var index = variable.ValuesList.IndexOf(listItem); + if (index < variable.ValuesList.Count - 1) + { + variable.ValuesList.Move(index, index + 1); + } + + var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray()); + EditVariableDialogValueTxtBox.Text = newValues; + } + + private void RemoveListVariableButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + var listItem = ((MenuFlyoutItem)sender).DataContext as Variable.ValuesListItem; + if (listItem == null) + { + return; + } + + var variable = EditVariableDialog.DataContext as Variable; + variable.ValuesList.Remove(listItem); + + var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray()); + EditVariableDialogValueTxtBox.Text = newValues; + } + + private void InsertListEntryBeforeButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + var listItem = (sender as MenuFlyoutItem)?.DataContext as Variable.ValuesListItem; + if (listItem == null) + { + return; + } + + var variable = EditVariableDialog.DataContext as Variable; + var index = variable.ValuesList.IndexOf(listItem); + variable.ValuesList.Insert(index, new Variable.ValuesListItem { Text = string.Empty }); + + var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray()); + EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged; + EditVariableDialogValueTxtBox.Text = newValues; + EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged; + } + + private void InsertListEntryAfterButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + var listItem = (sender as MenuFlyoutItem)?.DataContext as Variable.ValuesListItem; + if (listItem == null) + { + return; + } + + var variable = EditVariableDialog.DataContext as Variable; + var index = variable.ValuesList.IndexOf(listItem); + variable.ValuesList.Insert(index + 1, new Variable.ValuesListItem { Text = string.Empty }); + + var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray()); + EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged; + EditVariableDialogValueTxtBox.Text = newValues; + EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged; + } + + private void EditVariableValuesListTextBox_LostFocus(object sender, Microsoft.UI.Xaml.RoutedEventArgs e) + { + var listItem = (sender as TextBox)?.DataContext as Variable.ValuesListItem; + if (listItem == null) + { + return; + } + + if (listItem.Text == (sender as TextBox)?.Text) + { + return; + } + + listItem.Text = (sender as TextBox)?.Text; + var variable = EditVariableDialog.DataContext as Variable; + + var newValues = string.Join(";", variable.ValuesList?.Select(x => x.Text).ToArray()); + EditVariableDialogValueTxtBox.TextChanged -= EditVariableDialogValueTxtBox_TextChanged; + EditVariableDialogValueTxtBox.Text = newValues; + EditVariableDialogValueTxtBox.TextChanged += EditVariableDialogValueTxtBox_TextChanged; + } + + private void InvalidStateInfoBar_CloseButtonClick(InfoBar sender, object args) + { + ViewModel.EnvironmentState = EnvironmentState.Unchanged; + } + + private void AddVariableFlyout_Closed(object sender, object e) + { + CancelAddVariable(); + ConfirmAddVariableBtn.IsEnabled = false; + } + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/ElevationHelper.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/ElevationHelper.cs new file mode 100644 index 0000000000..9b3bc16e2f --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/ElevationHelper.cs @@ -0,0 +1,20 @@ +// 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.Security.Principal; + +namespace EnvironmentVariables.Helpers +{ + public class ElevationHelper : IElevationHelper + { + private readonly bool _isElevated; + + public bool IsElevated => _isElevated; + + public ElevationHelper() + { + _isElevated = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); + } + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/EnvironmentVariablesHelper.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/EnvironmentVariablesHelper.cs new file mode 100644 index 0000000000..4933050e82 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/EnvironmentVariablesHelper.cs @@ -0,0 +1,189 @@ +// 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 EnvironmentVariables.Helpers.Win32; +using EnvironmentVariables.Models; +using ManagedCommon; +using Microsoft.Win32; + +namespace EnvironmentVariables.Helpers +{ + internal sealed class EnvironmentVariablesHelper + { + internal static string GetBackupVariableName(Variable variable, string profileName) + { + return variable.Name + "_PowerToys_" + profileName; + } + + internal static Variable GetExisting(string variableName) + { + var userVariables = Environment.GetEnvironmentVariables(EnvironmentVariableTarget.User); + + foreach (DictionaryEntry variable in userVariables) + { + var key = variable.Key as string; + if (key.Equals(variableName, StringComparison.OrdinalIgnoreCase)) + { + return new Variable(key, userVariables[key] as string, VariablesSetType.User); + } + } + + var systemVariables = Environment.GetEnvironmentVariables(EnvironmentVariableTarget.Machine); + + foreach (DictionaryEntry variable in systemVariables) + { + var key = variable.Key as string; + if (key.Equals(variableName, StringComparison.OrdinalIgnoreCase)) + { + return new Variable(key, systemVariables[key] as string, VariablesSetType.System); + } + } + + return null; + } + + private static RegistryKey OpenEnvironmentKeyIfExists(bool fromMachine, bool writable) + { + RegistryKey baseKey; + string keyName; + + if (fromMachine) + { + baseKey = Registry.LocalMachine; + keyName = @"System\CurrentControlSet\Control\Session Manager\Environment"; + } + else + { + baseKey = Registry.CurrentUser; + keyName = "Environment"; + } + + return baseKey.OpenSubKey(keyName, writable: writable); + } + + private static void SetEnvironmentVariableFromRegistryWithoutNotify(string variable, string value, bool fromMachine) + { + const int MaxUserEnvVariableLength = 255; // User-wide env vars stored in the registry have names limited to 255 chars + if (!fromMachine && variable.Length >= MaxUserEnvVariableLength) + { + Logger.LogError("Can't apply variable - name too long."); + return; + } + + using (RegistryKey environmentKey = OpenEnvironmentKeyIfExists(fromMachine, writable: true)) + { + if (environmentKey != null) + { + if (value == null) + { + environmentKey.DeleteValue(variable, throwOnMissingValue: false); + } + else + { + environmentKey.SetValue(variable, value); + } + } + } + } + + internal static void NotifyEnvironmentChange() + { + unsafe + { + // send a WM_SETTINGCHANGE message to all windows + fixed (char* lParam = "Environment") + { + _ = NativeMethods.SendNotifyMessage(new IntPtr(NativeMethods.HWND_BROADCAST), NativeMethods.WindowMessage.WM_SETTINGSCHANGED, (IntPtr)0x12345, (IntPtr)lParam); + } + } + } + + internal static void GetVariables(EnvironmentVariableTarget target, VariablesSet set) + { + var variables = Environment.GetEnvironmentVariables(target); + var sortedList = new SortedList(); + + foreach (DictionaryEntry variable in variables) + { + string key = variable.Key as string; + string value = variable.Value as string; + + if (string.IsNullOrEmpty(key)) + { + continue; + } + + Variable entry = new Variable(key, value, set.Type); + sortedList.Add(key, entry); + } + + set.Variables = new System.Collections.ObjectModel.ObservableCollection(sortedList.Values); + } + + internal static bool SetVariableWithoutNotify(Variable variable) + { + bool fromMachine = variable.ParentType switch + { + VariablesSetType.Profile => false, + VariablesSetType.User => false, + VariablesSetType.System => true, + _ => throw new NotImplementedException(), + }; + + SetEnvironmentVariableFromRegistryWithoutNotify(variable.Name, variable.Values, fromMachine); + + return true; + } + + internal static bool SetVariable(Variable variable) + { + bool fromMachine = variable.ParentType switch + { + VariablesSetType.Profile => false, + VariablesSetType.User => false, + VariablesSetType.System => true, + _ => throw new NotImplementedException(), + }; + + SetEnvironmentVariableFromRegistryWithoutNotify(variable.Name, variable.Values, fromMachine); + NotifyEnvironmentChange(); + + return true; + } + + internal static bool UnsetVariableWithoutNotify(Variable variable) + { + bool fromMachine = variable.ParentType switch + { + VariablesSetType.Profile => false, + VariablesSetType.User => false, + VariablesSetType.System => true, + _ => throw new NotImplementedException(), + }; + + SetEnvironmentVariableFromRegistryWithoutNotify(variable.Name, null, fromMachine); + + return true; + } + + internal static bool UnsetVariable(Variable variable) + { + bool fromMachine = variable.ParentType switch + { + VariablesSetType.Profile => false, + VariablesSetType.User => false, + VariablesSetType.System => true, + _ => throw new NotImplementedException(), + }; + + SetEnvironmentVariableFromRegistryWithoutNotify(variable.Name, null, fromMachine); + NotifyEnvironmentChange(); + + return true; + } + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/EnvironmentVariablesService.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/EnvironmentVariablesService.cs new file mode 100644 index 0000000000..6fde9872fc --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/EnvironmentVariablesService.cs @@ -0,0 +1,60 @@ +// 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 System.IO.Abstractions; +using System.Text.Json; +using System.Threading.Tasks; +using EnvironmentVariables.Models; + +namespace EnvironmentVariables.Helpers +{ + internal sealed class EnvironmentVariablesService : IEnvironmentVariablesService + { + private const string ProfilesJsonFileSubPath = "Microsoft\\PowerToys\\EnvironmentVariables\\"; + + private readonly string _profilesJsonFilePath; + + private readonly IFileSystem _fileSystem; + + private readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions + { + WriteIndented = true, + }; + + public string ProfilesJsonFilePath => _profilesJsonFilePath; + + public EnvironmentVariablesService(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + + _profilesJsonFilePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), ProfilesJsonFileSubPath, "profiles.json"); + } + + public void Dispose() + { + } + + public List ReadProfiles() + { + if (!_fileSystem.File.Exists(ProfilesJsonFilePath)) + { + return new List(); + } + + var fileContent = _fileSystem.File.ReadAllText(ProfilesJsonFilePath); + var profiles = JsonSerializer.Deserialize>(fileContent); + + return profiles; + } + + public async Task WriteAsync(IEnumerable profiles) + { + string jsonData = JsonSerializer.Serialize(profiles, _serializerOptions); + await _fileSystem.File.WriteAllTextAsync(ProfilesJsonFilePath, jsonData); + } + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/IElevationHelper.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/IElevationHelper.cs new file mode 100644 index 0000000000..0b27b0a544 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/IElevationHelper.cs @@ -0,0 +1,11 @@ +// 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 EnvironmentVariables.Helpers +{ + public interface IElevationHelper + { + bool IsElevated { get; } + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/IEnvironmentVariablesService.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/IEnvironmentVariablesService.cs new file mode 100644 index 0000000000..5a1604ab1b --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/IEnvironmentVariablesService.cs @@ -0,0 +1,20 @@ +// 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.Threading.Tasks; +using EnvironmentVariables.Models; + +namespace EnvironmentVariables.Helpers +{ + public interface IEnvironmentVariablesService : IDisposable + { + string ProfilesJsonFilePath { get; } + + List ReadProfiles(); + + Task WriteAsync(IEnumerable profiles); + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/NativeMethods.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/NativeMethods.cs new file mode 100644 index 0000000000..1ca3e2428c --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/NativeMethods.cs @@ -0,0 +1,54 @@ +// 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.Runtime.InteropServices; + +namespace EnvironmentVariables.Helpers.Win32 +{ + public static class NativeMethods + { + internal const int HWND_BROADCAST = 0xffff; + + internal delegate IntPtr WinProc(IntPtr hWnd, WindowMessage msg, IntPtr wParam, IntPtr lParam); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] + internal static extern int SendNotifyMessage(IntPtr hWnd, WindowMessage msg, IntPtr wParam, IntPtr lParam); + + [DllImport("User32.dll")] + internal static extern int GetDpiForWindow(IntPtr hwnd); + + [DllImport("user32.dll", EntryPoint = "SetWindowLong")] + internal static extern int SetWindowLong32(IntPtr hWnd, WindowLongIndexFlags nIndex, WinProc newProc); + + [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")] + internal static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, WindowLongIndexFlags nIndex, WinProc newProc); + + [DllImport("user32.dll")] + internal static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, WindowMessage msg, IntPtr wParam, IntPtr lParam); + + [Flags] + internal enum WindowLongIndexFlags : int + { + GWL_WNDPROC = -4, + } + + internal enum WindowMessage : int + { + WM_SETTINGSCHANGED = 0x001A, + } + + internal static IntPtr SetWindowLongPtr(IntPtr hWnd, WindowLongIndexFlags nIndex, WinProc newProc) + { + if (IntPtr.Size == 8) + { + return SetWindowLongPtr64(hWnd, nIndex, newProc); + } + else + { + return new IntPtr(SetWindowLong32(hWnd, nIndex, newProc)); + } + } + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/ResourceLoaderInstance.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/ResourceLoaderInstance.cs new file mode 100644 index 0000000000..b0a11730b9 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Helpers/ResourceLoaderInstance.cs @@ -0,0 +1,17 @@ +// 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 Microsoft.Windows.ApplicationModel.Resources; + +namespace EnvironmentVariables.Helpers +{ + internal static class ResourceLoaderInstance + { + internal static ResourceLoader ResourceLoader { get; private set; } + + static ResourceLoaderInstance() + { + ResourceLoader = new ResourceLoader("PowerToys.EnvironmentVariables.pri"); + } + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Models/DefaultVariablesSet.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Models/DefaultVariablesSet.cs new file mode 100644 index 0000000000..9c07c1df3b --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Models/DefaultVariablesSet.cs @@ -0,0 +1,16 @@ +// 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 EnvironmentVariables.Models +{ + public class DefaultVariablesSet : VariablesSet + { + public DefaultVariablesSet(Guid id, string name, VariablesSetType type) + : base(id, name, type) + { + } + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Models/EnvironmentState.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Models/EnvironmentState.cs new file mode 100644 index 0000000000..12837055cd --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Models/EnvironmentState.cs @@ -0,0 +1,14 @@ +// 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 EnvironmentVariables.Models +{ + public enum EnvironmentState + { + Unchanged = 0, + ChangedOnStartup, + EnvironmentMessageReceived, + ProfileNotApplicable, + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Models/ProfileVariablesSet.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Models/ProfileVariablesSet.cs new file mode 100644 index 0000000000..db9306efa7 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Models/ProfileVariablesSet.cs @@ -0,0 +1,164 @@ +// 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.ObjectModel; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using EnvironmentVariables.Helpers; +using ManagedCommon; + +namespace EnvironmentVariables.Models +{ + public partial class ProfileVariablesSet : VariablesSet + { + [ObservableProperty] + private bool _isEnabled; + + public ProfileVariablesSet() + : base() + { + Type = VariablesSetType.Profile; + IconPath = ProfileIconPath; + } + + public ProfileVariablesSet(Guid id, string name) + : base(id, name, VariablesSetType.Profile) + { + IconPath = ProfileIconPath; + } + + public Task Apply() + { + return Task.Run(() => + { + foreach (var variable in Variables) + { + var applyToSystem = variable.ApplyToSystem; + + // Get existing variable with the same name if it exist + var variableToOverride = EnvironmentVariablesHelper.GetExisting(variable.Name); + + // It exists. Rename it to preserve it. + if (variableToOverride != null && variableToOverride.ParentType == VariablesSetType.User) + { + variableToOverride.Name = EnvironmentVariablesHelper.GetBackupVariableName(variableToOverride, this.Name); + + // Backup the variable + if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variableToOverride)) + { + Logger.LogError("Failed to set backup variable."); + } + } + + if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variable)) + { + Logger.LogError("Failed to set profile variable."); + } + } + + EnvironmentVariablesHelper.NotifyEnvironmentChange(); + }); + } + + public Task UnApply() + { + return Task.Run(() => + { + foreach (var variable in Variables) + { + UnapplyVariable(variable); + } + + EnvironmentVariablesHelper.NotifyEnvironmentChange(); + }); + } + + public void UnapplyVariable(Variable variable) + { + // Unset the variable + if (!EnvironmentVariablesHelper.UnsetVariableWithoutNotify(variable)) + { + Logger.LogError("Failed to unset variable."); + } + + var originalName = variable.Name; + var backupName = EnvironmentVariablesHelper.GetBackupVariableName(variable, this.Name); + + // Get backup variable if it exist + var backupVariable = EnvironmentVariablesHelper.GetExisting(backupName); + + if (backupVariable != null) + { + var variableToRestore = new Variable(originalName, backupVariable.Values, backupVariable.ParentType); + + if (!EnvironmentVariablesHelper.UnsetVariableWithoutNotify(backupVariable)) + { + Logger.LogError("Failed to unset backup variable."); + } + + if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variableToRestore)) + { + Logger.LogError("Failed to restore backup variable."); + } + } + } + + public bool IsCorrectlyApplied() + { + if (!IsEnabled) + { + return false; + } + + foreach (var variable in Variables) + { + var applied = EnvironmentVariablesHelper.GetExisting(variable.Name); + if (applied != null && applied.Values == variable.Values && applied.ParentType == VariablesSetType.User) + { + continue; + } + + return false; + } + + return true; + } + + public bool IsApplicable() + { + foreach (var variable in Variables) + { + if (!variable.Validate()) + { + return false; + } + + // Get existing variable with the same name if it exist + var variableToOverride = EnvironmentVariablesHelper.GetExisting(variable.Name); + + // It exists. Backup is needed. + if (variableToOverride != null && variableToOverride.ParentType == VariablesSetType.User) + { + variableToOverride.Name = EnvironmentVariablesHelper.GetBackupVariableName(variableToOverride, this.Name); + if (!variableToOverride.Validate()) + { + return false; + } + } + } + + return true; + } + + public ProfileVariablesSet Clone() + { + var clone = new ProfileVariablesSet(this.Id, this.Name); + clone.Variables = new ObservableCollection(this.Variables); + clone.IsEnabled = this.IsEnabled; + + return clone; + } + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Models/Variable.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Models/Variable.cs new file mode 100644 index 0000000000..bb93189124 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Models/Variable.cs @@ -0,0 +1,202 @@ +// 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.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using EnvironmentVariables.Helpers; +using ManagedCommon; + +namespace EnvironmentVariables.Models +{ + public partial class Variable : ObservableObject, IJsonOnDeserialized + { + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(Valid))] + [NotifyPropertyChangedFor(nameof(ShowAsList))] + private string _name; + + [ObservableProperty] + private string _values; + + [ObservableProperty] + private bool _applyToSystem; + + [JsonIgnore] + public bool IsEditable + { + get + { + return ParentType != VariablesSetType.System || App.GetService().IsElevated; + } + } + + [JsonIgnore] + public VariablesSetType ParentType { get; set; } + + // To store the strings in the Values List with actual objects that can be referenced and identity compared + public class ValuesListItem + { + public string Text { get; set; } + } + + [ObservableProperty] + [property: JsonIgnore] + [JsonIgnore] + private ObservableCollection _valuesList; + + [JsonIgnore] + public bool Valid => Validate(); + + [JsonIgnore] + public bool ShowAsList => IsList(); + + private bool IsList() + { + List listVariables = new() { "PATH", "PATHEXT", "PSMODULEPATH" }; + + foreach (var name in listVariables) + { + if (Name.Equals(name, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + public void OnDeserialized() + { + // No need to save ValuesList to the Json, so we are generating it after deserializing + ValuesList = ValuesStringToValuesListItemCollection(Values); + } + + public Variable() + { + } + + public Variable(string name, string values, VariablesSetType parentType) + { + Name = name; + Values = values; + ParentType = parentType; + + ValuesList = ValuesStringToValuesListItemCollection(Values); + } + + internal static ObservableCollection ValuesStringToValuesListItemCollection(string values) + { + return new ObservableCollection(values.Split(';').Select(x => new ValuesListItem { Text = x })); + } + + internal Task Update(Variable edited, bool propagateChange, ProfileVariablesSet parentProfile) + { + bool nameChanged = Name != edited.Name; + + var clone = this.Clone(); + + // Update state + Name = edited.Name; + Values = edited.Values; + + ValuesList = ValuesStringToValuesListItemCollection(Values); + + return Task.Run(() => + { + // Apply changes + if (propagateChange) + { + if (nameChanged) + { + if (!EnvironmentVariablesHelper.UnsetVariable(clone)) + { + Logger.LogError("Failed to unset original variable."); + } + + if (parentProfile != null) + { + var backupName = EnvironmentVariablesHelper.GetBackupVariableName(clone, parentProfile.Name); + + // Get backup variable if it exist + var backupVariable = EnvironmentVariablesHelper.GetExisting(backupName); + if (backupVariable != null) + { + var variableToRestore = new Variable(clone.Name, backupVariable.Values, backupVariable.ParentType); + + if (!EnvironmentVariablesHelper.UnsetVariableWithoutNotify(backupVariable)) + { + Logger.LogError("Failed to unset backup variable."); + } + + if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variableToRestore)) + { + Logger.LogError("Failed to restore backup variable."); + } + } + } + } + + // Get existing variable with the same name if it exist + var variableToOverride = EnvironmentVariablesHelper.GetExisting(Name); + + // It exists. Rename it to preserve it. + if (variableToOverride != null && variableToOverride.ParentType == VariablesSetType.User && parentProfile != null) + { + // Gets which name the backup variable should have. + variableToOverride.Name = EnvironmentVariablesHelper.GetBackupVariableName(variableToOverride, parentProfile.Name); + + // Only create a backup variable if there's not one already, to avoid overriding. (solves Path nuking errors, for example, after editing path on an enabled profile) + if (EnvironmentVariablesHelper.GetExisting(variableToOverride.Name) == null) + { + // Backup the variable + if (!EnvironmentVariablesHelper.SetVariableWithoutNotify(variableToOverride)) + { + Logger.LogError("Failed to set backup variable."); + } + } + } + + if (!EnvironmentVariablesHelper.SetVariable(this)) + { + Logger.LogError("Failed to set new variable."); + } + } + }); + } + + internal Variable Clone(bool profile = false) + { + return new Variable + { + Name = Name, + Values = Values, + ParentType = profile ? VariablesSetType.Profile : ParentType, + ValuesList = ValuesStringToValuesListItemCollection(Values), + }; + } + + public bool Validate() + { + if (string.IsNullOrWhiteSpace(Name)) + { + return false; + } + + const int MaxUserEnvVariableLength = 255; // User-wide env vars stored in the registry have names limited to 255 chars + if (ParentType != VariablesSetType.System && Name.Length >= MaxUserEnvVariableLength) + { + Logger.LogError("Variable name too long."); + return false; + } + + return true; + } + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Models/VariablesSet.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Models/VariablesSet.cs new file mode 100644 index 0000000000..a1e352d4e9 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Models/VariablesSet.cs @@ -0,0 +1,70 @@ +// 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.ObjectModel; +using System.Linq; +using System.Text.Json.Serialization; +using CommunityToolkit.Mvvm.ComponentModel; +using EnvironmentVariables.ViewModels; + +namespace EnvironmentVariables.Models +{ + public partial class VariablesSet : ObservableObject + { + public static readonly Guid UserGuid = new Guid("92F7AA9A-AE31-49CD-83C8-80A71E432AA5"); + public static readonly Guid SystemGuid = new Guid("F679C74D-DB00-4795-92E1-B1F6A4833279"); + + private static readonly string UserIconPath = "/Assets/EnvironmentVariables/UserIcon.png"; + private static readonly string SystemIconPath = "/Assets/EnvironmentVariables/SystemIcon.png"; + protected static readonly string ProfileIconPath = "/Assets/EnvironmentVariables/ProfileIcon.png"; + + public Guid Id { get; set; } + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(Valid))] + private string _name; + + [JsonIgnore] + public VariablesSetType Type { get; set; } + + [JsonIgnore] + public string IconPath { get; set; } + + [ObservableProperty] + private ObservableCollection _variables; + + public bool Valid => Validate(); + + public VariablesSet() + { + } + + public VariablesSet(Guid id, string name, VariablesSetType type) + { + Id = id; + Name = name; + Type = type; + Variables = new ObservableCollection(); + + IconPath = Type switch + { + VariablesSetType.User => UserIconPath, + VariablesSetType.System => SystemIconPath, + VariablesSetType.Profile => ProfileIconPath, + _ => throw new NotImplementedException(), + }; + } + + private bool Validate() + { + if (string.IsNullOrWhiteSpace(Name)) + { + return false; + } + + return true; + } + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Models/VariablesSetType.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Models/VariablesSetType.cs new file mode 100644 index 0000000000..6b59b05b68 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Models/VariablesSetType.cs @@ -0,0 +1,15 @@ +// 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 EnvironmentVariables.Models +{ + public enum VariablesSetType + { + Path = 0, + Duplicate, + Profile, + User, + System, + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Program.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Program.cs new file mode 100644 index 0000000000..cc8349fe7c --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Program.cs @@ -0,0 +1,45 @@ +// 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.Threading; +using ManagedCommon; +using Microsoft.UI.Dispatching; +using Microsoft.Windows.AppLifecycle; + +namespace EnvironmentVariables +{ + public static class Program + { + [STAThread] + public static void Main(string[] args) + { + Logger.InitializeLogger("\\EnvironmentVariables\\Logs"); + + WinRT.ComWrappersSupport.InitializeComWrappers(); + + if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredEnvironmentVariablesEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled) + { + Logger.LogWarning("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator."); + return; + } + + var instanceKey = AppInstance.FindOrRegisterForKey("PowerToys_EnvironmentVariables_Instance"); + + if (instanceKey.IsCurrent) + { + Microsoft.UI.Xaml.Application.Start((p) => + { + var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread()); + SynchronizationContext.SetSynchronizationContext(context); + _ = new App(); + }); + } + else + { + Logger.LogWarning("Another instance of Environment Variables is running. Exiting."); + } + } + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Strings/en-us/Resources.resw b/src/modules/EnvironmentVariables/EnvironmentVariables/Strings/en-us/Resources.resw new file mode 100644 index 0000000000..f7310f306c --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Strings/en-us/Resources.resw @@ -0,0 +1,281 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Default variables + + + Default variables + + + New profile + + + You can create profiles to quickly apply a set of preconfigured variables + + + Profiles + + + System + + + User + + + Environment Variables + Title of the window when running as user + + + Cancel + + + Edit variable + + + Name + + + Save + + + Value + + + Save + + + New profile + + + Enabled + + + Add variable + + + Variable name + + + Variable value + + + Existing + + + New + + + Administrator: Environment Variables + Title of the window when running as administrator + + + Cancel + + + Add + + + List of applied variables + + + Applied variables + + + Variables + + + Delete + + + Are you sure you want to delete this profile? Deleting applied profile will remove all profile variables. + + + Administrator permissions are required to edit System variables + + + No + + + Yes + + + Changes were made outside of this app. + + + Variables included in applied profile have been modified. Review the latest changes before applying the profile again. + + + Cancel + + + Variables have been modified. Reload to get the latest changes. + + + Add variable + + + Are you sure you want to delete this variable? + + + Edit profile + + + Add variable + + + Add, remove or edit USER and SYSTEM variables + + + Edit + + + More options + + + Move down + + + Move up + + + Insert Before + + + Insert After + + + Apply to SYSTEM? + + + Delete + + + Remove + + + Add variable + + + Profile can not be applied. + + + Variables or backup variables are invalid. + + \ No newline at end of file diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Telemetry/EnvironmentVariablesOpenedEvent.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Telemetry/EnvironmentVariablesOpenedEvent.cs new file mode 100644 index 0000000000..113d88faed --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Telemetry/EnvironmentVariablesOpenedEvent.cs @@ -0,0 +1,16 @@ +// 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.Diagnostics.Tracing; +using Microsoft.PowerToys.Telemetry; +using Microsoft.PowerToys.Telemetry.Events; + +namespace EnvironmentVariables.Telemetry +{ + [EventData] + public class EnvironmentVariablesOpenedEvent : EventBase, IEvent + { + public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Telemetry/EnvironmentVariablesProfileEnabledEvent.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Telemetry/EnvironmentVariablesProfileEnabledEvent.cs new file mode 100644 index 0000000000..2d32b8e8c5 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Telemetry/EnvironmentVariablesProfileEnabledEvent.cs @@ -0,0 +1,18 @@ +// 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.Diagnostics.Tracing; +using Microsoft.PowerToys.Telemetry; +using Microsoft.PowerToys.Telemetry.Events; + +namespace EnvironmentVariables.Telemetry +{ + [EventData] + public class EnvironmentVariablesProfileEnabledEvent : EventBase, IEvent + { + public bool Enabled { get; set; } + + public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/Telemetry/EnvironmentVariablesVariableChangedEvent.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/Telemetry/EnvironmentVariablesVariableChangedEvent.cs new file mode 100644 index 0000000000..1583adb9fa --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/Telemetry/EnvironmentVariablesVariableChangedEvent.cs @@ -0,0 +1,24 @@ +// 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.Diagnostics.Tracing; +using EnvironmentVariables.Models; +using Microsoft.PowerToys.Telemetry; +using Microsoft.PowerToys.Telemetry.Events; + +namespace EnvironmentVariables.Telemetry +{ + [EventData] + public class EnvironmentVariablesVariableChangedEvent : EventBase, IEvent + { + public VariablesSetType VariablesType { get; set; } + + public EnvironmentVariablesVariableChangedEvent(VariablesSetType type) + { + this.VariablesType = type; + } + + public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/ViewModels/MainViewModel.cs b/src/modules/EnvironmentVariables/EnvironmentVariables/ViewModels/MainViewModel.cs new file mode 100644 index 0000000000..164325b4bb --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/ViewModels/MainViewModel.cs @@ -0,0 +1,417 @@ +// 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.Collections.ObjectModel; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using EnvironmentVariables.Helpers; +using EnvironmentVariables.Models; +using ManagedCommon; +using Microsoft.PowerToys.Telemetry; +using Microsoft.UI.Dispatching; + +namespace EnvironmentVariables.ViewModels +{ + public partial class MainViewModel : ObservableObject + { + private readonly IEnvironmentVariablesService _environmentVariablesService; + + private readonly DispatcherQueue _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + + public DefaultVariablesSet UserDefaultSet { get; private set; } = new DefaultVariablesSet(VariablesSet.UserGuid, ResourceLoaderInstance.ResourceLoader.GetString("User"), VariablesSetType.User); + + public DefaultVariablesSet SystemDefaultSet { get; private set; } = new DefaultVariablesSet(VariablesSet.SystemGuid, ResourceLoaderInstance.ResourceLoader.GetString("System"), VariablesSetType.System); + + public VariablesSet DefaultVariables { get; private set; } = new DefaultVariablesSet(Guid.NewGuid(), "DefaultVariables", VariablesSetType.User); + + [ObservableProperty] + private ObservableCollection _profiles; + + [ObservableProperty] + private ObservableCollection _appliedVariables = new ObservableCollection(); + + [ObservableProperty] + private bool _isElevated; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsInfoBarButtonVisible))] + private EnvironmentState _environmentState; + + public bool IsInfoBarButtonVisible => EnvironmentState == EnvironmentState.EnvironmentMessageReceived; + + public ProfileVariablesSet AppliedProfile { get; set; } + + public MainViewModel(IEnvironmentVariablesService environmentVariablesService) + { + _environmentVariablesService = environmentVariablesService; + var isElevated = App.GetService().IsElevated; + IsElevated = isElevated; + } + + private void LoadDefaultVariables() + { + UserDefaultSet.Variables.Clear(); + SystemDefaultSet.Variables.Clear(); + DefaultVariables.Variables.Clear(); + + EnvironmentVariablesHelper.GetVariables(EnvironmentVariableTarget.Machine, SystemDefaultSet); + EnvironmentVariablesHelper.GetVariables(EnvironmentVariableTarget.User, UserDefaultSet); + + foreach (var variable in UserDefaultSet.Variables) + { + DefaultVariables.Variables.Add(variable); + } + + foreach (var variable in SystemDefaultSet.Variables) + { + DefaultVariables.Variables.Add(variable); + } + } + + [RelayCommand] + public void LoadEnvironmentVariables() + { + LoadDefaultVariables(); + LoadProfiles(); + PopulateAppliedVariables(); + } + + private void LoadProfiles() + { + try + { + var profiles = _environmentVariablesService.ReadProfiles(); + foreach (var profile in profiles) + { + profile.PropertyChanged += Profile_PropertyChanged; + + foreach (var variable in profile.Variables) + { + variable.ParentType = VariablesSetType.Profile; + } + } + + var appliedProfiles = profiles.Where(x => x.IsEnabled).ToList(); + if (appliedProfiles.Count > 0) + { + var appliedProfile = appliedProfiles.First(); + if (appliedProfile.IsCorrectlyApplied()) + { + AppliedProfile = appliedProfile; + EnvironmentState = EnvironmentState.Unchanged; + } + else + { + EnvironmentState = EnvironmentState.ChangedOnStartup; + appliedProfile.IsEnabled = false; + } + } + + Profiles = new ObservableCollection(profiles); + } + catch (Exception ex) + { + // Show some error + Logger.LogError("Failed to load profiles.json file", ex); + + Profiles = new ObservableCollection(); + } + } + + private void PopulateAppliedVariables() + { + LoadDefaultVariables(); + + var variables = new List(); + if (AppliedProfile != null) + { + variables = variables.Concat(AppliedProfile.Variables.OrderBy(x => x.Name)).ToList(); + } + + variables = variables.Concat(UserDefaultSet.Variables.OrderBy(x => x.Name)).Concat(SystemDefaultSet.Variables.OrderBy(x => x.Name)).ToList(); + + // Handle PATH variable - add USER value to the end of the SYSTEM value + var profilePath = variables.Where(x => x.Name.Equals("PATH", StringComparison.OrdinalIgnoreCase) && x.ParentType == VariablesSetType.Profile).FirstOrDefault(); + var userPath = variables.Where(x => x.Name.Equals("PATH", StringComparison.OrdinalIgnoreCase) && x.ParentType == VariablesSetType.User).FirstOrDefault(); + var systemPath = variables.Where(x => x.Name.Equals("PATH", StringComparison.OrdinalIgnoreCase) && x.ParentType == VariablesSetType.System).FirstOrDefault(); + + if (systemPath != null) + { + var clone = systemPath.Clone(); + clone.ParentType = VariablesSetType.Path; + + if (userPath != null) + { + clone.Values += ";" + userPath.Values; + variables.Remove(userPath); + } + + if (profilePath != null) + { + variables.Remove(profilePath); + } + + variables.Insert(variables.IndexOf(systemPath), clone); + variables.Remove(systemPath); + } + + variables = variables.GroupBy(x => x.Name).Select(y => y.First()).ToList(); + + // Find duplicates + var duplicates = variables.Where(x => !x.Name.Equals("PATH", StringComparison.OrdinalIgnoreCase)).GroupBy(x => x.Name.ToLower(CultureInfo.InvariantCulture)).Where(g => g.Count() > 1); + foreach (var duplicate in duplicates) + { + var userVar = duplicate.ElementAt(0); + var systemVar = duplicate.ElementAt(1); + + var clone = userVar.Clone(); + clone.ParentType = VariablesSetType.Duplicate; + clone.Name = systemVar.Name; + variables.Insert(variables.IndexOf(userVar), clone); + variables.Remove(userVar); + variables.Remove(systemVar); + } + + variables = variables.OrderBy(x => x.ParentType).ToList(); + AppliedVariables = new ObservableCollection(variables); + } + + internal void AddDefaultVariable(Variable variable, VariablesSetType type) + { + if (type == VariablesSetType.User) + { + UserDefaultSet.Variables.Add(variable); + UserDefaultSet.Variables = new ObservableCollection(UserDefaultSet.Variables.OrderBy(x => x.Name).ToList()); + } + else if (type == VariablesSetType.System) + { + SystemDefaultSet.Variables.Add(variable); + SystemDefaultSet.Variables = new ObservableCollection(SystemDefaultSet.Variables.OrderBy(x => x.Name).ToList()); + } + + EnvironmentVariablesHelper.SetVariable(variable); + PopulateAppliedVariables(); + } + + internal void EditVariable(Variable original, Variable edited, ProfileVariablesSet variablesSet) + { + bool propagateChange = variablesSet == null /* not a profile */ || variablesSet.Id.Equals(AppliedProfile?.Id); + bool changed = original.Name != edited.Name || original.Values != edited.Values; + if (changed) + { + var task = original.Update(edited, propagateChange, variablesSet); + task.ContinueWith(x => + { + _dispatcherQueue.TryEnqueue(() => + { + PopulateAppliedVariables(); + }); + }); + + PowerToysTelemetry.Log.WriteEvent(new Telemetry.EnvironmentVariablesVariableChangedEvent(original.ParentType)); + + _ = Task.Run(SaveAsync); + } + } + + internal void AddProfile(ProfileVariablesSet profile) + { + profile.PropertyChanged += Profile_PropertyChanged; + if (profile.IsEnabled) + { + UnsetAppliedProfile(); + SetAppliedProfile(profile); + } + + Profiles.Add(profile); + + _ = Task.Run(SaveAsync); + } + + internal void UpdateProfile(ProfileVariablesSet updatedProfile) + { + var existingProfile = Profiles.Where(x => x.Id == updatedProfile.Id).FirstOrDefault(); + if (existingProfile != null) + { + if (updatedProfile.IsEnabled) + { + // Let's unset the profile before applying the update. Even if this one is the one that's currently set. + UnsetAppliedProfile(); + } + + existingProfile.Name = updatedProfile.Name; + existingProfile.IsEnabled = updatedProfile.IsEnabled; + existingProfile.Variables = updatedProfile.Variables; + } + + _ = Task.Run(SaveAsync); + } + + private async Task SaveAsync() + { + try + { + await _environmentVariablesService.WriteAsync(Profiles); + } + catch (Exception ex) + { + // Show some error + Logger.LogError("Failed to save to profiles.json file", ex); + } + } + + private void Profile_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + var profile = sender as ProfileVariablesSet; + + if (profile != null) + { + if (e.PropertyName == nameof(ProfileVariablesSet.IsEnabled)) + { + if (profile.IsEnabled) + { + UnsetAppliedProfile(); + SetAppliedProfile(profile); + + var telemetryEnabled = new Telemetry.EnvironmentVariablesProfileEnabledEvent() + { + Enabled = true, + }; + + PowerToysTelemetry.Log.WriteEvent(telemetryEnabled); + } + else + { + UnsetAppliedProfile(); + + var telemetryEnabled = new Telemetry.EnvironmentVariablesProfileEnabledEvent() + { + Enabled = false, + }; + + PowerToysTelemetry.Log.WriteEvent(telemetryEnabled); + } + } + } + + _ = Task.Run(SaveAsync); + } + + private void SetAppliedProfile(ProfileVariablesSet profile) + { + if (profile != null) + { + if (!profile.IsApplicable()) + { + profile.PropertyChanged -= Profile_PropertyChanged; + profile.IsEnabled = false; + profile.PropertyChanged += Profile_PropertyChanged; + + EnvironmentState = EnvironmentState.ProfileNotApplicable; + + return; + } + } + + var task = profile.Apply(); + task.ContinueWith((a) => + { + _dispatcherQueue.TryEnqueue(() => + { + PopulateAppliedVariables(); + }); + }); + AppliedProfile = profile; + } + + private void UnsetAppliedProfile() + { + if (AppliedProfile != null) + { + var appliedProfile = AppliedProfile; + appliedProfile.PropertyChanged -= Profile_PropertyChanged; + var task = AppliedProfile.UnApply(); + task.ContinueWith((a) => + { + _dispatcherQueue.TryEnqueue(() => + { + PopulateAppliedVariables(); + }); + }); + AppliedProfile.IsEnabled = false; + AppliedProfile = null; + appliedProfile.PropertyChanged += Profile_PropertyChanged; + } + } + + internal void RemoveProfile(ProfileVariablesSet profile) + { + if (profile.IsEnabled) + { + UnsetAppliedProfile(); + } + + Profiles.Remove(profile); + + _ = Task.Run(SaveAsync); + } + + internal void DeleteVariable(Variable variable, ProfileVariablesSet profile) + { + bool propagateChange = true; + + if (profile != null) + { + // Profile variable + profile.Variables.Remove(variable); + + if (!profile.IsEnabled) + { + propagateChange = false; + } + + _ = Task.Run(SaveAsync); + } + else + { + if (variable.ParentType == VariablesSetType.User) + { + UserDefaultSet.Variables.Remove(variable); + } + else if (variable.ParentType == VariablesSetType.System) + { + SystemDefaultSet.Variables.Remove(variable); + } + } + + if (propagateChange) + { + var task = Task.Run(() => + { + if (profile == null) + { + EnvironmentVariablesHelper.UnsetVariable(variable); + } + else + { + profile.UnapplyVariable(variable); + } + }); + task.ContinueWith((a) => + { + _dispatcherQueue.TryEnqueue(() => + { + PopulateAppliedVariables(); + }); + }); + } + } + } +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariables/app.manifest b/src/modules/EnvironmentVariables/EnvironmentVariables/app.manifest new file mode 100644 index 0000000000..cbe8c5420c --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariables/app.manifest @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + PerMonitorV2 + + + \ No newline at end of file diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/EnvironmentVariablesModuleInterface.base.rc b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/EnvironmentVariablesModuleInterface.base.rc new file mode 100644 index 0000000000..5fa3c8b90d --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/EnvironmentVariablesModuleInterface.base.rc @@ -0,0 +1,40 @@ +#include +#include "resource.h" +#include "../../../common/version/version.h" + +#define APSTUDIO_READONLY_SYMBOLS +#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 diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/EnvironmentVariablesModuleInterface.vcxproj b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/EnvironmentVariablesModuleInterface.vcxproj new file mode 100644 index 0000000000..6bf3bb3b20 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/EnvironmentVariablesModuleInterface.vcxproj @@ -0,0 +1,111 @@ + + + + + + + 17.0 + Win32Proj + {b9420661-b0e4-4241-abd4-4a27a1f64250} + EnvironmentVariablesModuleInterface + EnvironmentVariablesModuleInterface + + + + DynamicLibrary + true + v143 + Unicode + + + DynamicLibrary + false + v143 + true + Unicode + + + + + + + + + + + + ..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\ + PowerToys.EnvironmentVariablesModuleInterface + + + + Level3 + true + WIN32;_DEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + Use + + + Windows + true + false + + + + + Level3 + true + true + true + WIN32;NDEBUG;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + Use + + + Windows + true + true + true + false + + + + + $(SolutionDir)src;$(SolutionDir)src\modules;$(SolutionDir)src\common\Telemetry;%(AdditionalIncludeDirectories) + + + + + + + + + + + + Create + + + + + + + + + + + + {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd} + + + {6955446d-23f7-4023-9bb3-8657f904af99} + + + {cc6e41ac-8174-4e8a-8d22-85dd7f4851df} + + + + + + + \ No newline at end of file diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/EnvironmentVariablesModuleInterface.vcxproj.filters b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/EnvironmentVariablesModuleInterface.vcxproj.filters new file mode 100644 index 0000000000..14b6af8966 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/EnvironmentVariablesModuleInterface.vcxproj.filters @@ -0,0 +1,52 @@ + + + + + {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 + + + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Resource Files + + + Resource Files + + + + + Resource Files + + + \ No newline at end of file diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/Resource.resx b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/Resource.resx new file mode 100644 index 0000000000..d9331c4ad0 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/Resource.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Environment Variables + + \ No newline at end of file diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/dllmain.cpp b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/dllmain.cpp new file mode 100644 index 0000000000..9adbb3cd10 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/dllmain.cpp @@ -0,0 +1,280 @@ +// dllmain.cpp : Defines the entry point for the DLL application. +#include "pch.h" + +#include "Generated Files/resource.h" +#include "trace.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +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; +} + +namespace +{ + // Name of the powertoy module. + inline const std::wstring ModuleKey = L"EnvironmentVariables"; +} + +class EnvironmentVariablesModuleInterface : public PowertoyModuleIface +{ +private: + bool m_enabled = false; + + std::wstring app_name; + + //contains the non localized key of the powertoy + std::wstring app_key; + + HANDLE m_hProcess; + + HANDLE m_hShowEvent; + + EventWaiter m_showEventWaiter; + + HANDLE m_hShowAdminEvent; + + EventWaiter m_showAdminEventWaiter; + + bool is_process_running() + { + return WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT; + } + + void bring_process_to_front() + { + auto enum_windows = [](HWND hwnd, LPARAM param) -> BOOL { + HANDLE process_handle = reinterpret_cast(param); + DWORD window_process_id = 0; + + GetWindowThreadProcessId(hwnd, &window_process_id); + if (GetProcessId(process_handle) == window_process_id) + { + SetForegroundWindow(hwnd); + return FALSE; + } + return TRUE; + }; + + EnumWindows(enum_windows, (LPARAM)m_hProcess); + } + + void launch_process(bool runas) + { + Logger::trace("EnvironmentVariablesModuleInterface::launch_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"WinUI3Apps\\PowerToys.EnvironmentVariables.exe"; + sei.nShow = SW_SHOWNORMAL; + sei.lpParameters = executable_args.data(); + + if (runas) + { + sei.lpVerb = L"runas"; + } + + if (ShellExecuteExW(&sei)) + { + Logger::trace("Successfully started the Environment Variables process"); + } + else + { + Logger::error(L"Environment Variables failed to start. {}", get_last_error_or_default(GetLastError())); + } + + m_hProcess = sei.hProcess; + } + +public: + EnvironmentVariablesModuleInterface() + { + app_name = GET_RESOURCE_STRING(IDS_ENVIRONMENT_VARIABLES_NAME); + app_key = ModuleKey; + LoggerHelpers::init_logger(app_key, L"ModuleInterface", LogSettings::environmentVariablesLoggerName); + + m_hShowEvent = CreateDefaultEvent(CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_EVENT); + if (!m_hShowEvent) + { + Logger::error(L"Failed to create show Environment Variables event"); + auto message = get_last_error_message(GetLastError()); + if (message.has_value()) + { + Logger::error(message.value()); + } + } + + m_hShowAdminEvent = CreateDefaultEvent(CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_ADMIN_EVENT); + if (!m_hShowAdminEvent) + { + Logger::error(L"Failed to create show Environment Variables admin event"); + auto message = get_last_error_message(GetLastError()); + if (message.has_value()) + { + Logger::error(message.value()); + } + } + + m_showEventWaiter = EventWaiter(CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_EVENT, [&](int err) { + if (m_enabled && err == ERROR_SUCCESS) + { + Logger::trace(L"{} event was signaled", CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_EVENT); + + if (is_process_running()) + { + bring_process_to_front(); + } + else + { + launch_process(false); + } + + Trace::ActivateEnvironmentVariables(); + } + }); + + m_showAdminEventWaiter = EventWaiter(CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_ADMIN_EVENT, [&](int err) { + if (m_enabled && err == ERROR_SUCCESS) + { + Logger::trace(L"{} event was signaled", CommonSharedConstants::SHOW_ENVIRONMENT_VARIABLES_ADMIN_EVENT); + + if (is_process_running()) + { + bring_process_to_front(); + } + else + { + launch_process(true); + } + + Trace::ActivateEnvironmentVariables(); + } + }); + } + + ~EnvironmentVariablesModuleInterface() + { + m_enabled = false; + } + + // Destroy the powertoy and free memory + virtual void destroy() override + { + Logger::trace("EnvironmentVariablesModuleInterface::destroy()"); + + if (m_hShowEvent) + { + CloseHandle(m_hShowEvent); + m_hShowEvent = nullptr; + } + + if (m_hShowAdminEvent) + { + CloseHandle(m_hShowAdminEvent); + m_hShowAdminEvent = nullptr; + } + + delete this; + } + + // Return the localized display name of the powertoy + virtual const wchar_t* get_name() override + { + return app_name.c_str(); + } + + // Return the non localized key of the powertoy, this will be cached by the runner + virtual const wchar_t* get_key() override + { + return app_key.c_str(); + } + + // 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::getConfiguredEnvironmentVariablesEnabledValue(); + } + + virtual bool get_config(wchar_t* /*buffer*/, int* /*buffer_size*/) override + { + return false; + } + + virtual void call_custom_action(const wchar_t* /*action*/) override + { + } + + virtual void set_config(const wchar_t* /*config*/) override + { + } + + virtual bool is_enabled() override + { + return m_enabled; + } + + virtual void enable() + { + Logger::trace("EnvironmentVariablesModuleInterface::enable()"); + m_enabled = true; + Trace::EnableEnvironmentVariables(true); + } + + virtual void disable() + { + Logger::trace("EnvironmentVariablesModuleInterface::disable()"); + if (m_enabled) + { + if (m_hShowEvent) + { + ResetEvent(m_hShowEvent); + } + + if (m_hShowAdminEvent) + { + ResetEvent(m_hShowAdminEvent); + } + + TerminateProcess(m_hProcess, 1); + } + + m_enabled = false; + Trace::EnableEnvironmentVariables(false); + } +}; + +extern "C" __declspec(dllexport) PowertoyModuleIface * __cdecl powertoy_create() +{ + return new EnvironmentVariablesModuleInterface(); +} \ No newline at end of file diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/pch.cpp b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/pch.cpp new file mode 100644 index 0000000000..64b7eef6d6 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/pch.cpp @@ -0,0 +1,5 @@ +// pch.cpp: source file corresponding to the pre-compiled header + +#include "pch.h" + +// When you are using pre-compiled headers, this source file is necessary for compilation to succeed. diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/pch.h b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/pch.h new file mode 100644 index 0000000000..6f70098567 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/pch.h @@ -0,0 +1,18 @@ +// pch.h: This is a precompiled header file. +// Files listed below are compiled only once, improving build performance for future builds. +// This also affects IntelliSense performance, including code completion and many code browsing features. +// However, files listed here are ALL re-compiled if any one of them is updated between builds. +// Do not add files here that you will be updating frequently as this negates the performance advantage. + +#ifndef PCH_H +#define PCH_H + +#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers +// Windows Header Files +#include +#include + +#include +#include + +#endif //PCH_H diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/resource.base.h b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/resource.base.h new file mode 100644 index 0000000000..da4dc076a8 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/resource.base.h @@ -0,0 +1,13 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by AlwaysOnTopModuleInterface.rc + +////////////////////////////// +// Non-localizable + +#define FILE_DESCRIPTION "PowerToys Environment Variables Module" +#define INTERNAL_NAME "PowerToys.EnvironmentVariablesModuleInterface" +#define ORIGINAL_FILENAME "PowerToys.EnvironmentVariablesModuleInterface.dll" + +// Non-localizable +////////////////////////////// diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/trace.cpp b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/trace.cpp new file mode 100644 index 0000000000..bb458c1b6d --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/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() noexcept +{ + TraceLoggingRegister(g_hProvider); +} + +void Trace::UnregisterProvider() noexcept +{ + TraceLoggingUnregister(g_hProvider); +} + +// Log if the user has Environment Variables enabled or disabled +void Trace::EnableEnvironmentVariables(const bool enabled) noexcept +{ + TraceLoggingWrite( + g_hProvider, + "EnvironmentVariables_EnableEnvironmentVariables", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE), + TraceLoggingBoolean(enabled, "Enabled")); +} + +// Log that the user tried to activate the editor +void Trace::ActivateEnvironmentVariables() noexcept +{ + TraceLoggingWrite( + g_hProvider, + "EnvironmentVariables_Activate", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); +} diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/trace.h b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/trace.h new file mode 100644 index 0000000000..0898da4602 --- /dev/null +++ b/src/modules/EnvironmentVariables/EnvironmentVariablesModuleInterface/trace.h @@ -0,0 +1,14 @@ +#pragma once + +class Trace +{ +public: + static void RegisterProvider() noexcept; + static void UnregisterProvider() noexcept; + + // Log if the user has EnvironmentVariables enabled or disabled + static void EnableEnvironmentVariables(const bool enabled) noexcept; + + // Log that the user tried to activate the editor + static void ActivateEnvironmentVariables() noexcept; +}; diff --git a/src/modules/keyboardmanager/dll/dllmain.cpp b/src/modules/keyboardmanager/dll/dllmain.cpp index 0f0ea94bee..e76fd5f716 100644 --- a/src/modules/keyboardmanager/dll/dllmain.cpp +++ b/src/modules/keyboardmanager/dll/dllmain.cpp @@ -168,6 +168,13 @@ public: { return m_enabled; } + + // Returns whether the PowerToys should be enabled by default + virtual bool is_enabled_by_default() const override + { + return false; + } + }; extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/Utility.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/Utility.cs index 8fd8529fef..876a1dabf2 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/Utility.cs +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/Utility.cs @@ -61,11 +61,25 @@ namespace Microsoft.PowerToys.Run.Plugin.PowerToys.Components AcceleratorModifiers = ModifierKeys.Control | ModifierKeys.Shift, Action = _ => { - using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShowHostsAdminSharedEvent())) - { - eventHandle.Set(); - } - + using var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShowHostsAdminSharedEvent()); + eventHandle.Set(); + return true; + }, + }); + } + else if (Key == UtilityKey.EnvironmentVariables) + { + results.Add(new ContextMenuResult + { + Title = Resources.Action_Run_As_Administrator, + Glyph = "\xE7EF", + FontFamily = "Segoe MDL2 Assets", + AcceleratorKey = System.Windows.Input.Key.Enter, + AcceleratorModifiers = ModifierKeys.Control | ModifierKeys.Shift, + Action = _ => + { + using var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShowEnvironmentVariablesAdminSharedEvent()); + eventHandle.Set(); return true; }, }); diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityHelper.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityHelper.cs index 3d495479a9..b49eb4ecd0 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityHelper.cs +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityHelper.cs @@ -20,6 +20,7 @@ namespace Microsoft.PowerToys.Run.Plugin.PowerToys.Components UtilityKey.ShortcutGuide => "Images/ShortcutGuide.png", UtilityKey.RegistryPreview => "Images/RegistryPreview.png", UtilityKey.CropAndLock => "Images/CropAndLock.png", + UtilityKey.EnvironmentVariables => "Images/EnvironmentVariables.png", _ => null, }; } @@ -36,6 +37,7 @@ namespace Microsoft.PowerToys.Run.Plugin.PowerToys.Components UtilityKey.ShortcutGuide => SettingsDeepLink.SettingsWindow.ShortcutGuide, UtilityKey.RegistryPreview => SettingsDeepLink.SettingsWindow.RegistryPreview, UtilityKey.CropAndLock => SettingsDeepLink.SettingsWindow.CropAndLock, + UtilityKey.EnvironmentVariables => SettingsDeepLink.SettingsWindow.EnvironmentVariables, _ => null, }; } diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityKey.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityKey.cs index 33cb97b4a6..bc4db24615 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityKey.cs +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityKey.cs @@ -14,5 +14,6 @@ namespace Microsoft.PowerToys.Run.Plugin.PowerToys.Components ShortcutGuide = 5, RegistryPreview = 6, CropAndLock = 7, + EnvironmentVariables = 8, } } diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityProvider.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityProvider.cs index 59fc0c82f5..6497da89db 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityProvider.cs +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Components/UtilityProvider.cs @@ -170,6 +170,20 @@ namespace Microsoft.PowerToys.Run.Plugin.PowerToys })); } + if (GPOWrapper.GetConfiguredEnvironmentVariablesEnabledValue() != GpoRuleConfigured.Disabled) + { + _utilities.Add(new Utility( + UtilityKey.EnvironmentVariables, + Resources.Environment_Variables, + generalSettings.Enabled.EnvironmentVariables, + (_) => + { + using var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.ShowEnvironmentVariablesSharedEvent()); + eventHandle.Set(); + return true; + })); + } + _watcher = new FileSystemWatcher { Path = Path.GetDirectoryName(settingsUtils.GetSettingsFilePath()), @@ -214,6 +228,7 @@ namespace Microsoft.PowerToys.Run.Plugin.PowerToys case UtilityKey.ShortcutGuide: u.Enable(generalSettings.Enabled.ShortcutGuide); break; case UtilityKey.RegistryPreview: u.Enable(generalSettings.Enabled.RegistryPreview); break; case UtilityKey.CropAndLock: u.Enable(generalSettings.Enabled.CropAndLock); break; + case UtilityKey.EnvironmentVariables: u.Enable(generalSettings.Enabled.EnvironmentVariables); break; } } diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Microsoft.PowerToys.Run.Plugin.PowerToys.csproj b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Microsoft.PowerToys.Run.Plugin.PowerToys.csproj index 08ddf577dc..f56985e9ff 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Microsoft.PowerToys.Run.Plugin.PowerToys.csproj +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Microsoft.PowerToys.Run.Plugin.PowerToys.csproj @@ -105,6 +105,10 @@ Images\ShortcutGuide.png PreserveNewest + + Images\EnvironmentVariables.png + PreserveNewest + diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Properties/Resources.Designer.cs b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Properties/Resources.Designer.cs index 05d0f47ec5..4f01b8edb3 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Properties/Resources.Designer.cs +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Properties/Resources.Designer.cs @@ -105,6 +105,15 @@ namespace Microsoft.PowerToys.Run.Plugin.PowerToys.Properties { } } + /// + /// Looks up a localized string similar to Environment Variables. + /// + internal static string Environment_Variables { + get { + return ResourceManager.GetString("Environment_Variables", resourceCulture); + } + } + /// /// Looks up a localized string similar to FancyZones Editor. /// diff --git a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Properties/Resources.resx b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Properties/Resources.resx index cbc299caa9..325697d375 100644 --- a/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Properties/Resources.resx +++ b/src/modules/launcher/Plugins/Microsoft.PowerToys.Run.Plugin.PowerToys/Properties/Resources.resx @@ -135,6 +135,10 @@ Crop And Lock (Thumbnail) "Crop And Lock" is the name of the utility, "Thumbnail" is the activation mode + + Environment Variables + "Environment Variables" is the name of the utility + FancyZones Editor "FancyZones" is the name of the utility diff --git a/src/modules/registrypreview/RegistryPreviewUI/MainWindow.Utilities.cs b/src/modules/registrypreview/RegistryPreviewUI/MainWindow.Utilities.cs index 0941943639..450c3f5e78 100644 --- a/src/modules/registrypreview/RegistryPreviewUI/MainWindow.Utilities.cs +++ b/src/modules/registrypreview/RegistryPreviewUI/MainWindow.Utilities.cs @@ -774,7 +774,19 @@ namespace RegistryPreview // 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); + + // this used to use Replace but that would replace all instances of the same key name, which causes bugs. + try + { + int removeAt = fullPath.LastIndexOf(string.Format(CultureInfo.InvariantCulture, @"\{0}", individualKeys[i]), StringComparison.InvariantCulture); + if (removeAt > -1) + { + fullPath = fullPath.Substring(0, removeAt); + } + } + catch + { + } // 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) diff --git a/src/runner/main.cpp b/src/runner/main.cpp index eb1fb48503..7749fc9f17 100644 --- a/src/runner/main.cpp +++ b/src/runner/main.cpp @@ -81,7 +81,7 @@ inline wil::unique_mutex_nothrow create_msi_mutex() void open_menu_from_another_instance(std::optional settings_window) { const HWND hwnd_main = FindWindowW(L"PToyTrayIconWindow", nullptr); - LPARAM msg = static_cast(ESettingsWindowNames::Overview); + LPARAM msg = static_cast(ESettingsWindowNames::Dashboard); if (settings_window.has_value() && settings_window.value() != "") { msg = static_cast(ESettingsWindowNames_from_string(settings_window.value())); @@ -155,6 +155,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow L"WinUI3Apps/PowerToys.MeasureToolModuleInterface.dll", L"WinUI3Apps/PowerToys.HostsModuleInterface.dll", L"WinUI3Apps/PowerToys.Peek.dll", + L"WinUI3Apps/PowerToys.EnvironmentVariablesModuleInterface.dll", L"PowerToys.MouseWithoutBordersModuleInterface.dll", L"PowerToys.CropAndLockModuleInterface.dll", }; diff --git a/src/runner/settings_window.cpp b/src/runner/settings_window.cpp index bcba0915b1..86a6f25c13 100644 --- a/src/runner/settings_window.cpp +++ b/src/runner/settings_window.cpp @@ -596,7 +596,7 @@ void open_settings_window(std::optional settings_window, bool show } else { - current_settings_ipc->send(L"{\"ShowYourself\":\"Overview\"}"); + current_settings_ipc->send(L"{\"ShowYourself\":\"Dashboard\"}"); } } } @@ -676,6 +676,10 @@ std::string ESettingsWindowNames_to_string(ESettingsWindowNames value) return "RegistryPreview"; case ESettingsWindowNames::CropAndLock: return "CropAndLock"; + case ESettingsWindowNames::EnvironmentVariables: + return "EnvironmentVariables"; + case ESettingsWindowNames::Dashboard: + return "Dashboard"; default: { Logger::error(L"Can't convert ESettingsWindowNames value={} to string", static_cast(value)); @@ -755,11 +759,19 @@ ESettingsWindowNames ESettingsWindowNames_from_string(std::string value) { return ESettingsWindowNames::CropAndLock; } + else if (value == "EnvironmentVariables") + { + return ESettingsWindowNames::EnvironmentVariables; + } + else if (value == "Dashboard") + { + return ESettingsWindowNames::Dashboard; + } else { Logger::error(L"Can't convert string value={} to ESettingsWindowNames", winrt::to_hstring(value)); assert(false); } - return ESettingsWindowNames::Overview; + return ESettingsWindowNames::Dashboard; } diff --git a/src/runner/settings_window.h b/src/runner/settings_window.h index 2a14ec46e0..0f213b28bc 100644 --- a/src/runner/settings_window.h +++ b/src/runner/settings_window.h @@ -4,7 +4,8 @@ enum class ESettingsWindowNames { - Overview = 0, + Dashboard = 0, + Overview, Awake, ColorPicker, FancyZones, @@ -21,6 +22,7 @@ enum class ESettingsWindowNames PowerOCR, RegistryPreview, CropAndLock, + EnvironmentVariables, }; 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 b93ad376b3..27af49c4eb 100644 --- a/src/settings-ui/Settings.UI.Library/EnabledModules.cs +++ b/src/settings-ui/Settings.UI.Library/EnabledModules.cs @@ -427,6 +427,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library } } + private bool environmentVariables = true; + + [JsonPropertyName("EnvironmentVariables")] + public bool EnvironmentVariables + { + get => environmentVariables; + set + { + if (environmentVariables != value) + { + LogTelemetryEvent(value); + environmentVariables = value; + } + } + } + private void NotifyChange() { notifyEnabledChangedAction?.Invoke(); diff --git a/src/settings-ui/Settings.UI.Library/EnvironmentVariablesProperties.cs b/src/settings-ui/Settings.UI.Library/EnvironmentVariablesProperties.cs new file mode 100644 index 0000000000..2d2b93d95f --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/EnvironmentVariablesProperties.cs @@ -0,0 +1,20 @@ +// 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 Settings.UI.Library.Enumerations; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class EnvironmentVariablesProperties + { + [JsonConverter(typeof(BoolPropertyJsonConverter))] + public bool LaunchAdministrator { get; set; } + + public EnvironmentVariablesProperties() + { + LaunchAdministrator = true; + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/EnvironmentVariablesSettings.cs b/src/settings-ui/Settings.UI.Library/EnvironmentVariablesSettings.cs new file mode 100644 index 0000000000..0263d88bfc --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/EnvironmentVariablesSettings.cs @@ -0,0 +1,46 @@ +// 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.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class EnvironmentVariablesSettings : BasePTModuleSettings, ISettingsConfig + { + public const string ModuleName = "EnvironmentVariables"; + + [JsonPropertyName("properties")] + public EnvironmentVariablesProperties Properties { get; set; } + + public EnvironmentVariablesSettings() + { + Properties = new EnvironmentVariablesProperties(); + Version = "1.0"; + Name = ModuleName; + } + + public virtual void Save(ISettingsUtils settingsUtils) + { + // Save settings to file + var options = new JsonSerializerOptions + { + WriteIndented = true, + }; + + if (settingsUtils == null) + { + throw new ArgumentNullException(nameof(settingsUtils)); + } + + settingsUtils.SaveSettings(JsonSerializer.Serialize(this, options), ModuleName); + } + + public string GetModuleName() => Name; + + public bool UpgradeSettingsConfiguration() => false; + } +} diff --git a/src/settings-ui/Settings.UI/Assets/Settings/FluentIcons/FluentIconsEnvironmentVariables.png b/src/settings-ui/Settings.UI/Assets/Settings/FluentIcons/FluentIconsEnvironmentVariables.png new file mode 100644 index 0000000000..159f7c0da1 Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Settings/FluentIcons/FluentIconsEnvironmentVariables.png differ diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Modules/EnvironmentVariables.png b/src/settings-ui/Settings.UI/Assets/Settings/Modules/EnvironmentVariables.png new file mode 100644 index 0000000000..97e2ab67e6 Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Settings/Modules/EnvironmentVariables.png differ diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Modules/OOBE/EnvironmentVariables.png b/src/settings-ui/Settings.UI/Assets/Settings/Modules/OOBE/EnvironmentVariables.png new file mode 100644 index 0000000000..dcfb68d067 Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Settings/Modules/OOBE/EnvironmentVariables.png differ diff --git a/src/settings-ui/Settings.UI/Converters/ModuleItemTemplateSelector.cs b/src/settings-ui/Settings.UI/Converters/ModuleItemTemplateSelector.cs new file mode 100644 index 0000000000..b34331a0e7 --- /dev/null +++ b/src/settings-ui/Settings.UI/Converters/ModuleItemTemplateSelector.cs @@ -0,0 +1,33 @@ +// 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 Microsoft.PowerToys.Settings.UI.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Microsoft.PowerToys.Settings.UI.Converters +{ + public class ModuleItemTemplateSelector : DataTemplateSelector + { + public DataTemplate TextTemplate { get; set; } + + public DataTemplate ButtonTemplate { get; set; } + + public DataTemplate ShortcutTemplate { get; set; } + + public DataTemplate KBMTemplate { get; set; } + + protected override DataTemplate SelectTemplateCore(object item, DependencyObject container) + { + switch (item) + { + case DashboardModuleButtonItem: return ButtonTemplate; + case DashboardModuleShortcutItem: return ShortcutTemplate; + case DashboardModuleTextItem: return TextTemplate; + case DashboardModuleKBMItem: return KBMTemplate; + default: return TextTemplate; + } + } + } +} diff --git a/src/settings-ui/Settings.UI/Converters/NegativeBoolToVisibilityConverter.cs b/src/settings-ui/Settings.UI/Converters/NegativeBoolToVisibilityConverter.cs new file mode 100644 index 0000000000..df9f02eca7 --- /dev/null +++ b/src/settings-ui/Settings.UI/Converters/NegativeBoolToVisibilityConverter.cs @@ -0,0 +1,37 @@ +// 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.Globalization; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Data; + +namespace Microsoft.PowerToys.Settings.UI.Converters +{ + public class NegativeBoolToVisibilityConverter : IValueConverter + { + object IValueConverter.Convert(object value, Type targetType, object parameter, string language) + { + if ((bool)value) + { + return Visibility.Collapsed; + } + + return Visibility.Visible; + } + + object IValueConverter.ConvertBack(object value, Type targetType, object parameter, string language) + { + if (value is Visibility) + { + if ((Visibility)value == Visibility.Visible) + { + return false; + } + } + + return true; + } + } +} diff --git a/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs b/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs index af54f7c679..1a5091f5f4 100644 --- a/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs +++ b/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs @@ -11,6 +11,7 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Enums Awake, ColorPicker, CropAndLock, + EnvironmentVariables, FancyZones, FileLocksmith, FileExplorer, diff --git a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj index bffa7cc353..fb3ee12164 100644 --- a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj +++ b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj @@ -21,6 +21,9 @@ PowerToys.Settings.pri + + + @@ -124,5 +127,11 @@ Always + + + + $(DefaultXamlRuntime) + + diff --git a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs index ba9988dd2d..202cadc306 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs @@ -60,7 +60,7 @@ namespace Microsoft.PowerToys.Settings.UI public bool ShowScoobe { get; set; } - public Type StartupPage { get; set; } = typeof(Views.GeneralPage); + public Type StartupPage { get; set; } = typeof(Views.DashboardPage); public static Action IPCMessageReceivedCallback { get; set; } @@ -218,8 +218,8 @@ namespace Microsoft.PowerToys.Settings.UI settingsWindow.NavigateToSection(StartupPage); ShowMessageDialog("The application is running in Debug mode.", "DEBUG"); #else - /* If we try to run Settings as a standalone app, it will start PowerToys.exe if not running and open Settings again through it in the General page. */ - SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.Overview, true); + /* If we try to run Settings as a standalone app, it will start PowerToys.exe if not running and open Settings again through it in the Dashboard page. */ + SettingsDeepLink.OpenSettings(SettingsDeepLink.SettingsWindow.Dashboard, true); Exit(); #endif } @@ -380,6 +380,7 @@ namespace Microsoft.PowerToys.Settings.UI { switch (settingWindow) { + case "Dashboard": return typeof(DashboardPage); case "Overview": return typeof(GeneralPage); case "AlwaysOnTop": return typeof(AlwaysOnTopPage); case "Awake": return typeof(AwakePage); @@ -403,10 +404,11 @@ namespace Microsoft.PowerToys.Settings.UI case "PastePlain": return typeof(PastePlainPage); case "Peek": return typeof(PeekPage); case "CropAndLock": return typeof(CropAndLockPage); + case "EnvironmentVariables": return typeof(EnvironmentVariablesPage); default: - // Fallback to general + // Fallback to Dashboard Debug.Assert(false, "Unexpected SettingsWindow argument value"); - return typeof(GeneralPage); + return typeof(DashboardPage); } } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.cs index 5e5d3ee846..8b36831984 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.cs @@ -116,7 +116,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls case 91: // The left Windows key case 92: // The right Windows key - PathIcon winIcon = XamlReader.Load(@"") as PathIcon; + PathIcon winIcon = XamlReader.Load(@"") as PathIcon; Viewbox winIconContainer = new Viewbox(); winIconContainer.Child = winIcon; winIconContainer.HorizontalAlignment = HorizontalAlignment.Center; @@ -143,6 +143,10 @@ namespace Microsoft.PowerToys.Settings.UI.Controls { return (Style)App.Current.Resources["SmallOutline" + styleName]; } + else if (VisualType == VisualType.TextOnly) + { + return (Style)App.Current.Resources["Only" + styleName]; + } else { return (Style)App.Current.Resources["Default" + styleName]; @@ -181,6 +185,7 @@ namespace Microsoft.PowerToys.Settings.UI.Controls { Small, SmallOutline, + TextOnly, Large, } } diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml index 2d469563a2..68590a040a 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/KeyVisual/KeyVisual.xaml @@ -5,9 +5,7 @@ 16 12 - + + + + + \ No newline at end of file diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml index 1a51b17874..1f1fc9e071 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml +++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ShortcutControl/ShortcutWithTextLabelControl.xaml @@ -23,9 +23,7 @@ ItemsSource="{x:Bind Keys}"> - + @@ -41,7 +39,6 @@ diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml.cs index ebb1928937..f28ccd25ce 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/Flyout/LaunchPage.xaml.cs @@ -42,6 +42,21 @@ namespace Microsoft.PowerToys.Settings.UI.Flyout } break; + case "EnvironmentVariables": // Launch Environment Variables + { + bool launchAdmin = SettingsRepository.GetInstance(new SettingsUtils()).SettingsConfig.Properties.LaunchAdministrator; + string eventName = !App.IsElevated && launchAdmin + ? Constants.ShowEnvironmentVariablesAdminSharedEvent() + : Constants.ShowEnvironmentVariablesSharedEvent(); + + using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName)) + { + eventHandle.Set(); + } + } + + break; + case "FancyZones": // Launch FancyZones Editor using (var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, Constants.FZEToggleEvent())) { diff --git a/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs index 142ba0b6b4..874e3b6130 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/MainWindow.xaml.cs @@ -100,6 +100,9 @@ namespace Microsoft.PowerToys.Settings.UI case "CropAndLock": needToUpdate = generalSettingsConfig.Enabled.CropAndLock != isEnabled; generalSettingsConfig.Enabled.CropAndLock = isEnabled; break; + case "EnvironmentVariables": + needToUpdate = generalSettingsConfig.Enabled.EnvironmentVariables != isEnabled; + generalSettingsConfig.Enabled.EnvironmentVariables = isEnabled; break; case "FancyZones": needToUpdate = generalSettingsConfig.Enabled.FancyZones != isEnabled; generalSettingsConfig.Enabled.FancyZones = isEnabled; break; diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeEnvironmentVariables.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeEnvironmentVariables.xaml new file mode 100644 index 0000000000..4a8ee0e2c2 --- /dev/null +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeEnvironmentVariables.xaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + +