Compare commits

..

82 Commits

Author SHA1 Message Date
Lea Anthony
2e01710412 v2.0.0-alpha.63 2021-04-04 05:15:09 +10:00
Lea Anthony
1b0193161c Improvements for handling Info.plist 2021-04-04 05:14:16 +10:00
Lea Anthony
1b377fb575 Support enter as alias for return 2021-04-03 16:52:54 +11:00
Lea Anthony
1203ae64b8 v2.0.0-alpha.62 2021-03-31 15:44:28 +11:00
Lea Anthony
26ed8002b9 Menu bar font fix 2021-03-31 15:43:28 +11:00
Lea Anthony
cf23bffc67 v2.0.0-alpha.61 2021-03-30 22:22:48 +11:00
Lea Anthony
d70f6fffe7 Remove autorelease of colour. Fixes high sierra. 2021-03-30 22:21:49 +11:00
Lea Anthony
54c99fc386 Move some methods to main thread 2021-03-29 20:45:50 +11:00
Lea Anthony
86c1ea5e6a Initial support for compression 2021-03-27 20:59:14 +11:00
Lea Anthony
b394c1914c v2.0.0-alpha.60 2021-03-26 20:54:42 +11:00
Lea Anthony
91c2ddf155 Allow styling of menu items that have submenus 2021-03-26 20:54:19 +11:00
Lea Anthony
712ad96d2a v2.0.0-alpha.59 2021-03-26 18:30:00 +11:00
Lea Anthony
86b4a4f2f5 Don't clean directory when doing universal builds 2021-03-26 18:29:28 +11:00
Lea Anthony
4b9786abc9 v2.0.0-alpha.58 2021-03-26 18:13:10 +11:00
Lea Anthony
fd96ebc050 Better verbose output 2021-03-26 18:12:42 +11:00
Lea Anthony
939e0f5975 Use default broken image for invalid images 2021-03-26 17:51:49 +11:00
Lea Anthony
6a7a288a0f Limit StartsAtLogin to app bundles 2021-03-26 16:52:59 +11:00
Lea Anthony
0564d0aa98 v2.0.0-alpha.57 2021-03-26 16:32:10 +11:00
Lea Anthony
3a136a73ca Add package for mac functions 2021-03-26 15:57:30 +11:00
Lea Anthony
50c219307f Add clean flag 2021-03-26 14:10:25 +11:00
Lea Anthony
de3038b302 v2.0.0-alpha.56 2021-03-25 21:13:40 +11:00
Lea Anthony
6eb4b0a419 Fix packaging universal builds 2021-03-25 21:12:29 +11:00
Lea Anthony
41d2158375 Support building arm64 & universal binaries. 2021-03-25 21:12:29 +11:00
Lea Anthony
5d7f57e80b Initial ARM support! 2021-03-25 21:12:29 +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
65 changed files with 1692 additions and 785 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) * [Balakrishna Prasad Ganne](https://github.com/aayush420)
* [Charaf Rezrazi](https://github.com/Rezrazi) * [Charaf Rezrazi](https://github.com/Rezrazi)
* [misitebao](https://github.com/misitebao) * [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 #!/usr/bin/env bash
echo "**** Checking if Wails passes unit tests ****" echo "**** Checking if Wails passes unit tests ****"
if ! go test ./... if ! go test ./lib/... ./runtime/... ./cmd/...
then then
echo "" echo ""
echo "ERROR: Unit tests failed!" echo "ERROR: Unit tests failed!"

View File

@@ -3,8 +3,10 @@ package build
import ( import (
"fmt" "fmt"
"io" "io"
"os"
"runtime" "runtime"
"strings" "strings"
"text/tabwriter"
"time" "time"
"github.com/leaanthony/clir" "github.com/leaanthony/clir"
@@ -23,8 +25,8 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
command := app.NewSubCommand("build", "Builds the application") command := app.NewSubCommand("build", "Builds the application")
// Setup target type flag // Setup target type flag
description := "Type of application to build. Valid types: " + validTargetTypes.Join(",") //description := "Type of application to build. Valid types: " + validTargetTypes.Join(",")
command.StringFlag("t", description, &outputType) //command.StringFlag("t", description, &outputType)
// Setup production flag // Setup production flag
production := false production := false
@@ -37,28 +39,46 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
compilerCommand := "go" compilerCommand := "go"
command.StringFlag("compiler", "Use a different go compiler to build, eg go1.15beta1", &compilerCommand) command.StringFlag("compiler", "Use a different go compiler to build, eg go1.15beta1", &compilerCommand)
compress := false
command.BoolFlag("compress", "Compress final binary", &compress)
// Setup Platform flag // Setup Platform flag
platform := runtime.GOOS platform := runtime.GOOS
command.StringFlag("platform", "Platform to target", &platform) command.StringFlag("platform", "Platform to target", &platform)
// Quiet Build // Verbosity
quiet := false verbosity := 1
command.BoolFlag("q", "Suppress output to console", &quiet) command.IntFlag("v", "Verbosity level (0 - silent, 1 - default, 2 - verbose)", &verbosity)
// ldflags to pass to `go` // ldflags to pass to `go`
ldflags := "" ldflags := ""
command.StringFlag("ldflags", "optional ldflags", &ldflags) command.StringFlag("ldflags", "optional ldflags", &ldflags)
// Log to file // Log to file
logFile := "" //logFile := ""
command.StringFlag("l", "Log to file", &logFile) //command.StringFlag("l", "Log to file", &logFile)
// Retain assets // Retain assets
keepAssets := false keepAssets := false
command.BoolFlag("k", "Keep generated assets", &keepAssets) command.BoolFlag("k", "Keep generated assets", &keepAssets)
// Retain assets
outputFilename := ""
command.StringFlag("o", "Output filename", &outputFilename)
// Clean build directory
cleanBuildDirectory := false
command.BoolFlag("clean", "Clean the build directory before building", &cleanBuildDirectory)
appleIdentity := ""
if runtime.GOOS == "darwin" {
command.StringFlag("sign", "Signs your app with the given identity.", &appleIdentity)
}
command.Action(func() error { command.Action(func() error {
quiet := verbosity == 0
// Create logger // Create logger
logger := clilogger.New(w) logger := clilogger.New(w)
logger.Mute(quiet) logger.Mute(quiet)
@@ -72,9 +92,10 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
app.PrintBanner() app.PrintBanner()
} }
task := fmt.Sprintf("Building %s Application", strings.Title(outputType)) // Ensure package is used with apple identity
logger.Println(task) if appleIdentity != "" && pack == false {
logger.Println(strings.Repeat("-", len(task))) return fmt.Errorf("must use `-package` flag when using `-sign`")
}
// Setup mode // Setup mode
mode := build.Debug mode := build.Debug
@@ -82,18 +103,75 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
mode = build.Production 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)
}
if compress && platform == "darwin/universal" {
println("Warning: compress flag unsupported for universal binaries. Ignoring.")
compress = false
}
// Create BuildOptions // Create BuildOptions
buildOptions := &build.Options{ buildOptions := &build.Options{
Logger: logger, Logger: logger,
OutputType: outputType, OutputType: outputType,
Mode: mode, OutputFile: outputFilename,
Pack: pack, CleanBuildDirectory: cleanBuildDirectory,
Platform: platform, Mode: mode,
LDFlags: ldflags, Pack: pack,
Compiler: compilerCommand, LDFlags: ldflags,
KeepAssets: keepAssets, Compiler: compilerCommand,
KeepAssets: keepAssets,
AppleIdentity: appleIdentity,
Verbosity: verbosity,
Compress: compress,
} }
// 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, "\n")
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, "Compress: \t%t\n", buildOptions.Compress)
fmt.Fprintf(w, "Build Mode: \t%s\n", buildModeText)
fmt.Fprintf(w, "Package: \t%t\n", buildOptions.Pack)
fmt.Fprintf(w, "Clean Build Dir: \t%t\n", buildOptions.CleanBuildDirectory)
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) return doBuild(buildOptions)
}) })
} }

View File

