mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-19 15:01:29 +02:00
show macos dock icon when any window is visible
This commit is contained in:
@@ -8,6 +8,7 @@ This is the Wails v3 desktop UI for NetBird. Go services live in `services/`; th
|
||||
|
||||
### Go (top-level package `main`)
|
||||
- `main.go` — app entry. Builds the shared gRPC `Conn`, constructs services, registers them with Wails, creates the main webview window, then starts (in order) the Linux SNI watcher → tray → `peers.Watch` → `app.Run`. CLI flags: `--daemon-addr`, `--log-file` (repeatable; default is **empty** so "no flag" is distinguishable from explicit `--log-file console` — when empty `parseFlagsAndInitLog` falls back to `console` for `InitLog` and returns `userSetLogFile=false`), `--log-level` (`trace|debug|info|warn|error`, default `info`). See "GUI debug logging" below for what `userSetLogFile` gates.
|
||||
- `dock_darwin.go` / `dock_other.go` — flips the macOS dock entry to follow window visibility. Wails seeds `Mac.ActivationPolicy` at startup but exposes no runtime setter, so `dock_darwin.go` calls `[NSApp setActivationPolicy:]` directly via cgo (with `activateIgnoringOtherApps:` on the 0→1 transition only — without it a newly-shown window opens behind whatever app the user is currently focused on; gating on the transition stops auxiliaries opening on top of an already-visible main from stealing focus). **The Go side is one call** — `main.go` does `initDockObserver()` from the `ApplicationStarted` hook (so NSApp is up). Everything else lives in cgo: the observer registers default-`NSNotificationCenter` block subscriptions for `NSWindowDidChangeOcclusionStateNotification` and `NSWindowWillCloseNotification` (`object:nil` so windows created later are picked up automatically — no per-window plumbing on the Go side, no `WindowManager` constructor wiring), each firing `refreshDockPolicy`. `refreshDockPolicy` `dispatch_async`s a recount onto the main queue, enumerates `[NSApp windows]`, filters by Wails's `WebviewWindow` subclass, and asks each for `NSWindow.isVisible` — true iff the window is ordered in **regardless of occlusion**. That's the fix for the occlusion problem we'd otherwise hit relying on Wails's WindowHide event (Wails maps `windowDidChangeOcclusionState` to it, so covering our window with another app would have looked like a hide). `dispatch_async` also defers past any pending `orderOut:` / `close:` from the originating notification, so destroy-on-close auxiliaries have actually left `[NSApp windows]` by the time we look. A `lastDockState` static int on the C side suppresses no-op flips (and the `activate` focus-steal that goes with them). Linux/Windows: `dock_other.go` is a no-op `initDockObserver()` since their panels already gate taskbar entries on window visibility.
|
||||
- `tray.go` — `Tray` struct + menu. Subscribes to `EventStatus`, `EventSystem`, `EventUpdateAvailable`, `EventUpdateProgress`. Owns per-status icon/dot, Profiles submenu, Connect/Disconnect swap, About → Update, session-expired toast.
|
||||
- **Tray menu updates go through `relayoutMenu` (whole-tree rebuild), never in-place submenu mutation.** Any dynamic menu change — status-text transitions (`tray_status.go applyStatus`, which also folds in daemon-version and session-deadline changes into one aggregated relayout per push), Profiles submenu (`tray_profiles.go loadProfiles` → caches rows under `profilesMu`, then `fillProfileSubmenu`), Exit Node submenu (`tray_exitnodes.go refreshExitNodes` → `fillExitNodeSubmenu`), and the About → Update row (`tray_update.go applyState` → `onMenuChange` callback) — rebuilds the entire menu via `Tray.relayoutMenu` (`buildMenu()` + repaint cached state + single `t.tray.SetMenu`). Serialised by `menuMu`. **Why:** on KDE/Plasma the StatusNotifierItem host caches a submenu's layout the first time it's opened (`GetLayout` for that submenu id) and never re-fetches it on a `LayoutUpdated(parent=0)` signal — so the old `submenu.Clear()`+`Add()` left both the visible rows AND the click→id mapping frozen on the first snapshot. Because `Clear()`+`Add()` allocates fresh monotonic item ids each time (Wails `menuitem.go`), clicks then sent ids the rebuilt `itemMap` no longer knew, and silently no-op'd ("Manage Profiles" stopped responding after the first switch). `buildMenu()` allocates a brand-new submenu container id each relayout, which Plasma treats as unseen and re-queries on next open — fixing both the stale paint and the dead clicks. Confirmed via `dbus-monitor`: a re-opened submenu issued no `GetLayout` until its container id changed. The whole-tree `SetMenu` also subsumes the older darwin detached-NSMenu workaround. `fill*Submenu` helpers are pure UI (read caches, no daemon fetch, no `SetMenu`) so `relayoutMenu` never recurses back into the fetchers.
|
||||
- **Tray concurrency model (menuMu domain).** Wails dispatches `Event.On` listeners and menu `OnClick` callbacks on **fresh goroutines** (so `applyStatus` runs are concurrent with each other and with relayouts), and Wails `MenuItem` setters are not goroutine-safe. Hence `t.menu` and every `*Item`/`*Submenu` field on `Tray` are `menuMu`-owned: `buildMenu` reassigns them all on each relayout, and repaints happen from the caches inside `relayoutMenu` (caches are committed *before* the menu is touched, which makes a write on an orphaned pre-relayout item self-healing). Two sanctioned out-of-lock accesses: the Connect/Disconnect `OnClick` closures capture their own item, and `refreshSessionExpiresLabel` (30s ticker) snapshots `sessionExpiresItem` under `menuMu`. `relayoutMenu` is the **only post-startup `tray.SetMenu` call site** — never push a menu pointer snapshotted outside `menuMu` (it can reinstall a stale tree); `applyStatusIndicator` is `SetBitmap`-only, the relayout's trailing `SetMenu` is what repaints the macOS dot. Tray code never runs on the OS main thread, so taking `menuMu` from any goroutine can't deadlock the setters' `dispatch_sync`/`InvokeSync`.
|
||||
|
||||
70
client/ui/dock_darwin.go
Normal file
70
client/ui/dock_darwin.go
Normal file
@@ -0,0 +1,70 @@
|
||||
//go:build darwin
|
||||
|
||||
package main
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c
|
||||
#cgo LDFLAGS: -framework Cocoa
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
static int lastDockState = -1;
|
||||
|
||||
static void refreshDockPolicy(void) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
Class cls = NSClassFromString(@"WebviewWindow");
|
||||
if (cls == nil) {
|
||||
return;
|
||||
}
|
||||
int visible = 0;
|
||||
for (NSWindow *w in [NSApp windows]) {
|
||||
if ([w isKindOfClass:cls] && [w isVisible]) {
|
||||
visible = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (visible == lastDockState) {
|
||||
return;
|
||||
}
|
||||
lastDockState = visible;
|
||||
|
||||
// Set application to "Regular" and show dock icon (when visible) or to "Accessory" (when hidden)
|
||||
if (visible) {
|
||||
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
|
||||
[NSApp activateIgnoringOtherApps:YES];
|
||||
} else {
|
||||
[NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static int dockObserverInstalled = 0;
|
||||
|
||||
static void initDockObserver(void) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (dockObserverInstalled) {
|
||||
return;
|
||||
}
|
||||
dockObserverInstalled = 1;
|
||||
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
|
||||
void (^trigger)(NSNotification *) = ^(NSNotification *_) {
|
||||
refreshDockPolicy();
|
||||
};
|
||||
|
||||
[nc addObserverForName:NSWindowDidChangeOcclusionStateNotification
|
||||
object:nil
|
||||
queue:nil
|
||||
usingBlock:trigger];
|
||||
[nc addObserverForName:NSWindowWillCloseNotification
|
||||
object:nil
|
||||
queue:nil
|
||||
usingBlock:trigger];
|
||||
|
||||
refreshDockPolicy();
|
||||
});
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
func initDockObserver() {
|
||||
C.initDockObserver()
|
||||
}
|
||||
6
client/ui/dock_other.go
Normal file
6
client/ui/dock_other.go
Normal file
@@ -0,0 +1,6 @@
|
||||
//go:build !darwin && !android && !ios && !freebsd && !js
|
||||
|
||||
package main
|
||||
|
||||
// macOS-only; no-op on other operating systems
|
||||
func initDockObserver() {}
|
||||
@@ -109,6 +109,7 @@ func main() {
|
||||
// authorized).
|
||||
app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(*application.ApplicationEvent) {
|
||||
go requestNotificationAuthorization(notifier)
|
||||
initDockObserver()
|
||||
})
|
||||
|
||||
bundle, prefStore, localizer := buildI18n(app)
|
||||
|
||||
Reference in New Issue
Block a user