Initial Windows systray support

This commit is contained in:
Lea Anthony 2022-09-30 08:09:37 +10:00
commit bf088ea19c
No known key found for this signature in database
GPG key ID: 33DAF7BB90A58405
17 changed files with 1009 additions and 1 deletions

View file

@ -0,0 +1,9 @@
package options
import "github.com/wailsapp/wails/v2/pkg/options"
// Frontend contains options for creating the Frontend
type Frontend struct {
options.App
HasMainWindow bool
}

View file

@ -0,0 +1,25 @@
package platform
import (
"github.com/wailsapp/wails/v2/internal/platform/systray"
"github.com/wailsapp/wails/v2/pkg/menu"
)
import "github.com/samber/lo"
type SysTray interface {
// SetTitle sets the title of the tray menu
SetTitle(title string)
SetIcons(lightModeIcon []byte, darkModeIcon []byte) error
SetTooltip(tooltip string) error
Show() error
Hide() error
Run() error
Close()
AppendMenu(label string, callback menu.Callback)
AppendMenuItem(item *menu.MenuItem)
AppendSeparator()
}
func NewSysTray() SysTray {
return lo.Must(systray.New())
}

View file

@ -0,0 +1,53 @@
package systray
import (
"errors"
"github.com/wailsapp/wails/v2/internal/platform/win32"
"github.com/wailsapp/wails/v2/pkg/menu"
)
func displayMenu(hwnd win32.HWND, menuItems []*menu.MenuItem) error {
popupMenu := win32.CreatePopupMenu()
for index, item := range menuItems {
var ret bool
itemID := win32.MenuItemMsgID + index
flags := win32.MF_STRING
if item.Disabled {
flags = flags | win32.MF_GRAYED
}
if item.Checked {
flags = flags | win32.MF_CHECKED
}
//if item.BarBreak {
// flags = flags | win32.MF_MENUBARBREAK
//}
if item.IsSeparator() {
flags = flags | win32.MF_SEPARATOR
}
ret = win32.AppendMenu(popupMenu, uintptr(flags), uintptr(itemID), item.Label)
if ret == false {
return errors.New("AppendMenu failed")
}
}
x, y, ok := win32.GetCursorPos()
if ok == false {
return errors.New("GetCursorPos failed")
}
if win32.SetForegroundWindow(hwnd) == false {
return errors.New("SetForegroundWindow failed")
}
if win32.TrackPopupMenu(popupMenu, win32.TPM_LEFTALIGN, x, y-5, hwnd) == false {
return errors.New("TrackPopupMenu failed")
}
if win32.PostMessage(hwnd, win32.WM_NULL, 0, 0) == 0 {
return errors.New("PostMessage failed")
}
return nil
}

View file