@@ -23,14 +23,14 @@ func fatal(message string) {
} }
func banner(_ *clir.Cli) 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() { func main() {
var err error var err error
app := clir.NewCli("Wails", "Go/HTML Application Framework", version) app := clir.NewCli("Wails", "Go/HTML Appkit", version)
app.SetBannerFunction(banner) app.SetBannerFunction(banner)

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,10 +5,6 @@
#ifndef COMMON_H #ifndef COMMON_H
#define COMMON_H #define COMMON_H
#define OBJC_OLD_DISPATCH_PROTOTYPES 1
#include <objc/objc-runtime.h>
#include <CoreGraphics/CoreGraphics.h>
#include <stdio.h> #include <stdio.h>
#include <stdarg.h> #include <stdarg.h>
#include "string.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); FREE_AND_SET(contextMenu->contextMenuData, contextMenuData);
// Grab the content view and show the menu // 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 // Get the triggering event
id menuEvent = msg(mainWindow, s("currentEvent")); id menuEvent = msg_reg(mainWindow, s("currentEvent"));
if( contextMenu->nsmenu == NULL ) { if( contextMenu->nsmenu == NULL ) {
// GetMenu creates the NSMenu // GetMenu creates the NSMenu
@@ -93,7 +93,7 @@ void ShowContextMenu(ContextMenuStore* store, id mainWindow, const char *context
} }
// Show popup // 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 SetApplicationMenu(struct Application*, const char *);
extern void AddTrayMenu(struct Application*, const char *menuTrayJSON); extern void AddTrayMenu(struct Application*, const char *menuTrayJSON);
extern void SetTrayMenu(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 UpdateTrayMenuLabel(struct Application*, const char* JSON);
extern void AddContextMenu(struct Application*, char *contextMenuJSON); extern void AddContextMenu(struct Application*, char *contextMenuJSON);
extern void UpdateContextMenu(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) { func (c *Client) UpdateContextMenu(contextMenuJSON string) {
C.UpdateContextMenu(c.app.app, c.app.string2CString(contextMenuJSON)) 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 return nil
} }

View File

@@ -13,22 +13,41 @@
// Macros to make it slightly more sane // Macros to make it slightly more sane
#define msg objc_msgSend #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 c(str) (id)objc_getClass(str)
#define s(str) sel_registerName(str) #define s(str) sel_registerName(str)
#define u(str) sel_getUid(str) #define u(str) sel_getUid(str)
#define str(input) msg(c("NSString"), s("stringWithUTF8String:"), input) #define str(input) ((id(*)(id, SEL, const char *))objc_msgSend)(c("NSString"), s("stringWithUTF8String:"), input)
#define strunicode(input) msg(c("NSString"), s("stringWithFormat:"), str("%C"), (unsigned short)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(input, s("UTF8String")) #define cstr(input) (const char *)msg_reg(input, s("UTF8String"))
#define url(input) msg(c("NSURL"), s("fileURLWithPath:"), str(input)) #define url(input) msg_id(c("NSURL"), s("fileURLWithPath:"), str(input))
#define ALLOC(classname) msg(c(classname), s("alloc")) #define ALLOC(classname) msg_reg(c(classname), s("alloc"))
#define ALLOC_INIT(classname) msg(msg(c(classname), s("alloc")), s("init")) #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_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_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 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 #define NSBackingStoreBuffered 2
@@ -98,6 +117,8 @@
#define NSAlertSecondButtonReturn 1001 #define NSAlertSecondButtonReturn 1001
#define NSAlertThirdButtonReturn 1002 #define NSAlertThirdButtonReturn 1002
#define BrokenImage "iVBORw0KGgoAAAANSUhEUgAAABAAAAASCAMAAABl5a5YAAABj1BMVEWopan///+koqSWk5P9/v3///////////+AgACMiovz8/PB0fG9z+3i4+WysbGBfX1Erh80rACLiYqBxolEsDhHlDEbqQDDx+CNho7W1tj4+/bw+O3P5Mn4/f/W1tbK6sX////b2dn////////////8/Pz6+vro6Ojj4+P////G1PL////EzNydmp2cmZnd3eDF1PHs8v/o8P/Q3vrS3vfE0vCdmpqZkpr19/3N2vXI1vPH1fOgnqDg6frP3PbCytvHx8irqq6HhIZtuGtjnlZetU1Xs0NWskBNsi7w9v/d6P7w9P3S4Pzr8Pvl7PrY5PrU4PjQ3fjD1Ozo6Om30NjGzNi7ubm34K+UxKmbnaWXlJeUjpSPi4tppF1TtjxSsTf2+f7L2PTr7e3H2+3V7+q+0uXg4OPg4eLR1uG7z+Hg4ODGzODV2N7V1trP5dmxzs65vcfFxMWq0cKxxr+/vr+0s7apxbWaxrCv2qao05+dlp2Uuo2Dn4F8vIB6xnyAoHmAym9zqGpctENLryNFsgoblJpnAAAAKnRSTlP+hP7+5ZRmYgL+/f39/f39/f38/Pz8/Pv69+7j083My8GocnBPTTMWEgjxeITOAAABEklEQVQY0y3KZXuCYBiG4ceYuu7u3nyVAaKOMBBQ7O5Yd3f3fvheDnd9u8/jBkGwNxP6sjOWVQvY/ftrzfT6bd3yEhCnYZqiaYoKiwX/gXkFiHySTcUTLJMsZ9v8nQvgssWYOEKedKpcOO6CUXD5IlGEY5hLUbyDAAZ6HRf1bnkoavOsFQibg+Q4nuNYL+ON5PHD5nBaraRVyxnzGf6BJzUi2QQCQgMyk8tleL7dg1owpJ17D5IkvV100EingeOopPyo6vfAuXF+9hbDTknZCIaUoeK4efKwG4iT6xDewd7imGlid7gGwv37b6Oh9jwaTdOf/Tc1qH7UZVmuP6G5qZfBr9cAGNy4KiDd4tXIs7tS+QO9aUKvPAIKuQAAAABJRU5ErkJggg=="
struct Application; struct Application;
int releaseNSObject(void *const context, struct hashmap_element_s *const e); int releaseNSObject(void *const context, struct hashmap_element_s *const e);
void TitlebarAppearsTransparent(struct Application* app); void TitlebarAppearsTransparent(struct Application* app);
@@ -118,4 +139,8 @@ void SetActivationPolicy(struct Application* app, int policy);
void* lookupStringConstant(id constantName); void* lookupStringConstant(id constantName);
void HasURLHandlers(struct Application* app);
id createImageFromBase64Data(const char *data, bool isTemplateImage);
#endif #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 // Free nsmenu if we have it
if ( menu->menu != NULL ) { if ( menu->menu != NULL ) {
msg(menu->menu, s("release")); msg_reg(menu->menu, s("release"));
} }
free(menu); free(menu);
@@ -120,17 +120,17 @@ const char* createMenuClickedMessage(const char *menuItemID, const char *data, e
// Callback for text menu items // Callback for text menu items
void menuItemCallback(id self, SEL cmd, id sender) { 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; const char *message;
// Update checkbox / radio item // Update checkbox / radio item
if( callbackData->menuItemType == Checkbox) { if( callbackData->menuItemType == Checkbox) {
// Toggle state // Toggle state
bool state = msg(callbackData->menuItem, s("state")); bool state = msg_reg(callbackData->menuItem, s("state"));
msg(callbackData->menuItem, s("setState:"), (state? NSControlStateValueOff : NSControlStateValueOn)); msg_int(callbackData->menuItem, s("setState:"), (state? NSControlStateValueOff : NSControlStateValueOn));
} else if( callbackData->menuItemType == Radio ) { } else if( callbackData->menuItemType == Radio ) {
// Check the menu items' current state // 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 it's already selected, exit early
if (selected) return; if (selected) return;
@@ -142,13 +142,13 @@ void menuItemCallback(id self, SEL cmd, id sender) {
id thisMember = members[0]; id thisMember = members[0];
int count = 0; int count = 0;
while(thisMember != NULL) { while(thisMember != NULL) {
msg(thisMember, s("setState:"), NSControlStateValueOff); msg_int(thisMember, s("setState:"), NSControlStateValueOff);
count = count + 1; count = count + 1;
thisMember = members[count]; thisMember = members[count];
} }
// check the selected menu item // check the selected menu item
msg(callbackData->menuItem, s("setState:"), NSControlStateValueOn); msg_int(callbackData->menuItem, s("setState:"), NSControlStateValueOn);
} }
const char *menuID = callbackData->menuID; const char *menuID = callbackData->menuID;
@@ -189,6 +189,9 @@ id processAcceleratorKey(const char *key) {
if( STREQ(key, "return") ) { if( STREQ(key, "return") ) {
return strunicode(0x000d); return strunicode(0x000d);
} }
if( STREQ(key, "enter") ) {
return strunicode(0x000d);
}
if( STREQ(key, "escape") ) { if( STREQ(key, "escape") ) {
return strunicode(0x001b); return strunicode(0x001b);
} }
@@ -345,61 +348,61 @@ id processAcceleratorKey(const char *key) {
void addSeparator(id menu) { void addSeparator(id menu) {
id item = msg(c("NSMenuItem"), s("separatorItem")); id item = msg_reg(c("NSMenuItem"), s("separatorItem"));
msg(menu, s("addItem:"), item); msg_id(menu, s("addItem:"), item);
} }
id createMenuItemNoAutorelease( id title, const char *action, const char *key) { id createMenuItemNoAutorelease( id title, const char *action, const char *key) {
id item = ALLOC("NSMenuItem"); 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; return item;
} }
id createMenuItem(id title, const char *action, const char *key) { id createMenuItem(id title, const char *action, const char *key) {
id item = ALLOC("NSMenuItem"); 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));
msg(item, s("autorelease")); msg_reg(item, s("autorelease"));
return item; return item;
} }
id addMenuItem(id menu, const char *title, const char *action, const char *key, bool disabled) { id addMenuItem(id menu, const char *title, const char *action, const char *key, bool disabled) {
id item = createMenuItem(str(title), action, key); id item = createMenuItem(str(title), action, key);
msg(item, s("setEnabled:"), !disabled); msg_bool(item, s("setEnabled:"), !disabled);
msg(menu, s("addItem:"), item); msg_id(menu, s("addItem:"), item);
return item; return item;
} }
id createMenu(id title) { id createMenu(id title) {
id menu = ALLOC("NSMenu"); id menu = ALLOC("NSMenu");
msg(menu, s("initWithTitle:"), title); msg_id(menu, s("initWithTitle:"), title);
msg(menu, s("setAutoenablesItems:"), NO); msg_bool(menu, s("setAutoenablesItems:"), NO);
// msg(menu, s("autorelease")); // msg(menu, s("autorelease"));
return menu; return menu;
} }
void createDefaultAppMenu(id parentMenu) { void createDefaultAppMenu(id parentMenu) {
// App Menu // 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 appMenuItem = createMenuItemNoAutorelease(appName, NULL, "");
id appMenu = createMenu(appName); id appMenu = createMenu(appName);
msg(appMenuItem, s("setSubmenu:"), appMenu); msg_id(appMenuItem, s("setSubmenu:"), appMenu);
msg(parentMenu, s("addItem:"), appMenuItem); 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"); 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); 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); addMenuItem(appMenu, "Show All", "unhideAllApplications:", "", FALSE);
addSeparator(appMenu); addSeparator(appMenu);
title = msg(str("Quit "), s("stringByAppendingString:"), appName); title = msg_id(str("Quit "), s("stringByAppendingString:"), appName);
item = createMenuItem(title, "terminate:", "q"); item = createMenuItem(title, "terminate:", "q");
msg(appMenu, s("addItem:"), item); msg_id(appMenu, s("addItem:"), item);
} }
void createDefaultEditMenu(id parentMenu) { void createDefaultEditMenu(id parentMenu) {
@@ -407,8 +410,8 @@ void createDefaultEditMenu(id parentMenu) {
id editMenuItem = createMenuItemNoAutorelease(str("Edit"), NULL, ""); id editMenuItem = createMenuItemNoAutorelease(str("Edit"), NULL, "");
id editMenu = createMenu(str("Edit")); id editMenu = createMenu(str("Edit"));
msg(editMenuItem, s("setSubmenu:"), editMenu); msg_id(editMenuItem, s("setSubmenu:"), editMenu);
msg(parentMenu, s("addItem:"), editMenuItem); msg_id(parentMenu, s("addItem:"), editMenuItem);
addMenuItem(editMenu, "Undo", "undo:", "z", FALSE); addMenuItem(editMenu, "Undo", "undo:", "z", FALSE);
addMenuItem(editMenu, "Redo", "redo:", "y", FALSE); addMenuItem(editMenu, "Redo", "redo:", "y", FALSE);
@@ -436,7 +439,7 @@ void processMenuRole(Menu *menu, id parentMenu, JsonNode *item) {
} }
if ( STREQ(roleName, "hideothers")) { if ( STREQ(roleName, "hideothers")) {
id hideOthers = addMenuItem(parentMenu, "Hide Others", "hideOtherApplications:", "h", FALSE); id hideOthers = addMenuItem(parentMenu, "Hide Others", "hideOtherApplications:", "h", FALSE);
msg(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand)); msg_int(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand));
return; return;
} }
if ( STREQ(roleName, "unhide")) { if ( STREQ(roleName, "unhide")) {
@@ -473,7 +476,7 @@ void processMenuRole(Menu *menu, id parentMenu, JsonNode *item) {
} }
if( STREQ(roleName, "pasteandmatchstyle")) { if( STREQ(roleName, "pasteandmatchstyle")) {
id pasteandmatchstyle = addMenuItem(parentMenu, "Paste and Match Style", "pasteandmatchstyle:", "v", FALSE); 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")) { if ( STREQ(roleName, "selectall")) {
addMenuItem(parentMenu, "Select All", "selectAll:", "a", FALSE); addMenuItem(parentMenu, "Select All", "selectAll:", "a", FALSE);
@@ -508,6 +511,7 @@ unsigned long parseModifiers(const char **modifiers) {
const char *thisModifier = modifiers[0]; const char *thisModifier = modifiers[0];
int count = 0; int count = 0;
while( thisModifier != NULL ) { while( thisModifier != NULL ) {
// Determine flags // Determine flags
if( STREQ(thisModifier, "cmdorctrl") ) { if( STREQ(thisModifier, "cmdorctrl") ) {
result |= NSEventModifierFlagCommand; result |= NSEventModifierFlagCommand;
@@ -521,7 +525,7 @@ unsigned long parseModifiers(const char **modifiers) {
if( STREQ(thisModifier, "super") ) { if( STREQ(thisModifier, "super") ) {
result |= NSEventModifierFlagCommand; result |= NSEventModifierFlagCommand;
} }
if( STREQ(thisModifier, "control") ) { if( STREQ(thisModifier, "ctrl") ) {
result |= NSEventModifierFlagControl; result |= NSEventModifierFlagControl;
} }
count++; count++;
@@ -539,18 +543,18 @@ id processRadioMenuItem(Menu *menu, id parentmenu, const char *title, const char
// Create a MenuItemCallbackData // Create a MenuItemCallbackData
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, item, menuid, Radio); MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, item, menuid, Radio);
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), callback); id wrappedId = msg_id(c("NSValue"), s("valueWithPointer:"), (id)callback);
msg(item, s("setRepresentedObject:"), wrappedId); msg_id(item, s("setRepresentedObject:"), wrappedId);
id key = processAcceleratorKey(acceleratorkey); 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_bool(item, s("setEnabled:"), !disabled);
msg(item, s("autorelease")); msg_reg(item, s("autorelease"));
msg(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff)); msg_int(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
msg(parentmenu, s("addItem:"), item); msg_id(parentmenu, s("addItem:"), item);
return item; return item;
} }
@@ -565,73 +569,37 @@ id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const c
// Create a MenuItemCallbackData // Create a MenuItemCallbackData
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, item, menuid, Checkbox); MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, item, menuid, Checkbox);
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), callback); id wrappedId = msg_id(c("NSValue"), s("valueWithPointer:"), (id)callback);
msg(item, s("setRepresentedObject:"), wrappedId); msg_id(item, s("setRepresentedObject:"), wrappedId);
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuItemCallback:"), str(key)); ((id(*)(id, SEL, id, SEL, id))objc_msgSend)(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuItemCallback:"), str(key));
msg(item, s("setEnabled:"), !disabled); msg_bool(item, s("setEnabled:"), !disabled);
msg(item, s("autorelease")); msg_reg(item, s("autorelease"));
msg(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff)); msg_int(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
msg(parentmenu, s("addItem:"), item); msg_id(parentmenu, s("addItem:"), item);
return 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 createAttributedString(const char* title, const char* fontName, int fontSize, const char* RGBA) {
id item = ALLOC("NSMenuItem");
// Create a MenuItemCallbackData // Create new Dictionary
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);
}
// Process Menu Item attributes
id dictionary = ALLOC_INIT("NSMutableDictionary"); id dictionary = ALLOC_INIT("NSMutableDictionary");
// Process font
id font;
CGFloat fontSizeFloat = (CGFloat)fontSize; CGFloat fontSizeFloat = (CGFloat)fontSize;
// Check if valid // Use default font
id fontNameAsNSString = str(fontName); id font = ((id(*)(id, SEL, CGFloat))objc_msgSend)(c("NSFont"), s("menuBarFontOfSize:"), fontSizeFloat);
id fontsOnSystem = msg(msg(c("NSFontManager"), s("sharedFontManager")), s("availableFonts"));
bool valid = msg(fontsOnSystem, s("containsObject:"), fontNameAsNSString); // Check user supplied font
if( valid ) { if( STR_HAS_CHARS(fontName) ) {
font = msg(c("NSFont"), s("fontWithName:size:"), fontNameAsNSString, fontSizeFloat); id fontNameAsNSString = str(fontName);
} else { id userFont = ((id(*)(id, SEL, id, CGFloat))objc_msgSend)(c("NSFont"), s("fontWithName:size:"), fontNameAsNSString, fontSizeFloat);
bool supportsMonospacedDigitSystemFont = (bool) msg(c("NSFont"), s("respondsToSelector:"), s("monospacedDigitSystemFontOfSize:weight:")); if( userFont != NULL ) {
if( supportsMonospacedDigitSystemFont ) { font = userFont;
font = msg(c("NSFont"), s("monospacedDigitSystemFontOfSize:weight:"), fontSizeFloat, NSFontWeightRegular);
} else {
font = msg(c("NSFont"), s("menuFontOfSize:"), fontSizeFloat);
} }
} }
// Add font to dictionary // Add font to dictionary
msg(dictionary, s("setObject:forKey:"), font, lookupStringConstant(str("NSFontAttributeName"))); id fan = lookupStringConstant(str("NSFontAttributeName"));
msg_id_id(dictionary, s("setObject:forKey:"), font, fan);
// Add offset to dictionary
id offset = msg(c("NSNumber"), s("numberWithFloat:"), 0.0);
msg(dictionary, s("setObject:forKey:"), offset, lookupStringConstant(str("NSBaselineOffsetAttributeName")));
// RGBA // RGBA
if( RGBA != NULL && strlen(RGBA) > 0) { if( RGBA != NULL && strlen(RGBA) > 0) {
@@ -641,32 +609,70 @@ id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char
r = g = b = a = 255; r = g = b = a = 255;
int count = sscanf(RGBA, "#%02hx%02hx%02hx%02hx", &r, &g, &b, &a); int count = sscanf(RGBA, "#%02hx%02hx%02hx%02hx", &r, &g, &b, &a);
if (count > 0) { if (count > 0) {
id colour = msg(c("NSColor"), s("colorWithCalibratedRed:green:blue:alpha:"), id colour = ((id(*)(id, SEL, CGFloat, CGFloat, CGFloat, CGFloat))objc_msgSend)(c("NSColor"), s("colorWithCalibratedRed:green:blue:alpha:"),
(float)r / 255.0, (CGFloat)r / (CGFloat)255.0,
(float)g / 255.0, (CGFloat)g / (CGFloat)255.0,
(float)b / 255.0, (CGFloat)b / (CGFloat)255.0,
(float)a / 255.0); (CGFloat)a / (CGFloat)255.0);
msg(dictionary, s("setObject:forKey:"), colour, lookupStringConstant(str("NSForegroundColorAttributeName"))); id NSForegroundColorAttributeName = lookupStringConstant(str("NSForegroundColorAttributeName"));
msg(colour, s("release")); msg_id_id(dictionary, s("setObject:forKey:"), colour, NSForegroundColorAttributeName);
} }
} }
id attributedString = ALLOC("NSMutableAttributedString"); id attributedString = ALLOC("NSMutableAttributedString");
msg(attributedString, s("initWithString:attributes:"), str(title), dictionary); msg_id_id(attributedString, s("initWithString:attributes:"), str(title), dictionary);
msg(dictionary, s("release")); msg_reg(attributedString, s("autorelease"));
msg_reg(dictionary, s("autorelease"));
return attributedString;
}
msg(item, s("setAttributedTitle:"), attributedString); 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) {
msg(attributedString, s("autorelease")); id item = ALLOC("NSMenuItem");
msg(item, s("setEnabled:"), !disabled); // Create a MenuItemCallbackData
msg(item, s("autorelease")); 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 nsimage = createImageFromBase64Data(image, templateImage);
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 // Process modifiers
if( modifiers != NULL ) { if( modifiers != NULL && !alternate) {
unsigned long modifierFlags = parseModifiers(modifiers); 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; return item;
} }
@@ -687,38 +693,6 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
return; 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 // This is a user menu. Get the common data
// Get the label // Get the label
const char *label = getJSONString(item, "Label"); const char *label = getJSONString(item, "Label");
@@ -726,6 +700,11 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
label = "(empty)"; label = "(empty)";
} }
// Is this an alternate menu item?
bool alternate = false;
getJSONBool(item, "MacAlternate", &alternate);
const char *menuid = getJSONString(item, "ID"); const char *menuid = getJSONString(item, "ID");
if ( menuid == NULL) { if ( menuid == NULL) {
menuid = ""; menuid = "";
@@ -746,7 +725,7 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
bool templateImage = false; bool templateImage = false;
getJSONBool(item, "MacTemplateImage", &templateImage); getJSONBool(item, "MacTemplateImage", &templateImage);
int fontSize = 12; int fontSize = 0;
getJSONInt(item, "FontSize", &fontSize); getJSONInt(item, "FontSize", &fontSize);
// If we have an accelerator // If we have an accelerator
@@ -779,9 +758,36 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
// Get the Type // Get the Type
JsonNode *type = json_find_member(item, "Type"); JsonNode *type = json_find_member(item, "Type");
if( type != NULL ) { if( type != NULL ) {
if( STREQ(type->string_, "Text") || STREQ(type->string_, "Submenu")) {
id thisMenuItem = processTextMenuItem(menu, parentMenu, label, menuid, disabled, acceleratorkey, modifiers, tooltip, image, fontName, fontSize, RGBA, templateImage, alternate);
if( STREQ(type->string_, "Text")) { // Check if this node has a submenu
processTextMenuItem(menu, parentMenu, label, menuid, disabled, acceleratorkey, modifiers, tooltip, image, fontName, fontSize, RGBA, templateImage); 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 thisMenu = createMenu(str(name));
msg_id(thisMenuItem, s("setSubmenu:"), thisMenu);
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);
}
}
} }
else if ( STREQ(type->string_, "Separator")) { else if ( STREQ(type->string_, "Separator")) {
addSeparator(parentMenu); addSeparator(parentMenu);
@@ -800,7 +806,6 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
processRadioMenuItem(menu, parentMenu, label, menuid, disabled, checked, ""); processRadioMenuItem(menu, parentMenu, label, menuid, disabled, checked, "");
} }
} }
if ( modifiers != NULL ) { if ( modifiers != NULL ) {

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 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 processMenuItem(Menu *menu, id parentMenu, JsonNode *item);
void processMenuData(Menu *menu, JsonNode *menuData); void processMenuData(Menu *menu, JsonNode *menuData);
void processRadioGroupJSON(Menu *menu, JsonNode *radioGroup) ; void processRadioGroupJSON(Menu *menu, JsonNode *radioGroup);
id GetMenu(Menu *menu); id GetMenu(Menu *menu);
id createAttributedString(const char* title, const char* fontName, int fontSize, const char* RGBA);
#endif //ASSETS_C_MENU_DARWIN_H #endif //ASSETS_C_MENU_DARWIN_H

View File

@@ -6,6 +6,8 @@
#include "traymenu_darwin.h" #include "traymenu_darwin.h"
#include "trayicons.h" #include "trayicons.h"
extern Class trayMenuDelegateClass;
// A cache for all our tray menu icons // A cache for all our tray menu icons
// Global because it's a singleton // Global because it's a singleton
struct hashmap_s trayIconCache; struct hashmap_s trayIconCache;
@@ -29,12 +31,23 @@ TrayMenu* NewTrayMenu(const char* menuJSON) {
result->ID = mustJSONString(processedJSON, "ID"); result->ID = mustJSONString(processedJSON, "ID");
result->label = mustJSONString(processedJSON, "Label"); result->label = mustJSONString(processedJSON, "Label");
result->icon = mustJSONString(processedJSON, "Icon"); result->icon = mustJSONString(processedJSON, "Image");
JsonNode* processedMenu = mustJSONObject(processedJSON, "ProcessedMenu"); 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 // Create the menu
JsonNode* processedMenu = mustJSONObject(processedJSON, "ProcessedMenu");
result->menu = NewMenu(processedMenu); result->menu = NewMenu(processedMenu);
result->delegate = NULL;
// Init tray status bar item // Init tray status bar item
result->statusbaritem = NULL; 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 // Exit early if NULL
if( trayMenu->label == NULL ) { if( trayMenu->label == NULL ) {
return; return;
} }
// Update button label // Update button label
id statusBarButton = msg(trayMenu->statusbaritem, s("button")); id statusBarButton = msg_reg(trayMenu->statusbaritem, s("button"));
msg(statusBarButton, s("setTitle:"), str(label)); 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) { void UpdateTrayIcon(TrayMenu *trayMenu) {
@@ -68,44 +89,57 @@ void UpdateTrayIcon(TrayMenu *trayMenu) {
return; return;
} }
id statusBarButton = msg(trayMenu->statusbaritem, s("button")); id statusBarButton = msg_reg(trayMenu->statusbaritem, s("button"));
// Empty icon means remove it // Empty icon means remove it
if( STREMPTY(trayMenu->icon) ) { if( STREMPTY(trayMenu->icon) ) {
// Remove image // Remove image
msg(statusBarButton, s("setImage:"), NULL); msg_id(statusBarButton, s("setImage:"), NULL);
return; return;
} }
id trayImage = hashmap_get(&trayIconCache, trayMenu->icon, strlen(trayMenu->icon)); 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) {
trayImage = createImageFromBase64Data(trayMenu->icon, trayMenu->templateImage);
}
msg_int(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition);
msg_id(statusBarButton, s("setImage:"), trayImage);
} }
void ShowTrayMenu(TrayMenu* trayMenu) { void ShowTrayMenu(TrayMenu* trayMenu) {
// Create a status bar item if we don't have one // Create a status bar item if we don't have one
if( trayMenu->statusbaritem == NULL ) { if( trayMenu->statusbaritem == NULL ) {
id statusBar = msg( c("NSStatusBar"), s("systemStatusBar") ); id statusBar = msg_reg( c("NSStatusBar"), s("systemStatusBar") );
trayMenu->statusbaritem = msg(statusBar, s("statusItemWithLength:"), NSVariableStatusItemLength); trayMenu->statusbaritem = ((id(*)(id, SEL, CGFloat))objc_msgSend)(statusBar, s("statusItemWithLength:"), NSVariableStatusItemLength);
msg(trayMenu->statusbaritem, s("retain")); msg_reg(trayMenu->statusbaritem, s("retain"));
} }
id statusBarButton = msg(trayMenu->statusbaritem, s("button")); id statusBarButton = msg_reg(trayMenu->statusbaritem, s("button"));
msg(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition); msg_uint(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition);
// Update the icon if needed // Update the icon if needed
UpdateTrayIcon(trayMenu); UpdateTrayIcon(trayMenu);
// Update the label if needed // 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 // Update the menu
id menu = GetMenu(trayMenu->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 // UpdateTrayMenuInPlace receives 2 menus. The current menu gets
@@ -147,12 +181,16 @@ void DeleteTrayMenu(TrayMenu* trayMenu) {
// Free the status item // Free the status item
if ( trayMenu->statusbaritem != NULL ) { if ( trayMenu->statusbaritem != NULL ) {
id statusBar = msg( c("NSStatusBar"), s("systemStatusBar") ); id statusBar = msg_reg( c("NSStatusBar"), s("systemStatusBar") );
msg(statusBar, s("removeStatusItem:"), trayMenu->statusbaritem); msg_id(statusBar, s("removeStatusItem:"), trayMenu->statusbaritem);
msg(trayMenu->statusbaritem, s("release")); msg_reg(trayMenu->statusbaritem, s("release"));
trayMenu->statusbaritem = NULL; trayMenu->statusbaritem = NULL;
} }
if ( trayMenu->delegate != NULL ) {
msg_reg(trayMenu->delegate, s("release"));
}
// Free the tray menu memory // Free the tray menu memory
MEMFREE(trayMenu); MEMFREE(trayMenu);
} }
@@ -182,9 +220,9 @@ void LoadTrayIcons() {
int length = atoi((const char *)lengthAsString); int length = atoi((const char *)lengthAsString);
// Create the icon and add to the hashmap // 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"); 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); hashmap_put(&trayIconCache, (const char *)name, strlen((const char *)name), trayImage);
} }
} }

