Compare commits

...

160 Commits

Author SHA1 Message Date
Lea Anthony
eb4bff89da v2.0.0-alpha.44 2021-03-04 06:18:31 +11:00
Lea Anthony
c66dc777f3 Remove debug logging 2021-03-04 06:18:11 +11:00
Lea Anthony
9003462457 v2.0.0-alpha.43 2021-03-04 06:09:17 +11:00
Lea Anthony
e124f0a220 Support Alternative menu items 2021-03-04 06:07:45 +11:00
Lea Anthony
c6d3f57712 v2.0.0-alpha.42 2021-03-01 08:49:31 +11:00
Lea Anthony
b4c669ff86 Support custom protocols 2021-02-28 22:08:23 +11:00
Lea Anthony
2d1b2c0947 Guard app signing 2021-02-28 15:29:15 +11:00
Lea Anthony
4a0c5aa785 v2.0.0-alpha.41 2021-02-27 20:33:42 +11:00
Lea Anthony
f48d7f8f60 Add support for -sign 2021-02-27 20:32:29 +11:00
Lea Anthony
651f24f641 update vanilla template 2021-02-27 20:06:49 +11:00
Lea Anthony
8fd77148ca update vanilla template 2021-02-27 16:59:16 +11:00
Lea Anthony
0dc0762fdf update vanilla template 2021-02-27 16:45:30 +11:00
Lea Anthony
1a92550709 update vanilla template 2021-02-27 16:08:53 +11:00
Lea Anthony
bffc15bc14 fix: terminate app on window close 2021-02-27 14:49:44 +11:00
Lea Anthony
198d206c46 Update basic template to new API 2021-02-27 14:07:27 +11:00
Lea Anthony
bb8e848ef6 Run go mod tidy before compilation 2021-02-27 14:03:54 +11:00
Lea Anthony
bac3e9e5c1 Fix asset dir perms 2021-02-27 13:54:39 +11:00
Lea Anthony
bc5eddeb66 v2.0.0-alpha.40 2021-02-26 15:31:37 +11:00
Lea Anthony
8e7258d812 Add locking for tray operations 2021-02-26 15:23:39 +11:00
Lea Anthony
7118762cec v2.0.0-alpha.39 2021-02-25 22:05:38 +11:00
Lea Anthony
6af92cf0a4 Delete Tray for bridge 2021-02-25 19:57:36 +11:00
matryer
1bb91634f7 avoid fmt in happy path 2021-02-25 07:39:42 +00:00
Lea Anthony
f71ce7913f v2.0.0-alpha.38 2021-02-24 21:50:39 +11:00
Lea Anthony
53db687a26 Support DeleteTrayMenuByID 2021-02-24 21:49:55 +11:00
Lea Anthony
13939d3d6b v2.0.0-alpha.37 2021-02-23 20:15:01 +11:00
Lea Anthony
552c6b8711 fix: modifiers 2021-02-23 18:57:59 +11:00
Lea Anthony
feee2b3db2 v2.0.0-alpha.36 2021-02-23 08:53:26 +11:00
Lea Anthony
9889c2bdbb Support Activation Policy 2021-02-23 08:52:56 +11:00
Lea Anthony
2432fccf71 v2.0.0-alpha.35 2021-02-22 20:17:22 +11:00
Lea Anthony
70510fd180 @wails/runtime v1.3.10 2021-02-22 20:16:14 +11:00
Lea Anthony
17c6201469 Menu off by default in dev. Toggle with backtick. 2021-02-22 20:14:44 +11:00
Lea Anthony
0f209c8900 v2.0.0-alpha.34 2021-02-22 19:39:07 +11:00
Lea Anthony
cbf043585c v2.0.0-alpha.33 2021-02-22 19:33:44 +11:00
Lea Anthony
5ae621ceaa Start signal handler a little later 2021-02-22 19:33:18 +11:00
Lea Anthony
1231b59443 better signal handling for shutdown 2021-02-22 17:39:33 +11:00
Lea Anthony
b18d4fbf41 v2.0.0-alpha.32 2021-02-22 09:00:47 +11:00
Lea Anthony
9ec5605e63 fix: loglevel duplication 2021-02-22 09:00:09 +11:00
Lea Anthony
98a4de8878 v2.0.0-alpha.31 2021-02-21 20:38:14 +11:00
Lea Anthony
5fe709f558 Trigger clean shutdown on Quit 2021-02-21 20:37:42 +11:00
Lea Anthony
5231a6893b v2.0.0-alpha.30 2021-02-21 19:28:07 +11:00
Lea Anthony
74f3ce990f Support loglevel flag in dev mode 2021-02-21 19:26:20 +11:00
Lea Anthony
998a913853 Hide dev warnings by default 2021-02-21 16:36:56 +11:00
Lea Anthony
964844835c Better Wails update messaging 2021-02-21 06:12:19 +11:00
Lea Anthony
4e152bb849 v2.0.0-alpha.29 2021-02-21 05:56:11 +11:00
Lea Anthony
51133d098c Slight refactor of backend module generation 2021-02-21 05:52:42 +11:00
Lea Anthony
d4191e7d1b Support parsing keyboard shortcuts 2021-02-20 21:32:32 +11:00
Lea Anthony
9c273bc745 v2.0.0-alpha.28 2021-02-20 16:04:42 +11:00
Lea Anthony
c6f6ad6beb Improved dev reload. Early abort for bad app. Don't reload if bad build. 2021-02-20 16:04:03 +11:00
Lea Anthony
4362a14459 Dev colours 2021-02-20 15:25:40 +11:00
Lea Anthony
0080e9e311 Gimme some colour 2021-02-20 15:14:20 +11:00
Lea Anthony
83d9297cac v2.0.0-alpha.27 2021-02-20 14:51:38 +11:00
Lea Anthony
d8ad250608 Template Image support 2021-02-19 05:25:23 +11:00
Lea Anthony
eac8880f6d Initial support for tray menus 2021-02-13 20:26:21 +11:00
Lea Anthony
f47100e71c Sort generated methods to appease git diff. 2021-02-13 18:04:00 +11:00
Lea Anthony
9a81a57d13 Revert bootstrapping. 2021-02-12 15:08:54 +11:00
Lea Anthony
354429bc28 Support UpdateTrayLabel in dev mode. Small refactor. 2021-02-12 14:52:47 +11:00
Lea Anthony
99d4d9490c Improved dialog support for dev mode 2021-02-12 14:50:58 +11:00
Lea Anthony
61afe711bd v2.0.0-alpha.26 2021-02-11 07:17:28 +11:00
Lea Anthony
6e3cfc157f Add bridge.js 2021-02-11 07:09:40 +11:00
Lea Anthony
30d762372f @wails/runtime v1.3.9 2021-02-11 07:04:01 +11:00
Lea Anthony
2dbaabb74c Keep package.json files 2021-02-11 07:02:57 +11:00
Lea Anthony
f8bae0430f Add windowDrag shim for bridge 2021-02-11 06:52:07 +11:00
Lea Anthony
21c07497d7 Move bridge to Svelte build. Initial support for trays in dev mode 2021-02-10 22:53:14 +11:00
Lea Anthony
9b9bcd657f v2.0.0-alpha.25 2021-02-09 21:21:51 +11:00
Lea Anthony
02038aa543 Bugfix dialogs 2021-02-09 21:21:31 +11:00
Lea Anthony
9910d1127a v2.0.0-alpha.24 2021-02-09 21:11:11 +11:00
Lea Anthony
21a0245985 Generate bindings in package. Support dialogs in dev mode. 2021-02-09 21:10:06 +11:00
Lea Anthony
e860f3a00e v2.0.0-alpha.23 2021-02-08 08:57:32 +11:00
Lea Anthony
2e480d2c52 Move to Go 1.16 2021-02-08 08:55:18 +11:00
Lea Anthony
2c0f93d647 v2.0.0-alpha.22 2021-02-08 06:52:21 +11:00
Lea Anthony
41f973c7d5 Update runtime package 2021-02-08 06:51:49 +11:00
Lea Anthony
2d1447d558 darwin bridge js 2021-02-08 06:37:57 +11:00
Lea Anthony
7c22cbcf38 Bridge working for calls. Notify TBD 2021-02-08 06:36:13 +11:00
Lea Anthony
e4b03f510b First step to bridge support 2021-02-06 21:50:21 +11:00
Lea Anthony
8dfd206aa9 Updates for using the bridge 2021-02-06 13:36:56 +11:00
Lea Anthony
6dabab1d2e v2.0.0-alpha.21 2021-02-03 07:20:08 +11:00
Lea Anthony
c3bd8b1a85 Remove debug info 2021-02-03 07:15:52 +11:00
Lea Anthony
e1b7332c47 Graceful shutdown 2021-02-03 07:14:44 +11:00
Lea Anthony
5cd08e45f0 v2.0.0-alpha.20 2021-01-31 22:50:01 +11:00
Lea Anthony
c2d03f0e6e Update NSFontWeightRegular to float const 2021-01-31 22:49:29 +11:00
Lea Anthony
d3501f4cb7 v2.0.0-alpha.19 2021-01-31 21:59:16 +11:00
Lea Anthony
ee82cd25b7 Menu Items default to 12 pt on Mac 2021-01-31 21:58:41 +11:00
Lea Anthony
bbb07e17d9 v2.0.0-alpha.18 2021-01-31 21:20:40 +11:00
Lea Anthony
e6b40b55c4 Add nil guard for binding 2021-01-31 21:10:30 +11:00
Lea Anthony
7573f68df3 Add argument guard for methods 2021-01-31 15:35:33 +11:00
Lea Anthony
ceaacc7ba9 Re-added Store.Set() using deep copy 2021-01-30 21:36:59 +11:00
Lea Anthony
0e24f75753 Deep copy initial value in store 2021-01-30 21:29:36 +11:00
Lea Anthony
82b9deeee4 Unexport set 2021-01-30 20:47:10 +11:00
Lea Anthony
cfa40b797f More race condition fixes 2021-01-29 13:03:05 +11:00
Lea Anthony
5aeb68acb7 Fix for calculating new window size 2021-01-28 19:26:26 +11:00
Lea Anthony
b81101414f Support Min/Max Size in Window runtime 2021-01-28 18:48:07 +11:00
Lea Anthony
7ae89d04bb Fixed data race in store. Handle nils better 2021-01-28 07:15:29 +11:00
Lea Anthony
1c566f3802 Fixed data race in servicebus 2021-01-28 06:23:49 +11:00
Lea Anthony
c9c3c9ab90 Don't bind startup/shutdown methods 2021-01-27 21:12:17 +11:00
Lea Anthony
56394ac50e Use mutex when doing a Store resync 2021-01-27 06:03:17 +11:00
Lea Anthony
f7c2f12ab2 Added error handler for dealing with loading user js code 2021-01-26 16:01:06 +11:00
Lea Anthony
a6bb6e0c93 v2.0.0-alpha.17 2021-01-26 12:30:45 +11:00
Lea Anthony
4a5863e6ac Ported update command 2021-01-26 12:30:14 +11:00
Lea Anthony
913fe8d184 v2.0.0-alpha.16 2021-01-26 11:11:23 +11:00
Lea Anthony
4ce8130cdf Replace CreateApp with single Run() method 2021-01-26 11:09:17 +11:00
Lea Anthony
fe87463b78 Move Bind() into app config 2021-01-26 07:04:12 +11:00
Lea Anthony
fe0f0e29e8 Update CreateApp API 2021-01-26 06:45:23 +11:00
Lea Anthony
83d6dac7cf Add appType to builds. Update server code to compile 2021-01-26 06:40:41 +11:00
Lea Anthony
02500e0930 Update vanilla template with new API 2021-01-26 06:40:18 +11:00
Lea Anthony
5e1187f437 Startup/Shutdown hook warnings 2021-01-26 06:39:54 +11:00
Lea Anthony
064ff3b65e Change build wording 2021-01-26 06:38:54 +11:00
Lea Anthony
b5c7019bf0 Fix compile warnings 2021-01-26 06:37:33 +11:00
Lea Anthony
e9d16e77a3 Add support for loglevel flag in debug builds 2021-01-25 21:42:31 +11:00
Lea Anthony
2415d4c531 v2.0.0-alpha.15 2021-01-25 21:21:44 +11:00
Lea Anthony
3f75213ce3 Small linting fixes 2021-01-25 21:05:20 +11:00
Lea Anthony
6120ceabf1 Support Fonts & Colours for menuitems 2021-01-25 21:04:57 +11:00
Lea Anthony
95a95d1750 Ensure store does initial resync 2021-01-25 21:02:36 +11:00
Lea Anthony
d923e84456 v2.0.0-alpha.14 2021-01-23 21:00:04 +11:00
Lea Anthony
343f573e78 Fix menu tooltips 2021-01-23 20:59:02 +11:00
Lea Anthony
c6d87da4f0 v2.0.0-alpha.13 2021-01-23 20:39:43 +11:00
Lea Anthony
a9faebe51a MenuItem image support 2021-01-23 20:32:42 +11:00
Lea Anthony
d436f5d1be v2.0.0-alpha.12 2021-01-23 16:22:00 +11:00
Lea Anthony
f40899821f Support ToolTips 2021-01-23 16:14:48 +11:00
Lea Anthony
2a64ed19a3 v2.0.0-alpha.11 2021-01-22 14:57:50 +11:00
Lea Anthony
47bca0be88 Support updating tray labels in an efficient manner 2021-01-16 11:35:49 +11:00
Lea Anthony
7ac8cc6b8b Add Menu.Merge() to combine 2 menus 2021-01-15 11:53:55 +11:00
Lea Anthony
b435ec1217 v2.0.0-alpha.10 2021-01-14 20:43:26 +11:00
Lea Anthony
688d4fee6a UpdateTrayMenu -> SetTrayMenu (upsert). Support no menus on trays. 2021-01-14 16:13:59 +11:00
Lea Anthony
29ffeaa9f3 Misc tidy up 2021-01-14 13:55:20 +11:00
Lea Anthony
742e4ba2cb Remove WailsInit and WailsShutdown methodsr 2021-01-14 11:07:06 +11:00
Lea Anthony
0a0063de1f bump version 2021-01-14 00:28:15 +11:00
Lea Anthony
1b7d1e61cc v2.0.0-alpha.8 2021-01-13 23:47:15 +11:00
Lea Anthony
15a273458e Ensure build directory exists when building 2021-01-13 23:46:47 +11:00
Lea Anthony
eef8eb756f Bump version 2021-01-13 22:56:09 +11:00
Lea Anthony
e65118e962 Fixed and refactored context menu support 2021-01-13 22:51:44 +11:00
Lea Anthony
de06fc7dcc Remove a ton of unused code 2021-01-13 21:29:20 +11:00
Lea Anthony
a86fbbb440 Removal of menu IDs. WARNING: context menus are currently broken 2021-01-13 21:14:44 +11:00
Lea Anthony
3045ec107f attempt at preventing menu flicker when removing an icon 2021-01-13 20:47:08 +11:00
Lea Anthony
3a9557ad30 Support removal of tray icons 2021-01-13 20:38:46 +11:00
Lea Anthony
583153383a Refactor, tidy up and trim the fat! 2021-01-13 20:19:01 +11:00
Lea Anthony
3f53e8fd5f Refactor, tidy up and trim the fat! 2021-01-13 11:47:54 +11:00
Lea Anthony
5c9402323a Initial support for multiple traymenus 2021-01-13 10:28:32 +11:00
Lea Anthony
1921862b53 Partially introduce context menu changes back 2021-01-12 21:20:08 +11:00
Lea Anthony
0f7acd39fc [WIP] Fix the madness 2021-01-12 20:39:19 +11:00
Lea Anthony
1a7507f524 [WIP] Add tray menu store + refactor 2021-01-12 16:55:30 +11:00
Lea Anthony
db6dde3e50 [WIP] Support tray in menu manager 2021-01-12 15:55:28 +11:00
Lea Anthony
4e58b7697a Add context menu to menumanager. Slight refactor. 2021-01-11 14:19:01 +11:00
Lea Anthony
55d7d9693f Removed Menu GetByID / RemoveByID 2021-01-11 13:41:07 +11:00
Lea Anthony
b4b7c9d306 Implemented MenuItem.Remove() to remove from the menu structures 2021-01-11 11:57:49 +11:00
Lea Anthony
a4153fae57 Remove Menu.On() 2021-01-11 11:25:48 +11:00
Lea Anthony
8053357d99 Support Menu updates! 2021-01-11 11:21:28 +11:00
Lea Anthony
7347d2caa2 Add wails debug command 2021-01-11 11:20:25 +11:00
Lea Anthony
e6491bcbb7 Fix bad deallocation of context menus 2021-01-11 07:03:59 +11:00
Lea Anthony
26a291dbee [WIP] Use json payload for click message 2021-01-11 06:40:00 +11:00
Lea Anthony
8ee8c9b07c [WIP] New menu processor 2021-01-10 21:31:13 +11:00
Lea Anthony
3a2d01813a Don't inline functions in debug builds 2021-01-10 20:32:25 +11:00
Lea Anthony
d2dadc386f Add combo modifier to kitchen sink 2021-01-08 11:39:18 +11:00
Lea Anthony
faa8f02b08 Bugfix for message dialog icons 2021-01-08 11:39:02 +11:00
Lea Anthony
fbee9ba240 Support UpdateContextMenus. Submenus are now *menu.Menu. Tidy up++ 2021-01-08 06:28:51 +11:00
Lea Anthony
2a69786d7e Remove old code (woohoo!) 2021-01-07 21:36:39 +11:00
Lea Anthony
f460bf91ef [WIP] Normalisation of context menus creation/callbacks.
TODO: UpdateContextMenu()
2021-01-07 21:34:04 +11:00
Lea Anthony
bd74d45a91 Normalisation of callbacks for menus. App menu converted to new Menus. 2021-01-06 20:50:41 +11:00
Lea Anthony
c65522f0b6 Huge refactor of menus. Start of normalisation of callbacks. 2021-01-06 17:36:59 +11:00
Lea Anthony
5f2c437136 Bugfix dealloc contextmenus. Create common.h. WIP: new menu handling 2021-01-06 12:53:11 +11:00
Lea Anthony
87e974e080 Refactor/Tidy up Ffenestri darwin 2021-01-06 11:56:01 +11:00
153 changed files with 10823 additions and 3572 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,33 +5,42 @@ import (
"io"
"os"
"os/signal"
"path/filepath"
"runtime"
"strings"
"syscall"
"time"
"github.com/wailsapp/wails/v2/internal/colour"
"github.com/fsnotify/fsnotify"
"github.com/leaanthony/clir"
"github.com/leaanthony/slicer"
"github.com/wailsapp/wails/v2/internal/fs"
"github.com/wailsapp/wails/v2/internal/process"
"github.com/wailsapp/wails/v2/pkg/clilogger"
"github.com/wailsapp/wails/v2/pkg/commands/build"
)
func LogGreen(message string, args ...interface{}) {
text := fmt.Sprintf(message, args...)
println(colour.Green(text))
}
func LogRed(message string, args ...interface{}) {
text := fmt.Sprintf(message, args...)
println(colour.Red(text))
}
func LogDarkYellow(message string, args ...interface{}) {
text := fmt.Sprintf(message, args...)
println(colour.DarkYellow(text))
}
// AddSubcommand adds the `dev` command for the Wails application
func AddSubcommand(app *clir.Cli, w io.Writer) error {
command := app.NewSubCommand("dev", "Development mode")
outputType := "desktop"
validTargetTypes := slicer.String([]string{"desktop", "hybrid", "server"})
// Setup target type flag
description := "Type of application to develop. Valid types: " + validTargetTypes.Join(",")
command.StringFlag("t", description, &outputType)
// Passthrough ldflags
ldflags := ""
command.StringFlag("ldflags", "optional ldflags", &ldflags)
@@ -42,15 +51,17 @@ 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)
// extensions to trigger rebuilds
showWarnings := false
command.BoolFlag("w", "Show warnings", &showWarnings)
loglevel := ""
command.StringFlag("loglevel", "Loglevel to use - Trace, Debug, Info, Warning, Error", &loglevel)
command.Action(func() error {
// 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 +75,6 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
defer watcher.Close()
var debugBinaryProcess *process.Process = nil
var buildFrontend bool = true
var extensionsThatTriggerARebuild = strings.Split(extensions, ",")
// Setup signal handler
@@ -75,8 +85,13 @@ 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)
newProcess, err := restartApp(logger, "dev", ldflags, compilerCommand, debugBinaryProcess, loglevel)
if newProcess != nil {
debugBinaryProcess = newProcess
}
if err != nil {
return err
}
go debounce(100*time.Millisecond, watcher.Events, debounceQuit, func(event fsnotify.Event) {
// logger.Println("event: %+v", event)
@@ -85,8 +100,11 @@ 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)
logger.Println("Watching directory: %s", event.Name)
err := watcher.Add(event.Name)
if err != nil {
logger.Fatal("%s", err.Error())
}
LogGreen("[New Directory] Watching new directory: %s", event.Name)
}
}
return
@@ -95,38 +113,33 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
// Check for file writes
if event.Op&fsnotify.Write == fsnotify.Write {
// logger.Println("modified file: %s", event.Name)
var rebuild bool = false
// Iterate all file patterns
for _, pattern := range extensionsThatTriggerARebuild {
rebuild = strings.HasSuffix(event.Name, pattern)
if err != nil {
logger.Fatal(err.Error())
}
if rebuild {
// Only build frontend when the file isn't a Go file
buildFrontend = !strings.HasSuffix(event.Name, "go")
if strings.HasSuffix(event.Name, pattern) {
rebuild = true
break
}
}
if !rebuild {
logger.Println("Filename change: %s did not match extension list %s", event.Name, extensions)
if showWarnings {
LogDarkYellow("[File change] %s did not match extension list (%s)", event.Name, extensions)
}
return
}
if buildFrontend {
logger.Println("Full rebuild triggered: %s updated", event.Name)
} else {
logger.Println("Partial build triggered: %s updated", event.Name)
}
LogGreen("[Attempting rebuild] %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, loglevel)
if err != nil {
fmt.Printf("Error during build: %s", err.Error())
return
}
// If we have a new process, save it
if newBinaryProcess != nil {
debugBinaryProcess = newBinaryProcess
@@ -136,23 +149,28 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
})
// Get project dir
dir, err := os.Getwd()
projectDir, err := os.Getwd()
if err != nil {
return err
}
// Get all subdirectories
dirs, err := fs.GetSubdirectories(dir)
dirs, err := fs.GetSubdirectories(projectDir)
if err != nil {
return err
}
LogGreen("Watching (sub)/directory: %s", projectDir)
// Setup a watcher for non-node_modules directories
dirs.Each(func(dir string) {
if strings.Contains(dir, "node_modules") {
return
}
logger.Println("Watching directory: %s", dir)
// Ignore build directory
if strings.HasPrefix(dir, filepath.Join(projectDir, "build")) {
return
}
err = watcher.Add(dir)
if err != nil {
logger.Fatal(err.Error())
@@ -164,7 +182,7 @@ func AddSubcommand(app *clir.Cli, w io.Writer) error {
for quit == false {
select {
case <-quitChannel:
println()
LogGreen("\nCaught quit")
// Notify debouncer to quit
debounceQuit <- true
quit = true
@@ -173,10 +191,13 @@ 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")
LogGreen("Development mode exited")
return nil
})
@@ -203,15 +224,15 @@ 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, loglevel string) (*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
LogRed("Build error - continuing to run current version")
LogDarkYellow(err.Error())
return nil, nil
}
logger.Println("Build new binary: %s", appBinary)
// Kill existing binary if need be
if debugBinaryProcess != nil {
@@ -227,21 +248,24 @@ func restartApp(logger *clilogger.CLILogger, outputType string, ldflags string,
// TODO: Generate `backend.js`
// Start up new binary
newProcess := process.NewProcess(logger, appBinary)
newProcess := process.NewProcess(logger, appBinary, "-loglevel", loglevel)
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())
outputFile := fmt.Sprintf("dev-%d", time.Now().Unix())
// Create BuildOptions
buildOptions := &build.Options{
@@ -253,7 +277,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)

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

View File

@@ -1,10 +1,16 @@
package main
import (
"fmt"
"os"
"github.com/wailsapp/wails/v2/internal/colour"
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/update"
"github.com/leaanthony/clir"
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/build"
"github.com/wailsapp/wails/v2/cmd/wails/internal/commands/debug"
"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"
@@ -16,17 +22,28 @@ func fatal(message string) {
os.Exit(1)
}
func banner(_ *clir.Cli) string {
return fmt.Sprintf("%s %s - Go/HTML Application Framework", colour.Yellow("Wails"), colour.DarkRed(version))
}
func main() {
var err error
app := clir.NewCli("Wails", "Go/HTML Application Framework", version)
app.SetBannerFunction(banner)
build.AddBuildSubcommand(app, os.Stdout)
err = initialise.AddSubcommand(app, os.Stdout)
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())
@@ -42,6 +59,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())

View File

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

View File

@@ -1,11 +1,13 @@
module github.com/wailsapp/wails/v2
go 1.15
go 1.16
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
@@ -13,16 +15,15 @@ require (
github.com/leaanthony/slicer v1.5.0
github.com/matryer/is v1.4.0
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/olekukonko/tablewriter v0.0.4
github.com/pkg/errors v0.9.1
github.com/tdewolff/minify v2.3.6+incompatible
github.com/tdewolff/parse v2.3.4+incompatible // indirect
github.com/tdewolff/test v1.0.6 // indirect
github.com/wzshiming/ctc v1.2.3
github.com/xyproto/xpm v1.2.1
golang.org/x/net v0.0.0-20200822124328-c89045814202
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c
golang.org/x/tools v0.0.0-20200902012652-d1954cc86c82
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
nhooyr.io/websocket v1.8.6
)

View File

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

View File

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

View File

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

View File

@@ -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,16 @@ 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
url *subsystem.URL
dispatcher *messagedispatcher.Dispatcher
menuManager *menumanager.Manager
// Indicates if the app is in debug mode
debug bool
@@ -42,6 +49,10 @@ type App struct {
// Application Stores
loglevelStore *runtime.Store
appconfigStore *runtime.Store
// Startup/Shutdown
startupCallback func(*runtime.Runtime)
shutdownCallback func()
}
// Create App
@@ -54,13 +65,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,13 +113,10 @@ func (a *App) Run() error {
var err error
// Setup signal handler
signalsubsystem, err := signal.NewManager(a.servicebus, a.logger)
if err != nil {
return err
}
a.signal = signalsubsystem
a.signal.Start()
// Setup a context
var subsystemWaitGroup sync.WaitGroup
parentContext := context.WithValue(context.Background(), "waitgroup", &subsystemWaitGroup)
ctx, cancel := context.WithCancel(parentContext)
// Start the service bus
a.servicebus.Debug()
@@ -92,12 +125,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)
if err != nil {
return err
}
@@ -111,17 +139,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 {
@@ -144,62 +161,47 @@ func (a *App) Run() error {
return err
}
if a.options.Mac.URLHandlers != nil {
// Start the url handler subsystem
url, err := subsystem.NewURL(a.servicebus, a.logger, a.options.Mac.URLHandlers)
if err != nil {
return err
}
a.url = url
err = a.url.Start()
if err != nil {
return err
}
}
// Start the eventing subsystem
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 +213,42 @@ func (a *App) Run() error {
return err
}
result := a.window.Run(dispatcher, bindingDump, a.debug)
// Setup signal handler
signalsubsystem, err := signal.NewManager(ctx, cancel, a.servicebus, a.logger)
if err != nil {
return err
}
a.signal = signalsubsystem
a.signal.Start()
err = a.window.Run(dispatcher, bindingDump, a.debug)
a.logger.Trace("Ffenestri.Run() exited")
if err != nil {
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())
// Shutdown callback
if a.shutdownCallback != nil {
a.shutdownCallback()
}
return nil
}

244
v2/internal/app/dev.go Normal file
View File

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

View File

@@ -75,7 +75,7 @@ func CreateApp(options *Options) *App {
webserver: webserver.NewWebServer(myLogger),
servicebus: servicebus.New(myLogger),
logger: myLogger,
bindings: binding.NewBindings(myLogger),
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())
}
}

View File

@@ -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())
}
}

View File

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

View File

@@ -2,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
}

