Compare commits

...

60 Commits

Author SHA1 Message Date
Lea Anthony
2e01710412 v2.0.0-alpha.63 2021-04-04 05:15:09 +10:00
Lea Anthony
1b0193161c Improvements for handling Info.plist 2021-04-04 05:14:16 +10:00
Lea Anthony
1b377fb575 Support enter as alias for return 2021-04-03 16:52:54 +11:00
Lea Anthony
1203ae64b8 v2.0.0-alpha.62 2021-03-31 15:44:28 +11:00
Lea Anthony
26ed8002b9 Menu bar font fix 2021-03-31 15:43:28 +11:00
Lea Anthony
cf23bffc67 v2.0.0-alpha.61 2021-03-30 22:22:48 +11:00
Lea Anthony
d70f6fffe7 Remove autorelease of colour. Fixes high sierra. 2021-03-30 22:21:49 +11:00
Lea Anthony
54c99fc386 Move some methods to main thread 2021-03-29 20:45:50 +11:00
Lea Anthony
86c1ea5e6a Initial support for compression 2021-03-27 20:59:14 +11:00
Lea Anthony
b394c1914c v2.0.0-alpha.60 2021-03-26 20:54:42 +11:00
Lea Anthony
91c2ddf155 Allow styling of menu items that have submenus 2021-03-26 20:54:19 +11:00
Lea Anthony
712ad96d2a v2.0.0-alpha.59 2021-03-26 18:30:00 +11:00
Lea Anthony
86b4a4f2f5 Don't clean directory when doing universal builds 2021-03-26 18:29:28 +11:00
Lea Anthony
4b9786abc9 v2.0.0-alpha.58 2021-03-26 18:13:10 +11:00
Lea Anthony
fd96ebc050 Better verbose output 2021-03-26 18:12:42 +11:00
Lea Anthony
939e0f5975 Use default broken image for invalid images 2021-03-26 17:51:49 +11:00
Lea Anthony
6a7a288a0f Limit StartsAtLogin to app bundles 2021-03-26 16:52:59 +11:00
Lea Anthony
0564d0aa98 v2.0.0-alpha.57 2021-03-26 16:32:10 +11:00
Lea Anthony
3a136a73ca Add package for mac functions 2021-03-26 15:57:30 +11:00
Lea Anthony
50c219307f Add clean flag 2021-03-26 14:10:25 +11:00
Lea Anthony
de3038b302 v2.0.0-alpha.56 2021-03-25 21:13:40 +11:00
Lea Anthony
6eb4b0a419 Fix packaging universal builds 2021-03-25 21:12:29 +11:00
Lea Anthony
41d2158375 Support building arm64 & universal binaries. 2021-03-25 21:12:29 +11:00
Lea Anthony
5d7f57e80b Initial ARM support! 2021-03-25 21:12:29 +11:00
Lea Anthony
4ce3e1d1bf v2.0.0-alpha.55 2021-03-21 20:59:23 +11:00
Lea Anthony
e5f2746810 Better font name support 2021-03-21 20:57:41 +11:00
Lea Anthony
92ebf506dd Get app compiling 2021-03-20 18:32:09 +11:00
Lea Anthony
9ab06152c5 Refactor doctor for windows 2021-03-20 16:27:35 +11:00
Lea Anthony
bf36b6a59d Doctor working for Windows 2021-03-20 16:24:02 +11:00
Lea Anthony
4b9f6c4fb1 Update CONTRIBUTORS.md 2021-03-20 15:55:14 +11:00
Amaury Tobias Quiroz
b1a42c8dea fix: support rel="modulepreload" on assetbundle as javascript (#621)
In some bundler enviroments like vite the output on index.html uses link
tag with rel="modulepreload" to load javascript, add support to handle
this files on assetbundle, more info
https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/modulepreload

fix #620

Co-authored-by: amaury-tobias <amaury.tobiasqr@gmai.com>
2021-03-20 15:51:57 +11:00
Travis McLane
cbd98b5a1a update scripts/build.sh to run test only on v1 2021-03-20 15:01:09 +11:00
Lea Anthony
c8e0aea69c v2.0.0-alpha.54 2021-03-19 09:17:18 +11:00
Lea Anthony
7c0b236eb0 Fix for hiding window on close 2021-03-19 09:14:49 +11:00
Lea Anthony
16debbd109 v2.0.0-alpha.53 2021-03-18 20:55:26 +11:00
Lea Anthony
39bfa5d910 Support disabling tray menu. Fix font sizing. Tooltip in tray menu support. 2021-03-18 20:54:53 +11:00
Lea Anthony
6424579a9e v2.0.0-alpha.52 2021-03-17 23:30:42 +11:00
Lea Anthony
a962ae6f63 Support rich text in Tray labels 2021-03-17 23:30:08 +11:00
Lea Anthony
c7dee158ba tray menu Icon->Image. Support template images. 2021-03-17 22:24:09 +11:00
Lea Anthony
237d25089d Update CLI banner 2021-03-17 21:59:31 +11:00
Lea Anthony
265328d648 v2.0.0-alpha.51 2021-03-15 06:13:51 +11:00
Lea Anthony
6f40e85a6e Support startup urls when app not running 2021-03-15 06:13:00 +11:00
Lea Anthony
96d8509da3 v2.0.0-alpha.50 2021-03-13 15:17:35 +11:00
Lea Anthony
598445ab0f v2.0.0-alpha.49 2021-03-13 15:00:19 +11:00
Lea Anthony
24788e2fd3 Fix template images 2021-03-13 14:59:12 +11:00
Lea Anthony
bbf4dde43f Support upserting environment variables 2021-03-12 23:41:13 +11:00
Lea Anthony
0afd27ab45 Add FileLogger option 2021-03-12 23:40:35 +11:00
Lea Anthony
d498423ec2 v2.0.0-alpha.48 2021-03-09 22:28:07 +11:00
Mat Ryer
66ce84973c fixes for machines running TouchBar addresses https://github.com/matryer/xbar/issues/610 2021-03-09 11:24:46 +00:00
Lea Anthony
55e6a0f312 v2.0.0-alpha.47 2021-03-07 16:24:20 +11:00
Lea Anthony
81e83fdf18 Ensure modifiers are lowercase when parsing 2021-03-07 16:21:30 +11:00
Lea Anthony
f9b79d24f8 Guard against nil url messages 2021-03-06 15:51:06 +11:00
Lea Anthony
0599a47bfe v2.0.0-alpha.46 2021-03-06 15:43:44 +11:00
Lea Anthony
817c55d318 Support base64 images in tray 2021-03-06 15:43:11 +11:00
Lea Anthony
14146c8c0c v2.0.0-alpha.45 2021-03-06 00:29:00 +11:00
Lea Anthony
18adac20d4 Tray menu open/close events 2021-03-06 00:25:34 +11:00
Lea Anthony
eb4bff89da v2.0.0-alpha.44 2021-03-04 06:18:31 +11:00
Lea Anthony
c66dc777f3 Remove debug logging 2021-03-04 06:18:11 +11:00
Lea Anthony
9003462457 v2.0.0-alpha.43 2021-03-04 06:09:17 +11:00
Lea Anthony
e124f0a220 Support Alternative menu items 2021-03-04 06:07:45 +11:00
44 changed files with 1267 additions and 756 deletions

View File

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

View File

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

View File

@@ -3,8 +3,10 @@ package build
import (
"fmt"
"io"
"os"
"runtime"
"strings"
"text/tabwriter"
"time"
"github.com/leaanthony/clir"
@@ -23,8 +25,8 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
command := app.NewSubCommand("build", "Builds the application")
// Setup target type flag
description := "Type of application to build. Valid types: " + validTargetTypes.Join(",")
command.StringFlag("t", description, &outputType)
//description := "Type of application to build. Valid types: " + validTargetTypes.Join(",")
//command.StringFlag("t", description, &outputType)
// Setup production flag
production := false
@@ -37,26 +39,37 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
compilerCommand := "go"
command.StringFlag("compiler", "Use a different go compiler to build, eg go1.15beta1", &compilerCommand)
compress := false
command.BoolFlag("compress", "Compress final binary", &compress)
// Setup Platform flag
platform := runtime.GOOS
command.StringFlag("platform", "Platform to target", &platform)
// Quiet Build
quiet := false
command.BoolFlag("q", "Suppress output to console", &quiet)
// Verbosity
verbosity := 1
command.IntFlag("v", "Verbosity level (0 - silent, 1 - default, 2 - verbose)", &verbosity)
// ldflags to pass to `go`
ldflags := ""
command.StringFlag("ldflags", "optional ldflags", &ldflags)
// Log to file
logFile := ""
command.StringFlag("l", "Log to file", &logFile)
//logFile := ""
//command.StringFlag("l", "Log to file", &logFile)
// Retain assets
keepAssets := false
command.BoolFlag("k", "Keep generated assets", &keepAssets)
// Retain assets
outputFilename := ""
command.StringFlag("o", "Output filename", &outputFilename)
// 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)
@@ -64,6 +77,8 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
command.Action(func() error {
quiet := verbosity == 0
// Create logger
logger := clilogger.New(w)
logger.Mute(quiet)
@@ -82,29 +97,81 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
return fmt.Errorf("must use `-package` flag when using `-sign`")
}
task := fmt.Sprintf("Building %s Application", strings.Title(outputType))
logger.Println(task)
logger.Println(strings.Repeat("-", len(task)))
// Setup mode
mode := build.Debug
if production {
mode = build.Production
}
// Check platform
validPlatformArch := slicer.String([]string{
"darwin",
"darwin/amd64",
"darwin/arm64",
"darwin/universal",
//"linux/amd64",
//"linux/arm-7",
//"windows/amd64",
})
if !validPlatformArch.Contains(platform) {
return fmt.Errorf("platform %s is not supported", platform)
}
if compress && platform == "darwin/universal" {
println("Warning: compress flag unsupported for universal binaries. Ignoring.")
compress = false
}
// Create BuildOptions
buildOptions := &build.Options{
Logger: logger,
OutputType: outputType,
Mode: mode,
Pack: pack,
Platform: platform,
LDFlags: ldflags,
Compiler: compilerCommand,
KeepAssets: keepAssets,
AppleIdentity: appleIdentity,
Logger: logger,
OutputType: outputType,
OutputFile: outputFilename,
CleanBuildDirectory: cleanBuildDirectory,
Mode: mode,
Pack: pack,
LDFlags: ldflags,
Compiler: compilerCommand,
KeepAssets: keepAssets,
AppleIdentity: appleIdentity,
Verbosity: verbosity,
Compress: compress,
}
// Calculate platform and arch
platformSplit := strings.Split(platform, "/")
buildOptions.Platform = platformSplit[0]
buildOptions.Arch = runtime.GOARCH
if len(platformSplit) == 2 {
buildOptions.Arch = platformSplit[1]
}
// Start a new tabwriter
w := new(tabwriter.Writer)
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
buildModeText := "debug"
if production {
buildModeText = "production"
}
// Write out the system information
fmt.Fprintf(w, "\n")
fmt.Fprintf(w, "App Type: \t%s\n", buildOptions.OutputType)
fmt.Fprintf(w, "Platform: \t%s\n", buildOptions.Platform)
fmt.Fprintf(w, "Arch: \t%s\n", buildOptions.Arch)
fmt.Fprintf(w, "Compiler: \t%s\n", buildOptions.Compiler)
fmt.Fprintf(w, "Compress: \t%t\n", buildOptions.Compress)
fmt.Fprintf(w, "Build Mode: \t%s\n", buildModeText)
fmt.Fprintf(w, "Package: \t%t\n", buildOptions.Pack)
fmt.Fprintf(w, "Clean Build Dir: \t%t\n", buildOptions.CleanBuildDirectory)
fmt.Fprintf(w, "KeepAssets: \t%t\n", buildOptions.KeepAssets)
fmt.Fprintf(w, "LDFlags: \t\"%s\"\n", buildOptions.LDFlags)
if len(buildOptions.OutputFile) > 0 {
fmt.Fprintf(w, "Output File: \t%s\n", buildOptions.OutputFile)
}
fmt.Fprintf(w, "\n")
w.Flush()
return doBuild(buildOptions)
})
}

