mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-12 16:39:09 +02:00
[client/ui] Fix tray submenu not updating on KDE/Plasma
The Profiles and Exit Node submenus (and the About version/Update rows) stopped reflecting changes on KDE/Plasma: after the first profile switch the menu froze on its initial snapshot, and "Manage Profiles" — plus the profile rows themselves — stopped responding to clicks entirely. Root cause (confirmed via dbus-monitor): Plasma's StatusNotifierItem host caches a submenu's layout the first time it is opened (GetLayout for that submenu id) and never re-fetches it on a LayoutUpdated(parent=0) signal. The old submenu.Clear()+Add() repaint allocated fresh monotonic item ids each time but reused the same submenu container id, so Plasma kept showing the stale snapshot and, on click, sent the stale ids back — which the rebuilt itemMap no longer knew, silently no-op'ing the click. Fix: route every dynamic tray-menu change through a new relayoutMenu that rebuilds the whole tree (buildMenu + repaint cached state + a single SetMenu), allocating brand-new submenu container ids. Plasma treats those as unseen and re-queries them on next open, fixing both the stale paint and the dead clicks. loadProfiles/refreshExitNodes now cache their rows and drive relayoutMenu; the update row goes through a new onMenuChange hook; the daemon-version row relayouts too. relayoutMenu is serialised by menuMu and the fill*Submenu helpers are pure UI (no fetch, no SetMenu) so it never recurses. The whole-tree SetMenu also subsumes the prior darwin detached-NSMenu workaround.
This commit is contained in:
+34
-20
@@ -43,14 +43,16 @@ func (t *Tray) loadConfig() {
|
||||
t.profileMu.Unlock()
|
||||
}
|
||||
|
||||
// loadProfiles refreshes the Profiles submenu from the daemon. Each
|
||||
// entry is a checkbox showing the active profile and switches on click.
|
||||
// Called on ApplicationStarted, after a successful switchProfile, and
|
||||
// from applyStatus whenever the daemon's status text changes — the
|
||||
// last case catches profile flips driven by another channel (CLI
|
||||
// "netbird profile select", autoconnect picking the persisted profile
|
||||
// after the UI's first ListProfiles, etc.) since the daemon does not
|
||||
// emit a dedicated active-profile event.
|
||||
// loadProfiles fetches the profile list from the daemon, caches it under
|
||||
// profilesMu, and drives a full tray relayout (relayoutMenu) so the Profiles
|
||||
// submenu repaints. Called on ApplicationStarted, after a successful
|
||||
// switchProfile, and from applyStatus whenever the daemon's status text
|
||||
// changes — the last case catches profile flips driven by another channel
|
||||
// (CLI "netbird profile select", autoconnect picking the persisted profile
|
||||
// after the UI's first ListProfiles, etc.) since the daemon does not emit a
|
||||
// dedicated active-profile event. The relayout (rather than a Clear()+Add()
|
||||
// into the live submenu) is what makes KDE/Plasma actually repaint and keep
|
||||
// the click→id mapping live — see relayoutMenu's doc comment.
|
||||
func (t *Tray) loadProfiles() {
|
||||
if t.profileSubmenu == nil {
|
||||
return
|
||||
@@ -69,6 +71,29 @@ func (t *Tray) loadProfiles() {
|
||||
log.Debugf("list profiles: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.profilesMu.Lock()
|
||||
t.profiles = profiles
|
||||
t.profilesUser = username
|
||||
t.profilesMu.Unlock()
|
||||
|
||||
t.relayoutMenu()
|
||||
}
|
||||
|
||||
// fillProfileSubmenu paints the cached profile rows into the (freshly built)
|
||||
// Profiles submenu and updates the parent label + email row. Pure UI: it
|
||||
// never fetches and never calls SetMenu — relayoutMenu owns the single
|
||||
// SetMenu that pushes the whole tree. Reads the rows captured by loadProfiles
|
||||
// under profilesMu.
|
||||
func (t *Tray) fillProfileSubmenu() {
|
||||
if t.profileSubmenu == nil {
|
||||
return
|
||||
}
|
||||
t.profilesMu.Lock()
|
||||
profiles := append([]services.Profile(nil), t.profiles...)
|
||||
username := t.profilesUser
|
||||
t.profilesMu.Unlock()
|
||||
|
||||
sort.Slice(profiles, func(i, j int) bool { return profiles[i].Name < profiles[j].Name })
|
||||
|
||||
t.profileSubmenu.Clear()
|
||||
@@ -102,7 +127,7 @@ func (t *Tray) loadProfiles() {
|
||||
t.profileSubmenu.Add(t.loc.T("tray.menu.manageProfiles")).OnClick(func(*application.Context) {
|
||||
t.svc.WindowManager.OpenSettings("profiles")
|
||||
})
|
||||
log.Infof("tray loadProfiles: received %d profile(s) for user %q, active=%q", len(profiles), username, activeName)
|
||||
log.Infof("tray fillProfileSubmenu: %d profile(s) for user %q, active=%q", len(profiles), username, activeName)
|
||||
if t.profileSubmenuItem != nil && activeName != "" {
|
||||
t.profileSubmenuItem.SetLabel(activeName)
|
||||
}
|
||||
@@ -114,17 +139,6 @@ func (t *Tray) loadProfiles() {
|
||||
t.profileEmailItem.SetHidden(true)
|
||||
}
|
||||
}
|
||||
// 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 cancels any in-flight profile switch, then starts a new one.
|
||||
|
||||
Reference in New Issue
Block a user