Compare commits

...

13 commits

Author SHA1 Message Date
Lea Anthony
a11655c452
Merge master 2022-07-09 22:55:03 +10:00
Lea Anthony
11cbfb68f1
Merge branch 'master' into exp
# Conflicts:
#	v2/internal/frontend/desktop/windows/win32/consts.go
2022-07-09 22:50:01 +10:00
Lea Anthony
319d59296e Support loading images into Windows tray 2022-05-14 06:44:22 +10:00
Lea Anthony
368733b7cc WIP 2022-05-13 20:49:54 +10:00
Lea Anthony
ee5ca1a84a
Add SetLabel, minor api updates 2022-05-07 20:39:08 +10:00
Lea Anthony
1947a9a17a
Hugely improved + darkmode bitmap support. Dynamic scaling factor support 2022-05-05 21:15:50 +10:00
Lea Anthony
13fb5352d8
Support sizing tray menu 2022-05-03 08:31:42 +10:00
Lea Anthony
58b6e4c335
Support retina images 2022-05-02 20:16:04 +10:00
Lea Anthony
ae0933c82a
Move TrayMenu from config to runtime
Support better tray menu API
Move object creation to main thread
2022-05-02 18:57:35 +10:00
Lea Anthony
c1d4aeb3cd
Merge branch 'master' into exp 2022-05-01 20:55:41 +10:00
Lea Anthony
1830c64c79
Better Checked implementation 2022-04-30 21:39:19 +10:00
Lea Anthony
ee50170374
Support activation policy 2022-04-30 17:51:11 +10:00
Lea Anthony
3f82ceabba
Initial Tray support for Mac 2022-04-30 12:00:00 +10:00
27 changed files with 1230 additions and 294 deletions

1
.gitignore vendored
View file

@ -32,3 +32,4 @@ v2/test/kitchensink/frontend/package.json.md5
!v2/internal/ffenestri/windows/x64/WebView2Loader.dll !v2/internal/ffenestri/windows/x64/WebView2Loader.dll
.idea/ .idea/
v2/cmd/wails/internal/commands/initialise/templates/testtemplates/ v2/cmd/wails/internal/commands/initialise/templates/testtemplates/
v2/internal/frontend/desktop/darwin/test.xcodeproj

View file

@ -17,6 +17,7 @@
@property bool startHidden; @property bool startHidden;
@property bool startFullscreen; @property bool startFullscreen;
@property (retain) WailsWindow* mainWindow; @property (retain) WailsWindow* mainWindow;
@property int activationPolicy;
@end @end

View file

