重排 Overlay 的右键菜单:行数少了,图标也不要了
在 Overlay 里,对着网页空白处点右键,之前会弹出 21 行。最上面五行是后退、前进、刷新、查找、全选,这是你点右键时大概率想要的东西。下面十几行全是「这个标签页」的设置:分屏、复制地址、在浏览器打开、清除 Cookie、开发者工具、独立会话、显示时自动刷新、总是在浏览器打开链接、关闭通知、静音、User Agent、一排颜色,最后是关闭(开发者工具那项正式版默认不显示)。右键一张带链接的图片是 27 行,将近 760pt 高,差不多占了 13 寸 MacBook Air 屏幕高度的八成。
那十几项大多是设一次就不再碰的东西,却每一次右键都摆在眼前。这件事我拖了很久,因为当初是我自己定的「Tab 设置保持平铺」。这次终于推翻了。
先把所有菜单摆出来
动手之前,我把 Overlay 里所有会弹右键菜单的地方列了一遍,一共 15 处。Tab 栏上有网页标签、内置页标签、分组卡片三种;网页里按光标底下是什么分成空白处、链接、图片、带链接的图片、选中的文字、输入框、输入框里的选区,还有一种「没拿到现场」(PDF,或者页面在我们之前把右键吞掉了);弹出窗口两种;再加上固定在侧栏的扩展图标和托盘那份 App 菜单。
光在脑子里想,排不清楚。我做了个网页原型,按 App 里真实的规则把每一张菜单按 1:1 的尺寸画出来,几个候选方案并排,底下一张表列出每个场景、每个方案各是几行、多高。比到后来,我干脆让它能拖:每一项可以上下拖、拖进一行图标、拖进子菜单、拖进「隐藏」,菜单图标能整体关掉,也能单独关某一项。拖完导出一段文字,照着改代码。
自己拖过一遍和看方案对比,差别挺大。有几处是拖的时候才发现的,比如「复制标签页地址」和「在浏览器打开」其实说的就是这一页,应该和查找、全选放在一起,而不是混在 Tab 设置里。
一条不能破的约束
Overlay 有个无边框模式,按 ⌘B 把侧栏和地址栏都藏起来,只剩网页。这时候网页上的右键菜单是唯一能碰到 Tab 功能的地方。
所以 Tab 功能在网页菜单里只能收起来,不能删。无边框时菜单的第一项是「退出无边框模式」;「复制标签页地址」和「在浏览器打开」在输入框的右键菜单里也给。为了不让以后哪次改动把这条破坏掉,我写了个单测,把两种宿主(面板里的标签、弹出窗口)和九个场景开关全部组合一遍,一共 1024 种,断言面板标签的菜单里这些项一个都不少。
现在的样子
右键网页空白处,从上到下是:
后退、前进、刷新;复制标签页地址、在浏览器打开、查找、全选;同组时的刷新分组、取消分组;扩展加的项;一行图标;「更多 ▸」;关闭。
一行图标里是分屏,一道竖线,然后是显示地址栏、独立会话、显示时自动刷新、总是在浏览器打开链接、关闭通知、静音(设置里配了代理时还有代理)。「更多」里是 User Agent、清除 Cookie 和缓存、开发者工具和颜色。
同样的场景(标签在一个分组里、装了一个会往菜单里加项的翻译扩展),21 行变成 13 行;带链接的图片从 27 行变成 20 行。Tab 栏上的菜单只从 16 行减到 14 行,因为我顺手加了后退和前进:它们作用于你右键的那个标签,不一定是正在看的那个。链接菜单里也多了一项「分屏打开链接」,新页面开在当前这一格右边。
为什么去掉图标
原来每一项前面都有个图标,是从早期 Tauri 版本的图标一一照搬过来的。真看下来,大部分图标没在传达什么。清除 Cookie 是一把刷子,独立会话是一个带问号的人像,你得先读文字才知道它指什么。图标还有个代价:NSMenu 只要有一项带图,整列文字都会让出图标的位置,每一行往右挪一截。
我把我们自己设的图标都去掉之后,截图里剪切、复制、粘贴、全选几项还带着图标,别的没有,比之前更乱。一查,是 macOS 26 自己加的:只要菜单项的动作是 copy:、cut:、paste:、selectAll: 这类标准 selector,AppKit 就自动挂一个 SF Symbol 上去,跟标题无关。我写了个小程序验证,把一项的标题写成 "Whatever",动作设成 copy:,照样配上复制的图标。撤销和重做没有图标,是因为它们本来就没用标准的 undo::用标准 selector 的话,AppKit 会把标题改写成跟系统语言走的 "Undo",中文界面里冒出一个英文词。
官方的关闭办法是 NSMenuItem.preferredImageVisibility,可那是 macOS 27 才有的。在 26 上能用的是 NSMenuEnableActionImages 这个 defaults 键,设成 NO 就不再自动配图。要在任何菜单建出来之前设置,我把它放在了 main.swift 里 NSApplication.shared 之前;用 register(defaults:) 注册就生效,不用写进用户的偏好文件。顺带一提,macOS 27 SDK 里 NSMenuItem.image 的注释写着,AppKit 以后会 "typically hide images"。苹果自己也在往这个方向走。
最后是全 App 的菜单都不配图标:右键菜单、托盘菜单、固定扩展的菜单、菜单栏的「编辑」菜单。开关只有一处,想恢复改一个布尔值就行。
例外是网页扩展加的项,它们保留扩展自己的图标。你在菜单里认「这是哪个扩展的」主要靠那个图标,去掉反而难找。代价是菜单里只要有一个扩展项,AppKit 还是会给整列文字留出图标位,我没办法单独控制。
为什么又有一行图标
去掉图标之后再塞一行图标,看起来有点矛盾。区别在于这一行里,图标就是开关本身。六个开关都是「开了就一直开着」的状态,收进子菜单就看不到现在开没开;一行图标,开着的那几个用强调色,扫一眼就知道。名字靠悬停提示。
在 NSMenu 里放一行图标
第一版是我自己画的:一个 NSMenuItem.view,和颜色那一排同一个做法。后来发现方向键会直接跳过这一行。原因很简单,挂自定义视图的菜单项没有动作,AppKit 把它当成不可用,键盘导航就绕过去了。
可以给它补个动作让键盘停进来,再自己处理左右键、回车、焦点样式、VoiceOver,但画出来永远和系统菜单不太一样。我换成了 macOS 14 加进来的原生横排菜单:把一个子菜单的 presentationStyle 设成 .palette,它的项就横着排在父菜单那一行里。访达右键菜单里那排标签颜色就是这个。
换之前我得先确认它的键盘行为。合成的鼠标事件不会让菜单真正进入跟踪状态,我之前在这上面被骗过一次,按它的结论改了两版都没用。后来发现,在一个加到 eventTracking 模式的计时器里用 NSApp.postEvent 往自己进程投键盘事件,能驱动真实的菜单跟踪。这样测下来:↓ 能停进这一行,←/→ 在格子之间走,回车触发;键盘停住的那一格是系统的蓝色方块,开着的项是强调色,VoiceOver 也是系统给的。
接下来是三个没想到的地方。
第一个是图标大小。直接给 SF Symbol,横排会按自己的规格放大,14pt 的符号画出来大约 26×20,比旁边的文字大一圈。给普通图片则会等比塞进一个 18×18 的框里。所以我先把 14pt 的符号居中画在一张 18×18 的透明底图上再交给它,缩放比例正好是 1。
第二个是竖线。横排里放 NSMenuItem.separator() 只会留一段 32pt 的空白,没有线。放一个不可用的格子、图片画一道竖线,能显示,键盘也会跳过,可格子宽度是系统定死的 31.75pt。我试了 4pt 宽的图、很宽的图、带 alignmentRect 的图、9pt 宽的自定义视图、系统的竖线符号,格子一样宽,竖线两侧空得很难看。最后的做法有点野:菜单打开后,在跟踪循环里每 5 毫秒找一次那一格的视图,找到就往那一行里加一根 1pt 的线,放在两格交界处,菜单打开后二十来毫秒画上。颜色用 separatorColor,我量了像素,亮度 0.685,菜单自带的横线是 0.682。只用公开接口,找不到就不画,不影响功能。
第三个是悬停名称。系统 tooltip 在 Overlay 这种非激活面板里经常不出来,键盘走过去时更是没有。我自己的 tooltip 有 150 毫秒延迟,靠 Task.sleep 计时,可菜单开着的时候主队列不派发,这个延迟永远等不完。现在是在横排菜单的 willHighlight 回调里(鼠标和键盘都会触发)找到那一格图标所在的 NSImageView,换成屏幕坐标,同步把提示画在它上方;提示窗口的层级抬到菜单之上一级,不然会被菜单盖住。
局限
Overlay 用的是系统的 NSMenu,输入框右键菜单最底下的「自动填充」和「服务」是系统在绘制时加上的,不在 menu.items 里,我去不掉。苹果自己的文本编辑也一样。要彻底去掉只能把整套菜单换成自己画的,那样能完全控制样式,丢掉的是键盘导航、辅助功能和暗色适配,对 Overlay 不划算。去掉图标之后,「服务」那一项还带不带系统图标,我也还没在机器上确认。
竖线是往系统视图里补画的。哪天 macOS 改了这一行的内部结构,最坏的结果是线不见了,点击和键盘不受影响。菜单出现的头一两帧也可能还没有这根线。
这些都还没在 macOS 27 上跑过。一行图标也需要学一下才认得全,提示能帮一点,但第一次用的人还是得悬停看名字。