mirror of
https://github.com/taigrr/wails.git
synced 2026-04-04 14:12:40 -07:00
Compare commits
99 Commits
v2.0.0-alp
...
feature/v2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d1447d558 | ||
|
|
7c22cbcf38 | ||
|
|
e4b03f510b | ||
|
|
8dfd206aa9 | ||
|
|
6dabab1d2e | ||
|
|
c3bd8b1a85 | ||
|
|
e1b7332c47 | ||
|
|
5cd08e45f0 | ||
|
|
c2d03f0e6e | ||
|
|
d3501f4cb7 | ||
|
|
ee82cd25b7 | ||
|
|
bbb07e17d9 | ||
|
|
e6b40b55c4 | ||
|
|
7573f68df3 | ||
|
|
ceaacc7ba9 | ||
|
|
0e24f75753 | ||
|
|
82b9deeee4 | ||
|
|
cfa40b797f | ||
|
|
5aeb68acb7 | ||
|
|
b81101414f | ||
|
|
7ae89d04bb | ||
|
|
1c566f3802 | ||
|
|
c9c3c9ab90 | ||
|
|
56394ac50e | ||
|
|
f7c2f12ab2 | ||
|
|
a6bb6e0c93 | ||
|
|
4a5863e6ac | ||
|
|
913fe8d184 | ||
|
|
4ce8130cdf | ||
|
|
fe87463b78 | ||
|
|
fe0f0e29e8 | ||
|
|
83d6dac7cf | ||
|
|
02500e0930 | ||
|
|
5e1187f437 | ||
|
|
064ff3b65e | ||
|
|
b5c7019bf0 | ||
|
|
e9d16e77a3 | ||
|
|
2415d4c531 | ||
|
|
3f75213ce3 | ||
|
|
6120ceabf1 | ||
|
|
95a95d1750 | ||
|
|
d923e84456 | ||
|
|
343f573e78 | ||
|
|
c6d87da4f0 | ||
|
|
a9faebe51a | ||
|
|
d436f5d1be | ||
|
|
f40899821f | ||
|
|
2a64ed19a3 | ||
|
|
47bca0be88 | ||
|
|
7ac8cc6b8b | ||
|
|
b435ec1217 | ||
|
|
688d4fee6a | ||
|
|
29ffeaa9f3 | ||
|
|
742e4ba2cb | ||
|
|
0a0063de1f | ||
|
|
1b7d1e61cc | ||
|
|
15a273458e | ||
|
|
eef8eb756f | ||
|
|
e65118e962 | ||
|
|
de06fc7dcc | ||
|
|
a86fbbb440 | ||
|
|
3045ec107f | ||
|
|
3a9557ad30 | ||
|
|
583153383a | ||
|
|
3f53e8fd5f | ||
|
|
5c9402323a | ||
|
|
1921862b53 | ||
|
|
0f7acd39fc | ||
|
|
1a7507f524 | ||
|
|
db6dde3e50 | ||
|
|
4e58b7697a | ||
|
|
55d7d9693f | ||
|
|
b4b7c9d306 | ||
|
|
a4153fae57 | ||
|
|
8053357d99 | ||
|
|
7347d2caa2 | ||
|
|
e6491bcbb7 | ||
|
|
26a291dbee | ||
|
|
8ee8c9b07c | ||
|
|
3a2d01813a | ||
|
|
d2dadc386f | ||
|
|
faa8f02b08 | ||
|
|
fbee9ba240 | ||
|
|
2a69786d7e | ||
|
|
f460bf91ef | ||
|
|
bd74d45a91 | ||
|
|
c65522f0b6 | ||
|
|
5f2c437136 | ||
|
|
87e974e080 | ||
|
|
f77729fc0b | ||
|
|
2a8ce96830 | ||
|
|
9be539cfb8 | ||
|
|
e44f2fe06d | ||
|
|
ad65d55abd | ||
|
|
2b5bbfd897 | ||
|
|
d2020fedda | ||
|
|
98789bd85a | ||
|
|
e6ace2fafd | ||
|
|
a55fc4d0e9 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -22,8 +22,6 @@ v2/test/**/frontend/dist
|
||||
v2/test/**/build/
|
||||
v2/test/frameless/icon.png
|
||||
v2/test/hidden/icon.png
|
||||
v2/internal/ffenestri/runtime.c
|
||||
v2/internal/runtime/assets/desktop.js
|
||||
v2/test/kitchensink/frontend/public/bundle.*
|
||||
v2/pkg/parser/testproject/frontend/wails
|
||||
v2/test/kitchensink/frontend/public
|
||||
|
||||
@@ -147,7 +147,12 @@ This project was mainly coded to the following albums:
|
||||
|
||||
[](https://app.fossa.io/projects/git%2Bgithub.com%2Fwailsapp%2Fwails?ref=badge_large)
|
||||
|
||||
## Special Thank You
|
||||
## Special Thanks
|
||||
|
||||
<p align="center" style="text-align: center">
|
||||
A *huge* thanks to <a href="https://pace.dev"><img src="pace.jpeg"/> Pace</a> for sponsoring the project and helping the efforts to get Wails ported to Apple Silicon!<br/><br/>
|
||||
If you are looking for a Project Management tool that's powerful but quick and easy to use, check them out!<br/><br/>
|
||||
</p>
|
||||
|
||||
<p align="center" style="text-align: center">
|
||||
A special thank you to JetBrains for donating licenses to us!<br/><br/>
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -26,7 +26,7 @@ export function OpenURL(url) {
|
||||
* Opens the given filename using the system's default file handler
|
||||
*
|
||||
* @export
|
||||
* @param {sting} filename
|
||||
* @param {string} filename
|
||||
* @returns
|
||||
*/
|
||||
export function OpenFile(filename) {
|
||||
|
||||
@@ -62,7 +62,7 @@ if (window.crypto) {
|
||||
export function Call(bindingName, data, timeout) {
|
||||
|
||||
// Timeout infinite by default
|
||||
if (timeout == null || timeout == undefined) {
|
||||
if (timeout == null) {
|
||||
timeout = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ function Invoke(message) {
|
||||
*
|
||||
* @export
|
||||
* @param {string} type
|
||||
* @param {string} payload
|
||||
* @param {Object} payload
|
||||
* @param {string=} callbackID
|
||||
*/
|
||||
export function SendMessage(type, payload, callbackID) {
|
||||
|
||||
123
v2/cmd/wails/internal/commands/debug/debug.go
Normal file
123
v2/cmd/wails/internal/commands/debug/debug.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/wailsapp/wails/v2/internal/shell"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/leaanthony/clir"
|
||||
"github.com/leaanthony/slicer"
|
||||
"github.com/wailsapp/wails/v2/pkg/clilogger"
|
||||
"github.com/wailsapp/wails/v2/pkg/commands/build"
|
||||
)
|
||||
|
||||
// AddSubcommand adds the `debug` command for the Wails application
|
||||
func AddSubcommand(app *clir.Cli, w io.Writer) error {
|
||||
|
||||
outputType := "desktop"
|
||||
|
||||
validTargetTypes := slicer.String([]string{"desktop", "hybrid", "server"})
|
||||
|
||||
command := app.NewSubCommand("debug", "Builds the application then runs delve on the binary")
|
||||
|
||||
// Setup target type flag
|
||||
description := "Type of application to build. Valid types: " + validTargetTypes.Join(",")
|
||||
command.StringFlag("t", description, &outputType)
|
||||
|
||||
compilerCommand := "go"
|
||||
command.StringFlag("compiler", "Use a different go compiler to build, eg go1.15beta1", &compilerCommand)
|
||||
|
||||
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)
|
||||
|
||||
command.Action(func() error {
|
||||
|
||||
// Create logger
|
||||
logger := clilogger.New(w)
|
||||
logger.Mute(quiet)
|
||||
|
||||
// Validate output type
|
||||
if !validTargetTypes.Contains(outputType) {
|
||||
return fmt.Errorf("output type '%s' is not valid", outputType)
|
||||
}
|
||||
|
||||
if !quiet {
|
||||
app.PrintBanner()
|
||||
}
|
||||
|
||||
task := fmt.Sprintf("Building %s Application", strings.Title(outputType))
|
||||
logger.Println(task)
|
||||
logger.Println(strings.Repeat("-", len(task)))
|
||||
|
||||
// Setup mode
|
||||
mode := build.Debug
|
||||
|
||||
// Create BuildOptions
|
||||
buildOptions := &build.Options{
|
||||
Logger: logger,
|
||||
OutputType: outputType,
|
||||
Mode: mode,
|
||||
Pack: false,
|
||||
Platform: runtime.GOOS,
|
||||
LDFlags: ldflags,
|
||||
Compiler: compilerCommand,
|
||||
KeepAssets: false,
|
||||
}
|
||||
|
||||
outputFilename, err := doDebugBuild(buildOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check delve exists
|
||||
delveExists := shell.CommandExists("dlv")
|
||||
if !delveExists {
|
||||
return fmt.Errorf("cannot launch delve (Is it installed?)")
|
||||
}
|
||||
|
||||
// Get cwd
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Launch delve
|
||||
println("Launching Delve on port 2345...")
|
||||
command := shell.CreateCommand(cwd, "dlv", "--listen=:2345", "--headless=true", "--api-version=2", "--accept-multiclient", "exec", outputFilename)
|
||||
return command.Run()
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// doDebugBuild is our main build command
|
||||
func doDebugBuild(buildOptions *build.Options) (string, error) {
|
||||
|
||||
// Start Time
|
||||
start := time.Now()
|
||||
|
||||
outputFilename, err := build.Build(buildOptions)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Output stats
|
||||
elapsed := time.Since(start)
|
||||
buildOptions.Logger.Println("")
|
||||
buildOptions.Logger.Println(fmt.Sprintf("Built '%s' in %s.", outputFilename, elapsed.Round(time.Millisecond).String()))
|
||||
buildOptions.Logger.Println("")
|
||||
|
||||
return outputFilename, nil
|
||||
}
|
||||
@@ -10,9 +10,10 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"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"
|
||||
@@ -24,14 +25,6 @@ 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)
|
||||
@@ -42,15 +35,10 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
|
||||
|
||||
// extensions to trigger rebuilds
|
||||
extensions := "go"
|
||||
command.StringFlag("m", "Extensions to trigger rebuilds (comma separated) eg go,js,css,html", &extensions)
|
||||
command.StringFlag("e", "Extensions to trigger rebuilds (comma separated) eg go,js,css,html", &extensions)
|
||||
|
||||
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()
|
||||
@@ -64,7 +52,7 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
|
||||
defer watcher.Close()
|
||||
|
||||
var debugBinaryProcess *process.Process = nil
|
||||
var buildFrontend bool = true
|
||||
var buildFrontend bool = false
|
||||
var extensionsThatTriggerARebuild = strings.Split(extensions, ",")
|
||||
|
||||
// Setup signal handler
|
||||
@@ -75,8 +63,10 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
|
||||
|
||||
// Do initial build
|
||||
logger.Println("Building application for development...")
|
||||
debugBinaryProcess = restartApp(logger, outputType, ldflags, compilerCommand, buildFrontend, debugBinaryProcess)
|
||||
|
||||
debugBinaryProcess, err = restartApp(logger, "dev", ldflags, compilerCommand, debugBinaryProcess)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go debounce(100*time.Millisecond, watcher.Events, debounceQuit, func(event fsnotify.Event) {
|
||||
// logger.Println("event: %+v", event)
|
||||
|
||||
@@ -85,7 +75,10 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
|
||||
// If this is a folder, add it to our watch list
|
||||
if fs.DirExists(event.Name) {
|
||||
if !strings.Contains(event.Name, "node_modules") {
|
||||
watcher.Add(event.Name)
|
||||
err := watcher.Add(event.Name)
|
||||
if err != nil {
|
||||
logger.Fatal("%s", err.Error())
|
||||
}
|
||||
logger.Println("Watching directory: %s", event.Name)
|
||||
}
|
||||
}
|
||||
@@ -95,7 +88,7 @@ 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)
|
||||
logger.Println("modified file: %s", event.Name)
|
||||
var rebuild bool = false
|
||||
|
||||
// Iterate all file patterns
|
||||
@@ -116,17 +109,16 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
|
||||
return
|
||||
}
|
||||
|
||||
if buildFrontend {
|
||||
logger.Println("Full rebuild triggered: %s updated", event.Name)
|
||||
} else {
|
||||
logger.Println("Partial build triggered: %s updated", event.Name)
|
||||
}
|
||||
logger.Println("Partial build triggered: %s updated", event.Name)
|
||||
|
||||
// Do a rebuild
|
||||
|
||||
// Try and build the app
|
||||
newBinaryProcess := restartApp(logger, outputType, ldflags, compilerCommand, buildFrontend, debugBinaryProcess)
|
||||
|
||||
newBinaryProcess, err := restartApp(logger, "dev", ldflags, compilerCommand, debugBinaryProcess)
|
||||
if err != nil {
|
||||
fmt.Printf("Error during build: %s", err.Error())
|
||||
return
|
||||
}
|
||||
// If we have a new process, save it
|
||||
if newBinaryProcess != nil {
|
||||
debugBinaryProcess = newBinaryProcess
|
||||
@@ -164,7 +156,7 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
|
||||
for quit == false {
|
||||
select {
|
||||
case <-quitChannel:
|
||||
println()
|
||||
println("Caught quit")
|
||||
// Notify debouncer to quit
|
||||
debounceQuit <- true
|
||||
quit = true
|
||||
@@ -173,7 +165,10 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
|
||||
|
||||
// Kill the current program if running
|
||||
if debugBinaryProcess != nil {
|
||||
debugBinaryProcess.Kill()
|
||||
err := debugBinaryProcess.Kill()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
logger.Println("Development mode exited")
|
||||
@@ -203,13 +198,13 @@ exit:
|
||||
}
|
||||
}
|
||||
|
||||
func restartApp(logger *clilogger.CLILogger, outputType string, ldflags string, compilerCommand string, buildFrontend bool, debugBinaryProcess *process.Process) *process.Process {
|
||||
func restartApp(logger *clilogger.CLILogger, outputType string, ldflags string, compilerCommand string, debugBinaryProcess *process.Process) (*process.Process, error) {
|
||||
|
||||
appBinary, err := buildApp(logger, outputType, ldflags, compilerCommand, buildFrontend)
|
||||
appBinary, err := buildApp(logger, outputType, ldflags, compilerCommand)
|
||||
println()
|
||||
if err != nil {
|
||||
logger.Println("[ERROR] Build Failed: %s", err.Error())
|
||||
return nil
|
||||
logger.Fatal(err.Error())
|
||||
return nil, errors.Wrap(err, "Build Failed:")
|
||||
}
|
||||
logger.Println("Build new binary: %s", appBinary)
|
||||
|
||||
@@ -231,14 +226,17 @@ func restartApp(logger *clilogger.CLILogger, outputType string, ldflags string,
|
||||
err = newProcess.Start()
|
||||
if err != nil {
|
||||
// Remove binary
|
||||
fs.DeleteFile(appBinary)
|
||||
deleteError := fs.DeleteFile(appBinary)
|
||||
if deleteError != nil {
|
||||
logger.Fatal("Unable to delete app binary: " + appBinary)
|
||||
}
|
||||
logger.Fatal("Unable to start application: %s", err.Error())
|
||||
}
|
||||
|
||||
return newProcess
|
||||
return newProcess, nil
|
||||
}
|
||||
|
||||
func buildApp(logger *clilogger.CLILogger, outputType string, ldflags string, compilerCommand string, buildFrontend bool) (string, error) {
|
||||
func buildApp(logger *clilogger.CLILogger, outputType string, ldflags string, compilerCommand string) (string, error) {
|
||||
|
||||
// Create random output file
|
||||
outputFile := fmt.Sprintf("debug-%d", time.Now().Unix())
|
||||
@@ -253,7 +251,7 @@ func buildApp(logger *clilogger.CLILogger, outputType string, ldflags string, co
|
||||
LDFlags: ldflags,
|
||||
Compiler: compilerCommand,
|
||||
OutputFile: outputFile,
|
||||
IgnoreFrontend: !buildFrontend,
|
||||
IgnoreFrontend: true,
|
||||
}
|
||||
|
||||
return build.Build(buildOptions)
|
||||
|
||||
164
v2/cmd/wails/internal/commands/update/update.go
Normal file
164
v2/cmd/wails/internal/commands/update/update.go
Normal file
@@ -0,0 +1,164 @@
|
||||
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 Wails.`)
|
||||
|
||||
// Setup flags
|
||||
var prereleaseRequired bool
|
||||
command.BoolFlag("pre", "Update to latest Prerelease", &prereleaseRequired)
|
||||
|
||||
var specificVersion string
|
||||
command.StringFlag("version", "Install a specific version (Overrides other flags)", &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 " + 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 updated to " + desiredVersion)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -3,8 +3,11 @@ package main
|
||||
import (
|
||||
"os"
|
||||
|
||||
"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"
|
||||
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/dev"
|
||||
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/doctor"
|
||||
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/generate"
|
||||
@@ -19,7 +22,6 @@ func fatal(message string) {
|
||||
func main() {
|
||||
|
||||
var err error
|
||||
version := "v2.0.0-alpha"
|
||||
|
||||
app := clir.NewCli("Wails", "Go/HTML Application Framework", version)
|
||||
|
||||
@@ -28,6 +30,11 @@ func main() {
|
||||
if err != nil {
|
||||
fatal(err.Error())
|
||||
}
|
||||
|
||||
err = debug.AddSubcommand(app, os.Stdout)
|
||||
if err != nil {
|
||||
fatal(err.Error())
|
||||
}
|
||||
err = doctor.AddSubcommand(app, os.Stdout)
|
||||
if err != nil {
|
||||
fatal(err.Error())
|
||||
@@ -43,6 +50,11 @@ 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())
|
||||
|
||||
3
v2/cmd/wails/version.go
Normal file
3
v2/cmd/wails/version.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package main
|
||||
|
||||
var version = "v2.0.0-alpha.21"
|
||||
@@ -1,11 +1,13 @@
|
||||
module github.com/wailsapp/wails/v2
|
||||
|
||||
go 1.13
|
||||
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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
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=
|
||||
|
||||
@@ -2,11 +2,39 @@
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"github.com/wailsapp/wails/v2/pkg/logger"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Init initialises the application for a debug environment
|
||||
func (a *App) Init() error {
|
||||
// Indicate debug mode
|
||||
a.debug = true
|
||||
// Enable dev tools
|
||||
a.options.DevTools = true
|
||||
|
||||
if a.appType == "desktop" {
|
||||
// Enable dev tools
|
||||
a.options.DevTools = true
|
||||
}
|
||||
|
||||
// Set log levels
|
||||
greeting := flag.String("loglevel", "debug", "Loglevel to use - Trace, Debug, Info, Warning, Error")
|
||||
flag.Parse()
|
||||
if len(*greeting) > 0 {
|
||||
switch strings.ToLower(*greeting) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// +build !desktop,!hybrid,!server
|
||||
// +build !desktop,!hybrid,!server,!dev
|
||||
|
||||
package app
|
||||
|
||||
@@ -10,6 +10,8 @@ package app
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
)
|
||||
|
||||
@@ -22,6 +24,8 @@ type App struct {
|
||||
|
||||
// Indicates if the app is running in debug mode
|
||||
debug bool
|
||||
|
||||
logger *logger.Logger
|
||||
}
|
||||
|
||||
// CreateApp returns a null application
|
||||
@@ -35,7 +39,3 @@ func (a *App) Run() error {
|
||||
os.Exit(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Bind the dummy interface
|
||||
func (a *App) Bind(_ interface{}) {
|
||||
}
|
||||
|
||||
@@ -3,9 +3,13 @@
|
||||
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"
|
||||
"github.com/wailsapp/wails/v2/internal/menumanager"
|
||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher"
|
||||
"github.com/wailsapp/wails/v2/internal/runtime"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
@@ -16,6 +20,8 @@ import (
|
||||
|
||||
// App defines a Wails application structure
|
||||
type App struct {
|
||||
appType string
|
||||
|
||||
window *ffenestri.Application
|
||||
servicebus *servicebus.ServiceBus
|
||||
logger *logger.Logger
|
||||
@@ -23,15 +29,15 @@ type App struct {
|
||||
options *options.App
|
||||
|
||||
// Subsystems
|
||||
log *subsystem.Log
|
||||
runtime *subsystem.Runtime
|
||||
event *subsystem.Event
|
||||
binding *subsystem.Binding
|
||||
call *subsystem.Call
|
||||
menu *subsystem.Menu
|
||||
tray *subsystem.Tray
|
||||
contextmenus *subsystem.ContextMenus
|
||||
dispatcher *messagedispatcher.Dispatcher
|
||||
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
|
||||
@@ -42,6 +48,10 @@ type App struct {
|
||||
// Application Stores
|
||||
loglevelStore *runtime.Store
|
||||
appconfigStore *runtime.Store
|
||||
|
||||
// Startup/Shutdown
|
||||
startupCallback func(*runtime.Runtime)
|
||||
shutdownCallback func()
|
||||
}
|
||||
|
||||
// Create App
|
||||
@@ -54,13 +64,38 @@ func CreateApp(appoptions *options.App) (*App, error) {
|
||||
myLogger := logger.New(appoptions.Logger)
|
||||
myLogger.SetLogLevel(appoptions.LogLevel)
|
||||
|
||||
window := ffenestri.NewApplicationWithConfig(appoptions, myLogger)
|
||||
// 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)
|
||||
}
|
||||
|
||||
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{
|
||||
window: window,
|
||||
servicebus: servicebus.New(myLogger),
|
||||
logger: myLogger,
|
||||
bindings: binding.NewBindings(myLogger),
|
||||
appType: "desktop",
|
||||
window: window,
|
||||
servicebus: servicebus.New(myLogger),
|
||||
logger: myLogger,
|
||||
bindings: binding.NewBindings(myLogger, appoptions.Bind, bindingExemptions),
|
||||
menuManager: menuManager,
|
||||
startupCallback: appoptions.Startup,
|
||||
shutdownCallback: appoptions.Shutdown,
|
||||
}
|
||||
|
||||
result.options = appoptions
|
||||
@@ -77,8 +112,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)
|
||||
signalsubsystem, err := signal.NewManager(ctx, cancel, a.servicebus, a.logger, a.shutdownCallback)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -92,12 +132,7 @@ func (a *App) Run() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start the runtime
|
||||
applicationMenu := options.GetApplicationMenu(a.options)
|
||||
trayMenu := options.GetTray(a.options)
|
||||
contextMenus := options.GetContextMenus(a.options)
|
||||
|
||||
runtimesubsystem, err := subsystem.NewRuntime(a.servicebus, a.logger, applicationMenu, trayMenu, contextMenus)
|
||||
runtimesubsystem, err := subsystem.NewRuntime(ctx, a.servicebus, a.logger, a.startupCallback, a.shutdownCallback)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -111,17 +146,6 @@ 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 {
|
||||
@@ -145,61 +169,33 @@ func (a *App) Run() error {
|
||||
}
|
||||
|
||||
// Start the eventing subsystem
|
||||
event, err := subsystem.NewEvent(a.servicebus, a.logger)
|
||||
eventsubsystem, err := subsystem.NewEvent(ctx, a.servicebus, a.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.event = event
|
||||
a.event = eventsubsystem
|
||||
err = a.event.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Optionally start the menu subsystem
|
||||
if applicationMenu != nil {
|
||||
menusubsystem, err := subsystem.NewMenu(applicationMenu, a.servicebus, a.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.menu = menusubsystem
|
||||
err = a.menu.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Optionally start the tray subsystem
|
||||
if trayMenu != nil {
|
||||
traysubsystem, err := subsystem.NewTray(trayMenu, a.servicebus, a.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.tray = traysubsystem
|
||||
err = a.tray.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Optionally start the context menu subsystem
|
||||
if contextMenus != nil {
|
||||
contextmenussubsystem, err := subsystem.NewContextMenus(contextMenus, a.servicebus, a.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.contextmenus = contextmenussubsystem
|
||||
err = a.contextmenus.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Start the call subsystem
|
||||
call, err := subsystem.NewCall(a.servicebus, a.logger, a.bindings.DB(), a.runtime.GoRuntime())
|
||||
// Start the menu subsystem
|
||||
menusubsystem, err := subsystem.NewMenu(ctx, a.servicebus, a.logger, a.menuManager)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.call = call
|
||||
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
|
||||
@@ -211,23 +207,29 @@ func (a *App) Run() error {
|
||||
return err
|
||||
}
|
||||
|
||||
result := a.window.Run(dispatcher, bindingDump, a.debug)
|
||||
err = 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
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
237
v2/internal/app/dev.go
Normal file
237
v2/internal/app/dev.go
Normal file
@@ -0,0 +1,237 @@
|
||||
// +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)
|
||||
myLogger.SetLogLevel(appoptions.LogLevel)
|
||||
|
||||
// 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),
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// Setup signal handler
|
||||
signalsubsystem, err := signal.NewManager(ctx, cancel, a.servicebus, a.logger, a.shutdownCallback)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.signal = signalsubsystem
|
||||
a.signal.Start()
|
||||
|
||||
// Start the service bus
|
||||
a.servicebus.Debug()
|
||||
err = a.servicebus.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
runtimesubsystem, err := subsystem.NewRuntime(ctx, a.servicebus, a.logger, a.startupCallback, a.shutdownCallback)
|
||||
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
|
||||
}
|
||||
|
||||
err = a.bridge.Run(dispatcher, 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
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
@@ -75,7 +75,7 @@ func CreateApp(options *Options) *App {
|
||||
webserver: webserver.NewWebServer(myLogger),
|
||||
servicebus: servicebus.New(myLogger),
|
||||
logger: myLogger,
|
||||
bindings: binding.NewBindings(myLogger),
|
||||
bindings: binding.NewBindings(myLogger, options.Bind),
|
||||
}
|
||||
|
||||
// Initialise the app
|
||||
@@ -192,14 +192,3 @@ 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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,13 @@ 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"
|
||||
@@ -17,12 +20,16 @@ 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
|
||||
@@ -30,28 +37,40 @@ 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(options *Options) *App {
|
||||
options.mergeDefaults()
|
||||
// We ignore the inputs (for now)
|
||||
func CreateApp(appoptions *options.App) (*App, error) {
|
||||
|
||||
// TODO: Allow logger output override on CLI
|
||||
myLogger := logger.New(os.Stdout)
|
||||
myLogger.SetLogLevel(logger.TRACE)
|
||||
// Merge default options
|
||||
options.MergeDefaults(appoptions)
|
||||
|
||||
// Set up logger
|
||||
myLogger := logger.New(appoptions.Logger)
|
||||
myLogger.SetLogLevel(appoptions.LogLevel)
|
||||
|
||||
result := &App{
|
||||
bindings: binding.NewBindings(myLogger),
|
||||
logger: myLogger,
|
||||
servicebus: servicebus.New(myLogger),
|
||||
webserver: webserver.NewWebServer(myLogger),
|
||||
appType: "server",
|
||||
bindings: binding.NewBindings(myLogger, options.Bind),
|
||||
logger: myLogger,
|
||||
servicebus: servicebus.New(myLogger),
|
||||
webserver: webserver.NewWebServer(myLogger),
|
||||
startupCallback: appoptions.Startup,
|
||||
shutdownCallback: appoptions.Shutdown,
|
||||
}
|
||||
|
||||
// Initialise app
|
||||
result.Init()
|
||||
|
||||
return result
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Run the application
|
||||
@@ -88,8 +107,21 @@ 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)
|
||||
log, err := subsystem.NewLog(a.servicebus, a.logger, a.loglevelStore)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -102,14 +134,6 @@ 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 {
|
||||
@@ -127,7 +151,7 @@ func (a *App) Run() error {
|
||||
a.event.Start()
|
||||
|
||||
// Start the call subsystem
|
||||
call, err := subsystem.NewCall(a.servicebus, a.logger, a.bindings.DB())
|
||||
call, err := subsystem.NewCall(a.servicebus, a.logger, a.bindings.DB(), a.runtime.GoRuntime())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -147,14 +171,3 @@ 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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,28 +2,53 @@ 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
|
||||
db *DB
|
||||
logger logger.CustomLogger
|
||||
exemptions slicer.StringSlicer
|
||||
}
|
||||
|
||||
// NewBindings returns a new Bindings object
|
||||
func NewBindings(logger *logger.Logger) *Bindings {
|
||||
return &Bindings{
|
||||
func NewBindings(logger *logger.Logger, structPointersToBind []interface{}, exemptions []interface{}) *Bindings {
|
||||
result := &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 := getMethods(structPtr)
|
||||
methods, err := b.getMethods(structPtr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot bind value to app: %s", err.Error())
|
||||
}
|
||||
@@ -34,29 +59,8 @@ func (b *Bindings) Add(structPtr interface{}) error {
|
||||
structName := splitName[1]
|
||||
methodName := splitName[2]
|
||||
|
||||
// Is this WailsInit?
|
||||
if method.IsWailsInit() {
|
||||
err := b.db.AddWailsInit(method)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.logger.Trace("Registered WailsInit method: %s", method.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
// Is this WailsShutdown?
|
||||
if method.IsWailsShutdown() {
|
||||
err := b.db.AddWailsShutdown(method)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.logger.Trace("Registered WailsShutdown method: %s", method.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
// Add it as a regular method
|
||||
b.db.AddMethod(packageName, structName, methodName, method)
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BoundMethod defines all the data related to a Go method that is
|
||||
@@ -17,58 +16,6 @@ type BoundMethod struct {
|
||||
Method reflect.Value `json:"-"`
|
||||
}
|
||||
|
||||
// IsWailsInit returns true if the method name is "WailsInit"
|
||||
func (b *BoundMethod) IsWailsInit() bool {
|
||||
return strings.HasSuffix(b.Name, "WailsInit")
|
||||
}
|
||||
|
||||
// IsWailsShutdown returns true if the method name is "WailsShutdown"
|
||||
func (b *BoundMethod) IsWailsShutdown() bool {
|
||||
return strings.HasSuffix(b.Name, "WailsShutdown")
|
||||
}
|
||||
|
||||
// VerifyWailsInit checks if the WailsInit signature is correct
|
||||
func (b *BoundMethod) VerifyWailsInit() error {
|
||||
// Must only have 1 input
|
||||
if b.InputCount() != 1 {
|
||||
return fmt.Errorf("invalid method signature for %s: expected `WailsInit(*wails.Runtime) error`", b.Name)
|
||||
}
|
||||
|
||||
// Check input type
|
||||
if !b.Inputs[0].IsType("*runtime.Runtime") {
|
||||
return fmt.Errorf("invalid method signature for %s: expected `WailsInit(*wails.Runtime) error`", b.Name)
|
||||
}
|
||||
|
||||
// Must only have 1 output
|
||||
if b.OutputCount() != 1 {
|
||||
return fmt.Errorf("invalid method signature for %s: expected `WailsInit(*wails.Runtime) error`", b.Name)
|
||||
}
|
||||
|
||||
// Check output type
|
||||
if !b.Outputs[0].IsError() {
|
||||
return fmt.Errorf("invalid method signature for %s: expected `WailsInit(*wails.Runtime) error`", b.Name)
|
||||
}
|
||||
|
||||
// Input must be of type Runtime
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyWailsShutdown checks if the WailsShutdown signature is correct
|
||||
func (b *BoundMethod) VerifyWailsShutdown() error {
|
||||
// Must have no inputs
|
||||
if b.InputCount() != 0 {
|
||||
return fmt.Errorf("invalid method signature for WailsShutdown: expected `WailsShutdown()`")
|
||||
}
|
||||
|
||||
// Must have no outputs
|
||||
if b.OutputCount() != 0 {
|
||||
return fmt.Errorf("invalid method signature for WailsShutdown: expected `WailsShutdown()`")
|
||||
}
|
||||
|
||||
// Input must be of type Runtime
|
||||
return nil
|
||||
}
|
||||
|
||||
// InputCount returns the number of inputs this bound method has
|
||||
func (b *BoundMethod) InputCount() int {
|
||||
return len(b.Inputs)
|
||||
@@ -83,6 +30,9 @@ 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()
|
||||
|
||||
@@ -15,10 +15,6 @@ type DB struct {
|
||||
// It used for performance gains at runtime
|
||||
methodMap map[string]*BoundMethod
|
||||
|
||||
// These are slices of methods registered using WailsInit and WailsShutdown
|
||||
wailsInitMethods []*BoundMethod
|
||||
wailsShutdownMethods []*BoundMethod
|
||||
|
||||
// Lock to ensure sync access to the data
|
||||
lock sync.RWMutex
|
||||
}
|
||||
@@ -94,38 +90,6 @@ func (d *DB) AddMethod(packageName string, structName string, methodName string,
|
||||
|
||||
}
|
||||
|
||||
// AddWailsInit checks the given method is a WailsInit method and if it
|
||||
// is, adds it to the list of WailsInit methods
|
||||
func (d *DB) AddWailsInit(method *BoundMethod) error {
|
||||
err := method.VerifyWailsInit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Lock the db whilst processing and unlock on return
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
d.wailsInitMethods = append(d.wailsInitMethods, method)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddWailsShutdown checks the given method is a WailsInit method and if it
|
||||
// is, adds it to the list of WailsShutdown methods
|
||||
func (d *DB) AddWailsShutdown(method *BoundMethod) error {
|
||||
err := method.VerifyWailsShutdown()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Lock the db whilst processing and unlock on return
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
d.wailsShutdownMethods = append(d.wailsShutdownMethods, method)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToJSON converts the method map to JSON
|
||||
func (d *DB) ToJSON() (string, error) {
|
||||
|
||||
@@ -138,13 +102,3 @@ func (d *DB) ToJSON() (string, error) {
|
||||
// Return zero copy string as this string will be read only
|
||||
return *(*string)(unsafe.Pointer(&bytes)), err
|
||||
}
|
||||
|
||||
// WailsInitMethods returns the list of registered WailsInit methods
|
||||
func (d *DB) WailsInitMethods() []*BoundMethod {
|
||||
return d.wailsInitMethods
|
||||
}
|
||||
|
||||
// WailsShutdownMethods returns the list of registered WailsInit methods
|
||||
func (d *DB) WailsShutdownMethods() []*BoundMethod {
|
||||
return d.wailsShutdownMethods
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func isStruct(value interface{}) bool {
|
||||
return reflect.ValueOf(value).Kind() == reflect.Struct
|
||||
}
|
||||
|
||||
func getMethods(value interface{}) ([]*BoundMethod, error) {
|
||||
func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) {
|
||||
|
||||
// Create result placeholder
|
||||
var result []*BoundMethod
|
||||
@@ -56,6 +56,11 @@ func 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,
|
||||
|
||||
99
v2/internal/bridge/bridge.go
Normal file
99
v2/internal/bridge/bridge.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"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
|
||||
}
|
||||
|
||||
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, bindings string, debug bool) error {
|
||||
|
||||
// Ensure we cancel the context when we shutdown
|
||||
defer b.cancel()
|
||||
|
||||
b.bindings = bindings
|
||||
b.dispatcher = dispatcher
|
||||
|
||||
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.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.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()
|
||||
}
|
||||
121
v2/internal/bridge/client.go
Normal file
121
v2/internal/bridge/client.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v2/pkg/options/dialog"
|
||||
)
|
||||
|
||||
type BridgeClient struct {
|
||||
session *session
|
||||
}
|
||||
|
||||
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) {
|
||||
b.session.log.Info("OpenDialog unsupported in Bridge mode")
|
||||
}
|
||||
|
||||
func (b BridgeClient) SaveDialog(dialogOptions *dialog.SaveDialog, callbackID string) {
|
||||
b.session.log.Info("SaveDialog unsupported in Bridge mode")
|
||||
}
|
||||
|
||||
func (b BridgeClient) MessageDialog(dialogOptions *dialog.MessageDialog, callbackID string) {
|
||||
b.session.log.Info("MessageDialog unsupported in Bridge mode")
|
||||
}
|
||||
|
||||
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.log.Info("SetTrayMenu unsupported in Bridge mode")
|
||||
}
|
||||
|
||||
func (b BridgeClient) UpdateTrayMenuLabel(JSON string) {
|
||||
b.session.log.Info("UpdateTrayMenuLabel unsupported in Bridge mode")
|
||||
}
|
||||
|
||||
func (b BridgeClient) UpdateContextMenu(contextMenuJSON string) {
|
||||
b.session.log.Info("UpdateContextMenu unsupported in Bridge mode")
|
||||
}
|
||||
|
||||
func newBridgeClient(session *session) *BridgeClient {
|
||||
return &BridgeClient{
|
||||
session: session,
|
||||
}
|
||||
}
|
||||
1
v2/internal/bridge/darwin.js
Normal file
1
v2/internal/bridge/darwin.js
Normal file
File diff suppressed because one or more lines are too long
145
v2/internal/bridge/session.go
Normal file
145
v2/internal/bridge/session.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"log"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"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
|
||||
}
|
||||
|
||||
func newSession(conn *websocket.Conn, 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,
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
for {
|
||||
messageType, buffer, err := s.conn.ReadMessage()
|
||||
if messageType == -1 {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
s.log.Error("Error reading message: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
v2/internal/deepcopy/LICENSE
Normal file
21
v2/internal/deepcopy/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
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.
|
||||
125
v2/internal/deepcopy/deepcopy.go
Normal file
125
v2/internal/deepcopy/deepcopy.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
1110
v2/internal/deepcopy/deepcopy_test.go
Normal file
1110
v2/internal/deepcopy/deepcopy_test.go
Normal file
File diff suppressed because it is too large
Load Diff
95
v2/internal/ffenestri/common.c
Normal file
95
v2/internal/ffenestri/common.c
Normal file
@@ -0,0 +1,95 @@
|
||||
//
|
||||
// Created by Lea Anthony on 6/1/21.
|
||||
//
|
||||
|
||||
#include "common.h"
|
||||
|
||||
// Credit: https://stackoverflow.com/a/8465083
|
||||
char* concat(const char *string1, const char *string2)
|
||||
{
|
||||
const size_t len1 = strlen(string1);
|
||||
const size_t len2 = strlen(string2);
|
||||
char *result = malloc(len1 + len2 + 1);
|
||||
strcpy(result, string1);
|
||||
memcpy(result + len1, string2, len2 + 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 10k is more than enough for a log message
|
||||
#define MAXMESSAGE 1024*10
|
||||
char abortbuffer[MAXMESSAGE];
|
||||
|
||||
void ABORT(const char *message, ...) {
|
||||
const char *temp = concat("FATAL: ", message);
|
||||
va_list args;
|
||||
va_start(args, message);
|
||||
vsnprintf(abortbuffer, MAXMESSAGE, temp, args);
|
||||
printf("%s\n", &abortbuffer[0]);
|
||||
MEMFREE(temp);
|
||||
va_end(args);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
int freeHashmapItem(void *const context, struct hashmap_element_s *const e) {
|
||||
free(e->data);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* getJSONString(JsonNode *item, const char* key) {
|
||||
// Get key
|
||||
JsonNode *node = json_find_member(item, key);
|
||||
const char *result = "";
|
||||
if ( node != NULL && node->tag == JSON_STRING) {
|
||||
result = node->string_;
|
||||
}
|
||||
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);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
JsonNode* mustJSONObject(JsonNode *node, const char* key) {
|
||||
struct JsonNode* result = getJSONObject(node, key);
|
||||
if ( result == NULL ) {
|
||||
ABORT_JSON(node, key);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool getJSONInt(JsonNode *item, const char* key, int *result) {
|
||||
JsonNode *node = json_find_member(item, key);
|
||||
if ( node != NULL && node->tag == JSON_NUMBER) {
|
||||
*result = (int) node->number_;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonNode* mustParseJSON(const char* JSON) {
|
||||
JsonNode* parsedUpdate = json_decode(JSON);
|
||||
if ( parsedUpdate == NULL ) {
|
||||
ABORT("Unable to decode JSON: %s\n", JSON);
|
||||
}
|
||||
return parsedUpdate;
|
||||
}
|
||||
40
v2/internal/ffenestri/common.h
Normal file
40
v2/internal/ffenestri/common.h
Normal file
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// Created by Lea Anthony on 6/1/21.
|
||||
//
|
||||
|
||||
#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"
|
||||
#include "hashmap.h"
|
||||
#include "vec.h"
|
||||
#include "json.h"
|
||||
|
||||
#define STREQ(a,b) strcmp(a, b) == 0
|
||||
#define STREMPTY(string) strlen(string) == 0
|
||||
#define STRCOPY(a) concat(a, "")
|
||||
#define STR_HAS_CHARS(input) input != NULL && strlen(input) > 0
|
||||
#define MEMFREE(input) free((void*)input); input = NULL;
|
||||
#define FREE_AND_SET(variable, value) if( variable != NULL ) { MEMFREE(variable); } variable = 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);
|
||||
const char* getJSONString(JsonNode *item, const char* key);
|
||||
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 getJSONInt(JsonNode *item, const char* key, int *result);
|
||||
|
||||
JsonNode* mustParseJSON(const char* JSON);
|
||||
|
||||
#endif //ASSETS_C_COMMON_H
|
||||
99
v2/internal/ffenestri/contextmenus_darwin.c
Normal file
99
v2/internal/ffenestri/contextmenus_darwin.c
Normal file
@@ -0,0 +1,99 @@
|
||||
////
|
||||
//// 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(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);
|
||||
|
||||
}
|
||||
|
||||
33
v2/internal/ffenestri/contextmenus_darwin.h
Normal file
33
v2/internal/ffenestri/contextmenus_darwin.h
Normal file
@@ -0,0 +1,33 @@
|
||||
////
|
||||
//// Created by Lea Anthony on 6/1/21.
|
||||
////
|
||||
//
|
||||
#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
|
||||
65
v2/internal/ffenestri/contextmenustore_darwin.c
Normal file
65
v2/internal/ffenestri/contextmenustore_darwin.c
Normal file
@@ -0,0 +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);
|
||||
|
||||
}
|
||||
27
v2/internal/ffenestri/contextmenustore_darwin.h
Normal file
27
v2/internal/ffenestri/contextmenustore_darwin.h
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// Created by Lea Anthony on 7/1/21.
|
||||
//
|
||||
|
||||
#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
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"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"
|
||||
@@ -31,7 +33,10 @@ type Application struct {
|
||||
memory []unsafe.Pointer
|
||||
|
||||
// This is the main app pointer
|
||||
app unsafe.Pointer
|
||||
app *C.struct_Application
|
||||
|
||||
// Manages menus
|
||||
menuManager *menumanager.Manager
|
||||
|
||||
// Logger
|
||||
logger logger.CustomLogger
|
||||
@@ -52,10 +57,11 @@ func init() {
|
||||
}
|
||||
|
||||
// NewApplicationWithConfig creates a new application based on the given config
|
||||
func NewApplicationWithConfig(config *options.App, logger *logger.Logger) *Application {
|
||||
func NewApplicationWithConfig(config *options.App, logger *logger.Logger, menuManager *menumanager.Manager) *Application {
|
||||
return &Application{
|
||||
config: config,
|
||||
logger: logger.CustomLogger("Ffenestri"),
|
||||
config: config,
|
||||
logger: logger.CustomLogger("Ffenestri"),
|
||||
menuManager: menuManager,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,10 +119,11 @@ 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)
|
||||
app := C.NewApplication(title, width, height, resizable, devtools, fullscreen, startHidden, logLevel)
|
||||
hideWindowOnClose := a.bool2Cint(a.config.HideWindowOnClose)
|
||||
app := C.NewApplication(title, width, height, resizable, devtools, fullscreen, startHidden, logLevel, hideWindowOnClose)
|
||||
|
||||
// Save app reference
|
||||
a.app = unsafe.Pointer(app)
|
||||
a.app = (*C.struct_Application)(app)
|
||||
|
||||
// Set Min Window Size
|
||||
minWidth := C.int(a.config.MinWidth)
|
||||
@@ -152,7 +159,10 @@ func (a *Application) Run(incomingDispatcher Dispatcher, bindings string, debug
|
||||
dispatcher = incomingDispatcher.RegisterClient(newClient(a))
|
||||
|
||||
// Process platform settings
|
||||
a.processPlatformSettings()
|
||||
err := a.processPlatformSettings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check we could initialise the application
|
||||
if app != nil {
|
||||
|
||||
@@ -2,41 +2,43 @@
|
||||
#define __FFENESTRI_H__
|
||||
|
||||
#include <stdio.h>
|
||||
struct Application;
|
||||
|
||||
extern void *NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden, int logLevel);
|
||||
extern void SetMinWindowSize(void *app, int minWidth, int minHeight);
|
||||
extern void SetMaxWindowSize(void *app, int maxWidth, int maxHeight);
|
||||
extern void Run(void *app, int argc, char **argv);
|
||||
extern void DestroyApplication(void *app);
|
||||
extern void SetDebug(void *app, int flag);
|
||||
extern void SetBindings(void *app, const char *bindings);
|
||||
extern void ExecJS(void *app, const char *script);
|
||||
extern void Hide(void *app);
|
||||
extern void Show(void *app);
|
||||
extern void Center(void *app);
|
||||
extern void Maximise(void *app);
|
||||
extern void Unmaximise(void *app);
|
||||
extern void ToggleMaximise(void *app);
|
||||
extern void Minimise(void *app);
|
||||
extern void Unminimise(void *app);
|
||||
extern void ToggleMinimise(void *app);
|
||||
extern void SetColour(void *app, int red, int green, int blue, int alpha);
|
||||
extern void SetSize(void *app, int width, int height);
|
||||
extern void SetPosition(void *app, int x, int y);
|
||||
extern void Quit(void *app);
|
||||
extern void SetTitle(void *app, const char *title);
|
||||
extern void Fullscreen(void *app);
|
||||
extern void UnFullscreen(void *app);
|
||||
extern void ToggleFullscreen(void *app);
|
||||
extern void DisableFrame(void *app);
|
||||
extern void OpenDialog(void *appPointer, 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(void *appPointer, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int showHiddenFiles, int canCreateDirectories, int treatPackagesAsDirectories);
|
||||
extern void MessageDialog(void *appPointer, 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(void *appPointer, char *callbackID);
|
||||
extern void UpdateMenu(void *app, char *menuAsJSON);
|
||||
extern void UpdateTray(void *app, char *menuAsJSON);
|
||||
extern void UpdateContextMenus(void *app, char *contextMenusAsJSON);
|
||||
extern void UpdateTrayLabel(void *app, const char *label);
|
||||
extern void UpdateTrayIcon(void *app, const char *label);
|
||||
extern struct Application *NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden, int logLevel, int hideWindowOnClose);
|
||||
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);
|
||||
extern void DestroyApplication(struct Application*);
|
||||
extern void SetDebug(struct Application*, int flag);
|
||||
extern void SetBindings(struct Application*, const char *bindings);
|
||||
extern void ExecJS(struct Application*, const char *script);
|
||||
extern void Hide(struct Application*);
|
||||
extern void Show(struct Application*);
|
||||
extern void Center(struct Application*);
|
||||
extern void Maximise(struct Application*);
|
||||
extern void Unmaximise(struct Application*);
|
||||
extern void ToggleMaximise(struct Application*);
|
||||
extern void Minimise(struct Application*);
|
||||
extern void Unminimise(struct Application*);
|
||||
extern void ToggleMinimise(struct Application*);
|
||||
extern void SetColour(struct Application*, int red, int green, int blue, int alpha);
|
||||
extern void SetSize(struct Application*, int width, int height);
|
||||
extern void SetPosition(struct Application*, int x, int y);
|
||||
extern void Quit(struct Application*);
|
||||
extern void SetTitle(struct Application*, const char *title);
|
||||
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 SetApplicationMenu(struct Application*, const char *);
|
||||
extern void AddTrayMenu(struct Application*, const char *menuTrayJSON);
|
||||
extern void SetTrayMenu(struct Application*, const char *menuTrayJSON);
|
||||
extern void UpdateTrayMenuLabel(struct Application*, const char* JSON);
|
||||
extern void AddContextMenu(struct Application*, char *contextMenuJSON);
|
||||
extern void UpdateContextMenu(struct Application*, char *contextMenuJSON);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -12,12 +12,11 @@ package ffenestri
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||
"strconv"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/options/dialog"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
)
|
||||
|
||||
// Client is our implentation of messageDispatcher.Client
|
||||
@@ -115,6 +114,14 @@ 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)
|
||||
@@ -122,7 +129,7 @@ func (c *Client) WindowSetColour(colour int) {
|
||||
}
|
||||
|
||||
// OpenDialog will open a dialog with the given title and filter
|
||||
func (c *Client) OpenDialog(dialogOptions *options.OpenDialog, callbackID string) {
|
||||
func (c *Client) OpenDialog(dialogOptions *dialog.OpenDialog, callbackID string) {
|
||||
C.OpenDialog(c.app.app,
|
||||
c.app.string2CString(callbackID),
|
||||
c.app.string2CString(dialogOptions.Title),
|
||||
@@ -140,7 +147,7 @@ func (c *Client) OpenDialog(dialogOptions *options.OpenDialog, callbackID string
|
||||
}
|
||||
|
||||
// SaveDialog will open a dialog with the given title and filter
|
||||
func (c *Client) SaveDialog(dialogOptions *options.SaveDialog, callbackID string) {
|
||||
func (c *Client) SaveDialog(dialogOptions *dialog.SaveDialog, callbackID string) {
|
||||
C.SaveDialog(c.app.app,
|
||||
c.app.string2CString(callbackID),
|
||||
c.app.string2CString(dialogOptions.Title),
|
||||
@@ -154,7 +161,7 @@ func (c *Client) SaveDialog(dialogOptions *options.SaveDialog, callbackID string
|
||||
}
|
||||
|
||||
// MessageDialog will open a message dialog with the given options
|
||||
func (c *Client) MessageDialog(dialogOptions *options.MessageDialog, callbackID string) {
|
||||
func (c *Client) MessageDialog(dialogOptions *dialog.MessageDialog, callbackID string) {
|
||||
|
||||
// Sanity check button length
|
||||
if len(dialogOptions.Buttons) > 4 {
|
||||
@@ -186,57 +193,18 @@ func (c *Client) DarkModeEnabled(callbackID string) {
|
||||
C.DarkModeEnabled(c.app.app, c.app.string2CString(callbackID))
|
||||
}
|
||||
|
||||
func (c *Client) UpdateMenu(menu *menu.Menu) {
|
||||
|
||||
// Guard against nil menus
|
||||
if menu == nil {
|
||||
return
|
||||
}
|
||||
// Process the menu
|
||||
processedMenu := NewProcessedMenu(menu)
|
||||
menuJSON, err := json.Marshal(processedMenu)
|
||||
if err != nil {
|
||||
c.app.logger.Error("Error processing updated Menu: %s", err.Error())
|
||||
return
|
||||
}
|
||||
C.UpdateMenu(c.app.app, c.app.string2CString(string(menuJSON)))
|
||||
func (c *Client) SetApplicationMenu(applicationMenuJSON string) {
|
||||
C.SetApplicationMenu(c.app.app, c.app.string2CString(applicationMenuJSON))
|
||||
}
|
||||
|
||||
func (c *Client) UpdateTray(menu *menu.Menu) {
|
||||
|
||||
// Guard against nil menus
|
||||
if menu == nil {
|
||||
return
|
||||
}
|
||||
// Process the menu
|
||||
processedMenu := NewProcessedMenu(menu)
|
||||
trayMenuJSON, err := json.Marshal(processedMenu)
|
||||
if err != nil {
|
||||
c.app.logger.Error("Error processing updated Tray: %s", err.Error())
|
||||
return
|
||||
}
|
||||
C.UpdateTray(c.app.app, c.app.string2CString(string(trayMenuJSON)))
|
||||
func (c *Client) SetTrayMenu(trayMenuJSON string) {
|
||||
C.SetTrayMenu(c.app.app, c.app.string2CString(trayMenuJSON))
|
||||
}
|
||||
|
||||
func (c *Client) UpdateTrayLabel(label string) {
|
||||
C.UpdateTrayLabel(c.app.app, c.app.string2CString(label))
|
||||
func (c *Client) UpdateTrayMenuLabel(JSON string) {
|
||||
C.UpdateTrayMenuLabel(c.app.app, c.app.string2CString(JSON))
|
||||
}
|
||||
|
||||
func (c *Client) UpdateTrayIcon(name string) {
|
||||
C.UpdateTrayIcon(c.app.app, c.app.string2CString(name))
|
||||
}
|
||||
|
||||
func (c *Client) UpdateContextMenus(contextMenus *menu.ContextMenus) {
|
||||
|
||||
// Guard against nil contextMenus
|
||||
if contextMenus == nil {
|
||||
return
|
||||
}
|
||||
// Process the menu
|
||||
contextMenusJSON, err := json.Marshal(contextMenus)
|
||||
if err != nil {
|
||||
c.app.logger.Error("Error processing updated Context Menus: %s", err.Error())
|
||||
return
|
||||
}
|
||||
C.UpdateContextMenus(c.app.app, c.app.string2CString(string(contextMenusJSON)))
|
||||
func (c *Client) UpdateContextMenu(contextMenuJSON string) {
|
||||
C.UpdateContextMenu(c.app.app, c.app.string2CString(contextMenuJSON))
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,17 +2,13 @@ package ffenestri
|
||||
|
||||
/*
|
||||
#cgo darwin CFLAGS: -DFFENESTRI_DARWIN=1
|
||||
#cgo darwin LDFLAGS: -framework WebKit -lobjc
|
||||
#cgo darwin LDFLAGS: -framework WebKit -framework CoreFoundation -lobjc
|
||||
|
||||
#include "ffenestri.h"
|
||||
#include "ffenestri_darwin.h"
|
||||
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
)
|
||||
|
||||
func (a *Application) processPlatformSettings() error {
|
||||
|
||||
@@ -63,54 +59,32 @@ func (a *Application) processPlatformSettings() error {
|
||||
}
|
||||
|
||||
// Process menu
|
||||
applicationMenu := options.GetApplicationMenu(a.config)
|
||||
if applicationMenu != nil {
|
||||
|
||||
/*
|
||||
As radio groups need to be manually managed on OSX,
|
||||
we preprocess the menu to determine the radio groups.
|
||||
This is defined as any adjacent menu item of type "RadioType".
|
||||
We keep a record of every radio group member we discover by saving
|
||||
a list of all members of the group and the number of members
|
||||
in the group (this last one is for optimisation at the C layer).
|
||||
*/
|
||||
processedMenu := NewProcessedMenu(applicationMenu)
|
||||
applicationMenuJSON, err := json.Marshal(processedMenu)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
C.SetMenu(a.app, a.string2CString(string(applicationMenuJSON)))
|
||||
//applicationMenu := options.GetApplicationMenu(a.config)
|
||||
applicationMenu := a.menuManager.GetApplicationMenuJSON()
|
||||
if applicationMenu != "" {
|
||||
C.SetApplicationMenu(a.app, a.string2CString(applicationMenu))
|
||||
}
|
||||
|
||||
// Process tray
|
||||
tray := options.GetTray(a.config)
|
||||
if tray != nil {
|
||||
|
||||
/*
|
||||
As radio groups need to be manually managed on OSX,
|
||||
we preprocess the menu to determine the radio groups.
|
||||
This is defined as any adjacent menu item of type "RadioType".
|
||||
We keep a record of every radio group member we discover by saving
|
||||
a list of all members of the group and the number of members
|
||||
in the group (this last one is for optimisation at the C layer).
|
||||
*/
|
||||
processedMenu := NewProcessedMenu(tray.Menu)
|
||||
trayMenuJSON, err := json.Marshal(processedMenu)
|
||||
if err != nil {
|
||||
return err
|
||||
trays, err := a.menuManager.GetTrayMenus()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if trays != nil {
|
||||
for _, tray := range trays {
|
||||
C.AddTrayMenu(a.app, a.string2CString(tray))
|
||||
}
|
||||
C.SetTray(a.app, a.string2CString(string(trayMenuJSON)), a.string2CString(tray.Label), a.string2CString(tray.Icon))
|
||||
}
|
||||
|
||||
// Process context menus
|
||||
contextMenus := options.GetContextMenus(a.config)
|
||||
contextMenus, err := a.menuManager.GetContextMenus()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if contextMenus != nil {
|
||||
contextMenusJSON, err := json.Marshal(contextMenus)
|
||||
fmt.Printf("\n\nCONTEXT MENUS:\n %+v\n\n", string(contextMenusJSON))
|
||||
if err != nil {
|
||||
return err
|
||||
for _, contextMenu := range contextMenus {
|
||||
C.AddContextMenu(a.app, a.string2CString(contextMenu))
|
||||
}
|
||||
C.SetContextMenus(a.app, a.string2CString(string(contextMenusJSON)))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -2,18 +2,114 @@
|
||||
#ifndef FFENESTRI_DARWIN_H
|
||||
#define FFENESTRI_DARWIN_H
|
||||
|
||||
extern void TitlebarAppearsTransparent(void *);
|
||||
extern void HideTitle(void *);
|
||||
extern void HideTitleBar(void *);
|
||||
extern void FullSizeContent(void *);
|
||||
extern void UseToolbar(void *);
|
||||
extern void HideToolbarSeparator(void *);
|
||||
extern void DisableFrame(void *);
|
||||
extern void SetAppearance(void *, const char *);
|
||||
extern void WebviewIsTransparent(void *);
|
||||
extern void WindowBackgroundIsTranslucent(void *);
|
||||
extern void SetMenu(void *, const char *);
|
||||
extern void SetTray(void *, const char *, const char *, const char *);
|
||||
extern void SetContextMenus(void *, const char *);
|
||||
|
||||
#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 c(str) (id)objc_getClass(str)
|
||||
#define s(str) sel_registerName(str)
|
||||
#define u(str) sel_getUid(str)
|
||||
#define str(input) msg(c("NSString"), s("stringWithUTF8String:"), input)
|
||||
#define strunicode(input) msg(c("NSString"), s("stringWithFormat:"), str("%C"), (unsigned short)input)
|
||||
#define cstr(input) (const char *)msg(input, s("UTF8String"))
|
||||
#define url(input) msg(c("NSURL"), s("fileURLWithPath:"), str(input))
|
||||
#define ALLOC(classname) msg(c(classname), s("alloc"))
|
||||
#define ALLOC_INIT(classname) msg(msg(c(classname), s("alloc")), s("init"))
|
||||
#define GET_FRAME(receiver) ((CGRect(*)(id, SEL))objc_msgSend_stret)(receiver, s("frame"))
|
||||
#define GET_BOUNDS(receiver) ((CGRect(*)(id, SEL))objc_msgSend_stret)(receiver, s("bounds"))
|
||||
#define GET_BACKINGSCALEFACTOR(receiver) ((CGFloat(*)(id, SEL))msg)(receiver, s("backingScaleFactor"))
|
||||
|
||||
#define ON_MAIN_THREAD(str) dispatch( ^{ str; } )
|
||||
#define MAIN_WINDOW_CALL(str) msg(app->mainWindow, s((str)))
|
||||
|
||||
#define NSBackingStoreBuffered 2
|
||||
|
||||
#define NSWindowStyleMaskBorderless 0
|
||||
#define NSWindowStyleMaskTitled 1
|
||||
#define NSWindowStyleMaskClosable 2
|
||||
#define NSWindowStyleMaskMiniaturizable 4
|
||||
#define NSWindowStyleMaskResizable 8
|
||||
#define NSWindowStyleMaskFullscreen 1 << 14
|
||||
|
||||
#define NSVisualEffectMaterialWindowBackground 12
|
||||
#define NSVisualEffectBlendingModeBehindWindow 0
|
||||
#define NSVisualEffectStateFollowsWindowActiveState 0
|
||||
#define NSVisualEffectStateActive 1
|
||||
#define NSVisualEffectStateInactive 2
|
||||
|
||||
#define NSViewWidthSizable 2
|
||||
#define NSViewHeightSizable 16
|
||||
|
||||
#define NSWindowBelow -1
|
||||
#define NSWindowAbove 1
|
||||
|
||||
#define NSSquareStatusItemLength -2.0
|
||||
#define NSVariableStatusItemLength -1.0
|
||||
|
||||
#define NSWindowTitleHidden 1
|
||||
#define NSWindowStyleMaskFullSizeContentView 1 << 15
|
||||
|
||||
#define NSEventModifierFlagCommand 1 << 20
|
||||
#define NSEventModifierFlagOption 1 << 19
|
||||
#define NSEventModifierFlagControl 1 << 18
|
||||
#define NSEventModifierFlagShift 1 << 17
|
||||
|
||||
#define NSControlStateValueMixed -1
|
||||
#define NSControlStateValueOff 0
|
||||
#define NSControlStateValueOn 1
|
||||
|
||||
// Unbelievably, if the user swaps their button preference
|
||||
// then right buttons are reported as left buttons
|
||||
#define NSEventMaskLeftMouseDown 1 << 1
|
||||
#define NSEventMaskLeftMouseUp 1 << 2
|
||||
#define NSEventMaskRightMouseDown 1 << 3
|
||||
#define NSEventMaskRightMouseUp 1 << 4
|
||||
|
||||
#define NSEventTypeLeftMouseDown 1
|
||||
#define NSEventTypeLeftMouseUp 2
|
||||
#define NSEventTypeRightMouseDown 3
|
||||
#define NSEventTypeRightMouseUp 4
|
||||
|
||||
#define NSNoImage 0
|
||||
#define NSImageOnly 1
|
||||
#define NSImageLeft 2
|
||||
#define NSImageRight 3
|
||||
#define NSImageBelow 4
|
||||
#define NSImageAbove 5
|
||||
#define NSImageOverlaps 6
|
||||
|
||||
#define NSAlertStyleWarning 0
|
||||
#define NSAlertStyleInformational 1
|
||||
#define NSAlertStyleCritical 2
|
||||
|
||||
#define NSAlertFirstButtonReturn 1000
|
||||
#define NSAlertSecondButtonReturn 1001
|
||||
#define NSAlertThirdButtonReturn 1002
|
||||
|
||||
struct Application;
|
||||
int releaseNSObject(void *const context, struct hashmap_element_s *const e);
|
||||
void TitlebarAppearsTransparent(struct Application* app);
|
||||
void HideTitle(struct Application* app);
|
||||
void HideTitleBar(struct Application* app);
|
||||
void FullSizeContent(struct Application* app);
|
||||
void UseToolbar(struct Application* app);
|
||||
void HideToolbarSeparator(struct Application* app);
|
||||
void DisableFrame(struct Application* app);
|
||||
void SetAppearance(struct Application* app, const char *);
|
||||
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* lookupStringConstant(id constantName);
|
||||
|
||||
#endif
|
||||
@@ -1,80 +0,0 @@
|
||||
package ffenestri
|
||||
|
||||
import "github.com/wailsapp/wails/v2/pkg/menu"
|
||||
|
||||
// ProcessedMenu is the original menu with the addition
|
||||
// of radio groups extracted from the menu data
|
||||
type ProcessedMenu struct {
|
||||
Menu *menu.Menu
|
||||
RadioGroups []*RadioGroup
|
||||
currentRadioGroup []string
|
||||
}
|
||||
|
||||
// RadioGroup holds all the members of the same radio group
|
||||
type RadioGroup struct {
|
||||
Members []string
|
||||
Length int
|
||||
}
|
||||
|
||||
// NewProcessedMenu processed the given menu and returns
|
||||
// the original menu with the extracted radio groups
|
||||
func NewProcessedMenu(menu *menu.Menu) *ProcessedMenu {
|
||||
result := &ProcessedMenu{
|
||||
Menu: menu,
|
||||
RadioGroups: []*RadioGroup{},
|
||||
currentRadioGroup: []string{},
|
||||
}
|
||||
|
||||
result.processMenu()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (p *ProcessedMenu) processMenu() {
|
||||
// Loop over top level menus
|
||||
for _, item := range p.Menu.Items {
|
||||
// Process MenuItem
|
||||
p.processMenuItem(item)
|
||||
}
|
||||
|
||||
p.finaliseRadioGroup()
|
||||
}
|
||||
|
||||
func (p *ProcessedMenu) processMenuItem(item *menu.MenuItem) {
|
||||
|
||||
switch item.Type {
|
||||
|
||||
// We need to recurse submenus
|
||||
case menu.SubmenuType:
|
||||
|
||||
// Finalise any current radio groups as they don't trickle down to submenus
|
||||
p.finaliseRadioGroup()
|
||||
|
||||
// Process each submenu item
|
||||
for _, subitem := range item.SubMenu {
|
||||
p.processMenuItem(subitem)
|
||||
}
|
||||
case menu.RadioType:
|
||||
// Add the item to the radio group
|
||||
p.currentRadioGroup = append(p.currentRadioGroup, item.ID)
|
||||
default:
|
||||
p.finaliseRadioGroup()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProcessedMenu) finaliseRadioGroup() {
|
||||
|
||||
// If we were processing a radio group, fix up the references
|
||||
if len(p.currentRadioGroup) > 0 {
|
||||
|
||||
// Create new radiogroup
|
||||
group := &RadioGroup{
|
||||
Members: p.currentRadioGroup,
|
||||
Length: len(p.currentRadioGroup),
|
||||
}
|
||||
p.RadioGroups = append(p.RadioGroups, group)
|
||||
|
||||
// Empty the radio group
|
||||
p.currentRadioGroup = []string{}
|
||||
}
|
||||
}
|
||||
885
v2/internal/ffenestri/menu_darwin.c
Normal file
885
v2/internal/ffenestri/menu_darwin.c
Normal file
@@ -0,0 +1,885 @@
|
||||
//
|
||||
// Created by Lea Anthony on 6/1/21.
|
||||
//
|
||||
|
||||
#include "ffenestri_darwin.h"
|
||||
#include "menu_darwin.h"
|
||||
#include "contextmenus_darwin.h"
|
||||
#include "common.h"
|
||||
|
||||
// NewMenu creates a new Menu struct, saving the given menu structure as JSON
|
||||
Menu* NewMenu(JsonNode *menuData) {
|
||||
|
||||
Menu *result = malloc(sizeof(Menu));
|
||||
|
||||
result->processedMenu = menuData;
|
||||
|
||||
// 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->parentData = NULL;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Menu* NewApplicationMenu(const char *menuAsJSON) {
|
||||
|
||||
// Parse the menu json
|
||||
JsonNode *processedMenu = json_decode(menuAsJSON);
|
||||
if( processedMenu == NULL ) {
|
||||
// Parse error!
|
||||
ABORT("Unable to parse Menu JSON: %s", menuAsJSON);
|
||||
}
|
||||
|
||||
Menu *result = NewMenu(processedMenu);
|
||||
result->menuType = ApplicationMenuType;
|
||||
return result;
|
||||
}
|
||||
|
||||
MenuItemCallbackData* CreateMenuItemCallbackData(Menu *menu, id menuItem, const char *menuID, enum MenuItemType menuItemType) {
|
||||
MenuItemCallbackData* result = malloc(sizeof(MenuItemCallbackData));
|
||||
|
||||
result->menu = menu;
|
||||
result->menuID = menuID;
|
||||
result->menuItem = menuItem;
|
||||
result->menuItemType = menuItemType;
|
||||
|
||||
// Store reference to this so we can destroy later
|
||||
vec_push(&menu->callbackDataCache, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void DeleteMenu(Menu *menu) {
|
||||
|
||||
// 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);
|
||||
|
||||
// Free up the processed menu memory
|
||||
if (menu->processedMenu != NULL) {
|
||||
json_delete(menu->processedMenu);
|
||||
menu->processedMenu = NULL;
|
||||
}
|
||||
|
||||
// Release the vector memory
|
||||
vec_deinit(&menu->callbackDataCache);
|
||||
|
||||
// Free nsmenu if we have it
|
||||
if ( menu->menu != NULL ) {
|
||||
msg(menu->menu, s("release"));
|
||||
}
|
||||
|
||||
free(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) {
|
||||
|
||||
JsonNode *jsonObject = json_mkobject();
|
||||
if (menuItemID == NULL ) {
|
||||
ABORT("Item ID NULL for menu!!\n");
|
||||
}
|
||||
json_append_member(jsonObject, "menuItemID", json_mkstring(menuItemID));
|
||||
json_append_member(jsonObject, "menuType", json_mkstring(MenuTypeAsString[(int)menuType]));
|
||||
if (data != NULL) {
|
||||
json_append_member(jsonObject, "data", json_mkstring(data));
|
||||
}
|
||||
if (parentID != NULL) {
|
||||
json_append_member(jsonObject, "parentID", json_mkstring(parentID));
|
||||
}
|
||||
const char *payload = json_encode(jsonObject);
|
||||
json_delete(jsonObject);
|
||||
const char *result = concat("MC", payload);
|
||||
MEMFREE(payload);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Callback for text menu items
|
||||
void menuItemCallback(id self, SEL cmd, id sender) {
|
||||
MenuItemCallbackData *callbackData = (MenuItemCallbackData *)msg(msg(sender, s("representedObject")), s("pointerValue"));
|
||||
const char *message;
|
||||
|
||||
// Update checkbox / radio item
|
||||
if( callbackData->menuItemType == Checkbox) {
|
||||
// Toggle state
|
||||
bool state = msg(callbackData->menuItem, s("state"));
|
||||
msg(callbackData->menuItem, s("setState:"), (state? NSControlStateValueOff : NSControlStateValueOn));
|
||||
} else if( callbackData->menuItemType == Radio ) {
|
||||
// Check the menu items' current state
|
||||
bool selected = msg(callbackData->menuItem, s("state"));
|
||||
|
||||
// If it's already selected, exit early
|
||||
if (selected) return;
|
||||
|
||||
// Get this item's radio group members and turn them off
|
||||
id *members = (id*)hashmap_get(&(callbackData->menu->radioGroupMap), (char*)callbackData->menuID, strlen(callbackData->menuID));
|
||||
|
||||
// Uncheck all members of the group
|
||||
id thisMember = members[0];
|
||||
int count = 0;
|
||||
while(thisMember != NULL) {
|
||||
msg(thisMember, s("setState:"), NSControlStateValueOff);
|
||||
count = count + 1;
|
||||
thisMember = members[count];
|
||||
}
|
||||
|
||||
// check the selected menu item
|
||||
msg(callbackData->menuItem, s("setState:"), NSControlStateValueOn);
|
||||
}
|
||||
|
||||
const char *menuID = callbackData->menuID;
|
||||
const char *data = NULL;
|
||||
enum MenuType menuType = callbackData->menu->menuType;
|
||||
const char *parentID = NULL;
|
||||
|
||||
// Generate message to send to backend
|
||||
if( menuType == ContextMenuType ) {
|
||||
// Get the context menu data from the menu
|
||||
ContextMenu* contextMenu = (ContextMenu*) callbackData->menu->parentData;
|
||||
data = contextMenu->contextMenuData;
|
||||
parentID = contextMenu->ID;
|
||||
} else if ( menuType == TrayMenuType ) {
|
||||
parentID = (const char*) callbackData->menu->parentData;
|
||||
}
|
||||
|
||||
message = createMenuClickedMessage(menuID, data, menuType, parentID);
|
||||
|
||||
// Notify the backend
|
||||
messageFromWindowCallback(message);
|
||||
MEMFREE(message);
|
||||
}
|
||||
|
||||
id processAcceleratorKey(const char *key) {
|
||||
|
||||
// Guard against no accelerator key
|
||||
if( key == NULL ) {
|
||||
return str("");
|
||||
}
|
||||
|
||||
if( STREQ(key, "Backspace") ) {
|
||||
return strunicode(0x0008);
|
||||
}
|
||||
if( STREQ(key, "Tab") ) {
|
||||
return strunicode(0x0009);
|
||||
}
|
||||
if( STREQ(key, "Return") ) {
|
||||
return strunicode(0x000d);
|
||||
}
|
||||
if( STREQ(key, "Escape") ) {
|
||||
return strunicode(0x001b);
|
||||
}
|
||||
if( STREQ(key, "Left") ) {
|
||||
return strunicode(0x001c);
|
||||
}
|
||||
if( STREQ(key, "Right") ) {
|
||||
return strunicode(0x001d);
|
||||
}
|
||||
if( STREQ(key, "Up") ) {
|
||||
return strunicode(0x001e);
|
||||
}
|
||||
if( STREQ(key, "Down") ) {
|
||||
return strunicode(0x001f);
|
||||
}
|
||||
if( STREQ(key, "Space") ) {
|
||||
return strunicode(0x0020);
|
||||
}
|
||||
if( STREQ(key, "Delete") ) {
|
||||
return strunicode(0x007f);
|
||||
}
|
||||
if( STREQ(key, "Home") ) {
|
||||
return strunicode(0x2196);
|
||||
}
|
||||
if( STREQ(key, "End") ) {
|
||||
return strunicode(0x2198);
|
||||
}
|
||||
if( STREQ(key, "Page Up") ) {
|
||||
return strunicode(0x21de);
|
||||
}
|
||||
if( STREQ(key, "Page Down") ) {
|
||||
return strunicode(0x21df);
|
||||
}
|
||||
if( STREQ(key, "F1") ) {
|
||||
return strunicode(0xf704);
|
||||
}
|
||||
if( STREQ(key, "F2") ) {
|
||||
return strunicode(0xf705);
|
||||
}
|
||||
if( STREQ(key, "F3") ) {
|
||||
return strunicode(0xf706);
|
||||
}
|
||||
if( STREQ(key, "F4") ) {
|
||||
return strunicode(0xf707);
|
||||
}
|
||||
if( STREQ(key, "F5") ) {
|
||||
return strunicode(0xf708);
|
||||
}
|
||||
if( STREQ(key, "F6") ) {
|
||||
return strunicode(0xf709);
|
||||
}
|
||||
if( STREQ(key, "F7") ) {
|
||||
return strunicode(0xf70a);
|
||||
}
|
||||
if( STREQ(key, "F8") ) {
|
||||
return strunicode(0xf70b);
|
||||
}
|
||||
if( STREQ(key, "F9") ) {
|
||||
return strunicode(0xf70c);
|
||||
}
|
||||
if( STREQ(key, "F10") ) {
|
||||
return strunicode(0xf70d);
|
||||
}
|
||||
if( STREQ(key, "F11") ) {
|
||||
return strunicode(0xf70e);
|
||||
}
|
||||
if( STREQ(key, "F12") ) {
|
||||
return strunicode(0xf70f);
|
||||
}
|
||||
if( STREQ(key, "F13") ) {
|
||||
return strunicode(0xf710);
|
||||
}
|
||||
if( STREQ(key, "F14") ) {
|
||||
return strunicode(0xf711);
|
||||
}
|
||||
if( STREQ(key, "F15") ) {
|
||||
return strunicode(0xf712);
|
||||
}
|
||||
if( STREQ(key, "F16") ) {
|
||||
return strunicode(0xf713);
|
||||
}
|
||||
if( STREQ(key, "F17") ) {
|
||||
return strunicode(0xf714);
|
||||
}
|
||||
if( STREQ(key, "F18") ) {
|
||||
return strunicode(0xf715);
|
||||
}
|
||||
if( STREQ(key, "F19") ) {
|
||||
return strunicode(0xf716);
|
||||
}
|
||||
if( STREQ(key, "F20") ) {
|
||||
return strunicode(0xf717);
|
||||
}
|
||||
if( STREQ(key, "F21") ) {
|
||||
return strunicode(0xf718);
|
||||
}
|
||||
if( STREQ(key, "F22") ) {
|
||||
return strunicode(0xf719);
|
||||
}
|
||||
if( STREQ(key, "F23") ) {
|
||||
return strunicode(0xf71a);
|
||||
}
|
||||
if( STREQ(key, "F24") ) {
|
||||
return strunicode(0xf71b);
|
||||
}
|
||||
if( STREQ(key, "F25") ) {
|
||||
return strunicode(0xf71c);
|
||||
}
|
||||
if( STREQ(key, "F26") ) {
|
||||
return strunicode(0xf71d);
|
||||
}
|
||||
if( STREQ(key, "F27") ) {
|
||||
return strunicode(0xf71e);
|
||||
}
|
||||
if( STREQ(key, "F28") ) {
|
||||
return strunicode(0xf71f);
|
||||
}
|
||||
if( STREQ(key, "F29") ) {
|
||||
return strunicode(0xf720);
|
||||
}
|
||||
if( STREQ(key, "F30") ) {
|
||||
return strunicode(0xf721);
|
||||
}
|
||||
if( STREQ(key, "F31") ) {
|
||||
return strunicode(0xf722);
|
||||
}
|
||||
if( STREQ(key, "F32") ) {
|
||||
return strunicode(0xf723);
|
||||
}
|
||||
if( STREQ(key, "F33") ) {
|
||||
return strunicode(0xf724);
|
||||
}
|
||||
if( STREQ(key, "F34") ) {
|
||||
return strunicode(0xf725);
|
||||
}
|
||||
if( STREQ(key, "F35") ) {
|
||||
return strunicode(0xf726);
|
||||
}
|
||||
// if( STREQ(key, "Insert") ) {
|
||||
// return strunicode(0xf727);
|
||||
// }
|
||||
// if( STREQ(key, "PrintScreen") ) {
|
||||
// return strunicode(0xf72e);
|
||||
// }
|
||||
// if( STREQ(key, "ScrollLock") ) {
|
||||
// return strunicode(0xf72f);
|
||||
// }
|
||||
if( STREQ(key, "NumLock") ) {
|
||||
return strunicode(0xf739);
|
||||
}
|
||||
|
||||
return str(key);
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// This converts a string array of modifiers into the
|
||||
// equivalent MacOS Modifier Flags
|
||||
unsigned long parseModifiers(const char **modifiers) {
|
||||
|
||||
// Our result is a modifier flag list
|
||||
unsigned long result = 0;
|
||||
|
||||
const char *thisModifier = modifiers[0];
|
||||
int count = 0;
|
||||
while( thisModifier != NULL ) {
|
||||
// Determine flags
|
||||
if( STREQ(thisModifier, "CmdOrCtrl") ) {
|
||||
result |= NSEventModifierFlagCommand;
|
||||
}
|
||||
if( STREQ(thisModifier, "OptionOrAlt") ) {
|
||||
result |= NSEventModifierFlagOption;
|
||||
}
|
||||
if( STREQ(thisModifier, "Shift") ) {
|
||||
result |= NSEventModifierFlagShift;
|
||||
}
|
||||
if( STREQ(thisModifier, "Super") ) {
|
||||
result |= NSEventModifierFlagCommand;
|
||||
}
|
||||
if( STREQ(thisModifier, "Control") ) {
|
||||
result |= NSEventModifierFlagControl;
|
||||
}
|
||||
count++;
|
||||
thisModifier = modifiers[count];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
id processRadioMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *acceleratorkey) {
|
||||
id item = ALLOC("NSMenuItem");
|
||||
|
||||
// Store the item in the menu item map
|
||||
hashmap_put(&menu->menuItemMap, (char*)menuid, strlen(menuid), item);
|
||||
|
||||
// Create a MenuItemCallbackData
|
||||
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, item, menuid, 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 *menuid, bool disabled, bool checked, const char *key) {
|
||||
|
||||
id item = ALLOC("NSMenuItem");
|
||||
|
||||
// Store the item in the menu item map
|
||||
hashmap_put(&menu->menuItemMap, (char*)menuid, strlen(menuid), item);
|
||||
|
||||
// Create a MenuItemCallbackData
|
||||
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, item, menuid, Checkbox);
|
||||
|
||||
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), callback);
|
||||
msg(item, s("setRepresentedObject:"), wrappedId);
|
||||
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuItemCallback:"), str(key));
|
||||
msg(item, s("setEnabled:"), !disabled);
|
||||
msg(item, s("autorelease"));
|
||||
msg(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
|
||||
msg(parentmenu, s("addItem:"), item);
|
||||
return item;
|
||||
}
|
||||
|
||||
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers, const char* tooltip, const char* image, const char* fontName, int fontSize, const char* RGBA) {
|
||||
id item = ALLOC("NSMenuItem");
|
||||
|
||||
// Create a MenuItemCallbackData
|
||||
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, item, menuid, Text);
|
||||
|
||||
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), callback);
|
||||
msg(item, s("setRepresentedObject:"), wrappedId);
|
||||
|
||||
id key = processAcceleratorKey(acceleratorkey);
|
||||
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title),
|
||||
s("menuItemCallback:"), key);
|
||||
|
||||
if( tooltip != NULL ) {
|
||||
msg(item, s("setToolTip:"), str(tooltip));
|
||||
}
|
||||
|
||||
// Process image
|
||||
if( image != NULL && strlen(image) > 0) {
|
||||
id data = ALLOC("NSData");
|
||||
id imageData = msg(data, s("initWithBase64EncodedString:options:"), str(image), 0);
|
||||
id nsimage = ALLOC("NSImage");
|
||||
msg(nsimage, s("initWithData:"), imageData);
|
||||
msg(item, s("setImage:"), nsimage);
|
||||
}
|
||||
|
||||
// Process Menu Item attributes
|
||||
id dictionary = ALLOC_INIT("NSMutableDictionary");
|
||||
|
||||
// Process font
|
||||
id font;
|
||||
CGFloat fontSizeFloat = (CGFloat)fontSize;
|
||||
|
||||
// Check if valid
|
||||
id fontNameAsNSString = str(fontName);
|
||||
id fontsOnSystem = msg(msg(c("NSFontManager"), s("sharedFontManager")), s("availableFonts"));
|
||||
bool valid = msg(fontsOnSystem, s("containsObject:"), fontNameAsNSString);
|
||||
if( valid ) {
|
||||
font = msg(c("NSFont"), s("fontWithName:size:"), fontNameAsNSString, fontSizeFloat);
|
||||
} else {
|
||||
bool supportsMonospacedDigitSystemFont = (bool) msg(c("NSFont"), s("respondsToSelector:"), s("monospacedDigitSystemFontOfSize:weight:"));
|
||||
if( supportsMonospacedDigitSystemFont ) {
|
||||
font = msg(c("NSFont"), s("monospacedDigitSystemFontOfSize:weight:"), fontSizeFloat, NSFontWeightRegular);
|
||||
} else {
|
||||
font = msg(c("NSFont"), s("menuFontOfSize:"), fontSizeFloat);
|
||||
}
|
||||
}
|
||||
|
||||
// Add font to dictionary
|
||||
msg(dictionary, s("setObject:forKey:"), font, lookupStringConstant(str("NSFontAttributeName")));
|
||||
|
||||
// Add offset to dictionary
|
||||
id offset = msg(c("NSNumber"), s("numberWithFloat:"), 0.0);
|
||||
msg(dictionary, s("setObject:forKey:"), offset, lookupStringConstant(str("NSBaselineOffsetAttributeName")));
|
||||
|
||||
// RGBA
|
||||
if( RGBA != NULL && strlen(RGBA) > 0) {
|
||||
unsigned short r, g, b, a;
|
||||
|
||||
// white by default
|
||||
r = g = b = a = 255;
|
||||
int count = sscanf(RGBA, "#%02hx%02hx%02hx%02hx", &r, &g, &b, &a);
|
||||
if (count > 0) {
|
||||
id colour = msg(c("NSColor"), s("colorWithCalibratedRed:green:blue:alpha:"),
|
||||
(float)r / 255.0,
|
||||
(float)g / 255.0,
|
||||
(float)b / 255.0,
|
||||
(float)a / 255.0);
|
||||
msg(dictionary, s("setObject:forKey:"), colour, lookupStringConstant(str("NSForegroundColorAttributeName")));
|
||||
msg(colour, s("release"));
|
||||
}
|
||||
}
|
||||
|
||||
id attributedString = ALLOC("NSMutableAttributedString");
|
||||
msg(attributedString, s("initWithString:attributes:"), str(title), dictionary);
|
||||
msg(dictionary, s("release"));
|
||||
|
||||
msg(item, s("setAttributedTitle:"), attributedString);
|
||||
msg(attributedString, s("autorelease"));
|
||||
|
||||
msg(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 = false;
|
||||
getJSONBool(item, "Hidden", &hidden);
|
||||
if( hidden ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the role
|
||||
JsonNode *role = json_find_member(item, "Role");
|
||||
if( role != NULL ) {
|
||||
processMenuRole(menu, parentMenu, role);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a submenu
|
||||
JsonNode *submenu = json_find_member(item, "SubMenu");
|
||||
if( submenu != NULL ) {
|
||||
// Get the label
|
||||
JsonNode *menuNameNode = json_find_member(item, "Label");
|
||||
const char *name = "";
|
||||
if ( menuNameNode != NULL) {
|
||||
name = menuNameNode->string_;
|
||||
}
|
||||
|
||||
id thisMenuItem = createMenuItemNoAutorelease(str(name), NULL, "");
|
||||
id thisMenu = createMenu(str(name));
|
||||
|
||||
msg(thisMenuItem, s("setSubmenu:"), thisMenu);
|
||||
msg(parentMenu, s("addItem:"), thisMenuItem);
|
||||
|
||||
JsonNode *submenuItems = json_find_member(submenu, "Items");
|
||||
// If we have no items, just return
|
||||
if ( submenuItems == NULL ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Loop over submenu items
|
||||
JsonNode *item;
|
||||
json_foreach(item, submenuItems) {
|
||||
// Get item label
|
||||
processMenuItem(menu, thisMenu, item);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// This is a user menu. Get the common data
|
||||
// Get the label
|
||||
const char *label = getJSONString(item, "Label");
|
||||
if ( label == NULL) {
|
||||
label = "(empty)";
|
||||
}
|
||||
|
||||
const char *menuid = getJSONString(item, "ID");
|
||||
if ( menuid == NULL) {
|
||||
menuid = "";
|
||||
}
|
||||
|
||||
bool disabled = false;
|
||||
getJSONBool(item, "Disabled", &disabled);
|
||||
|
||||
// Get the Accelerator
|
||||
JsonNode *accelerator = json_find_member(item, "Accelerator");
|
||||
const char *acceleratorkey = NULL;
|
||||
const char **modifiers = NULL;
|
||||
|
||||
const char *tooltip = getJSONString(item, "Tooltip");
|
||||
const char *image = getJSONString(item, "Image");
|
||||
const char *fontName = getJSONString(item, "FontName");
|
||||
const char *RGBA = getJSONString(item, "RGBA");
|
||||
int fontSize = 12;
|
||||
getJSONInt(item, "FontSize", &fontSize);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the Type
|
||||
JsonNode *type = json_find_member(item, "Type");
|
||||
if( type != NULL ) {
|
||||
|
||||
if( STREQ(type->string_, "Text")) {
|
||||
processTextMenuItem(menu, parentMenu, label, menuid, disabled, acceleratorkey, modifiers, tooltip, image, fontName, fontSize, RGBA);
|
||||
}
|
||||
else if ( STREQ(type->string_, "Separator")) {
|
||||
addSeparator(parentMenu);
|
||||
}
|
||||
else if ( STREQ(type->string_, "Checkbox")) {
|
||||
// Get checked state
|
||||
bool checked = false;
|
||||
getJSONBool(item, "Checked", &checked);
|
||||
|
||||
processCheckboxMenuItem(menu, parentMenu, label, menuid, disabled, checked, "");
|
||||
}
|
||||
else if ( STREQ(type->string_, "Radio")) {
|
||||
// Get checked state
|
||||
bool checked = false;
|
||||
getJSONBool(item, "Checked", &checked);
|
||||
|
||||
processRadioMenuItem(menu, parentMenu, label, menuid, disabled, checked, "");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ( modifiers != NULL ) {
|
||||
free(modifiers);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
void processMenuData(Menu *menu, JsonNode *menuData) {
|
||||
JsonNode *items = json_find_member(menuData, "Items");
|
||||
if( items == NULL ) {
|
||||
// Parse error!
|
||||
ABORT("Unable to find 'Items' in menu JSON!");
|
||||
}
|
||||
|
||||
// Iterate items
|
||||
JsonNode *item;
|
||||
json_foreach(item, items) {
|
||||
// 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 GetMenu(Menu *menu) {
|
||||
|
||||
// Pull out the menu data
|
||||
JsonNode *menuData = json_find_member(menu->processedMenu, "Menu");
|
||||
if( menuData == NULL ) {
|
||||
ABORT("Unable to find Menu data: %s", menu->processedMenu);
|
||||
}
|
||||
|
||||
menu->menu = createMenu(str(""));
|
||||
|
||||
// Process the menu data
|
||||
processMenuData(menu, menuData);
|
||||
|
||||
// Create the radiogroup cache
|
||||
JsonNode *radioGroups = json_find_member(menu->processedMenu, "RadioGroups");
|
||||
if( radioGroups == NULL ) {
|
||||
// Parse error!
|
||||
ABORT("Unable to find RadioGroups data: %s", menu->processedMenu);
|
||||
}
|
||||
|
||||
// Iterate radio groups
|
||||
JsonNode *radioGroup;
|
||||
json_foreach(radioGroup, radioGroups) {
|
||||
// Get item label
|
||||
processRadioGroupJSON(menu, radioGroup);
|
||||
}
|
||||
|
||||
return menu->menu;
|
||||
}
|
||||
|
||||
114
v2/internal/ffenestri/menu_darwin.h
Normal file
114
v2/internal/ffenestri/menu_darwin.h
Normal file
@@ -0,0 +1,114 @@
|
||||
//
|
||||
// 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",
|
||||
};
|
||||
|
||||
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 *);
|
||||
|
||||
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;
|
||||
|
||||
|
||||
typedef struct {
|
||||
id menuItem;
|
||||
Menu *menu;
|
||||
const char *menuID;
|
||||
enum MenuItemType menuItemType;
|
||||
} MenuItemCallbackData;
|
||||
|
||||
|
||||
|
||||
// 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);
|
||||
void processMenuItem(Menu *menu, id parentMenu, JsonNode *item);
|
||||
void processMenuData(Menu *menu, JsonNode *menuData);
|
||||
|
||||
void processRadioGroupJSON(Menu *menu, JsonNode *radioGroup) ;
|
||||
id GetMenu(Menu *menu);
|
||||
#endif //ASSETS_C_MENU_DARWIN_H
|
||||
5
v2/internal/ffenestri/runtime_darwin.c
Normal file
5
v2/internal/ffenestri/runtime_darwin.c
Normal file
File diff suppressed because one or more lines are too long
202
v2/internal/ffenestri/traymenu_darwin.c
Normal file
202
v2/internal/ffenestri/traymenu_darwin.c
Normal file
@@ -0,0 +1,202 @@
|
||||
//
|
||||
// Created by Lea Anthony on 12/1/21.
|
||||
//
|
||||
|
||||
#include "common.h"
|
||||
#include "traymenu_darwin.h"
|
||||
#include "trayicons.h"
|
||||
|
||||
// A cache for all our tray menu icons
|
||||
// Global because it's a singleton
|
||||
struct hashmap_s trayIconCache;
|
||||
|
||||
TrayMenu* NewTrayMenu(const char* menuJSON) {
|
||||
TrayMenu* result = malloc(sizeof(TrayMenu));
|
||||
|
||||
/*
|
||||
{"ID":"0","Label":"Test Tray Label","Icon":"","ProcessedMenu":{"Menu":{"Items":[{"ID":"0","Label":"Show Window","Type":"Text","Disabled":false,"Hidden":false,"Checked":false,"Foreground":0,"Background":0},{"ID":"1","Label":"Hide Window","Type":"Text","Disabled":false,"Hidden":false,"Checked":false,"Foreground":0,"Background":0},{"ID":"2","Label":"Minimise Window","Type":"Text","Disabled":false,"Hidden":false,"Checked":false,"Foreground":0,"Background":0},{"ID":"3","Label":"Unminimise Window","Type":"Text","Disabled":false,"Hidden":false,"Checked":false,"Foreground":0,"Background":0}]},"RadioGroups":null}}
|
||||
*/
|
||||
JsonNode* processedJSON = json_decode(menuJSON);
|
||||
if( processedJSON == NULL ) {
|
||||
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, "Icon");
|
||||
JsonNode* processedMenu = mustJSONObject(processedJSON, "ProcessedMenu");
|
||||
|
||||
// Create the menu
|
||||
result->menu = NewMenu(processedMenu);
|
||||
|
||||
// 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;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void DumpTrayMenu(TrayMenu* trayMenu) {
|
||||
printf(" ['%s':%p] = { label: '%s', icon: '%s', menu: %p, statusbar: %p }\n", trayMenu->ID, trayMenu, trayMenu->label, trayMenu->icon, trayMenu->menu, trayMenu->statusbaritem );
|
||||
}
|
||||
|
||||
|
||||
void UpdateTrayLabel(TrayMenu *trayMenu, const char *label) {
|
||||
|
||||
// Exit early if NULL
|
||||
if( trayMenu->label == NULL ) {
|
||||
return;
|
||||
}
|
||||
// Update button label
|
||||
id statusBarButton = msg(trayMenu->statusbaritem, s("button"));
|
||||
msg(statusBarButton, s("setTitle:"), str(label));
|
||||
}
|
||||
|
||||
void UpdateTrayIcon(TrayMenu *trayMenu) {
|
||||
|
||||
// Exit early if NULL
|
||||
if( trayMenu->icon == NULL ) {
|
||||
return;
|
||||
}
|
||||
|
||||
id statusBarButton = msg(trayMenu->statusbaritem, s("button"));
|
||||
|
||||
// Empty icon means remove it
|
||||
if( STREMPTY(trayMenu->icon) ) {
|
||||
// Remove image
|
||||
msg(statusBarButton, s("setImage:"), NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
id trayImage = hashmap_get(&trayIconCache, trayMenu->icon, strlen(trayMenu->icon));
|
||||
msg(statusBarButton, s("setImagePosition:"), trayMenu->trayIconPosition);
|
||||
msg(statusBarButton, s("setImage:"), trayImage);
|
||||
}
|
||||
|
||||
|
||||
|
||||
void ShowTrayMenu(TrayMenu* trayMenu) {
|
||||
|
||||
// Create a status bar item if we don't have one
|
||||
if( trayMenu->statusbaritem == NULL ) {
|
||||
id statusBar = msg( c("NSStatusBar"), s("systemStatusBar") );
|
||||
trayMenu->statusbaritem = msg(statusBar, s("statusItemWithLength:"), NSVariableStatusItemLength);
|
||||
msg(trayMenu->statusbaritem, s("retain"));
|
||||
|
||||
}
|
||||
|
||||
id 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);
|
||||
|
||||
// Update the menu
|
||||
id menu = GetMenu(trayMenu->menu);
|
||||
msg(trayMenu->statusbaritem, s("setMenu:"), menu);
|
||||
}
|
||||
|
||||
// UpdateTrayMenuInPlace receives 2 menus. The current menu gets
|
||||
// updated with the data from the new menu.
|
||||
void UpdateTrayMenuInPlace(TrayMenu* currentMenu, TrayMenu* newMenu) {
|
||||
|
||||
// Delete the old menu
|
||||
DeleteMenu(currentMenu->menu);
|
||||
|
||||
// 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->trayIconPosition = newMenu->trayIconPosition;
|
||||
currentMenu->icon = newMenu->icon;
|
||||
|
||||
}
|
||||
|
||||
void DeleteTrayMenu(TrayMenu* trayMenu) {
|
||||
|
||||
// printf("Freeing TrayMenu:\n");
|
||||
// DumpTrayMenu(trayMenu);
|
||||
|
||||
// Delete the menu
|
||||
DeleteMenu(trayMenu->menu);
|
||||
|
||||
// Free JSON
|
||||
if (trayMenu->processedJSON != NULL ) {
|
||||
json_delete(trayMenu->processedJSON);
|
||||
}
|
||||
|
||||
// Free the status item
|
||||
if ( trayMenu->statusbaritem != NULL ) {
|
||||
id statusBar = msg( c("NSStatusBar"), s("systemStatusBar") );
|
||||
msg(statusBar, s("removeStatusItem:"), trayMenu->statusbaritem);
|
||||
msg(trayMenu->statusbaritem, s("release"));
|
||||
trayMenu->statusbaritem = NULL;
|
||||
}
|
||||
|
||||
// Free the tray menu memory
|
||||
MEMFREE(trayMenu);
|
||||
}
|
||||
|
||||
void LoadTrayIcons() {
|
||||
|
||||
// Allocate the Tray Icons
|
||||
if( 0 != hashmap_create((const unsigned)4, &trayIconCache)) {
|
||||
// Couldn't allocate map
|
||||
ABORT("Not enough memory to allocate trayIconCache!");
|
||||
}
|
||||
|
||||
unsigned int count = 0;
|
||||
while( 1 ) {
|
||||
const unsigned char *name = trayIcons[count++];
|
||||
if( name == 0x00 ) {
|
||||
break;
|
||||
}
|
||||
const unsigned char *lengthAsString = trayIcons[count++];
|
||||
if( name == 0x00 ) {
|
||||
break;
|
||||
}
|
||||
const unsigned char *data = trayIcons[count++];
|
||||
if( data == 0x00 ) {
|
||||
break;
|
||||
}
|
||||
int length = atoi((const char *)lengthAsString);
|
||||
|
||||
// Create the icon and add to the hashmap
|
||||
id imageData = msg(c("NSData"), s("dataWithBytes:length:"), data, length);
|
||||
id trayImage = ALLOC("NSImage");
|
||||
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);
|
||||
}
|
||||
38
v2/internal/ffenestri/traymenu_darwin.h
Normal file
38
v2/internal/ffenestri/traymenu_darwin.h
Normal file
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// Created by Lea Anthony on 12/1/21.
|
||||
//
|
||||
|
||||
#ifndef TRAYMENU_DARWIN_H
|
||||
#define TRAYMENU_DARWIN_H
|
||||
|
||||
#include "common.h"
|
||||
#include "menu_darwin.h"
|
||||
|
||||
typedef struct {
|
||||
|
||||
const char *label;
|
||||
const char *icon;
|
||||
const char *ID;
|
||||
|
||||
Menu* menu;
|
||||
|
||||
id statusbaritem;
|
||||
int trayIconPosition;
|
||||
|
||||
JsonNode* processedJSON;
|
||||
|
||||
} TrayMenu;
|
||||
|
||||
TrayMenu* NewTrayMenu(const char *trayJSON);
|
||||
void DumpTrayMenu(TrayMenu* trayMenu);
|
||||
void ShowTrayMenu(TrayMenu* trayMenu);
|
||||
void UpdateTrayMenuInPlace(TrayMenu* currentMenu, TrayMenu* newMenu);
|
||||
void UpdateTrayIcon(TrayMenu *trayMenu);
|
||||
void UpdateTrayLabel(TrayMenu *trayMenu, const char*);
|
||||
|
||||
void LoadTrayIcons();
|
||||
void UnloadTrayIcons();
|
||||
|
||||
void DeleteTrayMenu(TrayMenu* trayMenu);
|
||||
|
||||
#endif //TRAYMENU_DARWIN_H
|
||||
133
v2/internal/ffenestri/traymenustore_darwin.c
Normal file
133
v2/internal/ffenestri/traymenustore_darwin.c
Normal file
@@ -0,0 +1,133 @@
|
||||
//
|
||||
// Created by Lea Anthony on 12/1/21.
|
||||
//
|
||||
|
||||
#include "common.h"
|
||||
#include "traymenustore_darwin.h"
|
||||
#include "traymenu_darwin.h"
|
||||
#include <stdlib.h>
|
||||
|
||||
TrayMenuStore* NewTrayMenuStore() {
|
||||
|
||||
TrayMenuStore* result = malloc(sizeof(TrayMenuStore));
|
||||
|
||||
// Allocate Tray Menu Store
|
||||
if( 0 != hashmap_create((const unsigned)4, &result->trayMenuMap)) {
|
||||
ABORT("[NewTrayMenuStore] Not enough memory to allocate trayMenuMap!");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int dumpTrayMenu(void *const context, struct hashmap_element_s *const e) {
|
||||
DumpTrayMenu(e->data);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void DumpTrayMenuStore(TrayMenuStore* store) {
|
||||
hashmap_iterate_pairs(&store->trayMenuMap, dumpTrayMenu, NULL);
|
||||
}
|
||||
|
||||
void AddTrayMenuToStore(TrayMenuStore* store, const char* menuJSON) {
|
||||
TrayMenu* newMenu = NewTrayMenu(menuJSON);
|
||||
|
||||
//TODO: check if there is already an entry for this menu
|
||||
hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), 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 ShowTrayMenusInStore(TrayMenuStore* store) {
|
||||
if( hashmap_num_entries(&store->trayMenuMap) > 0 ) {
|
||||
hashmap_iterate_pairs(&store->trayMenuMap, showTrayMenu, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int freeTrayMenu(void *const context, struct hashmap_element_s *const e) {
|
||||
DeleteTrayMenu(e->data);
|
||||
return -1;
|
||||
}
|
||||
|
||||
void DeleteTrayMenuStore(TrayMenuStore *store) {
|
||||
|
||||
// Delete context menus
|
||||
if (hashmap_num_entries(&store->trayMenuMap) > 0) {
|
||||
if (0 != hashmap_iterate_pairs(&store->trayMenuMap, freeTrayMenu, NULL)) {
|
||||
ABORT("[DeleteContextMenuStore] Failed to release contextMenuStore entries!");
|
||||
}
|
||||
}
|
||||
|
||||
// Destroy tray menu map
|
||||
hashmap_destroy(&store->trayMenuMap);
|
||||
}
|
||||
|
||||
TrayMenu* GetTrayMenuFromStore(TrayMenuStore* store, const char* menuID) {
|
||||
// Get the current menu
|
||||
return hashmap_get(&store->trayMenuMap, menuID, strlen(menuID));
|
||||
}
|
||||
|
||||
TrayMenu* MustGetTrayMenuFromStore(TrayMenuStore* store, const char* menuID) {
|
||||
// Get the current menu
|
||||
TrayMenu* result = hashmap_get(&store->trayMenuMap, menuID, strlen(menuID));
|
||||
if (result == NULL ) {
|
||||
ABORT("Unable to find TrayMenu with ID '%s' in the TrayMenuStore!", menuID);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON) {
|
||||
// Parse the JSON
|
||||
JsonNode *parsedUpdate = mustParseJSON(JSON);
|
||||
|
||||
// Get the data out
|
||||
const char* ID = mustJSONString(parsedUpdate, "ID");
|
||||
const char* Label = mustJSONString(parsedUpdate, "Label");
|
||||
|
||||
// Check we have this menu
|
||||
TrayMenu *menu = MustGetTrayMenuFromStore(store, ID);
|
||||
UpdateTrayLabel(menu, Label);
|
||||
|
||||
}
|
||||
|
||||
void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON) {
|
||||
TrayMenu* newMenu = NewTrayMenu(menuJSON);
|
||||
// DumpTrayMenu(newMenu);
|
||||
|
||||
// Get the current menu
|
||||
TrayMenu *currentMenu = GetTrayMenuFromStore(store, newMenu->ID);
|
||||
|
||||
// If we don't have a menu, we create one
|
||||
if ( currentMenu == NULL ) {
|
||||
// Store the new menu
|
||||
hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
|
||||
|
||||
// Show it
|
||||
ShowTrayMenu(newMenu);
|
||||
return;
|
||||
}
|
||||
// DumpTrayMenu(currentMenu);
|
||||
|
||||
// Save the status bar reference
|
||||
newMenu->statusbaritem = currentMenu->statusbaritem;
|
||||
|
||||
hashmap_remove(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID));
|
||||
|
||||
// Delete the current menu
|
||||
DeleteMenu(currentMenu->menu);
|
||||
currentMenu->menu = NULL;
|
||||
|
||||
// Free the tray menu memory
|
||||
MEMFREE(currentMenu);
|
||||
|
||||
hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
|
||||
|
||||
// Show the updated menu
|
||||
ShowTrayMenu(newMenu);
|
||||
|
||||
}
|
||||
27
v2/internal/ffenestri/traymenustore_darwin.h
Normal file
27
v2/internal/ffenestri/traymenustore_darwin.h
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// Created by Lea Anthony on 7/1/21.
|
||||
//
|
||||
|
||||
#ifndef TRAYMENUSTORE_DARWIN_H
|
||||
#define TRAYMENUSTORE_DARWIN_H
|
||||
|
||||
typedef struct {
|
||||
|
||||
int dummy;
|
||||
|
||||
// This is our tray menu map
|
||||
// It maps tray IDs to TrayMenu*
|
||||
struct hashmap_s trayMenuMap;
|
||||
|
||||
} TrayMenuStore;
|
||||
|
||||
TrayMenuStore* NewTrayMenuStore();
|
||||
|
||||
void AddTrayMenuToStore(TrayMenuStore* store, const char* menuJSON);
|
||||
void UpdateTrayMenuInStore(TrayMenuStore* store, const char* menuJSON);
|
||||
void ShowTrayMenusInStore(TrayMenuStore* store);
|
||||
void DeleteTrayMenuStore(TrayMenuStore* store);
|
||||
|
||||
void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON);
|
||||
|
||||
#endif //TRAYMENUSTORE_DARWIN_H
|
||||
103
v2/internal/github/github.go
Normal file
103
v2/internal/github/github.go
Normal file
@@ -0,0 +1,103 @@
|
||||
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
|
||||
}
|
||||
106
v2/internal/github/semver.go
Normal file
106
v2/internal/github/semver.go
Normal file
@@ -0,0 +1,106 @@
|
||||
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]
|
||||
}
|
||||
@@ -156,7 +156,7 @@ func (a *AssetBundle) WriteToCFile(targetDir string) (string, error) {
|
||||
var cdata strings.Builder
|
||||
|
||||
// Write header
|
||||
header := `// assets.c
|
||||
header := `// assets.h
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Ă‚ MODIWL.
|
||||
// This file was auto-generated. DO NOT MODIFY.
|
||||
|
||||
@@ -185,7 +185,7 @@ func (a *AssetBundle) WriteToCFile(targetDir string) (string, error) {
|
||||
}
|
||||
|
||||
// Save file
|
||||
assetsFile := filepath.Join(targetDir, "assets.c")
|
||||
assetsFile := filepath.Join(targetDir, "assets.h")
|
||||
err = ioutil.WriteFile(assetsFile, []byte(cdata.String()), 0600)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
46
v2/internal/menumanager/applicationmenu.go
Normal file
46
v2/internal/menumanager/applicationmenu.go
Normal file
@@ -0,0 +1,46 @@
|
||||
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
|
||||
}
|
||||
60
v2/internal/menumanager/contextmenu.go
Normal file
60
v2/internal/menumanager/contextmenu.go
Normal file
@@ -0,0 +1,60 @@
|
||||
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
|
||||
}
|
||||
|
||||
func (m *Manager) AddContextMenu(contextMenu *menu.ContextMenu) {
|
||||
|
||||
newContextMenu := 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()
|
||||
}
|
||||
75
v2/internal/menumanager/menuitemmap.go
Normal file
75
v2/internal/menumanager/menuitemmap.go
Normal file
@@ -0,0 +1,75 @@
|
||||
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]
|
||||
}
|
||||
90
v2/internal/menumanager/menumanager.go
Normal file
90
v2/internal/menumanager/menumanager.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package menumanager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
|
||||
// The application menu.
|
||||
applicationMenu *menu.Menu
|
||||
applicationMenuJSON string
|
||||
|
||||
// Our application menu mappings
|
||||
applicationMenuItemMap *MenuItemMap
|
||||
|
||||
// Context menus
|
||||
contextMenus map[string]*ContextMenu
|
||||
contextMenuPointers map[*menu.ContextMenu]string
|
||||
|
||||
// Tray menu stores
|
||||
trayMenus map[string]*TrayMenu
|
||||
trayMenuPointers map[*menu.TrayMenu]string
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
if menuItem.Click == nil {
|
||||
// No callback
|
||||
return fmt.Errorf("No callback for menu '%s'", menuItem.Label)
|
||||
}
|
||||
|
||||
// Create new Callback struct
|
||||
callbackData := &menu.CallbackData{
|
||||
MenuItem: menuItem,
|
||||
ContextData: data,
|
||||
}
|
||||
|
||||
// Call back!
|
||||
go menuItem.Click(callbackData)
|
||||
|
||||
return nil
|
||||
}
|
||||
169
v2/internal/menumanager/processedMenu.go
Normal file
169
v2/internal/menumanager/processedMenu.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package menumanager
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"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"`
|
||||
|
||||
// Colour
|
||||
RGBA string `json:",omitempty"`
|
||||
|
||||
// Font
|
||||
FontSize int `json:",omitempty"`
|
||||
FontName string `json:",omitempty"`
|
||||
|
||||
// Image - base64 image data
|
||||
Image string `json:",omitempty"`
|
||||
|
||||
// Tooltip
|
||||
Tooltip string `json:",omitempty"`
|
||||
}
|
||||
|
||||
func NewProcessedMenuItem(menuItemMap *MenuItemMap, menuItem *menu.MenuItem) *ProcessedMenuItem {
|
||||
|
||||
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,
|
||||
Tooltip: menuItem.Tooltip,
|
||||
}
|
||||
|
||||
if menuItem.SubMenu != nil {
|
||||
result.SubMenu = NewProcessedMenu(menuItemMap, menuItem.SubMenu)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
type ProcessedMenu struct {
|
||||
Items []*ProcessedMenuItem
|
||||
}
|
||||
|
||||
func NewProcessedMenu(menuItemMap *MenuItemMap, menu *menu.Menu) *ProcessedMenu {
|
||||
|
||||
result := &ProcessedMenu{}
|
||||
if menu != nil {
|
||||
for _, item := range menu.Items {
|
||||
processedMenuItem := NewProcessedMenuItem(menuItemMap, item)
|
||||
result.Items = append(result.Items, processedMenuItem)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// WailsMenu is the original menu with the addition
|
||||
// of radio groups extracted from the menu data
|
||||
type WailsMenu struct {
|
||||
Menu *ProcessedMenu
|
||||
RadioGroups []*RadioGroup
|
||||
currentRadioGroup []string
|
||||
}
|
||||
|
||||
// RadioGroup holds all the members of the same radio group
|
||||
type RadioGroup struct {
|
||||
Members []string
|
||||
Length int
|
||||
}
|
||||
|
||||
func NewWailsMenu(menuItemMap *MenuItemMap, menu *menu.Menu) *WailsMenu {
|
||||
result := &WailsMenu{}
|
||||
|
||||
// Process the menus
|
||||
result.Menu = NewProcessedMenu(menuItemMap, menu)
|
||||
|
||||
// Process the radio groups
|
||||
result.processRadioGroups()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (w *WailsMenu) AsJSON() (string, error) {
|
||||
|
||||
menuAsJSON, err := json.Marshal(w)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
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) {
|
||||
|
||||
switch item.Type {
|
||||
|
||||
// We need to recurse submenus
|
||||
case menu.SubmenuType:
|
||||
|
||||
// Finalise any current radio groups as they don't trickle down to submenus
|
||||
w.finaliseRadioGroup()
|
||||
|
||||
// Process each submenu item
|
||||
for _, subitem := range item.SubMenu.Items {
|
||||
w.processMenuItem(subitem)
|
||||
}
|
||||
case menu.RadioType:
|
||||
// Add the item to the radio group
|
||||
w.currentRadioGroup = append(w.currentRadioGroup, item.ID)
|
||||
default:
|
||||
w.finaliseRadioGroup()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WailsMenu) finaliseRadioGroup() {
|
||||
|
||||
// If we were processing a radio group, fix up the references
|
||||
if len(w.currentRadioGroup) > 0 {
|
||||
|
||||
// Create new radiogroup
|
||||
group := &RadioGroup{
|
||||
Members: w.currentRadioGroup,
|
||||
Length: len(w.currentRadioGroup),
|
||||
}
|
||||
w.RadioGroups = append(w.RadioGroups, group)
|
||||
|
||||
// Empty the radio group
|
||||
w.currentRadioGroup = []string{}
|
||||
}
|
||||
}
|
||||
134
v2/internal/menumanager/traymenu.go
Normal file
134
v2/internal/menumanager/traymenu.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package menumanager
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var trayMenuID int
|
||||
var trayMenuIDMutex sync.Mutex
|
||||
|
||||
func generateTrayID() string {
|
||||
trayMenuIDMutex.Lock()
|
||||
result := fmt.Sprintf("%d", trayMenuID)
|
||||
trayMenuID++
|
||||
trayMenuIDMutex.Unlock()
|
||||
return result
|
||||
}
|
||||
|
||||
type TrayMenu struct {
|
||||
ID string
|
||||
Label string
|
||||
Icon string
|
||||
menuItemMap *MenuItemMap
|
||||
menu *menu.Menu
|
||||
ProcessedMenu *WailsMenu
|
||||
}
|
||||
|
||||
func (t *TrayMenu) AsJSON() (string, error) {
|
||||
data, err := json.Marshal(t)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
func NewTrayMenu(trayMenu *menu.TrayMenu) *TrayMenu {
|
||||
|
||||
result := &TrayMenu{
|
||||
Label: trayMenu.Label,
|
||||
Icon: trayMenu.Icon,
|
||||
menu: trayMenu.Menu,
|
||||
menuItemMap: NewMenuItemMap(),
|
||||
}
|
||||
|
||||
result.menuItemMap.AddMenu(trayMenu.Menu)
|
||||
result.ProcessedMenu = NewWailsMenu(result.menuItemMap, result.menu)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// Create the updated tray menu
|
||||
updatedTrayMenu := NewTrayMenu(trayMenu)
|
||||
updatedTrayMenu.ID = trayID
|
||||
|
||||
// Save the reference
|
||||
m.trayMenus[trayID] = updatedTrayMenu
|
||||
|
||||
return updatedTrayMenu.AsJSON()
|
||||
}
|
||||
|
||||
func (m *Manager) GetTrayMenus() ([]string, error) {
|
||||
result := []string{}
|
||||
for _, trayMenu := range m.trayMenus {
|
||||
JSON, err := trayMenu.AsJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, JSON)
|
||||
}
|
||||
|
||||
return result, 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
|
||||
}
|
||||
|
||||
update := &LabelUpdate{
|
||||
ID: trayID,
|
||||
Label: trayMenu.Label,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -2,12 +2,11 @@ package messagedispatcher
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/dialog"
|
||||
)
|
||||
|
||||
// Client defines what a frontend client can do
|
||||
@@ -15,9 +14,9 @@ type Client interface {
|
||||
Quit()
|
||||
NotifyEvent(message string)
|
||||
CallResult(message string)
|
||||
OpenDialog(dialogOptions *options.OpenDialog, callbackID string)
|
||||
SaveDialog(dialogOptions *options.SaveDialog, callbackID string)
|
||||
MessageDialog(dialogOptions *options.MessageDialog, callbackID string)
|
||||
OpenDialog(dialogOptions *dialog.OpenDialog, callbackID string)
|
||||
SaveDialog(dialogOptions *dialog.SaveDialog, callbackID string)
|
||||
MessageDialog(dialogOptions *dialog.MessageDialog, callbackID string)
|
||||
WindowSetTitle(title string)
|
||||
WindowShow()
|
||||
WindowHide()
|
||||
@@ -28,15 +27,16 @@ 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)
|
||||
DarkModeEnabled(callbackID string)
|
||||
UpdateMenu(menu *menu.Menu)
|
||||
UpdateTray(menu *menu.Menu)
|
||||
UpdateTrayLabel(label string)
|
||||
UpdateTrayIcon(name string)
|
||||
UpdateContextMenus(contextMenus *menu.ContextMenus)
|
||||
SetApplicationMenu(menuJSON string)
|
||||
SetTrayMenu(trayMenuJSON string)
|
||||
UpdateTrayMenuLabel(JSON string)
|
||||
UpdateContextMenu(contextMenuJSON string)
|
||||
}
|
||||
|
||||
// DispatchClient is what the frontends use to interface with the
|
||||
|
||||
@@ -26,6 +26,10 @@ var messageParsers = map[byte]func(string) (*parsedMessage, error){
|
||||
// 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");
|
||||
}
|
||||
|
||||
parseMethod := messageParsers[message[0]]
|
||||
if parseMethod == nil {
|
||||
return nil, fmt.Errorf("message type '%c' invalid", message[0])
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
// systemMessageParser does what it says on the tin!
|
||||
func systemMessageParser(message string) (*parsedMessage, error) {
|
||||
|
||||
// Sanity check: system messages must be at least 4 bytes
|
||||
if len(message) < 4 {
|
||||
// Sanity check: system messages must be at least 2 bytes
|
||||
if len(message) < 2 {
|
||||
return nil, fmt.Errorf("system message was an invalid length")
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ func systemMessageParser(message string) (*parsedMessage, error) {
|
||||
// Format of system response messages: S<command><callbackID>|<payload>
|
||||
// DarkModeEnabled
|
||||
case 'D':
|
||||
if len(message) < 4 {
|
||||
return nil, fmt.Errorf("system message was an invalid length")
|
||||
}
|
||||
message = message[1:]
|
||||
idx := strings.IndexByte(message, '|')
|
||||
if idx < 0 {
|
||||
@@ -34,6 +37,10 @@ func systemMessageParser(message string) (*parsedMessage, error) {
|
||||
topic := "systemresponse:" + callbackID
|
||||
responseMessage = &parsedMessage{Topic: topic, Data: payloadData == "T"}
|
||||
|
||||
// This is our startup hook - the frontend is now ready
|
||||
case 'S':
|
||||
topic := "hooks:startup"
|
||||
responseMessage = &parsedMessage{Topic: topic, Data: nil}
|
||||
default:
|
||||
return nil, fmt.Errorf("Invalid message to systemMessageParser()")
|
||||
}
|
||||
|
||||
@@ -1,32 +1,30 @@
|
||||
package messagedispatcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||
"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"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
)
|
||||
|
||||
// Dispatcher translates messages received from the frontend
|
||||
// and publishes them onto the service bus
|
||||
type Dispatcher struct {
|
||||
quitChannel <-chan *servicebus.Message
|
||||
resultChannel <-chan *servicebus.Message
|
||||
eventChannel <-chan *servicebus.Message
|
||||
windowChannel <-chan *servicebus.Message
|
||||
dialogChannel <-chan *servicebus.Message
|
||||
systemChannel <-chan *servicebus.Message
|
||||
menuChannel <-chan *servicebus.Message
|
||||
contextMenuChannel <-chan *servicebus.Message
|
||||
trayChannel <-chan *servicebus.Message
|
||||
running bool
|
||||
quitChannel <-chan *servicebus.Message
|
||||
resultChannel <-chan *servicebus.Message
|
||||
eventChannel <-chan *servicebus.Message
|
||||
windowChannel <-chan *servicebus.Message
|
||||
dialogChannel <-chan *servicebus.Message
|
||||
systemChannel <-chan *servicebus.Message
|
||||
menuChannel <-chan *servicebus.Message
|
||||
|
||||
servicebus *servicebus.ServiceBus
|
||||
logger logger.CustomLogger
|
||||
@@ -34,6 +32,13 @@ 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.
|
||||
@@ -78,29 +83,22 @@ func New(servicebus *servicebus.ServiceBus, logger *logger.Logger) (*Dispatcher,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contextMenuChannel, err := servicebus.Subscribe("contextmenufrontend:")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trayChannel, err := servicebus.Subscribe("trayfrontend:")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Create context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
result := &Dispatcher{
|
||||
servicebus: servicebus,
|
||||
eventChannel: eventChannel,
|
||||
logger: logger.CustomLogger("Message Dispatcher"),
|
||||
clients: make(map[string]*DispatchClient),
|
||||
resultChannel: resultChannel,
|
||||
quitChannel: quitChannel,
|
||||
windowChannel: windowChannel,
|
||||
dialogChannel: dialogChannel,
|
||||
systemChannel: systemChannel,
|
||||
menuChannel: menuChannel,
|
||||
trayChannel: trayChannel,
|
||||
contextMenuChannel: contextMenuChannel,
|
||||
servicebus: servicebus,
|
||||
eventChannel: eventChannel,
|
||||
logger: logger.CustomLogger("Message Dispatcher"),
|
||||
clients: make(map[string]*DispatchClient),
|
||||
resultChannel: resultChannel,
|
||||
quitChannel: quitChannel,
|
||||
windowChannel: windowChannel,
|
||||
dialogChannel: dialogChannel,
|
||||
systemChannel: systemChannel,
|
||||
menuChannel: menuChannel,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
@@ -111,15 +109,18 @@ func (d *Dispatcher) Start() error {
|
||||
|
||||
d.logger.Trace("Starting")
|
||||
|
||||
d.running = true
|
||||
d.wg.Add(1)
|
||||
|
||||
// Spin off a go routine
|
||||
go func() {
|
||||
for d.running {
|
||||
defer d.logger.Trace("Shutdown")
|
||||
for {
|
||||
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:
|
||||
@@ -132,15 +133,8 @@ func (d *Dispatcher) Start() error {
|
||||
d.processSystemMessage(systemMessage)
|
||||
case menuMessage := <-d.menuChannel:
|
||||
d.processMenuMessage(menuMessage)
|
||||
case contextMenuMessage := <-d.contextMenuChannel:
|
||||
d.processContextMenuMessage(contextMenuMessage)
|
||||
case trayMessage := <-d.trayChannel:
|
||||
d.processTrayMessage(trayMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// Call shutdown
|
||||
d.shutdown()
|
||||
}()
|
||||
|
||||
return nil
|
||||
@@ -154,10 +148,6 @@ 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 {
|
||||
@@ -368,6 +358,38 @@ 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)
|
||||
}
|
||||
@@ -389,7 +411,7 @@ func (d *Dispatcher) processDialogMessage(result *servicebus.Message) {
|
||||
dialogType := splitTopic[2]
|
||||
switch dialogType {
|
||||
case "open":
|
||||
dialogOptions, ok := result.Data().(*options.OpenDialog)
|
||||
dialogOptions, ok := result.Data().(*dialog.OpenDialog)
|
||||
if !ok {
|
||||
d.logger.Error("Invalid data for 'dialog:select:open' : %#v", result.Data())
|
||||
return
|
||||
@@ -403,7 +425,7 @@ func (d *Dispatcher) processDialogMessage(result *servicebus.Message) {
|
||||
client.frontend.OpenDialog(dialogOptions, callbackID)
|
||||
}
|
||||
case "save":
|
||||
dialogOptions, ok := result.Data().(*options.SaveDialog)
|
||||
dialogOptions, ok := result.Data().(*dialog.SaveDialog)
|
||||
if !ok {
|
||||
d.logger.Error("Invalid data for 'dialog:select:save' : %#v", result.Data())
|
||||
return
|
||||
@@ -417,7 +439,7 @@ func (d *Dispatcher) processDialogMessage(result *servicebus.Message) {
|
||||
client.frontend.SaveDialog(dialogOptions, callbackID)
|
||||
}
|
||||
case "message":
|
||||
dialogOptions, ok := result.Data().(*options.MessageDialog)
|
||||
dialogOptions, ok := result.Data().(*dialog.MessageDialog)
|
||||
if !ok {
|
||||
d.logger.Error("Invalid data for 'dialog:select:message' : %#v", result.Data())
|
||||
return
|
||||
@@ -449,11 +471,11 @@ func (d *Dispatcher) processMenuMessage(result *servicebus.Message) {
|
||||
|
||||
command := splitTopic[1]
|
||||
switch command {
|
||||
case "update":
|
||||
case "updateappmenu":
|
||||
|
||||
updatedMenu, ok := result.Data().(*menu.Menu)
|
||||
updatedMenu, ok := result.Data().(string)
|
||||
if !ok {
|
||||
d.logger.Error("Invalid data for 'menufrontend:update' : %#v",
|
||||
d.logger.Error("Invalid data for 'menufrontend:updateappmenu' : %#v",
|
||||
result.Data())
|
||||
return
|
||||
}
|
||||
@@ -461,97 +483,57 @@ func (d *Dispatcher) processMenuMessage(result *servicebus.Message) {
|
||||
// TODO: Work out what we mean in a multi window environment...
|
||||
// For now we will just pick the first one
|
||||
for _, client := range d.clients {
|
||||
client.frontend.UpdateMenu(updatedMenu)
|
||||
client.frontend.SetApplicationMenu(updatedMenu)
|
||||
}
|
||||
|
||||
case "settraymenu":
|
||||
trayMenuJSON, ok := result.Data().(string)
|
||||
if !ok {
|
||||
d.logger.Error("Invalid data for 'menufrontend:settraymenu' : %#v",
|
||||
result.Data())
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Work out what we mean in a multi window environment...
|
||||
// For now we will just pick the first one
|
||||
for _, client := range d.clients {
|
||||
client.frontend.SetTrayMenu(trayMenuJSON)
|
||||
}
|
||||
|
||||
case "updatecontextmenu":
|
||||
updatedContextMenu, ok := result.Data().(string)
|
||||
if !ok {
|
||||
d.logger.Error("Invalid data for 'menufrontend:updatecontextmenu' : %#v",
|
||||
result.Data())
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Work out what we mean in a multi window environment...
|
||||
// For now we will just pick the first one
|
||||
for _, client := range d.clients {
|
||||
client.frontend.UpdateContextMenu(updatedContextMenu)
|
||||
}
|
||||
|
||||
case "updatetraymenulabel":
|
||||
updatedTrayMenuLabel, ok := result.Data().(string)
|
||||
if !ok {
|
||||
d.logger.Error("Invalid data for 'menufrontend:updatetraymenulabel' : %#v",
|
||||
result.Data())
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Work out what we mean in a multi window environment...
|
||||
// For now we will just pick the first one
|
||||
for _, client := range d.clients {
|
||||
client.frontend.UpdateTrayMenuLabel(updatedTrayMenuLabel)
|
||||
}
|
||||
|
||||
default:
|
||||
d.logger.Error("Unknown menufrontend command: %s", command)
|
||||
}
|
||||
}
|
||||
func (d *Dispatcher) processContextMenuMessage(result *servicebus.Message) {
|
||||
splitTopic := strings.Split(result.Topic(), ":")
|
||||
if len(splitTopic) < 2 {
|
||||
d.logger.Error("Invalid contextmenu message : %#v", result.Data())
|
||||
return
|
||||
}
|
||||
|
||||
command := splitTopic[1]
|
||||
switch command {
|
||||
case "update":
|
||||
|
||||
updatedContextMenus, ok := result.Data().(*menu.ContextMenus)
|
||||
if !ok {
|
||||
d.logger.Error("Invalid data for 'contextmenufrontend:update' : %#v",
|
||||
result.Data())
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Work out what we mean in a multi window environment...
|
||||
// For now we will just pick the first one
|
||||
for _, client := range d.clients {
|
||||
client.frontend.UpdateContextMenus(updatedContextMenus)
|
||||
}
|
||||
|
||||
default:
|
||||
d.logger.Error("Unknown contextmenufrontend command: %s", command)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dispatcher) processTrayMessage(result *servicebus.Message) {
|
||||
splitTopic := strings.Split(result.Topic(), ":")
|
||||
if len(splitTopic) < 2 {
|
||||
d.logger.Error("Invalid tray message : %#v", result.Data())
|
||||
return
|
||||
}
|
||||
|
||||
command := splitTopic[1]
|
||||
switch command {
|
||||
case "update":
|
||||
|
||||
updatedMenu, ok := result.Data().(*menu.Menu)
|
||||
if !ok {
|
||||
d.logger.Error("Invalid data for 'trayfrontend:update' : %#v",
|
||||
result.Data())
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Work out what we mean in a multi window environment...
|
||||
// For now we will just pick the first one
|
||||
for _, client := range d.clients {
|
||||
client.frontend.UpdateTray(updatedMenu)
|
||||
}
|
||||
|
||||
case "setlabel":
|
||||
|
||||
updatedLabel, ok := result.Data().(string)
|
||||
if !ok {
|
||||
d.logger.Error("Invalid data for 'trayfrontend:setlabel' : %#v",
|
||||
result.Data())
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Work out what we mean in a multi window environment...
|
||||
// For now we will just pick the first one
|
||||
for _, client := range d.clients {
|
||||
client.frontend.UpdateTrayLabel(updatedLabel)
|
||||
}
|
||||
|
||||
case "seticon":
|
||||
|
||||
iconname, ok := result.Data().(string)
|
||||
if !ok {
|
||||
d.logger.Error("Invalid data for 'trayfrontend:seticon' : %#v",
|
||||
result.Data())
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Work out what we mean in a multi window environment...
|
||||
// For now we will just pick the first one
|
||||
for _, client := range d.clients {
|
||||
client.frontend.UpdateTrayIcon(iconname)
|
||||
}
|
||||
|
||||
default:
|
||||
d.logger.Error("Unknown menufrontend command: %s", command)
|
||||
}
|
||||
func (d *Dispatcher) Close() {
|
||||
d.cancel()
|
||||
d.wg.Wait()
|
||||
}
|
||||
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
var internalMethods = slicer.String([]string{"WailsInit", "Wails Shutdown"})
|
||||
|
||||
var structCache = make(map[string]*ParsedStruct)
|
||||
var boundStructs = make(map[string]*ParsedStruct)
|
||||
var boundMethods = []string{}
|
||||
@@ -49,7 +47,7 @@ func ParseProject(projectPath string) (BoundStructs, error) {
|
||||
cfg := &packages.Config{Mode: packages.NeedFiles | packages.NeedSyntax | packages.NeedTypesInfo}
|
||||
pkgs, err := packages.Load(cfg, projectPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "load: %v\n", err)
|
||||
_, _ = fmt.Fprintf(os.Stderr, "load: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if packages.PrintErrors(pkgs) > 0 {
|
||||
@@ -203,10 +201,6 @@ func ParseProject(projectPath string) (BoundStructs, error) {
|
||||
// This is a struct pointer method
|
||||
i, ok := se.X.(*ast.Ident)
|
||||
if ok {
|
||||
// We want to ignore Internal functions
|
||||
if internalMethods.Contains(x.Name.Name) {
|
||||
continue
|
||||
}
|
||||
// If we haven't already found this struct,
|
||||
// Create a placeholder in the cache
|
||||
parsedStruct := structCache[i.Name]
|
||||
@@ -437,4 +431,6 @@ func ParseProject(projectPath string) (BoundStructs, error) {
|
||||
println()
|
||||
println("}")
|
||||
println()
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/clilogger"
|
||||
@@ -16,11 +17,14 @@ type Process struct {
|
||||
|
||||
// NewProcess creates a new process struct
|
||||
func NewProcess(logger *clilogger.CLILogger, cmd string, args ...string) *Process {
|
||||
return &Process{
|
||||
result := &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
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||
)
|
||||
|
||||
// ContextMenus defines all ContextMenu related operations
|
||||
type ContextMenus interface {
|
||||
On(menuID string, callback func(*menu.MenuItem, string))
|
||||
Update()
|
||||
GetByID(menuID string) *menu.MenuItem
|
||||
RemoveByID(id string) bool
|
||||
}
|
||||
|
||||
type contextMenus struct {
|
||||
bus *servicebus.ServiceBus
|
||||
contextmenus *menu.ContextMenus
|
||||
}
|
||||
|
||||
// newContextMenus creates a new ContextMenu struct
|
||||
func newContextMenus(bus *servicebus.ServiceBus, contextmenus *menu.ContextMenus) ContextMenus {
|
||||
return &contextMenus{
|
||||
bus: bus,
|
||||
contextmenus: contextmenus,
|
||||
}
|
||||
}
|
||||
|
||||
// On registers a listener for a particular event
|
||||
func (t *contextMenus) On(menuID string, callback func(*menu.MenuItem, string)) {
|
||||
t.bus.Publish("contextmenus:on", &message.ContextMenusOnMessage{
|
||||
MenuID: menuID,
|
||||
Callback: callback,
|
||||
})
|
||||
}
|
||||
|
||||
func (t *contextMenus) Update() {
|
||||
t.bus.Publish("contextmenus:update", t.contextmenus)
|
||||
}
|
||||
|
||||
func (t *contextMenus) GetByID(menuItemID string) *menu.MenuItem {
|
||||
return t.contextmenus.GetByID(menuItemID)
|
||||
}
|
||||
|
||||
func (t *contextMenus) RemoveByID(menuItemID string) bool {
|
||||
return t.contextmenus.RemoveByID(menuItemID)
|
||||
}
|
||||
@@ -5,14 +5,14 @@ import (
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/crypto"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
dialogoptions "github.com/wailsapp/wails/v2/pkg/options/dialog"
|
||||
)
|
||||
|
||||
// Dialog defines all Dialog related operations
|
||||
type Dialog interface {
|
||||
Open(dialogOptions *options.OpenDialog) []string
|
||||
Save(dialogOptions *options.SaveDialog) string
|
||||
Message(dialogOptions *options.MessageDialog) string
|
||||
Open(dialogOptions *dialogoptions.OpenDialog) []string
|
||||
Save(dialogOptions *dialogoptions.SaveDialog) string
|
||||
Message(dialogOptions *dialogoptions.MessageDialog) string
|
||||
}
|
||||
|
||||
// dialog exposes the Dialog interface
|
||||
@@ -45,7 +45,7 @@ func (r *dialog) processTitleAndFilter(params ...string) (string, string) {
|
||||
}
|
||||
|
||||
// Open prompts the user to select a file
|
||||
func (r *dialog) Open(dialogOptions *options.OpenDialog) []string {
|
||||
func (r *dialog) Open(dialogOptions *dialogoptions.OpenDialog) []string {
|
||||
|
||||
// Create unique dialog callback
|
||||
uniqueCallback := crypto.RandomID()
|
||||
@@ -70,7 +70,7 @@ func (r *dialog) Open(dialogOptions *options.OpenDialog) []string {
|
||||
}
|
||||
|
||||
// Save prompts the user to select a file
|
||||
func (r *dialog) Save(dialogOptions *options.SaveDialog) string {
|
||||
func (r *dialog) Save(dialogOptions *dialogoptions.SaveDialog) string {
|
||||
|
||||
// Create unique dialog callback
|
||||
uniqueCallback := crypto.RandomID()
|
||||
@@ -95,7 +95,7 @@ func (r *dialog) Save(dialogOptions *options.SaveDialog) string {
|
||||
}
|
||||
|
||||
// Message show a message to the user
|
||||
func (r *dialog) Message(dialogOptions *options.MessageDialog) string {
|
||||
func (r *dialog) Message(dialogOptions *dialogoptions.MessageDialog) string {
|
||||
|
||||
// Create unique dialog callback
|
||||
uniqueCallback := crypto.RandomID()
|
||||
|
||||
@@ -65,7 +65,7 @@ export function SetBindings(bindingsMap) {
|
||||
window.backend[packageName][structName][methodName] = function () {
|
||||
|
||||
// No timeout by default
|
||||
var timeout = 0;
|
||||
let timeout = 0;
|
||||
|
||||
// Actual function
|
||||
function dynamic() {
|
||||
@@ -89,19 +89,3 @@ 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;
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ 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) {
|
||||
@@ -21,7 +22,7 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
|
||||
error: JSON.stringify(error),
|
||||
stack: function() { return JSON.stringify(new Error().stack); }(),
|
||||
};
|
||||
window.wails.Log.Error(JSON.stringify(errorMessage));
|
||||
RaiseError(errorMessage);
|
||||
};
|
||||
|
||||
// Initialise the Runtime
|
||||
|
||||
@@ -13,6 +13,7 @@ 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! :-)
|
||||
*
|
||||
@@ -43,7 +44,7 @@ class Listener {
|
||||
}
|
||||
}
|
||||
|
||||
var eventListeners = {};
|
||||
let eventListeners = {};
|
||||
|
||||
/**
|
||||
* Registers an event listener that will be invoked `maxCallbacks` times before being destroyed
|
||||
@@ -96,7 +97,7 @@ export function Once(eventName, callback) {
|
||||
function notifyListeners(eventData) {
|
||||
|
||||
// Get the event name
|
||||
var eventName = eventData.name;
|
||||
let eventName = eventData.name;
|
||||
|
||||
// Check if we have any listeners for this event
|
||||
if (eventListeners[eventName]) {
|
||||
@@ -110,7 +111,7 @@ function notifyListeners(eventData) {
|
||||
// Get next listener
|
||||
const listener = eventListeners[eventName][count];
|
||||
|
||||
var data = eventData.data;
|
||||
let data = eventData.data;
|
||||
|
||||
// Do the callback
|
||||
const destroy = listener.Callback(data);
|
||||
@@ -120,7 +121,7 @@ function notifyListeners(eventData) {
|
||||
}
|
||||
}
|
||||
|
||||
// Update callbacks with new list of listners
|
||||
// Update callbacks with new list of listeners
|
||||
eventListeners[eventName] = newEventListenerList;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,4 @@ export function Init() {
|
||||
|
||||
// Do platform specific Init
|
||||
Platform.Init();
|
||||
|
||||
window.wailsloader.runtime = true;
|
||||
}
|
||||
@@ -25,6 +25,10 @@ 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
|
||||
|
||||
@@ -27,7 +27,7 @@ export function Init() {
|
||||
// Setup drag handler
|
||||
// Based on code from: https://github.com/patr0nus/DeskGap
|
||||
window.addEventListener('mousedown', function (e) {
|
||||
var currentElement = e.target;
|
||||
let currentElement = e.target;
|
||||
while (currentElement != null) {
|
||||
if (currentElement.hasAttribute('data-wails-no-drag')) {
|
||||
break;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
bridge.js
|
||||
225
v2/internal/runtime/js/runtime/bridge.js
Normal file
225
v2/internal/runtime/js/runtime/bridge.js
Normal file
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The lightweight framework for web-like apps
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
/* jshint esversion: 6 */
|
||||
|
||||
function init() {
|
||||
// Bridge object
|
||||
window.wailsbridge = {
|
||||
reconnectOverlay: null,
|
||||
reconnectTimer: 300,
|
||||
wsURL: 'ws://' + window.location.hostname + ':34115/bridge',
|
||||
connectionState: null,
|
||||
config: {},
|
||||
websocket: null,
|
||||
callback: null,
|
||||
overlayHTML:
|
||||
'<div class="wails-reconnect-overlay"><div class="wails-reconnect-overlay-content"><div class="wails-reconnect-overlay-loadingspinner"></div></div></div>',
|
||||
overlayCSS:
|
||||
'.wails-reconnect-overlay{position:fixed;top:0;left:0;width:100%;height:100%;backdrop-filter: blur(20px) saturate(160%) contrast(45%) brightness(140%);display:none;z-index:999999}.wails-reconnect-overlay-content{position:relative;top:50%;transform:translateY(-50%);margin: 0;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAC8AAAAuCAMAAACPpbA7AAAAflBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQAAAAAAAAAAAABAQEEBAQAAAAAAAAEBAQAAAADAwMAAAABAQEAAAAAAAAAAAAAAAAAAAACAgICAgIBAQEAAAAAAAAAAAAAAAAAAAACAgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBQWCC3waAAAAKXRSTlMALgUMIBk0+xEqJs70Xhb3lu3EjX2EZTlv5eHXvbarQj3cdmpXSqOeUDwaqNAAAAKCSURBVEjHjZTntqsgEIUPVVCwtxg1vfD+L3hHRe8K6snZf+KKn8OewvzsSSeXLruLnz+KHs0gr6DkT3xsRkU6VVn4Ha/UxLe1Z4y64i847sykPBh/AvQ7ry3eFN70oKrfcBJYvm/tQ1qxP4T3emXPeXAkvodPUvtdjbhk+Ft4c0hslTiXVOzxOJ15NWUblQhRsdu3E1AfCjj3Gdm18zSOsiH8Lk4TB480ksy62fiqNo4OpyU8O21l6+hyRtS6z8r1pHlmle5sR1/WXS6Mq2Nl+YeKt3vr+vdH/q4O68tzXuwkiZmngYb4R8Co1jh0+Ww2UTyWxBvtyxLO7QVjO3YOD/lWZpbXDGellFG2Mws58mMnjVZSn7p+XvZ6IF4nn02OJZV0aTO22arp/DgLPtrgpVoi6TPbZm4XQBjY159w02uO0BDdYsfrOEi0M2ulRXlCIPAOuN1NOVhi+riBR3dgwQplYsZRZJLXq23Mlo5njkbY0rZFu3oiNIYG2kqsbVz67OlNuZZIOlfxHDl0UpyRX86z/OYC/3qf1A1xTrMp/PWWM4ePzf8DDp1nesQRpcFk7BlwdzN08ZIALJpCaciQXO0f6k4dnuT/Ewg4l7qSTNzm2SykdHn6GJ12mWc6aCNj/g1cTXpB8YFfr0uVc96aFkkqiIiX4nO+salKwGtIkvfB+Ja8DxMeD3hIXP5mTOYPB4eVT0+32I5ykvPZjesnkGgIREgYnmLrPb0PdV3hoLup2TjcGBPM4mgsfF5BrawZR4/GpzYQzQfrUZCf0TCWYo2DqhdhTJBQ6j4xqmmLN5LjdRIY8LWExiFUsSrza/nmFBqw3I9tEZB9h0lIQSO9if8DkISDAj8CDawAAAAASUVORK5CYII=);background-repeat:no-repeat;background-position:center}.wails-reconnect-overlay-loadingspinner{pointer-events:none;width:2.5em;height:2.5em;border:.4em solid transparent;border-color:#f00 #eee0 #f00 #eee0;border-radius:50%;animation:loadingspin 1s linear infinite;margin:auto;padding:2.5em}@keyframes loadingspin{100%{transform:rotate(360deg); opacity}}',
|
||||
log: function (message) {
|
||||
// eslint-disable-next-line
|
||||
console.log(
|
||||
'%c wails bridge %c ' + message + ' ',
|
||||
'background: #aa0000; color: #fff; border-radius: 3px 0px 0px 3px; padding: 1px; font-size: 0.7rem',
|
||||
'background: #009900; color: #fff; border-radius: 0px 3px 3px 0px; padding: 1px; font-size: 0.7rem'
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function setupIPCBridge() {
|
||||
// darwin
|
||||
window.webkit = {
|
||||
messageHandlers: {
|
||||
external: {
|
||||
postMessage: (message) => {
|
||||
window.wailsbridge.websocket.send(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Adapted from webview - thanks zserge!
|
||||
function injectCSS(css) {
|
||||
const elem = document.createElement('style');
|
||||
elem.setAttribute('type', 'text/css');
|
||||
elem.appendChild(document.createTextNode(css));
|
||||
const head = document.head || document.getElementsByTagName('head')[0];
|
||||
head.appendChild(elem);
|
||||
}
|
||||
|
||||
// Creates a node in the Dom
|
||||
/**
|
||||
* @param {HTMLElement} parent
|
||||
* @param {string} elementType
|
||||
* @param {string} id
|
||||
* @param {string|null} className
|
||||
* @param {string|null} content
|
||||
*/
|
||||
function createNode(parent, elementType, id, className, content) {
|
||||
const d = document.createElement(elementType);
|
||||
if (id) {
|
||||
d.id = id;
|
||||
}
|
||||
if (className) {
|
||||
d.className = className;
|
||||
}
|
||||
if (content) {
|
||||
d.innerHTML = content;
|
||||
}
|
||||
parent.appendChild(d);
|
||||
return d;
|
||||
}
|
||||
|
||||
// Sets up the overlay
|
||||
function setupOverlay() {
|
||||
const body = document.body;
|
||||
const wailsBridgeNode = createNode(body, 'div', 'wails-bridge', null, null);
|
||||
wailsBridgeNode.innerHTML = window.wailsbridge.overlayHTML;
|
||||
|
||||
// Inject the overlay CSS
|
||||
injectCSS(window.wailsbridge.overlayCSS);
|
||||
}
|
||||
|
||||
// Start the Wails Bridge
|
||||
function startBridge() {
|
||||
// Setup the overlay
|
||||
setupOverlay();
|
||||
|
||||
window.wailsbridge.websocket = null;
|
||||
window.wailsbridge.connectTimer = null;
|
||||
window.wailsbridge.reconnectOverlay = document.querySelector(
|
||||
'.wails-reconnect-overlay'
|
||||
);
|
||||
window.wailsbridge.connectionState = 'disconnected';
|
||||
|
||||
// Shows the overlay
|
||||
function showReconnectOverlay() {
|
||||
window.wailsbridge.reconnectOverlay.style.display = 'block';
|
||||
}
|
||||
|
||||
// Hides the overlay
|
||||
const hideReconnectOverlay = function () {
|
||||
window.wailsbridge.reconnectOverlay.style.display = 'none';
|
||||
}
|
||||
window.wailsbridge.hideReconnectOverlay = hideReconnectOverlay;
|
||||
|
||||
// Adds a script to the Dom.
|
||||
// Removes it if second parameter is true.
|
||||
function addScript(script, remove) {
|
||||
const s = document.createElement('script');
|
||||
s.setAttribute('type', 'text/javascript');
|
||||
s.textContent = script;
|
||||
document.head.appendChild(s);
|
||||
|
||||
// Remove internal messages from the DOM
|
||||
if (remove) {
|
||||
s.parentNode.removeChild(s);
|
||||
}
|
||||
}
|
||||
|
||||
// Handles incoming websocket connections
|
||||
function handleConnect() {
|
||||
window.wailsbridge.log('Connected to backend');
|
||||
setupIPCBridge();
|
||||
hideReconnectOverlay();
|
||||
clearInterval(window.wailsbridge.connectTimer);
|
||||
window.wailsbridge.websocket.onclose = handleDisconnect;
|
||||
window.wailsbridge.websocket.onmessage = handleMessage;
|
||||
window.wailsbridge.connectionState = 'connected';
|
||||
}
|
||||
|
||||
// Handles websocket disconnects
|
||||
function handleDisconnect() {
|
||||
window.wailsbridge.log('Disconnected from backend');
|
||||
window.wailsbridge.websocket = null;
|
||||
window.wailsbridge.connectionState = 'disconnected';
|
||||
showReconnectOverlay();
|
||||
connect();
|
||||
}
|
||||
|
||||
// Try to connect to the backend every 1s (default value).
|
||||
// Change this value in the main wailsbridge object.
|
||||
function connect() {
|
||||
window.wailsbridge.connectTimer = setInterval(function () {
|
||||
if (window.wailsbridge.websocket == null) {
|
||||
window.wailsbridge.websocket = new WebSocket(window.wailsbridge.wsURL);
|
||||
window.wailsbridge.websocket.onopen = handleConnect;
|
||||
window.wailsbridge.websocket.onerror = function (e) {
|
||||
e.stopImmediatePropagation();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
window.wailsbridge.websocket = null;
|
||||
return false;
|
||||
};
|
||||
}
|
||||
}, window.wailsbridge.reconnectTimer);
|
||||
}
|
||||
|
||||
function handleMessage(message) {
|
||||
// As a bridge we ignore js and css injections
|
||||
switch (message.data[0]) {
|
||||
// Wails library - inject!
|
||||
case 'b':
|
||||
message = message.data.slice(1)
|
||||
addScript(message);
|
||||
window.wailsbridge.log('Loaded Wails Runtime');
|
||||
|
||||
// Now wails runtime is loaded, wails for the ready event
|
||||
// and callback to the main app
|
||||
// window.wails.Events.On('wails:loaded', function () {
|
||||
if (window.wailsbridge.callback) {
|
||||
window.wailsbridge.log('Notifying application');
|
||||
window.wailsbridge.callback(window.wails);
|
||||
}
|
||||
// });
|
||||
break;
|
||||
// // Notifications
|
||||
// case 'n':
|
||||
// addScript(message.data.slice(1), true);
|
||||
// break;
|
||||
// // Binding
|
||||
// case 'b':
|
||||
// const binding = message.data.slice(1);
|
||||
// //log("Binding: " + binding)
|
||||
// window.wails._.NewBinding(binding);
|
||||
// break;
|
||||
// // Call back
|
||||
case 'c':
|
||||
const callbackData = message.data.slice(1);
|
||||
window.wails._.Callback(callbackData);
|
||||
break;
|
||||
default:
|
||||
window.wailsbridge.log('Unknown message: ' + message.data);
|
||||
}
|
||||
}
|
||||
|
||||
// Start by showing the overlay...
|
||||
showReconnectOverlay();
|
||||
|
||||
// ...and attempt to connect
|
||||
connect();
|
||||
}
|
||||
|
||||
function InitBridge(callback) {
|
||||
// Set up the bridge
|
||||
init();
|
||||
|
||||
// Save the callback
|
||||
window.wailsbridge.callback = callback;
|
||||
|
||||
// Start Bridge
|
||||
startBridge();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
InitBridge: InitBridge,
|
||||
}
|
||||
@@ -9,13 +9,25 @@ The lightweight framework for web-like apps
|
||||
*/
|
||||
/* jshint esversion: 6 */
|
||||
|
||||
import { InitBridge } from './bridge';
|
||||
|
||||
/**
|
||||
* Initialises the Wails runtime
|
||||
* ready will execute the callback when Wails has loaded
|
||||
* and initialised.
|
||||
*
|
||||
* @param {function} callback
|
||||
*/
|
||||
function Init(callback) {
|
||||
window.wails._.Init(callback);
|
||||
function ready(callback) {
|
||||
|
||||
// If window.wails exists, we are ready
|
||||
if( window.wails ) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
// If not we need to setup the bridge
|
||||
InitBridge(callback);
|
||||
}
|
||||
|
||||
module.exports = Init;
|
||||
module.exports = {
|
||||
ready: ready,
|
||||
};
|
||||
@@ -23,7 +23,7 @@ module.exports = {
|
||||
Browser: Browser,
|
||||
Dialog: Dialog,
|
||||
Events: Events,
|
||||
Init: Init,
|
||||
ready: Init.ready,
|
||||
Log: Log,
|
||||
System: System,
|
||||
Store: Store,
|
||||
|
||||
2
v2/internal/runtime/js/runtime/package-lock.json
generated
2
v2/internal/runtime/js/runtime/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wails/runtime",
|
||||
"version": "1.2.13",
|
||||
"version": "1.2.24",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wails/runtime",
|
||||
"version": "1.2.22",
|
||||
"version": "1.3.6",
|
||||
"description": "Wails V2 Javascript runtime library",
|
||||
"main": "main.js",
|
||||
"types": "runtime.d.ts",
|
||||
|
||||
@@ -13,7 +13,7 @@ const Events = require('./events');
|
||||
|
||||
/**
|
||||
* Registers an event listener that will be invoked when the user changes the
|
||||
* desktop theme (light mode / dark mode). The callback receives a boolean which
|
||||
* desktop theme (light mode / dark mode). The callback receives a booleanean which
|
||||
* indicates if dark mode is enabled.
|
||||
*
|
||||
* @export
|
||||
@@ -43,12 +43,12 @@ function DarkModeEnabled() {
|
||||
* Mac Title Bar Config
|
||||
* Check out https://github.com/lukakerr/NSWindowStyles for some examples of these settings
|
||||
* @typedef {Object} MacTitleBar
|
||||
* @param {bool} TitleBarAppearsTransparent - NSWindow.titleBarAppearsTransparent
|
||||
* @param {bool} HideTitle - NSWindow.hideTitle
|
||||
* @param {bool} HideTitleBar - NSWindow.hideTitleBar
|
||||
* @param {bool} FullSizeContent - Makes the webview portion of the window the full size of the window, even over the titlebar
|
||||
* @param {bool} UseToolbar - Set true to add a blank toolbar to the window (makes the title bar larger)
|
||||
* @param {bool} HideToolbarSeparator - Set true to remove the separator between the toolbar and the main content area
|
||||
* @param {boolean} TitleBarAppearsTransparent - NSWindow.titleBarAppearsTransparent
|
||||
* @param {boolean} HideTitle - NSWindow.hideTitle
|
||||
* @param {boolean} HideTitleBar - NSWindow.hideTitleBar
|
||||
* @param {boolean} FullSizeContent - Makes the webview portion of the window the full size of the window, even over the titlebar
|
||||
* @param {boolean} UseToolbar - Set true to add a blank toolbar to the window (makes the title bar larger)
|
||||
* @param {boolean} HideToolbarSeparator - Set true to remove the separator between the toolbar and the main content area
|
||||
*
|
||||
*/
|
||||
|
||||
@@ -65,8 +65,8 @@ function DarkModeEnabled() {
|
||||
* @param {number} MinHeight - Window Minimum Height
|
||||
* @param {number} MaxWidth - Window Maximum Width
|
||||
* @param {number} MaxHeight - Window Maximum Height
|
||||
* @param {bool} StartHidden - Start with window hidden
|
||||
* @param {bool} DevTools - Enables the window devtools
|
||||
* @param {boolean} StartHidden - Start with window hidden
|
||||
* @param {boolean} DevTools - Enables the window devtools
|
||||
* @param {number} RBGA - The initial window colour. Convert to hex then it'll mean 0xRRGGBBAA
|
||||
* @param {MacAppConfig} [Mac] - Configuration when running on Mac
|
||||
* @param {LinuxAppConfig} [Linux] - Configuration when running on Linux
|
||||
@@ -88,11 +88,23 @@ function AppConfig() {
|
||||
return window.wails.System.AppConfig.get();
|
||||
}
|
||||
|
||||
function LogLevel() {
|
||||
return window.wails.System.LogLevel();
|
||||
}
|
||||
|
||||
function Platform() {
|
||||
return window.wails.System.Platform();
|
||||
}
|
||||
|
||||
function AppType() {
|
||||
return window.wails.System.AppType();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
OnThemeChange: OnThemeChange,
|
||||
DarkModeEnabled: DarkModeEnabled,
|
||||
LogLevel: window.wails.System.LogLevel,
|
||||
Platform: window.wails.System.Platform,
|
||||
AppType: window.wails.System.AppType,
|
||||
LogLevel: LogLevel,
|
||||
Platform: Platform,
|
||||
AppType: AppType,
|
||||
AppConfig: AppConfig,
|
||||
};
|
||||
@@ -13,7 +13,7 @@ module.exports = {
|
||||
mode: 'production',
|
||||
output: {
|
||||
path: path.resolve(__dirname, '..', 'assets'),
|
||||
filename: 'desktop.js',
|
||||
filename: 'desktop_'+platform+'.js',
|
||||
library: 'Wails'
|
||||
},
|
||||
resolve: {
|
||||
|
||||
@@ -1,48 +1,41 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||
)
|
||||
|
||||
// Menu defines all Menu related operations
|
||||
type Menu interface {
|
||||
On(menuID string, callback func(*menu.MenuItem))
|
||||
Update()
|
||||
GetByID(menuID string) *menu.MenuItem
|
||||
RemoveByID(id string) bool
|
||||
UpdateApplicationMenu()
|
||||
UpdateContextMenu(contextMenu *menu.ContextMenu)
|
||||
SetTrayMenu(trayMenu *menu.TrayMenu)
|
||||
UpdateTrayMenuLabel(trayMenu *menu.TrayMenu)
|
||||
}
|
||||
|
||||
type menuRuntime struct {
|
||||
bus *servicebus.ServiceBus
|
||||
menu *menu.Menu
|
||||
bus *servicebus.ServiceBus
|
||||
}
|
||||
|
||||
// newMenu creates a new Menu struct
|
||||
func newMenu(bus *servicebus.ServiceBus, menu *menu.Menu) Menu {
|
||||
func newMenu(bus *servicebus.ServiceBus) Menu {
|
||||
return &menuRuntime{
|
||||
bus: bus,
|
||||
menu: menu,
|
||||
bus: bus,
|
||||
}
|
||||
}
|
||||
|
||||
// On registers a listener for a particular event
|
||||
func (m *menuRuntime) On(menuID string, callback func(*menu.MenuItem)) {
|
||||
m.bus.Publish("menu:on", &message.MenuOnMessage{
|
||||
MenuID: menuID,
|
||||
Callback: callback,
|
||||
})
|
||||
func (m *menuRuntime) UpdateApplicationMenu() {
|
||||
m.bus.Publish("menu:updateappmenu", nil)
|
||||
}
|
||||
|
||||
func (m *menuRuntime) Update() {
|
||||
m.bus.Publish("menu:update", m.menu)
|
||||
func (m *menuRuntime) UpdateContextMenu(contextMenu *menu.ContextMenu) {
|
||||
m.bus.Publish("menu:updatecontextmenu", contextMenu)
|
||||
}
|
||||
|
||||
func (m *menuRuntime) GetByID(menuID string) *menu.MenuItem {
|
||||
return m.menu.GetByID(menuID)
|
||||
func (m *menuRuntime) SetTrayMenu(trayMenu *menu.TrayMenu) {
|
||||
m.bus.Publish("menu:settraymenu", trayMenu)
|
||||
}
|
||||
|
||||
func (m *menuRuntime) RemoveByID(id string) bool {
|
||||
return m.menu.RemoveByID(id)
|
||||
func (m *menuRuntime) UpdateTrayMenuLabel(trayMenu *menu.TrayMenu) {
|
||||
m.bus.Publish("menu:updatetraymenulabel", trayMenu)
|
||||
}
|
||||
|
||||
@@ -2,37 +2,33 @@ package runtime
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||
)
|
||||
|
||||
// Runtime is a means for the user to interact with the application at runtime
|
||||
type Runtime struct {
|
||||
Browser Browser
|
||||
Events Events
|
||||
Window Window
|
||||
Dialog Dialog
|
||||
System System
|
||||
Menu Menu
|
||||
ContextMenu ContextMenus
|
||||
Tray Tray
|
||||
Store *StoreProvider
|
||||
Log Log
|
||||
bus *servicebus.ServiceBus
|
||||
Browser Browser
|
||||
Events Events
|
||||
Window Window
|
||||
Dialog Dialog
|
||||
System System
|
||||
Menu Menu
|
||||
Store *StoreProvider
|
||||
Log Log
|
||||
bus *servicebus.ServiceBus
|
||||
shutdownCallback func()
|
||||
}
|
||||
|
||||
// New creates a new runtime
|
||||
func New(serviceBus *servicebus.ServiceBus, menu *menu.Menu, trayMenu *menu.TrayOptions, contextMenus *menu.ContextMenus) *Runtime {
|
||||
func New(serviceBus *servicebus.ServiceBus, shutdownCallback func()) *Runtime {
|
||||
result := &Runtime{
|
||||
Browser: newBrowser(),
|
||||
Events: newEvents(serviceBus),
|
||||
Window: newWindow(serviceBus),
|
||||
Dialog: newDialog(serviceBus),
|
||||
System: newSystem(serviceBus),
|
||||
Menu: newMenu(serviceBus, menu),
|
||||
Tray: newTray(serviceBus, trayMenu),
|
||||
ContextMenu: newContextMenus(serviceBus, contextMenus),
|
||||
Log: newLog(serviceBus),
|
||||
bus: serviceBus,
|
||||
Browser: newBrowser(),
|
||||
Events: newEvents(serviceBus),
|
||||
Window: newWindow(serviceBus),
|
||||
Dialog: newDialog(serviceBus),
|
||||
System: newSystem(serviceBus),
|
||||
Menu: newMenu(serviceBus),
|
||||
Log: newLog(serviceBus),
|
||||
bus: serviceBus,
|
||||
}
|
||||
result.Store = newStore(result)
|
||||
return result
|
||||
@@ -40,5 +36,11 @@ func New(serviceBus *servicebus.ServiceBus, menu *menu.Menu, trayMenu *menu.Tray
|
||||
|
||||
// Quit the application
|
||||
func (r *Runtime) Quit() {
|
||||
// Call back to user's shutdown method if defined
|
||||
if r.shutdownCallback != nil {
|
||||
r.shutdownCallback()
|
||||
}
|
||||
|
||||
// Start shutdown of Wails
|
||||
r.bus.Publish("quit", "runtime.Quit()")
|
||||
}
|
||||
|
||||
85
v2/internal/runtime/scripts/rebuild.go
Normal file
85
v2/internal/runtime/scripts/rebuild.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/fs"
|
||||
"github.com/wailsapp/wails/v2/internal/shell"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
sourceDir := fs.RelativePath("../../../internal/runtime/js")
|
||||
|
||||
platforms := []string{"darwin"}
|
||||
|
||||
for _, platform := range platforms {
|
||||
println("Building JS Runtime for " + platform)
|
||||
envvars := []string{"WAILSPLATFORM=" + platform}
|
||||
|
||||
// Split up the InstallCommand and execute it
|
||||
stdout, stderr, err := shell.RunCommand(sourceDir, "npm", "install")
|
||||
if err != nil {
|
||||
for _, l := range strings.Split(stdout, "\n") {
|
||||
fmt.Printf(" %s\n", l)
|
||||
}
|
||||
for _, l := range strings.Split(stderr, "\n") {
|
||||
fmt.Printf(" %s\n", l)
|
||||
}
|
||||
}
|
||||
|
||||
runtimeDir := fs.RelativePath("../js")
|
||||
cmd := shell.CreateCommand(runtimeDir, "npm", "run", "build:desktop")
|
||||
cmd.Env = append(os.Environ(), envvars...)
|
||||
var stdo, stde bytes.Buffer
|
||||
cmd.Stdout = &stdo
|
||||
cmd.Stderr = &stde
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
for _, l := range strings.Split(stdo.String(), "\n") {
|
||||
fmt.Printf(" %s\n", l)
|
||||
}
|
||||
for _, l := range strings.Split(stde.String(), "\n") {
|
||||
fmt.Printf(" %s\n", l)
|
||||
}
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
wailsJS := fs.RelativePath("../assets/desktop_" + platform + ".js")
|
||||
runtimeData, err := ioutil.ReadFile(wailsJS)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Copy this file to bridge directory for embedding
|
||||
bridgeDir := fs.RelativePath("../../bridge/" + platform + ".js")
|
||||
println("Copying", wailsJS, "to", bridgeDir)
|
||||
err = fs.CopyFile(wailsJS, bridgeDir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Convert to C structure
|
||||
runtimeC := `
|
||||
// runtime.c (c) 2019-Present Lea Anthony.
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Ă‚ MODIWL
|
||||
// This file was auto-generated. DO NOT MODIFY.
|
||||
const unsigned char runtime[]={`
|
||||
for _, b := range runtimeData {
|
||||
runtimeC += fmt.Sprintf("0x%x, ", b)
|
||||
}
|
||||
runtimeC += "0x00};"
|
||||
|
||||
// Save file
|
||||
outputFile := fs.RelativePath(fmt.Sprintf("../../ffenestri/runtime_%s.c", platform))
|
||||
|
||||
if err := ioutil.WriteFile(outputFile, []byte(runtimeC), 0600); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,12 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
golog "log"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/deepcopy"
|
||||
)
|
||||
|
||||
// Options defines the optional data that may be used
|
||||
@@ -64,21 +67,31 @@ func fatal(err error) {
|
||||
// New creates a new store
|
||||
func (p *StoreProvider) New(name string, defaultValue interface{}) *Store {
|
||||
|
||||
dataType := reflect.TypeOf(defaultValue)
|
||||
if defaultValue == nil {
|
||||
golog.Fatal("Cannot initialise a store with nil")
|
||||
}
|
||||
|
||||
result := Store{
|
||||
name: name,
|
||||
runtime: p.runtime,
|
||||
data: reflect.ValueOf(defaultValue),
|
||||
dataType: dataType,
|
||||
name: name,
|
||||
runtime: p.runtime,
|
||||
}
|
||||
|
||||
// Setup the sync listener
|
||||
result.setupListener()
|
||||
|
||||
result.Set(defaultValue)
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
func (s *Store) lock() {
|
||||
s.mux.Lock()
|
||||
}
|
||||
|
||||
func (s *Store) unlock() {
|
||||
s.mux.Unlock()
|
||||
}
|
||||
|
||||
// OnError takes a function that will be called
|
||||
// whenever an error occurs
|
||||
func (s *Store) OnError(callback func(error)) {
|
||||
@@ -105,7 +118,7 @@ func (s *Store) processUpdatedData(data string) error {
|
||||
}
|
||||
|
||||
// Lock mutex for writing
|
||||
s.mux.Lock()
|
||||
s.lock()
|
||||
|
||||
// Handle nulls
|
||||
if newData == nil {
|
||||
@@ -116,7 +129,7 @@ func (s *Store) processUpdatedData(data string) error {
|
||||
}
|
||||
|
||||
// Unlock mutex
|
||||
s.mux.Unlock()
|
||||
s.unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -146,20 +159,34 @@ func (s *Store) setupListener() {
|
||||
// Resetting the curent data will resync
|
||||
s.resync()
|
||||
})
|
||||
|
||||
// Do initial resync
|
||||
s.resync()
|
||||
}
|
||||
|
||||
func (s *Store) resync() {
|
||||
// Stringify data
|
||||
newdata, err := json.Marshal(s.data.Interface())
|
||||
if err != nil {
|
||||
if s.errorHandler != nil {
|
||||
s.errorHandler(err)
|
||||
return
|
||||
|
||||
// Lock
|
||||
s.lock()
|
||||
defer s.unlock()
|
||||
|
||||
var result string
|
||||
|
||||
if s.data.IsValid() {
|
||||
rawdata, err := json.Marshal(s.data.Interface())
|
||||
if err != nil {
|
||||
if s.errorHandler != nil {
|
||||
s.errorHandler(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
result = string(rawdata)
|
||||
} else {
|
||||
result = "{}"
|
||||
}
|
||||
|
||||
// Emit event to front end
|
||||
s.runtime.Events.Emit("wails:sync:store:updatedbybackend:"+s.name, string(newdata))
|
||||
s.runtime.Events.Emit("wails:sync:store:updatedbybackend:"+s.name, result)
|
||||
|
||||
// Notify subscribers
|
||||
s.notify()
|
||||
@@ -172,7 +199,9 @@ func (s *Store) notify() {
|
||||
for _, callback := range s.callbacks {
|
||||
|
||||
// Build args
|
||||
s.lock()
|
||||
args := []reflect.Value{s.data}
|
||||
s.unlock()
|
||||
|
||||
if s.notifySynchronously {
|
||||
callback.Call(args)
|
||||
@@ -187,16 +216,31 @@ func (s *Store) notify() {
|
||||
// and notify listeners of the change
|
||||
func (s *Store) Set(data interface{}) error {
|
||||
|
||||
inType := reflect.TypeOf(data)
|
||||
if data == nil {
|
||||
return fmt.Errorf("cannot set store to nil")
|
||||
}
|
||||
|
||||
if inType != s.dataType {
|
||||
return fmt.Errorf("invalid data given in Store.Set(). Expected %s, got %s", s.dataType.String(), inType.String())
|
||||
s.lock()
|
||||
|
||||
dataCopy := deepcopy.Copy(data)
|
||||
|
||||
if dataCopy != nil {
|
||||
inType := reflect.TypeOf(dataCopy)
|
||||
|
||||
if inType != s.dataType && s.data.IsValid() {
|
||||
s.unlock()
|
||||
return fmt.Errorf("invalid data given in Store.Set(). Expected %s, got %s", s.dataType.String(), inType.String())
|
||||
}
|
||||
}
|
||||
|
||||
if s.dataType == nil {
|
||||
s.dataType = reflect.TypeOf(dataCopy)
|
||||
}
|
||||
|
||||
// Save data
|
||||
s.mux.Lock()
|
||||
s.data = reflect.ValueOf(data)
|
||||
s.mux.Unlock()
|
||||
s.data = reflect.ValueOf(dataCopy)
|
||||
|
||||
s.unlock()
|
||||
|
||||
// Resync with subscribers
|
||||
s.resync()
|
||||
@@ -247,7 +291,9 @@ func (s *Store) Subscribe(callback interface{}) {
|
||||
|
||||
callbackFunc := reflect.ValueOf(callback)
|
||||
|
||||
s.lock()
|
||||
s.callbacks = append(s.callbacks, callbackFunc)
|
||||
s.unlock()
|
||||
}
|
||||
|
||||
// updaterCheck ensures the given function to Update() is
|
||||
@@ -297,7 +343,9 @@ func (s *Store) Update(updater interface{}) {
|
||||
}
|
||||
|
||||
// Build args
|
||||
s.lock()
|
||||
args := []reflect.Value{s.data}
|
||||
s.unlock()
|
||||
|
||||
// Make call
|
||||
results := reflect.ValueOf(updater).Call(args)
|
||||
@@ -308,5 +356,12 @@ func (s *Store) Update(updater interface{}) {
|
||||
|
||||
// Get returns the value of the data that's kept in the current state / Store
|
||||
func (s *Store) Get() interface{} {
|
||||
s.lock()
|
||||
defer s.unlock()
|
||||
|
||||
if !s.data.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.data.Interface()
|
||||
}
|
||||
|
||||
165
v2/internal/runtime/store_test.go
Normal file
165
v2/internal/runtime/store_test.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
internallogger "github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
"github.com/wailsapp/wails/v2/pkg/logger"
|
||||
|
||||
is2 "github.com/matryer/is"
|
||||
)
|
||||
|
||||
func TestStoreProvider_NewWithNilDefault(t *testing.T) {
|
||||
is := is2.New(t)
|
||||
|
||||
defaultLogger := logger.NewDefaultLogger()
|
||||
testLogger := internallogger.New(defaultLogger)
|
||||
//testLogger.SetLogLevel(logger.TRACE)
|
||||
serviceBus := servicebus.New(testLogger)
|
||||
err := serviceBus.Start()
|
||||
is.NoErr(err)
|
||||
defer serviceBus.Stop()
|
||||
|
||||
testRuntime := New(serviceBus)
|
||||
storeProvider := newStore(testRuntime)
|
||||
|
||||
testStore := storeProvider.New("test", 0)
|
||||
|
||||
// You should be able to write a new value into a
|
||||
// store initialised with nil
|
||||
err = testStore.Set(100)
|
||||
is.NoErr(err)
|
||||
|
||||
// You shouldn't be able to write different types to the
|
||||
// store
|
||||
err = testStore.Set(false)
|
||||
is.True(err != nil)
|
||||
}
|
||||
|
||||
func TestStoreProvider_NewWithScalarDefault(t *testing.T) {
|
||||
is := is2.New(t)
|
||||
|
||||
defaultLogger := logger.NewDefaultLogger()
|
||||
testLogger := internallogger.New(defaultLogger)
|
||||
//testLogger.SetLogLevel(logger.TRACE)
|
||||
serviceBus := servicebus.New(testLogger)
|
||||
err := serviceBus.Start()
|
||||
is.NoErr(err)
|
||||
defer serviceBus.Stop()
|
||||
|
||||
testRuntime := New(serviceBus)
|
||||
storeProvider := newStore(testRuntime)
|
||||
testStore := storeProvider.New("test", 100)
|
||||
value := testStore.Get()
|
||||
is.Equal(value, 100)
|
||||
testStore.resync()
|
||||
value = testStore.Get()
|
||||
is.Equal(value, 100)
|
||||
}
|
||||
|
||||
func TestStoreProvider_NewWithStructDefault(t *testing.T) {
|
||||
is := is2.New(t)
|
||||
|
||||
defaultLogger := logger.NewDefaultLogger()
|
||||
testLogger := internallogger.New(defaultLogger)
|
||||
//testLogger.SetLogLevel(logger.TRACE)
|
||||
serviceBus := servicebus.New(testLogger)
|
||||
err := serviceBus.Start()
|
||||
is.NoErr(err)
|
||||
defer serviceBus.Stop()
|
||||
|
||||
testRuntime := New(serviceBus)
|
||||
storeProvider := newStore(testRuntime)
|
||||
|
||||
type TestValue struct {
|
||||
Name string
|
||||
}
|
||||
testValue := &TestValue{
|
||||
Name: "hi",
|
||||
}
|
||||
|
||||
testStore := storeProvider.New("test", testValue)
|
||||
|
||||
err = testStore.Set(testValue)
|
||||
is.NoErr(err)
|
||||
testStore.resync()
|
||||
value := testStore.Get()
|
||||
is.Equal(value, testValue)
|
||||
is.Equal(value.(*TestValue).Name, "hi")
|
||||
|
||||
testValue = &TestValue{
|
||||
Name: "there",
|
||||
}
|
||||
err = testStore.Set(testValue)
|
||||
is.NoErr(err)
|
||||
testStore.resync()
|
||||
value = testStore.Get()
|
||||
is.Equal(value, testValue)
|
||||
is.Equal(value.(*TestValue).Name, "there")
|
||||
|
||||
}
|
||||
|
||||
func TestStoreProvider_RapidReadWrite(t *testing.T) {
|
||||
is := is2.New(t)
|
||||
|
||||
defaultLogger := logger.NewDefaultLogger()
|
||||
testLogger := internallogger.New(defaultLogger)
|
||||
//testLogger.SetLogLevel(logger.TRACE)
|
||||
serviceBus := servicebus.New(testLogger)
|
||||
err := serviceBus.Start()
|
||||
is.NoErr(err)
|
||||
defer serviceBus.Stop()
|
||||
|
||||
testRuntime := New(serviceBus)
|
||||
storeProvider := newStore(testRuntime)
|
||||
|
||||
testStore := storeProvider.New("test", 1)
|
||||
|
||||
ctx, _ := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
readers := 100
|
||||
writers := 100
|
||||
wg.Add(readers + writers)
|
||||
// Setup readers
|
||||
go func(testStore *Store, ctx context.Context) {
|
||||
for readerCount := 0; readerCount < readers; readerCount++ {
|
||||
go func(store *Store, ctx context.Context, id int) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
wg.Done()
|
||||
return
|
||||
default:
|
||||
store.Get()
|
||||
}
|
||||
}
|
||||
}(testStore, ctx, readerCount)
|
||||
}
|
||||
}(testStore, ctx)
|
||||
|
||||
// Setup writers
|
||||
go func(testStore *Store, ctx context.Context) {
|
||||
for writerCount := 0; writerCount < writers; writerCount++ {
|
||||
go func(store *Store, ctx context.Context, id int) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
wg.Done()
|
||||
return
|
||||
default:
|
||||
err := store.Set(rand.Int())
|
||||
is.NoErr(err)
|
||||
}
|
||||
}
|
||||
}(testStore, ctx, writerCount)
|
||||
}
|
||||
}(testStore, ctx)
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
@@ -44,6 +44,7 @@ func (r *system) IsDarkMode() bool {
|
||||
systemResponseChannel, err := r.bus.Subscribe(responseTopic)
|
||||
if err != nil {
|
||||
fmt.Printf("ERROR: Cannot subscribe to bus topic: %+v\n", err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
message := "system:isdarkmode:" + uniqueCallback
|
||||
|
||||
8
v2/internal/runtime/system_default.go
Normal file
8
v2/internal/runtime/system_default.go
Normal file
@@ -0,0 +1,8 @@
|
||||
// +build !desktop,!server
|
||||
|
||||
package runtime
|
||||
|
||||
// AppType returns the application type, EG: desktop
|
||||
func (r *system) AppType() string {
|
||||
return "default"
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||
)
|
||||
|
||||
// Tray defines all Tray related operations
|
||||
type Tray interface {
|
||||
On(menuID string, callback func(*menu.MenuItem))
|
||||
Update()
|
||||
GetByID(menuID string) *menu.MenuItem
|
||||
RemoveByID(id string) bool
|
||||
SetLabel(label string)
|
||||
SetIcon(name string)
|
||||
}
|
||||
|
||||
type trayRuntime struct {
|
||||
bus *servicebus.ServiceBus
|
||||
trayMenu *menu.TrayOptions
|
||||
}
|
||||
|
||||
// newTray creates a new Menu struct
|
||||
func newTray(bus *servicebus.ServiceBus, menu *menu.TrayOptions) Tray {
|
||||
return &trayRuntime{
|
||||
bus: bus,
|
||||
trayMenu: menu,
|
||||
}
|
||||
}
|
||||
|
||||
// On registers a listener for a particular event
|
||||
func (t *trayRuntime) On(menuID string, callback func(*menu.MenuItem)) {
|
||||
t.bus.Publish("tray:on", &message.TrayOnMessage{
|
||||
MenuID: menuID,
|
||||
Callback: callback,
|
||||
})
|
||||
}
|
||||
|
||||
func (t *trayRuntime) Update() {
|
||||
t.bus.Publish("tray:update", t.trayMenu)
|
||||
}
|
||||
|
||||
func (t *trayRuntime) SetLabel(label string) {
|
||||
t.bus.Publish("tray:setlabel", label)
|
||||
}
|
||||
func (t *trayRuntime) SetIcon(name string) {
|
||||
t.bus.Publish("tray:seticon", name)
|
||||
}
|
||||
|
||||
func (t *trayRuntime) GetByID(menuID string) *menu.MenuItem {
|
||||
return t.trayMenu.Menu.GetByID(menuID)
|
||||
}
|
||||
|
||||
func (t *trayRuntime) RemoveByID(id string) bool {
|
||||
return t.trayMenu.Menu.RemoveByID(id)
|
||||
}
|
||||
@@ -18,6 +18,8 @@ type Window interface {
|
||||
Unminimise()
|
||||
SetTitle(title string)
|
||||
SetSize(width int, height int)
|
||||
SetMinSize(width int, height int)
|
||||
SetMaxSize(width int, height int)
|
||||
SetPosition(x int, y int)
|
||||
Fullscreen()
|
||||
UnFullscreen()
|
||||
@@ -85,6 +87,18 @@ func (w *window) SetSize(width int, height int) {
|
||||
w.bus.Publish(message, "")
|
||||
}
|
||||
|
||||
// SetSize sets the size of the window
|
||||
func (w *window) SetMinSize(width int, height int) {
|
||||
message := fmt.Sprintf("window:minsize:%d:%d", width, height)
|
||||
w.bus.Publish(message, "")
|
||||
}
|
||||
|
||||
// SetSize sets the size of the window
|
||||
func (w *window) SetMaxSize(width int, height int) {
|
||||
message := fmt.Sprintf("window:maxsize:%d:%d", width, height)
|
||||
w.bus.Publish(message, "")
|
||||
}
|
||||
|
||||
// SetPosition sets the position of the window
|
||||
func (w *window) SetPosition(x int, y int) {
|
||||
message := fmt.Sprintf("window:position:%d:%d", x, y)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package servicebus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -12,23 +13,26 @@ import (
|
||||
type ServiceBus struct {
|
||||
listeners map[string][]chan *Message
|
||||
messageQueue chan *Message
|
||||
quitChannel chan struct{}
|
||||
wg sync.WaitGroup
|
||||
lock sync.RWMutex
|
||||
closed bool
|
||||
debug bool
|
||||
logger logger.CustomLogger
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// New creates a new ServiceBus
|
||||
// The internal message queue is set to 100 messages
|
||||
// Listener queues are set to 10
|
||||
func New(logger *logger.Logger) *ServiceBus {
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &ServiceBus{
|
||||
listeners: make(map[string][]chan *Message),
|
||||
messageQueue: make(chan *Message, 100),
|
||||
quitChannel: make(chan struct{}, 1),
|
||||
logger: logger.CustomLogger("Service Bus"),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,23 +67,22 @@ func (s *ServiceBus) Debug() {
|
||||
// Start the service bus
|
||||
func (s *ServiceBus) Start() error {
|
||||
|
||||
s.logger.Trace("Starting")
|
||||
|
||||
// Prevent starting when closed
|
||||
if s.closed {
|
||||
return fmt.Errorf("cannot call start on closed servicebus")
|
||||
}
|
||||
|
||||
// We run in a different thread
|
||||
go func() {
|
||||
s.logger.Trace("Starting")
|
||||
|
||||
quit := false
|
||||
s.wg.Add(1)
|
||||
go func() {
|
||||
defer s.logger.Trace("Stopped")
|
||||
|
||||
// Loop until we get a quit message
|
||||
for !quit {
|
||||
for {
|
||||
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
|
||||
// Listen for messages
|
||||
case message := <-s.messageQueue:
|
||||
@@ -90,16 +93,9 @@ func (s *ServiceBus) Start() error {
|
||||
}
|
||||
// Dispatch message
|
||||
s.dispatchMessage(message)
|
||||
|
||||
// Listen for quit messages
|
||||
case <-s.quitChannel:
|
||||
quit = true
|
||||
}
|
||||
}
|
||||
|
||||
// Indicate we have shut down
|
||||
s.wg.Done()
|
||||
|
||||
}()
|
||||
|
||||
return nil
|
||||
@@ -116,10 +112,7 @@ func (s *ServiceBus) Stop() error {
|
||||
s.closed = true
|
||||
|
||||
// Send quit message
|
||||
s.quitChannel <- struct{}{}
|
||||
|
||||
// Wait for dispatcher to stop
|
||||
s.wg.Wait()
|
||||
s.cancel()
|
||||
|
||||
// Close down subscriber channels
|
||||
s.lock.Lock()
|
||||
@@ -134,7 +127,6 @@ func (s *ServiceBus) Stop() error {
|
||||
// Close message queue
|
||||
close(s.messageQueue)
|
||||
|
||||
s.logger.Trace("Stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -171,7 +163,6 @@ func (s *ServiceBus) Subscribe(topic string) (<-chan *Message, error) {
|
||||
func (s *ServiceBus) Publish(topic string, data interface{}) {
|
||||
// Prevent publish when closed
|
||||
if s.closed {
|
||||
s.logger.Fatal("cannot call publish on closed servicebus")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -183,7 +174,6 @@ func (s *ServiceBus) Publish(topic string, data interface{}) {
|
||||
func (s *ServiceBus) PublishForTarget(topic string, data interface{}, target string) {
|
||||
// Prevent publish when closed
|
||||
if s.closed {
|
||||
s.logger.Fatal("cannot call publish on closed servicebus")
|
||||
return
|
||||
}
|
||||
message := NewMessageForTarget(topic, data, target)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package signal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
gosignal "os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
@@ -20,24 +22,29 @@ type Manager struct {
|
||||
// signalChannel
|
||||
signalchannel chan os.Signal
|
||||
|
||||
// Quit channel
|
||||
quitChannel <-chan *servicebus.Message
|
||||
// ctx
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
// The shutdown callback to notify the user's app that a shutdown
|
||||
// has started
|
||||
shutdownCallback func()
|
||||
|
||||
// Parent waitgroup
|
||||
wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewManager creates a new signal manager
|
||||
func NewManager(bus *servicebus.ServiceBus, logger *logger.Logger) (*Manager, error) {
|
||||
|
||||
// Register quit channel
|
||||
quitChannel, err := bus.Subscribe("quit")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func NewManager(ctx context.Context, cancel context.CancelFunc, bus *servicebus.ServiceBus, logger *logger.Logger, shutdownCallback func()) (*Manager, error) {
|
||||
|
||||
result := &Manager{
|
||||
bus: bus,
|
||||
logger: logger.CustomLogger("Event Manager"),
|
||||
signalchannel: make(chan os.Signal, 2),
|
||||
quitChannel: quitChannel,
|
||||
bus: bus,
|
||||
logger: logger.CustomLogger("Event Manager"),
|
||||
signalchannel: make(chan os.Signal, 2),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
shutdownCallback: shutdownCallback,
|
||||
wg: ctx.Value("waitgroup").(*sync.WaitGroup),
|
||||
}
|
||||
|
||||
return result, nil
|
||||
@@ -49,20 +56,28 @@ func (m *Manager) Start() {
|
||||
// Hook into interrupts
|
||||
gosignal.Notify(m.signalchannel, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
// Spin off signal listener
|
||||
m.wg.Add(1)
|
||||
|
||||
// Spin off signal listener and wait for either a cancellation
|
||||
// or signal
|
||||
go func() {
|
||||
running := true
|
||||
for running {
|
||||
select {
|
||||
case <-m.signalchannel:
|
||||
println()
|
||||
m.logger.Trace("Ctrl+C detected. Shutting down...")
|
||||
m.bus.Publish("quit", "ctrl-c pressed")
|
||||
case <-m.quitChannel:
|
||||
running = false
|
||||
break
|
||||
select {
|
||||
case <-m.signalchannel:
|
||||
println()
|
||||
m.logger.Trace("Ctrl+C detected. Shutting down...")
|
||||
m.bus.Publish("quit", "ctrl-c pressed")
|
||||
|
||||
// Shutdown app first
|
||||
if m.shutdownCallback != nil {
|
||||
m.shutdownCallback()
|
||||
}
|
||||
|
||||
// Start shutdown of Wails
|
||||
m.cancel()
|
||||
|
||||
case <-m.ctx.Done():
|
||||
}
|
||||
m.logger.Trace("Shutdown")
|
||||
m.wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -5,16 +5,14 @@ import (
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/runtime"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Binding is the Binding subsystem. It manages all service bus messages
|
||||
// starting with "binding".
|
||||
type Binding struct {
|
||||
quitChannel <-chan *servicebus.Message
|
||||
bindingChannel <-chan *servicebus.Message
|
||||
running bool
|
||||
|
||||
running bool
|
||||
|
||||
// Binding db
|
||||
bindings *binding.Bindings
|
||||
@@ -26,22 +24,9 @@ type Binding struct {
|
||||
runtime *runtime.Runtime
|
||||
}
|
||||
|
||||
func showError(err error) {
|
||||
// Add a slight delay so log buffer clears
|
||||
time.Sleep(1 * time.Second)
|
||||
println("\n\n\n\n\n\n")
|
||||
println("Fatal Error in WailsInit(): " + err.Error())
|
||||
}
|
||||
|
||||
// NewBinding creates a new binding subsystem. Uses the given bindings db for reference.
|
||||
func NewBinding(bus *servicebus.ServiceBus, logger *logger.Logger, bindings *binding.Bindings, runtime *runtime.Runtime) (*Binding, error) {
|
||||
|
||||
// Register quit channel
|
||||
quitChannel, err := bus.Subscribe("quit")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Subscribe to event messages
|
||||
bindingChannel, err := bus.Subscribe("binding")
|
||||
if err != nil {
|
||||
@@ -49,23 +34,12 @@ func NewBinding(bus *servicebus.ServiceBus, logger *logger.Logger, bindings *bin
|
||||
}
|
||||
|
||||
result := &Binding{
|
||||
quitChannel: quitChannel,
|
||||
bindingChannel: bindingChannel,
|
||||
logger: logger.CustomLogger("Binding Subsystem"),
|
||||
bindings: bindings,
|
||||
runtime: runtime,
|
||||
}
|
||||
|
||||
// Call WailsInit methods once the frontend is loaded
|
||||
runtime.Events.On("wails:loaded", func(...interface{}) {
|
||||
result.logger.Trace("Calling WailsInit() methods")
|
||||
err := result.CallWailsInit()
|
||||
if err != nil {
|
||||
showError(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
})
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -80,48 +54,16 @@ func (b *Binding) Start() error {
|
||||
go func() {
|
||||
for b.running {
|
||||
select {
|
||||
case <-b.quitChannel:
|
||||
b.running = false
|
||||
case bindingMessage := <-b.bindingChannel:
|
||||
b.logger.Trace("Got binding message: %+v", bindingMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// Call shutdown
|
||||
b.shutdown()
|
||||
b.logger.Trace("Shutdown")
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CallWailsInit will callback to the registered WailsInit
|
||||
// methods with the runtime object
|
||||
func (b *Binding) CallWailsInit() error {
|
||||
for _, wailsinit := range b.bindings.DB().WailsInitMethods() {
|
||||
_, err := wailsinit.Call([]interface{}{b.runtime})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CallWailsShutdown will callback to the registered WailsShutdown
|
||||
// methods with the runtime object
|
||||
func (b *Binding) CallWailsShutdown() error {
|
||||
for _, wailsshutdown := range b.bindings.DB().WailsShutdownMethods() {
|
||||
_, err := wailsshutdown.Call([]interface{}{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Binding) shutdown() {
|
||||
err := b.CallWailsShutdown()
|
||||
if err != nil {
|
||||
showError(err)
|
||||
}
|
||||
b.logger.Trace("Shutdown")
|
||||
func (b *Binding) Close() {
|
||||
b.running = false
|
||||
}
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
package subsystem
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/options/dialog"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/binding"
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
|
||||
"github.com/wailsapp/wails/v2/internal/runtime"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
)
|
||||
|
||||
// Call is the Call subsystem. It manages all service bus messages
|
||||
// starting with "call".
|
||||
type Call struct {
|
||||
quitChannel <-chan *servicebus.Message
|
||||
callChannel <-chan *servicebus.Message
|
||||
running bool
|
||||
|
||||
// quit flag
|
||||
shouldQuit bool
|
||||
|
||||
// bindings DB
|
||||
DB *binding.DB
|
||||
@@ -31,16 +35,16 @@ type Call struct {
|
||||
|
||||
// runtime
|
||||
runtime *runtime.Runtime
|
||||
|
||||
// context
|
||||
ctx context.Context
|
||||
|
||||
// parent waitgroup
|
||||
wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewCall creates a new call subsystem
|
||||
func NewCall(bus *servicebus.ServiceBus, logger *logger.Logger, DB *binding.DB, runtime *runtime.Runtime) (*Call, error) {
|
||||
|
||||
// Register quit channel
|
||||
quitChannel, err := bus.Subscribe("quit")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func NewCall(ctx context.Context, bus *servicebus.ServiceBus, logger *logger.Logger, DB *binding.DB, runtime *runtime.Runtime) (*Call, error) {
|
||||
|
||||
// Subscribe to event messages
|
||||
callChannel, err := bus.Subscribe("call:invoke")
|
||||
@@ -49,12 +53,13 @@ func NewCall(bus *servicebus.ServiceBus, logger *logger.Logger, DB *binding.DB,
|
||||
}
|
||||
|
||||
result := &Call{
|
||||
quitChannel: quitChannel,
|
||||
callChannel: callChannel,
|
||||
logger: logger.CustomLogger("Call Subsystem"),
|
||||
DB: DB,
|
||||
bus: bus,
|
||||
runtime: runtime,
|
||||
ctx: ctx,
|
||||
wg: ctx.Value("waitgroup").(*sync.WaitGroup),
|
||||
}
|
||||
|
||||
return result, nil
|
||||
@@ -63,22 +68,21 @@ func NewCall(bus *servicebus.ServiceBus, logger *logger.Logger, DB *binding.DB,
|
||||
// Start the subsystem
|
||||
func (c *Call) Start() error {
|
||||
|
||||
c.running = true
|
||||
c.wg.Add(1)
|
||||
|
||||
// Spin off a go routine
|
||||
go func() {
|
||||
for c.running {
|
||||
defer c.logger.Trace("Shutdown")
|
||||
for {
|
||||
select {
|
||||
case <-c.quitChannel:
|
||||
c.running = false
|
||||
case <-c.ctx.Done():
|
||||
c.wg.Done()
|
||||
return
|
||||
case callMessage := <-c.callChannel:
|
||||
// TODO: Check if this works ok in a goroutine
|
||||
c.processCall(callMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// Call shutdown
|
||||
c.shutdown()
|
||||
}()
|
||||
|
||||
return nil
|
||||
@@ -131,7 +135,7 @@ func (c *Call) processSystemCall(payload *message.CallMessage, clientID string)
|
||||
darkModeEnabled := c.runtime.System.IsDarkMode()
|
||||
c.sendResult(darkModeEnabled, payload, clientID)
|
||||
case "Dialog.Open":
|
||||
dialogOptions := new(options.OpenDialog)
|
||||
dialogOptions := new(dialog.OpenDialog)
|
||||
err := json.Unmarshal(payload.Args[0], dialogOptions)
|
||||
if err != nil {
|
||||
c.logger.Error("Error decoding: %s", err)
|
||||
@@ -139,7 +143,7 @@ func (c *Call) processSystemCall(payload *message.CallMessage, clientID string)
|
||||
result := c.runtime.Dialog.Open(dialogOptions)
|
||||
c.sendResult(result, payload, clientID)
|
||||
case "Dialog.Save":
|
||||
dialogOptions := new(options.SaveDialog)
|
||||
dialogOptions := new(dialog.SaveDialog)
|
||||
err := json.Unmarshal(payload.Args[0], dialogOptions)
|
||||
if err != nil {
|
||||
c.logger.Error("Error decoding: %s", err)
|
||||
@@ -147,7 +151,7 @@ func (c *Call) processSystemCall(payload *message.CallMessage, clientID string)
|
||||
result := c.runtime.Dialog.Save(dialogOptions)
|
||||
c.sendResult(result, payload, clientID)
|
||||
case "Dialog.Message":
|
||||
dialogOptions := new(options.MessageDialog)
|
||||
dialogOptions := new(dialog.MessageDialog)
|
||||
err := json.Unmarshal(payload.Args[0], dialogOptions)
|
||||
if err != nil {
|
||||
c.logger.Error("Error decoding: %s", err)
|
||||
@@ -190,10 +194,6 @@ func (c *Call) sendError(err error, payload *message.CallMessage, clientID strin
|
||||
c.bus.PublishForTarget("call:result", string(messageData), clientID)
|
||||
}
|
||||
|
||||
func (c *Call) shutdown() {
|
||||
c.logger.Trace("Shutdown")
|
||||
}
|
||||
|
||||
// CallbackMessage defines a message that contains the result of a call
|
||||
type CallbackMessage struct {
|
||||
Result interface{} `json:"result"`
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
package subsystem
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||
)
|
||||
|
||||
// ContextMenus is the subsystem that handles the operation of context menus. It manages all service bus messages
|
||||
// starting with "contextmenus".
|
||||
type ContextMenus struct {
|
||||
quitChannel <-chan *servicebus.Message
|
||||
menuChannel <-chan *servicebus.Message
|
||||
running bool
|
||||
|
||||
// Event listeners
|
||||
listeners map[string][]func(*menu.MenuItem, string)
|
||||
menuItems map[string]*menu.MenuItem
|
||||
notifyLock sync.RWMutex
|
||||
|
||||
// logger
|
||||
logger logger.CustomLogger
|
||||
|
||||
// The context menus
|
||||
contextMenus *menu.ContextMenus
|
||||
|
||||
// Service Bus
|
||||
bus *servicebus.ServiceBus
|
||||
}
|
||||
|
||||
// NewContextMenus creates a new context menu subsystem
|
||||
func NewContextMenus(contextMenus *menu.ContextMenus, bus *servicebus.ServiceBus, logger *logger.Logger) (*ContextMenus, error) {
|
||||
|
||||
// Register quit channel
|
||||
quitChannel, err := bus.Subscribe("quit")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Subscribe to menu messages
|
||||
menuChannel, err := bus.Subscribe("contextmenus:")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &ContextMenus{
|
||||
quitChannel: quitChannel,
|
||||
menuChannel: menuChannel,
|
||||
logger: logger.CustomLogger("Context Menu Subsystem"),
|
||||
listeners: make(map[string][]func(*menu.MenuItem, string)),
|
||||
menuItems: make(map[string]*menu.MenuItem),
|
||||
contextMenus: contextMenus,
|
||||
bus: bus,
|
||||
}
|
||||
|
||||
// Build up list of item/id pairs
|
||||
result.processContextMenus(contextMenus)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type contextMenuData struct {
|
||||
MenuItemID string `json:"menuItemID"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// Start the subsystem
|
||||
func (c *ContextMenus) Start() error {
|
||||
|
||||
c.logger.Trace("Starting")
|
||||
|
||||
c.running = true
|
||||
|
||||
// Spin off a go routine
|
||||
go func() {
|
||||
for c.running {
|
||||
select {
|
||||
case <-c.quitChannel:
|
||||
c.running = false
|
||||
break
|
||||
case menuMessage := <-c.menuChannel:
|
||||
splitTopic := strings.Split(menuMessage.Topic(), ":")
|
||||
menuMessageType := splitTopic[1]
|
||||
switch menuMessageType {
|
||||
case "clicked":
|
||||
if len(splitTopic) != 2 {
|
||||
c.logger.Error("Received clicked message with invalid topic format. Expected 2 sections in topic, got %s", splitTopic)
|
||||
continue
|
||||
}
|
||||
c.logger.Trace("Got Context Menu clicked Message: %s %+v", menuMessage.Topic(), menuMessage.Data())
|
||||
contextMenuDataJSON := menuMessage.Data().(string)
|
||||
|
||||
var data contextMenuData
|
||||
err := json.Unmarshal([]byte(contextMenuDataJSON), &data)
|
||||
if err != nil {
|
||||
c.logger.Trace("Cannot process contextMenuDataJSON %s", string(contextMenuDataJSON))
|
||||
return
|
||||
}
|
||||
|
||||
// Get the menu item
|
||||
menuItem := c.menuItems[data.MenuItemID]
|
||||
if menuItem == nil {
|
||||
c.logger.Trace("Cannot process menuitem id %s - unknown", data.MenuItemID)
|
||||
return
|
||||
}
|
||||
|
||||
// Is the menu item a checkbox?
|
||||
if menuItem.Type == menu.CheckboxType {
|
||||
// Toggle state
|
||||
menuItem.Checked = !menuItem.Checked
|
||||
}
|
||||
|
||||
// Notify listeners
|
||||
c.notifyListeners(data, menuItem)
|
||||
case "on":
|
||||
listenerDetails := menuMessage.Data().(*message.ContextMenusOnMessage)
|
||||
id := listenerDetails.MenuID
|
||||
c.listeners[id] = append(c.listeners[id], listenerDetails.Callback)
|
||||
|
||||
// Make sure we catch any menu updates
|
||||
case "update":
|
||||
updatedMenu := menuMessage.Data().(*menu.ContextMenus)
|
||||
c.processContextMenus(updatedMenu)
|
||||
|
||||
// Notify frontend of menu change
|
||||
c.bus.Publish("contextmenufrontend:update", updatedMenu)
|
||||
|
||||
default:
|
||||
c.logger.Error("unknown context menu message: %+v", menuMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call shutdown
|
||||
c.shutdown()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ContextMenus) processContextMenus(contextMenus *menu.ContextMenus) {
|
||||
// Initialise the variables
|
||||
c.menuItems = make(map[string]*menu.MenuItem)
|
||||
c.contextMenus = contextMenus
|
||||
|
||||
for _, contextMenu := range contextMenus.Items {
|
||||
for _, item := range contextMenu.Items {
|
||||
c.processMenuItem(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ContextMenus) processMenuItem(item *menu.MenuItem) {
|
||||
|
||||
if item.SubMenu != nil {
|
||||
for _, submenuitem := range item.SubMenu {
|
||||
c.processMenuItem(submenuitem)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if item.ID != "" {
|
||||
if c.menuItems[item.ID] != nil {
|
||||
c.logger.Error("Context Menu id '%s' is used by multiple menu items: %s %s", c.menuItems[item.ID].Label, item.Label)
|
||||
return
|
||||
}
|
||||
c.menuItems[item.ID] = item
|
||||
}
|
||||
}
|
||||
|
||||
// Notifies listeners that the given menu was clicked
|
||||
func (c *ContextMenus) notifyListeners(contextData contextMenuData, menuItem *menu.MenuItem) {
|
||||
|
||||
// Get list of menu listeners
|
||||
listeners := c.listeners[contextData.MenuItemID]
|
||||
if listeners == nil {
|
||||
c.logger.Trace("No listeners for MenuItem with ID '%s'", contextData.MenuItemID)
|
||||
return
|
||||
}
|
||||
|
||||
// Lock the listeners
|
||||
c.notifyLock.Lock()
|
||||
|
||||
// Callback in goroutine
|
||||
for _, listener := range listeners {
|
||||
go listener(menuItem, contextData.Data)
|
||||
}
|
||||
|
||||
// Unlock
|
||||
c.notifyLock.Unlock()
|
||||
}
|
||||
|
||||
func (c *ContextMenus) shutdown() {
|
||||
c.logger.Trace("Shutdown")
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package subsystem
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -22,9 +23,7 @@ type eventListener struct {
|
||||
// Event is the Eventing subsystem. It manages all service bus messages
|
||||
// starting with "event".
|
||||
type Event struct {
|
||||
quitChannel <-chan *servicebus.Message
|
||||
eventChannel <-chan *servicebus.Message
|
||||
running bool
|
||||
|
||||
// Event listeners
|
||||
listeners map[string][]*eventListener
|
||||
@@ -32,16 +31,16 @@ type Event struct {
|
||||
|
||||
// logger
|
||||
logger logger.CustomLogger
|
||||
|
||||
// ctx
|
||||
ctx context.Context
|
||||
|
||||
// parent waitgroup
|
||||
wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewEvent creates a new log subsystem
|
||||
func NewEvent(bus *servicebus.ServiceBus, logger *logger.Logger) (*Event, error) {
|
||||
|
||||
// Register quit channel
|
||||
quitChannel, err := bus.Subscribe("quit")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func NewEvent(ctx context.Context, bus *servicebus.ServiceBus, logger *logger.Logger) (*Event, error) {
|
||||
|
||||
// Subscribe to event messages
|
||||
eventChannel, err := bus.Subscribe("event")
|
||||
@@ -50,10 +49,11 @@ func NewEvent(bus *servicebus.ServiceBus, logger *logger.Logger) (*Event, error)
|
||||
}
|
||||
|
||||
result := &Event{
|
||||
quitChannel: quitChannel,
|
||||
eventChannel: eventChannel,
|
||||
logger: logger.CustomLogger("Event Subsystem"),
|
||||
listeners: make(map[string][]*eventListener),
|
||||
ctx: ctx,
|
||||
wg: ctx.Value("waitgroup").(*sync.WaitGroup),
|
||||
}
|
||||
|
||||
return result, nil
|
||||
@@ -80,15 +80,16 @@ func (e *Event) Start() error {
|
||||
|
||||
e.logger.Trace("Starting")
|
||||
|
||||
e.running = true
|
||||
e.wg.Add(1)
|
||||
|
||||
// Spin off a go routine
|
||||
go func() {
|
||||
for e.running {
|
||||
defer e.logger.Trace("Shutdown")
|
||||
for {
|
||||
select {
|
||||
case <-e.quitChannel:
|
||||
e.running = false
|
||||
break
|
||||
case <-e.ctx.Done():
|
||||
e.wg.Done()
|
||||
return
|
||||
case eventMessage := <-e.eventChannel:
|
||||
splitTopic := strings.Split(eventMessage.Topic(), ":")
|
||||
eventType := splitTopic[1]
|
||||
@@ -128,8 +129,6 @@ func (e *Event) Start() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Call shutdown
|
||||
e.shutdown()
|
||||
}()
|
||||
|
||||
return nil
|
||||
@@ -190,7 +189,3 @@ func (e *Event) notifyListeners(eventName string, message *message.EventMessage)
|
||||
// Unlock
|
||||
e.notifyLock.Unlock()
|
||||
}
|
||||
|
||||
func (e *Event) shutdown() {
|
||||
e.logger.Trace("Shutdown")
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package subsystem
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/runtime"
|
||||
@@ -12,15 +14,23 @@ import (
|
||||
// Log is the Logging subsystem. It handles messages with topics starting
|
||||
// with "log:"
|
||||
type Log struct {
|
||||
logChannel <-chan *servicebus.Message
|
||||
quitChannel <-chan *servicebus.Message
|
||||
running bool
|
||||
logChannel <-chan *servicebus.Message
|
||||
|
||||
// quit flag
|
||||
shouldQuit bool
|
||||
|
||||
// Logger!
|
||||
logger *logger.Logger
|
||||
|
||||
// Loglevel store
|
||||
logLevelStore *runtime.Store
|
||||
|
||||
// Context for shutdown
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
|
||||
// internal waitgroup
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewLog creates a new log subsystem
|
||||
@@ -32,17 +42,14 @@ func NewLog(bus *servicebus.ServiceBus, logger *logger.Logger, logLevelStore *ru
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Subscribe to quit messages
|
||||
quitChannel, err := bus.Subscribe("quit")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
result := &Log{
|
||||
logChannel: logChannel,
|
||||
quitChannel: quitChannel,
|
||||
logger: logger,
|
||||
logLevelStore: logLevelStore,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
@@ -51,15 +58,17 @@ func NewLog(bus *servicebus.ServiceBus, logger *logger.Logger, logLevelStore *ru
|
||||
// Start the subsystem
|
||||
func (l *Log) Start() error {
|
||||
|
||||
l.running = true
|
||||
l.wg.Add(1)
|
||||
|
||||
// Spin off a go routine
|
||||
go func() {
|
||||
for l.running {
|
||||
defer l.logger.Trace("Logger Shutdown")
|
||||
|
||||
for l.shouldQuit == false {
|
||||
select {
|
||||
case <-l.quitChannel:
|
||||
l.running = false
|
||||
break
|
||||
case <-l.ctx.Done():
|
||||
l.wg.Done()
|
||||
return
|
||||
case logMessage := <-l.logChannel:
|
||||
logType := strings.TrimPrefix(logMessage.Topic(), "log:")
|
||||
switch logType {
|
||||
@@ -98,8 +107,12 @@ func (l *Log) Start() error {
|
||||
}
|
||||
}
|
||||
}
|
||||
l.logger.Trace("Logger Shutdown")
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Log) Close() {
|
||||
l.cancel()
|
||||
l.wg.Wait()
|
||||
}
|
||||
|
||||
@@ -1,55 +1,44 @@
|
||||
package subsystem
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/messagedispatcher/message"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||
)
|
||||
|
||||
// eventListener holds a callback function which is invoked when
|
||||
// the event listened for is emitted. It has a counter which indicates
|
||||
// how the total number of events it is interested in. A value of zero
|
||||
// means it does not expire (default).
|
||||
// type eventListener struct {
|
||||
// callback func(...interface{}) // Function to call with emitted event data
|
||||
// counter int // The number of times this callback may be called. -1 = infinite
|
||||
// delete bool // Flag to indicate that this listener should be deleted
|
||||
// }
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
"github.com/wailsapp/wails/v2/internal/menumanager"
|
||||
"github.com/wailsapp/wails/v2/internal/servicebus"
|
||||
)
|
||||
|
||||
// Menu is the subsystem that handles the operation of menus. It manages all service bus messages
|
||||
// starting with "menu".
|
||||
type Menu struct {
|
||||
quitChannel <-chan *servicebus.Message
|
||||
menuChannel <-chan *servicebus.Message
|
||||
running bool
|
||||
|
||||
// Event listeners
|
||||
listeners map[string][]func(*menu.MenuItem)
|
||||
menuItems map[string]*menu.MenuItem
|
||||
notifyLock sync.RWMutex
|
||||
// shutdown flag
|
||||
shouldQuit bool
|
||||
|
||||
// logger
|
||||
logger logger.CustomLogger
|
||||
|
||||
// The application menu
|
||||
applicationMenu *menu.Menu
|
||||
|
||||
// Service Bus
|
||||
bus *servicebus.ServiceBus
|
||||
|
||||
// Menu Manager
|
||||
menuManager *menumanager.Manager
|
||||
|
||||
// ctx
|
||||
ctx context.Context
|
||||
|
||||
// parent waitgroup
|
||||
wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewMenu creates a new menu subsystem
|
||||
func NewMenu(applicationMenu *menu.Menu, bus *servicebus.ServiceBus, logger *logger.Logger) (*Menu, error) {
|
||||
|
||||
// Register quit channel
|
||||
quitChannel, err := bus.Subscribe("quit")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func NewMenu(ctx context.Context, bus *servicebus.ServiceBus, logger *logger.Logger, menuManager *menumanager.Manager) (*Menu, error) {
|
||||
|
||||
// Subscribe to menu messages
|
||||
menuChannel, err := bus.Subscribe("menu:")
|
||||
@@ -58,18 +47,14 @@ func NewMenu(applicationMenu *menu.Menu, bus *servicebus.ServiceBus, logger *log
|
||||
}
|
||||
|
||||
result := &Menu{
|
||||
quitChannel: quitChannel,
|
||||
menuChannel: menuChannel,
|
||||
logger: logger.CustomLogger("Menu Subsystem"),
|
||||
listeners: make(map[string][]func(*menu.MenuItem)),
|
||||
menuItems: make(map[string]*menu.MenuItem),
|
||||
applicationMenu: applicationMenu,
|
||||
bus: bus,
|
||||
menuChannel: menuChannel,
|
||||
logger: logger.CustomLogger("Menu Subsystem"),
|
||||
bus: bus,
|
||||
menuManager: menuManager,
|
||||
ctx: ctx,
|
||||
wg: ctx.Value("waitgroup").(*sync.WaitGroup),
|
||||
}
|
||||
|
||||
// Build up list of item/id pairs
|
||||
result.processMenu(applicationMenu)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -78,15 +63,16 @@ func (m *Menu) Start() error {
|
||||
|
||||
m.logger.Trace("Starting")
|
||||
|
||||
m.running = true
|
||||
m.wg.Add(1)
|
||||
|
||||
// Spin off a go routine
|
||||
go func() {
|
||||
for m.running {
|
||||
defer m.logger.Trace("Shutdown")
|
||||
for {
|
||||
select {
|
||||
case <-m.quitChannel:
|
||||
m.running = false
|
||||
break
|
||||
case <-m.ctx.Done():
|
||||
m.wg.Done()
|
||||
return
|
||||
case menuMessage := <-m.menuChannel:
|
||||
splitTopic := strings.Split(menuMessage.Topic(), ":")
|
||||
menuMessageType := splitTopic[1]
|
||||
@@ -97,99 +83,77 @@ func (m *Menu) Start() error {
|
||||
continue
|
||||
}
|
||||
m.logger.Trace("Got Menu clicked Message: %s %+v", menuMessage.Topic(), menuMessage.Data())
|
||||
menuid := menuMessage.Data().(string)
|
||||
|
||||
// Get the menu item
|
||||
menuItem := m.menuItems[menuid]
|
||||
if menuItem == nil {
|
||||
m.logger.Trace("Cannot process menuid %s - unknown", menuid)
|
||||
type ClickCallbackMessage struct {
|
||||
MenuItemID string `json:"menuItemID"`
|
||||
MenuType string `json:"menuType"`
|
||||
Data string `json:"data"`
|
||||
ParentID string `json:"parentID"`
|
||||
}
|
||||
|
||||
var callbackData ClickCallbackMessage
|
||||
payload := []byte(menuMessage.Data().(string))
|
||||
err := json.Unmarshal(payload, &callbackData)
|
||||
if err != nil {
|
||||
m.logger.Error("%s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Is the menu item a checkbox?
|
||||
if menuItem.Type == menu.CheckboxType {
|
||||
// Toggle state
|
||||
menuItem.Checked = !menuItem.Checked
|
||||
err = m.menuManager.ProcessClick(callbackData.MenuItemID, callbackData.Data, callbackData.MenuType, callbackData.ParentID)
|
||||
if err != nil {
|
||||
m.logger.Trace("%s", err.Error())
|
||||
}
|
||||
|
||||
// Notify listeners
|
||||
m.notifyListeners(menuid, menuItem)
|
||||
case "on":
|
||||
listenerDetails := menuMessage.Data().(*message.MenuOnMessage)
|
||||
id := listenerDetails.MenuID
|
||||
m.listeners[id] = append(m.listeners[id], listenerDetails.Callback)
|
||||
|
||||
// Make sure we catch any menu updates
|
||||
case "update":
|
||||
updatedMenu := menuMessage.Data().(*menu.Menu)
|
||||
m.processMenu(updatedMenu)
|
||||
case "updateappmenu":
|
||||
updatedMenu, err := m.menuManager.UpdateApplicationMenu()
|
||||
if err != nil {
|
||||
m.logger.Trace("%s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Notify frontend of menu change
|
||||
m.bus.Publish("menufrontend:update", updatedMenu)
|
||||
m.bus.Publish("menufrontend:updateappmenu", updatedMenu)
|
||||
|
||||
case "updatecontextmenu":
|
||||
contextMenu := menuMessage.Data().(*menu.ContextMenu)
|
||||
updatedMenu, err := m.menuManager.UpdateContextMenu(contextMenu)
|
||||
if err != nil {
|
||||
m.logger.Trace("%s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Notify frontend of menu change
|
||||
m.bus.Publish("menufrontend:updatecontextmenu", updatedMenu)
|
||||
|
||||
case "settraymenu":
|
||||
trayMenu := menuMessage.Data().(*menu.TrayMenu)
|
||||
updatedMenu, err := m.menuManager.SetTrayMenu(trayMenu)
|
||||
if err != nil {
|
||||
m.logger.Trace("%s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Notify frontend of menu change
|
||||
m.bus.Publish("menufrontend:settraymenu", updatedMenu)
|
||||
|
||||
case "updatetraymenulabel":
|
||||
trayMenu := menuMessage.Data().(*menu.TrayMenu)
|
||||
updatedLabel, err := m.menuManager.UpdateTrayMenuLabel(trayMenu)
|
||||
if err != nil {
|
||||
m.logger.Trace("%s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Notify frontend of menu change
|
||||
m.bus.Publish("menufrontend:updatetraymenulabel", updatedLabel)
|
||||
|
||||
default:
|
||||
m.logger.Error("unknown menu message: %+v", menuMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call shutdown
|
||||
m.shutdown()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Menu) processMenu(applicationMenu *menu.Menu) {
|
||||
// Initialise the variables
|
||||
m.menuItems = make(map[string]*menu.MenuItem)
|
||||
m.applicationMenu = applicationMenu
|
||||
|
||||
for _, item := range applicationMenu.Items {
|
||||
m.processMenuItem(item)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Menu) processMenuItem(item *menu.MenuItem) {
|
||||
|
||||
if item.SubMenu != nil {
|
||||
for _, submenuitem := range item.SubMenu {
|
||||
m.processMenuItem(submenuitem)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if item.ID != "" {
|
||||
if m.menuItems[item.ID] != nil {
|
||||
m.logger.Error("Menu id '%s' is used by multiple menu items: %s %s", m.menuItems[item.ID].Label, item.Label)
|
||||
return
|
||||
}
|
||||
m.menuItems[item.ID] = item
|
||||
}
|
||||
}
|
||||
|
||||
// Notifies listeners that the given menu was clicked
|
||||
func (m *Menu) notifyListeners(menuid string, menuItem *menu.MenuItem) {
|
||||
|
||||
// Get list of menu listeners
|
||||
listeners := m.listeners[menuid]
|
||||
if listeners == nil {
|
||||
m.logger.Trace("No listeners for MenuItem with ID '%s'", menuid)
|
||||
return
|
||||
}
|
||||
|
||||
// Lock the listeners
|
||||
m.notifyLock.Lock()
|
||||
|
||||
// Callback in goroutine
|
||||
for _, listener := range listeners {
|
||||
go listener(menuItem)
|
||||
}
|
||||
|
||||
// Unlock
|
||||
m.notifyLock.Unlock()
|
||||
}
|
||||
|
||||
func (m *Menu) shutdown() {
|
||||
m.logger.Trace("Shutdown")
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package subsystem
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/wailsapp/wails/v2/pkg/menu"
|
||||
"strings"
|
||||
|
||||
"github.com/wailsapp/wails/v2/internal/logger"
|
||||
@@ -13,24 +13,27 @@ import (
|
||||
// Runtime is the Runtime subsystem. It handles messages with topics starting
|
||||
// with "runtime:"
|
||||
type Runtime struct {
|
||||
quitChannel <-chan *servicebus.Message
|
||||
runtimeChannel <-chan *servicebus.Message
|
||||
running bool
|
||||
|
||||
// The hooks channel allows us to hook into frontend startup
|
||||
hooksChannel <-chan *servicebus.Message
|
||||
startupCallback func(*runtime.Runtime)
|
||||
shutdownCallback func()
|
||||
|
||||
// quit flag
|
||||
shouldQuit bool
|
||||
|
||||
logger logger.CustomLogger
|
||||
|
||||
// Runtime library
|
||||
runtime *runtime.Runtime
|
||||
|
||||
//ctx
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewRuntime creates a new runtime subsystem
|
||||
func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger, menu *menu.Menu, trayMenu *menu.TrayOptions, contextMenus *menu.ContextMenus) (*Runtime, error) {
|
||||
|
||||
// Register quit channel
|
||||
quitChannel, err := bus.Subscribe("quit")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func NewRuntime(ctx context.Context, bus *servicebus.ServiceBus, logger *logger.Logger, startupCallback func(*runtime.Runtime), shutdownCallback func()) (*Runtime, error) {
|
||||
|
||||
// Subscribe to log messages
|
||||
runtimeChannel, err := bus.Subscribe("runtime:")
|
||||
@@ -38,11 +41,20 @@ func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger, menu *menu.Me
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Subscribe to log messages
|
||||
hooksChannel, err := bus.Subscribe("hooks:")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &Runtime{
|
||||
quitChannel: quitChannel,
|
||||
runtimeChannel: runtimeChannel,
|
||||
logger: logger.CustomLogger("Runtime Subsystem"),
|
||||
runtime: runtime.New(bus, menu, trayMenu, contextMenus),
|
||||
runtimeChannel: runtimeChannel,
|
||||
hooksChannel: hooksChannel,
|
||||
logger: logger.CustomLogger("Runtime Subsystem"),
|
||||
runtime: runtime.New(bus, shutdownCallback),
|
||||
startupCallback: startupCallback,
|
||||
shutdownCallback: shutdownCallback,
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
@@ -51,15 +63,26 @@ func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger, menu *menu.Me
|
||||
// Start the subsystem
|
||||
func (r *Runtime) Start() error {
|
||||
|
||||
r.running = true
|
||||
|
||||
// Spin off a go routine
|
||||
go func() {
|
||||
for r.running {
|
||||
defer r.logger.Trace("Shutdown")
|
||||
for {
|
||||
select {
|
||||
case <-r.quitChannel:
|
||||
r.running = false
|
||||
break
|
||||
case hooksMessage := <-r.hooksChannel:
|
||||
r.logger.Trace(fmt.Sprintf("Received hooksmessage: %+v", hooksMessage))
|
||||
messageSlice := strings.Split(hooksMessage.Topic(), ":")
|
||||
hook := messageSlice[1]
|
||||
switch hook {
|
||||
case "startup":
|
||||
if r.startupCallback != nil {
|
||||
go r.startupCallback(r.runtime)
|
||||
} else {
|
||||
r.logger.Warning("no startup callback registered!")
|
||||
}
|
||||
default:
|
||||
r.logger.Error("unknown hook message: %+v", hooksMessage)
|
||||
continue
|
||||
}
|
||||
case runtimeMessage := <-r.runtimeChannel:
|
||||
r.logger.Trace(fmt.Sprintf("Received message: %+v", runtimeMessage))
|
||||
// Topics have the format: "runtime:category:call"
|
||||
@@ -84,11 +107,10 @@ func (r *Runtime) Start() error {
|
||||
if err != nil {
|
||||
r.logger.Error(err.Error())
|
||||
}
|
||||
case <-r.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Call shutdown
|
||||
r.shutdown()
|
||||
}()
|
||||
|
||||
return nil
|
||||
@@ -99,10 +121,6 @@ func (r *Runtime) GoRuntime() *runtime.Runtime {
|
||||
return r.runtime
|
||||
}
|
||||
|
||||
func (r *Runtime) shutdown() {
|
||||
r.logger.Trace("Shutdown")
|
||||
}
|
||||
|
||||
func (r *Runtime) processBrowserMessage(method string, data interface{}) error {
|
||||
switch method {
|
||||
case "open":
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user