diff --git a/v3/examples/systray/main.go b/v3/examples/systray/main.go index ef1757a5..bbb80255 100644 --- a/v3/examples/systray/main.go +++ b/v3/examples/systray/main.go @@ -26,7 +26,12 @@ func main() { }, }) - window := app.NewWebviewWindow().Hide() + window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + Width: 500, + Height: 800, + //Frameless: true, + Hidden: true, + }) systemTray := app.NewSystemTray() if runtime.GOOS == "darwin" { @@ -67,6 +72,11 @@ func main() { systemTray.OnClick(func() { window.SetTitle(fmt.Sprintf("Clicked %d times", clickCount())) + err := systemTray.PositionWindow(window) + if err != nil { + application.InfoDialog().SetTitle("Error").SetMessage(err.Error()).Show() + return + } window.Show().Focus() }) diff --git a/v3/pkg/application/systemtray.go b/v3/pkg/application/systemtray.go index 4cb95e14..50dc1008 100644 --- a/v3/pkg/application/systemtray.go +++ b/v3/pkg/application/systemtray.go @@ -1,5 +1,9 @@ package application +import ( + "fmt" +) + type IconPosition int const ( @@ -23,6 +27,9 @@ type systemTrayImpl interface { setTemplateIcon(icon []byte) destroy() setDarkModeIcon(icon []byte) + bounds() (*Rect, error) + getScreen() (*Screen, error) + positionWindow(window *WebviewWindow) error } type SystemTray struct { @@ -74,6 +81,15 @@ func (s *SystemTray) run() { invokeSync(s.impl.run) } +func (s *SystemTray) PositionWindow(window *WebviewWindow) error { + if s.impl == nil { + return fmt.Errorf("system tray not running") + } + return invokeSyncWithError(func() error { + return s.impl.positionWindow(window) + }) +} + func (s *SystemTray) SetIcon(icon []byte) *SystemTray { if s.impl == nil { s.icon = icon diff --git a/v3/pkg/application/systemtray_darwin.go b/v3/pkg/application/systemtray_darwin.go index 5e68e312..26f8d81d 100644 --- a/v3/pkg/application/systemtray_darwin.go +++ b/v3/pkg/application/systemtray_darwin.go @@ -72,6 +72,27 @@ void systemTrayDestroy(void* nsStatusItem) { }); } +void systemTrayGetBounds(void* nsStatusItem, NSRect *rect) { + NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem; + *rect = statusItem.button.window.frame; +} + +// Get the screen for the system tray +NSScreen* getScreenForSystemTray(void* nsStatusItem) { + NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem; + NSRect frame = statusItem.button.frame; + NSArray *screens = NSScreen.screens; + NSScreen *associatedScreen = nil; + + for (NSScreen *screen in screens) { + if (NSPointInRect(frame.origin, screen.frame)) { + associatedScreen = screen; + break; + } + } + return associatedScreen; +} + */ import "C" import ( @@ -99,6 +120,93 @@ func (s *macosSystemTray) setMenu(menu *Menu) { s.menu = menu } +func (s *macosSystemTray) positionWindow(window *WebviewWindow) error { + + // Get the trayBounds of this system tray + trayBounds, err := s.bounds() + if err != nil { + return err + } + + // Get the current screen trayBounds + currentScreen, err := s.getScreen() + if err != nil { + return err + } + + screenBounds := currentScreen.Bounds + + // Determine which quadrant of the screen the system tray is in + // ---------- + // | 1 | 2 | + // ---------- + // | 3 | 4 | + // ---------- + quadrant := 4 + if trayBounds.X < screenBounds.Width/2 { + quadrant -= 1 + } + if trayBounds.Y < screenBounds.Height/2 { + quadrant -= 2 + } + + // Get the center height of the window + windowWidthCenter := window.Width() / 2 + // Get the center height of the system tray + systemTrayWidthCenter := trayBounds.Width / 2 + + // Position the window based on the quadrant + // It will be centered on the system tray and if it goes off-screen it will be moved back on screen + switch quadrant { + case 1: + // The X will be 0 and the Y will be the system tray Y + // Center the window on the system tray + window.SetRelativePosition(0, trayBounds.Y) + case 2: + // The Y will be 0 and the X will make the center of the window line up with the center of the system tray + windowX := trayBounds.X + systemTrayWidthCenter - windowWidthCenter + // If the end of the window goes off-screen, move it back enough to be on screen + if windowX+window.Width() > screenBounds.Width { + windowX = screenBounds.Width - window.Width() + } + window.SetRelativePosition(windowX, 0) + case 3: + // The X will be 0 and the Y will be the system tray Y - the height of the window + windowY := trayBounds.Y - window.Height() + // If the end of the window goes off-screen, move it back enough to be on screen + if windowY < 0 { + windowY = 0 + } + window.SetRelativePosition(0, windowY) + case 4: + // The Y will be 0 and the X will make the center of the window line up with the center of the system tray - the height of the window + windowX := trayBounds.X + systemTrayWidthCenter - windowWidthCenter + windowY := trayBounds.Y - window.Height() + // If the end of the window goes off-screen, move it back enough to be on screen + if windowX+window.Width() > screenBounds.Width { + windowX = screenBounds.Width - window.Width() + } + window.SetRelativePosition(windowX, windowY) + } + return nil +} + +func (s *macosSystemTray) getScreen() (*Screen, error) { + cScreen := C.getScreenForSystemTray(s.nsStatusItem) + return cScreenToScreen(cScreen), nil +} + +func (s *macosSystemTray) bounds() (*Rect, error) { + var rect C.NSRect + rect = C.systemTrayGetBounds(s.nsStatusItem) + return &Rect{ + X: int(rect.origin.x), + Y: int(rect.origin.y), + Width: int(rect.size.width), + Height: int(rect.size.height), + }, nil +} + func (s *macosSystemTray) run() { globalApplication.dispatchOnMainThread(func() { if s.nsStatusItem != nil { diff --git a/v3/pkg/application/systemtray_linux.go b/v3/pkg/application/systemtray_linux.go index 8cc74460..a2df18f1 100644 --- a/v3/pkg/application/systemtray_linux.go +++ b/v3/pkg/application/systemtray_linux.go @@ -20,6 +20,18 @@ func (s *linuxSystemTray) setMenu(menu *Menu) { s.menu = menu } +func (s *linuxSystemTray) positionWindow(window *WebviewWindow) error { + panic("not implemented") +} + +func (s *linuxSystemTray) getScreen() (*Screen, error) { + panic("not implemented") +} + +func (s *linuxSystemTray) bounds() (*Rect, error) { + panic("not implemented") +} + func (s *linuxSystemTray) run() { globalApplication.dispatchOnMainThread(func() { // if s.nsStatusItem != nil { diff --git a/v3/pkg/application/systemtray_windows.go b/v3/pkg/application/systemtray_windows.go index 16abd727..06e48477 100644 --- a/v3/pkg/application/systemtray_windows.go +++ b/v3/pkg/application/systemtray_windows.go @@ -3,6 +3,7 @@ package application import ( + "fmt" "github.com/wailsapp/wails/v3/pkg/icons" "syscall" "unsafe" @@ -29,6 +30,106 @@ type windowsSystemTray struct { currentIcon w32.HICON } +func (s *windowsSystemTray) positionWindow(window *WebviewWindow) error { + + // Get the trayBounds of this system tray + trayBounds, err := s.bounds() + if err != nil { + return err + } + + // Get the current screen trayBounds + currentScreen, err := s.getScreen() + if err != nil { + return err + } + + screenBounds := currentScreen.WorkArea + + taskbarBounds := w32.GetTaskbarPosition() + switch taskbarBounds.UEdge { + case w32.ABE_LEFT: + if trayBounds == nil { + // Move it to the bottom left corner of the screen + window.SetRelativePosition(0, screenBounds.Height-window.Height()) + return nil + } + newHeight := trayBounds.Y - (window.Height() / 2) + if newHeight < 0 { + newHeight = 0 + } + // Move it to the top left corner of the screen + window.SetRelativePosition(0, newHeight) + case w32.ABE_TOP: + if trayBounds == nil { + // Move it to the top right corner of the screen + window.SetRelativePosition(screenBounds.Width-window.Width(), 0) + return nil + } + newWidth := trayBounds.X - (window.Width() / 2) + if newWidth > screenBounds.Width-window.Width() { + newWidth = screenBounds.Width - window.Width() + } + // Move it to the top left corner of the screen + window.SetRelativePosition(newWidth, 0) + case w32.ABE_RIGHT: + if trayBounds == nil { + // Move it to the bottom right corner of the screen + window.SetRelativePosition(screenBounds.Width-window.Width(), screenBounds.Height-window.Height()) + return nil + } + newHeight := trayBounds.Y - (window.Height() / 2) + if newHeight > screenBounds.Height-window.Height() { + newHeight = screenBounds.Height - window.Height() + } + window.SetRelativePosition(screenBounds.Width-window.Width(), newHeight) + case w32.ABE_BOTTOM: + if trayBounds == nil { + // Move it to the bottom right corner of the screen + window.SetRelativePosition(screenBounds.Width-window.Width(), screenBounds.Height-window.Height()) + return nil + } + newWidth := trayBounds.X - (window.Width() / 2) + if newWidth > screenBounds.Width-window.Width() { + newWidth = screenBounds.Width - window.Width() + } + window.SetRelativePosition(newWidth, screenBounds.Height-window.Height()) + } + return nil +} + +func (s *windowsSystemTray) bounds() (*Rect, error) { + bounds, err := w32.GetSystrayBounds(s.hwnd, s.uid) + if err != nil { + return nil, err + } + + monitor := w32.MonitorFromWindow(s.hwnd, w32.MONITOR_DEFAULTTONEAREST) + if monitor == 0 { + return nil, fmt.Errorf("failed to get monitor") + } + + // Get the taskbar rect + taskbarRect := w32.GetTaskbarPosition() + + flyoutOpen := !w32.RectInRect(bounds, &taskbarRect.Rc) + if flyoutOpen { + return nil, nil + } + + return &Rect{ + X: int(bounds.Left), + Y: int(bounds.Top), + Width: int(bounds.Right - bounds.Left), + Height: int(bounds.Bottom - bounds.Top), + }, nil +} + +func (s *windowsSystemTray) getScreen() (*Screen, error) { + // Get the screen for this systray + return getScreen(s.hwnd) +} + func (s *windowsSystemTray) setMenu(menu *Menu) { s.updateMenu(menu) } diff --git a/v3/pkg/application/webview_window_windows.go b/v3/pkg/application/webview_window_windows.go index 1ff4b011..062b840d 100644 --- a/v3/pkg/application/webview_window_windows.go +++ b/v3/pkg/application/webview_window_windows.go @@ -323,7 +323,7 @@ func (w *windowsWebviewWindow) size() (int, int) { rect := w32.GetWindowRect(w.hwnd) width := int(rect.Right - rect.Left) height := int(rect.Bottom - rect.Top) - width, height = w.scaleToDefaultDPI(width, height) + //width, height = w.scaleToDefaultDPI(width, height) return width, height } @@ -439,7 +439,7 @@ func (w *windowsWebviewWindow) setHTML(html string) { } func (w *windowsWebviewWindow) setRelativePosition(x int, y int) { - x, y = w.scaleWithWindowDPI(x, y) + //x, y = w.scaleWithWindowDPI(x, y) info := w32.GetMonitorInfoForWindow(w.hwnd) workRect := info.RcWork w32.SetWindowPos(w.hwnd, w32.HWND_TOP, int(workRect.Left)+x, int(workRect.Top)+y, 0, 0, w32.SWP_NOSIZE) @@ -565,10 +565,8 @@ func (w *windowsWebviewWindow) hide() { w32.ShowWindow(w.hwnd, w32.SW_HIDE) } -// Get the screen for the current window -func (w *windowsWebviewWindow) getScreen() (*Screen, error) { - hMonitor := w32.MonitorFromWindow(w.hwnd, w32.MONITOR_DEFAULTTONEAREST) - +func getScreen(hwnd w32.HWND) (*Screen, error) { + hMonitor := w32.MonitorFromWindow(hwnd, w32.MONITOR_DEFAULTTONEAREST) var mi w32.MONITORINFOEX mi.CbSize = uint32(unsafe.Sizeof(mi)) w32.GetMonitorInfoEx(hMonitor, &mi) @@ -603,9 +601,22 @@ func (w *windowsWebviewWindow) getScreen() (*Screen, error) { return &thisScreen, nil } +// Get the screen for the current window +func (w *windowsWebviewWindow) getScreen() (*Screen, error) { + return getScreen(w.hwnd) +} + func (w *windowsWebviewWindow) setFrameless(b bool) { - //TODO implement me - panic("implement me") + // If the window is already frameless, don't do anything + if w.parent.options.Frameless == b { + return + } + // Remove or add the frame + if b { + w32.SetWindowLong(w.hwnd, w32.GWL_STYLE, w32.WS_VISIBLE|w32.WS_POPUP) + } else { + w32.SetWindowLong(w.hwnd, w32.GWL_STYLE, w32.WS_VISIBLE|w32.WS_OVERLAPPEDWINDOW) + } } func newWindowImpl(parent *WebviewWindow) *windowsWebviewWindow { diff --git a/v3/pkg/w32/shcore.go b/v3/pkg/w32/shcore.go index 72e9aab3..39e72b91 100644 --- a/v3/pkg/w32/shcore.go +++ b/v3/pkg/w32/shcore.go @@ -27,3 +27,13 @@ func GetDPIForMonitor(hmonitor HMONITOR, dpiType MONITOR_DPI_TYPE, dpiX *UINT, d return ret } + +func GetNotificationFlyoutBounds() (*RECT, error) { + var rect RECT + res, _, err := procSystemParametersInfo.Call(SPI_GETNOTIFYWINDOWRECT, 0, uintptr(unsafe.Pointer(&rect)), 0) + if res == 0 { + _ = err + return nil, err + } + return &rect, nil +} diff --git a/v3/pkg/w32/shell32.go b/v3/pkg/w32/shell32.go index 53735628..4c213cc6 100644 --- a/v3/pkg/w32/shell32.go +++ b/v3/pkg/w32/shell32.go @@ -204,19 +204,30 @@ var ( var ( modshell32 = syscall.NewLazyDLL("shell32.dll") - procSHBrowseForFolder = modshell32.NewProc("SHBrowseForFolderW") - procSHGetPathFromIDList = modshell32.NewProc("SHGetPathFromIDListW") - procDragAcceptFiles = modshell32.NewProc("DragAcceptFiles") - procDragQueryFile = modshell32.NewProc("DragQueryFileW") - procDragQueryPoint = modshell32.NewProc("DragQueryPoint") - procDragFinish = modshell32.NewProc("DragFinish") - procShellExecute = modshell32.NewProc("ShellExecuteW") - procExtractIcon = modshell32.NewProc("ExtractIconW") - procGetSpecialFolderPath = modshell32.NewProc("SHGetSpecialFolderPathW") - procShellNotifyIcon = modshell32.NewProc("Shell_NotifyIconW") - procSHGetKnownFolderPath = modshell32.NewProc("SHGetKnownFolderPath") + procSHBrowseForFolder = modshell32.NewProc("SHBrowseForFolderW") + procSHGetPathFromIDList = modshell32.NewProc("SHGetPathFromIDListW") + procDragAcceptFiles = modshell32.NewProc("DragAcceptFiles") + procDragQueryFile = modshell32.NewProc("DragQueryFileW") + procDragQueryPoint = modshell32.NewProc("DragQueryPoint") + procDragFinish = modshell32.NewProc("DragFinish") + procShellExecute = modshell32.NewProc("ShellExecuteW") + procExtractIcon = modshell32.NewProc("ExtractIconW") + procGetSpecialFolderPath = modshell32.NewProc("SHGetSpecialFolderPathW") + procShellNotifyIcon = modshell32.NewProc("Shell_NotifyIconW") + procShellNotifyIconGetRect = modshell32.NewProc("Shell_NotifyIconGetRect") + procSHGetKnownFolderPath = modshell32.NewProc("SHGetKnownFolderPath") + procSHAppBarMessage = modshell32.NewProc("SHAppBarMessage") ) +type APPBARDATA struct { + CbSize uint32 + HWnd HWND + UCallbackMessage uint32 + UEdge uint32 + Rc RECT + LParam uintptr +} + func ShellNotifyIcon(cmd uintptr, nid *NOTIFYICONDATA) bool { ret, _, _ := procShellNotifyIcon.Call(cmd, uintptr(unsafe.Pointer(nid))) return ret == 1 @@ -370,3 +381,35 @@ func SHGetSpecialFolderPath(hwndOwner HWND, lpszPath *uint16, csidl CSIDL, fCrea return ret != 0 } + +func GetSystrayBounds(hwnd HWND, uid uint32) (*RECT, error) { + var rect RECT + identifier := NOTIFYICONIDENTIFIER{ + CbSize: uint32(unsafe.Sizeof(NOTIFYICONIDENTIFIER{})), + HWnd: hwnd, + UId: uid, + } + ret, _, _ := procShellNotifyIconGetRect.Call( + uintptr(unsafe.Pointer(&identifier)), + uintptr(unsafe.Pointer(&rect))) + + if ret != S_OK { + return nil, syscall.GetLastError() + } + + return &rect, nil +} + +// GetTaskbarPosition returns the location of the taskbar. +func GetTaskbarPosition() *APPBARDATA { + var result APPBARDATA + result.CbSize = uint32(unsafe.Sizeof(APPBARDATA{})) + ret, _, _ := procSHAppBarMessage.Call( + ABM_GETTASKBARPOS, + uintptr(unsafe.Pointer(&result))) + if ret == 0 { + return nil + } + + return &result +} diff --git a/v3/pkg/w32/typedef.go b/v3/pkg/w32/typedef.go index a60dbd44..db02dacf 100644 --- a/v3/pkg/w32/typedef.go +++ b/v3/pkg/w32/typedef.go @@ -677,6 +677,29 @@ type NOTIFYICONDATA struct { HBalloonIcon HICON } +const SPI_GETNOTIFYWINDOWRECT = 0x0040 + +// Taskbar constants +const ABM_GETTASKBARPOS = 0x00000005 +const ABM_GETSTATE = 0x00000004 +const ABM_GETAUTOHIDEBAR = 0x00000007 +const ABM_SETSTATE = 0x0000000a +const ABM_SETAUTOHIDEBAR = 0x00000008 +const ABM_WINDOWPOSCHANGED = 0x00000009 +const ABM_SETPOS = 0x00000003 + +const ABE_LEFT = 0 +const ABE_TOP = 1 +const ABE_RIGHT = 2 +const ABE_BOTTOM = 3 + +type NOTIFYICONIDENTIFIER struct { + CbSize uint32 + HWnd HWND + UId uint32 + GuidItem windows.GUID +} + // http://msdn.microsoft.com/en-us/library/windows/desktop/ms534067.aspx type GdiplusStartupInput struct { GdiplusVersion uint32 diff --git a/v3/pkg/w32/user32.go b/v3/pkg/w32/user32.go index 0afd7d37..10ad2ddd 100644 --- a/v3/pkg/w32/user32.go +++ b/v3/pkg/w32/user32.go @@ -759,6 +759,11 @@ func PtInRect(rect *RECT, x, y int) bool { return ret != 0 } +func RectInRect(rect1, rect2 *RECT) bool { + return rect1.Left >= rect2.Left && rect1.Right <= rect2.Right && + rect1.Top >= rect2.Top && rect1.Bottom <= rect2.Bottom +} + func SetRect(rect *RECT, left, top, right, bottom int) bool { ret, _, _ := procSetRect.Call( uintptr(unsafe.Pointer(rect)),