From 1142fc9593c5a2ff457363faa447b5603df61205 Mon Sep 17 00:00:00 2001 From: Z2549 Date: Tue, 29 Sep 2026 19:08:42 +0800 Subject: [PATCH 1/2] fix(shell): re-read deferred native submenus after their owner fills them Shell popups such as "Send To" are populated lazily: their owner keeps inserting items into the HMENU after menu::construct_with_hmenu has already enumerated it. The Breeze widget was therefore built from a stale snapshot (usually a single leftover item), so opening e.g. "Send To" showed an empty popup even though the items arrived a few milliseconds later. Evidence from the debug.log attached to #378: 14.458 [info] Menu widget init from data: 1 14.462 [warning] Late menu mutation via SetMenuItemInfoW (...) 14.462..14.665 Late menu mutation via InsertMenuItemW (...) x45 The mutation hooks already detect these late changes, but they only re-synced *state* (sync_native_menu_item_update). Extend the same idea to the item list: * contextmenu: remember handle_menu_msg / init_popup_lparam so a menu can be re-read, and add send_init_msg = false so that re-reading does not ask the owner to populate the popup a second time. * menu_widget: add native_content_dirty + resync_from_native(), which rebuilds the children from the current native menu, and run it at the start of update() so the new content is laid out in the same frame. * hooks: mark the matching submenu dirty on any late mutation, hopping to the render loop thread first since the widget tree is not thread-safe. Also log the contents of a native submenu when it is built and when it is re-synced, which makes this class of problem diagnosable from debug.log alone. --- src/shell/contextmenu/contextmenu.cc | 30 +++++++++++++++++-- src/shell/contextmenu/contextmenu.h | 18 ++++++++---- src/shell/contextmenu/hooks.cc | 36 +++++++++++++++++++++++ src/shell/contextmenu/menu_widget.cc | 44 ++++++++++++++++++++++++++++ src/shell/contextmenu/menu_widget.h | 6 ++++ 5 files changed, 126 insertions(+), 8 deletions(-) diff --git a/src/shell/contextmenu/contextmenu.cc b/src/shell/contextmenu/contextmenu.cc index 3136b172..708df75c 100644 --- a/src/shell/contextmenu/contextmenu.cc +++ b/src/shell/contextmenu/contextmenu.cc @@ -136,7 +136,7 @@ std::vector extract_hotkeys(const std::string &name) { menu menu::construct_with_hmenu( HMENU hMenu, HWND hWnd, bool is_top, std::function HandleMenuMsg, - LPARAM init_popup_lparam) { + LPARAM init_popup_lparam, bool send_init_msg) { menu m; if (!HandleMenuMsg) @@ -144,8 +144,10 @@ menu menu::construct_with_hmenu( SendMessageW(hWnd, message, wParam, lParam); }; - HandleMenuMsg(WM_INITMENUPOPUP, reinterpret_cast(hMenu), - init_popup_lparam); + if (send_init_msg) { + HandleMenuMsg(WM_INITMENUPOPUP, reinterpret_cast(hMenu), + init_popup_lparam); + } for (int i = 0; i < GetMenuItemCount(hMenu); i++) { menu_item item; wchar_t buffer[256]; @@ -179,7 +181,12 @@ menu menu::construct_with_hmenu( if (info.hSubMenu) { auto main_thread_id = GetCurrentThreadId(); int submenu_pos = i; + // Only for the diagnostic below; `buffer` must not be captured. + auto owner_name = wstring_to_utf8(strip_extra_infos(buffer)); item.submenu = [=](std::shared_ptr mw) { + spdlog::info("Initialising native submenu '{}' (hMenu={}, " + "parent_pos={})", + owner_name, (void *)info.hSubMenu, submenu_pos); auto task = [&]() { mw->init_from_data(menu::construct_with_hmenu( info.hSubMenu, hWnd, false, HandleMenuMsg, @@ -293,6 +300,23 @@ menu menu::construct_with_hmenu( m.parent_window = hWnd; m.native_handle = hMenu; m.is_top_level = is_top; + m.handle_menu_msg = HandleMenuMsg; + m.init_popup_lparam = init_popup_lparam; + + if (!is_top) { + std::string names; + for (size_t i = 0; i < m.items.size() && i < 6; i++) { + if (!names.empty()) + names += " | "; + if (m.items[i].type == menu_item::type::spacer) + names += ""; + else + names += m.items[i].name.value_or(""); + } + spdlog::info("Native submenu contents (hMenu={}, items={}): {}", + (void *)hMenu, m.items.size(), names); + } + return m; } } // namespace mb_shell diff --git a/src/shell/contextmenu/contextmenu.h b/src/shell/contextmenu/contextmenu.h index ce76570e..f7828313 100644 --- a/src/shell/contextmenu/contextmenu.h +++ b/src/shell/contextmenu/contextmenu.h @@ -19,11 +19,19 @@ struct menu { void *native_handle = nullptr; bool is_top_level = false; - static menu - construct_with_hmenu(HMENU hMenu, HWND hWnd, bool is_top = true, - std::function - HandleMenuMsg = {}, - LPARAM init_popup_lparam = 0xFFFFFFFF); + // Kept so a deferred submenu can be re-read later: shell popups such as + // "Send To" keep inserting items into their HMENU after we have already + // built the widget for it. + std::function handle_menu_msg; + LPARAM init_popup_lparam = 0xFFFFFFFF; + + // send_init_msg = false re-reads the current contents of hMenu without + // sending WM_INITMENUPOPUP again, so the menu owner is not asked to + // populate (and therefore duplicate) the menu a second time. + static menu construct_with_hmenu( + HMENU hMenu, HWND hWnd, bool is_top = true, + std::function HandleMenuMsg = {}, + LPARAM init_popup_lparam = 0xFFFFFFFF, bool send_init_msg = true); }; std::optional diff --git a/src/shell/contextmenu/hooks.cc b/src/shell/contextmenu/hooks.cc index 41770cb6..dd1acaa3 100644 --- a/src/shell/contextmenu/hooks.cc +++ b/src/shell/contextmenu/hooks.cc @@ -323,6 +323,38 @@ void sync_native_menu_item_update(HMENU hMenu, UINT item, BOOL fByPosition, }, true); } + +// Shell popups such as "Send To" are populated lazily: their owner keeps +// inserting items into the HMENU after we already built the Breeze widget for +// them. Mark the matching submenu so the render loop re-reads it (see +// menu_widget::resync_from_native). The widget tree may only be touched from +// the render loop thread, so hop over there first. +void schedule_submenu_resync(HMENU hMenu) { + if (!current_live_menu()) { + return; + } + + auto render = mb_shell::menu_render::current; + if (!render || !(*render) || !(*render)->rt) { + return; + } + + (*render)->rt->post_loop_thread_task( + [hMenu]() { + auto root = current_root_menu_widget(); + if (!root) { + return; + } + + auto target = find_menu_widget_by_handle(root, hMenu); + if (!target || target->menu_data.is_top_level) { + return; + } + + target->native_content_dirty = true; + }, + true); +} } // namespace #define WARN_LATE_MENU_MUTATION(API_NAME, HMENU_VALUE, FMT, ...) \ @@ -332,6 +364,10 @@ void sync_native_menu_item_update(HMENU hMenu, UINT item, BOOL fByPosition, " (current_menu={}, hMenu={}, " FMT ")", \ current_live_menu(), (void *)(HMENU_VALUE), \ __VA_ARGS__); \ + /* Any late change may also have added/removed items, so let the \ + submenu re-read its native popup. Coalesced by the per-frame \ + native_content_dirty flag. */ \ + schedule_submenu_resync(HMENU_VALUE); \ } \ } while (false) diff --git a/src/shell/contextmenu/menu_widget.cc b/src/shell/contextmenu/menu_widget.cc index db5b5f78..9c12f7b7 100644 --- a/src/shell/contextmenu/menu_widget.cc +++ b/src/shell/contextmenu/menu_widget.cc @@ -347,6 +347,13 @@ void mb_shell::menu_widget::arm_background_animation( } void mb_shell::menu_widget::update(ui::update_context &ctx) { + // Do it before laying out the children so the new content is measured and + // painted in this same frame. + if (native_content_dirty) { + native_content_dirty = false; + resync_from_native(); + } + if (dying_time) { if (dying_time.changed() && is_top_level_menu) { y->animate_to(*y - 10); @@ -964,6 +971,43 @@ void mb_shell::menu_widget::init_from_data(menu menu_data) { update_icon_width(); this->menu_data = menu_data; } + +void mb_shell::menu_widget::resync_from_native() { + if (is_top_level_menu || !menu_data.native_handle) + return; + + auto hMenu = (HMENU)menu_data.native_handle; + auto hWnd = (HWND)menu_data.parent_window; + + // The owner has already populated the popup, so read it without sending + // WM_INITMENUPOPUP again (that would ask it to add everything twice). + auto fresh = menu::construct_with_hmenu(hMenu, hWnd, false, + menu_data.handle_menu_msg, + menu_data.init_popup_lparam, + /*send_init_msg=*/false); + + if (fresh.items.empty()) + return; + + spdlog::info( + "Re-syncing deferred native submenu (hMenu={}, items {} -> {})", + (void *)hMenu, children.size(), fresh.items.size()); + + for (auto &child : children) { + if (auto item = child->downcast()) + item->hide_submenu(); + } + + if (current_submenu) { + current_submenu->close(); + current_submenu = nullptr; + } + rendering_submenus.clear(); + children.clear(); + + init_from_data(fresh); + needs_repaint = true; +} void mb_shell::menu_widget::update_icon_width() { bool has_icon = std::ranges::any_of(children, [](auto &item) { if (!item->template downcast()) diff --git a/src/shell/contextmenu/menu_widget.h b/src/shell/contextmenu/menu_widget.h index f73dc990..cb273e04 100644 --- a/src/shell/contextmenu/menu_widget.h +++ b/src/shell/contextmenu/menu_widget.h @@ -120,6 +120,12 @@ struct menu_widget : public ui::flex_widget { bool bg_appear_initialized = false; std::optional bg_start_rect; void init_from_data(menu menu_data); + + // Set when the native popup behind this submenu changed after we built it. + // Shell popups such as "Send To" are filled in lazily, so we have to + // re-read them once their owner has finished adding items. + bool native_content_dirty = false; + void resync_from_native(); void arm_background_animation( std::optional initial_rect = std::nullopt); bool animate_appear_started = false; From b5e8d1d931fb880cfc5b21d828ea447a804ee1d9 Mon Sep 17 00:00:00 2001 From: Z2549 <1211940555@qq.com> Date: Tue, 29 Sep 2026 21:15:09 +0800 Subject: [PATCH 2/2] fix(shell): stop forcing CMF_EXTENDEDVERBS via the shell32 patch The shell32 part of fix_win11_menu rewrites mov ecx, 0x10 ; VK_SHIFT call [GetKeyState] into a constant `mov rax, 0xffff`, i.e. it makes the shell believe the SHIFT key is held down forever. The code right after that call is mov ecx, edi bts ecx, 8 ; ecx |= 0x100 (CMF_EXTENDEDVERBS) cmp si, ax cmovle ecx, edi ; SHIFT not held -> drop 0x100 so the patch forces CMF_EXTENDEDVERBS (0x100) onto every context menu. Explorer's "Send To" extension is driven by exactly that flag: it then enumerates every first-level folder under %USERPROFILE% and keeps appending them to the submenu until its command-ID range runs out. That is what issues #212, #240 and #323 report ("Send To lists all folders in the user profile", "extra items injected everywhere"). The ExplorerFrame patch above - the one keyed on both VK_SHIFT and VK_F10 - is what actually makes Explorer use the classic HMENU context menu. The shell32 patch only adds CMF_EXTENDEDVERBS and contributes nothing to that goal, so gate it behind a new `patch_shell32_dll` option and leave it disabled by default. Verified locally on Windows 11 26100 by restoring the original 12 bytes at shell32 RVA 0x2B212D in a live explorer.exe: the Send To submenu goes back to the real entries only, and no other context menu item disappears. --- resources/schema.json | 6 +++++ src/shell/config.h | 6 +++++ src/shell/fix_win11_menu.cc | 49 +++++++++++++++++++++++-------------- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/resources/schema.json b/resources/schema.json index fadd228c..16a34b97 100644 --- a/resources/schema.json +++ b/resources/schema.json @@ -384,6 +384,12 @@ "type": "boolean", "default": true }, + "patch_shell32_dll": { + "title": "Patch shell32.dll SHIFT Key Check", + "description": "Forces CMF_EXTENDEDVERBS onto every context menu by making Explorer believe SHIFT is held down. This makes the Send To submenu enumerate every folder under the user profile, so it is disabled by default.", + "type": "boolean", + "default": false + }, "search_large_dwItemData_range": { "title": "Search Larger Range of DWItemData", "description": "Search for a larger range of DWItemData", diff --git a/src/shell/config.h b/src/shell/config.h index 141c0350..b20db24e 100644 --- a/src/shell/config.h +++ b/src/shell/config.h @@ -119,6 +119,12 @@ struct config { bool hotkeys = true; bool show_settings_button = true; bool patch_explorerframe_dll = true; + // Patching this shell32 SHIFT-key check makes Explorer believe SHIFT + // is held down, which forces CMF_EXTENDEDVERBS onto every menu. + // That makes the "Send To" submenu enumerate every folder under + // %USERPROFILE% (#212 / #240 / #323) while contributing nothing + // else, so it stays off by default. + bool patch_shell32_dll = false; // debug purpose only bool search_large_dwItemData_range = false; diff --git a/src/shell/fix_win11_menu.cc b/src/shell/fix_win11_menu.cc index b50fdb15..cbdf0732 100644 --- a/src/shell/fix_win11_menu.cc +++ b/src/shell/fix_win11_menu.cc @@ -162,25 +162,36 @@ void mb_shell::fix_win11_menu::install() { return false; }; - if (auto shell32 = proc->module("shell32.dll")) { - // mov ecx, 10 - // call GetKeyState/GetAsyncKeyState - auto disasm = shell32.value()->section(".text")->disassembly(); - - // the function to determine if show win10 menu or win11 menu - // calls SetMessageExtraInfo, so we use it as a hint - for (auto &ins : disasm) { - if (imported_call_target(ins) != extraInfo) - continue; - - if (patch_key_state_check( - ins.ptr() - .find_upwards({0xCC, 0xCC, 0xCC, 0xCC, 0xCC}) - ->range_size(0xB50) - .disassembly(), - 0x10)) { - spdlog::info("Patched shell32.dll for win11 menu fix"); - break; + // Patching the shell32 SHIFT-key check forces + // CMF_EXTENDEDVERBS onto every menu, which makes the "Send To" + // submenu enumerate every folder under %USERPROFILE% + // (#212 / #240 / #323). The ExplorerFrame patch above is what + // actually selects the classic HMENU menu, so this stays off + // unless a setup really needs it. + if (config::current->context_menu.patch_shell32_dll) { + if (auto shell32 = proc->module("shell32.dll")) { + // mov ecx, 10 + // call GetKeyState/GetAsyncKeyState + auto disasm = + shell32.value()->section(".text")->disassembly(); + + // the function to determine if show win10 menu or win11 + // menu calls SetMessageExtraInfo, so we use it as a hint + for (auto &ins : disasm) { + if (imported_call_target(ins) != extraInfo) + continue; + + if (patch_key_state_check( + ins.ptr() + .find_upwards( + {0xCC, 0xCC, 0xCC, 0xCC, 0xCC}) + ->range_size(0xB50) + .disassembly(), + 0x10)) { + spdlog::info( + "Patched shell32.dll for win11 menu fix"); + break; + } } } }