diff --git a/.gitignore b/.gitignore index 32e4ec11e..d621a4358 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ v2/test/kitchensink/frontend/package.json.md5 !v2/internal/ffenestri/windows/x64/WebView2Loader.dll .idea/ v2/cmd/wails/internal/commands/initialise/templates/testtemplates/ +v2/internal/frontend/desktop/darwin/test.xcodeproj \ No newline at end of file diff --git a/v2/internal/frontend/desktop/darwin/AppDelegate.h b/v2/internal/frontend/desktop/darwin/AppDelegate.h index e2dd841c9..56399ee9f 100644 --- a/v2/internal/frontend/desktop/darwin/AppDelegate.h +++ b/v2/internal/frontend/desktop/darwin/AppDelegate.h @@ -17,6 +17,7 @@ @property bool startHidden; @property bool startFullscreen; @property (retain) WailsWindow* mainWindow; +@property int activationPolicy; @end diff --git a/v2/internal/frontend/desktop/darwin/AppDelegate.m b/v2/internal/frontend/desktop/darwin/AppDelegate.m index 6d46deae4..0f47caf67 100644 --- a/v2/internal/frontend/desktop/darwin/AppDelegate.m +++ b/v2/internal/frontend/desktop/darwin/AppDelegate.m @@ -9,6 +9,7 @@ #import #import "AppDelegate.h" +#import "message.h" @implementation AppDelegate - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender { @@ -25,6 +26,8 @@ } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { + processNotification(0); // Notify Go + [NSApp setActivationPolicy:self.activationPolicy]; [NSApp activateIgnoringOtherApps:YES]; if ( self.startFullscreen ) { NSWindowCollectionBehavior behaviour = [self.mainWindow collectionBehavior]; diff --git a/v2/internal/frontend/desktop/darwin/Application.h b/v2/internal/frontend/desktop/darwin/Application.h index d61fd6395..bebaf84f3 100644 --- a/v2/internal/frontend/desktop/darwin/Application.h +++ b/v2/internal/frontend/desktop/darwin/Application.h @@ -18,7 +18,7 @@ #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); -void Run(void*, const char* url); +void Run(void *inctx, const char* url, int activationPolicy); void SetTitle(void* ctx, const char *title); void Center(void* ctx); @@ -57,6 +57,16 @@ void AppendSubmenu(void* parent, void* child); void AppendRole(void *inctx, void *inMenu, int role); void SetAsApplicationMenu(void *inctx, void *inMenu); 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* 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); + +int ScalingFactor(void *ctx); + #endif /* Application_h */ diff --git a/v2/internal/frontend/desktop/darwin/Application.m b/v2/internal/frontend/desktop/darwin/Application.m index 6673d07c5..22095d23a 100644 --- a/v2/internal/frontend/desktop/darwin/Application.m +++ b/v2/internal/frontend/desktop/darwin/Application.m @@ -11,6 +11,7 @@ #import "AppDelegate.h" #import "WailsMenu.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) { @@ -19,7 +20,7 @@ WailsContext* Create(const char* title, int width, int height, int frameless, in WailsContext *result = [WailsContext new]; result.debug = debug; - + if ( windowStartState == WindowStartsFullscreen ) { 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 SetTitle:safeInit(title)]; [result Center]; - + switch( windowStartState ) { case WindowStartsMaximised: [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) { WailsContext *ctx = (__bridge WailsContext*) inctx; NSRect frame = [ctx.mainWindow frame]; @@ -277,6 +282,55 @@ void AppendRole(void *inctx, void *inMenu, int 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) { NSString *title = @""; if (name != nil) { @@ -301,8 +355,8 @@ void SetAsApplicationMenu(void *inctx, void *inMenu) { void UpdateApplicationMenu(void *inctx) { WailsContext *ctx = (__bridge WailsContext*) inctx; ON_MAIN_THREAD( - NSApplication *app = [NSApplication sharedApplication]; - [app setMainMenu:ctx.applicationMenu]; + NSApplication *app = [NSApplication sharedApplication]; + [app setMainMenu:ctx.applicationMenu]; ) } @@ -327,7 +381,7 @@ void UpdateMenuItem(void* nsmenuitem, int checked) { ON_MAIN_THREAD( WailsMenuItem *menuItem = (__bridge WailsMenuItem*) nsmenuitem; [menuItem setState:(checked == 1?NSControlStateValueOn:NSControlStateValueOff)]; - ) + ) } @@ -336,12 +390,39 @@ void AppendSeparator(void* inMenu) { [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; NSApplication *app = [NSApplication sharedApplication]; AppDelegate* delegate = [AppDelegate new]; + delegate.activationPolicy = activationPolicy; [app setDelegate:(id)delegate]; ctx.appdelegate = delegate; delegate.mainWindow = ctx.mainWindow; diff --git a/v2/internal/frontend/desktop/darwin/WailsContext.h b/v2/internal/frontend/desktop/darwin/WailsContext.h index 8982615d5..9b9e6103b 100644 --- a/v2/internal/frontend/desktop/darwin/WailsContext.h +++ b/v2/internal/frontend/desktop/darwin/WailsContext.h @@ -23,6 +23,7 @@ @property NSSize userMinSize; @property NSSize userMaxSize; +@property int activationPolicy; - (BOOL) canBecomeKeyWindow; - (void) applyWindowConstraints; diff --git a/v2/internal/frontend/desktop/darwin/frontend.go b/v2/internal/frontend/desktop/darwin/frontend.go index bdf5fd9d6..b37815b9e 100644 --- a/v2/internal/frontend/desktop/darwin/frontend.go +++ b/v2/internal/frontend/desktop/darwin/frontend.go @@ -3,15 +3,6 @@ package darwin -/* -#cgo CFLAGS: -x objective-c -#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit -#import -#import "Application.h" -#import "WailsContext.h" - -#include -*/ import "C" import ( "bytes" @@ -31,14 +22,33 @@ import ( "github.com/wailsapp/wails/v2/internal/frontend" "github.com/wailsapp/wails/v2/internal/frontend/assetserver" "github.com/wailsapp/wails/v2/internal/logger" + "github.com/wailsapp/wails/v2/pkg/menu" "github.com/wailsapp/wails/v2/pkg/options" ) +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework WebKit +#import +#import "Application.h" +#import "WailsContext.h" + +#include +*/ +import "C" + const startURL = "wails://wails/" +type NotificationType uint8 + +const ( + ApplicationDidFinishLaunching NotificationType = 0 +) + var messageBuffer = make(chan string, 100) var requestBuffer = make(chan *request, 100) var callbackBuffer = make(chan uint, 10) +var notificationBuffer = make(chan NotificationType, 10) type Frontend struct { @@ -57,15 +67,22 @@ type Frontend struct { mainWindow *Window bindings *binding.Bindings 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 { result := &Frontend{ - frontendOptions: appoptions, - logger: myLogger, - bindings: appBindings, - dispatcher: dispatcher, - ctx: ctx, + frontendOptions: appoptions, + logger: myLogger, + bindings: appBindings, + dispatcher: dispatcher, + ctx: ctx, + trayMenus: make(map[*menu.TrayMenu]*NSTrayMenu), + notificationCallbacks: make(map[NotificationType][]func()), } result.startURL, _ = url.Parse(startURL) @@ -88,10 +105,17 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger. go result.startMessageProcessor() go result.startCallbackProcessor() + go result.startNotificationsProcessor() + + result.registerNotificationCallback(ApplicationDidFinishLaunching, result.processTrayMenus) return result } +func (f *Frontend) registerNotificationCallback(notificationType NotificationType, callback func()) { + f.notificationCallbacks[notificationType] = append(f.notificationCallbacks[notificationType], callback) +} + func (f *Frontend) startMessageProcessor() { for message := range messageBuffer { 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() { f.ExecJS("runtime.WindowReload();") @@ -149,7 +178,11 @@ func (f *Frontend) Run(ctx context.Context) error { 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 } @@ -396,6 +429,11 @@ func processMessage(message *C.char) { messageBuffer <- goMessage } +//export processNotification +func processNotification(notification NotificationType) { + notificationBuffer <- notification +} + //export processURLRequest func processURLRequest(ctx unsafe.Pointer, url *C.char, method *C.char, headers *C.char, body unsafe.Pointer, bodyLen C.int) { var goBody []byte @@ -416,3 +454,19 @@ func processURLRequest(ctx unsafe.Pointer, url *C.char, method *C.char, headers func processCallback(callbackID uint) { 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) + } +} diff --git a/v2/internal/frontend/desktop/darwin/menu.go b/v2/internal/frontend/desktop/darwin/menu.go index 8afb63fb9..f22097b01 100644 --- a/v2/internal/frontend/desktop/darwin/menu.go +++ b/v2/internal/frontend/desktop/darwin/menu.go @@ -14,12 +14,86 @@ package darwin */ import "C" import ( + "fmt" + "sync" "unsafe" + "github.com/google/uuid" + "github.com/wailsapp/wails/v2/pkg/menu" "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 { context unsafe.Pointer nsmenu unsafe.Pointer @@ -53,6 +127,15 @@ type MenuItem struct { 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 { c := NewCalloc() defer c.Free() @@ -69,6 +152,7 @@ func (m *NSMenu) AddMenuItem(menuItem *menu.MenuItem) *MenuItem { 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)) + menuItem.Impl = result return result } diff --git a/v2/internal/frontend/desktop/darwin/message.h b/v2/internal/frontend/desktop/darwin/message.h index f0a5f482b..a6e27ee0d 100644 --- a/v2/internal/frontend/desktop/darwin/message.h +++ b/v2/internal/frontend/desktop/darwin/message.h @@ -20,6 +20,8 @@ void processMessageDialogResponse(int); void processOpenFileDialogResponse(const char*); void processSaveFileDialogResponse(const char*); void processCallback(int); +void processNotification(int); +void objectCreated(int, void*); #ifdef __cplusplus } diff --git a/v2/internal/frontend/desktop/darwin/traymenu.go b/v2/internal/frontend/desktop/darwin/traymenu.go new file mode 100644 index 000000000..6539b359e --- /dev/null +++ b/v2/internal/frontend/desktop/darwin/traymenu.go @@ -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 +#import "Application.h" +#import "WailsContext.h" + +#include +*/ +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)) +} diff --git a/v2/internal/frontend/desktop/darwin/window.go b/v2/internal/frontend/desktop/darwin/window.go index cbb79c325..0f14eab47 100644 --- a/v2/internal/frontend/desktop/darwin/window.go +++ b/v2/internal/frontend/desktop/darwin/window.go @@ -40,6 +40,10 @@ func bool2Cint(value bool) C.int { return C.int(0) } +func ScalingFactor(window *Window) int { + return int(C.ScalingFactor(window.context)) +} + func NewWindow(frontendOptions *options.App, debugMode bool) *Window { c := NewCalloc() @@ -119,9 +123,9 @@ func (w *Window) Center() { C.Center(w.context) } -func (w *Window) Run(url string) { +func (w *Window) Run(url string, activationPolicy C.int) { _url := C.CString(url) - C.Run(w.context, _url) + C.Run(w.context, _url, activationPolicy) C.free(unsafe.Pointer(_url)) } diff --git a/v2/internal/frontend/desktop/windows/png.go b/v2/internal/frontend/desktop/windows/png.go new file mode 100644 index 000000000..87e7a8d96 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/png.go @@ -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 +} diff --git a/v2/internal/frontend/desktop/windows/traymenu.go b/v2/internal/frontend/desktop/windows/traymenu.go new file mode 100644 index 000000000..a2efee650 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/traymenu.go @@ -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 +} diff --git a/v2/internal/frontend/desktop/windows/win32/consts.go b/v2/internal/frontend/desktop/windows/win32/consts.go index 8731664b7..e976001f6 100644 --- a/v2/internal/frontend/desktop/windows/win32/consts.go +++ b/v2/internal/frontend/desktop/windows/win32/consts.go @@ -12,12 +12,21 @@ type HRESULT int32 type HANDLE uintptr var ( - moduser32 = syscall.NewLazyDLL("user32.dll") - procSystemParametersInfo = moduser32.NewProc("SystemParametersInfoW") - procGetWindowLong = moduser32.NewProc("GetWindowLongW") - procSetClassLong = moduser32.NewProc("SetClassLongW") - procSetClassLongPtr = moduser32.NewProc("SetClassLongPtrW") - procShowWindow = moduser32.NewProc("ShowWindow") + moduser32 = syscall.NewLazyDLL("user32.dll") + procSystemParametersInfo = moduser32.NewProc("SystemParametersInfoW") + procGetWindowLong = moduser32.NewProc("GetWindowLongW") + procSetClassLong = moduser32.NewProc("SetClassLongW") + procSetClassLongPtr = moduser32.NewProc("SetClassLongPtrW") + 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 ( moddwmapi = syscall.NewLazyDLL("dwmapi.dll") @@ -36,3 +45,217 @@ func IsWindowsVersionAtLeast(major, minor, buildNumber int) bool { windowsVersion.Minor >= minor && 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 +) diff --git a/v2/internal/frontend/desktop/windows/win32/tray.go b/v2/internal/frontend/desktop/windows/win32/tray.go new file mode 100644 index 000000000..897106139 --- /dev/null +++ b/v2/internal/frontend/desktop/windows/win32/tray.go @@ -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 +} diff --git a/v2/internal/frontend/devserver/devserver.go b/v2/internal/frontend/devserver/devserver.go index 1198183ef..784f3e203 100644 --- a/v2/internal/frontend/devserver/devserver.go +++ b/v2/internal/frontend/devserver/devserver.go @@ -46,6 +46,10 @@ type DevWebServer struct { devServerAddr string } +func (d *DevWebServer) TrayMenuAdd(trayMenu *menu.TrayMenu) menu.TrayMenuImpl { + return d.desktopFrontend.TrayMenuAdd(trayMenu) +} + func (d *DevWebServer) WindowSetSystemDefaultTheme() { d.desktopFrontend.WindowSetSystemDefaultTheme() } diff --git a/v2/internal/frontend/frontend.go b/v2/internal/frontend/frontend.go index 4e2da2b27..fa5da491c 100644 --- a/v2/internal/frontend/frontend.go +++ b/v2/internal/frontend/frontend.go @@ -102,4 +102,7 @@ type Frontend interface { // Browser BrowserOpenURL(url string) + + // Tray Menu + TrayMenuAdd(trayMenu *menu.TrayMenu) menu.TrayMenuImpl } diff --git a/v2/internal/menumanager/contextmenu.go b/v2/internal/menumanager/contextmenu.go index 77c47891c..b87b476b7 100644 --- a/v2/internal/menumanager/contextmenu.go +++ b/v2/internal/menumanager/contextmenu.go @@ -1,12 +1,15 @@ package menumanager -import ( - "encoding/json" - "fmt" - - "github.com/wailsapp/wails/v2/pkg/menu" -) +import "github.com/wailsapp/wails/v2/pkg/menu" +// +//import ( +// "encoding/json" +// "fmt" +// +// "github.com/wailsapp/wails/v2/pkg/menu" +//) +// type ContextMenu struct { ID string ProcessedMenu *WailsMenu @@ -14,48 +17,49 @@ type ContextMenu struct { menu *menu.Menu } -func (t *ContextMenu) AsJSON() (string, error) { - data, err := json.Marshal(t) - if err != nil { - return "", err - } - return string(data), nil -} - -func NewContextMenu(contextMenu *menu.ContextMenu) *ContextMenu { - - result := &ContextMenu{ - ID: contextMenu.ID, - menu: contextMenu.Menu, - menuItemMap: NewMenuItemMap(), - } - - result.menuItemMap.AddMenu(contextMenu.Menu) - result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu) - - return result -} - -func (m *Manager) AddContextMenu(contextMenu *menu.ContextMenu) { - - newContextMenu := NewContextMenu(contextMenu) - - // Save the references - m.contextMenus[contextMenu.ID] = newContextMenu - m.contextMenuPointers[contextMenu] = contextMenu.ID -} - -func (m *Manager) UpdateContextMenu(contextMenu *menu.ContextMenu) (string, error) { - contextMenuID, contextMenuKnown := m.contextMenuPointers[contextMenu] - 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) - - // Save the reference - m.contextMenus[contextMenuID] = updatedContextMenu - - return updatedContextMenu.AsJSON() -} +// +//func (t *ContextMenu) AsJSON() (string, error) { +// data, err := json.Marshal(t) +// if err != nil { +// return "", err +// } +// return string(data), nil +//} +// +//func NewContextMenu(contextMenu *menu.ContextMenu) *ContextMenu { +// +// result := &ContextMenu{ +// ID: contextMenu.ID, +// menu: contextMenu.Menu, +// menuItemMap: NewMenuItemMap(), +// } +// +// result.menuItemMap.AddMenu(contextMenu.Menu) +// result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu) +// +// return result +//} +// +//func (m *Manager) AddContextMenu(contextMenu *menu.ContextMenu) { +// +// newContextMenu := NewContextMenu(contextMenu) +// +// // Save the references +// m.contextMenus[contextMenu.ID] = newContextMenu +// m.contextMenuPointers[contextMenu] = contextMenu.ID +//} +// +//func (m *Manager) UpdateContextMenu(contextMenu *menu.ContextMenu) (string, error) { +// contextMenuID, contextMenuKnown := m.contextMenuPointers[contextMenu] +// 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) +// +// // Save the reference +// m.contextMenus[contextMenuID] = updatedContextMenu +// +// return updatedContextMenu.AsJSON() +//} diff --git a/v2/internal/menumanager/menumanager.go b/v2/internal/menumanager/menumanager.go index ea7939415..92756edff 100644 --- a/v2/internal/menumanager/menumanager.go +++ b/v2/internal/menumanager/menumanager.go @@ -2,6 +2,7 @@ package menumanager import ( "fmt" + "github.com/wailsapp/wails/v2/pkg/menu" ) diff --git a/v2/internal/menumanager/traymenu.go b/v2/internal/menumanager/traymenu.go index aed5b05ac..ebbbe7c3b 100644 --- a/v2/internal/menumanager/traymenu.go +++ b/v2/internal/menumanager/traymenu.go @@ -1,15 +1,11 @@ package menumanager import ( - "encoding/json" - "fmt" "strconv" - "strings" "sync" "github.com/leaanthony/go-ansi-parser" - "github.com/pkg/errors" "github.com/wailsapp/wails/v2/pkg/menu" ) @@ -32,7 +28,7 @@ type TrayMenu struct { FontName string Disabled bool Tooltip string `json:",omitempty"` - Image string + Image []byte MacTemplateImage bool RGBA string menuItemMap *MenuItemMap @@ -42,181 +38,182 @@ type TrayMenu struct { StyledLabel []*ansi.StyledText `json:",omitempty"` } -func (t *TrayMenu) AsJSON() (string, error) { - data, err := json.Marshal(t) - if err != nil { - return "", err - } - return string(data), nil -} - -func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu { - - // Parse ANSI text - var styledLabel []*ansi.StyledText - tempLabel := trayMenu.Label - if strings.Contains(tempLabel, "\033[") { - parsedLabel, err := ansi.Parse(tempLabel) - if err == nil { - styledLabel = parsedLabel - } - } - - result := &TrayMenu{ - Label: trayMenu.Label, - FontName: trayMenu.FontName, - FontSize: trayMenu.FontSize, - Disabled: trayMenu.Disabled, - Tooltip: trayMenu.Tooltip, - Image: trayMenu.Image, - MacTemplateImage: trayMenu.MacTemplateImage, - menu: trayMenu.Menu, - RGBA: trayMenu.RGBA, - menuItemMap: NewMenuItemMap(), - trayMenu: trayMenu, - StyledLabel: styledLabel, - } - - result.menuItemMap.AddMenu(trayMenu.Menu) - result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu) - - return result -} - -func (m *Manager) OnTrayMenuOpen(id string) { - trayMenu, ok := m.trayMenus[id] - if !ok { - return - } - if trayMenu.trayMenu.OnOpen == nil { - return - } - go trayMenu.trayMenu.OnOpen() -} - -func (m *Manager) OnTrayMenuClose(id string) { - trayMenu, ok := m.trayMenus[id] - if !ok { - return - } - if trayMenu.trayMenu.OnClose == nil { - return - } - go trayMenu.trayMenu.OnClose() -} - -func (m *Manager) AddTrayMenu(trayMenu *menu.TrayMenu) (string, error) { - newTrayMenu := NewTrayMenu(trayMenu) - - // Hook up a new ID - trayID := generateTrayID() - newTrayMenu.ID = trayID - - // Save the references - m.trayMenus[trayID] = newTrayMenu - m.trayMenuPointers[trayMenu] = trayID - - return newTrayMenu.AsJSON() -} - -func (m *Manager) GetTrayID(trayMenu *menu.TrayMenu) (string, error) { - trayID, exists := m.trayMenuPointers[trayMenu] - if !exists { - return "", fmt.Errorf("Unable to find menu ID for tray menu!") - } - return trayID, nil -} - -// SetTrayMenu updates or creates a menu -func (m *Manager) SetTrayMenu(trayMenu *menu.TrayMenu) (string, error) { - trayID, trayMenuKnown := m.trayMenuPointers[trayMenu] - if !trayMenuKnown { - return m.AddTrayMenu(trayMenu) - } - - // Create the updated tray menu - updatedTrayMenu := NewTrayMenu(trayMenu) - updatedTrayMenu.ID = trayID - - // Save the reference - m.trayMenus[trayID] = updatedTrayMenu - - return updatedTrayMenu.AsJSON() -} - -func (m *Manager) GetTrayMenus() ([]string, error) { - result := []string{} - for _, trayMenu := range m.trayMenus { - JSON, err := trayMenu.AsJSON() - if err != nil { - return nil, err - } - result = append(result, JSON) - } - - return result, nil -} - -func (m *Manager) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) (string, error) { - trayID, trayMenuKnown := m.trayMenuPointers[trayMenu] - if !trayMenuKnown { - return "", fmt.Errorf("[UpdateTrayMenuLabel] unknown tray id for tray %s", trayMenu.Label) - } - - type LabelUpdate struct { - ID string - Label string `json:",omitempty"` - FontName string `json:",omitempty"` - FontSize int - RGBA string `json:",omitempty"` - Disabled bool - Tooltip string `json:",omitempty"` - Image string `json:",omitempty"` - MacTemplateImage bool - StyledLabel []*ansi.StyledText `json:",omitempty"` - } - - // Parse ANSI text - var styledLabel []*ansi.StyledText - tempLabel := trayMenu.Label - if strings.Contains(tempLabel, "\033[") { - parsedLabel, err := ansi.Parse(tempLabel) - if err == nil { - styledLabel = parsedLabel - } - } - - update := &LabelUpdate{ - ID: trayID, - Label: trayMenu.Label, - FontName: trayMenu.FontName, - FontSize: trayMenu.FontSize, - Disabled: trayMenu.Disabled, - Tooltip: trayMenu.Tooltip, - Image: trayMenu.Image, - MacTemplateImage: trayMenu.MacTemplateImage, - RGBA: trayMenu.RGBA, - StyledLabel: styledLabel, - } - - data, err := json.Marshal(update) - if err != nil { - return "", errors.Wrap(err, "[UpdateTrayMenuLabel] ") - } - - return string(data), nil - -} - -func (m *Manager) GetContextMenus() ([]string, error) { - result := []string{} - for _, contextMenu := range m.contextMenus { - JSON, err := contextMenu.AsJSON() - if err != nil { - return nil, err - } - result = append(result, JSON) - } - - return result, nil -} +// +//func (t *TrayMenu) AsJSON() (string, error) { +// data, err := json.Marshal(t) +// if err != nil { +// return "", err +// } +// return string(data), nil +//} +// +//func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu { +// +// // Parse ANSI text +// var styledLabel []*ansi.StyledText +// tempLabel := trayMenu.Label +// if strings.Contains(tempLabel, "\033[") { +// parsedLabel, err := ansi.Parse(tempLabel) +// if err == nil { +// styledLabel = parsedLabel +// } +// } +// +// result := &TrayMenu{ +// Label: trayMenu.Label, +// FontName: trayMenu.FontName, +// FontSize: trayMenu.FontSize, +// Disabled: trayMenu.Disabled, +// Tooltip: trayMenu.Tooltip, +// Image: trayMenu.Image, +// MacTemplateImage: trayMenu.MacTemplateImage, +// menu: trayMenu.Menu, +// RGBA: trayMenu.RGBA, +// menuItemMap: NewMenuItemMap(), +// trayMenu: trayMenu, +// StyledLabel: styledLabel, +// } +// +// result.menuItemMap.AddMenu(trayMenu.Menu) +// result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu) +// +// return result +//} +// +//func (m *Manager) OnTrayMenuOpen(id string) { +// trayMenu, ok := m.trayMenus[id] +// if !ok { +// return +// } +// if trayMenu.trayMenu.OnOpen == nil { +// return +// } +// go trayMenu.trayMenu.OnOpen() +//} +// +//func (m *Manager) OnTrayMenuClose(id string) { +// trayMenu, ok := m.trayMenus[id] +// if !ok { +// return +// } +// if trayMenu.trayMenu.OnClose == nil { +// return +// } +// go trayMenu.trayMenu.OnClose() +//} +// +//func (m *Manager) AddTrayMenu(trayMenu *menu.TrayMenu) (string, error) { +// newTrayMenu := NewTrayMenu(trayMenu) +// +// // Hook up a new ID +// trayID := generateTrayID() +// newTrayMenu.ID = trayID +// +// // Save the references +// m.trayMenus[trayID] = newTrayMenu +// m.trayMenuPointers[trayMenu] = trayID +// +// return newTrayMenu.AsJSON() +//} +// +//func (m *Manager) GetTrayID(trayMenu *menu.TrayMenu) (string, error) { +// trayID, exists := m.trayMenuPointers[trayMenu] +// if !exists { +// return "", fmt.Errorf("Unable to find menu ID for tray menu!") +// } +// return trayID, nil +//} +// +//// SetTrayMenu updates or creates a menu +//func (m *Manager) SetTrayMenu(trayMenu *menu.TrayMenu) (string, error) { +// trayID, trayMenuKnown := m.trayMenuPointers[trayMenu] +// if !trayMenuKnown { +// return m.AddTrayMenu(trayMenu) +// } +// +// // Create the updated tray menu +// updatedTrayMenu := NewTrayMenu(trayMenu) +// updatedTrayMenu.ID = trayID +// +// // Save the reference +// m.trayMenus[trayID] = updatedTrayMenu +// +// return updatedTrayMenu.AsJSON() +//} +// +//func (m *Manager) GetTrayMenus() ([]string, error) { +// result := []string{} +// for _, trayMenu := range m.trayMenus { +// JSON, err := trayMenu.AsJSON() +// if err != nil { +// return nil, err +// } +// result = append(result, JSON) +// } +// +// return result, nil +//} +// +//func (m *Manager) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) (string, error) { +// trayID, trayMenuKnown := m.trayMenuPointers[trayMenu] +// if !trayMenuKnown { +// return "", fmt.Errorf("[UpdateTrayMenuLabel] unknown tray id for tray %s", trayMenu.Label) +// } +// +// type LabelUpdate struct { +// ID string +// Label string `json:",omitempty"` +// FontName string `json:",omitempty"` +// FontSize int +// RGBA string `json:",omitempty"` +// Disabled bool +// Tooltip string `json:",omitempty"` +// Image []byte `json:",omitempty"` +// MacTemplateImage bool +// StyledLabel []*ansi.StyledText `json:",omitempty"` +// } +// +// // Parse ANSI text +// var styledLabel []*ansi.StyledText +// tempLabel := trayMenu.Label +// if strings.Contains(tempLabel, "\033[") { +// parsedLabel, err := ansi.Parse(tempLabel) +// if err == nil { +// styledLabel = parsedLabel +// } +// } +// +// update := &LabelUpdate{ +// ID: trayID, +// Label: trayMenu.Label, +// FontName: trayMenu.FontName, +// FontSize: trayMenu.FontSize, +// Disabled: trayMenu.Disabled, +// Tooltip: trayMenu.Tooltip, +// Image: trayMenu.Image, +// MacTemplateImage: trayMenu.MacTemplateImage, +// RGBA: trayMenu.RGBA, +// StyledLabel: styledLabel, +// } +// +// data, err := json.Marshal(update) +// if err != nil { +// return "", errors.Wrap(err, "[UpdateTrayMenuLabel] ") +// } +// +// return string(data), nil +// +//} +// +//func (m *Manager) GetContextMenus() ([]string, error) { +// result := []string{} +// for _, contextMenu := range m.contextMenus { +// JSON, err := contextMenu.AsJSON() +// if err != nil { +// return nil, err +// } +// result = append(result, JSON) +// } +// +// return result, nil +//} diff --git a/v2/pkg/events/events.go b/v2/pkg/events/events.go new file mode 100644 index 000000000..40727ba1a --- /dev/null +++ b/v2/pkg/events/events.go @@ -0,0 +1,3 @@ +package events + +const ThemeChanged = ":wails:themechanged" diff --git a/v2/pkg/menu/menu.go b/v2/pkg/menu/menu.go index 0c3ddb618..c7ad7caf9 100644 --- a/v2/pkg/menu/menu.go +++ b/v2/pkg/menu/menu.go @@ -44,16 +44,23 @@ func (m *Menu) AddRadio(label string, checked bool, accelerator *keys.Accelerato } // AddSeparator adds a separator to the menu -func (m *Menu) AddSeparator() { +func (m *Menu) AddSeparator() *MenuItem { item := Separator() m.Append(item) + return item } -func (m *Menu) AddSubmenu(label string) *Menu { +func (m *Menu) AddSubmenu(label string) *MenuItem { submenu := NewMenu() item := SubMenu(label, submenu) 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) { diff --git a/v2/pkg/menu/menuitem.go b/v2/pkg/menu/menuitem.go index ba9574eb3..f5b34dff2 100644 --- a/v2/pkg/menu/menuitem.go +++ b/v2/pkg/menu/menuitem.go @@ -6,6 +6,11 @@ import ( "github.com/wailsapp/wails/v2/pkg/menu/keys" ) +type MenuItemImpl interface { + SetChecked(bool) + SetLabel(string) +} + // MenuItem represents a menuitem contained in a menu type MenuItem struct { // Label is what appears as the menu text @@ -53,6 +58,9 @@ type MenuItem struct { // Used for locking when removing elements removeLock sync.Mutex + + // Implementation of the runtime methods + Impl MenuItemImpl } // Parent returns the parent of the menu item. @@ -216,6 +224,21 @@ func (m *MenuItem) insertItemAtIndex(index int, target *MenuItem) bool { 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 func Text(label string, accelerator *keys.Accelerator, click Callback) *MenuItem { return &MenuItem{ diff --git a/v2/pkg/menu/tray.go b/v2/pkg/menu/tray.go index 7554795ad..1c8ec07b4 100644 --- a/v2/pkg/menu/tray.go +++ b/v2/pkg/menu/tray.go @@ -1,20 +1,95 @@ 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 type TrayMenu struct { + ctx context.Context // Label is the text we wish to display in the tray Label string - // Image is the name of the tray icon we wish to display. - // These are read up during build from /trayicons and - // the filenames are used as IDs, minus the extension - // EG: /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 + Image *TrayImage // Text Colour RGBA string @@ -27,7 +102,7 @@ type TrayMenu struct { Tooltip string // Callback function when menu clicked - //Click Callback `json:"-"` + Click Callback // Disabled makes the item unselectable Disabled bool @@ -40,4 +115,67 @@ type TrayMenu struct { // OnClose is called when the Menu is closed 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) + } } diff --git a/v2/pkg/options/mac/mac.go b/v2/pkg/options/mac/mac.go index 033cfd1a2..2f574607f 100644 --- a/v2/pkg/options/mac/mac.go +++ b/v2/pkg/options/mac/mac.go @@ -1,12 +1,12 @@ package mac -//type ActivationPolicy int -// -//const ( -// NSApplicationActivationPolicyRegular ActivationPolicy = 0 -// NSApplicationActivationPolicyAccessory ActivationPolicy = 1 -// NSApplicationActivationPolicyProhibited ActivationPolicy = 2 -//) +type ActivationPolicy int + +const ( + NSApplicationActivationPolicyRegular ActivationPolicy = 0 + NSApplicationActivationPolicyAccessory ActivationPolicy = 1 + NSApplicationActivationPolicyProhibited ActivationPolicy = 2 +) type AboutInfo struct { Title string @@ -20,7 +20,7 @@ type Options struct { Appearance AppearanceType WebviewIsTransparent bool WindowIsTranslucent bool - //ActivationPolicy ActivationPolicy - About *AboutInfo + About *AboutInfo + ActivationPolicy ActivationPolicy //URLHandlers map[string]func(string) } diff --git a/website/docs/introduction.mdx b/website/docs/introduction.mdx index 846b21e1d..dbbd0884c 100644 --- a/website/docs/introduction.mdx +++ b/website/docs/introduction.mdx @@ -6,19 +6,19 @@ sidebar_position: 1 ## 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 -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 -written using Wails. It has menus, supports light and dark desktop themes, and the main window uses translucency that -gives it that 'frosty' effect of a native app. +Wails also offers out-of-the-box integration with the native system libraries, providing native menus, dialogs, +windowing functions, as well as dark and light themes. It also provides a simple way to use native window translucency, +allowing you to create modern, beautiful applications.

- - - + + +

## Quick Start Templates diff --git a/website/static/img/varly1.png b/website/static/img/varly1.png new file mode 100644 index 000000000..0dac80579 Binary files /dev/null and b/website/static/img/varly1.png differ