@ -0,0 +1,352 @@
/*
* Based on code originally from https://github.com/tadvi/systray. Copyright (C) 2019 The Systray Authors. All Rights Reserved.
*/
package systray
import (
"errors"
"github.com/wailsapp/wails/v2/internal/platform/win32"
"github.com/wailsapp/wails/v2/pkg/menu"
"golang.org/x/sys/windows"
"syscall"
"unsafe"
)
var (
user32 = syscall.MustLoadDLL("user32.dll")
DefWindowProc = user32.MustFindProc("DefWindowProcW")
RegisterClassEx = user32.MustFindProc("RegisterClassExW")
CreateWindowEx = user32.MustFindProc("CreateWindowExW")
)
type Systray struct {
id uint32
hwnd win32.HWND
hinst win32.HINSTANCE
lclick func()
rclick func()
lightModeIcon win32.HICON
darkModeIcon win32.HICON
Menu []*menu.MenuItem
quit chan struct{}
}
func (p *Systray) Close() {
err := p.Stop()
if err != nil {
println(err.Error())
}
}
// SetTitle is unused on Windows
func (p *Systray) SetTitle(_ string) {}
func New() (*Systray, error) {
ni := &Systray{lclick: func() {}, rclick: func() {}}
MainClassName := "WailsSystray"
ni.hinst, _ = RegisterWindow(MainClassName, ni.WinProc)
mhwnd := win32.CreateWindowEx(
win32.WS_EX_CONTROLPARENT,
win32.MustStringToUTF16Ptr(MainClassName),
win32.MustStringToUTF16Ptr(""),
win32.WS_OVERLAPPEDWINDOW|win32.WS_CLIPSIBLINGS,
win32.CW_USEDEFAULT,
win32.CW_USEDEFAULT,
win32.CW_USEDEFAULT,
win32.CW_USEDEFAULT,
0,
0,
0,
unsafe.Pointer(nil))
if mhwnd == 0 {
return nil, errors.New("create main win failed")
}
NotifyIconClassName := "NotifyIconForm"
_, err := RegisterWindow(NotifyIconClassName, ni.WinProc)
if err != nil {
return nil, err
}
hwnd, _, _ := CreateWindowEx.Call(
0,
uintptr(unsafe.Pointer(win32.MustStringToUTF16Ptr(NotifyIconClassName))),
0,
0,
0,
0,
0,
0,
uintptr(win32.HWND_MESSAGE),
0,
0,
0)
if hwnd == 0 {
return nil, errors.New("create notify win failed")
}
ni.hwnd = win32.HWND(hwnd) // Important to keep this inside struct.
nid := win32.NOTIFYICONDATA{
HWnd: win32.HWND(hwnd),
UFlags: win32.NIF_MESSAGE | win32.NIF_STATE,
DwState: win32.NIS_HIDDEN,
DwStateMask: win32.NIS_HIDDEN,
UCallbackMessage: win32.NotifyIconMessageId,
}
nid.CbSize = uint32(unsafe.Sizeof(nid))
ret := win32.ShellNotifyIcon(win32.NIM_ADD, &nid)
if ret == 0 {
return nil, errors.New("shell notify create failed")
}
nid.UVersion = win32.NOTIFYICON_VERSION
ret = win32.ShellNotifyIcon(win32.NIM_SETVERSION, &nid)
if ret == 0 {
return nil, errors.New("shell notify version failed")
}
ni.id = nid.UID
return ni, nil
}
func (p *Systray) HWND() win32.HWND {
return p.hwnd
}
// AppendMenu add menu item.
func (p *Systray) AppendMenu(label string, onclick menu.Callback) {
p.Menu = append(p.Menu, &menu.MenuItem{Type: menu.TextType, Label: label, Click: onclick})
}
// AppendMenuItem add menu item.
func (p *Systray) AppendMenuItem(item *menu.MenuItem) {
p.Menu = append(p.Menu, item)
}
// AppendSeparator to the menu.
func (p *Systray) AppendSeparator() {
p.Menu = append(p.Menu, menu.Separator())
}
func (p *Systray) Stop() error {
nid := p.newNotifyIconData()
ret := win32.ShellNotifyIcon(win32.NIM_DELETE, &nid)
if ret == 0 {
return errors.New("shell notify delete failed")
}
return nil
}
func (p *Systray) Click(fn func()) {
p.lclick = fn
}
func (p *Systray) OnRightClick(fn func()) {
p.rclick = fn
}
func (p *Systray) SetTooltip(tooltip string) error {
nid := p.newNotifyIconData()
nid.UFlags = win32.NIF_TIP
copy(nid.SzTip[:], win32.MustUTF16FromString(tooltip))
ret := win32.ShellNotifyIcon(win32.NIM_MODIFY, &nid)
if ret == 0 {
return errors.New("shell notify tooltip failed")
}
return nil
}
func (p *Systray) ShowMessage(title, msg string, bigIcon bool) error {
nid := p.newNotifyIconData()
if bigIcon == true {
nid.DwInfoFlags = win32.NIIF_USER
}
nid.CbSize = uint32(unsafe.Sizeof(nid))
nid.UFlags = win32.NIF_INFO
copy(nid.SzInfoTitle[:], win32.MustUTF16FromString(title))
copy(nid.SzInfo[:], win32.MustUTF16FromString(msg))
ret := win32.ShellNotifyIcon(win32.NIM_MODIFY, &nid)
if ret == 0 {
return errors.New("shell notify tooltip failed")
}
return nil
}
func (p *Systray) newNotifyIconData() win32.NOTIFYICONDATA {
nid := win32.NOTIFYICONDATA{
UID: p.id,
HWnd: p.hwnd,
}
nid.CbSize = uint32(unsafe.Sizeof(nid))
return nid
}
func (p *Systray) Show() error {
return p.setVisible(true)
}
func (p *Systray) Hide() error {
return p.setVisible(false)
}
func (p *Systray) setVisible(visible bool) error {
nid := p.newNotifyIconData()
nid.UFlags = win32.NIF_STATE
nid.DwStateMask = win32.NIS_HIDDEN
if !visible {
nid.DwState = win32.NIS_HIDDEN
}
ret := win32.ShellNotifyIcon(win32.NIM_MODIFY, &nid)
if ret == 0 {
return errors.New("shell notify tooltip failed")
}
return nil
}
func (p *Systray) SetIcons(lightModeIcon []byte, darkModeIcon []byte) error {
p.lightModeIcon = p.getIcon(lightModeIcon)
p.darkModeIcon = p.getIcon(darkModeIcon)
return p.updateIcon()
}
func (p *Systray) getIcon(icon []byte) win32.HICON {
result, err := win32.CreateHIconFromPNG(icon)
if err != nil {
result = win32.LoadIconWithResourceID(0, uintptr(win32.IDI_APPLICATION))
}
return result
}
func (p *Systray) setIcon(hicon win32.HICON) error {
nid := p.newNotifyIconData()
nid.UFlags = win32.NIF_ICON
if hicon == 0 {
nid.HIcon = 0
} else {
nid.HIcon = hicon
}
ret := win32.ShellNotifyIcon(win32.NIM_MODIFY, &nid)
if ret == 0 {
return errors.New("shell notify icon failed")
}
return nil
}
func (p *Systray) WinProc(hwnd win32.HWND, msg uint32, wparam, lparam uintptr) uintptr {
switch msg {
case win32.NotifyIconMessageId:
if lparam == win32.WM_LBUTTONUP {
p.lclick()
if len(p.Menu) > 0 {
err := displayMenu(p.hwnd, p.Menu)
if err != nil {
return 0
}
}
} else if lparam == win32.WM_RBUTTONUP {
p.rclick()
if len(p.Menu) > 0 {
err := displayMenu(p.hwnd, p.Menu)
if err != nil {
return 0
}
}
}
case win32.WM_SETTINGCHANGE:
settingChanged := windows.UTF16PtrToString((*uint16)(unsafe.Pointer(lparam)))
if settingChanged == "ImmersiveColorSet" {
err := p.updateIcon()
if err != nil {
println("update icon failed", err.Error())
}
}
return 0
case win32.WM_COMMAND:
cmdMsgID := int(wparam & 0xffff)
switch cmdMsgID {
default:
if cmdMsgID >= win32.MenuItemMsgID && cmdMsgID < (win32.MenuItemMsgID+len(p.Menu)) {
itemIndex := cmdMsgID - win32.MenuItemMsgID
menuItem := p.Menu[itemIndex]
menuItem.Click(nil)
}
}
}
result, _, _ := DefWindowProc.Call(uintptr(hwnd), uintptr(msg), wparam, lparam)
return result
}
func (p *Systray) Run() error {
var msg win32.MSG
for {
rt := win32.GetMessage(&msg)
switch int(rt) {
case 0:
println("Quitting Run()")
return nil
case -1:
return errors.New("run failed")
}
if win32.IsDialogMessage(p.hwnd, &msg) == 0 {
win32.TranslateMessage(&msg)
win32.DispatchMessage(&msg)
}
}
}
func (p *Systray) updateIcon() error {
if win32.IsCurrentlyDarkMode() {
return p.setIcon(p.darkModeIcon)
}
return p.setIcon(p.lightModeIcon)
}
func RegisterWindow(name string, proc win32.WindowProc) (win32.HINSTANCE, error) {
hinst := win32.GetModuleHandle(0)
if hinst == 0 {
return 0, errors.New("get module handle failed")
}
hicon := win32.LoadIconWithResourceID(0, uintptr(win32.IDI_APPLICATION))
if hicon == 0 {
return 0, errors.New("load icon failed")
}
hcursor := win32.LoadCursorWithResourceID(0, uintptr(win32.IDC_ARROW))
if hcursor == 0 {
return 0, errors.New("load cursor failed")
}
hi := win32.HINSTANCE(hinst)
var wc win32.WNDCLASSEX
wc.CbSize = uint32(unsafe.Sizeof(wc))
wc.LpfnWndProc = syscall.NewCallback(proc)
wc.HInstance = win32.HINSTANCE(hinst)
wc.HIcon = hicon
wc.HCursor = hcursor
wc.HbrBackground = win32.COLOR_BTNFACE + 1
wc.LpszClassName = win32.MustStringToUTF16Ptr(name)
atom, _, _ := RegisterClassEx.Call(uintptr(unsafe.Pointer(&wc)))
if atom == 0 {
return 0, errors.New("register class failed")
}
return hi, nil
}

