[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",
Width: 960,
Height: 640,
Hidden: false,
Hidden: true,
BackgroundColour: application.NewRGB(24, 26, 29),
URL: "/",
Mac: application.MacWindow{

View File

@@ -139,32 +139,15 @@ func NewTray(app *application.App, window *application.WebviewWindow, svc TraySe
t.applyIcon()
t.tray.SetTooltip(trayTooltip)
t.tray.SetMenu(t.buildMenu())
// Tray click handling is platform-specific by design:
//
// On Windows and macOS the OS-level tray protocol cleanly separates left
// and right click. AttachWindow plus an explicit OnClick gives the
// expected "click the icon to toggle the window, right-click to open the
// menu" UX, and the platform never delivers both events at once.
//
// On Linux the tray rides on the org.kde.StatusNotifierItem D-Bus protocol
// (libayatana-appindicator). The SNI Activate signal *is* left-click, but
// 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() })
}
// 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
// StatusNotifierItem spec, and the legacy Fyne client. On Linux,
// AttachWindow plus Wails3's applySmartDefaults would also pop the
// window alongside the menu on environments like GNOME Shell with the
// AppIndicator extension, so we intentionally skip both AttachWindow
// and OnClick here. Right-click still opens the menu through Wails'
// default rightClickHandler fallback.
app.Event.On(services.EventStatus, t.onStatusEvent)
app.Event.On(services.EventSystem, t.onSystemEvent)
@@ -195,16 +178,17 @@ func (t *Tray) buildMenu() *application.Menu {
SetEnabled(false)
menu.AddSeparator()
// On Linux the tray icon's left-click handler is intentionally unbound
// (see NewTray for the rationale), so expose the window through an
// explicit menu entry. Windows and macOS get the window via left-click.
if runtime.GOOS == "linux" {
menu.Add(menuOpenNetBird).OnClick(func(*application.Context) { t.ShowWindow() })
menu.AddSeparator()
}
// The tray icon's left-click handler is intentionally unbound (see
// NewTray for the rationale), so expose the window through an explicit
// menu entry on every platform.
menu.Add(menuOpenNetBird).OnClick(func(*application.Context) { t.ShowWindow() })
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.downItem = menu.Add(menuDisconnect).OnClick(func(*application.Context) { t.handleDisconnect() })
t.downItem.SetEnabled(false)
t.downItem.SetHidden(true)
menu.AddSeparator()
@@ -245,17 +229,6 @@ func (t *Tray) buildMenu() *application.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) {
if t.window == nil {
return
@@ -466,9 +439,11 @@ func (t *Tray) applyStatus(st services.Status) {
t.statusItem.SetEnabled(needsLogin)
}
if t.upItem != nil {
t.upItem.SetHidden(connected || needsLogin)
t.upItem.SetEnabled(!connected && !needsLogin)
}
if t.downItem != nil {
t.downItem.SetHidden(!connected)
t.downItem.SetEnabled(connected)
}
}