[client/ui] Refresh Profiles submenu by rebuilding the tray menu

Wails v3 alpha's submenu.Update() builds a fresh, detached NSMenu on
darwin instead of mutating the one attached to the parent menu item at
initial setup, so the visible Profiles entries stayed frozen on the
empty snapshot captured when the tray was registered: clicks reached
the new Go MenuItem objects (and the daemon SwitchProfile RPC ran), but
the checkmark never moved and reopening the menu still showed the old
selection.

Cache the top-level menu and call tray.SetMenu(t.menu) after each
loadProfiles refresh; macosSystemTray.setMenu clears and rebuilds the
entire NSMenu tree against the cached pointer, which propagates submenu
content changes to the visible menu.

Also adds INFO logs around profile click / SwitchProfile RPC / list
refresh so the active-profile flow is observable end-to-end.
This commit is contained in:
Zoltan Papp
2026-05-12 21:24:52 +02:00
parent e3efaa5e59
commit 4988f2aa68

View File

@@ -122,6 +122,7 @@ type Tray struct {
window *application.WebviewWindow
svc TrayServices
menu *application.Menu
statusItem *application.MenuItem
upItem *application.MenuItem
downItem *application.MenuItem
@@ -156,7 +157,8 @@ func NewTray(app *application.App, window *application.WebviewWindow, svc TraySe
t.tray = app.SystemTray.New()
t.applyIcon()
t.tray.SetTooltip(trayTooltip)
t.tray.SetMenu(t.buildMenu())
t.menu = t.buildMenu()
t.tray.SetMenu(t.menu)
// Left-click on the tray icon opens the menu on every platform. The
// window is reached through the explicit "Open NetBird" entry. This
// matches macOS NSStatusItem convention (click → menu), the Linux
@@ -721,19 +723,32 @@ func (t *Tray) loadProfiles() {
}
sort.Slice(profiles, func(i, j int) bool { return profiles[i].Name < profiles[j].Name })
log.Infof("tray loadProfiles: received %d profile(s) for user %q", len(profiles), username)
t.profileSubmenu.Clear()
for _, p := range profiles {
name := p.Name
active := p.IsActive
log.Infof("tray loadProfiles: profile=%q active=%v", name, active)
item := t.profileSubmenu.AddCheckbox(name, active)
item.OnClick(func(*application.Context) {
log.Infof("tray profile click: profile=%q wasActive=%v", name, active)
if active {
return
}
t.switchProfile(name)
})
}
t.profileSubmenu.Update()
// Wails v3 alpha's submenu.Update() builds a fresh, detached NSMenu on
// darwin that never replaces the empty NSMenu attached to the parent
// menu item at initial setup — so the visible Profiles menu stays
// frozen on the snapshot taken when the tray was registered. Re-running
// SetMenu on the top-level rebuilds the entire NSMenu tree against the
// cached pointer and is the only path that propagates submenu changes.
if t.menu != nil {
t.tray.SetMenu(t.menu)
} else {
t.profileSubmenu.Update()
}
}
// switchProfile runs the daemon RPC in a goroutine so the menu click
@@ -748,14 +763,16 @@ func (t *Tray) switchProfile(name string) {
log.Errorf("get current user: %v", err)
return
}
log.Infof("tray switchProfile: sending SwitchProfile RPC profile=%q user=%q", name, username)
if err := t.svc.Profiles.Switch(ctx, services.ProfileRef{
ProfileName: name,
Username: username,
}); err != nil {
log.Errorf("switch profile to %s: %v", name, err)
log.Errorf("tray switchProfile: SwitchProfile RPC failed profile=%q err=%v", name, err)
t.notifyError(fmt.Sprintf("Failed to switch to %s", name))
return
}
log.Infof("tray switchProfile: SwitchProfile RPC succeeded profile=%q", name)
t.loadProfiles()
}()
}