View file

@ -0,0 +1,155 @@
package win32
import (
"syscall"
"unsafe"
)
var (
modKernel32 = syscall.NewLazyDLL("kernel32.dll")
procGetModuleHandle = modKernel32.NewProc("GetModuleHandleW")
moduser32 = syscall.NewLazyDLL("user32.dll")
procRegisterClassEx = moduser32.NewProc("RegisterClassExW")
procLoadIcon = moduser32.NewProc("LoadIconW")
procLoadCursor = moduser32.NewProc("LoadCursorW")
procCreateWindowEx = moduser32.NewProc("CreateWindowExW")
procPostMessage = moduser32.NewProc("PostMessageW")
procGetCursorPos = moduser32.NewProc("GetCursorPos")
procSetForegroundWindow = moduser32.NewProc("SetForegroundWindow")
procCreatePopupMenu = moduser32.NewProc("CreatePopupMenu")
procTrackPopupMenu = moduser32.NewProc("TrackPopupMenu")
procAppendMenuW = moduser32.NewProc("AppendMenuW")
procCreateIconFromResourceEx = moduser32.NewProc("CreateIconFromResourceEx")
procGetMessageW = moduser32.NewProc("GetMessageW")
procIsDialogMessage = moduser32.NewProc("IsDialogMessageW")
procTranslateMessage = moduser32.NewProc("TranslateMessage")
procDispatchMessage = moduser32.NewProc("DispatchMessageW")
modshell32 = syscall.NewLazyDLL("shell32.dll")
procShellNotifyIcon = modshell32.NewProc("Shell_NotifyIconW")
)
type HANDLE uintptr
type HINSTANCE HANDLE
type HICON HANDLE
type HCURSOR HANDLE
type HBRUSH HANDLE
type HWND HANDLE
type HMENU HANDLE
type ATOM uint16
const (
WM_LBUTTONUP = 0x0202
WM_LBUTTONDBLCLK = 0x0203
WM_RBUTTONUP = 0x0205
WM_USER = 0x0400
WM_TRAYICON = WM_USER + 69
WM_SETTINGCHANGE = 0x001A
WS_EX_APPWINDOW = 0x00040000
WS_OVERLAPPEDWINDOW = 0x00000000 | 0x00C00000 | 0x00080000 | 0x00040000 | 0x00020000 | 0x00010000
CW_USEDEFAULT = 0x80000000
NIM_ADD = 0x00000000
NIM_MODIFY = 0x00000001
NIM_DELETE = 0x00000002
NIM_SETVERSION = 0x00000004
NIF_MESSAGE = 0x00000001
NIF_ICON = 0x00000002
NIF_TIP = 0x00000004
NIF_STATE = 0x00000008
NIF_INFO = 0x00000010
NIS_HIDDEN = 0x00000001
NIIF_NONE = 0x00000000
NIIF_INFO = 0x00000001
NIIF_WARNING = 0x00000002
NIIF_ERROR = 0x00000003
NIIF_USER = 0x00000004
NIIF_NOSOUND = 0x00000010
NIIF_LARGE_ICON = 0x00000020
NIIF_RESPECT_QUIET_TIME = 0x00000080
NIIF_ICON_MASK = 0x0000000F
IMAGE_BITMAP = 0
IMAGE_ICON = 1
LR_LOADFROMFILE = 0x00000010
LR_DEFAULTSIZE = 0x00000040
IDC_ARROW = 32512
COLOR_WINDOW = 5
COLOR_BTNFACE = 15
GWLP_USERDATA = -21
WS_CLIPSIBLINGS = 0x04000000
WS_EX_CONTROLPARENT = 0x00010000
HWND_MESSAGE = ^HWND(2)
NOTIFYICON_VERSION = 4
IDI_APPLICATION = 32512
WM_APP = 32768
WM_COMMAND = 273
MenuItemMsgID = WM_APP + 1024
NotifyIconMessageId = WM_APP + iota
MF_STRING = 0x00000000
MF_ENABLED = 0x00000000
MF_GRAYED = 0x00000001
MF_DISABLED = 0x00000002
MF_SEPARATOR = 0x00000800
MF_CHECKED = 0x00000008
MF_MENUBARBREAK = 0x00000020
TPM_LEFTALIGN = 0x0000
WM_NULL = 0
CS_VREDRAW = 0x0001
CS_HREDRAW = 0x0002
)
type WindowProc func(hwnd HWND, msg uint32, wparam, lparam uintptr) uintptr
func GetModuleHandle(value uintptr) uintptr {
result, _, _ := procGetModuleHandle.Call(value)
return result
}
func GetMessage(msg *MSG) uintptr {
rt, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&msg)), 0, 0, 0)
return rt
}
func PostMessage(hwnd HWND, msg uint32, wParam, lParam uintptr) uintptr {
ret, _, _ := procPostMessage.Call(
uintptr(hwnd),
uintptr(msg),
wParam,
lParam)
return ret
}
func ShellNotifyIcon(cmd uintptr, nid *NOTIFYICONDATA) uintptr {
ret, _, _ := procShellNotifyIcon.Call(cmd, uintptr(unsafe.Pointer(nid)))
return ret
}
func IsDialogMessage(hwnd HWND, msg *MSG) uintptr {
ret, _, _ := procIsDialogMessage.Call(uintptr(hwnd), uintptr(unsafe.Pointer(msg)))
return ret
}
func TranslateMessage(msg *MSG) uintptr {
ret, _, _ := procTranslateMessage.Call(uintptr(unsafe.Pointer(msg)))
return ret
}
func DispatchMessage(msg *MSG) uintptr {
ret, _, _ := procDispatchMessage.Call(uintptr(unsafe.Pointer(msg)))
return ret
}

