macOS 全键盘导航
动机
我不是键盘狂热者,只是个人是比较偏向于全键盘操作,至于鼠标键盘之间的效率问题,某些方面讲也是仁者见仁,智者见智的,推荐一下这篇文章: 使用鼠标可以提升编程效率吗? | NIL。我认为键盘操作比起鼠标操作更能容忍高延迟的屏幕显示,也认可光标到达目标控件前的移动距离确实低效。不过,即便如今键盘操作能覆盖我八九成的工作场景,鼠标操作仍作为一种手段的方法而时常故意为之。
对我而言最大的困扰其实是键鼠的切换成本。外接键盘鼠标的话,一次挪动手掌是需要肘与肩这样的大关节参与,而日常使用中整个操作序列难以避免的要散布着几个鼠标操作,非常难受。特别是 macOS 下,默认的应用/窗口切换行为有时还是很让人迷惑,难以避免要用光标辅助一下。
当然做到全键盘操作,像聚焦(spotlight)、Alfred 之类工具是必不可少,我认为有点复杂度的生产工具都需要提供功能模糊搜索(fuzzy searching)甚至 transient.el 这类的功能。但像这样的工具配置还有快捷键相关的就不在这文章讨论之类,这篇文章主要分享我如何用键盘覆盖鼠标大部分场景下导航操作(Navigation)的经验。
应用内导航
一般化
很大一部分 GUI Apps 对全键盘使用并不友好,快捷键时常只能覆盖部分场景,更别谈记忆快捷键的成本了,记住的只能是日常通用操作或天天使用的生产力工具,当然像 cheatsheet 之类的辅助工具可能有所帮助。但更好的是一套一般化的导航方案,比如说遍历窗口的所有可交互元素,并为其编号,通过键盘输入记号与其交互。
vimac 就是类似的应用。它通过无障碍 API (Accessibility API),去遍历当前窗口的元素 1,采用 DFS 遍历为控件编上快捷键。触发的时候把光标移动到控件位置,并执行相应操作。
除了左键单击外还支持以下按键行为 2:
- 右键,Shift
- 双击,Command
- 移动光标,Option
vimac 称之为 Hint 模式,默认按住空格键可以触发。另外 Hint 模式还遍历了以下区域的控件:
- 通知中心(notificationcenterui)
- 右侧菜单(extrasMenuBar)
- App 菜单(menuBar)
另外,名字有 vi
, 自然少不了 hjkl
。vimac 称之为 Scroll 模式。
Scroll 模式同样会遍历当前窗口的所有子控件,并找到所有如下元素的可滚动控件3:
- AXScrollArea
然后将焦点定位到第一个 ScrollArea 4,可按 ⇥(Tab) 能切换到不同 ScrollArea
滚动是通过模拟鼠标滚轮实现的,比如 gg
, G
滚动到顶部/底部,就是执行滚动 Int16.max
个像素。
let event = CGEvent.init(scrollWheelEvent2Source: nil, units: .pixel, wheelCount: 2, wheel1: yAxis, wheel2: xAxis, wheel3: 0)!
vimac 的关键功能是通过无障碍 API 实现的,vimac 只能在支持无障碍 API 的应用上生效,好在原生控件是默认支持的,大部分场景下都能起作用。一些 App 需要特殊支持:
- chromium,Accessibility Technical Documentation
- electron,Accessibility | Electron
Chrome
浏览器需要单独拎出来说一说,重点当然是推荐 vimium 这扩展。vimac 相当于简化版的 vimium. vimium 的交互对象不限于控件(超链接),还能触达文本,可惜对文本的操作只有复制如果能支持右键菜单那就更好了。总之能熟悉 vi 的话用起来应该是得心应手的,按 ?
也能看到所有操作指南。
vimium 基本上能覆盖我大部分需要使用鼠标的场景。特别需要提一下的是 macOS 上 Chrome 的一个坑,当 ⌘+l
将焦点定位到地址栏后,竟没有快捷键让焦点回到页面。这个问题曾折腾了我不少时间,最终找到除了用鼠标点一下外的几种花式办法:
- tab 一次次切换焦点,直到页面
- 地址栏执行
javascript:
-
⌘+f
搜索任意字符(但大概率会重置当前视口) ⌘+⌥+↑
第四点对应的快捷键是, ⌘ Command
+ ⌥ Option
+ ↑
/ ↓
。其可以在地址栏、标签栏、书签、页面间切换焦点,虽然这个按键组合不太友好,但相比其他办法还是可以接受的。这是最近我整理资料才发现的,相当于 Windows/Linux 下的 F6/Ctrl+F6
, 应该是后面版本才更新的。
不过我养成用 vimium omnibar 代替地址栏操作的习惯后,就摆脱这困扰了。相信定位到地址栏的目的 80% 是复制当前地址,可以直接用 yy
搞定。如果需要编辑地址则 ge
搞定。
vimium 覆盖不了的场景,频率最高的就是对于非 iframe
的其他可滚动元素,无法避免用鼠标点一下获得焦点,hjkl
才能对该滚动区域生效。至今还不能完美解决5,还是比较难受的,具体见 Make it possible to focus a scrollable div without using the mouse · Issue #425
另外,对于一些页面如果有快捷键冲突需要禁用 vimium, 比如 gmail, youtube… 那么 vimac 也是能派上用场的。
窗口导航
macOS 上的窗口导航可是个大坑。如果把 ⌘-⇥(Tab)
当成窗口切换去用,肯定会遇到不少难以理解的迷惑行为。比如 WeChat 常常是唤不起窗口的。实际上 ⌘-⇥
切换的是应用而不是窗口,而切换应用时部分状态的窗口是不会被带起的。
这个坑的一大因素就是用户需要面对的窗口(或应用)状态繁多:
- 正常
- 全屏
- 隐藏(
C-h
隐藏当前应用的所有窗口) - 最小化
- 无窗口
切换应用时,能带起的是正常状态、全屏状态和隐藏状态下的窗口。但不一致的是 ⌘-`` 只能循环切换正常状态下的窗口,或在切换界面按下
↓` 进入 App Exposé 也只能看到正常状态下的窗口。如果应用没有窗口或只有最小化的窗口也是能被切换,但切换后没有任何反应,也十分迷惑。
但当使用场景进入到多屏幕多空间,同时应用拥有多个全屏窗口或者混合全屏窗口和普通窗口。切换行为才是最让人迷惑的,我到现在也没搞懂它的逻辑,反正我在屏幕 A 的应用 A 的普通窗口 A 切换同屏幕的其他应用窗口后,就再也无法通过 ⌘-⇥
回到这个窗口,焦点会落在屏幕 C 的应用 A 的全屏窗口上。
在这里我强烈推荐使用 AltTab 代替 ⌘-⇥
,AltTab 的切换对象就是窗口,非常直观。同时支持所有状态的窗口,包括无窗口的应用,切换的行为相当于在 Dock 栏点击应用图标,会重新打开被关闭的窗口。对各种窗口状态与屏幕、空间都整理的比较清晰,可以微调过滤不要的窗口状态,同时在能在切换器界面显示窗口的状态与所在的空间。这个可能意义不大,但仍要截个图说明一下,因为我一开始也搞不清楚这些小图标的意思。
建议将 Shortcut 2 设置为 Active app 并绑定给 `⌘-`` 。全面替换掉系统的默认行为 ,用了一段时间基本没有什么不适,可能有一些 Bug 但利大于弊。
另外 lwouis/alt-tab-macos 也是一个相当活跃的项目,作者还在一直保持更新。
屏幕间导航
屏幕间导航并不是指切换显示设置里的主屏幕,不过说起这个主屏幕主要是要提一下另外一个迷惑行为,一般认为 Dock 栏和 ⌘-⇥(Tab)
选择器是出现在主屏幕。实际上 Dock 栏和⌘-⇥
选择器出现的屏幕是,主屏幕沿着 Dock 栏方位继续延伸直到到达整体屏幕空间的边缘,这时所在的屏幕才是 Dock 栏和⌘-⇥
选择器出现的屏幕,更像是真正的主屏幕,这个屏幕在 AltTab 称之为 Screen including menubar 。 虽然有点迷惑,但仔细想想也是合理的,毕竟 Dock 是需要边缘激活的。
重新回到主题上,那屏幕间导航是什么意思?控制屏幕焦点的主体不就是眼睛吗,切换屏幕不就是转动一下眼球或脖子的功夫?话是这样说没错,但是我认为 macOS 是有一个当前全屏的概念的,这个屏幕并不是拥有当前输入焦点的窗口所在的屏幕,AltTab 称之为 Active screen ,而是当前鼠标指针所在的屏幕,AltTab 称之 Screen including mouse ,姑且称为 鼠标屏幕 。
为什么需要定义这样一个概念,很遗憾这是 Mission Control 生效的屏幕,同时也是唤起 Spotlight 的屏幕。为了通过快捷键C-→/C-←
切换期望的屏幕的空间(Space)6,需要先切换当前的鼠标屏幕。屏幕间导航也就是指这个。
所以几年前我写了一个小工具 hopscotch, 来实现通过快捷键切换鼠标屏幕。更早之前用的是 @virushuo 的 CatchMouse 来解决这个这个痛点,不过这个工具只能通过编号切换显示器,对于这种场景还是有点别扭。更符合直觉的上一个或下一个循环切换,就趁着 swiftui 刚出来,用它撸了个这个小工具,边学边做,做的比较粗糙,也就自己用而已。
最近写这文章,就顺便把 AltTab 的活跃屏幕(Active scree)也搬运过来,实现快捷键将鼠标屏幕切换到活跃屏幕。这样,通过 ⌘-⇥
切换键盘输入焦点后,就能一个快捷键把 Mission Control 的焦点也带过来,十分符合操作逻辑。只可惜找不到不用无障碍 API 获取其它应用窗口的坐标和尺寸的办法,所以要使用这个功能只能赋予 Accessibility 权限。
用了 AltTab 后,其实 hopscotch 大部分作用场景是能被 AltTab 替代。主要区别是 Mission Control 是基于空间切换的,而 AltTab 是基于窗口切换的,目的都是为了切换到目标窗口,显然 AltTab 更为直接,但俗话说技多不压身啊,多个提供一种方法也没什么不好的。
Footnotes
1 见 vimac/TraverseGenericElementService.swift#L40
2 见 vimac/HintModeController.swift#L32
3 见 vimac/QueryScrollAreasService#L35
4 见 vimac/ScrollModeActiveViewController#L37
5 其实代码是有对滚动元素进行判断的,见 vimium/link_hints.js#973, vimium/scroller.js#L103
6 macOS 上的空间相当于虚拟桌面的意思,见在 Mac 上的多个空间中工作。