diff --git a/.github/actions/spell-check/allow/names.txt b/.github/actions/spell-check/allow/names.txt index c83877e253..95c1427b9d 100644 --- a/.github/actions/spell-check/allow/names.txt +++ b/.github/actions/spell-check/allow/names.txt @@ -64,6 +64,7 @@ Essey ethanfangg ferraridavide frankychen +gaardmark gabime Galaxi Garside diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 458e3087b2..e7d66027ae 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -80,6 +80,8 @@ asf AShortcut ASingle ASSOCCHANGED +ASSOCF +ASSOCSTR ASYNCWINDOWPLACEMENT ASYNCWINDOWPOS atl @@ -318,6 +320,7 @@ Dedup DEFAULTBOOTSTRAPPERINSTALLFOLDER DEFAULTCOLOR DEFAULTFLAGS +DEFAULTICON DEFAULTONLY DEFAULTTONEAREST DEFAULTTONULL @@ -332,6 +335,7 @@ deletethis DENORMAL depersist deprioritized +DESELECTOTHERS DESKTOPABSOLUTEEDITING DESKTOPABSOLUTEPARSING desktopshorcutinstalled @@ -428,6 +432,7 @@ encodedlaunch encryptor endpointvolume ENDSESSION +ENSUREVISIBLE ENTERSIZEMOVE ENU EOAC @@ -509,6 +514,7 @@ FOLDERID folderpath FORCEMINIMIZE formatetc +FORPARSING FRAMECHANGED frm Froml @@ -563,6 +569,7 @@ Hashset hashtag HASHVAL HASSTRINGS +HASSUBCOMMANDS hbitmap hbm hbmp @@ -641,6 +648,7 @@ IBeam ICapture IClass ICONERROR +ICONLOCATION IData IDD IDesktop @@ -658,6 +666,7 @@ IFACEMETHOD IFACEMETHODIMP IFile IFilter +IGNOREUNKNOWN IGraphics iid Iindex @@ -719,6 +728,7 @@ ISettings IShell isocpp iss +ISSEPARATOR ITask ith ITHUMBNAIL @@ -994,6 +1004,8 @@ newdev NEWDIALOGSTYLE newitem newpath +newplus +NEWPLUSCONTEXTMENU newrow newsgroups NIF @@ -1006,7 +1018,10 @@ NOAGGREGATION NOASYNC NOCLOSEPROCESS NOCOALESCE +NOCOMM +NOCONFIRMMKDIR NOCOPYBITS +NOCOPYSECURITYATTRIBS nodeca nodoc NODRAWCAPTION @@ -1014,6 +1029,7 @@ NODRAWICON NOINHERITLAYOUT NOINTERFACE NOLINKINFO +NOMCX NOMINMAX NOMIRRORBITMAP NOMOVE @@ -1414,6 +1430,8 @@ SHELLEXECUTEINFO SHELLEXECUTEINFOW shellscalingapi SHFILEINFO +SHFILEOPSTRUCT +SHGDN SHGDNF SHGFI shinfo @@ -1421,6 +1439,7 @@ shldisp shlobj shlwapi shmem +SHNAMEMAPPING shobjidl SHORTCUTATLEAST shortcutcontrol @@ -1525,7 +1544,7 @@ stringtable stringval Strm Strmiids -Strret +strret strsafe strutil sttngs @@ -1541,6 +1560,7 @@ svchost SVGIn SVGIO svgz +SVSI SWC SWFO SWP @@ -1745,6 +1765,7 @@ VSTHRD VSTT vswhere Vtbl +WANTMAPPINGHANDLE WANTPALM wbem Wbemidl @@ -1756,6 +1777,7 @@ WCE wcex WClass wcsicmp +wcsncpy wcsnicmp WDA wdp diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json index b873c42b90..ed5bd325a7 100644 --- a/.pipelines/ESRPSigning_core.json +++ b/.pipelines/ESRPSigning_core.json @@ -178,6 +178,9 @@ "PowerToys.MouseWithoutBordersHelper.dll", "PowerToys.MouseWithoutBordersHelper.exe", + "WinUI3Apps\\PowerToys.NewPlus.ShellExtension.dll", + "WinUI3Apps\\NewPlusPackage.msix", + "PowerAccent.Core.dll", "PowerToys.PowerAccent.dll", "PowerToys.PowerAccent.exe", diff --git a/PowerToys.sln b/PowerToys.sln index d6c33873e2..6f214eb5c0 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -582,6 +582,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerToys.Settings.DSC.Sche {020A7474-3601-4160-A159-D7B70B77B15F} = {020A7474-3601-4160-A159-D7B70B77B15F} EndProjectSection EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "NewPlus.ShellExtension", "src\modules\NewPlus\NewShellExtensionContextMenu\NewShellExtensionContextMenu.vcxproj", "{8ACB33D9-C95B-47D4-8363-9731EE0930A0}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "New+", "New+", "{CA716AE6-FE5C-40AC-BB8F-2C87912687AC}" +EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerToys.Interop", "src\common\interop\PowerToys.Interop.vcxproj", "{F055103B-F80B-4D0C-BF48-057C55620033}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workspaces", "Workspaces", "{A2221D7E-55E7-4BEA-90D1-4F162D670BBF}" @@ -2621,6 +2625,16 @@ Global {1D6893CB-BC0C-46A8-A76C-9728706CA51A}.Release|x64.Build.0 = Release|x64 {1D6893CB-BC0C-46A8-A76C-9728706CA51A}.Release|x86.ActiveCfg = Release|x64 {1D6893CB-BC0C-46A8-A76C-9728706CA51A}.Release|x86.Build.0 = Release|x64 + {8ACB33D9-C95B-47D4-8363-9731EE0930A0}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {8ACB33D9-C95B-47D4-8363-9731EE0930A0}.Debug|ARM64.Build.0 = Debug|ARM64 + {8ACB33D9-C95B-47D4-8363-9731EE0930A0}.Debug|x64.ActiveCfg = Debug|x64 + {8ACB33D9-C95B-47D4-8363-9731EE0930A0}.Debug|x64.Build.0 = Debug|x64 + {8ACB33D9-C95B-47D4-8363-9731EE0930A0}.Debug|x86.ActiveCfg = Debug|x64 + {8ACB33D9-C95B-47D4-8363-9731EE0930A0}.Release|ARM64.ActiveCfg = Release|ARM64 + {8ACB33D9-C95B-47D4-8363-9731EE0930A0}.Release|ARM64.Build.0 = Release|ARM64 + {8ACB33D9-C95B-47D4-8363-9731EE0930A0}.Release|x64.ActiveCfg = Release|x64 + {8ACB33D9-C95B-47D4-8363-9731EE0930A0}.Release|x64.Build.0 = Release|x64 + {8ACB33D9-C95B-47D4-8363-9731EE0930A0}.Release|x86.ActiveCfg = Release|x64 {F055103B-F80B-4D0C-BF48-057C55620033}.Debug|ARM64.ActiveCfg = Debug|ARM64 {F055103B-F80B-4D0C-BF48-057C55620033}.Debug|ARM64.Build.0 = Debug|ARM64 {F055103B-F80B-4D0C-BF48-057C55620033}.Debug|x64.ActiveCfg = Debug|x64 @@ -2920,6 +2934,8 @@ Global {3A9A791E-94A9-49F8-8401-C11CE288D5FB} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} {C0974915-8A1D-4BF0-977B-9587D3807AB7} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} {1D6893CB-BC0C-46A8-A76C-9728706CA51A} = {557C4636-D7E1-4838-A504-7D19B725EE95} + {8ACB33D9-C95B-47D4-8363-9731EE0930A0} = {CA716AE6-FE5C-40AC-BB8F-2C87912687AC} + {CA716AE6-FE5C-40AC-BB8F-2C87912687AC} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} {F055103B-F80B-4D0C-BF48-057C55620033} = {5A7818A8-109C-4E1C-850D-1A654E234B0E} {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} {BE126CBB-AE12-406A-9837-A05ACFCA57A7} = {A2221D7E-55E7-4BEA-90D1-4F162D670BBF} diff --git a/installer/PowerToysSetup/NewPlus.wxs b/installer/PowerToysSetup/NewPlus.wxs new file mode 100644 index 0000000000..80fd5a94f4 --- /dev/null +++ b/installer/PowerToysSetup/NewPlus.wxs @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/installer/PowerToysSetup/PowerToysInstaller.wixproj b/installer/PowerToysSetup/PowerToysInstaller.wixproj index 6e83f2ef37..6d9ca4a6c5 100644 --- a/installer/PowerToysSetup/PowerToysInstaller.wixproj +++ b/installer/PowerToysSetup/PowerToysInstaller.wixproj @@ -1,9 +1,7 @@ - + - Version=$(Version);MonacoSRCHarvestPath=$(ProjectDir)..\..\x64\$(Configuration)\Assets\Monaco\monacoSRC Release @@ -102,8 +99,8 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil + - @@ -124,9 +121,7 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil - - @@ -184,9 +179,8 @@ call powershell.exe -NonInteractive -executionpolicy Unrestricted -File $(MSBuil --> - + - diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs index 22940acbb5..3a30002a37 100644 --- a/installer/PowerToysSetup/Product.wxs +++ b/installer/PowerToysSetup/Product.wxs @@ -73,6 +73,8 @@ + + diff --git a/installer/PowerToysSetup/generateAllFileComponents.ps1 b/installer/PowerToysSetup/generateAllFileComponents.ps1 index 6c2f91436b..3b8bd59d3b 100644 --- a/installer/PowerToysSetup/generateAllFileComponents.ps1 +++ b/installer/PowerToysSetup/generateAllFileComponents.ps1 @@ -58,6 +58,10 @@ Invoke-Expression -Command "$PSScriptRoot\generateFileComponents.ps1 -fileListNa Invoke-Expression -Command "$PSScriptRoot\generateFileList.ps1 -fileDepsJson """" -fileListName ImageResizerAssetsFiles -wxsFilePath $PSScriptRoot\ImageResizer.wxs -depsPath ""$PSScriptRoot..\..\..\$platform\Release\Assets\ImageResizer""" Invoke-Expression -Command "$PSScriptRoot\generateFileComponents.ps1 -fileListName ""ImageResizerAssetsFiles"" -wxsFilePath $PSScriptRoot\ImageResizer.wxs -regroot $registryroot" +#New+ +Invoke-Expression -Command "$PSScriptRoot\generateFileList.ps1 -fileDepsJson """" -fileListName NewPlusAssetsFiles -wxsFilePath $PSScriptRoot\NewPlus.wxs -depsPath ""$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\NewPlus""" +Invoke-Expression -Command "$PSScriptRoot\generateFileComponents.ps1 -fileListName ""NewPlusAssetsFiles"" -wxsFilePath $PSScriptRoot\NewPlus.wxs -regroot $registryroot" + #Peek Invoke-Expression -Command "$PSScriptRoot\generateFileList.ps1 -fileDepsJson """" -fileListName PeekAssetsFiles -wxsFilePath $PSScriptRoot\Peek.wxs -depsPath ""$PSScriptRoot..\..\..\$platform\Release\WinUI3Apps\Assets\Peek\""" Invoke-Expression -Command "$PSScriptRoot\generateFileComponents.ps1 -fileListName ""PeekAssetsFiles"" -wxsFilePath $PSScriptRoot\Peek.wxs -regroot $registryroot" diff --git a/installer/PowerToysSetupCustomActions/CustomAction.cpp b/installer/PowerToysSetupCustomActions/CustomAction.cpp index 6e931e4749..216a05aaab 100644 --- a/installer/PowerToysSetupCustomActions/CustomAction.cpp +++ b/installer/PowerToysSetupCustomActions/CustomAction.cpp @@ -1148,7 +1148,7 @@ UINT __stdcall UnRegisterContextMenuPackagesCA(MSIHANDLE hInstall) try { // Packages to unregister - const std::vector packagesToRemoveDisplayName{ { L"PowerRenameContextMenu" }, { L"ImageResizerContextMenu" }, { L"FileLocksmithContextMenu" } }; + const std::vector packagesToRemoveDisplayName{ { L"PowerRenameContextMenu" }, { L"ImageResizerContextMenu" }, { L"FileLocksmithContextMenu" }, { L"NewPlusContextMenu" } }; PackageManager packageManager; diff --git a/installer/PowerToysSetupCustomActions/PowerToysSetupCustomActions.vcxproj b/installer/PowerToysSetupCustomActions/PowerToysSetupCustomActions.vcxproj index b8efdc3385..b58e19460d 100644 --- a/installer/PowerToysSetupCustomActions/PowerToysSetupCustomActions.vcxproj +++ b/installer/PowerToysSetupCustomActions/PowerToysSetupCustomActions.vcxproj @@ -60,6 +60,7 @@ call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\ImageResizer.wxs"" ""$(ProjectDir)..\PowerToysSetup\ImageResizer.wxs.bk"""" call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\KeyboardManager.wxs"" ""$(ProjectDir)..\PowerToysSetup\KeyboardManager.wxs.bk"""" call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\MouseWithoutBorders.wxs"" ""$(ProjectDir)..\PowerToysSetup\MouseWithoutBorders.wxs.bk"""" + call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\NewPlus.wxs"" ""$(ProjectDir)..\PowerToysSetup\NewPlus.wxs.bk"""" call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\Peek.wxs"" ""$(ProjectDir)..\PowerToysSetup\Peek.wxs.bk"""" call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\PowerRename.wxs"" ""$(ProjectDir)..\PowerToysSetup\PowerRename.wxs.bk"""" call cmd /C "copy ""$(ProjectDir)..\PowerToysSetup\Product.wxs"" ""$(ProjectDir)..\PowerToysSetup\Product.wxs.bk"""" diff --git a/src/common/GPOWrapper/GPOWrapper.cpp b/src/common/GPOWrapper/GPOWrapper.cpp index 08c896d0f3..c3dd26cf8f 100644 --- a/src/common/GPOWrapper/GPOWrapper.cpp +++ b/src/common/GPOWrapper/GPOWrapper.cpp @@ -176,6 +176,10 @@ namespace winrt::PowerToys::GPOWrapper::implementation { return static_cast(powertoys_gpo::getAllowedAdvancedPasteOnlineAIModelsValue()); } + GpoRuleConfigured GPOWrapper::GetConfiguredNewPlusEnabledValue() + { + return static_cast(powertoys_gpo::getConfiguredNewPlusEnabledValue()); + } GpoRuleConfigured GPOWrapper::GetConfiguredWorkspacesEnabledValue() { return static_cast(powertoys_gpo::getConfiguredWorkspacesEnabledValue()); diff --git a/src/common/GPOWrapper/GPOWrapper.h b/src/common/GPOWrapper/GPOWrapper.h index b0392cea1e..e4b12853d8 100644 --- a/src/common/GPOWrapper/GPOWrapper.h +++ b/src/common/GPOWrapper/GPOWrapper.h @@ -50,6 +50,7 @@ namespace winrt::PowerToys::GPOWrapper::implementation static GpoRuleConfigured GetConfiguredQoiPreviewEnabledValue(); static GpoRuleConfigured GetConfiguredQoiThumbnailsEnabledValue(); static GpoRuleConfigured GetAllowedAdvancedPasteOnlineAIModelsValue(); + static GpoRuleConfigured GetConfiguredNewPlusEnabledValue(); static GpoRuleConfigured GetConfiguredWorkspacesEnabledValue(); static GpoRuleConfigured GetConfiguredMwbClipboardSharingEnabledValue(); static GpoRuleConfigured GetConfiguredMwbFileTransferEnabledValue(); diff --git a/src/common/GPOWrapper/GPOWrapper.idl b/src/common/GPOWrapper/GPOWrapper.idl index 81ff61121a..7d00378389 100644 --- a/src/common/GPOWrapper/GPOWrapper.idl +++ b/src/common/GPOWrapper/GPOWrapper.idl @@ -54,6 +54,7 @@ namespace PowerToys static GpoRuleConfigured GetConfiguredQoiPreviewEnabledValue(); static GpoRuleConfigured GetConfiguredQoiThumbnailsEnabledValue(); static GpoRuleConfigured GetAllowedAdvancedPasteOnlineAIModelsValue(); + static GpoRuleConfigured GetConfiguredNewPlusEnabledValue(); static GpoRuleConfigured GetConfiguredWorkspacesEnabledValue(); static GpoRuleConfigured GetConfiguredMwbClipboardSharingEnabledValue(); static GpoRuleConfigured GetConfiguredMwbFileTransferEnabledValue(); diff --git a/src/common/ManagedCommon/ModuleType.cs b/src/common/ManagedCommon/ModuleType.cs index 6f15bc3a15..7a1913c05c 100644 --- a/src/common/ManagedCommon/ModuleType.cs +++ b/src/common/ManagedCommon/ModuleType.cs @@ -22,6 +22,7 @@ namespace ManagedCommon MouseJump, MousePointerCrosshairs, MouseWithoutBorders, + NewPlus, Peek, PowerRename, PowerLauncher, diff --git a/src/common/Themes/theme_helpers.cpp b/src/common/Themes/theme_helpers.cpp index 4357a38228..6d18f228ba 100644 --- a/src/common/Themes/theme_helpers.cpp +++ b/src/common/Themes/theme_helpers.cpp @@ -3,13 +3,23 @@ #include "dwmapi.h" #include #include -#pragma comment (lib,"Dwmapi.lib") +#pragma comment(lib, "Dwmapi.lib") #define DWMWA_USE_IMMERSIVE_DARK_MODE 20 #define HKEY_WINDOWS_THEME L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize" // based on https://stackoverflow.com/questions/51334674/how-to-detect-windows-10-light-dark-mode-in-win32-application -AppTheme ThemeHelpers::GetAppTheme() +Theme ThemeHelpers::GetAppTheme() +{ + return ThemeRegistryHelper(L"AppsUseLightTheme"); +} + +Theme ThemeHelpers::GetSystemTheme() +{ + return ThemeRegistryHelper(L"SystemUsesLightTheme"); +} + +Theme ThemeHelpers::ThemeRegistryHelper(LPCWSTR theme_key) { // The value is expected to be a REG_DWORD, which is a signed 32-bit little-endian auto buffer = std::vector(4); @@ -17,21 +27,22 @@ AppTheme ThemeHelpers::GetAppTheme() auto res = RegGetValueW( HKEY_CURRENT_USER, HKEY_WINDOWS_THEME, - L"AppsUseLightTheme", - RRF_RT_REG_DWORD, // expected value type + theme_key, + RRF_RT_REG_DWORD, nullptr, buffer.data(), &cbData); if (res != ERROR_SUCCESS) { - return AppTheme::Light; + // Defaulting to Light + return Theme::Light; } // convert bytes written to our buffer to an int, assuming little-endian auto i = static_cast(buffer[3] << 24 | buffer[2] << 16 | buffer[1] << 8 | buffer[0]); - return AppTheme(i); + return Theme(i); } void ThemeHelpers::SetImmersiveDarkMode(HWND window, bool enabled) diff --git a/src/common/Themes/theme_helpers.h b/src/common/Themes/theme_helpers.h index 026a971988..fd7c968ea1 100644 --- a/src/common/Themes/theme_helpers.h +++ b/src/common/Themes/theme_helpers.h @@ -1,7 +1,7 @@ #pragma once #include -enum class AppTheme +enum class Theme { Dark = 0, Light = 1 @@ -9,6 +9,10 @@ enum class AppTheme struct ThemeHelpers { - static AppTheme GetAppTheme(); - static void ThemeHelpers::SetImmersiveDarkMode(HWND window, bool enabled); + static Theme GetAppTheme(); + static Theme GetSystemTheme(); + static void SetImmersiveDarkMode(HWND window, bool enabled); + +protected: + static Theme ThemeRegistryHelper(LPCWSTR theme_key); }; \ No newline at end of file diff --git a/src/common/Themes/theme_listener.h b/src/common/Themes/theme_listener.h index d9c01925cf..a6ab4464fc 100644 --- a/src/common/Themes/theme_listener.h +++ b/src/common/Themes/theme_listener.h @@ -22,7 +22,7 @@ public: dwThreadId = 0; } - AppTheme AppTheme; + Theme AppTheme; void ThemeListener::AddChangedHandler(THEME_HANDLE handle); void ThemeListener::DelChangedHandler(THEME_HANDLE handle); void CheckTheme(); diff --git a/src/common/Themes/windows_colors.cpp b/src/common/Themes/windows_colors.cpp index e5a8eec732..365f875540 100644 --- a/src/common/Themes/windows_colors.cpp +++ b/src/common/Themes/windows_colors.cpp @@ -66,7 +66,7 @@ WindowsColors::Color WindowsColors::get_background_color() bool WindowsColors::is_dark_mode() { - return ThemeHelpers::GetAppTheme() == AppTheme::Dark; + return ThemeHelpers::GetAppTheme() == Theme::Dark; } bool WindowsColors::update() diff --git a/src/common/logger/logger_settings.h b/src/common/logger/logger_settings.h index 345286a38b..814547ef0b 100644 --- a/src/common/logger/logger_settings.h +++ b/src/common/logger/logger_settings.h @@ -69,6 +69,7 @@ struct LogSettings inline const static std::string environmentVariablesLoggerName = "environment-variables"; inline const static std::wstring cmdNotFoundLogPath = L"Logs\\cmd-not-found-log.txt"; inline const static std::string cmdNotFoundLoggerName = "cmd-not-found"; + inline const static std::string newLoggerName = "NewPlus"; inline const static std::string workspacesLauncherLoggerName = "workspaces-launcher"; inline const static std::wstring workspacesLauncherLogPath = L"workspaces-launcher-log.txt"; inline const static std::string workspacesSnapshotToolLoggerName = "workspaces-snapshot-tool"; diff --git a/src/common/utils/gpo.h b/src/common/utils/gpo.h index 8f71a21205..6fcfab48b6 100644 --- a/src/common/utils/gpo.h +++ b/src/common/utils/gpo.h @@ -60,6 +60,7 @@ namespace powertoys_gpo { const std::wstring POLICY_CONFIGURE_ENABLED_ENVIRONMENT_VARIABLES = L"ConfigureEnabledUtilityEnvironmentVariables"; const std::wstring POLICY_CONFIGURE_ENABLED_QOI_PREVIEW = L"ConfigureEnabledUtilityFileExplorerQOIPreview"; const std::wstring POLICY_CONFIGURE_ENABLED_QOI_THUMBNAILS = L"ConfigureEnabledUtilityFileExplorerQOIThumbnails"; + const std::wstring POLICY_CONFIGURE_ENABLED_NEWPLUS = L"ConfigureEnabledUtilityNewPlus"; const std::wstring POLICY_CONFIGURE_ENABLED_WORKSPACES = L"ConfigureEnabledUtilityWorkspaces"; // The registry value names for PowerToys installer and update policies. @@ -515,6 +516,11 @@ namespace powertoys_gpo { return getUtilityEnabledValue(POLICY_ALLOW_ADVANCED_PASTE_ONLINE_AI_MODELS); } + inline gpo_rule_configured_t getConfiguredNewPlusEnabledValue() + { + return getUtilityEnabledValue(POLICY_CONFIGURE_ENABLED_NEWPLUS); + } + inline gpo_rule_configured_t getConfiguredMwbClipboardSharingEnabledValue() { return getUtilityEnabledValue(POLICY_MWB_CLIPBOARD_SHARING_ENABLED); diff --git a/src/dsc/Microsoft.PowerToys.Configure/examples/disableAllModules.dsc.yaml b/src/dsc/Microsoft.PowerToys.Configure/examples/disableAllModules.dsc.yaml index 0cfc689316..5324df75ea 100644 --- a/src/dsc/Microsoft.PowerToys.Configure/examples/disableAllModules.dsc.yaml +++ b/src/dsc/Microsoft.PowerToys.Configure/examples/disableAllModules.dsc.yaml @@ -35,6 +35,8 @@ properties: Enabled: false MouseWithoutBorders: Enabled: false + NewPlus: + Enabled: false Peek: Enabled: false PowerRename: diff --git a/src/dsc/Microsoft.PowerToys.Configure/examples/enableAllModules.dsc.yaml b/src/dsc/Microsoft.PowerToys.Configure/examples/enableAllModules.dsc.yaml index c3bd636177..5fa895ddfd 100644 --- a/src/dsc/Microsoft.PowerToys.Configure/examples/enableAllModules.dsc.yaml +++ b/src/dsc/Microsoft.PowerToys.Configure/examples/enableAllModules.dsc.yaml @@ -35,6 +35,8 @@ properties: Enabled: true MouseWithoutBorders: Enabled: true + NewPlus: + Enabled: true Peek: Enabled: true PowerRename: diff --git a/src/gpo/assets/PowerToys.admx b/src/gpo/assets/PowerToys.admx index a79e6f627c..9386fa6e31 100644 --- a/src/gpo/assets/PowerToys.admx +++ b/src/gpo/assets/PowerToys.admx @@ -1,11 +1,11 @@ - + - + @@ -21,6 +21,7 @@ + @@ -335,6 +336,16 @@ + + + + + + + + + + diff --git a/src/gpo/assets/en-US/PowerToys.adml b/src/gpo/assets/en-US/PowerToys.adml index 3a616559fa..4fcc123114 100644 --- a/src/gpo/assets/en-US/PowerToys.adml +++ b/src/gpo/assets/en-US/PowerToys.adml @@ -1,7 +1,7 @@ - + PowerToys PowerToys @@ -26,6 +26,7 @@ PowerToys version 0.81.1 or later PowerToys version 0.83.0 or later PowerToys version 0.84.0 or later + PowerToys version 0.85.0 or later This policy configures the enabled state for all PowerToys utilities. @@ -217,6 +218,7 @@ If you disable or don't configure this policy, no predefined rules are applied. Mouse Jump: Configure enabled state Mouse Pointer Crosshairs: Configure enabled state Mouse Without Borders: Configure enabled state + New+: Configure enabled state Peek: Configure enabled state Power Rename: Configure enabled state PowerToys Run: Configure enabled state diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/AppxManifest.xml b/src/modules/NewPlus/NewShellExtensionContextMenu/AppxManifest.xml new file mode 100644 index 0000000000..c8a181f8ff --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/AppxManifest.xml @@ -0,0 +1,56 @@ + + + + + PowerToys New+ + Microsoft + Assets\NewPlus\StoreLogo.png + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/LargeTile.png b/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/LargeTile.png new file mode 100644 index 0000000000..f8a9f8f193 Binary files /dev/null and b/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/LargeTile.png differ diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/New_dark.ico b/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/New_dark.ico new file mode 100644 index 0000000000..dbe2f51a37 Binary files /dev/null and b/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/New_dark.ico differ diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/New_light.ico b/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/New_light.ico new file mode 100644 index 0000000000..5e9ae33a48 Binary files /dev/null and b/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/New_light.ico differ diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/Open_templates_dark.ico b/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/Open_templates_dark.ico new file mode 100644 index 0000000000..faf7cda080 Binary files /dev/null and b/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/Open_templates_dark.ico differ diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/Open_templates_light.ico b/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/Open_templates_light.ico new file mode 100644 index 0000000000..ebd453d7f4 Binary files /dev/null and b/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/Open_templates_light.ico differ diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/SmallTile.png b/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/SmallTile.png new file mode 100644 index 0000000000..8773d6366a Binary files /dev/null and b/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/SmallTile.png differ diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/SplashScreen.png b/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/SplashScreen.png new file mode 100644 index 0000000000..9d421b9526 Binary files /dev/null and b/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/SplashScreen.png differ diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/Square150x150Logo.png b/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/Square150x150Logo.png new file mode 100644 index 0000000000..f8a9f8f193 Binary files /dev/null and b/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/Square150x150Logo.png differ diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/Square44x44Logo.png b/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/Square44x44Logo.png new file mode 100644 index 0000000000..8773d6366a Binary files /dev/null and b/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/Square44x44Logo.png differ diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/StoreLogo.png b/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/StoreLogo.png new file mode 100644 index 0000000000..dfbca016bc Binary files /dev/null and b/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/StoreLogo.png differ diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/Wide310x150Logo.png b/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/Wide310x150Logo.png new file mode 100644 index 0000000000..266b41d25b Binary files /dev/null and b/src/modules/NewPlus/NewShellExtensionContextMenu/Assets/NewPlus/Wide310x150Logo.png differ diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj new file mode 100644 index 0000000000..9444e849c0 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj @@ -0,0 +1,236 @@ + + + + + + + + 17.0 + Win32Proj + {8acb33d9-c95b-47d4-8363-9731ee0930a0} + NewPlusShellExtension + 10.0.20348.0 + NewPlus.ShellExtension + + + + DynamicLibrary + true + v143 + Unicode + + + DynamicLibrary + false + v143 + true + Unicode + + + + + + + + + + + + .dll + ..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\ + PowerToys.NewPlus.ShellExtension + $(SolutionDir)$(Platform)\$(Configuration)\TemporaryBuild\obj\$(ProjectName)\ + + + + + ..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\ + PowerToys.NewPlus.ShellExtension + $(SolutionDir)$(Platform)\$(Configuration)\TemporaryBuild\obj\$(ProjectName)\ + + + + + + Level3 + true + WIN32;_DEBUG;NEWPLUSCONTEXTMENU_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + Use + pch.h + stdcpplatest + ..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories) + + + Windows + true + false + dll.def + runtimeobject.lib;$(CoreLibraryDependencies) + + + + + del $(OutDir)\NewPlusPackage.msix /q +MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv + + + + + + + + + Level3 + true + true + true + WIN32;NDEBUG;NEWPLUSCONTEXTMENU_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) + true + Use + pch.h + stdcpplatest + ..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories) + + + Windows + true + true + true + false + dll.def + runtimeobject.lib;$(CoreLibraryDependencies) + + + + + del $(OutDir)\NewPlusPackage.msix /q +MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create + + + + + + + + true + Document + $(OutDir)\Assets\NewPlus\Templates + + + true + Document + $(OutDir)\Assets\NewPlus\Templates\Example folder + + + true + Document + $(OutDir)\Assets\NewPlus\Templates\Example folder + + + + + {d9b8fc84-322a-4f9f-bbb9-20915c47ddfd} + + + {6955446d-23f7-4023-9bb3-8657f904af99} + + + {98537082-0fdb-40de-abd8-0dc5a4269bab} + + + {cc6e41ac-8174-4e8a-8d22-85dd7f4851df} + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + + Designer + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj.filters b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj.filters new file mode 100644 index 0000000000..2e8323a6f9 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj.filters @@ -0,0 +1,203 @@ + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Generated Files + + + Header Files + + + + + + Asset Files + + + Asset Files + + + Asset Files + + + Asset Files + + + Asset Files + + + Asset Files + + + Asset Files + + + Asset Files + + + Asset Files + + + Asset Files + + + Asset Files + + + Asset Files + + + Resource Files + + + Resource Files + + + Source Files + + + Source Files + + + Asset Files + + + Asset Files + + + Asset Files + + + Asset Files + + + Asset Files + + + Asset Files + + + Asset Files + + + Asset Files + + + Asset Files + + + Asset Files + + + Asset Files + + + Asset Files + + + + + {82bf7d46-1201-4fc2-99e0-6c1f48f97f1f} + + + {0c915b9c-0e6a-4d16-8620-507a10fc29c9} + + + {0c64a1a0-1f9e-4663-8649-454da469d15c} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {4f319851-7d86-4180-b3a3-fd497a320c34} + + + {a998f674-d126-488b-8457-7673fe986f94} + + + {b442cb0f-9f62-46e8-b269-fefa8ceacb21} + + + {e7904759-7b6c-4609-9c6d-e3eae3cb866c} + + + + + Generated Files + + + + + Template Examples\Example folder + + + Template Examples\Example folder + + + Template Examples + + + \ No newline at end of file diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/TemplateExamples/Any files or folders placed in the template folder are available via New+.txt b/src/modules/NewPlus/NewShellExtensionContextMenu/TemplateExamples/Any files or folders placed in the template folder are available via New+.txt new file mode 100644 index 0000000000..6f4b67067d --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/TemplateExamples/Any files or folders placed in the template folder are available via New+.txt @@ -0,0 +1 @@ +Learn more about New+ by visiting https://aka.ms/PowerToysOverview_NewPlus \ No newline at end of file diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/TemplateExamples/Example folder/Another example txt file.txt b/src/modules/NewPlus/NewShellExtensionContextMenu/TemplateExamples/Example folder/Another example txt file.txt new file mode 100644 index 0000000000..c2e12898d5 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/TemplateExamples/Example folder/Another example txt file.txt @@ -0,0 +1 @@ +Another example txt file \ No newline at end of file diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/TemplateExamples/Example folder/Example txt file.txt b/src/modules/NewPlus/NewShellExtensionContextMenu/TemplateExamples/Example folder/Example txt file.txt new file mode 100644 index 0000000000..e3c944dc54 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/TemplateExamples/Example folder/Example txt file.txt @@ -0,0 +1 @@ +Example txt file \ No newline at end of file diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/constants.h b/src/modules/NewPlus/NewShellExtensionContextMenu/constants.h new file mode 100644 index 0000000000..48c7054d36 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/constants.h @@ -0,0 +1,34 @@ +#pragma once + +#include "pch.h" + +namespace newplus::constants::non_localizable +{ + constexpr WCHAR powertoy_key[] = L"NewPlus"; + + constexpr WCHAR powertoy_name[] = L"NewPlus"; + + constexpr WCHAR settings_json_data_file_path[] = L"\\settings.json"; + + constexpr WCHAR settings_json_key_hide_file_extension[] = L"HideFileExtension"; + + constexpr WCHAR settings_json_key_hide_starting_digits[] = L"HideStartingDigits"; + + constexpr WCHAR settings_json_key_template_location[] = L"TemplateLocation"; + + constexpr WCHAR context_menu_package_name[] = L"NewPlusContextMenu"; + + constexpr WCHAR msix_package_name[] = L"NewPlusPackage.msix"; + + constexpr WCHAR module_name[] = L"NewPlus.ShellExtension"; + + constexpr WCHAR new_icon_light_resource_relative_path[] = L"\\Assets\\NewPlus\\New_light.ico"; + + constexpr WCHAR new_icon_dark_resource_relative_path[] = L"\\Assets\\NewPlus\\New_dark.ico"; + + constexpr WCHAR open_templates_icon_light_resource_relative_path[] = L"\\Assets\\NewPlus\\Open_templates_light.ico"; + + constexpr WCHAR open_templates_icon_dark_resource_relative_path[] = L"\\Assets\\NewPlus\\Open_templates_dark.ico"; + + constexpr WCHAR desktop_ini_filename[] = L"desktop.ini"; +} diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/dll.def b/src/modules/NewPlus/NewShellExtensionContextMenu/dll.def new file mode 100644 index 0000000000..f306dc6c13 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/dll.def @@ -0,0 +1,5 @@ +LIBRARY +EXPORTS +DllCanUnloadNow PRIVATE +DllGetClassObject PRIVATE +DllGetActivationFactory PRIVATE diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/dll_main.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/dll_main.cpp new file mode 100644 index 0000000000..5ad3537bb2 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/dll_main.cpp @@ -0,0 +1,41 @@ +#include "pch.h" + +#include "shell_context_menu.h" +#include "dll_main.h" +#include "trace.h" + +HMODULE module_instance_handle = 0; + +BOOL APIENTRY DllMain(HMODULE module_handle, DWORD ul_reason_for_call, LPVOID reserved) +{ + switch (ul_reason_for_call) + { + case DLL_PROCESS_ATTACH: + module_instance_handle = module_handle; + Trace::RegisterProvider(); + newplus::utilities::init_logger(); + break; + + case DLL_PROCESS_DETACH: + Trace::UnregisterProvider(); + break; + } + return TRUE; +} + +STDAPI DllGetActivationFactory(_In_ HSTRING activatableClassId, _COM_Outptr_ IActivationFactory** factory) +{ + return Module::GetModule().GetActivationFactory(activatableClassId, factory); +} + +STDAPI DllCanUnloadNow() +{ + return Module::GetModule().GetObjectCount() == 0 ? S_OK : S_FALSE; +} + +STDAPI DllGetClassObject(_In_ REFCLSID rclsid, _In_ REFIID riid, _Outptr_ LPVOID FAR* ppv) +{ + return Module::GetModule().GetClassObject(rclsid, riid, ppv); +} + +CoCreatableClass(shell_context_menu) diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/dll_main.h b/src/modules/NewPlus/NewShellExtensionContextMenu/dll_main.h new file mode 100644 index 0000000000..f6c4284a10 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/dll_main.h @@ -0,0 +1,3 @@ +#pragma once + +extern HMODULE module_instance_handle; \ No newline at end of file diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/new.base.rc b/src/modules/NewPlus/NewShellExtensionContextMenu/new.base.rc new file mode 100644 index 0000000000..529f658260 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/new.base.rc @@ -0,0 +1,49 @@ +#include +#include "Generated Files/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" + 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 + END +END + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_ICON1 ICON "Assets/NewPlus/New_light.ico" diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h b/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h new file mode 100644 index 0000000000..55bafb8b56 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/new_utilities.h @@ -0,0 +1,177 @@ +#pragma once + +#include "pch.h" + +#include +#include + +#include "constants.h" +#include "settings.h" + +#pragma comment(lib, "Shlwapi.lib") + +namespace newplus::utilities +{ + + inline std::wstring get_explorer_icon(std::filesystem::path path) + { + SHFILEINFO shell_file_info = { 0 }; + const std::wstring filepath = path.wstring(); + DWORD_PTR result = SHGetFileInfo(filepath.c_str(), 0, &shell_file_info, sizeof(shell_file_info), SHGFI_ICONLOCATION); + std::wstring icon_path = shell_file_info.szDisplayName; + if (icon_path != L"") + { + const int icon_index = shell_file_info.iIcon; + std::wstring icon_resource = icon_path + std::wstring(L",") + std::to_wstring(icon_index); + return icon_resource; + } + + WCHAR icon_resource_specifier[MAX_PATH] = { 0 }; + DWORD buffer_length = MAX_PATH; + const std::wstring extension = path.extension().wstring(); + const HRESULT hr = AssocQueryString(ASSOCF_INIT_IGNOREUNKNOWN, + ASSOCSTR_DEFAULTICON, + extension.c_str(), + NULL, + icon_resource_specifier, + &buffer_length); + const std::wstring icon_resource = icon_resource_specifier; + return icon_resource; + } + + inline bool is_hidden(const std::filesystem::path path) + { + const std::filesystem::path::string_type name = path.filename(); + if (name == constants::non_localizable::desktop_ini_filename) + { + return true; + } + + return false; + } + + inline bool wstring_same_when_comparing_ignore_case(std::wstring stringA, std::wstring stringB) + { + transform(stringA.begin(), stringA.end(), stringA.begin(), towupper); + transform(stringB.begin(), stringB.end(), stringB.begin(), towupper); + + return (stringA == stringB); + } + + inline void process_pending_window_messages(HWND window_handle = NULL) + { + if (window_handle == NULL) + { + window_handle = GetActiveWindow(); + } + + MSG current_message; + while (PeekMessage(¤t_message, window_handle, NULL, NULL, PM_REMOVE)) + { + DispatchMessage(¤t_message); + } + } + + inline std::wstring get_new_template_folder_location() + { + return NewSettingsInstance().GetTemplateLocation(); + } + + inline bool get_newplus_setting_hide_extension() + { + return NewSettingsInstance().GetHideFileExtension(); + } + + inline bool get_newplus_setting_hide_starting_digits() + { + return NewSettingsInstance().GetHideStartingDigits(); + } + + inline void create_folder_if_not_exist(const std::filesystem::path path) + { + std::filesystem::create_directory(path); + } + + inline std::wstring get_new_icon_resource_filepath(const HMODULE module_instance_handle, const Theme theme) + { + auto iconResourcePath = get_module_folderpath(module_instance_handle); + + if (theme == Theme::Dark) + { + iconResourcePath += constants::non_localizable::new_icon_dark_resource_relative_path; + } + else + { + // Defaulting to the Light icon + iconResourcePath += constants::non_localizable::new_icon_light_resource_relative_path; + } + + return iconResourcePath; + } + + inline std::wstring get_open_templates_icon_resource_filepath(const HMODULE module_instance_handle, const Theme theme) + { + auto iconResourcePath = get_module_folderpath(module_instance_handle); + + if (theme == Theme::Dark) + { + iconResourcePath += constants::non_localizable::open_templates_icon_dark_resource_relative_path; + } + else + { + // Defaulting to the Light icon + iconResourcePath += constants::non_localizable::open_templates_icon_light_resource_relative_path; + } + + return iconResourcePath; + } + + inline void init_logger() + { + LoggerHelpers::init_logger( + constants::non_localizable::powertoy_name, + constants::non_localizable::module_name, + LogSettings::newLoggerName); + } + + inline void register_msix_package() + { + if (package::IsWin11OrGreater()) + { + static const auto new_dll_path = get_module_folderpath(module_instance_handle); + auto new_package_uri = new_dll_path + L"\\" + constants::non_localizable::msix_package_name; + + if (!package::IsPackageRegistered(constants::non_localizable::context_menu_package_name)) + { + package::RegisterSparsePackage(new_dll_path, new_package_uri); + } + } + } + + inline std::wstring get_path_from_unknown_site(const ComPtr site_of_folder) + { + ComPtr service_provider; + site_of_folder->QueryInterface(IID_PPV_ARGS(&service_provider)); + ComPtr folder_view; + service_provider->QueryService(__uuidof(IFolderView), IID_PPV_ARGS(&folder_view)); + ComPtr shell_folder; + folder_view->GetFolder(IID_PPV_ARGS(&shell_folder)); + STRRET strings_returned; + shell_folder->GetDisplayNameOf(0, SHGDN_FORPARSING, &strings_returned); + LPWSTR path; + StrRetToStr(&strings_returned, NULL, &path); + return path; + } + + inline std::wstring get_path_from_folder_view(const ComPtr folder_view) + { + ComPtr shell_folder; + folder_view->GetFolder(IID_PPV_ARGS(&shell_folder)); + STRRET strings_returned; + shell_folder->GetDisplayNameOf(0, SHGDN_FORPARSING, &strings_returned); + LPWSTR path; + StrRetToStr(&strings_returned, NULL, &path); + return path; + } + +} diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/packages.config b/src/modules/NewPlus/NewShellExtensionContextMenu/packages.config new file mode 100644 index 0000000000..ff4b059648 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/pch.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/pch.cpp new file mode 100644 index 0000000000..1a6a81e238 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/pch.cpp @@ -0,0 +1,3 @@ +// pch.cpp: source file corresponding to the pre-compiled header + +#include "pch.h" diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/pch.h b/src/modules/NewPlus/NewShellExtensionContextMenu/pch.h new file mode 100644 index 0000000000..981209e500 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/pch.h @@ -0,0 +1,37 @@ +// Precompiled header file. + +#pragma once + +#define WIN32_LEAN_AND_MEAN +#define NOMCX +#define NOHELP +#define NOCOMM + +// Windows and STL +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// PowerToys project common +#include +#include +#include +#include +#include +#include + +// New project specific +#include "dll_main.h" +#include "template_folder.h" +#include "settings.h" +#include "new_utilities.h" diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/powertoys_module.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/powertoys_module.cpp new file mode 100644 index 0000000000..2962f236fc --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/powertoys_module.cpp @@ -0,0 +1,134 @@ +#include "pch.h" + +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include "constants.h" +#include "settings.h" +#include "trace.h" +#include "new_utilities.h" +#include "Generated Files/resource.h" + +// Note: Settings are managed via Settings and UI Settings +class NewModule : public PowertoyModuleIface +{ +public: + NewModule() + { + init_settings(); + } + + virtual const wchar_t* get_name() override + { + static const std::wstring localized_context_menu_item = + GET_RESOURCE_STRING_FALLBACK(IDS_CONTEXT_MENU_ITEM_NEW, L"New+"); + + return localized_context_menu_item.c_str(); + } + + virtual const wchar_t* get_key() override + { + // This setting key must match EnabledModules.cs [JsonPropertyName("NewPlus")] + return newplus::constants::non_localizable::powertoy_key; + } + + virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override + { + return powertoys_gpo::getConfiguredNewPlusEnabledValue(); + } + + virtual bool get_config(_Out_ PWSTR buffer, _Out_ int* buffer_size) override + { + // Not implemented as Settings are propagating via json + return true; + } + + virtual void set_config(PCWSTR config) override + { + // The following just checks to see if the Template Location was changed for metrics purposes + // Note: We are not saving the settings here and instead relying on read/write of json in Settings App .cs code paths + try + { + json::JsonObject config_as_json = json::JsonValue::Parse(winrt::to_hstring(config)).GetObjectW(); + + const auto latest_location_value = config_as_json.GetNamedString(newplus::constants::non_localizable::settings_json_key_template_location).data(); + const auto existing_location_value = NewSettingsInstance().GetTemplateLocation(); + + if (!newplus::utilities::wstring_same_when_comparing_ignore_case(latest_location_value, existing_location_value)) + { + Trace::EventChangedTemplateLocation(); + } + } + catch (std::exception& e) + { + Logger::error("Configuration parsing failed: {}", std::string{ e.what() }); + } + } + + virtual bool is_enabled_by_default() const override + { + return false; + } + + virtual void enable() override + { + Logger::info("New+ enabled via Settings UI"); + + newplus::utilities::register_msix_package(); + + powertoy_new_enabled = true; + } + + virtual void disable() override + { + Logger::info("New+ disabled via Settings UI"); + + powertoy_new_enabled = false; + } + + virtual bool is_enabled() override + { + return powertoy_new_enabled; + } + + virtual void hide_file_extension(bool hide_file_extension) + { + Logger::info("New+ hide file extension {}", hide_file_extension); + } + + virtual void hide_starting_digits(bool hide_starting_digits) + { + Logger::info("New+ hide starting digits {}", hide_starting_digits); + } + + virtual void template_location(std::wstring path_location) + { + Logger::info("New+ template location"); + } + + virtual void destroy() override + { + delete this; + } + +private: + bool powertoy_new_enabled = false; + + void init_settings() + { + powertoy_new_enabled = NewSettingsInstance().GetEnabled(); + } +}; + +extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() +{ + return new NewModule(); +} diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/resource.base.h b/src/modules/NewPlus/NewShellExtensionContextMenu/resource.base.h new file mode 100644 index 0000000000..09a4567ccc --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/resource.base.h @@ -0,0 +1,13 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by AlwaysOnTop.rc + +////////////////////////////// +// Non-localizable + +#define FILE_DESCRIPTION "PowerToys.New+" +#define INTERNAL_NAME "PowerToys.New+" +#define ORIGINAL_FILENAME "PowerToys.NewPlus.ShellExtension.dll" + +// Non-localizable +////////////////////////////// diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/resources.resx b/src/modules/NewPlus/NewShellExtensionContextMenu/resources.resx new file mode 100644 index 0000000000..7db1823f95 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/resources.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + New+ + The main context menu item that users click on. This should be localized to match New in Windows. e.g. Danish it would become Ny+ + + + Open templates + The menu item in the context menu that enables user to open the folder that contains their templates. + + + Templates + Default subfolder name where templates are stored. + + \ No newline at end of file diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/settings.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/settings.cpp new file mode 100644 index 0000000000..a7ce32998d --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/settings.cpp @@ -0,0 +1,225 @@ +#include "pch.h" + +#include +#include +#include + +#include "settings.h" +#include "constants.h" +#include "Generated Files/resource.h" + +// NewSettings are stored in PowerToys/New/settings.json +// The New PowerToy enabled state is stored in the general PowerToys/settings.json + +static bool LastModifiedTime(const std::wstring& file_Path, FILETIME* returned_file_timestamp) +{ + WIN32_FILE_ATTRIBUTE_DATA attr{}; + if (GetFileAttributesExW(file_Path.c_str(), GetFileExInfoStandard, &attr)) + { + *returned_file_timestamp = attr.ftLastWriteTime; + return true; + } + return false; +} + +NewSettings::NewSettings() +{ + // New+ overall enable state is stored in the general settings json file + general_settings_json_file_path = PTSettingsHelper::get_powertoys_general_save_file_location(); + + // New+' actual settings are stored in new_settings_json_file_path + std::wstring settings_save_path = PTSettingsHelper::get_module_save_folder_location(newplus::constants::non_localizable::powertoy_key); + new_settings_json_file_path = settings_save_path + newplus::constants::non_localizable::settings_json_data_file_path; + + RefreshEnabledState(); + + Load(); +} + +void NewSettings::Save() +{ + json::JsonObject new_settings_json_data; + + new_settings_json_data.SetNamedValue(newplus::constants::non_localizable::settings_json_key_hide_file_extension, + json::value(new_settings.hide_file_extension)); + + new_settings_json_data.SetNamedValue(newplus::constants::non_localizable::settings_json_key_hide_starting_digits, + json::value(new_settings.hide_starting_digits)); + + new_settings_json_data.SetNamedValue(newplus::constants::non_localizable::settings_json_key_template_location, + json::value(new_settings.template_location)); + + json::to_file(new_settings_json_file_path, new_settings_json_data); + + GetSystemTimeAsFileTime(&new_settings_last_loaded_timestamp); +} + +void NewSettings::Load() +{ + if (!std::filesystem::exists(new_settings_json_file_path)) + { + InitializeWithDefaultSettings(); + + Save(); + } + else + { + ParseJson(); + } +} + +void NewSettings::InitializeWithDefaultSettings() +{ + // Init the default New settings - in case the New/settings.json doesn't exist + // Currently a similar defaulting logic is also in InitializeWithDefaultSettings in NewViewModel.cs + SetHideFileExtension(true); + + SetTemplateLocation(GetTemplateLocationDefaultPath()); +} + +void NewSettings::RefreshEnabledState() +{ + // Load json general settings from data file, if it was modified since we last checked + FILETIME last_modified_timestamp{}; + if (!(LastModifiedTime(general_settings_json_file_path, &last_modified_timestamp) && + CompareFileTime(&last_modified_timestamp, &general_settings_last_loaded_timestamp) == 1)) + { + return; + } + + general_settings_last_loaded_timestamp = last_modified_timestamp; + + auto json = json::from_file(general_settings_json_file_path); + if (!json) + { + return; + } + + // Load the enabled settings for the New PowerToy via the general settings + const json::JsonObject& json_general_settings = json.value(); + try + { + json::JsonObject powertoy_new_enabled_state; + json::get(json_general_settings, L"enabled", powertoy_new_enabled_state, json::JsonObject{}); + json::get(powertoy_new_enabled_state, newplus::constants::non_localizable::powertoy_key, new_settings.enabled, false); + } + catch (const winrt::hresult_error&) + { + Logger::error(L"New+ unable to load enabled state from json"); + } +} + +void NewSettings::Reload() +{ + // Load json New settings from data file, if it was modified since we last checked. + FILETIME very_latest_modified_timestamp{}; + if (LastModifiedTime(new_settings_json_file_path, &very_latest_modified_timestamp) && + CompareFileTime(&very_latest_modified_timestamp, &new_settings_last_loaded_timestamp) == 1) + { + Load(); + } +} + +void NewSettings::ParseJson() +{ + auto json = json::from_file(new_settings_json_file_path); + if (json) + { + try + { + const json::JsonObject& new_settings_json = json.value(); + + if (json::has(new_settings_json, newplus::constants::non_localizable::settings_json_key_hide_file_extension, json::JsonValueType::Boolean)) + { + new_settings.hide_file_extension = new_settings_json.GetNamedBoolean( + newplus::constants::non_localizable::settings_json_key_hide_file_extension); + } + + if (json::has(new_settings_json, newplus::constants::non_localizable::settings_json_key_hide_starting_digits, json::JsonValueType::Boolean)) + { + new_settings.hide_starting_digits = new_settings_json.GetNamedBoolean( + newplus::constants::non_localizable::settings_json_key_hide_starting_digits); + } + + if (json::has(new_settings_json, newplus::constants::non_localizable::settings_json_key_template_location, json::JsonValueType::String)) + { + new_settings.template_location = new_settings_json.GetNamedString( + newplus::constants::non_localizable::settings_json_key_template_location); + } + } + catch (const winrt::hresult_error&) + { + } + } + GetSystemTimeAsFileTime(&new_settings_last_loaded_timestamp); +} + +bool NewSettings::GetEnabled() +{ + auto gpoSetting = powertoys_gpo::getConfiguredNewPlusEnabledValue(); + if (gpoSetting == powertoys_gpo::gpo_rule_configured_enabled) + { + return true; + } + if (gpoSetting == powertoys_gpo::gpo_rule_configured_disabled) + { + return false; + } + + Reload(); + + RefreshEnabledState(); + + return new_settings.enabled; +} + +bool NewSettings::GetHideFileExtension() const +{ + return new_settings.hide_file_extension; +} + +void NewSettings::SetHideFileExtension(const bool hide_file_extension) +{ + new_settings.hide_file_extension = hide_file_extension; +} + +bool NewSettings::GetHideStartingDigits() const +{ + return new_settings.hide_starting_digits; +} + +void NewSettings::SetHideStartingDigits(const bool hide_starting_digits) +{ + new_settings.hide_starting_digits = hide_starting_digits; +} + +std::wstring NewSettings::GetTemplateLocation() const +{ + return new_settings.template_location; +} + +void NewSettings::SetTemplateLocation(const std::wstring template_location) +{ + new_settings.template_location = template_location; +} + +std::wstring NewSettings::GetTemplateLocationDefaultPath() +{ + static const std::wstring default_template_sub_folder_name = + GET_RESOURCE_STRING_FALLBACK( + IDS_DEFAULT_TEMPLATE_SUB_FOLDER_NAME_WHERE_TEMPLATES_ARE_STORED, + L"Templates"); + + static const std::wstring full_path = PTSettingsHelper::get_module_save_folder_location( + newplus::constants::non_localizable::powertoy_key) + + L"\\" + default_template_sub_folder_name; + + return full_path; +} + +NewSettings& NewSettingsInstance() +{ + static NewSettings instance; + + return instance; +} diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/settings.h b/src/modules/NewPlus/NewShellExtensionContextMenu/settings.h new file mode 100644 index 0000000000..545ba7b2cc --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/settings.h @@ -0,0 +1,45 @@ +#pragma once + +#include "pch.h" + +class NewSettings +{ +public: + NewSettings(); + + bool GetEnabled(); + bool GetHideFileExtension() const; + void SetHideFileExtension(const bool hide_file_extension); + bool GetHideStartingDigits() const; + void SetHideStartingDigits(const bool hide_starting_digits); + std::wstring GetTemplateLocation() const; + void SetTemplateLocation(const std::wstring template_location); + + void Save(); + void Load(); + +private: + struct Settings + { + // These values are not used + bool enabled{ false }; + bool hide_file_extension{ true }; + bool hide_starting_digits{ true }; + std::wstring template_location; + }; + + void RefreshEnabledState(); + void InitializeWithDefaultSettings(); + std::wstring GetTemplateLocationDefaultPath(); + + void Reload(); + void ParseJson(); + + Settings new_settings; + std::wstring general_settings_json_file_path; + std::wstring new_settings_json_file_path; + FILETIME general_settings_last_loaded_timestamp{}; + FILETIME new_settings_last_loaded_timestamp{}; +}; + +NewSettings& NewSettingsInstance(); diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_menu.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_menu.cpp new file mode 100644 index 0000000000..b3bf2992d5 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_menu.cpp @@ -0,0 +1,87 @@ +#include "pch.h" + +#include "shell_context_menu.h" +#include "shell_context_sub_menu.h" +#include "shell_context_sub_menu_item.h" +#include "template_folder.h" +#include "new_utilities.h" +#include "settings.h" +#include "trace.h" +#include "Generated Files/resource.h" + +using namespace Microsoft::WRL; +using namespace newplus; + +#pragma region IExplorerCommand +IFACEMETHODIMP shell_context_menu::GetTitle(_In_opt_ IShellItemArray*, _Outptr_result_nullonfailure_ PWSTR* returned_title) +{ + static const std::wstring localized_context_menu_item = + GET_RESOURCE_STRING_FALLBACK(IDS_CONTEXT_MENU_ITEM_NEW, L"New+"); + + return SHStrDup(localized_context_menu_item.c_str(), returned_title); +} + +IFACEMETHODIMP shell_context_menu::GetIcon(_In_opt_ IShellItemArray*, _Outptr_result_nullonfailure_ PWSTR* returned_icon) +{ + *returned_icon = nullptr; + + static const auto icon_resource_filepath = utilities::get_new_icon_resource_filepath(module_instance_handle, ThemeHelpers::GetAppTheme()); + + return SHStrDup(icon_resource_filepath.c_str(), returned_icon); +} + +IFACEMETHODIMP shell_context_menu::GetToolTip(_In_opt_ IShellItemArray*, _Outptr_result_nullonfailure_ PWSTR* returned_tool_tip) +{ + *returned_tool_tip = nullptr; + return E_NOTIMPL; +} + +IFACEMETHODIMP shell_context_menu::GetCanonicalName(_Out_ GUID* returned_id) +{ + *returned_id = __uuidof(this); + return S_OK; +} + +IFACEMETHODIMP shell_context_menu::GetState(_In_opt_ IShellItemArray*, _In_ BOOL, _Out_ EXPCMDSTATE* returned_state) +{ + if (!NewSettingsInstance().GetEnabled()) + { + *returned_state = ECS_HIDDEN; + } + else + { + *returned_state = ECS_ENABLED; + } + + return S_OK; +} + +IFACEMETHODIMP shell_context_menu::Invoke(_In_opt_ IShellItemArray*, _In_opt_ IBindCtx*) noexcept +{ + return E_NOTIMPL; +} + +IFACEMETHODIMP shell_context_menu::GetFlags(_Out_ EXPCMDFLAGS* returned_menu_item_flags) +{ + *returned_menu_item_flags = ECF_HASSUBCOMMANDS; + return S_OK; +} + +IFACEMETHODIMP shell_context_menu::EnumSubCommands(_COM_Outptr_ IEnumExplorerCommand** returned_enum_commands) +{ + auto e = Make(site_of_folder); + return e->QueryInterface(IID_PPV_ARGS(returned_enum_commands)); +} +#pragma endregion + +#pragma region IObjectWithSite +IFACEMETHODIMP shell_context_menu::SetSite(_In_ IUnknown* site) noexcept +{ + this->site_of_folder = site; + return S_OK; +} +IFACEMETHODIMP shell_context_menu::GetSite(_In_ REFIID riid, _COM_Outptr_ void** returned_site) noexcept +{ + return this->site_of_folder.CopyTo(riid, returned_site); +} +#pragma endregion diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_menu.h b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_menu.h new file mode 100644 index 0000000000..564069254d --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_menu.h @@ -0,0 +1,37 @@ +#pragma once + +#include "pch.h" + +using namespace Microsoft::WRL; + +#define NEW_SHELL_EXTENSION_EXPLORER_COMMAND_UUID_STR "69824FC6-4660-4A09-9E7C-48DA63C6CC0F" + +// File Explorer context menu "New+" +class __declspec(uuid(NEW_SHELL_EXTENSION_EXPLORER_COMMAND_UUID_STR)) shell_context_menu final : + public RuntimeClass< + RuntimeClassFlags, + IExplorerCommand, + IObjectWithSite> +{ +public: +#pragma region IExplorerCommand + IFACEMETHODIMP GetTitle(_In_opt_ IShellItemArray*, _Outptr_result_nullonfailure_ PWSTR* returned_title); + IFACEMETHODIMP GetIcon(_In_opt_ IShellItemArray*, _Outptr_result_nullonfailure_ PWSTR* returned_icon); + IFACEMETHODIMP GetToolTip(_In_opt_ IShellItemArray*, _Outptr_result_nullonfailure_ PWSTR* returned_tool_tip); + IFACEMETHODIMP GetCanonicalName(_Out_ GUID* returned_id); + IFACEMETHODIMP GetState(_In_opt_ IShellItemArray*, _In_ BOOL, _Out_ EXPCMDSTATE* returned_state); + IFACEMETHODIMP Invoke(_In_opt_ IShellItemArray*, _In_opt_ IBindCtx*) noexcept; + IFACEMETHODIMP GetFlags(_Out_ EXPCMDFLAGS* returned_menu_item_flags); + IFACEMETHODIMP EnumSubCommands(_COM_Outptr_ IEnumExplorerCommand** returned_enum_commands); +#pragma endregion + +#pragma region IObjectWithSite + IFACEMETHODIMP SetSite(_In_ IUnknown* site) noexcept; + + IFACEMETHODIMP GetSite(_In_ REFIID riid, _COM_Outptr_ void** site) noexcept; +#pragma endregion + +protected: + HINSTANCE instance_handle = 0; + ComPtr site_of_folder; +}; diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu.cpp new file mode 100644 index 0000000000..148dffd952 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu.cpp @@ -0,0 +1,80 @@ +#include "pch.h" +#include "shell_context_sub_menu.h" +#include "trace.h" + +using namespace Microsoft::WRL; + +// // Sub context menu command enumerator +shell_context_sub_menu::shell_context_sub_menu(const ComPtr site_of_folder) +{ + this->site_of_folder = site_of_folder; + + // Determine the New+ Template folder location + const std::filesystem::path root = utilities::get_new_template_folder_location(); + + // Create the New+ Template folder location if it doesn't exist (very rare scenario) + utilities::create_folder_if_not_exist(root); + + // Scan the folder for any files and folders (the templates) + templates = new template_folder(root); + templates->rescan_template_folder(); + + // Add template items to context menu + const auto number_of_templates = templates->list_of_templates.size(); + int index = 0; + for (int i = 0; i < number_of_templates; i++) + { + explorer_menu_item_commands.push_back(Make(templates->get_template_item(i), site_of_folder)); + } + + // Add separator to context menu + explorer_menu_item_commands.push_back(Make()); + + // Add "Open templates" item to context menu + explorer_menu_item_commands.push_back(Make(root)); + + current_command = explorer_menu_item_commands.cbegin(); + + // Log that context menu was shown and with how many items + Trace::EventShowTemplateItems(number_of_templates); +} + +// IEnumExplorerCommand +IFACEMETHODIMP shell_context_sub_menu::Next(ULONG celt, __out_ecount_part(celt, *pceltFetched) IExplorerCommand** apUICommand, __out_opt ULONG* pceltFetched) +{ + ULONG fetched{ 0 }; + + if (pceltFetched) + { + *pceltFetched = 0ul; + } + + for (ULONG i = 0; (i < celt) && (current_command != explorer_menu_item_commands.cend()); i++) + { + current_command->CopyTo(&apUICommand[0]); + current_command++; + fetched++; + } + + if (pceltFetched) + { + *pceltFetched = fetched; + } + + return (fetched == celt) ? S_OK : S_FALSE; +} + +IFACEMETHODIMP shell_context_sub_menu::Skip(ULONG) +{ + return E_NOTIMPL; +} +IFACEMETHODIMP shell_context_sub_menu::Reset() +{ + current_command = explorer_menu_item_commands.cbegin(); + return S_OK; +} +IFACEMETHODIMP shell_context_sub_menu::Clone(__deref_out IEnumExplorerCommand** ppenum) +{ + *ppenum = nullptr; + return E_NOTIMPL; +} diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu.h b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu.h new file mode 100644 index 0000000000..2fef78ed63 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu.h @@ -0,0 +1,29 @@ +#pragma once + +#include "pch.h" + +#include "template_folder.h" +#include "new_utilities.h" +#include "shell_context_sub_menu_item.h" + +using namespace Microsoft::WRL; +using namespace newplus; + +// // Sub context menu command enumerator +class shell_context_sub_menu final : public RuntimeClass, IEnumExplorerCommand> +{ +public: + shell_context_sub_menu(const ComPtr site_of_folder); + + // IEnumExplorerCommand + IFACEMETHODIMP Next(ULONG celt, __out_ecount_part(celt, *pceltFetched) IExplorerCommand** apUICommand, __out_opt ULONG* pceltFetched); + IFACEMETHODIMP Skip(ULONG); + IFACEMETHODIMP Reset(); + IFACEMETHODIMP Clone(__deref_out IEnumExplorerCommand** ppenum); + +protected: + std::vector> explorer_menu_item_commands; + std::vector>::const_iterator current_command; + template_folder* templates; + ComPtr site_of_folder; +}; diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu_item.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu_item.cpp new file mode 100644 index 0000000000..bdae885009 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu_item.cpp @@ -0,0 +1,160 @@ +#include "pch.h" +#include "shell_context_sub_menu_item.h" + +#include "trace.h" +#include "Generated Files/resource.h" + +using namespace Microsoft::WRL; + +// Sub context menu containing the actual list of templates +shell_context_sub_menu_item::shell_context_sub_menu_item() +{ + this->template_entry = nullptr; +} + +shell_context_sub_menu_item::shell_context_sub_menu_item(const template_item* template_entry, const ComPtr site_of_folder) +{ + this->template_entry = template_entry; + this->site_of_folder = site_of_folder; +} + +IFACEMETHODIMP shell_context_sub_menu_item::GetTitle(_In_opt_ IShellItemArray* items, _Outptr_result_nullonfailure_ PWSTR* title) +{ + return SHStrDup(this->template_entry->get_menu_title( + !utilities::get_newplus_setting_hide_extension(), + !utilities::get_newplus_setting_hide_starting_digits() + ).c_str(), title); +} + +IFACEMETHODIMP shell_context_sub_menu_item::GetIcon(_In_opt_ IShellItemArray*, _Outptr_result_nullonfailure_ PWSTR* icon) +{ + return SHStrDup(this->template_entry->get_explorer_icon().c_str(), icon); +} + +IFACEMETHODIMP shell_context_sub_menu_item::GetToolTip(_In_opt_ IShellItemArray*, _Outptr_result_nullonfailure_ PWSTR* infoTip) +{ + *infoTip = nullptr; + return E_NOTIMPL; +} +IFACEMETHODIMP shell_context_sub_menu_item::GetCanonicalName(_Out_ GUID* guidCommandName) +{ + *guidCommandName = GUID_NULL; + return S_OK; +} +IFACEMETHODIMP shell_context_sub_menu_item::GetState(_In_opt_ IShellItemArray* selection, _In_ BOOL, _Out_ EXPCMDSTATE* returned_state) +{ + // Commented out for performance reasons + + //DWORD object_count = 0; + //selection->GetCount(&object_count); + + //if (object_count == 1) + //{ + // *returned_state = ECS_ENABLED; + //} + //else + //{ + // *returned_state = ECS_HIDDEN; + //} + + *returned_state = ECS_ENABLED; + return S_OK; +} + +IFACEMETHODIMP shell_context_sub_menu_item::Invoke(_In_opt_ IShellItemArray*, _In_opt_ IBindCtx*) noexcept +{ + try + { + // Determine target path of where context menu was displayed + const auto target_path_name = utilities::get_path_from_unknown_site(site_of_folder); + + // Determine initial filename + std::filesystem::path source_fullpath = template_entry->path; + std::filesystem::path target_fullpath = std::wstring(target_path_name) + + L"\\" + + this->template_entry->get_target_filename(!utilities::get_newplus_setting_hide_starting_digits()); + + // Copy file and determine final filename + std::filesystem::path target_final_fullpath = this->template_entry->copy_object_to(GetActiveWindow(), target_fullpath); + + Trace::EventCopyTemplate(target_final_fullpath.extension().c_str()); + + // Refresh folder items + SHChangeNotify(SHCNE_CREATE, SHCNF_PATH | SHCNF_FLUSH, target_final_fullpath.wstring().c_str(), NULL); + + // Enter rename mode + this->template_entry->enter_rename_mode(site_of_folder, target_final_fullpath); + + Trace::EventCopyTemplateResult(S_OK); + + return S_OK; + } + catch (const std::exception& ex) + { + Trace::EventCopyTemplateResult(S_FALSE); + Logger::error(ex.what()); + } + + return S_FALSE; +} + +IFACEMETHODIMP shell_context_sub_menu_item::GetFlags(_Out_ EXPCMDFLAGS* returned_flags) +{ + *returned_flags = ECF_DEFAULT; + return S_OK; +} + +IFACEMETHODIMP shell_context_sub_menu_item::EnumSubCommands(_COM_Outptr_ IEnumExplorerCommand** enumCommands) +{ + *enumCommands = nullptr; + return E_NOTIMPL; +} + +// Sub context menu - separator +IFACEMETHODIMP separator_context_menu_item::GetTitle(_In_opt_ IShellItemArray* items, _Outptr_result_nullonfailure_ PWSTR* title) +{ + title = nullptr; + + // NOTE: Must by S_FALSE for the separator to show up + return S_FALSE; +} + +IFACEMETHODIMP separator_context_menu_item::GetIcon(_In_opt_ IShellItemArray*, _Outptr_result_nullonfailure_ PWSTR* icon) +{ + *icon = nullptr; + return E_NOTIMPL; +} + +IFACEMETHODIMP separator_context_menu_item::GetFlags(_Out_ EXPCMDFLAGS* returned_flags) +{ + *returned_flags = ECF_ISSEPARATOR; + return S_OK; +} + +// Sub context menu - "Open templates" New+ folder +template_folder_context_menu_item::template_folder_context_menu_item(const std::filesystem::path shell_template_folder) +{ + this->shell_template_folder = shell_template_folder; +} + +IFACEMETHODIMP template_folder_context_menu_item::GetTitle(_In_opt_ IShellItemArray* items, _Outptr_result_nullonfailure_ PWSTR* name) +{ + static const std::wstring localized_context_menu_item = + GET_RESOURCE_STRING_FALLBACK(IDS_CONTEXT_MENU_ITEM_OPEN_TEMPLATES, L"Open templates"); + + return SHStrDup(localized_context_menu_item.c_str(), name); +} + +IFACEMETHODIMP template_folder_context_menu_item::GetIcon(_In_opt_ IShellItemArray*, _Outptr_result_nullonfailure_ PWSTR* icon) +{ + return SHStrDup(utilities::get_open_templates_icon_resource_filepath(module_instance_handle, ThemeHelpers::GetAppTheme()).c_str(), icon); +} + +IFACEMETHODIMP template_folder_context_menu_item::Invoke(_In_opt_ IShellItemArray* selection, _In_opt_ IBindCtx*) noexcept +{ + Logger::info(L"Open templates folder"); + const std::wstring verb_hardcoded_do_not_change = L"open"; + ShellExecute(nullptr, verb_hardcoded_do_not_change.c_str(), shell_template_folder.c_str(), NULL, NULL, SW_SHOWNORMAL); + + return S_OK; +} diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu_item.h b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu_item.h new file mode 100644 index 0000000000..38d24973ed --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/shell_context_sub_menu_item.h @@ -0,0 +1,64 @@ +#pragma once + +#include "pch.h" +#include "template_folder.h" +#include "template_item.h" +#include "new_utilities.h" + +using namespace Microsoft::WRL; +using namespace newplus; + +// The sub-context-menu that displays the list of templates +class shell_context_sub_menu_item : public RuntimeClass, IExplorerCommand> +{ +public: + shell_context_sub_menu_item(const template_item* template_entry, const ComPtr site_of_folder); + + // IExplorerCommand + IFACEMETHODIMP GetTitle(_In_opt_ IShellItemArray* items, _Outptr_result_nullonfailure_ PWSTR* title); + + IFACEMETHODIMP GetIcon(_In_opt_ IShellItemArray*, _Outptr_result_nullonfailure_ PWSTR* icon); + + IFACEMETHODIMP GetToolTip(_In_opt_ IShellItemArray*, _Outptr_result_nullonfailure_ PWSTR* infoTip); + + IFACEMETHODIMP GetCanonicalName(_Out_ GUID* guidCommandName); + + IFACEMETHODIMP GetState(_In_opt_ IShellItemArray* selection, _In_ BOOL okToBeSlow, _Out_ EXPCMDSTATE* returned_state); + + IFACEMETHODIMP Invoke(_In_opt_ IShellItemArray* selection, _In_opt_ IBindCtx*) noexcept; + + IFACEMETHODIMP GetFlags(_Out_ EXPCMDFLAGS* returned_flags); + + IFACEMETHODIMP EnumSubCommands(_COM_Outptr_ IEnumExplorerCommand** enumCommands); + +protected: + shell_context_sub_menu_item(); + const template_item* template_entry; + ComPtr site_of_folder; +}; + +// Sub-context-menu separator between the list of templates menu-items and "Open templates" menu-item +class separator_context_menu_item final : public shell_context_sub_menu_item +{ +public: + IFACEMETHODIMP GetTitle(_In_opt_ IShellItemArray* items, _Outptr_result_nullonfailure_ PWSTR* title); + + IFACEMETHODIMP GetIcon(_In_opt_ IShellItemArray*, _Outptr_result_nullonfailure_ PWSTR* icon); + + IFACEMETHODIMP GetFlags(_Out_ EXPCMDFLAGS* returned_flags); +}; + +// Sub-context-menu - The "Open templates" menu-item +class template_folder_context_menu_item final : public shell_context_sub_menu_item +{ +public: + template_folder_context_menu_item(const std::filesystem::path shell_template_folder); + + IFACEMETHODIMP GetTitle(_In_opt_ IShellItemArray* items, _Outptr_result_nullonfailure_ PWSTR* name); + + IFACEMETHODIMP GetIcon(_In_opt_ IShellItemArray*, _Outptr_result_nullonfailure_ PWSTR* icon); + + IFACEMETHODIMP Invoke(_In_opt_ IShellItemArray* selection, _In_opt_ IBindCtx*) noexcept; + + std::filesystem::path shell_template_folder; +}; diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/template_folder.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/template_folder.cpp new file mode 100644 index 0000000000..b4bfdf63f5 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/template_folder.cpp @@ -0,0 +1,52 @@ +#include "pch.h" +#include +#include "template_folder.h" + +using namespace newplus; + +template_folder::template_folder(){}; + +template_folder::template_folder(const std::filesystem::path newplus_template_folder) +{ + this->template_folder_path = newplus_template_folder; +} + +void template_folder::init() +{ + rescan_template_folder(); +} + +void template_folder::rescan_template_folder() +{ + list_of_templates.clear(); + + std::list> dirs; + std::list> files; + for (const auto& entry : std::filesystem::directory_iterator(template_folder_path)) + { + if (entry.is_directory()) + { + dirs.push_back({ entry.path().wstring(), new template_item(entry) }); + } + else + { + if (!utilities::is_hidden(entry.path())) + { + files.push_back({ entry.path().wstring(), new template_item(entry) }); + } + } + } + + // List of templates are sorted, with template-directories/folders first then followed by template-files + dirs.sort(); + files.sort(); + list_of_templates = dirs; + list_of_templates.splice(list_of_templates.end(), files); +} + +template_item* template_folder::get_template_item(const int index) const +{ + auto it = list_of_templates.begin(); + std::advance(it, index); + return it->second; +} diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/template_folder.h b/src/modules/NewPlus/NewShellExtensionContextMenu/template_folder.h new file mode 100644 index 0000000000..fe644be2e9 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/template_folder.h @@ -0,0 +1,28 @@ +#pragma once + +#include "pch.h" +#include +#include +#include +#include +#include "template_item.h" + +namespace newplus +{ + class template_folder + { + public: + template_folder(const std::filesystem::path newplus_template_folder); + void rescan_template_folder(); + + std::filesystem::path template_folder_path; + std::list> list_of_templates; + + template_item* get_template_item(const int index) const; + + protected: + template_folder(); + void init(); + }; + +} \ No newline at end of file diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp new file mode 100644 index 0000000000..3c863be18e --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.cpp @@ -0,0 +1,147 @@ + + +#include "pch.h" +#include "template_item.h" +#include +#include "new_utilities.h" +#include +#include +#include +#include + +using namespace Microsoft::WRL; +using namespace newplus; + +template_item::template_item(const std::filesystem::path entry) +{ + path = entry; +} + +std::wstring template_item::get_menu_title(const bool show_extension, const bool show_starting_digits) const +{ + std::wstring title = path.filename(); + + if (!show_starting_digits) + { + // Hide starting digits, spaces, and . + title = remove_starting_digits_from_filename(title); + } + + if (show_extension || !path.has_extension()) + { + return title; + } + + std::wstring ext = path.extension(); + title = title.substr(0, title.length() - ext.length()); + + return title; +} + +std::wstring template_item::get_target_filename(const bool include_starting_digits) const +{ + std::wstring filename = path.filename(); + + if (!include_starting_digits) + { + // Remove starting digits, spaces, and . + filename = remove_starting_digits_from_filename(filename); + } + + return filename; +} + +std::wstring template_item::remove_starting_digits_from_filename(std::wstring filename) const +{ + filename.erase(0, min(filename.find_first_not_of(L"0123456789 ."), filename.size())); + + return filename; +} + +std::wstring template_item::get_explorer_icon() const +{ + return utilities::get_explorer_icon(path); +} + +std::filesystem::path template_item::copy_object_to(const HWND window_handle, const std::filesystem::path destination) const +{ + // SHFILEOPSTRUCT wants the from and to paths to be terminated with two NULLs, + wchar_t double_terminated_path_from[MAX_PATH + 1] = { 0 }; + wcsncpy_s(double_terminated_path_from, this->path.c_str(), this->path.string().length()); + double_terminated_path_from[this->path.string().length() + 1] = 0; + + wchar_t double_terminated_path_to[MAX_PATH + 1] = { 0 }; + wcsncpy_s(double_terminated_path_to, destination.c_str(), destination.string().length()); + double_terminated_path_to[destination.string().length() + 1] = 0; + + SHFILEOPSTRUCT file_operation_params = { 0 }; + file_operation_params.wFunc = FO_COPY; + file_operation_params.hwnd = window_handle; + file_operation_params.pFrom = double_terminated_path_from; + file_operation_params.pTo = double_terminated_path_to; + file_operation_params.fFlags = FOF_RENAMEONCOLLISION | FOF_ALLOWUNDO | FOF_NOCONFIRMMKDIR | FOF_NOCOPYSECURITYATTRIBS | FOF_WANTMAPPINGHANDLE; + + const int result = SHFileOperation(&file_operation_params); + + if (!file_operation_params.hNameMappings) + { + // No file name collision on copy + return destination; + } + + struct file_operation_collision_mapping + { + int index; + SHNAMEMAPPING* mapping; + }; + + file_operation_collision_mapping* mapping = static_cast(file_operation_params.hNameMappings); + SHNAMEMAPPING* map = &mapping->mapping[0]; + std::wstring final_path(map->pszNewPath); + + SHFreeNameMappings(file_operation_params.hNameMappings); + + return final_path; +} + +void template_item::enter_rename_mode(const ComPtr site, const std::filesystem::path target_fullpath) const +{ + std::thread thread_for_renaming_workaround(rename_on_other_thread_workaround, site, target_fullpath); + thread_for_renaming_workaround.detach(); +} + +void template_item::rename_on_other_thread_workaround(const ComPtr site, const std::filesystem::path target_fullpath) +{ + // Have been unable to have Windows Explorer Shell enter rename mode from the main thread + // Sleep for a bit to only enter rename mode when icon has been drawn. Not strictly needed. + const std::chrono::milliseconds approx_wait_for_icon_redraw_not_needed{ 350 }; + std::this_thread::sleep_for(std::chrono::milliseconds(approx_wait_for_icon_redraw_not_needed)); + + const std::wstring filename = target_fullpath.filename(); + + ComPtr service_provider; + site->QueryInterface(IID_PPV_ARGS(&service_provider)); + ComPtr folder_view; + service_provider->QueryService(__uuidof(IFolderView), IID_PPV_ARGS(&folder_view)); + + int count = 0; + folder_view->ItemCount(SVGIO_ALLVIEW, &count); + + for (int i = 0; i < count; ++i) + { + std::wstring path_of_item(MAX_PATH, 0); + LPITEMIDLIST pidl; + + folder_view->Item(i, &pidl); + SHGetPathFromIDList(pidl, &path_of_item[0]); + CoTaskMemFree(pidl); + + std::wstring current_filename = std::filesystem::path(path_of_item.c_str()).filename(); + + if (utilities::wstring_same_when_comparing_ignore_case(filename, current_filename)) + { + folder_view->SelectItem(i, SVSI_EDIT | SVSI_SELECT | SVSI_DESELECTOTHERS | SVSI_ENSUREVISIBLE | SVSI_FOCUSED); + break; + } + } +} diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.h b/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.h new file mode 100644 index 0000000000..ffb1e8839f --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/template_item.h @@ -0,0 +1,35 @@ +#pragma once + +#include "pch.h" +#include +#include +#include +#include + +using namespace Microsoft::WRL; + +namespace newplus +{ + class template_item + { + public: + template_item(const std::filesystem::path entry); + + std::wstring get_menu_title(const bool show_extension, const bool show_starting_digits) const; + + std::wstring get_target_filename(const bool include_starting_digits) const; + + std::wstring get_explorer_icon() const; + + std::filesystem::path copy_object_to(const HWND window_handle, const std::filesystem::path destination) const; + + void enter_rename_mode(const ComPtr site, const std::filesystem::path target_folder) const; + + std::filesystem::path path; + + private: + static void rename_on_other_thread_workaround(const ComPtr site, const std::filesystem::path target_fullpath); + + std::wstring remove_starting_digits_from_filename(std::wstring filename) const; + }; +} \ No newline at end of file diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/trace.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/trace.cpp new file mode 100644 index 0000000000..0def375024 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/trace.cpp @@ -0,0 +1,70 @@ +#include "pch.h" + +#include "trace.h" +#include + +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); +} + +void Trace::EventToggleOnOff(_In_ const bool enabled) noexcept +{ + TraceLoggingWrite( + g_hProvider, + "NewPlus_EventToggleOnOff", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE), + TraceLoggingBoolean(enabled, "Enabled")); +} + +void Trace::EventChangedTemplateLocation() noexcept +{ + TraceLoggingWrite( + g_hProvider, + "NewPlus_ChangedTemplateLocation", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); +} + +void Trace::EventShowTemplateItems(const size_t number_of_templates) noexcept +{ + TraceLoggingWrite( + g_hProvider, + "NewPlus_EventShowTemplateItems", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE), + TraceLoggingValue(number_of_templates, "Count")); +} + +void Trace::EventCopyTemplate(_In_ const std::wstring template_file_extension) noexcept +{ + TraceLoggingWrite( + g_hProvider, + "NewPlus_EventCopyTemplate", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE), + TraceLoggingWideString(template_file_extension.c_str(), "Ext")); +} + +void Trace::EventCopyTemplateResult(_In_ const HRESULT hr) noexcept +{ + TraceLoggingWrite( + g_hProvider, + "NewPlus_EventCopyTemplateResult", + ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance), + TraceLoggingHResult(hr), + TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE)); +} diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/trace.h b/src/modules/NewPlus/NewShellExtensionContextMenu/trace.h new file mode 100644 index 0000000000..2d87e88588 --- /dev/null +++ b/src/modules/NewPlus/NewShellExtensionContextMenu/trace.h @@ -0,0 +1,15 @@ +#pragma once + +#include "pch.h" + +class Trace +{ +public: + static void RegisterProvider() noexcept; + static void UnregisterProvider() noexcept; + static void EventToggleOnOff(_In_ const bool new_enabled_state) noexcept; + static void EventChangedTemplateLocation() noexcept; + static void EventShowTemplateItems(_In_ const size_t number_of_templates) noexcept; + static void EventCopyTemplate(_In_ const std::wstring template_file_extension) noexcept; + static void EventCopyTemplateResult(_In_ const HRESULT hr) noexcept; +}; diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorLibrary/EditKeyboardWindow.cpp b/src/modules/keyboardmanager/KeyboardManagerEditorLibrary/EditKeyboardWindow.cpp index 6cc78c78ab..5779f8739e 100644 --- a/src/modules/keyboardmanager/KeyboardManagerEditorLibrary/EditKeyboardWindow.cpp +++ b/src/modules/keyboardmanager/KeyboardManagerEditorLibrary/EditKeyboardWindow.cpp @@ -49,7 +49,7 @@ static ThemeListener theme_listener{}; static void handleTheme() { auto theme = theme_listener.AppTheme; - auto isDark = theme == AppTheme::Dark; + auto isDark = theme == Theme::Dark; Logger::info(L"Theme is now {}", isDark ? L"Dark" : L"Light"); if (hwndEditKeyboardNativeWindow != nullptr) { @@ -137,7 +137,7 @@ inline void CreateEditKeyboardWindowImpl(HINSTANCE hInst, KBMEditor::KeyboardMan windowClass.lpfnWndProc = EditKeyboardWindowProc; windowClass.hInstance = hInst; windowClass.lpszClassName = szWindowClass; - windowClass.hbrBackground = CreateSolidBrush((ThemeHelpers::GetAppTheme() == AppTheme::Dark) ? 0x00000000 : 0x00FFFFFF); + windowClass.hbrBackground = CreateSolidBrush((ThemeHelpers::GetAppTheme() == Theme::Dark) ? 0x00000000 : 0x00FFFFFF); windowClass.hIcon = static_cast(LoadImageW( windowClass.hInstance, MAKEINTRESOURCE(IDS_KEYBOARDMANAGER_ICON), diff --git a/src/modules/keyboardmanager/KeyboardManagerEditorLibrary/EditShortcutsWindow.cpp b/src/modules/keyboardmanager/KeyboardManagerEditorLibrary/EditShortcutsWindow.cpp index e36bcf4d7c..9656598b4e 100644 --- a/src/modules/keyboardmanager/KeyboardManagerEditorLibrary/EditShortcutsWindow.cpp +++ b/src/modules/keyboardmanager/KeyboardManagerEditorLibrary/EditShortcutsWindow.cpp @@ -43,7 +43,7 @@ static ThemeListener theme_listener{}; static void handleTheme() { auto theme = theme_listener.AppTheme; - auto isDark = theme == AppTheme::Dark; + auto isDark = theme == Theme::Dark; Logger::info(L"Theme is now {}", isDark ? L"Dark" : L"Light"); if (hwndEditShortcutsNativeWindow != nullptr) { @@ -90,7 +90,7 @@ inline void CreateEditShortcutsWindowImpl(HINSTANCE hInst, KBMEditor::KeyboardMa windowClass.lpfnWndProc = EditShortcutsWindowProc; windowClass.hInstance = hInst; windowClass.lpszClassName = szWindowClass; - windowClass.hbrBackground = CreateSolidBrush((ThemeHelpers::GetAppTheme() == AppTheme::Dark) ? 0x00000000 : 0x00FFFFFF); + windowClass.hbrBackground = CreateSolidBrush((ThemeHelpers::GetAppTheme() == Theme::Dark) ? 0x00000000 : 0x00FFFFFF); windowClass.hIcon = static_cast(LoadImageW( windowClass.hInstance, MAKEINTRESOURCE(IDS_KEYBOARDMANAGER_ICON), diff --git a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.cpp b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.cpp index 52b140c047..d1c747fc65 100644 --- a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.cpp +++ b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.cpp @@ -43,7 +43,7 @@ HWND CurrentWindow; void handleTheme() { auto theme = theme_listener.AppTheme; - auto isDark = theme == AppTheme::Dark; + auto isDark = theme == Theme::Dark; Logger::info(L"Theme is now {}", isDark ? L"Dark" : L"Light"); ThemeHelpers::SetImmersiveDarkMode(CurrentWindow, isDark); } diff --git a/src/runner/main.cpp b/src/runner/main.cpp index a8f34854bc..dd35697e09 100644 --- a/src/runner/main.cpp +++ b/src/runner/main.cpp @@ -154,6 +154,7 @@ int runner(bool isProcessElevated, bool openSettings, std::string settingsWindow L"WinUI3Apps/PowerToys.FileLocksmithExt.dll", L"WinUI3Apps/PowerToys.RegistryPreviewExt.dll", L"WinUI3Apps/PowerToys.MeasureToolModuleInterface.dll", + L"WinUI3Apps/PowerToys.NewPlus.ShellExtension.dll", L"WinUI3Apps/PowerToys.HostsModuleInterface.dll", L"WinUI3Apps/PowerToys.Peek.dll", L"WinUI3Apps/PowerToys.EnvironmentVariablesModuleInterface.dll", diff --git a/src/runner/settings_window.cpp b/src/runner/settings_window.cpp index 6b48cfaadc..c8829773f7 100644 --- a/src/runner/settings_window.cpp +++ b/src/runner/settings_window.cpp @@ -684,6 +684,8 @@ std::string ESettingsWindowNames_to_string(ESettingsWindowNames value) return "Dashboard"; case ESettingsWindowNames::AdvancedPaste: return "AdvancedPaste"; + case ESettingsWindowNames::NewPlus: + return "NewPlus"; default: { Logger::error(L"Can't convert ESettingsWindowNames value={} to string", static_cast(value)); @@ -779,6 +781,10 @@ ESettingsWindowNames ESettingsWindowNames_from_string(std::string value) { return ESettingsWindowNames::AdvancedPaste; } + else if (value == "NewPlus") + { + return ESettingsWindowNames::NewPlus; + } else { Logger::error(L"Can't convert string value={} to ESettingsWindowNames", winrt::to_hstring(value)); diff --git a/src/runner/settings_window.h b/src/runner/settings_window.h index 741ad56479..6fd5afca0f 100644 --- a/src/runner/settings_window.h +++ b/src/runner/settings_window.h @@ -25,6 +25,7 @@ enum class ESettingsWindowNames CropAndLock, EnvironmentVariables, AdvancedPaste, + NewPlus, }; 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 75f783cf61..ebf70464d5 100644 --- a/src/settings-ui/Settings.UI.Library/EnabledModules.cs +++ b/src/settings-ui/Settings.UI.Library/EnabledModules.cs @@ -462,6 +462,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library } } + private bool newPlus; + + [JsonPropertyName("NewPlus")] // This key must match newplus::constants::non_localizable + public bool NewPlus + { + get => newPlus; + set + { + if (newPlus != value) + { + LogTelemetryEvent(value); + newPlus = value; + } + } + } + private bool workspaces = true; [JsonPropertyName("Workspaces")] diff --git a/src/settings-ui/Settings.UI.Library/NewPlusSettings.cs b/src/settings-ui/Settings.UI.Library/NewPlusSettings.cs new file mode 100644 index 0000000000..6010185f8c --- /dev/null +++ b/src/settings-ui/Settings.UI.Library/NewPlusSettings.cs @@ -0,0 +1,48 @@ +// 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 System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using Microsoft.PowerToys.Settings.UI.Library.Interfaces; +using Settings.UI.Library.Resources; + +namespace Microsoft.PowerToys.Settings.UI.Library +{ + public class NewPlusSettings : ISettingsConfig + { + public const string ModuleName = "NewPlus"; + + public void InitializeWithDefaultSettings() + { + // This code path should never happen + } + + public string ToJsonString() + { + return JsonSerializer.Serialize(this); + } + + [JsonPropertyName("HideFileExtension")] + public bool HideFileExtension { get; set; } + + [JsonPropertyName("HideStartingDigits")] + public bool HideStartingDigits { get; set; } + + [JsonPropertyName("TemplateLocation")] + public string TemplateLocation { get; set; } + + public string GetModuleName() + { + return ModuleName; + } + + public bool UpgradeSettingsConfiguration() + { + return false; + } + } +} diff --git a/src/settings-ui/Settings.UI.Library/Utilities/Helper.cs b/src/settings-ui/Settings.UI.Library/Utilities/Helper.cs index cace7f2288..3b0f377810 100644 --- a/src/settings-ui/Settings.UI.Library/Utilities/Helper.cs +++ b/src/settings-ui/Settings.UI.Library/Utilities/Helper.cs @@ -96,6 +96,12 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Utilities return Directory.GetParent(settingsPath).FullName; } + public static string GetPowerToysInstallationWinUI3AppsAssetsFolder() + { + // return .\PowerToys\WinUI3Apps\Assets + return Path.Combine(GetPowerToysInstallationFolder(), "WinUI3Apps", "Assets"); + } + private static readonly global::PowerToys.Interop.LayoutMapManaged LayoutMap = new global::PowerToys.Interop.LayoutMapManaged(); public static string GetKeyName(uint key) @@ -148,6 +154,30 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Utilities } } + public static void CopyDirectory(string source_directory, string destination_directory, bool copy_recursively) + { + var current_directory_info = new DirectoryInfo(source_directory); + + DirectoryInfo[] source_subdirectories = current_directory_info.GetDirectories(); + + Directory.CreateDirectory(destination_directory); + + foreach (FileInfo file in current_directory_info.GetFiles()) + { + string destination_file_path = Path.Combine(destination_directory, file.Name); + file.CopyTo(destination_file_path, true); + } + + if (copy_recursively) + { + foreach (DirectoryInfo subdirectory in source_subdirectories) + { + string newDestinationDir = Path.Combine(destination_directory, subdirectory.Name); + CopyDirectory(subdirectory.FullName, newDestinationDir, true); + } + } + } + public static readonly uint VirtualKeyWindows = global::PowerToys.Interop.Constants.VK_WIN_BOTH; } } diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Icons/NewPlus.png b/src/settings-ui/Settings.UI/Assets/Settings/Icons/NewPlus.png new file mode 100644 index 0000000000..1c35c610c8 Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Settings/Icons/NewPlus.png differ diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Modules/NewPlus.png b/src/settings-ui/Settings.UI/Assets/Settings/Modules/NewPlus.png new file mode 100644 index 0000000000..116a3f2969 Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Settings/Modules/NewPlus.png differ diff --git a/src/settings-ui/Settings.UI/Assets/Settings/Modules/OOBE/NewPlus.png b/src/settings-ui/Settings.UI/Assets/Settings/Modules/OOBE/NewPlus.png new file mode 100644 index 0000000000..637795c678 Binary files /dev/null and b/src/settings-ui/Settings.UI/Assets/Settings/Modules/OOBE/NewPlus.png differ diff --git a/src/settings-ui/Settings.UI/Helpers/ModuleHelper.cs b/src/settings-ui/Settings.UI/Helpers/ModuleHelper.cs index dd59fa02d9..c3eef509c0 100644 --- a/src/settings-ui/Settings.UI/Helpers/ModuleHelper.cs +++ b/src/settings-ui/Settings.UI/Helpers/ModuleHelper.cs @@ -62,6 +62,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers case ModuleType.MouseJump: return generalSettingsConfig.Enabled.MouseJump; case ModuleType.MousePointerCrosshairs: return generalSettingsConfig.Enabled.MousePointerCrosshairs; case ModuleType.MouseWithoutBorders: return generalSettingsConfig.Enabled.MouseWithoutBorders; + case ModuleType.NewPlus: return generalSettingsConfig.Enabled.NewPlus; case ModuleType.Peek: return generalSettingsConfig.Enabled.Peek; case ModuleType.PowerRename: return generalSettingsConfig.Enabled.PowerRename; case ModuleType.PowerLauncher: return generalSettingsConfig.Enabled.PowerLauncher; @@ -95,6 +96,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers case ModuleType.MouseJump: generalSettingsConfig.Enabled.MouseJump = isEnabled; break; case ModuleType.MousePointerCrosshairs: generalSettingsConfig.Enabled.MousePointerCrosshairs = isEnabled; break; case ModuleType.MouseWithoutBorders: generalSettingsConfig.Enabled.MouseWithoutBorders = isEnabled; break; + case ModuleType.NewPlus: generalSettingsConfig.Enabled.NewPlus = isEnabled; break; case ModuleType.Peek: generalSettingsConfig.Enabled.Peek = isEnabled; break; case ModuleType.PowerRename: generalSettingsConfig.Enabled.PowerRename = isEnabled; break; case ModuleType.PowerLauncher: generalSettingsConfig.Enabled.PowerLauncher = isEnabled; break; @@ -127,6 +129,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers case ModuleType.MouseJump: return GPOWrapper.GetConfiguredMouseJumpEnabledValue(); case ModuleType.MousePointerCrosshairs: return GPOWrapper.GetConfiguredMousePointerCrosshairsEnabledValue(); case ModuleType.MouseWithoutBorders: return GPOWrapper.GetConfiguredMouseWithoutBordersEnabledValue(); + case ModuleType.NewPlus: return GPOWrapper.GetConfiguredNewPlusEnabledValue(); case ModuleType.Peek: return GPOWrapper.GetConfiguredPeekEnabledValue(); case ModuleType.PowerRename: return GPOWrapper.GetConfiguredPowerRenameEnabledValue(); case ModuleType.PowerLauncher: return GPOWrapper.GetConfiguredPowerLauncherEnabledValue(); @@ -160,6 +163,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers ModuleType.MouseJump => typeof(MouseUtilsPage), ModuleType.MousePointerCrosshairs => typeof(MouseUtilsPage), ModuleType.MouseWithoutBorders => typeof(MouseWithoutBordersPage), + ModuleType.NewPlus => typeof(NewPlusPage), ModuleType.Peek => typeof(PeekPage), ModuleType.PowerRename => typeof(PowerRenamePage), ModuleType.PowerLauncher => typeof(PowerLauncherPage), diff --git a/src/settings-ui/Settings.UI/Helpers/ShellGetFolder.cs b/src/settings-ui/Settings.UI/Helpers/ShellGetFolder.cs index 32acc06a72..849655b4c9 100644 --- a/src/settings-ui/Settings.UI/Helpers/ShellGetFolder.cs +++ b/src/settings-ui/Settings.UI/Helpers/ShellGetFolder.cs @@ -26,6 +26,11 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers } public static string GetFolderDialog(IntPtr hwndOwner) + { + return GetFolderDialogWithFlags(hwndOwner, 0); + } + + public static string GetFolderDialogWithFlags(IntPtr hwndOwner, uint ulFlags) { // windows MAX_PATH with long path enable can be approximated 32k char long // allocating more than double (unicode) to hold the path @@ -37,7 +42,7 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers browseInfo.PidlRoot = IntPtr.Zero; browseInfo.PszDisplayName = null; browseInfo.LpszTitle = null; - browseInfo.UlFlags = 0; + browseInfo.UlFlags = ulFlags; browseInfo.Lpfn = null; browseInfo.LParam = IntPtr.Zero; browseInfo.IImage = 0; @@ -61,5 +66,10 @@ namespace Microsoft.PowerToys.Settings.UI.Helpers return sb.ToString(); } + + public struct FolderDialogFlags + { + public const uint _BIF_NEWDIALOGSTYLE = 0x00000040; + } } } diff --git a/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs b/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs index f5119535d5..14646c6647 100644 --- a/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs +++ b/src/settings-ui/Settings.UI/OOBE/Enums/PowerToysModules.cs @@ -33,5 +33,6 @@ namespace Microsoft.PowerToys.Settings.UI.OOBE.Enums Workspaces, WhatsNew, RegistryPreview, + NewPlus, } } diff --git a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj index f1cf072673..179fd04286 100644 --- a/src/settings-ui/Settings.UI/PowerToys.Settings.csproj +++ b/src/settings-ui/Settings.UI/PowerToys.Settings.csproj @@ -23,7 +23,6 @@ - diff --git a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs index f624ff1bba..8d38ec94c3 100644 --- a/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs +++ b/src/settings-ui/Settings.UI/SettingsXAML/App.xaml.cs @@ -424,6 +424,7 @@ namespace Microsoft.PowerToys.Settings.UI case "Peek": return typeof(PeekPage); case "CropAndLock": return typeof(CropAndLockPage); case "EnvironmentVariables": return typeof(EnvironmentVariablesPage); + case "NewPlus": return typeof(NewPlusPage); case "Workspaces": return typeof(WorkspacesPage); default: // Fallback to Dashboard diff --git a/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeNewPlus.xaml b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeNewPlus.xaml new file mode 100644 index 0000000000..5e657073f4 --- /dev/null +++ b/src/settings-ui/Settings.UI/SettingsXAML/OOBE/Views/OobeNewPlus.xaml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + +