[v3 windows] Position window above systray

This commit is contained in:
Lea Anthony
2023-06-30 21:17:50 +10:00
parent c27366a270
commit 3c81fb06e8
10 changed files with 359 additions and 20 deletions
+11 -1
View File
@@ -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()
})
+16
View File
@@ -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
+108
View File
@@ -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<NSScreen *> *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 {
+12
View File
@@ -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 {
+101
View File
@@ -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)
}
+19 -8
View File
@@ -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 {
+10
View File
@@ -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
}
+54 -11
View File
@@ -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
}
+23
View File
@@ -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
+5
View File
@@ -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)),