用了一段时间之后,遇到一个很具体的问题:浏览器的窗口在桌面 3,我人在桌面 2,点 Touch Bar 上的浏览器图标,应该像点系统 Dock 一样切到桌面 3,而不是只把浏览器变成前台,自己还停在原地。

这件事看起来应该是系统自带的能力,实测却不是。这一篇记录怎么一步步查清楚,以及最后的解法。

先弄清楚系统到底做了什么

系统设置里有一项“切换到某个应用时,切换到包含该应用打开窗口的空间”,默认是开着的。但实测它只在用户亲手点 Dock 或者按 ⌘Tab 时生效。后台 App 用 openApplication 或者 activate 发起的激活,只会让目标 App 变成前台,桌面不动。

在我的机器上有 3 个桌面,浏览器的窗口在最后一个,当前在中间那个。我逐个方法试,结果如下:openApplication 和 activate,浏览器变成前台但桌面不动;只用私有接口把窗口设为前台并发送关键窗口事件,同样桌面不动;再加上辅助功能的 Raise,才真正切到了窗口所在的桌面,反过来也能切回来。

解法:设为前台加辅助功能 Raise

最终的做法和 AltTab 相同:当目标 App 在当前桌面没有窗口、但在别的桌面有窗口时,找到最靠前的那个窗口,先用 SkyLight 里的私有接口把它设为前台窗口,再用辅助功能的 AXRaise,系统随之切到那个桌面。找窗口时,最小化的窗口不属于任何桌面,会被跳过,交给普通的启动流程去还原。

整个流程放在后台队列里,辅助功能调用设置一秒超时,目标 App 卡死时不会拖住 Touch Bar。没有辅助功能权限,或者任何一步失败,都退回普通的切换,只是不切桌面。所以这个权限是可选的。

最难的一个坑:别的桌面上的窗口,标准接口看不到

辅助功能的标准接口只返回当前桌面上的窗口。我去查那个在桌面 3 的浏览器窗口,得到的是 0 个。解决办法还是参考 AltTab:用一个私有的构造函数,按元素编号 0 到 999 逐个构造辅助功能元素,再用另一个私有函数把元素对上窗口编号。实测大约 60 到 80 毫秒,点击时感觉不到延迟。

这也解释了为什么这类功能很少有人做:它同时依赖两个私有接口,加上一个很不直观的枚举技巧。

决策流程:什么时候走这条路

点击一个图标时,程序按这样的顺序判断:如果 App 没在运行,或者已经被隐藏,就走普通的启动或显示;否则按窗口所属的桌面编号,把它的候选窗口排好序,当前(或正在去的)桌面上的在前,别的桌面上的在后,最小化的窗口不属于任何桌面所以不算,选出第一个真实窗口,设为前台再 Raise;没有辅助功能权限,或者任何一步失败,就退回普通启动。这个判断最早是“先看当前桌面上有没有窗口,有就普通激活”,后来因为连点会失效,改成了现在这样,原因见第 08 篇。

测试方法:只在你没动键鼠的时候测

切换类测试有一个特殊的难处:它会真的改变你屏幕上的桌面和前台 App,而你的操作反过来又会干扰测试结果。有一次测出“切到了桌面 4、前台是另一个无关的 App”,和别的结果矛盾,原因是测试时我正在用电脑。

之后我给所有切换类测试加了一道门槛:用户停手 8 秒才开始,测试中一有键盘鼠标输入,这一轮就作废;每一步都切回原来的 App 和桌面。测试用的临时 App,也是专门编译的,不去碰正在使用的窗口。后来还发现,只看系统的“键鼠空闲时间”并不可靠,程序自己的动作也会把它清零,这个坑记在第 08 篇里。

这一篇的经验:先做实验,再动手实现

遇到“系统应该会做”的事,先做一个最小的实验,确认系统到底做不做,再决定要不要自己实现。如果一开始就假设 openApplication 会切桌面,我会在一个错误的前提上写很多代码;实际上几个小实验就把这条路排除了,剩下的才是真正需要写的部分。

用到的私有接口,以及缺了怎么办

这条路用到七个私有接口:SkyLight 里的获取连接、查询窗口所在桌面、把窗口设为前台进程和投递事件记录,以及获取进程序列号、按窗口编号取辅助功能元素、按编号构造元素。它们都在运行时解析,缺了任何一个,整条路就关闭,退回普通切换。