View File

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

View File

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

View File

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

View File

@@ -23,7 +23,7 @@ func isStruct(value interface{}) bool {
return reflect.ValueOf(value).Kind() == reflect.Struct
}
func 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,

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View 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)
}
}

File diff suppressed because it is too large Load Diff

View 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;
}

View 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

View 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);
}

View 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

View 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);
}

View 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

View File

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

View File

@@ -2,41 +2,44 @@
#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 DeleteTrayMenuByID(struct Application*, const char *id);
extern void UpdateTrayMenuLabel(struct Application*, const char* JSON);
extern void AddContextMenu(struct Application*, char *contextMenuJSON);
extern void UpdateContextMenu(struct Application*, char *contextMenuJSON);
#endif

View File

@@ -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,22 @@ 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) UpdateContextMenu(contextMenuJSON string) {
C.UpdateContextMenu(c.app.app, c.app.string2CString(contextMenuJSON))
}
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) DeleteTrayMenuByID(id string) {
C.DeleteTrayMenuByID(c.app.app, c.app.string2CString(id))
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,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 {
@@ -52,6 +48,9 @@ func (a *Application) processPlatformSettings() error {
C.SetAppearance(a.app, a.string2CString(string(mac.Appearance)))
}
// Set activation policy
C.SetActivationPolicy(a.app, C.int(mac.ActivationPolicy))
// Check if the webview should be transparent
if mac.WebviewIsTransparent {
C.WebviewIsTransparent(a.app)
@@ -63,54 +62,37 @@ 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)))
}
// Process URL Handlers
if a.config.Mac.URLHandlers != nil {
C.HasURLHandlers(a.app)
}
return nil