View file

@ -0,0 +1,9 @@
package win32
import "unsafe"
func GetCursorPos() (x, y int, ok bool) {
pt := POINT{}
ret, _, _ := procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt)))
return int(pt.X), int(pt.Y), ret != 0
}

View file

@ -0,0 +1,39 @@
package win32
import (
"unsafe"
)
func CreateIconFromResourceEx(presbits uintptr, dwResSize uint32, isIcon bool, version uint32, cxDesired int, cyDesired int, flags uint) (uintptr, error) {
icon := 0
if isIcon {
icon = 1
}
r, _, err := procCreateIconFromResourceEx.Call(
presbits,
uintptr(dwResSize),
uintptr(icon),
uintptr(version),
uintptr(cxDesired),
uintptr(cyDesired),
uintptr(flags),
)
if r == 0 {
return 0, err
}
return r, nil
}
// CreateHIconFromPNG creates a HICON from a PNG file
func CreateHIconFromPNG(pngData []byte) (HICON, error) {
icon, err := CreateIconFromResourceEx(
uintptr(unsafe.Pointer(&pngData[0])),
uint32(len(pngData)),
true,
0x00030000,
0,
0,
LR_DEFAULTSIZE)
return HICON(icon), err
}

View file

@ -0,0 +1,29 @@
package win32
func CreatePopupMenu() HMENU {
ret, _, _ := procCreatePopupMenu.Call(0, 0, 0, 0)
return HMENU(ret)
}
func TrackPopupMenu(menu HMENU, flags uint, x, y int, wnd HWND) bool {
ret, _, _ := procTrackPopupMenu.Call(
uintptr(menu),
uintptr(flags),
uintptr(x),
uintptr(y),
0,
uintptr(wnd),
0,
)
return ret != 0
}
func AppendMenu(menu HMENU, flags uintptr, id uintptr, text string) bool {
ret, _, _ := procAppendMenuW.Call(
uintptr(menu),
flags,
id,
MustStringToUTF16uintptr(text),
)
return ret != 0
}

