Compare commits

...

90 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
Lea Anthony
7118762cec v2.0.0-alpha.39 2021-02-25 22:05:38 +11:00
Lea Anthony
6af92cf0a4 Delete Tray for bridge 2021-02-25 19:57:36 +11:00
matryer
1bb91634f7 avoid fmt in happy path 2021-02-25 07:39:42 +00:00
Lea Anthony
f71ce7913f v2.0.0-alpha.38 2021-02-24 21:50:39 +11:00
Lea Anthony
53db687a26 Support DeleteTrayMenuByID 2021-02-24 21:49:55 +11:00
Lea Anthony
13939d3d6b v2.0.0-alpha.37 2021-02-23 20:15:01 +11:00
Lea Anthony
552c6b8711 fix: modifiers 2021-02-23 18:57:59 +11:00
Lea Anthony
feee2b3db2 v2.0.0-alpha.36 2021-02-23 08:53:26 +11:00
Lea Anthony
9889c2bdbb Support Activation Policy 2021-02-23 08:52:56 +11:00
Lea Anthony
2432fccf71 v2.0.0-alpha.35 2021-02-22 20:17:22 +11:00
Lea Anthony
70510fd180 @wails/runtime v1.3.10 2021-02-22 20:16:14 +11:00
Lea Anthony
17c6201469 Menu off by default in dev. Toggle with backtick. 2021-02-22 20:14:44 +11:00
Lea Anthony
0f209c8900 v2.0.0-alpha.34 2021-02-22 19:39:07 +11:00
Lea Anthony
cbf043585c v2.0.0-alpha.33 2021-02-22 19:33:44 +11:00
Lea Anthony
5ae621ceaa Start signal handler a little later 2021-02-22 19:33:18 +11:00
Lea Anthony
1231b59443 better signal handling for shutdown 2021-02-22 17:39:33 +11:00
Lea Anthony
b18d4fbf41 v2.0.0-alpha.32 2021-02-22 09:00:47 +11:00
Lea Anthony
9ec5605e63 fix: loglevel duplication 2021-02-22 09:00:09 +11:00
Lea Anthony
98a4de8878 v2.0.0-alpha.31 2021-02-21 20:38:14 +11:00
Lea Anthony
5fe709f558 Trigger clean shutdown on Quit 2021-02-21 20:37:42 +11:00
Lea Anthony
5231a6893b v2.0.0-alpha.30 2021-02-21 19:28:07 +11:00
Lea Anthony
74f3ce990f Support loglevel flag in dev mode 2021-02-21 19:26:20 +11:00
Lea Anthony
998a913853 Hide dev warnings by default 2021-02-21 16:36:56 +11:00
Lea Anthony
964844835c Better Wails update messaging 2021-02-21 06:12:19 +11:00
Lea Anthony
4e152bb849 v2.0.0-alpha.29 2021-02-21 05:56:11 +11:00
Lea Anthony
51133d098c Slight refactor of backend module generation 2021-02-21 05:52:42 +11:00
Lea Anthony
d4191e7d1b Support parsing keyboard shortcuts 2021-02-20 21:32:32 +11:00
Lea Anthony
9c273bc745 v2.0.0-alpha.28 2021-02-20 16:04:42 +11:00
Lea Anthony
c6f6ad6beb Improved dev reload. Early abort for bad app. Don't reload if bad build. 2021-02-20 16:04:03 +11:00
Lea Anthony
4362a14459 Dev colours 2021-02-20 15:25:40 +11:00
Lea Anthony
0080e9e311 Gimme some colour 2021-02-20 15:14:20 +11:00
Lea Anthony
83d9297cac v2.0.0-alpha.27 2021-02-20 14:51:38 +11:00
76 changed files with 2131 additions and 727 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

