Compare commits

..

3 Commits

Author SHA1 Message Date
Lea Anthony
480dcb7895 TEMP COMMIT 2021-01-22 15:26:58 +11:00
Lea Anthony
c8d89cf002 [WIP] Use simpler menu id mechanism. Smaller json. Remove pointer reliance. 2021-01-18 06:07:55 +11:00
Lea Anthony
fc669ede37 Tidy up debug output 2021-01-16 14:43:32 +11:00
160 changed files with 5324 additions and 10094 deletions

View File

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

View File

@@ -3,12 +3,12 @@
"browser": true,
"es6": true,
"amd": true,
"node": true
"node": true,
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 2016,
"sourceType": "module"
"sourceType": "module",
},
"rules": {
"indent": [

View File

@@ -26,7 +26,7 @@ export function OpenURL(url) {
* Opens the given filename using the system's default file handler
*
* @export
* @param {string} filename
* @param {sting} filename
* @returns
*/
export function OpenFile(filename) {

View File

@@ -62,7 +62,7 @@ if (window.crypto) {
export function Call(bindingName, data, timeout) {
// Timeout infinite by default
if (timeout == null) {
if (timeout == null || timeout == undefined) {
timeout = 0;
}

View File

@@ -45,7 +45,7 @@ function Invoke(message) {
*
* @export
* @param {string} type
* @param {Object} payload
* @param {string} payload
* @param {string=} callbackID
*/
export function SendMessage(type, payload, callbackID) {

View File

@@ -1 +1 @@
index.js
bridge.js

View File

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

View File

@@ -3,10 +3,8 @@ package build
import (
"fmt"
"io"
"os"
"runtime"
"strings"
"text/tabwriter"
"time"
"github.com/leaanthony/clir"
@@ -25,8 +23,8 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
command := app.NewSubCommand("build", "Builds the application")
// Setup target type flag
//description := "Type of application to build. Valid types: " + validTargetTypes.Join(",")
//command.StringFlag("t", description, &outputType)
description := "Type of application to build. Valid types: " + validTargetTypes.Join(",")
command.StringFlag("t", description, &outputType)
// Setup production flag
production := false
@@ -43,35 +41,24 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
platform := runtime.GOOS
command.StringFlag("platform", "Platform to target", &platform)
// Verbosity
verbosity := 1
command.IntFlag("v", "Verbosity level (0 - silent, 1 - default, 2 - verbose)", &verbosity)
// Quiet Build
quiet := false
command.BoolFlag("q", "Suppress output to console", &quiet)
// ldflags to pass to `go`
ldflags := ""
command.StringFlag("ldflags", "optional ldflags", &ldflags)
// Log to file
//logFile := ""
//command.StringFlag("l", "Log to file", &logFile)
logFile := ""
command.StringFlag("l", "Log to file", &logFile)
// Retain assets
keepAssets := false
command.BoolFlag("k", "Keep generated assets", &keepAssets)
// Retain assets
outputFilename := ""
command.StringFlag("o", "Output filename", &outputFilename)
appleIdentity := ""
if runtime.GOOS == "darwin" {
command.StringFlag("sign", "Signs your app with the given identity.", &appleIdentity)
}
command.Action(func() error {
quiet := verbosity == 0
// Create logger
logger := clilogger.New(w)
logger.Mute(quiet)
@@ -85,10 +72,9 @@ 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)))
// Setup mode
mode := build.Debug
@@ -96,66 +82,18 @@ func AddBuildSubcommand(app *clir.Cli, w io.Writer) {
mode = build.Production
}
// Check platform
validPlatformArch := slicer.String([]string{
"darwin",
"darwin/amd64",
"darwin/arm64",
"darwin/universal",
//"linux/amd64",
//"linux/arm-7",
//"windows/amd64",
})
if !validPlatformArch.Contains(platform) {
return fmt.Errorf("platform %s is not supported", platform)
}
// Create BuildOptions
buildOptions := &build.Options{
Logger: logger,
OutputType: outputType,
OutputFile: outputFilename,
Mode: mode,
Pack: pack,
LDFlags: ldflags,
Compiler: compilerCommand,
KeepAssets: keepAssets,
AppleIdentity: appleIdentity,
Verbosity: verbosity,
Logger: logger,
OutputType: outputType,
Mode: mode,
Pack: pack,
Platform: platform,
LDFlags: ldflags,
Compiler: compilerCommand,
KeepAssets: keepAssets,
}
// Calculate platform and arch
platformSplit := strings.Split(platform, "/")
buildOptions.Platform = platformSplit[0]
buildOptions.Arch = runtime.GOARCH
if len(platformSplit) == 2 {
buildOptions.Arch = platformSplit[1]
}
// Start a new tabwriter
w := new(tabwriter.Writer)
w.Init(os.Stdout, 8, 8, 0, '\t', 0)
buildModeText := "debug"
if production {
buildModeText = "production"
}
// Write out the system information
fmt.Fprintf(w, "App Type: \t%s\n", buildOptions.OutputType)
fmt.Fprintf(w, "Platform: \t%s\n", buildOptions.Platform)
fmt.Fprintf(w, "Arch: \t%s\n", buildOptions.Arch)
fmt.Fprintf(w, "Compiler: \t%s\n", buildOptions.Compiler)
fmt.Fprintf(w, "Build Mode: \t%s\n", buildModeText)
fmt.Fprintf(w, "Package: \t%t\n", buildOptions.Pack)
fmt.Fprintf(w, "KeepAssets: \t%t\n", buildOptions.KeepAssets)
fmt.Fprintf(w, "LDFlags: \t\"%s\"\n", buildOptions.LDFlags)
if len(buildOptions.OutputFile) > 0 {
fmt.Fprintf(w, "Output File: \t%s\n", buildOptions.OutputFile)
}
fmt.Fprintf(w, "\n")
w.Flush()
return doBuild(buildOptions)
})
}

View File

@@ -5,42 +5,33 @@ import (
"io"
"os"
"os/signal"
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
"github.com/wailsapp/wails/v2/internal/colour"
"github.com/fsnotify/fsnotify"
"github.com/leaanthony/clir"
"github.com/leaanthony/slicer"
"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 {
command := app.NewSubCommand("dev", "Development mode")
outputType := "desktop"
validTargetTypes := slicer.String([]string{"desktop", "hybrid", "server"})
// Setup target type flag
description := "Type of application to develop. Valid types: " + validTargetTypes.Join(",")
command.StringFlag("t", description, &outputType)
// Passthrough ldflags
ldflags := ""
command.StringFlag("ldflags", "optional ldflags", &ldflags)
@@ -51,17 +42,15 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
// extensions to trigger rebuilds
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.StringFlag("m", "Extensions to trigger rebuilds (comma separated) eg go,js,css,html", &extensions)
command.Action(func() error {
// Validate inputs
if !validTargetTypes.Contains(outputType) {
return fmt.Errorf("output type '%s' is not valid", outputType)
}
// Create logger
logger := clilogger.New(w)
app.PrintBanner()
@@ -75,6 +64,7 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
defer watcher.Close()
var debugBinaryProcess *process.Process = nil
var buildFrontend bool = true
var extensionsThatTriggerARebuild = strings.Split(extensions, ",")
// Setup signal handler
@@ -85,13 +75,8 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
// 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
}
debugBinaryProcess = restartApp(logger, outputType, ldflags, compilerCommand, buildFrontend, debugBinaryProcess)
go debounce(100*time.Millisecond, watcher.Events, debounceQuit, func(event fsnotify.Event) {
// logger.Println("event: %+v", event)
@@ -104,7 +89,7 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
if err != nil {
logger.Fatal("%s", err.Error())
}
LogGreen("[New Directory] Watching new directory: %s", event.Name)
logger.Println("Watching directory: %s", event.Name)
}
}
return
@@ -113,33 +98,38 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
// Check for file writes
if event.Op&fsnotify.Write == fsnotify.Write {
// logger.Println("modified file: %s", event.Name)
var rebuild bool = false
// Iterate all file patterns
for _, pattern := range extensionsThatTriggerARebuild {
if strings.HasSuffix(event.Name, pattern) {
rebuild = true
rebuild = strings.HasSuffix(event.Name, pattern)
if err != nil {
logger.Fatal(err.Error())
}
if rebuild {
// Only build frontend when the file isn't a Go file
buildFrontend = !strings.HasSuffix(event.Name, "go")
break
}
}
if !rebuild {
if showWarnings {
LogDarkYellow("[File change] %s did not match extension list (%s)", event.Name, extensions)
}
logger.Println("Filename change: %s did not match extension list %s", event.Name, extensions)
return
}
LogGreen("[Attempting rebuild] %s updated", event.Name)
if buildFrontend {
logger.Println("Full rebuild triggered: %s updated", event.Name)
} else {
logger.Println("Partial build triggered: %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
}
newBinaryProcess := restartApp(logger, outputType, ldflags, compilerCommand, buildFrontend, debugBinaryProcess)
// If we have a new process, save it
if newBinaryProcess != nil {
debugBinaryProcess = newBinaryProcess
@@ -149,28 +139,23 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
})
// Get project dir
projectDir, err := os.Getwd()
dir, err := os.Getwd()
if err != nil {
return err
}
// Get all subdirectories
dirs, err := fs.GetSubdirectories(projectDir)
dirs, err := fs.GetSubdirectories(dir)
if err != nil {
return err
}
LogGreen("Watching (sub)/directory: %s", projectDir)
// 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
}
logger.Println("Watching directory: %s", dir)
err = watcher.Add(dir)
if err != nil {
logger.Fatal(err.Error())
@@ -182,7 +167,7 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
for quit == false {
select {
case <-quitChannel:
LogGreen("\nCaught quit")
println()
// Notify debouncer to quit
debounceQuit <- true
quit = true
@@ -197,7 +182,7 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
}
}
LogGreen("Development mode exited")
logger.Println("Development mode exited")
return nil
})
@@ -224,15 +209,15 @@ exit:
}
}
func restartApp(logger *clilogger.CLILogger, outputType string, ldflags string, compilerCommand string, debugBinaryProcess *process.Process, loglevel string) (*process.Process, error) {
func restartApp(logger *clilogger.CLILogger, outputType string, ldflags string, compilerCommand string, buildFrontend bool, debugBinaryProcess *process.Process) *process.Process {
appBinary, err := buildApp(logger, outputType, ldflags, compilerCommand)
appBinary, err := buildApp(logger, outputType, ldflags, compilerCommand, buildFrontend)
println()
if err != nil {
LogRed("Build error - continuing to run current version")
LogDarkYellow(err.Error())
return nil, nil
logger.Println("[ERROR] Build Failed: %s", err.Error())
return nil
}
logger.Println("Build new binary: %s", appBinary)
// Kill existing binary if need be
if debugBinaryProcess != nil {
@@ -248,7 +233,7 @@ func restartApp(logger *clilogger.CLILogger, outputType string, ldflags string,
// TODO: Generate `backend.js`
// Start up new binary
newProcess := process.NewProcess(logger, appBinary, "-loglevel", loglevel)
newProcess := process.NewProcess(logger, appBinary)
err = newProcess.Start()
if err != nil {
// Remove binary
@@ -259,13 +244,13 @@ func restartApp(logger *clilogger.CLILogger, outputType string, ldflags string,
logger.Fatal("Unable to start application: %s", err.Error())
}
return newProcess, nil
return newProcess
}
func buildApp(logger *clilogger.CLILogger, outputType string, ldflags string, compilerCommand string) (string, error) {
func buildApp(logger *clilogger.CLILogger, outputType string, ldflags string, compilerCommand string, buildFrontend bool) (string, error) {
// Create random output file
outputFile := fmt.Sprintf("dev-%d", time.Now().Unix())
outputFile := fmt.Sprintf("debug-%d", time.Now().Unix())
// Create BuildOptions
buildOptions := &build.Options{
@@ -277,7 +262,7 @@ func buildApp(logger *clilogger.CLILogger, outputType string, ldflags string, co
LDFlags: ldflags,
Compiler: compilerCommand,
OutputFile: outputFile,
IgnoreFrontend: true,
IgnoreFrontend: !buildFrontend,
}
return build.Build(buildOptions)

View File

@@ -1,164 +0,0 @@
package update
import (
"fmt"
"io"
"log"
"os"
"github.com/wailsapp/wails/v2/internal/shell"
"github.com/wailsapp/wails/v2/internal/github"
"github.com/leaanthony/clir"
"github.com/wailsapp/wails/v2/pkg/clilogger"
)
// AddSubcommand adds the `init` command for the Wails application
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 the Wails CLI.`)
// Setup flags
var prereleaseRequired bool
command.BoolFlag("pre", "Update CLI to latest Prerelease", &prereleaseRequired)
var specificVersion string
command.StringFlag("version", "Install a specific version (Overrides other flags) of the CLI", &specificVersion)
command.Action(func() error {
// Create logger
logger := clilogger.New(w)
// Print banner
app.PrintBanner()
logger.Println("Checking for updates...")
var desiredVersion *github.SemanticVersion
var err error
var valid bool
if len(specificVersion) > 0 {
// Check if this is a valid version
valid, err = github.IsValidTag(specificVersion)
if err == nil {
if !valid {
err = fmt.Errorf("version '%s' is invalid", specificVersion)
} else {
desiredVersion, err = github.NewSemanticVersion(specificVersion)
}
}
} else {
if prereleaseRequired {
desiredVersion, err = github.GetLatestPreRelease()
} else {
desiredVersion, err = github.GetLatestStableRelease()
}
}
if err != nil {
return err
}
fmt.Println()
fmt.Println(" Current Version : " + currentVersion)
if len(specificVersion) > 0 {
fmt.Printf(" Desired Version : v%s\n", desiredVersion)
} else {
if prereleaseRequired {
fmt.Printf(" Latest Prerelease : v%s\n", desiredVersion)
} else {
fmt.Printf(" Latest Release : v%s\n", desiredVersion)
}
}
return updateToVersion(logger, desiredVersion, len(specificVersion) > 0, currentVersion)
})
return nil
}
func updateToVersion(logger *clilogger.CLILogger, targetVersion *github.SemanticVersion, force bool, currentVersion string) error {
var targetVersionString = "v" + targetVersion.String()
// Early exit
if targetVersionString == currentVersion {
logger.Println("Looks like you're up to date!")
return nil
}
var desiredVersion string
if !force {
compareVersion := currentVersion
currentVersion, err := github.NewSemanticVersion(compareVersion)
if err != nil {
return err
}
var success bool
// Release -> Pre-Release = Massage current version to prerelease format
if targetVersion.IsPreRelease() && currentVersion.IsRelease() {
testVersion, err := github.NewSemanticVersion(compareVersion + "-0")
if err != nil {
return err
}
success, _ = targetVersion.IsGreaterThan(testVersion)
}
// Pre-Release -> Release = Massage target version to prerelease format
if targetVersion.IsRelease() && currentVersion.IsPreRelease() {
// We are ok with greater than or equal
mainversion := currentVersion.MainVersion()
targetVersion, err = github.NewSemanticVersion(targetVersion.String())
if err != nil {
return err
}
success, _ = targetVersion.IsGreaterThanOrEqual(mainversion)
}
// Release -> Release = Standard check
if (targetVersion.IsRelease() && currentVersion.IsRelease()) ||
(targetVersion.IsPreRelease() && currentVersion.IsPreRelease()) {
success, _ = targetVersion.IsGreaterThan(currentVersion)
}
// Compare
if !success {
logger.Println("Error: The requested version is lower than the current version.")
logger.Println("If this is what you really want to do, use `wails update -version %s`", targetVersionString)
return nil
}
desiredVersion = "v" + targetVersion.String()
} else {
desiredVersion = "v" + targetVersion.String()
}
fmt.Println()
logger.Print("Installing Wails CLI " + desiredVersion + "...")
// Run command in non module directory
homeDir, err := os.UserHomeDir()
if err != nil {
log.Fatal("Cannot find home directory! Please file a bug report!")
}
sout, serr, err := shell.RunCommand(homeDir, "go", "get", "github.com/wailsapp/wails/v2/cmd/wails@"+desiredVersion)
if err != nil {
logger.Println("Failed.")
logger.Println(sout + `\n` + serr)
return err
}
fmt.Println()
logger.Println("Wails CLI updated to " + desiredVersion)
return nil
}

View File

@@ -1,13 +1,8 @@
package main
import (
"fmt"
"os"
"github.com/wailsapp/wails/v2/internal/colour"
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/update"
"github.com/leaanthony/clir"
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/build"
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/debug"
@@ -22,17 +17,11 @@ func fatal(message string) {
os.Exit(1)
}
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 Appkit", version)
app.SetBannerFunction(banner)
app := clir.NewCli("Wails", "Go/HTML Application Framework", version)
build.AddBuildSubcommand(app, os.Stdout)
err = initialise.AddSubcommand(app, os.Stdout)
@@ -59,11 +48,6 @@ func main() {
fatal(err.Error())
}
err = update.AddSubcommand(app, os.Stdout, version)
if err != nil {
fatal(err.Error())
}
err = app.Run()
if err != nil {
println("\n\nERROR: " + err.Error())

View File

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

View File

@@ -1,13 +1,11 @@
module github.com/wailsapp/wails/v2
go 1.16
go 1.15
require (
github.com/Masterminds/semver v1.5.0
github.com/davecgh/go-spew v1.1.1
github.com/fatih/structtag v1.2.0
github.com/fsnotify/fsnotify v1.4.9
github.com/gorilla/websocket v1.4.1
github.com/imdario/mergo v0.3.11
github.com/jackmordaunt/icns v1.0.0
github.com/leaanthony/clir v1.0.4
@@ -15,15 +13,16 @@ require (
github.com/leaanthony/slicer v1.5.0
github.com/matryer/is v1.4.0
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/olekukonko/tablewriter v0.0.4
github.com/pkg/errors v0.9.1
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
github.com/xyproto/xpm v1.2.1
golang.org/x/net v0.0.0-20200822124328-c89045814202
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c
golang.org/x/tools v0.0.0-20200902012652-d1954cc86c82
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
nhooyr.io/websocket v1.8.6
)

View File

@@ -1,5 +1,3 @@
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -11,6 +9,7 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
@@ -40,6 +39,9 @@ github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGn
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8=
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leaanthony/clir v1.0.4 h1:Dov2y9zWJmZr7CjaCe86lKa4b5CSxskGAt2yBkoDyiU=
github.com/leaanthony/clir v1.0.4/go.mod h1:k/RBkdkFl18xkkACMCLt09bhiZnrGORoxmomeMvDpE0=
github.com/leaanthony/gosod v0.0.4 h1:v4hepo4IyL8E8c9qzDsvYcA0KGh7Npf8As74K5ibQpI=
@@ -60,13 +62,17 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLD
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tdewolff/minify v2.3.6+incompatible h1:2hw5/9ZvxhWLvBUnHE06gElGYz+Jv9R4Eys0XUzItYo=
github.com/tdewolff/minify v2.3.6+incompatible/go.mod h1:9Ov578KJUmAWpS6NeZwRZyT56Uf6o3Mcz9CEsg8USYs=
@@ -78,10 +84,6 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/wzshiming/ctc v1.2.3 h1:q+hW3IQNsjIlOFBTGZZZeIXTElFM4grF4spW/errh/c=
github.com/wzshiming/ctc v1.2.3/go.mod h1:2tVAtIY7SUyraSk0JxvwmONNPFL4ARavPuEsg5+KA28=
github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae h1:tpXvBXC3hpQBDCc9OojJZCQMVRAbT3TTdUMP8WguXkY=
github.com/wzshiming/winseq v0.0.0-20200112104235-db357dc107ae/go.mod h1:VTAq37rkGeV+WOybvZwjXiJOicICdpLCN8ifpISjK20=
github.com/xyproto/xpm v1.2.1 h1:trdvGjjWBsOOKzBBUPT6JvaIQM3acJEEYfbxN7M96wg=
github.com/xyproto/xpm v1.2.1/go.mod h1:cMnesLsD0PBXLgjDfTDEaKr8XyTFsnP1QycSqRw7BiY=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -98,26 +100,33 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/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 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200107162124-548cf772de50/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c h1:UIcGWL6/wpCfyGuJnRFJRurA+yj8RrW7Q6x2YMCXt6c=
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200902012652-d1954cc86c82 h1:shxDsb9Dz27xzk3A0DxP0JuJnZMpKrdg8+E14eiUAX4=
golang.org/x/tools v0.0.0-20200902012652-d1954cc86c82/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -2,40 +2,11 @@
package app
import (
"flag"
"strings"
"github.com/wailsapp/wails/v2/pkg/logger"
)
// Init initialises the application for a debug environment
func (a *App) Init() error {
// Indicate debug mode
a.debug = true
if a.appType == "desktop" {
// Enable dev tools
a.options.DevTools = true
}
// Set log levels
loglevel := flag.String("loglevel", "debug", "Loglevel to use - Trace, Debug, Info, Warning, Error")
flag.Parse()
if len(*loglevel) > 0 {
switch strings.ToLower(*loglevel) {
case "trace":
a.logger.SetLogLevel(logger.TRACE)
case "info":
a.logger.SetLogLevel(logger.INFO)
case "warning":
a.logger.SetLogLevel(logger.WARNING)
case "error":
a.logger.SetLogLevel(logger.ERROR)
default:
a.logger.SetLogLevel(logger.DEBUG)
}
}
// Enable dev tools
a.options.DevTools = true
return nil
}

View File

@@ -1,4 +1,4 @@
// +build !desktop,!hybrid,!server,!dev
// +build !desktop,!hybrid,!server
package app
@@ -8,9 +8,8 @@ package app
// will be unknown and the application will not work as expected.
import (
"os"
"github.com/wailsapp/wails/v2/internal/logger"
"os"
"github.com/wailsapp/wails/v2/pkg/options"
)
@@ -39,3 +38,7 @@ func (a *App) Run() error {
os.Exit(1)
return nil
}
// Bind the dummy interface
func (a *App) Bind(_ interface{}) {
}

View File

@@ -3,9 +3,6 @@
package app
import (
"context"
"sync"
"github.com/wailsapp/wails/v2/internal/binding"
"github.com/wailsapp/wails/v2/internal/ffenestri"
"github.com/wailsapp/wails/v2/internal/logger"
@@ -20,8 +17,6 @@ import (
// App defines a Wails application structure
type App struct {
appType string
window *ffenestri.Application
servicebus *servicebus.ServiceBus
logger *logger.Logger
@@ -29,13 +24,12 @@ type App struct {
options *options.App
// Subsystems
log *subsystem.Log
runtime *subsystem.Runtime
event *subsystem.Event
//binding *subsystem.Binding
log *subsystem.Log
runtime *subsystem.Runtime
event *subsystem.Event
binding *subsystem.Binding
call *subsystem.Call
menu *subsystem.Menu
url *subsystem.URL
dispatcher *messagedispatcher.Dispatcher
menuManager *menumanager.Manager
@@ -85,15 +79,11 @@ func CreateApp(appoptions *options.App) (*App, error) {
window := ffenestri.NewApplicationWithConfig(appoptions, myLogger, menuManager)
// Create binding exemptions - Ugly hack. There must be a better way
bindingExemptions := []interface{}{appoptions.Startup, appoptions.Shutdown}
result := &App{
appType: "desktop",
window: window,
servicebus: servicebus.New(myLogger),
logger: myLogger,
bindings: binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions),
bindings: binding.NewBindings(myLogger),
menuManager: menuManager,
startupCallback: appoptions.Startup,
shutdownCallback: appoptions.Shutdown,
@@ -113,10 +103,13 @@ func (a *App) Run() error {
var err error
// Setup a context
var subsystemWaitGroup sync.WaitGroup
parentContext := context.WithValue(context.Background(), "waitgroup", &subsystemWaitGroup)
ctx, cancel := context.WithCancel(parentContext)
// Setup signal handler
signalsubsystem, err := signal.NewManager(a.servicebus, a.logger)
if err != nil {
return err
}
a.signal = signalsubsystem
a.signal.Start()
// Start the service bus
a.servicebus.Debug()
@@ -125,7 +118,7 @@ func (a *App) Run() error {
return err
}
runtimesubsystem, err := subsystem.NewRuntime(ctx, a.servicebus, a.logger, a.startupCallback)
runtimesubsystem, err := subsystem.NewRuntime(a.servicebus, a.logger, a.startupCallback, a.shutdownCallback)
if err != nil {
return err
}
@@ -139,6 +132,17 @@ func (a *App) Run() error {
a.loglevelStore = a.runtime.GoRuntime().Store.New("wails:loglevel", a.options.LogLevel)
a.appconfigStore = a.runtime.GoRuntime().Store.New("wails:appconfig", a.options)
// Start the binding subsystem
bindingsubsystem, err := subsystem.NewBinding(a.servicebus, a.logger, a.bindings, a.runtime.GoRuntime())
if err != nil {
return err
}
a.binding = bindingsubsystem
err = a.binding.Start()
if err != nil {
return err
}
// Start the logging subsystem
log, err := subsystem.NewLog(a.servicebus, a.logger, a.loglevelStore)
if err != nil {
@@ -161,32 +165,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)
event, err := subsystem.NewEvent(a.servicebus, a.logger)
if err != nil {
return err
}
a.event = eventsubsystem
a.event = event
err = a.event.Start()
if err != nil {
return err
}
// Start the menu subsystem
menusubsystem, err := subsystem.NewMenu(ctx, a.servicebus, a.logger, a.menuManager)
menusubsystem, err := subsystem.NewMenu(a.servicebus, a.logger, a.menuManager)
if err != nil {
return err
}
@@ -197,11 +188,11 @@ func (a *App) Run() error {
}
// Start the call subsystem
callSubsystem, err := subsystem.NewCall(ctx, a.servicebus, a.logger, a.bindings.DB(), a.runtime.GoRuntime())
call, err := subsystem.NewCall(a.servicebus, a.logger, a.bindings.DB(), a.runtime.GoRuntime())
if err != nil {
return err
}
a.call = callSubsystem
a.call = call
err = a.call.Start()
if err != nil {
return err
@@ -213,42 +204,23 @@ 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)
result := a.window.Run(dispatcher, bindingDump, a.debug)
a.logger.Trace("Ffenestri.Run() exited")
if err != nil {
return err
}
// Close down all the subsystems
a.logger.Trace("Cancelling subsystems")
cancel()
subsystemWaitGroup.Wait()
a.logger.Trace("Cancelling dispatcher")
dispatcher.Close()
// Close log
a.logger.Trace("Stopping log")
log.Close()
a.logger.Trace("Stopping Service bus")
err = a.servicebus.Stop()
if err != nil {
return err
}
// Shutdown callback
if a.shutdownCallback != nil {
a.shutdownCallback()
}
return nil
return result
}
// Bind a struct to the application by passing in
// a pointer to it
func (a *App) Bind(structPtr interface{}) {
// Add the struct to the bindings
err := a.bindings.Add(structPtr)
if err != nil {
a.logger.Fatal("Error during binding: " + err.Error())
}
}

View File

@@ -1,244 +0,0 @@
// +build dev
package app
import (
"context"
"sync"
"github.com/wailsapp/wails/v2/internal/bridge"
"github.com/wailsapp/wails/v2/internal/menumanager"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/internal/binding"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/messagedispatcher"
"github.com/wailsapp/wails/v2/internal/runtime"
"github.com/wailsapp/wails/v2/internal/servicebus"
"github.com/wailsapp/wails/v2/internal/signal"
"github.com/wailsapp/wails/v2/internal/subsystem"
)
// App defines a Wails application structure
type App struct {
appType string
servicebus *servicebus.ServiceBus
logger *logger.Logger
signal *signal.Manager
options *options.App
// Subsystems
log *subsystem.Log
runtime *subsystem.Runtime
event *subsystem.Event
//binding *subsystem.Binding
call *subsystem.Call
menu *subsystem.Menu
dispatcher *messagedispatcher.Dispatcher
menuManager *menumanager.Manager
// Indicates if the app is in debug mode
debug bool
// This is our binding DB
bindings *binding.Bindings
// Application Stores
loglevelStore *runtime.Store
appconfigStore *runtime.Store
// Startup/Shutdown
startupCallback func(*runtime.Runtime)
shutdownCallback func()
// Bridge
bridge *bridge.Bridge
}
// Create App
func CreateApp(appoptions *options.App) (*App, error) {
// Merge default options
options.MergeDefaults(appoptions)
// Set up logger
myLogger := logger.New(appoptions.Logger)
// Create the menu manager
menuManager := menumanager.NewManager()
// Process the application menu
menuManager.SetApplicationMenu(options.GetApplicationMenu(appoptions))
// Process context menus
contextMenus := options.GetContextMenus(appoptions)
for _, contextMenu := range contextMenus {
menuManager.AddContextMenu(contextMenu)
}
// Process tray menus
trayMenus := options.GetTrayMenus(appoptions)
for _, trayMenu := range trayMenus {
menuManager.AddTrayMenu(trayMenu)
}
// Create binding exemptions - Ugly hack. There must be a better way
bindingExemptions := []interface{}{appoptions.Startup, appoptions.Shutdown}
result := &App{
appType: "dev",
bindings: binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions),
logger: myLogger,
servicebus: servicebus.New(myLogger),
startupCallback: appoptions.Startup,
shutdownCallback: appoptions.Shutdown,
bridge: bridge.NewBridge(myLogger),
menuManager: menuManager,
}
result.options = appoptions
// Initialise the app
err := result.Init()
return result, err
}
// Run the application
func (a *App) Run() error {
var err error
// Setup a context
var subsystemWaitGroup sync.WaitGroup
parentContext := context.WithValue(context.Background(), "waitgroup", &subsystemWaitGroup)
ctx, cancel := context.WithCancel(parentContext)
// Start the service bus
a.servicebus.Debug()
err = a.servicebus.Start()
if err != nil {
return err
}
runtimesubsystem, err := subsystem.NewRuntime(ctx, a.servicebus, a.logger, a.startupCallback)
if err != nil {
return err
}
a.runtime = runtimesubsystem
err = a.runtime.Start()
if err != nil {
return err
}
// Application Stores
a.loglevelStore = a.runtime.GoRuntime().Store.New("wails:loglevel", a.options.LogLevel)
a.appconfigStore = a.runtime.GoRuntime().Store.New("wails:appconfig", a.options)
// Start the logging subsystem
log, err := subsystem.NewLog(a.servicebus, a.logger, a.loglevelStore)
if err != nil {
return err
}
a.log = log
err = a.log.Start()
if err != nil {
return err
}
// create the dispatcher
dispatcher, err := messagedispatcher.New(a.servicebus, a.logger)
if err != nil {
return err
}
a.dispatcher = dispatcher
err = dispatcher.Start()
if err != nil {
return err
}
// Start the eventing subsystem
eventsubsystem, err := subsystem.NewEvent(ctx, a.servicebus, a.logger)
if err != nil {
return err
}
a.event = eventsubsystem
err = a.event.Start()
if err != nil {
return err
}
// Start the menu subsystem
menusubsystem, err := subsystem.NewMenu(ctx, a.servicebus, a.logger, a.menuManager)
if err != nil {
return err
}
a.menu = menusubsystem
err = a.menu.Start()
if err != nil {
return err
}
// Start the call subsystem
callSubsystem, err := subsystem.NewCall(ctx, a.servicebus, a.logger, a.bindings.DB(), a.runtime.GoRuntime())
if err != nil {
return err
}
a.call = callSubsystem
err = a.call.Start()
if err != nil {
return err
}
// Dump bindings as a debug
bindingDump, err := a.bindings.ToJSON()
if err != nil {
return err
}
// 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 {
return err
}
// Close down all the subsystems
a.logger.Trace("Cancelling subsystems")
cancel()
subsystemWaitGroup.Wait()
a.logger.Trace("Cancelling dispatcher")
dispatcher.Close()
// Close log
a.logger.Trace("Stopping log")
log.Close()
a.logger.Trace("Stopping Service bus")
err = a.servicebus.Stop()
if err != nil {
return err
}
// Shutdown callback
if a.shutdownCallback != nil {
a.shutdownCallback()
}
return nil
}

View File

@@ -75,7 +75,7 @@ func CreateApp(options *Options) *App {
webserver: webserver.NewWebServer(myLogger),
servicebus: servicebus.New(myLogger),
logger: myLogger,
bindings: binding.NewBindings(myLogger, options.Bind),
bindings: binding.NewBindings(myLogger),
}
// Initialise the app
@@ -192,3 +192,14 @@ func (a *App) Run() error {
return cli.Run()
}
// Bind a struct to the application by passing in
// a pointer to it
func (a *App) Bind(structPtr interface{}) {
// Add the struct to the bindings
err := a.bindings.Add(structPtr)
if err != nil {
a.logger.Fatal("Error during binding: " + err.Error())
}
}

View File

@@ -6,13 +6,10 @@ import (
"os"
"path/filepath"
"github.com/wailsapp/wails/v2/pkg/options"
"github.com/leaanthony/clir"
"github.com/wailsapp/wails/v2/internal/binding"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/messagedispatcher"
"github.com/wailsapp/wails/v2/internal/runtime"
"github.com/wailsapp/wails/v2/internal/servicebus"
"github.com/wailsapp/wails/v2/internal/subsystem"
"github.com/wailsapp/wails/v2/internal/webserver"
@@ -20,16 +17,12 @@ import (
// App defines a Wails application structure
type App struct {
appType string
binding *subsystem.Binding
call *subsystem.Call
event *subsystem.Event
log *subsystem.Log
runtime *subsystem.Runtime
options *options.App
bindings *binding.Bindings
logger *logger.Logger
dispatcher *messagedispatcher.Dispatcher
@@ -37,40 +30,28 @@ type App struct {
webserver *webserver.WebServer
debug bool
// Application Stores
loglevelStore *runtime.Store
appconfigStore *runtime.Store
// Startup/Shutdown
startupCallback func(*runtime.Runtime)
shutdownCallback func()
}
// Create App
func CreateApp(appoptions *options.App) (*App, error) {
func CreateApp(options *Options) *App {
options.mergeDefaults()
// We ignore the inputs (for now)
// Merge default options
options.MergeDefaults(appoptions)
// Set up logger
myLogger := logger.New(appoptions.Logger)
myLogger.SetLogLevel(appoptions.LogLevel)
// TODO: Allow logger output override on CLI
myLogger := logger.New(os.Stdout)
myLogger.SetLogLevel(logger.TRACE)
result := &App{
appType: "server",
bindings: binding.NewBindings(myLogger, options.Bind),
logger: myLogger,
servicebus: servicebus.New(myLogger),
webserver: webserver.NewWebServer(myLogger),
startupCallback: appoptions.Startup,
shutdownCallback: appoptions.Shutdown,
bindings: binding.NewBindings(myLogger),
logger: myLogger,
servicebus: servicebus.New(myLogger),
webserver: webserver.NewWebServer(myLogger),
}
// Initialise app
result.Init()
return result, nil
return result
}
// Run the application
@@ -107,21 +88,8 @@ func (a *App) Run() error {
if debugMode {
a.servicebus.Debug()
}
// Start the runtime
runtime, err := subsystem.NewRuntime(a.servicebus, a.logger, a.startupCallback, a.shutdownCallback)
if err != nil {
return err
}
a.runtime = runtime
a.runtime.Start()
// Application Stores
a.loglevelStore = a.runtime.GoRuntime().Store.New("wails:loglevel", a.options.LogLevel)
a.appconfigStore = a.runtime.GoRuntime().Store.New("wails:appconfig", a.options)
a.servicebus.Start()
log, err := subsystem.NewLog(a.servicebus, a.logger, a.loglevelStore)
log, err := subsystem.NewLog(a.servicebus, a.logger)
if err != nil {
return err
}
@@ -134,6 +102,14 @@ func (a *App) Run() error {
a.dispatcher = dispatcher
a.dispatcher.Start()
// Start the runtime
runtime, err := subsystem.NewRuntime(a.servicebus, a.logger)
if err != nil {
return err
}
a.runtime = runtime
a.runtime.Start()
// Start the binding subsystem
binding, err := subsystem.NewBinding(a.servicebus, a.logger, a.bindings, runtime.GoRuntime())
if err != nil {
@@ -151,7 +127,7 @@ func (a *App) Run() error {
a.event.Start()
// Start the call subsystem
call, err := subsystem.NewCall(a.servicebus, a.logger, a.bindings.DB(), a.runtime.GoRuntime())
call, err := subsystem.NewCall(a.servicebus, a.logger, a.bindings.DB())
if err != nil {
return err
}
@@ -171,3 +147,14 @@ func (a *App) Run() error {
return cli.Run()
}
// Bind a struct to the application by passing in
// a pointer to it
func (a *App) Bind(structPtr interface{}) {
// Add the struct to the bindings
err := a.bindings.Add(structPtr)
if err != nil {
a.logger.Fatal("Error during binding: " + err.Error())
}
}

View File

@@ -1,11 +0,0 @@
{
"name": "backend",
"version": "1.0.0",
"description": "Package to wrap backend method calls",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}

View File

@@ -2,53 +2,28 @@ package binding
import (
"fmt"
"reflect"
"runtime"
"strings"
"github.com/leaanthony/slicer"
"github.com/wailsapp/wails/v2/internal/logger"
)
type Bindings struct {
db *DB
logger logger.CustomLogger
exemptions slicer.StringSlicer
db *DB
logger logger.CustomLogger
}
// NewBindings returns a new Bindings object
func NewBindings(logger *logger.Logger, structPointersToBind []interface{}, exemptions []interface{}) *Bindings {
result := &Bindings{
func NewBindings(logger *logger.Logger) *Bindings {
return &Bindings{
db: newDB(),
logger: logger.CustomLogger("Bindings"),
}
for _, exemption := range exemptions {
if exemptions == nil {
continue
}
name := runtime.FuncForPC(reflect.ValueOf(exemption).Pointer()).Name()
// Yuk yuk yuk! Is there a better way?
name = strings.TrimSuffix(name, "-fm")
result.exemptions.Add(name)
}
// Add the structs to bind
for _, ptr := range structPointersToBind {
err := result.Add(ptr)
if err != nil {
logger.Fatal("Error during binding: " + err.Error())
}
}
return result
}
// Add the given struct methods to the Bindings
func (b *Bindings) Add(structPtr interface{}) error {
methods, err := b.getMethods(structPtr)
methods, err := getMethods(structPtr)
if err != nil {
return fmt.Errorf("cannot bind value to app: %s", err.Error())
}
@@ -61,6 +36,7 @@ func (b *Bindings) Add(structPtr interface{}) error {
// Add it as a regular method
b.db.AddMethod(packageName, structName, methodName, method)
}
return nil
}

View File

@@ -30,9 +30,6 @@ func (b *BoundMethod) OutputCount() int {
func (b *BoundMethod) ParseArgs(args []json.RawMessage) ([]interface{}, error) {
result := make([]interface{}, b.InputCount())
if len(args) != b.InputCount() {
return nil, fmt.Errorf("received %d arguments to method '%s', expected %d", len(args), b.Name, b.InputCount())
}
for index, arg := range args {
typ := b.Inputs[index].reflectType
inputValue := reflect.New(typ).Interface()

View File

@@ -1,174 +0,0 @@
package binding
import (
"bytes"
_ "embed"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/wailsapp/wails/v2/internal/fs"
"github.com/leaanthony/slicer"
)
const _comment = `
const backend = {
main: {
"xbarApp": {
"GetCategories": () => {
window.backend.main.xbarApp.GetCategories.call(arguments);
},
/**
* @param {string} arg1
*/
"InstallPlugin": (arg1) => {
window.backend.main.xbarApp.InstallPlugin.call(arguments);
},
"GetPlugins": () => {
window.backend.main.xbarApp.GetPlugins.call(arguments);
}
}
}
}
export default backend;`
//go:embed assets/package.json
var packageJSON []byte
func (b *Bindings) GenerateBackendJS() {
store := b.db.store
var output bytes.Buffer
output.WriteString(`// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Ă‚ MODIWL
// This file is automatically generated. DO NOT EDIT
const backend = {`)
output.WriteString("\n")
var sortedPackageNames slicer.StringSlicer
for packageName := range store {
sortedPackageNames.Add(packageName)
}
sortedPackageNames.Sort()
sortedPackageNames.Each(func(packageName string) {
packages := store[packageName]
output.WriteString(fmt.Sprintf(" \"%s\": {", packageName))
output.WriteString("\n")
var sortedStructNames slicer.StringSlicer
for structName := range packages {
sortedStructNames.Add(structName)
}
sortedStructNames.Sort()
sortedStructNames.Each(func(structName string) {
structs := packages[structName]
output.WriteString(fmt.Sprintf(" \"%s\": {", structName))
output.WriteString("\n")
var sortedMethodNames slicer.StringSlicer
for methodName := range structs {
sortedMethodNames.Add(methodName)
}
sortedMethodNames.Sort()
sortedMethodNames.Each(func(methodName string) {
methodDetails := structs[methodName]
output.WriteString(" /**\n")
output.WriteString(" * " + methodName + "\n")
var args slicer.StringSlicer
for count, input := range methodDetails.Inputs {
arg := fmt.Sprintf("arg%d", count+1)
args.Add(arg)
output.WriteString(fmt.Sprintf(" * @param {%s} %s - Go Type: %s\n", goTypeToJSDocType(input.TypeName), arg, input.TypeName))
}
returnType := "Promise"
returnTypeDetails := ""
if methodDetails.OutputCount() > 0 {
firstType := goTypeToJSDocType(methodDetails.Outputs[0].TypeName)
returnType += "<" + firstType
if methodDetails.OutputCount() == 2 {
secondType := goTypeToJSDocType(methodDetails.Outputs[1].TypeName)
returnType += "|" + secondType
}
returnType += ">"
returnTypeDetails = " - Go Type: " + methodDetails.Outputs[0].TypeName
}
output.WriteString(" * @returns {" + returnType + "} " + returnTypeDetails + "\n")
output.WriteString(" */\n")
argsString := args.Join(", ")
output.WriteString(fmt.Sprintf(" \"%s\": (%s) => {", methodName, argsString))
output.WriteString("\n")
output.WriteString(fmt.Sprintf(" return window.backend.%s.%s.%s(%s);", packageName, structName, methodName, argsString))
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;`)
output.WriteString("\n")
dirname, err := fs.RelativeToCwd("frontend/src/backend")
if err != nil {
log.Fatal(err)
}
if !fs.DirExists(dirname) {
err := fs.Mkdir(dirname)
if err != nil {
log.Fatal(err)
}
}
packageJsonFile := filepath.Join(dirname, "package.json")
if !fs.FileExists(packageJsonFile) {
err := os.WriteFile(packageJsonFile, packageJSON, 0755)
if err != nil {
log.Fatal(err)
}
}
filename := filepath.Join(dirname, "index.js")
err = os.WriteFile(filename, output.Bytes(), 0755)
if err != nil {
log.Fatal(err)
}
}
func goTypeToJSDocType(input string) string {
switch true {
case input == "string":
return "string"
case input == "error":
return "Error"
case
strings.HasPrefix(input, "int"),
strings.HasPrefix(input, "uint"),
strings.HasPrefix(input, "float"):
return "number"
case input == "bool":
return "boolean"
case strings.HasPrefix(input, "[]"):
arrayType := goTypeToJSDocType(input[2:])
return "Array.<" + arrayType + ">"
default:
return "any"
}
}

View File

@@ -23,7 +23,7 @@ func isStruct(value interface{}) bool {
return reflect.ValueOf(value).Kind() == reflect.Struct
}
func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) {
func getMethods(value interface{}) ([]*BoundMethod, error) {
// Create result placeholder
var result []*BoundMethod
@@ -56,11 +56,6 @@ func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) {
fullMethodName := baseName + "." + methodName
method := structValue.MethodByName(methodName)
methodReflectName := runtime.FuncForPC(methodDef.Func.Pointer()).Name()
if b.exemptions.Contains(methodReflectName) {
continue
}
// Create new method
boundMethod := &BoundMethod{
Name: fullMethodName,

View File

@@ -1,113 +0,0 @@
package bridge
import (
"context"
"log"
"net/http"
"sync"
"github.com/wailsapp/wails/v2/internal/menumanager"
"github.com/wailsapp/wails/v2/internal/messagedispatcher"
"github.com/gorilla/websocket"
"github.com/wailsapp/wails/v2/internal/logger"
)
type Bridge struct {
upgrader websocket.Upgrader
server *http.Server
myLogger *logger.Logger
bindings string
dispatcher *messagedispatcher.Dispatcher
mu sync.Mutex
sessions map[string]*session
ctx context.Context
cancel context.CancelFunc
// Dialog client
dialog *messagedispatcher.DispatchClient
// Menus
menumanager *menumanager.Manager
}
func NewBridge(myLogger *logger.Logger) *Bridge {
result := &Bridge{
myLogger: myLogger,
upgrader: websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }},
sessions: make(map[string]*session),
}
myLogger.SetLogLevel(1)
ctx, cancel := context.WithCancel(context.Background())
result.ctx = ctx
result.cancel = cancel
result.server = &http.Server{Addr: ":34115"}
http.HandleFunc("/bridge", result.wsBridgeHandler)
return result
}
func (b *Bridge) Run(dispatcher *messagedispatcher.Dispatcher, menumanager *menumanager.Manager, bindings string, debug bool) error {
// Ensure we cancel the context when we shutdown
defer b.cancel()
b.bindings = bindings
b.dispatcher = dispatcher
b.menumanager = menumanager
// Setup dialog handler
dialogClient := NewDialogClient(b.myLogger)
b.dialog = dispatcher.RegisterClient(dialogClient)
dialogClient.dispatcher = b.dialog
b.myLogger.Info("Bridge mode started.")
err := b.server.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
return err
}
return nil
}
func (b *Bridge) wsBridgeHandler(w http.ResponseWriter, r *http.Request) {
c, err := b.upgrader.Upgrade(w, r, nil)
if err != nil {
log.Print("upgrade:", err)
return
}
if err != nil {
http.Error(w, "Could not open websocket connection", http.StatusBadRequest)
}
b.myLogger.Info("Connection from frontend accepted [%s].", c.RemoteAddr().String())
b.startSession(c)
}
func (b *Bridge) startSession(conn *websocket.Conn) {
// Create a new session for this connection
s := newSession(conn, b.menumanager, b.bindings, b.dispatcher, b.myLogger, b.ctx)
// Setup the close handler
conn.SetCloseHandler(func(int, string) error {
b.myLogger.Info("Connection dropped [%s].", s.Identifier())
b.dispatcher.RemoveClient(s.client)
b.mu.Lock()
delete(b.sessions, s.Identifier())
b.mu.Unlock()
return nil
})
b.mu.Lock()
go s.start(len(b.sessions) == 0)
b.sessions[s.Identifier()] = s
b.mu.Unlock()
}

View File

@@ -1,134 +0,0 @@
package bridge
import (
"github.com/wailsapp/wails/v2/pkg/options/dialog"
)
type BridgeClient struct {
session *session
// Tray menu cache to send to reconnecting clients
messageCache chan string
}
func (b BridgeClient) DeleteTrayMenuByID(id string) {
b.session.sendMessage("TD" + id)
}
func NewBridgeClient() *BridgeClient {
return &BridgeClient{
messageCache: make(chan string, 100),
}
}
func (b BridgeClient) Quit() {
b.session.log.Info("Quit unsupported in Bridge mode")
}
func (b BridgeClient) NotifyEvent(message string) {
//b.session.sendMessage("n" + message)
b.session.log.Info("NotifyEvent: %s", message)
b.session.log.Info("NotifyEvent unsupported in Bridge mode")
}
func (b BridgeClient) CallResult(message string) {
b.session.sendMessage("c" + message)
}
func (b BridgeClient) OpenDialog(dialogOptions *dialog.OpenDialog, callbackID string) {
// Handled by dialog_client
}
func (b BridgeClient) SaveDialog(dialogOptions *dialog.SaveDialog, callbackID string) {
// Handled by dialog_client
}
func (b BridgeClient) MessageDialog(dialogOptions *dialog.MessageDialog, callbackID string) {
// Handled by dialog_client
}
func (b BridgeClient) WindowSetTitle(title string) {
b.session.log.Info("WindowSetTitle unsupported in Bridge mode")
}
func (b BridgeClient) WindowShow() {
b.session.log.Info("WindowShow unsupported in Bridge mode")
}
func (b BridgeClient) WindowHide() {
b.session.log.Info("WindowHide unsupported in Bridge mode")
}
func (b BridgeClient) WindowCenter() {
b.session.log.Info("WindowCenter unsupported in Bridge mode")
}
func (b BridgeClient) WindowMaximise() {
b.session.log.Info("WindowMaximise unsupported in Bridge mode")
}
func (b BridgeClient) WindowUnmaximise() {
b.session.log.Info("WindowUnmaximise unsupported in Bridge mode")
}
func (b BridgeClient) WindowMinimise() {
b.session.log.Info("WindowMinimise unsupported in Bridge mode")
}
func (b BridgeClient) WindowUnminimise() {
b.session.log.Info("WindowUnminimise unsupported in Bridge mode")
}
func (b BridgeClient) WindowPosition(x int, y int) {
b.session.log.Info("WindowPosition unsupported in Bridge mode")
}
func (b BridgeClient) WindowSize(width int, height int) {
b.session.log.Info("WindowSize unsupported in Bridge mode")
}
func (b BridgeClient) WindowSetMinSize(width int, height int) {
b.session.log.Info("WindowSetMinSize unsupported in Bridge mode")
}
func (b BridgeClient) WindowSetMaxSize(width int, height int) {
b.session.log.Info("WindowSetMaxSize unsupported in Bridge mode")
}
func (b BridgeClient) WindowFullscreen() {
b.session.log.Info("WindowFullscreen unsupported in Bridge mode")
}
func (b BridgeClient) WindowUnFullscreen() {
b.session.log.Info("WindowUnFullscreen unsupported in Bridge mode")
}
func (b BridgeClient) WindowSetColour(colour int) {
b.session.log.Info("WindowSetColour unsupported in Bridge mode")
}
func (b BridgeClient) DarkModeEnabled(callbackID string) {
b.session.log.Info("DarkModeEnabled unsupported in Bridge mode")
}
func (b BridgeClient) SetApplicationMenu(menuJSON string) {
b.session.log.Info("SetApplicationMenu unsupported in Bridge mode")
}
func (b BridgeClient) SetTrayMenu(trayMenuJSON string) {
b.session.sendMessage("TS" + trayMenuJSON)
}
func (b BridgeClient) UpdateTrayMenuLabel(trayMenuJSON string) {
b.session.sendMessage("TU" + trayMenuJSON)
}
func (b BridgeClient) UpdateContextMenu(contextMenuJSON string) {
b.session.log.Info("UpdateContextMenu unsupported in Bridge mode")
}
func newBridgeClient(session *session) *BridgeClient {
return &BridgeClient{
session: session,
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,144 +0,0 @@
package bridge
import (
"fmt"
"os/exec"
"strconv"
"strings"
"github.com/wailsapp/wails/v2/internal/messagedispatcher"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/leaanthony/slicer"
"github.com/wailsapp/wails/v2/pkg/options/dialog"
)
type DialogClient struct {
dispatcher *messagedispatcher.DispatchClient
log *logger.Logger
}
func (d *DialogClient) DeleteTrayMenuByID(id string) {
}
func NewDialogClient(log *logger.Logger) *DialogClient {
return &DialogClient{
log: log,
}
}
func (d *DialogClient) Quit() {
}
func (d *DialogClient) NotifyEvent(message string) {
}
func (d *DialogClient) CallResult(message string) {
}
func (d *DialogClient) OpenDialog(dialogOptions *dialog.OpenDialog, callbackID string) {
}
func (d *DialogClient) SaveDialog(dialogOptions *dialog.SaveDialog, callbackID string) {
}
func (d *DialogClient) MessageDialog(dialogOptions *dialog.MessageDialog, callbackID string) {
osa, err := exec.LookPath("osascript")
if err != nil {
d.log.Info("MessageDialog unavailable (osascript not found)")
return
}
var btns slicer.StringSlicer
defaultButton := ""
cancelButton := ""
for index, btn := range dialogOptions.Buttons {
btns.Add(strconv.Quote(btn))
if btn == dialogOptions.DefaultButton {
defaultButton = fmt.Sprintf("default button %d", index+1)
}
if btn == dialogOptions.CancelButton {
cancelButton = fmt.Sprintf("cancel button %d", index+1)
}
}
buttons := "{" + btns.Join(",") + "}"
script := fmt.Sprintf("display dialog \"%s\" buttons %s %s %s with title \"%s\"", dialogOptions.Message, buttons, defaultButton, cancelButton, dialogOptions.Title)
go func() {
out, err := exec.Command(osa, "-e", script).Output()
if err != nil {
// Assume user has pressed cancel button
if dialogOptions.CancelButton != "" {
d.dispatcher.DispatchMessage("DM" + callbackID + "|" + dialogOptions.CancelButton)
return
}
d.log.Error("Dialog had bad exit code. If this was a Cancel button, add 'CancelButton' to the dialog.MessageDialog struct. Error: %s", err.Error())
d.dispatcher.DispatchMessage("DM" + callbackID + "|error - check logs")
return
}
buttonPressed := strings.TrimSpace(strings.TrimPrefix(string(out), "button returned:"))
d.dispatcher.DispatchMessage("DM" + callbackID + "|" + buttonPressed)
}()
}
func (d *DialogClient) WindowSetTitle(title string) {
}
func (d *DialogClient) WindowShow() {
}
func (d *DialogClient) WindowHide() {
}
func (d *DialogClient) WindowCenter() {
}
func (d *DialogClient) WindowMaximise() {
}
func (d *DialogClient) WindowUnmaximise() {
}
func (d *DialogClient) WindowMinimise() {
}
func (d *DialogClient) WindowUnminimise() {
}
func (d *DialogClient) WindowPosition(x int, y int) {
}
func (d *DialogClient) WindowSize(width int, height int) {
}
func (d *DialogClient) WindowSetMinSize(width int, height int) {
}
func (d *DialogClient) WindowSetMaxSize(width int, height int) {
}
func (d *DialogClient) WindowFullscreen() {
}
func (d *DialogClient) WindowUnFullscreen() {
}
func (d *DialogClient) WindowSetColour(colour int) {
}
func (d *DialogClient) DarkModeEnabled(callbackID string) {
}
func (d *DialogClient) SetApplicationMenu(menuJSON string) {
}
func (d *DialogClient) SetTrayMenu(trayMenuJSON string) {
}
func (d *DialogClient) UpdateTrayMenuLabel(trayMenuJSON string) {
}
func (d *DialogClient) UpdateContextMenu(contextMenuJSON string) {
}

View File

@@ -1,162 +0,0 @@
package bridge
import (
"context"
_ "embed"
"log"
"runtime"
"time"
"github.com/wailsapp/wails/v2/internal/menumanager"
"github.com/wailsapp/wails/v2/internal/messagedispatcher"
"github.com/gorilla/websocket"
"github.com/wailsapp/wails/v2/internal/logger"
)
//go:embed darwin.js
var darwinRuntime string
// session represents a single websocket session
type session struct {
bindings string
conn *websocket.Conn
//eventManager interfaces.EventManager
log *logger.Logger
//ipc interfaces.IPCManager
// Mutex for writing to the socket
shutdown chan bool
writeChan chan []byte
done bool
// context
ctx context.Context
// client
client *messagedispatcher.DispatchClient
// Menus
menumanager *menumanager.Manager
}
func newSession(conn *websocket.Conn, menumanager *menumanager.Manager, bindings string, dispatcher *messagedispatcher.Dispatcher, logger *logger.Logger, ctx context.Context) *session {
result := &session{
conn: conn,
bindings: bindings,
log: logger,
shutdown: make(chan bool),
writeChan: make(chan []byte, 100),
ctx: ctx,
menumanager: menumanager,
}
result.client = dispatcher.RegisterClient(newBridgeClient(result))
return result
}
// Identifier returns a string identifier for the remote connection.
// Taking the form of the client's <ip address>:<port>.
func (s *session) Identifier() string {
if s.conn != nil {
return s.conn.RemoteAddr().String()
}
return ""
}
func (s *session) sendMessage(msg string) error {
if !s.done {
s.writeChan <- []byte(msg)
}
return nil
}
func (s *session) start(firstSession bool) {
s.log.SetLogLevel(1)
s.log.Info("Connected to frontend.")
go s.writePump()
var wailsRuntime string
switch runtime.GOOS {
case "darwin":
wailsRuntime = darwinRuntime
default:
log.Fatal("platform not supported")
}
bindingsMessage := "window.wailsbindings = `" + s.bindings + "`;"
s.log.Info(bindingsMessage)
bootstrapMessage := bindingsMessage + wailsRuntime
s.sendMessage("b" + bootstrapMessage)
// Send menus
traymenus, err := s.menumanager.GetTrayMenus()
if err != nil {
s.log.Error(err.Error())
}
for _, trayMenu := range traymenus {
s.sendMessage("TS" + trayMenu)
}
for {
messageType, buffer, err := s.conn.ReadMessage()
if messageType == -1 {
return
}
if err != nil {
s.log.Error("Error reading message: %v", err)
err = s.conn.Close()
return
}
message := string(buffer)
s.log.Debug("Got message: %#v\n", message)
// Dispatch message as normal
s.client.DispatchMessage(message)
if s.done {
break
}
}
}
// Shutdown
func (s *session) Shutdown() {
s.conn.Close()
s.done = true
s.log.Info("session %v exit", s.Identifier())
}
// writePump pulls messages from the writeChan and sends them to the client
// since it uses a channel to read the messages the socket is protected without locks
func (s *session) writePump() {
s.log.Debug("Session %v - writePump start", s.Identifier())
defer s.log.Debug("Session %v - writePump shutdown", s.Identifier())
for {
select {
case <-s.ctx.Done():
s.Shutdown()
return
case msg, ok := <-s.writeChan:
s.conn.SetWriteDeadline(time.Now().Add(1 * time.Second))
if !ok {
s.log.Debug("writeChan was closed!")
s.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
if err := s.conn.WriteMessage(websocket.TextMessage, msg); err != nil {
s.log.Debug(err.Error())
return
}
}
}
}

View File

@@ -1,89 +0,0 @@
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

@@ -0,0 +1,40 @@
package counter
import "sync"
type Counter struct {
initialValue uint64
value uint64
lock sync.Mutex
}
func NewCounter(initialValue uint64) *Counter {
return &Counter{
initialValue: initialValue,
value: initialValue,
}
}
// SetValue sets the value to the given value
func (c *Counter) SetValue(value uint64) {
c.lock.Lock()
c.value = value
c.lock.Unlock()
}
// Increment adds 1 to the counter and returns its value
func (c *Counter) Increment() uint64 {
var result uint64
c.lock.Lock()
c.value++
result = c.value
c.lock.Unlock()
return result
}
// Reset the value to the initial value
func (c *Counter) Reset() {
c.lock.Lock()
c.value = c.initialValue
c.lock.Unlock()
}

View File

@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2014 Joel
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,125 +0,0 @@
// deepcopy makes deep copies of things. A standard copy will copy the
// pointers: deep copy copies the values pointed to. Unexported field
// values are not copied.
//
// Copyright (c)2014-2016, Joel Scoble (github.com/mohae), all rights reserved.
// License: MIT, for more details check the included LICENSE file.
package deepcopy
import (
"reflect"
"time"
)
// Interface for delegating copy process to type
type Interface interface {
DeepCopy() interface{}
}
// Iface is an alias to Copy; this exists for backwards compatibility reasons.
func Iface(iface interface{}) interface{} {
return Copy(iface)
}
// Copy creates a deep copy of whatever is passed to it and returns the copy
// in an interface{}. The returned value will need to be asserted to the
// correct type.
func Copy(src interface{}) interface{} {
if src == nil {
return nil
}
// Make the interface a reflect.Value
original := reflect.ValueOf(src)
// Make a copy of the same type as the original.
cpy := reflect.New(original.Type()).Elem()
// Recursively copy the original.
copyRecursive(original, cpy)
// Return the copy as an interface.
return cpy.Interface()
}
// copyRecursive does the actual copying of the interface. It currently has
// limited support for what it can handle. Add as needed.
func copyRecursive(original, cpy reflect.Value) {
// check for implement deepcopy.Interface
if original.CanInterface() {
if copier, ok := original.Interface().(Interface); ok {
cpy.Set(reflect.ValueOf(copier.DeepCopy()))
return
}
}
// handle according to original's Kind
switch original.Kind() {
case reflect.Ptr:
// Get the actual value being pointed to.
originalValue := original.Elem()
// if it isn't valid, return.
if !originalValue.IsValid() {
return
}
cpy.Set(reflect.New(originalValue.Type()))
copyRecursive(originalValue, cpy.Elem())
case reflect.Interface:
// If this is a nil, don't do anything
if original.IsNil() {
return
}
// Get the value for the interface, not the pointer.
originalValue := original.Elem()
// Get the value by calling Elem().
copyValue := reflect.New(originalValue.Type()).Elem()
copyRecursive(originalValue, copyValue)
cpy.Set(copyValue)
case reflect.Struct:
t, ok := original.Interface().(time.Time)
if ok {
cpy.Set(reflect.ValueOf(t))
return
}
// Go through each field of the struct and copy it.
for i := 0; i < original.NumField(); i++ {
// The Type's StructField for a given field is checked to see if StructField.PkgPath
// is set to determine if the field is exported or not because CanSet() returns false
// for settable fields. I'm not sure why. -mohae
if original.Type().Field(i).PkgPath != "" {
continue
}
copyRecursive(original.Field(i), cpy.Field(i))
}
case reflect.Slice:
if original.IsNil() {
return
}
// Make a new slice and copy each element.
cpy.Set(reflect.MakeSlice(original.Type(), original.Len(), original.Cap()))
for i := 0; i < original.Len(); i++ {
copyRecursive(original.Index(i), cpy.Index(i))
}
case reflect.Map:
if original.IsNil() {
return
}
cpy.Set(reflect.MakeMap(original.Type()))
for _, key := range original.MapKeys() {
originalValue := original.MapIndex(key)
copyValue := reflect.New(originalValue.Type()).Elem()
copyRecursive(originalValue, copyValue)
copyKey := Copy(key.Interface())
cpy.SetMapIndex(reflect.ValueOf(copyKey), copyValue)
}
default:
cpy.Set(original)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,4 +10,8 @@ License: http://git.ozlabs.org/?p=ccan;a=blob;f=licenses/BSD-MIT;h=89de354795ec7
## hashmap
Homepage: https://github.com/sheredom/hashmap.h
License: https://github.com/sheredom/hashmap.h/blob/master/LICENSE
License: https://github.com/sheredom/hashmap.h/blob/master/LICENSE
## utf8.h
Homepage: https://github.com/sheredom/utf8.h
License: https://github.com/sheredom/utf8.h/blob/master/LICENSE

View File

@@ -38,21 +38,31 @@ int freeHashmapItem(void *const context, struct hashmap_element_s *const e) {
const char* getJSONString(JsonNode *item, const char* key) {
// Get key
JsonNode *node = json_find_member(item, key);
const char *result = "";
const char *result = NULL;
if ( node != NULL && node->tag == JSON_STRING) {
result = node->string_;
}
return result;
}
const char* getJSONStringDefault(JsonNode *item, const char* key, const char* defaultValue) {
const char* result = getJSONString(item, key);
if ( result == NULL ) result = defaultValue;
return result;
}
void ABORT_JSON(JsonNode *node, const char* key) {
ABORT("Unable to read required key '%s' from JSON: %s\n", key, json_encode(node));
}
const char* mustJSONString(JsonNode *node, const char* key) {
const char* result = getJSONString(node, key);
if ( result == NULL ) {
ABORT_JSON(node, key);
const char* mustJSONString(JsonNode *item, const char* key) {
JsonNode *member = json_find_member(item, key);
if ( member == NULL ) {
ABORT_JSON(item, key);
}
const char *result = "";
if ( member != NULL && member->tag == JSON_STRING) {
result = member->string_;
}
return result;
}
@@ -68,15 +78,23 @@ JsonNode* getJSONObject(JsonNode* node, const char* key) {
return json_find_member(node, key);
}
bool getJSONBool(JsonNode *item, const char* key, bool *result) {
JsonNode *node = json_find_member(item, key);
if ( node != NULL && node->tag == JSON_BOOL) {
*result = node->bool_;
return true;
bool getJSONBool(JsonNode *node, const char* key) {
JsonNode *result = json_find_member(node, key);
if ( result != NULL && result->tag == JSON_BOOL) {
return result->bool_;
}
return false;
}
int mustJSONInt(JsonNode *node, const char* key) {
JsonNode *result = json_find_member(node, key);
if ( result == NULL || result->tag != JSON_NUMBER) {
ABORT_JSON(result, key);
}
return (int) result->number_;
}
bool getJSONInt(JsonNode *item, const char* key, int *result) {
JsonNode *node = json_find_member(item, key);
if ( node != NULL && node->tag == JSON_NUMBER) {

View File

@@ -5,6 +5,10 @@
#ifndef COMMON_H
#define COMMON_H
#define OBJC_OLD_DISPATCH_PROTOTYPES 1
#include <objc/objc-runtime.h>
#include <CoreGraphics/CoreGraphics.h>
#include <stdio.h>
#include <stdarg.h>
#include "string.h"
@@ -14,23 +18,45 @@
#define STREQ(a,b) strcmp(a, b) == 0
#define STREMPTY(string) strlen(string) == 0
#define STRCOPY(a) concat(a, "")
#define STRCOPY(str) (str == NULL ? NULL : concat(str, ""))
#define STR_HAS_CHARS(input) input != NULL && strlen(input) > 0
#define MEMFREE(input) free((void*)input); input = NULL;
#define MEMFREE(input) if(input != NULL) { free((void*)input); input = NULL; }
#define FREE_AND_SET(variable, value) if( variable != NULL ) { MEMFREE(variable); } variable = value
#define NEW(struct) malloc(sizeof(struct));
#define HASHMAP_INIT(hashmap, initialSize, name) \
if( 0 != hashmap_create((const unsigned)initialSize, &hashmap)) { \
ABORT("Not enough memory to allocate %s!\n", name); \
}
#define HASHMAP_PUT(hashmap, key, value) hashmap_put(&hashmap, key, strlen(key), value);
#define HASHMAP_GET(hashmap, key) hashmap_get(&hashmap, key, strlen(key));
#define HASHMAP_DESTROY(hashmap) hashmap_destroy(&hashmap);
#define HASHMAP_SIZE(hashmap) hashmap_num_entries(&hashmap)
#define HASHMAP_ITERATE(hashmap, function, context) if( hashmap_num_entries(&hashmap) > 0 ) { \
if (0!=hashmap_iterate_pairs(&hashmap, function, context)) { \
ABORT("failed to iterate hashmap entries!"); \
} \
}
#define JSON_ADD_STRING(jsonObject, key, value) if( value != NULL ) { json_append_member(jsonObject, (char*)key, json_mkstring(value)); }
#define JSON_ADD_NUMBER(jsonObject, key, value) json_append_member(jsonObject, (char*)key, json_mknumber(value));
#define JSON_ADD_OBJECT(jsonObject, key, value) if( value != NULL ) { json_append_member(jsonObject, (char*)key, value); }
#define JSON_ADD_BOOL(jsonObject, key, value) json_append_member(jsonObject, (char*)key, json_mkbool(value));
#define JSON_ADD_ELEMENT(jsonObject, value) json_append_element(jsonObject, value);
// Credit: https://stackoverflow.com/a/8465083
char* concat(const char *string1, const char *string2);
void ABORT(const char *message, ...);
int freeHashmapItem(void *const context, struct hashmap_element_s *const e);
int freeHashmapItem(void *context, struct hashmap_element_s *e);
const char* getJSONString(JsonNode *item, const char* key);
const char* getJSONStringDefault(JsonNode *item, const char* key, const char* defaultValue);
const char* mustJSONString(JsonNode *node, const char* key);
JsonNode* getJSONObject(JsonNode* node, const char* key);
JsonNode* mustJSONObject(JsonNode *node, const char* key);
bool getJSONBool(JsonNode *item, const char* key, bool *result);
bool getJSONBool(JsonNode *item, const char* key);
bool getJSONInt(JsonNode *item, const char* key, int *result);
int mustJSONInt(JsonNode *node, const char* key);
JsonNode* mustParseJSON(const char* JSON);
#endif //ASSETS_C_COMMON_H

View File

@@ -1,99 +1,99 @@
////
//// Created by Lea Anthony on 6/1/21.
//////
////// Created by Lea Anthony on 6/1/21.
//////
////
//
#include "ffenestri_darwin.h"
#include "common.h"
#include "contextmenus_darwin.h"
#include "menu_darwin.h"
ContextMenu* NewContextMenu(const char* contextMenuJSON) {
ContextMenu* result = malloc(sizeof(ContextMenu));
JsonNode* processedJSON = json_decode(contextMenuJSON);
if( processedJSON == NULL ) {
ABORT("[NewTrayMenu] Unable to parse TrayMenu JSON: %s", contextMenuJSON);
}
// Save reference to this json
result->processedJSON = processedJSON;
result->ID = mustJSONString(processedJSON, "ID");
JsonNode* processedMenu = mustJSONObject(processedJSON, "ProcessedMenu");
result->menu = NewMenu(processedMenu);
result->nsmenu = NULL;
result->menu->menuType = ContextMenuType;
result->menu->parentData = result;
result->contextMenuData = NULL;
return result;
}
ContextMenu* GetContextMenuByID(ContextMenuStore* store, const char *contextMenuID) {
return (ContextMenu*)hashmap_get(&store->contextMenuMap, (char*)contextMenuID, strlen(contextMenuID));
}
void DeleteContextMenu(ContextMenu* contextMenu) {
// Free Menu
DeleteMenu(contextMenu->menu);
// Delete any context menu data we may have stored
if( contextMenu->contextMenuData != NULL ) {
MEMFREE(contextMenu->contextMenuData);
}
// Free JSON
if (contextMenu->processedJSON != NULL ) {
json_delete(contextMenu->processedJSON);
contextMenu->processedJSON = NULL;
}
// Free context menu
free(contextMenu);
}
int freeContextMenu(void *const context, struct hashmap_element_s *const e) {
DeleteContextMenu(e->data);
return -1;
}
void ShowContextMenu(ContextMenuStore* store, id mainWindow, const char *contextMenuID, const char *contextMenuData) {
// If no context menu ID was given, abort
if( contextMenuID == NULL ) {
return;
}
ContextMenu* contextMenu = GetContextMenuByID(store, contextMenuID);
// We don't need the ID now
MEMFREE(contextMenuID);
if( contextMenu == NULL ) {
// Free context menu data
if( contextMenuData != NULL ) {
MEMFREE(contextMenuData);
return;
}
}
// We need to store the context menu data. Free existing data if we have it
// and set to the new value.
FREE_AND_SET(contextMenu->contextMenuData, contextMenuData);
// Grab the content view and show the menu
id contentView = msg_reg(mainWindow, s("contentView"));
// Get the triggering event
id menuEvent = msg_reg(mainWindow, s("currentEvent"));
if( contextMenu->nsmenu == NULL ) {
// GetMenu creates the NSMenu
contextMenu->nsmenu = GetMenu(contextMenu->menu);
}
// Show popup
((id(*)(id, SEL, id, id, id))objc_msgSend)(c("NSMenu"), s("popUpContextMenu:withEvent:forView:"), contextMenu->nsmenu, menuEvent, contentView);
}
//#include "ffenestri_darwin.h"
//#include "common.h"
//#include "contextmenus_darwin.h"
//#include "menu_darwin.h"
//
//ContextMenu* NewContextMenu(const char* contextMenuJSON, struct TrayMenuStore *store) {
// ContextMenu* result = malloc(sizeof(ContextMenu));
//
// JsonNode* processedJSON = json_decode(contextMenuJSON);
// if( processedJSON == NULL ) {
// ABORT("[NewTrayMenu] Unable to parse TrayMenu JSON: %s", contextMenuJSON);
// }
// // Save reference to this json
// result->processedJSON = processedJSON;
//
// result->ID = mustJSONString(processedJSON, "ID");
// JsonNode* processedMenu = mustJSONObject(processedJSON, "ProcessedMenu");
//
//// result->menu = NewMenu(processedMenu);
// result->nsmenu = NULL;
// result->menu->menuType = ContextMenuType;
// result->menu->store = store;
// result->contextMenuData = NULL;
// return result;
//}
//
//ContextMenu* GetContextMenuByID(ContextMenuStore* store, const char *contextMenuID) {
// return (ContextMenu*)hashmap_get(&store->contextMenuMap, (char*)contextMenuID, strlen(contextMenuID));
//}
//
//void DeleteContextMenu(ContextMenu* contextMenu) {
// // Free Menu
// DeleteMenu(contextMenu->menu);
//
// // Delete any context menu data we may have stored
// if( contextMenu->contextMenuData != NULL ) {
// MEMFREE(contextMenu->contextMenuData);
// }
//
// // Free JSON
// if (contextMenu->processedJSON != NULL ) {
// json_delete(contextMenu->processedJSON);
// contextMenu->processedJSON = NULL;
// }
//
// // Free context menu
// free(contextMenu);
//}
//
//int freeContextMenu(void *const context, struct hashmap_element_s *const e) {
// DeleteContextMenu(e->data);
// return -1;
//}
//
//void ShowContextMenu(ContextMenuStore* store, id mainWindow, const char *contextMenuID, const char *contextMenuData) {
//
// // If no context menu ID was given, abort
// if( contextMenuID == NULL ) {
// return;
// }
//
// ContextMenu* contextMenu = GetContextMenuByID(store, contextMenuID);
//
// // We don't need the ID now
// MEMFREE(contextMenuID);
//
// if( contextMenu == NULL ) {
// // Free context menu data
// if( contextMenuData != NULL ) {
// MEMFREE(contextMenuData);
// return;
// }
// }
//
// // We need to store the context menu data. Free existing data if we have it
// // and set to the new value.
// FREE_AND_SET(contextMenu->contextMenuData, contextMenuData);
//
// // Grab the content view and show the menu
// id contentView = msg(mainWindow, s("contentView"));
//
// // Get the triggering event
// id menuEvent = msg(mainWindow, s("currentEvent"));
//
//// if( contextMenu->nsmenu == NULL ) {
//// // GetMenu creates the NSMenu
//// contextMenu->nsmenu = GetMenu(contextMenu->menu);
//// }
//
// // Show popup
// msg(c("NSMenu"), s("popUpContextMenu:withEvent:forView:"), contextMenu->nsmenu, menuEvent, contentView);
//
//}
//

View File

@@ -1,33 +1,33 @@
//////
////// Created by Lea Anthony on 6/1/21.
//////
////
//// Created by Lea Anthony on 6/1/21.
////
//#ifndef CONTEXTMENU_DARWIN_H
//#define CONTEXTMENU_DARWIN_H
//
#ifndef CONTEXTMENU_DARWIN_H
#define CONTEXTMENU_DARWIN_H
#include "json.h"
#include "menu_darwin.h"
#include "contextmenustore_darwin.h"
typedef struct {
const char* ID;
id nsmenu;
Menu* menu;
JsonNode* processedJSON;
// Context menu data is given by the frontend when clicking a context menu.
// We send this to the backend when an item is selected
const char* contextMenuData;
} ContextMenu;
ContextMenu* NewContextMenu(const char* contextMenuJSON);
ContextMenu* GetContextMenuByID( ContextMenuStore* store, const char *contextMenuID);
void DeleteContextMenu(ContextMenu* contextMenu);
int freeContextMenu(void *const context, struct hashmap_element_s *const e);
void ShowContextMenu(ContextMenuStore* store, id mainWindow, const char *contextMenuID, const char *contextMenuData);
#endif //CONTEXTMENU_DARWIN_H
//#include "json.h"
//#include "menu_darwin.h"
//#include "contextmenustore_darwin.h"
//
//typedef struct {
// const char* ID;
// id nsmenu;
// Menu* menu;
//
// JsonNode* processedJSON;
//
// // Context menu data is given by the frontend when clicking a context menu.
// // We send this to the backend when an item is selected
// const char* contextMenuData;
//} ContextMenu;
//
//
//ContextMenu* NewContextMenu(const char* contextMenuJSON);
//
//ContextMenu* GetContextMenuByID( ContextMenuStore* store, const char *contextMenuID);
//void DeleteContextMenu(ContextMenu* contextMenu);
//int freeContextMenu(void *const context, struct hashmap_element_s *const e);
//
//void ShowContextMenu(ContextMenuStore* store, id mainWindow, const char *contextMenuID, const char *contextMenuData);
//
//#endif //CONTEXTMENU_DARWIN_H

View File

@@ -1,65 +1,65 @@
#include "contextmenus_darwin.h"
#include "contextmenustore_darwin.h"
ContextMenuStore* NewContextMenuStore() {
ContextMenuStore* result = malloc(sizeof(ContextMenuStore));
// Allocate Context Menu Store
if( 0 != hashmap_create((const unsigned)4, &result->contextMenuMap)) {
ABORT("[NewContextMenus] Not enough memory to allocate contextMenuStore!");
}
return result;
}
void AddContextMenuToStore(ContextMenuStore* store, const char* contextMenuJSON) {
ContextMenu* newMenu = NewContextMenu(contextMenuJSON);
//TODO: check if there is already an entry for this menu
hashmap_put(&store->contextMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
}
ContextMenu* GetContextMenuFromStore(ContextMenuStore* store, const char* menuID) {
// Get the current menu
return hashmap_get(&store->contextMenuMap, menuID, strlen(menuID));
}
void UpdateContextMenuInStore(ContextMenuStore* store, const char* menuJSON) {
ContextMenu* newContextMenu = NewContextMenu(menuJSON);
// Get the current menu
ContextMenu *currentMenu = GetContextMenuFromStore(store, newContextMenu->ID);
if ( currentMenu == NULL ) {
ABORT("Attempted to update unknown context menu with ID '%s'.", newContextMenu->ID);
}
hashmap_remove(&store->contextMenuMap, newContextMenu->ID, strlen(newContextMenu->ID));
// Save the status bar reference
DeleteContextMenu(currentMenu);
hashmap_put(&store->contextMenuMap, newContextMenu->ID, strlen(newContextMenu->ID), newContextMenu);
}
void DeleteContextMenuStore(ContextMenuStore* store) {
// Guard against NULLs
if( store == NULL ) {
return;
}
// Delete context menus
if( hashmap_num_entries(&store->contextMenuMap) > 0 ) {
if (0 != hashmap_iterate_pairs(&store->contextMenuMap, freeContextMenu, NULL)) {
ABORT("[DeleteContextMenuStore] Failed to release contextMenuStore entries!");
}
}
// Free context menu hashmap
hashmap_destroy(&store->contextMenuMap);
}
//
//#include "contextmenus_darwin.h"
//#include "contextmenustore_darwin.h"
//
//ContextMenuStore* NewContextMenuStore() {
//
// ContextMenuStore* result = malloc(sizeof(ContextMenuStore));
//
// // Allocate Context Menu Store
// if( 0 != hashmap_create((const unsigned)4, &result->contextMenuMap)) {
// ABORT("[NewContextMenus] Not enough memory to allocate contextMenuStore!");
// }
//
// return result;
//}
//
//void AddContextMenuToStore(ContextMenuStore* store, const char* contextMenuJSON) {
// ContextMenu* newMenu = NewContextMenu(contextMenuJSON);
//
// //TODO: check if there is already an entry for this menu
// hashmap_put(&store->contextMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
//}
//
//ContextMenu* GetContextMenuFromStore(ContextMenuStore* store, const char* menuID) {
// // Get the current menu
// return hashmap_get(&store->contextMenuMap, menuID, strlen(menuID));
//}
//
//void UpdateContextMenuInStore(ContextMenuStore* store, const char* menuJSON) {
// ContextMenu* newContextMenu = NewContextMenu(menuJSON);
//
// // Get the current menu
// ContextMenu *currentMenu = GetContextMenuFromStore(store, newContextMenu->ID);
// if ( currentMenu == NULL ) {
// ABORT("Attempted to update unknown context menu with ID '%s'.", newContextMenu->ID);
// }
//
// hashmap_remove(&store->contextMenuMap, newContextMenu->ID, strlen(newContextMenu->ID));
//
// // Save the status bar reference
// DeleteContextMenu(currentMenu);
//
// hashmap_put(&store->contextMenuMap, newContextMenu->ID, strlen(newContextMenu->ID), newContextMenu);
//
//}
//
//
//void DeleteContextMenuStore(ContextMenuStore* store) {
//
// // Guard against NULLs
// if( store == NULL ) {
// return;
// }
//
// // Delete context menus
// if( hashmap_num_entries(&store->contextMenuMap) > 0 ) {
// if (0 != hashmap_iterate_pairs(&store->contextMenuMap, freeContextMenu, NULL)) {
// ABORT("[DeleteContextMenuStore] Failed to release contextMenuStore entries!");
// }
// }
//
// // Free context menu hashmap
// hashmap_destroy(&store->contextMenuMap);
//
//}

View File

@@ -1,27 +1,27 @@
////
//// Created by Lea Anthony on 7/1/21.
////
//
// Created by Lea Anthony on 7/1/21.
//#ifndef CONTEXTMENUSTORE_DARWIN_H
//#define CONTEXTMENUSTORE_DARWIN_H
//
#ifndef CONTEXTMENUSTORE_DARWIN_H
#define CONTEXTMENUSTORE_DARWIN_H
#include "common.h"
typedef struct {
int dummy;
// This is our context menu store which keeps track
// of all instances of ContextMenus
struct hashmap_s contextMenuMap;
} ContextMenuStore;
ContextMenuStore* NewContextMenuStore();
void DeleteContextMenuStore(ContextMenuStore* store);
void UpdateContextMenuInStore(ContextMenuStore* store, const char* menuJSON);
void AddContextMenuToStore(ContextMenuStore* store, const char* contextMenuJSON);
#endif //CONTEXTMENUSTORE_DARWIN_H
//#include "common.h"
//
//typedef struct {
//
// int dummy;
//
// // This is our context menu store which keeps track
// // of all instances of ContextMenus
// struct hashmap_s contextMenuMap;
//
//} ContextMenuStore;
//
//ContextMenuStore* NewContextMenuStore();
//
//void DeleteContextMenuStore(ContextMenuStore* store);
//void UpdateContextMenuInStore(ContextMenuStore* store, const char* menuJSON);
//
//void AddContextMenuToStore(ContextMenuStore* store, const char* contextMenuJSON);
//
//#endif //CONTEXTMENUSTORE_DARWIN_H

View File

@@ -1,12 +1,11 @@
package ffenestri
import (
"github.com/wailsapp/wails/v2/internal/menumanager"
"runtime"
"strings"
"unsafe"
"github.com/wailsapp/wails/v2/internal/menumanager"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/messagedispatcher"
"github.com/wailsapp/wails/v2/pkg/options"
@@ -119,8 +118,7 @@ func (a *Application) Run(incomingDispatcher Dispatcher, bindings string, debug
fullscreen := a.bool2Cint(a.config.Fullscreen)
startHidden := a.bool2Cint(a.config.StartHidden)
logLevel := C.int(a.config.LogLevel)
hideWindowOnClose := a.bool2Cint(a.config.HideWindowOnClose)
app := C.NewApplication(title, width, height, resizable, devtools, fullscreen, startHidden, logLevel, hideWindowOnClose)
app := C.NewApplication(title, width, height, resizable, devtools, fullscreen, startHidden, logLevel)
// Save app reference
a.app = (*C.struct_Application)(app)

View File

@@ -4,7 +4,7 @@
#include <stdio.h>
struct Application;
extern struct Application *NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden, int logLevel, int hideWindowOnClose);
extern struct Application *NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden, int logLevel);
extern void SetMinWindowSize(struct Application*, int minWidth, int minHeight);
extern void SetMaxWindowSize(struct Application*, int maxWidth, int maxHeight);
extern void Run(struct Application*, int argc, char **argv);
@@ -30,16 +30,15 @@ extern void Fullscreen(struct Application*);
extern void UnFullscreen(struct Application*);
extern void ToggleFullscreen(struct Application*);
extern void DisableFrame(struct Application*);
extern void OpenDialog(struct Application*, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int allowFiles, int allowDirs, int allowMultiple, int showHiddenFiles, int canCreateDirectories, int resolvesAliases, int treatPackagesAsDirectories);
extern void SaveDialog(struct Application*, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int showHiddenFiles, int canCreateDirectories, int treatPackagesAsDirectories);
extern void MessageDialog(struct Application*, char *callbackID, char *type, char *title, char *message, char *icon, char *button1, char *button2, char *button3, char *button4, char *defaultButton, char *cancelButton);
extern void DarkModeEnabled(struct Application*, char *callbackID);
extern void OpenDialog(struct Application*, const char *callbackID, const char *title, const char *filters, const char *defaultFilename, const char *defaultDir, int allowFiles, int allowDirs, int allowMultiple, int showHiddenFiles, int canCreateDirectories, int resolvesAliases, int treatPackagesAsDirectories);
extern void SaveDialog(struct Application*, const char *callbackID, const char *title, const char *filters, const char *defaultFilename, const char *defaultDir, int showHiddenFiles, int canCreateDirectories, int treatPackagesAsDirectories);
extern void MessageDialog(struct Application*, const char *callbackID, const char *type, const char *title, const char *message, const char *icon, const char *button1, const char *button2, const char *button3, const char *button4, const char *defaultButton, const char *cancelButton);
extern void DarkModeEnabled(struct Application*, const 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);
extern void AddContextMenu(struct Application*, const char *contextMenuJSON);
extern void UpdateContextMenu(struct Application*, const char *contextMenuJSON);
#endif

View File

@@ -12,9 +12,8 @@ package ffenestri
import "C"
import (
"strconv"
"github.com/wailsapp/wails/v2/pkg/options/dialog"
"strconv"
"github.com/wailsapp/wails/v2/internal/logger"
)
@@ -114,14 +113,6 @@ func (c *Client) WindowSize(width int, height int) {
C.SetSize(c.app.app, C.int(width), C.int(height))
}
func (c *Client) WindowSetMinSize(width int, height int) {
C.SetMinWindowSize(c.app.app, C.int(width), C.int(height))
}
func (c *Client) WindowSetMaxSize(width int, height int) {
C.SetMaxWindowSize(c.app.app, C.int(width), C.int(height))
}
// WindowSetColour sets the window colour
func (c *Client) WindowSetColour(colour int) {
r, g, b, a := intToColour(colour)
@@ -208,7 +199,3 @@ 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))
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ package ffenestri
/*
#cgo darwin CFLAGS: -DFFENESTRI_DARWIN=1
#cgo darwin LDFLAGS: -framework WebKit -framework CoreFoundation -lobjc
#cgo darwin LDFLAGS: -framework WebKit -lobjc
#include "ffenestri.h"
#include "ffenestri_darwin.h"
@@ -48,9 +48,6 @@ 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)
@@ -61,15 +58,15 @@ func (a *Application) processPlatformSettings() error {
C.WindowBackgroundIsTranslucent(a.app)
}
// Process menu
//applicationMenu := options.GetApplicationMenu(a.config)
applicationMenu := a.menuManager.GetApplicationMenuJSON()
if applicationMenu != "" {
C.SetApplicationMenu(a.app, a.string2CString(applicationMenu))
}
//// Process menu
////applicationMenu := options.GetApplicationMenu(a.config)
//applicationMenu := a.menuManager.GetApplicationMenuJSON()
//if applicationMenu != "" {
// C.SetApplicationMenu(a.app, a.string2CString(applicationMenu))
//}
// Process tray
trays, err := a.menuManager.GetTrayMenus()
trays, err := a.menuManager.GetTrayMenusAsJSON()
if err != nil {
return err
}
@@ -79,21 +76,16 @@ func (a *Application) processPlatformSettings() error {
}
}
// Process context menus
contextMenus, err := a.menuManager.GetContextMenus()
if err != nil {
return err
}
if contextMenus != nil {
for _, contextMenu := range contextMenus {
C.AddContextMenu(a.app, a.string2CString(contextMenu))
}
}
// Process URL Handlers
if a.config.Mac.URLHandlers != nil {
C.HasURLHandlers(a.app)
}
//// Process context menus
//contextMenus, err := a.menuManager.GetContextMenus()
//if err != nil {
// return err
//}
//if contextMenus != nil {
// for _, contextMenu := range contextMenus {
// C.AddContextMenu(a.app, a.string2CString(contextMenu))
// }
//}
return nil
}

View File

@@ -6,48 +6,30 @@
#define OBJC_OLD_DISPATCH_PROTOTYPES 1
#include <objc/objc-runtime.h>
#include <CoreGraphics/CoreGraphics.h>
#include <CoreFoundation/CoreFoundation.h>
#include "json.h"
#include "hashmap.h"
#include "stdlib.h"
// Macros to make it slightly more sane
#define msg objc_msgSend
#define msg_reg ((id(*)(id, SEL))objc_msgSend)
#define msg_id ((id(*)(id, SEL, id))objc_msgSend)
#define msg_id_id ((id(*)(id, SEL, id, id))objc_msgSend)
#define msg_bool ((id(*)(id, SEL, BOOL))objc_msgSend)
#define msg_int ((id(*)(id, SEL, int))objc_msgSend)
#define msg_uint ((id(*)(id, SEL, unsigned int))objc_msgSend)
#define msg_float ((id(*)(id, SEL, float))objc_msgSend)
#define kInternetEventClass 'GURL'
#define kAEGetURL 'GURL'
#define keyDirectObject '----'
#define c(str) (id)objc_getClass(str)
#define s(str) sel_registerName(str)
#define u(str) sel_getUid(str)
#define str(input) ((id(*)(id, SEL, const char *))objc_msgSend)(c("NSString"), s("stringWithUTF8String:"), input)
#define strunicode(input) ((id(*)(id, SEL, id, unsigned short))objc_msgSend)(c("NSString"), s("stringWithFormat:"), str("%C"), (unsigned short)input)
#define cstr(input) (const char *)msg_reg(input, s("UTF8String"))
#define url(input) msg_id(c("NSURL"), s("fileURLWithPath:"), str(input))
#define ALLOC(classname) msg_reg(c(classname), s("alloc"))
#define ALLOC_INIT(classname) msg_reg(msg_reg(c(classname), s("alloc")), s("init"))
#define str(input) msg(c("NSString"), s("stringWithUTF8String:"), input)
#define strunicode(input) msg(c("NSString"), s("stringWithFormat:"), str("%C"), (unsigned short)input)
#define cstr(input) (const char *)msg(input, s("UTF8String"))
#define url(input) msg(c("NSURL"), s("fileURLWithPath:"), str(input))
#define RELEASE(input) if( input != NULL ) { msg(input, s("release")); }
#if defined (__aarch64__)
#define GET_FRAME(receiver) ((CGRect(*)(id, SEL))objc_msgSend)(receiver, s("frame"))
#define GET_BOUNDS(receiver) ((CGRect(*)(id, SEL))objc_msgSend)(receiver, s("bounds"))
#endif
#if defined (__x86_64__)
#define ALLOC(classname) msg(c(classname), s("alloc"))
#define ALLOC_INIT(classname) msg(msg(c(classname), s("alloc")), s("init"))
#define GET_FRAME(receiver) ((CGRect(*)(id, SEL))objc_msgSend_stret)(receiver, s("frame"))
#define GET_BOUNDS(receiver) ((CGRect(*)(id, SEL))objc_msgSend_stret)(receiver, s("bounds"))
#endif
#define GET_BACKINGSCALEFACTOR(receiver) ((CGFloat(*)(id, SEL))objc_msgSend)(receiver, s("backingScaleFactor"))
#define GET_BACKINGSCALEFACTOR(receiver) ((CGFloat(*)(id, SEL))msg)(receiver, s("backingScaleFactor"))
#define ON_MAIN_THREAD(str) dispatch( ^{ str; } )
#define MAIN_WINDOW_CALL(str) msg_reg(app->mainWindow, s((str)))
#define MAIN_WINDOW_CALL(str) msg(app->mainWindow, s((str)))
#define NSBackingStoreBuffered 2
@@ -85,10 +67,6 @@
#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
@@ -131,12 +109,5 @@ void WebviewIsTransparent(struct Application* app);
void WindowBackgroundIsTranslucent(struct Application* app);
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

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

View File

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

View File

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

View File

@@ -0,0 +1,276 @@
//
// Created by Lea Anthony on 18/1/21.
//
#include "menu.h"
Menu* NewMenu(struct JsonNode* menuData, struct JsonNode* radioData, MenuManager* manager) {
if( menuData == NULL ) return NULL;
if( manager == NULL ) return NULL;
Menu *result = NEW(Menu);
// No label by default
result->label = STRCOPY(getJSONStringDefault(menuData, "l", ""));
// Setup platform specific menu data
SetupMenuPlatformData(result);
// Init menu item list
vec_init(&result->menuItems);
// Get the menu items
JsonNode* items = getJSONObject(menuData, "i");
if( items != NULL ) {
// Process the menu data
JsonNode *item;
json_foreach(item, items) {
const char *ID = mustJSONString(item, "I");
MenuItem *menuItem = HASHMAP_GET(manager->menuItems, ID);
if (menuItem == NULL) {
// Process each menu item
menuItem = processMenuItem(result, item, manager);
// Filter out separators
if (menuItem->ID != NULL) {
HASHMAP_PUT(manager->menuItems, menuItem->ID, menuItem);
}
}
AddMenuItemToMenu(result, menuItem, manager);
}
if (radioData != NULL) {
// Iterate radio groups
JsonNode *radioGroup;
json_foreach(radioGroup, radioData) {
// Get item label
processRadioGroup(result, radioGroup, manager);
}
}
}
return result;
}
MenuItem* processMenuItem(Menu *menu, JsonNode *item, MenuManager *manager) {
// Get the role
const char *role = getJSONString(item, "r");
if( role != NULL ) {
// Roles override everything else
// return NewMenuItemForRole(role, menu, item, manager);
}
Menu* submenu = NULL;
// Check if this is a submenu
// JsonNode *submenuData = json_find_member(item, "S");
// if( submenuData != NULL ) {
// submenu = NewMenu(submenuData)
// // Get the label
// JsonNode *menuNameNode = json_find_member(item, "l");
// const char *name = "";
// if ( menuNameNode != NULL) {
// name = menuNameNode->string_;
// }
//
// id thisMenuItem = createMenuItemNoAutorelease(str(name), NULL, "");
// id thisMenu = createMenu(str(name));
//
// msg(thisMenuItem, s("setSubmenu:"), thisMenu);
// msg(parentMenu, s("addItem:"), thisMenuItem);
//
// JsonNode *submenuItems = json_find_member(submenu, "i");
// // If we have no items, just return
// if ( submenuItems == NULL ) {
// return;
// }
//
// // Loop over submenu items
// JsonNode *item;
// json_foreach(item, submenuItems) {
// // Get item label
// processMenuItem(menu, thisMenu, item);
// }
//
// return;
// }
// Get the ID
const char *menuItemID = mustJSONString(item, "I");
// Get the label(s)
const char* label = getJSONStringDefault(item, "l", "");
const char* alternateLabel = getJSONString(item, "L");
bool checked = getJSONBool(item, "c");
bool hidden = getJSONBool(item, "h");
bool disabled = getJSONBool(item, "d");
const char* RGBA = getJSONString(item, "R");
const char* font = getJSONString(item, "F");
const char* image = getJSONString(item, "i");
int fontSize = 0;
getJSONInt(item, "F", &fontSize);
// Get the Accelerator
JsonNode *accelerator = json_find_member(item, "a");
const char *acceleratorKey = NULL;
const char **modifiers = NULL;
// If we have an accelerator
if( accelerator != NULL ) {
// Get the key
acceleratorKey = getJSONString(accelerator, "Key");
// Check if there are modifiers
JsonNode *modifiersList = json_find_member(accelerator, "Modifiers");
if ( modifiersList != NULL ) {
// Allocate an array of strings
int noOfModifiers = json_array_length(modifiersList);
// Do we have any?
if (noOfModifiers > 0) {
modifiers = malloc(sizeof(const char *) * (noOfModifiers + 1));
JsonNode *modifier;
int count = 0;
// Iterate the modifiers and save a reference to them in our new array
json_foreach(modifier, modifiersList) {
// Get modifier name
modifiers[count] = STRCOPY(modifier->string_);
count++;
}
// Null terminate the modifier list
modifiers[count] = NULL;
}
}
}
// has callback?
bool hasCallback = getJSONBool(item, "C");
// Get the Type
const char *type = mustJSONString(item, "t");
MenuItem* menuItem;
enum MenuItemType menuItemType;
if( STREQ(type, "t")) {
menuItemType = Text;
}
else if ( STREQ(type, "s")) {
menuItemType = Separator;
}
else if ( STREQ(type, "c")) {
menuItemType = Checkbox;
}
else if ( STREQ(type, "r")) {
menuItemType = Radio;
} else {
menuItemType = -1;
ABORT("Unknown Menu Item type '%s'!", type);
}
menuItem = NewMenuItem(menuItemType, menuItemID, label, alternateLabel, disabled, hidden, checked, RGBA, font, fontSize, image, acceleratorKey, modifiers, hasCallback, submenu);
return menuItem;
}
void DeleteMenu(Menu* menu) {
// NULL guard
if( menu == NULL ) return;
// Delete the platform specific menu data
DeleteMenuPlatformData(menu);
// Clean up other data
MEMFREE(menu->label);
// Clear the menu items vector
vec_deinit(&menu->menuItems);
}
const char* MenuAsJSON(Menu* menu) {
if( menu == NULL ) return NULL;
JsonNode *jsonObject = json_mkobject();
JSON_ADD_STRING(jsonObject, "Label", menu->label);
return json_encode(jsonObject);
}
JsonNode* MenuAsJSONObject(Menu* menu) {
if( menu == NULL ) return NULL;
JsonNode *jsonObject = json_mkobject();
JSON_ADD_STRING(jsonObject, "Label", menu->label);
if( vec_size(&menu->menuItems) > 0 ) {
JsonNode* items = json_mkarray();
int i; MenuItem *menuItem;
vec_foreach(&menu->menuItems, menuItem, i) {
JSON_ADD_ELEMENT(items, MenuItemAsJSONObject(menuItem));
}
JSON_ADD_OBJECT(jsonObject, "Items", items);
}
return jsonObject;
}
void processRadioGroup(Menu *menu, JsonNode *radioGroup, MenuManager* manager) {
JsonNode *members = json_find_member(radioGroup, "Members");
JsonNode *member;
int groupLength = json_array_length(radioGroup);
// Allocate array
size_t arrayLength = sizeof(id)*(groupLength+1);
MenuItem* memberList[arrayLength];
// Build the radio group items
int count=0;
json_foreach(member, members) {
// Get menu by id
MenuItem* menuItem = HASHMAP_GET(manager->menuItems, (char*)member->string_);
// Save Member
memberList[count] = menuItem;
count = count + 1;
}
// Null terminate array
memberList[groupLength] = 0;
// Store the members
json_foreach(member, members) {
// Copy the memberList
char *newMemberList = (char *)malloc(arrayLength);
memcpy(newMemberList, memberList, arrayLength);
// add group to each member of group
HASHMAP_PUT(menu->radioGroups, member->string_, newMemberList);
}
}
void AddMenuItemToMenu(Menu* menu, MenuItem* menuItem, MenuManager* manager) {
vec_push(&menu->menuItems, menuItem);
PlatformAddMenuItemToMenu(menu, menuItem, manager);
}
// Creates a JSON message for the given menuItemID and data
const char* CreateMenuClickedMessage(const char *menuItemID, const char *data) {
JsonNode *jsonObject = json_mkobject();
if (menuItemID == NULL ) {
ABORT("Item ID NULL for menu!!\n");
}
json_append_member(jsonObject, "i", json_mkstring(menuItemID));
if (data != NULL) {
json_append_member(jsonObject, "data", json_mkstring(data));
}
const char *payload = json_encode(jsonObject);
json_delete(jsonObject);
const char *result = concat("MC", payload);
MEMFREE(payload);
return result;
}

View File

@@ -0,0 +1,178 @@
//
// Created by Lea Anthony on 18/1/21.
//
#ifndef MENU_H
#define MENU_H
#include "common.h"
enum MenuItemType {Text = 0, Checkbox = 1, Radio = 2, Separator = 3};
static const char *MenuItemTypeAsString[] = {
"Text", "Checkbox", "Radio", "Separator",
};
enum MenuType {ApplicationMenuType = 0, ContextMenuType = 1, TrayMenuType = 2};
static const char *MenuTypeAsString[] = {
"ApplicationMenu", "ContextMenu", "TrayMenu",
};
typedef struct {
// Menu label
const char *label;
// A list of menuItem IDs that make up this menu
vec_void_t menuItems;
// The platform specific menu data
void *platformData;
} Menu;
typedef struct {
// ID of the tray
const char *ID;
// The tray label
const char *Label;
// The icon name
const char *Icon;
// The menu
Menu* Menu;
// Platform specific data
void* platformData;
} TrayMenu;
typedef struct {
Menu* menu;
} ApplicationMenu;
typedef struct {
const char* ID;
Menu* menu;
} ContextMenu;
typedef struct {
// This is our menu item map using the menuItem ID as a key
// map[string]*MenuItem
struct hashmap_s menuItems;
// This is our context menu map using the context menu ID as a key
// map[string]*ContextMenu
struct hashmap_s contextMenus;
// This is our tray menu map using the tray menu ID as a key
// map[string]*TrayMenu
struct hashmap_s trayMenus;
// Application Menu
Menu* applicationMenu;
// Context menu data
const char* contextMenuData;
} MenuManager;
typedef struct {
MenuManager* manager;
const char *menuItemID;
enum MenuItemType menuItemType;
} MenuItemCallbackData;
typedef struct {
const char *ID;
const char* label;
const char* alternateLabel;
bool disabled;
bool hidden;
const char* colour;
const char* font;
int fontSize;
const char* image;
bool checked;
// Keyboard shortcut
const char* acceleratorKey;
const char** modifiers;
// Type of menuItem
enum MenuItemType type;
// Indicates if the menuItem has a callback
bool hasCallback;
// The platform specific menu data
void* platformData;
// Submenu
Menu* submenu;
// Radio group for this item
vec_void_t radioGroup;
// Data for handling callbacks
MenuItemCallbackData *callbackData;
} MenuItem;
// MenuItem
MenuItem* NewMenuItem(enum MenuItemType type, const char* ID, const char* label, const char* alternateLabel, bool disabled, bool hidden, bool checked, const char* colour, const char* font, int fontsize, const char* image, const char* acceleratorKey, const char** modifiers, bool hasCallback, Menu* submenu);
void DeleteMenuItem(MenuItem* menuItem);
JsonNode* MenuItemAsJSONObject(MenuItem* menuItem);
// Menu
Menu* NewMenu(JsonNode* menuData, JsonNode* radioData, MenuManager* menuManager);
void DeleteMenu(Menu* menu);
const char* MenuAsJSON(Menu* menu);
JsonNode* MenuAsJSONObject(Menu* menu);
void AddMenuItemToMenu(Menu* menu, MenuItem* menuItem, MenuManager* manager);
MenuItem* processMenuItem(Menu *menu, JsonNode *item, MenuManager *manager);
void processRadioGroup(Menu *menu, JsonNode *radioGroup, MenuManager* manager);
// Tray
TrayMenu* NewTrayMenu(JsonNode* trayJSON, MenuManager *manager);
void DeleteTrayMenu(TrayMenu *trayMenu);
const char* TrayMenuAsJSON(TrayMenu* menu);
// MenuManager
MenuManager* NewMenuManager();
void DeleteMenuManager(MenuManager* manager);
TrayMenu* AddTrayMenuToManager(MenuManager* manager, const char* trayMenuJSON);
void ShowTrayMenus(MenuManager* manager);
// Callbacks
MenuItemCallbackData* NewMenuItemCallbackData(MenuManager *manager, const char* menuItemID, enum MenuItemType menuItemType);
void DeleteMenuItemCallbackData(MenuItemCallbackData* callbackData);
const char* CreateMenuClickedMessage(const char *menuItemID, const char *data);
// Platform
void SetupMenuPlatformData(Menu* menu);
void DeleteMenuPlatformData(Menu* menu);
void SetupMenuItemPlatformData(MenuItem* menuItem);
void DeleteMenuItemPlatformData(MenuItem* menuItem);
void SetupTrayMenuPlatformData(TrayMenu* menu);
void DeleteTrayMenuPlatformData(TrayMenu* menu);
void PlatformAddMenuItemToMenu(Menu *menu, MenuItem* menuItem, MenuManager* manager);
void PlatformUpdateTrayIcon(TrayMenu *menu);
// Platform specific methods
void UnloadTrayIcons();
void LoadTrayIcons();
void ShowTrayMenu(TrayMenu* trayMenu);
#endif //MENU_H

File diff suppressed because it is too large Load Diff

View File

@@ -1,116 +1,21 @@
//
// Created by Lea Anthony on 6/1/21.
// Created by Lea Anthony on 18/1/21.
//
#ifndef MENU_DARWIN_H
#define MENU_DARWIN_H
#include "common.h"
#include "ffenestri_darwin.h"
enum MenuItemType {Text = 0, Checkbox = 1, Radio = 2};
enum MenuType {ApplicationMenuType = 0, ContextMenuType = 1, TrayMenuType = 2};
static const char *MenuTypeAsString[] = {
"ApplicationMenu", "ContextMenu", "TrayMenu",
};
typedef struct _NSRange {
unsigned long location;
unsigned long length;
} NSRange;
#define NSFontWeightUltraLight -0.8
#define NSFontWeightThin -0.6
#define NSFontWeightLight -0.4
#define NSFontWeightRegular 0.0
#define NSFontWeightMedium 0.23
#define NSFontWeightSemibold 0.3
#define NSFontWeightBold 0.4
#define NSFontWeightHeavy 0.56
#define NSFontWeightBlack 0.62
extern void messageFromWindowCallback(const char *);
#include "menu.h"
typedef struct {
const char *title;
/*** Internal ***/
// The decoded version of the Menu JSON
JsonNode *processedMenu;
struct hashmap_s menuItemMap;
struct hashmap_s radioGroupMap;
// Vector to keep track of callback data memory
vec_void_t callbackDataCache;
// The NSMenu for this menu
id menu;
// The parent data, eg ContextMenuStore or Tray
void *parentData;
// The commands for the menu callbacks
const char *callbackCommand;
// This indicates if we are an Application Menu, tray menu or context menu
enum MenuType menuType;
} Menu;
id Menu;
} MacMenu;
typedef struct {
id menuItem;
Menu *menu;
const char *menuID;
enum MenuItemType menuItemType;
} MenuItemCallbackData;
id MenuItem;
} MacMenuItem;
typedef struct {
id statusBarItem;
int iconPosition;
} MacTrayMenu;
// NewMenu creates a new Menu struct, saving the given menu structure as JSON
Menu* NewMenu(JsonNode *menuData);
Menu* NewApplicationMenu(const char *menuAsJSON);
MenuItemCallbackData* CreateMenuItemCallbackData(Menu *menu, id menuItem, const char *menuID, enum MenuItemType menuItemType);
void DeleteMenu(Menu *menu);
// Creates a JSON message for the given menuItemID and data
const char* createMenuClickedMessage(const char *menuItemID, const char *data, enum MenuType menuType, const char *parentID);
// Callback for text menu items
void menuItemCallback(id self, SEL cmd, id sender);
id processAcceleratorKey(const char *key);
void addSeparator(id menu);
id createMenuItemNoAutorelease( id title, const char *action, const char *key);
id createMenuItem(id title, const char *action, const char *key);
id addMenuItem(id menu, const char *title, const char *action, const char *key, bool disabled);
id createMenu(id title);
void createDefaultAppMenu(id parentMenu);
void createDefaultEditMenu(id parentMenu);
void processMenuRole(Menu *menu, id parentMenu, JsonNode *item);
// This converts a string array of modifiers into the
// equivalent MacOS Modifier Flags
unsigned long parseModifiers(const char **modifiers);
id processRadioMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *acceleratorkey);
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, bool alternate);
void processMenuItem(Menu *menu, id parentMenu, JsonNode *item);
void processMenuData(Menu *menu, JsonNode *menuData);
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
void PlatformMenuItemCallback(id self, SEL cmd, id sender);

View File

@@ -0,0 +1,542 @@
//
// Created by Lea Anthony on 6/1/21.
//
#include "ffenestri_darwin.h"
#include "menu_darwin_old.h"
#include "contextmenus_darwin.h"
#include "traymenustore_darwin.h"
// NewMenu creates a new Menu struct, saving the given menu structure as JSON
Menu* NewMenu(JsonNode *menuData, JsonNode *radioGroups, struct TrayMenuStore *store) {
Menu *result = malloc(sizeof(Menu));
// No title by default
result->title = "";
// Initialise menuCallbackDataCache
vec_init(&result->callbackDataCache);
// Allocate MenuItem Map
if( 0 != hashmap_create((const unsigned)16, &result->menuItemMap)) {
ABORT("[NewMenu] Not enough memory to allocate menuItemMap!");
}
// Allocate the Radio Group Map
if( 0 != hashmap_create((const unsigned)4, &result->radioGroupMap)) {
ABORT("[NewMenu] Not enough memory to allocate radioGroupMap!");
}
// Init other members
result->menu = NULL;
result->store = store;
// Process the menu
ProcessMenu(result, menuData, radioGroups);
return result;
}
Menu* NewApplicationMenu(const char *menuAsJSON, struct TrayMenuStore *store) {
// Parse the menu json
JsonNode *processedMenu = json_decode(menuAsJSON);
if( processedMenu == NULL ) {
// Parse error!
ABORT("Unable to parse Menu JSON: %s", menuAsJSON);
}
// TODO - fixup
Menu *result = NewMenu(processedMenu, NULL, store);
result->menuType = ApplicationMenuType;
return result;
}
//TODO: Put traymenu store in callback instead of menu as it'll never be null
MenuItemCallbackData* CreateMenuItemCallbackData(Menu *menu, const char *menuItemID, enum MenuItemType menuItemType) {
MenuItemCallbackData* result = malloc(sizeof(MenuItemCallbackData));
result->store = menu->store;
result->menuItemID = STRCOPY(menuItemID);
result->menuItemType = menuItemType;
// Store reference to this so we can destroy later
vec_push(&menu->callbackDataCache, result);
return result;
}
void DeleteMenu(Menu *menu) {
if( menu == NULL ) {
return;
}
// Free menu item hashmap
hashmap_destroy(&menu->menuItemMap);
// Free radio group members
if( hashmap_num_entries(&menu->radioGroupMap) > 0 ) {
if (0 != hashmap_iterate_pairs(&menu->radioGroupMap, freeHashmapItem, NULL)) {
ABORT("[DeleteMenu] Failed to release radioGroupMap entries!");
}
}
// Free radio groups hashmap
hashmap_destroy(&menu->radioGroupMap);
// Release the callback cache memory
int i; MenuItemCallbackData *callback;
vec_foreach(&menu->callbackDataCache, callback, i) {
MEMFREE(callback->menuItemID);
}
vec_deinit(&menu->callbackDataCache);
// Free nsmenu if we have it
if ( menu->menu != NULL ) {
msg(menu->menu, s("release"));
}
free(menu);
}
void addSeparator(id menu) {
id item = msg(c("NSMenuItem"), s("separatorItem"));
msg(menu, s("addItem:"), item);
}
id createMenuItemNoAutorelease( id title, const char *action, const char *key) {
id item = ALLOC("NSMenuItem");
msg(item, s("initWithTitle:action:keyEquivalent:"), title, s(action), str(key));
return item;
}
id createMenuItem(id title, const char *action, const char *key) {
id item = ALLOC("NSMenuItem");
msg(item, s("initWithTitle:action:keyEquivalent:"), title, s(action), str(key));
msg(item, s("autorelease"));
return item;
}
id addMenuItem(id menu, const char *title, const char *action, const char *key, bool disabled) {
id item = createMenuItem(str(title), action, key);
msg(item, s("setEnabled:"), !disabled);
msg(menu, s("addItem:"), item);
return item;
}
id createMenu(id title) {
id menu = ALLOC("NSMenu");
msg(menu, s("initWithTitle:"), title);
msg(menu, s("setAutoenablesItems:"), NO);
// msg(menu, s("autorelease"));
return menu;
}
void createDefaultAppMenu(id parentMenu) {
// App Menu
id appName = msg(msg(c("NSProcessInfo"), s("processInfo")), s("processName"));
id appMenuItem = createMenuItemNoAutorelease(appName, NULL, "");
id appMenu = createMenu(appName);
msg(appMenuItem, s("setSubmenu:"), appMenu);
msg(parentMenu, s("addItem:"), appMenuItem);
id title = msg(str("Hide "), s("stringByAppendingString:"), appName);
id item = createMenuItem(title, "hide:", "h");
msg(appMenu, s("addItem:"), item);
id hideOthers = addMenuItem(appMenu, "Hide Others", "hideOtherApplications:", "h", FALSE);
msg(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand));
addMenuItem(appMenu, "Show All", "unhideAllApplications:", "", FALSE);
addSeparator(appMenu);
title = msg(str("Quit "), s("stringByAppendingString:"), appName);
item = createMenuItem(title, "terminate:", "q");
msg(appMenu, s("addItem:"), item);
}
void createDefaultEditMenu(id parentMenu) {
// Edit Menu
id editMenuItem = createMenuItemNoAutorelease(str("Edit"), NULL, "");
id editMenu = createMenu(str("Edit"));
msg(editMenuItem, s("setSubmenu:"), editMenu);
msg(parentMenu, s("addItem:"), editMenuItem);
addMenuItem(editMenu, "Undo", "undo:", "z", FALSE);
addMenuItem(editMenu, "Redo", "redo:", "y", FALSE);
addSeparator(editMenu);
addMenuItem(editMenu, "Cut", "cut:", "x", FALSE);
addMenuItem(editMenu, "Copy", "copy:", "c", FALSE);
addMenuItem(editMenu, "Paste", "paste:", "v", FALSE);
addMenuItem(editMenu, "Select All", "selectAll:", "a", FALSE);
}
void processMenuRole(Menu *menu, id parentMenu, JsonNode *item) {
const char *roleName = item->string_;
if ( STREQ(roleName, "appMenu") ) {
createDefaultAppMenu(parentMenu);
return;
}
if ( STREQ(roleName, "editMenu")) {
createDefaultEditMenu(parentMenu);
return;
}
if ( STREQ(roleName, "hide")) {
addMenuItem(parentMenu, "Hide Window", "hide:", "h", FALSE);
return;
}
if ( STREQ(roleName, "hideothers")) {
id hideOthers = addMenuItem(parentMenu, "Hide Others", "hideOtherApplications:", "h", FALSE);
msg(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand));
return;
}
if ( STREQ(roleName, "unhide")) {
addMenuItem(parentMenu, "Show All", "unhideAllApplications:", "", FALSE);
return;
}
if ( STREQ(roleName, "front")) {
addMenuItem(parentMenu, "Bring All to Front", "arrangeInFront:", "", FALSE);
return;
}
if ( STREQ(roleName, "undo")) {
addMenuItem(parentMenu, "Undo", "undo:", "z", FALSE);
return;
}
if ( STREQ(roleName, "redo")) {
addMenuItem(parentMenu, "Redo", "redo:", "y", FALSE);
return;
}
if ( STREQ(roleName, "cut")) {
addMenuItem(parentMenu, "Cut", "cut:", "x", FALSE);
return;
}
if ( STREQ(roleName, "copy")) {
addMenuItem(parentMenu, "Copy", "copy:", "c", FALSE);
return;
}
if ( STREQ(roleName, "paste")) {
addMenuItem(parentMenu, "Paste", "paste:", "v", FALSE);
return;
}
if ( STREQ(roleName, "delete")) {
addMenuItem(parentMenu, "Delete", "delete:", "", FALSE);
return;
}
if( STREQ(roleName, "pasteandmatchstyle")) {
id pasteandmatchstyle = addMenuItem(parentMenu, "Paste and Match Style", "pasteandmatchstyle:", "v", FALSE);
msg(pasteandmatchstyle, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagShift | NSEventModifierFlagCommand));
}
if ( STREQ(roleName, "selectall")) {
addMenuItem(parentMenu, "Select All", "selectAll:", "a", FALSE);
return;
}
if ( STREQ(roleName, "minimize")) {
addMenuItem(parentMenu, "Minimize", "miniaturize:", "m", FALSE);
return;
}
if ( STREQ(roleName, "zoom")) {
addMenuItem(parentMenu, "Zoom", "performZoom:", "", FALSE);
return;
}
if ( STREQ(roleName, "quit")) {
addMenuItem(parentMenu, "Quit (More work TBD)", "terminate:", "q", FALSE);
return;
}
if ( STREQ(roleName, "togglefullscreen")) {
addMenuItem(parentMenu, "Toggle Full Screen", "toggleFullScreen:", "f", FALSE);
return;
}
}
id processRadioMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuItemID, bool disabled, bool checked, const char *acceleratorkey, bool hascallback) {
id item = ALLOC("NSMenuItem");
// Store the item in the menu item map
// hashmap_put(&menu->menuItemMap, (char*)menuItemID, strlen(menuItemID), item);
SaveMenuItemInStore((TrayMenuStore *) menu->store, menuItemID, item);
if( hascallback ) {
// Create a MenuItemCallbackData
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, menuItemID, Radio);
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);
msg(item, s("setEnabled:"), !disabled);
msg(item, s("autorelease"));
msg(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
msg(parentmenu, s("addItem:"), item);
return item;
}
id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuItemID, bool disabled, bool checked, const char *key, bool hascallback) {
id item = ALLOC("NSMenuItem");
// Store the item in the menu item map
// hashmap_put(&menu->menuItemMap, (char*)menuItemID, strlen(menuItemID), item);
SaveMenuItemInStore((TrayMenuStore *) menu->store, menuItemID, item);
if( hascallback ) {
// Create a MenuItemCallbackData
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, menuItemID, Checkbox);
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), callback);
msg(item, s("setRepresentedObject:"), wrappedId);
}
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuItemCallback:"), str(key));
msg(item, s("setEnabled:"), !disabled);
msg(item, s("autorelease"));
msg(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
msg(parentmenu, s("addItem:"), item);
return item;
}
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuItemID, bool disabled, const char *acceleratorkey, const char **modifiers, bool hascallback) {
id item = ALLOC("NSMenuItem");
SaveMenuItemInStore((TrayMenuStore *) menu->store, menuItemID, item);
if( hascallback ) {
// Create a MenuItemCallbackData
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, menuItemID, 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);
msg(item, s("setEnabled:"), !disabled);
msg(item, s("autorelease"));
// Process modifiers
if( modifiers != NULL ) {
unsigned long modifierFlags = parseModifiers(modifiers);
msg(item, s("setKeyEquivalentModifierMask:"), modifierFlags);
}
msg(parentMenu, s("addItem:"), item);
return item;
}
void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
// Check if this item is hidden and if so, exit early!
bool hidden = getJSONBool(item, "h");
if( hidden ) {
return;
}
// Get the role
JsonNode *role = json_find_member(item, "r");
if( role != NULL ) {
processMenuRole(menu, parentMenu, role);
return;
}
// Check if this is a submenu
JsonNode *submenu = json_find_member(item, "S");
if( submenu != NULL ) {
// Get the label
JsonNode *menuNameNode = json_find_member(item, "l");
const char *name = "";
if ( menuNameNode != NULL) {
name = menuNameNode->string_;
}
id thisMenuItem = createMenuItemNoAutorelease(str(name), NULL, "");
id thisMenu = createMenu(str(name));
msg(thisMenuItem, s("setSubmenu:"), thisMenu);
msg(parentMenu, s("addItem:"), thisMenuItem);
JsonNode *submenuItems = json_find_member(submenu, "i");
// If we have no items, just return
if ( submenuItems == NULL ) {
return;
}
// Loop over submenu items
JsonNode *item;
json_foreach(item, submenuItems) {
// Get item label
processMenuItem(menu, thisMenu, item);
}
return;
}
// This is a user menu. Get the common data
// Get the label
const char *label = getJSONString(item, "l");
if ( label == NULL) {
label = "(empty)";
}
const char *menuItemID = getJSONString(item, "I");
if ( menuItemID == NULL) {
menuItemID = "";
}
bool disabled = getJSONBool(item, "d");
// Get the Accelerator
JsonNode *accelerator = json_find_member(item, "a");
const char *acceleratorkey = NULL;
const char **modifiers = NULL;
// If we have an accelerator
if( accelerator != NULL ) {
// Get the key
acceleratorkey = getJSONString(accelerator, "Key");
// Check if there are modifiers
JsonNode *modifiersList = json_find_member(accelerator, "Modifiers");
if ( modifiersList != NULL ) {
// Allocate an array of strings
int noOfModifiers = json_array_length(modifiersList);
// Do we have any?
if (noOfModifiers > 0) {
modifiers = malloc(sizeof(const char *) * (noOfModifiers + 1));
JsonNode *modifier;
int count = 0;
// Iterate the modifiers and save a reference to them in our new array
json_foreach(modifier, modifiersList) {
// Get modifier name
modifiers[count] = modifier->string_;
count++;
}
// Null terminate the modifier list
modifiers[count] = NULL;
}
}
}
// has callback?
bool hascallback = getJSONBool(item, "C");
// Get the Type
const char *type = mustJSONString(item, "t");
if( STREQ(type, "t")) {
processTextMenuItem(menu, parentMenu, label, menuItemID, disabled, acceleratorkey, modifiers, hascallback);
}
else if ( STREQ(type, "s")) {
addSeparator(parentMenu);
}
else if ( STREQ(type, "c")) {
// Get checked state
bool checked = getJSONBool(item, "c");
processCheckboxMenuItem(menu, parentMenu, label, menuItemID, disabled, checked, "", hascallback);
}
else if ( STREQ(type, "r")) {
// Get checked state
bool checked = getJSONBool(item, "c");
processRadioMenuItem(menu, parentMenu, label, menuItemID, disabled, checked, "", hascallback);
}
if ( modifiers != NULL ) {
free(modifiers);
}
return;
}
void processMenuData(Menu *menu, JsonNode *menuData) {
// Iterate items
JsonNode *item;
json_foreach(item, menuData) {
// Process each menu item
processMenuItem(menu, menu->menu, item);
}
}
void processRadioGroupJSON(Menu *menu, JsonNode *radioGroup) {
int groupLength;
getJSONInt(radioGroup, "Length", &groupLength);
JsonNode *members = json_find_member(radioGroup, "Members");
JsonNode *member;
// Allocate array
size_t arrayLength = sizeof(id)*(groupLength+1);
id memberList[arrayLength];
// Build the radio group items
int count=0;
json_foreach(member, members) {
// Get menu by id
id menuItem = (id)hashmap_get(&menu->menuItemMap, (char*)member->string_, strlen(member->string_));
// Save Member
memberList[count] = menuItem;
count = count + 1;
}
// Null terminate array
memberList[groupLength] = 0;
// Store the members
json_foreach(member, members) {
// Copy the memberList
char *newMemberList = (char *)malloc(arrayLength);
memcpy(newMemberList, memberList, arrayLength);
// add group to each member of group
hashmap_put(&menu->radioGroupMap, member->string_, strlen(member->string_), newMemberList);
}
}
id ProcessMenu(Menu *menu, JsonNode *menuData, JsonNode *radioGroups) {
// exit if we have no meny
if( menuData == NULL ) {
return NULL;
}
menu->menu = createMenu(str(""));
// Process the menu data
processMenuData(menu, menuData);
if( radioGroups != NULL ) {
// Iterate radio groups
JsonNode *radioGroup;
json_foreach(radioGroup, radioGroups) {
// Get item label
processRadioGroupJSON(menu, radioGroup);
}
}
return menu->menu;
}

View File

@@ -0,0 +1,97 @@
//
// Created by Lea Anthony on 6/1/21.
//
#ifndef MENU_DARWIN_H
#define MENU_DARWIN_H
#include "common.h"
#include "ffenestri_darwin.h"
enum MenuItemType {Text = 0, Checkbox = 1, Radio = 2};
enum MenuType {ApplicationMenuType = 0, ContextMenuType = 1, TrayMenuType = 2};
static const char *MenuTypeAsString[] = {
"ApplicationMenu", "ContextMenu", "TrayMenu",
};
extern void messageFromWindowCallback(const char *);
struct TrayMenuStore;
typedef struct {
const char *title;
/*** Internal ***/
struct hashmap_s menuItemMap;
struct hashmap_s radioGroupMap;
// Vector to keep track of callback data memory
vec_void_t callbackDataCache;
// The NSMenu for this menu
id menu;
// A reference to the Menu store
struct TrayMenuStore *store;
// The commands for the menu callbacks
const char *callbackCommand;
// This indicates if we are an Application Menu, tray menu or context menu
enum MenuType menuType;
} Menu;
typedef struct {
struct TrayMenuStore *store;
Menu *menu;
const char *menuItemID;
enum MenuItemType menuItemType;
} MenuItemCallbackData;
// NewMenu creates a new Menu struct, saving the given menu structure as JSON
Menu* NewMenu(JsonNode *menuData, JsonNode *radioGroups, struct TrayMenuStore *store);
Menu* NewApplicationMenu(const char *menuAsJSON, struct TrayMenuStore *store);
MenuItemCallbackData* CreateMenuItemCallbackData(Menu *menu, const char *menuItemID, enum MenuItemType menuItemType);
void DeleteMenu(Menu *menu);
// Creates a JSON message for the given menuItemID and data
const char* createMenuClickedMessage(const char *menuItemID, const char *data);
// Callback for text menu items
void menuItemCallback(id self, SEL cmd, id sender);
id processAcceleratorKey(const char *key);
void addSeparator(id menu);
id createMenuItemNoAutorelease( id title, const char *action, const char *key);
id createMenuItem(id title, const char *action, const char *key);
id addMenuItem(id menu, const char *title, const char *action, const char *key, bool disabled);
id createMenu(id title);
void createDefaultAppMenu(id parentMenu);
void createDefaultEditMenu(id parentMenu);
void processMenuRole(Menu *menu, id parentMenu, JsonNode *item);
// This converts a string array of modifiers into the
// equivalent MacOS Modifier Flags
unsigned long parseModifiers(const char **modifiers);
id processRadioMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *acceleratorkey, bool hasCallback);
id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *key, bool hasCallback);
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, bool hasCallback);
void processMenuItem(Menu *menu, id parentMenu, JsonNode *item);
void processMenuData(Menu *menu, JsonNode *menuData);
void processRadioGroupJSON(Menu *menu, JsonNode *radioGroup) ;
id ProcessMenu(Menu *menu, JsonNode *menuData, JsonNode *radioGroup);
#endif //ASSETS_C_MENU_DARWIN_H

View File

@@ -0,0 +1,101 @@
//
// Created by Lea Anthony on 18/1/21.
//
#include "menu.h"
MenuItem* NewMenuItem(enum MenuItemType type, const char* ID, const char* label, const char* alternateLabel, bool disabled, bool hidden, bool checked, const char* colour, const char* font, int fontsize, const char* image, const char* acceleratorKey, const char** modifiers, bool hasCallback, Menu* submenu) {
MenuItem *result = NEW(MenuItem);
// Setup
result->ID = STRCOPY(ID);
result->label = STRCOPY(label);
result->alternateLabel = STRCOPY(alternateLabel);
result->disabled = disabled;
result->hidden = hidden;
result->colour = STRCOPY(colour);
result->font = STRCOPY(font);
result->fontSize = fontsize;
result->image = STRCOPY(image);
result->acceleratorKey = STRCOPY(acceleratorKey);
result->modifiers = modifiers;
result->hasCallback = hasCallback;
result->type = type;
result->checked = checked;
result->submenu = submenu;
result->callbackData = NULL;
vec_init(&result->radioGroup);
SetupMenuItemPlatformData(result);
return result;
}
void DeleteMenuItem(MenuItem* menuItem) {
MEMFREE(menuItem->ID);
MEMFREE(menuItem->label);
MEMFREE(menuItem->alternateLabel);
MEMFREE(menuItem->colour);
MEMFREE(menuItem->font);
MEMFREE(menuItem->image);
MEMFREE(menuItem->acceleratorKey);
// Iterate the modifiers and free elements
if( menuItem->modifiers != NULL ) {
int i = 0;
const char *nextItem = menuItem->modifiers[0];
while (nextItem != NULL) {
MEMFREE(nextItem);
i++;
nextItem = menuItem->modifiers[i];
}
MEMFREE(menuItem->modifiers);
}
DeleteMenuItemCallbackData(menuItem->callbackData);
DeleteMenuItemPlatformData(menuItem);
MEMFREE(menuItem);
}
JsonNode* MenuItemAsJSONObject(MenuItem* menuItem) {
JsonNode* result = json_mkobject();
JSON_ADD_STRING(result, "ID", menuItem->ID);
JSON_ADD_STRING(result, "label", menuItem->label);
JSON_ADD_STRING(result, "alternateLabel", menuItem->alternateLabel);
JSON_ADD_BOOL(result, "disabled", menuItem->disabled);
JSON_ADD_BOOL(result, "hidden", menuItem->hidden);
JSON_ADD_STRING(result, "colour", menuItem->colour);
JSON_ADD_STRING(result, "font", menuItem->font);
JSON_ADD_NUMBER(result, "fontsize", menuItem->fontSize);
JSON_ADD_STRING(result, "image", menuItem->image);
JSON_ADD_STRING(result, "acceleratorKey", menuItem->acceleratorKey);
JSON_ADD_BOOL(result, "hasCallback", menuItem->hasCallback);
JSON_ADD_STRING(result, "type", MenuItemTypeAsString[menuItem->type]);
JSON_ADD_BOOL(result, "checked", menuItem->checked);
return result;
}
MenuItemCallbackData* NewMenuItemCallbackData(MenuManager *manager, const char* menuItemID, enum MenuItemType menuItemType) {
MenuItemCallbackData* result = NEW(MenuItemCallbackData);
result->manager = manager;
result->menuItemID = STRCOPY(menuItemID);
result->menuItemType = menuItemType;
return result;
}
void DeleteMenuItemCallbackData(MenuItemCallbackData* callbackData) {
if( callbackData == NULL ) return;
MEMFREE(callbackData->menuItemID);
MEMFREE(callbackData);
}

View File

@@ -0,0 +1,81 @@
//
// Created by Lea Anthony on 18/1/21.
//
#include "menu.h"
MenuManager* NewMenuManager() {
MenuManager* result = malloc(sizeof(MenuManager));
// Allocate Hashmaps
HASHMAP_INIT(result->menuItems, 32, "menuItems");
HASHMAP_INIT(result->contextMenus, 4, "contextMenus");
HASHMAP_INIT(result->trayMenus, 4, "trayMenus");
// Initialise other data
result->applicationMenu = NULL;
result->contextMenuData = NULL;
return result;
}
int deleteTrayMenu(void *const context, struct hashmap_element_s *const e) {
DeleteTrayMenu(e->data);
return -1; // Remove from hashmap
}
int deleteMenuItem(void *const context, struct hashmap_element_s *const e) {
DeleteMenuItem(e->data);
return -1; // Remove from hashmap
}
void DeleteMenuManager(MenuManager* manager) {
// Iterate hashmaps and delete items
HASHMAP_ITERATE(manager->trayMenus, deleteTrayMenu, NULL);
HASHMAP_ITERATE(manager->menuItems, deleteMenuItem, NULL);
// Delete applicationMenu
DeleteMenu(manager->applicationMenu);
// Delete Hashmaps
HASHMAP_DESTROY(manager->trayMenus);
HASHMAP_DESTROY(manager->contextMenus);
HASHMAP_DESTROY(manager->menuItems);
// Delete context menu data
MEMFREE(manager->contextMenuData);
}
TrayMenu* AddTrayMenuToManager(MenuManager* manager, const char* trayMenuJSON) {
// Parse JSON
struct JsonNode* parsedJSON = mustParseJSON(trayMenuJSON);
// Get the ID
const char *ID = mustJSONString(parsedJSON, "I");
// Check if there is already an entry for this menu
TrayMenu* existingTrayMenu = HASHMAP_GET(manager->trayMenus, ID);
if ( existingTrayMenu != NULL ) {
json_delete(parsedJSON);
return existingTrayMenu;
}
// Create new menu
TrayMenu* newMenu = NewTrayMenu(parsedJSON, manager);
HASHMAP_PUT(manager->trayMenus, newMenu->ID, newMenu);
return newMenu;
}
int showTrayMenu(void *const context, struct hashmap_element_s *const e) {
ShowTrayMenu(e->data);
// 0 to retain element, -1 to delete.
return 0;
}
void ShowTrayMenus(MenuManager* manager) {
HASHMAP_ITERATE(manager->trayMenus, showTrayMenu, NULL);
}

View File

@@ -0,0 +1,69 @@
//
// Created by Lea Anthony on 18/1/21.
//
#include "menu.h"
TrayMenu* NewTrayMenu(JsonNode* parsedJSON, MenuManager *manager) {
// NULL GUARD
if(parsedJSON == NULL) ABORT("[NewTrayMenu] parsedJSON == NULL");
// Create new tray menu
TrayMenu* result = NEW(TrayMenu);
// Initialise other data
result->ID = STRCOPY(mustJSONString(parsedJSON, "I"));
result->Label = STRCOPY(getJSONString(parsedJSON, "l"));
result->Icon = STRCOPY(getJSONString(parsedJSON, "i"));
// Process menu
struct JsonNode* menuJSON = getJSONObject(parsedJSON, "m");
struct JsonNode* radioJSON = getJSONObject(parsedJSON, "r");
result->Menu = NewMenu(menuJSON, radioJSON, manager);
// Setup platform data
SetupTrayMenuPlatformData(result);
return result;
}
void DeleteTrayMenu(TrayMenu *trayMenu) {
// NULL guard
if( trayMenu == NULL ) return;
// Free the strings
MEMFREE(trayMenu->ID);
MEMFREE(trayMenu->Label);
MEMFREE(trayMenu->Icon);
// Delete the menu
DeleteMenu(trayMenu->Menu);
// Delete the platform data
DeleteTrayMenuPlatformData(trayMenu);
// Free tray menu
MEMFREE(trayMenu);
}
const char* TrayMenuAsJSON(TrayMenu* menu) {
JsonNode *jsonObject = json_mkobject();
JSON_ADD_STRING(jsonObject, "ID", menu->ID);
JSON_ADD_STRING(jsonObject, "Label", menu->Label);
JSON_ADD_STRING(jsonObject, "Icon", menu->Icon);
JSON_ADD_OBJECT(jsonObject, "Menu", MenuAsJSONObject(menu->Menu));
return json_encode(jsonObject);
}
void UpdateTrayIcon(TrayMenu *trayMenu) {
// Exit early if NULL
if( trayMenu->Icon == NULL ) {
return;
}
PlatformUpdateTrayIcon(trayMenu);
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,25 @@
cmake_minimum_required(VERSION 3.17)
project(test_app)
set(CMAKE_CXX_STANDARD 14)
set (CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address -g")
set (CMAKE_LINKER_FLAGS_DEBUG "${CMAKE_LINKER_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address -fsanitize=leak -g")
add_executable(test_app test.c)
add_library(menus STATIC ../menu_manager.c ../menu.c ../menu_tray.c ../menu_item.c )
add_library(common STATIC ../common.c ../utf8.h)
add_library(json STATIC ../json.c)
add_library(vec STATIC ../vec.c)
if( CMAKE_HOST_APPLE )
find_library(WEBKIT WebKit)
add_library(runtime STATIC ../runtime_darwin.c)
add_library(ffenestri STATIC ../ffenestri_darwin.c)
add_library(defaulticons STATIC ../defaultdialogicons_darwin.c)
add_library(platform STATIC ../menu_darwin.c)
target_link_libraries(test_app objc ${WEBKIT} ffenestri platform runtime)
endif()
target_link_libraries(test_app vec json common menus)
include_directories(..)
include_directories(.)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,21 @@
cmake_minimum_required(VERSION 3.17)
project(test_menumanager)
set(CMAKE_CXX_STANDARD 14)
set (CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address -g")
set (CMAKE_LINKER_FLAGS_DEBUG "${CMAKE_LINKER_FLAGS_DEBUG} -fno-omit-frame-pointer -fsanitize=address -fsanitize=leak -g")
add_executable(test_menumanager test.c minunit.h)
add_library(menus STATIC ../menu_manager.c ../menu.c ../menu_tray.c ../menu_item.c)
add_library(common STATIC ../common.c ../utf8.h)
add_library(json STATIC ../json.c)
add_library(vec STATIC ../vec.c)
if( CMAKE_HOST_APPLE )
add_library(platform STATIC ../menu_darwin.c)
target_link_libraries(test_menumanager objc)
endif()
target_link_libraries(test_menumanager vec json common menus platform)
include_directories(..)
include_directories(.)

View File

@@ -0,0 +1,20 @@
Copyright (c) 2012 David Siñuela Pastor, siu.4coders@gmail.com
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,391 @@
/*
* Copyright (c) 2012 David Siñuela Pastor, siu.4coders@gmail.com
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef MINUNIT_MINUNIT_H
#define MINUNIT_MINUNIT_H
#ifdef __cplusplus
extern "C" {
#endif
#if defined(_WIN32)
#include <Windows.h>
#if defined(_MSC_VER) && _MSC_VER < 1900
#define snprintf _snprintf
#define __func__ __FUNCTION__
#endif
#elif defined(__unix__) || defined(__unix) || defined(unix) || (defined(__APPLE__) && defined(__MACH__))
/* Change POSIX C SOURCE version for pure c99 compilers */
#if !defined(_POSIX_C_SOURCE) || _POSIX_C_SOURCE < 200112L
#undef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200112L
#endif
#include <unistd.h> /* POSIX flags */
#include <time.h> /* clock_gettime(), time() */
#include <sys/time.h> /* gethrtime(), gettimeofday() */
#include <sys/resource.h>
#include <sys/times.h>
#include <string.h>
#if defined(__MACH__) && defined(__APPLE__)
#include <mach/mach.h>
#include <mach/mach_time.h>
#endif
#if __GNUC__ >= 5 && !defined(__STDC_VERSION__)
#define __func__ __extension__ __FUNCTION__
#endif
#else
#error "Unable to define timers for an unknown OS."
#endif
#include <stdio.h>
#include <math.h>
/* Maximum length of last message */
#define MINUNIT_MESSAGE_LEN 1024
/* Accuracy with which floats are compared */
#define MINUNIT_EPSILON 1E-12
/* Misc. counters */
static int minunit_run = 0;
static int minunit_assert = 0;
static int minunit_fail = 0;
static int minunit_status = 0;
/* Timers */
static double minunit_real_timer = 0;
static double minunit_proc_timer = 0;
/* Last message */
static char minunit_last_message[MINUNIT_MESSAGE_LEN];
/* Test setup and teardown function pointers */
static void (*minunit_setup)(void) = NULL;
static void (*minunit_teardown)(void) = NULL;
/* Definitions */
#define MU_TEST(method_name) static void method_name(void)
#define MU_TEST_SUITE(suite_name) static void suite_name(void)
#define MU__SAFE_BLOCK(block) do {\
block\
} while(0)
/* Run test suite and unset setup and teardown functions */
#define MU_RUN_SUITE(suite_name) MU__SAFE_BLOCK(\
suite_name();\
minunit_setup = NULL;\
minunit_teardown = NULL;\
)
/* Configure setup and teardown functions */
#define MU_SUITE_CONFIGURE(setup_fun, teardown_fun) MU__SAFE_BLOCK(\
minunit_setup = setup_fun;\
minunit_teardown = teardown_fun;\
)
/* Test runner */
#define MU_RUN_TEST(test) MU__SAFE_BLOCK(\
if (minunit_real_timer==0 && minunit_proc_timer==0) {\
minunit_real_timer = mu_timer_real();\
minunit_proc_timer = mu_timer_cpu();\
}\
if (minunit_setup) (*minunit_setup)();\
minunit_status = 0;\
test();\
minunit_run++;\
if (minunit_status) {\
minunit_fail++;\
printf("F");\
printf("\n%s\n", minunit_last_message);\
}\
fflush(stdout);\
if (minunit_teardown) (*minunit_teardown)();\
)
/* Report */
#define MU_REPORT() MU__SAFE_BLOCK(\
double minunit_end_real_timer;\
double minunit_end_proc_timer;\
printf("\n\n%d tests, %d assertions, %d failures\n", minunit_run, minunit_assert, minunit_fail);\
minunit_end_real_timer = mu_timer_real();\
minunit_end_proc_timer = mu_timer_cpu();\
printf("\nFinished in %.8f seconds (real) %.8f seconds (proc)\n\n",\
minunit_end_real_timer - minunit_real_timer,\
minunit_end_proc_timer - minunit_proc_timer);\
)
#define MU_EXIT_CODE minunit_fail
/* Assertions */
#define mu_check(test) MU__SAFE_BLOCK(\
minunit_assert++;\
if (!(test)) {\
snprintf(minunit_last_message, MINUNIT_MESSAGE_LEN, "%s failed:\n\t%s:%d: %s", __func__, __FILE__, __LINE__, #test);\
minunit_status = 1;\
return;\
} else {\
printf(".");\
}\
)
#define mu_fail(message) MU__SAFE_BLOCK(\
minunit_assert++;\
snprintf(minunit_last_message, MINUNIT_MESSAGE_LEN, "%s failed:\n\t%s:%d: %s", __func__, __FILE__, __LINE__, message);\
minunit_status = 1;\
return;\
)
#define mu_assert(test, message) MU__SAFE_BLOCK(\
minunit_assert++;\
if (!(test)) {\
snprintf(minunit_last_message, MINUNIT_MESSAGE_LEN, "%s failed:\n\t%s:%d: %s", __func__, __FILE__, __LINE__, message);\
minunit_status = 1;\
return;\
} else {\
printf(".");\
}\
)
#define mu_assert_int_eq(expected, result) MU__SAFE_BLOCK(\
int minunit_tmp_e;\
int minunit_tmp_r;\
minunit_assert++;\
minunit_tmp_e = (expected);\
minunit_tmp_r = (result);\
if (minunit_tmp_e != minunit_tmp_r) {\
snprintf(minunit_last_message, MINUNIT_MESSAGE_LEN, "%s failed:\n\t%s:%d: %d expected but was %d", __func__, __FILE__, __LINE__, minunit_tmp_e, minunit_tmp_r);\
minunit_status = 1;\
return;\
} else {\
printf(".");\
}\
)
#define mu_assert_double_eq(expected, result) MU__SAFE_BLOCK(\
double minunit_tmp_e;\
double minunit_tmp_r;\
minunit_assert++;\
minunit_tmp_e = (expected);\
minunit_tmp_r = (result);\
if (fabs(minunit_tmp_e-minunit_tmp_r) > MINUNIT_EPSILON) {\
int minunit_significant_figures = 1 - log10(MINUNIT_EPSILON);\
snprintf(minunit_last_message, MINUNIT_MESSAGE_LEN, "%s failed:\n\t%s:%d: %.*g expected but was %.*g", __func__, __FILE__, __LINE__, minunit_significant_figures, minunit_tmp_e, minunit_significant_figures, minunit_tmp_r);\
minunit_status = 1;\
return;\
} else {\
printf(".");\
}\
)
#define mu_assert_string_eq(expected, result) MU__SAFE_BLOCK(\
const char* minunit_tmp_e = expected;\
const char* minunit_tmp_r = result;\
minunit_assert++;\
if (!minunit_tmp_e) {\
minunit_tmp_e = "<null pointer>";\
}\
if (!minunit_tmp_r) {\
minunit_tmp_r = "<null pointer>";\
}\
if(strcmp(minunit_tmp_e, minunit_tmp_r) != 0) {\
snprintf(minunit_last_message, MINUNIT_MESSAGE_LEN, "%s failed:\n\t%s:%d: '%s' expected but was '%s'", __func__, __FILE__, __LINE__, minunit_tmp_e, minunit_tmp_r);\
minunit_status = 1;\
return;\
} else {\
printf(".");\
}\
)
/*
* The following two functions were written by David Robert Nadeau
* from http://NadeauSoftware.com/ and distributed under the
* Creative Commons Attribution 3.0 Unported License
*/
/**
* Returns the real time, in seconds, or -1.0 if an error occurred.
*
* Time is measured since an arbitrary and OS-dependent start time.
* The returned real time is only useful for computing an elapsed time
* between two calls to this function.
*/
static double mu_timer_real(void)
{
#if defined(_WIN32)
/* Windows 2000 and later. ---------------------------------- */
LARGE_INTEGER Time;
LARGE_INTEGER Frequency;
QueryPerformanceFrequency(&Frequency);
QueryPerformanceCounter(&Time);
Time.QuadPart *= 1000000;
Time.QuadPart /= Frequency.QuadPart;
return (double)Time.QuadPart / 1000000.0;
#elif (defined(__hpux) || defined(hpux)) || ((defined(__sun__) || defined(__sun) || defined(sun)) && (defined(__SVR4) || defined(__svr4__)))
/* HP-UX, Solaris. ------------------------------------------ */
return (double)gethrtime( ) / 1000000000.0;
#elif defined(__MACH__) && defined(__APPLE__)
/* OSX. ----------------------------------------------------- */
static double timeConvert = 0.0;
if ( timeConvert == 0.0 )
{
mach_timebase_info_data_t timeBase;
(void)mach_timebase_info( &timeBase );
timeConvert = (double)timeBase.numer /
(double)timeBase.denom /
1000000000.0;
}
return (double)mach_absolute_time( ) * timeConvert;
#elif defined(_POSIX_VERSION)
/* POSIX. --------------------------------------------------- */
struct timeval tm;
#if defined(_POSIX_TIMERS) && (_POSIX_TIMERS > 0)
{
struct timespec ts;
#if defined(CLOCK_MONOTONIC_PRECISE)
/* BSD. --------------------------------------------- */
const clockid_t id = CLOCK_MONOTONIC_PRECISE;
#elif defined(CLOCK_MONOTONIC_RAW)
/* Linux. ------------------------------------------- */
const clockid_t id = CLOCK_MONOTONIC_RAW;
#elif defined(CLOCK_HIGHRES)
/* Solaris. ----------------------------------------- */
const clockid_t id = CLOCK_HIGHRES;
#elif defined(CLOCK_MONOTONIC)
/* AIX, BSD, Linux, POSIX, Solaris. ----------------- */
const clockid_t id = CLOCK_MONOTONIC;
#elif defined(CLOCK_REALTIME)
/* AIX, BSD, HP-UX, Linux, POSIX. ------------------- */
const clockid_t id = CLOCK_REALTIME;
#else
const clockid_t id = (clockid_t)-1; /* Unknown. */
#endif /* CLOCK_* */
if ( id != (clockid_t)-1 && clock_gettime( id, &ts ) != -1 )
return (double)ts.tv_sec +
(double)ts.tv_nsec / 1000000000.0;
/* Fall thru. */
}
#endif /* _POSIX_TIMERS */
/* AIX, BSD, Cygwin, HP-UX, Linux, OSX, POSIX, Solaris. ----- */
gettimeofday( &tm, NULL );
return (double)tm.tv_sec + (double)tm.tv_usec / 1000000.0;
#else
return -1.0; /* Failed. */
#endif
}
/**
* Returns the amount of CPU time used by the current process,
* in seconds, or -1.0 if an error occurred.
*/
static double mu_timer_cpu(void)
{
#if defined(_WIN32)
/* Windows -------------------------------------------------- */
FILETIME createTime;
FILETIME exitTime;
FILETIME kernelTime;
FILETIME userTime;
/* This approach has a resolution of 1/64 second. Unfortunately, Windows' API does not offer better */
if ( GetProcessTimes( GetCurrentProcess( ),
&createTime, &exitTime, &kernelTime, &userTime ) != 0 )
{
ULARGE_INTEGER userSystemTime;
memcpy(&userSystemTime, &userTime, sizeof(ULARGE_INTEGER));
return (double)userSystemTime.QuadPart / 10000000.0;
}
#elif defined(__unix__) || defined(__unix) || defined(unix) || (defined(__APPLE__) && defined(__MACH__))
/* AIX, BSD, Cygwin, HP-UX, Linux, OSX, and Solaris --------- */
#if defined(_POSIX_TIMERS) && (_POSIX_TIMERS > 0)
/* Prefer high-res POSIX timers, when available. */
{
clockid_t id;
struct timespec ts;
#if _POSIX_CPUTIME > 0
/* Clock ids vary by OS. Query the id, if possible. */
if ( clock_getcpuclockid( 0, &id ) == -1 )
#endif
#if defined(CLOCK_PROCESS_CPUTIME_ID)
/* Use known clock id for AIX, Linux, or Solaris. */
id = CLOCK_PROCESS_CPUTIME_ID;
#elif defined(CLOCK_VIRTUAL)
/* Use known clock id for BSD or HP-UX. */
id = CLOCK_VIRTUAL;
#else
id = (clockid_t)-1;
#endif
if ( id != (clockid_t)-1 && clock_gettime( id, &ts ) != -1 )
return (double)ts.tv_sec +
(double)ts.tv_nsec / 1000000000.0;
}
#endif
#if defined(RUSAGE_SELF)
{
struct rusage rusage;
if ( getrusage( RUSAGE_SELF, &rusage ) != -1 )
return (double)rusage.ru_utime.tv_sec +
(double)rusage.ru_utime.tv_usec / 1000000.0;
}
#endif
#if defined(_SC_CLK_TCK)
{
const double ticks = (double)sysconf( _SC_CLK_TCK );
struct tms tms;
if ( times( &tms ) != (clock_t)-1 )
return (double)tms.tms_utime / ticks;
}
#endif
#if defined(CLOCKS_PER_SEC)
{
clock_t cl = clock( );
if ( cl != (clock_t)-1 )
return (double)cl / (double)CLOCKS_PER_SEC;
}
#endif
#endif
return -1; /* Failed. */
}
#ifdef __cplusplus
}
#endif
#endif /* MINUNIT_MINUNIT_H */

