From e950ba2e880b29f26997add4902b6454c30b49d0 Mon Sep 17 00:00:00 2001 From: projektion0 <98270100+projektion0@users.noreply.github.com> Date: Sat, 17 Oct 2026 23:59:28 +0200 Subject: [PATCH 1/5] Build: support Visual Studio 2026 and add a Start Menu solution --- BUILDING.md | 25 +++ Directory.Build.props | 10 ++ Directory.Build.targets | 10 ++ NuGet.Config | 7 + OpenShell-VS2026.sln | 155 ++++++++++++++++++ Src/ClassicExplorer/ClassicExplorer.vcxproj | 2 +- .../ClassicExplorerSettings.vcxproj | 2 +- Src/ClassicIE/ClassicIE.vcxproj | 2 +- .../ClassicIEDLL/ClassicIEDLL.vcxproj | 2 +- Src/Lib/Lib.vcxproj | 2 +- Src/Setup/Setup.vcxproj | 2 +- Src/Setup/SetupHelper/SetupHelper.vcxproj | 2 +- Src/Setup/Utility/Utility.vcxproj | 2 +- Src/Setup/en-US/en-US.vcxproj | 2 +- Src/Skins/ClassicSkin/ClassicSkin.vcxproj | 2 +- Src/Skins/ClassicSkin7/ClassicSkin7.vcxproj | 2 +- Src/Skins/FullGlass/FullGlass.vcxproj | 2 +- Src/Skins/Immersive/Immersive.vcxproj | 2 +- Src/Skins/Immersive7/Immersive7.vcxproj | 2 +- Src/Skins/Metallic7/Metallic7.vcxproj | 2 +- Src/Skins/Metro/Metro.vcxproj | 2 +- Src/Skins/Metro7/Metro7.vcxproj | 2 +- Src/Skins/Midnight7/Midnight7.vcxproj | 2 +- Src/Skins/SmokedGlass/SmokedGlass.vcxproj | 2 +- Src/Skins/Win7Aero/Win7Aero.vcxproj | 2 +- Src/Skins/Win7Aero7/Win7Aero7.vcxproj | 2 +- Src/Skins/Win7Basic/Win7Basic.vcxproj | 2 +- Src/Skins/Win8/Win8.vcxproj | 2 +- Src/Skins/Win87/Win87.vcxproj | 2 +- Src/Skins/WinXP/WinXP.vcxproj | 2 +- Src/StartMenu/StartMenu.vcxproj | 2 +- .../StartMenuDLL/StartMenuDLL.vcxproj | 2 +- .../StartMenuHelper/StartMenuHelper.vcxproj | 2 +- .../DesktopToasts/DesktopToasts.vcxproj | 2 +- Src/Update/Update.vcxproj | 2 +- 35 files changed, 237 insertions(+), 30 deletions(-) create mode 100644 BUILDING.md create mode 100644 Directory.Build.props create mode 100644 Directory.Build.targets create mode 100644 NuGet.Config create mode 100644 OpenShell-VS2026.sln diff --git a/BUILDING.md b/BUILDING.md new file mode 100644 index 000000000..79a1f0ad5 --- /dev/null +++ b/BUILDING.md @@ -0,0 +1,25 @@ +# Building the Start Menu with Visual Studio + +Open `OpenShell-VS2026.sln` in Visual Studio 2026 and choose **Release / x64**. +Install the Desktop development with C++ workload, C++ ATL support for the +selected toolset, and a Windows 11 SDK. Allow NuGet package restore. + +The root solution builds the Start Menu, its helper DLL, and the bundled skins. +It does not build an installer or the Classic Explorer/Classic IE components. +Build output is under `build/bin/Release64`; skins are copied into its `Skins` +subfolder. Local binaries are unsigned. Language DLLs are distributed separately. + +From a Visual Studio developer command prompt: + +```bat +MSBuild OpenShell-VS2026.sln /t:Restore /p:RestorePackagesConfig=true +MSBuild OpenShell-VS2026.sln /m /p:Configuration=Release /p:Platform=x64 +``` + +Projects select v145 with Visual Studio 2026 and retain v143 with Visual Studio +2022. `OpenShellPlatformToolset` can override this selection. The existing +`Src/OpenShell.sln` and release packaging scripts remain available; see +`Src/BUILDME.txt` for the additional installer tools. + +Release scripts can continue to supply `CS_VERSION` and `CS_VERSION_ORIG`. +Direct IDE builds default to version 4.4.1000. diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 000000000..6ba5aae28 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,10 @@ + + + + + v145 + v143 + 4.4.1000 + $(CS_VERSION) + + diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 000000000..d3636157a --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 000000000..de1952cda --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/OpenShell-VS2026.sln b/OpenShell-VS2026.sln new file mode 100644 index 000000000..e786a5388 --- /dev/null +++ b/OpenShell-VS2026.sln @@ -0,0 +1,155 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 18 +VisualStudioVersion = 18.10.12201.205 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "StartMenu", "Src\StartMenu\StartMenu.vcxproj", "{87D5FE20-AF86-458A-9AA3-3131EB06179B}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Lib", "Src\Lib\Lib.vcxproj", "{D42FE717-485B-492D-884A-1999F6D51154}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ClassicSkin7", "Src\Skins\ClassicSkin7\ClassicSkin7.vcxproj", "{31C016FB-9EA1-4AF5-987A-37210C04DA06}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ClassicSkin", "Src\Skins\ClassicSkin\ClassicSkin.vcxproj", "{9EC23CA9-384A-4EEB-979E-69879DC1A78C}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "FullGlass", "Src\Skins\FullGlass\FullGlass.vcxproj", "{066C9721-26D5-4C4D-868E-50C2BA0A8196}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Immersive7", "Src\Skins\Immersive7\Immersive7.vcxproj", "{75809D15-8403-420A-BBE6-05F478D88D8E}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Immersive", "Src\Skins\Immersive\Immersive.vcxproj", "{BD28B058-230E-42DF-9FB1-FFBB0153F498}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Metallic7", "Src\Skins\Metallic7\Metallic7.vcxproj", "{CA5BFC96-428D-42F5-9F7D-CDDE048A357C}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Metro7", "Src\Skins\Metro7\Metro7.vcxproj", "{598AB4AC-008E-4501-90B3-C5213834C1DA}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Metro", "Src\Skins\Metro\Metro.vcxproj", "{63BAF573-170B-4FA0-AEE3-16E04F3E9DF5}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Midnight7", "Src\Skins\Midnight7\Midnight7.vcxproj", "{7BD26CB3-5280-48FD-9A86-C13E321018D5}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SmokedGlass", "Src\Skins\SmokedGlass\SmokedGlass.vcxproj", "{66D1EAA4-65D1-45CC-9989-E616FC0575EB}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Win7Aero7", "Src\Skins\Win7Aero7\Win7Aero7.vcxproj", "{A2CCDE9F-17CE-461E-8BD9-00261B8855A6}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Win7Aero", "Src\Skins\Win7Aero\Win7Aero.vcxproj", "{EA65FDDD-CB77-417F-8BB4-2F3ECB5B3E75}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Win7Basic", "Src\Skins\Win7Basic\Win7Basic.vcxproj", "{404821C5-4EE4-4908-A759-5EF6DAC14AB6}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Win87", "Src\Skins\Win87\Win87.vcxproj", "{5C875214-0E3A-4CF0-BC0C-BFF6FAA4C089}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Win8", "Src\Skins\Win8\Win8.vcxproj", "{ED74EBA9-1BCB-4B8F-9AE1-DC63B3C24A94}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WinXP", "Src\Skins\WinXP\WinXP.vcxproj", "{81EB6336-366C-47DD-82CF-FF6C36CCD2B5}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "StartMenuDLL", "Src\StartMenu\StartMenuDLL\StartMenuDLL.vcxproj", "{85DEECBB-1F9B-4983-9D54-3BF42182B7E7}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "StartMenuHelper", "Src\StartMenu\StartMenuHelper\StartMenuHelper.vcxproj", "{A42C6159-ACA8-46D1-A0FB-19C398B137D5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Skins", "Skins", "{409484D8-C0DB-4991-AF03-124128EDEF98}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Release|x64 = Release|x64 + Debug|x64 = Debug|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {87D5FE20-AF86-458A-9AA3-3131EB06179B}.Release|x64.ActiveCfg = Release|x64 + {87D5FE20-AF86-458A-9AA3-3131EB06179B}.Release|x64.Build.0 = Release|x64 + {87D5FE20-AF86-458A-9AA3-3131EB06179B}.Debug|x64.ActiveCfg = Debug|x64 + {87D5FE20-AF86-458A-9AA3-3131EB06179B}.Debug|x64.Build.0 = Debug|x64 + {D42FE717-485B-492D-884A-1999F6D51154}.Release|x64.ActiveCfg = Release|x64 + {D42FE717-485B-492D-884A-1999F6D51154}.Release|x64.Build.0 = Release|x64 + {D42FE717-485B-492D-884A-1999F6D51154}.Debug|x64.ActiveCfg = Debug|x64 + {D42FE717-485B-492D-884A-1999F6D51154}.Debug|x64.Build.0 = Debug|x64 + {31C016FB-9EA1-4AF5-987A-37210C04DA06}.Release|x64.ActiveCfg = Resource|Win32 + {31C016FB-9EA1-4AF5-987A-37210C04DA06}.Release|x64.Build.0 = Resource|Win32 + {31C016FB-9EA1-4AF5-987A-37210C04DA06}.Debug|x64.ActiveCfg = Resource|Win32 + {31C016FB-9EA1-4AF5-987A-37210C04DA06}.Debug|x64.Build.0 = Resource|Win32 + {9EC23CA9-384A-4EEB-979E-69879DC1A78C}.Release|x64.ActiveCfg = Resource|Win32 + {9EC23CA9-384A-4EEB-979E-69879DC1A78C}.Release|x64.Build.0 = Resource|Win32 + {9EC23CA9-384A-4EEB-979E-69879DC1A78C}.Debug|x64.ActiveCfg = Resource|Win32 + {9EC23CA9-384A-4EEB-979E-69879DC1A78C}.Debug|x64.Build.0 = Resource|Win32 + {066C9721-26D5-4C4D-868E-50C2BA0A8196}.Release|x64.ActiveCfg = Resource|Win32 + {066C9721-26D5-4C4D-868E-50C2BA0A8196}.Release|x64.Build.0 = Resource|Win32 + {066C9721-26D5-4C4D-868E-50C2BA0A8196}.Debug|x64.ActiveCfg = Resource|Win32 + {066C9721-26D5-4C4D-868E-50C2BA0A8196}.Debug|x64.Build.0 = Resource|Win32 + {75809D15-8403-420A-BBE6-05F478D88D8E}.Release|x64.ActiveCfg = Resource|Win32 + {75809D15-8403-420A-BBE6-05F478D88D8E}.Release|x64.Build.0 = Resource|Win32 + {75809D15-8403-420A-BBE6-05F478D88D8E}.Debug|x64.ActiveCfg = Resource|Win32 + {75809D15-8403-420A-BBE6-05F478D88D8E}.Debug|x64.Build.0 = Resource|Win32 + {BD28B058-230E-42DF-9FB1-FFBB0153F498}.Release|x64.ActiveCfg = Resource|Win32 + {BD28B058-230E-42DF-9FB1-FFBB0153F498}.Release|x64.Build.0 = Resource|Win32 + {BD28B058-230E-42DF-9FB1-FFBB0153F498}.Debug|x64.ActiveCfg = Resource|Win32 + {BD28B058-230E-42DF-9FB1-FFBB0153F498}.Debug|x64.Build.0 = Resource|Win32 + {CA5BFC96-428D-42F5-9F7D-CDDE048A357C}.Release|x64.ActiveCfg = Resource|Win32 + {CA5BFC96-428D-42F5-9F7D-CDDE048A357C}.Release|x64.Build.0 = Resource|Win32 + {CA5BFC96-428D-42F5-9F7D-CDDE048A357C}.Debug|x64.ActiveCfg = Resource|Win32 + {CA5BFC96-428D-42F5-9F7D-CDDE048A357C}.Debug|x64.Build.0 = Resource|Win32 + {598AB4AC-008E-4501-90B3-C5213834C1DA}.Release|x64.ActiveCfg = Resource|Win32 + {598AB4AC-008E-4501-90B3-C5213834C1DA}.Release|x64.Build.0 = Resource|Win32 + {598AB4AC-008E-4501-90B3-C5213834C1DA}.Debug|x64.ActiveCfg = Resource|Win32 + {598AB4AC-008E-4501-90B3-C5213834C1DA}.Debug|x64.Build.0 = Resource|Win32 + {63BAF573-170B-4FA0-AEE3-16E04F3E9DF5}.Release|x64.ActiveCfg = Resource|Win32 + {63BAF573-170B-4FA0-AEE3-16E04F3E9DF5}.Release|x64.Build.0 = Resource|Win32 + {63BAF573-170B-4FA0-AEE3-16E04F3E9DF5}.Debug|x64.ActiveCfg = Resource|Win32 + {63BAF573-170B-4FA0-AEE3-16E04F3E9DF5}.Debug|x64.Build.0 = Resource|Win32 + {7BD26CB3-5280-48FD-9A86-C13E321018D5}.Release|x64.ActiveCfg = Resource|Win32 + {7BD26CB3-5280-48FD-9A86-C13E321018D5}.Release|x64.Build.0 = Resource|Win32 + {7BD26CB3-5280-48FD-9A86-C13E321018D5}.Debug|x64.ActiveCfg = Resource|Win32 + {7BD26CB3-5280-48FD-9A86-C13E321018D5}.Debug|x64.Build.0 = Resource|Win32 + {66D1EAA4-65D1-45CC-9989-E616FC0575EB}.Release|x64.ActiveCfg = Resource|Win32 + {66D1EAA4-65D1-45CC-9989-E616FC0575EB}.Release|x64.Build.0 = Resource|Win32 + {66D1EAA4-65D1-45CC-9989-E616FC0575EB}.Debug|x64.ActiveCfg = Resource|Win32 + {66D1EAA4-65D1-45CC-9989-E616FC0575EB}.Debug|x64.Build.0 = Resource|Win32 + {A2CCDE9F-17CE-461E-8BD9-00261B8855A6}.Release|x64.ActiveCfg = Resource|Win32 + {A2CCDE9F-17CE-461E-8BD9-00261B8855A6}.Release|x64.Build.0 = Resource|Win32 + {A2CCDE9F-17CE-461E-8BD9-00261B8855A6}.Debug|x64.ActiveCfg = Resource|Win32 + {A2CCDE9F-17CE-461E-8BD9-00261B8855A6}.Debug|x64.Build.0 = Resource|Win32 + {EA65FDDD-CB77-417F-8BB4-2F3ECB5B3E75}.Release|x64.ActiveCfg = Resource|Win32 + {EA65FDDD-CB77-417F-8BB4-2F3ECB5B3E75}.Release|x64.Build.0 = Resource|Win32 + {EA65FDDD-CB77-417F-8BB4-2F3ECB5B3E75}.Debug|x64.ActiveCfg = Resource|Win32 + {EA65FDDD-CB77-417F-8BB4-2F3ECB5B3E75}.Debug|x64.Build.0 = Resource|Win32 + {404821C5-4EE4-4908-A759-5EF6DAC14AB6}.Release|x64.ActiveCfg = Resource|Win32 + {404821C5-4EE4-4908-A759-5EF6DAC14AB6}.Release|x64.Build.0 = Resource|Win32 + {404821C5-4EE4-4908-A759-5EF6DAC14AB6}.Debug|x64.ActiveCfg = Resource|Win32 + {404821C5-4EE4-4908-A759-5EF6DAC14AB6}.Debug|x64.Build.0 = Resource|Win32 + {5C875214-0E3A-4CF0-BC0C-BFF6FAA4C089}.Release|x64.ActiveCfg = Resource|Win32 + {5C875214-0E3A-4CF0-BC0C-BFF6FAA4C089}.Release|x64.Build.0 = Resource|Win32 + {5C875214-0E3A-4CF0-BC0C-BFF6FAA4C089}.Debug|x64.ActiveCfg = Resource|Win32 + {5C875214-0E3A-4CF0-BC0C-BFF6FAA4C089}.Debug|x64.Build.0 = Resource|Win32 + {ED74EBA9-1BCB-4B8F-9AE1-DC63B3C24A94}.Release|x64.ActiveCfg = Resource|Win32 + {ED74EBA9-1BCB-4B8F-9AE1-DC63B3C24A94}.Release|x64.Build.0 = Resource|Win32 + {ED74EBA9-1BCB-4B8F-9AE1-DC63B3C24A94}.Debug|x64.ActiveCfg = Resource|Win32 + {ED74EBA9-1BCB-4B8F-9AE1-DC63B3C24A94}.Debug|x64.Build.0 = Resource|Win32 + {81EB6336-366C-47DD-82CF-FF6C36CCD2B5}.Release|x64.ActiveCfg = Resource|Win32 + {81EB6336-366C-47DD-82CF-FF6C36CCD2B5}.Release|x64.Build.0 = Resource|Win32 + {81EB6336-366C-47DD-82CF-FF6C36CCD2B5}.Debug|x64.ActiveCfg = Resource|Win32 + {81EB6336-366C-47DD-82CF-FF6C36CCD2B5}.Debug|x64.Build.0 = Resource|Win32 + {85DEECBB-1F9B-4983-9D54-3BF42182B7E7}.Release|x64.ActiveCfg = Release|x64 + {85DEECBB-1F9B-4983-9D54-3BF42182B7E7}.Release|x64.Build.0 = Release|x64 + {85DEECBB-1F9B-4983-9D54-3BF42182B7E7}.Debug|x64.ActiveCfg = Debug|x64 + {85DEECBB-1F9B-4983-9D54-3BF42182B7E7}.Debug|x64.Build.0 = Debug|x64 + {A42C6159-ACA8-46D1-A0FB-19C398B137D5}.Release|x64.ActiveCfg = Release|x64 + {A42C6159-ACA8-46D1-A0FB-19C398B137D5}.Release|x64.Build.0 = Release|x64 + {A42C6159-ACA8-46D1-A0FB-19C398B137D5}.Debug|x64.ActiveCfg = Debug|x64 + {A42C6159-ACA8-46D1-A0FB-19C398B137D5}.Debug|x64.Build.0 = Debug|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {31C016FB-9EA1-4AF5-987A-37210C04DA06} = {409484D8-C0DB-4991-AF03-124128EDEF98} + {9EC23CA9-384A-4EEB-979E-69879DC1A78C} = {409484D8-C0DB-4991-AF03-124128EDEF98} + {066C9721-26D5-4C4D-868E-50C2BA0A8196} = {409484D8-C0DB-4991-AF03-124128EDEF98} + {75809D15-8403-420A-BBE6-05F478D88D8E} = {409484D8-C0DB-4991-AF03-124128EDEF98} + {BD28B058-230E-42DF-9FB1-FFBB0153F498} = {409484D8-C0DB-4991-AF03-124128EDEF98} + {CA5BFC96-428D-42F5-9F7D-CDDE048A357C} = {409484D8-C0DB-4991-AF03-124128EDEF98} + {598AB4AC-008E-4501-90B3-C5213834C1DA} = {409484D8-C0DB-4991-AF03-124128EDEF98} + {63BAF573-170B-4FA0-AEE3-16E04F3E9DF5} = {409484D8-C0DB-4991-AF03-124128EDEF98} + {7BD26CB3-5280-48FD-9A86-C13E321018D5} = {409484D8-C0DB-4991-AF03-124128EDEF98} + {66D1EAA4-65D1-45CC-9989-E616FC0575EB} = {409484D8-C0DB-4991-AF03-124128EDEF98} + {A2CCDE9F-17CE-461E-8BD9-00261B8855A6} = {409484D8-C0DB-4991-AF03-124128EDEF98} + {EA65FDDD-CB77-417F-8BB4-2F3ECB5B3E75} = {409484D8-C0DB-4991-AF03-124128EDEF98} + {404821C5-4EE4-4908-A759-5EF6DAC14AB6} = {409484D8-C0DB-4991-AF03-124128EDEF98} + {5C875214-0E3A-4CF0-BC0C-BFF6FAA4C089} = {409484D8-C0DB-4991-AF03-124128EDEF98} + {ED74EBA9-1BCB-4B8F-9AE1-DC63B3C24A94} = {409484D8-C0DB-4991-AF03-124128EDEF98} + {81EB6336-366C-47DD-82CF-FF6C36CCD2B5} = {409484D8-C0DB-4991-AF03-124128EDEF98} + EndGlobalSection +EndGlobal diff --git a/Src/ClassicExplorer/ClassicExplorer.vcxproj b/Src/ClassicExplorer/ClassicExplorer.vcxproj index 20faa958e..a15ec5964 100644 --- a/Src/ClassicExplorer/ClassicExplorer.vcxproj +++ b/Src/ClassicExplorer/ClassicExplorer.vcxproj @@ -59,7 +59,7 @@ DynamicLibrary - v143 + $(OpenShellPlatformToolset) Static Unicode true diff --git a/Src/ClassicExplorer/ClassicExplorerSettings/ClassicExplorerSettings.vcxproj b/Src/ClassicExplorer/ClassicExplorerSettings/ClassicExplorerSettings.vcxproj index 1ebf5ecb1..33fefb9aa 100644 --- a/Src/ClassicExplorer/ClassicExplorerSettings/ClassicExplorerSettings.vcxproj +++ b/Src/ClassicExplorer/ClassicExplorerSettings/ClassicExplorerSettings.vcxproj @@ -23,7 +23,7 @@ Application - v143 + $(OpenShellPlatformToolset) Static Unicode true diff --git a/Src/ClassicIE/ClassicIE.vcxproj b/Src/ClassicIE/ClassicIE.vcxproj index c883b879a..7cefe675b 100644 --- a/Src/ClassicIE/ClassicIE.vcxproj +++ b/Src/ClassicIE/ClassicIE.vcxproj @@ -47,7 +47,7 @@ Application - v143 + $(OpenShellPlatformToolset) Static Unicode true diff --git a/Src/ClassicIE/ClassicIEDLL/ClassicIEDLL.vcxproj b/Src/ClassicIE/ClassicIEDLL/ClassicIEDLL.vcxproj index 4944070e7..49a040191 100644 --- a/Src/ClassicIE/ClassicIEDLL/ClassicIEDLL.vcxproj +++ b/Src/ClassicIE/ClassicIEDLL/ClassicIEDLL.vcxproj @@ -59,7 +59,7 @@ DynamicLibrary - v143 + $(OpenShellPlatformToolset) Static Unicode true diff --git a/Src/Lib/Lib.vcxproj b/Src/Lib/Lib.vcxproj index 03954dade..4234c33e9 100644 --- a/Src/Lib/Lib.vcxproj +++ b/Src/Lib/Lib.vcxproj @@ -43,7 +43,7 @@ StaticLibrary - v143 + $(OpenShellPlatformToolset) Static Unicode true diff --git a/Src/Setup/Setup.vcxproj b/Src/Setup/Setup.vcxproj index f937d143f..4cd9199a5 100644 --- a/Src/Setup/Setup.vcxproj +++ b/Src/Setup/Setup.vcxproj @@ -19,7 +19,7 @@ Application - v143 + $(OpenShellPlatformToolset) Unicode true diff --git a/Src/Setup/SetupHelper/SetupHelper.vcxproj b/Src/Setup/SetupHelper/SetupHelper.vcxproj index 21afa7d9a..d99aa0633 100644 --- a/Src/Setup/SetupHelper/SetupHelper.vcxproj +++ b/Src/Setup/SetupHelper/SetupHelper.vcxproj @@ -19,7 +19,7 @@ Application - v143 + $(OpenShellPlatformToolset) Unicode true diff --git a/Src/Setup/Utility/Utility.vcxproj b/Src/Setup/Utility/Utility.vcxproj index ba3c5ad49..5d8cf7d0a 100644 --- a/Src/Setup/Utility/Utility.vcxproj +++ b/Src/Setup/Utility/Utility.vcxproj @@ -35,7 +35,7 @@ Application - v143 + $(OpenShellPlatformToolset) Static Unicode true diff --git a/Src/Setup/en-US/en-US.vcxproj b/Src/Setup/en-US/en-US.vcxproj index 235ec9dc3..6ddcb2892 100644 --- a/Src/Setup/en-US/en-US.vcxproj +++ b/Src/Setup/en-US/en-US.vcxproj @@ -15,7 +15,7 @@ DynamicLibrary - v143 + $(OpenShellPlatformToolset) Unicode diff --git a/Src/Skins/ClassicSkin/ClassicSkin.vcxproj b/Src/Skins/ClassicSkin/ClassicSkin.vcxproj index 695405b96..042431333 100644 --- a/Src/Skins/ClassicSkin/ClassicSkin.vcxproj +++ b/Src/Skins/ClassicSkin/ClassicSkin.vcxproj @@ -15,7 +15,7 @@ DynamicLibrary - v143 + $(OpenShellPlatformToolset) Unicode diff --git a/Src/Skins/ClassicSkin7/ClassicSkin7.vcxproj b/Src/Skins/ClassicSkin7/ClassicSkin7.vcxproj index 5877c621c..49b1a29c8 100644 --- a/Src/Skins/ClassicSkin7/ClassicSkin7.vcxproj +++ b/Src/Skins/ClassicSkin7/ClassicSkin7.vcxproj @@ -15,7 +15,7 @@ DynamicLibrary - v143 + $(OpenShellPlatformToolset) Unicode diff --git a/Src/Skins/FullGlass/FullGlass.vcxproj b/Src/Skins/FullGlass/FullGlass.vcxproj index 1339fb114..87c0ed6cb 100644 --- a/Src/Skins/FullGlass/FullGlass.vcxproj +++ b/Src/Skins/FullGlass/FullGlass.vcxproj @@ -15,7 +15,7 @@ DynamicLibrary - v143 + $(OpenShellPlatformToolset) Unicode diff --git a/Src/Skins/Immersive/Immersive.vcxproj b/Src/Skins/Immersive/Immersive.vcxproj index d3d17b969..5fabc1159 100644 --- a/Src/Skins/Immersive/Immersive.vcxproj +++ b/Src/Skins/Immersive/Immersive.vcxproj @@ -15,7 +15,7 @@ DynamicLibrary - v143 + $(OpenShellPlatformToolset) Unicode diff --git a/Src/Skins/Immersive7/Immersive7.vcxproj b/Src/Skins/Immersive7/Immersive7.vcxproj index 17ac64983..2969d247b 100644 --- a/Src/Skins/Immersive7/Immersive7.vcxproj +++ b/Src/Skins/Immersive7/Immersive7.vcxproj @@ -15,7 +15,7 @@ DynamicLibrary - v143 + $(OpenShellPlatformToolset) Unicode diff --git a/Src/Skins/Metallic7/Metallic7.vcxproj b/Src/Skins/Metallic7/Metallic7.vcxproj index b58e9d15b..b9ce88606 100644 --- a/Src/Skins/Metallic7/Metallic7.vcxproj +++ b/Src/Skins/Metallic7/Metallic7.vcxproj @@ -15,7 +15,7 @@ DynamicLibrary - v143 + $(OpenShellPlatformToolset) Unicode diff --git a/Src/Skins/Metro/Metro.vcxproj b/Src/Skins/Metro/Metro.vcxproj index 7028f5689..577c798b0 100644 --- a/Src/Skins/Metro/Metro.vcxproj +++ b/Src/Skins/Metro/Metro.vcxproj @@ -15,7 +15,7 @@ DynamicLibrary - v143 + $(OpenShellPlatformToolset) Unicode diff --git a/Src/Skins/Metro7/Metro7.vcxproj b/Src/Skins/Metro7/Metro7.vcxproj index 42575ab62..87059970d 100644 --- a/Src/Skins/Metro7/Metro7.vcxproj +++ b/Src/Skins/Metro7/Metro7.vcxproj @@ -15,7 +15,7 @@ DynamicLibrary - v143 + $(OpenShellPlatformToolset) Unicode diff --git a/Src/Skins/Midnight7/Midnight7.vcxproj b/Src/Skins/Midnight7/Midnight7.vcxproj index 481e62396..3adf42ffc 100644 --- a/Src/Skins/Midnight7/Midnight7.vcxproj +++ b/Src/Skins/Midnight7/Midnight7.vcxproj @@ -15,7 +15,7 @@ DynamicLibrary - v143 + $(OpenShellPlatformToolset) Unicode diff --git a/Src/Skins/SmokedGlass/SmokedGlass.vcxproj b/Src/Skins/SmokedGlass/SmokedGlass.vcxproj index c00b9b276..e8f3c6f53 100644 --- a/Src/Skins/SmokedGlass/SmokedGlass.vcxproj +++ b/Src/Skins/SmokedGlass/SmokedGlass.vcxproj @@ -15,7 +15,7 @@ DynamicLibrary - v143 + $(OpenShellPlatformToolset) Unicode diff --git a/Src/Skins/Win7Aero/Win7Aero.vcxproj b/Src/Skins/Win7Aero/Win7Aero.vcxproj index 78038a833..7a820913e 100644 --- a/Src/Skins/Win7Aero/Win7Aero.vcxproj +++ b/Src/Skins/Win7Aero/Win7Aero.vcxproj @@ -15,7 +15,7 @@ DynamicLibrary - v143 + $(OpenShellPlatformToolset) Unicode diff --git a/Src/Skins/Win7Aero7/Win7Aero7.vcxproj b/Src/Skins/Win7Aero7/Win7Aero7.vcxproj index 8aa65f752..525fc5cfc 100644 --- a/Src/Skins/Win7Aero7/Win7Aero7.vcxproj +++ b/Src/Skins/Win7Aero7/Win7Aero7.vcxproj @@ -15,7 +15,7 @@ DynamicLibrary - v143 + $(OpenShellPlatformToolset) Unicode diff --git a/Src/Skins/Win7Basic/Win7Basic.vcxproj b/Src/Skins/Win7Basic/Win7Basic.vcxproj index d80072d71..b9b5d28cd 100644 --- a/Src/Skins/Win7Basic/Win7Basic.vcxproj +++ b/Src/Skins/Win7Basic/Win7Basic.vcxproj @@ -15,7 +15,7 @@ DynamicLibrary - v143 + $(OpenShellPlatformToolset) Unicode diff --git a/Src/Skins/Win8/Win8.vcxproj b/Src/Skins/Win8/Win8.vcxproj index c3fbbe0a8..b0491eaea 100644 --- a/Src/Skins/Win8/Win8.vcxproj +++ b/Src/Skins/Win8/Win8.vcxproj @@ -15,7 +15,7 @@ DynamicLibrary - v143 + $(OpenShellPlatformToolset) Unicode diff --git a/Src/Skins/Win87/Win87.vcxproj b/Src/Skins/Win87/Win87.vcxproj index c2d326ab8..4feab0200 100644 --- a/Src/Skins/Win87/Win87.vcxproj +++ b/Src/Skins/Win87/Win87.vcxproj @@ -15,7 +15,7 @@ DynamicLibrary - v143 + $(OpenShellPlatformToolset) Unicode diff --git a/Src/Skins/WinXP/WinXP.vcxproj b/Src/Skins/WinXP/WinXP.vcxproj index 4ffed1547..9535bf33d 100644 --- a/Src/Skins/WinXP/WinXP.vcxproj +++ b/Src/Skins/WinXP/WinXP.vcxproj @@ -15,7 +15,7 @@ DynamicLibrary - v143 + $(OpenShellPlatformToolset) Unicode diff --git a/Src/StartMenu/StartMenu.vcxproj b/Src/StartMenu/StartMenu.vcxproj index bfc0f016f..676089621 100644 --- a/Src/StartMenu/StartMenu.vcxproj +++ b/Src/StartMenu/StartMenu.vcxproj @@ -47,7 +47,7 @@ Application - v143 + $(OpenShellPlatformToolset) Static Unicode true diff --git a/Src/StartMenu/StartMenuDLL/StartMenuDLL.vcxproj b/Src/StartMenu/StartMenuDLL/StartMenuDLL.vcxproj index 0412e588c..7d828042f 100644 --- a/Src/StartMenu/StartMenuDLL/StartMenuDLL.vcxproj +++ b/Src/StartMenu/StartMenuDLL/StartMenuDLL.vcxproj @@ -47,7 +47,7 @@ DynamicLibrary - v143 + $(OpenShellPlatformToolset) Static Unicode true diff --git a/Src/StartMenu/StartMenuHelper/StartMenuHelper.vcxproj b/Src/StartMenu/StartMenuHelper/StartMenuHelper.vcxproj index 3e7790656..af7994c0b 100644 --- a/Src/StartMenu/StartMenuHelper/StartMenuHelper.vcxproj +++ b/Src/StartMenu/StartMenuHelper/StartMenuHelper.vcxproj @@ -59,7 +59,7 @@ DynamicLibrary - v143 + $(OpenShellPlatformToolset) Static Unicode true diff --git a/Src/Update/DesktopToasts/DesktopToasts.vcxproj b/Src/Update/DesktopToasts/DesktopToasts.vcxproj index 5561e5bfc..1968484ee 100644 --- a/Src/Update/DesktopToasts/DesktopToasts.vcxproj +++ b/Src/Update/DesktopToasts/DesktopToasts.vcxproj @@ -20,7 +20,7 @@ DynamicLibrary - v143 + $(OpenShellPlatformToolset) Unicode true diff --git a/Src/Update/Update.vcxproj b/Src/Update/Update.vcxproj index 52f961ac1..c253acb16 100644 --- a/Src/Update/Update.vcxproj +++ b/Src/Update/Update.vcxproj @@ -19,7 +19,7 @@ Application - v143 + $(OpenShellPlatformToolset) Static Unicode true From a68cd87b30be3b18c68a7b6c0ac0661a4838c5b8 Mon Sep 17 00:00:00 2001 From: projektion0 <98270100+projektion0@users.noreply.github.com> Date: Sat, 17 Oct 2026 23:59:28 +0200 Subject: [PATCH 2/5] StartMenu: allow a separate left Start button on Windows 11 --- Src/StartMenu/StartMenuDLL/SettingsUI.cpp | 8 ++- Src/StartMenu/StartMenuDLL/StartMenuDLL.cpp | 66 +++++++++++++++++---- Src/StartMenu/StartMenuDLL/StartMenuDLL.h | 3 +- Src/StartMenu/StartMenuDLL/StartMenuDLL.rc | 16 +++++ Src/StartMenu/StartMenuDLL/resource.h | 2 + 5 files changed, 80 insertions(+), 15 deletions(-) diff --git a/Src/StartMenu/StartMenuDLL/SettingsUI.cpp b/Src/StartMenu/StartMenuDLL/SettingsUI.cpp index af9376103..c86bd0d5d 100644 --- a/Src/StartMenu/StartMenuDLL/SettingsUI.cpp +++ b/Src/StartMenu/StartMenuDLL/SettingsUI.cpp @@ -4458,6 +4458,7 @@ CSetting g_Settings[]={ {L"StartButton",CSetting::TYPE_GROUP,IDS_START_BUTTON}, {L"EnableStartButton",CSetting::TYPE_BOOL,IDS_ENABLE_BUTTON,IDS_ENABLE_BUTTON_TIP,1}, + {L"StartButtonLeft",CSetting::TYPE_BOOL,IDS_BUTTON_LEFT,IDS_BUTTON_LEFT_TIP,0,0,L"EnableStartButton"}, {L"AllTaskbars",CSetting::TYPE_BOOL,IDS_ENABLE_TASKBARS,IDS_ENABLE_TASKBARS_TIP,1,0,L"EnableStartButton"}, {L"StartButtonTip",CSetting::TYPE_STRING,IDS_BUTTON_TIP,IDS_BUTTON_TIP_TIP,L"$Menu.Start",0,L"EnableStartButton"}, {L"StartButtonType",CSetting::TYPE_INT,IDS_BUTTON_TYPE,IDS_BUTTON_TYPE_TIP,0,0,L"EnableStartButton"}, @@ -4734,6 +4735,8 @@ void UpdateSettings( void ) UpdateSetting(L"Language",language,false); } + HideSetting(L"StartButtonLeft",!IsWin11()); + TMenuStyle menuStyle=MENU_CLASSIC1; { const CSetting *pSetting=FindSetting(L"MenuStyle"); @@ -5169,11 +5172,12 @@ void InitSettings( void ) } static int g_ButtonPath, g_ButtonSize, g_ButtonIcon, g_ButtonOffset, g_ButtonIconSize, g_ButtonText, g_ButtonTip, g_ButtonAllTaskbars; -static bool g_bButtonEnable, g_bTaskbarsChanged, g_bTaskbarsUpdated; +static bool g_bButtonEnable, g_bButtonLeft, g_bTaskbarsChanged, g_bTaskbarsUpdated; static void StoreButtonSettings( void ) { g_bButtonEnable=GetSettingBool(L"EnableStartButton"); + g_bButtonLeft=GetSettingBool(L"StartButtonLeft"); g_ButtonAllTaskbars=GetSettingBool(L"AllTaskbars"); g_ButtonTip=CalcFNVHash(GetSettingString(L"StartButtonTip")); g_ButtonPath=GetSettingInt(L"StartButtonType"); @@ -5205,7 +5209,7 @@ static void UpdateButtons( bool bForce ) } if (path==START_BUTTON_CUSTOM) path=CalcFNVHash(GetSettingString(L"StartButtonPath")); - bool bRecreate=(g_bButtonEnable!=GetSettingBool(L"EnableStartButton") || path!=g_ButtonPath || g_ButtonSize!=GetSettingInt(L"StartButtonSize") || g_ButtonOffset!=GetSettingInt(L"StartButtonOffset") + bool bRecreate=(g_bButtonLeft!=GetSettingBool(L"StartButtonLeft") || g_bButtonEnable!=GetSettingBool(L"EnableStartButton") || path!=g_ButtonPath || g_ButtonSize!=GetSettingInt(L"StartButtonSize") || g_ButtonOffset!=GetSettingInt(L"StartButtonOffset") || g_ButtonIcon!=icon || g_ButtonIconSize!=GetSettingInt(L"StartButtonIconSize") || g_ButtonText!=text || g_ButtonTip!=tip || g_ButtonAllTaskbars!=allTaskbars); if ((g_bTaskbarsChanged && !g_bTaskbarsUpdated) || bForce || bRecreate) { diff --git a/Src/StartMenu/StartMenuDLL/StartMenuDLL.cpp b/Src/StartMenu/StartMenuDLL/StartMenuDLL.cpp index 583c5b765..c08ec4d20 100644 --- a/Src/StartMenu/StartMenuDLL/StartMenuDLL.cpp +++ b/Src/StartMenu/StartMenuDLL/StartMenuDLL.cpp @@ -387,10 +387,17 @@ TaskbarInfo *GetTaskbarInfo( size_t taskbarId ) return (it==g_TaskbarInfos.end())?NULL:&it->second; } +// Keep the native Windows 11 button independent of our optional left button. +// ExplorerPatcher uses the legacy taskbar layout and keeps its existing behavior. +static bool IsStartButtonLeft( void ) +{ + return IsWin11() && !g_epTaskbar && GetSettingBool(L"StartButtonLeft"); +} + static TaskbarInfo *FindTaskBarInfoButton( HWND button ) { for (id_taskbar_map::iterator it=g_TaskbarInfos.begin();it!=g_TaskbarInfos.end();++it) - if (it->second.startButton==button || it->second.oldButton==button) + if (it->second.startButton==button || (!IsStartButtonLeft() && it->second.oldButton==button)) return &it->second; return NULL; } @@ -686,6 +693,16 @@ bool PointAroundStartButton( size_t taskbarId, const CPoint &pt ) if (!PtInRect(&rc,pt)) return false; + // Never include the native button (or the gap leading to it) in our hit area. + if (IsStartButtonLeft()) + { + if (!taskBar->bReplaceButton || !taskBar->startButton) + return false; + CRect rcButton; + GetWindowRect(taskBar->startButton,&rcButton); + return PtInRect(&rcButton,pt)!=FALSE; + } + bool rtl=GetWindowLongPtr(taskBar->taskBar,GWL_EXSTYLE)&WS_EX_LAYOUTRTL; CRect rcStart; @@ -851,7 +868,7 @@ class CMonitorModeEvents: public IAppVisibilityEvents if (g_CurrentWSMTaskbar!=-1 && g_CurrentWSMTaskbar!=g_CurrentCSMTaskbar) PressStartButton(g_CurrentWSMTaskbar,false); g_CurrentWSMTaskbar=taskbarId; - if (g_CurrentWSMTaskbar!=-1) + if (g_CurrentWSMTaskbar!=-1 && !IsStartButtonLeft()) PressStartButton(g_CurrentWSMTaskbar,true); } } @@ -1315,6 +1332,7 @@ static void UpdateStartButtonPosition(const TaskbarInfo* taskBar, const WINDOWPO } } + const bool bLeft=IsStartButtonLeft(); RECT rcOldButton; if (taskBar->oldButton) GetWindowRect(taskBar->oldButton, &rcOldButton); @@ -1328,11 +1346,13 @@ static void UpdateStartButtonPosition(const TaskbarInfo* taskBar, const WINDOWPO x = rcTask.left; else x = rcTask.right - taskBar->startButtonSize.cx; - y = taskBar->oldButton ? rcOldButton.top : rcTask.top; + y = (taskBar->oldButton && !bLeft) ? rcOldButton.top : rcTask.top; } else { - if (GetWindowLongPtr(taskBar->rebar, GWL_EXSTYLE) & WS_EX_LAYOUTRTL) + if (bLeft) + x = rcTask.left; + else if (GetWindowLongPtr(taskBar->rebar, GWL_EXSTYLE) & WS_EX_LAYOUTRTL) x = (taskBar->oldButton ? rcOldButton.right : rcTask.right) - taskBar->startButtonSize.cx; else x = taskBar->oldButton ? rcOldButton.left : rcTask.left; @@ -1345,7 +1365,7 @@ static void UpdateStartButtonPosition(const TaskbarInfo* taskBar, const WINDOWPO // Start button on Win11 is a bit shifted to the right // We will shift our Aero button to cover original button - if (IsWin11() && (x == info.rcMonitor.left) && (GetStartButtonType() == START_BUTTON_AERO) && !g_epTaskbar) + if (!bLeft && IsWin11() && (x == info.rcMonitor.left) && (GetStartButtonType() == START_BUTTON_AERO) && !g_epTaskbar) x += ScaleForDpi(taskBar->taskBar, 6); if (GetStartButtonType() == START_BUTTON_CUSTOM) @@ -1402,10 +1422,10 @@ static LRESULT CALLBACK SubclassWin81StartButton( HWND hWnd, UINT uMsg, WPARAM w PostMessage(taskBar->taskBar,WM_SIZE,SIZE_RESTORED,MAKELONG(rcTask.right-rcTask.left,rcTask.bottom-rcTask.top)); } } - if (uMsg==WM_POINTERACTIVATE && CMenuContainer::IsMenuOpened()) + if (!IsStartButtonLeft() && uMsg==WM_POINTERACTIVATE && CMenuContainer::IsMenuOpened()) return MA_NOACTIVATE; #ifdef START_TOUCH - if (uMsg==WM_POINTERUP || uMsg==WM_POINTERUPDATE || uMsg==WM_POINTERUP) + if (!IsStartButtonLeft() && (uMsg==WM_POINTERUP || uMsg==WM_POINTERUPDATE || uMsg==WM_POINTERUP)) { POINTER_INPUT_TYPE type; GetPointerType2(GET_POINTERID_WPARAM(wParam),&type); @@ -1697,7 +1717,7 @@ static LRESULT CALLBACK SubclassTrayButtonProc( HWND hWnd, UINT uMsg, WPARAM wPa if (uMsg==WM_WINDOWPOSCHANGING) { const TaskbarInfo *taskBar=GetTaskbarInfo((int)dwRefData); - if (taskBar && (taskBar->bReplaceButton || taskBar->bHideButton)) + if (taskBar && !IsStartButtonLeft() && (taskBar->bReplaceButton || taskBar->bHideButton)) { WINDOWPOS *pPos=(WINDOWPOS*)lParam; if (!(pPos->flags&SWP_NOMOVE) || !(pPos->flags&SWP_NOSIZE)) @@ -2126,7 +2146,7 @@ static LRESULT CALLBACK SubclassRebarProc( HWND hWnd, UINT uMsg, WPARAM wParam, if (uMsg==WM_WINDOWPOSCHANGING) { const TaskbarInfo *taskBar=GetTaskbarInfo((int)dwRefData); - if (taskBar && (taskBar->bReplaceButton || taskBar->bHideButton)) + if (taskBar && !IsStartButtonLeft() && (taskBar->bReplaceButton || taskBar->bHideButton)) { WINDOWPOS *pPos=(WINDOWPOS*)lParam; if (!(pPos->flags&SWP_NOMOVE) || !(pPos->flags&SWP_NOSIZE)) @@ -2496,7 +2516,28 @@ void UpdateTaskBars( TUpdateTaskbar update ) { TaskbarInfo &taskBar=it->second; bool bButton2=bButton && (bAll || taskBar.taskBar==g_TaskBar); - bool bHideButton2=bButton; + bool bHideButton2=bButton && !IsStartButtonLeft(); + if (taskBar.bSeparateButton!=IsStartButtonLeft()) + { + // Restore the native button's drag target when the two buttons are separate. + // The custom button always keeps its own target in RecreateStartButton. + taskBar.bSeparateButton=IsStartButtonLeft(); + if (taskBar.oldButton) + { + RevokeDragDrop(taskBar.oldButton); + if (taskBar.bSeparateButton) + { + if (taskBar.pOriginalTarget) + RegisterDragDrop(taskBar.oldButton,taskBar.pOriginalTarget); + } + else + { + CStartMenuTarget *pNewTarget=new CStartMenuTarget(taskBar.taskbarId); + RegisterDragDrop(taskBar.oldButton,pNewTarget); + pNewTarget->Release(); + } + } + } if (taskBar.oldButton) { if (bHideButton2 && !bButton2) @@ -3629,14 +3670,15 @@ static LRESULT CALLBACK HookDesktopThreadMouse(int code, WPARAM wParam, LPARAM l if (!taskBar) { taskBar = FindTaskBarInfoBar(GetAncestor(info->hwnd, GA_ROOT)); // click on taskbar - if (taskBar && !PointAroundStartButton(taskBar->taskbarId)) + if (taskBar && !PointAroundStartButton(taskBar->taskbarId,info->pt)) taskBar = NULL; } if (taskBar && (info->hwnd != taskBar->startButton) && taskBar->oldButton) { // steal messages from other than our custom button window - PostMessage(taskBar->oldButton, (UINT)wParam, 0, MAKELPARAM(info->pt.x, info->pt.y)); + HWND target=IsStartButtonLeft()?taskBar->startButton:taskBar->oldButton; + PostMessage(target, (UINT)wParam, 0, MAKELPARAM(info->pt.x, info->pt.y)); return 1; } } diff --git a/Src/StartMenu/StartMenuDLL/StartMenuDLL.h b/Src/StartMenu/StartMenuDLL/StartMenuDLL.h index 456a88475..81b8ed39f 100644 --- a/Src/StartMenu/StartMenuDLL/StartMenuDLL.h +++ b/Src/StartMenu/StartMenuDLL/StartMenuDLL.h @@ -47,7 +47,7 @@ void EnableStartTooltip( bool bEnable ); struct TaskbarInfo { - TaskbarInfo( void ) { taskbarId=pointerId=0; taskBar=startButton=oldButton=rebar=taskList=chevron=news=desktop=NULL; startButtonSize.cx=startButtonSize.cy=0; oldButtonSize.cx=oldButtonSize.cy=0; bTimer=bCustomLook=bReplaceButton=bHideButton=bRecreatingButton=bThemeChanging=false; } + TaskbarInfo( void ) { taskbarId=pointerId=0; taskBar=startButton=oldButton=rebar=taskList=chevron=news=desktop=NULL; startButtonSize.cx=startButtonSize.cy=0; oldButtonSize.cx=oldButtonSize.cy=0; bTimer=bCustomLook=bReplaceButton=bHideButton=bRecreatingButton=bThemeChanging=bSeparateButton=false; } int taskbarId; HWND taskBar; HWND startButton; // either own start button or the win7 start button (depending on bReplaceButton) @@ -66,6 +66,7 @@ struct TaskbarInfo bool bHideButton; // hide old start button (if we have own button) bool bRecreatingButton; bool bThemeChanging; + bool bSeparateButton; // native button drag target restored for the separate left button std::vector trayButtons; // ordered by Z order (for win10) std::vector taskbarParts; CComPtr pOriginalTarget; diff --git a/Src/StartMenu/StartMenuDLL/StartMenuDLL.rc b/Src/StartMenu/StartMenuDLL/StartMenuDLL.rc index d65ffce32..60e87266b 100644 --- a/Src/StartMenu/StartMenuDLL/StartMenuDLL.rc +++ b/Src/StartMenu/StartMenuDLL/StartMenuDLL.rc @@ -1339,7 +1339,23 @@ BEGIN IDS_PINNED_PATH_TIP "The path to use as the Pinned folder. If the path does not exist, it will be created (if possible) after opening the start menu. Close this window after updating this setting to update your context menu.\n\nNote: If you do not have permissions for the selected path, you will not be able to pin items until you take ownership of the new folder" END +STRINGTABLE +BEGIN + IDS_BUTTON_LEFT "Keep Open-Shell start button at left edge (Windows 11)" + IDS_BUTTON_LEFT_TIP "Places the Open-Shell start button at the left edge of each enabled taskbar and keeps the Windows start button available at its normal position. Requires the Windows 11 taskbar. This option does not apply to ExplorerPatcher's legacy taskbar." +END + #endif // English (U.S.) resources + +// These new IDs have their own string-table block, so existing localized +// resources continue to fall back to the external language DLL as before. +LANGUAGE LANG_GERMAN, SUBLANG_GERMAN +STRINGTABLE +BEGIN + IDS_BUTTON_LEFT "Open-Shell-Startknopf links anordnen (Windows 11)" + IDS_BUTTON_LEFT_TIP "Ordnet den Open-Shell-Startknopf am linken Rand jeder aktivierten Taskleiste an. Der Windows-Startknopf bleibt an seiner normalen Position erreichbar. Erfordert die Windows-11-Taskleiste. Gilt nicht fuer die klassische Taskleiste von ExplorerPatcher." +END +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US ///////////////////////////////////////////////////////////////////////////// diff --git a/Src/StartMenu/StartMenuDLL/resource.h b/Src/StartMenu/StartMenuDLL/resource.h index 81acb7478..5ab23ac3d 100644 --- a/Src/StartMenu/StartMenuDLL/resource.h +++ b/Src/StartMenu/StartMenuDLL/resource.h @@ -788,6 +788,8 @@ #define IDS_ALT_ACCELERATORS_TIP 3689 #define IDS_BUTTON_OFFSET 3690 #define IDS_BUTTON_OFFSET_TIP 3691 +#define IDS_BUTTON_LEFT 7120 +#define IDS_BUTTON_LEFT_TIP 7121 #define IDS_STRING7001 7001 #define IDS_STRING7002 7002 #define IDS_STRING7003 7003 From 824b0631bb73f47b5c4d08ba1922aa4fca72f994 Mon Sep 17 00:00:00 2001 From: projektion0 <98270100+projektion0@users.noreply.github.com> Date: Sat, 17 Oct 2026 23:59:55 +0200 Subject: [PATCH 3/5] StartMenu: keep native async drops responsive and refresh open menus Preserve native Shell async transfer negotiation, retain menus without stealing focus from transfer dialogs, and observe folder changes. Restore outside-click dismissal and keep the Shell drag preview visible above raised submenus. --- Src/StartMenu/StartMenuDLL/DragDrop.cpp | 155 +++++++++-- Src/StartMenu/StartMenuDLL/MenuContainer.cpp | 240 ++++++++++++++++++ Src/StartMenu/StartMenuDLL/MenuContainer.h | 24 ++ Src/StartMenu/StartMenuDLL/MenuFolderWatch.h | 38 +++ Src/StartMenu/StartMenuDLL/MenuPaint.cpp | 5 + Src/StartMenu/StartMenuDLL/ProgramsTree.cpp | 18 +- Src/StartMenu/StartMenuDLL/StartMenuDLL.cpp | 27 ++ Src/StartMenu/StartMenuDLL/StartMenuDLL.h | 3 + .../StartMenuDLL/StartMenuDLL.vcxproj | 1 + .../StartMenuDLL/StartMenuDLL.vcxproj.filters | 3 + 10 files changed, 482 insertions(+), 32 deletions(-) create mode 100644 Src/StartMenu/StartMenuDLL/MenuFolderWatch.h diff --git a/Src/StartMenu/StartMenuDLL/DragDrop.cpp b/Src/StartMenu/StartMenuDLL/DragDrop.cpp index 7b3262db8..3250fb9eb 100644 --- a/Src/StartMenu/StartMenuDLL/DragDrop.cpp +++ b/Src/StartMenu/StartMenuDLL/DragDrop.cpp @@ -16,6 +16,86 @@ #include "FileHelper.h" #include +// Let the native shell target negotiate its own asynchronous transfer. A +// synchronous file-conflict dialog can deadlock inside the OLE drag loop while +// its UI thread releases a COM reference back to this STA. +static HRESULT EnableShellAsyncDrop( IDataObject *pDataObj ) +{ + CComQIPtr pAsync(pDataObj); + return pAsync?pAsync->SetAsyncMode(VARIANT_TRUE):E_NOINTERFACE; +} + +static bool WasShellDropAsync( IDataObject *pDataObj ) +{ + CComQIPtr pAsync(pDataObj); + BOOL asynchronous=FALSE; + // InOperation tells the source that StartOperation was used; it is not + // a completion event and must never be polled to wait for completion. + return pAsync && SUCCEEDED(pAsync->InOperation(&asynchronous)) && asynchronous; +} + +// Our icon cache contains premultiplied 32-bit DIBs. The Shell drag-image +// helper expects straight alpha and takes ownership of its bitmap on success. +static HRESULT InitializeMenuDragImage( IDragSourceHelper2 *helper, IDataObject *data, HBITMAP icon ) +{ + if (!helper || !data || !icon) return E_INVALIDARG; + HBITMAP copy=(HBITMAP)CopyImage(icon,IMAGE_BITMAP,0,0,LR_CREATEDIBSECTION); + if (!copy) return E_OUTOFMEMORY; + BITMAP bitmap={}; + if (!GetObject(copy,sizeof(bitmap),&bitmap) || !bitmap.bmBits || bitmap.bmBitsPixel!=32) + { + DeleteObject(copy); + return E_FAIL; + } + for (int y=0;y>24; + if (alpha && alpha<255) + { + DWORD red=(std::min)(255UL,(((pixel>>16)&255)*255+alpha/2)/alpha); + DWORD green=(std::min)(255UL,(((pixel>>8)&255)*255+alpha/2)/alpha); + DWORD blue=(std::min)(255UL,((pixel&255)*255+alpha/2)/alpha); + pixels[x]=(alpha<<24)|(red<<16)|(green<<8)|blue; + } + else if (!alpha) + pixels[x]=0; + } + } + SHDRAGIMAGE image={{bitmap.bmWidth,bitmap.bmHeight},{bitmap.bmWidth/2,bitmap.bmHeight},copy,CLR_NONE}; + helper->SetFlags(DSH_ALLOWDROPDESCRIPTIONTEXT); + HRESULT result=helper->InitializeFromBitmap(&image,data); + if (FAILED(result)) DeleteObject(copy); + return result; +} + +// A newly raised topmost submenu can cover the existing Shell drag window. +// Show(TRUE) and DragOver do not repair its Z order. Restart only the visual +// helper, preserving the actual OLE target and the source data object identity. +static HRESULT RestoreShellDragImage( IDropTargetHelper *helper, HWND target, IDataObject *data, const POINT &point, DWORD effect ) +{ + if (!helper || !target || !data) return E_INVALIDARG; + POINT position=point; + helper->DragLeave(); + HRESULT result=helper->DragEnter(target,data,&position,effect); + if (SUCCEEDED(result)) result=helper->DragOver(&position,effect); + return result; +} + +void CMenuContainer::RestoreActiveDragImage( void ) +{ + for (auto menu : s_Menus) + if (!menu->m_bDestroyed && menu->m_pDragObject && menu->m_pDropTargetHelper && menu->m_DragImageWindow) + { + CComPtr data=menu->m_pDragObject; + CComPtr helper=menu->m_pDropTargetHelper; + RestoreShellDragImage(helper,menu->m_DragImageWindow,data,menu->m_DragImagePoint,menu->m_DragImageEffect); + break; + } +} + class CMetroDataObject: public IDataObject { public: @@ -211,12 +291,8 @@ CComPtr CMenuContainer::CreateMetroDataObject( const CItemManager:: if (m_pDragSourceHelper) { g_ItemManager.UpdateItemInfo(pInfo,CItemManager::INFO_EXTRA_LARGE_ICON|CItemManager::INFO_REFRESH_NOW,false); - int iconSize=CItemManager::EXTRA_LARGE_ICON_SIZE; - SHDRAGIMAGE di={{iconSize,iconSize},{iconSize/2,iconSize},NULL,CLR_NONE}; - di.hbmpDragImage=(HBITMAP)CopyImage(pInfo->extraLargeIcon->bitmap,IMAGE_BITMAP,0,0,0); - m_pDragSourceHelper->SetFlags(DSH_ALLOWDROPDESCRIPTIONTEXT); - if (di.hbmpDragImage) - m_pDragSourceHelper->InitializeFromBitmap(&di,pDataObject); + if (pInfo->extraLargeIcon) + InitializeMenuDragImage(m_pDragSourceHelper,pDataObject,pInfo->extraLargeIcon->bitmap); } } else @@ -279,7 +355,6 @@ bool CMenuContainer::DragOut( int index, bool bApp ) pDataObj=CreateMetroDataObject(item.pItemInfo); else { - bool bMetroLink=false; if (FAILED(SHBindToParent(item.pItem1,IID_IShellFolder,(void**)&pFolder,&child))) return true; if (FAILED(pFolder->GetUIObjectOf(NULL,1,&child,IID_IDataObject,NULL,(void**)&pDataObj))) @@ -288,17 +363,15 @@ bool CMenuContainer::DragOut( int index, bool bApp ) { CItemManager::RWLock lock(&g_ItemManager,false,CItemManager::RWLOCK_ITEMS); bProtectedLink=!m_bSubMenu || item.pItemInfo->IsProtectedLink(); - bMetroLink=item.pItemInfo->IsMetroLink(); } - if (m_pDragSourceHelper && bMetroLink) + if (m_pDragSourceHelper && item.pItemInfo) { - g_ItemManager.UpdateItemInfo(item.pItemInfo,CItemManager::INFO_EXTRA_LARGE_ICON|CItemManager::INFO_REFRESH_NOW,false); - int iconSize=CItemManager::EXTRA_LARGE_ICON_SIZE; - SHDRAGIMAGE di={{iconSize,iconSize},{iconSize/2,iconSize},NULL,CLR_NONE}; - di.hbmpDragImage=(HBITMAP)CopyImage(item.pItemInfo->extraLargeIcon->bitmap,IMAGE_BITMAP,0,0,0); - m_pDragSourceHelper->SetFlags(DSH_ALLOWDROPDESCRIPTIONTEXT); - if (di.hbmpDragImage) - m_pDragSourceHelper->InitializeFromBitmap(&di,pDataObj); + // Reuse the icon already displayed by the menu. Do not extract a new + // icon through Shell extensions or pump COM messages before the drag. + MenuSkin::TItemDrawType drawType=index==m_ProgramTreeIndex?MenuSkin::PROGRAMS_TREE_ITEM:item.drawType; + const CItemManager::IconInfo *icon=s_Skin.ItemSettings[drawType].iconSize==MenuSkin::ICON_SIZE_LARGE? + item.pItemInfo->largeIcon:item.pItemInfo->smallIcon; + if (icon) InitializeMenuDragImage(m_pDragSourceHelper,pDataObj,icon->bitmap); } } if (bProtectedLink) @@ -317,12 +390,8 @@ bool CMenuContainer::DragOut( int index, bool bApp ) } } - // force synchronous operation - { - CComQIPtr pAsync(pDataObj); - if (pAsync) - pAsync->SetAsyncMode(FALSE); - } + // This shell-created data object owns its asynchronous lifetime. + EnableShellAsyncDrop(pDataObj); // do drag drop s_pDragSource=this; @@ -336,6 +405,7 @@ bool CMenuContainer::DragOut( int index, bool bApp ) SetTimer(TIMER_DRAG,100); DWORD dwEffect=DROPEFFECT_COPY|DROPEFFECT_MOVE|DROPEFFECT_LINK; HRESULT res=SHDoDragDrop(NULL,pDataObj,NULL,dwEffect,&dwEffect); + bool bAsyncDrop=WasShellDropAsync(pDataObj); s_pDragSource=NULL; s_bDragFromTree=false; @@ -352,6 +422,15 @@ bool CMenuContainer::DragOut( int index, bool bApp ) return true; } + if (bAsyncDrop) + { + // Leave the menu usable while the native shell owns the transfer. + // Do not validate files early or take focus back from its dialog. + KeepAfterAsyncDrop(); + QueueFolderRefresh(); + return true; + } + if (pFolder && res==DRAGDROP_S_DROP && !m_bDestroyed) { // check if the item still exists. refresh the menu if it doesn't @@ -465,6 +544,9 @@ HRESULT STDMETHODCALLTYPE CMenuContainer::DragEnter( IDataObject *pDataObj, DWOR { s_bRightDrag=(grfKeyState&MK_RBUTTON)!=0; m_pDragObject=pDataObj; + m_DragImageWindow=m_hWnd; + m_DragImagePoint={pt.x,pt.y}; + m_DragImageEffect=*pdwEffect; if (m_pDropTargetHelper) { POINT p={pt.x,pt.y}; @@ -487,12 +569,17 @@ HRESULT STDMETHODCALLTYPE CMenuContainer::DragEnter( IDataObject *pDataObj, DWOR } m_DragHoverTime=GetMessageTime()-10000; m_DragHoverItem=-1; + // Also cover entry into an already raised topmost menu. + if (m_pDropTargetHelper) + RestoreShellDragImage(m_pDropTargetHelper,m_hWnd,m_pDragObject,m_DragImagePoint,*pdwEffect); return S_OK; } HRESULT STDMETHODCALLTYPE CMenuContainer::DragOver( DWORD grfKeyState, POINTL pt, DWORD *pdwEffect ) { POINT p0={pt.x,pt.y}; + m_DragImagePoint=p0; + m_DragImageEffect=*pdwEffect; if (m_pDropTargetHelper) m_pDropTargetHelper->DragOver(&p0,*pdwEffect); @@ -557,7 +644,9 @@ HRESULT STDMETHODCALLTYPE CMenuContainer::DragOver( DWORD grfKeyState, POINTL pt { SetDropTip(m_pDragObject,false); SetInsertMark(-1,false); - return m_pDropTarget->DragOver(grfKeyState,pt,pdwEffect); + HRESULT result=m_pDropTarget->DragOver(grfKeyState,pt,pdwEffect); + m_DragImageEffect=*pdwEffect; + return result; } s_bRightDrag=(grfKeyState&MK_RBUTTON)!=0; @@ -667,11 +756,13 @@ HRESULT STDMETHODCALLTYPE CMenuContainer::DragOver( DWORD grfKeyState, POINTL pt } else SetDropTip(m_pDragObject,false); + m_DragImageEffect=*pdwEffect; return S_OK; } HRESULT STDMETHODCALLTYPE CMenuContainer::DragLeave( void ) { + m_DragImageWindow=NULL; if (m_pDropTarget) { m_pDropTarget->DragLeave(); @@ -691,6 +782,7 @@ HRESULT STDMETHODCALLTYPE CMenuContainer::DragLeave( void ) HRESULT STDMETHODCALLTYPE CMenuContainer::Drop( IDataObject *pDataObj, DWORD grfKeyState, POINTL pt, DWORD *pdwEffect ) { + m_DragImageWindow=NULL; if (s_pDragSource) { if (!s_pDragSource->m_bDestroyed) @@ -705,6 +797,11 @@ HRESULT STDMETHODCALLTYPE CMenuContainer::Drop( IDataObject *pDataObj, DWORD grf { m_pDragObject=NULL; HRESULT res=m_pDropTarget->Drop(pDataObj,grfKeyState,pt,pdwEffect); + if (WasShellDropAsync(pDataObj)) + { + KeepAfterAsyncDrop(); + QueueFolderRefresh(); + } m_pDropTarget=NULL; return res; } @@ -783,6 +880,7 @@ HRESULT STDMETHODCALLTYPE CMenuContainer::Drop( IDataObject *pDataObj, DWORD grf *pdwEffect=DROPEFFECT_LINK; bool bDropped=false; + bool bAsyncDrop=false; if (!m_bSubMenu && (*pdwEffect&DROPEFFECT_LINK) && !s_bRightDrag) { // if dropping a single folder onto the main menu, create a fake folder @@ -849,9 +947,7 @@ HRESULT STDMETHODCALLTYPE CMenuContainer::Drop( IDataObject *pDataObj, DWORD grf dwEffect=*pdwEffect; pTarget->DragOver(MK_LBUTTON|grfKeyState,pt,pdwEffect); } - CComQIPtr pAsync=pDataObj; - if (pAsync) - pAsync->SetAsyncMode(FALSE); + // Preserve the async mode negotiated by the data source. for (auto& it : s_Menus) { if (!it->m_bDestroyed) @@ -866,6 +962,8 @@ HRESULT STDMETHODCALLTYPE CMenuContainer::Drop( IDataObject *pDataObj, DWORD grf s_bPreventClosing=true; AddRef(); pTarget->Drop(pDataObj,grfKeyState,pt,pdwEffect); + bAsyncDrop=WasShellDropAsync(pDataObj); + if (bAsyncDrop) KeepAfterAsyncDrop(); s_bPreventClosing=bOld; for (auto& it : s_Menus) { @@ -882,7 +980,7 @@ HRESULT STDMETHODCALLTYPE CMenuContainer::Drop( IDataObject *pDataObj, DWORD grf AddRef(); } - if (!m_bDestroyed) + if (!m_bDestroyed && !bAsyncDrop) { SetForegroundWindow(m_hWnd); SetActiveWindow(); @@ -909,7 +1007,8 @@ HRESULT STDMETHODCALLTYPE CMenuContainer::Drop( IDataObject *pDataObj, DWORD grf } if (m_bTwoColumns && s_MenuMode==MODE_JUMPLIST) SetMenuMode(MODE_NORMAL); - PostRefreshMessage(); + if (bAsyncDrop) QueueFolderRefresh(); + else PostRefreshMessage(); Release(); } return S_OK; diff --git a/Src/StartMenu/StartMenuDLL/MenuContainer.cpp b/Src/StartMenu/StartMenuDLL/MenuContainer.cpp index 3337b66a8..5d2cdea0e 100644 --- a/Src/StartMenu/StartMenuDLL/MenuContainer.cpp +++ b/Src/StartMenu/StartMenuDLL/MenuContainer.cpp @@ -325,6 +325,8 @@ bool CMenuContainer::s_bNoClose=false; bool CMenuContainer::s_bHasTouch=false; char CMenuContainer::s_bActiveDirectory=-1; bool CMenuContainer::s_bPreventClosing=false; +bool CMenuContainer::s_bKeepAfterAsyncDrop=false; +HHOOK CMenuContainer::s_AsyncDropMouseHook=NULL; bool CMenuContainer::s_bDragClosed=false; bool CMenuContainer::s_bTempHidden=false; bool CMenuContainer::s_bDisableHover=false; @@ -695,11 +697,13 @@ CMenuContainer::CMenuContainer( CMenuContainer *pParent, int index, int options, InitializeSpecialFolders(); m_bDestroyed=false; + if (s_Menus.empty()) s_bKeepAfterAsyncDrop=false; s_Menus.push_back(this); m_Submenu=-1; m_SubJumpItem=-1; m_bTrackMouse=false; m_RefreshPosted=0; + m_bFolderDirty=m_bTreeDirty=m_bFolderRefreshing=m_bFolderWatchStopped=false; m_SubShowTime=0; m_bInsertAfter=false; @@ -743,6 +747,9 @@ CMenuContainer::CMenuContainer( CMenuContainer *pParent, int index, int options, m_SearchHash=0; m_pDropTargetInfo=NULL; m_DropTargetIndex=-1; + m_DragImageWindow=NULL; + m_DragImagePoint.x=m_DragImagePoint.y=0; + m_DragImageEffect=DROPEFFECT_NONE; m_PanPosY=m_Overpan=m_PointerId=0; m_InputCookie=0; m_ScrollTheme=NULL; @@ -4483,6 +4490,7 @@ LRESULT CMenuContainer::OnCreate( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& } m_pDropTargetProxy=new CDropTargetProxy(this); RegisterDragDrop(m_hWnd,m_pDropTargetProxy); + StartFolderWatch(); if (!m_bSubMenu && s_pFrameworkInputPane) s_pFrameworkInputPane->AdviseWithHWND(m_hWnd,this,&m_InputCookie); PlayMenuSound(m_bSubMenu?SOUND_POPUP:SOUND_MAIN); @@ -5135,6 +5143,163 @@ void CMenuContainer::SetActiveWindow( void ) SetWindowPos(s_TaskBar,0,0,0,0,SWP_NOSIZE|SWP_NOMOVE); // make sure the top menu stays behind the taskbar } +bool CMenuContainer::CanRefreshFolders( void ) const +{ + if ((m_Options&(CONTAINER_SEARCH|CONTAINER_JUMPLIST)) || + s_bPreventClosing || s_pDragSource || m_pDragObject || m_pDropTarget || + m_ContextItem>=0 || m_bFolderRefreshing) + return false; + HWND capture=GetCapture(); + if (capture && (capture==m_hWnd || ::IsChild(m_hWnd,capture))) + return false; + if (m_pProgramsTree && m_pProgramsTree->m_hWnd && TreeView_GetEditControl(m_pProgramsTree->m_hWnd)) + return false; + return !s_bWin7Style || s_MenuMode==MODE_NORMAL || s_MenuMode==MODE_PROGRAMS; +} + +void CMenuContainer::QueueFolderRefresh( void ) +{ + if (!m_bDestroyed && !m_bFolderWatchStopped) + { + m_bFolderDirty=true; + SetTimer(TIMER_FOLDER_REFRESH,250); + } +} + +void CMenuContainer::StartFolderWatch( void ) +{ + if (m_Options&(CONTAINER_SEARCH|CONTAINER_JUMPLIST)) return; + for (int i=0;i<2;i++) + { + m_FolderWatch.Add(m_hWnd,MCM_FOLDERCHANGED,m_Path1[i]); + m_FolderWatch.Add(m_hWnd,MCM_FOLDERCHANGED,m_Path2[i]); + } + if (!m_bSubMenu) + { + ShGetKnownFolderIDList(FOLDERID_Programs,&m_ProgramWatchRoots[0]); + if (!s_bNoCommonFolders) + ShGetKnownFolderIDList(FOLDERID_CommonPrograms,&m_ProgramWatchRoots[1]); + for (int i=0;i<2;i++) + m_FolderWatch.Add(m_hWnd,MCM_FOLDERCHANGED,m_ProgramWatchRoots[i],true); + } +} + +LRESULT CMenuContainer::OnFolderChanged( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ) +{ + PIDLIST_ABSOLUTE *changed=NULL; + LONG event=0; + HANDLE notification=SHChangeNotification_Lock((HANDLE)wParam,(DWORD)lParam,&changed,&event); + if (!notification) return 0; + for (int side=0;side<2;side++) + { + if (!changed || !changed[side]) continue; + for (int i=0;i<2;i++) + { + if (CMenuFolderWatch::Contains(m_Path1[i],changed[side]) || + CMenuFolderWatch::Contains(m_Path2[i],changed[side])) + m_bFolderDirty=true; + if (CMenuFolderWatch::Contains(m_ProgramWatchRoots[i],changed[side],true)) + m_bTreeDirty=true; + } + } + SHChangeNotification_Unlock(notification); + // Coalesce a batch of shell notifications; never rebuild under its lock. + if (!m_bFolderWatchStopped && (m_bFolderDirty || m_bTreeDirty)) + SetTimer(TIMER_FOLDER_REFRESH,250); + return 0; +} + +int CMenuContainer::FindRefreshItem( PCIDLIST_ABSOLUTE pidl, TMenuID id, unsigned int nameHash ) const +{ + for (int i=0;i<(int)m_Items.size();i++) + { + const MenuItem &item=m_Items[i]; + if (pidl) + { + if ((item.pItem1 && ILIsEqual(pidl,item.pItem1)) || (item.pItem2 && ILIsEqual(pidl,item.pItem2))) + return i; + } + else if (item.id==id && item.nameHash==nameHash) + return i; + } + return -1; +} + +void CMenuContainer::RefreshFolderContents( void ) +{ + // Keep the child window and its item identity while the parent list changes. + // The normal MCM_REFRESH handler intentionally closes children and takes focus. + CMenuContainer *child=NULL; + for (auto menu : s_Menus) + if (!menu->m_bDestroyed && menu->m_pParent==this) { child=menu; break; } + CComPtr childLifetime=child?static_cast(child):NULL; + CAbsolutePidl childPidl,hotPidl; + TMenuID childId=MENU_NO,hotId=MENU_NO; + unsigned int childHash=0,hotHash=0; + RECT oldAnchor={}; + bool hadHot=m_HotItem>=0 && m_HotItem<(int)m_Items.size(); + bool hotArrow=m_bHotArrow; + if (hadHot) + { + const MenuItem &item=m_Items[m_HotItem]; + hotPidl.Clone(item.pItem1?item.pItem1:item.pItem2); + hotId=item.id; hotHash=item.nameHash; + } + if (child && child->m_ParentIndex>=0 && child->m_ParentIndex<(int)m_Items.size()) + { + const MenuItem &item=m_Items[child->m_ParentIndex]; + childPidl.Clone(item.pItem1?item.pItem1:item.pItem2); + childId=item.id; childHash=item.nameHash; + GetItemRect(child->m_ParentIndex,oldAnchor); + MapWindowPoints(NULL,&oldAnchor); + } + else child=NULL; + if (m_ScrollHeight>0 && m_FolderHash[0]) s_MenuScrolls[m_FolderHash[0]]=m_ScrollOffset; + else s_MenuScrolls.erase(m_FolderHash[0]); + KillTimer(TIMER_HOVER); + m_HoverItem=-1; + SetHotItem(-1); + InitItems(); + if (m_bDestroyed) return; + int childIndex=child?FindRefreshItem(childPidl,childId,childHash):-1; + if (child && !child->m_bDestroyed) + { + child->m_ParentIndex=childIndex; + if (childIndex>=0) SetSubmenu(childIndex); + else + { + // An actually deleted/moved folder cannot retain a valid submenu. + for (auto menu : s_Menus) + for (auto parent=menu;parent;parent=parent->m_pParent) + if (parent==child) { if (!menu->m_bDestroyed) menu->PostMessage(WM_CLOSE); break; } + } + } + InitWindow(); + SetHotItem(hadHot?FindRefreshItem(hotPidl,hotId,hotHash):-1,hotArrow,false); + if (childIndex>=0 && child && !child->m_bDestroyed) + { + RECT anchor,before,after; + GetItemRect(childIndex,anchor); MapWindowPoints(NULL,&anchor); + child->GetWindowRect(&before); + int dx=before.left>=oldAnchor.left?anchor.right-oldAnchor.right:anchor.left-oldAnchor.left; + child->SetWindowPos(NULL,before.left+dx,before.top+anchor.top-oldAnchor.top,0,0, + SWP_NOZORDER|SWP_NOACTIVATE|SWP_NOSIZE); + child->InitWindow(); // constrain the updated placement to the work area + child->GetWindowRect(&after); + for (auto menu : s_Menus) + if (menu!=child && !menu->m_bDestroyed) + for (auto parent=menu->m_pParent;parent;parent=parent->m_pParent) + if (parent==child) + { + RECT rect; menu->GetWindowRect(&rect); + menu->SetWindowPos(NULL,rect.left+after.left-before.left,rect.top+after.top-before.top, + 0,0,SWP_NOZORDER|SWP_NOACTIVATE|SWP_NOSIZE); + break; + } + } + Invalidate(); +} + void CMenuContainer::PostRefreshMessage( void ) { if (!m_bDestroyed) @@ -5240,6 +5405,26 @@ LRESULT CMenuContainer::OnWorkAreaChanged( UINT uMsg, WPARAM wParam, LPARAM lPar LRESULT CMenuContainer::OnTimer( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ) { + if (wParam==TIMER_FOLDER_REFRESH) + { + if (m_bFolderWatchStopped || m_bDestroyed) { KillTimer(TIMER_FOLDER_REFRESH); return 0; } + if (!CanRefreshFolders()) + return 0; // retry after the current drag, popup menu, or search + KillTimer(TIMER_FOLDER_REFRESH); + CComPtr keepAlive=static_cast(this); + bool treeDirty=m_bTreeDirty; + m_bTreeDirty=false; + if (m_bFolderDirty && !m_bDestroyed) + { + m_bFolderDirty=false; + m_bFolderRefreshing=true; + RefreshFolderContents(); + m_bFolderRefreshing=false; + } + if (treeDirty && !m_bDestroyed && m_pProgramsTree && m_pProgramsTree->m_hWnd) + m_pProgramsTree->PostMessage(CProgramsTree::TVM_REFRESH); + return 0; + } if (wParam==TIMER_HOVER) { LOG_MENU(LOG_MOUSE,L"End Hover, hover=%d%s, hot=%d, submenu=%d",m_HoverItem,m_bHoverArrow?L"(arrow)":L"",m_HotItem,m_Submenu); @@ -6185,6 +6370,9 @@ LRESULT CMenuContainer::OnChar( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& b LRESULT CMenuContainer::OnDestroy( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ) { LOG_MENU(LOG_OPEN,L"Close Menu, ptr=%p",this); + m_bFolderWatchStopped=true; + m_FolderWatch.Clear(); + KillTimer(TIMER_FOLDER_REFRESH); if (m_pAccessible) { NotifyWinEvent(EVENT_SYSTEM_MENUPOPUPEND,m_hWnd,OBJID_CLIENT,CHILDID_SELF); @@ -6226,6 +6414,9 @@ LRESULT CMenuContainer::OnDestroy( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL m_bDestroyed=true; if (this==s_Menus[0]) { + if (s_AsyncDropMouseHook) UnhookWindowsHookEx(s_AsyncDropMouseHook); + s_AsyncDropMouseHook=NULL; + s_bKeepAfterAsyncDrop=false; // cleanup when the last menu is closed if (s_Theme) CloseThemeData(s_Theme); @@ -6423,6 +6614,8 @@ LRESULT CMenuContainer::OnActivate( UINT uMsg, WPARAM wParam, LPARAM lParam, BOO #ifndef PREVENT_CLOSING if (s_bPreventClosing) return 0; + if (s_bKeepAfterAsyncDrop && IsShellOperationWindow((HWND)lParam)) + return 0; if (lParam) { @@ -6448,6 +6641,53 @@ LRESULT CMenuContainer::OnActivate( UINT uMsg, WPARAM wParam, LPARAM lParam, BOO return 0; } +// The menu may already be inactive after a native transfer dialog took focus. +// Observe subsequent button presses independently of WM_ACTIVATE. The hook runs +// on this UI thread, never consumes input, and only posts work to the root menu. +void CMenuContainer::KeepAfterAsyncDrop( void ) +{ + s_bKeepAfterAsyncDrop=true; + if (!s_AsyncDropMouseHook && s_FirstMenu) + s_AsyncDropMouseHook=SetWindowsHookEx(WH_MOUSE_LL,AsyncDropMouseHook,g_Instance,0); +} + +LRESULT CALLBACK CMenuContainer::AsyncDropMouseHook( int code, WPARAM wParam, LPARAM lParam ) +{ + if (code==HC_ACTION && s_bKeepAfterAsyncDrop && !s_bPreventClosing && !s_pDragSource && s_FirstMenu && + (wParam==WM_LBUTTONDOWN || wParam==WM_RBUTTONDOWN || wParam==WM_MBUTTONDOWN || wParam==WM_XBUTTONDOWN)) + { + const MSLLHOOKSTRUCT *mouse=(const MSLLHOOKSTRUCT*)lParam; + // Resolve before the click can dismiss its dialog or popup. + HWND target=WindowFromPoint(mouse->pt); + bool operation=target && IsShellOperationWindow(GetAncestor(target,GA_ROOT)); + ::PostMessage(s_FirstMenu,MCM_OUTSIDECLICK,(WPARAM)target,operation); + } + return CallNextHookEx(s_AsyncDropMouseHook,code,wParam,lParam); +} + +LRESULT CMenuContainer::OnOutsideClick( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ) +{ + if (!s_bKeepAfterAsyncDrop || s_bPreventClosing || s_pDragSource || m_bDestroyed) + return 0; + for (std::vector::const_iterator it=s_Menus.begin();it!=s_Menus.end();++it) + if (!(*it)->m_bDestroyed && ((*it)->m_pDragObject || (*it)->m_pDropTarget || (*it)->m_ContextItem>=0)) + return 0; + + HWND target=(HWND)wParam; + if (IsMenuWindow(target) || target==s_StartButton || (s_StartButton && ::IsChild(s_StartButton,target))) + return 0; + if (lParam || (target && IsShellOperationWindow(GetAncestor(target,GA_ROOT)))) + return 0; + // Use the existing close path without activating the taskbar or a previous app. + for (std::vector::reverse_iterator it=s_Menus.rbegin();it!=s_Menus.rend();++it) + if (!(*it)->m_bDestroyed && !(*it)->m_bClosing) + { + (*it)->m_bClosing=true; + (*it)->PostMessage(WM_CLOSE); + } + return 0; +} + LRESULT CMenuContainer::OnMouseActivate( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ) { if (m_Submenu>=0 || (m_Options&CONTAINER_SEARCH)) diff --git a/Src/StartMenu/StartMenuDLL/MenuContainer.h b/Src/StartMenu/StartMenuDLL/MenuContainer.h index 057982104..b5d3f6d7a 100644 --- a/Src/StartMenu/StartMenuDLL/MenuContainer.h +++ b/Src/StartMenu/StartMenuDLL/MenuContainer.h @@ -10,6 +10,7 @@ #include "SearchManager.h" #include "DragDrop.h" #include "TouchHelper.h" +#include "MenuFolderWatch.h" #include #include #include @@ -312,6 +313,8 @@ class CMenuContainer: public IDropTarget, public IFrameworkInputPaneHandler, pub MESSAGE_HANDLER( WM_GESTURE, OnGesture ) MESSAGE_HANDLER( WM_GESTURENOTIFY, OnGestureNotify ) MESSAGE_HANDLER( MCM_REFRESH, OnRefresh ) + MESSAGE_HANDLER( MCM_FOLDERCHANGED, OnFolderChanged ) + MESSAGE_HANDLER( MCM_OUTSIDECLICK, OnOutsideClick ) MESSAGE_HANDLER( MCM_SETCONTEXTITEM, OnSetContextItem ) MESSAGE_HANDLER( MCM_REDRAWEDIT, OnRedrawEdit ) MESSAGE_HANDLER( MCM_REFRESHICONS, OnRefreshIcons ) @@ -367,9 +370,19 @@ class CMenuContainer: public IDropTarget, public IFrameworkInputPaneHandler, pub LRESULT OnDestroy( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ); LRESULT OnShowWindow( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ); LRESULT OnRefresh( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ); + LRESULT OnFolderChanged( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ); + void StartFolderWatch( void ); + void RefreshFolderContents( void ); + void QueueFolderRefresh( void ); + bool CanRefreshFolders( void ) const; + int FindRefreshItem( PCIDLIST_ABSOLUTE pidl, TMenuID id, unsigned int nameHash ) const; LRESULT OnPaint( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ); LRESULT OnEraseBkgnd( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ) { return 1; } LRESULT OnActivate( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ); + LRESULT OnOutsideClick( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ); + static void KeepAfterAsyncDrop( void ); + static void RestoreActiveDragImage( void ); + static LRESULT CALLBACK AsyncDropMouseHook( int code, WPARAM wParam, LPARAM lParam ); LRESULT OnMouseActivate( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ); LRESULT OnMouseMove( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ); LRESULT OnMouseLeave( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ); @@ -614,6 +627,9 @@ class CMenuContainer: public IDropTarget, public IFrameworkInputPaneHandler, pub bool m_bRefreshItems; bool m_bWorkAreaPosted; volatile DWORD m_RefreshPosted; + CMenuFolderWatch m_FolderWatch; + CAbsolutePidl m_ProgramWatchRoots[2]; + bool m_bFolderDirty, m_bTreeDirty, m_bFolderRefreshing, m_bFolderWatchStopped; int m_Options; const StdMenuItem *m_pStdItem; // the first item CMenuContainer *m_pParent; // parent menu @@ -645,6 +661,9 @@ class CMenuContainer: public IDropTarget, public IFrameworkInputPaneHandler, pub CComPtr m_pDropTargetHelper; // to show images while dragging CComPtr m_pDragSourceHelper; CComPtr m_pDragObject; + HWND m_DragImageWindow; // current menu or embedded tree receiving the drag + POINT m_DragImagePoint; // last screen coordinates passed to the Shell helper + DWORD m_DragImageEffect; CComPtr m_pDropTarget; const CItemManager::ItemInfo *m_pDropTargetInfo; int m_DropTargetIndex; @@ -763,6 +782,7 @@ class CMenuContainer: public IDropTarget, public IFrameworkInputPaneHandler, pub TIMER_BALLOON_HIDE=6, TIMER_SEARCH=7, TIMER_DRAG=8, + TIMER_FOLDER_REFRESH=9, MCM_REFRESH=WM_USER+10, // posted to force the container to refresh its contents MCM_SETCONTEXTITEM=WM_USER+11, // sets the item for the context menu. wParam is the nameHash of the item @@ -770,6 +790,8 @@ class CMenuContainer: public IDropTarget, public IFrameworkInputPaneHandler, pub MCM_REFRESHICONS=WM_USER+13, // refreshes the icon list and redraws all menus MCM_SETHOTITEM=WM_USER+14, // sets the hot item MCM_WORKAREACHANGED=WM_USER+15, // when the working area is changed + MCM_FOLDERCHANGED=WM_USER+16, // SHChangeNotifyRegister shared-memory notification + MCM_OUTSIDECLICK=WM_USER+17, // click outside an inactive menu after an async drop // some constants MIN_SCROLL_HEIGHT=13, // the scroll buttons are at least this tall @@ -886,6 +908,8 @@ class CMenuContainer: public IDropTarget, public IFrameworkInputPaneHandler, pub static bool s_bHasTouch; // the device has integrated touch digitizer static char s_bActiveDirectory; // the Active Directory services are available (-1 - uninitialized) static bool s_bPreventClosing; // prevents the menus from closing even if they lose focus + static bool s_bKeepAfterAsyncDrop; // keep menus when the native transfer dialog gains focus + static HHOOK s_AsyncDropMouseHook; // exists only for the retained menu session static bool s_bDragClosed; // the menu was closed during a drag operation static bool s_bTempHidden; // the menu windows are temporarily hidden static bool s_bDisableHover; // disable hovering while the search box has the focus diff --git a/Src/StartMenu/StartMenuDLL/MenuFolderWatch.h b/Src/StartMenu/StartMenuDLL/MenuFolderWatch.h new file mode 100644 index 000000000..820650045 --- /dev/null +++ b/Src/StartMenu/StartMenuDLL/MenuFolderWatch.h @@ -0,0 +1,38 @@ +// Open-Shell: directory notifications for an open menu; no file-operation worker. +#pragma once +#include +#include +#include + +class CMenuFolderWatch +{ +public: + CMenuFolderWatch( void ) = default; + CMenuFolderWatch( const CMenuFolderWatch& ) = delete; + CMenuFolderWatch& operator=( const CMenuFolderWatch& ) = delete; + ~CMenuFolderWatch( void ) { Clear(); } + bool Add( HWND window, UINT message, PCIDLIST_ABSOLUTE folder, bool recursive=false ) + { + if (!folder) return false; + SHChangeNotifyEntry entry={folder,recursive?TRUE:FALSE}; + ULONG cookie=SHChangeNotifyRegister(window, + SHCNRF_ShellLevel|SHCNRF_InterruptLevel|SHCNRF_NewDelivery| + (recursive?SHCNRF_RecursiveInterrupt:0), + SHCNE_RENAMEITEM|SHCNE_CREATE|SHCNE_DELETE|SHCNE_MKDIR|SHCNE_RMDIR| + SHCNE_UPDATEITEM|SHCNE_UPDATEDIR|SHCNE_RENAMEFOLDER|SHCNE_ATTRIBUTES, + message,1,&entry); + if (cookie) m_Cookies.push_back(cookie); + return cookie!=0; + } + void Clear( void ) + { + for (ULONG cookie : m_Cookies) SHChangeNotifyDeregister(cookie); + m_Cookies.clear(); + } + static bool Contains( PCIDLIST_ABSOLUTE folder, PCIDLIST_ABSOLUTE changed, bool recursive=false ) + { + return folder && changed && (ILIsEqual(folder,changed) || ILIsParent(folder,changed,recursive?FALSE:TRUE)); + } +private: + std::vector m_Cookies; +}; diff --git a/Src/StartMenu/StartMenuDLL/MenuPaint.cpp b/Src/StartMenu/StartMenuDLL/MenuPaint.cpp index 9ea9d6710..16ed3e959 100644 --- a/Src/StartMenu/StartMenuDLL/MenuPaint.cpp +++ b/Src/StartMenu/StartMenuDLL/MenuPaint.cpp @@ -3043,6 +3043,8 @@ void CMenuContainer::AnimateMenu( int flags, int speed, const RECT &rect ) s_UserPicture.SetWindowPos(NULL,&s_UserPictureRect,SWP_NOZORDER|SWP_NOACTIVATE|SWP_SHOWWINDOW); } + RestoreActiveDragImage(); // keep the preview above the newly raised menu + // animate auto time0=steady_clock::now(); while (true) @@ -3099,6 +3101,8 @@ void CMenuContainer::AnimateMenu( int flags, int speed, const RECT &rect ) rgn0=ExtCreateRegion(&xform,rgnSize,(RGNDATA*)&buf[0]); } + RestoreActiveDragImage(); // keep the preview above the newly raised menu + // animate auto time0=steady_clock::now(); int movex=0, movey=0; @@ -3169,6 +3173,7 @@ void CMenuContainer::AnimateMenu( int flags, int speed, const RECT &rect ) { // no animation SetWindowPos((flags&AW_TOPMOST)?HWND_TOPMOST:HWND_TOP,&rect,SWP_SHOWWINDOW|((flags&AW_ACTIVATE)?0:SWP_NOACTIVATE)); + RestoreActiveDragImage(); RedrawWindow(NULL,NULL,RDW_INVALIDATE|RDW_FRAME|RDW_UPDATENOW); if (bUserPic) { diff --git a/Src/StartMenu/StartMenuDLL/ProgramsTree.cpp b/Src/StartMenu/StartMenuDLL/ProgramsTree.cpp index 2e55c32b5..ed4b73933 100644 --- a/Src/StartMenu/StartMenuDLL/ProgramsTree.cpp +++ b/Src/StartMenu/StartMenuDLL/ProgramsTree.cpp @@ -1203,6 +1203,9 @@ void CProgramsTree::SelectLast( void ) HRESULT CProgramsTree::DragEnter( IDataObject *pDataObj, DWORD grfKeyState, POINTL pt, DWORD *pdwEffect ) { CMenuContainer::s_bRightDrag=(grfKeyState&MK_RBUTTON)!=0; + m_pOwner->m_DragImageWindow=m_hWnd; + m_pOwner->m_DragImagePoint={pt.x,pt.y}; + m_pOwner->m_DragImageEffect=*pdwEffect; if (m_pOwner->m_pDropTargetHelper) { POINT p={pt.x,pt.y}; @@ -1211,6 +1214,7 @@ HRESULT CProgramsTree::DragEnter( IDataObject *pDataObj, DWORD grfKeyState, POIN m_pOwner->m_DragHoverTime=GetMessageTime()-10000; m_HoverItem=NULL; m_pOwner->m_pDragObject=pDataObj; + CMenuContainer::RestoreActiveDragImage(); return S_OK; } @@ -1286,6 +1290,8 @@ HRESULT CProgramsTree::DragOver( DWORD grfKeyState, POINTL pt, DWORD *pdwEffect { CMenuContainer::s_bRightDrag=(grfKeyState&MK_RBUTTON)!=0; POINT p={pt.x,pt.y}; + m_pOwner->m_DragImagePoint=p; + m_pOwner->m_DragImageEffect=*pdwEffect; if (m_pOwner->m_pDropTargetHelper) { m_pOwner->m_pDropTargetHelper->DragOver(&p,*pdwEffect); @@ -1439,6 +1445,7 @@ HRESULT CProgramsTree::DragOver( DWORD grfKeyState, POINTL pt, DWORD *pdwEffect HRESULT CProgramsTree::DragLeave( void ) { + m_pOwner->m_DragImageWindow=NULL; if (m_pOwner->m_pDropTargetHelper) m_pOwner->m_pDropTargetHelper->DragLeave(); m_pOwner->m_pDragObject.Release(); @@ -1450,6 +1457,7 @@ HRESULT CProgramsTree::DragLeave( void ) HRESULT CProgramsTree::Drop( IDataObject *pDataObj, DWORD grfKeyState, POINTL pt, DWORD *pdwEffect ) { + m_pOwner->m_DragImageWindow=NULL; if (CMenuContainer::s_pDragSource) { if (!CMenuContainer::s_pDragSource->m_bDestroyed) @@ -1591,9 +1599,7 @@ HRESULT CProgramsTree::Drop( IDataObject *pDataObj, DWORD grfKeyState, POINTL pt dwEffect=*pdwEffect; pTarget->DragOver(MK_LBUTTON|grfKeyState,pt,pdwEffect); } - CComQIPtr pAsync=pDataObj; - if (pAsync) - pAsync->SetAsyncMode(FALSE); + // Preserve the async mode negotiated by the data source. for (std::vector::iterator it=CMenuContainer::s_Menus.begin();it!=CMenuContainer::s_Menus.end();++it) if (!(*it)->m_bDestroyed) (*it)->EnableWindow(FALSE); // disable all menus @@ -1601,11 +1607,15 @@ HRESULT CProgramsTree::Drop( IDataObject *pDataObj, DWORD grfKeyState, POINTL pt CMenuContainer::s_bPreventClosing=true; m_pOwner->AddRef(); pTarget->Drop(pDataObj,grfKeyState,pt,pdwEffect); + CComQIPtr async(pDataObj); + BOOL asynchronous=FALSE; + bool bAsyncDrop=async && SUCCEEDED(async->InOperation(&asynchronous)) && asynchronous; + if (bAsyncDrop) CMenuContainer::KeepAfterAsyncDrop(); CMenuContainer::s_bPreventClosing=bOld; for (std::vector::iterator it=CMenuContainer::s_Menus.begin();it!=CMenuContainer::s_Menus.end();++it) if (!(*it)->m_bDestroyed) (*it)->EnableWindow(TRUE); // enable all menus - if (!m_pOwner->m_bDestroyed) + if (!m_pOwner->m_bDestroyed && !bAsyncDrop) { SetForegroundWindow(m_pOwner->m_hWnd); m_pOwner->SetActiveWindow(); diff --git a/Src/StartMenu/StartMenuDLL/StartMenuDLL.cpp b/Src/StartMenu/StartMenuDLL/StartMenuDLL.cpp index c08ec4d20..9377e9849 100644 --- a/Src/StartMenu/StartMenuDLL/StartMenuDLL.cpp +++ b/Src/StartMenu/StartMenuDLL/StartMenuDLL.cpp @@ -285,6 +285,31 @@ static bool CreateImmersiveShell( CComPtr &ptr ) /////////////////////////////////////////////////////////////////////////////// +// Shell transfer dialogs may live on a different UI thread and have no owner. +// Other dialog classes are accepted only through our own owner chain. +bool IsShellOperationWindow( HWND window ) +{ + if (!window) window=GetForegroundWindow(); + DWORD process=0; + if (!GetWindowThreadProcessId(window,&process) || process!=GetCurrentProcessId()) + return false; + wchar_t name[64]; + if (!GetClassName(window,name,_countof(name))) + return false; + if (wcscmp(name,L"OperationStatusWindow")==0) + return true; + if (wcscmp(name,L"#32770")!=0) + return false; + for (int depth=0;window && depth<32;depth++,window=GetWindow(window,GW_OWNER)) + { + if (window==g_OwnerWindow && g_OwnerWindow) + return true; + if (!GetWindowThreadProcessId(window,&process) || process!=GetCurrentProcessId()) + break; + } + return false; +} + // COwnerWindow - a special window used as owner for some UI elements, like the ones created by IContextMenu::InvokeCommand. // A menu window cannot be used because it may disappear immediately after InvokeCommand. Some UI elements, like the UAC-related // stuff can be created long after InvokeCommand returns and the menu may be deleted by then. @@ -309,6 +334,8 @@ class COwnerWindow: public CWindowImpl if (CMenuContainer::s_bPreventClosing) return 0; + if (CMenuContainer::s_bKeepAfterAsyncDrop && IsShellOperationWindow((HWND)lParam)) + return 0; // check if another menu window is being activated // if not, close all menus diff --git a/Src/StartMenu/StartMenuDLL/StartMenuDLL.h b/Src/StartMenu/StartMenuDLL/StartMenuDLL.h index 81b8ed39f..a918c97be 100644 --- a/Src/StartMenu/StartMenuDLL/StartMenuDLL.h +++ b/Src/StartMenu/StartMenuDLL/StartMenuDLL.h @@ -77,6 +77,9 @@ struct TaskbarInfo TaskbarInfo *GetTaskbarInfo( size_t taskbarId ); UINT GetTaskbarPosition( HWND taskBar, MONITORINFO *pInfo, HMONITOR *pMonitor, RECT *pRc ); +// Native shell transfer UI in this process; does not activate or signal it. +bool IsShellOperationWindow( HWND window ); + extern HWND STARTMENUAPI g_TaskBar, g_OwnerWindow; extern HWND g_TopWin7Menu, g_AllPrograms, g_ProgramsButton, g_UserPic; // from the Windows menu extern HWND g_ProgWin; diff --git a/Src/StartMenu/StartMenuDLL/StartMenuDLL.vcxproj b/Src/StartMenu/StartMenuDLL/StartMenuDLL.vcxproj index 7d828042f..6b7b05110 100644 --- a/Src/StartMenu/StartMenuDLL/StartMenuDLL.vcxproj +++ b/Src/StartMenu/StartMenuDLL/StartMenuDLL.vcxproj @@ -101,6 +101,7 @@ + diff --git a/Src/StartMenu/StartMenuDLL/StartMenuDLL.vcxproj.filters b/Src/StartMenu/StartMenuDLL/StartMenuDLL.vcxproj.filters index fc2bd40cb..7370d95e4 100644 --- a/Src/StartMenu/StartMenuDLL/StartMenuDLL.vcxproj.filters +++ b/Src/StartMenu/StartMenuDLL/StartMenuDLL.vcxproj.filters @@ -101,6 +101,9 @@ Header Files + + Header Files + Header Files From 386cb97b1315039f3320f2c1f097f768a60041e5 Mon Sep 17 00:00:00 2001 From: YellowNest <59575587+YellowNest@users.noreply.github.com> Date: Sat, 17 Oct 2026 08:43:10 +0200 Subject: [PATCH 4/5] Fix open file location to use shortcut target --- Src/StartMenu/StartMenuDLL/MenuCommands.cpp | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Src/StartMenu/StartMenuDLL/MenuCommands.cpp b/Src/StartMenu/StartMenuDLL/MenuCommands.cpp index ac35f04f7..1be55825a 100644 --- a/Src/StartMenu/StartMenuDLL/MenuCommands.cpp +++ b/Src/StartMenu/StartMenuDLL/MenuCommands.cpp @@ -71,6 +71,21 @@ static HRESULT SHOpenFolderAndSelectItemsSafe(PCIDLIST_ABSOLUTE pidlFolder, UINT }); } +// Open a shortcut's target location, not the folder containing the shortcut itself. +static HRESULT OpenContainingItemSafe(PCIDLIST_ABSOLUTE pidl) +{ + return ExecuteOnSTAThread([=]() { + CComPtr item, target; + CAbsolutePidl targetPidl; + if (SUCCEEDED(SHCreateItemFromIDList(pidl, IID_IShellItem, (void**)&item)) && + SUCCEEDED(item->BindToHandler(NULL, BHID_LinkTargetItem, IID_IShellItem, (void**)&target)) && + SUCCEEDED(SHGetIDListFromObject(target, &targetPidl))) + return SHOpenFolderAndSelectItems(targetPidl, 0, NULL, 0); + + return SHOpenFolderAndSelectItems(pidl, 0, NULL, 0); + }); +} + static CString g_RenameText; static POINT g_RenamePos; @@ -3178,7 +3193,7 @@ void CMenuContainer::ActivateItem( int index, TActivateType type, const POINT *p // Use the shell API on a worker STA for that operation, without passing // the apartment-bound IContextMenu interface to another thread. if (verb.CompareNoCase(L"opencontaining") == 0) - hr = SHOpenFolderAndSelectItemsSafe(pItemPidl1, 0, NULL, 0); + hr = OpenContainingItemSafe(pItemPidl1); else hr = pInvokeMenu->InvokeCommand((LPCMINVOKECOMMANDINFO)&info); LOG_MENU(LOG_EXECUTE,L"Invoke command, ptr=%p, res=%d",this,hr); From 5394160b17e11a6824fd84d7dffb68e174bf1076 Mon Sep 17 00:00:00 2001 From: projektion0 <98270100+projektion0@users.noreply.github.com> Date: Sat, 17 Oct 2026 23:59:55 +0200 Subject: [PATCH 5/5] Tests: exercise open-containing resolution with native Shell links --- .gitignore | 3 + tests/README.md | 38 +++++++++++ tests/run_open_containing.py | 35 ++++++++++ tests/test_open_containing.cpp | 115 +++++++++++++++++++++++++++++++++ 4 files changed, 191 insertions(+) create mode 100644 tests/README.md create mode 100644 tests/run_open_containing.py create mode 100644 tests/test_open_containing.cpp diff --git a/.gitignore b/.gitignore index 75591d3db..0ec23cf05 100644 --- a/.gitignore +++ b/.gitignore @@ -360,3 +360,6 @@ Src/Setup/Temp/ *.zip *.bin *.chm + +# Disposable native Shell regression test fixtures +/build/open-containing-tests/ diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 000000000..258997600 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,38 @@ +# Open file location regression test + +The `opencontaining` command must select a shortcut's target instead of selecting +the `.lnk` in its Start Menu folder. The separate shortcut-folder command keeps +its existing behavior. + +The production fix in `MenuCommands.cpp` follows +[YellowNest/aaa8c635](https://github.com/YellowNest/Open-Shell-Menu/commit/aaa8c635abbc5e75533e6e76807370390409f59c). +It resolves `BHID_LinkTargetItem` and obtains the target PIDL inside the existing +worker STA. If item creation, link binding or PIDL extraction fails, the original +PIDL is used. The apartment-bound context-menu interface stays on its own thread. + +Run from the project root: + +```bat +python tests\run_open_containing.py +``` + +Requires the installed Visual Studio C++ toolset with ATL, Windows SDK and Python. +The runner extracts the actual production STA executor and both folder-opening +helpers, then compiles them with the production `ComHelper.h` PIDL ownership code. +Only the final `SHOpenFolderAndSelectItems` call is replaced by a capture function; +Shell shortcut creation, binding and PIDL extraction use real Windows APIs. + +The test covers program links with spaces, Unicode and arguments; folder links; +ordinary files and folders; invalid shortcuts; invalid input; error propagation; +and the separate command that opens the shortcut's own folder. It also verifies +the worker's STA, unchanged selection arguments, and caller message pumping. + +Test artifacts and private fixtures are written under `build/open-containing-tests`. +They are disposable. No Explorer windows are opened and no user files are changed. +Passing this test does not establish full Start Menu/Explorer UI verification. + +Release build: + +```bat +MSBuild OpenShell-VS2026.sln /m /p:Configuration=Release /p:Platform=x64 +``` diff --git a/tests/run_open_containing.py b/tests/run_open_containing.py new file mode 100644 index 000000000..64aaa4a2c --- /dev/null +++ b/tests/run_open_containing.py @@ -0,0 +1,35 @@ +"""Test the exact production STA executor and opencontaining helper with real Shell links.""" +from pathlib import Path +import os +import subprocess + +root = Path(__file__).resolve().parent.parent +source = (root / 'Src/StartMenu/StartMenuDLL/MenuCommands.cpp').read_text(encoding='utf-8') + +def function(signature): + start = source.index(signature) + end = source.index('{', start) + 1 + depth = 1 + while depth: + if source[end] == '{': depth += 1 + if source[end] == '}': depth -= 1 + end += 1 + return source[start:end] + +functions = '\n\n'.join(function(signature) for signature in [ + 'template \nstatic auto ExecuteOnSTAThread(', + 'static HRESULT SHOpenFolderAndSelectItemsSafe(', + 'static HRESULT OpenContainingItemSafe(', +]) +output = root / 'build/open-containing-tests' +output.mkdir(parents=True, exist_ok=True) +template = (root / 'tests/test_open_containing.cpp').read_text(encoding='utf-8') +(output / 'test.cpp').write_text(template.replace('// PRODUCTION_FUNCTIONS', functions), encoding='utf-8') +vswhere = Path(os.environ['ProgramFiles(x86)']) / 'Microsoft Visual Studio/Installer/vswhere.exe' +vs = Path(subprocess.check_output([str(vswhere), '-latest', '-products', '*', '-property', 'installationPath'], text=True).strip()) +toolsets = sorted(p for p in (vs / 'VC/Tools/MSVC').iterdir() if (p / 'atlmfc/include/atlbase.h').exists()) +command = ('call "{}" -vcvars_ver={} >nul && cl /nologo /std:c++20 /EHsc /W4 ' + '/DUNICODE /D_UNICODE /I"{}" test.cpp /Fe:test.exe ' + 'user32.lib ole32.lib shell32.lib uuid.lib && test.exe "{}"').format( + vs / 'VC/Auxiliary/Build/vcvars64.bat', toolsets[-1].name, root / 'Src/Lib', output) +subprocess.run(command, shell=True, cwd=output, check=True, timeout=45) diff --git a/tests/test_open_containing.cpp b/tests/test_open_containing.cpp new file mode 100644 index 000000000..60ef96e75 --- /dev/null +++ b/tests/test_open_containing.cpp @@ -0,0 +1,115 @@ +#define NOMINMAX +#define STRICT_TYPED_ITEMIDS +#include +#include +#include +#include "ComHelper.h" +#include +#include +#include + +static int checks=0,calls=0; +static DWORD callerThread=0,selectionThread=0; +static APTTYPE selectionApartment=APTTYPE_CURRENT; +static bool selectionArguments=false,messagePumped=false; +static HWND callerWindow=NULL; +static HANDLE messageEvent=NULL; +static HRESULT selectionResult=S_OK; +static CAbsolutePidl selected; + +static void Check(bool value,const char *description) +{ + if (!value) { fprintf(stderr,"FAIL: %s\n",description); ExitProcess(1); } + checks++; printf("PASS: %s\n",description); fflush(stdout); +} +static LRESULT CALLBACK WindowProc(HWND window,UINT message,WPARAM wParam,LPARAM lParam) +{ + if (message==WM_APP+1) { SetEvent(messageEvent); return 0; } + return DefWindowProc(window,message,wParam,lParam); +} +// Replace only the final Explorer-opening call. Link binding and PIDL extraction +// remain real Windows APIs running through the actual production STA executor. +static HRESULT CaptureSelection(PCIDLIST_ABSOLUTE pidl,UINT count,PCUITEMID_CHILD_ARRAY children,DWORD flags) +{ + calls++; + selected.Clone(pidl); + selectionThread=GetCurrentThreadId(); + APTTYPEQUALIFIER qualifier; + CoGetApartmentType(&selectionApartment,&qualifier); + selectionArguments=count==0 && children==NULL && flags==0; + PostMessage(callerWindow,WM_APP+1,0,0); + messagePumped=WaitForSingleObject(messageEvent,3000)==WAIT_OBJECT_0; + return selectionResult; +} +#define SHOpenFolderAndSelectItems CaptureSelection +// PRODUCTION_FUNCTIONS +#undef SHOpenFolderAndSelectItems + +static CAbsolutePidl Parse(const std::wstring &path) +{ + CAbsolutePidl pidl; + Check(SUCCEEDED(SHParseDisplayName(path.c_str(),NULL,&pidl,0,NULL)),"private fixture PIDL resolved"); + return pidl; +} +static void MakeFile(const std::wstring &path) +{ + HANDLE file=CreateFile(path.c_str(),GENERIC_WRITE,0,NULL,CREATE_NEW,FILE_ATTRIBUTE_NORMAL,NULL); + Check(file!=INVALID_HANDLE_VALUE,"new private fixture file created without overwriting user files"); + CloseHandle(file); +} +static void MakeShortcut(const std::wstring &path,const std::wstring &target) +{ + CComPtr link; + Check(SUCCEEDED(link.CoCreateInstance(CLSID_ShellLink)),"real Shell shortcut created"); + Check(SUCCEEDED(link->SetPath(target.c_str())) && SUCCEEDED(link->SetArguments(L"--fixture-argument")),"shortcut target and independent arguments stored"); + CComQIPtr persist(link); + Check(persist && SUCCEEDED(persist->Save(path.c_str(),FALSE)),"new private shortcut saved"); +} +static void RunCase(const char *name,PCIDLIST_ABSOLUTE original,PCIDLIST_ABSOLUTE expected,HRESULT apiResult=S_OK,bool exploreShortcut=false) +{ + printf("Case: %s\n",name); fflush(stdout); + calls=0; selected.Clear(); selectionThread=0; + selectionApartment=APTTYPE_CURRENT; + selectionArguments=messagePumped=false; + selectionResult=apiResult; + ResetEvent(messageEvent); + HRESULT result=exploreShortcut?SHOpenFolderAndSelectItemsSafe(original,0,NULL,0):OpenContainingItemSafe(original); + Check(calls==1,"exactly one folder-selection request issued"); + Check(result==apiResult,"folder-selection HRESULT preserved"); + Check(expected?selected && ILIsEqual(selected,expected):!selected,"Explorer would receive the expected target or fallback PIDL"); + Check(selectionThread && selectionThread!=callerThread && selectionApartment==APTTYPE_STA,"selection and link resolution run on a separate STA"); + Check(selectionArguments,"single-item selection arguments preserved"); + Check(messagePumped,"caller processes its own messages while the worker runs"); +} +int wmain(int argc,wchar_t **argv) +{ + if (argc!=2) return 2; + Check(SUCCEEDED(CoInitializeEx(NULL,COINIT_APARTMENTTHREADED)),"caller STA initialized"); + callerThread=GetCurrentThreadId(); + messageEvent=CreateEvent(NULL,TRUE,FALSE,NULL); + Check(messageEvent!=NULL,"private synchronization event created"); + WNDCLASS wc={}; wc.lpfnWndProc=WindowProc; wc.hInstance=GetModuleHandle(NULL); wc.lpszClassName=L"OpenShell.OpenContaining.Test"; + Check(RegisterClass(&wc)!=0,"private message-window class registered"); + callerWindow=CreateWindowEx(0,wc.lpszClassName,L"",0,0,0,0,0,HWND_MESSAGE,NULL,wc.hInstance,NULL); + Check(callerWindow!=NULL,"private invisible caller window created"); + std::wstring base=std::wstring(argv[1])+L"\\fixtures-"+std::to_wstring(GetCurrentProcessId())+L"-"+std::to_wstring(GetTickCount64()); + Check(CreateDirectory(base.c_str(),NULL)!=FALSE,"fresh isolated fixture root created"); + std::wstring links=base+L"\\Start Menu",programs=base+L"\\Program Files (\u00dc)"; + Check(CreateDirectory(links.c_str(),NULL) && CreateDirectory(programs.c_str(),NULL),"shortcut folder differs from program target folder"); + std::wstring program=programs+L"\\Application.exe"; + MakeFile(program); + std::wstring shortcut=links+L"\\Application \u00e4.lnk",folderShortcut=links+L"\\Folder.lnk",invalidShortcut=links+L"\\Invalid.lnk"; + MakeShortcut(shortcut,program); MakeShortcut(folderShortcut,programs); MakeFile(invalidShortcut); + auto programPidl=Parse(program),folderPidl=Parse(programs),linkPidl=Parse(shortcut),folderLinkPidl=Parse(folderShortcut),invalidPidl=Parse(invalidShortcut); + Check(!ILIsEqual(linkPidl,programPidl),"regression fixture distinguishes shortcut from program PIDL"); + RunCase("shortcut to program with spaces, Unicode and arguments",linkPidl,programPidl); + RunCase("shortcut to folder",folderLinkPidl,folderPidl); + RunCase("ordinary program file",programPidl,programPidl); + RunCase("ordinary folder",folderPidl,folderPidl); + RunCase("unresolvable invalid shortcut falls back",invalidPidl,invalidPidl); + RunCase("invalid input is passed through to the final API",NULL,NULL,E_INVALIDARG); + RunCase("target-folder API failure is not hidden by opening the shortcut folder",linkPidl,programPidl,E_ACCESSDENIED); + RunCase("separate shortcut-folder command remains unchanged",linkPidl,linkPidl,S_OK,true); + DestroyWindow(callerWindow); CloseHandle(messageEvent); + printf("%d checks passed. No Explorer windows were opened; fixtures are under build/open-containing-tests.\n",checks); +}