Compare commits

...

34 Commits

Author SHA1 Message Date
Lea Anthony
e65118e962 Fixed and refactored context menu support 2021-01-13 22:51:44 +11:00
Lea Anthony
de06fc7dcc Remove a ton of unused code 2021-01-13 21:29:20 +11:00
Lea Anthony
a86fbbb440 Removal of menu IDs. WARNING: context menus are currently broken 2021-01-13 21:14:44 +11:00
Lea Anthony
3045ec107f attempt at preventing menu flicker when removing an icon 2021-01-13 20:47:08 +11:00
Lea Anthony
3a9557ad30 Support removal of tray icons 2021-01-13 20:38:46 +11:00
Lea Anthony
583153383a Refactor, tidy up and trim the fat! 2021-01-13 20:19:01 +11:00
Lea Anthony
3f53e8fd5f Refactor, tidy up and trim the fat! 2021-01-13 11:47:54 +11:00
Lea Anthony
5c9402323a Initial support for multiple traymenus 2021-01-13 10:28:32 +11:00
Lea Anthony
1921862b53 Partially introduce context menu changes back 2021-01-12 21:20:08 +11:00
Lea Anthony
0f7acd39fc [WIP] Fix the madness 2021-01-12 20:39:19 +11:00
Lea Anthony
1a7507f524 [WIP] Add tray menu store + refactor 2021-01-12 16:55:30 +11:00
Lea Anthony
db6dde3e50 [WIP] Support tray in menu manager 2021-01-12 15:55:28 +11:00
Lea Anthony
4e58b7697a Add context menu to menumanager. Slight refactor. 2021-01-11 14:19:01 +11:00
Lea Anthony
55d7d9693f Removed Menu GetByID / RemoveByID 2021-01-11 13:41:07 +11:00
Lea Anthony
b4b7c9d306 Implemented MenuItem.Remove() to remove from the menu structures 2021-01-11 11:57:49 +11:00
Lea Anthony
a4153fae57 Remove Menu.On() 2021-01-11 11:25:48 +11:00
Lea Anthony
8053357d99 Support Menu updates! 2021-01-11 11:21:28 +11:00
Lea Anthony
7347d2caa2 Add wails debug command 2021-01-11 11:20:25 +11:00
Lea Anthony
e6491bcbb7 Fix bad deallocation of context menus 2021-01-11 07:03:59 +11:00
Lea Anthony
26a291dbee [WIP] Use json payload for click message 2021-01-11 06:40:00 +11:00
Lea Anthony
8ee8c9b07c [WIP] New menu processor 2021-01-10 21:31:13 +11:00
Lea Anthony
3a2d01813a Don't inline functions in debug builds 2021-01-10 20:32:25 +11:00
Lea Anthony
d2dadc386f Add combo modifier to kitchen sink 2021-01-08 11:39:18 +11:00
Lea Anthony
faa8f02b08 Bugfix for message dialog icons 2021-01-08 11:39:02 +11:00
Lea Anthony
fbee9ba240 Support UpdateContextMenus. Submenus are now *menu.Menu. Tidy up++ 2021-01-08 06:28:51 +11:00
Lea Anthony
2a69786d7e Remove old code (woohoo!) 2021-01-07 21:36:39 +11:00
Lea Anthony
f460bf91ef [WIP] Normalisation of context menus creation/callbacks.
TODO: UpdateContextMenu()
2021-01-07 21:34:04 +11:00
Lea Anthony
bd74d45a91 Normalisation of callbacks for menus. App menu converted to new Menus. 2021-01-06 20:50:41 +11:00
Lea Anthony
c65522f0b6 Huge refactor of menus. Start of normalisation of callbacks. 2021-01-06 17:36:59 +11:00
Lea Anthony
5f2c437136 Bugfix dealloc contextmenus. Create common.h. WIP: new menu handling 2021-01-06 12:53:11 +11:00
Lea Anthony
87e974e080 Refactor/Tidy up Ffenestri darwin 2021-01-06 11:56:01 +11:00
Lea Anthony
f77729fc0b v2.0.0-alpha.6 2021-01-05 14:28:50 +11:00
Lea Anthony
2a8ce96830 Remove custom asset bundling. Use Go's embed instead! 2021-01-05 14:28:18 +11:00
Lea Anthony
9be539cfb8 Force rebuild each time to pick up .h changes Tidy up go.mod. Bump version. 2021-01-05 14:01:53 +11:00
56 changed files with 3215 additions and 2792 deletions

View File

@@ -0,0 +1,123 @@
package debug
import (
"fmt"
"github.com/wailsapp/wails/v2/internal/shell"
"io"
"os"
"runtime"
"strings"
"time"
"github.com/leaanthony/clir"
"github.com/leaanthony/slicer"
"github.com/wailsapp/wails/v2/pkg/clilogger"
"github.com/wailsapp/wails/v2/pkg/commands/build"
)
// AddSubcommand adds the `debug` command for the Wails application
func AddSubcommand(app *clir.Cli, w io.Writer) error {
outputType := "desktop"
validTargetTypes := slicer.String([]string{"desktop", "hybrid", "server"})
command := app.NewSubCommand("debug", "Builds the application then runs delve on the binary")
// Setup target type flag
description := "Type of application to build. Valid types: " + validTargetTypes.Join(",")
command.StringFlag("t", description, &outputType)
compilerCommand := "go"
command.StringFlag("compiler", "Use a different go compiler to build, eg go1.15beta1", &compilerCommand)
quiet := false
command.BoolFlag("q", "Suppress output to console", &quiet)
// ldflags to pass to `go`
ldflags := ""
command.StringFlag("ldflags", "optional ldflags", &ldflags)
// Log to file
logFile := ""
command.StringFlag("l", "Log to file", &logFile)
command.Action(func() error {
// Create logger
logger := clilogger.New(w)
logger.Mute(quiet)
// Validate output type
if !validTargetTypes.Contains(outputType) {
return fmt.Errorf("output type '%s' is not valid", outputType)
}
if !quiet {
app.PrintBanner()
}
task := fmt.Sprintf("Building %s Application", strings.Title(outputType))
logger.Println(task)
logger.Println(strings.Repeat("-", len(task)))
// Setup mode
mode := build.Debug
// Create BuildOptions
buildOptions := &build.Options{
Logger: logger,
OutputType: outputType,
Mode: mode,
Pack: false,
Platform: runtime.GOOS,
LDFlags: ldflags,
Compiler: compilerCommand,
KeepAssets: false,
}
outputFilename, err := doDebugBuild(buildOptions)
if err != nil {
return err
}
// Check delve exists
delveExists := shell.CommandExists("dlv")
if !delveExists {
return fmt.Errorf("cannot launch delve (Is it installed?)")
}
// Get cwd
cwd, err := os.Getwd()
if err != nil {
return err
}
// Launch delve
println("Launching Delve on port 2345...")
command := shell.CreateCommand(cwd, "dlv", "--listen=:2345", "--headless=true", "--api-version=2", "--accept-multiclient", "exec", outputFilename)
return command.Run()
})
return nil
}
// doDebugBuild is our main build command
func doDebugBuild(buildOptions *build.Options) (string, error) {
// Start Time
start := time.Now()
outputFilename, err := build.Build(buildOptions)
if err != nil {
return "", err
}
// Output stats
elapsed := time.Since(start)
buildOptions.Logger.Println("")
buildOptions.Logger.Println(fmt.Sprintf("Built '%s' in %s.", outputFilename, elapsed.Round(time.Millisecond).String()))
buildOptions.Logger.Println("")
return outputFilename, nil
}

View File