View File

@@ -0,0 +1,71 @@
//
// Created by Lea Anthony on 12/1/21.
//
#include "minunit.h"
#include "menu.h"
#define empty "{\"I\":\"T1\"}"
#define emptyExpected "{\"ID\":\"T1\",\"Label\":null,\"Icon\":null,\"Menu\":null}"
#define labelOnly "{\"I\":\"T1\",\"l\":\"test\"}"
#define labelOnlyExpected "{\"ID\":\"T1\",\"Label\":\"test\",\"Icon\":null,\"Menu\":null}"
#define iconOnly "{\"I\":\"T1\",\"i\":\"svelte\"}"
#define iconOnlyExpected "{\"ID\":\"T1\",\"Label\":null,\"Icon\":\"svelte\",\"Menu\":null}"
#define iconLabel "{\"I\":\"T1\",\"l\":\"test\",\"i\":\"svelte\"}"
#define iconLabelExpected "{\"ID\":\"T1\",\"Label\":\"test\",\"Icon\":\"svelte\",\"Menu\":null}"
#define blankLabel "{\"I\":\"T1\"}"
#define blankLabelExpected "{\"ID\":\"T1\",\"Label\":null,\"Icon\":null,\"Menu\":null}"
#define blankMenu "{\"I\":\"T1\"}"
#define blankMenuExpected "{\"ID\":\"T1\",\"Label\":null,\"Icon\":null,\"Menu\":null}"
#define menuTextItem "{\"I\":\"T1\",\"l\":\"test\",\"m\":[{\"I\":\"1\",\"l\":\"test\",\"t\":\"t\"}]}"
#define menuTextItemExpected "{\"ID\":\"T1\",\"Label\":\"test\",\"Icon\":null,\"Menu\":{\"Label\":\"\",\"Items\":[{\"ID\":\"1\",\"label\":\"test\",\"alternateLabel\":null,\"disabled\":false,\"hidden\":false,\"colour\":null,\"font\":null,\"fontsize\":0,\"image\":null,\"acceleratorKey\":null,\"hasCallback\":false,\"type\":\"Text\",\"checked\":false}]}}"
#define checkboxItem "{\"I\":\"T1\",\"m\":[{\"I\":\"1\",\"l\":\"test\",\"t\":\"c\",\"c\":true}]}"
#define checkboxItemExpected "{\"ID\":\"T1\",\"Label\":null,\"Icon\":null,\"Menu\":{\"Label\":\"\",\"Items\":[{\"ID\":\"1\",\"label\":\"test\",\"alternateLabel\":null,\"disabled\":false,\"hidden\":false,\"colour\":null,\"font\":null,\"fontsize\":0,\"image\":null,\"acceleratorKey\":null,\"hasCallback\":false,\"type\":\"Checkbox\",\"checked\":true}]}}"
#define radioGroupItems "{\"I\":\"T1\",\"m\":[{\"I\":\"1\",\"l\":\"option 1\",\"t\":\"r\",\"c\":true},{\"I\":\"2\",\"l\":\"option 2\",\"t\":\"r\"},{\"I\":\"3\",\"l\":\"option 3\",\"t\":\"r\"}],\"r\":[{\"Members\":[\"1\",\"2\",\"3\"],\"Length\":3}]}"
#define radioGroupItemsExpected ""
#define callbackItem "{\"I\":\"T1\",\"m\":[{\"I\":\"1\",\"l\":\"Preferences\",\"t\":\"t\",\"C\":true}]}"
#define callbackItemExpected ""
const char* tests[] = {
empty, emptyExpected,
labelOnly, labelOnlyExpected,
iconOnly, iconOnlyExpected,
iconLabel, iconLabelExpected,
blankLabel, blankLabelExpected,
blankMenu, blankMenuExpected,
menuTextItem, menuTextItemExpected,
checkboxItem, checkboxItemExpected,
radioGroupItems, radioGroupItemsExpected,
callbackItem, callbackItemExpected,
};
MU_TEST(manager_creation) {
MenuManager* manager = NewMenuManager();
mu_assert(manager->applicationMenu == NULL, "app menu");
mu_assert(manager->contextMenuData == NULL, "context menu data");
mu_assert_int_eq(hashmap_num_entries(&manager->contextMenus), 0);
mu_assert_int_eq(hashmap_num_entries(&manager->trayMenus), 0);
mu_assert_int_eq(hashmap_num_entries(&manager->menuItems), 0);
DeleteMenuManager(manager);
}
MU_TEST(add_tray) {
for( int count = 0; count < sizeof(tests) / sizeof(tests[0]); count += 2 ) {
MenuManager* manager = NewMenuManager();
TrayMenu* tray = AddTrayMenu(manager, tests[count]);
const char* trayJSON = TrayMenuAsJSON(tray);
mu_assert_string_eq(tests[count+1], trayJSON);
DeleteMenuManager(manager);
}
}
MU_TEST_SUITE(test_suite) {
MU_RUN_TEST(manager_creation);
MU_RUN_TEST(add_tray);
}
int main(int argc, char *argv[]) {
MU_RUN_SUITE(test_suite);
MU_REPORT();
return MU_EXIT_CODE;
}