@ -9,6 +9,7 @@
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#import "AppDelegate.h" #import "AppDelegate.h"
#import "message.h"
@implementation AppDelegate @implementation AppDelegate
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender {
@ -25,6 +26,8 @@
} }
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
processNotification(0); // Notify Go
[NSApp setActivationPolicy:self.activationPolicy];
[NSApp activateIgnoringOtherApps:YES]; [NSApp activateIgnoringOtherApps:YES];
if ( self.startFullscreen ) { if ( self.startFullscreen ) {
NSWindowCollectionBehavior behaviour = [self.mainWindow collectionBehavior]; NSWindowCollectionBehavior behaviour = [self.mainWindow collectionBehavior];

View file

@ -18,7 +18,7 @@
#define WindowStartsFullscreen 3 #define WindowStartsFullscreen 3
WailsContext* Create(const char* title, int width, int height, int frameless, int resizable, int fullscreen, int fullSizeContent, int hideTitleBar, int titlebarAppearsTransparent, int hideTitle, int useToolbar, int hideToolbarSeparator, int webviewIsTransparent, int alwaysOnTop, int hideWindowOnClose, const char *appearance, int windowIsTranslucent, int debug, int windowStartState, int startsHidden, int minWidth, int minHeight, int maxWidth, int maxHeight); WailsContext* Create(const char* title, int width, int height, int frameless, int resizable, int fullscreen, int fullSizeContent, int hideTitleBar, int titlebarAppearsTransparent, int hideTitle, int useToolbar, int hideToolbarSeparator, int webviewIsTransparent, int alwaysOnTop, int hideWindowOnClose, const char *appearance, int windowIsTranslucent, int debug, int windowStartState, int startsHidden, int minWidth, int minHeight, int maxWidth, int maxHeight);
void Run(void*, const char* url); void Run(void *inctx, const char* url, int activationPolicy);
void SetTitle(void* ctx, const char *title); void SetTitle(void* ctx, const char *title);
void Center(void* ctx); void Center(void* ctx);
@ -57,6 +57,16 @@ void AppendSubmenu(void* parent, void* child);
void AppendRole(void *inctx, void *inMenu, int role); void AppendRole(void *inctx, void *inMenu, int role);
void SetAsApplicationMenu(void *inctx, void *inMenu); void SetAsApplicationMenu(void *inctx, void *inMenu);
void UpdateApplicationMenu(void *inctx); void UpdateApplicationMenu(void *inctx);
void SetMenuItemChecked(void* nsMenuItem, int checked);
/* Tray Menu */
void NewNSStatusItem(int id, int length);
void SetTrayMenu(void *nsStatusItem, void* nsMenu);
void SetTrayMenuLabel(void *nsStatusItem, const char *label);
void SetTrayImage(void *nsStatusItem, void *imageData, int imageDataLength, int template, int position);
/* MenuItems */
void SetMenuItemLabel(void *nsStatusItem, const char *label);
void SetAbout(void *inctx, const char* title, const char* description, void* imagedata, int datalen); void SetAbout(void *inctx, const char* title, const char* description, void* imagedata, int datalen);
void* AppendMenuItem(void* inctx, void* nsmenu, const char* label, const char* shortcutKey, int modifiers, int disabled, int checked, int menuItemID); void* AppendMenuItem(void* inctx, void* nsmenu, const char* label, const char* shortcutKey, int modifiers, int disabled, int checked, int menuItemID);
@ -65,4 +75,7 @@ void UpdateMenuItem(void* nsmenuitem, int checked);
NSString* safeInit(const char* input); NSString* safeInit(const char* input);
int ScalingFactor(void *ctx);
#endif /* Application_h */ #endif /* Application_h */

View file

@ -11,6 +11,7 @@
#import "AppDelegate.h" #import "AppDelegate.h"
#import "WailsMenu.h" #import "WailsMenu.h"
#import "WailsMenuItem.h" #import "WailsMenuItem.h"
#import "message.h"
WailsContext* Create(const char* title, int width, int height, int frameless, int resizable, int fullscreen, int fullSizeContent, int hideTitleBar, int titlebarAppearsTransparent, int hideTitle, int useToolbar, int hideToolbarSeparator, int webviewIsTransparent, int alwaysOnTop, int hideWindowOnClose, const char *appearance, int windowIsTranslucent, int debug, int windowStartState, int startsHidden, int minWidth, int minHeight, int maxWidth, int maxHeight) { WailsContext* Create(const char* title, int width, int height, int frameless, int resizable, int fullscreen, int fullSizeContent, int hideTitleBar, int titlebarAppearsTransparent, int hideTitle, int useToolbar, int hideToolbarSeparator, int webviewIsTransparent, int alwaysOnTop, int hideWindowOnClose, const char *appearance, int windowIsTranslucent, int debug, int windowStartState, int startsHidden, int minWidth, int minHeight, int maxWidth, int maxHeight) {
@ -19,7 +20,7 @@ WailsContext* Create(const char* title, int width, int height, int frameless, in
WailsContext *result = [WailsContext new]; WailsContext *result = [WailsContext new];
result.debug = debug; result.debug = debug;
if ( windowStartState == WindowStartsFullscreen ) { if ( windowStartState == WindowStartsFullscreen ) {
fullscreen = 1; fullscreen = 1;
} }
@ -27,7 +28,7 @@ WailsContext* Create(const char* title, int width, int height, int frameless, in
[result CreateWindow:width :height :frameless :resizable :fullscreen :fullSizeContent :hideTitleBar :titlebarAppearsTransparent :hideTitle :useToolbar :hideToolbarSeparator :webviewIsTransparent :hideWindowOnClose :safeInit(appearance) :windowIsTranslucent :minWidth :minHeight :maxWidth :maxHeight]; [result CreateWindow:width :height :frameless :resizable :fullscreen :fullSizeContent :hideTitleBar :titlebarAppearsTransparent :hideTitle :useToolbar :hideToolbarSeparator :webviewIsTransparent :hideWindowOnClose :safeInit(appearance) :windowIsTranslucent :minWidth :minHeight :maxWidth :maxHeight];
[result SetTitle:safeInit(title)]; [result SetTitle:safeInit(title)];
[result Center]; [result Center];
switch( windowStartState ) { switch( windowStartState ) {
case WindowStartsMaximised: case WindowStartsMaximised:
[result.mainWindow zoom:nil]; [result.mainWindow zoom:nil];
@ -172,6 +173,10 @@ void ToggleMaximise(void* inctx) {
); );
} }
void SetMenuItemChecked(void* nsMenuItem, int checked) {
[(NSMenuItem*)nsMenuItem setState:(checked == 0 ? NSOffState : NSOnState)];
}
const char* GetSize(void *inctx) { const char* GetSize(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx; WailsContext *ctx = (__bridge WailsContext*) inctx;
NSRect frame = [ctx.mainWindow frame]; NSRect frame = [ctx.mainWindow frame];
@ -277,6 +282,55 @@ void AppendRole(void *inctx, void *inMenu, int role) {
[menu appendRole :ctx :role]; [menu appendRole :ctx :role];
} }
void NewNSStatusItem(int id, int length) {
dispatch_async(dispatch_get_main_queue(), ^{
NSStatusBar *statusBar = [NSStatusBar systemStatusBar];
// Map Go to Cocoa length. 0 = NSVariableStatusItemLength.
CGFloat length = NSVariableStatusItemLength;
if( length == 1 ) {
length = NSSquareStatusItemLength;
}
NSStatusItem *result = [[statusBar statusItemWithLength:length] retain];
objectCreated(id,result);
});
}
void DeleteStatusItem(void *_nsStatusItem) {
NSStatusItem *nsStatusItem = (NSStatusItem*) _nsStatusItem;
[nsStatusItem release];
}
void on_main_thread(void (^l)(void)) {
dispatch_async(dispatch_get_main_queue(), l);
}
void SetTrayMenuLabel(void *_nsStatusItem, const char *label) {
on_main_thread(^{
NSStatusItem *nsStatusItem = (NSStatusItem*) _nsStatusItem;
nsStatusItem.button.title = safeInit(label);
free((void*)label);
});
}
void SetTrayMenu(void *nsStatusItem, void* nsMenu) {
ON_MAIN_THREAD(
[(NSStatusItem*)nsStatusItem setMenu:(NSMenu *)nsMenu];
)
}
/**** Menu Item ****/
void SetMenuItemLabel(void *_nsMenuItem, const char *label) {
on_main_thread(^{
NSMenuItem *nsMenuItem = (NSMenuItem*) _nsMenuItem;
[ nsMenuItem setTitle:safeInit(label) ];
free((void*)label);
});
}
void* NewMenu(const char *name) { void* NewMenu(const char *name) {
NSString *title = @""; NSString *title = @"";
if (name != nil) { if (name != nil) {
@ -301,8 +355,8 @@ void SetAsApplicationMenu(void *inctx, void *inMenu) {
void UpdateApplicationMenu(void *inctx) { void UpdateApplicationMenu(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx; WailsContext *ctx = (__bridge WailsContext*) inctx;
ON_MAIN_THREAD( ON_MAIN_THREAD(
NSApplication *app = [NSApplication sharedApplication]; NSApplication *app = [NSApplication sharedApplication];
[app setMainMenu:ctx.applicationMenu]; [app setMainMenu:ctx.applicationMenu];
) )
} }
@ -327,7 +381,7 @@ void UpdateMenuItem(void* nsmenuitem, int checked) {
ON_MAIN_THREAD( ON_MAIN_THREAD(
WailsMenuItem *menuItem = (__bridge WailsMenuItem*) nsmenuitem; WailsMenuItem *menuItem = (__bridge WailsMenuItem*) nsmenuitem;
[menuItem setState:(checked == 1?NSControlStateValueOn:NSControlStateValueOff)]; [menuItem setState:(checked == 1?NSControlStateValueOn:NSControlStateValueOff)];
) )
} }
@ -336,12 +390,39 @@ void AppendSeparator(void* inMenu) {
[menu AppendSeparator]; [menu AppendSeparator];
} }
void SetTrayImage(void *nsStatusItem, void *imageData, int imageDataLength, int template, int position) {
ON_MAIN_THREAD(
NSStatusItem *statusItem = (NSStatusItem*) nsStatusItem;
NSData *nsdata = [NSData dataWithBytes:imageData length:imageDataLength];
NSImage *image = [[[NSImage alloc] initWithData:nsdata] autorelease];
if(template) {
image.template = true;
}
image.size = NSMakeSize(22.0, 22.0);
statusItem.button.image = image;
// Swap NSNoImage and NSImageLeading because we wanted NSImageLeading to be default in Go
int actualPosition = position;
if( position == 7) {
actualPosition = 0;
} else if (position == 0) {
actualPosition = 7;
}
[statusItem.button setImagePosition:actualPosition];
)
}
int ScalingFactor(void *ctx) {
CGFloat scale = [((WailsContext*)ctx).mainWindow backingScaleFactor];
return (int)scale;
}
void Run(void *inctx, const char* url) { void Run(void *inctx, const char* url, int activationPolicy) {
WailsContext *ctx = (__bridge WailsContext*) inctx; WailsContext *ctx = (__bridge WailsContext*) inctx;
NSApplication *app = [NSApplication sharedApplication]; NSApplication *app = [NSApplication sharedApplication];
AppDelegate* delegate = [AppDelegate new]; AppDelegate* delegate = [AppDelegate new];
delegate.activationPolicy = activationPolicy;
[app setDelegate:(id)delegate]; [app setDelegate:(id)delegate];
ctx.appdelegate = delegate; ctx.appdelegate = delegate;
delegate.mainWindow = ctx.mainWindow; delegate.mainWindow = ctx.mainWindow;

View file

@ -23,6 +23,7 @@
@property NSSize userMinSize; @property NSSize userMinSize;
@property NSSize userMaxSize; @property NSSize userMaxSize;
@property int activationPolicy;
- (BOOL) canBecomeKeyWindow; - (BOOL) canBecomeKeyWindow;
- (void) applyWindowConstraints; - (void) applyWindowConstraints;

View file

@ -3,15 +3,6 @@
package darwin package darwin
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit
#import <Foundation/Foundation.h>
#import "Application.h"
#import "WailsContext.h"
#include <stdlib.h>
*/
import "C" import "C"
import ( import (
"bytes" "bytes"
@ -31,14 +22,33 @@ import (
"github.com/wailsapp/wails/v2/internal/frontend" "github.com/wailsapp/wails/v2/internal/frontend"
"github.com/wailsapp/wails/v2/internal/frontend/assetserver" "github.com/wailsapp/wails/v2/internal/frontend/assetserver"
"github.com/wailsapp/wails/v2/internal/logger" "github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options"
) )
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit
#import <Foundation/Foundation.h>
#import "Application.h"
#import "WailsContext.h"
#include <stdlib.h>
*/
import "C"
const startURL = "wails://wails/" const startURL = "wails://wails/"
type NotificationType uint8
const (
ApplicationDidFinishLaunching NotificationType = 0
)
var messageBuffer = make(chan string, 100) var messageBuffer = make(chan string, 100)
var requestBuffer = make(chan *request, 100) var requestBuffer = make(chan *request, 100)
var callbackBuffer = make(chan uint, 10) var callbackBuffer = make(chan uint, 10)
var notificationBuffer = make(chan NotificationType, 10)
type Frontend struct { type Frontend struct {
@ -57,15 +67,22 @@ type Frontend struct {
mainWindow *Window mainWindow *Window
bindings *binding.Bindings bindings *binding.Bindings
dispatcher frontend.Dispatcher dispatcher frontend.Dispatcher
trayMenus map[*menu.TrayMenu]*NSTrayMenu
applicationDidFinishLaunching bool
notificationCallbacks map[NotificationType][]func()
trayMenusBuffer []*menu.TrayMenu
} }
func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) *Frontend { func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher) *Frontend {
result := &Frontend{ result := &Frontend{
frontendOptions: appoptions, frontendOptions: appoptions,
logger: myLogger, logger: myLogger,
bindings: appBindings, bindings: appBindings,
dispatcher: dispatcher, dispatcher: dispatcher,
ctx: ctx, ctx: ctx,
trayMenus: make(map[*menu.TrayMenu]*NSTrayMenu),
notificationCallbacks: make(map[NotificationType][]func()),
} }
result.startURL, _ = url.Parse(startURL) result.startURL, _ = url.Parse(startURL)
@ -88,10 +105,17 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
go result.startMessageProcessor() go result.startMessageProcessor()
go result.startCallbackProcessor() go result.startCallbackProcessor()
go result.startNotificationsProcessor()
result.registerNotificationCallback(ApplicationDidFinishLaunching, result.processTrayMenus)
return result return result
} }
func (f *Frontend) registerNotificationCallback(notificationType NotificationType, callback func()) {
f.notificationCallbacks[notificationType] = append(f.notificationCallbacks[notificationType], callback)
}
func (f *Frontend) startMessageProcessor() { func (f *Frontend) startMessageProcessor() {
for message := range messageBuffer { for message := range messageBuffer {
f.processMessage(message) f.processMessage(message)
@ -110,6 +134,11 @@ func (f *Frontend) startCallbackProcessor() {
} }
} }
} }
func (f *Frontend) startNotificationsProcessor() {
for notification := range notificationBuffer {
f.handleNotification(notification)
}
}
func (f *Frontend) WindowReload() { func (f *Frontend) WindowReload() {
f.ExecJS("runtime.WindowReload();") f.ExecJS("runtime.WindowReload();")
@ -149,7 +178,11 @@ func (f *Frontend) Run(ctx context.Context) error {
f.frontendOptions.OnStartup(f.ctx) f.frontendOptions.OnStartup(f.ctx)
} }
}() }()
mainWindow.Run(f.startURL.String()) var activationPolicy C.int
if f.frontendOptions != nil && f.frontendOptions.Mac != nil {
activationPolicy = C.int(f.frontendOptions.Mac.ActivationPolicy)
}
mainWindow.Run(f.startURL.String(), activationPolicy)
return nil return nil
} }
@ -396,6 +429,11 @@ func processMessage(message *C.char) {
messageBuffer <- goMessage messageBuffer <- goMessage
} }
//export processNotification
func processNotification(notification NotificationType) {
notificationBuffer <- notification
}
//export processURLRequest //export processURLRequest
func processURLRequest(ctx unsafe.Pointer, url *C.char, method *C.char, headers *C.char, body unsafe.Pointer, bodyLen C.int) { func processURLRequest(ctx unsafe.Pointer, url *C.char, method *C.char, headers *C.char, body unsafe.Pointer, bodyLen C.int) {
var goBody []byte var goBody []byte
@ -416,3 +454,19 @@ func processURLRequest(ctx unsafe.Pointer, url *C.char, method *C.char, headers
func processCallback(callbackID uint) { func processCallback(callbackID uint) {
callbackBuffer <- callbackID callbackBuffer <- callbackID
} }
func (f *Frontend) handleNotification(notification NotificationType) {
switch notification {
case ApplicationDidFinishLaunching:
f.applicationDidFinishLaunching = true
for _, callback := range f.notificationCallbacks[notification] {
go callback()
}
}
}
func (f *Frontend) processTrayMenus() {
for _, trayMenu := range f.trayMenusBuffer {
f.mainWindow.TrayMenuAdd(trayMenu)
}
}

View file

@ -14,12 +14,86 @@ package darwin
*/ */
import "C" import "C"
import ( import (
"fmt"
"sync"
"unsafe" "unsafe"
"github.com/google/uuid"
"github.com/wailsapp/wails/v2/pkg/menu" "github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/menu/keys" "github.com/wailsapp/wails/v2/pkg/menu/keys"
) )
var createNSObjectMap = make(map[uint32]chan unsafe.Pointer)
var createNSObjectMapLock sync.RWMutex
func waitNSObjectCreate(id uint32, fn func()) unsafe.Pointer {
waitchan := make(chan unsafe.Pointer)
createNSObjectMapLock.Lock()
createNSObjectMap[id] = waitchan
createNSObjectMapLock.Unlock()
fn()
result := <-waitchan
createNSObjectMapLock.Lock()
createNSObjectMap[id] = nil
createNSObjectMapLock.Unlock()
return result
}
//export objectCreated
func objectCreated(id uint32, pointer unsafe.Pointer) {
createNSObjectMapLock.Lock()
createNSObjectMap[id] <- pointer
createNSObjectMapLock.Unlock()
}
func NewNSTrayMenu(context unsafe.Pointer, trayMenu *menu.TrayMenu, scalingFactor int) *NSTrayMenu {
c := NewCalloc()
defer c.Free()
id := uuid.New().ID()
nsStatusItem := waitNSObjectCreate(id, func() {
C.NewNSStatusItem(C.int(id), C.int(trayMenu.Sizing))
})
result := &NSTrayMenu{
context: context,
nsStatusItem: nsStatusItem,
scalingFactor: scalingFactor,
}
result.SetLabel(trayMenu.Label)
result.SetMenu(trayMenu.Menu)
result.SetImage(trayMenu.Image)
return result
}
func (n *NSTrayMenu) SetImage(image *menu.TrayImage) {
if image == nil {
return
}
bitmap := image.GetBestBitmap(n.scalingFactor, false)
if bitmap == nil {
fmt.Printf("[Warning] No TrayMenu Image available for scaling factor %dx\n", n.scalingFactor)
return
}
C.SetTrayImage(n.nsStatusItem,
unsafe.Pointer(&bitmap[0]),
C.int(len(bitmap)),
bool2Cint(image.IsTemplate),
C.int(image.Position),
)
}
func (n *NSTrayMenu) SetMenu(menu *menu.Menu) {
if menu == nil {
return
}
theMenu := NewNSMenu(n.context, "")
processMenu(theMenu, menu)
C.SetTrayMenu(n.nsStatusItem, theMenu.nsmenu)
}
type NSMenu struct { type NSMenu struct {
context unsafe.Pointer context unsafe.Pointer
nsmenu unsafe.Pointer nsmenu unsafe.Pointer
@ -53,6 +127,15 @@ type MenuItem struct {
radioGroupMembers []*MenuItem radioGroupMembers []*MenuItem
} }
func (m *MenuItem) SetChecked(value bool) {
C.SetMenuItemChecked(m.nsmenuitem, bool2Cint(value))
}
func (m *MenuItem) SetLabel(label string) {
cLabel := C.CString(label)
C.SetMenuItemLabel(m.nsmenuitem, cLabel)
}
func (m *NSMenu) AddMenuItem(menuItem *menu.MenuItem) *MenuItem { func (m *NSMenu) AddMenuItem(menuItem *menu.MenuItem) *MenuItem {
c := NewCalloc() c := NewCalloc()
defer c.Free() defer c.Free()
@ -69,6 +152,7 @@ func (m *NSMenu) AddMenuItem(menuItem *menu.MenuItem) *MenuItem {
result.id = createMenuItemID(result) result.id = createMenuItemID(result)
result.nsmenuitem = C.AppendMenuItem(m.context, m.nsmenu, c.String(menuItem.Label), key, modifier, bool2Cint(menuItem.Disabled), bool2Cint(menuItem.Checked), C.int(result.id)) result.nsmenuitem = C.AppendMenuItem(m.context, m.nsmenu, c.String(menuItem.Label), key, modifier, bool2Cint(menuItem.Disabled), bool2Cint(menuItem.Checked), C.int(result.id))
menuItem.Impl = result
return result return result
} }

View file

@ -20,6 +20,8 @@ void processMessageDialogResponse(int);
void processOpenFileDialogResponse(const char*); void processOpenFileDialogResponse(const char*);
void processSaveFileDialogResponse(const char*); void processSaveFileDialogResponse(const char*);
void processCallback(int); void processCallback(int);
void processNotification(int);
void objectCreated(int, void*);
#ifdef __cplusplus #ifdef __cplusplus
} }

View file

@ -0,0 +1,44 @@
//go:build darwin
// +build darwin
package darwin
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit
#import <Foundation/Foundation.h>
#import "Application.h"
#import "WailsContext.h"
#include <stdlib.h>
*/
import "C"
import (
"unsafe"
"github.com/wailsapp/wails/v2/pkg/menu"
)
func (f *Frontend) TrayMenuAdd(trayMenu *menu.TrayMenu) menu.TrayMenuImpl {
nsTrayMenu := f.mainWindow.TrayMenuAdd(trayMenu)
f.trayMenus[trayMenu] = nsTrayMenu
return nsTrayMenu
}
type NSTrayMenu struct {
context unsafe.Pointer
nsStatusItem unsafe.Pointer // NSStatusItem
scalingFactor int
}
func (n *NSTrayMenu) SetLabel(label string) {
if label == "" {
return
}
cLabel := C.CString(label)
C.SetTrayMenuLabel(n.nsStatusItem, cLabel)
}
func (w *Window) TrayMenuAdd(trayMenu *menu.TrayMenu) *NSTrayMenu {
return NewNSTrayMenu(w.context, trayMenu, ScalingFactor(w))
}

View file

@ -40,6 +40,10 @@ func bool2Cint(value bool) C.int {
return C.int(0) return C.int(0)
} }
func ScalingFactor(window *Window) int {
return int(C.ScalingFactor(window.context))
}
func NewWindow(frontendOptions *options.App, debugMode bool) *Window { func NewWindow(frontendOptions *options.App, debugMode bool) *Window {
c := NewCalloc() c := NewCalloc()
@ -119,9 +123,9 @@ func (w *Window) Center() {
C.Center(w.context) C.Center(w.context)
} }
func (w *Window) Run(url string) { func (w *Window) Run(url string, activationPolicy C.int) {
_url := C.CString(url) _url := C.CString(url)
C.Run(w.context, _url) C.Run(w.context, _url, activationPolicy)
C.free(unsafe.Pointer(_url)) C.free(unsafe.Pointer(_url))
} }

View file

@ -0,0 +1,36 @@
package windows
import (
"bufio"
"bytes"
"golang.org/x/image/draw"
"image"
"image/png"
)
func ResizePNG(in []byte, size int) ([]byte, error) {
imagedata, _, err := image.Decode(bytes.NewReader(in))
if err != nil {
return nil, err
}
// Scale image
rect := image.Rect(0, 0, size, size)
rawdata := image.NewRGBA(rect)
scale := draw.CatmullRom
scale.Scale(rawdata, rect, imagedata, imagedata.Bounds(), draw.Over, nil)
// Convert back to PNG
icondata := new(bytes.Buffer)
writer := bufio.NewWriter(icondata)
err = png.Encode(writer, rawdata)
if err != nil {
return nil, err
}
err = writer.Flush()
if err != nil {
return nil, err
}
// Save image data
return icondata.Bytes(), nil
}

View file

@ -0,0 +1,79 @@
//go:build windows
// +build windows
package windows
import (
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/win32"
"github.com/wailsapp/wails/v2/pkg/menu"
"log"
"sync"
"unsafe"
)
var uids uint32
var lock sync.RWMutex
func newUID() uint32 {
lock.Lock()
result := uids
uids++
lock.Unlock()
return result
}
type Win32TrayMenu struct {
hwnd uintptr
uid uint32
icon uintptr
}
func (w *Win32TrayMenu) SetLabel(label string) {}
func (w *Win32TrayMenu) SetMenu(menu *menu.Menu) {}
func (w *Win32TrayMenu) SetImage(image *menu.TrayImage) {
data := w.newNotifyIconData()
bitmap := image.GetBestBitmap(1, false)
icon, err := win32.CreateIconFromResourceEx(uintptr(unsafe.Pointer(&bitmap[0])), uint32(len(bitmap)), true, 0x30000, 0, 0, 0)
if err != nil {
log.Fatal(err.Error())
}
data.UFlags |= win32.NIF_ICON
data.HIcon = icon
if _, err := win32.NotifyIcon(win32.NIM_MODIFY, data); err != nil {
log.Fatal(err.Error())
}
}
func (f *Frontend) NewWin32TrayMenu(trayMenu *menu.TrayMenu) *Win32TrayMenu {
result := &Win32TrayMenu{
hwnd: f.mainWindow.Handle(),
uid: newUID(),
}
data := result.newNotifyIconData()
data.UFlags |= win32.NIF_MESSAGE | win32.NIF_ICON
data.UCallbackMessage = win32.WM_APP + result.uid
if _, err := win32.NotifyIcon(win32.NIM_ADD, data); err != nil {
log.Fatal(err.Error())
}
return result
}
func (w *Win32TrayMenu) newNotifyIconData() *win32.NOTIFYICONDATA {
var data win32.NOTIFYICONDATA
data.CbSize = uint32(unsafe.Sizeof(data))
data.UFlags = win32.NIF_GUID
data.HWnd = w.hwnd
data.UID = w.uid
return &data
}
func (f *Frontend) TrayMenuAdd(trayMenu *menu.TrayMenu) menu.TrayMenuImpl {
win32TrayMenu := f.NewWin32TrayMenu(trayMenu)
return win32TrayMenu
}

View file

@ -12,12 +12,21 @@ type HRESULT int32
type HANDLE uintptr type HANDLE uintptr
var ( var (
moduser32 = syscall.NewLazyDLL("user32.dll") moduser32 = syscall.NewLazyDLL("user32.dll")
procSystemParametersInfo = moduser32.NewProc("SystemParametersInfoW") procSystemParametersInfo = moduser32.NewProc("SystemParametersInfoW")
procGetWindowLong = moduser32.NewProc("GetWindowLongW") procGetWindowLong = moduser32.NewProc("GetWindowLongW")
procSetClassLong = moduser32.NewProc("SetClassLongW") procSetClassLong = moduser32.NewProc("SetClassLongW")
procSetClassLongPtr = moduser32.NewProc("SetClassLongPtrW") procSetClassLongPtr = moduser32.NewProc("SetClassLongPtrW")
procShowWindow = moduser32.NewProc("ShowWindow") procShowWindow = moduser32.NewProc("ShowWindow")
procLookupIconIdFromDirectoryEx = moduser32.NewProc("LookupIconIdFromDirectoryEx")
procCreateIconFromResourceEx = moduser32.NewProc("CreateIconFromResourceEx")
procCreateIconIndirect = moduser32.NewProc("CreateIconIndirect")
procLoadImageW = moduser32.NewProc("LoadImageW")
)
var (
modshell32 = syscall.NewLazyDLL("shell32.dll")
procShellNotifyIcon = modshell32.NewProc("Shell_NotifyIconW")
) )
var ( var (
moddwmapi = syscall.NewLazyDLL("dwmapi.dll") moddwmapi = syscall.NewLazyDLL("dwmapi.dll")
@ -36,3 +45,217 @@ func IsWindowsVersionAtLeast(major, minor, buildNumber int) bool {
windowsVersion.Minor >= minor && windowsVersion.Minor >= minor &&
windowsVersion.Build >= buildNumber windowsVersion.Build >= buildNumber
} }
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa373931.aspx
type GUID struct {
Data1 uint32
Data2 uint16
Data3 uint16
Data4 [8]byte
}
const (
WM_APP = 32768
WM_ACTIVATE = 6
WM_ACTIVATEAPP = 28
WM_AFXFIRST = 864
WM_AFXLAST = 895
WM_ASKCBFORMATNAME = 780
WM_CANCELJOURNAL = 75
WM_CANCELMODE = 31
WM_CAPTURECHANGED = 533
WM_CHANGECBCHAIN = 781
WM_CHAR = 258
WM_CHARTOITEM = 47
WM_CHILDACTIVATE = 34
WM_CLEAR = 771
WM_CLOSE = 16
WM_COMMAND = 273
WM_COMMNOTIFY = 68 /* OBSOLETE */
WM_COMPACTING = 65
WM_COMPAREITEM = 57
WM_CONTEXTMENU = 123
WM_COPY = 769
WM_COPYDATA = 74
WM_CREATE = 1
WM_CTLCOLORBTN = 309
WM_CTLCOLORDLG = 310
WM_CTLCOLOREDIT = 307
WM_CTLCOLORLISTBOX = 308
WM_CTLCOLORMSGBOX = 306
WM_CTLCOLORSCROLLBAR = 311
WM_CTLCOLORSTATIC = 312
WM_CUT = 768
WM_DEADCHAR = 259
WM_DELETEITEM = 45
WM_DESTROY = 2
WM_DESTROYCLIPBOARD = 775
WM_DEVICECHANGE = 537
WM_DEVMODECHANGE = 27
WM_DISPLAYCHANGE = 126
WM_DRAWCLIPBOARD = 776
WM_DRAWITEM = 43
WM_DROPFILES = 563
WM_ENABLE = 10
WM_ENDSESSION = 22
WM_ENTERIDLE = 289
WM_ENTERMENULOOP = 529
WM_ENTERSIZEMOVE = 561
WM_ERASEBKGND = 20
WM_EXITMENULOOP = 530
WM_EXITSIZEMOVE = 562
WM_FONTCHANGE = 29
WM_GETDLGCODE = 135
WM_GETFONT = 49
WM_GETHOTKEY = 51
WM_GETICON = 127
WM_GETMINMAXINFO = 36
WM_GETTEXT = 13
WM_GETTEXTLENGTH = 14
WM_HANDHELDFIRST = 856
WM_HANDHELDLAST = 863
WM_HELP = 83
WM_HOTKEY = 786
WM_HSCROLL = 276
WM_HSCROLLCLIPBOARD = 782
WM_ICONERASEBKGND = 39
WM_INITDIALOG = 272
WM_INITMENU = 278
WM_INITMENUPOPUP = 279
WM_INPUT = 0x00FF
WM_INPUTLANGCHANGE = 81
WM_INPUTLANGCHANGEREQUEST = 80
WM_KEYDOWN = 256
WM_KEYUP = 257
WM_KILLFOCUS = 8
WM_MDIACTIVATE = 546
WM_MDICASCADE = 551
WM_MDICREATE = 544
WM_MDIDESTROY = 545
WM_MDIGETACTIVE = 553
WM_MDIICONARRANGE = 552
WM_MDIMAXIMIZE = 549
WM_MDINEXT = 548
WM_MDIREFRESHMENU = 564
WM_MDIRESTORE = 547
WM_MDISETMENU = 560
WM_MDITILE = 550
WM_MEASUREITEM = 44
WM_GETOBJECT = 0x003D
WM_CHANGEUISTATE = 0x0127
WM_UPDATEUISTATE = 0x0128
WM_QUERYUISTATE = 0x0129
WM_UNINITMENUPOPUP = 0x0125
WM_MENURBUTTONUP = 290
WM_MENUCOMMAND = 0x0126
WM_MENUGETOBJECT = 0x0124
WM_MENUDRAG = 0x0123
WM_APPCOMMAND = 0x0319
WM_MENUCHAR = 288
WM_MENUSELECT = 287
WM_MOVE = 3
WM_MOVING = 534
WM_NCACTIVATE = 134
WM_NCCALCSIZE = 131
WM_NCCREATE = 129
WM_NCDESTROY = 130
WM_NCHITTEST = 132
WM_NCLBUTTONDBLCLK = 163
WM_NCLBUTTONDOWN = 161
WM_NCLBUTTONUP = 162
WM_NCMBUTTONDBLCLK = 169
WM_NCMBUTTONDOWN = 167
WM_NCMBUTTONUP = 168
WM_NCXBUTTONDOWN = 171
WM_NCXBUTTONUP = 172
WM_NCXBUTTONDBLCLK = 173
WM_NCMOUSEHOVER = 0x02A0
WM_NCMOUSELEAVE = 0x02A2
WM_NCMOUSEMOVE = 160
WM_NCPAINT = 133
WM_NCRBUTTONDBLCLK = 166
WM_NCRBUTTONDOWN = 164
WM_NCRBUTTONUP = 165
WM_NEXTDLGCTL = 40
WM_NEXTMENU = 531
WM_NOTIFY = 78
WM_NOTIFYFORMAT = 85
WM_NULL = 0
WM_PAINT = 15
WM_PAINTCLIPBOARD = 777
WM_PAINTICON = 38
WM_PALETTECHANGED = 785
WM_PALETTEISCHANGING = 784
WM_PARENTNOTIFY = 528
WM_PASTE = 770
WM_PENWINFIRST = 896
WM_PENWINLAST = 911
WM_POWER = 72
WM_PRINT = 791
WM_PRINTCLIENT = 792
WM_QUERYDRAGICON = 55
WM_QUERYENDSESSION = 17
WM_QUERYNEWPALETTE = 783
WM_QUERYOPEN = 19
WM_QUEUESYNC = 35
WM_QUIT = 18
WM_RENDERALLFORMATS = 774
WM_RENDERFORMAT = 773
WM_SETCURSOR = 32
WM_SETFOCUS = 7
WM_SETFONT = 48
WM_SETHOTKEY = 50
WM_SETICON = 128
WM_SETREDRAW = 11
WM_SETTEXT = 12
WM_SETTINGCHANGE = 26
WM_SHOWWINDOW = 24
WM_SIZE = 5
WM_SIZECLIPBOARD = 779
WM_SIZING = 532
WM_SPOOLERSTATUS = 42
WM_STYLECHANGED = 125
WM_STYLECHANGING = 124
WM_SYSCHAR = 262
WM_SYSCOLORCHANGE = 21
WM_SYSCOMMAND = 274
WM_SYSDEADCHAR = 263
WM_SYSKEYDOWN = 260
WM_SYSKEYUP = 261
WM_TCARD = 82
WM_THEMECHANGED = 794
WM_TIMECHANGE = 30
WM_TIMER = 275
WM_UNDO = 772
WM_USER = 1024
WM_USERCHANGED = 84
WM_VKEYTOITEM = 46
WM_VSCROLL = 277
WM_VSCROLLCLIPBOARD = 778
WM_WINDOWPOSCHANGED = 71
WM_WINDOWPOSCHANGING = 70
WM_WININICHANGE = 26
WM_KEYFIRST = 256
WM_KEYLAST = 264
WM_SYNCPAINT = 136
WM_MOUSEACTIVATE = 33
WM_MOUSEMOVE = 512
WM_LBUTTONDOWN = 513
WM_LBUTTONUP = 514
WM_LBUTTONDBLCLK = 515
WM_RBUTTONDOWN = 516
WM_RBUTTONUP = 517
WM_RBUTTONDBLCLK = 518
WM_MBUTTONDOWN = 519
WM_MBUTTONUP = 520
WM_MBUTTONDBLCLK = 521
WM_MOUSEWHEEL = 522
WM_MOUSEFIRST = 512
WM_XBUTTONDOWN = 523
WM_XBUTTONUP = 524
WM_XBUTTONDBLCLK = 525
WM_MOUSELAST = 525
WM_MOUSEHOVER = 0x2A1
WM_MOUSELEAVE = 0x2A3
WM_CLIPBOARDUPDATE = 0x031D
)

View file

@ -0,0 +1,130 @@
package win32
import "unsafe"
const (
NIF_MESSAGE = 0x00000001
NIF_ICON = 0x00000002
NIF_TIP = 0x00000004
NIF_STATE = 0x00000008
NIF_INFO = 0x00000010
NIF_GUID = 0x00000020
NIF_REALTIME = 0x00000040
NIF_SHOWTIP = 0x00000080
NIM_ADD = 0x00000000
NIM_MODIFY = 0x00000001
NIM_DELETE = 0x00000002
NIM_SETFOCUS = 0x00000003
NIM_SETVERSION = 0x00000004
NIS_HIDDEN = 0x00000001
NIS_SHAREDICON = 0x00000002
NIN_BALLOONSHOW = 0x0402
NIN_BALLOONTIMEOUT = 0x0404
NIN_BALLOONUSERCLICK = 0x0405
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
)
type NOTIFYICONDATA struct {
CbSize uint32
HWnd uintptr
UID uint32
UFlags uint32
UCallbackMessage uint32
HIcon uintptr
SzTip [128]uint16
DwState uint32
DwStateMask uint32
SzInfo [256]uint16
UVersion uint32
SzInfoTitle [64]uint16
DwInfoFlags uint32
GUIDItem GUID
HBalloonIcon uintptr
}
func NotifyIcon(msg uint32, lpData *NOTIFYICONDATA) (int32, error) {
r, _, err := procShellNotifyIcon.Call(
uintptr(msg),
uintptr(unsafe.Pointer(lpData)))
if r == 0 {
return 0, err
}
return int32(r), nil
}
func LookupIconIdFromDirectoryEx(presbits uintptr, isIcon bool, cxDesired int, cyDesired int, flags uint) (int32, error) {
var icon uint32 = 0
if isIcon {
icon = 1
}
r, _, err := procLookupIconIdFromDirectoryEx.Call(
presbits,
uintptr(icon),
uintptr(cxDesired),
uintptr(cyDesired),
uintptr(flags),
)
if r == 0 {
return 0, err
}
return int32(r), nil
}
func CreateIconIndirect(data uintptr) (uintptr, error) {
r, _, err := procCreateIconIndirect.Call(
data,
)
if r == 0 {
return 0, err
}
return r, nil
}
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
}
func LoadImage(
hInst uintptr,
name *uint16,
type_ uint32,
cx, cy int32,
fuLoad uint32) (uintptr, error) {
r, _, err := procLoadImageW.Call(
hInst,
uintptr(unsafe.Pointer(name)),
uintptr(type_),
uintptr(cx),
uintptr(cy),
uintptr(fuLoad))
if r == 0 {
return 0, err
}
return r, nil
}

View file

@ -46,6 +46,10 @@ type DevWebServer struct {
devServerAddr string devServerAddr string
} }
func (d *DevWebServer) TrayMenuAdd(trayMenu *menu.TrayMenu) menu.TrayMenuImpl {
return d.desktopFrontend.TrayMenuAdd(trayMenu)
}
func (d *DevWebServer) WindowSetSystemDefaultTheme() { func (d *DevWebServer) WindowSetSystemDefaultTheme() {
d.desktopFrontend.WindowSetSystemDefaultTheme() d.desktopFrontend.WindowSetSystemDefaultTheme()
} }

View file

@ -102,4 +102,7 @@ type Frontend interface {
// Browser // Browser
BrowserOpenURL(url string) BrowserOpenURL(url string)
// Tray Menu
TrayMenuAdd(trayMenu *menu.TrayMenu) menu.TrayMenuImpl
} }

View file

@ -1,12 +1,15 @@
package menumanager package menumanager
import ( import "github.com/wailsapp/wails/v2/pkg/menu"
"encoding/json"
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu"
)
//
//import (
// "encoding/json"
// "fmt"
//
// "github.com/wailsapp/wails/v2/pkg/menu"
//)
//
type ContextMenu struct { type ContextMenu struct {
ID string ID string
ProcessedMenu *WailsMenu ProcessedMenu *WailsMenu
@ -14,48 +17,49 @@ type ContextMenu struct {
menu *menu.Menu menu *menu.Menu
} }
func (t *ContextMenu) AsJSON() (string, error) { //
data, err := json.Marshal(t) //func (t *ContextMenu) AsJSON() (string, error) {
if err != nil { // data, err := json.Marshal(t)
return "", err // if err != nil {
} // return "", err
return string(data), nil // }
} // return string(data), nil
//}
func NewContextMenu(contextMenu *menu.ContextMenu) *ContextMenu { //
//func NewContextMenu(contextMenu *menu.ContextMenu) *ContextMenu {
result := &ContextMenu{ //
ID: contextMenu.ID, // result := &ContextMenu{
menu: contextMenu.Menu, // ID: contextMenu.ID,
menuItemMap: NewMenuItemMap(), // menu: contextMenu.Menu,
} // menuItemMap: NewMenuItemMap(),
// }
result.menuItemMap.AddMenu(contextMenu.Menu) //
result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu) // result.menuItemMap.AddMenu(contextMenu.Menu)
// result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu)
return result //
} // return result
//}
func (m *Manager) AddContextMenu(contextMenu *menu.ContextMenu) { //
//func (m *Manager) AddContextMenu(contextMenu *menu.ContextMenu) {
newContextMenu := NewContextMenu(contextMenu) //
// newContextMenu := NewContextMenu(contextMenu)
// Save the references //
m.contextMenus[contextMenu.ID] = newContextMenu // // Save the references
m.contextMenuPointers[contextMenu] = contextMenu.ID // m.contextMenus[contextMenu.ID] = newContextMenu
} // m.contextMenuPointers[contextMenu] = contextMenu.ID
//}
func (m *Manager) UpdateContextMenu(contextMenu *menu.ContextMenu) (string, error) { //
contextMenuID, contextMenuKnown := m.contextMenuPointers[contextMenu] //func (m *Manager) UpdateContextMenu(contextMenu *menu.ContextMenu) (string, error) {
if !contextMenuKnown { // contextMenuID, contextMenuKnown := m.contextMenuPointers[contextMenu]
return "", fmt.Errorf("unknown Context Menu '%s'. Please add the context menu using AddContextMenu()", contextMenu.ID) // if !contextMenuKnown {
} // return "", fmt.Errorf("unknown Context Menu '%s'. Please add the context menu using AddContextMenu()", contextMenu.ID)
// }
// Create the updated context menu //
updatedContextMenu := NewContextMenu(contextMenu) // // Create the updated context menu
// updatedContextMenu := NewContextMenu(contextMenu)
// Save the reference //
m.contextMenus[contextMenuID] = updatedContextMenu // // Save the reference
// m.contextMenus[contextMenuID] = updatedContextMenu
return updatedContextMenu.AsJSON() //
} // return updatedContextMenu.AsJSON()
//}

View file

@ -2,6 +2,7 @@ package menumanager
import ( import (
"fmt" "fmt"
"github.com/wailsapp/wails/v2/pkg/menu" "github.com/wailsapp/wails/v2/pkg/menu"
) )

View file

@ -1,15 +1,11 @@
package menumanager package menumanager
import ( import (
"encoding/json"
"fmt"
"strconv" "strconv"
"strings"
"sync" "sync"
"github.com/leaanthony/go-ansi-parser" "github.com/leaanthony/go-ansi-parser"
"github.com/pkg/errors"
"github.com/wailsapp/wails/v2/pkg/menu" "github.com/wailsapp/wails/v2/pkg/menu"
) )
@ -32,7 +28,7 @@ type TrayMenu struct {
FontName string FontName string
Disabled bool Disabled bool
Tooltip string `json:",omitempty"` Tooltip string `json:",omitempty"`
Image string Image []byte
MacTemplateImage bool MacTemplateImage bool
RGBA string RGBA string
menuItemMap *MenuItemMap menuItemMap *MenuItemMap
@ -42,181 +38,182 @@ type TrayMenu struct {
StyledLabel []*ansi.StyledText `json:",omitempty"` StyledLabel []*ansi.StyledText `json:",omitempty"`
} }
func (t *TrayMenu) AsJSON() (string, error) { //
data, err := json.Marshal(t) //func (t *TrayMenu) AsJSON() (string, error) {
if err != nil { // data, err := json.Marshal(t)
return "", err // if err != nil {
} // return "", err
return string(data), nil // }
} // return string(data), nil
//}
func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu { //
//func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu {
// Parse ANSI text //
var styledLabel []*ansi.StyledText // // Parse ANSI text
tempLabel := trayMenu.Label // var styledLabel []*ansi.StyledText
if strings.Contains(tempLabel, "\033[") { // tempLabel := trayMenu.Label
parsedLabel, err := ansi.Parse(tempLabel) // if strings.Contains(tempLabel, "\033[") {
if err == nil { // parsedLabel, err := ansi.Parse(tempLabel)
styledLabel = parsedLabel // if err == nil {
} // styledLabel = parsedLabel
} // }
// }
result := &TrayMenu{ //
Label: trayMenu.Label, // result := &TrayMenu{
FontName: trayMenu.FontName, // Label: trayMenu.Label,
FontSize: trayMenu.FontSize, // FontName: trayMenu.FontName,
Disabled: trayMenu.Disabled, // FontSize: trayMenu.FontSize,
Tooltip: trayMenu.Tooltip, // Disabled: trayMenu.Disabled,
Image: trayMenu.Image, // Tooltip: trayMenu.Tooltip,
MacTemplateImage: trayMenu.MacTemplateImage, // Image: trayMenu.Image,
menu: trayMenu.Menu, // MacTemplateImage: trayMenu.MacTemplateImage,
RGBA: trayMenu.RGBA, // menu: trayMenu.Menu,
menuItemMap: NewMenuItemMap(), // RGBA: trayMenu.RGBA,
trayMenu: trayMenu, // menuItemMap: NewMenuItemMap(),
StyledLabel: styledLabel, // trayMenu: trayMenu,
} // StyledLabel: styledLabel,
// }
result.menuItemMap.AddMenu(trayMenu.Menu) //
result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu) // result.menuItemMap.AddMenu(trayMenu.Menu)
// result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu)
return result //
} // return result
//}
func (m *Manager) OnTrayMenuOpen(id string) { //
trayMenu, ok := m.trayMenus[id] //func (m *Manager) OnTrayMenuOpen(id string) {
if !ok { // trayMenu, ok := m.trayMenus[id]
return // if !ok {
} // return
if trayMenu.trayMenu.OnOpen == nil { // }
return // if trayMenu.trayMenu.OnOpen == nil {
} // return
go trayMenu.trayMenu.OnOpen() // }
} // go trayMenu.trayMenu.OnOpen()
//}
func (m *Manager) OnTrayMenuClose(id string) { //
trayMenu, ok := m.trayMenus[id] //func (m *Manager) OnTrayMenuClose(id string) {
if !ok { // trayMenu, ok := m.trayMenus[id]
return // if !ok {
} // return
if trayMenu.trayMenu.OnClose == nil { // }
return // if trayMenu.trayMenu.OnClose == nil {
} // return
go trayMenu.trayMenu.OnClose() // }
} // go trayMenu.trayMenu.OnClose()
//}
func (m *Manager) AddTrayMenu(trayMenu *menu.TrayMenu) (string, error) { //
newTrayMenu := NewTrayMenu(trayMenu) //func (m *Manager) AddTrayMenu(trayMenu *menu.TrayMenu) (string, error) {
// newTrayMenu := NewTrayMenu(trayMenu)
// Hook up a new ID //
trayID := generateTrayID() // // Hook up a new ID
newTrayMenu.ID = trayID // trayID := generateTrayID()
// newTrayMenu.ID = trayID
// Save the references //
m.trayMenus[trayID] = newTrayMenu // // Save the references
m.trayMenuPointers[trayMenu] = trayID // m.trayMenus[trayID] = newTrayMenu
// m.trayMenuPointers[trayMenu] = trayID
return newTrayMenu.AsJSON() //
} // return newTrayMenu.AsJSON()
//}
func (m *Manager) GetTrayID(trayMenu *menu.TrayMenu) (string, error) { //
trayID, exists := m.trayMenuPointers[trayMenu] //func (m *Manager) GetTrayID(trayMenu *menu.TrayMenu) (string, error) {
if !exists { // trayID, exists := m.trayMenuPointers[trayMenu]
return "", fmt.Errorf("Unable to find menu ID for tray menu!") // if !exists {
} // return "", fmt.Errorf("Unable to find menu ID for tray menu!")
return trayID, nil // }
} // return trayID, nil
//}
// SetTrayMenu updates or creates a menu //
func (m *Manager) SetTrayMenu(trayMenu *menu.TrayMenu) (string, error) { //// SetTrayMenu updates or creates a menu
trayID, trayMenuKnown := m.trayMenuPointers[trayMenu] //func (m *Manager) SetTrayMenu(trayMenu *menu.TrayMenu) (string, error) {
if !trayMenuKnown { // trayID, trayMenuKnown := m.trayMenuPointers[trayMenu]
return m.AddTrayMenu(trayMenu) // if !trayMenuKnown {
} // return m.AddTrayMenu(trayMenu)
// }
// Create the updated tray menu //
updatedTrayMenu := NewTrayMenu(trayMenu) // // Create the updated tray menu
updatedTrayMenu.ID = trayID // updatedTrayMenu := NewTrayMenu(trayMenu)
// updatedTrayMenu.ID = trayID
// Save the reference //
m.trayMenus[trayID] = updatedTrayMenu // // Save the reference
// m.trayMenus[trayID] = updatedTrayMenu
return updatedTrayMenu.AsJSON() //
} // return updatedTrayMenu.AsJSON()
//}
func (m *Manager) GetTrayMenus() ([]string, error) { //
result := []string{} //func (m *Manager) GetTrayMenus() ([]string, error) {
for _, trayMenu := range m.trayMenus { // result := []string{}
JSON, err := trayMenu.AsJSON() // for _, trayMenu := range m.trayMenus {
if err != nil { // JSON, err := trayMenu.AsJSON()
return nil, err // if err != nil {
} // return nil, err
result = append(result, JSON) // }
} // result = append(result, JSON)
// }
return result, nil //
} // return result, nil
//}
func (m *Manager) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) (string, error) { //
trayID, trayMenuKnown := m.trayMenuPointers[trayMenu] //func (m *Manager) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) (string, error) {
if !trayMenuKnown { // trayID, trayMenuKnown := m.trayMenuPointers[trayMenu]
return "", fmt.Errorf("[UpdateTrayMenuLabel] unknown tray id for tray %s", trayMenu.Label) // if !trayMenuKnown {
} // return "", fmt.Errorf("[UpdateTrayMenuLabel] unknown tray id for tray %s", trayMenu.Label)
// }
type LabelUpdate struct { //
ID string // type LabelUpdate struct {
Label string `json:",omitempty"` // ID string
FontName string `json:",omitempty"` // Label string `json:",omitempty"`
FontSize int // FontName string `json:",omitempty"`
RGBA string `json:",omitempty"` // FontSize int
Disabled bool // RGBA string `json:",omitempty"`
Tooltip string `json:",omitempty"` // Disabled bool
Image string `json:",omitempty"` // Tooltip string `json:",omitempty"`
MacTemplateImage bool // Image []byte `json:",omitempty"`
StyledLabel []*ansi.StyledText `json:",omitempty"` // MacTemplateImage bool
} // StyledLabel []*ansi.StyledText `json:",omitempty"`
// }
// Parse ANSI text //
var styledLabel []*ansi.StyledText // // Parse ANSI text
tempLabel := trayMenu.Label // var styledLabel []*ansi.StyledText
if strings.Contains(tempLabel, "\033[") { // tempLabel := trayMenu.Label
parsedLabel, err := ansi.Parse(tempLabel) // if strings.Contains(tempLabel, "\033[") {
if err == nil { // parsedLabel, err := ansi.Parse(tempLabel)
styledLabel = parsedLabel // if err == nil {
} // styledLabel = parsedLabel
} // }
// }
update := &LabelUpdate{ //
ID: trayID, // update := &LabelUpdate{
Label: trayMenu.Label, // ID: trayID,
FontName: trayMenu.FontName, // Label: trayMenu.Label,
FontSize: trayMenu.FontSize, // FontName: trayMenu.FontName,
Disabled: trayMenu.Disabled, // FontSize: trayMenu.FontSize,
Tooltip: trayMenu.Tooltip, // Disabled: trayMenu.Disabled,
Image: trayMenu.Image, // Tooltip: trayMenu.Tooltip,
MacTemplateImage: trayMenu.MacTemplateImage, // Image: trayMenu.Image,
RGBA: trayMenu.RGBA, // MacTemplateImage: trayMenu.MacTemplateImage,
StyledLabel: styledLabel, // RGBA: trayMenu.RGBA,
} // StyledLabel: styledLabel,
// }
data, err := json.Marshal(update) //
if err != nil { // data, err := json.Marshal(update)
return "", errors.Wrap(err, "[UpdateTrayMenuLabel] ") // if err != nil {
} // return "", errors.Wrap(err, "[UpdateTrayMenuLabel] ")
// }
return string(data), nil //
// return string(data), nil
} //
//}
func (m *Manager) GetContextMenus() ([]string, error) { //
result := []string{} //func (m *Manager) GetContextMenus() ([]string, error) {
for _, contextMenu := range m.contextMenus { // result := []string{}
JSON, err := contextMenu.AsJSON() // for _, contextMenu := range m.contextMenus {
if err != nil { // JSON, err := contextMenu.AsJSON()
return nil, err // if err != nil {
} // return nil, err
result = append(result, JSON) // }
} // result = append(result, JSON)
// }
return result, nil //
} // return result, nil
//}

3
v2/pkg/events/events.go Normal file
View file

@ -0,0 +1,3 @@
package events
const ThemeChanged = ":wails:themechanged"

View file

@ -44,16 +44,23 @@ func (m *Menu) AddRadio(label string, checked bool, accelerator *keys.Accelerato
} }
// AddSeparator adds a separator to the menu // AddSeparator adds a separator to the menu
func (m *Menu) AddSeparator() { func (m *Menu) AddSeparator() *MenuItem {
item := Separator() item := Separator()
m.Append(item) m.Append(item)
return item
} }
func (m *Menu) AddSubmenu(label string) *Menu { func (m *Menu) AddSubmenu(label string) *MenuItem {
submenu := NewMenu() submenu := NewMenu()
item := SubMenu(label, submenu) item := SubMenu(label, submenu)
m.Append(item) m.Append(item)
return submenu return item
}
func (m *Menu) InsertSubmenu(label string, submenu *Menu) *MenuItem {
item := SubMenu(label, submenu)
m.Append(item)
return item
} }
func (m *Menu) Prepend(item *MenuItem) { func (m *Menu) Prepend(item *MenuItem) {

View file

@ -6,6 +6,11 @@ import (
"github.com/wailsapp/wails/v2/pkg/menu/keys" "github.com/wailsapp/wails/v2/pkg/menu/keys"
) )
type MenuItemImpl interface {
SetChecked(bool)
SetLabel(string)
}
// MenuItem represents a menuitem contained in a menu // MenuItem represents a menuitem contained in a menu
type MenuItem struct { type MenuItem struct {
// Label is what appears as the menu text // Label is what appears as the menu text
@ -53,6 +58,9 @@ type MenuItem struct {
// Used for locking when removing elements // Used for locking when removing elements
removeLock sync.Mutex removeLock sync.Mutex
// Implementation of the runtime methods
Impl MenuItemImpl
} }
// Parent returns the parent of the menu item. // Parent returns the parent of the menu item.
@ -216,6 +224,21 @@ func (m *MenuItem) insertItemAtIndex(index int, target *MenuItem) bool {
return true return true
} }
func (m *MenuItem) SetChecked(b bool) {
if m.Checked != b {
m.Checked = b
m.Impl.SetChecked(b)
}
}
func (m *MenuItem) SetLabel(name string) {
if m.Label == name {
return
}
m.Label = name
m.Impl.SetLabel(name)
}
// Text is a helper to create basic Text menu items // Text is a helper to create basic Text menu items
func Text(label string, accelerator *keys.Accelerator, click Callback) *MenuItem { func Text(label string, accelerator *keys.Accelerator, click Callback) *MenuItem {
return &MenuItem{ return &MenuItem{

View file

@ -1,20 +1,95 @@
package menu package menu
import (
"context"
"log"
goruntime "runtime"
"github.com/wailsapp/wails/v2/pkg/events"
)
type TrayMenuAdd interface {
TrayMenuAdd(menu *TrayMenu) TrayMenuImpl
}
type TrayMenuImpl interface {
SetLabel(string)
SetImage(*TrayImage)
SetMenu(*Menu)
}
type EventsImpl interface {
On(eventName string, callback func(...interface{}))
}
type ImagePosition int
const (
ImageLeading ImagePosition = 0
ImageOnly ImagePosition = 1
ImageLeft ImagePosition = 2
ImageRight ImagePosition = 3
ImageBelow ImagePosition = 4
ImageAbove ImagePosition = 5
ImageOverlaps ImagePosition = 6
NoImage ImagePosition = 7
ImageTrailing ImagePosition = 8
)
type TraySizing int
const (
Variable TraySizing = 0
Square TraySizing = 1
)
type TrayImage struct {
// Bitmaps hold images for different scaling factors
// First = 1x, Second = 2x, etc
Bitmaps [][]byte
BitmapsDark [][]byte
IsTemplate bool
Position ImagePosition
}
func (t *TrayImage) getBestBitmap(scale int, isDarkMode bool) []byte {
bitmapsToCheck := t.Bitmaps
if isDarkMode {
bitmapsToCheck = t.BitmapsDark
}
if scale < 1 || scale >= len(bitmapsToCheck) {
return nil
}
for i := scale; i > 0; i-- {
if bitmapsToCheck[i] != nil {
return bitmapsToCheck[i]
}
}
return nil
}
// GetBestBitmap will attempt to return the best bitmap for the theme
// If dark theme is used and no dark theme bitmap exists, then it will
// revert to light theme bitmaps
func (t *TrayImage) GetBestBitmap(scale int, isDarkMode bool) []byte {
var result []byte
if isDarkMode {
result = t.getBestBitmap(scale, true)
if result != nil {
return result
}
}
return t.getBestBitmap(scale, false)
}
// TrayMenu are the options // TrayMenu are the options
type TrayMenu struct { type TrayMenu struct {
ctx context.Context
// Label is the text we wish to display in the tray // Label is the text we wish to display in the tray
Label string Label string
// Image is the name of the tray icon we wish to display. Image *TrayImage
// These are read up during build from <projectdir>/trayicons and
// the filenames are used as IDs, minus the extension
// EG: <projectdir>/trayicons/main.png can be referenced here with "main"
// If the image is not a filename, it will be treated as base64 image data
Image string
// MacTemplateImage indicates that on a Mac, this image is a template image
MacTemplateImage bool
// Text Colour // Text Colour
RGBA string RGBA string
@ -27,7 +102,7 @@ type TrayMenu struct {
Tooltip string Tooltip string
// Callback function when menu clicked // Callback function when menu clicked
//Click Callback `json:"-"` Click Callback
// Disabled makes the item unselectable // Disabled makes the item unselectable
Disabled bool Disabled bool
@ -40,4 +115,67 @@ type TrayMenu struct {
// OnClose is called when the Menu is closed // OnClose is called when the Menu is closed
OnClose func() OnClose func()
/* Mac Options */
Sizing TraySizing
// This is the reference to the OS specific implementation
impl TrayMenuImpl
// Theme change callback
themeChangeCallback func(data ...interface{})
}
func NewTrayMenu() *TrayMenu {
return &TrayMenu{}
}
func (t *TrayMenu) Show(ctx context.Context) {
if ctx == nil {
log.Fatal("TrayMenu.Show() called before Run()")
}
t.ctx = ctx
result := ctx.Value("frontend")
if result == nil {
pc, _, _, _ := goruntime.Caller(1)
funcName := goruntime.FuncForPC(pc).Name()
log.Fatalf("invalid context at '%s'", funcName)
}
t.impl = result.(TrayMenuAdd).TrayMenuAdd(t)
if t.themeChangeCallback == nil {
t.themeChangeCallback = func(data ...interface{}) {
println("Update button image")
if t.Image != nil {
// Update the image
t.SetImage(t.Image)
}
}
result := ctx.Value("events")
if result != nil {
result.(EventsImpl).On(events.ThemeChanged, t.themeChangeCallback)
}
}
}
func (t *TrayMenu) SetLabel(label string) {
t.Label = label
if t.impl != nil {
t.impl.SetLabel(label)
}
}
func (t *TrayMenu) SetImage(image *TrayImage) {
t.Image = image
if t.impl != nil {
t.impl.SetImage(image)
}
}
func (t *TrayMenu) SetMenu(menu *Menu) {
t.Menu = menu
if t.impl != nil {
t.impl.SetMenu(menu)
}
} }

View file

@ -1,12 +1,12 @@
package mac package mac
//type ActivationPolicy int type ActivationPolicy int
//
//const ( const (
// NSApplicationActivationPolicyRegular ActivationPolicy = 0 NSApplicationActivationPolicyRegular ActivationPolicy = 0
// NSApplicationActivationPolicyAccessory ActivationPolicy = 1 NSApplicationActivationPolicyAccessory ActivationPolicy = 1
// NSApplicationActivationPolicyProhibited ActivationPolicy = 2 NSApplicationActivationPolicyProhibited ActivationPolicy = 2
//) )
type AboutInfo struct { type AboutInfo struct {
Title string Title string
@ -20,7 +20,7 @@ type Options struct {
Appearance AppearanceType Appearance AppearanceType
WebviewIsTransparent bool WebviewIsTransparent bool
WindowIsTranslucent bool WindowIsTranslucent bool
//ActivationPolicy ActivationPolicy About *AboutInfo
About *AboutInfo ActivationPolicy ActivationPolicy
//URLHandlers map[string]func(string) //URLHandlers map[string]func(string)
} }

View file

@ -6,19 +6,19 @@ sidebar_position: 1
## Overview ## Overview
Wails is a project that enables you to write desktop apps using Go and web technologies. Wails is a toolkit that enables you to write beautiful desktop apps using Go.
Consider it a lightweight and fast Electron alternative for Go. You can easily build applications with the flexibility Consider it a lightweight and fast Electron alternative for Go. You can easily build applications with the flexibility
and power of Go, combined with a rich, modern frontend. and power of Go, with modern frontend technology.
Wails doesn't hold back with the eye candy either! This is [xbar](https://xbarapp.com) - a desktop application for MacOS Wails also offers out-of-the-box integration with the native system libraries, providing native menus, dialogs,
written using Wails. It has menus, supports light and dark desktop themes, and the main window uses translucency that windowing functions, as well as dark and light themes. It also provides a simple way to use native window translucency,
gives it that 'frosty' effect of a native app. allowing you to create modern, beautiful applications.
<p class="text--center"> <p class="text--center">
<a href="https://xbarapp.com"> <a href="https://varly.app">
<img src="/img/xbar-app-preview-2.png" width="75%" /> <img src="/img/varly1.png" width="75%"/>
</a> </a>
</p> </p>
## Quick Start Templates ## Quick Start Templates

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB