Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions resources/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions src/shell/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
30 changes: 27 additions & 3 deletions src/shell/contextmenu/contextmenu.cc
Original file line number Diff line number Diff line change
Expand Up @@ -136,16 +136,18 @@ std::vector<std::string> extract_hotkeys(const std::string &name) {
menu menu::construct_with_hmenu(
HMENU hMenu, HWND hWnd, bool is_top,
std::function<void(int, WPARAM, LPARAM)> HandleMenuMsg,
LPARAM init_popup_lparam) {
LPARAM init_popup_lparam, bool send_init_msg) {
menu m;

if (!HandleMenuMsg)
HandleMenuMsg = [=](int message, WPARAM wParam, LPARAM lParam) {
SendMessageW(hWnd, message, wParam, lParam);
};

HandleMenuMsg(WM_INITMENUPOPUP, reinterpret_cast<WPARAM>(hMenu),
init_popup_lparam);
if (send_init_msg) {
HandleMenuMsg(WM_INITMENUPOPUP, reinterpret_cast<WPARAM>(hMenu),
init_popup_lparam);
}
for (int i = 0; i < GetMenuItemCount(hMenu); i++) {
menu_item item;
wchar_t buffer[256];
Expand Down Expand Up @@ -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<menu_widget> 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,
Expand Down Expand Up @@ -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 += "<spacer>";
else
names += m.items[i].name.value_or("<unnamed>");
}
spdlog::info("Native submenu contents (hMenu={}, items={}): {}",
(void *)hMenu, m.items.size(), names);
}

return m;
}
} // namespace mb_shell
18 changes: 13 additions & 5 deletions src/shell/contextmenu/contextmenu.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<void(int, WPARAM, LPARAM)>
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<void(int, WPARAM, LPARAM)> 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<void(int, WPARAM, LPARAM)> HandleMenuMsg = {},
LPARAM init_popup_lparam = 0xFFFFFFFF, bool send_init_msg = true);
};

std::optional<int>
Expand Down
36 changes: 36 additions & 0 deletions src/shell/contextmenu/hooks.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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, ...) \
Expand All @@ -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)

Expand Down
44 changes: 44 additions & 0 deletions src/shell/contextmenu/menu_widget.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<menu_item_normal_widget>())
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<menu_item_normal_widget>())
Expand Down
6 changes: 6 additions & 0 deletions src/shell/contextmenu/menu_widget.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ struct menu_widget : public ui::flex_widget {
bool bg_appear_initialized = false;
std::optional<menu_animation_rect> 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<menu_animation_rect> initial_rect = std::nullopt);
bool animate_appear_started = false;
Expand Down
49 changes: 30 additions & 19 deletions src/shell/fix_win11_menu.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
}
Expand Down