@@ -85,7 +85,10 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
// If this is a folder, add it to our watch list
if fs.DirExists(event.Name) {
if !strings.Contains(event.Name, "node_modules") {
watcher.Add(event.Name)
err := watcher.Add(event.Name)
if err != nil {
logger.Fatal("%s", err.Error())
}
logger.Println("Watching directory: %s", event.Name)
}
}
@@ -173,7 +176,10 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
// Kill the current program if running
if debugBinaryProcess != nil {
debugBinaryProcess.Kill()
err := debugBinaryProcess.Kill()
if err != nil {
return err
}
}
logger.Println("Development mode exited")
@@ -231,7 +237,10 @@ func restartApp(logger *clilogger.CLILogger, outputType string, ldflags string,
err = newProcess.Start()
if err != nil {
// Remove binary
fs.DeleteFile(appBinary)
deleteError := fs.DeleteFile(appBinary)
if deleteError != nil {
logger.Fatal("Unable to delete app binary: " + appBinary)
}
logger.Fatal("Unable to start application: %s", err.Error())
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/leaanthony/clir"
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/build"
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/debug"
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/dev"
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/doctor"
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/generate"
@@ -27,6 +28,11 @@ func main() {
if err != nil {
fatal(err.Error())
}
err = debug.AddSubcommand(app, os.Stdout)
if err != nil {
fatal(err.Error())
}
err = doctor.AddSubcommand(app, os.Stdout)
if err != nil {
fatal(err.Error())

View File

@@ -1,3 +1,3 @@
package main
var version = "v2.0.0-alpha.4"
var version = "v2.0.0-alpha.6"

View File

@@ -1,6 +1,6 @@
module github.com/wailsapp/wails/v2
go 1.13
go 1.15
require (
github.com/davecgh/go-spew v1.1.1

View File

@@ -6,6 +6,7 @@ import (
"github.com/wailsapp/wails/v2/internal/binding"
"github.com/wailsapp/wails/v2/internal/ffenestri"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/menumanager"
"github.com/wailsapp/wails/v2/internal/messagedispatcher"
"github.com/wailsapp/wails/v2/internal/runtime"
"github.com/wailsapp/wails/v2/internal/servicebus"
@@ -23,15 +24,15 @@ type App struct {
options *options.App
// Subsystems
log *subsystem.Log
runtime *subsystem.Runtime
event *subsystem.Event
binding *subsystem.Binding
call *subsystem.Call
menu *subsystem.Menu
tray *subsystem.Tray
contextmenus *subsystem.ContextMenus
dispatcher *messagedispatcher.Dispatcher
log *subsystem.Log
runtime *subsystem.Runtime
event *subsystem.Event
binding *subsystem.Binding
call *subsystem.Call
menu *subsystem.Menu
dispatcher *messagedispatcher.Dispatcher
menuManager *menumanager.Manager
// Indicates if the app is in debug mode
debug bool
@@ -54,13 +55,32 @@ func CreateApp(appoptions *options.App) (*App, error) {
myLogger := logger.New(appoptions.Logger)
myLogger.SetLogLevel(appoptions.LogLevel)
window := ffenestri.NewApplicationWithConfig(appoptions, myLogger)
// Create the menu manager
menuManager := menumanager.NewManager()
// Process the application menu
menuManager.SetApplicationMenu(options.GetApplicationMenu(appoptions))
// Process context menus
contextMenus := options.GetContextMenus(appoptions)
for _, contextMenu := range contextMenus {
menuManager.AddContextMenu(contextMenu)
}
// Process tray menus
trayMenus := options.GetTrayMenus(appoptions)
for _, trayMenu := range trayMenus {
menuManager.AddTrayMenu(trayMenu)
}
window := ffenestri.NewApplicationWithConfig(appoptions, myLogger, menuManager)
result := &App{
window: window,
servicebus: servicebus.New(myLogger),
logger: myLogger,
bindings: binding.NewBindings(myLogger),
window: window,
servicebus: servicebus.New(myLogger),
logger: myLogger,
bindings: binding.NewBindings(myLogger),
menuManager: menuManager,
}
result.options = appoptions
@@ -92,12 +112,7 @@ func (a *App) Run() error {
return err
}
// Start the runtime
applicationMenu := options.GetApplicationMenu(a.options)
trayMenu := options.GetTray(a.options)
contextMenus := options.GetContextMenus(a.options)
runtimesubsystem, err := subsystem.NewRuntime(a.servicebus, a.logger, applicationMenu, trayMenu, contextMenus)
runtimesubsystem, err := subsystem.NewRuntime(a.servicebus, a.logger)
if err != nil {
return err
}
@@ -155,43 +170,15 @@ func (a *App) Run() error {
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
}
// Start the menu subsystem
menusubsystem, err := subsystem.NewMenu(a.servicebus, a.logger, a.menuManager)
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
}
}
// Optionally start the context menu subsystem
if contextMenus != nil {
contextmenussubsystem, err := subsystem.NewContextMenus(contextMenus, a.servicebus, a.logger)
if err != nil {
return err
}
a.contextmenus = contextmenussubsystem
err = a.contextmenus.Start()
if err != nil {
return err
}
a.menu = menusubsystem
err = a.menu.Start()
if err != nil {
return err
}
// Start the call subsystem

View File

@@ -0,0 +1,88 @@
//
// Created by Lea Anthony on 6/1/21.
//
#include "common.h"
// Credit: https://stackoverflow.com/a/8465083
char* concat(const char *string1, const char *string2)
{
const size_t len1 = strlen(string1);
const size_t len2 = strlen(string2);
char *result = malloc(len1 + len2 + 1);
strcpy(result, string1);
memcpy(result + len1, string2, len2 + 1);
return result;
}
// 10k is more than enough for a log message
#define MAXMESSAGE 1024*10
char abortbuffer[MAXMESSAGE];
void ABORT(const char *message, ...) {
const char *temp = concat("FATAL: ", message);
va_list args;
va_start(args, message);
vsnprintf(abortbuffer, MAXMESSAGE, temp, args);
printf("%s\n", &abortbuffer[0]);
MEMFREE(temp);
va_end(args);
exit(1);
}
int freeHashmapItem(void *const context, struct hashmap_element_s *const e) {
free(e->data);
return -1;
}
const char* getJSONString(JsonNode *item, const char* key) {
// Get key
JsonNode *node = json_find_member(item, key);
const char *result = "";
if ( node != NULL && node->tag == JSON_STRING) {
result = node->string_;
}
return result;
}
void ABORT_JSON(JsonNode *node, const char* key) {
ABORT("Unable to read required key '%s' from JSON: %s\n", key, json_encode(node));
}
const char* mustJSONString(JsonNode *node, const char* key) {
const char* result = getJSONString(node, key);
if ( result == NULL ) {
ABORT_JSON(node, key);
}
return result;
}
JsonNode* mustJSONObject(JsonNode *node, const char* key) {
struct JsonNode* result = getJSONObject(node, key);
if ( result == NULL ) {
ABORT_JSON(node, key);
}
return result;
}
JsonNode* getJSONObject(JsonNode* node, const char* key) {
return json_find_member(node, key);
}
bool getJSONBool(JsonNode *item, const char* key, bool *result) {
JsonNode *node = json_find_member(item, key);
if ( node != NULL && node->tag == JSON_BOOL) {
*result = node->bool_;
return true;
}
return false;
}
bool getJSONInt(JsonNode *item, const char* key, int *result) {
JsonNode *node = json_find_member(item, key);
if ( node != NULL && node->tag == JSON_NUMBER) {
*result = (int) node->number_;
return true;
}
return false;
}

View File

@@ -0,0 +1,38 @@
//
// Created by Lea Anthony on 6/1/21.
//
#ifndef COMMON_H
#define COMMON_H
#define OBJC_OLD_DISPATCH_PROTOTYPES 1
#include <objc/objc-runtime.h>
#include <CoreGraphics/CoreGraphics.h>
#include <stdio.h>
#include <stdarg.h>
#include "string.h"
#include "hashmap.h"
#include "vec.h"
#include "json.h"
#define STREQ(a,b) strcmp(a, b) == 0
#define STREMPTY(string) strlen(string) == 0
#define STRCOPY(a) concat(a, "")
#define STR_HAS_CHARS(input) input != NULL && strlen(input) > 0
#define MEMFREE(input) free((void*)input); input = NULL;
#define FREE_AND_SET(variable, value) if( variable != NULL ) { MEMFREE(variable); } variable = value
// Credit: https://stackoverflow.com/a/8465083
char* concat(const char *string1, const char *string2);
void ABORT(const char *message, ...);
int freeHashmapItem(void *const context, struct hashmap_element_s *const e);
const char* getJSONString(JsonNode *item, const char* key);
const char* mustJSONString(JsonNode *node, const char* key);
JsonNode* getJSONObject(JsonNode* node, const char* key);
JsonNode* mustJSONObject(JsonNode *node, const char* key);
bool getJSONBool(JsonNode *item, const char* key, bool *result);
bool getJSONInt(JsonNode *item, const char* key, int *result);
#endif //ASSETS_C_COMMON_H

View File

@@ -0,0 +1,99 @@
////
//// Created by Lea Anthony on 6/1/21.
////
//
#include "ffenestri_darwin.h"
#include "common.h"
#include "contextmenus_darwin.h"
#include "menu_darwin.h"
ContextMenu* NewContextMenu(const char* contextMenuJSON) {
ContextMenu* result = malloc(sizeof(ContextMenu));
JsonNode* processedJSON = json_decode(contextMenuJSON);
if( processedJSON == NULL ) {
ABORT("[NewTrayMenu] Unable to parse TrayMenu JSON: %s", contextMenuJSON);
}
// Save reference to this json
result->processedJSON = processedJSON;
result->ID = mustJSONString(processedJSON, "ID");
JsonNode* processedMenu = mustJSONObject(processedJSON, "ProcessedMenu");
result->menu = NewMenu(processedMenu);
result->nsmenu = NULL;
result->menu->menuType = ContextMenuType;
result->menu->parentData = result;
result->contextMenuData = NULL;
return result;
}
ContextMenu* GetContextMenuByID(ContextMenuStore* store, const char *contextMenuID) {
return (ContextMenu*)hashmap_get(&store->contextMenuMap, (char*)contextMenuID, strlen(contextMenuID));
}
void DeleteContextMenu(ContextMenu* contextMenu) {
// Free Menu
DeleteMenu(contextMenu->menu);
// Delete any context menu data we may have stored
if( contextMenu->contextMenuData != NULL ) {
MEMFREE(contextMenu->contextMenuData);
}
// Free JSON
if (contextMenu->processedJSON != NULL ) {
json_delete(contextMenu->processedJSON);
contextMenu->processedJSON = NULL;
}
// Free context menu
free(contextMenu);
}
int freeContextMenu(void *const context, struct hashmap_element_s *const e) {
DeleteContextMenu(e->data);
return -1;
}
void ShowContextMenu(ContextMenuStore* store, id mainWindow, const char *contextMenuID, const char *contextMenuData) {
// If no context menu ID was given, abort
if( contextMenuID == NULL ) {
return;
}
ContextMenu* contextMenu = GetContextMenuByID(store, contextMenuID);
// We don't need the ID now
MEMFREE(contextMenuID);
if( contextMenu == NULL ) {
// Free context menu data
if( contextMenuData != NULL ) {
MEMFREE(contextMenuData);
return;
}
}
// We need to store the context menu data. Free existing data if we have it
// and set to the new value.
FREE_AND_SET(contextMenu->contextMenuData, contextMenuData);
// Grab the content view and show the menu
id contentView = msg(mainWindow, s("contentView"));
// Get the triggering event
id menuEvent = msg(mainWindow, s("currentEvent"));
if( contextMenu->nsmenu == NULL ) {
// GetMenu creates the NSMenu
contextMenu->nsmenu = GetMenu(contextMenu->menu);
}
// Show popup
msg(c("NSMenu"), s("popUpContextMenu:withEvent:forView:"), contextMenu->nsmenu, menuEvent, contentView);
}

View File

@@ -0,0 +1,33 @@
////
//// Created by Lea Anthony on 6/1/21.
////
//
#ifndef CONTEXTMENU_DARWIN_H
#define CONTEXTMENU_DARWIN_H
#include "json.h"
#include "menu_darwin.h"
#include "contextmenustore_darwin.h"
typedef struct {
const char* ID;
id nsmenu;
Menu* menu;
JsonNode* processedJSON;
// Context menu data is given by the frontend when clicking a context menu.
// We send this to the backend when an item is selected
const char* contextMenuData;
} ContextMenu;
ContextMenu* NewContextMenu(const char* contextMenuJSON);
ContextMenu* GetContextMenuByID( ContextMenuStore* store, const char *contextMenuID);
void DeleteContextMenu(ContextMenu* contextMenu);
int freeContextMenu(void *const context, struct hashmap_element_s *const e);
void ShowContextMenu(ContextMenuStore* store, id mainWindow, const char *contextMenuID, const char *contextMenuData);
#endif //CONTEXTMENU_DARWIN_H

View File

@@ -0,0 +1,65 @@
#include "contextmenus_darwin.h"
#include "contextmenustore_darwin.h"
ContextMenuStore* NewContextMenuStore() {
ContextMenuStore* result = malloc(sizeof(ContextMenuStore));
// Allocate Context Menu Store
if( 0 != hashmap_create((const unsigned)4, &result->contextMenuMap)) {
ABORT("[NewContextMenus] Not enough memory to allocate contextMenuStore!");
}
return result;
}
void AddContextMenuToStore(ContextMenuStore* store, const char* contextMenuJSON) {
ContextMenu* newMenu = NewContextMenu(contextMenuJSON);
//TODO: check if there is already an entry for this menu
hashmap_put(&store->contextMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
}
ContextMenu* GetContextMenuFromStore(ContextMenuStore* store, const char* menuID) {
// Get the current menu
return hashmap_get(&store->contextMenuMap, menuID, strlen(menuID));
}
void UpdateContextMenuInStore(ContextMenuStore* store, const char* menuJSON) {
ContextMenu* newContextMenu = NewContextMenu(menuJSON);
// Get the current menu
ContextMenu *currentMenu = GetContextMenuFromStore(store, newContextMenu->ID);
if ( currentMenu == NULL ) {
ABORT("Attempted to update unknown context menu with ID '%s'.", newContextMenu->ID);
}
hashmap_remove(&store->contextMenuMap, newContextMenu->ID, strlen(newContextMenu->ID));
// Save the status bar reference
DeleteContextMenu(currentMenu);
hashmap_put(&store->contextMenuMap, newContextMenu->ID, strlen(newContextMenu->ID), newContextMenu);
}
void DeleteContextMenuStore(ContextMenuStore* store) {
// Guard against NULLs
if( store == NULL ) {
return;
}
// Delete context menus
if( hashmap_num_entries(&store->contextMenuMap) > 0 ) {
if (0 != hashmap_iterate_pairs(&store->contextMenuMap, freeContextMenu, NULL)) {
ABORT("[DeleteContextMenuStore] Failed to release contextMenuStore entries!");
}
}
// Free context menu hashmap
hashmap_destroy(&store->contextMenuMap);
}

View File

@@ -0,0 +1,27 @@
//
// Created by Lea Anthony on 7/1/21.
//
#ifndef CONTEXTMENUSTORE_DARWIN_H
#define CONTEXTMENUSTORE_DARWIN_H
#include "common.h"
typedef struct {
int dummy;
// This is our context menu store which keeps track
// of all instances of ContextMenus
struct hashmap_s contextMenuMap;
} ContextMenuStore;
ContextMenuStore* NewContextMenuStore();
void DeleteContextMenuStore(ContextMenuStore* store);
void UpdateContextMenuInStore(ContextMenuStore* store, const char* menuJSON);
void AddContextMenuToStore(ContextMenuStore* store, const char* contextMenuJSON);
#endif //CONTEXTMENUSTORE_DARWIN_H

View File

@@ -1,6 +1,7 @@
package ffenestri
import (
"github.com/wailsapp/wails/v2/internal/menumanager"
"runtime"
"strings"
"unsafe"
@@ -31,7 +32,10 @@ type Application struct {
memory []unsafe.Pointer
// This is the main app pointer
app unsafe.Pointer
app *C.struct_Application
// Manages menus
menuManager *menumanager.Manager
// Logger
logger logger.CustomLogger
@@ -52,10 +56,11 @@ func init() {
}
// NewApplicationWithConfig creates a new application based on the given config
func NewApplicationWithConfig(config *options.App, logger *logger.Logger) *Application {
func NewApplicationWithConfig(config *options.App, logger *logger.Logger, menuManager *menumanager.Manager) *Application {
return &Application{
config: config,
logger: logger.CustomLogger("Ffenestri"),
config: config,
logger: logger.CustomLogger("Ffenestri"),
menuManager: menuManager,
}
}
@@ -116,7 +121,7 @@ func (a *Application) Run(incomingDispatcher Dispatcher, bindings string, debug
app := C.NewApplication(title, width, height, resizable, devtools, fullscreen, startHidden, logLevel)
// Save app reference
a.app = unsafe.Pointer(app)
a.app = (*C.struct_Application)(app)
// Set Min Window Size
minWidth := C.int(a.config.MinWidth)
@@ -152,7 +157,10 @@ func (a *Application) Run(incomingDispatcher Dispatcher, bindings string, debug
dispatcher = incomingDispatcher.RegisterClient(newClient(a))
// Process platform settings
a.processPlatformSettings()
err := a.processPlatformSettings()
if err != nil {
return err
}
// Check we could initialise the application
if app != nil {

View File

@@ -2,41 +2,42 @@
#define __FFENESTRI_H__
#include <stdio.h>
struct Application;
extern void *NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden, int logLevel);
extern void SetMinWindowSize(void *app, int minWidth, int minHeight);
extern void SetMaxWindowSize(void *app, int maxWidth, int maxHeight);
extern void Run(void *app, int argc, char **argv);
extern void DestroyApplication(void *app);
extern void SetDebug(void *app, int flag);
extern void SetBindings(void *app, const char *bindings);
extern void ExecJS(void *app, const char *script);
extern void Hide(void *app);
extern void Show(void *app);
extern void Center(void *app);
extern void Maximise(void *app);
extern void Unmaximise(void *app);
extern void ToggleMaximise(void *app);
extern void Minimise(void *app);
extern void Unminimise(void *app);
extern void ToggleMinimise(void *app);
extern void SetColour(void *app, int red, int green, int blue, int alpha);
extern void SetSize(void *app, int width, int height);
extern void SetPosition(void *app, int x, int y);
extern void Quit(void *app);
extern void SetTitle(void *app, const char *title);
extern void Fullscreen(void *app);
extern void UnFullscreen(void *app);
extern void ToggleFullscreen(void *app);
extern void DisableFrame(void *app);
extern void OpenDialog(void *appPointer, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int allowFiles, int allowDirs, int allowMultiple, int showHiddenFiles, int canCreateDirectories, int resolvesAliases, int treatPackagesAsDirectories);
extern void SaveDialog(void *appPointer, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int showHiddenFiles, int canCreateDirectories, int treatPackagesAsDirectories);
extern void MessageDialog(void *appPointer, char *callbackID, char *type, char *title, char *message, char *icon, char *button1, char *button2, char *button3, char *button4, char *defaultButton, char *cancelButton);
extern void DarkModeEnabled(void *appPointer, char *callbackID);
extern void UpdateMenu(void *app, char *menuAsJSON);
extern void UpdateTray(void *app, char *menuAsJSON);
extern void UpdateContextMenus(void *app, char *contextMenusAsJSON);
extern void UpdateTrayLabel(void *app, const char *label);
extern void UpdateTrayIcon(void *app, const char *label);
extern struct Application *NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden, int logLevel);
extern void SetMinWindowSize(struct Application*, int minWidth, int minHeight);
extern void SetMaxWindowSize(struct Application*, int maxWidth, int maxHeight);
extern void Run(struct Application*, int argc, char **argv);
extern void DestroyApplication(struct Application*);
extern void SetDebug(struct Application*, int flag);
extern void SetBindings(struct Application*, const char *bindings);
extern void ExecJS(struct Application*, const char *script);
extern void Hide(struct Application*);
extern void Show(struct Application*);
extern void Center(struct Application*);
extern void Maximise(struct Application*);
extern void Unmaximise(struct Application*);
extern void ToggleMaximise(struct Application*);
extern void Minimise(struct Application*);
extern void Unminimise(struct Application*);
extern void ToggleMinimise(struct Application*);
extern void SetColour(struct Application*, int red, int green, int blue, int alpha);
extern void SetSize(struct Application*, int width, int height);
extern void SetPosition(struct Application*, int x, int y);
extern void Quit(struct Application*);
extern void SetTitle(struct Application*, const char *title);
extern void Fullscreen(struct Application*);
extern void UnFullscreen(struct Application*);
extern void ToggleFullscreen(struct Application*);
extern void DisableFrame(struct Application*);
extern void OpenDialog(struct Application*, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int allowFiles, int allowDirs, int allowMultiple, int showHiddenFiles, int canCreateDirectories, int resolvesAliases, int treatPackagesAsDirectories);
extern void SaveDialog(struct Application*, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int showHiddenFiles, int canCreateDirectories, int treatPackagesAsDirectories);
extern void MessageDialog(struct Application*, char *callbackID, char *type, char *title, char *message, char *icon, char *button1, char *button2, char *button3, char *button4, char *defaultButton, char *cancelButton);
extern void DarkModeEnabled(struct Application*, char *callbackID);
extern void SetApplicationMenu(struct Application*, const char *);
extern void AddTrayMenu(struct Application*, const char *menuTrayJSON);
extern void UpdateTrayMenu(struct Application*, const char *menuTrayJSON);
extern void AddContextMenu(struct Application*, char *contextMenuJSON);
extern void UpdateContextMenu(struct Application*, char *contextMenuJSON);
#endif

View File

@@ -12,8 +12,6 @@ package ffenestri
import "C"
import (
"encoding/json"
"github.com/wailsapp/wails/v2/pkg/menu"
"strconv"
"github.com/wailsapp/wails/v2/internal/logger"
@@ -186,57 +184,14 @@ func (c *Client) DarkModeEnabled(callbackID string) {
C.DarkModeEnabled(c.app.app, c.app.string2CString(callbackID))
}
func (c *Client) UpdateMenu(menu *menu.Menu) {
// Guard against nil menus
if menu == nil {
return
}
// Process the menu
processedMenu := NewProcessedMenu(menu)
menuJSON, err := json.Marshal(processedMenu)
if err != nil {
c.app.logger.Error("Error processing updated Menu: %s", err.Error())
return
}
C.UpdateMenu(c.app.app, c.app.string2CString(string(menuJSON)))
func (c *Client) SetApplicationMenu(applicationMenuJSON string) {
C.SetApplicationMenu(c.app.app, c.app.string2CString(applicationMenuJSON))
}
func (c *Client) UpdateTray(menu *menu.Menu) {
// Guard against nil menus
if menu == nil {
return
}
// Process the menu
processedMenu := NewProcessedMenu(menu)
trayMenuJSON, err := json.Marshal(processedMenu)
if err != nil {
c.app.logger.Error("Error processing updated Tray: %s", err.Error())
return
}
C.UpdateTray(c.app.app, c.app.string2CString(string(trayMenuJSON)))
func (c *Client) UpdateTrayMenu(trayMenuJSON string) {
C.UpdateTrayMenu(c.app.app, c.app.string2CString(trayMenuJSON))
}
func (c *Client) UpdateTrayLabel(label string) {
C.UpdateTrayLabel(c.app.app, c.app.string2CString(label))
}
func (c *Client) UpdateTrayIcon(name string) {
C.UpdateTrayIcon(c.app.app, c.app.string2CString(name))
}
func (c *Client) UpdateContextMenus(contextMenus *menu.ContextMenus) {
// Guard against nil contextMenus
if contextMenus == nil {
return
}
// Process the menu
contextMenusJSON, err := json.Marshal(contextMenus)
if err != nil {
c.app.logger.Error("Error processing updated Context Menus: %s", err.Error())
return
}
C.UpdateContextMenus(c.app.app, c.app.string2CString(string(contextMenusJSON)))
func (c *Client) UpdateContextMenu(contextMenuJSON string) {
C.UpdateContextMenu(c.app.app, c.app.string2CString(contextMenuJSON))
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,15 +4,11 @@ package ffenestri
#cgo darwin CFLAGS: -DFFENESTRI_DARWIN=1
#cgo darwin LDFLAGS: -framework WebKit -lobjc
#include "ffenestri.h"
#include "ffenestri_darwin.h"
*/
import "C"
import (
"encoding/json"
"fmt"
"github.com/wailsapp/wails/v2/pkg/options"
)
func (a *Application) processPlatformSettings() error {
@@ -63,54 +59,32 @@ func (a *Application) processPlatformSettings() error {
}
// Process menu
applicationMenu := options.GetApplicationMenu(a.config)
if applicationMenu != 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(applicationMenu)
applicationMenuJSON, err := json.Marshal(processedMenu)
if err != nil {
return err
}
C.SetMenu(a.app, a.string2CString(string(applicationMenuJSON)))
//applicationMenu := options.GetApplicationMenu(a.config)
applicationMenu := a.menuManager.GetApplicationMenuJSON()
if applicationMenu != "" {
C.SetApplicationMenu(a.app, a.string2CString(applicationMenu))
}
// Process tray
tray := options.GetTray(a.config)
if 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(tray.Menu)
trayMenuJSON, err := json.Marshal(processedMenu)
if err != nil {
return err
trays, err := a.menuManager.GetTrayMenus()
if err != nil {
return err
}
if trays != nil {
for _, tray := range trays {
C.AddTrayMenu(a.app, a.string2CString(tray))
}
C.SetTray(a.app, a.string2CString(string(trayMenuJSON)), a.string2CString(tray.Label), a.string2CString(tray.Icon))
}
// Process context menus
contextMenus := options.GetContextMenus(a.config)
contextMenus, err := a.menuManager.GetContextMenus()
if err != nil {
return err
}
if contextMenus != nil {
contextMenusJSON, err := json.Marshal(contextMenus)
fmt.Printf("\n\nCONTEXT MENUS:\n %+v\n\n", string(contextMenusJSON))
if err != nil {
return err
for _, contextMenu := range contextMenus {
C.AddContextMenu(a.app, a.string2CString(contextMenu))
}
C.SetContextMenus(a.app, a.string2CString(string(contextMenusJSON)))
}
return nil

View File

@@ -2,18 +2,112 @@
#ifndef FFENESTRI_DARWIN_H
#define FFENESTRI_DARWIN_H
extern void TitlebarAppearsTransparent(void *);
extern void HideTitle(void *);
extern void HideTitleBar(void *);
extern void FullSizeContent(void *);
extern void UseToolbar(void *);
extern void HideToolbarSeparator(void *);
extern void DisableFrame(void *);
extern void SetAppearance(void *, const char *);
extern void WebviewIsTransparent(void *);
extern void WindowBackgroundIsTranslucent(void *);
extern void SetMenu(void *, const char *);
extern void SetTray(void *, const char *, const char *, const char *);
extern void SetContextMenus(void *, const char *);
#define OBJC_OLD_DISPATCH_PROTOTYPES 1
#include <objc/objc-runtime.h>
#include <CoreGraphics/CoreGraphics.h>
#include "json.h"
#include "hashmap.h"
#include "stdlib.h"
// Macros to make it slightly more sane
#define msg objc_msgSend
#define c(str) (id)objc_getClass(str)
#define s(str) sel_registerName(str)
#define u(str) sel_getUid(str)
#define str(input) msg(c("NSString"), s("stringWithUTF8String:"), input)
#define strunicode(input) msg(c("NSString"), s("stringWithFormat:"), str("%C"), (unsigned short)input)
#define cstr(input) (const char *)msg(input, s("UTF8String"))
#define url(input) msg(c("NSURL"), s("fileURLWithPath:"), str(input))
#define ALLOC(classname) msg(c(classname), s("alloc"))
#define ALLOC_INIT(classname) msg(msg(c(classname), s("alloc")), s("init"))
#define GET_FRAME(receiver) ((CGRect(*)(id, SEL))objc_msgSend_stret)(receiver, s("frame"))
#define GET_BOUNDS(receiver) ((CGRect(*)(id, SEL))objc_msgSend_stret)(receiver, s("bounds"))
#define GET_BACKINGSCALEFACTOR(receiver) ((CGFloat(*)(id, SEL))msg)(receiver, s("backingScaleFactor"))
#define ON_MAIN_THREAD(str) dispatch( ^{ str; } )
#define MAIN_WINDOW_CALL(str) msg(app->mainWindow, s((str)))
#define NSBackingStoreBuffered 2
#define NSWindowStyleMaskBorderless 0
#define NSWindowStyleMaskTitled 1
#define NSWindowStyleMaskClosable 2
#define NSWindowStyleMaskMiniaturizable 4
#define NSWindowStyleMaskResizable 8
#define NSWindowStyleMaskFullscreen 1 << 14
#define NSVisualEffectMaterialWindowBackground 12
#define NSVisualEffectBlendingModeBehindWindow 0
#define NSVisualEffectStateFollowsWindowActiveState 0
#define NSVisualEffectStateActive 1
#define NSVisualEffectStateInactive 2
#define NSViewWidthSizable 2
#define NSViewHeightSizable 16
#define NSWindowBelow -1
#define NSWindowAbove 1
#define NSSquareStatusItemLength -2.0
#define NSVariableStatusItemLength -1.0
#define NSWindowTitleHidden 1
#define NSWindowStyleMaskFullSizeContentView 1 << 15
#define NSEventModifierFlagCommand 1 << 20
#define NSEventModifierFlagOption 1 << 19
#define NSEventModifierFlagControl 1 << 18
#define NSEventModifierFlagShift 1 << 17
#define NSControlStateValueMixed -1
#define NSControlStateValueOff 0
#define NSControlStateValueOn 1
// Unbelievably, if the user swaps their button preference
// then right buttons are reported as left buttons
#define NSEventMaskLeftMouseDown 1 << 1
#define NSEventMaskLeftMouseUp 1 << 2
#define NSEventMaskRightMouseDown 1 << 3
#define NSEventMaskRightMouseUp 1 << 4
#define NSEventTypeLeftMouseDown 1
#define NSEventTypeLeftMouseUp 2
#define NSEventTypeRightMouseDown 3
#define NSEventTypeRightMouseUp 4
#define NSNoImage 0
#define NSImageOnly 1
#define NSImageLeft 2
#define NSImageRight 3
#define NSImageBelow 4
#define NSImageAbove 5
#define NSImageOverlaps 6
#define NSAlertStyleWarning 0
#define NSAlertStyleInformational 1
#define NSAlertStyleCritical 2
#define NSAlertFirstButtonReturn 1000
#define NSAlertSecondButtonReturn 1001
#define NSAlertThirdButtonReturn 1002
struct Application;
int releaseNSObject(void *const context, struct hashmap_element_s *const e);
void TitlebarAppearsTransparent(struct Application* app);
void HideTitle(struct Application* app);
void HideTitleBar(struct Application* app);
void FullSizeContent(struct Application* app);
void UseToolbar(struct Application* app);
void HideToolbarSeparator(struct Application* app);
void DisableFrame(struct Application* app);
void SetAppearance(struct Application* app, const char *);
void WebviewIsTransparent(struct Application* app);
void WindowBackgroundIsTranslucent(struct Application* app);
void SetTray(struct Application* app, const char *, const char *, const char *);
void SetContextMenus(struct Application* app, const char *);
void AddTrayMenu(struct Application* app, const char *);
#endif

View File

@@ -1,80 +0,0 @@
package ffenestri
import "github.com/wailsapp/wails/v2/pkg/menu"
// ProcessedMenu is the original menu with the addition
// of radio groups extracted from the menu data
type ProcessedMenu struct {
Menu *menu.Menu
RadioGroups []*RadioGroup
currentRadioGroup []string
}
// RadioGroup holds all the members of the same radio group
type RadioGroup struct {
Members []string
Length int
}
// NewProcessedMenu processed the given menu and returns
// the original menu with the extracted radio groups
func NewProcessedMenu(menu *menu.Menu) *ProcessedMenu {
result := &ProcessedMenu{
Menu: menu,
RadioGroups: []*RadioGroup{},
currentRadioGroup: []string{},
}
result.processMenu()
return result
}
func (p *ProcessedMenu) processMenu() {
// Loop over top level menus
for _, item := range p.Menu.Items {
// Process MenuItem
p.processMenuItem(item)
}
p.finaliseRadioGroup()
}
func (p *ProcessedMenu) processMenuItem(item *menu.MenuItem) {
switch item.Type {
// We need to recurse submenus
case menu.SubmenuType:
// Finalise any current radio groups as they don't trickle down to submenus
p.finaliseRadioGroup()
// Process each submenu item
for _, subitem := range item.SubMenu {
p.processMenuItem(subitem)
}
case menu.RadioType:
// Add the item to the radio group
p.currentRadioGroup = append(p.currentRadioGroup, item.ID)
default:
p.finaliseRadioGroup()
}
}
func (p *ProcessedMenu) finaliseRadioGroup() {
// If we were processing a radio group, fix up the references
if len(p.currentRadioGroup) > 0 {
// Create new radiogroup
group := &RadioGroup{
Members: p.currentRadioGroup,
Length: len(p.currentRadioGroup),
}
p.RadioGroups = append(p.RadioGroups, group)
// Empty the radio group
p.currentRadioGroup = []string{}
}
}

View File

@@ -0,0 +1,808 @@
//
// Created by Lea Anthony on 6/1/21.
//
#include "ffenestri_darwin.h"
#include "menu_darwin.h"
#include "contextmenus_darwin.h"
// NewMenu creates a new Menu struct, saving the given menu structure as JSON
Menu* NewMenu(JsonNode *menuData) {
Menu *result = malloc(sizeof(Menu));
result->processedMenu = menuData;
// No title by default
result->title = "";
// Initialise menuCallbackDataCache
vec_init(&result->callbackDataCache);
// Allocate MenuItem Map
if( 0 != hashmap_create((const unsigned)16, &result->menuItemMap)) {
ABORT("[NewMenu] Not enough memory to allocate menuItemMap!");
}
// Allocate the Radio Group Map
if( 0 != hashmap_create((const unsigned)4, &result->radioGroupMap)) {
ABORT("[NewMenu] Not enough memory to allocate radioGroupMap!");
}
// Init other members
result->menu = NULL;
result->parentData = NULL;
return result;
}
Menu* NewApplicationMenu(const char *menuAsJSON) {
// Parse the menu json
JsonNode *processedMenu = json_decode(menuAsJSON);
if( processedMenu == NULL ) {
// Parse error!
ABORT("Unable to parse Menu JSON: %s", menuAsJSON);
}
Menu *result = NewMenu(processedMenu);
result->menuType = ApplicationMenuType;
return result;
}
MenuItemCallbackData* CreateMenuItemCallbackData(Menu *menu, id menuItem, const char *menuID, enum MenuItemType menuItemType) {
MenuItemCallbackData* result = malloc(sizeof(MenuItemCallbackData));
result->menu = menu;
result->menuID = menuID;
result->menuItem = menuItem;
result->menuItemType = menuItemType;
// Store reference to this so we can destroy later
vec_push(&menu->callbackDataCache, result);
return result;
}
void DeleteMenu(Menu *menu) {
// Free menu item hashmap
hashmap_destroy(&menu->menuItemMap);
// Free radio group members
if( hashmap_num_entries(&menu->radioGroupMap) > 0 ) {
if (0 != hashmap_iterate_pairs(&menu->radioGroupMap, freeHashmapItem, NULL)) {
ABORT("[DeleteMenu] Failed to release radioGroupMap entries!");
}
}
// Free radio groups hashmap
hashmap_destroy(&menu->radioGroupMap);
// Free up the processed menu memory
if (menu->processedMenu != NULL) {
json_delete(menu->processedMenu);
menu->processedMenu = NULL;
}
// Release the vector memory
vec_deinit(&menu->callbackDataCache);
// Free nsmenu if we have it
if ( menu->menu != NULL ) {
msg(menu->menu, s("release"));
}
free(menu);
}
// Creates a JSON message for the given menuItemID and data
const char* createMenuClickedMessage(const char *menuItemID, const char *data, enum MenuType menuType, const char *parentID) {
JsonNode *jsonObject = json_mkobject();
json_append_member(jsonObject, "menuItemID", json_mkstring(menuItemID));
json_append_member(jsonObject, "menuType", json_mkstring(MenuTypeAsString[(int)menuType]));
if (data != NULL) {
json_append_member(jsonObject, "data", json_mkstring(data));
}
if (parentID != NULL) {
json_append_member(jsonObject, "parentID", json_mkstring(parentID));
}
const char *payload = json_encode(jsonObject);
json_delete(jsonObject);
const char *result = concat("MC", payload);
MEMFREE(payload);
return result;
}
// Callback for text menu items
void menuItemCallback(id self, SEL cmd, id sender) {
MenuItemCallbackData *callbackData = (MenuItemCallbackData *)msg(msg(sender, s("representedObject")), s("pointerValue"));
const char *message;
// Update checkbox / radio item
if( callbackData->menuItemType == Checkbox) {
// Toggle state
bool state = msg(callbackData->menuItem, s("state"));
msg(callbackData->menuItem, s("setState:"), (state? NSControlStateValueOff : NSControlStateValueOn));
} else if( callbackData->menuItemType == Radio ) {
// Check the menu items' current state
bool selected = msg(callbackData->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(&(callbackData->menu->radioGroupMap), (char*)callbackData->menuID, strlen(callbackData->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(callbackData->menuItem, s("setState:"), NSControlStateValueOn);
}
const char *menuID = callbackData->menuID;
const char *data = NULL;
enum MenuType menuType = callbackData->menu->menuType;
const char *parentID = NULL;
// Generate message to send to backend
if( menuType == ContextMenuType ) {
// Get the context menu data from the menu
ContextMenu* contextMenu = (ContextMenu*) callbackData->menu->parentData;
data = contextMenu->contextMenuData;
parentID = contextMenu->ID;
} else if ( menuType == TrayMenuType ) {
parentID = (const char*) callbackData->menu->parentData;
}
message = createMenuClickedMessage(menuID, data, menuType, parentID);
// Notify the backend
messageFromWindowCallback(message);
MEMFREE(message);
}
id processAcceleratorKey(const char *key) {
// Guard against no accelerator key
if( key == NULL ) {
return str("");
}
if( STREQ(key, "Backspace") ) {
return strunicode(0x0008);
}
if( STREQ(key, "Tab") ) {
return strunicode(0x0009);
}
if( STREQ(key, "Return") ) {
return strunicode(0x000d);
}
if( STREQ(key, "Escape") ) {
return strunicode(0x001b);
}
if( STREQ(key, "Left") ) {
return strunicode(0x001c);
}
if( STREQ(key, "Right") ) {
return strunicode(0x001d);
}
if( STREQ(key, "Up") ) {
return strunicode(0x001e);
}
if( STREQ(key, "Down") ) {
return strunicode(0x001f);
}
if( STREQ(key, "Space") ) {
return strunicode(0x0020);
}
if( STREQ(key, "Delete") ) {
return strunicode(0x007f);
}
if( STREQ(key, "Home") ) {
return strunicode(0x2196);
}
if( STREQ(key, "End") ) {
return strunicode(0x2198);
}
if( STREQ(key, "Page Up") ) {
return strunicode(0x21de);
}
if( STREQ(key, "Page Down") ) {
return strunicode(0x21df);
}
if( STREQ(key, "F1") ) {
return strunicode(0xf704);
}
if( STREQ(key, "F2") ) {
return strunicode(0xf705);
}
if( STREQ(key, "F3") ) {
return strunicode(0xf706);
}
if( STREQ(key, "F4") ) {
return strunicode(0xf707);
}
if( STREQ(key, "F5") ) {
return strunicode(0xf708);
}
if( STREQ(key, "F6") ) {
return strunicode(0xf709);
}
if( STREQ(key, "F7") ) {
return strunicode(0xf70a);
}
if( STREQ(key, "F8") ) {
return strunicode(0xf70b);
}
if( STREQ(key, "F9") ) {
return strunicode(0xf70c);
}
if( STREQ(key, "F10") ) {
return strunicode(0xf70d);
}
if( STREQ(key, "F11") ) {
return strunicode(0xf70e);
}
if( STREQ(key, "F12") ) {
return strunicode(0xf70f);
}
if( STREQ(key, "F13") ) {
return strunicode(0xf710);
}
if( STREQ(key, "F14") ) {
return strunicode(0xf711);
}
if( STREQ(key, "F15") ) {
return strunicode(0xf712);
}
if( STREQ(key, "F16") ) {
return strunicode(0xf713);
}
if( STREQ(key, "F17") ) {
return strunicode(0xf714);
}
if( STREQ(key, "F18") ) {
return strunicode(0xf715);
}
if( STREQ(key, "F19") ) {
return strunicode(0xf716);
}
if( STREQ(key, "F20") ) {
return strunicode(0xf717);
}
if( STREQ(key, "F21") ) {
return strunicode(0xf718);
}
if( STREQ(key, "F22") ) {
return strunicode(0xf719);
}
if( STREQ(key, "F23") ) {
return strunicode(0xf71a);
}
if( STREQ(key, "F24") ) {
return strunicode(0xf71b);
}
if( STREQ(key, "F25") ) {
return strunicode(0xf71c);
}
if( STREQ(key, "F26") ) {
return strunicode(0xf71d);
}
if( STREQ(key, "F27") ) {
return strunicode(0xf71e);
}
if( STREQ(key, "F28") ) {
return strunicode(0xf71f);
}
if( STREQ(key, "F29") ) {
return strunicode(0xf720);
}
if( STREQ(key, "F30") ) {
return strunicode(0xf721);
}
if( STREQ(key, "F31") ) {
return strunicode(0xf722);
}
if( STREQ(key, "F32") ) {
return strunicode(0xf723);
}
if( STREQ(key, "F33") ) {
return strunicode(0xf724);
}
if( STREQ(key, "F34") ) {
return strunicode(0xf725);
}
if( STREQ(key, "F35") ) {
return strunicode(0xf726);
}
// if( STREQ(key, "Insert") ) {
// return strunicode(0xf727);
// }
// if( STREQ(key, "PrintScreen") ) {
// return strunicode(0xf72e);
// }
// if( STREQ(key, "ScrollLock") ) {
// return strunicode(0xf72f);
// }
if( STREQ(key, "NumLock") ) {
return strunicode(0xf739);
}
return str(key);
}
void addSeparator(id menu) {
id item = msg(c("NSMenuItem"), s("separatorItem"));
msg(menu, s("addItem:"), item);
}
id createMenuItemNoAutorelease( id title, const char *action, const char *key) {
id item = ALLOC("NSMenuItem");
msg(item, s("initWithTitle:action:keyEquivalent:"), title, s(action), str(key));
return item;
}
id createMenuItem(id title, const char *action, const char *key) {
id item = ALLOC("NSMenuItem");
msg(item, s("initWithTitle:action:keyEquivalent:"), title, s(action), str(key));
msg(item, s("autorelease"));
return item;
}
id addMenuItem(id menu, const char *title, const char *action, const char *key, bool disabled) {
id item = createMenuItem(str(title), action, key);
msg(item, s("setEnabled:"), !disabled);
msg(menu, s("addItem:"), item);
return item;
}
id createMenu(id title) {
id menu = ALLOC("NSMenu");
msg(menu, s("initWithTitle:"), title);
msg(menu, s("setAutoenablesItems:"), NO);
// msg(menu, s("autorelease"));
return menu;
}
void createDefaultAppMenu(id parentMenu) {
// App Menu
id appName = msg(msg(c("NSProcessInfo"), s("processInfo")), s("processName"));
id appMenuItem = createMenuItemNoAutorelease(appName, NULL, "");
id appMenu = createMenu(appName);
msg(appMenuItem, s("setSubmenu:"), appMenu);
msg(parentMenu, s("addItem:"), appMenuItem);
id title = msg(str("Hide "), s("stringByAppendingString:"), appName);
id item = createMenuItem(title, "hide:", "h");
msg(appMenu, s("addItem:"), item);
id hideOthers = addMenuItem(appMenu, "Hide Others", "hideOtherApplications:", "h", FALSE);
msg(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand));
addMenuItem(appMenu, "Show All", "unhideAllApplications:", "", FALSE);
addSeparator(appMenu);
title = msg(str("Quit "), s("stringByAppendingString:"), appName);
item = createMenuItem(title, "terminate:", "q");
msg(appMenu, s("addItem:"), item);
}
void createDefaultEditMenu(id parentMenu) {
// Edit Menu
id editMenuItem = createMenuItemNoAutorelease(str("Edit"), NULL, "");
id editMenu = createMenu(str("Edit"));
msg(editMenuItem, s("setSubmenu:"), editMenu);
msg(parentMenu, s("addItem:"), editMenuItem);
addMenuItem(editMenu, "Undo", "undo:", "z", FALSE);
addMenuItem(editMenu, "Redo", "redo:", "y", FALSE);
addSeparator(editMenu);
addMenuItem(editMenu, "Cut", "cut:", "x", FALSE);
addMenuItem(editMenu, "Copy", "copy:", "c", FALSE);
addMenuItem(editMenu, "Paste", "paste:", "v", FALSE);
addMenuItem(editMenu, "Select All", "selectAll:", "a", FALSE);
}
void processMenuRole(Menu *menu, id parentMenu, JsonNode *item) {
const char *roleName = item->string_;
if ( STREQ(roleName, "appMenu") ) {
createDefaultAppMenu(parentMenu);
return;
}
if ( STREQ(roleName, "editMenu")) {
createDefaultEditMenu(parentMenu);
return;
}
if ( STREQ(roleName, "hide")) {
addMenuItem(parentMenu, "Hide Window", "hide:", "h", FALSE);
return;
}
if ( STREQ(roleName, "hideothers")) {
id hideOthers = addMenuItem(parentMenu, "Hide Others", "hideOtherApplications:", "h", FALSE);
msg(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand));
return;
}
if ( STREQ(roleName, "unhide")) {
addMenuItem(parentMenu, "Show All", "unhideAllApplications:", "", FALSE);
return;
}
if ( STREQ(roleName, "front")) {
addMenuItem(parentMenu, "Bring All to Front", "arrangeInFront:", "", FALSE);
return;
}
if ( STREQ(roleName, "undo")) {
addMenuItem(parentMenu, "Undo", "undo:", "z", FALSE);
return;
}
if ( STREQ(roleName, "redo")) {
addMenuItem(parentMenu, "Redo", "redo:", "y", FALSE);
return;
}
if ( STREQ(roleName, "cut")) {
addMenuItem(parentMenu, "Cut", "cut:", "x", FALSE);
return;
}
if ( STREQ(roleName, "copy")) {
addMenuItem(parentMenu, "Copy", "copy:", "c", FALSE);
return;
}
if ( STREQ(roleName, "paste")) {
addMenuItem(parentMenu, "Paste", "paste:", "v", FALSE);
return;
}
if ( STREQ(roleName, "delete")) {
addMenuItem(parentMenu, "Delete", "delete:", "", FALSE);
return;
}
if( STREQ(roleName, "pasteandmatchstyle")) {
id pasteandmatchstyle = addMenuItem(parentMenu, "Paste and Match Style", "pasteandmatchstyle:", "v", FALSE);
msg(pasteandmatchstyle, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagShift | NSEventModifierFlagCommand));
}
if ( STREQ(roleName, "selectall")) {
addMenuItem(parentMenu, "Select All", "selectAll:", "a", FALSE);
return;
}
if ( STREQ(roleName, "minimize")) {
addMenuItem(parentMenu, "Minimize", "miniaturize:", "m", FALSE);
return;
}
if ( STREQ(roleName, "zoom")) {
addMenuItem(parentMenu, "Zoom", "performZoom:", "", FALSE);
return;
}
if ( STREQ(roleName, "quit")) {
addMenuItem(parentMenu, "Quit (More work TBD)", "terminate:", "q", FALSE);
return;
}
if ( STREQ(roleName, "togglefullscreen")) {
addMenuItem(parentMenu, "Toggle Full Screen", "toggleFullScreen:", "f", FALSE);
return;
}
}
// This converts a string array of modifiers into the
// equivalent MacOS Modifier Flags
unsigned long parseModifiers(const char **modifiers) {
// Our result is a modifier flag list
unsigned long result = 0;
const char *thisModifier = modifiers[0];
int count = 0;
while( thisModifier != NULL ) {
// Determine flags
if( STREQ(thisModifier, "CmdOrCtrl") ) {
result |= NSEventModifierFlagCommand;
}
if( STREQ(thisModifier, "OptionOrAlt") ) {
result |= NSEventModifierFlagOption;
}
if( STREQ(thisModifier, "Shift") ) {
result |= NSEventModifierFlagShift;
}
if( STREQ(thisModifier, "Super") ) {
result |= NSEventModifierFlagCommand;
}
if( STREQ(thisModifier, "Control") ) {
result |= NSEventModifierFlagControl;
}
count++;
thisModifier = modifiers[count];
}
return result;
}
id processRadioMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *acceleratorkey) {
id item = ALLOC("NSMenuItem");
// Store the item in the menu item map
hashmap_put(&menu->menuItemMap, (char*)menuid, strlen(menuid), item);
// Create a MenuItemCallbackData
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, item, menuid, Radio);
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), callback);
msg(item, s("setRepresentedObject:"), wrappedId);
id key = processAcceleratorKey(acceleratorkey);
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuItemCallback:"), key);
msg(item, s("setEnabled:"), !disabled);
msg(item, s("autorelease"));
msg(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
msg(parentmenu, s("addItem:"), item);
return item;
}
id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *key) {
id item = ALLOC("NSMenuItem");
// Store the item in the menu item map
hashmap_put(&menu->menuItemMap, (char*)menuid, strlen(menuid), item);
// Create a MenuItemCallbackData
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, item, menuid, Checkbox);
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), callback);
msg(item, s("setRepresentedObject:"), wrappedId);
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuItemCallback:"), str(key));
msg(item, s("setEnabled:"), !disabled);
msg(item, s("autorelease"));
msg(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
msg(parentmenu, s("addItem:"), item);
return item;
}
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers) {
id item = ALLOC("NSMenuItem");
// Create a MenuItemCallbackData
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, item, menuid, Text);
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), callback);
msg(item, s("setRepresentedObject:"), wrappedId);
id key = processAcceleratorKey(acceleratorkey);
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title),
s("menuItemCallback:"), key);
msg(item, s("setEnabled:"), !disabled);
msg(item, s("autorelease"));
// Process modifiers
if( modifiers != NULL ) {
unsigned long modifierFlags = parseModifiers(modifiers);
msg(item, s("setKeyEquivalentModifierMask:"), modifierFlags);
}
msg(parentMenu, s("addItem:"), item);
return item;
}
void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
// Check if this item is hidden and if so, exit early!
bool hidden = false;
getJSONBool(item, "Hidden", &hidden);
if( hidden ) {
return;
}
// Get the role
JsonNode *role = json_find_member(item, "Role");
if( role != NULL ) {
processMenuRole(menu, parentMenu, role);
return;
}
// Check if this is a submenu
JsonNode *submenu = json_find_member(item, "SubMenu");
if( submenu != NULL ) {
// Get the label
JsonNode *menuNameNode = json_find_member(item, "Label");
const char *name = "";
if ( menuNameNode != NULL) {
name = menuNameNode->string_;
}
id thisMenuItem = createMenuItemNoAutorelease(str(name), NULL, "");
id thisMenu = createMenu(str(name));
msg(thisMenuItem, s("setSubmenu:"), thisMenu);
msg(parentMenu, s("addItem:"), thisMenuItem);
JsonNode *submenuItems = json_find_member(submenu, "Items");
// If we have no items, just return
if ( submenuItems == NULL ) {
return;
}
// Loop over submenu items
JsonNode *item;
json_foreach(item, submenuItems) {
// Get item label
processMenuItem(menu, thisMenu, item);
}
return;
}
// This is a user menu. Get the common data
// Get the label
const char *label = getJSONString(item, "Label");
if ( label == NULL) {
label = "(empty)";
}
const char *menuid = getJSONString(item, "ID");
if ( menuid == NULL) {
menuid = "";
}
bool disabled = false;
getJSONBool(item, "Disabled", &disabled);
// Get the Accelerator
JsonNode *accelerator = json_find_member(item, "Accelerator");
const char *acceleratorkey = NULL;
const char **modifiers = NULL;
// If we have an accelerator
if( accelerator != NULL ) {
// Get the key
acceleratorkey = getJSONString(accelerator, "Key");
// Check if there are modifiers
JsonNode *modifiersList = json_find_member(accelerator, "Modifiers");
if ( modifiersList != NULL ) {
// Allocate an array of strings
int noOfModifiers = json_array_length(modifiersList);
// Do we have any?
if (noOfModifiers > 0) {
modifiers = malloc(sizeof(const char *) * (noOfModifiers + 1));
JsonNode *modifier;
int count = 0;
// Iterate the modifiers and save a reference to them in our new array
json_foreach(modifier, modifiersList) {
// Get modifier name
modifiers[count] = modifier->string_;
count++;
}
// Null terminate the modifier list
modifiers[count] = NULL;
}
}
}
// Get the Type
JsonNode *type = json_find_member(item, "Type");
if( type != NULL ) {
if( STREQ(type->string_, "Text")) {
processTextMenuItem(menu, parentMenu, label, menuid, disabled, acceleratorkey, modifiers);
}
else if ( STREQ(type->string_, "Separator")) {
addSeparator(parentMenu);
}
else if ( STREQ(type->string_, "Checkbox")) {
// Get checked state
bool checked = false;
getJSONBool(item, "Checked", &checked);
processCheckboxMenuItem(menu, parentMenu, label, menuid, disabled, checked, "");
}
else if ( STREQ(type->string_, "Radio")) {
// Get checked state
bool checked = false;
getJSONBool(item, "Checked", &checked);
processRadioMenuItem(menu, parentMenu, label, menuid, disabled, checked, "");
}
}
if ( modifiers != NULL ) {
free(modifiers);
}
return;
}
void processMenuData(Menu *menu, JsonNode *menuData) {
JsonNode *items = json_find_member(menuData, "Items");
if( items == NULL ) {
// Parse error!
ABORT("Unable to find 'Items' in menu JSON!");
}
// Iterate items
JsonNode *item;
json_foreach(item, items) {
// Process each menu item
processMenuItem(menu, menu->menu, item);
}
}
void processRadioGroupJSON(Menu *menu, JsonNode *radioGroup) {
int groupLength;
getJSONInt(radioGroup, "Length", &groupLength);
JsonNode *members = json_find_member(radioGroup, "Members");
JsonNode *member;
// Allocate array
size_t arrayLength = sizeof(id)*(groupLength+1);
id memberList[arrayLength];
// Build the radio group items
int count=0;
json_foreach(member, members) {
// Get menu by id
id menuItem = (id)hashmap_get(&menu->menuItemMap, (char*)member->string_, strlen(member->string_));
// Save Member
memberList[count] = menuItem;
count = count + 1;
}
// Null terminate array
memberList[groupLength] = 0;
// Store the members
json_foreach(member, members) {
// Copy the memberList
char *newMemberList = (char *)malloc(arrayLength);
memcpy(newMemberList, memberList, arrayLength);
// add group to each member of group
hashmap_put(&menu->radioGroupMap, member->string_, strlen(member->string_), newMemberList);
}
}
id GetMenu(Menu *menu) {
// Pull out the menu data
JsonNode *menuData = json_find_member(menu->processedMenu, "Menu");
if( menuData == NULL ) {
ABORT("Unable to find Menu data: %s", menu->processedMenu);
}
menu->menu = createMenu(str(""));
// Process the menu data
processMenuData(menu, menuData);
// Create the radiogroup cache
JsonNode *radioGroups = json_find_member(menu->processedMenu, "RadioGroups");
if( radioGroups == NULL ) {
// Parse error!
ABORT("Unable to find RadioGroups data: %s", menu->processedMenu);
}
// Iterate radio groups
JsonNode *radioGroup;
json_foreach(radioGroup, radioGroups) {
// Get item label
processRadioGroupJSON(menu, radioGroup);
}
return menu->menu;
}

View File

@@ -0,0 +1,100 @@
//
// Created by Lea Anthony on 6/1/21.
//
#ifndef MENU_DARWIN_H
#define MENU_DARWIN_H
#include "common.h"
#include "ffenestri_darwin.h"
enum MenuItemType {Text = 0, Checkbox = 1, Radio = 2};
enum MenuType {ApplicationMenuType = 0, ContextMenuType = 1, TrayMenuType = 2};
static const char *MenuTypeAsString[] = {
"ApplicationMenu", "ContextMenu", "TrayMenu",
};
extern void messageFromWindowCallback(const char *);
typedef struct {
const char *title;
/*** Internal ***/
// The decoded version of the Menu JSON
JsonNode *processedMenu;
struct hashmap_s menuItemMap;
struct hashmap_s radioGroupMap;
// Vector to keep track of callback data memory
vec_void_t callbackDataCache;
// The NSMenu for this menu
id menu;
// The parent data, eg ContextMenuStore or Tray
void *parentData;
// The commands for the menu callbacks
const char *callbackCommand;
// This indicates if we are an Application Menu, tray menu or context menu
enum MenuType menuType;
} Menu;
typedef struct {
id menuItem;
Menu *menu;
const char *menuID;
enum MenuItemType menuItemType;
} MenuItemCallbackData;
// NewMenu creates a new Menu struct, saving the given menu structure as JSON
Menu* NewMenu(JsonNode *menuData);
Menu* NewApplicationMenu(const char *menuAsJSON);
MenuItemCallbackData* CreateMenuItemCallbackData(Menu *menu, id menuItem, const char *menuID, enum MenuItemType menuItemType);
void DeleteMenu(Menu *menu);
// Creates a JSON message for the given menuItemID and data
const char* createMenuClickedMessage(const char *menuItemID, const char *data, enum MenuType menuType, const char *parentID);
// Callback for text menu items
void menuItemCallback(id self, SEL cmd, id sender);
id processAcceleratorKey(const char *key);
void addSeparator(id menu);
id createMenuItemNoAutorelease( id title, const char *action, const char *key);
id createMenuItem(id title, const char *action, const char *key);
id addMenuItem(id menu, const char *title, const char *action, const char *key, bool disabled);
id createMenu(id title);
void createDefaultAppMenu(id parentMenu);
void createDefaultEditMenu(id parentMenu);
void processMenuRole(Menu *menu, id parentMenu, JsonNode *item);
// This converts a string array of modifiers into the
// equivalent MacOS Modifier Flags
unsigned long parseModifiers(const char **modifiers);
id processRadioMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *acceleratorkey);
id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *key);
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers);
void processMenuItem(Menu *menu, id parentMenu, JsonNode *item);
void processMenuData(Menu *menu, JsonNode *menuData);
void processRadioGroupJSON(Menu *menu, JsonNode *radioGroup) ;
id GetMenu(Menu *menu);
#endif //ASSETS_C_MENU_DARWIN_H

View File

@@ -0,0 +1,199 @@
//
// Created by Lea Anthony on 12/1/21.
//
#include "common.h"
#include "traymenu_darwin.h"
#include "trayicons.h"
// A cache for all our tray menu icons
// Global because it's a singleton
struct hashmap_s trayIconCache;
TrayMenu* NewTrayMenu(const char* menuJSON) {
TrayMenu* result = malloc(sizeof(TrayMenu));
/*
{"ID":"0","Label":"Test Tray Label","Icon":"","ProcessedMenu":{"Menu":{"Items":[{"ID":"0","Label":"Show Window","Type":"Text","Disabled":false,"Hidden":false,"Checked":false,"Foreground":0,"Background":0},{"ID":"1","Label":"Hide Window","Type":"Text","Disabled":false,"Hidden":false,"Checked":false,"Foreground":0,"Background":0},{"ID":"2","Label":"Minimise Window","Type":"Text","Disabled":false,"Hidden":false,"Checked":false,"Foreground":0,"Background":0},{"ID":"3","Label":"Unminimise Window","Type":"Text","Disabled":false,"Hidden":false,"Checked":false,"Foreground":0,"Background":0}]},"RadioGroups":null}}
*/
JsonNode* processedJSON = json_decode(menuJSON);
if( processedJSON == NULL ) {
ABORT("[NewTrayMenu] Unable to parse TrayMenu JSON: %s", menuJSON);
}
// Save reference to this json
result->processedJSON = processedJSON;
// TODO: Make this configurable
result->trayIconPosition = NSImageLeft;
result->ID = mustJSONString(processedJSON, "ID");
result->label = mustJSONString(processedJSON, "Label");
result->icon = mustJSONString(processedJSON, "Icon");
JsonNode* processedMenu = mustJSONObject(processedJSON, "ProcessedMenu");
// Create the menu
result->menu = NewMenu(processedMenu);
// Init tray status bar item
result->statusbaritem = NULL;
// Set the menu type and store the tray ID in the parent data
result->menu->menuType = TrayMenuType;
result->menu->parentData = (void*) result->ID;
return result;
}
void DumpTrayMenu(TrayMenu* trayMenu) {
printf(" ['%s':%p] = { label: '%s', icon: '%s', menu: %p, statusbar: %p }\n", trayMenu->ID, trayMenu, trayMenu->label, trayMenu->icon, trayMenu->menu, trayMenu->statusbaritem );
}
void ShowTrayMenu(TrayMenu* trayMenu) {
// Create a status bar item if we don't have one
if( trayMenu->statusbaritem == NULL ) {
id statusBar = msg( c("NSStatusBar"), s("systemStatusBar") );
trayMenu->statusbaritem = msg(statusBar, s("statusItemWithLength:"), NSVariableStatusItemLength);
msg(trayMenu->statusbaritem, s("retain"));
}
id statusBarButton = msg(trayMenu->statusbaritem, s("button"));
msg(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition);
// Update the icon if needed
UpdateTrayMenuIcon(trayMenu);
// Update the label if needed
UpdateTrayMenuLabel(trayMenu);
// Update the menu
id menu = GetMenu(trayMenu->menu);
msg(trayMenu->statusbaritem, s("setMenu:"), menu);
}
void UpdateTrayMenuLabel(TrayMenu *trayMenu) {
// Exit early if NULL
if( trayMenu->label == NULL ) {
return;
}
// We don't check for a
id statusBarButton = msg(trayMenu->statusbaritem, s("button"));
msg(statusBarButton, s("setTitle:"), str(trayMenu->label));
}
void UpdateTrayMenuIcon(TrayMenu *trayMenu) {
// Exit early if NULL
if( trayMenu->icon == NULL ) {
return;
}
id statusBarButton = msg(trayMenu->statusbaritem, s("button"));
// Empty icon means remove it
if( STREMPTY(trayMenu->icon) ) {
// Remove image
msg(statusBarButton, s("setImage:"), NULL);
return;
}
id trayImage = hashmap_get(&trayIconCache, trayMenu->icon, strlen(trayMenu->icon));
msg(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition);
msg(statusBarButton, s("setImage:"), trayImage);
}
// UpdateTrayMenuInPlace receives 2 menus. The current menu gets
// updated with the data from the new menu.
void UpdateTrayMenuInPlace(TrayMenu* currentMenu, TrayMenu* newMenu) {
// Delete the old menu
DeleteMenu(currentMenu->menu);
// Set the new one
currentMenu->menu = newMenu->menu;
// Delete the old JSON
json_delete(currentMenu->processedJSON);
// Set the new JSON
currentMenu->processedJSON = newMenu->processedJSON;
// Copy the other data
currentMenu->ID = newMenu->ID;
currentMenu->label = newMenu->label;
currentMenu->trayIconPosition = newMenu->trayIconPosition;
currentMenu->icon = newMenu->icon;
}
void DeleteTrayMenu(TrayMenu* trayMenu) {
// printf("Freeing TrayMenu:\n");
// DumpTrayMenu(trayMenu);
// Delete the menu
DeleteMenu(trayMenu->menu);
// Free JSON
if (trayMenu->processedJSON != NULL ) {
json_delete(trayMenu->processedJSON);
}
// Free the status item
if ( trayMenu->statusbaritem != NULL ) {
id statusBar = msg( c("NSStatusBar"), s("systemStatusBar") );
msg(statusBar, s("removeStatusItem:"), trayMenu->statusbaritem);
msg(trayMenu->statusbaritem, s("release"));
trayMenu->statusbaritem = NULL;
}
// Free the tray menu memory
MEMFREE(trayMenu);
}
void LoadTrayIcons() {
// Allocate the Tray Icons
if( 0 != hashmap_create((const unsigned)4, &trayIconCache)) {
// Couldn't allocate map
ABORT("Not enough memory to allocate trayIconCache!");
}
unsigned int count = 0;
while( 1 ) {
const unsigned char *name = trayIcons[count++];
if( name == 0x00 ) {
break;
}
const unsigned char *lengthAsString = trayIcons[count++];
if( name == 0x00 ) {
break;
}
const unsigned char *data = trayIcons[count++];
if( data == 0x00 ) {
break;
}
int length = atoi((const char *)lengthAsString);
// Create the icon and add to the hashmap
id imageData = msg(c("NSData"), s("dataWithBytes:length:"), data, length);
id trayImage = ALLOC("NSImage");
msg(trayImage, s("initWithData:"), imageData);
hashmap_put(&trayIconCache, (const char *)name, strlen((const char *)name), trayImage);
}
}
void UnloadTrayIcons() {
// Release the tray cache images
if( hashmap_num_entries(&trayIconCache) > 0 ) {
if (0!=hashmap_iterate_pairs(&trayIconCache, releaseNSObject, NULL)) {
ABORT("failed to release hashmap entries!");
}
}
//Free radio groups hashmap
hashmap_destroy(&trayIconCache);
}

View File

@@ -0,0 +1,38 @@
//
// Created by Lea Anthony on 12/1/21.
//
#ifndef TRAYMENU_DARWIN_H
#define TRAYMENU_DARWIN_H
#include "common.h"
#include "menu_darwin.h"
typedef struct {
const char *label;
const char *icon;
const char *ID;
Menu* menu;
id statusbaritem;
int trayIconPosition;
JsonNode* processedJSON;
} TrayMenu;
TrayMenu* NewTrayMenu(const char *trayJSON);
void DumpTrayMenu(TrayMenu* trayMenu);
void ShowTrayMenu(TrayMenu* trayMenu);
void UpdateTrayMenuInPlace(TrayMenu* currentMenu, TrayMenu* newMenu);
void UpdateTrayMenuIcon(TrayMenu *trayMenu);
void UpdateTrayMenuLabel(TrayMenu *trayMenu);
void LoadTrayIcons();
void UnloadTrayIcons();
void DeleteTrayMenu(TrayMenu* trayMenu);
#endif //TRAYMENU_DARWIN_H

View File

@@ -0,0 +1,107 @@
//
// Created by Lea Anthony on 12/1/21.
//
#include "common.h"
#include "traymenustore_darwin.h"
#include "traymenu_darwin.h"
#include <stdlib.h>
TrayMenuStore* NewTrayMenuStore() {
TrayMenuStore* result = malloc(sizeof(TrayMenuStore));
// Allocate Tray Menu Store
if( 0 != hashmap_create((const unsigned)4, &result->trayMenuMap)) {
ABORT("[NewTrayMenuStore] Not enough memory to allocate trayMenuMap!");
}
return result;
}
int dumpTrayMenu(void *const context, struct hashmap_element_s *const e) {
DumpTrayMenu(e->data);
return 0;
}
void DumpTrayMenuStore(TrayMenuStore* store) {
hashmap_iterate_pairs(&store->trayMenuMap, dumpTrayMenu, NULL);
}
void AddTrayMenuToStore(TrayMenuStore* store, const char* menuJSON) {
TrayMenu* newMenu = NewTrayMenu(menuJSON);
//TODO: check if there is already an entry for this menu
hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
}
int showTrayMenu(void *const context, struct hashmap_element_s *const e) {
ShowTrayMenu(e->data);
// 0 to retain element, -1 to delete.
return 0;
}
void ShowTrayMenusInStore(TrayMenuStore* store) {
if( hashmap_num_entries(&store->trayMenuMap) > 0 ) {
hashmap_iterate_pairs(&store->trayMenuMap, showTrayMenu, NULL);
}
}
int freeTrayMenu(void *const context, struct hashmap_element_s *const e) {
DeleteTrayMenu(e->data);
return -1;
}
void DeleteTrayMenuStore(TrayMenuStore *store) {
// Delete context menus
if (hashmap_num_entries(&store->trayMenuMap) > 0) {
if (0 != hashmap_iterate_pairs(&store->trayMenuMap, freeTrayMenu, NULL)) {
ABORT("[DeleteContextMenuStore] Failed to release contextMenuStore entries!");
}
}
// Destroy tray menu map
hashmap_destroy(&store->trayMenuMap);
}
TrayMenu* GetTrayMenuFromStore(TrayMenuStore* store, const char* menuID) {
// Get the current menu
return hashmap_get(&store->trayMenuMap, menuID, strlen(menuID));
}
void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON) {
TrayMenu* newMenu = NewTrayMenu(menuJSON);
// Get the current menu
TrayMenu *currentMenu = GetTrayMenuFromStore(store, newMenu->ID);
if ( currentMenu == NULL ) {
ABORT("Attempted to update unknown tray menu with ID '%s'.", newMenu->ID);
}
// Save the status bar reference
newMenu->statusbaritem = currentMenu->statusbaritem;
hashmap_remove(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID));
// Delete the current menu
DeleteMenu(currentMenu->menu);
currentMenu->menu = NULL;
// Free JSON
if (currentMenu->processedJSON != NULL ) {
json_delete(currentMenu->processedJSON);
currentMenu->processedJSON = NULL;
}
// Free the tray menu memory
MEMFREE(currentMenu);
hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
// Show the updated menu
ShowTrayMenu(newMenu);
}

View File

@@ -0,0 +1,25 @@
//
// Created by Lea Anthony on 7/1/21.
//
#ifndef TRAYMENUSTORE_DARWIN_H
#define TRAYMENUSTORE_DARWIN_H
typedef struct {
int dummy;
// This is our tray menu map
// It maps tray IDs to TrayMenu*
struct hashmap_s trayMenuMap;
} TrayMenuStore;
TrayMenuStore* NewTrayMenuStore();
void AddTrayMenuToStore(TrayMenuStore* store, const char* menuJSON);
void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON);
void ShowTrayMenusInStore(TrayMenuStore* store);
void DeleteTrayMenuStore(TrayMenuStore* store);
#endif //TRAYMENUSTORE_DARWIN_H

View File

@@ -0,0 +1,46 @@
package menumanager
import "github.com/wailsapp/wails/v2/pkg/menu"
func (m *Manager) SetApplicationMenu(applicationMenu *menu.Menu) error {
if applicationMenu == nil {
return nil
}
m.applicationMenu = applicationMenu
// Reset the menu map
m.applicationMenuItemMap = NewMenuItemMap()
// Add the menu to the menu map
m.applicationMenuItemMap.AddMenu(applicationMenu)
return m.processApplicationMenu()
}
func (m *Manager) GetApplicationMenuJSON() string {
return m.applicationMenuJSON
}
// UpdateApplicationMenu reprocesses the application menu to pick up structure
// changes etc
// Returns the JSON representation of the updated menu
func (m *Manager) UpdateApplicationMenu() (string, error) {
m.applicationMenuItemMap = NewMenuItemMap()
m.applicationMenuItemMap.AddMenu(m.applicationMenu)
err := m.processApplicationMenu()
return m.applicationMenuJSON, err
}
func (m *Manager) processApplicationMenu() error {
// Process the menu
processedApplicationMenu := NewWailsMenu(m.applicationMenuItemMap, m.applicationMenu)
applicationMenuJSON, err := processedApplicationMenu.AsJSON()
if err != nil {
return err
}
m.applicationMenuJSON = applicationMenuJSON
return nil
}

View File

@@ -0,0 +1,60 @@
package menumanager
import (
"encoding/json"
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu"
)
type ContextMenu struct {
ID string
ProcessedMenu *WailsMenu
menuItemMap *MenuItemMap
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()
}

View File

@@ -0,0 +1,72 @@
package menumanager
import (
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu"
"sync"
)
// MenuItemMap holds a mapping between menuIDs and menu items
type MenuItemMap struct {
idToMenuItemMap map[string]*menu.MenuItem
menuItemToIDMap map[*menu.MenuItem]string
// We use a simple counter to keep track of unique menu IDs
menuIDCounter int64
menuIDCounterMutex sync.Mutex
}
func NewMenuItemMap() *MenuItemMap {
result := &MenuItemMap{
idToMenuItemMap: make(map[string]*menu.MenuItem),
menuItemToIDMap: make(map[*menu.MenuItem]string),
}
return result
}
func (m *MenuItemMap) AddMenu(menu *menu.Menu) {
for _, item := range menu.Items {
m.processMenuItem(item)
}
}
func (m *MenuItemMap) Dump() {
println("idToMenuItemMap:")
for key, value := range m.idToMenuItemMap {
fmt.Printf(" %s\t%p\n", key, value)
}
println("\nmenuItemToIDMap")
for key, value := range m.menuItemToIDMap {
fmt.Printf(" %p\t%s\n", key, value)
}
}
// GenerateMenuID returns a unique string ID for a menu item
func (m *MenuItemMap) generateMenuID() string {
m.menuIDCounterMutex.Lock()
result := fmt.Sprintf("%d", m.menuIDCounter)
m.menuIDCounter++
m.menuIDCounterMutex.Unlock()
return result
}
func (m *MenuItemMap) processMenuItem(item *menu.MenuItem) {
if item.SubMenu != nil {
for _, submenuitem := range item.SubMenu.Items {
m.processMenuItem(submenuitem)
}
}
// Create a unique ID for this menu item
menuID := m.generateMenuID()
// Store references
m.idToMenuItemMap[menuID] = item
m.menuItemToIDMap[item] = menuID
}
func (m *MenuItemMap) getMenuItemByID(menuId string) *menu.MenuItem {
return m.idToMenuItemMap[menuId]
}

View File

@@ -0,0 +1,90 @@
package menumanager
import (
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu"
)
type Manager struct {
// The application menu.
applicationMenu *menu.Menu
applicationMenuJSON string
// Our application menu mappings
applicationMenuItemMap *MenuItemMap
// Context menus
contextMenus map[string]*ContextMenu
contextMenuPointers map[*menu.ContextMenu]string
// Tray menu stores
trayMenus map[string]*TrayMenu
trayMenuPointers map[*menu.TrayMenu]string
}
func NewManager() *Manager {
return &Manager{
applicationMenuItemMap: NewMenuItemMap(),
contextMenus: make(map[string]*ContextMenu),
contextMenuPointers: make(map[*menu.ContextMenu]string),
trayMenus: make(map[string]*TrayMenu),
trayMenuPointers: make(map[*menu.TrayMenu]string),
}
}
func (m *Manager) getMenuItemByID(menuMap *MenuItemMap, menuId string) *menu.MenuItem {
return menuMap.idToMenuItemMap[menuId]
}
func (m *Manager) ProcessClick(menuID string, data string, menuType string, parentID string) error {
var menuItemMap *MenuItemMap
switch menuType {
case "ApplicationMenu":
menuItemMap = m.applicationMenuItemMap
case "ContextMenu":
contextMenu := m.contextMenus[parentID]
if contextMenu == nil {
return fmt.Errorf("unknown context menu: %s", parentID)
}
menuItemMap = contextMenu.menuItemMap
case "TrayMenu":
trayMenu := m.trayMenus[parentID]
if trayMenu == nil {
return fmt.Errorf("unknown tray menu: %s", parentID)
}
menuItemMap = trayMenu.menuItemMap
default:
return fmt.Errorf("unknown menutype: %s", menuType)
}
// Get the menu item
menuItem := menuItemMap.getMenuItemByID(menuID)
if menuItem == nil {
return fmt.Errorf("Cannot process menuid %s - unknown", menuID)
}
// Is the menu item a checkbox?
if menuItem.Type == menu.CheckboxType {
// Toggle state
menuItem.Checked = !menuItem.Checked
}
if menuItem.Click == nil {
// No callback
return fmt.Errorf("No callback for menu '%s'", menuItem.Label)
}
// Create new Callback struct
callbackData := &menu.CallbackData{
MenuItem: menuItem,
ContextData: data,
}
// Call back!
go menuItem.Click(callbackData)
return nil
}

View File

@@ -0,0 +1,156 @@
package menumanager
import (
"encoding/json"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/menu/keys"
)
type ProcessedMenuItem struct {
ID string
// Label is what appears as the menu text
Label string
// Role is a predefined menu type
Role menu.Role `json:"Role,omitempty"`
// Accelerator holds a representation of a key binding
Accelerator *keys.Accelerator `json:"Accelerator,omitempty"`
// Type of MenuItem, EG: Checkbox, Text, Separator, Radio, Submenu
Type menu.Type
// Disabled makes the item unselectable
Disabled bool
// Hidden ensures that the item is not shown in the menu
Hidden bool
// Checked indicates if the item is selected (used by Checkbox and Radio types only)
Checked bool
// Submenu contains a list of menu items that will be shown as a submenu
//SubMenu []*MenuItem `json:"SubMenu,omitempty"`
SubMenu *ProcessedMenu `json:"SubMenu,omitempty"`
// Foreground colour in hex RGBA format EG: 0xFF0000FF = #FF0000FF = red
Foreground int
// Background colour
Background int
}
func NewProcessedMenuItem(menuItemMap *MenuItemMap, menuItem *menu.MenuItem) *ProcessedMenuItem {
ID := menuItemMap.menuItemToIDMap[menuItem]
result := &ProcessedMenuItem{
ID: ID,
Label: menuItem.Label,
Role: menuItem.Role,
Accelerator: menuItem.Accelerator,
Type: menuItem.Type,
Disabled: menuItem.Disabled,
Hidden: menuItem.Hidden,
Checked: menuItem.Checked,
Foreground: menuItem.Foreground,
Background: menuItem.Background,
}
if menuItem.SubMenu != nil {
result.SubMenu = NewProcessedMenu(menuItemMap, menuItem.SubMenu)
}
return result
}
type ProcessedMenu struct {
Items []*ProcessedMenuItem
}
func NewProcessedMenu(menuItemMap *MenuItemMap, menu *menu.Menu) *ProcessedMenu {
result := &ProcessedMenu{}
for _, item := range menu.Items {
processedMenuItem := NewProcessedMenuItem(menuItemMap, item)
result.Items = append(result.Items, processedMenuItem)
}
return result
}
// WailsMenu is the original menu with the addition
// of radio groups extracted from the menu data
type WailsMenu struct {
Menu *ProcessedMenu
RadioGroups []*RadioGroup
currentRadioGroup []string
}
// RadioGroup holds all the members of the same radio group
type RadioGroup struct {
Members []string
Length int
}
func NewWailsMenu(menuItemMap *MenuItemMap, menu *menu.Menu) *WailsMenu {
result := &WailsMenu{}
// Process the menus
result.Menu = NewProcessedMenu(menuItemMap, menu)
// Process the radio groups
result.processRadioGroups()
return result
}
func (w *WailsMenu) AsJSON() (string, error) {
menuAsJSON, err := json.Marshal(w)
if err != nil {
return "", err
}
return string(menuAsJSON), nil
}
func (w *WailsMenu) processRadioGroups() {
// Loop over top level menus
for _, item := range w.Menu.Items {
// Process MenuItem
w.processMenuItem(item)
}
w.finaliseRadioGroup()
}
func (w *WailsMenu) processMenuItem(item *ProcessedMenuItem) {
switch item.Type {
// We need to recurse submenus
case menu.SubmenuType:
// Finalise any current radio groups as they don't trickle down to submenus
w.finaliseRadioGroup()
// Process each submenu item
for _, subitem := range item.SubMenu.Items {
w.processMenuItem(subitem)
}
case menu.RadioType:
// Add the item to the radio group
w.currentRadioGroup = append(w.currentRadioGroup, item.ID)
default:
w.finaliseRadioGroup()
}
}
func (w *WailsMenu) finaliseRadioGroup() {
// If we were processing a radio group, fix up the references
if len(w.currentRadioGroup) > 0 {
// Create new radiogroup
group := &RadioGroup{
Members: w.currentRadioGroup,
Length: len(w.currentRadioGroup),
}
w.RadioGroups = append(w.RadioGroups, group)
// Empty the radio group
w.currentRadioGroup = []string{}
}
}

View File

@@ -0,0 +1,105 @@
package menumanager
import (
"encoding/json"
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu"
"sync"
)
var trayMenuID int
var trayMenuIDMutex sync.Mutex
func generateTrayID() string {
trayMenuIDMutex.Lock()
result := fmt.Sprintf("%d", trayMenuID)
trayMenuID++
trayMenuIDMutex.Unlock()
return result
}
type TrayMenu struct {
ID string
Label string
Icon string
menuItemMap *MenuItemMap
menu *menu.Menu
ProcessedMenu *WailsMenu
}
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 {
result := &TrayMenu{
Label: trayMenu.Label,
Icon: trayMenu.Icon,
menu: trayMenu.Menu,
menuItemMap: NewMenuItemMap(),
}
result.menuItemMap.AddMenu(trayMenu.Menu)
result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu)
return result
}
func (m *Manager) AddTrayMenu(trayMenu *menu.TrayMenu) {
newTrayMenu := NewTrayMenu(trayMenu)
// Hook up a new ID
trayID := generateTrayID()
newTrayMenu.ID = trayID
// Save the references
m.trayMenus[trayID] = newTrayMenu
m.trayMenuPointers[trayMenu] = trayID
}
func (m *Manager) UpdateTrayMenu(trayMenu *menu.TrayMenu) (string, error) {
trayID, trayMenuKnown := m.trayMenuPointers[trayMenu]
if !trayMenuKnown {
return "", fmt.Errorf("unknown Tray Menu '%s'. Please add the tray menu using AddTrayMenu()", trayMenu.Label)
}
// 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) 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
}

