Files
arcrun-collector/cmd/arcrun-app/tray_darwin.m
T
Leo 5860ef72df 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 手動點。
2026-08-05 02:56:41 +08:00

102 lines
5.0 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//go:build darwin
// tray_darwin.m — macOS 選單列 icont194 三修,原生實作)
//
// 🔴 為什麼不用 energye/systray:實測證明它在 Wails 之下**根本沒建立托盤**。
// systray.go:83 的 setInternalLoop(true) 只在 systray.Run() 裡呼叫;
// RunWithExternalLoop 沒有 ⇒ registerSystray() 第一行就
// `if (!internalLoop) return;` ⇒ delegate 從沒建立
// ⇒ onReady 不會被呼叫、enable_on_click 不會執行、左右鍵事件根本不存在。
// 最小重現(scratchpad/traytest):READY 從未印出、點擊無反應。
// ⇒ 直接用 NSStatusItem,行為完全由我們掌握。
#import <Cocoa/Cocoa.h>
extern void trayOnLeftClick(void);
extern void trayOnQuit(void);
@interface ArcrunTray : NSObject
@property (strong) NSStatusItem *item;
@property (strong) NSMenu *menu;
- (void)onClick:(id)sender;
- (void)onQuit:(id)sender;
@end
@implementation ArcrunTray
// 左鍵=開視窗;右鍵=彈選單。用 currentEvent 分辨(與系統慣例一致)。
- (void)onClick:(id)sender {
NSEvent *e = [NSApp currentEvent];
if (e.type == NSEventTypeRightMouseUp ||
(e.type == NSEventTypeLeftMouseUp && (e.modifierFlags & NSEventModifierFlagControl))) {
// 右鍵(或 Ctrl+左鍵,macOS 慣例):把選單彈在 icon 底下。
// popUpStatusItemMenu 自 10.14 起 deprecated ⇒ 改用「暫時掛上選單、
// 送一次 click、再拿掉」——這是官方建議的替代寫法,且不會讓左鍵失效。
self.item.menu = self.menu;
[self.item.button performClick:nil];
self.item.menu = nil;
return;
}
trayOnLeftClick();
}
- (void)onQuit:(id)sender { trayOnQuit(); }
@end
static ArcrunTray *gTray = nil;
static void createTrayNow(NSData *iconData, NSString *tip, NSString *quitTitle);
// 用一個小物件把參數帶到主執行緒(performSelectorOnMainThread 只能傳一個 object
@interface ArcrunTrayBoot : NSObject
@property (strong) NSData *iconData;
@property (strong) NSString *tip;
@property (strong) NSString *quitTitle;
- (void)run;
@end
@implementation ArcrunTrayBoot
- (void)run { createTrayNow(self.iconData, self.tip, self.quitTitle); }
@end
// createTray:兩個條件同時要滿足,缺一個都會靜默失敗(兩個我都踩過):
// ① **主執行緒**——NSStatusItem 內部會 new NSWindow
// (在別的 goroutine 呼叫 ⇒ "NSWindow should only be instantiated on the main thread!"
// ② **NSApplication 已初始化**——[NSStatusBar systemStatusBar] 在 NSApp
// 還沒 finishLaunching 時拿不到東西 ⇒ icon 不會出現、也不報錯
// (實測:在 wails.Run() 之前呼叫 ⇒ menu bar item 數 = 0
// ⇒ 用 dispatch_async 丟到 main queueWails 起來後才會被執行,且保證在主執行緒。
void createTray(const char *iconBytes, int iconLen, const char *tooltip, const char *quitTitle) {
// 先把參數複製一份——呼叫端的 C 字串在 dispatch 執行時可能已被釋放
NSData *iconData = [NSData dataWithBytes:iconBytes length:iconLen];
NSString *tip = [NSString stringWithUTF8String:tooltip];
NSString *qt = [NSString stringWithUTF8String:quitTitle];
// 🔴 **不能用 dispatch_async(main_queue)**——實測 Wails 佔住主執行緒後
// 沒有跑標準 run loop,排進 main queue 的 block **永遠不會被執行**
// log 只印到 "dispatching…"block 內完全沒動靜)。
// 改用 performSelectorOnMainThread:它走的是 run loop 的 common modes
// 在 Wails 的 loop 之下仍會被處理。
ArcrunTrayBoot *boot = [[ArcrunTrayBoot alloc] init];
boot.iconData = iconData; boot.tip = tip; boot.quitTitle = qt;
[boot performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:NO];
}
static void createTrayNow(NSData *iconData, NSString *tip, NSString *quitTitle) {
gTray = [[ArcrunTray alloc] init];
gTray.item = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
NSImage *img = [[NSImage alloc] initWithData:iconData];
[img setSize:NSMakeSize(18, 18)];
[img setTemplate:YES]; // 由系統依深淺色自動上色
gTray.item.button.image = img;
gTray.item.button.toolTip = tip;
// 右鍵選單只有一項(leo:「托盤裡只剩下按右鍵會結束」)
gTray.menu = [[NSMenu alloc] init];
NSMenuItem *q = [[NSMenuItem alloc] initWithTitle:quitTitle
action:@selector(onQuit:) keyEquivalent:@""];
[q setTarget:gTray];
[gTray.menu addItem:q];
// ⚠️ **不要** setMenu:——一旦設了選單,按鈕的 action 就不會被呼叫,
// 左鍵也會變成彈選單(這正是 systray 那個「建了選單滑鼠事件失效」的同一件事)。
// 改成自己在 onClick: 裡判斷左右鍵,右鍵才 popUpStatusItemMenu。
[gTray.item.button setTarget:gTray];
[gTray.item.button setAction:@selector(onClick:)];
[gTray.item.button sendActionOn:(NSEventMaskLeftMouseUp | NSEventMaskRightMouseUp)];
}