diff --git a/resources/schema.json b/resources/schema.json index fadd228..16a34b9 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 141c035..b20db24 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/contextmenu/contextmenu.cc b/src/shell/contextmenu/contextmenu.cc index 3136b17..708df75 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 ce76570..f782831 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 41770cb..dd1acaa 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 db5b5f7..9c12f7b 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 f73dc99..cb273e0 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; diff --git a/src/shell/fix_win11_menu.cc b/src/shell/fix_win11_menu.cc index b50fdb1..cbdf073 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; + } } } }