mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
Initial Windows systray support
This commit is contained in:
parent
03dd514768
commit
bf088ea19c
17 changed files with 1009 additions and 1 deletions
9
v2/internal/frontend/options/frontend.go
Normal file
9
v2/internal/frontend/options/frontend.go
Normal 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
|
||||
}
|
||||
25
v2/internal/platform/systray.go
Normal file
25
v2/internal/platform/systray.go
Normal 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())
|
||||
}
|
||||
53
v2/internal/platform/systray/menu.go
Normal file
53
v2/internal/platform/systray/menu.go
Normal 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
|
||||
}
|
||||
352
v2/internal/platform/systray/windows.go
Normal file
352
v2/internal/platform/systray/windows.go
Normal 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
|
||||
}
|
||||
155
v2/internal/platform/win32/consts.go
Normal file
155
v2/internal/platform/win32/consts.go
Normal 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
|
||||
}
|
||||
9
v2/internal/platform/win32/cursor.go
Normal file
9
v2/internal/platform/win32/cursor.go
Normal 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
|
||||
}
|
||||
39
v2/internal/platform/win32/icon.go
Normal file
39
v2/internal/platform/win32/icon.go
Normal 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
|
||||
}
|
||||
29
v2/internal/platform/win32/menu.go
Normal file
29
v2/internal/platform/win32/menu.go
Normal 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
|
||||
}
|
||||
54
v2/internal/platform/win32/structs.go
Normal file
54
v2/internal/platform/win32/structs.go
Normal 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
|
||||
}
|
||||
19
v2/internal/platform/win32/theme.go
Normal file
19
v2/internal/platform/win32/theme.go
Normal 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
|
||||
}
|
||||
132
v2/internal/platform/win32/window.go
Normal file
132
v2/internal/platform/win32/window.go
Normal 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
|
||||
}
|
||||
7
v2/internal/platform/win32/winres.go
Normal file
7
v2/internal/platform/win32/winres.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package win32
|
||||
|
||||
import "unsafe"
|
||||
|
||||
func MakeIntResource(id uint16) *uint16 {
|
||||
return (*uint16)(unsafe.Pointer(uintptr(id)))
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
9
v2/pkg/application/events.go
Normal file
9
v2/pkg/application/events.go
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
package application
|
||||
|
||||
type EventType int
|
||||
|
||||
const (
|
||||
StartUp EventType = iota
|
||||
ShutDown
|
||||
DomReady
|
||||
)
|
||||
66
v2/pkg/application/systray.go
Normal file
66
v2/pkg/application/systray.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue