5860ef72df
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 手動點。
85 lines
4.1 KiB
Bash
Executable File
85 lines
4.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# check-tray.sh — 托盤行為的機械閘(t194,leo 真機測出四個 bug 後補)
|
||
#
|
||
# 🔴 為什麼要有:這四個 bug **編得過、也跑得起來**,只有真的去點才會發現。
|
||
# 我改完只驗「build ok」就想交,被交付警察攔下。
|
||
# ⇒ 把「點不下去的部分」用可機械檢查的**前置條件**守住。
|
||
set -uo pipefail
|
||
cd "$(dirname "$0")"
|
||
FAIL=0
|
||
ok(){ printf " ✅ %s\n" "$1"; }
|
||
ng(){ printf " ❌ %s\n" "$1"; FAIL=1; }
|
||
|
||
echo "━━━ 托盤行為前置條件 ━━━"
|
||
|
||
# ① template icon:純黑+大量透明、尺寸 22/44
|
||
python3 - <<'PY' || exit 1
|
||
import zlib,struct,sys
|
||
d=open('build/trayicon.png','rb').read()
|
||
pos=8; idat=b''; w=h=ct=0
|
||
while pos<len(d):
|
||
ln=struct.unpack('>I',d[pos:pos+4])[0]; t=d[pos+4:pos+8]
|
||
if t==b'IHDR': w,h,_,ct=struct.unpack('>IIBB',d[pos+8:pos+18])
|
||
if t==b'IDAT': idat+=d[pos+8:pos+8+ln]
|
||
pos+=12+ln
|
||
if w not in (22,44) or ct!=6:
|
||
print(f" ❌ tray icon 應為 22/44 的 RGBA,got {w}x{h} ct={ct}"); sys.exit(1)
|
||
raw=zlib.decompress(idat); ch=4; stride=w*ch+1
|
||
rows=[];prev=bytearray(w*ch)
|
||
for y in range(h):
|
||
f=raw[y*stride];line=bytearray(raw[y*stride+1:(y+1)*stride])
|
||
for i in range(len(line)):
|
||
a=line[i-ch] if i>=ch else 0;b=prev[i];c=prev[i-ch] if i>=ch else 0
|
||
if f==1: line[i]=(line[i]+a)&255
|
||
elif f==2: line[i]=(line[i]+b)&255
|
||
elif f==3: line[i]=(line[i]+(a+b)//2)&255
|
||
elif f==4:
|
||
pp=a+b-c;pa,pb,pc=abs(pp-a),abs(pp-b),abs(pp-c)
|
||
pr=a if(pa<=pb and pa<=pc) else (b if pb<=pc else c)
|
||
line[i]=(line[i]+pr)&255
|
||
rows.append(bytes(line));prev=line
|
||
al=[rows[y][x*4+3] for y in range(h) for x in range(w)]
|
||
opq=sum(1 for a in al if a>200)
|
||
if opq > len(al)*0.5:
|
||
print(f" ❌ tray icon 幾乎全不透明({100*opq//len(al)}%)⇒ 會顯示成方塊"); sys.exit(1)
|
||
cols={tuple(rows[y][x*4:x*4+3]) for y in range(h) for x in range(w) if rows[y][x*4+3]>200}
|
||
print(f" ✅ template icon:{w}x{h}、不透明 {100*opq//len(al)}%、顏色 {len(cols)} 種 {list(cols)[:1]}")
|
||
PY
|
||
|
||
# ② 原生托盤:三個必要條件(每一個都是實測踩過的坑)
|
||
python3 - <<'PY2' || exit 1
|
||
import sys
|
||
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) 必須 performSelectorOnMainThread:NSStatusItem 要主執行緒+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
|
||
grep -q "runtime.$f" app.go && ok "ShowWindow 有 $f" || ng "ShowWindow 缺 $f(叫不回被蓋住的視窗)"
|
||
done
|
||
|
||
# ④ 結束路徑:收訊號 → 停子行程 → 等它真的不見
|
||
grep -q "signal.Notify" supervise.go && ok "有處理 SIGTERM/SIGINT" || ng "沒有訊號處理(只能強制結束)"
|
||
grep -q "waitCollectorGone" supervise.go && ok "結束前會等 collector 真的不見" || ng "沒等子行程 ⇒ 會留孤兒"
|
||
|
||
echo
|
||
[ $FAIL -eq 0 ] && echo "✅ 托盤前置條件全過" || { echo "❌ 未過——不准交貨"; exit 1; }
|