View File

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

View File

@@ -16,6 +16,11 @@ TrayMenuStore* NewTrayMenuStore() {
ABORT("[NewTrayMenuStore] Not enough memory to allocate trayMenuMap!"); 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; return result;
} }
@@ -25,15 +30,19 @@ int dumpTrayMenu(void *const context, struct hashmap_element_s *const e) {
} }
void DumpTrayMenuStore(TrayMenuStore* store) { void DumpTrayMenuStore(TrayMenuStore* store) {
pthread_mutex_lock(&store->lock);
hashmap_iterate_pairs(&store->trayMenuMap, dumpTrayMenu, NULL); hashmap_iterate_pairs(&store->trayMenuMap, dumpTrayMenu, NULL);
pthread_mutex_unlock(&store->lock);
} }
void AddTrayMenuToStore(TrayMenuStore* store, const char* menuJSON) { void AddTrayMenuToStore(TrayMenuStore* store, const char* menuJSON) {
TrayMenu* newMenu = NewTrayMenu(menuJSON); TrayMenu* newMenu = NewTrayMenu(menuJSON);
pthread_mutex_lock(&store->lock);
//TODO: check if there is already an entry for this menu //TODO: check if there is already an entry for this menu
hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu); 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) { 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) { void ShowTrayMenusInStore(TrayMenuStore* store) {
pthread_mutex_lock(&store->lock);
if( hashmap_num_entries(&store->trayMenuMap) > 0 ) { if( hashmap_num_entries(&store->trayMenuMap) > 0 ) {
hashmap_iterate_pairs(&store->trayMenuMap, showTrayMenu, NULL); hashmap_iterate_pairs(&store->trayMenuMap, showTrayMenu, NULL);
} }
pthread_mutex_unlock(&store->lock);
} }
int freeTrayMenu(void *const context, struct hashmap_element_s *const e) { int freeTrayMenu(void *const context, struct hashmap_element_s *const e) {
DeleteTrayMenu(e->data); DeleteTrayMenu(e->data);
return -1; return -1;
@@ -65,22 +75,39 @@ void DeleteTrayMenuStore(TrayMenuStore *store) {
// Destroy tray menu map // Destroy tray menu map
hashmap_destroy(&store->trayMenuMap); hashmap_destroy(&store->trayMenuMap);
pthread_mutex_destroy(&store->lock);
} }
TrayMenu* GetTrayMenuFromStore(TrayMenuStore* store, const char* menuID) { TrayMenu* GetTrayMenuFromStore(TrayMenuStore* store, const char* menuID) {
// Get the current menu // 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) { TrayMenu* MustGetTrayMenuFromStore(TrayMenuStore* store, const char* menuID) {
// Get the current menu // Get the current menu
pthread_mutex_lock(&store->lock);
TrayMenu* result = hashmap_get(&store->trayMenuMap, menuID, strlen(menuID)); TrayMenu* result = hashmap_get(&store->trayMenuMap, menuID, strlen(menuID));
pthread_mutex_unlock(&store->lock);
if (result == NULL ) { if (result == NULL ) {
ABORT("Unable to find TrayMenu with ID '%s' in the TrayMenuStore!", menuID); ABORT("Unable to find TrayMenu with ID '%s' in the TrayMenuStore!", menuID);
} }
return result; 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) { void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON) {
// Parse the JSON // Parse the JSON
JsonNode *parsedUpdate = mustParseJSON(JSON); JsonNode *parsedUpdate = mustParseJSON(JSON);
@@ -91,7 +118,17 @@ void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON) {
// Check we have this menu // Check we have this menu
TrayMenu *menu = MustGetTrayMenuFromStore(store, ID); 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 we don't have a menu, we create one
if ( currentMenu == NULL ) { if ( currentMenu == NULL ) {
// Store the new menu // Store the new menu
pthread_mutex_lock(&store->lock);
hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu); hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
pthread_mutex_unlock(&store->lock);
// Show it // Show it
ShowTrayMenu(newMenu); ShowTrayMenu(newMenu);
@@ -116,7 +155,9 @@ void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON) {
// Save the status bar reference // Save the status bar reference
newMenu->statusbaritem = currentMenu->statusbaritem; newMenu->statusbaritem = currentMenu->statusbaritem;
pthread_mutex_lock(&store->lock);
hashmap_remove(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID)); hashmap_remove(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID));
pthread_mutex_unlock(&store->lock);
// Delete the current menu // Delete the current menu
DeleteMenu(currentMenu->menu); DeleteMenu(currentMenu->menu);
@@ -125,9 +166,10 @@ void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON) {
// Free the tray menu memory // Free the tray menu memory
MEMFREE(currentMenu); MEMFREE(currentMenu);
pthread_mutex_lock(&store->lock);
hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu); hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
pthread_mutex_unlock(&store->lock);
// Show the updated menu // Show the updated menu
ShowTrayMenu(newMenu); ShowTrayMenu(newMenu);
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,29 +3,39 @@ package menumanager
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"strconv"
"sync"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/wailsapp/wails/v2/pkg/menu" "github.com/wailsapp/wails/v2/pkg/menu"
"sync"
) )
var trayMenuID int var trayMenuID int
var trayMenuIDMutex sync.Mutex var trayMenuIDMutex sync.Mutex
func generateTrayID() string { func generateTrayID() string {
var idStr string
trayMenuIDMutex.Lock() trayMenuIDMutex.Lock()
result := fmt.Sprintf("%d", trayMenuID) idStr = strconv.Itoa(trayMenuID)
trayMenuID++ trayMenuID++
trayMenuIDMutex.Unlock() trayMenuIDMutex.Unlock()
return result return idStr
} }
type TrayMenu struct { type TrayMenu struct {
ID string ID string
Label string Label string
Icon string FontSize int
menuItemMap *MenuItemMap FontName string
menu *menu.Menu Disabled bool
ProcessedMenu *WailsMenu 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) { func (t *TrayMenu) AsJSON() (string, error) {
@@ -39,10 +49,17 @@ func (t *TrayMenu) AsJSON() (string, error) {
func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu { func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu {
result := &TrayMenu{ result := &TrayMenu{
Label: trayMenu.Label, Label: trayMenu.Label,
Icon: trayMenu.Icon, FontName: trayMenu.FontName,
menu: trayMenu.Menu, FontSize: trayMenu.FontSize,
menuItemMap: NewMenuItemMap(), 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) result.menuItemMap.AddMenu(trayMenu.Menu)
@@ -51,6 +68,28 @@ func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu {
return result 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) { func (m *Manager) AddTrayMenu(trayMenu *menu.TrayMenu) (string, error) {
newTrayMenu := NewTrayMenu(trayMenu) newTrayMenu := NewTrayMenu(trayMenu)
@@ -65,6 +104,14 @@ func (m *Manager) AddTrayMenu(trayMenu *menu.TrayMenu) (string, error) {
return newTrayMenu.AsJSON() 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 // SetTrayMenu updates or creates a menu
func (m *Manager) SetTrayMenu(trayMenu *menu.TrayMenu) (string, error) { func (m *Manager) SetTrayMenu(trayMenu *menu.TrayMenu) (string, error) {
trayID, trayMenuKnown := m.trayMenuPointers[trayMenu] trayID, trayMenuKnown := m.trayMenuPointers[trayMenu]
@@ -102,13 +149,27 @@ func (m *Manager) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) (string, error) {
} }
type LabelUpdate struct { type LabelUpdate struct {
ID string ID string
Label string Label string
FontName string
FontSize int
RGBA string
Disabled bool
Tooltip string
Image string
MacTemplateImage bool
} }
update := &LabelUpdate{ update := &LabelUpdate{
ID: trayID, ID: trayID,
Label: trayMenu.Label, 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) data, err := json.Marshal(update)

View File

@@ -37,6 +37,7 @@ type Client interface {
SetTrayMenu(trayMenuJSON string) SetTrayMenu(trayMenuJSON string)
UpdateTrayMenuLabel(JSON string) UpdateTrayMenuLabel(JSON string)
UpdateContextMenu(contextMenuJSON string) UpdateContextMenu(contextMenuJSON string)
DeleteTrayMenuByID(id string)
} }
// DispatchClient is what the frontends use to interface with the // 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:] callbackid := message[2:]
topic = "menu:clicked" topic = "menu:clicked"
data = callbackid data = callbackid
case 'o':
callbackid := message[2:]
topic = "menu:ontrayopen"
data = callbackid
case 'c':
callbackid := message[2:]
topic = "menu:ontrayclose"
data = callbackid
default: default:
return nil, fmt.Errorf("invalid menu message: %s", message) 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, 'M': menuMessageParser,
'T': trayMessageParser, 'T': trayMessageParser,
'X': contextMenusMessageParser, 'X': contextMenusMessageParser,
'U': urlMessageParser,
} }
// 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 { if len(message) == 0 {
return nil, fmt.Errorf("MessageParser received blank message"); return nil, fmt.Errorf("MessageParser received blank message")
} }
parseMethod := messageParsers[message[0]] 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 // This is our startup hook - the frontend is now ready
case 'S': case 'S':
topic := "hooks:startup" topic := "hooks:startup"
responseMessage = &parsedMessage{Topic: topic, Data: nil} startupURL := message[1:]
responseMessage = &parsedMessage{Topic: topic, Data: startupURL}
default: default:
return nil, fmt.Errorf("Invalid message to systemMessageParser()") 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 { for _, client := range d.clients {
client.frontend.UpdateTrayMenuLabel(updatedTrayMenuLabel) 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: default:
d.logger.Error("Unknown menufrontend command: %s", command) 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); let selectedMenu = writable(null);
function fade(node, { delay = 0, duration = 400, easing = identity } = {}) { function fade(node, { delay = 0, duration = 400, easing = identity } = {}) {
@@ -1666,6 +1678,11 @@
let trayLabelData = JSON.parse(updateTrayLabelJSON); let trayLabelData = JSON.parse(updateTrayLabelJSON);
updateTrayLabel(trayLabelData); updateTrayLabel(trayLabelData);
break break
case 'D':
// Delete Tray Menu
const id = trayMessage.slice(1);
deleteTrayMenu(id);
break
default: default:
log('Unknown tray message: ' + message.data); log('Unknown tray message: ' + message.data);
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@wails/runtime", "name": "@wails/runtime",
"version": "1.3.10", "version": "1.3.12",
"description": "Wails V2 Javascript runtime library", "description": "Wails V2 Javascript runtime library",
"main": "main.js", "main": "main.js",
"types": "runtime.d.ts", "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); export let selectedMenu = writable(null);

View File

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

View File

@@ -10,6 +10,7 @@ type Menu interface {
UpdateApplicationMenu() UpdateApplicationMenu()
UpdateContextMenu(contextMenu *menu.ContextMenu) UpdateContextMenu(contextMenu *menu.ContextMenu)
SetTrayMenu(trayMenu *menu.TrayMenu) SetTrayMenu(trayMenu *menu.TrayMenu)
DeleteTrayMenu(trayMenu *menu.TrayMenu)
UpdateTrayMenuLabel(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) { func (m *menuRuntime) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) {
m.bus.Publish("menu:updatetraymenulabel", 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(), ":") splitTopic := strings.Split(menuMessage.Topic(), ":")
menuMessageType := splitTopic[1] menuMessageType := splitTopic[1]
switch menuMessageType { switch menuMessageType {
case "ontrayopen":
trayID := menuMessage.Data().(string)
m.menuManager.OnTrayMenuOpen(trayID)
case "ontrayclose":
trayID := menuMessage.Data().(string)
m.menuManager.OnTrayMenuClose(trayID)
case "clicked": case "clicked":
if len(splitTopic) != 2 { if len(splitTopic) != 2 {
m.logger.Error("Received clicked message with invalid topic format. Expected 2 sections in topic, got %s", splitTopic) 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 // Notify frontend of menu change
m.bus.Publish("menufrontend:settraymenu", updatedMenu) 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": case "updatetraymenulabel":
trayMenu := menuMessage.Data().(*menu.TrayMenu) trayMenu := menuMessage.Data().(*menu.TrayMenu)
updatedLabel, err := m.menuManager.UpdateTrayMenuLabel(trayMenu) updatedLabel, err := m.menuManager.UpdateTrayMenuLabel(trayMenu)

View File

@@ -34,6 +34,9 @@ type Runtime struct {
// Startup Hook // Startup Hook
startupOnce sync.Once startupOnce sync.Once
// Service bus
bus *servicebus.ServiceBus
} }
// NewRuntime creates a new runtime subsystem // NewRuntime creates a new runtime subsystem
@@ -58,6 +61,7 @@ func NewRuntime(ctx context.Context, bus *servicebus.ServiceBus, logger *logger.
runtime: runtime.New(bus), runtime: runtime.New(bus),
startupCallback: startupCallback, startupCallback: startupCallback,
ctx: ctx, ctx: ctx,
bus: bus,
} }
return result, nil return result, nil
@@ -79,7 +83,15 @@ func (r *Runtime) Start() error {
case "startup": case "startup":
if r.startupCallback != nil { if r.startupCallback != nil {
r.startupOnce.Do(func() { 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 { } else {
r.logger.Warning("no startup callback registered!") 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 // Ignore errors as it isn't a showstopper
key, _ := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE) 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") 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 return err
} }
i.OS = osinfo 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 return nil
} }

View File

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

View File

@@ -4,7 +4,7 @@
<link rel="stylesheet" href="/main.css"> <link rel="stylesheet" href="/main.css">
</head> </head>
<body> <body data-wails-drag>
<div id="logo"></div> <div id="logo"></div>
<div id="input"> <div id="input">
<input id="name" type="text"></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 package main
import ( import (
"github.com/wailsapp/wails/v2"
"log" "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() { func main() {
// Create application with options // Create application with options
app, err := wails.CreateApp("{{.ProjectName}}", 1024, 768) app := NewBasic()
if err != nil {
log.Fatal(err)
}
app.Bind(newBasic()) err := wails.Run(&options.App{
Title: "{{.ProjectName}}",
err = app.Run() 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 { if err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@@ -10,6 +10,8 @@ import (
"runtime" "runtime"
"strings" "strings"
"github.com/pkg/errors"
"github.com/leaanthony/slicer" "github.com/leaanthony/slicer"
"github.com/wailsapp/wails/v2/internal/assetdb" "github.com/wailsapp/wails/v2/internal/assetdb"
"github.com/wailsapp/wails/v2/internal/fs" "github.com/wailsapp/wails/v2/internal/fs"
@@ -19,6 +21,10 @@ import (
"github.com/wailsapp/wails/v2/pkg/clilogger" "github.com/wailsapp/wails/v2/pkg/clilogger"
) )
const (
VERBOSE int = 2
)
// BaseBuilder is the common builder struct // BaseBuilder is the common builder struct
type BaseBuilder struct { type BaseBuilder struct {
filesToDelete slicer.StringSlicer filesToDelete slicer.StringSlicer
@@ -142,9 +148,30 @@ 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 // CompileProject compiles the project
func (b *BaseBuilder) CompileProject(options *Options) error { func (b *BaseBuilder) CompileProject(options *Options) error {
verbose := options.Verbosity == VERBOSE
// Run go mod tidy first
cmd := exec.Command(options.Compiler, "mod", "tidy")
if verbose {
println("")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
err := cmd.Run()
if err != nil {
return err
}
// Default go build command // Default go build command
commands := slicer.String([]string{"build"}) commands := slicer.String([]string{"build"})
@@ -188,9 +215,11 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
// Get application build directory // Get application build directory
appDir := options.BuildDirectory appDir := options.BuildDirectory
err := cleanBuildDirectory(options) if options.CleanBuildDirectory {
if err != nil { err = cleanBuildDirectory(options)
return err if err != nil {
return err
}
} }
if options.LDFlags != "" { if options.LDFlags != "" {
@@ -199,10 +228,7 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
} }
// Set up output filename // Set up output filename
outputFile := options.OutputFile outputFile := b.OutputFilename(options)
if outputFile == "" {
outputFile = b.projectData.OutputFilename
}
compiledBinary := filepath.Join(appDir, outputFile) compiledBinary := filepath.Join(appDir, outputFile)
commands.Add("-o") commands.Add("-o")
commands.Add(compiledBinary) commands.Add(compiledBinary)
@@ -211,14 +237,15 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
options.CompiledBinary = compiledBinary options.CompiledBinary = compiledBinary
// Create the command // Create the command
cmd := exec.Command(options.Compiler, commands.AsSlice()...) cmd = exec.Command(options.Compiler, commands.AsSlice()...)
if verbose {
println(" Build command:", commands.Join(" "))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
// Set the directory // Set the directory
cmd.Dir = b.projectData.Path cmd.Dir = b.projectData.Path
// Set GO111MODULE environment variable
cmd.Env = append(os.Environ(), "GO111MODULE=on")
// Add CGO flags // Add CGO flags
// We use the project/build dir as a temporary place for our generated c headers // We use the project/build dir as a temporary place for our generated c headers
buildBaseDir, err := fs.RelativeToCwd("build") buildBaseDir, err := fs.RelativeToCwd("build")
@@ -226,7 +253,32 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
return err return err
} }
cmd.Env = append(os.Environ(), fmt.Sprintf("CGO_CFLAGS=-I%s", buildBaseDir)) cmd.Env = os.Environ() // inherit env
// 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"
})
if verbose {
println(" Environment:", strings.Join(cmd.Env, " "))
}
// Setup buffers // Setup buffers
var stdo, stde bytes.Buffer var stdo, stde bytes.Buffer
@@ -241,16 +293,37 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
return fmt.Errorf("%s\n%s", err, string(stde.Bytes())) return fmt.Errorf("%s\n%s", err, string(stde.Bytes()))
} }
if !options.Compress {
return nil
}
// Do we have upx installed?
if !shell.CommandExists("upx") {
println("Warning: Cannot compress binary: upx not found")
return nil
}
if verbose {
println(" Compressing with:", "upx", "--best", "--no-color", "--no-progress", options.CompiledBinary)
}
output, err := exec.Command(options.BuildDirectory, "upx", "--best", "--no-color", "--no-progress", options.CompiledBinary).Output()
if err != nil {
return errors.Wrap(err, "Error during compression:")
}
if verbose {
println(output)
}
return nil return nil
} }
// NpmInstall runs "npm install" in the given directory // NpmInstall runs "npm install" in the given directory
func (b *BaseBuilder) NpmInstall(sourceDir string) error { func (b *BaseBuilder) NpmInstall(sourceDir string, verbose bool) error {
return b.NpmInstallUsingCommand(sourceDir, "npm install") return b.NpmInstallUsingCommand(sourceDir, "npm install", verbose)
} }
// NpmInstallUsingCommand runs the given install command in the specified npm project directory // 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") packageJSON := filepath.Join(sourceDir, "package.json")
@@ -292,7 +365,7 @@ func (b *BaseBuilder) NpmInstallUsingCommand(sourceDir string, installCommand st
// Split up the InstallCommand and execute it // Split up the InstallCommand and execute it
cmd := strings.Split(installCommand, " ") cmd := strings.Split(installCommand, " ")
stdout, stderr, err := shell.RunCommand(sourceDir, cmd[0], cmd[1:]...) stdout, stderr, err := shell.RunCommand(sourceDir, cmd[0], cmd[1:]...)
if err != nil { if verbose || err != nil {
for _, l := range strings.Split(stdout, "\n") { for _, l := range strings.Split(stdout, "\n") {
fmt.Printf(" %s\n", l) fmt.Printf(" %s\n", l)
} }
@@ -341,31 +414,40 @@ func (b *BaseBuilder) NpmRunWithEnvironment(projectDir, buildTarget string, verb
func (b *BaseBuilder) BuildFrontend(outputLogger *clilogger.CLILogger) error { func (b *BaseBuilder) BuildFrontend(outputLogger *clilogger.CLILogger) error {
// TODO: Fix this up from the CLI // TODO: Fix this up from the CLI
verbose := false verbose := b.options.Verbosity == VERBOSE
frontendDir := filepath.Join(b.projectData.Path, "frontend") frontendDir := filepath.Join(b.projectData.Path, "frontend")
// Check there is an 'InstallCommand' provided in wails.json // Check there is an 'InstallCommand' provided in wails.json
if b.projectData.InstallCommand == "" { if b.projectData.InstallCommand == "" {
// No - don't install // No - don't install
outputLogger.Println(" - No Install command. Skipping.") outputLogger.Println("No Install command. Skipping.")
} else { } else {
// Do install if needed // Do install if needed
outputLogger.Println(" - Installing dependencies...") outputLogger.Print("Installing frontend dependencies: ")
if err := b.NpmInstallUsingCommand(frontendDir, b.projectData.InstallCommand); err != nil { if verbose {
outputLogger.Println("")
outputLogger.Println(" Install command: '" + b.projectData.InstallCommand + "'")
}
if err := b.NpmInstallUsingCommand(frontendDir, b.projectData.InstallCommand, verbose); err != nil {
return err return err
} }
outputLogger.Println("Done.")
} }
// Check if there is a build command // Check if there is a build command
if b.projectData.BuildCommand == "" { if b.projectData.BuildCommand == "" {
outputLogger.Println(" - No Build command. Skipping.") outputLogger.Println("No Build command. Skipping.")
// No - ignore // No - ignore
return nil return nil
} }
outputLogger.Println(" - Compiling Frontend Project") outputLogger.Print("Compiling frontend: ")
cmd := strings.Split(b.projectData.BuildCommand, " ") cmd := strings.Split(b.projectData.BuildCommand, " ")
if verbose {
outputLogger.Println("")
outputLogger.Println(" Build command: '" + strings.Join(cmd, " ") + "'")
}
stdout, stderr, err := shell.RunCommand(frontendDir, cmd[0], cmd[1:]...) stdout, stderr, err := shell.RunCommand(frontendDir, cmd[0], cmd[1:]...)
if verbose || err != nil { if verbose || err != nil {
for _, l := range strings.Split(stdout, "\n") { for _, l := range strings.Split(stdout, "\n") {
@@ -375,7 +457,12 @@ func (b *BaseBuilder) BuildFrontend(outputLogger *clilogger.CLILogger) error {
fmt.Printf(" %s\n", l) 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 // ExtractAssets gets the assets from the index.html file
@@ -384,3 +471,22 @@ func (b *BaseBuilder) ExtractAssets() (*html.AssetBundle, error) {
// Read in html // Read in html
return html.NewAssetBundle(b.projectData.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" "path/filepath"
"runtime" "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/internal/project"
"github.com/wailsapp/wails/v2/pkg/clilogger" "github.com/wailsapp/wails/v2/pkg/clilogger"
) )
@@ -25,19 +28,24 @@ var modeMap = []string{"Debug", "Production"}
// Options contains all the build options as well as the project data // Options contains all the build options as well as the project data
type Options struct { type Options struct {
LDFlags string // Optional flags to pass to linker LDFlags string // Optional flags to pass to linker
Logger *clilogger.CLILogger // All output to the logger Logger *clilogger.CLILogger // All output to the logger
OutputType string // EG: desktop, server.... OutputType string // EG: desktop, server....
Mode Mode // release or debug Mode Mode // release or debug
ProjectData *project.Project // The project data ProjectData *project.Project // The project data
Pack bool // Create a package for the app after building Pack bool // Create a package for the app after building
Platform string // The platform to build for Platform string // The platform to build for
Compiler string // The compiler command to use Arch string // The architecture to build for
IgnoreFrontend bool // Indicates if the frontend does not need building Compiler string // The compiler command to use
OutputFile string // Override the output filename IgnoreFrontend bool // Indicates if the frontend does not need building
BuildDirectory string // Directory to use for building the application OutputFile string // Override the output filename
CompiledBinary string // Fully qualified path to the compiled binary BuildDirectory string // Directory to use for building the application
KeepAssets bool // /Keep the generated assets/files CleanBuildDirectory bool // Indicates if the build directory should be cleaned before building
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)
Compress bool // Compress the final binary
AppleIdentity string
} }
// GetModeAsString returns the current mode as a string // GetModeAsString returns the current mode as a string
@@ -57,12 +65,6 @@ func Build(options *Options) (string, error) {
return "", err 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 // Load project
projectData, err := project.Load(cwd) projectData, err := project.Load(cwd)
if err != nil { if err != nil {
@@ -70,7 +72,7 @@ func Build(options *Options) (string, error) {
} }
options.ProjectData = projectData options.ProjectData = projectData
// Calculate build dir // Set build directory
options.BuildDirectory = filepath.Join(options.ProjectData.Path, "build", options.Platform, options.OutputType) options.BuildDirectory = filepath.Join(options.ProjectData.Path, "build", options.Platform, options.OutputType)
// Save the project type // Save the project type
@@ -106,7 +108,6 @@ func Build(options *Options) (string, error) {
// return "", err // return "", err
// } // }
if !options.IgnoreFrontend { if !options.IgnoreFrontend {
outputLogger.Println(" - Building Project Frontend")
err = builder.BuildFrontend(outputLogger) err = builder.BuildFrontend(outputLogger)
if err != nil { if err != nil {
return "", err return "", err
@@ -114,30 +115,74 @@ func Build(options *Options) (string, error) {
} }
// Build the base assets // Build the base assets
outputLogger.Println(" - Compiling Assets")
err = builder.BuildAssets(options) err = builder.BuildAssets(options)
if err != nil { if err != nil {
return "", err return "", err
} }
// Compile the application // Compile the application
outputLogger.Print(" - Compiling Application in " + GetModeAsString(options.Mode) + " mode...") outputLogger.Print("Compiling application: ")
err = builder.CompileProject(options)
if err != nil { if options.Platform == "darwin" && options.Arch == "universal" {
return "", err outputFile := builder.OutputFilename(options)
amd64Filename := outputFile + "-amd64"
arm64Filename := outputFile + "-arm64"
// Build amd64 first
options.Arch = "amd64"
options.OutputFile = amd64Filename
options.CleanBuildDirectory = false
if options.Verbosity == VERBOSE {
println()
println(" Building AMD64 Target:", filepath.Join(options.BuildDirectory, options.OutputFile))
}
err = builder.CompileProject(options)
if err != nil {
return "", err
}
// Build arm64
options.Arch = "arm64"
options.OutputFile = arm64Filename
options.CleanBuildDirectory = false
if options.Verbosity == VERBOSE {
println(" Building ARM64 Target:", filepath.Join(options.BuildDirectory, options.OutputFile))
}
err = builder.CompileProject(options)
if err != nil {
return "", err
}
// Run lipo
if options.Verbosity == VERBOSE {
println(" Running lipo: ", "lipo", "-create", "-output", outputFile, amd64Filename, arm64Filename)
}
_, 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? // Do we need to pack the app?
if options.Pack { if options.Pack {
outputLogger.Println(" - Packaging Application") outputLogger.Print("Packaging application: ")
// TODO: Allow cross platform build // TODO: Allow cross platform build
err = packageProject(options, runtime.GOOS) err = packageProject(options, runtime.GOOS)
if err != nil { if err != nil {
return "", err return "", err
} }
outputLogger.Println("Done.")
} }
return projectData.OutputFilename, nil return projectData.OutputFilename, nil

View File

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

View File

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

View File

@@ -49,4 +49,4 @@ Example:
## Mac ## Mac
The `mac` directory holds files specific to Mac builds, such as `info.plist`. These may be edited and used as part of the build. The `mac` directory holds files specific to Mac builds, such as `Info.plist`. These may be edited and used as part of the build.

View File

@@ -2,9 +2,11 @@ package build
import ( import (
"bytes" "bytes"
"fmt"
"image" "image"
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec"
"path" "path"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -40,7 +42,7 @@ func packageApplication(options *Options) error {
return errors.Wrap(err, "Cannot move file: "+options.ProjectData.OutputFilename) return errors.Wrap(err, "Cannot move file: "+options.ProjectData.OutputFilename)
} }
// Generate info.plist // Generate Info.plist
err = processPList(options, contentsDirectory) err = processPList(options, contentsDirectory)
if err != nil { if err != nil {
return err return err
@@ -52,13 +54,21 @@ func packageApplication(options *Options) error {
return err return err
} }
// Sign app if needed
if options.AppleIdentity != "" {
err = signApplication(options)
if err != nil {
return err
}
}
return nil return nil
} }
func processPList(options *Options, contentsDirectory string) error { func processPList(options *Options, contentsDirectory string) error {
// Check if plist already exists in project dir // Check if plist already exists in project dir
plistFile := filepath.Join(options.ProjectData.AssetsDir, "mac", "info.plist") plistFile := filepath.Join(options.ProjectData.AssetsDir, "mac", "Info.plist")
// If the file doesn't exist, generate it // If the file doesn't exist, generate it
if !fs.FileExists(plistFile) { if !fs.FileExists(plistFile) {
@@ -69,7 +79,7 @@ func processPList(options *Options, contentsDirectory string) error {
} }
// Copy it to the contents directory // Copy it to the contents directory
targetFile := filepath.Join(contentsDirectory, "info.plist") targetFile := filepath.Join(contentsDirectory, "Info.plist")
return fs.CopyFile(plistFile, targetFile) return fs.CopyFile(plistFile, targetFile)
} }
@@ -78,11 +88,11 @@ func generateDefaultPlist(options *Options, targetPlistFile string) error {
exe := defaultString(options.OutputFile, name) exe := defaultString(options.OutputFile, name)
version := "1.0.0" version := "1.0.0"
author := defaultString(options.ProjectData.Author.Name, "Anonymous") author := defaultString(options.ProjectData.Author.Name, "Anonymous")
packageID := strings.Join([]string{"wails", name, version}, ".") packageID := strings.Join([]string{"wails", name}, ".")
plistData := newPlistData(name, exe, packageID, version, author) plistData := newPlistData(name, exe, packageID, version, author)
tmpl := template.New("infoPlist") tmpl := template.New("infoPlist")
plistTemplate := fs.RelativePath("./internal/packager/darwin/info.plist") plistTemplate := fs.RelativePath("./internal/packager/darwin/Info.plist")
infoPlist, err := ioutil.ReadFile(plistTemplate) infoPlist, err := ioutil.ReadFile(plistTemplate)
if err != nil { if err != nil {
return errors.Wrap(err, "Cannot open plist template") return errors.Wrap(err, "Cannot open plist template")
@@ -176,3 +186,21 @@ func processApplicationIcon(resourceDir string, iconsDir string) (err error) {
}() }()
return icns.Encode(dest, srcImg) 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 { func (s *ServerBuilder) BuildRuntime(options *Options) error {
sourceDir := fs.RelativePath("../../../internal/runtime/js") 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 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)
}

60
v2/pkg/mac/mac_darwin.go Normal file
View File

@@ -0,0 +1,60 @@
// Package mac provides MacOS related utility functions for Wails applications
package mac
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/leaanthony/slicer"
"github.com/pkg/errors"
"github.com/wailsapp/wails/v2/internal/shell"
)
// StartAtLogin will either add or remove this application to/from the login
// items, depending on the given boolean flag. The limitation is that the
// currently running app must be in an app bundle.
func StartAtLogin(enabled bool) error {
exe, err := os.Executable()
if err != nil {
return errors.Wrap(err, "Error running os.Executable:")
}
binName := filepath.Base(exe)
if !strings.HasSuffix(exe, "/Contents/MacOS/"+binName) {
return fmt.Errorf("app needs to be running as package.app file to start at login")
}
appPath := strings.TrimSuffix(exe, "/Contents/MacOS/"+binName)
var command string
if enabled {
command = fmt.Sprintf("tell application \"System Events\" to make login item at end with properties {name: \"%s\",path:\"%s\", hidden:false}", binName, appPath)
} else {
command = fmt.Sprintf("tell application \"System Events\" to delete login item \"%s\"", binName)
}
_, stde, err := shell.RunCommand("/tmp", "osascript", "-e", command)
if err != nil {
errors.Wrap(err, stde)
}
return nil
}
// StartsAtLogin will indicate if this application is in the login
// items. The limitation is that the currently running app must be
// in an app bundle.
func StartsAtLogin() (bool, error) {
exe, err := os.Executable()
if err != nil {
return false, err
}
binName := filepath.Base(exe)
if !strings.HasSuffix(exe, "/Contents/MacOS/"+binName) {
return false, fmt.Errorf("app needs to be running as package.app file to start at login")
}
results, stde, err := shell.RunCommand("/tmp", "osascript", "-e", `tell application "System Events" to get the name of every login item`)
if err != nil {
return false, errors.Wrap(err, stde)
}
results = strings.TrimSpace(results)
startupApps := slicer.String(strings.Split(results, ", "))
return startupApps.Contains(binName), nil
}

View File

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

View File

@@ -8,7 +8,7 @@ import (
"github.com/leaanthony/slicer" "github.com/leaanthony/slicer"
) )
var namedKeys = slicer.String([]string{"backspace", "tab", "return", "escape", "left", "right", "up", "down", "space", "delete", "home", "end", "page up", "page down", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12", "f13", "f14", "f15", "f16", "f17", "f18", "f19", "f20", "f21", "f22", "f23", "f24", "f25", "f26", "f27", "f28", "f29", "f30", "f31", "f32", "f33", "f34", "f35", "numlock"}) var namedKeys = slicer.String([]string{"backspace", "tab", "return", "enter", "escape", "left", "right", "up", "down", "space", "delete", "home", "end", "page up", "page down", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12", "f13", "f14", "f15", "f16", "f17", "f18", "f19", "f20", "f21", "f22", "f23", "f24", "f25", "f26", "f27", "f28", "f29", "f30", "f31", "f32", "f33", "f34", "f35", "numlock"})
func parseKey(key string) (string, bool) { func parseKey(key string) (string, bool) {

View File

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

View File

@@ -6,12 +6,38 @@ type TrayMenu struct {
// 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
// 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 // These are read up during build from <projectdir>/trayicons and
// the filenames are used as IDs, minus the extension // the filenames are used as IDs, minus the extension
// EG: <projectdir>/trayicons/main.png can be referenced here with "main" // 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 is the initial menu we wish to use for the tray
Menu *Menu 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 TrayMenus []*menu.TrayMenu
ContextMenus []*menu.ContextMenu ContextMenus []*menu.ContextMenu
ActivationPolicy ActivationPolicy ActivationPolicy ActivationPolicy
URLHandlers map[string]func(string)
} }

View File

@@ -1 +1 @@
info.plist Info.plist