Tray Collapse:把 GNOME 顶栏挤爆的托盘图标,折进一个可展开的抽屉
🛠️ 工具 开源项目 GNOME ubuntu 8

TL;DR:一个 GNOME Shell 42 扩展,把一堆 AppIndicator 托盘图标收进顶栏里一个可展开的「抽屉」,腾出空间、不再顶掉时钟。难点不在 UI,而在两件事:GNOME 根本没给你一个「托盘容器」可搬,得直接把图标的 actor 在面板和抽屉之间重新认父;以及对付那些每次启动都换一个随机 ID 的流氓应用,否则同一个软件会在设置里冒出一长串重复项。

起因:时钟被顶没了

GNOME 顶栏用久了,装的带托盘图标的应用一多,AppIndicator 图标就排成一长条,往中间的时钟挤。我有强迫症,看不得这个。想要的效果很简单:平时把不常点的图标收起来,顶栏只留一个小按钮,需要时点开抽屉再操作。 找了一圈没有称手的,于是写了 Tray Collapse

项目地址:https://github.com/fnidore/tray-collapse

核心难点:GNOME 不给你「托盘容器」

直觉上你会以为有个 trayBox 之类的容器,把它整个挪走就行。并没有。AppIndicator 的每个图标都是直接挂在 Main.panel.statusArea 下、最终落在面板右侧 _rightBox 里的独立 actor。所以折叠的本质是:把单个图标的 actor 从面板上摘下来,重新挂到我自己的抽屉容器里——也就是 GTK/Clutter 里所谓的 reparenting。

先找出所有 AppIndicator 图标(它们的 key 有统一前缀):

const statusArea = Main.panel.statusArea;
for (const key in statusArea) {
    if (!key.startsWith(APPINDICATOR_PREFIX))
        continue;
    const indicator = statusArea[key];
    // …决定这个 indicator 该进抽屉还是留顶栏…
}

搬移就是「从旧父节点摘下、加到新父节点」。进抽屉和回顶栏是一对镜像操作:

_moveIn(container) {
    const parent = container.get_parent();
    if (parent) parent.remove_child(container);   // 先从顶栏摘下
    this._drawerBox.add_child(container);          // 挂进抽屉
}
_moveOut(container) {
    this._detach(container);
    Main.panel._rightBox.insert_child_at_index(container, 0);  // 放回顶栏最左
}

关键点:搬的是同一个 actor,不是复制一份。所以图标的菜单、点击行为、动态更新全都原封不动——抽屉里的图标仍是个功能完整的面板按钮,而不是一张截图占位。这也是我坚持用 reparenting 而不是「自己画一排假图标再转发点击」的原因:后者要模拟整套菜单交互,几乎不可能做对。

抽屉的开关:一个胶囊按钮

顶栏只留一个胶囊按钮,点一下切换抽屉的可见性,顺便换个图标(收起/展开两个符号):

_toggle() {
    this._expanded = !this._expanded;
    this._drawerBox.visible = this._expanded;
    this._icon.gicon = this._expanded
        ? this._iconExpanded : this._iconCollapsed;
}

抽屉不展开时 visible = false不占顶栏横向空间——这正是「腾地方」的核心。

最大的坑:每次启动换 ID 的流氓图标

这是让我返工最多的地方。理想情况下每个应用有个稳定 ID,我记住「这个该收、那个该留」就行。但现实是,有些 AppIndicator 应用上报的 ID 形如 legacy:appname:1873452,后面那串数字每次启动都变。后果很难绷:你昨天把某个图标设成「收进抽屉」,今天重启它换了 ID,扩展不认识了,于是它又跑回顶栏;而设置页里则堆着一排「同一个应用、不同随机尾号」的幽灵条目。

解法是给 ID 做归一化——把随机尾号削掉,只认前面稳定的部分:

function canonicalId(id) {
    const m = /^legacy:(.+):\d+$/.exec(String(id));
    return m ? `legacy:${m[1]}` : String(id);
}

