mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-20 10:59:30 +08:00
Fix macOS systray DPI scaling and offset.
Added new systray examples.
This commit is contained in:
parent
a0ff53b629
commit
beacf06c7d
2
v3/.gitignore
vendored
2
v3/.gitignore
vendored
@ -1,6 +1,6 @@
|
||||
examples/kitchensink/kitchensink
|
||||
cmd/wails3/wails
|
||||
/examples/systray/systray
|
||||
/examples/systray-menu/systray
|
||||
/examples/window/window
|
||||
/examples/dialogs/dialogs
|
||||
/examples/menu/menu
|
||||
|
16
v3/examples/systray-basic/README.md
Normal file
16
v3/examples/systray-basic/README.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Systray Basic Example
|
||||
|
||||
This example creates a simple system tray with an attached window.
|
||||
The window is hidden by default and toggled by left-clicking on the systray icon.
|
||||
The window will hide automatically when it loses focus.
|
||||
|
||||
On Windows, if the icon is in the notification flyout,
|
||||
then the window will be shown in the bottom right corner.
|
||||
|
||||
# Status
|
||||
|
||||
| Platform | Status |
|
||||
|----------|---------|
|
||||
| Mac | Working |
|
||||
| Windows | Working |
|
||||
| Linux | |
|
56
v3/examples/systray-basic/main.go
Normal file
56
v3/examples/systray-basic/main.go
Normal file
@ -0,0 +1,56 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"log"
|
||||
"runtime"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/icons"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := application.New(application.Options{
|
||||
Name: "Systray Demo",
|
||||
Description: "A demo of the Systray API",
|
||||
Assets: application.AlphaAssets,
|
||||
Mac: application.MacOptions{
|
||||
ActivationPolicy: application.ActivationPolicyAccessory,
|
||||
},
|
||||
})
|
||||
|
||||
systemTray := app.NewSystemTray()
|
||||
|
||||
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
|
||||
Width: 500,
|
||||
Height: 500,
|
||||
Name: "Systray Demo Window",
|
||||
Frameless: true,
|
||||
AlwaysOnTop: true,
|
||||
Hidden: true,
|
||||
DisableResize: true,
|
||||
ShouldClose: func(window *application.WebviewWindow) bool {
|
||||
window.Hide()
|
||||
return false
|
||||
},
|
||||
Windows: application.WindowsWindow{
|
||||
HiddenOnTaskbar: true,
|
||||
},
|
||||
KeyBindings: map[string]func(window *application.WebviewWindow){
|
||||
"F12": func(window *application.WebviewWindow) {
|
||||
systemTray.OpenMenu()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if runtime.GOOS == "darwin" {
|
||||
systemTray.SetTemplateIcon(icons.SystrayMacTemplate)
|
||||
}
|
||||
|
||||
systemTray.AttachWindow(window).WindowOffset(5)
|
||||
|
||||
err := app.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
16
v3/examples/systray-custom/README.md
Normal file
16
v3/examples/systray-custom/README.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Systray Custom Example
|
||||
|
||||
This example creates a simple system tray and uses hooks to attach a custom window.
|
||||
The window is hidden by default and toggled by left-clicking on the systray icon.
|
||||
The window will hide automatically when it loses focus.
|
||||
|
||||
On Windows, if the icon is in the notification flyout,
|
||||
then the window will be shown in the bottom right corner.
|
||||
|
||||
# Status
|
||||
|
||||
| Platform | Status |
|
||||
|----------|---------|
|
||||
| Mac | Working |
|
||||
| Windows | |
|
||||
| Linux | |
|
68
v3/examples/systray-custom/main.go
Normal file
68
v3/examples/systray-custom/main.go
Normal file
@ -0,0 +1,68 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"log"
|
||||
"runtime"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/icons"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := application.New(application.Options{
|
||||
Name: "Systray Demo",
|
||||
Description: "A demo of the Systray API",
|
||||
Assets: application.AlphaAssets,
|
||||
Mac: application.MacOptions{
|
||||
ActivationPolicy: application.ActivationPolicyAccessory,
|
||||
},
|
||||
})
|
||||
|
||||
systemTray := app.NewSystemTray()
|
||||
|
||||
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
|
||||
Width: 500,
|
||||
Height: 500,
|
||||
Name: "Systray Demo Window",
|
||||
Frameless: true,
|
||||
AlwaysOnTop: true,
|
||||
Hidden: true,
|
||||
DisableResize: true,
|
||||
ShouldClose: func(window *application.WebviewWindow) bool {
|
||||
window.Hide()
|
||||
return false
|
||||
},
|
||||
Windows: application.WindowsWindow{
|
||||
HiddenOnTaskbar: true,
|
||||
},
|
||||
})
|
||||
|
||||
if runtime.GOOS == "darwin" {
|
||||
systemTray.SetTemplateIcon(icons.SystrayMacTemplate)
|
||||
}
|
||||
|
||||
systemTray.OnClick(func() {
|
||||
println("System tray clicked!")
|
||||
if window.IsVisible() {
|
||||
window.Hide()
|
||||
} else {
|
||||
window.Show()
|
||||
}
|
||||
})
|
||||
|
||||
systemTray.OnDoubleClick(func() {
|
||||
println("System tray double clicked!")
|
||||
})
|
||||
|
||||
systemTray.OnRightClick(func() {
|
||||
println("System tray right clicked!")
|
||||
})
|
||||
|
||||
systemTray.AttachWindow(window).WindowOffset(5)
|
||||
|
||||
err := app.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
# Systray Example
|
||||
# Systray Menu Example
|
||||
|
||||
This example creates a system tray with an attached window and a menu.
|
||||
The window is hidden by default and toggled by left-clicking on the systray icon.
|
||||
@ -12,6 +12,6 @@ then the window will be shown in the bottom right corner.
|
||||
|
||||
| Platform | Status |
|
||||
|----------|---------|
|
||||
| Mac | |
|
||||
| Mac | Working |
|
||||
| Windows | Working |
|
||||
| Linux | |
|
@ -21,7 +21,7 @@ func main() {
|
||||
|
||||
systemTray := app.NewSystemTray()
|
||||
|
||||
_ = app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
|
||||
window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
|
||||
Width: 500,
|
||||
Height: 500,
|
||||
Name: "Systray Demo Window",
|
||||
@ -92,19 +92,8 @@ func main() {
|
||||
})
|
||||
|
||||
systemTray.SetMenu(myMenu)
|
||||
systemTray.OnClick(func() {
|
||||
println("System tray clicked!")
|
||||
})
|
||||
|
||||
systemTray.OnDoubleClick(func() {
|
||||
println("System tray double clicked!")
|
||||
})
|
||||
|
||||
systemTray.OnRightClick(func() {
|
||||
println("System tray right clicked!")
|
||||
})
|
||||
|
||||
//systemTray.AttachWindow(window).WindowOffset(5)
|
||||
systemTray.AttachWindow(window).WindowOffset(2)
|
||||
|
||||
err := app.Run()
|
||||
if err != nil {
|
@ -36,6 +36,7 @@ require (
|
||||
golang.org/x/sys v0.28.0
|
||||
golang.org/x/term v0.26.0
|
||||
golang.org/x/tools v0.23.0
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.21.0
|
||||
)
|
||||
|
@ -407,6 +407,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
||||
|
@ -14,6 +14,7 @@ import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"fmt"
|
||||
"github.com/leaanthony/go-ansi-parser"
|
||||
)
|
||||
|
||||
@ -29,6 +30,7 @@ type macosSystemTray struct {
|
||||
iconPosition int
|
||||
isTemplateIcon bool
|
||||
parent *SystemTray
|
||||
lastClickedScreen unsafe.Pointer
|
||||
}
|
||||
|
||||
func (s *macosSystemTray) openMenu() {
|
||||
@ -68,60 +70,48 @@ func (s *macosSystemTray) setMenu(menu *Menu) {
|
||||
}
|
||||
|
||||
func (s *macosSystemTray) positionWindow(window *WebviewWindow, offset int) error {
|
||||
// Get the window's native window
|
||||
impl := window.impl.(*macosWebviewWindow)
|
||||
|
||||
// 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
|
||||
|
||||
// Get the center height of the window
|
||||
windowWidthCenter := window.Width() / 2
|
||||
|
||||
// Get the center height of the system tray
|
||||
systemTrayWidthCenter := trayBounds.Width / 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, int(C.statusBarHeight())+offset)
|
||||
// Position the window relative to the systray
|
||||
C.systemTrayPositionWindow(s.nsStatusItem, impl.nsWindow, C.int(offset))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *macosSystemTray) getScreen() (*Screen, error) {
|
||||
return getScreenForSystray(s)
|
||||
if s.lastClickedScreen != nil {
|
||||
// Get the screen frame
|
||||
frame := C.NSScreen_frame(s.lastClickedScreen)
|
||||
result := &Screen{
|
||||
Bounds: Rect{
|
||||
X: int(frame.origin.x),
|
||||
Y: int(frame.origin.y),
|
||||
Width: int(frame.size.width),
|
||||
Height: int(frame.size.height),
|
||||
},
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
return nil, fmt.Errorf("no screen available")
|
||||
}
|
||||
|
||||
func (s *macosSystemTray) bounds() (*Rect, error) {
|
||||
var rect C.NSRect
|
||||
C.systemTrayGetBounds(s.nsStatusItem, &rect)
|
||||
// Get the screen height for the screen that the systray is on
|
||||
screen, err := getScreenForSystray(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var screen unsafe.Pointer
|
||||
C.systemTrayGetBounds(s.nsStatusItem, &rect, &screen)
|
||||
|
||||
// Invert Y axis based on screen height
|
||||
rect.origin.y = C.double(screen.Bounds.Height) - rect.origin.y - rect.size.height
|
||||
// Store the screen for use in positionWindow
|
||||
s.lastClickedScreen = screen
|
||||
|
||||
return &Rect{
|
||||
// Return the screen-relative coordinates
|
||||
result := &Rect{
|
||||
X: int(rect.origin.x),
|
||||
Y: int(rect.origin.y),
|
||||
Width: int(rect.size.width),
|
||||
Height: int(rect.size.height),
|
||||
}, nil
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *macosSystemTray) run() {
|
||||
|
@ -1,5 +1,7 @@
|
||||
//go:build darwin
|
||||
|
||||
#include <Cocoa/Cocoa.h>
|
||||
|
||||
@interface StatusItemController : NSObject
|
||||
@property long id;
|
||||
- (void)statusItemClicked:(id)sender;
|
||||
@ -8,11 +10,15 @@
|
||||
void* systemTrayNew(long id);
|
||||
void systemTraySetLabel(void* nsStatusItem, char *label);
|
||||
void systemTraySetANSILabel(void* nsStatusItem, void* attributedString);
|
||||
void systemTraySetLabelColor(void* nsStatusItem, char *fg, char *bg);
|
||||
void* createAttributedString(char *title, char *FG, char *BG);
|
||||
void* appendAttributedString(void* original, char* label, char* fg, char* bg);
|
||||
NSImage* imageFromBytes(const unsigned char *bytes, int length);
|
||||
void systemTraySetIcon(void* nsStatusItem, void* nsImage, int position, bool isTemplate);
|
||||
void systemTrayDestroy(void* nsStatusItem);
|
||||
void showMenu(void* nsStatusItem, void *nsMenu);
|
||||
void systemTrayGetBounds(void* nsStatusItem, NSRect *rect);
|
||||
void systemTrayGetBounds(void* nsStatusItem, NSRect *rect, void **screen);
|
||||
NSRect NSScreen_frame(void* screen);
|
||||
void windowSetScreen(void* window, void* screen, int yOffset);
|
||||
int statusBarHeight();
|
||||
void systemTrayPositionWindow(void* nsStatusItem, void* nsWindow, int offset);
|
@ -160,10 +160,36 @@ void showMenu(void* nsStatusItem, void *nsMenu) {
|
||||
});
|
||||
}
|
||||
|
||||
void systemTrayGetBounds(void* nsStatusItem, NSRect *rect) {
|
||||
void systemTrayGetBounds(void* nsStatusItem, NSRect *rect, void **outScreen) {
|
||||
NSStatusItem *statusItem = (NSStatusItem *)nsStatusItem;
|
||||
NSRect buttonFrame = statusItem.button.frame;
|
||||
*rect = [statusItem.button.window convertRectToScreen:buttonFrame];
|
||||
NSStatusBarButton *button = statusItem.button;
|
||||
|
||||
// Get mouse location and find the screen it's on
|
||||
NSPoint mouseLocation = [NSEvent mouseLocation];
|
||||
NSScreen *screen = nil;
|
||||
NSArray *screens = [NSScreen screens];
|
||||
|
||||
for (NSScreen *candidate in screens) {
|
||||
NSRect frame = [candidate frame];
|
||||
if (NSPointInRect(mouseLocation, frame)) {
|
||||
screen = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!screen) {
|
||||
screen = [NSScreen mainScreen];
|
||||
}
|
||||
|
||||
// Get button frame in screen coordinates
|
||||
NSRect buttonFrame = button.frame;
|
||||
NSRect buttonFrameScreen = [button.window convertRectToScreen:buttonFrame];
|
||||
|
||||
*rect = buttonFrameScreen;
|
||||
*outScreen = (void*)screen;
|
||||
}
|
||||
|
||||
NSRect NSScreen_frame(void* screen) {
|
||||
return [(NSScreen*)screen frame];
|
||||
}
|
||||
|
||||
int statusBarHeight() {
|
||||
@ -171,3 +197,48 @@ int statusBarHeight() {
|
||||
CGFloat menuBarHeight = [mainMenu menuBarHeight];
|
||||
return (int)menuBarHeight;
|
||||
}
|
||||
|
||||
void systemTrayPositionWindow(void* nsStatusItem, void* nsWindow, int offset) {
|
||||
// Get the status item's button
|
||||
NSStatusBarButton *button = [(NSStatusItem*)nsStatusItem button];
|
||||
|
||||
// Get the frame in screen coordinates
|
||||
NSRect frame = [button.window convertRectToScreen:button.frame];
|
||||
|
||||
// Get the screen that contains the status item
|
||||
NSScreen *screen = [button.window screen];
|
||||
if (screen == nil) {
|
||||
screen = [NSScreen mainScreen];
|
||||
}
|
||||
|
||||
// Get screen's backing scale factor (DPI)
|
||||
CGFloat scaleFactor = [screen backingScaleFactor];
|
||||
|
||||
// Get the window's frame
|
||||
NSRect windowFrame = [(NSWindow*)nsWindow frame];
|
||||
|
||||
// Calculate the horizontal position (centered under the status item)
|
||||
CGFloat windowX = frame.origin.x + (frame.size.width - windowFrame.size.width) / 2;
|
||||
|
||||
// If the window would go off the right edge of the screen, adjust it
|
||||
if (windowX + windowFrame.size.width > screen.frame.origin.x + screen.frame.size.width) {
|
||||
windowX = screen.frame.origin.x + screen.frame.size.width - windowFrame.size.width;
|
||||
}
|
||||
// If the window would go off the left edge of the screen, adjust it
|
||||
if (windowX < screen.frame.origin.x) {
|
||||
windowX = screen.frame.origin.x;
|
||||
}
|
||||
|
||||
// Get screen metrics
|
||||
NSRect screenFrame = [screen frame];
|
||||
NSRect visibleFrame = [screen visibleFrame];
|
||||
|
||||
// Calculate the vertical position
|
||||
CGFloat scaledOffset = offset * scaleFactor;
|
||||
CGFloat windowY = visibleFrame.origin.y + visibleFrame.size.height - windowFrame.size.height - scaledOffset;
|
||||
|
||||
// Set the window's frame
|
||||
windowFrame.origin.x = windowX;
|
||||
windowFrame.origin.y = windowY;
|
||||
[(NSWindow*)nsWindow setFrame:windowFrame display:YES animate:NO];
|
||||
}
|
||||
|
@ -32,5 +32,6 @@
|
||||
|
||||
@end
|
||||
|
||||
void windowSetScreen(void* window, void* screen, int yOffset);
|
||||
|
||||
#endif /* WebviewWindowDelegate_h */
|
||||
|
@ -719,3 +719,29 @@ extern bool hasListeners(unsigned int);
|
||||
|
||||
// GENERATED EVENTS END
|
||||
@end
|
||||
|
||||
void windowSetScreen(void* window, void* screen, int yOffset) {
|
||||
WebviewWindow* nsWindow = (WebviewWindow*)window;
|
||||
NSScreen* nsScreen = (NSScreen*)screen;
|
||||
|
||||
// Get current frame
|
||||
NSRect frame = [nsWindow frame];
|
||||
|
||||
// Convert frame to screen coordinates
|
||||
NSRect screenFrame = [nsScreen frame];
|
||||
NSRect currentScreenFrame = [[nsWindow screen] frame];
|
||||
|
||||
// Calculate the menubar height for the target screen
|
||||
NSRect visibleFrame = [nsScreen visibleFrame];
|
||||
CGFloat menubarHeight = screenFrame.size.height - visibleFrame.size.height;
|
||||
|
||||
// Calculate the distance from the top of the current screen
|
||||
CGFloat topOffset = currentScreenFrame.origin.y + currentScreenFrame.size.height - frame.origin.y;
|
||||
|
||||
// Position relative to new screen's top, accounting for menubar
|
||||
frame.origin.x = screenFrame.origin.x + (frame.origin.x - currentScreenFrame.origin.x);
|
||||
frame.origin.y = screenFrame.origin.y + screenFrame.size.height - topOffset - menubarHeight - yOffset;
|
||||
|
||||
// Set the frame which moves the window to the new screen
|
||||
[nsWindow setFrame:frame display:YES];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user