View File

@@ -2,8 +2,6 @@ package messagedispatcher
import (
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
"github.com/wailsapp/wails/v2/internal/servicebus"
@@ -32,11 +30,9 @@ type Client interface {
WindowUnFullscreen()
WindowSetColour(colour int)
DarkModeEnabled(callbackID string)
UpdateMenu(menu *menu.Menu)
UpdateTray(menu *menu.Menu)
UpdateTrayLabel(label string)
UpdateTrayIcon(name string)
UpdateContextMenus(contextMenus *menu.ContextMenus)
SetApplicationMenu(menuJSON string)
UpdateTrayMenu(trayMenuJSON string)
UpdateContextMenu(contextMenuJSON string)
}
// DispatchClient is what the frontends use to interface with the

View File

@@ -26,6 +26,10 @@ var messageParsers = map[byte]func(string) (*parsedMessage, error){
// Parse will attempt to parse the given message
func Parse(message string) (*parsedMessage, error) {
if len(message) == 0 {
return nil, fmt.Errorf("MessageParser received blank message");
}
parseMethod := messageParsers[message[0]]
if parseMethod == nil {
return nil, fmt.Errorf("message type '%c' invalid", message[0])

View File

@@ -2,7 +2,6 @@ package messagedispatcher
import (
"encoding/json"
"github.com/wailsapp/wails/v2/pkg/menu"
"strconv"
"strings"
"sync"
@@ -17,16 +16,14 @@ import (
// Dispatcher translates messages received from the frontend
// and publishes them onto the service bus
type Dispatcher struct {
quitChannel <-chan *servicebus.Message
resultChannel <-chan *servicebus.Message
eventChannel <-chan *servicebus.Message
windowChannel <-chan *servicebus.Message
dialogChannel <-chan *servicebus.Message
systemChannel <-chan *servicebus.Message
menuChannel <-chan *servicebus.Message
contextMenuChannel <-chan *servicebus.Message
trayChannel <-chan *servicebus.Message
running bool
quitChannel <-chan *servicebus.Message
resultChannel <-chan *servicebus.Message
eventChannel <-chan *servicebus.Message
windowChannel <-chan *servicebus.Message
dialogChannel <-chan *servicebus.Message
systemChannel <-chan *servicebus.Message
menuChannel <-chan *servicebus.Message
running bool
servicebus *servicebus.ServiceBus
logger logger.CustomLogger
@@ -78,29 +75,17 @@ func New(servicebus *servicebus.ServiceBus, logger *logger.Logger) (*Dispatcher,
return nil, err
}
contextMenuChannel, err := servicebus.Subscribe("contextmenufrontend:")
if err != nil {
return nil, err
}
trayChannel, err := servicebus.Subscribe("trayfrontend:")
if err != nil {
return nil, err
}
result := &Dispatcher{
servicebus: servicebus,
eventChannel: eventChannel,
logger: logger.CustomLogger("Message Dispatcher"),
clients: make(map[string]*DispatchClient),
resultChannel: resultChannel,
quitChannel: quitChannel,
windowChannel: windowChannel,
dialogChannel: dialogChannel,
systemChannel: systemChannel,
menuChannel: menuChannel,
trayChannel: trayChannel,
contextMenuChannel: contextMenuChannel,
servicebus: servicebus,
eventChannel: eventChannel,
logger: logger.CustomLogger("Message Dispatcher"),
clients: make(map[string]*DispatchClient),
resultChannel: resultChannel,
quitChannel: quitChannel,
windowChannel: windowChannel,
dialogChannel: dialogChannel,
systemChannel: systemChannel,
menuChannel: menuChannel,
}
return result, nil
@@ -132,10 +117,6 @@ func (d *Dispatcher) Start() error {
d.processSystemMessage(systemMessage)
case menuMessage := <-d.menuChannel:
d.processMenuMessage(menuMessage)
case contextMenuMessage := <-d.contextMenuChannel:
d.processContextMenuMessage(contextMenuMessage)
case trayMessage := <-d.trayChannel:
d.processTrayMessage(trayMessage)
}
}
@@ -449,11 +430,11 @@ func (d *Dispatcher) processMenuMessage(result *servicebus.Message) {
command := splitTopic[1]
switch command {
case "update":
case "updateappmenu":
updatedMenu, ok := result.Data().(*menu.Menu)
updatedMenu, ok := result.Data().(string)
if !ok {
d.logger.Error("Invalid data for 'menufrontend:update' : %#v",
d.logger.Error("Invalid data for 'menufrontend:updateappmenu' : %#v",
result.Data())
return
}
@@ -461,94 +442,34 @@ func (d *Dispatcher) processMenuMessage(result *servicebus.Message) {
// TODO: Work out what we mean in a multi window environment...
// For now we will just pick the first one
for _, client := range d.clients {
client.frontend.UpdateMenu(updatedMenu)
}
default:
d.logger.Error("Unknown menufrontend command: %s", command)
}
}
func (d *Dispatcher) processContextMenuMessage(result *servicebus.Message) {
splitTopic := strings.Split(result.Topic(), ":")
if len(splitTopic) < 2 {
d.logger.Error("Invalid contextmenu message : %#v", result.Data())
return
}
command := splitTopic[1]
switch command {
case "update":
updatedContextMenus, ok := result.Data().(*menu.ContextMenus)
if !ok {
d.logger.Error("Invalid data for 'contextmenufrontend:update' : %#v",
result.Data())
return
}
// TODO: Work out what we mean in a multi window environment...
// For now we will just pick the first one
for _, client := range d.clients {
client.frontend.UpdateContextMenus(updatedContextMenus)
}
default:
d.logger.Error("Unknown contextmenufrontend command: %s", command)
}
}
func (d *Dispatcher) processTrayMessage(result *servicebus.Message) {
splitTopic := strings.Split(result.Topic(), ":")
if len(splitTopic) < 2 {
d.logger.Error("Invalid tray message : %#v", result.Data())
return
}
command := splitTopic[1]
switch command {
case "update":
updatedMenu, ok := result.Data().(*menu.Menu)
if !ok {
d.logger.Error("Invalid data for 'trayfrontend:update' : %#v",
result.Data())
return
}
// TODO: Work out what we mean in a multi window environment...
// For now we will just pick the first one
for _, client := range d.clients {
client.frontend.UpdateTray(updatedMenu)
}
case "setlabel":
updatedLabel, ok := result.Data().(string)
if !ok {
d.logger.Error("Invalid data for 'trayfrontend:setlabel' : %#v",
result.Data())
return
}
// TODO: Work out what we mean in a multi window environment...
// For now we will just pick the first one
for _, client := range d.clients {
client.frontend.UpdateTrayLabel(updatedLabel)
}
case "seticon":
iconname, ok := result.Data().(string)
if !ok {
d.logger.Error("Invalid data for 'trayfrontend:seticon' : %#v",
result.Data())
return
}
// TODO: Work out what we mean in a multi window environment...
// For now we will just pick the first one
for _, client := range d.clients {
client.frontend.UpdateTrayIcon(iconname)
client.frontend.SetApplicationMenu(updatedMenu)
}
case "updatetraymenu":
updatedTrayMenu, ok := result.Data().(string)
if !ok {
d.logger.Error("Invalid data for 'menufrontend:updatetraymenu' : %#v",
result.Data())
return
}
// TODO: Work out what we mean in a multi window environment...
// For now we will just pick the first one
for _, client := range d.clients {
client.frontend.UpdateTrayMenu(updatedTrayMenu)
}
case "updatecontextmenu":
updatedContextMenu, ok := result.Data().(string)
if !ok {
d.logger.Error("Invalid data for 'menufrontend:updatecontextmenu' : %#v",
result.Data())
return
}
// TODO: Work out what we mean in a multi window environment...
// For now we will just pick the first one
for _, client := range d.clients {
client.frontend.UpdateContextMenu(updatedContextMenu)
}
default:

View File

@@ -1,48 +0,0 @@
package runtime
import (
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
"github.com/wailsapp/wails/v2/internal/servicebus"
"github.com/wailsapp/wails/v2/pkg/menu"
)
// ContextMenus defines all ContextMenu related operations
type ContextMenus interface {
On(menuID string, callback func(*menu.MenuItem, string))
Update()
GetByID(menuID string) *menu.MenuItem
RemoveByID(id string) bool
}
type contextMenus struct {
bus *servicebus.ServiceBus
contextmenus *menu.ContextMenus
}
// newContextMenus creates a new ContextMenu struct
func newContextMenus(bus *servicebus.ServiceBus, contextmenus *menu.ContextMenus) ContextMenus {
return &contextMenus{
bus: bus,
contextmenus: contextmenus,
}
}
// On registers a listener for a particular event
func (t *contextMenus) On(menuID string, callback func(*menu.MenuItem, string)) {
t.bus.Publish("contextmenus:on", &message.ContextMenusOnMessage{
MenuID: menuID,
Callback: callback,
})
}
func (t *contextMenus) Update() {
t.bus.Publish("contextmenus:update", t.contextmenus)
}
func (t *contextMenus) GetByID(menuItemID string) *menu.MenuItem {
return t.contextmenus.GetByID(menuItemID)
}
func (t *contextMenus) RemoveByID(menuItemID string) bool {
return t.contextmenus.RemoveByID(menuItemID)
}

View File

@@ -1,48 +1,36 @@
package runtime
import (
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
"github.com/wailsapp/wails/v2/internal/servicebus"
"github.com/wailsapp/wails/v2/pkg/menu"
)
// Menu defines all Menu related operations
type Menu interface {
On(menuID string, callback func(*menu.MenuItem))
Update()
GetByID(menuID string) *menu.MenuItem
RemoveByID(id string) bool
UpdateApplicationMenu()
UpdateContextMenu(contextMenu *menu.ContextMenu)
UpdateTrayMenu(trayMenu *menu.TrayMenu)
}
type menuRuntime struct {
bus *servicebus.ServiceBus
menu *menu.Menu
bus *servicebus.ServiceBus
}
// newMenu creates a new Menu struct
func newMenu(bus *servicebus.ServiceBus, menu *menu.Menu) Menu {
func newMenu(bus *servicebus.ServiceBus) Menu {
return &menuRuntime{
bus: bus,
menu: menu,
bus: bus,
}
}
// On registers a listener for a particular event
func (m *menuRuntime) On(menuID string, callback func(*menu.MenuItem)) {
m.bus.Publish("menu:on", &message.MenuOnMessage{
MenuID: menuID,
Callback: callback,
})
func (m *menuRuntime) UpdateApplicationMenu() {
m.bus.Publish("menu:updateappmenu", nil)
}
func (m *menuRuntime) Update() {
m.bus.Publish("menu:update", m.menu)
func (m *menuRuntime) UpdateContextMenu(contextMenu *menu.ContextMenu) {
m.bus.Publish("menu:updatecontextmenu", contextMenu)
}
func (m *menuRuntime) GetByID(menuID string) *menu.MenuItem {
return m.menu.GetByID(menuID)
}
func (m *menuRuntime) RemoveByID(id string) bool {
return m.menu.RemoveByID(id)
func (m *menuRuntime) UpdateTrayMenu(trayMenu *menu.TrayMenu) {
m.bus.Publish("menu:updatetraymenu", trayMenu)
}

View File

@@ -2,37 +2,32 @@ package runtime
import (
"github.com/wailsapp/wails/v2/internal/servicebus"
"github.com/wailsapp/wails/v2/pkg/menu"
)
// Runtime is a means for the user to interact with the application at runtime
type Runtime struct {
Browser Browser
Events Events
Window Window
Dialog Dialog
System System
Menu Menu
ContextMenu ContextMenus
Tray Tray
Store *StoreProvider
Log Log
bus *servicebus.ServiceBus
Browser Browser
Events Events
Window Window
Dialog Dialog
System System
Menu Menu
Store *StoreProvider
Log Log
bus *servicebus.ServiceBus
}
// New creates a new runtime
func New(serviceBus *servicebus.ServiceBus, menu *menu.Menu, trayMenu *menu.Tray, contextMenus *menu.ContextMenus) *Runtime {
func New(serviceBus *servicebus.ServiceBus) *Runtime {
result := &Runtime{
Browser: newBrowser(),
Events: newEvents(serviceBus),
Window: newWindow(serviceBus),
Dialog: newDialog(serviceBus),
System: newSystem(serviceBus),
Menu: newMenu(serviceBus, menu),
Tray: newTray(serviceBus, trayMenu),
ContextMenu: newContextMenus(serviceBus, contextMenus),
Log: newLog(serviceBus),
bus: serviceBus,
Browser: newBrowser(),
Events: newEvents(serviceBus),
Window: newWindow(serviceBus),
Dialog: newDialog(serviceBus),
System: newSystem(serviceBus),
Menu: newMenu(serviceBus),
Log: newLog(serviceBus),
bus: serviceBus,
}
result.Store = newStore(result)
return result

View File

@@ -1,67 +0,0 @@
package runtime
import (
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
"github.com/wailsapp/wails/v2/internal/servicebus"
"github.com/wailsapp/wails/v2/pkg/menu"
)
// Tray defines all Tray related operations
type Tray interface {
NewTray(id string) *menu.Tray
On(menuID string, callback func(*menu.MenuItem))
Update(tray ...*menu.Tray)
GetByID(menuID string) *menu.MenuItem
RemoveByID(id string) bool
SetLabel(label string)
SetIcon(name string)
}
type trayRuntime struct {
bus *servicebus.ServiceBus
trayMenu *menu.Tray
}
// newTray creates a new Menu struct
func newTray(bus *servicebus.ServiceBus, menu *menu.Tray) Tray {
return &trayRuntime{
bus: bus,
trayMenu: menu,
}
}
// On registers a listener for a particular event
func (t *trayRuntime) On(menuID string, callback func(*menu.MenuItem)) {
t.bus.Publish("tray:on", &message.TrayOnMessage{
MenuID: menuID,
Callback: callback,
})
}
// NewTray creates a new Tray item
func (t *trayRuntime) NewTray(trayID string) *menu.Tray {
return &menu.Tray{
ID: trayID,
}
}
func (t *trayRuntime) Update(tray ...*menu.Tray) {
//trayToUpdate := t.trayMenu
t.bus.Publish("tray:update", t.trayMenu)
}
func (t *trayRuntime) SetLabel(label string) {
t.bus.Publish("tray:setlabel", label)
}
func (t *trayRuntime) SetIcon(name string) {
t.bus.Publish("tray:seticon", name)
}
func (t *trayRuntime) GetByID(menuID string) *menu.MenuItem {
return t.trayMenu.Menu.GetByID(menuID)
}
func (t *trayRuntime) RemoveByID(id string) bool {
return t.trayMenu.Menu.RemoveByID(id)
}

View File

@@ -1,200 +0,0 @@
package subsystem
import (
"encoding/json"
"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"
)
// ContextMenus is the subsystem that handles the operation of context menus. It manages all service bus messages
// starting with "contextmenus".
type ContextMenus struct {
quitChannel <-chan *servicebus.Message
menuChannel <-chan *servicebus.Message
running bool
// Event listeners
listeners map[string][]func(*menu.MenuItem, string)
menuItems map[string]*menu.MenuItem
notifyLock sync.RWMutex
// logger
logger logger.CustomLogger
// The context menus
contextMenus *menu.ContextMenus
// Service Bus
bus *servicebus.ServiceBus
}
// NewContextMenus creates a new context menu subsystem
func NewContextMenus(contextMenus *menu.ContextMenus, bus *servicebus.ServiceBus, logger *logger.Logger) (*ContextMenus, error) {
// Register quit channel
quitChannel, err := bus.Subscribe("quit")
if err != nil {
return nil, err
}
// Subscribe to menu messages
menuChannel, err := bus.Subscribe("contextmenus:")
if err != nil {
return nil, err
}
result := &ContextMenus{
quitChannel: quitChannel,
menuChannel: menuChannel,
logger: logger.CustomLogger("Context Menu Subsystem"),
listeners: make(map[string][]func(*menu.MenuItem, string)),
menuItems: make(map[string]*menu.MenuItem),
contextMenus: contextMenus,
bus: bus,
}
// Build up list of item/id pairs
result.processContextMenus(contextMenus)
return result, nil
}
type contextMenuData struct {
MenuItemID string `json:"menuItemID"`
Data string `json:"data"`
}
// Start the subsystem
func (c *ContextMenus) Start() error {
c.logger.Trace("Starting")
c.running = true
// Spin off a go routine
go func() {
for c.running {
select {
case <-c.quitChannel:
c.running = false
break
case menuMessage := <-c.menuChannel:
splitTopic := strings.Split(menuMessage.Topic(), ":")
menuMessageType := splitTopic[1]
switch menuMessageType {
case "clicked":
if len(splitTopic) != 2 {
c.logger.Error("Received clicked message with invalid topic format. Expected 2 sections in topic, got %s", splitTopic)
continue
}
c.logger.Trace("Got Context Menu clicked Message: %s %+v", menuMessage.Topic(), menuMessage.Data())
contextMenuDataJSON := menuMessage.Data().(string)
var data contextMenuData
err := json.Unmarshal([]byte(contextMenuDataJSON), &data)
if err != nil {
c.logger.Trace("Cannot process contextMenuDataJSON %s", string(contextMenuDataJSON))
return
}
// Get the menu item
menuItem := c.menuItems[data.MenuItemID]
if menuItem == nil {
c.logger.Trace("Cannot process menuitem id %s - unknown", data.MenuItemID)
return
}
// Is the menu item a checkbox?
if menuItem.Type == menu.CheckboxType {
// Toggle state
menuItem.Checked = !menuItem.Checked
}
// Notify listeners
c.notifyListeners(data, menuItem)
case "on":
listenerDetails := menuMessage.Data().(*message.ContextMenusOnMessage)
id := listenerDetails.MenuID
c.listeners[id] = append(c.listeners[id], listenerDetails.Callback)
// Make sure we catch any menu updates
case "update":
updatedMenu := menuMessage.Data().(*menu.ContextMenus)
c.processContextMenus(updatedMenu)
// Notify frontend of menu change
c.bus.Publish("contextmenufrontend:update", updatedMenu)
default:
c.logger.Error("unknown context menu message: %+v", menuMessage)
}
}
}
// Call shutdown
c.shutdown()
}()
return nil
}
func (c *ContextMenus) processContextMenus(contextMenus *menu.ContextMenus) {
// Initialise the variables
c.menuItems = make(map[string]*menu.MenuItem)
c.contextMenus = contextMenus
for _, contextMenu := range contextMenus.Items {
for _, item := range contextMenu.Items {
c.processMenuItem(item)
}
}
}
func (c *ContextMenus) processMenuItem(item *menu.MenuItem) {
if item.SubMenu != nil {
for _, submenuitem := range item.SubMenu {
c.processMenuItem(submenuitem)
}
return
}
if item.ID != "" {
if c.menuItems[item.ID] != nil {
c.logger.Error("Context Menu id '%s' is used by multiple menu items: %s %s", c.menuItems[item.ID].Label, item.Label)
return
}
c.menuItems[item.ID] = item
}
}
// Notifies listeners that the given menu was clicked
func (c *ContextMenus) notifyListeners(contextData contextMenuData, menuItem *menu.MenuItem) {
// Get list of menu listeners
listeners := c.listeners[contextData.MenuItemID]
if listeners == nil {
c.logger.Trace("No listeners for MenuItem with ID '%s'", contextData.MenuItemID)
return
}
// Lock the listeners
c.notifyLock.Lock()
// Callback in goroutine
for _, listener := range listeners {
go listener(menuItem, contextData.Data)
}
// Unlock
c.notifyLock.Unlock()
}
func (c *ContextMenus) shutdown() {
c.logger.Trace("Shutdown")
}

View File

@@ -1,25 +1,15 @@
package subsystem
import (
"encoding/json"
"github.com/wailsapp/wails/v2/pkg/menu"
"strings"
"sync"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
"github.com/wailsapp/wails/v2/internal/menumanager"
"github.com/wailsapp/wails/v2/internal/servicebus"
"github.com/wailsapp/wails/v2/pkg/menu"
)
// eventListener holds a callback function which is invoked when
// the event listened for is emitted. It has a counter which indicates
// how the total number of events it is interested in. A value of zero
// means it does not expire (default).
// type eventListener struct {
// callback func(...interface{}) // Function to call with emitted event data
// counter int // The number of times this callback may be called. -1 = infinite
// delete bool // Flag to indicate that this listener should be deleted
// }
// Menu is the subsystem that handles the operation of menus. It manages all service bus messages
// starting with "menu".
type Menu struct {
@@ -27,23 +17,18 @@ type Menu struct {
menuChannel <-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 application menu
applicationMenu *menu.Menu
// Service Bus
bus *servicebus.ServiceBus
// Menu Manager
menuManager *menumanager.Manager
}
// NewMenu creates a new menu subsystem
func NewMenu(applicationMenu *menu.Menu, bus *servicebus.ServiceBus, logger *logger.Logger) (*Menu, error) {
func NewMenu(bus *servicebus.ServiceBus, logger *logger.Logger, menuManager *menumanager.Manager) (*Menu, error) {
// Register quit channel
quitChannel, err := bus.Subscribe("quit")
@@ -58,18 +43,13 @@ func NewMenu(applicationMenu *menu.Menu, bus *servicebus.ServiceBus, logger *log
}
result := &Menu{
quitChannel: quitChannel,
menuChannel: menuChannel,
logger: logger.CustomLogger("Menu Subsystem"),
listeners: make(map[string][]func(*menu.MenuItem)),
menuItems: make(map[string]*menu.MenuItem),
applicationMenu: applicationMenu,
bus: bus,
quitChannel: quitChannel,
menuChannel: menuChannel,
logger: logger.CustomLogger("Menu Subsystem"),
bus: bus,
menuManager: menuManager,
}
// Build up list of item/id pairs
result.processMenu(applicationMenu)
return result, nil
}
@@ -97,35 +77,59 @@ func (m *Menu) Start() error {
continue
}
m.logger.Trace("Got Menu clicked Message: %s %+v", menuMessage.Topic(), menuMessage.Data())
menuid := menuMessage.Data().(string)
// Get the menu item
menuItem := m.menuItems[menuid]
if menuItem == nil {
m.logger.Trace("Cannot process menuid %s - unknown", menuid)
type ClickCallbackMessage struct {
MenuItemID string `json:"menuItemID"`
MenuType string `json:"menuType"`
Data string `json:"data"`
ParentID string `json:"parentID"`
}
var callbackData ClickCallbackMessage
payload := []byte(menuMessage.Data().(string))
err := json.Unmarshal(payload, &callbackData)
if err != nil {
m.logger.Error("%s", err.Error())
return
}
// Is the menu item a checkbox?
if menuItem.Type == menu.CheckboxType {
// Toggle state
menuItem.Checked = !menuItem.Checked
err = m.menuManager.ProcessClick(callbackData.MenuItemID, callbackData.Data, callbackData.MenuType, callbackData.ParentID)
if err != nil {
m.logger.Trace("%s", err.Error())
}
// Notify listeners
m.notifyListeners(menuid, menuItem)
case "on":
listenerDetails := menuMessage.Data().(*message.MenuOnMessage)
id := listenerDetails.MenuID
m.listeners[id] = append(m.listeners[id], listenerDetails.Callback)
// Make sure we catch any menu updates
case "update":
updatedMenu := menuMessage.Data().(*menu.Menu)
m.processMenu(updatedMenu)
case "updateappmenu":
updatedMenu, err := m.menuManager.UpdateApplicationMenu()
if err != nil {
m.logger.Trace("%s", err.Error())
return
}
// Notify frontend of menu change
m.bus.Publish("menufrontend:update", updatedMenu)
m.bus.Publish("menufrontend:updateappmenu", updatedMenu)
case "updatecontextmenu":
contextMenu := menuMessage.Data().(*menu.ContextMenu)
updatedMenu, err := m.menuManager.UpdateContextMenu(contextMenu)
if err != nil {
m.logger.Trace("%s", err.Error())
return
}
// Notify frontend of menu change
m.bus.Publish("menufrontend:updatecontextmenu", updatedMenu)
case "updatetraymenu":
trayMenu := menuMessage.Data().(*menu.TrayMenu)
updatedMenu, err := m.menuManager.UpdateTrayMenu(trayMenu)
if err != nil {
m.logger.Trace("%s", err.Error())
return
}
// Notify frontend of menu change
m.bus.Publish("menufrontend:updatetraymenu", updatedMenu)
default:
m.logger.Error("unknown menu message: %+v", menuMessage)
@@ -140,56 +144,6 @@ func (m *Menu) Start() error {
return nil
}
func (m *Menu) processMenu(applicationMenu *menu.Menu) {
// Initialise the variables
m.menuItems = make(map[string]*menu.MenuItem)
m.applicationMenu = applicationMenu
for _, item := range applicationMenu.Items {
m.processMenuItem(item)
}
}
func (m *Menu) processMenuItem(item *menu.MenuItem) {
if item.SubMenu != nil {
for _, submenuitem := range item.SubMenu {
m.processMenuItem(submenuitem)
}
return
}
if item.ID != "" {
if m.menuItems[item.ID] != nil {
m.logger.Error("Menu id '%s' is used by multiple menu items: %s %s", m.menuItems[item.ID].Label, item.Label)
return
}
m.menuItems[item.ID] = item
}
}
// Notifies listeners that the given menu was clicked
func (m *Menu) notifyListeners(menuid string, menuItem *menu.MenuItem) {
// Get list of menu listeners
listeners := m.listeners[menuid]
if listeners == nil {
m.logger.Trace("No listeners for MenuItem with ID '%s'", menuid)
return
}
// Lock the listeners
m.notifyLock.Lock()
// Callback in goroutine
for _, listener := range listeners {
go listener(menuItem)
}
// Unlock
m.notifyLock.Unlock()
}
func (m *Menu) shutdown() {
m.logger.Trace("Shutdown")
}

View File

@@ -2,7 +2,6 @@ package subsystem
import (
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu"
"strings"
"github.com/wailsapp/wails/v2/internal/logger"
@@ -24,7 +23,7 @@ type Runtime struct {
}
// NewRuntime creates a new runtime subsystem
func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger, menu *menu.Menu, trayMenu *menu.Tray, contextMenus *menu.ContextMenus) (*Runtime, error) {
func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger) (*Runtime, error) {
// Register quit channel
quitChannel, err := bus.Subscribe("quit")
@@ -42,7 +41,7 @@ func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger, menu *menu.Me
quitChannel: quitChannel,
runtimeChannel: runtimeChannel,
logger: logger.CustomLogger("Runtime Subsystem"),
runtime: runtime.New(bus, menu, trayMenu, contextMenus),
runtime: runtime.New(bus),
}
return result, nil

View File

@@ -1,202 +0,0 @@
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.Tray
// Service Bus
bus *servicebus.ServiceBus
}
// NewTray creates a new menu subsystem
func NewTray(trayMenu *menu.Tray, 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.Menu)
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 Tray 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.TrayOnMessage)
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)
// Make sure we catch any menu updates
case "setlabel":
updatedLabel := menuMessage.Data().(string)
t.trayMenu.Label = updatedLabel
// Notify frontend of menu change
t.bus.Publish("trayfrontend:setlabel", updatedLabel)
// Make sure we catch any icon updates
case "seticon":
iconname := menuMessage.Data().(string)
t.trayMenu.Label = iconname
// Notify frontend of menu change
t.bus.Publish("trayfrontend:seticon", iconname)
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.Menu = 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

@@ -148,6 +148,19 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
// Default go build command
commands := slicer.String([]string{"build"})
// Add better debugging flags
if options.Mode == Debug {
commands.Add("-gcflags")
commands.Add(`"all=-N -l"`)
}
// TODO: Work out if we can make this more efficient
// We need to do a full build as CGO doesn't detect updates
// to .h files, and we package assets into .h file. We could
// potentially try and see if the assets have changed but will
// this take as much time as a `-a` build?
commands.Add("-a")
var tags slicer.StringSlicer
tags.Add(options.OutputType)
@@ -198,6 +211,7 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
options.CompiledBinary = compiledBinary
// Create the command
fmt.Printf("Compile command: %+v", commands.AsSlice())
cmd := exec.Command(options.Compiler, commands.AsSlice()...)
// Set the directory

View File

@@ -47,12 +47,6 @@ func (d *DesktopBuilder) BuildAssets(options *Options) error {
return err
}
// Build static assets
err = d.buildCustomAssets(d.projectData)
if err != nil {
return err
}
return nil
}

View File

@@ -6,7 +6,6 @@ The structure is:
* dialog - Icons for dialogs
* tray - Icons for the system tray
* custom - A place for assets you wish to bundle in the application
* mac - MacOS specific files
* linux - Linux specific files
* windows - Windows specific files
@@ -48,16 +47,6 @@ Example:
* `mypic.png` - May be referenced using `runtime.Tray.SetIcon("mypic")`
## Custom
Any file in this directory will be embedded into the app using the Wails asset bundler.
Assets can be retrieved using the following methods:
* `wails.Assets().Read(filename string) ([]byte, error)`
* `wails.Assets().String(filename string) (string, error)`
The filename should include the path to the file relative to the `custom` directory.
## Mac
The `mac` directory holds files specific to Mac builds, such as `info.plist`. These may be edited and used as part of the build.

8
v2/pkg/menu/callback.go Normal file
View File

@@ -0,0 +1,8 @@
package menu
type CallbackData struct {
MenuItem *MenuItem
ContextData string
}
type Callback func(*CallbackData)

View File

@@ -1,38 +1,13 @@
package menu
type ContextMenus struct {
Items map[string]*Menu
type ContextMenu struct {
ID string
Menu *Menu
}
func NewContextMenus() *ContextMenus {
return &ContextMenus{
Items: make(map[string]*Menu),
func NewContextMenu(ID string, menu *Menu) *ContextMenu {
return &ContextMenu{
ID: ID,
Menu: menu,
}
}
func (c *ContextMenus) AddMenu(ID string, menu *Menu) {
c.Items[ID] = menu
}
func (c *ContextMenus) GetByID(menuID string) *MenuItem {
// Loop over menu items
for _, item := range c.Items {
result := item.GetByID(menuID)
if result != nil {
return result
}
}
return nil
}
func (c *ContextMenus) RemoveByID(id string) bool {
// Loop over menu items
for _, item := range c.Items {
result := item.RemoveByID(id)
if result == true {
return result
}
}
return false
}

View File

@@ -12,6 +12,10 @@ func (m *Menu) Append(item *MenuItem) {
m.Items = append(m.Items, item)
}
func (m *Menu) Prepend(item *MenuItem) {
m.Items = append([]*MenuItem{item}, m.Items...)
}
func NewMenuFromItems(first *MenuItem, rest ...*MenuItem) *Menu {
var result = NewMenu()
@@ -23,29 +27,8 @@ func NewMenuFromItems(first *MenuItem, rest ...*MenuItem) *Menu {
return result
}
func (m *Menu) GetByID(menuID string) *MenuItem {
// Loop over menu items
func (m *Menu) setParent(menuItem *MenuItem) {
for _, item := range m.Items {
result := item.getByID(menuID)
if result != nil {
return result
}
item.parent = menuItem
}
return nil
}
func (m *Menu) RemoveByID(id string) bool {
// Loop over menu items
for index, item := range m.Items {
if item.ID == id {
m.Items = append(m.Items[:index], m.Items[index+1:]...)
return true
}
result := item.removeByID(id)
if result == true {
return result
}
}
return false
}

View File

@@ -1,11 +1,12 @@
package menu
import "github.com/wailsapp/wails/v2/pkg/menu/keys"
import (
"github.com/wailsapp/wails/v2/pkg/menu/keys"
"sync"
)
// MenuItem represents a menuitem contained in a menu
type MenuItem struct {
// The unique identifier of this menu item
ID string `json:"ID,omitempty"`
// Label is what appears as the menu text
Label string
// Role is a predefined menu type
@@ -21,7 +22,11 @@ type MenuItem struct {
// Checked indicates if the item is selected (used by Checkbox and Radio types only)
Checked bool
// Submenu contains a list of menu items that will be shown as a submenu
SubMenu []*MenuItem `json:"SubMenu,omitempty"`
//SubMenu []*MenuItem `json:"SubMenu,omitempty"`
SubMenu *Menu `json:"SubMenu,omitempty"`
// Callback function when menu clicked
Click Callback `json:"-"`
// Foreground colour in hex RGBA format EG: 0xFF0000FF = #FF0000FF = red
Foreground int
@@ -31,6 +36,9 @@ type MenuItem struct {
// This holds the menu item's parent.
parent *MenuItem
// Used for locking when removing elements
removeLock sync.Mutex
}
// Parent returns the parent of the menu item.
@@ -48,7 +56,7 @@ func (m *MenuItem) Append(item *MenuItem) bool {
return false
}
item.parent = m
m.SubMenu = append(m.SubMenu, item)
m.SubMenu.Append(item)
return true
}
@@ -61,43 +69,23 @@ func (m *MenuItem) Prepend(item *MenuItem) bool {
return false
}
item.parent = m
m.SubMenu = append([]*MenuItem{item}, m.SubMenu...)
m.SubMenu.Prepend(item)
return true
}
func (m *MenuItem) getByID(id string) *MenuItem {
// If I have the ID return me!
if m.ID == id {
return m
}
// Check submenus
for _, submenu := range m.SubMenu {
result := submenu.getByID(id)
if result != nil {
return result
}
}
return nil
func (m *MenuItem) Remove() {
// Iterate my parent's children
m.Parent().removeChild(m)
}
func (m *MenuItem) removeByID(id string) bool {
for index, item := range m.SubMenu {
if item.ID == id {
m.SubMenu = append(m.SubMenu[:index], m.SubMenu[index+1:]...)
return true
}
if item.isSubMenu() {
result := item.removeByID(id)
if result == true {
return result
}
func (m *MenuItem) removeChild(item *MenuItem) {
m.removeLock.Lock()
for index, child := range m.SubMenu.Items {
if item == child {
m.SubMenu.Items = append(m.SubMenu.Items[:index], m.SubMenu.Items[index+1:]...)
}
}
return false
m.removeLock.Unlock()
}
// InsertAfter attempts to add the given item after this item in the parent
@@ -181,7 +169,7 @@ func (m *MenuItem) getItemIndex(target *MenuItem) int {
}
// hunt down that bad boy
for index, item := range m.SubMenu {
for index, item := range m.SubMenu.Items {
if item == target {
return index
}
@@ -196,7 +184,7 @@ func (m *MenuItem) getItemIndex(target *MenuItem) int {
func (m *MenuItem) insertItemAtIndex(index int, target *MenuItem) bool {
// If index is OOB, return false
if index > len(m.SubMenu) {
if index > len(m.SubMenu.Items) {
return false
}
@@ -204,23 +192,23 @@ func (m *MenuItem) insertItemAtIndex(index int, target *MenuItem) bool {
target.parent = m
// If index is last item, then just regular append
if index == len(m.SubMenu) {
m.SubMenu = append(m.SubMenu, target)
if index == len(m.SubMenu.Items) {
m.SubMenu.Items = append(m.SubMenu.Items, target)
return true
}
m.SubMenu = append(m.SubMenu[:index+1], m.SubMenu[index:]...)
m.SubMenu[index] = target
m.SubMenu.Items = append(m.SubMenu.Items[:index+1], m.SubMenu.Items[index:]...)
m.SubMenu.Items[index] = target
return true
}
// Text is a helper to create basic Text menu items
func Text(label string, id string, accelerator *keys.Accelerator) *MenuItem {
func Text(label string, accelerator *keys.Accelerator, click Callback) *MenuItem {
return &MenuItem{
ID: id,
Label: label,
Type: TextType,
Accelerator: accelerator,
Click: click,
}
}
@@ -232,56 +220,49 @@ func Separator() *MenuItem {
}
// Radio is a helper to create basic Radio menu items with an accelerator
func Radio(label string, id string, selected bool, accelerator *keys.Accelerator) *MenuItem {
func Radio(label string, selected bool, accelerator *keys.Accelerator, click Callback) *MenuItem {
return &MenuItem{
ID: id,
Label: label,
Type: RadioType,
Checked: selected,
Accelerator: accelerator,
Click: click,
}
}
// Checkbox is a helper to create basic Checkbox menu items
func Checkbox(label string, id string, checked bool, accelerator *keys.Accelerator) *MenuItem {
func Checkbox(label string, checked bool, accelerator *keys.Accelerator, click Callback) *MenuItem {
return &MenuItem{
ID: id,
Label: label,
Type: CheckboxType,
Checked: checked,
Accelerator: accelerator,
Click: click,
}
}
// SubMenu is a helper to create Submenus
func SubMenu(label string, items []*MenuItem) *MenuItem {
func SubMenu(label string, menu *Menu) *MenuItem {
result := &MenuItem{
Label: label,
SubMenu: items,
SubMenu: menu,
Type: SubmenuType,
}
// Fix up parent pointers
for _, item := range items {
item.parent = result
}
menu.setParent(result)
return result
}
// SubMenuWithID is a helper to create Submenus with an ID
func SubMenuWithID(label string, id string, items []*MenuItem) *MenuItem {
func SubMenuWithID(label string, menu *Menu) *MenuItem {
result := &MenuItem{
Label: label,
SubMenu: items,
ID: id,
SubMenu: menu,
Type: SubmenuType,
}
// Fix up parent pointers
for _, item := range items {
item.parent = result
}
menu.setParent(result)
return result
}

View File

@@ -1,10 +1,7 @@
package menu
// Tray are the options
type Tray struct {
// The ID of this tray
ID string
// TrayMenu are the options
type TrayMenu struct {
// Label is the text we wish to display in the tray
Label string

View File

@@ -9,6 +9,6 @@ type Options struct {
WebviewIsTransparent bool
WindowBackgroundIsTranslucent bool
Menu *menu.Menu
Tray *menu.Tray
ContextMenus *menu.ContextMenus
TrayMenus []*menu.TrayMenu
ContextMenus []*menu.ContextMenu
}

View File

@@ -24,8 +24,8 @@ type App struct {
StartHidden bool
DevTools bool
RGBA int
ContextMenus *menu.ContextMenus
Tray *menu.Tray
ContextMenus []*menu.ContextMenu
TrayMenus []*menu.TrayMenu
Menu *menu.Menu
Mac *mac.Options
Logger logger.Logger `json:"-"`
@@ -41,25 +41,25 @@ func MergeDefaults(appoptions *App) {
}
func GetTray(appoptions *App) *menu.Tray {
var result *menu.Tray
func GetTrayMenus(appoptions *App) []*menu.TrayMenu {
var result []*menu.TrayMenu
switch runtime.GOOS {
case "darwin":
if appoptions.Mac != nil {
result = appoptions.Mac.Tray
result = appoptions.Mac.TrayMenus
}
//case "linux":
// if appoptions.Linux != nil {
// result = appoptions.Linux.Tray
// result = appoptions.Linux.TrayMenu
// }
//case "windows":
// if appoptions.Windows != nil {
// result = appoptions.Windows.Tray
// result = appoptions.Windows.TrayMenu
// }
}
if result == nil {
result = appoptions.Tray
result = appoptions.TrayMenus
}
return result
@@ -74,11 +74,11 @@ func GetApplicationMenu(appoptions *App) *menu.Menu {
}
//case "linux":
// if appoptions.Linux != nil {
// result = appoptions.Linux.Tray
// result = appoptions.Linux.TrayMenu
// }
//case "windows":
// if appoptions.Windows != nil {
// result = appoptions.Windows.Tray
// result = appoptions.Windows.TrayMenu
// }
}
@@ -89,31 +89,26 @@ func GetApplicationMenu(appoptions *App) *menu.Menu {
return result
}
func GetContextMenus(appoptions *App) *menu.ContextMenus {
var result *menu.ContextMenus
func GetContextMenus(appoptions *App) []*menu.ContextMenu {
var result []*menu.ContextMenu
result = appoptions.ContextMenus
var contextMenuOverrides *menu.ContextMenus
switch runtime.GOOS {
case "darwin":
if appoptions.Mac != nil {
contextMenuOverrides = appoptions.Mac.ContextMenus
result = appoptions.Mac.ContextMenus
}
//case "linux":
// if appoptions.Linux != nil {
// result = appoptions.Linux.Tray
// result = appoptions.Linux.TrayMenu
// }
//case "windows":
// if appoptions.Windows != nil {
// result = appoptions.Windows.Tray
// result = appoptions.Windows.TrayMenu
// }
}
// Overwrite defaults with OS Specific context menus
if contextMenuOverrides != nil {
for id, contextMenu := range contextMenuOverrides.Items {
result.AddMenu(id, contextMenu)
}
if result == nil {
result = appoptions.ContextMenus
}
return result

View File

@@ -10,9 +10,11 @@ import (
// ContextMenu struct
type ContextMenu struct {
runtime *wails.Runtime
counter int
lock sync.Mutex
runtime *wails.Runtime
counter int
lock sync.Mutex
testContextMenu *menu.ContextMenu
clickedMenu *menu.MenuItem
}
// WailsInit is called at application startup
@@ -20,21 +22,34 @@ func (c *ContextMenu) WailsInit(runtime *wails.Runtime) error {
// Perform your setup here
c.runtime = runtime
// Setup Menu Listeners
c.runtime.ContextMenu.On("Test Context Menu", func(mi *menu.MenuItem, contextData string) {
fmt.Printf("\n\nContext Data = '%s'\n\n", contextData)
c.lock.Lock()
c.counter++
mi.Label = fmt.Sprintf("Clicked %d times", c.counter)
c.lock.Unlock()
c.runtime.ContextMenu.Update()
})
return nil
}
func createContextMenus() *menu.ContextMenus {
result := menu.NewContextMenus()
result.AddMenu("test", menu.NewMenuFromItems(menu.Text("Clicked 0 times", "Test Context Menu", nil)))
return result
// Setup Menu Listeners
func (c *ContextMenu) updateContextMenu(_ *menu.CallbackData) {
c.lock.Lock()
c.counter++
c.clickedMenu.Label = fmt.Sprintf("Clicked %d times", c.counter)
c.lock.Unlock()
c.runtime.Menu.UpdateContextMenu(c.testContextMenu)
}
func (c *ContextMenu) createContextMenus() []*menu.ContextMenu {
c.clickedMenu = menu.Text("Clicked 0 times", nil, c.updateContextMenu)
c.testContextMenu = menu.NewContextMenu("test", menu.NewMenuFromItems(
c.clickedMenu,
menu.Separator(),
menu.Checkbox("I am a checkbox", false, nil, nil),
menu.Separator(),
menu.Radio("Radio Option 1", true, nil, nil),
menu.Radio("Radio Option 2", false, nil, nil),
menu.Radio("Radio Option 3", false, nil, nil),
menu.Separator(),
menu.SubMenu("A Submenu", menu.NewMenuFromItems(
menu.Text("Hello", nil, nil),
)),
))
return []*menu.ContextMenu{
c.testContextMenu,
}
}

View File

@@ -3,7 +3,6 @@ package main
import (
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/mac"
"log"
@@ -11,6 +10,10 @@ import (
func main() {
Menu := &Menu{}
Tray := &Tray{}
ContextMenu := &ContextMenu{}
// Create application with options
app, err := wails.CreateAppWithOptions(&options.App{
Title: "Kitchen Sink",
@@ -21,17 +24,14 @@ func main() {
//Tray: menu.NewMenuFromItems(menu.AppMenu()),
//Menu: menu.NewMenuFromItems(menu.AppMenu()),
//StartHidden: true,
ContextMenus: createContextMenus(),
ContextMenus: ContextMenu.createContextMenus(),
Mac: &mac.Options{
WebviewIsTransparent: true,
WindowBackgroundIsTranslucent: true,
// Comment out line below to see Window.SetTitle() work
TitleBar: mac.TitleBarHiddenInset(),
Menu: createApplicationMenu(),
Tray: &menu.Tray{
Icon: "light",
Menu: createApplicationTray(),
},
TitleBar: mac.TitleBarHiddenInset(),
Menu: Menu.createApplicationMenu(),
TrayMenus: Tray.createTrayMenus(),
},
LogLevel: logger.TRACE,
})
@@ -46,9 +46,9 @@ func main() {
app.Bind(&System{})
app.Bind(&Dialog{})
app.Bind(&Window{})
app.Bind(&Menu{})
app.Bind(&Tray{})
app.Bind(&ContextMenu{})
app.Bind(Menu)
app.Bind(Tray)
app.Bind(ContextMenu)
err = app.Run()
if err != nil {

View File

@@ -3,7 +3,6 @@ package main
import (
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu/keys"
"math/rand"
"strconv"
"sync"
@@ -16,9 +15,14 @@ type Menu struct {
runtime *wails.Runtime
dynamicMenuCounter int
dynamicMenuOneItems []*menu.MenuItem
lock sync.Mutex
dynamicMenuItems map[string]*menu.MenuItem
anotherDynamicMenuCounter int
// Menus
removeMenuItem *menu.MenuItem
dynamicMenuOneSubmenu *menu.MenuItem
}
// WailsInit is called at application startup
@@ -26,26 +30,6 @@ func (m *Menu) WailsInit(runtime *wails.Runtime) error {
// Perform your setup here
m.runtime = runtime
// Setup Menu Listeners
m.runtime.Menu.On("hello", func(mi *menu.MenuItem) {
fmt.Printf("The '%s' menu was clicked\n", mi.Label)
})
m.runtime.Menu.On("checkbox-menu", func(mi *menu.MenuItem) {
fmt.Printf("The '%s' menu was clicked\n", mi.Label)
fmt.Printf("It is now %v\n", mi.Checked)
})
m.runtime.Menu.On("😀option-1", func(mi *menu.MenuItem) {
fmt.Printf("We can use UTF-8 IDs: %s\n", mi.Label)
})
m.runtime.Menu.On("show-dynamic-menus-2", func(mi *menu.MenuItem) {
mi.Hidden = true
// Create dynamic menu items 2 submenu
m.createDynamicMenuTwo()
})
// Setup dynamic menus
m.runtime.Menu.On("Add Menu Item", m.addMenu)
return nil
}
@@ -59,177 +43,164 @@ func (m *Menu) decrementcounter() int {
return m.dynamicMenuCounter
}
func (m *Menu) addMenu(mi *menu.MenuItem) {
func (m *Menu) addDynamicMenusOneMenu(data *menu.CallbackData) {
// Lock because this method will be called in a gorouting
m.lock.Lock()
defer m.lock.Unlock()
// Get this menu's parent
mi := data.MenuItem
parent := mi.Parent()
counter := m.incrementcounter()
menuText := "Dynamic Menu Item " + strconv.Itoa(counter)
parent.Append(menu.Text(menuText, menuText, nil))
// parent.Append(menu.Text(menuText, menuText, menu.Key("[")))
newDynamicMenu := menu.Text(menuText, nil, nil)
m.dynamicMenuOneItems = append(m.dynamicMenuOneItems, newDynamicMenu)
parent.Append(newDynamicMenu)
// If this is the first dynamic menu added, let's add a remove menu item
if counter == 1 {
removeMenu := menu.Text("Remove "+menuText,
"Remove Last Item", keys.CmdOrCtrl("-"))
parent.Prepend(removeMenu)
m.runtime.Menu.On("Remove Last Item", m.removeMenu)
m.removeMenuItem = menu.Text("Remove "+menuText, keys.CmdOrCtrl("-"), m.removeDynamicMenuOneMenu)
parent.Prepend(m.removeMenuItem)
} else {
removeMenu := m.runtime.Menu.GetByID("Remove Last Item")
// Test if the remove menu hasn't already been removed in another thread
if removeMenu != nil {
removeMenu.Label = "Remove " + menuText
if m.removeMenuItem != nil {
m.removeMenuItem.Label = "Remove " + menuText
}
}
m.runtime.Menu.Update()
m.runtime.Menu.UpdateApplicationMenu()
}
func (m *Menu) removeMenu(_ *menu.MenuItem) {
func (m *Menu) removeDynamicMenuOneMenu(_ *menu.CallbackData) {
//
// Lock because this method will be called in a goroutine
m.lock.Lock()
defer m.lock.Unlock()
// Get the id of the last dynamic menu
menuID := "Dynamic Menu Item " + strconv.Itoa(m.dynamicMenuCounter)
// Get the last menu we added
lastItemIndex := len(m.dynamicMenuOneItems) - 1
lastMenuAdded := m.dynamicMenuOneItems[lastItemIndex]
// Remove the last menu item by ID
m.runtime.Menu.RemoveByID(menuID)
// Remove from slice
m.dynamicMenuOneItems = m.dynamicMenuOneItems[:lastItemIndex]
// Remove the item from the menu
lastMenuAdded.Remove()
// Update the counter
counter := m.decrementcounter()
// If we deleted the last dynamic menu, remove the "Remove Last Item" menu
if counter == 0 {
m.runtime.Menu.RemoveByID("Remove Last Item")
// Remove it!
m.removeMenuItem.Remove()
} else {
// Update label
menuText := "Dynamic Menu Item " + strconv.Itoa(counter)
removeMenu := m.runtime.Menu.GetByID("Remove Last Item")
// Test if the remove menu hasn't already been removed in another thread
if removeMenu == nil {
return
}
removeMenu.Label = "Remove " + menuText
m.removeMenuItem.Label = "Remove " + menuText
}
// parent.Append(menu.Text(menuText, menuText, menu.Key("[")))
m.runtime.Menu.Update()
m.runtime.Menu.UpdateApplicationMenu()
}
func (m *Menu) createDynamicMenuTwo() {
func (m *Menu) createDynamicMenuTwo(data *menu.CallbackData) {
// Hide this menu
data.MenuItem.Hidden = true
// Create our submenu
dm2 := menu.SubMenu("Dynamic Menus 2", []*menu.MenuItem{
menu.Text("Insert Before Random Menu Item",
"Insert Before Random", keys.CmdOrCtrl("]")),
menu.Text("Insert After Random Menu Item",
"Insert After Random", keys.CmdOrCtrl("[")),
dm2 := menu.SubMenu("Dynamic Menus 2", menu.NewMenuFromItems(
menu.Text("Insert Before Random Menu Item", keys.CmdOrCtrl("]"), m.insertBeforeRandom),
menu.Text("Insert After Random Menu Item", keys.CmdOrCtrl("["), m.insertAfterRandom),
menu.Separator(),
})
))
m.runtime.Menu.On("Insert Before Random", m.insertBeforeRandom)
m.runtime.Menu.On("Insert After Random", m.insertAfterRandom)
//m.runtime.Menu.On("Insert Before Random", m.insertBeforeRandom)
//m.runtime.Menu.On("Insert After Random", m.insertAfterRandom)
// Initialise out map
// Initialise dynamicMenuItems
m.dynamicMenuItems = make(map[string]*menu.MenuItem)
// Create some random menu items
m.anotherDynamicMenuCounter = 5
for index := 0; index < m.anotherDynamicMenuCounter; index++ {
text := "Other Dynamic Menu Item " + strconv.Itoa(index+1)
item := menu.Text(text, text, nil)
item := menu.Text(text, nil, nil)
m.dynamicMenuItems[text] = item
dm2.Append(item)
}
// Insert this menu after Dynamic Menu Item 1
dm1 := m.runtime.Menu.GetByID("Dynamic Menus 1")
if dm1 == nil {
return
}
dm1.InsertAfter(dm2)
m.runtime.Menu.Update()
m.dynamicMenuOneSubmenu.InsertAfter(dm2)
m.runtime.Menu.UpdateApplicationMenu()
}
func (m *Menu) insertBeforeRandom(_ *menu.MenuItem) {
func (m *Menu) insertBeforeRandom(_ *menu.CallbackData) {
// Lock because this method will be called in a goroutine
m.lock.Lock()
defer m.lock.Unlock()
// Pick a random menu
var randomItemID string
var count int
var random = rand.Intn(len(m.dynamicMenuItems))
for randomItemID = range m.dynamicMenuItems {
if count == random {
break
}
count++
var randomMenuItem *menu.MenuItem
for _, randomMenuItem = range m.dynamicMenuItems {
break
}
m.anotherDynamicMenuCounter++
text := "Other Dynamic Menu Item " + strconv.Itoa(
m.anotherDynamicMenuCounter+1)
newItem := menu.Text(text, text, nil)
m.dynamicMenuItems[text] = newItem
item := m.runtime.Menu.GetByID(randomItemID)
if item == nil {
if randomMenuItem == nil {
return
}
m.runtime.Log.Info(fmt.Sprintf(
"Inserting menu item '%s' before menu item '%s'", newItem.Label,
item.Label))
item.InsertBefore(newItem)
m.runtime.Menu.Update()
}
func (m *Menu) insertAfterRandom(_ *menu.MenuItem) {
// Lock because this method will be called in a goroutine
m.lock.Lock()
defer m.lock.Unlock()
// Pick a random menu
var randomItemID string
var count int
var random = rand.Intn(len(m.dynamicMenuItems))
for randomItemID = range m.dynamicMenuItems {
if count == random {
break
}
count++
}
m.anotherDynamicMenuCounter++
text := "Other Dynamic Menu Item " + strconv.Itoa(
m.anotherDynamicMenuCounter+1)
newItem := menu.Text(text, text, nil)
item := m.runtime.Menu.GetByID(randomItemID)
text := "Other Dynamic Menu Item " + strconv.Itoa(m.anotherDynamicMenuCounter)
newItem := menu.Text(text, nil, nil)
m.dynamicMenuItems[text] = newItem
m.runtime.Log.Info(fmt.Sprintf(
"Inserting menu item '%s' after menu item '%s'", newItem.Label,
item.Label))
m.runtime.Log.Info(fmt.Sprintf("Inserting menu item '%s' before menu item '%s'", newItem.Label, randomMenuItem.Label))
item.InsertAfter(newItem)
m.runtime.Menu.Update()
randomMenuItem.InsertBefore(newItem)
m.runtime.Menu.UpdateApplicationMenu()
}
func createApplicationMenu() *menu.Menu {
func (m *Menu) insertAfterRandom(_ *menu.CallbackData) {
// Pick a random menu
var randomMenuItem *menu.MenuItem
for _, randomMenuItem = range m.dynamicMenuItems {
break
}
if randomMenuItem == nil {
return
}
m.anotherDynamicMenuCounter++
text := "Other Dynamic Menu Item " + strconv.Itoa(m.anotherDynamicMenuCounter)
newItem := menu.Text(text, nil, nil)
m.dynamicMenuItems[text] = newItem
m.runtime.Log.Info(fmt.Sprintf("Inserting menu item '%s' after menu item '%s'", newItem.Label, randomMenuItem.Label))
randomMenuItem.InsertBefore(newItem)
m.runtime.Menu.UpdateApplicationMenu()
}
func (m *Menu) processPlainText(callbackData *menu.CallbackData) {
label := callbackData.MenuItem.Label
fmt.Printf("\n\n\n\n\n\n\nMenu Item label = `%s`\n\n\n\n\n", label)
}
func (m *Menu) createApplicationMenu() *menu.Menu {
m.dynamicMenuOneSubmenu = menu.SubMenuWithID("Dynamic Menus 1", menu.NewMenuFromItems(
menu.Text("Add Menu Item", keys.CmdOrCtrl("+"), m.addDynamicMenusOneMenu),
menu.Separator(),
))
// Create menu
myMenu := menu.DefaultMacMenu()
windowMenu := menu.SubMenu("Test", []*menu.MenuItem{
windowMenu := menu.SubMenu("Test", menu.NewMenuFromItems(
menu.Togglefullscreen(),
menu.Minimize(),
menu.Zoom(),
@@ -244,89 +215,90 @@ func createApplicationMenu() *menu.Menu {
menu.Front(),
menu.SubMenu("Test Submenu", []*menu.MenuItem{
menu.Text("Plain text", "plain text", nil),
menu.Text("Show Dynamic Menus 2 Submenu", "show-dynamic-menus-2", nil),
menu.SubMenu("Accelerators", []*menu.MenuItem{
menu.SubMenu("Modifiers", []*menu.MenuItem{
menu.Text("Shift accelerator", "Shift", keys.Shift("o")),
menu.Text("Control accelerator", "Control", keys.Control("o")),
menu.Text("Command accelerator", "Command", keys.CmdOrCtrl("o")),
menu.Text("Option accelerator", "Option", keys.OptionOrAlt("o")),
}),
menu.SubMenu("System Keys", []*menu.MenuItem{
menu.Text("Backspace", "Backspace", keys.Key("Backspace")),
menu.Text("Tab", "Tab", keys.Key("Tab")),
menu.Text("Return", "Return", keys.Key("Return")),
menu.Text("Escape", "Escape", keys.Key("Escape")),
menu.Text("Left", "Left", keys.Key("Left")),
menu.Text("Right", "Right", keys.Key("Right")),
menu.Text("Up", "Up", keys.Key("Up")),
menu.Text("Down", "Down", keys.Key("Down")),
menu.Text("Space", "Space", keys.Key("Space")),
menu.Text("Delete", "Delete", keys.Key("Delete")),
menu.Text("Home", "Home", keys.Key("Home")),
menu.Text("End", "End", keys.Key("End")),
menu.Text("Page Up", "Page Up", keys.Key("Page Up")),
menu.Text("Page Down", "Page Down", keys.Key("Page Down")),
menu.Text("NumLock", "NumLock", keys.Key("NumLock")),
}),
menu.SubMenu("Function Keys", []*menu.MenuItem{
menu.Text("F1", "F1", keys.Key("F1")),
menu.Text("F2", "F2", keys.Key("F2")),
menu.Text("F3", "F3", keys.Key("F3")),
menu.Text("F4", "F4", keys.Key("F4")),
menu.Text("F5", "F5", keys.Key("F5")),
menu.Text("F6", "F6", keys.Key("F6")),
menu.Text("F7", "F7", keys.Key("F7")),
menu.Text("F8", "F8", keys.Key("F8")),
menu.Text("F9", "F9", keys.Key("F9")),
menu.Text("F10", "F10", keys.Key("F10")),
menu.Text("F11", "F11", keys.Key("F11")),
menu.Text("F12", "F12", keys.Key("F12")),
menu.Text("F13", "F13", keys.Key("F13")),
menu.Text("F14", "F14", keys.Key("F14")),
menu.Text("F15", "F15", keys.Key("F15")),
menu.Text("F16", "F16", keys.Key("F16")),
menu.Text("F17", "F17", keys.Key("F17")),
menu.Text("F18", "F18", keys.Key("F18")),
menu.Text("F19", "F19", keys.Key("F19")),
menu.Text("F20", "F20", keys.Key("F20")),
}),
menu.SubMenu("Standard Keys", []*menu.MenuItem{
menu.Text("Backtick", "Backtick", keys.Key("`")),
menu.Text("Plus", "Plus", keys.Key("+")),
}),
}),
menu.SubMenuWithID("Dynamic Menus 1", "Dynamic Menus 1", []*menu.MenuItem{
menu.Text("Add Menu Item", "Add Menu Item", keys.CmdOrCtrl("+")),
menu.Separator(),
}),
{
menu.SubMenu("Test Submenu", menu.NewMenuFromItems(
menu.Text("Plain text", nil, m.processPlainText),
menu.Text("Show Dynamic Menus 2 Submenu", nil, m.createDynamicMenuTwo),
menu.SubMenu("Accelerators", menu.NewMenuFromItems(
menu.SubMenu("Modifiers", menu.NewMenuFromItems(
menu.Text("Shift accelerator", keys.Shift("o"), nil),
menu.Text("Control accelerator", keys.Control("o"), nil),
menu.Text("Command accelerator", keys.CmdOrCtrl("o"), nil),
menu.Text("Option accelerator", keys.OptionOrAlt("o"), nil),
menu.Text("Combo accelerator", keys.Combo("o", keys.CmdOrCtrlKey, keys.ShiftKey), nil),
)),
menu.SubMenu("System Keys", menu.NewMenuFromItems(
menu.Text("Backspace", keys.Key("Backspace"), nil),
menu.Text("Tab", keys.Key("Tab"), nil),
menu.Text("Return", keys.Key("Return"), nil),
menu.Text("Escape", keys.Key("Escape"), nil),
menu.Text("Left", keys.Key("Left"), nil),
menu.Text("Right", keys.Key("Right"), nil),
menu.Text("Up", keys.Key("Up"), nil),
menu.Text("Down", keys.Key("Down"), nil),
menu.Text("Space", keys.Key("Space"), nil),
menu.Text("Delete", keys.Key("Delete"), nil),
menu.Text("Home", keys.Key("Home"), nil),
menu.Text("End", keys.Key("End"), nil),
menu.Text("Page Up", keys.Key("Page Up"), nil),
menu.Text("Page Down", keys.Key("Page Down"), nil),
menu.Text("NumLock", keys.Key("NumLock"), nil),
)),
menu.SubMenu("Function Keys", menu.NewMenuFromItems(
menu.Text("F1", keys.Key("F1"), nil),
menu.Text("F2", keys.Key("F2"), nil),
menu.Text("F3", keys.Key("F3"), nil),
menu.Text("F4", keys.Key("F4"), nil),
menu.Text("F5", keys.Key("F5"), nil),
menu.Text("F6", keys.Key("F6"), nil),
menu.Text("F7", keys.Key("F7"), nil),
menu.Text("F8", keys.Key("F8"), nil),
menu.Text("F9", keys.Key("F9"), nil),
menu.Text("F10", keys.Key("F10"), nil),
menu.Text("F11", keys.Key("F11"), nil),
menu.Text("F12", keys.Key("F12"), nil),
menu.Text("F13", keys.Key("F13"), nil),
menu.Text("F14", keys.Key("F14"), nil),
menu.Text("F15", keys.Key("F15"), nil),
menu.Text("F16", keys.Key("F16"), nil),
menu.Text("F17", keys.Key("F17"), nil),
menu.Text("F18", keys.Key("F18"), nil),
menu.Text("F19", keys.Key("F19"), nil),
menu.Text("F20", keys.Key("F20"), nil),
)),
menu.SubMenu("Standard Keys", menu.NewMenuFromItems(
menu.Text("Backtick", keys.Key("`"), nil),
menu.Text("Plus", keys.Key("+"), nil),
)),
)),
m.dynamicMenuOneSubmenu,
&menu.MenuItem{
Label: "Disabled Menu",
Type: menu.TextType,
Accelerator: keys.Combo("p", keys.CmdOrCtrlKey, keys.ShiftKey),
Disabled: true,
},
{
&menu.MenuItem{
Label: "Hidden Menu",
Type: menu.TextType,
Hidden: true,
},
{
ID: "checkbox-menu 1",
&menu.MenuItem{
Label: "Checkbox Menu 1",
Type: menu.CheckboxType,
Accelerator: keys.CmdOrCtrl("l"),
Checked: true,
Click: func(data *menu.CallbackData) {
fmt.Printf("The '%s' menu was clicked\n", data.MenuItem.Label)
fmt.Printf("It is now %v\n", data.MenuItem.Checked)
},
},
menu.Checkbox("Checkbox Menu 2", "checkbox-menu 2", false, nil),
menu.Checkbox("Checkbox Menu 2", false, nil, nil),
menu.Separator(),
menu.Radio("😀 Option 1", "😀option-1", true, nil),
menu.Radio("😺 Option 2", "option-2", false, nil),
menu.Radio("❤️ Option 3", "option-3", false, nil),
}),
})
menu.Radio("😀 Option 1", true, nil, nil),
menu.Radio("😺 Option 2", false, nil, nil),
menu.Radio("❤️ Option 3", false, nil, nil),
)),
))
myMenu.Append(windowMenu)
return myMenu

View File

@@ -3,7 +3,6 @@ package main
import (
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/menu/keys"
"strconv"
"sync"
)
@@ -12,10 +11,12 @@ import (
type Tray struct {
runtime *wails.Runtime
dynamicMenuCounter int
lock sync.Mutex
dynamicMenuItems map[string]*menu.MenuItem
anotherDynamicMenuCounter int
dynamicMenuCounter int
lock sync.Mutex
dynamicMenuItems map[string]*menu.MenuItem
trayMenu *menu.TrayMenu
secondTrayMenu *menu.TrayMenu
done bool
}
@@ -25,35 +26,35 @@ func (t *Tray) WailsInit(runtime *wails.Runtime) error {
// Perform your setup here
t.runtime = runtime
// Setup Menu Listeners
t.runtime.Tray.On("Show Window", func(mi *menu.MenuItem) {
t.runtime.Window.Show()
})
t.runtime.Tray.On("Hide Window", func(mi *menu.MenuItem) {
t.runtime.Window.Hide()
})
t.runtime.Tray.On("Minimise Window", func(mi *menu.MenuItem) {
t.runtime.Window.Minimise()
})
t.runtime.Tray.On("Unminimise Window", func(mi *menu.MenuItem) {
t.runtime.Window.Unminimise()
})
// Auto switch between light / dark tray icons
t.runtime.Events.OnThemeChange(func(darkMode bool) {
if darkMode {
t.runtime.Tray.SetIcon("light")
return
}
t.runtime.Tray.SetIcon("dark")
})
//// Auto switch between light / dark tray icons
//t.runtime.Events.OnThemeChange(func(darkMode bool) {
// if darkMode {
// t.runtime.Tray.SetIcon("light")
// return
// }
//
// t.runtime.Tray.SetIcon("dark")
//})
return nil
}
func (t *Tray) showWindow(_ *menu.CallbackData) {
t.runtime.Window.Show()
}
func (t *Tray) hideWindow(_ *menu.CallbackData) {
t.runtime.Window.Hide()
}
func (t *Tray) unminimiseWindow(_ *menu.CallbackData) {
t.runtime.Window.Unminimise()
}
func (t *Tray) minimiseWindow(_ *menu.CallbackData) {
t.runtime.Window.Minimise()
}
func (t *Tray) WailsShutdown() {
t.done = true
}
@@ -68,77 +69,95 @@ func (t *Tray) decrementcounter() int {
return t.dynamicMenuCounter
}
func (t *Tray) addMenu(mi *menu.MenuItem) {
func (t *Tray) SvelteIcon(_ *menu.CallbackData) {
t.secondTrayMenu.Icon = "svelte"
t.runtime.Menu.UpdateTrayMenu(t.secondTrayMenu)
}
func (t *Tray) NoIcon(_ *menu.CallbackData) {
t.secondTrayMenu.Icon = ""
t.runtime.Menu.UpdateTrayMenu(t.secondTrayMenu)
}
func (t *Tray) LightIcon(_ *menu.CallbackData) {
t.secondTrayMenu.Icon = "light"
t.runtime.Menu.UpdateTrayMenu(t.secondTrayMenu)
}
func (t *Tray) DarkIcon(_ *menu.CallbackData) {
t.secondTrayMenu.Icon = "dark"
t.runtime.Menu.UpdateTrayMenu(t.secondTrayMenu)
}
// Lock because this method will be called in a gorouting
t.lock.Lock()
defer t.lock.Unlock()
//func (t *Tray) removeMenu(_ *menu.MenuItem) {
//
// // Lock because this method will be called in a goroutine
// t.lock.Lock()
// defer t.lock.Unlock()
//
// // Get the id of the last dynamic menu
// menuID := "Dynamic Menu Item " + strconv.Itoa(t.dynamicMenuCounter)
//
// // Remove the last menu item by ID
// t.runtime.Tray.RemoveByID(menuID)
//
// // Update the counter
// counter := t.decrementcounter()
//
// // If we deleted the last dynamic menu, remove the "Remove Last Item" menu
// if counter == 0 {
// t.runtime.Tray.RemoveByID("Remove Last Item")
// } else {
// // Update label
// menuText := "Dynamic Menu Item " + strconv.Itoa(counter)
// removeMenu := t.runtime.Tray.GetByID("Remove Last Item")
// // Test if the remove menu hasn't already been removed in another thread
// if removeMenu == nil {
// return
// }
// removeMenu.Label = "Remove " + menuText
// }
//
// // parent.Append(menu.Text(menuText, menuText, menu.Key("[")))
// t.runtime.Tray.Update()
//}
// Get this menu's parent
parent := mi.Parent()
counter := t.incrementcounter()
menuText := "Dynamic Menu Item " + strconv.Itoa(counter)
parent.Append(menu.Text(menuText, menuText, nil))
// parent.Append(menu.Text(menuText, menuText, menu.Key("[")))
//func (t *Tray) SetIcon(trayIconID string) {
// t.runtime.Tray.SetIcon(trayIconID)
//}
// If this is the first dynamic menu added, let's add a remove menu item
if counter == 1 {
removeMenu := menu.Text("Remove "+menuText,
"Remove Last Item", keys.CmdOrCtrl("-"))
parent.Prepend(removeMenu)
t.runtime.Tray.On("Remove Last Item", t.removeMenu)
} else {
removeMenu := t.runtime.Tray.GetByID("Remove Last Item")
// Test if the remove menu hasn't already been removed in another thread
if removeMenu != nil {
removeMenu.Label = "Remove " + menuText
}
func (t *Tray) createTrayMenus() []*menu.TrayMenu {
trayMenu := &menu.TrayMenu{}
trayMenu.Label = "Test Tray Label"
trayMenu.Menu = menu.NewMenuFromItems(
menu.Text("Show Window", nil, t.showWindow),
menu.Text("Hide Window", nil, t.hideWindow),
menu.Text("Minimise Window", nil, t.minimiseWindow),
menu.Text("Unminimise Window", nil, t.unminimiseWindow),
)
t.trayMenu = trayMenu
secondTrayMenu := &menu.TrayMenu{}
secondTrayMenu.Label = "Another tray label"
secondTrayMenu.Icon = "svelte"
secondTrayMenu.Menu = menu.NewMenuFromItems(
menu.Text("Update Label", nil, func(_ *menu.CallbackData) {
// Lock because this method will be called in a goroutine
t.lock.Lock()
defer t.lock.Unlock()
counter := t.incrementcounter()
trayLabel := "Updated Label " + strconv.Itoa(counter)
secondTrayMenu.Label = trayLabel
t.runtime.Menu.UpdateTrayMenu(t.secondTrayMenu)
}),
menu.SubMenu("Select Icon", menu.NewMenuFromItems(
menu.Text("Svelte", nil, t.SvelteIcon),
menu.Text("Light", nil, t.LightIcon),
menu.Text("Dark", nil, t.DarkIcon),
menu.Text("None", nil, t.NoIcon),
)),
)
t.secondTrayMenu = secondTrayMenu
return []*menu.TrayMenu{
trayMenu,
secondTrayMenu,
}
t.runtime.Tray.Update()
}
func (t *Tray) removeMenu(_ *menu.MenuItem) {
// Lock because this method will be called in a goroutine
t.lock.Lock()
defer t.lock.Unlock()
// Get the id of the last dynamic menu
menuID := "Dynamic Menu Item " + strconv.Itoa(t.dynamicMenuCounter)
// Remove the last menu item by ID
t.runtime.Tray.RemoveByID(menuID)
// Update the counter
counter := t.decrementcounter()
// If we deleted the last dynamic menu, remove the "Remove Last Item" menu
if counter == 0 {
t.runtime.Tray.RemoveByID("Remove Last Item")
} else {
// Update label
menuText := "Dynamic Menu Item " + strconv.Itoa(counter)
removeMenu := t.runtime.Tray.GetByID("Remove Last Item")
// Test if the remove menu hasn't already been removed in another thread
if removeMenu == nil {
return
}
removeMenu.Label = "Remove " + menuText
}
// parent.Append(menu.Text(menuText, menuText, menu.Key("[")))
t.runtime.Tray.Update()
}
func (t *Tray) SetIcon(trayIconID string) {
t.runtime.Tray.SetIcon(trayIconID)
}
func createApplicationTray() *menu.Menu {
trayMenu := &menu.Menu{}
trayMenu.Append(menu.Text("Show Window", "Show Window", nil))
trayMenu.Append(menu.Text("Hide Window", "Hide Window", nil))
trayMenu.Append(menu.Text("Minimise Window", "Minimise Window", nil))
trayMenu.Append(menu.Text("Unminimise Window", "Unminimise Window", nil))
return trayMenu
}