View File

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

View File

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

View File

@@ -5,10 +5,6 @@
#ifndef COMMON_H
#define COMMON_H
#define OBJC_OLD_DISPATCH_PROTOTYPES 1
#include <objc/objc-runtime.h>
#include <CoreGraphics/CoreGraphics.h>
#include <stdio.h>
#include <stdarg.h>
#include "string.h"

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,13 @@
// Macros to make it slightly more sane
#define msg objc_msgSend
#define msg_reg ((id(*)(id, SEL))objc_msgSend)
#define msg_id ((id(*)(id, SEL, id))objc_msgSend)
#define msg_id_id ((id(*)(id, SEL, id, id))objc_msgSend)
#define msg_bool ((id(*)(id, SEL, BOOL))objc_msgSend)
#define msg_int ((id(*)(id, SEL, int))objc_msgSend)
#define msg_uint ((id(*)(id, SEL, unsigned int))objc_msgSend)
#define msg_float ((id(*)(id, SEL, float))objc_msgSend)
#define kInternetEventClass 'GURL'
#define kAEGetURL 'GURL'
#define keyDirectObject '----'
@@ -21,18 +27,27 @@
#define c(str) (id)objc_getClass(str)
#define s(str) sel_registerName(str)
#define u(str) sel_getUid(str)
#define str(input) msg(c("NSString"), s("stringWithUTF8String:"), input)
#define strunicode(input) msg(c("NSString"), s("stringWithFormat:"), str("%C"), (unsigned short)input)
#define cstr(input) (const char *)msg(input, s("UTF8String"))
#define url(input) msg(c("NSURL"), s("fileURLWithPath:"), str(input))
#define ALLOC(classname) msg(c(classname), s("alloc"))
#define ALLOC_INIT(classname) msg(msg(c(classname), s("alloc")), s("init"))
#define str(input) ((id(*)(id, SEL, const char *))objc_msgSend)(c("NSString"), s("stringWithUTF8String:"), input)
#define strunicode(input) ((id(*)(id, SEL, id, unsigned short))objc_msgSend)(c("NSString"), s("stringWithFormat:"), str("%C"), (unsigned short)input)
#define cstr(input) (const char *)msg_reg(input, s("UTF8String"))
#define url(input) msg_id(c("NSURL"), s("fileURLWithPath:"), str(input))
#define ALLOC(classname) msg_reg(c(classname), s("alloc"))
#define ALLOC_INIT(classname) msg_reg(msg_reg(c(classname), s("alloc")), s("init"))
#if defined (__aarch64__)
#define GET_FRAME(receiver) ((CGRect(*)(id, SEL))objc_msgSend)(receiver, s("frame"))
#define GET_BOUNDS(receiver) ((CGRect(*)(id, SEL))objc_msgSend)(receiver, s("bounds"))
#endif
#if defined (__x86_64__)
#define GET_FRAME(receiver) ((CGRect(*)(id, SEL))objc_msgSend_stret)(receiver, s("frame"))
#define GET_BOUNDS(receiver) ((CGRect(*)(id, SEL))objc_msgSend_stret)(receiver, s("bounds"))
#define GET_BACKINGSCALEFACTOR(receiver) ((CGFloat(*)(id, SEL))msg)(receiver, s("backingScaleFactor"))
#endif
#define GET_BACKINGSCALEFACTOR(receiver) ((CGFloat(*)(id, SEL))objc_msgSend)(receiver, s("backingScaleFactor"))
#define ON_MAIN_THREAD(str) dispatch( ^{ str; } )
#define MAIN_WINDOW_CALL(str) msg(app->mainWindow, s((str)))
#define MAIN_WINDOW_CALL(str) msg_reg(app->mainWindow, s((str)))
#define NSBackingStoreBuffered 2
@@ -102,6 +117,8 @@
#define NSAlertSecondButtonReturn 1001
#define NSAlertThirdButtonReturn 1002
#define BrokenImage "iVBORw0KGgoAAAANSUhEUgAAABAAAAASCAMAAABl5a5YAAABj1BMVEWopan///+koqSWk5P9/v3///////////+AgACMiovz8/PB0fG9z+3i4+WysbGBfX1Erh80rACLiYqBxolEsDhHlDEbqQDDx+CNho7W1tj4+/bw+O3P5Mn4/f/W1tbK6sX////b2dn////////////8/Pz6+vro6Ojj4+P////G1PL////EzNydmp2cmZnd3eDF1PHs8v/o8P/Q3vrS3vfE0vCdmpqZkpr19/3N2vXI1vPH1fOgnqDg6frP3PbCytvHx8irqq6HhIZtuGtjnlZetU1Xs0NWskBNsi7w9v/d6P7w9P3S4Pzr8Pvl7PrY5PrU4PjQ3fjD1Ozo6Om30NjGzNi7ubm34K+UxKmbnaWXlJeUjpSPi4tppF1TtjxSsTf2+f7L2PTr7e3H2+3V7+q+0uXg4OPg4eLR1uG7z+Hg4ODGzODV2N7V1trP5dmxzs65vcfFxMWq0cKxxr+/vr+0s7apxbWaxrCv2qao05+dlp2Uuo2Dn4F8vIB6xnyAoHmAym9zqGpctENLryNFsgoblJpnAAAAKnRSTlP+hP7+5ZRmYgL+/f39/f39/f38/Pz8/Pv69+7j083My8GocnBPTTMWEgjxeITOAAABEklEQVQY0y3KZXuCYBiG4ceYuu7u3nyVAaKOMBBQ7O5Yd3f3fvheDnd9u8/jBkGwNxP6sjOWVQvY/ftrzfT6bd3yEhCnYZqiaYoKiwX/gXkFiHySTcUTLJMsZ9v8nQvgssWYOEKedKpcOO6CUXD5IlGEY5hLUbyDAAZ6HRf1bnkoavOsFQibg+Q4nuNYL+ON5PHD5nBaraRVyxnzGf6BJzUi2QQCQgMyk8tleL7dg1owpJ17D5IkvV100EingeOopPyo6vfAuXF+9hbDTknZCIaUoeK4efKwG4iT6xDewd7imGlid7gGwv37b6Oh9jwaTdOf/Tc1qH7UZVmuP6G5qZfBr9cAGNy4KiDd4tXIs7tS+QO9aUKvPAIKuQAAAABJRU5ErkJggg=="
struct Application;
int releaseNSObject(void *const context, struct hashmap_element_s *const e);
void TitlebarAppearsTransparent(struct Application* app);
@@ -124,4 +141,6 @@ void* lookupStringConstant(id constantName);
void HasURLHandlers(struct Application* app);
id createImageFromBase64Data(const char *data, bool isTemplateImage);
#endif

