mirror of
https://github.com/wavetermdev/wails.git
synced 2026-08-05 13:53:43 -07:00
[v3 windows] Position window above systray
This commit is contained in:
@@ -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()
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)),
|
||||
|
||||
Reference in New Issue
Block a user