diff --git a/docs/src/content/docs/changelog.mdx b/docs/src/content/docs/changelog.mdx index 0ceda3213..3fd303a1b 100644 --- a/docs/src/content/docs/changelog.mdx +++ b/docs/src/content/docs/changelog.mdx @@ -43,6 +43,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add function `application.NewServiceWithOptions` to initialise services with additional configuration by [@leaanthony](https://github.com/leaanthony) in [#4024](https://github.com/wailsapp/wails/pull/4024) - More documentation by [@leaanthony](https://github.com/leaanthony) - Support cancellation of events in standard event listeners by [@leaanthony](https://github.com/leaanthony) +- Systray `Hide`, `Show` and `Destroy` support by [@leaanthony](https://github.com/leaanthony) ### Fixed diff --git a/docs/src/content/docs/guides/customizing-windows.mdx b/docs/src/content/docs/guides/customising-windows.mdx similarity index 98% rename from docs/src/content/docs/guides/customizing-windows.mdx rename to docs/src/content/docs/guides/customising-windows.mdx index 7c4bdd604..bded8bd8b 100644 --- a/docs/src/content/docs/guides/customizing-windows.mdx +++ b/docs/src/content/docs/guides/customising-windows.mdx @@ -1,5 +1,5 @@ --- -title: Customizing Windows in Wails +title: Customising Windows in Wails sidebar: order: 10 --- diff --git a/docs/src/content/docs/learn/events.mdx b/docs/src/content/docs/learn/events.mdx index ad8021b4a..fcfe6b16c 100644 --- a/docs/src/content/docs/learn/events.mdx +++ b/docs/src/content/docs/learn/events.mdx @@ -205,6 +205,7 @@ INF I always run after hooks! | Event Name | Common Event | Description | + |------------|--------------|-------------| | WindowDidBecomeKey | WindowFocus | Window became key window | | WindowDidBecomeMain | - | Window became main window | | WindowDidBeginSheet | - | Sheet began | @@ -233,7 +234,7 @@ INF I always run after hooks! | WindowDidExitVersionBrowser | - | Window exited version browser | | WindowDidExpose | - | Window exposed | | WindowDidFocus | WindowFocus | Window gained focus | - | WindowDidMiniaturise | WindowMinimise | Window minimised | + | WindowDidMiniaturize | WindowMinimise | Window minimised | | WindowDidMove | WindowDidMove | Window moved | | WindowDidOrderOffScreen | - | Window ordered off screen | | WindowDidOrderOnScreen | - | Window ordered on screen | @@ -255,7 +256,7 @@ INF I always run after hooks! | WindowMaximise | WindowMaximise | Window maximised | | WindowShouldClose | WindowClosing | Window should close | | WindowShow | WindowShow | Window shown | - | WindowUnMaximise | WindowUnMaximise | Window unmaximised | + | WindowUnMaximize | WindowUnMaximise | Window unmaximised | | WindowZoomIn | WindowZoomIn | Window zoomed in | | WindowZoomOut | WindowZoomOut | Window zoomed out | | WindowZoomReset | WindowZoomReset | Window zoom reset | diff --git a/docs/src/content/docs/learn/systray.mdx b/docs/src/content/docs/learn/systray.mdx new file mode 100644 index 000000000..c0caded1d --- /dev/null +++ b/docs/src/content/docs/learn/systray.mdx @@ -0,0 +1,212 @@ +--- +title: System Tray +description: Learn how to create and use system tray icons in Wails +--- +import {Badge} from '@astrojs/starlight/components'; + +## Introduction + +The system tray (also known as the notification area) is a section of the desktop environment where applications can display icons and menus. In Wails, you can easily add a system tray icon to your application with full control over its appearance and behavior. + +## Basic Usage + +To create a basic system tray icon: + +```go +app := application.New(options) +systray := app.NewSystemTray() +systray.SetLabel("My App") +systray.SetIcon(iconBytes) +systray.Run() +``` + +## Setting the Icon + +The system tray icon can be set using embedded image files. First, import the `embed` package and declare your icon files: + +```go +import "embed" + +//go:embed assets/icon.png assets/icon-dark.png +var iconFS embed.FS +``` + +Then read and set the icons: + +```go +// Read icon data +iconBytes, _ := iconFS.ReadFile("assets/icon.png") +darkModeIconBytes, _ := iconFS.ReadFile("assets/icon-dark.png") + +// Set icons +systray.SetIcon(iconBytes) +systray.SetDarkModeIcon(darkModeIconBytes) +``` + +Supported image formats include PNG and JPEG. For best results, use icons with appropriate sizes: +- Windows: 16x16 or 32x32 pixels +- macOS: 18x18 to 128x128 pixels +- Linux: Varies by desktop environment + +On macOS, you can mark the icon as a template image for automatic dark/light mode adaptation: + +```go +systray.SetTemplateIcon(iconBytes) +``` + +For more details on creating template icons, read this [great article](https://bjango.com/articles/designingmenubarextras/). + +## Setting the Label + +You can set a text label for your system tray icon: + +```go +systray.SetLabel("My App") +``` + +The label will appear next to the icon in the system tray. On some platforms, this text may be truncated if it's too long. + +## Adding a Menu + +You can add a menu to your system tray icon: + +```go +menu := application.NewMenu() +menu.Add("Open").OnClick(func() { + // Handle click +}) +menu.Add("Quit").OnClick(func() { + app.Quit() +}) + +systray.SetMenu(menu) +``` + +## Attaching a Window + +You can attach a window to a system tray icon to gain a number of desirable features: +- The attached window will start hidden +- Left-clicking on the system tray icon will toggle the visibility of the attached window +- Right-clicking on the system tray icon will show the system tray menu, if given + +Here's a complete example: + +```go +app := application.New() + +// Create system tray +systray := app.NewSystemTray() +systray.SetLabel("My App") + +// Create a window +window := app.NewWebviewWindow() + +// Attach the window to the system tray +systray.AttachWindow(window) + +// Optional: Set window offset from tray icon +systray.WindowOffset(10) + +// Optional: Set debounce time for window show/hide +systray.WindowDebounce(200 * time.Millisecond) + +// Add a menu (optional) +menu := application.NewMenu() +menu.Add("Open").OnClick(func() { + window.Show() +}) +menu.Add("Quit").OnClick(func() { + app.Quit() +}) +systray.SetMenu(menu) + +systray.Run() +``` + +## Icon Position + +On macOS, you can control the position of the system tray icon relative to other icons: + +```go +systray.SetIconPosition(application.IconPositionRight) +``` + +Available positions: +- `NSImageNone` +- `NSImageOnly` +- `NSImageLeft` +- `NSImageRight` +- `NSImageBelow` +- `NSImageAbove` +- `NSImageOverlaps` +- `NSImageLeading` +- `NSImageTrailing` + +## Destroying the System Tray + +When you're done with the system tray, you should destroy it to release resources: + +```go +systray.Destroy() +``` + +## Platform Considerations + +- **macOS**: Icons support template images for automatic dark/light mode +- **Windows**: Icons should be 16x16 or 32x32 pixels +- **Linux**: Uses the StatusNotifierItem specification + +## Examples + +Explore these examples for more advanced usage: + +- [Basic System Tray](/examples/systray-basic) +- [System Tray with Menu](/examples/systray-menu) +- [Custom System Tray](/examples/systray-custom) + +## API Reference + +### Core Methods +| Method | Description | +|--------------------------------|--------------------------------------------| +| `NewSystemTray()` | Creates a new system tray instance | +| `Run()` | Starts the system tray | +| `SetLabel(label string)` | Sets the text label | +| `SetIcon(icon []byte)` | Sets the icon image | +| `SetDarkModeIcon(icon []byte)` | Sets the dark mode variant of the icon | +| `SetTemplateIcon(icon []byte)` | Marks the icon as a template image (macOS) | +| `SetIconPosition(position int)`| Sets the icon position (macOS) | +| `Destroy()` | Destroys the system tray | + +### Menu Management +| Method | Description | +|-----------------------|----------------------------------| +| `SetMenu(menu *Menu)` | Attaches a menu to the tray icon | +| `OpenMenu()` | Programmatically opens the menu | + +### Event Handlers +| Method | Description | +|--------------------------------------|-----------------------------------| +| `OnClick(handler func())` | Handles left-click events | +| `OnRightClick(handler func())` | Handles right-click events | +| `OnDoubleClick(handler func())` | Handles left-double-click events | +| `OnRightDoubleClick(handler func())` | Handles right-double-click events | +| `OnMouseEnter(handler func())` | Handles mouse enter events | +| `OnMouseLeave(handler func())` | Handles mouse leave events | + +### Window Attachment + +| Method | Description | +|------------------------------------------|---------------------------------------------| +| `AttachWindow(window *WebviewWindow)` | Associates a window with the tray icon | +| `WindowOffset(offset int)` | Sets the offset between the tray and window | +| `WindowDebounce(debounce time.Duration)` | Sets the debounce time for window show/hide | + +### Visibility Control + +| Method | Description | +|----------|-----------------------------| +| `Show()` | Makes the tray icon visible | +| `Hide()` | Hides the tray icon | + +See the [SystemTray API Reference](/api/systemtray) for complete documentation. diff --git a/v3/examples/systray-menu/main.go b/v3/examples/systray-menu/main.go index 99c5164da..f3a6e6408 100644 --- a/v3/examples/systray-menu/main.go +++ b/v3/examples/systray-menu/main.go @@ -5,6 +5,7 @@ import ( "github.com/wailsapp/wails/v3/pkg/events" "log" "runtime" + "time" "github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/icons" @@ -90,7 +91,12 @@ func main() { myMenu.AddRadio("Radio 1", true).OnClick(radioCallback) myMenu.AddRadio("Radio 2", false).OnClick(radioCallback) myMenu.AddRadio("Radio 3", false).OnClick(radioCallback) - + myMenu.AddSeparator() + myMenu.Add("Hide System tray for 3 seconds...").OnClick(func(ctx *application.Context) { + systemTray.Hide() + time.Sleep(3 * time.Second) + systemTray.Show() + }) myMenu.AddSeparator() myMenu.Add("Quit").OnClick(func(ctx *application.Context) { app.Quit() diff --git a/v3/pkg/application/application.go b/v3/pkg/application/application.go index 6bbcb745f..3f0f7a48f 100644 --- a/v3/pkg/application/application.go +++ b/v3/pkg/application/application.go @@ -799,6 +799,14 @@ func (a *App) OnShutdown(f func()) { a.shutdownTasks = append(a.shutdownTasks, f) } +func (a *App) destroySystemTray(tray *SystemTray) { + // Remove the system tray from the a.systemTrays map + a.systemTraysLock.Lock() + delete(a.systemTrays, tray.id) + a.systemTraysLock.Unlock() + tray.destroy() +} + func (a *App) cleanup() { if a.performingShutdown { return diff --git a/v3/pkg/application/systemtray.go b/v3/pkg/application/systemtray.go index 2df93b64a..d7a5b5716 100644 --- a/v3/pkg/application/systemtray.go +++ b/v3/pkg/application/systemtray.go @@ -28,7 +28,7 @@ type systemTrayImpl interface { run() setIcon(icon []byte) setMenu(menu *Menu) - setIconPosition(position int) + setIconPosition(position IconPosition) setTemplateIcon(icon []byte) destroy() setDarkModeIcon(icon []byte) @@ -36,10 +36,8 @@ type systemTrayImpl interface { getScreen() (*Screen, error) positionWindow(window *WebviewWindow, offset int) error openMenu() -} - -type PositionOptions struct { - Buffer int + Show() + Hide() } type SystemTray struct { @@ -47,7 +45,7 @@ type SystemTray struct { label string icon []byte darkModeIcon []byte - iconPosition int + iconPosition IconPosition clickHandler func() rightClickHandler func() @@ -162,7 +160,7 @@ func (s *SystemTray) SetMenu(menu *Menu) *SystemTray { return s } -func (s *SystemTray) SetIconPosition(iconPosition int) *SystemTray { +func (s *SystemTray) SetIconPosition(iconPosition IconPosition) *SystemTray { if s.impl == nil { s.iconPosition = iconPosition } else { @@ -185,6 +183,10 @@ func (s *SystemTray) SetTemplateIcon(icon []byte) *SystemTray { return s } +func (s *SystemTray) Destroy() { + globalApplication.destroySystemTray(s) +} + func (s *SystemTray) destroy() { if s.impl == nil { return @@ -222,6 +224,24 @@ func (s *SystemTray) OnMouseLeave(handler func()) *SystemTray { return s } +func (s *SystemTray) Show() { + if s.impl == nil { + return + } + InvokeSync(func() { + s.impl.Show() + }) +} + +func (s *SystemTray) Hide() { + if s.impl == nil { + return + } + InvokeSync(func() { + s.impl.Hide() + }) +} + type WindowAttachConfig struct { // Window is the window to attach to the system tray. If it's null, the request to attach will be ignored. Window *WebviewWindow diff --git a/v3/pkg/application/systemtray_darwin.go b/v3/pkg/application/systemtray_darwin.go index 07e3748f2..0d60940a1 100644 --- a/v3/pkg/application/systemtray_darwin.go +++ b/v3/pkg/application/systemtray_darwin.go @@ -9,6 +9,24 @@ package application #include "Cocoa/Cocoa.h" #include "menuitem_darwin.h" #include "systemtray_darwin.h" + +// Show the system tray icon +static void systemTrayShow(void* nsStatusItem) { + dispatch_async(dispatch_get_main_queue(), ^{ + // Get the NSStatusItem + NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem; + [statusItem setVisible:YES]; + }); +} + +// Hide the system tray icon +static void systemTrayHide(void* nsStatusItem) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem; + [statusItem setVisible:NO]; + }); +} + */ import "C" import ( @@ -27,12 +45,26 @@ type macosSystemTray struct { nsStatusItem unsafe.Pointer nsImage unsafe.Pointer nsMenu unsafe.Pointer - iconPosition int + iconPosition IconPosition isTemplateIcon bool parent *SystemTray lastClickedScreen unsafe.Pointer } +func (s *macosSystemTray) Show() { + if s.nsStatusItem == nil { + return + } + C.systemTrayShow(s.nsStatusItem) +} + +func (s *macosSystemTray) Hide() { + if s.nsStatusItem == nil { + return + } + C.systemTrayHide(s.nsStatusItem) +} + func (s *macosSystemTray) openMenu() { if s.nsMenu == nil { return @@ -61,7 +93,7 @@ func systrayClickCallback(id C.long, buttonID C.int) { systemTray.processClick(button(buttonID)) } -func (s *macosSystemTray) setIconPosition(position int) { +func (s *macosSystemTray) setIconPosition(position IconPosition) { s.iconPosition = position } diff --git a/v3/pkg/application/systemtray_linux.go b/v3/pkg/application/systemtray_linux.go index 10908e7e0..ae89c7e78 100644 --- a/v3/pkg/application/systemtray_linux.go +++ b/v3/pkg/application/systemtray_linux.go @@ -724,6 +724,16 @@ func (s *linuxSystemTray) SecondaryActivate(x int32, y int32) (err *dbus.Error) return } +// Show is a no-op for Linux +func (s *linuxSystemTray) Show() { + // No-op +} + +// Hide is a no-op for Linux +func (s *linuxSystemTray) Hide() { + // No-op +} + // tooltip is our data for a tooltip property. // Param names need to match the generated code... type tooltip = struct { diff --git a/v3/pkg/application/systemtray_windows.go b/v3/pkg/application/systemtray_windows.go index 59a1d3dd7..3f2ed89d6 100644 --- a/v3/pkg/application/systemtray_windows.go +++ b/v3/pkg/application/systemtray_windows.go @@ -394,3 +394,11 @@ func (s *windowsSystemTray) destroy() { globalApplication.debug(syscall.GetLastError().Error()) } } + +func (s *windowsSystemTray) Show() { + // No-op +} + +func (s *windowsSystemTray) Hide() { + // No-op +}