5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-02 23:02:19 +08:00

Preliminary Tray support

This commit is contained in:
Lea Anthony 2020-12-06 21:05:21 +11:00
parent 65bea04080
commit 11bf564b73
No known key found for this signature in database
GPG Key ID: 33DAF7BB90A58405
7 changed files with 499 additions and 83 deletions

View File

@ -33,6 +33,7 @@ type App struct {
binding *subsystem.Binding
call *subsystem.Call
menu *subsystem.Menu
tray *subsystem.Tray
dispatcher *messagedispatcher.Dispatcher
// Indicates if the app is in debug mode
@ -47,7 +48,7 @@ type App struct {
}
// Create App
func CreateApp(options *options.App) *App {
func CreateApp(options *options.App) (*App, error) {
// Merge default options
options.MergeDefaults()
@ -68,15 +69,17 @@ func CreateApp(options *options.App) *App {
result.options = options
// Initialise the app
result.Init()
err := result.Init()
return result
return result, err
}
// Run the application
func (a *App) Run() error {
var err error
// Setup signal handler
signalsubsystem, err := signal.NewManager(a.servicebus, a.logger)
if err != nil {
@ -87,7 +90,10 @@ func (a *App) Run() error {
// Start the service bus
a.servicebus.Debug()
a.servicebus.Start()
err = a.servicebus.Start()
if err == nil {
return err
}
// Start the runtime
runtimesubsystem, err := subsystem.NewRuntime(a.servicebus, a.logger,
@ -96,7 +102,10 @@ func (a *App) Run() error {
return err
}
a.runtime = runtimesubsystem
a.runtime.Start()
err = a.runtime.Start()
if err == nil {
return err
}
// Application Stores
a.loglevelStore = a.runtime.GoRuntime().Store.New("wails:loglevel", a.options.LogLevel)
@ -109,7 +118,10 @@ func (a *App) Run() error {
return err
}
a.binding = bindingsubsystem
a.binding.Start()
err = a.binding.Start()
if err == nil {
return err
}
// Start the logging subsystem
log, err := subsystem.NewLog(a.servicebus, a.logger, a.loglevelStore)
@ -117,7 +129,10 @@ func (a *App) Run() error {
return err
}
a.log = log
a.log.Start()
err = a.log.Start()
if err == nil {
return err
}
// create the dispatcher
dispatcher, err := messagedispatcher.New(a.servicebus, a.logger)
@ -125,7 +140,10 @@ func (a *App) Run() error {
return err
}
a.dispatcher = dispatcher
dispatcher.Start()
err = dispatcher.Start()
if err == nil {
return err
}
// Start the eventing subsystem
event, err := subsystem.NewEvent(a.servicebus, a.logger)
@ -133,26 +151,53 @@ func (a *App) Run() error {
return err
}
a.event = event
a.event.Start()
err = a.event.Start()
if err == nil {
return err
}
// Start the menu subsystem
var platformMenu *menu.Menu
var applicationMenu *menu.Menu
var trayMenu *menu.Menu
switch goruntime.GOOS {
case "darwin":
platformMenu = a.options.Mac.Menu
applicationMenu = a.options.Mac.Menu
trayMenu = a.options.Mac.Tray
// case "linux":
// platformMenu = a.options.Linux.Menu
// applicationMenu = a.options.Linux.Menu
// case "windows":
// platformMenu = a.options.Windows.Menu
// applicationMenu = a.options.Windows.Menu
default:
return fmt.Errorf("unsupported OS: %s", goruntime.GOOS)
}
menusubsystem, err := subsystem.NewMenu(platformMenu, a.servicebus, a.logger)
if err != nil {
return err
// Optionally start the menu subsystem
if applicationMenu != nil {
menusubsystem, err := subsystem.NewMenu(applicationMenu, a.servicebus,
a.logger)
if err != nil {
return err
}
a.menu = menusubsystem
err = a.menu.Start()
if err == nil {
return err
}
}
// Optionally start the tray subsystem
if trayMenu != nil {
traysubsystem, err := subsystem.NewTray(trayMenu, a.servicebus,
a.logger)
if err != nil {
return err
}
a.tray = traysubsystem
err = a.tray.Start()
if err == nil {
return err
}
}
a.menu = menusubsystem
a.menu.Start()
// Start the call subsystem
call, err := subsystem.NewCall(a.servicebus, a.logger, a.bindings.DB(), a.runtime.GoRuntime())
@ -160,7 +205,10 @@ func (a *App) Run() error {
return err
}
a.call = call
a.call.Start()
err = a.call.Start()
if err == nil {
return err
}
// Dump bindings as a debug
bindingDump, err := a.bindings.ToJSON()
@ -170,7 +218,10 @@ func (a *App) Run() error {
result := a.window.Run(dispatcher, bindingDump, a.debug)
a.logger.Trace("Ffenestri.Run() exited")
a.servicebus.Stop()
err = a.servicebus.Stop()
if err == nil {
return err
}
return result
}

View File

@ -74,12 +74,18 @@ extern const char *icon[];
// MAIN DEBUG FLAG
int debug;
// MenuItem map
// MenuItem map for the application menu
struct hashmap_s menuItemMapForApplicationMenu;
// RadioGroup map. Maps a menuitem id with its associated radio group items
// RadioGroup map for the application menu. Maps a menuitem id with its associated radio group items
struct hashmap_s radioGroupMapForApplicationMenu;
// MenuItem map for the tray menu
struct hashmap_s menuItemMapForTrayMenu;
// RadioGroup map for the tray menu. Maps a menuitem id with its associated radio group items
struct hashmap_s radioGroupMapForTrayMenu;
// Dispatch Method
typedef void (^dispatchMethod)(void);
@ -341,7 +347,7 @@ void messageHandler(id self, SEL cmd, id contentController, id message) {
}
// Callback for menu items
void menuItemPressed(id self, SEL cmd, id sender) {
void menuItemPressedForApplicationMenu(id self, SEL cmd, id sender) {
const char *menuID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue"));
// Notify the backend
const char *message = concat("MC", menuID);
@ -349,10 +355,17 @@ void menuItemPressed(id self, SEL cmd, id sender) {
free((void*)message);
}
// Callback for tray items
void menuItemPressedForTrayMenu(id self, SEL cmd, id sender) {
const char *menuID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue"));
// Notify the backend
const char *message = concat("TC", menuID);
messageFromWindowCallback(message);
free((void*)message);
}
// Callback for menu items
void checkboxMenuItemPressedForApplicationMenu(id self, SEL cmd, id sender, struct
hashmap_s
*menuItemMap) {
void checkboxMenuItemPressedForApplicationMenu(id self, SEL cmd, id sender, struct hashmap_s *menuItemMap) {
const char *menuID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue"));
// Get the menu item from the menu item map
@ -370,7 +383,26 @@ hashmap_s
free((void*)message);
}
// radioMenuItemPressed
// Callback for tray menu items
void checkboxMenuItemPressedForTrayMenu(id self, SEL cmd, id sender, struct hashmap_s *menuItemMap) {
const char *menuID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue"));
// Get the menu item from the menu item map
id menuItem = (id)hashmap_get(&menuItemMapForTrayMenu, (char*)menuID, strlen(menuID));
// Get the current state
bool state = msg(menuItem, s("state"));
// Toggle the state
msg(menuItem, s("setState:"), (state? NSControlStateValueOff : NSControlStateValueOn));
// Notify the backend
const char *message = concat("TC", menuID);
messageFromWindowCallback(message);
free((void*)message);
}
// radioMenuItemPressedForApplicationMenu
void radioMenuItemPressedForApplicationMenu(id self, SEL cmd, id sender) {
const char *menuID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue"));
@ -406,6 +438,43 @@ void radioMenuItemPressedForApplicationMenu(id self, SEL cmd, id sender) {
free((void*)message);
}
// radioMenuItemPressedForTrayMenu
void radioMenuItemPressedForTrayMenu(id self, SEL cmd, id sender) {
const char *menuID = (const char *)msg(msg(sender, s("representedObject")), s("pointerValue"));
// Get the menu item from the menu item map
id menuItem = (id)hashmap_get(&menuItemMapForTrayMenu, (char*)menuID, strlen(menuID));
// Check the menu items' current state
bool selected = msg(menuItem, s("state"));
// If it's already selected, exit early
if (selected) {
return;
}
// Get this item's radio group members and turn them off
id *members = (id*)hashmap_get(&radioGroupMapForTrayMenu, (char*)menuID, strlen(menuID));
// Uncheck all members of the group
id thisMember = members[0];
int count = 0;
while(thisMember != NULL) {
msg(thisMember, s("setState:"), NSControlStateValueOff);
count = count + 1;
thisMember = members[count];
}
// check the selected menu item
msg(menuItem, s("setState:"), NSControlStateValueOn);
// Notify the backend
const char *message = concat("TC", menuID);
messageFromWindowCallback(message);
free((void*)message);
}
// closeWindow is called when the close button is pressed
void closeWindow(id self, SEL cmd, id sender) {
struct Application *app = (struct Application *) objc_getAssociatedObject(self, "application");
@ -459,6 +528,22 @@ void allocateMenuHashMaps(struct Application *app) {
}
}
void allocateTrayHashMaps(struct Application *app) {
// Allocate new menuItem map
if( 0 != hashmap_create((const unsigned)16, &menuItemMapForTrayMenu)) {
// Couldn't allocate map
Fatal(app, "Not enough memory to allocate menuItemMapForTrayMenu!");
return;
}
// Allocate the Radio Group Cache
if( 0 != hashmap_create((const unsigned)4, &radioGroupMapForTrayMenu)) {
// Couldn't allocate map
Fatal(app, "Not enough memory to allocate radioGroupMapForTrayMenu!");
return;
}
}
void* NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden, int logLevel) {
// Setup main application struct
struct Application *result = malloc(sizeof(struct Application));
@ -548,21 +633,45 @@ void destroyMenu(struct Application *app) {
json_delete(app->processedMenu);
app->processedMenu = NULL;
}
// Release the tray menu json if we have it
if ( app->trayMenuAsJSON != NULL ) {
free((void*)app->trayMenuAsJSON);
app->trayMenuAsJSON = NULL;
}
// Release processed menu
if( app->processedTrayMenu != NULL) {
json_delete(app->processedTrayMenu);
app->processedTrayMenu = NULL;
}
}
void destroyTray(struct Application *app) {
// If we don't have a tray, exit!
if( app->trayMenuAsJSON == NULL ) {
return;
}
// Free menu item hashmap
hashmap_destroy(&menuItemMapForTrayMenu);
// Free radio group members
if( hashmap_num_entries(&radioGroupMapForTrayMenu) > 0 ) {
if (0!=hashmap_iterate_pairs(&radioGroupMapForTrayMenu, freeHashmapItem, NULL)) {
Fatal(app, "failed to deallocate hashmap entries!");
}
}
//Free radio groups hashmap
hashmap_destroy(&radioGroupMapForTrayMenu);
// Release the menu json if we have it
if ( app->trayMenuAsJSON != NULL ) {
free((void*)app->trayMenuAsJSON);
app->trayMenuAsJSON = NULL;
}
// Release processed tray
if( app->processedTrayMenu != NULL) {
json_delete(app->processedTrayMenu);
app->processedTrayMenu = NULL;
}
}
void DestroyApplication(struct Application *app) {
Debug(app, "Destroying Application");
@ -582,8 +691,12 @@ void DestroyApplication(struct Application *app) {
msg( c("NSEvent"), s("removeMonitor:"), app->mouseUpMonitor);
}
// Destroy the menu
destroyMenu(app);
// Destroy the tray
destroyTray(app);
// Remove script handlers
msg(app->manager, s("removeScriptMessageHandlerForName:"), str("windowDrag"));
msg(app->manager, s("removeScriptMessageHandlerForName:"), str("external"));
@ -1028,11 +1141,12 @@ void createDelegate(struct Application *app) {
class_addMethod(delegateClass, s("applicationWillTerminate:"), (IMP) closeWindow, "v@:@");
// Menu Callbacks
class_addMethod(delegateClass, s("menuCallback:"), (IMP)menuItemPressed, "v@:@");
class_addMethod(delegateClass, s("checkboxMenuCallbackForApplicationMenu:"), (IMP)checkboxMenuItemPressedForApplicationMenu, "v@:@");
class_addMethod(delegateClass, s("radioMenuCallbackForApplicationMenu:"),
(IMP)
radioMenuItemPressedForApplicationMenu, "v@:@");
class_addMethod(delegateClass, s("menuCallbackForApplicationMenu:"), (IMP)menuItemPressedForApplicationMenu, "v@:@");
class_addMethod(delegateClass, s("checkboxMenuCallbackForApplicationMenu:"), (IMP) checkboxMenuItemPressedForApplicationMenu, "v@:@");
class_addMethod(delegateClass, s("radioMenuCallbackForApplicationMenu:"), (IMP) radioMenuItemPressedForApplicationMenu, "v@:@");
class_addMethod(delegateClass, s("menuCallbackForTrayMenu:"), (IMP)menuItemPressedForTrayMenu, "v@:@");
class_addMethod(delegateClass, s("checkboxMenuCallbackForTrayMenu:"), (IMP) checkboxMenuItemPressedForTrayMenu, "v@:@");
class_addMethod(delegateClass, s("radioMenuCallbackForTrayMenu:"), (IMP) radioMenuItemPressedForTrayMenu, "v@:@");
// Script handler
@ -1476,14 +1590,14 @@ id processAcceleratorKey(const char *key) {
}
id parseTextMenuItem(struct Application *app, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers) {
id parseTextMenuItem(struct Application *app, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, const char *menuCallback) {
id item = ALLOC("NSMenuItem");
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), menuid);
msg(item, s("setRepresentedObject:"), wrappedId);
id key = processAcceleratorKey(acceleratorkey);
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title),
s("menuCallback:"), key);
s(menuCallback), key);
msg(item, s("setEnabled:"), !disabled);
msg(item, s("autorelease"));
@ -1542,7 +1656,7 @@ id parseRadioMenuItem(struct Application *app, id parentmenu, const char *title,
void parseMenuItem(struct Application *app, id parentMenu, JsonNode *item,
struct hashmap_s *menuItemMap, const char *checkboxCallbackFunction, const char
*radioCallbackFunction) {
*radioCallbackFunction, const char *menuCallbackFunction) {
// Check if this item is hidden and if so, exit early!
bool hidden = false;
@ -1578,8 +1692,7 @@ struct hashmap_s *menuItemMap, const char *checkboxCallbackFunction, const char
JsonNode *item;
json_foreach(item, submenu) {
// Get item label
parseMenuItem(app, thisMenu, item, menuItemMap, checkboxCallbackFunction, radioCallbackFunction
);
parseMenuItem(app, thisMenu, item, menuItemMap, checkboxCallbackFunction, radioCallbackFunction, menuCallbackFunction);
}
return;
@ -1634,7 +1747,7 @@ struct hashmap_s *menuItemMap, const char *checkboxCallbackFunction, const char
if( type != NULL ) {
if( STREQ(type->string_, "Text")) {
parseTextMenuItem(app, parentMenu, label, menuid, disabled, acceleratorkey, modifiers);
parseTextMenuItem(app, parentMenu, label, menuid, disabled, acceleratorkey, modifiers, menuCallbackFunction);
}
else if ( STREQ(type->string_, "Separator")) {
addSeparator(parentMenu);
@ -1644,16 +1757,14 @@ struct hashmap_s *menuItemMap, const char *checkboxCallbackFunction, const char
bool checked = false;
getJSONBool(item, "Checked", &checked);
parseCheckboxMenuItem(app, parentMenu, label, menuid, disabled, checked,
"", menuItemMap, checkboxCallbackFunction);
parseCheckboxMenuItem(app, parentMenu, label, menuid, disabled, checked, "", menuItemMap, checkboxCallbackFunction);
}
else if ( STREQ(type->string_, "Radio")) {
// Get checked state
bool checked = false;
getJSONBool(item, "Checked", &checked);
parseRadioMenuItem(app, parentMenu, label, menuid, disabled, checked, "",
menuItemMap, radioCallbackFunction);
parseRadioMenuItem(app, parentMenu, label, menuid, disabled, checked, "", menuItemMap, radioCallbackFunction);
}
if ( modifiers != NULL ) {
@ -1664,8 +1775,7 @@ struct hashmap_s *menuItemMap, const char *checkboxCallbackFunction, const char
}
}
void parseMenu(struct Application *app, id parentMenu, JsonNode *menu, struct
hashmap_s *menuItemMap, const char *checkboxCallbackFunction, const char *radioCallbackFunction) {
void parseMenu(struct Application *app, id parentMenu, JsonNode *menu, struct hashmap_s *menuItemMap, const char *checkboxCallbackFunction, const char *radioCallbackFunction, const char *menuCallbackFunction) {
JsonNode *items = json_find_member(menu, "Items");
if( items == NULL ) {
// Parse error!
@ -1677,8 +1787,7 @@ hashmap_s *menuItemMap, const char *checkboxCallbackFunction, const char *radioC
JsonNode *item;
json_foreach(item, items) {
// Get item label
parseMenuItem(app, parentMenu, item, menuItemMap, checkboxCallbackFunction, radioCallbackFunction
);
parseMenuItem(app, parentMenu, item, menuItemMap, checkboxCallbackFunction, radioCallbackFunction, menuCallbackFunction);
}
}
@ -1764,7 +1873,7 @@ void parseMenuData(struct Application *app) {
parseMenu(app, menubar, menuData, &menuItemMapForApplicationMenu,
"checkboxMenuCallbackForApplicationMenu:", "radioMenuCallbackForApplicationMenu:");
"checkboxMenuCallbackForApplicationMenu:", "radioMenuCallbackForApplicationMenu:", "menuCallbackForApplicationMenu:");
// Create the radiogroup cache
JsonNode *radioGroups = json_find_member(app->processedMenu, "RadioGroups");
@ -1807,17 +1916,64 @@ void UpdateMenu(struct Application *app, const char *menuAsJSON) {
}
void parseTrayData(struct Application *app) {
id statusBar = msg( c("NSStatusBar"), s("systemStatusBar") );
id statusItem = msg(statusBar, s("statusItemWithLength:"), -1.0);
msg(statusItem, s("retain"));
id statusBarButton = msg(statusItem, s("button"));
// msg(statusBarButton, s("setImage:"),
// msg(c("NSImage"), s("imageNamed:"),
// msg(c("NSString"), s("stringWithUTF8String:"), tray->icon)));
msg(statusItem, s("setMenu:"), NULL);
// Allocate the hashmaps we need
allocateTrayHashMaps(app);
// Create a new menu
id traymenu = createMenu(str(""));
// Create a new menu bar
id statusBar = msg( c("NSStatusBar"), s("systemStatusBar") );
id statusItem = msg(statusBar, s("statusItemWithLength:"), -1.0);
msg(statusItem, s("retain"));
id statusBarButton = msg(statusItem, s("button"));
Debug(app, ">>>>>>>>>>> TRAY MENU: %s", app->trayMenuAsJSON);
// Parse the processed menu json
app->processedTrayMenu = json_decode(app->trayMenuAsJSON);
if( app->processedTrayMenu == NULL ) {
// Parse error!
Fatal(app, "Unable to parse Tray JSON: %s", app->trayMenuAsJSON);
return;
}
// Pull out the Menu
JsonNode *trayMenuData = json_find_member(app->processedTrayMenu, "Menu");
if( trayMenuData == NULL ) {
// Parse error!
Fatal(app, "Unable to find Menu data: %s", app->processedTrayMenu);
return;
}
parseMenu(app, traymenu, trayMenuData, &menuItemMapForTrayMenu,
"checkboxMenuCallbackForTrayMenu:", "radioMenuCallbackForTrayMenu:", "menuCallbackForTrayMenu:");
// Create the radiogroup cache
JsonNode *radioGroups = json_find_member(app->processedTrayMenu, "RadioGroups");
if( radioGroups == NULL ) {
// Parse error!
Fatal(app, "Unable to find RadioGroups data: %s", app->processedTrayMenu);
return;
}
// Iterate radio groups
JsonNode *radioGroup;
json_foreach(radioGroup, radioGroups) {
// Get item label
processRadioGroup(radioGroup, &menuItemMapForTrayMenu, &radioGroupMapForTrayMenu);
}
msg(statusItem, s("setMenu:"), traymenu);
}
@ -1987,9 +2143,13 @@ void Run(struct Application *app, int argc, char **argv) {
}
// If we have a tray menu, process it
printf
("\n\n\n*****************************************************************************************************************************************************************************************************************\n\n\n");
if( app->trayMenuAsJSON != NULL ) {
parseTrayData(app);
}
printf
("\n\n\n*****************************************************************************************************************************************************************************************************************\n\n\n");
// Finally call run
Debug(app, "Run called");

View File

@ -15,6 +15,7 @@ extern void SetAppearance(void *, const char *);
extern void WebviewIsTransparent(void *);
extern void SetWindowBackgroundIsTranslucent(void *);
extern void SetMenu(void *, const char *);
extern void SetTray(void *, const char *);
*/
import "C"
import (
@ -79,27 +80,33 @@ func (a *Application) processPlatformSettings() error {
We keep a record of every radio group member we discover by saving
a list of all members of the group and the number of members
in the group (this last one is for optimisation at the C layer).
Example:
{
"RadioGroups": [
{
"Members": [
"option-1",
"option-2",
"option-3"
],
"Length": 3
}
]
}
*/
processedMenu := NewProcessedMenu(mac.Menu)
menuJSON, err := json.Marshal(processedMenu)
applicationMenuJSON, err := json.Marshal(processedMenu)
if err != nil {
return err
}
C.SetMenu(a.app, a.string2CString(string(menuJSON)))
C.SetMenu(a.app, a.string2CString(string(applicationMenuJSON)))
}
// Process tray
if mac.Tray != nil {
/*
As radio groups need to be manually managed on OSX,
we preprocess the menu to determine the radio groups.
This is defined as any adjacent menu item of type "RadioType".
We keep a record of every radio group member we discover by saving
a list of all members of the group and the number of members
in the group (this last one is for optimisation at the C layer).
*/
processedMenu := NewProcessedMenu(mac.Tray)
trayMenuJSON, err := json.Marshal(processedMenu)
if err != nil {
return err
}
C.SetTray(a.app, a.string2CString(string(trayMenuJSON)))
println("******************** SET TRAY!!!!! &&&&&&&&&&&&&&&&&&&&&&&&&&")
}
return nil

View File

@ -0,0 +1,186 @@
package subsystem
import (
"strings"
"sync"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
"github.com/wailsapp/wails/v2/internal/servicebus"
"github.com/wailsapp/wails/v2/pkg/menu"
)
// Tray is the subsystem that handles the operation of the tray menu.
// It manages all service bus messages starting with "tray".
type Tray struct {
quitChannel <-chan *servicebus.Message
trayChannel <-chan *servicebus.Message
running bool
// Event listeners
listeners map[string][]func(*menu.MenuItem)
menuItems map[string]*menu.MenuItem
notifyLock sync.RWMutex
// logger
logger logger.CustomLogger
// The tray menu
trayMenu *menu.Menu
// Service Bus
bus *servicebus.ServiceBus
}
// NewTray creates a new menu subsystem
func NewTray(trayMenu *menu.Menu, bus *servicebus.ServiceBus,
logger *logger.Logger) (*Tray, error) {
// Register quit channel
quitChannel, err := bus.Subscribe("quit")
if err != nil {
return nil, err
}
// Subscribe to menu messages
trayChannel, err := bus.Subscribe("tray:")
if err != nil {
return nil, err
}
result := &Tray{
quitChannel: quitChannel,
trayChannel: trayChannel,
logger: logger.CustomLogger("Tray Subsystem"),
listeners: make(map[string][]func(*menu.MenuItem)),
menuItems: make(map[string]*menu.MenuItem),
trayMenu: trayMenu,
bus: bus,
}
// Build up list of item/id pairs
result.processMenu(trayMenu)
return result, nil
}
// Start the subsystem
func (t *Tray) Start() error {
t.logger.Trace("Starting")
t.running = true
// Spin off a go routine
go func() {
for t.running {
select {
case <-t.quitChannel:
t.running = false
break
case menuMessage := <-t.trayChannel:
splitTopic := strings.Split(menuMessage.Topic(), ":")
menuMessageType := splitTopic[1]
switch menuMessageType {
case "clicked":
if len(splitTopic) != 2 {
t.logger.Error("Received clicked message with invalid topic format. Expected 2 sections in topic, got %s", splitTopic)
continue
}
t.logger.Trace("Got Menu clicked Message: %s %+v", menuMessage.Topic(), menuMessage.Data())
menuid := menuMessage.Data().(string)
// Get the menu item
menuItem := t.menuItems[menuid]
if menuItem == nil {
t.logger.Trace("Cannot process menuid %s - unknown", menuid)
return
}
// Is the menu item a checkbox?
if menuItem.Type == menu.CheckboxType {
// Toggle state
menuItem.Checked = !menuItem.Checked
}
// Notify listeners
t.notifyListeners(menuid, menuItem)
case "on":
listenerDetails := menuMessage.Data().(*message.MenuOnMessage)
id := listenerDetails.MenuID
t.listeners[id] = append(t.listeners[id], listenerDetails.Callback)
// Make sure we catch any menu updates
case "update":
updatedMenu := menuMessage.Data().(*menu.Menu)
t.processMenu(updatedMenu)
// Notify frontend of menu change
t.bus.Publish("trayfrontend:update", updatedMenu)
default:
t.logger.Error("unknown tray message: %+v", menuMessage)
}
}
}
// Call shutdown
t.shutdown()
}()
return nil
}
func (t *Tray) processMenu(trayMenu *menu.Menu) {
// Initialise the variables
t.menuItems = make(map[string]*menu.MenuItem)
t.trayMenu = trayMenu
for _, item := range trayMenu.Items {
t.processMenuItem(item)
}
}
func (t *Tray) processMenuItem(item *menu.MenuItem) {
if item.SubMenu != nil {
for _, submenuitem := range item.SubMenu {
t.processMenuItem(submenuitem)
}
return
}
if item.ID != "" {
if t.menuItems[item.ID] != nil {
t.logger.Error("Menu id '%s' is used by multiple menu items: %s %s", t.menuItems[item.ID].Label, item.Label)
return
}
t.menuItems[item.ID] = item
}
}
// Notifies listeners that the given menu was clicked
func (t *Tray) notifyListeners(menuid string, menuItem *menu.MenuItem) {
// Get list of menu listeners
listeners := t.listeners[menuid]
if listeners == nil {
t.logger.Trace("No listeners for MenuItem with ID '%s'", menuid)
return
}
// Lock the listeners
t.notifyLock.Lock()
// Callback in goroutine
for _, listener := range listeners {
go listener(menuItem)
}
// Unlock
t.notifyLock.Unlock()
}
func (t *Tray) shutdown() {
t.logger.Trace("Shutdown")
}

View File

@ -9,4 +9,5 @@ type Options struct {
WebviewIsTransparent bool
WindowBackgroundIsTranslucent bool
Menu *menu.Menu
Tray *menu.Menu
}

View File

@ -1,10 +1,11 @@
package main
import (
wails "github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/mac"
"log"
)
func main() {
@ -22,6 +23,7 @@ func main() {
// Comment out line below to see Window.SetTitle() work
TitleBar: mac.TitleBarHiddenInset(),
Menu: createApplicationMenu(),
Tray: createApplicationTray(),
},
LogLevel: logger.TRACE,
})
@ -34,5 +36,8 @@ func main() {
app.Bind(&Window{})
app.Bind(&Menu{})
app.Run()
err := app.Run()
if err != nil {
log.Fatal(err)
}
}

View File

@ -6,7 +6,7 @@ import (
"strconv"
"sync"
wails "github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/menu"
)
@ -213,6 +213,12 @@ func (m *Menu) insertAfterRandom(_ *menu.MenuItem) {
m.runtime.Menu.Update()
}
func createApplicationTray() *menu.Menu {
trayMenu := &menu.Menu{}
trayMenu.Append(menu.Text("Hello from the tray!", "hi"))
return trayMenu
}
func createApplicationMenu() *menu.Menu {
// Create menu