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 手動點。
102 lines
5.0 KiB
Objective-C
102 lines
5.0 KiB
Objective-C
//go:build darwin
|
||
// tray_darwin.m — macOS 選單列 icon(t194 三修,原生實作)
|
||
//
|
||
// 🔴 為什麼不用 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 queue:Wails 起來後才會被執行,且保證在主執行緒。
|
||
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)];
|
||
}
|