View file

@ -0,0 +1,54 @@
package win32
type NOTIFYICONDATA struct {
CbSize uint32
HWnd HWND
UID uint32
UFlags uint32
UCallbackMessage uint32
HIcon HICON
SzTip [128]uint16
DwState uint32
DwStateMask uint32
SzInfo [256]uint16
UVersion uint32
SzInfoTitle [64]uint16
DwInfoFlags uint32
GuidItem GUID
HBalloonIcon HICON
}
type GUID struct {
Data1 uint32
Data2 uint16
Data3 uint16
Data4 [8]byte
}
type WNDCLASSEX struct {
CbSize uint32
Style uint32
LpfnWndProc uintptr
CbClsExtra int32
CbWndExtra int32
HInstance HINSTANCE
HIcon HICON
HCursor HCURSOR
HbrBackground HBRUSH
LpszMenuName *uint16
LpszClassName *uint16
HIconSm HICON
}
type MSG struct {
HWnd HWND
Message uint32
WParam uintptr
LParam uintptr
Time uint32
Pt POINT
}
type POINT struct {
X, Y int32
}

View file

@ -0,0 +1,19 @@
package win32
import (
"golang.org/x/sys/windows/registry"
)
func IsCurrentlyDarkMode() bool {
key, err := registry.OpenKey(registry.CURRENT_USER, `SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize`, registry.QUERY_VALUE)
if err != nil {
return false
}
defer key.Close()
AppsUseLightTheme, _, err := key.GetIntegerValue("AppsUseLightTheme")
if err != nil {
return false
}
return AppsUseLightTheme == 0
}

