Compare commits

...

61 Commits

Author SHA1 Message Date
Lea Anthony
d2f233abfb Fix packaging universal builds 2021-03-25 21:02:16 +11:00
Lea Anthony
2dd8833e67 Support building arm64 & universal binaries. 2021-03-25 20:55:59 +11:00
Lea Anthony
3c30924493 Initial ARM support! 2021-03-25 07:20:36 +11:00
Lea Anthony
4ce3e1d1bf v2.0.0-alpha.55 2021-03-21 20:59:23 +11:00
Lea Anthony
e5f2746810 Better font name support 2021-03-21 20:57:41 +11:00
Lea Anthony
92ebf506dd Get app compiling 2021-03-20 18:32:09 +11:00
Lea Anthony
9ab06152c5 Refactor doctor for windows 2021-03-20 16:27:35 +11:00
Lea Anthony
bf36b6a59d Doctor working for Windows 2021-03-20 16:24:02 +11:00
Lea Anthony
4b9f6c4fb1 Update CONTRIBUTORS.md 2021-03-20 15:55:14 +11:00
Amaury Tobias Quiroz
b1a42c8dea fix: support rel="modulepreload" on assetbundle as javascript (#621)
In some bundler enviroments like vite the output on index.html uses link
tag with rel="modulepreload" to load javascript, add support to handle
this files on assetbundle, more info
https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/modulepreload

fix #620

Co-authored-by: amaury-tobias <amaury.tobiasqr@gmai.com>
2021-03-20 15:51:57 +11:00
Travis McLane
cbd98b5a1a update scripts/build.sh to run test only on v1 2021-03-20 15:01:09 +11:00
Lea Anthony
c8e0aea69c v2.0.0-alpha.54 2021-03-19 09:17:18 +11:00
Lea Anthony
7c0b236eb0 Fix for hiding window on close 2021-03-19 09:14:49 +11:00
Lea Anthony
16debbd109 v2.0.0-alpha.53 2021-03-18 20:55:26 +11:00
Lea Anthony
39bfa5d910 Support disabling tray menu. Fix font sizing. Tooltip in tray menu support. 2021-03-18 20:54:53 +11:00
Lea Anthony
6424579a9e v2.0.0-alpha.52 2021-03-17 23:30:42 +11:00
Lea Anthony
a962ae6f63 Support rich text in Tray labels 2021-03-17 23:30:08 +11:00
Lea Anthony
c7dee158ba tray menu Icon->Image. Support template images. 2021-03-17 22:24:09 +11:00
Lea Anthony
237d25089d Update CLI banner 2021-03-17 21:59:31 +11:00
Lea Anthony
265328d648 v2.0.0-alpha.51 2021-03-15 06:13:51 +11:00
Lea Anthony
6f40e85a6e Support startup urls when app not running 2021-03-15 06:13:00 +11:00
Lea Anthony
96d8509da3 v2.0.0-alpha.50 2021-03-13 15:17:35 +11:00
Lea Anthony
598445ab0f v2.0.0-alpha.49 2021-03-13 15:00:19 +11:00
Lea Anthony
24788e2fd3 Fix template images 2021-03-13 14:59:12 +11:00
Lea Anthony
bbf4dde43f Support upserting environment variables 2021-03-12 23:41:13 +11:00
Lea Anthony
0afd27ab45 Add FileLogger option 2021-03-12 23:40:35 +11:00
Lea Anthony
d498423ec2 v2.0.0-alpha.48 2021-03-09 22:28:07 +11:00
Mat Ryer
66ce84973c fixes for machines running TouchBar addresses https://github.com/matryer/xbar/issues/610 2021-03-09 11:24:46 +00:00
Lea Anthony
55e6a0f312 v2.0.0-alpha.47 2021-03-07 16:24:20 +11:00
Lea Anthony
81e83fdf18 Ensure modifiers are lowercase when parsing 2021-03-07 16:21:30 +11:00
Lea Anthony
f9b79d24f8 Guard against nil url messages 2021-03-06 15:51:06 +11:00
Lea Anthony
0599a47bfe v2.0.0-alpha.46 2021-03-06 15:43:44 +11:00
Lea Anthony
817c55d318 Support base64 images in tray 2021-03-06 15:43:11 +11:00
Lea Anthony
14146c8c0c v2.0.0-alpha.45 2021-03-06 00:29:00 +11:00
Lea Anthony
18adac20d4 Tray menu open/close events 2021-03-06 00:25:34 +11:00
Lea Anthony
eb4bff89da v2.0.0-alpha.44 2021-03-04 06:18:31 +11:00
Lea Anthony
c66dc777f3 Remove debug logging 2021-03-04 06:18:11 +11:00
Lea Anthony
9003462457 v2.0.0-alpha.43 2021-03-04 06:09:17 +11:00
Lea Anthony
e124f0a220 Support Alternative menu items 2021-03-04 06:07:45 +11:00
Lea Anthony
c6d3f57712 v2.0.0-alpha.42 2021-03-01 08:49:31 +11:00
Lea Anthony
b4c669ff86 Support custom protocols 2021-02-28 22:08:23 +11:00
Lea Anthony
2d1b2c0947 Guard app signing 2021-02-28 15:29:15 +11:00
Lea Anthony
4a0c5aa785 v2.0.0-alpha.41 2021-02-27 20:33:42 +11:00
Lea Anthony
f48d7f8f60 Add support for -sign 2021-02-27 20:32:29 +11:00
Lea Anthony
651f24f641 update vanilla template 2021-02-27 20:06:49 +11:00
Lea Anthony
8fd77148ca update vanilla template 2021-02-27 16:59:16 +11:00
Lea Anthony
0dc0762fdf update vanilla template 2021-02-27 16:45:30 +11:00
Lea Anthony
1a92550709 update vanilla template 2021-02-27 16:08:53 +11:00
Lea Anthony
bffc15bc14 fix: terminate app on window close 2021-02-27 14:49:44 +11:00
Lea Anthony
198d206c46 Update basic template to new API 2021-02-27 14:07:27 +11:00
Lea Anthony
bb8e848ef6 Run go mod tidy before compilation 2021-02-27 14:03:54 +11:00
Lea Anthony
bac3e9e5c1 Fix asset dir perms 2021-02-27 13:54:39 +11:00
Lea Anthony
bc5eddeb66 v2.0.0-alpha.40 2021-02-26 15:31:37 +11:00
Lea Anthony
8e7258d812 Add locking for tray operations 2021-02-26 15:23:39 +11:00
Lea Anthony
7118762cec v2.0.0-alpha.39 2021-02-25 22:05:38 +11:00
Lea Anthony
6af92cf0a4 Delete Tray for bridge 2021-02-25 19:57:36 +11:00
matryer
1bb91634f7 avoid fmt in happy path 2021-02-25 07:39:42 +00:00
Lea Anthony
f71ce7913f v2.0.0-alpha.38 2021-02-24 21:50:39 +11:00
Lea Anthony
53db687a26 Support DeleteTrayMenuByID 2021-02-24 21:49:55 +11:00
Lea Anthony
13939d3d6b v2.0.0-alpha.37 2021-02-23 20:15:01 +11:00
Lea Anthony
552c6b8711 fix: modifiers 2021-02-23 18:57:59 +11:00
60 changed files with 1524 additions and 508 deletions

View File

@@ -40,4 +40,5 @@ Wails is what it is because of the time and effort given by these great people.
* [Balakrishna Prasad Ganne](https://github.com/aayush420)
* [Charaf Rezrazi](https://github.com/Rezrazi)
* [misitebao](https://github.com/misitebao)
* [Elie Grenon](https://github.com/DrunkenPoney)
* [Elie Grenon](https://github.com/DrunkenPoney)
* [Amaury Tobias Quiroz](https://github.com/amaury-tobias)

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
echo "**** Checking if Wails passes unit tests ****"
if ! go test ./...
if ! go test ./lib/... ./runtime/... ./cmd/...
then
echo ""
echo "ERROR: Unit tests failed!"

View File

@@ -3,8 +3,10 @@ package build
import (
"fmt"
"io"
"os"
"runtime"
"strings"
"text/tabwriter"
"time"
"github.com/leaanthony/clir"
@@ -23,8 +25,8 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
command := app.NewSubCommand("build", "Builds the application")
// Setup target type flag
description := "Type of application to build. Valid types: " + validTargetTypes.Join(",")
command.StringFlag("t", description, &outputType)
//description := "Type of application to build. Valid types: " + validTargetTypes.Join(",")
//command.StringFlag("t", description, &outputType)
// Setup production flag
production := false
@@ -41,24 +43,35 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
platform := runtime.GOOS
command.StringFlag("platform", "Platform to target", &platform)
// Quiet Build
quiet := false
command.BoolFlag("q", "Suppress output to console", &quiet)
// Verbosity
verbosity := 1
command.IntFlag("v", "Verbosity level (0 - silent, 1 - default, 2 - verbose)", &verbosity)
// ldflags to pass to `go`
ldflags := ""
command.StringFlag("ldflags", "optional ldflags", &ldflags)
// Log to file
logFile := ""
command.StringFlag("l", "Log to file", &logFile)
//logFile := ""
//command.StringFlag("l", "Log to file", &logFile)
// Retain assets
keepAssets := false
command.BoolFlag("k", "Keep generated assets", &keepAssets)
// Retain assets
outputFilename := ""
command.StringFlag("o", "Output filename", &outputFilename)
appleIdentity := ""
if runtime.GOOS == "darwin" {
command.StringFlag("sign", "Signs your app with the given identity.", &appleIdentity)
}
command.Action(func() error {
quiet := verbosity == 0
// Create logger
logger := clilogger.New(w)
logger.Mute(quiet)
@@ -72,9 +85,10 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
app.PrintBanner()
}
task := fmt.Sprintf("Building %s Application", strings.Title(outputType))
logger.Println(task)
logger.Println(strings.Repeat("-", len(task)))
// Ensure package is used with apple identity
if appleIdentity != "" && pack == false {
return fmt.Errorf("must use `-package` flag when using `-sign`")
}
// Setup mode
mode := build.Debug
@@ -82,18 +96,66 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
mode = build.Production
}
// Check platform
validPlatformArch := slicer.String([]string{
"darwin",
"darwin/amd64",
"darwin/arm64",
"darwin/universal",
//"linux/amd64",
//"linux/arm-7",
//"windows/amd64",
})
if !validPlatformArch.Contains(platform) {
return fmt.Errorf("platform %s is not supported", platform)
}
// Create BuildOptions
buildOptions := &build.Options{
Logger: logger,
OutputType: outputType,
Mode: mode,
Pack: pack,
Platform: platform,
LDFlags: ldflags,
Compiler: compilerCommand,
KeepAssets: keepAssets,
Logger: logger,
OutputType: outputType,
OutputFile: outputFilename,
Mode: mode,
Pack: pack,
LDFlags: ldflags,
Compiler: compilerCommand,
KeepAssets: keepAssets,
AppleIdentity: appleIdentity,
Verbosity: verbosity,
}
// Calculate platform and arch
platformSplit := strings.Split(platform, "/")
buildOptions.Platform = platformSplit[0]
buildOptions.Arch = runtime.GOARCH
if len(platformSplit) == 2 {
buildOptions.Arch = platformSplit[1]
}
// Start a new tabwriter
w := new(tabwriter.Writer)
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
buildModeText := "debug"
if production {
buildModeText = "production"
}
// Write out the system information
fmt.Fprintf(w, "App Type: \t%s\n", buildOptions.OutputType)
fmt.Fprintf(w, "Platform: \t%s\n", buildOptions.Platform)
fmt.Fprintf(w, "Arch: \t%s\n", buildOptions.Arch)
fmt.Fprintf(w, "Compiler: \t%s\n", buildOptions.Compiler)
fmt.Fprintf(w, "Build Mode: \t%s\n", buildModeText)
fmt.Fprintf(w, "Package: \t%t\n", buildOptions.Pack)
fmt.Fprintf(w, "KeepAssets: \t%t\n", buildOptions.KeepAssets)
fmt.Fprintf(w, "LDFlags: \t\"%s\"\n", buildOptions.LDFlags)
if len(buildOptions.OutputFile) > 0 {
fmt.Fprintf(w, "Output File: \t%s\n", buildOptions.OutputFile)
}
fmt.Fprintf(w, "\n")
w.Flush()
return doBuild(buildOptions)
})
}

View File

@@ -23,14 +23,14 @@ func fatal(message string) {
}
func banner(_ *clir.Cli) string {
return fmt.Sprintf("%s %s - Go/HTML Application Framework", colour.Yellow("Wails"), colour.DarkRed(version))
return fmt.Sprintf("%s %s", colour.Yellow("Wails CLI"), colour.DarkRed(version))
}
func main() {
var err error
app := clir.NewCli("Wails", "Go/HTML Application Framework", version)
app := clir.NewCli("Wails", "Go/HTML Appkit", version)
app.SetBannerFunction(banner)

View File

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

View File

@@ -35,6 +35,7 @@ type App struct {
//binding *subsystem.Binding
call *subsystem.Call
menu *subsystem.Menu
url *subsystem.URL
dispatcher *messagedispatcher.Dispatcher
menuManager *menumanager.Manager
@@ -160,6 +161,19 @@ func (a *App) Run() error {
return err
}
if a.options.Mac.URLHandlers != nil {
// Start the url handler subsystem
url, err := subsystem.NewURL(a.servicebus, a.logger, a.options.Mac.URLHandlers)
if err != nil {
return err
}
a.url = url
err = a.url.Start()
if err != nil {
return err
}
}
// Start the eventing subsystem
eventsubsystem, err := subsystem.NewEvent(ctx, a.servicebus, a.logger)
if err != nil {

View File

@@ -11,6 +11,10 @@ type BridgeClient struct {
messageCache chan string
}
func (b BridgeClient) DeleteTrayMenuByID(id string) {
b.session.sendMessage("TD" + id)
}
func NewBridgeClient() *BridgeClient {
return &BridgeClient{
messageCache: make(chan string, 100),

View File

@@ -19,6 +19,9 @@ type DialogClient struct {
log *logger.Logger
}
func (d *DialogClient) DeleteTrayMenuByID(id string) {
}
func NewDialogClient(log *logger.Logger) *DialogClient {
return &DialogClient{
log: log,

View File

@@ -5,10 +5,6 @@
#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"

View File

@@ -82,10 +82,10 @@ void ShowContextMenu(ContextMenuStore* store, id mainWindow, const char *context
FREE_AND_SET(contextMenu->contextMenuData, contextMenuData);
// Grab the content view and show the menu
id contentView = msg(mainWindow, s("contentView"));
id contentView = msg_reg(mainWindow, s("contentView"));
// Get the triggering event
id menuEvent = msg(mainWindow, s("currentEvent"));
id menuEvent = msg_reg(mainWindow, s("currentEvent"));
if( contextMenu->nsmenu == NULL ) {
// GetMenu creates the NSMenu
@@ -93,7 +93,7 @@ void ShowContextMenu(ContextMenuStore* store, id mainWindow, const char *context
}
// Show popup
msg(c("NSMenu"), s("popUpContextMenu:withEvent:forView:"), contextMenu->nsmenu, menuEvent, contentView);
((id(*)(id, SEL, id, id, id))objc_msgSend)(c("NSMenu"), s("popUpContextMenu:withEvent:forView:"), contextMenu->nsmenu, menuEvent, contentView);
}

View File

@@ -37,6 +37,7 @@ extern void DarkModeEnabled(struct Application*, char *callbackID);
extern void SetApplicationMenu(struct Application*, const char *);
extern void AddTrayMenu(struct Application*, const char *menuTrayJSON);
extern void SetTrayMenu(struct Application*, const char *menuTrayJSON);
extern void DeleteTrayMenuByID(struct Application*, const char *id);
extern void UpdateTrayMenuLabel(struct Application*, const char* JSON);
extern void AddContextMenu(struct Application*, char *contextMenuJSON);
extern void UpdateContextMenu(struct Application*, char *contextMenuJSON);

View File

@@ -208,3 +208,7 @@ func (c *Client) UpdateTrayMenuLabel(JSON string) {
func (c *Client) UpdateContextMenu(contextMenuJSON string) {
C.UpdateContextMenu(c.app.app, c.app.string2CString(contextMenuJSON))
}
func (c *Client) DeleteTrayMenuByID(id string) {
C.DeleteTrayMenuByID(c.app.app, c.app.string2CString(id))
}

File diff suppressed because it is too large Load Diff

View File

@@ -90,5 +90,10 @@ func (a *Application) processPlatformSettings() error {
}
}
// Process URL Handlers
if a.config.Mac.URLHandlers != nil {
C.HasURLHandlers(a.app)
}
return nil
}

View File

@@ -13,22 +13,41 @@
// Macros to make it slightly more sane
#define msg objc_msgSend
#define msg_reg ((id(*)(id, SEL))objc_msgSend)
#define msg_id ((id(*)(id, SEL, id))objc_msgSend)
#define msg_id_id ((id(*)(id, SEL, id, id))objc_msgSend)
#define msg_bool ((id(*)(id, SEL, BOOL))objc_msgSend)
#define msg_int ((id(*)(id, SEL, int))objc_msgSend)
#define msg_uint ((id(*)(id, SEL, unsigned int))objc_msgSend)
#define msg_float ((id(*)(id, SEL, float))objc_msgSend)
#define kInternetEventClass 'GURL'
#define kAEGetURL 'GURL'
#define keyDirectObject '----'
#define c(str) (id)objc_getClass(str)
#define s(str) sel_registerName(str)
#define u(str) sel_getUid(str)
#define str(input) msg(c("NSString"), s("stringWithUTF8String:"), input)
#define strunicode(input) msg(c("NSString"), s("stringWithFormat:"), str("%C"), (unsigned short)input)
#define cstr(input) (const char *)msg(input, s("UTF8String"))
#define url(input) msg(c("NSURL"), s("fileURLWithPath:"), str(input))
#define ALLOC(classname) msg(c(classname), s("alloc"))
#define ALLOC_INIT(classname) msg(msg(c(classname), s("alloc")), s("init"))
#define str(input) ((id(*)(id, SEL, const char *))objc_msgSend)(c("NSString"), s("stringWithUTF8String:"), input)
#define strunicode(input) ((id(*)(id, SEL, id, unsigned short))objc_msgSend)(c("NSString"), s("stringWithFormat:"), str("%C"), (unsigned short)input)
#define cstr(input) (const char *)msg_reg(input, s("UTF8String"))
#define url(input) msg_id(c("NSURL"), s("fileURLWithPath:"), str(input))
#define ALLOC(classname) msg_reg(c(classname), s("alloc"))
#define ALLOC_INIT(classname) msg_reg(msg_reg(c(classname), s("alloc")), s("init"))
#if defined (__aarch64__)
#define GET_FRAME(receiver) ((CGRect(*)(id, SEL))objc_msgSend)(receiver, s("frame"))
#define GET_BOUNDS(receiver) ((CGRect(*)(id, SEL))objc_msgSend)(receiver, s("bounds"))
#endif
#if defined (__x86_64__)
#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"))
#endif
#define GET_BACKINGSCALEFACTOR(receiver) ((CGFloat(*)(id, SEL))objc_msgSend)(receiver, s("backingScaleFactor"))
#define ON_MAIN_THREAD(str) dispatch( ^{ str; } )
#define MAIN_WINDOW_CALL(str) msg(app->mainWindow, s((str)))
#define MAIN_WINDOW_CALL(str) msg_reg(app->mainWindow, s((str)))
#define NSBackingStoreBuffered 2
@@ -118,4 +137,6 @@ void SetActivationPolicy(struct Application* app, int policy);
void* lookupStringConstant(id constantName);
void HasURLHandlers(struct Application* app);
#endif

View File

@@ -0,0 +1,78 @@
typedef struct {
} Application;
struct Application *NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden, int logLevel, int hideWindowOnClose) {
}
void SetMinWindowSize(struct Application* app, int minWidth, int minHeight) {
}
void SetMaxWindowSize(struct Application* app, int maxWidth, int maxHeight) {
}
void Run(struct Application* app, int argc, char **argv) {
}
void DestroyApplication(struct Application* app) {
}
void SetDebug(struct Application* app, int flag) {
}
void SetBindings(struct Application* app, const char *bindings) {
}
void ExecJS(struct Application* app, const char *script) {
}
void Hide(struct Application* app) {
}
void Show(struct Application* app) {
}
void Center(struct Application* app) {
}
void Maximise(struct Application* app) {
}
void Unmaximise(struct Application* app) {
}
void ToggleMaximise(struct Application* app) {
}
void Minimise(struct Application* app) {
}
void Unminimise(struct Application* app) {
}
void ToggleMinimise(struct Application* app) {
}
void SetColour(struct Application* app, int red, int green, int blue, int alpha) {
}
void SetSize(struct Application* app, int width, int height) {
}
void SetPosition(struct Application* app, int x, int y) {
}
void Quit(struct Application* app) {
}
void SetTitle(struct Application* app, const char *title) {
}
void Fullscreen(struct Application* app) {
}
void UnFullscreen(struct Application* app) {
}
void ToggleFullscreen(struct Application* app) {
}
void DisableFrame(struct Application* app) {
}
void OpenDialog(struct Application* app, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int allowFiles, int allowDirs, int allowMultiple, int showHiddenFiles, int canCreateDirectories, int resolvesAliases, int treatPackagesAsDirectories) {
}
void SaveDialog(struct Application* app, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int showHiddenFiles, int canCreateDirectories, int treatPackagesAsDirectories) {
}
void MessageDialog(struct Application* app, char *callbackID, char *type, char *title, char *message, char *icon, char *button1, char *button2, char *button3, char *button4, char *defaultButton, char *cancelButton) {
}
void DarkModeEnabled(struct Application* app, char *callbackID) {
}
void SetApplicationMenu(struct Application* app, const char *applicationMenuJSON) {
}
void AddTrayMenu(struct Application* app, const char *menuTrayJSON) {
}
void SetTrayMenu(struct Application* app, const char *menuTrayJSON) {
}
void DeleteTrayMenuByID(struct Application* app, const char *id) {
}
void UpdateTrayMenuLabel(struct Application* app, const char* JSON) {
}
void AddContextMenu(struct Application* app, char *contextMenuJSON) {
}
void UpdateContextMenu(struct Application* app, char *contextMenuJSON) {
}

View File

@@ -0,0 +1,14 @@
package ffenestri
/*
#include "ffenestri.h"
#include "ffenestri_windows.h"
*/
import "C"
func (a *Application) processPlatformSettings() error {
return nil
}

View File

@@ -0,0 +1,5 @@
#ifndef _FFENESTRI_WINDOWS_
#define _FFENESTRI_WINDOWS_
#endif

View File

@@ -90,7 +90,7 @@ void DeleteMenu(Menu *menu) {
// Free nsmenu if we have it
if ( menu->menu != NULL ) {
msg(menu->menu, s("release"));
msg_reg(menu->menu, s("release"));
}
free(menu);
@@ -120,17 +120,17 @@ const char* createMenuClickedMessage(const char *menuItemID, const char *data, e
// Callback for text menu items
void menuItemCallback(id self, SEL cmd, id sender) {
MenuItemCallbackData *callbackData = (MenuItemCallbackData *)msg(msg(sender, s("representedObject")), s("pointerValue"));
MenuItemCallbackData *callbackData = (MenuItemCallbackData *)msg_reg(msg_reg(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));
bool state = msg_reg(callbackData->menuItem, s("state"));
msg_int(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"));
bool selected = (bool)msg_reg(callbackData->menuItem, s("state"));
// If it's already selected, exit early
if (selected) return;
@@ -142,13 +142,13 @@ void menuItemCallback(id self, SEL cmd, id sender) {
id thisMember = members[0];
int count = 0;
while(thisMember != NULL) {
msg(thisMember, s("setState:"), NSControlStateValueOff);
msg_int(thisMember, s("setState:"), NSControlStateValueOff);
count = count + 1;
thisMember = members[count];
}
// check the selected menu item
msg(callbackData->menuItem, s("setState:"), NSControlStateValueOn);
msg_int(callbackData->menuItem, s("setState:"), NSControlStateValueOn);
}
const char *menuID = callbackData->menuID;
@@ -345,61 +345,61 @@ id processAcceleratorKey(const char *key) {
void addSeparator(id menu) {
id item = msg(c("NSMenuItem"), s("separatorItem"));
msg(menu, s("addItem:"), item);
id item = msg_reg(c("NSMenuItem"), s("separatorItem"));
msg_id(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));
((id(*)(id, SEL, id, SEL, id))objc_msgSend)(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"));
((id(*)(id, SEL, id, SEL, id))objc_msgSend)(item, s("initWithTitle:action:keyEquivalent:"), title, s(action), str(key));
msg_reg(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);
msg_bool(item, s("setEnabled:"), !disabled);
msg_id(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_id(menu, s("initWithTitle:"), title);
msg_bool(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 appName = msg_reg(msg_reg(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);
msg_id(appMenuItem, s("setSubmenu:"), appMenu);
msg_id(parentMenu, s("addItem:"), appMenuItem);
id title = msg(str("Hide "), s("stringByAppendingString:"), appName);
id title = msg_id(str("Hide "), s("stringByAppendingString:"), appName);
id item = createMenuItem(title, "hide:", "h");
msg(appMenu, s("addItem:"), item);
msg_id(appMenu, s("addItem:"), item);
id hideOthers = addMenuItem(appMenu, "Hide Others", "hideOtherApplications:", "h", FALSE);
msg(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand));
msg_int(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand));
addMenuItem(appMenu, "Show All", "unhideAllApplications:", "", FALSE);
addSeparator(appMenu);
title = msg(str("Quit "), s("stringByAppendingString:"), appName);
title = msg_id(str("Quit "), s("stringByAppendingString:"), appName);
item = createMenuItem(title, "terminate:", "q");
msg(appMenu, s("addItem:"), item);
msg_id(appMenu, s("addItem:"), item);
}
void createDefaultEditMenu(id parentMenu) {
@@ -407,8 +407,8 @@ void createDefaultEditMenu(id parentMenu) {
id editMenuItem = createMenuItemNoAutorelease(str("Edit"), NULL, "");
id editMenu = createMenu(str("Edit"));
msg(editMenuItem, s("setSubmenu:"), editMenu);
msg(parentMenu, s("addItem:"), editMenuItem);
msg_id(editMenuItem, s("setSubmenu:"), editMenu);
msg_id(parentMenu, s("addItem:"), editMenuItem);
addMenuItem(editMenu, "Undo", "undo:", "z", FALSE);
addMenuItem(editMenu, "Redo", "redo:", "y", FALSE);
@@ -436,7 +436,7 @@ void processMenuRole(Menu *menu, id parentMenu, JsonNode *item) {
}
if ( STREQ(roleName, "hideothers")) {
id hideOthers = addMenuItem(parentMenu, "Hide Others", "hideOtherApplications:", "h", FALSE);
msg(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand));
msg_int(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand));
return;
}
if ( STREQ(roleName, "unhide")) {
@@ -473,7 +473,7 @@ void processMenuRole(Menu *menu, id parentMenu, JsonNode *item) {
}
if( STREQ(roleName, "pasteandmatchstyle")) {
id pasteandmatchstyle = addMenuItem(parentMenu, "Paste and Match Style", "pasteandmatchstyle:", "v", FALSE);
msg(pasteandmatchstyle, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagShift | NSEventModifierFlagCommand));
msg_int(pasteandmatchstyle, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagShift | NSEventModifierFlagCommand));
}
if ( STREQ(roleName, "selectall")) {
addMenuItem(parentMenu, "Select All", "selectAll:", "a", FALSE);
@@ -508,6 +508,7 @@ unsigned long parseModifiers(const char **modifiers) {
const char *thisModifier = modifiers[0];
int count = 0;
while( thisModifier != NULL ) {
// Determine flags
if( STREQ(thisModifier, "cmdorctrl") ) {
result |= NSEventModifierFlagCommand;
@@ -521,7 +522,7 @@ unsigned long parseModifiers(const char **modifiers) {
if( STREQ(thisModifier, "super") ) {
result |= NSEventModifierFlagCommand;
}
if( STREQ(thisModifier, "control") ) {
if( STREQ(thisModifier, "ctrl") ) {
result |= NSEventModifierFlagControl;
}
count++;
@@ -539,18 +540,18 @@ id processRadioMenuItem(Menu *menu, id parentmenu, const char *title, const char
// Create a MenuItemCallbackData
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, item, menuid, Radio);
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), callback);
msg(item, s("setRepresentedObject:"), wrappedId);
id wrappedId = msg_id(c("NSValue"), s("valueWithPointer:"), (id)callback);
msg_id(item, s("setRepresentedObject:"), wrappedId);
id key = processAcceleratorKey(acceleratorkey);
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuItemCallback:"), key);
((id(*)(id, SEL, id, SEL, id))objc_msgSend)(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_bool(item, s("setEnabled:"), !disabled);
msg_reg(item, s("autorelease"));
msg_int(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
msg(parentmenu, s("addItem:"), item);
msg_id(parentmenu, s("addItem:"), item);
return item;
}
@@ -565,74 +566,42 @@ id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const c
// 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);
id wrappedId = msg_id(c("NSValue"), s("valueWithPointer:"), (id)callback);
msg_id(item, s("setRepresentedObject:"), wrappedId);
((id(*)(id, SEL, id, SEL, id))objc_msgSend)(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuItemCallback:"), str(key));
msg_bool(item, s("setEnabled:"), !disabled);
msg_reg(item, s("autorelease"));
msg_int(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
msg_id(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, const char* tooltip, const char* image, const char* fontName, int fontSize, const char* RGBA, bool templateImage) {
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);
if( tooltip != NULL ) {
msg(item, s("setToolTip:"), str(tooltip));
}
// Process image
if( image != NULL && strlen(image) > 0) {
id data = ALLOC("NSData");
id imageData = msg(data, s("initWithBase64EncodedString:options:"), str(image), 0);
id nsimage = ALLOC("NSImage");
msg(nsimage, s("initWithData:"), imageData);
if( templateImage ) {
msg(nsimage, s("template"), YES);
}
msg(item, s("setImage:"), nsimage);
}
id createAttributedString(const char* title, const char* fontName, int fontSize, const char* RGBA) {
// Process Menu Item attributes
id dictionary = ALLOC_INIT("NSMutableDictionary");
// Process font
id font;
CGFloat fontSizeFloat = (CGFloat)fontSize;
// Check if valid
id fontNameAsNSString = str(fontName);
id fontsOnSystem = msg(msg(c("NSFontManager"), s("sharedFontManager")), s("availableFonts"));
bool valid = msg(fontsOnSystem, s("containsObject:"), fontNameAsNSString);
if( valid ) {
font = msg(c("NSFont"), s("fontWithName:size:"), fontNameAsNSString, fontSizeFloat);
} else {
bool supportsMonospacedDigitSystemFont = (bool) msg(c("NSFont"), s("respondsToSelector:"), s("monospacedDigitSystemFontOfSize:weight:"));
id font = ((id(*)(id, SEL, id, CGFloat))objc_msgSend)(c("NSFont"), s("fontWithName:size:"), fontNameAsNSString, fontSizeFloat);
if( font == NULL ) {
bool supportsMonospacedDigitSystemFont = (bool) ((id(*)(id, SEL, SEL))objc_msgSend)(c("NSFont"), s("respondsToSelector:"), s("monospacedDigitSystemFontOfSize:weight:"));
if( supportsMonospacedDigitSystemFont ) {
font = msg(c("NSFont"), s("monospacedDigitSystemFontOfSize:weight:"), fontSizeFloat, NSFontWeightRegular);
font = ((id(*)(id, SEL, CGFloat, CGFloat))objc_msgSend)(c("NSFont"), s("monospacedDigitSystemFontOfSize:weight:"), fontSizeFloat, (CGFloat)NSFontWeightRegular);
} else {
font = msg(c("NSFont"), s("menuFontOfSize:"), fontSizeFloat);
font = ((id(*)(id, SEL, CGFloat))objc_msgSend)(c("NSFont"), s("menuFontOfSize:"), fontSizeFloat);
}
}
// Add font to dictionary
msg(dictionary, s("setObject:forKey:"), font, lookupStringConstant(str("NSFontAttributeName")));
// Add offset to dictionary
id offset = msg(c("NSNumber"), s("numberWithFloat:"), 0.0);
msg(dictionary, s("setObject:forKey:"), offset, lookupStringConstant(str("NSBaselineOffsetAttributeName")));
id fan = lookupStringConstant(str("NSFontAttributeName"));
msg_id_id(dictionary, s("setObject:forKey:"), font, fan);
id offset = msg_float(c("NSNumber"), s("numberWithFloat:"), (float)0.0);
id offsetAttrName = lookupStringConstant(str("NSBaselineOffsetAttributeName"));
msg_id_id(dictionary, s("setObject:forKey:"), offset, offsetAttrName);
// RGBA
if( RGBA != NULL && strlen(RGBA) > 0) {
unsigned short r, g, b, a;
@@ -641,32 +610,77 @@ id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char
r = g = b = a = 255;
int count = sscanf(RGBA, "#%02hx%02hx%02hx%02hx", &r, &g, &b, &a);
if (count > 0) {
id colour = msg(c("NSColor"), s("colorWithCalibratedRed:green:blue:alpha:"),
(float)r / 255.0,
(float)g / 255.0,
(float)b / 255.0,
(float)a / 255.0);
msg(dictionary, s("setObject:forKey:"), colour, lookupStringConstant(str("NSForegroundColorAttributeName")));
msg(colour, s("release"));
id colour = ((id(*)(id, SEL, CGFloat, CGFloat, CGFloat, CGFloat))objc_msgSend)(c("NSColor"), s("colorWithCalibratedRed:green:blue:alpha:"),
(CGFloat)r / (CGFloat)255.0,
(CGFloat)g / (CGFloat)255.0,
(CGFloat)b / (CGFloat)255.0,
(CGFloat)a / (CGFloat)255.0);
id NSForegroundColorAttributeName = lookupStringConstant(str("NSForegroundColorAttributeName"));
msg_id_id(dictionary, s("setObject:forKey:"), colour, NSForegroundColorAttributeName);
msg_reg(colour, s("autorelease"));
}
}
id attributedString = ALLOC("NSMutableAttributedString");
msg(attributedString, s("initWithString:attributes:"), str(title), dictionary);
msg(dictionary, s("release"));
msg_id_id(attributedString, s("initWithString:attributes:"), str(title), dictionary);
msg_reg(attributedString, s("autorelease"));
msg_reg(dictionary, s("release"));
return attributedString;
}
msg(item, s("setAttributedTitle:"), attributedString);
msg(attributedString, s("autorelease"));
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, const char* tooltip, const char* image, const char* fontName, int fontSize, const char* RGBA, bool templateImage, bool alternate) {
id item = ALLOC("NSMenuItem");
msg(item, s("setEnabled:"), !disabled);
msg(item, s("autorelease"));
// Create a MenuItemCallbackData
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, item, menuid, Text);
id wrappedId = msg_id(c("NSValue"), s("valueWithPointer:"), (id)callback);
msg_id(item, s("setRepresentedObject:"), wrappedId);
if( !alternate ) {
id key = processAcceleratorKey(acceleratorkey);
((id(*)(id, SEL, id, SEL, id))objc_msgSend)(item, s("initWithTitle:action:keyEquivalent:"), str(title),
s("menuItemCallback:"), key);
} else {
((id(*)(id, SEL, id, SEL, id))objc_msgSend)(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuItemCallback:"), str(""));
}
if( tooltip != NULL ) {
msg_id(item, s("setToolTip:"), str(tooltip));
}
// Process image
if( image != NULL && strlen(image) > 0) {
id data = ALLOC("NSData");
id imageData = ((id(*)(id, SEL, id, int))objc_msgSend)(data, s("initWithBase64EncodedString:options:"), str(image), 0);
id nsimage = ALLOC("NSImage");
msg_id(nsimage, s("initWithData:"), imageData);
if( templateImage ) {
msg_bool(nsimage, s("setTemplate:"), YES);
}
msg_id(item, s("setImage:"), nsimage);
}
id attributedString = createAttributedString(title, fontName, fontSize, RGBA);
msg_id(item, s("setAttributedTitle:"), attributedString);
//msg_id(item, s("setTitle:"), str(title));
msg_bool(item, s("setEnabled:"), !disabled);
msg_reg(item, s("autorelease"));
// Process modifiers
if( modifiers != NULL ) {
if( modifiers != NULL && !alternate) {
unsigned long modifierFlags = parseModifiers(modifiers);
msg(item, s("setKeyEquivalentModifierMask:"), modifierFlags);
((id(*)(id, SEL, unsigned long))objc_msgSend)(item, s("setKeyEquivalentModifierMask:"), modifierFlags);
}
msg(parentMenu, s("addItem:"), item);
// alternate
if( alternate ) {
msg_bool(item, s("setAlternate:"), true);
msg_int(item, s("setKeyEquivalentModifierMask:"), NSEventModifierFlagOption);
}
msg_id(parentMenu, s("addItem:"), item);
return item;
}
@@ -700,8 +714,8 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
id thisMenuItem = createMenuItemNoAutorelease(str(name), NULL, "");
id thisMenu = createMenu(str(name));
msg(thisMenuItem, s("setSubmenu:"), thisMenu);
msg(parentMenu, s("addItem:"), thisMenuItem);
msg_id(thisMenuItem, s("setSubmenu:"), thisMenu);
msg_id(parentMenu, s("addItem:"), thisMenuItem);
JsonNode *submenuItems = json_find_member(submenu, "Items");
// If we have no items, just return
@@ -726,6 +740,11 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
label = "(empty)";
}
// Is this an alternate menu item?
bool alternate = false;
getJSONBool(item, "MacAlternate", &alternate);
const char *menuid = getJSONString(item, "ID");
if ( menuid == NULL) {
menuid = "";
@@ -746,7 +765,7 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
bool templateImage = false;
getJSONBool(item, "MacTemplateImage", &templateImage);
int fontSize = 12;
int fontSize = 0;
getJSONInt(item, "FontSize", &fontSize);
// If we have an accelerator
@@ -781,7 +800,7 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
if( type != NULL ) {
if( STREQ(type->string_, "Text")) {
processTextMenuItem(menu, parentMenu, label, menuid, disabled, acceleratorkey, modifiers, tooltip, image, fontName, fontSize, RGBA, templateImage);
processTextMenuItem(menu, parentMenu, label, menuid, disabled, acceleratorkey, modifiers, tooltip, image, fontName, fontSize, RGBA, templateImage, alternate);
}
else if ( STREQ(type->string_, "Separator")) {
addSeparator(parentMenu);

View File

@@ -105,10 +105,12 @@ id processRadioMenuItem(Menu *menu, id parentmenu, const char *title, const char
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, const char* tooltip, const char* image, const char* fontName, int fontSize, const char* RGBA, bool templateImage);
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, const char* tooltip, const char* image, const char* fontName, int fontSize, const char* RGBA, bool templateImage, bool alternate);
void processMenuItem(Menu *menu, id parentMenu, JsonNode *item);
void processMenuData(Menu *menu, JsonNode *menuData);
void processRadioGroupJSON(Menu *menu, JsonNode *radioGroup) ;
void processRadioGroupJSON(Menu *menu, JsonNode *radioGroup);
id GetMenu(Menu *menu);
id createAttributedString(const char* title, const char* fontName, int fontSize, const char* RGBA);
#endif //ASSETS_C_MENU_DARWIN_H

View File

@@ -6,6 +6,8 @@
#include "traymenu_darwin.h"
#include "trayicons.h"
extern Class trayMenuDelegateClass;
// A cache for all our tray menu icons
// Global because it's a singleton
struct hashmap_s trayIconCache;
@@ -29,12 +31,23 @@ TrayMenu* NewTrayMenu(const char* menuJSON) {
result->ID = mustJSONString(processedJSON, "ID");
result->label = mustJSONString(processedJSON, "Label");
result->icon = mustJSONString(processedJSON, "Icon");
JsonNode* processedMenu = mustJSONObject(processedJSON, "ProcessedMenu");
result->icon = mustJSONString(processedJSON, "Image");
result->fontName = getJSONString(processedJSON, "FontName");
result->RGBA = getJSONString(processedJSON, "RGBA");
getJSONBool(processedJSON, "MacTemplateImage", &result->templateImage);
result->fontSize = 0;
getJSONInt(processedJSON, "FontSize", &result->fontSize);
result->tooltip = NULL;
result->tooltip = getJSONString(processedJSON, "Tooltip");
result->disabled = false;
getJSONBool(processedJSON, "Disabled", &result->disabled);
// Create the menu
JsonNode* processedMenu = mustJSONObject(processedJSON, "ProcessedMenu");
result->menu = NewMenu(processedMenu);
result->delegate = NULL;
// Init tray status bar item
result->statusbaritem = NULL;
@@ -50,15 +63,23 @@ void DumpTrayMenu(TrayMenu* trayMenu) {
}
void UpdateTrayLabel(TrayMenu *trayMenu, const char *label) {
void UpdateTrayLabel(TrayMenu *trayMenu, const char *label, const char *fontName, int fontSize, const char *RGBA, const char *tooltip, bool disabled) {
// Exit early if NULL
if( trayMenu->label == NULL ) {
return;
}
// Update button label
id statusBarButton = msg(trayMenu->statusbaritem, s("button"));
msg(statusBarButton, s("setTitle:"), str(label));
id statusBarButton = msg_reg(trayMenu->statusbaritem, s("button"));
id attributedString = createAttributedString(label, fontName, fontSize, RGBA);
if( tooltip != NULL ) {
msg_id(statusBarButton, s("setToolTip:"), str(tooltip));
}
msg_bool(statusBarButton, s("setEnabled:"), !disabled);
msg_id(statusBarButton, s("setAttributedTitle:"), attributedString);
}
void UpdateTrayIcon(TrayMenu *trayMenu) {
@@ -68,44 +89,64 @@ void UpdateTrayIcon(TrayMenu *trayMenu) {
return;
}
id statusBarButton = msg(trayMenu->statusbaritem, s("button"));
id statusBarButton = msg_reg(trayMenu->statusbaritem, s("button"));
// Empty icon means remove it
if( STREMPTY(trayMenu->icon) ) {
// Remove image
msg(statusBarButton, s("setImage:"), NULL);
msg_id(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);
// If we don't have the image in the icon cache then assume it's base64 encoded image data
if (trayImage == NULL) {
id data = ALLOC("NSData");
id imageData = ((id(*)(id, SEL, id, int))objc_msgSend)(data, s("initWithBase64EncodedString:options:"), str(trayMenu->icon), 0);
trayImage = ALLOC("NSImage");
msg_id(trayImage, s("initWithData:"), imageData);
if( trayMenu->templateImage ) {
msg_bool(trayImage, s("setTemplate:"), YES);
}
}
msg_int(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition);
msg_id(statusBarButton, s("setImage:"), trayImage);
}
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 statusBar = msg_reg( c("NSStatusBar"), s("systemStatusBar") );
trayMenu->statusbaritem = ((id(*)(id, SEL, CGFloat))objc_msgSend)(statusBar, s("statusItemWithLength:"), NSVariableStatusItemLength);
msg_reg(trayMenu->statusbaritem, s("retain"));
}
id statusBarButton = msg(trayMenu->statusbaritem, s("button"));
msg(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition);
id statusBarButton = msg_reg(trayMenu->statusbaritem, s("button"));
msg_uint(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition);
// Update the icon if needed
UpdateTrayIcon(trayMenu);
// Update the label if needed
UpdateTrayLabel(trayMenu, trayMenu->label);
UpdateTrayLabel(trayMenu, trayMenu->label, trayMenu->fontName, trayMenu->fontSize, trayMenu->RGBA, trayMenu->tooltip, trayMenu->disabled);
// Update the menu
id menu = GetMenu(trayMenu->menu);
msg(trayMenu->statusbaritem, s("setMenu:"), menu);
objc_setAssociatedObject(menu, "trayMenuID", str(trayMenu->ID), OBJC_ASSOCIATION_ASSIGN);
// Create delegate
id trayMenuDelegate = msg_reg((id)trayMenuDelegateClass, s("new"));
msg_id(menu, s("setDelegate:"), trayMenuDelegate);
objc_setAssociatedObject(trayMenuDelegate, "menu", menu, OBJC_ASSOCIATION_ASSIGN);
// Create menu delegate
trayMenu->delegate = trayMenuDelegate;
msg_id(trayMenu->statusbaritem, s("setMenu:"), menu);
}
// UpdateTrayMenuInPlace receives 2 menus. The current menu gets
@@ -147,12 +188,16 @@ void DeleteTrayMenu(TrayMenu* trayMenu) {
// 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"));
id statusBar = msg_reg( c("NSStatusBar"), s("systemStatusBar") );
msg_id(statusBar, s("removeStatusItem:"), trayMenu->statusbaritem);
msg_reg(trayMenu->statusbaritem, s("release"));
trayMenu->statusbaritem = NULL;
}
if ( trayMenu->delegate != NULL ) {
msg_reg(trayMenu->delegate, s("release"));
}
// Free the tray menu memory
MEMFREE(trayMenu);
}
@@ -182,9 +227,9 @@ void LoadTrayIcons() {
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 imageData = ((id(*)(id, SEL, id, int))objc_msgSend)(c("NSData"), s("dataWithBytes:length:"), data, length);
id trayImage = ALLOC("NSImage");
msg(trayImage, s("initWithData:"), imageData);
msg_id(trayImage, s("initWithData:"), imageData);
hashmap_put(&trayIconCache, (const char *)name, strlen((const char *)name), trayImage);
}
}

View File

@@ -13,14 +13,24 @@ typedef struct {
const char *label;
const char *icon;
const char *ID;
const char *tooltip;
bool templateImage;
const char *fontName;
int fontSize;
const char *RGBA;
bool disabled;
Menu* menu;
id statusbaritem;
int trayIconPosition;
unsigned int trayIconPosition;
JsonNode* processedJSON;
id delegate;
} TrayMenu;
TrayMenu* NewTrayMenu(const char *trayJSON);
@@ -28,7 +38,7 @@ void DumpTrayMenu(TrayMenu* trayMenu);
void ShowTrayMenu(TrayMenu* trayMenu);
void UpdateTrayMenuInPlace(TrayMenu* currentMenu, TrayMenu* newMenu);
void UpdateTrayIcon(TrayMenu *trayMenu);
void UpdateTrayLabel(TrayMenu *trayMenu, const char*);
void UpdateTrayLabel(TrayMenu *trayMenu, const char *label, const char *fontName, int fontSize, const char *RGBA, const char *tooltip, bool disabled);
void LoadTrayIcons();
void UnloadTrayIcons();

View File

@@ -16,6 +16,11 @@ TrayMenuStore* NewTrayMenuStore() {
ABORT("[NewTrayMenuStore] Not enough memory to allocate trayMenuMap!");
}
if (pthread_mutex_init(&result->lock, NULL) != 0) {
printf("\n mutex init has failed\n");
exit(1);
}
return result;
}
@@ -25,15 +30,19 @@ int dumpTrayMenu(void *const context, struct hashmap_element_s *const e) {
}
void DumpTrayMenuStore(TrayMenuStore* store) {
pthread_mutex_lock(&store->lock);
hashmap_iterate_pairs(&store->trayMenuMap, dumpTrayMenu, NULL);
pthread_mutex_unlock(&store->lock);
}
void AddTrayMenuToStore(TrayMenuStore* store, const char* menuJSON) {
TrayMenu* newMenu = NewTrayMenu(menuJSON);
pthread_mutex_lock(&store->lock);
//TODO: check if there is already an entry for this menu
hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
pthread_mutex_unlock(&store->lock);
}
int showTrayMenu(void *const context, struct hashmap_element_s *const e) {
@@ -43,12 +52,13 @@ int showTrayMenu(void *const context, struct hashmap_element_s *const e) {
}
void ShowTrayMenusInStore(TrayMenuStore* store) {
pthread_mutex_lock(&store->lock);
if( hashmap_num_entries(&store->trayMenuMap) > 0 ) {
hashmap_iterate_pairs(&store->trayMenuMap, showTrayMenu, NULL);
}
pthread_mutex_unlock(&store->lock);
}
int freeTrayMenu(void *const context, struct hashmap_element_s *const e) {
DeleteTrayMenu(e->data);
return -1;
@@ -65,22 +75,39 @@ void DeleteTrayMenuStore(TrayMenuStore *store) {
// Destroy tray menu map
hashmap_destroy(&store->trayMenuMap);
pthread_mutex_destroy(&store->lock);
}
TrayMenu* GetTrayMenuFromStore(TrayMenuStore* store, const char* menuID) {
// Get the current menu
return hashmap_get(&store->trayMenuMap, menuID, strlen(menuID));
pthread_mutex_lock(&store->lock);
TrayMenu* result = hashmap_get(&store->trayMenuMap, menuID, strlen(menuID));
pthread_mutex_unlock(&store->lock);
return result;
}
TrayMenu* MustGetTrayMenuFromStore(TrayMenuStore* store, const char* menuID) {
// Get the current menu
pthread_mutex_lock(&store->lock);
TrayMenu* result = hashmap_get(&store->trayMenuMap, menuID, strlen(menuID));
pthread_mutex_unlock(&store->lock);
if (result == NULL ) {
ABORT("Unable to find TrayMenu with ID '%s' in the TrayMenuStore!", menuID);
}
return result;
}
void DeleteTrayMenuInStore(TrayMenuStore* store, const char* ID) {
TrayMenu *menu = MustGetTrayMenuFromStore(store, ID);
pthread_mutex_lock(&store->lock);
hashmap_remove(&store->trayMenuMap, ID, strlen(ID));
pthread_mutex_unlock(&store->lock);
DeleteTrayMenu(menu);
}
void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON) {
// Parse the JSON
JsonNode *parsedUpdate = mustParseJSON(JSON);
@@ -91,7 +118,17 @@ void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON) {
// Check we have this menu
TrayMenu *menu = MustGetTrayMenuFromStore(store, ID);
UpdateTrayLabel(menu, Label);
const char *fontName = getJSONString(parsedUpdate, "FontName");
const char *RGBA = getJSONString(parsedUpdate, "RGBA");
int fontSize = 0;
getJSONInt(parsedUpdate, "FontSize", &fontSize);
const char *tooltip = getJSONString(parsedUpdate, "Tooltip");
bool disabled = false;
getJSONBool(parsedUpdate, "Disabled", &disabled);
UpdateTrayLabel(menu, Label, fontName, fontSize, RGBA, tooltip, disabled);
}
@@ -105,7 +142,9 @@ void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON) {
// If we don't have a menu, we create one
if ( currentMenu == NULL ) {
// Store the new menu
pthread_mutex_lock(&store->lock);
hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
pthread_mutex_unlock(&store->lock);
// Show it
ShowTrayMenu(newMenu);
@@ -116,7 +155,9 @@ void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON) {
// Save the status bar reference
newMenu->statusbaritem = currentMenu->statusbaritem;
pthread_mutex_lock(&store->lock);
hashmap_remove(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID));
pthread_mutex_unlock(&store->lock);
// Delete the current menu
DeleteMenu(currentMenu->menu);
@@ -125,9 +166,10 @@ void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON) {
// Free the tray menu memory
MEMFREE(currentMenu);
pthread_mutex_lock(&store->lock);
hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
pthread_mutex_unlock(&store->lock);
// Show the updated menu
ShowTrayMenu(newMenu);
}

View File

@@ -5,6 +5,10 @@
#ifndef TRAYMENUSTORE_DARWIN_H
#define TRAYMENUSTORE_DARWIN_H
#include "traymenu_darwin.h"
#include <pthread.h>
typedef struct {
int dummy;
@@ -13,6 +17,8 @@ typedef struct {
// It maps tray IDs to TrayMenu*
struct hashmap_s trayMenuMap;
pthread_mutex_t lock;
} TrayMenuStore;
TrayMenuStore* NewTrayMenuStore();
@@ -22,6 +28,9 @@ void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON);
void ShowTrayMenusInStore(TrayMenuStore* store);
void DeleteTrayMenuStore(TrayMenuStore* store);
TrayMenu* GetTrayMenuByID(TrayMenuStore* store, const char* menuID);
void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON);
void DeleteTrayMenuInStore(TrayMenuStore* store, const char* id);
#endif //TRAYMENUSTORE_DARWIN_H

View File

@@ -40,7 +40,7 @@ func Mkdir(dirname string) error {
// Returns error on failure
func MkDirs(fullPath string, mode ...os.FileMode) error {
var perms os.FileMode
perms = 0700
perms = 0755
if len(mode) == 1 {
perms = mode[0]
}
@@ -243,7 +243,7 @@ func CopyDir(src string, dst string) (err error) {
return fmt.Errorf("destination already exists")
}
err = os.MkdirAll(dst, si.Mode())
err = MkDirs(dst)
if err != nil {
return
}

View File

@@ -112,6 +112,9 @@ func (a *AssetBundle) processHTML(htmldata string) error {
if attr.Key == "as" && attr.Val == "script" {
asset.Type = AssetTypes.JS
}
if attr.Key == "rel" && attr.Val == "modulepreload" {
asset.Type = AssetTypes.JS
}
}
// Ensure we don't include duplicates

View File

@@ -37,6 +37,7 @@ type ProcessedMenuItem struct {
// Image - base64 image data
Image string `json:",omitempty"`
MacTemplateImage bool `json:", omitempty"`
MacAlternate bool `json:", omitempty"`
// Tooltip
Tooltip string `json:",omitempty"`
@@ -60,6 +61,7 @@ func NewProcessedMenuItem(menuItemMap *MenuItemMap, menuItem *menu.MenuItem) *Pr
FontName: menuItem.FontName,
Image: menuItem.Image,
MacTemplateImage: menuItem.MacTemplateImage,
MacAlternate: menuItem.MacAlternate,
Tooltip: menuItem.Tooltip,
}

View File

@@ -3,29 +3,39 @@ package menumanager
import (
"encoding/json"
"fmt"
"strconv"
"sync"
"github.com/pkg/errors"
"github.com/wailsapp/wails/v2/pkg/menu"
"sync"
)
var trayMenuID int
var trayMenuIDMutex sync.Mutex
func generateTrayID() string {
var idStr string
trayMenuIDMutex.Lock()
result := fmt.Sprintf("%d", trayMenuID)
idStr = strconv.Itoa(trayMenuID)
trayMenuID++
trayMenuIDMutex.Unlock()
return result
return idStr
}
type TrayMenu struct {
ID string
Label string
Icon string
menuItemMap *MenuItemMap
menu *menu.Menu
ProcessedMenu *WailsMenu
ID string
Label string
FontSize int
FontName string
Disabled bool
Tooltip string `json:",omitempty"`
Image string
MacTemplateImage bool
RGBA string
menuItemMap *MenuItemMap
menu *menu.Menu
ProcessedMenu *WailsMenu
trayMenu *menu.TrayMenu
}
func (t *TrayMenu) AsJSON() (string, error) {
@@ -39,10 +49,17 @@ func (t *TrayMenu) AsJSON() (string, error) {
func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu {
result := &TrayMenu{
Label: trayMenu.Label,
Icon: trayMenu.Icon,
menu: trayMenu.Menu,
menuItemMap: NewMenuItemMap(),
Label: trayMenu.Label,
FontName: trayMenu.FontName,
FontSize: trayMenu.FontSize,
Disabled: trayMenu.Disabled,
Tooltip: trayMenu.Tooltip,
Image: trayMenu.Image,
MacTemplateImage: trayMenu.MacTemplateImage,
menu: trayMenu.Menu,
RGBA: trayMenu.RGBA,
menuItemMap: NewMenuItemMap(),
trayMenu: trayMenu,
}
result.menuItemMap.AddMenu(trayMenu.Menu)
@@ -51,6 +68,28 @@ func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu {
return result
}
func (m *Manager) OnTrayMenuOpen(id string) {
trayMenu, ok := m.trayMenus[id]
if !ok {
return
}
if trayMenu.trayMenu.OnOpen == nil {
return
}
go trayMenu.trayMenu.OnOpen()
}
func (m *Manager) OnTrayMenuClose(id string) {
trayMenu, ok := m.trayMenus[id]
if !ok {
return
}
if trayMenu.trayMenu.OnClose == nil {
return
}
go trayMenu.trayMenu.OnClose()
}
func (m *Manager) AddTrayMenu(trayMenu *menu.TrayMenu) (string, error) {
newTrayMenu := NewTrayMenu(trayMenu)
@@ -65,6 +104,14 @@ func (m *Manager) AddTrayMenu(trayMenu *menu.TrayMenu) (string, error) {
return newTrayMenu.AsJSON()
}
func (m *Manager) GetTrayID(trayMenu *menu.TrayMenu) (string, error) {
trayID, exists := m.trayMenuPointers[trayMenu]
if !exists {
return "", fmt.Errorf("Unable to find menu ID for tray menu!")
}
return trayID, nil
}
// SetTrayMenu updates or creates a menu
func (m *Manager) SetTrayMenu(trayMenu *menu.TrayMenu) (string, error) {
trayID, trayMenuKnown := m.trayMenuPointers[trayMenu]
@@ -102,13 +149,27 @@ func (m *Manager) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) (string, error) {
}
type LabelUpdate struct {
ID string
Label string
ID string
Label string
FontName string
FontSize int
RGBA string
Disabled bool
Tooltip string
Image string
MacTemplateImage bool
}
update := &LabelUpdate{
ID: trayID,
Label: trayMenu.Label,
ID: trayID,
Label: trayMenu.Label,
FontName: trayMenu.FontName,
FontSize: trayMenu.FontSize,
Disabled: trayMenu.Disabled,
Tooltip: trayMenu.Tooltip,
Image: trayMenu.Image,
MacTemplateImage: trayMenu.MacTemplateImage,
RGBA: trayMenu.RGBA,
}
data, err := json.Marshal(update)

View File

@@ -37,6 +37,7 @@ type Client interface {
SetTrayMenu(trayMenuJSON string)
UpdateTrayMenuLabel(JSON string)
UpdateContextMenu(contextMenuJSON string)
DeleteTrayMenuByID(id string)
}
// DispatchClient is what the frontends use to interface with the

View File

@@ -32,6 +32,14 @@ func menuMessageParser(message string) (*parsedMessage, error) {
callbackid := message[2:]
topic = "menu:clicked"
data = callbackid
case 'o':
callbackid := message[2:]
topic = "menu:ontrayopen"
data = callbackid
case 'c':
callbackid := message[2:]
topic = "menu:ontrayclose"
data = callbackid
default:
return nil, fmt.Errorf("invalid menu message: %s", message)
}

View File

@@ -21,13 +21,14 @@ var messageParsers = map[byte]func(string) (*parsedMessage, error){
'M': menuMessageParser,
'T': trayMessageParser,
'X': contextMenusMessageParser,
'U': urlMessageParser,
}
// Parse will attempt to parse the given message
func Parse(message string) (*parsedMessage, error) {
if len(message) == 0 {
return nil, fmt.Errorf("MessageParser received blank message");
return nil, fmt.Errorf("MessageParser received blank message")
}
parseMethod := messageParsers[message[0]]

View File

@@ -40,7 +40,8 @@ func systemMessageParser(message string) (*parsedMessage, error) {
// This is our startup hook - the frontend is now ready
case 'S':
topic := "hooks:startup"
responseMessage = &parsedMessage{Topic: topic, Data: nil}
startupURL := message[1:]
responseMessage = &parsedMessage{Topic: topic, Data: startupURL}
default:
return nil, fmt.Errorf("Invalid message to systemMessageParser()")
}

View File

@@ -0,0 +1,20 @@
package message
import "fmt"
// urlMessageParser does what it says on the tin!
func urlMessageParser(message string) (*parsedMessage, error) {
// Sanity check: URL messages must be at least 2 bytes
if len(message) < 2 {
return nil, fmt.Errorf("log message was an invalid length")
}
// Switch on the log type
switch message[1] {
case 'C':
return &parsedMessage{Topic: "url:handler", Data: message[2:]}, nil
default:
return nil, fmt.Errorf("url message type '%c' invalid", message[1])
}
}

View File

@@ -527,6 +527,17 @@ func (d *Dispatcher) processMenuMessage(result *servicebus.Message) {
for _, client := range d.clients {
client.frontend.UpdateTrayMenuLabel(updatedTrayMenuLabel)
}
case "deletetraymenu":
traymenuid, ok := result.Data().(string)
if !ok {
d.logger.Error("Invalid data for 'menufrontend:updatetraymenulabel' : %#v",
result.Data())
return
}
for _, client := range d.clients {
client.frontend.DeleteTrayMenuByID(traymenuid)
}
default:
d.logger.Error("Unknown menufrontend command: %s", command)

View File

@@ -649,6 +649,18 @@
});
}
function deleteTrayMenu(id) {
trays.update((current) => {
// Remove existing if it exists, else add
const index = current.findIndex(item => item.ID === id);
if ( index === -1 ) {
return log("ERROR: Attempted to delete tray index ")
}
current.splice(index, 1);
return current;
});
}
let selectedMenu = writable(null);
function fade(node, { delay = 0, duration = 400, easing = identity } = {}) {
@@ -1666,6 +1678,11 @@
let trayLabelData = JSON.parse(updateTrayLabelJSON);
updateTrayLabel(trayLabelData);
break
case 'D':
// Delete Tray Menu
const id = trayMessage.slice(1);
deleteTrayMenu(id);
break
default:
log('Unknown tray message: ' + message.data);
}

View File

@@ -1,6 +1,6 @@
{
"name": "@wails/runtime",
"version": "1.3.10",
"version": "1.3.12",
"description": "Wails V2 Javascript runtime library",
"main": "main.js",
"types": "runtime.d.ts",

View File

@@ -49,4 +49,16 @@ export function updateTrayLabel(tray) {
})
}
export function deleteTrayMenu(id) {
trays.update((current) => {
// Remove existing if it exists, else add
const index = current.findIndex(item => item.ID === id);
if ( index === -1 ) {
return log("ERROR: Attempted to delete tray index ", id, "but it doesn't exist")
}
current.splice(index, 1);
return current;
})
}
export let selectedMenu = writable(null);

View File

@@ -10,7 +10,7 @@ The lightweight framework for web-like apps
/* jshint esversion: 6 */
import {setTray, hideOverlay, showOverlay, updateTrayLabel} from "./store";
import {setTray, hideOverlay, showOverlay, updateTrayLabel, deleteTrayMenu} from "./store";
import {log} from "./log";
let websocket = null;
@@ -154,6 +154,11 @@ function handleMessage(message) {
let trayLabelData = JSON.parse(updateTrayLabelJSON)
updateTrayLabel(trayLabelData)
break
case 'D':
// Delete Tray Menu
const id = trayMessage.slice(1);
deleteTrayMenu(id)
break
default:
log('Unknown tray message: ' + message.data);
}

View File

@@ -10,6 +10,7 @@ type Menu interface {
UpdateApplicationMenu()
UpdateContextMenu(contextMenu *menu.ContextMenu)
SetTrayMenu(trayMenu *menu.TrayMenu)
DeleteTrayMenu(trayMenu *menu.TrayMenu)
UpdateTrayMenuLabel(trayMenu *menu.TrayMenu)
}
@@ -39,3 +40,7 @@ func (m *menuRuntime) SetTrayMenu(trayMenu *menu.TrayMenu) {
func (m *menuRuntime) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) {
m.bus.Publish("menu:updatetraymenulabel", trayMenu)
}
func (m *menuRuntime) DeleteTrayMenu(trayMenu *menu.TrayMenu) {
m.bus.Publish("menu:deletetraymenu", trayMenu)
}

View File

@@ -77,6 +77,12 @@ func (m *Menu) Start() error {
splitTopic := strings.Split(menuMessage.Topic(), ":")
menuMessageType := splitTopic[1]
switch menuMessageType {
case "ontrayopen":
trayID := menuMessage.Data().(string)
m.menuManager.OnTrayMenuOpen(trayID)
case "ontrayclose":
trayID := menuMessage.Data().(string)
m.menuManager.OnTrayMenuClose(trayID)
case "clicked":
if len(splitTopic) != 2 {
m.logger.Error("Received clicked message with invalid topic format. Expected 2 sections in topic, got %s", splitTopic)
@@ -137,6 +143,17 @@ func (m *Menu) Start() error {
// Notify frontend of menu change
m.bus.Publish("menufrontend:settraymenu", updatedMenu)
case "deletetraymenu":
trayMenu := menuMessage.Data().(*menu.TrayMenu)
trayID, err := m.menuManager.GetTrayID(trayMenu)
if err != nil {
m.logger.Trace("%s", err.Error())
return
}
// Notify frontend of menu change
m.bus.Publish("menufrontend:deletetraymenu", trayID)
case "updatetraymenulabel":
trayMenu := menuMessage.Data().(*menu.TrayMenu)
updatedLabel, err := m.menuManager.UpdateTrayMenuLabel(trayMenu)

View File

@@ -34,6 +34,9 @@ type Runtime struct {
// Startup Hook
startupOnce sync.Once
// Service bus
bus *servicebus.ServiceBus
}
// NewRuntime creates a new runtime subsystem
@@ -58,6 +61,7 @@ func NewRuntime(ctx context.Context, bus *servicebus.ServiceBus, logger *logger.
runtime: runtime.New(bus),
startupCallback: startupCallback,
ctx: ctx,
bus: bus,
}
return result, nil
@@ -79,7 +83,15 @@ func (r *Runtime) Start() error {
case "startup":
if r.startupCallback != nil {
r.startupOnce.Do(func() {
go r.startupCallback(r.runtime)
go func() {
r.startupCallback(r.runtime)
// If we got a url, publish it now startup completed
url, ok := hooksMessage.Data().(string)
if ok && len(url) > 0 {
r.bus.Publish("url:handler", url)
}
}()
})
} else {
r.logger.Warning("no startup callback registered!")

View File

@@ -0,0 +1,98 @@
package subsystem
import (
"context"
"strings"
"sync"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/servicebus"
)
// URL is the URL Handler subsystem. It handles messages with topics starting
// with "url:"
type URL struct {
urlChannel <-chan *servicebus.Message
// quit flag
shouldQuit bool
// Logger!
logger *logger.Logger
// Context for shutdown
ctx context.Context
cancel context.CancelFunc
// internal waitgroup
wg sync.WaitGroup
// Handlers
handlers map[string]func(string)
}
// NewURL creates a new log subsystem
func NewURL(bus *servicebus.ServiceBus, logger *logger.Logger, handlers map[string]func(string)) (*URL, error) {
// Subscribe to log messages
urlChannel, err := bus.Subscribe("url")
if err != nil {
return nil, err
}
ctx, cancel := context.WithCancel(context.Background())
result := &URL{
urlChannel: urlChannel,
logger: logger,
ctx: ctx,
cancel: cancel,
handlers: handlers,
}
return result, nil
}
// Start the subsystem
func (u *URL) Start() error {
u.wg.Add(1)
// Spin off a go routine
go func() {
defer u.logger.Trace("URL Shutdown")
for u.shouldQuit == false {
select {
case <-u.ctx.Done():
u.wg.Done()
return
case urlMessage := <-u.urlChannel:
// Guard against nil messages
if urlMessage == nil {
continue
}
messageType := strings.TrimPrefix(urlMessage.Topic(), "url:")
switch messageType {
case "handler":
url := urlMessage.Data().(string)
splitURL := strings.Split(url, ":")
protocol := splitURL[0]
callback, ok := u.handlers[protocol]
if ok {
go callback(url)
}
default:
u.logger.Error("unknown url message: %+v", urlMessage)
}
}
}
}()
return nil
}
func (u *URL) Close() {
u.cancel()
u.wg.Wait()
}

View File

@@ -17,13 +17,14 @@ func platformInfo() (*OS, error) {
// Ignore errors as it isn't a showstopper
key, _ := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
defer key.Close()
fmt.Printf("%+v\n", key)
// Ignore errors as it isn't a showstopper
productName, _, _ := key.GetStringValue("ProductName")
fmt.Println(productName)
currentBuild, _, _ := key.GetStringValue("CurrentBuildNumber")
displayVersion, _, _ := key.GetStringValue("DisplayVersion")
releaseId, _, _ := key.GetStringValue("ReleaseId")
return nil, nil
result.Name = productName
result.Version = fmt.Sprintf("%s (Build: %s)", releaseId, currentBuild)
result.ID = displayVersion
return &result, key.Close()
}

View File

@@ -12,10 +12,5 @@ func (i *Info) discover() error {
return err
}
i.OS = osinfo
// dll := syscall.MustLoadDLL("kernel32.dll")
// p := dll.MustFindProc("GetVersion")
// v, _, _ := p.Call()
// fmt.Printf("Windows version %d.%d (Build %d)\n", byte(v), uint8(v>>8), uint16(v>>16))
return nil
}

View File

@@ -12,20 +12,19 @@ type Basic struct {
}
// newBasic creates a new Basic application struct
func newBasic() *Basic {
func NewBasic() *Basic {
return &Basic{}
}
// WailsInit is called at application startup
func (b *Basic) WailsInit(runtime *wails.Runtime) error {
// startup is called at application startup
func (b *Basic) startup(runtime *wails.Runtime) {
// Perform your setup here
b.runtime = runtime
runtime.Window.SetTitle("{{.ProjectName}}")
return nil
}
// WailsShutdown is called at application termination
func (b *Basic) WailsShutdown() {
// shutdown is called at application termination
func (b *Basic) shutdown() {
// Perform your teardown here
}

View File

@@ -4,7 +4,7 @@
<link rel="stylesheet" href="/main.css">
</head>
<body>
<body data-wails-drag>
<div id="logo"></div>
<div id="input">
<input id="name" type="text"></input>

File diff suppressed because one or more lines are too long

View File

@@ -1,21 +1,38 @@
package main
import (
"github.com/wailsapp/wails/v2"
"log"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/logger"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/options/mac"
)
func main() {
// Create application with options
app, err := wails.CreateApp("{{.ProjectName}}", 1024, 768)
if err != nil {
log.Fatal(err)
}
app := NewBasic()
app.Bind(newBasic())
err = app.Run()
err := wails.Run(&options.App{
Title: "{{.ProjectName}}",
Width: 800,
Height: 600,
DisableResize: true,
Mac: &mac.Options{
WebviewIsTransparent: true,
WindowBackgroundIsTranslucent: true,
TitleBar: mac.TitleBarHiddenInset(),
Menu: menu.DefaultMacMenu(),
},
LogLevel: logger.DEBUG,
Startup: app.startup,
Shutdown: app.shutdown,
Bind: []interface{}{
app,
},
})
if err != nil {
log.Fatal(err)
}

View File

@@ -19,6 +19,10 @@ import (
"github.com/wailsapp/wails/v2/pkg/clilogger"
)
const (
VERBOSE int = 2
)
// BaseBuilder is the common builder struct
type BaseBuilder struct {
filesToDelete slicer.StringSlicer
@@ -142,9 +146,28 @@ func (b *BaseBuilder) CleanUp() {
})
}
func (b *BaseBuilder) OutputFilename(options *Options) string {
outputFile := options.OutputFile
if outputFile == "" {
outputFile = b.projectData.OutputFilename
}
return outputFile
}
// CompileProject compiles the project
func (b *BaseBuilder) CompileProject(options *Options) error {
// Run go mod tidy first
cmd := exec.Command(options.Compiler, "mod", "tidy")
if options.Verbosity == VERBOSE {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
err := cmd.Run()
if err != nil {
return err
}
// Default go build command
commands := slicer.String([]string{"build"})
@@ -160,6 +183,7 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
// potentially try and see if the assets have changed but will
// this take as much time as a `-a` build?
commands.Add("-a")
commands.Add("-x")
var tags slicer.StringSlicer
tags.Add(options.OutputType)
@@ -188,10 +212,10 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
// Get application build directory
appDir := options.BuildDirectory
err := cleanBuildDirectory(options)
if err != nil {
return err
}
//err = cleanBuildDirectory(options)
//if err != nil {
// return err
//}
if options.LDFlags != "" {
commands.Add("-ldflags")
@@ -199,10 +223,7 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
}
// Set up output filename
outputFile := options.OutputFile
if outputFile == "" {
outputFile = b.projectData.OutputFilename
}
outputFile := b.OutputFilename(options)
compiledBinary := filepath.Join(appDir, outputFile)
commands.Add("-o")
commands.Add(compiledBinary)
@@ -211,14 +232,14 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
options.CompiledBinary = compiledBinary
// Create the command
cmd := exec.Command(options.Compiler, commands.AsSlice()...)
cmd = exec.Command(options.Compiler, commands.AsSlice()...)
if options.Verbosity == VERBOSE {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
// Set the directory
cmd.Dir = b.projectData.Path
// Set GO111MODULE environment variable
cmd.Env = append(os.Environ(), "GO111MODULE=on")
// Add CGO flags
// We use the project/build dir as a temporary place for our generated c headers
buildBaseDir, err := fs.RelativeToCwd("build")
@@ -226,31 +247,47 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
return err
}
cmd.Env = append(os.Environ(), fmt.Sprintf("CGO_CFLAGS=-I%s", buildBaseDir))
cmd.Env = os.Environ() // inherit env
// Setup buffers
var stdo, stde bytes.Buffer
cmd.Stdout = &stdo
cmd.Stderr = &stde
// Use upsertEnv so we don't overwrite user's CGO_CFLAGS
cmd.Env = upsertEnv(cmd.Env, "CGO_CFLAGS", func(v string) string {
if v != "" {
v += " "
}
v += "-I" + buildBaseDir
return v
})
cmd.Env = upsertEnv(cmd.Env, "GOOS", func(v string) string {
return options.Platform
})
cmd.Env = upsertEnv(cmd.Env, "GOARCH", func(v string) string {
return options.Arch
})
cmd.Env = upsertEnv(cmd.Env, "CGO_ENABLED", func(v string) string {
return "1"
})
// Run command
err = cmd.Run()
// Format error if we have one
if err != nil {
return fmt.Errorf("%s\n%s", err, string(stde.Bytes()))
return err
}
return nil
}
// NpmInstall runs "npm install" in the given directory
func (b *BaseBuilder) NpmInstall(sourceDir string) error {
return b.NpmInstallUsingCommand(sourceDir, "npm install")
func (b *BaseBuilder) NpmInstall(sourceDir string, verbose bool) error {
return b.NpmInstallUsingCommand(sourceDir, "npm install", verbose)
}
// NpmInstallUsingCommand runs the given install command in the specified npm project directory
func (b *BaseBuilder) NpmInstallUsingCommand(sourceDir string, installCommand string) error {
func (b *BaseBuilder) NpmInstallUsingCommand(sourceDir string, installCommand string, verbose bool) error {
packageJSON := filepath.Join(sourceDir, "package.json")
@@ -292,7 +329,7 @@ func (b *BaseBuilder) NpmInstallUsingCommand(sourceDir string, installCommand st
// Split up the InstallCommand and execute it
cmd := strings.Split(installCommand, " ")
stdout, stderr, err := shell.RunCommand(sourceDir, cmd[0], cmd[1:]...)
if err != nil {
if verbose || err != nil {
for _, l := range strings.Split(stdout, "\n") {
fmt.Printf(" %s\n", l)
}
@@ -341,31 +378,40 @@ func (b *BaseBuilder) NpmRunWithEnvironment(projectDir, buildTarget string, verb
func (b *BaseBuilder) BuildFrontend(outputLogger *clilogger.CLILogger) error {
// TODO: Fix this up from the CLI
verbose := false
verbose := b.options.Verbosity == VERBOSE
frontendDir := filepath.Join(b.projectData.Path, "frontend")
// Check there is an 'InstallCommand' provided in wails.json
if b.projectData.InstallCommand == "" {
// No - don't install
outputLogger.Println(" - No Install command. Skipping.")
outputLogger.Println("No Install command. Skipping.")
} else {
// Do install if needed
outputLogger.Println(" - Installing dependencies...")
if err := b.NpmInstallUsingCommand(frontendDir, b.projectData.InstallCommand); err != nil {
outputLogger.Print("Installing frontend dependencies: ")
if verbose {
outputLogger.Println("")
outputLogger.Println("\tCommand: " + b.projectData.InstallCommand)
}
if err := b.NpmInstallUsingCommand(frontendDir, b.projectData.InstallCommand, verbose); err != nil {
return err
}
outputLogger.Println("Done.")
}
// Check if there is a build command
if b.projectData.BuildCommand == "" {
outputLogger.Println(" - No Build command. Skipping.")
outputLogger.Println("No Build command. Skipping.")
// No - ignore
return nil
}
outputLogger.Println(" - Compiling Frontend Project")
outputLogger.Print("Compiling frontend: ")
cmd := strings.Split(b.projectData.BuildCommand, " ")
if verbose {
outputLogger.Println("")
outputLogger.Println("\tCommand: '" + strings.Join(cmd, " ") + "'")
}
stdout, stderr, err := shell.RunCommand(frontendDir, cmd[0], cmd[1:]...)
if verbose || err != nil {
for _, l := range strings.Split(stdout, "\n") {
@@ -375,7 +421,12 @@ func (b *BaseBuilder) BuildFrontend(outputLogger *clilogger.CLILogger) error {
fmt.Printf(" %s\n", l)
}
}
return err
if err != nil {
return err
}
outputLogger.Println("Done.")
return nil
}
// ExtractAssets gets the assets from the index.html file
@@ -384,3 +435,22 @@ func (b *BaseBuilder) ExtractAssets() (*html.AssetBundle, error) {
// Read in html
return html.NewAssetBundle(b.projectData.HTML)
}
func upsertEnv(env []string, key string, update func(v string) string) []string {
newEnv := make([]string, len(env), len(env)+1)
found := false
for i := range env {
if strings.HasPrefix(env[i], key+"=") {
eqIndex := strings.Index(env[i], "=")
val := env[i][eqIndex+1:]
newEnv[i] = fmt.Sprintf("%s=%v", key, update(val))
found = true
continue
}
newEnv[i] = env[i]
}
if !found {
newEnv = append(newEnv, fmt.Sprintf("%s=%v", key, update("")))
}
return newEnv
}

View File

@@ -0,0 +1,31 @@
package build
import "testing"
func TestUpdateEnv(t *testing.T) {
env := []string{"one=1", "two=a=b", "three="}
newEnv := upsertEnv(env, "two", func(v string) string {
return v + "+added"
})
newEnv = upsertEnv(newEnv, "newVar", func(v string) string {
return "added"
})
newEnv = upsertEnv(newEnv, "three", func(v string) string {
return "3"
})
if len(newEnv) != 4 {
t.Errorf("expected: 4, got: %d", len(newEnv))
}
if newEnv[1] != "two=a=b+added" {
t.Errorf("expected: \"two=a=b+added\", got: %q", newEnv[1])
}
if newEnv[2] != "three=3" {
t.Errorf("expected: \"three=3\", got: %q", newEnv[2])
}
if newEnv[3] != "newVar=added" {
t.Errorf("expected: \"newVar=added\", got: %q", newEnv[3])
}
}

View File

@@ -6,7 +6,10 @@ import (
"path/filepath"
"runtime"
"github.com/leaanthony/slicer"
"github.com/wailsapp/wails/v2/internal/fs"
"github.com/wailsapp/wails/v2/internal/shell"
"github.com/wailsapp/wails/v2/internal/project"
"github.com/wailsapp/wails/v2/pkg/clilogger"
)
@@ -32,12 +35,15 @@ type Options struct {
ProjectData *project.Project // The project data
Pack bool // Create a package for the app after building
Platform string // The platform to build for
Arch string // The architecture to build for
Compiler string // The compiler command to use
IgnoreFrontend bool // Indicates if the frontend does not need building
OutputFile string // Override the output filename
BuildDirectory string // Directory to use for building the application
CompiledBinary string // Fully qualified path to the compiled binary
KeepAssets bool // /Keep the generated assets/files
Verbosity int // Verbosity level (0 - silent, 1 - default, 2 - verbose)
AppleIdentity string
}
// GetModeAsString returns the current mode as a string
@@ -57,12 +63,6 @@ func Build(options *Options) (string, error) {
return "", err
}
// Check platform
validPlatforms := slicer.String([]string{"linux", "darwin"})
if !validPlatforms.Contains(options.Platform) {
return "", fmt.Errorf("platform %s not supported", options.Platform)
}
// Load project
projectData, err := project.Load(cwd)
if err != nil {
@@ -70,7 +70,7 @@ func Build(options *Options) (string, error) {
}
options.ProjectData = projectData
// Calculate build dir
// Set build directory
options.BuildDirectory = filepath.Join(options.ProjectData.Path, "build", options.Platform, options.OutputType)
// Save the project type
@@ -106,7 +106,6 @@ func Build(options *Options) (string, error) {
// return "", err
// }
if !options.IgnoreFrontend {
outputLogger.Println(" - Building Project Frontend")
err = builder.BuildFrontend(outputLogger)
if err != nil {
return "", err
@@ -114,30 +113,62 @@ func Build(options *Options) (string, error) {
}
// Build the base assets
outputLogger.Println(" - Compiling Assets")
err = builder.BuildAssets(options)
if err != nil {
return "", err
}
// Compile the application
outputLogger.Print(" - Compiling Application in " + GetModeAsString(options.Mode) + " mode...")
err = builder.CompileProject(options)
if err != nil {
return "", err
outputLogger.Print("Compiling application: ")
if options.Platform == "darwin" && options.Arch == "universal" {
outputFile := builder.OutputFilename(options)
amd64Filename := outputFile + "-amd64"
arm64Filename := outputFile + "-arm64"
// Build amd64 first
options.Arch = "amd64"
options.OutputFile = amd64Filename
err = builder.CompileProject(options)
if err != nil {
return "", err
}
// Build arm64
options.Arch = "arm64"
options.OutputFile = arm64Filename
err = builder.CompileProject(options)
if err != nil {
return "", err
}
// Run lipo
_, stderr, err := shell.RunCommand(options.BuildDirectory, "lipo", "-create", "-output", outputFile, amd64Filename, arm64Filename)
if err != nil {
return "", fmt.Errorf("%s - %s", err.Error(), stderr)
}
// Remove temp binaries
fs.DeleteFile(filepath.Join(options.BuildDirectory, amd64Filename))
fs.DeleteFile(filepath.Join(options.BuildDirectory, arm64Filename))
projectData.OutputFilename = outputFile
options.CompiledBinary = filepath.Join(options.BuildDirectory, outputFile)
} else {
err = builder.CompileProject(options)
if err != nil {
return "", err
}
}
outputLogger.Println("done.")
outputLogger.Println("Done.")
// Do we need to pack the app?
if options.Pack {
outputLogger.Println(" - Packaging Application")
outputLogger.Print("Packaging application: ")
// TODO: Allow cross platform build
err = packageProject(options, runtime.GOOS)
if err != nil {
return "", err
}
outputLogger.Println("Done.")
}
return projectData.OutputFilename, nil

View File

@@ -12,5 +12,6 @@ type Builder interface {
BuildFrontend(*clilogger.CLILogger) error
BuildRuntime(*Options) error
CompileProject(*Options) error
OutputFilename(*Options) string
CleanUp()
}

View File

@@ -55,7 +55,7 @@ func (d *DesktopBuilder) BuildBaseAssets(assets *html.AssetBundle, options *Opti
var err error
outputLogger := options.Logger
outputLogger.Print(" - Embedding Assets...")
outputLogger.Print("Building assets: ")
// Get target asset directory
assetDir, err := fs.RelativeToCwd("build")
@@ -96,7 +96,7 @@ func (d *DesktopBuilder) BuildBaseAssets(assets *html.AssetBundle, options *Opti
return err
}
outputLogger.Println("done.")
outputLogger.Println("Done.")
return nil
}
@@ -125,11 +125,11 @@ func (d *DesktopBuilder) BuildRuntime(options *Options) error {
sourceDir := fs.RelativePath("../../../internal/runtime/js")
if err := d.NpmInstall(sourceDir); err != nil {
if err := d.NpmInstall(sourceDir, options.Verbosity == VERBOSE); err != nil {
return err
}
outputLogger.Print(" - Embedding Runtime...")
outputLogger.Print("Embedding Runtime: ")
envvars := []string{"WAILSPLATFORM=" + options.Platform}
if err := d.NpmRunWithEnvironment(sourceDir, "build:desktop", false, envvars); err != nil {
return err

View File

@@ -2,9 +2,11 @@ package build
import (
"bytes"
"fmt"
"image"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
@@ -52,6 +54,14 @@ func packageApplication(options *Options) error {
return err
}
// Sign app if needed
if options.AppleIdentity != "" {
err = signApplication(options)
if err != nil {
return err
}
}
return nil
}
@@ -176,3 +186,21 @@ func processApplicationIcon(resourceDir string, iconsDir string) (err error) {
}()
return icns.Encode(dest, srcImg)
}
func signApplication(options *Options) error {
bundlename := filepath.Join(options.BuildDirectory, options.ProjectData.Name+".app")
identity := fmt.Sprintf(`"%s"`, options.AppleIdentity)
cmd := exec.Command("codesign", "--sign", identity, "--deep", "--force", "--verbose", "--timestamp", "--options", "runtime", bundlename)
var stdo, stde bytes.Buffer
cmd.Stdout = &stdo
cmd.Stderr = &stde
// Run command
err := cmd.Run()
// Format error if we have one
if err != nil {
return fmt.Errorf("%s\n%s", err, string(stde.Bytes()))
}
return nil
}

View File

@@ -98,7 +98,7 @@ func (s *ServerBuilder) BuildBaseAssets(assets *html.AssetBundle) error {
func (s *ServerBuilder) BuildRuntime(options *Options) error {
sourceDir := fs.RelativePath("../../../internal/runtime/js")
if err := s.NpmInstall(sourceDir); err != nil {
if err := s.NpmInstall(sourceDir, options.Verbosity == VERBOSE); err != nil {
return err
}

View File

@@ -0,0 +1,66 @@
package logger
import (
"log"
"os"
)
// FileLogger is a utility to log messages to a number of destinations
type FileLogger struct {
filename string
}
// NewFileLogger creates a new Logger.
func NewFileLogger(filename string) Logger {
return &FileLogger{
filename: filename,
}
}
// Print works like Sprintf.
func (l *FileLogger) Print(message string) {
f, err := os.OpenFile(l.filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatal(err)
}
if _, err := f.WriteString(message); err != nil {
f.Close()
log.Fatal(err)
}
f.Close()
}
func (l *FileLogger) Println(message string) {
l.Print(message + "\n")
}
// Trace level logging. Works like Sprintf.
func (l *FileLogger) Trace(message string) {
l.Println("TRACE | " + message)
}
// Debug level logging. Works like Sprintf.
func (l *FileLogger) Debug(message string) {
l.Println("DEBUG | " + message)
}
// Info level logging. Works like Sprintf.
func (l *FileLogger) Info(message string) {
l.Println("INFO | " + message)
}
// Warning level logging. Works like Sprintf.
func (l *FileLogger) Warning(message string) {
l.Println("WARN | " + message)
}
// Error level logging. Works like Sprintf.
func (l *FileLogger) Error(message string) {
l.Println("ERROR | " + message)
}
// Fatal level logging. Works like Sprintf.
func (l *FileLogger) Fatal(message string) {
l.Println("FATAL | " + message)
os.Exit(1)
}

View File

@@ -10,15 +10,15 @@ type Modifier string
const (
// CmdOrCtrlKey represents Command on Mac and Control on other platforms
CmdOrCtrlKey Modifier = "CmdOrCtrl"
CmdOrCtrlKey Modifier = "cmdorctrl"
// OptionOrAltKey represents Option on Mac and Alt on other platforms
OptionOrAltKey Modifier = "OptionOrAlt"
OptionOrAltKey Modifier = "optionoralt"
// ShiftKey represents the shift key on all systems
ShiftKey Modifier = "Shift"
ShiftKey Modifier = "shift"
// SuperKey represents Command on Mac and the Windows key on the other platforms
SuperKey Modifier = "Super"
SuperKey Modifier = "super"
// ControlKey represents the control key on all systems
ControlKey Modifier = "Control"
ControlKey Modifier = "ctrl"
)
var modifierMap = map[string]Modifier{
@@ -30,7 +30,8 @@ var modifierMap = map[string]Modifier{
}
func parseModifier(text string) (*Modifier, error) {
result, valid := modifierMap[text]
lowertext := strings.ToLower(text)
result, valid := modifierMap[lowertext]
if !valid {
return nil, fmt.Errorf("'%s' is not a valid modifier", text)
}

View File

@@ -29,7 +29,7 @@ type MenuItem struct {
// Callback function when menu clicked
Click Callback `json:"-"`
// Colour
// Text Colour
RGBA string
// Font
@@ -39,9 +39,12 @@ type MenuItem struct {
// Image - base64 image data
Image string
// MacTemplateImage indicates that on a mac, this image is a template image
// MacTemplateImage indicates that on a Mac, this image is a template image
MacTemplateImage bool
// MacAlternate indicates that this item is an alternative to the previous menu item
MacAlternate bool
// Tooltip
Tooltip string

View File

@@ -6,12 +6,38 @@ type TrayMenu struct {
// Label is the text we wish to display in the tray
Label string
// Icon is the name of the tray icon we wish to display.
// Image is the name of the tray icon we wish to display.
// These are read up during build from <projectdir>/trayicons and
// the filenames are used as IDs, minus the extension
// EG: <projectdir>/trayicons/main.png can be referenced here with "main"
Icon string
// If the image is not a filename, it will be treated as base64 image data
Image string
// MacTemplateImage indicates that on a Mac, this image is a template image
MacTemplateImage bool
// Text Colour
RGBA string
// Font
FontSize int
FontName string
// Tooltip
Tooltip string
// Callback function when menu clicked
//Click Callback `json:"-"`
// Disabled makes the item unselectable
Disabled bool
// Menu is the initial menu we wish to use for the tray
Menu *Menu
// OnOpen is called when the Menu is opened
OnOpen func()
// OnClose is called when the Menu is closed
OnClose func()
}

View File

@@ -20,4 +20,5 @@ type Options struct {
TrayMenus []*menu.TrayMenu
ContextMenus []*menu.ContextMenu
ActivationPolicy ActivationPolicy
URLHandlers map[string]func(string)
}