View File

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

View File

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

View File

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

View File

@@ -90,7 +90,7 @@ void DeleteMenu(Menu *menu) {
// Free nsmenu if we have it
if ( menu->menu != NULL ) {
msg(menu->menu, s("release"));
msg_reg(menu->menu, s("release"));
}
free(menu);
@@ -120,17 +120,17 @@ const char* createMenuClickedMessage(const char *menuItemID, const char *data, e
// Callback for text menu items
void menuItemCallback(id self, SEL cmd, id sender) {
MenuItemCallbackData *callbackData = (MenuItemCallbackData *)msg(msg(sender, s("representedObject")), s("pointerValue"));
MenuItemCallbackData *callbackData = (MenuItemCallbackData *)msg_reg(msg_reg(sender, s("representedObject")), s("pointerValue"));
const char *message;
// Update checkbox / radio item
if( callbackData->menuItemType == Checkbox) {
// Toggle state
bool state = msg(callbackData->menuItem, s("state"));
msg(callbackData->menuItem, s("setState:"), (state? NSControlStateValueOff : NSControlStateValueOn));
bool state = msg_reg(callbackData->menuItem, s("state"));
msg_int(callbackData->menuItem, s("setState:"), (state? NSControlStateValueOff : NSControlStateValueOn));
} else if( callbackData->menuItemType == Radio ) {
// Check the menu items' current state
bool selected = msg(callbackData->menuItem, s("state"));
bool selected = (bool)msg_reg(callbackData->menuItem, s("state"));
// If it's already selected, exit early
if (selected) return;
@@ -142,13 +142,13 @@ void menuItemCallback(id self, SEL cmd, id sender) {
id thisMember = members[0];
int count = 0;
while(thisMember != NULL) {
msg(thisMember, s("setState:"), NSControlStateValueOff);
msg_int(thisMember, s("setState:"), NSControlStateValueOff);
count = count + 1;
thisMember = members[count];
}
// check the selected menu item
msg(callbackData->menuItem, s("setState:"), NSControlStateValueOn);
msg_int(callbackData->menuItem, s("setState:"), NSControlStateValueOn);
}
const char *menuID = callbackData->menuID;
@@ -189,6 +189,9 @@ id processAcceleratorKey(const char *key) {
if( STREQ(key, "return") ) {
return strunicode(0x000d);
}
if( STREQ(key, "enter") ) {
return strunicode(0x000d);
}
if( STREQ(key, "escape") ) {
return strunicode(0x001b);
}
@@ -345,61 +348,61 @@ id processAcceleratorKey(const char *key) {
void addSeparator(id menu) {
id item = msg(c("NSMenuItem"), s("separatorItem"));
msg(menu, s("addItem:"), item);
id item = msg_reg(c("NSMenuItem"), s("separatorItem"));
msg_id(menu, s("addItem:"), item);
}
id createMenuItemNoAutorelease( id title, const char *action, const char *key) {
id item = ALLOC("NSMenuItem");
msg(item, s("initWithTitle:action:keyEquivalent:"), title, s(action), str(key));
((id(*)(id, SEL, id, SEL, id))objc_msgSend)(item, s("initWithTitle:action:keyEquivalent:"), title, s(action), str(key));
return item;
}
id createMenuItem(id title, const char *action, const char *key) {
id item = ALLOC("NSMenuItem");
msg(item, s("initWithTitle:action:keyEquivalent:"), title, s(action), str(key));
msg(item, s("autorelease"));
((id(*)(id, SEL, id, SEL, id))objc_msgSend)(item, s("initWithTitle:action:keyEquivalent:"), title, s(action), str(key));
msg_reg(item, s("autorelease"));
return item;
}
id addMenuItem(id menu, const char *title, const char *action, const char *key, bool disabled) {
id item = createMenuItem(str(title), action, key);
msg(item, s("setEnabled:"), !disabled);
msg(menu, s("addItem:"), item);
msg_bool(item, s("setEnabled:"), !disabled);
msg_id(menu, s("addItem:"), item);
return item;
}
id createMenu(id title) {
id menu = ALLOC("NSMenu");
msg(menu, s("initWithTitle:"), title);
msg(menu, s("setAutoenablesItems:"), NO);
msg_id(menu, s("initWithTitle:"), title);
msg_bool(menu, s("setAutoenablesItems:"), NO);
// msg(menu, s("autorelease"));
return menu;
}
void createDefaultAppMenu(id parentMenu) {
// App Menu
id appName = msg(msg(c("NSProcessInfo"), s("processInfo")), s("processName"));
id appName = msg_reg(msg_reg(c("NSProcessInfo"), s("processInfo")), s("processName"));
id appMenuItem = createMenuItemNoAutorelease(appName, NULL, "");
id appMenu = createMenu(appName);
msg(appMenuItem, s("setSubmenu:"), appMenu);
msg(parentMenu, s("addItem:"), appMenuItem);
msg_id(appMenuItem, s("setSubmenu:"), appMenu);
msg_id(parentMenu, s("addItem:"), appMenuItem);
id title = msg(str("Hide "), s("stringByAppendingString:"), appName);
id title = msg_id(str("Hide "), s("stringByAppendingString:"), appName);
id item = createMenuItem(title, "hide:", "h");
msg(appMenu, s("addItem:"), item);
msg_id(appMenu, s("addItem:"), item);
id hideOthers = addMenuItem(appMenu, "Hide Others", "hideOtherApplications:", "h", FALSE);
msg(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand));
msg_int(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand));
addMenuItem(appMenu, "Show All", "unhideAllApplications:", "", FALSE);
addSeparator(appMenu);
title = msg(str("Quit "), s("stringByAppendingString:"), appName);
title = msg_id(str("Quit "), s("stringByAppendingString:"), appName);
item = createMenuItem(title, "terminate:", "q");
msg(appMenu, s("addItem:"), item);
msg_id(appMenu, s("addItem:"), item);
}
void createDefaultEditMenu(id parentMenu) {
@@ -407,8 +410,8 @@ void createDefaultEditMenu(id parentMenu) {
id editMenuItem = createMenuItemNoAutorelease(str("Edit"), NULL, "");
id editMenu = createMenu(str("Edit"));
msg(editMenuItem, s("setSubmenu:"), editMenu);
msg(parentMenu, s("addItem:"), editMenuItem);
msg_id(editMenuItem, s("setSubmenu:"), editMenu);
msg_id(parentMenu, s("addItem:"), editMenuItem);
addMenuItem(editMenu, "Undo", "undo:", "z", FALSE);
addMenuItem(editMenu, "Redo", "redo:", "y", FALSE);
@@ -436,7 +439,7 @@ void processMenuRole(Menu *menu, id parentMenu, JsonNode *item) {
}
if ( STREQ(roleName, "hideothers")) {
id hideOthers = addMenuItem(parentMenu, "Hide Others", "hideOtherApplications:", "h", FALSE);
msg(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand));
msg_int(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand));
return;
}
if ( STREQ(roleName, "unhide")) {
@@ -473,7 +476,7 @@ void processMenuRole(Menu *menu, id parentMenu, JsonNode *item) {
}
if( STREQ(roleName, "pasteandmatchstyle")) {
id pasteandmatchstyle = addMenuItem(parentMenu, "Paste and Match Style", "pasteandmatchstyle:", "v", FALSE);
msg(pasteandmatchstyle, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagShift | NSEventModifierFlagCommand));
msg_int(pasteandmatchstyle, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagShift | NSEventModifierFlagCommand));
}
if ( STREQ(roleName, "selectall")) {
addMenuItem(parentMenu, "Select All", "selectAll:", "a", FALSE);
@@ -540,18 +543,18 @@ id processRadioMenuItem(Menu *menu, id parentmenu, const char *title, const char
// Create a MenuItemCallbackData
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, item, menuid, Radio);
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), callback);
msg(item, s("setRepresentedObject:"), wrappedId);
id wrappedId = msg_id(c("NSValue"), s("valueWithPointer:"), (id)callback);
msg_id(item, s("setRepresentedObject:"), wrappedId);
id key = processAcceleratorKey(acceleratorkey);
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuItemCallback:"), key);
((id(*)(id, SEL, id, SEL, id))objc_msgSend)(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuItemCallback:"), key);
msg(item, s("setEnabled:"), !disabled);
msg(item, s("autorelease"));
msg(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
msg_bool(item, s("setEnabled:"), !disabled);
msg_reg(item, s("autorelease"));
msg_int(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
msg(parentmenu, s("addItem:"), item);
msg_id(parentmenu, s("addItem:"), item);
return item;
}
@@ -566,73 +569,37 @@ id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const c
// Create a MenuItemCallbackData
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, item, menuid, Checkbox);
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), callback);
msg(item, s("setRepresentedObject:"), wrappedId);
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuItemCallback:"), str(key));
msg(item, s("setEnabled:"), !disabled);
msg(item, s("autorelease"));
msg(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
msg(parentmenu, s("addItem:"), item);
id wrappedId = msg_id(c("NSValue"), s("valueWithPointer:"), (id)callback);
msg_id(item, s("setRepresentedObject:"), wrappedId);
((id(*)(id, SEL, id, SEL, id))objc_msgSend)(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuItemCallback:"), str(key));
msg_bool(item, s("setEnabled:"), !disabled);
msg_reg(item, s("autorelease"));
msg_int(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
msg_id(parentmenu, s("addItem:"), item);
return item;
}
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, const char* tooltip, const char* image, const char* fontName, int fontSize, const char* RGBA, bool templateImage) {
id item = ALLOC("NSMenuItem");
id createAttributedString(const char* title, const char* fontName, int fontSize, const char* RGBA) {
// 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
// Create new Dictionary
id dictionary = ALLOC_INIT("NSMutableDictionary");
// Process font
id font;
CGFloat fontSizeFloat = (CGFloat)fontSize;
// Check if valid
id fontNameAsNSString = str(fontName);
id fontsOnSystem = msg(msg(c("NSFontManager"), s("sharedFontManager")), s("availableFonts"));
bool valid = msg(fontsOnSystem, s("containsObject:"), fontNameAsNSString);
if( valid ) {
font = msg(c("NSFont"), s("fontWithName:size:"), fontNameAsNSString, fontSizeFloat);
} else {
bool supportsMonospacedDigitSystemFont = (bool) msg(c("NSFont"), s("respondsToSelector:"), s("monospacedDigitSystemFontOfSize:weight:"));
if( supportsMonospacedDigitSystemFont ) {
font = msg(c("NSFont"), s("monospacedDigitSystemFontOfSize:weight:"), fontSizeFloat, NSFontWeightRegular);
} else {
font = msg(c("NSFont"), s("menuFontOfSize:"), fontSizeFloat);
// Use default font
id font = ((id(*)(id, SEL, CGFloat))objc_msgSend)(c("NSFont"), s("menuBarFontOfSize:"), fontSizeFloat);
// Check user supplied font
if( STR_HAS_CHARS(fontName) ) {
id fontNameAsNSString = str(fontName);
id userFont = ((id(*)(id, SEL, id, CGFloat))objc_msgSend)(c("NSFont"), s("fontWithName:size:"), fontNameAsNSString, fontSizeFloat);
if( userFont != NULL ) {
font = userFont;
}
}
// Add font to dictionary
msg(dictionary, s("setObject:forKey:"), font, lookupStringConstant(str("NSFontAttributeName")));
// Add offset to dictionary
id offset = msg(c("NSNumber"), s("numberWithFloat:"), 0.0);
msg(dictionary, s("setObject:forKey:"), offset, lookupStringConstant(str("NSBaselineOffsetAttributeName")));
id fan = lookupStringConstant(str("NSFontAttributeName"));
msg_id_id(dictionary, s("setObject:forKey:"), font, fan);
// RGBA
if( RGBA != NULL && strlen(RGBA) > 0) {
@@ -642,32 +609,70 @@ id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char
r = g = b = a = 255;
int count = sscanf(RGBA, "#%02hx%02hx%02hx%02hx", &r, &g, &b, &a);
if (count > 0) {
id colour = msg(c("NSColor"), s("colorWithCalibratedRed:green:blue:alpha:"),
(float)r / 255.0,
(float)g / 255.0,
(float)b / 255.0,
(float)a / 255.0);
msg(dictionary, s("setObject:forKey:"), colour, lookupStringConstant(str("NSForegroundColorAttributeName")));
msg(colour, s("release"));
id colour = ((id(*)(id, SEL, CGFloat, CGFloat, CGFloat, CGFloat))objc_msgSend)(c("NSColor"), s("colorWithCalibratedRed:green:blue:alpha:"),
(CGFloat)r / (CGFloat)255.0,
(CGFloat)g / (CGFloat)255.0,
(CGFloat)b / (CGFloat)255.0,
(CGFloat)a / (CGFloat)255.0);
id NSForegroundColorAttributeName = lookupStringConstant(str("NSForegroundColorAttributeName"));
msg_id_id(dictionary, s("setObject:forKey:"), colour, NSForegroundColorAttributeName);
}
}
id attributedString = ALLOC("NSMutableAttributedString");
msg(attributedString, s("initWithString:attributes:"), str(title), dictionary);
msg(dictionary, s("release"));
msg_id_id(attributedString, s("initWithString:attributes:"), str(title), dictionary);
msg_reg(attributedString, s("autorelease"));
msg_reg(dictionary, s("autorelease"));
return attributedString;
}
msg(item, s("setAttributedTitle:"), attributedString);
msg(attributedString, s("autorelease"));
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, const char* tooltip, const char* image, const char* fontName, int fontSize, const char* RGBA, bool templateImage, bool alternate) {
id item = ALLOC("NSMenuItem");
msg(item, s("setEnabled:"), !disabled);
msg(item, s("autorelease"));
// Create a MenuItemCallbackData
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, item, menuid, Text);
id wrappedId = msg_id(c("NSValue"), s("valueWithPointer:"), (id)callback);
msg_id(item, s("setRepresentedObject:"), wrappedId);
if( !alternate ) {
id key = processAcceleratorKey(acceleratorkey);
((id(*)(id, SEL, id, SEL, id))objc_msgSend)(item, s("initWithTitle:action:keyEquivalent:"), str(title),
s("menuItemCallback:"), key);
} else {
((id(*)(id, SEL, id, SEL, id))objc_msgSend)(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuItemCallback:"), str(""));
}
if( tooltip != NULL ) {
msg_id(item, s("setToolTip:"), str(tooltip));
}
// Process image
if( image != NULL && strlen(image) > 0) {
id nsimage = createImageFromBase64Data(image, templateImage);
msg_id(item, s("setImage:"), nsimage);
}
id attributedString = createAttributedString(title, fontName, fontSize, RGBA);
msg_id(item, s("setAttributedTitle:"), attributedString);
//msg_id(item, s("setTitle:"), str(title));
msg_bool(item, s("setEnabled:"), !disabled);
msg_reg(item, s("autorelease"));
// Process modifiers
if( modifiers != NULL ) {
if( modifiers != NULL && !alternate) {
unsigned long modifierFlags = parseModifiers(modifiers);
msg(item, s("setKeyEquivalentModifierMask:"), modifierFlags);
((id(*)(id, SEL, unsigned long))objc_msgSend)(item, s("setKeyEquivalentModifierMask:"), modifierFlags);
}
msg(parentMenu, s("addItem:"), item);
// alternate
if( alternate ) {
msg_bool(item, s("setAlternate:"), true);
msg_int(item, s("setKeyEquivalentModifierMask:"), NSEventModifierFlagOption);
}
msg_id(parentMenu, s("addItem:"), item);
return item;
}
@@ -688,38 +693,6 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
return;
}
// Check if this is a submenu
JsonNode *submenu = json_find_member(item, "SubMenu");
if( submenu != NULL ) {
// Get the label
JsonNode *menuNameNode = json_find_member(item, "Label");
const char *name = "";
if ( menuNameNode != NULL) {
name = menuNameNode->string_;
}
id thisMenuItem = createMenuItemNoAutorelease(str(name), NULL, "");
id thisMenu = createMenu(str(name));
msg(thisMenuItem, s("setSubmenu:"), thisMenu);
msg(parentMenu, s("addItem:"), thisMenuItem);
JsonNode *submenuItems = json_find_member(submenu, "Items");
// If we have no items, just return
if ( submenuItems == NULL ) {
return;
}
// Loop over submenu items
JsonNode *item;
json_foreach(item, submenuItems) {
// Get item label
processMenuItem(menu, thisMenu, item);
}
return;
}
// This is a user menu. Get the common data
// Get the label
const char *label = getJSONString(item, "Label");
@@ -727,6 +700,11 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
label = "(empty)";
}
// Is this an alternate menu item?
bool alternate = false;
getJSONBool(item, "MacAlternate", &alternate);
const char *menuid = getJSONString(item, "ID");
if ( menuid == NULL) {
menuid = "";
@@ -747,7 +725,7 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
bool templateImage = false;
getJSONBool(item, "MacTemplateImage", &templateImage);
int fontSize = 12;
int fontSize = 0;
getJSONInt(item, "FontSize", &fontSize);
// If we have an accelerator
@@ -780,9 +758,36 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
// Get the Type
JsonNode *type = json_find_member(item, "Type");
if( type != NULL ) {
if( STREQ(type->string_, "Text") || STREQ(type->string_, "Submenu")) {
id thisMenuItem = processTextMenuItem(menu, parentMenu, label, menuid, disabled, acceleratorkey, modifiers, tooltip, image, fontName, fontSize, RGBA, templateImage, alternate);
if( STREQ(type->string_, "Text")) {
processTextMenuItem(menu, parentMenu, label, menuid, disabled, acceleratorkey, modifiers, tooltip, image, fontName, fontSize, RGBA, templateImage);
// Check if this node has a submenu
JsonNode *submenu = json_find_member(item, "SubMenu");
if( submenu != NULL ) {
// Get the label
JsonNode *menuNameNode = json_find_member(item, "Label");
const char *name = "";
if ( menuNameNode != NULL) {
name = menuNameNode->string_;
}
id thisMenu = createMenu(str(name));
msg_id(thisMenuItem, s("setSubmenu:"), thisMenu);
JsonNode *submenuItems = json_find_member(submenu, "Items");
// If we have no items, just return
if ( submenuItems == NULL ) {
return;
}
// Loop over submenu items
JsonNode *item;
json_foreach(item, submenuItems) {
// Get item label
processMenuItem(menu, thisMenu, item);
}
}
}
else if ( STREQ(type->string_, "Separator")) {
addSeparator(parentMenu);
@@ -801,7 +806,6 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
processRadioMenuItem(menu, parentMenu, label, menuid, disabled, checked, "");
}
}
if ( modifiers != NULL ) {

View File

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

View File

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

View File

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

View File

@@ -118,7 +118,17 @@ void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON) {
// Check we have this menu
TrayMenu *menu = MustGetTrayMenuFromStore(store, ID);
UpdateTrayLabel(menu, Label);
const char *fontName = getJSONString(parsedUpdate, "FontName");
const char *RGBA = getJSONString(parsedUpdate, "RGBA");
int fontSize = 0;
getJSONInt(parsedUpdate, "FontSize", &fontSize);
const char *tooltip = getJSONString(parsedUpdate, "Tooltip");
bool disabled = false;
getJSONBool(parsedUpdate, "Disabled", &disabled);
UpdateTrayLabel(menu, Label, fontName, fontSize, RGBA, tooltip, disabled);
}

View File

@@ -5,6 +5,8 @@
#ifndef TRAYMENUSTORE_DARWIN_H
#define TRAYMENUSTORE_DARWIN_H
#include "traymenu_darwin.h"
#include <pthread.h>
typedef struct {
@@ -26,6 +28,8 @@ void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON);
void ShowTrayMenusInStore(TrayMenuStore* store);
void DeleteTrayMenuStore(TrayMenuStore* store);
TrayMenu* GetTrayMenuByID(TrayMenuStore* store, const char* menuID);
void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON);
void DeleteTrayMenuInStore(TrayMenuStore* store, const char* id);

View File

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

View File

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

View File

@@ -23,12 +23,19 @@ func generateTrayID() string {
}
type TrayMenu struct {
ID string
Label string
Icon string
menuItemMap *MenuItemMap
menu *menu.Menu
ProcessedMenu *WailsMenu
ID string
Label string
FontSize int
FontName string
Disabled bool
Tooltip string `json:",omitempty"`
Image string
MacTemplateImage bool
RGBA string
menuItemMap *MenuItemMap
menu *menu.Menu
ProcessedMenu *WailsMenu
trayMenu *menu.TrayMenu
}
func (t *TrayMenu) AsJSON() (string, error) {
@@ -42,10 +49,17 @@ func (t *TrayMenu) AsJSON() (string, error) {
func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu {
result := &TrayMenu{
Label: trayMenu.Label,
Icon: trayMenu.Icon,
menu: trayMenu.Menu,
menuItemMap: NewMenuItemMap(),
Label: trayMenu.Label,
FontName: trayMenu.FontName,
FontSize: trayMenu.FontSize,
Disabled: trayMenu.Disabled,
Tooltip: trayMenu.Tooltip,
Image: trayMenu.Image,
MacTemplateImage: trayMenu.MacTemplateImage,
menu: trayMenu.Menu,
RGBA: trayMenu.RGBA,
menuItemMap: NewMenuItemMap(),
trayMenu: trayMenu,
}
result.menuItemMap.AddMenu(trayMenu.Menu)
@@ -54,6 +68,28 @@ func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu {
return result
}
func (m *Manager) OnTrayMenuOpen(id string) {
trayMenu, ok := m.trayMenus[id]
if !ok {
return
}
if trayMenu.trayMenu.OnOpen == nil {
return
}
go trayMenu.trayMenu.OnOpen()
}
func (m *Manager) OnTrayMenuClose(id string) {
trayMenu, ok := m.trayMenus[id]
if !ok {
return
}
if trayMenu.trayMenu.OnClose == nil {
return
}
go trayMenu.trayMenu.OnClose()
}
func (m *Manager) AddTrayMenu(trayMenu *menu.TrayMenu) (string, error) {
newTrayMenu := NewTrayMenu(trayMenu)
@@ -113,13 +149,27 @@ func (m *Manager) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) (string, error) {
}
type LabelUpdate struct {
ID string
Label string
ID string
Label string
FontName string
FontSize int
RGBA string
Disabled bool
Tooltip string
Image string
MacTemplateImage bool
}
update := &LabelUpdate{
ID: trayID,
Label: trayMenu.Label,
ID: trayID,
Label: trayMenu.Label,
FontName: trayMenu.FontName,
FontSize: trayMenu.FontSize,
Disabled: trayMenu.Disabled,
Tooltip: trayMenu.Tooltip,
Image: trayMenu.Image,
MacTemplateImage: trayMenu.MacTemplateImage,
RGBA: trayMenu.RGBA,
}
data, err := json.Marshal(update)

View File

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

View File

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

View File

@@ -77,6 +77,12 @@ func (m *Menu) Start() error {
splitTopic := strings.Split(menuMessage.Topic(), ":")
menuMessageType := splitTopic[1]
switch menuMessageType {
case "ontrayopen":
trayID := menuMessage.Data().(string)
m.menuManager.OnTrayMenuOpen(trayID)
case "ontrayclose":
trayID := menuMessage.Data().(string)
m.menuManager.OnTrayMenuClose(trayID)
case "clicked":
if len(splitTopic) != 2 {
m.logger.Error("Received clicked message with invalid topic format. Expected 2 sections in topic, got %s", splitTopic)

View File

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

View File

@@ -68,6 +68,10 @@ func (u *URL) Start() error {
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":

View File

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

View File

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

View File

@@ -10,6 +10,8 @@ import (
"runtime"
"strings"
"github.com/pkg/errors"
"github.com/leaanthony/slicer"
"github.com/wailsapp/wails/v2/internal/assetdb"
"github.com/wailsapp/wails/v2/internal/fs"
@@ -19,6 +21,10 @@ import (
"github.com/wailsapp/wails/v2/pkg/clilogger"
)
const (
VERBOSE int = 2
)
// BaseBuilder is the common builder struct
type BaseBuilder struct {
filesToDelete slicer.StringSlicer
@@ -142,11 +148,25 @@ func (b *BaseBuilder) CleanUp() {
})
}
func (b *BaseBuilder) OutputFilename(options *Options) string {
outputFile := options.OutputFile
if outputFile == "" {
outputFile = b.projectData.OutputFilename
}
return outputFile
}
// CompileProject compiles the project
func (b *BaseBuilder) CompileProject(options *Options) error {
verbose := options.Verbosity == VERBOSE
// Run go mod tidy first
cmd := exec.Command(options.Compiler, "mod", "tidy")
if verbose {
println("")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
err := cmd.Run()
if err != nil {
return err
@@ -195,9 +215,11 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
// Get application build directory
appDir := options.BuildDirectory
err = cleanBuildDirectory(options)
if err != nil {
return err
if options.CleanBuildDirectory {
err = cleanBuildDirectory(options)
if err != nil {
return err
}
}
if options.LDFlags != "" {
@@ -206,10 +228,7 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
}
// Set up output filename
outputFile := options.OutputFile
if outputFile == "" {
outputFile = b.projectData.OutputFilename
}
outputFile := b.OutputFilename(options)
compiledBinary := filepath.Join(appDir, outputFile)
commands.Add("-o")
commands.Add(compiledBinary)
@@ -219,13 +238,14 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
// Create the command
cmd = exec.Command(options.Compiler, commands.AsSlice()...)
if verbose {
println(" Build command:", commands.Join(" "))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
// Set the directory
cmd.Dir = b.projectData.Path
// Set GO111MODULE environment variable
cmd.Env = append(os.Environ(), "GO111MODULE=on")
// Add CGO flags
// We use the project/build dir as a temporary place for our generated c headers
buildBaseDir, err := fs.RelativeToCwd("build")
@@ -233,7 +253,32 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
return err
}
cmd.Env = append(os.Environ(), fmt.Sprintf("CGO_CFLAGS=-I%s", buildBaseDir))
cmd.Env = os.Environ() // inherit env
// Use upsertEnv so we don't overwrite user's CGO_CFLAGS
cmd.Env = upsertEnv(cmd.Env, "CGO_CFLAGS", func(v string) string {
if v != "" {
v += " "
}
v += "-I" + buildBaseDir
return v
})
cmd.Env = upsertEnv(cmd.Env, "GOOS", func(v string) string {
return options.Platform
})
cmd.Env = upsertEnv(cmd.Env, "GOARCH", func(v string) string {
return options.Arch
})
cmd.Env = upsertEnv(cmd.Env, "CGO_ENABLED", func(v string) string {
return "1"
})
if verbose {
println(" Environment:", strings.Join(cmd.Env, " "))
}
// Setup buffers
var stdo, stde bytes.Buffer
@@ -248,16 +293,37 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
return fmt.Errorf("%s\n%s", err, string(stde.Bytes()))
}
if !options.Compress {
return nil
}
// Do we have upx installed?
if !shell.CommandExists("upx") {
println("Warning: Cannot compress binary: upx not found")
return nil
}
if verbose {
println(" Compressing with:", "upx", "--best", "--no-color", "--no-progress", options.CompiledBinary)
}
output, err := exec.Command(options.BuildDirectory, "upx", "--best", "--no-color", "--no-progress", options.CompiledBinary).Output()
if err != nil {
return errors.Wrap(err, "Error during compression:")
}
if verbose {
println(output)
}
return nil
}
// NpmInstall runs "npm install" in the given directory
func (b *BaseBuilder) NpmInstall(sourceDir string) error {
return b.NpmInstallUsingCommand(sourceDir, "npm install")
func (b *BaseBuilder) NpmInstall(sourceDir string, verbose bool) error {
return b.NpmInstallUsingCommand(sourceDir, "npm install", verbose)
}
// NpmInstallUsingCommand runs the given install command in the specified npm project directory
func (b *BaseBuilder) NpmInstallUsingCommand(sourceDir string, installCommand string) error {
func (b *BaseBuilder) NpmInstallUsingCommand(sourceDir string, installCommand string, verbose bool) error {
packageJSON := filepath.Join(sourceDir, "package.json")
@@ -299,7 +365,7 @@ func (b *BaseBuilder) NpmInstallUsingCommand(sourceDir string, installCommand st
// Split up the InstallCommand and execute it
cmd := strings.Split(installCommand, " ")
stdout, stderr, err := shell.RunCommand(sourceDir, cmd[0], cmd[1:]...)
if err != nil {
if verbose || err != nil {
for _, l := range strings.Split(stdout, "\n") {
fmt.Printf(" %s\n", l)
}
@@ -348,31 +414,40 @@ func (b *BaseBuilder) NpmRunWithEnvironment(projectDir, buildTarget string, verb
func (b *BaseBuilder) BuildFrontend(outputLogger *clilogger.CLILogger) error {
// TODO: Fix this up from the CLI
verbose := false
verbose := b.options.Verbosity == VERBOSE
frontendDir := filepath.Join(b.projectData.Path, "frontend")
// Check there is an 'InstallCommand' provided in wails.json
if b.projectData.InstallCommand == "" {
// No - don't install
outputLogger.Println(" - No Install command. Skipping.")
outputLogger.Println("No Install command. Skipping.")
} else {
// Do install if needed
outputLogger.Println(" - Installing dependencies...")
if err := b.NpmInstallUsingCommand(frontendDir, b.projectData.InstallCommand); err != nil {
outputLogger.Print("Installing frontend dependencies: ")
if verbose {
outputLogger.Println("")
outputLogger.Println(" Install command: '" + b.projectData.InstallCommand + "'")
}
if err := b.NpmInstallUsingCommand(frontendDir, b.projectData.InstallCommand, verbose); err != nil {
return err
}
outputLogger.Println("Done.")
}
// Check if there is a build command
if b.projectData.BuildCommand == "" {
outputLogger.Println(" - No Build command. Skipping.")
outputLogger.Println("No Build command. Skipping.")
// No - ignore
return nil
}
outputLogger.Println(" - Compiling Frontend Project")
outputLogger.Print("Compiling frontend: ")
cmd := strings.Split(b.projectData.BuildCommand, " ")
if verbose {
outputLogger.Println("")
outputLogger.Println(" Build command: '" + strings.Join(cmd, " ") + "'")
}
stdout, stderr, err := shell.RunCommand(frontendDir, cmd[0], cmd[1:]...)
if verbose || err != nil {
for _, l := range strings.Split(stdout, "\n") {
@@ -382,7 +457,12 @@ func (b *BaseBuilder) BuildFrontend(outputLogger *clilogger.CLILogger) error {
fmt.Printf(" %s\n", l)
}
}
return err
if err != nil {
return err
}
outputLogger.Println("Done.")
return nil
}
// ExtractAssets gets the assets from the index.html file
@@ -391,3 +471,22 @@ func (b *BaseBuilder) ExtractAssets() (*html.AssetBundle, error) {
// Read in html
return html.NewAssetBundle(b.projectData.HTML)
}
func upsertEnv(env []string, key string, update func(v string) string) []string {
newEnv := make([]string, len(env), len(env)+1)
found := false
for i := range env {
if strings.HasPrefix(env[i], key+"=") {
eqIndex := strings.Index(env[i], "=")
val := env[i][eqIndex+1:]
newEnv[i] = fmt.Sprintf("%s=%v", key, update(val))
found = true
continue
}
newEnv[i] = env[i]
}
if !found {
newEnv = append(newEnv, fmt.Sprintf("%s=%v", key, update("")))
}
return newEnv
}

View File

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

View File

@@ -6,7 +6,10 @@ import (
"path/filepath"
"runtime"
"github.com/leaanthony/slicer"
"github.com/wailsapp/wails/v2/internal/fs"
"github.com/wailsapp/wails/v2/internal/shell"
"github.com/wailsapp/wails/v2/internal/project"
"github.com/wailsapp/wails/v2/pkg/clilogger"
)
@@ -25,20 +28,24 @@ var modeMap = []string{"Debug", "Production"}
// Options contains all the build options as well as the project data
type Options struct {
LDFlags string // Optional flags to pass to linker
Logger *clilogger.CLILogger // All output to the logger
OutputType string // EG: desktop, server....
Mode Mode // release or debug
ProjectData *project.Project // The project data
Pack bool // Create a package for the app after building
Platform string // The platform to build for
Compiler string // The compiler command to use
IgnoreFrontend bool // Indicates if the frontend does not need building
OutputFile string // Override the output filename
BuildDirectory string // Directory to use for building the application
CompiledBinary string // Fully qualified path to the compiled binary
KeepAssets bool // /Keep the generated assets/files
AppleIdentity string
LDFlags string // Optional flags to pass to linker
Logger *clilogger.CLILogger // All output to the logger
OutputType string // EG: desktop, server....
Mode Mode // release or debug
ProjectData *project.Project // The project data
Pack bool // Create a package for the app after building
Platform string // The platform to build for
Arch string // The architecture to build for
Compiler string // The compiler command to use
IgnoreFrontend bool // Indicates if the frontend does not need building
OutputFile string // Override the output filename
BuildDirectory string // Directory to use for building the application
CleanBuildDirectory bool // Indicates if the build directory should be cleaned before building
CompiledBinary string // Fully qualified path to the compiled binary
KeepAssets bool // Keep the generated assets/files
Verbosity int // Verbosity level (0 - silent, 1 - default, 2 - verbose)
Compress bool // Compress the final binary
AppleIdentity string
}
// GetModeAsString returns the current mode as a string
@@ -58,12 +65,6 @@ func Build(options *Options) (string, error) {
return "", err
}
// Check platform
validPlatforms := slicer.String([]string{"linux", "darwin"})
if !validPlatforms.Contains(options.Platform) {
return "", fmt.Errorf("platform %s not supported", options.Platform)
}
// Load project
projectData, err := project.Load(cwd)
if err != nil {
@@ -71,7 +72,7 @@ func Build(options *Options) (string, error) {
}
options.ProjectData = projectData
// Calculate build dir
// Set build directory
options.BuildDirectory = filepath.Join(options.ProjectData.Path, "build", options.Platform, options.OutputType)
// Save the project type
@@ -107,7 +108,6 @@ func Build(options *Options) (string, error) {
// return "", err
// }
if !options.IgnoreFrontend {
outputLogger.Println(" - Building Project Frontend")
err = builder.BuildFrontend(outputLogger)
if err != nil {
return "", err
@@ -115,30 +115,74 @@ func Build(options *Options) (string, error) {
}
// Build the base assets
outputLogger.Println(" - Compiling Assets")
err = builder.BuildAssets(options)
if err != nil {
return "", err
}
// Compile the application
outputLogger.Print(" - Compiling Application in " + GetModeAsString(options.Mode) + " mode...")
err = builder.CompileProject(options)
if err != nil {
return "", err
outputLogger.Print("Compiling application: ")
if options.Platform == "darwin" && options.Arch == "universal" {
outputFile := builder.OutputFilename(options)
amd64Filename := outputFile + "-amd64"
arm64Filename := outputFile + "-arm64"
// Build amd64 first
options.Arch = "amd64"
options.OutputFile = amd64Filename
options.CleanBuildDirectory = false
if options.Verbosity == VERBOSE {
println()
println(" Building AMD64 Target:", filepath.Join(options.BuildDirectory, options.OutputFile))
}
err = builder.CompileProject(options)
if err != nil {
return "", err
}
// Build arm64
options.Arch = "arm64"
options.OutputFile = arm64Filename
options.CleanBuildDirectory = false
if options.Verbosity == VERBOSE {
println(" Building ARM64 Target:", filepath.Join(options.BuildDirectory, options.OutputFile))
}
err = builder.CompileProject(options)
if err != nil {
return "", err
}
// Run lipo
if options.Verbosity == VERBOSE {
println(" Running lipo: ", "lipo", "-create", "-output", outputFile, amd64Filename, arm64Filename)
}
_, stderr, err := shell.RunCommand(options.BuildDirectory, "lipo", "-create", "-output", outputFile, amd64Filename, arm64Filename)
if err != nil {
return "", fmt.Errorf("%s - %s", err.Error(), stderr)
}
// Remove temp binaries
fs.DeleteFile(filepath.Join(options.BuildDirectory, amd64Filename))
fs.DeleteFile(filepath.Join(options.BuildDirectory, arm64Filename))
projectData.OutputFilename = outputFile
options.CompiledBinary = filepath.Join(options.BuildDirectory, outputFile)
} else {
err = builder.CompileProject(options)
if err != nil {
return "", err
}
}
outputLogger.Println("done.")
outputLogger.Println("Done.")
// Do we need to pack the app?
if options.Pack {
outputLogger.Println(" - Packaging Application")
outputLogger.Print("Packaging application: ")
// TODO: Allow cross platform build
err = packageProject(options, runtime.GOOS)
if err != nil {
return "", err
}
outputLogger.Println("Done.")
}
return projectData.OutputFilename, nil

View File

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

View File

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

View File

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

View File

@@ -42,7 +42,7 @@ func packageApplication(options *Options) error {
return errors.Wrap(err, "Cannot move file: "+options.ProjectData.OutputFilename)
}
// Generate info.plist
// Generate Info.plist
err = processPList(options, contentsDirectory)
if err != nil {
return err
@@ -68,7 +68,7 @@ func packageApplication(options *Options) error {
func processPList(options *Options, contentsDirectory string) error {
// Check if plist already exists in project dir
plistFile := filepath.Join(options.ProjectData.AssetsDir, "mac", "info.plist")
plistFile := filepath.Join(options.ProjectData.AssetsDir, "mac", "Info.plist")
// If the file doesn't exist, generate it
if !fs.FileExists(plistFile) {
@@ -79,7 +79,7 @@ func processPList(options *Options, contentsDirectory string) error {
}
// Copy it to the contents directory
targetFile := filepath.Join(contentsDirectory, "info.plist")
targetFile := filepath.Join(contentsDirectory, "Info.plist")
return fs.CopyFile(plistFile, targetFile)
}
@@ -88,11 +88,11 @@ func generateDefaultPlist(options *Options, targetPlistFile string) error {
exe := defaultString(options.OutputFile, name)
version := "1.0.0"
author := defaultString(options.ProjectData.Author.Name, "Anonymous")
packageID := strings.Join([]string{"wails", name, version}, ".")
packageID := strings.Join([]string{"wails", name}, ".")
plistData := newPlistData(name, exe, packageID, version, author)
tmpl := template.New("infoPlist")
plistTemplate := fs.RelativePath("./internal/packager/darwin/info.plist")
plistTemplate := fs.RelativePath("./internal/packager/darwin/Info.plist")
infoPlist, err := ioutil.ReadFile(plistTemplate)
if err != nil {
return errors.Wrap(err, "Cannot open plist template")

View File

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

View File

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

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

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

View File

@@ -30,7 +30,8 @@ var modifierMap = map[string]Modifier{
}
func parseModifier(text string) (*Modifier, error) {
result, valid := modifierMap[text]
lowertext := strings.ToLower(text)
result, valid := modifierMap[lowertext]
if !valid {
return nil, fmt.Errorf("'%s' is not a valid modifier", text)
}

View File

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

View File

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

View File

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

View File

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