View file

@ -0,0 +1,132 @@
package win32
import (
"fmt"
"github.com/samber/lo"
"syscall"
"unsafe"
)
func LoadIconWithResourceID(instance HINSTANCE, res uintptr) HICON {
ret, _, _ := procLoadIcon.Call(
uintptr(instance),
res)
return HICON(ret)
}
func LoadCursorWithResourceID(instance HINSTANCE, res uintptr) HCURSOR {
ret, _, _ := procLoadCursor.Call(
uintptr(instance),
res)
return HCURSOR(ret)
}
func RegisterClassEx(wndClassEx *WNDCLASSEX) ATOM {
ret, _, _ := procRegisterClassEx.Call(uintptr(unsafe.Pointer(wndClassEx)))
return ATOM(ret)
}
func RegisterClass(className string, wndproc uintptr, instance HINSTANCE) error {
classNamePtr, err := syscall.UTF16PtrFromString(className)
if err != nil {
return err
}
icon := LoadIconWithResourceID(instance, IDI_APPLICATION)
var wc WNDCLASSEX
wc.CbSize = uint32(unsafe.Sizeof(wc))
wc.Style = CS_HREDRAW | CS_VREDRAW
wc.LpfnWndProc = wndproc
wc.HInstance = instance
wc.HbrBackground = COLOR_WINDOW + 1
wc.HIcon = icon
wc.HCursor = LoadCursorWithResourceID(0, IDC_ARROW)
wc.LpszClassName = classNamePtr
wc.LpszMenuName = nil
wc.HIconSm = icon
if ret := RegisterClassEx(&wc); ret == 0 {
return syscall.GetLastError()
}
return nil
}
func CreateWindow(className string, instance HINSTANCE, parent HWND, exStyle, style uint) HWND {
classNamePtr := lo.Must(syscall.UTF16PtrFromString(className))
result := CreateWindowEx(
exStyle,
classNamePtr,
nil,
style,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
parent,
0,
instance,
nil)
if result == 0 {
errStr := fmt.Sprintf("Error occurred in CreateWindow(%s, %v, %d, %d)", className, parent, exStyle, style)
panic(errStr)
}
return result
}
func CreateWindowEx(exStyle uint, className, windowName *uint16,
style uint, x, y, width, height int, parent HWND, menu HMENU,
instance HINSTANCE, param unsafe.Pointer) HWND {
ret, _, _ := procCreateWindowEx.Call(
uintptr(exStyle),
uintptr(unsafe.Pointer(className)),
uintptr(unsafe.Pointer(windowName)),
uintptr(style),
uintptr(x),
uintptr(y),
uintptr(width),
uintptr(height),
uintptr(parent),
uintptr(menu),
uintptr(instance),
uintptr(param))
return HWND(ret)
}
func MustStringToUTF16Ptr(input string) *uint16 {
ret, err := syscall.UTF16PtrFromString(input)
if err != nil {
panic(err)
}
return ret
}
func MustStringToUTF16uintptr(input string) uintptr {
ret, err := syscall.UTF16PtrFromString(input)
if err != nil {
panic(err)
}
return uintptr(unsafe.Pointer(ret))
}
func MustUTF16FromString(input string) []uint16 {
ret, err := syscall.UTF16FromString(input)
if err != nil {
panic(err)
}
return ret
}
func SetForegroundWindow(wnd HWND) bool {
ret, _, _ := procSetForegroundWindow.Call(
uintptr(wnd),
)
return ret != 0
}

