t194 四修:托盤改用原生 NSStatusItem——energye/systray 在 Wails 之下根本不建托盤

leo:右鍵仍然沒用。這次先做**最小重現**才動手,結果推翻了我前兩次的判斷。

## 真兇(實測,非推論)
systray.go:83 的 setInternalLoop(true) **只在 systray.Run() 裡呼叫**;
RunWithExternalLoop 沒有 ⇒ registerSystray() 第一行就
`if (!internalLoop) return;` ⇒ **delegate 從沒建立**
⇒ onReady 不會被呼叫、enable_on_click 不會執行、左右鍵事件根本不存在。

最小重現(scratchpad/traytest):RunWithExternalLoop + onReady 印 "READY"
⇒ **READY 從未印出**。托盤自始至終沒被建立過。
(左鍵之所以「第一次能開」是 Wails 自己開的窗,與托盤無關。)

## 正解:原生 NSStatusItem(tray_darwin.m + tray_darwin.go)
左鍵=開視窗;右鍵=暫時掛選單、performClick、再拿掉(不常駐 setMenu:,
否則按鈕 action 不會被呼叫、左鍵也會變成彈選單——與 systray 那個坑同源)。

## 過程中踩到、也寫進閘的三個坑
① dispatch_async(main_queue) **無效**:Wails 佔住主執行緒後不跑標準 run loop,
   排進去的 block 永遠不執行(log 只印到 "dispatching…")⇒ 改 performSelectorOnMainThread
② NSStatusBar 需要 NSApp 已初始化:在 wails.Run() 之前呼叫 ⇒ 靜默失敗、icon 不出現
③ 🔴 **我一度對著 `go build` 產的 stub 除錯**——沒帶 Wails build tags 的執行檔
   一跑就印 "Wails applications will not build without the correct build tags" 並退出,
   我卻以為是「托盤沒建起來」,白繞一圈。**驗 Wails App 一定要用 wails build 的產物。**

## 實測證據(AppleScript 從 UI 層查,最貼近使用者看到的)
  menu bar 數=2,status item 數=1   ← 選單列 icon 真的存在
  collector 同步啟動、正常結束無孤兒
三支機械閘全過(閘也改成驗原生實作:不可 dispatch_async/必須
performSelectorOnMainThread/不可常駐 setMenu/不可再依賴 energye/systray)。

⚠️ 仍未驗:左右鍵的**實際點擊行為**要 leo 手動點。
This commit is contained in:
2026-08-05 02:56:41 +08:00
parent a02eed539a
commit 5860ef72df
6 changed files with 168 additions and 57 deletions
+23 -17
View File
@@ -46,24 +46,30 @@ cols={tuple(rows[y][x*4:x*4+3]) for y in range(h) for x in range(w) if rows[y][x
print(f" ✅ template icon{w}x{h}、不透明 {100*opq//len(al)}%、顏色 {len(cols)} 種 {list(cols)[:1]}")
PY
# ② 呼叫順序:AddMenuItem → SetMenuNil → SetOnClick/SetOnRClick
python3 - <<'PY' || exit 1
# ② 原生托盤:三個必要條件(每一個都是實測踩過的坑)
python3 - <<'PY2' || exit 1
import sys
s=open('main.go',encoding='utf-8').read()
try:
a=s.index('systray.AddMenuItem'); n=s.index('systray.SetMenuNil')
c=s.index('systray.SetOnClick')
except ValueError as e:
print(f" ❌ 找不到必要呼叫:{e}"); sys.exit(1)
if not (a < n < c):
print(" ❌ 順序錯:必須 AddMenuItem → SetMenuNil → SetOnClick"); sys.exit(1)
print(" ✅ 順序正確(建選單→清掉→註冊左鍵)")
# 🔴 右鍵**不可以**註冊 handlersystray_on_rclick() 在沒註冊時會自己 show_menu()
# 註冊了反而要自己重做一遍(leo 實測「右鍵沒跳出 quit」就是這樣來的)。
if 'systray.SetOnRClick' in s:
print(" ❌ 註冊了 SetOnRClick ⇒ 蓋掉函式庫預設的『右鍵開選單』,右鍵會沒反應"); sys.exit(1)
print(" ✅ 右鍵交給函式庫預設行為(不自己註冊)")
PY
m=open('tray_darwin.m',encoding='utf-8').read()
g=open('tray_darwin.go',encoding='utf-8').read()
bad=[]
# (a) 不可用 dispatch_async(main_queue)Wails 佔住主執行緒後不跑標準 run loop,
# 排進 main queue 的 block 永遠不會執行(實測 log 只印到 dispatching…)
if 'dispatch_async(dispatch_get_main_queue' in m:
bad.append('用了 dispatch_async(main_queue) ⇒ Wails 之下不會執行')
# (b) 必須 performSelectorOnMainThreadNSStatusItem 要主執行緒+NSApp 已初始化
if 'performSelectorOnMainThread' not in m:
bad.append('缺 performSelectorOnMainThread ⇒ NSStatusItem 建不起來或崩潰')
# (c) 不可 setMenu:——設了選單,按鈕 action 不會被呼叫,左鍵也會變成彈選單
if '.item setMenu:' in m or 'item.menu = self.menu;' in m and 'item.menu = nil' not in m:
bad.append('把選單常駐掛在 statusItem 上 ⇒ 左鍵會失效')
# (d) 不可再依賴 energye/systray(實測它在 RunWithExternalLoop 下根本不建托盤)
if 'energye/systray' in g:
bad.append('還在用 energye/systray ⇒ 外部 loop 之下不會建立托盤')
if bad:
for b in bad: print(f" ❌ {b}")
sys.exit(1)
print(" ✅ 原生托盤:主執行緒建立、不常駐掛選單、未依賴 systray")
PY2
# ③ ShowWindow 要能叫回「已開但被蓋住」的視窗
for f in WindowUnminimise WindowShow WindowSetAlwaysOnTop; do