光归一化还不够,还得清理历史遗留的脏条目,但又不能误伤——有些应用确实会同时开好几个合法图标(互为 "peer",比如一个应用的多个实例)。所以清理时跳过仍然活着的、以及互为 peer 的,只删真正过期的,并把旧 ID 在用户的「收进抽屉」名单里改写成新 ID:

for (const [oldId, old] of [...map]) {
    if (liveIds.has(oldId) || entry.peers.has(oldId))
        continue;                       // 活着的、或互为 peer 的,留
    map.delete(oldId);                  // 只删过期的幽灵
    this._renameInList('collapsed-ids', oldId, it.id);  // 顺手迁移用户设置
}

这段逻辑看着不起眼,却是「重启后设置依然生效、设置页不堆垃圾」的关键。我前后改了好几版才把误删 peer、漏迁移设置这些边角填平。

设置怎么跨进程:known-indicators 这座桥

GNOME 扩展分两个进程:扩展本体跑在 gnome-shell 里,设置界面(prefs)是另起的进程。设置页要展示「你都见过哪些图标、各自标题是什么」,但它够不到 gnome-shell 里的实时状态。我的桥是 gsettings——扩展把见过的图标序列化成 JSON 存进一个 key,prefs 读它来渲染列表:

const arr = [...map].map(([id, v]) =>
    ({ id, title: v.title, peers: [...v.peers] }));
this._settings.set_string('known-indicators', JSON.stringify(arr));

整条数据流:

gnome-shell 进程                         prefs 进程(独立)
┌──────────────────────┐                ┌─────────────────────┐
│ 扫描 statusArea       │                │ 读 known-indicators  │
│ 归一化/去重 indicators │  gsettings     │ 渲染「逐图标」开关   │
│ 写 known-indicators ──┼──(JSON 字符串)─▶│ 用户勾选 → 写        │
│ 读 collapsed/pinned ◀─┼────────────────┤   collapsed/pinned   │
└──────────────────────┘                └─────────────────────┘

可调的配置键

全部行为通过 6 个 gsettings 键控制:

key 类型 默认 作用
default-collapse bool false 新出现的图标是否默认收进抽屉
pinned-ids string[] [] 强制常驻顶栏的图标 ID
collapsed-ids string[] [] 强制收进抽屉的图标 ID
known-indicators string(JSON) '[]' 扩展维护的「已知图标」记录,供设置页渲染
drawer-spacing int (0–40) 12 抽屉里图标的横向间距(px)
panel-hpadding int (0–24) 12 顶栏托盘图标的水平内边距(px),只影响 AppIndicator 图标

「逐图标控制」就是设置页把 known-indicators 列出来,你勾哪个、它就把对应 ID 写进 pinned-idscollapsed-ids

安装

# 源码(开发)
./install.sh
# 或从 Releases 下扩展包 / Ubuntu deb 装系统级
gnome-extensions install <zip>

要求:GNOME Shell 42、X11 会话、AppIndicator 扩展(Ubuntu 22.04 自带)。卸载会把所有图标原样搬回顶栏,不留残留。界面截图见仓库 README

小结

  • reparenting 同一个 actor 是整个扩展能成立的根基——折的是真图标,菜单交互零损耗。
  • canonicalId 归一化随机 ID 是从「能用」到「重启后依然好用」的分水岭,也是我花时间最多的地方。
  • gsettings 当跨进程桥 是 GNOME 扩展 shell↔prefs 通信的惯用解法,known-indicators 这个 JSON key 就是这么来的。

如果你的 GNOME 顶栏也被托盘图标挤满了,欢迎试试、提 issue。我另外两个 X11 桌面小工具——GNOME 顶栏宠物 NotchiLinux 全局语音输入 Voice Input——同样是折腾桌面环境的产物,感兴趣可以顺手看看。

Tray Collapse:把 GNOME 顶栏挤爆的托盘图标,折进一个可展开的抽屉
http://localhost:8090/archives/tray-collapse
作者
fnidore
发布于
更新于
许可