Compare commits

..

76 Commits

Author SHA1 Message Date
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
65 changed files with 1690 additions and 759 deletions

View File

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

View File

@@ -57,6 +57,11 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
keepAssets := false
command.BoolFlag("k", "Keep generated assets", &keepAssets)
appleIdentity := ""
if runtime.GOOS == "darwin" {
command.StringFlag("sign", "Signs your app with the given identity.", &appleIdentity)
}
command.Action(func() error {
// Create logger
@@ -72,6 +77,11 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
app.PrintBanner()
}
// Ensure package is used with apple identity
if appleIdentity != "" && pack == false {
return fmt.Errorf("must use `-package` flag when using `-sign`")
}
task := fmt.Sprintf("Building %s Application", strings.Title(outputType))
logger.Println(task)
logger.Println(strings.Repeat("-", len(task)))
@@ -84,14 +94,15 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
// Create BuildOptions
buildOptions := &build.Options{
Logger: logger,
OutputType: outputType,
Mode: mode,
Pack: pack,
Platform: platform,
LDFlags: ldflags,
Compiler: compilerCommand,
KeepAssets: keepAssets,
Logger: logger,
OutputType: outputType,
Mode: mode,
Pack: pack,
Platform: platform,
LDFlags: ldflags,
Compiler: compilerCommand,
KeepAssets: keepAssets,
AppleIdentity: appleIdentity,
}
return doBuild(buildOptions)

View File

@@ -1,30 +1,41 @@
package dev
import (
"context"
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
"github.com/wailsapp/wails/v2/pkg/commands/build"
"github.com/wailsapp/wails/v2/internal/process"
"github.com/wzshiming/ctc"
"github.com/wailsapp/wails/v2/internal/fs"
"github.com/wailsapp/wails/v2/internal/colour"
"github.com/fsnotify/fsnotify"
"github.com/leaanthony/clir"
"github.com/wailsapp/wails/v2/internal/fs"
"github.com/wailsapp/wails/v2/internal/process"
"github.com/wailsapp/wails/v2/pkg/clilogger"
"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
func AddSubcommand(app *clir.Cli, w io.Writer) error {
@@ -42,6 +53,13 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
extensions := "go"
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 {
// Create logger
@@ -49,262 +67,215 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
app.PrintBanner()
// TODO: Check you are in a project directory
watcher, err := fsnotify.NewWatcher()
if err != nil {
return err
}
defer watcher.Close()
var debugBinaryProcess *process.Process = nil
var extensionsThatTriggerARebuild = strings.Split(extensions, ",")
reloader, err := NewReloader(logger, extensionsThatTriggerARebuild, ldflags, compilerCommand)
// Setup signal handler
quitChannel := make(chan os.Signal, 1)
signal.Notify(quitChannel, os.Interrupt, os.Kill, syscall.SIGTERM)
debounceQuit := make(chan bool, 1)
// Do initial build
logger.Println("Building application for development...")
newProcess, err := restartApp(logger, "dev", ldflags, compilerCommand, debugBinaryProcess, loglevel)
if newProcess != nil {
debugBinaryProcess = newProcess
}
if err != nil {
return err
}
go debounce(100*time.Millisecond, watcher.Events, debounceQuit, func(event fsnotify.Event) {
// logger.Println("event: %+v", event)
// Check for new directories
if event.Op&fsnotify.Create == fsnotify.Create {
// If this is a folder, add it to our watch list
if fs.DirExists(event.Name) {
if !strings.Contains(event.Name, "node_modules") {
err := watcher.Add(event.Name)
if err != nil {
logger.Fatal("%s", err.Error())
}
LogGreen("[New Directory] Watching new directory: %s", event.Name)
}
}
return
}
// Check for file writes
if event.Op&fsnotify.Write == fsnotify.Write {
var rebuild bool = false
// Iterate all file patterns
for _, pattern := range extensionsThatTriggerARebuild {
if strings.HasSuffix(event.Name, pattern) {
rebuild = true
break
}
}
if !rebuild {
if showWarnings {
LogDarkYellow("[File change] %s did not match extension list (%s)", event.Name, extensions)
}
return
}
LogGreen("[Attempting rebuild] %s updated", event.Name)
// Do a rebuild
// Try and build the app
newBinaryProcess, err := restartApp(logger, "dev", ldflags, compilerCommand, debugBinaryProcess, loglevel)
if err != nil {
fmt.Printf("Error during build: %s", err.Error())
return
}
// If we have a new process, save it
if newBinaryProcess != nil {
debugBinaryProcess = newBinaryProcess
}
}
})
// Get project dir
projectDir, err := os.Getwd()
if err != nil {
return err
}
// Start
err = reloader.Start()
// Get all subdirectories
dirs, err := fs.GetSubdirectories(projectDir)
if err != nil {
println("ERRRRRRRRRR: %+v", err)
return err
}
logger.Println("\nDevelopment mode exited")
LogGreen("Watching (sub)/directory: %s", projectDir)
return err
// Setup a watcher for non-node_modules directories
dirs.Each(func(dir string) {
if strings.Contains(dir, "node_modules") {
return
}
// Ignore build directory
if strings.HasPrefix(dir, filepath.Join(projectDir, "build")) {
return
}
err = watcher.Add(dir)
if err != nil {
logger.Fatal(err.Error())
}
})
// Wait until we get a quit signal
quit := false
for quit == false {
select {
case <-quitChannel:
LogGreen("\nCaught quit")
// Notify debouncer to quit
debounceQuit <- true
quit = true
}
}
// Kill the current program if running
if debugBinaryProcess != nil {
err := debugBinaryProcess.Kill()
if err != nil {
return err
}
}
LogGreen("Development mode exited")
return nil
})
return nil
}
type Reloader struct {
// Main context
ctx context.Context
// Signal context
signalContext context.Context
// notify
watcher *fsnotify.Watcher
// Logger
logger *clilogger.CLILogger
// Extensions to listen for
extensionsThatTriggerARebuild []string
// The binary we are running
binary *process.Process
// options
ldflags string
compiler string
}
func NewReloader(logger *clilogger.CLILogger, extensionsThatTriggerARebuild []string, ldFlags string, compiler string) (*Reloader, error) {
var result Reloader
// Create context
result.ctx = context.Background()
// Signal context (we don't need cancel)
signalContext, _ := signal.NotifyContext(result.ctx, os.Interrupt, os.Kill, syscall.SIGTERM)
result.signalContext = signalContext
// Create watcher
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, err
}
result.watcher = watcher
// Logger
result.logger = logger
// Extensions
result.extensionsThatTriggerARebuild = extensionsThatTriggerARebuild
// Options
result.ldflags = ldFlags
result.compiler = compiler
return &result, nil
}
func (r *Reloader) Start() error {
err := r.rebuildBinary()
if err != nil {
return err
}
// Get project dir
dir, err := os.Getwd()
if err != nil {
return err
}
// Get all subdirectories
dirs, err := fs.GetSubdirectories(dir)
if err != nil {
return err
}
// Setup a watcher for non-node_modules directories
r.logger.Println("Watching (sub)directories: %s", dir)
dirs.Each(func(dir string) {
if strings.Contains(dir, "node_modules") {
return
}
err = r.watcher.Add(dir)
if err != nil {
r.logger.Fatal(err.Error())
}
})
// Main loop
// Credit: https://drailing.net/2018/01/debounce-function-for-golang/
func debounce(interval time.Duration, input chan fsnotify.Event, quitChannel chan bool, cb func(arg fsnotify.Event)) {
var item fsnotify.Event
timer := time.NewTimer(interval)
exit:
for {
select {
case <-r.signalContext.Done():
if r.binary != nil {
println("Binary is not nil - kill")
return r.binary.Kill()
}
return nil
case event := <-r.watcher.Events:
err := r.processWatcherEvent(event)
if err != nil {
println("error from processWatcherEvent. Calling cancel()")
println("Calling kill")
return r.binary.Kill()
case item = <-input:
timer.Reset(interval)
case <-timer.C:
if item.Name != "" {
cb(item)
}
case <-quitChannel:
break exit
}
}
}
func (r *Reloader) processWatcherEvent(event fsnotify.Event) error {
func restartApp(logger *clilogger.CLILogger, outputType string, ldflags string, compilerCommand string, debugBinaryProcess *process.Process, loglevel string) (*process.Process, error) {
// Check for new directories
if event.Op&fsnotify.Create == fsnotify.Create {
// If this is a folder, add it to our watch list
if fs.DirExists(event.Name) {
if !strings.Contains(event.Name, "node_modules") {
err := r.watcher.Add(event.Name)
if err != nil {
return err
}
r.logger.Println("Watching directory: %s", event.Name)
}
}
return nil
}
// Check for file writes
if event.Op&fsnotify.Write == fsnotify.Write {
var rebuild bool
// Iterate all file patterns
for _, pattern := range r.extensionsThatTriggerARebuild {
if strings.HasSuffix(event.Name, pattern) {
rebuild = true
}
}
if !rebuild {
return nil
}
r.logger.Println("\n%s[Build triggered] %s %s", ctc.ForegroundGreen|ctc.ForegroundBright, event.Name, ctc.Reset)
return r.rebuildBinary()
}
return nil
}
func (r *Reloader) rebuildBinary() error {
// rebuild binary
binary, err := r.buildApp()
appBinary, err := buildApp(logger, outputType, ldflags, compilerCommand)
println()
if err != nil {
return err
LogRed("Build error - continuing to run current version")
LogDarkYellow(err.Error())
return nil, nil
}
// Kill current binary if running
if r.binary != nil {
err = r.binary.Kill()
if err != nil {
return err
// Kill existing binary if need be
if debugBinaryProcess != nil {
killError := debugBinaryProcess.Kill()
if killError != nil {
logger.Fatal("Unable to kill debug binary (PID: %d)!", debugBinaryProcess.PID())
}
debugBinaryProcess = nil
}
newProcess := process.NewProcess(r.ctx, r.logger, binary)
// TODO: Generate `backend.js`
// Start up new binary
newProcess := process.NewProcess(logger, appBinary, "-loglevel", loglevel)
err = newProcess.Start()
if err != nil {
return err
// Remove binary
deleteError := fs.DeleteFile(appBinary)
if deleteError != nil {
logger.Fatal("Unable to delete app binary: " + appBinary)
}
logger.Fatal("Unable to start application: %s", err.Error())
}
// Ensure process runs correctly
return nil
return newProcess, nil
}
//func restartApp(logger *clilogger.CLILogger, outputType string, ldflags string, compilerCommand string, debugBinaryProcess *process.Process) (*process.Process, error) {
//
// appBinary, err := buildApp(logger, outputType, ldflags, compilerCommand)
// println()
// if err != nil {
// logger.Fatal(err.Error())
// return nil, errors.Wrap(err, "Build Failed:")
// }
// logger.Println("Build new binary: %s", appBinary)
//
// // Kill existing binary if need be
// if debugBinaryProcess != nil {
// killError := debugBinaryProcess.Kill()
//
// if killError != nil {
// logger.Fatal("Unable to kill debug binary (PID: %d)!", debugBinaryProcess.PID())
// }
//
// debugBinaryProcess = nil
// }
//
// // TODO: Generate `backend.js`
//
// // Start up new binary
// newProcess := process.NewProcess(logger, appBinary)
// err = newProcess.Start()
// if err != nil {
// // Remove binary
// deleteError := fs.DeleteFile(appBinary)
// if deleteError != nil {
// logger.Fatal("Unable to delete app binary: " + appBinary)
// }
// logger.Fatal("Unable to start application: %s", err.Error())
// }
//
// // Check if port is open
// timeout := time.Second
// conn, err := net.DialTimeout("tcp", net.JoinHostPort("host", port), timeout)
// if err != nil {
// return
// }
// newProcess.Running
// return newProcess, nil
//}
// buildapp attempts to compile the application
// It returns the path to the new binary or an error
func (r *Reloader) buildApp() (string, error) {
func buildApp(logger *clilogger.CLILogger, outputType string, ldflags string, compilerCommand string) (string, error) {
// Create random output file
outputFile := fmt.Sprintf("debug-%d", time.Now().Unix())
outputFile := fmt.Sprintf("dev-%d", time.Now().Unix())
// Create BuildOptions
buildOptions := &build.Options{
Logger: r.logger,
OutputType: "dev",
Logger: logger,
OutputType: outputType,
Mode: build.Debug,
Pack: false,
Platform: runtime.GOOS,
LDFlags: r.ldflags,
Compiler: r.compiler,
LDFlags: ldflags,
Compiler: compilerCommand,
OutputFile: outputFile,
IgnoreFrontend: true,
}

View File

@@ -18,14 +18,14 @@ import (
func AddSubcommand(app *clir.Cli, w io.Writer, currentVersion string) error {
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
var prereleaseRequired bool
command.BoolFlag("pre", "Update to latest Prerelease", &prereleaseRequired)
command.BoolFlag("pre", "Update CLI to latest Prerelease", &prereleaseRequired)
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 {
@@ -143,7 +143,7 @@ func updateToVersion(logger *clilogger.CLILogger, targetVersion *github.Semantic
}
fmt.Println()
logger.Print("Installing Wails " + desiredVersion + "...")
logger.Print("Installing Wails CLI " + desiredVersion + "...")
// Run command in non module directory
homeDir, err := os.UserHomeDir()
@@ -158,7 +158,7 @@ func updateToVersion(logger *clilogger.CLILogger, targetVersion *github.Semantic
return err
}
fmt.Println()
logger.Println("Wails updated to " + desiredVersion)
logger.Println("Wails CLI updated to " + desiredVersion)
return nil
}

View File

@@ -4,7 +4,7 @@ import (
"fmt"
"os"
"github.com/wzshiming/ctc"
"github.com/wailsapp/wails/v2/internal/colour"
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/update"
@@ -22,27 +22,15 @@ func fatal(message string) {
os.Exit(1)
}
func col(colour ctc.Color, text string) string {
return fmt.Sprintf("%s%s%s", colour, text, ctc.Reset)
}
func Yellow(str string) string {
return col(ctc.ForegroundBrightYellow, str)
}
func Red(str string) string {
return col(ctc.ForegroundBrightRed, str)
}
func banner(cli *clir.Cli) string {
return fmt.Sprintf("%s %s - Go/HTML Application Framework", Yellow("Wails"), Red(version))
func banner(_ *clir.Cli) string {
return fmt.Sprintf("%s %s", colour.Yellow("Wails CLI"), colour.DarkRed(version))
}
func main() {
var err error
app := clir.NewCli("Wails", "Go/HTML Application Framework", version)
app := clir.NewCli("Wails", "Go/HTML Appkit", version)
app.SetBannerFunction(banner)

View File

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

View File

@@ -20,10 +20,9 @@ require (
github.com/tdewolff/minify v2.3.6+incompatible
github.com/tdewolff/parse v2.3.4+incompatible // indirect
github.com/tdewolff/test v1.0.6 // indirect
github.com/wzshiming/ctc v1.2.3 // indirect
github.com/wzshiming/ctc v1.2.3
github.com/xyproto/xpm v1.2.1
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/tools v0.0.0-20200902012652-d1954cc86c82
nhooyr.io/websocket v1.8.6

View File

@@ -96,8 +96,6 @@ 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/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-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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@@ -20,10 +20,10 @@ func (a *App) Init() error {
}
// 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()
if len(*greeting) > 0 {
switch strings.ToLower(*greeting) {
if len(*loglevel) > 0 {
switch strings.ToLower(*loglevel) {
case "trace":
a.logger.SetLogLevel(logger.TRACE)
case "info":

View File

@@ -35,6 +35,7 @@ type App struct {
//binding *subsystem.Binding
call *subsystem.Call
menu *subsystem.Menu
url *subsystem.URL
dispatcher *messagedispatcher.Dispatcher
menuManager *menumanager.Manager
@@ -117,14 +118,6 @@ func (a *App) Run() error {
parentContext := context.WithValue(context.Background(), "waitgroup", &subsystemWaitGroup)
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
a.servicebus.Debug()
err = a.servicebus.Start()
@@ -132,7 +125,7 @@ func (a *App) Run() error {
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 {
return err
}
@@ -168,6 +161,19 @@ func (a *App) Run() error {
return err
}
if a.options.Mac.URLHandlers != nil {
// Start the url handler subsystem
url, err := subsystem.NewURL(a.servicebus, a.logger, a.options.Mac.URLHandlers)
if err != nil {
return err
}
a.url = url
err = a.url.Start()
if err != nil {
return err
}
}
// Start the eventing subsystem
eventsubsystem, err := subsystem.NewEvent(ctx, a.servicebus, a.logger)
if err != nil {
@@ -207,6 +213,14 @@ func (a *App) Run() error {
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)
a.logger.Trace("Ffenestri.Run() exited")
if err != nil {
@@ -231,5 +245,10 @@ func (a *App) Run() error {
return err
}
// Shutdown callback
if a.shutdownCallback != nil {
a.shutdownCallback()
}
return nil
}

View File

@@ -66,7 +66,6 @@ func CreateApp(appoptions *options.App) (*App, error) {
// Set up logger
myLogger := logger.New(appoptions.Logger)
myLogger.SetLogLevel(appoptions.LogLevel)
// Create the menu manager
menuManager := menumanager.NewManager()
@@ -119,14 +118,6 @@ func (a *App) Run() error {
parentContext := context.WithValue(context.Background(), "waitgroup", &subsystemWaitGroup)
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
a.servicebus.Debug()
err = a.servicebus.Start()
@@ -134,7 +125,7 @@ func (a *App) Run() error {
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 {
return err
}
@@ -212,6 +203,14 @@ func (a *App) Run() error {
// Generate backend.js
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)
a.logger.Trace("Bridge.Run() exited")
if err != nil {
@@ -236,6 +235,10 @@ func (a *App) Run() error {
return err
}
// Shutdown callback
if a.shutdownCallback != nil {
a.shutdownCallback()
}
return nil
}

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -24,6 +24,8 @@ struct hashmap_s dialogIconCache;
// Dispatch Method
typedef void (^dispatchMethod)(void);
TrayMenuStore *TrayMenuStoreSingleton;
// dispatch will execute the given `func` pointer
void dispatch(dispatchMethod func) {
dispatch_async(dispatch_get_main_queue(), func);
@@ -46,6 +48,18 @@ int hashmap_log(void *const context, struct hashmap_element_s *const e) {
return 0;
}
void filelog(const char *message) {
FILE *fp = fopen("/tmp/wailslog.txt", "ab");
if (fp != NULL)
{
fputs(message, fp);
fclose(fp);
}
}
// The delegate class for tray menus
Class trayMenuDelegateClass;
// Utility function to visualise a hashmap
void dumpHashmap(const char *name, struct hashmap_s *hashmap) {
printf("%s = { ", name);
@@ -79,6 +93,7 @@ struct Application {
id mouseEvent;
id mouseDownMonitor;
id mouseUpMonitor;
int activationPolicy;
// Window Data
const char *title;
@@ -112,13 +127,12 @@ struct Application {
int useToolBar;
int hideToolbarSeparator;
int windowBackgroundIsTranslucent;
int hasURLHandlers;
const char *startupURL;
// Menu
Menu *applicationMenu;
// Tray
TrayMenuStore* trayMenuStore;
// Context Menus
ContextMenuStore *contextMenuStore;
@@ -131,6 +145,9 @@ struct Application {
// shutting down flag
bool shuttingDown;
// Running flag
bool running;
};
// Debug works like sprintf but mutes if the global debug flag is true
@@ -253,7 +270,7 @@ void Hide(struct Application *app) {
if( app->shuttingDown ) return;
ON_MAIN_THREAD(
msg(app->application, s("hide:"))
msg(app->mainWindow, s("orderOut:"));
);
}
@@ -291,8 +308,19 @@ void messageHandler(id self, SEL cmd, id contentController, id message) {
// TODO: Check this actually does reduce flicker
msg(app->config, s("setValue:forKey:"), msg(c("NSNumber"), s("numberWithBool:"), 0), str("suppressesIncrementalRendering"));
// We are now running!
app->running = true;
// Notify backend we are ready (system startup)
app->sendMessageToBackend("SS");
const char *readyMessage = "SS";
if( app->startupURL == NULL ) {
app->sendMessageToBackend("SS");
return;
}
readyMessage = concat("SS", app->startupURL);
app->sendMessageToBackend(readyMessage);
MEMFREE(readyMessage);
} else if( strcmp(name, "windowDrag") == 0 ) {
// Guard against null events
@@ -390,6 +418,11 @@ void ExecJS(struct Application *app, const char *js) {
void willFinishLaunching(id self, SEL cmd, id sender) {
struct Application *app = (struct Application *) objc_getAssociatedObject(self, "application");
// If there are URL Handlers, register a listener for them
if( app->hasURLHandlers ) {
id eventManager = msg(c("NSAppleEventManager"), s("sharedAppleEventManager"));
msg(eventManager, s("setEventHandler:andSelector:forEventClass:andEventID:"), self, s("getUrl:withReplyEvent:"), kInternetEventClass, kAEGetURL);
}
messageFromWindowCallback("Ej{\"name\":\"wails:launched\",\"data\":[]}");
}
@@ -414,12 +447,6 @@ void themeChanged(id self, SEL cmd, id sender) {
}
}
// void willFinishLaunching(id self) {
// struct Application *app = (struct Application *) objc_getAssociatedObject(self, "application");
// Debug(app, "willFinishLaunching called!");
// }
int releaseNSObject(void *const context, struct hashmap_element_s *const e) {
msg(e->data, s("release"));
return -1;
@@ -452,6 +479,10 @@ void DestroyApplication(struct Application *app) {
Debug(app, "Almost a double free for app->bindings");
}
if( app->startupURL != NULL ) {
MEMFREE(app->startupURL);
}
// Remove mouse monitors
if( app->mouseDownMonitor != NULL ) {
msg( c("NSEvent"), s("removeMonitor:"), app->mouseDownMonitor);
@@ -466,7 +497,7 @@ void DestroyApplication(struct Application *app) {
}
// Delete the tray menu store
DeleteTrayMenuStore(app->trayMenuStore);
DeleteTrayMenuStore(TrayMenuStoreSingleton);
// Delete the context menu store
DeleteContextMenuStore(app->contextMenuStore);
@@ -498,31 +529,6 @@ void DestroyApplication(struct Application *app) {
Debug(app, "Finished Destroying Application");
}
// Quit will stop the cocoa application and free up all the memory
// used by the application
void Quit(struct Application *app) {
Debug(app, "Quit Called");
ON_MAIN_THREAD (
// Terminate app
msg(app->application, s("stop:"), NULL);
id fakeevent = msg(c("NSEvent"),
s("otherEventWithType:location:modifierFlags:timestamp:windowNumber:context:subtype:data1:data2:"),
15, // Type
msg(c("CGPoint"), s("init:x:y:"), 0, 0), // location
0, // flags
0, // timestamp
0, // window
NULL, // context
0, // subtype
0, // data1
0 // data2
);
msg(c("NSApp"), s("postEvent:atStart:"), fakeevent, true);
// msg(c(app->mainWindow), s("performClose:"))
);
}
// SetTitle sets the main window title to the given string
void SetTitle(struct Application *app, const char *title) {
// Guard against calling during shutdown
@@ -1058,7 +1064,7 @@ void AddTrayMenu(struct Application *app, const char *trayMenuJSON) {
// Guard against calling during shutdown
if( app->shuttingDown ) return;
AddTrayMenuToStore(app->trayMenuStore, trayMenuJSON);
AddTrayMenuToStore(TrayMenuStoreSingleton, trayMenuJSON);
}
void SetTrayMenu(struct Application *app, const char* trayMenuJSON) {
@@ -1067,7 +1073,13 @@ void SetTrayMenu(struct Application *app, const char* trayMenuJSON) {
if( app->shuttingDown ) return;
ON_MAIN_THREAD(
UpdateTrayMenuInStore(app->trayMenuStore, trayMenuJSON);
UpdateTrayMenuInStore(TrayMenuStoreSingleton, trayMenuJSON);
);
}
void DeleteTrayMenuByID(struct Application *app, const char *id) {
ON_MAIN_THREAD(
DeleteTrayMenuInStore(TrayMenuStoreSingleton, id);
);
}
@@ -1076,7 +1088,7 @@ void UpdateTrayMenuLabel(struct Application* app, const char* JSON) {
if( app->shuttingDown ) return;
ON_MAIN_THREAD(
UpdateTrayMenuLabelInStore(app->trayMenuStore, JSON);
UpdateTrayMenuLabelInStore(TrayMenuStoreSingleton, JSON);
);
}
@@ -1137,7 +1149,7 @@ void processDecorations(struct Application *app) {
void createApplication(struct Application *app) {
id application = msg(c("NSApplication"), s("sharedApplication"));
app->application = application;
msg(application, s("setActivationPolicy:"), 0);
msg(application, s("setActivationPolicy:"), app->activationPolicy);
}
void DarkModeEnabled(struct Application *app, const char *callbackID) {
@@ -1161,27 +1173,59 @@ void DarkModeEnabled(struct Application *app, const char *callbackID) {
);
}
void getURL(id self, SEL selector, id event, id replyEvent) {
struct Application *app = (struct Application *)objc_getAssociatedObject(self, "application");
id desc = msg(event, s("paramDescriptorForKeyword:"), keyDirectObject);
id url = msg(desc, s("stringValue"));
const char* curl = cstr(url);
if( curl == NULL ) {
return;
}
// If this was an incoming URL, but we aren't running yet
// save it to return when we complete
if( app->running != true ) {
app->startupURL = STRCOPY(curl);
return;
}
const char* message = concat("UC", curl);
messageFromWindowCallback(message);
MEMFREE(message);
}
void openURLs(id self, SEL selector, id event) {
filelog("\n\nI AM HERE!!!!!\n\n");
}
void createDelegate(struct Application *app) {
// Define delegate
Class delegateClass = objc_allocateClassPair((Class) c("NSObject"), "AppDelegate", 0);
bool resultAddProtoc = class_addProtocol(delegateClass, objc_getProtocol("NSApplicationDelegate"));
class_addMethod(delegateClass, s("applicationShouldTerminateAfterLastWindowClosed:"), (IMP) no, "c@:@");
// class_addMethod(delegateClass, s("applicationWillTerminate:"), (IMP) closeWindow, "v@:@");
class_addMethod(delegateClass, s("applicationWillFinishLaunching:"), (IMP) willFinishLaunching, "v@:@");
// Define delegate
Class appDelegate = objc_allocateClassPair((Class) c("NSResponder"), "AppDelegate", 0);
class_addProtocol(appDelegate, objc_getProtocol("NSTouchBarProvider"));
class_addMethod(appDelegate, s("applicationShouldTerminateAfterLastWindowClosed:"), (IMP) no, "c@:@");
class_addMethod(appDelegate, s("applicationWillFinishLaunching:"), (IMP) willFinishLaunching, "v@:@");
// All Menu Items use a common callback
class_addMethod(delegateClass, s("menuItemCallback:"), (IMP)menuItemCallback, "v@:@");
class_addMethod(appDelegate, s("menuItemCallback:"), (IMP)menuItemCallback, "v@:@");
// If there are URL Handlers, register the callback method
if( app->hasURLHandlers ) {
class_addMethod(appDelegate, s("getUrl:withReplyEvent:"), (IMP) getURL, "i@:@@");
}
// Script handler
class_addMethod(delegateClass, s("userContentController:didReceiveScriptMessage:"), (IMP) messageHandler, "v@:@@");
objc_registerClassPair(delegateClass);
class_addMethod(appDelegate, s("userContentController:didReceiveScriptMessage:"), (IMP) messageHandler, "v@:@@");
objc_registerClassPair(appDelegate);
// Create delegate
id delegate = msg((id)delegateClass, s("new"));
id delegate = msg((id)appDelegate, s("new"));
objc_setAssociatedObject(delegate, "application", (id)app, OBJC_ASSOCIATION_ASSIGN);
// Theme change listener
class_addMethod(delegateClass, s("themeChanged:"), (IMP) themeChanged, "v@:@@");
class_addMethod(appDelegate, s("themeChanged:"), (IMP) themeChanged, "v@:@@");
// Get defaultCenter
id defaultCenter = msg(c("NSDistributedNotificationCenter"), s("defaultCenter"));
@@ -1193,15 +1237,21 @@ void createDelegate(struct Application *app) {
}
bool windowShouldClose(id self, SEL cmd, id sender) {
msg(sender, s("orderBack:"));
msg(sender, s("orderOut:"));
return false;
}
}
bool windowShouldExit(id self, SEL cmd, id sender) {
msg(sender, s("orderOut:"));
messageFromWindowCallback("WC");
return false;
}
void createMainWindow(struct Application *app) {
// Create main window
id mainWindow = ALLOC("NSWindow");
mainWindow = msg(mainWindow, s("initWithContentRect:styleMask:backing:defer:"),
CGRectMake(0, 0, app->width, app->height), app->decorations, NSBackingStoreBuffered, NO);
CGRectMake(0, 0, app->width, app->height), app->decorations, NSBackingStoreBuffered, NO);
msg(mainWindow, s("autorelease"));
// Set Appearance
@@ -1215,14 +1265,16 @@ void createMainWindow(struct Application *app) {
msg(mainWindow, s("setTitlebarAppearsTransparent:"), app->titlebarAppearsTransparent ? YES : NO);
msg(mainWindow, s("setTitleVisibility:"), app->hideTitle);
if( app->hideWindowOnClose ) {
// Create window delegate to override windowShouldClose
Class delegateClass = objc_allocateClassPair((Class) c("NSObject"), "WindowDelegate", 0);
bool resultAddProtoc = class_addProtocol(delegateClass, objc_getProtocol("NSWindowDelegate"));
// Create window delegate to override windowShouldClose
Class delegateClass = objc_allocateClassPair((Class) c("NSObject"), "WindowDelegate", 0);
bool resultAddProtoc = class_addProtocol(delegateClass, objc_getProtocol("NSWindowDelegate"));
if( app->hideWindowOnClose ) {
class_replaceMethod(delegateClass, s("windowShouldClose:"), (IMP) windowShouldClose, "v@:@");
app->windowDelegate = msg((id)delegateClass, s("new"));
msg(mainWindow, s("setDelegate:"), app->windowDelegate);
}
} else {
class_replaceMethod(delegateClass, s("windowShouldClose:"), (IMP) windowShouldExit, "v@:@");
}
app->windowDelegate = msg((id)delegateClass, s("new"));
msg(mainWindow, s("setDelegate:"), app->windowDelegate);
app->mainWindow = mainWindow;
}
@@ -1641,6 +1693,35 @@ void processUserDialogIcons(struct Application *app) {
}
void TrayMenuWillOpen(id self, SEL selector, id menu) {
// Extract tray menu id from menu
id trayMenuIDStr = objc_getAssociatedObject(menu, "trayMenuID");
const char* trayMenuID = cstr(trayMenuIDStr);
const char *message = concat("Mo", trayMenuID);
messageFromWindowCallback(message);
MEMFREE(message);
}
void TrayMenuDidClose(id self, SEL selector, id menu) {
// Extract tray menu id from menu
id trayMenuIDStr = objc_getAssociatedObject(menu, "trayMenuID");
const char* trayMenuID = cstr(trayMenuIDStr);
const char *message = concat("Mc", trayMenuID);
messageFromWindowCallback(message);
MEMFREE(message);
}
void createTrayMenuDelegate() {
// Define delegate
trayMenuDelegateClass = objc_allocateClassPair((Class) c("NSObject"), "MenuDelegate", 0);
class_addProtocol(trayMenuDelegateClass, objc_getProtocol("NSMenuDelegate"));
class_addMethod(trayMenuDelegateClass, s("menuWillOpen:"), (IMP) TrayMenuWillOpen, "v@:@");
class_addMethod(trayMenuDelegateClass, s("menuDidClose:"), (IMP) TrayMenuDidClose, "v@:@");
// Script handler
objc_registerClassPair(trayMenuDelegateClass);
}
void Run(struct Application *app, int argc, char **argv) {
@@ -1653,6 +1734,9 @@ void Run(struct Application *app, int argc, char **argv) {
// Define delegate
createDelegate(app);
// Define tray delegate
createTrayMenuDelegate();
// Create the main window
createMainWindow(app);
@@ -1823,7 +1907,7 @@ void Run(struct Application *app, int argc, char **argv) {
}
// Setup initial trays
ShowTrayMenusInStore(app->trayMenuStore);
ShowTrayMenusInStore(TrayMenuStoreSingleton);
// Process dialog icons
processUserDialogIcons(app);
@@ -1837,6 +1921,23 @@ void Run(struct Application *app, int argc, char **argv) {
MEMFREE(internalCode);
}
void SetActivationPolicy(struct Application* app, int policy) {
app->activationPolicy = policy;
}
void HasURLHandlers(struct Application* app) {
app->hasURLHandlers = 1;
}
// Quit will stop the cocoa application and free up all the memory
// used by the application
void Quit(struct Application *app) {
Debug(app, "Quit Called");
msg(app->application, s("stop:"), NULL);
SetSize(app, 0, 0);
Show(app);
Hide(app);
}
void* NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden, int logLevel, int hideWindowOnClose) {
@@ -1883,7 +1984,7 @@ void* NewApplication(const char *title, int width, int height, int resizable, in
result->applicationMenu = NULL;
// Tray
result->trayMenuStore = NewTrayMenuStore();
TrayMenuStoreSingleton = NewTrayMenuStore();
// Context Menus
result->contextMenuStore = NewContextMenuStore();
@@ -1899,6 +2000,14 @@ void* NewApplication(const char *title, int width, int height, int resizable, in
result->shuttingDown = false;
result->activationPolicy = NSApplicationActivationPolicyRegular;
result->hasURLHandlers = 0;
result->startupURL = NULL;
result->running = false;
return (void*) result;
}

View File

@@ -48,6 +48,9 @@ func (a *Application) processPlatformSettings() error {
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
if mac.WebviewIsTransparent {
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
}

View File

@@ -14,6 +14,10 @@
// Macros to make it slightly more sane
#define msg objc_msgSend
#define kInternetEventClass 'GURL'
#define kAEGetURL 'GURL'
#define keyDirectObject '----'
#define c(str) (id)objc_getClass(str)
#define s(str) sel_registerName(str)
#define u(str) sel_getUid(str)
@@ -66,6 +70,10 @@
#define NSControlStateValueOff 0
#define NSControlStateValueOn 1
#define NSApplicationActivationPolicyRegular 0
#define NSApplicationActivationPolicyAccessory 1
#define NSApplicationActivationPolicyProhibited 2
// Unbelievably, if the user swaps their button preference
// then right buttons are reported as left buttons
#define NSEventMaskLeftMouseDown 1 << 1
@@ -110,6 +118,10 @@ void SetTray(struct Application* app, const char *, const char *, const char *);
//void SetContextMenus(struct Application* app, const char *);
void AddTrayMenu(struct Application* app, const char *);
void SetActivationPolicy(struct Application* app, int policy);
void* lookupStringConstant(id constantName);
void HasURLHandlers(struct Application* app);
#endif

View File

@@ -180,151 +180,151 @@ id processAcceleratorKey(const char *key) {
return str("");
}
if( STREQ(key, "Backspace") ) {
if( STREQ(key, "backspace") ) {
return strunicode(0x0008);
}
if( STREQ(key, "Tab") ) {
if( STREQ(key, "tab") ) {
return strunicode(0x0009);
}
if( STREQ(key, "Return") ) {
if( STREQ(key, "return") ) {
return strunicode(0x000d);
}
if( STREQ(key, "Escape") ) {
if( STREQ(key, "escape") ) {
return strunicode(0x001b);
}
if( STREQ(key, "Left") ) {
if( STREQ(key, "left") ) {
return strunicode(0x001c);
}
if( STREQ(key, "Right") ) {
if( STREQ(key, "right") ) {
return strunicode(0x001d);
}
if( STREQ(key, "Up") ) {
if( STREQ(key, "up") ) {
return strunicode(0x001e);
}
if( STREQ(key, "Down") ) {
if( STREQ(key, "down") ) {
return strunicode(0x001f);
}
if( STREQ(key, "Space") ) {
if( STREQ(key, "space") ) {
return strunicode(0x0020);
}
if( STREQ(key, "Delete") ) {
if( STREQ(key, "delete") ) {
return strunicode(0x007f);
}
if( STREQ(key, "Home") ) {
if( STREQ(key, "home") ) {
return strunicode(0x2196);
}
if( STREQ(key, "End") ) {
if( STREQ(key, "end") ) {
return strunicode(0x2198);
}
if( STREQ(key, "Page Up") ) {
if( STREQ(key, "page up") ) {
return strunicode(0x21de);
}
if( STREQ(key, "Page Down") ) {
if( STREQ(key, "page down") ) {
return strunicode(0x21df);
}
if( STREQ(key, "F1") ) {
if( STREQ(key, "f1") ) {
return strunicode(0xf704);
}
if( STREQ(key, "F2") ) {
if( STREQ(key, "f2") ) {
return strunicode(0xf705);
}
if( STREQ(key, "F3") ) {
if( STREQ(key, "f3") ) {
return strunicode(0xf706);
}
if( STREQ(key, "F4") ) {
if( STREQ(key, "f4") ) {
return strunicode(0xf707);
}
if( STREQ(key, "F5") ) {
if( STREQ(key, "f5") ) {
return strunicode(0xf708);
}
if( STREQ(key, "F6") ) {
if( STREQ(key, "f6") ) {
return strunicode(0xf709);
}
if( STREQ(key, "F7") ) {
if( STREQ(key, "f7") ) {
return strunicode(0xf70a);
}
if( STREQ(key, "F8") ) {
if( STREQ(key, "f8") ) {
return strunicode(0xf70b);
}
if( STREQ(key, "F9") ) {
if( STREQ(key, "f9") ) {
return strunicode(0xf70c);
}
if( STREQ(key, "F10") ) {
if( STREQ(key, "f10") ) {
return strunicode(0xf70d);
}
if( STREQ(key, "F11") ) {
if( STREQ(key, "f11") ) {
return strunicode(0xf70e);
}
if( STREQ(key, "F12") ) {
if( STREQ(key, "f12") ) {
return strunicode(0xf70f);
}
if( STREQ(key, "F13") ) {
if( STREQ(key, "f13") ) {
return strunicode(0xf710);
}
if( STREQ(key, "F14") ) {
if( STREQ(key, "f14") ) {
return strunicode(0xf711);
}
if( STREQ(key, "F15") ) {
if( STREQ(key, "f15") ) {
return strunicode(0xf712);
}
if( STREQ(key, "F16") ) {
if( STREQ(key, "f16") ) {
return strunicode(0xf713);
}
if( STREQ(key, "F17") ) {
if( STREQ(key, "f17") ) {
return strunicode(0xf714);
}
if( STREQ(key, "F18") ) {
if( STREQ(key, "f18") ) {
return strunicode(0xf715);
}
if( STREQ(key, "F19") ) {
if( STREQ(key, "f19") ) {
return strunicode(0xf716);
}
if( STREQ(key, "F20") ) {
if( STREQ(key, "f20") ) {
return strunicode(0xf717);
}
if( STREQ(key, "F21") ) {
if( STREQ(key, "f21") ) {
return strunicode(0xf718);
}
if( STREQ(key, "F22") ) {
if( STREQ(key, "f22") ) {
return strunicode(0xf719);
}
if( STREQ(key, "F23") ) {
if( STREQ(key, "f23") ) {
return strunicode(0xf71a);
}
if( STREQ(key, "F24") ) {
if( STREQ(key, "f24") ) {
return strunicode(0xf71b);
}
if( STREQ(key, "F25") ) {
if( STREQ(key, "f25") ) {
return strunicode(0xf71c);
}
if( STREQ(key, "F26") ) {
if( STREQ(key, "f26") ) {
return strunicode(0xf71d);
}
if( STREQ(key, "F27") ) {
if( STREQ(key, "f27") ) {
return strunicode(0xf71e);
}
if( STREQ(key, "F28") ) {
if( STREQ(key, "f28") ) {
return strunicode(0xf71f);
}
if( STREQ(key, "F29") ) {
if( STREQ(key, "f29") ) {
return strunicode(0xf720);
}
if( STREQ(key, "F30") ) {
if( STREQ(key, "f30") ) {
return strunicode(0xf721);
}
if( STREQ(key, "F31") ) {
if( STREQ(key, "f31") ) {
return strunicode(0xf722);
}
if( STREQ(key, "F32") ) {
if( STREQ(key, "f32") ) {
return strunicode(0xf723);
}
if( STREQ(key, "F33") ) {
if( STREQ(key, "f33") ) {
return strunicode(0xf724);
}
if( STREQ(key, "F34") ) {
if( STREQ(key, "f34") ) {
return strunicode(0xf725);
}
if( STREQ(key, "F35") ) {
if( STREQ(key, "f35") ) {
return strunicode(0xf726);
}
// if( STREQ(key, "Insert") ) {
@@ -336,7 +336,7 @@ id processAcceleratorKey(const char *key) {
// if( STREQ(key, "ScrollLock") ) {
// return strunicode(0xf72f);
// }
if( STREQ(key, "NumLock") ) {
if( STREQ(key, "numLock") ) {
return strunicode(0xf739);
}
@@ -508,20 +508,21 @@ unsigned long parseModifiers(const char **modifiers) {
const char *thisModifier = modifiers[0];
int count = 0;
while( thisModifier != NULL ) {
// Determine flags
if( STREQ(thisModifier, "CmdOrCtrl") ) {
if( STREQ(thisModifier, "cmdorctrl") ) {
result |= NSEventModifierFlagCommand;
}
if( STREQ(thisModifier, "OptionOrAlt") ) {
if( STREQ(thisModifier, "optionoralt") ) {
result |= NSEventModifierFlagOption;
}
if( STREQ(thisModifier, "Shift") ) {
if( STREQ(thisModifier, "shift") ) {
result |= NSEventModifierFlagShift;
}
if( STREQ(thisModifier, "Super") ) {
if( STREQ(thisModifier, "super") ) {
result |= NSEventModifierFlagCommand;
}
if( STREQ(thisModifier, "Control") ) {
if( STREQ(thisModifier, "ctrl") ) {
result |= NSEventModifierFlagControl;
}
count++;
@@ -575,34 +576,7 @@ id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const c
return item;
}
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, const char* tooltip, const char* image, const char* fontName, int fontSize, const char* RGBA, bool templateImage) {
id item = ALLOC("NSMenuItem");
// Create a MenuItemCallbackData
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, item, menuid, Text);
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), callback);
msg(item, s("setRepresentedObject:"), wrappedId);
id key = processAcceleratorKey(acceleratorkey);
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title),
s("menuItemCallback:"), key);
if( tooltip != NULL ) {
msg(item, s("setToolTip:"), str(tooltip));
}
// Process image
if( image != NULL && strlen(image) > 0) {
id data = ALLOC("NSData");
id imageData = msg(data, s("initWithBase64EncodedString:options:"), str(image), 0);
id nsimage = ALLOC("NSImage");
msg(nsimage, s("initWithData:"), imageData);
if( templateImage ) {
msg(nsimage, s("template"), YES);
}
msg(item, s("setImage:"), nsimage);
}
id createAttributedString(const char* title, const char* fontName, int fontSize, const char* RGBA) {
// Process Menu Item attributes
id dictionary = ALLOC_INIT("NSMutableDictionary");
@@ -653,19 +627,61 @@ id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char
id attributedString = ALLOC("NSMutableAttributedString");
msg(attributedString, s("initWithString:attributes:"), str(title), dictionary);
msg(dictionary, s("release"));
msg(item, s("setAttributedTitle:"), attributedString);
msg(attributedString, s("autorelease"));
msg(dictionary, s("release"));
return 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) {
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);
if( !alternate ) {
id key = processAcceleratorKey(acceleratorkey);
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title),
s("menuItemCallback:"), key);
} else {
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuItemCallback:"), str(""));
}
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("setTemplate:"), YES);
}
msg(item, s("setImage:"), nsimage);
}
id attributedString = createAttributedString(title, fontName, fontSize, RGBA);
msg(item, s("setAttributedTitle:"), attributedString);
msg(item, s("setEnabled:"), !disabled);
msg(item, s("autorelease"));
// Process modifiers
if( modifiers != NULL ) {
if( modifiers != NULL && !alternate) {
unsigned long modifierFlags = parseModifiers(modifiers);
msg(item, s("setKeyEquivalentModifierMask:"), modifierFlags);
}
// alternate
if( alternate ) {
msg(item, s("setAlternate:"), true);
msg(item, s("setKeyEquivalentModifierMask:"), NSEventModifierFlagOption);
}
msg(parentMenu, s("addItem:"), item);
return item;
@@ -726,6 +742,11 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
label = "(empty)";
}
// Is this an alternate menu item?
bool alternate = false;
getJSONBool(item, "MacAlternate", &alternate);
const char *menuid = getJSONString(item, "ID");
if ( menuid == NULL) {
menuid = "";
@@ -746,7 +767,7 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
bool templateImage = false;
getJSONBool(item, "MacTemplateImage", &templateImage);
int fontSize = 12;
int fontSize = 0;
getJSONInt(item, "FontSize", &fontSize);
// If we have an accelerator
@@ -781,7 +802,7 @@ void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
if( type != NULL ) {
if( STREQ(type->string_, "Text")) {
processTextMenuItem(menu, parentMenu, label, menuid, disabled, acceleratorkey, modifiers, tooltip, image, fontName, fontSize, RGBA, templateImage);
processTextMenuItem(menu, parentMenu, label, menuid, disabled, acceleratorkey, modifiers, tooltip, image, fontName, fontSize, RGBA, templateImage, alternate);
}
else if ( STREQ(type->string_, "Separator")) {
addSeparator(parentMenu);

View File

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

View File

@@ -6,6 +6,8 @@
#include "traymenu_darwin.h"
#include "trayicons.h"
extern Class trayMenuDelegateClass;
// A cache for all our tray menu icons
// Global because it's a singleton
struct hashmap_s trayIconCache;
@@ -29,12 +31,23 @@ TrayMenu* NewTrayMenu(const char* menuJSON) {
result->ID = mustJSONString(processedJSON, "ID");
result->label = mustJSONString(processedJSON, "Label");
result->icon = mustJSONString(processedJSON, "Icon");
JsonNode* processedMenu = mustJSONObject(processedJSON, "ProcessedMenu");
result->icon = mustJSONString(processedJSON, "Image");
result->fontName = getJSONString(processedJSON, "FontName");
result->RGBA = getJSONString(processedJSON, "RGBA");
getJSONBool(processedJSON, "MacTemplateImage", &result->templateImage);
result->fontSize = 0;
getJSONInt(processedJSON, "FontSize", &result->fontSize);
result->tooltip = NULL;
result->tooltip = getJSONString(processedJSON, "Tooltip");
result->disabled = false;
getJSONBool(processedJSON, "Disabled", &result->disabled);
// Create the menu
JsonNode* processedMenu = mustJSONObject(processedJSON, "ProcessedMenu");
result->menu = NewMenu(processedMenu);
result->delegate = NULL;
// Init tray status bar item
result->statusbaritem = NULL;
@@ -50,7 +63,7 @@ void DumpTrayMenu(TrayMenu* trayMenu) {
}
void UpdateTrayLabel(TrayMenu *trayMenu, const char *label) {
void UpdateTrayLabel(TrayMenu *trayMenu, const char *label, const char *fontName, int fontSize, const char *RGBA, const char *tooltip, bool disabled) {
// Exit early if NULL
if( trayMenu->label == NULL ) {
@@ -58,7 +71,15 @@ void UpdateTrayLabel(TrayMenu *trayMenu, const char *label) {
}
// Update button label
id statusBarButton = msg(trayMenu->statusbaritem, s("button"));
msg(statusBarButton, s("setTitle:"), str(label));
id attributedString = createAttributedString(label, fontName, fontSize, RGBA);
if( tooltip != NULL ) {
msg(statusBarButton, s("setToolTip:"), str(tooltip));
}
msg(statusBarButton, s("setEnabled:"), !disabled);
msg(statusBarButton, s("setAttributedTitle:"), attributedString);
}
void UpdateTrayIcon(TrayMenu *trayMenu) {
@@ -78,12 +99,24 @@ void UpdateTrayIcon(TrayMenu *trayMenu) {
}
id trayImage = hashmap_get(&trayIconCache, trayMenu->icon, strlen(trayMenu->icon));
// 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 = msg(data, s("initWithBase64EncodedString:options:"), str(trayMenu->icon), 0);
trayImage = ALLOC("NSImage");
msg(trayImage, s("initWithData:"), imageData);
if( trayMenu->templateImage ) {
msg(trayImage, s("setTemplate:"), YES);
}
}
msg(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition);
msg(statusBarButton, s("setImage:"), trayImage);
}
void ShowTrayMenu(TrayMenu* trayMenu) {
// Create a status bar item if we don't have one
@@ -91,7 +124,6 @@ void ShowTrayMenu(TrayMenu* trayMenu) {
id statusBar = msg( c("NSStatusBar"), s("systemStatusBar") );
trayMenu->statusbaritem = msg(statusBar, s("statusItemWithLength:"), NSVariableStatusItemLength);
msg(trayMenu->statusbaritem, s("retain"));
}
id statusBarButton = msg(trayMenu->statusbaritem, s("button"));
@@ -101,10 +133,20 @@ void ShowTrayMenu(TrayMenu* trayMenu) {
UpdateTrayIcon(trayMenu);
// Update the label if needed
UpdateTrayLabel(trayMenu, trayMenu->label);
UpdateTrayLabel(trayMenu, trayMenu->label, trayMenu->fontName, trayMenu->fontSize, trayMenu->RGBA, trayMenu->tooltip, trayMenu->disabled);
// Update the menu
id menu = GetMenu(trayMenu->menu);
objc_setAssociatedObject(menu, "trayMenuID", str(trayMenu->ID), OBJC_ASSOCIATION_ASSIGN);
// Create delegate
id trayMenuDelegate = msg((id)trayMenuDelegateClass, s("new"));
msg(menu, s("setDelegate:"), trayMenuDelegate);
objc_setAssociatedObject(trayMenuDelegate, "menu", menu, OBJC_ASSOCIATION_ASSIGN);
// Create menu delegate
trayMenu->delegate = trayMenuDelegate;
msg(trayMenu->statusbaritem, s("setMenu:"), menu);
}
@@ -153,6 +195,10 @@ void DeleteTrayMenu(TrayMenu* trayMenu) {
trayMenu->statusbaritem = NULL;
}
if ( trayMenu->delegate != NULL ) {
msg(trayMenu->delegate, s("release"));
}
// Free the tray menu memory
MEMFREE(trayMenu);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
package process
import (
"context"
"os"
"os/exec"
@@ -10,16 +9,18 @@ import (
// Process defines a process that can be executed
type Process struct {
logger *clilogger.CLILogger
cmd *exec.Cmd
running bool
logger *clilogger.CLILogger
cmd *exec.Cmd
exitChannel chan bool
Running bool
}
// NewProcess creates a new process struct
func NewProcess(ctx context.Context, logger *clilogger.CLILogger, cmd string, args ...string) *Process {
func NewProcess(logger *clilogger.CLILogger, cmd string, args ...string) *Process {
result := &Process{
logger: logger,
cmd: exec.CommandContext(ctx, cmd, args...),
logger: logger,
cmd: exec.Command(cmd, args...),
exitChannel: make(chan bool, 1),
}
result.cmd.Stdout = os.Stdout
result.cmd.Stderr = os.Stderr
@@ -34,30 +35,35 @@ func (p *Process) Start() error {
return err
}
p.running = true
p.Running = true
go func(p *Process) {
p.logger.Println("Starting process (PID: %d)", p.cmd.Process.Pid)
err := p.cmd.Wait()
go func(cmd *exec.Cmd, running *bool, logger *clilogger.CLILogger, exitChannel chan bool) {
logger.Println("Starting process (PID: %d)", cmd.Process.Pid)
err := cmd.Wait()
if err != nil {
p.running = false
p.logger.Println("Process failed to run: %s", err.Error())
return
if err.Error() != "signal: killed" {
logger.Fatal("Fatal error from app: " + err.Error())
}
}
p.logger.Println("Exiting process (PID: %d)", p.cmd.Process.Pid)
}(p)
logger.Println("Exiting process (PID: %d)", cmd.Process.Pid)
*running = false
exitChannel <- true
}(p.cmd, &p.Running, p.logger, p.exitChannel)
return nil
}
// Kill the process
func (p *Process) Kill() error {
if p.running {
println("Calling kill")
p.running = false
return p.cmd.Process.Kill()
if !p.Running {
return nil
}
return nil
err := p.cmd.Process.Kill()
// Wait for command to exit properly
<-p.exitChannel
return err
}
// PID returns the process PID

View File

@@ -620,7 +620,7 @@
}
/** Menubar **/
const menuVisible = writable(true);
const menuVisible = writable(false);
/** 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);
function fade(node, { delay = 0, duration = 400, easing = identity } = {}) {
@@ -779,18 +791,18 @@
function add_css$1() {
var style = element("style");
style.id = "svelte-1ucacnf-style";
style.textContent = ".menu.svelte-1ucacnf.svelte-1ucacnf{padding:5px;background-color:#0008;color:#EEF;border-radius:5px;margin-top:5px;position:absolute;backdrop-filter:blur(3px) saturate(160%) contrast(45%) brightness(140%);border:1px solid rgb(88,88,88);box-shadow:0 0 1px rgb(146,146,148) inset}.menuitem.svelte-1ucacnf.svelte-1ucacnf{display:flex;align-items:center;padding:1px 5px}.menuitem.svelte-1ucacnf.svelte-1ucacnf:hover{display:flex;align-items:center;background-color:rgb(57,131,223);padding:1px 5px;border-radius:5px}.menuitem.svelte-1ucacnf img.svelte-1ucacnf{padding-right:5px}.separator.svelte-1ucacnf.svelte-1ucacnf{padding-top:5px;width:100%;padding-bottom:5px}.separator.svelte-1ucacnf.svelte-1ucacnf:hover{background-color:#0000}";
style.id = "svelte-1oysp7o-style";
style.textContent = ".menu.svelte-1oysp7o.svelte-1oysp7o{padding:3px;background-color:#0008;color:#EEF;border-radius:5px;margin-top:5px;position:absolute;backdrop-filter:blur(3px) saturate(160%) contrast(45%) brightness(140%);border:1px solid rgb(88,88,88);box-shadow:0 0 1px rgb(146,146,148) inset}.menuitem.svelte-1oysp7o.svelte-1oysp7o{display:flex;align-items:center;padding:1px 5px}.menuitem.svelte-1oysp7o.svelte-1oysp7o:hover{display:flex;align-items:center;background-color:rgb(57,131,223);padding:1px 5px;border-radius:5px}.menuitem.svelte-1oysp7o img.svelte-1oysp7o{padding-right:5px}";
append(document.head, style);
}
function get_each_context(ctx, list, i) {
const child_ctx = ctx.slice();
child_ctx[3] = list[i];
child_ctx[2] = list[i];
return child_ctx;
}
// (14:0) {#if visible}
// (8:0) {#if !hidden}
function create_if_block$1(ctx) {
let div;
let if_block = /*menu*/ ctx[0].Menu && create_if_block_1(ctx);
@@ -799,7 +811,7 @@
c() {
div = element("div");
if (if_block) if_block.c();
attr(div, "class", "menu svelte-1ucacnf");
attr(div, "class", "menu svelte-1oysp7o");
},
m(target, anchor) {
insert(target, div, anchor);
@@ -826,7 +838,7 @@
};
}
// (16:4) {#if menu.Menu }
// (10:4) {#if menu.Menu }
function create_if_block_1(ctx) {
let each_1_anchor;
let each_value = /*menu*/ ctx[0].Menu.Items;
@@ -852,7 +864,7 @@
insert(target, each_1_anchor, anchor);
},
p(ctx, dirty) {
if (dirty & /*click, menu*/ 1) {
if (dirty & /*menu*/ 1) {
each_value = /*menu*/ ctx[0].Menu.Items;
let i;
@@ -882,41 +894,44 @@
};
}
// (25:52)
function create_if_block_4(ctx) {
// (13:12) {#if menuItem.Image }
function create_if_block_2(ctx) {
let div;
let img;
let img_src_value;
return {
c() {
div = element("div");
div.innerHTML = `<hr/>`;
attr(div, "class", "separator svelte-1ucacnf");
img = element("img");
attr(img, "alt", "");
if (img.src !== (img_src_value = "data:image/png;base64," + /*menuItem*/ ctx[2].Image)) attr(img, "src", img_src_value);
attr(img, "class", "svelte-1oysp7o");
},
m(target, anchor) {
insert(target, div, anchor);
append(div, img);
},
p(ctx, dirty) {
if (dirty & /*menu*/ 1 && img.src !== (img_src_value = "data:image/png;base64," + /*menuItem*/ ctx[2].Image)) {
attr(img, "src", img_src_value);
}
},
p: noop,
d(detaching) {
if (detaching) detach(div);
}
};
}
// (18:12) {#if menuItem.Type === "Text" }
function create_if_block_2(ctx) {
// (11:8) {#each menu.Menu.Items as menuItem}
function create_each_block(ctx) {
let div1;
let t0;
let div0;
let t1_value = /*menuItem*/ ctx[3].Label + "";
let t1_value = /*menuItem*/ ctx[2].Label + "";
let t1;
let t2;
let mounted;
let dispose;
let if_block = /*menuItem*/ ctx[3].Image && create_if_block_3(ctx);
function click_handler() {
return /*click_handler*/ ctx[2](/*menuItem*/ ctx[3]);
}
let if_block = /*menuItem*/ ctx[2].Image && create_if_block_2(ctx);
return {
c() {
@@ -927,7 +942,7 @@
t1 = text(t1_value);
t2 = space();
attr(div0, "class", "menulabel");
attr(div1, "class", "menuitem svelte-1ucacnf");
attr(div1, "class", "menuitem svelte-1oysp7o");
},
m(target, anchor) {
insert(target, div1, anchor);
@@ -936,20 +951,13 @@
append(div1, div0);
append(div0, t1);
append(div1, t2);
if (!mounted) {
dispose = listen(div1, "click", click_handler);
mounted = true;
}
},
p(new_ctx, dirty) {
ctx = new_ctx;
if (/*menuItem*/ ctx[3].Image) {
p(ctx, dirty) {
if (/*menuItem*/ ctx[2].Image) {
if (if_block) {
if_block.p(ctx, dirty);
} else {
if_block = create_if_block_3(ctx);
if_block = create_if_block_2(ctx);
if_block.c();
if_block.m(div1, t0);
}
@@ -958,93 +966,18 @@
if_block = null;
}
if (dirty & /*menu*/ 1 && t1_value !== (t1_value = /*menuItem*/ ctx[3].Label + "")) set_data(t1, t1_value);
if (dirty & /*menu*/ 1 && t1_value !== (t1_value = /*menuItem*/ ctx[2].Label + "")) set_data(t1, t1_value);
},
d(detaching) {
if (detaching) detach(div1);
if (if_block) if_block.d();
mounted = false;
dispose();
}
};
}
// (20:12) {#if menuItem.Image }
function create_if_block_3(ctx) {
let div;
let img;
let img_src_value;
return {
c() {
div = element("div");
img = element("img");
attr(img, "alt", "");
if (img.src !== (img_src_value = "data:image/png;base64," + /*menuItem*/ ctx[3].Image)) attr(img, "src", img_src_value);
attr(img, "class", "svelte-1ucacnf");
},
m(target, anchor) {
insert(target, div, anchor);
append(div, img);
},
p(ctx, dirty) {
if (dirty & /*menu*/ 1 && img.src !== (img_src_value = "data:image/png;base64," + /*menuItem*/ ctx[3].Image)) {
attr(img, "src", img_src_value);
}
},
d(detaching) {
if (detaching) detach(div);
}
};
}
// (17:8) {#each menu.Menu.Items as menuItem}
function create_each_block(ctx) {
let if_block_anchor;
function select_block_type(ctx, dirty) {
if (/*menuItem*/ ctx[3].Type === "Text") return create_if_block_2;
if (/*menuItem*/ ctx[3].Type === "Separator") return create_if_block_4;
}
let current_block_type = select_block_type(ctx);
let if_block = current_block_type && current_block_type(ctx);
return {
c() {
if (if_block) if_block.c();
if_block_anchor = empty();
},
m(target, anchor) {
if (if_block) if_block.m(target, anchor);
insert(target, if_block_anchor, anchor);
},
p(ctx, dirty) {
if (current_block_type === (current_block_type = select_block_type(ctx)) && if_block) {
if_block.p(ctx, dirty);
} else {
if (if_block) if_block.d(1);
if_block = current_block_type && current_block_type(ctx);
if (if_block) {
if_block.c();
if_block.m(if_block_anchor.parentNode, if_block_anchor);
}
}
},
d(detaching) {
if (if_block) {
if_block.d(detaching);
}
if (detaching) detach(if_block_anchor);
}
};
}
function create_fragment$1(ctx) {
let if_block_anchor;
let if_block = /*visible*/ ctx[1] && create_if_block$1(ctx);
let if_block = !/*hidden*/ ctx[1] && create_if_block$1(ctx);
return {
c() {
@@ -1056,7 +989,7 @@
insert(target, if_block_anchor, anchor);
},
p(ctx, [dirty]) {
if (/*visible*/ ctx[1]) {
if (!/*hidden*/ ctx[1]) {
if (if_block) {
if_block.p(ctx, dirty);
} else {
@@ -1078,29 +1011,23 @@
};
}
function click(id) {
console.log("MenuItem", id, "pressed");
}
function instance$1($$self, $$props, $$invalidate) {
let { menu } = $$props;
console.log({ menu });
let { visible = false } = $$props;
const click_handler = menuItem => click(menuItem.ID);
let { hidden = true } = $$props;
$$self.$$set = $$props => {
if ("menu" in $$props) $$invalidate(0, menu = $$props.menu);
if ("visible" in $$props) $$invalidate(1, visible = $$props.visible);
if ("hidden" in $$props) $$invalidate(1, hidden = $$props.hidden);
};
return [menu, visible, click_handler];
return [menu, hidden];
}
class Menu extends SvelteComponent {
constructor(options) {
super();
if (!document.getElementById("svelte-1ucacnf-style")) add_css$1();
init(this, options, instance$1, create_fragment$1, safe_not_equal, { menu: 0, visible: 1 });
if (!document.getElementById("svelte-1oysp7o-style")) add_css$1();
init(this, options, instance$1, create_fragment$1, safe_not_equal, { menu: 0, hidden: 1 });
}
}
@@ -1115,7 +1042,7 @@
append(document_1.head, style);
}
// (48:4) {#if tray.ProcessedMenu }
// (47:4) {#if tray.ProcessedMenu }
function create_if_block$2(ctx) {
let menu;
let current;
@@ -1123,7 +1050,7 @@
menu = new Menu({
props: {
menu: /*tray*/ ctx[0].ProcessedMenu,
visible: /*visible*/ ctx[1]
hidden: /*hidden*/ ctx[1]
}
});
@@ -1138,7 +1065,7 @@
p(ctx, dirty) {
const menu_changes = {};
if (dirty & /*tray*/ 1) menu_changes.menu = /*tray*/ ctx[0].ProcessedMenu;
if (dirty & /*visible*/ 2) menu_changes.visible = /*visible*/ ctx[1];
if (dirty & /*hidden*/ 2) menu_changes.hidden = /*hidden*/ ctx[1];
menu.$set(menu_changes);
},
i(local) {
@@ -1242,7 +1169,6 @@
function clickOutside(node) {
const handleClick = event => {
if (node && !node.contains(event.target) && !event.defaultPrevented) {
console.log("click outside of node");
node.dispatchEvent(new CustomEvent("click_outside", node));
}
};
@@ -1257,7 +1183,7 @@
}
function instance$2($$self, $$props, $$invalidate) {
let visible;
let hidden;
let $selectedMenu;
component_subscribe($$self, selectedMenu, $$value => $$invalidate(4, $selectedMenu = $$value));
let { tray = null } = $$props;
@@ -1280,11 +1206,11 @@
$$self.$$.update = () => {
if ($$self.$$.dirty & /*$selectedMenu, tray*/ 17) {
$$invalidate(1, visible = $selectedMenu === tray);
$$invalidate(1, hidden = $selectedMenu !== tray);
}
};
return [tray, visible, closeMenu, trayClicked, $selectedMenu];
return [tray, hidden, closeMenu, trayClicked, $selectedMenu];
}
class TrayMenu extends SvelteComponent {
@@ -1306,11 +1232,11 @@
function get_each_context$1(ctx, list, i) {
const child_ctx = ctx.slice();
child_ctx[8] = list[i];
child_ctx[9] = list[i];
return child_ctx;
}
// (29:0) {#if $menuVisible }
// (38:0) {#if $menuVisible }
function create_if_block$3(ctx) {
let div;
let span1;
@@ -1422,11 +1348,11 @@
};
}
// (32:4) {#each $trays as tray}
// (41:4) {#each $trays as tray}
function create_each_block$1(ctx) {
let traymenu;
let current;
traymenu = new TrayMenu({ props: { tray: /*tray*/ ctx[8] } });
traymenu = new TrayMenu({ props: { tray: /*tray*/ ctx[9] } });
return {
c() {
@@ -1438,7 +1364,7 @@
},
p(ctx, dirty) {
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);
},
i(local) {
@@ -1459,6 +1385,8 @@
function create_fragment$3(ctx) {
let if_block_anchor;
let current;
let mounted;
let dispose;
let if_block = /*$menuVisible*/ ctx[1] && create_if_block$3(ctx);
return {
@@ -1470,6 +1398,11 @@
if (if_block) if_block.m(target, anchor);
insert(target, if_block_anchor, anchor);
current = true;
if (!mounted) {
dispose = listen(window, "keydown", /*handleKeydown*/ ctx[3]);
mounted = true;
}
},
p(ctx, [dirty]) {
if (/*$menuVisible*/ ctx[1]) {
@@ -1507,6 +1440,8 @@
d(detaching) {
if (if_block) if_block.d(detaching);
if (detaching) detach(if_block_anchor);
mounted = false;
dispose();
}
};
}
@@ -1526,7 +1461,7 @@
onMount(() => {
const interval = setInterval(
() => {
$$invalidate(3, time = new Date());
$$invalidate(4, time = new Date());
},
1000
);
@@ -1536,33 +1471,52 @@
};
});
function handleKeydown(e) {
// Backtick toggle
if (e.keyCode == 192) {
menuVisible.update(current => {
return !current;
});
}
}
$$self.$$.update = () => {
if ($$self.$$.dirty & /*time*/ 8) {
$$invalidate(4, day = time.toLocaleString("default", { weekday: "short" }));
if ($$self.$$.dirty & /*time*/ 16) {
$$invalidate(5, day = time.toLocaleString("default", { weekday: "short" }));
}
if ($$self.$$.dirty & /*time*/ 8) {
$$invalidate(5, dom = time.getDate());
if ($$self.$$.dirty & /*time*/ 16) {
$$invalidate(6, dom = time.getDate());
}
if ($$self.$$.dirty & /*time*/ 8) {
$$invalidate(6, mon = time.toLocaleString("default", { month: "short" }));
if ($$self.$$.dirty & /*time*/ 16) {
$$invalidate(7, mon = time.toLocaleString("default", { month: "short" }));
}
if ($$self.$$.dirty & /*time*/ 8) {
$$invalidate(7, currentTime = time.toLocaleString("en-US", {
if ($$self.$$.dirty & /*time*/ 16) {
$$invalidate(8, currentTime = time.toLocaleString("en-US", {
hour: "numeric",
minute: "numeric",
hour12: true
}).toLowerCase());
}
if ($$self.$$.dirty & /*day, dom, mon, currentTime*/ 240) {
if ($$self.$$.dirty & /*day, dom, mon, currentTime*/ 480) {
$$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 {
@@ -1724,6 +1678,11 @@
let trayLabelData = JSON.parse(updateTrayLabelJSON);
updateTrayLabel(trayLabelData);
break
case 'D':
// Delete Tray Menu
const id = trayMessage.slice(1);
deleteTrayMenu(id);
break
default:
log('Unknown tray message: ' + message.data);
}

View File

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

View File

@@ -2,29 +2,19 @@
export let menu;
console.log({menu})
export let visible = false;
function click(id) {
console.log("MenuItem", id, "pressed")
}
export let hidden = true;
</script>
{#if visible}
{#if !hidden}
<div class="menu">
{#if menu.Menu }
{#each menu.Menu.Items as menuItem}
{#if menuItem.Type === "Text" }
<div class="menuitem" on:click={() => click(menuItem.ID)}>
<div class="menuitem">
{#if menuItem.Image }
<div><img alt="" src="data:image/png;base64,{menuItem.Image}"/></div>
{/if}
<div class="menulabel">{menuItem.Label}</div>
<div class="menulabel">{menuItem.Label}</div>
</div>
{:else if menuItem.Type === "Separator"}
<div class="separator"><hr/></div>
{/if}
{/each}
{/if}
</div>
@@ -33,7 +23,7 @@
<style>
.menu {
padding: 5px;
padding: 3px;
background-color: #0008;
color: #EEF;
border-radius: 5px;
@@ -42,6 +32,7 @@
backdrop-filter: blur(3px) saturate(160%) contrast(45%) brightness(140%);
border: 1px solid rgb(88,88,88);
box-shadow: 0 0 1px rgb(146,146,148) inset;
}
.menuitem {
@@ -62,13 +53,4 @@
padding-right: 5px;
}
.separator {
padding-top: 5px;
width: 100%;
padding-bottom: 5px;
}
.separator:hover {
background-color: #0000;
}
</style>

View File

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

View File

@@ -4,7 +4,7 @@
export let tray = null;
$: visible = $selectedMenu === tray;
$: hidden = $selectedMenu !== tray;
function closeMenu() {
selectedMenu.set(null);
@@ -23,7 +23,6 @@
const handleClick = event => {
if (node && !node.contains(event.target) && !event.defaultPrevented) {
console.log("click outside of node")
node.dispatchEvent(
new CustomEvent('click_outside', node)
)
@@ -46,7 +45,7 @@
<!--{/if}-->
<span class="label" on:click={trayClicked}>{tray.Label}</span>
{#if tray.ProcessedMenu }
<Menu menu="{tray.ProcessedMenu}" visible="{visible}"/>
<Menu menu="{tray.ProcessedMenu}" {hidden}/>
{/if}
</span>

View File

@@ -13,7 +13,7 @@ export function hideOverlay() {
}
/** Menubar **/
export const menuVisible = writable(true);
export const menuVisible = writable(false);
export function showMenuBar() {
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);

View File

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

View File

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

View File

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

View File

@@ -26,25 +26,20 @@ type Manager struct {
ctx context.Context
cancel context.CancelFunc
// The shutdown callback to notify the user's app that a shutdown
// has started
shutdownCallback func()
// Parent waitgroup
wg *sync.WaitGroup
}
// 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{
bus: bus,
logger: logger.CustomLogger("Event Manager"),
signalchannel: make(chan os.Signal, 2),
ctx: ctx,
cancel: cancel,
shutdownCallback: shutdownCallback,
wg: ctx.Value("waitgroup").(*sync.WaitGroup),
bus: bus,
logger: logger.CustomLogger("Event Manager"),
signalchannel: make(chan os.Signal, 2),
ctx: ctx,
cancel: cancel,
wg: ctx.Value("waitgroup").(*sync.WaitGroup),
}
return result, nil
@@ -67,11 +62,6 @@ func (m *Manager) Start() {
m.logger.Trace("Ctrl+C detected. Shutting down...")
m.bus.Publish("quit", "ctrl-c pressed")
// Shutdown app first
if m.shutdownCallback != nil {
m.shutdownCallback()
}
// Start shutdown of Wails
m.cancel()
@@ -80,5 +70,4 @@ func (m *Manager) Start() {
m.logger.Trace("Shutdown")
m.wg.Done()
}()
}

View File

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

View File

@@ -34,10 +34,13 @@ type Runtime struct {
// Startup Hook
startupOnce sync.Once
// Service bus
bus *servicebus.ServiceBus
}
// 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
runtimeChannel, err := bus.Subscribe("runtime:")
@@ -52,13 +55,13 @@ func NewRuntime(ctx context.Context, bus *servicebus.ServiceBus, logger *logger.
}
result := &Runtime{
runtimeChannel: runtimeChannel,
hooksChannel: hooksChannel,
logger: logger.CustomLogger("Runtime Subsystem"),
runtime: runtime.New(bus, shutdownCallback),
startupCallback: startupCallback,
shutdownCallback: shutdownCallback,
ctx: ctx,
runtimeChannel: runtimeChannel,
hooksChannel: hooksChannel,
logger: logger.CustomLogger("Runtime Subsystem"),
runtime: runtime.New(bus),
startupCallback: startupCallback,
ctx: ctx,
bus: bus,
}
return result, nil
@@ -80,7 +83,15 @@ func (r *Runtime) Start() error {
case "startup":
if r.startupCallback != nil {
r.startupOnce.Do(func() {
go r.startupCallback(r.runtime)
go func() {
r.startupCallback(r.runtime)
// If we got a url, publish it now startup completed
url, ok := hooksMessage.Data().(string)
if ok && len(url) > 0 {
r.bus.Publish("url:handler", url)
}
}()
})
} else {
r.logger.Warning("no startup callback registered!")

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -4,6 +4,8 @@ import (
"fmt"
"io"
"os"
"github.com/wailsapp/wails/v2/internal/colour"
)
// 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
func (c *CLILogger) Fatal(message string, args ...interface{}) {
temp := fmt.Sprintf(message, args...)
_, err := fmt.Fprintln(c.Writer, "FATAL: "+temp)
_, err := fmt.Fprintln(c.Writer, colour.Red("FATAL: "+temp))
if err != nil {
println("FATAL: ", err)
println(colour.Red("FATAL: " + err.Error()))
}
os.Exit(1)
}

View File

@@ -145,6 +145,13 @@ func (b *BaseBuilder) CleanUp() {
// CompileProject compiles the project
func (b *BaseBuilder) CompileProject(options *Options) error {
// Run go mod tidy first
cmd := exec.Command(options.Compiler, "mod", "tidy")
err := cmd.Run()
if err != nil {
return err
}
// Default go build command
commands := slicer.String([]string{"build"})
@@ -188,7 +195,7 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
// Get application build directory
appDir := options.BuildDirectory
err := cleanBuildDirectory(options)
err = cleanBuildDirectory(options)
if err != nil {
return err
}
@@ -211,14 +218,11 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
options.CompiledBinary = compiledBinary
// Create the command
cmd := exec.Command(options.Compiler, commands.AsSlice()...)
cmd = exec.Command(options.Compiler, commands.AsSlice()...)
// Set the directory
cmd.Dir = b.projectData.Path
// Set GO111MODULE environment variable
cmd.Env = append(os.Environ(), "GO111MODULE=on")
// Add CGO flags
// We use the project/build dir as a temporary place for our generated c headers
buildBaseDir, err := fs.RelativeToCwd("build")
@@ -226,7 +230,16 @@ func (b *BaseBuilder) CompileProject(options *Options) error {
return err
}
cmd.Env = append(os.Environ(), fmt.Sprintf("CGO_CFLAGS=-I%s", buildBaseDir))
cmd.Env = os.Environ() // inherit env
// Use upsertEnv so we don't overwrite user's CGO_CFLAGS
cmd.Env = upsertEnv(cmd.Env, "CGO_CFLAGS", func(v string) string {
if v != "" {
v += " "
}
v += "-I" + buildBaseDir
return v
})
// Setup buffers
var stdo, stde bytes.Buffer
@@ -384,3 +397,22 @@ func (b *BaseBuilder) ExtractAssets() (*html.AssetBundle, error) {
// Read in html
return html.NewAssetBundle(b.projectData.HTML)
}
func upsertEnv(env []string, key string, update func(v string) string) []string {
newEnv := make([]string, len(env), len(env)+1)
found := false
for i := range env {
if strings.HasPrefix(env[i], key+"=") {
eqIndex := strings.Index(env[i], "=")
val := env[i][eqIndex+1:]
newEnv[i] = fmt.Sprintf("%s=%v", key, update(val))
found = true
continue
}
newEnv[i] = env[i]
}
if !found {
newEnv = append(newEnv, fmt.Sprintf("%s=%v", key, update("")))
}
return newEnv
}

View File

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

View File

@@ -38,6 +38,7 @@ type Options struct {
BuildDirectory string // Directory to use for building the application
CompiledBinary string // Fully qualified path to the compiled binary
KeepAssets bool // /Keep the generated assets/files
AppleIdentity string
}
// GetModeAsString returns the current mode as a string

View File

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

View File

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

View File

@@ -1,21 +1,44 @@
package keys
import (
"fmt"
"strings"
)
// Modifier is actually a string
type Modifier string
const (
// CmdOrCtrlKey represents Command on Mac and Control on other platforms
CmdOrCtrlKey Modifier = "CmdOrCtrl"
CmdOrCtrlKey Modifier = "cmdorctrl"
// OptionOrAltKey represents Option on Mac and Alt on other platforms
OptionOrAltKey Modifier = "OptionOrAlt"
OptionOrAltKey Modifier = "optionoralt"
// ShiftKey represents the shift key on all systems
ShiftKey Modifier = "Shift"
ShiftKey Modifier = "shift"
// SuperKey represents Command on Mac and the Windows key on the other platforms
SuperKey Modifier = "Super"
SuperKey Modifier = "super"
// ControlKey represents the control key on all systems
ControlKey Modifier = "Control"
ControlKey Modifier = "ctrl"
)
var modifierMap = map[string]Modifier{
"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
type Accelerator struct {
Key string
@@ -25,14 +48,14 @@ type Accelerator struct {
// Key creates a standard key Accelerator
func Key(key string) *Accelerator {
return &Accelerator{
Key: key,
Key: strings.ToLower(key),
}
}
// CmdOrCtrl creates a 'CmdOrCtrl' Accelerator
func CmdOrCtrl(key string) *Accelerator {
return &Accelerator{
Key: key,
Key: strings.ToLower(key),
Modifiers: []Modifier{CmdOrCtrlKey},
}
}
@@ -40,7 +63,7 @@ func CmdOrCtrl(key string) *Accelerator {
// OptionOrAlt creates a 'OptionOrAlt' Accelerator
func OptionOrAlt(key string) *Accelerator {
return &Accelerator{
Key: key,
Key: strings.ToLower(key),
Modifiers: []Modifier{OptionOrAltKey},
}
}
@@ -48,7 +71,7 @@ func OptionOrAlt(key string) *Accelerator {
// Shift creates a 'Shift' Accelerator
func Shift(key string) *Accelerator {
return &Accelerator{
Key: key,
Key: strings.ToLower(key),
Modifiers: []Modifier{ShiftKey},
}
}
@@ -56,7 +79,7 @@ func Shift(key string) *Accelerator {
// Control creates a 'Control' Accelerator
func Control(key string) *Accelerator {
return &Accelerator{
Key: key,
Key: strings.ToLower(key),
Modifiers: []Modifier{ControlKey},
}
}
@@ -64,7 +87,7 @@ func Control(key string) *Accelerator {
// Super creates a 'Super' Accelerator
func Super(key string) *Accelerator {
return &Accelerator{
Key: key,
Key: strings.ToLower(key),
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
Click Callback `json:"-"`
// Colour
// Text Colour
RGBA string
// Font
@@ -39,9 +39,12 @@ type MenuItem struct {
// Image - base64 image data
Image string
// MacTemplateImage indicates that on a mac, this image is a template image
// MacTemplateImage indicates that on a Mac, this image is a template image
MacTemplateImage bool
// MacAlternate indicates that this item is an alternative to the previous menu item
MacAlternate bool
// Tooltip
Tooltip string

View File

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

View File

@@ -2,6 +2,14 @@ package mac
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
type Options struct {
TitleBar *TitleBar
@@ -11,4 +19,6 @@ type Options struct {
Menu *menu.Menu
TrayMenus []*menu.TrayMenu
ContextMenus []*menu.ContextMenu
ActivationPolicy ActivationPolicy
URLHandlers map[string]func(string)
}