View File

@@ -2,18 +2,126 @@
#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 kInternetEventClass 'GURL'
#define kAEGetURL 'GURL'
#define keyDirectObject '----'
#define c(str) (id)objc_getClass(str)
#define s(str) sel_registerName(str)
#define u(str) sel_getUid(str)
#define str(input) 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
#define NSApplicationActivationPolicyRegular 0
#define NSApplicationActivationPolicyAccessory 1
#define NSApplicationActivationPolicyProhibited 2
// Unbelievably, if the user swaps their button preference
// then right buttons are reported as left buttons
#define NSEventMaskLeftMouseDown 1 << 1
#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 SetActivationPolicy(struct Application* app, int policy);
void* lookupStringConstant(id constantName);
void HasURLHandlers(struct Application* app);
#endif

View File

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

View File

@@ -0,0 +1,907 @@
//
// 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, "ctrl") ) {
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, bool templateImage, bool alternate) {
id item = ALLOC("NSMenuItem");
// Create a MenuItemCallbackData
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, item, menuid, Text);
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), callback);
msg(item, s("setRepresentedObject:"), wrappedId);
if( !alternate ) {
id key = processAcceleratorKey(acceleratorkey);
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title),
s("menuItemCallback:"), key);
} else {
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuItemCallback:"), str(""));
}
if( tooltip != NULL ) {
msg(item, s("setToolTip:"), str(tooltip));
}
// Process image
if( image != NULL && strlen(image) > 0) {
id data = ALLOC("NSData");
id imageData = msg(data, s("initWithBase64EncodedString:options:"), str(image), 0);
id nsimage = ALLOC("NSImage");
msg(nsimage, s("initWithData:"), imageData);
if( templateImage ) {
msg(nsimage, s("template"), YES);
}
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 && !alternate) {
unsigned long modifierFlags = parseModifiers(modifiers);
msg(item, s("setKeyEquivalentModifierMask:"), modifierFlags);
}
// alternate
if( alternate ) {
msg(item, s("setAlternate:"), true);
msg(item, s("setKeyEquivalentModifierMask:"), NSEventModifierFlagOption);
}
msg(parentMenu, s("addItem:"), item);
return item;
}
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)";
}
// Is this an alternate menu item?
bool alternate = false;
getJSONBool(item, "MacAlternate", &alternate);
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");
bool templateImage = false;
getJSONBool(item, "MacTemplateImage", &templateImage);
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, templateImage, alternate);
}
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;
}

