//go:build darwin #import #include "_cgo_export.h" // CdropStatusBarController owns the menu bar item and routes the three menu // actions back into Go. It is deliberately NOT the NSApplication delegate — // Wails owns that, and stealing it would break Wails' window / runtime / custom // scheme handling. A status item can be created independently after launch, so // we never touch the delegate. @interface CdropStatusBarController : NSObject @property (strong) NSStatusItem *item; @property (strong) NSMenu *menu; - (void)showWindow:(id)sender; - (void)openSettings:(id)sender; - (void)quit:(id)sender; - (void)statusItemClicked:(id)sender; @end @implementation CdropStatusBarController - (void)showWindow:(id)sender { cdropOnShowWindow(); } - (void)openSettings:(id)sender { cdropOnOpenSettings(); } - (void)quit:(id)sender { cdropOnQuit(); } // showMenu pops the menu now. Temporarily attaching item.menu and performClick // shows it and BLOCKS until dismissed (menu tracking is modal); detaching after // keeps left-clicks flowing through statusItemClicked: (so left-click opens the // window directly instead of popping the menu). - (void)showMenu { self.item.menu = self.menu; [self.item.button performClick:nil]; self.item.menu = nil; } // statusItemClicked fires on left/right mouse-up. Click routing (the Windows tray // should mirror this once implemented): // - left-click → open the main window // - right-click / control-click → menu (Show / Settings / Quit) // We route clicks manually instead of setting item.menu — item.menu would pop the // menu on left-click too, leaving no way for left-click to open the window. - (void)statusItemClicked:(id)sender { NSEvent *e = [NSApp currentEvent]; BOOL secondary = (e.type == NSEventTypeRightMouseUp) || ((e.modifierFlags & NSEventModifierFlagControl) != 0); if (secondary) { [self showMenu]; // right-click / control-click → menu } else { cdropOnShowWindow(); // left-click → open the main window } } @end // File-scope strong reference (ARC) keeps the controller and its status item // alive for the process lifetime. static CdropStatusBarController *gController = nil; // cdropStatusBarInstall creates the menu bar item + its menu on the main thread. // The icon is the embedded brand mascot (a colored, NON-template image — keeps // brand color instead of tinting to the menu bar). It's displayed at the menu // bar thickness (logical points); the high-res (256px) source rep keeps it crisp // on retina. If decoding fails it falls back to the text title. The PNG bytes are // copied into NSData synchronously here (before the async block) so Go can free // its C buffer right after this call. Labels arrive as UTF-8 from Go so this // source stays ASCII-only. Idempotent. void cdropStatusBarInstall(const void *iconPNG, int iconLen, const char *title, const char *showLabel, const char *settingsLabel, const char *quitLabel) { NSData *iconData = (iconPNG != NULL && iconLen > 0) ? [NSData dataWithBytes:iconPNG length:(NSUInteger)iconLen] : nil; NSString *t = [NSString stringWithUTF8String:title]; NSString *sl = [NSString stringWithUTF8String:showLabel]; NSString *gl = [NSString stringWithUTF8String:settingsLabel]; NSString *ql = [NSString stringWithUTF8String:quitLabel]; dispatch_async(dispatch_get_main_queue(), ^{ if (gController != nil) { return; } gController = [[CdropStatusBarController alloc] init]; NSStatusItem *item = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength]; NSImage *icon = (iconData != nil) ? [[NSImage alloc] initWithData:iconData] : nil; if (icon != nil) { CGFloat th = [[NSStatusBar systemStatusBar] thickness]; [icon setSize:NSMakeSize(th, th)]; // fill bar height; retina uses 256px rep icon.template = NO; // keep brand color, don't tint item.button.image = icon; item.button.accessibilityLabel = t; // image has no text → a11y label } else { item.button.title = t; } NSMenu *menu = [[NSMenu alloc] init]; NSMenuItem *show = [[NSMenuItem alloc] initWithTitle:sl action:@selector(showWindow:) keyEquivalent:@""]; show.target = gController; [menu addItem:show]; NSMenuItem *settings = [[NSMenuItem alloc] initWithTitle:gl action:@selector(openSettings:) keyEquivalent:@""]; settings.target = gController; [menu addItem:settings]; [menu addItem:[NSMenuItem separatorItem]]; NSMenuItem *quit = [[NSMenuItem alloc] initWithTitle:ql action:@selector(quit:) keyEquivalent:@""]; quit.target = gController; [menu addItem:quit]; // Don't set item.menu (that auto-pops on mouse-down, killing double-click // detection). Store the menu and route clicks through statusItemClicked:. gController.item = item; gController.menu = menu; item.button.target = gController; item.button.action = @selector(statusItemClicked:); [item.button sendActionOn:(NSEventMaskLeftMouseUp | NSEventMaskRightMouseUp)]; }); } // cdropSetActivationPolicy flips the Dock presence on the main thread. // policy: 0 = regular (Dock icon shown), 1 = accessory (Dock hidden, menu bar // item only). void cdropSetActivationPolicy(int policy) { dispatch_async(dispatch_get_main_queue(), ^{ NSApplicationActivationPolicy p = (policy == 1) ? NSApplicationActivationPolicyAccessory : NSApplicationActivationPolicyRegular; [NSApp setActivationPolicy:p]; }); }