mirror of
https://github.com/taigrr/wails.git
synced 2026-04-17 04:05:12 -07:00
Compare commits
35 Commits
v2.0.0-alp
...
v2.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a0063de1f | ||
|
|
1b7d1e61cc | ||
|
|
15a273458e | ||
|
|
eef8eb756f | ||
|
|
e65118e962 | ||
|
|
de06fc7dcc | ||
|
|
a86fbbb440 | ||
|
|
3045ec107f | ||
|
|
3a9557ad30 | ||
|
|
583153383a | ||
|
|
3f53e8fd5f | ||
|
|
5c9402323a | ||
|
|
1921862b53 | ||
|
|
0f7acd39fc | ||
|
|
1a7507f524 | ||
|
|
db6dde3e50 | ||
|
|
4e58b7697a | ||
|
|
55d7d9693f | ||
|
|
b4b7c9d306 | ||
|
|
a4153fae57 | ||
|
|
8053357d99 | ||
|
|
7347d2caa2 | ||
|
|
e6491bcbb7 | ||
|
|
26a291dbee | ||
|
|
8ee8c9b07c | ||
|
|
3a2d01813a | ||
|
|
d2dadc386f | ||
|
|
faa8f02b08 | ||
|
|
fbee9ba240 | ||
|
|
2a69786d7e | ||
|
|
f460bf91ef | ||
|
|
bd74d45a91 | ||
|
|
c65522f0b6 | ||
|
|
5f2c437136 | ||
|
|
87e974e080 |
123
v2/cmd/wails/internal/commands/debug/debug.go
Normal file
123
v2/cmd/wails/internal/commands/debug/debug.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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 this is a folder, add it to our watch list
|
||||||
if fs.DirExists(event.Name) {
|
if fs.DirExists(event.Name) {
|
||||||
if !strings.Contains(event.Name, "node_modules") {
|
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)
|
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
|
// Kill the current program if running
|
||||||
if debugBinaryProcess != nil {
|
if debugBinaryProcess != nil {
|
||||||
debugBinaryProcess.Kill()
|
err := debugBinaryProcess.Kill()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Println("Development mode exited")
|
logger.Println("Development mode exited")
|
||||||
@@ -231,7 +237,10 @@ func restartApp(logger *clilogger.CLILogger, outputType string, ldflags string,
|
|||||||
err = newProcess.Start()
|
err = newProcess.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Remove binary
|
// 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())
|
logger.Fatal("Unable to start application: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/leaanthony/clir"
|
"github.com/leaanthony/clir"
|
||||||
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/build"
|
"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/dev"
|
||||||
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/doctor"
|
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/doctor"
|
||||||
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/generate"
|
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/generate"
|
||||||
@@ -27,6 +28,11 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
fatal(err.Error())
|
fatal(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = debug.AddSubcommand(app, os.Stdout)
|
||||||
|
if err != nil {
|
||||||
|
fatal(err.Error())
|
||||||
|
}
|
||||||
err = doctor.AddSubcommand(app, os.Stdout)
|
err = doctor.AddSubcommand(app, os.Stdout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fatal(err.Error())
|
fatal(err.Error())
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
var version = "v2.0.0-alpha.6"
|
var version = "v2.0.0-alpha.9"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/wailsapp/wails/v2/internal/binding"
|
"github.com/wailsapp/wails/v2/internal/binding"
|
||||||
"github.com/wailsapp/wails/v2/internal/ffenestri"
|
"github.com/wailsapp/wails/v2/internal/ffenestri"
|
||||||
"github.com/wailsapp/wails/v2/internal/logger"
|
"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/messagedispatcher"
|
||||||
"github.com/wailsapp/wails/v2/internal/runtime"
|
"github.com/wailsapp/wails/v2/internal/runtime"
|
||||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||||
@@ -23,15 +24,15 @@ type App struct {
|
|||||||
options *options.App
|
options *options.App
|
||||||
|
|
||||||
// Subsystems
|
// Subsystems
|
||||||
log *subsystem.Log
|
log *subsystem.Log
|
||||||
runtime *subsystem.Runtime
|
runtime *subsystem.Runtime
|
||||||
event *subsystem.Event
|
event *subsystem.Event
|
||||||
binding *subsystem.Binding
|
binding *subsystem.Binding
|
||||||
call *subsystem.Call
|
call *subsystem.Call
|
||||||
menu *subsystem.Menu
|
menu *subsystem.Menu
|
||||||
tray *subsystem.Tray
|
dispatcher *messagedispatcher.Dispatcher
|
||||||
contextmenus *subsystem.ContextMenus
|
|
||||||
dispatcher *messagedispatcher.Dispatcher
|
menuManager *menumanager.Manager
|
||||||
|
|
||||||
// Indicates if the app is in debug mode
|
// Indicates if the app is in debug mode
|
||||||
debug bool
|
debug bool
|
||||||
@@ -54,13 +55,32 @@ func CreateApp(appoptions *options.App) (*App, error) {
|
|||||||
myLogger := logger.New(appoptions.Logger)
|
myLogger := logger.New(appoptions.Logger)
|
||||||
myLogger.SetLogLevel(appoptions.LogLevel)
|
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{
|
result := &App{
|
||||||
window: window,
|
window: window,
|
||||||
servicebus: servicebus.New(myLogger),
|
servicebus: servicebus.New(myLogger),
|
||||||
logger: myLogger,
|
logger: myLogger,
|
||||||
bindings: binding.NewBindings(myLogger),
|
bindings: binding.NewBindings(myLogger),
|
||||||
|
menuManager: menuManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
result.options = appoptions
|
result.options = appoptions
|
||||||
@@ -92,12 +112,7 @@ func (a *App) Run() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the runtime
|
runtimesubsystem, err := subsystem.NewRuntime(a.servicebus, a.logger)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -155,43 +170,15 @@ func (a *App) Run() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionally start the menu subsystem
|
// Start the menu subsystem
|
||||||
if applicationMenu != nil {
|
menusubsystem, err := subsystem.NewMenu(a.servicebus, a.logger, a.menuManager)
|
||||||
menusubsystem, err := subsystem.NewMenu(applicationMenu, a.servicebus, a.logger)
|
if err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
a.menu = menusubsystem
|
|
||||||
err = a.menu.Start()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
a.menu = menusubsystem
|
||||||
// Optionally start the tray subsystem
|
err = a.menu.Start()
|
||||||
if trayMenu != nil {
|
if err != nil {
|
||||||
traysubsystem, err := subsystem.NewTray(trayMenu, a.servicebus, a.logger)
|
return err
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the call subsystem
|
// Start the call subsystem
|
||||||
|
|||||||
88
v2/internal/ffenestri/common.c
Normal file
88
v2/internal/ffenestri/common.c
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
38
v2/internal/ffenestri/common.h
Normal file
38
v2/internal/ffenestri/common.h
Normal 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
|
||||||
99
v2/internal/ffenestri/contextmenus_darwin.c
Normal file
99
v2/internal/ffenestri/contextmenus_darwin.c
Normal 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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
33
v2/internal/ffenestri/contextmenus_darwin.h
Normal file
33
v2/internal/ffenestri/contextmenus_darwin.h
Normal 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
|
||||||
65
v2/internal/ffenestri/contextmenustore_darwin.c
Normal file
65
v2/internal/ffenestri/contextmenustore_darwin.c
Normal 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);
|
||||||
|
|
||||||
|
}
|
||||||
27
v2/internal/ffenestri/contextmenustore_darwin.h
Normal file
27
v2/internal/ffenestri/contextmenustore_darwin.h
Normal 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
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package ffenestri
|
package ffenestri
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/wailsapp/wails/v2/internal/menumanager"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
@@ -31,7 +32,10 @@ type Application struct {
|
|||||||
memory []unsafe.Pointer
|
memory []unsafe.Pointer
|
||||||
|
|
||||||
// This is the main app pointer
|
// This is the main app pointer
|
||||||
app unsafe.Pointer
|
app *C.struct_Application
|
||||||
|
|
||||||
|
// Manages menus
|
||||||
|
menuManager *menumanager.Manager
|
||||||
|
|
||||||
// Logger
|
// Logger
|
||||||
logger logger.CustomLogger
|
logger logger.CustomLogger
|
||||||
@@ -52,10 +56,11 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewApplicationWithConfig creates a new application based on the given config
|
// 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{
|
return &Application{
|
||||||
config: config,
|
config: config,
|
||||||
logger: logger.CustomLogger("Ffenestri"),
|
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)
|
app := C.NewApplication(title, width, height, resizable, devtools, fullscreen, startHidden, logLevel)
|
||||||
|
|
||||||
// Save app reference
|
// Save app reference
|
||||||
a.app = unsafe.Pointer(app)
|
a.app = (*C.struct_Application)(app)
|
||||||
|
|
||||||
// Set Min Window Size
|
// Set Min Window Size
|
||||||
minWidth := C.int(a.config.MinWidth)
|
minWidth := C.int(a.config.MinWidth)
|
||||||
@@ -152,7 +157,10 @@ func (a *Application) Run(incomingDispatcher Dispatcher, bindings string, debug
|
|||||||
dispatcher = incomingDispatcher.RegisterClient(newClient(a))
|
dispatcher = incomingDispatcher.RegisterClient(newClient(a))
|
||||||
|
|
||||||
// Process platform settings
|
// Process platform settings
|
||||||
a.processPlatformSettings()
|
err := a.processPlatformSettings()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Check we could initialise the application
|
// Check we could initialise the application
|
||||||
if app != nil {
|
if app != nil {
|
||||||
|
|||||||
@@ -2,41 +2,42 @@
|
|||||||
#define __FFENESTRI_H__
|
#define __FFENESTRI_H__
|
||||||
|
|
||||||
#include <stdio.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 struct Application *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 SetMinWindowSize(struct Application*, int minWidth, int minHeight);
|
||||||
extern void SetMaxWindowSize(void *app, int maxWidth, int maxHeight);
|
extern void SetMaxWindowSize(struct Application*, int maxWidth, int maxHeight);
|
||||||
extern void Run(void *app, int argc, char **argv);
|
extern void Run(struct Application*, int argc, char **argv);
|
||||||
extern void DestroyApplication(void *app);
|
extern void DestroyApplication(struct Application*);
|
||||||
extern void SetDebug(void *app, int flag);
|
extern void SetDebug(struct Application*, int flag);
|
||||||
extern void SetBindings(void *app, const char *bindings);
|
extern void SetBindings(struct Application*, const char *bindings);
|
||||||
extern void ExecJS(void *app, const char *script);
|
extern void ExecJS(struct Application*, const char *script);
|
||||||
extern void Hide(void *app);
|
extern void Hide(struct Application*);
|
||||||
extern void Show(void *app);
|
extern void Show(struct Application*);
|
||||||
extern void Center(void *app);
|
extern void Center(struct Application*);
|
||||||
extern void Maximise(void *app);
|
extern void Maximise(struct Application*);
|
||||||
extern void Unmaximise(void *app);
|
extern void Unmaximise(struct Application*);
|
||||||
extern void ToggleMaximise(void *app);
|
extern void ToggleMaximise(struct Application*);
|
||||||
extern void Minimise(void *app);
|
extern void Minimise(struct Application*);
|
||||||
extern void Unminimise(void *app);
|
extern void Unminimise(struct Application*);
|
||||||
extern void ToggleMinimise(void *app);
|
extern void ToggleMinimise(struct Application*);
|
||||||
extern void SetColour(void *app, int red, int green, int blue, int alpha);
|
extern void SetColour(struct Application*, int red, int green, int blue, int alpha);
|
||||||
extern void SetSize(void *app, int width, int height);
|
extern void SetSize(struct Application*, int width, int height);
|
||||||
extern void SetPosition(void *app, int x, int y);
|
extern void SetPosition(struct Application*, int x, int y);
|
||||||
extern void Quit(void *app);
|
extern void Quit(struct Application*);
|
||||||
extern void SetTitle(void *app, const char *title);
|
extern void SetTitle(struct Application*, const char *title);
|
||||||
extern void Fullscreen(void *app);
|
extern void Fullscreen(struct Application*);
|
||||||
extern void UnFullscreen(void *app);
|
extern void UnFullscreen(struct Application*);
|
||||||
extern void ToggleFullscreen(void *app);
|
extern void ToggleFullscreen(struct Application*);
|
||||||
extern void DisableFrame(void *app);
|
extern void DisableFrame(struct Application*);
|
||||||
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 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(void *appPointer, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int showHiddenFiles, int canCreateDirectories, 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(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 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(void *appPointer, char *callbackID);
|
extern void DarkModeEnabled(struct Application*, char *callbackID);
|
||||||
extern void UpdateMenu(void *app, char *menuAsJSON);
|
extern void SetApplicationMenu(struct Application*, const char *);
|
||||||
extern void UpdateTray(void *app, char *menuAsJSON);
|
extern void AddTrayMenu(struct Application*, const char *menuTrayJSON);
|
||||||
extern void UpdateContextMenus(void *app, char *contextMenusAsJSON);
|
extern void UpdateTrayMenu(struct Application*, const char *menuTrayJSON);
|
||||||
extern void UpdateTrayLabel(void *app, const char *label);
|
extern void AddContextMenu(struct Application*, char *contextMenuJSON);
|
||||||
extern void UpdateTrayIcon(void *app, const char *label);
|
extern void UpdateContextMenu(struct Application*, char *contextMenuJSON);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ package ffenestri
|
|||||||
import "C"
|
import "C"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v2/internal/logger"
|
"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))
|
C.DarkModeEnabled(c.app.app, c.app.string2CString(callbackID))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) UpdateMenu(menu *menu.Menu) {
|
func (c *Client) SetApplicationMenu(applicationMenuJSON string) {
|
||||||
|
C.SetApplicationMenu(c.app.app, c.app.string2CString(applicationMenuJSON))
|
||||||
// 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) UpdateTray(menu *menu.Menu) {
|
func (c *Client) UpdateTrayMenu(trayMenuJSON string) {
|
||||||
|
C.UpdateTrayMenu(c.app.app, c.app.string2CString(trayMenuJSON))
|
||||||
// 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) UpdateTrayLabel(label string) {
|
func (c *Client) UpdateContextMenu(contextMenuJSON string) {
|
||||||
C.UpdateTrayLabel(c.app.app, c.app.string2CString(label))
|
C.UpdateContextMenu(c.app.app, c.app.string2CString(contextMenuJSON))
|
||||||
}
|
|
||||||
|
|
||||||
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)))
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,15 +4,11 @@ package ffenestri
|
|||||||
#cgo darwin CFLAGS: -DFFENESTRI_DARWIN=1
|
#cgo darwin CFLAGS: -DFFENESTRI_DARWIN=1
|
||||||
#cgo darwin LDFLAGS: -framework WebKit -lobjc
|
#cgo darwin LDFLAGS: -framework WebKit -lobjc
|
||||||
|
|
||||||
|
#include "ffenestri.h"
|
||||||
#include "ffenestri_darwin.h"
|
#include "ffenestri_darwin.h"
|
||||||
|
|
||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/wailsapp/wails/v2/pkg/options"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (a *Application) processPlatformSettings() error {
|
func (a *Application) processPlatformSettings() error {
|
||||||
|
|
||||||
@@ -63,54 +59,32 @@ func (a *Application) processPlatformSettings() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process menu
|
// Process menu
|
||||||
applicationMenu := options.GetApplicationMenu(a.config)
|
//applicationMenu := options.GetApplicationMenu(a.config)
|
||||||
if applicationMenu != nil {
|
applicationMenu := a.menuManager.GetApplicationMenuJSON()
|
||||||
|
if applicationMenu != "" {
|
||||||
/*
|
C.SetApplicationMenu(a.app, a.string2CString(applicationMenu))
|
||||||
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)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process tray
|
// Process tray
|
||||||
tray := options.GetTray(a.config)
|
trays, err := a.menuManager.GetTrayMenus()
|
||||||
if tray != nil {
|
if err != nil {
|
||||||
|
return err
|
||||||
/*
|
}
|
||||||
As radio groups need to be manually managed on OSX,
|
if trays != nil {
|
||||||
we preprocess the menu to determine the radio groups.
|
for _, tray := range trays {
|
||||||
This is defined as any adjacent menu item of type "RadioType".
|
C.AddTrayMenu(a.app, a.string2CString(tray))
|
||||||
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
|
|
||||||
}
|
}
|
||||||
C.SetTray(a.app, a.string2CString(string(trayMenuJSON)), a.string2CString(tray.Label), a.string2CString(tray.Icon))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process context menus
|
// Process context menus
|
||||||
contextMenus := options.GetContextMenus(a.config)
|
contextMenus, err := a.menuManager.GetContextMenus()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if contextMenus != nil {
|
if contextMenus != nil {
|
||||||
contextMenusJSON, err := json.Marshal(contextMenus)
|
for _, contextMenu := range contextMenus {
|
||||||
fmt.Printf("\n\nCONTEXT MENUS:\n %+v\n\n", string(contextMenusJSON))
|
C.AddContextMenu(a.app, a.string2CString(contextMenu))
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
C.SetContextMenus(a.app, a.string2CString(string(contextMenusJSON)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -2,18 +2,112 @@
|
|||||||
#ifndef FFENESTRI_DARWIN_H
|
#ifndef FFENESTRI_DARWIN_H
|
||||||
#define FFENESTRI_DARWIN_H
|
#define FFENESTRI_DARWIN_H
|
||||||
|
|
||||||
extern void TitlebarAppearsTransparent(void *);
|
|
||||||
extern void HideTitle(void *);
|
#define OBJC_OLD_DISPATCH_PROTOTYPES 1
|
||||||
extern void HideTitleBar(void *);
|
#include <objc/objc-runtime.h>
|
||||||
extern void FullSizeContent(void *);
|
#include <CoreGraphics/CoreGraphics.h>
|
||||||
extern void UseToolbar(void *);
|
#include "json.h"
|
||||||
extern void HideToolbarSeparator(void *);
|
#include "hashmap.h"
|
||||||
extern void DisableFrame(void *);
|
#include "stdlib.h"
|
||||||
extern void SetAppearance(void *, const char *);
|
|
||||||
extern void WebviewIsTransparent(void *);
|
// Macros to make it slightly more sane
|
||||||
extern void WindowBackgroundIsTranslucent(void *);
|
#define msg objc_msgSend
|
||||||
extern void SetMenu(void *, const char *);
|
|
||||||
extern void SetTray(void *, const char *, const char *, const char *);
|
#define c(str) (id)objc_getClass(str)
|
||||||
extern void SetContextMenus(void *, const char *);
|
#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
|
#endif
|
||||||
@@ -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{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
808
v2/internal/ffenestri/menu_darwin.c
Normal file
808
v2/internal/ffenestri/menu_darwin.c
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
100
v2/internal/ffenestri/menu_darwin.h
Normal file
100
v2/internal/ffenestri/menu_darwin.h
Normal 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
|
||||||
199
v2/internal/ffenestri/traymenu_darwin.c
Normal file
199
v2/internal/ffenestri/traymenu_darwin.c
Normal 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);
|
||||||
|
}
|
||||||
38
v2/internal/ffenestri/traymenu_darwin.h
Normal file
38
v2/internal/ffenestri/traymenu_darwin.h
Normal 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
|
||||||
107
v2/internal/ffenestri/traymenustore_darwin.c
Normal file
107
v2/internal/ffenestri/traymenustore_darwin.c
Normal 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);
|
||||||
|
|
||||||
|
}
|
||||||
25
v2/internal/ffenestri/traymenustore_darwin.h
Normal file
25
v2/internal/ffenestri/traymenustore_darwin.h
Normal 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
|
||||||
46
v2/internal/menumanager/applicationmenu.go
Normal file
46
v2/internal/menumanager/applicationmenu.go
Normal 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
|
||||||
|
}
|
||||||
60
v2/internal/menumanager/contextmenu.go
Normal file
60
v2/internal/menumanager/contextmenu.go
Normal 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()
|
||||||
|
}
|
||||||
72
v2/internal/menumanager/menuitemmap.go
Normal file
72
v2/internal/menumanager/menuitemmap.go
Normal 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]
|
||||||
|
}
|
||||||
90
v2/internal/menumanager/menumanager.go
Normal file
90
v2/internal/menumanager/menumanager.go
Normal 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
|
||||||
|
}
|
||||||
156
v2/internal/menumanager/processedMenu.go
Normal file
156
v2/internal/menumanager/processedMenu.go
Normal 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{}
|
||||||
|
}
|
||||||
|
}
|
||||||
105
v2/internal/menumanager/traymenu.go
Normal file
105
v2/internal/menumanager/traymenu.go
Normal 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
|
||||||
|
}
|
||||||
@@ -2,8 +2,6 @@ package messagedispatcher
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v2/internal/logger"
|
"github.com/wailsapp/wails/v2/internal/logger"
|
||||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
|
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
|
||||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||||
@@ -32,11 +30,9 @@ type Client interface {
|
|||||||
WindowUnFullscreen()
|
WindowUnFullscreen()
|
||||||
WindowSetColour(colour int)
|
WindowSetColour(colour int)
|
||||||
DarkModeEnabled(callbackID string)
|
DarkModeEnabled(callbackID string)
|
||||||
UpdateMenu(menu *menu.Menu)
|
SetApplicationMenu(menuJSON string)
|
||||||
UpdateTray(menu *menu.Menu)
|
UpdateTrayMenu(trayMenuJSON string)
|
||||||
UpdateTrayLabel(label string)
|
UpdateContextMenu(contextMenuJSON string)
|
||||||
UpdateTrayIcon(name string)
|
|
||||||
UpdateContextMenus(contextMenus *menu.ContextMenus)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DispatchClient is what the frontends use to interface with the
|
// DispatchClient is what the frontends use to interface with the
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ var messageParsers = map[byte]func(string) (*parsedMessage, error){
|
|||||||
// Parse will attempt to parse the given message
|
// Parse will attempt to parse the given message
|
||||||
func Parse(message string) (*parsedMessage, error) {
|
func Parse(message string) (*parsedMessage, error) {
|
||||||
|
|
||||||
|
if len(message) == 0 {
|
||||||
|
return nil, fmt.Errorf("MessageParser received blank message");
|
||||||
|
}
|
||||||
|
|
||||||
parseMethod := messageParsers[message[0]]
|
parseMethod := messageParsers[message[0]]
|
||||||
if parseMethod == nil {
|
if parseMethod == nil {
|
||||||
return nil, fmt.Errorf("message type '%c' invalid", message[0])
|
return nil, fmt.Errorf("message type '%c' invalid", message[0])
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package messagedispatcher
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -17,16 +16,14 @@ import (
|
|||||||
// Dispatcher translates messages received from the frontend
|
// Dispatcher translates messages received from the frontend
|
||||||
// and publishes them onto the service bus
|
// and publishes them onto the service bus
|
||||||
type Dispatcher struct {
|
type Dispatcher struct {
|
||||||
quitChannel <-chan *servicebus.Message
|
quitChannel <-chan *servicebus.Message
|
||||||
resultChannel <-chan *servicebus.Message
|
resultChannel <-chan *servicebus.Message
|
||||||
eventChannel <-chan *servicebus.Message
|
eventChannel <-chan *servicebus.Message
|
||||||
windowChannel <-chan *servicebus.Message
|
windowChannel <-chan *servicebus.Message
|
||||||
dialogChannel <-chan *servicebus.Message
|
dialogChannel <-chan *servicebus.Message
|
||||||
systemChannel <-chan *servicebus.Message
|
systemChannel <-chan *servicebus.Message
|
||||||
menuChannel <-chan *servicebus.Message
|
menuChannel <-chan *servicebus.Message
|
||||||
contextMenuChannel <-chan *servicebus.Message
|
running bool
|
||||||
trayChannel <-chan *servicebus.Message
|
|
||||||
running bool
|
|
||||||
|
|
||||||
servicebus *servicebus.ServiceBus
|
servicebus *servicebus.ServiceBus
|
||||||
logger logger.CustomLogger
|
logger logger.CustomLogger
|
||||||
@@ -78,29 +75,17 @@ func New(servicebus *servicebus.ServiceBus, logger *logger.Logger) (*Dispatcher,
|
|||||||
return nil, err
|
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{
|
result := &Dispatcher{
|
||||||
servicebus: servicebus,
|
servicebus: servicebus,
|
||||||
eventChannel: eventChannel,
|
eventChannel: eventChannel,
|
||||||
logger: logger.CustomLogger("Message Dispatcher"),
|
logger: logger.CustomLogger("Message Dispatcher"),
|
||||||
clients: make(map[string]*DispatchClient),
|
clients: make(map[string]*DispatchClient),
|
||||||
resultChannel: resultChannel,
|
resultChannel: resultChannel,
|
||||||
quitChannel: quitChannel,
|
quitChannel: quitChannel,
|
||||||
windowChannel: windowChannel,
|
windowChannel: windowChannel,
|
||||||
dialogChannel: dialogChannel,
|
dialogChannel: dialogChannel,
|
||||||
systemChannel: systemChannel,
|
systemChannel: systemChannel,
|
||||||
menuChannel: menuChannel,
|
menuChannel: menuChannel,
|
||||||
trayChannel: trayChannel,
|
|
||||||
contextMenuChannel: contextMenuChannel,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
@@ -132,10 +117,6 @@ func (d *Dispatcher) Start() error {
|
|||||||
d.processSystemMessage(systemMessage)
|
d.processSystemMessage(systemMessage)
|
||||||
case menuMessage := <-d.menuChannel:
|
case menuMessage := <-d.menuChannel:
|
||||||
d.processMenuMessage(menuMessage)
|
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]
|
command := splitTopic[1]
|
||||||
switch command {
|
switch command {
|
||||||
case "update":
|
case "updateappmenu":
|
||||||
|
|
||||||
updatedMenu, ok := result.Data().(*menu.Menu)
|
updatedMenu, ok := result.Data().(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
d.logger.Error("Invalid data for 'menufrontend:update' : %#v",
|
d.logger.Error("Invalid data for 'menufrontend:updateappmenu' : %#v",
|
||||||
result.Data())
|
result.Data())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -461,94 +442,34 @@ func (d *Dispatcher) processMenuMessage(result *servicebus.Message) {
|
|||||||
// TODO: Work out what we mean in a multi window environment...
|
// TODO: Work out what we mean in a multi window environment...
|
||||||
// For now we will just pick the first one
|
// For now we will just pick the first one
|
||||||
for _, client := range d.clients {
|
for _, client := range d.clients {
|
||||||
client.frontend.UpdateMenu(updatedMenu)
|
client.frontend.SetApplicationMenu(updatedMenu)
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
case "updatetraymenu":
|
||||||
d.logger.Error("Unknown menufrontend command: %s", command)
|
updatedTrayMenu, ok := result.Data().(string)
|
||||||
}
|
if !ok {
|
||||||
}
|
d.logger.Error("Invalid data for 'menufrontend:updatetraymenu' : %#v",
|
||||||
func (d *Dispatcher) processContextMenuMessage(result *servicebus.Message) {
|
result.Data())
|
||||||
splitTopic := strings.Split(result.Topic(), ":")
|
return
|
||||||
if len(splitTopic) < 2 {
|
}
|
||||||
d.logger.Error("Invalid contextmenu message : %#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 {
|
||||||
command := splitTopic[1]
|
client.frontend.UpdateTrayMenu(updatedTrayMenu)
|
||||||
switch command {
|
}
|
||||||
case "update":
|
case "updatecontextmenu":
|
||||||
|
updatedContextMenu, ok := result.Data().(string)
|
||||||
updatedContextMenus, ok := result.Data().(*menu.ContextMenus)
|
if !ok {
|
||||||
if !ok {
|
d.logger.Error("Invalid data for 'menufrontend:updatecontextmenu' : %#v",
|
||||||
d.logger.Error("Invalid data for 'contextmenufrontend:update' : %#v",
|
result.Data())
|
||||||
result.Data())
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
// TODO: Work out what we mean in a multi window environment...
|
||||||
// TODO: Work out what we mean in a multi window environment...
|
// For now we will just pick the first one
|
||||||
// For now we will just pick the first one
|
for _, client := range d.clients {
|
||||||
for _, client := range d.clients {
|
client.frontend.UpdateContextMenu(updatedContextMenu)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -1,48 +1,36 @@
|
|||||||
package runtime
|
package runtime
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
|
|
||||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Menu defines all Menu related operations
|
// Menu defines all Menu related operations
|
||||||
type Menu interface {
|
type Menu interface {
|
||||||
On(menuID string, callback func(*menu.MenuItem))
|
UpdateApplicationMenu()
|
||||||
Update()
|
UpdateContextMenu(contextMenu *menu.ContextMenu)
|
||||||
GetByID(menuID string) *menu.MenuItem
|
UpdateTrayMenu(trayMenu *menu.TrayMenu)
|
||||||
RemoveByID(id string) bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type menuRuntime struct {
|
type menuRuntime struct {
|
||||||
bus *servicebus.ServiceBus
|
bus *servicebus.ServiceBus
|
||||||
menu *menu.Menu
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// newMenu creates a new Menu struct
|
// newMenu creates a new Menu struct
|
||||||
func newMenu(bus *servicebus.ServiceBus, menu *menu.Menu) Menu {
|
func newMenu(bus *servicebus.ServiceBus) Menu {
|
||||||
return &menuRuntime{
|
return &menuRuntime{
|
||||||
bus: bus,
|
bus: bus,
|
||||||
menu: menu,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// On registers a listener for a particular event
|
func (m *menuRuntime) UpdateApplicationMenu() {
|
||||||
func (m *menuRuntime) On(menuID string, callback func(*menu.MenuItem)) {
|
m.bus.Publish("menu:updateappmenu", nil)
|
||||||
m.bus.Publish("menu:on", &message.MenuOnMessage{
|
|
||||||
MenuID: menuID,
|
|
||||||
Callback: callback,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *menuRuntime) Update() {
|
func (m *menuRuntime) UpdateContextMenu(contextMenu *menu.ContextMenu) {
|
||||||
m.bus.Publish("menu:update", m.menu)
|
m.bus.Publish("menu:updatecontextmenu", contextMenu)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *menuRuntime) GetByID(menuID string) *menu.MenuItem {
|
func (m *menuRuntime) UpdateTrayMenu(trayMenu *menu.TrayMenu) {
|
||||||
return m.menu.GetByID(menuID)
|
m.bus.Publish("menu:updatetraymenu", trayMenu)
|
||||||
}
|
|
||||||
|
|
||||||
func (m *menuRuntime) RemoveByID(id string) bool {
|
|
||||||
return m.menu.RemoveByID(id)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,37 +2,32 @@ package runtime
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
"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
|
// Runtime is a means for the user to interact with the application at runtime
|
||||||
type Runtime struct {
|
type Runtime struct {
|
||||||
Browser Browser
|
Browser Browser
|
||||||
Events Events
|
Events Events
|
||||||
Window Window
|
Window Window
|
||||||
Dialog Dialog
|
Dialog Dialog
|
||||||
System System
|
System System
|
||||||
Menu Menu
|
Menu Menu
|
||||||
ContextMenu ContextMenus
|
Store *StoreProvider
|
||||||
Tray Tray
|
Log Log
|
||||||
Store *StoreProvider
|
bus *servicebus.ServiceBus
|
||||||
Log Log
|
|
||||||
bus *servicebus.ServiceBus
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new runtime
|
// 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{
|
result := &Runtime{
|
||||||
Browser: newBrowser(),
|
Browser: newBrowser(),
|
||||||
Events: newEvents(serviceBus),
|
Events: newEvents(serviceBus),
|
||||||
Window: newWindow(serviceBus),
|
Window: newWindow(serviceBus),
|
||||||
Dialog: newDialog(serviceBus),
|
Dialog: newDialog(serviceBus),
|
||||||
System: newSystem(serviceBus),
|
System: newSystem(serviceBus),
|
||||||
Menu: newMenu(serviceBus, menu),
|
Menu: newMenu(serviceBus),
|
||||||
Tray: newTray(serviceBus, trayMenu),
|
Log: newLog(serviceBus),
|
||||||
ContextMenu: newContextMenus(serviceBus, contextMenus),
|
bus: serviceBus,
|
||||||
Log: newLog(serviceBus),
|
|
||||||
bus: serviceBus,
|
|
||||||
}
|
}
|
||||||
result.Store = newStore(result)
|
result.Store = newStore(result)
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
@@ -1,25 +1,15 @@
|
|||||||
package subsystem
|
package subsystem
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v2/internal/logger"
|
"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/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
|
// Menu is the subsystem that handles the operation of menus. It manages all service bus messages
|
||||||
// starting with "menu".
|
// starting with "menu".
|
||||||
type Menu struct {
|
type Menu struct {
|
||||||
@@ -27,23 +17,18 @@ type Menu struct {
|
|||||||
menuChannel <-chan *servicebus.Message
|
menuChannel <-chan *servicebus.Message
|
||||||
running bool
|
running bool
|
||||||
|
|
||||||
// Event listeners
|
|
||||||
listeners map[string][]func(*menu.MenuItem)
|
|
||||||
menuItems map[string]*menu.MenuItem
|
|
||||||
notifyLock sync.RWMutex
|
|
||||||
|
|
||||||
// logger
|
// logger
|
||||||
logger logger.CustomLogger
|
logger logger.CustomLogger
|
||||||
|
|
||||||
// The application menu
|
|
||||||
applicationMenu *menu.Menu
|
|
||||||
|
|
||||||
// Service Bus
|
// Service Bus
|
||||||
bus *servicebus.ServiceBus
|
bus *servicebus.ServiceBus
|
||||||
|
|
||||||
|
// Menu Manager
|
||||||
|
menuManager *menumanager.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMenu creates a new menu subsystem
|
// 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
|
// Register quit channel
|
||||||
quitChannel, err := bus.Subscribe("quit")
|
quitChannel, err := bus.Subscribe("quit")
|
||||||
@@ -58,18 +43,13 @@ func NewMenu(applicationMenu *menu.Menu, bus *servicebus.ServiceBus, logger *log
|
|||||||
}
|
}
|
||||||
|
|
||||||
result := &Menu{
|
result := &Menu{
|
||||||
quitChannel: quitChannel,
|
quitChannel: quitChannel,
|
||||||
menuChannel: menuChannel,
|
menuChannel: menuChannel,
|
||||||
logger: logger.CustomLogger("Menu Subsystem"),
|
logger: logger.CustomLogger("Menu Subsystem"),
|
||||||
listeners: make(map[string][]func(*menu.MenuItem)),
|
bus: bus,
|
||||||
menuItems: make(map[string]*menu.MenuItem),
|
menuManager: menuManager,
|
||||||
applicationMenu: applicationMenu,
|
|
||||||
bus: bus,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build up list of item/id pairs
|
|
||||||
result.processMenu(applicationMenu)
|
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,35 +77,59 @@ func (m *Menu) Start() error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
m.logger.Trace("Got Menu clicked Message: %s %+v", menuMessage.Topic(), menuMessage.Data())
|
m.logger.Trace("Got Menu clicked Message: %s %+v", menuMessage.Topic(), menuMessage.Data())
|
||||||
menuid := menuMessage.Data().(string)
|
|
||||||
|
|
||||||
// Get the menu item
|
type ClickCallbackMessage struct {
|
||||||
menuItem := m.menuItems[menuid]
|
MenuItemID string `json:"menuItemID"`
|
||||||
if menuItem == nil {
|
MenuType string `json:"menuType"`
|
||||||
m.logger.Trace("Cannot process menuid %s - unknown", menuid)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is the menu item a checkbox?
|
err = m.menuManager.ProcessClick(callbackData.MenuItemID, callbackData.Data, callbackData.MenuType, callbackData.ParentID)
|
||||||
if menuItem.Type == menu.CheckboxType {
|
if err != nil {
|
||||||
// Toggle state
|
m.logger.Trace("%s", err.Error())
|
||||||
menuItem.Checked = !menuItem.Checked
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// Make sure we catch any menu updates
|
||||||
case "update":
|
case "updateappmenu":
|
||||||
updatedMenu := menuMessage.Data().(*menu.Menu)
|
updatedMenu, err := m.menuManager.UpdateApplicationMenu()
|
||||||
m.processMenu(updatedMenu)
|
if err != nil {
|
||||||
|
m.logger.Trace("%s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Notify frontend of menu change
|
// 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:
|
default:
|
||||||
m.logger.Error("unknown menu message: %+v", menuMessage)
|
m.logger.Error("unknown menu message: %+v", menuMessage)
|
||||||
@@ -140,56 +144,6 @@ func (m *Menu) Start() error {
|
|||||||
return nil
|
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() {
|
func (m *Menu) shutdown() {
|
||||||
m.logger.Trace("Shutdown")
|
m.logger.Trace("Shutdown")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package subsystem
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v2/internal/logger"
|
"github.com/wailsapp/wails/v2/internal/logger"
|
||||||
@@ -24,7 +23,7 @@ type Runtime struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewRuntime creates a new runtime subsystem
|
// 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
|
// Register quit channel
|
||||||
quitChannel, err := bus.Subscribe("quit")
|
quitChannel, err := bus.Subscribe("quit")
|
||||||
@@ -42,7 +41,7 @@ func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger, menu *menu.Me
|
|||||||
quitChannel: quitChannel,
|
quitChannel: quitChannel,
|
||||||
runtimeChannel: runtimeChannel,
|
runtimeChannel: runtimeChannel,
|
||||||
logger: logger.CustomLogger("Runtime Subsystem"),
|
logger: logger.CustomLogger("Runtime Subsystem"),
|
||||||
runtime: runtime.New(bus, menu, trayMenu, contextMenus),
|
runtime: runtime.New(bus),
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
|
|||||||
@@ -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")
|
|
||||||
}
|
|
||||||
@@ -148,6 +148,12 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
|
|||||||
// Default go build command
|
// Default go build command
|
||||||
commands := slicer.String([]string{"build"})
|
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
|
// TODO: Work out if we can make this more efficient
|
||||||
// We need to do a full build as CGO doesn't detect updates
|
// 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
|
// to .h files, and we package assets into .h file. We could
|
||||||
@@ -205,6 +211,7 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
|
|||||||
options.CompiledBinary = compiledBinary
|
options.CompiledBinary = compiledBinary
|
||||||
|
|
||||||
// Create the command
|
// Create the command
|
||||||
|
fmt.Printf("Compile command: %+v", commands.AsSlice())
|
||||||
cmd := exec.Command(options.Compiler, commands.AsSlice()...)
|
cmd := exec.Command(options.Compiler, commands.AsSlice()...)
|
||||||
|
|
||||||
// Set the directory
|
// Set the directory
|
||||||
|
|||||||
@@ -63,6 +63,14 @@ func (d *DesktopBuilder) BuildBaseAssets(assets *html.AssetBundle, options *Opti
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make dir if it doesn't exist
|
||||||
|
if !fs.DirExists(assetDir) {
|
||||||
|
err := fs.Mkdir(assetDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Dump assets as C
|
// Dump assets as C
|
||||||
assetsFile, err := assets.WriteToCFile(assetDir)
|
assetsFile, err := assets.WriteToCFile(assetDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
8
v2/pkg/menu/callback.go
Normal file
8
v2/pkg/menu/callback.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package menu
|
||||||
|
|
||||||
|
type CallbackData struct {
|
||||||
|
MenuItem *MenuItem
|
||||||
|
ContextData string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Callback func(*CallbackData)
|
||||||
@@ -1,38 +1,13 @@
|
|||||||
package menu
|
package menu
|
||||||
|
|
||||||
type ContextMenus struct {
|
type ContextMenu struct {
|
||||||
Items map[string]*Menu
|
ID string
|
||||||
|
Menu *Menu
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewContextMenus() *ContextMenus {
|
func NewContextMenu(ID string, menu *Menu) *ContextMenu {
|
||||||
return &ContextMenus{
|
return &ContextMenu{
|
||||||
Items: make(map[string]*Menu),
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ func (m *Menu) Append(item *MenuItem) {
|
|||||||
m.Items = append(m.Items, item)
|
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 {
|
func NewMenuFromItems(first *MenuItem, rest ...*MenuItem) *Menu {
|
||||||
|
|
||||||
var result = NewMenu()
|
var result = NewMenu()
|
||||||
@@ -23,29 +27,8 @@ func NewMenuFromItems(first *MenuItem, rest ...*MenuItem) *Menu {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Menu) GetByID(menuID string) *MenuItem {
|
func (m *Menu) setParent(menuItem *MenuItem) {
|
||||||
|
|
||||||
// Loop over menu items
|
|
||||||
for _, item := range m.Items {
|
for _, item := range m.Items {
|
||||||
result := item.getByID(menuID)
|
item.parent = menuItem
|
||||||
if result != nil {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package menu
|
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
|
// MenuItem represents a menuitem contained in a menu
|
||||||
type MenuItem struct {
|
type MenuItem struct {
|
||||||
// The unique identifier of this menu item
|
|
||||||
ID string `json:"ID,omitempty"`
|
|
||||||
// Label is what appears as the menu text
|
// Label is what appears as the menu text
|
||||||
Label string
|
Label string
|
||||||
// Role is a predefined menu type
|
// 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 indicates if the item is selected (used by Checkbox and Radio types only)
|
||||||
Checked bool
|
Checked bool
|
||||||
// Submenu contains a list of menu items that will be shown as a submenu
|
// 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 colour in hex RGBA format EG: 0xFF0000FF = #FF0000FF = red
|
||||||
Foreground int
|
Foreground int
|
||||||
@@ -31,6 +36,9 @@ type MenuItem struct {
|
|||||||
|
|
||||||
// This holds the menu item's parent.
|
// This holds the menu item's parent.
|
||||||
parent *MenuItem
|
parent *MenuItem
|
||||||
|
|
||||||
|
// Used for locking when removing elements
|
||||||
|
removeLock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parent returns the parent of the menu item.
|
// Parent returns the parent of the menu item.
|
||||||
@@ -48,7 +56,7 @@ func (m *MenuItem) Append(item *MenuItem) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
item.parent = m
|
item.parent = m
|
||||||
m.SubMenu = append(m.SubMenu, item)
|
m.SubMenu.Append(item)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,43 +69,23 @@ func (m *MenuItem) Prepend(item *MenuItem) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
item.parent = m
|
item.parent = m
|
||||||
m.SubMenu = append([]*MenuItem{item}, m.SubMenu...)
|
m.SubMenu.Prepend(item)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MenuItem) getByID(id string) *MenuItem {
|
func (m *MenuItem) Remove() {
|
||||||
|
// Iterate my parent's children
|
||||||
// If I have the ID return me!
|
m.Parent().removeChild(m)
|
||||||
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) removeByID(id string) bool {
|
func (m *MenuItem) removeChild(item *MenuItem) {
|
||||||
|
m.removeLock.Lock()
|
||||||
for index, item := range m.SubMenu {
|
for index, child := range m.SubMenu.Items {
|
||||||
if item.ID == id {
|
if item == child {
|
||||||
m.SubMenu = append(m.SubMenu[:index], m.SubMenu[index+1:]...)
|
m.SubMenu.Items = append(m.SubMenu.Items[:index], m.SubMenu.Items[index+1:]...)
|
||||||
return true
|
|
||||||
}
|
|
||||||
if item.isSubMenu() {
|
|
||||||
result := item.removeByID(id)
|
|
||||||
if result == true {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
m.removeLock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// InsertAfter attempts to add the given item after this item in the parent
|
// 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
|
// hunt down that bad boy
|
||||||
for index, item := range m.SubMenu {
|
for index, item := range m.SubMenu.Items {
|
||||||
if item == target {
|
if item == target {
|
||||||
return index
|
return index
|
||||||
}
|
}
|
||||||
@@ -196,7 +184,7 @@ func (m *MenuItem) getItemIndex(target *MenuItem) int {
|
|||||||
func (m *MenuItem) insertItemAtIndex(index int, target *MenuItem) bool {
|
func (m *MenuItem) insertItemAtIndex(index int, target *MenuItem) bool {
|
||||||
|
|
||||||
// If index is OOB, return false
|
// If index is OOB, return false
|
||||||
if index > len(m.SubMenu) {
|
if index > len(m.SubMenu.Items) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,23 +192,23 @@ func (m *MenuItem) insertItemAtIndex(index int, target *MenuItem) bool {
|
|||||||
target.parent = m
|
target.parent = m
|
||||||
|
|
||||||
// If index is last item, then just regular append
|
// If index is last item, then just regular append
|
||||||
if index == len(m.SubMenu) {
|
if index == len(m.SubMenu.Items) {
|
||||||
m.SubMenu = append(m.SubMenu, target)
|
m.SubMenu.Items = append(m.SubMenu.Items, target)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
m.SubMenu = append(m.SubMenu[:index+1], m.SubMenu[index:]...)
|
m.SubMenu.Items = append(m.SubMenu.Items[:index+1], m.SubMenu.Items[index:]...)
|
||||||
m.SubMenu[index] = target
|
m.SubMenu.Items[index] = target
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text is a helper to create basic Text menu items
|
// 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{
|
return &MenuItem{
|
||||||
ID: id,
|
|
||||||
Label: label,
|
Label: label,
|
||||||
Type: TextType,
|
Type: TextType,
|
||||||
Accelerator: accelerator,
|
Accelerator: accelerator,
|
||||||
|
Click: click,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,56 +220,49 @@ func Separator() *MenuItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Radio is a helper to create basic Radio menu items with an accelerator
|
// 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{
|
return &MenuItem{
|
||||||
ID: id,
|
|
||||||
Label: label,
|
Label: label,
|
||||||
Type: RadioType,
|
Type: RadioType,
|
||||||
Checked: selected,
|
Checked: selected,
|
||||||
Accelerator: accelerator,
|
Accelerator: accelerator,
|
||||||
|
Click: click,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checkbox is a helper to create basic Checkbox menu items
|
// 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{
|
return &MenuItem{
|
||||||
ID: id,
|
|
||||||
Label: label,
|
Label: label,
|
||||||
Type: CheckboxType,
|
Type: CheckboxType,
|
||||||
Checked: checked,
|
Checked: checked,
|
||||||
Accelerator: accelerator,
|
Accelerator: accelerator,
|
||||||
|
Click: click,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubMenu is a helper to create Submenus
|
// SubMenu is a helper to create Submenus
|
||||||
func SubMenu(label string, items []*MenuItem) *MenuItem {
|
func SubMenu(label string, menu *Menu) *MenuItem {
|
||||||
result := &MenuItem{
|
result := &MenuItem{
|
||||||
Label: label,
|
Label: label,
|
||||||
SubMenu: items,
|
SubMenu: menu,
|
||||||
Type: SubmenuType,
|
Type: SubmenuType,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix up parent pointers
|
menu.setParent(result)
|
||||||
for _, item := range items {
|
|
||||||
item.parent = result
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubMenuWithID is a helper to create Submenus with an ID
|
// 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{
|
result := &MenuItem{
|
||||||
Label: label,
|
Label: label,
|
||||||
SubMenu: items,
|
SubMenu: menu,
|
||||||
ID: id,
|
|
||||||
Type: SubmenuType,
|
Type: SubmenuType,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix up parent pointers
|
menu.setParent(result)
|
||||||
for _, item := range items {
|
|
||||||
item.parent = result
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
package menu
|
package menu
|
||||||
|
|
||||||
// Tray are the options
|
// TrayMenu are the options
|
||||||
type Tray struct {
|
type TrayMenu struct {
|
||||||
|
|
||||||
// The ID of this tray
|
|
||||||
ID string
|
|
||||||
|
|
||||||
// Label is the text we wish to display in the tray
|
// Label is the text we wish to display in the tray
|
||||||
Label string
|
Label string
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ type Options struct {
|
|||||||
WebviewIsTransparent bool
|
WebviewIsTransparent bool
|
||||||
WindowBackgroundIsTranslucent bool
|
WindowBackgroundIsTranslucent bool
|
||||||
Menu *menu.Menu
|
Menu *menu.Menu
|
||||||
Tray *menu.Tray
|
TrayMenus []*menu.TrayMenu
|
||||||
ContextMenus *menu.ContextMenus
|
ContextMenus []*menu.ContextMenu
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ type App struct {
|
|||||||
StartHidden bool
|
StartHidden bool
|
||||||
DevTools bool
|
DevTools bool
|
||||||
RGBA int
|
RGBA int
|
||||||
ContextMenus *menu.ContextMenus
|
ContextMenus []*menu.ContextMenu
|
||||||
Tray *menu.Tray
|
TrayMenus []*menu.TrayMenu
|
||||||
Menu *menu.Menu
|
Menu *menu.Menu
|
||||||
Mac *mac.Options
|
Mac *mac.Options
|
||||||
Logger logger.Logger `json:"-"`
|
Logger logger.Logger `json:"-"`
|
||||||
@@ -41,25 +41,25 @@ func MergeDefaults(appoptions *App) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetTray(appoptions *App) *menu.Tray {
|
func GetTrayMenus(appoptions *App) []*menu.TrayMenu {
|
||||||
var result *menu.Tray
|
var result []*menu.TrayMenu
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "darwin":
|
case "darwin":
|
||||||
if appoptions.Mac != nil {
|
if appoptions.Mac != nil {
|
||||||
result = appoptions.Mac.Tray
|
result = appoptions.Mac.TrayMenus
|
||||||
}
|
}
|
||||||
//case "linux":
|
//case "linux":
|
||||||
// if appoptions.Linux != nil {
|
// if appoptions.Linux != nil {
|
||||||
// result = appoptions.Linux.Tray
|
// result = appoptions.Linux.TrayMenu
|
||||||
// }
|
// }
|
||||||
//case "windows":
|
//case "windows":
|
||||||
// if appoptions.Windows != nil {
|
// if appoptions.Windows != nil {
|
||||||
// result = appoptions.Windows.Tray
|
// result = appoptions.Windows.TrayMenu
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
if result == nil {
|
if result == nil {
|
||||||
result = appoptions.Tray
|
result = appoptions.TrayMenus
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -74,11 +74,11 @@ func GetApplicationMenu(appoptions *App) *menu.Menu {
|
|||||||
}
|
}
|
||||||
//case "linux":
|
//case "linux":
|
||||||
// if appoptions.Linux != nil {
|
// if appoptions.Linux != nil {
|
||||||
// result = appoptions.Linux.Tray
|
// result = appoptions.Linux.TrayMenu
|
||||||
// }
|
// }
|
||||||
//case "windows":
|
//case "windows":
|
||||||
// if appoptions.Windows != nil {
|
// if appoptions.Windows != nil {
|
||||||
// result = appoptions.Windows.Tray
|
// result = appoptions.Windows.TrayMenu
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,31 +89,26 @@ func GetApplicationMenu(appoptions *App) *menu.Menu {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetContextMenus(appoptions *App) *menu.ContextMenus {
|
func GetContextMenus(appoptions *App) []*menu.ContextMenu {
|
||||||
var result *menu.ContextMenus
|
var result []*menu.ContextMenu
|
||||||
|
|
||||||
result = appoptions.ContextMenus
|
|
||||||
var contextMenuOverrides *menu.ContextMenus
|
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "darwin":
|
case "darwin":
|
||||||
if appoptions.Mac != nil {
|
if appoptions.Mac != nil {
|
||||||
contextMenuOverrides = appoptions.Mac.ContextMenus
|
result = appoptions.Mac.ContextMenus
|
||||||
}
|
}
|
||||||
//case "linux":
|
//case "linux":
|
||||||
// if appoptions.Linux != nil {
|
// if appoptions.Linux != nil {
|
||||||
// result = appoptions.Linux.Tray
|
// result = appoptions.Linux.TrayMenu
|
||||||
// }
|
// }
|
||||||
//case "windows":
|
//case "windows":
|
||||||
// if appoptions.Windows != nil {
|
// if appoptions.Windows != nil {
|
||||||
// result = appoptions.Windows.Tray
|
// result = appoptions.Windows.TrayMenu
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overwrite defaults with OS Specific context menus
|
if result == nil {
|
||||||
if contextMenuOverrides != nil {
|
result = appoptions.ContextMenus
|
||||||
for id, contextMenu := range contextMenuOverrides.Items {
|
|
||||||
result.AddMenu(id, contextMenu)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ import (
|
|||||||
|
|
||||||
// ContextMenu struct
|
// ContextMenu struct
|
||||||
type ContextMenu struct {
|
type ContextMenu struct {
|
||||||
runtime *wails.Runtime
|
runtime *wails.Runtime
|
||||||
counter int
|
counter int
|
||||||
lock sync.Mutex
|
lock sync.Mutex
|
||||||
|
testContextMenu *menu.ContextMenu
|
||||||
|
clickedMenu *menu.MenuItem
|
||||||
}
|
}
|
||||||
|
|
||||||
// WailsInit is called at application startup
|
// WailsInit is called at application startup
|
||||||
@@ -20,21 +22,34 @@ func (c *ContextMenu) WailsInit(runtime *wails.Runtime) error {
|
|||||||
// Perform your setup here
|
// Perform your setup here
|
||||||
c.runtime = runtime
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createContextMenus() *menu.ContextMenus {
|
// Setup Menu Listeners
|
||||||
result := menu.NewContextMenus()
|
func (c *ContextMenu) updateContextMenu(_ *menu.CallbackData) {
|
||||||
result.AddMenu("test", menu.NewMenuFromItems(menu.Text("Clicked 0 times", "Test Context Menu", nil)))
|
c.lock.Lock()
|
||||||
return result
|
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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"github.com/wailsapp/wails/v2"
|
"github.com/wailsapp/wails/v2"
|
||||||
"github.com/wailsapp/wails/v2/pkg/logger"
|
"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"
|
||||||
"github.com/wailsapp/wails/v2/pkg/options/mac"
|
"github.com/wailsapp/wails/v2/pkg/options/mac"
|
||||||
"log"
|
"log"
|
||||||
@@ -11,6 +10,10 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
|
Menu := &Menu{}
|
||||||
|
Tray := &Tray{}
|
||||||
|
ContextMenu := &ContextMenu{}
|
||||||
|
|
||||||
// Create application with options
|
// Create application with options
|
||||||
app, err := wails.CreateAppWithOptions(&options.App{
|
app, err := wails.CreateAppWithOptions(&options.App{
|
||||||
Title: "Kitchen Sink",
|
Title: "Kitchen Sink",
|
||||||
@@ -21,17 +24,14 @@ func main() {
|
|||||||
//Tray: menu.NewMenuFromItems(menu.AppMenu()),
|
//Tray: menu.NewMenuFromItems(menu.AppMenu()),
|
||||||
//Menu: menu.NewMenuFromItems(menu.AppMenu()),
|
//Menu: menu.NewMenuFromItems(menu.AppMenu()),
|
||||||
//StartHidden: true,
|
//StartHidden: true,
|
||||||
ContextMenus: createContextMenus(),
|
ContextMenus: ContextMenu.createContextMenus(),
|
||||||
Mac: &mac.Options{
|
Mac: &mac.Options{
|
||||||
WebviewIsTransparent: true,
|
WebviewIsTransparent: true,
|
||||||
WindowBackgroundIsTranslucent: true,
|
WindowBackgroundIsTranslucent: true,
|
||||||
// Comment out line below to see Window.SetTitle() work
|
// Comment out line below to see Window.SetTitle() work
|
||||||
TitleBar: mac.TitleBarHiddenInset(),
|
TitleBar: mac.TitleBarHiddenInset(),
|
||||||
Menu: createApplicationMenu(),
|
Menu: Menu.createApplicationMenu(),
|
||||||
Tray: &menu.Tray{
|
TrayMenus: Tray.createTrayMenus(),
|
||||||
Icon: "light",
|
|
||||||
Menu: createApplicationTray(),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
LogLevel: logger.TRACE,
|
LogLevel: logger.TRACE,
|
||||||
})
|
})
|
||||||
@@ -46,9 +46,9 @@ func main() {
|
|||||||
app.Bind(&System{})
|
app.Bind(&System{})
|
||||||
app.Bind(&Dialog{})
|
app.Bind(&Dialog{})
|
||||||
app.Bind(&Window{})
|
app.Bind(&Window{})
|
||||||
app.Bind(&Menu{})
|
app.Bind(Menu)
|
||||||
app.Bind(&Tray{})
|
app.Bind(Tray)
|
||||||
app.Bind(&ContextMenu{})
|
app.Bind(ContextMenu)
|
||||||
|
|
||||||
err = app.Run()
|
err = app.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/wailsapp/wails/v2/pkg/menu/keys"
|
"github.com/wailsapp/wails/v2/pkg/menu/keys"
|
||||||
"math/rand"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -16,9 +15,14 @@ type Menu struct {
|
|||||||
runtime *wails.Runtime
|
runtime *wails.Runtime
|
||||||
|
|
||||||
dynamicMenuCounter int
|
dynamicMenuCounter int
|
||||||
|
dynamicMenuOneItems []*menu.MenuItem
|
||||||
lock sync.Mutex
|
lock sync.Mutex
|
||||||
dynamicMenuItems map[string]*menu.MenuItem
|
dynamicMenuItems map[string]*menu.MenuItem
|
||||||
anotherDynamicMenuCounter int
|
anotherDynamicMenuCounter int
|
||||||
|
|
||||||
|
// Menus
|
||||||
|
removeMenuItem *menu.MenuItem
|
||||||
|
dynamicMenuOneSubmenu *menu.MenuItem
|
||||||
}
|
}
|
||||||
|
|
||||||
// WailsInit is called at application startup
|
// WailsInit is called at application startup
|
||||||
@@ -26,26 +30,6 @@ func (m *Menu) WailsInit(runtime *wails.Runtime) error {
|
|||||||
// Perform your setup here
|
// Perform your setup here
|
||||||
m.runtime = runtime
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,177 +43,164 @@ func (m *Menu) decrementcounter() int {
|
|||||||
return m.dynamicMenuCounter
|
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
|
// Lock because this method will be called in a gorouting
|
||||||
m.lock.Lock()
|
m.lock.Lock()
|
||||||
defer m.lock.Unlock()
|
defer m.lock.Unlock()
|
||||||
|
|
||||||
// Get this menu's parent
|
// Get this menu's parent
|
||||||
|
mi := data.MenuItem
|
||||||
parent := mi.Parent()
|
parent := mi.Parent()
|
||||||
counter := m.incrementcounter()
|
counter := m.incrementcounter()
|
||||||
menuText := "Dynamic Menu Item " + strconv.Itoa(counter)
|
menuText := "Dynamic Menu Item " + strconv.Itoa(counter)
|
||||||
parent.Append(menu.Text(menuText, menuText, nil))
|
newDynamicMenu := menu.Text(menuText, nil, nil)
|
||||||
// parent.Append(menu.Text(menuText, menuText, menu.Key("[")))
|
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 this is the first dynamic menu added, let's add a remove menu item
|
||||||
if counter == 1 {
|
if counter == 1 {
|
||||||
removeMenu := menu.Text("Remove "+menuText,
|
m.removeMenuItem = menu.Text("Remove "+menuText, keys.CmdOrCtrl("-"), m.removeDynamicMenuOneMenu)
|
||||||
"Remove Last Item", keys.CmdOrCtrl("-"))
|
parent.Prepend(m.removeMenuItem)
|
||||||
parent.Prepend(removeMenu)
|
|
||||||
m.runtime.Menu.On("Remove Last Item", m.removeMenu)
|
|
||||||
} else {
|
} else {
|
||||||
removeMenu := m.runtime.Menu.GetByID("Remove Last Item")
|
|
||||||
// Test if the remove menu hasn't already been removed in another thread
|
// Test if the remove menu hasn't already been removed in another thread
|
||||||
if removeMenu != nil {
|
if m.removeMenuItem != nil {
|
||||||
removeMenu.Label = "Remove " + menuText
|
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
|
// Lock because this method will be called in a goroutine
|
||||||
m.lock.Lock()
|
m.lock.Lock()
|
||||||
defer m.lock.Unlock()
|
defer m.lock.Unlock()
|
||||||
|
|
||||||
// Get the id of the last dynamic menu
|
// Get the last menu we added
|
||||||
menuID := "Dynamic Menu Item " + strconv.Itoa(m.dynamicMenuCounter)
|
lastItemIndex := len(m.dynamicMenuOneItems) - 1
|
||||||
|
lastMenuAdded := m.dynamicMenuOneItems[lastItemIndex]
|
||||||
|
|
||||||
// Remove the last menu item by ID
|
// Remove from slice
|
||||||
m.runtime.Menu.RemoveByID(menuID)
|
m.dynamicMenuOneItems = m.dynamicMenuOneItems[:lastItemIndex]
|
||||||
|
|
||||||
|
// Remove the item from the menu
|
||||||
|
lastMenuAdded.Remove()
|
||||||
|
|
||||||
// Update the counter
|
// Update the counter
|
||||||
counter := m.decrementcounter()
|
counter := m.decrementcounter()
|
||||||
|
|
||||||
// If we deleted the last dynamic menu, remove the "Remove Last Item" menu
|
// If we deleted the last dynamic menu, remove the "Remove Last Item" menu
|
||||||
if counter == 0 {
|
if counter == 0 {
|
||||||
m.runtime.Menu.RemoveByID("Remove Last Item")
|
// Remove it!
|
||||||
|
m.removeMenuItem.Remove()
|
||||||
} else {
|
} else {
|
||||||
// Update label
|
// Update label
|
||||||
menuText := "Dynamic Menu Item " + strconv.Itoa(counter)
|
menuText := "Dynamic Menu Item " + strconv.Itoa(counter)
|
||||||
removeMenu := m.runtime.Menu.GetByID("Remove Last Item")
|
m.removeMenuItem.Label = "Remove " + menuText
|
||||||
// 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("[")))
|
// 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
|
// Create our submenu
|
||||||
dm2 := menu.SubMenu("Dynamic Menus 2", []*menu.MenuItem{
|
dm2 := menu.SubMenu("Dynamic Menus 2", menu.NewMenuFromItems(
|
||||||
menu.Text("Insert Before Random Menu Item",
|
menu.Text("Insert Before Random Menu Item", keys.CmdOrCtrl("]"), m.insertBeforeRandom),
|
||||||
"Insert Before Random", keys.CmdOrCtrl("]")),
|
menu.Text("Insert After Random Menu Item", keys.CmdOrCtrl("["), m.insertAfterRandom),
|
||||||
menu.Text("Insert After Random Menu Item",
|
|
||||||
"Insert After Random", keys.CmdOrCtrl("[")),
|
|
||||||
menu.Separator(),
|
menu.Separator(),
|
||||||
})
|
))
|
||||||
|
|
||||||
m.runtime.Menu.On("Insert Before Random", m.insertBeforeRandom)
|
//m.runtime.Menu.On("Insert Before Random", m.insertBeforeRandom)
|
||||||
m.runtime.Menu.On("Insert After Random", m.insertAfterRandom)
|
//m.runtime.Menu.On("Insert After Random", m.insertAfterRandom)
|
||||||
|
|
||||||
// Initialise out map
|
// Initialise dynamicMenuItems
|
||||||
m.dynamicMenuItems = make(map[string]*menu.MenuItem)
|
m.dynamicMenuItems = make(map[string]*menu.MenuItem)
|
||||||
|
|
||||||
// Create some random menu items
|
// Create some random menu items
|
||||||
m.anotherDynamicMenuCounter = 5
|
m.anotherDynamicMenuCounter = 5
|
||||||
for index := 0; index < m.anotherDynamicMenuCounter; index++ {
|
for index := 0; index < m.anotherDynamicMenuCounter; index++ {
|
||||||
text := "Other Dynamic Menu Item " + strconv.Itoa(index+1)
|
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
|
m.dynamicMenuItems[text] = item
|
||||||
dm2.Append(item)
|
dm2.Append(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert this menu after Dynamic Menu Item 1
|
m.dynamicMenuOneSubmenu.InsertAfter(dm2)
|
||||||
dm1 := m.runtime.Menu.GetByID("Dynamic Menus 1")
|
m.runtime.Menu.UpdateApplicationMenu()
|
||||||
if dm1 == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dm1.InsertAfter(dm2)
|
|
||||||
m.runtime.Menu.Update()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Menu) insertBeforeRandom(_ *menu.MenuItem) {
|
func (m *Menu) insertBeforeRandom(_ *menu.CallbackData) {
|
||||||
|
|
||||||
// Lock because this method will be called in a goroutine
|
// Lock because this method will be called in a goroutine
|
||||||
m.lock.Lock()
|
m.lock.Lock()
|
||||||
defer m.lock.Unlock()
|
defer m.lock.Unlock()
|
||||||
|
|
||||||
// Pick a random menu
|
// Pick a random menu
|
||||||
var randomItemID string
|
var randomMenuItem *menu.MenuItem
|
||||||
var count int
|
for _, randomMenuItem = range m.dynamicMenuItems {
|
||||||
var random = rand.Intn(len(m.dynamicMenuItems))
|
break
|
||||||
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)
|
|
||||||
m.dynamicMenuItems[text] = newItem
|
|
||||||
|
|
||||||
item := m.runtime.Menu.GetByID(randomItemID)
|
if randomMenuItem == nil {
|
||||||
if item == nil {
|
|
||||||
return
|
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++
|
m.anotherDynamicMenuCounter++
|
||||||
text := "Other Dynamic Menu Item " + strconv.Itoa(
|
text := "Other Dynamic Menu Item " + strconv.Itoa(m.anotherDynamicMenuCounter)
|
||||||
m.anotherDynamicMenuCounter+1)
|
newItem := menu.Text(text, nil, nil)
|
||||||
newItem := menu.Text(text, text, nil)
|
|
||||||
|
|
||||||
item := m.runtime.Menu.GetByID(randomItemID)
|
|
||||||
m.dynamicMenuItems[text] = newItem
|
m.dynamicMenuItems[text] = newItem
|
||||||
|
|
||||||
m.runtime.Log.Info(fmt.Sprintf(
|
m.runtime.Log.Info(fmt.Sprintf("Inserting menu item '%s' before menu item '%s'", newItem.Label, randomMenuItem.Label))
|
||||||
"Inserting menu item '%s' after menu item '%s'", newItem.Label,
|
|
||||||
item.Label))
|
|
||||||
|
|
||||||
item.InsertAfter(newItem)
|
randomMenuItem.InsertBefore(newItem)
|
||||||
m.runtime.Menu.Update()
|
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
|
// Create menu
|
||||||
myMenu := menu.DefaultMacMenu()
|
myMenu := menu.DefaultMacMenu()
|
||||||
|
|
||||||
windowMenu := menu.SubMenu("Test", []*menu.MenuItem{
|
windowMenu := menu.SubMenu("Test", menu.NewMenuFromItems(
|
||||||
menu.Togglefullscreen(),
|
menu.Togglefullscreen(),
|
||||||
menu.Minimize(),
|
menu.Minimize(),
|
||||||
menu.Zoom(),
|
menu.Zoom(),
|
||||||
@@ -244,89 +215,90 @@ func createApplicationMenu() *menu.Menu {
|
|||||||
|
|
||||||
menu.Front(),
|
menu.Front(),
|
||||||
|
|
||||||
menu.SubMenu("Test Submenu", []*menu.MenuItem{
|
menu.SubMenu("Test Submenu", menu.NewMenuFromItems(
|
||||||
menu.Text("Plain text", "plain text", nil),
|
menu.Text("Plain text", nil, m.processPlainText),
|
||||||
menu.Text("Show Dynamic Menus 2 Submenu", "show-dynamic-menus-2", nil),
|
menu.Text("Show Dynamic Menus 2 Submenu", nil, m.createDynamicMenuTwo),
|
||||||
menu.SubMenu("Accelerators", []*menu.MenuItem{
|
menu.SubMenu("Accelerators", menu.NewMenuFromItems(
|
||||||
menu.SubMenu("Modifiers", []*menu.MenuItem{
|
menu.SubMenu("Modifiers", menu.NewMenuFromItems(
|
||||||
menu.Text("Shift accelerator", "Shift", keys.Shift("o")),
|
menu.Text("Shift accelerator", keys.Shift("o"), nil),
|
||||||
menu.Text("Control accelerator", "Control", keys.Control("o")),
|
menu.Text("Control accelerator", keys.Control("o"), nil),
|
||||||
menu.Text("Command accelerator", "Command", keys.CmdOrCtrl("o")),
|
menu.Text("Command accelerator", keys.CmdOrCtrl("o"), nil),
|
||||||
menu.Text("Option accelerator", "Option", keys.OptionOrAlt("o")),
|
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.MenuItem{
|
)),
|
||||||
menu.Text("Backspace", "Backspace", keys.Key("Backspace")),
|
menu.SubMenu("System Keys", menu.NewMenuFromItems(
|
||||||
menu.Text("Tab", "Tab", keys.Key("Tab")),
|
menu.Text("Backspace", keys.Key("Backspace"), nil),
|
||||||
menu.Text("Return", "Return", keys.Key("Return")),
|
menu.Text("Tab", keys.Key("Tab"), nil),
|
||||||
menu.Text("Escape", "Escape", keys.Key("Escape")),
|
menu.Text("Return", keys.Key("Return"), nil),
|
||||||
menu.Text("Left", "Left", keys.Key("Left")),
|
menu.Text("Escape", keys.Key("Escape"), nil),
|
||||||
menu.Text("Right", "Right", keys.Key("Right")),
|
menu.Text("Left", keys.Key("Left"), nil),
|
||||||
menu.Text("Up", "Up", keys.Key("Up")),
|
menu.Text("Right", keys.Key("Right"), nil),
|
||||||
menu.Text("Down", "Down", keys.Key("Down")),
|
menu.Text("Up", keys.Key("Up"), nil),
|
||||||
menu.Text("Space", "Space", keys.Key("Space")),
|
menu.Text("Down", keys.Key("Down"), nil),
|
||||||
menu.Text("Delete", "Delete", keys.Key("Delete")),
|
menu.Text("Space", keys.Key("Space"), nil),
|
||||||
menu.Text("Home", "Home", keys.Key("Home")),
|
menu.Text("Delete", keys.Key("Delete"), nil),
|
||||||
menu.Text("End", "End", keys.Key("End")),
|
menu.Text("Home", keys.Key("Home"), nil),
|
||||||
menu.Text("Page Up", "Page Up", keys.Key("Page Up")),
|
menu.Text("End", keys.Key("End"), nil),
|
||||||
menu.Text("Page Down", "Page Down", keys.Key("Page Down")),
|
menu.Text("Page Up", keys.Key("Page Up"), nil),
|
||||||
menu.Text("NumLock", "NumLock", keys.Key("NumLock")),
|
menu.Text("Page Down", keys.Key("Page Down"), nil),
|
||||||
}),
|
menu.Text("NumLock", keys.Key("NumLock"), nil),
|
||||||
menu.SubMenu("Function Keys", []*menu.MenuItem{
|
)),
|
||||||
menu.Text("F1", "F1", keys.Key("F1")),
|
menu.SubMenu("Function Keys", menu.NewMenuFromItems(
|
||||||
menu.Text("F2", "F2", keys.Key("F2")),
|
menu.Text("F1", keys.Key("F1"), nil),
|
||||||
menu.Text("F3", "F3", keys.Key("F3")),
|
menu.Text("F2", keys.Key("F2"), nil),
|
||||||
menu.Text("F4", "F4", keys.Key("F4")),
|
menu.Text("F3", keys.Key("F3"), nil),
|
||||||
menu.Text("F5", "F5", keys.Key("F5")),
|
menu.Text("F4", keys.Key("F4"), nil),
|
||||||
menu.Text("F6", "F6", keys.Key("F6")),
|
menu.Text("F5", keys.Key("F5"), nil),
|
||||||
menu.Text("F7", "F7", keys.Key("F7")),
|
menu.Text("F6", keys.Key("F6"), nil),
|
||||||
menu.Text("F8", "F8", keys.Key("F8")),
|
menu.Text("F7", keys.Key("F7"), nil),
|
||||||
menu.Text("F9", "F9", keys.Key("F9")),
|
menu.Text("F8", keys.Key("F8"), nil),
|
||||||
menu.Text("F10", "F10", keys.Key("F10")),
|
menu.Text("F9", keys.Key("F9"), nil),
|
||||||
menu.Text("F11", "F11", keys.Key("F11")),
|
menu.Text("F10", keys.Key("F10"), nil),
|
||||||
menu.Text("F12", "F12", keys.Key("F12")),
|
menu.Text("F11", keys.Key("F11"), nil),
|
||||||
menu.Text("F13", "F13", keys.Key("F13")),
|
menu.Text("F12", keys.Key("F12"), nil),
|
||||||
menu.Text("F14", "F14", keys.Key("F14")),
|
menu.Text("F13", keys.Key("F13"), nil),
|
||||||
menu.Text("F15", "F15", keys.Key("F15")),
|
menu.Text("F14", keys.Key("F14"), nil),
|
||||||
menu.Text("F16", "F16", keys.Key("F16")),
|
menu.Text("F15", keys.Key("F15"), nil),
|
||||||
menu.Text("F17", "F17", keys.Key("F17")),
|
menu.Text("F16", keys.Key("F16"), nil),
|
||||||
menu.Text("F18", "F18", keys.Key("F18")),
|
menu.Text("F17", keys.Key("F17"), nil),
|
||||||
menu.Text("F19", "F19", keys.Key("F19")),
|
menu.Text("F18", keys.Key("F18"), nil),
|
||||||
menu.Text("F20", "F20", keys.Key("F20")),
|
menu.Text("F19", keys.Key("F19"), nil),
|
||||||
}),
|
menu.Text("F20", keys.Key("F20"), nil),
|
||||||
menu.SubMenu("Standard Keys", []*menu.MenuItem{
|
)),
|
||||||
menu.Text("Backtick", "Backtick", keys.Key("`")),
|
menu.SubMenu("Standard Keys", menu.NewMenuFromItems(
|
||||||
menu.Text("Plus", "Plus", keys.Key("+")),
|
menu.Text("Backtick", keys.Key("`"), nil),
|
||||||
}),
|
menu.Text("Plus", keys.Key("+"), nil),
|
||||||
}),
|
)),
|
||||||
menu.SubMenuWithID("Dynamic Menus 1", "Dynamic Menus 1", []*menu.MenuItem{
|
)),
|
||||||
menu.Text("Add Menu Item", "Add Menu Item", keys.CmdOrCtrl("+")),
|
m.dynamicMenuOneSubmenu,
|
||||||
menu.Separator(),
|
&menu.MenuItem{
|
||||||
}),
|
|
||||||
{
|
|
||||||
Label: "Disabled Menu",
|
Label: "Disabled Menu",
|
||||||
Type: menu.TextType,
|
Type: menu.TextType,
|
||||||
Accelerator: keys.Combo("p", keys.CmdOrCtrlKey, keys.ShiftKey),
|
Accelerator: keys.Combo("p", keys.CmdOrCtrlKey, keys.ShiftKey),
|
||||||
Disabled: true,
|
Disabled: true,
|
||||||
},
|
},
|
||||||
{
|
&menu.MenuItem{
|
||||||
Label: "Hidden Menu",
|
Label: "Hidden Menu",
|
||||||
Type: menu.TextType,
|
Type: menu.TextType,
|
||||||
Hidden: true,
|
Hidden: true,
|
||||||
},
|
},
|
||||||
{
|
&menu.MenuItem{
|
||||||
ID: "checkbox-menu 1",
|
|
||||||
Label: "Checkbox Menu 1",
|
Label: "Checkbox Menu 1",
|
||||||
Type: menu.CheckboxType,
|
Type: menu.CheckboxType,
|
||||||
Accelerator: keys.CmdOrCtrl("l"),
|
Accelerator: keys.CmdOrCtrl("l"),
|
||||||
Checked: true,
|
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.Separator(),
|
||||||
menu.Radio("😀 Option 1", "😀option-1", true, nil),
|
menu.Radio("😀 Option 1", true, nil, nil),
|
||||||
menu.Radio("😺 Option 2", "option-2", false, nil),
|
menu.Radio("😺 Option 2", false, nil, nil),
|
||||||
menu.Radio("❤️ Option 3", "option-3", false, nil),
|
menu.Radio("❤️ Option 3", false, nil, nil),
|
||||||
}),
|
)),
|
||||||
})
|
))
|
||||||
|
|
||||||
myMenu.Append(windowMenu)
|
myMenu.Append(windowMenu)
|
||||||
return myMenu
|
return myMenu
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"github.com/wailsapp/wails/v2"
|
"github.com/wailsapp/wails/v2"
|
||||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||||
"github.com/wailsapp/wails/v2/pkg/menu/keys"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
@@ -12,10 +11,12 @@ import (
|
|||||||
type Tray struct {
|
type Tray struct {
|
||||||
runtime *wails.Runtime
|
runtime *wails.Runtime
|
||||||
|
|
||||||
dynamicMenuCounter int
|
dynamicMenuCounter int
|
||||||
lock sync.Mutex
|
lock sync.Mutex
|
||||||
dynamicMenuItems map[string]*menu.MenuItem
|
dynamicMenuItems map[string]*menu.MenuItem
|
||||||
anotherDynamicMenuCounter int
|
|
||||||
|
trayMenu *menu.TrayMenu
|
||||||
|
secondTrayMenu *menu.TrayMenu
|
||||||
|
|
||||||
done bool
|
done bool
|
||||||
}
|
}
|
||||||
@@ -25,35 +26,35 @@ func (t *Tray) WailsInit(runtime *wails.Runtime) error {
|
|||||||
// Perform your setup here
|
// Perform your setup here
|
||||||
t.runtime = runtime
|
t.runtime = runtime
|
||||||
|
|
||||||
// Setup Menu Listeners
|
//// Auto switch between light / dark tray icons
|
||||||
t.runtime.Tray.On("Show Window", func(mi *menu.MenuItem) {
|
//t.runtime.Events.OnThemeChange(func(darkMode bool) {
|
||||||
t.runtime.Window.Show()
|
// if darkMode {
|
||||||
})
|
// t.runtime.Tray.SetIcon("light")
|
||||||
t.runtime.Tray.On("Hide Window", func(mi *menu.MenuItem) {
|
// return
|
||||||
t.runtime.Window.Hide()
|
// }
|
||||||
})
|
//
|
||||||
|
// t.runtime.Tray.SetIcon("dark")
|
||||||
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")
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
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() {
|
func (t *Tray) WailsShutdown() {
|
||||||
t.done = true
|
t.done = true
|
||||||
}
|
}
|
||||||
@@ -68,77 +69,95 @@ func (t *Tray) decrementcounter() int {
|
|||||||
return t.dynamicMenuCounter
|
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
|
//func (t *Tray) removeMenu(_ *menu.MenuItem) {
|
||||||
t.lock.Lock()
|
//
|
||||||
defer t.lock.Unlock()
|
// // 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
|
//func (t *Tray) SetIcon(trayIconID string) {
|
||||||
parent := mi.Parent()
|
// t.runtime.Tray.SetIcon(trayIconID)
|
||||||
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("[")))
|
|
||||||
|
|
||||||
// If this is the first dynamic menu added, let's add a remove menu item
|
func (t *Tray) createTrayMenus() []*menu.TrayMenu {
|
||||||
if counter == 1 {
|
trayMenu := &menu.TrayMenu{}
|
||||||
removeMenu := menu.Text("Remove "+menuText,
|
trayMenu.Label = "Test Tray Label"
|
||||||
"Remove Last Item", keys.CmdOrCtrl("-"))
|
trayMenu.Menu = menu.NewMenuFromItems(
|
||||||
parent.Prepend(removeMenu)
|
menu.Text("Show Window", nil, t.showWindow),
|
||||||
t.runtime.Tray.On("Remove Last Item", t.removeMenu)
|
menu.Text("Hide Window", nil, t.hideWindow),
|
||||||
} else {
|
menu.Text("Minimise Window", nil, t.minimiseWindow),
|
||||||
removeMenu := t.runtime.Tray.GetByID("Remove Last Item")
|
menu.Text("Unminimise Window", nil, t.unminimiseWindow),
|
||||||
// Test if the remove menu hasn't already been removed in another thread
|
)
|
||||||
if removeMenu != nil {
|
t.trayMenu = trayMenu
|
||||||
removeMenu.Label = "Remove " + menuText
|
|
||||||
}
|
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user