View file

@ -0,0 +1,7 @@
package win32
import "unsafe"
func MakeIntResource(id uint16) *uint16 {
return (*uint16)(unsafe.Pointer(uintptr(id)))
}

View file

@ -1,6 +1,7 @@
package application
import (
"context"
"github.com/wailsapp/wails/v2/internal/app"
"github.com/wailsapp/wails/v2/internal/signal"
"github.com/wailsapp/wails/v2/pkg/menu"
@ -12,6 +13,9 @@ type Application struct {
application *app.App
options *options.App
// System Trays
systemTrays []*SystemTray
// running flag
running bool
}
@ -66,10 +70,43 @@ func (a *Application) Run() error {
a.running = true
return a.application.Run()
err = a.application.Run()
a.Quit()
return err
}
// Quit will shut down the application
func (a *Application) Quit() {
for _, systray := range a.systemTrays {
systray.Close()
}
a.application.Shutdown()
}
// Bind the given struct to the application
func (a *Application) Bind(boundStruct any) {
a.options.Bind = append(a.options.Bind, boundStruct)
}
func (a *Application) On(eventType EventType, callback func()) {
c := func(ctx context.Context) {
callback()
}
switch eventType {
case StartUp:
a.options.OnStartup = c
case ShutDown:
a.options.OnShutdown = c
case DomReady:
a.options.OnDomReady = c
}
}
func (a *Application) NewSystemTray(options *options.SystemTray) *SystemTray {
systemTray := newSystemTray(options)
a.systemTrays = append(a.systemTrays, systemTray)
println("created systemTray")
return systemTray
}

View file

@ -0,0 +1,9 @@
package application
type EventType int
const (
StartUp EventType = iota
ShutDown
DomReady
)

View file

@ -0,0 +1,66 @@
package application
import (
"github.com/wailsapp/wails/v2/internal/platform"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/options"
)
// SystemTray defines a system tray!
type SystemTray struct {
title string
hidden bool
lightModeIcon []byte
darkModeIcon []byte
tooltip string
startHidden bool
// The platform specific implementation
impl platform.SysTray
}
func newSystemTray(options *options.SystemTray) *SystemTray {
return &SystemTray{
impl: platform.NewSysTray(),
title: options.Title,
lightModeIcon: options.LightModeIcon,
darkModeIcon: options.DarkModeIcon,
tooltip: options.Tooltip,
startHidden: options.StartHidden,
}
}
func (t *SystemTray) run() {
t.impl.SetTitle(t.title)
t.impl.SetIcons(t.lightModeIcon, t.darkModeIcon)
t.impl.SetTooltip(t.tooltip)
if !t.startHidden {
t.impl.Show()
}
t.impl.Run()
}
func (t *SystemTray) SetTitle(title string) {
t.title = title
t.impl.SetTitle(title)
}
func (t *SystemTray) AppendMenu(label string, callback menu.Callback) {
t.impl.AppendMenu(label, callback)
}
func (t *SystemTray) Run() error {
t.run()
return nil
}
func (t *SystemTray) Close() {
t.impl.Close()
}
func (t *SystemTray) AppendSeperator() {
t.impl.AppendSeparator()
}
func (t *SystemTray) AppendMenuItem(item *menu.MenuItem) {
t.impl.AppendMenuItem(item)
}

View file

@ -239,6 +239,10 @@ func (m *MenuItem) SetLabel(name string) {
m.Impl.SetLabel(name)
}
func (m *MenuItem) IsSeparator() bool {
return m.Type == SeparatorType
}
// Text is a helper to create basic Text menu items
func Text(label string, accelerator *keys.Accelerator, click Callback) *MenuItem {
return &MenuItem{

View file

@ -170,3 +170,12 @@ func processDragOptions(appoptions *App) {
appoptions.CSSDragProperty = html.EscapeString(appoptions.CSSDragProperty)
appoptions.CSSDragValue = html.EscapeString(appoptions.CSSDragValue)
}
// SystemTray contains options for the system tray
type SystemTray struct {
LightModeIcon []byte
DarkModeIcon []byte
Title string
Tooltip string
StartHidden bool
}