View File

@@ -6,13 +6,11 @@
#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;
TrayMenu* NewTrayMenu(const char* menuJSON) {
TrayMenu* NewTrayMenu(const char* menuJSON, struct TrayMenuStore* store) {
TrayMenu* result = malloc(sizeof(TrayMenu));
/*
@@ -23,37 +21,29 @@ TrayMenu* NewTrayMenu(const char* menuJSON) {
ABORT("[NewTrayMenu] Unable to parse TrayMenu JSON: %s", menuJSON);
}
// Save reference to this json
result->processedJSON = processedJSON;
// TODO: Make this configurable
result->trayIconPosition = NSImageLeft;
result->ID = mustJSONString(processedJSON, "ID");
result->label = mustJSONString(processedJSON, "Label");
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);
result->ID = STRCOPY(mustJSONString(processedJSON, "I"));
result->label = STRCOPY(getJSONString(processedJSON, "l"));
result->icon = STRCOPY(getJSONString(processedJSON, "i"));
result->menu = NULL;
// Create the menu
JsonNode* processedMenu = mustJSONObject(processedJSON, "ProcessedMenu");
result->menu = NewMenu(processedMenu);
JsonNode* menu = getJSONObject(processedJSON, "m");
if( menu != NULL ) {
result->delegate = NULL;
JsonNode* radioGroups = getJSONObject(processedJSON, "r");
// Create the menu
result->menu = NewMenu(menu, radioGroups, store);
result->menu->menuType = TrayMenuType;
}
// Init tray status bar item
result->statusbaritem = NULL;
// Set the menu type and store the tray ID in the parent data
result->menu->menuType = TrayMenuType;
result->menu->parentData = (void*) result->ID;
// Free JSON
json_delete(processedJSON);
return result;
}
@@ -63,25 +53,6 @@ void DumpTrayMenu(TrayMenu* trayMenu) {
}
void UpdateTrayLabel(TrayMenu *trayMenu, const char *label, const char *fontName, int fontSize, const char *RGBA, const char *tooltip, bool disabled) {
// Exit early if NULL
if( trayMenu->label == NULL ) {
return;
}
// Update button label
id statusBarButton = msg_reg(trayMenu->statusbaritem, s("button"));
id attributedString = createAttributedString(label, fontName, fontSize, RGBA);
if( tooltip != NULL ) {
msg_id(statusBarButton, s("setToolTip:"), str(tooltip));
}
msg_bool(statusBarButton, s("setEnabled:"), !disabled);
msg_id(statusBarButton, s("setAttributedTitle:"), attributedString);
}
void UpdateTrayIcon(TrayMenu *trayMenu) {
// Exit early if NULL
@@ -89,64 +60,45 @@ void UpdateTrayIcon(TrayMenu *trayMenu) {
return;
}
id statusBarButton = msg_reg(trayMenu->statusbaritem, s("button"));
id statusBarButton = msg(trayMenu->statusbaritem, s("button"));
// Empty icon means remove it
if( STREMPTY(trayMenu->icon) ) {
// Remove image
msg_id(statusBarButton, s("setImage:"), NULL);
msg(statusBarButton, s("setImage:"), NULL);
return;
}
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 = ((id(*)(id, SEL, id, int))objc_msgSend)(data, s("initWithBase64EncodedString:options:"), str(trayMenu->icon), 0);
trayImage = ALLOC("NSImage");
msg_id(trayImage, s("initWithData:"), imageData);
if( trayMenu->templateImage ) {
msg_bool(trayImage, s("setTemplate:"), YES);
}
}
msg_int(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition);
msg_id(statusBarButton, s("setImage:"), trayImage);
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
if( trayMenu->statusbaritem == NULL ) {
id statusBar = msg_reg( c("NSStatusBar"), s("systemStatusBar") );
trayMenu->statusbaritem = ((id(*)(id, SEL, CGFloat))objc_msgSend)(statusBar, s("statusItemWithLength:"), NSVariableStatusItemLength);
msg_reg(trayMenu->statusbaritem, s("retain"));
id statusBar = msg( c("NSStatusBar"), s("systemStatusBar") );
trayMenu->statusbaritem = msg(statusBar, s("statusItemWithLength:"), NSVariableStatusItemLength);
msg(trayMenu->statusbaritem, s("retain"));
}
id statusBarButton = msg_reg(trayMenu->statusbaritem, s("button"));
msg_uint(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition);
id statusBarButton = msg(trayMenu->statusbaritem, s("button"));
msg(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition);
// Update the icon if needed
UpdateTrayIcon(trayMenu);
// Update the label if needed
UpdateTrayLabel(trayMenu, trayMenu->label, trayMenu->fontName, trayMenu->fontSize, trayMenu->RGBA, trayMenu->tooltip, trayMenu->disabled);
UpdateTrayLabel(trayMenu, trayMenu->label);
// Update the menu
id menu = GetMenu(trayMenu->menu);
objc_setAssociatedObject(menu, "trayMenuID", str(trayMenu->ID), OBJC_ASSOCIATION_ASSIGN);
// Create delegate
id trayMenuDelegate = msg_reg((id)trayMenuDelegateClass, s("new"));
msg_id(menu, s("setDelegate:"), trayMenuDelegate);
objc_setAssociatedObject(trayMenuDelegate, "menu", menu, OBJC_ASSOCIATION_ASSIGN);
// Create menu delegate
trayMenu->delegate = trayMenuDelegate;
msg_id(trayMenu->statusbaritem, s("setMenu:"), menu);
if (trayMenu->menu != NULL ) {
msg(trayMenu->statusbaritem, s("setMenu:"), trayMenu->menu->menu);
}
}
// UpdateTrayMenuInPlace receives 2 menus. The current menu gets
@@ -159,17 +111,11 @@ void UpdateTrayMenuInPlace(TrayMenu* currentMenu, TrayMenu* newMenu) {
// Set the new one
currentMenu->menu = newMenu->menu;
// Delete the old JSON
json_delete(currentMenu->processedJSON);
// Set the new JSON
currentMenu->processedJSON = newMenu->processedJSON;
// Copy the other data
currentMenu->ID = newMenu->ID;
currentMenu->label = newMenu->label;
currentMenu->ID = STRCOPY(newMenu->ID);
currentMenu->label = STRCOPY(newMenu->label);
currentMenu->trayIconPosition = newMenu->trayIconPosition;
currentMenu->icon = newMenu->icon;
currentMenu->icon = STRCOPY(newMenu->icon);
}
@@ -181,23 +127,19 @@ void DeleteTrayMenu(TrayMenu* trayMenu) {
// Delete the menu
DeleteMenu(trayMenu->menu);
// Free JSON
if (trayMenu->processedJSON != NULL ) {
json_delete(trayMenu->processedJSON);
}
// Free strings
MEMFREE(trayMenu->label);
MEMFREE(trayMenu->icon);
MEMFREE(trayMenu->ID);
// Free the status item
if ( trayMenu->statusbaritem != NULL ) {
id statusBar = msg_reg( c("NSStatusBar"), s("systemStatusBar") );
msg_id(statusBar, s("removeStatusItem:"), trayMenu->statusbaritem);
msg_reg(trayMenu->statusbaritem, s("release"));
id statusBar = msg( c("NSStatusBar"), s("systemStatusBar") );
msg(statusBar, s("removeStatusItem:"), trayMenu->statusbaritem);
msg(trayMenu->statusbaritem, s("release"));
trayMenu->statusbaritem = NULL;
}
if ( trayMenu->delegate != NULL ) {
msg_reg(trayMenu->delegate, s("release"));
}
// Free the tray menu memory
MEMFREE(trayMenu);
}
@@ -227,21 +169,10 @@ void LoadTrayIcons() {
int length = atoi((const char *)lengthAsString);
// Create the icon and add to the hashmap
id imageData = ((id(*)(id, SEL, id, int))objc_msgSend)(c("NSData"), s("dataWithBytes:length:"), data, length);
id imageData = msg(c("NSData"), s("dataWithBytes:length:"), data, length);
id trayImage = ALLOC("NSImage");
msg_id(trayImage, s("initWithData:"), imageData);
msg(trayImage, s("initWithData:"), imageData);
hashmap_put(&trayIconCache, (const char *)name, strlen((const char *)name), trayImage);
}
}
void UnloadTrayIcons() {
// Release the tray cache images
if( hashmap_num_entries(&trayIconCache) > 0 ) {
if (0!=hashmap_iterate_pairs(&trayIconCache, releaseNSObject, NULL)) {
ABORT("failed to release hashmap entries!");
}
}
//Free radio groups hashmap
hashmap_destroy(&trayIconCache);
}

View File

@@ -6,39 +6,26 @@
#define TRAYMENU_DARWIN_H
#include "common.h"
#include "menu_darwin.h"
#include "menu_darwin_old.h"
typedef struct {
const char *label;
const char *icon;
const char *ID;
const char *tooltip;
bool templateImage;
const char *fontName;
int fontSize;
const char *RGBA;
bool disabled;
Menu* menu;
id statusbaritem;
unsigned int trayIconPosition;
JsonNode* processedJSON;
id delegate;
int trayIconPosition;
} TrayMenu;
TrayMenu* NewTrayMenu(const char *trayJSON);
TrayMenu* NewTrayMenu(const char *trayJSON, struct TrayMenuStore* store);
void DumpTrayMenu(TrayMenu* trayMenu);
void ShowTrayMenu(TrayMenu* trayMenu);
void UpdateTrayMenuInPlace(TrayMenu* currentMenu, TrayMenu* newMenu);
void UpdateTrayIcon(TrayMenu *trayMenu);
void UpdateTrayLabel(TrayMenu *trayMenu, const char *label, const char *fontName, int fontSize, const char *RGBA, const char *tooltip, bool disabled);
void UpdateTrayLabel(TrayMenu *trayMenu, const char*);
void LoadTrayIcons();
void UnloadTrayIcons();

View File

@@ -16,9 +16,9 @@ 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);
// Allocate menu item store
if( 0 != hashmap_create((const unsigned)8, &result->menuItemMap)) {
ABORT("[NewTrayMenuStore] Not enough memory to allocate menuItemMap!");
}
return result;
@@ -30,19 +30,15 @@ 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, (struct TrayMenuStore *) store);
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) {
@@ -52,13 +48,12 @@ 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;
@@ -76,38 +71,33 @@ void DeleteTrayMenuStore(TrayMenuStore *store) {
// Destroy tray menu map
hashmap_destroy(&store->trayMenuMap);
pthread_mutex_destroy(&store->lock);
// Destroy menu item map
hashmap_destroy(&store->menuItemMap);
}
TrayMenu* GetTrayMenuFromStore(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);
return result;
return hashmap_get(&store->trayMenuMap, menuID, strlen(menuID));
}
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);
id GetMenuItemFromStore(TrayMenuStore* store, const char* menuItemID) {
return hashmap_get(&store->menuItemMap, menuItemID, strlen(menuItemID));
}
void SaveMenuItemInStore(TrayMenuStore* store, const char* menuItemID, id nsmenuitem) {
hashmap_put(&store->menuItemMap, menuItemID, strlen(menuItemID), nsmenuitem);
}
void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON) {
// Parse the JSON
JsonNode *parsedUpdate = mustParseJSON(JSON);
@@ -118,23 +108,12 @@ void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON) {
// Check we have this menu
TrayMenu *menu = MustGetTrayMenuFromStore(store, ID);
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);
UpdateTrayLabel(menu, Label);
}
void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON) {
TrayMenu* newMenu = NewTrayMenu(menuJSON);
// DumpTrayMenu(newMenu);
TrayMenu* UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON) {
TrayMenu* newMenu = NewTrayMenu(menuJSON, (struct TrayMenuStore *) store);
// Get the current menu
TrayMenu *currentMenu = GetTrayMenuFromStore(store, newMenu->ID);
@@ -142,22 +121,15 @@ 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);
return;
return newMenu;
}
// DumpTrayMenu(currentMenu);
// 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);
@@ -166,10 +138,13 @@ 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);
return newMenu;
}
const char* GetContextMenuDataFromStore(TrayMenuStore *store) {
return store->contextMenuData;
}

View File

@@ -7,8 +7,6 @@
#include "traymenu_darwin.h"
#include <pthread.h>
typedef struct {
int dummy;
@@ -17,20 +15,26 @@ typedef struct {
// It maps tray IDs to TrayMenu*
struct hashmap_s trayMenuMap;
pthread_mutex_t lock;
// This is our menu item map
// It maps menu Item IDs to NSMenuItems
struct hashmap_s menuItemMap;
const char* contextMenuData;
} TrayMenuStore;
TrayMenuStore* NewTrayMenuStore();
void AddTrayMenuToStore(TrayMenuStore* store, const char* menuJSON);
void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON);
TrayMenu* UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON);
void ShowTrayMenusInStore(TrayMenuStore* store);
void DeleteTrayMenuStore(TrayMenuStore* store);
TrayMenu* GetTrayMenuByID(TrayMenuStore* store, const char* menuID);
void SaveMenuItemInStore(TrayMenuStore* store, const char* menuItemID, id nsmenuitem);
TrayMenu* GetTrayMenuFromStore(TrayMenuStore* store, const char* menuID);
id GetMenuItemFromStore(TrayMenuStore* store, const char* menuItemID);
void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON);
void DeleteTrayMenuInStore(TrayMenuStore* store, const char* id);
const char* GetContextMenuDataFromStore(TrayMenuStore *store);
#endif //TRAYMENUSTORE_DARWIN_H

1292
v2/internal/ffenestri/utf8.h Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -44,6 +44,8 @@
( vec_splice_(vec_unpack_(v), start, count),\
(v)->length -= (count) )
#define vec_size(v) \
(v)->length
#define vec_swapsplice(v, start, count)\
( vec_swapsplice_(vec_unpack_(v), start, count),\

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 = 0755
perms = 0700
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 = MkDirs(dst)
err = os.MkdirAll(dst, si.Mode())
if err != nil {
return
}

View File

@@ -1,103 +0,0 @@
package github
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"sort"
"strings"
)
// GetVersionTags gets the list of tags on the Wails repo
// It returns a list of sorted tags in descending order
func GetVersionTags() ([]*SemanticVersion, error) {
result := []*SemanticVersion{}
var err error
resp, err := http.Get("https://api.github.com/repos/wailsapp/wails/tags")
if err != nil {
return result, err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return result, err
}
data := []map[string]interface{}{}
err = json.Unmarshal(body, &data)
if err != nil {
return result, err
}
// Convert tag data to Version structs
for _, tag := range data {
version := tag["name"].(string)
if !strings.HasPrefix(version, "v2") {
continue
}
semver, err := NewSemanticVersion(version)
if err != nil {
return result, err
}
result = append(result, semver)
}
// Reverse Sort
sort.Sort(sort.Reverse(SemverCollection(result)))
return result, err
}
// GetLatestStableRelease gets the latest stable release on GitHub
func GetLatestStableRelease() (result *SemanticVersion, err error) {
tags, err := GetVersionTags()
if err != nil {
return nil, err
}
for _, tag := range tags {
if tag.IsRelease() {
return tag, nil
}
}
return nil, fmt.Errorf("no release tag found")
}
// GetLatestPreRelease gets the latest prerelease on GitHub
func GetLatestPreRelease() (result *SemanticVersion, err error) {
tags, err := GetVersionTags()
if err != nil {
return nil, err
}
for _, tag := range tags {
if tag.IsPreRelease() {
return tag, nil
}
}
return nil, fmt.Errorf("no prerelease tag found")
}
// IsValidTag returns true if the given string is a valid tag
func IsValidTag(tagVersion string) (bool, error) {
if tagVersion[0] == 'v' {
tagVersion = tagVersion[1:]
}
tags, err := GetVersionTags()
if err != nil {
return false, err
}
for _, tag := range tags {
if tag.String() == tagVersion {
return true, nil
}
}
return false, nil
}

View File

@@ -1,106 +0,0 @@
package github
import (
"fmt"
"github.com/Masterminds/semver"
)
// SemanticVersion is a struct containing a semantic version
type SemanticVersion struct {
Version *semver.Version
}
// NewSemanticVersion creates a new SemanticVersion object with the given version string
func NewSemanticVersion(version string) (*SemanticVersion, error) {
semverVersion, err := semver.NewVersion(version)
if err != nil {
return nil, err
}
return &SemanticVersion{
Version: semverVersion,
}, nil
}
// IsRelease returns true if it's a release version
func (s *SemanticVersion) IsRelease() bool {
// Limit to v2
if s.Version.Major() != 2 {
return false
}
return len(s.Version.Prerelease()) == 0 && len(s.Version.Metadata()) == 0
}
// IsPreRelease returns true if it's a prerelease version
func (s *SemanticVersion) IsPreRelease() bool {
// Limit to v1
if s.Version.Major() != 2 {
return false
}
return len(s.Version.Prerelease()) > 0
}
func (s *SemanticVersion) String() string {
return s.Version.String()
}
// IsGreaterThan returns true if this version is greater than the given version
func (s *SemanticVersion) IsGreaterThan(version *SemanticVersion) (bool, error) {
// Set up new constraint
constraint, err := semver.NewConstraint("> " + version.Version.String())
if err != nil {
return false, err
}
// Check if the desired one is greater than the requested on
success, msgs := constraint.Validate(s.Version)
if !success {
return false, msgs[0]
}
return true, nil
}
// IsGreaterThanOrEqual returns true if this version is greater than or equal the given version
func (s *SemanticVersion) IsGreaterThanOrEqual(version *SemanticVersion) (bool, error) {
// Set up new constraint
constraint, err := semver.NewConstraint(">= " + version.Version.String())
if err != nil {
return false, err
}
// Check if the desired one is greater than the requested on
success, msgs := constraint.Validate(s.Version)
if !success {
return false, msgs[0]
}
return true, nil
}
// MainVersion returns the main version of any version+prerelease+metadata
// EG: MainVersion("1.2.3-pre") => "1.2.3"
func (s *SemanticVersion) MainVersion() *SemanticVersion {
mainVersion := fmt.Sprintf("%d.%d.%d", s.Version.Major(), s.Version.Minor(), s.Version.Patch())
result, _ := NewSemanticVersion(mainVersion)
return result
}
// SemverCollection is a collection of SemanticVersion objects
type SemverCollection []*SemanticVersion
// Len returns the length of a collection. The number of Version instances
// on the slice.
func (c SemverCollection) Len() int {
return len(c)
}
// Less is needed for the sort interface to compare two Version objects on the
// slice. If checks if one is less than the other.
func (c SemverCollection) Less(i, j int) bool {
return c[i].Version.LessThan(c[j].Version)
}
// Swap is needed for the sort interface to replace the Version objects
// at two different positions in the slice.
func (c SemverCollection) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
}

View File

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

View File

@@ -1,46 +0,0 @@
package menumanager
import "github.com/wailsapp/wails/v2/pkg/menu"
func (m *Manager) SetApplicationMenu(applicationMenu *menu.Menu) error {
if applicationMenu == nil {
return nil
}
m.applicationMenu = applicationMenu
// Reset the menu map
m.applicationMenuItemMap = NewMenuItemMap()
// Add the menu to the menu map
m.applicationMenuItemMap.AddMenu(applicationMenu)
return m.processApplicationMenu()
}
func (m *Manager) GetApplicationMenuJSON() string {
return m.applicationMenuJSON
}
// UpdateApplicationMenu reprocesses the application menu to pick up structure
// changes etc
// Returns the JSON representation of the updated menu
func (m *Manager) UpdateApplicationMenu() (string, error) {
m.applicationMenuItemMap = NewMenuItemMap()
m.applicationMenuItemMap.AddMenu(m.applicationMenu)
err := m.processApplicationMenu()
return m.applicationMenuJSON, err
}
func (m *Manager) processApplicationMenu() error {
// Process the menu
processedApplicationMenu := NewWailsMenu(m.applicationMenuItemMap, m.applicationMenu)
applicationMenuJSON, err := processedApplicationMenu.AsJSON()
if err != nil {
return err
}
m.applicationMenuJSON = applicationMenuJSON
return nil
}

View File

@@ -1,60 +1,65 @@
package menumanager
import (
"encoding/json"
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu"
)
type ContextMenu struct {
ID string
ProcessedMenu *WailsMenu
menuItemMap *MenuItemMap
menu *menu.Menu
}
func (t *ContextMenu) AsJSON() (string, error) {
data, err := json.Marshal(t)
if err != nil {
return "", err
}
return string(data), nil
}
func NewContextMenu(contextMenu *menu.ContextMenu) *ContextMenu {
result := &ContextMenu{
ID: contextMenu.ID,
menu: contextMenu.Menu,
menuItemMap: NewMenuItemMap(),
}
result.menuItemMap.AddMenu(contextMenu.Menu)
result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu)
return result
}
import "github.com/wailsapp/wails/v2/pkg/menu"
//
//import (
// "encoding/json"
// "fmt"
// "github.com/wailsapp/wails/v2/pkg/menu"
//)
//
//type ContextMenu struct {
// ID string
// ProcessedMenu *WailsMenu
// menuItemMap *MenuItemMap
// menu *menu.Menu
//}
//
//func (t *ContextMenu) AsJSON() (string, error) {
// data, err := json.Marshal(t)
// if err != nil {
// return "", err
// }
// return string(data), nil
//}
//
//func (m *Manager) NewContextMenu(contextMenu *menu.ContextMenu) *ContextMenu {
//
// result := &ContextMenu{
// ID: contextMenu.ID,
// menu: contextMenu.Menu,
// menuItemMap: NewMenuItemMap(),
// }
//
// result.menuItemMap.AddMenu(contextMenu.Menu)
// result.ProcessedMenu = m.NewWailsMenu(result.menuItemMap, result.menu)
//
// return result
//}
//
func (m *Manager) AddContextMenu(contextMenu *menu.ContextMenu) {
newContextMenu := NewContextMenu(contextMenu)
// Save the references
m.contextMenus[contextMenu.ID] = newContextMenu
m.contextMenuPointers[contextMenu] = contextMenu.ID
//
// newContextMenu := m.NewContextMenu(contextMenu)
//
// // Save the references
// m.contextMenus[contextMenu.ID] = newContextMenu
// m.contextMenuPointers[contextMenu] = contextMenu.ID
}
//
func (m *Manager) UpdateContextMenu(contextMenu *menu.ContextMenu) (string, error) {
contextMenuID, contextMenuKnown := m.contextMenuPointers[contextMenu]
if !contextMenuKnown {
return "", fmt.Errorf("unknown Context Menu '%s'. Please add the context menu using AddContextMenu()", contextMenu.ID)
}
// Create the updated context menu
updatedContextMenu := NewContextMenu(contextMenu)
// Save the reference
m.contextMenus[contextMenuID] = updatedContextMenu
return updatedContextMenu.AsJSON()
// contextMenuID, contextMenuKnown := m.contextMenuPointers[contextMenu]
// if !contextMenuKnown {
// return "", fmt.Errorf("unknown Context Menu '%s'. Please add the context menu using AddContextMenu()", contextMenu.ID)
// }
//
// // Create the updated context menu
// updatedContextMenu := m.NewContextMenu(contextMenu)
//
// // Save the reference
// m.contextMenus[contextMenuID] = updatedContextMenu
//
// return updatedContextMenu.AsJSON()
return "", nil
}

View File

@@ -1,75 +0,0 @@
package menumanager
import (
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu"
"sync"
)
// MenuItemMap holds a mapping between menuIDs and menu items
type MenuItemMap struct {
idToMenuItemMap map[string]*menu.MenuItem
menuItemToIDMap map[*menu.MenuItem]string
// We use a simple counter to keep track of unique menu IDs
menuIDCounter int64
menuIDCounterMutex sync.Mutex
}
func NewMenuItemMap() *MenuItemMap {
result := &MenuItemMap{
idToMenuItemMap: make(map[string]*menu.MenuItem),
menuItemToIDMap: make(map[*menu.MenuItem]string),
}
return result
}
func (m *MenuItemMap) AddMenu(menu *menu.Menu) {
if menu == nil {
return
}
for _, item := range menu.Items {
m.processMenuItem(item)
}
}
func (m *MenuItemMap) Dump() {
println("idToMenuItemMap:")
for key, value := range m.idToMenuItemMap {
fmt.Printf(" %s\t%p\n", key, value)
}
println("\nmenuItemToIDMap")
for key, value := range m.menuItemToIDMap {
fmt.Printf(" %p\t%s\n", key, value)
}
}
// GenerateMenuID returns a unique string ID for a menu item
func (m *MenuItemMap) generateMenuID() string {
m.menuIDCounterMutex.Lock()
result := fmt.Sprintf("%d", m.menuIDCounter)
m.menuIDCounter++
m.menuIDCounterMutex.Unlock()
return result
}
func (m *MenuItemMap) processMenuItem(item *menu.MenuItem) {
if item.SubMenu != nil {
for _, submenuitem := range item.SubMenu.Items {
m.processMenuItem(submenuitem)
}
}
// Create a unique ID for this menu item
menuID := m.generateMenuID()
// Store references
m.idToMenuItemMap[menuID] = item
m.menuItemToIDMap[item] = menuID
}
func (m *MenuItemMap) getMenuItemByID(menuId string) *menu.MenuItem {
return m.idToMenuItemMap[menuId]
}

View File

@@ -2,89 +2,53 @@ package menumanager
import (
"fmt"
"github.com/wailsapp/wails/v2/internal/counter"
"github.com/wailsapp/wails/v2/pkg/menu"
)
type Manager struct {
// The application menu.
applicationMenu *menu.Menu
applicationMenuJSON string
// MenuItemMap is a map of all menu items against a generated ID
menuItemMap map[string]*menu.MenuItem
menuItemIDCounter *counter.Counter
processedMenuItems map[*menu.MenuItem]*ProcessedMenuItem
// Our application menu mappings
applicationMenuItemMap *MenuItemMap
// Menus
menuIDCounter *counter.Counter
// Context menus
contextMenus map[string]*ContextMenu
contextMenuPointers map[*menu.ContextMenu]string
// Tray menu stores
trayMenus map[string]*TrayMenu
trayMenuPointers map[*menu.TrayMenu]string
// Map wails menus to internal menus
trayMenuMap map[*menu.TrayMenu]*TrayMenu
trayMenuIDCounter *counter.Counter
}
func NewManager() *Manager {
return &Manager{
applicationMenuItemMap: NewMenuItemMap(),
contextMenus: make(map[string]*ContextMenu),
contextMenuPointers: make(map[*menu.ContextMenu]string),
trayMenus: make(map[string]*TrayMenu),
trayMenuPointers: make(map[*menu.TrayMenu]string),
trayMenuMap: make(map[*menu.TrayMenu]*TrayMenu),
trayMenuIDCounter: counter.NewCounter(0),
menuIDCounter: counter.NewCounter(0),
menuItemMap: make(map[string]*menu.MenuItem),
menuItemIDCounter: counter.NewCounter(0),
processedMenuItems: make(map[*menu.MenuItem]*ProcessedMenuItem),
}
}
func (m *Manager) getMenuItemByID(menuMap *MenuItemMap, menuId string) *menu.MenuItem {
return menuMap.idToMenuItemMap[menuId]
}
func (m *Manager) ProcessClick(menuID string, data string, menuType string, parentID string) error {
var menuItemMap *MenuItemMap
switch menuType {
case "ApplicationMenu":
menuItemMap = m.applicationMenuItemMap
case "ContextMenu":
contextMenu := m.contextMenus[parentID]
if contextMenu == nil {
return fmt.Errorf("unknown context menu: %s", parentID)
}
menuItemMap = contextMenu.menuItemMap
case "TrayMenu":
trayMenu := m.trayMenus[parentID]
if trayMenu == nil {
return fmt.Errorf("unknown tray menu: %s", parentID)
}
menuItemMap = trayMenu.menuItemMap
default:
return fmt.Errorf("unknown menutype: %s", menuType)
}
// Get the menu item
menuItem := menuItemMap.getMenuItemByID(menuID)
func (m *Manager) ProcessClick(menuID string, data string) error {
// Get item from callback map
menuItem := m.menuItemMap[menuID]
if menuItem == nil {
return fmt.Errorf("Cannot process menuid %s - unknown", menuID)
}
// Is the menu item a checkbox?
if menuItem.Type == menu.CheckboxType {
// Toggle state
menuItem.Checked = !menuItem.Checked
return fmt.Errorf("menuItem doesn't exist")
}
if menuItem.Click == nil {
// No callback
return fmt.Errorf("No callback for menu '%s'", menuItem.Label)
return fmt.Errorf("menuItem 'does not have a callback")
}
// Create new Callback struct
callbackData := &menu.CallbackData{
MenuItem: menuItem,
ContextData: data,
}
// Call back!
// Callback!
go menuItem.Click(callbackData)
return nil
}

View File

@@ -2,88 +2,165 @@ package menumanager
import (
"encoding/json"
"fmt"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/menu/keys"
)
type ProcessedMenuItem struct {
ID string
// Label is what appears as the menu text
Label string `json:",omitempty"`
// Role is a predefined menu type
Role menu.Role `json:",omitempty"`
// Accelerator holds a representation of a key binding
Accelerator *keys.Accelerator `json:",omitempty"`
// Type of MenuItem, EG: Checkbox, Text, Separator, Radio, Submenu
Type menu.Type
// Disabled makes the item unselectable
Disabled bool `json:",omitempty"`
// Hidden ensures that the item is not shown in the menu
Hidden bool `json:",omitempty"`
// Checked indicates if the item is selected (used by Checkbox and Radio types only)
Checked bool `json:",omitempty"`
// Submenu contains a list of menu items that will be shown as a submenu
//SubMenu []*MenuItem `json:"SubMenu,omitempty"`
SubMenu *ProcessedMenu `json:",omitempty"`
type ProcessedMenu struct {
ID string `json:"I"`
// Colour
RGBA string `json:",omitempty"`
// Font
FontSize int `json:",omitempty"`
FontName string `json:",omitempty"`
// Image - base64 image data
Image string `json:",omitempty"`
MacTemplateImage bool `json:", omitempty"`
MacAlternate bool `json:", omitempty"`
// Tooltip
Tooltip string `json:",omitempty"`
Items []*ProcessedMenuItem `json:"i,omitempty"`
RadioGroups []*RadioGroup `json:"r,omitempty"`
}
func NewProcessedMenuItem(menuItemMap *MenuItemMap, menuItem *menu.MenuItem) *ProcessedMenuItem {
func (m *Manager) ProcessMenu(menu *menu.Menu) *ProcessedMenu {
wm := m.NewWailsMenu(menu)
if len(wm.Menu) == 0 {
return nil
}
return &ProcessedMenu{
ID: m.generateMenuID(),
Items: wm.Menu,
RadioGroups: wm.RadioGroups,
}
}
type ProcessedMenuItem struct {
// ID of the menu item
ID string `json:"I"`
// Label is what appears as the menu text
Label string `json:"l,omitempty"`
// AlternateLabel is a secondary label (Used by Mac)
AlternateLabel string `json:"L,omitempty"`
// Role is a predefined menu type
Role menu.Role `json:"r,omitempty"`
// Accelerator holds a representation of a key binding
Accelerator *keys.Accelerator `json:"a,omitempty"`
// Type of MenuItem, EG: Checkbox, Text, Separator, Radio, Submenu
Type menu.Type `json:"t,omitempty"`
// Font to use for the menu item
Font string `json:"f,omitempty"`
// Font to use for the menu item
FontSize int `json:"F,omitempty"`
// RGBA is the colour of the menu item
RGBA string `json:"R,omitempty"`
// Image is an image for the menu item (base64 string)
Image string `json:"i,omitempty"`
// Disabled makes the item unselectable
Disabled *bool `json:"d,omitempty"`
// Hidden ensures that the item is not shown in the menu
Hidden *bool `json:"h,omitempty"`
// Checked indicates if the item is selected (used by Checkbox and Radio types only)
Checked *bool `json:"c,omitempty"`
// Submenu contains a list of menu items that will be shown as a submenu
//SubMenu []*MenuItem `json:"SubMenu,omitempty"`
SubMenu []*ProcessedMenuItem `json:"s,omitempty"`
// Indicates if this item has a callback
HasCallback *bool `json:"C,omitempty"`
}
func (m *Manager) generateMenuItemID() string {
return fmt.Sprintf("%d", m.menuItemIDCounter.Increment())
}
func (m *Manager) generateMenuID() string {
return fmt.Sprintf("%d", m.menuIDCounter.Increment())
}
func (m *Manager) NewProcessedMenuItem(menuItem *menu.MenuItem) *ProcessedMenuItem {
// Check if this menu has already been processed.
// This is to prevent duplicates.
existingMenuItem := m.processedMenuItems[menuItem]
if existingMenuItem != nil {
return &ProcessedMenuItem{ID: existingMenuItem.ID}
}
ID := m.generateMenuItemID()
ID := menuItemMap.menuItemToIDMap[menuItem]
result := &ProcessedMenuItem{
ID: ID,
Label: menuItem.Label,
Role: menuItem.Role,
Accelerator: menuItem.Accelerator,
Type: menuItem.Type,
Disabled: menuItem.Disabled,
Hidden: menuItem.Hidden,
Checked: menuItem.Checked,
SubMenu: nil,
RGBA: menuItem.RGBA,
FontSize: menuItem.FontSize,
FontName: menuItem.FontName,
Image: menuItem.Image,
MacTemplateImage: menuItem.MacTemplateImage,
MacAlternate: menuItem.MacAlternate,
Tooltip: menuItem.Tooltip,
ID: ID,
Label: menuItem.Label,
AlternateLabel: menuItem.AlternateLabel,
Role: menuItem.Role,
Accelerator: menuItem.Accelerator,
Type: menuItem.Type,
Font: menuItem.Font,
FontSize: menuItem.FontSize,
RGBA: menuItem.RGBA,
Image: menuItem.Image,
Disabled: nil,
Hidden: nil,
Checked: nil,
SubMenu: nil,
HasCallback: nil,
}
if menuItem.Hidden {
result.Hidden = new(bool)
*result.Hidden = true
}
if menuItem.Disabled {
result.Disabled = new(bool)
*result.Disabled = true
}
if menuItem.Checked {
result.Checked = new(bool)
*result.Checked = true
}
if menuItem.Click != nil {
result.HasCallback = new(bool)
*result.HasCallback = true
}
if menuItem.SubMenu != nil {
result.SubMenu = NewProcessedMenu(menuItemMap, menuItem.SubMenu)
result.SubMenu = m.NewProcessedMenu(menuItem.SubMenu)
}
// Add menu item to item map
m.menuItemMap[ID] = menuItem
// Add processed Item to processedMenuItems
m.processedMenuItems[menuItem] = result
return result
}
type ProcessedMenu struct {
Items []*ProcessedMenuItem
}
func (m *Manager) NewProcessedMenu(menu *menu.Menu) []*ProcessedMenuItem {
func NewProcessedMenu(menuItemMap *MenuItemMap, menu *menu.Menu) *ProcessedMenu {
if menu == nil {
return nil
}
result := &ProcessedMenu{}
if menu != nil {
for _, item := range menu.Items {
processedMenuItem := NewProcessedMenuItem(menuItemMap, item)
result.Items = append(result.Items, processedMenuItem)
}
if menu.Items == nil {
return nil
}
var result []*ProcessedMenuItem
for _, item := range menu.Items {
processedMenuItem := m.NewProcessedMenuItem(item)
result = append(result, processedMenuItem)
}
return result
@@ -92,8 +169,8 @@ func NewProcessedMenu(menuItemMap *MenuItemMap, menu *menu.Menu) *ProcessedMenu
// WailsMenu is the original menu with the addition
// of radio groups extracted from the menu data
type WailsMenu struct {
Menu *ProcessedMenu
RadioGroups []*RadioGroup
Menu []*ProcessedMenuItem `json:",omitempty"`
RadioGroups []*RadioGroup `json:",omitempty"`
currentRadioGroup []string
}
@@ -103,11 +180,12 @@ type RadioGroup struct {
Length int
}
func NewWailsMenu(menuItemMap *MenuItemMap, menu *menu.Menu) *WailsMenu {
func (m *Manager) NewWailsMenu(menu *menu.Menu) *WailsMenu {
result := &WailsMenu{}
// Process the menus
result.Menu = NewProcessedMenu(menuItemMap, menu)
result.Menu = m.NewProcessedMenu(menu)
// Process the radio groups
result.processRadioGroups()
@@ -124,17 +202,7 @@ func (w *WailsMenu) AsJSON() (string, error) {
return string(menuAsJSON), nil
}
func (w *WailsMenu) processRadioGroups() {
// Loop over top level menus
for _, item := range w.Menu.Items {
// Process MenuItem
w.processMenuItem(item)
}
w.finaliseRadioGroup()
}
func (w *WailsMenu) processMenuItem(item *ProcessedMenuItem) {
func (w *WailsMenu) processMenuItemForRadioGroups(item *ProcessedMenuItem) {
switch item.Type {
@@ -145,8 +213,8 @@ func (w *WailsMenu) processMenuItem(item *ProcessedMenuItem) {
w.finaliseRadioGroup()
// Process each submenu item
for _, subitem := range item.SubMenu.Items {
w.processMenuItem(subitem)
for _, subitem := range item.SubMenu {
w.processMenuItemForRadioGroups(subitem)
}
case menu.RadioType:
// Add the item to the radio group
@@ -156,6 +224,20 @@ func (w *WailsMenu) processMenuItem(item *ProcessedMenuItem) {
}
}
func (w *WailsMenu) processRadioGroups() {
if w.Menu == nil {
return
}
// Loop over top level menus
for _, item := range w.Menu {
// Process MenuItem
w.processMenuItemForRadioGroups(item)
}
w.finaliseRadioGroup()
}
func (w *WailsMenu) finaliseRadioGroup() {
// If we were processing a radio group, fix up the references

View File

@@ -3,39 +3,14 @@ package menumanager
import (
"encoding/json"
"fmt"
"strconv"
"sync"
"github.com/pkg/errors"
"github.com/wailsapp/wails/v2/pkg/menu"
)
var trayMenuID int
var trayMenuIDMutex sync.Mutex
func generateTrayID() string {
var idStr string
trayMenuIDMutex.Lock()
idStr = strconv.Itoa(trayMenuID)
trayMenuID++
trayMenuIDMutex.Unlock()
return idStr
}
type TrayMenu struct {
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
ID string `json:"I"`
Label string `json:"l,omitempty"`
Icon string `json:"i,omitempty"`
Menu *ProcessedMenu `json:"m,omitempty"`
}
func (t *TrayMenu) AsJSON() (string, error) {
@@ -46,92 +21,44 @@ func (t *TrayMenu) AsJSON() (string, error) {
return string(data), nil
}
func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu {
func (m *Manager) newTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu {
result := &TrayMenu{
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 := TrayMenu{
ID: m.generateTrayID(),
Label: trayMenu.Label,
Icon: trayMenu.Icon,
Menu: m.ProcessMenu(trayMenu.Menu),
}
result.menuItemMap.AddMenu(trayMenu.Menu)
result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu)
return &result
}
func (m *Manager) AddTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu {
result := m.newTrayMenu(trayMenu)
// Add to map
m.trayMenuMap[trayMenu] = result
return result
}
func (m *Manager) OnTrayMenuOpen(id string) {
trayMenu, ok := m.trayMenus[id]
if !ok {
return
}
if trayMenu.trayMenu.OnOpen == nil {
return
}
go trayMenu.trayMenu.OnOpen()
func (m *Manager) generateTrayID() string {
return fmt.Sprintf("T%d", m.trayMenuIDCounter.Increment())
}
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)
// Hook up a new ID
trayID := generateTrayID()
newTrayMenu.ID = trayID
// Save the references
m.trayMenus[trayID] = newTrayMenu
m.trayMenuPointers[trayMenu] = trayID
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]
if !trayMenuKnown {
return m.AddTrayMenu(trayMenu)
func (m *Manager) GetTrayMenus() ([]*TrayMenu, error) {
var result []*TrayMenu
for _, trayMenu := range m.trayMenuMap {
result = append(result, trayMenu)
}
// Create the updated tray menu
updatedTrayMenu := NewTrayMenu(trayMenu)
updatedTrayMenu.ID = trayID
// Save the reference
m.trayMenus[trayID] = updatedTrayMenu
return updatedTrayMenu.AsJSON()
return result, nil
}
func (m *Manager) GetTrayMenus() ([]string, error) {
result := []string{}
for _, trayMenu := range m.trayMenus {
func (m *Manager) GetTrayMenusAsJSON() ([]string, error) {
var result []string
for _, trayMenu := range m.trayMenuMap {
JSON, err := trayMenu.AsJSON()
if err != nil {
return nil, err
@@ -142,54 +69,11 @@ func (m *Manager) GetTrayMenus() ([]string, error) {
return result, nil
}
// SetTrayMenu updates or creates a menu
func (m *Manager) SetTrayMenu(trayMenu *menu.TrayMenu) (string, error) {
return "", nil
}
func (m *Manager) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) (string, error) {
trayID, trayMenuKnown := m.trayMenuPointers[trayMenu]
if !trayMenuKnown {
return "", fmt.Errorf("[UpdateTrayMenuLabel] unknown tray id for tray %s", trayMenu.Label)
}
type LabelUpdate struct {
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,
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)
if err != nil {
return "", errors.Wrap(err, "[UpdateTrayMenuLabel] ")
}
return string(data), nil
}
func (m *Manager) GetContextMenus() ([]string, error) {
result := []string{}
for _, contextMenu := range m.contextMenus {
JSON, err := contextMenu.AsJSON()
if err != nil {
return nil, err
}
result = append(result, JSON)
}
return result, nil
return "", nil
}

View File

@@ -0,0 +1,99 @@
package menumanager
import (
"github.com/matryer/is"
"github.com/wailsapp/wails/v2/pkg/menu"
"testing"
)
func TestManager_AddTrayMenu(t *testing.T) {
is := is.New(t)
simpleLabel := menu.Text("test", nil, nil)
checkbox := menu.Checkbox("test", true, nil, nil)
radioGroup1 := menu.Radio("option 1", true, nil, nil)
radioGroup2 := menu.Radio("option 2", false, nil, nil)
radioGroup3 := menu.Radio("option 3", false, nil, nil)
callback := menu.Text("Preferences", nil, func(_ *menu.CallbackData) {})
empty := &menu.TrayMenu{}
labelOnly := &menu.TrayMenu{Label: "test"}
iconOnly := &menu.TrayMenu{Icon: "svelte"}
iconLabel := &menu.TrayMenu{Icon: "svelte", Label: "test"}
blankLabel := &menu.TrayMenu{Label: ""}
blankMenu := &menu.TrayMenu{Menu: menu.NewMenu()}
menuTextItem := &menu.TrayMenu{Menu: menu.NewMenuFromItems(simpleLabel)}
checkboxItem := &menu.TrayMenu{Menu: menu.NewMenuFromItems(checkbox)}
radioGroupItems := &menu.TrayMenu{Menu: menu.NewMenuFromItems(radioGroup1, radioGroup2, radioGroup3)}
callbackItem := &menu.TrayMenu{Menu: menu.NewMenuFromItems(callback)}
tests := []struct {
trayMenu *menu.TrayMenu
want string
}{
{empty, "{\"I\":\"T1\"}"},
{labelOnly, "{\"I\":\"T1\",\"l\":\"test\"}"},
{iconOnly, "{\"I\":\"T1\",\"i\":\"svelte\"}"},
{iconLabel, "{\"I\":\"T1\",\"l\":\"test\",\"i\":\"svelte\"}"},
{blankLabel, "{\"I\":\"T1\"}"},
{blankMenu, "{\"I\":\"T1\"}"},
{menuTextItem, "{\"I\":\"T1\",\"m\":{\"I\":\"1\",\"i\":[{\"I\":\"1\",\"l\":\"test\",\"t\":\"t\"}]}}"},
{checkboxItem, "{\"I\":\"T1\",\"m\":{\"I\":\"1\",\"i\":[{\"I\":\"1\",\"l\":\"test\",\"t\":\"c\",\"c\":true}]}}"},
{radioGroupItems, "{\"I\":\"T1\",\"m\":[{\"I\":\"1\",\"l\":\"option 1\",\"t\":\"r\",\"c\":true},{\"I\":\"2\",\"l\":\"option 2\",\"t\":\"r\"},{\"I\":\"3\",\"l\":\"option 3\",\"t\":\"r\"}],\"r\":[{\"Members\":[\"1\",\"2\",\"3\"],\"Length\":3}]}"},
{callbackItem, "{\"I\":\"T1\",\"m\":[{\"I\":\"1\",\"l\":\"Preferences\",\"t\":\"t\",\"C\":true}]}"},
}
for _, tt := range tests {
m := NewManager()
got := m.AddTrayMenu(tt.trayMenu)
JSON, err := got.AsJSON()
is.NoErr(err)
is.Equal(JSON, tt.want)
}
}
func TestManager_CallbackMap(t *testing.T) {
is := is.New(t)
simpleLabel := menu.Text("test", nil, nil)
simpleLabelWithCallback := menu.Text("test", nil, func(_ *menu.CallbackData) {})
checkboxWithCallback := menu.Checkbox("test", true, nil, func(_ *menu.CallbackData) {})
submenu := menu.SubMenu("test", menu.NewMenuFromItems(checkboxWithCallback))
blankMenu := &menu.TrayMenu{Menu: menu.NewMenu()}
noCallback := &menu.TrayMenu{Menu: menu.NewMenuFromItems(simpleLabel)}
oneMenuWithCallback := &menu.TrayMenu{Menu: menu.NewMenuFromItems(simpleLabelWithCallback)}
duplicateCallbacks := &menu.TrayMenu{Menu: menu.NewMenuFromItems(simpleLabelWithCallback, simpleLabelWithCallback)}
twoMenusWithCallbacks := &menu.TrayMenu{Menu: menu.NewMenuFromItems(simpleLabelWithCallback, checkboxWithCallback)}
duplicateMenusWithCallbacks := &menu.TrayMenu{Menu: menu.NewMenuFromItems(simpleLabelWithCallback, checkboxWithCallback, simpleLabelWithCallback, checkboxWithCallback)}
submenuWithCallback := &menu.TrayMenu{Menu: menu.NewMenuFromItems(submenu)}
duplicateSubmenus := &menu.TrayMenu{Menu: menu.NewMenuFromItems(submenu, submenu)}
tests := []struct {
trayMenu *menu.TrayMenu
trays int
menuItems int
JSON string
}{
{blankMenu, 1, 0, "{\"I\":\"T1\"}"},
{noCallback, 1, 1, "{\"I\":\"T1\",\"m\":[{\"I\":\"1\",\"l\":\"test\",\"t\":\"t\"}]}"},
{oneMenuWithCallback, 1, 1, "{\"I\":\"T1\",\"m\":[{\"I\":\"1\",\"l\":\"test\",\"t\":\"t\",\"C\":true}]}"},
{duplicateCallbacks, 1, 1, "{\"I\":\"T1\",\"m\":[{\"I\":\"1\",\"l\":\"test\",\"t\":\"t\",\"C\":true},{\"I\":\"1\"}]}"},
{twoMenusWithCallbacks, 1, 2, "{\"I\":\"T1\",\"m\":[{\"I\":\"1\",\"l\":\"test\",\"t\":\"t\",\"C\":true},{\"I\":\"2\",\"l\":\"test\",\"t\":\"c\",\"c\":true,\"C\":true}]}"},
{duplicateMenusWithCallbacks, 1, 2, "{\"I\":\"T1\",\"m\":[{\"I\":\"1\",\"l\":\"test\",\"t\":\"t\",\"C\":true},{\"I\":\"2\",\"l\":\"test\",\"t\":\"c\",\"c\":true,\"C\":true},{\"I\":\"1\"},{\"I\":\"2\"}]}"},
{submenuWithCallback, 1, 2, "{\"I\":\"T1\",\"m\":[{\"I\":\"1\",\"l\":\"test\",\"t\":\"S\",\"s\":[{\"I\":\"2\",\"l\":\"test\",\"t\":\"c\",\"c\":true,\"C\":true}]}]}"},
{duplicateSubmenus, 1, 2, "{\"I\":\"T1\",\"m\":[{\"I\":\"1\",\"l\":\"test\",\"t\":\"S\",\"s\":[{\"I\":\"2\",\"l\":\"test\",\"t\":\"c\",\"c\":true,\"C\":true}]},{\"I\":\"1\"}]}"},
}
for _, test := range tests {
m := NewManager()
tm := m.AddTrayMenu(test.trayMenu)
is.Equal(len(m.trayMenuMap), test.trays)
is.Equal(len(m.menuItemMap), test.menuItems)
JSON, err := tm.AsJSON()
is.NoErr(err)
is.Equal(JSON, test.JSON)
}
}

View File

@@ -2,7 +2,6 @@ package messagedispatcher
import (
"fmt"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
"github.com/wailsapp/wails/v2/internal/servicebus"
@@ -27,8 +26,6 @@ type Client interface {
WindowUnminimise()
WindowPosition(x int, y int)
WindowSize(width int, height int)
WindowSetMinSize(width int, height int)
WindowSetMaxSize(width int, height int)
WindowFullscreen()
WindowUnFullscreen()
WindowSetColour(colour int)
@@ -37,7 +34,6 @@ 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

@@ -27,7 +27,7 @@ func dialogMessageParser(message string) (*parsedMessage, error) {
if idx < 0 {
return nil, fmt.Errorf("Invalid dialog response message format: %+v", message)
}
callbackID := message[:idx]
callbackID := message[:idx+1]
payloadData := message[idx+1:]
switch dialogType {

View File

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

View File

@@ -1,20 +0,0 @@
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

@@ -1,14 +1,12 @@
package messagedispatcher
import (
"context"
"encoding/json"
"github.com/wailsapp/wails/v2/pkg/options/dialog"
"strconv"
"strings"
"sync"
"github.com/wailsapp/wails/v2/pkg/options/dialog"
"github.com/wailsapp/wails/v2/internal/crypto"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
@@ -25,6 +23,7 @@ type Dispatcher struct {
dialogChannel <-chan *servicebus.Message
systemChannel <-chan *servicebus.Message
menuChannel <-chan *servicebus.Message
running bool
servicebus *servicebus.ServiceBus
logger logger.CustomLogger
@@ -32,13 +31,6 @@ type Dispatcher struct {
// Clients
clients map[string]*DispatchClient
lock sync.RWMutex
// Context for cancellation
ctx context.Context
cancel context.CancelFunc
// internal wait group
wg sync.WaitGroup
}
// New dispatcher. Needs a service bus to send to.
@@ -83,9 +75,6 @@ func New(servicebus *servicebus.ServiceBus, logger *logger.Logger) (*Dispatcher,
return nil, err
}
// Create context
ctx, cancel := context.WithCancel(context.Background())
result := &Dispatcher{
servicebus: servicebus,
eventChannel: eventChannel,
@@ -97,8 +86,6 @@ func New(servicebus *servicebus.ServiceBus, logger *logger.Logger) (*Dispatcher,
dialogChannel: dialogChannel,
systemChannel: systemChannel,
menuChannel: menuChannel,
ctx: ctx,
cancel: cancel,
}
return result, nil
@@ -109,18 +96,15 @@ func (d *Dispatcher) Start() error {
d.logger.Trace("Starting")
d.wg.Add(1)
d.running = true
// Spin off a go routine
go func() {
defer d.logger.Trace("Shutdown")
for {
for d.running {
select {
case <-d.ctx.Done():
d.wg.Done()
return
case <-d.quitChannel:
d.processQuit()
d.running = false
case resultMessage := <-d.resultChannel:
d.processCallResult(resultMessage)
case eventMessage := <-d.eventChannel:
@@ -135,6 +119,9 @@ func (d *Dispatcher) Start() error {
d.processMenuMessage(menuMessage)
}
}
// Call shutdown
d.shutdown()
}()
return nil
@@ -148,6 +135,10 @@ func (d *Dispatcher) processQuit() {
}
}
func (d *Dispatcher) shutdown() {
d.logger.Trace("Shutdown")
}
// RegisterClient will register the given callback with the dispatcher
// and return a DispatchClient that the caller can use to send messages
func (d *Dispatcher) RegisterClient(client Client) *DispatchClient {
@@ -358,38 +349,6 @@ func (d *Dispatcher) processWindowMessage(result *servicebus.Message) {
for _, client := range d.clients {
client.frontend.WindowSize(w, h)
}
case "minsize":
// We need 2 arguments
if len(splitTopic) != 4 {
d.logger.Error("Invalid number of parameters for 'window:minsize' : %#v", result.Data())
return
}
w, err1 := strconv.Atoi(splitTopic[2])
h, err2 := strconv.Atoi(splitTopic[3])
if err1 != nil || err2 != nil {
d.logger.Error("Invalid integer parameters for 'window:minsize' : %#v", result.Data())
return
}
// Notifh clients
for _, client := range d.clients {
client.frontend.WindowSetMinSize(w, h)
}
case "maxsize":
// We need 2 arguments
if len(splitTopic) != 4 {
d.logger.Error("Invalid number of parameters for 'window:maxsize' : %#v", result.Data())
return
}
w, err1 := strconv.Atoi(splitTopic[2])
h, err2 := strconv.Atoi(splitTopic[3])
if err1 != nil || err2 != nil {
d.logger.Error("Invalid integer parameters for 'window:maxsize' : %#v", result.Data())
return
}
// Notifh clients
for _, client := range d.clients {
client.frontend.WindowSetMaxSize(w, h)
}
default:
d.logger.Error("Unknown window command: %s", command)
}
@@ -527,24 +486,8 @@ 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)
}
}
func (d *Dispatcher) Close() {
d.cancel()
d.wg.Wait()
}

View File

@@ -1,7 +1,6 @@
package process
import (
"os"
"os/exec"
"github.com/wailsapp/wails/v2/pkg/clilogger"
@@ -17,19 +16,17 @@ type Process struct {
// NewProcess creates a new process struct
func NewProcess(logger *clilogger.CLILogger, cmd string, args ...string) *Process {
result := &Process{
return &Process{
logger: logger,
cmd: exec.Command(cmd, args...),
exitChannel: make(chan bool, 1),
}
result.cmd.Stdout = os.Stdout
result.cmd.Stderr = os.Stderr
return result
}
// Start the process
func (p *Process) Start() error {
p.cmd.
err := p.cmd.Start()
if err != nil {
return err
@@ -39,12 +36,7 @@ func (p *Process) Start() error {
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 {
if err.Error() != "signal: killed" {
logger.Fatal("Fatal error from app: " + err.Error())
}
}
cmd.Wait()
logger.Println("Exiting process (PID: %d)", cmd.Process.Pid)
*running = false
exitChannel <- true

File diff suppressed because one or more lines are too long

View File

@@ -65,7 +65,7 @@ export function SetBindings(bindingsMap) {
window.backend[packageName][structName][methodName] = function () {
// No timeout by default
let timeout = 0;
var timeout = 0;
// Actual function
function dynamic() {
@@ -89,3 +89,19 @@ export function SetBindings(bindingsMap) {
});
});
}
// /**
// * Determines if the given identifier is valid Javascript
// *
// * @param {boolean} name
// * @returns
// */
// function isValidIdentifier(name) {
// // Don't xss yourself :-)
// try {
// new Function('var ' + name);
// return true;
// } catch (e) {
// return false;
// }
// }

View File

@@ -10,7 +10,6 @@ The lightweight framework for web-like apps
/* jshint esversion: 6 */
import { SetBindings } from './bindings';
import { Init } from './main';
import {RaiseError} from '../desktop/darwin';
// Setup global error handler
window.onerror = function (msg, url, lineNo, columnNo, error) {
@@ -22,7 +21,7 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
error: JSON.stringify(error),
stack: function() { return JSON.stringify(new Error().stack); }(),
};
RaiseError(errorMessage);
window.wails.Log.Error(JSON.stringify(errorMessage));
};
// Initialise the Runtime

View File

@@ -13,7 +13,6 @@ import { Error } from './log';
import { SendMessage } from 'ipc';
// Defines a single listener with a maximum number of times to callback
/**
* The Listener class defines a listener! :-)
*
@@ -44,7 +43,7 @@ class Listener {
}
}
let eventListeners = {};
var eventListeners = {};
/**
* Registers an event listener that will be invoked `maxCallbacks` times before being destroyed
@@ -97,7 +96,7 @@ export function Once(eventName, callback) {
function notifyListeners(eventData) {
// Get the event name
let eventName = eventData.name;
var eventName = eventData.name;
// Check if we have any listeners for this event
if (eventListeners[eventName]) {
@@ -111,7 +110,7 @@ function notifyListeners(eventData) {
// Get next listener
const listener = eventListeners[eventName][count];
let data = eventData.data;
var data = eventData.data;
// Do the callback
const destroy = listener.Callback(data);
@@ -121,7 +120,7 @@ function notifyListeners(eventData) {
}
}
// Update callbacks with new list of listeners
// Update callbacks with new list of listners
eventListeners[eventName] = newEventListenerList;
}
}

View File

@@ -62,4 +62,6 @@ export function Init() {
// Do platform specific Init
Platform.Init();
window.wailsloader.runtime = true;
}

View File

@@ -25,10 +25,6 @@ export function SendMessage(message) {
window.webkit.messageHandlers.external.postMessage(message);
}
export function RaiseError(message) {
window.webkit.messageHandlers.error.postMessage(message);
}
export function Init() {
// Setup drag handler

View File

@@ -27,7 +27,7 @@ export function Init() {
// Setup drag handler
// Based on code from: https://github.com/patr0nus/DeskGap
window.addEventListener('mousedown', function (e) {
let currentElement = e.target;
var currentElement = e.target;
while (currentElement != null) {
if (currentElement.hasAttribute('data-wails-no-drag')) {
break;

Some files were not shown because too many files have changed in this diff Show More