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:
parent
65bea04080
commit
11bf564b73
@ -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
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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
|
||||
|
186
v2/internal/subsystem/tray.go
Normal file
186
v2/internal/subsystem/tray.go
Normal 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")
|
||||
}
|
@ -9,4 +9,5 @@ type Options struct {
|
||||
WebviewIsTransparent bool
|
||||
WindowBackgroundIsTranslucent bool
|
||||
Menu *menu.Menu
|
||||
Tray *menu.Menu
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user