View 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, bool templateImage, bool alternate);
void processMenuItem(Menu *menu, id parentMenu, JsonNode *item);
void processMenuData(Menu *menu, JsonNode *menuData);
void processRadioGroupJSON(Menu *menu, JsonNode *radioGroup) ;
id GetMenu(Menu *menu);
#endif //ASSETS_C_MENU_DARWIN_H

File diff suppressed because one or more lines are too long

View 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);
}

View 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

View File

@@ -0,0 +1,165 @@
//
// 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!");
}
if (pthread_mutex_init(&result->lock, NULL) != 0) {
printf("\n mutex init has failed\n");
exit(1);
}
return result;
}
int dumpTrayMenu(void *const context, struct hashmap_element_s *const e) {
DumpTrayMenu(e->data);
return 0;
}
void DumpTrayMenuStore(TrayMenuStore* store) {
pthread_mutex_lock(&store->lock);
hashmap_iterate_pairs(&store->trayMenuMap, dumpTrayMenu, NULL);
pthread_mutex_unlock(&store->lock);
}
void AddTrayMenuToStore(TrayMenuStore* store, const char* menuJSON) {
TrayMenu* newMenu = NewTrayMenu(menuJSON);
pthread_mutex_lock(&store->lock);
//TODO: check if there is already an entry for this menu
hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
pthread_mutex_unlock(&store->lock);
}
int showTrayMenu(void *const context, struct hashmap_element_s *const e) {
ShowTrayMenu(e->data);
// 0 to retain element, -1 to delete.
return 0;
}
void ShowTrayMenusInStore(TrayMenuStore* store) {
pthread_mutex_lock(&store->lock);
if( hashmap_num_entries(&store->trayMenuMap) > 0 ) {
hashmap_iterate_pairs(&store->trayMenuMap, showTrayMenu, NULL);
}
pthread_mutex_unlock(&store->lock);
}
int freeTrayMenu(void *const context, struct hashmap_element_s *const e) {
DeleteTrayMenu(e->data);
return -1;
}
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);
pthread_mutex_destroy(&store->lock);
}
TrayMenu* GetTrayMenuFromStore(TrayMenuStore* store, const char* menuID) {
// Get the current menu
pthread_mutex_lock(&store->lock);
TrayMenu* result = hashmap_get(&store->trayMenuMap, menuID, strlen(menuID));
pthread_mutex_unlock(&store->lock);
return result;
}
TrayMenu* MustGetTrayMenuFromStore(TrayMenuStore* store, const char* menuID) {
// Get the current menu
pthread_mutex_lock(&store->lock);
TrayMenu* result = hashmap_get(&store->trayMenuMap, menuID, strlen(menuID));
pthread_mutex_unlock(&store->lock);
if (result == NULL ) {
ABORT("Unable to find TrayMenu with ID '%s' in the TrayMenuStore!", menuID);
}
return result;
}
void DeleteTrayMenuInStore(TrayMenuStore* store, const char* ID) {
TrayMenu *menu = MustGetTrayMenuFromStore(store, ID);
pthread_mutex_lock(&store->lock);
hashmap_remove(&store->trayMenuMap, ID, strlen(ID));
pthread_mutex_unlock(&store->lock);
DeleteTrayMenu(menu);
}
void UpdateTrayMenuLabelInStore(TrayMenuStore* store, const char* JSON) {
// Parse the JSON
JsonNode *parsedUpdate = mustParseJSON(JSON);
// 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
pthread_mutex_lock(&store->lock);
hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
pthread_mutex_unlock(&store->lock);
// Show it
ShowTrayMenu(newMenu);
return;
}
// DumpTrayMenu(currentMenu);
// Save the status bar reference
newMenu->statusbaritem = currentMenu->statusbaritem;
pthread_mutex_lock(&store->lock);
hashmap_remove(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID));
pthread_mutex_unlock(&store->lock);
// Delete the current menu
DeleteMenu(currentMenu->menu);
currentMenu->menu = NULL;
// Free the tray menu memory
MEMFREE(currentMenu);
pthread_mutex_lock(&store->lock);
hashmap_put(&store->trayMenuMap, newMenu->ID, strlen(newMenu->ID), newMenu);
pthread_mutex_unlock(&store->lock);
// Show the updated menu
ShowTrayMenu(newMenu);
}

