Skip to content

fix(shell): 修复「发送到」子菜单空白与混入用户主目录项 - #390

Closed
Z2549 wants to merge 2 commits into
std-microblock:masterfrom
Z2549:fix/native-submenu-extended-verbs
Closed

Z2549 wants to merge 2 commits into
std-microblock:masterfrom
Z2549:fix/native-submenu-extended-verbs

Conversation

@Z2549

@Z2549 Z2549 commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

修复「发送到」子菜单的两个独立缺陷。

1. 展开「发送到」时子菜单空白(#378)

menu::construct_with_hmenu() 在发出 WM_INITMENUPOPUP 之后立刻用
GetMenuItemCount() 枚举子菜单内容。但 Shell 对「发送到」「打开方式」这类子菜单是
延迟填充的 —— 消息返回时它还是空的,于是渲染出一个空白子菜单。

修复方式:

  • menu 新增 handle_menu_msg / init_popup_lparam 成员,construct_with_hmenu()
    增加 send_init_msg 参数,允许「只读当前内容、不再转发」;
  • menu_widget 新增 native_content_dirty 标记与 resync_from_native(),在
    update() 开头重新读取原生 HMENU;
  • hooks.cc 新增 schedule_submenu_resync(HMENU),挂进 WARN_LATE_MENU_MUTATION
    宏(覆盖 13 个 hook),经 post_loop_thread_task 投递,使子菜单在 Shell 填完之后
    被重读一次。

2. 「发送到」里混进用户主目录下的所有文件夹(#212 / #240 / #323)

fix_win11_menu 会给 explorer 进程里的 shell32.dll 打一处内存补丁,把

mov ecx, 0x10            ; VK_SHIFT
call [GetKeyState]

整段替换成常量 mov rax, 0xffff —— 也就是让 Shell 永远认为 SHIFT 处于按下状态。
紧随其后的原始代码是:

mov ecx, edi
bts ecx, 8               ; ecx |= 0x100  (CMF_EXTENDEDVERBS)
cmp si, ax
cmovle ecx, edi          ; SHIFT 未按下 -> 去掉 0x100

于是这个补丁会让每一个上下文菜单都带上 CMF_EXTENDEDVERBS(0x100)。而 Explorer 的
「发送到」扩展正是被这个标志驱动的:它会把 %USERPROFILE% 下所有一级目录(含隐藏目录)
追加进「发送到」子菜单,一直填到 Explorer 分配给它的命令 ID 区间用尽为止。

实测数据(Windows 11 26100,shell32 26100.4768):

  • 子菜单前 5 项是真实「发送到」项(wID 31006..31010),随后 41 项是用户主目录条目,
    wID 31011..31051 连续递增、正好用尽;
  • 转发 WM_INITMENUPOPUP 之前 GetMenuItemCount() 就已经是 46 —— 说明这些多余项与
    Breeze 自己的转发无关;
  • 把本机 explorer 进程里该处 12 字节还原成原始指令后,「发送到」立刻恢复成真实的 5 项,
    并且其它菜单项一个都没少(「以管理员身份运行」「打开文件所在的位置」等扩展动词仍在)。

既然 CMF_EXTENDEDVERBS 对这个补丁本身的目标(让 Explorer 走经典 HMENU 菜单)毫无贡献
—— 真正做到这一点的是 ExplorerFrame.dll 那一处(它同时判断 VK_SHIFT 和 VK_F10,
逻辑精准,本 PR 保持不动)—— 就把 shell32 这处包进新的配置项
context_menu.patch_shell32_dll,默认关闭。

变更范围

8 个文件,+168 / −27:

resources/schema.json                |  6 +++++
src/shell/config.h                   |  6 +++++
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 +++++
src/shell/fix_win11_menu.cc          | 49 ++++++++++++++++++++++--------------

config.json 不需要任何改动即可生效(未写入的字段会落到新的默认值)。如需恢复旧行为,
在 context_menu 段写 "patch_shell32_dll": true。

验证

关于 CI

本 PR 只包含上述两处业务修复,不含任何为了让 fork 的 CI 跑起来而做的构建侧改动
(跳过未配置的 sentry secrets、绕开依赖漂移等)。若上游 master 的 CI 本身是红的,
与本 PR 无关。

Z2549 and others added 2 commits September 29, 2026 21:28
…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 std-microblock#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.
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 std-microblock#212, std-microblock#240 and std-microblock#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.
@std-microblock

Copy link
Copy Markdown
Owner

整段替换成常量 mov rax, 0xffff —— 也就是让 Shell 永远认为 SHIFT 处于按下状态。
紧随其后的原始代码是:

mov ecx, edi
bts ecx, 8 ; ecx |= 0x100 (CMF_EXTENDEDVERBS)
cmp si, ax
cmovle ecx, edi ; SHIFT 未按下 -> 去掉 0x100
于是这个补丁会让每一个上下文菜单都带上 CMF_EXTENDEDVERBS(0x100)。而 Explorer 的
「发送到」扩展正是被这个标志驱动的:它会把 %USERPROFILE% 下所有一级目录(含隐藏目录)
追加进「发送到」子菜单,一直填到 Explorer 分配给它的命令 ID 区间用尽为止。

ExplorerFrame.dll 的 patch 是用于侧边栏的,这个patch是用于正常菜单的,不能直接删掉啊

@std-microblock

Copy link
Copy Markdown
Owner

menu::construct_with_hmenu() 在发出 WM_INITMENUPOPUP 之后立刻用
GetMenuItemCount() 枚举子菜单内容。但 Shell 对「发送到」「打开方式」这类子菜单是
延迟填充的 —— 消息返回时它还是空的,于是渲染出一个空白子菜单。

现在应该已经有一个方式来补充新增的菜单了...? 我看看吧

@std-microblock

Copy link
Copy Markdown
Owner

这两个问题我自己看下吧,感谢

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants