Compare commits

...

58 Commits

Author SHA1 Message Date
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
50 changed files with 1487 additions and 509 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
@@ -41,24 +43,39 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
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 +89,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 +100,68 @@ 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)
}
// 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,
} }
// Calculate platform and arch
platformSplit := strings.Split(platform, "/")
buildOptions.Platform = platformSplit[0]
buildOptions.Arch = runtime.GOARCH
if len(platformSplit) == 2 {
buildOptions.Arch = platformSplit[1]
}
// Start a new tabwriter
w := new(tabwriter.Writer)
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
buildModeText := "debug"
if production {
buildModeText = "production"
}
// Write out the system information
fmt.Fprintf(w, "App Type: \t%s\n", buildOptions.OutputType)
fmt.Fprintf(w, "Platform: \t%s\n", buildOptions.Platform)
fmt.Fprintf(w, "Arch: \t%s\n", buildOptions.Arch)
fmt.Fprintf(w, "Compiler: \t%s\n", buildOptions.Compiler)
fmt.Fprintf(w, "Build Mode: \t%s\n", buildModeText)
fmt.Fprintf(w, "Package: \t%t\n", buildOptions.Pack)
fmt.Fprintf(w, "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.39" var version = "v2.0.0-alpha.57"

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

@@ -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);
} }

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
@@ -118,4 +137,6 @@ void SetActivationPolicy(struct Application* app, int policy);
void* lookupStringConstant(id constantName); void* lookupStringConstant(id constantName);
void HasURLHandlers(struct Application* app);
#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;
@@ -345,61 +345,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 +407,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 +436,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 +473,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);
@@ -540,18 +540,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;
} }
@@ -566,74 +566,42 @@ 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
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 // Process Menu Item attributes
id dictionary = ALLOC_INIT("NSMutableDictionary"); id dictionary = ALLOC_INIT("NSMutableDictionary");
// Process font // Process font
id font;
CGFloat fontSizeFloat = (CGFloat)fontSize; CGFloat fontSizeFloat = (CGFloat)fontSize;
// Check if valid // Check if valid
id fontNameAsNSString = str(fontName); id fontNameAsNSString = str(fontName);
id fontsOnSystem = msg(msg(c("NSFontManager"), s("sharedFontManager")), s("availableFonts")); id font = ((id(*)(id, SEL, id, CGFloat))objc_msgSend)(c("NSFont"), s("fontWithName:size:"), fontNameAsNSString, fontSizeFloat);
bool valid = msg(fontsOnSystem, s("containsObject:"), fontNameAsNSString); if( font == NULL ) {
if( valid ) { bool supportsMonospacedDigitSystemFont = (bool) ((id(*)(id, SEL, SEL))objc_msgSend)(c("NSFont"), s("respondsToSelector:"), s("monospacedDigitSystemFontOfSize:weight:"));
font = msg(c("NSFont"), s("fontWithName:size:"), fontNameAsNSString, fontSizeFloat);
} else {
bool supportsMonospacedDigitSystemFont = (bool) msg(c("NSFont"), s("respondsToSelector:"), s("monospacedDigitSystemFontOfSize:weight:"));
if( supportsMonospacedDigitSystemFont ) { if( supportsMonospacedDigitSystemFont ) {
font = msg(c("NSFont"), s("monospacedDigitSystemFontOfSize:weight:"), fontSizeFloat, NSFontWeightRegular); font = ((id(*)(id, SEL, CGFloat, CGFloat))objc_msgSend)(c("NSFont"), s("monospacedDigitSystemFontOfSize:weight:"), fontSizeFloat, (CGFloat)NSFontWeightRegular);
} else { } else {
font = msg(c("NSFont"), s("menuFontOfSize:"), fontSizeFloat); font = ((id(*)(id, SEL, CGFloat))objc_msgSend)(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_float(c("NSNumber"), s("numberWithFloat:"), (float)0.0);
id offset = msg(c("NSNumber"), s("numberWithFloat:"), 0.0); id offsetAttrName = lookupStringConstant(str("NSBaselineOffsetAttributeName"));
msg(dictionary, s("setObject:forKey:"), offset, lookupStringConstant(str("NSBaselineOffsetAttributeName"))); msg_id_id(dictionary, s("setObject:forKey:"), offset, offsetAttrName);
// RGBA // RGBA
if( RGBA != NULL && strlen(RGBA) > 0) { if( RGBA != NULL && strlen(RGBA) > 0) {
unsigned short r, g, b, a; unsigned short r, g, b, a;
@@ -642,32 +610,77 @@ 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);
msg_reg(colour, s("autorelease"));
} }
} }
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("release"));
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 data = ALLOC("NSData");
id imageData = ((id(*)(id, SEL, id, int))objc_msgSend)(data, s("initWithBase64EncodedString:options:"), str(image), 0);
id nsimage = ALLOC("NSImage");
msg_id(nsimage, s("initWithData:"), imageData);
if( templateImage ) {
msg_bool(nsimage, s("setTemplate:"), YES);
}
msg_id(item, s("setImage:"), nsimage);
}
id attributedString = createAttributedString(title, fontName, fontSize, RGBA);
msg_id(item, s("setAttributedTitle:"), attributedString);
//msg_id(item, s("setTitle:"), str(title));
msg_bool(item, s("setEnabled:"), !disabled);
msg_reg(item, s("autorelease"));
// Process modifiers // 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;
} }
@@ -701,8 +714,8 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
id thisMenuItem = createMenuItemNoAutorelease(str(name), NULL, ""); id thisMenuItem = createMenuItemNoAutorelease(str(name), NULL, "");
id thisMenu = createMenu(str(name)); id thisMenu = createMenu(str(name));
msg(thisMenuItem, s("setSubmenu:"), thisMenu); msg_id(thisMenuItem, s("setSubmenu:"), thisMenu);
msg(parentMenu, s("addItem:"), thisMenuItem); msg_id(parentMenu, s("addItem:"), thisMenuItem);
JsonNode *submenuItems = json_find_member(submenu, "Items"); JsonNode *submenuItems = json_find_member(submenu, "Items");
// If we have no items, just return // If we have no items, just return
@@ -727,6 +740,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 = "";
@@ -747,7 +765,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
@@ -782,7 +800,7 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
if( type != NULL ) { if( type != NULL ) {
if( STREQ(type->string_, "Text")) { if( STREQ(type->string_, "Text")) {
processTextMenuItem(menu, parentMenu, label, menuid, disabled, acceleratorkey, modifiers, tooltip, image, fontName, fontSize, RGBA, templateImage); processTextMenuItem(menu, parentMenu, label, menuid, disabled, acceleratorkey, modifiers, tooltip, image, fontName, fontSize, RGBA, templateImage, alternate);
} }
else if ( STREQ(type->string_, "Separator")) { else if ( STREQ(type->string_, "Separator")) {
addSeparator(parentMenu); addSeparator(parentMenu);

View File

@@ -105,10 +105,12 @@ id processRadioMenuItem(Menu *menu, id parentmenu, const char *title, const char
id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *key); id 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,64 @@ 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) {
id data = ALLOC("NSData");
id imageData = ((id(*)(id, SEL, id, int))objc_msgSend)(data, s("initWithBase64EncodedString:options:"), str(trayMenu->icon), 0);
trayImage = ALLOC("NSImage");
msg_id(trayImage, s("initWithData:"), imageData);
if( trayMenu->templateImage ) {
msg_bool(trayImage, s("setTemplate:"), YES);
}
}
msg_int(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition);
msg_id(statusBarButton, s("setImage:"), trayImage);
} }
void ShowTrayMenu(TrayMenu* trayMenu) { 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 +188,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 +227,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,9 +52,11 @@ 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) {
@@ -64,16 +75,24 @@ 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);
} }
@@ -81,8 +100,11 @@ TrayMenu* MustGetTrayMenuFromStore(TrayMenuStore* store, const char* menuID) {
} }
void DeleteTrayMenuInStore(TrayMenuStore* store, const char* ID) { void DeleteTrayMenuInStore(TrayMenuStore* store, const char* ID) {
TrayMenu *menu = MustGetTrayMenuFromStore(store, ID); TrayMenu *menu = MustGetTrayMenuFromStore(store, ID);
pthread_mutex_lock(&store->lock);
hashmap_remove(&store->trayMenuMap, ID, strlen(ID)); hashmap_remove(&store->trayMenuMap, ID, strlen(ID));
pthread_mutex_unlock(&store->lock);
DeleteTrayMenu(menu); DeleteTrayMenu(menu);
} }
@@ -96,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);
} }
@@ -110,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);
@@ -121,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);
@@ -130,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,8 @@ 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); void DeleteTrayMenuInStore(TrayMenuStore* store, const char* id);

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

@@ -23,12 +23,19 @@ func generateTrayID() string {
} }
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) {
@@ -42,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)
@@ -54,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)
@@ -113,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

@@ -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

@@ -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)

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

@@ -19,6 +19,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 +146,28 @@ func (b *BaseBuilder) CleanUp() {
}) })
} }
func (b *BaseBuilder) OutputFilename(options *Options) string {
outputFile := options.OutputFile
if outputFile == "" {
outputFile = b.projectData.OutputFilename
}
return outputFile
}
// CompileProject compiles the project // CompileProject compiles the project
func (b *BaseBuilder) CompileProject(options *Options) error { func (b *BaseBuilder) CompileProject(options *Options) error {
// Run go mod tidy first
cmd := exec.Command(options.Compiler, "mod", "tidy")
if options.Verbosity == VERBOSE {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
err := cmd.Run()
if err != nil {
return err
}
// Default go build command // Default go build command
commands := slicer.String([]string{"build"}) commands := slicer.String([]string{"build"})
@@ -160,6 +183,7 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
// potentially try and see if the assets have changed but will // potentially try and see if the assets have changed but will
// this take as much time as a `-a` build? // this take as much time as a `-a` build?
commands.Add("-a") commands.Add("-a")
commands.Add("-x")
var tags slicer.StringSlicer var tags slicer.StringSlicer
tags.Add(options.OutputType) tags.Add(options.OutputType)
@@ -188,9 +212,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 +225,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 +234,14 @@ 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 options.Verbosity == VERBOSE {
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,31 +249,47 @@ 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
// Setup buffers // Use upsertEnv so we don't overwrite user's CGO_CFLAGS
var stdo, stde bytes.Buffer cmd.Env = upsertEnv(cmd.Env, "CGO_CFLAGS", func(v string) string {
cmd.Stdout = &stdo if v != "" {
cmd.Stderr = &stde v += " "
}
v += "-I" + buildBaseDir
return v
})
cmd.Env = upsertEnv(cmd.Env, "GOOS", func(v string) string {
return options.Platform
})
cmd.Env = upsertEnv(cmd.Env, "GOARCH", func(v string) string {
return options.Arch
})
cmd.Env = upsertEnv(cmd.Env, "CGO_ENABLED", func(v string) string {
return "1"
})
// Run command // Run command
err = cmd.Run() err = cmd.Run()
// Format error if we have one // Format error if we have one
if err != nil { if err != nil {
return fmt.Errorf("%s\n%s", err, string(stde.Bytes())) return err
} }
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 +331,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 +380,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("\tCommand: " + 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("\tCommand: '" + 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 +423,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 +437,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,23 @@ 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)
AppleIdentity string
} }
// GetModeAsString returns the current mode as a string // GetModeAsString returns the current mode as a string
@@ -57,12 +64,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 +71,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 +107,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 +114,62 @@ 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
err = builder.CompileProject(options)
if err != nil {
return "", err
}
// Build arm64
options.Arch = "arm64"
options.OutputFile = arm64Filename
err = builder.CompileProject(options)
if err != nil {
return "", err
}
// Run lipo
_, stderr, err := shell.RunCommand(options.BuildDirectory, "lipo", "-create", "-output", outputFile, amd64Filename, arm64Filename)
if err != nil {
return "", fmt.Errorf("%s - %s", err.Error(), stderr)
}
// Remove temp binaries
fs.DeleteFile(filepath.Join(options.BuildDirectory, amd64Filename))
fs.DeleteFile(filepath.Join(options.BuildDirectory, arm64Filename))
projectData.OutputFilename = outputFile
options.CompiledBinary = filepath.Join(options.BuildDirectory, outputFile)
} else {
err = builder.CompileProject(options)
if err != nil {
return "", err
}
} }
outputLogger.Println("done.") outputLogger.Println("Done.")
// Do we need to pack the app? // 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

@@ -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"
@@ -52,6 +54,14 @@ 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
} }
@@ -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)
}

51
v2/pkg/mac/mac.go Normal file
View File

@@ -0,0 +1,51 @@
// build
package mac
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/leaanthony/slicer"
"github.com/pkg/errors"
"github.com/wailsapp/wails/v2/internal/shell"
)
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 startup")
}
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
}
func StartsAtLogin() (bool, error) {
exe, err := os.Executable()
if err != nil {
return false, err
}
binName := filepath.Base(exe)
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

@@ -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

@@ -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)
} }