View File

@@ -0,0 +1,32 @@
//
// Created by Lea Anthony on 7/1/21.
//
#ifndef TRAYMENUSTORE_DARWIN_H
#define TRAYMENUSTORE_DARWIN_H
#include <pthread.h>
typedef struct {
int dummy;
// This is our tray menu map
// It maps tray IDs to TrayMenu*
struct hashmap_s trayMenuMap;
pthread_mutex_t lock;
} 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);
void DeleteTrayMenuInStore(TrayMenuStore* store, const char* id);
#endif //TRAYMENUSTORE_DARWIN_H

View File

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

View File

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

View 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]
}

View 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
}

View 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()
}

View 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]
}

View 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
}

View File

@@ -0,0 +1,174 @@
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"`
MacTemplateImage bool `json:", omitempty"`
MacAlternate bool `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,
MacTemplateImage: menuItem.MacTemplateImage,
MacAlternate: menuItem.MacAlternate,
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{}
}
}

View File

@@ -0,0 +1,145 @@
package menumanager
import (
"encoding/json"
"fmt"
"strconv"
"sync"
"github.com/pkg/errors"
"github.com/wailsapp/wails/v2/pkg/menu"
)
var trayMenuID int
var trayMenuIDMutex sync.Mutex
func generateTrayID() string {
var idStr string
trayMenuIDMutex.Lock()
idStr = strconv.Itoa(trayMenuID)
trayMenuID++
trayMenuIDMutex.Unlock()
return idStr
}
type TrayMenu struct {
ID string
Label string
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()
}
func (m *Manager) GetTrayID(trayMenu *menu.TrayMenu) (string, error) {
trayID, exists := m.trayMenuPointers[trayMenu]
if !exists {
return "", fmt.Errorf("Unable to find menu ID for tray menu!")
}
return trayID, nil
}
// SetTrayMenu updates or creates a menu
func (m *Manager) SetTrayMenu(trayMenu *menu.TrayMenu) (string, error) {
trayID, trayMenuKnown := m.trayMenuPointers[trayMenu]
if !trayMenuKnown {
return m.AddTrayMenu(trayMenu)
}
// 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
}

View File

@@ -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,17 @@ 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)
DeleteTrayMenuByID(id string)
}
// DispatchClient is what the frontends use to interface with the

View File

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

View File

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

View File

@@ -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()")
}

View File

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

View File

@@ -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,68 @@ 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)
}
case "deletetraymenu":
traymenuid, ok := result.Data().(string)
if !ok {
d.logger.Error("Invalid data for 'menufrontend:updatetraymenulabel' : %#v",
result.Data())
return
}
for _, client := range d.clients {
client.frontend.DeleteTrayMenuByID(traymenuid)
}
default:
d.logger.Error("Unknown menufrontend command: %s", command)
}
}
func (d *Dispatcher) 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()
}

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@@ -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;
// }
// }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1 +1 @@
bridge.js
src

File diff suppressed because it is too large Load Diff

View File

@@ -9,13 +9,25 @@ The lightweight framework for web-like apps
*/
/* jshint esversion: 6 */
import bridge 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
bridge.InitBridge(callback);
}
module.exports = Init;
module.exports = {
ready: ready,
};

View File

@@ -23,7 +23,7 @@ module.exports = {
Browser: Browser,
Dialog: Dialog,
Events: Events,
Init: Init,
ready: Init.ready,
Log: Log,
System: System,
Store: Store,

View File

@@ -1,6 +1,6 @@
{
"name": "@wails/runtime",
"version": "1.2.13",
"version": "1.2.24",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

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

View File

@@ -0,0 +1,56 @@
<script>
export let menu;
export let hidden = true;
</script>
{#if !hidden}
<div class="menu">
{#if menu.Menu }
{#each menu.Menu.Items as menuItem}
<div class="menuitem">
{#if menuItem.Image }
<div><img alt="" src="data:image/png;base64,{menuItem.Image}"/></div>
{/if}
<div class="menulabel">{menuItem.Label}</div>
</div>
{/each}
{/if}
</div>
{/if}
<style>
.menu {
padding: 3px;
background-color: #0008;
color: #EEF;
border-radius: 5px;
margin-top: 5px;
position: absolute;
backdrop-filter: blur(3px) saturate(160%) contrast(45%) brightness(140%);
border: 1px solid rgb(88,88,88);
box-shadow: 0 0 1px rgb(146,146,148) inset;
}
.menuitem {
display: flex;
align-items: center;
padding: 1px 5px;
}
.menuitem:hover {
display: flex;
align-items: center;
background-color: rgb(57,131,223);
padding: 1px 5px;
border-radius: 5px;
}
.menuitem img {
padding-right: 5px;
}
</style>

View File

@@ -0,0 +1,72 @@
<script>
import {menuVisible} from './store'
import {fade} from 'svelte/transition';
import {trays} from './store'
import TrayMenu from "./TrayMenu.svelte";
import {onMount} from "svelte";
let time = new Date();
$: day = time.toLocaleString("default", { weekday: "short" })
$: dom = time.getDate()
$: mon = time.toLocaleString("default", { month: "short" })
$: currentTime = time.toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true }).toLowerCase()
$: dateTimeString = `${day} ${dom} ${mon} ${currentTime}`
onMount(() => {
const interval = setInterval(() => {
time = new Date();
}, 1000);
return () => {
clearInterval(interval);
};
});
function handleKeydown(e) {
// Backtick toggle
if( e.keyCode == 192 ) {
menuVisible.update( (current) => {
return !current;
});
}
}
</script>
{#if $menuVisible }
<div class="wails-menubar" transition:fade>
<span class="tray-menus">
{#each $trays as tray}
<TrayMenu {tray}/>
{/each}
<span class="time">{dateTimeString}</span>
</span>
</div>
{/if}
<svelte:window on:keydown={handleKeydown}/>
<style>
.tray-menus {
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.wails-menubar { position: relative;
display: block;
top: 0;
height: 2rem;
width: 100%;
border-bottom: 1px solid #b3b3b3;
box-shadow: 0 0 10px 0 #33333360;
}
.time {
padding-left: 0.5rem;
padding-right: 1.5rem;
overflow: visible;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,55 @@
<script>
import { overlayVisible } from './store'
import { fade, } from 'svelte/transition';
</script>
{#if $overlayVisible }
<div class="wails-reconnect-overlay" transition:fade="{{ duration: 200 }}">
<div class="wails-reconnect-overlay-content">
<div class="wails-reconnect-overlay-loadingspinner"></div>
</div>
</div>
{/if}
<style>
.wails-reconnect-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
backdrop-filter: blur(20px) saturate(160%) contrast(45%) brightness(140%);
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);
}
}
</style>

View File

@@ -0,0 +1,65 @@
<script>
import Menu from "./Menu.svelte";
import { selectedMenu } from "./store";
export let tray = null;
$: hidden = $selectedMenu !== tray;
function closeMenu() {
selectedMenu.set(null);
}
function trayClicked() {
if ( $selectedMenu !== tray ) {
selectedMenu.set(tray);
} else {
selectedMenu.set(null);
}
}
// Source: https://svelte.dev/repl/0ace7a508bd843b798ae599940a91783?version=3.16.7
/** Dispatch event on click outside of node */
function clickOutside(node) {
const handleClick = event => {
if (node && !node.contains(event.target) && !event.defaultPrevented) {
node.dispatchEvent(
new CustomEvent('click_outside', node)
)
}
}
document.addEventListener('click', handleClick, true);
return {
destroy() {
document.removeEventListener('click', handleClick, true);
}
}
}
</script>
<span class="tray-menu" use:clickOutside on:click_outside={closeMenu}>
<!--{#if tray.Image && tray.Image.length > 0}-->
<!-- <img alt="" src="data:image/png;base64,{tray.Image}"/>-->
<!--{/if}-->
<span class="label" on:click={trayClicked}>{tray.Label}</span>
{#if tray.ProcessedMenu }
<Menu menu="{tray.ProcessedMenu}" {hidden}/>
{/if}
</span>
<style>
.tray-menu {
padding-left: 0.5rem;
padding-right: 0.5rem;
overflow: visible;
font-size: 14px;
}
.label {
text-align: right;
padding-right: 10px;
}
</style>

View File

@@ -0,0 +1,9 @@
export function log(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'
);
}

View File

@@ -0,0 +1,45 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The lightweight framework for web-like apps
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
import Overlay from './Overlay.svelte';
import MenuBar from './Menubar.svelte';
import {showOverlay} from "./store";
import {StartWebsocket} from "./websocket";
let components = {};
function setupMenuBar() {
components.menubar = new MenuBar({
target: document.body,
});
}
// Sets up the overlay
function setupOverlay() {
components.overlay = new Overlay({
target: document.body,
anchor: document.querySelector('#wails-bridge'),
});
}
export function InitBridge(callback) {
setupMenuBar()
// Setup the overlay
setupOverlay();
// Start by showing the overlay...
showOverlay();
// ...and attempt to connect
StartWebsocket(callback);
}

View File

@@ -0,0 +1,525 @@
{
"name": "bridge",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@babel/code-frame": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz",
"integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==",
"dev": true,
"requires": {
"@babel/highlight": "^7.12.13"
}
},
"@babel/helper-validator-identifier": {
"version": "7.12.11",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz",
"integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==",
"dev": true
},
"@babel/highlight": {
"version": "7.12.13",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.12.13.tgz",
"integrity": "sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.12.11",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
}
},
"@rollup/plugin-commonjs": {
"version": "17.1.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-17.1.0.tgz",
"integrity": "sha512-PoMdXCw0ZyvjpCMT5aV4nkL0QywxP29sODQsSGeDpr/oI49Qq9tRtAsb/LbYbDzFlOydVEqHmmZWFtXJEAX9ew==",
"dev": true,
"requires": {
"@rollup/pluginutils": "^3.1.0",
"commondir": "^1.0.1",
"estree-walker": "^2.0.1",
"glob": "^7.1.6",
"is-reference": "^1.2.1",
"magic-string": "^0.25.7",
"resolve": "^1.17.0"
},
"dependencies": {
"estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true
}
}
},
"@rollup/plugin-node-resolve": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.1.1.tgz",
"integrity": "sha512-zlBXR4eRS+2m79TsUZWhsd0slrHUYdRx4JF+aVQm+MI0wsKdlpC2vlDVjmlGvtZY1vsefOT9w3JxvmWSBei+Lg==",
"dev": true,
"requires": {
"@rollup/pluginutils": "^3.1.0",
"@types/resolve": "1.17.1",
"builtin-modules": "^3.1.0",
"deepmerge": "^4.2.2",
"is-module": "^1.0.0",
"resolve": "^1.19.0"
}
},
"@rollup/pluginutils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz",
"integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==",
"dev": true,
"requires": {
"@types/estree": "0.0.39",
"estree-walker": "^1.0.1",
"picomatch": "^2.2.2"
}
},
"@types/estree": {
"version": "0.0.39",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
"integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
"dev": true
},
"@types/node": {
"version": "14.14.25",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.25.tgz",
"integrity": "sha512-EPpXLOVqDvisVxtlbvzfyqSsFeQxltFbluZNRndIb8tr9KiBnYNLzrc1N3pyKUCww2RNrfHDViqDWWE1LCJQtQ==",
"dev": true
},
"@types/resolve": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
"integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
"dev": true
},
"builtin-modules": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz",
"integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==",
"dev": true
},
"chalk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
"commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
"dev": true
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
},
"deepmerge": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
"dev": true
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"dev": true
},
"estree-walker": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
"integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
"dev": true
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"requires": {
"function-bind": "^1.1.1"
}
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"is-core-module": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz",
"integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==",
"dev": true,
"requires": {
"has": "^1.0.3"
}
},
"is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
"dev": true
},
"is-reference": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
"integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
"dev": true,
"requires": {
"@types/estree": "*"
}
},
"jest-worker": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz",
"integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==",
"dev": true,
"requires": {
"@types/node": "*",
"merge-stream": "^2.0.0",
"supports-color": "^7.0.0"
},
"dependencies": {
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
},
"magic-string": {
"version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
"integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
"dev": true,
"requires": {
"sourcemap-codec": "^1.4.4"
}
},
"merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
"dev": true
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1"
}
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
},
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
},
"picomatch": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
"integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
"dev": true
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
"dev": true,
"requires": {
"safe-buffer": "^5.1.0"
}
},
"require-relative": {
"version": "0.8.7",
"resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz",
"integrity": "sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=",
"dev": true
},
"resolve": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz",
"integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==",
"dev": true,
"requires": {
"is-core-module": "^2.1.0",
"path-parse": "^1.0.6"
}
},
"rollup": {
"version": "2.38.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.38.5.tgz",
"integrity": "sha512-VoWt8DysFGDVRGWuHTqZzT02J0ASgjVq/hPs9QcBOGMd7B+jfTr/iqMVEyOi901rE3xq+Deq66GzIT1yt7sGwQ==",
"dev": true,
"requires": {
"fsevents": "~2.3.1"
}
},
"rollup-plugin-svelte": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-svelte/-/rollup-plugin-svelte-7.1.0.tgz",
"integrity": "sha512-vopCUq3G+25sKjwF5VilIbiY6KCuMNHP1PFvx2Vr3REBNMDllKHFZN2B9jwwC+MqNc3UPKkjXnceLPEjTjXGXg==",
"dev": true,
"requires": {
"require-relative": "^0.8.7",
"rollup-pluginutils": "^2.8.2"
}
},
"rollup-plugin-terser": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz",
"integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.10.4",
"jest-worker": "^26.2.1",
"serialize-javascript": "^4.0.0",
"terser": "^5.0.0"
}
},
"rollup-pluginutils": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz",
"integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==",
"dev": true,
"requires": {
"estree-walker": "^0.6.1"
},
"dependencies": {
"estree-walker": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
"integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
"dev": true
}
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true
},
"serialize-javascript": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz",
"integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==",
"dev": true,
"requires": {
"randombytes": "^2.1.0"
}
},
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"dev": true
},
"source-map-support": {
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
"integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
"dev": true,
"requires": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
},
"dependencies": {
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
}
}
},
"sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
},
"svelte": {
"version": "3.32.2",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.32.2.tgz",
"integrity": "sha512-Zxh1MQQl/+vnToKbU1Per+PoMN8Jb2MeKJcGxiOsCGR677hXw7jkMfbnNXq33+dxIzV/HfA4xtoSPJrqeB0VUg==",
"dev": true
},
"terser": {
"version": "5.5.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.5.1.tgz",
"integrity": "sha512-6VGWZNVP2KTUcltUQJ25TtNjx/XgdDsBDKGt8nN0MpydU36LmbPPcMBd2kmtZNNGVVDLg44k7GKeHHj+4zPIBQ==",
"dev": true,
"requires": {
"commander": "^2.20.0",
"source-map": "~0.7.2",
"source-map-support": "~0.5.19"
}
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
}
}
}

View File

@@ -0,0 +1,21 @@
{
"name": "bridge",
"version": "1.0.0",
"description": "Wails bridge",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "npx rollup -c",
"watch": "npx rollup -c -w"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@rollup/plugin-commonjs": "^17.1.0",
"@rollup/plugin-node-resolve": "^11.1.1",
"rollup": "^2.38.5",
"rollup-plugin-svelte": "^7.1.0",
"rollup-plugin-terser": "^7.0.2",
"svelte": "^3.32.2"
}
}

View File

@@ -0,0 +1,37 @@
import resolve from '@rollup/plugin-node-resolve';
// import commonjs from '@rollup/plugin-commonjs';
import svelte from 'rollup-plugin-svelte';
import { terser } from "rollup-plugin-terser";
export default [
// browser-friendly UMD build
{
input: 'main.js',
output: {
name: 'bridge',
file: '../bridge.js',
format: 'umd',
exports: "named"
},
plugins: [
svelte({
// Optionally, preprocess components with svelte.preprocess:
// https://svelte.dev/docs#svelte_preprocess
// preprocess: {
// style: ({content}) => {
// return transformStyles(content);
// }
// },
// Emit CSS as "files" for other plugins to process. default is true
emitCss: false,
}),
resolve({browser: true}), // so Rollup can find `ms`
// commonjs() // so Rollup can convert `ms` to an ES module
// terser(),
]
},
];

View File

@@ -0,0 +1,64 @@
import { writable } from 'svelte/store';
import {log} from "./log";
/** Overlay */
export const overlayVisible = writable(false);
export function showOverlay() {
overlayVisible.set(true);
}
export function hideOverlay() {
overlayVisible.set(false);
}
/** Menubar **/
export const menuVisible = writable(false);
export function showMenuBar() {
menuVisible.set(true);
}
export function hideMenuBar() {
menuVisible.set(false);
}
/** Trays **/
export const trays = writable([]);
export function setTray(tray) {
trays.update((current) => {
// Remove existing if it exists, else add
const index = current.findIndex(item => item.ID === tray.ID);
if ( index === -1 ) {
current.push(tray);
} else {
current[index] = tray;
}
return current;
})
}
export function updateTrayLabel(tray) {
trays.update((current) => {
// Remove existing if it exists, else add
const index = current.findIndex(item => item.ID === tray.ID);
if ( index === -1 ) {
return log("ERROR: Attempted to update tray index ", tray.ID, "but it doesn't exist")
}
current[index].Label = tray.Label;
return current;
})
}
export function deleteTrayMenu(id) {
trays.update((current) => {
// Remove existing if it exists, else add
const index = current.findIndex(item => item.ID === id);
if ( index === -1 ) {
return log("ERROR: Attempted to delete tray index ", id, "but it doesn't exist")
}
current.splice(index, 1);
return current;
})
}
export let selectedMenu = writable(null);

View File

@@ -0,0 +1,170 @@
/*
_ __ _ __
| | / /___ _(_) /____
| | /| / / __ `/ / / ___/
| |/ |/ / /_/ / / (__ )
|__/|__/\__,_/_/_/____/
The lightweight framework for web-like apps
(c) Lea Anthony 2019-present
*/
/* jshint esversion: 6 */
import {setTray, hideOverlay, showOverlay, updateTrayLabel, deleteTrayMenu} from "./store";
import {log} from "./log";
let websocket = null;
let callback = null;
let connectTimer;
export function StartWebsocket(userCallback) {
callback = userCallback;
window.onbeforeunload = function() {
if( websocket ) {
websocket.onclose = function () { };
websocket.close();
websocket = null;
}
}
// ...and attempt to connect
connect();
}
function setupIPCBridge() {
// darwin
window.webkit = {
messageHandlers: {
external: {
postMessage: (message) => {
websocket.send(message);
}
},
windowDrag: {
postMessage: () => {
// Ignore window drag events
}
}
}
};
}
// Handles incoming websocket connections
function handleConnect() {
log('Connected to backend');
setupIPCBridge();
hideOverlay();
clearInterval(connectTimer);
websocket.onclose = handleDisconnect;
websocket.onmessage = handleMessage;
}
// Handles websocket disconnects
function handleDisconnect() {
log('Disconnected from backend');
websocket = null;
showOverlay();
connect();
}
// Try to connect to the backend every 1s (default value).
function connect() {
connectTimer = setInterval(function () {
if (websocket == null) {
websocket = new WebSocket('ws://' + window.location.hostname + ':34115/bridge');
websocket.onopen = handleConnect;
websocket.onerror = function (e) {
e.stopImmediatePropagation();
e.stopPropagation();
e.preventDefault();
websocket = null;
return false;
};
}
}, 1000);
}
// 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);
}
}
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);
log('Loaded Wails Runtime');
// We need to now send a message to the backend telling it
// we have loaded (System Start)
window.webkit.messageHandlers.external.postMessage("SS");
// Now wails runtime is loaded, wails for the ready event
// and callback to the main app
// window.wails.Events.On('wails:loaded', function () {
if (callback) {
log('Notifying application');
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;
// Tray
case 'T':
const trayMessage = message.data.slice(1);
switch (trayMessage[0]) {
case 'S':
// Set tray
const trayJSON = trayMessage.slice(1);
let tray = JSON.parse(trayJSON)
setTray(tray)
break
case 'U':
// Update label
const updateTrayLabelJSON = trayMessage.slice(1);
let trayLabelData = JSON.parse(updateTrayLabelJSON)
updateTrayLabel(trayLabelData)
break
case 'D':
// Delete Tray Menu
const id = trayMessage.slice(1);
deleteTrayMenu(id)
break
default:
log('Unknown tray message: ' + message.data);
}
break;
default:
log('Unknown message: ' + message.data);
}
}

View File

@@ -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,
};

View File

@@ -13,7 +13,7 @@ module.exports = {
mode: 'production',
output: {
path: path.resolve(__dirname, '..', 'assets'),
filename: 'desktop.js',
filename: 'desktop_'+platform+'.js',
library: 'Wails'
},
resolve: {

View File

@@ -1,48 +1,46 @@
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)
DeleteTrayMenu(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)
}
func (m *menuRuntime) DeleteTrayMenu(trayMenu *menu.TrayMenu) {
m.bus.Publish("menu:deletetraymenu", trayMenu)
}

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