[client/ui] Tray menu opens on click; hide window at startup

Left-click on the tray icon now opens the menu on every platform — the
window is reached through a new "Open NetBird" entry. Only the action
that matches the current daemon state is shown: Connect when
disconnected, Disconnect when connected. The main window starts hidden
and is only surfaced via the tray, single-instance launch, or daemon
events.
This commit is contained in:
Zoltan Papp
2026-05-11 12:01:46 +02:00
parent 09052949a2
commit 7f560df9be
2 changed files with 21 additions and 46 deletions

View File

@@ -118,7 +118,7 @@ func main() {
Title: "NetBird", Title: "NetBird",
Width: 960, Width: 960,
Height: 640, Height: 640,
Hidden: false, Hidden: true,
BackgroundColour: application.NewRGB(24, 26, 29), BackgroundColour: application.NewRGB(24, 26, 29),
URL: "/", URL: "/",
Mac: application.MacWindow{ Mac: application.MacWindow{

View File

@@ -139,32 +139,15 @@ func NewTray(app *application.App, window *application.WebviewWindow, svc TraySe
t.applyIcon() t.applyIcon()
t.tray.SetTooltip(trayTooltip) t.tray.SetTooltip(trayTooltip)
t.tray.SetMenu(t.buildMenu()) t.tray.SetMenu(t.buildMenu())
// Tray click handling is platform-specific by design: // Left-click on the tray icon opens the menu on every platform. The
// // window is reached through the explicit "Open NetBird" entry. This
// On Windows and macOS the OS-level tray protocol cleanly separates left // matches macOS NSStatusItem convention (click → menu), the Linux
// and right click. AttachWindow plus an explicit OnClick gives the // StatusNotifierItem spec, and the legacy Fyne client. On Linux,
// expected "click the icon to toggle the window, right-click to open the // AttachWindow plus Wails3's applySmartDefaults would also pop the
// menu" UX, and the platform never delivers both events at once. // window alongside the menu on environments like GNOME Shell with the
// // AppIndicator extension, so we intentionally skip both AttachWindow
// On Linux the tray rides on the org.kde.StatusNotifierItem D-Bus protocol // and OnClick here. Right-click still opens the menu through Wails'
// (libayatana-appindicator). The SNI Activate signal *is* left-click, but // default rightClickHandler fallback.
// several environments — GNOME Shell with the AppIndicator extension is
// the loudest offender — also pop the attached menu on left-click,
// regardless of the ItemIsMenu property the spec defines for that purpose.
// Worse, AttachWindow on its own is enough to trigger this: Wails3's
// SystemTray.applySmartDefaults installs ToggleWindow as the default
// click handler whenever a window is attached, so even without an
// explicit OnClick the window pops up alongside the menu. The result
// looks like a bug to users.
//
// Mirror the legacy Fyne client's behaviour on Linux: skip both
// AttachWindow and OnClick so left-click only opens the menu, and expose
// the window through an explicit "Open NetBird" item. Right-click still
// opens the menu through Wails' default rightClickHandler fallback.
if runtime.GOOS != "linux" {
t.tray.AttachWindow(window)
t.tray.OnClick(func() { t.toggleWindow() })
}
app.Event.On(services.EventStatus, t.onStatusEvent) app.Event.On(services.EventStatus, t.onStatusEvent)
app.Event.On(services.EventSystem, t.onSystemEvent) app.Event.On(services.EventSystem, t.onSystemEvent)
@@ -195,16 +178,17 @@ func (t *Tray) buildMenu() *application.Menu {
SetEnabled(false) SetEnabled(false)
menu.AddSeparator() menu.AddSeparator()
// On Linux the tray icon's left-click handler is intentionally unbound // The tray icon's left-click handler is intentionally unbound (see
// (see NewTray for the rationale), so expose the window through an // NewTray for the rationale), so expose the window through an explicit
// explicit menu entry. Windows and macOS get the window via left-click. // menu entry on every platform.
if runtime.GOOS == "linux" { menu.Add(menuOpenNetBird).OnClick(func(*application.Context) { t.ShowWindow() })
menu.Add(menuOpenNetBird).OnClick(func(*application.Context) { t.ShowWindow() }) menu.AddSeparator()
menu.AddSeparator() // Only the action that applies to the current state is visible: Connect
} // when disconnected, Disconnect when connected. applyStatus swaps them on
// each daemon status change.
t.upItem = menu.Add(menuConnect).OnClick(func(*application.Context) { t.handleConnect() }) t.upItem = menu.Add(menuConnect).OnClick(func(*application.Context) { t.handleConnect() })
t.downItem = menu.Add(menuDisconnect).OnClick(func(*application.Context) { t.handleDisconnect() }) t.downItem = menu.Add(menuDisconnect).OnClick(func(*application.Context) { t.handleDisconnect() })
t.downItem.SetEnabled(false) t.downItem.SetHidden(true)
menu.AddSeparator() menu.AddSeparator()
@@ -245,17 +229,6 @@ func (t *Tray) buildMenu() *application.Menu {
return menu return menu
} }
func (t *Tray) toggleWindow() {
if t.window == nil {
return
}
if t.window.IsVisible() {
t.window.Hide()
return
}
t.window.Show()
}
func (t *Tray) openRoute(route string) { func (t *Tray) openRoute(route string) {
if t.window == nil { if t.window == nil {
return return
@@ -466,9 +439,11 @@ func (t *Tray) applyStatus(st services.Status) {
t.statusItem.SetEnabled(needsLogin) t.statusItem.SetEnabled(needsLogin)
} }
if t.upItem != nil { if t.upItem != nil {
t.upItem.SetHidden(connected || needsLogin)
t.upItem.SetEnabled(!connected && !needsLogin) t.upItem.SetEnabled(!connected && !needsLogin)
} }
if t.downItem != nil { if t.downItem != nil {
t.downItem.SetHidden(!connected)
t.downItem.SetEnabled(connected) t.downItem.SetEnabled(connected)
} }
} }