@@ -5,12 +5,13 @@ import (
"io" "io"
"os" "os"
"os/signal" "os/signal"
"path/filepath"
"runtime" "runtime"
"strings" "strings"
"syscall" "syscall"
"time" "time"
"github.com/pkg/errors" "github.com/wailsapp/wails/v2/internal/colour"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
"github.com/leaanthony/clir" "github.com/leaanthony/clir"
@@ -20,6 +21,21 @@ import (
"github.com/wailsapp/wails/v2/pkg/commands/build" "github.com/wailsapp/wails/v2/pkg/commands/build"
) )
func LogGreen(message string, args ...interface{}) {
text := fmt.Sprintf(message, args...)
println(colour.Green(text))
}
func LogRed(message string, args ...interface{}) {
text := fmt.Sprintf(message, args...)
println(colour.Red(text))
}
func LogDarkYellow(message string, args ...interface{}) {
text := fmt.Sprintf(message, args...)
println(colour.DarkYellow(text))
}
// AddSubcommand adds the `dev` command for the Wails application // AddSubcommand adds the `dev` command for the Wails application
func AddSubcommand(app *clir.Cli, w io.Writer) error { func AddSubcommand(app *clir.Cli, w io.Writer) error {
@@ -37,6 +53,13 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
extensions := "go" extensions := "go"
command.StringFlag("e", "Extensions to trigger rebuilds (comma separated) eg go,js,css,html", &extensions) command.StringFlag("e", "Extensions to trigger rebuilds (comma separated) eg go,js,css,html", &extensions)
// extensions to trigger rebuilds
showWarnings := false
command.BoolFlag("w", "Show warnings", &showWarnings)
loglevel := ""
command.StringFlag("loglevel", "Loglevel to use - Trace, Debug, Info, Warning, Error", &loglevel)
command.Action(func() error { command.Action(func() error {
// Create logger // Create logger
@@ -52,7 +75,6 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
defer watcher.Close() defer watcher.Close()
var debugBinaryProcess *process.Process = nil var debugBinaryProcess *process.Process = nil
var buildFrontend bool = false
var extensionsThatTriggerARebuild = strings.Split(extensions, ",") var extensionsThatTriggerARebuild = strings.Split(extensions, ",")
// Setup signal handler // Setup signal handler
@@ -63,7 +85,10 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
// Do initial build // Do initial build
logger.Println("Building application for development...") logger.Println("Building application for development...")
debugBinaryProcess, err = restartApp(logger, "dev", ldflags, compilerCommand, debugBinaryProcess) newProcess, err := restartApp(logger, "dev", ldflags, compilerCommand, debugBinaryProcess, loglevel)
if newProcess != nil {
debugBinaryProcess = newProcess
}
if err != nil { if err != nil {
return err return err
} }
@@ -79,7 +104,7 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
if err != nil { if err != nil {
logger.Fatal("%s", err.Error()) logger.Fatal("%s", err.Error())
} }
logger.Println("Watching directory: %s", event.Name) LogGreen("[New Directory] Watching new directory: %s", event.Name)
} }
} }
return return
@@ -88,33 +113,29 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
// Check for file writes // Check for file writes
if event.Op&fsnotify.Write == fsnotify.Write { if event.Op&fsnotify.Write == fsnotify.Write {
logger.Println("modified file: %s", event.Name)
var rebuild bool = false var rebuild bool = false
// Iterate all file patterns // Iterate all file patterns
for _, pattern := range extensionsThatTriggerARebuild { for _, pattern := range extensionsThatTriggerARebuild {
rebuild = strings.HasSuffix(event.Name, pattern) if strings.HasSuffix(event.Name, pattern) {
if err != nil { rebuild = true
logger.Fatal(err.Error())
}
if rebuild {
// Only build frontend when the file isn't a Go file
buildFrontend = !strings.HasSuffix(event.Name, "go")
break break
} }
} }
if !rebuild { if !rebuild {
logger.Println("Filename change: %s did not match extension list (%s)", event.Name, extensions) if showWarnings {
LogDarkYellow("[File change] %s did not match extension list (%s)", event.Name, extensions)
}
return return
} }
logger.Println("Partial build triggered: %s updated", event.Name) LogGreen("[Attempting rebuild] %s updated", event.Name)
// Do a rebuild // Do a rebuild
// Try and build the app // Try and build the app
newBinaryProcess, err := restartApp(logger, "dev", ldflags, compilerCommand, debugBinaryProcess) newBinaryProcess, err := restartApp(logger, "dev", ldflags, compilerCommand, debugBinaryProcess, loglevel)
if err != nil { if err != nil {
fmt.Printf("Error during build: %s", err.Error()) fmt.Printf("Error during build: %s", err.Error())
return return
@@ -128,23 +149,28 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
}) })
// Get project dir // Get project dir
dir, err := os.Getwd() projectDir, err := os.Getwd()
if err != nil { if err != nil {
return err return err
} }
// Get all subdirectories // Get all subdirectories
dirs, err := fs.GetSubdirectories(dir) dirs, err := fs.GetSubdirectories(projectDir)
if err != nil { if err != nil {
return err return err
} }
LogGreen("Watching (sub)/directory: %s", projectDir)
// Setup a watcher for non-node_modules directories // Setup a watcher for non-node_modules directories
dirs.Each(func(dir string) { dirs.Each(func(dir string) {
if strings.Contains(dir, "node_modules") { if strings.Contains(dir, "node_modules") {
return return
} }
logger.Println("Watching directory: %s", dir) // Ignore build directory
if strings.HasPrefix(dir, filepath.Join(projectDir, "build")) {
return
}
err = watcher.Add(dir) err = watcher.Add(dir)
if err != nil { if err != nil {
logger.Fatal(err.Error()) logger.Fatal(err.Error())
@@ -156,7 +182,7 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
for quit == false { for quit == false {
select { select {
case <-quitChannel: case <-quitChannel:
println("Caught quit") LogGreen("\nCaught quit")
// Notify debouncer to quit // Notify debouncer to quit
debounceQuit <- true debounceQuit <- true
quit = true quit = true
@@ -171,7 +197,7 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
} }
} }
logger.Println("Development mode exited") LogGreen("Development mode exited")
return nil return nil
}) })
@@ -198,15 +224,15 @@ exit:
} }
} }
func restartApp(logger *clilogger.CLILogger, outputType string, ldflags string, compilerCommand string, debugBinaryProcess *process.Process) (*process.Process, error) { func restartApp(logger *clilogger.CLILogger, outputType string, ldflags string, compilerCommand string, debugBinaryProcess *process.Process, loglevel string) (*process.Process, error) {
appBinary, err := buildApp(logger, outputType, ldflags, compilerCommand) appBinary, err := buildApp(logger, outputType, ldflags, compilerCommand)
println() println()
if err != nil { if err != nil {
logger.Fatal(err.Error()) LogRed("Build error - continuing to run current version")
return nil, errors.Wrap(err, "Build Failed:") LogDarkYellow(err.Error())
return nil, nil
} }
logger.Println("Build new binary: %s", appBinary)
// Kill existing binary if need be // Kill existing binary if need be
if debugBinaryProcess != nil { if debugBinaryProcess != nil {
@@ -222,7 +248,7 @@ func restartApp(logger *clilogger.CLILogger, outputType string, ldflags string,
// TODO: Generate `backend.js` // TODO: Generate `backend.js`
// Start up new binary // Start up new binary
newProcess := process.NewProcess(logger, appBinary) newProcess := process.NewProcess(logger, appBinary, "-loglevel", loglevel)
err = newProcess.Start() err = newProcess.Start()
if err != nil { if err != nil {
// Remove binary // Remove binary
@@ -239,7 +265,7 @@ func restartApp(logger *clilogger.CLILogger, outputType string, ldflags string,
func buildApp(logger *clilogger.CLILogger, outputType string, ldflags string, compilerCommand string) (string, error) { func buildApp(logger *clilogger.CLILogger, outputType string, ldflags string, compilerCommand string) (string, error) {
// Create random output file // Create random output file
outputFile := fmt.Sprintf("debug-%d", time.Now().Unix()) outputFile := fmt.Sprintf("dev-%d", time.Now().Unix())
// Create BuildOptions // Create BuildOptions
buildOptions := &build.Options{ buildOptions := &build.Options{

View File

@@ -18,14 +18,14 @@ import (
func AddSubcommand(app *clir.Cli, w io.Writer, currentVersion string) error { func AddSubcommand(app *clir.Cli, w io.Writer, currentVersion string) error {
command := app.NewSubCommand("update", "Update the Wails CLI") command := app.NewSubCommand("update", "Update the Wails CLI")
command.LongDescription(`This command allows you to update your version of Wails.`) command.LongDescription(`This command allows you to update your version of the Wails CLI.`)
// Setup flags // Setup flags
var prereleaseRequired bool var prereleaseRequired bool
command.BoolFlag("pre", "Update to latest Prerelease", &prereleaseRequired) command.BoolFlag("pre", "Update CLI to latest Prerelease", &prereleaseRequired)
var specificVersion string var specificVersion string
command.StringFlag("version", "Install a specific version (Overrides other flags)", &specificVersion) command.StringFlag("version", "Install a specific version (Overrides other flags) of the CLI", &specificVersion)
command.Action(func() error { command.Action(func() error {
@@ -143,7 +143,7 @@ func updateToVersion(logger *clilogger.CLILogger, targetVersion *github.Semantic
} }
fmt.Println() fmt.Println()
logger.Print("Installing Wails " + desiredVersion + "...") logger.Print("Installing Wails CLI " + desiredVersion + "...")
// Run command in non module directory // Run command in non module directory
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
@@ -158,7 +158,7 @@ func updateToVersion(logger *clilogger.CLILogger, targetVersion *github.Semantic
return err return err
} }
fmt.Println() fmt.Println()
logger.Println("Wails updated to " + desiredVersion) logger.Println("Wails CLI updated to " + desiredVersion)
return nil return nil
} }

View File

@@ -1,8 +1,11 @@
package main package main
import ( import (
"fmt"
"os" "os"
"github.com/wailsapp/wails/v2/internal/colour"
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/update" "github.com/wailsapp/wails/v2/cmd/wails/internal/commands/update"
"github.com/leaanthony/clir" "github.com/leaanthony/clir"
@@ -19,11 +22,17 @@ func fatal(message string) {
os.Exit(1) os.Exit(1)
} }
func banner(_ *clir.Cli) string {
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)
build.AddBuildSubcommand(app, os.Stdout) build.AddBuildSubcommand(app, os.Stdout)
err = initialise.AddSubcommand(app, os.Stdout) err = initialise.AddSubcommand(app, os.Stdout)

View File

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

View File

@@ -20,9 +20,9 @@ require (
github.com/tdewolff/minify v2.3.6+incompatible github.com/tdewolff/minify v2.3.6+incompatible
github.com/tdewolff/parse v2.3.4+incompatible // indirect github.com/tdewolff/parse v2.3.4+incompatible // indirect
github.com/tdewolff/test v1.0.6 // indirect github.com/tdewolff/test v1.0.6 // indirect
github.com/wzshiming/ctc v1.2.3
github.com/xyproto/xpm v1.2.1 github.com/xyproto/xpm v1.2.1
golang.org/x/net v0.0.0-20200822124328-c89045814202 golang.org/x/net v0.0.0-20200822124328-c89045814202
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c
golang.org/x/tools v0.0.0-20200902012652-d1954cc86c82 golang.org/x/tools v0.0.0-20200902012652-d1954cc86c82
nhooyr.io/websocket v1.8.6 nhooyr.io/websocket v1.8.6

View File

@@ -78,6 +78,10 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/wzshiming/ctc v1.2.3 h1:q+hW3IQNsjIlOFBTGZZZeIXTElFM4grF4spW/errh/c=
github.com/wzshiming/ctc v1.2.3/go.mod h1:2tVAtIY7SUyraSk0JxvwmONNPFL4ARavPuEsg5+KA28=
github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae h1:tpXvBXC3hpQBDCc9OojJZCQMVRAbT3TTdUMP8WguXkY=
github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae/go.mod h1:VTAq37rkGeV+WOybvZwjXiJOicICdpLCN8ifpISjK20=
github.com/xyproto/xpm v1.2.1 h1:trdvGjjWBsOOKzBBUPT6JvaIQM3acJEEYfbxN7M96wg= github.com/xyproto/xpm v1.2.1 h1:trdvGjjWBsOOKzBBUPT6JvaIQM3acJEEYfbxN7M96wg=
github.com/xyproto/xpm v1.2.1/go.mod h1:cMnesLsD0PBXLgjDfTDEaKr8XyTFsnP1QycSqRw7BiY= github.com/xyproto/xpm v1.2.1/go.mod h1:cMnesLsD0PBXLgjDfTDEaKr8XyTFsnP1QycSqRw7BiY=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -92,11 +96,10 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200107162124-548cf772de50/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c h1:UIcGWL6/wpCfyGuJnRFJRurA+yj8RrW7Q6x2YMCXt6c= golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c h1:UIcGWL6/wpCfyGuJnRFJRurA+yj8RrW7Q6x2YMCXt6c=

View File

@@ -4,8 +4,9 @@ package app
import ( import (
"flag" "flag"
"github.com/wailsapp/wails/v2/pkg/logger"
"strings" "strings"
"github.com/wailsapp/wails/v2/pkg/logger"
) )
// Init initialises the application for a debug environment // Init initialises the application for a debug environment
@@ -19,10 +20,10 @@ func (a *App) Init() error {
} }
// Set log levels // Set log levels
greeting := flag.String("loglevel", "debug", "Loglevel to use - Trace, Debug, Info, Warning, Error") loglevel := flag.String("loglevel", "debug", "Loglevel to use - Trace, Debug, Info, Warning, Error")
flag.Parse() flag.Parse()
if len(*greeting) > 0 { if len(*loglevel) > 0 {
switch strings.ToLower(*greeting) { switch strings.ToLower(*loglevel) {
case "trace": case "trace":
a.logger.SetLogLevel(logger.TRACE) a.logger.SetLogLevel(logger.TRACE)
case "info": case "info":

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
@@ -117,14 +118,6 @@ func (a *App) Run() error {
parentContext := context.WithValue(context.Background(), "waitgroup", &subsystemWaitGroup) parentContext := context.WithValue(context.Background(), "waitgroup", &subsystemWaitGroup)
ctx, cancel := context.WithCancel(parentContext) ctx, cancel := context.WithCancel(parentContext)
// Setup signal handler
signalsubsystem, err := signal.NewManager(ctx, cancel, a.servicebus, a.logger, a.shutdownCallback)
if err != nil {
return err
}
a.signal = signalsubsystem
a.signal.Start()
// Start the service bus // Start the service bus
a.servicebus.Debug() a.servicebus.Debug()
err = a.servicebus.Start() err = a.servicebus.Start()
@@ -132,7 +125,7 @@ func (a *App) Run() error {
return err return err
} }
runtimesubsystem, err := subsystem.NewRuntime(ctx, a.servicebus, a.logger, a.startupCallback, a.shutdownCallback) runtimesubsystem, err := subsystem.NewRuntime(ctx, a.servicebus, a.logger, a.startupCallback)
if err != nil { if err != nil {
return err return err
} }
@@ -168,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 {
@@ -207,6 +213,14 @@ func (a *App) Run() error {
return err return err
} }
// Setup signal handler
signalsubsystem, err := signal.NewManager(ctx, cancel, a.servicebus, a.logger)
if err != nil {
return err
}
a.signal = signalsubsystem
a.signal.Start()
err = a.window.Run(dispatcher, bindingDump, a.debug) err = a.window.Run(dispatcher, bindingDump, a.debug)
a.logger.Trace("Ffenestri.Run() exited") a.logger.Trace("Ffenestri.Run() exited")
if err != nil { if err != nil {
@@ -231,5 +245,10 @@ func (a *App) Run() error {
return err return err
} }
// Shutdown callback
if a.shutdownCallback != nil {
a.shutdownCallback()
}
return nil return nil
} }

View File

@@ -66,7 +66,6 @@ func CreateApp(appoptions *options.App) (*App, error) {
// Set up logger // Set up logger
myLogger := logger.New(appoptions.Logger) myLogger := logger.New(appoptions.Logger)
myLogger.SetLogLevel(appoptions.LogLevel)
// Create the menu manager // Create the menu manager
menuManager := menumanager.NewManager() menuManager := menumanager.NewManager()
@@ -119,14 +118,6 @@ func (a *App) Run() error {
parentContext := context.WithValue(context.Background(), "waitgroup", &subsystemWaitGroup) parentContext := context.WithValue(context.Background(), "waitgroup", &subsystemWaitGroup)
ctx, cancel := context.WithCancel(parentContext) ctx, cancel := context.WithCancel(parentContext)
// Setup signal handler
signalsubsystem, err := signal.NewManager(ctx, cancel, a.servicebus, a.logger, a.shutdownCallback)
if err != nil {
return err
}
a.signal = signalsubsystem
a.signal.Start()
// Start the service bus // Start the service bus
a.servicebus.Debug() a.servicebus.Debug()
err = a.servicebus.Start() err = a.servicebus.Start()
@@ -134,7 +125,7 @@ func (a *App) Run() error {
return err return err
} }
runtimesubsystem, err := subsystem.NewRuntime(ctx, a.servicebus, a.logger, a.startupCallback, a.shutdownCallback) runtimesubsystem, err := subsystem.NewRuntime(ctx, a.servicebus, a.logger, a.startupCallback)
if err != nil { if err != nil {
return err return err
} }
@@ -212,6 +203,14 @@ func (a *App) Run() error {
// Generate backend.js // Generate backend.js
a.bindings.GenerateBackendJS() a.bindings.GenerateBackendJS()
// Setup signal handler
signalsubsystem, err := signal.NewManager(ctx, cancel, a.servicebus, a.logger)
if err != nil {
return err
}
a.signal = signalsubsystem
a.signal.Start()
err = a.bridge.Run(dispatcher, a.menuManager, bindingDump, a.debug) err = a.bridge.Run(dispatcher, a.menuManager, bindingDump, a.debug)
a.logger.Trace("Bridge.Run() exited") a.logger.Trace("Bridge.Run() exited")
if err != nil { if err != nil {
@@ -236,6 +235,10 @@ func (a *App) Run() error {
return err return err
} }
// Shutdown callback
if a.shutdownCallback != nil {
a.shutdownCallback()
}
return nil return nil
} }

View File

@@ -58,7 +58,7 @@ const backend = {`)
sortedPackageNames.Add(packageName) sortedPackageNames.Add(packageName)
} }
sortedPackageNames.Sort() sortedPackageNames.Sort()
for _, packageName := range sortedPackageNames.AsSlice() { sortedPackageNames.Each(func(packageName string) {
packages := store[packageName] packages := store[packageName]
output.WriteString(fmt.Sprintf(" \"%s\": {", packageName)) output.WriteString(fmt.Sprintf(" \"%s\": {", packageName))
output.WriteString("\n") output.WriteString("\n")
@@ -67,7 +67,8 @@ const backend = {`)
sortedStructNames.Add(structName) sortedStructNames.Add(structName)
} }
sortedStructNames.Sort() sortedStructNames.Sort()
for _, structName := range sortedStructNames.AsSlice() {
sortedStructNames.Each(func(structName string) {
structs := packages[structName] structs := packages[structName]
output.WriteString(fmt.Sprintf(" \"%s\": {", structName)) output.WriteString(fmt.Sprintf(" \"%s\": {", structName))
output.WriteString("\n") output.WriteString("\n")
@@ -78,7 +79,7 @@ const backend = {`)
} }
sortedMethodNames.Sort() sortedMethodNames.Sort()
for _, methodName := range sortedMethodNames.AsSlice() { sortedMethodNames.Each(func(methodName string) {
methodDetails := structs[methodName] methodDetails := structs[methodName]
output.WriteString(" /**\n") output.WriteString(" /**\n")
output.WriteString(" * " + methodName + "\n") output.WriteString(" * " + methodName + "\n")
@@ -109,13 +110,16 @@ const backend = {`)
output.WriteString("\n") output.WriteString("\n")
output.WriteString(fmt.Sprintf(" },")) output.WriteString(fmt.Sprintf(" },"))
output.WriteString("\n") output.WriteString("\n")
}
})
output.WriteString(fmt.Sprintf(" }")) output.WriteString(fmt.Sprintf(" }"))
output.WriteString("\n") output.WriteString("\n")
} })
output.WriteString(fmt.Sprintf(" }\n")) output.WriteString(fmt.Sprintf(" }\n"))
output.WriteString("\n") output.WriteString("\n")
} })
output.WriteString(`}; output.WriteString(`};
export default backend;`) export default backend;`)

View File

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

View File

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

View File

@@ -0,0 +1,89 @@
package colour
import (
"fmt"
"strings"
"github.com/wzshiming/ctc"
)
func Col(col ctc.Color, text string) string {
return fmt.Sprintf("%s%s%s", col, text, ctc.Reset)
}
func Yellow(text string) string {
return Col(ctc.ForegroundBrightYellow, text)
}
func Red(text string) string {
return Col(ctc.ForegroundBrightRed, text)
}
func Blue(text string) string {
return Col(ctc.ForegroundBrightBlue, text)
}
func Green(text string) string {
return Col(ctc.ForegroundBrightGreen, text)
}
func Cyan(text string) string {
return Col(ctc.ForegroundBrightCyan, text)
}
func Magenta(text string) string {
return Col(ctc.ForegroundBrightMagenta, text)
}
func White(text string) string {
return Col(ctc.ForegroundBrightWhite, text)
}
func Black(text string) string {
return Col(ctc.ForegroundBrightBlack, text)
}
func DarkYellow(text string) string {
return Col(ctc.ForegroundYellow, text)
}
func DarkRed(text string) string {
return Col(ctc.ForegroundRed, text)
}
func DarkBlue(text string) string {
return Col(ctc.ForegroundBlue, text)
}
func DarkGreen(text string) string {
return Col(ctc.ForegroundGreen, text)
}
func DarkCyan(text string) string {
return Col(ctc.ForegroundCyan, text)
}
func DarkMagenta(text string) string {
return Col(ctc.ForegroundMagenta, text)
}
func DarkWhite(text string) string {
return Col(ctc.ForegroundWhite, text)
}
func DarkBlack(text string) string {
return Col(ctc.ForegroundBlack, text)
}
var rainbowCols = []func(string) string{Red, Yellow, Green, Cyan, Blue, Magenta}
func Rainbow(text string) string {
var builder strings.Builder
for i := 0; i < len(text); i++ {
fn := rainbowCols[i%len(rainbowCols)]
builder.WriteString(fn(text[i : i+1]))
}
return builder.String()
}

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -48,6 +48,9 @@ func (a *Application) processPlatformSettings() error {
C.SetAppearance(a.app, a.string2CString(string(mac.Appearance))) C.SetAppearance(a.app, a.string2CString(string(mac.Appearance)))
} }
// Set activation policy
C.SetActivationPolicy(a.app, C.int(mac.ActivationPolicy))
// Check if the webview should be transparent // Check if the webview should be transparent
if mac.WebviewIsTransparent { if mac.WebviewIsTransparent {
C.WebviewIsTransparent(a.app) C.WebviewIsTransparent(a.app)
@@ -87,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
@@ -66,6 +85,10 @@
#define NSControlStateValueOff 0 #define NSControlStateValueOff 0
#define NSControlStateValueOn 1 #define NSControlStateValueOn 1
#define NSApplicationActivationPolicyRegular 0
#define NSApplicationActivationPolicyAccessory 1
#define NSApplicationActivationPolicyProhibited 2
// Unbelievably, if the user swaps their button preference // Unbelievably, if the user swaps their button preference
// then right buttons are reported as left buttons // then right buttons are reported as left buttons
#define NSEventMaskLeftMouseDown 1 << 1 #define NSEventMaskLeftMouseDown 1 << 1
@@ -110,6 +133,10 @@ void SetTray(struct Application* app, const char *, const char *, const char *);
//void SetContextMenus(struct Application* app, const char *); //void SetContextMenus(struct Application* app, const char *);
void AddTrayMenu(struct Application* app, const char *); void AddTrayMenu(struct Application* app, const char *);
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;
@@ -180,151 +180,151 @@ id processAcceleratorKey(const char *key) {
return str(""); return str("");
} }
if( STREQ(key, "Backspace") ) { if( STREQ(key, "backspace") ) {
return strunicode(0x0008); return strunicode(0x0008);
} }
if( STREQ(key, "Tab") ) { if( STREQ(key, "tab") ) {
return strunicode(0x0009); return strunicode(0x0009);
} }
if( STREQ(key, "Return") ) { if( STREQ(key, "return") ) {
return strunicode(0x000d); return strunicode(0x000d);
} }
if( STREQ(key, "Escape") ) { if( STREQ(key, "escape") ) {
return strunicode(0x001b); return strunicode(0x001b);
} }
if( STREQ(key, "Left") ) { if( STREQ(key, "left") ) {
return strunicode(0x001c); return strunicode(0x001c);
} }
if( STREQ(key, "Right") ) { if( STREQ(key, "right") ) {
return strunicode(0x001d); return strunicode(0x001d);
} }
if( STREQ(key, "Up") ) { if( STREQ(key, "up") ) {
return strunicode(0x001e); return strunicode(0x001e);
} }
if( STREQ(key, "Down") ) { if( STREQ(key, "down") ) {
return strunicode(0x001f); return strunicode(0x001f);
} }
if( STREQ(key, "Space") ) { if( STREQ(key, "space") ) {
return strunicode(0x0020); return strunicode(0x0020);
} }
if( STREQ(key, "Delete") ) { if( STREQ(key, "delete") ) {
return strunicode(0x007f); return strunicode(0x007f);
} }
if( STREQ(key, "Home") ) { if( STREQ(key, "home") ) {
return strunicode(0x2196); return strunicode(0x2196);
} }
if( STREQ(key, "End") ) { if( STREQ(key, "end") ) {
return strunicode(0x2198); return strunicode(0x2198);
} }
if( STREQ(key, "Page Up") ) { if( STREQ(key, "page up") ) {
return strunicode(0x21de); return strunicode(0x21de);
} }
if( STREQ(key, "Page Down") ) { if( STREQ(key, "page down") ) {
return strunicode(0x21df); return strunicode(0x21df);
} }
if( STREQ(key, "F1") ) { if( STREQ(key, "f1") ) {
return strunicode(0xf704); return strunicode(0xf704);
} }
if( STREQ(key, "F2") ) { if( STREQ(key, "f2") ) {
return strunicode(0xf705); return strunicode(0xf705);
} }
if( STREQ(key, "F3") ) { if( STREQ(key, "f3") ) {
return strunicode(0xf706); return strunicode(0xf706);
} }
if( STREQ(key, "F4") ) { if( STREQ(key, "f4") ) {
return strunicode(0xf707); return strunicode(0xf707);
} }
if( STREQ(key, "F5") ) { if( STREQ(key, "f5") ) {
return strunicode(0xf708); return strunicode(0xf708);
} }
if( STREQ(key, "F6") ) { if( STREQ(key, "f6") ) {
return strunicode(0xf709); return strunicode(0xf709);
} }
if( STREQ(key, "F7") ) { if( STREQ(key, "f7") ) {
return strunicode(0xf70a); return strunicode(0xf70a);
} }
if( STREQ(key, "F8") ) { if( STREQ(key, "f8") ) {
return strunicode(0xf70b); return strunicode(0xf70b);
} }
if( STREQ(key, "F9") ) { if( STREQ(key, "f9") ) {
return strunicode(0xf70c); return strunicode(0xf70c);
} }
if( STREQ(key, "F10") ) { if( STREQ(key, "f10") ) {
return strunicode(0xf70d); return strunicode(0xf70d);
} }
if( STREQ(key, "F11") ) { if( STREQ(key, "f11") ) {
return strunicode(0xf70e); return strunicode(0xf70e);
} }
if( STREQ(key, "F12") ) { if( STREQ(key, "f12") ) {
return strunicode(0xf70f); return strunicode(0xf70f);
} }
if( STREQ(key, "F13") ) { if( STREQ(key, "f13") ) {
return strunicode(0xf710); return strunicode(0xf710);
} }
if( STREQ(key, "F14") ) { if( STREQ(key, "f14") ) {
return strunicode(0xf711); return strunicode(0xf711);
} }
if( STREQ(key, "F15") ) { if( STREQ(key, "f15") ) {
return strunicode(0xf712); return strunicode(0xf712);
} }
if( STREQ(key, "F16") ) { if( STREQ(key, "f16") ) {
return strunicode(0xf713); return strunicode(0xf713);
} }
if( STREQ(key, "F17") ) { if( STREQ(key, "f17") ) {
return strunicode(0xf714); return strunicode(0xf714);
} }
if( STREQ(key, "F18") ) { if( STREQ(key, "f18") ) {
return strunicode(0xf715); return strunicode(0xf715);
} }
if( STREQ(key, "F19") ) { if( STREQ(key, "f19") ) {
return strunicode(0xf716); return strunicode(0xf716);
} }
if( STREQ(key, "F20") ) { if( STREQ(key, "f20") ) {
return strunicode(0xf717); return strunicode(0xf717);
} }
if( STREQ(key, "F21") ) { if( STREQ(key, "f21") ) {
return strunicode(0xf718); return strunicode(0xf718);
} }
if( STREQ(key, "F22") ) { if( STREQ(key, "f22") ) {
return strunicode(0xf719); return strunicode(0xf719);
} }
if( STREQ(key, "F23") ) { if( STREQ(key, "f23") ) {
return strunicode(0xf71a); return strunicode(0xf71a);
} }
if( STREQ(key, "F24") ) { if( STREQ(key, "f24") ) {
return strunicode(0xf71b); return strunicode(0xf71b);
} }
if( STREQ(key, "F25") ) { if( STREQ(key, "f25") ) {
return strunicode(0xf71c); return strunicode(0xf71c);
} }
if( STREQ(key, "F26") ) { if( STREQ(key, "f26") ) {
return strunicode(0xf71d); return strunicode(0xf71d);
} }
if( STREQ(key, "F27") ) { if( STREQ(key, "f27") ) {
return strunicode(0xf71e); return strunicode(0xf71e);
} }
if( STREQ(key, "F28") ) { if( STREQ(key, "f28") ) {
return strunicode(0xf71f); return strunicode(0xf71f);
} }
if( STREQ(key, "F29") ) { if( STREQ(key, "f29") ) {
return strunicode(0xf720); return strunicode(0xf720);
} }
if( STREQ(key, "F30") ) { if( STREQ(key, "f30") ) {
return strunicode(0xf721); return strunicode(0xf721);
} }
if( STREQ(key, "F31") ) { if( STREQ(key, "f31") ) {
return strunicode(0xf722); return strunicode(0xf722);
} }
if( STREQ(key, "F32") ) { if( STREQ(key, "f32") ) {
return strunicode(0xf723); return strunicode(0xf723);
} }
if( STREQ(key, "F33") ) { if( STREQ(key, "f33") ) {
return strunicode(0xf724); return strunicode(0xf724);
} }
if( STREQ(key, "F34") ) { if( STREQ(key, "f34") ) {
return strunicode(0xf725); return strunicode(0xf725);
} }
if( STREQ(key, "F35") ) { if( STREQ(key, "f35") ) {
return strunicode(0xf726); return strunicode(0xf726);
} }
// if( STREQ(key, "Insert") ) { // if( STREQ(key, "Insert") ) {
@@ -336,7 +336,7 @@ id processAcceleratorKey(const char *key) {
// if( STREQ(key, "ScrollLock") ) { // if( STREQ(key, "ScrollLock") ) {
// return strunicode(0xf72f); // return strunicode(0xf72f);
// } // }
if( STREQ(key, "NumLock") ) { if( STREQ(key, "numLock") ) {
return strunicode(0xf739); return strunicode(0xf739);
} }
@@ -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);
@@ -508,20 +508,21 @@ unsigned long parseModifiers(const char **modifiers) {
const char *thisModifier = modifiers[0]; const char *thisModifier = modifiers[0];
int count = 0; int count = 0;
while( thisModifier != NULL ) { while( thisModifier != NULL ) {
// Determine flags // Determine flags
if( STREQ(thisModifier, "CmdOrCtrl") ) { if( STREQ(thisModifier, "cmdorctrl") ) {
result |= NSEventModifierFlagCommand; result |= NSEventModifierFlagCommand;
} }
if( STREQ(thisModifier, "OptionOrAlt") ) { if( STREQ(thisModifier, "optionoralt") ) {
result |= NSEventModifierFlagOption; result |= NSEventModifierFlagOption;
} }
if( STREQ(thisModifier, "Shift") ) { if( STREQ(thisModifier, "shift") ) {
result |= NSEventModifierFlagShift; result |= NSEventModifierFlagShift;
} }
if( STREQ(thisModifier, "Super") ) { if( STREQ(thisModifier, "super") ) {
result |= NSEventModifierFlagCommand; result |= NSEventModifierFlagCommand;
} }
if( STREQ(thisModifier, "Control") ) { if( STREQ(thisModifier, "ctrl") ) {
result |= NSEventModifierFlagControl; result |= NSEventModifierFlagControl;
} }
count++; count++;
@@ -539,18 +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;
} }
@@ -565,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;
@@ -641,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;
} }
@@ -700,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
@@ -726,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 = "";
@@ -746,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
@@ -781,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,12 +52,13 @@ int showTrayMenu(void *const context, struct hashmap_element_s *const e) {
} }
void ShowTrayMenusInStore(TrayMenuStore* store) { void ShowTrayMenusInStore(TrayMenuStore* store) {
pthread_mutex_lock(&store->lock);
if( hashmap_num_entries(&store->trayMenuMap) > 0 ) { if( hashmap_num_entries(&store->trayMenuMap) > 0 ) {
hashmap_iterate_pairs(&store->trayMenuMap, showTrayMenu, NULL); hashmap_iterate_pairs(&store->trayMenuMap, showTrayMenu, NULL);
} }
pthread_mutex_unlock(&store->lock);
} }
int freeTrayMenu(void *const context, struct hashmap_element_s *const e) { int freeTrayMenu(void *const context, struct hashmap_element_s *const e) {
DeleteTrayMenu(e->data); DeleteTrayMenu(e->data);
return -1; return -1;
@@ -65,22 +75,39 @@ void DeleteTrayMenuStore(TrayMenuStore *store) {
// Destroy tray menu map // Destroy tray menu map
hashmap_destroy(&store->trayMenuMap); hashmap_destroy(&store->trayMenuMap);
pthread_mutex_destroy(&store->lock);
} }
TrayMenu* GetTrayMenuFromStore(TrayMenuStore* store, const char* menuID) { TrayMenu* GetTrayMenuFromStore(TrayMenuStore* store, const char* menuID) {
// Get the current menu // Get the current menu
return hashmap_get(&store->trayMenuMap, menuID, strlen(menuID)); pthread_mutex_lock(&store->lock);
TrayMenu* result = hashmap_get(&store->trayMenuMap, menuID, strlen(menuID));
pthread_mutex_unlock(&store->lock);
return result;
} }
TrayMenu* MustGetTrayMenuFromStore(TrayMenuStore* store, const char* menuID) { TrayMenu* MustGetTrayMenuFromStore(TrayMenuStore* store, const char* menuID) {
// Get the current menu // Get the current menu
pthread_mutex_lock(&store->lock);
TrayMenu* result = hashmap_get(&store->trayMenuMap, menuID, strlen(menuID)); TrayMenu* result = hashmap_get(&store->trayMenuMap, menuID, strlen(menuID));
pthread_mutex_unlock(&store->lock);
if (result == NULL ) { if (result == NULL ) {
ABORT("Unable to find TrayMenu with ID '%s' in the TrayMenuStore!", menuID); ABORT("Unable to find TrayMenu with ID '%s' in the TrayMenuStore!", menuID);
} }
return result; return result;
} }
void DeleteTrayMenuInStore(TrayMenuStore* store, const char* ID) {
TrayMenu *menu = MustGetTrayMenuFromStore(store, ID);
pthread_mutex_lock(&store->lock);
hashmap_remove(&store->trayMenuMap, ID, strlen(ID));
pthread_mutex_unlock(&store->lock);
DeleteTrayMenu(menu);
}
void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON) { void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON) {
// Parse the JSON // Parse the JSON
JsonNode *parsedUpdate = mustParseJSON(JSON); JsonNode *parsedUpdate = mustParseJSON(JSON);
@@ -91,7 +118,17 @@ void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON) {
// Check we have this menu // Check we have this menu
TrayMenu *menu = MustGetTrayMenuFromStore(store, ID); TrayMenu *menu = MustGetTrayMenuFromStore(store, ID);
UpdateTrayLabel(menu, Label);
const char *fontName = getJSONString(parsedUpdate, "FontName");
const char *RGBA = getJSONString(parsedUpdate, "RGBA");
int fontSize = 0;
getJSONInt(parsedUpdate, "FontSize", &fontSize);
const char *tooltip = getJSONString(parsedUpdate, "Tooltip");
bool disabled = false;
getJSONBool(parsedUpdate, "Disabled", &disabled);
UpdateTrayLabel(menu, Label, fontName, fontSize, RGBA, tooltip, disabled);
} }
@@ -105,7 +142,9 @@ void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON) {
// If we don't have a menu, we create one // If we don't have a menu, we create one
if ( currentMenu == NULL ) { if ( currentMenu == NULL ) {
// Store the new menu // Store the new menu
pthread_mutex_lock(&store->lock);
hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu); hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
pthread_mutex_unlock(&store->lock);
// Show it // Show it
ShowTrayMenu(newMenu); ShowTrayMenu(newMenu);
@@ -116,7 +155,9 @@ void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON) {
// Save the status bar reference // Save the status bar reference
newMenu->statusbaritem = currentMenu->statusbaritem; newMenu->statusbaritem = currentMenu->statusbaritem;
pthread_mutex_lock(&store->lock);
hashmap_remove(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID)); hashmap_remove(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID));
pthread_mutex_unlock(&store->lock);
// Delete the current menu // Delete the current menu
DeleteMenu(currentMenu->menu); DeleteMenu(currentMenu->menu);
@@ -125,9 +166,10 @@ void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON) {
// Free the tray menu memory // Free the tray menu memory
MEMFREE(currentMenu); MEMFREE(currentMenu);
pthread_mutex_lock(&store->lock);
hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu); hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
pthread_mutex_unlock(&store->lock);
// Show the updated menu // Show the updated menu
ShowTrayMenu(newMenu); ShowTrayMenu(newMenu);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -39,7 +39,12 @@ func (p *Process) Start() error {
go func(cmd *exec.Cmd, running *bool, logger *clilogger.CLILogger, exitChannel chan bool) { go func(cmd *exec.Cmd, running *bool, logger *clilogger.CLILogger, exitChannel chan bool) {
logger.Println("Starting process (PID: %d)", cmd.Process.Pid) logger.Println("Starting process (PID: %d)", cmd.Process.Pid)
cmd.Wait() err := cmd.Wait()
if err != nil {
if err.Error() != "signal: killed" {
logger.Fatal("Fatal error from app: " + err.Error())
}
}
logger.Println("Exiting process (PID: %d)", cmd.Process.Pid) logger.Println("Exiting process (PID: %d)", cmd.Process.Pid)
*running = false *running = false
exitChannel <- true exitChannel <- true

View File

@@ -620,7 +620,7 @@
} }
/** Menubar **/ /** Menubar **/
const menuVisible = writable(true); const menuVisible = writable(false);
/** Trays **/ /** Trays **/
@@ -649,6 +649,18 @@
}); });
} }
function deleteTrayMenu(id) {
trays.update((current) => {
// Remove existing if it exists, else add
const index = current.findIndex(item => item.ID === id);
if ( index === -1 ) {
return log("ERROR: Attempted to delete tray index ")
}
current.splice(index, 1);
return current;
});
}
let selectedMenu = writable(null); let selectedMenu = writable(null);
function fade(node, { delay = 0, duration = 400, easing = identity } = {}) { function fade(node, { delay = 0, duration = 400, easing = identity } = {}) {
@@ -1220,11 +1232,11 @@
function get_each_context$1(ctx, list, i) { function get_each_context$1(ctx, list, i) {
const child_ctx = ctx.slice(); const child_ctx = ctx.slice();
child_ctx[8] = list[i]; child_ctx[9] = list[i];
return child_ctx; return child_ctx;
} }
// (29:0) {#if $menuVisible } // (38:0) {#if $menuVisible }
function create_if_block$3(ctx) { function create_if_block$3(ctx) {
let div; let div;
let span1; let span1;
@@ -1336,11 +1348,11 @@
}; };
} }
// (32:4) {#each $trays as tray} // (41:4) {#each $trays as tray}
function create_each_block$1(ctx) { function create_each_block$1(ctx) {
let traymenu; let traymenu;
let current; let current;
traymenu = new TrayMenu({ props: { tray: /*tray*/ ctx[8] } }); traymenu = new TrayMenu({ props: { tray: /*tray*/ ctx[9] } });
return { return {
c() { c() {
@@ -1352,7 +1364,7 @@
}, },
p(ctx, dirty) { p(ctx, dirty) {
const traymenu_changes = {}; const traymenu_changes = {};
if (dirty & /*$trays*/ 4) traymenu_changes.tray = /*tray*/ ctx[8]; if (dirty & /*$trays*/ 4) traymenu_changes.tray = /*tray*/ ctx[9];
traymenu.$set(traymenu_changes); traymenu.$set(traymenu_changes);
}, },
i(local) { i(local) {
@@ -1373,6 +1385,8 @@
function create_fragment$3(ctx) { function create_fragment$3(ctx) {
let if_block_anchor; let if_block_anchor;
let current; let current;
let mounted;
let dispose;
let if_block = /*$menuVisible*/ ctx[1] && create_if_block$3(ctx); let if_block = /*$menuVisible*/ ctx[1] && create_if_block$3(ctx);
return { return {
@@ -1384,6 +1398,11 @@
if (if_block) if_block.m(target, anchor); if (if_block) if_block.m(target, anchor);
insert(target, if_block_anchor, anchor); insert(target, if_block_anchor, anchor);
current = true; current = true;
if (!mounted) {
dispose = listen(window, "keydown", /*handleKeydown*/ ctx[3]);
mounted = true;
}
}, },
p(ctx, [dirty]) { p(ctx, [dirty]) {
if (/*$menuVisible*/ ctx[1]) { if (/*$menuVisible*/ ctx[1]) {
@@ -1421,6 +1440,8 @@
d(detaching) { d(detaching) {
if (if_block) if_block.d(detaching); if (if_block) if_block.d(detaching);
if (detaching) detach(if_block_anchor); if (detaching) detach(if_block_anchor);
mounted = false;
dispose();
} }
}; };
} }
@@ -1440,7 +1461,7 @@
onMount(() => { onMount(() => {
const interval = setInterval( const interval = setInterval(
() => { () => {
$$invalidate(3, time = new Date()); $$invalidate(4, time = new Date());
}, },
1000 1000
); );
@@ -1450,33 +1471,52 @@
}; };
}); });
function handleKeydown(e) {
// Backtick toggle
if (e.keyCode == 192) {
menuVisible.update(current => {
return !current;
});
}
}
$$self.$$.update = () => { $$self.$$.update = () => {
if ($$self.$$.dirty & /*time*/ 8) { if ($$self.$$.dirty & /*time*/ 16) {
$$invalidate(4, day = time.toLocaleString("default", { weekday: "short" })); $$invalidate(5, day = time.toLocaleString("default", { weekday: "short" }));
} }
if ($$self.$$.dirty & /*time*/ 8) { if ($$self.$$.dirty & /*time*/ 16) {
$$invalidate(5, dom = time.getDate()); $$invalidate(6, dom = time.getDate());
} }
if ($$self.$$.dirty & /*time*/ 8) { if ($$self.$$.dirty & /*time*/ 16) {
$$invalidate(6, mon = time.toLocaleString("default", { month: "short" })); $$invalidate(7, mon = time.toLocaleString("default", { month: "short" }));
} }
if ($$self.$$.dirty & /*time*/ 8) { if ($$self.$$.dirty & /*time*/ 16) {
$$invalidate(7, currentTime = time.toLocaleString("en-US", { $$invalidate(8, currentTime = time.toLocaleString("en-US", {
hour: "numeric", hour: "numeric",
minute: "numeric", minute: "numeric",
hour12: true hour12: true
}).toLowerCase()); }).toLowerCase());
} }
if ($$self.$$.dirty & /*day, dom, mon, currentTime*/ 240) { if ($$self.$$.dirty & /*day, dom, mon, currentTime*/ 480) {
$$invalidate(0, dateTimeString = `${day} ${dom} ${mon} ${currentTime}`); $$invalidate(0, dateTimeString = `${day} ${dom} ${mon} ${currentTime}`);
} }
}; };
return [dateTimeString, $menuVisible, $trays, time, day, dom, mon, currentTime]; return [
dateTimeString,
$menuVisible,
$trays,
handleKeydown,
time,
day,
dom,
mon,
currentTime
];
} }
class Menubar extends SvelteComponent { class Menubar extends SvelteComponent {
@@ -1638,6 +1678,11 @@
let trayLabelData = JSON.parse(updateTrayLabelJSON); let trayLabelData = JSON.parse(updateTrayLabelJSON);
updateTrayLabel(trayLabelData); updateTrayLabel(trayLabelData);
break break
case 'D':
// Delete Tray Menu
const id = trayMessage.slice(1);
deleteTrayMenu(id);
break
default: default:
log('Unknown tray message: ' + message.data); log('Unknown tray message: ' + message.data);
} }

View File

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

View File

@@ -24,6 +24,15 @@
}; };
}); });
function handleKeydown(e) {
// Backtick toggle
if( e.keyCode == 192 ) {
menuVisible.update( (current) => {
return !current;
});
}
}
</script> </script>
{#if $menuVisible } {#if $menuVisible }
@@ -37,6 +46,8 @@
</div> </div>
{/if} {/if}
<svelte:window on:keydown={handleKeydown}/>
<style> <style>
.tray-menus { .tray-menus {

View File

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

View File

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

View File

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

View File

@@ -6,20 +6,19 @@ import (
// Runtime is a means for the user to interact with the application at runtime // Runtime is a means for the user to interact with the application at runtime
type Runtime struct { type Runtime struct {
Browser Browser Browser Browser
Events Events Events Events
Window Window Window Window
Dialog Dialog Dialog Dialog
System System System System
Menu Menu Menu Menu
Store *StoreProvider Store *StoreProvider
Log Log Log Log
bus *servicebus.ServiceBus bus *servicebus.ServiceBus
shutdownCallback func()
} }
// New creates a new runtime // New creates a new runtime
func New(serviceBus *servicebus.ServiceBus, shutdownCallback func()) *Runtime { func New(serviceBus *servicebus.ServiceBus) *Runtime {
result := &Runtime{ result := &Runtime{
Browser: newBrowser(), Browser: newBrowser(),
Events: newEvents(serviceBus), Events: newEvents(serviceBus),
@@ -36,11 +35,6 @@ func New(serviceBus *servicebus.ServiceBus, shutdownCallback func()) *Runtime {
// Quit the application // Quit the application
func (r *Runtime) Quit() { func (r *Runtime) Quit() {
// Call back to user's shutdown method if defined
if r.shutdownCallback != nil {
r.shutdownCallback()
}
// Start shutdown of Wails // Start shutdown of Wails
r.bus.Publish("quit", "runtime.Quit()") r.bus.Publish("quit", "runtime.Quit()")
} }

View File

@@ -26,25 +26,20 @@ type Manager struct {
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
// The shutdown callback to notify the user's app that a shutdown
// has started
shutdownCallback func()
// Parent waitgroup // Parent waitgroup
wg *sync.WaitGroup wg *sync.WaitGroup
} }
// NewManager creates a new signal manager // NewManager creates a new signal manager
func NewManager(ctx context.Context, cancel context.CancelFunc, bus *servicebus.ServiceBus, logger *logger.Logger, shutdownCallback func()) (*Manager, error) { func NewManager(ctx context.Context, cancel context.CancelFunc, bus *servicebus.ServiceBus, logger *logger.Logger) (*Manager, error) {
result := &Manager{ result := &Manager{
bus: bus, bus: bus,
logger: logger.CustomLogger("Event Manager"), logger: logger.CustomLogger("Event Manager"),
signalchannel: make(chan os.Signal, 2), signalchannel: make(chan os.Signal, 2),
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
shutdownCallback: shutdownCallback, wg: ctx.Value("waitgroup").(*sync.WaitGroup),
wg: ctx.Value("waitgroup").(*sync.WaitGroup),
} }
return result, nil return result, nil
@@ -67,11 +62,6 @@ func (m *Manager) Start() {
m.logger.Trace("Ctrl+C detected. Shutting down...") m.logger.Trace("Ctrl+C detected. Shutting down...")
m.bus.Publish("quit", "ctrl-c pressed") m.bus.Publish("quit", "ctrl-c pressed")
// Shutdown app first
if m.shutdownCallback != nil {
m.shutdownCallback()
}
// Start shutdown of Wails // Start shutdown of Wails
m.cancel() m.cancel()

View File

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

View File

@@ -34,10 +34,13 @@ 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
func NewRuntime(ctx context.Context, bus *servicebus.ServiceBus, logger *logger.Logger, startupCallback func(*runtime.Runtime), shutdownCallback func()) (*Runtime, error) { func NewRuntime(ctx context.Context, bus *servicebus.ServiceBus, logger *logger.Logger, startupCallback func(*runtime.Runtime)) (*Runtime, error) {
// Subscribe to log messages // Subscribe to log messages
runtimeChannel, err := bus.Subscribe("runtime:") runtimeChannel, err := bus.Subscribe("runtime:")
@@ -52,13 +55,13 @@ func NewRuntime(ctx context.Context, bus *servicebus.ServiceBus, logger *logger.
} }
result := &Runtime{ result := &Runtime{
runtimeChannel: runtimeChannel, runtimeChannel: runtimeChannel,
hooksChannel: hooksChannel, hooksChannel: hooksChannel,
logger: logger.CustomLogger("Runtime Subsystem"), logger: logger.CustomLogger("Runtime Subsystem"),
runtime: runtime.New(bus, shutdownCallback), runtime: runtime.New(bus),
startupCallback: startupCallback, startupCallback: startupCallback,
shutdownCallback: shutdownCallback, ctx: ctx,
ctx: ctx, bus: bus,
} }
return result, nil return result, nil
@@ -80,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

@@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"github.com/wailsapp/wails/v2/internal/colour"
) )
// CLILogger is used by the cli // CLILogger is used by the cli
@@ -51,9 +53,9 @@ func (c *CLILogger) Println(message string, args ...interface{}) {
// Fatal prints the given message then aborts // Fatal prints the given message then aborts
func (c *CLILogger) Fatal(message string, args ...interface{}) { func (c *CLILogger) Fatal(message string, args ...interface{}) {
temp := fmt.Sprintf(message, args...) temp := fmt.Sprintf(message, args...)
_, err := fmt.Fprintln(c.Writer, "FATAL: "+temp) _, err := fmt.Fprintln(c.Writer, colour.Red("FATAL: "+temp))
if err != nil { if err != nil {
println("FATAL: ", err) println(colour.Red("FATAL: " + err.Error()))
} }
os.Exit(1) os.Exit(1)
} }

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

@@ -1,21 +1,44 @@
package keys package keys
import (
"fmt"
"strings"
)
// Modifier is actually a string // Modifier is actually a string
type Modifier string type Modifier string
const ( const (
// CmdOrCtrlKey represents Command on Mac and Control on other platforms // CmdOrCtrlKey represents Command on Mac and Control on other platforms
CmdOrCtrlKey Modifier = "CmdOrCtrl" CmdOrCtrlKey Modifier = "cmdorctrl"
// OptionOrAltKey represents Option on Mac and Alt on other platforms // OptionOrAltKey represents Option on Mac and Alt on other platforms
OptionOrAltKey Modifier = "OptionOrAlt" OptionOrAltKey Modifier = "optionoralt"
// ShiftKey represents the shift key on all systems // ShiftKey represents the shift key on all systems
ShiftKey Modifier = "Shift" ShiftKey Modifier = "shift"
// SuperKey represents Command on Mac and the Windows key on the other platforms // SuperKey represents Command on Mac and the Windows key on the other platforms
SuperKey Modifier = "Super" SuperKey Modifier = "super"
// ControlKey represents the control key on all systems // ControlKey represents the control key on all systems
ControlKey Modifier = "Control" ControlKey Modifier = "ctrl"
) )
var modifierMap = map[string]Modifier{
"cmdorctrl": CmdOrCtrlKey,
"optionoralt": OptionOrAltKey,
"shift": ShiftKey,
"super": SuperKey,
"ctrl": ControlKey,
}
func parseModifier(text string) (*Modifier, error) {
lowertext := strings.ToLower(text)
result, valid := modifierMap[lowertext]
if !valid {
return nil, fmt.Errorf("'%s' is not a valid modifier", text)
}
return &result, nil
}
// Accelerator holds the keyboard shortcut for a menu item // Accelerator holds the keyboard shortcut for a menu item
type Accelerator struct { type Accelerator struct {
Key string Key string
@@ -25,14 +48,14 @@ type Accelerator struct {
// Key creates a standard key Accelerator // Key creates a standard key Accelerator
func Key(key string) *Accelerator { func Key(key string) *Accelerator {
return &Accelerator{ return &Accelerator{
Key: key, Key: strings.ToLower(key),
} }
} }
// CmdOrCtrl creates a 'CmdOrCtrl' Accelerator // CmdOrCtrl creates a 'CmdOrCtrl' Accelerator
func CmdOrCtrl(key string) *Accelerator { func CmdOrCtrl(key string) *Accelerator {
return &Accelerator{ return &Accelerator{
Key: key, Key: strings.ToLower(key),
Modifiers: []Modifier{CmdOrCtrlKey}, Modifiers: []Modifier{CmdOrCtrlKey},
} }
} }
@@ -40,7 +63,7 @@ func CmdOrCtrl(key string) *Accelerator {
// OptionOrAlt creates a 'OptionOrAlt' Accelerator // OptionOrAlt creates a 'OptionOrAlt' Accelerator
func OptionOrAlt(key string) *Accelerator { func OptionOrAlt(key string) *Accelerator {
return &Accelerator{ return &Accelerator{
Key: key, Key: strings.ToLower(key),
Modifiers: []Modifier{OptionOrAltKey}, Modifiers: []Modifier{OptionOrAltKey},
} }
} }
@@ -48,7 +71,7 @@ func OptionOrAlt(key string) *Accelerator {
// Shift creates a 'Shift' Accelerator // Shift creates a 'Shift' Accelerator
func Shift(key string) *Accelerator { func Shift(key string) *Accelerator {
return &Accelerator{ return &Accelerator{
Key: key, Key: strings.ToLower(key),
Modifiers: []Modifier{ShiftKey}, Modifiers: []Modifier{ShiftKey},
} }
} }
@@ -56,7 +79,7 @@ func Shift(key string) *Accelerator {
// Control creates a 'Control' Accelerator // Control creates a 'Control' Accelerator
func Control(key string) *Accelerator { func Control(key string) *Accelerator {
return &Accelerator{ return &Accelerator{
Key: key, Key: strings.ToLower(key),
Modifiers: []Modifier{ControlKey}, Modifiers: []Modifier{ControlKey},
} }
} }
@@ -64,7 +87,7 @@ func Control(key string) *Accelerator {
// Super creates a 'Super' Accelerator // Super creates a 'Super' Accelerator
func Super(key string) *Accelerator { func Super(key string) *Accelerator {
return &Accelerator{ return &Accelerator{
Key: key, Key: strings.ToLower(key),
Modifiers: []Modifier{SuperKey}, Modifiers: []Modifier{SuperKey},
} }
} }

View File

@@ -0,0 +1,90 @@
package keys
import (
"fmt"
"strconv"
"strings"
"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"})
func parseKey(key string) (string, bool) {
// Lowercase!
key = strings.ToLower(key)
// Check special case
if key == "plus" {
return "+", true
}
// Handle named keys
if namedKeys.Contains(key) {
return key, true
}
// Check we only have a single character
if len(key) != 1 {
return "", false
}
runeKey := rune(key[0])
// This may be too inclusive
if strconv.IsPrint(runeKey) {
return key, true
}
return "", false
}
func Parse(shortcut string) (*Accelerator, error) {
var result Accelerator
// Split the shortcut by +
components := strings.Split(shortcut, "+")
// If we only have one it should be a key
// We require components
if len(components) == 0 {
return nil, fmt.Errorf("no components given to validateComponents")
}
// Keep track of modifiers we have processed
var modifiersProcessed slicer.StringSlicer
// Check components
for index, component := range components {
// If last component
if index == len(components)-1 {
processedkey, validKey := parseKey(component)
if !validKey {
return nil, fmt.Errorf("'%s' is not a valid key", component)
}
result.Key = processedkey
continue
}
// Not last component - needs to be modifier
lowercaseComponent := strings.ToLower(component)
thisModifier, valid := modifierMap[lowercaseComponent]
if !valid {
return nil, fmt.Errorf("'%s' is not a valid modifier", component)
}
// Needs to be unique
if modifiersProcessed.Contains(lowercaseComponent) {
return nil, fmt.Errorf("Modifier '%s' is defined twice for shortcut: %s", component, shortcut)
}
// Save this data
result.Modifiers = append(result.Modifiers, thisModifier)
modifiersProcessed.Add(lowercaseComponent)
}
return &result, nil
}

View File

@@ -0,0 +1,38 @@
package keys
import (
"testing"
"github.com/matryer/is"
)
func TestParse(t *testing.T) {
i := is.New(t)
type args struct {
Input string
Expected *Accelerator
}
gooddata := []args{
{"CmdOrCtrl+A", CmdOrCtrl("A")},
{"SHIFT+.", Shift(".")},
{"CTRL+plus", Control("+")},
{"CTRL+SHIFT+escApe", Combo("escape", ControlKey, ShiftKey)},
{";", Key(";")},
{"Super+Tab", Super("tab")},
{"OptionOrAlt+Page Down", OptionOrAlt("Page Down")},
}
for _, tt := range gooddata {
result, err := Parse(tt.Input)
i.NoErr(err)
i.Equal(result, tt.Expected)
}
baddata := []string{"CmdOrCrl+A", "SHIT+.", "CTL+plus", "CTRL+SHIF+esApe", "escap", "Sper+Tab", "OptionOrAlt"}
for _, d := range baddata {
result, err := Parse(d)
i.True(err != nil)
i.Equal(result, "")
}
}

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

@@ -2,6 +2,14 @@ package mac
import "github.com/wailsapp/wails/v2/pkg/menu" import "github.com/wailsapp/wails/v2/pkg/menu"
type ActivationPolicy int
const (
NSApplicationActivationPolicyRegular ActivationPolicy = 0
NSApplicationActivationPolicyAccessory ActivationPolicy = 1
NSApplicationActivationPolicyProhibited ActivationPolicy = 2
)
// Options are options specific to Mac // Options are options specific to Mac
type Options struct { type Options struct {
TitleBar *TitleBar TitleBar *TitleBar
@@ -11,4 +19,6 @@ type Options struct {
Menu *menu.Menu Menu *menu.Menu
TrayMenus []*menu.TrayMenu TrayMenus []*menu.TrayMenu
ContextMenus []*menu.ContextMenu ContextMenus []*menu.ContextMenu
ActivationPolicy ActivationPolicy
URLHandlers map[string]func(string)
} }