Merge branch 'exp' into feature/1521_Support_Tray_Menus

# Conflicts:
#	.gitignore
#	v2/internal/frontend/desktop/windows/win32/consts.go
#	v2/internal/frontend/devserver/devserver.go
#	website/docs/introduction.mdx
This commit is contained in:
Lea Anthony 2022-09-23 19:53:07 +10:00
commit e5a1ddb3f3
No known key found for this signature in database
GPG key ID: 33DAF7BB90A58405
26 changed files with 1217 additions and 280 deletions

2
.gitignore vendored
View file

@ -31,3 +31,5 @@ v2/test/kitchensink/frontend/package.json.md5
v2/cmd/wails/internal/commands/initialise/templates/testtemplates/
.env
/website/static/img/.cache.json
v2/internal/frontend/desktop/darwin/test.xcodeproj

View file

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

View file

@ -9,6 +9,7 @@
#import <Cocoa/Cocoa.h>
#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];

View file

@ -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);
@ -62,6 +62,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);
@ -70,4 +80,7 @@ void UpdateMenuItem(void* nsmenuitem, int checked);
NSString* safeInit(const char* input);
int ScalingFactor(void *ctx);
#endif /* Application_h */

View file

@ -12,6 +12,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) {
@ -20,7 +21,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;
}
@ -28,7 +29,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];
@ -170,6 +171,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];
@ -304,6 +309,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) {
@ -328,8 +382,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];
)
}
@ -354,7 +408,7 @@ void UpdateMenuItem(void* nsmenuitem, int checked) {
ON_MAIN_THREAD(
WailsMenuItem *menuItem = (__bridge WailsMenuItem*) nsmenuitem;
[menuItem setState:(checked == 1?NSControlStateValueOn:NSControlStateValueOff)];
)
)
}
@ -363,12 +417,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;

View file

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

View file

@ -3,15 +3,6 @@
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 (
"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 <Foundation/Foundation.h>
#import "Application.h"
#import "WailsContext.h"
#include <stdlib.h>
*/
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)
@ -93,10 +110,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)
@ -115,6 +139,11 @@ func (f *Frontend) startCallbackProcessor() {
}
}
}
func (f *Frontend) startNotificationsProcessor() {
for notification := range notificationBuffer {
f.handleNotification(notification)
}
}
func (f *Frontend) WindowReload() {
f.ExecJS("runtime.WindowReload();")
@ -154,7 +183,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
}
@ -438,6 +471,11 @@ func processMessage(message *C.char) {
messageBuffer <- goMessage
}
//export processNotification
func processNotification(notification NotificationType) {
notificationBuffer <- notification
}
//export processURLRequest
func processURLRequest(ctx unsafe.Pointer, requestId C.ulonglong, url *C.char, method *C.char, headers *C.char, body unsafe.Pointer, bodyLen C.int) {
var goBody []byte
@ -459,3 +497,19 @@ func processURLRequest(ctx unsafe.Pointer, requestId C.ulonglong, url *C.char, m
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)
}
}

View file

@ -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
}

View file

@ -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
}

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)
}
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))
}

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

@ -23,6 +23,15 @@ var (
procGetWindowRect = moduser32.NewProc("GetWindowRect")
procGetMonitorInfo = moduser32.NewProc("GetMonitorInfoW")
procMonitorFromWindow = moduser32.NewProc("MonitorFromWindow")
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")
@ -41,3 +50,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
)

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

@ -56,6 +56,10 @@ func (d *DevWebServer) Show() {
d.desktopFrontend.Show()
}
func (d *DevWebServer) TrayMenuAdd(trayMenu *menu.TrayMenu) menu.TrayMenuImpl {
return d.desktopFrontend.TrayMenuAdd(trayMenu)
}
func (d *DevWebServer) WindowSetSystemDefaultTheme() {
d.desktopFrontend.WindowSetSystemDefaultTheme()
}

View file

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

View file

@ -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()
//}

View file

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

View file

@ -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
//}

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
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) {

View file

@ -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{

View file

@ -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 <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
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)
}
}

View file

@ -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)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB